@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,450 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { flattenTree, MANIFEST_FILE } from "../core/content-metadata.js";
5
+ import { docFile, localFile } from "../core/files.js";
6
+ import { DEFAULT_LANG } from "../core/language.js";
7
+ import { contentLinks, embeddedImages, parseMdast } from "../core/markdown.js";
8
+ import { parseDocLink } from "../core/routing.js";
9
+ import { themeVariantFile } from "../core/theme-variants.js";
10
+ import { assetProse, checkProse, markdownProse } from "./check-prose.js";
11
+ import { CHECK_KINDS, } from "./check-types.js";
12
+ import { parseContentSource } from "./code-blocks.js";
13
+ import { compileSite } from "./compile.js";
14
+ import { collectRefs, parseDoc } from "./dialect.js";
15
+ import { contentLanguageCode, isContentLanguage } from "./language-codes.js";
16
+ import { siteContentLanguages, translationFile } from "./locales.js";
17
+ import { parseManifestText } from "./manifest.js";
18
+ import { textfilePath } from "./textfile.js";
19
+ import { siteThemes, themeVariants } from "./theme-variants.js";
20
+ export { CHECK_KINDS } from "./check-types.js";
21
+ const messageOf = (error) => error instanceof Error ? error.message : String(error);
22
+ const finish = (report, options) => {
23
+ const key = (d) => JSON.stringify([d.file, d.line ?? 0, d.code, d.language, d.theme, d.message]);
24
+ report.diagnostics = [...new Map(report.diagnostics.map((d) => [key(d), d])).values()];
25
+ const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
26
+ report.diagnostics.sort((a, b) => {
27
+ return compare(a.file, b.file) || (a.line ?? 0) - (b.line ?? 0) || compare(key(a), key(b));
28
+ });
29
+ for (const diagnostic of report.diagnostics) {
30
+ if (diagnostic.severity === "error")
31
+ report.summary.errors++;
32
+ else if (diagnostic.severity === "warning")
33
+ report.summary.warnings++;
34
+ else
35
+ report.summary.info++;
36
+ }
37
+ if (report.exitCode !== 2)
38
+ report.exitCode =
39
+ report.summary.errors || (options.warningsAsErrors && report.summary.warnings) ? 1 : 0;
40
+ return report;
41
+ };
42
+ /** Read-only, offline consistency audit. Content errors are returned, never written or repaired. */
43
+ export const checkSite = (root, options = {}) => {
44
+ const report = {
45
+ schemaVersion: 1,
46
+ root: resolve(root),
47
+ checks: [],
48
+ languages: [],
49
+ themes: [],
50
+ diagnostics: [],
51
+ summary: { errors: 0, warnings: 0, info: 0 },
52
+ exitCode: 0,
53
+ };
54
+ const has = (check) => report.checks.includes(check);
55
+ const add = (code, severity, file, message, context = {}, extra = {}) => {
56
+ const check = extra.check ??
57
+ (/^(LANGUAGE_|TRANSLATION_)/.test(code)
58
+ ? "languages"
59
+ : /^THEME_/.test(code)
60
+ ? "themes"
61
+ : /^(RENDER_|DOCUMENT_|ASSET_|REMOTE_)/.test(code)
62
+ ? "render"
63
+ : "config");
64
+ if (check === "config" || has(check))
65
+ report.diagnostics.push({ code, severity, file, message, ...context, ...extra, check });
66
+ };
67
+ try {
68
+ for (const key of Object.keys(options))
69
+ if (!["only", "allowSharedSvg", "warningsAsErrors"].includes(key))
70
+ throw new Error(`Unknown check option: ${key}`);
71
+ if (options.only !== undefined &&
72
+ (!Array.isArray(options.only) ||
73
+ !options.only.length ||
74
+ options.only.some((check) => !CHECK_KINDS.includes(check))))
75
+ throw new Error(`only must be a non-empty list of: ${CHECK_KINDS.join(", ")}`);
76
+ report.checks = CHECK_KINDS.filter((check) => !options.only || options.only.includes(check));
77
+ for (const key of ["allowSharedSvg", "warningsAsErrors"])
78
+ if (options[key] !== undefined && typeof options[key] !== "boolean")
79
+ throw new Error(`${key} must be a boolean`);
80
+ }
81
+ catch (error) {
82
+ add("INVOCATION_INVALID", "error", MANIFEST_FILE, messageOf(error));
83
+ report.exitCode = 2;
84
+ return finish(report, options);
85
+ }
86
+ const cache = new Map();
87
+ const read = (file, context = {}, missing = "FILE_MISSING") => {
88
+ let data = cache.get(file);
89
+ if (data === undefined) {
90
+ try {
91
+ const path = textfilePath(report.root, file);
92
+ if (!statSync(path).isFile())
93
+ throw new Error("Expected a regular file");
94
+ data = readFileSync(path);
95
+ cache.set(file, data);
96
+ }
97
+ catch (error) {
98
+ add(error.code === "ENOENT"
99
+ ? missing
100
+ : "FILE_UNREADABLE", "error", file, messageOf(error), context);
101
+ cache.set(file, null);
102
+ }
103
+ }
104
+ return data ?? undefined;
105
+ };
106
+ const text = (file, data, context) => {
107
+ try {
108
+ const raw = new TextDecoder("utf-8", { fatal: true }).decode(data);
109
+ if (raw.includes("\0"))
110
+ throw new Error("Expected UTF-8 text, not binary data");
111
+ return raw;
112
+ }
113
+ catch (error) {
114
+ add("FILE_UNREADABLE", "error", file, messageOf(error), context);
115
+ return undefined;
116
+ }
117
+ };
118
+ const scan = (directory, ancestors = new Set()) => {
119
+ try {
120
+ const path = textfilePath(report.root, directory);
121
+ if (!existsSync(path))
122
+ return [];
123
+ if (ancestors.has(path))
124
+ throw new Error("Directory symlink cycle");
125
+ const next = new Set([...ancestors, path]);
126
+ return readdirSync(path, { withFileTypes: true })
127
+ .sort((a, b) => (a.name < b.name ? -1 : 1))
128
+ .flatMap((entry) => {
129
+ const file = `${directory}/${entry.name}`;
130
+ if (entry.name.startsWith("."))
131
+ return [];
132
+ try {
133
+ if (!localFile(file))
134
+ throw new Error("Invalid local file path");
135
+ return statSync(textfilePath(report.root, file)).isDirectory()
136
+ ? scan(file, next)
137
+ : [file];
138
+ }
139
+ catch (error) {
140
+ add("FILE_UNREADABLE", "error", file, messageOf(error));
141
+ return [];
142
+ }
143
+ });
144
+ }
145
+ catch (error) {
146
+ add("FILE_UNREADABLE", "error", directory, messageOf(error));
147
+ return [];
148
+ }
149
+ };
150
+ const directories = (directory) => {
151
+ try {
152
+ const path = textfilePath(report.root, directory);
153
+ if (!existsSync(path))
154
+ return [];
155
+ return readdirSync(path)
156
+ .filter((name) => !name.startsWith("."))
157
+ .sort();
158
+ }
159
+ catch (error) {
160
+ add("FILE_UNREADABLE", "error", directory, messageOf(error));
161
+ return [];
162
+ }
163
+ };
164
+ try {
165
+ if (!statSync(report.root).isDirectory())
166
+ throw new Error("Docs root must be a directory");
167
+ const data = read(MANIFEST_FILE);
168
+ if (!data)
169
+ return finish(report, options);
170
+ const raw = text(MANIFEST_FILE, data, {});
171
+ if (raw === undefined)
172
+ return finish(report, options);
173
+ const manifest = parseManifestText(raw);
174
+ for (const message of manifest.errors)
175
+ add("MANIFEST_INVALID", "error", MANIFEST_FILE, message);
176
+ if (manifest.errors.length)
177
+ return finish(report, options);
178
+ let sourceLanguage;
179
+ try {
180
+ sourceLanguage = contentLanguageCode(manifest.defaultLang ?? DEFAULT_LANG);
181
+ }
182
+ catch (error) {
183
+ add("LANGUAGE_INVALID", "error", MANIFEST_FILE, messageOf(error), {}, { check: "config" });
184
+ return finish(report, options);
185
+ }
186
+ report.languages = siteContentLanguages(report.root, manifest).sort();
187
+ report.themes = siteThemes(report.root, manifest).sort();
188
+ const localeDirectories = directories("locales");
189
+ for (const language of localeDirectories) {
190
+ if (!isContentLanguage(language))
191
+ add("LANGUAGE_INVALID", "error", `locales/${language}`, "Translation directories require an ISO 639-3 code.");
192
+ else if (!report.languages.includes(language))
193
+ add("LANGUAGE_UNDECLARED", "warning", `locales/${language}`, "Language is not offered by the site declaration and will not be displayed.", { language });
194
+ }
195
+ const themeDirectories = directories("themes");
196
+ for (const theme of themeDirectories) {
197
+ if (!/^[a-z][a-z0-9-]*$/.test(theme) || (theme !== "light" && theme !== "dark"))
198
+ add("THEME_INVALID", "error", `themes/${theme}`, "Theme cannot be selected in the viewer.", { theme });
199
+ else if (!report.themes.includes(theme))
200
+ add("THEME_UNDECLARED", "warning", `themes/${theme}`, "Theme variant is not enabled by the site declaration and will not be displayed.", { theme });
201
+ }
202
+ const docs = new Map(flattenTree(manifest.tree).map(({ id }) => [docFile(id, manifest.files), id]));
203
+ const visited = new Set();
204
+ const checkedThemes = new Set();
205
+ const checkAsset = (file, context, withThemes = true) => {
206
+ if (/^[a-z][a-z\d+.-]*:/i.test(file)) {
207
+ add("REMOTE_UNCHECKED", "info", file, "Remote content is not fetched by the offline audit.", context);
208
+ return;
209
+ }
210
+ file = file.replace(/^(\.\/)+/, "");
211
+ const key = JSON.stringify([file, context.language, context.theme]);
212
+ if (visited.has(key)) {
213
+ if (withThemes)
214
+ checkThemes(file, context);
215
+ return;
216
+ }
217
+ visited.add(key);
218
+ const data = read(file, context);
219
+ if (!data)
220
+ return;
221
+ if (/\.(md|callouth|termth|graphth|schemath|txt|html|svg)$/.test(file)) {
222
+ const raw = text(file, data, context);
223
+ if (raw !== undefined) {
224
+ const syntax = /\.(callouth|termth|graphth|schemath)$/.exec(file)?.[1];
225
+ for (const error of (syntax
226
+ ? parseContentSource(syntax, raw, file)?.errors
227
+ : []) ?? [])
228
+ add("ASSET_INVALID", "error", file, error.message, context, {
229
+ line: error.line + 1,
230
+ });
231
+ const declared = /<(?:svg|html)\b[^>]*\s(?:xml:)?lang\s*=\s*["']([^"']+)["']/i.exec(raw)?.[1];
232
+ if (/\.(svg|html)$/.test(file) && declared) {
233
+ try {
234
+ const actual = contentLanguageCode(declared);
235
+ if (actual !== context.language)
236
+ add("LANGUAGE_MISMATCH", "error", file, `Markup declares ${actual}; expected ${context.language}.`, context, { detectedLanguage: actual });
237
+ }
238
+ catch (error) {
239
+ add("LANGUAGE_INVALID", "error", file, messageOf(error), context);
240
+ }
241
+ }
242
+ for (const finding of has("languages")
243
+ ? checkProse(assetProse(file, raw) ?? [], context.language ?? sourceLanguage)
244
+ : [])
245
+ report.diagnostics.push({
246
+ ...context,
247
+ ...finding,
248
+ file,
249
+ check: "languages",
250
+ });
251
+ if (/\.(md|callouth)$/.test(file))
252
+ checkRefs(raw, context, file);
253
+ }
254
+ }
255
+ else
256
+ add("LANGUAGE_UNCHECKED", "info", file, "Language checking is unavailable for this file format; code and binary files are not classified.", context);
257
+ if (withThemes)
258
+ checkThemes(file, context);
259
+ };
260
+ const checkThemes = (file, context) => {
261
+ if (!has("themes") && !has("languages"))
262
+ return;
263
+ const key = JSON.stringify([file, context.language]);
264
+ if (file.startsWith("themes/") || checkedThemes.has(key))
265
+ return;
266
+ checkedThemes.add(key);
267
+ const original = read(file, context);
268
+ let variants;
269
+ try {
270
+ variants = themeVariants(report.root, file, report.themes);
271
+ }
272
+ catch (error) {
273
+ add("FILE_UNREADABLE", "error", file, messageOf(error), context);
274
+ return;
275
+ }
276
+ const hashes = new Map();
277
+ const hash = (buffer) => createHash("sha256").update(buffer).digest("hex");
278
+ if (original)
279
+ hashes.set(hash(original), file);
280
+ for (const theme of report.themes) {
281
+ if (!manifest.themes && !variants[theme])
282
+ continue;
283
+ const target = variants[theme] ?? themeVariantFile(file, theme);
284
+ const ctx = { ...context, sourceFile: file, theme };
285
+ const variant = read(target, ctx, "THEME_VARIANT_MISSING");
286
+ if (!variant)
287
+ continue;
288
+ const digest = has("themes") ? hash(variant) : "";
289
+ const relatedFile = hashes.get(digest);
290
+ if (relatedFile && !(options.allowSharedSvg && file.toLowerCase().endsWith(".svg")))
291
+ add("THEME_CONTENT_IDENTICAL", "error", target, "Theme variant has identical bytes to another variant or the source.", ctx, { relatedFile });
292
+ hashes.set(digest, target);
293
+ checkAsset(target, ctx, false);
294
+ }
295
+ };
296
+ const checkRefs = (raw, context, owner) => {
297
+ const tree = parseMdast(raw);
298
+ const refs = collectRefs(tree);
299
+ for (const file of [...refs.figures, ...refs.textfiles])
300
+ checkAsset(file, context);
301
+ for (const file of refs.terms)
302
+ checkAsset(file, context, false);
303
+ for (const image of embeddedImages(tree))
304
+ if (/^https?:\/\//i.test(image.url))
305
+ add("REMOTE_UNCHECKED", "info", owner, "Remote content is not fetched by the offline audit.", context, { line: image.position?.start.line });
306
+ for (const link of contentLinks(tree)) {
307
+ try {
308
+ const target = parseDocLink(link.url);
309
+ if (target && !docs.has(target.file))
310
+ add("DOCUMENT_INVALID", "error", owner, `Linked document is not registered: ${target.file}`, context, { line: link.position?.start.line, relatedFile: target.file });
311
+ }
312
+ catch (error) {
313
+ add("DOCUMENT_INVALID", "error", owner, messageOf(error), context, {
314
+ line: link.position?.start.line,
315
+ });
316
+ }
317
+ }
318
+ };
319
+ const checkDocument = (file, sourceFile, language, documentId) => {
320
+ const context = { sourceFile, language, documentId };
321
+ if (file !== sourceFile && !manifest.languages) {
322
+ try {
323
+ if (!existsSync(textfilePath(report.root, file)))
324
+ return;
325
+ }
326
+ catch (error) {
327
+ add("FILE_UNREADABLE", "error", file, messageOf(error), context);
328
+ return;
329
+ }
330
+ }
331
+ const data = read(file, context, file === sourceFile ? "FILE_MISSING" : "TRANSLATION_MISSING");
332
+ if (!data)
333
+ return;
334
+ const raw = text(file, data, context);
335
+ if (raw === undefined)
336
+ return;
337
+ const doc = parseDoc(raw, undefined, file);
338
+ for (const message of doc.errors)
339
+ add("DOCUMENT_INVALID", "error", file, message, context, {
340
+ line: Number(/^L(\d+):/.exec(message)?.[1]) || undefined,
341
+ });
342
+ if (doc.frontmatter?.lang) {
343
+ try {
344
+ const actual = contentLanguageCode(doc.frontmatter.lang);
345
+ if (actual !== language)
346
+ add("LANGUAGE_MISMATCH", "error", file, `Front-matter declares ${actual}; expected ${language}.`, context, { detectedLanguage: actual });
347
+ }
348
+ catch (error) {
349
+ add("LANGUAGE_INVALID", "error", file, messageOf(error), context);
350
+ }
351
+ }
352
+ if (has("languages")) {
353
+ const prose = markdownProse(doc.root);
354
+ if (doc.frontmatter?.title)
355
+ prose.unshift({ text: doc.frontmatter.title, line: 2, column: 1 });
356
+ for (const finding of checkProse(prose, language))
357
+ report.diagnostics.push({ ...context, ...finding, file, check: "languages" });
358
+ }
359
+ if (has("languages") && file !== sourceFile) {
360
+ const sourceData = read(sourceFile, context);
361
+ const source = sourceData && text(sourceFile, sourceData, context);
362
+ if (source !== undefined &&
363
+ markdownProse(doc.root).some((segment) => /\p{L}/u.test(segment.text)) &&
364
+ parseDoc(source).body.trim() === doc.body.trim())
365
+ add("TRANSLATION_IDENTICAL", "warning", file, "Translation body is identical to the source.", context, { relatedFile: sourceFile });
366
+ }
367
+ checkRefs(raw, context, file);
368
+ };
369
+ for (const [file, id] of docs)
370
+ for (const language of report.languages)
371
+ checkDocument(language === sourceLanguage ? file : translationFile(file, language), file, language, id);
372
+ for (const language of has("languages")
373
+ ? localeDirectories.filter(isContentLanguage)
374
+ : []) {
375
+ for (const file of scan(`locales/${language}`)) {
376
+ const sourceFile = file.slice(`locales/${language}/`.length);
377
+ if (docs.has(sourceFile) && language !== sourceLanguage)
378
+ continue;
379
+ if ((!docs.has(sourceFile) && sourceFile.endsWith(".md")) ||
380
+ language === sourceLanguage) {
381
+ add("TRANSLATION_ORPHAN", "warning", file, "Translation has no registered source document, or duplicates the source-language directory.", { sourceFile, language });
382
+ }
383
+ else {
384
+ try {
385
+ if (!existsSync(textfilePath(report.root, sourceFile)))
386
+ add("TRANSLATION_ORPHAN", "warning", file, "Translation has no source file.", { sourceFile, language });
387
+ }
388
+ catch (error) {
389
+ add("FILE_UNREADABLE", "error", file, messageOf(error));
390
+ }
391
+ }
392
+ checkAsset(file, { sourceFile, language }, false);
393
+ }
394
+ }
395
+ for (const theme of has("themes") || has("languages") ? themeDirectories : [])
396
+ for (const file of scan(`themes/${theme}`)) {
397
+ const sourceFile = file.slice(`themes/${theme}/`.length);
398
+ try {
399
+ if (!existsSync(textfilePath(report.root, sourceFile))) {
400
+ add("THEME_VARIANT_ORPHAN", "warning", file, "Theme variant has no source file.", { sourceFile, theme });
401
+ checkAsset(file, { sourceFile, theme, language: sourceLanguage }, false);
402
+ }
403
+ else {
404
+ const language = /^locales\/([^/]+)\//.exec(sourceFile)?.[1] ?? sourceLanguage;
405
+ if (!checkedThemes.has(JSON.stringify([sourceFile, language])))
406
+ checkAsset(sourceFile, { language });
407
+ }
408
+ }
409
+ catch (error) {
410
+ add("FILE_UNREADABLE", "error", file, messageOf(error));
411
+ }
412
+ }
413
+ for (const file of manifest.files?.order ?? [])
414
+ if (!docs.has(file))
415
+ checkAsset(file, { language: sourceLanguage });
416
+ for (const file of [manifest.icon, ...Object.values(manifest.files?.icons ?? {})])
417
+ if (file)
418
+ checkAsset(file, { language: sourceLanguage }, false);
419
+ if (has("render")) {
420
+ // Resolve with the viewer's compiler, including translated links and theme-specific assets.
421
+ for (const language of report.languages) {
422
+ try {
423
+ for (const directory of ["content", "figures", "icons"])
424
+ textfilePath(report.root, directory);
425
+ const compiled = compileSite(report.root, { locale: language });
426
+ for (const message of compiled.siteErrors) {
427
+ const prefix = message.split(":")[0] ?? "";
428
+ add("RENDER_INVALID", "error", localFile(prefix) ? prefix : MANIFEST_FILE, message, { language });
429
+ }
430
+ for (const [sourceFile, source] of compiled.sources) {
431
+ const candidate = language === sourceLanguage
432
+ ? sourceFile
433
+ : translationFile(sourceFile, language);
434
+ const file = cache.get(candidate) ? candidate : sourceFile;
435
+ for (const message of source.errors)
436
+ add("DOCUMENT_INVALID", "error", file, message, { sourceFile, language, documentId: docs.get(sourceFile) }, { line: Number(/^L(\d+):/.exec(message)?.[1]) || undefined });
437
+ }
438
+ }
439
+ catch (error) {
440
+ add("RENDER_INVALID", "error", MANIFEST_FILE, messageOf(error), { language });
441
+ }
442
+ }
443
+ }
444
+ }
445
+ catch (error) {
446
+ add("CHECK_FAILED", "error", ".", messageOf(error));
447
+ report.exitCode = 2;
448
+ }
449
+ return finish(report, options);
450
+ };
@@ -0,0 +1,17 @@
1
+ import type { Code } from "mdast";
2
+ import type { Block } from "../core/blocks.js";
3
+ export declare const parseContentSource: (language: string, source: string, id: string, caption?: string) => {
4
+ blocks: Block[];
5
+ errors: {
6
+ line: number;
7
+ message: string;
8
+ }[];
9
+ } | undefined;
10
+ /** Fenced and file-backed definitions share their parsers and rendered blocks. */
11
+ export declare const parseContentCode: (node: Code, raw: string, file?: string) => {
12
+ blocks: Block[];
13
+ errors: {
14
+ line: number;
15
+ message: string;
16
+ }[];
17
+ } | undefined;
@@ -0,0 +1,198 @@
1
+ import { visit } from "unist-util-visit";
2
+ import { parseFigureLink } from "../core/file-embed.js";
3
+ import { parseGraphSource } from "../core/graph.js";
4
+ import { inlineToText, listItems, parseMdast } from "../core/markdown.js";
5
+ import { parseDocLink } from "../core/routing.js";
6
+ import { parseSchemaSource } from "../core/schema-graph.js";
7
+ import { CODE_TEMPLATES } from "../core/taxonomy.js";
8
+ import { parseTermReference } from "../core/term-reference.js";
9
+ import { parseTermSource } from "./terms.js";
10
+ const delimitedRows = (source, separator) => {
11
+ const rows = [];
12
+ let row = [];
13
+ let cell = "";
14
+ let quoted = false;
15
+ let closed = false;
16
+ let rowStart = 0;
17
+ const text = source.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
18
+ for (let i = 0; i <= text.length; i++) {
19
+ const char = text[i];
20
+ if (quoted) {
21
+ if (char === undefined)
22
+ throw new Error("Unclosed quoted field");
23
+ if (char === '"' && text[i + 1] === '"') {
24
+ cell += '"';
25
+ i++;
26
+ }
27
+ else if (char === '"') {
28
+ quoted = false;
29
+ closed = true;
30
+ }
31
+ else
32
+ cell += char;
33
+ }
34
+ else if (char === '"' && !cell && !closed)
35
+ quoted = true;
36
+ else if (char === separator || char === "\n" || char === undefined) {
37
+ row.push(cell);
38
+ cell = "";
39
+ closed = false;
40
+ if (char !== separator) {
41
+ if (i > rowStart)
42
+ rows.push(row);
43
+ row = [];
44
+ rowStart = i + 1;
45
+ }
46
+ }
47
+ else {
48
+ if (closed || char === '"')
49
+ throw new Error("Unexpected text after a quoted field");
50
+ cell += char;
51
+ }
52
+ }
53
+ if (!rows.length || rows.some((row) => row.length !== rows[0]?.length))
54
+ throw new Error("Expected equally sized rows");
55
+ return rows;
56
+ };
57
+ export const parseContentSource = (language, source, id, caption) => {
58
+ if (language === "csv" || language === "tsv") {
59
+ try {
60
+ const [headers, ...rows] = delimitedRows(source, language === "csv" ? "," : "\t");
61
+ return {
62
+ blocks: [
63
+ {
64
+ type: "table",
65
+ dataSource: id,
66
+ headers: headers ?? [],
67
+ rows,
68
+ ...(caption ? { caption } : {}),
69
+ },
70
+ ],
71
+ errors: [],
72
+ };
73
+ }
74
+ catch (error) {
75
+ return {
76
+ blocks: [],
77
+ errors: [
78
+ { line: 0, message: error instanceof Error ? error.message : String(error) },
79
+ ],
80
+ };
81
+ }
82
+ }
83
+ if (language === "mermaid")
84
+ return {
85
+ blocks: [{ type: "code", lang: language, lines: source.split("\n") }],
86
+ errors: [],
87
+ };
88
+ if (language === "callouth") {
89
+ const root = parseMdast(source);
90
+ const errors = [];
91
+ const allowed = new Set([
92
+ "root",
93
+ "paragraph",
94
+ "text",
95
+ "inlineCode",
96
+ "link",
97
+ "strong",
98
+ "emphasis",
99
+ "list",
100
+ "listItem",
101
+ ]);
102
+ if (!root.children.length)
103
+ errors.push({ line: 0, message: "callout is empty" });
104
+ visit(root, (node, _index, parent) => {
105
+ const line = (node.position?.start.line ?? 1) - 1;
106
+ if (!allowed.has(node.type) ||
107
+ (parent?.type === "listItem" && node.type !== "paragraph" && node.type !== "list"))
108
+ errors.push({
109
+ line,
110
+ message: "use paragraphs and lists with text, code, or links",
111
+ });
112
+ if (node.type === "link") {
113
+ try {
114
+ if (!node.url || node.url.startsWith("doc:"))
115
+ throw new Error("Use a Markdown file link");
116
+ parseDocLink(node.url);
117
+ parseFigureLink(node.url);
118
+ parseTermReference(node.url);
119
+ }
120
+ catch (error) {
121
+ errors.push({
122
+ line,
123
+ message: error instanceof Error ? error.message : String(error),
124
+ });
125
+ }
126
+ }
127
+ });
128
+ const body = root.children.flatMap((node) => node.type === "paragraph"
129
+ ? [{ type: "p", text: inlineToText(node.children) }]
130
+ : node.type === "list"
131
+ ? [
132
+ {
133
+ type: "list",
134
+ items: listItems(node),
135
+ ...(node.ordered ? { ordered: true } : {}),
136
+ },
137
+ ]
138
+ : []);
139
+ return {
140
+ blocks: [{ type: "callout", body, ...(caption ? { title: caption } : {}) }],
141
+ errors,
142
+ };
143
+ }
144
+ if (language === "schemath") {
145
+ const lines = source.split("\n");
146
+ const indent = Math.min(...lines
147
+ .filter((line) => line.trim())
148
+ .map((line) => /^\s*/.exec(line)?.[0].length ?? 0));
149
+ const parsed = parseSchemaSource(lines.map((line) => line.slice(indent)).join("\n"));
150
+ if (!source.trim())
151
+ parsed.errors.push({ line: 0, message: "schema is empty" });
152
+ return {
153
+ blocks: [
154
+ {
155
+ type: "schemaGraph",
156
+ source,
157
+ graph: parsed.model,
158
+ ...(caption ? { caption } : {}),
159
+ },
160
+ ],
161
+ errors: parsed.errors,
162
+ };
163
+ }
164
+ if (language === "graphth") {
165
+ const parsed = parseGraphSource(source);
166
+ if (!source.trim())
167
+ parsed.errors.push({ line: 0, message: "graph is empty" });
168
+ return {
169
+ blocks: [
170
+ { type: "graph", source, graph: parsed.model, ...(caption ? { caption } : {}) },
171
+ ],
172
+ errors: parsed.errors,
173
+ };
174
+ }
175
+ if (language === "termth") {
176
+ const parsed = parseTermSource(id, source);
177
+ return {
178
+ blocks: parsed.entries.map((entry) => ({ type: "term", entry })),
179
+ errors: parsed.errors,
180
+ };
181
+ }
182
+ return undefined;
183
+ };
184
+ /** Fenced and file-backed definitions share their parsers and rendered blocks. */
185
+ export const parseContentCode = (node, raw, file = "inline") => {
186
+ if (!node.lang || !Object.hasOwn(CODE_TEMPLATES, node.lang))
187
+ return undefined;
188
+ const errors = [];
189
+ const slice = raw.slice(node.position?.start.offset, node.position?.end.offset);
190
+ const opening = /^(`{3,}|~{3,})/.exec(slice)?.[1];
191
+ const closing = /\n {0,3}(`{3,}|~{3,})[ \t]*$/.exec(slice)?.[1];
192
+ if (!opening || !closing || closing[0] !== opening[0] || closing.length < opening.length)
193
+ errors.push({ line: -1, message: "close the code fence" });
194
+ if (node.lang === "termth" && node.meta)
195
+ errors.push({ line: -1, message: "termth takes no ID; write name and definition as YAML" });
196
+ const parsed = parseContentSource(node.lang, node.value, `${file}#L${node.position?.start.line ?? 1}`, node.meta?.trim());
197
+ return { blocks: parsed?.blocks ?? [], errors: [...errors, ...(parsed?.errors ?? [])] };
198
+ };
@@ -0,0 +1,10 @@
1
+ import type { LanguageRegistration } from "shiki/core";
2
+ export declare const CODE_LANGUAGES_DIR = ".dooc/code-languages";
3
+ export declare const loadCodeGrammars: (names: readonly string[]) => Promise<LanguageRegistration[]>;
4
+ /** Renderer-owned registrations; Markdown content is never changed. */
5
+ export declare const loadCodeLanguages: (root: string) => Promise<{
6
+ codeLanguages: string[];
7
+ codeGrammars: LanguageRegistration[];
8
+ }>;
9
+ /** Persist a Shiki language or imported TextMate JSON under the renderer's directory. */
10
+ export declare const addCodeLanguage: (root: string, input: string) => Promise<string>;