@godcreator/word-report 0.4.0 → 0.4.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.
@@ -0,0 +1,1289 @@
1
+ import { a as walk, r as stableValue, s as ReportContentError, t as assembleDocument } from "./assemble-BpllQNVY.mjs";
2
+ import remarkFrontmatter from "remark-frontmatter";
3
+ import remarkGfm from "remark-gfm";
4
+ import remarkMath from "remark-math";
5
+ import remarkMdx from "remark-mdx";
6
+ import remarkParse from "remark-parse";
7
+ import { unified } from "unified";
8
+ import { parse, stringify } from "yaml";
9
+ //#region src/core/mdx/parse.ts
10
+ const mdxParser = unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkMdx).use(remarkGfm).use(remarkMath);
11
+ /** 源位置写成「行:列」,放在 ReportContentError 的 pointer 里 */
12
+ const positionOf = (node) => node.position ? `${node.position.start.line}:${node.position.start.column}` : "";
13
+ /** 只解析、不执行 MDX:标签没闭合、正文里裸的 < 与 { 这类语法错在这里报出,补上文件与行列再抛 */
14
+ function parseMdx(text, file) {
15
+ try {
16
+ return mdxParser.parse(text);
17
+ } catch (error) {
18
+ const place = error;
19
+ const at = place.line ? `${place.line}:${place.column ?? 1}` : "";
20
+ throw new ReportContentError(`MDX 语法错误:${place.reason ?? place.message ?? String(error)}`, file, at);
21
+ }
22
+ }
23
+ //#endregion
24
+ //#region src/core/mdx/context.ts
25
+ function located(context, node, source) {
26
+ context.locations.set(node, {
27
+ file: context.file,
28
+ pointer: positionOf(source)
29
+ });
30
+ return node;
31
+ }
32
+ function failAt(context, source, message) {
33
+ throw new ReportContentError(message, context.file, positionOf(source));
34
+ }
35
+ /** 只含注释的花括号表达式(MDX 里写注释的唯一办法):解析后的程序体是空的 */
36
+ function isCommentOnlyExpression(node) {
37
+ return (node.type === "mdxFlowExpression" || node.type === "mdxTextExpression") && node.data?.estree?.body?.length === 0;
38
+ }
39
+ //#endregion
40
+ //#region src/core/mdx/ids.ts
41
+ const ID_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
42
+ /** 编译器生成的 id 都以它开头;手写的 id 不许用,两边就不会撞 */
43
+ const GENERATED_ID_PREFIX = "auto-";
44
+ /** 手写的 id:规则照 docreport,另外不许占用生成 id 的前缀 */
45
+ function explicitIdOf(raw, file, at) {
46
+ if (!ID_PATTERN.test(raw)) throw new ReportContentError(`id「${raw}」不合规则:要以字母开头,只用字母、数字、_ 与 -,最长 64 个字符`, file, at);
47
+ if (raw.startsWith("auto-")) throw new ReportContentError(`id「${raw}」以 ${GENERATED_ID_PREFIX} 开头:这个前缀留给编译器生成的 id,手写的 id 换个名字`, file, at);
48
+ return raw;
49
+ }
50
+ function fnv1a36(text) {
51
+ let hash = 2166136261;
52
+ for (let i = 0; i < text.length; i++) {
53
+ hash ^= text.charCodeAt(i);
54
+ hash = Math.imul(hash, 16777619);
55
+ }
56
+ return (hash >>> 0).toString(36);
57
+ }
58
+ function withoutIds(value) {
59
+ if (Array.isArray(value)) return value.map(withoutIds);
60
+ if (value && typeof value === "object") {
61
+ const copy = {};
62
+ for (const [key, child] of Object.entries(value)) if (key !== "id") copy[key] = withoutIds(child);
63
+ return copy;
64
+ }
65
+ return value;
66
+ }
67
+ /**
68
+ * 没写 id 的节点先登记,整份文档转完后一次分配:`auto-<种类>-<指纹>`,指纹取种类加去掉所有 id 的节点内容
69
+ * (节只取标题:节里改了字,标题的锚点与目录链接不跟着变),同一份内容每次编译得到同一个 id;内容相同的按登记先后加 -2、-3
70
+ */
71
+ var GeneratedIds = class {
72
+ pending = [];
73
+ /** signature 不给就用整个节点(去掉 id);登记时节点的子内容可以还没填完,分配时才取指纹 */
74
+ request(node, kind, signature) {
75
+ this.pending.push({
76
+ node,
77
+ kind,
78
+ signature
79
+ });
80
+ return node;
81
+ }
82
+ assignAll() {
83
+ const occurrences = /* @__PURE__ */ new Map();
84
+ for (const { node, kind, signature } of this.pending) {
85
+ const base = `${GENERATED_ID_PREFIX}${kind}-${fnv1a36(`${kind}:${stableValue(withoutIds(signature ?? node))}`)}`;
86
+ const occurrence = (occurrences.get(base) ?? 0) + 1;
87
+ occurrences.set(base, occurrence);
88
+ node.id = occurrence === 1 ? base : `${base}-${occurrence}`;
89
+ }
90
+ }
91
+ };
92
+ //#endregion
93
+ //#region src/core/mdx/jsx.ts
94
+ /** 组件属性只收字符串字面量:写成 {…} 表达式、展开、没列出的属性、缺必填的属性都报错 */
95
+ function attributesOf(context, element, spec) {
96
+ const values = {};
97
+ for (const attribute of element.attributes) {
98
+ if (attribute.type !== "mdxJsxAttribute") failAt(context, element, `<${element.name}> 上不能写 {…} 展开属性`);
99
+ const name = attribute.name;
100
+ if (!Object.hasOwn(spec, name)) {
101
+ const allowed = Object.keys(spec);
102
+ failAt(context, element, `<${element.name}> 没有属性 ${name}${allowed.length ? `;能写的只有 ${allowed.join("、")}` : ";它不收任何属性"}`);
103
+ }
104
+ if (typeof attribute.value !== "string") failAt(context, element, `<${element.name}> 的 ${name} 要写成字符串,如 ${name}="…",不写成 {…} 也不省略值`);
105
+ if (Object.hasOwn(values, name)) failAt(context, element, `<${element.name}> 的 ${name} 写了两次`);
106
+ values[name] = attribute.value;
107
+ }
108
+ for (const [name, need] of Object.entries(spec)) if (need === "required" && !Object.hasOwn(values, name)) failAt(context, element, `<${element.name}> 缺少属性 ${name}`);
109
+ return values;
110
+ }
111
+ function positiveIntegerOf(context, element, name, raw) {
112
+ if (!/^[1-9]\d*$/.test(raw)) failAt(context, element, `<${element.name}> 的 ${name} 要是正整数,这里写的是「${raw}」`);
113
+ return Number(raw);
114
+ }
115
+ const isWhitespaceText$1 = (node) => node.type === "text" && !String(node.value).trim();
116
+ /**
117
+ * 组件的子元素:标签各占一行时是块级元素,写在同一行时被包进一个段落成了行内元素;两种写法都认,
118
+ * 去掉空白与纯注释后只剩元素,别的内容报错
119
+ */
120
+ function elementChildrenOf(context, element) {
121
+ const children = [];
122
+ const collect = (nodes) => {
123
+ for (const node of nodes) {
124
+ if (isWhitespaceText$1(node) || isCommentOnlyExpression(node)) continue;
125
+ if (node.type === "paragraph") collect(node.children);
126
+ else if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") children.push(node);
127
+ else failAt(context, node, `<${element.name}> 里只能放组件,这里出现了 ${node.type}`);
128
+ }
129
+ };
130
+ collect(element.children);
131
+ return children;
132
+ }
133
+ const FLOW_TYPES = /* @__PURE__ */ new Set([
134
+ "paragraph",
135
+ "heading",
136
+ "list",
137
+ "code",
138
+ "math",
139
+ "table",
140
+ "blockquote",
141
+ "thematicBreak",
142
+ "html",
143
+ "mdxJsxFlowElement",
144
+ "mdxFlowExpression",
145
+ "footnoteDefinition",
146
+ "definition"
147
+ ]);
148
+ /** 组件里的一行文字(图题、表题、小标题):标签在同一行时直接是行内内容,各占一行时包在一个段落里 */
149
+ function phrasingChildrenOf(context, element) {
150
+ const children = element.children.filter((node) => !isCommentOnlyExpression(node));
151
+ if (children.length === 1 && children[0].type === "paragraph") return children[0].children;
152
+ if (children.every((node) => !FLOW_TYPES.has(node.type))) return children;
153
+ return failAt(context, element, `<${element.name}> 里只能写一段文字`);
154
+ }
155
+ //#endregion
156
+ //#region src/core/mdx/inline.ts
157
+ const MARK_ORDER = [
158
+ "strong",
159
+ "em",
160
+ "sub",
161
+ "sup",
162
+ "code"
163
+ ];
164
+ const MARK_ELEMENTS = {
165
+ strong: "strong",
166
+ em: "em",
167
+ sub: "sub",
168
+ sup: "sup"
169
+ };
170
+ /** 只能单独成段的组件:写进一段文字中间时报错 */
171
+ const BLOCK_COMPONENTS = /* @__PURE__ */ new Set([
172
+ "Figure",
173
+ "Figures",
174
+ "Table",
175
+ "Abstract",
176
+ "Bibliography",
177
+ "Reference",
178
+ "Caption",
179
+ "Panel",
180
+ "table",
181
+ "tr",
182
+ "th",
183
+ "td"
184
+ ]);
185
+ /** 一段行内 MDX → docreport 行内节点:相邻、标记相同的文字合并,软换行按两侧字符连上或换成空格 */
186
+ function inlinesOf(context, nodes) {
187
+ const collected = [];
188
+ collectInlines(context, nodes, [], collected);
189
+ return joinedSoftBreaks(mergedAdjacentTexts(collected));
190
+ }
191
+ function textWithMarks(text, marks) {
192
+ const sorted = MARK_ORDER.filter((mark) => marks.includes(mark));
193
+ return sorted.length ? {
194
+ type: "text",
195
+ text,
196
+ marks: sorted
197
+ } : {
198
+ type: "text",
199
+ text
200
+ };
201
+ }
202
+ function idTargetsOf(context, element, raw) {
203
+ const targets = raw.split(/\s+/).filter(Boolean);
204
+ if (!targets.length) failAt(context, element, `<${element.name}> 的 to 不能空`);
205
+ return targets.map((target) => explicitIdOf(target, context.file, positionOf(element)));
206
+ }
207
+ function rejectChildren(context, element) {
208
+ if (element.children.some((child) => !isCommentOnlyExpression(child))) failAt(context, element, `<${element.name}> 不收子内容,写成 <${element.name} … />`);
209
+ }
210
+ function collectInlines(context, nodes, marks, out) {
211
+ for (const node of nodes) switch (node.type) {
212
+ case "text":
213
+ out.push(located(context, textWithMarks(node.value, marks), node));
214
+ break;
215
+ case "emphasis":
216
+ collectInlines(context, node.children, [...marks, "em"], out);
217
+ break;
218
+ case "strong":
219
+ collectInlines(context, node.children, [...marks, "strong"], out);
220
+ break;
221
+ case "inlineCode":
222
+ out.push(located(context, textWithMarks(node.value, [...marks, "code"]), node));
223
+ break;
224
+ case "inlineMath":
225
+ out.push(located(context, context.ids.request({
226
+ type: "math",
227
+ id: "",
228
+ latex: node.value
229
+ }, "m"), node));
230
+ break;
231
+ case "break":
232
+ out.push(located(context, { type: "break" }, node));
233
+ break;
234
+ case "link":
235
+ out.push(located(context, context.ids.request({
236
+ type: "link",
237
+ id: "",
238
+ text: linkTextOf(context, node),
239
+ href: node.url
240
+ }, "link"), node));
241
+ break;
242
+ case "footnoteReference":
243
+ out.push(located(context, context.ids.request({
244
+ type: "note_ref",
245
+ id: "",
246
+ target: explicitIdOf(node.label ?? node.identifier, context.file, positionOf(node))
247
+ }, "fn"), node));
248
+ break;
249
+ case "mdxJsxTextElement":
250
+ collectElement(context, node, marks, out);
251
+ break;
252
+ case "mdxTextExpression":
253
+ if (!isCommentOnlyExpression(node)) failAt(context, node, "正文里不能写 {…} 表达式;只想写花括号就写成 \\{ \\},注释写成 {/* … */}");
254
+ break;
255
+ case "image":
256
+ case "imageReference": failAt(context, node, "Markdown 图片 ![…](…) 不收:图写成 <Figure src=\"assets/…\">图题</Figure>");
257
+ case "linkReference": failAt(context, node, "不支持引用式链接 [文字][名字]:链接写成 [文字](https://…)");
258
+ case "delete": failAt(context, node, "不支持删除线 ~~…~~");
259
+ case "html": failAt(context, node, "不支持原始 HTML");
260
+ default: failAt(context, node, `这里不能写 ${node.type}`);
261
+ }
262
+ }
263
+ function collectElement(context, element, marks, out) {
264
+ const name = element.name;
265
+ if (name !== null && Object.hasOwn(MARK_ELEMENTS, name)) {
266
+ attributesOf(context, element, {});
267
+ collectInlines(context, element.children, [...marks, MARK_ELEMENTS[name]], out);
268
+ return;
269
+ }
270
+ switch (name) {
271
+ case "Ref": {
272
+ const attributes = attributesOf(context, element, {
273
+ to: "required",
274
+ show: "optional"
275
+ });
276
+ rejectChildren(context, element);
277
+ if (/\s/.test(attributes["to"].trim())) failAt(context, element, "<Ref> 的 to 只能写一个 id");
278
+ const [target] = idTargetsOf(context, element, attributes["to"]);
279
+ const show = attributes["show"];
280
+ if (show !== void 0 && show !== "number" && show !== "title") failAt(context, element, `<Ref> 的 show 只能是 number 或 title(不写就显示「图 3-2」这种全称),这里写的是「${show}」`);
281
+ const display = show ?? "label";
282
+ out.push(located(context, context.ids.request({
283
+ type: "ref",
284
+ id: "",
285
+ target,
286
+ display
287
+ }, "ref"), element));
288
+ return;
289
+ }
290
+ case "Cite": {
291
+ const attributes = attributesOf(context, element, { to: "required" });
292
+ rejectChildren(context, element);
293
+ out.push(located(context, context.ids.request({
294
+ type: "cite",
295
+ id: "",
296
+ targets: idTargetsOf(context, element, attributes["to"])
297
+ }, "cite"), element));
298
+ return;
299
+ }
300
+ case "br":
301
+ attributesOf(context, element, {});
302
+ rejectChildren(context, element);
303
+ out.push(located(context, { type: "break" }, element));
304
+ return;
305
+ default:
306
+ if (name !== null && BLOCK_COMPONENTS.has(name)) failAt(context, element, `<${name}> 要单独成段,前后空一行,不写在一段文字中间`);
307
+ failAt(context, element, `认不出组件 <${name ?? ""}>:行内只能用 Ref、Cite、sub、sup、strong、em、br`);
308
+ }
309
+ }
310
+ function linkTextOf(context, link) {
311
+ const inlines = [];
312
+ collectInlines(context, link.children, [], inlines);
313
+ if (inlines.some((inline) => inline.type !== "text" || inline.marks)) failAt(context, link, "链接文字只能是纯文字");
314
+ return inlines.map((inline) => inline.text).join("");
315
+ }
316
+ const sameMarks = (a, b) => (a.marks ?? []).join() === (b.marks ?? []).join();
317
+ function mergedAdjacentTexts(inlines) {
318
+ const merged = [];
319
+ for (const inline of inlines) {
320
+ const previous = merged.at(-1);
321
+ if (inline.type === "text" && previous?.type === "text" && sameMarks(previous, inline)) previous.text += inline.text;
322
+ else merged.push(inline);
323
+ }
324
+ return merged;
325
+ }
326
+ /** 中日韩文字、全角标点与全角符号:软换行两侧都是它们时直接连上,不留空格 */
327
+ const CJK = /[⺀-⿿ -〿぀-ヿ㄀-ㇿ㐀-䶿一-鿿豈-﫿︰-﹏＀-￯]/;
328
+ function joinedSoftBreaks(inlines) {
329
+ const lastCharOf = (inline) => inline?.type === "text" ? inline.text.at(-1) : void 0;
330
+ const firstCharOf = (inline) => inline?.type === "text" ? inline.text[0] : void 0;
331
+ inlines.forEach((inline, index) => {
332
+ if (inline.type !== "text") return;
333
+ inline.text = inline.text.replace(/[ \t]*\r?\n[ \t]*/g, (match, offset, whole) => {
334
+ const before = offset > 0 ? whole[offset - 1] : lastCharOf(inlines[index - 1]);
335
+ const after = offset + match.length < whole.length ? whole[offset + match.length] : firstCharOf(inlines[index + 1]);
336
+ return before !== void 0 && after !== void 0 && CJK.test(before) && CJK.test(after) ? "" : " ";
337
+ });
338
+ });
339
+ return inlines.filter((inline) => inline.type !== "text" || inline.text !== "");
340
+ }
341
+ /** 图题这类行内内容的纯文字:图不写 alt 时拿它当 alt */
342
+ function plainTextOf(inlines) {
343
+ return inlines.map((inline) => {
344
+ switch (inline.type) {
345
+ case "text": return inline.text;
346
+ case "math": return `$${inline.latex}$`;
347
+ case "link": return inline.text;
348
+ case "break": return " ";
349
+ default: return "";
350
+ }
351
+ }).join("").trim();
352
+ }
353
+ //#endregion
354
+ //#region src/core/mdx/blocks.ts
355
+ const ALLOWED_BLOCKS = {
356
+ top: /* @__PURE__ */ new Set([
357
+ "paragraph",
358
+ "list",
359
+ "figure",
360
+ "table",
361
+ "equation",
362
+ "code",
363
+ "bibliography",
364
+ "abstract"
365
+ ]),
366
+ listItem: /* @__PURE__ */ new Set(["paragraph", "list"]),
367
+ cell: /* @__PURE__ */ new Set([
368
+ "paragraph",
369
+ "list",
370
+ "figure",
371
+ "table",
372
+ "equation",
373
+ "code"
374
+ ]),
375
+ note: /* @__PURE__ */ new Set([
376
+ "paragraph",
377
+ "list",
378
+ "code"
379
+ ]),
380
+ abstract: /* @__PURE__ */ new Set(["paragraph"])
381
+ };
382
+ const BLOCK_NAMES = {
383
+ paragraph: "段落",
384
+ list: "列表",
385
+ figure: "图",
386
+ table: "表",
387
+ equation: "公式",
388
+ code: "代码块",
389
+ bibliography: "<Bibliography />",
390
+ abstract: "<Abstract>"
391
+ };
392
+ const PLACE_NAMES = {
393
+ top: "正文",
394
+ listItem: "列表项",
395
+ cell: "表格单元格",
396
+ note: "脚注",
397
+ abstract: "<Abstract>"
398
+ };
399
+ const explicitOrRequested = (context, node, rawId, source, kind) => {
400
+ if (rawId !== void 0) node.id = explicitIdOf(rawId, context.file, positionOf(source));
401
+ else context.ids.request(node, kind);
402
+ return located(context, node, source);
403
+ };
404
+ /** 一个文件最外层的内容:按标题建节(# 是章),标题之间的块放进最近的节 */
405
+ function sectionsOf(context, root, fileNumbering) {
406
+ const top = [];
407
+ const open = [];
408
+ for (const node of root.children) {
409
+ if (node.type === "yaml") continue;
410
+ if (node.type === "heading") {
411
+ while (open.length && open.at(-1).depth >= node.depth) open.pop();
412
+ const parent = open.at(-1);
413
+ const parentDepth = parent?.depth ?? 0;
414
+ if (node.depth !== parentDepth + 1) failAt(context, node, parentDepth === 0 ? `这里是文件里最外层的标题,要写成一级标题 #,写成了 ${node.depth} 级` : `标题跳级:上一级是 ${parentDepth} 级,这里写了 ${node.depth} 级`);
415
+ const section = sectionOf(context, node, !parent ? fileNumbering : parent.section.numbering === "none" ? "none" : "decimal");
416
+ (parent?.section.children ?? top).push(section);
417
+ open.push({
418
+ depth: node.depth,
419
+ section
420
+ });
421
+ continue;
422
+ }
423
+ (open.at(-1)?.section.children ?? top).push(...blocksOf(context, [node], "top"));
424
+ }
425
+ return top;
426
+ }
427
+ function sectionOf(context, heading, numbering) {
428
+ if (heading.children.some((child) => child.type !== "text")) failAt(context, heading, "标题里只能写文字(不能有公式、加粗、链接、组件)");
429
+ const raw = heading.children.map((child) => child.value).join("");
430
+ const anchor = /\s*\[#([^\]]*)\]\s*$/.exec(raw);
431
+ const title = (anchor ? raw.slice(0, anchor.index) : raw).trim();
432
+ if (!title) failAt(context, heading, "标题不能空");
433
+ const section = {
434
+ type: "section",
435
+ id: "",
436
+ title,
437
+ numbering,
438
+ children: []
439
+ };
440
+ if (anchor) {
441
+ section.id = explicitIdOf(anchor[1], context.file, positionOf(heading));
442
+ return located(context, section, heading);
443
+ }
444
+ context.ids.request(section, "sec", title);
445
+ return located(context, section, heading);
446
+ }
447
+ /** 一串块级 MDX → docreport 块;放在不允许的位置的块报错 */
448
+ function blocksOf(context, nodes, place) {
449
+ const blocks = [];
450
+ for (const node of nodes) for (const block of blockOf(context, node, place)) {
451
+ if (!ALLOWED_BLOCKS[place].has(block.type)) failAt(context, node, `${PLACE_NAMES[place]}里不能放${BLOCK_NAMES[block.type] ?? block.type}`);
452
+ blocks.push(block);
453
+ }
454
+ return blocks;
455
+ }
456
+ const isWhitespaceText = (node) => node.type === "text" && !String(node.value).trim();
457
+ function blockOf(context, node, place) {
458
+ switch (node.type) {
459
+ case "paragraph": {
460
+ const meaningful = node.children.filter((child) => !isCommentOnlyExpression(child) && !isWhitespaceText(child));
461
+ if (meaningful.length === 0) return [];
462
+ const only = meaningful.length === 1 ? meaningful[0] : void 0;
463
+ if (only?.type === "mdxJsxTextElement" && BLOCK_COMPONENTS.has(only.name)) return [componentOf(context, only, place)];
464
+ return [paragraphOf(context, node.children, node)];
465
+ }
466
+ case "list": return [listOf(context, node)];
467
+ case "code": return [codeOf(context, node)];
468
+ case "math":
469
+ if (node.meta) failAt(context, node, "$$ 公式不编号、不写 id;要编号或被引用就写成 ```math <id> 代码块");
470
+ return [located(context, context.ids.request({
471
+ type: "equation",
472
+ id: "",
473
+ latex: node.value,
474
+ numbered: false
475
+ }, "eq"), node)];
476
+ case "table": return [tableFromGfm(context, node, false, void 0, void 0)];
477
+ case "mdxJsxFlowElement": return [componentOf(context, node, place)];
478
+ case "mdxFlowExpression":
479
+ if (!isCommentOnlyExpression(node)) failAt(context, node, "正文里不能写 {…} 表达式;注释写成 {/* … */}");
480
+ return [];
481
+ case "footnoteDefinition": {
482
+ const id = explicitIdOf(node.label ?? node.identifier, context.file, positionOf(node));
483
+ context.notes.push(located(context, {
484
+ id,
485
+ blocks: blocksOf(context, node.children, "note")
486
+ }, node));
487
+ return [];
488
+ }
489
+ case "heading": return failAt(context, node, "标题只能写在文件的最外层,不能写进列表、组件或脚注");
490
+ case "mdxjsEsm": return failAt(context, node, "报告里不写 import / export:组件都是内置的");
491
+ case "blockquote": return failAt(context, node, "不支持引用块 >");
492
+ case "thematicBreak": return failAt(context, node, "不支持分隔线");
493
+ case "html": return failAt(context, node, "不支持原始 HTML");
494
+ case "definition": return failAt(context, node, "不支持链接定义 [名字]: 地址");
495
+ case "yaml": return failAt(context, node, "文件头只能写在文件开头");
496
+ default: return failAt(context, node, `这里不能写 ${node.type}`);
497
+ }
498
+ }
499
+ function paragraphOf(context, children, source) {
500
+ return located(context, context.ids.request({
501
+ type: "paragraph",
502
+ id: "",
503
+ content: inlinesOf(context, children)
504
+ }, "p"), source);
505
+ }
506
+ function listOf(context, node) {
507
+ const ordered = Boolean(node.ordered);
508
+ const list = {
509
+ type: "list",
510
+ id: "",
511
+ ordered,
512
+ items: []
513
+ };
514
+ if (ordered && typeof node.start === "number" && node.start !== 1) list.start = node.start;
515
+ for (const item of node.children) {
516
+ if (item.checked !== null && item.checked !== void 0) failAt(context, item, "不支持任务列表 [ ] / [x]");
517
+ list.items.push(located(context, context.ids.request({
518
+ id: "",
519
+ blocks: blocksOf(context, item.children, "listItem")
520
+ }, "li"), item));
521
+ }
522
+ return located(context, context.ids.request(list, "list"), node);
523
+ }
524
+ function codeOf(context, node) {
525
+ if (node.lang === "math") {
526
+ const meta = (node.meta ?? "").trim();
527
+ if (/\s/.test(meta)) failAt(context, node, "```math 后面只能写一个 id");
528
+ return explicitOrRequested(context, {
529
+ type: "equation",
530
+ id: "",
531
+ latex: node.value,
532
+ numbered: true
533
+ }, meta || void 0, node, "eq");
534
+ }
535
+ if (node.meta) failAt(context, node, `代码块的语言名 ${node.lang} 后面不能再写别的`);
536
+ return located(context, context.ids.request({
537
+ type: "code",
538
+ id: "",
539
+ language: node.lang ?? "text",
540
+ text: node.value
541
+ }, "code"), node);
542
+ }
543
+ function componentOf(context, element, place) {
544
+ switch (element.name) {
545
+ case "Figure": return figureOf(context, element);
546
+ case "Figures": return multiFigureOf(context, element);
547
+ case "Table": return numberedTableOf(context, element);
548
+ case "Abstract": return abstractOf(context, element, place);
549
+ case "Bibliography":
550
+ attributesOf(context, element, {});
551
+ if (element.children.some((child) => !isCommentOnlyExpression(child))) failAt(context, element, "<Bibliography /> 不收子内容");
552
+ return located(context, context.ids.request({
553
+ type: "bibliography",
554
+ id: ""
555
+ }, "bib"), element);
556
+ case "Reference": return failAt(context, element, "<Reference> 只能写在报告目录的 references.mdx 里");
557
+ case "Caption": return failAt(context, element, "<Caption> 只能写在 <Table> 或 <Figures> 里");
558
+ case "Panel": return failAt(context, element, "<Panel> 只能写在 <Figures> 里");
559
+ case "table":
560
+ case "tr":
561
+ case "th":
562
+ case "td": return failAt(context, element, `<${element.name}> 只能写在 <Table> 里:不编号的简单表格直接写 GFM 表格`);
563
+ default: return failAt(context, element, `认不出组件 <${element.name ?? ""}>:块级只能用 Figure、Figures、Table、Abstract、Bibliography`);
564
+ }
565
+ }
566
+ function captionOf(context, element, what) {
567
+ const caption = inlinesOf(context, phrasingChildrenOf(context, element));
568
+ if (!caption.length) failAt(context, element, `${what}不能空`);
569
+ return caption;
570
+ }
571
+ function figureOf(context, element) {
572
+ const attributes = attributesOf(context, element, {
573
+ id: "optional",
574
+ src: "required",
575
+ alt: "optional"
576
+ });
577
+ const caption = captionOf(context, element, "<Figure> 的图题");
578
+ return explicitOrRequested(context, {
579
+ type: "figure",
580
+ id: "",
581
+ src: attributes["src"],
582
+ caption,
583
+ alt: attributes["alt"] ?? plainTextOf(caption)
584
+ }, attributes["id"], element, "fig");
585
+ }
586
+ function multiFigureOf(context, element) {
587
+ const attributes = attributesOf(context, element, {
588
+ id: "optional",
589
+ rows: "required",
590
+ columns: "required"
591
+ });
592
+ const children = elementChildrenOf(context, element);
593
+ const captions = children.filter((child) => child.name === "Caption");
594
+ if (captions.length !== 1) failAt(context, element, "<Figures> 里要有且只有一个 <Caption> 写整个图的图题");
595
+ for (const child of children) if (child.name !== "Caption" && child.name !== "Panel") failAt(context, child, `<Figures> 里只能放 <Caption> 与 <Panel>,不能放 <${child.name}>`);
596
+ attributesOf(context, captions[0], {});
597
+ const caption = captionOf(context, captions[0], "<Figures> 的图题");
598
+ const panels = children.filter((child) => child.name === "Panel").map((panel) => panelOf(context, panel));
599
+ const figure = {
600
+ type: "figure",
601
+ id: "",
602
+ caption,
603
+ rows: positiveIntegerOf(context, element, "rows", attributes["rows"]),
604
+ columns: positiveIntegerOf(context, element, "columns", attributes["columns"]),
605
+ panels
606
+ };
607
+ return explicitOrRequested(context, figure, attributes["id"], element, "fig");
608
+ }
609
+ function panelOf(context, element) {
610
+ const attributes = attributesOf(context, element, {
611
+ id: "optional",
612
+ row: "required",
613
+ column: "required",
614
+ label: "required",
615
+ src: "required",
616
+ alt: "optional"
617
+ });
618
+ const caption = inlinesOf(context, phrasingChildrenOf(context, element));
619
+ const alt = attributes["alt"] ?? plainTextOf(caption);
620
+ if (!alt) failAt(context, element, "<Panel> 没有小标题时要写 alt");
621
+ const panel = {
622
+ id: "",
623
+ row: positiveIntegerOf(context, element, "row", attributes["row"]),
624
+ column: positiveIntegerOf(context, element, "column", attributes["column"]),
625
+ label: attributes["label"],
626
+ src: attributes["src"],
627
+ alt
628
+ };
629
+ if (caption.length) panel.caption = caption;
630
+ return explicitOrRequested(context, panel, attributes["id"], element, "panel");
631
+ }
632
+ function numberedTableOf(context, element) {
633
+ const attributes = attributesOf(context, element, { id: "optional" });
634
+ const parts = element.children.filter((child) => !isCommentOnlyExpression(child) && !isWhitespaceText(child)).flatMap((child) => child.type === "paragraph" ? child.children.filter((inner) => !isWhitespaceText(inner)) : [child]);
635
+ let caption;
636
+ if (parts[0]?.name === "Caption") {
637
+ attributesOf(context, parts[0], {});
638
+ caption = captionOf(context, parts.shift(), "<Caption> 的表题");
639
+ }
640
+ const body = parts[0];
641
+ if (parts.length !== 1 || !body || !(body.type === "table" || body.name === "table")) failAt(context, element, "<Table> 里先写可省的 <Caption>,再写一张表(GFM 表格或 <table>),别的不放");
642
+ return body.type === "table" ? tableFromGfm(context, body, true, caption, attributes["id"] ?? null, element) : tableFromJsx(context, body, caption, attributes["id"] ?? null, element);
643
+ }
644
+ function finishedTable(context, table, caption, rawId, source) {
645
+ if (caption) table.caption = caption;
646
+ return explicitOrRequested(context, table, rawId ?? void 0, source, "tbl");
647
+ }
648
+ function cellOf(context, blocks, source, spans = {}) {
649
+ return located(context, context.ids.request({
650
+ id: "",
651
+ ...spans,
652
+ blocks
653
+ }, "cell"), source);
654
+ }
655
+ const columnsOf = (context, count) => Array.from({ length: count }, () => context.ids.request({ id: "" }, "col"));
656
+ function tableFromGfm(context, node, numbered, caption, rawId, source = node) {
657
+ const rows = node.children;
658
+ const width = rows[0].children.length;
659
+ return finishedTable(context, {
660
+ type: "table",
661
+ id: "",
662
+ numbered,
663
+ headerRows: 1,
664
+ columns: columnsOf(context, width),
665
+ rows: rows.map((row) => {
666
+ if (row.children.length !== width) failAt(context, row, `这一行有 ${row.children.length} 格,表头有 ${width} 格:每行格数要和表头一样`);
667
+ return located(context, context.ids.request({
668
+ id: "",
669
+ cells: row.children.map((cell) => cellOf(context, [paragraphOf(context, cell.children, cell)], cell))
670
+ }, "row"), row);
671
+ })
672
+ }, caption, rawId, source);
673
+ }
674
+ function spanOf(context, cell, attributes, name) {
675
+ return attributes[name] === void 0 ? void 0 : positiveIntegerOf(context, cell, name, attributes[name]);
676
+ }
677
+ function jsxCellOf(context, cell) {
678
+ const attributes = attributesOf(context, cell, {
679
+ rowSpan: "optional",
680
+ colSpan: "optional"
681
+ });
682
+ const spans = {};
683
+ const rowSpan = spanOf(context, cell, attributes, "rowSpan"), colSpan = spanOf(context, cell, attributes, "colSpan");
684
+ if (rowSpan !== void 0) spans.rowSpan = rowSpan;
685
+ if (colSpan !== void 0) spans.colSpan = colSpan;
686
+ const children = cell.children.filter((child) => !isCommentOnlyExpression(child) && !isWhitespaceText(child));
687
+ return cellOf(context, children.length === 0 || cell.type === "mdxJsxTextElement" ? [paragraphOf(context, children, cell)] : blocksOf(context, children, "cell"), cell, spans);
688
+ }
689
+ function tableFromJsx(context, element, caption, rawId, source) {
690
+ attributesOf(context, element, {});
691
+ const rowElements = elementChildrenOf(context, element);
692
+ if (!rowElements.length) failAt(context, element, "<table> 里至少要有一行 <tr>");
693
+ let headerRows = 0, headerEnded = false;
694
+ const rows = rowElements.map((row) => {
695
+ if (row.name !== "tr") failAt(context, row, `<table> 里只能放 <tr>,不能放 <${row.name}>`);
696
+ attributesOf(context, row, {});
697
+ const cellElements = elementChildrenOf(context, row);
698
+ if (!cellElements.length) failAt(context, row, "<tr> 里至少要有一格");
699
+ for (const cell of cellElements) if (cell.name !== "th" && cell.name !== "td") failAt(context, cell, `<tr> 里只能放 <th> 与 <td>,不能放 <${cell.name}>`);
700
+ if (cellElements.every((cell) => cell.name === "th") && !headerEnded) headerRows++;
701
+ else {
702
+ headerEnded = true;
703
+ const header = cellElements.find((cell) => cell.name === "th");
704
+ if (header) failAt(context, header, "<th> 只能用在表格开头连续的表头行里");
705
+ }
706
+ return located(context, context.ids.request({
707
+ id: "",
708
+ cells: cellElements.map((cell) => jsxCellOf(context, cell))
709
+ }, "row"), row);
710
+ });
711
+ const width = rows[0].cells.reduce((sum, cell) => sum + (cell.colSpan ?? 1), 0);
712
+ return finishedTable(context, {
713
+ type: "table",
714
+ id: "",
715
+ numbered: true,
716
+ headerRows,
717
+ columns: columnsOf(context, width),
718
+ rows
719
+ }, caption, rawId, source);
720
+ }
721
+ function abstractOf(context, element, place) {
722
+ if (context.role !== "index" || place !== "top") failAt(context, element, "<Abstract> 只能写在 index.mdx 的正文最前面");
723
+ const attributes = attributesOf(context, element, {
724
+ keywords: "required",
725
+ title: "optional"
726
+ });
727
+ const keywords = attributes["keywords"].split(";").map((keyword) => keyword.trim());
728
+ if (keywords.some((keyword) => !keyword)) failAt(context, element, "<Abstract> 的 keywords 用全角分号「;」隔开,每个关键词都不能空");
729
+ const abstract = {
730
+ type: "abstract",
731
+ id: "",
732
+ paragraphs: blocksOf(context, element.children.filter((child) => !isWhitespaceText(child)), "abstract"),
733
+ keywords
734
+ };
735
+ if (attributes["title"] !== void 0) abstract["title"] = attributes["title"];
736
+ return located(context, context.ids.request(abstract, "abs"), element);
737
+ }
738
+ //#endregion
739
+ //#region src/core/mdx/frontmatter.ts
740
+ function frontmatterOf(root, file) {
741
+ const node = root.children.find((child) => child.type === "yaml");
742
+ if (!node) return void 0;
743
+ const at = positionOf(node);
744
+ let value;
745
+ try {
746
+ value = parse(node.value, { uniqueKeys: true });
747
+ } catch (error) {
748
+ throw new ReportContentError(`文件头不是合法的 YAML:${error.message}`, file, at);
749
+ }
750
+ if (value === null || value === void 0) return {
751
+ fields: {},
752
+ at
753
+ };
754
+ if (typeof value !== "object" || Array.isArray(value)) throw new ReportContentError("文件头要写成「键: 值」的映射", file, at);
755
+ return {
756
+ fields: value,
757
+ at
758
+ };
759
+ }
760
+ function rejectUnknownKeys(fields, allowed, file, at) {
761
+ for (const key of Object.keys(fields)) if (!allowed.includes(key)) throw new ReportContentError(`文件头认不出「${key}」:这里只能写 ${allowed.join("、")}`, file, at);
762
+ }
763
+ function stringField(fields, key, file, at) {
764
+ const value = fields[key];
765
+ if (value === void 0) return void 0;
766
+ if (typeof value !== "string") throw new ReportContentError(`文件头的 ${key} 要是字符串`, file, at);
767
+ return value;
768
+ }
769
+ function stringListField(fields, key, file, at) {
770
+ const value = fields[key];
771
+ if (value === void 0) return void 0;
772
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new ReportContentError(`文件头的 ${key} 要是字符串列表`, file, at);
773
+ return value;
774
+ }
775
+ const INDEX_KEYS = [
776
+ "title",
777
+ "shortTitle",
778
+ "language",
779
+ "authors",
780
+ "date",
781
+ "revision",
782
+ "fields",
783
+ "chapters"
784
+ ];
785
+ /** 入口 index.mdx 的文件头:报告元数据与章节列表 */
786
+ function indexFrontmatterOf(root, file) {
787
+ const frontmatter = frontmatterOf(root, file);
788
+ if (!frontmatter) throw new ReportContentError("index.mdx 要有文件头(--- 包起来的 YAML),至少写 title 与 language", file, "1:1");
789
+ const { fields, at } = frontmatter;
790
+ rejectUnknownKeys(fields, INDEX_KEYS, file, at);
791
+ const title = stringField(fields, "title", file, at);
792
+ const language = stringField(fields, "language", file, at);
793
+ if (title === void 0) throw new ReportContentError("文件头缺少 title", file, at);
794
+ if (language === void 0) throw new ReportContentError("文件头缺少 language(中文报告写 zh-CN)", file, at);
795
+ const metadata = {
796
+ title,
797
+ language
798
+ };
799
+ const shortTitle = stringField(fields, "shortTitle", file, at);
800
+ const authors = stringListField(fields, "authors", file, at);
801
+ const date = stringField(fields, "date", file, at);
802
+ const revision = stringField(fields, "revision", file, at);
803
+ if (shortTitle !== void 0) metadata.shortTitle = shortTitle;
804
+ if (authors !== void 0) metadata.authors = authors;
805
+ if (date !== void 0) metadata.date = date;
806
+ if (revision !== void 0) metadata.revision = revision;
807
+ if (fields["fields"] !== void 0) metadata.fields = metadataFieldsOf(fields["fields"], file, at);
808
+ return {
809
+ metadata,
810
+ chapters: checkedChapterNames(stringListField(fields, "chapters", file, at) ?? [], file, at)
811
+ };
812
+ }
813
+ function metadataFieldsOf(value, file, at) {
814
+ const shapeError = new ReportContentError("文件头的 fields 要是列表,每项写 id、label、value 三个字符串", file, at);
815
+ if (!Array.isArray(value)) throw shapeError;
816
+ return value.map((item) => {
817
+ if (!item || typeof item !== "object" || Array.isArray(item)) throw shapeError;
818
+ const entry = item;
819
+ if (Object.keys(entry).some((key) => ![
820
+ "id",
821
+ "label",
822
+ "value"
823
+ ].includes(key))) throw shapeError;
824
+ const [id, label, fieldValue] = [
825
+ entry["id"],
826
+ entry["label"],
827
+ entry["value"]
828
+ ];
829
+ if (typeof id !== "string" || typeof label !== "string" || typeof fieldValue !== "string") throw shapeError;
830
+ return {
831
+ id,
832
+ label,
833
+ value: fieldValue
834
+ };
835
+ });
836
+ }
837
+ const RESERVED_FILE_NAMES = ["index.mdx", "references.mdx"];
838
+ /** 章文件都放在报告目录根上:只收文件名,不收子目录与上级目录 */
839
+ function checkedChapterNames(chapters, file, at) {
840
+ const seen = /* @__PURE__ */ new Set();
841
+ for (const chapter of chapters) {
842
+ if (!/^[^\\/:*?"<>|\x00-\x1f]+\.mdx$/.test(chapter) || chapter.startsWith(".")) throw new ReportContentError(`chapters 里的「${chapter}」要是报告目录根上的 .mdx 文件名,不带目录`, file, at);
843
+ if (RESERVED_FILE_NAMES.includes(chapter)) throw new ReportContentError(`chapters 里不能写 ${chapter}`, file, at);
844
+ if (seen.has(chapter)) throw new ReportContentError(`chapters 里「${chapter}」写了两次`, file, at);
845
+ seen.add(chapter);
846
+ }
847
+ return chapters;
848
+ }
849
+ /** 章文件头只认 numbering,管这个文件里的一级标题;不写就是 decimal */
850
+ function chapterNumberingOf(root, file) {
851
+ const frontmatter = frontmatterOf(root, file);
852
+ if (!frontmatter) return "decimal";
853
+ const { fields, at } = frontmatter;
854
+ rejectUnknownKeys(fields, ["numbering"], file, at);
855
+ const numbering = stringField(fields, "numbering", file, at) ?? "decimal";
856
+ if (numbering !== "decimal" && numbering !== "appendix" && numbering !== "none") throw new ReportContentError(`numbering 只能是 decimal、appendix 或 none,这里写的是「${numbering}」`, file, at);
857
+ return numbering;
858
+ }
859
+ function rejectFrontmatter(root, file, why) {
860
+ const node = root.children.find((child) => child.type === "yaml");
861
+ if (node) throw new ReportContentError(`${why}不写文件头`, file, positionOf(node));
862
+ }
863
+ //#endregion
864
+ //#region src/core/mdx/compile.ts
865
+ /** 报告目录里固定名字的两个文件 */
866
+ const MDX_ENTRY_NAME = "index.mdx";
867
+ const MDX_REFERENCES_NAME = "references.mdx";
868
+ const directoryOf = (file) => file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
869
+ const inDirectory = (directory, name) => directory ? `${directory}/${name}` : name;
870
+ /** 读入口原文就知道还要读哪些文件:章按 chapters 的顺序,文献列表 references.mdx 有没有由调用方看磁盘 */
871
+ function mdxReportInputsOf(indexText, entry) {
872
+ const { chapters } = indexFrontmatterOf(parseMdx(indexText, entry), entry);
873
+ return {
874
+ chapters: chapters.map((chapter) => inDirectory(directoryOf(entry), chapter)),
875
+ references: inDirectory(directoryOf(entry), MDX_REFERENCES_NAME)
876
+ };
877
+ }
878
+ function referencesOf(context, root) {
879
+ rejectFrontmatter(root, context.file, "references.mdx ");
880
+ const entries = [];
881
+ for (const node of root.children) {
882
+ if (isCommentOnlyExpression(node)) continue;
883
+ const element = node.type === "paragraph" && node.children.length === 1 ? node.children[0] : node;
884
+ if (element.type !== "mdxJsxFlowElement" && element.type !== "mdxJsxTextElement" || element.name !== "Reference") failAt(context, node, "references.mdx 里只能写 <Reference id=\"…\" url=\"…\">著录文字</Reference>,一条一段");
885
+ const attributes = attributesOf(context, element, {
886
+ id: "required",
887
+ url: "optional"
888
+ });
889
+ const text = inlinesOf(context, phrasingChildrenOf(context, element));
890
+ if (text.length !== 1 || text[0].type !== "text" || text[0].marks) failAt(context, element, "<Reference> 的著录文字只能是一行纯文字");
891
+ const entry = {
892
+ id: explicitIdOf(attributes["id"], context.file, positionOf(element)),
893
+ text: text[0].text
894
+ };
895
+ if (attributes["url"] !== void 0) entry.url = attributes["url"];
896
+ entries.push(located(context, entry, element));
897
+ }
898
+ return entries;
899
+ }
900
+ function locationsByPointer(document, locations) {
901
+ const sources = Object.create(null);
902
+ const byPointer = /* @__PURE__ */ new Map();
903
+ for (const { node, pointer } of walk(document)) {
904
+ const location = locations.get(node);
905
+ if (!location) continue;
906
+ byPointer.set(pointer, location);
907
+ if (typeof node.id === "string") sources[node.id] = location;
908
+ }
909
+ const locate = (pointer) => {
910
+ for (let current = pointer;; current = current.slice(0, current.lastIndexOf("/"))) {
911
+ const location = byPointer.get(current);
912
+ if (location) return location;
913
+ if (!current) return void 0;
914
+ }
915
+ };
916
+ return {
917
+ sources,
918
+ locate
919
+ };
920
+ }
921
+ /** 整份 MDX 报告 → 一份 docreport 文档(入口正文在前,各章按 chapters 的顺序接在后面);只解析、不执行 MDX */
922
+ function compileMdxReport({ reportId, entry, files }) {
923
+ if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(reportId)) throw new ReportContentError(`报告目录名「${reportId}」不能当文档 id:MDX 报告的目录名要以字母开头`, entry);
924
+ const textOf = (file) => {
925
+ if (!Object.hasOwn(files, file)) throw new ReportContentError("找不到这个文件", file);
926
+ return files[file];
927
+ };
928
+ const ids = new GeneratedIds();
929
+ const locations = /* @__PURE__ */ new WeakMap();
930
+ const notes = [];
931
+ const contextFor = (file, role) => ({
932
+ file,
933
+ role,
934
+ ids,
935
+ locations,
936
+ notes
937
+ });
938
+ const indexRoot = parseMdx(textOf(entry), entry);
939
+ const { metadata, chapters } = indexFrontmatterOf(indexRoot, entry);
940
+ const blocks = sectionsOf(contextFor(entry, "index"), indexRoot, "decimal");
941
+ const chapterFiles = chapters.map((chapter) => inDirectory(directoryOf(entry), chapter));
942
+ for (const file of chapterFiles) {
943
+ const root = parseMdx(textOf(file), file);
944
+ blocks.push(...sectionsOf(contextFor(file, "chapter"), root, chapterNumberingOf(root, file)));
945
+ }
946
+ const referencesFile = inDirectory(directoryOf(entry), MDX_REFERENCES_NAME);
947
+ const hasReferences = Object.hasOwn(files, referencesFile);
948
+ const references = hasReferences ? referencesOf(contextFor(referencesFile, "references"), parseMdx(files[referencesFile], referencesFile)) : [];
949
+ ids.assignAll();
950
+ const document = {
951
+ format: "docreport",
952
+ schemaVersion: 1,
953
+ id: reportId,
954
+ metadata,
955
+ blocks
956
+ };
957
+ if (notes.length) document.notes = notes;
958
+ if (references.length) document.references = references;
959
+ return {
960
+ document,
961
+ ...locationsByPointer(document, locations),
962
+ files: [
963
+ entry,
964
+ ...chapterFiles,
965
+ ...hasReferences ? [referencesFile] : []
966
+ ]
967
+ };
968
+ }
969
+ function rawMessageOf(error) {
970
+ const prefix = `${error.file}${error.pointer ? ":" + error.pointer : ""}${error.file || error.pointer ? ": " : ""}`;
971
+ return error.message.startsWith(prefix) ? error.message.slice(prefix.length) : error.message;
972
+ }
973
+ /** 组装报的错指着编译结果里的 JSON 指针:换成 MDX 文件与行列再抛 */
974
+ function relocated(error, compiled, entry) {
975
+ if (!(error instanceof ReportContentError) || error.file !== entry) return error;
976
+ const location = compiled.locate(error.pointer);
977
+ return location ? new ReportContentError(rawMessageOf(error), location.file, location.pointer) : error;
978
+ }
979
+ /** 编译并组装一份 MDX 报告:结果和 JSON 报告的组装结果同形,sources 指向 MDX 的行列,dependencies 是编进来的 MDX 文件 */
980
+ function assembleMdxReport(input, options = {}) {
981
+ const compiled = compileMdxReport(input);
982
+ let result;
983
+ try {
984
+ result = assembleDocument(input.entry, { [input.entry]: compiled.document }, options);
985
+ } catch (error) {
986
+ throw relocated(error, compiled, input.entry);
987
+ }
988
+ return {
989
+ ...result,
990
+ sources: {
991
+ ...result.sources,
992
+ ...compiled.sources
993
+ },
994
+ dependencies: compiled.files
995
+ };
996
+ }
997
+ //#endregion
998
+ //#region src/core/mdx/emit.ts
999
+ const UNICODE_PUNCTUATION = /[\p{P}\p{S}]/u;
1000
+ const MARK_WRAP_ORDER = [
1001
+ "code",
1002
+ "sub",
1003
+ "sup",
1004
+ "em",
1005
+ "strong"
1006
+ ];
1007
+ /** Markdown 与 MDX 的特殊字符一律加反斜杠;GFM 会把网址与邮箱自动变成链接,冒号、点与 @ 也拆开 */
1008
+ function escapedText(text) {
1009
+ return text.replace(/[\\`*_{}[\]<>$|~&#@]/g, (character) => `\\${character}`).replace(/:\/\//g, "\\://").replace(/www\./gi, (match) => `${match.slice(0, 3)}\\.`);
1010
+ }
1011
+ /** 一段的开头像列表、标题、引用的,再加一层转义 */
1012
+ function escapedLineStart(line) {
1013
+ return line.replace(/^(\d+)([.)])/, "$1\\$2").replace(/^([-+])(\s|$)/, "\\$1$2");
1014
+ }
1015
+ function codeSpan(text) {
1016
+ const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
1017
+ const fence = "`".repeat(longest + 1);
1018
+ const padding = text.startsWith("`") || text.endsWith("`") ? " " : "";
1019
+ return `${fence}${padding}${text}${padding}${fence}`;
1020
+ }
1021
+ /** 加粗、斜体在内容首尾不是空白与标点时用 ** 与 *(中文旁边才认得出),否则用 <strong> 与 <em> */
1022
+ function wrappedWithMark(inner, plain, mark) {
1023
+ if (mark === "sub" || mark === "sup") return `<${mark}>${inner}</${mark}>`;
1024
+ const first = plain[0], last = plain.at(-1);
1025
+ if (first !== void 0 && last !== void 0 && !/\s/.test(first) && !/\s/.test(last) && !UNICODE_PUNCTUATION.test(first) && !UNICODE_PUNCTUATION.test(last)) return mark === "strong" ? `**${inner}**` : `*${inner}*`;
1026
+ return `<${mark}>${inner}</${mark}>`;
1027
+ }
1028
+ function attributeOf(name, value, problems, at) {
1029
+ if (!value.includes("\"")) return ` ${name}="${value}"`;
1030
+ if (!value.includes("'")) return ` ${name}='${value}'`;
1031
+ problems.push(`${at}:${name} 里同时有单引号和双引号,写不成组件属性`);
1032
+ return "";
1033
+ }
1034
+ var MdxWriter = class {
1035
+ problems = [];
1036
+ images = Object.create(null);
1037
+ keptIds;
1038
+ assets;
1039
+ constructor(keptIds, assets) {
1040
+ this.keptIds = keptIds;
1041
+ this.assets = assets;
1042
+ }
1043
+ idAttribute(id, at) {
1044
+ return id !== void 0 && this.keptIds.has(id) ? attributeOf("id", id, this.problems, at) : "";
1045
+ }
1046
+ /** 图片按文件名放进新报告的 assets/:同名不同图报错 */
1047
+ imagePath(src, at) {
1048
+ const original = this.assets[src] ?? src;
1049
+ const name = original.slice(original.lastIndexOf("/") + 1);
1050
+ const previous = this.images[name];
1051
+ if (previous !== void 0 && previous !== src) this.problems.push(`${at}:图片 ${name} 和 ${previous} 同名,放进同一个 assets/ 会互相覆盖`);
1052
+ this.images[name] = src;
1053
+ return `assets/${name}`;
1054
+ }
1055
+ inlines(inlines, at) {
1056
+ let previousWasMath = false;
1057
+ return inlines.map((inline, index) => {
1058
+ const here = `${at}/${index}`;
1059
+ if (inline.type === "math" && previousWasMath) this.problems.push(`${here}:两个行内公式紧挨着,写成 $…$$…$ 会被读成一个`);
1060
+ previousWasMath = inline.type === "math";
1061
+ switch (inline.type) {
1062
+ case "text": {
1063
+ let written = (inline.marks ?? []).includes("code") ? codeSpan(inline.text) : escapedText(inline.text);
1064
+ for (const mark of MARK_WRAP_ORDER) if (mark !== "code" && inline.marks?.includes(mark)) written = wrappedWithMark(written, inline.text, mark);
1065
+ return written;
1066
+ }
1067
+ case "math":
1068
+ if (inline.latex.includes("$") || /[\r\n]/.test(inline.latex)) this.problems.push(`${here}:行内公式里有 $ 或换行,写不成 $…$`);
1069
+ return `$${inline.latex}$`;
1070
+ case "ref": return `<Ref to="${inline.target}"${inline.display === "label" ? "" : ` show="${inline.display}"`} />`;
1071
+ case "cite": return `<Cite to="${inline.targets.join(" ")}" />`;
1072
+ case "link": return `[${escapedText(inline.text)}](${/[\s()<>]/.test(inline.href) ? `<${inline.href}>` : inline.href})`;
1073
+ case "note_ref": return `[^${inline.target}]`;
1074
+ case "break": return "<br />";
1075
+ }
1076
+ }).join("");
1077
+ }
1078
+ /** 段落各占一行写出;表格单元格里的段落不在行首,不用防它被读成列表,空段落就是空格子 */
1079
+ paragraph(content, at, insideCell = false) {
1080
+ if (!content.length && !insideCell) this.problems.push(`${at}:没有内容的段落在 MDX 里写不出来`);
1081
+ const written = this.inlines(content, `${at}/content`);
1082
+ return insideCell ? written : escapedLineStart(written);
1083
+ }
1084
+ /** 一个单元格:只有一段(含空段,即空格子)写成行内内容,否则 null(要写成块);没有块的格子编译回来是一个空段 */
1085
+ cellInline(blocks, at) {
1086
+ if (blocks.length === 0) return "";
1087
+ const only = blocks.length === 1 ? blocks[0] : void 0;
1088
+ return only?.type === "paragraph" ? this.paragraph(only.content, `${at}/blocks/0`, true) : null;
1089
+ }
1090
+ /** 块之间空一行;相邻两个同类列表中间放一个注释,否则会并成一个列表 */
1091
+ blocks(blocks, at, depth, parentNumbering) {
1092
+ const lines = [];
1093
+ blocks.forEach((block, index) => {
1094
+ const previous = blocks[index - 1];
1095
+ if (block.type === "list" && previous?.type === "list" && previous.ordered === block.ordered) lines.push("{/* 两个列表的分界 */}");
1096
+ lines.push(this.block(block, `${at}/${index}`, depth, parentNumbering));
1097
+ });
1098
+ return lines;
1099
+ }
1100
+ block(block, at, depth, parentNumbering) {
1101
+ switch (block.type) {
1102
+ case "section": return this.section(block, at, depth, parentNumbering);
1103
+ case "paragraph": return this.paragraph(block.content, at);
1104
+ case "list": return this.list(block, at, "");
1105
+ case "figure": return this.figure(block, at);
1106
+ case "table": return this.table(block, at);
1107
+ case "equation": {
1108
+ if (!block.numbered) {
1109
+ if (block.latex.includes("$$")) this.problems.push(`${at}:公式里有 $$,写不成 $$ 块`);
1110
+ if (this.keptIds.has(block.id)) this.problems.push(`${at}:不编号的公式被引用或写在 layout.json 里,MDX 里不编号的公式没有 id`);
1111
+ return `$$\n${block.latex}\n$$`;
1112
+ }
1113
+ const fence = "`".repeat(Math.max(3, ...(block.latex.match(/`{3,}/g) ?? []).map((run) => run.length + 1)));
1114
+ return `${fence}math${this.keptIds.has(block.id) ? ` ${block.id}` : ""}\n${block.latex}\n${fence}`;
1115
+ }
1116
+ case "code": {
1117
+ if (block.language === "math") this.problems.push(`${at}:语言是 math 的代码块在 MDX 里会被当成编号公式`);
1118
+ const fence = "`".repeat(Math.max(3, ...(block.text.match(/`{3,}/g) ?? []).map((run) => run.length + 1)));
1119
+ return `${fence}${block.language}\n${block.text}\n${fence}`;
1120
+ }
1121
+ case "bibliography":
1122
+ this.problems.push(`${at}:<Bibliography /> 只能放在文末(不写时组装会补上)`);
1123
+ return "";
1124
+ case "abstract": return this.abstract(block, at);
1125
+ }
1126
+ }
1127
+ abstract(block, at) {
1128
+ if (block.keywords.some((keyword) => keyword.includes(";"))) this.problems.push(`${at}:关键词里有全角分号,<Abstract keywords> 用它分隔`);
1129
+ const title = block.title !== void 0 ? attributeOf("title", block.title, this.problems, at) : "";
1130
+ const paragraphs = block.paragraphs.map((paragraph, index) => this.paragraph(paragraph.content, `${at}/paragraphs/${index}`));
1131
+ return `<Abstract keywords="${block.keywords.join(";")}"${title}>\n\n${paragraphs.join("\n\n")}\n\n</Abstract>`;
1132
+ }
1133
+ section(section, at, depth, parentNumbering) {
1134
+ if (depth > 6) this.problems.push(`${at}:标题超过六级`);
1135
+ const expected = parentNumbering === "none" ? "none" : "decimal";
1136
+ if (depth > 1 && section.numbering !== expected) this.problems.push(`${at}:${parentNumbering} 节下面的 ${section.numbering} 子节在 MDX 里写不出来(子节跟着上级编号)`);
1137
+ if (/\[#[^\]]*\]\s*$/.test(section.title)) this.problems.push(`${at}:标题末尾的 [#…] 会被当成节的 id`);
1138
+ return [`${"#".repeat(depth)} ${escapedText(section.title)}${this.keptIds.has(section.id) ? ` [#${section.id}]` : ""}`, ...this.blocks(section.children, `${at}/children`, depth + 1, section.numbering)].join("\n\n");
1139
+ }
1140
+ list(list, at, indent) {
1141
+ return list.items.map((item, index) => {
1142
+ const marker = list.ordered ? `${(list.start ?? 1) + index}. ` : "- ";
1143
+ const continuation = indent + " ".repeat(marker.length);
1144
+ return item.blocks.map((block, blockIndex) => {
1145
+ const here = `${at}/items/${index}/blocks/${blockIndex}`;
1146
+ return block.type === "list" ? this.list(block, here, continuation) : this.paragraph(block.content, here);
1147
+ }).map((part, partIndex) => partIndex === 0 ? `${indent}${marker}${part.trimStart()}` : part.startsWith(continuation) ? part : `${continuation}${part}`).join("\n\n");
1148
+ }).join("\n");
1149
+ }
1150
+ figure(figure, at) {
1151
+ const caption = this.inlines(figure.caption, `${at}/caption`);
1152
+ if (!figure.panels) {
1153
+ const alt = figure.alt === plainTextOf(figure.caption) ? "" : attributeOf("alt", figure.alt, this.problems, at);
1154
+ return `<Figure${this.idAttribute(figure.id, at)} src="${this.imagePath(figure.src, at)}"${alt}>${caption}</Figure>`;
1155
+ }
1156
+ const panels = figure.panels.map((panel, index) => {
1157
+ const here = `${at}/panels/${index}`;
1158
+ const panelCaption = panel.caption ? this.inlines(panel.caption, `${here}/caption`) : "";
1159
+ const alt = panel.caption && panel.alt === plainTextOf(panel.caption) ? "" : attributeOf("alt", panel.alt, this.problems, here);
1160
+ const open = ` <Panel${this.idAttribute(panel.id, here)} row="${panel.row}" column="${panel.column}"${attributeOf("label", panel.label, this.problems, here)} src="${this.imagePath(panel.src, here)}"${alt}`;
1161
+ return panelCaption ? `${open}>${panelCaption}</Panel>` : `${open} />`;
1162
+ });
1163
+ return [
1164
+ `<Figures${this.idAttribute(figure.id, at)} rows="${figure.rows}" columns="${figure.columns}">`,
1165
+ ` <Caption>${caption}</Caption>`,
1166
+ ...panels,
1167
+ "</Figures>"
1168
+ ].join("\n");
1169
+ }
1170
+ table(table, at) {
1171
+ const caption = table.caption ? this.inlines(table.caption, `${at}/caption`) : void 0;
1172
+ if (!table.numbered) {
1173
+ if (caption !== void 0) this.problems.push(`${at}:带表题的不编号表格在 MDX 里写不出来`);
1174
+ if (!this.fitsGfm(table)) this.problems.push(`${at}:不编号的表格只能写成 GFM 表格,这张有合并单元格、多行表头或多段内容`);
1175
+ return this.gfmTable(table, at);
1176
+ }
1177
+ const body = this.fitsGfm(table) ? this.gfmTable(table, at) : this.jsxTable(table, at);
1178
+ return [
1179
+ `<Table${this.idAttribute(table.id, at)}>`,
1180
+ ...caption !== void 0 ? [` <Caption>${caption}</Caption>`] : [],
1181
+ "",
1182
+ body,
1183
+ "",
1184
+ "</Table>"
1185
+ ].join("\n");
1186
+ }
1187
+ fitsGfm(table) {
1188
+ const fitsOneGfmCell = (blocks) => blocks.length === 0 || blocks.length === 1 && blocks[0].type === "paragraph" && blocks[0].content.every((inline) => inline.type !== "break" && (inline.type !== "math" || !inline.latex.includes("|")));
1189
+ return table.headerRows === 1 && table.rows.every((row) => row.cells.length === table.columns.length && row.cells.every((cell) => !cell.rowSpan && !cell.colSpan && fitsOneGfmCell(cell.blocks)));
1190
+ }
1191
+ gfmTable(table, at) {
1192
+ const lines = table.rows.map((row, rowIndex) => `| ${row.cells.map((cell, cellIndex) => this.cellInline(cell.blocks, `${at}/rows/${rowIndex}/cells/${cellIndex}`) ?? "").join(" | ")} |`);
1193
+ lines.splice(1, 0, `| ${table.columns.map(() => "---").join(" | ")} |`);
1194
+ return lines.join("\n");
1195
+ }
1196
+ jsxTable(table, at) {
1197
+ return [
1198
+ "<table>",
1199
+ ...table.rows.map((row, rowIndex) => {
1200
+ const tag = rowIndex < table.headerRows ? "th" : "td";
1201
+ return ` <tr>${row.cells.map((cell, cellIndex) => {
1202
+ const here = `${at}/rows/${rowIndex}/cells/${cellIndex}`;
1203
+ const spans = `${cell.rowSpan ? ` rowSpan="${cell.rowSpan}"` : ""}${cell.colSpan ? ` colSpan="${cell.colSpan}"` : ""}`;
1204
+ const inline = this.cellInline(cell.blocks, here);
1205
+ if (inline !== null) return `<${tag}${spans}>${inline}</${tag}>`;
1206
+ return `<${tag}${spans}>\n\n${this.blocks(cell.blocks, `${here}/blocks`, 7, "decimal").join("\n\n")}\n\n</${tag}>`;
1207
+ }).join("")}</tr>`;
1208
+ }),
1209
+ "</table>"
1210
+ ].join("\n");
1211
+ }
1212
+ };
1213
+ const isGeneratedBibliography = (block, document) => block.type === "bibliography" && block.id === `${document.id.slice(0, 48)}_bibliography`;
1214
+ /** 哪些 id 要写出来:被 ref 指到的、layout.json 的键、文献与脚注(它们本来就靠 id 对应) */
1215
+ function keptIdsOf(document, layoutKeys) {
1216
+ const kept = new Set(layoutKeys);
1217
+ for (const { node } of walk(document)) if (node.type === "ref") kept.add(node.target);
1218
+ for (const reference of document.references ?? []) kept.add(reference.id);
1219
+ for (const note of document.notes ?? []) kept.add(note.id);
1220
+ return kept;
1221
+ }
1222
+ function chapterFileNames(sections) {
1223
+ const taken = /* @__PURE__ */ new Set([MDX_ENTRY_NAME, MDX_REFERENCES_NAME]);
1224
+ return sections.map((section, index) => {
1225
+ const preferred = /^[a-z0-9][a-z0-9-]{0,40}$/.test(section.id) ? `${section.id}.mdx` : `chapter-${index + 1}.mdx`;
1226
+ const name = taken.has(preferred) ? `chapter-${index + 1}.mdx` : preferred;
1227
+ taken.add(name);
1228
+ return name;
1229
+ });
1230
+ }
1231
+ /** GFM 只认同一个文件里的脚注定义:每条脚注的定义写进引用它的那个文件(入口正文或某一章)的末尾 */
1232
+ function notesByFile(document, frontBlocks, sections, names, writer) {
1233
+ const fileOfNote = /* @__PURE__ */ new Map();
1234
+ const collect = (value, file) => {
1235
+ for (const { node } of walk(value)) if (node.type === "note_ref") fileOfNote.set(node.target, file);
1236
+ };
1237
+ collect(frontBlocks, MDX_ENTRY_NAME);
1238
+ sections.forEach((section, index) => collect(section, names[index]));
1239
+ const byFile = /* @__PURE__ */ new Map();
1240
+ (document.notes ?? []).forEach((note, index) => {
1241
+ const at = `/notes/${index}`;
1242
+ const [first, ...rest] = note.blocks;
1243
+ if (first?.type !== "paragraph" || rest.length) writer.problems.push(`${at}:脚注只转一段文字的,这条有 ${note.blocks.length} 块`);
1244
+ const file = fileOfNote.get(note.id) ?? "index.mdx";
1245
+ byFile.set(file, [...byFile.get(file) ?? [], `[^${note.id}]: ${first?.type === "paragraph" ? writer.paragraph(first.content, `${at}/blocks/0`) : ""}`]);
1246
+ });
1247
+ return byFile;
1248
+ }
1249
+ /** 组装好的文档 → MDX 报告。assets 是组装结果的 assets(图在组装结果里的路径 → 原图路径) */
1250
+ function mdxReportPlanOf(document, assets, layoutKeys) {
1251
+ const keptIds = keptIdsOf(document, layoutKeys);
1252
+ const writer = new MdxWriter(keptIds, assets);
1253
+ const firstSection = document.blocks.findIndex((block) => block.type === "section");
1254
+ const frontBlocks = (firstSection < 0 ? document.blocks : document.blocks.slice(0, firstSection)).filter((block, index, all) => !(firstSection < 0 && index === all.length - 1 && isGeneratedBibliography(block, document)));
1255
+ const sections = [];
1256
+ document.blocks.forEach((block, index) => {
1257
+ if (index < firstSection || firstSection < 0) return;
1258
+ if (block.type === "section") sections.push(block);
1259
+ else if (!(index === document.blocks.length - 1 && isGeneratedBibliography(block, document))) writer.problems.push(`/blocks/${index}:第一章之后的顶层 ${block.type} 在 MDX 里写不出来(顶层内容只能写在 index.mdx 的正文、第一章之前)`);
1260
+ });
1261
+ const names = chapterFileNames(sections);
1262
+ const notesOf = notesByFile(document, frontBlocks, sections, names, writer);
1263
+ const files = Object.create(null);
1264
+ for (const [index, section] of sections.entries()) {
1265
+ const at = `/blocks/${firstSection + index}`;
1266
+ const frontmatter = section.numbering === "decimal" ? "" : `---\nnumbering: ${section.numbering}\n---\n\n`;
1267
+ files[names[index]] = `${frontmatter}${[writer.section(section, at, 1, "decimal"), ...notesOf.get(names[index]) ?? []].join("\n\n")}\n`;
1268
+ }
1269
+ const { metadata } = document;
1270
+ const header = { title: metadata.title };
1271
+ if (metadata.shortTitle !== void 0) header["shortTitle"] = metadata.shortTitle;
1272
+ header["language"] = metadata.language;
1273
+ if (metadata.authors !== void 0) header["authors"] = metadata.authors;
1274
+ if (metadata.date !== void 0) header["date"] = metadata.date;
1275
+ if (metadata.revision !== void 0) header["revision"] = metadata.revision;
1276
+ if (metadata.fields !== void 0) header["fields"] = metadata.fields;
1277
+ if (names.length) header["chapters"] = names;
1278
+ const body = [...writer.blocks(frontBlocks, "/blocks", 1, "decimal"), ...notesOf.get("index.mdx") ?? []].join("\n\n");
1279
+ files[MDX_ENTRY_NAME] = `---\n${stringify(header, { lineWidth: 0 })}---\n${body ? `\n${body}\n` : ""}`;
1280
+ if (document.references?.length) files[MDX_REFERENCES_NAME] = `${document.references.map((reference, index) => `<Reference${attributeOf("id", reference.id, writer.problems, `/references/${index}`)}${reference.url !== void 0 ? attributeOf("url", reference.url, writer.problems, `/references/${index}`) : ""}>${escapedText(reference.text)}</Reference>`).join("\n\n")}\n`;
1281
+ return {
1282
+ files,
1283
+ images: writer.images,
1284
+ keptIds,
1285
+ problems: writer.problems
1286
+ };
1287
+ }
1288
+ //#endregion
1289
+ export { assembleMdxReport as a, GENERATED_ID_PREFIX as c, MDX_REFERENCES_NAME as i, mdxReportPlanOf as n, compileMdxReport as o, MDX_ENTRY_NAME as r, mdxReportInputsOf as s, keptIdsOf as t };