@staticbolt/docs 0.0.1-beta.3 → 0.0.1-beta.4
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/README.md +15 -13
- package/lib/index.d.mts +27 -32
- package/lib/plugins.mjs +143 -151
- package/package.json +9 -3
- package/sources/parts/document/document.part.html +2 -0
- package/sources/parts/sidebar/sidebar.part.html +1 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ export default defineConfig({
|
|
|
33
33
|
}),
|
|
34
34
|
|
|
35
35
|
plugins.htmlLayoutPlugin({ tags: ["layout", "part", "icon"] }),
|
|
36
|
-
theme.
|
|
36
|
+
theme.htmlCodeBlockPlugin({ rehypeExpressiveCodeOptions }),
|
|
37
37
|
theme.katexFontsPlugin(),
|
|
38
38
|
theme.pagefindPlugin(),
|
|
39
39
|
theme.llmsPlugin({ siteUrl: "https://example.com" }),
|
|
@@ -96,6 +96,7 @@ Copy this as `sources/layouts/document.layout.html` and edit the names:
|
|
|
96
96
|
src="@staticbolt/docs/parts/document/document.part.html"
|
|
97
97
|
title="My Project"
|
|
98
98
|
>
|
|
99
|
+
<slot name="sidebar" slot="sidebar"></slot>
|
|
99
100
|
<part slot="icons" src="@parts/social-icons/social-icons.part.html"></part>
|
|
100
101
|
<slot />
|
|
101
102
|
</part>
|
|
@@ -107,17 +108,17 @@ Copy this as `sources/layouts/document.layout.html` and edit the names:
|
|
|
107
108
|
|
|
108
109
|
To change the page structure, copy `node_modules/@staticbolt/docs/sources/parts/document/document.part.html` into the project and point the layout at the copy; it references the other parts by package path, so it works as is.
|
|
109
110
|
|
|
110
|
-
| Part | Role
|
|
111
|
-
| --------------------- |
|
|
112
|
-
| `document` | The whole page. `title`, `minHeading`, `maxHeading`, `mermaid`,
|
|
113
|
-
| `header` | Takes `title`. Slot `icons` for the social links.
|
|
114
|
-
| `sidebar` |
|
|
115
|
-
| `table-of-content` | `-desktop` takes `minHeading` and `maxHeading`, `-mobile` none.
|
|
116
|
-
| `pagination-links` | Previous/next links read from the sidebar.
|
|
117
|
-
| `callouts` | Build-time script turning collapsible callouts into accordions.
|
|
118
|
-
| `mermaid` | Renders ` ```mermaid ` fences. Removes itself from pages without. Needs `mermaid` installed.
|
|
119
|
-
| `color-scheme-switch` | Already inside header and sidebar.
|
|
120
|
-
| `badge` | Used by the sidebar for `data.badgeText` / `data.badgeVariant`.
|
|
111
|
+
| Part | Role |
|
|
112
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
113
|
+
| `document` | The whole page. `title`, `minHeading`, `maxHeading`, `mermaid`, slots `sidebar` and `icons`, default slot. |
|
|
114
|
+
| `header` | Takes `title`. Slot `icons` for the social links. |
|
|
115
|
+
| `sidebar` | Default slot for the tree, filled by `contentPlugin` through the layout's `sidebar` slot. Slot `icons` for the social links. |
|
|
116
|
+
| `table-of-content` | `-desktop` takes `minHeading` and `maxHeading`, `-mobile` none. |
|
|
117
|
+
| `pagination-links` | Previous/next links read from the sidebar. |
|
|
118
|
+
| `callouts` | Build-time script turning collapsible callouts into accordions. |
|
|
119
|
+
| `mermaid` | Renders ` ```mermaid ` fences. Removes itself from pages without. Needs `mermaid` installed. |
|
|
120
|
+
| `color-scheme-switch` | Already inside header and sidebar. |
|
|
121
|
+
| `badge` | Used by the sidebar for `data.badgeText` / `data.badgeVariant`. |
|
|
121
122
|
|
|
122
123
|
## Styles
|
|
123
124
|
|
|
@@ -145,4 +146,5 @@ Any `--sb-*` variable from `global.css` can be overridden the same way.
|
|
|
145
146
|
## Overriding a part
|
|
146
147
|
|
|
147
148
|
Copy it from `node_modules/@staticbolt/docs/sources/parts/` into the project and point the layout at the copy.
|
|
148
|
-
For the
|
|
149
|
+
For the folder and link parts pass the copy through `contentPlugin`'s `directoryLayout` or `linkLayout`.
|
|
150
|
+
A layout of your own has to forward the `sidebar` slot down to the sidebar part, as the one above does.
|
package/lib/index.d.mts
CHANGED
|
@@ -5,7 +5,6 @@ import { Root } from "hast";
|
|
|
5
5
|
import { LanguageRegistration } from "shiki";
|
|
6
6
|
import { Plugin as Plugin$1 } from "unified";
|
|
7
7
|
import { Root as Root$1 } from "mdast";
|
|
8
|
-
import { Node } from "unist";
|
|
9
8
|
//#region plugins/code-block-plugin.d.ts
|
|
10
9
|
interface HtmlCodeBlockPlugin {
|
|
11
10
|
/**
|
|
@@ -21,7 +20,7 @@ interface HtmlCodeBlockPlugin {
|
|
|
21
20
|
* replaces the tag with a syntax highlighted code block. Use the `lang` attribute to set the language, and the other attributes
|
|
22
21
|
* for the rest of the expressive-code metadata.
|
|
23
22
|
*/
|
|
24
|
-
declare function
|
|
23
|
+
declare function htmlCodeBlockPlugin(options?: HtmlCodeBlockPlugin): Plugin;
|
|
25
24
|
//#endregion
|
|
26
25
|
//#region plugins/collapsible-plugin.d.ts
|
|
27
26
|
interface CollapsiblePluginOptions {
|
|
@@ -161,38 +160,15 @@ interface ContentPluginOptions {
|
|
|
161
160
|
* @default true
|
|
162
161
|
*/
|
|
163
162
|
collapsed?: boolean;
|
|
164
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Defines the layout name/path to be used for markdown files.\
|
|
165
|
+
* The sidebar tree goes into the layout's `sidebar` slot, so every layout listed here has to carry a `<slot name="sidebar">`,
|
|
166
|
+
* or forward one to the part that does.
|
|
167
|
+
*/
|
|
165
168
|
layouts: {
|
|
166
169
|
default: string;
|
|
167
170
|
[key: string]: string;
|
|
168
171
|
};
|
|
169
|
-
/** The layout into which the generated sidebar HTML will be injected. */
|
|
170
|
-
sidebarLayout?: {
|
|
171
|
-
/**
|
|
172
|
-
* Path to the layout file.
|
|
173
|
-
*
|
|
174
|
-
* @default the theme's `sidebar.part.html`
|
|
175
|
-
*/
|
|
176
|
-
path?: string;
|
|
177
|
-
/**
|
|
178
|
-
* The custom-element tag name used for this layout.
|
|
179
|
-
*
|
|
180
|
-
* If omitted, the tag name will be auto extracted from the layout path
|
|
181
|
-
*/
|
|
182
|
-
tag?: string;
|
|
183
|
-
/**
|
|
184
|
-
* The attribute on the layout tag that holds the source path.
|
|
185
|
-
*
|
|
186
|
-
* @default "src"
|
|
187
|
-
*/
|
|
188
|
-
sourceAttribute?: string;
|
|
189
|
-
/**
|
|
190
|
-
* The slot name into which the sidebar is inserted.
|
|
191
|
-
*
|
|
192
|
-
* @default "sidebar"
|
|
193
|
-
*/
|
|
194
|
-
slotName?: string;
|
|
195
|
-
};
|
|
196
172
|
/** The layout used to render a collapsible directory node. */
|
|
197
173
|
directoryLayout?: {
|
|
198
174
|
/**
|
|
@@ -293,6 +269,25 @@ interface LlmsPluginOptions {
|
|
|
293
269
|
title?: string;
|
|
294
270
|
/** One-line summary quoted under the heading. */
|
|
295
271
|
summary?: string;
|
|
272
|
+
/**
|
|
273
|
+
* Glob patterns for the pages to list.
|
|
274
|
+
*
|
|
275
|
+
* @default ["pages/**\/*.md"]
|
|
276
|
+
*/
|
|
277
|
+
include?: string[];
|
|
278
|
+
/**
|
|
279
|
+
* Glob patterns to leave out.
|
|
280
|
+
*
|
|
281
|
+
* @default [ ]
|
|
282
|
+
*/
|
|
283
|
+
exclude?: string[];
|
|
284
|
+
/**
|
|
285
|
+
* Path of the pair of files, relative to the output directory and without the extension: `"llms"` writes `llms.txt` and
|
|
286
|
+
* `llms-full.txt`. Give each plugin a path of its own when a site runs more than one.
|
|
287
|
+
*
|
|
288
|
+
* @default "llms"
|
|
289
|
+
*/
|
|
290
|
+
output?: string;
|
|
296
291
|
}
|
|
297
292
|
/**
|
|
298
293
|
* Writes `llms.txt` and `llms-full.txt`, plus a copy of every page's markdown beside its HTML.
|
|
@@ -381,6 +376,6 @@ interface PagefindPluginOptions {
|
|
|
381
376
|
declare function pagefindPlugin({ glob }?: PagefindPluginOptions): Plugin;
|
|
382
377
|
//#endregion
|
|
383
378
|
//#region plugins/wrap-table.d.ts
|
|
384
|
-
declare function wrapTables(): (tree:
|
|
379
|
+
declare function wrapTables(): (tree: Root) => void;
|
|
385
380
|
//#endregion
|
|
386
|
-
export { CollapsiblePluginOptions, HtmlCodeBlockPlugin, PersistentCache, RemarkObsidianCommentsOptions, cachedRehypeExpressiveCode, contentPlugin, ecCachePlugin, ecCacheSalt, htmlWithTypeScriptScripts, katexFontsPlugin, llmsPlugin, normalizeTitle, pagefindPlugin, pluginCollapsible, rehypeMermaidFence, remarkObsidianComments, remarkObsidianTags, sha1, wrapTables };
|
|
381
|
+
export { CollapsiblePluginOptions, HtmlCodeBlockPlugin, PersistentCache, RemarkObsidianCommentsOptions, cachedRehypeExpressiveCode, contentPlugin, ecCachePlugin, ecCacheSalt, htmlCodeBlockPlugin, htmlWithTypeScriptScripts, katexFontsPlugin, llmsPlugin, normalizeTitle, pagefindPlugin, pluginCollapsible, rehypeMermaidFence, remarkObsidianComments, remarkObsidianTags, sha1, wrapTables };
|
package/lib/plugins.mjs
CHANGED
|
@@ -5,13 +5,12 @@ import { ExpressiveCodeBlock } from "expressive-code";
|
|
|
5
5
|
import { toHtml } from "hast-util-to-html";
|
|
6
6
|
import rehypeExpressiveCode, { createRenderer } from "rehype-expressive-code";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { cpSync, existsSync, globSync, mkdirSync, readFileSync,
|
|
8
|
+
import { cpSync, existsSync, globSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import yaml from "yaml";
|
|
11
11
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
12
12
|
import { visit } from "unist-util-visit";
|
|
13
13
|
import * as pagefind from "pagefind";
|
|
14
|
-
import { selectAll } from "hast-util-select";
|
|
15
14
|
//#region plugins/ec-cache.ts
|
|
16
15
|
/**
|
|
17
16
|
* Persistent cross-build caches for expressive-code rendering.
|
|
@@ -119,7 +118,7 @@ function cachedRehypeExpressiveCode(options) {
|
|
|
119
118
|
* replaces the tag with a syntax highlighted code block. Use the `lang` attribute to set the language, and the other attributes
|
|
120
119
|
* for the rest of the expressive-code metadata.
|
|
121
120
|
*/
|
|
122
|
-
function
|
|
121
|
+
function htmlCodeBlockPlugin(options = {}) {
|
|
123
122
|
const tag = options.tagName ?? "code-block";
|
|
124
123
|
let renderCache;
|
|
125
124
|
let diskCache;
|
|
@@ -417,11 +416,6 @@ function prepareOptions(options) {
|
|
|
417
416
|
const icons = options.icons ?? "";
|
|
418
417
|
const publish = options.publish ?? true;
|
|
419
418
|
const collapsed = options.collapsed ?? true;
|
|
420
|
-
const sidebarLayout = options.sidebarLayout ?? {};
|
|
421
|
-
sidebarLayout.path ??= themeSidebarPart("sidebar.part.html");
|
|
422
|
-
sidebarLayout.tag ??= path.basename(sidebarLayout.path).split(".")[1];
|
|
423
|
-
sidebarLayout.sourceAttribute ??= "src";
|
|
424
|
-
sidebarLayout.slotName ??= "sidebar";
|
|
425
419
|
const directoryLayout = options.directoryLayout ?? {};
|
|
426
420
|
directoryLayout.path ??= themeSidebarPart("directory.part.html");
|
|
427
421
|
directoryLayout.tag ??= path.basename(directoryLayout.path).split(".")[1];
|
|
@@ -439,7 +433,6 @@ function prepareOptions(options) {
|
|
|
439
433
|
icons,
|
|
440
434
|
publish,
|
|
441
435
|
collapsed,
|
|
442
|
-
sidebarLayout,
|
|
443
436
|
directoryLayout,
|
|
444
437
|
linkLayout
|
|
445
438
|
};
|
|
@@ -500,51 +493,6 @@ function parseSidebarFrontmatter(file) {
|
|
|
500
493
|
};
|
|
501
494
|
}
|
|
502
495
|
//#endregion
|
|
503
|
-
//#region plugins/content-plugin/render-tree.ts
|
|
504
|
-
/** `<icon>` source for a node, `undefined` when the project has no icons directory or the node no icon. */
|
|
505
|
-
function iconSource(icons, icon) {
|
|
506
|
-
if (!icons || !icon) return;
|
|
507
|
-
return `${icons.replace(/\/$/, "")}/${icon}.icon.html`;
|
|
508
|
-
}
|
|
509
|
-
function renderDirectory(directory, items, { directoryLayout, customizations, icons }) {
|
|
510
|
-
const customization = customizations[directory.realPath];
|
|
511
|
-
const data = {
|
|
512
|
-
title: customization?.label ?? directory.title,
|
|
513
|
-
collapsed: customization?.collapsed ?? directory.collapsed,
|
|
514
|
-
...customization?.data,
|
|
515
|
-
iconSrc: iconSource(icons, customization?.data?.icon)
|
|
516
|
-
};
|
|
517
|
-
return `<li class="sidebar-list-item">
|
|
518
|
-
<${directoryLayout.tag} dataJson='${escapeAttributeValue(JSON.stringify(data))}' ${directoryLayout.sourceAttribute}="${directoryLayout.path}">
|
|
519
|
-
<ul class="sidebar-list">${items}</ul>
|
|
520
|
-
</${directoryLayout.tag}>
|
|
521
|
-
</li>`;
|
|
522
|
-
}
|
|
523
|
-
function renderLink(link, { linkLayout, customizations, icons }) {
|
|
524
|
-
const customization = customizations[link.realPath];
|
|
525
|
-
const data = {
|
|
526
|
-
title: customization?.label ?? link.title,
|
|
527
|
-
fullTitle: link.pageTitle ?? link.title,
|
|
528
|
-
href: encodeURIComponent(link.filePath),
|
|
529
|
-
...customization?.data,
|
|
530
|
-
iconSrc: iconSource(icons, customization?.data?.icon)
|
|
531
|
-
};
|
|
532
|
-
return `<li class="sidebar-list-item">
|
|
533
|
-
<${linkLayout.tag} dataJson='${escapeAttributeValue(JSON.stringify(data))}' ${linkLayout.sourceAttribute}="${linkLayout.path}"></${linkLayout.tag}>
|
|
534
|
-
</li>`;
|
|
535
|
-
}
|
|
536
|
-
function renderTree(items, options) {
|
|
537
|
-
let tree = "";
|
|
538
|
-
for (const item of items) {
|
|
539
|
-
if (isItem(item)) {
|
|
540
|
-
tree += renderLink(item, options);
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
tree += renderDirectory(item, renderTree(item.items, options), options);
|
|
544
|
-
}
|
|
545
|
-
return tree;
|
|
546
|
-
}
|
|
547
|
-
//#endregion
|
|
548
496
|
//#region plugins/content-plugin/generate-tree.ts
|
|
549
497
|
function getOrCreateDirectory(tree, title, realPath, collapsed) {
|
|
550
498
|
let directory = tree.find((entry) => "items" in entry && entry.title === title);
|
|
@@ -610,7 +558,7 @@ function generateTree(options) {
|
|
|
610
558
|
const fileNameWithoutExtension = path.parse(fileName).name;
|
|
611
559
|
const title = fileNameWithoutExtension === "index" && directories.length === 0 ? "Home" : normalizeTitle(fileNameWithoutExtension);
|
|
612
560
|
const pathAsLink = fileNameWithoutExtension === "index" ? path.dirname(file) : path.join(path.dirname(file), fileNameWithoutExtension);
|
|
613
|
-
const href =
|
|
561
|
+
const href = this.resolver.aliasPath(pathAsLink) + "/";
|
|
614
562
|
currentTree.push({
|
|
615
563
|
title,
|
|
616
564
|
filePath: href,
|
|
@@ -620,48 +568,126 @@ function generateTree(options) {
|
|
|
620
568
|
}
|
|
621
569
|
applyOrdering(autoGenerated, options.customizations);
|
|
622
570
|
return {
|
|
623
|
-
tree:
|
|
571
|
+
tree: autoGenerated,
|
|
624
572
|
notPublished
|
|
625
573
|
};
|
|
626
574
|
}
|
|
627
575
|
//#endregion
|
|
576
|
+
//#region plugins/content-plugin/render-tree.ts
|
|
577
|
+
/** `<icon>` source for a node, `undefined` when the project has no icons directory or the node no icon. */
|
|
578
|
+
function iconSource(icons, icon) {
|
|
579
|
+
if (!icons || !icon) return;
|
|
580
|
+
return `${icons.replace(/\/$/, "")}/${icon}.icon.html`;
|
|
581
|
+
}
|
|
582
|
+
function renderDirectory(directory, items, { directoryLayout, customizations, icons }) {
|
|
583
|
+
const customization = customizations[directory.realPath];
|
|
584
|
+
const data = {
|
|
585
|
+
title: customization?.label ?? directory.title,
|
|
586
|
+
collapsed: customization?.collapsed ?? directory.collapsed,
|
|
587
|
+
...customization?.data,
|
|
588
|
+
iconSrc: iconSource(icons, customization?.data?.icon)
|
|
589
|
+
};
|
|
590
|
+
return `<li class="sidebar-list-item">
|
|
591
|
+
<${directoryLayout.tag} dataJson='${escapeAttributeValue(JSON.stringify(data))}' ${directoryLayout.sourceAttribute}="${directoryLayout.path}">
|
|
592
|
+
<ul class="sidebar-list">${items}</ul>
|
|
593
|
+
</${directoryLayout.tag}>
|
|
594
|
+
</li>`;
|
|
595
|
+
}
|
|
596
|
+
function renderLink(link, { linkLayout, customizations, icons }) {
|
|
597
|
+
const customization = customizations[link.realPath];
|
|
598
|
+
const data = {
|
|
599
|
+
title: customization?.label ?? link.title,
|
|
600
|
+
fullTitle: link.pageTitle ?? link.title,
|
|
601
|
+
href: encodeURIComponent(link.filePath),
|
|
602
|
+
...customization?.data,
|
|
603
|
+
iconSrc: iconSource(icons, customization?.data?.icon)
|
|
604
|
+
};
|
|
605
|
+
return `<li class="sidebar-list-item">
|
|
606
|
+
<${linkLayout.tag} dataJson='${escapeAttributeValue(JSON.stringify(data))}' ${linkLayout.sourceAttribute}="${linkLayout.path}"></${linkLayout.tag}>
|
|
607
|
+
</li>`;
|
|
608
|
+
}
|
|
609
|
+
function renderTree(items, options) {
|
|
610
|
+
let tree = "";
|
|
611
|
+
for (const item of items) {
|
|
612
|
+
if (isItem(item)) {
|
|
613
|
+
tree += renderLink(item, options);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
tree += renderDirectory(item, renderTree(item.items, options), options);
|
|
617
|
+
}
|
|
618
|
+
return tree;
|
|
619
|
+
}
|
|
620
|
+
//#endregion
|
|
628
621
|
//#region plugins/content-plugin/content-plugin.ts
|
|
629
|
-
/**
|
|
630
|
-
* Turns an alias, a package specifier, or an absolute path into the path the build works with: relative to the project root when
|
|
631
|
-
* the file lives inside it, absolute otherwise.
|
|
632
|
-
*/
|
|
622
|
+
/** Turns an alias, a package specifier, or an absolute path into a path relative to the project root. */
|
|
633
623
|
function locate(source) {
|
|
634
624
|
const aliased = this.resolver.resolveAlias(source);
|
|
635
625
|
if (aliased) return aliased;
|
|
636
626
|
const resolved = path.isAbsolute(source) ? source : this.resolver.resolve(source, path.join(this.root, "index.html"))?.path;
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
627
|
+
return resolved ? path.relative(this.root, resolved) : source;
|
|
628
|
+
}
|
|
629
|
+
/** The layout's empty `<slot name="sidebar">`, with the attributes around the name captured for the `slot` it forwards to. */
|
|
630
|
+
const slotRegex = /<slot\b([^>]*)\bname\s*=\s*["']sidebar["']([^>]*?)(\/>|>\s*<\/slot\s*>)/g;
|
|
631
|
+
/** The tree in place of the slot, sent to the same slot of the part below when the slot was forwarding to one. */
|
|
632
|
+
function fillSlot(content, tree) {
|
|
633
|
+
return content.replace(slotRegex, (_, before, after) => {
|
|
634
|
+
const target = /\bslot\s*=\s*["']([^"']*)["']/.exec(`${before} ${after}`)?.[1];
|
|
635
|
+
return `<ul class="sidebar-list"${target ? ` slot="${target}"` : ""}>${tree}</ul>`;
|
|
636
|
+
});
|
|
640
637
|
}
|
|
641
638
|
function contentPlugin(options) {
|
|
642
639
|
const prepared = prepareOptions(options);
|
|
643
|
-
const slotRegex = new RegExp(String.raw`<slot\s+name\s*=\s*['"]sidebar['"]\s*(\/>|>\s*<\/\s*slot\s*>)`, "g");
|
|
644
640
|
let tree = "";
|
|
645
641
|
let notPublished = /* @__PURE__ */ new Set();
|
|
646
|
-
|
|
642
|
+
/**
|
|
643
|
+
* The layouts this instance serves through `read`, each the file it is read from with the tree in its `sidebar` slot.
|
|
644
|
+
*
|
|
645
|
+
* @key served path, relative to the root
|
|
646
|
+
* @value the layout file it is a copy of
|
|
647
|
+
*/
|
|
648
|
+
const served = /* @__PURE__ */ new Map();
|
|
649
|
+
const isInScope = (relativePath, root) => {
|
|
650
|
+
return path.isPathMatch(relativePath, {
|
|
651
|
+
include: prepared.include,
|
|
652
|
+
ignore: prepared.exclude,
|
|
653
|
+
root
|
|
654
|
+
});
|
|
655
|
+
};
|
|
656
|
+
const generate = (app) => {
|
|
657
|
+
const result = generateTree.call(app, prepared);
|
|
658
|
+
return {
|
|
659
|
+
tree: renderTree(result.tree, prepared),
|
|
660
|
+
notPublished: result.notPublished
|
|
661
|
+
};
|
|
662
|
+
};
|
|
647
663
|
return {
|
|
648
664
|
name: "content-plugin",
|
|
649
665
|
setup() {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
666
|
+
const instance = (this.pluginData.contentPluginInstances ?? 0) + 1;
|
|
667
|
+
this.pluginData.contentPluginInstances = instance;
|
|
668
|
+
for (const key in prepared.layouts) {
|
|
669
|
+
const original = locate.call(this, prepared.layouts[key]);
|
|
670
|
+
if (instance === 1) {
|
|
671
|
+
served.set(original, original);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const [name, ...rest] = path.basename(original).split(".");
|
|
675
|
+
const copy = path.join(path.dirname(original), [
|
|
676
|
+
name,
|
|
677
|
+
`content-${instance}`,
|
|
678
|
+
...rest
|
|
679
|
+
].join("."));
|
|
680
|
+
served.set(copy, original);
|
|
681
|
+
this.resolver.files.add(path.resolve(this.root, copy));
|
|
682
|
+
prepared.layouts[key] = copy;
|
|
683
|
+
}
|
|
658
684
|
for (const pathKey in prepared.customizations) {
|
|
659
685
|
const resolvedPath = this.resolver.resolveAlias(pathKey);
|
|
660
686
|
if (!resolvedPath) continue;
|
|
661
687
|
prepared.customizations[resolvedPath] = prepared.customizations[pathKey];
|
|
662
688
|
delete prepared.customizations[pathKey];
|
|
663
689
|
}
|
|
664
|
-
({tree, notPublished} =
|
|
690
|
+
({tree, notPublished} = generate(this));
|
|
665
691
|
for (const file of notPublished) {
|
|
666
692
|
this.entryPoints.delete(file);
|
|
667
693
|
this.entryPoints.delete(`./${file}`);
|
|
@@ -672,37 +698,30 @@ function contentPlugin(options) {
|
|
|
672
698
|
},
|
|
673
699
|
read(absolutePath) {
|
|
674
700
|
const relativePath = path.relative(this.root, absolutePath);
|
|
675
|
-
|
|
676
|
-
if (path.
|
|
677
|
-
if (
|
|
678
|
-
include: prepared.include,
|
|
679
|
-
ignore: prepared.exclude,
|
|
680
|
-
root: this.root
|
|
681
|
-
})) return;
|
|
701
|
+
const original = served.get(relativePath);
|
|
702
|
+
if (original) return fillSlot(readFileSync(path.resolve(this.root, original), "utf8"), tree);
|
|
703
|
+
if (path.extname(relativePath) !== ".md" || !isInScope(relativePath, this.root)) return;
|
|
682
704
|
const pageContent = readFileSync(absolutePath, "utf8");
|
|
683
705
|
const frontMatterBlock = pageContent.match(/^---\n(.*?)\n---/s);
|
|
684
706
|
const frontMatter = {
|
|
685
707
|
layout: "default",
|
|
686
708
|
...frontMatterBlock ? yaml.parse(frontMatterBlock[1]) : {}
|
|
687
709
|
};
|
|
688
|
-
const
|
|
689
|
-
frontMatter.layout = path.relative(path.dirname(
|
|
710
|
+
const layout = prepared.layouts[frontMatter.layout] ?? prepared.layouts.default;
|
|
711
|
+
frontMatter.layout = served.has(layout) ? path.relative(path.dirname(absolutePath), path.resolve(this.root, layout)) : layout;
|
|
690
712
|
for (const key in frontMatter) if (key === "publish" || key.startsWith("sidebar.")) delete frontMatter[key];
|
|
691
713
|
const body = frontMatterBlock ? pageContent.slice(frontMatterBlock[0].length) : pageContent;
|
|
692
714
|
return `---\n${yaml.stringify(frontMatter)}\n---\n${body}`;
|
|
693
715
|
},
|
|
694
716
|
onFileEvent(_, id) {
|
|
695
|
-
if (
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
root: this.root
|
|
699
|
-
})) return;
|
|
700
|
-
const result = generateTree.call(this, prepared);
|
|
717
|
+
for (const [copy, original] of served) if (original === id && copy !== id) this.watcher?.emit("change", copy);
|
|
718
|
+
if (!isInScope(id, this.root)) return;
|
|
719
|
+
const result = generate(this);
|
|
701
720
|
notPublished = result.notPublished;
|
|
702
721
|
if (tree === result.tree) return;
|
|
703
722
|
this.log.info("Re-generating sidebar tree");
|
|
704
723
|
tree = result.tree;
|
|
705
|
-
this.watcher?.emit("change",
|
|
724
|
+
for (const layout of served.keys()) this.watcher?.emit("change", layout);
|
|
706
725
|
}
|
|
707
726
|
};
|
|
708
727
|
}
|
|
@@ -755,14 +774,14 @@ const markdownLink = /\[([^\]]+)\]\([^)]*\)/g;
|
|
|
755
774
|
* needs no link-following. Nothing is curated by hand.
|
|
756
775
|
*/
|
|
757
776
|
function llmsPlugin(options = {}) {
|
|
758
|
-
const { siteUrl, title = "Documentation", summary } = options;
|
|
777
|
+
const { siteUrl, title = "Documentation", summary, include = ["pages/**/*.md"], exclude = [], output = "llms" } = options;
|
|
759
778
|
return {
|
|
760
779
|
name: "llms",
|
|
761
780
|
async postBuild() {
|
|
762
781
|
if (!siteUrl) return;
|
|
763
782
|
const origin = siteUrl.replace(/\/+$/, "");
|
|
764
783
|
const isBuilt = (file) => Boolean(this.findMetadata({ id: file }) ?? this.findMetadata({ id: `./${file}` }));
|
|
765
|
-
const entries = await collectPages(this.root, isBuilt);
|
|
784
|
+
const entries = await collectPages(this.root, isBuilt, include, exclude);
|
|
766
785
|
if (entries.length === 0) return;
|
|
767
786
|
for (const entry of entries) {
|
|
768
787
|
const target = join(this.outdir, entry.route, "index.md");
|
|
@@ -787,24 +806,30 @@ function llmsPlugin(options = {}) {
|
|
|
787
806
|
manifest.push(`- [${entry.title}](${url})${entry.description ? `: ${entry.description}` : ""}`);
|
|
788
807
|
}
|
|
789
808
|
}
|
|
790
|
-
|
|
809
|
+
const manifestFile = `${output}.txt`;
|
|
810
|
+
const fullFile = `${output}-full.txt`;
|
|
811
|
+
await mkdir(dirname(join(this.outdir, manifestFile)), { recursive: true });
|
|
812
|
+
await writeFile(join(this.outdir, manifestFile), manifest.join("\n") + "\n", "utf8");
|
|
791
813
|
const full = [[
|
|
792
814
|
...heading,
|
|
793
815
|
"",
|
|
794
|
-
`> Every document from ${origin}
|
|
816
|
+
`> Every document from ${origin}/${manifestFile} inlined in full, so no links need following.`
|
|
795
817
|
].join("\n"), ...entries.map((entry) => {
|
|
796
818
|
const source = `${origin}/${entry.route}${entry.route ? "/" : ""}index.md`;
|
|
797
819
|
return `${demoteHeadings(entry.document, entry.title)}\n\nSource: ${source}`;
|
|
798
820
|
})].join("\n\n---\n\n") + "\n";
|
|
799
|
-
await writeFile(join(this.outdir,
|
|
821
|
+
await writeFile(join(this.outdir, fullFile), rewriteAliases(full, origin), "utf8");
|
|
800
822
|
const kilobytes = Math.round(Buffer.byteLength(full) / 1024);
|
|
801
|
-
this.log.success(`Wrote
|
|
823
|
+
this.log.success(`Wrote ${manifestFile} (${entries.length} links) and ${fullFile} (${kilobytes} KB).`);
|
|
802
824
|
}
|
|
803
825
|
};
|
|
804
826
|
}
|
|
805
827
|
/** Pages the build produced, so anything the markdown plugin dropped (`publish: false`) stays out of the llms files too. */
|
|
806
|
-
async function collectPages(root, isBuilt) {
|
|
807
|
-
const files = globSync(
|
|
828
|
+
async function collectPages(root, isBuilt, include, exclude) {
|
|
829
|
+
const files = globSync(include, {
|
|
830
|
+
cwd: root,
|
|
831
|
+
exclude
|
|
832
|
+
}).map((file) => file.replaceAll("\\", "/")).filter((file) => isBuilt(file)).toSorted(byDepthThenPath);
|
|
808
833
|
const entries = [];
|
|
809
834
|
for (const file of files) {
|
|
810
835
|
const document = clean(await readFile(join(root, file), "utf8"));
|
|
@@ -890,6 +915,13 @@ function byDepthThenPath(a, b) {
|
|
|
890
915
|
}
|
|
891
916
|
//#endregion
|
|
892
917
|
//#region plugins/mermaid-fence.ts
|
|
918
|
+
/** The sole `<code>` child of a `<pre>`, ignoring the whitespace some serialisers insert, or nothing when the shape differs. */
|
|
919
|
+
function onlyCodeChild(node) {
|
|
920
|
+
const children = node.children.filter((child) => !(child.type === "text" && child.value.trim() === ""));
|
|
921
|
+
const [child] = children;
|
|
922
|
+
if (children.length !== 1 || child.type !== "element" || child.tagName !== "code") return;
|
|
923
|
+
return child;
|
|
924
|
+
}
|
|
893
925
|
/**
|
|
894
926
|
* Rehype plugin: converts fenced mermaid code blocks into <pre class="mermaid"> elements.
|
|
895
927
|
*
|
|
@@ -900,60 +932,21 @@ function byDepthThenPath(a, b) {
|
|
|
900
932
|
const rehypeMermaidFence = () => {
|
|
901
933
|
return (tree) => {
|
|
902
934
|
visit(tree, "element", (node) => {
|
|
903
|
-
if (
|
|
904
|
-
const
|
|
905
|
-
if (
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
935
|
+
if (node.tagName !== "pre") return;
|
|
936
|
+
const code = onlyCodeChild(node);
|
|
937
|
+
if (!code) return;
|
|
938
|
+
const className = code.properties?.className;
|
|
939
|
+
if (!(Array.isArray(className) ? className : [className]).some((name) => String(name) === "language-mermaid")) return;
|
|
940
|
+
const text = code.children.filter((child) => child.type === "text");
|
|
941
|
+
if (text.length === 0) return;
|
|
909
942
|
node.properties = { className: ["mermaid"] };
|
|
910
943
|
node.children = [{
|
|
911
944
|
type: "text",
|
|
912
|
-
value:
|
|
945
|
+
value: text.map((child) => child.value).join("")
|
|
913
946
|
}];
|
|
914
947
|
});
|
|
915
948
|
};
|
|
916
949
|
};
|
|
917
|
-
function isPreElement(node) {
|
|
918
|
-
return node.tagName === "pre";
|
|
919
|
-
}
|
|
920
|
-
/** Returns the sole <code> child of a <pre>, or null if the structure doesn't match (e.g. multiple children, wrong tag). */
|
|
921
|
-
function findSingleCodeChild(preNode) {
|
|
922
|
-
const nonWhitespaceChildren = [];
|
|
923
|
-
for (const child of preNode.children) {
|
|
924
|
-
if (child.type === "text" && child.value.trim() === "") continue;
|
|
925
|
-
nonWhitespaceChildren.push(child);
|
|
926
|
-
}
|
|
927
|
-
if (nonWhitespaceChildren.length !== 1) return null;
|
|
928
|
-
const [onlyChild] = nonWhitespaceChildren;
|
|
929
|
-
if (onlyChild.type !== "element") return null;
|
|
930
|
-
if (onlyChild.tagName !== "code") return null;
|
|
931
|
-
return onlyChild;
|
|
932
|
-
}
|
|
933
|
-
/**
|
|
934
|
-
* Returns true when the element's className list contains "language-mermaid". Handles both string and array forms that hast
|
|
935
|
-
* allows.
|
|
936
|
-
*/
|
|
937
|
-
function hasMermaidLanguageClass(codeElement) {
|
|
938
|
-
const rawClassName = codeElement.properties?.className;
|
|
939
|
-
if (rawClassName === void 0 || rawClassName === null) return false;
|
|
940
|
-
const classNames = Array.isArray(rawClassName) ? rawClassName : [String(rawClassName)];
|
|
941
|
-
for (const className of classNames) if (className === "language-mermaid") return true;
|
|
942
|
-
return false;
|
|
943
|
-
}
|
|
944
|
-
/**
|
|
945
|
-
* Concatenates all text-node descendants of an element. Returns null when there are no text nodes (empty code block is valid →
|
|
946
|
-
* ""). A <code> block produced by remark-rehype always has exactly one Text child, but we handle edge cases defensively.
|
|
947
|
-
*/
|
|
948
|
-
function extractTextContent(element) {
|
|
949
|
-
let content = "";
|
|
950
|
-
let found = false;
|
|
951
|
-
for (const child of element.children) if (child.type === "text") {
|
|
952
|
-
content += child.value;
|
|
953
|
-
found = true;
|
|
954
|
-
}
|
|
955
|
-
return found ? content : null;
|
|
956
|
-
}
|
|
957
950
|
//#endregion
|
|
958
951
|
//#region plugins/obsidian-comments.ts
|
|
959
952
|
const commentPattern = /(?<!\\)%%[\s\S]*?(?<!\\)%%/g;
|
|
@@ -1108,18 +1101,17 @@ function pagefindPlugin({ glob = "**/*.html" } = {}) {
|
|
|
1108
1101
|
//#region plugins/wrap-table.ts
|
|
1109
1102
|
function wrapTables() {
|
|
1110
1103
|
return (tree) => {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
const wrapper = {
|
|
1104
|
+
visit(tree, "element", (node, index, parent) => {
|
|
1105
|
+
if (node.tagName !== "table" || !parent || typeof index !== "number") return;
|
|
1106
|
+
parent.children[index] = {
|
|
1115
1107
|
type: "element",
|
|
1116
1108
|
tagName: "div",
|
|
1117
1109
|
properties: { class: "table-wrapper" },
|
|
1118
1110
|
children: [node]
|
|
1119
1111
|
};
|
|
1120
|
-
|
|
1112
|
+
return "skip";
|
|
1121
1113
|
});
|
|
1122
1114
|
};
|
|
1123
1115
|
}
|
|
1124
1116
|
//#endregion
|
|
1125
|
-
export {
|
|
1117
|
+
export { PersistentCache, cachedRehypeExpressiveCode, contentPlugin, ecCachePlugin, ecCacheSalt, htmlCodeBlockPlugin, htmlWithTypeScriptScripts, katexFontsPlugin, llmsPlugin, normalizeTitle, pagefindPlugin, pluginCollapsible, rehypeMermaidFence, remarkObsidianComments, remarkObsidianTags, sha1, wrapTables };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@staticbolt/docs",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.4",
|
|
4
4
|
"description": "Documentation theme for staticbolt: layout parts, styles, and plugins shared by every docs site.",
|
|
5
5
|
"author": "Ahmed ALABSI",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,7 +66,6 @@
|
|
|
66
66
|
"@pagefind/component-ui": "^1.5.2",
|
|
67
67
|
"@xt0rted/expressive-code-file-icons": "^1.0.0",
|
|
68
68
|
"expressive-code": "^0.42.0",
|
|
69
|
-
"hast-util-select": "^6.0.2",
|
|
70
69
|
"hast-util-to-html": "^9.0.5",
|
|
71
70
|
"pagefind": "^1.5.2",
|
|
72
71
|
"rehype-expressive-code": "^0.42.0",
|
|
@@ -76,7 +75,14 @@
|
|
|
76
75
|
},
|
|
77
76
|
"overrides": {
|
|
78
77
|
"expressive-code": "^0.42.0",
|
|
79
|
-
"@expressive-code/core": "^0.42.0"
|
|
78
|
+
"@expressive-code/core": "^0.42.0",
|
|
79
|
+
"d3-array": "^3.2.4",
|
|
80
|
+
"d3-shape": "^3.2.0",
|
|
81
|
+
"d3-path": "^3.1.0",
|
|
82
|
+
"internmap": "^2.0.3",
|
|
83
|
+
"marked": "^18.0.11",
|
|
84
|
+
"cose-base": "^2.2.0",
|
|
85
|
+
"layout-base": "^2.0.1"
|
|
80
86
|
},
|
|
81
87
|
"devDependencies": {
|
|
82
88
|
"@eslint/js": "^10.0.1",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
@attr maxHeading - Highest heading level in the table of contents. Default 3.
|
|
6
6
|
@attr mermaid - Present to render mermaid fences. Needs the mermaid package.
|
|
7
7
|
@slot - The page content.
|
|
8
|
+
@slot sidebar - The sidebar tree, filled in by contentPlugin through the layout.
|
|
8
9
|
@slot logo - Rendered before the title in the header link.
|
|
9
10
|
@slot icons - Links rendered in the header and the sidebar footer.
|
|
10
11
|
-->
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
|
|
22
23
|
<div class="markdown-layout">
|
|
23
24
|
<part src="@staticbolt/docs/parts/sidebar/sidebar.part.html">
|
|
25
|
+
<slot name="sidebar"></slot>
|
|
24
26
|
<slot name="icons" slot="icons"></slot>
|
|
25
27
|
</part>
|
|
26
28
|
|