feat: 完成 DOCX R4 元素级视觉门禁
建立 Chromium、Word 与 WPS 的可编辑内容、段落几何、页眉页脚、封面、媒体和逐页视觉比较链路。 支持封面独立分节、正文页码重启、主题页边距与横向纸张,并将自然分页差异降为诊断项。 修正代码块内边距和折叠表格单元格边框映射,14 套主题生产矩阵与六组布局视觉矩阵通过。
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user