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(""); 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)); }