docs search: local plugin, enriched library, tests, UX
## 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:
@@ -12,7 +12,7 @@ RUN uv run --with griffe python docs/scripts/extract_docs.py
|
||||
|
||||
# Export bibliography (gracefully handles missing bib.sqlite)
|
||||
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
|
||||
|
||||
19
docs/docs/api/index.md
Normal file
19
docs/docs/api/index.md
Normal 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 |
|
||||
@@ -43,4 +43,4 @@ src/
|
||||
## Links
|
||||
|
||||
- [Library](/library) — searchable bibliography of CMS rules, regulations, and manuals
|
||||
- [API Reference](/api) — auto-generated from docstrings
|
||||
- [API Reference](api/) — auto-generated from docstrings
|
||||
|
||||
@@ -7,7 +7,7 @@ const config = {
|
||||
favicon: "img/favicon.png",
|
||||
url: "http://docs.homelab.fhirworx.io",
|
||||
baseUrl: "/",
|
||||
onBrokenLinks: "warn",
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
|
||||
markdown: {
|
||||
@@ -36,6 +36,19 @@ const config = {
|
||||
],
|
||||
],
|
||||
|
||||
themes: [
|
||||
[
|
||||
"@easyops-cn/docusaurus-search-local",
|
||||
{
|
||||
hashed: true,
|
||||
language: ["en"],
|
||||
indexDocs: true,
|
||||
indexPages: true,
|
||||
docsRouteBasePath: "/",
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
|
||||
545
docs/package-lock.json
generated
545
docs/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.7.0",
|
||||
"@docusaurus/preset-classic": "^3.7.0",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.55.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
@@ -4055,6 +4056,156 @@
|
||||
"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": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||
@@ -4623,6 +4774,18 @@
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
|
||||
@@ -4635,6 +4798,259 @@
|
||||
"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": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -5184,6 +5600,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": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
@@ -6859,6 +7285,12 @@
|
||||
"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": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
@@ -8063,6 +8495,31 @@
|
||||
"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": {
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
|
||||
@@ -9777,6 +10234,12 @@
|
||||
"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": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@@ -10363,6 +10826,15 @@
|
||||
"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": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
@@ -10542,6 +11014,24 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
@@ -17317,6 +17819,15 @@
|
||||
"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": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
@@ -18236,6 +18747,40 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.7.0",
|
||||
"@docusaurus/preset-classic": "^3.7.0",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.55.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
@@ -25,8 +26,16 @@
|
||||
"typescript": "~5.6.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [">0.5%", "not dead", "not op_mini all"],
|
||||
"development": ["last 3 chrome version", "last 3 firefox version", "last 5 safari version"]
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 3 chrome version",
|
||||
"last 3 firefox version",
|
||||
"last 5 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
|
||||
@@ -48,8 +48,19 @@ def main() -> None:
|
||||
serialized_items = []
|
||||
for item in items:
|
||||
abstract = item.abstract or ""
|
||||
if len(abstract) > 300:
|
||||
abstract = abstract[:297] + "..."
|
||||
creator_rows = store._con().execute(
|
||||
"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(
|
||||
{
|
||||
"key": item.key,
|
||||
@@ -61,6 +72,7 @@ def main() -> None:
|
||||
"tags": item.tags,
|
||||
"collections": item.collections,
|
||||
"institution": item.institution,
|
||||
"creators": creators,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -72,6 +84,8 @@ def main() -> None:
|
||||
|
||||
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
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}")
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import React, { useState, useMemo, useEffect, useRef } from "react";
|
||||
import Layout from "@theme/Layout";
|
||||
|
||||
// Types matching export_library.py output
|
||||
@@ -12,6 +12,7 @@ interface LibraryItem {
|
||||
tags: string[];
|
||||
collections: string[];
|
||||
institution: string;
|
||||
creators: string[];
|
||||
}
|
||||
|
||||
interface Collection {
|
||||
@@ -34,6 +35,15 @@ interface LibraryData {
|
||||
|
||||
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")
|
||||
function groupTags(tags: Tag[]): 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 {
|
||||
const initParams =
|
||||
typeof window !== "undefined"
|
||||
? new URLSearchParams(window.location.search)
|
||||
: new URLSearchParams();
|
||||
|
||||
const [data, setData] = useState<LibraryData | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [selectedCollection, setSelectedCollection] = useState("");
|
||||
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
|
||||
const [search, setSearch] = useState(initParams.get("q") || "");
|
||||
const [selectedCollection, setSelectedCollection] = useState(
|
||||
initParams.get("collection") || "",
|
||||
);
|
||||
const [selectedTags, setSelectedTags] = useState<Set<string>>(
|
||||
new Set(initParams.get("tags")?.split(",").filter(Boolean) || []),
|
||||
);
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const debouncedSearch = useDebounce(search, 200);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/library.json")
|
||||
.then((r) => r.json())
|
||||
@@ -60,6 +82,30 @@ export default function Library(): JSX.Element {
|
||||
.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(
|
||||
() => (data ? groupTags(data.tags) : {}),
|
||||
[data],
|
||||
@@ -79,18 +125,19 @@ export default function Library(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
if (search.trim()) {
|
||||
const q = search.toLowerCase();
|
||||
if (debouncedSearch.trim()) {
|
||||
const q = debouncedSearch.toLowerCase();
|
||||
items = items.filter(
|
||||
(i) =>
|
||||
i.title.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;
|
||||
}, [data, search, selectedCollection, selectedTags]);
|
||||
}, [data, debouncedSearch, selectedCollection, selectedTags]);
|
||||
|
||||
const pageItems = useMemo(
|
||||
() => 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 }}>
|
||||
<div style={{ marginBottom: "1rem" }}>
|
||||
<input
|
||||
ref={searchRef}
|
||||
type="text"
|
||||
placeholder="Search title, abstract, or tags..."
|
||||
placeholder="Search title, abstract, tags, or authors... (⌘K)"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
|
||||
0
tests/docs/__init__.py
Normal file
0
tests/docs/__init__.py
Normal file
144
tests/docs/test_docs_build.py
Normal file
144
tests/docs/test_docs_build.py
Normal 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"
|
||||
Reference in New Issue
Block a user