@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.
Files changed (213) hide show
  1. package/README.md +26 -2
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +244 -0
  4. package/dist/code.global.js +285 -0
  5. package/dist/compiler/bundle-urls.d.ts +3 -0
  6. package/dist/compiler/bundle-urls.js +294 -0
  7. package/dist/compiler/bundle.d.ts +2 -0
  8. package/dist/compiler/bundle.js +100 -0
  9. package/dist/compiler/check-prose.d.ts +11 -0
  10. package/dist/compiler/check-prose.js +138 -0
  11. package/dist/compiler/check-types.d.ts +44 -0
  12. package/dist/compiler/check-types.js +1 -0
  13. package/dist/compiler/check.d.ts +5 -0
  14. package/dist/compiler/check.js +450 -0
  15. package/dist/compiler/code-blocks.d.ts +17 -0
  16. package/dist/compiler/code-blocks.js +198 -0
  17. package/dist/compiler/code-languages.d.ts +10 -0
  18. package/dist/compiler/code-languages.js +109 -0
  19. package/dist/compiler/compile.d.ts +95 -0
  20. package/dist/compiler/compile.js +410 -0
  21. package/dist/compiler/config.d.ts +7 -0
  22. package/dist/compiler/config.js +52 -0
  23. package/dist/compiler/dialect.d.ts +24 -0
  24. package/dist/compiler/dialect.js +198 -0
  25. package/dist/compiler/drawio.d.ts +26 -0
  26. package/dist/compiler/drawio.js +176 -0
  27. package/dist/compiler/figures.d.ts +14 -0
  28. package/dist/compiler/figures.js +221 -0
  29. package/dist/compiler/file-cache.d.ts +14 -0
  30. package/dist/compiler/file-cache.js +61 -0
  31. package/dist/compiler/files.d.ts +29 -0
  32. package/dist/compiler/files.js +431 -0
  33. package/dist/compiler/fonts.d.ts +10 -0
  34. package/dist/compiler/fonts.js +54 -0
  35. package/dist/compiler/format.d.ts +1 -0
  36. package/dist/compiler/format.js +42 -0
  37. package/dist/compiler/from-mdast.d.ts +16 -0
  38. package/dist/compiler/from-mdast.js +142 -0
  39. package/dist/compiler/icons.d.ts +14 -0
  40. package/dist/compiler/icons.js +85 -0
  41. package/dist/compiler/language-codes.d.ts +7 -0
  42. package/dist/compiler/language-codes.js +17 -0
  43. package/dist/compiler/locales.d.ts +7 -0
  44. package/dist/compiler/locales.js +67 -0
  45. package/dist/compiler/manifest.d.ts +6 -0
  46. package/dist/compiler/manifest.js +30 -0
  47. package/dist/compiler/ops.d.ts +44 -0
  48. package/dist/compiler/ops.js +362 -0
  49. package/dist/compiler/public-file.d.ts +2 -0
  50. package/dist/compiler/public-file.js +93 -0
  51. package/dist/compiler/serialize.d.ts +3 -0
  52. package/dist/compiler/serialize.js +16 -0
  53. package/dist/compiler/terms.d.ts +9 -0
  54. package/dist/compiler/terms.js +44 -0
  55. package/dist/compiler/textfile-remote.d.ts +12 -0
  56. package/dist/compiler/textfile-remote.js +124 -0
  57. package/dist/compiler/textfile.d.ts +9 -0
  58. package/dist/compiler/textfile.js +113 -0
  59. package/dist/compiler/theme-variants.d.ts +4 -0
  60. package/dist/compiler/theme-variants.js +22 -0
  61. package/dist/compiler/zip.d.ts +6 -0
  62. package/dist/compiler/zip.js +48 -0
  63. package/dist/core/blocks.d.ts +99 -0
  64. package/dist/core/blocks.js +29 -0
  65. package/dist/core/code-file.d.ts +12 -0
  66. package/dist/core/code-file.js +68 -0
  67. package/dist/core/content-metadata.d.ts +48 -0
  68. package/dist/core/content-metadata.js +264 -0
  69. package/dist/core/content-types.d.ts +45 -0
  70. package/dist/core/content-types.js +1 -0
  71. package/dist/core/figure-frame.d.ts +12 -0
  72. package/dist/core/figure-frame.js +108 -0
  73. package/dist/core/file-embed.d.ts +13 -0
  74. package/dist/core/file-embed.js +43 -0
  75. package/dist/core/files.d.ts +15 -0
  76. package/dist/core/files.js +59 -0
  77. package/dist/core/flow-base-css.generated.d.ts +1 -0
  78. package/dist/core/flow-base-css.generated.js +4 -0
  79. package/dist/core/flow-css.d.ts +9 -0
  80. package/dist/core/flow-css.js +54 -0
  81. package/dist/core/graph-common.d.ts +32 -0
  82. package/dist/core/graph-common.js +113 -0
  83. package/dist/core/graph.d.ts +117 -0
  84. package/dist/core/graph.js +480 -0
  85. package/dist/core/html.d.ts +29 -0
  86. package/dist/core/html.js +444 -0
  87. package/dist/core/icons.d.ts +1 -0
  88. package/dist/core/icons.js +1 -0
  89. package/dist/core/inline.d.ts +27 -0
  90. package/dist/core/inline.js +42 -0
  91. package/dist/core/language.d.ts +41 -0
  92. package/dist/core/language.js +44 -0
  93. package/dist/core/languages/ja.d.ts +2 -0
  94. package/dist/core/languages/ja.js +5 -0
  95. package/dist/core/markdown.d.ts +27 -0
  96. package/dist/core/markdown.js +154 -0
  97. package/dist/core/previewers.d.ts +37 -0
  98. package/dist/core/previewers.js +103 -0
  99. package/dist/core/routing.d.ts +20 -0
  100. package/dist/core/routing.js +64 -0
  101. package/dist/core/schema-comments.d.ts +14 -0
  102. package/dist/core/schema-comments.js +52 -0
  103. package/dist/core/schema-graph-flow.d.ts +9 -0
  104. package/dist/core/schema-graph-flow.js +379 -0
  105. package/dist/core/schema-graph-mount.d.ts +9 -0
  106. package/dist/core/schema-graph-mount.js +105 -0
  107. package/dist/core/schema-graph.d.ts +254 -0
  108. package/dist/core/schema-graph.js +981 -0
  109. package/dist/core/table-ui.d.ts +2 -0
  110. package/dist/core/table-ui.js +180 -0
  111. package/dist/core/taxonomy.d.ts +12 -0
  112. package/dist/core/taxonomy.js +49 -0
  113. package/dist/core/term-reference.d.ts +5 -0
  114. package/dist/core/term-reference.js +19 -0
  115. package/dist/core/terms-ui.d.ts +6 -0
  116. package/dist/core/terms-ui.js +22 -0
  117. package/dist/core/textfile.d.ts +10 -0
  118. package/dist/core/textfile.js +39 -0
  119. package/dist/core/theme-variants.d.ts +3 -0
  120. package/dist/core/theme-variants.js +37 -0
  121. package/dist/core/toc-ui.d.ts +2 -0
  122. package/dist/core/toc-ui.js +161 -0
  123. package/dist/core/video-ui.d.ts +2 -0
  124. package/dist/core/video-ui.js +253 -0
  125. package/dist/core/workspace.d.ts +15 -0
  126. package/dist/core/workspace.js +28 -0
  127. package/dist/edit.global.js +305 -0
  128. package/dist/flow.global.js +4959 -0
  129. package/dist/index.d.ts +17 -0
  130. package/dist/index.js +31 -0
  131. package/dist/mcp.d.ts +24 -0
  132. package/dist/mcp.js +467 -0
  133. package/dist/plugins/actions.d.ts +45 -0
  134. package/dist/plugins/actions.js +125 -0
  135. package/dist/plugins/blocks.d.ts +92 -0
  136. package/dist/plugins/blocks.js +99 -0
  137. package/dist/plugins/index.d.ts +4 -0
  138. package/dist/plugins/index.js +1 -0
  139. package/dist/plugins/oauth.d.ts +14 -0
  140. package/dist/plugins/oauth.js +164 -0
  141. package/dist/plugins/runtime.d.ts +24 -0
  142. package/dist/plugins/runtime.js +339 -0
  143. package/dist/plugins/types.d.ts +56 -0
  144. package/dist/plugins/types.js +1 -0
  145. package/dist/plugins/view.d.ts +29 -0
  146. package/dist/plugins/view.js +60 -0
  147. package/dist/ui.global.js +77 -0
  148. package/dist/viewer/assets.d.ts +2 -0
  149. package/dist/viewer/assets.js +3 -0
  150. package/dist/viewer/build.d.ts +12 -0
  151. package/dist/viewer/build.js +55 -0
  152. package/dist/viewer/bundle-network.d.ts +2 -0
  153. package/dist/viewer/bundle-network.js +88 -0
  154. package/dist/viewer/code-file.d.ts +20 -0
  155. package/dist/viewer/code-file.js +42 -0
  156. package/dist/viewer/editor-tools.d.ts +38 -0
  157. package/dist/viewer/editor-tools.js +211 -0
  158. package/dist/viewer/file-references.d.ts +14 -0
  159. package/dist/viewer/file-references.js +38 -0
  160. package/dist/viewer/font-picker.d.ts +4 -0
  161. package/dist/viewer/font-picker.js +145 -0
  162. package/dist/viewer/font-settings.d.ts +11 -0
  163. package/dist/viewer/font-settings.js +50 -0
  164. package/dist/viewer/fonts/LICENSE.txt +119 -0
  165. package/dist/viewer/fonts/udev-gothic-regular.woff2 +0 -0
  166. package/dist/viewer/fonts.d.ts +2 -0
  167. package/dist/viewer/fonts.js +13 -0
  168. package/dist/viewer/git-diff.d.ts +3 -0
  169. package/dist/viewer/git-diff.js +33 -0
  170. package/dist/viewer/git-types.d.ts +73 -0
  171. package/dist/viewer/git-types.js +1 -0
  172. package/dist/viewer/git.d.ts +5 -0
  173. package/dist/viewer/git.js +337 -0
  174. package/dist/viewer/hmr.d.ts +53 -0
  175. package/dist/viewer/hmr.js +335 -0
  176. package/dist/viewer/i18n.d.ts +6 -0
  177. package/dist/viewer/i18n.js +65 -0
  178. package/dist/viewer/ja-lint.d.ts +3 -0
  179. package/dist/viewer/ja-lint.js +44 -0
  180. package/dist/viewer/language-picker.d.ts +2 -0
  181. package/dist/viewer/language-picker.js +233 -0
  182. package/dist/viewer/languages.d.ts +11 -0
  183. package/dist/viewer/languages.js +20 -0
  184. package/dist/viewer/locales/eng.json +294 -0
  185. package/dist/viewer/locales/jpn.json +294 -0
  186. package/dist/viewer/locales/zho.json +294 -0
  187. package/dist/viewer/nav-focus.d.ts +2 -0
  188. package/dist/viewer/nav-focus.js +64 -0
  189. package/dist/viewer/plugin-blocks.d.ts +3 -0
  190. package/dist/viewer/plugin-blocks.js +115 -0
  191. package/dist/viewer/plugin-config.d.ts +35 -0
  192. package/dist/viewer/plugin-config.js +57 -0
  193. package/dist/viewer/plugin-view.d.ts +3 -0
  194. package/dist/viewer/plugin-view.js +28 -0
  195. package/dist/viewer/plugins.d.ts +8 -0
  196. package/dist/viewer/plugins.js +214 -0
  197. package/dist/viewer/resize.d.ts +4 -0
  198. package/dist/viewer/resize.js +238 -0
  199. package/dist/viewer/resource-library.d.ts +29 -0
  200. package/dist/viewer/resource-library.js +104 -0
  201. package/dist/viewer/search.d.ts +16 -0
  202. package/dist/viewer/search.js +358 -0
  203. package/dist/viewer/serve.d.ts +206 -0
  204. package/dist/viewer/serve.js +3233 -0
  205. package/dist/viewer/site-icon.d.ts +8 -0
  206. package/dist/viewer/site-icon.js +56 -0
  207. package/dist/viewer/textfile.d.ts +9 -0
  208. package/dist/viewer/textfile.js +50 -0
  209. package/dist/viewer/theme-picker.d.ts +2 -0
  210. package/dist/viewer/theme-picker.js +135 -0
  211. package/dist/viewer/workspace.d.ts +4 -0
  212. package/dist/viewer/workspace.js +47 -0
  213. 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
+ };
@@ -0,0 +1,2 @@
1
+ import type { LanguageAdapter } from "../language.js";
2
+ export declare const ja: LanguageAdapter;
@@ -0,0 +1,5 @@
1
+ export const ja = {
2
+ code: "ja",
3
+ // Browsers do not spellcheck Japanese; check embedded English words instead.
4
+ spellcheckLang: "en",
5
+ };