Feat/pagefind search (#56)

* feat: pagefind search engine

* fix: search now works

* fixed layout

---------

Co-authored-by: hugo <hugoms@me.com>
This commit is contained in:
Georg Bauer
2026-03-15 11:44:33 +01:00
committed by GitHub
parent f03b087c13
commit 1e7d60e63e
19 changed files with 561 additions and 42 deletions

View File

@@ -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"
]
}
}

167
package-lock.json generated
View File

@@ -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"
}

View File

@@ -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": {

View File

@@ -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<string, PreviewAssetDefinition> = {
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'),

View File

@@ -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<SearchIndexResult> {
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 };
}

View File

@@ -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)); }

View File

@@ -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();
}
});
})();`;

View File

@@ -21,4 +21,7 @@
<script defer src="/assets/lightbox.min.js"></script>
<script defer src="/assets/vanilla-calendar.min.js"></script>
<script defer src="/assets/calendar-runtime.js"></script>
<script defer src="/assets/search-runtime.js"></script>
<link rel="stylesheet" href="{{ language_prefix }}/pagefind/pagefind-ui.css" />
<script defer src="{{ language_prefix }}/pagefind/pagefind-ui.js"></script>
</head>

View File

@@ -7,6 +7,17 @@
<a class="language-switcher-badge" href="{{ lang.href_prefix | default: '/' }}" data-lang-prefix="{{ lang.href_prefix }}" title="{{ lang.code }}">{{ lang.flag }}</a>
{% endif %}
{% endfor %}
<div class="blog-search-widget" aria-label="{{ 'render.search.ariaLabel' | i18n: language }}">
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="{{ 'render.search.ariaLabel' | i18n: language }}">
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</button>
<div class="blog-search-panel" data-blog-search-panel hidden>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ 'render.search.placeholder' | i18n: language }}"></div>
</div>
</div>
</nav>
<script>
(function(){
@@ -15,4 +26,16 @@
links.forEach(function(a){a.href=(a.dataset.langPrefix||'')+path;});
}());
</script>
{% else %}
<div class="blog-search-standalone" aria-label="{{ 'render.search.ariaLabel' | i18n: language }}">
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="{{ 'render.search.ariaLabel' | i18n: language }}">
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</button>
<div class="blog-search-panel" data-blog-search-panel hidden>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ 'render.search.placeholder' | i18n: language }}"></div>
</div>
</div>
{% endif %}

View File

@@ -17,7 +17,7 @@
{% endfor %}
</div>
{% endif %}
<article class="single-post" data-template="single-post">
<article class="single-post" data-template="single-post" data-pagefind-body>
<div class="post">{{ post.content | markdown: post.id, post_data_json_by_id, canonical_post_path_by_slug, canonical_media_path_by_source_path, language, language_prefix }}</div>
</article>
{% if backlinks.size > 0 %}

View File

@@ -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 {

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -1074,7 +1074,7 @@ describe('PreviewServer', () => {
const mainIndex = html.indexOf('<main>');
const h1Index = html.indexOf('<h1>Explicit Single Post Title</h1>');
const articleIndex = html.indexOf('<article class="single-post" data-template="single-post">');
const articleIndex = html.indexOf('<article class="single-post" data-template="single-post" data-pagefind-body>');
expect(mainIndex).toBeGreaterThan(-1);
expect(h1Index).toBeGreaterThan(mainIndex);
expect(articleIndex).toBeGreaterThan(mainIndex);

View File

@@ -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);
});
});
});

View File

@@ -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),
}));