@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/README.md +1 -1
- package/dist/{assemble-3cx8L2vo.mjs → assemble-BpllQNVY.mjs} +17 -11
- package/dist/cli.d.mts +11 -2
- package/dist/cli.mjs +41 -11
- package/dist/{export-TbclsIsE.mjs → convert-o7bPWP9A.mjs} +227 -23
- package/dist/core.d.mts +56 -1
- package/dist/core.mjs +3 -1
- package/dist/{export-DC764Umz.d.mts → export-Da4yhrZa.d.mts} +22 -3
- package/dist/mdx-4Rs1N1nv.mjs +1289 -0
- package/dist/node.d.mts +11 -5
- package/dist/node.mjs +3 -3
- package/dist/report-files-BsIvifZe.mjs +103 -0
- package/dist/site-server.mjs +49 -29
- package/dist/site.mjs +1 -1
- package/engine/src/docreport/compose.py +29 -25
- package/engine/src/docreport/content.py +3 -0
- package/package.json +10 -3
- package/dist/report-files-DM9Ea6yK.mjs +0 -72
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` |
|
|
16
|
+
| `@godcreator/word-report` | 报告:正文写成 MDX 或 JSON 的报告文档、报告列表与报告页、命令行 `report-docx`(检查、导出 Word、JSON 转 MDX) |
|
|
17
17
|
|
|
18
18
|
| 插件(`plugins/`) | 做什么 |
|
|
19
19
|
| --- | --- |
|
|
@@ -994,12 +994,21 @@ function assembleDocument(entry, files, options = {}) {
|
|
|
994
994
|
}
|
|
995
995
|
const counts = {
|
|
996
996
|
decimal: 0,
|
|
997
|
-
appendix: 0
|
|
998
|
-
figure: 0,
|
|
999
|
-
table: 0
|
|
997
|
+
appendix: 0
|
|
1000
998
|
};
|
|
1001
999
|
let appendixSeen = false;
|
|
1002
|
-
const
|
|
1000
|
+
const chapterCounts = {
|
|
1001
|
+
figure: /* @__PURE__ */ new Map(),
|
|
1002
|
+
table: /* @__PURE__ */ new Map(),
|
|
1003
|
+
equation: /* @__PURE__ */ new Map()
|
|
1004
|
+
};
|
|
1005
|
+
/** 图、编号表格、编号公式各自按所在的顶层章从 1 编,写成「章号-n」;导出 Word 的题注同样取章号,不在编号章里就没有章号可取 */
|
|
1006
|
+
function chapterNumber(node, kind, chapter) {
|
|
1007
|
+
if (!chapter) fail(node, `Numbered ${kind} outside a numbered chapter`);
|
|
1008
|
+
const n = (chapterCounts[kind].get(chapter) ?? 0) + 1;
|
|
1009
|
+
chapterCounts[kind].set(chapter, n);
|
|
1010
|
+
return `${chapter}-${n}`;
|
|
1011
|
+
}
|
|
1003
1012
|
function blockNumbers(blocks, depth = 0, chapter, parentNumber, parentMode, tables = 0) {
|
|
1004
1013
|
let decimal = 0;
|
|
1005
1014
|
for (const node of blocks) {
|
|
@@ -1029,18 +1038,15 @@ function assembleDocument(entry, files, options = {}) {
|
|
|
1029
1038
|
continue;
|
|
1030
1039
|
}
|
|
1031
1040
|
if (node.type === "figure") {
|
|
1032
|
-
number =
|
|
1041
|
+
number = chapterNumber(node, "figure", chapter);
|
|
1033
1042
|
label = `图 ${number}`;
|
|
1034
1043
|
}
|
|
1035
1044
|
if (node.type === "table" && node.numbered) {
|
|
1036
|
-
number =
|
|
1045
|
+
number = chapterNumber(node, "table", chapter);
|
|
1037
1046
|
label = `表 ${number}`;
|
|
1038
1047
|
}
|
|
1039
1048
|
if (node.type === "equation" && node.numbered) {
|
|
1040
|
-
|
|
1041
|
-
const n = (eqCounts.get(chapter) ?? 0) + 1;
|
|
1042
|
-
eqCounts.set(chapter, n);
|
|
1043
|
-
number = `${chapter}-${n}`;
|
|
1049
|
+
number = chapterNumber(node, "equation", chapter);
|
|
1044
1050
|
label = `式(${number})`;
|
|
1045
1051
|
}
|
|
1046
1052
|
if (node.id) index[node.id] = {
|
|
@@ -1186,4 +1192,4 @@ function alphabet(n) {
|
|
|
1186
1192
|
return s;
|
|
1187
1193
|
}
|
|
1188
1194
|
//#endregion
|
|
1189
|
-
export {
|
|
1195
|
+
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,18 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as ConvertReportResult, c as ReportCheckResult, n as ReportExportResult, t as ReportExportOptions } from "./export-Da4yhrZa.mjs";
|
|
2
2
|
//#region src/node/cli.d.ts
|
|
3
|
+
/** export、status 的入口要看磁盘上是哪种报告,由 main 补上 */
|
|
3
4
|
type CliCommand = {
|
|
4
5
|
command: 'export' | 'status';
|
|
5
|
-
|
|
6
|
+
reportId: string;
|
|
7
|
+
options: Omit<ReportExportOptions, 'entry'>;
|
|
6
8
|
} | {
|
|
7
9
|
command: 'check';
|
|
8
10
|
siteDir: string;
|
|
9
11
|
json: boolean;
|
|
12
|
+
} | {
|
|
13
|
+
command: 'convert';
|
|
14
|
+
siteDir: string;
|
|
15
|
+
sourceId: string;
|
|
16
|
+
targetId: string;
|
|
10
17
|
};
|
|
11
18
|
/** 解析命令行;要显示说明时回 undefined。参数不对直接报错 */
|
|
12
19
|
declare function parseCliArgs(args: string[], cwd?: string): CliCommand | undefined;
|
|
13
20
|
interface CliDependencies {
|
|
14
21
|
reportExport?: (options: ReportExportOptions) => Promise<ReportExportResult>;
|
|
15
22
|
checkReports?: (siteDir: string) => Promise<ReportCheckResult>;
|
|
23
|
+
reportEntryIn?: (siteDir: string, reportId: string) => Promise<string>;
|
|
24
|
+
convertReportToMdx?: (siteDir: string, sourceId: string, targetId: string) => Promise<ConvertReportResult>;
|
|
16
25
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
17
26
|
stdout?: (text: string) => void;
|
|
18
27
|
stderr?: (text: string) => void;
|
package/dist/cli.mjs
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { i as checkReports, r as reportExport, t as convertReportToMdx } from "./convert-o7bPWP9A.mjs";
|
|
3
|
+
import { o as reportEntryIn, s as reportEntryOf } from "./report-files-BsIvifZe.mjs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { realpathSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
//#region src/node/cli.ts
|
|
8
8
|
const help = `用法:report-docx <export|status> <报告 id> [选项]
|
|
9
9
|
report-docx check [选项]
|
|
10
|
+
report-docx convert <JSON 报告 id> <新报告 id> [--site <目录>]
|
|
10
11
|
|
|
11
|
-
报告在站点目录的 reports/<报告 id
|
|
12
|
-
export
|
|
13
|
-
status
|
|
14
|
-
check
|
|
12
|
+
报告在站点目录的 reports/<报告 id>/:入口是 content.json(JSON 报告)或 index.mdx(MDX 报告)。
|
|
13
|
+
export 导出这份报告的 Word 与 PDF,等这次导出做完,把结果按 JSON 打出来
|
|
14
|
+
status 只看这份报告的导出状态,不启动 Word
|
|
15
|
+
check 检查站点里全部报告(JSON 报告的每份与每一章、MDX 报告整份)的格式、引用与图片,有问题时退出码非零
|
|
16
|
+
convert 把一份 JSON 报告转成一份新的 MDX 报告,转完编译回来和原报告逐项比对,对不上就删掉新目录、退出码非零;不动原报告
|
|
15
17
|
|
|
16
18
|
选项:
|
|
17
19
|
--site <目录> 站点目录(默认当前目录)
|
|
@@ -43,7 +45,7 @@ function flagValuesOf(flags, allowed) {
|
|
|
43
45
|
}
|
|
44
46
|
/** 解析命令行;要显示说明时回 undefined。参数不对直接报错 */
|
|
45
47
|
function parseCliArgs(args, cwd = process.cwd()) {
|
|
46
|
-
if (args.length === 0) throw new Error("缺少命令:export、status 或
|
|
48
|
+
if (args.length === 0) throw new Error("缺少命令:export、status、check 或 convert;用 --help 查看用法");
|
|
47
49
|
if (args.includes("--help") || args.includes("-h")) return void 0;
|
|
48
50
|
const [command, ...rest] = args;
|
|
49
51
|
if (command === "check") {
|
|
@@ -54,17 +56,30 @@ function parseCliArgs(args, cwd = process.cwd()) {
|
|
|
54
56
|
json: switches.has("--json")
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
|
-
if (command
|
|
59
|
+
if (command === "convert") {
|
|
60
|
+
const [sourceId, targetId, ...flags] = rest;
|
|
61
|
+
if (sourceId === void 0 || targetId === void 0 || sourceId.startsWith("-") || targetId.startsWith("-")) throw new Error("convert 要给两个报告 id:要转的 JSON 报告、新的 MDX 报告");
|
|
62
|
+
reportEntryOf(sourceId);
|
|
63
|
+
reportEntryOf(targetId);
|
|
64
|
+
const { values } = flagValuesOf(flags, ["--site"]);
|
|
65
|
+
return {
|
|
66
|
+
command,
|
|
67
|
+
siteDir: path.resolve(cwd, values.get("--site") ?? "."),
|
|
68
|
+
sourceId,
|
|
69
|
+
targetId
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (command !== "export" && command !== "status") throw new Error(`认不出的命令 ${JSON.stringify(command)}:只有 export、status、check、convert;用 --help 查看用法`);
|
|
58
73
|
const [reportId, ...flags] = rest;
|
|
59
74
|
if (reportId === void 0 || reportId.startsWith("-")) throw new Error(`${command} 要给报告 id(站点 reports/ 下的目录名)`);
|
|
60
|
-
|
|
75
|
+
reportEntryOf(reportId);
|
|
61
76
|
const { values } = flagValuesOf(flags, ["--site", "--engine"]);
|
|
62
77
|
const engine = values.get("--engine");
|
|
63
78
|
return {
|
|
64
79
|
command,
|
|
80
|
+
reportId,
|
|
65
81
|
options: {
|
|
66
82
|
siteDir: path.resolve(cwd, values.get("--site") ?? "."),
|
|
67
|
-
entry,
|
|
68
83
|
mode: "complete",
|
|
69
84
|
action: command === "export" ? "start" : "status",
|
|
70
85
|
...engine !== void 0 ? { engineDir: path.resolve(cwd, engine) } : {}
|
|
@@ -78,6 +93,12 @@ function checkReportText(result) {
|
|
|
78
93
|
if (result.problems.length === 0) lines.push("没有问题。");
|
|
79
94
|
return `${lines.join("\n")}\n`;
|
|
80
95
|
}
|
|
96
|
+
function convertReportText(result) {
|
|
97
|
+
if (result.converted) return `转好了:${result.targetDir}\n${result.files.map((file) => ` ${file}`).join("\n")}\n`;
|
|
98
|
+
const lines = [`没有转换:${result.reason}`];
|
|
99
|
+
for (const problem of result.problems) lines.push(` ${problem}`);
|
|
100
|
+
return `${lines.join("\n")}\n`;
|
|
101
|
+
}
|
|
81
102
|
/** export 要等引擎做完同一个任务才退出:引擎在固定下来的快照上工作,进程退了就没人等它的结果 */
|
|
82
103
|
async function runExport(options, dependencies) {
|
|
83
104
|
let result = await dependencies.reportExport(options);
|
|
@@ -113,7 +134,16 @@ async function main(args, dependencies = {}) {
|
|
|
113
134
|
write(parsed.json ? `${JSON.stringify(result)}\n` : checkReportText(result));
|
|
114
135
|
return result.problems.length > 0 ? 1 : 0;
|
|
115
136
|
}
|
|
116
|
-
|
|
137
|
+
if (parsed.command === "convert") {
|
|
138
|
+
const result = await (dependencies.convertReportToMdx ?? convertReportToMdx)(parsed.siteDir, parsed.sourceId, parsed.targetId);
|
|
139
|
+
write(convertReportText(result));
|
|
140
|
+
return result.converted ? 0 : 1;
|
|
141
|
+
}
|
|
142
|
+
const entry = await (dependencies.reportEntryIn ?? reportEntryIn)(parsed.options.siteDir, parsed.reportId);
|
|
143
|
+
const result = await runExport({
|
|
144
|
+
...parsed.options,
|
|
145
|
+
entry
|
|
146
|
+
}, {
|
|
117
147
|
reportExport: dependencies.reportExport ?? reportExport,
|
|
118
148
|
sleep
|
|
119
149
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
1
|
+
import { a as walk, i as validateDocument, n as safeRelativePath, o as parseReportJson, r as stableValue, s as ReportContentError, t as assembleDocument } from "./assemble-BpllQNVY.mjs";
|
|
2
|
+
import { a as assembleMdxReport, n as mdxReportPlanOf, r as MDX_ENTRY_NAME, s as mdxReportInputsOf } from "./mdx-4Rs1N1nv.mjs";
|
|
3
|
+
import { l as reportsDirName, o as reportEntryIn, r as isMdxEntry, s as reportEntryOf, t as JSON_ENTRY_NAME } from "./report-files-BsIvifZe.mjs";
|
|
4
|
+
import { access, copyFile, mkdir, open, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
7
|
import sharp from "sharp";
|
|
@@ -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
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
/**
|
|
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++;
|
|
@@ -216,7 +272,7 @@ async function checkReports(siteDir) {
|
|
|
216
272
|
//#endregion
|
|
217
273
|
//#region src/node/export.ts
|
|
218
274
|
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
219
|
-
const exists = async (file) => access(file).then(() => true, () => false);
|
|
275
|
+
const exists$1 = async (file) => access(file).then(() => true, () => false);
|
|
220
276
|
async function saved(file) {
|
|
221
277
|
try {
|
|
222
278
|
return JSON.parse(await readFile(file, "utf8"));
|
|
@@ -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("
|
|
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) {
|
|
@@ -268,12 +324,12 @@ async function engineFingerprint(engine, compiler) {
|
|
|
268
324
|
}
|
|
269
325
|
inputs.push(["compiler/package.json", hash(await readFile(path.join(compiler, "package.json")))]);
|
|
270
326
|
await compilerFiles("dist");
|
|
271
|
-
if (await exists(path.join(compiler, "src"))) await compilerFiles("src");
|
|
327
|
+
if (await exists$1(path.join(compiler, "src"))) await compilerFiles("src");
|
|
272
328
|
return hash(JSON.stringify(inputs.sort(([a], [b]) => a.localeCompare(b))));
|
|
273
329
|
}
|
|
274
|
-
async function layoutOf(root, entry) {
|
|
330
|
+
async function layoutOf$1(root, entry) {
|
|
275
331
|
const file = path.join(root, path.dirname(entry), "layout.json");
|
|
276
|
-
if (!await exists(file)) return { file };
|
|
332
|
+
if (!await exists$1(file)) return { file };
|
|
277
333
|
const resolved = await realpath(file), relative = path.relative(await realpath(root), resolved);
|
|
278
334
|
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("布局文件不在站点目录里");
|
|
279
335
|
const bytes = await readFile(resolved);
|
|
@@ -294,7 +350,7 @@ async function capture(site, engine, compiler, entry, mode) {
|
|
|
294
350
|
mode
|
|
295
351
|
});
|
|
296
352
|
rejectMultiImageFigures(report.document);
|
|
297
|
-
const layout = await layoutOf(root, entry);
|
|
353
|
+
const layout = await layoutOf$1(root, entry);
|
|
298
354
|
const engineHash = await engineFingerprint(engine, compiler);
|
|
299
355
|
return {
|
|
300
356
|
report,
|
|
@@ -500,7 +556,7 @@ async function reportExport(options) {
|
|
|
500
556
|
const engine = path.resolve(options.engineDir ?? process.env.DOCREPORT_ENGINE_ROOT ?? packagedEngineDir(compiler));
|
|
501
557
|
const key = keyFor(site, entry, mode), active = await latestAttempt(engine, compiler, key, site), previous = await previousJob(engine, key, site, entry, mode);
|
|
502
558
|
const outputs = await outputPaths(engine, previous);
|
|
503
|
-
if (!await exists(path.join(engine, ".venv/Scripts/python.exe"))) return {
|
|
559
|
+
if (!await exists$1(path.join(engine, ".venv/Scripts/python.exe"))) return {
|
|
504
560
|
state: "unavailable",
|
|
505
561
|
message: `Word 引擎 ${engine} 里还没有 Python 环境:在那里运行 python -m venv .venv 与 .venv\\Scripts\\pip install -r requirements.txt(要本机装着 Word 与 MathType);引擎放在别处时用 --engine 或环境变量 DOCREPORT_ENGINE_ROOT 指过去`,
|
|
506
562
|
...outputs
|
|
@@ -559,4 +615,152 @@ async function reportExport(options) {
|
|
|
559
615
|
};
|
|
560
616
|
}
|
|
561
617
|
//#endregion
|
|
562
|
-
|
|
618
|
+
//#region src/node/convert.ts
|
|
619
|
+
const MAX_LISTED_DIFFERENCES = 20;
|
|
620
|
+
async function exists(file) {
|
|
621
|
+
return stat(file).then(() => true, (error) => {
|
|
622
|
+
if (error.code === "ENOENT") return false;
|
|
623
|
+
throw error;
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
async function layoutOf(reportDir) {
|
|
627
|
+
const file = path.join(reportDir, "layout.json");
|
|
628
|
+
if (!await exists(file)) return void 0;
|
|
629
|
+
const bytes = await readFile(file);
|
|
630
|
+
return {
|
|
631
|
+
bytes,
|
|
632
|
+
keys: Object.keys(JSON.parse(bytes.toString("utf8")))
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
const MARK_ORDER = [
|
|
636
|
+
"strong",
|
|
637
|
+
"em",
|
|
638
|
+
"sub",
|
|
639
|
+
"sup",
|
|
640
|
+
"code"
|
|
641
|
+
];
|
|
642
|
+
/**
|
|
643
|
+
* 比对用的样子:只留写出来的 id(其余由编译器重新生成)、图片路径只看文件名、文档 id 不比、
|
|
644
|
+
* 相邻且标记相同的文字合并、标记按固定顺序、有序列表的起始数 1 不写、没有块的单元格当成一个空段(MDX 的空格子编译成空段,Word 里两者一样)
|
|
645
|
+
*/
|
|
646
|
+
function comparable(document, keptIds) {
|
|
647
|
+
const normalize = (value) => {
|
|
648
|
+
if (Array.isArray(value)) {
|
|
649
|
+
const items = value.map(normalize);
|
|
650
|
+
const merged = [];
|
|
651
|
+
for (const item of items) {
|
|
652
|
+
const previous = merged.at(-1);
|
|
653
|
+
if (item?.["type"] === "text" && previous?.["type"] === "text" && stableValue(previous["marks"] ?? []) === stableValue(item["marks"] ?? [])) previous["text"] = `${previous["text"]}${item["text"]}`;
|
|
654
|
+
else merged.push(item);
|
|
655
|
+
}
|
|
656
|
+
return merged;
|
|
657
|
+
}
|
|
658
|
+
if (!value || typeof value !== "object") return value;
|
|
659
|
+
const copy = {};
|
|
660
|
+
for (const [key, child] of Object.entries(value)) {
|
|
661
|
+
if (key === "id" && !keptIds.has(child)) continue;
|
|
662
|
+
if (key === "src" && typeof child === "string") copy[key] = child.slice(child.lastIndexOf("/") + 1);
|
|
663
|
+
else if (key === "marks" && Array.isArray(child)) copy[key] = MARK_ORDER.filter((mark) => child.includes(mark));
|
|
664
|
+
else copy[key] = normalize(child);
|
|
665
|
+
}
|
|
666
|
+
if (copy["type"] === "list" && copy["start"] === 1) delete copy["start"];
|
|
667
|
+
if (Array.isArray(copy["cells"])) {
|
|
668
|
+
for (const cell of copy["cells"]) if (cell["blocks"].length === 0) cell["blocks"] = [{
|
|
669
|
+
type: "paragraph",
|
|
670
|
+
content: []
|
|
671
|
+
}];
|
|
672
|
+
}
|
|
673
|
+
return copy;
|
|
674
|
+
};
|
|
675
|
+
const { id: _documentId, ...rest } = document;
|
|
676
|
+
return normalize(rest);
|
|
677
|
+
}
|
|
678
|
+
function differencesOf(before, after, pointer = "") {
|
|
679
|
+
if (stableValue(before) === stableValue(after)) return [];
|
|
680
|
+
const bothArrays = Array.isArray(before) && Array.isArray(after);
|
|
681
|
+
const bothObjects = !bothArrays && before && after && typeof before === "object" && typeof after === "object" && !Array.isArray(before) && !Array.isArray(after);
|
|
682
|
+
if (bothArrays && before.length === after.length) return before.flatMap((item, index) => differencesOf(item, after[index], `${pointer}/${index}`));
|
|
683
|
+
if (bothObjects) return [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort().flatMap((key) => differencesOf(before[key], after[key], `${pointer}/${key}`));
|
|
684
|
+
return [`${pointer || "/"}:原报告 ${JSON.stringify(before)},转回来 ${JSON.stringify(after)}`];
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* 把一份 JSON 报告转成一份新的 MDX 报告(reports/<新 id>/),不动原报告。
|
|
688
|
+
* 原报告里有 MDX 写不出的内容就一个文件都不写;写完编译回来和原报告逐项比对,对不上就删掉新目录
|
|
689
|
+
*/
|
|
690
|
+
async function convertReportToMdx(siteDir, sourceId, targetId) {
|
|
691
|
+
const site = await realpath(siteDir);
|
|
692
|
+
const sourceEntry = await reportEntryIn(site, sourceId);
|
|
693
|
+
if (isMdxEntry(sourceEntry)) throw new Error(`报告 ${sourceId} 已经是 MDX 报告`);
|
|
694
|
+
if (!/^[a-z]/.test(targetId)) throw new Error(`新报告 id「${targetId}」要以字母开头:MDX 报告的目录名就是文档 id`);
|
|
695
|
+
const targetDir = path.join(site, path.dirname(reportEntryOf(targetId)));
|
|
696
|
+
if (await exists(targetDir)) throw new Error(`reports/${targetId} 已经存在:convert 只写新目录`);
|
|
697
|
+
const source = await readReport(path.join(site, sourceEntry), {
|
|
698
|
+
root: site,
|
|
699
|
+
mode: "complete"
|
|
700
|
+
});
|
|
701
|
+
const layout = await layoutOf(path.join(site, reportsDirName, sourceId));
|
|
702
|
+
const plan = mdxReportPlanOf(source.document, source.assets, layout?.keys ?? []);
|
|
703
|
+
if (plan.problems.length) return {
|
|
704
|
+
converted: false,
|
|
705
|
+
reason: `原报告里有 ${plan.problems.length} 处 MDX 写不出来,没有写任何文件`,
|
|
706
|
+
problems: plan.problems
|
|
707
|
+
};
|
|
708
|
+
await mkdir(path.join(targetDir, "assets"), { recursive: true });
|
|
709
|
+
const written = [];
|
|
710
|
+
try {
|
|
711
|
+
for (const [name, text] of Object.entries(plan.files)) {
|
|
712
|
+
await writeFile(path.join(targetDir, name), text);
|
|
713
|
+
written.push(name);
|
|
714
|
+
}
|
|
715
|
+
for (const [name, compiledPath] of Object.entries(plan.images)) {
|
|
716
|
+
await copyFile(path.join(site, source.assets[compiledPath]), path.join(targetDir, "assets", name));
|
|
717
|
+
written.push(`assets/${name}`);
|
|
718
|
+
}
|
|
719
|
+
if (layout) {
|
|
720
|
+
await writeFile(path.join(targetDir, "layout.json"), layout.bytes);
|
|
721
|
+
written.push("layout.json");
|
|
722
|
+
}
|
|
723
|
+
let converted;
|
|
724
|
+
try {
|
|
725
|
+
converted = await readReport(path.join(targetDir, "index.mdx"), {
|
|
726
|
+
root: site,
|
|
727
|
+
mode: "complete"
|
|
728
|
+
});
|
|
729
|
+
} catch (error) {
|
|
730
|
+
await rm(targetDir, {
|
|
731
|
+
recursive: true,
|
|
732
|
+
force: true
|
|
733
|
+
});
|
|
734
|
+
return {
|
|
735
|
+
converted: false,
|
|
736
|
+
reason: "转出来的 MDX 编译不过,已删掉新目录",
|
|
737
|
+
problems: [error instanceof Error ? error.message : String(error)]
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
const differences = differencesOf(comparable(source.document, plan.keptIds), comparable(converted.document, plan.keptIds));
|
|
741
|
+
if (differences.length) {
|
|
742
|
+
await rm(targetDir, {
|
|
743
|
+
recursive: true,
|
|
744
|
+
force: true
|
|
745
|
+
});
|
|
746
|
+
return {
|
|
747
|
+
converted: false,
|
|
748
|
+
reason: `转回来和原报告有 ${differences.length} 处对不上,已删掉新目录`,
|
|
749
|
+
problems: differences.slice(0, MAX_LISTED_DIFFERENCES)
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
} catch (error) {
|
|
753
|
+
await rm(targetDir, {
|
|
754
|
+
recursive: true,
|
|
755
|
+
force: true
|
|
756
|
+
});
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
converted: true,
|
|
761
|
+
targetDir,
|
|
762
|
+
files: written.sort()
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
//#endregion
|
|
766
|
+
export { readReport as a, checkReports as i, reportEntry as n, reportExport as r, convertReportToMdx as t };
|
package/dist/core.d.mts
CHANGED
|
@@ -10,4 +10,59 @@ 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
|
-
|
|
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
|
+
//#region src/core/mdx/emit.d.ts
|
|
49
|
+
/**
|
|
50
|
+
* 组装好的 docreport 文档 → 一份 MDX 报告的各个文件:convert 命令用它迁移 JSON 报告。
|
|
51
|
+
* 只有被引用的、layout.json 里出现的、文献与脚注的 id 写出来,其余交给编译器重新生成;
|
|
52
|
+
* MDX 写不出的内容记进 problems(带 JSON 指针),有一条就不该写文件
|
|
53
|
+
*/
|
|
54
|
+
interface MdxReportPlan {
|
|
55
|
+
/** 文件名(相对新报告目录)→ MDX 原文 */
|
|
56
|
+
files: Record<string, string>;
|
|
57
|
+
/** 新报告里的图片文件名(assets/ 下)→ 原图在组装结果里的路径(AssemblyResult.assets 的键) */
|
|
58
|
+
images: Record<string, string>;
|
|
59
|
+
/** 写出来的手写 id:比对时只有它们要求两边一样 */
|
|
60
|
+
keptIds: Set<string>;
|
|
61
|
+
problems: string[];
|
|
62
|
+
}
|
|
63
|
+
/** 哪些 id 要写出来:被 ref 指到的、layout.json 的键、文献与脚注(它们本来就靠 id 对应) */
|
|
64
|
+
declare function keptIdsOf(document: ReportDocument, layoutKeys: string[]): Set<string>;
|
|
65
|
+
/** 组装好的文档 → MDX 报告。assets 是组装结果的 assets(图在组装结果里的路径 → 原图路径) */
|
|
66
|
+
declare function mdxReportPlanOf(document: ReportDocument, assets: Record<string, string>, layoutKeys: string[]): MdxReportPlan;
|
|
67
|
+
//#endregion
|
|
68
|
+
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, type MdxReportPlan, MultiImageFigure, Paragraph, ReportBlock, ReportContentError, ReportDocument, ReportIndex, ReportInline, ReportList, Section, SingleImageFigure, SourceLocation, Table, TableCell, assembleDocument, assembleMdxReport, compileMdxReport, keptIdsOf, mdxReportInputsOf, mdxReportPlanOf, parseReportJson, safeRelativePath, validateDocument };
|
package/dist/core.mjs
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
import { i as validateDocument, n as safeRelativePath, o as parseReportJson, s as ReportContentError, t as assembleDocument } from "./assemble-BpllQNVY.mjs";
|
|
2
|
+
import { a as assembleMdxReport, c as GENERATED_ID_PREFIX, i as MDX_REFERENCES_NAME, n as mdxReportPlanOf, o as compileMdxReport, r as MDX_ENTRY_NAME, s as mdxReportInputsOf, t as keptIdsOf } from "./mdx-4Rs1N1nv.mjs";
|
|
3
|
+
export { GENERATED_ID_PREFIX, MDX_ENTRY_NAME, MDX_REFERENCES_NAME, ReportContentError, assembleDocument, assembleMdxReport, compileMdxReport, keptIdsOf, mdxReportInputsOf, mdxReportPlanOf, parseReportJson, safeRelativePath, validateDocument };
|
|
@@ -20,9 +20,28 @@ interface ReportCheckResult {
|
|
|
20
20
|
mode: 'standalone' | 'complete';
|
|
21
21
|
})[];
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* 检查站点 reports/ 下的每个 content.json(每份报告与每一章)与每份 MDX 报告(报告目录根上的 index.mdx,只整份检查:MDX 的章不是独立文档)。
|
|
25
|
+
* 只用本包的代码,不执行站点里的脚本
|
|
26
|
+
*/
|
|
24
27
|
declare function checkReports(siteDir: string): Promise<ReportCheckResult>;
|
|
25
28
|
//#endregion
|
|
29
|
+
//#region src/node/convert.d.ts
|
|
30
|
+
type ConvertReportResult = {
|
|
31
|
+
converted: true;
|
|
32
|
+
targetDir: string;
|
|
33
|
+
files: string[];
|
|
34
|
+
} | {
|
|
35
|
+
converted: false;
|
|
36
|
+
reason: string;
|
|
37
|
+
problems: string[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* 把一份 JSON 报告转成一份新的 MDX 报告(reports/<新 id>/),不动原报告。
|
|
41
|
+
* 原报告里有 MDX 写不出的内容就一个文件都不写;写完编译回来和原报告逐项比对,对不上就删掉新目录
|
|
42
|
+
*/
|
|
43
|
+
declare function convertReportToMdx(siteDir: string, sourceId: string, targetId: string): Promise<ConvertReportResult>;
|
|
44
|
+
//#endregion
|
|
26
45
|
//#region src/node/export.d.ts
|
|
27
46
|
type Mode = 'complete' | 'standalone';
|
|
28
47
|
type Job = {
|
|
@@ -38,7 +57,7 @@ type Job = {
|
|
|
38
57
|
};
|
|
39
58
|
interface ReportExportOptions {
|
|
40
59
|
siteDir: string;
|
|
41
|
-
/**
|
|
60
|
+
/** 相对站点目录的入口路径:reports/<报告 id>/content.json 或 reports/<报告 id>/index.mdx(reportEntryIn) */
|
|
42
61
|
entry: string;
|
|
43
62
|
mode?: Mode;
|
|
44
63
|
action?: 'start' | 'status';
|
|
@@ -64,4 +83,4 @@ declare function reportEntry(entry: unknown): string;
|
|
|
64
83
|
/** Package API: callers can start or inspect a render without a website or workbench process. */
|
|
65
84
|
declare function reportExport(options: ReportExportOptions): Promise<ReportExportResult>;
|
|
66
85
|
//#endregion
|
|
67
|
-
export {
|
|
86
|
+
export { ConvertReportResult as a, ReportCheckResult as c, reportExport as i, checkReports as l, ReportExportResult as n, convertReportToMdx as o, reportEntry as r, ReportCheckDiagnostic as s, ReportExportOptions as t };
|