Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

search #543

Merged
merged 118 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
48d9b8e
search index as a data loader, search page
Fil Jan 14, 2024
ea34bb4
copy the lib for now (node_modules/minisearch/dist/es/index.js)
Fil Jan 14, 2024
3343484
import npm:minisearch (#551)
mbostock Jan 16, 2024
ed5fc1a
search in the side bar (WIP)
Fil Jan 30, 2024
04a53ff
don't index theme demos…
Fil Jan 30, 2024
e3d4479
move css
Fil Jan 30, 2024
e4ce57a
fix links for index, subfolder
Fil Jan 31, 2024
dacd1d8
focus on cmd-K
Fil Feb 1, 2024
f13fc81
remove unused "category" field
Fil Feb 1, 2024
75e2a05
filter out some terms (like long strings with numbers, e.g. API keys)…
Fil Feb 1, 2024
1f35cd9
remove html tags
Fil Feb 1, 2024
7d49430
index title from front-matter
Fil Feb 1, 2024
b9d1e73
index.md is /
Fil Feb 1, 2024
d029f6a
front-matter title for the intro page
Fil Feb 1, 2024
4e26cc7
remove hard-coded input
Fil Feb 1, 2024
f22d073
handle ctrl-K
Fil Feb 1, 2024
91998b8
nicer UI
Fil Feb 1, 2024
494a0f6
- adds a search option to config
Fil Feb 1, 2024
89990a3
document search
Fil Feb 1, 2024
99de1be
on cmd-K, select the search input contents
Fil Feb 1, 2024
083af45
Merge branch 'main' into fil/minisearch
Fil Feb 2, 2024
1cfee23
use front-matter for title and index options; index h1 if no # was found
Fil Feb 2, 2024
5d2c846
this seems to work in all screen widths (?)
Fil Feb 2, 2024
170f911
cleaner css, optional rendering
Fil Feb 2, 2024
c7bcd40
DRY
Fil Feb 2, 2024
aa6eb4d
pass root path cleanly
Fil Feb 2, 2024
a072188
fix ctrl-k again
Fil Feb 2, 2024
8cd307b
session storage for the summary open toggle
Fil Feb 2, 2024
08eeedf
test
Fil Feb 2, 2024
cf722d0
prettier
Fil Feb 2, 2024
d90a4ef
Merge branch 'main' into fil/minisearch
Fil Feb 2, 2024
1d1f834
space
Fil Feb 2, 2024
d7baec2
load MiniSearch on demand, fix jumpiness.
Fil Feb 5, 2024
934ec66
Imports of node built-ins should use the node: protocol
Fil Feb 5, 2024
08fd026
Merge branch 'main' into fil/minisearch
Fil Feb 5, 2024
c2190b7
remove async cruft; hide menu when showing results
Fil Feb 5, 2024
48e6dbc
limit to 11 (10+) results
Fil Feb 5, 2024
7c3aaef
* special route for minisearch.json
Fil Feb 5, 2024
cf2d396
fix tests
Fil Feb 5, 2024
b267d2e
first-of-type (temporary change)
Fil Feb 5, 2024
d7a07cc
fix two bugs:
Fil Feb 5, 2024
e202b03
update documentation
Fil Feb 5, 2024
1096b5e
not experimental anymore
Fil Feb 5, 2024
775480c
fix test (I do want to see that indexed contents)
Fil Feb 5, 2024
3da3eb5
Merge branch 'main' into fil/minisearch
Fil Feb 7, 2024
c067b9b
use parseMardown
Fil Feb 7, 2024
88c4a57
position search input; add magnifier icon
Fil Feb 8, 2024
15964b1
Merge branch 'main' into fil/minisearch
Fil Feb 8, 2024
6a8326a
keyboard navigation
Fil Feb 8, 2024
e07936d
fix tests
Fil Feb 8, 2024
7c293bb
Merge branch 'main' into fil/minisearch
Fil Feb 8, 2024
2417bdf
prettier
Fil Feb 8, 2024
7d0d262
remove observablehq-search-focus from sessionStorage as soon as we've…
Fil Feb 8, 2024
eb7ffd3
fix dot colors when navigating on keyboard
Fil Feb 8, 2024
4c790eb
Escape on an empty field blurs the input; avoid indirection
Fil Feb 8, 2024
00b64f7
oops
Fil Feb 8, 2024
81cc96e
fix tests
Fil Feb 8, 2024
26c1f2d
alias slash
Fil Feb 8, 2024
b6ff319
fix tests again
Fil Feb 8, 2024
dab1092
dismantle session navigation
Fil Feb 8, 2024
fabf7cb
fix typo
Fil Feb 8, 2024
926b3af
fix tests
Fil Feb 8, 2024
c368962
Merge branch 'main' into fil/minisearch
Fil Feb 8, 2024
ffe417c
10+ results
Fil Feb 8, 2024
3169a97
fix dot position
Fil Feb 8, 2024
e945f05
apply style suggestions from review
Fil Feb 8, 2024
0b83f70
fix styles (review comments)
Fil Feb 9, 2024
3f8ce81
Merge branch 'main' into fil/minisearch
Fil Feb 9, 2024
d58a490
fix next/prev logic on the edge
Fil Feb 9, 2024
14b787c
fix blur
Fil Feb 9, 2024
c33447d
remove italics
Fil Feb 9, 2024
c84c908
fix sidebar opening on small screen
Fil Feb 9, 2024
4f169aa
Merge branch 'main' into fil/minisearch
Fil Feb 9, 2024
5e7c6bf
fix tests
Fil Feb 9, 2024
f8a1291
fix input style
Fil Feb 10, 2024
75c98d6
don't wrap long titles
Fil Feb 10, 2024
3e5db98
fix tests
Fil Feb 10, 2024
71cda06
fix a race condition where you'd try to navigate before the results a…
Fil Feb 10, 2024
5c90870
on hover, avoid a conflict between the cross (clear input) and the ct…
Fil Feb 10, 2024
238ea19
fix a race condition
Fil Feb 10, 2024
e8b83bc
Remove the confusing fuzzy vs. exact dot colors, and trust the releva…
Fil Feb 10, 2024
48a30f1
documentation
Fil Feb 10, 2024
19cdb2c
prettier
Fil Feb 10, 2024
b5b88c5
fixes for Safari
Fil Feb 10, 2024
373463d
fix tests
Fil Feb 10, 2024
bf52416
prettier
Fil Feb 10, 2024
6131091
12 bytes
Fil Feb 10, 2024
3e613ee
Merge branch 'main' into fil/minisearch
Fil Feb 12, 2024
e0bc602
Merge branch 'main' into fil/minisearch
Fil Feb 13, 2024
d8d37e3
Update src/minisearch.json.ts
Fil Feb 13, 2024
ea9893a
Update src/client/search.js
Fil Feb 13, 2024
6c27e5c
remove obsolete comment
Fil Feb 13, 2024
bbd561e
Update src/client/search.js
Fil Feb 13, 2024
075ef4e
use dynamic import
Fil Feb 13, 2024
41086ce
Merge branch 'main' into fil/minisearch
Fil Feb 13, 2024
7d5a704
use safe methods to inject href, title
Fil Feb 13, 2024
38c7e82
fix tests
Fil Feb 13, 2024
de90ed0
prettier
Fil Feb 14, 2024
a967cca
apply progress-bar design by @ramonaisonline
Fil Feb 14, 2024
7bd5943
Merge branch 'main' into fil/minisearch
Fil Feb 14, 2024
a363396
rollup the minisearch client into the search.js bundle
Fil Feb 14, 2024
5fd1eaf
Merge branch 'main' into fil/minisearch
Fil Feb 14, 2024
725ba2f
simpler
Fil Feb 14, 2024
2dbbc7e
Merge branch 'main' into fil/minisearch
mbostock Feb 14, 2024
c49ea69
conditional search.js deploy
mbostock Feb 14, 2024
f393224
delete undesired search.js
mbostock Feb 14, 2024
3b803d7
polish
mbostock Feb 14, 2024
497babf
more edits
mbostock Feb 14, 2024
3cb72a8
pretty-ing
mbostock Feb 14, 2024
9f13e36
more polish
mbostock Feb 14, 2024
cc94066
bundle minisearch
mbostock Feb 14, 2024
8121a6a
more polish
mbostock Feb 14, 2024
a10c578
scroll active result into view
mbostock Feb 14, 2024
baa191b
docs edits
mbostock Feb 14, 2024
673f541
rename to search.ts
mbostock Feb 14, 2024
e5318c6
fix import order
mbostock Feb 14, 2024
48452e7
copy edit
mbostock Feb 14, 2024
1c13d63
Merge branch 'main' into fil/minisearch
mbostock Feb 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,7 @@ The table of contents configuration can also be set in the page’s YAML front m
toc: false
---
```

## search

Whether to enable [search](./search) on the project; defaults to false.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
toc: false
index: false
---

<style>
Expand Down
33 changes: 33 additions & 0 deletions docs/search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
index: true
---

# Search

Framework provides built-in full-text page search using [MiniSearch](https://lucaong.github.io/minisearch/). Search results are queried on the client, with fuzzy and prefix matching, using a static index computed during build.

<div class="tip">Search is not enabled by default. It is intended for larger projects with lots of static text, such as reports and documentation. Search will not index dynamic content such as data or charts. To enable search, set the <a href="./config#search"><b>search</b> option</a> to true in your config.</div>

Search works in two stages: when Framework builds the site, it creates an index of the contents. On the client, as soon as the user focuses the search input and starts typing, the index is retrieved and the matching pages are displayed in the sidebar. The user can then click on a result, or use the up ↑ and down ↓ arrow keys to navigate, then type return to open the page.

Pages are indexed each time you build or deploy your project. When working in preview, they are reindexed every 10 minutes.

By default, all the pages found in the project root (`docs` by default) or defined in the [**pages** config option](./config#pages) are indexed; you can however opt-out a page from the index by specifying an index: false property in its front matter:

```yaml
---
title: This page won’t be indexed
index: false
---
```

Likewise, a page that is not referenced in **pages** can opt-in by having index: true in its front matter:

```yaml
---
title: A page that is not in the sidebar, but gets indexed
index: true
---
```

Search is case-insensitive. The indexing script tries to avoid common pitfalls by ignoring HTML tags and non-word characters such as punctuation. It also ignores long words, as well as sequences that contain more than 6 digits (such as API keys, for example).
3 changes: 2 additions & 1 deletion observablehq.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,6 @@ export default {
</div>
</div>`,
footer: `© ${new Date().getUTCFullYear()} Observable, Inc.`,
style: "style.css"
style: "style.css",
search: true
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"markdown-it": "^13.0.2",
"markdown-it-anchor": "^8.6.7",
"mime": "^3.0.0",
"minisearch": "^6.3.0",
"open": "^9.1.0",
"rollup": "^4.6.0",
"rollup-plugin-esbuild": "^6.1.0",
Expand Down
43 changes: 24 additions & 19 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {createImportResolver, rewriteModule} from "./javascript/imports.js";
import type {Logger, Writer} from "./logger.js";
import {renderServerless} from "./render.js";
import {bundleStyles, rollupClient} from "./rollup.js";
import {searchIndex} from "./search.js";
import {Telemetry} from "./telemetry.js";
import {faint} from "./tty.js";
import {resolvePath} from "./url.js";
Expand All @@ -22,24 +23,6 @@ const EXTRA_FILES = new Map([
]
]);

// TODO Remove library helpers (e.g., duckdb) when they are published to npm.
function clientBundles(clientPath: string): [entry: string, name: string][] {
return [
[clientPath, "client.js"],
["./src/client/stdlib.js", "stdlib.js"],
["./src/client/stdlib/dot.js", "stdlib/dot.js"],
["./src/client/stdlib/duckdb.js", "stdlib/duckdb.js"],
["./src/client/stdlib/inputs.css", "stdlib/inputs.css"],
["./src/client/stdlib/inputs.js", "stdlib/inputs.js"],
["./src/client/stdlib/mermaid.js", "stdlib/mermaid.js"],
["./src/client/stdlib/sqlite.js", "stdlib/sqlite.js"],
["./src/client/stdlib/tex.js", "stdlib/tex.js"],
["./src/client/stdlib/vega-lite.js", "stdlib/vega-lite.js"],
["./src/client/stdlib/xlsx.js", "stdlib/xlsx.js"],
["./src/client/stdlib/zip.js", "stdlib/zip.js"]
];
}

export interface BuildOptions {
config: Config;
clientEntry?: string;
Expand Down Expand Up @@ -106,7 +89,23 @@ export async function build(

// Generate the client bundles.
if (addPublic) {
for (const [entry, name] of clientBundles(clientEntry)) {
for (const [entry, name] of [
[clientEntry, "client.js"],
["./src/client/stdlib.js", "stdlib.js"],
// TODO Prune this list based on which libraries are actually used.
// TODO Remove library helpers (e.g., duckdb) when they are published to npm.
["./src/client/stdlib/dot.js", "stdlib/dot.js"],
["./src/client/stdlib/duckdb.js", "stdlib/duckdb.js"],
["./src/client/stdlib/inputs.css", "stdlib/inputs.css"],
["./src/client/stdlib/inputs.js", "stdlib/inputs.js"],
["./src/client/stdlib/mermaid.js", "stdlib/mermaid.js"],
["./src/client/stdlib/sqlite.js", "stdlib/sqlite.js"],
["./src/client/stdlib/tex.js", "stdlib/tex.js"],
["./src/client/stdlib/vega-lite.js", "stdlib/vega-lite.js"],
["./src/client/stdlib/xlsx.js", "stdlib/xlsx.js"],
["./src/client/stdlib/zip.js", "stdlib/zip.js"],
...(config.search ? [["./src/client/search.js", "search.js"]] : [])
]) {
const clientPath = getClientPath(entry);
const outputPath = join("_observablehq", name);
effects.output.write(`${faint("bundle")} ${clientPath} ${faint("→")} `);
Expand All @@ -115,6 +114,12 @@ export async function build(
: rollupClient(clientPath, {minify: true}));
await effects.writeFile(outputPath, code);
}
if (config.search) {
const outputPath = join("_observablehq", "minisearch.json");
const code = await searchIndex(config, effects);
effects.output.write(`${faint("search")} ${faint("→")} `);
await effects.writeFile(outputPath, code);
}
for (const style of styles) {
if ("path" in style) {
const outputPath = join("_import", style.path);
Expand Down
32 changes: 32 additions & 0 deletions src/client/search-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const container = document.querySelector("#observablehq-search")!;

// Set the short dynamically based on the client’s platform.
container.setAttribute("data-shortcut", `${/Mac|iPhone/.test(navigator.platform) ? "⌘" : "Alt-"}K`);

// Load search.js on demand
const input = container.querySelector<HTMLInputElement>("input")!;
const base = container.getAttribute("data-root");
const load = () => import(`${base}_observablehq/search.js`);
input.addEventListener("focus", load, {once: true});
input.addEventListener("keydown", load, {once: true});

// Focus on meta-K and /
const toggle = document.querySelector("#observablehq-sidebar-toggle")!;
addEventListener("keydown", (event) => {
if (
(event.code === "KeyK" && event.metaKey && !event.altKey && !event.ctrlKey) ||
(event.key === "/" && !event.metaKey && !event.altKey && !event.ctrlKey && event.target === document.body)
) {
// Force the sidebar to be temporarily open while the search input is
// focused. (We can’t use :focus-within because the sidebar isn’t focusable
// while it is invisible, and we don’t want to keep the sidebar open
// persistently after you blur the search input.)
toggle.classList.add("observablehq-sidebar-open");
input.focus();
input.select();
event.preventDefault();
}
});

// Allow the sidebar to close when the search input is blurred.
input.addEventListener("blur", () => toggle.classList.remove("observablehq-sidebar-open"));
78 changes: 78 additions & 0 deletions src/client/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import MiniSearch from "minisearch";

const container = document.querySelector("#observablehq-search");
const sidebar = document.querySelector("#observablehq-sidebar");
const shortcut = container.getAttribute("data-shortcut");
const input = container.querySelector("input");
const resultsContainer = document.querySelector("#observablehq-search-results");
const activeClass = "observablehq-link-active";
let currentValue;

const index = await fetch(import.meta.resolve("./minisearch.json"))
.then((response) => {
if (!response.ok) throw new Error(`unable to load minisearch.json: ${response.status}`);
return response.json();
})
.then((json) =>
MiniSearch.loadJS(json, {
...json.options,
processTerm: (term) => term.slice(0, 15).toLowerCase() // see src/minisearch.json.ts
})
);

input.addEventListener("input", () => {
if (currentValue === input.value) return;
currentValue = input.value;
if (!currentValue.length) {
container.setAttribute("data-shortcut", shortcut);
sidebar.classList.remove("observablehq-search-results");
resultsContainer.innerHTML = "";
return;
}
container.setAttribute("data-shortcut", ""); // prevent conflict with close button
sidebar.classList.add("observablehq-search-results"); // hide pages while showing search results
const results = index.search(currentValue, {boost: {title: 4}, fuzzy: 0.15, prefix: true});
resultsContainer.innerHTML =
results.length === 0
? "<div>no results</div>"
: `<div>${results.length.toLocaleString("en-US")} result${results.length === 1 ? "" : "s"}</div><ol>${results
.map(renderResult)
.join("")}</ol>`;
});

function renderResult({id, score, title}, i) {
return `<li data-score="${Math.min(5, Math.round(0.6 * score))}" class="observablehq-link${
i === 0 ? ` ${activeClass}` : ""
}"><a href="${escapeDoubleQuote(import.meta.resolve(`../${id}`))}">${escapeText(title)}</a></li>`;
}

function escapeDoubleQuote(text) {
return text.replace(/["&]/g, entity);
}

function escapeText(text) {
return text.replace(/[<&]/g, entity);
}

function entity(character) {
return `&#${character.charCodeAt(0).toString()};`;
}

// Handle a race condition where an input event fires while awaiting the index fetch.
input.dispatchEvent(new Event("input"));
Fil marked this conversation as resolved.
Show resolved Hide resolved

input.addEventListener("keydown", (event) => {
const {code} = event;
if (code === "Escape" && input.value === "") return input.blur();
if (code === "ArrowDown" || code === "ArrowUp" || code === "Enter") {
const results = resultsContainer.querySelector("ol");
if (!results) return;
let activeResult = results.querySelector(`.${activeClass}`);
if (code === "Enter") return activeResult.querySelector("a").click();
activeResult.classList.remove(activeClass);
if (code === "ArrowUp") activeResult = activeResult.previousElementSibling ?? results.lastElementChild;
else activeResult = activeResult.nextElementSibling ?? results.firstElementChild;
activeResult.classList.add(activeClass);
activeResult.scrollIntoView({block: "nearest"});
}
});
5 changes: 4 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface Config {
toc: TableOfContents;
style: null | Style; // defaults to {theme: ["light", "dark"]}
deploy: null | {workspace: string; project: string};
search: boolean; // default to false
}

export async function readConfig(configPath?: string, root?: string): Promise<Config> {
Expand Down Expand Up @@ -93,6 +94,7 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro
sidebar,
style,
theme = "default",
search,
deploy,
scripts = [],
head = "",
Expand All @@ -118,7 +120,8 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro
footer = String(footer);
toc = normalizeToc(toc);
deploy = deploy ? {workspace: String(deploy.workspace).replace(/^@+/, ""), project: String(deploy.project)} : null;
return {root, output, base, title, sidebar, pages, pager, scripts, head, header, footer, toc, style, deploy};
search = Boolean(search);
return {root, output, base, title, sidebar, pages, pager, scripts, head, header, footer, toc, style, deploy, search};
}

function normalizeBase(base: any): string {
Expand Down
5 changes: 5 additions & 0 deletions src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {diffMarkdown, parseMarkdown} from "./markdown.js";
import type {ParseResult} from "./markdown.js";
import {renderPreview, resolveStylesheet} from "./render.js";
import {bundleStyles, rollupClient} from "./rollup.js";
import {searchIndex} from "./search.js";
import {Telemetry} from "./telemetry.js";
import {bold, faint, green, link, red} from "./tty.js";
import {relativeUrl} from "./url.js";
Expand Down Expand Up @@ -108,6 +109,10 @@ export class PreviewServer {
}
} else if (pathname === "/_observablehq/client.js") {
end(req, res, await rollupClient(getClientPath("./src/client/preview.js")), "text/javascript");
} else if (pathname === "/_observablehq/search.js") {
end(req, res, await rollupClient(getClientPath("./src/client/search.js")), "text/javascript");
} else if (pathname === "/_observablehq/minisearch.json") {
end(req, res, await searchIndex(config), "application/json");
} else if ((match = /^\/_observablehq\/theme-(?<theme>[\w-]+(,[\w-]+)*)?\.css$/.exec(pathname))) {
end(req, res, await bundleStyles({theme: match.groups!.theme?.split(",") ?? []}), "text/css");
} else if (pathname.startsWith("/_observablehq/")) {
Expand Down
19 changes: 15 additions & 4 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type RenderInternalOptions =
| {preview: true}; // preview

async function render(parseResult: ParseResult, options: RenderOptions & RenderInternalOptions): Promise<string> {
const {root, base, path, pages, title, preview} = options;
const {root, base, path, pages, title, preview, search} = options;
const sidebar = parseResult.data?.sidebar !== undefined ? Boolean(parseResult.data.sidebar) : options.sidebar;
const toc = mergeToc(parseResult.data?.toc, options.toc);
return String(html`<!DOCTYPE html>
Expand Down Expand Up @@ -90,7 +90,7 @@ import ${preview || parseResult.cells.length > 0 ? `{${preview ? "open, " : ""}d
${
preview ? `\nopen({hash: ${JSON.stringify(parseResult.hash)}, eval: (body) => (0, eval)(body)});\n` : ""
}${parseResult.cells.map((cell) => `\n${renderDefineCell(cell)}`).join("")}`)}
</script>${sidebar ? html`\n${await renderSidebar(title, pages, path)}` : ""}${
</script>${sidebar ? html`\n${await renderSidebar(title, pages, path, search)}` : ""}${
toc.show ? html`\n${renderToc(findHeaders(parseResult), toc.label)}` : ""
}
<div id="observablehq-center">${renderHeader(options, parseResult.data)}
Expand All @@ -100,7 +100,7 @@ ${html.unsafe(parseResult.html)}</main>${renderFooter(path, options, parseResult
`);
}

async function renderSidebar(title = "Home", pages: (Page | Section)[], path: string): Promise<Html> {
async function renderSidebar(title = "Home", pages: (Page | Section)[], path: string, search: boolean): Promise<Html> {
return html`<input id="observablehq-sidebar-toggle" type="checkbox" title="Toggle sidebar">
<label id="observablehq-sidebar-backdrop" for="observablehq-sidebar-toggle"></label>
<nav id="observablehq-sidebar">
Expand All @@ -109,7 +109,18 @@ async function renderSidebar(title = "Home", pages: (Page | Section)[], path: st
<li class="observablehq-link${
normalizePath(path) === "/index" ? " observablehq-link-active" : ""
}"><a href="${relativeUrl(path, "/")}">${title}</a></li>
</ol>
</ol>${
search
? html`\n <div id="observablehq-search" data-root="${relativeUrl(
path,
"/"
)}"><input type="search" placeholder="Search"></div>
<div id="observablehq-search-results"></div>
<script>{${html.unsafe(
(await rollupClient(getClientPath("./src/client/search-init.ts"), {minify: true})).trim()
)}}</script>`
: ""
}
<ol>${pages.map((p, i) =>
"pages" in p
? html`${i > 0 && "path" in pages[i - 1] ? html`</ol>` : ""}
Expand Down
7 changes: 5 additions & 2 deletions src/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const STYLE_MODULES = {
...Object.fromEntries(THEMES.map(({name, path}) => [`observablehq:theme-${name}.css`, path]))
};

// These libraries are currently bundled in to a wrapper.
const BUNDLED_MODULES = ["@observablehq/inputs", "minisearch"];

function rewriteInputsNamespace(code: string) {
return code.replace(/\b__ns__\b/g, "inputs-3a86ea");
}
Expand All @@ -38,7 +41,7 @@ export async function rollupClient(clientPath: string, {minify = false} = {}): P
input: clientPath,
external: [/^https:/],
plugins: [
nodeResolve({resolveOnly: ["@observablehq/inputs"]}),
nodeResolve({resolveOnly: BUNDLED_MODULES}),
importResolve(clientPath),
esbuild({
target: "es2022",
Expand Down Expand Up @@ -109,7 +112,7 @@ async function resolveImport(source: string, specifier: string | AstNode): Promi
? {id: relativeUrl(source, getClientPath("./src/client/stdlib/zip.js")), external: true} // TODO publish to npm
: specifier.startsWith("npm:")
? {id: await resolveNpmImport(specifier.slice("npm:".length))}
: source !== specifier && !isPathImport(specifier) && specifier !== "@observablehq/inputs"
: source !== specifier && !isPathImport(specifier) && !BUNDLED_MODULES.includes(specifier)
? {id: await resolveNpmImport(specifier), external: true}
: null;
}
Expand Down
Loading