@godcreator/word-report 0.4.0 → 0.5.0

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/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  | `@godcreator/code-pin`、`@godcreator/doc-export`、`@godcreator/trajectory` | 钉码、生成物、轨迹卡与星轨盘 |
14
14
  | `@godcreator/docfirst` | 命令行 `docfirst`:核对、确认、生成 |
15
15
  | `@godcreator/collab` | 项目协作:任务与模块、日志与日历、通知、合并检查 |
16
- | `@godcreator/word-report` | 报告:JSON 报告文档、报告列表与报告页、命令行 `report-docx`(检查、导出 Word) |
16
+ | `@godcreator/word-report` | 报告:正文写成 MDX 或 JSON 的报告文档、报告列表与报告页、命令行 `report-docx`(检查、导出 Word、JSON 转 MDX) |
17
17
 
18
18
  | 插件(`plugins/`) | 做什么 |
19
19
  | --- | --- |
@@ -182,6 +182,29 @@ const schema = {
182
182
  "latex": {
183
183
  "type": "string",
184
184
  "minLength": 1
185
+ },
186
+ "plainText": {
187
+ "type": "array",
188
+ "minItems": 1,
189
+ "description": "Word writes these runs as text instead of a MathType object; the web view still renders latex.",
190
+ "items": {
191
+ "type": "object",
192
+ "properties": {
193
+ "text": { "$ref": "#/$defs/line" },
194
+ "marks": {
195
+ "type": "array",
196
+ "items": { "enum": [
197
+ "em",
198
+ "sub",
199
+ "sup"
200
+ ] },
201
+ "uniqueItems": true,
202
+ "not": { "allOf": [{ "contains": { "const": "sub" } }, { "contains": { "const": "sup" } }] }
203
+ }
204
+ },
205
+ "required": ["text"],
206
+ "additionalProperties": false
207
+ }
185
208
  }
186
209
  },
187
210
  "required": [
@@ -1192,4 +1215,4 @@ function alphabet(n) {
1192
1215
  return s;
1193
1216
  }
1194
1217
  //#endregion
1195
- export { parseReportJson as a, walk as i, safeRelativePath as n, ReportContentError as o, validateDocument as r, assembleDocument as t };
1218
+ export { walk as a, validateDocument as i, safeRelativePath as n, parseReportJson as o, stableValue as r, ReportContentError as s, assembleDocument as t };
package/dist/cli.d.mts CHANGED
@@ -1,8 +1,10 @@
1
- import { n as ReportExportResult, o as ReportCheckResult, t as ReportExportOptions } from "./export-DC764Umz.mjs";
1
+ import { n as ReportExportResult, o as ReportCheckResult, t as ReportExportOptions } from "./export-DMKj4gl2.mjs";
2
2
  //#region src/node/cli.d.ts
3
+ /** export、status 的入口要看磁盘上是哪种报告,由 main 补上 */
3
4
  type CliCommand = {
4
5
  command: 'export' | 'status';
5
- options: ReportExportOptions;
6
+ reportId: string;
7
+ options: Omit<ReportExportOptions, 'entry'>;
6
8
  } | {
7
9
  command: 'check';
8
10
  siteDir: string;
@@ -13,6 +15,7 @@ declare function parseCliArgs(args: string[], cwd?: string): CliCommand | undefi
13
15
  interface CliDependencies {
14
16
  reportExport?: (options: ReportExportOptions) => Promise<ReportExportResult>;
15
17
  checkReports?: (siteDir: string) => Promise<ReportCheckResult>;
18
+ reportEntryIn?: (siteDir: string, reportId: string) => Promise<string>;
16
19
  sleep?: (milliseconds: number) => Promise<void>;
17
20
  stdout?: (text: string) => void;
18
21
  stderr?: (text: string) => void;
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { n as reportExport, r as checkReports } from "./export-DJQiBPPU.mjs";
3
- import { r as reportEntryOf } from "./report-files-CgWzLxH-.mjs";
2
+ import { n as reportExport, r as checkReports } from "./export-B01-yaAq.mjs";
3
+ import { o as reportEntryIn, s as reportEntryOf } from "./report-files-Dfr71bRv.mjs";
4
4
  import path from "node:path";
5
5
  import { realpathSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
@@ -8,10 +8,10 @@ import { fileURLToPath } from "node:url";
8
8
  const help = `用法:report-docx <export|status> <报告 id> [选项]
9
9
  report-docx check [选项]
10
10
 
11
- 报告在站点目录的 reports/<报告 id>/content.json。
12
- export 导出这份报告的 Word 与 PDF,等这次导出做完,把结果按 JSON 打出来
13
- status 只看这份报告的导出状态,不启动 Word
14
- check 检查站点里全部报告(每份报告、每一章)的格式、引用与图片,有问题时退出码非零
11
+ 报告在站点目录的 reports/<报告 id>/:入口是 content.json(JSON 报告)或 index.mdx(MDX 报告)。
12
+ export 导出这份报告的 Word 与 PDF,等这次导出做完,把结果按 JSON 打出来
13
+ status 只看这份报告的导出状态,不启动 Word
14
+ check 检查站点里全部报告(JSON 报告的每份与每一章、MDX 报告整份)的格式、引用与图片,有问题时退出码非零
15
15
 
16
16
  选项:
17
17
  --site <目录> 站点目录(默认当前目录)
@@ -57,14 +57,14 @@ function parseCliArgs(args, cwd = process.cwd()) {
57
57
  if (command !== "export" && command !== "status") throw new Error(`认不出的命令 ${JSON.stringify(command)}:只有 export、status、check;用 --help 查看用法`);
58
58
  const [reportId, ...flags] = rest;
59
59
  if (reportId === void 0 || reportId.startsWith("-")) throw new Error(`${command} 要给报告 id(站点 reports/ 下的目录名)`);
60
- const entry = reportEntryOf(reportId);
60
+ reportEntryOf(reportId);
61
61
  const { values } = flagValuesOf(flags, ["--site", "--engine"]);
62
62
  const engine = values.get("--engine");
63
63
  return {
64
64
  command,
65
+ reportId,
65
66
  options: {
66
67
  siteDir: path.resolve(cwd, values.get("--site") ?? "."),
67
- entry,
68
68
  mode: "complete",
69
69
  action: command === "export" ? "start" : "status",
70
70
  ...engine !== void 0 ? { engineDir: path.resolve(cwd, engine) } : {}
@@ -113,7 +113,11 @@ async function main(args, dependencies = {}) {
113
113
  write(parsed.json ? `${JSON.stringify(result)}\n` : checkReportText(result));
114
114
  return result.problems.length > 0 ? 1 : 0;
115
115
  }
116
- const result = await runExport(parsed.options, {
116
+ const entry = await (dependencies.reportEntryIn ?? reportEntryIn)(parsed.options.siteDir, parsed.reportId);
117
+ const result = await runExport({
118
+ ...parsed.options,
119
+ entry
120
+ }, {
117
121
  reportExport: dependencies.reportExport ?? reportExport,
118
122
  sleep
119
123
  });
package/dist/core.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as SingleImageFigure, E as TableCell, S as Section, T as Table, _ as ReportContentError, a as CellBlock, b as ReportInline, c as ContentWarning, d as FigurePanel, f as Include, g as ReportBlock, h as Paragraph, i as Bibliography, l as Equation, m as MultiImageFigure, n as AssemblyOptions, o as Code, p as IndexEntry, r as AssemblyResult, s as CompiledReport, t as Abstract, u as Figure, v as ReportDocument, w as SourceLocation, x as ReportList, y as ReportIndex } from "./types-Bcl48xQN.mjs";
1
+ import { C as Section, D as TableCell, E as Table, S as ReportList, T as SourceLocation, _ as ReportBlock, a as CellBlock, b as ReportIndex, c as ContentWarning, d as FigurePanel, f as Include, g as PlainMathRun, h as Paragraph, i as Bibliography, l as Equation, m as MultiImageFigure, n as AssemblyOptions, o as Code, p as IndexEntry, r as AssemblyResult, s as CompiledReport, t as Abstract, u as Figure, v as ReportContentError, w as SingleImageFigure, x as ReportInline, y as ReportDocument } from "./types-Ea2OT6Ac.mjs";
2
2
  //#region src/core/assemble.d.ts
3
3
  declare function assembleDocument(entry: string, files: Record<string, string | ReportDocument>, options?: AssemblyOptions): AssemblyResult;
4
4
  //#endregion
@@ -10,4 +10,39 @@ declare function parseReportJson(text: string, file?: string): unknown;
10
10
  declare function validateDocument(value: unknown, file?: string): asserts value is ReportDocument;
11
11
  declare function safeRelativePath(raw: string, asset?: boolean): string;
12
12
  //#endregion
13
- export { Abstract, AssemblyOptions, AssemblyResult, Bibliography, CellBlock, Code, CompiledReport, ContentWarning, Equation, Figure, FigurePanel, Include, IndexEntry, MultiImageFigure, Paragraph, ReportBlock, ReportContentError, ReportDocument, ReportIndex, ReportInline, ReportList, Section, SingleImageFigure, SourceLocation, Table, TableCell, assembleDocument, parseReportJson, safeRelativePath, validateDocument };
13
+ //#region src/core/mdx/compile.d.ts
14
+ /** 报告目录里固定名字的两个文件 */
15
+ declare const MDX_ENTRY_NAME = "index.mdx";
16
+ declare const MDX_REFERENCES_NAME = "references.mdx";
17
+ interface MdxReportInput {
18
+ /** 报告目录名,用作文档 id */
19
+ reportId: string;
20
+ /** 入口 index.mdx 在 files 里的键;章与文献列表的路径相对它所在的目录 */
21
+ entry: string;
22
+ /** 路径 → MDX 原文:入口、chapters 列的各章,以及存在时的 references.mdx */
23
+ files: Record<string, string>;
24
+ }
25
+ interface CompiledMdxReport {
26
+ document: ReportDocument;
27
+ /** 节点 id → MDX 文件与「行:列」 */
28
+ sources: Record<string, SourceLocation>;
29
+ /** 编译结果里的 JSON 指针 → 最近的有记录的上级节点的 MDX 位置 */
30
+ locate(pointer: string): SourceLocation | undefined;
31
+ /** 编进来的 MDX 文件,按入口、各章、文献列表的顺序 */
32
+ files: string[];
33
+ }
34
+ /** 读入口原文就知道还要读哪些文件:章按 chapters 的顺序,文献列表 references.mdx 有没有由调用方看磁盘 */
35
+ declare function mdxReportInputsOf(indexText: string, entry: string): {
36
+ chapters: string[];
37
+ references: string;
38
+ };
39
+ /** 整份 MDX 报告 → 一份 docreport 文档(入口正文在前,各章按 chapters 的顺序接在后面);只解析、不执行 MDX */
40
+ declare function compileMdxReport({ reportId, entry, files }: MdxReportInput): CompiledMdxReport;
41
+ /** 编译并组装一份 MDX 报告:结果和 JSON 报告的组装结果同形,sources 指向 MDX 的行列,dependencies 是编进来的 MDX 文件 */
42
+ declare function assembleMdxReport(input: MdxReportInput, options?: AssemblyOptions): AssemblyResult;
43
+ //#endregion
44
+ //#region src/core/mdx/ids.d.ts
45
+ /** 编译器生成的 id 都以它开头;手写的 id 不许用,两边就不会撞 */
46
+ declare const GENERATED_ID_PREFIX = "auto-";
47
+ //#endregion
48
+ export { Abstract, AssemblyOptions, AssemblyResult, Bibliography, CellBlock, Code, type CompiledMdxReport, CompiledReport, ContentWarning, Equation, Figure, FigurePanel, GENERATED_ID_PREFIX, Include, IndexEntry, MDX_ENTRY_NAME, MDX_REFERENCES_NAME, type MdxReportInput, MultiImageFigure, Paragraph, PlainMathRun, ReportBlock, ReportContentError, ReportDocument, ReportIndex, ReportInline, ReportList, Section, SingleImageFigure, SourceLocation, Table, TableCell, assembleDocument, assembleMdxReport, compileMdxReport, mdxReportInputsOf, parseReportJson, safeRelativePath, validateDocument };
package/dist/core.mjs CHANGED
@@ -1 +1,3 @@
1
- export { o as ReportContentError, t as assembleDocument, a as parseReportJson, n as safeRelativePath, r as validateDocument } from "./assemble-D25NLmLx.mjs";
1
+ import { i as validateDocument, n as safeRelativePath, o as parseReportJson, s as ReportContentError, t as assembleDocument } from "./assemble-BEt9_xsB.mjs";
2
+ import { a as mdxReportInputsOf, i as compileMdxReport, n as MDX_REFERENCES_NAME, o as GENERATED_ID_PREFIX, r as assembleMdxReport, t as MDX_ENTRY_NAME } from "./mdx-C1RVv6E-.mjs";
3
+ export { GENERATED_ID_PREFIX, MDX_ENTRY_NAME, MDX_REFERENCES_NAME, ReportContentError, assembleDocument, assembleMdxReport, compileMdxReport, mdxReportInputsOf, parseReportJson, safeRelativePath, validateDocument };
@@ -1,5 +1,6 @@
1
- import { a as parseReportJson, i as walk, n as safeRelativePath, o as ReportContentError, r as validateDocument, t as assembleDocument } from "./assemble-D25NLmLx.mjs";
2
- import { a as reportsDirName } from "./report-files-CgWzLxH-.mjs";
1
+ import { a as walk, i as validateDocument, n as safeRelativePath, o as parseReportJson, s as ReportContentError, t as assembleDocument } from "./assemble-BEt9_xsB.mjs";
2
+ import { a as mdxReportInputsOf, r as assembleMdxReport, t as MDX_ENTRY_NAME } from "./mdx-C1RVv6E-.mjs";
3
+ import { l as reportsDirName, t as JSON_ENTRY_NAME } from "./report-files-Dfr71bRv.mjs";
3
4
  import { access, mkdir, open, readFile, readdir, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
4
5
  import path from "node:path";
5
6
  import { createHash, randomUUID } from "node:crypto";
@@ -51,18 +52,20 @@ async function readReport(entryAbsolute, options = {}) {
51
52
  throw new ReportContentError("Unreadable report file", key);
52
53
  }
53
54
  }
55
+ async function readText(absolute, format) {
56
+ const { bytes, key } = await readWithin(absolute);
57
+ try {
58
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
59
+ } catch {
60
+ throw new ReportContentError(`Document must be UTF-8 ${format}`, key);
61
+ }
62
+ }
54
63
  async function load(absolute) {
55
64
  const key = relative(absolute);
56
65
  if (visiting.has(key)) throw new ReportContentError("Circular document include", key);
57
66
  if (Object.hasOwn(files, key)) return;
58
67
  visiting.add(key);
59
- const { bytes } = await readWithin(absolute);
60
- let text;
61
- try {
62
- text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
63
- } catch {
64
- throw new ReportContentError("Document must be UTF-8 JSON", key);
65
- }
68
+ const text = await readText(absolute, "JSON");
66
69
  files[key] = text;
67
70
  const document = parseReportJson(text, key);
68
71
  validateDocument(document, key);
@@ -76,8 +79,28 @@ async function readReport(entryAbsolute, options = {}) {
76
79
  }
77
80
  visiting.delete(key);
78
81
  }
79
- await load(entry);
80
- const result = assembleDocument(relative(entry), files, options);
82
+ /** MDX 报告:入口、各章与文献列表都进快照,编译成一份文档再组装 */
83
+ async function loadMdx() {
84
+ const entryKey = relative(entry);
85
+ files[entryKey] = await readText(entry, "MDX");
86
+ const { chapters, references } = mdxReportInputsOf(files[entryKey], entryKey);
87
+ for (const chapter of chapters) files[chapter] = await readText(path.resolve(root, chapter), "MDX");
88
+ if (await stat(path.resolve(root, references)).then((info) => info.isFile(), (error) => {
89
+ if (error.code === "ENOENT") return false;
90
+ throw error;
91
+ })) files[references] = await readText(path.resolve(root, references), "MDX");
92
+ return assembleMdxReport({
93
+ reportId: path.basename(path.dirname(entry)),
94
+ entry: entryKey,
95
+ files
96
+ }, options);
97
+ }
98
+ let result;
99
+ if (path.basename(entry) === "index.mdx") result = await loadMdx();
100
+ else {
101
+ await load(entry);
102
+ result = assembleDocument(relative(entry), files, options);
103
+ }
81
104
  const assetBytes = Object.create(null), normalized = /* @__PURE__ */ new Map();
82
105
  for (const [src, source] of Object.entries(result.assets)) {
83
106
  const folded = source.normalize("NFC").toLowerCase(), previous = normalized.get(folded);
@@ -121,7 +144,10 @@ async function readReport(entryAbsolute, options = {}) {
121
144
  }
122
145
  //#endregion
123
146
  //#region src/node/check.ts
124
- /** 检查站点 reports/ 下的每个 content.json(每份报告与每一章):只用本包的代码,不执行站点里的脚本 */
147
+ /**
148
+ * 检查站点 reports/ 下的每个 content.json(每份报告与每一章)与每份 MDX 报告(报告目录根上的 index.mdx,只整份检查:MDX 的章不是独立文档)。
149
+ * 只用本包的代码,不执行站点里的脚本
150
+ */
125
151
  async function checkReports(siteDir) {
126
152
  const result = {
127
153
  checked: 0,
@@ -136,7 +162,8 @@ async function checkReports(siteDir) {
136
162
  throw new ReportContentError("没有这个站点目录", siteDir);
137
163
  }
138
164
  const relative = (file) => path.relative(root, file).split(path.sep).join("/");
139
- const entries = [], directories = /* @__PURE__ */ new Set();
165
+ const entries = [], mdxEntries = [], directories = /* @__PURE__ */ new Set();
166
+ const reports = path.join(root, reportsDirName);
140
167
  function diagnostic(error, file, mode) {
141
168
  const known = error instanceof ReportContentError;
142
169
  result.problems.push({
@@ -158,9 +185,9 @@ async function checkReports(siteDir) {
158
185
  if (item.isSymbolicLink()) isDirectory = (await stat(file)).isDirectory();
159
186
  if (isDirectory) await scan(file);
160
187
  else if (item.name === "content.json") entries.push(file);
188
+ else if (item.name === "index.mdx" && path.dirname(directory) === reports) mdxEntries.push(file);
161
189
  }
162
190
  }
163
- const reports = path.join(root, reportsDirName);
164
191
  try {
165
192
  await stat(reports);
166
193
  } catch (error) {
@@ -169,6 +196,35 @@ async function checkReports(siteDir) {
169
196
  }
170
197
  await scan(reports);
171
198
  entries.sort();
199
+ mdxEntries.sort();
200
+ for (const entry of mdxEntries) {
201
+ const file = relative(entry);
202
+ result.checked++;
203
+ try {
204
+ if (entries.includes(path.join(path.dirname(entry), "content.json"))) throw new ReportContentError(`报告目录里 ${JSON_ENTRY_NAME} 和 ${MDX_ENTRY_NAME} 只能留一个`, relative(path.dirname(entry)));
205
+ const report = await readReport(entry, {
206
+ root,
207
+ mode: "complete"
208
+ });
209
+ result.warnings.push(...report.warnings.map((warning) => ({
210
+ ...warning,
211
+ entry: file,
212
+ mode: "complete"
213
+ })));
214
+ result.runs.push({
215
+ file,
216
+ mode: "complete",
217
+ passed: true
218
+ });
219
+ } catch (error) {
220
+ diagnostic(error, file, "complete");
221
+ result.runs.push({
222
+ file,
223
+ mode: "complete",
224
+ passed: false
225
+ });
226
+ }
227
+ }
172
228
  for (const entry of entries) {
173
229
  const file = relative(entry);
174
230
  result.checked++;
@@ -242,7 +298,7 @@ function installedPackageDir() {
242
298
  const packagedEngineDir = (packageDir) => path.join(packageDir, "engine");
243
299
  /** 入口相对站点目录,不收本机的任意路径 */
244
300
  function reportEntry(entry) {
245
- if (typeof entry !== "string" || !entry.endsWith("/content.json") || /[\\:\x00-\x1f]/.test(entry) || entry.startsWith("/") || entry.split("/").some((part) => !part || part === "." || part === "..")) throw new Error("报告入口要是站点目录里的相对 content.json 路径,如 reports/<报告 id>/content.json");
301
+ if (typeof entry !== "string" || !(entry.endsWith("/content.json") || entry.endsWith("/index.mdx")) || /[\\:\x00-\x1f]/.test(entry) || entry.startsWith("/") || entry.split("/").some((part) => !part || part === "." || part === "..")) throw new Error("报告入口要是站点目录里的相对路径,如 reports/<报告 id>/content.json 或 reports/<报告 id>/index.mdx");
246
302
  return entry;
247
303
  }
248
304
  function reportMode(value) {
@@ -502,7 +558,7 @@ async function reportExport(options) {
502
558
  const outputs = await outputPaths(engine, previous);
503
559
  if (!await exists(path.join(engine, ".venv/Scripts/python.exe"))) return {
504
560
  state: "unavailable",
505
- message: `Word 引擎 ${engine} 里还没有 Python 环境:在那里运行 python -m venv .venv 与 .venv\\Scripts\\pip install -r requirements.txt(要本机装着 Word 与 MathType);引擎放在别处时用 --engine 或环境变量 DOCREPORT_ENGINE_ROOT 指过去`,
561
+ message: `Word 引擎 ${engine} 里还没有 Python 环境:在那里运行 python -m venv .venv、.venv\\Scripts\\pip install -r requirements.txt 与 .venv\\Scripts\\pip install --no-build-isolation --no-deps -e .(要本机装着 Word 与 MathType);node_modules 里的路径太长时 Word 打不开图片,把 engine/ 复制到路径短的目录、用 --engine 或环境变量 DOCREPORT_ENGINE_ROOT 指过去`,
506
562
  ...outputs
507
563
  };
508
564
  let current;
@@ -1,4 +1,4 @@
1
- import { c as ContentWarning } from "./types-Bcl48xQN.mjs";
1
+ import { c as ContentWarning } from "./types-Ea2OT6Ac.mjs";
2
2
  //#region src/node/check.d.ts
3
3
  interface ReportCheckDiagnostic {
4
4
  file: string;
@@ -20,7 +20,10 @@ interface ReportCheckResult {
20
20
  mode: 'standalone' | 'complete';
21
21
  })[];
22
22
  }
23
- /** 检查站点 reports/ 下的每个 content.json(每份报告与每一章):只用本包的代码,不执行站点里的脚本 */
23
+ /**
24
+ * 检查站点 reports/ 下的每个 content.json(每份报告与每一章)与每份 MDX 报告(报告目录根上的 index.mdx,只整份检查:MDX 的章不是独立文档)。
25
+ * 只用本包的代码,不执行站点里的脚本
26
+ */
24
27
  declare function checkReports(siteDir: string): Promise<ReportCheckResult>;
25
28
  //#endregion
26
29
  //#region src/node/export.d.ts
@@ -38,7 +41,7 @@ type Job = {
38
41
  };
39
42
  interface ReportExportOptions {
40
43
  siteDir: string;
41
- /** 相对站点目录的 content.json 路径,如 reports/<报告 id>/content.json(reportEntryOf) */
44
+ /** 相对站点目录的入口路径:reports/<报告 id>/content.json 或 reports/<报告 id>/index.mdx(reportEntryIn) */
42
45
  entry: string;
43
46
  mode?: Mode;
44
47
  action?: 'start' | 'status';