@godcreator/word-report 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{assemble-D25NLmLx.mjs → assemble-BpllQNVY.mjs} +1 -1
- package/dist/cli.d.mts +11 -2
- package/dist/cli.mjs +41 -11
- package/dist/{export-DJQiBPPU.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.d.mts +7 -3
- package/dist/site-server.mjs +97 -21
- package/dist/site.mjs +1 -1
- package/package.json +10 -3
- package/dist/report-files-CgWzLxH-.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.d.mts
CHANGED
|
@@ -10,15 +10,19 @@ type WordReportSite = {
|
|
|
10
10
|
hrefBase: string;
|
|
11
11
|
pageComments: (page: string) => ReportPageComments;
|
|
12
12
|
};
|
|
13
|
-
/** loader 用到的路由参数:报告页是 :report,图片再加
|
|
13
|
+
/** loader 用到的路由参数:报告页是 :report,图片再加 *;图片的 loader 收到 request 时按 If-None-Match 回 304 */
|
|
14
14
|
type ReportRouteArgs = {
|
|
15
15
|
params: Partial<Record<string, string>>;
|
|
16
|
+
request?: Request;
|
|
16
17
|
};
|
|
17
18
|
/** 报告列表:站点 reports/ 下的每份报告各一行;读不对的报告也列出来,带上原话 */
|
|
18
19
|
declare function reportListLoader(_args: ReportRouteArgs, site: WordReportSite): Promise<ReportListData>;
|
|
19
20
|
/** 报告页:整份报告组装成一页(各章按 include 的顺序接在一起);报告读不对直接报错,原话在错误页上 */
|
|
20
21
|
declare function reportPageLoader({ params }: ReportRouteArgs, site: WordReportSite): Promise<ReportPageData>;
|
|
21
|
-
/**
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* 报告里的一张图:地址是组装后的路径 assets/<文档 id>/<文件名>,只给这份报告用到的图。
|
|
24
|
+
* 浏览器每次都来问(no-cache),ETag 由文件大小与修改时间算出,没变就回 304、不再传图
|
|
25
|
+
*/
|
|
26
|
+
declare function reportAssetLoader({ params, request }: ReportRouteArgs, site: WordReportSite): Promise<Response>;
|
|
23
27
|
//#endregion
|
|
24
28
|
export { type ReportListData, type ReportPageComments, type ReportPageData, ReportRouteArgs, type ReportSummary, type ReportTocItem, WordReportSite, reportAssetLoader, reportListLoader, reportPageLoader };
|
package/dist/site-server.mjs
CHANGED
|
@@ -1,18 +1,85 @@
|
|
|
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";
|
|
5
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
4
6
|
import path from "node:path";
|
|
5
7
|
import { createReadStream } from "node:fs";
|
|
6
8
|
import { Readable } from "node:stream";
|
|
7
9
|
//#region src/site/loaders.server.ts
|
|
8
10
|
/**
|
|
9
|
-
* `@godcreator/word-report/site/server`:报告列表、报告页与报告里图片的 loader
|
|
11
|
+
* `@godcreator/word-report/site/server`:报告列表、报告页与报告里图片的 loader。报告从站点 reports/ 现读,
|
|
12
|
+
* 原文没变时沿用上一次的组装结果。
|
|
10
13
|
*/
|
|
11
14
|
/** 组装结果里有 Object.create(null) 的对象:换成普通对象再交给 React Router 序列化 */
|
|
12
15
|
const plainOf = (result) => JSON.parse(JSON.stringify(result));
|
|
16
|
+
/**
|
|
17
|
+
* 每份报告最近一次组装,键是站点目录加报告 id。一页报告的几十张图同时请求,组装一次要几百毫秒到几秒、
|
|
18
|
+
* 全在一个线程上排队:不共用的话,最后一张图要等几十次组装
|
|
19
|
+
*/
|
|
20
|
+
const latestAssemblies = /* @__PURE__ */ new Map();
|
|
21
|
+
async function assembleFromDisk(siteDir, 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
|
+
}
|
|
38
|
+
const files = await readReportFiles(siteDir, entry);
|
|
39
|
+
const entryDocument = JSON.parse(files[entry]);
|
|
40
|
+
const result = assembleDocument(entry, files, { mode: "complete" });
|
|
41
|
+
return {
|
|
42
|
+
files,
|
|
43
|
+
absentFiles: [`${reportDir}/${MDX_ENTRY_NAME}`],
|
|
44
|
+
result,
|
|
45
|
+
chapterCount: entryDocument.blocks.filter((block) => block.type === "include").length
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** 上次组装读过的每个文件原文都没变、当时不存在的文件仍不存在;读过的文件没了也算变了,交给重新组装去报错 */
|
|
49
|
+
async function unchangedOnDisk(siteDir, assembly) {
|
|
50
|
+
const root = await realpath(siteDir);
|
|
51
|
+
const textOrMissing = async (relative) => {
|
|
52
|
+
try {
|
|
53
|
+
return await readFile(path.join(root, relative), "utf8");
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.code === "ENOENT") return null;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
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);
|
|
61
|
+
}
|
|
62
|
+
/** 报告的整份组装:原文和上次一样就用上次的,同时到的请求共用同一次组装;结果只读,要改先复制 */
|
|
63
|
+
async function latestAssembly(siteDir, reportId) {
|
|
64
|
+
const key = `${path.resolve(siteDir)}\0${reportId}`;
|
|
65
|
+
const latest = latestAssemblies.get(key);
|
|
66
|
+
if (latest !== void 0) {
|
|
67
|
+
const assembly = await latest;
|
|
68
|
+
if (await unchangedOnDisk(siteDir, assembly)) return assembly;
|
|
69
|
+
}
|
|
70
|
+
const newer = latestAssemblies.get(key);
|
|
71
|
+
if (newer !== void 0 && newer !== latest) return newer;
|
|
72
|
+
const fresh = assembleFromDisk(siteDir, reportId);
|
|
73
|
+
latestAssemblies.set(key, fresh);
|
|
74
|
+
try {
|
|
75
|
+
return await fresh;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (latestAssemblies.get(key) === fresh) latestAssemblies.delete(key);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
13
81
|
async function assembledReport(siteDir, reportId) {
|
|
14
|
-
|
|
15
|
-
return assembleDocument(entry, await readReportFiles(siteDir, entry), { mode: "complete" });
|
|
82
|
+
return (await latestAssembly(siteDir, reportId)).result;
|
|
16
83
|
}
|
|
17
84
|
/** 地址里的报告 id:形状不对或站点里没有这份报告都是 404 */
|
|
18
85
|
async function reportIdOf(params, siteDir) {
|
|
@@ -20,31 +87,26 @@ async function reportIdOf(params, siteDir) {
|
|
|
20
87
|
if (!REPORT_ID_PATTERN.test(reportId) || !(await reportIdsOf(siteDir)).includes(reportId)) throw new Response(`站点里没有报告 ${reportId}`, { status: 404 });
|
|
21
88
|
return reportId;
|
|
22
89
|
}
|
|
23
|
-
function summaryOf(reportId,
|
|
24
|
-
const metadata = result
|
|
25
|
-
const entryDocument = files === null ? null : JSON.parse(files[reportEntryOf(reportId)]);
|
|
90
|
+
function summaryOf(reportId, read, problem) {
|
|
91
|
+
const metadata = read?.result.document.metadata ?? {};
|
|
26
92
|
return {
|
|
27
93
|
id: reportId,
|
|
28
94
|
title: metadata.title ?? reportId,
|
|
29
95
|
revision: metadata.revision ?? null,
|
|
30
96
|
date: metadata.date ?? null,
|
|
31
97
|
authors: metadata.authors ?? [],
|
|
32
|
-
chapterCount:
|
|
33
|
-
warningCount: result
|
|
98
|
+
chapterCount: read?.chapterCount ?? 0,
|
|
99
|
+
warningCount: read?.result.warnings.length ?? 0,
|
|
34
100
|
problem
|
|
35
101
|
};
|
|
36
102
|
}
|
|
37
103
|
/** 报告列表:站点 reports/ 下的每份报告各一行;读不对的报告也列出来,带上原话 */
|
|
38
104
|
async function reportListLoader(_args, site) {
|
|
39
105
|
const reports = [];
|
|
40
|
-
for (const reportId of await reportIdsOf(site.siteDir)) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
reports.push(summaryOf(reportId, files, assembleDocument(reportEntryOf(reportId), files, { mode: "complete" }), null));
|
|
45
|
-
} catch (error) {
|
|
46
|
-
reports.push(summaryOf(reportId, null, null, error instanceof Error ? error.message : String(error)));
|
|
47
|
-
}
|
|
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)));
|
|
48
110
|
}
|
|
49
111
|
return {
|
|
50
112
|
siteName: site.siteName,
|
|
@@ -88,15 +150,29 @@ const imageContentTypes = {
|
|
|
88
150
|
".jpg": "image/jpeg",
|
|
89
151
|
".jpeg": "image/jpeg"
|
|
90
152
|
};
|
|
91
|
-
/**
|
|
92
|
-
|
|
153
|
+
/**
|
|
154
|
+
* 报告里的一张图:地址是组装后的路径 assets/<文档 id>/<文件名>,只给这份报告用到的图。
|
|
155
|
+
* 浏览器每次都来问(no-cache),ETag 由文件大小与修改时间算出,没变就回 304、不再传图
|
|
156
|
+
*/
|
|
157
|
+
async function reportAssetLoader({ params, request }, site) {
|
|
93
158
|
const reportId = await reportIdOf(params, site.siteDir);
|
|
94
159
|
const compiledPath = `assets/${params["*"] ?? ""}`;
|
|
95
160
|
const source = (await assembledReport(site.siteDir, reportId)).assets[compiledPath];
|
|
96
161
|
if (source === void 0) return new Response(`报告 ${reportId} 里没有图 ${compiledPath}`, { status: 404 });
|
|
97
162
|
const contentType = imageContentTypes[path.extname(source).toLowerCase()];
|
|
98
163
|
if (contentType === void 0) throw new Error(`报告 ${reportId} 的图 ${source} 不是 png 或 jpg`);
|
|
99
|
-
|
|
164
|
+
const file = path.join(site.siteDir, source);
|
|
165
|
+
const { size, mtimeMs } = await stat(file);
|
|
166
|
+
const headers = {
|
|
167
|
+
"content-type": contentType,
|
|
168
|
+
"cache-control": "no-cache",
|
|
169
|
+
etag: `"${size.toString(16)}-${Math.trunc(mtimeMs).toString(16)}"`
|
|
170
|
+
};
|
|
171
|
+
if (request?.headers.get("if-none-match") === headers.etag) return new Response(null, {
|
|
172
|
+
status: 304,
|
|
173
|
+
headers
|
|
174
|
+
});
|
|
175
|
+
return new Response(Readable.toWeb(createReadStream(file)), { headers });
|
|
100
176
|
}
|
|
101
177
|
//#endregion
|
|
102
178
|
export { reportAssetLoader, reportListLoader, reportPageLoader };
|
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";
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godcreator/word-report",
|
|
3
|
-
"version": "0.4.
|
|
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-D25NLmLx.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 };
|