easy-template-x 6.2.2 → 6.2.3
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/cjs/easy-template-x.cjs +71 -46
- package/dist/es/easy-template-x.mjs +71 -46
- package/dist/types/office/relationship.d.ts +2 -2
- package/package.json +16 -19
- package/src/compilation/tagParser.ts +1 -1
- package/src/delimiters.ts +1 -1
- package/src/office/officeMarkup.ts +2 -2
- package/src/office/openXmlPart.ts +1 -1
- package/src/office/relationship.ts +22 -6
- package/src/office/relsFile.ts +21 -22
- package/src/templateHandler.ts +9 -9
- package/src/utils/path.ts +8 -1
- package/src/xml/xml.ts +19 -19
- package/src/zip/jsZipHelper.ts +1 -1
- package/src/zip/zip.ts +3 -0
|
@@ -202,8 +202,16 @@ function isNumber(value) {
|
|
|
202
202
|
class Path {
|
|
203
203
|
static getFilename(path) {
|
|
204
204
|
const lastSlashIndex = path.lastIndexOf('/');
|
|
205
|
-
return path.
|
|
205
|
+
return path.substring(lastSlashIndex + 1);
|
|
206
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get the directory of a path.
|
|
210
|
+
* Exclude the last slash.
|
|
211
|
+
*
|
|
212
|
+
* Example:
|
|
213
|
+
* /folder/subfolder/file.txt -> /folder/subfolder
|
|
214
|
+
*/
|
|
207
215
|
static getDirectory(path) {
|
|
208
216
|
const lastSlashIndex = path.lastIndexOf('/');
|
|
209
217
|
return path.substring(0, lastSlashIndex);
|
|
@@ -461,6 +469,9 @@ class Zip {
|
|
|
461
469
|
this.binaryFormat = binaryFormat;
|
|
462
470
|
}
|
|
463
471
|
getFile(path) {
|
|
472
|
+
if (path && path.startsWith('/')) {
|
|
473
|
+
path = path.substring(1);
|
|
474
|
+
}
|
|
464
475
|
const internalZipObject = this.zip.files[path];
|
|
465
476
|
if (!internalZipObject) return null;
|
|
466
477
|
return new ZipObject(internalZipObject, this.binaryFormat);
|
|
@@ -872,7 +883,7 @@ let Modify$1 = class Modify {
|
|
|
872
883
|
insertBefore(newNode, referenceNode) {
|
|
873
884
|
if (!newNode) throw new InternalArgumentMissingError("newNode");
|
|
874
885
|
if (!referenceNode) throw new InternalArgumentMissingError("referenceNode");
|
|
875
|
-
if (!referenceNode.parentNode) throw new Error(`'
|
|
886
|
+
if (!referenceNode.parentNode) throw new Error(`'referenceNode' has no parent`);
|
|
876
887
|
const childNodes = referenceNode.parentNode.childNodes;
|
|
877
888
|
const beforeNodeIndex = childNodes.indexOf(referenceNode);
|
|
878
889
|
xml.modify.insertChild(referenceNode.parentNode, newNode, beforeNodeIndex);
|
|
@@ -887,7 +898,7 @@ let Modify$1 = class Modify {
|
|
|
887
898
|
insertAfter(newNode, referenceNode) {
|
|
888
899
|
if (!newNode) throw new InternalArgumentMissingError("newNode");
|
|
889
900
|
if (!referenceNode) throw new InternalArgumentMissingError("referenceNode");
|
|
890
|
-
if (!referenceNode.parentNode) throw new Error(`'
|
|
901
|
+
if (!referenceNode.parentNode) throw new Error(`'referenceNode' has no parent`);
|
|
891
902
|
const childNodes = referenceNode.parentNode.childNodes;
|
|
892
903
|
const referenceNodeIndex = childNodes.indexOf(referenceNode);
|
|
893
904
|
xml.modify.insertChild(referenceNode.parentNode, newNode, referenceNodeIndex + 1);
|
|
@@ -1024,7 +1035,7 @@ let Modify$1 = class Modify {
|
|
|
1024
1035
|
* `false` then the original child node is the first child of `right`.
|
|
1025
1036
|
*/
|
|
1026
1037
|
splitByChild(parent, child, removeChild) {
|
|
1027
|
-
if (child.parentNode != parent) throw new Error(`Node '
|
|
1038
|
+
if (child.parentNode != parent) throw new Error(`Node 'child' is not a direct child of 'parent'.`);
|
|
1028
1039
|
|
|
1029
1040
|
// create childless clone 'left'
|
|
1030
1041
|
const left = xml.create.cloneNode(parent, false);
|
|
@@ -1132,20 +1143,35 @@ const RelType = Object.freeze({
|
|
|
1132
1143
|
Table: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table'
|
|
1133
1144
|
});
|
|
1134
1145
|
class Relationship {
|
|
1135
|
-
static fromXml(xml) {
|
|
1146
|
+
static fromXml(partDir, xml) {
|
|
1136
1147
|
return new Relationship({
|
|
1137
1148
|
id: xml.attributes?.['Id'],
|
|
1138
1149
|
type: xml.attributes?.['Type'],
|
|
1139
|
-
target: Relationship.normalizeRelTarget(xml.attributes?.['Target']),
|
|
1150
|
+
target: Relationship.normalizeRelTarget(partDir, xml.attributes?.['Target']),
|
|
1140
1151
|
targetMode: xml.attributes?.['TargetMode']
|
|
1141
1152
|
});
|
|
1142
1153
|
}
|
|
1143
|
-
static normalizeRelTarget(target) {
|
|
1154
|
+
static normalizeRelTarget(partDir, target) {
|
|
1144
1155
|
if (!target) {
|
|
1145
1156
|
return target;
|
|
1146
1157
|
}
|
|
1158
|
+
|
|
1159
|
+
// Remove leading slashes from input
|
|
1160
|
+
if (partDir.startsWith('/')) {
|
|
1161
|
+
partDir = partDir.substring(1);
|
|
1162
|
+
}
|
|
1147
1163
|
if (target.startsWith('/')) {
|
|
1148
|
-
|
|
1164
|
+
target = target.substring(1);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Convert target to relative path
|
|
1168
|
+
if (target.startsWith(partDir)) {
|
|
1169
|
+
target = target.substring(partDir.length);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Remove leading slashes from output
|
|
1173
|
+
if (target.startsWith('/')) {
|
|
1174
|
+
target = target.substring(1);
|
|
1149
1175
|
}
|
|
1150
1176
|
return target;
|
|
1151
1177
|
}
|
|
@@ -1156,11 +1182,11 @@ class Relationship {
|
|
|
1156
1182
|
const node = xml.create.generalNode('Relationship');
|
|
1157
1183
|
node.attributes = {};
|
|
1158
1184
|
|
|
1159
|
-
//
|
|
1185
|
+
// Set only non-empty attributes
|
|
1160
1186
|
for (const propKey of Object.keys(this)) {
|
|
1161
1187
|
const value = this[propKey];
|
|
1162
1188
|
if (value && typeof value === 'string') {
|
|
1163
|
-
const attrName = propKey[0].toUpperCase() + propKey.
|
|
1189
|
+
const attrName = propKey[0].toUpperCase() + propKey.substring(1);
|
|
1164
1190
|
node.attributes[attrName] = value;
|
|
1165
1191
|
}
|
|
1166
1192
|
}
|
|
@@ -1365,20 +1391,20 @@ class RelsFile {
|
|
|
1365
1391
|
* Returns the rel ID.
|
|
1366
1392
|
*/
|
|
1367
1393
|
async add(relTarget, relType, relTargetMode) {
|
|
1368
|
-
//
|
|
1394
|
+
// If relTarget is an internal file it should be relative to the part dir
|
|
1369
1395
|
if (this.partDir && relTarget.startsWith(this.partDir)) {
|
|
1370
|
-
relTarget = relTarget.
|
|
1396
|
+
relTarget = relTarget.substring(this.partDir.length + 1);
|
|
1371
1397
|
}
|
|
1372
1398
|
|
|
1373
|
-
//
|
|
1399
|
+
// Parse rels file
|
|
1374
1400
|
await this.parseRelsFile();
|
|
1375
1401
|
|
|
1376
|
-
//
|
|
1402
|
+
// Already exists?
|
|
1377
1403
|
const relTargetKey = this.getRelTargetKey(relType, relTarget);
|
|
1378
1404
|
let relId = this.relTargets[relTargetKey];
|
|
1379
1405
|
if (relId) return relId;
|
|
1380
1406
|
|
|
1381
|
-
//
|
|
1407
|
+
// Create rel node
|
|
1382
1408
|
relId = this.getNextRelId();
|
|
1383
1409
|
const rel = new Relationship({
|
|
1384
1410
|
id: relId,
|
|
@@ -1387,11 +1413,11 @@ class RelsFile {
|
|
|
1387
1413
|
targetMode: relTargetMode
|
|
1388
1414
|
});
|
|
1389
1415
|
|
|
1390
|
-
//
|
|
1416
|
+
// Update lookups
|
|
1391
1417
|
this.rels[relId] = rel;
|
|
1392
1418
|
this.relTargets[relTargetKey] = relId;
|
|
1393
1419
|
|
|
1394
|
-
//
|
|
1420
|
+
// Return
|
|
1395
1421
|
return relId;
|
|
1396
1422
|
}
|
|
1397
1423
|
async list() {
|
|
@@ -1410,20 +1436,20 @@ class RelsFile {
|
|
|
1410
1436
|
* Called automatically by the holding `Docx` before exporting.
|
|
1411
1437
|
*/
|
|
1412
1438
|
async save() {
|
|
1413
|
-
//
|
|
1439
|
+
// Not change - no need to save
|
|
1414
1440
|
if (!this.rels) return;
|
|
1415
1441
|
|
|
1416
|
-
//
|
|
1442
|
+
// Create rels xml
|
|
1417
1443
|
const root = this.createRootNode();
|
|
1418
1444
|
root.childNodes = Object.values(this.rels).map(rel => rel.toXml());
|
|
1419
1445
|
|
|
1420
|
-
//
|
|
1446
|
+
// Serialize and save
|
|
1421
1447
|
const xmlContent = xml.parser.serializeFile(root);
|
|
1422
1448
|
this.zip.setFile(this.relsFilePath, xmlContent);
|
|
1423
1449
|
}
|
|
1424
1450
|
|
|
1425
1451
|
//
|
|
1426
|
-
//
|
|
1452
|
+
// Private methods
|
|
1427
1453
|
//
|
|
1428
1454
|
|
|
1429
1455
|
getNextRelId() {
|
|
@@ -1435,10 +1461,10 @@ class RelsFile {
|
|
|
1435
1461
|
return relId;
|
|
1436
1462
|
}
|
|
1437
1463
|
async parseRelsFile() {
|
|
1438
|
-
//
|
|
1464
|
+
// Already parsed
|
|
1439
1465
|
if (this.rels) return;
|
|
1440
1466
|
|
|
1441
|
-
//
|
|
1467
|
+
// Parse xml
|
|
1442
1468
|
let root;
|
|
1443
1469
|
const relsFile = this.zip.getFile(this.relsFilePath);
|
|
1444
1470
|
if (relsFile) {
|
|
@@ -1448,24 +1474,23 @@ class RelsFile {
|
|
|
1448
1474
|
root = this.createRootNode();
|
|
1449
1475
|
}
|
|
1450
1476
|
|
|
1451
|
-
//
|
|
1477
|
+
// Parse relationship nodes
|
|
1452
1478
|
this.rels = {};
|
|
1453
1479
|
this.relTargets = {};
|
|
1454
1480
|
for (const relNode of root.childNodes) {
|
|
1455
|
-
const
|
|
1481
|
+
const genRelNode = relNode;
|
|
1482
|
+
const attributes = genRelNode.attributes;
|
|
1456
1483
|
if (!attributes) continue;
|
|
1457
1484
|
const idAttr = attributes['Id'];
|
|
1458
1485
|
if (!idAttr) continue;
|
|
1459
1486
|
|
|
1460
|
-
//
|
|
1461
|
-
const rel = Relationship.fromXml(
|
|
1487
|
+
// Store rel
|
|
1488
|
+
const rel = Relationship.fromXml(this.partDir, genRelNode);
|
|
1462
1489
|
this.rels[idAttr] = rel;
|
|
1463
1490
|
|
|
1464
|
-
//
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
if (typeAttr && targetAttr) {
|
|
1468
|
-
const relTargetKey = this.getRelTargetKey(typeAttr, targetAttr);
|
|
1491
|
+
// Create rel target lookup
|
|
1492
|
+
if (rel.type && rel.target) {
|
|
1493
|
+
const relTargetKey = this.getRelTargetKey(rel.type, rel.target);
|
|
1469
1494
|
this.relTargets[relTargetKey] = idAttr;
|
|
1470
1495
|
}
|
|
1471
1496
|
}
|
|
@@ -1521,7 +1546,7 @@ class OpenXmlPart {
|
|
|
1521
1546
|
async getText() {
|
|
1522
1547
|
const xmlDocument = await this.xmlRoot();
|
|
1523
1548
|
|
|
1524
|
-
//
|
|
1549
|
+
// Ugly but good enough...
|
|
1525
1550
|
const xmlString = xml.parser.serializeFile(xmlDocument);
|
|
1526
1551
|
const domDocument = xml.parser.domParse(xmlString);
|
|
1527
1552
|
return domDocument.documentElement.textContent;
|
|
@@ -1836,7 +1861,7 @@ class Query {
|
|
|
1836
1861
|
*/
|
|
1837
1862
|
containingTextNode(node) {
|
|
1838
1863
|
if (!node) return null;
|
|
1839
|
-
if (!xml.query.isTextNode(node)) throw new Error(`'Invalid argument
|
|
1864
|
+
if (!xml.query.isTextNode(node)) throw new Error(`'Invalid argument node. Expected a XmlTextNode.`);
|
|
1840
1865
|
return xml.query.findParent(node, officeMarkup.query.isTextNode);
|
|
1841
1866
|
}
|
|
1842
1867
|
|
|
@@ -1953,7 +1978,7 @@ class Modify {
|
|
|
1953
1978
|
splitParagraphByTextNode(paragraph, textNode, removeTextNode) {
|
|
1954
1979
|
// input validation
|
|
1955
1980
|
const containingParagraph = officeMarkup.query.containingParagraphNode(textNode);
|
|
1956
|
-
if (containingParagraph != paragraph) throw new Error(`Node '
|
|
1981
|
+
if (containingParagraph != paragraph) throw new Error(`Node 'textNode' is not a contained in the specified paragraph.`);
|
|
1957
1982
|
const runNode = officeMarkup.query.containingRunNode(textNode);
|
|
1958
1983
|
const wordTextNode = officeMarkup.query.containingTextNode(textNode);
|
|
1959
1984
|
|
|
@@ -4654,7 +4679,7 @@ class Delimiters {
|
|
|
4654
4679
|
constructor(initial) {
|
|
4655
4680
|
Object.assign(this, initial);
|
|
4656
4681
|
this.encodeAndValidate();
|
|
4657
|
-
if (this.containerTagOpen === this.containerTagClose) throw new Error(
|
|
4682
|
+
if (this.containerTagOpen === this.containerTagClose) throw new Error(`containerTagOpen can not be equal to containerTagClose`);
|
|
4658
4683
|
}
|
|
4659
4684
|
encodeAndValidate() {
|
|
4660
4685
|
const keys = ['tagStart', 'tagEnd', 'containerTagOpen', 'containerTagClose'];
|
|
@@ -4697,12 +4722,12 @@ class TemplateHandler {
|
|
|
4697
4722
|
/**
|
|
4698
4723
|
* Version number of the `easy-template-x` library.
|
|
4699
4724
|
*/
|
|
4700
|
-
version = "6.2.
|
|
4725
|
+
version = "6.2.3" ;
|
|
4701
4726
|
constructor(options) {
|
|
4702
4727
|
this.options = new TemplateHandlerOptions(options);
|
|
4703
4728
|
|
|
4704
4729
|
//
|
|
4705
|
-
//
|
|
4730
|
+
// This is the library's composition root
|
|
4706
4731
|
//
|
|
4707
4732
|
|
|
4708
4733
|
const delimiterSearcher = new DelimiterSearcher();
|
|
@@ -4733,14 +4758,14 @@ class TemplateHandler {
|
|
|
4733
4758
|
}
|
|
4734
4759
|
|
|
4735
4760
|
//
|
|
4736
|
-
//
|
|
4761
|
+
// Public methods
|
|
4737
4762
|
//
|
|
4738
4763
|
|
|
4739
4764
|
async process(templateFile, data) {
|
|
4740
|
-
//
|
|
4765
|
+
// Load the docx file
|
|
4741
4766
|
const docx = await Docx.load(templateFile);
|
|
4742
4767
|
|
|
4743
|
-
//
|
|
4768
|
+
// Prepare context
|
|
4744
4769
|
const scopeData = new ScopeData(data);
|
|
4745
4770
|
scopeData.scopeDataResolver = this.options.scopeDataResolver;
|
|
4746
4771
|
const context = {
|
|
@@ -4755,18 +4780,18 @@ class TemplateHandler {
|
|
|
4755
4780
|
for (const part of contentParts) {
|
|
4756
4781
|
context.currentPart = part;
|
|
4757
4782
|
|
|
4758
|
-
//
|
|
4783
|
+
// Extensions - before compilation
|
|
4759
4784
|
await this.callExtensions(this.options.extensions?.beforeCompilation, scopeData, context);
|
|
4760
4785
|
|
|
4761
|
-
//
|
|
4786
|
+
// Compilation (do replacements)
|
|
4762
4787
|
const xmlRoot = await part.xmlRoot();
|
|
4763
4788
|
await this.compiler.compile(xmlRoot, scopeData, context);
|
|
4764
4789
|
|
|
4765
|
-
//
|
|
4790
|
+
// Extensions - after compilation
|
|
4766
4791
|
await this.callExtensions(this.options.extensions?.afterCompilation, scopeData, context);
|
|
4767
4792
|
}
|
|
4768
4793
|
|
|
4769
|
-
//
|
|
4794
|
+
// Export the result
|
|
4770
4795
|
return docx.export();
|
|
4771
4796
|
}
|
|
4772
4797
|
async parseTags(templateFile) {
|
|
@@ -4828,7 +4853,7 @@ class TemplateHandler {
|
|
|
4828
4853
|
}
|
|
4829
4854
|
|
|
4830
4855
|
//
|
|
4831
|
-
//
|
|
4856
|
+
// Private methods
|
|
4832
4857
|
//
|
|
4833
4858
|
|
|
4834
4859
|
async callExtensions(extensions, scopeData, context) {
|
|
@@ -200,8 +200,16 @@ function isNumber(value) {
|
|
|
200
200
|
class Path {
|
|
201
201
|
static getFilename(path) {
|
|
202
202
|
const lastSlashIndex = path.lastIndexOf('/');
|
|
203
|
-
return path.
|
|
203
|
+
return path.substring(lastSlashIndex + 1);
|
|
204
204
|
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get the directory of a path.
|
|
208
|
+
* Exclude the last slash.
|
|
209
|
+
*
|
|
210
|
+
* Example:
|
|
211
|
+
* /folder/subfolder/file.txt -> /folder/subfolder
|
|
212
|
+
*/
|
|
205
213
|
static getDirectory(path) {
|
|
206
214
|
const lastSlashIndex = path.lastIndexOf('/');
|
|
207
215
|
return path.substring(0, lastSlashIndex);
|
|
@@ -459,6 +467,9 @@ class Zip {
|
|
|
459
467
|
this.binaryFormat = binaryFormat;
|
|
460
468
|
}
|
|
461
469
|
getFile(path) {
|
|
470
|
+
if (path && path.startsWith('/')) {
|
|
471
|
+
path = path.substring(1);
|
|
472
|
+
}
|
|
462
473
|
const internalZipObject = this.zip.files[path];
|
|
463
474
|
if (!internalZipObject) return null;
|
|
464
475
|
return new ZipObject(internalZipObject, this.binaryFormat);
|
|
@@ -870,7 +881,7 @@ let Modify$1 = class Modify {
|
|
|
870
881
|
insertBefore(newNode, referenceNode) {
|
|
871
882
|
if (!newNode) throw new InternalArgumentMissingError("newNode");
|
|
872
883
|
if (!referenceNode) throw new InternalArgumentMissingError("referenceNode");
|
|
873
|
-
if (!referenceNode.parentNode) throw new Error(`'
|
|
884
|
+
if (!referenceNode.parentNode) throw new Error(`'referenceNode' has no parent`);
|
|
874
885
|
const childNodes = referenceNode.parentNode.childNodes;
|
|
875
886
|
const beforeNodeIndex = childNodes.indexOf(referenceNode);
|
|
876
887
|
xml.modify.insertChild(referenceNode.parentNode, newNode, beforeNodeIndex);
|
|
@@ -885,7 +896,7 @@ let Modify$1 = class Modify {
|
|
|
885
896
|
insertAfter(newNode, referenceNode) {
|
|
886
897
|
if (!newNode) throw new InternalArgumentMissingError("newNode");
|
|
887
898
|
if (!referenceNode) throw new InternalArgumentMissingError("referenceNode");
|
|
888
|
-
if (!referenceNode.parentNode) throw new Error(`'
|
|
899
|
+
if (!referenceNode.parentNode) throw new Error(`'referenceNode' has no parent`);
|
|
889
900
|
const childNodes = referenceNode.parentNode.childNodes;
|
|
890
901
|
const referenceNodeIndex = childNodes.indexOf(referenceNode);
|
|
891
902
|
xml.modify.insertChild(referenceNode.parentNode, newNode, referenceNodeIndex + 1);
|
|
@@ -1022,7 +1033,7 @@ let Modify$1 = class Modify {
|
|
|
1022
1033
|
* `false` then the original child node is the first child of `right`.
|
|
1023
1034
|
*/
|
|
1024
1035
|
splitByChild(parent, child, removeChild) {
|
|
1025
|
-
if (child.parentNode != parent) throw new Error(`Node '
|
|
1036
|
+
if (child.parentNode != parent) throw new Error(`Node 'child' is not a direct child of 'parent'.`);
|
|
1026
1037
|
|
|
1027
1038
|
// create childless clone 'left'
|
|
1028
1039
|
const left = xml.create.cloneNode(parent, false);
|
|
@@ -1130,20 +1141,35 @@ const RelType = Object.freeze({
|
|
|
1130
1141
|
Table: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table'
|
|
1131
1142
|
});
|
|
1132
1143
|
class Relationship {
|
|
1133
|
-
static fromXml(xml) {
|
|
1144
|
+
static fromXml(partDir, xml) {
|
|
1134
1145
|
return new Relationship({
|
|
1135
1146
|
id: xml.attributes?.['Id'],
|
|
1136
1147
|
type: xml.attributes?.['Type'],
|
|
1137
|
-
target: Relationship.normalizeRelTarget(xml.attributes?.['Target']),
|
|
1148
|
+
target: Relationship.normalizeRelTarget(partDir, xml.attributes?.['Target']),
|
|
1138
1149
|
targetMode: xml.attributes?.['TargetMode']
|
|
1139
1150
|
});
|
|
1140
1151
|
}
|
|
1141
|
-
static normalizeRelTarget(target) {
|
|
1152
|
+
static normalizeRelTarget(partDir, target) {
|
|
1142
1153
|
if (!target) {
|
|
1143
1154
|
return target;
|
|
1144
1155
|
}
|
|
1156
|
+
|
|
1157
|
+
// Remove leading slashes from input
|
|
1158
|
+
if (partDir.startsWith('/')) {
|
|
1159
|
+
partDir = partDir.substring(1);
|
|
1160
|
+
}
|
|
1145
1161
|
if (target.startsWith('/')) {
|
|
1146
|
-
|
|
1162
|
+
target = target.substring(1);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Convert target to relative path
|
|
1166
|
+
if (target.startsWith(partDir)) {
|
|
1167
|
+
target = target.substring(partDir.length);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Remove leading slashes from output
|
|
1171
|
+
if (target.startsWith('/')) {
|
|
1172
|
+
target = target.substring(1);
|
|
1147
1173
|
}
|
|
1148
1174
|
return target;
|
|
1149
1175
|
}
|
|
@@ -1154,11 +1180,11 @@ class Relationship {
|
|
|
1154
1180
|
const node = xml.create.generalNode('Relationship');
|
|
1155
1181
|
node.attributes = {};
|
|
1156
1182
|
|
|
1157
|
-
//
|
|
1183
|
+
// Set only non-empty attributes
|
|
1158
1184
|
for (const propKey of Object.keys(this)) {
|
|
1159
1185
|
const value = this[propKey];
|
|
1160
1186
|
if (value && typeof value === 'string') {
|
|
1161
|
-
const attrName = propKey[0].toUpperCase() + propKey.
|
|
1187
|
+
const attrName = propKey[0].toUpperCase() + propKey.substring(1);
|
|
1162
1188
|
node.attributes[attrName] = value;
|
|
1163
1189
|
}
|
|
1164
1190
|
}
|
|
@@ -1363,20 +1389,20 @@ class RelsFile {
|
|
|
1363
1389
|
* Returns the rel ID.
|
|
1364
1390
|
*/
|
|
1365
1391
|
async add(relTarget, relType, relTargetMode) {
|
|
1366
|
-
//
|
|
1392
|
+
// If relTarget is an internal file it should be relative to the part dir
|
|
1367
1393
|
if (this.partDir && relTarget.startsWith(this.partDir)) {
|
|
1368
|
-
relTarget = relTarget.
|
|
1394
|
+
relTarget = relTarget.substring(this.partDir.length + 1);
|
|
1369
1395
|
}
|
|
1370
1396
|
|
|
1371
|
-
//
|
|
1397
|
+
// Parse rels file
|
|
1372
1398
|
await this.parseRelsFile();
|
|
1373
1399
|
|
|
1374
|
-
//
|
|
1400
|
+
// Already exists?
|
|
1375
1401
|
const relTargetKey = this.getRelTargetKey(relType, relTarget);
|
|
1376
1402
|
let relId = this.relTargets[relTargetKey];
|
|
1377
1403
|
if (relId) return relId;
|
|
1378
1404
|
|
|
1379
|
-
//
|
|
1405
|
+
// Create rel node
|
|
1380
1406
|
relId = this.getNextRelId();
|
|
1381
1407
|
const rel = new Relationship({
|
|
1382
1408
|
id: relId,
|
|
@@ -1385,11 +1411,11 @@ class RelsFile {
|
|
|
1385
1411
|
targetMode: relTargetMode
|
|
1386
1412
|
});
|
|
1387
1413
|
|
|
1388
|
-
//
|
|
1414
|
+
// Update lookups
|
|
1389
1415
|
this.rels[relId] = rel;
|
|
1390
1416
|
this.relTargets[relTargetKey] = relId;
|
|
1391
1417
|
|
|
1392
|
-
//
|
|
1418
|
+
// Return
|
|
1393
1419
|
return relId;
|
|
1394
1420
|
}
|
|
1395
1421
|
async list() {
|
|
@@ -1408,20 +1434,20 @@ class RelsFile {
|
|
|
1408
1434
|
* Called automatically by the holding `Docx` before exporting.
|
|
1409
1435
|
*/
|
|
1410
1436
|
async save() {
|
|
1411
|
-
//
|
|
1437
|
+
// Not change - no need to save
|
|
1412
1438
|
if (!this.rels) return;
|
|
1413
1439
|
|
|
1414
|
-
//
|
|
1440
|
+
// Create rels xml
|
|
1415
1441
|
const root = this.createRootNode();
|
|
1416
1442
|
root.childNodes = Object.values(this.rels).map(rel => rel.toXml());
|
|
1417
1443
|
|
|
1418
|
-
//
|
|
1444
|
+
// Serialize and save
|
|
1419
1445
|
const xmlContent = xml.parser.serializeFile(root);
|
|
1420
1446
|
this.zip.setFile(this.relsFilePath, xmlContent);
|
|
1421
1447
|
}
|
|
1422
1448
|
|
|
1423
1449
|
//
|
|
1424
|
-
//
|
|
1450
|
+
// Private methods
|
|
1425
1451
|
//
|
|
1426
1452
|
|
|
1427
1453
|
getNextRelId() {
|
|
@@ -1433,10 +1459,10 @@ class RelsFile {
|
|
|
1433
1459
|
return relId;
|
|
1434
1460
|
}
|
|
1435
1461
|
async parseRelsFile() {
|
|
1436
|
-
//
|
|
1462
|
+
// Already parsed
|
|
1437
1463
|
if (this.rels) return;
|
|
1438
1464
|
|
|
1439
|
-
//
|
|
1465
|
+
// Parse xml
|
|
1440
1466
|
let root;
|
|
1441
1467
|
const relsFile = this.zip.getFile(this.relsFilePath);
|
|
1442
1468
|
if (relsFile) {
|
|
@@ -1446,24 +1472,23 @@ class RelsFile {
|
|
|
1446
1472
|
root = this.createRootNode();
|
|
1447
1473
|
}
|
|
1448
1474
|
|
|
1449
|
-
//
|
|
1475
|
+
// Parse relationship nodes
|
|
1450
1476
|
this.rels = {};
|
|
1451
1477
|
this.relTargets = {};
|
|
1452
1478
|
for (const relNode of root.childNodes) {
|
|
1453
|
-
const
|
|
1479
|
+
const genRelNode = relNode;
|
|
1480
|
+
const attributes = genRelNode.attributes;
|
|
1454
1481
|
if (!attributes) continue;
|
|
1455
1482
|
const idAttr = attributes['Id'];
|
|
1456
1483
|
if (!idAttr) continue;
|
|
1457
1484
|
|
|
1458
|
-
//
|
|
1459
|
-
const rel = Relationship.fromXml(
|
|
1485
|
+
// Store rel
|
|
1486
|
+
const rel = Relationship.fromXml(this.partDir, genRelNode);
|
|
1460
1487
|
this.rels[idAttr] = rel;
|
|
1461
1488
|
|
|
1462
|
-
//
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
if (typeAttr && targetAttr) {
|
|
1466
|
-
const relTargetKey = this.getRelTargetKey(typeAttr, targetAttr);
|
|
1489
|
+
// Create rel target lookup
|
|
1490
|
+
if (rel.type && rel.target) {
|
|
1491
|
+
const relTargetKey = this.getRelTargetKey(rel.type, rel.target);
|
|
1467
1492
|
this.relTargets[relTargetKey] = idAttr;
|
|
1468
1493
|
}
|
|
1469
1494
|
}
|
|
@@ -1519,7 +1544,7 @@ class OpenXmlPart {
|
|
|
1519
1544
|
async getText() {
|
|
1520
1545
|
const xmlDocument = await this.xmlRoot();
|
|
1521
1546
|
|
|
1522
|
-
//
|
|
1547
|
+
// Ugly but good enough...
|
|
1523
1548
|
const xmlString = xml.parser.serializeFile(xmlDocument);
|
|
1524
1549
|
const domDocument = xml.parser.domParse(xmlString);
|
|
1525
1550
|
return domDocument.documentElement.textContent;
|
|
@@ -1834,7 +1859,7 @@ class Query {
|
|
|
1834
1859
|
*/
|
|
1835
1860
|
containingTextNode(node) {
|
|
1836
1861
|
if (!node) return null;
|
|
1837
|
-
if (!xml.query.isTextNode(node)) throw new Error(`'Invalid argument
|
|
1862
|
+
if (!xml.query.isTextNode(node)) throw new Error(`'Invalid argument node. Expected a XmlTextNode.`);
|
|
1838
1863
|
return xml.query.findParent(node, officeMarkup.query.isTextNode);
|
|
1839
1864
|
}
|
|
1840
1865
|
|
|
@@ -1951,7 +1976,7 @@ class Modify {
|
|
|
1951
1976
|
splitParagraphByTextNode(paragraph, textNode, removeTextNode) {
|
|
1952
1977
|
// input validation
|
|
1953
1978
|
const containingParagraph = officeMarkup.query.containingParagraphNode(textNode);
|
|
1954
|
-
if (containingParagraph != paragraph) throw new Error(`Node '
|
|
1979
|
+
if (containingParagraph != paragraph) throw new Error(`Node 'textNode' is not a contained in the specified paragraph.`);
|
|
1955
1980
|
const runNode = officeMarkup.query.containingRunNode(textNode);
|
|
1956
1981
|
const wordTextNode = officeMarkup.query.containingTextNode(textNode);
|
|
1957
1982
|
|
|
@@ -4652,7 +4677,7 @@ class Delimiters {
|
|
|
4652
4677
|
constructor(initial) {
|
|
4653
4678
|
Object.assign(this, initial);
|
|
4654
4679
|
this.encodeAndValidate();
|
|
4655
|
-
if (this.containerTagOpen === this.containerTagClose) throw new Error(
|
|
4680
|
+
if (this.containerTagOpen === this.containerTagClose) throw new Error(`containerTagOpen can not be equal to containerTagClose`);
|
|
4656
4681
|
}
|
|
4657
4682
|
encodeAndValidate() {
|
|
4658
4683
|
const keys = ['tagStart', 'tagEnd', 'containerTagOpen', 'containerTagClose'];
|
|
@@ -4695,12 +4720,12 @@ class TemplateHandler {
|
|
|
4695
4720
|
/**
|
|
4696
4721
|
* Version number of the `easy-template-x` library.
|
|
4697
4722
|
*/
|
|
4698
|
-
version = "6.2.
|
|
4723
|
+
version = "6.2.3" ;
|
|
4699
4724
|
constructor(options) {
|
|
4700
4725
|
this.options = new TemplateHandlerOptions(options);
|
|
4701
4726
|
|
|
4702
4727
|
//
|
|
4703
|
-
//
|
|
4728
|
+
// This is the library's composition root
|
|
4704
4729
|
//
|
|
4705
4730
|
|
|
4706
4731
|
const delimiterSearcher = new DelimiterSearcher();
|
|
@@ -4731,14 +4756,14 @@ class TemplateHandler {
|
|
|
4731
4756
|
}
|
|
4732
4757
|
|
|
4733
4758
|
//
|
|
4734
|
-
//
|
|
4759
|
+
// Public methods
|
|
4735
4760
|
//
|
|
4736
4761
|
|
|
4737
4762
|
async process(templateFile, data) {
|
|
4738
|
-
//
|
|
4763
|
+
// Load the docx file
|
|
4739
4764
|
const docx = await Docx.load(templateFile);
|
|
4740
4765
|
|
|
4741
|
-
//
|
|
4766
|
+
// Prepare context
|
|
4742
4767
|
const scopeData = new ScopeData(data);
|
|
4743
4768
|
scopeData.scopeDataResolver = this.options.scopeDataResolver;
|
|
4744
4769
|
const context = {
|
|
@@ -4753,18 +4778,18 @@ class TemplateHandler {
|
|
|
4753
4778
|
for (const part of contentParts) {
|
|
4754
4779
|
context.currentPart = part;
|
|
4755
4780
|
|
|
4756
|
-
//
|
|
4781
|
+
// Extensions - before compilation
|
|
4757
4782
|
await this.callExtensions(this.options.extensions?.beforeCompilation, scopeData, context);
|
|
4758
4783
|
|
|
4759
|
-
//
|
|
4784
|
+
// Compilation (do replacements)
|
|
4760
4785
|
const xmlRoot = await part.xmlRoot();
|
|
4761
4786
|
await this.compiler.compile(xmlRoot, scopeData, context);
|
|
4762
4787
|
|
|
4763
|
-
//
|
|
4788
|
+
// Extensions - after compilation
|
|
4764
4789
|
await this.callExtensions(this.options.extensions?.afterCompilation, scopeData, context);
|
|
4765
4790
|
}
|
|
4766
4791
|
|
|
4767
|
-
//
|
|
4792
|
+
// Export the result
|
|
4768
4793
|
return docx.export();
|
|
4769
4794
|
}
|
|
4770
4795
|
async parseTags(templateFile) {
|
|
@@ -4826,7 +4851,7 @@ class TemplateHandler {
|
|
|
4826
4851
|
}
|
|
4827
4852
|
|
|
4828
4853
|
//
|
|
4829
|
-
//
|
|
4854
|
+
// Private methods
|
|
4830
4855
|
//
|
|
4831
4856
|
|
|
4832
4857
|
async callExtensions(extensions, scopeData, context) {
|
|
@@ -15,8 +15,8 @@ export declare const RelType: Readonly<{
|
|
|
15
15
|
}>;
|
|
16
16
|
export type RelTargetMode = 'Internal' | 'External';
|
|
17
17
|
export declare class Relationship {
|
|
18
|
-
static fromXml(xml: XmlGeneralNode): Relationship;
|
|
19
|
-
static normalizeRelTarget(target: string): string;
|
|
18
|
+
static fromXml(partDir: string, xml: XmlGeneralNode): Relationship;
|
|
19
|
+
static normalizeRelTarget(partDir: string, target: string): string;
|
|
20
20
|
id: string;
|
|
21
21
|
type: string;
|
|
22
22
|
target: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "easy-template-x",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.3",
|
|
4
4
|
"description": "Generate docx documents from templates, in Node or in the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"docx",
|
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
"clean": "rimraf .tmp dist test-reports && yarn clean-dist-verify",
|
|
43
43
|
"typecheck": "tsc --noEmit",
|
|
44
44
|
"lint": "eslint \"./{src,test}/**/!(*.d).ts\"",
|
|
45
|
-
"test": "
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test-ui": "vitest --ui",
|
|
47
|
+
"test-watch": "vitest",
|
|
46
48
|
"quality": "yarn typecheck && yarn lint && yarn test",
|
|
47
49
|
"build-src": "rollup -c",
|
|
48
50
|
"build-types": "tsc -p tsconfig.types.json --emitDeclarationOnly",
|
|
@@ -63,33 +65,28 @@
|
|
|
63
65
|
"lodash.get": "4.4.2"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
|
-
"@babel/core": "7.
|
|
67
|
-
"@babel/preset-env": "7.
|
|
68
|
-
"@babel/preset-typescript": "7.
|
|
69
|
-
"@eslint/js": "9.
|
|
68
|
+
"@babel/core": "7.28.4",
|
|
69
|
+
"@babel/preset-env": "7.28.3",
|
|
70
|
+
"@babel/preset-typescript": "7.27.1",
|
|
71
|
+
"@eslint/js": "9.35.0",
|
|
70
72
|
"@rollup/plugin-alias": "5.1.1",
|
|
71
73
|
"@rollup/plugin-replace": "6.0.2",
|
|
72
74
|
"@types/babel__preset-env": "7.10.0",
|
|
73
75
|
"@types/eslint__js": "8.42.3",
|
|
74
|
-
"@types/jest": "29.5.14",
|
|
75
76
|
"@types/jszip": "3.4.1",
|
|
76
|
-
"@types/node": "
|
|
77
|
-
"@
|
|
78
|
-
"babel-jest": "29.7.0",
|
|
77
|
+
"@types/node": "24.3.1",
|
|
78
|
+
"@vitest/ui": "2.1.8",
|
|
79
79
|
"babel-loader": "10.0.0",
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"globals": "16.0.0",
|
|
83
|
-
"jest": "29.7.0",
|
|
84
|
-
"jest-html-reporters": "3.1.7",
|
|
85
|
-
"jest-junit": "16.0.0",
|
|
80
|
+
"eslint": "9.35.0",
|
|
81
|
+
"globals": "16.3.0",
|
|
86
82
|
"lorem-ipsum": "2.0.8",
|
|
87
83
|
"rimraf": "6.0.1",
|
|
88
|
-
"rollup": "4.
|
|
84
|
+
"rollup": "4.50.1",
|
|
89
85
|
"rollup-plugin-auto-external": "2.0.0",
|
|
90
86
|
"rollup-plugin-babel": "4.4.0",
|
|
91
87
|
"rollup-plugin-node-resolve": "5.2.0",
|
|
92
|
-
"typescript": "5.
|
|
93
|
-
"typescript-eslint": "8.
|
|
88
|
+
"typescript": "5.9.2",
|
|
89
|
+
"typescript-eslint": "8.42.0",
|
|
90
|
+
"vitest": "2.1.8"
|
|
94
91
|
}
|
|
95
92
|
}
|
package/src/delimiters.ts
CHANGED
|
@@ -14,7 +14,7 @@ export class Delimiters {
|
|
|
14
14
|
this.encodeAndValidate();
|
|
15
15
|
|
|
16
16
|
if (this.containerTagOpen === this.containerTagClose)
|
|
17
|
-
throw new Error(
|
|
17
|
+
throw new Error(`containerTagOpen can not be equal to containerTagClose`);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
private encodeAndValidate() {
|
|
@@ -116,7 +116,7 @@ class Query {
|
|
|
116
116
|
return null;
|
|
117
117
|
|
|
118
118
|
if (!xml.query.isTextNode(node))
|
|
119
|
-
throw new Error(`'Invalid argument
|
|
119
|
+
throw new Error(`'Invalid argument node. Expected a XmlTextNode.`);
|
|
120
120
|
|
|
121
121
|
return xml.query.findParent(node, officeMarkup.query.isTextNode) as XmlGeneralNode;
|
|
122
122
|
}
|
|
@@ -262,7 +262,7 @@ class Modify {
|
|
|
262
262
|
// input validation
|
|
263
263
|
const containingParagraph = officeMarkup.query.containingParagraphNode(textNode);
|
|
264
264
|
if (containingParagraph != paragraph)
|
|
265
|
-
throw new Error(`Node '
|
|
265
|
+
throw new Error(`Node 'textNode' is not a contained in the specified paragraph.`);
|
|
266
266
|
|
|
267
267
|
const runNode = officeMarkup.query.containingRunNode(textNode);
|
|
268
268
|
const wordTextNode = officeMarkup.query.containingTextNode(textNode);
|
|
@@ -50,7 +50,7 @@ export class OpenXmlPart {
|
|
|
50
50
|
public async getText(): Promise<string> {
|
|
51
51
|
const xmlDocument = await this.xmlRoot();
|
|
52
52
|
|
|
53
|
-
//
|
|
53
|
+
// Ugly but good enough...
|
|
54
54
|
const xmlString = xml.parser.serializeFile(xmlDocument);
|
|
55
55
|
const domDocument = xml.parser.domParse(xmlString);
|
|
56
56
|
|
|
@@ -23,22 +23,38 @@ export type RelTargetMode = 'Internal' | 'External';
|
|
|
23
23
|
|
|
24
24
|
export class Relationship {
|
|
25
25
|
|
|
26
|
-
public static fromXml(xml: XmlGeneralNode): Relationship {
|
|
26
|
+
public static fromXml(partDir: string, xml: XmlGeneralNode): Relationship {
|
|
27
27
|
return new Relationship({
|
|
28
28
|
id: xml.attributes?.['Id'],
|
|
29
29
|
type: xml.attributes?.['Type'],
|
|
30
|
-
target: Relationship.normalizeRelTarget(xml.attributes?.['Target']),
|
|
30
|
+
target: Relationship.normalizeRelTarget(partDir, xml.attributes?.['Target']),
|
|
31
31
|
targetMode: xml.attributes?.['TargetMode'] as RelTargetMode,
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
public static normalizeRelTarget(target: string): string {
|
|
35
|
+
public static normalizeRelTarget(partDir: string, target: string): string {
|
|
36
36
|
if (!target) {
|
|
37
37
|
return target;
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
// Remove leading slashes from input
|
|
41
|
+
if (partDir.startsWith('/')) {
|
|
42
|
+
partDir = partDir.substring(1);
|
|
43
|
+
}
|
|
44
|
+
if (target.startsWith('/')) {
|
|
45
|
+
target = target.substring(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Convert target to relative path
|
|
49
|
+
if (target.startsWith(partDir)) {
|
|
50
|
+
target = target.substring(partDir.length);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Remove leading slashes from output
|
|
39
54
|
if (target.startsWith('/')) {
|
|
40
|
-
|
|
55
|
+
target = target.substring(1);
|
|
41
56
|
}
|
|
57
|
+
|
|
42
58
|
return target;
|
|
43
59
|
}
|
|
44
60
|
|
|
@@ -56,11 +72,11 @@ export class Relationship {
|
|
|
56
72
|
const node = xml.create.generalNode('Relationship');
|
|
57
73
|
node.attributes = {};
|
|
58
74
|
|
|
59
|
-
//
|
|
75
|
+
// Set only non-empty attributes
|
|
60
76
|
for (const propKey of Object.keys(this)) {
|
|
61
77
|
const value = (this as any)[propKey];
|
|
62
78
|
if (value && typeof value === 'string') {
|
|
63
|
-
const attrName = propKey[0].toUpperCase() + propKey.
|
|
79
|
+
const attrName = propKey[0].toUpperCase() + propKey.substring(1);
|
|
64
80
|
node.attributes[attrName] = value;
|
|
65
81
|
}
|
|
66
82
|
}
|
package/src/office/relsFile.ts
CHANGED
|
@@ -31,21 +31,21 @@ export class RelsFile {
|
|
|
31
31
|
*/
|
|
32
32
|
public async add(relTarget: string, relType: string, relTargetMode?: RelTargetMode): Promise<string> {
|
|
33
33
|
|
|
34
|
-
//
|
|
34
|
+
// If relTarget is an internal file it should be relative to the part dir
|
|
35
35
|
if (this.partDir && relTarget.startsWith(this.partDir)) {
|
|
36
|
-
relTarget = relTarget.
|
|
36
|
+
relTarget = relTarget.substring(this.partDir.length + 1);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// Parse rels file
|
|
40
40
|
await this.parseRelsFile();
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// Already exists?
|
|
43
43
|
const relTargetKey = this.getRelTargetKey(relType, relTarget);
|
|
44
44
|
let relId = this.relTargets[relTargetKey];
|
|
45
45
|
if (relId)
|
|
46
46
|
return relId;
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// Create rel node
|
|
49
49
|
relId = this.getNextRelId();
|
|
50
50
|
const rel = new Relationship({
|
|
51
51
|
id: relId,
|
|
@@ -54,11 +54,11 @@ export class RelsFile {
|
|
|
54
54
|
targetMode: relTargetMode
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
//
|
|
57
|
+
// Update lookups
|
|
58
58
|
this.rels[relId] = rel;
|
|
59
59
|
this.relTargets[relTargetKey] = relId;
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// Return
|
|
62
62
|
return relId;
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -80,21 +80,21 @@ export class RelsFile {
|
|
|
80
80
|
*/
|
|
81
81
|
public async save(): Promise<void> {
|
|
82
82
|
|
|
83
|
-
//
|
|
83
|
+
// Not change - no need to save
|
|
84
84
|
if (!this.rels)
|
|
85
85
|
return;
|
|
86
86
|
|
|
87
|
-
//
|
|
87
|
+
// Create rels xml
|
|
88
88
|
const root = this.createRootNode();
|
|
89
89
|
root.childNodes = Object.values(this.rels).map(rel => rel.toXml());
|
|
90
90
|
|
|
91
|
-
//
|
|
91
|
+
// Serialize and save
|
|
92
92
|
const xmlContent = xml.parser.serializeFile(root);
|
|
93
93
|
this.zip.setFile(this.relsFilePath, xmlContent);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
//
|
|
97
|
-
//
|
|
97
|
+
// Private methods
|
|
98
98
|
//
|
|
99
99
|
|
|
100
100
|
private getNextRelId(): string {
|
|
@@ -110,11 +110,11 @@ export class RelsFile {
|
|
|
110
110
|
|
|
111
111
|
private async parseRelsFile(): Promise<void> {
|
|
112
112
|
|
|
113
|
-
//
|
|
113
|
+
// Already parsed
|
|
114
114
|
if (this.rels)
|
|
115
115
|
return;
|
|
116
116
|
|
|
117
|
-
//
|
|
117
|
+
// Parse xml
|
|
118
118
|
let root: XmlNode;
|
|
119
119
|
const relsFile = this.zip.getFile(this.relsFilePath);
|
|
120
120
|
if (relsFile) {
|
|
@@ -124,12 +124,13 @@ export class RelsFile {
|
|
|
124
124
|
root = this.createRootNode();
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
//
|
|
127
|
+
// Parse relationship nodes
|
|
128
128
|
this.rels = {};
|
|
129
129
|
this.relTargets = {};
|
|
130
130
|
for (const relNode of root.childNodes) {
|
|
131
|
+
const genRelNode = relNode as XmlGeneralNode;
|
|
131
132
|
|
|
132
|
-
const attributes =
|
|
133
|
+
const attributes = genRelNode.attributes;
|
|
133
134
|
if (!attributes)
|
|
134
135
|
continue;
|
|
135
136
|
|
|
@@ -137,15 +138,13 @@ export class RelsFile {
|
|
|
137
138
|
if (!idAttr)
|
|
138
139
|
continue;
|
|
139
140
|
|
|
140
|
-
//
|
|
141
|
-
const rel = Relationship.fromXml(
|
|
141
|
+
// Store rel
|
|
142
|
+
const rel = Relationship.fromXml(this.partDir, genRelNode);
|
|
142
143
|
this.rels[idAttr] = rel;
|
|
143
144
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (typeAttr && targetAttr) {
|
|
148
|
-
const relTargetKey = this.getRelTargetKey(typeAttr, targetAttr);
|
|
145
|
+
// Create rel target lookup
|
|
146
|
+
if (rel.type && rel.target) {
|
|
147
|
+
const relTargetKey = this.getRelTargetKey(rel.type, rel.target);
|
|
149
148
|
this.relTargets[relTargetKey] = idAttr;
|
|
150
149
|
}
|
|
151
150
|
}
|
package/src/templateHandler.ts
CHANGED
|
@@ -22,7 +22,7 @@ export class TemplateHandler {
|
|
|
22
22
|
this.options = new TemplateHandlerOptions(options);
|
|
23
23
|
|
|
24
24
|
//
|
|
25
|
-
//
|
|
25
|
+
// This is the library's composition root
|
|
26
26
|
//
|
|
27
27
|
|
|
28
28
|
const delimiterSearcher = new DelimiterSearcher();
|
|
@@ -64,15 +64,15 @@ export class TemplateHandler {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
//
|
|
67
|
-
//
|
|
67
|
+
// Public methods
|
|
68
68
|
//
|
|
69
69
|
|
|
70
70
|
public async process<T extends Binary>(templateFile: T, data: TemplateData): Promise<T> {
|
|
71
71
|
|
|
72
|
-
//
|
|
72
|
+
// Load the docx file
|
|
73
73
|
const docx = await Docx.load(templateFile);
|
|
74
74
|
|
|
75
|
-
//
|
|
75
|
+
// Prepare context
|
|
76
76
|
const scopeData = new ScopeData(data);
|
|
77
77
|
scopeData.scopeDataResolver = this.options.scopeDataResolver;
|
|
78
78
|
const context: TemplateContext = {
|
|
@@ -89,18 +89,18 @@ export class TemplateHandler {
|
|
|
89
89
|
|
|
90
90
|
context.currentPart = part;
|
|
91
91
|
|
|
92
|
-
//
|
|
92
|
+
// Extensions - before compilation
|
|
93
93
|
await this.callExtensions(this.options.extensions?.beforeCompilation, scopeData, context);
|
|
94
94
|
|
|
95
|
-
//
|
|
95
|
+
// Compilation (do replacements)
|
|
96
96
|
const xmlRoot = await part.xmlRoot();
|
|
97
97
|
await this.compiler.compile(xmlRoot, scopeData, context);
|
|
98
98
|
|
|
99
|
-
//
|
|
99
|
+
// Extensions - after compilation
|
|
100
100
|
await this.callExtensions(this.options.extensions?.afterCompilation, scopeData, context);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
//
|
|
103
|
+
// Export the result
|
|
104
104
|
return docx.export();
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -171,7 +171,7 @@ export class TemplateHandler {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
//
|
|
174
|
-
//
|
|
174
|
+
// Private methods
|
|
175
175
|
//
|
|
176
176
|
|
|
177
177
|
private async callExtensions(extensions: TemplateExtension[], scopeData: ScopeData, context: TemplateContext): Promise<void> {
|
package/src/utils/path.ts
CHANGED
|
@@ -2,9 +2,16 @@ export class Path {
|
|
|
2
2
|
|
|
3
3
|
public static getFilename(path: string): string {
|
|
4
4
|
const lastSlashIndex = path.lastIndexOf('/');
|
|
5
|
-
return path.
|
|
5
|
+
return path.substring(lastSlashIndex + 1);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Get the directory of a path.
|
|
10
|
+
* Exclude the last slash.
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* /folder/subfolder/file.txt -> /folder/subfolder
|
|
14
|
+
*/
|
|
8
15
|
public static getDirectory(path: string): string {
|
|
9
16
|
const lastSlashIndex = path.lastIndexOf('/');
|
|
10
17
|
return path.substring(0, lastSlashIndex);
|
package/src/xml/xml.ts
CHANGED
|
@@ -41,7 +41,7 @@ class Parser {
|
|
|
41
41
|
|
|
42
42
|
public domParse(str: string): Document {
|
|
43
43
|
if (str === null || str === undefined)
|
|
44
|
-
throw new InternalArgumentMissingError(
|
|
44
|
+
throw new InternalArgumentMissingError("str");
|
|
45
45
|
|
|
46
46
|
return Parser.parser.parseFromString(str, "text/xml");
|
|
47
47
|
}
|
|
@@ -53,7 +53,7 @@ class Parser {
|
|
|
53
53
|
*/
|
|
54
54
|
public encodeValue(str: string): string {
|
|
55
55
|
if (str === null || str === undefined)
|
|
56
|
-
throw new InternalArgumentMissingError(
|
|
56
|
+
throw new InternalArgumentMissingError("str");
|
|
57
57
|
if (typeof str !== 'string')
|
|
58
58
|
throw new TypeError(`Expected a string, got '${(str as any).constructor.name}'.`);
|
|
59
59
|
|
|
@@ -165,7 +165,7 @@ class Create {
|
|
|
165
165
|
|
|
166
166
|
public cloneNode<T extends XmlNode>(node: T, deep: boolean): T {
|
|
167
167
|
if (!node)
|
|
168
|
-
throw new InternalArgumentMissingError(
|
|
168
|
+
throw new InternalArgumentMissingError("node");
|
|
169
169
|
|
|
170
170
|
if (!deep) {
|
|
171
171
|
const clone = Object.assign({}, node);
|
|
@@ -339,9 +339,9 @@ class Query {
|
|
|
339
339
|
*/
|
|
340
340
|
public siblingsInRange(firstNode: XmlNode, lastNode: XmlNode): XmlNode[] {
|
|
341
341
|
if (!firstNode)
|
|
342
|
-
throw new InternalArgumentMissingError(
|
|
342
|
+
throw new InternalArgumentMissingError("firstNode");
|
|
343
343
|
if (!lastNode)
|
|
344
|
-
throw new InternalArgumentMissingError(
|
|
344
|
+
throw new InternalArgumentMissingError("lastNode");
|
|
345
345
|
|
|
346
346
|
const range: XmlNode[] = [];
|
|
347
347
|
let curNode = firstNode;
|
|
@@ -380,12 +380,12 @@ class Modify {
|
|
|
380
380
|
*/
|
|
381
381
|
public insertBefore(newNode: XmlNode, referenceNode: XmlNode): void {
|
|
382
382
|
if (!newNode)
|
|
383
|
-
throw new InternalArgumentMissingError(
|
|
383
|
+
throw new InternalArgumentMissingError("newNode");
|
|
384
384
|
if (!referenceNode)
|
|
385
|
-
throw new InternalArgumentMissingError(
|
|
385
|
+
throw new InternalArgumentMissingError("referenceNode");
|
|
386
386
|
|
|
387
387
|
if (!referenceNode.parentNode)
|
|
388
|
-
throw new Error(`'
|
|
388
|
+
throw new Error(`'referenceNode' has no parent`);
|
|
389
389
|
|
|
390
390
|
const childNodes = referenceNode.parentNode.childNodes;
|
|
391
391
|
const beforeNodeIndex = childNodes.indexOf(referenceNode);
|
|
@@ -400,12 +400,12 @@ class Modify {
|
|
|
400
400
|
*/
|
|
401
401
|
public insertAfter(newNode: XmlNode, referenceNode: XmlNode): void {
|
|
402
402
|
if (!newNode)
|
|
403
|
-
throw new InternalArgumentMissingError(
|
|
403
|
+
throw new InternalArgumentMissingError("newNode");
|
|
404
404
|
if (!referenceNode)
|
|
405
|
-
throw new InternalArgumentMissingError(
|
|
405
|
+
throw new InternalArgumentMissingError("referenceNode");
|
|
406
406
|
|
|
407
407
|
if (!referenceNode.parentNode)
|
|
408
|
-
throw new Error(`'
|
|
408
|
+
throw new Error(`'referenceNode' has no parent`);
|
|
409
409
|
|
|
410
410
|
const childNodes = referenceNode.parentNode.childNodes;
|
|
411
411
|
const referenceNodeIndex = childNodes.indexOf(referenceNode);
|
|
@@ -414,11 +414,11 @@ class Modify {
|
|
|
414
414
|
|
|
415
415
|
public insertChild(parent: XmlNode, child: XmlNode, childIndex: number): void {
|
|
416
416
|
if (!parent)
|
|
417
|
-
throw new InternalArgumentMissingError(
|
|
417
|
+
throw new InternalArgumentMissingError("parent");
|
|
418
418
|
if (xml.query.isTextNode(parent))
|
|
419
419
|
throw new Error('Appending children to text nodes is forbidden');
|
|
420
420
|
if (!child)
|
|
421
|
-
throw new InternalArgumentMissingError(
|
|
421
|
+
throw new InternalArgumentMissingError("child");
|
|
422
422
|
|
|
423
423
|
if (!parent.childNodes)
|
|
424
424
|
parent.childNodes = [];
|
|
@@ -449,11 +449,11 @@ class Modify {
|
|
|
449
449
|
|
|
450
450
|
public appendChild(parent: XmlNode, child: XmlNode): void {
|
|
451
451
|
if (!parent)
|
|
452
|
-
throw new InternalArgumentMissingError(
|
|
452
|
+
throw new InternalArgumentMissingError("parent");
|
|
453
453
|
if (xml.query.isTextNode(parent))
|
|
454
454
|
throw new Error('Appending children to text nodes is forbidden');
|
|
455
455
|
if (!child)
|
|
456
|
-
throw new InternalArgumentMissingError(
|
|
456
|
+
throw new InternalArgumentMissingError("child");
|
|
457
457
|
|
|
458
458
|
if (!parent.childNodes)
|
|
459
459
|
parent.childNodes = [];
|
|
@@ -477,7 +477,7 @@ class Modify {
|
|
|
477
477
|
*/
|
|
478
478
|
public remove(node: XmlNode): void {
|
|
479
479
|
if (!node)
|
|
480
|
-
throw new InternalArgumentMissingError(
|
|
480
|
+
throw new InternalArgumentMissingError("node");
|
|
481
481
|
|
|
482
482
|
if (!node.parentNode)
|
|
483
483
|
throw new Error('Node has no parent');
|
|
@@ -497,9 +497,9 @@ class Modify {
|
|
|
497
497
|
public removeChild(parent: XmlNode, childIndex: number): XmlNode;
|
|
498
498
|
public removeChild(parent: XmlNode, childOrIndex: XmlNode | number): XmlNode {
|
|
499
499
|
if (!parent)
|
|
500
|
-
throw new InternalArgumentMissingError(
|
|
500
|
+
throw new InternalArgumentMissingError("parent");
|
|
501
501
|
if (childOrIndex === null || childOrIndex === undefined)
|
|
502
|
-
throw new InternalArgumentMissingError(
|
|
502
|
+
throw new InternalArgumentMissingError("childOrIndex");
|
|
503
503
|
|
|
504
504
|
if (!parent.childNodes || !parent.childNodes.length)
|
|
505
505
|
throw new Error('Parent node has node children');
|
|
@@ -579,7 +579,7 @@ class Modify {
|
|
|
579
579
|
public splitByChild(parent: XmlNode, child: XmlNode, removeChild: boolean): [XmlNode, XmlNode] {
|
|
580
580
|
|
|
581
581
|
if (child.parentNode != parent)
|
|
582
|
-
throw new Error(`Node '
|
|
582
|
+
throw new Error(`Node 'child' is not a direct child of 'parent'.`);
|
|
583
583
|
|
|
584
584
|
// create childless clone 'left'
|
|
585
585
|
const left = xml.create.cloneNode(parent, false);
|
package/src/zip/jsZipHelper.ts
CHANGED
|
@@ -10,7 +10,7 @@ export class JsZipHelper {
|
|
|
10
10
|
public static toJsZipOutputType(binaryOrType: Binary | Constructor<Binary>): JSZip.OutputType {
|
|
11
11
|
|
|
12
12
|
if (!binaryOrType)
|
|
13
|
-
throw new InternalArgumentMissingError(
|
|
13
|
+
throw new InternalArgumentMissingError("binaryOrType");
|
|
14
14
|
|
|
15
15
|
let binaryType: Constructor<Binary>;
|
|
16
16
|
if (typeof binaryOrType === 'function') {
|
package/src/zip/zip.ts
CHANGED