From ce1b439704f15db35429c32b7089818c89d3af76 Mon Sep 17 00:00:00 2001 From: Andre Light <7719153+lightmotive@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:07:00 -0600 Subject: [PATCH] test: add vitest harness with pure function extraction for link builders --- package-lock.json | 1266 ++++++++++++++++++++++++++++++++++++--- package.json | 7 +- src/linkBuilder.test.ts | 704 ++++++++++++++++++++++ src/linkBuilder.ts | 195 ++++++ src/main.ts | 185 ++---- vitest.config.ts | 7 + 6 files changed, 2120 insertions(+), 244 deletions(-) create mode 100644 src/linkBuilder.test.ts create mode 100644 src/linkBuilder.ts create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index 847aa8b..c962709 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-sample-plugin", - "version": "1.5.1", + "version": "1.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-sample-plugin", - "version": "1.5.1", + "version": "1.5.2", "license": "MIT", "dependencies": { "dotenv": "^16.6.1" @@ -22,32 +22,42 @@ "eslint-plugin-obsidianmd": "^0.1.9", "obsidian": "latest", "tslib": "2.4.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.4" } }, - "node_modules/@codemirror/state": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.0.tgz", - "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "dev": true, "license": "MIT", - "peer": true, + "optional": true, "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@codemirror/view": { - "version": "6.38.6", - "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.38.6.tgz", - "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "dev": true, "license": "MIT", - "peer": true, + "optional": true, "dependencies": { - "@codemirror/state": "^6.5.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@esbuild/android-arm": { @@ -603,23 +613,6 @@ "url": "https://eslint.org/donate" } }, - "node_modules/@eslint/json": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/json/-/json-0.14.0.tgz", - "integrity": "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@eslint/core": "^0.17.0", - "@eslint/plugin-kit": "^0.4.1", - "@humanwhocodes/momoa": "^3.3.10", - "natural-compare": "^1.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@eslint/object-schema": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", @@ -682,17 +675,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/momoa": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.10.tgz", - "integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=18" - } - }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", @@ -707,13 +689,12 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@microsoft/eslint-plugin-sdl": { "version": "1.1.0", @@ -753,6 +734,35 @@ "ret": "~0.1.10" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgr/core": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.2.tgz", @@ -766,6 +776,270 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "dev": true, + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -773,6 +1047,35 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/codemirror": { "version": "5.60.8", "resolved": "https://registry.npmmirror.com/@types/codemirror/-/codemirror-5.60.8.tgz", @@ -783,6 +1086,13 @@ "@types/tern": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "8.56.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", @@ -1065,6 +1375,119 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitest/expect": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1288,6 +1711,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -1410,6 +1843,16 @@ "node": ">=6" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1454,13 +1897,12 @@ "dev": true, "license": "MIT" }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -1592,6 +2034,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -1774,6 +2226,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2605,6 +3064,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2615,6 +3084,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2738,6 +3217,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "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", @@ -3647,6 +4141,267 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3683,6 +4438,16 @@ "loose-envify": "cli.js" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3743,6 +4508,25 @@ "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3917,6 +4701,17 @@ "@codemirror/view": "6.38.6" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4025,6 +4820,20 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -4048,6 +4857,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4205,6 +5043,40 @@ "node": ">=0.12" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -4452,6 +5324,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -4587,14 +5490,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/style-mod": { - "version": "4.1.3", - "resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.3.tgz", - "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4659,6 +5554,23 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -4676,6 +5588,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/toml-eslint-parser": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/toml-eslint-parser/-/toml-eslint-parser-0.9.3.tgz", @@ -4843,31 +5765,6 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", - "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.58.0", - "@typescript-eslint/parser": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0", - "@typescript-eslint/utils": "8.58.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -4904,13 +5801,173 @@ "punycode": "^2.1.0" } }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "node_modules/vite": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.15", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } }, "node_modules/which": { "version": "2.0.2", @@ -5017,6 +6074,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 1f218ca..722572c 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "release": "node scripts/release-tag.mjs", "bump": "node scripts/bump.mjs", "lint": "eslint src/ --ext .ts", - "lint:fix": "eslint src/ --ext .ts --fix" + "lint:fix": "eslint src/ --ext .ts --fix", + "test": "vitest run", + "test:watch": "vitest" }, "keywords": [], "author": "", @@ -28,7 +30,8 @@ "eslint-plugin-obsidianmd": "^0.1.9", "obsidian": "latest", "tslib": "2.4.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.4" }, "dependencies": { "dotenv": "^16.6.1" diff --git a/src/linkBuilder.test.ts b/src/linkBuilder.test.ts new file mode 100644 index 0000000..2a97504 --- /dev/null +++ b/src/linkBuilder.test.ts @@ -0,0 +1,704 @@ +import { describe, it, expect } from 'vitest'; +import { buildHeadingLink, buildBlockLink, buildFileLink, extractBlockDisplayText, encodeMarkdownLinkUrl } from './linkBuilder'; +import { LinkFormat } from './type'; + +// --------------------------------------------------------------------------- +// encodeMarkdownLinkUrl +// --------------------------------------------------------------------------- + +describe('encodeMarkdownLinkUrl', () => { + it('encodes spaces as %20', () => { + expect(encodeMarkdownLinkUrl('My Note')).toBe('My%20Note'); + }); + + it('encodes multiple spaces', () => { + expect(encodeMarkdownLinkUrl('a b c')).toBe('a%20b%20c'); + }); + + it('returns string unchanged when no spaces', () => { + expect(encodeMarkdownLinkUrl('MyNote#Heading')).toBe('MyNote#Heading'); + }); + + it('does not encode other special characters', () => { + expect(encodeMarkdownLinkUrl('Note#Head^id')).toBe('Note#Head^id'); + }); +}); + +// --------------------------------------------------------------------------- +// buildHeadingLink +// --------------------------------------------------------------------------- + +describe('buildHeadingLink', () => { + const defaults = { + linkFormat: LinkFormat.WIKILINK, + useHeadingAsDisplayText: true, + headingLinkSeparator: '#', + }; + + // -- Wiki format --------------------------------------------------------- + + describe('wiki format', () => { + it('creates a basic heading link with display text', () => { + const result = buildHeadingLink({ + ...defaults, + heading: 'Getting Started', + filename: 'MyNote', + }); + expect(result.link).toBe('[[MyNote#Getting Started|Getting Started]]'); + expect(result.isNoteLink).toBe(false); + }); + + it('simplifies to [[filename]] when heading equals filename exactly', () => { + const result = buildHeadingLink({ + ...defaults, + heading: 'MyNote', + filename: 'MyNote', + }); + expect(result.link).toBe('[[MyNote]]'); + expect(result.isNoteLink).toBe(true); + }); + + it('simplifies linkContent when heading matches filename case-insensitively', () => { + const result = buildHeadingLink({ + ...defaults, + heading: 'mynote', + filename: 'MyNote', + }); + // linkContent simplified to filename, but since filename !== heading, + // it falls to the else branch with displayText vs linkContent + expect(result.link).toBe('[[MyNote|mynote]]'); + expect(result.isNoteLink).toBe(true); + }); + + it('simplifies when heading matches filename with spaces removed', () => { + // e.g. filename "SomeThing" heading "Some Thing" + const result = buildHeadingLink({ + ...defaults, + heading: 'Some Thing', + filename: 'SomeThing', + }); + expect(result.link).toBe('[[SomeThing|Some Thing]]'); + expect(result.isNoteLink).toBe(true); + }); + + it('omits display text alias when displayText equals linkContent', () => { + // When useHeadingAsDisplayText is false AND separator is '#', + // displayText = "filename#heading" which equals linkContent + const result = buildHeadingLink({ + ...defaults, + heading: 'Intro', + filename: 'MyNote', + useHeadingAsDisplayText: false, + headingLinkSeparator: '#', + }); + expect(result.link).toBe('[[MyNote#Intro]]'); + }); + + it('includes alias when using non-default separator', () => { + const result = buildHeadingLink({ + ...defaults, + heading: 'Intro', + filename: 'MyNote', + useHeadingAsDisplayText: false, + headingLinkSeparator: ' > ', + }); + expect(result.link).toBe('[[MyNote#Intro|MyNote > Intro]]'); + }); + + it('uses frontmatter title in display text when heading-as-display is off', () => { + const result = buildHeadingLink({ + ...defaults, + heading: 'Setup', + filename: 'my-note', + frontmatterTitle: 'My Note', + useHeadingAsDisplayText: false, + headingLinkSeparator: '#', + }); + // displayText = "My Note#Setup", linkContent = "my-note#Setup" + expect(result.link).toBe('[[my-note#Setup|My Note#Setup]]'); + }); + + it('strips [[ ]] wrapper from heading', () => { + const result = buildHeadingLink({ + ...defaults, + heading: '[[Inner Link]]', + filename: 'MyNote', + }); + expect(result.link).toBe('[[MyNote#Inner Link|Inner Link]]'); + }); + + it('falls back separator to # when headingLinkSeparator is empty', () => { + const result = buildHeadingLink({ + ...defaults, + heading: 'Intro', + filename: 'MyNote', + useHeadingAsDisplayText: false, + headingLinkSeparator: '', + }); + expect(result.link).toBe('[[MyNote#Intro]]'); + }); + }); + + // -- Markdown format ------------------------------------------------------ + + describe('markdown format', () => { + const md = { ...defaults, linkFormat: LinkFormat.MDLINK }; + + it('creates a basic markdown heading link', () => { + const result = buildHeadingLink({ + ...md, + heading: 'Getting Started', + filename: 'MyNote', + }); + expect(result.link).toBe('[Getting Started](MyNote#Getting%20Started)'); + }); + + it('always includes #heading even when filename matches heading', () => { + const result = buildHeadingLink({ + ...md, + heading: 'MyNote', + filename: 'MyNote', + }); + // Markdown format always uses filename#heading; no spaces so no encoding + expect(result.link).toBe('[MyNote](MyNote#MyNote)'); + expect(result.isNoteLink).toBe(true); + }); + + it('uses filename+separator+heading when heading-as-display is off', () => { + const result = buildHeadingLink({ + ...md, + heading: 'Intro', + filename: 'MyNote', + useHeadingAsDisplayText: false, + headingLinkSeparator: ' - ', + }); + // Display text is not encoded, only the URL portion is + expect(result.link).toBe('[MyNote - Intro](MyNote#Intro)'); + }); + + it('uses frontmatter title in display text', () => { + const result = buildHeadingLink({ + ...md, + heading: 'Setup', + filename: 'my-note', + frontmatterTitle: 'My Note', + useHeadingAsDisplayText: false, + headingLinkSeparator: '#', + }); + // No spaces in filename or heading, so no encoding needed + expect(result.link).toBe('[My Note#Setup](my-note#Setup)'); + }); + }); + + // -- compareIgnoreCase `includes` behavior -------------------------------- + // BUG: compareIgnoreCase uses filename.includes(heading), which causes + // false-positive note-link simplification when the filename merely contains + // the heading as a substring. Should be fixed in a future PR (invert these + // tests when fixing). + + describe('filename-heading substring matching (known bug)', () => { + it('false-positive: simplifies when filename contains heading as substring', () => { + // BUG: "JavaScript".includes("Java") → true, incorrectly simplifies. + // Expected correct behavior: should NOT simplify (not an exact or + // derived-name match). + const result = buildHeadingLink({ + ...defaults, + heading: 'Java', + filename: 'JavaScript', + }); + expect(result.link).toBe('[[JavaScript|Java]]'); + expect(result.isNoteLink).toBe(true); + }); + + it('does NOT simplify when heading contains filename as substring', () => { + // The includes check is directional: filename.includes(heading) + // "Note".includes("Notebook") → false + const result = buildHeadingLink({ + ...defaults, + heading: 'Notebook', + filename: 'Note', + }); + expect(result.link).toBe('[[Note#Notebook|Notebook]]'); + expect(result.isNoteLink).toBe(false); + }); + + it('false-positive: simplifies on space-removed substring match', () => { + // BUG: "SomeThingElse".includes("SomeThing") → true, incorrectly + // simplifies. The intended behavior (SomeThing/Some Thing) works, + // but non-exact substring matches like this are false positives. + const result = buildHeadingLink({ + ...defaults, + heading: 'Some Thing', + filename: 'SomeThingElse', + }); + expect(result.link).toBe('[[SomeThingElse|Some Thing]]'); + expect(result.isNoteLink).toBe(true); + }); + }); + + // -- Combinatorial -------------------------------------------------------- + + describe('combinatorial', () => { + it('frontmatterTitle has no effect when useHeadingAsDisplayText is true', () => { + const result = buildHeadingLink({ + ...defaults, + heading: 'Setup', + filename: 'my-note', + frontmatterTitle: 'My Custom Title', + useHeadingAsDisplayText: true, + }); + // Display text is the heading, not the frontmatter title + expect(result.link).toBe('[[my-note#Setup|Setup]]'); + }); + + it('frontmatterTitle + note-link simplification (wiki)', () => { + const result = buildHeadingLink({ + ...defaults, + heading: 'myNote', + filename: 'MyNote', + frontmatterTitle: 'My Pretty Title', + useHeadingAsDisplayText: false, + headingLinkSeparator: '#', + }); + // linkContent simplified to "MyNote", display = "My Pretty Title#myNote" + expect(result.link).toBe('[[MyNote|My Pretty Title#myNote]]'); + expect(result.isNoteLink).toBe(true); + }); + + it('empty heading triggers false note-link simplification (known bug)', () => { + // BUG: ''.includes('') is always true in JS, so compareIgnoreCase + // falsely matches any filename. Should produce a heading link with + // an empty fragment instead. Fix alongside compareIgnoreCase in a + // future PR. + const result = buildHeadingLink({ + ...defaults, + heading: '', + filename: 'MyNote', + }); + expect(result.link).toBe('[[MyNote|]]'); + expect(result.isNoteLink).toBe(true); + }); + + it('markdown format URL-encodes spaces in link URL', () => { + const result = buildHeadingLink({ + ...defaults, + linkFormat: LinkFormat.MDLINK, + heading: 'My Heading', + filename: 'My Note', + }); + expect(result.link).toBe('[My Heading](My%20Note#My%20Heading)'); + }); + }); +}); + +// --------------------------------------------------------------------------- +// extractBlockDisplayText +// --------------------------------------------------------------------------- + +describe('extractBlockDisplayText', () => { + describe('text cleaning', () => { + it('removes block ID suffix (^...)', () => { + const result = extractBlockDisplayText('Some text ^abc123', 'fallback', 3, 5); + expect(result).toBe('Some text'); + }); + + it('removes checkbox prefix (- [x] )', () => { + const result = extractBlockDisplayText('- [x] Task done', 'fallback', 3, 5); + expect(result).toBe('Task done'); + }); + + it('removes list prefix (- )', () => { + const result = extractBlockDisplayText('- List item here today', 'fallback', 3, 5); + expect(result).toBe('List item here'); + }); + + it('removes markdown syntax characters', () => { + const result = extractBlockDisplayText('**bold** and `code`', 'fallback', 5, 5); + expect(result).toBe('bold and code'); + }); + + it('returns blockId for empty/whitespace-only text', () => { + expect(extractBlockDisplayText('', 'myId', 3, 5)).toBe('myId'); + expect(extractBlockDisplayText(' ', 'myId', 3, 5)).toBe('myId'); + }); + + it('removes callout prefix (> )', () => { + const result = extractBlockDisplayText('> Some quoted text here', 'fallback', 3, 5); + expect(result).toBe('Some quoted text'); + }); + }); + + describe('English text', () => { + it('extracts first 3 words by default', () => { + const result = extractBlockDisplayText('The quick brown fox jumps', 'fallback', 3, 5); + expect(result).toBe('The quick brown'); + }); + + it('respects custom word limit', () => { + const result = extractBlockDisplayText('The quick brown fox jumps', 'fallback', 5, 5); + expect(result).toBe('The quick brown fox jumps'); + }); + + it('handles text with fewer words than limit', () => { + const result = extractBlockDisplayText('Hello world', 'fallback', 3, 5); + expect(result).toBe('Hello world'); + }); + + it('handles single word', () => { + const result = extractBlockDisplayText('Hello', 'fallback', 3, 5); + expect(result).toBe('Hello'); + }); + + it('defaults word limit to 3 if zero is passed', () => { + const result = extractBlockDisplayText('one two three four', 'fallback', 0, 5); + expect(result).toBe('one two three'); + }); + }); + + describe('CJK text', () => { + it('returns text as-is when within char limit', () => { + const result = extractBlockDisplayText('Hello', 'fallback', 3, 5); + // "Hello" is ASCII / English, so it hits the English branch + expect(result).toBe('Hello'); + }); + + it('truncates with ellipsis when text exceeds char limit and no separator found', () => { + const result = extractBlockDisplayText('这是一段很长的中文文本没有标点', 'fallback', 3, 5); + expect(result).toBe('这是一段很...'); + }); + + it('uses punctuation as separator when match is 3+ chars', () => { + // '三个字' is 3 chars (>= 3 and <= charLimit), so separator match is used + const result = extractBlockDisplayText('三个字,后面还有很多内容', 'fallback', 3, 5); + expect(result).toBe('三个字'); + }); + + it('falls back to char limit slice when separator match is too short (< 3 chars)', () => { + // '短语' is 2 chars (< 3), falls back to text.slice(0, charLimit) + const result = extractBlockDisplayText('短语,后面还有很多内容', 'fallback', 3, 5); + expect(result).toBe('短语,后面'); + }); + + it('returns full text when within char limit', () => { + const result = extractBlockDisplayText('短い文', 'fallback', 3, 5); + expect(result).toBe('短い文'); + }); + + it('defaults char limit to 5 if zero is passed', () => { + const result = extractBlockDisplayText('这是一段很长的中文文本', 'fallback', 3, 0); + expect(result).toBe('这是一段很...'); + }); + }); + + // -- Boundary values ------------------------------------------------------ + + describe('boundary values', () => { + it('English: exactly wordLimit words returns all words', () => { + const result = extractBlockDisplayText('one two three', 'fallback', 3, 5); + expect(result).toBe('one two three'); + }); + + it('English: wordLimit + 1 words truncates', () => { + const result = extractBlockDisplayText('one two three four', 'fallback', 3, 5); + expect(result).toBe('one two three'); + }); + + it('English: word limit of 1', () => { + const result = extractBlockDisplayText('Hello world', 'fallback', 1, 5); + expect(result).toBe('Hello'); + }); + + it('CJK: exactly charLimit chars returns text as-is', () => { + const result = extractBlockDisplayText('这是一段文', 'fallback', 3, 5); + expect(result).toBe('这是一段文'); + }); + + it('CJK: charLimit + 1 chars with no separator triggers truncation', () => { + const result = extractBlockDisplayText('这是一段文本', 'fallback', 3, 5); + expect(result).toBe('这是一段文...'); + }); + + it('CJK: char limit of 1 truncates with ellipsis', () => { + const result = extractBlockDisplayText('很长的文本', 'fallback', 3, 1); + expect(result).toBe('很...'); + }); + }); + + // -- Mixed content -------------------------------------------------------- + + describe('mixed content', () => { + it('mixed English/CJK is treated as non-English', () => { + // The ASCII regex fails if any non-ASCII char is present + const result = extractBlockDisplayText('Hello 世界 and more text', 'fallback', 3, 5); + expect(result).toBe('Hello'); + }); + + it('text that is all markdown syntax becomes empty after cleaning', () => { + const result = extractBlockDisplayText('**[]**', 'myId', 3, 5); + expect(result).toBe('myId'); + }); + + it('checkbox with different state markers', () => { + expect(extractBlockDisplayText('- [>] Deferred task here', 'id', 3, 5)) + .toBe('Deferred task here'); + expect(extractBlockDisplayText('- [ ] Unchecked task here', 'id', 3, 5)) + .toBe('Unchecked task here'); + }); + }); +}); + +// --------------------------------------------------------------------------- +// buildBlockLink +// --------------------------------------------------------------------------- + +describe('buildBlockLink', () => { + const defaults = { + filename: 'MyNote', + useBrief: true, + firstLine: 'The quick brown fox', + linkFormat: LinkFormat.WIKILINK, + autoBlockDisplayText: true, + autoEmbedBlockLink: false, + blockDisplayWordLimit: 3, + blockDisplayCharLimit: 5, + }; + + describe('wiki format', () => { + it('creates a block link with brief display text', () => { + const result = buildBlockLink({ ...defaults, blockId: 'abc123' }); + expect(result).toBe('[[MyNote#^abc123|The quick brown]]'); + }); + + it('uses blockId as display text when useBrief is false', () => { + const result = buildBlockLink({ ...defaults, blockId: 'abc123', useBrief: false }); + expect(result).toBe('[[MyNote#^abc123|abc123]]'); + }); + + it('omits display text when autoBlockDisplayText is false', () => { + const result = buildBlockLink({ + ...defaults, + blockId: 'abc123', + autoBlockDisplayText: false, + }); + expect(result).toBe('[[MyNote#^abc123]]'); + }); + + it('adds ! embed prefix', () => { + const result = buildBlockLink({ + ...defaults, + blockId: 'abc123', + autoEmbedBlockLink: true, + }); + expect(result).toBe('![[MyNote#^abc123|The quick brown]]'); + }); + }); + + describe('markdown format', () => { + const md = { ...defaults, linkFormat: LinkFormat.MDLINK }; + + it('creates a markdown block link with display text', () => { + const result = buildBlockLink({ ...md, blockId: 'abc123' }); + expect(result).toBe('[The quick brown](MyNote#^abc123)'); + }); + + it('omits display text when autoBlockDisplayText is false', () => { + const result = buildBlockLink({ + ...md, + blockId: 'abc123', + autoBlockDisplayText: false, + }); + expect(result).toBe('[](MyNote#^abc123)'); + }); + + it('adds ! embed prefix in markdown format', () => { + const result = buildBlockLink({ + ...md, + blockId: 'abc123', + autoEmbedBlockLink: true, + }); + expect(result).toBe('![The quick brown](MyNote#^abc123)'); + }); + }); + + it('uses blockId as display when firstLine is empty', () => { + const result = buildBlockLink({ + ...defaults, + blockId: 'abc123', + firstLine: '', + }); + expect(result).toBe('[[MyNote#^abc123|abc123]]'); + }); + + // -- Combinatorial -------------------------------------------------------- + + describe('combinatorial', () => { + it('embed + no display text (wiki) produces ![[...]]', () => { + const result = buildBlockLink({ + ...defaults, + blockId: 'abc123', + autoBlockDisplayText: false, + autoEmbedBlockLink: true, + }); + expect(result).toBe('![[MyNote#^abc123]]'); + }); + + it('embed + no display text (markdown) produces image-like syntax', () => { + // ![](path) is technically an image embed in standard markdown — + // documenting that this combo produces that syntax + const result = buildBlockLink({ + ...defaults, + linkFormat: LinkFormat.MDLINK, + blockId: 'abc123', + autoBlockDisplayText: false, + autoEmbedBlockLink: true, + }); + expect(result).toBe('![](MyNote#^abc123)'); + }); + + it('block ID with hyphens and underscores', () => { + const result = buildBlockLink({ + ...defaults, + blockId: 'quote-of_the-day', + }); + expect(result).toBe('[[MyNote#^quote-of_the-day|The quick brown]]'); + }); + + it('markdown format encodes spaces in filename', () => { + const result = buildBlockLink({ + ...defaults, + linkFormat: LinkFormat.MDLINK, + blockId: 'abc123', + filename: 'My Note', + }); + expect(result).toBe('[The quick brown](My%20Note#^abc123)'); + }); + + it('wiki format does NOT encode spaces in filename', () => { + const result = buildBlockLink({ + ...defaults, + blockId: 'abc123', + filename: 'My Note', + }); + expect(result).toBe('[[My Note#^abc123|The quick brown]]'); + }); + }); +}); + +// --------------------------------------------------------------------------- +// buildFileLink +// --------------------------------------------------------------------------- + +describe('buildFileLink', () => { + describe('wiki format', () => { + it('creates a wiki file link', () => { + const result = buildFileLink({ + filename: 'MyNote', + filePath: 'folder/MyNote.md', + linkFormat: LinkFormat.WIKILINK, + }); + expect(result).toBe('[[MyNote|MyNote]]'); + }); + + it('uses custom display text', () => { + const result = buildFileLink({ + filename: 'my-note', + filePath: 'folder/my-note.md', + displayText: 'My Note', + linkFormat: LinkFormat.WIKILINK, + }); + expect(result).toBe('[[my-note|My Note]]'); + }); + }); + + describe('markdown format', () => { + it('creates a markdown file link and strips .md', () => { + const result = buildFileLink({ + filename: 'MyNote', + filePath: 'folder/MyNote.md', + linkFormat: LinkFormat.MDLINK, + }); + expect(result).toBe('[MyNote](folder/MyNote)'); + }); + + it('normalizes backslashes in path', () => { + const result = buildFileLink({ + filename: 'MyNote', + filePath: 'folder\\subfolder\\MyNote.md', + linkFormat: LinkFormat.MDLINK, + }); + expect(result).toBe('[MyNote](folder/subfolder/MyNote)'); + }); + + it('uses custom display text', () => { + const result = buildFileLink({ + filename: 'my-note', + filePath: 'folder/my-note.md', + displayText: 'My Note', + linkFormat: LinkFormat.MDLINK, + }); + expect(result).toBe('[My Note](folder/my-note)'); + }); + + it('handles paths without .md extension', () => { + const result = buildFileLink({ + filename: 'MyNote', + filePath: 'folder/MyNote', + linkFormat: LinkFormat.MDLINK, + }); + expect(result).toBe('[MyNote](folder/MyNote)'); + }); + + it('encodes spaces as %20 in path', () => { + const result = buildFileLink({ + filename: 'My Note', + filePath: 'my folder/My Note.md', + linkFormat: LinkFormat.MDLINK, + }); + expect(result).toBe('[My Note](my%20folder/My%20Note)'); + }); + + it('root-level file with no directory prefix', () => { + const result = buildFileLink({ + filename: 'MyNote', + filePath: 'MyNote.md', + linkFormat: LinkFormat.MDLINK, + }); + expect(result).toBe('[MyNote](MyNote)'); + }); + }); + + // -- Edge cases ----------------------------------------------------------- + + describe('edge cases', () => { + it('wiki format without displayText produces redundant alias', () => { + // [[name|name]] — the alias duplicates the filename + const result = buildFileLink({ + filename: 'MyNote', + filePath: 'folder/MyNote.md', + linkFormat: LinkFormat.WIKILINK, + }); + expect(result).toBe('[[MyNote|MyNote]]'); + }); + + it('filename with parentheses in markdown format', () => { + // Parentheses can break [text](path) syntax in strict parsers + const result = buildFileLink({ + filename: 'Note (draft)', + filePath: 'folder/Note (draft).md', + linkFormat: LinkFormat.MDLINK, + }); + expect(result).toBe('[Note (draft)](folder/Note%20(draft))'); + }); + + it('filename with pipe in wiki format', () => { + // | is the alias separator — pipe in filename creates ambiguity + const result = buildFileLink({ + filename: 'Pros | Cons', + filePath: 'Pros | Cons.md', + displayText: 'Comparison', + linkFormat: LinkFormat.WIKILINK, + }); + expect(result).toBe('[[Pros | Cons|Comparison]]'); + }); + }); +}); diff --git a/src/linkBuilder.ts b/src/linkBuilder.ts new file mode 100644 index 0000000..e050031 --- /dev/null +++ b/src/linkBuilder.ts @@ -0,0 +1,195 @@ +import { LinkFormat } from './type'; + +// --- URL Encoding --- + +export function encodeMarkdownLinkUrl(url: string): string { + return url.replace(/ /g, '%20'); +} + +// --- Heading Link --- + +export interface BuildHeadingLinkOptions { + heading: string; + filename: string; + frontmatterTitle?: string; + linkFormat: LinkFormat; + useHeadingAsDisplayText: boolean; + headingLinkSeparator: string; +} + +export interface BuildHeadingLinkResult { + link: string; + isNoteLink: boolean; +} + +export function buildHeadingLink(options: BuildHeadingLinkOptions): BuildHeadingLinkResult { + const { filename, linkFormat, useHeadingAsDisplayText, headingLinkSeparator } = options; + const filenameOrTitle = options.frontmatterTitle || filename; + + // 提取标题文本和级别 + // 如果内容是[[内容]],移除[[]] + let selectedHeading = options.heading; + if (selectedHeading.startsWith('[[') && selectedHeading.endsWith(']]')) { + selectedHeading = selectedHeading.slice(2, -2); + } + + // 根据设置决定显示文本 + let displayText = selectedHeading; + if (!useHeadingAsDisplayText) { + // 如果不使用标题作为显示文本,则使用"文件名{连接符}标题名"格式 + const separator = headingLinkSeparator || '#'; + displayText = `${filenameOrTitle}${separator}${selectedHeading}`; + } + + let linkContent = `${filename}#${selectedHeading}`; + let isNoteLink = false; + + const compareIgnoreCase = (a: string, b: string): boolean => + a.toLowerCase() === b.toLowerCase() || a.toLowerCase().includes(b.toLowerCase()); + + // 特殊情况:如果文件名包含标题,则不添加指向标题的 # 部分 + // 我自己的情况——会把 SomeThing 给拆成 Some Thing 来做标题,所以也考虑空格替换的部分 + if ( + filename === selectedHeading || + compareIgnoreCase(filename, selectedHeading) || + compareIgnoreCase(filename, selectedHeading.replace(/\s+/g, '')) + ) { + linkContent = filename; + isNoteLink = true; + } + + let link = ''; + + // 根据设置选择链接格式 + if (linkFormat === LinkFormat.WIKILINK) { + // Wiki链接格式 + if (filename === selectedHeading) { + // 特殊情况:当文件名与标题相同时,直接链接到文件 + link = `[[${filename}]]`; + isNoteLink = true; + } else { + if (displayText === linkContent) { + // 特殊情况:当显示文本与 "文件名#标题" 相同时,省略显示文本 + link = `[[${linkContent}]]`; + } else { + link = `[[${linkContent}|${displayText}]]`; + } + } + } else { + // Markdown链接格式 + link = `[${displayText}](${encodeMarkdownLinkUrl(`${filename}#${selectedHeading}`)})`; + } + + return { link, isNoteLink }; +} + +// --- Block Display Text --- + +export function extractBlockDisplayText( + firstLine: string, + blockId: string, + wordLimit: number, + charLimit: number, +): string { + let text = firstLine; + // 先去掉结尾的 ^ 及其后面的内容(如果有的话) + text = text.replace(/\^.*\s*$/, ''); + text = text.trim().replace(/- \[.\]\s+/, '').replace('- ', '').replace(/=|\*|\[|\]|\(|\)|`|>\s+/g, ''); + + if (!text) return blockId; + + // 判断是否是纯英文,如果是纯英文(以及英文常用标点符号),提取前几个单词;否则,按下面的逻辑处理 + // 根据 ASCII 来判断"英文" + const isEnglish = /^[a-zA-Z\s,.!?"()[\]_^-~:;0-9]*$/.test(text); + + if (isEnglish) { + const limit = wordLimit || 3; + return text.trim().split(' ').slice(0, limit).join(' '); + } + + // CJK / 非英文语言 + const limit = charLimit || 5; + + if (text.length > limit) { + const separated = text.trim().match(/(\S+?)[~,.\-=[,。?!…:\n\s]/); + const tempText = separated ? separated[1] : text; + + if (tempText.length > limit) { + return tempText.slice(0, limit) + '...'; + } else if (tempText.length < 3) { + return text.slice(0, limit); + } else { + return tempText; + } + } + + return text; +} + +// --- Block Link --- + +export interface BuildBlockLinkOptions { + blockId: string; + filename: string; + useBrief: boolean; + firstLine: string; + linkFormat: LinkFormat; + autoBlockDisplayText: boolean; + autoEmbedBlockLink: boolean; + blockDisplayWordLimit: number; + blockDisplayCharLimit: number; +} + +export function buildBlockLink(options: BuildBlockLinkOptions): string { + const { + blockId, filename, useBrief, firstLine, + linkFormat, autoBlockDisplayText, autoEmbedBlockLink, + blockDisplayWordLimit, blockDisplayCharLimit, + } = options; + + let displayText = blockId; + if (useBrief && firstLine) { + displayText = extractBlockDisplayText(firstLine, blockId, blockDisplayWordLimit, blockDisplayCharLimit); + } + + let link: string; + + if (autoBlockDisplayText) { + link = linkFormat === LinkFormat.WIKILINK + ? `[[${filename}#^${blockId}|${displayText}]]` + : `[${displayText}](${encodeMarkdownLinkUrl(filename)}#^${blockId})`; // markdown 格式不能加 ^,不然会变成内联脚注语法 [^xxx] + } else { + link = linkFormat === LinkFormat.WIKILINK + ? `[[${filename}#^${blockId}]]` + : `[](${encodeMarkdownLinkUrl(filename)}#^${blockId})`; + } + + // 自动生成嵌入块 + if (autoEmbedBlockLink) { + link = '!' + link; + } + + return link; +} + +// --- File Link --- + +export interface BuildFileLinkOptions { + filename: string; + filePath: string; + displayText?: string; + linkFormat: LinkFormat; +} + +export function buildFileLink(options: BuildFileLinkOptions): string { + const { filename, linkFormat } = options; + const display = options.displayText || filename; + + if (linkFormat === LinkFormat.WIKILINK) { + return `[[${filename}|${display}]]`; + } + + let path = options.filePath.replace(/\\/g, '/'); + if (path.endsWith('.md')) path = path.slice(0, -3); + return `[${display}](${encodeMarkdownLinkUrl(path)})`; +} diff --git a/src/main.ts b/src/main.ts index 068fa27..bc15cb9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,9 @@ import { Editor, MarkdownView, Notice, Plugin, Menu, Platform, MarkdownFileInfo } from 'obsidian'; import { Language, TranslationKey, I18n } from './i18n'; -import { ContextData, ContextType, DEFAULT_SETTINGS, EasyCopySettings, LinkFormat, BlockIdInsertPosition } from './type'; +import { ContextData, ContextType, DEFAULT_SETTINGS, EasyCopySettings, BlockIdInsertPosition } from './type'; import { EasyCopySettingTab } from './settingTab'; import { BlockIdInputModal } from './blockIdModal'; +import { buildHeadingLink, buildBlockLink, buildFileLink } from './linkBuilder'; export default class EasyCopy extends Plugin { settings: EasyCopySettings; @@ -518,85 +519,31 @@ export default class EasyCopy extends Plugin { * 复制块链接 */ private copyBlockLink(content: string, filename: string, useBrief: boolean, firstLine=''): void { - const blockId = content; - - let text = firstLine; - - const autoDisplayText = this.settings.autoBlockDisplayText; - - // 先去掉结尾的 ^ 及其后面的内容(如果有的话) - text = text.replace(/\^.*\s*$/, ''); - text = text.trim().replace(/- \[.\]\s+/, '').replace('- ', '').replace(/=|\*|\[|\]|\(|\)|`|>\s+/g, ''); - - let displayText = blockId; - if (useBrief && text) { - - // 判断是否是纯英文,如果是纯英文(以及英文常用标点符号),提取前三个单词;否则,按下面的逻辑处理 - // 根据 ASCII 来判断"英文" - const isEnglish = /^[a-zA-Z\s,.!?"()[\]_^-~:;0-9]*$/.test(text); - - if (isEnglish) { - const wordLimit = this.settings.blockDisplayWordLimit || 3; - displayText = text.trim().split(' ').slice(0, wordLimit).join(' '); - } else { - const charLimit = this.settings.blockDisplayCharLimit || 5; - - const briefText = text; - - if (briefText.length > charLimit) { - const seperatedText = text.trim().match(/(\S+?)[~,.\-=[,。?!…:\n\s]/); - let tempText: string | null = null; - - if (seperatedText) { - tempText = seperatedText[1]; - } else { - tempText = briefText; - } - - if (tempText.length > charLimit) { - displayText = tempText.slice(0, charLimit) + '...'; - } else if (tempText.length < 3) { - displayText = briefText.slice(0, charLimit); - } else { - displayText = tempText; - } - } else { - displayText = briefText; - } - } - } - - // displayText = "^"+displayText; - let blockIdLink = this.settings.linkFormat === LinkFormat.WIKILINK - ? `[[${filename}#^${blockId}|${displayText}]]` - : `[${displayText}](${this.encodeMarkdownLinkUrl(filename)}#^${blockId})`; // markdown 格式不能加,不然会变成内联脚注语法 [^xxx] - - if (!autoDisplayText) { - blockIdLink = this.settings.linkFormat === LinkFormat.WIKILINK - ? `[[${filename}#^${blockId}]]` - : `[](${this.encodeMarkdownLinkUrl(filename)}#^${blockId})`; - } - - // 自动生成嵌入块 - if (this.settings.autoEmbedBlockLink) { - blockIdLink = "!" + blockIdLink; - } + const blockIdLink = buildBlockLink({ + blockId: content, + filename, + useBrief, + firstLine, + linkFormat: this.settings.linkFormat, + autoBlockDisplayText: this.settings.autoBlockDisplayText, + autoEmbedBlockLink: this.settings.autoEmbedBlockLink, + blockDisplayWordLimit: this.settings.blockDisplayWordLimit, + blockDisplayCharLimit: this.settings.blockDisplayCharLimit, + }); navigator.clipboard.writeText(blockIdLink); if (this.settings.showNotice) { - new Notice(this.t('block-id-copied') + `\n^${displayText}...`); + new Notice(this.t('block-id-copied')); } - return; } /** * 复制标题链接 */ private copyHeadingLink(content: string, filename: string): void { - - let filenameOrTitle = filename; - let title = ''; + // 优先使用 frontmatter 属性作为显示文本 + let frontmatterTitle: string | undefined; if (this.settings.useFrontmatterAsDisplay) { const file = this.app.workspace.getActiveFile?.() ?? (this.app.workspace.getActiveViewOfType?.(MarkdownView)?.file ?? null); if (file) { @@ -604,75 +551,27 @@ export default class EasyCopy extends Plugin { const frontmatter = fileCache?.frontmatter; const key = this.settings.frontmatterKey || 'title'; if (frontmatter && typeof frontmatter[key] === 'string' && frontmatter[key].trim()) { - title = frontmatter[key].trim(); - filenameOrTitle = title; + frontmatterTitle = frontmatter[key].trim(); } } } - // 提取标题文本和级别 - let selectedHeading = content; - // 如果内容是[[内容]],移除[[]] - if (selectedHeading.startsWith('[[') && selectedHeading.endsWith(']]')) { - selectedHeading = selectedHeading.slice(2, -2); - } - - // 根据设置决定显示文本 - let displayText = selectedHeading; - if (!this.settings.useHeadingAsDisplayText) { - // 如果不使用标题作为显示文本,则使用"文件名{连接符}标题名"格式 - const separator = this.settings.headingLinkSeparator || '#'; - displayText = `${filenameOrTitle}${separator}${selectedHeading}`; - } - - let headingReferenceLink = ""; - let noteFlag = 0; + const { link, isNoteLink } = buildHeadingLink({ + heading: content, + filename, + frontmatterTitle, + linkFormat: this.settings.linkFormat, + useHeadingAsDisplayText: this.settings.useHeadingAsDisplayText, + headingLinkSeparator: this.settings.headingLinkSeparator, + }); - let linkContent = `${filename}#${selectedHeading}`; + navigator.clipboard.writeText(link); - function compareIgnoreCase(a: string, b: string): boolean { - return a.toLowerCase() === b.toLowerCase() || a.toLowerCase().includes(b.toLowerCase()); - } - - // 特殊情况:如果文件名包含标题,则不添加指向标题的 # 部分 - // 我自己的情况——会把 SomeThing 给拆成 Some Thing 来做标题,所以也考虑空格替换的部分 - if (filename === selectedHeading || compareIgnoreCase(filename, selectedHeading) || compareIgnoreCase(filename, selectedHeading.replace(/\s+/g, ''))) { - linkContent = filename; + if (isNoteLink) { new Notice(this.t('note-link-simplified')); - noteFlag = 1; } - - // 根据设置选择链接格式 - if (this.settings.linkFormat === LinkFormat.WIKILINK) { - // Wiki链接格式 - if (filename === selectedHeading) { - // 特殊情况:当文件名与标题相同时,直接链接到文件 - headingReferenceLink = `[[${filename}]]`; - noteFlag = 1; - } else { - if (displayText === linkContent) { - // 特殊情况:当显示文本与 "文件名#标题" 相同时,省略显示文本 - headingReferenceLink = `[[${linkContent}]]`; - } else { - headingReferenceLink = `[[${linkContent}|${displayText}]]`; - } - } - } else { - // Markdown链接格式 - headingReferenceLink = `[${displayText}](${this.encodeMarkdownLinkUrl(`${filename}#${selectedHeading}`)})`; - - } - - // 复制到剪贴板 - navigator.clipboard.writeText(headingReferenceLink); - - // 显示通知 if (this.settings.showNotice) { - if (noteFlag) { - new Notice(this.t('note-link-copied')); - } else { - new Notice(this.t('heading-copied')); - } + new Notice(this.t(isNoteLink ? 'note-link-copied' : 'heading-copied')); } } @@ -680,8 +579,8 @@ export default class EasyCopy extends Plugin { * 复制当前文件链接(支持 Wiki/Markdown 格式) */ private copyCurrentFileLink(): void { - // 新增:优先使用 frontmatter 属性作为显示文本 - let displayText: string | undefined = undefined; + // 优先使用 frontmatter 属性作为显示文本 + let displayText: string | undefined; if (this.settings.useFrontmatterAsDisplay) { const file = this.app.workspace.getActiveFile?.() ?? (this.app.workspace.getActiveViewOfType?.(MarkdownView)?.file ?? null); if (file) { @@ -693,22 +592,20 @@ export default class EasyCopy extends Plugin { } } } - // 获取当前激活文件 + const file = this.app.workspace.getActiveFile?.() ?? (this.app.workspace.getActiveViewOfType?.(MarkdownView)?.file ?? null); if (!file) { new Notice(this.t('no-file')); return; } - const filename = file.basename; - let link = ''; - const display = displayText || filename; - if (this.settings.linkFormat === LinkFormat.WIKILINK) { - link = `[[${filename}|${display}]]`; - } else { - let path = file.path.replace(/\\/g, '/'); - if (path.endsWith('.md')) path = path.slice(0, -3); - link = `[${display}](${this.encodeMarkdownLinkUrl(path)})`; - } + + const link = buildFileLink({ + filename: file.basename, + filePath: file.path, + displayText, + linkFormat: this.settings.linkFormat, + }); + navigator.clipboard.writeText(link); if (this.settings.showNotice) { new Notice(this.t('file-link-copied')); @@ -820,8 +717,4 @@ export default class EasyCopy extends Plugin { const lang = window.localStorage.getItem("language") || 'en'; return lang; } - - private encodeMarkdownLinkUrl(url: string): string { - return url.replace(/ /g, '%20'); - } } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..81e01cb --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.ts'], + }, +});