@dooc-dev/dooc 0.0.0-stage → 0.0.1
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 +26 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +244 -0
- package/dist/code.global.js +285 -0
- package/dist/compiler/bundle-urls.d.ts +3 -0
- package/dist/compiler/bundle-urls.js +294 -0
- package/dist/compiler/bundle.d.ts +2 -0
- package/dist/compiler/bundle.js +100 -0
- package/dist/compiler/check-prose.d.ts +11 -0
- package/dist/compiler/check-prose.js +138 -0
- package/dist/compiler/check-types.d.ts +44 -0
- package/dist/compiler/check-types.js +1 -0
- package/dist/compiler/check.d.ts +5 -0
- package/dist/compiler/check.js +450 -0
- package/dist/compiler/code-blocks.d.ts +17 -0
- package/dist/compiler/code-blocks.js +198 -0
- package/dist/compiler/code-languages.d.ts +10 -0
- package/dist/compiler/code-languages.js +109 -0
- package/dist/compiler/compile.d.ts +95 -0
- package/dist/compiler/compile.js +410 -0
- package/dist/compiler/config.d.ts +7 -0
- package/dist/compiler/config.js +52 -0
- package/dist/compiler/dialect.d.ts +24 -0
- package/dist/compiler/dialect.js +198 -0
- package/dist/compiler/drawio.d.ts +26 -0
- package/dist/compiler/drawio.js +176 -0
- package/dist/compiler/figures.d.ts +14 -0
- package/dist/compiler/figures.js +221 -0
- package/dist/compiler/file-cache.d.ts +14 -0
- package/dist/compiler/file-cache.js +61 -0
- package/dist/compiler/files.d.ts +29 -0
- package/dist/compiler/files.js +431 -0
- package/dist/compiler/fonts.d.ts +10 -0
- package/dist/compiler/fonts.js +54 -0
- package/dist/compiler/format.d.ts +1 -0
- package/dist/compiler/format.js +42 -0
- package/dist/compiler/from-mdast.d.ts +16 -0
- package/dist/compiler/from-mdast.js +142 -0
- package/dist/compiler/icons.d.ts +14 -0
- package/dist/compiler/icons.js +85 -0
- package/dist/compiler/language-codes.d.ts +7 -0
- package/dist/compiler/language-codes.js +17 -0
- package/dist/compiler/locales.d.ts +7 -0
- package/dist/compiler/locales.js +67 -0
- package/dist/compiler/manifest.d.ts +6 -0
- package/dist/compiler/manifest.js +30 -0
- package/dist/compiler/ops.d.ts +44 -0
- package/dist/compiler/ops.js +362 -0
- package/dist/compiler/public-file.d.ts +2 -0
- package/dist/compiler/public-file.js +93 -0
- package/dist/compiler/serialize.d.ts +3 -0
- package/dist/compiler/serialize.js +16 -0
- package/dist/compiler/terms.d.ts +9 -0
- package/dist/compiler/terms.js +44 -0
- package/dist/compiler/textfile-remote.d.ts +12 -0
- package/dist/compiler/textfile-remote.js +124 -0
- package/dist/compiler/textfile.d.ts +9 -0
- package/dist/compiler/textfile.js +113 -0
- package/dist/compiler/theme-variants.d.ts +4 -0
- package/dist/compiler/theme-variants.js +22 -0
- package/dist/compiler/zip.d.ts +6 -0
- package/dist/compiler/zip.js +48 -0
- package/dist/core/blocks.d.ts +99 -0
- package/dist/core/blocks.js +29 -0
- package/dist/core/code-file.d.ts +12 -0
- package/dist/core/code-file.js +68 -0
- package/dist/core/content-metadata.d.ts +48 -0
- package/dist/core/content-metadata.js +264 -0
- package/dist/core/content-types.d.ts +45 -0
- package/dist/core/content-types.js +1 -0
- package/dist/core/figure-frame.d.ts +12 -0
- package/dist/core/figure-frame.js +108 -0
- package/dist/core/file-embed.d.ts +13 -0
- package/dist/core/file-embed.js +43 -0
- package/dist/core/files.d.ts +15 -0
- package/dist/core/files.js +59 -0
- package/dist/core/flow-base-css.generated.d.ts +1 -0
- package/dist/core/flow-base-css.generated.js +4 -0
- package/dist/core/flow-css.d.ts +9 -0
- package/dist/core/flow-css.js +54 -0
- package/dist/core/graph-common.d.ts +32 -0
- package/dist/core/graph-common.js +113 -0
- package/dist/core/graph.d.ts +117 -0
- package/dist/core/graph.js +480 -0
- package/dist/core/html.d.ts +29 -0
- package/dist/core/html.js +444 -0
- package/dist/core/icons.d.ts +1 -0
- package/dist/core/icons.js +1 -0
- package/dist/core/inline.d.ts +27 -0
- package/dist/core/inline.js +42 -0
- package/dist/core/language.d.ts +41 -0
- package/dist/core/language.js +44 -0
- package/dist/core/languages/ja.d.ts +2 -0
- package/dist/core/languages/ja.js +5 -0
- package/dist/core/markdown.d.ts +27 -0
- package/dist/core/markdown.js +154 -0
- package/dist/core/previewers.d.ts +37 -0
- package/dist/core/previewers.js +103 -0
- package/dist/core/routing.d.ts +20 -0
- package/dist/core/routing.js +64 -0
- package/dist/core/schema-comments.d.ts +14 -0
- package/dist/core/schema-comments.js +52 -0
- package/dist/core/schema-graph-flow.d.ts +9 -0
- package/dist/core/schema-graph-flow.js +379 -0
- package/dist/core/schema-graph-mount.d.ts +9 -0
- package/dist/core/schema-graph-mount.js +105 -0
- package/dist/core/schema-graph.d.ts +254 -0
- package/dist/core/schema-graph.js +981 -0
- package/dist/core/table-ui.d.ts +2 -0
- package/dist/core/table-ui.js +180 -0
- package/dist/core/taxonomy.d.ts +12 -0
- package/dist/core/taxonomy.js +49 -0
- package/dist/core/term-reference.d.ts +5 -0
- package/dist/core/term-reference.js +19 -0
- package/dist/core/terms-ui.d.ts +6 -0
- package/dist/core/terms-ui.js +22 -0
- package/dist/core/textfile.d.ts +10 -0
- package/dist/core/textfile.js +39 -0
- package/dist/core/theme-variants.d.ts +3 -0
- package/dist/core/theme-variants.js +37 -0
- package/dist/core/toc-ui.d.ts +2 -0
- package/dist/core/toc-ui.js +161 -0
- package/dist/core/video-ui.d.ts +2 -0
- package/dist/core/video-ui.js +253 -0
- package/dist/core/workspace.d.ts +15 -0
- package/dist/core/workspace.js +28 -0
- package/dist/edit.global.js +305 -0
- package/dist/flow.global.js +4959 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +31 -0
- package/dist/mcp.d.ts +24 -0
- package/dist/mcp.js +467 -0
- package/dist/plugins/actions.d.ts +45 -0
- package/dist/plugins/actions.js +125 -0
- package/dist/plugins/blocks.d.ts +92 -0
- package/dist/plugins/blocks.js +99 -0
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/oauth.d.ts +14 -0
- package/dist/plugins/oauth.js +164 -0
- package/dist/plugins/runtime.d.ts +24 -0
- package/dist/plugins/runtime.js +339 -0
- package/dist/plugins/types.d.ts +56 -0
- package/dist/plugins/types.js +1 -0
- package/dist/plugins/view.d.ts +29 -0
- package/dist/plugins/view.js +60 -0
- package/dist/ui.global.js +77 -0
- package/dist/viewer/assets.d.ts +2 -0
- package/dist/viewer/assets.js +3 -0
- package/dist/viewer/build.d.ts +12 -0
- package/dist/viewer/build.js +55 -0
- package/dist/viewer/bundle-network.d.ts +2 -0
- package/dist/viewer/bundle-network.js +88 -0
- package/dist/viewer/code-file.d.ts +20 -0
- package/dist/viewer/code-file.js +42 -0
- package/dist/viewer/editor-tools.d.ts +38 -0
- package/dist/viewer/editor-tools.js +211 -0
- package/dist/viewer/file-references.d.ts +14 -0
- package/dist/viewer/file-references.js +38 -0
- package/dist/viewer/font-picker.d.ts +4 -0
- package/dist/viewer/font-picker.js +145 -0
- package/dist/viewer/font-settings.d.ts +11 -0
- package/dist/viewer/font-settings.js +50 -0
- package/dist/viewer/fonts/LICENSE.txt +119 -0
- package/dist/viewer/fonts/udev-gothic-regular.woff2 +0 -0
- package/dist/viewer/fonts.d.ts +2 -0
- package/dist/viewer/fonts.js +13 -0
- package/dist/viewer/git-diff.d.ts +3 -0
- package/dist/viewer/git-diff.js +33 -0
- package/dist/viewer/git-types.d.ts +73 -0
- package/dist/viewer/git-types.js +1 -0
- package/dist/viewer/git.d.ts +5 -0
- package/dist/viewer/git.js +337 -0
- package/dist/viewer/hmr.d.ts +53 -0
- package/dist/viewer/hmr.js +335 -0
- package/dist/viewer/i18n.d.ts +6 -0
- package/dist/viewer/i18n.js +65 -0
- package/dist/viewer/ja-lint.d.ts +3 -0
- package/dist/viewer/ja-lint.js +44 -0
- package/dist/viewer/language-picker.d.ts +2 -0
- package/dist/viewer/language-picker.js +233 -0
- package/dist/viewer/languages.d.ts +11 -0
- package/dist/viewer/languages.js +20 -0
- package/dist/viewer/locales/eng.json +294 -0
- package/dist/viewer/locales/jpn.json +294 -0
- package/dist/viewer/locales/zho.json +294 -0
- package/dist/viewer/nav-focus.d.ts +2 -0
- package/dist/viewer/nav-focus.js +64 -0
- package/dist/viewer/plugin-blocks.d.ts +3 -0
- package/dist/viewer/plugin-blocks.js +115 -0
- package/dist/viewer/plugin-config.d.ts +35 -0
- package/dist/viewer/plugin-config.js +57 -0
- package/dist/viewer/plugin-view.d.ts +3 -0
- package/dist/viewer/plugin-view.js +28 -0
- package/dist/viewer/plugins.d.ts +8 -0
- package/dist/viewer/plugins.js +214 -0
- package/dist/viewer/resize.d.ts +4 -0
- package/dist/viewer/resize.js +238 -0
- package/dist/viewer/resource-library.d.ts +29 -0
- package/dist/viewer/resource-library.js +104 -0
- package/dist/viewer/search.d.ts +16 -0
- package/dist/viewer/search.js +358 -0
- package/dist/viewer/serve.d.ts +206 -0
- package/dist/viewer/serve.js +3233 -0
- package/dist/viewer/site-icon.d.ts +8 -0
- package/dist/viewer/site-icon.js +56 -0
- package/dist/viewer/textfile.d.ts +9 -0
- package/dist/viewer/textfile.js +50 -0
- package/dist/viewer/theme-picker.d.ts +2 -0
- package/dist/viewer/theme-picker.js +135 -0
- package/dist/viewer/workspace.d.ts +4 -0
- package/dist/viewer/workspace.js +47 -0
- package/package.json +127 -5
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// Normalized Block AST → HTML (renderer for the local preview). The preview goes through the same
|
|
2
|
+
// "just map blocks + inline segments to output" pipeline as consumers (= prevents rendering
|
|
3
|
+
// semantics from drifting). Headings and body text share the same weight (headings are also
|
|
4
|
+
// font-weight 400).
|
|
5
|
+
import { figureFrameHtml } from "./figure-frame.js";
|
|
6
|
+
import { parseFigureLink } from "./file-embed.js";
|
|
7
|
+
import { GRAPH_LAYOUT_MARGIN, GRAPH_LEGEND, graphUid, layoutGraph, } from "./graph.js";
|
|
8
|
+
import { CHECK_ICON } from "./icons.js";
|
|
9
|
+
import { inlineSegmenter, termMap } from "./inline.js";
|
|
10
|
+
import { termComparator } from "./language.js";
|
|
11
|
+
import { parseDocLink } from "./routing.js";
|
|
12
|
+
import { layoutSchemaGraph, SCHEMA_LAYOUT_MARGIN, schemaGraphClientData, schemaGraphUid, } from "./schema-graph.js";
|
|
13
|
+
import { docDisplayName } from "./taxonomy.js";
|
|
14
|
+
import { termAnchor } from "./term-reference.js";
|
|
15
|
+
import { textfileName } from "./textfile.js";
|
|
16
|
+
export const emptyCtx = { figures: new Map(), terms: [] };
|
|
17
|
+
export const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
18
|
+
// File links open their resource page; SVGs and terms also have hover previews.
|
|
19
|
+
const segmentsHtml = (text, ctx, seg) => inlineHtml(seg(text), ctx);
|
|
20
|
+
const inlineHtml = (segments, ctx) => segments
|
|
21
|
+
.map((s) => {
|
|
22
|
+
switch (s.kind) {
|
|
23
|
+
case "text":
|
|
24
|
+
return escapeHtml(s.text);
|
|
25
|
+
case "code":
|
|
26
|
+
return `<code>${escapeHtml(s.text)}</code>`;
|
|
27
|
+
case "strong":
|
|
28
|
+
return `<strong>${inlineHtml(s.children, ctx)}</strong>`;
|
|
29
|
+
case "emphasis":
|
|
30
|
+
return `<em>${inlineHtml(s.children, ctx)}</em>`;
|
|
31
|
+
case "link": {
|
|
32
|
+
const label = s.children ? inlineHtml(s.children, ctx) : escapeHtml(s.text);
|
|
33
|
+
let href = s.url;
|
|
34
|
+
try {
|
|
35
|
+
const ref = parseDocLink(s.url);
|
|
36
|
+
const target = ref && ctx.docLinks?.get(ref.file);
|
|
37
|
+
if (target && ref)
|
|
38
|
+
href =
|
|
39
|
+
target +
|
|
40
|
+
(ref.fragment ? `#${encodeURIComponent(ref.fragment)}` : "");
|
|
41
|
+
const figureRef = parseFigureLink(s.url);
|
|
42
|
+
const fig = figureRef &&
|
|
43
|
+
[...ctx.figures.values()].find((fig) => fig.file === figureRef.file);
|
|
44
|
+
if (fig && figureRef) {
|
|
45
|
+
href =
|
|
46
|
+
`/_files/file/${encodeURIComponent(fig.file)}` +
|
|
47
|
+
(figureRef.fragment
|
|
48
|
+
? `#${encodeURIComponent(figureRef.fragment)}`
|
|
49
|
+
: "");
|
|
50
|
+
const pop = fig.kind === "svg"
|
|
51
|
+
? `<span class="figpop">${figureFrameHtml(fig, "fig-frame figpop-frame")}</span>`
|
|
52
|
+
: "";
|
|
53
|
+
return `<a class="figref" href="${escapeHtml(href)}">${label}${pop}</a>`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* Invalid drafts retain their source link; validation reports the error. */
|
|
58
|
+
}
|
|
59
|
+
return `<a href="${escapeHtml(href)}">${label}</a>`;
|
|
60
|
+
}
|
|
61
|
+
case "term": {
|
|
62
|
+
const label = s.children ? inlineHtml(s.children, ctx) : escapeHtml(s.text);
|
|
63
|
+
const entry = termMap(ctx.terms).get(s.id);
|
|
64
|
+
if (!entry)
|
|
65
|
+
return label;
|
|
66
|
+
const href = ctx.termLinks?.get(s.id) ??
|
|
67
|
+
`/_files/terms#${encodeURIComponent(termAnchor(s.id))}`;
|
|
68
|
+
return `<a class="dictref" href="${escapeHtml(href)}">${label}<span class="dictpop">${escapeHtml(entry.definition)}${entry.description ? `<span class="dictpop-description">${escapeHtml(entry.description)}</span>` : ""}</span></a>`;
|
|
69
|
+
}
|
|
70
|
+
default:
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
.join("");
|
|
75
|
+
const listHtml = (items, ctx, seg, ordered = false) => {
|
|
76
|
+
const tag = ordered ? "ol" : "ul";
|
|
77
|
+
const lis = items
|
|
78
|
+
.map((it) => {
|
|
79
|
+
if (typeof it !== "string" && "blocks" in it)
|
|
80
|
+
return `<li>${it.blocks.map((block) => blockHtml(block, ctx, seg)).join("\n")}</li>`;
|
|
81
|
+
const text = typeof it === "string" ? it : it.text;
|
|
82
|
+
const children = typeof it === "string" || it.children.length === 0
|
|
83
|
+
? ""
|
|
84
|
+
: listHtml(it.children, ctx, seg, it.ordered === true);
|
|
85
|
+
return `<li>${segmentsHtml(text, ctx, seg)}${children}</li>`;
|
|
86
|
+
})
|
|
87
|
+
.join("");
|
|
88
|
+
return `<${tag}>${lis}</${tag}>`;
|
|
89
|
+
};
|
|
90
|
+
// caption: line 1 = the label "Fig.<name>", then a newline and the description (.cap keeps it via
|
|
91
|
+
// pre-line). The figure content is figure-frame.ts's sandboxed iframe (fully isolates svg / html;
|
|
92
|
+
// raster images use <img>).
|
|
93
|
+
const figureHtml = (fig, withLabel = true, caption) => {
|
|
94
|
+
const label = caption ?? fig.title ?? `Fig.${fig.name}`;
|
|
95
|
+
const cap = [withLabel ? label : undefined, fig.desc].filter(Boolean).join("\n");
|
|
96
|
+
return `<figure class="fig">${figureFrameHtml(fig, "fig-frame", caption)}${cap ? `<figcaption class="cap">${escapeHtml(cap)}</figcaption>` : ""}</figure>`;
|
|
97
|
+
};
|
|
98
|
+
const tableHtml = (b, ctx, seg) => {
|
|
99
|
+
if (b.dataSource) {
|
|
100
|
+
const rows = [b.headers, ...b.rows];
|
|
101
|
+
const mode = b.tableMode ?? "data";
|
|
102
|
+
const header = mode === "header";
|
|
103
|
+
const body = rows.slice(header ? 1 : 0);
|
|
104
|
+
const headings = b.headers
|
|
105
|
+
.map((cell, index) => `<th scope="col">${header ? escapeHtml(cell) : `<span data-i18n="Column">Column</span> ${index + 1}`}</th>`)
|
|
106
|
+
.join("");
|
|
107
|
+
const content = body
|
|
108
|
+
.map((row, index) => `<tr><th scope="row">${index + (header ? 2 : 1)}</th>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`)
|
|
109
|
+
.join("");
|
|
110
|
+
return `<section class="data-table" data-table-source="${escapeHtml(b.dataSource)}" data-table-mode="${mode}">
|
|
111
|
+
<div class="data-table-scroll" tabindex="0" role="region" aria-label="${escapeHtml(b.alt ?? b.caption ?? b.dataSource)}"><table><thead title="Click a column to sort; row numbers refer to the source" data-i18n-title="Click a column to sort; row numbers refer to the source"><tr><th scope="col">#</th>${headings}</tr></thead><tbody>${content}</tbody></table></div>
|
|
112
|
+
<div class="data-table-resize" role="separator" tabindex="0" aria-orientation="horizontal" aria-label="Resize table" data-i18n-aria-label="Resize table"></div>
|
|
113
|
+
<span hidden data-table-column-label data-i18n="Column">Column</span>
|
|
114
|
+
<template data-table-rows>${escapeHtml(JSON.stringify(rows))}</template></section>`;
|
|
115
|
+
}
|
|
116
|
+
const th = b.headers.map((h) => `<th>${segmentsHtml(h, ctx, seg)}</th>`).join("");
|
|
117
|
+
const trs = b.rows
|
|
118
|
+
.map((r) => `<tr>${r.map((c) => `<td>${segmentsHtml(c, ctx, seg)}</td>`).join("")}</tr>`)
|
|
119
|
+
.join("");
|
|
120
|
+
const table = `<table><thead><tr>${th}</tr></thead><tbody>${trs}</tbody></table>`;
|
|
121
|
+
if (b.collapsible) {
|
|
122
|
+
return `<details><summary>${escapeHtml(b.caption ?? "table")}</summary>${table}</details>`;
|
|
123
|
+
}
|
|
124
|
+
return `${table}${b.caption ? `<div class="cap">${escapeHtml(b.caption)}</div>` : ""}`;
|
|
125
|
+
};
|
|
126
|
+
// Numbers are rounded to one decimal place (keeps the SVG stable and short).
|
|
127
|
+
const n1 = (v) => String(Math.round(v * 10) / 10);
|
|
128
|
+
// One table box: header (rounded top edge) + field rows + note rows + transparent rects for hover
|
|
129
|
+
// hit-testing.
|
|
130
|
+
const schemaBoxHtml = (t) => {
|
|
131
|
+
const r = t.radius;
|
|
132
|
+
const head = [
|
|
133
|
+
`M${n1(t.x)},${n1(t.y + r)}`,
|
|
134
|
+
`a${r},${r} 0 0 1 ${r},-${r}`,
|
|
135
|
+
`h${n1(t.w - r * 2)}`,
|
|
136
|
+
`a${r},${r} 0 0 1 ${r},${r}`,
|
|
137
|
+
`v${n1(t.headerH - r)}`,
|
|
138
|
+
`h-${n1(t.w)}`,
|
|
139
|
+
"z",
|
|
140
|
+
].join(" ");
|
|
141
|
+
const rows = t.rows
|
|
142
|
+
.map((row) => {
|
|
143
|
+
if (row.kind === "note") {
|
|
144
|
+
return `<text class="sg-note" x="${n1(row.x)}" y="${n1(row.baseline)}">${escapeHtml(row.name)}</text>`;
|
|
145
|
+
}
|
|
146
|
+
const typeCls = row.kind === "pk" ? " sg-key" : row.kind === "scalar" ? "" : " sg-ref";
|
|
147
|
+
return [
|
|
148
|
+
`<text class="sg-fname" x="${n1(row.x)}" y="${n1(row.baseline)}">${escapeHtml(row.name)}</text>`,
|
|
149
|
+
`<text class="sg-ftype${typeCls}" x="${n1(row.typeX)}" y="${n1(row.baseline)}" text-anchor="end">${escapeHtml(row.type)}</text>`,
|
|
150
|
+
].join("");
|
|
151
|
+
})
|
|
152
|
+
.join("");
|
|
153
|
+
// Hover targets = the whole box (table) + fk rows (field). Field targets come last so they sit
|
|
154
|
+
// on top.
|
|
155
|
+
const hits = t.rows
|
|
156
|
+
.filter((row) => row.fieldIndex != null && (row.kind === "fk" || row.kind === "fkList"))
|
|
157
|
+
.map((row) => `<rect class="sg-hit sg-hf${t.index}-${row.fieldIndex}" x="${n1(t.x)}" y="${n1(row.y - 9.5)}" width="${n1(t.w)}" height="19"></rect>`)
|
|
158
|
+
.join("");
|
|
159
|
+
// data-sg-t = key the interaction layer (schema-graph-client.ts) uses to look up drag targets.
|
|
160
|
+
return [
|
|
161
|
+
`<g class="sg-g" data-sg-t="${t.index}">`,
|
|
162
|
+
`<rect class="sg-box sg-b${t.index}" x="${n1(t.x)}" y="${n1(t.y)}" width="${n1(t.w)}" height="${n1(t.h)}" rx="${r}"></rect>`,
|
|
163
|
+
`<path class="sg-head" d="${head}"></path>`,
|
|
164
|
+
`<text class="sg-tname" x="${n1(t.rows[0]?.x ?? t.x)}" y="${n1(t.headerBaseline)}">${escapeHtml(t.name)}</text>`,
|
|
165
|
+
rows,
|
|
166
|
+
`<rect class="sg-hit sg-h${t.index}" x="${n1(t.x)}" y="${n1(t.y)}" width="${n1(t.w)}" height="${n1(t.h)}"></rect>`,
|
|
167
|
+
hits,
|
|
168
|
+
"</g>",
|
|
169
|
+
].join("");
|
|
170
|
+
};
|
|
171
|
+
// schema graph (`schemath`) → self-contained SVG. Geometry comes from schema-graph.ts
|
|
172
|
+
// (deterministic layout); this is markup only. Colors reference `var(--doc-*, fallback)` like
|
|
173
|
+
// figures, and linked hover (highlighting related edges) is done entirely with CSS :has() (no JS =
|
|
174
|
+
// same behavior on every consumer). Classes / marker ids are namespaced by the model hash, so
|
|
175
|
+
// multiple graphs on the same page don't collide.
|
|
176
|
+
const schemaGraphHtml = (b) => {
|
|
177
|
+
if (b.graph.tables.length === 0) {
|
|
178
|
+
return `<div class="ph">${escapeHtml(b.alt || "schema graph")}<span> — no tables declared</span></div>`;
|
|
179
|
+
}
|
|
180
|
+
const layout = layoutSchemaGraph(b.graph);
|
|
181
|
+
const cls = `sg-${schemaGraphUid(b.graph)}`;
|
|
182
|
+
const arrow = `${cls}-arw`;
|
|
183
|
+
const accent = "var(--doc-accent-2, #0277bd)";
|
|
184
|
+
const rules = [
|
|
185
|
+
`.${cls}{width:100%;max-width:${layout.width}px;height:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}`,
|
|
186
|
+
`.${cls} .sg-frame{fill:color-mix(in srgb, var(--doc-fg, #000000) 3%, transparent);stroke:var(--doc-border, #dde3eb);stroke-width:1;stroke-dasharray:5 4}`,
|
|
187
|
+
`.${cls} .sg-glabel{font-size:11.5px;fill:var(--doc-fg-3, #5b6271)}`,
|
|
188
|
+
`.${cls} .sg-box{fill:var(--doc-panel, #ffffff);stroke:var(--doc-border, #dde3eb);stroke-width:1}`,
|
|
189
|
+
`.${cls} .sg-head{fill:color-mix(in srgb, ${accent} 10%, transparent)}`,
|
|
190
|
+
`.${cls} .sg-tname{font-size:12.5px;fill:var(--doc-fg, #000000)}`,
|
|
191
|
+
`.${cls} .sg-fname{font-size:11.5px;fill:var(--doc-fg-2, #2a3140)}`,
|
|
192
|
+
`.${cls} .sg-ftype{font-size:10.5px;fill:var(--doc-fg-3, #5b6271)}`,
|
|
193
|
+
`.${cls} .sg-key{fill:${accent}}`,
|
|
194
|
+
`.${cls} .sg-ref{fill:var(--doc-accent, #1b4994)}`,
|
|
195
|
+
`.${cls} .sg-note{font-size:10.5px;fill:var(--doc-fg-4, #9099a8)}`,
|
|
196
|
+
`.${cls} .sg-e{fill:none;stroke:color-mix(in srgb, var(--doc-fg, #000000) 26%, transparent);stroke-width:1.2}`,
|
|
197
|
+
`.${cls} .sg-list{stroke-dasharray:5 4}`,
|
|
198
|
+
`.${cls} .sg-hit{fill:transparent}`,
|
|
199
|
+
`.${cls} .sg-legend{font-size:10.5px;fill:var(--doc-fg-3, #5b6271)}`,
|
|
200
|
+
// Classes added by the interaction layer (schema-graph-client.ts). Where JS doesn't run
|
|
201
|
+
// they never appear, so the SVG stays static (= progressive enhancement).
|
|
202
|
+
`.${cls}.sg-live{touch-action:none}`,
|
|
203
|
+
`.${cls}.sg-live .sg-g{cursor:grab}`,
|
|
204
|
+
`.${cls}.sg-grabbing .sg-g{cursor:grabbing}`,
|
|
205
|
+
`.${cls} .sg-tool{opacity:0.35;transition:opacity .12s}`,
|
|
206
|
+
`.${cls}:hover .sg-tool,.${cls}.sg-expanded .sg-tool{opacity:1}`,
|
|
207
|
+
`.${cls} .sg-btn{cursor:pointer}`,
|
|
208
|
+
`.${cls} .sg-btn-bg{fill:var(--doc-panel, #ffffff);stroke:var(--doc-border, #dde3eb);stroke-width:1}`,
|
|
209
|
+
`.${cls} .sg-btn:hover .sg-btn-bg{stroke:${accent}}`,
|
|
210
|
+
`.${cls} .sg-btn-t{font-size:12px;fill:var(--doc-fg-2, #2a3140)}`,
|
|
211
|
+
];
|
|
212
|
+
// Table hover = highlight every edge connected to that table (dimming the rest); fk row hover =
|
|
213
|
+
// strongest highlight on that one edge.
|
|
214
|
+
for (const t of layout.tables) {
|
|
215
|
+
rules.push(`.${cls}:has(.sg-h${t.index}:hover) .sg-e{opacity:0.12}`, `.${cls}:has(.sg-h${t.index}:hover) .sg-et${t.index}{opacity:1;stroke:${accent};stroke-width:1.8}`, `.${cls}:has(.sg-h${t.index}:hover) .sg-b${t.index}{stroke:${accent}}`);
|
|
216
|
+
}
|
|
217
|
+
for (const e of layout.edges) {
|
|
218
|
+
rules.push(`.${cls}:has(.sg-hf${e.from}-${e.fieldIndex}:hover) .sg-ef${e.from}-${e.fieldIndex}{opacity:1;stroke:${accent};stroke-width:2.4}`);
|
|
219
|
+
}
|
|
220
|
+
const frames = layout.groups
|
|
221
|
+
.map((g) => `<rect class="sg-frame" x="${n1(g.x)}" y="${n1(g.y)}" width="${n1(g.w)}" height="${n1(g.h)}" rx="14"></rect><text class="sg-glabel" x="${n1(g.x + 4)}" y="${n1(g.y - 7)}">${escapeHtml(g.label)}</text>`)
|
|
222
|
+
.join("");
|
|
223
|
+
// data-sg-e = index the interaction layer uses to replace d after a drag (in lockstep with
|
|
224
|
+
// layout.edges).
|
|
225
|
+
const edges = layout.edges
|
|
226
|
+
.map((e, i) => `<path class="sg-e sg-et${e.from} sg-et${e.to} sg-ef${e.from}-${e.fieldIndex}${e.list ? " sg-list" : ""}" data-sg-e="${i}" d="${e.d}" marker-end="url(#${arrow})"></path>`)
|
|
227
|
+
.join("");
|
|
228
|
+
const legend = layout.legendY == null
|
|
229
|
+
? ""
|
|
230
|
+
: `<text class="sg-legend" x="${SCHEMA_LAYOUT_MARGIN}" y="${n1(layout.legendY)}">Solid = fk (1:1) / dashed = fk[] (1:N) / hover to highlight related references</text>`;
|
|
231
|
+
const desc = `${b.graph.tables.length} tables, ${b.graph.edges.length} references: ${b.graph.tables
|
|
232
|
+
.map((t) => t.name)
|
|
233
|
+
.join(", ")}`;
|
|
234
|
+
// Data read by the interactive UI (React Flow). It starts from the same model / same initial
|
|
235
|
+
// placement, so the picture doesn't jump when switching from static SVG → interactive graph
|
|
236
|
+
// (before mount / without JS, this SVG is the fallback as-is).
|
|
237
|
+
const clientData = schemaGraphClientData(cls, b.graph, layout, b.source);
|
|
238
|
+
const svg = [
|
|
239
|
+
`<svg xmlns="http://www.w3.org/2000/svg" class="${cls}" viewBox="0 0 ${layout.width} ${layout.height}" role="img" data-sg-layout="${escapeHtml(JSON.stringify(clientData))}">`,
|
|
240
|
+
`<title>${escapeHtml(b.alt ?? b.caption ?? "schema graph")}</title>`,
|
|
241
|
+
`<desc>${escapeHtml(desc)}</desc>`,
|
|
242
|
+
`<defs><marker id="${arrow}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" style="fill:context-stroke"></path></marker></defs>`,
|
|
243
|
+
`<style>${rules.join("\n")}</style>`,
|
|
244
|
+
// sg-view = target of zoom / pan (the toolbar band and legend stay in screen coordinates).
|
|
245
|
+
'<g class="sg-view">',
|
|
246
|
+
frames,
|
|
247
|
+
edges,
|
|
248
|
+
layout.tables.map(schemaBoxHtml).join(""),
|
|
249
|
+
"</g>",
|
|
250
|
+
legend,
|
|
251
|
+
"</svg>",
|
|
252
|
+
].join("");
|
|
253
|
+
return `<figure class="fig"${b.alt ? ` aria-label="${escapeHtml(b.alt)}"` : ""}>${svg}${b.caption ? `<figcaption class="cap">${escapeHtml(b.caption)}</figcaption>` : ""}</figure>`;
|
|
254
|
+
};
|
|
255
|
+
// One node box (rounded rect + centered label + transparent rect for hover hit-testing).
|
|
256
|
+
const graphBoxHtml = (n) => [
|
|
257
|
+
`<g class="gr-g" data-gr-n="${n.index}">`,
|
|
258
|
+
`<rect class="gr-box gr-b${n.index}" x="${n1(n.x)}" y="${n1(n.y)}" width="${n1(n.w)}" height="${n1(n.h)}" rx="${n.radius}"></rect>`,
|
|
259
|
+
`<text class="gr-nlabel" x="${n1(n.textX)}" y="${n1(n.baseline)}" text-anchor="middle">${escapeHtml(n.label)}</text>`,
|
|
260
|
+
`<rect class="gr-hit gr-h${n.index}" x="${n1(n.x)}" y="${n1(n.y)}" width="${n1(n.w)}" height="${n1(n.h)}"></rect>`,
|
|
261
|
+
"</g>",
|
|
262
|
+
].join("");
|
|
263
|
+
// Relationship graph (`graphth`) → self-contained SVG. Geometry comes from graph.ts
|
|
264
|
+
// (deterministic layout); this is markup only. Colors reference `var(--doc-*, fallback)` like
|
|
265
|
+
// figures / schema graph, and linked hover (highlighting related edges) is done entirely with CSS
|
|
266
|
+
// :has() (no JS = same behavior on every consumer). Classes / marker ids are namespaced by the
|
|
267
|
+
// model hash, so multiple graphs on the same page don't collide.
|
|
268
|
+
const graphHtml = (b) => {
|
|
269
|
+
if (b.graph.nodes.length === 0) {
|
|
270
|
+
return `<div class="ph">${escapeHtml(b.alt || "graph")}<span> — no nodes declared</span></div>`;
|
|
271
|
+
}
|
|
272
|
+
const layout = layoutGraph(b.graph);
|
|
273
|
+
const cls = `gr-${graphUid(b.graph)}`;
|
|
274
|
+
const arrow = `${cls}-arw`;
|
|
275
|
+
const accent = "var(--doc-accent-2, #0277bd)";
|
|
276
|
+
const rules = [
|
|
277
|
+
`.${cls}{width:100%;max-width:${layout.width}px;height:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}`,
|
|
278
|
+
`.${cls} .gr-frame{fill:color-mix(in srgb, var(--doc-fg, #000000) 3%, transparent);stroke:var(--doc-border, #dde3eb);stroke-width:1;stroke-dasharray:5 4}`,
|
|
279
|
+
`.${cls} .gr-glabel{font-size:11.5px;fill:var(--doc-fg-3, #5b6271)}`,
|
|
280
|
+
`.${cls} .gr-box{fill:var(--doc-panel, #ffffff);stroke:var(--doc-border, #dde3eb);stroke-width:1}`,
|
|
281
|
+
`.${cls} .gr-nlabel{font-size:12.5px;fill:var(--doc-fg, #000000)}`,
|
|
282
|
+
`.${cls} .gr-e{fill:none;stroke:color-mix(in srgb, var(--doc-fg, #000000) 26%, transparent);stroke-width:1.2}`,
|
|
283
|
+
`.${cls} .gr-dashed{stroke-dasharray:5 4}`,
|
|
284
|
+
`.${cls} .gr-elabel{font-size:10.5px;fill:var(--doc-fg-3, #5b6271)}`,
|
|
285
|
+
`.${cls} .gr-hit{fill:transparent}`,
|
|
286
|
+
`.${cls} .gr-legend{font-size:10.5px;fill:var(--doc-fg-3, #5b6271)}`,
|
|
287
|
+
];
|
|
288
|
+
// Node hover = highlight every edge connected to that node (dimming the rest).
|
|
289
|
+
for (const n of layout.nodes) {
|
|
290
|
+
rules.push(`.${cls}:has(.gr-h${n.index}:hover) .gr-e{opacity:0.12}`, `.${cls}:has(.gr-h${n.index}:hover) .gr-et${n.index}{opacity:1;stroke:${accent};stroke-width:1.8}`, `.${cls}:has(.gr-h${n.index}:hover) .gr-b${n.index}{stroke:${accent}}`);
|
|
291
|
+
}
|
|
292
|
+
const frames = layout.regions
|
|
293
|
+
.map((g) => `<rect class="gr-frame" x="${n1(g.x)}" y="${n1(g.y)}" width="${n1(g.w)}" height="${n1(g.h)}" rx="14"></rect><text class="gr-glabel" x="${n1(g.x + 4)}" y="${n1(g.y - 7)}">${escapeHtml(g.label)}</text>`)
|
|
294
|
+
.join("");
|
|
295
|
+
const edges = layout.edges
|
|
296
|
+
.map((e, i) => {
|
|
297
|
+
const marker = e.kind === "undirected" ? "" : ` marker-end="url(#${arrow})"`;
|
|
298
|
+
const dash = e.kind === "dashed" ? " gr-dashed" : "";
|
|
299
|
+
const path = `<path class="gr-e gr-et${e.from} gr-et${e.to}${dash}" data-gr-e="${i}" d="${e.d}"${marker}></path>`;
|
|
300
|
+
if (e.label == null) {
|
|
301
|
+
return path;
|
|
302
|
+
}
|
|
303
|
+
return `${path}<text class="gr-elabel" x="${n1(e.labelX ?? 0)}" y="${n1(e.labelY ?? 0)}" text-anchor="${e.labelAnchor === "middle" ? "middle" : "start"}">${escapeHtml(e.label)}</text>`;
|
|
304
|
+
})
|
|
305
|
+
.join("");
|
|
306
|
+
const legend = layout.legendY == null
|
|
307
|
+
? ""
|
|
308
|
+
: `<text class="gr-legend" x="${GRAPH_LAYOUT_MARGIN}" y="${n1(layout.legendY)}">${escapeHtml(GRAPH_LEGEND)}</text>`;
|
|
309
|
+
const desc = `${b.graph.nodes.length} nodes, ${b.graph.edges.length} edges: ${b.graph.nodes
|
|
310
|
+
.map((n) => n.label)
|
|
311
|
+
.join(", ")}`;
|
|
312
|
+
const svg = [
|
|
313
|
+
`<svg xmlns="http://www.w3.org/2000/svg" class="${cls}" viewBox="0 0 ${layout.width} ${layout.height}" role="img">`,
|
|
314
|
+
`<title>${escapeHtml(b.alt ?? b.caption ?? "graph")}</title>`,
|
|
315
|
+
`<desc>${escapeHtml(desc)}</desc>`,
|
|
316
|
+
`<defs><marker id="${arrow}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" style="fill:context-stroke"></path></marker></defs>`,
|
|
317
|
+
`<style>${rules.join("\n")}</style>`,
|
|
318
|
+
frames,
|
|
319
|
+
edges,
|
|
320
|
+
layout.nodes.map(graphBoxHtml).join(""),
|
|
321
|
+
legend,
|
|
322
|
+
"</svg>",
|
|
323
|
+
].join("");
|
|
324
|
+
return `<figure class="fig"${b.alt ? ` aria-label="${escapeHtml(b.alt)}"` : ""}>${svg}${b.caption ? `<figcaption class="cap">${escapeHtml(b.caption)}</figcaption>` : ""}</figure>`;
|
|
325
|
+
};
|
|
326
|
+
const TERM_ICON = '<svg class="term-icon" viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 3h11v14H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Zm-2 12a2 2 0 0 1 2-2h11M7 10l2-5 2 5M8 8h2"/></svg>';
|
|
327
|
+
/** Shared dictionary entries for definitions and the Resources list. */
|
|
328
|
+
export const termsHtml = (terms, language, sortable = false) => {
|
|
329
|
+
const entries = terms
|
|
330
|
+
.map((entry, sourceOrder) => ({ ...entry, sourceOrder }))
|
|
331
|
+
.sort(termComparator(language));
|
|
332
|
+
return `<section class="terms" aria-label="Terms">${sortable ? `<div class="dict-toolbar"><span>${entries.length} terms</span><label>Sort <select data-dict-sort><option value="asc">Name A → Z</option><option value="desc">Name Z → A</option><option value="source">Source order</option></select></label></div>` : ""}<div class="dict">${entries
|
|
333
|
+
.map((e, i) => `<fieldset class="dict-card" data-source-order="${e.sourceOrder}" data-sort-order="${i}" data-term="${escapeHtml(e.id)}"><legend id="${termAnchor(e.id)}">${TERM_ICON}<a class="term-name" href="/_files/terms#${encodeURIComponent(termAnchor(e.id))}"${e.description ? ` aria-describedby="term-description-${encodeURIComponent(e.id)}"` : ""}>${escapeHtml(e.name)}${e.description ? `<span class="term-description" role="tooltip" id="term-description-${encodeURIComponent(e.id)}">${escapeHtml(e.description)}</span>` : ""}</a></legend><p class="term-definition">${escapeHtml(e.definition)}</p></fieldset>`)
|
|
334
|
+
.join("")}</div></section>`;
|
|
335
|
+
};
|
|
336
|
+
const blockHtml = (b, ctx, seg) => {
|
|
337
|
+
switch (b.type) {
|
|
338
|
+
case "themeVariants":
|
|
339
|
+
return `<div data-theme-variants><div data-theme-variant="">${blockHtml(b.default, ctx, seg)}</div>${Object.entries(b.variants)
|
|
340
|
+
.map(([theme, block]) => `<div data-theme-variant="${escapeHtml(theme)}" hidden>${blockHtml(block, ctx, seg)}</div>`)
|
|
341
|
+
.join("")}</div>`;
|
|
342
|
+
case "external": {
|
|
343
|
+
const url = new URL(b.url);
|
|
344
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password)
|
|
345
|
+
return `<p>${escapeHtml(b.url)}</p>`;
|
|
346
|
+
return `<div class="external-embed" data-embed-url="${escapeHtml(b.url)}"><a href="${escapeHtml(b.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(b.alt || b.url)}</a></div>`;
|
|
347
|
+
}
|
|
348
|
+
case "h":
|
|
349
|
+
// id = slug (a single-doc page, so no namespace is needed).
|
|
350
|
+
return `<h${b.level} id="${escapeHtml(b.slug)}">${segmentsHtml(b.text, ctx, seg)}${ctx.sectionUrl === undefined ? "" : `<button class="section-copy" type="button" data-section-url="${escapeHtml(ctx.sectionUrl)}#${encodeURIComponent(b.slug)}" title="Copy section URL" aria-label="Copy section URL" data-i18n-title="Copy section URL" data-i18n-aria-label="Copy section URL"><svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="m8 12 4-4m-5 2-2 2a3 3 0 0 0 4 4l2-2m-2-8 2-2a3 3 0 0 1 4 4l-2 2"/></svg>${CHECK_ICON}</button>`}</h${b.level}>`;
|
|
351
|
+
case "p":
|
|
352
|
+
return `<p>${segmentsHtml(b.text, ctx, seg)}</p>`;
|
|
353
|
+
case "quote":
|
|
354
|
+
return `<blockquote>${b.blocks.map((block) => blockHtml(block, ctx, seg)).join("\n")}</blockquote>`;
|
|
355
|
+
case "list":
|
|
356
|
+
return listHtml(b.items, ctx, seg, b.ordered === true);
|
|
357
|
+
case "code": {
|
|
358
|
+
if (b.lang === "mermaid" && !b.unavailable)
|
|
359
|
+
return `<pre data-mermaid="pending" aria-label="${escapeHtml(b.alt || "Mermaid diagram")}" style="overflow:auto;background:var(--doc-bg);color:var(--doc-fg);padding:1rem">${escapeHtml(b.lines.join("\n"))}</pre>`;
|
|
360
|
+
const filename = b.filename ? textfileName(b.filename) : "";
|
|
361
|
+
const lang = b.filename ? /^.+(\.[^.]+)$/.exec(filename)?.[1] : (b.lang ?? "text");
|
|
362
|
+
const langLabel = lang ? `<span class="codeblk-lang">${escapeHtml(lang)}</span>` : "";
|
|
363
|
+
const filenameLabel = filename
|
|
364
|
+
? `<span class="codeblk-filename">${escapeHtml(filename)}</span>`
|
|
365
|
+
: "";
|
|
366
|
+
const title = b.filename
|
|
367
|
+
? ` title="${escapeHtml(`${b.filename}${b.range ?? ""}`)}"${b.alt ? ` role="region" aria-label="${escapeHtml(b.alt)}"` : ""}`
|
|
368
|
+
: "";
|
|
369
|
+
if (b.filename && b.unavailable) {
|
|
370
|
+
const message = b.unavailable === "missing" ? "File not found" : "File could not be loaded";
|
|
371
|
+
return `<div class="codeblk textfile unavailable"${title}><span class="codeblk-head">${langLabel}${filenameLabel}</span><p class="textfile-unavailable" role="status">${b.alt ? `${escapeHtml(b.alt)} — ` : ""}<span data-i18n="${message}">${message}</span></p></div>`;
|
|
372
|
+
}
|
|
373
|
+
// Copy/fold behavior and border-label styling are shared by the page shell.
|
|
374
|
+
return `<div class="codeblk${b.filename ? " textfile" : ""}" data-lang="${escapeHtml(b.lang ?? "text")}"${title}><span class="codeblk-head">${langLabel}${filenameLabel}<button class="codecopy" type="button" title="copy" aria-label="copy code"><svg class="ic-copy" viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>${CHECK_ICON}</button><button class="codefold" type="button" aria-expanded="true" aria-label="Collapse code" title="Collapse code" data-i18n-aria-label="Collapse code" data-i18n-title="Collapse code"><svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 7.5 5 5 5-5"/></svg></button></span><pre><code>${escapeHtml(b.lines.join("\n"))}</code></pre></div>`;
|
|
375
|
+
}
|
|
376
|
+
case "table":
|
|
377
|
+
return tableHtml(b, ctx, seg);
|
|
378
|
+
case "callout": {
|
|
379
|
+
const title = b.title ? `<div class="callout-title">${escapeHtml(b.title)}</div>` : "";
|
|
380
|
+
const body = b.body
|
|
381
|
+
.map((it) => it.type === "p"
|
|
382
|
+
? `<p>${segmentsHtml(it.text, ctx, seg)}</p>`
|
|
383
|
+
: listHtml(it.items, ctx, seg, it.ordered === true))
|
|
384
|
+
.join("");
|
|
385
|
+
return `<aside class="callout"${b.alt ? ` aria-label="${escapeHtml(b.alt)}"` : ""}>${title}<div class="callout-body">${body}</div></aside>`;
|
|
386
|
+
}
|
|
387
|
+
case "figureRef": {
|
|
388
|
+
const fig = ctx.figures.get(b.name);
|
|
389
|
+
if (fig) {
|
|
390
|
+
if (b.alt !== undefined)
|
|
391
|
+
return `<figure class="fig">${figureFrameHtml(fig, "fig-frame", b.alt)}</figure>`;
|
|
392
|
+
return figureHtml(fig, true, b.caption);
|
|
393
|
+
}
|
|
394
|
+
return `<div class="ph">${escapeHtml(b.alt || `Fig. ${b.name}`)}<span> — not found in figures/</span></div>`;
|
|
395
|
+
}
|
|
396
|
+
case "term":
|
|
397
|
+
return termsHtml([b.entry], ctx.language);
|
|
398
|
+
case "figures":
|
|
399
|
+
// Figure list. The h2 doubles as the label, so captions omit it. h2 id = fig name
|
|
400
|
+
// (anchor).
|
|
401
|
+
return [...ctx.figures.values()]
|
|
402
|
+
.map((f) => `<h2 id="${escapeHtml(f.name)}">${escapeHtml(f.title ?? f.name)}</h2>${figureHtml(f, false)}`)
|
|
403
|
+
.join("\n");
|
|
404
|
+
case "schemaGraph":
|
|
405
|
+
// table DSL → ER diagram. The parsed model is turned into SVG with a
|
|
406
|
+
// deterministic layout, so every consumer gets the same picture (no references outside
|
|
407
|
+
// the data).
|
|
408
|
+
return schemaGraphHtml(b);
|
|
409
|
+
case "graph":
|
|
410
|
+
// node / edge DSL → relationship graph. Like schemaGraph: model → deterministic layout
|
|
411
|
+
// → SVG.
|
|
412
|
+
return graphHtml(b);
|
|
413
|
+
default:
|
|
414
|
+
// Unknown block — degrade to a placeholder
|
|
415
|
+
// instead of emitting "undefined".
|
|
416
|
+
return `<div class="ph">${escapeHtml(`Unknown block (${b.type})`)}<span> — docs viewer needs an update</span></div>`;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
export const blocksHtml = (blocks, ctx = emptyCtx) => {
|
|
420
|
+
const seg = inlineSegmenter({
|
|
421
|
+
terms: ctx.terms,
|
|
422
|
+
});
|
|
423
|
+
return blocks
|
|
424
|
+
.map((b) => blockHtml(b, ctx, seg))
|
|
425
|
+
.filter((h) => h !== "")
|
|
426
|
+
.join("\n");
|
|
427
|
+
};
|
|
428
|
+
// The viewer moves this navigation into the fixed document header.
|
|
429
|
+
const inPageTocHtml = (toc, title = "Contents") => {
|
|
430
|
+
if (toc.length === 0) {
|
|
431
|
+
return "";
|
|
432
|
+
}
|
|
433
|
+
const items = toc
|
|
434
|
+
.map((t) => `<li class="lvl${t.depth}"><a href="#${escapeHtml(t.slug)}" data-toc-target="${escapeHtml(t.slug)}" title="${escapeHtml(t.text)}">${escapeHtml(t.text)}</a></li>`)
|
|
435
|
+
.join("");
|
|
436
|
+
return `<nav class="in-toc" aria-label="${escapeHtml(title)}"><button class="toc-current" type="button" aria-expanded="false">${escapeHtml(toc[0]?.text ?? "")}</button><ul class="toc-options" popover>${items}</ul><input class="toc-seek" type="range" min="0" max="1000" step="1" value="0" aria-label="${escapeHtml(title)}" disabled></nav>`;
|
|
437
|
+
};
|
|
438
|
+
export const docHtml = (fm, blocks, errors, ctx = emptyCtx, toc = []) => {
|
|
439
|
+
const errBox = errors.length > 0
|
|
440
|
+
? `<div class="errors">${errors.map((e) => `<div>${escapeHtml(e)}</div>`).join("")}</div>`
|
|
441
|
+
: "";
|
|
442
|
+
const lang = ctx.lang ? ` lang="${escapeHtml(ctx.lang)}"` : "";
|
|
443
|
+
return `<article${lang}>${errBox}<h1>${escapeHtml(docDisplayName(fm, ""))}</h1>${inPageTocHtml(toc, ctx.tocTitle)}${blocksHtml(blocks, ctx)}</article>`;
|
|
444
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CHECK_ICON = "<svg class=\"ic-ok\" viewBox=\"0 0 20 20\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m4 10 4 4 8-8\"/></svg>";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CHECK_ICON = '<svg class="ic-ok" viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 10 4 4 8-8"/></svg>';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TermEntry } from "./content-types.js";
|
|
2
|
+
export type InlineSegment = {
|
|
3
|
+
kind: "text";
|
|
4
|
+
text: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "code";
|
|
7
|
+
text: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "strong" | "emphasis";
|
|
10
|
+
text: string;
|
|
11
|
+
children: InlineSegment[];
|
|
12
|
+
} | {
|
|
13
|
+
kind: "link";
|
|
14
|
+
text: string;
|
|
15
|
+
url: string;
|
|
16
|
+
children?: InlineSegment[];
|
|
17
|
+
} | {
|
|
18
|
+
kind: "term";
|
|
19
|
+
id: string;
|
|
20
|
+
text: string;
|
|
21
|
+
children?: InlineSegment[];
|
|
22
|
+
};
|
|
23
|
+
export declare const termMap: (terms: readonly TermEntry[]) => Map<string, TermEntry>;
|
|
24
|
+
export interface InlineContext {
|
|
25
|
+
terms?: readonly TermEntry[];
|
|
26
|
+
}
|
|
27
|
+
export declare const inlineSegmenter: (ctx?: InlineContext) => ((text: string) => InlineSegment[]);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { parseMdast } from "./markdown.js";
|
|
2
|
+
import { parseTermReference } from "./term-reference.js";
|
|
3
|
+
export const termMap = (terms) => new Map(terms.map((entry) => [entry.id, entry]));
|
|
4
|
+
export const inlineSegmenter = (ctx = {}) => {
|
|
5
|
+
const terms = termMap(ctx.terms ?? []);
|
|
6
|
+
const segments = (nodes) => nodes.flatMap((node) => {
|
|
7
|
+
if (node.type === "inlineCode")
|
|
8
|
+
return [{ kind: "code", text: node.value }];
|
|
9
|
+
if ("value" in node)
|
|
10
|
+
return [{ kind: "text", text: node.value }];
|
|
11
|
+
if (!("children" in node))
|
|
12
|
+
return [];
|
|
13
|
+
const children = segments(node.children);
|
|
14
|
+
const text = children.map((child) => child.text).join("");
|
|
15
|
+
if (node.type === "strong" || node.type === "emphasis")
|
|
16
|
+
return [{ kind: node.type, text, children }];
|
|
17
|
+
if (node.type === "link") {
|
|
18
|
+
let id;
|
|
19
|
+
try {
|
|
20
|
+
id = parseTermReference(node.url);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* The compiler reports invalid paths. */
|
|
24
|
+
}
|
|
25
|
+
const formatted = children.some((child) => child.kind !== "text")
|
|
26
|
+
? { children }
|
|
27
|
+
: {};
|
|
28
|
+
return [
|
|
29
|
+
id
|
|
30
|
+
? {
|
|
31
|
+
kind: "term",
|
|
32
|
+
id,
|
|
33
|
+
text: text || terms.get(id)?.name || node.url,
|
|
34
|
+
...formatted,
|
|
35
|
+
}
|
|
36
|
+
: { kind: "link", text, url: node.url, ...formatted },
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
return children;
|
|
40
|
+
});
|
|
41
|
+
return (text) => segments(parseMdast(text).children);
|
|
42
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { TermEntry } from "./content-types.js";
|
|
2
|
+
export declare const DEFAULT_LANG = "en";
|
|
3
|
+
export declare const isContentLanguage: (code: string) => boolean;
|
|
4
|
+
/** Normalize site language declarations and translation directory names consistently. */
|
|
5
|
+
export declare const contentLanguageCode: (tag: string) => string;
|
|
6
|
+
/** BCP 47-shaped tag (`en`, `ja`, `pt-BR`, `zh-Hant-TW`). Not checked against a registry. */
|
|
7
|
+
export declare const LANG_TAG_RE: RegExp;
|
|
8
|
+
export declare const isLangTag: (s: string) => boolean;
|
|
9
|
+
/** One prose-lint finding. range = 0-indexed [start, end) offsets into the linted body string. */
|
|
10
|
+
export interface LintFinding {
|
|
11
|
+
ruleId: string;
|
|
12
|
+
message: string;
|
|
13
|
+
range: readonly [number, number];
|
|
14
|
+
}
|
|
15
|
+
export interface LanguageAdapter {
|
|
16
|
+
/**
|
|
17
|
+
* BCP 47 tag this adapter handles. A doc tag matches exactly, else by its primary subtag
|
|
18
|
+
* (`ja-JP` → `ja`).
|
|
19
|
+
*/
|
|
20
|
+
readonly code: string;
|
|
21
|
+
/** Order of term listings. Default: Intl.Collator for code, by name. */
|
|
22
|
+
compareTerms?: (a: TermEntry, b: TermEntry) => number;
|
|
23
|
+
/** Language of the editor's browser spellcheck. Default: the doc's tag. */
|
|
24
|
+
spellcheckLang?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Prose lint shown in the editor (Node only; runs on the server, so it may load heavy
|
|
27
|
+
* dependencies lazily). Offsets are relative to the body string. Absent = no lint.
|
|
28
|
+
*/
|
|
29
|
+
lint?: (body: string) => Promise<LintFinding[]>;
|
|
30
|
+
}
|
|
31
|
+
/** Shape of `<docs root>/docs.config.mjs` (its default export). */
|
|
32
|
+
export interface DocsConfig {
|
|
33
|
+
/** Extra adapters. They take precedence over the built-in ones with the same code. */
|
|
34
|
+
languages?: LanguageAdapter[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Adapter for a doc tag: exact code match, then the primary subtag, else a generic adapter
|
|
38
|
+
* carrying the tag (so collation still follows the language via Intl).
|
|
39
|
+
*/
|
|
40
|
+
export declare const resolveLanguage: (lang: string | undefined, adapters?: readonly LanguageAdapter[]) => LanguageAdapter;
|
|
41
|
+
export declare const termComparator: (language: LanguageAdapter | undefined) => NonNullable<LanguageAdapter["compareTerms"]>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { iso6393, iso6393To1 } from "iso-639-3";
|
|
2
|
+
export const DEFAULT_LANG = "en";
|
|
3
|
+
const contentCodes = new Set(iso6393.map((language) => language.iso6393));
|
|
4
|
+
const from1 = new Map(Object.entries(iso6393To1).map(([three, two]) => [two, three]));
|
|
5
|
+
export const isContentLanguage = (code) => contentCodes.has(code);
|
|
6
|
+
/** Normalize site language declarations and translation directory names consistently. */
|
|
7
|
+
export const contentLanguageCode = (tag) => {
|
|
8
|
+
const primary = tag.toLowerCase().split("-")[0] ?? "";
|
|
9
|
+
const code = from1.get(primary) ?? primary;
|
|
10
|
+
if (!isContentLanguage(code))
|
|
11
|
+
throw new Error(`Unknown ISO 639-3 language: ${tag}`);
|
|
12
|
+
return code;
|
|
13
|
+
};
|
|
14
|
+
/** BCP 47-shaped tag (`en`, `ja`, `pt-BR`, `zh-Hant-TW`). Not checked against a registry. */
|
|
15
|
+
export const LANG_TAG_RE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
16
|
+
export const isLangTag = (s) => LANG_TAG_RE.test(s);
|
|
17
|
+
/**
|
|
18
|
+
* Adapter for a doc tag: exact code match, then the primary subtag, else a generic adapter
|
|
19
|
+
* carrying the tag (so collation still follows the language via Intl).
|
|
20
|
+
*/
|
|
21
|
+
export const resolveLanguage = (lang, adapters = []) => {
|
|
22
|
+
const tag = lang || DEFAULT_LANG;
|
|
23
|
+
const lower = tag.toLowerCase();
|
|
24
|
+
const code = lower.split("-")[0] ?? lower;
|
|
25
|
+
const primary = iso6393To1[code] ?? code;
|
|
26
|
+
return (adapters.find((a) => a.code.toLowerCase() === lower) ??
|
|
27
|
+
adapters.find((a) => a.code.toLowerCase() === primary) ?? { code: tag });
|
|
28
|
+
};
|
|
29
|
+
const collator = (code) => {
|
|
30
|
+
try {
|
|
31
|
+
return new Intl.Collator(code, { sensitivity: "base" });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// A tag Intl rejects falls back to the default language's order.
|
|
35
|
+
return new Intl.Collator(DEFAULT_LANG, { sensitivity: "base" });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
export const termComparator = (language) => {
|
|
39
|
+
if (language?.compareTerms) {
|
|
40
|
+
return language.compareTerms;
|
|
41
|
+
}
|
|
42
|
+
const c = collator(language?.code ?? DEFAULT_LANG);
|
|
43
|
+
return (a, b) => c.compare(a.name, b.name);
|
|
44
|
+
};
|