diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..87834047 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +20.12.2 diff --git a/package-lock.json b/package-lock.json index 98f1df25..16a5eef6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,74 +37,37 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.5", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -155,9 +118,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", + "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -259,17 +222,17 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.21.tgz", - "integrity": "sha512-/ySDLGscFPNasfqStUuWWPfL78jompfIoVzLJPVVAHBh6rpG68+pI2Gk+fNLeI8/f1yPYL4s46EleVIc20F1Ow==", + "version": "20.12.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz", + "integrity": "sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/qs": { - "version": "6.9.12", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.12.tgz", - "integrity": "sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==" + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" }, "node_modules/abab": { "version": "1.0.4", @@ -438,15 +401,16 @@ } }, "node_modules/array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" }, "engines": { @@ -465,35 +429,17 @@ "node": ">=8" } }, - "node_modules/array.prototype.filter": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz", - "integrity": "sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz", - "integrity": "sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", + "es-abstract": "^1.23.2", "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" }, "engines": { @@ -1156,6 +1102,57 @@ "node": ">= 6" } }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1352,18 +1349,22 @@ "dev": true }, "node_modules/es-abstract": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.4.tgz", - "integrity": "sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.1", "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.6", + "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.2", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", "get-intrinsic": "^1.2.4", @@ -1371,15 +1372,16 @@ "globalthis": "^1.0.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.1", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.1", + "hasown": "^2.0.2", "internal-slot": "^1.0.7", "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", @@ -1387,17 +1389,17 @@ "object-keys": "^1.1.1", "object.assign": "^4.1.5", "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.0", + "safe-array-concat": "^1.1.2", "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.1", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -1406,12 +1408,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "dev": true - }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -1431,6 +1427,18 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", @@ -2470,9 +2478,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -2710,12 +2718,13 @@ } }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -2882,9 +2891,9 @@ } }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -3316,6 +3325,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -4294,28 +4318,29 @@ } }, "node_modules/object.entries": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", - "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -4325,27 +4350,28 @@ } }, "node_modules/object.groupby": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.2.tgz", - "integrity": "sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "dependencies": { - "array.prototype.filter": "^1.0.3", - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.0.0" + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -4539,6 +4565,12 @@ "dev": true, "optional": true }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -4608,11 +4640,11 @@ } }, "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -4907,13 +4939,13 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", - "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -5005,16 +5037,16 @@ "dev": true }, "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.2", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5062,11 +5094,11 @@ } }, "node_modules/side-channel": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", - "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" @@ -5236,14 +5268,15 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -5253,28 +5286,31 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5653,9 +5689,9 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz", - "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, "dependencies": { "call-bind": "^1.0.7", @@ -5811,16 +5847,16 @@ "dev": true }, "node_modules/which-typed-array": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", - "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.6", - "call-bind": "^1.0.5", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.1" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" diff --git a/src/wrappers/CitationWrapper.js b/src/wrappers/CitationWrapper.js index fff84959..7c4293e4 100644 --- a/src/wrappers/CitationWrapper.js +++ b/src/wrappers/CitationWrapper.js @@ -13,6 +13,24 @@ class CitationWrapper { this.citation = citation; } + /** + * Return a normalized form of a citation. + * + * I'm not really sure how to normalize a citation, but the main thing we can do is delete any key + * that is equivalent to ''. We could interconvert between `name` and + * `firstname/lastname/middlename`, but that's not really equivalent, is it? + */ + static normalize(citation) { + const normalizedCitation = {}; + Object.keys(citation).forEach((key) => { + // As long as citation[key] has a reasonable value, we copy it into the normalized citation. + if (citation[key]) { + normalizedCitation[key] = citation[key]; + } + }); + return normalizedCitation; + } + /** * Helper method to return a single name for a given agent entry. * The algorithm we use is: diff --git a/src/wrappers/PhylogenyWrapper.js b/src/wrappers/PhylogenyWrapper.js index fc9cd221..ccfe3340 100644 --- a/src/wrappers/PhylogenyWrapper.js +++ b/src/wrappers/PhylogenyWrapper.js @@ -2,7 +2,10 @@ * PhylogenyWrapper */ -const { has } = require('lodash'); +const { + has, + cloneDeep, +} = require('lodash'); /** Used to parse Newick strings. */ const newickJs = require('newick-js'); @@ -34,6 +37,22 @@ class PhylogenyWrapper { this.defaultNomenCode = defaultNomenCode; } + /** + * Return a normalized form of the phylogeny. + */ + static normalize(phylogeny) { + const normalizedPhylogeny = cloneDeep(phylogeny); + + // We could normalize the Newick string, but that doesn't seem very nice. + + // Normalize the source if there is one. + if ('source' in phylogeny) { + normalizedPhylogeny.source = CitationWrapper.normalize(phylogeny.source || {}); + } + + return normalizedPhylogeny; + } + static getErrorsInNewickString(newick) { // Given a Newick string, return a list of errors found in parsing this // string. The errors are returned as a list of objects, each of which diff --git a/src/wrappers/PhylorefWrapper.js b/src/wrappers/PhylorefWrapper.js index f6e6d180..e287a0c9 100644 --- a/src/wrappers/PhylorefWrapper.js +++ b/src/wrappers/PhylorefWrapper.js @@ -33,6 +33,22 @@ class PhylorefWrapper { return this.phyloref.internalSpecifiers; } + /** + * Normalize a phyloreference. + * + * @param phyloref + */ + static normalize(phyloref) { + const normalizedPhyloref = cloneDeep(phyloref); + + normalizedPhyloref.internalSpecifiers = (phyloref.internalSpecifiers || []) + .map(TaxonomicUnitWrapper.normalize); + normalizedPhyloref.externalSpecifiers = (phyloref.externalSpecifiers || []) + .map(TaxonomicUnitWrapper.normalize); + + return normalizedPhyloref; + } + /** Return the external specifiers of this phyloref (if any). */ get externalSpecifiers() { if (!has(this.phyloref, 'externalSpecifiers')) { diff --git a/src/wrappers/PhyxWrapper.js b/src/wrappers/PhyxWrapper.js index 93097682..7792a9ca 100644 --- a/src/wrappers/PhyxWrapper.js +++ b/src/wrappers/PhyxWrapper.js @@ -51,6 +51,32 @@ class PhyxWrapper { return owlterms.UNKNOWN_CODE; } + /** + * Return a provided Phyx document as a normalized JSON document. We ignore most keys -- including + * keys we don't know -- but any key that can be wrapped by one of the other Wrappers in this + * package will be wrapped and normalized before being returned. + * + * Normalization is mostly needed for TaxonomicUnitWrappers and its subclasses + * (TaxonConceptWrapper, TaxonNameWrapper), since these can be represented in several essentially + * identical ways. But if we implement it at every level, we can implement comparison code in + * Klados easily. + * + * Two Phyx documents should -- upon being normalized -- be comparable with each other with + * lodash.deepEqual(). + */ + static normalize(phyxDocument) { + const normalizedDocument = cloneDeep(phyxDocument); + + normalizedDocument.phylorefs = (phyxDocument.phylorefs || []).map(PhylorefWrapper.normalize); + normalizedDocument.phylogenies = (phyxDocument.phylogenies || []) + .map(PhylogenyWrapper.normalize); + if ('source' in phyxDocument) { + normalizedDocument.source = CitationWrapper.normalize(phyxDocument.source); + } + + return normalizedDocument; + } + /** * Generate an executable ontology from this Phyx document. The document is mostly in JSON-LD * already, except for three important things: diff --git a/src/wrappers/SpecimenWrapper.js b/src/wrappers/SpecimenWrapper.js index 6888a00f..ce8ab8fb 100644 --- a/src/wrappers/SpecimenWrapper.js +++ b/src/wrappers/SpecimenWrapper.js @@ -29,6 +29,25 @@ class SpecimenWrapper { this.specimen = specimen; } + /** + * Normalize the specified specimen. + * @param specimen A specimen to be normalized. + */ + static normalize(specimen) { + const wrapped = new SpecimenWrapper(specimen); + const normalizedSpecimen = { + '@type': SpecimenWrapper.TYPE_SPECIMEN, + label: wrapped.label, + 'dwc:basisOfRecord': wrapped.basisOfRecord, + occurrenceID: wrapped.occurrenceID, + catalogNumber: wrapped.catalogNumber, + institutionCode: wrapped.institutionCode, + collectionCode: wrapped.collectionCode, + }; + if ('@id' in specimen) normalizedSpecimen['@id'] = specimen['@id']; + return normalizedSpecimen; + } + /** * Parse the provided occurrence ID. The two expected formats are: * - 'urn:catalog:[institutionCode]:[collectionCode]:[catalogNumber]' diff --git a/src/wrappers/TaxonConceptWrapper.js b/src/wrappers/TaxonConceptWrapper.js index 9e4f3d19..91b21998 100644 --- a/src/wrappers/TaxonConceptWrapper.js +++ b/src/wrappers/TaxonConceptWrapper.js @@ -34,6 +34,23 @@ class TaxonConceptWrapper { this.defaultNomenCode = defaultNomenCode; } + /** + * Normalize the specified taxon concept. + * @param tc A taxon concept to be normalized. + */ + static normalize(tc) { + const wrapped = new TaxonConceptWrapper(tc); + const normalizedTC = { + '@type': TaxonConceptWrapper.TYPE_TAXON_CONCEPT, + label: wrapped.label, + hasName: TaxonNameWrapper.normalize(wrapped.taxonName), + nameString: wrapped.taxonName.nameComplete, + accordingTo: wrapped.accordingTo, + }; + if ('@id' in tc) normalizedTC['@id'] = tc['@id']; + return normalizedTC; + } + /** * Return the taxon name of this taxon concept (if any) as an object. */ @@ -89,10 +106,10 @@ class TaxonConceptWrapper { */ get accordingTo() { // Do we have any accordingTo information? - if (has(this.tunit, 'accordingTo')) return this.type.accordingTo; + if (has(this.tunit, 'accordingTo')) return this.tunit.accordingTo; // Do we have an accordingToString? - if (has(this.tunit, 'accordingToString')) return this.type.accordingToString; + if (has(this.tunit, 'accordingToString')) return this.tunit.accordingToString; // If not, we have no accodingTo information! return undefined; @@ -106,10 +123,10 @@ class TaxonConceptWrapper { */ get accordingToString() { // Do we have any accordingTo information? - if (has(this.tunit, 'accordingTo')) return JSON.stringify(this.type.accordingTo); + if (has(this.tunit, 'accordingTo')) return JSON.stringify(this.tunit.accordingTo); // Do we have an accordingToString? - if (has(this.tunit, 'accordingToString')) return this.type.accordingToString; + if (has(this.tunit, 'accordingToString')) return this.tunit.accordingToString; // If not, we have no accodingTo information! return undefined; diff --git a/src/wrappers/TaxonNameWrapper.js b/src/wrappers/TaxonNameWrapper.js index e70666fa..8dcfe3fd 100644 --- a/src/wrappers/TaxonNameWrapper.js +++ b/src/wrappers/TaxonNameWrapper.js @@ -143,6 +143,25 @@ class TaxonNameWrapper { return undefined; } + /** + * Normalize the specified taxon name. + * @param txname A taxon name to be normalized. + */ + static normalize(txname) { + const wrapped = new TaxonNameWrapper(txname); + const normalizedTxname = { + '@type': TaxonNameWrapper.TYPE_TAXON_NAME, + nomenclaturalCode: wrapped.nomenclaturalCode, + label: wrapped.label, + nameComplete: wrapped.nameComplete, + genusPart: wrapped.genusPart, + specificEpithet: wrapped.specificEpithet, + infraspecificEpithet: wrapped.infraspecificEpithet, + }; + if ('@id' in txname) normalizedTxname['@id'] = txname['@id']; + return normalizedTxname; + } + /** * Returns the nomenclatural code of this taxon name. */ diff --git a/src/wrappers/TaxonomicUnitWrapper.js b/src/wrappers/TaxonomicUnitWrapper.js index 7c63af18..26c7dcb2 100644 --- a/src/wrappers/TaxonomicUnitWrapper.js +++ b/src/wrappers/TaxonomicUnitWrapper.js @@ -59,6 +59,25 @@ class TaxonomicUnitWrapper { this.defaultNomenCode = defaultNomenCode; } + /** + * Normalize the specified taxonomic unit. + * @param tunit A taxonomic unit to be normalized. + */ + static normalize(tunit) { + const wrapped = new TaxonomicUnitWrapper(tunit); + if (wrapped.taxonConcept) { + return TaxonConceptWrapper.normalize(tunit); + } + if (wrapped.specimen) { + return SpecimenWrapper.normalize(tunit); + } + if (wrapped.externalReferences) { + // External references should only have an `@id`. + return tunit; + } + return tunit; + } + /** * What type of specifier is this? This is an array that could contain multiple * classes, but should contain one of: diff --git a/test/examples/correct/normalization/brochu_2003_normalization.json b/test/examples/correct/normalization/brochu_2003_normalization.json new file mode 100644 index 00000000..f7b5d195 --- /dev/null +++ b/test/examples/correct/normalization/brochu_2003_normalization.json @@ -0,0 +1,293 @@ +{ + "@context": "https://www.phyloref.org/phyx.js/context/v1.0.0/phyx.json", + "doi": "10.5281/zenodo.4562685", + "source": { + "authors": [ + { + "firstname": "Gaurav", + "lastname": "Vaidya" + } + ], + "year": 2021, + "title": "Digital representation of some of the clade definitions in Brochu 2003 in the Phyloreference Exchange (Phyx) format", + "journal": { + "name": "Zenodo" + }, + "identifier": [ + { + "type": "doi", + "id": "10.5281/zenodo.4562685" + } + ] + }, + "phylogenies": [ + { + "newick": "(Parasuchia,(rauisuchians,Aetosauria,(sphenosuchians,(protosuchians,(mesosuchians,(Hylaeochampsa,Aegyptosuchus,Stomatosuchus,(Allodaposuchus,('Gavialis gangeticus',(('Diplocynodon ratelii',('Alligator mississippiensis','Caiman crocodilus')Alligatoridae)Alligatoroidea,('Tomistoma schlegelii',('Osteolaemus tetraspis','Crocodylus niloticus')Crocodylinae)Crocodylidae)Brevirostres)Crocodylia))Eusuchia)Mesoeucrocodylia)Crocodyliformes)Crocodylomorpha))root;", + "label": "Fig 1 from Brochu 2003", + "@id": "#phylogeny0", + "source": { + "type": "article", + "title": "Phylogenetic approaches toward crocodylian history", + "authors": [ + { + "name": "Christopher A. Brochu", + "alternate": [ + "Brochu, Christopher A." + ], + "firstname": "Christopher", + "middlename": "A.", + "lastname": "Brochu" + } + ], + "year": 2003, + "figure": "1", + "identifier": [ + { + "type": "doi", + "id": "10.1146/annurev.earth.31.100901.141308" + } + ], + "link": [ + { + "url": "https://www.annualreviews.org/doi/10.1146/annurev.earth.31.100901.141308" + } + ], + "journal": { + "name": "Annual Review of Earth and Planetary Sciences", + "volume": "31", + "pages": "357--397", + "identifier": [ + { + "type": "eISSN", + "id": "1545-4495" + } + ] + } + } + } + ], + "phylorefs": [ + { + "@id": "#Alligatoridae1_same", + "label": "Alligatoridae", + "scientificNameAuthorship": { + "bibliographicCitation": "(Cuvier 1807)" + }, + "phylorefType": "phyloref:PhyloreferenceUsingMinimumClade", + "definition": "Alligatoridae (Cuvier 1807).\n\nLast common ancestor of Alligator mississippiensis and Caiman crocodilus and all of its descendents.", + "definitionSource": { + "type": "article", + "title": "Phylogenetic approaches toward crocodylian history", + "authors": [ + { + "name": "Christopher A. Brochu", + "alternate": [ + "Brochu, Christopher A." + ], + "firstname": "Christopher", + "middlename": "A.", + "lastname": "Brochu" + } + ], + "year": 2003, + "figure": "1", + "identifier": [ + { + "type": "doi", + "id": "10.1146/annurev.earth.31.100901.141308" + } + ], + "link": [ + { + "url": "https://www.annualreviews.org/doi/10.1146/annurev.earth.31.100901.141308" + } + ], + "journal": { + "name": "Annual Review of Earth and Planetary Sciences", + "volume": "31", + "pages": "357--397", + "identifier": [ + { + "type": "eISSN", + "id": "1545-4495" + } + ] + } + }, + "internalSpecifiers": [ + { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonConcept#TaxonConcept", + "hasName": { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonName#TaxonName", + "nomenclaturalCode": "http://rs.tdwg.org/ontology/voc/TaxonName#ICZN", + "label": "Caiman crocodilus", + "nameComplete": "Caiman crocodilus", + "genusPart": "Caiman", + "specificEpithet": "crocodilus" + } + }, + { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonConcept#TaxonConcept", + "hasName": { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonName#TaxonName", + "nomenclaturalCode": "http://rs.tdwg.org/ontology/voc/TaxonName#ICZN", + "label": "Alligator mississippiensis", + "nameComplete": "Alligator mississippiensis", + "genusPart": "Alligator", + "specificEpithet": "mississippiensis" + }, + "label": "Alligator mississippiensis" + } + ], + "externalSpecifiers": [] + }, + { + "@id": "#Alligatoridae2_same", + "label": "Alligatoridae", + "scientificNameAuthorship": { + "bibliographicCitation": "(Cuvier 1807)" + }, + "phylorefType": "phyloref:PhyloreferenceUsingMinimumClade", + "definition": "Alligatoridae (Cuvier 1807).\n\nLast common ancestor of Alligator mississippiensis and Caiman crocodilus and all of its descendents.", + "definitionSource": { + "type": "article", + "title": "Phylogenetic approaches toward crocodylian history", + "authors": [ + { + "name": "Christopher A. Brochu", + "alternate": [ + "Brochu, Christopher A." + ], + "firstname": "Christopher", + "middlename": "A.", + "lastname": "Brochu" + } + ], + "year": 2003, + "figure": "1", + "identifier": [ + { + "type": "doi", + "id": "10.1146/annurev.earth.31.100901.141308" + } + ], + "link": [ + { + "url": "https://www.annualreviews.org/doi/10.1146/annurev.earth.31.100901.141308" + } + ], + "journal": { + "name": "Annual Review of Earth and Planetary Sciences", + "volume": "31", + "pages": "357--397", + "identifier": [ + { + "type": "eISSN", + "id": "1545-4495" + } + ] + } + }, + "internalSpecifiers": [ + { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonConcept#TaxonConcept", + "hasName": { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonName#TaxonName", + "label": "Caiman crocodilus", + "nameComplete": "Caiman crocodilus", + "genusPart": "Caiman", + "specificEpithet": "crocodilus", + "nomenclaturalCode": "http://rs.tdwg.org/ontology/voc/TaxonName#ICZN" + }, + "label": "Caiman crocodilus" + }, + { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonConcept#TaxonConcept", + "hasName": { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonName#TaxonName", + "nomenclaturalCode": "http://rs.tdwg.org/ontology/voc/TaxonName#ICZN", + "label": "Alligator mississippiensis", + "nameComplete": "Alligator mississippiensis" + }, + "label": "Alligator mississippiensis" + } + ], + "externalSpecifiers": [] + }, + { + "@id": "#Alligatoridae2_different", + "label": "Alligatoridae", + "scientificNameAuthorship": { + "bibliographicCitation": "(Cuvier 1807)" + }, + "phylorefType": "phyloref:PhyloreferenceUsingMinimumClade", + "definition": "Alligatoridae (Cuvier 1807).\n\nLast common ancestor of Alligator mississippiensis and Caiman crocodilus and all of its descendents.", + "definitionSource": { + "type": "article", + "title": "Phylogenetic approaches toward crocodylian history", + "authors": [ + { + "name": "Christopher A. Brochu", + "alternate": [ + "Brochu, Christopher A." + ], + "firstname": "Christopher", + "middlename": "A.", + "lastname": "Brochu" + } + ], + "year": 2003, + "figure": "1", + "identifier": [ + { + "type": "doi", + "id": "10.1146/annurev.earth.31.100901.141308" + } + ], + "link": [ + { + "url": "https://www.annualreviews.org/doi/10.1146/annurev.earth.31.100901.141308" + } + ], + "journal": { + "name": "Annual Review of Earth and Planetary Sciences", + "volume": "31", + "pages": "357--397", + "identifier": [ + { + "type": "eISSN", + "id": "1545-4495" + } + ] + } + }, + "internalSpecifiers": [ + { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonConcept#TaxonConcept", + "hasName": { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonName#TaxonName", + "label": "Caiman crocodilus", + "nameComplete": "Caiman crocodilus", + "genusPart": "Caiman", + "specificEpithet": "crocodilus" + }, + "label": "Caiman crocodilus" + }, + { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonConcept#TaxonConcept", + "hasName": { + "@type": "http://rs.tdwg.org/ontology/voc/TaxonName#TaxonName", + "nomenclaturalCode": "http://rs.tdwg.org/ontology/voc/TaxonName#ICZN", + "label": "Alligator mississippiensis", + "nameComplete": "Alligator mississippiensis", + "genusPart": "Alligator", + "specificEpithet": "mississippiensis" + }, + "label": "Alligator mississippiensis" + } + ], + "externalSpecifiers": [] + } + ], + "defaultNomenclaturalCodeIRI": "http://rs.tdwg.org/ontology/voc/TaxonName#ICZN" +} diff --git a/test/jphyloref.js b/test/jphyloref.js index 96da3890..a9a3c2c0 100644 --- a/test/jphyloref.js +++ b/test/jphyloref.js @@ -17,7 +17,7 @@ const expect = chai.expect; * Constants */ // The version of JPhyloRef to download. -const JPHYLOREF_VERSION = '0.4.0'; +const JPHYLOREF_VERSION = '1.1.1'; // The URL from where JPhyloRef should be downloaded. const JPHYLOREF_URL = `https://repo.maven.apache.org/maven2/org/phyloref/jphyloref/${JPHYLOREF_VERSION}/jphyloref-${JPHYLOREF_VERSION}.jar`; diff --git a/test/normalization.js b/test/normalization.js new file mode 100644 index 00000000..e5fb06eb --- /dev/null +++ b/test/normalization.js @@ -0,0 +1,130 @@ +/* + * Test normalization on examples/correct/normalization files. + */ + +const fs = require('fs'); +const path = require('path'); + +const chai = require('chai'); + +const phyx = require('../src'); + +const expect = chai.expect; + +/** + * When making the comparisons, we need to remove the `@id`s which would otherwise be different + * between the phylorefs being compared. + * + * @param phyloref The phyloref whose '@id' needs to be removed. + */ +function removeId(phyloref) { + // Shallow copy the phyloref. + const copiedPhyloref = Object.assign({}, phyloref); + // Delete the '@id'. + delete copiedPhyloref['@id']; + return copiedPhyloref; +} + + +/** + * Test whether normalization of phyloreferences and phylogenies work as expected. + * This test does not cover Phyx normalization. + */ + +describe('Phyloref and phylogeny normalization', function () { + describe('Test all normalization Phyx files', function () { + /* + * Normalization Phyx files consist of a number of phyloreferences and phylogenies. We can + * test them by confirming: + * - every phyloref or phylogeny should be DIFFERENT from every other. + * - every phyloref or phylogeny whose `@id` ends with `_same` should be IDENTICAL to + * every other phyloref or phylogeny whose `@id` ends with `_same` after NORMALIZATION (to + * test non-same phyloref or phylogeny files, they should be placed in different files). + * - every phyloref or phylogeny whose `@id` ends with `_different` should be DIFFERENT to + * every phyloref or phylogeny whose `@id` ends with `_same` even after NORMALIZATION. + */ + const normalizationExamples = fs + .readdirSync(path.resolve(__dirname, './examples/correct/normalization')) + .filter(filename => filename.endsWith('.json')); + + normalizationExamples.forEach((example) => { + const basename = path.resolve(__dirname, './examples/correct/normalization', path.parse(example).name); + const jsonFilename = `${basename}.json`; + + describe(`Normalization test file '${example}'`, function () { + const phyxDoc = JSON.parse(fs.readFileSync(jsonFilename)); + const phylorefs = phyxDoc.phylorefs || []; + const samePhylorefs = phylorefs.filter(p => (p['@id'] || '').endsWith('_same')); + const differentPhylorefs = phylorefs.filter(p => (p['@id'] || '').endsWith('_different')); + + // We don't need phylogeny normalization yet, so there's no point in testing them. + describe('Test phylogenies', function () { + it("These tests have not yet been implemented since we don't have an urgent need for them."); + }); + + // So we only focus on phyloreference normalization. + describe('Test phyloreferences', function () { + it('should have multiple same phyloreferences for testing', function () { + expect(samePhylorefs).to.not.be.empty; + }); + + it('should not have any duplicate phylorefs (which would be pointless)', function () { + // No two phyloreferences in a normalization file should be deeply identical to each + // other, otherwise the test will be pointless. + phylorefs.forEach((phyloref1) => { + phylorefs.forEach((phyloref2) => { + if (phyloref1 === phyloref2) return; + expect(removeId(phyloref1)) + .to + .not + .deep + .equal(removeId(phyloref2), + 'No two phyloreferences in a single normalization file should be identical.'); + }); + }); + }); + + it('should have pairs of `_same` phylorefs that are different, but are identical after normalization', function () { + // Every pair of `_same` phyloreferences should be different. + samePhylorefs.forEach((phyloref1) => { + samePhylorefs.forEach((phyloref2) => { + if (phyloref1 === phyloref2) return; + expect( + removeId(phyx.PhylorefWrapper.normalize(phyloref1)) + ) + .to + .deep + .equal( + removeId(phyx.PhylorefWrapper.normalize(phyloref2)), + `Expected phyloref ${phyloref1['@id']} to deeply equal ${phyloref2['@id']} ` + + 'after normalization' + ); + }); + }); + }); + + it('should have pairs of `_different` phylorefs that are different before and after normalization', function () { + // Every pair of `_different` phyloreferences should be different from every `_same` + // phyloreference, even after normalization. + differentPhylorefs.forEach((phyloref1) => { + samePhylorefs.forEach((phyloref2) => { + if (phyloref1 === phyloref2) return; + expect( + removeId(phyx.PhylorefWrapper.normalize(phyloref1)) + ) + .to + .not + .deep + .equal( + removeId(phyx.PhylorefWrapper.normalize(phyloref2)), + `Expected phyloref ${phyloref1['@id']} to not deeply equal ${phyloref2['@id']} ` + + 'after normalization' + ); + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/test/scripts/phyx2owl.js b/test/scripts/phyx2owl.js index e73161bc..5daa52e4 100644 --- a/test/scripts/phyx2owl.js +++ b/test/scripts/phyx2owl.js @@ -64,7 +64,7 @@ describe(PHYX2OWL_JS, function () { }); it('should be able to convert the entire `test/examples/correct` directory', function () { const EXAMPLE_DIR = path.resolve(__dirname, '../examples/correct'); - const jsonFilesInExamples = fs.readdirSync(EXAMPLE_DIR, 'utf8') + const jsonFilesInExamples = fs.readdirSync(EXAMPLE_DIR, { recursive: true }) .filter(fileName => fileName.toLowerCase().endsWith('.json')); const result = child.spawnSync(PHYX2OWL_JS, [EXAMPLE_DIR, '--base-iri', 'http://example.org/phyx.js/example#'], {