Files
MorphDoc/packages/docx-engine/src/styles-transform.ts
T

1210 lines
29 KiB
TypeScript

import type { DocxFontFamily } from "@md-to-pdf/core";
import type {
DocxSlotStyleToken,
DocxStyleSlotName,
DocxThemeTokenSet
} from "@md-to-pdf/docx-theme-engine";
import type { ResolvedDocxThemeStyle } from "./style-presets.js";
import {
DOCX_SLOT_WORD_STYLE_BINDINGS,
collectDocxTokenFonts,
createDocxTokenSlotMap,
resolveTokenFonts
} from "./token-style-map.js";
import {
DRAWING_NAMESPACE,
WORD_NAMESPACE,
appendElement,
colorValue,
directChildren,
ensureDirectElement,
firstDirectChild,
millimetersToTwips,
parseXmlPart,
pointsToHalfPoints,
pointsToTwips,
removeDirectChildren,
serializeXmlPart,
type XmlElement
} from "./ooxml.js";
function setFonts(parent: XmlElement, fonts: DocxFontFamily) {
removeDirectChildren(parent, WORD_NAMESPACE, "rFonts");
appendElement(parent, WORD_NAMESPACE, "w:rFonts", {
"w:ascii": fonts.latin,
"w:hAnsi": fonts.latin,
"w:eastAsia": fonts.eastAsia,
"w:cs": fonts.complexScript ?? fonts.latin
});
}
function setRunStyle(
parent: XmlElement,
options: {
fonts?: DocxFontFamily;
sizePt?: number;
color?: string;
bold?: boolean;
italic?: boolean;
underline?: boolean;
backgroundColor?: string;
}
) {
removeDirectChildren(
parent,
WORD_NAMESPACE,
"rFonts",
"sz",
"szCs",
"color",
"b",
"bCs",
"i",
"iCs",
"u",
"shd"
);
if (options.fonts) {
setFonts(parent, options.fonts);
}
if (options.bold) {
appendElement(parent, WORD_NAMESPACE, "w:b");
appendElement(parent, WORD_NAMESPACE, "w:bCs");
}
if (options.italic) {
appendElement(parent, WORD_NAMESPACE, "w:i");
appendElement(parent, WORD_NAMESPACE, "w:iCs");
}
if (options.color) {
appendElement(parent, WORD_NAMESPACE, "w:color", {
"w:val": colorValue(options.color)
});
}
if (options.underline) {
appendElement(parent, WORD_NAMESPACE, "w:u", {
"w:val": "single"
});
}
if (options.backgroundColor) {
appendElement(parent, WORD_NAMESPACE, "w:shd", {
"w:val": "clear",
"w:color": "auto",
"w:fill": colorValue(options.backgroundColor)
});
}
if (options.sizePt !== undefined) {
const size = pointsToHalfPoints(options.sizePt);
appendElement(parent, WORD_NAMESPACE, "w:sz", {
"w:val": size
});
appendElement(parent, WORD_NAMESPACE, "w:szCs", {
"w:val": size
});
}
}
function setParagraphSpacing(
parent: XmlElement,
options: {
beforePt: number;
afterPt: number;
fontSizePt: number;
lineSpacing: number;
firstLineIndentChars?: number;
}
) {
removeDirectChildren(parent, WORD_NAMESPACE, "spacing", "ind");
appendElement(parent, WORD_NAMESPACE, "w:spacing", {
"w:before": pointsToTwips(options.beforePt),
"w:after": pointsToTwips(options.afterPt),
"w:line": pointsToTwips(
options.fontSizePt * options.lineSpacing
),
"w:lineRule": "exact"
});
if (options.firstLineIndentChars !== undefined) {
appendElement(parent, WORD_NAMESPACE, "w:ind", {
"w:firstLineChars": String(
Math.round(options.firstLineIndentChars * 100)
)
});
}
}
function findStyle(styles: XmlElement, styleId: string) {
return Array.from(
styles.getElementsByTagNameNS(WORD_NAMESPACE, "style")
).find(
(element) =>
element.getAttributeNS(WORD_NAMESPACE, "styleId") === styleId
);
}
function ensureStyle(
styles: XmlElement,
styleId: string,
type: "paragraph" | "character" | "table",
basedOn?: string
) {
const existing = findStyle(styles, styleId);
if (existing) {
return existing;
}
const style = appendElement(styles, WORD_NAMESPACE, "w:style", {
"w:type": type,
"w:customStyle": "1",
"w:styleId": styleId
});
appendElement(style, WORD_NAMESPACE, "w:name", {
"w:val": styleId.replace(/([a-z])([A-Z])/gu, "$1 $2")
});
if (basedOn) {
appendElement(style, WORD_NAMESPACE, "w:basedOn", {
"w:val": basedOn
});
}
return style;
}
function setWordAttribute(
element: XmlElement,
name: string,
value: string
) {
element.setAttributeNS(WORD_NAMESPACE, `w:${name}`, value);
}
function toggleProperty(
parent: XmlElement,
localName: string,
enabled: boolean | undefined
) {
if (enabled === undefined) {
return;
}
removeDirectChildren(parent, WORD_NAMESPACE, localName);
if (enabled) {
appendElement(parent, WORD_NAMESPACE, `w:${localName}`);
}
}
function fallbackFontsForSlot(
slot: DocxStyleSlotName,
fallback: ResolvedDocxThemeStyle
) {
if (slot.startsWith("heading-") || slot.endsWith("-title")) {
return fallback.headings.fonts;
}
if (slot === "inline-code" || slot === "code-block") {
return fallback.code.fonts;
}
if (
slot === "table" ||
slot === "table-header" ||
slot === "table-cell"
) {
return fallback.table.fonts;
}
if (slot === "caption") {
return fallback.caption.fonts;
}
return fallback.body.fonts;
}
function fallbackFontSizeForSlot(
slot: DocxStyleSlotName,
fallback: ResolvedDocxThemeStyle
) {
const headingMatch = /^heading-([1-6])$/u.exec(slot);
if (headingMatch) {
return fallback.headings.sizesPt[
Number.parseInt(headingMatch[1]!, 10) - 1
]!;
}
if (slot === "document-title") {
return fallback.headings.sizesPt[0] + 4;
}
if (slot.endsWith("-title")) {
return fallback.headings.sizesPt[0];
}
if (slot === "inline-code" || slot === "code-block") {
return fallback.code.sizePt;
}
if (
slot === "table" ||
slot === "table-header" ||
slot === "table-cell"
) {
return fallback.table.sizePt;
}
if (slot === "caption") {
return fallback.caption.sizePt;
}
return fallback.body.sizePt;
}
function applyTokenRunStyle(
parent: XmlElement,
token: DocxSlotStyleToken,
fallbackFonts: DocxFontFamily
) {
if (token.fontCandidates.length) {
setFonts(parent, resolveTokenFonts(token, fallbackFonts));
}
if (token.fontSizePt !== undefined) {
removeDirectChildren(parent, WORD_NAMESPACE, "sz", "szCs");
const size = pointsToHalfPoints(token.fontSizePt);
appendElement(parent, WORD_NAMESPACE, "w:sz", {
"w:val": size
});
appendElement(parent, WORD_NAMESPACE, "w:szCs", {
"w:val": size
});
}
if (token.color !== undefined) {
removeDirectChildren(parent, WORD_NAMESPACE, "color");
appendElement(parent, WORD_NAMESPACE, "w:color", {
"w:val": colorValue(token.color)
});
}
if (token.backgroundColor !== undefined) {
removeDirectChildren(parent, WORD_NAMESPACE, "shd");
appendElement(parent, WORD_NAMESPACE, "w:shd", {
"w:val": "clear",
"w:color": "auto",
"w:fill": colorValue(token.backgroundColor)
});
}
if (token.underline !== undefined) {
removeDirectChildren(parent, WORD_NAMESPACE, "u");
if (token.underline) {
appendElement(parent, WORD_NAMESPACE, "w:u", {
"w:val": "single"
});
}
}
if (token.letterSpacingPt !== undefined) {
removeDirectChildren(parent, WORD_NAMESPACE, "spacing");
appendElement(parent, WORD_NAMESPACE, "w:spacing", {
"w:val": pointsToTwips(token.letterSpacingPt)
});
}
toggleProperty(parent, "b", token.bold);
toggleProperty(parent, "bCs", token.bold);
toggleProperty(parent, "i", token.italic);
toggleProperty(parent, "iCs", token.italic);
toggleProperty(parent, "strike", token.strikethrough);
toggleProperty(parent, "vanish", token.hidden);
}
function applyTokenBorders(
parent: XmlElement,
token: DocxSlotStyleToken
) {
if (!token.borders) {
return;
}
removeDirectChildren(parent, WORD_NAMESPACE, "pBdr");
const borders = appendElement(
parent,
WORD_NAMESPACE,
"w:pBdr"
);
for (const side of [
"top",
"right",
"bottom",
"left"
] as const) {
const border = token.borders[side];
if (!border) {
continue;
}
appendElement(borders, WORD_NAMESPACE, `w:${side}`, {
"w:val": border.style,
"w:sz": String(
Math.max(0, Math.min(160, Math.round(border.widthPt * 8)))
),
"w:space": String(
Math.max(
0,
Math.min(
31,
Math.round(token.paddingPt?.[side] ?? 0)
)
)
),
"w:color": colorValue(border.color)
});
}
}
function applyTokenParagraphStyle(
parent: XmlElement,
token: DocxSlotStyleToken,
fallbackFontSizePt: number
) {
if (
token.spacingBeforePt !== undefined ||
token.spacingAfterPt !== undefined ||
token.lineSpacing !== undefined
) {
const spacing = ensureDirectElement(
parent,
WORD_NAMESPACE,
"w:spacing"
);
if (token.spacingBeforePt !== undefined) {
setWordAttribute(
spacing,
"before",
pointsToTwips(token.spacingBeforePt)
);
}
if (token.spacingAfterPt !== undefined) {
setWordAttribute(
spacing,
"after",
pointsToTwips(token.spacingAfterPt)
);
}
if (token.lineSpacing !== undefined) {
setWordAttribute(
spacing,
"line",
pointsToTwips(
(token.fontSizePt ?? fallbackFontSizePt) *
token.lineSpacing
)
);
setWordAttribute(spacing, "lineRule", "exact");
}
}
if (
token.firstLineIndentPt !== undefined ||
token.leftIndentPt !== undefined ||
token.rightIndentPt !== undefined
) {
const indent = ensureDirectElement(
parent,
WORD_NAMESPACE,
"w:ind"
);
if (token.firstLineIndentPt !== undefined) {
setWordAttribute(
indent,
"firstLine",
pointsToTwips(token.firstLineIndentPt)
);
indent.removeAttributeNS(WORD_NAMESPACE, "firstLineChars");
}
if (token.leftIndentPt !== undefined) {
setWordAttribute(
indent,
"left",
pointsToTwips(token.leftIndentPt)
);
indent.removeAttributeNS(WORD_NAMESPACE, "leftChars");
}
if (token.rightIndentPt !== undefined) {
setWordAttribute(
indent,
"right",
pointsToTwips(token.rightIndentPt)
);
}
}
if (token.alignment !== undefined) {
removeDirectChildren(parent, WORD_NAMESPACE, "jc");
appendElement(parent, WORD_NAMESPACE, "w:jc", {
"w:val":
token.alignment === "justify"
? "both"
: token.alignment
});
}
if (token.backgroundColor !== undefined) {
removeDirectChildren(parent, WORD_NAMESPACE, "shd");
appendElement(parent, WORD_NAMESPACE, "w:shd", {
"w:val": "clear",
"w:color": "auto",
"w:fill": colorValue(token.backgroundColor)
});
}
applyTokenBorders(parent, token);
toggleProperty(parent, "keepLines", token.keepLines);
toggleProperty(parent, "keepNext", token.keepWithNext);
toggleProperty(
parent,
"pageBreakBefore",
token.pageBreakBefore
);
}
function applyBodyStyle(
styles: XmlElement,
style: ResolvedDocxThemeStyle
) {
const defaults = firstDirectChild(
styles,
WORD_NAMESPACE,
"docDefaults"
);
if (!defaults) {
throw new Error("word/styles.xml 缺少 w:docDefaults");
}
const defaultRun = ensureDirectElement(
ensureDirectElement(
defaults,
WORD_NAMESPACE,
"w:rPrDefault"
),
WORD_NAMESPACE,
"w:rPr"
);
setRunStyle(defaultRun, {
fonts: style.body.fonts,
sizePt: style.body.sizePt,
color: style.body.color
});
const defaultParagraph = ensureDirectElement(
ensureDirectElement(
defaults,
WORD_NAMESPACE,
"w:pPrDefault"
),
WORD_NAMESPACE,
"w:pPr"
);
setParagraphSpacing(defaultParagraph, {
beforePt: style.body.spacingBeforePt,
afterPt: style.body.spacingAfterPt,
fontSizePt: style.body.sizePt,
lineSpacing: style.body.lineSpacing,
firstLineIndentChars: style.body.firstLineIndentChars
});
for (const styleId of [
"Normal",
"BodyText",
"FirstParagraph",
"FootnoteText",
"Definition",
"Figure"
]) {
const target = ensureStyle(
styles,
styleId,
"paragraph",
styleId === "Normal" ? undefined : "Normal"
);
const pPr = ensureDirectElement(
target,
WORD_NAMESPACE,
"w:pPr"
);
setParagraphSpacing(pPr, {
beforePt: style.body.spacingBeforePt,
afterPt: style.body.spacingAfterPt,
fontSizePt: style.body.sizePt,
lineSpacing: style.body.lineSpacing,
firstLineIndentChars: style.body.firstLineIndentChars
});
const rPr = ensureDirectElement(
target,
WORD_NAMESPACE,
"w:rPr"
);
setRunStyle(rPr, {
fonts: style.body.fonts,
sizePt: style.body.sizePt,
color: style.body.color
});
}
const compact = ensureStyle(
styles,
"Compact",
"paragraph",
"BodyText"
);
setParagraphSpacing(
ensureDirectElement(compact, WORD_NAMESPACE, "w:pPr"),
{
beforePt: 0,
afterPt: 0,
fontSizePt: style.body.sizePt,
lineSpacing: style.body.lineSpacing,
firstLineIndentChars: 0
}
);
}
function applyHeadings(
styles: XmlElement,
style: ResolvedDocxThemeStyle
) {
for (let level = 1; level <= 6; level += 1) {
const sizePt = style.headings.sizesPt[level - 1]!;
for (const styleId of [
`Heading${level}`,
`Heading${level}Char`
]) {
const target = ensureStyle(
styles,
styleId,
styleId.endsWith("Char") ? "character" : "paragraph",
styleId.endsWith("Char")
? "DefaultParagraphFont"
: "Normal"
);
if (!styleId.endsWith("Char")) {
const pPr = ensureDirectElement(
target,
WORD_NAMESPACE,
"w:pPr"
);
setParagraphSpacing(pPr, {
beforePt: style.headings.spacingBeforePt,
afterPt: style.headings.spacingAfterPt,
fontSizePt: sizePt,
lineSpacing: 1.25
});
if (!firstDirectChild(pPr, WORD_NAMESPACE, "keepNext")) {
appendElement(pPr, WORD_NAMESPACE, "w:keepNext");
}
if (!firstDirectChild(pPr, WORD_NAMESPACE, "keepLines")) {
appendElement(pPr, WORD_NAMESPACE, "w:keepLines");
}
}
setRunStyle(
ensureDirectElement(target, WORD_NAMESPACE, "w:rPr"),
{
fonts: style.headings.fonts,
sizePt,
color: style.headings.color,
bold: style.headings.bold
}
);
}
}
for (const [styleId, sizePt] of [
["Title", style.headings.sizesPt[0] + 4],
["TitleChar", style.headings.sizesPt[0] + 4],
["Subtitle", style.headings.sizesPt[1]],
["SubtitleChar", style.headings.sizesPt[1]]
] as const) {
const target = ensureStyle(
styles,
styleId,
styleId.endsWith("Char") ? "character" : "paragraph",
styleId.endsWith("Char")
? "DefaultParagraphFont"
: "Normal"
);
if (!styleId.endsWith("Char")) {
const pPr = ensureDirectElement(
target,
WORD_NAMESPACE,
"w:pPr"
);
setParagraphSpacing(pPr, {
beforePt: 0,
afterPt: style.headings.spacingAfterPt,
fontSizePt: sizePt,
lineSpacing: 1.25
});
removeDirectChildren(pPr, WORD_NAMESPACE, "jc");
appendElement(pPr, WORD_NAMESPACE, "w:jc", {
"w:val": "center"
});
}
setRunStyle(
ensureDirectElement(target, WORD_NAMESPACE, "w:rPr"),
{
fonts: style.headings.fonts,
sizePt,
color: style.headings.color,
bold: styleId.startsWith("Title")
}
);
}
}
function applyCodeAndQuote(
styles: XmlElement,
style: ResolvedDocxThemeStyle
) {
const inlineCode = ensureStyle(
styles,
"VerbatimChar",
"character",
"BodyTextChar"
);
setRunStyle(
ensureDirectElement(inlineCode, WORD_NAMESPACE, "w:rPr"),
{
fonts: style.code.fonts,
sizePt: style.code.sizePt,
color: style.code.color,
backgroundColor: style.code.backgroundColor
}
);
const sourceCode = ensureStyle(
styles,
"SourceCode",
"paragraph",
"Normal"
);
const codePPr = ensureDirectElement(
sourceCode,
WORD_NAMESPACE,
"w:pPr"
);
setParagraphSpacing(codePPr, {
beforePt: 3,
afterPt: 3,
fontSizePt: style.code.sizePt,
lineSpacing: style.code.lineSpacing,
firstLineIndentChars: 0
});
removeDirectChildren(codePPr, WORD_NAMESPACE, "shd", "pBdr");
appendElement(codePPr, WORD_NAMESPACE, "w:shd", {
"w:val": "clear",
"w:color": "auto",
"w:fill": colorValue(style.code.backgroundColor)
});
const codeBorders = appendElement(
codePPr,
WORD_NAMESPACE,
"w:pBdr"
);
for (const side of ["top", "left", "bottom", "right"]) {
appendElement(codeBorders, WORD_NAMESPACE, `w:${side}`, {
"w:val": "single",
"w:sz": "4",
"w:space": "2",
"w:color": colorValue(style.code.borderColor)
});
}
setRunStyle(
ensureDirectElement(sourceCode, WORD_NAMESPACE, "w:rPr"),
{
fonts: style.code.fonts,
sizePt: style.code.sizePt,
color: style.code.color,
backgroundColor: style.code.backgroundColor
}
);
const quote = ensureStyle(
styles,
"BlockText",
"paragraph",
"BodyText"
);
const quotePPr = ensureDirectElement(
quote,
WORD_NAMESPACE,
"w:pPr"
);
setParagraphSpacing(quotePPr, {
beforePt: 6,
afterPt: 6,
fontSizePt: style.body.sizePt,
lineSpacing: style.body.lineSpacing,
firstLineIndentChars: 0
});
removeDirectChildren(quotePPr, WORD_NAMESPACE, "ind", "shd", "pBdr");
appendElement(quotePPr, WORD_NAMESPACE, "w:ind", {
"w:leftChars": String(style.blockQuote.leftIndentChars * 100)
});
appendElement(quotePPr, WORD_NAMESPACE, "w:shd", {
"w:val": "clear",
"w:color": "auto",
"w:fill": colorValue(style.blockQuote.backgroundColor)
});
const quoteBorders = appendElement(
quotePPr,
WORD_NAMESPACE,
"w:pBdr"
);
appendElement(quoteBorders, WORD_NAMESPACE, "w:left", {
"w:val": "single",
"w:sz": "18",
"w:space": "6",
"w:color": colorValue(style.blockQuote.borderColor)
});
setRunStyle(
ensureDirectElement(quote, WORD_NAMESPACE, "w:rPr"),
{
fonts: style.body.fonts,
sizePt: style.body.sizePt,
color: style.blockQuote.color,
italic: style.blockQuote.italic
}
);
}
function applyTableAndCaption(
styles: XmlElement,
style: ResolvedDocxThemeStyle
) {
const table = ensureStyle(styles, "Table", "table", "TableNormal");
const tblPr = ensureDirectElement(
table,
WORD_NAMESPACE,
"w:tblPr"
);
removeDirectChildren(
tblPr,
WORD_NAMESPACE,
"tblW",
"tblCellMar",
"tblBorders"
);
const margins = appendElement(
tblPr,
WORD_NAMESPACE,
"w:tblCellMar"
);
const cellMargin = String(
millimetersToTwips(style.table.cellMarginMm)
);
for (const side of ["top", "left", "bottom", "right"]) {
appendElement(margins, WORD_NAMESPACE, `w:${side}`, {
"w:w": cellMargin,
"w:type": "dxa"
});
}
const borders = appendElement(
tblPr,
WORD_NAMESPACE,
"w:tblBorders"
);
for (const side of [
"top",
"left",
"bottom",
"right",
"insideH",
"insideV"
]) {
appendElement(borders, WORD_NAMESPACE, `w:${side}`, {
"w:val": "single",
"w:sz": "4",
"w:space": "0",
"w:color": colorValue(style.table.borderColor)
});
}
setRunStyle(
ensureDirectElement(table, WORD_NAMESPACE, "w:rPr"),
{
fonts: style.table.fonts,
sizePt: style.table.sizePt,
color: style.table.color
}
);
let firstRow = directChildren(
table,
WORD_NAMESPACE,
"tblStylePr"
).find(
(element) =>
element.getAttributeNS(WORD_NAMESPACE, "type") === "firstRow"
);
if (!firstRow) {
firstRow = appendElement(
table,
WORD_NAMESPACE,
"w:tblStylePr",
{ "w:type": "firstRow" }
);
}
setRunStyle(
ensureDirectElement(firstRow, WORD_NAMESPACE, "w:rPr"),
{
fonts: style.table.fonts,
sizePt: style.table.sizePt,
color: style.table.headerColor,
bold: true
}
);
const cellProperties = ensureDirectElement(
firstRow,
WORD_NAMESPACE,
"w:tcPr"
);
removeDirectChildren(cellProperties, WORD_NAMESPACE, "shd");
appendElement(cellProperties, WORD_NAMESPACE, "w:shd", {
"w:val": "clear",
"w:color": "auto",
"w:fill": colorValue(style.table.headerBackgroundColor)
});
for (const styleId of ["Caption", "TableCaption", "ImageCaption"]) {
const caption = ensureStyle(
styles,
styleId,
"paragraph",
styleId === "Caption" ? "Normal" : "Caption"
);
const pPr = ensureDirectElement(
caption,
WORD_NAMESPACE,
"w:pPr"
);
setParagraphSpacing(pPr, {
beforePt: 3,
afterPt: 6,
fontSizePt: style.caption.sizePt,
lineSpacing: 1.25
});
removeDirectChildren(pPr, WORD_NAMESPACE, "jc");
appendElement(pPr, WORD_NAMESPACE, "w:jc", {
"w:val": style.caption.alignment
});
setRunStyle(
ensureDirectElement(caption, WORD_NAMESPACE, "w:rPr"),
{
fonts: style.caption.fonts,
sizePt: style.caption.sizePt,
color: style.caption.color,
italic: style.caption.italic
}
);
}
const hyperlink = ensureStyle(
styles,
"Hyperlink",
"character",
"BodyTextChar"
);
setRunStyle(
ensureDirectElement(hyperlink, WORD_NAMESPACE, "w:rPr"),
{
color: style.hyperlink.color,
underline: style.hyperlink.underline
}
);
}
function applyTableTokenStyles(
styles: XmlElement,
slots: ReadonlyMap<
DocxStyleSlotName,
{ style: DocxSlotStyleToken }
>,
fallback: ResolvedDocxThemeStyle
) {
const tableToken = slots.get("table")?.style;
const cellToken =
slots.get("table-cell")?.style ?? tableToken;
const headerToken =
slots.get("table-header")?.style ?? cellToken;
if (!tableToken && !cellToken && !headerToken) {
return;
}
const table = ensureStyle(
styles,
"Table",
"table",
"TableNormal"
);
const tblPr = ensureDirectElement(
table,
WORD_NAMESPACE,
"w:tblPr"
);
removeDirectChildren(tblPr, WORD_NAMESPACE, "tblW");
const padding = cellToken?.paddingPt ?? tableToken?.paddingPt;
if (padding) {
removeDirectChildren(tblPr, WORD_NAMESPACE, "tblCellMar");
const margins = appendElement(
tblPr,
WORD_NAMESPACE,
"w:tblCellMar"
);
for (const side of [
"top",
"right",
"bottom",
"left"
] as const) {
appendElement(margins, WORD_NAMESPACE, `w:${side}`, {
"w:w": pointsToTwips(padding[side]),
"w:type": "dxa"
});
}
}
if (tableToken?.borders) {
removeDirectChildren(tblPr, WORD_NAMESPACE, "tblBorders");
const borders = appendElement(
tblPr,
WORD_NAMESPACE,
"w:tblBorders"
);
for (const side of [
"top",
"right",
"bottom",
"left"
] as const) {
const border = tableToken.borders[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)
});
}
}
if (cellToken) {
applyTokenRunStyle(
ensureDirectElement(table, WORD_NAMESPACE, "w:rPr"),
cellToken,
fallback.table.fonts
);
}
let firstRow = directChildren(
table,
WORD_NAMESPACE,
"tblStylePr"
).find(
(element) =>
element.getAttributeNS(WORD_NAMESPACE, "type") === "firstRow"
);
if (!firstRow) {
firstRow = appendElement(
table,
WORD_NAMESPACE,
"w:tblStylePr",
{ "w:type": "firstRow" }
);
}
if (headerToken) {
applyTokenRunStyle(
ensureDirectElement(firstRow, WORD_NAMESPACE, "w:rPr"),
headerToken,
fallback.table.fonts
);
if (headerToken.backgroundColor !== undefined) {
const tcPr = ensureDirectElement(
firstRow,
WORD_NAMESPACE,
"w:tcPr"
);
removeDirectChildren(tcPr, WORD_NAMESPACE, "shd");
appendElement(tcPr, WORD_NAMESPACE, "w:shd", {
"w:val": "clear",
"w:color": "auto",
"w:fill": colorValue(
headerToken.backgroundColor
)
});
}
}
}
function applyTokenStyles(
styles: XmlElement,
tokens: DocxThemeTokenSet,
fallback: ResolvedDocxThemeStyle
) {
const slots = createDocxTokenSlotMap(tokens);
const documentToken = slots.get("document")?.style;
if (documentToken) {
const defaults = firstDirectChild(
styles,
WORD_NAMESPACE,
"docDefaults"
);
if (defaults) {
applyTokenRunStyle(
ensureDirectElement(
ensureDirectElement(
defaults,
WORD_NAMESPACE,
"w:rPrDefault"
),
WORD_NAMESPACE,
"w:rPr"
),
documentToken,
fallback.body.fonts
);
applyTokenParagraphStyle(
ensureDirectElement(
ensureDirectElement(
defaults,
WORD_NAMESPACE,
"w:pPrDefault"
),
WORD_NAMESPACE,
"w:pPr"
),
documentToken,
documentToken.fontSizePt ?? fallback.body.sizePt
);
}
}
for (const [slot, entry] of slots) {
const bindings = DOCX_SLOT_WORD_STYLE_BINDINGS[slot];
if (!bindings) {
continue;
}
for (const binding of bindings) {
const target = ensureStyle(
styles,
binding.styleId,
binding.type,
binding.basedOn
);
if (binding.type === "paragraph") {
applyTokenParagraphStyle(
ensureDirectElement(
target,
WORD_NAMESPACE,
"w:pPr"
),
entry.style,
fallbackFontSizeForSlot(slot, fallback)
);
}
applyTokenRunStyle(
ensureDirectElement(
target,
WORD_NAMESPACE,
"w:rPr"
),
entry.style,
fallbackFontsForSlot(slot, fallback)
);
}
}
applyTableTokenStyles(styles, slots, fallback);
}
export function transformStylesXml(
content: Uint8Array,
style: ResolvedDocxThemeStyle,
tokens?: DocxThemeTokenSet
) {
const document = parseXmlPart(content, "word/styles.xml");
const styles = document.documentElement!;
if (
styles.namespaceURI !== WORD_NAMESPACE ||
styles.localName !== "styles"
) {
throw new Error("word/styles.xml 根元素无效");
}
applyBodyStyle(styles, style);
applyHeadings(styles, style);
applyCodeAndQuote(styles, style);
applyTableAndCaption(styles, style);
if (tokens) {
applyTokenStyles(styles, tokens, style);
}
return serializeXmlPart(document);
}
export function transformFontTableXml(
content: Uint8Array,
style: ResolvedDocxThemeStyle,
tokens?: DocxThemeTokenSet
) {
const document = parseXmlPart(content, "word/fontTable.xml");
const root = document.documentElement!;
const requiredFonts = new Set<string>();
for (const fonts of [
style.body.fonts,
style.headings.fonts,
style.code.fonts,
style.table.fonts,
style.caption.fonts
]) {
requiredFonts.add(fonts.latin);
requiredFonts.add(fonts.eastAsia);
if (fonts.complexScript) {
requiredFonts.add(fonts.complexScript);
}
}
if (tokens) {
for (const font of collectDocxTokenFonts(tokens)) {
requiredFonts.add(font);
}
}
const existing = new Set(
Array.from(
root.getElementsByTagNameNS(WORD_NAMESPACE, "font")
).map((element) => element.getAttributeNS(WORD_NAMESPACE, "name"))
);
for (const name of requiredFonts) {
if (!existing.has(name)) {
const font = appendElement(root, WORD_NAMESPACE, "w:font", {
"w:name": name
});
appendElement(font, WORD_NAMESPACE, "w:family", {
"w:val": name === style.code.fonts.latin ? "modern" : "auto"
});
}
}
return serializeXmlPart(document);
}
export function transformThemeFontsXml(
content: Uint8Array,
style: ResolvedDocxThemeStyle,
tokens?: DocxThemeTokenSet
) {
const document = parseXmlPart(content, "word/theme/theme1.xml");
const slots = tokens
? createDocxTokenSlotMap(tokens)
: undefined;
const headingFonts = resolveTokenFonts(
slots?.get("heading-1")?.style,
style.headings.fonts
);
const bodyFonts = resolveTokenFonts(
slots?.get("paragraph")?.style ??
slots?.get("document")?.style,
style.body.fonts
);
for (const schemeName of ["majorFont", "minorFont"]) {
const scheme = document.getElementsByTagNameNS(
DRAWING_NAMESPACE,
schemeName
)[0];
if (!scheme) {
continue;
}
const latin = scheme.getElementsByTagNameNS(
DRAWING_NAMESPACE,
"latin"
)[0];
const eastAsia = scheme.getElementsByTagNameNS(
DRAWING_NAMESPACE,
"ea"
)[0];
latin?.setAttribute(
"typeface",
schemeName === "majorFont"
? headingFonts.latin
: bodyFonts.latin
);
eastAsia?.setAttribute(
"typeface",
schemeName === "majorFont"
? headingFonts.eastAsia
: bodyFonts.eastAsia
);
}
return serializeXmlPart(document);
}