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
+122 -23
View File
@@ -25,6 +25,7 @@ import {
pointsToTwips,
removeDirectChildren,
serializeXmlPart,
wordCharacterSpacingTwips,
type XmlElement
} from "./ooxml.js";
@@ -62,7 +63,8 @@ function setRunStyle(
"i",
"iCs",
"u",
"shd"
"shd",
"spacing"
);
if (options.fonts) {
setFonts(parent, options.fonts);
@@ -100,6 +102,9 @@ function setRunStyle(
appendElement(parent, WORD_NAMESPACE, "w:szCs", {
"w:val": size
});
appendElement(parent, WORD_NAMESPACE, "w:spacing", {
"w:val": wordCharacterSpacingTwips(options.sizePt)
});
}
}
@@ -283,10 +288,16 @@ function applyTokenRunStyle(
});
}
}
if (token.letterSpacingPt !== undefined) {
if (
token.letterSpacingPt !== undefined ||
token.fontSizePt !== undefined
) {
removeDirectChildren(parent, WORD_NAMESPACE, "spacing");
appendElement(parent, WORD_NAMESPACE, "w:spacing", {
"w:val": pointsToTwips(token.letterSpacingPt)
"w:val": wordCharacterSpacingTwips(
token.fontSizePt,
token.letterSpacingPt ?? 0
)
});
}
toggleProperty(parent, "b", token.bold);
@@ -299,7 +310,8 @@ function applyTokenRunStyle(
function applyTokenBorders(
parent: XmlElement,
token: DocxSlotStyleToken
token: DocxSlotStyleToken,
horizontalContentPaddingInBorderSpace = false
) {
if (!token.borders) {
return;
@@ -330,7 +342,18 @@ function applyTokenBorders(
0,
Math.min(
31,
Math.round(token.paddingPt?.[side] ?? 0)
Math.round(
horizontalContentPaddingInBorderSpace && side === "left"
? Math.floor(
Math.max(
0,
(token.paddingPt?.left ?? 0) +
(token.contentPaddingLeftPt ?? 0) -
border.widthPt * 2
)
)
: token.paddingPt?.[side] ?? 0
)
)
)
),
@@ -342,11 +365,34 @@ function applyTokenBorders(
function applyTokenParagraphStyle(
parent: XmlElement,
token: DocxSlotStyleToken,
fallbackFontSizePt: number
fallbackFontSizePt: number,
horizontalPaddingThroughBorderSpace = false
) {
if (
for (const propertyName of ["autoSpaceDE", "autoSpaceDN"] as const) {
const automaticSpacing = ensureDirectElement(
parent,
WORD_NAMESPACE,
`w:${propertyName}`
);
setWordAttribute(automaticSpacing, "val", "0");
}
const spacingBeforePt =
token.spacingBeforePt !== undefined ||
(token.paddingPt?.top ?? 0) > 0
? (token.spacingBeforePt ?? 0) +
(token.borders?.top ? 0 : token.paddingPt?.top ?? 0)
: undefined;
const spacingAfterPt =
token.spacingAfterPt !== undefined ||
(token.paddingPt?.bottom ?? 0) > 0
? (token.spacingAfterPt ?? 0) +
(token.borders?.bottom
? 0
: token.paddingPt?.bottom ?? 0)
: undefined;
if (
spacingBeforePt !== undefined ||
spacingAfterPt !== undefined ||
token.lineSpacing !== undefined
) {
const spacing = ensureDirectElement(
@@ -354,18 +400,18 @@ function applyTokenParagraphStyle(
WORD_NAMESPACE,
"w:spacing"
);
if (token.spacingBeforePt !== undefined) {
if (spacingBeforePt !== undefined) {
setWordAttribute(
spacing,
"before",
pointsToTwips(token.spacingBeforePt)
pointsToTwips(spacingBeforePt)
);
}
if (token.spacingAfterPt !== undefined) {
if (spacingAfterPt !== undefined) {
setWordAttribute(
spacing,
"after",
pointsToTwips(token.spacingAfterPt)
pointsToTwips(spacingAfterPt)
);
}
if (token.lineSpacing !== undefined) {
@@ -380,10 +426,28 @@ function applyTokenParagraphStyle(
setWordAttribute(spacing, "lineRule", "exact");
}
}
const leftIndentPt =
token.leftIndentPt !== undefined ||
(token.paddingPt?.left ?? 0) > 0 ||
(token.contentPaddingLeftPt ?? 0) > 0 ||
(token.borders?.left?.widthPt ?? 0) > 0
? (token.leftIndentPt ?? 0) +
(token.paddingPt?.left ?? 0) +
(token.contentPaddingLeftPt ?? 0) +
(token.borders?.left?.widthPt ?? 0)
: undefined;
const rightIndentPt =
token.rightIndentPt !== undefined ||
(token.paddingPt?.right ?? 0) > 0 ||
(token.borders?.right?.widthPt ?? 0) > 0
? (token.rightIndentPt ?? 0) +
(token.paddingPt?.right ?? 0) +
(token.borders?.right?.widthPt ?? 0)
: undefined;
if (
token.firstLineIndentPt !== undefined ||
token.leftIndentPt !== undefined ||
token.rightIndentPt !== undefined
leftIndentPt !== undefined ||
rightIndentPt !== undefined
) {
const indent = ensureDirectElement(
parent,
@@ -398,19 +462,19 @@ function applyTokenParagraphStyle(
);
indent.removeAttributeNS(WORD_NAMESPACE, "firstLineChars");
}
if (token.leftIndentPt !== undefined) {
if (leftIndentPt !== undefined) {
setWordAttribute(
indent,
"left",
pointsToTwips(token.leftIndentPt)
pointsToTwips(leftIndentPt)
);
indent.removeAttributeNS(WORD_NAMESPACE, "leftChars");
}
if (token.rightIndentPt !== undefined) {
if (rightIndentPt !== undefined) {
setWordAttribute(
indent,
"right",
pointsToTwips(token.rightIndentPt)
pointsToTwips(rightIndentPt)
);
}
}
@@ -431,7 +495,11 @@ function applyTokenParagraphStyle(
"w:fill": colorValue(token.backgroundColor)
});
}
applyTokenBorders(parent, token);
applyTokenBorders(
parent,
token,
horizontalPaddingThroughBorderSpace
);
toggleProperty(parent, "keepLines", token.keepLines);
toggleProperty(parent, "keepNext", token.keepWithNext);
toggleProperty(
@@ -864,7 +932,8 @@ function applyTableAndCaption(
});
removeDirectChildren(pPr, WORD_NAMESPACE, "jc");
appendElement(pPr, WORD_NAMESPACE, "w:jc", {
"w:val": style.caption.alignment
"w:val":
styleId === "ImageCaption" ? "center" : style.caption.alignment
});
setRunStyle(
ensureDirectElement(caption, WORD_NAMESPACE, "w:rPr"),
@@ -940,7 +1009,14 @@ function applyTableTokenStyles(
});
}
}
if (tableToken?.borders) {
const tableBorders =
tableToken?.borders || cellToken?.borders
? {
...cellToken?.borders,
...tableToken?.borders
}
: undefined;
if (tableBorders) {
removeDirectChildren(tblPr, WORD_NAMESPACE, "tblBorders");
const borders = appendElement(
tblPr,
@@ -953,7 +1029,25 @@ function applyTableTokenStyles(
"bottom",
"left"
] as const) {
const border = tableToken.borders[side];
const border = tableBorders[side];
if (!border) {
continue;
}
appendElement(borders, WORD_NAMESPACE, `w:${side}`, {
"w:val": border.style,
"w:sz": String(Math.round(border.widthPt * 8)),
"w:space": "0",
"w:color": colorValue(border.color)
});
}
const insideHorizontal =
cellToken?.borders?.top ?? cellToken?.borders?.bottom;
const insideVertical =
cellToken?.borders?.left ?? cellToken?.borders?.right;
for (const [side, border] of [
["insideH", insideHorizontal],
["insideV", insideVertical]
] as const) {
if (!border) {
continue;
}
@@ -1067,14 +1161,19 @@ function applyTokenStyles(
binding.basedOn
);
if (binding.type === "paragraph") {
const paragraphToken =
binding.styleId === "ImageCaption"
? { ...entry.style, alignment: "center" as const }
: entry.style;
applyTokenParagraphStyle(
ensureDirectElement(
target,
WORD_NAMESPACE,
"w:pPr"
),
entry.style,
fallbackFontSizeForSlot(slot, fallback)
paragraphToken,
fallbackFontSizeForSlot(slot, fallback),
binding.styleId === "SourceCode"
);
}
applyTokenRunStyle(