1210 lines
29 KiB
TypeScript
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);
|
|
}
|