@dooc-dev/dooc 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +26 -2
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +244 -0
  4. package/dist/code.global.js +285 -0
  5. package/dist/compiler/bundle-urls.d.ts +3 -0
  6. package/dist/compiler/bundle-urls.js +294 -0
  7. package/dist/compiler/bundle.d.ts +2 -0
  8. package/dist/compiler/bundle.js +100 -0
  9. package/dist/compiler/check-prose.d.ts +11 -0
  10. package/dist/compiler/check-prose.js +138 -0
  11. package/dist/compiler/check-types.d.ts +44 -0
  12. package/dist/compiler/check-types.js +1 -0
  13. package/dist/compiler/check.d.ts +5 -0
  14. package/dist/compiler/check.js +450 -0
  15. package/dist/compiler/code-blocks.d.ts +17 -0
  16. package/dist/compiler/code-blocks.js +198 -0
  17. package/dist/compiler/code-languages.d.ts +10 -0
  18. package/dist/compiler/code-languages.js +109 -0
  19. package/dist/compiler/compile.d.ts +95 -0
  20. package/dist/compiler/compile.js +410 -0
  21. package/dist/compiler/config.d.ts +7 -0
  22. package/dist/compiler/config.js +52 -0
  23. package/dist/compiler/dialect.d.ts +24 -0
  24. package/dist/compiler/dialect.js +198 -0
  25. package/dist/compiler/drawio.d.ts +26 -0
  26. package/dist/compiler/drawio.js +176 -0
  27. package/dist/compiler/figures.d.ts +14 -0
  28. package/dist/compiler/figures.js +221 -0
  29. package/dist/compiler/file-cache.d.ts +14 -0
  30. package/dist/compiler/file-cache.js +61 -0
  31. package/dist/compiler/files.d.ts +29 -0
  32. package/dist/compiler/files.js +431 -0
  33. package/dist/compiler/fonts.d.ts +10 -0
  34. package/dist/compiler/fonts.js +54 -0
  35. package/dist/compiler/format.d.ts +1 -0
  36. package/dist/compiler/format.js +42 -0
  37. package/dist/compiler/from-mdast.d.ts +16 -0
  38. package/dist/compiler/from-mdast.js +142 -0
  39. package/dist/compiler/icons.d.ts +14 -0
  40. package/dist/compiler/icons.js +85 -0
  41. package/dist/compiler/language-codes.d.ts +7 -0
  42. package/dist/compiler/language-codes.js +17 -0
  43. package/dist/compiler/locales.d.ts +7 -0
  44. package/dist/compiler/locales.js +67 -0
  45. package/dist/compiler/manifest.d.ts +6 -0
  46. package/dist/compiler/manifest.js +30 -0
  47. package/dist/compiler/ops.d.ts +44 -0
  48. package/dist/compiler/ops.js +362 -0
  49. package/dist/compiler/public-file.d.ts +2 -0
  50. package/dist/compiler/public-file.js +93 -0
  51. package/dist/compiler/serialize.d.ts +3 -0
  52. package/dist/compiler/serialize.js +16 -0
  53. package/dist/compiler/terms.d.ts +9 -0
  54. package/dist/compiler/terms.js +44 -0
  55. package/dist/compiler/textfile-remote.d.ts +12 -0
  56. package/dist/compiler/textfile-remote.js +124 -0
  57. package/dist/compiler/textfile.d.ts +9 -0
  58. package/dist/compiler/textfile.js +113 -0
  59. package/dist/compiler/theme-variants.d.ts +4 -0
  60. package/dist/compiler/theme-variants.js +22 -0
  61. package/dist/compiler/zip.d.ts +6 -0
  62. package/dist/compiler/zip.js +48 -0
  63. package/dist/core/blocks.d.ts +99 -0
  64. package/dist/core/blocks.js +29 -0
  65. package/dist/core/code-file.d.ts +12 -0
  66. package/dist/core/code-file.js +68 -0
  67. package/dist/core/content-metadata.d.ts +48 -0
  68. package/dist/core/content-metadata.js +264 -0
  69. package/dist/core/content-types.d.ts +45 -0
  70. package/dist/core/content-types.js +1 -0
  71. package/dist/core/figure-frame.d.ts +12 -0
  72. package/dist/core/figure-frame.js +108 -0
  73. package/dist/core/file-embed.d.ts +13 -0
  74. package/dist/core/file-embed.js +43 -0
  75. package/dist/core/files.d.ts +15 -0
  76. package/dist/core/files.js +59 -0
  77. package/dist/core/flow-base-css.generated.d.ts +1 -0
  78. package/dist/core/flow-base-css.generated.js +4 -0
  79. package/dist/core/flow-css.d.ts +9 -0
  80. package/dist/core/flow-css.js +54 -0
  81. package/dist/core/graph-common.d.ts +32 -0
  82. package/dist/core/graph-common.js +113 -0
  83. package/dist/core/graph.d.ts +117 -0
  84. package/dist/core/graph.js +480 -0
  85. package/dist/core/html.d.ts +29 -0
  86. package/dist/core/html.js +444 -0
  87. package/dist/core/icons.d.ts +1 -0
  88. package/dist/core/icons.js +1 -0
  89. package/dist/core/inline.d.ts +27 -0
  90. package/dist/core/inline.js +42 -0
  91. package/dist/core/language.d.ts +41 -0
  92. package/dist/core/language.js +44 -0
  93. package/dist/core/languages/ja.d.ts +2 -0
  94. package/dist/core/languages/ja.js +5 -0
  95. package/dist/core/markdown.d.ts +27 -0
  96. package/dist/core/markdown.js +154 -0
  97. package/dist/core/previewers.d.ts +37 -0
  98. package/dist/core/previewers.js +103 -0
  99. package/dist/core/routing.d.ts +20 -0
  100. package/dist/core/routing.js +64 -0
  101. package/dist/core/schema-comments.d.ts +14 -0
  102. package/dist/core/schema-comments.js +52 -0
  103. package/dist/core/schema-graph-flow.d.ts +9 -0
  104. package/dist/core/schema-graph-flow.js +379 -0
  105. package/dist/core/schema-graph-mount.d.ts +9 -0
  106. package/dist/core/schema-graph-mount.js +105 -0
  107. package/dist/core/schema-graph.d.ts +254 -0
  108. package/dist/core/schema-graph.js +981 -0
  109. package/dist/core/table-ui.d.ts +2 -0
  110. package/dist/core/table-ui.js +180 -0
  111. package/dist/core/taxonomy.d.ts +12 -0
  112. package/dist/core/taxonomy.js +49 -0
  113. package/dist/core/term-reference.d.ts +5 -0
  114. package/dist/core/term-reference.js +19 -0
  115. package/dist/core/terms-ui.d.ts +6 -0
  116. package/dist/core/terms-ui.js +22 -0
  117. package/dist/core/textfile.d.ts +10 -0
  118. package/dist/core/textfile.js +39 -0
  119. package/dist/core/theme-variants.d.ts +3 -0
  120. package/dist/core/theme-variants.js +37 -0
  121. package/dist/core/toc-ui.d.ts +2 -0
  122. package/dist/core/toc-ui.js +161 -0
  123. package/dist/core/video-ui.d.ts +2 -0
  124. package/dist/core/video-ui.js +253 -0
  125. package/dist/core/workspace.d.ts +15 -0
  126. package/dist/core/workspace.js +28 -0
  127. package/dist/edit.global.js +305 -0
  128. package/dist/flow.global.js +4959 -0
  129. package/dist/index.d.ts +17 -0
  130. package/dist/index.js +31 -0
  131. package/dist/mcp.d.ts +24 -0
  132. package/dist/mcp.js +467 -0
  133. package/dist/plugins/actions.d.ts +45 -0
  134. package/dist/plugins/actions.js +125 -0
  135. package/dist/plugins/blocks.d.ts +92 -0
  136. package/dist/plugins/blocks.js +99 -0
  137. package/dist/plugins/index.d.ts +4 -0
  138. package/dist/plugins/index.js +1 -0
  139. package/dist/plugins/oauth.d.ts +14 -0
  140. package/dist/plugins/oauth.js +164 -0
  141. package/dist/plugins/runtime.d.ts +24 -0
  142. package/dist/plugins/runtime.js +339 -0
  143. package/dist/plugins/types.d.ts +56 -0
  144. package/dist/plugins/types.js +1 -0
  145. package/dist/plugins/view.d.ts +29 -0
  146. package/dist/plugins/view.js +60 -0
  147. package/dist/ui.global.js +77 -0
  148. package/dist/viewer/assets.d.ts +2 -0
  149. package/dist/viewer/assets.js +3 -0
  150. package/dist/viewer/build.d.ts +12 -0
  151. package/dist/viewer/build.js +55 -0
  152. package/dist/viewer/bundle-network.d.ts +2 -0
  153. package/dist/viewer/bundle-network.js +88 -0
  154. package/dist/viewer/code-file.d.ts +20 -0
  155. package/dist/viewer/code-file.js +42 -0
  156. package/dist/viewer/editor-tools.d.ts +38 -0
  157. package/dist/viewer/editor-tools.js +211 -0
  158. package/dist/viewer/file-references.d.ts +14 -0
  159. package/dist/viewer/file-references.js +38 -0
  160. package/dist/viewer/font-picker.d.ts +4 -0
  161. package/dist/viewer/font-picker.js +145 -0
  162. package/dist/viewer/font-settings.d.ts +11 -0
  163. package/dist/viewer/font-settings.js +50 -0
  164. package/dist/viewer/fonts/LICENSE.txt +119 -0
  165. package/dist/viewer/fonts/udev-gothic-regular.woff2 +0 -0
  166. package/dist/viewer/fonts.d.ts +2 -0
  167. package/dist/viewer/fonts.js +13 -0
  168. package/dist/viewer/git-diff.d.ts +3 -0
  169. package/dist/viewer/git-diff.js +33 -0
  170. package/dist/viewer/git-types.d.ts +73 -0
  171. package/dist/viewer/git-types.js +1 -0
  172. package/dist/viewer/git.d.ts +5 -0
  173. package/dist/viewer/git.js +337 -0
  174. package/dist/viewer/hmr.d.ts +53 -0
  175. package/dist/viewer/hmr.js +335 -0
  176. package/dist/viewer/i18n.d.ts +6 -0
  177. package/dist/viewer/i18n.js +65 -0
  178. package/dist/viewer/ja-lint.d.ts +3 -0
  179. package/dist/viewer/ja-lint.js +44 -0
  180. package/dist/viewer/language-picker.d.ts +2 -0
  181. package/dist/viewer/language-picker.js +233 -0
  182. package/dist/viewer/languages.d.ts +11 -0
  183. package/dist/viewer/languages.js +20 -0
  184. package/dist/viewer/locales/eng.json +294 -0
  185. package/dist/viewer/locales/jpn.json +294 -0
  186. package/dist/viewer/locales/zho.json +294 -0
  187. package/dist/viewer/nav-focus.d.ts +2 -0
  188. package/dist/viewer/nav-focus.js +64 -0
  189. package/dist/viewer/plugin-blocks.d.ts +3 -0
  190. package/dist/viewer/plugin-blocks.js +115 -0
  191. package/dist/viewer/plugin-config.d.ts +35 -0
  192. package/dist/viewer/plugin-config.js +57 -0
  193. package/dist/viewer/plugin-view.d.ts +3 -0
  194. package/dist/viewer/plugin-view.js +28 -0
  195. package/dist/viewer/plugins.d.ts +8 -0
  196. package/dist/viewer/plugins.js +214 -0
  197. package/dist/viewer/resize.d.ts +4 -0
  198. package/dist/viewer/resize.js +238 -0
  199. package/dist/viewer/resource-library.d.ts +29 -0
  200. package/dist/viewer/resource-library.js +104 -0
  201. package/dist/viewer/search.d.ts +16 -0
  202. package/dist/viewer/search.js +358 -0
  203. package/dist/viewer/serve.d.ts +206 -0
  204. package/dist/viewer/serve.js +3233 -0
  205. package/dist/viewer/site-icon.d.ts +8 -0
  206. package/dist/viewer/site-icon.js +56 -0
  207. package/dist/viewer/textfile.d.ts +9 -0
  208. package/dist/viewer/textfile.js +50 -0
  209. package/dist/viewer/theme-picker.d.ts +2 -0
  210. package/dist/viewer/theme-picker.js +135 -0
  211. package/dist/viewer/workspace.d.ts +4 -0
  212. package/dist/viewer/workspace.js +47 -0
  213. package/package.json +127 -5
@@ -0,0 +1,24 @@
1
+ import type { Root } from "mdast";
2
+ import type { TermEntry } from "../core/content-types.js";
3
+ import type { PreviewSettings } from "../core/previewers.js";
4
+ import { type Frontmatter } from "../core/taxonomy.js";
5
+ export { parseMdast } from "../core/markdown.js";
6
+ export interface ParsedDoc {
7
+ terms: TermEntry[];
8
+ frontmatter: Frontmatter | undefined;
9
+ /** Body text without the front-matter (becomes the manifest's body). */
10
+ body: string;
11
+ /** Source passed to parse; root position offsets use it. */
12
+ source: string;
13
+ root: Root;
14
+ errors: string[];
15
+ }
16
+ export declare const parseDoc: (input: string, settings?: PreviewSettings, file?: string) => ParsedDoc;
17
+ /**
18
+ * Collect files referenced by Markdown links and embeds for validation and export.
19
+ */
20
+ export declare const collectRefs: (root: Root, settings?: PreviewSettings) => {
21
+ figures: string[];
22
+ terms: string[];
23
+ textfiles: string[];
24
+ };
@@ -0,0 +1,198 @@
1
+ import { visit } from "unist-util-visit";
2
+ import { parse as parseYaml } from "yaml";
3
+ import { parseFigureLink, parseFileEmbed } from "../core/file-embed.js";
4
+ import { isLangTag } from "../core/language.js";
5
+ import { contentLinks, embeddedImages, parseMdast } from "../core/markdown.js";
6
+ import { parseDocLink } from "../core/routing.js";
7
+ import { FM_OPTIONAL, FM_REQUIRED } from "../core/taxonomy.js";
8
+ import { parseTermReference } from "../core/term-reference.js";
9
+ import { parseTextfileRange } from "../core/textfile.js";
10
+ import { parseContentCode } from "./code-blocks.js";
11
+ export { parseMdast } from "../core/markdown.js";
12
+ // Unsupported nodes are reported before conversion so source content is never silently dropped.
13
+ const ALLOWED_TYPES = new Set([
14
+ "root",
15
+ "yaml",
16
+ "paragraph",
17
+ "heading",
18
+ "list",
19
+ "listItem",
20
+ "code",
21
+ "inlineCode",
22
+ "table",
23
+ "tableRow",
24
+ "tableCell",
25
+ "text",
26
+ "link",
27
+ "image",
28
+ "strong",
29
+ "emphasis",
30
+ "blockquote",
31
+ ]);
32
+ const line = (n) => (n.position ? `L${n.position.start.line}` : "L?");
33
+ const checkFrontmatter = (raw, errors) => {
34
+ if (typeof raw !== "object" || raw == null || Array.isArray(raw)) {
35
+ errors.push("front-matter: a YAML object is required");
36
+ return undefined;
37
+ }
38
+ const fm = raw;
39
+ const required = FM_REQUIRED;
40
+ const allowed = [...FM_REQUIRED, ...FM_OPTIONAL];
41
+ for (const key of Object.keys(fm)) {
42
+ if (!allowed.includes(key)) {
43
+ errors.push(`front-matter: unknown key "${key}" (allowed: ${allowed.join(", ")})`);
44
+ }
45
+ }
46
+ for (const key of required) {
47
+ if (typeof fm[key] !== "string" || fm[key] === "") {
48
+ errors.push(`front-matter: "${key}" (string) is required`);
49
+ }
50
+ }
51
+ if (fm.lang !== undefined && !(typeof fm.lang === "string" && isLangTag(fm.lang))) {
52
+ errors.push('front-matter: "lang" must be a BCP 47 language tag (e.g. "en", "ja", "pt-BR")');
53
+ }
54
+ return errors.length === 0 ? fm : undefined;
55
+ };
56
+ const checkHeading = (n, errors) => {
57
+ if (n.depth < 1 || n.depth > 3) {
58
+ errors.push(`${line(n)}: only # / ## / ### headings are allowed (not h${n.depth})`);
59
+ }
60
+ };
61
+ // Quotes retain their block structure even when nested inside list items.
62
+ const checkListItem = (n, errors) => {
63
+ for (const child of n.children) {
64
+ if (child.type !== "paragraph" && child.type !== "list" && child.type !== "blockquote") {
65
+ errors.push(`${line(child)}: ${child.type} is not allowed directly under listItem (paragraph / list / quote only)`);
66
+ }
67
+ }
68
+ };
69
+ const checkLink = (n, errors) => {
70
+ try {
71
+ parseDocLink(n.url);
72
+ parseFigureLink(n.url);
73
+ parseTermReference(n.url);
74
+ }
75
+ catch (error) {
76
+ errors.push(`${line(n)}: ${error instanceof Error ? error.message : error}`);
77
+ }
78
+ if (n.url.startsWith("doc:"))
79
+ errors.push(`${line(n)}: use a Markdown file link instead of doc:`);
80
+ if (n.url === "") {
81
+ errors.push(`${line(n)}: link with an empty URL`);
82
+ }
83
+ };
84
+ export const parseDoc = (input, settings, file) => {
85
+ const errors = [];
86
+ const raw = input;
87
+ const root = parseMdast(raw);
88
+ const embeds = new Set(embeddedImages(root));
89
+ const terms = [];
90
+ const yamlNode = root.children[0];
91
+ let frontmatter;
92
+ let body = input;
93
+ if (yamlNode?.type === "yaml") {
94
+ try {
95
+ frontmatter = checkFrontmatter(parseYaml(yamlNode.value), errors);
96
+ }
97
+ catch (e) {
98
+ errors.push(`front-matter: YAML parse failed (${e instanceof Error ? e.message : e})`);
99
+ }
100
+ const end = yamlNode.position?.end.offset;
101
+ body = end != null ? input.slice(end).replace(/^\n+/, "") : raw;
102
+ }
103
+ else {
104
+ errors.push("front-matter (---) is required at the top");
105
+ }
106
+ visit(root, (node, _index, parent) => {
107
+ if (node.type === "text" &&
108
+ /(?:^|\n)[ \t]*:{2,}[a-z]+\b/.test(raw.slice(node.position?.start.offset, node.position?.end.offset)))
109
+ errors.push(`${line(node)}: removed directive; use Markdown file links or callouth / schemath / graphth / termth code fences`);
110
+ if (!ALLOWED_TYPES.has(node.type)) {
111
+ const hint = node.type === "html" ? ' (put "<" inside backticks or escape it as "\\<")' : "";
112
+ errors.push(`${line(node)}: ${node.type} is outside the dialect${hint}`);
113
+ return;
114
+ }
115
+ switch (node.type) {
116
+ case "code": {
117
+ const parsed = parseContentCode(node, raw, file);
118
+ if (!parsed)
119
+ break;
120
+ if (parent?.type !== "root") {
121
+ errors.push(`${line(node)}: ${node.lang} must be a top-level code block`);
122
+ break;
123
+ }
124
+ for (const error of parsed.errors)
125
+ errors.push(`L${(node.position?.start.line ?? 0) + 1 + error.line}: ${node.lang} ${error.message}`);
126
+ for (const block of parsed.blocks)
127
+ if (block.type === "term")
128
+ terms.push(block.entry);
129
+ break;
130
+ }
131
+ case "image": {
132
+ if (!embeds.has(node)) {
133
+ errors.push(`${line(node)}: file previews must occupy a standalone paragraph`);
134
+ break;
135
+ }
136
+ try {
137
+ const { range } = parseFileEmbed(node.url, settings);
138
+ if (range !== undefined)
139
+ parseTextfileRange(range);
140
+ }
141
+ catch (error) {
142
+ errors.push(`${line(node)}: ${error instanceof Error ? error.message : error}`);
143
+ }
144
+ break;
145
+ }
146
+ case "heading":
147
+ checkHeading(node, errors);
148
+ break;
149
+ case "listItem":
150
+ checkListItem(node, errors);
151
+ break;
152
+ case "link":
153
+ checkLink(node, errors);
154
+ break;
155
+ default:
156
+ break;
157
+ }
158
+ });
159
+ return { frontmatter, body, source: raw, root, errors, terms };
160
+ };
161
+ /**
162
+ * Collect files referenced by Markdown links and embeds for validation and export.
163
+ */
164
+ export const collectRefs = (root, settings) => {
165
+ const figures = [];
166
+ const terms = [];
167
+ const textfiles = [];
168
+ for (const image of embeddedImages(root)) {
169
+ try {
170
+ const { file, kind } = parseFileEmbed(image.url, settings);
171
+ if (kind === "textfile")
172
+ textfiles.push(file);
173
+ else if (kind !== "external")
174
+ figures.push(file);
175
+ }
176
+ catch {
177
+ // Invalid references are reported by parseDoc and must not reach file I/O.
178
+ }
179
+ }
180
+ for (const node of contentLinks(root)) {
181
+ try {
182
+ const file = parseTermReference(node.url);
183
+ if (file)
184
+ terms.push(file);
185
+ const figure = parseFigureLink(node.url);
186
+ if (figure)
187
+ figures.push(figure.file);
188
+ }
189
+ catch {
190
+ /* Validation reports invalid file paths. */
191
+ }
192
+ }
193
+ return {
194
+ figures: [...new Set(figures)],
195
+ terms: [...new Set(terms)],
196
+ textfiles: [...new Set(textfiles)],
197
+ };
198
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Extract the mxfile XML from the root <svg>'s content attribute.
3
+ * A prolog / DOCTYPE may precede it, so cut the start tag from the `<svg` position (the first `>`
4
+ * belongs to the prolog).
5
+ */
6
+ export declare const extractMxfile: (raw: string) => string | undefined;
7
+ /** The mxfile's first <diagram name="..."> = the draw.io page name (= figure description). */
8
+ export declare const mxfilePageName: (mxfile: string) => string | undefined;
9
+ /**
10
+ * Whether an svg in figures/ was exported by draw.io. To avoid catching hand-written figures, it
11
+ * checks for traces only draw.io emits: the mxfile copy (content attribute) / the drawio.com
12
+ * fallback <switch> / draw.io CSS variables (`--ge-*`) / an inline `color-scheme` on the root.
13
+ */
14
+ export declare const isDrawioSvg: (raw: string) => boolean;
15
+ export interface DrawioNormalized {
16
+ markup: string;
17
+ /** draw.io page name = figure description (for <desc>). Absent if exported without the copy. */
18
+ desc?: string;
19
+ }
20
+ /**
21
+ * Normalize a draw.io-exported SVG into an SVG figures.ts can handle. `name` is the figure's name
22
+ * (= the name in figures/<name>.svg, used for <title>Fig.<name></title>).
23
+ * The returned markup is still a raw <svg>…</svg> string; figures.ts goes on to do the theme
24
+ * injection and structural validation.
25
+ */
26
+ export declare const normalizeDrawioSvg: (name: string, raw: string) => DrawioNormalized;
@@ -0,0 +1,176 @@
1
+ // Normalization of SVGs exported from draw.io = this module is the SoT.
2
+ //
3
+ // Two ways to place them (see also the top of figures.ts):
4
+ // - figures/<name>.drawio.svg = File > Save as (Editable SVG). Carries a copy of the mxfile, so
5
+ // one file is both the editing source and the output (open in draw.io and Save to
6
+ // update it). Recommended
7
+ // - figures/<name>.svg = File > Export as > SVG (the editing source can live anywhere).
8
+ // Drift between the two (only the svg is stale) is handled by people re-exporting;
9
+ // the build doesn't verify the relationship (= no machinery beyond "make draw.io
10
+ // easy to work with locally")
11
+ // figures.ts passes .drawio.svg through here unconditionally, and .svg only when it comes from
12
+ // draw.io (`isDrawioSvg`), then feeds it to the same parseSvg as hand-written SVGs.
13
+ //
14
+ // **Colors stay in draw.io's scheme (not mapped to the var(--doc-*) of hand-written figures).**
15
+ // Because draw.io itself emits both light and dark values via CSS light-dark() on export:
16
+ // stroke: light-dark(rgb(0,0,0), rgb(255,255,255))
17
+ // fill: light-dark(rgb(218,232,252), rgb(29,41,59)) ← auto dark version of chosen color
18
+ // --ge-adaptive-bg: light-dark(#ffffff, var(--ge-dark-color, #121212))
19
+ // Figures are fully self-contained in terms of color. Mapping them onto the docs palette (=
20
+ // erroring on colors not in the table) is rejected: it takes away drawing freedom and would
21
+ // duplicate draw.io's own dark support. Some mismatch with the docs color scheme is acceptable.
22
+ //
23
+ // Only four things happen here:
24
+ // 1. Drop the XML prolog / DOCTYPE (figures.ts requires a single root <svg>; if left in, the
25
+ // theme injection breaks the prolog)
26
+ // 2. Drop the content attribute (the mxfile copy) from rendered output — it's editing data, not
27
+ // needed for distribution. Works whether "include a copy of the diagram" is ON or OFF at
28
+ // export
29
+ // 3. Drop the trailing "Text is not SVG - cannot display" fallback — it embeds an external
30
+ // link to drawio.com, and shipped figures shouldn't keep needless external links
31
+ // 4. Adjust the root's inline style so resolving light-dark() is left to the docs (below)
32
+ //
33
+ // What 4 does = hand only the "control over switching" light/dark to the docs:
34
+ // - draw.io writes `color-scheme: light dark` inline on the root. Inline beats the
35
+ // [data-doc-figure] style injected by figures.ts, so as-is the figure alone would follow the
36
+ // **OS setting** for light/dark and not the docs theme toggle (data-theme="dark"). So only
37
+ // the root's color-scheme declaration is removed, leaving it to figures.ts's theme injection.
38
+ // - For the background, point `--ge-dark-color` (the variable draw.io uses for dark surfaces)
39
+ // at the docs panel color. draw.io reads it through the variable, so this is a "stable
40
+ // override point".
41
+ //
42
+ // title / desc follow the hand-written figure convention: title is fixed to `Fig.<name>`
43
+ // (injected mechanically); desc = the figure description comes from the mxfile's <diagram name>
44
+ // (= the draw.io page name) — only available when "include a copy of the diagram" was ON at
45
+ // export. If OFF, there's no desc (not an error).
46
+ /** Unescape XML entities, numeric ones included (to extract the mxfile from content). */
47
+ const unescapeXml = (s) => s
48
+ .replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)))
49
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(Number.parseInt(h, 16)))
50
+ .replace(/&lt;/g, "<")
51
+ .replace(/&gt;/g, ">")
52
+ .replace(/&quot;/g, '"')
53
+ .replace(/&apos;/g, "'")
54
+ .replace(/&amp;/g, "&");
55
+ /** draw.io's default page names — meaningless as a description, so not turned into <desc>. */
56
+ const DEFAULT_PAGE_NAMES = new Set(["page-1", "ページ-1", "page 1", "untitled", ""]);
57
+ // The content attribute is "one long attribute carrying the mxfile".
58
+ const CONTENT_ATTR = /\scontent="(?:[^"\\]|\\.)*"/;
59
+ const rootHead = (raw) => {
60
+ const start = raw.indexOf("<svg");
61
+ if (start < 0) {
62
+ return undefined;
63
+ }
64
+ const end = raw.indexOf(">", start);
65
+ return raw.slice(start, end < 0 ? undefined : end + 1);
66
+ };
67
+ /**
68
+ * Extract the mxfile XML from the root <svg>'s content attribute.
69
+ * A prolog / DOCTYPE may precede it, so cut the start tag from the `<svg` position (the first `>`
70
+ * belongs to the prolog).
71
+ */
72
+ export const extractMxfile = (raw) => {
73
+ const head = rootHead(raw);
74
+ const m = head ? /\scontent="([^"]*)"/.exec(head) : null;
75
+ const xml = m?.[1] ? unescapeXml(m[1]) : undefined;
76
+ return xml?.includes("<mxfile") || xml?.includes("<mxGraphModel") ? xml : undefined;
77
+ };
78
+ /** The mxfile's first <diagram name="..."> = the draw.io page name (= figure description). */
79
+ export const mxfilePageName = (mxfile) => {
80
+ const m = /<diagram[^>]*\sname="([^"]*)"/.exec(mxfile);
81
+ const name = m?.[1] ? unescapeXml(m[1]).trim() : "";
82
+ return name ? name : undefined;
83
+ };
84
+ /**
85
+ * Whether an svg in figures/ was exported by draw.io. To avoid catching hand-written figures, it
86
+ * checks for traces only draw.io emits: the mxfile copy (content attribute) / the drawio.com
87
+ * fallback <switch> / draw.io CSS variables (`--ge-*`) / an inline `color-scheme` on the root.
88
+ */
89
+ export const isDrawioSvg = (raw) => {
90
+ if (extractMxfile(raw)) {
91
+ return true;
92
+ }
93
+ if (raw.includes("Text is not SVG - cannot display") || raw.includes("--ge-")) {
94
+ return true;
95
+ }
96
+ const head = rootHead(raw);
97
+ return !!head && /\sstyle="[^"]*color-scheme\s*:/.test(head);
98
+ };
99
+ // Strip the whole leading prolog. draw.io emits `<?xml?>` → `<!-- Do not edit this file with
100
+ // editors other than draw.io -->` → `<!DOCTYPE>` in that order (the .drawio.svg Save format inserts
101
+ // the comment), so peel items off until the root <svg> comes first, without assuming kind or order.
102
+ const PROLOG_ITEM = /^\s*(?:<\?xml[^>]*\?>|<!DOCTYPE[^>[]*(?:\[[\s\S]*?\])?>|<!--[\s\S]*?-->)\s*/i;
103
+ const stripProlog = (s) => {
104
+ let out = s.replace(/^\uFEFF/, "");
105
+ for (let m = PROLOG_ITEM.exec(out); m; m = PROLOG_ITEM.exec(out)) {
106
+ out = out.slice(m[0].length);
107
+ }
108
+ return out.trim();
109
+ };
110
+ // For renderers that can't draw foreignObject, draw.io appends a <switch> embedding an <a> link
111
+ // to drawio.com. Drop it so shipped figures don't keep a needless external link.
112
+ const stripFallbackLink = (s) => s.replace(/<switch>\s*<g\s+requiredFeatures="[^"]*"\s*\/>\s*<a\b[^>]*>[\s\S]*?<\/a>\s*<\/switch>/g, "");
113
+ /** Background the docs side can override (the variable draw.io uses for dark surfaces). */
114
+ const DARK_SURFACE = "--ge-dark-color: var(--doc-panel, #161b22)";
115
+ /**
116
+ * Remove the `color-scheme` declaration from the root <svg>'s inline style and point only the
117
+ * dark surface color at the docs. If color-scheme stayed, its inline strength would beat
118
+ * figures.ts's theme injection and the figure alone would follow the OS light/dark setting
119
+ * (= not follow the docs theme toggle).
120
+ */
121
+ const adaptRootStyle = (markup) => {
122
+ const open = markup.indexOf(">");
123
+ if (!markup.startsWith("<svg") || open < 0) {
124
+ return markup;
125
+ }
126
+ const head = markup.slice(0, open);
127
+ const rest = markup.slice(open);
128
+ const m = /\sstyle="([^"]*)"/.exec(head);
129
+ if (!m) {
130
+ return `${head} style="${DARK_SURFACE}"${rest}`;
131
+ }
132
+ const decls = (m[1] ?? "")
133
+ .split(";")
134
+ .map((d) => d.trim())
135
+ .filter((d) => d && !/^color-scheme\s*:/i.test(d));
136
+ decls.push(DARK_SURFACE);
137
+ return `${head.slice(0, m.index)} style="${decls.join("; ")}"${head.slice(m.index + m[0].length)}${rest}`;
138
+ };
139
+ /**
140
+ * Normalize a draw.io-exported SVG into an SVG figures.ts can handle. `name` is the figure's name
141
+ * (= the name in figures/<name>.svg, used for <title>Fig.<name></title>).
142
+ * The returned markup is still a raw <svg>…</svg> string; figures.ts goes on to do the theme
143
+ * injection and structural validation.
144
+ */
145
+ export const normalizeDrawioSvg = (name, raw) => {
146
+ const mxfile = extractMxfile(raw);
147
+ let markup = stripProlog(raw);
148
+ // Drop the content attribute — editing data doesn't go into the rendered output.
149
+ const headEnd = markup.indexOf(">");
150
+ if (headEnd >= 0) {
151
+ markup = markup.slice(0, headEnd).replace(CONTENT_ATTR, "") + markup.slice(headEnd);
152
+ }
153
+ markup = stripFallbackLink(markup).trim();
154
+ markup = adaptRootStyle(markup);
155
+ // Follow the same convention as hand-written figures: <title> fixed to Fig.<name>, <desc> is
156
+ // the description. The description is the draw.io page name (tab name) — only available from
157
+ // the mxfile copy included in the export.
158
+ //
159
+ // A default page name / no copy is **not an error**. <desc> is optional for figures/*.svg too
160
+ // (parseSvg accepts it missing); a figure without one just falls back to a caption of
161
+ // "Fig.<name>" and still renders = it isn't "unshippable".
162
+ const page = mxfile ? mxfilePageName(mxfile) : undefined;
163
+ const desc = page && !DEFAULT_PAGE_NAMES.has(page.toLowerCase()) ? page : undefined;
164
+ const esc = (v) => v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
165
+ const open = markup.indexOf(">");
166
+ if (open >= 0) {
167
+ // draw.io doesn't write <title>/<desc>, but drop any existing ones to avoid duplicates.
168
+ const body = markup
169
+ .slice(open + 1)
170
+ .replace(/^\s*<title[^>]*>[\s\S]*?<\/title>/, "")
171
+ .replace(/^\s*<desc[^>]*>[\s\S]*?<\/desc>/, "");
172
+ const meta = `<title>Fig.${esc(name)}</title>${desc ? `<desc>${esc(desc)}</desc>` : ""}`;
173
+ markup = `${markup.slice(0, open + 1)}${meta}${body}`;
174
+ }
175
+ return { markup, desc };
176
+ };
@@ -0,0 +1,14 @@
1
+ import type { FigureEntry } from "../core/content-types.js";
2
+ /** File relative to figures/ → the figure's registered name (stem without the extension). */
3
+ export declare const figureName: (file: string) => string;
4
+ /**
5
+ * Original file name of a figure added from the editing UI → a name following the figures/
6
+ * convention (svg / drawio.svg / html / raster). The stem is coerced to lowercase alphanumerics
7
+ * and _-, or "figure" if nothing remains. Unsupported extensions yield undefined.
8
+ */
9
+ export declare const figureUploadName: (raw: string) => string | undefined;
10
+ export type { FigureEntry } from "../core/content-types.js";
11
+ export declare const loadFigures: (figuresDir: string, extraFiles?: string[]) => {
12
+ figures: FigureEntry[];
13
+ errors: string[];
14
+ };
@@ -0,0 +1,221 @@
1
+ import { createHash } from "node:crypto";
2
+ // figures/ = the SoT for HTML-based Figs (figures). Two formats:
3
+ // - <name>.svg : self-contained SVG (xmlns + viewBox + <title>Fig.<name></title> +
4
+ // optional <desc>description</desc>)
5
+ // - <name>.html : self-contained HTML fragment (leading <!--fig title/desc --> meta comment,
6
+ // styles such as CSS animations bundled in <style>, no scripts)
7
+ // Both use var(--doc-*, fallback) for colors (follows the theme + works standalone). Content
8
+ // references them via `![alt text](figures/<file>)` (png / webp / jpeg become
9
+ // data-URI <img>s).
10
+ //
11
+ // Figures drawn in draw.io live in the same figures/. Two ways to place them; in both, name is
12
+ // the stem without the extension:
13
+ // - <name>.drawio.svg : output of draw.io's File > Save as (Editable SVG). Carries an mxfile
14
+ // copy (content=), so **one file is both editing source and output**
15
+ // (open in draw.io and Save to update; no re-export needed). Error if
16
+ // there's no mxfile
17
+ // - <name>.svg : output of File > Export as > SVG (the editing source can live
18
+ // anywhere). isDrawioSvg tells whether it's from draw.io (it coexists
19
+ // with hand-written figures)
20
+ // Both go through drawio.ts normalization (stripping prolog / mxfile copy / external links,
21
+ // injecting title) before the same validation as hand-written SVGs. Colors stay in draw.io's
22
+ // scheme (see the top of drawio.ts).
23
+ import { readdirSync, readFileSync } from "node:fs";
24
+ import { dirname } from "node:path";
25
+ import { extractMxfile, isDrawioSvg, normalizeDrawioSvg } from "./drawio.js";
26
+ import { textfilePath } from "./textfile.js";
27
+ const EXT_RE = /\.(drawio\.svg|svg|html|png|webp|gif|mp4|webm|jpe?g)$/;
28
+ const FILE_RE = /^[a-z0-9][a-z0-9_-]*\.(drawio\.svg|svg|html|png|webp|gif|mp4|webm|jpe?g)$/;
29
+ const IMAGE_MIME = {
30
+ png: "image/png",
31
+ webp: "image/webp",
32
+ gif: "image/gif",
33
+ jpg: "image/jpeg",
34
+ jpeg: "image/jpeg",
35
+ };
36
+ /** File relative to figures/ → the figure's registered name (stem without the extension). */
37
+ export const figureName = (file) => !FILE_RE.test(file.replace(/^figures\//, ""))
38
+ ? `file-${createHash("sha256").update(file).digest("hex").slice(0, 24)}`
39
+ : file.replace(/^figures\//, "").replace(EXT_RE, "");
40
+ /**
41
+ * Original file name of a figure added from the editing UI → a name following the figures/
42
+ * convention (svg / drawio.svg / html / raster). The stem is coerced to lowercase alphanumerics
43
+ * and _-, or "figure" if nothing remains. Unsupported extensions yield undefined.
44
+ */
45
+ export const figureUploadName = (raw) => {
46
+ const lower = raw.toLowerCase().split(/[\\/]/).pop() ?? "";
47
+ const ext = /\.(drawio\.svg|svg|html|png|webp|gif|mp4|webm|jpe?g)$/.exec(lower)?.[1];
48
+ if (ext == null) {
49
+ return undefined;
50
+ }
51
+ const stem = lower
52
+ .slice(0, -ext.length - 1)
53
+ .replace(/[^a-z0-9_-]+/g, "-")
54
+ .replace(/^[^a-z0-9]+|-+$/g, "");
55
+ return `${stem || "figure"}.${ext}`;
56
+ };
57
+ const LIGHT_VARS = "--doc-bg:#ffffff;--doc-bg-2:#f6f8fb;--doc-bg-3:#eef2f7;--doc-panel:#ffffff;--doc-panel-2:#f1f5fa;--doc-rail:#f3f6fa;--doc-border:#dde3eb;--doc-border-2:#c4ccd8;--doc-fg:#000000;--doc-fg-2:#2a3140;--doc-fg-3:#5b6271;--doc-fg-4:#9099a8;--doc-accent:#1b4994;--doc-accent-2:#0277bd;--doc-accent-fg:#ffffff;--doc-ok:#1f7a32;--doc-ok-2:#2ea043;--doc-warn:#b97600;--doc-warn-2:#f59f00;--doc-danger:#b53030;--doc-danger-2:#f04848;--doc-info:#0277bd;--doc-info-2:#4a8bff;";
58
+ const DARK_VARS = "--doc-bg:#000000;--doc-bg-2:#0a0d12;--doc-bg-3:#11161c;--doc-panel:#161b22;--doc-panel-2:#1e2530;--doc-rail:#0d1117;--doc-border:#222831;--doc-border-2:#323a47;--doc-fg:#ffffff;--doc-fg-2:#c9d1d9;--doc-fg-3:#8b939e;--doc-fg-4:#525a66;--doc-accent:#4a8bff;--doc-accent-2:#1b4994;--doc-accent-fg:#ffffff;--doc-ok:#2ea043;--doc-ok-2:#56d364;--doc-warn:#f59f00;--doc-warn-2:#ffc14d;--doc-danger:#f04848;--doc-danger-2:#ff7a7a;--doc-info:#4a8bff;--doc-info-2:#79b0ff;";
59
+ const FIGURE_THEME_STYLE = `<style data-doc-figure-theme>[data-doc-figure]{color-scheme:light;${LIGHT_VARS}}@media (prefers-color-scheme: dark){[data-doc-figure]{color-scheme:dark;${DARK_VARS}}}:root[data-theme="light"] [data-doc-figure],[data-doc-theme="light"] [data-doc-figure],[data-doc-figure][data-doc-theme="light"]{color-scheme:light;${LIGHT_VARS}}:root[data-theme="dark"] [data-doc-figure],[data-doc-theme="dark"] [data-doc-figure],[data-doc-figure][data-doc-theme="dark"]{color-scheme:dark;${DARK_VARS}}</style>`;
60
+ const firstTag = (svg, tag) => {
61
+ const m = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`).exec(svg);
62
+ return m ? m[1]?.replace(/&lt;/g, "<").replace(/&gt;/g, ">").trim() : undefined;
63
+ };
64
+ const HTML_META_RE = /^<!--fig\n([\s\S]*?)-->\n?/;
65
+ const normalizeHardcodedSurfaces = (markup) => markup
66
+ .replace(/background-color:\s*#fff(?:fff)?(?=[;}])/gi, "background-color:var(--doc-panel, #ffffff)")
67
+ .replace(/background:\s*#fff(?:fff)?(?=[;}])/gi, "background:var(--doc-panel, #ffffff)");
68
+ const withSvgTheme = (markup) => {
69
+ const headEnd = markup.indexOf(">");
70
+ if (headEnd < 0) {
71
+ return markup;
72
+ }
73
+ const head = markup.slice(0, headEnd);
74
+ const taggedHead = head.includes("data-doc-figure") ? head : `${head} data-doc-figure=""`;
75
+ return `${taggedHead}>${FIGURE_THEME_STYLE}${markup.slice(headEnd + 1)}`;
76
+ };
77
+ const withHtmlTheme = (markup) => {
78
+ const themed = normalizeHardcodedSurfaces(markup);
79
+ if (themed.includes("data-doc-figure") || !themed.startsWith("<")) {
80
+ return themed;
81
+ }
82
+ // A fragment can start with <style>; keep the theme outside authored elements.
83
+ return `<div data-doc-figure="">${FIGURE_THEME_STYLE}${themed}</div>`;
84
+ };
85
+ const parseSvg = (file, raw, errors) => {
86
+ const markup = withSvgTheme(normalizeHardcodedSurfaces(raw.trim()));
87
+ if (!markup.startsWith("<svg") || !markup.endsWith("</svg>")) {
88
+ errors.push(`${file}: a single root <svg>…</svg> is required`);
89
+ }
90
+ const head = markup.slice(0, markup.indexOf(">"));
91
+ if (!head.includes("xmlns=")) {
92
+ errors.push(`${file}: root <svg> requires xmlns (for standalone publishing)`);
93
+ }
94
+ if (!head.includes("viewBox=")) {
95
+ errors.push(`${file}: root <svg> requires viewBox (to scale with the width)`);
96
+ }
97
+ return {
98
+ kind: "svg",
99
+ markup,
100
+ title: firstTag(markup, "title"),
101
+ desc: firstTag(markup, "desc"),
102
+ };
103
+ };
104
+ const parseHtml = (file, raw, errors) => {
105
+ const m = HTML_META_RE.exec(raw);
106
+ if (!m) {
107
+ errors.push(`${file}: a <!--fig\\ntitle: ...\\n--> meta comment is required at the top`);
108
+ return { kind: "html", markup: raw.trim() };
109
+ }
110
+ const meta = new Map((m[1] ?? "")
111
+ .split("\n")
112
+ .filter((l) => l.includes(":"))
113
+ .map((l) => [l.slice(0, l.indexOf(":")).trim(), l.slice(l.indexOf(":") + 1).trim()]));
114
+ const markup = withHtmlTheme(raw.slice(m[0].length).trim());
115
+ if (!meta.get("title")) {
116
+ errors.push(`${file}: the meta comment requires title:`);
117
+ }
118
+ if (!markup.startsWith("<")) {
119
+ errors.push(`${file}: the meta comment must be followed by an HTML fragment`);
120
+ }
121
+ if (/<script/i.test(markup)) {
122
+ errors.push(`${file}: <script> is not allowed (self-contained static fragments only)`);
123
+ }
124
+ return { kind: "html", markup, title: meta.get("title"), desc: meta.get("desc") };
125
+ };
126
+ export const loadFigures = (figuresDir, extraFiles = []) => {
127
+ const errors = [];
128
+ const figures = [];
129
+ let files = [];
130
+ try {
131
+ files = readdirSync(figuresDir)
132
+ .filter((f) => /\.(svg|html|png|webp|gif|mp4|webm|jpe?g)$/.test(f))
133
+ .sort();
134
+ }
135
+ catch {
136
+ // A missing figures/ is valid.
137
+ }
138
+ files = [...new Set([...files.map((file) => `figures/${file}`), ...extraFiles])];
139
+ const keyframeOwner = new Map();
140
+ for (const file of files) {
141
+ if (file.startsWith("figures/") &&
142
+ !file.slice(8).includes("/") &&
143
+ !FILE_RE.test(file.slice(8))) {
144
+ errors.push(`${file}: file name must be <name>.svg|.drawio.svg|.html|.png|.webp|.jpg|.jpeg ([a-z0-9_-])`);
145
+ continue;
146
+ }
147
+ const name = figureName(file);
148
+ const dup = figures.find((f) => f.name === name);
149
+ if (dup) {
150
+ errors.push(`${file}: duplicate name "${name}" (also used by ${dup.file})`);
151
+ continue;
152
+ }
153
+ if (/\.(mp4|webm)$/.test(file)) {
154
+ const data = readFileSync(file.startsWith("figures/")
155
+ ? textfilePath(figuresDir, file.slice(8))
156
+ : textfilePath(dirname(figuresDir), file));
157
+ figures.push({
158
+ name,
159
+ file,
160
+ kind: "video",
161
+ markup: `<video data-dooc-video controls playsinline preload="metadata" aria-label="Video preview" src="data:video/${file.endsWith(".mp4") ? "mp4" : "webm"};base64,${data.toString("base64")}"></video>`,
162
+ });
163
+ continue;
164
+ }
165
+ const mime = IMAGE_MIME[file.slice(file.lastIndexOf(".") + 1)];
166
+ if (mime != null) {
167
+ // Rasters are inlined as data URIs; Markdown embeds supply their alternative text.
168
+ const data = readFileSync(file.startsWith("figures/")
169
+ ? textfilePath(figuresDir, file.slice(8))
170
+ : textfilePath(dirname(figuresDir), file)).toString("base64");
171
+ figures.push({
172
+ name,
173
+ file,
174
+ kind: "image",
175
+ markup: `<img src="data:${mime};base64,${data}" alt="${name}">`,
176
+ });
177
+ continue;
178
+ }
179
+ const raw = readFileSync(file.startsWith("figures/")
180
+ ? textfilePath(figuresDir, file.slice(8))
181
+ : textfilePath(dirname(figuresDir), file), "utf8");
182
+ // The point of .drawio.svg is "can be reopened in draw.io", so the mxfile copy is required
183
+ // (rejects an Export as > SVG output without the copy that was merely renamed).
184
+ const editable = file.endsWith(".drawio.svg");
185
+ if (editable && !extractMxfile(raw)) {
186
+ errors.push(`${file}: .drawio.svg doubles as the editable source, so it requires a copy of the mxfile (content=) (save it as .drawio.svg via File > Save as in draw.io)`);
187
+ }
188
+ const parsed = file.endsWith(".svg")
189
+ ? parseSvg(file, editable || isDrawioSvg(raw) ? normalizeDrawioSvg(name, raw).markup : raw, errors)
190
+ : parseHtml(file, raw, errors);
191
+ if (/var\(--doc-[a-z0-9-]+\)/.test(parsed.markup)) {
192
+ errors.push(`${file}: contains var(--doc-*) without a fallback (use the var(--doc-x, #hex) form)`);
193
+ }
194
+ // @keyframes names must not collide when several figures are inlined on one page.
195
+ // The same name with an identical body is harmless (e.g. xyflow's dashdraw, present in
196
+ // every capture figure), so that's allowed.
197
+ for (const km of parsed.markup.matchAll(/@keyframes\s+([A-Za-z0-9_-]+)\s*\{/g)) {
198
+ const kname = km[1] ?? "";
199
+ let depth = 1;
200
+ let i = (km.index ?? 0) + km[0].length;
201
+ while (i < parsed.markup.length && depth > 0) {
202
+ const ch = parsed.markup[i];
203
+ depth += ch === "{" ? 1 : ch === "}" ? -1 : 0;
204
+ i += 1;
205
+ }
206
+ const body = parsed.markup.slice((km.index ?? 0) + km[0].length, i).replace(/\s+/g, "");
207
+ const key = `${kname}`;
208
+ const owner = keyframeOwner.get(key);
209
+ if (owner && owner.file !== file && owner.body !== body) {
210
+ errors.push(`${file}: @keyframes "${kname}" conflicts with a different definition in ${owner.file}`);
211
+ }
212
+ keyframeOwner.set(key, { file, body });
213
+ }
214
+ figures.push({
215
+ name,
216
+ file,
217
+ ...parsed,
218
+ });
219
+ }
220
+ return { figures, errors };
221
+ };