diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e8424bd4..834fbf1d 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -10,7 +10,7 @@ "@heroicons/react": "^2.2.0", "@minify-html/wasm": "^0.18.1", "@node-core/rehype-shiki": "^1.4.1", - "@node-core/ui-components": "^1.6.0", + "@node-core/ui-components": "^1.6.1", "@orama/orama": "^3.1.18", "@orama/ui": "^1.5.4", "@rollup/plugin-virtual": "^3.0.2", @@ -1009,41 +1009,44 @@ } }, "node_modules/@node-core/ui-components": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@node-core/ui-components/-/ui-components-1.6.0.tgz", - "integrity": "sha512-LUX0y+qHV0rMF2eCXiJ9HjSg4Yuvk+WwjcTq8JC/bCXxUcKvwcqK8N+FjnSaG9xfuZqbaIlCsc4HcpbkJyevDQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@node-core/ui-components/-/ui-components-1.6.1.tgz", + "integrity": "sha512-WhOzFZEipuhE9Y+MzrpR/R6DfhfaySqom0mZWGeval1ToJZ3Hiq8Hk7xZUXPQvHZ6PN8hlhER4njFpyObUTcBQ==", "dependencies": { "@heroicons/react": "^2.2.0", + "@orama/core": "^1.2.16", "@orama/ui": "^1.5.4", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "~2.1.16", - "@radix-ui/react-label": "~2.1.8", - "@radix-ui/react-select": "~2.2.6", - "@radix-ui/react-separator": "~1.1.8", - "@radix-ui/react-tabs": "~1.1.13", - "@radix-ui/react-tooltip": "~1.2.8", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "~4.1.18", + "@types/react": "^19.2.13", "@vcarl/remark-headings": "~0.1.0", "classnames": "~2.5.1", - "postcss-calc": "^10.1.1", - "tailwindcss": "~4.1.17" + "postcss-calc": "10.1.1", + "postcss-cli": "11.0.1", + "react": "^19.2.4", + "tailwindcss": "~4.1.17", + "typescript": "5.9.3" }, "engines": { "node": ">=20" } }, "node_modules/@orama/core": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@orama/core/-/core-1.2.13.tgz", - "integrity": "sha512-FNjEFDxPtdLU/ycrpt4z3GMYn3mljTXmyyN7901nMtXWOilZK+lOa4IRUBQBXMcPtxsxfEZVuJuB0CvIo1ugAA==", + "version": "1.2.19", + "resolved": "https://registry.npmjs.org/@orama/core/-/core-1.2.19.tgz", + "integrity": "sha512-AVEI0eG/a1RUQK+tBloRMppQf46Ky4kIYKEVjo0V0VfIGZHdLOE2PJR4v949kFwiTnfSJCUaxgwM74FCA1uHUA==", "license": "AGPL-3.0", "peer": true, "dependencies": { "@orama/cuid2": "2.2.3", - "@orama/oramacore-events-parser": "0.0.5", - "zod": "3.24.3", - "zod-to-json-schema": "3.24.5" + "@orama/oramacore-events-parser": "0.0.5" } }, "node_modules/@orama/cuid2": { @@ -3105,13 +3108,13 @@ } }, "node_modules/@types/react": { - "version": "19.1.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", - "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", "peer": true, "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/semver": { @@ -3740,7 +3743,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3752,6 +3754,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/are-docs-informative": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", @@ -3807,6 +3822,18 @@ "dev": true, "license": "MIT" }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -3828,7 +3855,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3948,6 +3974,42 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -4008,7 +4070,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -4023,7 +4084,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4033,14 +4093,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4050,7 +4108,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -4065,7 +4122,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -4078,7 +4134,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -4096,7 +4151,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4109,7 +4163,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/colorette": { @@ -4212,9 +4265,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/debug": { @@ -4268,6 +4321,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4379,7 +4441,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4859,7 +4920,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4923,6 +4983,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4937,7 +5025,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -5427,6 +5514,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -5457,7 +5556,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5483,7 +5581,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -5522,7 +5619,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -5639,6 +5735,18 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5687,6 +5795,18 @@ "inBundle": true, "license": "MIT" }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lint-staged": { "version": "16.2.7", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", @@ -6898,6 +7018,15 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -7121,7 +7250,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -7143,6 +7271,15 @@ "node": ">=0.10" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/piscina": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", @@ -7200,6 +7337,99 @@ "postcss": "^8.4.38" } }, + "node_modules/postcss-cli": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-11.0.1.tgz", + "integrity": "sha512-0UnkNPSayHKRe/tc2YGW6XnSqqOA9eqpiRMgRlV1S6HdGi16vwJBx7lviARzbV1HpQHqLLRH3o8vTcB0cLc+5g==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.3.0", + "dependency-graph": "^1.0.0", + "fs-extra": "^11.0.0", + "picocolors": "^1.0.0", + "postcss-load-config": "^5.0.0", + "postcss-reporter": "^7.0.0", + "pretty-hrtime": "^1.0.3", + "read-cache": "^1.0.0", + "slash": "^5.0.0", + "tinyglobby": "^0.2.12", + "yargs": "^17.0.0" + }, + "bin": { + "postcss": "index.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-load-config": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.1.0.tgz", + "integrity": "sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1", + "yaml": "^2.4.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + } + } + }, + "node_modules/postcss-reporter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.1.0.tgz", + "integrity": "sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "thenby": "^1.3.4" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/postcss-selector-parser": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", @@ -7265,6 +7495,15 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/prism-react-renderer": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz", @@ -7295,9 +7534,9 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "peer": true, "engines": { @@ -7413,6 +7652,27 @@ } } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/reading-time": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", @@ -7609,7 +7869,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7836,6 +8095,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/slice-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", @@ -8119,6 +8390,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/thenby": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", + "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==", + "license": "Apache-2.0" + }, "node_modules/throttleit": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", @@ -8181,7 +8458,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -8334,6 +8610,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8517,6 +8794,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unrs-resolver": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.2.tgz", @@ -8716,7 +9002,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -8741,7 +9026,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -8760,7 +9044,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -8770,7 +9053,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8780,14 +9062,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8797,7 +9077,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -8812,7 +9091,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -8834,25 +9112,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 3967317b..f903131e 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@heroicons/react": "^2.2.0", "@minify-html/wasm": "^0.18.1", "@node-core/rehype-shiki": "^1.4.1", - "@node-core/ui-components": "^1.6.0", + "@node-core/ui-components": "^1.6.1", "@orama/orama": "^3.1.18", "@orama/ui": "^1.5.4", "@rollup/plugin-virtual": "^3.0.2", diff --git a/src/generators/jsx-ast/constants.mjs b/src/generators/jsx-ast/constants.mjs index e392d56c..ae22457f 100644 --- a/src/generators/jsx-ast/constants.mjs +++ b/src/generators/jsx-ast/constants.mjs @@ -161,8 +161,8 @@ export const TYPES_WITH_METHOD_SIGNATURES = [ 'classMethod', ]; -// Regex to trim leading whitespace and colons from strings -export const TRIMMABLE_PADDING_REGEX = /^[\s:]+/; +// Regex to trim leading whitespace, colons, and hyphens from strings +export const TRIMMABLE_PADDING_REGEX = /^[\s:-]+/; // Patterns to map deprecation "Type" text to AlertBox levels. // Order matters: first match wins. diff --git a/src/generators/jsx-ast/utils/__tests__/buildPropertyTable.test.mjs b/src/generators/jsx-ast/utils/__tests__/buildPropertyTable.test.mjs deleted file mode 100644 index 95d42aff..00000000 --- a/src/generators/jsx-ast/utils/__tests__/buildPropertyTable.test.mjs +++ /dev/null @@ -1,114 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import createPropertyTable, { - classifyTypeNode, - extractPropertyName, - extractTypeAnnotations, -} from '../buildPropertyTable.mjs'; - -describe('classifyTypeNode', () => { - it('identifies union separator', () => { - const node = { type: 'text', value: ' | ' }; - assert.equal(classifyTypeNode(node), 2); - }); - - it('identifies type reference', () => { - const node = { - type: 'link', - children: [{ type: 'inlineCode', value: '' }], - }; - assert.equal(classifyTypeNode(node), 1); - }); - - it('returns 0 for non-type nodes', () => { - const node = { type: 'text', value: 'regular text' }; - assert.equal(classifyTypeNode(node), 0); - }); -}); - -describe('extractPropertyName', () => { - it('extracts name from inlineCode', () => { - const children = [{ type: 'inlineCode', value: 'propName ' }]; - const result = extractPropertyName(children); - assert.equal(result.tagName, 'code'); - assert.equal(result.children[0].value, 'propName'); - }); -}); - -describe('extractTypeAnnotations', () => { - it('extracts type nodes until non-type node', () => { - const children = [ - { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, - { type: 'text', value: ' | ' }, - { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, - { type: 'text', value: ' - description' }, - ]; - - const result = extractTypeAnnotations(children); - - assert.equal(result.length, 3); - assert.equal(children.length, 1); // Only non-type node left - assert.equal(children[0].value, ' - description'); - }); - - it('handles single type node', () => { - const children = [ - { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, - { type: 'text', value: ' description' }, - ]; - - const result = extractTypeAnnotations(children); - - assert.equal(result.length, 1); - assert.equal(children.length, 1); - }); - - it('returns empty array for no type nodes', () => { - const children = [{ type: 'text', value: 'just text' }]; - const result = extractTypeAnnotations(children); - assert.equal(result.length, 0); - assert.equal(children.length, 1); - }); -}); - -describe('createPropertyTable', () => { - it('creates a table with headings by default', () => { - const node = { - children: [ - { - children: [ - { - children: [{ type: 'inlineCode', value: 'propName' }], - }, - ], - }, - ], - }; - - const result = createPropertyTable(node); - - assert.equal(result.tagName, 'table'); - assert.ok(result.children.find(child => child.tagName === 'thead')); - assert.ok(result.children.find(child => child.tagName === 'tbody')); - }); - - it('creates a table without headings when specified', () => { - const node = { - children: [ - { - children: [ - { - children: [{ type: 'inlineCode', value: 'propName' }], - }, - ], - }, - ], - }; - - const result = createPropertyTable(node, false); - - assert.equal(result.tagName, 'table'); - assert.ok(!result.children.find(child => child.tagName === 'thead')); - }); -}); diff --git a/src/generators/jsx-ast/utils/__tests__/buildSignature.test.mjs b/src/generators/jsx-ast/utils/__tests__/buildSignature.test.mjs deleted file mode 100644 index f65d7e6f..00000000 --- a/src/generators/jsx-ast/utils/__tests__/buildSignature.test.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import { generateSignature } from '../buildSignature.mjs'; - -describe('generateSignature', () => { - it('formats union return types without spaces as spaced ("|" surrounded)', () => { - const sig = generateSignature( - 'foo', - { - params: [], - return: { type: 'string|number' }, - }, - '' - ); - - assert.equal(sig, 'foo(): string | number'); - }); - - it('preserves already spaced union return types', () => { - const sig = generateSignature( - 'bar', - { - params: [], - return: { type: 'Promise | undefined' }, - }, - '' - ); - - assert.equal(sig, 'bar(): Promise | undefined'); - }); - - it('omits return type when undefined', () => { - const sig = generateSignature( - 'baz', - { - params: [], - return: undefined, - }, - '' - ); - - assert.equal(sig, 'baz()'); - }); -}); diff --git a/src/generators/jsx-ast/utils/__tests__/signature.test.mjs b/src/generators/jsx-ast/utils/__tests__/signature.test.mjs new file mode 100644 index 00000000..ec49ca0b --- /dev/null +++ b/src/generators/jsx-ast/utils/__tests__/signature.test.mjs @@ -0,0 +1,415 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { generateSignature, getFullName } from '../signature.mjs'; + +describe('generateSignature', () => { + describe('function signatures', () => { + it('formats union return types without spaces as spaced', () => { + const sig = generateSignature( + 'foo', + { + params: [], + return: { type: 'string|number' }, + }, + '' + ); + + assert.strictEqual(sig, 'foo(): string | number'); + }); + + it('preserves already spaced union return types', () => { + const sig = generateSignature( + 'bar', + { + params: [], + return: { type: 'Promise | undefined' }, + }, + '' + ); + + assert.strictEqual(sig, 'bar(): Promise | undefined'); + }); + + it('omits return type when undefined', () => { + const sig = generateSignature( + 'baz', + { + params: [], + return: undefined, + }, + '' + ); + + assert.strictEqual(sig, 'baz()'); + }); + + it('handles empty return type', () => { + const sig = generateSignature( + 'test', + { + params: [], + return: null, + }, + '' + ); + + assert.strictEqual(sig, 'test()'); + }); + + it('includes prefix when provided', () => { + const sig = generateSignature( + 'Constructor', + { + params: [], + return: undefined, + }, + 'new ' + ); + + assert.strictEqual(sig, 'new Constructor()'); + }); + + it('handles complex union types with multiple pipes', () => { + const sig = generateSignature( + 'complexFunc', + { + params: [], + return: { type: 'string|number|boolean|null' }, + }, + '' + ); + + assert.strictEqual( + sig, + 'complexFunc(): string | number | boolean | null' + ); + }); + + it('filters empty parts in union types', () => { + const sig = generateSignature( + 'filterFunc', + { + params: [], + return: { type: 'string||number|' }, + }, + '' + ); + + assert.strictEqual(sig, 'filterFunc(): string | number'); + }); + }); + + describe('parameters', () => { + it('handles single parameter without optional flag or default', () => { + const sig = generateSignature( + 'singleParam', + { + params: [{ name: 'value', optional: false }], + return: undefined, + }, + '' + ); + + assert.strictEqual(sig, 'singleParam(value)'); + }); + + it('handles multiple parameters', () => { + const sig = generateSignature( + 'multiParam', + { + params: [ + { name: 'first', optional: false }, + { name: 'second', optional: false }, + ], + return: undefined, + }, + '' + ); + + assert.strictEqual(sig, 'multiParam(first, second)'); + }); + + it('marks optional parameters with question mark', () => { + const sig = generateSignature( + 'optionalParam', + { + params: [ + { name: 'required', optional: false }, + { name: 'optional', optional: true }, + ], + return: undefined, + }, + '' + ); + + assert.strictEqual(sig, 'optionalParam(required, optional?)'); + }); + + it('marks parameters with defaults as optional', () => { + const sig = generateSignature( + 'defaultParam', + { + params: [ + { name: 'normal', optional: false }, + { name: 'withDefault', optional: false, default: 'defaultValue' }, + ], + return: undefined, + }, + '' + ); + + assert.strictEqual(sig, 'defaultParam(normal, withDefault?)'); + }); + + it('handles parameters that are both optional and have defaults', () => { + const sig = generateSignature( + 'bothOptionalAndDefault', + { + params: [{ name: 'param', optional: true, default: 'value' }], + return: undefined, + }, + '' + ); + + assert.strictEqual(sig, 'bothOptionalAndDefault(param?)'); + }); + + it('handles empty params array', () => { + const sig = generateSignature( + 'noParams', + { + params: [], + return: { type: 'string' }, + }, + '' + ); + + assert.strictEqual(sig, 'noParams(): string'); + }); + + it('handles params without optional property', () => { + const sig = generateSignature( + 'implicitOptional', + { + params: [{ name: 'param1' }, { name: 'param2', default: 'value' }], + return: undefined, + }, + '' + ); + + assert.strictEqual(sig, 'implicitOptional(param1, param2?)'); + }); + }); + + describe('class signatures', () => { + it('generates class signature with extends clause', () => { + const sig = generateSignature( + 'MyClass', + { + params: [], + extends: { type: 'BaseClass' }, + }, + '' + ); + + assert.strictEqual(sig, 'class MyClass extends BaseClass'); + }); + + it('generates class signature with extends and prefix', () => { + const sig = generateSignature( + 'MyClass', + { + params: [], + extends: { type: 'BaseClass' }, + }, + 'abstract ' + ); + + assert.strictEqual(sig, 'class abstract MyClass extends BaseClass'); + }); + + it('ignores params and return type for class with extends', () => { + const sig = generateSignature( + 'MyClass', + { + params: [{ name: 'ignored', optional: false }], + return: { type: 'ignored' }, + extends: { type: 'BaseClass' }, + }, + '' + ); + + assert.strictEqual(sig, 'class MyClass extends BaseClass'); + }); + + it('generates function signature when no extends present', () => { + const sig = generateSignature( + 'MyClass', + { + params: [{ name: 'param', optional: false }], + return: { type: 'MyClass' }, + }, + 'new ' + ); + + assert.strictEqual(sig, 'new MyClass(param): MyClass'); + }); + }); + + describe('edge cases', () => { + it('handles null return type', () => { + const sig = generateSignature( + 'nullReturn', + { + params: [], + return: null, + }, + '' + ); + + assert.strictEqual(sig, 'nullReturn()'); + }); + + it('handles missing extends property', () => { + const sig = generateSignature( + 'NoExtends', + { + params: [{ name: 'param' }], + return: { type: 'void' }, + }, + '' + ); + + assert.strictEqual(sig, 'NoExtends(param): void'); + }); + }); +}); + +describe('getFullName', () => { + it('returns fallback when name equals text', () => { + const result = getFullName({ name: 'test', text: 'test' }, 'fallback'); + assert.strictEqual(result, 'fallback'); + }); + + it('returns name as fallback when name equals text and no fallback provided', () => { + const result = getFullName({ name: 'test', text: 'test' }); + assert.strictEqual(result, 'test'); + }); + + it('extracts inline code that includes the name', () => { + const result = getFullName({ + name: 'myFunc', + text: 'This is `myFunc(param1, param2)` function', + }); + assert.strictEqual(result, 'myFunc'); + }); + + it('handles inline code with extra content after name', () => { + const result = getFullName({ + name: 'authenticate', + text: 'The `authenticate(user, password)` method', + }); + assert.strictEqual(result, 'authenticate'); + }); + + it('strips quotes from the beginning', () => { + const result = getFullName({ + name: 'func', + text: 'The `"func"()` method', + }); + assert.strictEqual(result, 'func'); + }); + + it('strips single quotes from the beginning', () => { + const result = getFullName({ + name: 'func', + text: "The `'func'()` method", + }); + assert.strictEqual(result, 'func'); + }); + + it('strips "new" keyword from the beginning', () => { + const result = getFullName({ + name: 'Constructor', + text: 'The `new Constructor()` call', + }); + assert.strictEqual(result, 'Constructor'); + }); + + it('strips "new " with space from the beginning', () => { + const result = getFullName({ + name: 'MyClass', + text: 'The `new MyClass(param)` constructor', + }); + assert.strictEqual(result, 'MyClass'); + }); + + it('returns fallback when no inline code found', () => { + const result = getFullName( + { + name: 'func', + text: 'This is a function without code blocks', + }, + 'fallback' + ); + assert.strictEqual(result, 'fallback'); + }); + + it('returns fallback when inline code does not include name', () => { + const result = getFullName( + { + name: 'myFunc', + text: 'This is `otherFunc()` function', + }, + 'fallback' + ); + assert.strictEqual(result, 'fallback'); + }); + + it('handles empty inline code', () => { + const result = getFullName( + { + name: 'func', + text: 'This has `` empty code', + }, + 'fallback' + ); + assert.strictEqual(result, 'fallback'); + }); + + it('handles multiple inline code blocks, uses first match', () => { + const result = getFullName({ + name: 'func', + text: 'This has `func()` and `other()` code', + }); + assert.strictEqual(result, 'func'); + }); + + it('handles complex inline code with parameters', () => { + const result = getFullName({ + name: 'processData', + text: 'The `processData(input, options = {})` method processes data', + }); + assert.strictEqual(result, 'processData'); + }); + + it('strips both quotes and new keyword', () => { + const result = getFullName({ + name: 'MyClass', + text: '`"new MyClass"()`', + }); + assert.strictEqual(result, 'MyClass'); + }); + + it('handles text with no backticks', () => { + const result = getFullName( + { + name: 'func', + text: 'This function does something', + }, + 'fallbackValue' + ); + assert.strictEqual(result, 'fallbackValue'); + }); +}); diff --git a/src/generators/jsx-ast/utils/__tests__/types.test.mjs b/src/generators/jsx-ast/utils/__tests__/types.test.mjs new file mode 100644 index 00000000..37caf401 --- /dev/null +++ b/src/generators/jsx-ast/utils/__tests__/types.test.mjs @@ -0,0 +1,627 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { + classifyTypeNode, + extractPropertyName, + extractTypeAnnotations, + parseListIntoProperties, +} from '../types.mjs'; + +// Mock remark processor for tests +const remark = { + runSync: () => ({ + body: [{ expression: 'mock-expression' }], + }), +}; + +describe('classifyTypeNode', () => { + it('returns 2 for union separator text node', () => { + const node = { type: 'text', value: ' | ' }; + assert.strictEqual(classifyTypeNode(node), 2); + }); + + it('returns 1 for type reference link with angle bracket inline code', () => { + const node = { + type: 'link', + children: [{ type: 'inlineCode', value: '' }], + }; + assert.strictEqual(classifyTypeNode(node), 1); + }); + + it('returns 1 for type reference with complex angle bracket content', () => { + const node = { + type: 'link', + children: [{ type: 'inlineCode', value: '>' }], + }; + assert.strictEqual(classifyTypeNode(node), 1); + }); + + it('returns 0 for regular text node', () => { + const node = { type: 'text', value: 'regular text' }; + assert.strictEqual(classifyTypeNode(node), 0); + }); + + it('returns 0 for link without inline code child', () => { + const node = { + type: 'link', + children: [{ type: 'text', value: 'regular link' }], + }; + assert.strictEqual(classifyTypeNode(node), 0); + }); + + it('returns 0 for link with inline code not starting with angle bracket', () => { + const node = { + type: 'link', + children: [{ type: 'inlineCode', value: 'regularCode' }], + }; + assert.strictEqual(classifyTypeNode(node), 0); + }); + + it('returns 0 for node without children', () => { + const node = { type: 'link' }; + assert.strictEqual(classifyTypeNode(node), 0); + }); + + it('returns 0 for node with empty children array', () => { + const node = { type: 'link', children: [] }; + assert.strictEqual(classifyTypeNode(node), 0); + }); + + it('returns 0 for different text values', () => { + const node1 = { type: 'text', value: '|' }; + const node2 = { type: 'text', value: ' |' }; + const node3 = { type: 'text', value: '| ' }; + + assert.strictEqual(classifyTypeNode(node1), 0); + assert.strictEqual(classifyTypeNode(node2), 0); + assert.strictEqual(classifyTypeNode(node3), 0); + }); + + it('returns 0 for non-link, non-text nodes', () => { + const nodes = [ + { type: 'paragraph' }, + { type: 'emphasis' }, + { type: 'strong' }, + { type: 'inlineCode' }, + ]; + + nodes.forEach(node => { + assert.strictEqual(classifyTypeNode(node), 0); + }); + }); +}); + +describe('extractPropertyName', () => { + it('extracts name from inline code and removes it from nodes', () => { + const nodes = [ + { type: 'inlineCode', value: 'propName' }, + { type: 'text', value: ' remaining text' }, + ]; + const current = {}; + + extractPropertyName(nodes, current); + + assert.deepStrictEqual(current, { name: 'propName' }); + assert.strictEqual(nodes.length, 1); + assert.strictEqual(nodes[0].value, ' remaining text'); + }); + + it('trims trailing whitespace from inline code value', () => { + const nodes = [{ type: 'inlineCode', value: 'propName ' }]; + const current = {}; + + extractPropertyName(nodes, current); + + assert.deepStrictEqual(current, { name: 'propName' }); + assert.strictEqual(nodes.length, 0); + }); + + it('handles empty nodes array gracefully', () => { + const nodes = []; + const current = {}; + + extractPropertyName(nodes, current); + + assert.deepStrictEqual(current, {}); + assert.strictEqual(nodes.length, 0); + }); + + it('does nothing when first node is not text or inlineCode', () => { + const nodes = [ + { type: 'emphasis', children: [{ type: 'text', value: 'emphasized' }] }, + ]; + const current = {}; + + extractPropertyName(nodes, current); + + assert.deepStrictEqual(current, {}); + assert.strictEqual(nodes.length, 1); + }); + + describe('text node processing', () => { + it('extracts "Returns" and sets kind to "return"', () => { + const nodes = [{ type: 'text', value: 'Returns: description here' }]; + const current = {}; + + extractPropertyName(nodes, current); + + assert.deepStrictEqual(current, { name: 'Returns', kind: 'return' }); + assert.strictEqual(nodes[0].value, 'description here'); + }); + + it('preserves node with remaining non-whitespace content', () => { + const nodes = [{ type: 'text', value: 'Returns: some content' }]; + const current = {}; + + extractPropertyName(nodes, current); + + assert.strictEqual(nodes.length, 1); + assert.strictEqual(nodes[0].value, 'some content'); + }); + + it('does nothing when text does not match typed list starters', () => { + const nodes = [{ type: 'text', value: 'regular text without colon' }]; + const current = {}; + + extractPropertyName(nodes, current); + + assert.deepStrictEqual(current, {}); + assert.strictEqual(nodes.length, 1); + assert.strictEqual(nodes[0].value, 'regular text without colon'); + }); + }); +}); + +describe('extractTypeAnnotations', () => { + it('extracts single type reference and returns expression', () => { + const nodes = [ + { + type: 'link', + children: [{ type: 'inlineCode', value: '' }], + }, + { type: 'text', value: ' description follows' }, + ]; + + const result = extractTypeAnnotations(nodes, remark); + + assert.strictEqual(result, 'mock-expression'); + assert.strictEqual(nodes.length, 1); + assert.strictEqual(nodes[0].value, ' description follows'); + }); + + it('extracts union types with separator', () => { + const nodes = [ + { + type: 'link', + children: [{ type: 'inlineCode', value: '' }], + }, + { type: 'text', value: ' | ' }, + { + type: 'link', + children: [{ type: 'inlineCode', value: '' }], + }, + { type: 'text', value: ' description' }, + ]; + + const result = extractTypeAnnotations(nodes, remark); + + assert.strictEqual(result, 'mock-expression'); + assert.strictEqual(nodes.length, 1); + assert.strictEqual(nodes[0].value, ' description'); + }); + + it('handles union separator at end without following type', () => { + const nodes = [ + { + type: 'link', + children: [{ type: 'inlineCode', value: '' }], + }, + { type: 'text', value: ' | ' }, + ]; + + const result = extractTypeAnnotations(nodes, remark); + + assert.strictEqual(result, 'mock-expression'); + assert.strictEqual(nodes.length, 0); + }); + + it('returns undefined when no type annotations found', () => { + const nodes = [ + { type: 'text', value: 'regular text' }, + { type: 'emphasis', children: [{ type: 'text', value: 'emphasized' }] }, + ]; + + const result = extractTypeAnnotations(nodes, remark); + + assert.strictEqual(result, undefined); + assert.strictEqual(nodes.length, 2); + }); + + it('stops extraction at first non-type node', () => { + const nodes = [ + { + type: 'link', + children: [{ type: 'inlineCode', value: '' }], + }, + { type: 'emphasis', children: [{ type: 'text', value: 'not a type' }] }, + { type: 'text', value: ' | ' }, // This shouldn't be consumed + ]; + + const result = extractTypeAnnotations(nodes, remark); + + assert.strictEqual(result, 'mock-expression'); + assert.strictEqual(nodes.length, 2); + assert.strictEqual(nodes[0].type, 'emphasis'); + assert.strictEqual(nodes[1].value, ' | '); + }); + + it('handles empty nodes array', () => { + const nodes = []; + + const result = extractTypeAnnotations(nodes, remark); + + assert.strictEqual(result, undefined); + assert.strictEqual(nodes.length, 0); + }); + + it('processes complex union types with multiple separators', () => { + const nodes = [ + { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, + { type: 'text', value: ' | ' }, + { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, + { type: 'text', value: ' | ' }, + { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, + { type: 'text', value: ' description' }, + ]; + + const result = extractTypeAnnotations(nodes, remark); + + assert.strictEqual(result, 'mock-expression'); + assert.strictEqual(nodes.length, 1); + assert.strictEqual(nodes[0].value, ' description'); + }); +}); + +describe('parseListIntoProperties', () => { + it('parses simple property with inline code name', () => { + const node = { + children: [ + { + children: [ + { + children: [ + { type: 'inlineCode', value: 'propName' }, + { type: 'text', value: ' description here' }, + ], + }, + ], + }, + ], + }; + + const result = parseListIntoProperties(node, remark); + + assert.deepStrictEqual(result, [ + { + children: undefined, + description: 'mock-expression', + name: 'propName', + optional: false, + type: undefined, + }, + ]); + }); + + it('parses property with type annotations', () => { + const node = { + children: [ + { + children: [ + { + children: [ + { type: 'inlineCode', value: 'prop' }, + { + type: 'link', + children: [{ type: 'inlineCode', value: '' }], + }, + { type: 'text', value: ' description' }, + ], + }, + ], + }, + ], + }; + + const result = parseListIntoProperties(node, remark); + + assert.deepStrictEqual(result, [ + { + children: undefined, + description: 'mock-expression', + name: 'prop', + optional: false, + type: 'mock-expression', + }, + ]); + }); + + it('detects optional properties with default expressions', () => { + const node = { + children: [ + { + children: [ + { + children: [ + { type: 'inlineCode', value: 'optionalProp' }, + { type: 'text', value: ' optional parameter description' }, + ], + }, + ], + }, + ], + }; + + const result = parseListIntoProperties(node, remark); + + assert.deepStrictEqual(result, [ + { + children: undefined, + description: 'mock-expression', + name: 'optionalProp', + optional: false, + type: undefined, + }, + ]); + }); + + it('handles properties without descriptions', () => { + const node = { + children: [ + { + children: [ + { + children: [{ type: 'inlineCode', value: 'propOnly' }], + }, + ], + }, + ], + }; + + const result = parseListIntoProperties(node, remark); + + assert.deepStrictEqual(result, [ + { + children: undefined, + name: 'propOnly', + type: undefined, + }, + ]); + }); + + it('trims padding from description text', () => { + // Mock TRIMMABLE_PADDING_REGEX + const node = { + children: [ + { + children: [ + { + children: [ + { type: 'inlineCode', value: 'prop' }, + { type: 'text', value: ' - description with padding' }, + ], + }, + ], + }, + ], + }; + + const result = parseListIntoProperties(node, remark); + + assert.deepStrictEqual(result, [ + { + children: undefined, + description: 'mock-expression', + name: 'prop', + optional: false, + type: undefined, + }, + ]); + }); + + it('processes nested list items as children', () => { + const node = { + children: [ + { + children: [ + { + children: [ + { type: 'inlineCode', value: 'parent' }, + { type: 'text', value: ' parent description' }, + ], + }, + { + type: 'list', + children: [ + { + children: [ + { + children: [ + { type: 'inlineCode', value: 'child' }, + { type: 'text', value: ' child description' }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }; + + const result = parseListIntoProperties(node, remark); + + assert.deepStrictEqual(result, [ + { + children: [ + { + children: undefined, + description: 'mock-expression', + name: 'child', + optional: false, + type: undefined, + }, + ], + description: 'mock-expression', + name: 'parent', + optional: false, + type: undefined, + }, + ]); + }); + + it('handles multiple list items', () => { + const node = { + children: [ + { + children: [ + { + children: [ + { type: 'inlineCode', value: 'first' }, + { type: 'text', value: ' first description' }, + ], + }, + ], + }, + { + children: [ + { + children: [ + { type: 'inlineCode', value: 'second' }, + { type: 'text', value: ' second description' }, + ], + }, + ], + }, + ], + }; + + const result = parseListIntoProperties(node, remark); + + assert.deepStrictEqual(result, [ + { + children: undefined, + description: 'mock-expression', + name: 'first', + optional: false, + type: undefined, + }, + { + children: undefined, + description: 'mock-expression', + name: 'second', + optional: false, + type: undefined, + }, + ]); + }); + + it('handles properties with typed list starters', () => { + const node = { + children: [ + { + children: [ + { + children: [ + { type: 'text', value: 'Returns: result description' }, + ], + }, + ], + }, + ], + }; + + const result = parseListIntoProperties(node, remark); + + assert.deepStrictEqual(result, [ + { + children: undefined, + description: 'mock-expression', + kind: 'return', + name: 'Returns', + optional: false, + type: undefined, + }, + ]); + }); + + it('handles empty list', () => { + const node = { + children: [], + }; + + const result = parseListIntoProperties(node, remark); + + assert.deepStrictEqual(result, []); + }); + + it('handles complex nested structure', () => { + const node = { + children: [ + { + children: [ + { + children: [ + { type: 'inlineCode', value: 'config' }, + { + type: 'link', + children: [{ type: 'inlineCode', value: '' }], + }, + { type: 'text', value: ' configuration object' }, + ], + }, + { + type: 'list', + children: [ + { + children: [ + { + children: [ + { type: 'inlineCode', value: 'timeout' }, + { + type: 'link', + children: [{ type: 'inlineCode', value: '' }], + }, + { type: 'text', value: ' timeout in milliseconds' }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }; + + const result = parseListIntoProperties(node, remark); + assert.deepStrictEqual(result, [ + { + children: [ + { + children: undefined, + description: 'mock-expression', + name: 'timeout', + optional: false, + type: 'mock-expression', + }, + ], + description: 'mock-expression', + name: 'config', + optional: false, + type: 'mock-expression', + }, + ]); + }); +}); diff --git a/src/generators/jsx-ast/utils/buildBarProps.mjs b/src/generators/jsx-ast/utils/buildBarProps.mjs index 2dd8c8b4..b8da32cf 100644 --- a/src/generators/jsx-ast/utils/buildBarProps.mjs +++ b/src/generators/jsx-ast/utils/buildBarProps.mjs @@ -3,7 +3,7 @@ import readingTime from 'reading-time'; import { visit } from 'unist-util-visit'; -import { getFullName } from './buildSignature.mjs'; +import { getFullName } from './signature.mjs'; import getConfig from '../../../utils/configuration/index.mjs'; import { GITHUB_EDIT_URL, diff --git a/src/generators/jsx-ast/utils/buildContent.mjs b/src/generators/jsx-ast/utils/buildContent.mjs index 363d1829..62487631 100644 --- a/src/generators/jsx-ast/utils/buildContent.mjs +++ b/src/generators/jsx-ast/utils/buildContent.mjs @@ -7,7 +7,6 @@ import { SKIP, visit } from 'unist-util-visit'; import { createJSXElement } from './ast.mjs'; import { buildMetaBarProps } from './buildBarProps.mjs'; -import createPropertyTable from './buildPropertyTable.mjs'; import { enforceArray } from '../../../utils/array.mjs'; import createQueries from '../../../utils/queries/index.mjs'; import { JSX_IMPORTS } from '../../web/constants.mjs'; @@ -21,7 +20,11 @@ import { TYPES_WITH_METHOD_SIGNATURES, TYPE_PREFIX_LENGTH, } from '../constants.mjs'; -import insertSignature, { getFullName } from './buildSignature.mjs'; +import { + insertSignatureCodeBlock, + createSignatureTable, + getFullName, +} from './signature.mjs'; import getConfig from '../../../utils/configuration/index.mjs'; import { GITHUB_BLOB_URL, @@ -242,7 +245,7 @@ export const transformHeadingNode = async ( // If the heading type supports method signatures, insert signature block if (TYPES_WITH_METHOD_SIGNATURES.includes(node.data.type)) { - insertSignature(parent, node, index + 1); + insertSignatureCodeBlock(parent, node, index + 1); } return [SKIP]; @@ -269,7 +272,8 @@ export const processEntry = (entry, remark) => { visit( content, createQueries.UNIST.isStronglyTypedList, - (node, idx, parent) => (parent.children[idx] = createPropertyTable(node)) + (node, idx, parent) => + (parent.children[idx] = createSignatureTable(node, remark)) ); return content; diff --git a/src/generators/jsx-ast/utils/buildPropertyTable.mjs b/src/generators/jsx-ast/utils/buildPropertyTable.mjs deleted file mode 100644 index 6e441d3a..00000000 --- a/src/generators/jsx-ast/utils/buildPropertyTable.mjs +++ /dev/null @@ -1,197 +0,0 @@ -import { h as createElement } from 'hastscript'; - -import createQueries from '../../../utils/queries/index.mjs'; -import { TRIMMABLE_PADDING_REGEX } from '../constants.mjs'; - -/** - * Determines if a node looks like part of a type annotation. - * - * Returns: - * - 2 if the node is a union separator (e.g. `' | '`) - * - 1 if the node is a type reference (e.g. ``) - * - 0 if it's not type-related - * - * @param {import('mdast').Node} node - The MDAST node to analyze - */ -export const classifyTypeNode = node => { - // Union separator is plain text with exact value `' | '` - if (node.type === 'text' && node.value === ' | ') { - return 2; - } - - // Type references like `` are links wrapping inlineCode - if ( - node.type === 'link' && - node.children?.[0]?.type === 'inlineCode' && - node.children[0].value?.startsWith('<') - ) { - return 1; - } - - return 0; // Not a type node -}; - -/** - * Attempts to extract the property name from the start of a paragraph. - * - * @param {Array} children - Paragraph content nodes - */ -export const extractPropertyName = children => { - if (!children.length) { - return; - } - - const first = children[0]; - - // Inline code (`foo`) becomes foo - if (first.type === 'inlineCode') { - // Remove the node from the children, as we processed it. - children.shift(); - return createElement('code', first.value.trimEnd()); - } - - // Text with a prefix like "Type:", "Param:", etc. - if (first.type === 'text') { - const starterMatch = first.value.match( - createQueries.QUERIES.typedListStarters - ); - if (starterMatch) { - // If the starter is 'Type', we don't have a property. - const label = starterMatch[1] !== 'Type' && starterMatch[1]; - - // Trim off the matched prefix - first.value = first.value.slice(starterMatch[0].length); - - // Remove node entirely if no value remains - if (!first.value.trim()) { - children.shift(); - } - - return label; - } - } - - return undefined; -}; - -/** - * Scans through the paragraph content and pulls out any type annotations. - * - * @param {Array} children - */ -export const extractTypeAnnotations = children => { - const types = []; - - while (children.length > 0) { - const typeKind = classifyTypeNode(children[0]); - - if (typeKind === 0) { - // Stop when the next node is not type-related - break; - } - - types.push(children.shift()); // Add type or union separator - - // If union separator, include next type too - if (typeKind === 2 && children.length) { - types.push(children.shift()); - } - } - - return types; -}; - -/** - * Converts an MDAST list of typed properties into structured property data. - * - * @param {import('mdast').List} node - The input list node - */ -export const parseListIntoProperties = node => { - const properties = []; - - for (const item of node.children) { - // The children of the first element (the paragraph) is the list. - const [{ children }, ...sublists] = item.children; - - const name = extractPropertyName(children); - - // After all of that shifting, remove the (possible) blank - // space. - if (children[0]?.type === 'text' && !children[0].value.trim()) { - children.shift(); - } - - const types = extractTypeAnnotations(children); - - // Clean up leading whitespace in remaining description - if (children[0]?.type === 'text') { - children[0].value = children[0].value.replace( - TRIMMABLE_PADDING_REGEX, - '' - ); - } - - properties.push({ - name, - types, - // The remaining children are the description - desc: children, - // Is there a list within this list? - sublist: sublists.find(createQueries.UNIST.isLooselyTypedList), - }); - } - - return properties; -}; - -/** - * Renders a table of properties based on parsed metadata from a Markdown list. - * - * @param {import('mdast').List} node - Source list node from MDAST - * @param {boolean} [withHeading=true] - Whether to include table headings - */ -const createPropertyTable = (node, withHeading = true) => { - const properties = parseListIntoProperties(node); - - // Generate table rows for each property - const rows = properties.flatMap(prop => { - const cells = [ - createElement('td', prop.name || '-'), - createElement('td', prop.types.length > 0 ? prop.types : '-'), - createElement('td', prop.desc.length > 0 ? prop.desc : '-'), - ]; - - const mainRow = createElement('tr', cells); - - // If the property has a nested sublist, add a second row for the subtable - if (prop.sublist) { - const nestedRow = createElement( - 'tr', - createElement( - 'td', - { colspan: cells.length }, - createPropertyTable(prop.sublist, false) - ) - ); - return [mainRow, nestedRow]; - } - - return mainRow; - }); - - // Render the full table - return withHeading - ? createElement('table', [ - createElement('thead', [ - createElement('tr', [ - createElement('th', 'Property'), - createElement('th', 'Type'), - createElement('th', 'Description'), - ]), - ]), - createElement('tbody', rows), - ]) - : createElement('table', rows); -}; - -export default createPropertyTable; diff --git a/src/generators/jsx-ast/utils/buildSignature.mjs b/src/generators/jsx-ast/utils/signature.mjs similarity index 84% rename from src/generators/jsx-ast/utils/buildSignature.mjs rename to src/generators/jsx-ast/utils/signature.mjs index aca80e41..6e0e6106 100644 --- a/src/generators/jsx-ast/utils/buildSignature.mjs +++ b/src/generators/jsx-ast/utils/signature.mjs @@ -1,9 +1,12 @@ import { h as createElement } from 'hastscript'; +import { createJSXElement } from './ast.mjs'; +import { parseListIntoProperties } from './types.mjs'; import { highlighter } from '../../../utils/highlighter.mjs'; import createQueries from '../../../utils/queries/index.mjs'; import { parseListItem } from '../../legacy-json/utils/parseList.mjs'; import parseSignature from '../../legacy-json/utils/parseSignature.mjs'; +import { JSX_IMPORTS } from '../../web/constants.mjs'; /** * Generates a string representation of a function or class signature. @@ -79,7 +82,7 @@ export const getFullName = ({ name, text }, fallback = name) => { return code?.includes(name) ? code .slice(0, code.indexOf(name) + name.length) // Truncate everything after the name. - .replace(/^["']|new\s*/, '') // Strip quotes or "new" keyword + .replace(/^["']|new\s*/g, '') // Strip quotes or "new" keyword : fallback; }; @@ -91,7 +94,7 @@ export const getFullName = ({ name, text }, fallback = name) => { * @param {import('@types/mdast').Heading} heading - The heading node with metadata. * @param {number} idx - The index at which the heading occurs in `parent.children`. */ -export default ({ children }, { data }, idx) => { +export const insertSignatureCodeBlock = ({ children }, { data }, idx) => { // Try to locate the parameter list immediately following the heading const listIdx = children.findIndex(createQueries.UNIST.isStronglyTypedList); @@ -127,3 +130,18 @@ export default ({ children }, { data }, idx) => { ) ); }; + +/** + * Renders a table of properties based on parsed metadata from a Markdown list. + * + * @param {import('mdast').List} node + * @param {import('unified').Processor} remark - The remark processor + */ +export const createSignatureTable = (node, remark) => { + const items = parseListIntoProperties(node, remark); + + return createJSXElement(JSX_IMPORTS.FunctionSignature.name, { + title: items.length > 1 && 'kind' in items[0] ? 'Attributes' : undefined, + items, + }); +}; diff --git a/src/generators/jsx-ast/utils/types.mjs b/src/generators/jsx-ast/utils/types.mjs new file mode 100644 index 00000000..3039b17a --- /dev/null +++ b/src/generators/jsx-ast/utils/types.mjs @@ -0,0 +1,155 @@ +import { u as createTree } from 'unist-builder'; + +import createQueries from '../../../utils/queries/index.mjs'; +import { transformNodesToString } from '../../../utils/unist.mjs'; +import { DEFAULT_EXPRESSION } from '../../legacy-json/constants.mjs'; +import { TRIMMABLE_PADDING_REGEX } from '../constants.mjs'; + +const { QUERIES, UNIST } = createQueries; + +/** + * Checks if the node is a union separator (`' | '`) or a type reference + * (a link wrapping `` inline code). + * + * @param {import('mdast').Node} node + * @returns {0 | 1 | 2} 0 = not type-related, 1 = type ref, 2 = union separator + */ +export const classifyTypeNode = node => { + if (node.type === 'text' && node.value === ' | ') { + return 2; + } + + const child = node.children?.[0]; + if ( + node.type === 'link' && + child?.type === 'inlineCode' && + child.value?.startsWith('<') + ) { + return 1; + } + + return 0; +}; + +/** + * Removes and returns the leading node if it's blank text. + * + * @param {Array} nodes + */ +export const shiftIfBlankText = nodes => { + if (nodes[0]?.type === 'text' && !nodes[0].value.trim()) { + nodes.shift(); + } +}; + +/** + * Extracts a property name from the front of a paragraph's children. + * Mutates `nodes` by shifting consumed nodes and updates the current object. + * + * @param {Array} nodes + * @param {Object} current - The current property object being built + */ +export const extractPropertyName = (nodes, current) => { + const first = nodes[0]; + if (!first) { + return; + } + + // `propName` → propName + if (first.type === 'inlineCode') { + nodes.shift(); + current.name = first.value.trimEnd(); + return; + } + + if (first.type !== 'text') { + return; + } + + // "Type:" / "Param:" etc. + const match = first.value.match(QUERIES.typedListStarters); + if (!match) { + return; + } + + // Consume the matched prefix; drop the node entirely if nothing remains + first.value = first.value.slice(match[0].length); + shiftIfBlankText(nodes); + + if (match[1]) { + current.name = match[1]; + // NOTE: We currently only have one "kind". Should others be added for other + // starters, just replace the `undefined` with the other kinds. + current.kind = match[1] === 'Returns' ? 'return' : undefined; + } +}; + +/** + * Consumes consecutive type-annotation nodes (type refs and union + * separators) from the front of `nodes` and updates the current object. + * + * @param {Array} nodes + * @param {import('unified').Processor} remark - The remark processor + */ +export const extractTypeAnnotations = (nodes, remark) => { + const types = []; + + while (nodes.length) { + const kind = classifyTypeNode(nodes[0]); + if (kind === 0) { + break; + } + + types.push(nodes.shift()); + + // A union separator implies another type follows + if (kind === 2 && nodes.length) { + types.push(nodes.shift()); + } + } + + if (types.length > 0) { + return remark.runSync(createTree('root', types)).body[0].expression; + } +}; + +/** + * Parses each list item into a structured property descriptor + * + * @param {import('mdast').List} node + * @param {import('unified').Processor} remark - The remark processor + */ +export const parseListIntoProperties = (node, remark) => + node?.children.map(item => { + const [{ children }, ...rest] = item.children; + const current = {}; + + extractPropertyName(children, current); + + // Strip stale whitespace left over after name extraction + shiftIfBlankText(children); + + current.type = extractTypeAnnotations(children, remark); + + if (children.length > 0) { + children[0].value &&= children[0].value.replace( + TRIMMABLE_PADDING_REGEX, + '' + ); + + current.optional = DEFAULT_EXPRESSION.test( + transformNodesToString(children) + ); + + current.description = remark.runSync( + createTree('root', children) + ).body[0].expression; + } + + current.children = parseListIntoProperties( + rest.find(UNIST.isLooselyTypedList), + remark + ); + + return current; + }); diff --git a/src/generators/web/constants.mjs b/src/generators/web/constants.mjs index 44efdabb..82632a3a 100644 --- a/src/generators/web/constants.mjs +++ b/src/generators/web/constants.mjs @@ -82,6 +82,10 @@ export const JSX_IMPORTS = { name: 'DataTag', source: '@node-core/ui-components/Common/DataTag', }, + FunctionSignature: { + name: 'FunctionSignature', + source: '@node-core/ui-components/Containers/FunctionSignature', + }, ArrowUpRightIcon: { name: 'ArrowUpRightIcon', source: '@heroicons/react/24/solid/ArrowUpRightIcon', diff --git a/src/generators/web/ui/index.css b/src/generators/web/ui/index.css index 3b0deb3e..3aa44b70 100644 --- a/src/generators/web/ui/index.css +++ b/src/generators/web/ui/index.css @@ -16,33 +16,6 @@ main { word-break: break-word; } - /* Don't overflow the parent */ - .overflow-container { - overflow-x: auto; - width: 100%; - table td a + a { - margin-right: 0; - } - } - - table { - td { - word-break: break-all; - } - - /* In tables, don't pad `a` elements */ - a { - padding-right: unset; - } - - @media (min-width: 1024px) { - td code { - overflow-wrap: normal; - word-break: normal; - } - } - } - /* Change history positioning */ div:has(> h1, > h2, > h3, > h4, > h5, > h6) { display: flex;