@godcreator/word-report 0.3.1 → 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.
package/dist/node.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as AssemblyOptions, r as AssemblyResult } from "./types-Bcl48xQN.mjs";
2
- import { a as ReportCheckDiagnostic, i as reportExport, n as ReportExportResult, o as ReportCheckResult, r as reportEntry, s as checkReports, t as ReportExportOptions } from "./export-DC764Umz.mjs";
2
+ import { a as ConvertReportResult, c as ReportCheckResult, i as reportExport, l as checkReports, n as ReportExportResult, o as convertReportToMdx, r as reportEntry, s as ReportCheckDiagnostic, t as ReportExportOptions } from "./export-Da4yhrZa.mjs";
3
3
  //#region src/node/read-report.d.ts
4
4
  interface SnapshotEntry {
5
5
  path: string;
@@ -18,18 +18,24 @@ declare function readReport(entryAbsolute: string, options?: AssemblyOptions & {
18
18
  }): Promise<ReadReportResult>;
19
19
  //#endregion
20
20
  //#region src/node/report-files.d.ts
21
- /** 站点里的报告:站点根下 reports/<报告 id>/content.json,章节与图片在同一个报告目录里 */
21
+ /** 站点里的报告:站点根下 reports/<报告 id>/,入口是 content.json(JSON 报告)或 index.mdx(MDX 报告),两种只能有一种 */
22
22
  declare const reportsDirName = "reports";
23
+ declare const JSON_ENTRY_NAME = "content.json";
23
24
  /** 报告 id 就是报告目录名,出现在网址里:小写字母、数字与 -,以字母或数字开头 */
24
25
  declare const REPORT_ID_PATTERN: RegExp;
25
- /** 报告的入口,相对站点目录 */
26
+ /** JSON 报告的入口,相对站点目录;报告是哪种格式要看磁盘,用 reportEntryIn */
26
27
  declare function reportEntryOf(reportId: string): string;
27
- /** 站点里的报告 id:reports/ 下有 content.json 的目录,按名字排;目录名不合 REPORT_ID_PATTERN 的直接报错,不悄悄跳过 */
28
+ declare const isMdxEntry: (entry: string) => boolean;
29
+ /** 一份报告的入口,相对站点目录:reports/<id>/content.json 或 reports/<id>/index.mdx */
30
+ declare function reportEntryIn(siteDir: string, reportId: string): Promise<string>;
31
+ /** 站点里的报告 id:reports/ 下有入口文件的目录,按名字排;目录名不合 REPORT_ID_PATTERN 的直接报错,不悄悄跳过 */
28
32
  declare function reportIdsOf(siteDir: string): Promise<string[]>;
29
33
  /**
30
34
  * 读一份报告组装要的全部 JSON 文件(入口与它 include 到的章节),键是相对站点目录的路径,交给 assembleDocument。
31
35
  * 不读图片、不解码:网页显示用;检查与导出用 readReport,它还核对图片与快照
32
36
  */
33
37
  declare function readReportFiles(siteDir: string, entry: string): Promise<Record<string, string>>;
38
+ /** 读一份 MDX 报告的全部原文:入口、chapters 列的各章、存在时的 references.mdx,键是相对站点目录的路径,交给 assembleMdxReport */
39
+ declare function readMdxReportFiles(siteDir: string, entry: string): Promise<Record<string, string>>;
34
40
  //#endregion
35
- export { REPORT_ID_PATTERN, type ReadReportResult, type ReportCheckDiagnostic, type ReportCheckResult, type ReportExportOptions, type ReportExportResult, type SnapshotEntry, checkReports, readReport, readReportFiles, reportEntry, reportEntryOf, reportExport, reportIdsOf, reportsDirName };
41
+ export { type ConvertReportResult, JSON_ENTRY_NAME, REPORT_ID_PATTERN, type ReadReportResult, type ReportCheckDiagnostic, type ReportCheckResult, type ReportExportOptions, type ReportExportResult, type SnapshotEntry, checkReports, convertReportToMdx, isMdxEntry, readMdxReportFiles, readReport, readReportFiles, reportEntry, reportEntryIn, reportEntryOf, reportExport, reportIdsOf, reportsDirName };
package/dist/node.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { i as readReport, n as reportExport, r as checkReports, t as reportEntry } from "./export-TbclsIsE.mjs";
2
- import { a as reportsDirName, i as reportIdsOf, n as readReportFiles, r as reportEntryOf, t as REPORT_ID_PATTERN } from "./report-files-DM9Ea6yK.mjs";
3
- export { REPORT_ID_PATTERN, checkReports, readReport, readReportFiles, reportEntry, reportEntryOf, reportExport, reportIdsOf, reportsDirName };
1
+ import { a as readReport, i as checkReports, n as reportEntry, r as reportExport, t as convertReportToMdx } from "./convert-o7bPWP9A.mjs";
2
+ import { a as readReportFiles, c as reportIdsOf, i as readMdxReportFiles, l as reportsDirName, n as REPORT_ID_PATTERN, o as reportEntryIn, r as isMdxEntry, s as reportEntryOf, t as JSON_ENTRY_NAME } from "./report-files-BsIvifZe.mjs";
3
+ export { JSON_ENTRY_NAME, REPORT_ID_PATTERN, checkReports, convertReportToMdx, isMdxEntry, readMdxReportFiles, readReport, readReportFiles, reportEntry, reportEntryIn, reportEntryOf, reportExport, reportIdsOf, reportsDirName };
@@ -0,0 +1,103 @@
1
+ import { i as validateDocument, n as safeRelativePath, o as parseReportJson, s as ReportContentError } from "./assemble-BpllQNVY.mjs";
2
+ import { r as MDX_ENTRY_NAME, s as mdxReportInputsOf } from "./mdx-4Rs1N1nv.mjs";
3
+ import { readFile, readdir, realpath, stat } from "node:fs/promises";
4
+ import path from "node:path";
5
+ //#region src/node/report-files.ts
6
+ /** 站点里的报告:站点根下 reports/<报告 id>/,入口是 content.json(JSON 报告)或 index.mdx(MDX 报告),两种只能有一种 */
7
+ const reportsDirName = "reports";
8
+ const JSON_ENTRY_NAME = "content.json";
9
+ /** 报告 id 就是报告目录名,出现在网址里:小写字母、数字与 -,以字母或数字开头 */
10
+ const REPORT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
11
+ /** JSON 报告的入口,相对站点目录;报告是哪种格式要看磁盘,用 reportEntryIn */
12
+ function reportEntryOf(reportId) {
13
+ if (!REPORT_ID_PATTERN.test(reportId)) throw new ReportContentError(`报告 id 只能是小写字母、数字与 -,以字母或数字开头:${JSON.stringify(reportId)}`);
14
+ return `${reportsDirName}/${reportId}/${JSON_ENTRY_NAME}`;
15
+ }
16
+ const isMdxEntry = (entry) => entry === "index.mdx" || entry.endsWith(`/index.mdx`);
17
+ async function isFile(file) {
18
+ return stat(file).then((info) => info.isFile(), (error) => {
19
+ if (error.code === "ENOENT") return false;
20
+ throw error;
21
+ });
22
+ }
23
+ /** 报告目录里有哪种入口:两种都有直接报错,都没有回 undefined */
24
+ async function entryNameIn(reportDir, reportId) {
25
+ const [hasJson, hasMdx] = await Promise.all([isFile(path.join(reportDir, JSON_ENTRY_NAME)), isFile(path.join(reportDir, MDX_ENTRY_NAME))]);
26
+ if (hasJson && hasMdx) throw new ReportContentError(`报告目录里 ${JSON_ENTRY_NAME} 和 ${MDX_ENTRY_NAME} 只能留一个`, `${reportsDirName}/${reportId}`);
27
+ return hasJson ? JSON_ENTRY_NAME : hasMdx ? MDX_ENTRY_NAME : void 0;
28
+ }
29
+ /** 一份报告的入口,相对站点目录:reports/<id>/content.json 或 reports/<id>/index.mdx */
30
+ async function reportEntryIn(siteDir, reportId) {
31
+ const jsonEntry = reportEntryOf(reportId);
32
+ const name = await entryNameIn(path.join(siteDir, path.dirname(jsonEntry)), reportId);
33
+ if (name === void 0) throw new ReportContentError(`站点里没有报告 ${reportId}:reports/${reportId}/ 下既没有 ${JSON_ENTRY_NAME} 也没有 ${MDX_ENTRY_NAME}`, siteDir);
34
+ return `${reportsDirName}/${reportId}/${name}`;
35
+ }
36
+ /** 站点里的报告 id:reports/ 下有入口文件的目录,按名字排;目录名不合 REPORT_ID_PATTERN 的直接报错,不悄悄跳过 */
37
+ async function reportIdsOf(siteDir) {
38
+ const reportsDir = path.join(siteDir, reportsDirName);
39
+ let entries;
40
+ try {
41
+ entries = await readdir(reportsDir, { withFileTypes: true });
42
+ } catch (error) {
43
+ if (error.code === "ENOENT") return [];
44
+ throw error;
45
+ }
46
+ const ids = [];
47
+ for (const entry of entries) {
48
+ if (!entry.isDirectory()) continue;
49
+ if (await entryNameIn(path.join(reportsDir, entry.name), entry.name) === void 0) continue;
50
+ reportEntryOf(entry.name);
51
+ ids.push(entry.name);
52
+ }
53
+ return ids.sort();
54
+ }
55
+ /** 读站点目录里的一个文件(相对路径),挡住经符号链接跑出站点目录的 */
56
+ async function readInsideSite(root, relative) {
57
+ let real;
58
+ try {
59
+ real = await realpath(path.join(root, relative));
60
+ } catch {
61
+ throw new ReportContentError("Missing or unreadable file", relative);
62
+ }
63
+ const inside = path.relative(root, real);
64
+ if (inside === ".." || inside.startsWith(".." + path.sep) || path.isAbsolute(inside)) throw new ReportContentError("File escapes report root", relative);
65
+ return readFile(real, "utf8");
66
+ }
67
+ /**
68
+ * 读一份报告组装要的全部 JSON 文件(入口与它 include 到的章节),键是相对站点目录的路径,交给 assembleDocument。
69
+ * 不读图片、不解码:网页显示用;检查与导出用 readReport,它还核对图片与快照
70
+ */
71
+ async function readReportFiles(siteDir, entry) {
72
+ const root = await realpath(siteDir);
73
+ const files = {};
74
+ const load = async (relative) => {
75
+ if (Object.hasOwn(files, relative)) return;
76
+ const text = await readInsideSite(root, relative);
77
+ files[relative] = text;
78
+ const document = parseReportJson(text, relative);
79
+ validateDocument(document, relative);
80
+ for (const [index, block] of document.blocks.entries()) {
81
+ if (block.type !== "include") continue;
82
+ try {
83
+ safeRelativePath(block.src);
84
+ } catch (error) {
85
+ throw new ReportContentError(error.message, relative, `/blocks/${index}/src`);
86
+ }
87
+ await load(path.posix.join(path.posix.dirname(relative), block.src));
88
+ }
89
+ };
90
+ await load(entry);
91
+ return files;
92
+ }
93
+ /** 读一份 MDX 报告的全部原文:入口、chapters 列的各章、存在时的 references.mdx,键是相对站点目录的路径,交给 assembleMdxReport */
94
+ async function readMdxReportFiles(siteDir, entry) {
95
+ const root = await realpath(siteDir);
96
+ const files = { [entry]: await readInsideSite(root, entry) };
97
+ const { chapters, references } = mdxReportInputsOf(files[entry], entry);
98
+ for (const chapter of chapters) files[chapter] = await readInsideSite(root, chapter);
99
+ if (await isFile(path.join(root, references))) files[references] = await readInsideSite(root, references);
100
+ return files;
101
+ }
102
+ //#endregion
103
+ export { readReportFiles as a, reportIdsOf as c, readMdxReportFiles as i, reportsDirName as l, REPORT_ID_PATTERN as n, reportEntryIn as o, isMdxEntry as r, reportEntryOf as s, JSON_ENTRY_NAME as t };
@@ -1,5 +1,6 @@
1
- import { t as assembleDocument } from "./assemble-3cx8L2vo.mjs";
2
- import { i as reportIdsOf, n as readReportFiles, r as reportEntryOf, t as REPORT_ID_PATTERN } from "./report-files-DM9Ea6yK.mjs";
1
+ import { t as assembleDocument } from "./assemble-BpllQNVY.mjs";
2
+ import { a as assembleMdxReport, r as MDX_ENTRY_NAME, s as mdxReportInputsOf } from "./mdx-4Rs1N1nv.mjs";
3
+ import { a as readReportFiles, c as reportIdsOf, i as readMdxReportFiles, n as REPORT_ID_PATTERN, o as reportEntryIn, r as isMdxEntry, t as JSON_ENTRY_NAME } from "./report-files-BsIvifZe.mjs";
3
4
  import { n as reportHrefOf } from "./page-data-DLPoVgXy.mjs";
4
5
  import { readFile, realpath, stat } from "node:fs/promises";
5
6
  import path from "node:path";
@@ -8,7 +9,7 @@ import { Readable } from "node:stream";
8
9
  //#region src/site/loaders.server.ts
9
10
  /**
10
11
  * `@godcreator/word-report/site/server`:报告列表、报告页与报告里图片的 loader。报告从站点 reports/ 现读,
11
- * 报告页与图片在 JSON 原文没变时沿用上一次的组装结果。
12
+ * 原文没变时沿用上一次的组装结果。
12
13
  */
13
14
  /** 组装结果里有 Object.create(null) 的对象:换成普通对象再交给 React Router 序列化 */
14
15
  const plainOf = (result) => JSON.parse(JSON.stringify(result));
@@ -18,75 +19,94 @@ const plainOf = (result) => JSON.parse(JSON.stringify(result));
18
19
  */
19
20
  const latestAssemblies = /* @__PURE__ */ new Map();
20
21
  async function assembleFromDisk(siteDir, reportId) {
21
- const entry = reportEntryOf(reportId);
22
+ const entry = await reportEntryIn(siteDir, reportId);
23
+ const reportDir = path.posix.dirname(entry);
24
+ if (isMdxEntry(entry)) {
25
+ const files = await readMdxReportFiles(siteDir, entry);
26
+ const { chapters, references } = mdxReportInputsOf(files[entry], entry);
27
+ return {
28
+ files,
29
+ absentFiles: [`${reportDir}/${JSON_ENTRY_NAME}`, ...Object.hasOwn(files, references) ? [] : [references]],
30
+ result: assembleMdxReport({
31
+ reportId,
32
+ entry,
33
+ files
34
+ }, { mode: "complete" }),
35
+ chapterCount: chapters.length
36
+ };
37
+ }
22
38
  const files = await readReportFiles(siteDir, entry);
39
+ const entryDocument = JSON.parse(files[entry]);
40
+ const result = assembleDocument(entry, files, { mode: "complete" });
23
41
  return {
24
42
  files,
25
- result: assembleDocument(entry, files, { mode: "complete" })
43
+ absentFiles: [`${reportDir}/${MDX_ENTRY_NAME}`],
44
+ result,
45
+ chapterCount: entryDocument.blocks.filter((block) => block.type === "include").length
26
46
  };
27
47
  }
28
- /** 上次组装读过的每个 JSON 文件原文都没变;文件没了也算变了,交给重新组装去报错 */
29
- async function sameFilesOnDisk(siteDir, files) {
48
+ /** 上次组装读过的每个文件原文都没变、当时不存在的文件仍不存在;读过的文件没了也算变了,交给重新组装去报错 */
49
+ async function unchangedOnDisk(siteDir, assembly) {
30
50
  const root = await realpath(siteDir);
31
- return (await Promise.all(Object.entries(files).map(async ([relative, text]) => {
51
+ const textOrMissing = async (relative) => {
32
52
  try {
33
- return await readFile(path.join(root, relative), "utf8") === text;
53
+ return await readFile(path.join(root, relative), "utf8");
34
54
  } catch (error) {
35
- if (error.code === "ENOENT") return false;
55
+ if (error.code === "ENOENT") return null;
36
56
  throw error;
37
57
  }
38
- }))).every(Boolean);
58
+ };
59
+ const [sameTexts, stillAbsent] = await Promise.all([Promise.all(Object.entries(assembly.files).map(async ([relative, text]) => await textOrMissing(relative) === text)), Promise.all(assembly.absentFiles.map(async (relative) => await textOrMissing(relative) === null))]);
60
+ return [...sameTexts, ...stillAbsent].every(Boolean);
39
61
  }
40
- /** 报告的整份组装结果:JSON 原文和上次一样就用上次的,同时到的请求共用同一次组装;结果只读,要改先复制 */
41
- async function assembledReport(siteDir, reportId) {
62
+ /** 报告的整份组装:原文和上次一样就用上次的,同时到的请求共用同一次组装;结果只读,要改先复制 */
63
+ async function latestAssembly(siteDir, reportId) {
42
64
  const key = `${path.resolve(siteDir)}\0${reportId}`;
43
65
  const latest = latestAssemblies.get(key);
44
66
  if (latest !== void 0) {
45
67
  const assembly = await latest;
46
- if (await sameFilesOnDisk(siteDir, assembly.files)) return assembly.result;
68
+ if (await unchangedOnDisk(siteDir, assembly)) return assembly;
47
69
  }
48
70
  const newer = latestAssemblies.get(key);
49
- if (newer !== void 0 && newer !== latest) return (await newer).result;
71
+ if (newer !== void 0 && newer !== latest) return newer;
50
72
  const fresh = assembleFromDisk(siteDir, reportId);
51
73
  latestAssemblies.set(key, fresh);
52
74
  try {
53
- return (await fresh).result;
75
+ return await fresh;
54
76
  } catch (error) {
55
77
  if (latestAssemblies.get(key) === fresh) latestAssemblies.delete(key);
56
78
  throw error;
57
79
  }
58
80
  }
81
+ async function assembledReport(siteDir, reportId) {
82
+ return (await latestAssembly(siteDir, reportId)).result;
83
+ }
59
84
  /** 地址里的报告 id:形状不对或站点里没有这份报告都是 404 */
60
85
  async function reportIdOf(params, siteDir) {
61
86
  const reportId = params["report"] ?? "";
62
87
  if (!REPORT_ID_PATTERN.test(reportId) || !(await reportIdsOf(siteDir)).includes(reportId)) throw new Response(`站点里没有报告 ${reportId}`, { status: 404 });
63
88
  return reportId;
64
89
  }
65
- function summaryOf(reportId, files, result, problem) {
66
- const metadata = result?.document.metadata ?? {};
67
- const entryDocument = files === null ? null : JSON.parse(files[reportEntryOf(reportId)]);
90
+ function summaryOf(reportId, read, problem) {
91
+ const metadata = read?.result.document.metadata ?? {};
68
92
  return {
69
93
  id: reportId,
70
94
  title: metadata.title ?? reportId,
71
95
  revision: metadata.revision ?? null,
72
96
  date: metadata.date ?? null,
73
97
  authors: metadata.authors ?? [],
74
- chapterCount: entryDocument?.blocks.filter((block) => block.type === "include").length ?? 0,
75
- warningCount: result?.warnings.length ?? 0,
98
+ chapterCount: read?.chapterCount ?? 0,
99
+ warningCount: read?.result.warnings.length ?? 0,
76
100
  problem
77
101
  };
78
102
  }
79
103
  /** 报告列表:站点 reports/ 下的每份报告各一行;读不对的报告也列出来,带上原话 */
80
104
  async function reportListLoader(_args, site) {
81
105
  const reports = [];
82
- for (const reportId of await reportIdsOf(site.siteDir)) {
83
- let files = null;
84
- try {
85
- files = await readReportFiles(site.siteDir, reportEntryOf(reportId));
86
- reports.push(summaryOf(reportId, files, assembleDocument(reportEntryOf(reportId), files, { mode: "complete" }), null));
87
- } catch (error) {
88
- reports.push(summaryOf(reportId, null, null, error instanceof Error ? error.message : String(error)));
89
- }
106
+ for (const reportId of await reportIdsOf(site.siteDir)) try {
107
+ reports.push(summaryOf(reportId, await latestAssembly(site.siteDir, reportId), null));
108
+ } catch (error) {
109
+ reports.push(summaryOf(reportId, null, error instanceof Error ? error.message : String(error)));
90
110
  }
91
111
  return {
92
112
  siteName: site.siteName,
package/dist/site.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as assembleDocument } from "./assemble-3cx8L2vo.mjs";
1
+ import { t as assembleDocument } from "./assemble-BpllQNVY.mjs";
2
2
  import { n as reportHrefOf, t as reportAssetHrefOf } from "./page-data-DLPoVgXy.mjs";
3
3
  import { Link, useLoaderData } from "react-router";
4
4
  import { DocsBody, DocsPage, DocsTitle } from "@fumadocs/base-ui/layouts/docs/page";
@@ -61,10 +61,19 @@ class Composer:
61
61
  if kind not in ('Table', 'Figure'):
62
62
  raise ValueError(f'Unknown caption kind: {kind}')
63
63
  name = {'Table': '表', 'Figure': '图'}[kind]
64
- number = self.field_token('SEQ ' + kind + r' \* ARABIC', bookmark)
64
+ number = self.chapter_number(r'STYLEREF 1 \s', 'SEQ ' + kind + r' \* ARABIC \s 1', bookmark)
65
65
  return self.paragraph(f'{name} {number} {text}',
66
66
  'Table Caption' if kind == 'Table' else -35)
67
67
 
68
+ def chapter_number(self, chapter_code, sequence_code, bookmark):
69
+ """Return「章号-序号」 placeholders; ``bookmark`` spans both fields so REF shows the whole number."""
70
+ chapter = self.field_token(chapter_code)
71
+ number = self.field_token(sequence_code)
72
+ index = len(self._pending_numbers)
73
+ begin, end = f'@NUMSTART{index}@', f'@NUMEND{index}@'
74
+ self._pending_numbers.append((begin, end, bookmark))
75
+ return f'{begin}{chapter}-{number}{end}'
76
+
68
77
  def list(self, items, ordered=False):
69
78
  doc = self.doc
70
79
  list_start = doc.Content.End - 1
@@ -100,12 +109,8 @@ class Composer:
100
109
  boldSymbol=bold_symbol))
101
110
  if inline:
102
111
  return self.paragraph(before + token + after)
103
- chapter = self.field_token(r'STYLEREF 1 \s')
104
- number = self.field_token(r'SEQ Equation \* ARABIC \s 1')
105
- index = len(self._pending_numbers)
106
- begin, end = f'@NUMSTART{index}@', f'@NUMEND{index}@'
107
- self._pending_numbers.append((begin, end, bookmark))
108
- return self.paragraph(f'\t{token}\t({begin}{chapter}-{number}{end})', 'Equation')
112
+ number = self.chapter_number(r'STYLEREF 1 \s', r'SEQ Equation \* ARABIC \s 1', bookmark)
113
+ return self.paragraph(f'\t{token}\t({number})', 'Equation')
109
114
 
110
115
  def footnote(self, token, text):
111
116
  rng = self.doc.Content.Duplicate
@@ -313,12 +318,26 @@ class ContentComposer(Composer):
313
318
  setattr(run.Font, {'strong': 'Bold', 'em': 'Italic', 'sup': 'Superscript', 'sub': 'Subscript'}[mark], True)
314
319
  return rng
315
320
 
321
+ def chapter_sequence(self, kind, bookmark):
322
+ """「章号-序号」 for an equation, figure or table in the current top-level chapter."""
323
+ if self.chapter_is_appendix:
324
+ # The SEQ "\s 1" restart only fires on built-in Heading 1
325
+ # paragraphs; the template's Appendix Heading is Normal-based,
326
+ # so appendix sequences never restart there. Giving each
327
+ # appendix its own SEQ identifier keeps a natural 1, 2, 3 …
328
+ # count that renumbers on insert or delete like main chapters,
329
+ # without touching heading styles or their linked lists.
330
+ letter = chr(ord('A') + self.appendix_count - 1)
331
+ return self.chapter_number(r'STYLEREF "Appendix Heading" \s',
332
+ 'SEQ ' + kind + 'Appendix' + letter + r' \* ARABIC', bookmark)
333
+ return self.chapter_number(r'STYLEREF 1 \s', 'SEQ ' + kind + r' \* ARABIC \s 1', bookmark)
334
+
316
335
  def numbered_caption(self, node, kind):
317
336
  from .content import bookmark
318
337
  numbered = node.get('numbered', True)
319
338
  prefix = ''
320
339
  if numbered:
321
- prefix = {'Figure': '图 ', 'Table': '表 '}[kind] + self.field_token('SEQ ' + kind + r' \* ARABIC', bookmark(node['id'], '_n')) + ' '
340
+ prefix = {'Figure': '图 ', 'Table': '表 '}[kind] + self.chapter_sequence(kind, bookmark(node['id'], '_n')) + ' '
322
341
  rng = self.rich_paragraph(node['caption'], 'Table Caption' if kind == 'Table' else -35, prefix=prefix)
323
342
  title = self.part(rng, utf16_length(prefix), rng.End - rng.Start - utf16_length(prefix) - 1)
324
343
  self.mark(node['id'], title, '_t')
@@ -416,24 +435,9 @@ class ContentComposer(Composer):
416
435
  token = self.math_token(node, False)
417
436
  suffix = ''
418
437
  if node['numbered']:
419
- chapter = self.field_token(r'STYLEREF 1 \s' if self.chapter_style == 'Heading 1' else r'STYLEREF "Appendix Heading" \s')
420
- if self.chapter_is_appendix:
421
- # The SEQ "\s 1" restart only fires on built-in Heading 1
422
- # paragraphs; the template's Appendix Heading is Normal-based,
423
- # so appendix sequences never restart there. Giving each
424
- # appendix its own SEQ identifier keeps a natural 1, 2, 3 …
425
- # count that renumbers on insert or delete like main chapters,
426
- # without touching heading styles or their linked lists.
427
- letter = chr(ord('A') + self.appendix_count - 1)
428
- number = self.field_token(r'SEQ EquationAppendix' + letter + r' \* ARABIC')
429
- else:
430
- number = self.field_token(r'SEQ Equation \* ARABIC \s 1')
431
- self.chapter_first_equation = False
432
- i = len(self._pending_numbers)
433
- begin, end = f'@NUMSTART{i}@', f'@NUMEND{i}@'
434
438
  from .content import bookmark
435
- self._pending_numbers.append((begin, end, bookmark(node['id'], '_n')))
436
- suffix = f'\t({begin}{chapter}-{number}{end})'
439
+ self.chapter_first_equation = False
440
+ suffix = '\t(' + self.chapter_sequence('Equation', bookmark(node['id'], '_n')) + ')'
437
441
  rng = self.paragraph('\t' + token + suffix, 'Equation')
438
442
  rng.ParagraphFormat.TabStops.ClearAll()
439
443
  rng.ParagraphFormat.TabStops.Add(self.width / 2, 1, 0)
@@ -166,7 +166,10 @@ def validate_content(content, directory):
166
166
  blocks(item["children"], depth + 1, scope, mode, tables)
167
167
  elif kind == "equation" and item["numbered"]:
168
168
  require(chapter is not None, f"{item['id']}: numbered equation outside a chapter")
169
+ elif kind == "figure":
170
+ require(chapter is not None, f"{item['id']}: numbered figure outside a chapter")
169
171
  elif kind == "table":
172
+ require(chapter is not None or not item["numbered"], f"{item['id']}: numbered table outside a chapter")
170
173
  require(tables < 6, f"{item['id']}: Word renderer supports at most six table levels")
171
174
  for row in item["rows"]:
172
175
  for cell in row["cells"]:
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@godcreator/word-report",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "author": {
5
5
  "name": "godcreator"
6
6
  },
7
7
  "type": "module",
8
- "description": "报告:JSON 报告文档的格式、校验与分章组装(.);读报告目录、检查、导出 Word 的命令行 report-docx(./node);报告页与报告列表(./site),两页与图片的 loader(./site/server);engine/ 是驱动本机 Word 的 Python 导出引擎与 .dotx 模板;template/ 是建站器复制进站点 app/features/word-report/ 的接线文件(两条页面路由、图片路由、侧栏入口、样式)。",
8
+ "description": "报告:JSON 报告文档的格式、校验与分章组装,MDX 写法编译成它(.);读报告目录、检查、导出 Word、JSON 报告转 MDX 的命令行 report-docx(./node);报告页与报告列表(./site),两页与图片的 loader(./site/server);engine/ 是驱动本机 Word 的 Python 导出引擎与 .dotx 模板;template/ 是建站器复制进站点 app/features/word-report/ 的接线文件(两条页面路由、图片路由、侧栏入口、样式)。",
9
9
  "license": "MIT",
10
10
  "exports": {
11
11
  ".": {
@@ -43,7 +43,14 @@
43
43
  ],
44
44
  "dependencies": {
45
45
  "@godcreator/comments": "^0.1.3",
46
- "sharp": "^0.35.5"
46
+ "remark-frontmatter": "^5.0.0",
47
+ "remark-gfm": "^4.0.1",
48
+ "remark-math": "^6.0.0",
49
+ "remark-mdx": "^3.1.0",
50
+ "remark-parse": "^11.0.0",
51
+ "sharp": "^0.35.5",
52
+ "unified": "^11.0.5",
53
+ "yaml": "^2.9.1"
47
54
  },
48
55
  "peerDependencies": {
49
56
  "@fumadocs/base-ui": "^16.17.1",
@@ -1,72 +0,0 @@
1
- import { a as parseReportJson, n as safeRelativePath, o as ReportContentError, r as validateDocument } from "./assemble-3cx8L2vo.mjs";
2
- import { readFile, readdir, realpath, stat } from "node:fs/promises";
3
- import path from "node:path";
4
- //#region src/node/report-files.ts
5
- /** 站点里的报告:站点根下 reports/<报告 id>/content.json,章节与图片在同一个报告目录里 */
6
- const reportsDirName = "reports";
7
- /** 报告 id 就是报告目录名,出现在网址里:小写字母、数字与 -,以字母或数字开头 */
8
- const REPORT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
9
- /** 报告的入口,相对站点目录 */
10
- function reportEntryOf(reportId) {
11
- if (!REPORT_ID_PATTERN.test(reportId)) throw new ReportContentError(`报告 id 只能是小写字母、数字与 -,以字母或数字开头:${JSON.stringify(reportId)}`);
12
- return `${reportsDirName}/${reportId}/content.json`;
13
- }
14
- /** 站点里的报告 id:reports/ 下有 content.json 的目录,按名字排;目录名不合 REPORT_ID_PATTERN 的直接报错,不悄悄跳过 */
15
- async function reportIdsOf(siteDir) {
16
- const reportsDir = path.join(siteDir, reportsDirName);
17
- let entries;
18
- try {
19
- entries = await readdir(reportsDir, { withFileTypes: true });
20
- } catch (error) {
21
- if (error.code === "ENOENT") return [];
22
- throw error;
23
- }
24
- const ids = [];
25
- for (const entry of entries) {
26
- if (!entry.isDirectory()) continue;
27
- if (!await stat(path.join(reportsDir, entry.name, "content.json")).then((info) => info.isFile(), (error) => {
28
- if (error.code === "ENOENT") return false;
29
- throw error;
30
- })) continue;
31
- reportEntryOf(entry.name);
32
- ids.push(entry.name);
33
- }
34
- return ids.sort();
35
- }
36
- /**
37
- * 读一份报告组装要的全部 JSON 文件(入口与它 include 到的章节),键是相对站点目录的路径,交给 assembleDocument。
38
- * 不读图片、不解码:网页显示用;检查与导出用 readReport,它还核对图片与快照
39
- */
40
- async function readReportFiles(siteDir, entry) {
41
- const root = await realpath(siteDir);
42
- const files = {};
43
- const load = async (relative) => {
44
- if (Object.hasOwn(files, relative)) return;
45
- const absolute = path.join(root, relative);
46
- let real;
47
- try {
48
- real = await realpath(absolute);
49
- } catch {
50
- throw new ReportContentError("Missing or unreadable file", relative);
51
- }
52
- const inside = path.relative(root, real);
53
- if (inside === ".." || inside.startsWith(".." + path.sep) || path.isAbsolute(inside)) throw new ReportContentError("File escapes report root", relative);
54
- const text = await readFile(real, "utf8");
55
- files[relative] = text;
56
- const document = parseReportJson(text, relative);
57
- validateDocument(document, relative);
58
- for (const [index, block] of document.blocks.entries()) {
59
- if (block.type !== "include") continue;
60
- try {
61
- safeRelativePath(block.src);
62
- } catch (error) {
63
- throw new ReportContentError(error.message, relative, `/blocks/${index}/src`);
64
- }
65
- await load(path.posix.join(path.posix.dirname(relative), block.src));
66
- }
67
- };
68
- await load(entry);
69
- return files;
70
- }
71
- //#endregion
72
- export { reportsDirName as a, reportIdsOf as i, readReportFiles as n, reportEntryOf as r, REPORT_ID_PATTERN as t };