@office-open/docx 0.9.7 → 0.10.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/dist/{context-BrtlmvVE.mjs → context-D9RMvxSu.mjs} +44 -6
- package/dist/context-D9RMvxSu.mjs.map +1 -0
- package/dist/{core-properties-CBMhgSrn.d.mts → core-properties-DNjQDzBy.d.mts} +102 -113
- package/dist/core-properties-DNjQDzBy.d.mts.map +1 -0
- package/dist/{generate-BMTVflV1.mjs → generate-h0Xp5BPT.mjs} +45 -33
- package/dist/generate-h0Xp5BPT.mjs.map +1 -0
- package/dist/generate.d.mts +1 -1
- package/dist/generate.mjs +1 -1
- package/dist/index.d.mts +10 -19
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +6 -6
- package/dist/{parse-K59nx6MF.mjs → parse-T79KhZTq.mjs} +18 -6
- package/dist/parse-T79KhZTq.mjs.map +1 -0
- package/dist/parse.d.mts +1 -1
- package/dist/parse.mjs +1 -1
- package/dist/{parts-BQBGNSGm.mjs → parts-BWmkYaBr.mjs} +637 -467
- package/dist/parts-BWmkYaBr.mjs.map +1 -0
- package/dist/patch/index.d.mts +26 -29
- package/dist/patch/index.d.mts.map +1 -1
- package/dist/patch/index.mjs +190 -41
- package/dist/patch/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/context-BrtlmvVE.mjs.map +0 -1
- package/dist/core-properties-CBMhgSrn.d.mts.map +0 -1
- package/dist/generate-BMTVflV1.mjs.map +0 -1
- package/dist/parse-K59nx6MF.mjs.map +0 -1
- package/dist/parts-BQBGNSGm.mjs.map +0 -1
package/dist/patch/index.d.mts
CHANGED
|
@@ -1,38 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { OutputByType, OutputType } from "@office-open/core";
|
|
3
|
-
import { Buffer } from "\u0000polyfill-node.buffer";
|
|
1
|
+
import { Mt as SectionChild, On as ParagraphChild, Xn as CommentOptions, yi as RunOptions } from "../core-properties-DNjQDzBy.mjs";
|
|
2
|
+
import { BasePatchOptions, CorePropertiesOptions, DataType, OutputByType, OutputType } from "@office-open/core";
|
|
4
3
|
|
|
5
4
|
//#region src/patch/from-docx.d.ts
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type: typeof PatchType.PARAGRAPH;
|
|
13
|
-
children: unknown[];
|
|
14
|
-
}
|
|
15
|
-
interface FilePatch {
|
|
16
|
-
type: typeof PatchType.DOCUMENT;
|
|
5
|
+
type PatchComment = Omit<CommentOptions, "id">;
|
|
6
|
+
type Patch = {
|
|
7
|
+
type: "paragraph";
|
|
8
|
+
children: (string | RunOptions | ParagraphChild)[];
|
|
9
|
+
} | {
|
|
10
|
+
type: "document";
|
|
17
11
|
children: SectionChild[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
data: InputDataType;
|
|
24
|
-
patches: Readonly<Record<string, Patch>>;
|
|
12
|
+
};
|
|
13
|
+
interface PatchDocumentOptions<T extends OutputType = OutputType> extends BasePatchOptions<T> {
|
|
14
|
+
placeholders?: Readonly<Record<string, Patch>>;
|
|
15
|
+
findReplace?: Readonly<Record<string, Patch>>;
|
|
16
|
+
coreProperties?: Partial<CorePropertiesOptions>;
|
|
25
17
|
keepOriginalStyles?: boolean;
|
|
26
|
-
placeholderDelimiters?: Readonly<{
|
|
27
|
-
start: string;
|
|
28
|
-
end: string;
|
|
29
|
-
}>;
|
|
30
18
|
recursive?: boolean;
|
|
19
|
+
append?: SectionChild[];
|
|
20
|
+
comments?: {
|
|
21
|
+
paragraphs?: Readonly<Record<number, PatchComment[]>>;
|
|
22
|
+
placeholders?: Readonly<Record<string, PatchComment[]>>;
|
|
23
|
+
};
|
|
31
24
|
}
|
|
32
|
-
declare const patchDocument: <T extends
|
|
25
|
+
declare const patchDocument: <T extends OutputType = OutputType>({
|
|
33
26
|
outputType,
|
|
34
27
|
data,
|
|
35
|
-
|
|
28
|
+
placeholders,
|
|
29
|
+
findReplace,
|
|
30
|
+
coreProperties,
|
|
31
|
+
append,
|
|
32
|
+
comments,
|
|
36
33
|
keepOriginalStyles,
|
|
37
34
|
placeholderDelimiters,
|
|
38
35
|
recursive
|
|
@@ -40,11 +37,11 @@ declare const patchDocument: <T extends PatchDocumentOutputType = PatchDocumentO
|
|
|
40
37
|
//#endregion
|
|
41
38
|
//#region src/patch/patch-detector.d.ts
|
|
42
39
|
interface PatchDetectorOptions {
|
|
43
|
-
data:
|
|
40
|
+
data: DataType;
|
|
44
41
|
}
|
|
45
42
|
declare const patchDetector: ({
|
|
46
43
|
data
|
|
47
44
|
}: PatchDetectorOptions) => Promise<string[]>;
|
|
48
45
|
//#endregion
|
|
49
|
-
export {
|
|
46
|
+
export { Patch, PatchComment, PatchDocumentOptions, patchDetector, patchDocument };
|
|
50
47
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/patch/from-docx.ts","../../src/patch/patch-detector.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/patch/from-docx.ts","../../src/patch/patch-detector.ts"],"mappings":";;;;KAsDY,YAAA,GAAe,IAAI,CAAC,cAAA;AAAA,KA4HpB,KAAA;EACN,IAAA;EAAmB,QAAA,YAAoB,UAAA,GAAa,cAAA;AAAA;EACpD,IAAA;EAAkB,QAAA,EAAU,YAAA;AAAA;AAAA,UAYjB,oBAAA,WACL,UAAA,GAAa,UAAA,UACf,gBAAA,CAAiB,CAAA;EAEzB,YAAA,GAAe,QAAA,CAAS,MAAA,SAAe,KAAA;EAEvC,WAAA,GAAc,QAAA,CAAS,MAAA,SAAe,KAAA;EAEtC,cAAA,GAAiB,OAAA,CAAQ,qBAAA;EACzB,kBAAA;EACA,SAAA;EAEA,MAAA,GAAS,YAAA;EAOT,QAAA;IACE,UAAA,GAAa,QAAA,CAAS,MAAA,SAAe,YAAA;IACrC,YAAA,GAAe,QAAA,CAAS,MAAA,SAAe,YAAA;EAAA;AAAA;AAAA,cAwB9B,aAAA,aAAiC,UAAA,GAAa,UAAA;EAAY,UAAA;EAAA,IAAA;EAAA,YAAA;EAAA,WAAA;EAAA,cAAA;EAAA,MAAA;EAAA,QAAA;EAAA,kBAAA;EAAA,qBAAA;EAAA;AAAA,GAWpE,oBAAA,CAAqB,CAAA,MAAK,OAAA,CAAQ,YAAA,CAAa,CAAA;;;UC1OxC,oBAAA;EACR,IAAA,EAAM,QAAQ;AAAA;AAAA,cA4BH,aAAA;EAAuB;AAAA,GAAU,oBAAA,KAAuB,OAAA"}
|
package/dist/patch/index.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { DOCX_NS, OoxmlMimeType, TargetModeType, appendContentType, appendRelationship, createReplacer, createTraverser, getNextRelationshipIndex, getReferencedMedia, replaceImagePlaceholders, strFromU8, toJson, toUint8Array, unzipSync, zipAndConvert } from "@office-open/core";
|
|
1
|
+
import { Y as DocumentAttributeNamespaces, it as stringifyRunInline, nt as stringifyChildDispatch, p as commentsDesc, rt as stringifyParagraphInline, tt as tableDesc, wn as Media } from "../parts-BWmkYaBr.mjs";
|
|
2
|
+
import { DOCX_NS, OoxmlMimeType, TargetModeType, appendContentType, appendRelationship, applyCorePropertiesOverride, createReplacer, createTraverser, getNextRelationshipIndex, getReferencedMedia, replaceImagePlaceholders, strFromU8, toJson, toUint8Array, unzipSync, zipAndConvert } from "@office-open/core";
|
|
3
3
|
import { escapeXml, js2xml, xml2js } from "@office-open/xml";
|
|
4
4
|
//#region src/patch/from-docx.ts
|
|
5
5
|
/** Reusable TextEncoder (stateless, safe to share). */
|
|
6
6
|
const encoder = new TextEncoder();
|
|
7
|
+
const COMMENTS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
|
|
8
|
+
const COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
|
|
7
9
|
/**
|
|
8
10
|
* Document patching module for modifying existing .docx files.
|
|
9
11
|
*
|
|
@@ -34,35 +36,52 @@ function createPatchContext(file, hyperlinkSink) {
|
|
|
34
36
|
addMedia: () => ""
|
|
35
37
|
};
|
|
36
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Serialize a patch child (SectionChild / ParagraphChild / string) into XML
|
|
41
|
+
* elements via the compile-path stringifiers. Shared by the placeholder
|
|
42
|
+
* replacer and body-level `append`. Relies on the module-level
|
|
43
|
+
* {@link currentPatchCtx} for relationship/media sinks.
|
|
44
|
+
*/
|
|
45
|
+
const formatChildElement = (child) => {
|
|
46
|
+
let xmlStr;
|
|
47
|
+
if (typeof child === "string") xmlStr = `<w:r><w:t xml:space="preserve">${escapeXml(child)}</w:t></w:r>`;
|
|
48
|
+
else if (typeof child === "object" && child !== null) {
|
|
49
|
+
const obj = child;
|
|
50
|
+
if ("paragraph" in obj) xmlStr = stringifyParagraphInline(obj.paragraph, currentPatchCtx);
|
|
51
|
+
else if ("table" in obj) xmlStr = tableDesc.stringify(obj.table, currentPatchCtx) ?? "";
|
|
52
|
+
else {
|
|
53
|
+
const jr = stringifyChildDispatch(child, currentPatchCtx);
|
|
54
|
+
if (jr !== void 0) xmlStr = Array.isArray(jr) ? jr.join("") : jr;
|
|
55
|
+
else xmlStr = stringifyRunInline(child, currentPatchCtx);
|
|
56
|
+
}
|
|
57
|
+
} else xmlStr = "<w:r/>";
|
|
58
|
+
return [xml2js(xmlStr, { captureSpacesBetweenElements: true }).elements[0]];
|
|
59
|
+
};
|
|
37
60
|
const docxReplacer = createReplacer({
|
|
38
61
|
ns: DOCX_NS,
|
|
39
|
-
formatChild:
|
|
40
|
-
let xmlStr;
|
|
41
|
-
if (typeof child === "string") xmlStr = `<w:r><w:t xml:space="preserve">${escapeXml(child)}</w:t></w:r>`;
|
|
42
|
-
else if (typeof child === "object" && child !== null) {
|
|
43
|
-
const obj = child;
|
|
44
|
-
if ("paragraph" in obj) xmlStr = stringifyParagraphInline(obj.paragraph, currentPatchCtx);
|
|
45
|
-
else if ("table" in obj) xmlStr = tableDesc.stringify(obj.table, currentPatchCtx) ?? "";
|
|
46
|
-
else {
|
|
47
|
-
const jr = stringifyChildDispatch(child, currentPatchCtx);
|
|
48
|
-
if (jr !== void 0) xmlStr = Array.isArray(jr) ? jr.join("") : jr;
|
|
49
|
-
else xmlStr = stringifyRunInline(child, currentPatchCtx);
|
|
50
|
-
}
|
|
51
|
-
} else xmlStr = "<w:r/>";
|
|
52
|
-
return [xml2js(xmlStr, { captureSpacesBetweenElements: true }).elements[0]];
|
|
53
|
-
}
|
|
62
|
+
formatChild: formatChildElement
|
|
54
63
|
});
|
|
55
|
-
/** Current patch context — set per file in the main loop. */
|
|
56
|
-
let currentPatchCtx;
|
|
57
64
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* @
|
|
65
|
+
* Splice block-level children into `<w:body>` before the trailing `<w:sectPr>`
|
|
66
|
+
* (the final section properties). If the body has no trailing sectPr, append at
|
|
67
|
+
* the end. Reuses {@link formatChildElement} so the same serialization and
|
|
68
|
+
* relationship/media sinks apply as placeholder patches.
|
|
61
69
|
*/
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
const appendToBody = (root, children) => {
|
|
71
|
+
const body = (root.elements?.find((e) => e.name === "w:document"))?.elements?.find((e) => e.name === "w:body");
|
|
72
|
+
if (!body) return;
|
|
73
|
+
const els = body.elements ?? (body.elements = []);
|
|
74
|
+
const newEls = [];
|
|
75
|
+
for (const child of children) newEls.push(...formatChildElement(child));
|
|
76
|
+
let insertAt = els.length;
|
|
77
|
+
for (let i = els.length - 1; i >= 0; i--) if (els[i].name === "w:sectPr") {
|
|
78
|
+
insertAt = i;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
els.splice(insertAt, 0, ...newEls);
|
|
65
82
|
};
|
|
83
|
+
/** Current patch context — set per file in the main loop. */
|
|
84
|
+
let currentPatchCtx;
|
|
66
85
|
const UTF16LE = new Uint8Array([255, 254]);
|
|
67
86
|
const UTF16BE = new Uint8Array([254, 255]);
|
|
68
87
|
const compareByteArrays = (a, b) => {
|
|
@@ -75,7 +94,7 @@ const compareByteArrays = (a, b) => {
|
|
|
75
94
|
*
|
|
76
95
|
* @publicApi
|
|
77
96
|
*/
|
|
78
|
-
const patchDocument = async ({ outputType, data,
|
|
97
|
+
const patchDocument = async ({ outputType, data, placeholders, findReplace, coreProperties, append, comments, keepOriginalStyles = true, placeholderDelimiters = {
|
|
79
98
|
end: "}}",
|
|
80
99
|
start: "{{"
|
|
81
100
|
}, recursive = true }) => {
|
|
@@ -83,6 +102,7 @@ const patchDocument = async ({ outputType, data, patches, keepOriginalStyles = t
|
|
|
83
102
|
const contexts = /* @__PURE__ */ new Map();
|
|
84
103
|
const media = new Media();
|
|
85
104
|
const file = { media };
|
|
105
|
+
const assignedComments = buildAssignedComments(zipContent, comments);
|
|
86
106
|
const map = /* @__PURE__ */ new Map();
|
|
87
107
|
const imageRelationshipAdditions = [];
|
|
88
108
|
const hyperlinkRelationshipAdditions = [];
|
|
@@ -134,18 +154,31 @@ const patchDocument = async ({ outputType, data, patches, keepOriginalStyles = t
|
|
|
134
154
|
if (!placeholderDelimiters?.start.trim() || !placeholderDelimiters?.end.trim()) throw new Error("Both start and end delimiters must be non-empty strings.");
|
|
135
155
|
const { start, end } = placeholderDelimiters;
|
|
136
156
|
currentPatchCtx = createPatchContext(file, hyperlinkSink);
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
if (key === "word/document.xml") {
|
|
158
|
+
for (const ac of assignedComments) if (ac.anchor.kind === "placeholder") wrapPlaceholderComment(json, `${start}${ac.anchor.key}${end}`, ac.id);
|
|
159
|
+
}
|
|
160
|
+
const entries = [];
|
|
161
|
+
if (placeholders) for (const [key, value] of Object.entries(placeholders)) entries.push({
|
|
162
|
+
find: `${start}${key}${end}`,
|
|
163
|
+
patch: value
|
|
164
|
+
});
|
|
165
|
+
if (findReplace) for (const [key, value] of Object.entries(findReplace)) entries.push({
|
|
166
|
+
find: key,
|
|
167
|
+
patch: value
|
|
168
|
+
});
|
|
169
|
+
for (const { find: patchText, patch: patchValue } of entries) while (true) {
|
|
170
|
+
const { didFindOccurrence } = docxReplacer({
|
|
171
|
+
context,
|
|
172
|
+
json,
|
|
173
|
+
keepOriginalStyles,
|
|
174
|
+
patch: patchValue,
|
|
175
|
+
patchText
|
|
176
|
+
});
|
|
177
|
+
if (!recursive || !didFindOccurrence) break;
|
|
178
|
+
}
|
|
179
|
+
if (append && append.length > 0 && key === "word/document.xml") appendToBody(json, append);
|
|
180
|
+
if (key === "word/document.xml") {
|
|
181
|
+
for (const ac of assignedComments) if (ac.anchor.kind === "paragraph") wrapParagraphComment(json, ac.anchor.index, ac.id);
|
|
149
182
|
}
|
|
150
183
|
for (const hl of hyperlinkSink) hyperlinkRelationshipAdditions.push({
|
|
151
184
|
hyperlink: {
|
|
@@ -165,6 +198,7 @@ const patchDocument = async ({ outputType, data, patches, keepOriginalStyles = t
|
|
|
165
198
|
}
|
|
166
199
|
map.set(key, json);
|
|
167
200
|
}
|
|
201
|
+
if (assignedComments.length > 0) mergeCommentsPart(map, assignedComments, file);
|
|
168
202
|
for (const { key, mediaDatas } of imageRelationshipAdditions) {
|
|
169
203
|
const relationshipKey = `word/_rels/${key.split("/").pop()}.rels`;
|
|
170
204
|
const relationshipsJson = map.get(relationshipKey) ?? createRelationshipFile();
|
|
@@ -194,7 +228,8 @@ const patchDocument = async ({ outputType, data, patches, keepOriginalStyles = t
|
|
|
194
228
|
appendContentType(contentTypesJson, "image/svg+xml", "svg");
|
|
195
229
|
}
|
|
196
230
|
const files = {};
|
|
197
|
-
|
|
231
|
+
const XML_DECL = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
|
|
232
|
+
for (const [key, value] of map) files[key] = key === "docProps/core.xml" && coreProperties ? encoder.encode(XML_DECL + applyCorePropertiesOverride(value, coreProperties)) : encoder.encode(js2xml(value));
|
|
198
233
|
for (const [key, value] of binaryContentMap) files[key] = value;
|
|
199
234
|
for (const { data: mediaData, fileName } of media.array) files[`word/media/${fileName}`] = mediaData instanceof Uint8Array ? mediaData : new Uint8Array(mediaData);
|
|
200
235
|
return await zipAndConvert(files, outputType, OoxmlMimeType.DOCX);
|
|
@@ -212,6 +247,120 @@ const createRelationshipFile = () => ({
|
|
|
212
247
|
type: "element"
|
|
213
248
|
}]
|
|
214
249
|
});
|
|
250
|
+
/** Build a bare OOXML element node with attributes and no children. */
|
|
251
|
+
function makeElement(name, attributes = {}) {
|
|
252
|
+
return {
|
|
253
|
+
type: "element",
|
|
254
|
+
name,
|
|
255
|
+
attributes,
|
|
256
|
+
elements: []
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/** A `<w:r>` carrying a `<w:commentReference>` (CommentReference run style). */
|
|
260
|
+
function commentReferenceRun(id) {
|
|
261
|
+
return xml2js(`<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="${id}"/></w:r>`).elements[0];
|
|
262
|
+
}
|
|
263
|
+
/** Largest `w:id` among existing `<w:comment>` entries (-1 when none). */
|
|
264
|
+
function maxCommentId(commentsPart) {
|
|
265
|
+
const root = commentsPart?.elements?.find((e) => e.name === "w:comments");
|
|
266
|
+
let max = -1;
|
|
267
|
+
for (const c of root?.elements ?? []) if (c.name === "w:comment") {
|
|
268
|
+
const id = Number(c.attributes?.["w:id"]);
|
|
269
|
+
if (Number.isFinite(id)) max = Math.max(max, id);
|
|
270
|
+
}
|
|
271
|
+
return max;
|
|
272
|
+
}
|
|
273
|
+
/** Assign continuation ids to every requested comment anchor. */
|
|
274
|
+
function buildAssignedComments(zipContent, comments) {
|
|
275
|
+
if (!comments) return [];
|
|
276
|
+
const raw = zipContent["word/comments.xml"];
|
|
277
|
+
let nextId = raw ? maxCommentId(toJson(strFromU8(raw))) + 1 : 0;
|
|
278
|
+
const assigned = [];
|
|
279
|
+
if (comments.placeholders) for (const [key, list] of Object.entries(comments.placeholders)) for (const options of list) assigned.push({
|
|
280
|
+
id: nextId++,
|
|
281
|
+
anchor: {
|
|
282
|
+
kind: "placeholder",
|
|
283
|
+
key
|
|
284
|
+
},
|
|
285
|
+
options
|
|
286
|
+
});
|
|
287
|
+
if (comments.paragraphs) for (const [indexStr, list] of Object.entries(comments.paragraphs)) {
|
|
288
|
+
const index = Number(indexStr);
|
|
289
|
+
for (const options of list) assigned.push({
|
|
290
|
+
id: nextId++,
|
|
291
|
+
anchor: {
|
|
292
|
+
kind: "paragraph",
|
|
293
|
+
index
|
|
294
|
+
},
|
|
295
|
+
options
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return assigned;
|
|
299
|
+
}
|
|
300
|
+
/** Wrap the Nth body paragraph with comment range markers + a reference run. */
|
|
301
|
+
function wrapParagraphComment(json, index, commentId) {
|
|
302
|
+
const body = json.elements?.find((e) => e.name === "w:document")?.elements?.find((e) => e.name === "w:body");
|
|
303
|
+
if (!body) return;
|
|
304
|
+
const target = (body.elements ?? []).filter((e) => e.name === "w:p")[index];
|
|
305
|
+
if (!target) throw new Error(`patchDocument: no paragraph at index ${index} to comment`);
|
|
306
|
+
const els = target.elements ?? (target.elements = []);
|
|
307
|
+
const start = makeElement("w:commentRangeStart", { "w:id": String(commentId) });
|
|
308
|
+
const end = makeElement("w:commentRangeEnd", { "w:id": String(commentId) });
|
|
309
|
+
const insertAt = els.length > 0 && els[0].name === "w:pPr" ? 1 : 0;
|
|
310
|
+
els.splice(insertAt, 0, start);
|
|
311
|
+
els.push(end, commentReferenceRun(commentId));
|
|
312
|
+
}
|
|
313
|
+
/** Wrap the first run whose `<w:t>` contains the placeholder with comment markers. */
|
|
314
|
+
function wrapPlaceholderComment(json, placeholder, commentId) {
|
|
315
|
+
const body = json.elements?.find((e) => e.name === "w:document")?.elements?.find((e) => e.name === "w:body");
|
|
316
|
+
if (!body) return;
|
|
317
|
+
for (const p of body.elements ?? []) {
|
|
318
|
+
if (p.name !== "w:p") continue;
|
|
319
|
+
const els = p.elements ?? [];
|
|
320
|
+
for (let i = 0; i < els.length; i++) {
|
|
321
|
+
if (els[i].name !== "w:r") continue;
|
|
322
|
+
const text = (els[i].elements ?? []).find((e) => e.name === "w:t")?.elements?.[0]?.text;
|
|
323
|
+
if (typeof text === "string" && text.includes(placeholder)) {
|
|
324
|
+
els.splice(i, 0, makeElement("w:commentRangeStart", { "w:id": String(commentId) }));
|
|
325
|
+
els.splice(i + 2, 0, makeElement("w:commentRangeEnd", { "w:id": String(commentId) }), commentReferenceRun(commentId));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Merge assigned comments into word/comments.xml (appending to an existing part
|
|
333
|
+
* or creating one) and wire document.xml.rels + content types when newly added.
|
|
334
|
+
*/
|
|
335
|
+
function mergeCommentsPart(map, assigned, file) {
|
|
336
|
+
const newOpts = assigned.map((ac) => ({
|
|
337
|
+
id: ac.id,
|
|
338
|
+
...ac.options
|
|
339
|
+
}));
|
|
340
|
+
const ctx = createPatchContext(file, []);
|
|
341
|
+
const newXml = commentsDesc.stringify({ children: newOpts }, ctx) ?? "";
|
|
342
|
+
const existingEl = map.get("word/comments.xml");
|
|
343
|
+
const wasNew = !existingEl;
|
|
344
|
+
if (existingEl) {
|
|
345
|
+
const existingRoot = existingEl.elements?.find((e) => e.name === "w:comments");
|
|
346
|
+
const newRoot = xml2js(newXml).elements?.find((e) => e.name === "w:comments");
|
|
347
|
+
existingRoot?.elements?.push(...newRoot?.elements ?? []);
|
|
348
|
+
} else map.set("word/comments.xml", toJson(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>${newXml}`));
|
|
349
|
+
if (wasNew) {
|
|
350
|
+
const relsKey = "word/_rels/document.xml.rels";
|
|
351
|
+
const relsJson = map.get(relsKey) ?? createRelationshipFile();
|
|
352
|
+
map.set(relsKey, relsJson);
|
|
353
|
+
appendRelationship(relsJson, getNextRelationshipIndex(relsJson), COMMENTS_REL_TYPE, "comments.xml");
|
|
354
|
+
}
|
|
355
|
+
const typesEl = map.get("[Content_Types].xml")?.elements?.find((e) => e.name === "Types");
|
|
356
|
+
if (typesEl) {
|
|
357
|
+
const els = typesEl.elements ?? (typesEl.elements = []);
|
|
358
|
+
if (!els.some((e) => e.name === "Override" && e.attributes?.["PartName"] === "/word/comments.xml")) els.push(makeElement("Override", {
|
|
359
|
+
PartName: "/word/comments.xml",
|
|
360
|
+
ContentType: COMMENTS_CONTENT_TYPE
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
215
364
|
//#endregion
|
|
216
365
|
//#region src/patch/patch-detector.ts
|
|
217
366
|
/**
|
|
@@ -238,7 +387,7 @@ const createRelationshipFile = () => ({
|
|
|
238
387
|
* const patches = {};
|
|
239
388
|
* for (const key of placeholders) {
|
|
240
389
|
* patches[key] = {
|
|
241
|
-
* type:
|
|
390
|
+
* type: "paragraph",
|
|
242
391
|
* children: [new TextRun(getUserData(key))],
|
|
243
392
|
* };
|
|
244
393
|
* });
|
|
@@ -267,6 +416,6 @@ const findPatchKeys = (text) => {
|
|
|
267
416
|
return text.match(/(?<=\{\{).+?(?=\}\})/gs) ?? [];
|
|
268
417
|
};
|
|
269
418
|
//#endregion
|
|
270
|
-
export {
|
|
419
|
+
export { patchDetector, patchDocument };
|
|
271
420
|
|
|
272
421
|
//# sourceMappingURL=index.mjs.map
|
package/dist/patch/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/patch/from-docx.ts","../../src/patch/patch-detector.ts"],"sourcesContent":["import { TargetModeType } from \"@office-open/core\";\nimport {\n DOCX_NS,\n OoxmlMimeType,\n appendContentType,\n appendRelationship,\n createReplacer,\n getNextRelationshipIndex,\n getReferencedMedia,\n replaceImagePlaceholders,\n strFromU8,\n toJson,\n unzipSync,\n zipAndConvert,\n} from \"@office-open/core\";\nimport type { OutputByType, OutputType } from \"@office-open/core\";\nimport { toUint8Array } from \"@office-open/core\";\nimport { escapeXml, js2xml, xml2js } from \"@office-open/xml\";\nimport type { Element } from \"@office-open/xml\";\nimport { DocumentAttributeNamespaces } from \"@parts/document\";\nimport {\n stringifyChildDispatch,\n stringifyParagraphInline,\n stringifyRunInline,\n} from \"@parts/inline\";\nimport type { ParagraphChild } from \"@parts/paragraph/paragraph\";\nimport type { ParagraphOptions } from \"@parts/paragraph/paragraph\";\nimport type { RunOptions } from \"@parts/paragraph/run/run\";\nimport { tableDesc } from \"@parts/table/descriptor\";\nimport type { TableOptions } from \"@parts/table/table\";\nimport { Media } from \"@shared/media\";\nimport type { SectionChild } from \"@shared/section\";\n\nimport type { BodyContext } from \"../context\";\nimport type { ViewWrapper } from \"../context\";\n\n/** Reusable TextEncoder (stateless, safe to share). */\nconst encoder = new TextEncoder();\n\n/**\n * Document patching module for modifying existing .docx files.\n *\n * Uses compile-path stringifiers (zero class instantiation) to serialize\n * patch content — no Formatter, no XmlComponent instances.\n *\n * @module\n */\n\n// ── Patch content stringification ──\n\n/**\n * Lightweight BodyContext adapter for patch serialization.\n * Captures hyperlink and image relationships for post-processing.\n */\nfunction createPatchContext(\n file: { media: Media },\n hyperlinkSink: Array<{ id: string; link: string }>,\n): BodyContext {\n return {\n fileData: file as unknown as BodyContext[\"fileData\"],\n file: file as unknown as BodyContext[\"file\"],\n viewWrapper: {\n relationships: {\n addRelationship: (linkId: string, _type: string, target: string, _mode?: string) => {\n hyperlinkSink.push({ id: linkId, link: target });\n },\n relationshipCount: 0,\n },\n } as unknown as BodyContext[\"viewWrapper\"],\n stringifyChild: () => \"\",\n addRelationship: () => \"\",\n addMedia: () => \"\",\n };\n}\n\nconst docxReplacer = createReplacer({\n ns: DOCX_NS,\n formatChild: (child: unknown): Element[] => {\n let xmlStr: string;\n\n if (typeof child === \"string\") {\n // Plain string → simple run\n xmlStr = `<w:r><w:t xml:space=\"preserve\">${escapeXml(child)}</w:t></w:r>`;\n } else if (typeof child === \"object\" && child !== null) {\n const obj = child as Record<string, unknown>;\n // SectionChild level (paragraph / table) — for DOCUMENT patches\n if (\"paragraph\" in obj) {\n xmlStr = stringifyParagraphInline(obj.paragraph as ParagraphOptions, currentPatchCtx);\n } else if (\"table\" in obj) {\n xmlStr = tableDesc.stringify(obj.table as TableOptions, currentPatchCtx) ?? \"\";\n } else {\n // ParagraphChild level — for PARAGRAPH patches\n // Try compile-path JSON child dispatch first\n const jr = stringifyChildDispatch(child as ParagraphChild, currentPatchCtx);\n if (jr !== undefined) {\n xmlStr = Array.isArray(jr) ? jr.join(\"\") : jr;\n } else {\n // RunOptions (plain objects with text/children/bold/etc.)\n xmlStr = stringifyRunInline(child as RunOptions, currentPatchCtx);\n }\n }\n } else {\n xmlStr = \"<w:r/>\";\n }\n\n const jsonObj = xml2js(xmlStr, { captureSpacesBetweenElements: true });\n return [jsonObj.elements![0]];\n },\n});\n\n/** Current patch context — set per file in the main loop. */\nlet currentPatchCtx: BodyContext;\n\n/**\n * Supported input data types for document patching.\n */\nexport type InputDataType = Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob;\n\n/**\n * Patch type enumeration.\n *\n * @publicApi\n */\nexport const PatchType = {\n DOCUMENT: \"file\",\n PARAGRAPH: \"paragraph\",\n} as const;\n\ninterface ParagraphPatch {\n type: typeof PatchType.PARAGRAPH;\n children: unknown[];\n}\n\ninterface FilePatch {\n type: typeof PatchType.DOCUMENT;\n children: SectionChild[];\n}\n\ninterface ImageRelationshipAddition {\n key: string;\n mediaDatas: { fileName: string }[];\n}\n\ninterface HyperlinkRelationshipAddition {\n key: string;\n hyperlink: { id: string; link: string };\n}\n\nexport type Patch = ParagraphPatch | FilePatch;\n\nexport type PatchDocumentOutputType = OutputType;\n\nexport interface PatchDocumentOptions<T extends PatchDocumentOutputType = PatchDocumentOutputType> {\n outputType: T;\n data: InputDataType;\n patches: Readonly<Record<string, Patch>>;\n keepOriginalStyles?: boolean;\n placeholderDelimiters?: Readonly<{\n start: string;\n end: string;\n }>;\n recursive?: boolean;\n}\n\nconst UTF16LE = new Uint8Array([0xff, 0xfe]);\nconst UTF16BE = new Uint8Array([0xfe, 0xff]);\n\nconst compareByteArrays = (a: Uint8Array, b: Uint8Array): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) {\n return false;\n }\n }\n return true;\n};\n\n/**\n * Patches an existing .docx document by replacing placeholders with new content.\n *\n * @publicApi\n */\nexport const patchDocument = async <T extends PatchDocumentOutputType = PatchDocumentOutputType>({\n outputType,\n data,\n patches,\n keepOriginalStyles = true,\n placeholderDelimiters = { end: \"}}\", start: \"{{\" } as const,\n recursive = true,\n}: PatchDocumentOptions<T>): Promise<OutputByType[T]> => {\n const zipContent = unzipSync(toUint8Array(data));\n const contexts = new Map<string, BodyContext>();\n const media = new Media();\n const file = { media } as BodyContext[\"file\"];\n\n const map = new Map<string, Element>();\n\n const imageRelationshipAdditions: ImageRelationshipAddition[] = [];\n const hyperlinkRelationshipAdditions: HyperlinkRelationshipAddition[] = [];\n let hasMedia = false;\n\n const binaryContentMap = new Map<string, Uint8Array>();\n\n for (const [key, value] of Object.entries(zipContent)) {\n const startBytes = value.slice(0, 2);\n if (compareByteArrays(startBytes, UTF16LE) || compareByteArrays(startBytes, UTF16BE)) {\n binaryContentMap.set(key, value);\n continue;\n }\n\n if (!key.endsWith(\".xml\") && !key.endsWith(\".rels\")) {\n binaryContentMap.set(key, value);\n continue;\n }\n\n const json = toJson(strFromU8(value));\n\n if (key === \"word/document.xml\") {\n const document = json.elements?.find((i) => i.name === \"w:document\");\n if (document && document.attributes) {\n for (const ns of [\"mc\", \"wp\", \"r\", \"w15\", \"m\"] as const) {\n document.attributes[`xmlns:${ns}`] = DocumentAttributeNamespaces[ns];\n }\n document.attributes[\"mc:Ignorable\"] =\n `${document.attributes[\"mc:Ignorable\"] || \"\"} w15`.trim();\n }\n }\n\n if (key.startsWith(\"word/\") && !key.endsWith(\".xml.rels\")) {\n const hyperlinkSink: Array<{ id: string; link: string }> = [];\n\n const context: BodyContext = {\n fileData: file,\n file,\n viewWrapper: {\n relationships: {\n addRelationship: (\n linkId: string,\n _: string,\n target: string,\n __: (typeof TargetModeType)[keyof typeof TargetModeType],\n ) => {\n hyperlinkRelationshipAdditions.push({\n hyperlink: {\n id: linkId,\n link: target,\n },\n key,\n });\n },\n },\n } as unknown as ViewWrapper,\n stringifyChild: () => \"\",\n addRelationship: () => \"\",\n addMedia: () => \"\",\n };\n contexts.set(key, context);\n\n if (!placeholderDelimiters?.start.trim() || !placeholderDelimiters?.end.trim()) {\n throw new Error(\"Both start and end delimiters must be non-empty strings.\");\n }\n\n const { start, end } = placeholderDelimiters;\n\n // Create compile-path context for stringifying patch children\n const patchCtx = createPatchContext(file, hyperlinkSink);\n currentPatchCtx = patchCtx;\n\n for (const [patchKey, patchValue] of Object.entries(patches)) {\n const patchText = `${start}${patchKey}${end}`;\n while (true) {\n const { didFindOccurrence } = docxReplacer({\n context,\n json,\n keepOriginalStyles,\n patch: patchValue,\n patchText,\n });\n if (!recursive || !didFindOccurrence) {\n break;\n }\n }\n }\n\n // Flush hyperlink relationships captured by the compile-path context\n for (const hl of hyperlinkSink) {\n hyperlinkRelationshipAdditions.push({\n hyperlink: { id: hl.id, link: hl.link },\n key,\n });\n }\n\n const mediaDatas = getReferencedMedia(JSON.stringify(json), media.array);\n if (mediaDatas.length > 0) {\n hasMedia = true;\n imageRelationshipAdditions.push({\n key,\n mediaDatas,\n });\n }\n }\n\n map.set(key, json);\n }\n\n for (const { key, mediaDatas } of imageRelationshipAdditions) {\n const relationshipKey = `word/_rels/${key.split(\"/\").pop()}.rels`;\n const relationshipsJson = map.get(relationshipKey) ?? createRelationshipFile();\n map.set(relationshipKey, relationshipsJson);\n\n const index = getNextRelationshipIndex(relationshipsJson);\n const newJson = replaceImagePlaceholders(\n JSON.stringify(map.get(key)),\n mediaDatas,\n index,\n \"plain\",\n );\n map.set(key, JSON.parse(newJson) as Element);\n\n for (let i = 0; i < mediaDatas.length; i++) {\n const { fileName } = mediaDatas[i];\n appendRelationship(\n relationshipsJson,\n index + i,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\",\n `media/${fileName}`,\n );\n }\n }\n\n for (const { key, hyperlink } of hyperlinkRelationshipAdditions) {\n const relationshipKey = `word/_rels/${key.split(\"/\").pop()}.rels`;\n\n const relationshipsJson = map.get(relationshipKey) ?? createRelationshipFile();\n map.set(relationshipKey, relationshipsJson);\n\n appendRelationship(\n relationshipsJson,\n hyperlink.id,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink\",\n hyperlink.link,\n TargetModeType.EXTERNAL,\n );\n }\n\n if (hasMedia) {\n const contentTypesJson = map.get(\"[Content_Types].xml\");\n\n if (!contentTypesJson) {\n throw new Error(\"Could not find content types file\");\n }\n\n appendContentType(contentTypesJson, \"image/png\", \"png\");\n appendContentType(contentTypesJson, \"image/jpeg\", \"jpeg\");\n appendContentType(contentTypesJson, \"image/jpeg\", \"jpg\");\n appendContentType(contentTypesJson, \"image/bmp\", \"bmp\");\n appendContentType(contentTypesJson, \"image/gif\", \"gif\");\n appendContentType(contentTypesJson, \"image/svg+xml\", \"svg\");\n }\n\n const files: Record<string, Uint8Array> = {};\n\n for (const [key, value] of map) {\n files[key] = encoder.encode(js2xml(value));\n }\n\n for (const [key, value] of binaryContentMap) {\n files[key] = value;\n }\n\n for (const { data: mediaData, fileName } of media.array) {\n files[`word/media/${fileName}`] =\n mediaData instanceof Uint8Array ? mediaData : new Uint8Array(mediaData);\n }\n\n return await zipAndConvert(files, outputType, OoxmlMimeType.DOCX);\n};\n\nconst createRelationshipFile = (): Element => ({\n declaration: {\n attributes: {\n encoding: \"UTF-8\",\n standalone: \"yes\",\n version: \"1.0\",\n },\n },\n elements: [\n {\n attributes: {\n xmlns: \"http://schemas.openxmlformats.org/package/2006/relationships\",\n },\n elements: [],\n name: \"Relationships\",\n type: \"element\",\n },\n ],\n});\n","/**\n * Patch detector for discovering placeholders in document templates.\n *\n * @module\n */\nimport { DOCX_NS, createTraverser, strFromU8, toJson, unzipSync } from \"@office-open/core\";\nimport { toUint8Array } from \"@office-open/core\";\n\nimport type { InputDataType } from \"./from-docx\";\n\n/**\n * Options for patch detection.\n *\n * @property data - The document template to scan for placeholders\n */\ninterface PatchDetectorOptions {\n data: InputDataType;\n}\n\n/**\n * Detects all placeholders present in a document template.\n *\n * Scans through all XML content in a .docx file to find placeholder text\n * enclosed in delimiters (default: {{placeholder}}). This is useful for\n * discovering what patches a template expects before performing replacement.\n *\n * @param options - Patch detector configuration\n * @returns Array of placeholder keys found in the document\n *\n * @example\n * ```typescript\n * const placeholders = await patchDetector({ data: templateBuffer });\n * // Returns: [\"name\", \"date\", \"address\"] if template contains {{name}}, {{date}}, {{address}}\n *\n * // Use detected placeholders to create patches\n * const patches = {};\n * for (const key of placeholders) {\n * patches[key] = {\n * type: PatchType.PARAGRAPH,\n * children: [new TextRun(getUserData(key))],\n * };\n * });\n * ```\n */\nexport const patchDetector = async ({ data }: PatchDetectorOptions): Promise<string[]> => {\n const zipContent = unzipSync(toUint8Array(data));\n const patches = new Set<string>();\n\n for (const [key, value] of Object.entries(zipContent)) {\n if (!key.endsWith(\".xml\") && !key.endsWith(\".rels\")) {\n continue;\n }\n if (key.startsWith(\"word/\") && !key.endsWith(\".xml.rels\")) {\n const json = toJson(strFromU8(value));\n const { traverse } = createTraverser(DOCX_NS);\n for (const p of traverse(json)) {\n for (const patch of findPatchKeys(p.text)) {\n patches.add(patch);\n }\n }\n }\n }\n return [...patches];\n};\n\n/**\n * Extracts placeholder keys from text using regex pattern.\n *\n * @param text - Text to search for placeholders\n * @returns Array of placeholder keys (without delimiters)\n */\nconst findPatchKeys = (text: string): string[] => {\n const pattern = /(?<=\\{\\{).+?(?=\\}\\})/gs;\n return text.match(pattern) ?? [];\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/patch/from-docx.ts","../../src/patch/patch-detector.ts"],"sourcesContent":["import { TargetModeType } from \"@office-open/core\";\nimport {\n DOCX_NS,\n OoxmlMimeType,\n appendContentType,\n appendRelationship,\n applyCorePropertiesOverride,\n createReplacer,\n getNextRelationshipIndex,\n getReferencedMedia,\n replaceImagePlaceholders,\n strFromU8,\n toJson,\n unzipSync,\n zipAndConvert,\n} from \"@office-open/core\";\nimport type {\n BasePatchOptions,\n CorePropertiesOptions,\n OutputByType,\n OutputType,\n} from \"@office-open/core\";\nimport { toUint8Array } from \"@office-open/core\";\nimport { escapeXml, js2xml, xml2js } from \"@office-open/xml\";\nimport type { Element } from \"@office-open/xml\";\nimport { commentsDesc } from \"@parts/comments\";\nimport { DocumentAttributeNamespaces } from \"@parts/document\";\nimport {\n stringifyChildDispatch,\n stringifyParagraphInline,\n stringifyRunInline,\n} from \"@parts/inline\";\nimport type { ParagraphChild } from \"@parts/paragraph/paragraph\";\nimport type { ParagraphOptions } from \"@parts/paragraph/paragraph\";\nimport type { CommentOptions } from \"@parts/paragraph/run/comment-run\";\nimport type { RunOptions } from \"@parts/paragraph/run/run\";\nimport { tableDesc } from \"@parts/table/descriptor\";\nimport type { TableOptions } from \"@parts/table/table\";\nimport { Media } from \"@shared/media\";\nimport type { SectionChild } from \"@shared/section\";\n\nimport type { BodyContext } from \"../context\";\nimport type { ViewWrapper } from \"../context\";\n\n/** Reusable TextEncoder (stateless, safe to share). */\nconst encoder = new TextEncoder();\n\nconst COMMENTS_REL_TYPE =\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments\";\nconst COMMENTS_CONTENT_TYPE =\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml\";\n\n/** A comment injected by patch — same as CommentOptions but without an id\n * (the patch assigns continuation ids). */\nexport type PatchComment = Omit<CommentOptions, \"id\">;\n\n/** A comment request resolved to an id and an anchor (paragraph index or placeholder). */\ninterface AssignedComment {\n id: number;\n anchor: { kind: \"paragraph\"; index: number } | { kind: \"placeholder\"; key: string };\n options: PatchComment;\n}\n\n/**\n * Document patching module for modifying existing .docx files.\n *\n * Uses compile-path stringifiers (zero class instantiation) to serialize\n * patch content — no Formatter, no XmlComponent instances.\n *\n * @module\n */\n\n// ── Patch content stringification ──\n\n/**\n * Lightweight BodyContext adapter for patch serialization.\n * Captures hyperlink and image relationships for post-processing.\n */\nfunction createPatchContext(\n file: { media: Media },\n hyperlinkSink: Array<{ id: string; link: string }>,\n): BodyContext {\n return {\n fileData: file as unknown as BodyContext[\"fileData\"],\n file: file as unknown as BodyContext[\"file\"],\n viewWrapper: {\n relationships: {\n addRelationship: (linkId: string, _type: string, target: string, _mode?: string) => {\n hyperlinkSink.push({ id: linkId, link: target });\n },\n relationshipCount: 0,\n },\n } as unknown as BodyContext[\"viewWrapper\"],\n stringifyChild: () => \"\",\n addRelationship: () => \"\",\n addMedia: () => \"\",\n };\n}\n\n/**\n * Serialize a patch child (SectionChild / ParagraphChild / string) into XML\n * elements via the compile-path stringifiers. Shared by the placeholder\n * replacer and body-level `append`. Relies on the module-level\n * {@link currentPatchCtx} for relationship/media sinks.\n */\nconst formatChildElement = (child: unknown): Element[] => {\n let xmlStr: string;\n\n if (typeof child === \"string\") {\n // Plain string → simple run\n xmlStr = `<w:r><w:t xml:space=\"preserve\">${escapeXml(child)}</w:t></w:r>`;\n } else if (typeof child === \"object\" && child !== null) {\n const obj = child as Record<string, unknown>;\n // SectionChild level (paragraph / table) — for DOCUMENT patches and append\n if (\"paragraph\" in obj) {\n xmlStr = stringifyParagraphInline(obj.paragraph as ParagraphOptions, currentPatchCtx);\n } else if (\"table\" in obj) {\n xmlStr = tableDesc.stringify(obj.table as TableOptions, currentPatchCtx) ?? \"\";\n } else {\n // ParagraphChild level — for PARAGRAPH patches\n // Try compile-path JSON child dispatch first\n const jr = stringifyChildDispatch(child as ParagraphChild, currentPatchCtx);\n if (jr !== undefined) {\n xmlStr = Array.isArray(jr) ? jr.join(\"\") : jr;\n } else {\n // RunOptions (plain objects with text/children/bold/etc.)\n xmlStr = stringifyRunInline(child as RunOptions, currentPatchCtx);\n }\n }\n } else {\n xmlStr = \"<w:r/>\";\n }\n\n const jsonObj = xml2js(xmlStr, { captureSpacesBetweenElements: true });\n return [jsonObj.elements![0]];\n};\n\nconst docxReplacer = createReplacer({\n ns: DOCX_NS,\n formatChild: formatChildElement,\n});\n\n/**\n * Splice block-level children into `<w:body>` before the trailing `<w:sectPr>`\n * (the final section properties). If the body has no trailing sectPr, append at\n * the end. Reuses {@link formatChildElement} so the same serialization and\n * relationship/media sinks apply as placeholder patches.\n */\nconst appendToBody = (root: Element, children: SectionChild[]): void => {\n const docEl = root.elements?.find((e) => e.name === \"w:document\");\n const body = docEl?.elements?.find((e) => e.name === \"w:body\");\n if (!body) return;\n const els = body.elements ?? (body.elements = []);\n const newEls: Element[] = [];\n for (const child of children) {\n newEls.push(...formatChildElement(child));\n }\n // Insert before the trailing <w:sectPr>; otherwise at the end.\n let insertAt = els.length;\n for (let i = els.length - 1; i >= 0; i--) {\n if (els[i].name === \"w:sectPr\") {\n insertAt = i;\n break;\n }\n }\n els.splice(insertAt, 0, ...newEls);\n};\n\n/** Current patch context — set per file in the main loop. */\nlet currentPatchCtx: BodyContext;\n\n/**\n * A patch operation: replace a placeholder with either inline run-level\n * content (`type: \"paragraph\"`) or block-level content (`type: \"document\"`).\n *\n * The `type` is a bare string literal (no PatchType enum) — the core\n * replacer already discriminates on `patch.type`.\n */\nexport type Patch =\n | { type: \"paragraph\"; children: (string | RunOptions | ParagraphChild)[] }\n | { type: \"document\"; children: SectionChild[] };\n\ninterface ImageRelationshipAddition {\n key: string;\n mediaDatas: { fileName: string }[];\n}\n\ninterface HyperlinkRelationshipAddition {\n key: string;\n hyperlink: { id: string; link: string };\n}\n\nexport interface PatchDocumentOptions<\n T extends OutputType = OutputType,\n> extends BasePatchOptions<T> {\n /** Placeholder substitutions: `{{key}}` (per delimiters) → patch content. */\n placeholders?: Readonly<Record<string, Patch>>;\n /** Literal find/replace: the find string → patch content (no delimiters added). */\n findReplace?: Readonly<Record<string, Patch>>;\n /** Core-properties metadata override (merged over the existing docProps/core.xml). */\n coreProperties?: Partial<CorePropertiesOptions>;\n keepOriginalStyles?: boolean;\n recursive?: boolean;\n /** Block-level content appended to the document body, before the final section break. */\n append?: SectionChild[];\n /**\n * Comments to inject. `paragraphs` anchors a comment to the Nth body paragraph\n * (0-based, wraps the whole paragraph); `placeholders` anchors to the run\n * containing a {{key}} (wrapped before placeholder substitution). Ids are\n * continued from any existing word/comments.xml; entries are merged in.\n */\n comments?: {\n paragraphs?: Readonly<Record<number, PatchComment[]>>;\n placeholders?: Readonly<Record<string, PatchComment[]>>;\n };\n}\n\nconst UTF16LE = new Uint8Array([0xff, 0xfe]);\nconst UTF16BE = new Uint8Array([0xfe, 0xff]);\n\nconst compareByteArrays = (a: Uint8Array, b: Uint8Array): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) {\n return false;\n }\n }\n return true;\n};\n\n/**\n * Patches an existing .docx document by replacing placeholders with new content.\n *\n * @publicApi\n */\nexport const patchDocument = async <T extends OutputType = OutputType>({\n outputType,\n data,\n placeholders,\n findReplace,\n coreProperties,\n append,\n comments,\n keepOriginalStyles = true,\n placeholderDelimiters = { end: \"}}\", start: \"{{\" } as const,\n recursive = true,\n}: PatchDocumentOptions<T>): Promise<OutputByType[T]> => {\n const zipContent = unzipSync(toUint8Array(data));\n const contexts = new Map<string, BodyContext>();\n const media = new Media();\n const file = { media } as BodyContext[\"file\"];\n\n // Resolve comment ids by continuation from any existing word/comments.xml.\n const assignedComments = buildAssignedComments(zipContent, comments);\n\n const map = new Map<string, Element>();\n\n const imageRelationshipAdditions: ImageRelationshipAddition[] = [];\n const hyperlinkRelationshipAdditions: HyperlinkRelationshipAddition[] = [];\n let hasMedia = false;\n\n const binaryContentMap = new Map<string, Uint8Array>();\n\n for (const [key, value] of Object.entries(zipContent)) {\n const startBytes = value.slice(0, 2);\n if (compareByteArrays(startBytes, UTF16LE) || compareByteArrays(startBytes, UTF16BE)) {\n binaryContentMap.set(key, value);\n continue;\n }\n\n if (!key.endsWith(\".xml\") && !key.endsWith(\".rels\")) {\n binaryContentMap.set(key, value);\n continue;\n }\n\n const json = toJson(strFromU8(value));\n\n if (key === \"word/document.xml\") {\n const document = json.elements?.find((i) => i.name === \"w:document\");\n if (document && document.attributes) {\n for (const ns of [\"mc\", \"wp\", \"r\", \"w15\", \"m\"] as const) {\n document.attributes[`xmlns:${ns}`] = DocumentAttributeNamespaces[ns];\n }\n document.attributes[\"mc:Ignorable\"] =\n `${document.attributes[\"mc:Ignorable\"] || \"\"} w15`.trim();\n }\n }\n\n if (key.startsWith(\"word/\") && !key.endsWith(\".xml.rels\")) {\n const hyperlinkSink: Array<{ id: string; link: string }> = [];\n\n const context: BodyContext = {\n fileData: file,\n file,\n viewWrapper: {\n relationships: {\n addRelationship: (\n linkId: string,\n _: string,\n target: string,\n __: (typeof TargetModeType)[keyof typeof TargetModeType],\n ) => {\n hyperlinkRelationshipAdditions.push({\n hyperlink: {\n id: linkId,\n link: target,\n },\n key,\n });\n },\n },\n } as unknown as ViewWrapper,\n stringifyChild: () => \"\",\n addRelationship: () => \"\",\n addMedia: () => \"\",\n };\n contexts.set(key, context);\n\n if (!placeholderDelimiters?.start.trim() || !placeholderDelimiters?.end.trim()) {\n throw new Error(\"Both start and end delimiters must be non-empty strings.\");\n }\n\n const { start, end } = placeholderDelimiters;\n\n // Create compile-path context for stringifying patch children\n const patchCtx = createPatchContext(file, hyperlinkSink);\n currentPatchCtx = patchCtx;\n\n // Anchor placeholder comments BEFORE substitution, wrapping the {{key}} run.\n if (key === \"word/document.xml\") {\n for (const ac of assignedComments) {\n if (ac.anchor.kind === \"placeholder\") {\n wrapPlaceholderComment(json, `${start}${ac.anchor.key}${end}`, ac.id);\n }\n }\n }\n\n // Build (find-text → patch) entries. Placeholders wrap the key in\n // delimiters; findReplace uses the literal key. Both share one engine.\n const entries: Array<{ find: string; patch: Patch }> = [];\n if (placeholders) {\n for (const [key, value] of Object.entries(placeholders)) {\n entries.push({ find: `${start}${key}${end}`, patch: value });\n }\n }\n if (findReplace) {\n for (const [key, value] of Object.entries(findReplace)) {\n entries.push({ find: key, patch: value });\n }\n }\n\n for (const { find: patchText, patch: patchValue } of entries) {\n while (true) {\n const { didFindOccurrence } = docxReplacer({\n context,\n json,\n keepOriginalStyles,\n patch: patchValue,\n patchText,\n });\n if (!recursive || !didFindOccurrence) {\n break;\n }\n }\n }\n\n // Append block-level children to the document body before the final sectPr.\n if (append && append.length > 0 && key === \"word/document.xml\") {\n appendToBody(json, append);\n }\n\n // Anchor paragraph comments AFTER body edits, wrapping the Nth paragraph.\n if (key === \"word/document.xml\") {\n for (const ac of assignedComments) {\n if (ac.anchor.kind === \"paragraph\") {\n wrapParagraphComment(json, ac.anchor.index, ac.id);\n }\n }\n }\n\n // Flush hyperlink relationships captured by the compile-path context\n for (const hl of hyperlinkSink) {\n hyperlinkRelationshipAdditions.push({\n hyperlink: { id: hl.id, link: hl.link },\n key,\n });\n }\n\n const mediaDatas = getReferencedMedia(JSON.stringify(json), media.array);\n if (mediaDatas.length > 0) {\n hasMedia = true;\n imageRelationshipAdditions.push({\n key,\n mediaDatas,\n });\n }\n }\n\n map.set(key, json);\n }\n\n // Merge injected comments into word/comments.xml + wire rels + content types.\n if (assignedComments.length > 0) {\n mergeCommentsPart(map, assignedComments, file);\n }\n\n for (const { key, mediaDatas } of imageRelationshipAdditions) {\n const relationshipKey = `word/_rels/${key.split(\"/\").pop()}.rels`;\n const relationshipsJson = map.get(relationshipKey) ?? createRelationshipFile();\n map.set(relationshipKey, relationshipsJson);\n\n const index = getNextRelationshipIndex(relationshipsJson);\n const newJson = replaceImagePlaceholders(\n JSON.stringify(map.get(key)),\n mediaDatas,\n index,\n \"plain\",\n );\n map.set(key, JSON.parse(newJson) as Element);\n\n for (let i = 0; i < mediaDatas.length; i++) {\n const { fileName } = mediaDatas[i];\n appendRelationship(\n relationshipsJson,\n index + i,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\",\n `media/${fileName}`,\n );\n }\n }\n\n for (const { key, hyperlink } of hyperlinkRelationshipAdditions) {\n const relationshipKey = `word/_rels/${key.split(\"/\").pop()}.rels`;\n\n const relationshipsJson = map.get(relationshipKey) ?? createRelationshipFile();\n map.set(relationshipKey, relationshipsJson);\n\n appendRelationship(\n relationshipsJson,\n hyperlink.id,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink\",\n hyperlink.link,\n TargetModeType.EXTERNAL,\n );\n }\n\n if (hasMedia) {\n const contentTypesJson = map.get(\"[Content_Types].xml\");\n\n if (!contentTypesJson) {\n throw new Error(\"Could not find content types file\");\n }\n\n appendContentType(contentTypesJson, \"image/png\", \"png\");\n appendContentType(contentTypesJson, \"image/jpeg\", \"jpeg\");\n appendContentType(contentTypesJson, \"image/jpeg\", \"jpg\");\n appendContentType(contentTypesJson, \"image/bmp\", \"bmp\");\n appendContentType(contentTypesJson, \"image/gif\", \"gif\");\n appendContentType(contentTypesJson, \"image/svg+xml\", \"svg\");\n }\n\n const files: Record<string, Uint8Array> = {};\n const XML_DECL = '<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>';\n\n for (const [key, value] of map) {\n files[key] =\n key === \"docProps/core.xml\" && coreProperties\n ? encoder.encode(XML_DECL + applyCorePropertiesOverride(value, coreProperties))\n : encoder.encode(js2xml(value));\n }\n\n for (const [key, value] of binaryContentMap) {\n files[key] = value;\n }\n\n for (const { data: mediaData, fileName } of media.array) {\n files[`word/media/${fileName}`] =\n mediaData instanceof Uint8Array ? mediaData : new Uint8Array(mediaData);\n }\n\n return await zipAndConvert(files, outputType, OoxmlMimeType.DOCX);\n};\n\nconst createRelationshipFile = (): Element => ({\n declaration: {\n attributes: {\n encoding: \"UTF-8\",\n standalone: \"yes\",\n version: \"1.0\",\n },\n },\n elements: [\n {\n attributes: {\n xmlns: \"http://schemas.openxmlformats.org/package/2006/relationships\",\n },\n elements: [],\n name: \"Relationships\",\n type: \"element\",\n },\n ],\n});\n\n/** Build a bare OOXML element node with attributes and no children. */\nfunction makeElement(name: string, attributes: Record<string, string> = {}): Element {\n return { type: \"element\", name, attributes, elements: [] };\n}\n\n/** A `<w:r>` carrying a `<w:commentReference>` (CommentReference run style). */\nfunction commentReferenceRun(id: number): Element {\n return xml2js(\n `<w:r><w:rPr><w:rStyle w:val=\"CommentReference\"/></w:rPr><w:commentReference w:id=\"${id}\"/></w:r>`,\n ).elements![0];\n}\n\n/** Largest `w:id` among existing `<w:comment>` entries (-1 when none). */\nfunction maxCommentId(commentsPart: Element | undefined): number {\n const root = commentsPart?.elements?.find((e) => e.name === \"w:comments\");\n let max = -1;\n for (const c of root?.elements ?? []) {\n if (c.name === \"w:comment\") {\n const id = Number(c.attributes?.[\"w:id\"]);\n if (Number.isFinite(id)) max = Math.max(max, id);\n }\n }\n return max;\n}\n\n/** Assign continuation ids to every requested comment anchor. */\nfunction buildAssignedComments(\n zipContent: Record<string, Uint8Array>,\n comments: PatchDocumentOptions[\"comments\"],\n): AssignedComment[] {\n if (!comments) return [];\n const raw = zipContent[\"word/comments.xml\"];\n let nextId = raw ? maxCommentId(toJson(strFromU8(raw))) + 1 : 0;\n const assigned: AssignedComment[] = [];\n if (comments.placeholders) {\n for (const [key, list] of Object.entries(comments.placeholders)) {\n for (const options of list) {\n assigned.push({ id: nextId++, anchor: { kind: \"placeholder\", key }, options });\n }\n }\n }\n if (comments.paragraphs) {\n for (const [indexStr, list] of Object.entries(comments.paragraphs)) {\n const index = Number(indexStr);\n for (const options of list) {\n assigned.push({ id: nextId++, anchor: { kind: \"paragraph\", index }, options });\n }\n }\n }\n return assigned;\n}\n\n/** Wrap the Nth body paragraph with comment range markers + a reference run. */\nfunction wrapParagraphComment(json: Element, index: number, commentId: number): void {\n const body = json.elements\n ?.find((e) => e.name === \"w:document\")\n ?.elements?.find((e) => e.name === \"w:body\");\n if (!body) return;\n const paragraphs = (body.elements ?? []).filter((e) => e.name === \"w:p\");\n const target = paragraphs[index];\n if (!target) {\n throw new Error(`patchDocument: no paragraph at index ${index} to comment`);\n }\n const els = target.elements ?? (target.elements = []);\n const start = makeElement(\"w:commentRangeStart\", { \"w:id\": String(commentId) });\n const end = makeElement(\"w:commentRangeEnd\", { \"w:id\": String(commentId) });\n // Insert start after a leading <w:pPr> (or at the head); end + ref at the tail.\n const insertAt = els.length > 0 && els[0].name === \"w:pPr\" ? 1 : 0;\n els.splice(insertAt, 0, start);\n els.push(end, commentReferenceRun(commentId));\n}\n\n/** Wrap the first run whose `<w:t>` contains the placeholder with comment markers. */\nfunction wrapPlaceholderComment(json: Element, placeholder: string, commentId: number): void {\n const body = json.elements\n ?.find((e) => e.name === \"w:document\")\n ?.elements?.find((e) => e.name === \"w:body\");\n if (!body) return;\n for (const p of body.elements ?? []) {\n if (p.name !== \"w:p\") continue;\n const els = p.elements ?? [];\n for (let i = 0; i < els.length; i++) {\n if (els[i].name !== \"w:r\") continue;\n const t = (els[i].elements ?? []).find((e) => e.name === \"w:t\");\n const text = t?.elements?.[0]?.text;\n if (typeof text === \"string\" && text.includes(placeholder)) {\n els.splice(i, 0, makeElement(\"w:commentRangeStart\", { \"w:id\": String(commentId) }));\n els.splice(\n i + 2,\n 0,\n makeElement(\"w:commentRangeEnd\", { \"w:id\": String(commentId) }),\n commentReferenceRun(commentId),\n );\n return; // only the first occurrence\n }\n }\n }\n}\n\n/**\n * Merge assigned comments into word/comments.xml (appending to an existing part\n * or creating one) and wire document.xml.rels + content types when newly added.\n */\nfunction mergeCommentsPart(\n map: Map<string, Element>,\n assigned: AssignedComment[],\n file: { media: Media },\n): void {\n const newOpts: CommentOptions[] = assigned.map((ac) => ({ id: ac.id, ...ac.options }));\n const ctx = createPatchContext(file, []);\n const newXml = commentsDesc.stringify({ children: newOpts }, ctx) ?? \"\";\n const existingEl = map.get(\"word/comments.xml\");\n const wasNew = !existingEl;\n\n if (existingEl) {\n // Append new <w:comment> elements to the existing <w:comments> root.\n const existingRoot = existingEl.elements?.find((e) => e.name === \"w:comments\");\n const newRoot = xml2js(newXml).elements?.find((e) => e.name === \"w:comments\");\n existingRoot?.elements?.push(...(newRoot?.elements ?? []));\n } else {\n map.set(\n \"word/comments.xml\",\n toJson(`<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>${newXml}`),\n );\n }\n\n if (wasNew) {\n const relsKey = \"word/_rels/document.xml.rels\";\n const relsJson = map.get(relsKey) ?? createRelationshipFile();\n map.set(relsKey, relsJson);\n appendRelationship(\n relsJson,\n getNextRelationshipIndex(relsJson),\n COMMENTS_REL_TYPE,\n \"comments.xml\",\n );\n }\n\n // Content types — ensure the comments Override exists (deduped). An existing\n // comments part normally already declares it, but defend against incomplete templates.\n const typesEl = map.get(\"[Content_Types].xml\")?.elements?.find((e) => e.name === \"Types\");\n if (typesEl) {\n const els = typesEl.elements ?? (typesEl.elements = []);\n if (\n !els.some((e) => e.name === \"Override\" && e.attributes?.[\"PartName\"] === \"/word/comments.xml\")\n ) {\n els.push(\n makeElement(\"Override\", {\n PartName: \"/word/comments.xml\",\n ContentType: COMMENTS_CONTENT_TYPE,\n }),\n );\n }\n }\n}\n","/**\n * Patch detector for discovering placeholders in document templates.\n *\n * @module\n */\nimport { DOCX_NS, createTraverser, strFromU8, toJson, unzipSync } from \"@office-open/core\";\nimport { toUint8Array } from \"@office-open/core\";\nimport type { DataType } from \"@office-open/core\";\n\n/**\n * Options for patch detection.\n *\n * @property data - The document template to scan for placeholders\n */\ninterface PatchDetectorOptions {\n data: DataType;\n}\n\n/**\n * Detects all placeholders present in a document template.\n *\n * Scans through all XML content in a .docx file to find placeholder text\n * enclosed in delimiters (default: {{placeholder}}). This is useful for\n * discovering what patches a template expects before performing replacement.\n *\n * @param options - Patch detector configuration\n * @returns Array of placeholder keys found in the document\n *\n * @example\n * ```typescript\n * const placeholders = await patchDetector({ data: templateBuffer });\n * // Returns: [\"name\", \"date\", \"address\"] if template contains {{name}}, {{date}}, {{address}}\n *\n * // Use detected placeholders to create patches\n * const patches = {};\n * for (const key of placeholders) {\n * patches[key] = {\n * type: \"paragraph\",\n * children: [new TextRun(getUserData(key))],\n * };\n * });\n * ```\n */\nexport const patchDetector = async ({ data }: PatchDetectorOptions): Promise<string[]> => {\n const zipContent = unzipSync(toUint8Array(data));\n const patches = new Set<string>();\n\n for (const [key, value] of Object.entries(zipContent)) {\n if (!key.endsWith(\".xml\") && !key.endsWith(\".rels\")) {\n continue;\n }\n if (key.startsWith(\"word/\") && !key.endsWith(\".xml.rels\")) {\n const json = toJson(strFromU8(value));\n const { traverse } = createTraverser(DOCX_NS);\n for (const p of traverse(json)) {\n for (const patch of findPatchKeys(p.text)) {\n patches.add(patch);\n }\n }\n }\n }\n return [...patches];\n};\n\n/**\n * Extracts placeholder keys from text using regex pattern.\n *\n * @param text - Text to search for placeholders\n * @returns Array of placeholder keys (without delimiters)\n */\nconst findPatchKeys = (text: string): string[] => {\n const pattern = /(?<=\\{\\{).+?(?=\\}\\})/gs;\n return text.match(pattern) ?? [];\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@office-open/docx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Generate, parse, and patch .docx documents with a declarative TypeScript API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"clippy",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@office-open/core": "0.
|
|
57
|
-
"@office-open/xml": "0.
|
|
56
|
+
"@office-open/core": "0.10.0",
|
|
57
|
+
"@office-open/xml": "0.10.0"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"dev": "basis build --stub",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"context-BrtlmvVE.mjs","names":[],"sources":["../src/parts/alt-chunk/alt-chunk-collection.ts","../src/parts/sub-doc/sub-doc-collection.ts","../src/parts/styles/external-styles-factory.ts","../src/context.ts"],"sourcesContent":["/**\n * AltChunk collection module for managing alternative format content parts.\n *\n * @module\n */\n\n/**\n * Stores alternative format chunk data for later serialization by the compiler.\n */\nexport interface AltChunkData {\n /** Unique key for this alt chunk (e.g., relId) */\n key: string;\n /** Raw content data */\n data: Uint8Array;\n /** Part sub-path within word/ (e.g., \"afchunks/afchunk1.html\") */\n path: string;\n /** File extension (e.g., \"html\", \"rtf\", \"txt\") */\n extension: string;\n /** MIME content type (e.g., \"text/html\", \"application/rtf\") */\n contentType: string;\n}\n\n/**\n * Manages alternative format chunk parts in a document.\n *\n * Stores external content (HTML, RTF, plain text) that will be\n * serialized into separate parts in the DOCX package.\n */\nexport class AltChunkCollection {\n private map: Map<string, AltChunkData>;\n\n public constructor() {\n this.map = new Map<string, AltChunkData>();\n }\n\n public addAltChunk(key: string, data: AltChunkData): void {\n this.map.set(key, data);\n }\n\n public get array(): AltChunkData[] {\n return [...this.map.values()];\n }\n}\n","/**\n * Sub-document collection module for managing sub-document parts.\n *\n * @module\n */\n\n/**\n * Stores sub-document data for later serialization by the compiler.\n */\nexport interface SubDocData {\n /** Raw document data (.docx bytes) */\n data: Uint8Array;\n /** Part sub-path within word/ (e.g., \"subdocs/subdoc1.docx\") */\n path: string;\n}\n\n/**\n * Manages sub-document parts in a document.\n */\nexport class SubDocCollection {\n private map: Map<string, SubDocData>;\n\n public constructor() {\n this.map = new Map<string, SubDocData>();\n }\n\n public addSubDoc(key: string, data: SubDocData): void {\n this.map.set(key, data);\n }\n\n public get array(): SubDocData[] {\n return [...this.map.values()];\n }\n}\n","/**\n * External styles factory module for WordprocessingML documents.\n *\n * Parses styles from external XML and returns raw XML strings.\n * No XmlComponent dependency.\n *\n * Reference: http://officeopenxml.com/WPstyles.php\n *\n * @module\n */\nimport { js2xml, xml2js } from \"@office-open/xml\";\nimport type { Element as XMLElement } from \"@office-open/xml\";\n\nimport type { StylesOptions } from \"./styles\";\n\n/**\n * Factory for creating styles from external XML sources.\n *\n * Parses styles from XML (typically from a styles.xml file)\n * and returns raw XML strings for each style element.\n */\nexport class ExternalStylesFactory {\n /**\n * Creates new Styles based on the given XML data.\n *\n * Parses the styles XML and converts each child to a raw XML string.\n */\n public newInstance(xmlData: string): StylesOptions {\n const xmlObj = xml2js(xmlData, { compact: false }) as XMLElement;\n\n let stylesXmlElement: XMLElement | undefined;\n for (const xmlElm of xmlObj.elements || []) {\n if (xmlElm.name === \"w:styles\") {\n stylesXmlElement = xmlElm;\n }\n }\n\n if (stylesXmlElement === undefined) {\n return { importedStyles: [], initialAttributes: {} };\n }\n\n const stylesElements = stylesXmlElement.elements || [];\n\n return {\n importedStyles: stylesElements.map((childElm) => ({\n _raw: js2xml({ elements: [childElm] }),\n })),\n initialAttributes: (stylesXmlElement.attributes as Record<string, string>) ?? {},\n };\n }\n}\n","/**\n * DOCX compilation context.\n *\n * DocxWriteContext holds all mutable state needed during document compilation.\n * generateDocument() creates a DocxWriteContext internally.\n *\n * @module\n */\n\nimport { Relationships } from \"@office-open/core\";\nimport { ChartCollection } from \"@office-open/core/chart\";\nimport type { ReadContext, WriteContext } from \"@office-open/core/descriptor\";\nimport { SmartArtCollection } from \"@office-open/core/smartart\";\nimport type { Element } from \"@office-open/xml\";\nimport { AltChunkCollection } from \"@parts/alt-chunk/alt-chunk-collection\";\nimport type { DocumentOptions } from \"@parts/core-properties\";\nimport type { SectionPropertiesOptions } from \"@parts/document/body/section-properties/section-properties\";\nimport { FontWrapper } from \"@parts/fonts/font-wrapper\";\nimport type { GlossaryDocumentOptions } from \"@parts/glossary-document\";\nimport type { HeaderFooterEntry } from \"@parts/header-footer\";\nimport { Numbering } from \"@parts/numbering\";\nimport type { ParagraphOptions } from \"@parts/paragraph/paragraph\";\nimport type { SettingsOptions } from \"@parts/settings/settings\";\nimport { Styles } from \"@parts/styles\";\nimport { ExternalStylesFactory } from \"@parts/styles/external-styles-factory\";\nimport { DefaultStylesFactory } from \"@parts/styles/factory\";\nimport { SubDocCollection } from \"@parts/sub-doc/sub-doc-collection\";\nimport type { WebSettingsOptions } from \"@parts/web-settings\";\nimport { Media } from \"@shared/media\";\nimport type { SectionOptions } from \"@shared/section\";\nimport type { SectionChild } from \"@shared/section\";\n\nimport type { DocxDocument } from \"./parse\";\n\n/** Interface for document view wrappers — provides relationships access. */\nexport interface ViewWrapper {\n relationships: Relationships;\n}\n\n// ── BodyContext ──\n\n/**\n * Context for body-level stringification.\n *\n * Pure JSON pipeline context — extends WriteContext for descriptor compatibility.\n * No dependency on XmlComponent Context (compile/ uses zero toXml calls).\n */\nexport interface BodyContext extends WriteContext {\n /** The root write context with all mutable document state. */\n fileData: DocxWriteContext;\n /** Alias for fileData — some descriptor internals access context.file. */\n file: DocxWriteContext;\n /** Current view wrapper for relationship access. */\n viewWrapper: { relationships: Relationships };\n /** Stringify a body-level child element — injected to break circular imports. */\n stringifyChild: (child: SectionChild, ctx: BodyContext) => string;\n}\n\n// ── DocxWriteContext ──\n\nexport class DocxWriteContext implements WriteContext {\n private _currentRelationshipId = 1;\n\n // --- Accessed by XmlComponent via context.file.* during toXml() ---\n declare public document: { relationships: Relationships };\n declare public numbering: Numbering;\n declare public media: Media;\n declare public charts: ChartCollection;\n declare public smartArts: SmartArtCollection;\n declare public altChunks: AltChunkCollection;\n declare public subDocs: SubDocCollection;\n declare public comments: { relationships: Relationships };\n declare public footNotes: {\n relationships: Relationships;\n notes: Map<number, (ParagraphOptions | string)[]>;\n };\n declare public endnotes: {\n relationships: Relationships;\n notes: Map<number, (ParagraphOptions | string)[]>;\n };\n\n // --- Additional state used by the compiler ---\n declare public fileRelationships: Relationships;\n declare public _settingsOptions: SettingsOptions;\n declare public styles: Styles;\n declare public fontTable: FontWrapper;\n declare public glossaryOptions: GlossaryDocumentOptions | undefined;\n declare public webSettings: WebSettingsOptions | undefined;\n\n // --- Section properties (one per section, raw options for descriptor pipeline) ---\n private _sectionProperties: SectionPropertiesOptions[] = [];\n public get sectionProperties(): readonly SectionPropertiesOptions[] {\n return this._sectionProperties;\n }\n\n // --- WriteContext interface (core descriptor pipeline) ---\n\n public addRelationship(_type: string, _target: string, _mode?: string): string {\n const id = this._currentRelationshipId++;\n return `rId${id}`;\n }\n\n public addMedia(_data: Uint8Array, _type: string): string {\n // DOCX media registration goes through Media.addImage() in compiler.\n return \"\";\n }\n\n // --- Internal tracking ---\n private _headers: HeaderFooterEntry[] = [];\n private _footers: HeaderFooterEntry[] = [];\n\n // --- Original input preserved for descriptor usage ---\n declare public _options: DocumentOptions;\n\n constructor(options: DocumentOptions) {\n this._options = options;\n\n this.numbering = new Numbering(options.numbering ? options.numbering : { config: [] });\n\n this.comments = { relationships: new Relationships() };\n this.fileRelationships = new Relationships();\n this.footNotes = { relationships: new Relationships(), notes: new Map() };\n this.endnotes = { relationships: new Relationships(), notes: new Map() };\n this.document = { relationships: new Relationships() };\n this._settingsOptions = {\n compatibility: options.compatibility,\n compatibilityModeVersion: options.compatabilityModeVersion,\n defaultTabStop: options.defaultTabStop,\n evenAndOddHeaders: options.evenAndOddHeaderAndFooters ? true : false,\n characterSpacingControl: options.characterSpacingControl,\n hyphenation: {\n autoHyphenation: options.hyphenation?.autoHyphenation,\n consecutiveHyphenLimit: options.hyphenation?.consecutiveHyphenLimit,\n doNotHyphenateCaps: options.hyphenation?.doNotHyphenateCaps,\n hyphenationZone: options.hyphenation?.hyphenationZone,\n },\n trackRevisions: options.features?.trackRevisions,\n updateFields: options.features?.updateFields,\n documentProtection: options.features?.documentProtection,\n view: options.view,\n zoom: options.zoom,\n writeProtection: options.writeProtection,\n displayBackgroundShape:\n options.displayBackgroundShape ?? (options.background?.image ? true : undefined),\n embedTrueTypeFonts: options.embedTrueTypeFonts,\n embedSystemFonts: options.embedSystemFonts,\n saveSubsetFonts: options.saveSubsetFonts,\n docVars: options.docVars,\n colorSchemeMapping: options.colorSchemeMapping,\n mailMerge: options.mailMerge,\n ...options.settings,\n };\n\n this.media = new Media();\n this.charts = new ChartCollection();\n this.smartArts = new SmartArtCollection();\n this.altChunks = new AltChunkCollection();\n this.subDocs = new SubDocCollection();\n\n if (options.externalStyles !== undefined) {\n const defaultFactory = new DefaultStylesFactory();\n const defaultStyles = defaultFactory.newInstance(options.styles?.default);\n const externalFactory = new ExternalStylesFactory();\n const externalStyles = externalFactory.newInstance(options.externalStyles);\n // Skip docDefaults AND latentStyles from default factory —\n // external styles already provide them; XSD requires docDefaults → latentStyles → style sequence\n const defaultStyleElements = defaultStyles.importedStyles!.slice(2);\n this.styles = new Styles({\n ...externalStyles,\n importedStyles: [...externalStyles.importedStyles!, ...defaultStyleElements],\n });\n } else if (options.styles) {\n const stylesFactory = new DefaultStylesFactory();\n const defaultStyles = stylesFactory.newInstance(options.styles.default);\n // importedStyles[0]=docDefaults, [1]=latentStyles, [2+]=builtin styles.\n // paragraphStyles/characterStyles stay available for numbering registration\n // below but are NOT emitted when round-tripping (the raw importedStyles\n // already carry every source style — emitting both would duplicate them).\n const {\n importedStyles: parsedStyles,\n paragraphStyles,\n characterStyles,\n ...restStyles\n } = options.styles;\n const merged = defaultStyles.importedStyles ? [...defaultStyles.importedStyles] : [];\n if (restStyles.docDefaultsXml) {\n const ddIdx = merged.findIndex((s) => s._raw.startsWith(\"<w:docDefaults\"));\n if (ddIdx >= 0) merged[ddIdx] = { _raw: restStyles.docDefaultsXml };\n }\n if (restStyles.latentStylesXml) {\n const latentIdx = merged.findIndex((s) => s._raw.startsWith(\"<w:latentStyles\"));\n if (latentIdx >= 0) merged[latentIdx] = { _raw: restStyles.latentStylesXml };\n }\n if (parsedStyles && parsedStyles.length > 0) {\n // Round-trip: emit verbatim source styles (suppress factory builtins\n // and structured re-emission).\n merged.splice(2);\n merged.push(...parsedStyles);\n this.styles = new Styles({ ...defaultStyles, importedStyles: merged, ...restStyles });\n } else {\n // Generation: factory builtins + structured custom styles.\n this.styles = new Styles({\n ...defaultStyles,\n paragraphStyles,\n characterStyles,\n ...restStyles,\n });\n }\n } else {\n const stylesFactory = new DefaultStylesFactory();\n this.styles = new Styles(stylesFactory.newInstance());\n }\n\n // Register numbering references from custom paragraph/character styles.\n // Style definitions may contain numbering properties whose concrete instances\n // are never created through the body paragraph processing path.\n if (options.styles?.paragraphStyles) {\n for (const style of options.styles.paragraphStyles) {\n const num = style.paragraph?.numbering;\n if (num) {\n this.numbering.createConcreteNumberingInstance(num.reference, num.instance ?? 0);\n }\n }\n }\n\n this.addDefaultRelationships();\n\n for (const section of options.sections) {\n this.addSection(section);\n }\n\n if (options.footnotes) {\n for (const key in options.footnotes) {\n this.footNotes.notes.set(parseFloat(key), options.footnotes[key].children);\n }\n }\n\n if (options.endnotes) {\n for (const key in options.endnotes) {\n this.endnotes.notes.set(parseFloat(key), options.endnotes[key].children);\n }\n }\n\n this.fontTable = new FontWrapper(options.fonts ?? []);\n this.glossaryOptions = options.glossary;\n this.webSettings = options.webSettings ?? undefined;\n\n if (options.glossary) {\n this.document.relationships.addRelationship(\n this._currentRelationshipId++,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument\",\n \"glossary/document.xml\",\n );\n }\n\n if (this.webSettings) {\n this.document.relationships.addRelationship(\n this._currentRelationshipId++,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings\",\n \"webSettings.xml\",\n );\n }\n }\n\n get headers(): HeaderFooterEntry[] {\n return this._headers;\n }\n\n get footers(): HeaderFooterEntry[] {\n return this._footers;\n }\n\n // --- Private helpers ---\n\n private addSection({ headers = {}, footers = {}, properties }: SectionOptions): void {\n const sectPrOptions: SectionPropertiesOptions = {\n ...properties,\n footerWrapperGroup: {\n default: footers.default ? this.createFooter(footers.default) : undefined,\n even: footers.even ? this.createFooter(footers.even) : undefined,\n first: footers.first ? this.createFooter(footers.first) : undefined,\n },\n headerWrapperGroup: {\n default: headers.default ? this.createHeader(headers.default) : undefined,\n even: headers.even ? this.createHeader(headers.even) : undefined,\n first: headers.first ? this.createHeader(headers.first) : undefined,\n },\n };\n this._sectionProperties.push(sectPrOptions);\n }\n\n private createHeader(header: SectionChild[]): HeaderFooterEntry {\n const referenceId = this._currentRelationshipId++;\n const entry: HeaderFooterEntry = {\n children: header,\n relationships: new Relationships(),\n referenceId,\n };\n this.addHeaderToDocument(entry);\n return entry;\n }\n\n private createFooter(footer: SectionChild[]): HeaderFooterEntry {\n const referenceId = this._currentRelationshipId++;\n const entry: HeaderFooterEntry = {\n children: footer,\n relationships: new Relationships(),\n referenceId,\n };\n this.addFooterToDocument(entry);\n return entry;\n }\n\n private addHeaderToDocument(header: HeaderFooterEntry): void {\n this._headers.push(header);\n this.document.relationships.addRelationship(\n header.referenceId,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header\",\n `header${this._headers.length}.xml`,\n );\n }\n\n private addFooterToDocument(footer: HeaderFooterEntry): void {\n this._footers.push(footer);\n this.document.relationships.addRelationship(\n footer.referenceId,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer\",\n `footer${this._footers.length}.xml`,\n );\n }\n\n private addDefaultRelationships(): void {\n this.fileRelationships.addRelationship(\n 1,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\",\n \"word/document.xml\",\n );\n this.fileRelationships.addRelationship(\n 2,\n \"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties\",\n \"docProps/core.xml\",\n );\n this.fileRelationships.addRelationship(\n 3,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties\",\n \"docProps/app.xml\",\n );\n this.fileRelationships.addRelationship(\n 4,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties\",\n \"docProps/custom.xml\",\n );\n\n this.document.relationships.addRelationship(\n this._currentRelationshipId++,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\",\n \"styles.xml\",\n );\n this.document.relationships.addRelationship(\n this._currentRelationshipId++,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering\",\n \"numbering.xml\",\n );\n this.document.relationships.addRelationship(\n this._currentRelationshipId++,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes\",\n \"footnotes.xml\",\n );\n this.document.relationships.addRelationship(\n this._currentRelationshipId++,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes\",\n \"endnotes.xml\",\n );\n this.document.relationships.addRelationship(\n this._currentRelationshipId++,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings\",\n \"settings.xml\",\n );\n // Comments is an optional part — only wire the document→comments relationship\n // when the document actually carries comments. Emitting it unconditionally\n // produces an orphan comments.xml that Word rejects as an OPC violation\n // (empty part with no [Content_Types] Override when content types are\n // passed through from the source on round-trip).\n if (this._options.comments?.children?.length) {\n this.document.relationships.addRelationship(\n this._currentRelationshipId++,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments\",\n \"comments.xml\",\n );\n }\n if (this._options.bibliography) {\n this.document.relationships.addRelationship(\n this._currentRelationshipId++,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/bibliography\",\n \"bibliography.xml\",\n );\n }\n\n // Theme — a raw-passthrough part. Only declare the document→theme\n // relationship when the source carried one, so Word can resolve theme\n // colors/fonts. Without it theme1.xml is an orphan part Word may reject.\n const themePart = this._options.rawParts?.find((p) => p.path.startsWith(\"word/theme/\"));\n if (themePart) {\n this.document.relationships.addRelationship(\n this._currentRelationshipId++,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme\",\n themePart.path.replace(/^word\\//, \"\"),\n );\n }\n\n // customXml storage — raw-passthrough parts. Declare the document→customXml\n // relationship for each item (itemProps are linked via the item's own .rels,\n // not directly by the document) so Word can bind cover-page metadata, etc.\n for (const part of this._options.rawParts ?? []) {\n if (\n part.path.startsWith(\"customXml/\") &&\n part.path.endsWith(\".xml\") &&\n !part.path.includes(\"/_rels/\") &&\n !part.path.includes(\"itemProps\")\n ) {\n this.document.relationships.addRelationship(\n this._currentRelationshipId++,\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml\",\n `../${part.path}`,\n );\n }\n }\n }\n}\n\n// ── DocxReadContext ──\n\n/**\n * DOCX-specific read context.\n *\n * Holds references to the parsed DocxDocument and cached style/numbering data\n * used throughout the DOCX parsing pipeline. Implements ReadContext for\n * descriptor pipeline compatibility.\n */\nexport class DocxReadContext implements ReadContext {\n /**\n * Path of the part currently being parsed. Each part carries its own .rels\n * with independent rId numbering, so drawings inside a part must resolve\n * image relationships against that part's rels. Defaults to the document body.\n */\n public currentPart = \"word/document.xml\";\n\n constructor(\n public docx: DocxDocument,\n public styleCache: Map<string, Element>,\n public numberingCache: Map<string, Element>,\n ) {}\n\n resolveRelationship(rId: string): string | undefined {\n const partMedia = this.docx.partRefs.partMedia.get(this.currentPart);\n if (partMedia) {\n const media = partMedia.get(rId);\n if (media) return media;\n }\n return (\n this.docx.partRefs.headers.get(rId) ??\n this.docx.partRefs.footers.get(rId) ??\n this.docx.partRefs.media.get(rId) ??\n this.docx.partRefs.charts.get(rId) ??\n this.docx.partRefs.diagramData.get(rId) ??\n this.docx.partRefs.afChunks.get(rId) ??\n this.docx.partRefs.subDocs.get(rId) ??\n this.docx.partRefs.hyperlinks.get(rId)\n );\n }\n\n /**\n * Run `fn` with `currentPart` temporarily set to `partPath`, restoring the\n * previous value afterwards. Use when parsing a sub-document part (header,\n * footer, footnotes, …) so its drawings resolve images from its own rels.\n */\n withPart<T>(partPath: string, fn: () => T): T {\n const prev = this.currentPart;\n this.currentPart = partPath;\n try {\n return fn();\n } finally {\n this.currentPart = prev;\n }\n }\n\n getPart(path: string): Element | undefined {\n return this.docx.doc.get(path);\n }\n\n getRaw(path: string): Uint8Array | undefined {\n return this.docx.doc.getRaw(path);\n }\n}\n"],"mappings":";;;;;;;;;;;;AA4BA,IAAa,qBAAb,MAAgC;CAC9B;CAEA,cAAqB;EACnB,KAAK,sBAAM,IAAI,IAA0B;CAC3C;CAEA,YAAmB,KAAa,MAA0B;EACxD,KAAK,IAAI,IAAI,KAAK,IAAI;CACxB;CAEA,IAAW,QAAwB;EACjC,OAAO,CAAC,GAAG,KAAK,IAAI,OAAO,CAAC;CAC9B;AACF;;;;;;ACvBA,IAAa,mBAAb,MAA8B;CAC5B;CAEA,cAAqB;EACnB,KAAK,sBAAM,IAAI,IAAwB;CACzC;CAEA,UAAiB,KAAa,MAAwB;EACpD,KAAK,IAAI,IAAI,KAAK,IAAI;CACxB;CAEA,IAAW,QAAsB;EAC/B,OAAO,CAAC,GAAG,KAAK,IAAI,OAAO,CAAC;CAC9B;AACF;;;;;;;;;;;;;;;;;;;ACZA,IAAa,wBAAb,MAAmC;;;;;;CAMjC,YAAmB,SAAgC;EACjD,MAAM,SAAS,OAAO,SAAS,EAAE,SAAS,MAAM,CAAC;EAEjD,IAAI;EACJ,KAAK,MAAM,UAAU,OAAO,YAAY,CAAC,GACvC,IAAI,OAAO,SAAS,YAClB,mBAAmB;EAIvB,IAAI,qBAAqB,KAAA,GACvB,OAAO;GAAE,gBAAgB,CAAC;GAAG,mBAAmB,CAAC;EAAE;EAKrD,OAAO;GACL,iBAHqB,iBAAiB,YAAY,CAAC,GAGpB,KAAK,cAAc,EAChD,MAAM,OAAO,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC,EACvC,EAAE;GACF,mBAAoB,iBAAiB,cAAyC,CAAC;EACjF;CACF;AACF;;;;;;;;;;;ACUA,IAAa,mBAAb,MAAsD;CACpD,yBAAiC;CA6BjC,qBAAyD,CAAC;CAC1D,IAAW,oBAAyD;EAClE,OAAO,KAAK;CACd;CAIA,gBAAuB,OAAe,SAAiB,OAAwB;EAE7E,OAAO,MAAM,KADG;CAElB;CAEA,SAAgB,OAAmB,OAAuB;EAExD,OAAO;CACT;CAGA,WAAwC,CAAC;CACzC,WAAwC,CAAC;CAKzC,YAAY,SAA0B;EACpC,KAAK,WAAW;EAEhB,KAAK,YAAY,IAAI,UAAU,QAAQ,YAAY,QAAQ,YAAY,EAAE,QAAQ,CAAC,EAAE,CAAC;EAErF,KAAK,WAAW,EAAE,eAAe,IAAI,cAAc,EAAE;EACrD,KAAK,oBAAoB,IAAI,cAAc;EAC3C,KAAK,YAAY;GAAE,eAAe,IAAI,cAAc;GAAG,uBAAO,IAAI,IAAI;EAAE;EACxE,KAAK,WAAW;GAAE,eAAe,IAAI,cAAc;GAAG,uBAAO,IAAI,IAAI;EAAE;EACvE,KAAK,WAAW,EAAE,eAAe,IAAI,cAAc,EAAE;EACrD,KAAK,mBAAmB;GACtB,eAAe,QAAQ;GACvB,0BAA0B,QAAQ;GAClC,gBAAgB,QAAQ;GACxB,mBAAmB,QAAQ,6BAA6B,OAAO;GAC/D,yBAAyB,QAAQ;GACjC,aAAa;IACX,iBAAiB,QAAQ,aAAa;IACtC,wBAAwB,QAAQ,aAAa;IAC7C,oBAAoB,QAAQ,aAAa;IACzC,iBAAiB,QAAQ,aAAa;GACxC;GACA,gBAAgB,QAAQ,UAAU;GAClC,cAAc,QAAQ,UAAU;GAChC,oBAAoB,QAAQ,UAAU;GACtC,MAAM,QAAQ;GACd,MAAM,QAAQ;GACd,iBAAiB,QAAQ;GACzB,wBACE,QAAQ,2BAA2B,QAAQ,YAAY,QAAQ,OAAO,KAAA;GACxE,oBAAoB,QAAQ;GAC5B,kBAAkB,QAAQ;GAC1B,iBAAiB,QAAQ;GACzB,SAAS,QAAQ;GACjB,oBAAoB,QAAQ;GAC5B,WAAW,QAAQ;GACnB,GAAG,QAAQ;EACb;EAEA,KAAK,QAAQ,IAAI,MAAM;EACvB,KAAK,SAAS,IAAI,gBAAgB;EAClC,KAAK,YAAY,IAAI,mBAAmB;EACxC,KAAK,YAAY,IAAI,mBAAmB;EACxC,KAAK,UAAU,IAAI,iBAAiB;EAEpC,IAAI,QAAQ,mBAAmB,KAAA,GAAW;GAExC,MAAM,gBAAgB,IADK,qBACQ,EAAE,YAAY,QAAQ,QAAQ,OAAO;GAExE,MAAM,iBAAiB,IADK,sBACS,EAAE,YAAY,QAAQ,cAAc;GAGzE,MAAM,uBAAuB,cAAc,eAAgB,MAAM,CAAC;GAClE,KAAK,SAAS,IAAI,OAAO;IACvB,GAAG;IACH,gBAAgB,CAAC,GAAG,eAAe,gBAAiB,GAAG,oBAAoB;GAC7E,CAAC;EACH,OAAO,IAAI,QAAQ,QAAQ;GAEzB,MAAM,gBAAgB,IADI,qBACQ,EAAE,YAAY,QAAQ,OAAO,OAAO;GAKtE,MAAM,EACJ,gBAAgB,cAChB,iBACA,iBACA,GAAG,eACD,QAAQ;GACZ,MAAM,SAAS,cAAc,iBAAiB,CAAC,GAAG,cAAc,cAAc,IAAI,CAAC;GACnF,IAAI,WAAW,gBAAgB;IAC7B,MAAM,QAAQ,OAAO,WAAW,MAAM,EAAE,KAAK,WAAW,gBAAgB,CAAC;IACzE,IAAI,SAAS,GAAG,OAAO,SAAS,EAAE,MAAM,WAAW,eAAe;GACpE;GACA,IAAI,WAAW,iBAAiB;IAC9B,MAAM,YAAY,OAAO,WAAW,MAAM,EAAE,KAAK,WAAW,iBAAiB,CAAC;IAC9E,IAAI,aAAa,GAAG,OAAO,aAAa,EAAE,MAAM,WAAW,gBAAgB;GAC7E;GACA,IAAI,gBAAgB,aAAa,SAAS,GAAG;IAG3C,OAAO,OAAO,CAAC;IACf,OAAO,KAAK,GAAG,YAAY;IAC3B,KAAK,SAAS,IAAI,OAAO;KAAE,GAAG;KAAe,gBAAgB;KAAQ,GAAG;IAAW,CAAC;GACtF,OAEE,KAAK,SAAS,IAAI,OAAO;IACvB,GAAG;IACH;IACA;IACA,GAAG;GACL,CAAC;EAEL,OAAO;GACL,MAAM,gBAAgB,IAAI,qBAAqB;GAC/C,KAAK,SAAS,IAAI,OAAO,cAAc,YAAY,CAAC;EACtD;EAKA,IAAI,QAAQ,QAAQ,iBAClB,KAAK,MAAM,SAAS,QAAQ,OAAO,iBAAiB;GAClD,MAAM,MAAM,MAAM,WAAW;GAC7B,IAAI,KACF,KAAK,UAAU,gCAAgC,IAAI,WAAW,IAAI,YAAY,CAAC;EAEnF;EAGF,KAAK,wBAAwB;EAE7B,KAAK,MAAM,WAAW,QAAQ,UAC5B,KAAK,WAAW,OAAO;EAGzB,IAAI,QAAQ,WACV,KAAK,MAAM,OAAO,QAAQ,WACxB,KAAK,UAAU,MAAM,IAAI,WAAW,GAAG,GAAG,QAAQ,UAAU,KAAK,QAAQ;EAI7E,IAAI,QAAQ,UACV,KAAK,MAAM,OAAO,QAAQ,UACxB,KAAK,SAAS,MAAM,IAAI,WAAW,GAAG,GAAG,QAAQ,SAAS,KAAK,QAAQ;EAI3E,KAAK,YAAY,IAAI,YAAY,QAAQ,SAAS,CAAC,CAAC;EACpD,KAAK,kBAAkB,QAAQ;EAC/B,KAAK,cAAc,QAAQ,eAAe,KAAA;EAE1C,IAAI,QAAQ,UACV,KAAK,SAAS,cAAc,gBAC1B,KAAK,0BACL,wFACA,uBACF;EAGF,IAAI,KAAK,aACP,KAAK,SAAS,cAAc,gBAC1B,KAAK,0BACL,mFACA,iBACF;CAEJ;CAEA,IAAI,UAA+B;EACjC,OAAO,KAAK;CACd;CAEA,IAAI,UAA+B;EACjC,OAAO,KAAK;CACd;CAIA,WAAmB,EAAE,UAAU,CAAC,GAAG,UAAU,CAAC,GAAG,cAAoC;EACnF,MAAM,gBAA0C;GAC9C,GAAG;GACH,oBAAoB;IAClB,SAAS,QAAQ,UAAU,KAAK,aAAa,QAAQ,OAAO,IAAI,KAAA;IAChE,MAAM,QAAQ,OAAO,KAAK,aAAa,QAAQ,IAAI,IAAI,KAAA;IACvD,OAAO,QAAQ,QAAQ,KAAK,aAAa,QAAQ,KAAK,IAAI,KAAA;GAC5D;GACA,oBAAoB;IAClB,SAAS,QAAQ,UAAU,KAAK,aAAa,QAAQ,OAAO,IAAI,KAAA;IAChE,MAAM,QAAQ,OAAO,KAAK,aAAa,QAAQ,IAAI,IAAI,KAAA;IACvD,OAAO,QAAQ,QAAQ,KAAK,aAAa,QAAQ,KAAK,IAAI,KAAA;GAC5D;EACF;EACA,KAAK,mBAAmB,KAAK,aAAa;CAC5C;CAEA,aAAqB,QAA2C;EAC9D,MAAM,cAAc,KAAK;EACzB,MAAM,QAA2B;GAC/B,UAAU;GACV,eAAe,IAAI,cAAc;GACjC;EACF;EACA,KAAK,oBAAoB,KAAK;EAC9B,OAAO;CACT;CAEA,aAAqB,QAA2C;EAC9D,MAAM,cAAc,KAAK;EACzB,MAAM,QAA2B;GAC/B,UAAU;GACV,eAAe,IAAI,cAAc;GACjC;EACF;EACA,KAAK,oBAAoB,KAAK;EAC9B,OAAO;CACT;CAEA,oBAA4B,QAAiC;EAC3D,KAAK,SAAS,KAAK,MAAM;EACzB,KAAK,SAAS,cAAc,gBAC1B,OAAO,aACP,8EACA,SAAS,KAAK,SAAS,OAAO,KAChC;CACF;CAEA,oBAA4B,QAAiC;EAC3D,KAAK,SAAS,KAAK,MAAM;EACzB,KAAK,SAAS,cAAc,gBAC1B,OAAO,aACP,8EACA,SAAS,KAAK,SAAS,OAAO,KAChC;CACF;CAEA,0BAAwC;EACtC,KAAK,kBAAkB,gBACrB,GACA,sFACA,mBACF;EACA,KAAK,kBAAkB,gBACrB,GACA,yFACA,mBACF;EACA,KAAK,kBAAkB,gBACrB,GACA,2FACA,kBACF;EACA,KAAK,kBAAkB,gBACrB,GACA,yFACA,qBACF;EAEA,KAAK,SAAS,cAAc,gBAC1B,KAAK,0BACL,8EACA,YACF;EACA,KAAK,SAAS,cAAc,gBAC1B,KAAK,0BACL,iFACA,eACF;EACA,KAAK,SAAS,cAAc,gBAC1B,KAAK,0BACL,iFACA,eACF;EACA,KAAK,SAAS,cAAc,gBAC1B,KAAK,0BACL,gFACA,cACF;EACA,KAAK,SAAS,cAAc,gBAC1B,KAAK,0BACL,gFACA,cACF;EAMA,IAAI,KAAK,SAAS,UAAU,UAAU,QACpC,KAAK,SAAS,cAAc,gBAC1B,KAAK,0BACL,gFACA,cACF;EAEF,IAAI,KAAK,SAAS,cAChB,KAAK,SAAS,cAAc,gBAC1B,KAAK,0BACL,oFACA,kBACF;EAMF,MAAM,YAAY,KAAK,SAAS,UAAU,MAAM,MAAM,EAAE,KAAK,WAAW,aAAa,CAAC;EACtF,IAAI,WACF,KAAK,SAAS,cAAc,gBAC1B,KAAK,0BACL,6EACA,UAAU,KAAK,QAAQ,WAAW,EAAE,CACtC;EAMF,KAAK,MAAM,QAAQ,KAAK,SAAS,YAAY,CAAC,GAC5C,IACE,KAAK,KAAK,WAAW,YAAY,KACjC,KAAK,KAAK,SAAS,MAAM,KACzB,CAAC,KAAK,KAAK,SAAS,SAAS,KAC7B,CAAC,KAAK,KAAK,SAAS,WAAW,GAE/B,KAAK,SAAS,cAAc,gBAC1B,KAAK,0BACL,iFACA,MAAM,KAAK,MACb;CAGN;AACF;;;;;;;;AAWA,IAAa,kBAAb,MAAoD;CASzC;CACA;CACA;;;;;;CALT,cAAqB;CAErB,YACE,MACA,YACA,gBACA;EAHO,KAAA,OAAA;EACA,KAAA,aAAA;EACA,KAAA,iBAAA;CACN;CAEH,oBAAoB,KAAiC;EACnD,MAAM,YAAY,KAAK,KAAK,SAAS,UAAU,IAAI,KAAK,WAAW;EACnE,IAAI,WAAW;GACb,MAAM,QAAQ,UAAU,IAAI,GAAG;GAC/B,IAAI,OAAO,OAAO;EACpB;EACA,OACE,KAAK,KAAK,SAAS,QAAQ,IAAI,GAAG,KAClC,KAAK,KAAK,SAAS,QAAQ,IAAI,GAAG,KAClC,KAAK,KAAK,SAAS,MAAM,IAAI,GAAG,KAChC,KAAK,KAAK,SAAS,OAAO,IAAI,GAAG,KACjC,KAAK,KAAK,SAAS,YAAY,IAAI,GAAG,KACtC,KAAK,KAAK,SAAS,SAAS,IAAI,GAAG,KACnC,KAAK,KAAK,SAAS,QAAQ,IAAI,GAAG,KAClC,KAAK,KAAK,SAAS,WAAW,IAAI,GAAG;CAEzC;;;;;;CAOA,SAAY,UAAkB,IAAgB;EAC5C,MAAM,OAAO,KAAK;EAClB,KAAK,cAAc;EACnB,IAAI;GACF,OAAO,GAAG;EACZ,UAAU;GACR,KAAK,cAAc;EACrB;CACF;CAEA,QAAQ,MAAmC;EACzC,OAAO,KAAK,KAAK,IAAI,IAAI,IAAI;CAC/B;CAEA,OAAO,MAAsC;EAC3C,OAAO,KAAK,KAAK,IAAI,OAAO,IAAI;CAClC;AACF"}
|