feat: 建立 DOCX 标准样式探针
This commit is contained in:
+8
-5
@@ -153,12 +153,15 @@ DOCX 样式令牌,支持自动映射、清单覆盖和预设降级;Front Mat
|
|||||||
DOCX 映射。
|
DOCX 映射。
|
||||||
|
|
||||||
`v0.6.0` 阶段 11 的协议骨架已建立为独立
|
`v0.6.0` 阶段 11 的协议骨架已建立为独立
|
||||||
`packages/docx-theme-engine`:定义 50 个稳定语义样式槽位、浏览器计算
|
`packages/docx-theme-engine`:定义 56 个稳定语义样式槽位、浏览器计算
|
||||||
样式快照、Word 目标样式令牌、属性来源、映射置信度和诊断协议。
|
样式快照、Word 目标样式令牌、属性来源、映射置信度和诊断协议。
|
||||||
`docxStyle` 支持 `auto`、`auto-with-overrides`、`explicit` 三种模式,
|
`docxStyle` 支持 `auto`、`auto-with-overrides`、`explicit` 三种模式,
|
||||||
普通外部主题缺少 DOCX 声明时默认进入自动映射;现有主题的 `preset` 和
|
普通外部主题缺少 DOCX 声明时默认进入自动映射;现有主题的 `preset` 和
|
||||||
扁平样式覆盖保持兼容。当前只建立公共契约与 workspace 构建边界,尚未
|
扁平样式覆盖保持兼容。标准探针 DOM 已覆盖普通文档、公文、简报、项目
|
||||||
实现样式探针页面、Chromium/Electron 采集器或接入 Pandoc 转换链路。
|
报告和标书,Profile 专属槽位避免同名类在不同结构上下文中互相污染;
|
||||||
|
平台无关采集器可以按稳定顺序读取全部 `getComputedStyle()` 并保留未
|
||||||
|
命中状态。当前尚未实现 Playwright/Electron 页面适配器,也未将快照
|
||||||
|
归一化为 Word 令牌或接入 Pandoc 转换链路。
|
||||||
|
|
||||||
## 2. 已完成
|
## 2. 已完成
|
||||||
|
|
||||||
@@ -1085,8 +1088,8 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
|||||||
- 阶段 10:已完成 Word 原生页眉页脚和第一轮全主题视觉审计;完整主题
|
- 阶段 10:已完成 Word 原生页眉页脚和第一轮全主题视觉审计;完整主题
|
||||||
映射门禁未通过;
|
映射门禁未通过;
|
||||||
- 阶段 11:已建立独立 `packages/docx-theme-engine`、标准语义槽位及
|
- 阶段 11:已建立独立 `packages/docx-theme-engine`、标准语义槽位及
|
||||||
快照、令牌、来源、置信度和诊断协议;下一步实现样式探针与
|
快照、令牌、来源、置信度、诊断协议、标准探针 DOM 和平台无关采集器;
|
||||||
Chromium/Electron 计算样式采集;
|
下一步实现 Playwright/Electron 计算样式页面适配;
|
||||||
- 阶段 12:建立统一语义文档模型,完成 Front Matter、封面、分节、
|
- 阶段 12:建立统一语义文档模型,完成 Front Matter、封面、分节、
|
||||||
表格宽度和分页控制;
|
表格宽度和分页控制;
|
||||||
- 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。
|
- 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ DOCX 主题映射引擎负责在浏览器主题 CSS 与 Word 样式之间建立
|
|||||||
|
|
||||||
当前阶段包含:
|
当前阶段包含:
|
||||||
|
|
||||||
- 标准 Markdown 与结构化文档语义槽位;
|
- 56 个标准 Markdown 与结构化文档语义槽位;
|
||||||
|
- 覆盖普通文档、公文、简报、项目报告和标书的标准探针 DOM;
|
||||||
- Chromium/Electron 计算样式快照协议;
|
- Chromium/Electron 计算样式快照协议;
|
||||||
|
- 不绑定具体浏览器实现的计算样式采集器;
|
||||||
- Word 目标样式令牌;
|
- Word 目标样式令牌;
|
||||||
- 自动映射、显式覆盖和预设降级配置;
|
- 自动映射、显式覆盖和预设降级配置;
|
||||||
- 样式来源、映射置信度和诊断协议;
|
- 样式来源、映射置信度和诊断协议;
|
||||||
@@ -20,6 +22,7 @@ DOCX 主题映射引擎负责在浏览器主题 CSS 与 Word 样式之间建立
|
|||||||
- 直接修改 OOXML;
|
- 直接修改 OOXML;
|
||||||
- 按主题 ID 维护专属转换分支。
|
- 按主题 ID 维护专属转换分支。
|
||||||
|
|
||||||
后续 Chromium 与 Electron 适配器只负责采集标准样式探针的
|
后续 Chromium 与 Electron 适配器只负责挂载探针、应用主题 CSS,并将
|
||||||
`getComputedStyle()` 结果。本包负责将快照归一化为 DOCX 样式令牌,
|
平台的 `getComputedStyle()` 接入本包采集器。本包负责将快照归一化为
|
||||||
`@md-to-pdf/docx-engine` 再消费令牌生成 `reference.docx` 和最终 OOXML。
|
DOCX 样式令牌,`@md-to-pdf/docx-engine` 再消费令牌生成
|
||||||
|
`reference.docx` 和最终 OOXML。
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
docxThemeStyleSnapshotSchema,
|
||||||
|
type DocxComputedStyle,
|
||||||
|
type DocxStyleSlotSnapshot,
|
||||||
|
type DocxThemeStyleSnapshot
|
||||||
|
} from "./snapshot.js";
|
||||||
|
import { DOCX_STYLE_SLOTS } from "./slots.js";
|
||||||
|
|
||||||
|
export interface DocxStyleProbeRoot {
|
||||||
|
querySelector(selector: string): unknown | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DocxComputedStyleReader = (
|
||||||
|
element: unknown
|
||||||
|
) => Readonly<DocxComputedStyle>;
|
||||||
|
|
||||||
|
export type DocxComputedStyleDeclaration = {
|
||||||
|
readonly [Property in keyof DocxComputedStyle]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CollectDocxThemeStyleSnapshotOptions {
|
||||||
|
root: DocxStyleProbeRoot;
|
||||||
|
readComputedStyle: DocxComputedStyleReader;
|
||||||
|
themeId: string;
|
||||||
|
themeFingerprint: string;
|
||||||
|
viewport: {
|
||||||
|
widthPx: number;
|
||||||
|
heightPx: number;
|
||||||
|
deviceScaleFactor: number;
|
||||||
|
};
|
||||||
|
rootFontSizePx: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readDocxComputedStyle(
|
||||||
|
style: DocxComputedStyleDeclaration
|
||||||
|
): DocxComputedStyle {
|
||||||
|
return {
|
||||||
|
fontFamily: style.fontFamily,
|
||||||
|
fontSize: style.fontSize,
|
||||||
|
fontWeight: style.fontWeight,
|
||||||
|
fontStyle: style.fontStyle,
|
||||||
|
color: style.color,
|
||||||
|
backgroundColor: style.backgroundColor,
|
||||||
|
lineHeight: style.lineHeight,
|
||||||
|
letterSpacing: style.letterSpacing,
|
||||||
|
textAlign: style.textAlign,
|
||||||
|
textIndent: style.textIndent,
|
||||||
|
textDecorationLine: style.textDecorationLine,
|
||||||
|
marginTop: style.marginTop,
|
||||||
|
marginRight: style.marginRight,
|
||||||
|
marginBottom: style.marginBottom,
|
||||||
|
marginLeft: style.marginLeft,
|
||||||
|
paddingTop: style.paddingTop,
|
||||||
|
paddingRight: style.paddingRight,
|
||||||
|
paddingBottom: style.paddingBottom,
|
||||||
|
paddingLeft: style.paddingLeft,
|
||||||
|
borderTop: style.borderTop,
|
||||||
|
borderRight: style.borderRight,
|
||||||
|
borderBottom: style.borderBottom,
|
||||||
|
borderLeft: style.borderLeft,
|
||||||
|
width: style.width,
|
||||||
|
maxWidth: style.maxWidth,
|
||||||
|
breakBefore: style.breakBefore,
|
||||||
|
breakAfter: style.breakAfter,
|
||||||
|
breakInside: style.breakInside,
|
||||||
|
display: style.display
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectDocxStyleSlotSnapshots(
|
||||||
|
root: DocxStyleProbeRoot,
|
||||||
|
readComputedStyle: DocxComputedStyleReader
|
||||||
|
): DocxStyleSlotSnapshot[] {
|
||||||
|
return DOCX_STYLE_SLOTS.map((definition) => {
|
||||||
|
const element = root.querySelector(definition.selector);
|
||||||
|
if (!element) {
|
||||||
|
return {
|
||||||
|
slot: definition.name,
|
||||||
|
matched: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
slot: definition.name,
|
||||||
|
matched: true,
|
||||||
|
computed: { ...readComputedStyle(element) }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectDocxThemeStyleSnapshot(
|
||||||
|
options: CollectDocxThemeStyleSnapshotOptions
|
||||||
|
): DocxThemeStyleSnapshot {
|
||||||
|
return docxThemeStyleSnapshotSchema.parse({
|
||||||
|
schemaVersion: 1,
|
||||||
|
themeId: options.themeId,
|
||||||
|
themeFingerprint: options.themeFingerprint,
|
||||||
|
viewport: options.viewport,
|
||||||
|
rootFontSizePx: options.rootFontSizePx,
|
||||||
|
slots: collectDocxStyleSlotSnapshots(
|
||||||
|
options.root,
|
||||||
|
options.readComputedStyle
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
export * from "./collector.js";
|
||||||
export * from "./configuration.js";
|
export * from "./configuration.js";
|
||||||
|
export * from "./probe.js";
|
||||||
export * from "./slots.js";
|
export * from "./slots.js";
|
||||||
export * from "./snapshot.js";
|
export * from "./snapshot.js";
|
||||||
export * from "./tokens.js";
|
export * from "./tokens.js";
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import {
|
||||||
|
DOCX_STYLE_SLOT_NAMES,
|
||||||
|
type DocxStyleSlotName
|
||||||
|
} from "./slots.js";
|
||||||
|
|
||||||
|
function attribute(slot: DocxStyleSlotName) {
|
||||||
|
return `data-docx-slot="${slot}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transparentPixel =
|
||||||
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";
|
||||||
|
|
||||||
|
export function createDocxStyleProbeMarkup(): string {
|
||||||
|
return `
|
||||||
|
<article id="write" ${attribute("document")}>
|
||||||
|
<header class="doc-metadata">
|
||||||
|
<h1 class="doc-metadata-title" ${attribute("document-title")}>文档标题</h1>
|
||||||
|
<p class="doc-author" ${attribute("document-author")}>示例作者</p>
|
||||||
|
</header>
|
||||||
|
<section class="docx-probe-markdown">
|
||||||
|
<h1 ${attribute("heading-1")}>一级标题</h1>
|
||||||
|
<h2 ${attribute("heading-2")}>二级标题</h2>
|
||||||
|
<h3 ${attribute("heading-3")}>三级标题</h3>
|
||||||
|
<h4 ${attribute("heading-4")}>四级标题</h4>
|
||||||
|
<h5 ${attribute("heading-5")}>五级标题</h5>
|
||||||
|
<h6 ${attribute("heading-6")}>六级标题</h6>
|
||||||
|
<p ${attribute("paragraph")}>
|
||||||
|
正文
|
||||||
|
<strong ${attribute("strong")}>粗体</strong>
|
||||||
|
<em ${attribute("emphasis")}>斜体</em>
|
||||||
|
<del ${attribute("strikethrough")}>删除线</del>
|
||||||
|
<code ${attribute("inline-code")}>inline()</code>
|
||||||
|
<a href="#probe" ${attribute("hyperlink")}>链接</a>
|
||||||
|
</p>
|
||||||
|
<ul ${attribute("unordered-list")}><li ${attribute("list-item")}>无序列表</li></ul>
|
||||||
|
<ol ${attribute("ordered-list")}><li>有序列表</li></ol>
|
||||||
|
<blockquote ${attribute("block-quote")}><p>引用内容</p></blockquote>
|
||||||
|
<pre class="md-fences" ${attribute("code-block")}><code>const value = 1;</code></pre>
|
||||||
|
<table ${attribute("table")}>
|
||||||
|
<thead><tr><th ${attribute("table-header")}>表头</th></tr></thead>
|
||||||
|
<tbody><tr><td ${attribute("table-cell")}>单元格</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
<figure ${attribute("figure")}>
|
||||||
|
<img src="${transparentPixel}" alt="样式探针" ${attribute("image")}>
|
||||||
|
<figcaption ${attribute("caption")}>图注</figcaption>
|
||||||
|
</figure>
|
||||||
|
<section class="footnotes" ${attribute("footnotes")}><p>脚注内容</p></section>
|
||||||
|
</section>
|
||||||
|
<section class="docx-probe-official">
|
||||||
|
<header class="doc-masthead doc-masthead-official" ${attribute("official-masthead")}>
|
||||||
|
<div class="doc-classification" ${attribute("official-classification")}>秘密 加急</div>
|
||||||
|
<p class="doc-issuer" ${attribute("official-issuer")}>示例发文机关</p>
|
||||||
|
<div class="doc-issue-row" ${attribute("official-issue-row")}>
|
||||||
|
<span class="doc-number" ${attribute("official-number")}>示例发〔2026〕1号</span>
|
||||||
|
<span class="doc-signatory" ${attribute("official-signatory")}><span>签发人:</span>张三</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<h1 class="doc-title" ${attribute("official-title")}>公文标题</h1>
|
||||||
|
<section class="doc-signature" ${attribute("official-signature")}><p>示例发文机关</p><time>2026年7月30日</time></section>
|
||||||
|
<footer class="doc-edition" ${attribute("official-edition")}><p class="doc-copy-to">抄送:示例单位</p><div class="doc-printing-row"><span>示例办公室</span><time>2026年7月30日印发</time></div></footer>
|
||||||
|
</section>
|
||||||
|
<section class="docx-probe-briefing">
|
||||||
|
<header class="doc-masthead doc-masthead-briefing" ${attribute("briefing-masthead")}>
|
||||||
|
<p class="doc-briefing-masthead">工作简报</p>
|
||||||
|
<div class="doc-briefing-meta" ${attribute("briefing-meta")}><span>第1期</span><span>示例单位</span><time>2026年7月30日</time></div>
|
||||||
|
</header>
|
||||||
|
<h1 class="doc-title" ${attribute("briefing-title")}>简报标题</h1>
|
||||||
|
<footer class="doc-briefing-contact" ${attribute("briefing-contact")}>联系人:示例</footer>
|
||||||
|
</section>
|
||||||
|
<header class="doc-cover doc-cover-project-report" ${attribute("project-report-cover")}>
|
||||||
|
<p class="doc-cover-project-name" ${attribute("project-report-project-name")}>示例建设项目</p>
|
||||||
|
<h1 class="doc-cover-title" ${attribute("project-report-title")}>可行性研究报告</h1>
|
||||||
|
<p class="doc-cover-version" ${attribute("project-report-version")}>送审稿 V1.0</p>
|
||||||
|
<p class="doc-cover-owner" ${attribute("project-report-owner")}><span>建设单位:</span>示例单位</p>
|
||||||
|
<p class="doc-cover-prepared-by" ${attribute("project-report-prepared-by")}><span>编制单位:</span>示例单位</p>
|
||||||
|
<time class="doc-cover-date" ${attribute("project-report-date")}>2026年7月</time>
|
||||||
|
</header>
|
||||||
|
<header class="doc-cover doc-cover-tender" ${attribute("tender-cover")}>
|
||||||
|
<p class="doc-cover-copy-mark" ${attribute("tender-copy-mark")}>正本</p>
|
||||||
|
<p class="doc-cover-project-name" ${attribute("tender-project-name")}>示例采购项目</p>
|
||||||
|
<p class="doc-cover-project-number" ${attribute("tender-project-number")}>ZB-2026-001</p>
|
||||||
|
<h1 class="doc-cover-title" ${attribute("tender-title")}>投标文件</h1>
|
||||||
|
<p class="doc-cover-volume" ${attribute("tender-volume")}>技术及商务文件</p>
|
||||||
|
<p class="doc-cover-bidder" ${attribute("tender-bidder")}><span>投标人:</span>示例公司</p>
|
||||||
|
<p class="doc-cover-representative" ${attribute("tender-representative")}><span>法定代表人或授权代表:</span>李四</p>
|
||||||
|
<time class="doc-cover-date" ${attribute("tender-date")}>2026年7月30日</time>
|
||||||
|
</header>
|
||||||
|
</article>`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listDocxStyleProbeSlots(
|
||||||
|
markup = createDocxStyleProbeMarkup()
|
||||||
|
): DocxStyleSlotName[] {
|
||||||
|
const matches = markup.matchAll(
|
||||||
|
/data-docx-slot="([^"]+)"/gu
|
||||||
|
);
|
||||||
|
return [...matches].map(
|
||||||
|
(match) => match[1] as DocxStyleSlotName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateDocxStyleProbeMarkup(
|
||||||
|
markup = createDocxStyleProbeMarkup()
|
||||||
|
): void {
|
||||||
|
const actual = listDocxStyleProbeSlots(markup);
|
||||||
|
const duplicates = actual.filter(
|
||||||
|
(name, index) => actual.indexOf(name) !== index
|
||||||
|
);
|
||||||
|
const actualSet = new Set(actual);
|
||||||
|
const missing = DOCX_STYLE_SLOT_NAMES.filter(
|
||||||
|
(name) => !actualSet.has(name)
|
||||||
|
);
|
||||||
|
const unexpected = actual.filter(
|
||||||
|
(name) =>
|
||||||
|
!DOCX_STYLE_SLOT_NAMES.includes(name as DocxStyleSlotName)
|
||||||
|
);
|
||||||
|
if (duplicates.length || missing.length || unexpected.length) {
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
duplicates.length
|
||||||
|
? `重复槽位:${[...new Set(duplicates)].join("、")}`
|
||||||
|
: "",
|
||||||
|
missing.length ? `缺少槽位:${missing.join("、")}` : "",
|
||||||
|
unexpected.length
|
||||||
|
? `未知槽位:${[...new Set(unexpected)].join("、")}`
|
||||||
|
: ""
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(";")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,23 +34,29 @@ export const DOCX_STYLE_SLOT_NAMES = [
|
|||||||
"official-issue-row",
|
"official-issue-row",
|
||||||
"official-number",
|
"official-number",
|
||||||
"official-signatory",
|
"official-signatory",
|
||||||
|
"official-title",
|
||||||
"official-signature",
|
"official-signature",
|
||||||
"official-edition",
|
"official-edition",
|
||||||
"briefing-masthead",
|
"briefing-masthead",
|
||||||
"briefing-meta",
|
"briefing-meta",
|
||||||
|
"briefing-title",
|
||||||
"briefing-contact",
|
"briefing-contact",
|
||||||
"cover",
|
"project-report-cover",
|
||||||
"cover-copy-mark",
|
"project-report-project-name",
|
||||||
"cover-project-name",
|
"project-report-title",
|
||||||
"cover-project-number",
|
"project-report-version",
|
||||||
"cover-title",
|
"project-report-owner",
|
||||||
"cover-volume",
|
"project-report-prepared-by",
|
||||||
"cover-owner",
|
"project-report-date",
|
||||||
"cover-prepared-by",
|
"tender-cover",
|
||||||
"cover-bidder",
|
"tender-copy-mark",
|
||||||
"cover-representative",
|
"tender-project-name",
|
||||||
"cover-version",
|
"tender-project-number",
|
||||||
"cover-date"
|
"tender-title",
|
||||||
|
"tender-volume",
|
||||||
|
"tender-bidder",
|
||||||
|
"tender-representative",
|
||||||
|
"tender-date"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const docxStyleSlotNameSchema = z.enum(
|
export const docxStyleSlotNameSchema = z.enum(
|
||||||
@@ -126,23 +132,29 @@ const kinds: Record<DocxStyleSlotName, DocxStyleSlotKind> = {
|
|||||||
"official-issue-row": "structure",
|
"official-issue-row": "structure",
|
||||||
"official-number": "structure",
|
"official-number": "structure",
|
||||||
"official-signatory": "structure",
|
"official-signatory": "structure",
|
||||||
|
"official-title": "structure",
|
||||||
"official-signature": "structure",
|
"official-signature": "structure",
|
||||||
"official-edition": "structure",
|
"official-edition": "structure",
|
||||||
"briefing-masthead": "structure",
|
"briefing-masthead": "structure",
|
||||||
"briefing-meta": "structure",
|
"briefing-meta": "structure",
|
||||||
|
"briefing-title": "structure",
|
||||||
"briefing-contact": "structure",
|
"briefing-contact": "structure",
|
||||||
cover: "structure",
|
"project-report-cover": "structure",
|
||||||
"cover-copy-mark": "structure",
|
"project-report-project-name": "structure",
|
||||||
"cover-project-name": "structure",
|
"project-report-title": "structure",
|
||||||
"cover-project-number": "structure",
|
"project-report-version": "structure",
|
||||||
"cover-title": "structure",
|
"project-report-owner": "structure",
|
||||||
"cover-volume": "structure",
|
"project-report-prepared-by": "structure",
|
||||||
"cover-owner": "structure",
|
"project-report-date": "structure",
|
||||||
"cover-prepared-by": "structure",
|
"tender-cover": "structure",
|
||||||
"cover-bidder": "structure",
|
"tender-copy-mark": "structure",
|
||||||
"cover-representative": "structure",
|
"tender-project-name": "structure",
|
||||||
"cover-version": "structure",
|
"tender-project-number": "structure",
|
||||||
"cover-date": "structure"
|
"tender-title": "structure",
|
||||||
|
"tender-volume": "structure",
|
||||||
|
"tender-bidder": "structure",
|
||||||
|
"tender-representative": "structure",
|
||||||
|
"tender-date": "structure"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DOCX_STYLE_SLOTS: readonly DocxStyleSlotDefinition[] =
|
export const DOCX_STYLE_SLOTS: readonly DocxStyleSlotDefinition[] =
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
DOCX_STYLE_SLOT_NAMES,
|
||||||
|
collectDocxStyleSlotSnapshots,
|
||||||
|
collectDocxThemeStyleSnapshot,
|
||||||
|
createDocxStyleProbeMarkup,
|
||||||
|
listDocxStyleProbeSlots,
|
||||||
|
readDocxComputedStyle,
|
||||||
|
validateDocxStyleProbeMarkup,
|
||||||
|
type DocxComputedStyle,
|
||||||
|
type DocxStyleProbeRoot
|
||||||
|
} from "../src/index.js";
|
||||||
|
|
||||||
|
const computedStyle: DocxComputedStyle = {
|
||||||
|
fontFamily: '"Microsoft YaHei", sans-serif',
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: "400",
|
||||||
|
fontStyle: "normal",
|
||||||
|
color: "rgb(17, 17, 17)",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||||
|
lineHeight: "28px",
|
||||||
|
letterSpacing: "normal",
|
||||||
|
textAlign: "start",
|
||||||
|
textIndent: "32px",
|
||||||
|
textDecorationLine: "none",
|
||||||
|
marginTop: "0px",
|
||||||
|
marginRight: "0px",
|
||||||
|
marginBottom: "12px",
|
||||||
|
marginLeft: "0px",
|
||||||
|
paddingTop: "0px",
|
||||||
|
paddingRight: "0px",
|
||||||
|
paddingBottom: "0px",
|
||||||
|
paddingLeft: "0px",
|
||||||
|
borderTop: "0px none rgb(17, 17, 17)",
|
||||||
|
borderRight: "0px none rgb(17, 17, 17)",
|
||||||
|
borderBottom: "0px none rgb(17, 17, 17)",
|
||||||
|
borderLeft: "0px none rgb(17, 17, 17)",
|
||||||
|
width: "640px",
|
||||||
|
maxWidth: "none",
|
||||||
|
breakBefore: "auto",
|
||||||
|
breakAfter: "auto",
|
||||||
|
breakInside: "auto",
|
||||||
|
display: "block"
|
||||||
|
};
|
||||||
|
|
||||||
|
function createRoot(matchedSlots: readonly string[]): DocxStyleProbeRoot {
|
||||||
|
const selectors = new Set(
|
||||||
|
matchedSlots.map(
|
||||||
|
(slot) => `[data-docx-slot="${slot}"]`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
querySelector(selector) {
|
||||||
|
return selectors.has(selector) ? { selector } : null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("DOCX 标准样式探针", () => {
|
||||||
|
it("每个标准槽位在探针中恰好出现一次", () => {
|
||||||
|
const markup = createDocxStyleProbeMarkup();
|
||||||
|
expect(() => validateDocxStyleProbeMarkup(markup)).not.toThrow();
|
||||||
|
const slots = listDocxStyleProbeSlots(markup);
|
||||||
|
expect(slots).toHaveLength(DOCX_STYLE_SLOT_NAMES.length);
|
||||||
|
expect(new Set(slots)).toEqual(
|
||||||
|
new Set(DOCX_STYLE_SLOT_NAMES)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("拒绝缺失和重复槽位的探针", () => {
|
||||||
|
const markup = createDocxStyleProbeMarkup()
|
||||||
|
.replace('data-docx-slot="paragraph"', "")
|
||||||
|
.replace(
|
||||||
|
"</article>",
|
||||||
|
'<p data-docx-slot="strong">重复</p></article>'
|
||||||
|
);
|
||||||
|
expect(() => validateDocxStyleProbeMarkup(markup)).toThrow(
|
||||||
|
/重复槽位:strong;缺少槽位:paragraph/u
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DOCX 计算样式采集", () => {
|
||||||
|
it("按稳定槽位顺序采集并保留未命中状态", () => {
|
||||||
|
const slots = collectDocxStyleSlotSnapshots(
|
||||||
|
createRoot(["document", "paragraph"]),
|
||||||
|
() => readDocxComputedStyle(computedStyle)
|
||||||
|
);
|
||||||
|
expect(slots).toHaveLength(DOCX_STYLE_SLOT_NAMES.length);
|
||||||
|
expect(slots[0]).toMatchObject({
|
||||||
|
slot: "document",
|
||||||
|
matched: true,
|
||||||
|
computed: { fontSize: "16px" }
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
slots.find((entry) => entry.slot === "paragraph")
|
||||||
|
).toMatchObject({ matched: true });
|
||||||
|
expect(
|
||||||
|
slots.find((entry) => entry.slot === "heading-1")
|
||||||
|
).toEqual({ slot: "heading-1", matched: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("生成可校验且与平台实现无关的主题快照", () => {
|
||||||
|
const snapshot = collectDocxThemeStyleSnapshot({
|
||||||
|
root: createRoot(DOCX_STYLE_SLOT_NAMES),
|
||||||
|
readComputedStyle: () =>
|
||||||
|
readDocxComputedStyle(computedStyle),
|
||||||
|
themeId: "external-clean",
|
||||||
|
themeFingerprint: "c".repeat(64),
|
||||||
|
viewport: {
|
||||||
|
widthPx: 794,
|
||||||
|
heightPx: 1123,
|
||||||
|
deviceScaleFactor: 1
|
||||||
|
},
|
||||||
|
rootFontSizePx: 16
|
||||||
|
});
|
||||||
|
expect(snapshot.slots).toHaveLength(
|
||||||
|
DOCX_STYLE_SLOT_NAMES.length
|
||||||
|
);
|
||||||
|
expect(snapshot.slots.every((slot) => slot.matched)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user