@waveso/docs 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/README.md +111 -22
- package/dist/docs-error.d.ts +74 -0
- package/dist/docs-error.js +40 -0
- package/dist/frontmatter.d.ts +39 -7
- package/dist/frontmatter.js +51 -24
- package/dist/highlighter.d.ts +2 -2
- package/dist/highlighter.js +3 -2
- package/dist/map-pooled.d.ts +26 -0
- package/dist/map-pooled.js +45 -0
- package/dist/meta.d.ts +7 -3
- package/dist/meta.js +61 -15
- package/dist/next.d.ts +41 -19
- package/dist/next.js +117 -21
- package/dist/plugins/rehype-capture-toc.js +26 -15
- package/dist/plugins/rehype-code-language.d.ts +24 -0
- package/dist/plugins/rehype-code-language.js +48 -0
- package/dist/plugins/rehype-fallback-heading-ids.d.ts +6 -0
- package/dist/plugins/rehype-fallback-heading-ids.js +51 -0
- package/dist/plugins/rehype-flatten-roots.d.ts +7 -0
- package/dist/plugins/rehype-flatten-roots.js +39 -0
- package/dist/plugins/remark-doc-links.d.ts +12 -1
- package/dist/plugins/remark-doc-links.js +147 -20
- package/dist/react/markdown-components.js +71 -6
- package/dist/react/search-dialog.d.ts +23 -7
- package/dist/react/search-dialog.js +46 -29
- package/dist/react/toc.js +28 -5
- package/dist/react/youtube.js +6 -4
- package/dist/render.d.ts +43 -9
- package/dist/render.js +112 -50
- package/dist/search-index.d.ts +32 -14
- package/dist/search-index.js +45 -51
- package/dist/search-options.d.ts +32 -1
- package/dist/search-options.js +66 -3
- package/dist/section-boundary.d.ts +17 -0
- package/dist/section-boundary.js +43 -0
- package/dist/source.d.ts +13 -1
- package/dist/source.js +152 -56
- package/dist/styles.css +236 -90
- package/dist/types.d.ts +41 -27
- package/package.json +13 -12
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @waveso/docs
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7fefb08: Fix 38 defects found in a pre-publish review. Several are behaviour changes; the ones you can notice are listed first.
|
|
8
|
+
|
|
9
|
+
**Dark mode is now opt-in.** The tokens used to switch on `prefers-color-scheme` alone, but the stylesheet styles the docs subtree rather than the page — so a light-only site with a `/docs` section served near-white text on the host's white background (1.23:1) to every visitor whose OS was in dark mode. Dark now requires `data-theme="dark"` or `class="dark"` on `<html>`; `data-theme="system"` opts back into following the OS. `color-scheme` is declared, so native scrollbars and form controls match.
|
|
10
|
+
|
|
11
|
+
**The `tailwindcss` peer dependency is gone.** Nothing in the package used Tailwind, and declaring it blocked `npm install` outright for any project on Tailwind 3 — npm range-checks an optional peer that happens to be installed. The `@source "./"` directive is gone with it; it was injecting 14 unrequested utilities into every Tailwind consumer's CSS.
|
|
12
|
+
|
|
13
|
+
**`zod` is a dependency now, not a peer**, for the same reason and a larger one: it is imported at module scope, and as a peer its `^4.4.3` range refused to install beside roughly 69% of the Zod in the ecosystem — 47% still on 3.x, plus every 4.x below 4.4.3. Your project's Zod is now irrelevant. `z` is re-exported from `@waveso/docs/frontmatter`, so extending `docFrontmatterSchema` needs no install and cannot pick up a second copy.
|
|
14
|
+
|
|
15
|
+
**Search indexes the full text of a section.** It previously truncated to 300 characters _before_ indexing, dropping ~80% of a normal corpus, and stored none of it for display either. Tokenisation now uses `Intl.Segmenter`, so CJK text is searchable at all — it previously returned zero hits. Both `buildSearchIndex` and `SearchDialog` accept MiniSearch overrides.
|
|
16
|
+
|
|
17
|
+
**Aliases are validated.** `aliases: ['v1:beta']` used to compile to a Next redirect _wildcard_: the build passed, then `/docs/v1-guide` — a real prerendered page — was permanently 308'd away. Metacharacters, relative segments and empty entries are now rejected when the page is read, naming the file. Linking to an alias also fails the build now, naming the page to link instead: an alias is never prerendered, so it was a green build and a hard 404.
|
|
18
|
+
|
|
19
|
+
**Relative images fail the build instead of shipping a broken `src`.** Nothing ever rewrote them, so the browser resolved them against the route and identical markdown requested a different file from every page. Absolute and external sources are unaffected.
|
|
20
|
+
|
|
21
|
+
**A custom `frontmatterSchema` can no longer drop the package's own fields.** All six — `title`, `description`, `label`, `draft`, `aliases`, `order` — are parsed from the raw YAML and laid back over your schema's output, so a custom schema can only ever _add_. A bare `z.object({ title, … })` type-checks but used to strip `draft` and `aliases`, publishing every draft, submitting them to Google, and returning no redirects. The price is that a `.default()`, `.transform()` or `.coerce` aimed at one of the six is not honoured: the YAML wins.
|
|
22
|
+
|
|
23
|
+
**Links to unusual URL schemes are dropped rather than rendered.** `javascript:`, `data:` and `vbscript:` never reach an `href` — including the obfuscated spellings a browser still navigates. The allowlist is GitHub's (`http`, `https`, `mailto`, `tel`, `sms`, `ftp`, `ftps`, `irc`, `ircs`, `xmpp`, `news`, `nntp`, `feed`, `git`, `matrix`); anything else keeps its text, loses its destination, and warns outside production.
|
|
24
|
+
|
|
25
|
+
**Absolute internal links are respelled before they are checked.** `/docs/café` and `/docs/caf%C3%A9` are the same page, and only the encoded form used to match — so the human-readable spelling every editor produces failed the build with "no such page exists" for a page that plainly exists.
|
|
26
|
+
|
|
27
|
+
Also fixed: `siteUrl` with a path silently truncated out of every canonical and the whole sitemap; ` ```JSON `/` ```Bash ` shipping unhighlighted; the search dialog's focus trap breaking on a click, its Close button navigating on Enter, and IME composition being consumed as "open result"; the search index cached forever against the first `indexUrl`; every focus indicator vanishing under Windows High Contrast; long tokens forcing horizontal scroll at 320px; a `draft: true` index page publishing its title as a public sidebar heading; symlinked and `.MD` files vanishing silently; percent-encoded and NFD filenames failing to resolve; `writeSearchIndex` truncating the served file in place; the TOC scrollspy dying permanently when headings mount late; YouTube re-stealing focus on every re-render; markdown images ignoring an author's `loading`; Shiki splicing an invalid `root` node into the tree; `renderAll` running unbounded; and `docs.source.nav()` being one request stale in dev while every page view scanned the filesystem twice over.
|
|
28
|
+
|
|
29
|
+
Every failure now throws with a `code` (`'broken-link'`, `'invalid-alias'`, `'invalid-frontmatter'`, …) and a message naming this package, so a host can branch on the kind of failure instead of matching message text. Where an underlying parser failed — js-yaml on frontmatter, `JSON.parse` on `meta.json` — its own error is attached as `cause`.
|
|
30
|
+
|
|
31
|
+
### API changes
|
|
32
|
+
|
|
33
|
+
- `extractSearchRecords(doc)` takes no options; `ExtractSearchRecordsOptions` and `excerptLength` are gone, since full section text is now indexed.
|
|
34
|
+
- `SearchRecord.id` is a slug (`page#anchor`), not an `href` — an href embedded `basePath`, so moving a site from `/docs` to `/reference` changed the identity of every record.
|
|
35
|
+
- `buildSearchIndex(records, options?)` and `<SearchDialog searchOptions={…}>` accept MiniSearch overrides. They must agree.
|
|
36
|
+
- `DocsSource.drafts()` is new, and `DocsRouteOptions` gains `excludeLangs`, which existed on the renderer and was reachable from nothing.
|
|
37
|
+
- Optional properties across the public option types are now spelled `?: T | undefined`, so a consumer with `exactOptionalPropertyTypes` can pass a possibly-undefined value — `siteUrl: process.env.SITE_URL` was previously a compile error.
|
|
38
|
+
- The CSS custom property `--wave-docs-header-height` is now `--wave-docs-scroll-padding`, which is what it actually controls.
|
|
39
|
+
- `engines.node` is `>=22.12.0`. Node 20 reached end of life in April 2026.
|
|
40
|
+
|
|
41
|
+
## 0.1.0
|
|
42
|
+
|
|
43
|
+
### Minor Changes
|
|
44
|
+
|
|
45
|
+
- adddaea: Initial release.
|
|
46
|
+
|
|
47
|
+
Markdown documentation for Next.js, from one content directory and one pipeline.
|
|
48
|
+
Markdown becomes hast in Node at build time, so the browser receives a tree of
|
|
49
|
+
nodes and a component map — never `unified`, `remark-parse` or Shiki.
|
|
50
|
+
|
|
51
|
+
- `createDocsRoute` wires a content directory to an App Router catch-all, with
|
|
52
|
+
`dynamicParams: false`, a real index route, awaited `params` and a canonical
|
|
53
|
+
URL on every page.
|
|
54
|
+
- Frontmatter is extensible through `frontmatterSchema`, typed as a
|
|
55
|
+
[Standard Schema](https://standardschema.dev) so Zod, Valibot and ArkType all
|
|
56
|
+
work and your fields are inferred with no type argument.
|
|
57
|
+
- Internal `.md` links are rewritten to routes and their targets checked, so a
|
|
58
|
+
link that works on GitHub cannot 404 once published.
|
|
59
|
+
- Table-of-contents ids come from the same `rehype-slug` pass that annotates the
|
|
60
|
+
document, so anchors match by construction rather than by a second parse.
|
|
61
|
+
- GitHub alert syntax, a click-to-load YouTube facade, section-scoped MiniSearch
|
|
62
|
+
records, a sidebar, a scrollspy TOC, a search dialog and a themeable
|
|
63
|
+
stylesheet.
|
|
64
|
+
|
|
65
|
+
- ed73890: Retheming now works from a plain `:root`, and config files highlight.
|
|
66
|
+
|
|
67
|
+
The stylesheet's own guidance — "redefine the tokens in your own `:root`" — could
|
|
68
|
+
not work against it. The dark tokens are `:root:not([data-theme='light'])`, which
|
|
69
|
+
is specificity (0,2,0), so an unlayered `:root` at (0,1,0) lost regardless of load
|
|
70
|
+
order; the cascade never reached source order. Overriding meant `:root:root:root`.
|
|
71
|
+
|
|
72
|
+
Every block now lives in a layer — `theme` for tokens, `base` for resets,
|
|
73
|
+
`components` for classes, declared in that order — and unlayered CSS outranks
|
|
74
|
+
every layer whatever its specificity. Inside the layer the dark blocks still beat
|
|
75
|
+
the light one, so OS following and `data-theme` are unchanged. The README gains a
|
|
76
|
+
Theming section, which it did not have.
|
|
77
|
+
|
|
78
|
+
Added the `ini` and `toml` grammars, and registered `cfg` and `conf` as aliases of
|
|
79
|
+
`ini`. Shiki resolves a fence against a grammar's own aliases rather than against
|
|
80
|
+
this package's loader keys, and `ini` ships only `properties` — so a `` cfg block
|
|
81
|
+
threw `Language 'cfg' not found` and `fallbackLanguage` rendered it as plain text.
|
|
82
|
+
The fence an author writes follows the filename: nobody types ``ini above a file
|
|
83
|
+
called `server.cfg`, and on a FiveM docs site that block is the most-read code on
|
|
84
|
+
the page.
|
package/README.md
CHANGED
|
@@ -41,11 +41,19 @@ Three things follow from that shape, and they are the reasons to choose this ove
|
|
|
41
41
|
pnpm add @waveso/docs
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
That is the whole installation. `react` and `react-dom` are required peers; `next` is optional, needed only by `@waveso/docs/next`.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
**Zod is not a peer.** It ships as a dependency of this package, so your project's Zod — version 3, version 4, or none at all — is irrelevant and nothing conflicts. When you extend the built-in schema, take `z` from here rather than from your own install:
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
```ts
|
|
49
|
+
import { docFrontmatterSchema, z } from '@waveso/docs/frontmatter';
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That is not a style preference. `.extend()` produces a schema only as trustworthy as the instance that built it, and re-exporting ours means the extension is built from the same module object by construction rather than by luck. Your own Zod stays yours, for everything else in your app.
|
|
53
|
+
|
|
54
|
+
**There is no `tailwindcss` peer and no Tailwind involved.** The stylesheet is plain CSS with `wave-docs-*` class names. It was declared as an optional peer once, which blocked `npm install` outright for any project on Tailwind 3 — npm still range-checks an optional peer that happens to be installed.
|
|
55
|
+
|
|
56
|
+
There is no `image-size` peer either. An `imageResolver` you write is welcome to read dimensions with it — but it is your dependency, in your own `package.json`. Declaring it here installed nothing and did not make `await import('image-size')` resolve for you; it only looked like it helped.
|
|
49
57
|
|
|
50
58
|
## Quick start
|
|
51
59
|
|
|
@@ -90,7 +98,7 @@ content/docs/
|
|
|
90
98
|
That is a working documentation site.
|
|
91
99
|
|
|
92
100
|
> [!IMPORTANT]
|
|
93
|
-
> `dynamicParams` must be written out as `false`. Route segment config is parsed statically before the module runs, so `export const dynamicParams = docs.dynamicParams` fails `next build`. Without it, Next
|
|
101
|
+
> `dynamicParams` must be written out as `false`. Route segment config is parsed statically before the module runs, so `export const dynamicParams = docs.dynamicParams` fails `next build`. Without it, Next invokes the route on a server at request time for every unlisted URL, to produce a 404 that was already knowable at build time — and `output: 'export'` refuses to build at all.
|
|
94
102
|
|
|
95
103
|
## Entry points
|
|
96
104
|
|
|
@@ -104,7 +112,7 @@ There is no root export. Every entry point is a subpath, so an import always nam
|
|
|
104
112
|
| `@waveso/docs/highlighter` | Node | `createDocsHighlighter`, `DEFAULT_DOCS_LANGS` |
|
|
105
113
|
| `@waveso/docs/search-index` | Node | `extractSearchRecords`, `buildSearchIndex`, `writeSearchIndex` |
|
|
106
114
|
| `@waveso/docs/react/*` | Browser + RSC | See [Components](#components) |
|
|
107
|
-
| `@waveso/docs/frontmatter` | Any | `docFrontmatterSchema`, `parseFrontmatter` |
|
|
115
|
+
| `@waveso/docs/frontmatter` | Any | `docFrontmatterSchema`, `parseFrontmatter`, `z` |
|
|
108
116
|
| `@waveso/docs/search-options` | Any | `SEARCH_INDEX_OPTIONS` |
|
|
109
117
|
| `@waveso/docs/types` | Any | Every shared type. Type-only |
|
|
110
118
|
| `@waveso/docs/styles.css` | — | The stylesheet |
|
|
@@ -211,8 +219,7 @@ Pass a `frontmatterSchema` and every `DocFile` and `RenderedDoc` carries your fi
|
|
|
211
219
|
|
|
212
220
|
```ts
|
|
213
221
|
// content/docs-schema.ts — one module, imported by every route file
|
|
214
|
-
import { docFrontmatterSchema } from '@waveso/docs/frontmatter';
|
|
215
|
-
import { z } from 'zod';
|
|
222
|
+
import { docFrontmatterSchema, z } from '@waveso/docs/frontmatter';
|
|
216
223
|
|
|
217
224
|
export const frontmatterSchema = docFrontmatterSchema.extend({
|
|
218
225
|
audience: z.enum(['user', 'operator']).exactOptional(),
|
|
@@ -227,7 +234,7 @@ doc?.frontmatter.audience; // 'user' | 'operator' | undefined
|
|
|
227
234
|
doc?.frontmatter.title; // string
|
|
228
235
|
```
|
|
229
236
|
|
|
230
|
-
Any [Standard Schema](https://standardschema.dev) validator works — Zod, Valibot, ArkType. The field is typed `StandardSchemaV1<unknown, TFrontmatter>` rather than as a Zod type, so the package does not dictate your validator
|
|
237
|
+
Any [Standard Schema](https://standardschema.dev) validator works — Zod, Valibot, ArkType. The field is typed `StandardSchemaV1<unknown, TFrontmatter>` rather than as a Zod type, so the package does not dictate your validator; a schema you hand over is never re-wrapped by the Zod in here. The `z` above is re-exported from this package precisely so that extending `docFrontmatterSchema` needs no install and no matching version.
|
|
231
238
|
|
|
232
239
|
Four things are worth knowing before you write one.
|
|
233
240
|
|
|
@@ -240,7 +247,9 @@ const docs = createDocsRoute<MyFrontmatter>({ contentDir: 'content/docs' });
|
|
|
240
247
|
|
|
241
248
|
**Unknown keys are stripped**, by Zod and by every other validator worth using. Declare every field you intend to read — under the base schema, a page with `audience: operator` parses fine and silently loses the value. `docFrontmatterSchema.extend(…)` keeps the built-ins; a `z.object({ … })` written from scratch does not.
|
|
242
249
|
|
|
243
|
-
**The
|
|
250
|
+
**The package's own fields survive a schema that forgets them.** `title` drives the `<h1>` fallback and `<title>`, `draft` the visibility filter, `aliases` the redirects, `order` and `label` the sidebar. These are parsed from the raw YAML and merged over your schema's output, so a custom schema can only ever *add* fields — it cannot drop or corrupt the ones the package reads itself.
|
|
251
|
+
|
|
252
|
+
That is a runtime guarantee, not a compile-time one, and the difference matters: `TFrontmatter extends DocFrontmatter` constrains only `title`, because the rest are optional. A `z.object({ title, audience })` type-checks perfectly and used to strip `draft` and `aliases` on the way through — publishing every draft, submitting them to Google, and silently returning no redirects at all. Prefer `docFrontmatterSchema.extend(…)` anyway: you then get the built-in fields in *your* inferred type, rather than merely at runtime.
|
|
244
253
|
|
|
245
254
|
**Export the schema from one module.** The filesystem scan is memoised per resolved config, and two schema objects count as the same schema only when they are the same object. Build one inline in each route file and each file pays for its own scan.
|
|
246
255
|
|
|
@@ -301,7 +310,33 @@ A ```` ```cfg ```` fence (or ```` ```conf ````) uses the `ini` grammar, because
|
|
|
301
310
|
the fence an author types follows the filename — nobody writes ```` ```ini ````
|
|
302
311
|
above a file called `server.cfg`.
|
|
303
312
|
|
|
304
|
-
Anything outside that set falls back to plain text rather than throwing. Pass `langs` to change the set, or `highlighter` to supply your own.
|
|
313
|
+
Anything outside that set falls back to plain text rather than throwing. Pass `langs` to change the set, or `highlighter` to supply your own. Fence languages are matched case-insensitively, so ```` ```JSON ```` and ```` ```Bash ```` highlight like their lowercase spellings rather than silently shipping monochrome.
|
|
314
|
+
|
|
315
|
+
### Images
|
|
316
|
+
|
|
317
|
+
**Absolute and external sources just work.** Put the file in `public/` and write ``.
|
|
318
|
+
|
|
319
|
+
```md
|
|
320
|
+
 ✅ served from public/
|
|
321
|
+
 ✅ external
|
|
322
|
+
 ⛔️ needs an imageResolver
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
A **relative** source is a different thing. Nothing in `public/` corresponds to it, and the browser would resolve it against the *route* — so `/docs/guide` and `/docs/guide/setup` request two different files from byte-identical markdown. Rather than ship that, a relative source with no `imageResolver` fails the build, naming the file and offering both fixes.
|
|
326
|
+
|
|
327
|
+
An `imageResolver` receives the source already folded against the markdown file's directory (`./diagram.png` in `guides/deploying.md` arrives as `guides/diagram.png`) and returns a public URL plus intrinsic dimensions — which `next/image` requires and markdown does not carry:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
createDocsRoute({
|
|
331
|
+
contentDir: 'content/docs',
|
|
332
|
+
imageResolver: async (src) => {
|
|
333
|
+
const { width, height } = await imageSize(path.join('content/docs', src));
|
|
334
|
+
return { src: `/docs-assets/${src}`, width, height };
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
A source that climbs above the content root fails the build whether or not a resolver is configured.
|
|
305
340
|
|
|
306
341
|
## Theming
|
|
307
342
|
|
|
@@ -318,19 +353,40 @@ your own `:root`, after the import:
|
|
|
318
353
|
```
|
|
319
354
|
|
|
320
355
|
That works because **everything this stylesheet declares lives in a `@layer`** —
|
|
321
|
-
`theme` for the tokens, `base` for element resets, `components` for the classes
|
|
322
|
-
and unlayered CSS outranks every layer regardless of specificity.
|
|
356
|
+
`theme` for the tokens, `base` for element resets, `components` for the classes
|
|
357
|
+
— and unlayered CSS outranks every layer regardless of specificity.
|
|
323
358
|
|
|
324
359
|
The distinction matters. The dark tokens are declared as
|
|
325
|
-
`:root
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
`:root:root:root`.
|
|
329
|
-
|
|
360
|
+
`:root[data-theme='dark']`, which is specificity (0,2,0). Outside a layer, an
|
|
361
|
+
unlayered `:root` at (0,1,0) would lose *no matter where it was loaded* — the
|
|
362
|
+
cascade never reaches source order — and overriding would mean writing
|
|
363
|
+
`:root:root:root`. Layered, source order settles it and a plain `:root` is
|
|
364
|
+
enough.
|
|
365
|
+
|
|
366
|
+
### Dark mode is opt-in
|
|
367
|
+
|
|
368
|
+
| On `<html>` | Result |
|
|
369
|
+
| --- | --- |
|
|
370
|
+
| nothing | Light |
|
|
371
|
+
| `class="dark"` | Dark |
|
|
372
|
+
| `data-theme="dark"` | Dark |
|
|
373
|
+
| `data-theme="system"` | Follows `prefers-color-scheme` |
|
|
374
|
+
|
|
375
|
+
`.dark` is honoured because [next-themes](https://github.com/pacocoursey/next-themes)
|
|
376
|
+
defaults to `attribute="class"` and never sets `data-theme`.
|
|
330
377
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
378
|
+
**This is deliberate, and it is a change.** The tokens used to switch on
|
|
379
|
+
`prefers-color-scheme` alone. But the stylesheet styles the docs subtree, not
|
|
380
|
+
the page — so on a light-only site with a `/docs` section, a visitor whose OS
|
|
381
|
+
was in dark mode got the near-white foreground ramp on the host's white
|
|
382
|
+
background: **1.23:1**, i.e. invisible. A stylesheet cannot assume it owns the
|
|
383
|
+
page it is dropped into, so it now switches only when the host says to.
|
|
384
|
+
|
|
385
|
+
If your site really does follow the OS and has no theme toggle, say so once:
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
<html lang="en" data-theme="system">
|
|
389
|
+
```
|
|
334
390
|
|
|
335
391
|
To restyle rather than retheme, override the classes — `.wave-docs-prose`,
|
|
336
392
|
`.wave-docs-skip-link`, and the rest — from your own unlayered CSS.
|
|
@@ -372,6 +428,24 @@ export function Search() {
|
|
|
372
428
|
|
|
373
429
|
MiniSearch is `import()`ed and the index fetched on hover, focus or first open — never on page load.
|
|
374
430
|
|
|
431
|
+
### What gets indexed
|
|
432
|
+
|
|
433
|
+
**The whole section**, not a preview of it. `extractSearchRecords` once truncated `text` to 300 characters *before* indexing, which dropped roughly 80% of a normal corpus — and because the default `combineWith: 'AND'` requires every term to land in the same record, a two-word query against a page that plainly contained both words returned nothing. Indexing and display are now separate concerns: the full text is searchable, and `storeFields` carries only what the dialog renders.
|
|
434
|
+
|
|
435
|
+
### CJK and other scripts
|
|
436
|
+
|
|
437
|
+
Tokenisation uses `Intl.Segmenter` where available, so Chinese, Japanese and Thai — which do not delimit words with spaces — index and query as words rather than as whole clauses. Without it, `search('安装')` matched nothing on a page that was entirely about 安装.
|
|
438
|
+
|
|
439
|
+
Both halves of the seam take the same overrides, and they must agree — an index built with one `tokenize` and queried with another matches nothing at all:
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
buildSearchIndex(records, { fuzzy: 0.1, prefix: true });
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
```tsx
|
|
446
|
+
<SearchDialog indexUrl="/search-index.json" searchOptions={{ fuzzy: 0.1, prefix: true }} />
|
|
447
|
+
```
|
|
448
|
+
|
|
375
449
|
## Configuration
|
|
376
450
|
|
|
377
451
|
```ts
|
|
@@ -388,7 +462,7 @@ interface DocsConfig<TFrontmatter extends DocFrontmatter = DocFrontmatter> {
|
|
|
388
462
|
|
|
389
463
|
| Option | Default | Purpose |
|
|
390
464
|
| --- | --- | --- |
|
|
391
|
-
| `langs` |
|
|
465
|
+
| `langs` | 18 grammars | Typed `readonly DocsLang[]`, so a typo is a compile error |
|
|
392
466
|
| `themes` | `github-light` / `github-dark` | Shiki theme pair |
|
|
393
467
|
| `highlighter` | built-in | Supply your own for grammars outside the set |
|
|
394
468
|
| `titleHeading` | `true` | Build an `<h1>` from `frontmatter.title` when the markdown has none |
|
|
@@ -415,9 +489,24 @@ export default () =>
|
|
|
415
489
|
createDocsSitemap({ contentDir: 'content/docs', siteUrl: 'https://example.com' });
|
|
416
490
|
```
|
|
417
491
|
|
|
492
|
+
`siteUrl` must be a bare origin. A path component (`https://example.com/product-docs`) is rejected, because `new URL(href, siteUrl)` discards it — every canonical and every sitemap entry would point somewhere that 404s. Put the path in `basePath`, which does take multiple segments.
|
|
493
|
+
|
|
494
|
+
**An alias is a redirect, not a page.** It is never prerendered, so linking one from your markdown fails the build and names the page to link instead. Aliases are also validated when the page is read:
|
|
495
|
+
|
|
496
|
+
| Alias | |
|
|
497
|
+
| --- | --- |
|
|
498
|
+
| `quickstart`, `guides/old-name` | ✅ |
|
|
499
|
+
| `v1:beta`, `c++`, `docs/(old)` | ⛔️ path-to-regexp metacharacters |
|
|
500
|
+
| `../escape`, `./here` | ⛔️ relative segments |
|
|
501
|
+
| `''` | ⛔️ empty |
|
|
502
|
+
|
|
503
|
+
The rejected spellings are not pedantry. Next compiles a redirect `source` as a path pattern, so `aliases: ['v1:beta']` installed a **wildcard** — it built green and then permanently 308'd `/docs/v1-guide`, a real prerendered page, away to somewhere else.
|
|
504
|
+
|
|
418
505
|
### Development
|
|
419
506
|
|
|
420
|
-
Markdown files are not in Next's module graph, so nothing recompiles a route module when one changes. `createDocsRoute` re-scans the content directory on every request outside `NODE_ENV=production`: edits appear on reload, new files are found without a restart
|
|
507
|
+
Markdown files are not in Next's module graph, so nothing recompiles a route module when one changes. `createDocsRoute` re-scans the content directory on every request outside `NODE_ENV=production`: edits appear on reload, new files are found without a restart, and the sidebar from `docs.source.nav()` agrees with the page body on the *same* request.
|
|
508
|
+
|
|
509
|
+
The rescan is shared. Next runs `generateMetadata` and your page concurrently, and a layout calling `nav()` is a third reader; invalidation is wrapped in `React.cache`, so the first of them re-reads the disk and the rest see that scan. Without it each invalidated the others' work in flight — measured at 22 `readdir` + 824 `readFile` per request on a 401-file tree, against 11 + 412 for one scan.
|
|
421
510
|
|
|
422
511
|
## Requirements
|
|
423
512
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region src/docs-error.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* One error shape for the whole package.
|
|
4
|
+
*
|
|
5
|
+
* Private — deliberately not an entry point in `package.json`. Consumers get
|
|
6
|
+
* the `code` off the error they already catch; they do not import a class and
|
|
7
|
+
* they cannot `instanceof` against a copy of this module resolved twice.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* What went wrong, as something a consumer can branch on.
|
|
11
|
+
*
|
|
12
|
+
* Before this existed, every failure was a bare `Error` and roughly half the
|
|
13
|
+
* messages omitted the package prefix, so a host wanting to downgrade (say)
|
|
14
|
+
* broken-link failures in dev had nothing to test but the message text — and
|
|
15
|
+
* `message.startsWith('@waveso/docs:')` was not even a reliable filter.
|
|
16
|
+
*/
|
|
17
|
+
type DocsErrorCode =
|
|
18
|
+
/** A link resolves to a route no published page owns. */
|
|
19
|
+
'broken-link' |
|
|
20
|
+
/** A link resolves to a page that exists but is `draft: true`. */
|
|
21
|
+
'draft-link' |
|
|
22
|
+
/** A link resolves to an alias, which is a redirect and not a page. */
|
|
23
|
+
'alias-link' |
|
|
24
|
+
/** An `aliases` entry is empty, escapes the root, or is not URL-safe. */
|
|
25
|
+
'invalid-alias' |
|
|
26
|
+
/** Two pages claim one alias, or an alias shadows a real route. */
|
|
27
|
+
'alias-collision' |
|
|
28
|
+
/** Two files resolve to the same route. */
|
|
29
|
+
'route-collision' |
|
|
30
|
+
/** A page's frontmatter is missing, malformed, or rejected by the schema. */
|
|
31
|
+
'invalid-frontmatter' |
|
|
32
|
+
/** A `meta.json` is malformed, or names something that is not there. */
|
|
33
|
+
'invalid-meta' |
|
|
34
|
+
/** An option passed to this package cannot be used as given. */
|
|
35
|
+
'invalid-config' |
|
|
36
|
+
/** `contentDir` does not point at a readable directory. */
|
|
37
|
+
'missing-content-dir' |
|
|
38
|
+
/** A markdown page is reachable only through a broken symbolic link. */
|
|
39
|
+
'broken-symlink' |
|
|
40
|
+
/** An `imageResolver` returned an unusable shape, threw, or was needed. */
|
|
41
|
+
'invalid-image' |
|
|
42
|
+
/** A theme name outside the supported set. */
|
|
43
|
+
'unknown-theme' |
|
|
44
|
+
/** A fence language outside the loaded set. */
|
|
45
|
+
'unknown-language' |
|
|
46
|
+
/** An optional peer (`next`) is absent or not the expected shape. */
|
|
47
|
+
'missing-peer' |
|
|
48
|
+
/** The search index could not be fetched or parsed. */
|
|
49
|
+
'search-index-unavailable' |
|
|
50
|
+
/** A plugin ran without the context this package always supplies. */
|
|
51
|
+
'internal';
|
|
52
|
+
/** An {@link Error} carrying a {@link DocsErrorCode}. */
|
|
53
|
+
interface DocsError extends Error {
|
|
54
|
+
readonly code: DocsErrorCode;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build an error that names this package and says what class of thing failed.
|
|
58
|
+
*
|
|
59
|
+
* The message is passed through untouched apart from the prefix, which is added
|
|
60
|
+
* only when absent — the wording at each throw site is the part a human reads,
|
|
61
|
+
* and several of them took real effort to get right.
|
|
62
|
+
*
|
|
63
|
+
* `code` is attached non-enumerably so it does not appear in `JSON.stringify`
|
|
64
|
+
* or a spread, which keeps error objects looking exactly as they did while
|
|
65
|
+
* still being branchable.
|
|
66
|
+
*
|
|
67
|
+
* The stack is left alone. Flattening it would hide the `dist/` frames a
|
|
68
|
+
* consumer does not care about, and also the ones a maintainer needs.
|
|
69
|
+
*/
|
|
70
|
+
declare function docsError(code: DocsErrorCode, message: string, options?: ErrorOptions): DocsError;
|
|
71
|
+
/** Narrow an unknown caught value to one of this package's errors. */
|
|
72
|
+
declare function isDocsError(value: unknown): value is DocsError;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { DocsError, DocsErrorCode, docsError, isDocsError };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/docs-error.ts
|
|
2
|
+
/**
|
|
3
|
+
* One error shape for the whole package.
|
|
4
|
+
*
|
|
5
|
+
* Private — deliberately not an entry point in `package.json`. Consumers get
|
|
6
|
+
* the `code` off the error they already catch; they do not import a class and
|
|
7
|
+
* they cannot `instanceof` against a copy of this module resolved twice.
|
|
8
|
+
*/
|
|
9
|
+
/** Prefix every message carries, so a stack trace names the culprit package. */
|
|
10
|
+
const PREFIX = "@waveso/docs: ";
|
|
11
|
+
/**
|
|
12
|
+
* Build an error that names this package and says what class of thing failed.
|
|
13
|
+
*
|
|
14
|
+
* The message is passed through untouched apart from the prefix, which is added
|
|
15
|
+
* only when absent — the wording at each throw site is the part a human reads,
|
|
16
|
+
* and several of them took real effort to get right.
|
|
17
|
+
*
|
|
18
|
+
* `code` is attached non-enumerably so it does not appear in `JSON.stringify`
|
|
19
|
+
* or a spread, which keeps error objects looking exactly as they did while
|
|
20
|
+
* still being branchable.
|
|
21
|
+
*
|
|
22
|
+
* The stack is left alone. Flattening it would hide the `dist/` frames a
|
|
23
|
+
* consumer does not care about, and also the ones a maintainer needs.
|
|
24
|
+
*/
|
|
25
|
+
function docsError(code, message, options) {
|
|
26
|
+
const error = new Error(message.startsWith(PREFIX) ? message : `${PREFIX}${message}`, options);
|
|
27
|
+
Object.defineProperty(error, "code", {
|
|
28
|
+
value: code,
|
|
29
|
+
enumerable: false,
|
|
30
|
+
writable: false,
|
|
31
|
+
configurable: true
|
|
32
|
+
});
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
/** Narrow an unknown caught value to one of this package's errors. */
|
|
36
|
+
function isDocsError(value) {
|
|
37
|
+
return value instanceof Error && typeof value.code === "string" && value.message.startsWith(PREFIX);
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { docsError, isDocsError };
|
package/dist/frontmatter.d.ts
CHANGED
|
@@ -6,10 +6,10 @@ import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
|
6
6
|
* The frontmatter fields the package itself understands, as a Zod schema.
|
|
7
7
|
*
|
|
8
8
|
* Optional fields use `.exactOptional()` rather than `.optional()` so the
|
|
9
|
-
* inferred type is `{ description?: string }
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* inferred type is `{ description?: string }`: absent stays absent, which is
|
|
10
|
+
* the only thing YAML can express — there is no way to author an explicit
|
|
11
|
+
* `undefined` in a frontmatter block. A consumer's schema may still use plain
|
|
12
|
+
* `.optional()`; {@link DocFrontmatter} accepts both.
|
|
13
13
|
*
|
|
14
14
|
* `description` is deliberately not length-capped. The 155–160 character
|
|
15
15
|
* advice for `<meta name="description">` is a pixel-width heuristic about
|
|
@@ -39,6 +39,17 @@ declare const docFrontmatterSchema: z.ZodObject<{
|
|
|
39
39
|
* `raw` is whatever the YAML parser produced — `unknown` by construction, so
|
|
40
40
|
* every field is checked rather than trusted.
|
|
41
41
|
*
|
|
42
|
+
* The six fields this package reads are always parsed from the raw block by
|
|
43
|
+
* {@link docFrontmatterSchema} and laid back over the result, so a custom
|
|
44
|
+
* `schema` can only ever *add* fields. It cannot drop `draft`, `aliases`,
|
|
45
|
+
* `order`, `label`, `description` or `title` — which a bare
|
|
46
|
+
* `z.object({ title, audience })` silently does — and it cannot redefine them
|
|
47
|
+
* into a shape the sidebar, the redirects and the sitemap do not expect. The
|
|
48
|
+
* price is that a `.default()`, `.transform()` or `.coerce` aimed at one of the
|
|
49
|
+
* six is not honoured: the YAML wins. The return type intersects
|
|
50
|
+
* {@link DocFrontmatter} for the same reason — the built-ins are there whether
|
|
51
|
+
* or not the schema declared them.
|
|
52
|
+
*
|
|
42
53
|
* Async because `~standard.validate` is allowed to return a promise and some
|
|
43
54
|
* validators do (any schema with an async refinement). The source layer is
|
|
44
55
|
* already async, so awaiting here costs nothing and refusing promises would
|
|
@@ -47,9 +58,30 @@ declare const docFrontmatterSchema: z.ZodObject<{
|
|
|
47
58
|
* @param raw - Parsed YAML frontmatter block.
|
|
48
59
|
* @param filePath - Path reported in the error. Pass the path the author
|
|
49
60
|
* would recognise (relative to the content root), not an absolute one.
|
|
50
|
-
|
|
61
|
+
*/
|
|
62
|
+
declare function parseFrontmatter(raw: unknown, filePath: string): Promise<DocFrontmatter>;
|
|
63
|
+
/**
|
|
64
|
+
* Validate one file's frontmatter against `schema`, or throw an error that
|
|
65
|
+
* names the file. See the two-argument overload for the contract.
|
|
66
|
+
*
|
|
67
|
+
* `TFrontmatter` comes from the schema and must never be named at the call
|
|
68
|
+
* site. That is what the two overloads buy: `parseFrontmatter<Mine>(raw, file)`
|
|
69
|
+
* with no schema is a compile error, where before it type-checked and then lied
|
|
70
|
+
* — nothing validated `Mine`, so its fields were `undefined` at runtime while
|
|
71
|
+
* typed as present.
|
|
72
|
+
*
|
|
73
|
+
* `undefined` is accepted here rather than the schema being optional so that a
|
|
74
|
+
* `DocsConfig<T>`'s own optional schema passes straight through. An earlier
|
|
75
|
+
* attempt made it required and broke exactly that, at `createDocsRoute` and at
|
|
76
|
+
* any consumer holding a typed config.
|
|
77
|
+
*
|
|
78
|
+
* @param raw - Parsed YAML frontmatter block.
|
|
79
|
+
* @param filePath - Path reported in the error. Pass the path the author
|
|
80
|
+
* would recognise (relative to the content root), not an absolute one.
|
|
81
|
+
* @param schema - Replacement schema, normally
|
|
51
82
|
* `docFrontmatterSchema.extend(...)`. Any Standard Schema validator works.
|
|
83
|
+
* `undefined` selects {@link docFrontmatterSchema}.
|
|
52
84
|
*/
|
|
53
|
-
declare function parseFrontmatter<TFrontmatter extends DocFrontmatter
|
|
85
|
+
declare function parseFrontmatter<TFrontmatter extends DocFrontmatter>(raw: unknown, filePath: string, schema: StandardSchemaV1<unknown, TFrontmatter> | undefined): Promise<TFrontmatter & DocFrontmatter>;
|
|
54
86
|
//#endregion
|
|
55
|
-
export { docFrontmatterSchema, parseFrontmatter };
|
|
87
|
+
export { docFrontmatterSchema, parseFrontmatter, z };
|
package/dist/frontmatter.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { docsError } from "./docs-error.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
//#region src/frontmatter.ts
|
|
3
4
|
/**
|
|
4
5
|
* The frontmatter fields the package itself understands, as a Zod schema.
|
|
5
6
|
*
|
|
6
7
|
* Optional fields use `.exactOptional()` rather than `.optional()` so the
|
|
7
|
-
* inferred type is `{ description?: string }
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* inferred type is `{ description?: string }`: absent stays absent, which is
|
|
9
|
+
* the only thing YAML can express — there is no way to author an explicit
|
|
10
|
+
* `undefined` in a frontmatter block. A consumer's schema may still use plain
|
|
11
|
+
* `.optional()`; {@link DocFrontmatter} accepts both.
|
|
11
12
|
*
|
|
12
13
|
* `description` is deliberately not length-capped. The 155–160 character
|
|
13
14
|
* advice for `<meta name="description">` is a pixel-width heuristic about
|
|
@@ -32,34 +33,60 @@ const docFrontmatterSchema = z.object({
|
|
|
32
33
|
order: z.number().exactOptional()
|
|
33
34
|
});
|
|
34
35
|
/**
|
|
35
|
-
*
|
|
36
|
+
* The package's own fields, every one of them optional, for the overlay pass
|
|
37
|
+
* in {@link parseFrontmatter}.
|
|
36
38
|
*
|
|
37
|
-
* `
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* already async, so awaiting here costs nothing and refusing promises would
|
|
43
|
-
* have made a documented half of the spec silently unsupported.
|
|
44
|
-
*
|
|
45
|
-
* @param raw - Parsed YAML frontmatter block.
|
|
46
|
-
* @param filePath - Path reported in the error. Pass the path the author
|
|
47
|
-
* would recognise (relative to the content root), not an absolute one.
|
|
48
|
-
* @param schema - Optional replacement schema, normally
|
|
49
|
-
* `docFrontmatterSchema.extend(...)`. Any Standard Schema validator works.
|
|
39
|
+
* `title` is optional here too, even though the package requires it: a custom
|
|
40
|
+
* schema may legitimately produce a title the YAML does not carry (a
|
|
41
|
+
* `.default()`), and demanding one of the raw block would fail that file. The
|
|
42
|
+
* check after the merge reads the merged title instead, so both routes to a
|
|
43
|
+
* title are accepted and no route to a missing one is.
|
|
50
44
|
*/
|
|
45
|
+
const builtInFields = docFrontmatterSchema.partial();
|
|
51
46
|
async function parseFrontmatter(raw, filePath, schema) {
|
|
52
47
|
const active = schema ?? docFrontmatterSchema;
|
|
53
48
|
let result;
|
|
54
49
|
try {
|
|
55
50
|
result = await active["~standard"].validate(raw ?? {});
|
|
56
51
|
} catch (error) {
|
|
57
|
-
throw
|
|
52
|
+
throw docsError("invalid-frontmatter", `Invalid frontmatter in ${filePath}: the schema threw while validating it. This is a bug in the schema, not in the YAML — check any \`refine\`/\`transform\`/\`check\` it declares.`, { cause: error });
|
|
58
53
|
}
|
|
59
|
-
if (result.issues === void 0) return result.value;
|
|
60
|
-
if (result.issues.length === 0) throw
|
|
61
|
-
const
|
|
62
|
-
throw
|
|
54
|
+
if (result.issues === void 0) return active === docFrontmatterSchema ? result.value : applyBuiltIns(result.value, raw, filePath);
|
|
55
|
+
if (result.issues.length === 0) throw docsError("invalid-frontmatter", `Invalid frontmatter in ${filePath}: the schema rejected it but reported no issues, so there is nothing to act on. This is a bug in the schema.`);
|
|
56
|
+
const source = schema === void 0 ? "Fix the YAML block at the top of that file." : "Fix the YAML block at the top of that file, or the `frontmatterSchema` in your docs config.";
|
|
57
|
+
throw docsError("invalid-frontmatter", `Invalid frontmatter in ${filePath}:\n${formatIssues(result.issues)}\n${source}`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Re-read the package's own fields from the raw block and lay them back over a
|
|
61
|
+
* custom schema's output.
|
|
62
|
+
*
|
|
63
|
+
* Without this, the guarantee that a `frontmatterSchema` keeps the built-ins
|
|
64
|
+
* rests on `TFrontmatter extends DocFrontmatter` — which constrains `title` and
|
|
65
|
+
* nothing else, because the other five fields are optional. So
|
|
66
|
+
* `z.object({ title, audience })`, written from scratch instead of as
|
|
67
|
+
* `docFrontmatterSchema.extend(...)`, type-checks and then strips `draft`,
|
|
68
|
+
* `aliases`, `order` and `label` on the way through. The package reads the
|
|
69
|
+
* absence as "not a draft, no redirects, no order": an unpublished page ships,
|
|
70
|
+
* is linked from the sidebar and is submitted to Google in the sitemap, and
|
|
71
|
+
* every renamed page 404s. `tsc` reports nothing, at either end.
|
|
72
|
+
*
|
|
73
|
+
* The overlay runs after the custom schema rather than instead of it, so a
|
|
74
|
+
* stricter rule on a built-in (`title: z.string().max(60)`) still fails the
|
|
75
|
+
* build — it just cannot change the value the package then reads.
|
|
76
|
+
*/
|
|
77
|
+
async function applyBuiltIns(value, raw, filePath) {
|
|
78
|
+
const result = await builtInFields["~standard"].validate(raw ?? {});
|
|
79
|
+
if (result.issues !== void 0) throw docsError("invalid-frontmatter", `Invalid frontmatter in ${filePath}:\n${formatIssues(result.issues)}\n@waveso/docs reads these fields itself, so they are validated even when your \`frontmatterSchema\` does not declare them. Fix the YAML block at the top of that file.`);
|
|
80
|
+
const merged = {
|
|
81
|
+
...value,
|
|
82
|
+
...result.value
|
|
83
|
+
};
|
|
84
|
+
if (typeof merged.title !== "string" || merged.title.length === 0) throw docsError("invalid-frontmatter", `Invalid frontmatter in ${filePath}: no \`title\`. Every page needs one — it drives the \`<h1>\` fallback, \`<title>\`, the sidebar and search. Add \`title:\` to the YAML block at the top of that file, or give your \`frontmatterSchema\` a title it can supply.`);
|
|
85
|
+
return merged;
|
|
86
|
+
}
|
|
87
|
+
/** Render a validator's issues as the bullet list under the file name. */
|
|
88
|
+
function formatIssues(issues) {
|
|
89
|
+
return issues.map((issue) => ` - ${formatIssuePath(issue.path)}: ${issue.message}`).join("\n");
|
|
63
90
|
}
|
|
64
91
|
/**
|
|
65
92
|
* Render an issue path as `aliases[0]` / `title`, never as `""`.
|
|
@@ -77,4 +104,4 @@ function formatIssuePath(path) {
|
|
|
77
104
|
}, "");
|
|
78
105
|
}
|
|
79
106
|
//#endregion
|
|
80
|
-
export { docFrontmatterSchema, parseFrontmatter };
|
|
107
|
+
export { docFrontmatterSchema, parseFrontmatter, z };
|
package/dist/highlighter.d.ts
CHANGED
|
@@ -81,9 +81,9 @@ interface DocsHighlighterOptions {
|
|
|
81
81
|
* than a build-time throw. The runtime check below stays for JavaScript
|
|
82
82
|
* callers and for values that arrive from JSON config.
|
|
83
83
|
*/
|
|
84
|
-
langs?: readonly DocsLang[];
|
|
84
|
+
langs?: readonly DocsLang[] | undefined;
|
|
85
85
|
/** Theme pair. Defaults to {@link DEFAULT_DOCS_THEMES}. */
|
|
86
|
-
themes?: DocsThemes;
|
|
86
|
+
themes?: DocsThemes | undefined;
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
89
|
* Create (or reuse) the process-wide highlighter for a given option set.
|
package/dist/highlighter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { docsError } from "./docs-error.js";
|
|
1
2
|
import { createHighlighterCore } from "shiki/core";
|
|
2
3
|
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
|
3
4
|
//#region src/highlighter.ts
|
|
@@ -155,11 +156,11 @@ function isDocsTheme(name) {
|
|
|
155
156
|
function createDocsHighlighter(options = {}) {
|
|
156
157
|
const themes = options.themes ?? DEFAULT_DOCS_THEMES;
|
|
157
158
|
const requested = options.langs ?? DEFAULT_DOCS_LANGS;
|
|
158
|
-
for (const theme of [themes.light, themes.dark]) if (!isDocsTheme(theme)) throw
|
|
159
|
+
for (const theme of [themes.light, themes.dark]) if (!isDocsTheme(theme)) throw docsError("unknown-theme", `@waveso/docs: unknown Shiki theme '${theme}'. Supported themes: ${Object.keys(THEME_LOADERS).sort().join(", ")}. To use another theme, pass your own highlighter to createDocsRenderer().`);
|
|
159
160
|
const langs = [...new Set(requested)].sort();
|
|
160
161
|
const loaders = /* @__PURE__ */ new Set();
|
|
161
162
|
for (const lang of langs) {
|
|
162
|
-
if (!isDocsLang(lang)) throw
|
|
163
|
+
if (!isDocsLang(lang)) throw docsError("unknown-language", `@waveso/docs: unknown code language '${lang}'. Supported languages: ${Object.keys(LANG_LOADERS).sort().join(", ")}. To use another grammar, pass your own highlighter to createDocsRenderer().`);
|
|
163
164
|
loaders.add(LANG_LOADERS[lang]);
|
|
164
165
|
}
|
|
165
166
|
const key = JSON.stringify({
|