@coding01/docsjs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/index.cjs +1609 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +1569 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1609 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DocsWordElement: () => DocsWordElement,
|
|
34
|
+
WordFidelityEditorReact: () => WordFidelityEditorReact,
|
|
35
|
+
WordFidelityEditorVue: () => WordFidelityEditorVue,
|
|
36
|
+
defineDocsWordElement: () => defineDocsWordElement
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/lib/htmlSnapshot.ts
|
|
41
|
+
var SNAPSHOT_SHELL_START = '<!DOCTYPE html><html><head><meta charset="utf-8"/>';
|
|
42
|
+
var SNAPSHOT_SHELL_END = "</head><body></body></html>";
|
|
43
|
+
function buildHtmlSnapshot(rawHtml) {
|
|
44
|
+
if (!rawHtml.trim()) {
|
|
45
|
+
return `${SNAPSHOT_SHELL_START}${SNAPSHOT_SHELL_END}`;
|
|
46
|
+
}
|
|
47
|
+
const hasHtmlTag = /<html[\s>]/i.test(rawHtml);
|
|
48
|
+
if (hasHtmlTag) {
|
|
49
|
+
return rawHtml;
|
|
50
|
+
}
|
|
51
|
+
return `${SNAPSHOT_SHELL_START}${SNAPSHOT_SHELL_END}`.replace(
|
|
52
|
+
"<body></body>",
|
|
53
|
+
`<body>${rawHtml}</body>`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/lib/docxHtml.ts
|
|
58
|
+
var import_jszip = __toESM(require("jszip"), 1);
|
|
59
|
+
function escapeHtml(text) {
|
|
60
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
61
|
+
}
|
|
62
|
+
function parseXml(xmlText) {
|
|
63
|
+
const parser = new DOMParser();
|
|
64
|
+
return parser.parseFromString(xmlText, "application/xml");
|
|
65
|
+
}
|
|
66
|
+
function queryAllByLocalName(root, localName) {
|
|
67
|
+
return Array.from(root.querySelectorAll("*")).filter((el) => el.localName === localName);
|
|
68
|
+
}
|
|
69
|
+
function queryByLocalName(root, localName) {
|
|
70
|
+
return queryAllByLocalName(root, localName)[0] ?? null;
|
|
71
|
+
}
|
|
72
|
+
function getAttr(node, name) {
|
|
73
|
+
if (!node) return null;
|
|
74
|
+
return node.getAttribute(name);
|
|
75
|
+
}
|
|
76
|
+
function emuToPx(emu) {
|
|
77
|
+
return emu * 96 / 914400;
|
|
78
|
+
}
|
|
79
|
+
function parseDrawingSizePx(drawing) {
|
|
80
|
+
const extentNode = queryAllByLocalName(drawing, "extent").find((node) => {
|
|
81
|
+
const parent = node.parentElement;
|
|
82
|
+
return parent?.localName === "inline" || parent?.localName === "anchor";
|
|
83
|
+
}) ?? null;
|
|
84
|
+
if (!extentNode) {
|
|
85
|
+
return { widthPx: null, heightPx: null };
|
|
86
|
+
}
|
|
87
|
+
const rawCx = getAttr(extentNode, "cx");
|
|
88
|
+
const rawCy = getAttr(extentNode, "cy");
|
|
89
|
+
const cx = rawCx ? Number.parseInt(rawCx, 10) : Number.NaN;
|
|
90
|
+
const cy = rawCy ? Number.parseInt(rawCy, 10) : Number.NaN;
|
|
91
|
+
const widthPx = Number.isFinite(cx) && cx > 0 ? emuToPx(cx) : null;
|
|
92
|
+
const heightPx = Number.isFinite(cy) && cy > 0 ? emuToPx(cy) : null;
|
|
93
|
+
return { widthPx, heightPx };
|
|
94
|
+
}
|
|
95
|
+
function imageDimensionAttributes(sizePx) {
|
|
96
|
+
const attrs = [];
|
|
97
|
+
if (sizePx.widthPx !== null) attrs.push(`width="${Math.round(sizePx.widthPx)}"`);
|
|
98
|
+
if (sizePx.heightPx !== null) attrs.push(`height="${Math.round(sizePx.heightPx)}"`);
|
|
99
|
+
if (sizePx.widthPx !== null || sizePx.heightPx !== null) {
|
|
100
|
+
const style = ["max-width:100%"];
|
|
101
|
+
if (sizePx.widthPx !== null) style.push(`width:${sizePx.widthPx.toFixed(2)}px`);
|
|
102
|
+
if (sizePx.heightPx !== null) style.push(`height:${sizePx.heightPx.toFixed(2)}px`);
|
|
103
|
+
attrs.push(`style="${style.join(";")}"`);
|
|
104
|
+
}
|
|
105
|
+
return attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
|
|
106
|
+
}
|
|
107
|
+
function parseDocRelsMap(relsXmlText) {
|
|
108
|
+
if (!relsXmlText) return {};
|
|
109
|
+
const rels = parseXml(relsXmlText);
|
|
110
|
+
const relationNodes = queryAllByLocalName(rels, "Relationship");
|
|
111
|
+
const map = {};
|
|
112
|
+
for (const rel of relationNodes) {
|
|
113
|
+
const id = getAttr(rel, "Id");
|
|
114
|
+
const target = getAttr(rel, "Target");
|
|
115
|
+
if (!id || !target) continue;
|
|
116
|
+
map[id] = target;
|
|
117
|
+
}
|
|
118
|
+
return map;
|
|
119
|
+
}
|
|
120
|
+
function extToMime(ext) {
|
|
121
|
+
const lower = ext.toLowerCase();
|
|
122
|
+
if (lower === "png") return "image/png";
|
|
123
|
+
if (lower === "jpg" || lower === "jpeg") return "image/jpeg";
|
|
124
|
+
if (lower === "gif") return "image/gif";
|
|
125
|
+
if (lower === "webp") return "image/webp";
|
|
126
|
+
if (lower === "bmp") return "image/bmp";
|
|
127
|
+
if (lower === "svg") return "image/svg+xml";
|
|
128
|
+
return "application/octet-stream";
|
|
129
|
+
}
|
|
130
|
+
async function imageRidToDataUrl(zip, relMap, rid) {
|
|
131
|
+
const relTarget = relMap[rid];
|
|
132
|
+
if (!relTarget) return null;
|
|
133
|
+
const normalized = relTarget.replace(/^\/+/, "");
|
|
134
|
+
const path = normalized.startsWith("word/") ? normalized : `word/${normalized}`;
|
|
135
|
+
const file = zip.file(path);
|
|
136
|
+
if (!file) return null;
|
|
137
|
+
const base64 = await file.async("base64");
|
|
138
|
+
const ext = path.split(".").pop() ?? "bin";
|
|
139
|
+
const mime = extToMime(ext);
|
|
140
|
+
return `data:${mime};base64,${base64}`;
|
|
141
|
+
}
|
|
142
|
+
function runStyleToCss(rPr) {
|
|
143
|
+
if (!rPr) return "";
|
|
144
|
+
const declarations = [];
|
|
145
|
+
if (queryByLocalName(rPr, "b")) declarations.push("font-weight:700");
|
|
146
|
+
if (queryByLocalName(rPr, "i")) declarations.push("font-style:italic");
|
|
147
|
+
if (queryByLocalName(rPr, "u")) declarations.push("text-decoration:underline");
|
|
148
|
+
if (queryByLocalName(rPr, "strike")) declarations.push("text-decoration:line-through");
|
|
149
|
+
const color = queryByLocalName(rPr, "color");
|
|
150
|
+
const colorVal = getAttr(color, "w:val") ?? getAttr(color, "val");
|
|
151
|
+
if (colorVal && colorVal.toLowerCase() !== "auto") declarations.push(`color:#${colorVal}`);
|
|
152
|
+
const highlight = queryByLocalName(rPr, "highlight");
|
|
153
|
+
const highlightVal = (getAttr(highlight, "w:val") ?? getAttr(highlight, "val") ?? "").toLowerCase();
|
|
154
|
+
if (highlightVal === "yellow") declarations.push("background-color:#fff200");
|
|
155
|
+
const vertAlign = queryByLocalName(rPr, "vertAlign");
|
|
156
|
+
const vertVal = (getAttr(vertAlign, "w:val") ?? getAttr(vertAlign, "val") ?? "").toLowerCase();
|
|
157
|
+
if (vertVal === "superscript") declarations.push("vertical-align:super;font-size:0.83em");
|
|
158
|
+
if (vertVal === "subscript") declarations.push("vertical-align:sub;font-size:0.83em");
|
|
159
|
+
return declarations.join(";");
|
|
160
|
+
}
|
|
161
|
+
function paragraphTag(paragraph) {
|
|
162
|
+
const pPr = queryByLocalName(paragraph, "pPr");
|
|
163
|
+
const pStyle = pPr ? queryByLocalName(pPr, "pStyle") : null;
|
|
164
|
+
const val = (getAttr(pStyle, "w:val") ?? getAttr(pStyle, "val") ?? "").toLowerCase();
|
|
165
|
+
if (val.includes("heading1") || val === "1" || val === "heading 1") return "h1";
|
|
166
|
+
if (val.includes("heading2") || val === "2" || val === "heading 2") return "h2";
|
|
167
|
+
if (val.includes("heading3") || val === "3" || val === "heading 3") return "h3";
|
|
168
|
+
return "p";
|
|
169
|
+
}
|
|
170
|
+
function paragraphAlignStyle(paragraph) {
|
|
171
|
+
const pPr = queryByLocalName(paragraph, "pPr");
|
|
172
|
+
const jc = pPr ? queryByLocalName(pPr, "jc") : null;
|
|
173
|
+
const align = (getAttr(jc, "w:val") ?? getAttr(jc, "val") ?? "").toLowerCase();
|
|
174
|
+
if (align === "center" || align === "right" || align === "left") {
|
|
175
|
+
return `text-align:${align};`;
|
|
176
|
+
}
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
function paragraphDataAttr(paragraphIndex) {
|
|
180
|
+
return paragraphIndex === null ? "" : ` data-word-p-index="${paragraphIndex}"`;
|
|
181
|
+
}
|
|
182
|
+
async function paragraphToHtml(zip, relMap, paragraph, paragraphIndex) {
|
|
183
|
+
const tag = paragraphTag(paragraph);
|
|
184
|
+
const alignStyle = paragraphAlignStyle(paragraph);
|
|
185
|
+
const dataAttr = paragraphDataAttr(paragraphIndex);
|
|
186
|
+
const runs = queryAllByLocalName(paragraph, "r");
|
|
187
|
+
if (runs.length === 0) {
|
|
188
|
+
return `<${tag}${dataAttr}${alignStyle ? ` style="${alignStyle}"` : ""}><br/></${tag}>`;
|
|
189
|
+
}
|
|
190
|
+
const parts = [];
|
|
191
|
+
for (const run of runs) {
|
|
192
|
+
const rPr = queryByLocalName(run, "rPr");
|
|
193
|
+
const css = runStyleToCss(rPr);
|
|
194
|
+
const drawing = queryByLocalName(run, "drawing");
|
|
195
|
+
if (drawing) {
|
|
196
|
+
const blip = queryByLocalName(drawing, "blip");
|
|
197
|
+
const rid = getAttr(blip, "r:embed") ?? getAttr(blip, "embed");
|
|
198
|
+
if (rid) {
|
|
199
|
+
const src = await imageRidToDataUrl(zip, relMap, rid);
|
|
200
|
+
if (src) {
|
|
201
|
+
const imageSize = parseDrawingSizePx(drawing);
|
|
202
|
+
const dimensionAttrs = imageDimensionAttributes(imageSize);
|
|
203
|
+
parts.push(`<img src="${src}" alt="word-image"${dimensionAttrs}/>`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const texts = queryAllByLocalName(run, "t").map((t) => t.textContent ?? "").join("");
|
|
209
|
+
const brs = queryAllByLocalName(run, "br").length;
|
|
210
|
+
const runText2 = `${escapeHtml(texts)}${"<br/>".repeat(brs)}`;
|
|
211
|
+
if (!runText2) continue;
|
|
212
|
+
if (css) {
|
|
213
|
+
parts.push(`<span style="${css}">${runText2}</span>`);
|
|
214
|
+
} else {
|
|
215
|
+
parts.push(runText2);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const content = parts.join("") || "<br/>";
|
|
219
|
+
return `<${tag}${dataAttr}${alignStyle ? ` style="${alignStyle}"` : ""}>${content}</${tag}>`;
|
|
220
|
+
}
|
|
221
|
+
function runText(run) {
|
|
222
|
+
const text = queryAllByLocalName(run, "t").map((t) => t.textContent ?? "").join("");
|
|
223
|
+
const brCount = queryAllByLocalName(run, "br").length;
|
|
224
|
+
return `${escapeHtml(text)}${"<br/>".repeat(brCount)}`;
|
|
225
|
+
}
|
|
226
|
+
function paragraphText(paragraph) {
|
|
227
|
+
const runs = queryAllByLocalName(paragraph, "r");
|
|
228
|
+
const content = runs.map((run) => runText(run)).join("");
|
|
229
|
+
return content || "<br/>";
|
|
230
|
+
}
|
|
231
|
+
function tableCellHtml(cell, paragraphIndexMap) {
|
|
232
|
+
const paragraphs = queryAllByLocalName(cell, "p");
|
|
233
|
+
if (paragraphs.length === 0) {
|
|
234
|
+
const text = queryAllByLocalName(cell, "t").map((t) => t.textContent ?? "").join("").trim();
|
|
235
|
+
return escapeHtml(text) || "<br/>";
|
|
236
|
+
}
|
|
237
|
+
return paragraphs.map((p) => {
|
|
238
|
+
const paragraphIndex = paragraphIndexMap.get(p) ?? null;
|
|
239
|
+
return `<p${paragraphDataAttr(paragraphIndex)}>${paragraphText(p)}</p>`;
|
|
240
|
+
}).join("");
|
|
241
|
+
}
|
|
242
|
+
function tableToHtml(table, paragraphIndexMap) {
|
|
243
|
+
const rows = queryAllByLocalName(table, "tr");
|
|
244
|
+
const htmlRows = rows.map((row) => {
|
|
245
|
+
const cells = queryAllByLocalName(row, "tc");
|
|
246
|
+
const htmlCells = cells.map((cell) => `<td style="border:1px solid #222;vertical-align:top;">${tableCellHtml(cell, paragraphIndexMap)}</td>`).join("");
|
|
247
|
+
return `<tr>${htmlCells}</tr>`;
|
|
248
|
+
});
|
|
249
|
+
return `<table style="border-collapse:collapse;table-layout:fixed;width:100%;border:1px solid #222;">${htmlRows.join("")}</table>`;
|
|
250
|
+
}
|
|
251
|
+
async function parseDocxToHtmlSnapshot(file) {
|
|
252
|
+
const maybeArrayBuffer = file.arrayBuffer;
|
|
253
|
+
const buffer = maybeArrayBuffer ? await maybeArrayBuffer.call(file) : await new Response(file).arrayBuffer();
|
|
254
|
+
const zip = await import_jszip.default.loadAsync(buffer);
|
|
255
|
+
const documentXmlText = await zip.file("word/document.xml")?.async("string");
|
|
256
|
+
if (!documentXmlText) {
|
|
257
|
+
throw new Error("DOCX missing document.xml");
|
|
258
|
+
}
|
|
259
|
+
const relsText = await zip.file("word/_rels/document.xml.rels")?.async("string");
|
|
260
|
+
const relMap = parseDocRelsMap(relsText ?? null);
|
|
261
|
+
const documentXml = parseXml(documentXmlText);
|
|
262
|
+
const body = queryByLocalName(documentXml, "body");
|
|
263
|
+
if (!body) {
|
|
264
|
+
throw new Error("DOCX missing body");
|
|
265
|
+
}
|
|
266
|
+
const paragraphIndexMap = /* @__PURE__ */ new Map();
|
|
267
|
+
queryAllByLocalName(documentXml, "p").forEach((paragraph, index) => {
|
|
268
|
+
paragraphIndexMap.set(paragraph, index);
|
|
269
|
+
});
|
|
270
|
+
const blockHtml = [];
|
|
271
|
+
for (const child of Array.from(body.children)) {
|
|
272
|
+
if (child.localName === "sectPr") continue;
|
|
273
|
+
if (child.localName === "p") {
|
|
274
|
+
const paragraphIndex = paragraphIndexMap.get(child) ?? null;
|
|
275
|
+
blockHtml.push(await paragraphToHtml(zip, relMap, child, paragraphIndex));
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (child.localName === "tbl") {
|
|
279
|
+
blockHtml.push(tableToHtml(child, paragraphIndexMap));
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return buildHtmlSnapshot(blockHtml.join("\n"));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/lib/pastePipeline.ts
|
|
287
|
+
function escapeAttr(value) {
|
|
288
|
+
return value.replaceAll("&", "&").replaceAll('"', """);
|
|
289
|
+
}
|
|
290
|
+
async function fileToDataUrl(file) {
|
|
291
|
+
const buffer = await new Response(file).arrayBuffer();
|
|
292
|
+
const bytes = new Uint8Array(buffer);
|
|
293
|
+
let binary = "";
|
|
294
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
295
|
+
const base64 = btoa(binary);
|
|
296
|
+
const mime = file.type || "application/octet-stream";
|
|
297
|
+
return `data:${mime};base64,${base64}`;
|
|
298
|
+
}
|
|
299
|
+
function isUnstableImageSrc(src) {
|
|
300
|
+
const normalized = src.trim().toLowerCase();
|
|
301
|
+
return normalized.startsWith("file:") || normalized.startsWith("blob:") || normalized.startsWith("cid:") || normalized.startsWith("mhtml:") || normalized.startsWith("ms-appx:") || normalized.startsWith("ms-appdata:");
|
|
302
|
+
}
|
|
303
|
+
async function replaceUnstableImageSrc(rawHtml, imageFiles) {
|
|
304
|
+
if (!rawHtml.trim() || imageFiles.length === 0) return rawHtml;
|
|
305
|
+
const parser = new DOMParser();
|
|
306
|
+
const doc = parser.parseFromString(rawHtml, "text/html");
|
|
307
|
+
const images = Array.from(doc.querySelectorAll("img"));
|
|
308
|
+
const replacementDataUrls = await Promise.all(imageFiles.map((file) => fileToDataUrl(file)));
|
|
309
|
+
let replacementIndex = 0;
|
|
310
|
+
for (const img of images) {
|
|
311
|
+
const src = img.getAttribute("src") ?? "";
|
|
312
|
+
if (isUnstableImageSrc(src) && replacementIndex < replacementDataUrls.length) {
|
|
313
|
+
img.setAttribute("src", replacementDataUrls[replacementIndex]);
|
|
314
|
+
replacementIndex += 1;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return doc.body.innerHTML;
|
|
318
|
+
}
|
|
319
|
+
async function buildImageOnlyHtml(imageFiles) {
|
|
320
|
+
if (imageFiles.length === 0) return "";
|
|
321
|
+
const urls = await Promise.all(imageFiles.map((f) => fileToDataUrl(f)));
|
|
322
|
+
return urls.map((url) => `<p><img src="${escapeAttr(url)}" alt="clipboard-image" /></p>`).join("\n");
|
|
323
|
+
}
|
|
324
|
+
async function extractFromClipboardDataTransfer(dataTransfer) {
|
|
325
|
+
const html = dataTransfer.getData("text/html") || "";
|
|
326
|
+
const text = dataTransfer.getData("text/plain") || "";
|
|
327
|
+
const imageFiles = Array.from(dataTransfer.items).filter((item) => item.kind === "file" && item.type.startsWith("image/")).map((item) => item.getAsFile()).filter((file) => file !== null);
|
|
328
|
+
const hydratedHtml = await replaceUnstableImageSrc(html, imageFiles);
|
|
329
|
+
const finalHtml = hydratedHtml.trim() ? hydratedHtml : await buildImageOnlyHtml(imageFiles);
|
|
330
|
+
return {
|
|
331
|
+
html: finalHtml,
|
|
332
|
+
text,
|
|
333
|
+
imageFiles
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
async function extractFromClipboardItems(items) {
|
|
337
|
+
let html = "";
|
|
338
|
+
let text = "";
|
|
339
|
+
const imageFiles = [];
|
|
340
|
+
for (const item of items) {
|
|
341
|
+
if (item.types.includes("text/html") && !html) {
|
|
342
|
+
const blob = await item.getType("text/html");
|
|
343
|
+
html = await blob.text();
|
|
344
|
+
}
|
|
345
|
+
if (item.types.includes("text/plain") && !text) {
|
|
346
|
+
const blob = await item.getType("text/plain");
|
|
347
|
+
text = await blob.text();
|
|
348
|
+
}
|
|
349
|
+
for (const type of item.types) {
|
|
350
|
+
if (!type.startsWith("image/")) continue;
|
|
351
|
+
const blob = await item.getType(type);
|
|
352
|
+
const name = `clipboard-${Date.now()}-${imageFiles.length}.${type.split("/")[1] ?? "bin"}`;
|
|
353
|
+
imageFiles.push(new File([blob], name, { type }));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const hydratedHtml = await replaceUnstableImageSrc(html, imageFiles);
|
|
357
|
+
const finalHtml = hydratedHtml.trim() ? hydratedHtml : await buildImageOnlyHtml(imageFiles);
|
|
358
|
+
return {
|
|
359
|
+
html: finalHtml,
|
|
360
|
+
text,
|
|
361
|
+
imageFiles
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/lib/htmlCompat.ts
|
|
366
|
+
function parseStyle(styleText) {
|
|
367
|
+
const map = /* @__PURE__ */ new Map();
|
|
368
|
+
for (const seg of styleText.split(";")) {
|
|
369
|
+
const [rawKey, ...rawValue] = seg.split(":");
|
|
370
|
+
const key = rawKey?.trim().toLowerCase();
|
|
371
|
+
const value = rawValue.join(":").trim();
|
|
372
|
+
if (!key || !value) continue;
|
|
373
|
+
map.set(key, value);
|
|
374
|
+
}
|
|
375
|
+
return map;
|
|
376
|
+
}
|
|
377
|
+
function serializeStyle(styleMap) {
|
|
378
|
+
return Array.from(styleMap.entries()).map(([k, v]) => `${k}: ${v}`).join("; ");
|
|
379
|
+
}
|
|
380
|
+
function applyMsoMappings(styleMap) {
|
|
381
|
+
const mappingPairs = [
|
|
382
|
+
["mso-ansi-font-size", "font-size"],
|
|
383
|
+
["mso-bidi-font-size", "font-size"],
|
|
384
|
+
["mso-hansi-font-size", "font-size"],
|
|
385
|
+
["mso-margin-top-alt", "margin-top"],
|
|
386
|
+
["mso-margin-bottom-alt", "margin-bottom"],
|
|
387
|
+
["mso-margin-left-alt", "margin-left"],
|
|
388
|
+
["mso-margin-right-alt", "margin-right"],
|
|
389
|
+
["mso-line-height-alt", "line-height"]
|
|
390
|
+
];
|
|
391
|
+
for (const [msoKey, cssKey] of mappingPairs) {
|
|
392
|
+
const value = styleMap.get(msoKey);
|
|
393
|
+
if (!value) continue;
|
|
394
|
+
if (!styleMap.has(cssKey)) {
|
|
395
|
+
styleMap.set(cssKey, value);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const msoFore = styleMap.get("mso-foreground");
|
|
399
|
+
if (msoFore && !styleMap.has("color")) {
|
|
400
|
+
styleMap.set("color", msoFore);
|
|
401
|
+
}
|
|
402
|
+
if (styleMap.get("mso-table-lspace") && !styleMap.has("margin-left")) {
|
|
403
|
+
styleMap.set("margin-left", "0");
|
|
404
|
+
}
|
|
405
|
+
if (styleMap.get("mso-table-rspace") && !styleMap.has("margin-right")) {
|
|
406
|
+
styleMap.set("margin-right", "0");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function applyListFallback(el, styleMap) {
|
|
410
|
+
const className = (el.getAttribute("class") ?? "").toLowerCase();
|
|
411
|
+
const msoList = styleMap.get("mso-list") ?? "";
|
|
412
|
+
const maybeList = className.includes("msolist") || msoList.length > 0;
|
|
413
|
+
if (!maybeList) return;
|
|
414
|
+
if (!styleMap.has("text-indent")) {
|
|
415
|
+
styleMap.set("text-indent", "0");
|
|
416
|
+
}
|
|
417
|
+
const marginLeft = styleMap.get("margin-left");
|
|
418
|
+
if (marginLeft && !styleMap.has("padding-left")) {
|
|
419
|
+
styleMap.set("padding-left", marginLeft);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function applyTagSpecificFallback(el, styleMap) {
|
|
423
|
+
const tag = el.tagName.toLowerCase();
|
|
424
|
+
if (tag === "table") {
|
|
425
|
+
if (!styleMap.has("border-collapse")) {
|
|
426
|
+
styleMap.set("border-collapse", "collapse");
|
|
427
|
+
}
|
|
428
|
+
if (!styleMap.has("border-spacing")) {
|
|
429
|
+
styleMap.set("border-spacing", "0");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if ((tag === "td" || tag === "th") && !styleMap.has("vertical-align")) {
|
|
433
|
+
styleMap.set("vertical-align", "top");
|
|
434
|
+
}
|
|
435
|
+
if (tag === "p" && !styleMap.has("min-height")) {
|
|
436
|
+
styleMap.set("min-height", "1em");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function normalizeWordEmptyParagraphs(doc) {
|
|
440
|
+
const paragraphs = Array.from(doc.querySelectorAll("p"));
|
|
441
|
+
for (const p of paragraphs) {
|
|
442
|
+
const text = (p.textContent ?? "").replace(/\u00a0/g, " ").trim();
|
|
443
|
+
const hasVisibleChildren = p.querySelector("img,table,svg,canvas,br") !== null;
|
|
444
|
+
if (!hasVisibleChildren && text.length === 0) {
|
|
445
|
+
p.innerHTML = "<br/>";
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function applyGlobalDocFixes(doc, options) {
|
|
450
|
+
const body = doc.body;
|
|
451
|
+
if (!body) return;
|
|
452
|
+
if (options?.forceBodyFontFamily) {
|
|
453
|
+
body.style.fontFamily = options.forceBodyFontFamily;
|
|
454
|
+
}
|
|
455
|
+
if (options?.forceHeadingFontFamily) {
|
|
456
|
+
const headings = Array.from(doc.querySelectorAll("h1,h2,h3,h4,h5,h6"));
|
|
457
|
+
for (const heading of headings) {
|
|
458
|
+
heading.style.fontFamily = options.forceHeadingFontFamily;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function applyWordHtmlCompatibility(doc, options) {
|
|
463
|
+
const elements = Array.from(doc.querySelectorAll("[style], p, table, td, th, li, div, span"));
|
|
464
|
+
for (const el of elements) {
|
|
465
|
+
const htmlEl = el;
|
|
466
|
+
const rawStyle = htmlEl.getAttribute("style") ?? "";
|
|
467
|
+
const styleMap = parseStyle(rawStyle);
|
|
468
|
+
applyMsoMappings(styleMap);
|
|
469
|
+
applyListFallback(htmlEl, styleMap);
|
|
470
|
+
applyTagSpecificFallback(htmlEl, styleMap);
|
|
471
|
+
if (styleMap.size > 0) {
|
|
472
|
+
htmlEl.setAttribute("style", serializeStyle(styleMap));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
normalizeWordEmptyParagraphs(doc);
|
|
476
|
+
applyGlobalDocFixes(doc, options);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/lib/styleProfile.ts
|
|
480
|
+
var import_jszip2 = __toESM(require("jszip"), 1);
|
|
481
|
+
var FALLBACK_PROFILE = {
|
|
482
|
+
bodyFontPx: 14.6667,
|
|
483
|
+
bodyLineHeightRatio: 1.158333,
|
|
484
|
+
bodyLineHeightPx: null,
|
|
485
|
+
bodyLineHeightRule: "auto",
|
|
486
|
+
paragraphAfterPx: 10.67,
|
|
487
|
+
contentWidthPx: 553.73,
|
|
488
|
+
pageHeightPx: 1122.53,
|
|
489
|
+
pageMarginTopPx: 96,
|
|
490
|
+
pageMarginBottomPx: 96,
|
|
491
|
+
titleFontPx: 32,
|
|
492
|
+
titleColor: "#0F4761",
|
|
493
|
+
titleAlign: "center",
|
|
494
|
+
bodyFontFamily: '"Times New Roman", "Noto Serif SC", serif',
|
|
495
|
+
titleFontFamily: 'DengXian, "Noto Sans SC", "Microsoft YaHei", sans-serif',
|
|
496
|
+
discoveredFonts: [],
|
|
497
|
+
tableCellPaddingTopPx: 0,
|
|
498
|
+
tableCellPaddingLeftPx: 7.2,
|
|
499
|
+
tableCellPaddingBottomPx: 0,
|
|
500
|
+
tableCellPaddingRightPx: 7.2,
|
|
501
|
+
paragraphProfiles: [],
|
|
502
|
+
trailingDateText: null,
|
|
503
|
+
trailingDateAlignedRight: false,
|
|
504
|
+
trailingDateParagraphIndex: null,
|
|
505
|
+
trailingEmptyParagraphCountBeforeDate: 0
|
|
506
|
+
};
|
|
507
|
+
function createFallbackWordStyleProfile(sourceFileName = "snapshot") {
|
|
508
|
+
return {
|
|
509
|
+
sourceFileName,
|
|
510
|
+
...FALLBACK_PROFILE,
|
|
511
|
+
paragraphProfiles: []
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function twipToPx(twip) {
|
|
515
|
+
return twip / 15;
|
|
516
|
+
}
|
|
517
|
+
function getAttr2(node, attr) {
|
|
518
|
+
if (!node) return null;
|
|
519
|
+
return node.getAttribute(attr);
|
|
520
|
+
}
|
|
521
|
+
function getTwipAttr(node, attr) {
|
|
522
|
+
const raw = getAttr2(node, attr);
|
|
523
|
+
if (!raw) return null;
|
|
524
|
+
const parsed = Number.parseFloat(raw);
|
|
525
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
526
|
+
}
|
|
527
|
+
function queryByLocalName2(root, localName) {
|
|
528
|
+
const all = Array.from(root.querySelectorAll("*"));
|
|
529
|
+
return all.find((el) => el.localName === localName) ?? null;
|
|
530
|
+
}
|
|
531
|
+
function queryAllByLocalName2(root, localName) {
|
|
532
|
+
const all = Array.from(root.querySelectorAll("*"));
|
|
533
|
+
return all.filter((el) => el.localName === localName);
|
|
534
|
+
}
|
|
535
|
+
function parseXml2(xmlText) {
|
|
536
|
+
const parser = new DOMParser();
|
|
537
|
+
return parser.parseFromString(xmlText, "application/xml");
|
|
538
|
+
}
|
|
539
|
+
function parsePageGeometry(documentXml) {
|
|
540
|
+
const sectPr = queryByLocalName2(documentXml, "sectPr");
|
|
541
|
+
if (!sectPr) {
|
|
542
|
+
return {
|
|
543
|
+
contentWidthPx: null,
|
|
544
|
+
pageHeightPx: null,
|
|
545
|
+
marginTopPx: null,
|
|
546
|
+
marginBottomPx: null
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
const pgSz = queryAllByLocalName2(sectPr, "pgSz")[0] ?? null;
|
|
550
|
+
const pgMar = queryAllByLocalName2(sectPr, "pgMar")[0] ?? null;
|
|
551
|
+
const pageW = getTwipAttr(pgSz, "w:w") ?? getTwipAttr(pgSz, "w") ?? null;
|
|
552
|
+
const pageH = getTwipAttr(pgSz, "w:h") ?? getTwipAttr(pgSz, "h") ?? null;
|
|
553
|
+
const left = getTwipAttr(pgMar, "w:left") ?? getTwipAttr(pgMar, "left") ?? 0;
|
|
554
|
+
const right = getTwipAttr(pgMar, "w:right") ?? getTwipAttr(pgMar, "right") ?? 0;
|
|
555
|
+
const top = getTwipAttr(pgMar, "w:top") ?? getTwipAttr(pgMar, "top") ?? null;
|
|
556
|
+
const bottom = getTwipAttr(pgMar, "w:bottom") ?? getTwipAttr(pgMar, "bottom") ?? null;
|
|
557
|
+
return {
|
|
558
|
+
contentWidthPx: pageW === null ? null : twipToPx(pageW - left - right),
|
|
559
|
+
pageHeightPx: pageH === null ? null : twipToPx(pageH),
|
|
560
|
+
marginTopPx: top === null ? null : twipToPx(top),
|
|
561
|
+
marginBottomPx: bottom === null ? null : twipToPx(bottom)
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function parseHeadingAlignFromDocument(documentXml) {
|
|
565
|
+
const paragraphs = queryAllByLocalName2(documentXml, "p");
|
|
566
|
+
for (const paragraph of paragraphs) {
|
|
567
|
+
const pPr = queryAllByLocalName2(paragraph, "pPr")[0] ?? null;
|
|
568
|
+
if (!pPr) continue;
|
|
569
|
+
const pStyle = queryAllByLocalName2(pPr, "pStyle")[0] ?? null;
|
|
570
|
+
const styleVal = getAttr2(pStyle, "w:val") ?? getAttr2(pStyle, "val") ?? "";
|
|
571
|
+
const isHeading = styleVal === "1" || styleVal.toLowerCase().includes("heading");
|
|
572
|
+
if (!isHeading) continue;
|
|
573
|
+
const jc = queryAllByLocalName2(pPr, "jc")[0] ?? null;
|
|
574
|
+
const alignRaw = (getAttr2(jc, "w:val") ?? getAttr2(jc, "val") ?? "").toLowerCase();
|
|
575
|
+
if (alignRaw === "center") return "center";
|
|
576
|
+
if (alignRaw === "right") return "right";
|
|
577
|
+
return "left";
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
function parseParagraphText(paragraph) {
|
|
582
|
+
const textNodes = queryAllByLocalName2(paragraph, "t");
|
|
583
|
+
return textNodes.map((node) => node.textContent ?? "").join("").trim();
|
|
584
|
+
}
|
|
585
|
+
function parseParagraphAlign(paragraph) {
|
|
586
|
+
const pPr = queryAllByLocalName2(paragraph, "pPr")[0] ?? null;
|
|
587
|
+
const jc = pPr ? queryAllByLocalName2(pPr, "jc")[0] ?? null : null;
|
|
588
|
+
const alignRaw = (getAttr2(jc, "w:val") ?? getAttr2(jc, "val") ?? "").toLowerCase();
|
|
589
|
+
if (alignRaw === "center") return "center";
|
|
590
|
+
if (alignRaw === "right") return "right";
|
|
591
|
+
return "left";
|
|
592
|
+
}
|
|
593
|
+
function parseTrailingDateAnchor(documentXml) {
|
|
594
|
+
const paragraphs = queryAllByLocalName2(documentXml, "p");
|
|
595
|
+
if (paragraphs.length === 0) {
|
|
596
|
+
return {
|
|
597
|
+
trailingDateText: null,
|
|
598
|
+
trailingDateAlignedRight: false,
|
|
599
|
+
trailingDateParagraphIndex: null,
|
|
600
|
+
trailingEmptyParagraphCountBeforeDate: 0
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
let lastNonEmptyIndex = -1;
|
|
604
|
+
for (let i = paragraphs.length - 1; i >= 0; i -= 1) {
|
|
605
|
+
if (parseParagraphText(paragraphs[i]).length > 0) {
|
|
606
|
+
lastNonEmptyIndex = i;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (lastNonEmptyIndex < 0) {
|
|
611
|
+
return {
|
|
612
|
+
trailingDateText: null,
|
|
613
|
+
trailingDateAlignedRight: false,
|
|
614
|
+
trailingDateParagraphIndex: null,
|
|
615
|
+
trailingEmptyParagraphCountBeforeDate: 0
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
const dateParagraph = paragraphs[lastNonEmptyIndex];
|
|
619
|
+
const dateText = parseParagraphText(dateParagraph);
|
|
620
|
+
const looksLikeDate = /\d{4}\s*年\s*\d+\s*月\s*\d+\s*日/.test(dateText);
|
|
621
|
+
const align = parseParagraphAlign(dateParagraph);
|
|
622
|
+
let trailingEmpty = 0;
|
|
623
|
+
for (let i = lastNonEmptyIndex - 1; i >= 0; i -= 1) {
|
|
624
|
+
if (parseParagraphText(paragraphs[i]).length === 0) {
|
|
625
|
+
trailingEmpty += 1;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
trailingDateText: looksLikeDate ? dateText : null,
|
|
632
|
+
trailingDateAlignedRight: looksLikeDate && align === "right",
|
|
633
|
+
trailingDateParagraphIndex: looksLikeDate ? lastNonEmptyIndex : null,
|
|
634
|
+
trailingEmptyParagraphCountBeforeDate: looksLikeDate ? trailingEmpty : 0
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function toInt(value) {
|
|
638
|
+
if (!value) return null;
|
|
639
|
+
const n = Number.parseInt(value, 10);
|
|
640
|
+
return Number.isFinite(n) ? n : null;
|
|
641
|
+
}
|
|
642
|
+
function parseNumberingMap(numberingXml) {
|
|
643
|
+
const levelMap = /* @__PURE__ */ new Map();
|
|
644
|
+
if (!numberingXml) return levelMap;
|
|
645
|
+
const abstractMap = /* @__PURE__ */ new Map();
|
|
646
|
+
const abstractNums = queryAllByLocalName2(numberingXml, "abstractNum");
|
|
647
|
+
for (const abs of abstractNums) {
|
|
648
|
+
const absId = toInt(getAttr2(abs, "w:abstractNumId") ?? getAttr2(abs, "abstractNumId"));
|
|
649
|
+
if (absId === null) continue;
|
|
650
|
+
const lvlNodes = queryAllByLocalName2(abs, "lvl");
|
|
651
|
+
const lvlMap = /* @__PURE__ */ new Map();
|
|
652
|
+
for (const lvl of lvlNodes) {
|
|
653
|
+
const ilvl = toInt(getAttr2(lvl, "w:ilvl") ?? getAttr2(lvl, "ilvl"));
|
|
654
|
+
if (ilvl === null) continue;
|
|
655
|
+
const numFmtNode = queryAllByLocalName2(lvl, "numFmt")[0] ?? null;
|
|
656
|
+
const lvlTextNode = queryAllByLocalName2(lvl, "lvlText")[0] ?? null;
|
|
657
|
+
lvlMap.set(ilvl, {
|
|
658
|
+
numFmt: getAttr2(numFmtNode, "w:val") ?? getAttr2(numFmtNode, "val") ?? null,
|
|
659
|
+
lvlText: getAttr2(lvlTextNode, "w:val") ?? getAttr2(lvlTextNode, "val") ?? null,
|
|
660
|
+
startAt: toInt(getAttr2(queryAllByLocalName2(lvl, "start")[0] ?? null, "w:val") ?? getAttr2(queryAllByLocalName2(lvl, "start")[0] ?? null, "val")) ?? 1
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
abstractMap.set(absId, lvlMap);
|
|
664
|
+
}
|
|
665
|
+
const nums = queryAllByLocalName2(numberingXml, "num");
|
|
666
|
+
for (const num of nums) {
|
|
667
|
+
const numId = toInt(getAttr2(num, "w:numId") ?? getAttr2(num, "numId"));
|
|
668
|
+
if (numId === null) continue;
|
|
669
|
+
const abstractRefNode = queryAllByLocalName2(num, "abstractNumId")[0] ?? null;
|
|
670
|
+
const absId = toInt(getAttr2(abstractRefNode, "w:val") ?? getAttr2(abstractRefNode, "val"));
|
|
671
|
+
if (absId === null) continue;
|
|
672
|
+
const lvlMap = abstractMap.get(absId);
|
|
673
|
+
if (!lvlMap) continue;
|
|
674
|
+
for (const [lvl, spec] of lvlMap.entries()) {
|
|
675
|
+
levelMap.set(`${numId}:${lvl}`, spec);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return levelMap;
|
|
679
|
+
}
|
|
680
|
+
function parseParagraphProfiles(documentXml, numberingMap) {
|
|
681
|
+
const paragraphs = queryAllByLocalName2(documentXml, "p");
|
|
682
|
+
return paragraphs.map((paragraph, index) => {
|
|
683
|
+
const text = parseParagraphText(paragraph);
|
|
684
|
+
const pPr = queryAllByLocalName2(paragraph, "pPr")[0] ?? null;
|
|
685
|
+
const spacing = pPr ? queryAllByLocalName2(pPr, "spacing")[0] ?? null : null;
|
|
686
|
+
const ind = pPr ? queryAllByLocalName2(pPr, "ind")[0] ?? null : null;
|
|
687
|
+
const numPr = pPr ? queryAllByLocalName2(pPr, "numPr")[0] ?? null : null;
|
|
688
|
+
const ilvlNode = numPr ? queryAllByLocalName2(numPr, "ilvl")[0] ?? null : null;
|
|
689
|
+
const numIdNode = numPr ? queryAllByLocalName2(numPr, "numId")[0] ?? null : null;
|
|
690
|
+
const listLevel = toInt(getAttr2(ilvlNode, "w:val") ?? getAttr2(ilvlNode, "val"));
|
|
691
|
+
const listNumId = toInt(getAttr2(numIdNode, "w:val") ?? getAttr2(numIdNode, "val"));
|
|
692
|
+
const listSpec = listNumId !== null && listLevel !== null ? numberingMap.get(`${listNumId}:${listLevel}`) : void 0;
|
|
693
|
+
const keepNextNode = pPr ? queryAllByLocalName2(pPr, "keepNext")[0] ?? null : null;
|
|
694
|
+
const keepLinesNode = pPr ? queryAllByLocalName2(pPr, "keepLines")[0] ?? null : null;
|
|
695
|
+
const pageBreakBeforeNode = pPr ? queryAllByLocalName2(pPr, "pageBreakBefore")[0] ?? null : null;
|
|
696
|
+
const renderedPageBreakNode = queryAllByLocalName2(paragraph, "lastRenderedPageBreak")[0] ?? null;
|
|
697
|
+
const sectionBreakNode = pPr ? queryAllByLocalName2(pPr, "sectPr")[0] ?? null : null;
|
|
698
|
+
const before = getTwipAttr(spacing, "w:before") ?? getTwipAttr(spacing, "before") ?? null;
|
|
699
|
+
const after = getTwipAttr(spacing, "w:after") ?? getTwipAttr(spacing, "after") ?? null;
|
|
700
|
+
const line = getTwipAttr(spacing, "w:line") ?? getTwipAttr(spacing, "line") ?? null;
|
|
701
|
+
const rawLineRule = (getAttr2(spacing, "w:lineRule") ?? getAttr2(spacing, "lineRule") ?? "auto").toLowerCase();
|
|
702
|
+
const lineHeightRule = line === null ? null : rawLineRule === "exact" ? "exact" : rawLineRule === "atleast" ? "atLeast" : "auto";
|
|
703
|
+
const left = getTwipAttr(ind, "w:left") ?? getTwipAttr(ind, "left") ?? null;
|
|
704
|
+
const right = getTwipAttr(ind, "w:right") ?? getTwipAttr(ind, "right") ?? null;
|
|
705
|
+
const firstLine = getTwipAttr(ind, "w:firstLine") ?? getTwipAttr(ind, "firstLine") ?? null;
|
|
706
|
+
const hanging = getTwipAttr(ind, "w:hanging") ?? getTwipAttr(ind, "hanging") ?? null;
|
|
707
|
+
const runs = parseRunProfiles(paragraph);
|
|
708
|
+
return {
|
|
709
|
+
index,
|
|
710
|
+
text,
|
|
711
|
+
isEmpty: text.length === 0,
|
|
712
|
+
align: parseParagraphAlign(paragraph),
|
|
713
|
+
beforePx: before === null ? null : twipToPx(before),
|
|
714
|
+
afterPx: after === null ? null : twipToPx(after),
|
|
715
|
+
lineHeightRatio: line === null || lineHeightRule !== "auto" ? null : line / 240,
|
|
716
|
+
lineHeightPx: line === null || lineHeightRule === "auto" ? null : twipToPx(line),
|
|
717
|
+
lineHeightRule,
|
|
718
|
+
indentLeftPx: left === null ? null : twipToPx(left),
|
|
719
|
+
indentRightPx: right === null ? null : twipToPx(right),
|
|
720
|
+
firstLinePx: firstLine === null ? null : twipToPx(firstLine),
|
|
721
|
+
hangingPx: hanging === null ? null : twipToPx(hanging),
|
|
722
|
+
listNumId,
|
|
723
|
+
listLevel,
|
|
724
|
+
listFormat: listSpec?.numFmt ?? null,
|
|
725
|
+
listTextPattern: listSpec?.lvlText ?? null,
|
|
726
|
+
listStartAt: listSpec?.startAt ?? 1,
|
|
727
|
+
keepNext: keepNextNode !== null && (getAttr2(keepNextNode, "w:val") ?? getAttr2(keepNextNode, "val") ?? "1") !== "0",
|
|
728
|
+
keepLines: keepLinesNode !== null && (getAttr2(keepLinesNode, "w:val") ?? getAttr2(keepLinesNode, "val") ?? "1") !== "0",
|
|
729
|
+
pageBreakBefore: renderedPageBreakNode !== null || pageBreakBeforeNode !== null && (getAttr2(pageBreakBeforeNode, "w:val") ?? getAttr2(pageBreakBeforeNode, "val") ?? "1") !== "0",
|
|
730
|
+
sectionBreakBefore: sectionBreakNode !== null,
|
|
731
|
+
runs
|
|
732
|
+
};
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
function parseTableDefaults(stylesXml) {
|
|
736
|
+
const tableStyles = queryAllByLocalName2(stylesXml, "style").filter((style) => {
|
|
737
|
+
const type = (getAttr2(style, "w:type") ?? getAttr2(style, "type") ?? "").toLowerCase();
|
|
738
|
+
return type === "table";
|
|
739
|
+
});
|
|
740
|
+
const targetStyle = tableStyles.find((style) => {
|
|
741
|
+
const styleId = (getAttr2(style, "w:styleId") ?? getAttr2(style, "styleId") ?? "").toLowerCase();
|
|
742
|
+
return styleId === "a1";
|
|
743
|
+
}) ?? tableStyles[0] ?? null;
|
|
744
|
+
if (!targetStyle) {
|
|
745
|
+
return { topPx: null, leftPx: null, bottomPx: null, rightPx: null };
|
|
746
|
+
}
|
|
747
|
+
const tblPr = queryAllByLocalName2(targetStyle, "tblPr")[0] ?? null;
|
|
748
|
+
const tblCellMar = tblPr ? queryAllByLocalName2(tblPr, "tblCellMar")[0] ?? null : null;
|
|
749
|
+
const top = tblCellMar ? queryAllByLocalName2(tblCellMar, "top")[0] ?? null : null;
|
|
750
|
+
const left = tblCellMar ? queryAllByLocalName2(tblCellMar, "left")[0] ?? null : null;
|
|
751
|
+
const bottom = tblCellMar ? queryAllByLocalName2(tblCellMar, "bottom")[0] ?? null : null;
|
|
752
|
+
const right = tblCellMar ? queryAllByLocalName2(tblCellMar, "right")[0] ?? null : null;
|
|
753
|
+
return {
|
|
754
|
+
topPx: (() => {
|
|
755
|
+
const v = getTwipAttr(top, "w:w") ?? getTwipAttr(top, "w") ?? null;
|
|
756
|
+
return v === null ? null : twipToPx(v);
|
|
757
|
+
})(),
|
|
758
|
+
leftPx: (() => {
|
|
759
|
+
const v = getTwipAttr(left, "w:w") ?? getTwipAttr(left, "w") ?? null;
|
|
760
|
+
return v === null ? null : twipToPx(v);
|
|
761
|
+
})(),
|
|
762
|
+
bottomPx: (() => {
|
|
763
|
+
const v = getTwipAttr(bottom, "w:w") ?? getTwipAttr(bottom, "w") ?? null;
|
|
764
|
+
return v === null ? null : twipToPx(v);
|
|
765
|
+
})(),
|
|
766
|
+
rightPx: (() => {
|
|
767
|
+
const v = getTwipAttr(right, "w:w") ?? getTwipAttr(right, "w") ?? null;
|
|
768
|
+
return v === null ? null : twipToPx(v);
|
|
769
|
+
})()
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function parseRunProfiles(paragraph) {
|
|
773
|
+
const runNodes = queryAllByLocalName2(paragraph, "r");
|
|
774
|
+
return runNodes.map((run) => {
|
|
775
|
+
const rPr = queryAllByLocalName2(run, "rPr")[0] ?? null;
|
|
776
|
+
const textNodes = queryAllByLocalName2(run, "t");
|
|
777
|
+
const breakNodes = queryAllByLocalName2(run, "br");
|
|
778
|
+
let text = textNodes.map((node) => node.textContent ?? "").join("");
|
|
779
|
+
if (breakNodes.length > 0) {
|
|
780
|
+
text += "\n".repeat(breakNodes.length);
|
|
781
|
+
}
|
|
782
|
+
if (!text) {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
const sz = rPr ? queryAllByLocalName2(rPr, "sz")[0] ?? null : null;
|
|
786
|
+
const halfPoints = getTwipAttr(sz, "w:val") ?? getTwipAttr(sz, "val") ?? null;
|
|
787
|
+
const fontSizePx = halfPoints === null ? null : halfPoints / 2 * (96 / 72);
|
|
788
|
+
const colorNode = rPr ? queryAllByLocalName2(rPr, "color")[0] ?? null : null;
|
|
789
|
+
const colorRaw = getAttr2(colorNode, "w:val") ?? getAttr2(colorNode, "val") ?? null;
|
|
790
|
+
const color = colorRaw && colorRaw.toLowerCase() !== "auto" ? `#${colorRaw}` : null;
|
|
791
|
+
const highlightNode = rPr ? queryAllByLocalName2(rPr, "highlight")[0] ?? null : null;
|
|
792
|
+
const highlightRaw = (getAttr2(highlightNode, "w:val") ?? getAttr2(highlightNode, "val") ?? "").toLowerCase();
|
|
793
|
+
const highlightMap = {
|
|
794
|
+
yellow: "#fff59d",
|
|
795
|
+
green: "#b9f6ca",
|
|
796
|
+
cyan: "#b2ebf2",
|
|
797
|
+
magenta: "#f8bbd0",
|
|
798
|
+
blue: "#bbdefb",
|
|
799
|
+
red: "#ffcdd2",
|
|
800
|
+
darkyellow: "#fbc02d",
|
|
801
|
+
darkgreen: "#66bb6a",
|
|
802
|
+
darkblue: "#64b5f6",
|
|
803
|
+
darkred: "#e57373",
|
|
804
|
+
darkcyan: "#4dd0e1",
|
|
805
|
+
darkmagenta: "#ba68c8",
|
|
806
|
+
gray: "#e0e0e0",
|
|
807
|
+
lightgray: "#f5f5f5"
|
|
808
|
+
};
|
|
809
|
+
const highlightColor = highlightRaw && highlightRaw !== "none" ? highlightMap[highlightRaw] ?? null : null;
|
|
810
|
+
const shdNode = rPr ? queryAllByLocalName2(rPr, "shd")[0] ?? null : null;
|
|
811
|
+
const shdFill = (getAttr2(shdNode, "w:fill") ?? getAttr2(shdNode, "fill") ?? "").toLowerCase();
|
|
812
|
+
const shadingColor = shdFill && shdFill !== "auto" ? `#${shdFill}` : null;
|
|
813
|
+
const spacingNode = rPr ? queryAllByLocalName2(rPr, "spacing")[0] ?? null : null;
|
|
814
|
+
const spacingVal = getTwipAttr(spacingNode, "w:val") ?? getTwipAttr(spacingNode, "val") ?? null;
|
|
815
|
+
const charSpacingPx = spacingVal === null ? null : spacingVal / 20 * (96 / 72);
|
|
816
|
+
const bNode = rPr ? queryAllByLocalName2(rPr, "b")[0] ?? null : null;
|
|
817
|
+
const iNode = rPr ? queryAllByLocalName2(rPr, "i")[0] ?? null : null;
|
|
818
|
+
const uNode = rPr ? queryAllByLocalName2(rPr, "u")[0] ?? null : null;
|
|
819
|
+
const strikeNode = rPr ? queryAllByLocalName2(rPr, "strike")[0] ?? null : null;
|
|
820
|
+
const shadowNode = rPr ? queryAllByLocalName2(rPr, "shadow")[0] ?? null : null;
|
|
821
|
+
const vertAlignNode = rPr ? queryAllByLocalName2(rPr, "vertAlign")[0] ?? null : null;
|
|
822
|
+
const bold = bNode !== null && (getAttr2(bNode, "w:val") ?? getAttr2(bNode, "val") ?? "1") !== "0";
|
|
823
|
+
const italic = iNode !== null && (getAttr2(iNode, "w:val") ?? getAttr2(iNode, "val") ?? "1") !== "0";
|
|
824
|
+
const underlineVal = (getAttr2(uNode, "w:val") ?? getAttr2(uNode, "val") ?? "").toLowerCase();
|
|
825
|
+
const underline = uNode !== null && underlineVal !== "none";
|
|
826
|
+
const strike = strikeNode !== null && (getAttr2(strikeNode, "w:val") ?? getAttr2(strikeNode, "val") ?? "1") !== "0";
|
|
827
|
+
const shadow = shadowNode !== null && (getAttr2(shadowNode, "w:val") ?? getAttr2(shadowNode, "val") ?? "1") !== "0";
|
|
828
|
+
const vertAlign = (getAttr2(vertAlignNode, "w:val") ?? getAttr2(vertAlignNode, "val") ?? "").toLowerCase();
|
|
829
|
+
const superscript = vertAlign === "superscript";
|
|
830
|
+
const subscript = vertAlign === "subscript";
|
|
831
|
+
const rFonts = rPr ? queryAllByLocalName2(rPr, "rFonts")[0] ?? null : null;
|
|
832
|
+
const fontFamily = getAttr2(rFonts, "w:eastAsia") ?? getAttr2(rFonts, "eastAsia") ?? getAttr2(rFonts, "w:ascii") ?? getAttr2(rFonts, "ascii") ?? getAttr2(rFonts, "w:hAnsi") ?? getAttr2(rFonts, "hAnsi") ?? null;
|
|
833
|
+
return {
|
|
834
|
+
text,
|
|
835
|
+
fontSizePx,
|
|
836
|
+
color,
|
|
837
|
+
highlightColor,
|
|
838
|
+
shadingColor,
|
|
839
|
+
charSpacingPx,
|
|
840
|
+
shadow,
|
|
841
|
+
bold,
|
|
842
|
+
italic,
|
|
843
|
+
underline,
|
|
844
|
+
strike,
|
|
845
|
+
superscript,
|
|
846
|
+
subscript,
|
|
847
|
+
fontFamily
|
|
848
|
+
};
|
|
849
|
+
}).filter((run) => run !== null);
|
|
850
|
+
}
|
|
851
|
+
function parseDefaults(stylesXml) {
|
|
852
|
+
const docDefaults = queryByLocalName2(stylesXml, "docDefaults");
|
|
853
|
+
if (!docDefaults) {
|
|
854
|
+
return { bodyFontPx: null, bodyLineHeightRatio: null, bodyLineHeightPx: null, bodyLineHeightRule: "auto", paragraphAfterPx: null };
|
|
855
|
+
}
|
|
856
|
+
const rPrDefault = queryByLocalName2(docDefaults, "rPrDefault");
|
|
857
|
+
const sz = rPrDefault ? queryByLocalName2(rPrDefault, "sz") : null;
|
|
858
|
+
const halfPoints = getTwipAttr(sz, "w:val") ?? getTwipAttr(sz, "val") ?? null;
|
|
859
|
+
const bodyFontPx = halfPoints === null ? null : halfPoints / 2 * (96 / 72);
|
|
860
|
+
const pPrDefault = queryByLocalName2(docDefaults, "pPrDefault");
|
|
861
|
+
const spacing = pPrDefault ? queryByLocalName2(pPrDefault, "spacing") : null;
|
|
862
|
+
const line = getTwipAttr(spacing, "w:line") ?? getTwipAttr(spacing, "line") ?? null;
|
|
863
|
+
const rawLineRule = (getAttr2(spacing, "w:lineRule") ?? getAttr2(spacing, "lineRule") ?? "auto").toLowerCase();
|
|
864
|
+
const bodyLineHeightRule = rawLineRule === "exact" ? "exact" : rawLineRule === "atleast" ? "atLeast" : "auto";
|
|
865
|
+
const bodyLineHeightRatio = line === null || bodyLineHeightRule !== "auto" ? null : line / 240;
|
|
866
|
+
const bodyLineHeightPx = line === null || bodyLineHeightRule === "auto" ? null : twipToPx(line);
|
|
867
|
+
const after = getTwipAttr(spacing, "w:after") ?? getTwipAttr(spacing, "after") ?? null;
|
|
868
|
+
const paragraphAfterPx = after === null ? null : twipToPx(after);
|
|
869
|
+
return { bodyFontPx, bodyLineHeightRatio, bodyLineHeightPx, bodyLineHeightRule, paragraphAfterPx };
|
|
870
|
+
}
|
|
871
|
+
function parseHeading1Style(stylesXml) {
|
|
872
|
+
const styles = queryAllByLocalName2(stylesXml, "style");
|
|
873
|
+
const headingStyle = styles.find((style) => {
|
|
874
|
+
const styleId = (getAttr2(style, "w:styleId") ?? getAttr2(style, "styleId") ?? "").toLowerCase();
|
|
875
|
+
const nameNode = queryByLocalName2(style, "name");
|
|
876
|
+
const nameVal = (getAttr2(nameNode, "w:val") ?? getAttr2(nameNode, "val") ?? "").toLowerCase();
|
|
877
|
+
return styleId === "1" || nameVal === "heading 1" || nameVal === "\u6807\u9898 1";
|
|
878
|
+
});
|
|
879
|
+
if (!headingStyle) {
|
|
880
|
+
return { titleFontPx: null, titleColor: null };
|
|
881
|
+
}
|
|
882
|
+
const rPr = queryByLocalName2(headingStyle, "rPr");
|
|
883
|
+
const sz = rPr ? queryByLocalName2(rPr, "sz") : null;
|
|
884
|
+
const halfPoints = getTwipAttr(sz, "w:val") ?? getTwipAttr(sz, "val") ?? null;
|
|
885
|
+
const titleFontPx = halfPoints === null ? null : halfPoints / 2 * (96 / 72);
|
|
886
|
+
const colorNode = rPr ? queryByLocalName2(rPr, "color") : null;
|
|
887
|
+
const colorRaw = getAttr2(colorNode, "w:val") ?? getAttr2(colorNode, "val") ?? null;
|
|
888
|
+
const titleColor = colorRaw ? `#${colorRaw}` : null;
|
|
889
|
+
return { titleFontPx, titleColor };
|
|
890
|
+
}
|
|
891
|
+
function parseFontTableFamilies(fontTableXml) {
|
|
892
|
+
if (!fontTableXml) return [];
|
|
893
|
+
const fontNodes = queryAllByLocalName2(fontTableXml, "font");
|
|
894
|
+
const families = fontNodes.map((node) => getAttr2(node, "w:name") ?? getAttr2(node, "name") ?? "").map((name) => name.trim()).filter((name) => name.length > 0);
|
|
895
|
+
return [...new Set(families)];
|
|
896
|
+
}
|
|
897
|
+
function hasFontLike(families, candidates) {
|
|
898
|
+
return families.some(
|
|
899
|
+
(family) => candidates.some((candidate) => family.toLowerCase().includes(candidate.toLowerCase()))
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
function inferBodyFontFamily(families) {
|
|
903
|
+
if (hasFontLike(families, ["times new roman"])) {
|
|
904
|
+
return '"Times New Roman", "Noto Serif SC", serif';
|
|
905
|
+
}
|
|
906
|
+
if (hasFontLike(families, ["dengxian", "\u7B49\u7EBF", "yahei", "hei", "song"])) {
|
|
907
|
+
return 'DengXian, "Microsoft YaHei", "PingFang SC", "Noto Sans SC", sans-serif';
|
|
908
|
+
}
|
|
909
|
+
return FALLBACK_PROFILE.bodyFontFamily;
|
|
910
|
+
}
|
|
911
|
+
function inferTitleFontFamily(families) {
|
|
912
|
+
if (hasFontLike(families, ["dengxian", "\u7B49\u7EBF"])) {
|
|
913
|
+
return 'DengXian, "Noto Sans SC", "Microsoft YaHei", sans-serif';
|
|
914
|
+
}
|
|
915
|
+
if (hasFontLike(families, ["times new roman"])) {
|
|
916
|
+
return '"Times New Roman", "Noto Serif SC", serif';
|
|
917
|
+
}
|
|
918
|
+
return FALLBACK_PROFILE.titleFontFamily;
|
|
919
|
+
}
|
|
920
|
+
async function parseDocxStyleProfile(file) {
|
|
921
|
+
const buffer = await file.arrayBuffer();
|
|
922
|
+
const zip = await import_jszip2.default.loadAsync(buffer);
|
|
923
|
+
const documentXmlText = await zip.file("word/document.xml")?.async("string");
|
|
924
|
+
const stylesXmlText = await zip.file("word/styles.xml")?.async("string");
|
|
925
|
+
const fontTableXmlText = await zip.file("word/fontTable.xml")?.async("string");
|
|
926
|
+
const numberingXmlText = await zip.file("word/numbering.xml")?.async("string");
|
|
927
|
+
if (!documentXmlText || !stylesXmlText) {
|
|
928
|
+
throw new Error("DOCX missing document.xml or styles.xml");
|
|
929
|
+
}
|
|
930
|
+
const documentXml = parseXml2(documentXmlText);
|
|
931
|
+
const stylesXml = parseXml2(stylesXmlText);
|
|
932
|
+
const fontTableXml = fontTableXmlText ? parseXml2(fontTableXmlText) : null;
|
|
933
|
+
const numberingXml = numberingXmlText ? parseXml2(numberingXmlText) : null;
|
|
934
|
+
const numberingMap = parseNumberingMap(numberingXml);
|
|
935
|
+
const defaults = parseDefaults(stylesXml);
|
|
936
|
+
const heading1 = parseHeading1Style(stylesXml);
|
|
937
|
+
const tableDefaults = parseTableDefaults(stylesXml);
|
|
938
|
+
const pageGeometry = parsePageGeometry(documentXml);
|
|
939
|
+
const titleAlign = parseHeadingAlignFromDocument(documentXml);
|
|
940
|
+
const trailingDate = parseTrailingDateAnchor(documentXml);
|
|
941
|
+
const discoveredFonts = parseFontTableFamilies(fontTableXml);
|
|
942
|
+
const bodyFontFamily = inferBodyFontFamily(discoveredFonts);
|
|
943
|
+
const titleFontFamily = inferTitleFontFamily(discoveredFonts);
|
|
944
|
+
const paragraphProfiles = parseParagraphProfiles(documentXml, numberingMap);
|
|
945
|
+
return {
|
|
946
|
+
sourceFileName: file.name,
|
|
947
|
+
bodyFontPx: defaults.bodyFontPx ?? FALLBACK_PROFILE.bodyFontPx,
|
|
948
|
+
bodyLineHeightRatio: defaults.bodyLineHeightRatio ?? FALLBACK_PROFILE.bodyLineHeightRatio,
|
|
949
|
+
bodyLineHeightPx: defaults.bodyLineHeightPx ?? FALLBACK_PROFILE.bodyLineHeightPx,
|
|
950
|
+
bodyLineHeightRule: defaults.bodyLineHeightRule ?? FALLBACK_PROFILE.bodyLineHeightRule,
|
|
951
|
+
paragraphAfterPx: defaults.paragraphAfterPx ?? FALLBACK_PROFILE.paragraphAfterPx,
|
|
952
|
+
contentWidthPx: pageGeometry.contentWidthPx ?? FALLBACK_PROFILE.contentWidthPx,
|
|
953
|
+
pageHeightPx: pageGeometry.pageHeightPx ?? FALLBACK_PROFILE.pageHeightPx,
|
|
954
|
+
pageMarginTopPx: pageGeometry.marginTopPx ?? FALLBACK_PROFILE.pageMarginTopPx,
|
|
955
|
+
pageMarginBottomPx: pageGeometry.marginBottomPx ?? FALLBACK_PROFILE.pageMarginBottomPx,
|
|
956
|
+
titleFontPx: heading1.titleFontPx ?? FALLBACK_PROFILE.titleFontPx,
|
|
957
|
+
titleColor: heading1.titleColor ?? FALLBACK_PROFILE.titleColor,
|
|
958
|
+
titleAlign: titleAlign ?? FALLBACK_PROFILE.titleAlign,
|
|
959
|
+
bodyFontFamily,
|
|
960
|
+
titleFontFamily,
|
|
961
|
+
discoveredFonts,
|
|
962
|
+
tableCellPaddingTopPx: tableDefaults.topPx ?? FALLBACK_PROFILE.tableCellPaddingTopPx,
|
|
963
|
+
tableCellPaddingLeftPx: tableDefaults.leftPx ?? FALLBACK_PROFILE.tableCellPaddingLeftPx,
|
|
964
|
+
tableCellPaddingBottomPx: tableDefaults.bottomPx ?? FALLBACK_PROFILE.tableCellPaddingBottomPx,
|
|
965
|
+
tableCellPaddingRightPx: tableDefaults.rightPx ?? FALLBACK_PROFILE.tableCellPaddingRightPx,
|
|
966
|
+
paragraphProfiles,
|
|
967
|
+
trailingDateText: trailingDate.trailingDateText,
|
|
968
|
+
trailingDateAlignedRight: trailingDate.trailingDateAlignedRight,
|
|
969
|
+
trailingDateParagraphIndex: trailingDate.trailingDateParagraphIndex,
|
|
970
|
+
trailingEmptyParagraphCountBeforeDate: trailingDate.trailingEmptyParagraphCountBeforeDate
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/lib/renderApply.ts
|
|
975
|
+
function setImportantStyle(el, prop, value) {
|
|
976
|
+
el.style.setProperty(prop, value, "important");
|
|
977
|
+
}
|
|
978
|
+
function escapeHtml2(text) {
|
|
979
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
980
|
+
}
|
|
981
|
+
function runStyleToCss2(run) {
|
|
982
|
+
const declarations = [];
|
|
983
|
+
if (run.fontSizePx !== null) declarations.push(`font-size:${run.fontSizePx.toFixed(2)}px`);
|
|
984
|
+
if (run.color) declarations.push(`color:${run.color}`);
|
|
985
|
+
if (run.highlightColor) declarations.push(`background-color:${run.highlightColor}`);
|
|
986
|
+
if (run.shadingColor) declarations.push(`background-color:${run.shadingColor}`);
|
|
987
|
+
if (run.charSpacingPx !== null) declarations.push(`letter-spacing:${run.charSpacingPx.toFixed(2)}px`);
|
|
988
|
+
if (run.shadow) declarations.push("text-shadow:0.5px 0.5px 0 rgba(0,0,0,0.28)");
|
|
989
|
+
if (run.bold) declarations.push("font-weight:700");
|
|
990
|
+
if (run.italic) declarations.push("font-style:italic");
|
|
991
|
+
const textDecorations = [];
|
|
992
|
+
if (run.underline) textDecorations.push("underline");
|
|
993
|
+
if (run.strike) textDecorations.push("line-through");
|
|
994
|
+
if (textDecorations.length > 0) declarations.push(`text-decoration:${textDecorations.join(" ")}`);
|
|
995
|
+
if (run.superscript) declarations.push("vertical-align:super");
|
|
996
|
+
if (run.subscript) declarations.push("vertical-align:sub");
|
|
997
|
+
if (run.superscript || run.subscript) declarations.push("font-size:0.83em");
|
|
998
|
+
if (run.fontFamily) declarations.push(`font-family:${run.fontFamily}`);
|
|
999
|
+
return declarations.join(";");
|
|
1000
|
+
}
|
|
1001
|
+
function paragraphToRunHtml(runs) {
|
|
1002
|
+
return runs.map((run) => {
|
|
1003
|
+
const css = runStyleToCss2(run);
|
|
1004
|
+
const parts = run.text.split("\n");
|
|
1005
|
+
const html = parts.map((part) => escapeHtml2(part)).join("<br/>");
|
|
1006
|
+
if (!css) return html;
|
|
1007
|
+
return `<span style="${css}">${html}</span>`;
|
|
1008
|
+
}).join("");
|
|
1009
|
+
}
|
|
1010
|
+
function toLowerLetter(n) {
|
|
1011
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
|
1012
|
+
if (n <= 0) return "a";
|
|
1013
|
+
let x = n;
|
|
1014
|
+
let out = "";
|
|
1015
|
+
while (x > 0) {
|
|
1016
|
+
x -= 1;
|
|
1017
|
+
out = alphabet[x % 26] + out;
|
|
1018
|
+
x = Math.floor(x / 26);
|
|
1019
|
+
}
|
|
1020
|
+
return out;
|
|
1021
|
+
}
|
|
1022
|
+
function toRoman(num) {
|
|
1023
|
+
if (num <= 0) return "I";
|
|
1024
|
+
const map = [
|
|
1025
|
+
[1e3, "M"],
|
|
1026
|
+
[900, "CM"],
|
|
1027
|
+
[500, "D"],
|
|
1028
|
+
[400, "CD"],
|
|
1029
|
+
[100, "C"],
|
|
1030
|
+
[90, "XC"],
|
|
1031
|
+
[50, "L"],
|
|
1032
|
+
[40, "XL"],
|
|
1033
|
+
[10, "X"],
|
|
1034
|
+
[9, "IX"],
|
|
1035
|
+
[5, "V"],
|
|
1036
|
+
[4, "IV"],
|
|
1037
|
+
[1, "I"]
|
|
1038
|
+
];
|
|
1039
|
+
let n = num;
|
|
1040
|
+
let result = "";
|
|
1041
|
+
for (const [v, s] of map) {
|
|
1042
|
+
while (n >= v) {
|
|
1043
|
+
result += s;
|
|
1044
|
+
n -= v;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return result;
|
|
1048
|
+
}
|
|
1049
|
+
function formatListMarker(format, counter) {
|
|
1050
|
+
switch ((format ?? "").toLowerCase()) {
|
|
1051
|
+
case "decimal":
|
|
1052
|
+
return `${counter}.`;
|
|
1053
|
+
case "lowerletter":
|
|
1054
|
+
return `${toLowerLetter(counter)}.`;
|
|
1055
|
+
case "upperletter":
|
|
1056
|
+
return `${toLowerLetter(counter).toUpperCase()}.`;
|
|
1057
|
+
case "lowerroman":
|
|
1058
|
+
return `${toRoman(counter).toLowerCase()}.`;
|
|
1059
|
+
case "upperroman":
|
|
1060
|
+
return `${toRoman(counter)}.`;
|
|
1061
|
+
case "bullet":
|
|
1062
|
+
default:
|
|
1063
|
+
return "\u2022";
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function formatListMarkerByPattern(pattern, currentLevel, countersByLevel, currentFormat) {
|
|
1067
|
+
if (!pattern || pattern.trim().length === 0) {
|
|
1068
|
+
return formatListMarker(currentFormat, countersByLevel[currentLevel] ?? 1);
|
|
1069
|
+
}
|
|
1070
|
+
const replaced = pattern.replace(/%(\d+)/g, (_, g1) => {
|
|
1071
|
+
const level1Based = Number.parseInt(g1, 10);
|
|
1072
|
+
if (!Number.isFinite(level1Based) || level1Based <= 0) return "";
|
|
1073
|
+
const levelIdx = level1Based - 1;
|
|
1074
|
+
const n = countersByLevel[levelIdx] ?? 0;
|
|
1075
|
+
if (n <= 0) return "";
|
|
1076
|
+
if (levelIdx === currentLevel) {
|
|
1077
|
+
return formatListMarker(currentFormat, n).replace(/\.$/, "");
|
|
1078
|
+
}
|
|
1079
|
+
return String(n);
|
|
1080
|
+
});
|
|
1081
|
+
const normalized = replaced.trim();
|
|
1082
|
+
if (!normalized) {
|
|
1083
|
+
return formatListMarker(currentFormat, countersByLevel[currentLevel] ?? 1);
|
|
1084
|
+
}
|
|
1085
|
+
return normalized;
|
|
1086
|
+
}
|
|
1087
|
+
function ensureStyleTag(doc, id) {
|
|
1088
|
+
let styleEl = doc.getElementById(id);
|
|
1089
|
+
if (!styleEl) {
|
|
1090
|
+
styleEl = doc.createElement("style");
|
|
1091
|
+
styleEl.id = id;
|
|
1092
|
+
doc.head.appendChild(styleEl);
|
|
1093
|
+
}
|
|
1094
|
+
return styleEl;
|
|
1095
|
+
}
|
|
1096
|
+
function applyBaseProfileCss(doc, styleProfile) {
|
|
1097
|
+
const styleEl = ensureStyleTag(doc, "__word_style_profile__");
|
|
1098
|
+
const targetWidthPx = styleProfile.contentWidthPx.toFixed(2);
|
|
1099
|
+
const topPaddingPx = styleProfile.pageMarginTopPx.toFixed(2);
|
|
1100
|
+
const bottomPaddingPx = styleProfile.pageMarginBottomPx.toFixed(2);
|
|
1101
|
+
const pageHeightPx = styleProfile.pageHeightPx.toFixed(2);
|
|
1102
|
+
const bodyLineHeightCss = styleProfile.bodyLineHeightRule === "auto" || styleProfile.bodyLineHeightPx === null ? styleProfile.bodyLineHeightRatio.toFixed(6) : `${styleProfile.bodyLineHeightPx.toFixed(2)}px`;
|
|
1103
|
+
styleEl.textContent = `
|
|
1104
|
+
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&family=Noto+Serif+SC:wght@400;700&display=swap');
|
|
1105
|
+
html, body { box-sizing: border-box; }
|
|
1106
|
+
body {
|
|
1107
|
+
min-height: ${pageHeightPx}px !important;
|
|
1108
|
+
width: ${targetWidthPx}px !important;
|
|
1109
|
+
max-width: calc(100% - 24px) !important;
|
|
1110
|
+
margin-left: auto !important;
|
|
1111
|
+
margin-right: auto !important;
|
|
1112
|
+
padding-top: ${topPaddingPx}px !important;
|
|
1113
|
+
padding-bottom: ${bottomPaddingPx}px !important;
|
|
1114
|
+
padding-left: 0 !important;
|
|
1115
|
+
padding-right: 0 !important;
|
|
1116
|
+
font-family: ${styleProfile.bodyFontFamily} !important;
|
|
1117
|
+
}
|
|
1118
|
+
p {
|
|
1119
|
+
font-size: ${styleProfile.bodyFontPx.toFixed(4)}px !important;
|
|
1120
|
+
line-height: ${bodyLineHeightCss} !important;
|
|
1121
|
+
margin-bottom: ${styleProfile.paragraphAfterPx.toFixed(2)}px !important;
|
|
1122
|
+
}
|
|
1123
|
+
table { border-collapse: collapse !important; border-spacing: 0 !important; }
|
|
1124
|
+
td, th {
|
|
1125
|
+
padding-top: ${styleProfile.tableCellPaddingTopPx.toFixed(2)}px !important;
|
|
1126
|
+
padding-left: ${styleProfile.tableCellPaddingLeftPx.toFixed(2)}px !important;
|
|
1127
|
+
padding-bottom: ${styleProfile.tableCellPaddingBottomPx.toFixed(2)}px !important;
|
|
1128
|
+
padding-right: ${styleProfile.tableCellPaddingRightPx.toFixed(2)}px !important;
|
|
1129
|
+
vertical-align: top !important;
|
|
1130
|
+
}
|
|
1131
|
+
h1 {
|
|
1132
|
+
font-size: ${styleProfile.titleFontPx.toFixed(2)}px !important;
|
|
1133
|
+
color: ${styleProfile.titleColor} !important;
|
|
1134
|
+
text-align: ${styleProfile.titleAlign} !important;
|
|
1135
|
+
font-family: ${styleProfile.titleFontFamily} !important;
|
|
1136
|
+
}
|
|
1137
|
+
`;
|
|
1138
|
+
}
|
|
1139
|
+
function applyInlineLayoutGuards(doc, styleProfile) {
|
|
1140
|
+
const body = doc.body;
|
|
1141
|
+
const targetWidthPx = styleProfile.contentWidthPx.toFixed(2);
|
|
1142
|
+
const topPaddingPx = styleProfile.pageMarginTopPx.toFixed(2);
|
|
1143
|
+
const bottomPaddingPx = styleProfile.pageMarginBottomPx.toFixed(2);
|
|
1144
|
+
const pageHeightPx = styleProfile.pageHeightPx.toFixed(2);
|
|
1145
|
+
setImportantStyle(body, "box-sizing", "border-box");
|
|
1146
|
+
setImportantStyle(body, "min-height", `${pageHeightPx}px`);
|
|
1147
|
+
setImportantStyle(body, "width", `${targetWidthPx}px`);
|
|
1148
|
+
setImportantStyle(body, "max-width", `${targetWidthPx}px`);
|
|
1149
|
+
setImportantStyle(body, "margin-left", "auto");
|
|
1150
|
+
setImportantStyle(body, "margin-right", "auto");
|
|
1151
|
+
setImportantStyle(body, "padding-top", `${topPaddingPx}px`);
|
|
1152
|
+
setImportantStyle(body, "padding-bottom", `${bottomPaddingPx}px`);
|
|
1153
|
+
setImportantStyle(body, "padding-left", "0");
|
|
1154
|
+
setImportantStyle(body, "padding-right", "0");
|
|
1155
|
+
setImportantStyle(body, "font-family", styleProfile.bodyFontFamily);
|
|
1156
|
+
for (const child of Array.from(body.children)) {
|
|
1157
|
+
if (!(child instanceof HTMLElement)) continue;
|
|
1158
|
+
const tag = child.tagName.toLowerCase();
|
|
1159
|
+
if (tag === "script" || tag === "style") continue;
|
|
1160
|
+
setImportantStyle(child, "box-sizing", "border-box");
|
|
1161
|
+
setImportantStyle(child, "max-width", "100%");
|
|
1162
|
+
}
|
|
1163
|
+
for (const img of Array.from(doc.body.querySelectorAll("img"))) {
|
|
1164
|
+
if (!(img instanceof HTMLElement)) continue;
|
|
1165
|
+
setImportantStyle(img, "max-width", "100%");
|
|
1166
|
+
setImportantStyle(img, "height", "auto");
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function normalizeEmptyParagraphMarkers(paragraphs) {
|
|
1170
|
+
for (const p of paragraphs) {
|
|
1171
|
+
const hasVisualContent = (p.textContent ?? "").trim().length > 0 || p.querySelector("img,table,svg,canvas") !== null;
|
|
1172
|
+
if (!hasVisualContent) {
|
|
1173
|
+
p.setAttribute("data-word-empty", "1");
|
|
1174
|
+
if (p.innerHTML.trim().length === 0) {
|
|
1175
|
+
p.innerHTML = "<br/>";
|
|
1176
|
+
}
|
|
1177
|
+
} else {
|
|
1178
|
+
p.removeAttribute("data-word-empty");
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
function hasMeaningfulParagraphAfter(paragraphs, index) {
|
|
1183
|
+
for (let i = index + 1; i < paragraphs.length; i += 1) {
|
|
1184
|
+
const p = paragraphs[i];
|
|
1185
|
+
const hasText = (p.textContent ?? "").trim().length > 0;
|
|
1186
|
+
const hasVisual = p.querySelector("img,table,svg,canvas") !== null;
|
|
1187
|
+
if (hasText || hasVisual) return true;
|
|
1188
|
+
}
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
function applyParagraphProfiles(doc, styleProfile) {
|
|
1192
|
+
const fallbackParagraphs = Array.from(doc.body.querySelectorAll("p"));
|
|
1193
|
+
fallbackParagraphs.forEach((p) => {
|
|
1194
|
+
p.classList.remove("__word-date-anchor");
|
|
1195
|
+
p.querySelectorAll("span.__word-list-marker").forEach((node) => node.remove());
|
|
1196
|
+
});
|
|
1197
|
+
const resolvedTargets = styleProfile.paragraphProfiles.map((profile, index) => {
|
|
1198
|
+
const byIndex = doc.body.querySelector(`[data-word-p-index="${profile.index}"]`) ?? null;
|
|
1199
|
+
const fallback = fallbackParagraphs[index] ?? null;
|
|
1200
|
+
return {
|
|
1201
|
+
profile,
|
|
1202
|
+
node: byIndex ?? fallback
|
|
1203
|
+
};
|
|
1204
|
+
});
|
|
1205
|
+
if (styleProfile.trailingDateAlignedRight && styleProfile.trailingDateText) {
|
|
1206
|
+
let dateParagraph = null;
|
|
1207
|
+
if (styleProfile.trailingDateParagraphIndex !== null && styleProfile.trailingDateParagraphIndex >= 0) {
|
|
1208
|
+
dateParagraph = doc.body.querySelector(
|
|
1209
|
+
`[data-word-p-index="${styleProfile.trailingDateParagraphIndex}"]`
|
|
1210
|
+
) ?? fallbackParagraphs[styleProfile.trailingDateParagraphIndex] ?? null;
|
|
1211
|
+
} else {
|
|
1212
|
+
dateParagraph = fallbackParagraphs.slice().reverse().find((p) => {
|
|
1213
|
+
const text = (p.textContent ?? "").replace(/\s+/g, "");
|
|
1214
|
+
const target = styleProfile.trailingDateText?.replace(/\s+/g, "") ?? "";
|
|
1215
|
+
return target.length > 0 && text.includes(target);
|
|
1216
|
+
}) ?? null;
|
|
1217
|
+
}
|
|
1218
|
+
const dateIndex = dateParagraph ? fallbackParagraphs.indexOf(dateParagraph) : -1;
|
|
1219
|
+
const hasContentAfterDate = dateIndex >= 0 ? hasMeaningfulParagraphAfter(fallbackParagraphs, dateIndex) : false;
|
|
1220
|
+
if (dateParagraph && !hasContentAfterDate) {
|
|
1221
|
+
let existingEmptyCount = 0;
|
|
1222
|
+
let cursor = dateParagraph.previousElementSibling;
|
|
1223
|
+
while (cursor && cursor.tagName.toLowerCase() === "p" && (cursor.textContent ?? "").trim().length === 0) {
|
|
1224
|
+
existingEmptyCount += 1;
|
|
1225
|
+
cursor = cursor.previousElementSibling;
|
|
1226
|
+
}
|
|
1227
|
+
const needed = Math.max(0, styleProfile.trailingEmptyParagraphCountBeforeDate - existingEmptyCount);
|
|
1228
|
+
for (let i = 0; i < needed; i += 1) {
|
|
1229
|
+
const spacer = doc.createElement("p");
|
|
1230
|
+
spacer.innerHTML = "<br/>";
|
|
1231
|
+
dateParagraph.parentElement?.insertBefore(spacer, dateParagraph);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const paragraphs = Array.from(doc.body.querySelectorAll("p"));
|
|
1236
|
+
const listCounters = /* @__PURE__ */ new Map();
|
|
1237
|
+
const orderedTargets = [];
|
|
1238
|
+
for (const target of resolvedTargets) {
|
|
1239
|
+
const para = target.node;
|
|
1240
|
+
const profile = target.profile;
|
|
1241
|
+
if (!para) continue;
|
|
1242
|
+
orderedTargets.push(para);
|
|
1243
|
+
para.removeAttribute("data-word-list");
|
|
1244
|
+
para.style.textAlign = profile.align;
|
|
1245
|
+
if (profile.beforePx !== null) para.style.marginTop = `${profile.beforePx.toFixed(2)}px`;
|
|
1246
|
+
if (profile.afterPx !== null) para.style.marginBottom = `${profile.afterPx.toFixed(2)}px`;
|
|
1247
|
+
if (profile.lineHeightRule === "auto" && profile.lineHeightRatio !== null) {
|
|
1248
|
+
para.style.lineHeight = profile.lineHeightRatio.toFixed(6);
|
|
1249
|
+
} else if ((profile.lineHeightRule === "exact" || profile.lineHeightRule === "atLeast") && profile.lineHeightPx !== null) {
|
|
1250
|
+
para.style.lineHeight = `${profile.lineHeightPx.toFixed(2)}px`;
|
|
1251
|
+
}
|
|
1252
|
+
if (profile.indentLeftPx !== null) para.style.marginLeft = `${profile.indentLeftPx.toFixed(2)}px`;
|
|
1253
|
+
if (profile.indentRightPx !== null) para.style.marginRight = `${profile.indentRightPx.toFixed(2)}px`;
|
|
1254
|
+
if (profile.firstLinePx !== null) para.style.textIndent = `${profile.firstLinePx.toFixed(2)}px`;
|
|
1255
|
+
if (profile.hangingPx !== null) para.style.textIndent = `${(-profile.hangingPx).toFixed(2)}px`;
|
|
1256
|
+
if (profile.runs.length > 0 && para.querySelector("img,table,svg,canvas") === null) {
|
|
1257
|
+
const currentTextNormalized = (para.textContent ?? "").replace(/\s+/g, "");
|
|
1258
|
+
const runTextNormalized = profile.runs.map((run) => run.text).join("").replace(/\s+/g, "");
|
|
1259
|
+
if (runTextNormalized.length > 0 && currentTextNormalized === runTextNormalized) {
|
|
1260
|
+
para.innerHTML = paragraphToRunHtml(profile.runs);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (profile.listNumId !== null && profile.listLevel !== null) {
|
|
1264
|
+
para.setAttribute("data-word-list", "1");
|
|
1265
|
+
if (profile.sectionBreakBefore) {
|
|
1266
|
+
listCounters.set(profile.listNumId, []);
|
|
1267
|
+
}
|
|
1268
|
+
const currentLevel = Math.max(0, profile.listLevel);
|
|
1269
|
+
const levels = listCounters.get(profile.listNumId) ?? [];
|
|
1270
|
+
const prevValue = levels[currentLevel] ?? profile.listStartAt - 1;
|
|
1271
|
+
const nextValue = prevValue + 1;
|
|
1272
|
+
levels[currentLevel] = nextValue;
|
|
1273
|
+
for (let lv = currentLevel + 1; lv < levels.length; lv += 1) {
|
|
1274
|
+
levels[lv] = 0;
|
|
1275
|
+
}
|
|
1276
|
+
listCounters.set(profile.listNumId, levels);
|
|
1277
|
+
const markerText = formatListMarkerByPattern(profile.listTextPattern, currentLevel, levels, profile.listFormat);
|
|
1278
|
+
const plainText = (para.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1279
|
+
const alreadyHasMarker = plainText.startsWith(markerText);
|
|
1280
|
+
if (!alreadyHasMarker) {
|
|
1281
|
+
const marker = doc.createElement("span");
|
|
1282
|
+
marker.className = "__word-list-marker";
|
|
1283
|
+
marker.textContent = `${markerText} `;
|
|
1284
|
+
marker.style.display = "inline-block";
|
|
1285
|
+
marker.style.minWidth = "1.8em";
|
|
1286
|
+
marker.style.marginLeft = currentLevel > 0 ? `${currentLevel * 1.2}em` : "0";
|
|
1287
|
+
marker.style.color = "inherit";
|
|
1288
|
+
marker.style.fontWeight = "inherit";
|
|
1289
|
+
para.prepend(marker);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
normalizeEmptyParagraphMarkers(paragraphs);
|
|
1294
|
+
return orderedTargets;
|
|
1295
|
+
}
|
|
1296
|
+
function paragraphHeightPx(paragraph) {
|
|
1297
|
+
const rect = paragraph.getBoundingClientRect();
|
|
1298
|
+
if (rect.height > 0) return rect.height;
|
|
1299
|
+
const lh = Number.parseFloat(getComputedStyle(paragraph).lineHeight || "0");
|
|
1300
|
+
if (Number.isFinite(lh) && lh > 0) return lh;
|
|
1301
|
+
return 16;
|
|
1302
|
+
}
|
|
1303
|
+
function insertPageSpacerBefore(doc, paragraph, heightPx) {
|
|
1304
|
+
if (heightPx <= 0.5) return;
|
|
1305
|
+
const spacer = doc.createElement("div");
|
|
1306
|
+
spacer.dataset.wordPageSpacer = "1";
|
|
1307
|
+
spacer.style.height = `${heightPx.toFixed(2)}px`;
|
|
1308
|
+
spacer.style.width = "100%";
|
|
1309
|
+
spacer.style.pointerEvents = "none";
|
|
1310
|
+
spacer.style.userSelect = "none";
|
|
1311
|
+
paragraph.parentElement?.insertBefore(spacer, paragraph);
|
|
1312
|
+
}
|
|
1313
|
+
function removePaginationSpacers(doc) {
|
|
1314
|
+
doc.querySelectorAll("[data-word-page-spacer='1']").forEach((node) => node.remove());
|
|
1315
|
+
}
|
|
1316
|
+
function estimateGroupHeight(paragraphs, idx, profile, contentHeight) {
|
|
1317
|
+
const currentH = paragraphHeightPx(paragraphs[idx]);
|
|
1318
|
+
if (!profile.keepNext) return currentH;
|
|
1319
|
+
const next = paragraphs[idx + 1];
|
|
1320
|
+
if (!next) return currentH;
|
|
1321
|
+
const nextH = paragraphHeightPx(next);
|
|
1322
|
+
const sum = currentH + nextH;
|
|
1323
|
+
if (sum > contentHeight) return currentH;
|
|
1324
|
+
return sum;
|
|
1325
|
+
}
|
|
1326
|
+
function applyKeepPagination(doc, styleProfile, paragraphs) {
|
|
1327
|
+
removePaginationSpacers(doc);
|
|
1328
|
+
const contentHeight = Math.max(120, styleProfile.pageHeightPx - styleProfile.pageMarginTopPx - styleProfile.pageMarginBottomPx);
|
|
1329
|
+
const count = Math.min(styleProfile.paragraphProfiles.length, paragraphs.length);
|
|
1330
|
+
let used = 0;
|
|
1331
|
+
for (let i = 0; i < count; i += 1) {
|
|
1332
|
+
const p = paragraphs[i];
|
|
1333
|
+
const profile = styleProfile.paragraphProfiles[i];
|
|
1334
|
+
const h2 = paragraphHeightPx(p);
|
|
1335
|
+
const forceBreak = profile.pageBreakBefore;
|
|
1336
|
+
if (forceBreak && used > 0) {
|
|
1337
|
+
insertPageSpacerBefore(doc, p, contentHeight - used);
|
|
1338
|
+
used = 0;
|
|
1339
|
+
}
|
|
1340
|
+
const groupHeight = estimateGroupHeight(paragraphs, i, profile, contentHeight);
|
|
1341
|
+
if ((profile.keepLines || profile.keepNext) && used > 0 && used + groupHeight > contentHeight) {
|
|
1342
|
+
insertPageSpacerBefore(doc, p, contentHeight - used);
|
|
1343
|
+
used = 0;
|
|
1344
|
+
}
|
|
1345
|
+
if (used > 0 && used + h2 > contentHeight) {
|
|
1346
|
+
insertPageSpacerBefore(doc, p, contentHeight - used);
|
|
1347
|
+
used = 0;
|
|
1348
|
+
}
|
|
1349
|
+
used += h2;
|
|
1350
|
+
if (used >= contentHeight) {
|
|
1351
|
+
used = used % contentHeight;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
function applyFormattingMarks(doc, showFormattingMarks) {
|
|
1356
|
+
const styleEl = ensureStyleTag(doc, "__word_view_options__");
|
|
1357
|
+
styleEl.textContent = `
|
|
1358
|
+
p[data-word-empty="1"]::before { content: "\\00a0"; }
|
|
1359
|
+
${showFormattingMarks ? `
|
|
1360
|
+
p::after {
|
|
1361
|
+
content: "\u21B5";
|
|
1362
|
+
color: #66aef9;
|
|
1363
|
+
font-size: 0.85em;
|
|
1364
|
+
margin-left: 3px;
|
|
1365
|
+
}
|
|
1366
|
+
br::after {
|
|
1367
|
+
content: "\u21B5";
|
|
1368
|
+
color: #66aef9;
|
|
1369
|
+
}
|
|
1370
|
+
` : ""}
|
|
1371
|
+
`;
|
|
1372
|
+
}
|
|
1373
|
+
function applyWordRenderModel({ doc, styleProfile, showFormattingMarks }) {
|
|
1374
|
+
const effectiveProfile = styleProfile ?? createFallbackWordStyleProfile("__default_a4__");
|
|
1375
|
+
applyWordHtmlCompatibility(doc, {
|
|
1376
|
+
forceBodyFontFamily: effectiveProfile.bodyFontFamily,
|
|
1377
|
+
forceHeadingFontFamily: effectiveProfile.titleFontFamily
|
|
1378
|
+
});
|
|
1379
|
+
let paragraphs = Array.from(doc.body.querySelectorAll("p"));
|
|
1380
|
+
normalizeEmptyParagraphMarkers(paragraphs);
|
|
1381
|
+
applyBaseProfileCss(doc, effectiveProfile);
|
|
1382
|
+
applyInlineLayoutGuards(doc, effectiveProfile);
|
|
1383
|
+
if (styleProfile) {
|
|
1384
|
+
paragraphs = applyParagraphProfiles(doc, styleProfile);
|
|
1385
|
+
applyKeepPagination(doc, styleProfile, paragraphs);
|
|
1386
|
+
}
|
|
1387
|
+
applyFormattingMarks(doc, showFormattingMarks);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/core/DocsWordElement.ts
|
|
1391
|
+
var BASE_CSS = `
|
|
1392
|
+
:host{display:block;border:1px solid #d8deea;border-radius:12px;background:#fff;overflow:hidden;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto}
|
|
1393
|
+
.toolbar{display:flex;gap:8px;flex-wrap:wrap;padding:10px;border-bottom:1px solid #e8edf6;background:#f8faff}
|
|
1394
|
+
button{border:1px solid #c8d2eb;background:#fff;border-radius:9px;padding:6px 10px;cursor:pointer}
|
|
1395
|
+
.paste{width:100%;min-height:40px;border:1px dashed #95acef;border-radius:10px;background:#edf3ff;padding:8px 10px;box-sizing:border-box;resize:vertical}
|
|
1396
|
+
.hint{display:block;padding:0 10px 10px;color:#4b5c82;font-size:12px}
|
|
1397
|
+
iframe{width:100%;min-height:760px;border:0}
|
|
1398
|
+
`;
|
|
1399
|
+
var DocsWordElement = class extends HTMLElement {
|
|
1400
|
+
rootRef;
|
|
1401
|
+
frame;
|
|
1402
|
+
pasteArea;
|
|
1403
|
+
fileInput;
|
|
1404
|
+
hint;
|
|
1405
|
+
htmlSnapshot;
|
|
1406
|
+
styleProfile = null;
|
|
1407
|
+
frameHeight = 0;
|
|
1408
|
+
constructor() {
|
|
1409
|
+
super();
|
|
1410
|
+
this.rootRef = this.attachShadow({ mode: "open" });
|
|
1411
|
+
this.htmlSnapshot = buildHtmlSnapshot("<p><br/></p>");
|
|
1412
|
+
const style = document.createElement("style");
|
|
1413
|
+
style.textContent = BASE_CSS;
|
|
1414
|
+
const toolbar = document.createElement("div");
|
|
1415
|
+
toolbar.className = "toolbar";
|
|
1416
|
+
const btnRead = document.createElement("button");
|
|
1417
|
+
btnRead.textContent = "\u4ECE\u7CFB\u7EDF\u526A\u8D34\u677F\u8BFB\u53D6";
|
|
1418
|
+
btnRead.onclick = () => void this.readClipboard();
|
|
1419
|
+
const btnUpload = document.createElement("button");
|
|
1420
|
+
btnUpload.textContent = "\u4E0A\u4F20 Word";
|
|
1421
|
+
btnUpload.onclick = () => this.fileInput.click();
|
|
1422
|
+
const btnClear = document.createElement("button");
|
|
1423
|
+
btnClear.textContent = "\u6E05\u7A7A";
|
|
1424
|
+
btnClear.onclick = () => this.clear();
|
|
1425
|
+
this.fileInput = document.createElement("input");
|
|
1426
|
+
this.fileInput.type = "file";
|
|
1427
|
+
this.fileInput.accept = ".docx";
|
|
1428
|
+
this.fileInput.style.display = "none";
|
|
1429
|
+
this.fileInput.onchange = () => void this.onUpload();
|
|
1430
|
+
toolbar.append(btnRead, btnUpload, btnClear, this.fileInput);
|
|
1431
|
+
this.pasteArea = document.createElement("textarea");
|
|
1432
|
+
this.pasteArea.className = "paste";
|
|
1433
|
+
this.pasteArea.placeholder = "\u5728\u6B64\u5904\u7C98\u8D34 Word/WPS/Google Docs \u5185\u5BB9\uFF08Ctrl/Cmd+V\uFF09";
|
|
1434
|
+
this.pasteArea.onpaste = (event) => {
|
|
1435
|
+
event.preventDefault();
|
|
1436
|
+
void this.applyFromClipboardData(event.clipboardData);
|
|
1437
|
+
};
|
|
1438
|
+
this.hint = document.createElement("span");
|
|
1439
|
+
this.hint.className = "hint";
|
|
1440
|
+
this.hint.textContent = "\u7B49\u5F85\u5185\u5BB9\u5BFC\u5165";
|
|
1441
|
+
this.frame = document.createElement("iframe");
|
|
1442
|
+
this.frame.sandbox.add("allow-same-origin", "allow-scripts");
|
|
1443
|
+
this.frame.onload = () => this.onFrameLoad();
|
|
1444
|
+
this.rootRef.append(style, toolbar, this.pasteArea, this.hint, this.frame);
|
|
1445
|
+
}
|
|
1446
|
+
connectedCallback() {
|
|
1447
|
+
this.renderSnapshot();
|
|
1448
|
+
}
|
|
1449
|
+
setSnapshot(rawHtml) {
|
|
1450
|
+
this.styleProfile = null;
|
|
1451
|
+
this.htmlSnapshot = buildHtmlSnapshot(rawHtml);
|
|
1452
|
+
this.renderSnapshot();
|
|
1453
|
+
this.hint.textContent = "\u5DF2\u52A0\u8F7D HTML \u5FEB\u7167";
|
|
1454
|
+
this.emitChange();
|
|
1455
|
+
}
|
|
1456
|
+
clear() {
|
|
1457
|
+
this.styleProfile = null;
|
|
1458
|
+
this.htmlSnapshot = buildHtmlSnapshot("<p><br/></p>");
|
|
1459
|
+
this.renderSnapshot();
|
|
1460
|
+
this.hint.textContent = "\u6587\u6863\u5DF2\u6E05\u7A7A";
|
|
1461
|
+
this.emitChange();
|
|
1462
|
+
}
|
|
1463
|
+
async onUpload() {
|
|
1464
|
+
const file = this.fileInput.files?.[0];
|
|
1465
|
+
if (!file) return;
|
|
1466
|
+
try {
|
|
1467
|
+
const [snapshot, profile] = await Promise.all([
|
|
1468
|
+
parseDocxToHtmlSnapshot(file),
|
|
1469
|
+
parseDocxStyleProfile(file)
|
|
1470
|
+
]);
|
|
1471
|
+
this.styleProfile = profile;
|
|
1472
|
+
this.htmlSnapshot = snapshot;
|
|
1473
|
+
this.renderSnapshot();
|
|
1474
|
+
this.hint.textContent = `\u5DF2\u52A0\u8F7D Word \u6587\u4EF6: ${profile.sourceFileName}`;
|
|
1475
|
+
this.emitChange();
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
this.emitError(error instanceof Error ? error.message : "Word \u89E3\u6790\u5931\u8D25");
|
|
1478
|
+
} finally {
|
|
1479
|
+
this.fileInput.value = "";
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
async readClipboard() {
|
|
1483
|
+
if (!navigator.clipboard?.read) {
|
|
1484
|
+
this.emitError("\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301 clipboard.read");
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
try {
|
|
1488
|
+
const items = await navigator.clipboard.read();
|
|
1489
|
+
const payload = await extractFromClipboardItems(items);
|
|
1490
|
+
this.applyPayload(payload.html, payload.text);
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
this.emitError(error instanceof Error ? error.message : "\u8BFB\u53D6\u526A\u8D34\u677F\u5931\u8D25");
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
async applyFromClipboardData(data) {
|
|
1496
|
+
if (!data) return;
|
|
1497
|
+
const payload = await extractFromClipboardDataTransfer(data);
|
|
1498
|
+
this.applyPayload(payload.html, payload.text);
|
|
1499
|
+
}
|
|
1500
|
+
applyPayload(html, text) {
|
|
1501
|
+
this.styleProfile = null;
|
|
1502
|
+
if (html.trim()) {
|
|
1503
|
+
this.htmlSnapshot = buildHtmlSnapshot(html);
|
|
1504
|
+
} else if (text.trim()) {
|
|
1505
|
+
this.htmlSnapshot = buildHtmlSnapshot(`<p>${text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")}</p>`);
|
|
1506
|
+
} else {
|
|
1507
|
+
this.hint.textContent = "\u672A\u68C0\u6D4B\u5230\u53EF\u5BFC\u5165\u5185\u5BB9";
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
this.renderSnapshot();
|
|
1511
|
+
this.hint.textContent = "\u5DF2\u5BFC\u5165\u526A\u8D34\u677F\u5185\u5BB9";
|
|
1512
|
+
this.emitChange();
|
|
1513
|
+
}
|
|
1514
|
+
onFrameLoad() {
|
|
1515
|
+
const doc = this.frame.contentDocument;
|
|
1516
|
+
if (!doc) return;
|
|
1517
|
+
applyWordRenderModel({
|
|
1518
|
+
doc,
|
|
1519
|
+
styleProfile: this.styleProfile,
|
|
1520
|
+
showFormattingMarks: false
|
|
1521
|
+
});
|
|
1522
|
+
this.syncHeight();
|
|
1523
|
+
window.setTimeout(() => this.syncHeight(), 120);
|
|
1524
|
+
}
|
|
1525
|
+
syncHeight() {
|
|
1526
|
+
const doc = this.frame.contentDocument;
|
|
1527
|
+
if (!doc) return;
|
|
1528
|
+
const measured = Math.max(760, doc.body.scrollHeight, doc.documentElement.scrollHeight);
|
|
1529
|
+
const next = measured + 24;
|
|
1530
|
+
if (Math.abs(next - this.frameHeight) < 2) return;
|
|
1531
|
+
this.frameHeight = next;
|
|
1532
|
+
this.frame.style.height = `${next}px`;
|
|
1533
|
+
}
|
|
1534
|
+
renderSnapshot() {
|
|
1535
|
+
this.frame.srcdoc = this.htmlSnapshot;
|
|
1536
|
+
}
|
|
1537
|
+
emitChange() {
|
|
1538
|
+
this.dispatchEvent(new CustomEvent("docsjs-change", { detail: { htmlSnapshot: this.htmlSnapshot } }));
|
|
1539
|
+
}
|
|
1540
|
+
emitError(message) {
|
|
1541
|
+
this.dispatchEvent(new CustomEvent("docsjs-error", { detail: { message } }));
|
|
1542
|
+
this.hint.textContent = `\u9519\u8BEF: ${message}`;
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
function defineDocsWordElement() {
|
|
1546
|
+
if (!customElements.get("docs-word-editor")) {
|
|
1547
|
+
customElements.define("docs-word-editor", DocsWordElement);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/react/WordFidelityEditorReact.tsx
|
|
1552
|
+
var import_react = __toESM(require("react"), 1);
|
|
1553
|
+
defineDocsWordElement();
|
|
1554
|
+
function WordFidelityEditorReact({ onChange, onError }) {
|
|
1555
|
+
const ref2 = (0, import_react.useRef)(null);
|
|
1556
|
+
(0, import_react.useEffect)(() => {
|
|
1557
|
+
const node = ref2.current;
|
|
1558
|
+
if (!node) return;
|
|
1559
|
+
const onChangeEvent = (event) => {
|
|
1560
|
+
const detail = event.detail;
|
|
1561
|
+
onChange?.(detail);
|
|
1562
|
+
};
|
|
1563
|
+
const onErrorEvent = (event) => {
|
|
1564
|
+
const detail = event.detail;
|
|
1565
|
+
onError?.(detail);
|
|
1566
|
+
};
|
|
1567
|
+
node.addEventListener("docsjs-change", onChangeEvent);
|
|
1568
|
+
node.addEventListener("docsjs-error", onErrorEvent);
|
|
1569
|
+
return () => {
|
|
1570
|
+
node.removeEventListener("docsjs-change", onChangeEvent);
|
|
1571
|
+
node.removeEventListener("docsjs-error", onErrorEvent);
|
|
1572
|
+
};
|
|
1573
|
+
}, [onChange, onError]);
|
|
1574
|
+
return import_react.default.createElement("docs-word-editor", { ref: ref2 });
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/vue/WordFidelityEditorVue.ts
|
|
1578
|
+
var import_vue = require("vue");
|
|
1579
|
+
defineDocsWordElement();
|
|
1580
|
+
var WordFidelityEditorVue = (0, import_vue.defineComponent)({
|
|
1581
|
+
name: "WordFidelityEditorVue",
|
|
1582
|
+
emits: ["change", "error"],
|
|
1583
|
+
setup(_, { emit }) {
|
|
1584
|
+
const elRef = (0, import_vue.ref)(null);
|
|
1585
|
+
const onChange = (event) => {
|
|
1586
|
+
emit("change", event.detail);
|
|
1587
|
+
};
|
|
1588
|
+
const onError = (event) => {
|
|
1589
|
+
emit("error", event.detail);
|
|
1590
|
+
};
|
|
1591
|
+
(0, import_vue.onMounted)(() => {
|
|
1592
|
+
elRef.value?.addEventListener("docsjs-change", onChange);
|
|
1593
|
+
elRef.value?.addEventListener("docsjs-error", onError);
|
|
1594
|
+
});
|
|
1595
|
+
(0, import_vue.onBeforeUnmount)(() => {
|
|
1596
|
+
elRef.value?.removeEventListener("docsjs-change", onChange);
|
|
1597
|
+
elRef.value?.removeEventListener("docsjs-error", onError);
|
|
1598
|
+
});
|
|
1599
|
+
return () => (0, import_vue.h)("docs-word-editor", { ref: elRef });
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1603
|
+
0 && (module.exports = {
|
|
1604
|
+
DocsWordElement,
|
|
1605
|
+
WordFidelityEditorReact,
|
|
1606
|
+
WordFidelityEditorVue,
|
|
1607
|
+
defineDocsWordElement
|
|
1608
|
+
});
|
|
1609
|
+
//# sourceMappingURL=index.cjs.map
|