diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 335fed4..8f0405e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -18,7 +18,8 @@ "Bash(npm test -- tests/renderer/i18nLocaleCompleteness.test.ts)", "WebFetch(domain:ricmac.org)", "WebFetch(domain:docs.mistral.ai)", - "Bash(npm uninstall dropbox date-fns @testing-library/user-event @types/dagre electron-store memfs)" + "Bash(npm uninstall dropbox date-fns @testing-library/user-event @types/dagre electron-store memfs)", + "WebSearch" ] } } diff --git a/package-lock.json b/package-lock.json index e183878..be50e80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "marked-react": "^3.0.2", "monaco-editor": "^0.55.1", "node-scp": "^0.0.25", + "pagefind": "^1.4.0", "pyodide": "^0.29.3", "react": "^19.2.4", "react-arborist": "^3.4.3", @@ -286,6 +287,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -864,6 +866,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -940,6 +943,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", "license": "MIT", + "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -961,6 +965,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.13.tgz", "integrity": "sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1056,6 +1061,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1096,6 +1102,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1465,7 +1472,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1487,7 +1493,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1504,7 +1509,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1519,7 +1523,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -3678,6 +3681,7 @@ "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.17.0.tgz", "integrity": "sha512-TLjSU9Otdpq0SpKHl1tD1Nc9MKhrsZbCFGot3EbCxRa8m1E5R1mMwoOjKMMM31IyF7fr+hPNHLpYfwbMKNusmg==", "license": "MIT", + "peer": true, "dependencies": { "@libsql/core": "^0.17.0", "@libsql/hrana-client": "^0.9.0", @@ -4317,6 +4321,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -4614,6 +4619,84 @@ "win32" ] }, + "node_modules/@pagefind/darwin-arm64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.4.0.tgz", + "integrity": "sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/darwin-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.4.0.tgz", + "integrity": "sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/freebsd-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.4.0.tgz", + "integrity": "sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@pagefind/linux-arm64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.4.0.tgz", + "integrity": "sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/linux-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.4.0.tgz", + "integrity": "sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/windows-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.4.0.tgz", + "integrity": "sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@picocss/pico": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@picocss/pico/-/pico-2.1.1.tgz", @@ -5184,8 +5267,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5409,6 +5491,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5419,6 +5502,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5526,6 +5610,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5914,6 +5999,7 @@ "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "4.0.18", "fflate": "^0.8.2", @@ -6138,6 +6224,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6189,6 +6276,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6817,6 +6905,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7543,8 +7632,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-env": { "version": "10.1.0", @@ -7680,7 +7768,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-cloud": { "version": "1.2.8", @@ -7936,6 +8025,7 @@ "integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.7.0", "builder-util": "26.4.1", @@ -8037,8 +8127,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dompurify": { "version": "3.3.2", @@ -8430,7 +8519,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -8451,7 +8539,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8487,16 +8574,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -8603,6 +8680,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8685,6 +8763,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9855,6 +9934,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -10381,6 +10461,7 @@ "integrity": "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.7.6", @@ -10725,7 +10806,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -11971,7 +12051,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -11984,6 +12063,7 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", + "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -12514,6 +12594,23 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pagefind": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.4.0.tgz", + "integrity": "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g==", + "license": "MIT", + "bin": { + "pagefind": "lib/runner/bin.cjs" + }, + "optionalDependencies": { + "@pagefind/darwin-arm64": "1.4.0", + "@pagefind/darwin-x64": "1.4.0", + "@pagefind/freebsd-x64": "1.4.0", + "@pagefind/linux-arm64": "1.4.0", + "@pagefind/linux-x64": "1.4.0", + "@pagefind/windows-x64": "1.4.0" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -12653,6 +12750,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12788,7 +12886,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -12806,7 +12903,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -12827,7 +12923,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -12843,7 +12938,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -12998,6 +13092,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", + "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -13031,6 +13126,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -13064,6 +13160,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz", "integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -13246,6 +13343,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13311,6 +13409,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13340,8 +13439,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-refresh": { "version": "0.18.0", @@ -13645,7 +13743,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -14554,7 +14651,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -15401,6 +15497,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15728,6 +15825,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -16287,6 +16385,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -16364,6 +16463,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28", @@ -16681,6 +16781,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index ce72606..ed400e3 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "marked-react": "^3.0.2", "monaco-editor": "^0.55.1", "node-scp": "^0.0.25", + "pagefind": "^1.4.0", "pyodide": "^0.29.3", "react": "^19.2.4", "react-arborist": "^3.4.3", @@ -132,7 +133,9 @@ "asar": true, "asarUnpack": [ "node_modules/onnxruntime-node/**", - "node_modules/usearch/**" + "node_modules/usearch/**", + "node_modules/pagefind/**", + "node_modules/@pagefind/**" ], "afterSign": "scripts/notarize.mjs", "directories": { diff --git a/src/main/engine/PageRenderer.ts b/src/main/engine/PageRenderer.ts index b494faf..824002d 100644 --- a/src/main/engine/PageRenderer.ts +++ b/src/main/engine/PageRenderer.ts @@ -10,6 +10,7 @@ import { PICO_THEME_NAMES } from '../shared/picoThemes'; import { CODE_ENHANCEMENTS_RUNTIME_JS } from './assets/codeEnhancementsRuntime'; import { CALENDAR_RUNTIME_JS } from './assets/calendarRuntime'; import { TAG_CLOUD_RUNTIME_JS } from './assets/tagCloudRuntime'; +import { SEARCH_RUNTIME_JS } from './assets/searchRuntime'; import { resolveRenderLanguageFromProjectPreferences, translateRender, getRenderTranslations } from '../shared/i18n'; function readLocalAsset(filename: string): string { @@ -312,6 +313,10 @@ export const PREVIEW_ASSETS: Record = { contentType: 'application/javascript; charset=utf-8', sourceText: CALENDAR_RUNTIME_JS, }, + 'search-runtime.js': { + contentType: 'application/javascript; charset=utf-8', + sourceText: SEARCH_RUNTIME_JS, + }, 'bds.css': { contentType: 'text/css; charset=utf-8', sourceText: readLocalAsset('bds.css'), diff --git a/src/main/engine/SearchIndexEngine.ts b/src/main/engine/SearchIndexEngine.ts new file mode 100644 index 0000000..3bd3801 --- /dev/null +++ b/src/main/engine/SearchIndexEngine.ts @@ -0,0 +1,107 @@ +import * as path from 'path'; +import * as os from 'os'; +import { spawn } from 'node:child_process'; + +export interface SearchIndexOptions { + htmlDir: string; + mainLanguage: string; + additionalLanguages: string[]; + onProgress?: (progress: number, message?: string) => void; +} + +export interface SearchIndexLanguageResult { + language: string; + pageCount: number; +} + +export interface SearchIndexResult { + languageIndexes: SearchIndexLanguageResult[]; +} + +function resolvePagefindBinaryPath(): string { + const cpu = os.arch(); + const platform = process.platform === 'win32' ? 'windows' : process.platform; + const execnames = ['pagefind_extended', 'pagefind']; + + for (const execname of execnames) { + const executable = platform === 'windows' ? `${execname}.exe` : execname; + try { + let resolved = require.resolve(`@pagefind/${platform}-${cpu}/bin/${executable}`); + // In packaged Electron, require.resolve returns a path through app.asar + // but the binary is in the unpacked directory alongside it + resolved = resolved.replace(/app\.asar([/\\])/, 'app.asar.unpacked$1'); + return resolved; + } catch { + // try next + } + } + + throw new Error( + `Failed to find pagefind binary for ${platform}-${cpu}. ` + + `Ensure @pagefind/${platform}-${cpu} is installed.`, + ); +} + +function runPagefind(args: string[]): Promise<{ stdout: string; stderr: string }> { + const binaryPath = resolvePagefindBinaryPath(); + return new Promise((resolve, reject) => { + const proc = spawn(binaryPath, args, { stdio: ['ignore', 'pipe', 'pipe'] }); + let stdout = ''; + let stderr = ''; + proc.stdout.on('data', (data: Buffer) => { stdout += data.toString(); }); + proc.stderr.on('data', (data: Buffer) => { stderr += data.toString(); }); + proc.on('error', reject); + proc.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Pagefind exited with code ${code}: ${stderr}`)); + } else { + resolve({ stdout, stderr }); + } + }); + }); +} + +function parsePageCount(output: string): number { + const match = output.match(/indexed\s+(\d+)\s+page/i) ?? output.match(/(\d+)\s+page/i); + return match ? parseInt(match[1], 10) : 0; +} + +/** + * Build Pagefind search indexes for a generated static site. + * + * For single-language sites, creates one index at `{htmlDir}/pagefind/`. + * For multilingual sites, creates per-language indexes: + * - main language: `{htmlDir}/pagefind/` (indexes root html) + * - additional languages: `{htmlDir}/{lang}/pagefind/` (indexes `{htmlDir}/{lang}/`) + */ +export async function buildSearchIndex(options: SearchIndexOptions): Promise { + const { htmlDir, mainLanguage, additionalLanguages, onProgress } = options; + + const languages = [mainLanguage, ...additionalLanguages]; + const results: SearchIndexLanguageResult[] = []; + + for (let i = 0; i < languages.length; i++) { + const lang = languages[i]; + const isMain = lang === mainLanguage; + const sourceDir = isMain ? htmlDir : path.join(htmlDir, lang); + const outputDir = isMain + ? path.join(htmlDir, 'pagefind') + : path.join(htmlDir, lang, 'pagefind'); + + const progressBase = Math.floor((i / languages.length) * 100); + + onProgress?.(progressBase, `Indexing search for ${lang}...`); + + const { stdout, stderr } = await runPagefind([ + '--site', sourceDir, + '--output-path', outputDir, + '--force-language', lang, + ]); + + const pageCount = parsePageCount(stdout + stderr); + results.push({ language: lang, pageCount }); + } + + onProgress?.(100, 'Search indexes built'); + return { languageIndexes: results }; +} diff --git a/src/main/engine/assets/bds.css b/src/main/engine/assets/bds.css index 30021f9..c67826c 100644 --- a/src/main/engine/assets/bds.css +++ b/src/main/engine/assets/bds.css @@ -148,3 +148,22 @@ main { display: grid; gap: 1rem; } .language-switcher-badge:hover, .language-switcher-badge:focus-visible { opacity: 1; border-color: var(--pico-color, var(--color)); } .language-switcher-badge-current { opacity: 1; border-color: var(--pico-primary, var(--primary)); } +.blog-search-widget, .blog-search-standalone { position: relative; margin-top: .15rem; } +.blog-search-standalone { position: fixed; right: .75rem; top: 1.5rem; z-index: 100; } +.blog-search-toggle { display: inline-flex; align-items: center; justify-content: center; border: 0; background: transparent; color: var(--pico-muted-color, var(--muted-color)); cursor: pointer; padding: .15rem; opacity: .7; transition: opacity .15s ease-in-out; } +.blog-search-toggle:hover, .blog-search-toggle:focus-visible { opacity: 1; color: var(--pico-color, var(--color)); } +.blog-search-toggle svg { display: block; } +.blog-search-panel { position: absolute; top: calc(100% + .25rem); right: 0; width: min(24rem, 90vw); z-index: 40; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); background: var(--pico-card-background-color, var(--card-background-color)); padding: .5rem; border-radius: .35rem; box-shadow: 0 4px 24px rgba(0,0,0,.25); } +.blog-search-panel .pagefind-ui { --pagefind-ui-scale: .8; --pagefind-ui-primary: var(--pico-primary, var(--primary)); --pagefind-ui-text: var(--pico-color, var(--color)); --pagefind-ui-background: var(--pico-card-background-color, var(--card-background-color)); --pagefind-ui-border: var(--pico-muted-border-color, var(--muted-border-color)); --pagefind-ui-tag: var(--pico-muted-border-color, var(--muted-border-color)); --pagefind-ui-border-width: 1px; --pagefind-ui-border-radius: .2rem; --pagefind-ui-image-border-radius: .2rem; --pagefind-ui-image-box-ratio: 0; --pagefind-ui-font: inherit; font-size: .85rem; } +.blog-search-panel .pagefind-ui__search-input { font-size: .85rem; padding: .3rem .5rem; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); background: var(--pico-background-color, var(--background-color)); color: var(--pico-color, var(--color)); border-radius: .2rem; width: 100%; } +.blog-search-panel .pagefind-ui__search-clear { color: var(--pico-muted-color, var(--muted-color)); background: none; font-size: .8rem; } +.blog-search-panel .pagefind-ui__search-clear:focus { outline-color: var(--pico-primary, var(--primary)); } +.blog-search-panel .pagefind-ui__drawer { max-height: min(60vh, 28rem); overflow-y: auto; } +.blog-search-panel .pagefind-ui__message { color: var(--pico-muted-color, var(--muted-color)); font-size: .78rem; padding: .25rem 0; } +.blog-search-panel .pagefind-ui__result { border-top: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); padding: .4rem 0; } +.blog-search-panel .pagefind-ui__result-link { color: var(--pico-primary, var(--primary)); font-size: .85rem; } +.blog-search-panel .pagefind-ui__result-title { font-size: .85rem; } +.blog-search-panel .pagefind-ui__result-excerpt { font-size: .78rem; color: var(--pico-muted-color, var(--muted-color)); } +.blog-search-panel .pagefind-ui__result-excerpt mark { background-color: var(--pico-primary-focus, rgba(255,223,0,.35)); color: inherit; } +.blog-search-panel .pagefind-ui__button { color: var(--pico-primary, var(--primary)); background: none; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); border-radius: .2rem; font-size: .78rem; cursor: pointer; } +.blog-search-panel .pagefind-ui__button:hover { border-color: var(--pico-primary, var(--primary)); } diff --git a/src/main/engine/assets/searchRuntime.ts b/src/main/engine/assets/searchRuntime.ts new file mode 100644 index 0000000..d9f55c7 --- /dev/null +++ b/src/main/engine/assets/searchRuntime.ts @@ -0,0 +1,52 @@ +export const SEARCH_RUNTIME_JS = String.raw`(() => { + const toggle = document.querySelector('[data-blog-search-toggle]'); + const panel = document.querySelector('[data-blog-search-panel]'); + const root = document.querySelector('[data-blog-search-root]'); + + if (!toggle || !panel || !root) { + return; + } + + let initialized = false; + + function initSearch() { + if (initialized || typeof PagefindUI === 'undefined') { + return; + } + initialized = true; + var placeholder = root.getAttribute('data-search-placeholder') || 'Search...'; + new PagefindUI({ + element: root, + showSubResults: true, + showImages: false, + translations: { placeholder: placeholder } + }); + var input = root.querySelector('input'); + if (input) { + input.focus(); + } + } + + toggle.addEventListener('click', function() { + var isHidden = panel.hasAttribute('hidden'); + if (isHidden) { + panel.removeAttribute('hidden'); + initSearch(); + } else { + panel.setAttribute('hidden', ''); + } + }); + + document.addEventListener('click', function(e) { + if (!panel.hasAttribute('hidden') && !panel.contains(e.target) && !toggle.contains(e.target)) { + panel.setAttribute('hidden', ''); + } + }); + + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape' && !panel.hasAttribute('hidden')) { + panel.setAttribute('hidden', ''); + toggle.focus(); + } + }); +})();`; diff --git a/src/main/engine/templates/partials/head.liquid b/src/main/engine/templates/partials/head.liquid index 8a16db5..0962196 100644 --- a/src/main/engine/templates/partials/head.liquid +++ b/src/main/engine/templates/partials/head.liquid @@ -21,4 +21,7 @@ + + + diff --git a/src/main/engine/templates/partials/language-switcher.liquid b/src/main/engine/templates/partials/language-switcher.liquid index db8b475..1fa9bb3 100644 --- a/src/main/engine/templates/partials/language-switcher.liquid +++ b/src/main/engine/templates/partials/language-switcher.liquid @@ -7,6 +7,17 @@ {{ lang.flag }} {% endif %} {% endfor %} +
+ + +
+{% else %} +
+ + +
{% endif %} diff --git a/src/main/engine/templates/single-post.liquid b/src/main/engine/templates/single-post.liquid index 9d86a16..1187ba6 100644 --- a/src/main/engine/templates/single-post.liquid +++ b/src/main/engine/templates/single-post.liquid @@ -17,7 +17,7 @@ {% endfor %} {% endif %} -
+
{{ post.content | markdown: post.id, post_data_json_by_id, canonical_post_path_by_slug, canonical_media_path_by_source_path, language, language_prefix }}
{% if backlinks.size > 0 %} diff --git a/src/main/ipc/blogHandlers.ts b/src/main/ipc/blogHandlers.ts index 7e3bd5a..d9e194c 100644 --- a/src/main/ipc/blogHandlers.ts +++ b/src/main/ipc/blogHandlers.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import { dialog } from 'electron'; import { resolvePublicBaseUrl, @@ -8,6 +9,7 @@ import { type ApplyValidationPreparation, } from '../engine/BlogGenerationEngine'; import { resolvePageTitle } from '../engine/PageRenderer'; +import { buildSearchIndex } from '../engine/SearchIndexEngine'; import type { EngineBundle } from '../engine/EngineBundle'; import type { TranslationValidationReport } from '../shared/electronApi'; import { autoTranslatePost, autoTranslateMediaMetadata } from './chatHandlers'; @@ -152,6 +154,26 @@ export function registerBlogHandlers(safeHandle: SafeHandle, bundle: EngineBundl runSectionTask('date', 'Render Date Archives', 'site-render-date'), ]); + await bundle.taskManager.runTask({ + id: `site-render-search-index-${taskTimestamp}`, + name: 'Build Search Index', + groupId: taskGroupId, + groupName: taskGroupName, + execute: async (onProgress) => { + const htmlDir = path.join(baseOptions.dataDir, 'html'); + const mainLanguage = (baseOptions.language ?? 'en').trim().toLowerCase(); + const additionalLanguages = (baseOptions.blogLanguages ?? []) + .map((lang) => lang.trim().toLowerCase()) + .filter((lang) => lang.length > 0 && lang !== mainLanguage); + return buildSearchIndex({ + htmlDir, + mainLanguage, + additionalLanguages, + onProgress: (progress, message) => onProgress(progress, message || 'Building search index...'), + }); + }, + }); + return mergeResults([coreResult, singleResult, categoryResult, tagResult, dateResult]); }); @@ -388,6 +410,27 @@ export function registerBlogHandlers(safeHandle: SafeHandle, bundle: EngineBundl }); }, }); + + // Phase 4: Rebuild search index + await bundle.taskManager.runTask({ + id: `site-validate-search-index-${taskTimestamp}`, + name: 'Build Search Index', + groupId: taskGroupId, + groupName: taskGroupName, + execute: async (onProgress) => { + const htmlDir = path.join(baseOptions.dataDir, 'html'); + const mainLanguage = (baseOptions.language ?? 'en').trim().toLowerCase(); + const additionalLanguages = (baseOptions.blogLanguages ?? []) + .map((lang) => lang.trim().toLowerCase()) + .filter((lang) => lang.length > 0 && lang !== mainLanguage); + return buildSearchIndex({ + htmlDir, + mainLanguage, + additionalLanguages, + onProgress: (progress, message) => onProgress(progress, message || 'Building search index...'), + }); + }, + }); } return { diff --git a/src/main/shared/i18n/locales/de.json b/src/main/shared/i18n/locales/de.json index 5fe202d..b1c5fcd 100644 --- a/src/main/shared/i18n/locales/de.json +++ b/src/main/shared/i18n/locales/de.json @@ -94,5 +94,7 @@ "task.rebuildEmbeddingIndex.clearing": "Index wird geleert…", "task.duplicateSearch.name": "Doppelte Beiträge finden", "task.duplicateSearch.searching": "Prüfe: {checked}/{total}", - "menu.item.fillMissingTranslations": "Fehlende Übersetzungen ausfüllen" + "menu.item.fillMissingTranslations": "Fehlende Übersetzungen ausfüllen", + "render.search.placeholder": "Suchen...", + "render.search.ariaLabel": "Seitensuche" } diff --git a/src/main/shared/i18n/locales/en.json b/src/main/shared/i18n/locales/en.json index fbd229c..4e83490 100644 --- a/src/main/shared/i18n/locales/en.json +++ b/src/main/shared/i18n/locales/en.json @@ -94,5 +94,7 @@ "task.rebuildEmbeddingIndex.clearing": "Clearing index…", "task.duplicateSearch.name": "Find Duplicate Posts", "task.duplicateSearch.searching": "Checking: {checked}/{total}", - "menu.item.fillMissingTranslations": "Fill Missing Translations" + "menu.item.fillMissingTranslations": "Fill Missing Translations", + "render.search.placeholder": "Search...", + "render.search.ariaLabel": "Site search" } diff --git a/src/main/shared/i18n/locales/es.json b/src/main/shared/i18n/locales/es.json index 363e285..2382d09 100644 --- a/src/main/shared/i18n/locales/es.json +++ b/src/main/shared/i18n/locales/es.json @@ -94,5 +94,7 @@ "task.rebuildEmbeddingIndex.clearing": "Vaciando índice…", "task.duplicateSearch.name": "Buscar entradas duplicadas", "task.duplicateSearch.searching": "Comprobando: {checked}/{total}", - "menu.item.fillMissingTranslations": "Completar traducciones faltantes" + "menu.item.fillMissingTranslations": "Completar traducciones faltantes", + "render.search.placeholder": "Buscar...", + "render.search.ariaLabel": "Buscar en el sitio" } diff --git a/src/main/shared/i18n/locales/fr.json b/src/main/shared/i18n/locales/fr.json index 8a2198d..79d8f8b 100644 --- a/src/main/shared/i18n/locales/fr.json +++ b/src/main/shared/i18n/locales/fr.json @@ -94,5 +94,7 @@ "task.rebuildEmbeddingIndex.clearing": "Vidage de l'index…", "task.duplicateSearch.name": "Trouver les articles en double", "task.duplicateSearch.searching": "Vérification : {checked}/{total}", - "menu.item.fillMissingTranslations": "Compléter les traductions manquantes" + "menu.item.fillMissingTranslations": "Compléter les traductions manquantes", + "render.search.placeholder": "Rechercher...", + "render.search.ariaLabel": "Recherche du site" } diff --git a/src/main/shared/i18n/locales/it.json b/src/main/shared/i18n/locales/it.json index 0fc10c3..40c66d8 100644 --- a/src/main/shared/i18n/locales/it.json +++ b/src/main/shared/i18n/locales/it.json @@ -94,5 +94,7 @@ "task.rebuildEmbeddingIndex.clearing": "Svuotamento indice…", "task.duplicateSearch.name": "Trova post duplicati", "task.duplicateSearch.searching": "Controllo: {checked}/{total}", - "menu.item.fillMissingTranslations": "Completa le traduzioni mancanti" + "menu.item.fillMissingTranslations": "Completa le traduzioni mancanti", + "render.search.placeholder": "Cerca...", + "render.search.ariaLabel": "Ricerca nel sito" } diff --git a/tests/engine/PreviewServer.test.ts b/tests/engine/PreviewServer.test.ts index 2b47b24..22f3004 100644 --- a/tests/engine/PreviewServer.test.ts +++ b/tests/engine/PreviewServer.test.ts @@ -1074,7 +1074,7 @@ describe('PreviewServer', () => { const mainIndex = html.indexOf('
'); const h1Index = html.indexOf('

Explicit Single Post Title

'); - const articleIndex = html.indexOf('
'); + const articleIndex = html.indexOf('
'); expect(mainIndex).toBeGreaterThan(-1); expect(h1Index).toBeGreaterThan(mainIndex); expect(articleIndex).toBeGreaterThan(mainIndex); diff --git a/tests/engine/SearchIndexEngine.test.ts b/tests/engine/SearchIndexEngine.test.ts new file mode 100644 index 0000000..4279a0b --- /dev/null +++ b/tests/engine/SearchIndexEngine.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +const { mockSpawn } = vi.hoisted(() => ({ + mockSpawn: vi.fn(), +})); + +vi.mock('node:child_process', () => ({ + spawn: mockSpawn, + default: { spawn: mockSpawn }, +})); + +function createMockProc(exitCode = 0, stdout = 'Indexed 5 pages\n', stderr = '') { + const stdoutCbs: ((data: Buffer) => void)[] = []; + const stderrCbs: ((data: Buffer) => void)[] = []; + const closeCbs: ((...args: unknown[]) => void)[] = []; + + return { + stdout: { + on: vi.fn((_event: string, cb: (data: Buffer) => void) => { + stdoutCbs.push(cb); + }), + }, + stderr: { + on: vi.fn((_event: string, cb: (data: Buffer) => void) => { + stderrCbs.push(cb); + }), + }, + on: vi.fn((event: string, cb: (...args: unknown[]) => void) => { + if (event === 'error') return; + if (event === 'close') { + closeCbs.push(cb); + Promise.resolve().then(() => { + for (const fn of stdoutCbs) fn(Buffer.from(stdout)); + for (const fn of stderrCbs) if (stderr) fn(Buffer.from(stderr)); + for (const fn of closeCbs) fn(exitCode); + }); + } + }), + }; +} + +beforeEach(() => { + vi.clearAllMocks(); + mockSpawn.mockImplementation(() => createMockProc(0, 'Indexed 5 pages\n')); +}); + +describe('SearchIndexEngine', () => { + describe('buildSearchIndex', () => { + it('spawns pagefind binary for a single language', async () => { + const { buildSearchIndex } = await import('../../src/main/engine/SearchIndexEngine'); + + const result = await buildSearchIndex({ + htmlDir: '/site/html', + mainLanguage: 'en', + additionalLanguages: [], + }); + + expect(mockSpawn).toHaveBeenCalledTimes(1); + const args = mockSpawn.mock.calls[0][1] as string[]; + expect(args).toEqual(expect.arrayContaining([ + '--site', '/site/html', + '--force-language', 'en', + '--output-path', '/site/html/pagefind', + ])); + expect(result.languageIndexes).toHaveLength(1); + expect(result.languageIndexes[0]).toEqual({ language: 'en', pageCount: 5 }); + }); + + it('spawns pagefind once per language for multilingual sites', async () => { + const { buildSearchIndex } = await import('../../src/main/engine/SearchIndexEngine'); + + const result = await buildSearchIndex({ + htmlDir: '/site/html', + mainLanguage: 'en', + additionalLanguages: ['de', 'fr'], + }); + + expect(mockSpawn).toHaveBeenCalledTimes(3); + const calls = mockSpawn.mock.calls; + + expect(calls[0][1]).toContain('en'); + expect(calls[1][1]).toContain('de'); + expect(calls[2][1]).toContain('fr'); + + expect(result.languageIndexes).toHaveLength(3); + }); + + it('writes per-language output to language subdirectories', async () => { + const { buildSearchIndex } = await import('../../src/main/engine/SearchIndexEngine'); + + await buildSearchIndex({ + htmlDir: '/site/html', + mainLanguage: 'en', + additionalLanguages: ['de'], + }); + + const calls = mockSpawn.mock.calls; + + const mainArgs = calls[0][1] as string[]; + const mainOutputIdx = mainArgs.indexOf('--output-path'); + expect(mainArgs[mainOutputIdx + 1]).toBe('/site/html/pagefind'); + + const deArgs = calls[1][1] as string[]; + const deOutputIdx = deArgs.indexOf('--output-path'); + expect(deArgs[deOutputIdx + 1]).toBe('/site/html/de/pagefind'); + }); + + it('throws when pagefind exits with non-zero code', async () => { + mockSpawn.mockImplementation(() => createMockProc(1, '', 'Something went wrong')); + const { buildSearchIndex } = await import('../../src/main/engine/SearchIndexEngine'); + + await expect(buildSearchIndex({ + htmlDir: '/site/html', + mainLanguage: 'en', + additionalLanguages: [], + })).rejects.toThrow('Pagefind exited with code 1'); + }); + + it('parses page count from pagefind output', async () => { + mockSpawn.mockImplementation(() => createMockProc(0, 'Running Pagefind\nIndexed 42 pages\nDone')); + const { buildSearchIndex } = await import('../../src/main/engine/SearchIndexEngine'); + + const result = await buildSearchIndex({ + htmlDir: '/site/html', + mainLanguage: 'en', + additionalLanguages: [], + }); + + expect(result.languageIndexes[0].pageCount).toBe(42); + }); + + it('reports progress via onProgress callback', async () => { + const { buildSearchIndex } = await import('../../src/main/engine/SearchIndexEngine'); + const onProgress = vi.fn(); + + await buildSearchIndex({ + htmlDir: '/site/html', + mainLanguage: 'en', + additionalLanguages: ['de'], + onProgress, + }); + + expect(onProgress).toHaveBeenCalled(); + const calls = onProgress.mock.calls; + expect(calls[calls.length - 1][0]).toBe(100); + }); + }); +}); diff --git a/tests/ipc/handlers.test.ts b/tests/ipc/handlers.test.ts index 1bb984d..63ba62d 100644 --- a/tests/ipc/handlers.test.ts +++ b/tests/ipc/handlers.test.ts @@ -321,6 +321,10 @@ vi.mock('../../src/main/engine/TaskManager', () => ({ TaskProgress: {}, })); +vi.mock('../../src/main/engine/SearchIndexEngine', () => ({ + buildSearchIndex: vi.fn().mockResolvedValue({ languageIndexes: [] }), +})); + vi.mock('../../src/main/database', () => ({ getDatabase: vi.fn(() => mockDatabase), }));