feat: 实现动态 DOCX 模板与样式映射
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
|
||||
import type {
|
||||
Document as XmlDocument,
|
||||
Element as XmlElement
|
||||
} from "@xmldom/xmldom";
|
||||
|
||||
export type { XmlDocument, XmlElement };
|
||||
|
||||
export const WORD_NAMESPACE =
|
||||
"http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
export const OFFICE_RELATIONSHIP_NAMESPACE =
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
||||
export const PACKAGE_RELATIONSHIP_NAMESPACE =
|
||||
"http://schemas.openxmlformats.org/package/2006/relationships";
|
||||
export const CONTENT_TYPES_NAMESPACE =
|
||||
"http://schemas.openxmlformats.org/package/2006/content-types";
|
||||
export const DRAWING_NAMESPACE =
|
||||
"http://schemas.openxmlformats.org/drawingml/2006/main";
|
||||
|
||||
const decoder = new TextDecoder("utf-8", { fatal: true });
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
export function parseXmlPart(
|
||||
content: Uint8Array,
|
||||
partName: string
|
||||
) {
|
||||
try {
|
||||
return new DOMParser({
|
||||
onError: (level, message) => {
|
||||
if (level !== "warning") {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
}).parseFromString(decoder.decode(content), "application/xml");
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`${partName} 不是有效 OOXML:${
|
||||
error instanceof Error ? error.message : "XML 解析失败"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeXmlPart(document: XmlDocument) {
|
||||
return encoder.encode(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n${new XMLSerializer().serializeToString(
|
||||
document.documentElement!
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
export function directChildren(
|
||||
parent: XmlElement,
|
||||
namespace: string,
|
||||
localName: string
|
||||
) {
|
||||
const children: XmlElement[] = [];
|
||||
for (const node of Array.from(parent.childNodes)) {
|
||||
const element = node as XmlElement;
|
||||
if (
|
||||
node.nodeType === 1 &&
|
||||
element.namespaceURI === namespace &&
|
||||
element.localName === localName
|
||||
) {
|
||||
children.push(element);
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
export function firstDirectChild(
|
||||
parent: XmlElement,
|
||||
namespace: string,
|
||||
localName: string
|
||||
) {
|
||||
return directChildren(parent, namespace, localName)[0];
|
||||
}
|
||||
|
||||
export function removeDirectChildren(
|
||||
parent: XmlElement,
|
||||
namespace: string,
|
||||
...localNames: string[]
|
||||
) {
|
||||
for (const node of Array.from(parent.childNodes)) {
|
||||
const element = node as XmlElement;
|
||||
if (
|
||||
node.nodeType === 1 &&
|
||||
element.namespaceURI === namespace &&
|
||||
localNames.includes(element.localName ?? "")
|
||||
) {
|
||||
parent.removeChild(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function appendElement(
|
||||
parent: XmlElement,
|
||||
namespace: string,
|
||||
qualifiedName: string,
|
||||
attributes: Record<string, string> = {}
|
||||
) {
|
||||
const element = parent.ownerDocument!.createElementNS(
|
||||
namespace,
|
||||
qualifiedName
|
||||
);
|
||||
for (const [name, value] of Object.entries(attributes)) {
|
||||
const prefix = name.includes(":") ? name.split(":")[0] : "";
|
||||
const attributeNamespace =
|
||||
prefix === "w"
|
||||
? WORD_NAMESPACE
|
||||
: prefix === "r"
|
||||
? OFFICE_RELATIONSHIP_NAMESPACE
|
||||
: null;
|
||||
element.setAttributeNS(attributeNamespace, name, value);
|
||||
}
|
||||
parent.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
export function ensureDirectElement(
|
||||
parent: XmlElement,
|
||||
namespace: string,
|
||||
qualifiedName: string
|
||||
) {
|
||||
const localName = qualifiedName.split(":").at(-1)!;
|
||||
return (
|
||||
firstDirectChild(parent, namespace, localName) ??
|
||||
appendElement(parent, namespace, qualifiedName)
|
||||
);
|
||||
}
|
||||
|
||||
export function colorValue(value: string) {
|
||||
if (!/^#[0-9a-f]{6}$/iu.test(value)) {
|
||||
throw new Error(`DOCX 颜色值无效:${value}`);
|
||||
}
|
||||
return value.replace(/^#/u, "").toUpperCase();
|
||||
}
|
||||
|
||||
export function pointsToHalfPoints(value: number) {
|
||||
return String(Math.round(value * 2));
|
||||
}
|
||||
|
||||
export function pointsToTwips(value: number) {
|
||||
return String(Math.round(value * 20));
|
||||
}
|
||||
|
||||
export function millimetersToTwips(value: number) {
|
||||
return Math.round((value * 1440) / 25.4);
|
||||
}
|
||||
Reference in New Issue
Block a user