easy-template-x 6.2.0 → 6.2.2
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/README.md +1 -1
- package/dist/cjs/easy-template-x.cjs +192 -106
- package/dist/es/easy-template-x.mjs +190 -107
- package/dist/types/compilation/delimiterSearcher.d.ts +0 -1
- package/dist/types/compilation/templateContext.d.ts +5 -0
- package/dist/types/office/contentTypesFile.d.ts +1 -1
- package/dist/types/office/mediaFiles.d.ts +1 -0
- package/dist/types/plugins/image/imagePlugin.d.ts +1 -0
- package/dist/types/plugins/index.d.ts +1 -0
- package/dist/types/utils/txt.d.ts +1 -0
- package/dist/types/xml/index.d.ts +4 -3
- package/dist/types/xml/xml.d.ts +3 -0
- package/dist/types/xml/xmlTreeIterator.d.ts +10 -0
- package/package.json +1 -1
- package/src/compilation/delimiterSearcher.ts +29 -56
- package/src/compilation/templateContext.ts +9 -0
- package/src/office/contentTypesFile.ts +20 -14
- package/src/office/mediaFiles.ts +19 -12
- package/src/plugins/image/imagePlugin.ts +49 -12
- package/src/plugins/index.ts +1 -0
- package/src/templateHandler.ts +5 -1
- package/src/utils/txt.ts +5 -0
- package/src/xml/index.ts +4 -3
- package/src/xml/xml.ts +22 -3
- package/src/xml/xmlTreeIterator.ts +67 -0
|
@@ -402,6 +402,10 @@ function stringValue(val) {
|
|
|
402
402
|
function normalizeDoubleQuotes(text) {
|
|
403
403
|
return text.replace(nonStandardDoubleQuotesRegex, standardDoubleQuotes);
|
|
404
404
|
}
|
|
405
|
+
function countOccurrences(text, substring) {
|
|
406
|
+
// https://stackoverflow.com/questions/4009756/how-to-count-string-occurrence-in-string
|
|
407
|
+
return (text.match(new RegExp(substring, 'g')) || []).length;
|
|
408
|
+
}
|
|
405
409
|
|
|
406
410
|
class JsZipHelper {
|
|
407
411
|
static toJsZipOutputType(binaryOrType) {
|
|
@@ -489,6 +493,71 @@ const XmlNodeType = Object.freeze({
|
|
|
489
493
|
const TEXT_NODE_NAME = '#text'; // see: https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeName
|
|
490
494
|
const COMMENT_NODE_NAME = '#comment';
|
|
491
495
|
|
|
496
|
+
class XmlDepthTracker {
|
|
497
|
+
depth = 0;
|
|
498
|
+
constructor(maxDepth) {
|
|
499
|
+
this.maxDepth = maxDepth;
|
|
500
|
+
}
|
|
501
|
+
increment() {
|
|
502
|
+
this.depth++;
|
|
503
|
+
if (this.depth > this.maxDepth) {
|
|
504
|
+
throw new MaxXmlDepthError(this.maxDepth);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
decrement() {
|
|
508
|
+
this.depth--;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
class XmlTreeIterator {
|
|
513
|
+
get node() {
|
|
514
|
+
return this._current;
|
|
515
|
+
}
|
|
516
|
+
constructor(initial, maxDepth) {
|
|
517
|
+
if (!initial) {
|
|
518
|
+
throw new InternalError("Initial node is required");
|
|
519
|
+
}
|
|
520
|
+
if (!maxDepth) {
|
|
521
|
+
throw new InternalError("Max depth is required");
|
|
522
|
+
}
|
|
523
|
+
this._current = initial;
|
|
524
|
+
this.depthTracker = new XmlDepthTracker(maxDepth);
|
|
525
|
+
}
|
|
526
|
+
next() {
|
|
527
|
+
if (!this._current) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
this._current = this.findNextNode(this._current);
|
|
531
|
+
return this._current;
|
|
532
|
+
}
|
|
533
|
+
setCurrent(node) {
|
|
534
|
+
this._current = node;
|
|
535
|
+
}
|
|
536
|
+
findNextNode(node) {
|
|
537
|
+
// Children
|
|
538
|
+
if (node.childNodes && node.childNodes.length) {
|
|
539
|
+
this.depthTracker.increment();
|
|
540
|
+
return node.childNodes[0];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Siblings
|
|
544
|
+
if (node.nextSibling) return node.nextSibling;
|
|
545
|
+
|
|
546
|
+
// Parent sibling
|
|
547
|
+
while (node.parentNode) {
|
|
548
|
+
if (node.parentNode.nextSibling) {
|
|
549
|
+
this.depthTracker.decrement();
|
|
550
|
+
return node.parentNode.nextSibling;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Go up
|
|
554
|
+
this.depthTracker.decrement();
|
|
555
|
+
node = node.parentNode;
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
492
561
|
class XmlUtils {
|
|
493
562
|
parser = new Parser();
|
|
494
563
|
create = new Create();
|
|
@@ -688,16 +757,19 @@ let Query$1 = class Query {
|
|
|
688
757
|
isTextNode(node) {
|
|
689
758
|
if (node.nodeType === XmlNodeType.Text || node.nodeName === TEXT_NODE_NAME) {
|
|
690
759
|
if (!(node.nodeType === XmlNodeType.Text && node.nodeName === TEXT_NODE_NAME)) {
|
|
691
|
-
throw new
|
|
760
|
+
throw new InternalError(`Invalid text node. Type: '${node.nodeType}', Name: '${node.nodeName}'.`);
|
|
692
761
|
}
|
|
693
762
|
return true;
|
|
694
763
|
}
|
|
695
764
|
return false;
|
|
696
765
|
}
|
|
766
|
+
isGeneralNode(node) {
|
|
767
|
+
return node.nodeType === XmlNodeType.General;
|
|
768
|
+
}
|
|
697
769
|
isCommentNode(node) {
|
|
698
770
|
if (node.nodeType === XmlNodeType.Comment || node.nodeName === COMMENT_NODE_NAME) {
|
|
699
771
|
if (!(node.nodeType === XmlNodeType.Comment && node.nodeName === COMMENT_NODE_NAME)) {
|
|
700
|
-
throw new
|
|
772
|
+
throw new InternalError(`Invalid comment node. Type: '${node.nodeType}', Name: '${node.nodeName}'.`);
|
|
701
773
|
}
|
|
702
774
|
return true;
|
|
703
775
|
}
|
|
@@ -776,6 +848,17 @@ let Query$1 = class Query {
|
|
|
776
848
|
range.push(lastNode);
|
|
777
849
|
return range;
|
|
778
850
|
}
|
|
851
|
+
descendants(node, maxDepth, predicate) {
|
|
852
|
+
const result = [];
|
|
853
|
+
const it = new XmlTreeIterator(node, maxDepth);
|
|
854
|
+
while (it.node) {
|
|
855
|
+
if (predicate(it.node)) {
|
|
856
|
+
result.push(it.node);
|
|
857
|
+
}
|
|
858
|
+
it.next();
|
|
859
|
+
}
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
779
862
|
};
|
|
780
863
|
let Modify$1 = class Modify {
|
|
781
864
|
/**
|
|
@@ -1028,22 +1111,6 @@ function recursiveRemoveEmptyTextNodes(node) {
|
|
|
1028
1111
|
}
|
|
1029
1112
|
const xml = new XmlUtils();
|
|
1030
1113
|
|
|
1031
|
-
class XmlDepthTracker {
|
|
1032
|
-
depth = 0;
|
|
1033
|
-
constructor(maxDepth) {
|
|
1034
|
-
this.maxDepth = maxDepth;
|
|
1035
|
-
}
|
|
1036
|
-
increment() {
|
|
1037
|
-
this.depth++;
|
|
1038
|
-
if (this.depth > this.maxDepth) {
|
|
1039
|
-
throw new MaxXmlDepthError(this.maxDepth);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
decrement() {
|
|
1043
|
-
this.depth--;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
1114
|
/**
|
|
1048
1115
|
* The types of relationships that can be created in a docx file.
|
|
1049
1116
|
* A non-comprehensive list.
|
|
@@ -1147,14 +1214,17 @@ class ContentTypesFile {
|
|
|
1147
1214
|
this.zip = zip;
|
|
1148
1215
|
}
|
|
1149
1216
|
async ensureContentType(mime) {
|
|
1150
|
-
//
|
|
1217
|
+
// Parse the content types file
|
|
1151
1218
|
await this.parseContentTypesFile();
|
|
1152
1219
|
|
|
1153
|
-
// already exists
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
//
|
|
1220
|
+
// Extension already exists
|
|
1221
|
+
//
|
|
1222
|
+
// Multiple extensions may map to the same mime type, but a single
|
|
1223
|
+
// extension must only map to one mime type.
|
|
1157
1224
|
const extension = MimeTypeHelper.getDefaultExtension(mime);
|
|
1225
|
+
if (this.contentTypes[extension]) return;
|
|
1226
|
+
|
|
1227
|
+
// Add new node
|
|
1158
1228
|
const typeNode = xml.create.generalNode('Default');
|
|
1159
1229
|
typeNode.attributes = {
|
|
1160
1230
|
"Extension": extension,
|
|
@@ -1162,13 +1232,13 @@ class ContentTypesFile {
|
|
|
1162
1232
|
};
|
|
1163
1233
|
this.root.childNodes.push(typeNode);
|
|
1164
1234
|
|
|
1165
|
-
//
|
|
1235
|
+
// Update state
|
|
1166
1236
|
this.addedNew = true;
|
|
1167
|
-
this.contentTypes[
|
|
1237
|
+
this.contentTypes[extension] = mime;
|
|
1168
1238
|
}
|
|
1169
|
-
async
|
|
1239
|
+
async xmlString() {
|
|
1170
1240
|
await this.parseContentTypesFile();
|
|
1171
|
-
return
|
|
1241
|
+
return xml.parser.serializeFile(this.root);
|
|
1172
1242
|
}
|
|
1173
1243
|
|
|
1174
1244
|
/**
|
|
@@ -1176,7 +1246,7 @@ class ContentTypesFile {
|
|
|
1176
1246
|
* Called automatically by the holding `Docx` before exporting.
|
|
1177
1247
|
*/
|
|
1178
1248
|
async save() {
|
|
1179
|
-
//
|
|
1249
|
+
// Not change - no need to save
|
|
1180
1250
|
if (!this.addedNew) return;
|
|
1181
1251
|
const xmlContent = xml.parser.serializeFile(this.root);
|
|
1182
1252
|
this.zip.setFile(ContentTypesFile.contentTypesFilePath, xmlContent);
|
|
@@ -1195,7 +1265,9 @@ class ContentTypesFile {
|
|
|
1195
1265
|
const genNode = node;
|
|
1196
1266
|
const contentTypeAttribute = genNode.attributes['ContentType'];
|
|
1197
1267
|
if (!contentTypeAttribute) continue;
|
|
1198
|
-
|
|
1268
|
+
const extensionAttribute = genNode.attributes['Extension'];
|
|
1269
|
+
if (!extensionAttribute) continue;
|
|
1270
|
+
this.contentTypes[extensionAttribute] = contentTypeAttribute;
|
|
1199
1271
|
}
|
|
1200
1272
|
}
|
|
1201
1273
|
}
|
|
@@ -1215,39 +1287,38 @@ class MediaFiles {
|
|
|
1215
1287
|
* Returns the media file path.
|
|
1216
1288
|
*/
|
|
1217
1289
|
async add(mediaFile, mime) {
|
|
1218
|
-
//
|
|
1290
|
+
// Check if already added
|
|
1219
1291
|
if (this.files.has(mediaFile)) return this.files.get(mediaFile);
|
|
1220
1292
|
|
|
1221
|
-
//
|
|
1293
|
+
// Hash existing media files
|
|
1222
1294
|
await this.hashMediaFiles();
|
|
1223
1295
|
|
|
1224
|
-
//
|
|
1296
|
+
// Hash the new file
|
|
1225
1297
|
// Note: Even though hashing the base64 string may seem inefficient
|
|
1226
1298
|
// (requires extra step in some cases) in practice it is significantly
|
|
1227
1299
|
// faster than hashing a 'binarystring'.
|
|
1228
1300
|
const base64 = await Binary.toBase64(mediaFile);
|
|
1229
1301
|
const hash = sha1(base64);
|
|
1230
1302
|
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1303
|
+
// Check if file already exists
|
|
1304
|
+
// Note: this can be optimized by keeping both mapping by filename as well as by hash
|
|
1233
1305
|
let path = Object.keys(this.hashes).find(p => this.hashes[p] === hash);
|
|
1234
1306
|
if (path) return path;
|
|
1235
1307
|
|
|
1236
|
-
//
|
|
1308
|
+
// Generate unique media file name
|
|
1309
|
+
const baseFilename = this.baseFilename(mime);
|
|
1237
1310
|
const extension = MimeTypeHelper.getDefaultExtension(mime);
|
|
1238
1311
|
do {
|
|
1239
1312
|
this.nextFileId++;
|
|
1240
|
-
path = `${MediaFiles.mediaDir}
|
|
1313
|
+
path = `${MediaFiles.mediaDir}/${baseFilename}${this.nextFileId}.${extension}`;
|
|
1241
1314
|
} while (this.hashes[path]);
|
|
1242
1315
|
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1316
|
+
// Add media to zip
|
|
1317
|
+
this.zip.setFile(path, mediaFile);
|
|
1245
1318
|
|
|
1246
|
-
//
|
|
1319
|
+
// Add media to our lookups
|
|
1247
1320
|
this.hashes[path] = hash;
|
|
1248
1321
|
this.files.set(mediaFile, path);
|
|
1249
|
-
|
|
1250
|
-
// return
|
|
1251
1322
|
return path;
|
|
1252
1323
|
}
|
|
1253
1324
|
async count() {
|
|
@@ -1263,9 +1334,15 @@ class MediaFiles {
|
|
|
1263
1334
|
if (!filename) continue;
|
|
1264
1335
|
const fileData = await this.zip.getFile(path).getContentBase64();
|
|
1265
1336
|
const fileHash = sha1(fileData);
|
|
1266
|
-
this.hashes[
|
|
1337
|
+
this.hashes[path] = fileHash;
|
|
1267
1338
|
}
|
|
1268
1339
|
}
|
|
1340
|
+
baseFilename(mime) {
|
|
1341
|
+
// Naive heuristic.
|
|
1342
|
+
// May need to be modified if we're going to support more mime types.
|
|
1343
|
+
const parts = mime.split('/');
|
|
1344
|
+
return parts[0];
|
|
1345
|
+
}
|
|
1269
1346
|
}
|
|
1270
1347
|
|
|
1271
1348
|
/**
|
|
@@ -2166,30 +2243,30 @@ class DelimiterSearcher {
|
|
|
2166
2243
|
|
|
2167
2244
|
const delimiters = [];
|
|
2168
2245
|
const match = new MatchState();
|
|
2169
|
-
const
|
|
2246
|
+
const it = new XmlTreeIterator(node, this.maxXmlDepth);
|
|
2170
2247
|
let lookForOpenDelimiter = true;
|
|
2171
|
-
while (node) {
|
|
2248
|
+
while (it.node) {
|
|
2172
2249
|
// Reset state on paragraph transition
|
|
2173
|
-
if (officeMarkup.query.isParagraphNode(node)) {
|
|
2250
|
+
if (officeMarkup.query.isParagraphNode(it.node)) {
|
|
2174
2251
|
match.reset();
|
|
2175
2252
|
}
|
|
2176
2253
|
|
|
2177
2254
|
// Skip irrelevant nodes
|
|
2178
|
-
if (!this.shouldSearchNode(
|
|
2179
|
-
|
|
2255
|
+
if (!this.shouldSearchNode(it)) {
|
|
2256
|
+
it.next();
|
|
2180
2257
|
continue;
|
|
2181
2258
|
}
|
|
2182
2259
|
|
|
2183
2260
|
// Search delimiters in text nodes
|
|
2184
|
-
match.openNodes.push(node);
|
|
2261
|
+
match.openNodes.push(it.node);
|
|
2185
2262
|
let textIndex = 0;
|
|
2186
|
-
while (textIndex < node.textContent.length) {
|
|
2263
|
+
while (textIndex < it.node.textContent.length) {
|
|
2187
2264
|
const delimiterPattern = lookForOpenDelimiter ? this.startDelimiter : this.endDelimiter;
|
|
2188
|
-
const char = node.textContent[textIndex];
|
|
2265
|
+
const char = it.node.textContent[textIndex];
|
|
2189
2266
|
|
|
2190
2267
|
// No match
|
|
2191
2268
|
if (char !== delimiterPattern[match.delimiterIndex]) {
|
|
2192
|
-
|
|
2269
|
+
textIndex = this.noMatch(it, textIndex, match);
|
|
2193
2270
|
textIndex++;
|
|
2194
2271
|
continue;
|
|
2195
2272
|
}
|
|
@@ -2207,14 +2284,14 @@ class DelimiterSearcher {
|
|
|
2207
2284
|
}
|
|
2208
2285
|
|
|
2209
2286
|
// Full delimiter match
|
|
2210
|
-
[
|
|
2287
|
+
[textIndex, lookForOpenDelimiter] = this.fullMatch(it, textIndex, lookForOpenDelimiter, match, delimiters);
|
|
2211
2288
|
textIndex++;
|
|
2212
2289
|
}
|
|
2213
|
-
|
|
2290
|
+
it.next();
|
|
2214
2291
|
}
|
|
2215
2292
|
return delimiters;
|
|
2216
2293
|
}
|
|
2217
|
-
noMatch(
|
|
2294
|
+
noMatch(it, textIndex, match) {
|
|
2218
2295
|
//
|
|
2219
2296
|
// Go back to first open node
|
|
2220
2297
|
//
|
|
@@ -2224,25 +2301,26 @@ class DelimiterSearcher {
|
|
|
2224
2301
|
// Delimiter is '{!' and template text contains the string '{{!'
|
|
2225
2302
|
//
|
|
2226
2303
|
if (match.firstMatchIndex !== -1) {
|
|
2227
|
-
node = first(match.openNodes);
|
|
2304
|
+
const node = first(match.openNodes);
|
|
2305
|
+
it.setCurrent(node);
|
|
2228
2306
|
textIndex = match.firstMatchIndex;
|
|
2229
2307
|
}
|
|
2230
2308
|
|
|
2231
2309
|
// Update state
|
|
2232
2310
|
match.reset();
|
|
2233
|
-
if (textIndex < node.textContent.length - 1) {
|
|
2234
|
-
match.openNodes.push(node);
|
|
2311
|
+
if (textIndex < it.node.textContent.length - 1) {
|
|
2312
|
+
match.openNodes.push(it.node);
|
|
2235
2313
|
}
|
|
2236
|
-
return
|
|
2314
|
+
return textIndex;
|
|
2237
2315
|
}
|
|
2238
|
-
fullMatch(
|
|
2316
|
+
fullMatch(it, textIndex, lookForOpenDelimiter, match, delimiters) {
|
|
2239
2317
|
// Move all delimiters characters to the same text node
|
|
2240
2318
|
if (match.openNodes.length > 1) {
|
|
2241
2319
|
const firstNode = first(match.openNodes);
|
|
2242
2320
|
const lastNode = last(match.openNodes);
|
|
2243
2321
|
officeMarkup.modify.joinTextNodesRange(firstNode, lastNode);
|
|
2244
|
-
textIndex += firstNode.textContent.length - node.textContent.length;
|
|
2245
|
-
|
|
2322
|
+
textIndex += firstNode.textContent.length - it.node.textContent.length;
|
|
2323
|
+
it.setCurrent(firstNode);
|
|
2246
2324
|
}
|
|
2247
2325
|
|
|
2248
2326
|
// Store delimiter
|
|
@@ -2252,41 +2330,18 @@ class DelimiterSearcher {
|
|
|
2252
2330
|
// Update state
|
|
2253
2331
|
lookForOpenDelimiter = !lookForOpenDelimiter;
|
|
2254
2332
|
match.reset();
|
|
2255
|
-
if (textIndex < node.textContent.length - 1) {
|
|
2256
|
-
match.openNodes.push(node);
|
|
2333
|
+
if (textIndex < it.node.textContent.length - 1) {
|
|
2334
|
+
match.openNodes.push(it.node);
|
|
2257
2335
|
}
|
|
2258
|
-
return [
|
|
2336
|
+
return [textIndex, lookForOpenDelimiter];
|
|
2259
2337
|
}
|
|
2260
|
-
shouldSearchNode(
|
|
2261
|
-
if (!xml.query.isTextNode(node)) return false;
|
|
2262
|
-
if (!node.textContent) return false;
|
|
2263
|
-
if (!node.parentNode) return false;
|
|
2264
|
-
if (!officeMarkup.query.isTextNode(node.parentNode)) return false;
|
|
2338
|
+
shouldSearchNode(it) {
|
|
2339
|
+
if (!xml.query.isTextNode(it.node)) return false;
|
|
2340
|
+
if (!it.node.textContent) return false;
|
|
2341
|
+
if (!it.node.parentNode) return false;
|
|
2342
|
+
if (!officeMarkup.query.isTextNode(it.node.parentNode)) return false;
|
|
2265
2343
|
return true;
|
|
2266
2344
|
}
|
|
2267
|
-
findNextNode(node, depth) {
|
|
2268
|
-
// Children
|
|
2269
|
-
if (node.childNodes && node.childNodes.length) {
|
|
2270
|
-
depth.increment();
|
|
2271
|
-
return node.childNodes[0];
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
// Siblings
|
|
2275
|
-
if (node.nextSibling) return node.nextSibling;
|
|
2276
|
-
|
|
2277
|
-
// Parent sibling
|
|
2278
|
-
while (node.parentNode) {
|
|
2279
|
-
if (node.parentNode.nextSibling) {
|
|
2280
|
-
depth.decrement();
|
|
2281
|
-
return node.parentNode.nextSibling;
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
// Go up
|
|
2285
|
-
depth.decrement();
|
|
2286
|
-
node = node.parentNode;
|
|
2287
|
-
}
|
|
2288
|
-
return null;
|
|
2289
|
-
}
|
|
2290
2345
|
createDelimiterMark(match, isOpenDelimiter) {
|
|
2291
2346
|
return {
|
|
2292
2347
|
index: match.firstMatchIndex,
|
|
@@ -2527,17 +2582,6 @@ class TemplatePlugin {
|
|
|
2527
2582
|
}
|
|
2528
2583
|
}
|
|
2529
2584
|
|
|
2530
|
-
/**
|
|
2531
|
-
* Apparently it is not that important for the ID to be unique...
|
|
2532
|
-
* Word displays two images correctly even if they both have the same ID.
|
|
2533
|
-
* Further more, Word will assign each a unique ID upon saving (it assigns
|
|
2534
|
-
* consecutive integers starting with 1).
|
|
2535
|
-
*
|
|
2536
|
-
* Note: The same principal applies to image names.
|
|
2537
|
-
*
|
|
2538
|
-
* Tested in Word v1908
|
|
2539
|
-
*/
|
|
2540
|
-
let nextImageId = 1;
|
|
2541
2585
|
class ImagePlugin extends TemplatePlugin {
|
|
2542
2586
|
contentType = 'image';
|
|
2543
2587
|
async simpleTagReplacements(tag, data, context) {
|
|
@@ -2554,12 +2598,47 @@ class ImagePlugin extends TemplatePlugin {
|
|
|
2554
2598
|
await context.docx.contentTypes.ensureContentType(content.format);
|
|
2555
2599
|
|
|
2556
2600
|
// Create the xml markup
|
|
2557
|
-
const imageId =
|
|
2601
|
+
const imageId = await this.getNextImageId(context);
|
|
2558
2602
|
const imageXml = this.createMarkup(imageId, relId, content);
|
|
2559
2603
|
const wordTextNode = officeMarkup.query.containingTextNode(tag.xmlTextNode);
|
|
2560
2604
|
xml.modify.insertAfter(imageXml, wordTextNode);
|
|
2561
2605
|
officeMarkup.modify.removeTag(tag.xmlTextNode);
|
|
2562
2606
|
}
|
|
2607
|
+
async getNextImageId(context) {
|
|
2608
|
+
// Init plugin context.
|
|
2609
|
+
if (!context.pluginContext[this.contentType]) {
|
|
2610
|
+
context.pluginContext[this.contentType] = {};
|
|
2611
|
+
}
|
|
2612
|
+
const pluginContext = context.pluginContext[this.contentType];
|
|
2613
|
+
if (!pluginContext.lastDrawingObjectId) {
|
|
2614
|
+
pluginContext.lastDrawingObjectId = {};
|
|
2615
|
+
}
|
|
2616
|
+
const lastIdMap = pluginContext.lastDrawingObjectId;
|
|
2617
|
+
const lastIdKey = context.currentPart.path;
|
|
2618
|
+
|
|
2619
|
+
// Get next image ID if already initialized.
|
|
2620
|
+
if (lastIdMap[lastIdKey]) {
|
|
2621
|
+
lastIdMap[lastIdKey]++;
|
|
2622
|
+
return lastIdMap[lastIdKey];
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// Init next image ID.
|
|
2626
|
+
const partRoot = await context.currentPart.xmlRoot();
|
|
2627
|
+
const maxDepth = context.options.maxXmlDepth;
|
|
2628
|
+
|
|
2629
|
+
// Get all existing doc props IDs
|
|
2630
|
+
// (docPr stands for "Drawing Object Non-Visual Properties", which isn't
|
|
2631
|
+
// exactly a good acronym but that's how it's called nevertheless)
|
|
2632
|
+
const docProps = xml.query.descendants(partRoot, maxDepth, node => {
|
|
2633
|
+
return xml.query.isGeneralNode(node) && node.nodeName === 'wp:docPr';
|
|
2634
|
+
});
|
|
2635
|
+
|
|
2636
|
+
// Start counting from the current max
|
|
2637
|
+
const ids = docProps.map(prop => parseInt(prop.attributes.id)).filter(isNumber);
|
|
2638
|
+
const maxId = Math.max(...ids, 0);
|
|
2639
|
+
lastIdMap[lastIdKey] = maxId + 1;
|
|
2640
|
+
return lastIdMap[lastIdKey];
|
|
2641
|
+
}
|
|
2563
2642
|
createMarkup(imageId, relId, content) {
|
|
2564
2643
|
// http://officeopenxml.com/drwPicInline.php
|
|
2565
2644
|
|
|
@@ -4616,7 +4695,7 @@ class TemplateHandler {
|
|
|
4616
4695
|
/**
|
|
4617
4696
|
* Version number of the `easy-template-x` library.
|
|
4618
4697
|
*/
|
|
4619
|
-
version = "6.2.
|
|
4698
|
+
version = "6.2.2" ;
|
|
4620
4699
|
constructor(options) {
|
|
4621
4700
|
this.options = new TemplateHandlerOptions(options);
|
|
4622
4701
|
|
|
@@ -4664,7 +4743,11 @@ class TemplateHandler {
|
|
|
4664
4743
|
scopeData.scopeDataResolver = this.options.scopeDataResolver;
|
|
4665
4744
|
const context = {
|
|
4666
4745
|
docx,
|
|
4667
|
-
currentPart: null
|
|
4746
|
+
currentPart: null,
|
|
4747
|
+
pluginContext: {},
|
|
4748
|
+
options: {
|
|
4749
|
+
maxXmlDepth: this.options.maxXmlDepth
|
|
4750
|
+
}
|
|
4668
4751
|
};
|
|
4669
4752
|
const contentParts = await docx.getContentParts();
|
|
4670
4753
|
for (const part of contentParts) {
|
|
@@ -4754,4 +4837,4 @@ class TemplateHandler {
|
|
|
4754
4837
|
}
|
|
4755
4838
|
}
|
|
4756
4839
|
|
|
4757
|
-
export { Base64, Binary, COMMENT_NODE_NAME, DelimiterSearcher, Delimiters, Docx, ImagePlugin, InternalArgumentMissingError, InternalError, LOOP_CONTENT_TYPE, LinkPlugin, LoopPlugin, MalformedFileError, MaxXmlDepthError, MimeType, MimeTypeHelper, MissingCloseDelimiterError, MissingStartDelimiterError, OfficeMarkup, OmlAttribute, OmlNode, OpenXmlPart, Path, PluginContent, RawXmlPlugin, Regex, RelType, Relationship, ScopeData, TEXT_CONTENT_TYPE, TEXT_NODE_NAME, TagDisposition, TagOptionsParseError, TagParser, TemplateCompiler, TemplateDataError, TemplateExtension, TemplateHandler, TemplateHandlerOptions, TemplatePlugin, TemplateSyntaxError, TextPlugin, UnclosedTagError, UnidentifiedFileTypeError, UnknownContentTypeError, UnopenedTagError, UnsupportedFileTypeError, Xlsx, XmlDepthTracker, XmlNodeType, XmlUtils, Zip, ZipObject, createDefaultPlugins, first, inheritsFrom, isNumber, isPromiseLike, last, normalizeDoubleQuotes, officeMarkup, pushMany, sha1, stringValue, toDictionary, xml };
|
|
4840
|
+
export { Base64, Binary, COMMENT_NODE_NAME, ChartPlugin, DelimiterSearcher, Delimiters, Docx, ImagePlugin, InternalArgumentMissingError, InternalError, LOOP_CONTENT_TYPE, LinkPlugin, LoopPlugin, MalformedFileError, MaxXmlDepthError, MimeType, MimeTypeHelper, MissingCloseDelimiterError, MissingStartDelimiterError, OfficeMarkup, OmlAttribute, OmlNode, OpenXmlPart, Path, PluginContent, RawXmlPlugin, Regex, RelType, Relationship, ScopeData, TEXT_CONTENT_TYPE, TEXT_NODE_NAME, TagDisposition, TagOptionsParseError, TagParser, TemplateCompiler, TemplateDataError, TemplateExtension, TemplateHandler, TemplateHandlerOptions, TemplatePlugin, TemplateSyntaxError, TextPlugin, UnclosedTagError, UnidentifiedFileTypeError, UnknownContentTypeError, UnopenedTagError, UnsupportedFileTypeError, Xlsx, XmlDepthTracker, XmlNodeType, XmlTreeIterator, XmlUtils, Zip, ZipObject, countOccurrences, createDefaultPlugins, first, inheritsFrom, isNumber, isPromiseLike, last, normalizeDoubleQuotes, officeMarkup, pushMany, sha1, stringValue, toDictionary, xml };
|
|
@@ -8,7 +8,7 @@ export declare class ContentTypesFile {
|
|
|
8
8
|
private readonly zip;
|
|
9
9
|
constructor(zip: Zip);
|
|
10
10
|
ensureContentType(mime: MimeType): Promise<void>;
|
|
11
|
-
|
|
11
|
+
xmlString(): Promise<string>;
|
|
12
12
|
save(): Promise<void>;
|
|
13
13
|
private parseContentTypesFile;
|
|
14
14
|
}
|
|
@@ -3,6 +3,7 @@ import { TemplatePlugin } from "src/plugins/templatePlugin";
|
|
|
3
3
|
export declare class ImagePlugin extends TemplatePlugin {
|
|
4
4
|
readonly contentType = "image";
|
|
5
5
|
simpleTagReplacements(tag: Tag, data: ScopeData, context: TemplateContext): Promise<void>;
|
|
6
|
+
private getNextImageId;
|
|
6
7
|
private createMarkup;
|
|
7
8
|
private docProperties;
|
|
8
9
|
private pictureMarkup;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
1
|
+
export * from "./xml";
|
|
2
|
+
export * from "./xmlDepthTracker";
|
|
3
|
+
export * from "./xmlNode";
|
|
4
|
+
export * from "./xmlTreeIterator";
|
package/dist/types/xml/xml.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { XmlGeneralNode, XmlNode } from "./xmlNode";
|
|
|
2
2
|
import { XmlCommentNode } from "./xmlNode";
|
|
3
3
|
import { XmlTextNode } from "./xmlNode";
|
|
4
4
|
import type { IMap } from "src/types";
|
|
5
|
+
export type XmlNodePredicate = (node: XmlNode) => boolean;
|
|
5
6
|
export declare class XmlUtils {
|
|
6
7
|
readonly parser: Parser;
|
|
7
8
|
readonly create: Create;
|
|
@@ -30,6 +31,7 @@ declare class Create {
|
|
|
30
31
|
}
|
|
31
32
|
declare class Query {
|
|
32
33
|
isTextNode(node: XmlNode): node is XmlTextNode;
|
|
34
|
+
isGeneralNode(node: XmlNode): node is XmlGeneralNode;
|
|
33
35
|
isCommentNode(node: XmlNode): node is XmlCommentNode;
|
|
34
36
|
lastTextChild(node: XmlNode, createIfMissing?: boolean): XmlTextNode;
|
|
35
37
|
findParent(node: XmlNode, predicate: (node: XmlNode) => boolean): XmlNode;
|
|
@@ -37,6 +39,7 @@ declare class Query {
|
|
|
37
39
|
findChild(node: XmlNode, predicate: (node: XmlNode) => boolean): XmlNode;
|
|
38
40
|
findChildByName(node: XmlNode, childName: string): XmlNode;
|
|
39
41
|
siblingsInRange(firstNode: XmlNode, lastNode: XmlNode): XmlNode[];
|
|
42
|
+
descendants(node: XmlNode, maxDepth: number, predicate: XmlNodePredicate): XmlNode[];
|
|
40
43
|
}
|
|
41
44
|
declare class Modify {
|
|
42
45
|
insertBefore(newNode: XmlNode, referenceNode: XmlNode): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { XmlNode } from "./xmlNode";
|
|
2
|
+
export declare class XmlTreeIterator<T extends XmlNode = XmlNode> {
|
|
3
|
+
get node(): T;
|
|
4
|
+
private _current;
|
|
5
|
+
private readonly depthTracker;
|
|
6
|
+
constructor(initial: XmlNode, maxDepth: number);
|
|
7
|
+
next(): XmlNode;
|
|
8
|
+
setCurrent(node: XmlNode): void;
|
|
9
|
+
private findNextNode;
|
|
10
|
+
}
|