@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/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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import { a as
|
|
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 };
|
package/dist/site-server.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { t as assembleDocument } from "./assemble-
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
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 =
|
|
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
|
-
|
|
43
|
+
absentFiles: [`${reportDir}/${MDX_ENTRY_NAME}`],
|
|
44
|
+
result,
|
|
45
|
+
chapterCount: entryDocument.blocks.filter((block) => block.type === "include").length
|
|
26
46
|
};
|
|
27
47
|
}
|
|
28
|
-
/**
|
|
29
|
-
async function
|
|
48
|
+
/** 上次组装读过的每个文件原文都没变、当时不存在的文件仍不存在;读过的文件没了也算变了,交给重新组装去报错 */
|
|
49
|
+
async function unchangedOnDisk(siteDir, assembly) {
|
|
30
50
|
const root = await realpath(siteDir);
|
|
31
|
-
|
|
51
|
+
const textOrMissing = async (relative) => {
|
|
32
52
|
try {
|
|
33
|
-
return await readFile(path.join(root, relative), "utf8")
|
|
53
|
+
return await readFile(path.join(root, relative), "utf8");
|
|
34
54
|
} catch (error) {
|
|
35
|
-
if (error.code === "ENOENT") return
|
|
55
|
+
if (error.code === "ENOENT") return null;
|
|
36
56
|
throw error;
|
|
37
57
|
}
|
|
38
|
-
}
|
|
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
|
-
/**
|
|
41
|
-
async function
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
66
|
-
const metadata = result
|
|
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:
|
|
75
|
-
warningCount: result
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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-
|
|
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.
|
|
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
|
-
|
|
104
|
-
|
|
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.
|
|
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.
|
|
436
|
-
suffix =
|
|
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
|
+
"version": "0.4.1",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "godcreator"
|
|
6
6
|
},
|
|
7
7
|
"type": "module",
|
|
8
|
-
"description": "报告:JSON
|
|
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
|
-
"
|
|
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 };
|