@dooc-dev/dooc 0.0.0-stage → 0.0.2

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 (221) hide show
  1. package/README.md +26 -2
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +311 -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/extension-package.d.ts +34 -0
  72. package/dist/core/extension-package.js +147 -0
  73. package/dist/core/figure-frame.d.ts +12 -0
  74. package/dist/core/figure-frame.js +108 -0
  75. package/dist/core/file-embed.d.ts +13 -0
  76. package/dist/core/file-embed.js +43 -0
  77. package/dist/core/files.d.ts +15 -0
  78. package/dist/core/files.js +59 -0
  79. package/dist/core/flow-base-css.generated.d.ts +1 -0
  80. package/dist/core/flow-base-css.generated.js +4 -0
  81. package/dist/core/flow-css.d.ts +9 -0
  82. package/dist/core/flow-css.js +54 -0
  83. package/dist/core/graph-common.d.ts +32 -0
  84. package/dist/core/graph-common.js +113 -0
  85. package/dist/core/graph.d.ts +117 -0
  86. package/dist/core/graph.js +480 -0
  87. package/dist/core/html.d.ts +29 -0
  88. package/dist/core/html.js +444 -0
  89. package/dist/core/icons.d.ts +2 -0
  90. package/dist/core/icons.js +2 -0
  91. package/dist/core/inline.d.ts +27 -0
  92. package/dist/core/inline.js +42 -0
  93. package/dist/core/language.d.ts +41 -0
  94. package/dist/core/language.js +44 -0
  95. package/dist/core/languages/ja.d.ts +2 -0
  96. package/dist/core/languages/ja.js +5 -0
  97. package/dist/core/markdown.d.ts +27 -0
  98. package/dist/core/markdown.js +154 -0
  99. package/dist/core/previewers.d.ts +37 -0
  100. package/dist/core/previewers.js +103 -0
  101. package/dist/core/routing.d.ts +20 -0
  102. package/dist/core/routing.js +64 -0
  103. package/dist/core/schema-comments.d.ts +14 -0
  104. package/dist/core/schema-comments.js +52 -0
  105. package/dist/core/schema-graph-flow.d.ts +9 -0
  106. package/dist/core/schema-graph-flow.js +379 -0
  107. package/dist/core/schema-graph-mount.d.ts +9 -0
  108. package/dist/core/schema-graph-mount.js +105 -0
  109. package/dist/core/schema-graph.d.ts +254 -0
  110. package/dist/core/schema-graph.js +981 -0
  111. package/dist/core/table-ui.d.ts +2 -0
  112. package/dist/core/table-ui.js +180 -0
  113. package/dist/core/taxonomy.d.ts +12 -0
  114. package/dist/core/taxonomy.js +49 -0
  115. package/dist/core/term-reference.d.ts +5 -0
  116. package/dist/core/term-reference.js +19 -0
  117. package/dist/core/terms-ui.d.ts +6 -0
  118. package/dist/core/terms-ui.js +22 -0
  119. package/dist/core/textfile.d.ts +10 -0
  120. package/dist/core/textfile.js +39 -0
  121. package/dist/core/theme-variants.d.ts +3 -0
  122. package/dist/core/theme-variants.js +37 -0
  123. package/dist/core/toc-ui.d.ts +2 -0
  124. package/dist/core/toc-ui.js +161 -0
  125. package/dist/core/video-ui.d.ts +2 -0
  126. package/dist/core/video-ui.js +253 -0
  127. package/dist/core/workspace.d.ts +15 -0
  128. package/dist/core/workspace.js +28 -0
  129. package/dist/edit.global.js +306 -0
  130. package/dist/flow.global.js +4959 -0
  131. package/dist/index.d.ts +17 -0
  132. package/dist/index.js +31 -0
  133. package/dist/mcp.d.ts +24 -0
  134. package/dist/mcp.js +467 -0
  135. package/dist/plugins/actions.d.ts +45 -0
  136. package/dist/plugins/actions.js +125 -0
  137. package/dist/plugins/blocks.d.ts +93 -0
  138. package/dist/plugins/blocks.js +100 -0
  139. package/dist/plugins/index.d.ts +4 -0
  140. package/dist/plugins/index.js +1 -0
  141. package/dist/plugins/local-storage.d.ts +18 -0
  142. package/dist/plugins/local-storage.js +115 -0
  143. package/dist/plugins/oauth.d.ts +14 -0
  144. package/dist/plugins/oauth.js +170 -0
  145. package/dist/plugins/runtime.d.ts +26 -0
  146. package/dist/plugins/runtime.js +395 -0
  147. package/dist/plugins/types.d.ts +59 -0
  148. package/dist/plugins/types.js +1 -0
  149. package/dist/plugins/view.d.ts +7 -0
  150. package/dist/plugins/view.js +21 -0
  151. package/dist/ui.global.js +78 -0
  152. package/dist/viewer/assets.d.ts +2 -0
  153. package/dist/viewer/assets.js +3 -0
  154. package/dist/viewer/build.d.ts +12 -0
  155. package/dist/viewer/build.js +55 -0
  156. package/dist/viewer/bundle-network.d.ts +2 -0
  157. package/dist/viewer/bundle-network.js +88 -0
  158. package/dist/viewer/code-file.d.ts +20 -0
  159. package/dist/viewer/code-file.js +42 -0
  160. package/dist/viewer/editor-tools.d.ts +38 -0
  161. package/dist/viewer/editor-tools.js +211 -0
  162. package/dist/viewer/extension-pack.d.ts +2 -0
  163. package/dist/viewer/extension-pack.js +36 -0
  164. package/dist/viewer/extensions.d.ts +10 -0
  165. package/dist/viewer/extensions.js +302 -0
  166. package/dist/viewer/file-references.d.ts +14 -0
  167. package/dist/viewer/file-references.js +38 -0
  168. package/dist/viewer/font-picker.d.ts +4 -0
  169. package/dist/viewer/font-picker.js +145 -0
  170. package/dist/viewer/font-settings.d.ts +11 -0
  171. package/dist/viewer/font-settings.js +50 -0
  172. package/dist/viewer/fonts/LICENSE.txt +119 -0
  173. package/dist/viewer/fonts/udev-gothic-regular.woff2 +0 -0
  174. package/dist/viewer/fonts.d.ts +2 -0
  175. package/dist/viewer/fonts.js +13 -0
  176. package/dist/viewer/git-diff.d.ts +3 -0
  177. package/dist/viewer/git-diff.js +33 -0
  178. package/dist/viewer/git-types.d.ts +73 -0
  179. package/dist/viewer/git-types.js +1 -0
  180. package/dist/viewer/git.d.ts +5 -0
  181. package/dist/viewer/git.js +337 -0
  182. package/dist/viewer/hmr.d.ts +53 -0
  183. package/dist/viewer/hmr.js +337 -0
  184. package/dist/viewer/i18n.d.ts +6 -0
  185. package/dist/viewer/i18n.js +65 -0
  186. package/dist/viewer/ja-lint.d.ts +3 -0
  187. package/dist/viewer/ja-lint.js +44 -0
  188. package/dist/viewer/language-picker.d.ts +2 -0
  189. package/dist/viewer/language-picker.js +233 -0
  190. package/dist/viewer/languages.d.ts +11 -0
  191. package/dist/viewer/languages.js +20 -0
  192. package/dist/viewer/locales/eng.json +317 -0
  193. package/dist/viewer/locales/jpn.json +317 -0
  194. package/dist/viewer/locales/zho.json +317 -0
  195. package/dist/viewer/nav-focus.d.ts +2 -0
  196. package/dist/viewer/nav-focus.js +64 -0
  197. package/dist/viewer/plugin-blocks.d.ts +3 -0
  198. package/dist/viewer/plugin-blocks.js +115 -0
  199. package/dist/viewer/plugin-config.d.ts +35 -0
  200. package/dist/viewer/plugin-config.js +57 -0
  201. package/dist/viewer/plugin-view.d.ts +2 -0
  202. package/dist/viewer/plugin-view.js +6 -0
  203. package/dist/viewer/plugins.d.ts +12 -0
  204. package/dist/viewer/plugins.js +267 -0
  205. package/dist/viewer/resize.d.ts +4 -0
  206. package/dist/viewer/resize.js +238 -0
  207. package/dist/viewer/resource-library.d.ts +29 -0
  208. package/dist/viewer/resource-library.js +104 -0
  209. package/dist/viewer/search.d.ts +16 -0
  210. package/dist/viewer/search.js +358 -0
  211. package/dist/viewer/serve.d.ts +210 -0
  212. package/dist/viewer/serve.js +3312 -0
  213. package/dist/viewer/site-icon.d.ts +8 -0
  214. package/dist/viewer/site-icon.js +56 -0
  215. package/dist/viewer/textfile.d.ts +9 -0
  216. package/dist/viewer/textfile.js +50 -0
  217. package/dist/viewer/theme-picker.d.ts +2 -0
  218. package/dist/viewer/theme-picker.js +135 -0
  219. package/dist/viewer/workspace.d.ts +4 -0
  220. package/dist/viewer/workspace.js +47 -0
  221. package/package.json +128 -5
@@ -0,0 +1,264 @@
1
+ // The content/ contract. Doc bodies sit flat in content/<id>.md, and only dooc.yaml
2
+ // holds the hierarchy and order (docs themselves have no root / child distinction; any doc can
3
+ // have children). id = the file name stem is the resolution key for URLs / file links and is
4
+ // unique site-wide. YAML reading/writing lives in compiler/manifest.ts; this file only does
5
+ // browser-safe tree validation and manipulation.
6
+ import { docFile, localFile, parseFileLayout } from "./files.js";
7
+ import { contentLanguageCode, DEFAULT_LANG, isLangTag } from "./language.js";
8
+ export const MANIFEST_FILE = "dooc.yaml";
9
+ export const DOC_ID_RE = /^[a-z0-9][a-z0-9-]*$/;
10
+ export const isDocId = (s) => DOC_ID_RE.test(s);
11
+ /** Doc file name directly under content/ (`<id>.md`) → id. undefined if the shape differs. */
12
+ export const docIdOfFile = (name) => {
13
+ const id = name.endsWith(".md") ? name.slice(0, -3) : "";
14
+ return isDocId(id) ? id : undefined;
15
+ };
16
+ /**
17
+ * Turn the parsed dooc.yaml (YAML → JS value) into a tree. Each element is `id` (no
18
+ * children) or `{ id: [children...] }` (a single-key map; a null value also means no children).
19
+ * Validates id shape and duplicates.
20
+ */
21
+ export const parseManifest = (value) => {
22
+ const errors = [];
23
+ let title;
24
+ let icon;
25
+ let defaultLang;
26
+ let files;
27
+ let languages;
28
+ let themes;
29
+ if (value != null && typeof value === "object" && !Array.isArray(value)) {
30
+ const manifest = value;
31
+ for (const key of Object.keys(manifest))
32
+ if (key !== "title" &&
33
+ key !== "icon" &&
34
+ key !== "defaultLang" &&
35
+ key !== "docs" &&
36
+ key !== "files" &&
37
+ key !== "languages" &&
38
+ key !== "themes")
39
+ errors.push(`${MANIFEST_FILE}: unknown field "${key}"`);
40
+ if (manifest.title !== undefined) {
41
+ if (typeof manifest.title !== "string" || !manifest.title.trim())
42
+ errors.push(`${MANIFEST_FILE}: title must be a non-empty string`);
43
+ else
44
+ title = manifest.title.trim();
45
+ }
46
+ if (manifest.icon !== undefined) {
47
+ if (typeof manifest.icon !== "string" ||
48
+ !localFile(manifest.icon.trim()) ||
49
+ !manifest.icon.trim().endsWith(".svg"))
50
+ errors.push(`${MANIFEST_FILE}: icon must be a local .svg file`);
51
+ else
52
+ icon = manifest.icon.trim();
53
+ }
54
+ if (manifest.defaultLang !== undefined) {
55
+ if (typeof manifest.defaultLang !== "string" || !isLangTag(manifest.defaultLang))
56
+ errors.push(`${MANIFEST_FILE}: defaultLang must be a BCP 47 language tag`);
57
+ else
58
+ defaultLang = manifest.defaultLang;
59
+ }
60
+ if (manifest.files !== undefined) {
61
+ try {
62
+ files = parseFileLayout(manifest.files);
63
+ }
64
+ catch (error) {
65
+ errors.push(`${MANIFEST_FILE}: ${error instanceof Error ? error.message : error}`);
66
+ }
67
+ }
68
+ if (manifest.languages !== undefined) {
69
+ try {
70
+ if (!Array.isArray(manifest.languages) ||
71
+ !manifest.languages.length ||
72
+ manifest.languages.some((tag) => typeof tag !== "string" || !isLangTag(tag)))
73
+ throw new Error("languages must be a non-empty list of language tags");
74
+ languages = manifest.languages.map(contentLanguageCode);
75
+ if (new Set(languages).size !== languages.length)
76
+ throw new Error("languages must contain distinct languages after normalization");
77
+ if (!languages.includes(contentLanguageCode(defaultLang ?? DEFAULT_LANG)))
78
+ throw new Error("languages must include defaultLang");
79
+ }
80
+ catch (error) {
81
+ errors.push(`${MANIFEST_FILE}: ${error instanceof Error ? error.message : error}`);
82
+ }
83
+ }
84
+ if (manifest.themes !== undefined) {
85
+ if (!Array.isArray(manifest.themes) ||
86
+ !manifest.themes.length ||
87
+ manifest.themes.some((theme) => theme !== "light" && theme !== "dark") ||
88
+ new Set(manifest.themes).size !== manifest.themes.length)
89
+ errors.push(`${MANIFEST_FILE}: themes must be a non-empty list of distinct light / dark themes supported by the viewer`);
90
+ else
91
+ themes = manifest.themes;
92
+ }
93
+ if (!Array.isArray(manifest.docs))
94
+ errors.push(`${MANIFEST_FILE}: docs must be a list`);
95
+ value = manifest.docs;
96
+ }
97
+ const seen = new Set();
98
+ const walk = (v, path) => {
99
+ if (v == null) {
100
+ return [];
101
+ }
102
+ if (!Array.isArray(v)) {
103
+ errors.push(`${MANIFEST_FILE}: ${path} must be a list`);
104
+ return [];
105
+ }
106
+ const nodes = [];
107
+ for (const [i, item] of v.entries()) {
108
+ const at = `${path}[${i}]`;
109
+ let id;
110
+ let children;
111
+ if (typeof item === "string") {
112
+ id = item;
113
+ }
114
+ else if (typeof item === "object" &&
115
+ item != null &&
116
+ !Array.isArray(item) &&
117
+ Object.keys(item).length === 1) {
118
+ [id, children] = Object.entries(item)[0] ?? [];
119
+ }
120
+ else {
121
+ errors.push(`${MANIFEST_FILE}: ${at} must be "id" or "id: [children...]"`);
122
+ continue;
123
+ }
124
+ if (typeof id !== "string" || !isDocId(id)) {
125
+ errors.push(`${MANIFEST_FILE}: ${at} id "${String(id)}" must be lowercase alphanumerics and hyphens`);
126
+ continue;
127
+ }
128
+ if (seen.has(id)) {
129
+ errors.push(`${MANIFEST_FILE}: duplicate id "${id}"`);
130
+ continue;
131
+ }
132
+ seen.add(id);
133
+ nodes.push({ id, children: walk(children, `${at}.${id}`) });
134
+ }
135
+ return nodes;
136
+ };
137
+ const tree = walk(value, "root");
138
+ if (files) {
139
+ for (const id of [...Object.keys(files.documents), ...Object.keys(files.icons ?? {})])
140
+ if (!seen.has(id))
141
+ errors.push(`${MANIFEST_FILE}: files references unknown document "${id}"`);
142
+ const paths = [...seen].map((id) => docFile(id, files));
143
+ if (new Set(paths).size !== paths.length)
144
+ errors.push(`${MANIFEST_FILE}: documents must use distinct files`);
145
+ }
146
+ return {
147
+ tree,
148
+ ...(title === undefined ? {} : { title }),
149
+ ...(icon === undefined ? {} : { icon }),
150
+ ...(defaultLang === undefined ? {} : { defaultLang }),
151
+ ...(files === undefined ? {} : { files }),
152
+ ...(languages === undefined ? {} : { languages }),
153
+ ...(themes === undefined ? {} : { themes }),
154
+ errors,
155
+ };
156
+ };
157
+ /** Tree → dooc.yaml text (serialized in the hand-written shape, keeping git diffs clean). */
158
+ export const serializeManifest = (tree, { title, icon, defaultLang, files, languages, themes, } = {}) => {
159
+ const write = (nodes, indent) => nodes.flatMap((n) => n.children.length > 0
160
+ ? [`${indent}- ${n.id}:`, ...write(n.children, `${indent} `)]
161
+ : [`${indent}- ${n.id}`]);
162
+ if (title !== undefined ||
163
+ icon !== undefined ||
164
+ defaultLang !== undefined ||
165
+ files !== undefined ||
166
+ languages !== undefined ||
167
+ themes !== undefined) {
168
+ const lines = write(tree, " ");
169
+ const metadata = `${title === undefined ? "" : `title: ${JSON.stringify(title)}\n`}${icon === undefined ? "" : `icon: ${JSON.stringify(icon)}\n`}${defaultLang === undefined ? "" : `defaultLang: ${JSON.stringify(defaultLang)}\n`}`;
170
+ return `${metadata}${files ? `files: ${JSON.stringify(files)}\n` : ""}${languages ? `languages: ${JSON.stringify(languages)}\n` : ""}${themes ? `themes: ${JSON.stringify(themes)}\n` : ""}docs:${lines.length ? `\n${lines.join("\n")}\n` : " []\n"}`;
171
+ }
172
+ const lines = write(tree, "");
173
+ return `# Doc hierarchy and order (each id = content/<id>.md)\n${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`;
174
+ };
175
+ /** Flatten the tree into display order (pre-order). parent omitted = top level. */
176
+ export const flattenTree = (tree) => {
177
+ const out = [];
178
+ const walk = (nodes, parent, depth) => {
179
+ for (const n of nodes) {
180
+ out.push(parent == null ? { id: n.id, depth } : { id: n.id, parent, depth });
181
+ walk(n.children, n.id, depth + 1);
182
+ }
183
+ };
184
+ walk(tree, undefined, 0);
185
+ return out;
186
+ };
187
+ const clone = (tree) => tree.map((n) => ({ id: n.id, children: clone(n.children) }));
188
+ const findNode = (tree, id) => {
189
+ for (const n of tree) {
190
+ if (n.id === id) {
191
+ return n;
192
+ }
193
+ const hit = findNode(n.children, id);
194
+ if (hit) {
195
+ return hit;
196
+ }
197
+ }
198
+ return undefined;
199
+ };
200
+ /** Detach the node with this id (along with its children). Returns the detached node. */
201
+ const detach = (tree, id) => {
202
+ const i = tree.findIndex((n) => n.id === id);
203
+ if (i !== -1) {
204
+ return tree.splice(i, 1)[0];
205
+ }
206
+ for (const n of tree) {
207
+ const hit = detach(n.children, id);
208
+ if (hit) {
209
+ return hit;
210
+ }
211
+ }
212
+ return undefined;
213
+ };
214
+ /** Child list of parent (omitted = top level). undefined if parent doesn't exist. */
215
+ const childList = (tree, parent) => parent == null ? tree : findNode(tree, parent)?.children;
216
+ /** Append a new doc to the end of parent's children. Duplicate id / missing parent → undefined. */
217
+ export const insertDoc = (tree, id, parent) => {
218
+ const next = clone(tree);
219
+ const list = childList(next, parent);
220
+ if (!list || findNode(next, id)) {
221
+ return undefined;
222
+ }
223
+ list.push({ id, children: [] });
224
+ return next;
225
+ };
226
+ /**
227
+ * Move a doc (with its children) to position index under parent. It can't be moved under
228
+ * itself or its own descendants (that would create a cycle). An invalid move returns undefined
229
+ * (no partial update).
230
+ */
231
+ export const moveDoc = (tree, id, parent, index) => {
232
+ const node = findNode(tree, id);
233
+ if (!node || (parent != null && (parent === id || findNode(node.children, parent)))) {
234
+ return undefined;
235
+ }
236
+ const next = clone(tree);
237
+ const moved = detach(next, id);
238
+ const list = childList(next, parent);
239
+ if (!moved || !list || !Number.isInteger(index) || index < 0 || index > list.length) {
240
+ return undefined;
241
+ }
242
+ list.splice(index, 0, moved);
243
+ return next;
244
+ };
245
+ /** Remove a doc. Docs with children are not removed (we don't decide where the children go). */
246
+ export const removeDoc = (tree, id) => {
247
+ const node = findNode(tree, id);
248
+ if (!node || node.children.length > 0) {
249
+ return undefined;
250
+ }
251
+ const next = clone(tree);
252
+ detach(next, id);
253
+ return next;
254
+ };
255
+ /** Change a doc's id (position and children stay as they are). */
256
+ export const renameDoc = (tree, oldId, newId) => {
257
+ const next = clone(tree);
258
+ const node = findNode(next, oldId);
259
+ if (!node || findNode(next, newId)) {
260
+ return undefined;
261
+ }
262
+ node.id = newId;
263
+ return next;
264
+ };
@@ -0,0 +1,45 @@
1
+ import type { Block, TocEntry } from "./blocks.js";
2
+ import type { Frontmatter } from "./taxonomy.js";
3
+ export interface TermEntry {
4
+ /** Root-relative .termth path, or a document position for an inline definition. */
5
+ id: string;
6
+ name: string;
7
+ description?: string;
8
+ /** Definition text (plain text). */
9
+ definition: string;
10
+ }
11
+ export interface FigureEntry {
12
+ name: string;
13
+ file: string;
14
+ kind: "svg" | "html" | "image" | "video";
15
+ /**
16
+ * Full figure content (SVG / HTML fragment / <img> data URI for rasters). Consumers render it
17
+ * inline; MCP can pass it as text.
18
+ */
19
+ markup: string;
20
+ /** By convention "Fig.<name>" (svg = <title>, html = `title:` in the meta comment). */
21
+ title?: string;
22
+ /** Figure description (used both for the displayed caption and as an explanation for LLMs). */
23
+ desc?: string;
24
+ }
25
+ export type CompiledDoc = Frontmatter & {
26
+ id: string;
27
+ /** Parent doc id (SoT: dooc.yaml). Omitted = top level. docs[] is in pre-order. */
28
+ parent?: string;
29
+ /** `content/<id>.md`. */
30
+ file: string;
31
+ /** Resolved language (BCP 47): front-matter `lang`, else the site default (language.ts). */
32
+ lang: string;
33
+ /** Sidebar icon (full content of icons/<id>.svg, 1:1). Omitted if there is none. */
34
+ icon?: string;
35
+ /** Source md (excluding front-matter). For MCP / copy; rendering uses blocks. */
36
+ body: string;
37
+ /** Normalized Block AST (the blocks.ts contract). Consumers just map it to rendering. */
38
+ blocks: Block[];
39
+ toc: TocEntry[];
40
+ /** Referenced figures and terms. */
41
+ refs: {
42
+ figures: string[];
43
+ terms: string[];
44
+ };
45
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ export declare const MAX_EXTENSION_BYTES: number;
2
+ export declare const MAX_EXTENSION_FILES = 256;
3
+ export interface ExtensionFile {
4
+ name: string;
5
+ data: Uint8Array;
6
+ }
7
+ export interface ExtensionPackage {
8
+ name: string;
9
+ version: string;
10
+ main: string;
11
+ icon?: {
12
+ light: string;
13
+ dark?: string;
14
+ };
15
+ files: ExtensionFile[];
16
+ }
17
+ export interface ExtensionSummary {
18
+ file: string;
19
+ name: string;
20
+ version: string;
21
+ hash: string;
22
+ size: number;
23
+ enabled: boolean;
24
+ plugins: string[];
25
+ icon?: ExtensionPackage["icon"];
26
+ error?: string;
27
+ readonly?: boolean;
28
+ }
29
+ export declare const extensionFile: (value: unknown) => string;
30
+ export declare const extensionPath: (value: string) => boolean;
31
+ export declare const extensionCrc: (data: Uint8Array) => number;
32
+ export declare const extensionManifest: (files: ExtensionFile[]) => ExtensionPackage;
33
+ /** Read bounded, single-disk ZIPs without extracting paths supplied by the archive. */
34
+ export declare const readExtensionZip: (bytes: Uint8Array, inflate: (data: Uint8Array, size: number) => Promise<Uint8Array>) => Promise<ExtensionPackage>;
@@ -0,0 +1,147 @@
1
+ export const MAX_EXTENSION_BYTES = 16 * 1024 * 1024;
2
+ export const MAX_EXTENSION_FILES = 256;
3
+ export const extensionFile = (value) => {
4
+ if (typeof value !== "string" || !/^[\w][\w. -]{0,119}\.zip$/.test(value))
5
+ throw new Error("Choose a plugin ZIP file");
6
+ return value;
7
+ };
8
+ export const extensionPath = (value) => !!value &&
9
+ value.length <= 240 &&
10
+ !/[\\:]/.test(value) &&
11
+ [...value].every((char) => char.charCodeAt(0) >= 32 && char.charCodeAt(0) !== 127) &&
12
+ value
13
+ .split("/")
14
+ .every((part) => !!part &&
15
+ part !== "." &&
16
+ part !== ".." &&
17
+ !part.endsWith(".") &&
18
+ !part.endsWith(" "));
19
+ const table = new Uint32Array(256).map((_, value) => {
20
+ let crc = value;
21
+ for (let bit = 0; bit < 8; bit++)
22
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
23
+ return crc >>> 0;
24
+ });
25
+ export const extensionCrc = (data) => {
26
+ let crc = 0xffffffff;
27
+ for (const byte of data)
28
+ crc = (crc >>> 8) ^ (table[(crc ^ byte) & 255] ?? 0);
29
+ return (crc ^ 0xffffffff) >>> 0;
30
+ };
31
+ export const extensionManifest = (files) => {
32
+ const entry = files.find((file) => file.name === "package.json");
33
+ if (!entry || entry.data.length > 65536)
34
+ throw new Error("The ZIP needs package.json at its root");
35
+ const manifest = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(entry.data));
36
+ if (!manifest ||
37
+ typeof manifest.name !== "string" ||
38
+ !/^(?:@[\w.-]+\/)?[\w.-]{1,100}$/.test(manifest.name) ||
39
+ typeof manifest.version !== "string" ||
40
+ !/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(manifest.version) ||
41
+ manifest.type !== "module" ||
42
+ typeof manifest.main !== "string" ||
43
+ !extensionPath(manifest.main) ||
44
+ !/\.m?js$/.test(manifest.main) ||
45
+ !files.some((file) => file.name === manifest.main))
46
+ throw new Error("package.json needs name, version, type: module and a bundled JavaScript main");
47
+ if (manifest.dependencies && Object.keys(manifest.dependencies).length)
48
+ throw new Error("Bundle dependencies into the JavaScript entry before packaging");
49
+ let icon;
50
+ if (manifest.icon !== undefined) {
51
+ const asset = files.find((file) => file.name === manifest.icon);
52
+ if (typeof manifest.icon !== "string" ||
53
+ !extensionPath(manifest.icon) ||
54
+ !manifest.icon.endsWith(".svg") ||
55
+ !asset ||
56
+ !asset.data.length ||
57
+ asset.data.length > 65536)
58
+ throw new Error("package.json icon must reference an SVG asset up to 64 KiB");
59
+ icon = {
60
+ light: `data:image/svg+xml;base64,${btoa(Array.from(asset.data, (byte) => String.fromCharCode(byte)).join(""))}`,
61
+ };
62
+ }
63
+ return { name: manifest.name, version: manifest.version, main: manifest.main, files, icon };
64
+ };
65
+ /** Read bounded, single-disk ZIPs without extracting paths supplied by the archive. */
66
+ export const readExtensionZip = async (bytes, inflate) => {
67
+ const invalid = () => new Error("Invalid or unsupported plugin ZIP");
68
+ if (bytes.length < 22 || bytes.length > MAX_EXTENSION_BYTES)
69
+ throw invalid();
70
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
71
+ let end = bytes.length - 22;
72
+ while (end >= Math.max(0, bytes.length - 65557) &&
73
+ (view.getUint32(end, true) !== 0x06054b50 ||
74
+ end + 22 + view.getUint16(end + 20, true) !== bytes.length))
75
+ end--;
76
+ if (end < Math.max(0, bytes.length - 65557) ||
77
+ view.getUint16(end + 4, true) ||
78
+ view.getUint16(end + 6, true))
79
+ throw invalid();
80
+ const count = view.getUint16(end + 10, true);
81
+ const directory = view.getUint32(end + 16, true);
82
+ if (!count ||
83
+ count > MAX_EXTENSION_FILES ||
84
+ view.getUint16(end + 8, true) !== count ||
85
+ directory + view.getUint32(end + 12, true) !== end)
86
+ throw invalid();
87
+ const files = [];
88
+ const names = new Set();
89
+ const ranges = [];
90
+ let at = directory, total = 0;
91
+ for (let index = 0; index < count; index++) {
92
+ if (at + 46 > end || view.getUint32(at, true) !== 0x02014b50)
93
+ throw invalid();
94
+ const flags = view.getUint16(at + 8, true), method = view.getUint16(at + 10, true);
95
+ const compressed = view.getUint32(at + 20, true), size = view.getUint32(at + 24, true);
96
+ const length = view.getUint16(at + 28, true), extra = view.getUint16(at + 30, true), comment = view.getUint16(at + 32, true);
97
+ const offset = view.getUint32(at + 42, true), type = (view.getUint32(at + 38, true) >>> 16) & 0xf000;
98
+ total += size;
99
+ if (flags & ~0x080e ||
100
+ ![0, 8].includes(method) ||
101
+ (method === 0 && size !== compressed) ||
102
+ total > MAX_EXTENSION_BYTES ||
103
+ at + 46 + length + extra + comment > end ||
104
+ offset + 30 > directory ||
105
+ view.getUint16(at + 34, true) ||
106
+ ![0, 0x8000, 0x4000].includes(type))
107
+ throw invalid();
108
+ const name = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(at + 46, at + 46 + length));
109
+ const folder = name.endsWith("/");
110
+ const path = folder ? name.slice(0, -1) : name;
111
+ const key = path.normalize("NFC").toLowerCase();
112
+ if (!extensionPath(path) ||
113
+ names.has(key) ||
114
+ (folder && size !== 0) ||
115
+ (!folder && type === 0x4000))
116
+ throw invalid();
117
+ names.add(key);
118
+ if (view.getUint32(offset, true) !== 0x04034b50 ||
119
+ view.getUint16(offset + 6, true) !== flags ||
120
+ view.getUint16(offset + 8, true) !== method)
121
+ throw invalid();
122
+ const localName = view.getUint16(offset + 26, true), localExtra = view.getUint16(offset + 28, true);
123
+ const start = offset + 30 + localName + localExtra;
124
+ if (start + compressed > directory ||
125
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(offset + 30, offset + 30 + localName)) !== name)
126
+ throw invalid();
127
+ if (ranges.some(([from, to]) => offset < to && start + compressed > from))
128
+ throw invalid();
129
+ ranges.push([offset, start + compressed]);
130
+ const input = bytes.subarray(start, start + compressed);
131
+ const data = method === 0 ? input.slice() : await inflate(input, size);
132
+ if (data.length !== size || extensionCrc(data) !== view.getUint32(at + 16, true))
133
+ throw invalid();
134
+ if (!folder)
135
+ files.push({ name, data });
136
+ at += 46 + length + extra + comment;
137
+ }
138
+ if (at !== end)
139
+ throw invalid();
140
+ for (const file of files)
141
+ if (files.some((other) => file.name
142
+ .normalize("NFC")
143
+ .toLowerCase()
144
+ .startsWith(`${other.name.normalize("NFC").toLowerCase()}/`)))
145
+ throw invalid();
146
+ return extensionManifest(files);
147
+ };
@@ -0,0 +1,12 @@
1
+ import type { FigureEntry } from "./content-types.js";
2
+ export type FigureTheme = "auto" | "light" | "dark";
3
+ export declare const figureSrcdoc: (fig: FigureEntry, theme?: FigureTheme) => string;
4
+ /** One figure → an isolated iframe (rasters stay as <img>). */
5
+ export declare const figureFrameHtml: (fig: FigureEntry, cls?: string, label?: string) => string;
6
+ /**
7
+ * Parent side of figure iframes: theme following (rewrites the srcdoc's data-doc-theme when
8
+ * <html data-theme> or the OS setting changes) and applying html figure heights. Also works for
9
+ * iframes inserted later (e.g. when edit swaps out the main content). Self-contained (it is
10
+ * stringified with toString and embedded in the page) — do not reference outside identifiers.
11
+ */
12
+ export declare function mountFigureFrames(doc: Document): void;
@@ -0,0 +1,108 @@
1
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2
+ const BASE_STYLE = "<style>html,body{margin:0;padding:0;height:100%;background:transparent;overflow:hidden}" +
3
+ 'html{color-scheme:light dark}html[data-doc-theme="light"]{color-scheme:light}html[data-doc-theme="dark"]{color-scheme:dark}' +
4
+ 'body{font:14px/1.6 system-ui,-apple-system,"Segoe UI","Noto Sans JP",sans-serif}' +
5
+ "body>svg{display:block;width:100%;height:100%}</style>";
6
+ // html figures only: report the content height to the parent (the parent finds its own iframe
7
+ // via event.source).
8
+ const HEIGHT_SCRIPT = '<script>(()=>{const s=()=>parent.postMessage({docFigureHeight:Math.ceil(document.body.scrollHeight)},"*");' +
9
+ 'document.documentElement.style.height="auto";document.body.style.height="auto";' +
10
+ "new ResizeObserver(s).observe(document.body);s();})()</script>";
11
+ export const figureSrcdoc = (fig, theme = "auto") => `<!doctype html><html data-doc-theme="${theme}"><head><meta charset="utf-8">${BASE_STYLE}</head><body>${fig.markup}${fig.kind === "html" ? HEIGHT_SCRIPT : ""}</body></html>`;
12
+ /**
13
+ * Display width of the svg root (width attribute, or the viewBox width if absent) and its aspect
14
+ * ratio. undefined if they can't be read.
15
+ */
16
+ const svgBox = (markup) => {
17
+ const root = /<svg\b[^>]*>/i.exec(markup)?.[0] ?? "";
18
+ const attr = (n) => new RegExp(`\\s${n}\\s*=\\s*["']([^"']*)["']`, "i").exec(root)?.[1];
19
+ const vb = attr("viewBox")
20
+ ?.trim()
21
+ .split(/[\s,]+/)
22
+ .map(Number);
23
+ const [w, h] = vb && vb.length === 4
24
+ ? [vb[2], vb[3]]
25
+ : [Number.parseFloat(attr("width") ?? ""), Number.parseFloat(attr("height") ?? "")];
26
+ if (!w || !h || !(w > 0) || !(h > 0)) {
27
+ return undefined;
28
+ }
29
+ const px = Number.parseFloat(attr("width") ?? "");
30
+ return { width: px > 0 && !/%/.test(attr("width") ?? "") ? px : w, w, h };
31
+ };
32
+ /** One figure → an isolated iframe (rasters stay as <img>). */
33
+ export const figureFrameHtml = (fig, cls = "fig-frame", label) => {
34
+ if (fig.kind === "video")
35
+ return label
36
+ ? fig.markup.replace(/\baria-label="[^"]*"/, () => `aria-label="${esc(label)}"`)
37
+ : fig.markup;
38
+ if (fig.kind === "image") {
39
+ return label === undefined
40
+ ? fig.markup
41
+ : fig.markup.replace(/\balt="[^"]*"/, () => `alt="${esc(label)}"`);
42
+ }
43
+ const box = fig.kind === "svg" ? svgBox(fig.markup) : undefined;
44
+ const style = box
45
+ ? `width:${box.width}px;aspect-ratio:${box.w} / ${box.h}`
46
+ : fig.kind === "svg"
47
+ ? "width:100%;height:240px"
48
+ : "width:100%;height:120px";
49
+ return `<iframe class="${cls}" data-doc-figure-frame="${fig.kind}" sandbox="${fig.kind === "html" ? "allow-scripts" : ""}" scrolling="no" loading="lazy" title="${esc(label || fig.title || `Fig.${fig.name}`)}" style="${style}" srcdoc="${esc(figureSrcdoc(fig))}"></iframe>`;
50
+ };
51
+ /**
52
+ * Parent side of figure iframes: theme following (rewrites the srcdoc's data-doc-theme when
53
+ * <html data-theme> or the OS setting changes) and applying html figure heights. Also works for
54
+ * iframes inserted later (e.g. when edit swaps out the main content). Self-contained (it is
55
+ * stringified with toString and embedded in the page) — do not reference outside identifiers.
56
+ */
57
+ export function mountFigureFrames(doc) {
58
+ const SEL = "iframe[data-doc-figure-frame]";
59
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
60
+ const theme = (element) => {
61
+ const t = element.closest("[data-theme]")?.dataset.theme ??
62
+ doc.documentElement.dataset.theme;
63
+ return t === "light" || t === "dark" ? t : media.matches ? "dark" : "light";
64
+ };
65
+ const apply = (f) => {
66
+ const src = f.getAttribute("srcdoc") ?? "";
67
+ const next = src.replace(/^(<!doctype html><html data-doc-theme=")[a-z]+"/, `$1${theme(f)}"`);
68
+ if (next !== src) {
69
+ f.setAttribute("srcdoc", next);
70
+ }
71
+ };
72
+ const all = () => {
73
+ for (const f of doc.querySelectorAll(SEL))
74
+ apply(f);
75
+ };
76
+ new MutationObserver((records) => {
77
+ for (const r of records) {
78
+ if (r.type === "attributes") {
79
+ all();
80
+ return;
81
+ }
82
+ for (const n of r.addedNodes) {
83
+ if (!(n instanceof Element))
84
+ continue;
85
+ if (n.matches(SEL))
86
+ apply(n);
87
+ for (const f of n.querySelectorAll(SEL))
88
+ apply(f);
89
+ }
90
+ }
91
+ }).observe(doc.documentElement, {
92
+ attributes: true,
93
+ attributeFilter: ["data-theme"],
94
+ childList: true,
95
+ subtree: true,
96
+ });
97
+ media.addEventListener("change", all);
98
+ window.addEventListener("message", (ev) => {
99
+ const h = ev.data?.docFigureHeight;
100
+ if (typeof h !== "number")
101
+ return;
102
+ for (const f of doc.querySelectorAll(`${SEL}[data-doc-figure-frame="html"]`)) {
103
+ if (f.contentWindow === ev.source)
104
+ f.style.height = `${h}px`;
105
+ }
106
+ });
107
+ all();
108
+ }
@@ -0,0 +1,13 @@
1
+ import { type PreviewSettings } from "./previewers.js";
2
+ /** Local visual links share the same asset catalog as image embeds. */
3
+ export declare const parseFigureLink: (source: string) => {
4
+ file: string;
5
+ fragment: string;
6
+ } | undefined;
7
+ /** Line fragments show source code; visual assets use the existing figures/ catalog. */
8
+ export declare const parseFileEmbed: (source: string, settings?: PreviewSettings) => {
9
+ file: string;
10
+ range?: string;
11
+ tableMode?: "header";
12
+ kind: "textfile" | "figure" | "html" | "external";
13
+ };
@@ -0,0 +1,43 @@
1
+ import { localFile } from "./files.js";
2
+ import { resolvePreviewer } from "./previewers.js";
3
+ import { parseFileReference } from "./textfile.js";
4
+ /** Local visual links share the same asset catalog as image embeds. */
5
+ export const parseFigureLink = (source) => {
6
+ if (/^(?:[a-z][a-z\d+.-]*:|\/)/i.test(source))
7
+ return undefined;
8
+ const path = decodeURIComponent(source.split(/[?#]/)[0] ?? "");
9
+ if (!/\.(svg|html|png|webp|gif|mp4|webm|jpe?g)$/.test(path))
10
+ return undefined;
11
+ const ref = parseFileReference(source);
12
+ const file = ref.file.replace(/^(\.\/)+/, "");
13
+ if (!localFile(file))
14
+ throw new Error("Figure links require a file inside the docs root");
15
+ return { file, fragment: ref.range?.slice(1) ?? "" };
16
+ };
17
+ /** Line fragments show source code; visual assets use the existing figures/ catalog. */
18
+ export const parseFileEmbed = (source, settings) => {
19
+ if (/^https?:\/\//i.test(source)) {
20
+ const url = new URL(source);
21
+ if (url.username || url.password)
22
+ throw new Error("Embed URLs must not contain credentials");
23
+ return { file: source, kind: "external" };
24
+ }
25
+ const ref = parseFileReference(source);
26
+ const previewer = resolvePreviewer(source, settings);
27
+ if (ref.range === "#H") {
28
+ if (previewer !== "table")
29
+ throw new Error("#H requires a CSV or TSV file");
30
+ return {
31
+ file: ref.file,
32
+ tableMode: "header",
33
+ kind: "textfile",
34
+ };
35
+ }
36
+ if (ref.range !== undefined ||
37
+ (previewer !== "image" && previewer !== "html" && previewer !== "video"))
38
+ return { ...ref, kind: "textfile" };
39
+ const file = ref.file.replace(/^(\.\/)+/, "");
40
+ if (!localFile(file))
41
+ throw new Error("Image and HTML previews require a local file inside the docs root");
42
+ return { file, kind: previewer === "html" ? "html" : "figure" };
43
+ };