@office-open/docx 0.6.4 → 0.6.5
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/index.d.mts +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +22 -602
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as __toCommonJS, n as __exportAll, r as __reExport, t as __esmMin } from "./chunk-090QGkrx.mjs";
|
|
2
|
-
import { AppProperties, BaseXmlComponent, BuilderElement, BuilderElement as BuilderElement$1, EMPTY_OBJECT, EmptyElement, Formatter, IgnoreIfEmptyXmlComponent, ImportedRootElementAttributes, ImportedXmlComponent, InitializableXmlComponent, NextAttributeComponent, OoxmlMimeType, PrettifyType, RawPassthrough, Relationships, TargetModeType, XmlAttributeComponent, XmlAttributeComponent as XmlAttributeComponent$1, XmlComponent, ZIP_STORED_LEVEL, addSmartArtRelationships, attrObj, chartAttr, convertEmuToPixels, convertInchesToTwip, convertMillimetersToTwip, convertPixelsToEmu, convertToXmlComponent, createDefault, createOverride, createPacker, getReferencedMedia, hasPlaceholders, hashedId, hpsMeasureObj, hpsMeasureObj as hpsMeasureObj$1, numberValObj, numberValObj as numberValObj$1, onOffObj, onOffObj as onOffObj$1, parseArchive, parseCorePropsElement, replaceChartPlaceholders, replaceImagePlaceholders, replaceSmartArtPlaceholders, strFromU8, stringContainerObj, stringEnumValObj, stringEnumValObj as stringEnumValObj$1, stringValObj, stringValObj as stringValObj$1, uniqueId, uniqueNumericIdCreator, uniqueNumericIdCreator as uniqueNumericIdCreator$1, uniqueUuid, unzipSync, wrapEl, xsdVerticalMergeRev, zipAndConvert } from "@office-open/core";
|
|
2
|
+
import { AppProperties, BaseXmlComponent, BuilderElement, BuilderElement as BuilderElement$1, DOCX_NS, EMPTY_OBJECT, EmptyElement, Formatter, Formatter as Formatter$1, IgnoreIfEmptyXmlComponent, ImportedRootElementAttributes, ImportedXmlComponent, InitializableXmlComponent, NextAttributeComponent, OoxmlMimeType, PrettifyType, RawPassthrough, Relationships, TargetModeType, XmlAttributeComponent, XmlAttributeComponent as XmlAttributeComponent$1, XmlComponent, ZIP_STORED_LEVEL, addSmartArtRelationships, appendContentType, appendRelationship, attrObj, chartAttr, convertEmuToPixels, convertInchesToTwip, convertMillimetersToTwip, convertPixelsToEmu, convertToXmlComponent, createDefault, createOverride, createPacker, createReplacer, createTraverser, getNextRelationshipIndex, getReferencedMedia, hasPlaceholders, hashedId, hpsMeasureObj, hpsMeasureObj as hpsMeasureObj$1, numberValObj, numberValObj as numberValObj$1, onOffObj, onOffObj as onOffObj$1, parseArchive, parseCorePropsElement, replaceChartPlaceholders, replaceImagePlaceholders, replaceSmartArtPlaceholders, strFromU8, stringContainerObj, stringEnumValObj, stringEnumValObj as stringEnumValObj$1, stringValObj, stringValObj as stringValObj$1, toJson, uniqueId, uniqueNumericIdCreator, uniqueNumericIdCreator as uniqueNumericIdCreator$1, uniqueUuid, unzipSync, wrapEl, xsdVerticalMergeRev, zipAndConvert } from "@office-open/core";
|
|
3
3
|
import { textToUint8Array, toUint8Array } from "undio";
|
|
4
4
|
import { PresetGeometry, buildFill, createBlipFill, createCustomGeometry, createEffectDag, createEffectList, createOutline, createScene3D, createShape3D, createTransform2D, extractBlipFillMedia } from "@office-open/core/drawingml";
|
|
5
5
|
import { ChartCollection, ChartSpace } from "@office-open/core/chart";
|
|
@@ -14559,7 +14559,7 @@ function replaceNumberingPlaceholders(xml, concreteNumberings) {
|
|
|
14559
14559
|
var Compiler = class {
|
|
14560
14560
|
formatter;
|
|
14561
14561
|
constructor() {
|
|
14562
|
-
this.formatter = new Formatter();
|
|
14562
|
+
this.formatter = new Formatter$1();
|
|
14563
14563
|
}
|
|
14564
14564
|
/**
|
|
14565
14565
|
* Compiles a File object into a flat file map suitable for fflate zipSync.
|
|
@@ -15097,581 +15097,30 @@ init_convenience_functions();
|
|
|
15097
15097
|
init_values();
|
|
15098
15098
|
__reExport(util_exports, values_exports);
|
|
15099
15099
|
//#endregion
|
|
15100
|
-
//#region src/patcher/util.ts
|
|
15101
|
-
/**
|
|
15102
|
-
* Utility functions for XML manipulation in document patching.
|
|
15103
|
-
*
|
|
15104
|
-
* @module
|
|
15105
|
-
*/
|
|
15106
|
-
init_text();
|
|
15107
|
-
/**
|
|
15108
|
-
* Converts XML string to JSON element structure.
|
|
15109
|
-
*
|
|
15110
|
-
* Parses XML text into a JavaScript object representation that can be
|
|
15111
|
-
* manipulated programmatically. Preserves spaces between elements for
|
|
15112
|
-
* accurate text handling.
|
|
15113
|
-
*
|
|
15114
|
-
* @param xmlData - The XML string to parse
|
|
15115
|
-
* @returns Parsed XML as an Element object
|
|
15116
|
-
*
|
|
15117
|
-
* @example
|
|
15118
|
-
* ```typescript
|
|
15119
|
-
* const element = toJson('<w:p><w:r><w:t>Hello</w:t></w:r></w:p>');
|
|
15120
|
-
* ```
|
|
15121
|
-
*/
|
|
15122
|
-
const toJson = (xmlData) => {
|
|
15123
|
-
return xml2js(xmlData, {
|
|
15124
|
-
captureSpacesBetweenElements: true,
|
|
15125
|
-
compact: false
|
|
15126
|
-
});
|
|
15127
|
-
};
|
|
15128
|
-
/**
|
|
15129
|
-
* Creates text element contents from a text string.
|
|
15130
|
-
*
|
|
15131
|
-
* Generates the XML element structure for a text node (w:t) by formatting
|
|
15132
|
-
* a Text component and extracting its element contents. Used when creating
|
|
15133
|
-
* new text runs during replacement operations.
|
|
15134
|
-
*
|
|
15135
|
-
* @param text - The text content to wrap in element structure
|
|
15136
|
-
* @returns Array of XML elements representing the text
|
|
15137
|
-
*
|
|
15138
|
-
* @example
|
|
15139
|
-
* ```typescript
|
|
15140
|
-
* const elements = createTextElementContents("Hello World");
|
|
15141
|
-
* // Returns XML elements for <w:t>Hello World</w:t>
|
|
15142
|
-
* ```
|
|
15143
|
-
*/
|
|
15144
|
-
const createTextElementContents = (text) => {
|
|
15145
|
-
return toJson(xml(buildText({ text }))).elements[0].elements ?? [];
|
|
15146
|
-
};
|
|
15147
|
-
/**
|
|
15148
|
-
* Adds xml:space="preserve" attribute to an element.
|
|
15149
|
-
*
|
|
15150
|
-
* The xml:space attribute instructs XML processors to preserve whitespace
|
|
15151
|
-
* in the element's content. This is important when text contains leading
|
|
15152
|
-
* or trailing spaces that must be maintained.
|
|
15153
|
-
*
|
|
15154
|
-
* @param element - The element to patch
|
|
15155
|
-
* @returns New element with xml:space attribute added
|
|
15156
|
-
*
|
|
15157
|
-
* @example
|
|
15158
|
-
* ```typescript
|
|
15159
|
-
* const patched = patchSpaceAttribute(textElement);
|
|
15160
|
-
* // Adds xml:space="preserve" to maintain whitespace
|
|
15161
|
-
* ```
|
|
15162
|
-
*/
|
|
15163
|
-
const patchSpaceAttribute = (element) => ({
|
|
15164
|
-
...element,
|
|
15165
|
-
attributes: { "xml:space": "preserve" }
|
|
15166
|
-
});
|
|
15167
|
-
/**
|
|
15168
|
-
* Retrieves first-level child elements by parent element name.
|
|
15169
|
-
*
|
|
15170
|
-
* Finds the first element with the specified name and returns its children.
|
|
15171
|
-
* Used to access collections like relationship elements or content type definitions.
|
|
15172
|
-
*
|
|
15173
|
-
* @param relationships - The parent XML element to search
|
|
15174
|
-
* @param id - The element name to find
|
|
15175
|
-
* @returns Array of child elements
|
|
15176
|
-
*
|
|
15177
|
-
* @example
|
|
15178
|
-
* ```typescript
|
|
15179
|
-
* const rels = getFirstLevelElements(relationshipsXml, "Relationships");
|
|
15180
|
-
* // Returns array of Relationship elements
|
|
15181
|
-
* ```
|
|
15182
|
-
*/
|
|
15183
|
-
const getFirstLevelElements = (relationships, id) => relationships.elements?.filter((e) => e.name === id)[0].elements ?? [];
|
|
15184
|
-
//#endregion
|
|
15185
|
-
//#region src/patcher/content-types-manager.ts
|
|
15186
|
-
/**
|
|
15187
|
-
* Appends a content type definition to the [Content_Types].xml structure.
|
|
15188
|
-
*
|
|
15189
|
-
* The [Content_Types].xml file declares the MIME types for all file extensions
|
|
15190
|
-
* in the OOXML package. This function adds a new content type if it doesn't
|
|
15191
|
-
* already exist, ensuring that newly added media files are properly declared.
|
|
15192
|
-
*
|
|
15193
|
-
* @param element - The [Content_Types].xml root element
|
|
15194
|
-
* @param contentType - The MIME type (e.g., "image/png")
|
|
15195
|
-
* @param extension - The file extension (e.g., "png")
|
|
15196
|
-
*
|
|
15197
|
-
* @example
|
|
15198
|
-
* ```typescript
|
|
15199
|
-
* appendContentType(contentTypesElement, "image/png", "png");
|
|
15200
|
-
* appendContentType(contentTypesElement, "image/jpeg", "jpg");
|
|
15201
|
-
* ```
|
|
15202
|
-
*/
|
|
15203
|
-
const appendContentType = (element, contentType, extension) => {
|
|
15204
|
-
const relationshipElements = getFirstLevelElements(element, "Types");
|
|
15205
|
-
if (relationshipElements.some((el) => el.type === "element" && el.name === "Default" && el?.attributes?.ContentType === contentType && el?.attributes?.Extension === extension)) return;
|
|
15206
|
-
relationshipElements.push({
|
|
15207
|
-
attributes: {
|
|
15208
|
-
ContentType: contentType,
|
|
15209
|
-
Extension: extension
|
|
15210
|
-
},
|
|
15211
|
-
name: "Default",
|
|
15212
|
-
type: "element"
|
|
15213
|
-
});
|
|
15214
|
-
};
|
|
15215
|
-
//#endregion
|
|
15216
|
-
//#region src/patcher/relationship-manager.ts
|
|
15217
|
-
/**
|
|
15218
|
-
* Extracts the numeric ID from a relationship ID string.
|
|
15219
|
-
*
|
|
15220
|
-
* @param relationshipId - Relationship ID in format "rId123"
|
|
15221
|
-
* @returns The numeric portion of the ID
|
|
15222
|
-
*/
|
|
15223
|
-
const getIdFromRelationshipId = (relationshipId) => {
|
|
15224
|
-
const output = parseInt(relationshipId.substring(3), 10);
|
|
15225
|
-
return isNaN(output) ? 0 : output;
|
|
15226
|
-
};
|
|
15227
|
-
/**
|
|
15228
|
-
* Determines the next available relationship ID number.
|
|
15229
|
-
*
|
|
15230
|
-
* Scans all existing relationships and returns the next sequential ID number
|
|
15231
|
-
* to use when adding a new relationship.
|
|
15232
|
-
*
|
|
15233
|
-
* @param relationships - The relationships XML element
|
|
15234
|
-
* @returns The next available relationship ID number
|
|
15235
|
-
*
|
|
15236
|
-
* @example
|
|
15237
|
-
* ```typescript
|
|
15238
|
-
* const nextId = getNextRelationshipIndex(relationshipsElement);
|
|
15239
|
-
* // If highest existing ID is rId5, returns 6
|
|
15240
|
-
* ```
|
|
15241
|
-
*/
|
|
15242
|
-
const getNextRelationshipIndex = (relationships) => {
|
|
15243
|
-
return getFirstLevelElements(relationships, "Relationships").map((e) => getIdFromRelationshipId(e.attributes?.Id?.toString() ?? "")).reduce((acc, curr) => Math.max(acc, curr), 0) + 1;
|
|
15244
|
-
};
|
|
15245
|
-
/**
|
|
15246
|
-
* Appends a new relationship to a .rels file structure.
|
|
15247
|
-
*
|
|
15248
|
-
* Relationships define connections between parts of an OOXML package,
|
|
15249
|
-
* such as linking documents to images, hyperlinks, or other resources.
|
|
15250
|
-
*
|
|
15251
|
-
* @param relationships - The relationships XML element
|
|
15252
|
-
* @param id - The relationship ID (number or string)
|
|
15253
|
-
* @param type - The relationship type URI
|
|
15254
|
-
* @param target - The target path or URI
|
|
15255
|
-
* @param targetMode - Optional target mode (Internal or External)
|
|
15256
|
-
* @returns The updated relationship elements array
|
|
15257
|
-
*
|
|
15258
|
-
* @example
|
|
15259
|
-
* ```typescript
|
|
15260
|
-
* appendRelationship(
|
|
15261
|
-
* relationshipsElement,
|
|
15262
|
-
* 6,
|
|
15263
|
-
* "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
15264
|
-
* "media/image1.png"
|
|
15265
|
-
* );
|
|
15266
|
-
* ```
|
|
15267
|
-
*/
|
|
15268
|
-
const appendRelationship = (relationships, id, type, target, targetMode) => {
|
|
15269
|
-
const relationshipElements = getFirstLevelElements(relationships, "Relationships");
|
|
15270
|
-
relationshipElements.push({
|
|
15271
|
-
attributes: {
|
|
15272
|
-
Id: `rId${id}`,
|
|
15273
|
-
Target: target,
|
|
15274
|
-
TargetMode: targetMode,
|
|
15275
|
-
Type: type
|
|
15276
|
-
},
|
|
15277
|
-
name: "Relationship",
|
|
15278
|
-
type: "element"
|
|
15279
|
-
});
|
|
15280
|
-
return relationshipElements;
|
|
15281
|
-
};
|
|
15282
|
-
//#endregion
|
|
15283
|
-
//#region src/patcher/paragraph-split-inject.ts
|
|
15284
|
-
var TokenNotFoundError = class extends Error {
|
|
15285
|
-
constructor(token) {
|
|
15286
|
-
super(`Token ${token} not found`);
|
|
15287
|
-
this.name = "TokenNotFoundError";
|
|
15288
|
-
}
|
|
15289
|
-
};
|
|
15290
|
-
/**
|
|
15291
|
-
* Finds the index of the run element containing a specific token.
|
|
15292
|
-
*
|
|
15293
|
-
* Searches through all run (w:r) elements in a paragraph to find which one
|
|
15294
|
-
* contains the specified token text. This is used to locate where new content
|
|
15295
|
-
* should be injected during replacement operations.
|
|
15296
|
-
*
|
|
15297
|
-
* @param paragraphElement - The paragraph element to search
|
|
15298
|
-
* @param token - The token text to find
|
|
15299
|
-
* @returns The index of the run element containing the token
|
|
15300
|
-
* @throws Error if the token is not found in any run
|
|
15301
|
-
*
|
|
15302
|
-
* @example
|
|
15303
|
-
* ```typescript
|
|
15304
|
-
* const index = findRunElementIndexWithToken(paragraph, "ɵ");
|
|
15305
|
-
* // Returns the index of the run containing the split token
|
|
15306
|
-
* ```
|
|
15307
|
-
*/
|
|
15308
|
-
const findRunElementIndexWithToken = (paragraphElement, token) => {
|
|
15309
|
-
for (let i = 0; i < (paragraphElement.elements ?? []).length; i++) {
|
|
15310
|
-
const element = paragraphElement.elements[i];
|
|
15311
|
-
if (element.type === "element" && element.name === "w:r") {
|
|
15312
|
-
const textElement = (element.elements ?? []).filter((e) => e.type === "element" && e.name === "w:t");
|
|
15313
|
-
for (const text of textElement) {
|
|
15314
|
-
if (!text.elements?.[0]) continue;
|
|
15315
|
-
if (text.elements[0].text?.includes(token)) return i;
|
|
15316
|
-
}
|
|
15317
|
-
}
|
|
15318
|
-
}
|
|
15319
|
-
throw new TokenNotFoundError(token);
|
|
15320
|
-
};
|
|
15321
|
-
/**
|
|
15322
|
-
* Splits a run element at a token position into left and right parts.
|
|
15323
|
-
*
|
|
15324
|
-
* Divides a run element at the location of a token, creating two separate
|
|
15325
|
-
* runs. This allows new content to be injected between the split parts while
|
|
15326
|
-
* preserving the original run's formatting properties.
|
|
15327
|
-
*
|
|
15328
|
-
* @param runElement - The run element to split
|
|
15329
|
-
* @param token - The token text marking the split point
|
|
15330
|
-
* @returns Object containing the left and right run elements
|
|
15331
|
-
*
|
|
15332
|
-
* @example
|
|
15333
|
-
* ```typescript
|
|
15334
|
-
* const { left, right } = splitRunElement(run, "ɵ");
|
|
15335
|
-
* // If run contains "Helloɵworld", left contains "Hello" and right contains "world"
|
|
15336
|
-
* ```
|
|
15337
|
-
*/
|
|
15338
|
-
const splitRunElement = (runElement, token) => {
|
|
15339
|
-
let splitIndex = -1;
|
|
15340
|
-
const splitElements = runElement.elements?.map((e, i) => {
|
|
15341
|
-
if (splitIndex !== -1) return e;
|
|
15342
|
-
if (e.type === "element" && e.name === "w:t") {
|
|
15343
|
-
const splitText = (e.elements?.[0]?.text ?? "").split(token);
|
|
15344
|
-
const newElements = splitText.map((t) => ({
|
|
15345
|
-
...e,
|
|
15346
|
-
...patchSpaceAttribute(e),
|
|
15347
|
-
elements: createTextElementContents(t)
|
|
15348
|
-
}));
|
|
15349
|
-
if (splitText.length > 1) splitIndex = i;
|
|
15350
|
-
return newElements;
|
|
15351
|
-
} else return e;
|
|
15352
|
-
}).flat() ?? [];
|
|
15353
|
-
return {
|
|
15354
|
-
left: {
|
|
15355
|
-
...JSON.parse(JSON.stringify(runElement)),
|
|
15356
|
-
elements: splitElements.slice(0, splitIndex + 1)
|
|
15357
|
-
},
|
|
15358
|
-
right: {
|
|
15359
|
-
...JSON.parse(JSON.stringify(runElement)),
|
|
15360
|
-
elements: splitElements.slice(splitIndex + 1)
|
|
15361
|
-
}
|
|
15362
|
-
};
|
|
15363
|
-
};
|
|
15364
|
-
//#endregion
|
|
15365
|
-
//#region src/patcher/paragraph-token-replacer.ts
|
|
15366
|
-
/**
|
|
15367
|
-
* Replacement modes for multi-run text replacement.
|
|
15368
|
-
*/
|
|
15369
|
-
const ReplaceMode = {
|
|
15370
|
-
/** Looking for the start of the replacement text */
|
|
15371
|
-
START: 0,
|
|
15372
|
-
/** Processing runs in the middle of the replacement text */
|
|
15373
|
-
MIDDLE: 1,
|
|
15374
|
-
/** Reached the end of the replacement text */
|
|
15375
|
-
END: 2
|
|
15376
|
-
};
|
|
15377
|
-
/**
|
|
15378
|
-
* Replaces a token with replacement text within a paragraph's run elements.
|
|
15379
|
-
*
|
|
15380
|
-
* Handles the complex case where placeholder text may span multiple runs
|
|
15381
|
-
* (text fragments) within a paragraph. Processes each run to replace the
|
|
15382
|
-
* appropriate portion of the token, handling start, middle, and end sections.
|
|
15383
|
-
*
|
|
15384
|
-
* @param paragraphElement - The paragraph XML element to modify
|
|
15385
|
-
* @param renderedParagraph - Pre-rendered paragraph structure with text positions
|
|
15386
|
-
* @param originalText - The token text to replace (e.g., "{{name}}")
|
|
15387
|
-
* @param replacementText - The text to replace it with (often a split token)
|
|
15388
|
-
* @returns The modified paragraph element
|
|
15389
|
-
*
|
|
15390
|
-
* @example
|
|
15391
|
-
* ```typescript
|
|
15392
|
-
* replaceTokenInParagraphElement({
|
|
15393
|
-
* paragraphElement,
|
|
15394
|
-
* renderedParagraph,
|
|
15395
|
-
* originalText: "{{placeholder}}",
|
|
15396
|
-
* replacementText: "ɵ",
|
|
15397
|
-
* });
|
|
15398
|
-
* ```
|
|
15399
|
-
*/
|
|
15400
|
-
const replaceTokenInParagraphElement = ({ paragraphElement, renderedParagraph, originalText, replacementText }) => {
|
|
15401
|
-
const startIndex = renderedParagraph.text.indexOf(originalText);
|
|
15402
|
-
const endIndex = startIndex + originalText.length - 1;
|
|
15403
|
-
let replaceMode = ReplaceMode.START;
|
|
15404
|
-
for (const run of renderedParagraph.runs) for (const { text, index, start, end } of run.parts) switch (replaceMode) {
|
|
15405
|
-
case ReplaceMode.START:
|
|
15406
|
-
if (startIndex >= start && startIndex <= end) {
|
|
15407
|
-
const offsetStartIndex = startIndex - start;
|
|
15408
|
-
const offsetEndIndex = Math.min(endIndex, end) - start;
|
|
15409
|
-
const partToReplace = text.substring(offsetStartIndex, offsetEndIndex + 1);
|
|
15410
|
-
if (partToReplace === "") continue;
|
|
15411
|
-
const firstPart = text.replace(partToReplace, replacementText);
|
|
15412
|
-
patchTextElement(paragraphElement.elements[run.index].elements[index], firstPart);
|
|
15413
|
-
replaceMode = ReplaceMode.MIDDLE;
|
|
15414
|
-
continue;
|
|
15415
|
-
}
|
|
15416
|
-
break;
|
|
15417
|
-
case ReplaceMode.MIDDLE:
|
|
15418
|
-
if (endIndex <= end) {
|
|
15419
|
-
const lastPart = text.substring(endIndex - start + 1);
|
|
15420
|
-
patchTextElement(paragraphElement.elements[run.index].elements[index], lastPart);
|
|
15421
|
-
const currentElement = paragraphElement.elements[run.index].elements[index];
|
|
15422
|
-
paragraphElement.elements[run.index].elements[index] = patchSpaceAttribute(currentElement);
|
|
15423
|
-
replaceMode = ReplaceMode.END;
|
|
15424
|
-
} else patchTextElement(paragraphElement.elements[run.index].elements[index], "");
|
|
15425
|
-
break;
|
|
15426
|
-
default:
|
|
15427
|
-
}
|
|
15428
|
-
return paragraphElement;
|
|
15429
|
-
};
|
|
15430
|
-
const patchTextElement = (element, text) => {
|
|
15431
|
-
element.elements = createTextElementContents(text);
|
|
15432
|
-
return element;
|
|
15433
|
-
};
|
|
15434
|
-
//#endregion
|
|
15435
|
-
//#region src/patcher/run-renderer.ts
|
|
15436
|
-
/**
|
|
15437
|
-
* Renders a paragraph element into a structured representation with text content.
|
|
15438
|
-
*
|
|
15439
|
-
* Extracts all text content from a paragraph (w:p) element by processing its
|
|
15440
|
-
* run (w:r) children. Calculates character positions for each text fragment
|
|
15441
|
-
* to enable precise text replacement operations.
|
|
15442
|
-
*
|
|
15443
|
-
* @param node - The paragraph element wrapper to render
|
|
15444
|
-
* @returns Rendered paragraph with text content, runs, and position information
|
|
15445
|
-
* @throws Error if the node is not a paragraph element
|
|
15446
|
-
*
|
|
15447
|
-
* @example
|
|
15448
|
-
* ```typescript
|
|
15449
|
-
* const rendered = renderParagraphNode(paragraphWrapper);
|
|
15450
|
-
* console.log(rendered.text); // "Hello World"
|
|
15451
|
-
* console.log(rendered.runs.length); // 2 (if text is in separate runs)
|
|
15452
|
-
* ```
|
|
15453
|
-
*/
|
|
15454
|
-
const renderParagraphNode = (node) => {
|
|
15455
|
-
if (node.element.name !== "w:p") throw new Error(`Invalid node type: ${node.element.name}`);
|
|
15456
|
-
if (!node.element.elements) return {
|
|
15457
|
-
index: -1,
|
|
15458
|
-
pathToParagraph: [],
|
|
15459
|
-
runs: [],
|
|
15460
|
-
text: ""
|
|
15461
|
-
};
|
|
15462
|
-
let currentRunStringLength = 0;
|
|
15463
|
-
const runs = node.element.elements.map((element, i) => ({
|
|
15464
|
-
element,
|
|
15465
|
-
i
|
|
15466
|
-
})).filter(({ element }) => element.name === "w:r").map(({ element, i }) => {
|
|
15467
|
-
const renderedRunNode = renderRunNode(element, i, currentRunStringLength);
|
|
15468
|
-
currentRunStringLength += renderedRunNode.text.length;
|
|
15469
|
-
return renderedRunNode;
|
|
15470
|
-
}).filter((e) => Boolean(e));
|
|
15471
|
-
const text = runs.reduce((acc, curr) => acc + curr.text, "");
|
|
15472
|
-
return {
|
|
15473
|
-
index: node.index,
|
|
15474
|
-
pathToParagraph: buildNodePath(node),
|
|
15475
|
-
runs,
|
|
15476
|
-
text
|
|
15477
|
-
};
|
|
15478
|
-
};
|
|
15479
|
-
const renderRunNode = (node, index, currentRunStringIndex) => {
|
|
15480
|
-
if (!node.elements) return {
|
|
15481
|
-
end: currentRunStringIndex,
|
|
15482
|
-
index: -1,
|
|
15483
|
-
parts: [],
|
|
15484
|
-
start: currentRunStringIndex,
|
|
15485
|
-
text: ""
|
|
15486
|
-
};
|
|
15487
|
-
let currentTextStringIndex = currentRunStringIndex;
|
|
15488
|
-
const parts = node.elements.map((element, i) => element.name === "w:t" && element.elements && element.elements.length > 0 ? (() => {
|
|
15489
|
-
const partStart = currentTextStringIndex;
|
|
15490
|
-
currentTextStringIndex += (element.elements[0].text?.toString() ?? "").length;
|
|
15491
|
-
return {
|
|
15492
|
-
end: currentTextStringIndex - 1,
|
|
15493
|
-
index: i,
|
|
15494
|
-
start: partStart,
|
|
15495
|
-
text: element.elements[0].text?.toString() ?? ""
|
|
15496
|
-
};
|
|
15497
|
-
})() : void 0).filter((e) => Boolean(e)).map((e) => e);
|
|
15498
|
-
const text = parts.reduce((acc, curr) => acc + curr.text, "");
|
|
15499
|
-
return {
|
|
15500
|
-
end: currentTextStringIndex - 1,
|
|
15501
|
-
index,
|
|
15502
|
-
parts,
|
|
15503
|
-
start: currentRunStringIndex,
|
|
15504
|
-
text
|
|
15505
|
-
};
|
|
15506
|
-
};
|
|
15507
|
-
const buildNodePath = (node) => node.parent ? [...buildNodePath(node.parent), node.index] : [node.index];
|
|
15508
|
-
//#endregion
|
|
15509
|
-
//#region src/patcher/traverser.ts
|
|
15510
|
-
const elementsToWrapper = (wrapper) => wrapper.element.elements?.map((e, i) => ({
|
|
15511
|
-
element: e,
|
|
15512
|
-
index: i,
|
|
15513
|
-
parent: wrapper
|
|
15514
|
-
})) ?? [];
|
|
15515
|
-
/**
|
|
15516
|
-
* Traverses an XML document tree to find and render all paragraphs.
|
|
15517
|
-
*
|
|
15518
|
-
* Uses breadth-first search to walk through the XML structure, identifying
|
|
15519
|
-
* all paragraph elements (w:p) and rendering their text content along with
|
|
15520
|
-
* positional information.
|
|
15521
|
-
*
|
|
15522
|
-
* @param node - The root XML element to traverse
|
|
15523
|
-
* @returns Array of rendered paragraph nodes with text content and positions
|
|
15524
|
-
*
|
|
15525
|
-
* @example
|
|
15526
|
-
* ```typescript
|
|
15527
|
-
* const paragraphs = traverse(documentElement);
|
|
15528
|
-
* paragraphs.forEach(p => console.log(p.text));
|
|
15529
|
-
* ```
|
|
15530
|
-
*/
|
|
15531
|
-
const traverse = (node) => {
|
|
15532
|
-
let renderedParagraphs = [];
|
|
15533
|
-
const queue = [...elementsToWrapper({
|
|
15534
|
-
element: node,
|
|
15535
|
-
index: 0,
|
|
15536
|
-
parent: void 0
|
|
15537
|
-
})];
|
|
15538
|
-
let currentNode;
|
|
15539
|
-
while (queue.length > 0) {
|
|
15540
|
-
currentNode = queue.shift();
|
|
15541
|
-
if (currentNode.element.name === "w:p") renderedParagraphs = [...renderedParagraphs, renderParagraphNode(currentNode)];
|
|
15542
|
-
queue.push(...elementsToWrapper(currentNode));
|
|
15543
|
-
}
|
|
15544
|
-
return renderedParagraphs;
|
|
15545
|
-
};
|
|
15546
|
-
/**
|
|
15547
|
-
* Finds all paragraphs containing specific text.
|
|
15548
|
-
*
|
|
15549
|
-
* Traverses the document and filters paragraphs to find those containing
|
|
15550
|
-
* the specified text string. Useful for locating placeholder text that
|
|
15551
|
-
* needs to be replaced.
|
|
15552
|
-
*
|
|
15553
|
-
* @param node - The root XML element to search
|
|
15554
|
-
* @param text - The text to search for
|
|
15555
|
-
* @returns Array of paragraph nodes containing the text
|
|
15556
|
-
*
|
|
15557
|
-
* @example
|
|
15558
|
-
* ```typescript
|
|
15559
|
-
* const matches = findLocationOfText(documentElement, "{{name}}");
|
|
15560
|
-
* // Returns all paragraphs containing "{{name}}"
|
|
15561
|
-
* ```
|
|
15562
|
-
*/
|
|
15563
|
-
const findLocationOfText = (node, text) => traverse(node).filter((p) => p.text.includes(text));
|
|
15564
|
-
//#endregion
|
|
15565
|
-
//#region src/patcher/replacer.ts
|
|
15566
|
-
/**
|
|
15567
|
-
* Replacer module for performing placeholder substitution in XML structures.
|
|
15568
|
-
*
|
|
15569
|
-
* @module
|
|
15570
|
-
*/
|
|
15571
|
-
const formatter = new Formatter();
|
|
15572
|
-
const SPLIT_TOKEN = "ɵ";
|
|
15573
|
-
/**
|
|
15574
|
-
* Replaces placeholder text in XML with new content from a patch.
|
|
15575
|
-
*
|
|
15576
|
-
* This function locates placeholder text within the XML structure and performs
|
|
15577
|
-
* the appropriate replacement based on the patch type (document or paragraph level).
|
|
15578
|
-
* It handles splitting runs, preserving styles, and injecting the new content.
|
|
15579
|
-
*
|
|
15580
|
-
* @param json - The XML element structure to search
|
|
15581
|
-
* @param patch - The patch definition containing replacement content
|
|
15582
|
-
* @param patchText - The placeholder text to find (e.g., "{{name}}")
|
|
15583
|
-
* @param context - The document context for formatting
|
|
15584
|
-
* @param keepOriginalStyles - Whether to preserve original text formatting
|
|
15585
|
-
* @returns Result containing the modified element and whether a replacement occurred
|
|
15586
|
-
*/
|
|
15587
|
-
const replacer = ({ json, patch, patchText, context, keepOriginalStyles = true }) => {
|
|
15588
|
-
const renderedParagraphs = findLocationOfText(json, patchText);
|
|
15589
|
-
if (renderedParagraphs.length === 0) return {
|
|
15590
|
-
didFindOccurrence: false,
|
|
15591
|
-
element: json
|
|
15592
|
-
};
|
|
15593
|
-
for (const renderedParagraph of renderedParagraphs) {
|
|
15594
|
-
const textJson = patch.children.map((c) => toJson(xml(formatter.format(c, context)))).map((c) => c.elements[0]);
|
|
15595
|
-
switch (patch.type) {
|
|
15596
|
-
case PatchType.DOCUMENT: {
|
|
15597
|
-
const parentElement = goToParentElementFromPath(json, renderedParagraph.pathToParagraph);
|
|
15598
|
-
const elementIndex = getLastElementIndexFromPath(renderedParagraph.pathToParagraph);
|
|
15599
|
-
parentElement.elements.splice(elementIndex, 1, ...textJson);
|
|
15600
|
-
break;
|
|
15601
|
-
}
|
|
15602
|
-
case PatchType.PARAGRAPH:
|
|
15603
|
-
default: {
|
|
15604
|
-
const paragraphElement = goToElementFromPath(json, renderedParagraph.pathToParagraph);
|
|
15605
|
-
replaceTokenInParagraphElement({
|
|
15606
|
-
originalText: patchText,
|
|
15607
|
-
paragraphElement,
|
|
15608
|
-
renderedParagraph,
|
|
15609
|
-
replacementText: SPLIT_TOKEN
|
|
15610
|
-
});
|
|
15611
|
-
const index = findRunElementIndexWithToken(paragraphElement, SPLIT_TOKEN);
|
|
15612
|
-
const runElementToBeReplaced = paragraphElement.elements[index];
|
|
15613
|
-
const { left, right } = splitRunElement(runElementToBeReplaced, SPLIT_TOKEN);
|
|
15614
|
-
let newRunElements = textJson;
|
|
15615
|
-
let patchedRightElement = right;
|
|
15616
|
-
if (keepOriginalStyles) {
|
|
15617
|
-
const runElementNonTextualElements = runElementToBeReplaced.elements.filter((e) => e.type === "element" && e.name === "w:rPr");
|
|
15618
|
-
newRunElements = textJson.map((e) => {
|
|
15619
|
-
if (e.type !== "element" || e.name !== "w:r" || e.elements?.some((c) => c.type === "element" && c.name === "w:rPr")) return e;
|
|
15620
|
-
return {
|
|
15621
|
-
...e,
|
|
15622
|
-
elements: [...runElementNonTextualElements, ...e.elements ?? []]
|
|
15623
|
-
};
|
|
15624
|
-
});
|
|
15625
|
-
patchedRightElement = {
|
|
15626
|
-
...right,
|
|
15627
|
-
elements: [...runElementNonTextualElements, ...right.elements]
|
|
15628
|
-
};
|
|
15629
|
-
}
|
|
15630
|
-
paragraphElement.elements.splice(index, 1, left, ...newRunElements, patchedRightElement);
|
|
15631
|
-
break;
|
|
15632
|
-
}
|
|
15633
|
-
}
|
|
15634
|
-
}
|
|
15635
|
-
return {
|
|
15636
|
-
didFindOccurrence: true,
|
|
15637
|
-
element: json
|
|
15638
|
-
};
|
|
15639
|
-
};
|
|
15640
|
-
const goToElementFromPath = (json, path) => {
|
|
15641
|
-
let element = json;
|
|
15642
|
-
for (let i = 1; i < path.length; i++) {
|
|
15643
|
-
const index = path[i];
|
|
15644
|
-
element = element.elements[index];
|
|
15645
|
-
}
|
|
15646
|
-
return element;
|
|
15647
|
-
};
|
|
15648
|
-
const goToParentElementFromPath = (json, path) => goToElementFromPath(json, path.slice(0, -1));
|
|
15649
|
-
const getLastElementIndexFromPath = (path) => path[path.length - 1];
|
|
15650
|
-
//#endregion
|
|
15651
15100
|
//#region src/patcher/from-docx.ts
|
|
15101
|
+
init_media();
|
|
15102
|
+
init_paragraph();
|
|
15103
|
+
init_relationship();
|
|
15104
|
+
init_convenience_functions();
|
|
15652
15105
|
/**
|
|
15653
15106
|
* Document patching module for modifying existing .docx files.
|
|
15654
15107
|
*
|
|
15655
|
-
* This module provides functionality to patch existing Word documents by replacing
|
|
15656
|
-
* placeholder text with new content while preserving the original document structure.
|
|
15657
|
-
*
|
|
15658
15108
|
* @module
|
|
15659
15109
|
*/
|
|
15660
|
-
|
|
15661
|
-
|
|
15662
|
-
|
|
15663
|
-
|
|
15110
|
+
const formatter = new Formatter();
|
|
15111
|
+
const docxReplacer = createReplacer({
|
|
15112
|
+
ns: DOCX_NS,
|
|
15113
|
+
formatChild: (child, context) => {
|
|
15114
|
+
return [toJson(xml(formatter.format(child, context))).elements[0]];
|
|
15115
|
+
}
|
|
15116
|
+
});
|
|
15664
15117
|
/**
|
|
15665
15118
|
* Patch type enumeration.
|
|
15666
15119
|
*
|
|
15667
|
-
* Determines how the replacement content should be inserted into the document.
|
|
15668
|
-
*
|
|
15669
15120
|
* @publicApi
|
|
15670
15121
|
*/
|
|
15671
15122
|
const PatchType = {
|
|
15672
|
-
/** Replace entire file-level elements (e.g., whole paragraphs) */
|
|
15673
15123
|
DOCUMENT: "file",
|
|
15674
|
-
/** Replace content within paragraphs (inline replacement) */
|
|
15675
15124
|
PARAGRAPH: "paragraph"
|
|
15676
15125
|
};
|
|
15677
15126
|
const UTF16LE = new Uint8Array([255, 254]);
|
|
@@ -15684,36 +15133,6 @@ const compareByteArrays = (a, b) => {
|
|
|
15684
15133
|
/**
|
|
15685
15134
|
* Patches an existing .docx document by replacing placeholders with new content.
|
|
15686
15135
|
*
|
|
15687
|
-
* This function opens an existing Word document, searches for placeholder text
|
|
15688
|
-
* (e.g., {{name}}), and replaces it with the provided content while preserving
|
|
15689
|
-
* the original document structure and optionally the original formatting.
|
|
15690
|
-
*
|
|
15691
|
-
* @param options - Configuration options for patching
|
|
15692
|
-
* @returns A promise resolving to the patched document in the specified output format
|
|
15693
|
-
*
|
|
15694
|
-
* @example
|
|
15695
|
-
* ```typescript
|
|
15696
|
-
* // Patch with paragraph content
|
|
15697
|
-
* const buffer = await patchDocument({
|
|
15698
|
-
* outputType: "nodebuffer",
|
|
15699
|
-
* data: templateBuffer,
|
|
15700
|
-
* patches: {
|
|
15701
|
-
* name: {
|
|
15702
|
-
* type: PatchType.PARAGRAPH,
|
|
15703
|
-
* children: [new TextRun({ text: "John Doe", bold: true })],
|
|
15704
|
-
* },
|
|
15705
|
-
* },
|
|
15706
|
-
* });
|
|
15707
|
-
*
|
|
15708
|
-
* // Patch with custom delimiters
|
|
15709
|
-
* const buffer = await patchDocument({
|
|
15710
|
-
* outputType: "nodebuffer",
|
|
15711
|
-
* data: templateBuffer,
|
|
15712
|
-
* patches: { ... },
|
|
15713
|
-
* placeholderDelimiters: { start: "<<", end: ">>" },
|
|
15714
|
-
* });
|
|
15715
|
-
* ```
|
|
15716
|
-
*
|
|
15717
15136
|
* @publicApi
|
|
15718
15137
|
*/
|
|
15719
15138
|
const patchDocument = async ({ outputType, data, patches, keepOriginalStyles, placeholderDelimiters = {
|
|
@@ -15722,7 +15141,7 @@ const patchDocument = async ({ outputType, data, patches, keepOriginalStyles, pl
|
|
|
15722
15141
|
}, recursive = true }) => {
|
|
15723
15142
|
const zipContent = unzipSync(toUint8Array(data));
|
|
15724
15143
|
const contexts = /* @__PURE__ */ new Map();
|
|
15725
|
-
const file = {
|
|
15144
|
+
const file = { media: new Media() };
|
|
15726
15145
|
const map = /* @__PURE__ */ new Map();
|
|
15727
15146
|
const imageRelationshipAdditions = [];
|
|
15728
15147
|
const hyperlinkRelationshipAdditions = [];
|
|
@@ -15757,7 +15176,7 @@ const patchDocument = async ({ outputType, data, patches, keepOriginalStyles, pl
|
|
|
15757
15176
|
fileData: file,
|
|
15758
15177
|
file,
|
|
15759
15178
|
stack: [],
|
|
15760
|
-
viewWrapper: {
|
|
15179
|
+
viewWrapper: { relationships: { addRelationship: (linkId, _, target, __) => {
|
|
15761
15180
|
hyperlinkRelationshipAdditions.push({
|
|
15762
15181
|
hyperlink: {
|
|
15763
15182
|
id: linkId,
|
|
@@ -15773,7 +15192,7 @@ const patchDocument = async ({ outputType, data, patches, keepOriginalStyles, pl
|
|
|
15773
15192
|
for (const [patchKey, patchValue] of Object.entries(patches)) {
|
|
15774
15193
|
const patchText = `${start}${patchKey}${end}`;
|
|
15775
15194
|
while (true) {
|
|
15776
|
-
const { didFindOccurrence } =
|
|
15195
|
+
const { didFindOccurrence } = docxReplacer({
|
|
15777
15196
|
context,
|
|
15778
15197
|
json,
|
|
15779
15198
|
keepOriginalStyles,
|
|
@@ -15838,14 +15257,11 @@ const patchDocument = async ({ outputType, data, patches, keepOriginalStyles, pl
|
|
|
15838
15257
|
appendContentType(contentTypesJson, "image/svg+xml", "svg");
|
|
15839
15258
|
}
|
|
15840
15259
|
const files = {};
|
|
15841
|
-
for (const [key, value] of map) files[key] = textToUint8Array(
|
|
15260
|
+
for (const [key, value] of map) files[key] = textToUint8Array(js2xml(value));
|
|
15842
15261
|
for (const [key, value] of binaryContentMap) files[key] = value;
|
|
15843
15262
|
for (const { data: mediaData, fileName } of file.media.array) files[`word/media/${fileName}`] = mediaData instanceof Uint8Array ? mediaData : new Uint8Array(mediaData);
|
|
15844
15263
|
return await zipAndConvert(files, outputType, OoxmlMimeType.DOCX);
|
|
15845
15264
|
};
|
|
15846
|
-
const toXml = (jsonObj) => {
|
|
15847
|
-
return js2xml(jsonObj, { attributeValueFn: (str) => String(str).replace(/&(?!amp;|lt;|gt;|quot;|apos;)/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'") });
|
|
15848
|
-
};
|
|
15849
15265
|
const createRelationshipFile = () => ({
|
|
15850
15266
|
declaration: { attributes: {
|
|
15851
15267
|
encoding: "UTF-8",
|
|
@@ -15896,7 +15312,11 @@ const patchDetector = async ({ data }) => {
|
|
|
15896
15312
|
const patches = /* @__PURE__ */ new Set();
|
|
15897
15313
|
for (const [key, value] of Object.entries(zipContent)) {
|
|
15898
15314
|
if (!key.endsWith(".xml") && !key.endsWith(".rels")) continue;
|
|
15899
|
-
if (key.startsWith("word/") && !key.endsWith(".xml.rels"))
|
|
15315
|
+
if (key.startsWith("word/") && !key.endsWith(".xml.rels")) {
|
|
15316
|
+
const json = toJson(strFromU8(value));
|
|
15317
|
+
const { traverse } = createTraverser(DOCX_NS);
|
|
15318
|
+
traverse(json).forEach((p) => findPatchKeys(p.text).forEach((patch) => patches.add(patch)));
|
|
15319
|
+
}
|
|
15900
15320
|
}
|
|
15901
15321
|
return [...patches];
|
|
15902
15322
|
};
|