@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,142 @@
1
+ // Converter from dialect mdast → normalized Block AST (internal module for build / preview only,
2
+ // outside the publish surface). The consumer contract (Block / TocEntry / slugify) lives in
3
+ // blocks.ts; validation is handled by dialect.ts.
4
+ import { basename, extname } from "node:path";
5
+ import { slugify } from "../core/blocks.js";
6
+ import { parseFileEmbed } from "../core/file-embed.js";
7
+ import { inlineSegmenter } from "../core/inline.js";
8
+ import { inlineToText, listItems, standaloneImages } from "../core/markdown.js";
9
+ import { textfileName } from "../core/textfile.js";
10
+ import { parseContentCode } from "./code-blocks.js";
11
+ import { figureName } from "./figures.js";
12
+ const tableBlock = (t) => {
13
+ const [head, ...rows] = t.children;
14
+ return {
15
+ type: "table",
16
+ headers: (head?.children ?? []).map((c) => inlineToText(c.children)),
17
+ rows: rows.map((r) => r.children.map((c) => inlineToText(c.children))),
18
+ };
19
+ };
20
+ /**
21
+ * Dialect mdast → normalized Block[] + TOC. Heading slugs are made unique within the document.
22
+ * `raw` is the whole source document, used to validate semantic code fences.
23
+ * `lines` holds the source start line at the same index as blocks (1-indexed relative to the
24
+ * whole raw, as root is).
25
+ * Only the edit tool's "body element ↔ source line" mapping uses it (the rendering model
26
+ * is unaffected).
27
+ */
28
+ export const toBlocks = (root, raw, settings, file) => {
29
+ const toc = [];
30
+ const lines = [];
31
+ const seen = new Map();
32
+ let unslugged = 0;
33
+ const inline = inlineSegmenter();
34
+ const convert = (nodes, topLevel = false) => {
35
+ const blocks = [];
36
+ for (const n of nodes) {
37
+ // Every push to blocks also pushes the source line in lockstep (defined in one place).
38
+ const add = (b) => {
39
+ blocks.push(b);
40
+ if (topLevel)
41
+ lines.push(n.position?.start.line ?? 1);
42
+ };
43
+ switch (n.type) {
44
+ case "yaml":
45
+ break;
46
+ case "heading": {
47
+ const h = n;
48
+ const depth = h.depth;
49
+ const text = inlineToText(h.children);
50
+ // Headings with no ASCII slug (e.g. all-Japanese text) are numbered per doc.
51
+ const label = inline(text)
52
+ .map((part) => part.text)
53
+ .join("");
54
+ const base = slugify(label) || `section-${++unslugged}`;
55
+ const dup = seen.get(base) ?? 0;
56
+ seen.set(base, dup + 1);
57
+ const slug = dup === 0 ? base : `${base}-${dup}`;
58
+ toc.push({ depth, text: label, slug });
59
+ add({ type: "h", level: depth, text, slug });
60
+ break;
61
+ }
62
+ case "blockquote":
63
+ add({ type: "quote", blocks: convert(n.children) });
64
+ break;
65
+ case "paragraph": {
66
+ const images = standaloneImages(n);
67
+ if (!images.length)
68
+ add({ type: "p", text: inlineToText(n.children) });
69
+ for (const image of images) {
70
+ try {
71
+ const { file, range, tableMode, kind } = parseFileEmbed(image.url, settings);
72
+ if (kind === "external") {
73
+ add({
74
+ type: "external",
75
+ url: file,
76
+ ...(image.alt ? { alt: image.alt } : {}),
77
+ });
78
+ if (topLevel)
79
+ lines[lines.length - 1] = image.position?.start.line ?? 1;
80
+ continue;
81
+ }
82
+ if (kind !== "textfile") {
83
+ add({
84
+ type: "figureRef",
85
+ name: figureName(file),
86
+ alt: image.alt ?? "",
87
+ });
88
+ if (topLevel)
89
+ lines[lines.length - 1] = image.position?.start.line ?? 1;
90
+ continue;
91
+ }
92
+ add({
93
+ type: "code",
94
+ filename: file,
95
+ ...(range !== undefined ? { range } : {}),
96
+ ...(tableMode ? { tableMode } : {}),
97
+ ...(image.alt ? { alt: image.alt } : {}),
98
+ lang: (extname(textfileName(file)).slice(1) ||
99
+ basename(textfileName(file))).toLowerCase(),
100
+ lines: [],
101
+ });
102
+ if (topLevel)
103
+ lines[lines.length - 1] = image.position?.start.line ?? 1;
104
+ }
105
+ catch {
106
+ // parseDoc reports malformed URLs; do not try to read them.
107
+ }
108
+ }
109
+ break;
110
+ }
111
+ case "list":
112
+ add({
113
+ type: "list",
114
+ items: listItems(n, convert),
115
+ ...(n.ordered ? { ordered: true } : {}),
116
+ });
117
+ break;
118
+ case "code": {
119
+ const parsed = parseContentCode(n, raw, file);
120
+ if (parsed) {
121
+ for (const block of parsed.blocks)
122
+ add(block);
123
+ break;
124
+ }
125
+ add({
126
+ type: "code",
127
+ lines: n.value.split("\n"),
128
+ ...(n.lang != null ? { lang: n.lang } : {}),
129
+ });
130
+ break;
131
+ }
132
+ case "table":
133
+ add(tableBlock(n));
134
+ break;
135
+ default:
136
+ break;
137
+ }
138
+ }
139
+ return blocks;
140
+ };
141
+ return { blocks: convert(root.children, true), toc, lines };
142
+ };
@@ -0,0 +1,14 @@
1
+ import { type FileLayout } from "../core/files.js";
2
+ export declare const ICONS_DIR = "icons";
3
+ /**
4
+ * Normalize an SVG to 1:1 (padding is added automatically = the shorter side is widened, centered;
5
+ * nothing is cropped). Builds a square viewBox from the root's viewBox (or width / height if it has
6
+ * none) and removes fixed width / height (so it scales to the display box). Returns undefined for
7
+ * SVGs whose dimensions can't be read.
8
+ */
9
+ export declare const squareSvg: (svg: string) => string | undefined;
10
+ /** Read icons/ (docId → svg markup). An icon with no doc is an error (missed rename / delete). */
11
+ export declare const loadIcons: (root: string, docIds: ReadonlySet<string>, siteIconFile?: string, layout?: FileLayout) => {
12
+ icons: Map<string, string>;
13
+ errors: string[];
14
+ };
@@ -0,0 +1,85 @@
1
+ // A doc's icon (1:1 SVG shown on its sidebar row). It lives at icons/<docId>.svg in the docs root.
2
+ // Separate from body-text figures (figures/): one per doc, following the doc's rename / deletion.
3
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { docIconFile } from "../core/files.js";
6
+ import { textfilePath } from "./textfile.js";
7
+ export const ICONS_DIR = "icons";
8
+ const ROOT_SVG_RE = /<svg\b[^>]*>/i;
9
+ const num = (v) => {
10
+ const n = v == null ? Number.NaN : Number.parseFloat(v);
11
+ return Number.isFinite(n) && n > 0 ? n : undefined;
12
+ };
13
+ /**
14
+ * Normalize an SVG to 1:1 (padding is added automatically = the shorter side is widened, centered;
15
+ * nothing is cropped). Builds a square viewBox from the root's viewBox (or width / height if it has
16
+ * none) and removes fixed width / height (so it scales to the display box). Returns undefined for
17
+ * SVGs whose dimensions can't be read.
18
+ */
19
+ export const squareSvg = (svg) => {
20
+ const root = ROOT_SVG_RE.exec(svg)?.[0];
21
+ if (!root) {
22
+ return undefined;
23
+ }
24
+ const attr = (name) => new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`, "i").exec(root)?.[1];
25
+ const vb = attr("viewBox")
26
+ ?.trim()
27
+ .split(/[\s,]+/)
28
+ .map(Number);
29
+ let [x, y, w, h] = vb && vb.length === 4 && vb.every(Number.isFinite)
30
+ ? vb
31
+ : [0, 0, num(attr("width")), num(attr("height"))];
32
+ if (x == null || y == null || w == null || h == null || w <= 0 || h <= 0) {
33
+ return undefined;
34
+ }
35
+ const side = Math.max(w, h);
36
+ x -= (side - w) / 2;
37
+ y -= (side - h) / 2;
38
+ const round = (n) => Math.round(n * 1000) / 1000;
39
+ // Rendered via <img> (data URI), so it won't draw without xmlns — add it if missing.
40
+ const xmlns = /\sxmlns\s*=/.test(root) ? "" : ' xmlns="http://www.w3.org/2000/svg"';
41
+ const next = root
42
+ .replace(/\s(viewBox|width|height|preserveAspectRatio)\s*=\s*(["'])[^"']*\2/gi, "")
43
+ .replace(/^<svg\b/i, `<svg${xmlns} viewBox="${[x, y, side, side].map(round).join(" ")}" preserveAspectRatio="xMidYMid meet"`);
44
+ return svg.replace(root, next);
45
+ };
46
+ /** Read icons/ (docId → svg markup). An icon with no doc is an error (missed rename / delete). */
47
+ export const loadIcons = (root, docIds, siteIconFile, layout) => {
48
+ const dir = join(root, ICONS_DIR);
49
+ const icons = new Map();
50
+ const errors = [];
51
+ for (const f of existsSync(dir) ? readdirSync(dir) : []) {
52
+ if (f.startsWith(".")) {
53
+ continue;
54
+ }
55
+ const id = f.endsWith(".svg") ? f.slice(0, -4) : "";
56
+ if (!docIds.has(id)) {
57
+ if (siteIconFile?.replace(/^\.\//, "") === `${ICONS_DIR}/${f}`)
58
+ continue;
59
+ errors.push(`${ICONS_DIR}/${f}: no matching doc (content/${id || f}.md)`);
60
+ continue;
61
+ }
62
+ const svg = readFileSync(join(dir, f), "utf8");
63
+ if (!ROOT_SVG_RE.test(svg)) {
64
+ errors.push(`${ICONS_DIR}/${f}: not an svg`);
65
+ continue;
66
+ }
67
+ icons.set(id, svg);
68
+ }
69
+ for (const id of docIds)
70
+ if (layout?.icons?.[id]) {
71
+ try {
72
+ const file = textfilePath(root, docIconFile(id, layout));
73
+ if (!existsSync(file))
74
+ continue;
75
+ const svg = readFileSync(file, "utf8");
76
+ if (!ROOT_SVG_RE.test(svg))
77
+ throw new Error("not an svg");
78
+ icons.set(id, svg);
79
+ }
80
+ catch (error) {
81
+ errors.push(`${docIconFile(id, layout)}: ${error}`);
82
+ }
83
+ }
84
+ return { icons, errors };
85
+ };
@@ -0,0 +1,7 @@
1
+ export { contentLanguageCode, isContentLanguage } from "../core/language.js";
2
+ export declare const contentLanguages: {
3
+ code: string;
4
+ name: string;
5
+ }[];
6
+ export declare const webLanguage: (code: string) => string;
7
+ export declare const languageName: (code: string) => string;
@@ -0,0 +1,17 @@
1
+ import { iso6393, iso6393To1 } from "iso-639-3";
2
+ export { contentLanguageCode, isContentLanguage } from "../core/language.js";
3
+ export const contentLanguages = iso6393
4
+ .map(({ iso6393: code, name }) => ({ code, name }))
5
+ .sort((a, b) => a.name.localeCompare(b.name));
6
+ const codes = new Map(iso6393.map((language) => [language.iso6393, language.name]));
7
+ export const webLanguage = (code) => iso6393To1[code] ?? code;
8
+ export const languageName = (code) => {
9
+ try {
10
+ return (new Intl.DisplayNames([webLanguage(code)], { type: "language" }).of(webLanguage(code)) ??
11
+ codes.get(code) ??
12
+ code);
13
+ }
14
+ catch {
15
+ return codes.get(code) ?? code;
16
+ }
17
+ };
@@ -0,0 +1,7 @@
1
+ import { type ParsedManifest } from "../core/content-metadata.js";
2
+ export declare const translationFile: (file: string, language: string) => string;
3
+ export declare const translationLanguages: (root: string) => string[];
4
+ /** Declared languages are authoritative; older sites retain directory discovery. */
5
+ export declare const siteContentLanguages: (root: string, manifest?: ParsedManifest) => string[];
6
+ export declare const translationFiles: (root: string) => string[];
7
+ export declare const setDocTranslation: (root: string, sourceLanguage: string, id: string, language: string, added: boolean) => void;
@@ -0,0 +1,67 @@
1
+ // Translations mirror source paths under locales/<ISO 639-3>/ as ordinary Markdown files.
2
+ import { existsSync, readdirSync, unlinkSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { isDocId } from "../core/content-metadata.js";
5
+ import { localFile } from "../core/files.js";
6
+ import { contentLanguageCode, DEFAULT_LANG } from "../core/language.js";
7
+ import { parseDoc } from "./dialect.js";
8
+ import { isContentLanguage } from "./language-codes.js";
9
+ import { docFile, readManifest } from "./manifest.js";
10
+ import { assembleDoc } from "./serialize.js";
11
+ import { readTextfile, textfilePath, writeTextfile } from "./textfile.js";
12
+ export const translationFile = (file, language) => {
13
+ if (!isContentLanguage(language))
14
+ throw new Error("Expected an ISO 639-3 language code");
15
+ if (!localFile(file) || !file.endsWith(".md"))
16
+ throw new Error("Expected a Markdown file path");
17
+ return `locales/${language}/${file}`;
18
+ };
19
+ export const translationLanguages = (root) => {
20
+ const dir = textfilePath(root, "locales");
21
+ return existsSync(dir)
22
+ ? readdirSync(dir, { withFileTypes: true })
23
+ .filter((entry) => entry.isDirectory() && isContentLanguage(entry.name))
24
+ .map((entry) => entry.name)
25
+ .sort()
26
+ : [];
27
+ };
28
+ /** Declared languages are authoritative; older sites retain directory discovery. */
29
+ export const siteContentLanguages = (root, manifest = readManifest(root)) => manifest.languages ?? [
30
+ ...new Set([
31
+ contentLanguageCode(manifest.defaultLang ?? DEFAULT_LANG),
32
+ ...translationLanguages(root),
33
+ ]),
34
+ ];
35
+ export const translationFiles = (root) => translationLanguages(root).flatMap((language) => {
36
+ const directory = `locales/${language}`;
37
+ return readdirSync(textfilePath(root, directory), { recursive: true, withFileTypes: true })
38
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
39
+ .map((entry) => relative(root, join(entry.parentPath, entry.name)).replaceAll("\\", "/"))
40
+ .filter(localFile)
41
+ .sort();
42
+ });
43
+ export const setDocTranslation = (root, sourceLanguage, id, language, added) => {
44
+ if (!isDocId(id))
45
+ throw new Error("Invalid document id");
46
+ if (language === sourceLanguage)
47
+ throw new Error("Source language cannot be removed");
48
+ const manifest = readManifest(root);
49
+ if (manifest.errors.length)
50
+ throw new Error(manifest.errors.join("\n"));
51
+ if (manifest.languages && !manifest.languages.includes(language))
52
+ throw new Error(`Language ${language} is not declared in dooc.yaml languages`);
53
+ const file = docFile(id, manifest.files);
54
+ const translated = translationFile(file, language);
55
+ const path = textfilePath(root, translated);
56
+ const source = readTextfile(root, file);
57
+ if (added === existsSync(path))
58
+ return;
59
+ if (added) {
60
+ const doc = parseDoc(source);
61
+ if (!doc.frontmatter)
62
+ throw new Error("Fix the source front-matter before translating");
63
+ writeTextfile(root, translated, assembleDoc({ ...doc.frontmatter, lang: language }, doc.body), null);
64
+ }
65
+ else
66
+ unlinkSync(path);
67
+ };
@@ -0,0 +1,6 @@
1
+ import { type DocNode, type ParsedManifest } from "../core/content-metadata.js";
2
+ export declare const parseManifestText: (raw: string) => ParsedManifest;
3
+ /** Empty tree if missing (a new docs root with zero docs). */
4
+ export declare const readManifest: (root: string) => ParsedManifest;
5
+ export declare const writeManifest: (root: string, tree: readonly DocNode[], metadata?: Omit<ParsedManifest, "tree" | "errors">) => void;
6
+ export { docFile } from "../core/files.js";
@@ -0,0 +1,30 @@
1
+ // Reads/writes dooc.yaml (node-dependent). Tree validation and manipulation live in
2
+ // core/content-metadata.ts.
3
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { parse } from "yaml";
6
+ import { MANIFEST_FILE, parseManifest, serializeManifest, } from "../core/content-metadata.js";
7
+ export const parseManifestText = (raw) => {
8
+ let value;
9
+ try {
10
+ value = parse(raw);
11
+ }
12
+ catch (e) {
13
+ return {
14
+ tree: [],
15
+ errors: [`${MANIFEST_FILE}: invalid YAML (${e instanceof Error ? e.message : e})`],
16
+ };
17
+ }
18
+ return parseManifest(value);
19
+ };
20
+ /** Empty tree if missing (a new docs root with zero docs). */
21
+ export const readManifest = (root) => {
22
+ const path = join(root, MANIFEST_FILE);
23
+ return existsSync(path)
24
+ ? parseManifestText(readFileSync(path, "utf8"))
25
+ : { tree: [], errors: [] };
26
+ };
27
+ export const writeManifest = (root, tree, metadata = {}) => {
28
+ writeFileSync(join(root, MANIFEST_FILE), serializeManifest(tree, metadata));
29
+ };
30
+ export { docFile } from "../core/files.js";
@@ -0,0 +1,44 @@
1
+ import { type ParsedManifest } from "../core/content-metadata.js";
2
+ export interface OpError {
3
+ error: string;
4
+ status: 400 | 404 | 409;
5
+ referencedBy?: string[];
6
+ }
7
+ export type OpResult<T> = T | OpError;
8
+ export declare const isOpError: (r: unknown) => r is OpError;
9
+ export { assembleDoc } from "./serialize.js";
10
+ export declare const createDocOps: (root: string) => {
11
+ contentDir: string;
12
+ figuresDir: string;
13
+ loadTree: () => OpResult<ParsedManifest>;
14
+ referencingFiles: (id: string, removed: readonly string[]) => string[];
15
+ figureFiles: () => Map<string, string>;
16
+ add: (id: string, title: string, parent?: string, filename?: string) => OpResult<{
17
+ id: string;
18
+ }>;
19
+ addFile: (name: string, title: string, parent?: string) => OpResult<{
20
+ id: string;
21
+ }>;
22
+ move: (id: string, parent?: string, index?: number) => OpResult<{
23
+ id: string;
24
+ }>;
25
+ rename: (oldId: string, newId: string) => OpResult<{
26
+ id: string;
27
+ updatedRefs: number;
28
+ }>;
29
+ remove: (id: string, force?: boolean) => OpResult<{
30
+ parent: string | undefined;
31
+ danglingRefs: number;
32
+ }>;
33
+ write: (id: string, raw: string) => OpResult<{
34
+ id: string;
35
+ }>;
36
+ addFigure: (file: string, data: Buffer) => OpResult<{
37
+ file: string;
38
+ path?: string;
39
+ }>;
40
+ removeFigure: (name: string, force?: boolean) => OpResult<{
41
+ file: string;
42
+ }>;
43
+ };
44
+ export type DocOps = ReturnType<typeof createDocOps>;