feat: 完成 DOCX R4 元素级视觉门禁

建立 Chromium、Word 与 WPS 的可编辑内容、段落几何、页眉页脚、封面、媒体和逐页视觉比较链路。

支持封面独立分节、正文页码重启、主题页边距与横向纸张,并将自然分页差异降为诊断项。

修正代码块内边距和折叠表格单元格边框映射,14 套主题生产矩阵与六组布局视觉矩阵通过。
This commit is contained in:
SkyJourney
2026-08-02 03:27:11 +08:00
parent f9f5fccfc9
commit 58f87cc19f
100 changed files with 10409 additions and 386 deletions
@@ -60,7 +60,11 @@ export function readDocxComputedStyle(
borderBottom: style.borderBottom,
borderLeft: style.borderLeft,
width: style.width,
...(style.containingBlockWidth
? { containingBlockWidth: style.containingBlockWidth }
: {}),
maxWidth: style.maxWidth,
minWidth: style.minWidth,
minHeight: style.minHeight,
height: style.height,
breakBefore: style.breakBefore,
@@ -69,7 +73,11 @@ export function readDocxComputedStyle(
display: style.display,
flexDirection: style.flexDirection,
alignItems: style.alignItems,
...(style.alignSelf ? { alignSelf: style.alignSelf } : {}),
justifyContent: style.justifyContent,
...(style.contentPaddingLeft
? { contentPaddingLeft: style.contentPaddingLeft }
: {}),
outline: style.outline,
outlineOffset: style.outlineOffset
};
+71 -2
View File
@@ -463,11 +463,21 @@ function normalizeComputedSlot(
};
}
}
if (widthSlots.has(context.slot) && context.documentWidthPx) {
const containingBlockWidthPt = computed.containingBlockWidth
? parseCssLengthToPt(computed.containingBlockWidth)
: undefined;
const relativeWidthPx =
containingBlockWidthPt !== undefined
? containingBlockWidthPt / 0.75
: context.documentWidthPx;
if (
(widthSlots.has(context.slot) || context.kind === "structure") &&
relativeWidthPx
) {
const widthPt = parseCssLengthToPt(computed.width);
const widthPx = widthPt === undefined ? undefined : widthPt / 0.75;
if (widthPx !== undefined) {
const percent = (widthPx / context.documentWidthPx) * 100;
const percent = (widthPx / relativeWidthPx) * 100;
style.widthPercent = Math.round(
Math.max(0, Math.min(100, percent))
);
@@ -482,6 +492,27 @@ function normalizeComputedSlot(
}
}
}
if (context.kind === "structure" && context.documentWidthPx) {
const percentage = /^([+-]?(?:\d+\.?\d*|\.\d+))%$/u.exec(
computed.minWidth
);
const minimumWidthPercent = percentage
? Number.parseFloat(percentage[1]!)
: (() => {
const minimumWidthPt = parseCssLengthToPt(computed.minWidth);
return minimumWidthPt === undefined
? undefined
: ((minimumWidthPt / 0.75) / context.documentWidthPx) * 100;
})();
if (
minimumWidthPercent !== undefined &&
minimumWidthPercent > 0
) {
style.minimumWidthPercent = roundDocxValue(
Math.min(100, minimumWidthPercent)
);
}
}
const minimumHeightPt = length(
context,
"min-height",
@@ -491,6 +522,15 @@ function normalizeComputedSlot(
if (minimumHeightPt !== undefined && minimumHeightPt > 0) {
style.minimumHeightPt = minimumHeightPt;
}
const contentPaddingLeftPt = computed.contentPaddingLeft
? length(context, "content-padding-left", computed.contentPaddingLeft, {
allowAuto: true,
nonNegative: true
})
: undefined;
if (contentPaddingLeftPt !== undefined && contentPaddingLeftPt > 0) {
style.contentPaddingLeftPt = contentPaddingLeftPt;
}
if (computed.display === "none") {
style.hidden = true;
}
@@ -512,6 +552,35 @@ function normalizeComputedSlot(
if (resolved) {
style.verticalAlignment = resolved;
}
const childAlignment = {
"flex-start": "left",
start: "left",
center: "center",
"flex-end": "right",
end: "right",
stretch: "stretch"
} as const;
const resolvedChildAlignment =
childAlignment[
computed.alignItems as keyof typeof childAlignment
];
if (resolvedChildAlignment) {
style.childAlignment = resolvedChildAlignment;
}
}
const selfAlignment = {
"flex-start": "left",
start: "left",
center: "center",
"flex-end": "right",
end: "right",
stretch: "stretch"
} as const;
const resolvedSelfAlignment = computed.alignSelf
? selfAlignment[computed.alignSelf as keyof typeof selfAlignment]
: undefined;
if (resolvedSelfAlignment) {
style.selfAlignment = resolvedSelfAlignment;
}
if (computed.display === "flex" || computed.display === "grid") {
diagnostic(context, {
+3 -3
View File
@@ -13,12 +13,12 @@ const transparentPixel =
export function createDocxStyleProbeMarkup(): string {
return `
<article id="write" ${attribute("document")}>
<h1 ${attribute("heading-1")}>一级标题</h1>
<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>
@@ -56,8 +56,8 @@ export function createDocxStyleProbeMarkup(): string {
</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 class="doc-signature" ${attribute("official-signature")}><p class="doc-signature-issuer" ${attribute("official-signature-issuer")}>示例发文机关</p><time class="doc-signature-date" ${attribute("official-signature-date")}>2026年7月30日</time></section>
<footer class="doc-edition" ${attribute("official-edition")}><p class="doc-copy-to" ${attribute("official-copy-to")}>抄送:示例单位</p><div class="doc-printing-row" ${attribute("official-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")}>
+42
View File
@@ -75,6 +75,7 @@ const computedProperties = [
"borderLeft",
"width",
"maxWidth",
"minWidth",
"minHeight",
"height",
"breakBefore",
@@ -83,6 +84,7 @@ const computedProperties = [
"display",
"flexDirection",
"alignItems",
"alignSelf",
"justifyContent",
"outline",
"outlineOffset"
@@ -104,6 +106,23 @@ body {
margin: 0;
padding: 0;
}
#write pre.md-fences > code,
#write .md-code-pagination-chunk > code {
display: block;
min-width: 0;
max-width: 100%;
padding: 0;
padding-left: 8px;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
font-family: inherit;
font-size: inherit;
font-weight: inherit;
line-height: inherit;
}
`;
export function createDocxThemeStyleCaptureScript(
@@ -190,6 +209,29 @@ export function createDocxThemeStyleCaptureScript(
for (const property of payload.properties) {
computed[property] = style[property];
}
const contentElement = element.firstElementChild;
if (contentElement) {
computed.contentPaddingLeft =
probeWindow.getComputedStyle(contentElement).paddingLeft;
}
const containingBlock = element.parentElement;
if (containingBlock) {
const containingStyle = probeWindow.getComputedStyle(
containingBlock
);
const containingRect = containingBlock.getBoundingClientRect();
const horizontalInsets = [
containingStyle.borderLeftWidth,
containingStyle.borderRightWidth,
containingStyle.paddingLeft,
containingStyle.paddingRight
].reduce(
(total, value) => total + (Number.parseFloat(value) || 0),
0
);
computed.containingBlockWidth =
Math.max(0, containingRect.width - horizontalInsets) + "px";
}
return {
slot: definition.name,
matched: true,
+8
View File
@@ -36,7 +36,11 @@ export const DOCX_STYLE_SLOT_NAMES = [
"official-signatory",
"official-title",
"official-signature",
"official-signature-issuer",
"official-signature-date",
"official-edition",
"official-copy-to",
"official-printing-row",
"briefing-masthead",
"briefing-meta",
"briefing-title",
@@ -134,7 +138,11 @@ const kinds: Record<DocxStyleSlotName, DocxStyleSlotKind> = {
"official-signatory": "structure",
"official-title": "structure",
"official-signature": "structure",
"official-signature-issuer": "structure",
"official-signature-date": "structure",
"official-edition": "structure",
"official-copy-to": "structure",
"official-printing-row": "structure",
"briefing-masthead": "structure",
"briefing-meta": "structure",
"briefing-title": "structure",
@@ -29,7 +29,9 @@ export const docxComputedStyleSchema = z.object({
borderBottom: cssValueSchema,
borderLeft: cssValueSchema,
width: cssValueSchema,
containingBlockWidth: cssValueSchema.optional(),
maxWidth: cssValueSchema,
minWidth: cssValueSchema,
minHeight: cssValueSchema,
height: cssValueSchema,
breakBefore: cssValueSchema,
@@ -38,7 +40,9 @@ export const docxComputedStyleSchema = z.object({
display: cssValueSchema,
flexDirection: cssValueSchema,
alignItems: cssValueSchema,
alignSelf: cssValueSchema.optional(),
justifyContent: cssValueSchema,
contentPaddingLeft: cssValueSchema.optional(),
outline: cssValueSchema,
outlineOffset: cssValueSchema
});
+8
View File
@@ -106,10 +106,18 @@ export const docxSlotStyleTokenSchema = z.object({
})
.optional(),
widthPercent: z.number().min(0).max(100).optional(),
minimumWidthPercent: z.number().min(0).max(100).optional(),
minimumHeightPt: z.number().min(0).max(10000).optional(),
contentPaddingLeftPt: z.number().min(0).max(1000).optional(),
verticalAlignment: z
.enum(["top", "center", "bottom"])
.optional(),
childAlignment: z
.enum(["left", "center", "right", "stretch"])
.optional(),
selfAlignment: z
.enum(["left", "center", "right", "stretch"])
.optional(),
hidden: z.boolean().optional(),
pageBreakBefore: z.boolean().optional(),
pageBreakAfter: z.boolean().optional(),