From 135db0214a3d21469205e1a181e473e0aeedf013 Mon Sep 17 00:00:00 2001 From: Mike Barkmin Date: Tue, 12 Nov 2024 22:47:42 +0100 Subject: [PATCH 1/7] add lunr search --- packages/hyperbook/build.ts | 34 +++++- packages/hyperbook/index.ts | 1 - packages/hyperbook/package.json | 8 +- packages/markdown/assets/client.js | 51 ++++++++- packages/markdown/assets/content.css | 101 ++++++++++++++++++ packages/markdown/assets/shell.css | 17 +-- packages/markdown/devBuild.mjs | 3 +- packages/markdown/package.json | 3 + packages/markdown/postbuild.mjs | 4 + packages/markdown/src/index.ts | 11 +- packages/markdown/src/mdastUtilToText.ts | 61 +++++++++++ packages/markdown/src/process.ts | 4 +- packages/markdown/src/rehypeHtmlStructure.ts | 26 ++++- packages/markdown/src/rehypeShell.ts | 75 +++++++++++++ .../markdown/src/remarkCollectHeadings.ts | 14 +-- .../src/remarkCollectSearchDocuments.ts | 48 +++++++++ .../tests/__snapshots__/process.test.ts.snap | 8 +- .../remarkCollectSearchDocuments.test.ts | 78 ++++++++++++++ packages/types/src/index.ts | 1 + .../vscode/schemas/hyperbook.schema.json | 3 + pnpm-lock.yaml | 73 ++++++++++--- website/de/book/configuration/book.md | 3 + website/de/book/elements/excalidraw.md | 2 + website/de/hyperbook.json | 1 + website/en/book/configuration/book.md | 3 + website/en/hyperbook.json | 1 + 26 files changed, 584 insertions(+), 50 deletions(-) create mode 100644 packages/markdown/src/mdastUtilToText.ts create mode 100644 packages/markdown/src/remarkCollectSearchDocuments.ts create mode 100644 packages/markdown/tests/remarkCollectSearchDocuments.test.ts diff --git a/packages/hyperbook/build.ts b/packages/hyperbook/build.ts index ba9696c1..2310e9b9 100644 --- a/packages/hyperbook/build.ts +++ b/packages/hyperbook/build.ts @@ -12,9 +12,10 @@ import { HyperbookContext, Navigation, } from "@hyperbook/types"; +import lunr from "lunr"; import { process as hyperbookProcess } from "@hyperbook/markdown"; -const ASSETS_FOLDER = "__hyperbook_assets"; +export const ASSETS_FOLDER = "__hyperbook_assets"; export async function runBuildProject( project: Hyperproject, @@ -129,6 +130,7 @@ async function runBuild( pagesAndSections.sections, pagesAndSections.pages, ); + const searchDocuments: any[] = []; let bookFiles = await vfile.listForFolder(root, "book"); if (filter) { @@ -147,6 +149,7 @@ async function runBuild( navigation, }; const result = await hyperbookProcess(file.markdown.content, ctx); + searchDocuments.push(...(result.data.searchDocuments || [])); for (let directive of Object.keys(result.data.directives || {})) { directives.add(directive); } @@ -204,6 +207,7 @@ async function runBuild( navigation, }; const result = await hyperbookProcess(file.markdown.content, ctx); + searchDocuments.push(...(result.data.searchDocuments || [])); for (let directive of Object.keys(result.data.directives || {})) { directives.add(directive); } @@ -309,5 +313,33 @@ async function runBuild( } process.stdout.write("\n"); + if (hyperbookJson.search) { + const documents: Record = {}; + const idx = lunr(function () { + this.ref("href"); + this.field("description"); + this.field("keywords"); + this.field("heading"); + this.field("content"); + + searchDocuments.forEach((doc) => { + const href = baseCtx.makeUrl(doc.href, "book"); + const docWithBase = { + ...doc, + href, + }; + this.add(docWithBase); + documents[href] = docWithBase; + }); + }); + + const js = ` +const LUNR_INDEX = ${JSON.stringify(idx)}; +const SEARCH_DOCUMENTS = ${JSON.stringify(documents)}; +`; + + await fs.writeFile(path.join(rootOut, ASSETS_FOLDER, "search.js"), js); + } + console.log(`${chalk.green(`[${prefix}]`)} Build success: ${rootOut}`); } diff --git a/packages/hyperbook/index.ts b/packages/hyperbook/index.ts index 0cb68ef8..cb94caa1 100644 --- a/packages/hyperbook/index.ts +++ b/packages/hyperbook/index.ts @@ -9,7 +9,6 @@ import { getPkgManager } from "./helpers/get-pkg-manager"; import { hyperproject } from "@hyperbook/fs"; import { runNew } from "./new"; import packageJson from "./package.json"; -import { deprecate } from "util"; const program = new Command(); diff --git a/packages/hyperbook/package.json b/packages/hyperbook/package.json index f6419615..fe8450db 100644 --- a/packages/hyperbook/package.json +++ b/packages/hyperbook/package.json @@ -36,6 +36,7 @@ "@types/archiver": "6.0.2", "@types/async-retry": "1.4.8", "@types/cross-spawn": "6.0.6", + "@types/lunr": "^2.3.7", "@types/prompts": "2.4.9", "@types/tar": "6.1.13", "@types/ws": "^8.5.12", @@ -43,16 +44,19 @@ "archiver": "7.0.1", "async-retry": "1.3.3", "chalk": "5.2.0", + "chokidar": "3.6.0", "commander": "12.1.0", "cpy": "11.0.1", "cross-spawn": "7.0.3", + "domutils": "^3.1.0", "got": "12.6.0", + "htmlparser2": "^9.1.0", + "lunr": "^2.3.9", + "mime": "^4.0.4", "prompts": "2.4.2", "rimraf": "5.0.7", "tar": "6.2.1", "update-check": "1.5.4", - "chokidar": "3.6.0", - "mime": "^4.0.4", "ws": "^8.18.0" }, "dependencies": { diff --git a/packages/markdown/assets/client.js b/packages/markdown/assets/client.js index 123395f5..0cb5e46f 100644 --- a/packages/markdown/assets/client.js +++ b/packages/markdown/assets/client.js @@ -33,13 +33,60 @@ var hyperbook = (function () { const tocDrawerEl = document.getElementById("toc-drawer"); tocDrawerEl.open = !tocDrawerEl.open; } + // search + + const searchInputEl = document.getElementById("search-input"); + searchInputEl.addEventListener("keypress", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + search(); + } + }); + + function searchToggle() { + const searchDrawerEl = document.getElementById("search-drawer"); + searchDrawerEl.open = !searchDrawerEl.open; + } + + function search() { + const resultsEl = document.getElementById("search-results"); + resultsEl.innerHTML = ""; + const query = searchInputEl.value; + const idx = window.lunr.Index.load(LUNR_INDEX); + const documents = SEARCH_DOCUMENTS; + const results = idx.search(query); + for (let result of results) { + const doc = documents[result.ref]; + + const container = document.createElement("a"); + container.href = doc.href; + container.classList.add("search-result"); + const heading = document.createElement("div"); + heading.textContent = doc.heading; + heading.classList.add("search-result-heading"); + const content = document.createElement("div"); + content.classList.add("search-result-content"); + const href = document.createElement("div"); + href.classList.add("search-result-href"); + href.textContent = doc.href; + if (doc.content.length > 200) { + content.textContent = doc.content.slice(0, 197) + "..."; + } else { + content.textContent = doc.content; + } + + container.appendChild(heading); + container.appendChild(content); + container.appendChild(href); + resultsEl.appendChild(container); + } + } function qrcodeOpen() { const qrCodeDialog = document.getElementById("qrcode-dialog"); const qrcodeEls = qrCodeDialog.getElementsByClassName("make-qrcode"); const urlEls = qrCodeDialog.getElementsByClassName("url"); const qrcodeEl = qrcodeEls[0]; - const urlEl = urlEls[0]; const qrcode = new window.QRCode({ content: window.location.href, padding: 0, @@ -113,6 +160,8 @@ var hyperbook = (function () { toggleBookmark, navToggle, tocToggle, + searchToggle, + search, qrcodeOpen, qrcodeClose, }; diff --git a/packages/markdown/assets/content.css b/packages/markdown/assets/content.css index f09fd01e..5e3fd337 100644 --- a/packages/markdown/assets/content.css +++ b/packages/markdown/assets/content.css @@ -377,6 +377,106 @@ figure { cursor: pointer; } +#search-toggle { + background-color: var(--color-text); + width: 24px; + height: 24px; + cursor: pointer; + margin-right: 16px; + mask-size: 100%; + mask-repeat: no-repeat; + mask-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgY2xhc3M9ImZlYXRoZXIgZmVhdGhlci1zZWFyY2giIGZpbGw9Im5vbmUiIGhlaWdodD0iMjQiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMTEiIGN5PSIxMSIgcj0iOCIvPjxsaW5lIHgxPSIyMSIgeDI9IjE2LjY1IiB5MT0iMjEiIHkyPSIxNi42NSIvPjwvc3ZnPg==') +} + +#search-toggle:hover { + background-color: var(--color-brand); +} + +#search-drawer { + overflow-y: auto; + background: var(--color-nav); +} + +#search-drawer .search-input { + display: flex; + margin-top: 16px; + margin-bottom: 16px; + gap: 8px; + border: 1px solid var(--color-nav-border); + background-color: var(--color-nav); + padding: 8px; + border-radius: 4px; + align-items: center; + justify-content: center; +} + +#search-drawer .search-input .search-icon { + background-color: var(--color-text); + width: 24px; + height: 24px; + mask-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgY2xhc3M9ImZlYXRoZXIgZmVhdGhlci1zZWFyY2giIGZpbGw9Im5vbmUiIGhlaWdodD0iMjQiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMTEiIGN5PSIxMSIgcj0iOCIvPjxsaW5lIHgxPSIyMSIgeDI9IjE2LjY1IiB5MT0iMjEiIHkyPSIxNi42NSIvPjwvc3ZnPg==') +} + +#search-drawer .search-input input { + flex: 1; + background: none; + color: var(--color-text); + border: none; +} + +#search-drawer .search-input .search-button { + background-color: var(--color-text); + width: 24px; + height: 24px; + cursor: pointer; + mask-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgY2xhc3M9ImZlYXRoZXIgZmVhdGhlci1hcnJvdy1yaWdodCIgZmlsbD0ibm9uZSIgaGVpZ2h0PSIyNCIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZSB4MT0iNSIgeDI9IjE5IiB5MT0iMTIiIHkyPSIxMiIvPjxwb2x5bGluZSBwb2ludHM9IjEyIDUgMTkgMTIgMTIgMTkiLz48L3N2Zz4=') +} + +#search-results { + display: flex; + flex-direction: column; + gap: 20px; +} + +#search-results .search-result { + display: flex; + gap: 4px; + flex-direction: column; + text-decoration: none; + color: var(--color-text); + font-size: 1rem; + border-radius: 8px; + background-color: var(--color-nav-border); + padding: 8px; +} + +#search-results .search-result-heading { + font-size: 1.2rem; + font-weight: 500; +} + +#search-results .search-result-content { + text-overflow: ellipsis; + font-size: 0.8rem; +} + +#search-results .search-result-href { + text-overflow: ellipsis; + overflow: hidden; + text-wrap: nowrap; + font-size: 0.8rem; + font-weight: 500; +} + +.search-drawer-content { + display: flex; + flex-direction: column; + padding: 8px; + height: calc(var(--app-height)); + overflow-y: auto; + gap: 8px; +} + #toc-drawer { overflow-y: auto; background: var(--color-nav); @@ -385,6 +485,7 @@ figure { .toc-drawer-content { display: flex; flex-direction: column; + height: calc(var(--app-height)); } .toc-drawer-content>nav { diff --git a/packages/markdown/assets/shell.css b/packages/markdown/assets/shell.css index 757e00ea..96151583 100644 --- a/packages/markdown/assets/shell.css +++ b/packages/markdown/assets/shell.css @@ -24,23 +24,13 @@ li { margin: 0; } -#nav-drawer { - background: var(--color-nav); - border-right-color: var(--color-nav-border); -} - side-drawer { position: absolute; - display: block; - visibility: hidden; - max-width: 0; - max-height: 0; } -side-drawer[open] { - visibility: visible; - max-width: 350px; - max-height: initial; +#nav-drawer { + background: var(--color-nav); + border-right-color: var(--color-nav-border); } .sidebar>.author, @@ -77,6 +67,7 @@ header.inverted { .branding { color: var(--color-brand-text); + flex: 1; } .branding:hover { diff --git a/packages/markdown/devBuild.mjs b/packages/markdown/devBuild.mjs index 122ed89e..f390abf7 100644 --- a/packages/markdown/devBuild.mjs +++ b/packages/markdown/devBuild.mjs @@ -6,10 +6,11 @@ const markdown = await fs.readFile("dev.md", "utf8"); const result = await process(markdown, { root: "", - qrcode: true, config: { name: "Hyperbook Dokumenation", description: "Dokumentation für Hyperbook erstellt mit Hyperbook", + qrcode: true, + search: true, author: { name: "OpenPatch", url: "https://openpatch.org", diff --git a/packages/markdown/package.json b/packages/markdown/package.json index 6a137479..c71d938a 100644 --- a/packages/markdown/package.json +++ b/packages/markdown/package.json @@ -42,6 +42,7 @@ "decircular": "^1.0.0", "handlebars": "^4.7.8", "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", "is-obj": "^3.0.0", "js-base64": "^3.7.7", "mdast-util-directive": "^3.0.0", @@ -65,6 +66,7 @@ "remark-unwrap-images": "4.0.0", "sort-keys": "^5.1.0", "unified": "^11.0.5", + "unist-util-find-after": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" }, @@ -80,6 +82,7 @@ "chalk": "^5.3.0", "chokidar": "3.6.0", "live-server": "^1.2.2", + "lunr": "^2.3.9", "mermaid": "11.3.0", "ncp": "^2.0.0", "scratchblocks": "^3.6.4", diff --git a/packages/markdown/postbuild.mjs b/packages/markdown/postbuild.mjs index af3fa5dc..b8e7e83c 100644 --- a/packages/markdown/postbuild.mjs +++ b/packages/markdown/postbuild.mjs @@ -79,6 +79,10 @@ async function postbuild() { "hyperbook-excalidraw.umd.js", ), }, + { + src: path.join("./node_modules", "lunr", "lunr.min.js"), + dst: path.join("./dist", "assets", "lunr.min.js"), + }, ]; for (let asset of assets) { diff --git a/packages/markdown/src/index.ts b/packages/markdown/src/index.ts index d6ce5cc2..535ffd4b 100644 --- a/packages/markdown/src/index.ts +++ b/packages/markdown/src/index.ts @@ -1,4 +1,4 @@ -import { process } from "./process"; +import { process, remark } from "./process"; declare module "vfile" { interface DataMap { @@ -14,6 +14,13 @@ declare module "vfile" { anchor: string; label: string; }[]; + searchDocuments: { + description: string; + keywords: string[]; + heading: string; + content: string; + href: string; + }[]; } } @@ -23,4 +30,4 @@ declare module "mdast" { } } -export { process }; +export { process, remark }; diff --git a/packages/markdown/src/mdastUtilToText.ts b/packages/markdown/src/mdastUtilToText.ts new file mode 100644 index 00000000..16d912fa --- /dev/null +++ b/packages/markdown/src/mdastUtilToText.ts @@ -0,0 +1,61 @@ +import { Nodes } from "mdast"; + +const emptyOptions: any = {}; + +export function toText(value: any, options?: any): string { + const settings = options || emptyOptions; + const includeImageAlt = + typeof settings.includeImageAlt === "boolean" + ? settings.includeImageAlt + : true; + const includeHtml = + typeof settings.includeHtml === "boolean" ? settings.includeHtml : true; + + return one(value, includeImageAlt, includeHtml); +} + +function one( + value: any, + includeImageAlt: boolean, + includeHtml: boolean, +): string { + if (node(value)) { + if ("value" in value) { + return value.type === "html" && !includeHtml ? "" : value.value; + } + + if (includeImageAlt && "alt" in value && value.alt) { + return value.alt; + } + + if ("children" in value) { + return all(value.children, includeImageAlt, includeHtml); + } + } + + if (Array.isArray(value)) { + return all(value, includeImageAlt, includeHtml); + } + + return ""; +} + +function all( + values: Array, + includeImageAlt: boolean, + includeHtml: boolean, +): string { + /** @type {Array} */ + const result: string[] = []; + let index = -1; + + while (++index < values.length) { + result[index] = one(values[index], includeImageAlt, includeHtml); + } + + return result.join(" "); +} + +function node(value: any): value is Nodes { + return Boolean(value && typeof value === "object"); +} diff --git a/packages/markdown/src/process.ts b/packages/markdown/src/process.ts index 5f367678..81573006 100644 --- a/packages/markdown/src/process.ts +++ b/packages/markdown/src/process.ts @@ -46,6 +46,7 @@ import remarkDirectiveTerm from "./remarkDirectiveTerm"; import remarkLink from "./remarkLink"; import remarkDirectivePagelist from "./remarkDirectivePagelist"; import rehypeQrCode from "./rehypeQrCode"; +import remarkCollectSearchDocuments from "./remarkCollectSearchDocuments"; export const remark = (ctx: HyperbookContext) => { const remarkPlugins: PluggableList = [ @@ -78,13 +79,14 @@ export const remark = (ctx: HyperbookContext) => { remarkDirectiveMermaid(ctx), remarkDirectiveExcalidraw(ctx), remarkDirectiveStruktog(ctx), - remarkCollectHeadings(ctx), remarkCode(ctx), remarkMath, remarkGemoji, remarkUnwrapImages, /* needs to be last directive */ remarkDirectiveProtect(ctx), + remarkCollectHeadings(ctx), + remarkCollectSearchDocuments(ctx), ]; const rehypePlugins: PluggableList = [ diff --git a/packages/markdown/src/rehypeHtmlStructure.ts b/packages/markdown/src/rehypeHtmlStructure.ts index 3e5839b2..db94e246 100644 --- a/packages/markdown/src/rehypeHtmlStructure.ts +++ b/packages/markdown/src/rehypeHtmlStructure.ts @@ -222,7 +222,7 @@ export default (ctx: HyperbookContext) => () => { type: "element", tagName: "meta", properties: { - property: "description", + name: "description", content: `${currentPage?.description || config.description}`, }, children: [], @@ -240,7 +240,7 @@ export default (ctx: HyperbookContext) => () => { type: "element", tagName: "meta", properties: { - property: "keywords", + name: "keywords", content: currentPage?.keywords ? `${currentPage?.keywords.join("")}` : undefined, @@ -335,6 +335,28 @@ HYPERBOOK_ASSETS = "${makeUrl("/", "assets")}" }, children: [], }, + ...(ctx.config.search + ? [ + { + type: "element", + tagName: "script", + properties: { + src: makeUrl(["lunr.min.js"], "assets"), + async: true, + }, + children: [], + } as ElementContent, + { + type: "element", + tagName: "script", + properties: { + src: makeUrl(["search.js"], "assets"), + async: true, + }, + children: [], + } as ElementContent, + ] + : []), { type: "element", tagName: "script", diff --git a/packages/markdown/src/rehypeShell.ts b/packages/markdown/src/rehypeShell.ts index 1a7bca6c..93819881 100644 --- a/packages/markdown/src/rehypeShell.ts +++ b/packages/markdown/src/rehypeShell.ts @@ -585,6 +585,81 @@ const makeHeaderElements = (ctx: HyperbookContext): ElementContent[] => { }); } + if (ctx.config.search) { + elements.push({ + type: "element", + tagName: "button", + properties: { + id: "search-toggle", + onclick: "hyperbook.searchToggle()", + title: "Search", + }, + children: [], + }); + elements.push({ + type: "element", + tagName: "side-drawer", + properties: { + id: "search-drawer", + right: true, + }, + children: [ + { + type: "element", + tagName: "div", + properties: { + class: "search-drawer-content", + }, + children: [ + { + type: "element", + tagName: "div", + properties: { + class: "search-input", + }, + children: [ + { + type: "element", + tagName: "div", + properties: { + class: "search-icon", + }, + children: [], + }, + { + type: "element", + tagName: "input", + properties: { + id: "search-input", + placerholder: "...", + }, + children: [], + }, + { + type: "element", + tagName: "button", + properties: { + class: "search-button", + onclick: "hyperbook.search()", + }, + children: [], + }, + ], + }, + { + type: "element", + tagName: "div", + properties: { + id: "search-results", + }, + children: [], + }, + ], + }, + ], + }); + } + return [ { type: "element", diff --git a/packages/markdown/src/remarkCollectHeadings.ts b/packages/markdown/src/remarkCollectHeadings.ts index 3f8cd830..ffc2a372 100644 --- a/packages/markdown/src/remarkCollectHeadings.ts +++ b/packages/markdown/src/remarkCollectHeadings.ts @@ -2,9 +2,9 @@ /// // import { HyperbookContext } from "@hyperbook/types"; -import { Root } from "mdast"; +import { Node, Root } from "mdast"; import { visit } from "unist-util-visit"; -import { VFile } from "vfile"; +import { VFile, VFileData } from "vfile"; import { Root as MdastRoot, Heading as AstHeading } from "mdast"; import { toString } from "mdast-util-to-string"; @@ -15,7 +15,7 @@ export default (ctx: HyperbookContext) => () => { }; }; -const getAnchor = (heading: AstHeading): string => { +export const getAnchor = (heading: AstHeading): string => { // If we have a heading, make it lower case if ((heading?.data as any)?.id) { return (heading.data as any).id as string; @@ -32,12 +32,8 @@ const getAnchor = (heading: AstHeading): string => { return anchor; }; -const getHeadings = (root: MdastRoot) => { - const headingList: { - level: 1 | 2 | 3 | 4 | 5 | 6; - label: string; - anchor: string; - }[] = []; +const getHeadings = (root: MdastRoot): VFileData["headings"] => { + const headingList: VFileData["headings"] = []; visit(root, "heading", (node: AstHeading) => { const heading = { diff --git a/packages/markdown/src/remarkCollectSearchDocuments.ts b/packages/markdown/src/remarkCollectSearchDocuments.ts new file mode 100644 index 00000000..465266c0 --- /dev/null +++ b/packages/markdown/src/remarkCollectSearchDocuments.ts @@ -0,0 +1,48 @@ +// Register directive nodes in mdast: +/// +// +import { HyperbookContext } from "@hyperbook/types"; +import { Root } from "mdast"; +import { visit } from "unist-util-visit"; +import { findAfter } from "unist-util-find-after"; +import { Node } from "unified/lib"; +import { VFile, VFileData } from "vfile"; +import { getAnchor } from "./remarkCollectHeadings"; +import { toText } from "./mdastUtilToText"; + +export default (ctx: HyperbookContext) => () => { + return (tree: Root, file: VFile) => { + const searchDocuments: VFileData["searchDocuments"] = []; + + visit(tree, function (node, index, parent) { + if (node.type === "heading") { + const start = node; + const startIndex = index; + const depth = start.depth; + + const isEnd = (node: Node) => + (node.type === "heading" && node.depth <= depth) || + node.type === "export"; + const end = findAfter(parent, start, isEnd); + const endIndex = parent.children.indexOf(end); + + const between = parent.children.slice( + startIndex, + endIndex > 0 ? endIndex : undefined, + ); + + const anchor = getAnchor(node); + const content = toText(between); + searchDocuments.push({ + href: `${ctx.navigation.current?.href || ""}#${anchor}`, + heading: toText(node) || ctx.navigation.current?.name || "", + content, + keywords: ctx.navigation.current?.keywords || [], + description: ctx.navigation.current?.description || "", + }); + } + }); + + file.data.searchDocuments = searchDocuments; + }; +}; diff --git a/packages/markdown/tests/__snapshots__/process.test.ts.snap b/packages/markdown/tests/__snapshots__/process.test.ts.snap index b78c3ca2..30e9f7a5 100644 --- a/packages/markdown/tests/__snapshots__/process.test.ts.snap +++ b/packages/markdown/tests/__snapshots__/process.test.ts.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`process > should add showLineNumbers 1`] = ` -"Markdown Referenz - Hyperbook Dokumenation -
✎ GitHub© Copyright 2024 by OpenPatch
" +
✎ GitHub© Copyright 2024 by OpenPatch
" `; exports[`process > should result in two link 1`] = ` @@ -175,7 +175,7 @@ body { padding: 0; } -
My Hyperbook
@@ -195,7 +195,7 @@ HYPERBOOK_ASSETS = "/assets/" in Python Teil des Programms .

-
© Copyright 2024
" +
© Copyright 2024
" `; exports[`process > should transform 1`] = ` @@ -270,7 +270,7 @@ body { padding: 0; } -
My Hyperbook
@@ -309,7 +309,7 @@ HYPERBOOK_ASSETS = "/assets/"
-
© Copyright 2024
" +
© Copyright 2024
" `; exports[`process > should transfrom complex context 1`] = ` @@ -384,7 +384,7 @@ body { padding: 0; } - -
✎ GitHub© Copyright 2024 by OpenPatch
" +
✎ GitHub© Copyright 2024 by OpenPatch
" `; From eb4980c90946c812f3eda777307023d2eeaf9ef4 Mon Sep 17 00:00:00 2001 From: Mike Barkmin Date: Fri, 15 Nov 2024 21:14:45 +0100 Subject: [PATCH 6/7] add other language support to search --- packages/hyperbook/build.ts | 34 ++++++++- packages/hyperbook/package.json | 1 + packages/markdown/package.json | 1 + packages/markdown/postbuild.mjs | 4 ++ packages/markdown/src/rehypeHtmlStructure.ts | 76 ++++++++++++++------ pnpm-lock.yaml | 11 +++ 6 files changed, 104 insertions(+), 23 deletions(-) diff --git a/packages/hyperbook/build.ts b/packages/hyperbook/build.ts index 8ce31a1e..ec28a92f 100644 --- a/packages/hyperbook/build.ts +++ b/packages/hyperbook/build.ts @@ -297,7 +297,21 @@ async function runBuild( const assetPath = path.join(assetsPath, asset); const assetOut = path.join(assetsOut, asset); if (!asset.startsWith("directive-")) { - await cp(assetPath, assetOut, { recursive: true }); + await cp(assetPath, assetOut, { + recursive: true, + filter: (src) => { + if (src.includes("lunr-languages")) { + return ( + hyperbookJson.language !== undefined && + hyperbookJson.language !== "en" && + (src.endsWith("lunr-languages") || + src.endsWith(`lunr.${hyperbookJson.language}.min.js`) || + src.endsWith(`lunr.stemmer.support.min.js`)) + ); + } + return true; + }, + }); } if (!process.env.CI) { readline.clearLine(process.stdout, 0); @@ -314,7 +328,25 @@ async function runBuild( if (hyperbookJson.search) { const documents: Record = {}; + console.log(`${chalk.blue(`[${prefix}]`)} Building search index`); + + let foundLanguage = false; + if (hyperbookJson.language && hyperbookJson.language !== "en") { + try { + require("lunr-languages/lunr.stemmer.support.js")(lunr); + require(`lunr-languages/lunr.${hyperbookJson.language}.js`)(lunr); + foundLanguage = true; + } catch (e) { + console.log( + `${chalk.yellow(`[${prefix}]`)} ${hyperbookJson.language} is no valid value for the lanuage key. See https://github.com/MihaiValentin/lunr-languages for possible values. Falling back to English.`, + ); + } + } const idx = lunr(function () { + if (foundLanguage) { + // @ts-ignore + this.use(lunr[hyperbookJson.language]); + } this.ref("href"); this.field("description"); this.field("keywords"); diff --git a/packages/hyperbook/package.json b/packages/hyperbook/package.json index fe8450db..cc6dc692 100644 --- a/packages/hyperbook/package.json +++ b/packages/hyperbook/package.json @@ -52,6 +52,7 @@ "got": "12.6.0", "htmlparser2": "^9.1.0", "lunr": "^2.3.9", + "lunr-languages": "^1.14.0", "mime": "^4.0.4", "prompts": "2.4.2", "rimraf": "5.0.7", diff --git a/packages/markdown/package.json b/packages/markdown/package.json index c71d938a..a7bf7f13 100644 --- a/packages/markdown/package.json +++ b/packages/markdown/package.json @@ -83,6 +83,7 @@ "chokidar": "3.6.0", "live-server": "^1.2.2", "lunr": "^2.3.9", + "lunr-languages": "^1.14.0", "mermaid": "11.3.0", "ncp": "^2.0.0", "scratchblocks": "^3.6.4", diff --git a/packages/markdown/postbuild.mjs b/packages/markdown/postbuild.mjs index b8e7e83c..80ffb738 100644 --- a/packages/markdown/postbuild.mjs +++ b/packages/markdown/postbuild.mjs @@ -83,6 +83,10 @@ async function postbuild() { src: path.join("./node_modules", "lunr", "lunr.min.js"), dst: path.join("./dist", "assets", "lunr.min.js"), }, + { + src: path.join("./node_modules", "lunr-languages", "min"), + dst: path.join("./dist", "assets", "lunr-languages"), + }, ]; for (let asset of assets) { diff --git a/packages/markdown/src/rehypeHtmlStructure.ts b/packages/markdown/src/rehypeHtmlStructure.ts index bbbebd4f..ce50b398 100644 --- a/packages/markdown/src/rehypeHtmlStructure.ts +++ b/packages/markdown/src/rehypeHtmlStructure.ts @@ -14,6 +14,59 @@ function parseFont(font: string): [string, string] { return [parts[0], "100%"]; } +const makeSearchScripts = (ctx: HyperbookContext): ElementContent[] => { + const elements: ElementContent[] = []; + if (ctx.config.search) { + elements.push({ + type: "element", + tagName: "script", + properties: { + src: ctx.makeUrl(["lunr.min.js"], "assets"), + defer: true, + }, + children: [], + }); + + if (ctx.config.language && ctx.config.language !== "en") { + elements.push({ + type: "element", + tagName: "script", + properties: { + src: ctx.makeUrl( + ["lunr-languages", "lunr.stemmer.support.min.js"], + "assets", + ), + defer: true, + }, + children: [], + }); + elements.push({ + type: "element", + tagName: "script", + properties: { + src: ctx.makeUrl( + ["lunr-languages", `lunr.${ctx.config.language}.min.js`], + "assets", + ), + defer: true, + }, + children: [], + }); + } + elements.push({ + type: "element", + tagName: "script", + properties: { + src: ctx.makeUrl(["search.js"], "assets"), + defer: true, + }, + children: [], + }); + } + + return elements; +}; + const makeRootCssElement = ({ makeUrl, config: { colors, font, fonts }, @@ -344,28 +397,7 @@ HYPERBOOK_ASSETS = "${makeUrl("/", "assets")}" }, children: [], }, - ...(ctx.config.search - ? [ - { - type: "element", - tagName: "script", - properties: { - src: makeUrl(["lunr.min.js"], "assets"), - defer: true, - }, - children: [], - } as ElementContent, - { - type: "element", - tagName: "script", - properties: { - src: makeUrl(["search.js"], "assets"), - defer: true, - }, - children: [], - } as ElementContent, - ] - : []), + ...makeSearchScripts(ctx), { type: "element", tagName: "script", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f86f1ff4..4868867b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,6 +190,9 @@ importers: lunr: specifier: ^2.3.9 version: 2.3.9 + lunr-languages: + specifier: ^1.14.0 + version: 1.14.0 mime: specifier: ^4.0.4 version: 4.0.4 @@ -341,6 +344,9 @@ importers: lunr: specifier: ^2.3.9 version: 2.3.9 + lunr-languages: + specifier: ^1.14.0 + version: 1.14.0 mermaid: specifier: 11.3.0 version: 11.3.0 @@ -4290,6 +4296,9 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lunr-languages@1.14.0: + resolution: {integrity: sha512-hWUAb2KqM3L7J5bcrngszzISY4BxrXn/Xhbb9TTCJYEGqlR1nG67/M14sp09+PTIRklobrn57IAxcdcO/ZFyNA==} + lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} @@ -11115,6 +11124,8 @@ snapshots: dependencies: yallist: 4.0.0 + lunr-languages@1.14.0: {} + lunr@2.3.9: {} magic-string@0.30.10: From eaeaf293532494607385f4e8d927ffb3716dcc6f Mon Sep 17 00:00:00 2001 From: Mike Barkmin Date: Fri, 15 Nov 2024 21:20:03 +0100 Subject: [PATCH 7/7] add changeset --- .changeset/beige-suits-arrive.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/beige-suits-arrive.md diff --git a/.changeset/beige-suits-arrive.md b/.changeset/beige-suits-arrive.md new file mode 100644 index 00000000..ab5ba56b --- /dev/null +++ b/.changeset/beige-suits-arrive.md @@ -0,0 +1,8 @@ +--- +"hyperbook": minor +"@hyperbook/markdown": minor +"hyperbook-studio": minor +"@hyperbook/types": minor +--- + +Add option to enable search. Just set the search key to true in your hyperbook config and a search icon will be visible in the top right hand corner.