@moku-labs/web 1.8.2 → 1.9.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/dist/browser.d.mts +40 -1
- package/dist/index.cjs +183 -10
- package/dist/index.d.cts +40 -1
- package/dist/index.d.mts +40 -1
- package/dist/index.mjs +183 -10
- package/package.json +8 -1
package/dist/browser.d.mts
CHANGED
|
@@ -1920,7 +1920,7 @@ type DataProvider = {
|
|
|
1920
1920
|
*/
|
|
1921
1921
|
declare const dataPlugin: import("@moku-labs/core").PluginInstance<"data", DataConfig, DataState, DataProvider, {}> & Record<never, never>;
|
|
1922
1922
|
declare namespace types_d_exports {
|
|
1923
|
-
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, LoadAllOptions, State };
|
|
1923
|
+
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, LoadAllOptions, MermaidDiagramOptions, State };
|
|
1924
1924
|
}
|
|
1925
1925
|
/**
|
|
1926
1926
|
* YAML frontmatter parsed from each article file.
|
|
@@ -2034,6 +2034,36 @@ interface ContentProvider {
|
|
|
2034
2034
|
*/
|
|
2035
2035
|
invalidate?(paths: readonly string[]): void;
|
|
2036
2036
|
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Options for build-time Mermaid diagram rendering (the `mermaid` key of
|
|
2039
|
+
* {@link FileSystemContentOptions}). Rendering is delegated to the OPTIONAL
|
|
2040
|
+
* peer dependency `mermaid-isomorphic`, so the config stays loosely typed —
|
|
2041
|
+
* its types are never imported here.
|
|
2042
|
+
*
|
|
2043
|
+
* @example
|
|
2044
|
+
* ```ts
|
|
2045
|
+
* fileSystemContent({ contentDir: "./content", trustedContent: true, mermaid: { mermaidConfig: { theme: "dark" } } });
|
|
2046
|
+
* ```
|
|
2047
|
+
*/
|
|
2048
|
+
type MermaidDiagramOptions = {
|
|
2049
|
+
/**
|
|
2050
|
+
* Mermaid configuration passed straight through to mermaid-isomorphic's
|
|
2051
|
+
* render call (e.g. `{ theme: "dark" }`). Loosely typed as a plain record
|
|
2052
|
+
* because the dependency is optional.
|
|
2053
|
+
*/
|
|
2054
|
+
mermaidConfig?: Record<string, unknown>;
|
|
2055
|
+
/**
|
|
2056
|
+
* TEST-ONLY seam: replaces the real mermaid-isomorphic batch renderer so
|
|
2057
|
+
* unit tests stay deterministic with no headless browser. Receives every
|
|
2058
|
+
* mermaid fence source of one document in order and must resolve to exactly
|
|
2059
|
+
* one SVG string per source. Never set this in an app.
|
|
2060
|
+
*
|
|
2061
|
+
* @param sources - Every mermaid fence source of one document, in order.
|
|
2062
|
+
* @param mermaidConfig - The configured mermaid pass-through config, if any.
|
|
2063
|
+
* @returns One SVG string per source, in order.
|
|
2064
|
+
*/
|
|
2065
|
+
renderDiagrams?: (sources: readonly string[], mermaidConfig?: Record<string, unknown>) => Promise<readonly string[]>;
|
|
2066
|
+
};
|
|
2037
2067
|
/**
|
|
2038
2068
|
* Options for the node filesystem provider {@link ContentProvider} `fileSystemContent`.
|
|
2039
2069
|
* These are the markdown-pipeline + source concerns that used to live on the content
|
|
@@ -2059,6 +2089,15 @@ type FileSystemContentOptions = {
|
|
|
2059
2089
|
*/
|
|
2060
2090
|
shikiTheme?: BundledTheme | ThemeRegistrationAny; /** Author applied to articles whose frontmatter omits author. Defaults to undefined. */
|
|
2061
2091
|
defaultAuthor?: string;
|
|
2092
|
+
/**
|
|
2093
|
+
* Build-time Mermaid diagrams: render fenced `mermaid` code blocks to static
|
|
2094
|
+
* inline SVG during the build (zero client-side JS). `true` enables with
|
|
2095
|
+
* defaults; an object passes {@link MermaidDiagramOptions}. Requires
|
|
2096
|
+
* `trustedContent: true` (the raw inline SVG would be stripped by the
|
|
2097
|
+
* sanitize pass) and the OPTIONAL peer dependency `mermaid-isomorphic`
|
|
2098
|
+
* (plus playwright with an installed browser). Defaults to disabled.
|
|
2099
|
+
*/
|
|
2100
|
+
mermaid?: boolean | MermaidDiagramOptions;
|
|
2062
2101
|
};
|
|
2063
2102
|
/**
|
|
2064
2103
|
* Internal mutable state of the filesystem provider: the lazy unified processor and
|
package/dist/index.cjs
CHANGED
|
@@ -1580,6 +1580,23 @@ function createContentState(_ctx) {
|
|
|
1580
1580
|
function validateContentConfig(config) {
|
|
1581
1581
|
if (!Array.isArray(config.providers) || config.providers.length === 0) throw new Error("[web] content: no provider composed.\n Add fileSystemContent(...) to pluginConfigs.content.providers.");
|
|
1582
1582
|
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Validates the `fileSystemContent` provider options (fail-fast at provider
|
|
1585
|
+
* construction). Throws when `mermaid` is enabled without `trustedContent: true`:
|
|
1586
|
+
* mermaid output is raw inline SVG, which the sanitize pass (the untrusted-content
|
|
1587
|
+
* XSS boundary) would strip — so the combination can never work. Errors use the
|
|
1588
|
+
* `[web]` prefix.
|
|
1589
|
+
*
|
|
1590
|
+
* @param options - The provider options to validate.
|
|
1591
|
+
* @throws {Error} If `mermaid` is enabled while `trustedContent` is not `true`.
|
|
1592
|
+
* @example
|
|
1593
|
+
* ```ts
|
|
1594
|
+
* validateFileSystemContentOptions({ contentDir: "./content", trustedContent: true, mermaid: true });
|
|
1595
|
+
* ```
|
|
1596
|
+
*/
|
|
1597
|
+
function validateFileSystemContentOptions(options) {
|
|
1598
|
+
if (Boolean(options.mermaid) && options.trustedContent !== true) throw new Error("[web] content: `mermaid` requires `trustedContent: true`.\n Mermaid diagrams render to raw inline SVG, which the sanitize pass would strip.\n Set trustedContent: true ONLY for fully author-controlled Markdown.");
|
|
1599
|
+
}
|
|
1583
1600
|
//#endregion
|
|
1584
1601
|
//#region src/plugins/content/index.ts
|
|
1585
1602
|
/**
|
|
@@ -11569,6 +11586,154 @@ function parseFrontmatter(raw, config) {
|
|
|
11569
11586
|
};
|
|
11570
11587
|
}
|
|
11571
11588
|
//#endregion
|
|
11589
|
+
//#region src/plugins/content/pipeline/mermaid.ts
|
|
11590
|
+
/** CSS class on the `<figure>` wrapper around each rendered diagram. */
|
|
11591
|
+
const MERMAID_FIGURE_CLASS = "mermaid-diagram";
|
|
11592
|
+
/**
|
|
11593
|
+
* Cached renderer promise — `createMermaidRenderer()` is called ONCE per process
|
|
11594
|
+
* and shared by every document (the underlying headless browser is expensive).
|
|
11595
|
+
*/
|
|
11596
|
+
let cachedRendererPromise;
|
|
11597
|
+
/**
|
|
11598
|
+
* Lazily import `mermaid-isomorphic` and create its batched renderer. The import
|
|
11599
|
+
* happens HERE (never at module load) so the optional dependency is only touched
|
|
11600
|
+
* when a document actually contains a mermaid fence. A failed import is wrapped
|
|
11601
|
+
* in an actionable error naming the missing package.
|
|
11602
|
+
*
|
|
11603
|
+
* @param importModule - Import thunk for the package; injectable so tests can
|
|
11604
|
+
* exercise both outcomes without the real dependency. Defaults to the real
|
|
11605
|
+
* dynamic `import("mermaid-isomorphic")`.
|
|
11606
|
+
* @returns The batched mermaid renderer.
|
|
11607
|
+
* @throws {Error} When the optional dependency cannot be loaded.
|
|
11608
|
+
* @example
|
|
11609
|
+
* ```ts
|
|
11610
|
+
* const renderer = await loadMermaidRenderer();
|
|
11611
|
+
* const results = await renderer(["graph TD; A-->B"]);
|
|
11612
|
+
* ```
|
|
11613
|
+
*/
|
|
11614
|
+
async function loadMermaidRenderer(importModule = () => import("mermaid-isomorphic")) {
|
|
11615
|
+
let moduleExports;
|
|
11616
|
+
try {
|
|
11617
|
+
moduleExports = await importModule();
|
|
11618
|
+
} catch (error) {
|
|
11619
|
+
throw new Error("[web] content: `mermaid` is enabled but the optional dependency \"mermaid-isomorphic\" could not be loaded.\n Install it (plus playwright and a browser):\n bun add -d mermaid-isomorphic playwright && bunx playwright install chromium", { cause: error });
|
|
11620
|
+
}
|
|
11621
|
+
return moduleExports.createMermaidRenderer();
|
|
11622
|
+
}
|
|
11623
|
+
/**
|
|
11624
|
+
* Unwrap mermaid-isomorphic's settled results into plain SVG strings, failing
|
|
11625
|
+
* the build on the first rejected diagram. The error quotes the diagram's first
|
|
11626
|
+
* line so the author can locate the broken fence.
|
|
11627
|
+
*
|
|
11628
|
+
* @param sources - The diagram sources, in the order they were rendered.
|
|
11629
|
+
* @param results - The settled render results (one per source).
|
|
11630
|
+
* @returns One SVG string per source, in order.
|
|
11631
|
+
* @throws {Error} When any diagram failed to render.
|
|
11632
|
+
* @example
|
|
11633
|
+
* ```ts
|
|
11634
|
+
* const svgs = unwrapMermaidResults(["graph TD; A-->B"], results);
|
|
11635
|
+
* ```
|
|
11636
|
+
*/
|
|
11637
|
+
function unwrapMermaidResults(sources, results) {
|
|
11638
|
+
return results.map((result, index) => {
|
|
11639
|
+
if (result.status === "rejected") {
|
|
11640
|
+
const firstLine = (sources[index] ?? "").split("\n", 1)[0] ?? "";
|
|
11641
|
+
throw new Error(`[web] content: mermaid diagram failed to render (diagram starts with "${firstLine}"): ${String(result.reason)}`);
|
|
11642
|
+
}
|
|
11643
|
+
return result.value.svg;
|
|
11644
|
+
});
|
|
11645
|
+
}
|
|
11646
|
+
/**
|
|
11647
|
+
* The REAL render path: lazily load mermaid-isomorphic (cached once per
|
|
11648
|
+
* process), render every fence of the document in ONE batched call, and unwrap
|
|
11649
|
+
* the results. Replaced in unit tests by the `renderDiagrams` seam.
|
|
11650
|
+
*
|
|
11651
|
+
* @param sources - Every mermaid fence source of one document, in order.
|
|
11652
|
+
* @param mermaidConfig - Optional mermaid configuration forwarded to the render call.
|
|
11653
|
+
* @returns One SVG string per source, in order.
|
|
11654
|
+
* @example
|
|
11655
|
+
* ```ts
|
|
11656
|
+
* const svgs = await renderWithMermaidIsomorphic(["graph TD; A-->B"]);
|
|
11657
|
+
* ```
|
|
11658
|
+
*/
|
|
11659
|
+
async function renderWithMermaidIsomorphic(sources, mermaidConfig) {
|
|
11660
|
+
cachedRendererPromise ??= loadMermaidRenderer();
|
|
11661
|
+
return unwrapMermaidResults(sources, await (await cachedRendererPromise)([...sources], mermaidConfig ? { mermaidConfig } : void 0));
|
|
11662
|
+
}
|
|
11663
|
+
/**
|
|
11664
|
+
* Collect every fenced `mermaid` code block in the tree (with the parent/index
|
|
11665
|
+
* needed to replace it later), in document order.
|
|
11666
|
+
*
|
|
11667
|
+
* @param tree - The mdast tree to scan.
|
|
11668
|
+
* @returns The fence sites found.
|
|
11669
|
+
* @example
|
|
11670
|
+
* ```ts
|
|
11671
|
+
* const fences = collectMermaidFences(tree);
|
|
11672
|
+
* ```
|
|
11673
|
+
*/
|
|
11674
|
+
function collectMermaidFences(tree) {
|
|
11675
|
+
const fences = [];
|
|
11676
|
+
(0, unist_util_visit.visit)(tree, "code", (node, index, parent) => {
|
|
11677
|
+
if (node.lang !== "mermaid") return;
|
|
11678
|
+
if (parent === void 0 || index === void 0) return;
|
|
11679
|
+
fences.push({
|
|
11680
|
+
node,
|
|
11681
|
+
parent,
|
|
11682
|
+
index
|
|
11683
|
+
});
|
|
11684
|
+
});
|
|
11685
|
+
return fences;
|
|
11686
|
+
}
|
|
11687
|
+
/**
|
|
11688
|
+
* Normalize the provider's `mermaid` config value (`boolean | options`) to a
|
|
11689
|
+
* plain {@link MermaidDiagramOptions} object for the transform factory.
|
|
11690
|
+
*
|
|
11691
|
+
* @param mermaid - The raw `FileSystemContentOptions.mermaid` value (truthy).
|
|
11692
|
+
* @returns The options object (`{}` for the bare `true` form).
|
|
11693
|
+
* @example
|
|
11694
|
+
* ```ts
|
|
11695
|
+
* normalizeMermaidOptions(true); // {}
|
|
11696
|
+
* normalizeMermaidOptions({ mermaidConfig: { theme: "dark" } });
|
|
11697
|
+
* ```
|
|
11698
|
+
*/
|
|
11699
|
+
function normalizeMermaidOptions(mermaid) {
|
|
11700
|
+
return typeof mermaid === "boolean" ? {} : mermaid;
|
|
11701
|
+
}
|
|
11702
|
+
/**
|
|
11703
|
+
* Remark transform factory: replaces every fenced `mermaid` code block with a
|
|
11704
|
+
* `<figure class="mermaid-diagram">` raw-HTML node carrying the diagram as
|
|
11705
|
+
* static inline SVG, rendered at build time (zero client-side JS). Runs at the
|
|
11706
|
+
* mdast stage, BEFORE remark-rehype; the bridge's `allowDangerousHtml` plus the
|
|
11707
|
+
* framework's `rehype-raw` default carry the SVG into the output. Documents
|
|
11708
|
+
* without a mermaid fence return immediately — `mermaid-isomorphic` is never
|
|
11709
|
+
* imported on that path. A diagram that fails to render fails the build with
|
|
11710
|
+
* its first line quoted.
|
|
11711
|
+
*
|
|
11712
|
+
* @param options - Mermaid options: `mermaidConfig` pass-through + the
|
|
11713
|
+
* test-only `renderDiagrams` seam.
|
|
11714
|
+
* @returns An async mdast tree transformer.
|
|
11715
|
+
* @example
|
|
11716
|
+
* ```ts
|
|
11717
|
+
* unified().use(remarkMermaidDiagrams, { mermaidConfig: { theme: "dark" } });
|
|
11718
|
+
* ```
|
|
11719
|
+
*/
|
|
11720
|
+
function remarkMermaidDiagrams(options = {}) {
|
|
11721
|
+
return async (tree) => {
|
|
11722
|
+
const fences = collectMermaidFences(tree);
|
|
11723
|
+
if (fences.length === 0) return;
|
|
11724
|
+
const sources = fences.map((fence) => fence.node.value);
|
|
11725
|
+
const svgs = await (options.renderDiagrams ?? renderWithMermaidIsomorphic)(sources, options.mermaidConfig);
|
|
11726
|
+
if (svgs.length !== sources.length) throw new Error(`[web] content: mermaid renderer returned ${svgs.length} result(s) for ${sources.length} diagram(s).`);
|
|
11727
|
+
for (const [position, fence] of fences.entries()) {
|
|
11728
|
+
const html = {
|
|
11729
|
+
type: "html",
|
|
11730
|
+
value: `<figure class="${MERMAID_FIGURE_CLASS}">${svgs[position] ?? ""}</figure>`
|
|
11731
|
+
};
|
|
11732
|
+
fence.parent.children[fence.index] = html;
|
|
11733
|
+
}
|
|
11734
|
+
};
|
|
11735
|
+
}
|
|
11736
|
+
//#endregion
|
|
11572
11737
|
//#region src/plugins/content/pipeline/plugins.ts
|
|
11573
11738
|
/**
|
|
11574
11739
|
* Type guard for remark-directive nodes (container/leaf/text).
|
|
@@ -11704,25 +11869,31 @@ function sectionDividerPlugin() {
|
|
|
11704
11869
|
}
|
|
11705
11870
|
/**
|
|
11706
11871
|
* The hardcoded framework default remark (Markdown-AST) plugins, in order:
|
|
11707
|
-
* parse, frontmatter, gfm, directive, pull-quote,
|
|
11708
|
-
* (`remark-rehype` with `allowDangerousHtml`).
|
|
11709
|
-
*
|
|
11710
|
-
*
|
|
11872
|
+
* parse, frontmatter, gfm, directive, pull-quote, the OPT-IN mermaid transform,
|
|
11873
|
+
* then the mdast→hast bridge (`remark-rehype` with `allowDangerousHtml`).
|
|
11874
|
+
* Pull-quote and mermaid run on the mdast before the bridge — pull-quote so the
|
|
11875
|
+
* directive carries its `hName`/`hProperties`, mermaid so the fence is replaced
|
|
11876
|
+
* with raw SVG HTML before Shiki could ever claim the code block.
|
|
11877
|
+
*
|
|
11878
|
+
* @param config - Optional provider configuration; only `mermaid` is read here
|
|
11879
|
+
* (truthy enables the mermaid transform at its fixed mdast position).
|
|
11711
11880
|
* @returns The ordered default remark pluggables.
|
|
11712
11881
|
* @example
|
|
11713
11882
|
* ```ts
|
|
11714
11883
|
* const remark = defaultRemarkPlugins();
|
|
11715
11884
|
* ```
|
|
11716
11885
|
*/
|
|
11717
|
-
function defaultRemarkPlugins() {
|
|
11718
|
-
|
|
11886
|
+
function defaultRemarkPlugins(config) {
|
|
11887
|
+
const plugins = [
|
|
11719
11888
|
remark_parse.default,
|
|
11720
11889
|
remark_frontmatter.default,
|
|
11721
11890
|
remark_gfm.default,
|
|
11722
11891
|
remark_directive.default,
|
|
11723
|
-
pullQuotePlugin
|
|
11724
|
-
[remark_rehype.default, { allowDangerousHtml: true }]
|
|
11892
|
+
pullQuotePlugin
|
|
11725
11893
|
];
|
|
11894
|
+
if (config?.mermaid) plugins.push([remarkMermaidDiagrams, normalizeMermaidOptions(config.mermaid)]);
|
|
11895
|
+
plugins.push([remark_rehype.default, { allowDangerousHtml: true }]);
|
|
11896
|
+
return plugins;
|
|
11726
11897
|
}
|
|
11727
11898
|
/**
|
|
11728
11899
|
* The hardcoded framework default rehype (HTML-AST) plugins, in order:
|
|
@@ -11863,14 +12034,15 @@ function applyPluggable(processor, plugin) {
|
|
|
11863
12034
|
* replacing, the defaults).
|
|
11864
12035
|
*
|
|
11865
12036
|
* @param processor - The unified processor under construction (mutated in place).
|
|
11866
|
-
* @param config - Resolved plugin configuration (provides `extraRemarkPlugins
|
|
12037
|
+
* @param config - Resolved plugin configuration (provides `extraRemarkPlugins`,
|
|
12038
|
+
* and `mermaid` which the defaults read to enable the mdast mermaid transform).
|
|
11867
12039
|
* @example
|
|
11868
12040
|
* ```ts
|
|
11869
12041
|
* applyRemarkPlugins(processor, config);
|
|
11870
12042
|
* ```
|
|
11871
12043
|
*/
|
|
11872
12044
|
function applyRemarkPlugins(processor, config) {
|
|
11873
|
-
for (const plugin of defaultRemarkPlugins()) applyPluggable(processor, plugin);
|
|
12045
|
+
for (const plugin of defaultRemarkPlugins(config)) applyPluggable(processor, plugin);
|
|
11874
12046
|
for (const plugin of config.extraRemarkPlugins ?? []) applyPluggable(processor, plugin);
|
|
11875
12047
|
}
|
|
11876
12048
|
/**
|
|
@@ -12022,6 +12194,7 @@ async function discoverSlugs(dir) {
|
|
|
12022
12194
|
* ```
|
|
12023
12195
|
*/
|
|
12024
12196
|
function fileSystemContent(options) {
|
|
12197
|
+
validateFileSystemContentOptions(options);
|
|
12025
12198
|
const state = {
|
|
12026
12199
|
processor: null,
|
|
12027
12200
|
slugs: null,
|
package/dist/index.d.cts
CHANGED
|
@@ -2671,7 +2671,7 @@ type Api$1 = {
|
|
|
2671
2671
|
*/
|
|
2672
2672
|
declare const cliPlugin: import("@moku-labs/core").PluginInstance<"cli", Config$1, State$1, Api$1, {}> & Record<never, never>;
|
|
2673
2673
|
declare namespace types_d_exports$2 {
|
|
2674
|
-
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, LoadAllOptions, State };
|
|
2674
|
+
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, LoadAllOptions, MermaidDiagramOptions, State };
|
|
2675
2675
|
}
|
|
2676
2676
|
/**
|
|
2677
2677
|
* YAML frontmatter parsed from each article file.
|
|
@@ -2785,6 +2785,36 @@ interface ContentProvider {
|
|
|
2785
2785
|
*/
|
|
2786
2786
|
invalidate?(paths: readonly string[]): void;
|
|
2787
2787
|
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Options for build-time Mermaid diagram rendering (the `mermaid` key of
|
|
2790
|
+
* {@link FileSystemContentOptions}). Rendering is delegated to the OPTIONAL
|
|
2791
|
+
* peer dependency `mermaid-isomorphic`, so the config stays loosely typed —
|
|
2792
|
+
* its types are never imported here.
|
|
2793
|
+
*
|
|
2794
|
+
* @example
|
|
2795
|
+
* ```ts
|
|
2796
|
+
* fileSystemContent({ contentDir: "./content", trustedContent: true, mermaid: { mermaidConfig: { theme: "dark" } } });
|
|
2797
|
+
* ```
|
|
2798
|
+
*/
|
|
2799
|
+
type MermaidDiagramOptions = {
|
|
2800
|
+
/**
|
|
2801
|
+
* Mermaid configuration passed straight through to mermaid-isomorphic's
|
|
2802
|
+
* render call (e.g. `{ theme: "dark" }`). Loosely typed as a plain record
|
|
2803
|
+
* because the dependency is optional.
|
|
2804
|
+
*/
|
|
2805
|
+
mermaidConfig?: Record<string, unknown>;
|
|
2806
|
+
/**
|
|
2807
|
+
* TEST-ONLY seam: replaces the real mermaid-isomorphic batch renderer so
|
|
2808
|
+
* unit tests stay deterministic with no headless browser. Receives every
|
|
2809
|
+
* mermaid fence source of one document in order and must resolve to exactly
|
|
2810
|
+
* one SVG string per source. Never set this in an app.
|
|
2811
|
+
*
|
|
2812
|
+
* @param sources - Every mermaid fence source of one document, in order.
|
|
2813
|
+
* @param mermaidConfig - The configured mermaid pass-through config, if any.
|
|
2814
|
+
* @returns One SVG string per source, in order.
|
|
2815
|
+
*/
|
|
2816
|
+
renderDiagrams?: (sources: readonly string[], mermaidConfig?: Record<string, unknown>) => Promise<readonly string[]>;
|
|
2817
|
+
};
|
|
2788
2818
|
/**
|
|
2789
2819
|
* Options for the node filesystem provider {@link ContentProvider} `fileSystemContent`.
|
|
2790
2820
|
* These are the markdown-pipeline + source concerns that used to live on the content
|
|
@@ -2810,6 +2840,15 @@ type FileSystemContentOptions = {
|
|
|
2810
2840
|
*/
|
|
2811
2841
|
shikiTheme?: BundledTheme | ThemeRegistrationAny; /** Author applied to articles whose frontmatter omits author. Defaults to undefined. */
|
|
2812
2842
|
defaultAuthor?: string;
|
|
2843
|
+
/**
|
|
2844
|
+
* Build-time Mermaid diagrams: render fenced `mermaid` code blocks to static
|
|
2845
|
+
* inline SVG during the build (zero client-side JS). `true` enables with
|
|
2846
|
+
* defaults; an object passes {@link MermaidDiagramOptions}. Requires
|
|
2847
|
+
* `trustedContent: true` (the raw inline SVG would be stripped by the
|
|
2848
|
+
* sanitize pass) and the OPTIONAL peer dependency `mermaid-isomorphic`
|
|
2849
|
+
* (plus playwright with an installed browser). Defaults to disabled.
|
|
2850
|
+
*/
|
|
2851
|
+
mermaid?: boolean | MermaidDiagramOptions;
|
|
2813
2852
|
};
|
|
2814
2853
|
/**
|
|
2815
2854
|
* Internal mutable state of the filesystem provider: the lazy unified processor and
|
package/dist/index.d.mts
CHANGED
|
@@ -2671,7 +2671,7 @@ type Api$1 = {
|
|
|
2671
2671
|
*/
|
|
2672
2672
|
declare const cliPlugin: import("@moku-labs/core").PluginInstance<"cli", Config$1, State$1, Api$1, {}> & Record<never, never>;
|
|
2673
2673
|
declare namespace types_d_exports$2 {
|
|
2674
|
-
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, LoadAllOptions, State };
|
|
2674
|
+
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, LoadAllOptions, MermaidDiagramOptions, State };
|
|
2675
2675
|
}
|
|
2676
2676
|
/**
|
|
2677
2677
|
* YAML frontmatter parsed from each article file.
|
|
@@ -2785,6 +2785,36 @@ interface ContentProvider {
|
|
|
2785
2785
|
*/
|
|
2786
2786
|
invalidate?(paths: readonly string[]): void;
|
|
2787
2787
|
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Options for build-time Mermaid diagram rendering (the `mermaid` key of
|
|
2790
|
+
* {@link FileSystemContentOptions}). Rendering is delegated to the OPTIONAL
|
|
2791
|
+
* peer dependency `mermaid-isomorphic`, so the config stays loosely typed —
|
|
2792
|
+
* its types are never imported here.
|
|
2793
|
+
*
|
|
2794
|
+
* @example
|
|
2795
|
+
* ```ts
|
|
2796
|
+
* fileSystemContent({ contentDir: "./content", trustedContent: true, mermaid: { mermaidConfig: { theme: "dark" } } });
|
|
2797
|
+
* ```
|
|
2798
|
+
*/
|
|
2799
|
+
type MermaidDiagramOptions = {
|
|
2800
|
+
/**
|
|
2801
|
+
* Mermaid configuration passed straight through to mermaid-isomorphic's
|
|
2802
|
+
* render call (e.g. `{ theme: "dark" }`). Loosely typed as a plain record
|
|
2803
|
+
* because the dependency is optional.
|
|
2804
|
+
*/
|
|
2805
|
+
mermaidConfig?: Record<string, unknown>;
|
|
2806
|
+
/**
|
|
2807
|
+
* TEST-ONLY seam: replaces the real mermaid-isomorphic batch renderer so
|
|
2808
|
+
* unit tests stay deterministic with no headless browser. Receives every
|
|
2809
|
+
* mermaid fence source of one document in order and must resolve to exactly
|
|
2810
|
+
* one SVG string per source. Never set this in an app.
|
|
2811
|
+
*
|
|
2812
|
+
* @param sources - Every mermaid fence source of one document, in order.
|
|
2813
|
+
* @param mermaidConfig - The configured mermaid pass-through config, if any.
|
|
2814
|
+
* @returns One SVG string per source, in order.
|
|
2815
|
+
*/
|
|
2816
|
+
renderDiagrams?: (sources: readonly string[], mermaidConfig?: Record<string, unknown>) => Promise<readonly string[]>;
|
|
2817
|
+
};
|
|
2788
2818
|
/**
|
|
2789
2819
|
* Options for the node filesystem provider {@link ContentProvider} `fileSystemContent`.
|
|
2790
2820
|
* These are the markdown-pipeline + source concerns that used to live on the content
|
|
@@ -2810,6 +2840,15 @@ type FileSystemContentOptions = {
|
|
|
2810
2840
|
*/
|
|
2811
2841
|
shikiTheme?: BundledTheme | ThemeRegistrationAny; /** Author applied to articles whose frontmatter omits author. Defaults to undefined. */
|
|
2812
2842
|
defaultAuthor?: string;
|
|
2843
|
+
/**
|
|
2844
|
+
* Build-time Mermaid diagrams: render fenced `mermaid` code blocks to static
|
|
2845
|
+
* inline SVG during the build (zero client-side JS). `true` enables with
|
|
2846
|
+
* defaults; an object passes {@link MermaidDiagramOptions}. Requires
|
|
2847
|
+
* `trustedContent: true` (the raw inline SVG would be stripped by the
|
|
2848
|
+
* sanitize pass) and the OPTIONAL peer dependency `mermaid-isomorphic`
|
|
2849
|
+
* (plus playwright with an installed browser). Defaults to disabled.
|
|
2850
|
+
*/
|
|
2851
|
+
mermaid?: boolean | MermaidDiagramOptions;
|
|
2813
2852
|
};
|
|
2814
2853
|
/**
|
|
2815
2854
|
* Internal mutable state of the filesystem provider: the lazy unified processor and
|
package/dist/index.mjs
CHANGED
|
@@ -1567,6 +1567,23 @@ function createContentState(_ctx) {
|
|
|
1567
1567
|
function validateContentConfig(config) {
|
|
1568
1568
|
if (!Array.isArray(config.providers) || config.providers.length === 0) throw new Error("[web] content: no provider composed.\n Add fileSystemContent(...) to pluginConfigs.content.providers.");
|
|
1569
1569
|
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Validates the `fileSystemContent` provider options (fail-fast at provider
|
|
1572
|
+
* construction). Throws when `mermaid` is enabled without `trustedContent: true`:
|
|
1573
|
+
* mermaid output is raw inline SVG, which the sanitize pass (the untrusted-content
|
|
1574
|
+
* XSS boundary) would strip — so the combination can never work. Errors use the
|
|
1575
|
+
* `[web]` prefix.
|
|
1576
|
+
*
|
|
1577
|
+
* @param options - The provider options to validate.
|
|
1578
|
+
* @throws {Error} If `mermaid` is enabled while `trustedContent` is not `true`.
|
|
1579
|
+
* @example
|
|
1580
|
+
* ```ts
|
|
1581
|
+
* validateFileSystemContentOptions({ contentDir: "./content", trustedContent: true, mermaid: true });
|
|
1582
|
+
* ```
|
|
1583
|
+
*/
|
|
1584
|
+
function validateFileSystemContentOptions(options) {
|
|
1585
|
+
if (Boolean(options.mermaid) && options.trustedContent !== true) throw new Error("[web] content: `mermaid` requires `trustedContent: true`.\n Mermaid diagrams render to raw inline SVG, which the sanitize pass would strip.\n Set trustedContent: true ONLY for fully author-controlled Markdown.");
|
|
1586
|
+
}
|
|
1570
1587
|
//#endregion
|
|
1571
1588
|
//#region src/plugins/content/index.ts
|
|
1572
1589
|
/**
|
|
@@ -11556,6 +11573,154 @@ function parseFrontmatter(raw, config) {
|
|
|
11556
11573
|
};
|
|
11557
11574
|
}
|
|
11558
11575
|
//#endregion
|
|
11576
|
+
//#region src/plugins/content/pipeline/mermaid.ts
|
|
11577
|
+
/** CSS class on the `<figure>` wrapper around each rendered diagram. */
|
|
11578
|
+
const MERMAID_FIGURE_CLASS = "mermaid-diagram";
|
|
11579
|
+
/**
|
|
11580
|
+
* Cached renderer promise — `createMermaidRenderer()` is called ONCE per process
|
|
11581
|
+
* and shared by every document (the underlying headless browser is expensive).
|
|
11582
|
+
*/
|
|
11583
|
+
let cachedRendererPromise;
|
|
11584
|
+
/**
|
|
11585
|
+
* Lazily import `mermaid-isomorphic` and create its batched renderer. The import
|
|
11586
|
+
* happens HERE (never at module load) so the optional dependency is only touched
|
|
11587
|
+
* when a document actually contains a mermaid fence. A failed import is wrapped
|
|
11588
|
+
* in an actionable error naming the missing package.
|
|
11589
|
+
*
|
|
11590
|
+
* @param importModule - Import thunk for the package; injectable so tests can
|
|
11591
|
+
* exercise both outcomes without the real dependency. Defaults to the real
|
|
11592
|
+
* dynamic `import("mermaid-isomorphic")`.
|
|
11593
|
+
* @returns The batched mermaid renderer.
|
|
11594
|
+
* @throws {Error} When the optional dependency cannot be loaded.
|
|
11595
|
+
* @example
|
|
11596
|
+
* ```ts
|
|
11597
|
+
* const renderer = await loadMermaidRenderer();
|
|
11598
|
+
* const results = await renderer(["graph TD; A-->B"]);
|
|
11599
|
+
* ```
|
|
11600
|
+
*/
|
|
11601
|
+
async function loadMermaidRenderer(importModule = () => import("mermaid-isomorphic")) {
|
|
11602
|
+
let moduleExports;
|
|
11603
|
+
try {
|
|
11604
|
+
moduleExports = await importModule();
|
|
11605
|
+
} catch (error) {
|
|
11606
|
+
throw new Error("[web] content: `mermaid` is enabled but the optional dependency \"mermaid-isomorphic\" could not be loaded.\n Install it (plus playwright and a browser):\n bun add -d mermaid-isomorphic playwright && bunx playwright install chromium", { cause: error });
|
|
11607
|
+
}
|
|
11608
|
+
return moduleExports.createMermaidRenderer();
|
|
11609
|
+
}
|
|
11610
|
+
/**
|
|
11611
|
+
* Unwrap mermaid-isomorphic's settled results into plain SVG strings, failing
|
|
11612
|
+
* the build on the first rejected diagram. The error quotes the diagram's first
|
|
11613
|
+
* line so the author can locate the broken fence.
|
|
11614
|
+
*
|
|
11615
|
+
* @param sources - The diagram sources, in the order they were rendered.
|
|
11616
|
+
* @param results - The settled render results (one per source).
|
|
11617
|
+
* @returns One SVG string per source, in order.
|
|
11618
|
+
* @throws {Error} When any diagram failed to render.
|
|
11619
|
+
* @example
|
|
11620
|
+
* ```ts
|
|
11621
|
+
* const svgs = unwrapMermaidResults(["graph TD; A-->B"], results);
|
|
11622
|
+
* ```
|
|
11623
|
+
*/
|
|
11624
|
+
function unwrapMermaidResults(sources, results) {
|
|
11625
|
+
return results.map((result, index) => {
|
|
11626
|
+
if (result.status === "rejected") {
|
|
11627
|
+
const firstLine = (sources[index] ?? "").split("\n", 1)[0] ?? "";
|
|
11628
|
+
throw new Error(`[web] content: mermaid diagram failed to render (diagram starts with "${firstLine}"): ${String(result.reason)}`);
|
|
11629
|
+
}
|
|
11630
|
+
return result.value.svg;
|
|
11631
|
+
});
|
|
11632
|
+
}
|
|
11633
|
+
/**
|
|
11634
|
+
* The REAL render path: lazily load mermaid-isomorphic (cached once per
|
|
11635
|
+
* process), render every fence of the document in ONE batched call, and unwrap
|
|
11636
|
+
* the results. Replaced in unit tests by the `renderDiagrams` seam.
|
|
11637
|
+
*
|
|
11638
|
+
* @param sources - Every mermaid fence source of one document, in order.
|
|
11639
|
+
* @param mermaidConfig - Optional mermaid configuration forwarded to the render call.
|
|
11640
|
+
* @returns One SVG string per source, in order.
|
|
11641
|
+
* @example
|
|
11642
|
+
* ```ts
|
|
11643
|
+
* const svgs = await renderWithMermaidIsomorphic(["graph TD; A-->B"]);
|
|
11644
|
+
* ```
|
|
11645
|
+
*/
|
|
11646
|
+
async function renderWithMermaidIsomorphic(sources, mermaidConfig) {
|
|
11647
|
+
cachedRendererPromise ??= loadMermaidRenderer();
|
|
11648
|
+
return unwrapMermaidResults(sources, await (await cachedRendererPromise)([...sources], mermaidConfig ? { mermaidConfig } : void 0));
|
|
11649
|
+
}
|
|
11650
|
+
/**
|
|
11651
|
+
* Collect every fenced `mermaid` code block in the tree (with the parent/index
|
|
11652
|
+
* needed to replace it later), in document order.
|
|
11653
|
+
*
|
|
11654
|
+
* @param tree - The mdast tree to scan.
|
|
11655
|
+
* @returns The fence sites found.
|
|
11656
|
+
* @example
|
|
11657
|
+
* ```ts
|
|
11658
|
+
* const fences = collectMermaidFences(tree);
|
|
11659
|
+
* ```
|
|
11660
|
+
*/
|
|
11661
|
+
function collectMermaidFences(tree) {
|
|
11662
|
+
const fences = [];
|
|
11663
|
+
visit(tree, "code", (node, index, parent) => {
|
|
11664
|
+
if (node.lang !== "mermaid") return;
|
|
11665
|
+
if (parent === void 0 || index === void 0) return;
|
|
11666
|
+
fences.push({
|
|
11667
|
+
node,
|
|
11668
|
+
parent,
|
|
11669
|
+
index
|
|
11670
|
+
});
|
|
11671
|
+
});
|
|
11672
|
+
return fences;
|
|
11673
|
+
}
|
|
11674
|
+
/**
|
|
11675
|
+
* Normalize the provider's `mermaid` config value (`boolean | options`) to a
|
|
11676
|
+
* plain {@link MermaidDiagramOptions} object for the transform factory.
|
|
11677
|
+
*
|
|
11678
|
+
* @param mermaid - The raw `FileSystemContentOptions.mermaid` value (truthy).
|
|
11679
|
+
* @returns The options object (`{}` for the bare `true` form).
|
|
11680
|
+
* @example
|
|
11681
|
+
* ```ts
|
|
11682
|
+
* normalizeMermaidOptions(true); // {}
|
|
11683
|
+
* normalizeMermaidOptions({ mermaidConfig: { theme: "dark" } });
|
|
11684
|
+
* ```
|
|
11685
|
+
*/
|
|
11686
|
+
function normalizeMermaidOptions(mermaid) {
|
|
11687
|
+
return typeof mermaid === "boolean" ? {} : mermaid;
|
|
11688
|
+
}
|
|
11689
|
+
/**
|
|
11690
|
+
* Remark transform factory: replaces every fenced `mermaid` code block with a
|
|
11691
|
+
* `<figure class="mermaid-diagram">` raw-HTML node carrying the diagram as
|
|
11692
|
+
* static inline SVG, rendered at build time (zero client-side JS). Runs at the
|
|
11693
|
+
* mdast stage, BEFORE remark-rehype; the bridge's `allowDangerousHtml` plus the
|
|
11694
|
+
* framework's `rehype-raw` default carry the SVG into the output. Documents
|
|
11695
|
+
* without a mermaid fence return immediately — `mermaid-isomorphic` is never
|
|
11696
|
+
* imported on that path. A diagram that fails to render fails the build with
|
|
11697
|
+
* its first line quoted.
|
|
11698
|
+
*
|
|
11699
|
+
* @param options - Mermaid options: `mermaidConfig` pass-through + the
|
|
11700
|
+
* test-only `renderDiagrams` seam.
|
|
11701
|
+
* @returns An async mdast tree transformer.
|
|
11702
|
+
* @example
|
|
11703
|
+
* ```ts
|
|
11704
|
+
* unified().use(remarkMermaidDiagrams, { mermaidConfig: { theme: "dark" } });
|
|
11705
|
+
* ```
|
|
11706
|
+
*/
|
|
11707
|
+
function remarkMermaidDiagrams(options = {}) {
|
|
11708
|
+
return async (tree) => {
|
|
11709
|
+
const fences = collectMermaidFences(tree);
|
|
11710
|
+
if (fences.length === 0) return;
|
|
11711
|
+
const sources = fences.map((fence) => fence.node.value);
|
|
11712
|
+
const svgs = await (options.renderDiagrams ?? renderWithMermaidIsomorphic)(sources, options.mermaidConfig);
|
|
11713
|
+
if (svgs.length !== sources.length) throw new Error(`[web] content: mermaid renderer returned ${svgs.length} result(s) for ${sources.length} diagram(s).`);
|
|
11714
|
+
for (const [position, fence] of fences.entries()) {
|
|
11715
|
+
const html = {
|
|
11716
|
+
type: "html",
|
|
11717
|
+
value: `<figure class="${MERMAID_FIGURE_CLASS}">${svgs[position] ?? ""}</figure>`
|
|
11718
|
+
};
|
|
11719
|
+
fence.parent.children[fence.index] = html;
|
|
11720
|
+
}
|
|
11721
|
+
};
|
|
11722
|
+
}
|
|
11723
|
+
//#endregion
|
|
11559
11724
|
//#region src/plugins/content/pipeline/plugins.ts
|
|
11560
11725
|
/**
|
|
11561
11726
|
* Type guard for remark-directive nodes (container/leaf/text).
|
|
@@ -11691,25 +11856,31 @@ function sectionDividerPlugin() {
|
|
|
11691
11856
|
}
|
|
11692
11857
|
/**
|
|
11693
11858
|
* The hardcoded framework default remark (Markdown-AST) plugins, in order:
|
|
11694
|
-
* parse, frontmatter, gfm, directive, pull-quote,
|
|
11695
|
-
* (`remark-rehype` with `allowDangerousHtml`).
|
|
11696
|
-
*
|
|
11697
|
-
*
|
|
11859
|
+
* parse, frontmatter, gfm, directive, pull-quote, the OPT-IN mermaid transform,
|
|
11860
|
+
* then the mdast→hast bridge (`remark-rehype` with `allowDangerousHtml`).
|
|
11861
|
+
* Pull-quote and mermaid run on the mdast before the bridge — pull-quote so the
|
|
11862
|
+
* directive carries its `hName`/`hProperties`, mermaid so the fence is replaced
|
|
11863
|
+
* with raw SVG HTML before Shiki could ever claim the code block.
|
|
11864
|
+
*
|
|
11865
|
+
* @param config - Optional provider configuration; only `mermaid` is read here
|
|
11866
|
+
* (truthy enables the mermaid transform at its fixed mdast position).
|
|
11698
11867
|
* @returns The ordered default remark pluggables.
|
|
11699
11868
|
* @example
|
|
11700
11869
|
* ```ts
|
|
11701
11870
|
* const remark = defaultRemarkPlugins();
|
|
11702
11871
|
* ```
|
|
11703
11872
|
*/
|
|
11704
|
-
function defaultRemarkPlugins() {
|
|
11705
|
-
|
|
11873
|
+
function defaultRemarkPlugins(config) {
|
|
11874
|
+
const plugins = [
|
|
11706
11875
|
remarkParse,
|
|
11707
11876
|
remarkFrontmatter,
|
|
11708
11877
|
remarkGfm,
|
|
11709
11878
|
remarkDirective,
|
|
11710
|
-
pullQuotePlugin
|
|
11711
|
-
[remarkRehype, { allowDangerousHtml: true }]
|
|
11879
|
+
pullQuotePlugin
|
|
11712
11880
|
];
|
|
11881
|
+
if (config?.mermaid) plugins.push([remarkMermaidDiagrams, normalizeMermaidOptions(config.mermaid)]);
|
|
11882
|
+
plugins.push([remarkRehype, { allowDangerousHtml: true }]);
|
|
11883
|
+
return plugins;
|
|
11713
11884
|
}
|
|
11714
11885
|
/**
|
|
11715
11886
|
* The hardcoded framework default rehype (HTML-AST) plugins, in order:
|
|
@@ -11850,14 +12021,15 @@ function applyPluggable(processor, plugin) {
|
|
|
11850
12021
|
* replacing, the defaults).
|
|
11851
12022
|
*
|
|
11852
12023
|
* @param processor - The unified processor under construction (mutated in place).
|
|
11853
|
-
* @param config - Resolved plugin configuration (provides `extraRemarkPlugins
|
|
12024
|
+
* @param config - Resolved plugin configuration (provides `extraRemarkPlugins`,
|
|
12025
|
+
* and `mermaid` which the defaults read to enable the mdast mermaid transform).
|
|
11854
12026
|
* @example
|
|
11855
12027
|
* ```ts
|
|
11856
12028
|
* applyRemarkPlugins(processor, config);
|
|
11857
12029
|
* ```
|
|
11858
12030
|
*/
|
|
11859
12031
|
function applyRemarkPlugins(processor, config) {
|
|
11860
|
-
for (const plugin of defaultRemarkPlugins()) applyPluggable(processor, plugin);
|
|
12032
|
+
for (const plugin of defaultRemarkPlugins(config)) applyPluggable(processor, plugin);
|
|
11861
12033
|
for (const plugin of config.extraRemarkPlugins ?? []) applyPluggable(processor, plugin);
|
|
11862
12034
|
}
|
|
11863
12035
|
/**
|
|
@@ -12009,6 +12181,7 @@ async function discoverSlugs(dir) {
|
|
|
12009
12181
|
* ```
|
|
12010
12182
|
*/
|
|
12011
12183
|
function fileSystemContent(options) {
|
|
12184
|
+
validateFileSystemContentOptions(options);
|
|
12012
12185
|
const state = {
|
|
12013
12186
|
processor: null,
|
|
12014
12187
|
slugs: null,
|
package/package.json
CHANGED
|
@@ -80,9 +80,15 @@
|
|
|
80
80
|
"unist-util-visit": "5.1.0"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
|
+
"mermaid-isomorphic": "^3.0.0",
|
|
83
84
|
"preact": "^10.29.2",
|
|
84
85
|
"preact-render-to-string": "^6.6.0"
|
|
85
86
|
},
|
|
87
|
+
"peerDependenciesMeta": {
|
|
88
|
+
"mermaid-isomorphic": {
|
|
89
|
+
"optional": true
|
|
90
|
+
}
|
|
91
|
+
},
|
|
86
92
|
"devDependencies": {
|
|
87
93
|
"@biomejs/biome": "2.4.16",
|
|
88
94
|
"@types/bun": "1.3.14",
|
|
@@ -96,6 +102,7 @@
|
|
|
96
102
|
"happy-dom": "20.9.0",
|
|
97
103
|
"jiti": "2.6.1",
|
|
98
104
|
"lefthook": "2.1.1",
|
|
105
|
+
"mermaid-isomorphic": "3.1.0",
|
|
99
106
|
"preact": "10.29.2",
|
|
100
107
|
"preact-render-to-string": "6.6.0",
|
|
101
108
|
"publint": "0.3.21",
|
|
@@ -118,5 +125,5 @@
|
|
|
118
125
|
"test:build-e2e": "bun test src/plugins/build/__tests__/e2e/",
|
|
119
126
|
"test:coverage": "vitest run --project unit --project integration --coverage"
|
|
120
127
|
},
|
|
121
|
-
"version": "1.
|
|
128
|
+
"version": "1.9.0"
|
|
122
129
|
}
|