feat: 完成 DOCX 通用结构映射与严格收口

This commit is contained in:
SkyJourney
2026-07-31 15:19:34 +08:00
parent 6e40374200
commit f3847f3e4b
31 changed files with 3175 additions and 64 deletions
+38 -3
View File
@@ -1,3 +1,6 @@
import {
randomUUID
} from "node:crypto";
import {
mkdir,
mkdtemp,
@@ -15,6 +18,7 @@ import {
type ExportConfig,
type MarkdownDocumentMetadata,
type PreparedDocxMedia,
type SemanticDocumentModel,
type ThemeManifest
} from "@md-to-pdf/core";
import type { DocxThemeTokenSet } from "@md-to-pdf/docx-theme-engine";
@@ -23,8 +27,10 @@ import {
createDynamicReferenceDocx,
type DynamicReferenceDocxResult
} from "./reference-builder.js";
import { finalizeGeneratedDocxStructure } from "./document-structure-transform.js";
import type { DynamicReferenceDocxOptions } from "./reference-options.js";
import { preparePandocMedia } from "./pandoc-media.js";
import { createPandocStructurePlan } from "./pandoc-structure.js";
import {
runPandocProcess,
type PandocProcessRunner
@@ -42,6 +48,8 @@ const MAXIMUM_PANDOC_STDERR_BYTES = 64 * 1024;
const DEFAULT_REFERENCE_CACHE_SIZE = 32;
const temporaryDirectoryPrefix = "md-to-pdf-docx-";
const mediaMapEnvironmentName = "MD_TO_PDF_DOCX_MEDIA_MAP";
const structurePlanEnvironmentName =
"MD_TO_PDF_DOCX_STRUCTURE_PLAN";
const luaFilterUrl = new URL(
"../assets/docx-media-filter.lua",
import.meta.url
@@ -60,6 +68,7 @@ export interface PandocDocxConversionInput {
theme: ThemeManifest;
themeTokens: DocxThemeTokenSet;
metadata: MarkdownDocumentMetadata;
semanticDocument: SemanticDocumentModel;
media: PreparedDocxMedia;
}
@@ -215,12 +224,18 @@ export class PandocDocxConverter {
signal?: AbortSignal
): Promise<PandocDocxConversionResult> {
let preparedMedia;
let structurePlan;
try {
preparedMedia = preparePandocMedia(input.media);
structurePlan = createPandocStructurePlan(
input.semanticDocument,
input.themeTokens,
{ markerSeed: randomUUID() }
);
} catch (error) {
throw new PandocDocxConversionError(
"DOCX_GENERATION_FAILED",
"DOCX 媒体映射无效",
"DOCX 媒体或语义结构映射无效",
{ cause: error }
);
}
@@ -251,6 +266,10 @@ export class PandocDocxConverter {
"docx-media-filter.lua"
),
mediaMap: path.join(temporaryDirectory, "media-map.json"),
structurePlan: path.join(
temporaryDirectory,
"structure-plan.json"
),
mediaDirectory: path.join(temporaryDirectory, "media"),
dataDirectory: path.join(temporaryDirectory, "pandoc-data")
};
@@ -267,6 +286,11 @@ export class PandocDocxConverter {
JSON.stringify(preparedMedia.map),
"utf8"
),
writeFile(
paths.structurePlan,
JSON.stringify(structurePlan),
"utf8"
),
...preparedMedia.files.map((file) =>
writeFile(
path.join(temporaryDirectory, file.relativePath),
@@ -291,7 +315,8 @@ export class PandocDocxConverter {
cwd: temporaryDirectory,
env: {
...(this.options.environment ?? process.env),
[mediaMapEnvironmentName]: paths.mediaMap
[mediaMapEnvironmentName]: paths.mediaMap,
[structurePlanEnvironmentName]: paths.structurePlan
},
timeoutMs: this.timeoutMs,
maxStdoutBytes: MAXIMUM_PANDOC_STDOUT_BYTES,
@@ -333,9 +358,19 @@ export class PandocDocxConverter {
"DOCX 输出大小无效"
);
}
const docx = await readFile(paths.output);
let docx: Uint8Array;
let validation: DynamicReferenceValidation;
try {
const pandocDocx = await readFile(paths.output);
const finalized = finalizeGeneratedDocxStructure(
pandocDocx,
structurePlan,
input.themeTokens
);
docx = finalized.content;
if (docx.byteLength > this.maximumOutputBytes) {
throw new Error("DOCX 结构收口后的输出大小无效");
}
validation = validateGeneratedDocx(docx);
} catch (error) {
throw new PandocDocxConversionError(