feat: 实现 Pandoc DOCX 转换服务
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
copyFile,
|
||||
mkdtemp,
|
||||
rm,
|
||||
writeFile
|
||||
} from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
defaultExportConfig,
|
||||
type PreparedDocxMedia,
|
||||
type ThemeManifest
|
||||
} from "@md-to-pdf/core";
|
||||
import {
|
||||
PandocDocxConversionError,
|
||||
PandocDocxConverter,
|
||||
type PandocProcessRunner,
|
||||
type PandocRuntimeProvider
|
||||
} from "../src/index.js";
|
||||
import { createTestBaselineReference } from "./reference-test-fixture.js";
|
||||
|
||||
function theme(): ThemeManifest {
|
||||
return {
|
||||
manifestVersion: 1,
|
||||
id: "test-theme",
|
||||
name: "测试主题",
|
||||
version: "1.0.0",
|
||||
description: "测试",
|
||||
author: "测试",
|
||||
license: "内部许可",
|
||||
entry: "theme.css",
|
||||
domPreset: "generic",
|
||||
defaultFontSize: "16px",
|
||||
supportedFeatures: [],
|
||||
category: "general",
|
||||
compatibleProfiles: [],
|
||||
docxStyle: { preset: "technical" },
|
||||
bundled: true
|
||||
};
|
||||
}
|
||||
|
||||
const emptyMedia: PreparedDocxMedia = {
|
||||
resources: [],
|
||||
echartsErrors: [],
|
||||
mermaidErrors: [],
|
||||
warnings: [],
|
||||
totalBytes: 0
|
||||
};
|
||||
|
||||
function input() {
|
||||
return {
|
||||
markdown: "# 测试",
|
||||
fileName: "测试.md",
|
||||
language: "zh-CN",
|
||||
exportConfig: defaultExportConfig,
|
||||
theme: theme(),
|
||||
metadata: {
|
||||
title: "测试",
|
||||
author: "测试人",
|
||||
subject: "",
|
||||
keywords: [],
|
||||
language: "zh-CN"
|
||||
},
|
||||
media: emptyMedia
|
||||
};
|
||||
}
|
||||
|
||||
function argumentAfter(arguments_: readonly string[], name: string) {
|
||||
const index = arguments_.indexOf(name);
|
||||
if (index < 0 || !arguments_[index + 1]) {
|
||||
throw new Error(`缺少参数 ${name}`);
|
||||
}
|
||||
return arguments_[index + 1]!;
|
||||
}
|
||||
|
||||
describe("Pandoc DOCX 转换器", () => {
|
||||
let temporaryRoot: string;
|
||||
const baseline = createTestBaselineReference();
|
||||
const runtime: PandocRuntimeProvider = {
|
||||
async getExecutablePath() {
|
||||
return "pandoc";
|
||||
},
|
||||
async getDefaultReferenceDocx() {
|
||||
return baseline;
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
temporaryRoot = await mkdtemp(
|
||||
path.join(os.tmpdir(), "docx-converter-test-")
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(temporaryRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("使用固定参数转换、校验并清理请求级临时目录", async () => {
|
||||
let requestDirectory = "";
|
||||
const runner = vi.fn<PandocProcessRunner>(
|
||||
async (_executable, arguments_, options) => {
|
||||
requestDirectory = options.cwd!;
|
||||
expect(arguments_).toContain("--lua-filter");
|
||||
expect(arguments_).toContain("--data-dir");
|
||||
expect(arguments_).toContain("--resource-path");
|
||||
expect(
|
||||
options.env?.MD_TO_PDF_DOCX_MEDIA_MAP
|
||||
).toContain("media-map.json");
|
||||
await copyFile(
|
||||
argumentAfter(arguments_, "--reference-doc"),
|
||||
argumentAfter(arguments_, "--output")
|
||||
);
|
||||
return {
|
||||
outcome: "completed",
|
||||
exitCode: 0,
|
||||
stdout: new Uint8Array(),
|
||||
stderr: ""
|
||||
};
|
||||
}
|
||||
);
|
||||
const converter = new PandocDocxConverter({
|
||||
runtime,
|
||||
runner,
|
||||
temporaryRoot
|
||||
});
|
||||
const result = await converter.convert(input());
|
||||
|
||||
expect(result.docx.byteLength).toBeGreaterThan(0);
|
||||
expect(result.validation.partCount).toBeGreaterThan(8);
|
||||
expect(runner).toHaveBeenCalledTimes(1);
|
||||
expect(existsSync(requestDirectory)).toBe(false);
|
||||
});
|
||||
|
||||
it("超时和无效输出均返回稳定错误并清理目录", async () => {
|
||||
let timeoutDirectory = "";
|
||||
const timeoutRunner = vi.fn<PandocProcessRunner>(
|
||||
async (_executable, _arguments, options) => {
|
||||
timeoutDirectory = options.cwd!;
|
||||
return {
|
||||
outcome: "timeout",
|
||||
exitCode: null,
|
||||
stdout: new Uint8Array(),
|
||||
stderr: ""
|
||||
};
|
||||
}
|
||||
);
|
||||
await expect(
|
||||
new PandocDocxConverter({
|
||||
runtime,
|
||||
runner: timeoutRunner,
|
||||
temporaryRoot
|
||||
}).convert(input())
|
||||
).rejects.toMatchObject({
|
||||
code: "DOCX_RENDER_TIMEOUT"
|
||||
} satisfies Partial<PandocDocxConversionError>);
|
||||
expect(existsSync(timeoutDirectory)).toBe(false);
|
||||
|
||||
const invalidRunner = vi.fn<PandocProcessRunner>(
|
||||
async (_executable, arguments_) => {
|
||||
await writeFile(
|
||||
argumentAfter(arguments_, "--output"),
|
||||
"not a docx"
|
||||
);
|
||||
return {
|
||||
outcome: "completed",
|
||||
exitCode: 0,
|
||||
stdout: new Uint8Array(),
|
||||
stderr: ""
|
||||
};
|
||||
}
|
||||
);
|
||||
await expect(
|
||||
new PandocDocxConverter({
|
||||
runtime,
|
||||
runner: invalidRunner,
|
||||
temporaryRoot
|
||||
}).convert(input())
|
||||
).rejects.toMatchObject({
|
||||
code: "DOCX_OUTPUT_INVALID"
|
||||
} satisfies Partial<PandocDocxConversionError>);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user