@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/dist/node.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as AssemblyOptions, r as AssemblyResult } from "./types-Bcl48xQN.mjs";
2
- import { a as ReportCheckDiagnostic, i as reportExport, n as ReportExportResult, o as ReportCheckResult, r as reportEntry, s as checkReports, t as ReportExportOptions } from "./export-DC764Umz.mjs";
2
+ import { a as ConvertReportResult, c as ReportCheckResult, i as reportExport, l as checkReports, n as ReportExportResult, o as convertReportToMdx, r as reportEntry, s as ReportCheckDiagnostic, t as ReportExportOptions } from "./export-Da4yhrZa.mjs";
3
3
  //#region src/node/read-report.d.ts
4
4
  interface SnapshotEntry {
5
5
  path: string;
@@ -18,18 +18,24 @@ declare function readReport(entryAbsolute: string, options?: AssemblyOptions & {
18
18
  }): Promise<ReadReportResult>;
19
19
  //#endregion
20
20
  //#region src/node/report-files.d.ts
21
- /** 站点里的报告:站点根下 reports/<报告 id>/content.json,章节与图片在同一个报告目录里 */
21
+ /** 站点里的报告:站点根下 reports/<报告 id>/,入口是 content.json(JSON 报告)或 index.mdx(MDX 报告),两种只能有一种 */
22
22
  declare const reportsDirName = "reports";
23
+ declare const JSON_ENTRY_NAME = "content.json";
23
24
  /** 报告 id 就是报告目录名,出现在网址里:小写字母、数字与 -,以字母或数字开头 */
24
25
  declare const REPORT_ID_PATTERN: RegExp;
25
- /** 报告的入口,相对站点目录 */
26
+ /** JSON 报告的入口,相对站点目录;报告是哪种格式要看磁盘,用 reportEntryIn */
26
27
  declare function reportEntryOf(reportId: string): string;
27
- /** 站点里的报告 id:reports/ 下有 content.json 的目录,按名字排;目录名不合 REPORT_ID_PATTERN 的直接报错,不悄悄跳过 */
28
+ declare const isMdxEntry: (entry: string) => boolean;
29
+ /** 一份报告的入口,相对站点目录:reports/<id>/content.json 或 reports/<id>/index.mdx */
30
+ declare function reportEntryIn(siteDir: string, reportId: string): Promise<string>;
31
+ /** 站点里的报告 id:reports/ 下有入口文件的目录,按名字排;目录名不合 REPORT_ID_PATTERN 的直接报错,不悄悄跳过 */
28
32
  declare function reportIdsOf(siteDir: string): Promise<string[]>;
29
33
  /**
30
34
  * 读一份报告组装要的全部 JSON 文件(入口与它 include 到的章节),键是相对站点目录的路径,交给 assembleDocument。
31
35
  * 不读图片、不解码:网页显示用;检查与导出用 readReport,它还核对图片与快照
32
36
  */
33
37
  declare function readReportFiles(siteDir: string, entry: string): Promise<Record<string, string>>;
38
+ /** 读一份 MDX 报告的全部原文:入口、chapters 列的各章、存在时的 references.mdx,键是相对站点目录的路径,交给 assembleMdxReport */
39
+ declare function readMdxReportFiles(siteDir: string, entry: string): Promise<Record<string, string>>;
34
40
  //#endregion
35
- export { REPORT_ID_PATTERN, type ReadReportResult, type ReportCheckDiagnostic, type ReportCheckResult, type ReportExportOptions, type ReportExportResult, type SnapshotEntry, checkReports, readReport, readReportFiles, reportEntry, reportEntryOf, reportExport, reportIdsOf, reportsDirName };
41
+ export { type ConvertReportResult, JSON_ENTRY_NAME, REPORT_ID_PATTERN, type ReadReportResult, type ReportCheckDiagnostic, type ReportCheckResult, type ReportExportOptions, type ReportExportResult, type SnapshotEntry, checkReports, convertReportToMdx, isMdxEntry, readMdxReportFiles, readReport, readReportFiles, reportEntry, reportEntryIn, reportEntryOf, reportExport, reportIdsOf, reportsDirName };
package/dist/node.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { i as readReport, n as reportExport, r as checkReports, t as reportEntry } from "./export-DJQiBPPU.mjs";
2
- import { a as reportsDirName, i as reportIdsOf, n as readReportFiles, r as reportEntryOf, t as REPORT_ID_PATTERN } from "./report-files-CgWzLxH-.mjs";
3
- export { REPORT_ID_PATTERN, checkReports, readReport, readReportFiles, reportEntry, reportEntryOf, reportExport, reportIdsOf, reportsDirName };
1
+ import { a as readReport, i as checkReports, n as reportEntry, r as reportExport, t as convertReportToMdx } from "./convert-o7bPWP9A.mjs";
2
+ import { a as readReportFiles, c as reportIdsOf, i as readMdxReportFiles, l as reportsDirName, n as REPORT_ID_PATTERN, o as reportEntryIn, r as isMdxEntry, s as reportEntryOf, t as JSON_ENTRY_NAME } from "./report-files-BsIvifZe.mjs";
3
+ export { JSON_ENTRY_NAME, REPORT_ID_PATTERN, checkReports, convertReportToMdx, isMdxEntry, readMdxReportFiles, readReport, readReportFiles, reportEntry, reportEntryIn, reportEntryOf, reportExport, reportIdsOf, reportsDirName };
@@ -0,0 +1,103 @@
1
+ import { i as validateDocument, n as safeRelativePath, o as parseReportJson, s as ReportContentError } from "./assemble-BpllQNVY.mjs";
2
+ import { r as MDX_ENTRY_NAME, s as mdxReportInputsOf } from "./mdx-4Rs1N1nv.mjs";
3
+ import { readFile, readdir, realpath, stat } from "node:fs/promises";
4
+ import path from "node:path";
5
+ //#region src/node/report-files.ts
6
+ /** 站点里的报告:站点根下 reports/<报告 id>/,入口是 content.json(JSON 报告)或 index.mdx(MDX 报告),两种只能有一种 */
7
+ const reportsDirName = "reports";
8
+ const JSON_ENTRY_NAME = "content.json";
9
+ /** 报告 id 就是报告目录名,出现在网址里:小写字母、数字与 -,以字母或数字开头 */
10
+ const REPORT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
11
+ /** JSON 报告的入口,相对站点目录;报告是哪种格式要看磁盘,用 reportEntryIn */
12
+ function reportEntryOf(reportId) {
13
+ if (!REPORT_ID_PATTERN.test(reportId)) throw new ReportContentError(`报告 id 只能是小写字母、数字与 -,以字母或数字开头:${JSON.stringify(reportId)}`);
14
+ return `${reportsDirName}/${reportId}/${JSON_ENTRY_NAME}`;
15
+ }
16
+ const isMdxEntry = (entry) => entry === "index.mdx" || entry.endsWith(`/index.mdx`);
17
+ async function isFile(file) {
18
+ return stat(file).then((info) => info.isFile(), (error) => {
19
+ if (error.code === "ENOENT") return false;
20
+ throw error;
21
+ });
22
+ }
23
+ /** 报告目录里有哪种入口:两种都有直接报错,都没有回 undefined */
24
+ async function entryNameIn(reportDir, reportId) {
25
+ const [hasJson, hasMdx] = await Promise.all([isFile(path.join(reportDir, JSON_ENTRY_NAME)), isFile(path.join(reportDir, MDX_ENTRY_NAME))]);
26
+ if (hasJson && hasMdx) throw new ReportContentError(`报告目录里 ${JSON_ENTRY_NAME} 和 ${MDX_ENTRY_NAME} 只能留一个`, `${reportsDirName}/${reportId}`);
27
+ return hasJson ? JSON_ENTRY_NAME : hasMdx ? MDX_ENTRY_NAME : void 0;
28
+ }
29
+ /** 一份报告的入口,相对站点目录:reports/<id>/content.json 或 reports/<id>/index.mdx */
30
+ async function reportEntryIn(siteDir, reportId) {
31
+ const jsonEntry = reportEntryOf(reportId);
32
+ const name = await entryNameIn(path.join(siteDir, path.dirname(jsonEntry)), reportId);
33
+ if (name === void 0) throw new ReportContentError(`站点里没有报告 ${reportId}:reports/${reportId}/ 下既没有 ${JSON_ENTRY_NAME} 也没有 ${MDX_ENTRY_NAME}`, siteDir);
34
+ return `${reportsDirName}/${reportId}/${name}`;
35
+ }
36
+ /** 站点里的报告 id:reports/ 下有入口文件的目录,按名字排;目录名不合 REPORT_ID_PATTERN 的直接报错,不悄悄跳过 */
37
+ async function reportIdsOf(siteDir) {
38
+ const reportsDir = path.join(siteDir, reportsDirName);
39
+ let entries;
40
+ try {
41
+ entries = await readdir(reportsDir, { withFileTypes: true });
42
+ } catch (error) {
43
+ if (error.code === "ENOENT") return [];
44
+ throw error;
45
+ }
46
+ const ids = [];
47
+ for (const entry of entries) {
48
+ if (!entry.isDirectory()) continue;
49
+ if (await entryNameIn(path.join(reportsDir, entry.name), entry.name) === void 0) continue;
50
+ reportEntryOf(entry.name);
51
+ ids.push(entry.name);
52
+ }
53
+ return ids.sort();
54
+ }
55
+ /** 读站点目录里的一个文件(相对路径),挡住经符号链接跑出站点目录的 */
56
+ async function readInsideSite(root, relative) {
57
+ let real;
58
+ try {
59
+ real = await realpath(path.join(root, relative));
60
+ } catch {
61
+ throw new ReportContentError("Missing or unreadable file", relative);
62
+ }
63
+ const inside = path.relative(root, real);
64
+ if (inside === ".." || inside.startsWith(".." + path.sep) || path.isAbsolute(inside)) throw new ReportContentError("File escapes report root", relative);
65
+ return readFile(real, "utf8");
66
+ }
67
+ /**
68
+ * 读一份报告组装要的全部 JSON 文件(入口与它 include 到的章节),键是相对站点目录的路径,交给 assembleDocument。
69
+ * 不读图片、不解码:网页显示用;检查与导出用 readReport,它还核对图片与快照
70
+ */
71
+ async function readReportFiles(siteDir, entry) {
72
+ const root = await realpath(siteDir);
73
+ const files = {};
74
+ const load = async (relative) => {
75
+ if (Object.hasOwn(files, relative)) return;
76
+ const text = await readInsideSite(root, relative);
77
+ files[relative] = text;
78
+ const document = parseReportJson(text, relative);
79
+ validateDocument(document, relative);
80
+ for (const [index, block] of document.blocks.entries()) {
81
+ if (block.type !== "include") continue;
82
+ try {
83
+ safeRelativePath(block.src);
84
+ } catch (error) {
85
+ throw new ReportContentError(error.message, relative, `/blocks/${index}/src`);
86
+ }
87
+ await load(path.posix.join(path.posix.dirname(relative), block.src));
88
+ }
89
+ };
90
+ await load(entry);
91
+ return files;
92
+ }
93
+ /** 读一份 MDX 报告的全部原文:入口、chapters 列的各章、存在时的 references.mdx,键是相对站点目录的路径,交给 assembleMdxReport */
94
+ async function readMdxReportFiles(siteDir, entry) {
95
+ const root = await realpath(siteDir);
96
+ const files = { [entry]: await readInsideSite(root, entry) };
97
+ const { chapters, references } = mdxReportInputsOf(files[entry], entry);
98
+ for (const chapter of chapters) files[chapter] = await readInsideSite(root, chapter);
99
+ if (await isFile(path.join(root, references))) files[references] = await readInsideSite(root, references);
100
+ return files;
101
+ }
102
+ //#endregion
103
+ export { readReportFiles as a, reportIdsOf as c, readMdxReportFiles as i, reportsDirName as l, REPORT_ID_PATTERN as n, reportEntryIn as o, isMdxEntry as r, reportEntryOf as s, JSON_ENTRY_NAME as t };
@@ -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
- /** 报告里的一张图:地址是组装后的路径 assets/<文档 id>/<文件名>,只给这份报告用到的图 */
22
- declare function reportAssetLoader({ params }: ReportRouteArgs, site: WordReportSite): Promise<Response>;
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 };
@@ -1,18 +1,85 @@
1
- import { t as assembleDocument } from "./assemble-D25NLmLx.mjs";
2
- import { i as reportIdsOf, n as readReportFiles, r as reportEntryOf, t as REPORT_ID_PATTERN } from "./report-files-CgWzLxH-.mjs";
1
+ import { t as assembleDocument } from "./assemble-BpllQNVY.mjs";
2
+ import { a as assembleMdxReport, r as MDX_ENTRY_NAME, s as mdxReportInputsOf } from "./mdx-4Rs1N1nv.mjs";
3
+ import { a as readReportFiles, c as reportIdsOf, i as readMdxReportFiles, n as REPORT_ID_PATTERN, o as reportEntryIn, r as isMdxEntry, t as JSON_ENTRY_NAME } from "./report-files-BsIvifZe.mjs";
3
4
  import { n as reportHrefOf } from "./page-data-DLPoVgXy.mjs";
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,报告每次从站点 reports/ 现读、现组装。
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
- const entry = reportEntryOf(reportId);
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, files, result, problem) {
24
- const metadata = result?.document.metadata ?? {};
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: entryDocument?.blocks.filter((block) => block.type === "include").length ?? 0,
33
- warningCount: result?.warnings.length ?? 0,
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
- let files = null;
42
- try {
43
- files = await readReportFiles(site.siteDir, reportEntryOf(reportId));
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
- /** 报告里的一张图:地址是组装后的路径 assets/<文档 id>/<文件名>,只给这份报告用到的图 */
92
- async function reportAssetLoader({ params }, site) {
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
- return new Response(Readable.toWeb(createReadStream(path.join(site.siteDir, source))), { headers: { "content-type": contentType } });
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-D25NLmLx.mjs";
1
+ import { t as assembleDocument } from "./assemble-BpllQNVY.mjs";
2
2
  import { n as reportHrefOf, t as reportAssetHrefOf } from "./page-data-DLPoVgXy.mjs";
3
3
  import { Link, useLoaderData } from "react-router";
4
4
  import { DocsBody, DocsPage, DocsTitle } from "@fumadocs/base-ui/layouts/docs/page";
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@godcreator/word-report",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "author": {
5
5
  "name": "godcreator"
6
6
  },
7
7
  "type": "module",
8
- "description": "报告:JSON 报告文档的格式、校验与分章组装(.);读报告目录、检查、导出 Word 的命令行 report-docx(./node);报告页与报告列表(./site),两页与图片的 loader(./site/server);engine/ 是驱动本机 Word 的 Python 导出引擎与 .dotx 模板;template/ 是建站器复制进站点 app/features/word-report/ 的接线文件(两条页面路由、图片路由、侧栏入口、样式)。",
8
+ "description": "报告:JSON 报告文档的格式、校验与分章组装,MDX 写法编译成它(.);读报告目录、检查、导出 Word、JSON 报告转 MDX 的命令行 report-docx(./node);报告页与报告列表(./site),两页与图片的 loader(./site/server);engine/ 是驱动本机 Word 的 Python 导出引擎与 .dotx 模板;template/ 是建站器复制进站点 app/features/word-report/ 的接线文件(两条页面路由、图片路由、侧栏入口、样式)。",
9
9
  "license": "MIT",
10
10
  "exports": {
11
11
  ".": {
@@ -43,7 +43,14 @@
43
43
  ],
44
44
  "dependencies": {
45
45
  "@godcreator/comments": "^0.1.3",
46
- "sharp": "^0.35.5"
46
+ "remark-frontmatter": "^5.0.0",
47
+ "remark-gfm": "^4.0.1",
48
+ "remark-math": "^6.0.0",
49
+ "remark-mdx": "^3.1.0",
50
+ "remark-parse": "^11.0.0",
51
+ "sharp": "^0.35.5",
52
+ "unified": "^11.0.5",
53
+ "yaml": "^2.9.1"
47
54
  },
48
55
  "peerDependencies": {
49
56
  "@fumadocs/base-ui": "^16.17.1",
@@ -1,72 +0,0 @@
1
- import { a as parseReportJson, n as safeRelativePath, o as ReportContentError, r as validateDocument } from "./assemble-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 };