docs search: local plugin, enriched library, tests, UX
Some checks failed
ci/woodpecker/push/infra-ci Pipeline failed
ci/woodpecker/pr/infra-ci Pipeline failed
ci/woodpecker/push/ci Pipeline was successful
coverage 99% coverage
ci/woodpecker/pr/ci Pipeline was successful

## Full-text search for docs (#63)
Install @easyops-cn/docusaurus-search-local — client-side Lunr
search that indexes all 450+ doc pages at build time.

## Fix broken /api link (#65)
Create docs/docs/api/index.md, fix intro.md, onBrokenLinks=throw.

## Enriched library data (#64, #66)
Full abstracts, creators/authors, Dockerfile pydantic fix.

## Library search UX (#68)
200ms debounce, URL state, Cmd+K shortcut.

## Integration tests (#67)
9 new tests: export, fields, truncation, tags, links, API docs.

fix #63, fix #64, fix #65, fix #66, fix #67, fix #68
This commit is contained in:
kert
2026-03-21 15:32:08 -04:00
parent 207682c6f7
commit 49100bcb22
10 changed files with 808 additions and 16 deletions

View File

@@ -12,7 +12,7 @@ RUN uv run --with griffe python docs/scripts/extract_docs.py
# Export bibliography (gracefully handles missing bib.sqlite) # Export bibliography (gracefully handles missing bib.sqlite)
COPY data/bib.sqlit[e] data/ COPY data/bib.sqlit[e] data/
RUN python3 docs/scripts/export_library.py RUN uv run --with pydantic python docs/scripts/export_library.py
# Stage 2: Build Docusaurus static site # Stage 2: Build Docusaurus static site

19
docs/docs/api/index.md Normal file
View File

@@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# API Reference
Auto-generated reference documentation for each package.
| Package | Description |
|---------|-------------|
| [aco](aco/) | ACO REACH analytics |
| [api](api/) | Homelab API clients |
| [bcda](bcda/) | Beneficiary Claims Data API |
| [bib](bib/) | Bibliography store |
| [bls](bls/) | Bureau of Labor Statistics |
| [ccw](ccw/) | Chronic Conditions Warehouse |
| [cms](cms/) | Shared CMS models |
| [pfs](pfs/) | Physician Fee Schedule |
| [rex](rex/) | Record expression engine |

View File

@@ -43,4 +43,4 @@ src/
## Links ## Links
- [Library](/library) — searchable bibliography of CMS rules, regulations, and manuals - [Library](/library) — searchable bibliography of CMS rules, regulations, and manuals
- [API Reference](/api) — auto-generated from docstrings - [API Reference](api/) — auto-generated from docstrings

View File

@@ -7,7 +7,7 @@ const config = {
favicon: "img/favicon.png", favicon: "img/favicon.png",
url: "http://docs.homelab.fhirworx.io", url: "http://docs.homelab.fhirworx.io",
baseUrl: "/", baseUrl: "/",
onBrokenLinks: "warn", onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn", onBrokenMarkdownLinks: "warn",
markdown: { markdown: {
@@ -36,6 +36,19 @@ const config = {
], ],
], ],
themes: [
[
"@easyops-cn/docusaurus-search-local",
{
hashed: true,
language: ["en"],
indexDocs: true,
indexPages: true,
docsRouteBasePath: "/",
},
],
],
themeConfig: themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */ /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({ ({

545
docs/package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@docusaurus/core": "^3.7.0", "@docusaurus/core": "^3.7.0",
"@docusaurus/preset-classic": "^3.7.0", "@docusaurus/preset-classic": "^3.7.0",
"@easyops-cn/docusaurus-search-local": "^0.55.1",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0", "prism-react-renderer": "^2.3.0",
@@ -4055,6 +4056,156 @@
"node": ">=20.0" "node": ">=20.0"
} }
}, },
"node_modules/@easyops-cn/autocomplete.js": {
"version": "0.38.1",
"resolved": "https://registry.npmjs.org/@easyops-cn/autocomplete.js/-/autocomplete.js-0.38.1.tgz",
"integrity": "sha512-drg76jS6syilOUmVNkyo1c7ZEBPcPuK+aJA7AksM5ZIIbV57DMHCywiCr+uHyv8BE5jUTU98j/H7gVrkHrWW3Q==",
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"immediate": "^3.2.3"
}
},
"node_modules/@easyops-cn/docusaurus-search-local": {
"version": "0.55.1",
"resolved": "https://registry.npmjs.org/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.55.1.tgz",
"integrity": "sha512-jmBKj1J+tajqNrCvECwKCQYTWwHVZDGApy8lLOYEPe+Dm0/f3Ccdw8BP5/OHNpltr7WDNY2roQXn+TWn2f1kig==",
"license": "MIT",
"dependencies": {
"@docusaurus/plugin-content-docs": "^2 || ^3",
"@docusaurus/theme-translations": "^2 || ^3",
"@docusaurus/utils": "^2 || ^3",
"@docusaurus/utils-common": "^2 || ^3",
"@docusaurus/utils-validation": "^2 || ^3",
"@easyops-cn/autocomplete.js": "^0.38.1",
"@node-rs/jieba": "^1.6.0",
"cheerio": "^1.0.0",
"clsx": "^2.1.1",
"comlink": "^4.4.2",
"debug": "^4.2.0",
"fs-extra": "^10.0.0",
"klaw-sync": "^6.0.0",
"lunr": "^2.3.9",
"lunr-languages": "^1.4.0",
"mark.js": "^8.11.1",
"tslib": "^2.4.0"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"@docusaurus/theme-common": "^2 || ^3",
"open-ask-ai": "^0.7.3",
"react": "^16.14.0 || ^17 || ^18 || ^19",
"react-dom": "^16.14.0 || 17 || ^18 || ^19"
},
"peerDependenciesMeta": {
"open-ask-ai": {
"optional": true
}
}
},
"node_modules/@easyops-cn/docusaurus-search-local/node_modules/cheerio": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
"integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
"license": "MIT",
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"encoding-sniffer": "^0.2.1",
"htmlparser2": "^10.1.0",
"parse5": "^7.3.0",
"parse5-htmlparser2-tree-adapter": "^7.1.0",
"parse5-parser-stream": "^7.1.2",
"undici": "^7.19.0",
"whatwg-mimetype": "^4.0.0"
},
"engines": {
"node": ">=20.18.1"
},
"funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
"node_modules/@easyops-cn/docusaurus-search-local/node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/@easyops-cn/docusaurus-search-local/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@easyops-cn/docusaurus-search-local/node_modules/htmlparser2": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"entities": "^7.0.1"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
"integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.0",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@hapi/hoek": { "node_modules/@hapi/hoek": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -4623,6 +4774,18 @@
"react": ">=16" "react": ">=16"
} }
}, },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@noble/hashes": { "node_modules/@noble/hashes": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
@@ -4635,6 +4798,259 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/@node-rs/jieba": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba/-/jieba-1.10.4.tgz",
"integrity": "sha512-GvDgi8MnBiyWd6tksojej8anIx18244NmIOc1ovEw8WKNUejcccLfyu8vj66LWSuoZuKILVtNsOy4jvg3aoxIw==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"optionalDependencies": {
"@node-rs/jieba-android-arm-eabi": "1.10.4",
"@node-rs/jieba-android-arm64": "1.10.4",
"@node-rs/jieba-darwin-arm64": "1.10.4",
"@node-rs/jieba-darwin-x64": "1.10.4",
"@node-rs/jieba-freebsd-x64": "1.10.4",
"@node-rs/jieba-linux-arm-gnueabihf": "1.10.4",
"@node-rs/jieba-linux-arm64-gnu": "1.10.4",
"@node-rs/jieba-linux-arm64-musl": "1.10.4",
"@node-rs/jieba-linux-x64-gnu": "1.10.4",
"@node-rs/jieba-linux-x64-musl": "1.10.4",
"@node-rs/jieba-wasm32-wasi": "1.10.4",
"@node-rs/jieba-win32-arm64-msvc": "1.10.4",
"@node-rs/jieba-win32-ia32-msvc": "1.10.4",
"@node-rs/jieba-win32-x64-msvc": "1.10.4"
}
},
"node_modules/@node-rs/jieba-android-arm-eabi": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-android-arm-eabi/-/jieba-android-arm-eabi-1.10.4.tgz",
"integrity": "sha512-MhyvW5N3Fwcp385d0rxbCWH42kqDBatQTyP8XbnYbju2+0BO/eTeCCLYj7Agws4pwxn2LtdldXRSKavT7WdzNA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-android-arm64": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-android-arm64/-/jieba-android-arm64-1.10.4.tgz",
"integrity": "sha512-XyDwq5+rQ+Tk55A+FGi6PtJbzf974oqnpyCcCPzwU3QVXJCa2Rr4Lci+fx8oOpU4plT3GuD+chXMYLsXipMgJA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-darwin-arm64": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-darwin-arm64/-/jieba-darwin-arm64-1.10.4.tgz",
"integrity": "sha512-G++RYEJ2jo0rxF9626KUy90wp06TRUjAsvY/BrIzEOX/ingQYV/HjwQzNPRR1P1o32a6/U8RGo7zEBhfdybL6w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-darwin-x64": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-darwin-x64/-/jieba-darwin-x64-1.10.4.tgz",
"integrity": "sha512-MmDNeOb2TXIZCPyWCi2upQnZpPjAxw5ZGEj6R8kNsPXVFALHIKMa6ZZ15LCOkSTsKXVC17j2t4h+hSuyYb6qfQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-freebsd-x64": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-freebsd-x64/-/jieba-freebsd-x64-1.10.4.tgz",
"integrity": "sha512-/x7aVQ8nqUWhpXU92RZqd333cq639i/olNpd9Z5hdlyyV5/B65LLy+Je2B2bfs62PVVm5QXRpeBcZqaHelp/bg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-linux-arm-gnueabihf": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm-gnueabihf/-/jieba-linux-arm-gnueabihf-1.10.4.tgz",
"integrity": "sha512-crd2M35oJBRLkoESs0O6QO3BBbhpv+tqXuKsqhIG94B1d02RVxtRIvSDwO33QurxqSdvN9IeSnVpHbDGkuXm3g==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-linux-arm64-gnu": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm64-gnu/-/jieba-linux-arm64-gnu-1.10.4.tgz",
"integrity": "sha512-omIzNX1psUzPcsdnUhGU6oHeOaTCuCjUgOA/v/DGkvWC1jLcnfXe4vdYbtXMh4XOCuIgS1UCcvZEc8vQLXFbXQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-linux-arm64-musl": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm64-musl/-/jieba-linux-arm64-musl-1.10.4.tgz",
"integrity": "sha512-Y/tiJ1+HeS5nnmLbZOE+66LbsPOHZ/PUckAYVeLlQfpygLEpLYdlh0aPpS5uiaWMjAXYZYdFkpZHhxDmSLpwpw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-linux-x64-gnu": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-x64-gnu/-/jieba-linux-x64-gnu-1.10.4.tgz",
"integrity": "sha512-WZO8ykRJpWGE9MHuZpy1lu3nJluPoeB+fIJJn5CWZ9YTVhNDWoCF4i/7nxz1ntulINYGQ8VVuCU9LD86Mek97g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-linux-x64-musl": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-x64-musl/-/jieba-linux-x64-musl-1.10.4.tgz",
"integrity": "sha512-uBBD4S1rGKcgCyAk6VCKatEVQb6EDD5I40v/DxODi5CuZVCANi9m5oee/MQbAoaX7RydA2f0OSCE9/tcwXEwUg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-wasm32-wasi": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-wasm32-wasi/-/jieba-wasm32-wasi-1.10.4.tgz",
"integrity": "sha512-Y2umiKHjuIJy0uulNDz9SDYHdfq5Hmy7jY5nORO99B4pySKkcrMjpeVrmWXJLIsEKLJwcCXHxz8tjwU5/uhz0A==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^0.2.3"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@node-rs/jieba-win32-arm64-msvc": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-arm64-msvc/-/jieba-win32-arm64-msvc-1.10.4.tgz",
"integrity": "sha512-nwMtViFm4hjqhz1it/juQnxpXgqlGltCuWJ02bw70YUDMDlbyTy3grCJPpQQpueeETcALUnTxda8pZuVrLRcBA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-win32-ia32-msvc": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-ia32-msvc/-/jieba-win32-ia32-msvc-1.10.4.tgz",
"integrity": "sha512-DCAvLx7Z+W4z5oKS+7vUowAJr0uw9JBw8x1Y23Xs/xMA4Em+OOSiaF5/tCJqZUCJ8uC4QeImmgDFiBqGNwxlyA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@node-rs/jieba-win32-x64-msvc": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-x64-msvc/-/jieba-win32-x64-msvc-1.10.4.tgz",
"integrity": "sha512-+sqemSfS1jjb+Tt7InNbNzrRh1Ua3vProVvC4BZRPg010/leCbGFFiQHpzcPRfpxAXZrzG5Y0YBTsPzN/I4yHQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -5184,6 +5600,16 @@
"node": ">=14.16" "node": ">=14.16"
} }
}, },
"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==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/body-parser": { "node_modules/@types/body-parser": {
"version": "1.19.6", "version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
@@ -6859,6 +7285,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/comlink": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
"integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==",
"license": "Apache-2.0"
},
"node_modules/comma-separated-tokens": { "node_modules/comma-separated-tokens": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@@ -8063,6 +8495,31 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/encoding-sniffer": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
"integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.3",
"whatwg-encoding": "^3.1.1"
},
"funding": {
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/encoding-sniffer/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.20.0", "version": "5.20.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
@@ -9777,6 +10234,12 @@
"node": ">=16.x" "node": ">=16.x"
} }
}, },
"node_modules/immediate": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz",
"integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==",
"license": "MIT"
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -10363,6 +10826,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.1.11"
}
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -10542,6 +11014,24 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"license": "MIT"
},
"node_modules/lunr-languages": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/lunr-languages/-/lunr-languages-1.14.0.tgz",
"integrity": "sha512-hWUAb2KqM3L7J5bcrngszzISY4BxrXn/Xhbb9TTCJYEGqlR1nG67/M14sp09+PTIRklobrn57IAxcdcO/ZFyNA==",
"license": "MPL-1.1"
},
"node_modules/mark.js": {
"version": "8.11.1",
"resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz",
"integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==",
"license": "MIT"
},
"node_modules/markdown-extensions": { "node_modules/markdown-extensions": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz",
@@ -13502,6 +13992,18 @@
"url": "https://github.com/inikulin/parse5?sponsor=1" "url": "https://github.com/inikulin/parse5?sponsor=1"
} }
}, },
"node_modules/parse5-parser-stream": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
"license": "MIT",
"dependencies": {
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5/node_modules/entities": { "node_modules/parse5/node_modules/entities": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
@@ -17317,6 +17819,15 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici": {
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz",
"integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.18.2", "version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
@@ -18236,6 +18747,40 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"@docusaurus/core": "^3.7.0", "@docusaurus/core": "^3.7.0",
"@docusaurus/preset-classic": "^3.7.0", "@docusaurus/preset-classic": "^3.7.0",
"@easyops-cn/docusaurus-search-local": "^0.55.1",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0", "prism-react-renderer": "^2.3.0",
@@ -25,8 +26,16 @@
"typescript": "~5.6.0" "typescript": "~5.6.0"
}, },
"browserslist": { "browserslist": {
"production": [">0.5%", "not dead", "not op_mini all"], "production": [
"development": ["last 3 chrome version", "last 3 firefox version", "last 5 safari version"] ">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
}, },
"engines": { "engines": {
"node": ">=18.0" "node": ">=18.0"

View File

@@ -48,8 +48,19 @@ def main() -> None:
serialized_items = [] serialized_items = []
for item in items: for item in items:
abstract = item.abstract or "" abstract = item.abstract or ""
if len(abstract) > 300: creator_rows = store._con().execute(
abstract = abstract[:297] + "..." "SELECT c.last_name, c.first_name FROM item_creators ic "
"JOIN creators c ON ic.creator_id = c.id "
"WHERE ic.item_id = (SELECT id FROM items WHERE key = ?) "
"ORDER BY ic.sort_order",
(item.key,),
).fetchall()
creators = [
f"{r['last_name']}, {r['first_name']}"
if r["first_name"]
else r["last_name"]
for r in creator_rows
]
serialized_items.append( serialized_items.append(
{ {
"key": item.key, "key": item.key,
@@ -61,6 +72,7 @@ def main() -> None:
"tags": item.tags, "tags": item.tags,
"collections": item.collections, "collections": item.collections,
"institution": item.institution, "institution": item.institution,
"creators": creators,
} }
) )
@@ -72,6 +84,8 @@ def main() -> None:
OUT_PATH.parent.mkdir(parents=True, exist_ok=True) OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUT_PATH.write_text(json.dumps(data, indent=2, default=str)) OUT_PATH.write_text(json.dumps(data, indent=2, default=str))
if serialized_items:
assert len(serialized_items) > 0, "library.json has no items"
print(f"Wrote {len(serialized_items)} items to {OUT_PATH}") print(f"Wrote {len(serialized_items)} items to {OUT_PATH}")

View File

@@ -1,4 +1,4 @@
import React, { useState, useMemo, useEffect } from "react"; import React, { useState, useMemo, useEffect, useRef } from "react";
import Layout from "@theme/Layout"; import Layout from "@theme/Layout";
// Types matching export_library.py output // Types matching export_library.py output
@@ -12,6 +12,7 @@ interface LibraryItem {
tags: string[]; tags: string[];
collections: string[]; collections: string[];
institution: string; institution: string;
creators: string[];
} }
interface Collection { interface Collection {
@@ -34,6 +35,15 @@ interface LibraryData {
const PAGE_SIZE = 100; const PAGE_SIZE = 100;
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Group tags by namespace (e.g., "module:pfs" -> "module") // Group tags by namespace (e.g., "module:pfs" -> "module")
function groupTags(tags: Tag[]): Record<string, Tag[]> { function groupTags(tags: Tag[]): Record<string, Tag[]> {
const groups: Record<string, Tag[]> = {}; const groups: Record<string, Tag[]> = {};
@@ -47,12 +57,24 @@ function groupTags(tags: Tag[]): Record<string, Tag[]> {
} }
export default function Library(): JSX.Element { export default function Library(): JSX.Element {
const initParams =
typeof window !== "undefined"
? new URLSearchParams(window.location.search)
: new URLSearchParams();
const [data, setData] = useState<LibraryData | null>(null); const [data, setData] = useState<LibraryData | null>(null);
const [search, setSearch] = useState(""); const [search, setSearch] = useState(initParams.get("q") || "");
const [selectedCollection, setSelectedCollection] = useState(""); const [selectedCollection, setSelectedCollection] = useState(
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set()); initParams.get("collection") || "",
);
const [selectedTags, setSelectedTags] = useState<Set<string>>(
new Set(initParams.get("tags")?.split(",").filter(Boolean) || []),
);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const debouncedSearch = useDebounce(search, 200);
const searchRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
fetch("/library.json") fetch("/library.json")
.then((r) => r.json()) .then((r) => r.json())
@@ -60,6 +82,30 @@ export default function Library(): JSX.Element {
.catch(() => setData({ items: [], collections: [], tags: [] })); .catch(() => setData({ items: [], collections: [], tags: [] }));
}, []); }, []);
// Sync filters to URL params
useEffect(() => {
const params = new URLSearchParams();
if (search) params.set("q", search);
if (selectedCollection) params.set("collection", selectedCollection);
if (selectedTags.size > 0) params.set("tags", [...selectedTags].join(","));
const url = params.toString()
? `?${params}`
: window.location.pathname;
window.history.replaceState(null, "", url);
}, [search, selectedCollection, selectedTags]);
// Cmd+K / Ctrl+K to focus search
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
searchRef.current?.focus();
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);
const tagGroups = useMemo( const tagGroups = useMemo(
() => (data ? groupTags(data.tags) : {}), () => (data ? groupTags(data.tags) : {}),
[data], [data],
@@ -79,18 +125,19 @@ export default function Library(): JSX.Element {
); );
} }
if (search.trim()) { if (debouncedSearch.trim()) {
const q = search.toLowerCase(); const q = debouncedSearch.toLowerCase();
items = items.filter( items = items.filter(
(i) => (i) =>
i.title.toLowerCase().includes(q) || i.title.toLowerCase().includes(q) ||
i.abstract.toLowerCase().includes(q) || i.abstract.toLowerCase().includes(q) ||
i.tags.some((t) => t.toLowerCase().includes(q)), i.tags.some((t) => t.toLowerCase().includes(q)) ||
i.creators.some((c) => c.toLowerCase().includes(q)),
); );
} }
return items; return items;
}, [data, search, selectedCollection, selectedTags]); }, [data, debouncedSearch, selectedCollection, selectedTags]);
const pageItems = useMemo( const pageItems = useMemo(
() => filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE), () => filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE),
@@ -210,8 +257,9 @@ export default function Library(): JSX.Element {
<section style={{ flex: 1, minWidth: 0 }}> <section style={{ flex: 1, minWidth: 0 }}>
<div style={{ marginBottom: "1rem" }}> <div style={{ marginBottom: "1rem" }}>
<input <input
ref={searchRef}
type="text" type="text"
placeholder="Search title, abstract, or tags..." placeholder="Search title, abstract, tags, or authors... (⌘K)"
value={search} value={search}
onChange={(e) => { onChange={(e) => {
setSearch(e.target.value); setSearch(e.target.value);

0
tests/docs/__init__.py Normal file
View File

View File

@@ -0,0 +1,144 @@
"""Integration tests for the docs build pipeline."""
from __future__ import annotations
import json
import re
import subprocess
import sys
from pathlib import Path
BIB_DB = Path("data/bib.sqlite")
EXPORT_SCRIPT = Path("docs/scripts/export_library.py")
LIBRARY_JSON = Path("docs/static/library.json")
DOCS_DIR = Path("docs/docs")
INTRO_MD = DOCS_DIR / "intro.md"
class TestExportLibrary:
"""Tests for docs/scripts/export_library.py."""
def test_export_runs_without_error(self):
result = subprocess.run(
[sys.executable, str(EXPORT_SCRIPT)],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_library_json_has_items(self):
"""library.json should have >0 items when bib.sqlite exists."""
if not BIB_DB.exists():
import pytest
pytest.skip("bib.sqlite not available")
# Run the export
subprocess.run([sys.executable, str(EXPORT_SCRIPT)], check=True)
data = json.loads(LIBRARY_JSON.read_text())
assert len(data["items"]) > 0, "library.json has no items"
assert len(data["tags"]) > 0, "library.json has no tags"
def test_item_has_required_fields(self):
"""Each item must have all required fields."""
if not LIBRARY_JSON.exists():
import pytest
pytest.skip("library.json not generated")
data = json.loads(LIBRARY_JSON.read_text())
if not data["items"]:
import pytest
pytest.skip("no items in library.json")
required = {
"key",
"item_type",
"title",
"url",
"abstract",
"tags",
"collections",
"creators",
}
for item in data["items"][:10]: # spot check first 10
missing = required - set(item.keys())
assert not missing, f"Item {item.get('key', '?')} missing fields: {missing}"
def test_abstracts_not_truncated(self):
"""Abstracts should not be truncated to 300 chars."""
if not LIBRARY_JSON.exists():
import pytest
pytest.skip("library.json not generated")
data = json.loads(LIBRARY_JSON.read_text())
# Find items with long abstracts
truncated = [
i["key"]
for i in data["items"]
if i["abstract"].endswith("...") and len(i["abstract"]) == 300
]
assert not truncated, f"Found truncated abstracts: {truncated[:5]}"
def test_tags_in_items_match_tag_list(self):
"""Every tag referenced by items should appear in the tags list."""
if not LIBRARY_JSON.exists():
import pytest
pytest.skip("library.json not generated")
data = json.loads(LIBRARY_JSON.read_text())
tag_names = {t["name"] for t in data["tags"]}
item_tags = set()
for item in data["items"]:
item_tags.update(item["tags"])
orphans = item_tags - tag_names
assert not orphans, f"Tags on items but not in tags list: {orphans}"
class TestIntroLinks:
"""Verify intro.md links resolve to real pages."""
def test_intro_exists(self):
assert INTRO_MD.exists(), "intro.md not found"
def test_no_broken_relative_links(self):
"""All relative links in intro.md should point to existing files."""
if not INTRO_MD.exists():
import pytest
pytest.skip("intro.md not found")
content = INTRO_MD.read_text()
# Find markdown links like [text](path)
links = re.findall(r"\[.*?\]\(([^)]+)\)", content)
for link in links:
if link.startswith("http") or link.startswith("#"):
continue
# Relative link — check it resolves
target = DOCS_DIR / link.lstrip("/")
# Could be a directory with index.md or a .md file
exists = (
target.exists()
or target.with_suffix(".md").exists()
or (target / "index.md").exists()
or target.parent.exists() # directory exists
)
assert exists, f"Broken link in intro.md: {link}"
class TestExtractDocs:
"""Verify the API doc extraction produces output."""
def test_api_docs_directory_exists(self):
api_dir = DOCS_DIR / "api"
if not api_dir.exists():
import pytest
pytest.skip("API docs not generated yet")
assert api_dir.is_dir()
def test_api_has_packages(self):
api_dir = DOCS_DIR / "api"
if not api_dir.exists():
import pytest
pytest.skip("API docs not generated yet")
packages = [d.name for d in api_dir.iterdir() if d.is_dir()]
assert len(packages) > 0, "No API doc packages found"