feat: 实现 DOCX 主题样式采集
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import fs from "node:fs";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createApplicationService } from "@md-to-pdf/application";
|
||||
import {
|
||||
DOCX_STYLE_SLOT_NAMES,
|
||||
createDocxThemeStyleFingerprint
|
||||
} from "@md-to-pdf/docx-theme-engine";
|
||||
import { chromium } from "playwright";
|
||||
import { captureDocxThemeStyleWithPlaywrightPage } from "../dist/playwright-docx-theme-style.js";
|
||||
|
||||
const directory = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repositoryDirectory = path.resolve(directory, "../../..");
|
||||
const outputDirectory = path.join(
|
||||
repositoryDirectory,
|
||||
"output",
|
||||
"docx-theme-styles"
|
||||
);
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
const application = createApplicationService({
|
||||
bundledRoot: path.join(repositoryDirectory, "themes"),
|
||||
localRoot: path.join(repositoryDirectory, ".local", "themes")
|
||||
});
|
||||
|
||||
const server = http.createServer(async (request, response) => {
|
||||
try {
|
||||
const url = new URL(request.url ?? "/", "http://localhost");
|
||||
if (url.pathname === "/") {
|
||||
response.writeHead(200, {
|
||||
"content-type": "text/html; charset=utf-8"
|
||||
});
|
||||
response.end("<!doctype html><html><body></body></html>");
|
||||
return;
|
||||
}
|
||||
const match = url.pathname.match(
|
||||
/^\/api\/themes\/([^/]+)\/assets\/(.+)$/u
|
||||
);
|
||||
if (!match) {
|
||||
response.writeHead(404);
|
||||
response.end("Not found");
|
||||
return;
|
||||
}
|
||||
const themeId = decodeURIComponent(match[1]);
|
||||
const assetPath = match[2]
|
||||
.split("/")
|
||||
.map((segment) => decodeURIComponent(segment))
|
||||
.join("/");
|
||||
const asset = await application.getThemeAsset(
|
||||
themeId,
|
||||
assetPath
|
||||
);
|
||||
if (!asset) {
|
||||
response.writeHead(404);
|
||||
response.end("Not found");
|
||||
return;
|
||||
}
|
||||
response.writeHead(200, {
|
||||
"access-control-allow-origin": "*",
|
||||
"content-type": asset.contentType,
|
||||
"x-content-type-options": "nosniff"
|
||||
});
|
||||
response.end(asset.content);
|
||||
} catch (error) {
|
||||
response.writeHead(500);
|
||||
response.end(
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("无法获取 DOCX 主题样式验收地址");
|
||||
}
|
||||
const baseUrl = `http://127.0.0.1:${address.port}/`;
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
|
||||
try {
|
||||
const context = await browser.newContext({
|
||||
locale: "zh-CN",
|
||||
serviceWorkers: "block"
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto(baseUrl, { waitUntil: "domcontentloaded" });
|
||||
const { themes } = await application.listThemes();
|
||||
const bundledThemes = themes
|
||||
.filter((theme) => theme.source === "bundled")
|
||||
.sort((first, second) =>
|
||||
first.id.localeCompare(second.id, "en")
|
||||
);
|
||||
const snapshots = [];
|
||||
for (const theme of bundledThemes) {
|
||||
console.error(`[DOCX theme styles] capturing ${theme.id}`);
|
||||
const themeCss = await application.getThemeCss(theme.id);
|
||||
assert(themeCss !== undefined, `主题 ${theme.id} 缺少 CSS`);
|
||||
const themeFingerprint =
|
||||
await createDocxThemeStyleFingerprint(theme.id, themeCss);
|
||||
const snapshot =
|
||||
await captureDocxThemeStyleWithPlaywrightPage(page, {
|
||||
themeId: theme.id,
|
||||
themeFingerprint,
|
||||
themeCss,
|
||||
baseUrl
|
||||
});
|
||||
assert(
|
||||
snapshot.slots.length === DOCX_STYLE_SLOT_NAMES.length,
|
||||
`主题 ${theme.id} 的槽位数量不正确`
|
||||
);
|
||||
const missing = snapshot.slots.filter((slot) => !slot.matched);
|
||||
assert(
|
||||
missing.length === 0,
|
||||
`主题 ${theme.id} 缺少槽位:${missing
|
||||
.map((slot) => slot.slot)
|
||||
.join("、")}`
|
||||
);
|
||||
snapshots.push(snapshot);
|
||||
}
|
||||
assert(
|
||||
snapshots.length === 14,
|
||||
`内置主题数量应为 14,实际为 ${snapshots.length}`
|
||||
);
|
||||
const paragraphStyles = new Set(
|
||||
snapshots.map(
|
||||
(snapshot) =>
|
||||
snapshot.slots.find((slot) => slot.slot === "paragraph")
|
||||
?.computed?.fontFamily
|
||||
)
|
||||
);
|
||||
assert(
|
||||
paragraphStyles.size > 1,
|
||||
"主题 CSS 未产生可区分的正文字体计算结果"
|
||||
);
|
||||
fs.mkdirSync(outputDirectory, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(outputDirectory, "snapshots.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
generatedAt: new Date().toISOString(),
|
||||
chromiumVersion: browser.version(),
|
||||
themeCount: snapshots.length,
|
||||
slotCount: DOCX_STYLE_SLOT_NAMES.length,
|
||||
snapshots
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`,
|
||||
"utf8"
|
||||
);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
chromiumVersion: browser.version(),
|
||||
themes: snapshots.length,
|
||||
slotsPerTheme: DOCX_STYLE_SLOT_NAMES.length,
|
||||
totalSlots:
|
||||
snapshots.length * DOCX_STYLE_SLOT_NAMES.length
|
||||
})
|
||||
);
|
||||
await context.close();
|
||||
} finally {
|
||||
await browser.close();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
Reference in New Issue
Block a user