@lucianpacurar/iso20022.js 0.2.17 → 0.2.18
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.js +2318 -485
- package/dist/index.mjs +2318 -485
- package/dist/test/camt/052/cash-management-account-report.test.d.ts +1 -0
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -30,6 +30,24 @@ function isExist(v) {
|
|
|
30
30
|
return typeof v !== 'undefined';
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Dangerous property names that could lead to prototype pollution or security issues
|
|
35
|
+
*/
|
|
36
|
+
const DANGEROUS_PROPERTY_NAMES = [
|
|
37
|
+
// '__proto__',
|
|
38
|
+
// 'constructor',
|
|
39
|
+
// 'prototype',
|
|
40
|
+
'hasOwnProperty',
|
|
41
|
+
'toString',
|
|
42
|
+
'valueOf',
|
|
43
|
+
'__defineGetter__',
|
|
44
|
+
'__defineSetter__',
|
|
45
|
+
'__lookupGetter__',
|
|
46
|
+
'__lookupSetter__'
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const criticalProperties = ["__proto__", "constructor", "prototype"];
|
|
50
|
+
|
|
33
51
|
const defaultOptions$2 = {
|
|
34
52
|
allowBooleanAttributes: false, //A tag can have attributes without any value
|
|
35
53
|
unpairedTags: []
|
|
@@ -52,19 +70,19 @@ function validate(xmlData, options) {
|
|
|
52
70
|
// check for byte order mark (BOM)
|
|
53
71
|
xmlData = xmlData.substr(1);
|
|
54
72
|
}
|
|
55
|
-
|
|
73
|
+
|
|
56
74
|
for (let i = 0; i < xmlData.length; i++) {
|
|
57
75
|
|
|
58
|
-
if (xmlData[i] === '<' && xmlData[i+1] === '?') {
|
|
59
|
-
i+=2;
|
|
60
|
-
i = readPI(xmlData,i);
|
|
76
|
+
if (xmlData[i] === '<' && xmlData[i + 1] === '?') {
|
|
77
|
+
i += 2;
|
|
78
|
+
i = readPI(xmlData, i);
|
|
61
79
|
if (i.err) return i;
|
|
62
|
-
}else if (xmlData[i] === '<') {
|
|
80
|
+
} else if (xmlData[i] === '<') {
|
|
63
81
|
//starting of tag
|
|
64
82
|
//read until you reach to '>' avoiding any '>' in attribute value
|
|
65
83
|
let tagStartPos = i;
|
|
66
84
|
i++;
|
|
67
|
-
|
|
85
|
+
|
|
68
86
|
if (xmlData[i] === '!') {
|
|
69
87
|
i = readCommentAndCDATA(xmlData, i);
|
|
70
88
|
continue;
|
|
@@ -100,14 +118,14 @@ function validate(xmlData, options) {
|
|
|
100
118
|
if (tagName.trim().length === 0) {
|
|
101
119
|
msg = "Invalid space after '<'.";
|
|
102
120
|
} else {
|
|
103
|
-
msg = "Tag '"+tagName+"' is an invalid name.";
|
|
121
|
+
msg = "Tag '" + tagName + "' is an invalid name.";
|
|
104
122
|
}
|
|
105
123
|
return getErrorObject('InvalidTag', msg, getLineNumberForPosition(xmlData, i));
|
|
106
124
|
}
|
|
107
125
|
|
|
108
126
|
const result = readAttributeStr(xmlData, i);
|
|
109
127
|
if (result === false) {
|
|
110
|
-
return getErrorObject('InvalidAttr', "Attributes for '"+tagName+"' have open quote.", getLineNumberForPosition(xmlData, i));
|
|
128
|
+
return getErrorObject('InvalidAttr', "Attributes for '" + tagName + "' have open quote.", getLineNumberForPosition(xmlData, i));
|
|
111
129
|
}
|
|
112
130
|
let attrStr = result.value;
|
|
113
131
|
i = result.index;
|
|
@@ -128,17 +146,17 @@ function validate(xmlData, options) {
|
|
|
128
146
|
}
|
|
129
147
|
} else if (closingTag) {
|
|
130
148
|
if (!result.tagClosed) {
|
|
131
|
-
return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
|
|
149
|
+
return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
|
|
132
150
|
} else if (attrStr.trim().length > 0) {
|
|
133
|
-
return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
|
|
151
|
+
return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
|
|
134
152
|
} else if (tags.length === 0) {
|
|
135
|
-
return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos));
|
|
153
|
+
return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos));
|
|
136
154
|
} else {
|
|
137
155
|
const otg = tags.pop();
|
|
138
156
|
if (tagName !== otg.tagName) {
|
|
139
157
|
let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos);
|
|
140
158
|
return getErrorObject('InvalidTag',
|
|
141
|
-
"Expected closing tag '"+otg.tagName+"' (opened in line "+openPos.line+", col "+openPos.col+") instead of closing tag '"+tagName+"'.",
|
|
159
|
+
"Expected closing tag '" + otg.tagName + "' (opened in line " + openPos.line + ", col " + openPos.col + ") instead of closing tag '" + tagName + "'.",
|
|
142
160
|
getLineNumberForPosition(xmlData, tagStartPos));
|
|
143
161
|
}
|
|
144
162
|
|
|
@@ -159,8 +177,8 @@ function validate(xmlData, options) {
|
|
|
159
177
|
//if the root level has been reached before ...
|
|
160
178
|
if (reachedRoot === true) {
|
|
161
179
|
return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
|
|
162
|
-
} else if(options.unpairedTags.indexOf(tagName) !== -1); else {
|
|
163
|
-
tags.push({tagName, tagStartPos});
|
|
180
|
+
} else if (options.unpairedTags.indexOf(tagName) !== -1) ; else {
|
|
181
|
+
tags.push({ tagName, tagStartPos });
|
|
164
182
|
}
|
|
165
183
|
tagFound = true;
|
|
166
184
|
}
|
|
@@ -174,7 +192,7 @@ function validate(xmlData, options) {
|
|
|
174
192
|
i++;
|
|
175
193
|
i = readCommentAndCDATA(xmlData, i);
|
|
176
194
|
continue;
|
|
177
|
-
} else if (xmlData[i+1] === '?') {
|
|
195
|
+
} else if (xmlData[i + 1] === '?') {
|
|
178
196
|
i = readPI(xmlData, ++i);
|
|
179
197
|
if (i.err) return i;
|
|
180
198
|
} else {
|
|
@@ -185,7 +203,7 @@ function validate(xmlData, options) {
|
|
|
185
203
|
if (afterAmp == -1)
|
|
186
204
|
return getErrorObject('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
|
|
187
205
|
i = afterAmp;
|
|
188
|
-
}else {
|
|
206
|
+
} else {
|
|
189
207
|
if (reachedRoot === true && !isWhiteSpace(xmlData[i])) {
|
|
190
208
|
return getErrorObject('InvalidXml', "Extra text at the end", getLineNumberForPosition(xmlData, i));
|
|
191
209
|
}
|
|
@@ -196,27 +214,27 @@ function validate(xmlData, options) {
|
|
|
196
214
|
}
|
|
197
215
|
}
|
|
198
216
|
} else {
|
|
199
|
-
if (
|
|
217
|
+
if (isWhiteSpace(xmlData[i])) {
|
|
200
218
|
continue;
|
|
201
219
|
}
|
|
202
|
-
return getErrorObject('InvalidChar', "char '"+xmlData[i]+"' is not expected.", getLineNumberForPosition(xmlData, i));
|
|
220
|
+
return getErrorObject('InvalidChar', "char '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
|
|
203
221
|
}
|
|
204
222
|
}
|
|
205
223
|
|
|
206
224
|
if (!tagFound) {
|
|
207
225
|
return getErrorObject('InvalidXml', 'Start tag expected.', 1);
|
|
208
|
-
}else if (tags.length == 1) {
|
|
209
|
-
|
|
210
|
-
}else if (tags.length > 0) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
} else if (tags.length == 1) {
|
|
227
|
+
return getErrorObject('InvalidTag', "Unclosed tag '" + tags[0].tagName + "'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
|
|
228
|
+
} else if (tags.length > 0) {
|
|
229
|
+
return getErrorObject('InvalidXml', "Invalid '" +
|
|
230
|
+
JSON.stringify(tags.map(t => t.tagName), null, 4).replace(/\r?\n/g, '') +
|
|
231
|
+
"' found.", { line: 1, col: 1 });
|
|
214
232
|
}
|
|
215
233
|
|
|
216
234
|
return true;
|
|
217
235
|
}
|
|
218
|
-
function isWhiteSpace(char){
|
|
219
|
-
return char === ' ' || char === '\t' || char === '\n'
|
|
236
|
+
function isWhiteSpace(char) {
|
|
237
|
+
return char === ' ' || char === '\t' || char === '\n' || char === '\r';
|
|
220
238
|
}
|
|
221
239
|
/**
|
|
222
240
|
* Read Processing insstructions and skip
|
|
@@ -350,25 +368,25 @@ function validateAttributeString(attrStr, options) {
|
|
|
350
368
|
for (let i = 0; i < matches.length; i++) {
|
|
351
369
|
if (matches[i][1].length === 0) {
|
|
352
370
|
//nospace before attribute name: a="sd"b="saf"
|
|
353
|
-
return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' has no space in starting.", getPositionFromMatch(matches[i]))
|
|
371
|
+
return getErrorObject('InvalidAttr', "Attribute '" + matches[i][2] + "' has no space in starting.", getPositionFromMatch(matches[i]))
|
|
354
372
|
} else if (matches[i][3] !== undefined && matches[i][4] === undefined) {
|
|
355
|
-
return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' is without value.", getPositionFromMatch(matches[i]));
|
|
373
|
+
return getErrorObject('InvalidAttr', "Attribute '" + matches[i][2] + "' is without value.", getPositionFromMatch(matches[i]));
|
|
356
374
|
} else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
|
|
357
375
|
//independent attribute: ab
|
|
358
|
-
return getErrorObject('InvalidAttr', "boolean attribute '"+matches[i][2]+"' is not allowed.", getPositionFromMatch(matches[i]));
|
|
376
|
+
return getErrorObject('InvalidAttr', "boolean attribute '" + matches[i][2] + "' is not allowed.", getPositionFromMatch(matches[i]));
|
|
359
377
|
}
|
|
360
378
|
/* else if(matches[i][6] === undefined){//attribute without value: ab=
|
|
361
379
|
return { err: { code:"InvalidAttr",msg:"attribute " + matches[i][2] + " has no value assigned."}};
|
|
362
380
|
} */
|
|
363
381
|
const attrName = matches[i][2];
|
|
364
382
|
if (!validateAttrName(attrName)) {
|
|
365
|
-
return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(matches[i]));
|
|
383
|
+
return getErrorObject('InvalidAttr', "Attribute '" + attrName + "' is an invalid name.", getPositionFromMatch(matches[i]));
|
|
366
384
|
}
|
|
367
|
-
if (!
|
|
385
|
+
if (!Object.prototype.hasOwnProperty.call(attrNames, attrName)) {
|
|
368
386
|
//check for duplicate attribute.
|
|
369
387
|
attrNames[attrName] = 1;
|
|
370
388
|
} else {
|
|
371
|
-
return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(matches[i]));
|
|
389
|
+
return getErrorObject('InvalidAttr', "Attribute '" + attrName + "' is repeated.", getPositionFromMatch(matches[i]));
|
|
372
390
|
}
|
|
373
391
|
}
|
|
374
392
|
|
|
@@ -447,6 +465,14 @@ function getPositionFromMatch(match) {
|
|
|
447
465
|
return match.startIndex + match[1].length;
|
|
448
466
|
}
|
|
449
467
|
|
|
468
|
+
const defaultOnDangerousProperty = (name) => {
|
|
469
|
+
if (DANGEROUS_PROPERTY_NAMES.includes(name)) {
|
|
470
|
+
return "__" + name;
|
|
471
|
+
}
|
|
472
|
+
return name;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
|
|
450
476
|
const defaultOptions$1 = {
|
|
451
477
|
preserveOrder: false,
|
|
452
478
|
attributeNamePrefix: '@_',
|
|
@@ -487,8 +513,38 @@ const defaultOptions$1 = {
|
|
|
487
513
|
},
|
|
488
514
|
// skipEmptyListItem: false
|
|
489
515
|
captureMetaData: false,
|
|
516
|
+
maxNestedTags: 100,
|
|
517
|
+
strictReservedNames: true,
|
|
518
|
+
jPath: true, // if true, pass jPath string to callbacks; if false, pass matcher instance
|
|
519
|
+
onDangerousProperty: defaultOnDangerousProperty
|
|
490
520
|
};
|
|
491
521
|
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Validates that a property name is safe to use
|
|
525
|
+
* @param {string} propertyName - The property name to validate
|
|
526
|
+
* @param {string} optionName - The option field name (for error message)
|
|
527
|
+
* @throws {Error} If property name is dangerous
|
|
528
|
+
*/
|
|
529
|
+
function validatePropertyName(propertyName, optionName) {
|
|
530
|
+
if (typeof propertyName !== 'string') {
|
|
531
|
+
return; // Only validate string property names
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const normalized = propertyName.toLowerCase();
|
|
535
|
+
if (DANGEROUS_PROPERTY_NAMES.some(dangerous => normalized === dangerous.toLowerCase())) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (criticalProperties.some(dangerous => normalized === dangerous.toLowerCase())) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
492
548
|
/**
|
|
493
549
|
* Normalizes processEntities option for backward compatibility
|
|
494
550
|
* @param {boolean|object} value
|
|
@@ -503,6 +559,7 @@ function normalizeProcessEntities(value) {
|
|
|
503
559
|
maxExpansionDepth: 10,
|
|
504
560
|
maxTotalExpansions: 1000,
|
|
505
561
|
maxExpandedLength: 100000,
|
|
562
|
+
maxEntityCount: 100,
|
|
506
563
|
allowedTags: null,
|
|
507
564
|
tagFilter: null
|
|
508
565
|
};
|
|
@@ -511,11 +568,12 @@ function normalizeProcessEntities(value) {
|
|
|
511
568
|
// Object config - merge with defaults
|
|
512
569
|
if (typeof value === 'object' && value !== null) {
|
|
513
570
|
return {
|
|
514
|
-
enabled: value.enabled !== false,
|
|
515
|
-
maxEntitySize: value.maxEntitySize ?? 10000,
|
|
516
|
-
maxExpansionDepth: value.maxExpansionDepth ??
|
|
517
|
-
maxTotalExpansions: value.maxTotalExpansions ??
|
|
518
|
-
maxExpandedLength: value.maxExpandedLength ?? 100000,
|
|
571
|
+
enabled: value.enabled !== false,
|
|
572
|
+
maxEntitySize: Math.max(1, value.maxEntitySize ?? 10000),
|
|
573
|
+
maxExpansionDepth: Math.max(1, value.maxExpansionDepth ?? 10000),
|
|
574
|
+
maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? Infinity),
|
|
575
|
+
maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000),
|
|
576
|
+
maxEntityCount: Math.max(1, value.maxEntityCount ?? 1000),
|
|
519
577
|
allowedTags: value.allowedTags ?? null,
|
|
520
578
|
tagFilter: value.tagFilter ?? null
|
|
521
579
|
};
|
|
@@ -528,8 +586,39 @@ function normalizeProcessEntities(value) {
|
|
|
528
586
|
const buildOptions = function (options) {
|
|
529
587
|
const built = Object.assign({}, defaultOptions$1, options);
|
|
530
588
|
|
|
589
|
+
// Validate property names to prevent prototype pollution
|
|
590
|
+
const propertyNameOptions = [
|
|
591
|
+
{ value: built.attributeNamePrefix, name: 'attributeNamePrefix' },
|
|
592
|
+
{ value: built.attributesGroupName, name: 'attributesGroupName' },
|
|
593
|
+
{ value: built.textNodeName, name: 'textNodeName' },
|
|
594
|
+
{ value: built.cdataPropName, name: 'cdataPropName' },
|
|
595
|
+
{ value: built.commentPropName, name: 'commentPropName' }
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
for (const { value, name } of propertyNameOptions) {
|
|
599
|
+
if (value) {
|
|
600
|
+
validatePropertyName(value, name);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (built.onDangerousProperty === null) {
|
|
605
|
+
built.onDangerousProperty = defaultOnDangerousProperty;
|
|
606
|
+
}
|
|
607
|
+
|
|
531
608
|
// Always normalize processEntities for backward compatibility and validation
|
|
532
609
|
built.processEntities = normalizeProcessEntities(built.processEntities);
|
|
610
|
+
built.unpairedTagsSet = new Set(built.unpairedTags);
|
|
611
|
+
// Convert old-style stopNodes for backward compatibility
|
|
612
|
+
if (built.stopNodes && Array.isArray(built.stopNodes)) {
|
|
613
|
+
built.stopNodes = built.stopNodes.map(node => {
|
|
614
|
+
if (typeof node === 'string' && node.startsWith('*.')) {
|
|
615
|
+
// Old syntax: *.tagname meant "tagname anywhere"
|
|
616
|
+
// Convert to new syntax: ..tagname
|
|
617
|
+
return '..' + node.substring(2);
|
|
618
|
+
}
|
|
619
|
+
return node;
|
|
620
|
+
});
|
|
621
|
+
}
|
|
533
622
|
//console.debug(built.processEntities)
|
|
534
623
|
return built;
|
|
535
624
|
};
|
|
@@ -542,23 +631,23 @@ if (typeof Symbol !== "function") {
|
|
|
542
631
|
METADATA_SYMBOL$1 = Symbol("XML Node Metadata");
|
|
543
632
|
}
|
|
544
633
|
|
|
545
|
-
class XmlNode{
|
|
634
|
+
class XmlNode {
|
|
546
635
|
constructor(tagname) {
|
|
547
636
|
this.tagname = tagname;
|
|
548
637
|
this.child = []; //nested tags, text, cdata, comments in order
|
|
549
|
-
this[":@"] =
|
|
638
|
+
this[":@"] = Object.create(null); //attributes map
|
|
550
639
|
}
|
|
551
|
-
add(key,val){
|
|
640
|
+
add(key, val) {
|
|
552
641
|
// this.child.push( {name : key, val: val, isCdata: isCdata });
|
|
553
|
-
if(key === "__proto__") key = "#__proto__";
|
|
554
|
-
this.child.push(
|
|
642
|
+
if (key === "__proto__") key = "#__proto__";
|
|
643
|
+
this.child.push({ [key]: val });
|
|
555
644
|
}
|
|
556
645
|
addChild(node, startIndex) {
|
|
557
|
-
if(node.tagname === "__proto__") node.tagname = "#__proto__";
|
|
558
|
-
if(node[":@"] && Object.keys(node[":@"]).length > 0){
|
|
559
|
-
this.child.push(
|
|
560
|
-
}else {
|
|
561
|
-
this.child.push(
|
|
646
|
+
if (node.tagname === "__proto__") node.tagname = "#__proto__";
|
|
647
|
+
if (node[":@"] && Object.keys(node[":@"]).length > 0) {
|
|
648
|
+
this.child.push({ [node.tagname]: node.child, [":@"]: node[":@"] });
|
|
649
|
+
} else {
|
|
650
|
+
this.child.push({ [node.tagname]: node.child });
|
|
562
651
|
}
|
|
563
652
|
// if requested, add the startIndex
|
|
564
653
|
if (startIndex !== undefined) {
|
|
@@ -580,8 +669,9 @@ class DocTypeReader {
|
|
|
580
669
|
}
|
|
581
670
|
|
|
582
671
|
readDocType(xmlData, i) {
|
|
672
|
+
const entities = Object.create(null);
|
|
673
|
+
let entityCount = 0;
|
|
583
674
|
|
|
584
|
-
const entities = {};
|
|
585
675
|
if (xmlData[i + 3] === 'O' &&
|
|
586
676
|
xmlData[i + 4] === 'C' &&
|
|
587
677
|
xmlData[i + 5] === 'T' &&
|
|
@@ -599,11 +689,20 @@ class DocTypeReader {
|
|
|
599
689
|
let entityName, val;
|
|
600
690
|
[entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr);
|
|
601
691
|
if (val.indexOf("&") === -1) { //Parameter entities are not supported
|
|
602
|
-
|
|
692
|
+
if (this.options.enabled !== false &&
|
|
693
|
+
this.options.maxEntityCount != null &&
|
|
694
|
+
entityCount >= this.options.maxEntityCount) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})`
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
//const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
|
|
700
|
+
const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
603
701
|
entities[entityName] = {
|
|
604
702
|
regx: RegExp(`&${escaped};`, "g"),
|
|
605
703
|
val: val
|
|
606
704
|
};
|
|
705
|
+
entityCount++;
|
|
607
706
|
}
|
|
608
707
|
}
|
|
609
708
|
else if (hasBody && hasSeq(xmlData, "!ELEMENT", i)) {
|
|
@@ -663,11 +762,12 @@ class DocTypeReader {
|
|
|
663
762
|
i = skipWhitespace(xmlData, i);
|
|
664
763
|
|
|
665
764
|
// Read entity name
|
|
666
|
-
|
|
765
|
+
const startIndex = i;
|
|
667
766
|
while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
|
|
668
|
-
entityName += xmlData[i];
|
|
669
767
|
i++;
|
|
670
768
|
}
|
|
769
|
+
let entityName = xmlData.substring(startIndex, i);
|
|
770
|
+
|
|
671
771
|
validateEntityName(entityName);
|
|
672
772
|
|
|
673
773
|
// Skip whitespace after entity name
|
|
@@ -688,7 +788,7 @@ class DocTypeReader {
|
|
|
688
788
|
|
|
689
789
|
// Validate entity size
|
|
690
790
|
if (this.options.enabled !== false &&
|
|
691
|
-
this.options.maxEntitySize &&
|
|
791
|
+
this.options.maxEntitySize != null &&
|
|
692
792
|
entityValue.length > this.options.maxEntitySize) {
|
|
693
793
|
throw new Error(
|
|
694
794
|
`Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})`
|
|
@@ -704,11 +804,13 @@ class DocTypeReader {
|
|
|
704
804
|
i = skipWhitespace(xmlData, i);
|
|
705
805
|
|
|
706
806
|
// Read notation name
|
|
707
|
-
|
|
807
|
+
|
|
808
|
+
const startIndex = i;
|
|
708
809
|
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
|
|
709
|
-
notationName += xmlData[i];
|
|
710
810
|
i++;
|
|
711
811
|
}
|
|
812
|
+
let notationName = xmlData.substring(startIndex, i);
|
|
813
|
+
|
|
712
814
|
!this.suppressValidationErr && validateEntityName(notationName);
|
|
713
815
|
|
|
714
816
|
// Skip whitespace after notation name
|
|
@@ -758,10 +860,11 @@ class DocTypeReader {
|
|
|
758
860
|
}
|
|
759
861
|
i++;
|
|
760
862
|
|
|
863
|
+
const startIndex = i;
|
|
761
864
|
while (i < xmlData.length && xmlData[i] !== startChar) {
|
|
762
|
-
identifierVal += xmlData[i];
|
|
763
865
|
i++;
|
|
764
866
|
}
|
|
867
|
+
identifierVal = xmlData.substring(startIndex, i);
|
|
765
868
|
|
|
766
869
|
if (xmlData[i] !== startChar) {
|
|
767
870
|
throw new Error(`Unterminated ${type} value`);
|
|
@@ -781,11 +884,11 @@ class DocTypeReader {
|
|
|
781
884
|
i = skipWhitespace(xmlData, i);
|
|
782
885
|
|
|
783
886
|
// Read element name
|
|
784
|
-
|
|
887
|
+
const startIndex = i;
|
|
785
888
|
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
|
|
786
|
-
elementName += xmlData[i];
|
|
787
889
|
i++;
|
|
788
890
|
}
|
|
891
|
+
let elementName = xmlData.substring(startIndex, i);
|
|
789
892
|
|
|
790
893
|
// Validate element name
|
|
791
894
|
if (!this.suppressValidationErr && !isName(elementName)) {
|
|
@@ -802,10 +905,12 @@ class DocTypeReader {
|
|
|
802
905
|
i++; // Move past '('
|
|
803
906
|
|
|
804
907
|
// Read content model
|
|
908
|
+
const startIndex = i;
|
|
805
909
|
while (i < xmlData.length && xmlData[i] !== ")") {
|
|
806
|
-
contentModel += xmlData[i];
|
|
807
910
|
i++;
|
|
808
911
|
}
|
|
912
|
+
contentModel = xmlData.substring(startIndex, i);
|
|
913
|
+
|
|
809
914
|
if (xmlData[i] !== ")") {
|
|
810
915
|
throw new Error("Unterminated content model");
|
|
811
916
|
}
|
|
@@ -826,11 +931,11 @@ class DocTypeReader {
|
|
|
826
931
|
i = skipWhitespace(xmlData, i);
|
|
827
932
|
|
|
828
933
|
// Read element name
|
|
829
|
-
let
|
|
934
|
+
let startIndex = i;
|
|
830
935
|
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
|
|
831
|
-
elementName += xmlData[i];
|
|
832
936
|
i++;
|
|
833
937
|
}
|
|
938
|
+
let elementName = xmlData.substring(startIndex, i);
|
|
834
939
|
|
|
835
940
|
// Validate element name
|
|
836
941
|
validateEntityName(elementName);
|
|
@@ -839,11 +944,11 @@ class DocTypeReader {
|
|
|
839
944
|
i = skipWhitespace(xmlData, i);
|
|
840
945
|
|
|
841
946
|
// Read attribute name
|
|
842
|
-
|
|
947
|
+
startIndex = i;
|
|
843
948
|
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
|
|
844
|
-
attributeName += xmlData[i];
|
|
845
949
|
i++;
|
|
846
950
|
}
|
|
951
|
+
let attributeName = xmlData.substring(startIndex, i);
|
|
847
952
|
|
|
848
953
|
// Validate attribute name
|
|
849
954
|
if (!validateEntityName(attributeName)) {
|
|
@@ -871,11 +976,13 @@ class DocTypeReader {
|
|
|
871
976
|
// Read the list of allowed notations
|
|
872
977
|
let allowedNotations = [];
|
|
873
978
|
while (i < xmlData.length && xmlData[i] !== ")") {
|
|
874
|
-
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
const startIndex = i;
|
|
875
982
|
while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") {
|
|
876
|
-
notation += xmlData[i];
|
|
877
983
|
i++;
|
|
878
984
|
}
|
|
985
|
+
let notation = xmlData.substring(startIndex, i);
|
|
879
986
|
|
|
880
987
|
// Validate notation name
|
|
881
988
|
notation = notation.trim();
|
|
@@ -901,10 +1008,11 @@ class DocTypeReader {
|
|
|
901
1008
|
attributeType += " (" + allowedNotations.join("|") + ")";
|
|
902
1009
|
} else {
|
|
903
1010
|
// Handle simple types (e.g., CDATA, ID, IDREF, etc.)
|
|
1011
|
+
const startIndex = i;
|
|
904
1012
|
while (i < xmlData.length && !/\s/.test(xmlData[i])) {
|
|
905
|
-
attributeType += xmlData[i];
|
|
906
1013
|
i++;
|
|
907
1014
|
}
|
|
1015
|
+
attributeType += xmlData.substring(startIndex, i);
|
|
908
1016
|
|
|
909
1017
|
// Validate simple attribute type
|
|
910
1018
|
const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"];
|
|
@@ -968,48 +1076,52 @@ const numRegex = /^([\-\+])?(0*)([0-9]*(\.[0-9]*)?)$/;
|
|
|
968
1076
|
// const octRegex = /^0x[a-z0-9]+/;
|
|
969
1077
|
// const binRegex = /0x[a-z0-9]+/;
|
|
970
1078
|
|
|
971
|
-
|
|
1079
|
+
|
|
972
1080
|
const consider = {
|
|
973
|
-
hex
|
|
1081
|
+
hex: true,
|
|
974
1082
|
// oct: false,
|
|
975
1083
|
leadingZeros: true,
|
|
976
1084
|
decimalPoint: "\.",
|
|
977
1085
|
eNotation: true,
|
|
978
|
-
//skipLike: /regex
|
|
1086
|
+
//skipLike: /regex/,
|
|
1087
|
+
infinity: "original", // "null", "infinity" (Infinity type), "string" ("Infinity" (the string literal))
|
|
979
1088
|
};
|
|
980
1089
|
|
|
981
|
-
function toNumber(str, options = {}){
|
|
982
|
-
options = Object.assign({}, consider, options
|
|
983
|
-
if(!str || typeof str !== "string"
|
|
984
|
-
|
|
985
|
-
let trimmedStr
|
|
986
|
-
|
|
987
|
-
if(
|
|
988
|
-
else if(
|
|
1090
|
+
function toNumber(str, options = {}) {
|
|
1091
|
+
options = Object.assign({}, consider, options);
|
|
1092
|
+
if (!str || typeof str !== "string") return str;
|
|
1093
|
+
|
|
1094
|
+
let trimmedStr = str.trim();
|
|
1095
|
+
|
|
1096
|
+
if (trimmedStr.length === 0) return str;
|
|
1097
|
+
else if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str;
|
|
1098
|
+
else if (trimmedStr === "0") return 0;
|
|
989
1099
|
else if (options.hex && hexRegex.test(trimmedStr)) {
|
|
990
1100
|
return parse_int(trimmedStr, 16);
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
}else if (
|
|
994
|
-
return
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1101
|
+
// }else if (options.oct && octRegex.test(str)) {
|
|
1102
|
+
// return Number.parseInt(val, 8);
|
|
1103
|
+
} else if (!isFinite(trimmedStr)) { //Infinity
|
|
1104
|
+
return handleInfinity(str, Number(trimmedStr), options);
|
|
1105
|
+
} else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation
|
|
1106
|
+
return resolveEnotation(str, trimmedStr, options);
|
|
1107
|
+
// }else if (options.parseBin && binRegex.test(str)) {
|
|
1108
|
+
// return Number.parseInt(val, 2);
|
|
1109
|
+
} else {
|
|
998
1110
|
//separate negative sign, leading zeros, and rest number
|
|
999
1111
|
const match = numRegex.exec(trimmedStr);
|
|
1000
1112
|
// +00.123 => [ , '+', '00', '.123', ..
|
|
1001
|
-
if(match){
|
|
1113
|
+
if (match) {
|
|
1002
1114
|
const sign = match[1] || "";
|
|
1003
1115
|
const leadingZeros = match[2];
|
|
1004
1116
|
let numTrimmedByZeros = trimZeros(match[3]); //complete num without leading zeros
|
|
1005
1117
|
const decimalAdjacentToLeadingZeros = sign ? // 0., -00., 000.
|
|
1006
|
-
str[leadingZeros.length+1] === "."
|
|
1118
|
+
str[leadingZeros.length + 1] === "."
|
|
1007
1119
|
: str[leadingZeros.length] === ".";
|
|
1008
1120
|
|
|
1009
1121
|
//trim ending zeros for floating number
|
|
1010
|
-
if(!options.leadingZeros //leading zeros are not allowed
|
|
1011
|
-
&& (leadingZeros.length > 1
|
|
1012
|
-
|| (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))){
|
|
1122
|
+
if (!options.leadingZeros //leading zeros are not allowed
|
|
1123
|
+
&& (leadingZeros.length > 1
|
|
1124
|
+
|| (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))) {
|
|
1013
1125
|
// 00, 00.3, +03.24, 03, 03.24
|
|
1014
1126
|
return str;
|
|
1015
1127
|
}
|
|
@@ -1017,54 +1129,59 @@ function toNumber(str, options = {}){
|
|
|
1017
1129
|
const num = Number(trimmedStr);
|
|
1018
1130
|
const parsedStr = String(num);
|
|
1019
1131
|
|
|
1020
|
-
if(
|
|
1021
|
-
if(parsedStr.search(/[eE]/) !== -1){ //given number is long and parsed to eNotation
|
|
1022
|
-
if(options.eNotation) return num;
|
|
1132
|
+
if (num === 0) return num;
|
|
1133
|
+
if (parsedStr.search(/[eE]/) !== -1) { //given number is long and parsed to eNotation
|
|
1134
|
+
if (options.eNotation) return num;
|
|
1023
1135
|
else return str;
|
|
1024
|
-
}else if(trimmedStr.indexOf(".") !== -1){ //floating number
|
|
1025
|
-
if(parsedStr === "0") return num; //0.0
|
|
1026
|
-
else if(parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000
|
|
1027
|
-
else if(
|
|
1136
|
+
} else if (trimmedStr.indexOf(".") !== -1) { //floating number
|
|
1137
|
+
if (parsedStr === "0") return num; //0.0
|
|
1138
|
+
else if (parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000
|
|
1139
|
+
else if (parsedStr === `${sign}${numTrimmedByZeros}`) return num;
|
|
1028
1140
|
else return str;
|
|
1029
1141
|
}
|
|
1030
|
-
|
|
1031
|
-
let n = leadingZeros? numTrimmedByZeros : trimmedStr;
|
|
1032
|
-
if(leadingZeros){
|
|
1142
|
+
|
|
1143
|
+
let n = leadingZeros ? numTrimmedByZeros : trimmedStr;
|
|
1144
|
+
if (leadingZeros) {
|
|
1033
1145
|
// -009 => -9
|
|
1034
|
-
return (n === parsedStr) || (sign+n === parsedStr) ? num : str
|
|
1035
|
-
}else
|
|
1146
|
+
return (n === parsedStr) || (sign + n === parsedStr) ? num : str
|
|
1147
|
+
} else {
|
|
1036
1148
|
// +9
|
|
1037
|
-
return (n === parsedStr) || (n === sign+parsedStr) ? num : str
|
|
1149
|
+
return (n === parsedStr) || (n === sign + parsedStr) ? num : str
|
|
1038
1150
|
}
|
|
1039
1151
|
}
|
|
1040
|
-
}else { //non-numeric string
|
|
1152
|
+
} else { //non-numeric string
|
|
1041
1153
|
return str;
|
|
1042
1154
|
}
|
|
1043
1155
|
}
|
|
1044
1156
|
}
|
|
1045
1157
|
|
|
1046
1158
|
const eNotationRegx = /^([-+])?(0*)(\d*(\.\d*)?[eE][-\+]?\d+)$/;
|
|
1047
|
-
function resolveEnotation(str,trimmedStr,options){
|
|
1048
|
-
if(!options.eNotation) return str;
|
|
1049
|
-
const notation = trimmedStr.match(eNotationRegx);
|
|
1050
|
-
if(notation){
|
|
1159
|
+
function resolveEnotation(str, trimmedStr, options) {
|
|
1160
|
+
if (!options.eNotation) return str;
|
|
1161
|
+
const notation = trimmedStr.match(eNotationRegx);
|
|
1162
|
+
if (notation) {
|
|
1051
1163
|
let sign = notation[1] || "";
|
|
1052
1164
|
const eChar = notation[3].indexOf("e") === -1 ? "E" : "e";
|
|
1053
1165
|
const leadingZeros = notation[2];
|
|
1054
1166
|
const eAdjacentToLeadingZeros = sign ? // 0E.
|
|
1055
|
-
str[leadingZeros.length+1] === eChar
|
|
1167
|
+
str[leadingZeros.length + 1] === eChar
|
|
1056
1168
|
: str[leadingZeros.length] === eChar;
|
|
1057
1169
|
|
|
1058
|
-
if(leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str;
|
|
1059
|
-
else if(leadingZeros.length === 1
|
|
1060
|
-
&& (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)){
|
|
1170
|
+
if (leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str;
|
|
1171
|
+
else if (leadingZeros.length === 1
|
|
1172
|
+
&& (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)) {
|
|
1173
|
+
return Number(trimmedStr);
|
|
1174
|
+
} else if (leadingZeros.length > 0) {
|
|
1175
|
+
// Has leading zeros — only accept if leadingZeros option allows it
|
|
1176
|
+
if (options.leadingZeros && !eAdjacentToLeadingZeros) {
|
|
1177
|
+
trimmedStr = (notation[1] || "") + notation[3];
|
|
1061
1178
|
return Number(trimmedStr);
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1179
|
+
} else return str;
|
|
1180
|
+
} else {
|
|
1181
|
+
// No leading zeros — always valid e-notation, parse it
|
|
1065
1182
|
return Number(trimmedStr);
|
|
1066
|
-
}
|
|
1067
|
-
}else {
|
|
1183
|
+
}
|
|
1184
|
+
} else {
|
|
1068
1185
|
return str;
|
|
1069
1186
|
}
|
|
1070
1187
|
}
|
|
@@ -1074,26 +1191,49 @@ function resolveEnotation(str,trimmedStr,options){
|
|
|
1074
1191
|
* @param {string} numStr without leading zeros
|
|
1075
1192
|
* @returns
|
|
1076
1193
|
*/
|
|
1077
|
-
function trimZeros(numStr){
|
|
1078
|
-
if(numStr && numStr.indexOf(".") !== -1){//float
|
|
1194
|
+
function trimZeros(numStr) {
|
|
1195
|
+
if (numStr && numStr.indexOf(".") !== -1) {//float
|
|
1079
1196
|
numStr = numStr.replace(/0+$/, ""); //remove ending zeros
|
|
1080
|
-
if(numStr === ".")
|
|
1081
|
-
else if(numStr[0] === ".")
|
|
1082
|
-
else if(numStr[numStr.length-1] === ".")
|
|
1197
|
+
if (numStr === ".") numStr = "0";
|
|
1198
|
+
else if (numStr[0] === ".") numStr = "0" + numStr;
|
|
1199
|
+
else if (numStr[numStr.length - 1] === ".") numStr = numStr.substring(0, numStr.length - 1);
|
|
1083
1200
|
return numStr;
|
|
1084
1201
|
}
|
|
1085
1202
|
return numStr;
|
|
1086
1203
|
}
|
|
1087
1204
|
|
|
1088
|
-
function parse_int(numStr, base){
|
|
1205
|
+
function parse_int(numStr, base) {
|
|
1089
1206
|
//polyfill
|
|
1090
|
-
if(parseInt) return parseInt(numStr, base);
|
|
1091
|
-
else if(Number.parseInt) return Number.parseInt(numStr, base);
|
|
1092
|
-
else if(window && window.parseInt) return window.parseInt(numStr, base);
|
|
1207
|
+
if (parseInt) return parseInt(numStr, base);
|
|
1208
|
+
else if (Number.parseInt) return Number.parseInt(numStr, base);
|
|
1209
|
+
else if (window && window.parseInt) return window.parseInt(numStr, base);
|
|
1093
1210
|
else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported")
|
|
1094
1211
|
}
|
|
1095
1212
|
|
|
1096
|
-
|
|
1213
|
+
/**
|
|
1214
|
+
* Handle infinite values based on user option
|
|
1215
|
+
* @param {string} str - original input string
|
|
1216
|
+
* @param {number} num - parsed number (Infinity or -Infinity)
|
|
1217
|
+
* @param {object} options - user options
|
|
1218
|
+
* @returns {string|number|null} based on infinity option
|
|
1219
|
+
*/
|
|
1220
|
+
function handleInfinity(str, num, options) {
|
|
1221
|
+
const isPositive = num === Infinity;
|
|
1222
|
+
|
|
1223
|
+
switch (options.infinity.toLowerCase()) {
|
|
1224
|
+
case "null":
|
|
1225
|
+
return null;
|
|
1226
|
+
case "infinity":
|
|
1227
|
+
return num; // Return Infinity or -Infinity
|
|
1228
|
+
case "string":
|
|
1229
|
+
return isPositive ? "Infinity" : "-Infinity";
|
|
1230
|
+
case "original":
|
|
1231
|
+
default:
|
|
1232
|
+
return str; // Return original string like "1e1000"
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function getIgnoreAttributesFn$1(ignoreAttributes) {
|
|
1097
1237
|
if (typeof ignoreAttributes === 'function') {
|
|
1098
1238
|
return ignoreAttributes
|
|
1099
1239
|
}
|
|
@@ -1112,6 +1252,1018 @@ function getIgnoreAttributesFn(ignoreAttributes) {
|
|
|
1112
1252
|
return () => false
|
|
1113
1253
|
}
|
|
1114
1254
|
|
|
1255
|
+
/**
|
|
1256
|
+
* Expression - Parses and stores a tag pattern expression
|
|
1257
|
+
*
|
|
1258
|
+
* Patterns are parsed once and stored in an optimized structure for fast matching.
|
|
1259
|
+
*
|
|
1260
|
+
* @example
|
|
1261
|
+
* const expr = new Expression("root.users.user");
|
|
1262
|
+
* const expr2 = new Expression("..user[id]:first");
|
|
1263
|
+
* const expr3 = new Expression("root/users/user", { separator: '/' });
|
|
1264
|
+
*/
|
|
1265
|
+
class Expression {
|
|
1266
|
+
/**
|
|
1267
|
+
* Create a new Expression
|
|
1268
|
+
* @param {string} pattern - Pattern string (e.g., "root.users.user", "..user[id]")
|
|
1269
|
+
* @param {Object} options - Configuration options
|
|
1270
|
+
* @param {string} options.separator - Path separator (default: '.')
|
|
1271
|
+
*/
|
|
1272
|
+
constructor(pattern, options = {}, data) {
|
|
1273
|
+
this.pattern = pattern;
|
|
1274
|
+
this.separator = options.separator || '.';
|
|
1275
|
+
this.segments = this._parse(pattern);
|
|
1276
|
+
this.data = data;
|
|
1277
|
+
// Cache expensive checks for performance (O(1) instead of O(n))
|
|
1278
|
+
this._hasDeepWildcard = this.segments.some(seg => seg.type === 'deep-wildcard');
|
|
1279
|
+
this._hasAttributeCondition = this.segments.some(seg => seg.attrName !== undefined);
|
|
1280
|
+
this._hasPositionSelector = this.segments.some(seg => seg.position !== undefined);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Parse pattern string into segments
|
|
1285
|
+
* @private
|
|
1286
|
+
* @param {string} pattern - Pattern to parse
|
|
1287
|
+
* @returns {Array} Array of segment objects
|
|
1288
|
+
*/
|
|
1289
|
+
_parse(pattern) {
|
|
1290
|
+
const segments = [];
|
|
1291
|
+
|
|
1292
|
+
// Split by separator but handle ".." specially
|
|
1293
|
+
let i = 0;
|
|
1294
|
+
let currentPart = '';
|
|
1295
|
+
|
|
1296
|
+
while (i < pattern.length) {
|
|
1297
|
+
if (pattern[i] === this.separator) {
|
|
1298
|
+
// Check if next char is also separator (deep wildcard)
|
|
1299
|
+
if (i + 1 < pattern.length && pattern[i + 1] === this.separator) {
|
|
1300
|
+
// Flush current part if any
|
|
1301
|
+
if (currentPart.trim()) {
|
|
1302
|
+
segments.push(this._parseSegment(currentPart.trim()));
|
|
1303
|
+
currentPart = '';
|
|
1304
|
+
}
|
|
1305
|
+
// Add deep wildcard
|
|
1306
|
+
segments.push({ type: 'deep-wildcard' });
|
|
1307
|
+
i += 2; // Skip both separators
|
|
1308
|
+
} else {
|
|
1309
|
+
// Regular separator
|
|
1310
|
+
if (currentPart.trim()) {
|
|
1311
|
+
segments.push(this._parseSegment(currentPart.trim()));
|
|
1312
|
+
}
|
|
1313
|
+
currentPart = '';
|
|
1314
|
+
i++;
|
|
1315
|
+
}
|
|
1316
|
+
} else {
|
|
1317
|
+
currentPart += pattern[i];
|
|
1318
|
+
i++;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Flush remaining part
|
|
1323
|
+
if (currentPart.trim()) {
|
|
1324
|
+
segments.push(this._parseSegment(currentPart.trim()));
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return segments;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Parse a single segment
|
|
1332
|
+
* @private
|
|
1333
|
+
* @param {string} part - Segment string (e.g., "user", "ns::user", "user[id]", "ns::user:first")
|
|
1334
|
+
* @returns {Object} Segment object
|
|
1335
|
+
*/
|
|
1336
|
+
_parseSegment(part) {
|
|
1337
|
+
const segment = { type: 'tag' };
|
|
1338
|
+
|
|
1339
|
+
// NEW NAMESPACE SYNTAX (v2.0):
|
|
1340
|
+
// ============================
|
|
1341
|
+
// Namespace uses DOUBLE colon (::)
|
|
1342
|
+
// Position uses SINGLE colon (:)
|
|
1343
|
+
//
|
|
1344
|
+
// Examples:
|
|
1345
|
+
// "user" → tag
|
|
1346
|
+
// "user:first" → tag + position
|
|
1347
|
+
// "user[id]" → tag + attribute
|
|
1348
|
+
// "user[id]:first" → tag + attribute + position
|
|
1349
|
+
// "ns::user" → namespace + tag
|
|
1350
|
+
// "ns::user:first" → namespace + tag + position
|
|
1351
|
+
// "ns::user[id]" → namespace + tag + attribute
|
|
1352
|
+
// "ns::user[id]:first" → namespace + tag + attribute + position
|
|
1353
|
+
// "ns::first" → namespace + tag named "first" (NO ambiguity!)
|
|
1354
|
+
//
|
|
1355
|
+
// This eliminates all ambiguity:
|
|
1356
|
+
// :: = namespace separator
|
|
1357
|
+
// : = position selector
|
|
1358
|
+
// [] = attributes
|
|
1359
|
+
|
|
1360
|
+
// Step 1: Extract brackets [attr] or [attr=value]
|
|
1361
|
+
let bracketContent = null;
|
|
1362
|
+
let withoutBrackets = part;
|
|
1363
|
+
|
|
1364
|
+
const bracketMatch = part.match(/^([^\[]+)(\[[^\]]*\])(.*)$/);
|
|
1365
|
+
if (bracketMatch) {
|
|
1366
|
+
withoutBrackets = bracketMatch[1] + bracketMatch[3];
|
|
1367
|
+
if (bracketMatch[2]) {
|
|
1368
|
+
const content = bracketMatch[2].slice(1, -1);
|
|
1369
|
+
if (content) {
|
|
1370
|
+
bracketContent = content;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Step 2: Check for namespace (double colon ::)
|
|
1376
|
+
let namespace = undefined;
|
|
1377
|
+
let tagAndPosition = withoutBrackets;
|
|
1378
|
+
|
|
1379
|
+
if (withoutBrackets.includes('::')) {
|
|
1380
|
+
const nsIndex = withoutBrackets.indexOf('::');
|
|
1381
|
+
namespace = withoutBrackets.substring(0, nsIndex).trim();
|
|
1382
|
+
tagAndPosition = withoutBrackets.substring(nsIndex + 2).trim(); // Skip ::
|
|
1383
|
+
|
|
1384
|
+
if (!namespace) {
|
|
1385
|
+
throw new Error(`Invalid namespace in pattern: ${part}`);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Step 3: Parse tag and position (single colon :)
|
|
1390
|
+
let tag = undefined;
|
|
1391
|
+
let positionMatch = null;
|
|
1392
|
+
|
|
1393
|
+
if (tagAndPosition.includes(':')) {
|
|
1394
|
+
const colonIndex = tagAndPosition.lastIndexOf(':'); // Use last colon for position
|
|
1395
|
+
const tagPart = tagAndPosition.substring(0, colonIndex).trim();
|
|
1396
|
+
const posPart = tagAndPosition.substring(colonIndex + 1).trim();
|
|
1397
|
+
|
|
1398
|
+
// Verify position is a valid keyword
|
|
1399
|
+
const isPositionKeyword = ['first', 'last', 'odd', 'even'].includes(posPart) ||
|
|
1400
|
+
/^nth\(\d+\)$/.test(posPart);
|
|
1401
|
+
|
|
1402
|
+
if (isPositionKeyword) {
|
|
1403
|
+
tag = tagPart;
|
|
1404
|
+
positionMatch = posPart;
|
|
1405
|
+
} else {
|
|
1406
|
+
// Not a valid position keyword, treat whole thing as tag
|
|
1407
|
+
tag = tagAndPosition;
|
|
1408
|
+
}
|
|
1409
|
+
} else {
|
|
1410
|
+
tag = tagAndPosition;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if (!tag) {
|
|
1414
|
+
throw new Error(`Invalid segment pattern: ${part}`);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
segment.tag = tag;
|
|
1418
|
+
if (namespace) {
|
|
1419
|
+
segment.namespace = namespace;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Step 4: Parse attributes
|
|
1423
|
+
if (bracketContent) {
|
|
1424
|
+
if (bracketContent.includes('=')) {
|
|
1425
|
+
const eqIndex = bracketContent.indexOf('=');
|
|
1426
|
+
segment.attrName = bracketContent.substring(0, eqIndex).trim();
|
|
1427
|
+
segment.attrValue = bracketContent.substring(eqIndex + 1).trim();
|
|
1428
|
+
} else {
|
|
1429
|
+
segment.attrName = bracketContent.trim();
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Step 5: Parse position selector
|
|
1434
|
+
if (positionMatch) {
|
|
1435
|
+
const nthMatch = positionMatch.match(/^nth\((\d+)\)$/);
|
|
1436
|
+
if (nthMatch) {
|
|
1437
|
+
segment.position = 'nth';
|
|
1438
|
+
segment.positionValue = parseInt(nthMatch[1], 10);
|
|
1439
|
+
} else {
|
|
1440
|
+
segment.position = positionMatch;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
return segment;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Get the number of segments
|
|
1449
|
+
* @returns {number}
|
|
1450
|
+
*/
|
|
1451
|
+
get length() {
|
|
1452
|
+
return this.segments.length;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Check if expression contains deep wildcard
|
|
1457
|
+
* @returns {boolean}
|
|
1458
|
+
*/
|
|
1459
|
+
hasDeepWildcard() {
|
|
1460
|
+
return this._hasDeepWildcard;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Check if expression has attribute conditions
|
|
1465
|
+
* @returns {boolean}
|
|
1466
|
+
*/
|
|
1467
|
+
hasAttributeCondition() {
|
|
1468
|
+
return this._hasAttributeCondition;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* Check if expression has position selectors
|
|
1473
|
+
* @returns {boolean}
|
|
1474
|
+
*/
|
|
1475
|
+
hasPositionSelector() {
|
|
1476
|
+
return this._hasPositionSelector;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Get string representation
|
|
1481
|
+
* @returns {string}
|
|
1482
|
+
*/
|
|
1483
|
+
toString() {
|
|
1484
|
+
return this.pattern;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* ExpressionSet - An indexed collection of Expressions for efficient bulk matching
|
|
1490
|
+
*
|
|
1491
|
+
* Instead of iterating all expressions on every tag, ExpressionSet pre-indexes
|
|
1492
|
+
* them at insertion time by depth and terminal tag name. At match time, only
|
|
1493
|
+
* the relevant bucket is evaluated — typically reducing checks from O(E) to O(1)
|
|
1494
|
+
* lookup plus O(small bucket) matches.
|
|
1495
|
+
*
|
|
1496
|
+
* Three buckets are maintained:
|
|
1497
|
+
* - `_byDepthAndTag` — exact depth + exact tag name (tightest, used first)
|
|
1498
|
+
* - `_wildcardByDepth` — exact depth + wildcard tag `*` (depth-matched only)
|
|
1499
|
+
* - `_deepWildcards` — expressions containing `..` (cannot be depth-indexed)
|
|
1500
|
+
*
|
|
1501
|
+
* @example
|
|
1502
|
+
* import { Expression, ExpressionSet } from 'fast-xml-tagger';
|
|
1503
|
+
*
|
|
1504
|
+
* // Build once at config time
|
|
1505
|
+
* const stopNodes = new ExpressionSet();
|
|
1506
|
+
* stopNodes.add(new Expression('root.users.user'));
|
|
1507
|
+
* stopNodes.add(new Expression('root.config.setting'));
|
|
1508
|
+
* stopNodes.add(new Expression('..script'));
|
|
1509
|
+
*
|
|
1510
|
+
* // Query on every tag — hot path
|
|
1511
|
+
* if (stopNodes.matchesAny(matcher)) { ... }
|
|
1512
|
+
*/
|
|
1513
|
+
class ExpressionSet {
|
|
1514
|
+
constructor() {
|
|
1515
|
+
/** @type {Map<string, import('./Expression.js').default[]>} depth:tag → expressions */
|
|
1516
|
+
this._byDepthAndTag = new Map();
|
|
1517
|
+
|
|
1518
|
+
/** @type {Map<number, import('./Expression.js').default[]>} depth → wildcard-tag expressions */
|
|
1519
|
+
this._wildcardByDepth = new Map();
|
|
1520
|
+
|
|
1521
|
+
/** @type {import('./Expression.js').default[]} expressions containing deep wildcard (..) */
|
|
1522
|
+
this._deepWildcards = [];
|
|
1523
|
+
|
|
1524
|
+
/** @type {Set<string>} pattern strings already added — used for deduplication */
|
|
1525
|
+
this._patterns = new Set();
|
|
1526
|
+
|
|
1527
|
+
/** @type {boolean} whether the set is sealed against further additions */
|
|
1528
|
+
this._sealed = false;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* Add an Expression to the set.
|
|
1533
|
+
* Duplicate patterns (same pattern string) are silently ignored.
|
|
1534
|
+
*
|
|
1535
|
+
* @param {import('./Expression.js').default} expression - A pre-constructed Expression instance
|
|
1536
|
+
* @returns {this} for chaining
|
|
1537
|
+
* @throws {TypeError} if called after seal()
|
|
1538
|
+
*
|
|
1539
|
+
* @example
|
|
1540
|
+
* set.add(new Expression('root.users.user'));
|
|
1541
|
+
* set.add(new Expression('..script'));
|
|
1542
|
+
*/
|
|
1543
|
+
add(expression) {
|
|
1544
|
+
if (this._sealed) {
|
|
1545
|
+
throw new TypeError(
|
|
1546
|
+
'ExpressionSet is sealed. Create a new ExpressionSet to add more expressions.'
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// Deduplicate by pattern string
|
|
1551
|
+
if (this._patterns.has(expression.pattern)) return this;
|
|
1552
|
+
this._patterns.add(expression.pattern);
|
|
1553
|
+
|
|
1554
|
+
if (expression.hasDeepWildcard()) {
|
|
1555
|
+
this._deepWildcards.push(expression);
|
|
1556
|
+
return this;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const depth = expression.length;
|
|
1560
|
+
const lastSeg = expression.segments[expression.segments.length - 1];
|
|
1561
|
+
const tag = lastSeg?.tag;
|
|
1562
|
+
|
|
1563
|
+
if (!tag || tag === '*') {
|
|
1564
|
+
// Can index by depth but not by tag
|
|
1565
|
+
if (!this._wildcardByDepth.has(depth)) this._wildcardByDepth.set(depth, []);
|
|
1566
|
+
this._wildcardByDepth.get(depth).push(expression);
|
|
1567
|
+
} else {
|
|
1568
|
+
// Tightest bucket: depth + tag
|
|
1569
|
+
const key = `${depth}:${tag}`;
|
|
1570
|
+
if (!this._byDepthAndTag.has(key)) this._byDepthAndTag.set(key, []);
|
|
1571
|
+
this._byDepthAndTag.get(key).push(expression);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
return this;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Add multiple expressions at once.
|
|
1579
|
+
*
|
|
1580
|
+
* @param {import('./Expression.js').default[]} expressions - Array of Expression instances
|
|
1581
|
+
* @returns {this} for chaining
|
|
1582
|
+
*
|
|
1583
|
+
* @example
|
|
1584
|
+
* set.addAll([
|
|
1585
|
+
* new Expression('root.users.user'),
|
|
1586
|
+
* new Expression('root.config.setting'),
|
|
1587
|
+
* ]);
|
|
1588
|
+
*/
|
|
1589
|
+
addAll(expressions) {
|
|
1590
|
+
for (const expr of expressions) this.add(expr);
|
|
1591
|
+
return this;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* Check whether a pattern string is already present in the set.
|
|
1596
|
+
*
|
|
1597
|
+
* @param {import('./Expression.js').default} expression
|
|
1598
|
+
* @returns {boolean}
|
|
1599
|
+
*/
|
|
1600
|
+
has(expression) {
|
|
1601
|
+
return this._patterns.has(expression.pattern);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* Number of expressions in the set.
|
|
1606
|
+
* @type {number}
|
|
1607
|
+
*/
|
|
1608
|
+
get size() {
|
|
1609
|
+
return this._patterns.size;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* Seal the set against further modifications.
|
|
1614
|
+
* Useful to prevent accidental mutations after config is built.
|
|
1615
|
+
* Calling add() or addAll() on a sealed set throws a TypeError.
|
|
1616
|
+
*
|
|
1617
|
+
* @returns {this}
|
|
1618
|
+
*/
|
|
1619
|
+
seal() {
|
|
1620
|
+
this._sealed = true;
|
|
1621
|
+
return this;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Whether the set has been sealed.
|
|
1626
|
+
* @type {boolean}
|
|
1627
|
+
*/
|
|
1628
|
+
get isSealed() {
|
|
1629
|
+
return this._sealed;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Test whether the matcher's current path matches any expression in the set.
|
|
1634
|
+
*
|
|
1635
|
+
* Evaluation order (cheapest → most expensive):
|
|
1636
|
+
* 1. Exact depth + tag bucket — O(1) lookup, typically 0–2 expressions
|
|
1637
|
+
* 2. Depth-only wildcard bucket — O(1) lookup, rare
|
|
1638
|
+
* 3. Deep-wildcard list — always checked, but usually small
|
|
1639
|
+
*
|
|
1640
|
+
* @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view)
|
|
1641
|
+
* @returns {boolean} true if any expression matches the current path
|
|
1642
|
+
*
|
|
1643
|
+
* @example
|
|
1644
|
+
* if (stopNodes.matchesAny(matcher)) {
|
|
1645
|
+
* // handle stop node
|
|
1646
|
+
* }
|
|
1647
|
+
*/
|
|
1648
|
+
matchesAny(matcher) {
|
|
1649
|
+
return this.findMatch(matcher) !== null;
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Find and return the first Expression that matches the matcher's current path.
|
|
1653
|
+
*
|
|
1654
|
+
* Uses the same evaluation order as matchesAny (cheapest → most expensive):
|
|
1655
|
+
* 1. Exact depth + tag bucket
|
|
1656
|
+
* 2. Depth-only wildcard bucket
|
|
1657
|
+
* 3. Deep-wildcard list
|
|
1658
|
+
*
|
|
1659
|
+
* @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view)
|
|
1660
|
+
* @returns {import('./Expression.js').default | null} the first matching Expression, or null
|
|
1661
|
+
*
|
|
1662
|
+
* @example
|
|
1663
|
+
* const expr = stopNodes.findMatch(matcher);
|
|
1664
|
+
* if (expr) {
|
|
1665
|
+
* // access expr.config, expr.pattern, etc.
|
|
1666
|
+
* }
|
|
1667
|
+
*/
|
|
1668
|
+
findMatch(matcher) {
|
|
1669
|
+
const depth = matcher.getDepth();
|
|
1670
|
+
const tag = matcher.getCurrentTag();
|
|
1671
|
+
|
|
1672
|
+
// 1. Tightest bucket — most expressions live here
|
|
1673
|
+
const exactKey = `${depth}:${tag}`;
|
|
1674
|
+
const exactBucket = this._byDepthAndTag.get(exactKey);
|
|
1675
|
+
if (exactBucket) {
|
|
1676
|
+
for (let i = 0; i < exactBucket.length; i++) {
|
|
1677
|
+
if (matcher.matches(exactBucket[i])) return exactBucket[i];
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// 2. Depth-matched wildcard-tag expressions
|
|
1682
|
+
const wildcardBucket = this._wildcardByDepth.get(depth);
|
|
1683
|
+
if (wildcardBucket) {
|
|
1684
|
+
for (let i = 0; i < wildcardBucket.length; i++) {
|
|
1685
|
+
if (matcher.matches(wildcardBucket[i])) return wildcardBucket[i];
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// 3. Deep wildcards — cannot be pre-filtered by depth or tag
|
|
1690
|
+
for (let i = 0; i < this._deepWildcards.length; i++) {
|
|
1691
|
+
if (matcher.matches(this._deepWildcards[i])) return this._deepWildcards[i];
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
return null;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* MatcherView - A lightweight read-only view over a Matcher's internal state.
|
|
1700
|
+
*
|
|
1701
|
+
* Created once by Matcher and reused across all callbacks. Holds a direct
|
|
1702
|
+
* reference to the parent Matcher so it always reflects current parser state
|
|
1703
|
+
* with zero copying or freezing overhead.
|
|
1704
|
+
*
|
|
1705
|
+
* Users receive this via {@link Matcher#readOnly} or directly from parser
|
|
1706
|
+
* callbacks. It exposes all query and matching methods but has no mutation
|
|
1707
|
+
* methods — misuse is caught at the TypeScript level rather than at runtime.
|
|
1708
|
+
*
|
|
1709
|
+
* @example
|
|
1710
|
+
* const matcher = new Matcher();
|
|
1711
|
+
* const view = matcher.readOnly();
|
|
1712
|
+
*
|
|
1713
|
+
* matcher.push("root", {});
|
|
1714
|
+
* view.getCurrentTag(); // "root"
|
|
1715
|
+
* view.getDepth(); // 1
|
|
1716
|
+
*/
|
|
1717
|
+
class MatcherView {
|
|
1718
|
+
/**
|
|
1719
|
+
* @param {Matcher} matcher - The parent Matcher instance to read from.
|
|
1720
|
+
*/
|
|
1721
|
+
constructor(matcher) {
|
|
1722
|
+
this._matcher = matcher;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/**
|
|
1726
|
+
* Get the path separator used by the parent matcher.
|
|
1727
|
+
* @returns {string}
|
|
1728
|
+
*/
|
|
1729
|
+
get separator() {
|
|
1730
|
+
return this._matcher.separator;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Get current tag name.
|
|
1735
|
+
* @returns {string|undefined}
|
|
1736
|
+
*/
|
|
1737
|
+
getCurrentTag() {
|
|
1738
|
+
const path = this._matcher.path;
|
|
1739
|
+
return path.length > 0 ? path[path.length - 1].tag : undefined;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Get current namespace.
|
|
1744
|
+
* @returns {string|undefined}
|
|
1745
|
+
*/
|
|
1746
|
+
getCurrentNamespace() {
|
|
1747
|
+
const path = this._matcher.path;
|
|
1748
|
+
return path.length > 0 ? path[path.length - 1].namespace : undefined;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Get current node's attribute value.
|
|
1753
|
+
* @param {string} attrName
|
|
1754
|
+
* @returns {*}
|
|
1755
|
+
*/
|
|
1756
|
+
getAttrValue(attrName) {
|
|
1757
|
+
const path = this._matcher.path;
|
|
1758
|
+
if (path.length === 0) return undefined;
|
|
1759
|
+
return path[path.length - 1].values?.[attrName];
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* Check if current node has an attribute.
|
|
1764
|
+
* @param {string} attrName
|
|
1765
|
+
* @returns {boolean}
|
|
1766
|
+
*/
|
|
1767
|
+
hasAttr(attrName) {
|
|
1768
|
+
const path = this._matcher.path;
|
|
1769
|
+
if (path.length === 0) return false;
|
|
1770
|
+
const current = path[path.length - 1];
|
|
1771
|
+
return current.values !== undefined && attrName in current.values;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* Get current node's sibling position (child index in parent).
|
|
1776
|
+
* @returns {number}
|
|
1777
|
+
*/
|
|
1778
|
+
getPosition() {
|
|
1779
|
+
const path = this._matcher.path;
|
|
1780
|
+
if (path.length === 0) return -1;
|
|
1781
|
+
return path[path.length - 1].position ?? 0;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* Get current node's repeat counter (occurrence count of this tag name).
|
|
1786
|
+
* @returns {number}
|
|
1787
|
+
*/
|
|
1788
|
+
getCounter() {
|
|
1789
|
+
const path = this._matcher.path;
|
|
1790
|
+
if (path.length === 0) return -1;
|
|
1791
|
+
return path[path.length - 1].counter ?? 0;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/**
|
|
1795
|
+
* Get current node's sibling index (alias for getPosition).
|
|
1796
|
+
* @returns {number}
|
|
1797
|
+
* @deprecated Use getPosition() or getCounter() instead
|
|
1798
|
+
*/
|
|
1799
|
+
getIndex() {
|
|
1800
|
+
return this.getPosition();
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Get current path depth.
|
|
1805
|
+
* @returns {number}
|
|
1806
|
+
*/
|
|
1807
|
+
getDepth() {
|
|
1808
|
+
return this._matcher.path.length;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/**
|
|
1812
|
+
* Get path as string.
|
|
1813
|
+
* @param {string} [separator] - Optional separator (uses default if not provided)
|
|
1814
|
+
* @param {boolean} [includeNamespace=true]
|
|
1815
|
+
* @returns {string}
|
|
1816
|
+
*/
|
|
1817
|
+
toString(separator, includeNamespace = true) {
|
|
1818
|
+
return this._matcher.toString(separator, includeNamespace);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* Get path as array of tag names.
|
|
1823
|
+
* @returns {string[]}
|
|
1824
|
+
*/
|
|
1825
|
+
toArray() {
|
|
1826
|
+
return this._matcher.path.map(n => n.tag);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/**
|
|
1830
|
+
* Match current path against an Expression.
|
|
1831
|
+
* @param {Expression} expression
|
|
1832
|
+
* @returns {boolean}
|
|
1833
|
+
*/
|
|
1834
|
+
matches(expression) {
|
|
1835
|
+
return this._matcher.matches(expression);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/**
|
|
1839
|
+
* Match any expression in the given set against the current path.
|
|
1840
|
+
* @param {ExpressionSet} exprSet
|
|
1841
|
+
* @returns {boolean}
|
|
1842
|
+
*/
|
|
1843
|
+
matchesAny(exprSet) {
|
|
1844
|
+
return exprSet.matchesAny(this._matcher);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
/**
|
|
1849
|
+
* Matcher - Tracks current path in XML/JSON tree and matches against Expressions.
|
|
1850
|
+
*
|
|
1851
|
+
* The matcher maintains a stack of nodes representing the current path from root to
|
|
1852
|
+
* current tag. It only stores attribute values for the current (top) node to minimize
|
|
1853
|
+
* memory usage. Sibling tracking is used to auto-calculate position and counter.
|
|
1854
|
+
*
|
|
1855
|
+
* Use {@link Matcher#readOnly} to obtain a {@link MatcherView} safe to pass to
|
|
1856
|
+
* user callbacks — it always reflects current state with no Proxy overhead.
|
|
1857
|
+
*
|
|
1858
|
+
* @example
|
|
1859
|
+
* const matcher = new Matcher();
|
|
1860
|
+
* matcher.push("root", {});
|
|
1861
|
+
* matcher.push("users", {});
|
|
1862
|
+
* matcher.push("user", { id: "123", type: "admin" });
|
|
1863
|
+
*
|
|
1864
|
+
* const expr = new Expression("root.users.user");
|
|
1865
|
+
* matcher.matches(expr); // true
|
|
1866
|
+
*/
|
|
1867
|
+
class Matcher {
|
|
1868
|
+
/**
|
|
1869
|
+
* Create a new Matcher.
|
|
1870
|
+
* @param {Object} [options={}]
|
|
1871
|
+
* @param {string} [options.separator='.'] - Default path separator
|
|
1872
|
+
*/
|
|
1873
|
+
constructor(options = {}) {
|
|
1874
|
+
this.separator = options.separator || '.';
|
|
1875
|
+
this.path = [];
|
|
1876
|
+
this.siblingStacks = [];
|
|
1877
|
+
// Each path node: { tag, values, position, counter, namespace? }
|
|
1878
|
+
// values only present for current (last) node
|
|
1879
|
+
// Each siblingStacks entry: Map<tagName, count> tracking occurrences at each level
|
|
1880
|
+
this._pathStringCache = null;
|
|
1881
|
+
this._view = new MatcherView(this);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
/**
|
|
1885
|
+
* Push a new tag onto the path.
|
|
1886
|
+
* @param {string} tagName
|
|
1887
|
+
* @param {Object|null} [attrValues=null]
|
|
1888
|
+
* @param {string|null} [namespace=null]
|
|
1889
|
+
*/
|
|
1890
|
+
push(tagName, attrValues = null, namespace = null) {
|
|
1891
|
+
this._pathStringCache = null;
|
|
1892
|
+
|
|
1893
|
+
// Remove values from previous current node (now becoming ancestor)
|
|
1894
|
+
if (this.path.length > 0) {
|
|
1895
|
+
this.path[this.path.length - 1].values = undefined;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// Get or create sibling tracking for current level
|
|
1899
|
+
const currentLevel = this.path.length;
|
|
1900
|
+
if (!this.siblingStacks[currentLevel]) {
|
|
1901
|
+
this.siblingStacks[currentLevel] = new Map();
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const siblings = this.siblingStacks[currentLevel];
|
|
1905
|
+
|
|
1906
|
+
// Create a unique key for sibling tracking that includes namespace
|
|
1907
|
+
const siblingKey = namespace ? `${namespace}:${tagName}` : tagName;
|
|
1908
|
+
|
|
1909
|
+
// Calculate counter (how many times this tag appeared at this level)
|
|
1910
|
+
const counter = siblings.get(siblingKey) || 0;
|
|
1911
|
+
|
|
1912
|
+
// Calculate position (total children at this level so far)
|
|
1913
|
+
let position = 0;
|
|
1914
|
+
for (const count of siblings.values()) {
|
|
1915
|
+
position += count;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// Update sibling count for this tag
|
|
1919
|
+
siblings.set(siblingKey, counter + 1);
|
|
1920
|
+
|
|
1921
|
+
// Create new node
|
|
1922
|
+
const node = {
|
|
1923
|
+
tag: tagName,
|
|
1924
|
+
position: position,
|
|
1925
|
+
counter: counter
|
|
1926
|
+
};
|
|
1927
|
+
|
|
1928
|
+
if (namespace !== null && namespace !== undefined) {
|
|
1929
|
+
node.namespace = namespace;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
if (attrValues !== null && attrValues !== undefined) {
|
|
1933
|
+
node.values = attrValues;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
this.path.push(node);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* Pop the last tag from the path.
|
|
1941
|
+
* @returns {Object|undefined} The popped node
|
|
1942
|
+
*/
|
|
1943
|
+
pop() {
|
|
1944
|
+
if (this.path.length === 0) return undefined;
|
|
1945
|
+
this._pathStringCache = null;
|
|
1946
|
+
|
|
1947
|
+
const node = this.path.pop();
|
|
1948
|
+
|
|
1949
|
+
if (this.siblingStacks.length > this.path.length + 1) {
|
|
1950
|
+
this.siblingStacks.length = this.path.length + 1;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
return node;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/**
|
|
1957
|
+
* Update current node's attribute values.
|
|
1958
|
+
* Useful when attributes are parsed after push.
|
|
1959
|
+
* @param {Object} attrValues
|
|
1960
|
+
*/
|
|
1961
|
+
updateCurrent(attrValues) {
|
|
1962
|
+
if (this.path.length > 0) {
|
|
1963
|
+
const current = this.path[this.path.length - 1];
|
|
1964
|
+
if (attrValues !== null && attrValues !== undefined) {
|
|
1965
|
+
current.values = attrValues;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/**
|
|
1971
|
+
* Get current tag name.
|
|
1972
|
+
* @returns {string|undefined}
|
|
1973
|
+
*/
|
|
1974
|
+
getCurrentTag() {
|
|
1975
|
+
return this.path.length > 0 ? this.path[this.path.length - 1].tag : undefined;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
/**
|
|
1979
|
+
* Get current namespace.
|
|
1980
|
+
* @returns {string|undefined}
|
|
1981
|
+
*/
|
|
1982
|
+
getCurrentNamespace() {
|
|
1983
|
+
return this.path.length > 0 ? this.path[this.path.length - 1].namespace : undefined;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
/**
|
|
1987
|
+
* Get current node's attribute value.
|
|
1988
|
+
* @param {string} attrName
|
|
1989
|
+
* @returns {*}
|
|
1990
|
+
*/
|
|
1991
|
+
getAttrValue(attrName) {
|
|
1992
|
+
if (this.path.length === 0) return undefined;
|
|
1993
|
+
return this.path[this.path.length - 1].values?.[attrName];
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
/**
|
|
1997
|
+
* Check if current node has an attribute.
|
|
1998
|
+
* @param {string} attrName
|
|
1999
|
+
* @returns {boolean}
|
|
2000
|
+
*/
|
|
2001
|
+
hasAttr(attrName) {
|
|
2002
|
+
if (this.path.length === 0) return false;
|
|
2003
|
+
const current = this.path[this.path.length - 1];
|
|
2004
|
+
return current.values !== undefined && attrName in current.values;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Get current node's sibling position (child index in parent).
|
|
2009
|
+
* @returns {number}
|
|
2010
|
+
*/
|
|
2011
|
+
getPosition() {
|
|
2012
|
+
if (this.path.length === 0) return -1;
|
|
2013
|
+
return this.path[this.path.length - 1].position ?? 0;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* Get current node's repeat counter (occurrence count of this tag name).
|
|
2018
|
+
* @returns {number}
|
|
2019
|
+
*/
|
|
2020
|
+
getCounter() {
|
|
2021
|
+
if (this.path.length === 0) return -1;
|
|
2022
|
+
return this.path[this.path.length - 1].counter ?? 0;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/**
|
|
2026
|
+
* Get current node's sibling index (alias for getPosition).
|
|
2027
|
+
* @returns {number}
|
|
2028
|
+
* @deprecated Use getPosition() or getCounter() instead
|
|
2029
|
+
*/
|
|
2030
|
+
getIndex() {
|
|
2031
|
+
return this.getPosition();
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* Get current path depth.
|
|
2036
|
+
* @returns {number}
|
|
2037
|
+
*/
|
|
2038
|
+
getDepth() {
|
|
2039
|
+
return this.path.length;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* Get path as string.
|
|
2044
|
+
* @param {string} [separator] - Optional separator (uses default if not provided)
|
|
2045
|
+
* @param {boolean} [includeNamespace=true]
|
|
2046
|
+
* @returns {string}
|
|
2047
|
+
*/
|
|
2048
|
+
toString(separator, includeNamespace = true) {
|
|
2049
|
+
const sep = separator || this.separator;
|
|
2050
|
+
const isDefault = (sep === this.separator && includeNamespace === true);
|
|
2051
|
+
|
|
2052
|
+
if (isDefault) {
|
|
2053
|
+
if (this._pathStringCache !== null) {
|
|
2054
|
+
return this._pathStringCache;
|
|
2055
|
+
}
|
|
2056
|
+
const result = this.path.map(n =>
|
|
2057
|
+
(n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
|
|
2058
|
+
).join(sep);
|
|
2059
|
+
this._pathStringCache = result;
|
|
2060
|
+
return result;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
return this.path.map(n =>
|
|
2064
|
+
(includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
|
|
2065
|
+
).join(sep);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Get path as array of tag names.
|
|
2070
|
+
* @returns {string[]}
|
|
2071
|
+
*/
|
|
2072
|
+
toArray() {
|
|
2073
|
+
return this.path.map(n => n.tag);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* Reset the path to empty.
|
|
2078
|
+
*/
|
|
2079
|
+
reset() {
|
|
2080
|
+
this._pathStringCache = null;
|
|
2081
|
+
this.path = [];
|
|
2082
|
+
this.siblingStacks = [];
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* Match current path against an Expression.
|
|
2087
|
+
* @param {Expression} expression
|
|
2088
|
+
* @returns {boolean}
|
|
2089
|
+
*/
|
|
2090
|
+
matches(expression) {
|
|
2091
|
+
const segments = expression.segments;
|
|
2092
|
+
|
|
2093
|
+
if (segments.length === 0) {
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
if (expression.hasDeepWildcard()) {
|
|
2098
|
+
return this._matchWithDeepWildcard(segments);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
return this._matchSimple(segments);
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
/**
|
|
2105
|
+
* @private
|
|
2106
|
+
*/
|
|
2107
|
+
_matchSimple(segments) {
|
|
2108
|
+
if (this.path.length !== segments.length) {
|
|
2109
|
+
return false;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
for (let i = 0; i < segments.length; i++) {
|
|
2113
|
+
if (!this._matchSegment(segments[i], this.path[i], i === this.path.length - 1)) {
|
|
2114
|
+
return false;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
return true;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
/**
|
|
2122
|
+
* @private
|
|
2123
|
+
*/
|
|
2124
|
+
_matchWithDeepWildcard(segments) {
|
|
2125
|
+
let pathIdx = this.path.length - 1;
|
|
2126
|
+
let segIdx = segments.length - 1;
|
|
2127
|
+
|
|
2128
|
+
while (segIdx >= 0 && pathIdx >= 0) {
|
|
2129
|
+
const segment = segments[segIdx];
|
|
2130
|
+
|
|
2131
|
+
if (segment.type === 'deep-wildcard') {
|
|
2132
|
+
segIdx--;
|
|
2133
|
+
|
|
2134
|
+
if (segIdx < 0) {
|
|
2135
|
+
return true;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
const nextSeg = segments[segIdx];
|
|
2139
|
+
let found = false;
|
|
2140
|
+
|
|
2141
|
+
for (let i = pathIdx; i >= 0; i--) {
|
|
2142
|
+
if (this._matchSegment(nextSeg, this.path[i], i === this.path.length - 1)) {
|
|
2143
|
+
pathIdx = i - 1;
|
|
2144
|
+
segIdx--;
|
|
2145
|
+
found = true;
|
|
2146
|
+
break;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
if (!found) {
|
|
2151
|
+
return false;
|
|
2152
|
+
}
|
|
2153
|
+
} else {
|
|
2154
|
+
if (!this._matchSegment(segment, this.path[pathIdx], pathIdx === this.path.length - 1)) {
|
|
2155
|
+
return false;
|
|
2156
|
+
}
|
|
2157
|
+
pathIdx--;
|
|
2158
|
+
segIdx--;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
return segIdx < 0;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
/**
|
|
2166
|
+
* @private
|
|
2167
|
+
*/
|
|
2168
|
+
_matchSegment(segment, node, isCurrentNode) {
|
|
2169
|
+
if (segment.tag !== '*' && segment.tag !== node.tag) {
|
|
2170
|
+
return false;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
if (segment.namespace !== undefined) {
|
|
2174
|
+
if (segment.namespace !== '*' && segment.namespace !== node.namespace) {
|
|
2175
|
+
return false;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
if (segment.attrName !== undefined) {
|
|
2180
|
+
if (!isCurrentNode) {
|
|
2181
|
+
return false;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
if (!node.values || !(segment.attrName in node.values)) {
|
|
2185
|
+
return false;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
if (segment.attrValue !== undefined) {
|
|
2189
|
+
if (String(node.values[segment.attrName]) !== String(segment.attrValue)) {
|
|
2190
|
+
return false;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
if (segment.position !== undefined) {
|
|
2196
|
+
if (!isCurrentNode) {
|
|
2197
|
+
return false;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
const counter = node.counter ?? 0;
|
|
2201
|
+
|
|
2202
|
+
if (segment.position === 'first' && counter !== 0) {
|
|
2203
|
+
return false;
|
|
2204
|
+
} else if (segment.position === 'odd' && counter % 2 !== 1) {
|
|
2205
|
+
return false;
|
|
2206
|
+
} else if (segment.position === 'even' && counter % 2 !== 0) {
|
|
2207
|
+
return false;
|
|
2208
|
+
} else if (segment.position === 'nth' && counter !== segment.positionValue) {
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
return true;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* Match any expression in the given set against the current path.
|
|
2218
|
+
* @param {ExpressionSet} exprSet
|
|
2219
|
+
* @returns {boolean}
|
|
2220
|
+
*/
|
|
2221
|
+
matchesAny(exprSet) {
|
|
2222
|
+
return exprSet.matchesAny(this);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/**
|
|
2226
|
+
* Create a snapshot of current state.
|
|
2227
|
+
* @returns {Object}
|
|
2228
|
+
*/
|
|
2229
|
+
snapshot() {
|
|
2230
|
+
return {
|
|
2231
|
+
path: this.path.map(node => ({ ...node })),
|
|
2232
|
+
siblingStacks: this.siblingStacks.map(map => new Map(map))
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* Restore state from snapshot.
|
|
2238
|
+
* @param {Object} snapshot
|
|
2239
|
+
*/
|
|
2240
|
+
restore(snapshot) {
|
|
2241
|
+
this._pathStringCache = null;
|
|
2242
|
+
this.path = snapshot.path.map(node => ({ ...node }));
|
|
2243
|
+
this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map));
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
/**
|
|
2247
|
+
* Return the read-only {@link MatcherView} for this matcher.
|
|
2248
|
+
*
|
|
2249
|
+
* The same instance is returned on every call — no allocation occurs.
|
|
2250
|
+
* It always reflects the current parser state and is safe to pass to
|
|
2251
|
+
* user callbacks without risk of accidental mutation.
|
|
2252
|
+
*
|
|
2253
|
+
* @returns {MatcherView}
|
|
2254
|
+
*
|
|
2255
|
+
* @example
|
|
2256
|
+
* const view = matcher.readOnly();
|
|
2257
|
+
* // pass view to callbacks — it stays in sync automatically
|
|
2258
|
+
* view.matches(expr); // ✓
|
|
2259
|
+
* view.getCurrentTag(); // ✓
|
|
2260
|
+
* // view.push(...) // ✗ method does not exist — caught by TypeScript
|
|
2261
|
+
*/
|
|
2262
|
+
readOnly() {
|
|
2263
|
+
return this._view;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
1115
2267
|
// const regx =
|
|
1116
2268
|
// '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
|
|
1117
2269
|
// .replace(/NAME/g, util.nameRegexp);
|
|
@@ -1119,6 +2271,57 @@ function getIgnoreAttributesFn(ignoreAttributes) {
|
|
|
1119
2271
|
//const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g");
|
|
1120
2272
|
//const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g");
|
|
1121
2273
|
|
|
2274
|
+
// Helper functions for attribute and namespace handling
|
|
2275
|
+
|
|
2276
|
+
/**
|
|
2277
|
+
* Extract raw attributes (without prefix) from prefixed attribute map
|
|
2278
|
+
* @param {object} prefixedAttrs - Attributes with prefix from buildAttributesMap
|
|
2279
|
+
* @param {object} options - Parser options containing attributeNamePrefix
|
|
2280
|
+
* @returns {object} Raw attributes for matcher
|
|
2281
|
+
*/
|
|
2282
|
+
function extractRawAttributes(prefixedAttrs, options) {
|
|
2283
|
+
if (!prefixedAttrs) return {};
|
|
2284
|
+
|
|
2285
|
+
// Handle attributesGroupName option
|
|
2286
|
+
const attrs = options.attributesGroupName
|
|
2287
|
+
? prefixedAttrs[options.attributesGroupName]
|
|
2288
|
+
: prefixedAttrs;
|
|
2289
|
+
|
|
2290
|
+
if (!attrs) return {};
|
|
2291
|
+
|
|
2292
|
+
const rawAttrs = {};
|
|
2293
|
+
for (const key in attrs) {
|
|
2294
|
+
// Remove the attribute prefix to get raw name
|
|
2295
|
+
if (key.startsWith(options.attributeNamePrefix)) {
|
|
2296
|
+
const rawName = key.substring(options.attributeNamePrefix.length);
|
|
2297
|
+
rawAttrs[rawName] = attrs[key];
|
|
2298
|
+
} else {
|
|
2299
|
+
// Attribute without prefix (shouldn't normally happen, but be safe)
|
|
2300
|
+
rawAttrs[key] = attrs[key];
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return rawAttrs;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
/**
|
|
2307
|
+
* Extract namespace from raw tag name
|
|
2308
|
+
* @param {string} rawTagName - Tag name possibly with namespace (e.g., "soap:Envelope")
|
|
2309
|
+
* @returns {string|undefined} Namespace or undefined
|
|
2310
|
+
*/
|
|
2311
|
+
function extractNamespace(rawTagName) {
|
|
2312
|
+
if (!rawTagName || typeof rawTagName !== 'string') return undefined;
|
|
2313
|
+
|
|
2314
|
+
const colonIndex = rawTagName.indexOf(':');
|
|
2315
|
+
if (colonIndex !== -1 && colonIndex > 0) {
|
|
2316
|
+
const ns = rawTagName.substring(0, colonIndex);
|
|
2317
|
+
// Don't treat xmlns as a namespace
|
|
2318
|
+
if (ns !== 'xmlns') {
|
|
2319
|
+
return ns;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return undefined;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
1122
2325
|
class OrderedObjParser {
|
|
1123
2326
|
constructor(options) {
|
|
1124
2327
|
this.options = options;
|
|
@@ -1159,22 +2362,35 @@ class OrderedObjParser {
|
|
|
1159
2362
|
this.readStopNodeData = readStopNodeData;
|
|
1160
2363
|
this.saveTextToParentTag = saveTextToParentTag;
|
|
1161
2364
|
this.addChild = addChild;
|
|
1162
|
-
this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes);
|
|
2365
|
+
this.ignoreAttributesFn = getIgnoreAttributesFn$1(this.options.ignoreAttributes);
|
|
1163
2366
|
this.entityExpansionCount = 0;
|
|
1164
2367
|
this.currentExpandedLength = 0;
|
|
1165
2368
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
2369
|
+
// Initialize path matcher for path-expression-matcher
|
|
2370
|
+
this.matcher = new Matcher();
|
|
2371
|
+
|
|
2372
|
+
// Live read-only proxy of matcher — PEM creates and caches this internally.
|
|
2373
|
+
// All user callbacks receive this instead of the mutable matcher.
|
|
2374
|
+
this.readonlyMatcher = this.matcher.readOnly();
|
|
2375
|
+
|
|
2376
|
+
// Flag to track if current node is a stop node (optimization)
|
|
2377
|
+
this.isCurrentNodeStopNode = false;
|
|
2378
|
+
|
|
2379
|
+
// Pre-compile stopNodes expressions
|
|
2380
|
+
this.stopNodeExpressionsSet = new ExpressionSet();
|
|
2381
|
+
const stopNodesOpts = this.options.stopNodes;
|
|
2382
|
+
if (stopNodesOpts && stopNodesOpts.length > 0) {
|
|
2383
|
+
for (let i = 0; i < stopNodesOpts.length; i++) {
|
|
2384
|
+
const stopNodeExp = stopNodesOpts[i];
|
|
2385
|
+
if (typeof stopNodeExp === 'string') {
|
|
2386
|
+
// Convert string to Expression object
|
|
2387
|
+
this.stopNodeExpressionsSet.add(new Expression(stopNodeExp));
|
|
2388
|
+
} else if (stopNodeExp instanceof Expression) {
|
|
2389
|
+
// Already an Expression object
|
|
2390
|
+
this.stopNodeExpressionsSet.add(stopNodeExp);
|
|
1176
2391
|
}
|
|
1177
2392
|
}
|
|
2393
|
+
this.stopNodeExpressionsSet.seal();
|
|
1178
2394
|
}
|
|
1179
2395
|
}
|
|
1180
2396
|
|
|
@@ -1195,33 +2411,36 @@ function addExternalEntities(externalEntities) {
|
|
|
1195
2411
|
/**
|
|
1196
2412
|
* @param {string} val
|
|
1197
2413
|
* @param {string} tagName
|
|
1198
|
-
* @param {string} jPath
|
|
2414
|
+
* @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
|
|
1199
2415
|
* @param {boolean} dontTrim
|
|
1200
2416
|
* @param {boolean} hasAttributes
|
|
1201
2417
|
* @param {boolean} isLeafNode
|
|
1202
2418
|
* @param {boolean} escapeEntities
|
|
1203
2419
|
*/
|
|
1204
2420
|
function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) {
|
|
2421
|
+
const options = this.options;
|
|
1205
2422
|
if (val !== undefined) {
|
|
1206
|
-
if (
|
|
2423
|
+
if (options.trimValues && !dontTrim) {
|
|
1207
2424
|
val = val.trim();
|
|
1208
2425
|
}
|
|
1209
2426
|
if (val.length > 0) {
|
|
1210
2427
|
if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);
|
|
1211
2428
|
|
|
1212
|
-
|
|
2429
|
+
// Pass jPath string or matcher based on options.jPath setting
|
|
2430
|
+
const jPathOrMatcher = options.jPath ? jPath.toString() : jPath;
|
|
2431
|
+
const newval = options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode);
|
|
1213
2432
|
if (newval === null || newval === undefined) {
|
|
1214
2433
|
//don't parse
|
|
1215
2434
|
return val;
|
|
1216
2435
|
} else if (typeof newval !== typeof val || newval !== val) {
|
|
1217
2436
|
//overwrite
|
|
1218
2437
|
return newval;
|
|
1219
|
-
} else if (
|
|
1220
|
-
return parseValue(val,
|
|
2438
|
+
} else if (options.trimValues) {
|
|
2439
|
+
return parseValue(val, options.parseTagValue, options.numberParseOptions);
|
|
1221
2440
|
} else {
|
|
1222
2441
|
const trimmedVal = val.trim();
|
|
1223
2442
|
if (trimmedVal === val) {
|
|
1224
|
-
return parseValue(val,
|
|
2443
|
+
return parseValue(val, options.parseTagValue, options.numberParseOptions);
|
|
1225
2444
|
} else {
|
|
1226
2445
|
return val;
|
|
1227
2446
|
}
|
|
@@ -1249,216 +2468,299 @@ function resolveNameSpace(tagname) {
|
|
|
1249
2468
|
const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm');
|
|
1250
2469
|
|
|
1251
2470
|
function buildAttributesMap(attrStr, jPath, tagName) {
|
|
1252
|
-
|
|
2471
|
+
const options = this.options;
|
|
2472
|
+
if (options.ignoreAttributes !== true && typeof attrStr === 'string') {
|
|
1253
2473
|
// attrStr = attrStr.replace(/\r?\n/g, ' ');
|
|
1254
2474
|
//attrStr = attrStr || attrStr.trim();
|
|
1255
2475
|
|
|
1256
2476
|
const matches = getAllMatches(attrStr, attrsRegx);
|
|
1257
2477
|
const len = matches.length; //don't make it inline
|
|
1258
2478
|
const attrs = {};
|
|
2479
|
+
|
|
2480
|
+
// Pre-process values once: trim + entity replacement
|
|
2481
|
+
// Reused in both matcher update and second pass
|
|
2482
|
+
const processedVals = new Array(len);
|
|
2483
|
+
let hasRawAttrs = false;
|
|
2484
|
+
const rawAttrsForMatcher = {};
|
|
2485
|
+
|
|
1259
2486
|
for (let i = 0; i < len; i++) {
|
|
1260
2487
|
const attrName = this.resolveNameSpace(matches[i][1]);
|
|
1261
|
-
|
|
1262
|
-
|
|
2488
|
+
const oldVal = matches[i][4];
|
|
2489
|
+
|
|
2490
|
+
if (attrName.length && oldVal !== undefined) {
|
|
2491
|
+
let val = oldVal;
|
|
2492
|
+
if (options.trimValues) val = val.trim();
|
|
2493
|
+
val = this.replaceEntitiesValue(val, tagName, this.readonlyMatcher);
|
|
2494
|
+
processedVals[i] = val;
|
|
2495
|
+
|
|
2496
|
+
rawAttrsForMatcher[attrName] = val;
|
|
2497
|
+
hasRawAttrs = true;
|
|
1263
2498
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// Update matcher ONCE before second pass, if applicable
|
|
2502
|
+
if (hasRawAttrs && typeof jPath === 'object' && jPath.updateCurrent) {
|
|
2503
|
+
jPath.updateCurrent(rawAttrsForMatcher);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// Hoist toString() once — path doesn't change during attribute processing
|
|
2507
|
+
const jPathStr = options.jPath ? jPath.toString() : this.readonlyMatcher;
|
|
2508
|
+
|
|
2509
|
+
// Second pass: apply processors, build final attrs
|
|
2510
|
+
let hasAttrs = false;
|
|
2511
|
+
for (let i = 0; i < len; i++) {
|
|
2512
|
+
const attrName = this.resolveNameSpace(matches[i][1]);
|
|
2513
|
+
|
|
2514
|
+
if (this.ignoreAttributesFn(attrName, jPathStr)) continue;
|
|
2515
|
+
|
|
2516
|
+
let aName = options.attributeNamePrefix + attrName;
|
|
2517
|
+
|
|
1266
2518
|
if (attrName.length) {
|
|
1267
|
-
if (
|
|
1268
|
-
aName =
|
|
2519
|
+
if (options.transformAttributeName) {
|
|
2520
|
+
aName = options.transformAttributeName(aName);
|
|
1269
2521
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
const newVal =
|
|
2522
|
+
aName = sanitizeName(aName, options);
|
|
2523
|
+
|
|
2524
|
+
if (matches[i][4] !== undefined) {
|
|
2525
|
+
// Reuse already-processed value — no double entity replacement
|
|
2526
|
+
const oldVal = processedVals[i];
|
|
2527
|
+
|
|
2528
|
+
const newVal = options.attributeValueProcessor(attrName, oldVal, jPathStr);
|
|
1277
2529
|
if (newVal === null || newVal === undefined) {
|
|
1278
|
-
//don't parse
|
|
1279
2530
|
attrs[aName] = oldVal;
|
|
1280
2531
|
} else if (typeof newVal !== typeof oldVal || newVal !== oldVal) {
|
|
1281
|
-
//overwrite
|
|
1282
2532
|
attrs[aName] = newVal;
|
|
1283
2533
|
} else {
|
|
1284
|
-
|
|
1285
|
-
attrs[aName] = parseValue(
|
|
1286
|
-
oldVal,
|
|
1287
|
-
this.options.parseAttributeValue,
|
|
1288
|
-
this.options.numberParseOptions
|
|
1289
|
-
);
|
|
2534
|
+
attrs[aName] = parseValue(oldVal, options.parseAttributeValue, options.numberParseOptions);
|
|
1290
2535
|
}
|
|
1291
|
-
|
|
2536
|
+
hasAttrs = true;
|
|
2537
|
+
} else if (options.allowBooleanAttributes) {
|
|
1292
2538
|
attrs[aName] = true;
|
|
2539
|
+
hasAttrs = true;
|
|
1293
2540
|
}
|
|
1294
2541
|
}
|
|
1295
2542
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
if (
|
|
2543
|
+
|
|
2544
|
+
if (!hasAttrs) return;
|
|
2545
|
+
|
|
2546
|
+
if (options.attributesGroupName) {
|
|
1300
2547
|
const attrCollection = {};
|
|
1301
|
-
attrCollection[
|
|
2548
|
+
attrCollection[options.attributesGroupName] = attrs;
|
|
1302
2549
|
return attrCollection;
|
|
1303
2550
|
}
|
|
1304
|
-
return attrs
|
|
2551
|
+
return attrs;
|
|
1305
2552
|
}
|
|
1306
2553
|
}
|
|
1307
|
-
|
|
1308
2554
|
const parseXml = function (xmlData) {
|
|
1309
2555
|
xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line
|
|
1310
2556
|
const xmlObj = new XmlNode('!xml');
|
|
1311
2557
|
let currentNode = xmlObj;
|
|
1312
2558
|
let textData = "";
|
|
1313
|
-
|
|
2559
|
+
|
|
2560
|
+
// Reset matcher for new document
|
|
2561
|
+
this.matcher.reset();
|
|
1314
2562
|
|
|
1315
2563
|
// Reset entity expansion counters for this document
|
|
1316
2564
|
this.entityExpansionCount = 0;
|
|
1317
2565
|
this.currentExpandedLength = 0;
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
2566
|
+
this.docTypeEntitiesKeys = [];
|
|
2567
|
+
this.lastEntitiesKeys = Object.keys(this.lastEntities);
|
|
2568
|
+
this.htmlEntitiesKeys = this.options.htmlEntities ? Object.keys(this.htmlEntities) : [];
|
|
2569
|
+
const options = this.options;
|
|
2570
|
+
const docTypeReader = new DocTypeReader(options.processEntities);
|
|
2571
|
+
const xmlLen = xmlData.length;
|
|
2572
|
+
for (let i = 0; i < xmlLen; i++) {//for each char in XML data
|
|
1321
2573
|
const ch = xmlData[i];
|
|
1322
2574
|
if (ch === '<') {
|
|
1323
2575
|
// const nextIndex = i+1;
|
|
1324
2576
|
// const _2ndChar = xmlData[nextIndex];
|
|
1325
|
-
|
|
2577
|
+
const c1 = xmlData.charCodeAt(i + 1);
|
|
2578
|
+
if (c1 === 47) {//Closing Tag '/'
|
|
1326
2579
|
const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.");
|
|
1327
2580
|
let tagName = xmlData.substring(i + 2, closeIndex).trim();
|
|
1328
2581
|
|
|
1329
|
-
if (
|
|
2582
|
+
if (options.removeNSPrefix) {
|
|
1330
2583
|
const colonIndex = tagName.indexOf(":");
|
|
1331
2584
|
if (colonIndex !== -1) {
|
|
1332
2585
|
tagName = tagName.substr(colonIndex + 1);
|
|
1333
2586
|
}
|
|
1334
2587
|
}
|
|
1335
2588
|
|
|
1336
|
-
|
|
1337
|
-
tagName = this.options.transformTagName(tagName);
|
|
1338
|
-
}
|
|
2589
|
+
tagName = transformTagName(options.transformTagName, tagName, "", options).tagName;
|
|
1339
2590
|
|
|
1340
2591
|
if (currentNode) {
|
|
1341
|
-
textData = this.saveTextToParentTag(textData, currentNode,
|
|
2592
|
+
textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
|
|
1342
2593
|
}
|
|
1343
2594
|
|
|
1344
2595
|
//check if last tag of nested tag was unpaired tag
|
|
1345
|
-
const lastTagName =
|
|
1346
|
-
if (tagName &&
|
|
2596
|
+
const lastTagName = this.matcher.getCurrentTag();
|
|
2597
|
+
if (tagName && options.unpairedTagsSet.has(tagName)) {
|
|
1347
2598
|
throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
|
|
1348
2599
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
2600
|
+
if (lastTagName && options.unpairedTagsSet.has(lastTagName)) {
|
|
2601
|
+
// Pop the unpaired tag
|
|
2602
|
+
this.matcher.pop();
|
|
1352
2603
|
this.tagsNodeStack.pop();
|
|
1353
|
-
} else {
|
|
1354
|
-
propIndex = jPath.lastIndexOf(".");
|
|
1355
2604
|
}
|
|
1356
|
-
|
|
2605
|
+
// Pop the closing tag
|
|
2606
|
+
this.matcher.pop();
|
|
2607
|
+
this.isCurrentNodeStopNode = false; // Reset flag when closing tag
|
|
1357
2608
|
|
|
1358
2609
|
currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
|
|
1359
2610
|
textData = "";
|
|
1360
2611
|
i = closeIndex;
|
|
1361
|
-
} else if (
|
|
2612
|
+
} else if (c1 === 63) { //'?'
|
|
1362
2613
|
|
|
1363
2614
|
let tagData = readTagExp(xmlData, i, false, "?>");
|
|
1364
2615
|
if (!tagData) throw new Error("Pi Tag is not closed.");
|
|
1365
2616
|
|
|
1366
|
-
textData = this.saveTextToParentTag(textData, currentNode,
|
|
1367
|
-
if ((
|
|
2617
|
+
textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
|
|
2618
|
+
if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) ; else {
|
|
1368
2619
|
|
|
1369
2620
|
const childNode = new XmlNode(tagData.tagName);
|
|
1370
|
-
childNode.add(
|
|
2621
|
+
childNode.add(options.textNodeName, "");
|
|
1371
2622
|
|
|
1372
2623
|
if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
|
|
1373
|
-
childNode[":@"] = this.buildAttributesMap(tagData.tagExp,
|
|
2624
|
+
childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName);
|
|
1374
2625
|
}
|
|
1375
|
-
this.addChild(currentNode, childNode,
|
|
2626
|
+
this.addChild(currentNode, childNode, this.readonlyMatcher, i);
|
|
1376
2627
|
}
|
|
1377
2628
|
|
|
1378
2629
|
|
|
1379
2630
|
i = tagData.closeIndex + 1;
|
|
1380
|
-
} else if (
|
|
2631
|
+
} else if (c1 === 33
|
|
2632
|
+
&& xmlData.charCodeAt(i + 2) === 45
|
|
2633
|
+
&& xmlData.charCodeAt(i + 3) === 45) { //'!--'
|
|
1381
2634
|
const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.");
|
|
1382
|
-
if (
|
|
2635
|
+
if (options.commentPropName) {
|
|
1383
2636
|
const comment = xmlData.substring(i + 4, endIndex - 2);
|
|
1384
2637
|
|
|
1385
|
-
textData = this.saveTextToParentTag(textData, currentNode,
|
|
2638
|
+
textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
|
|
1386
2639
|
|
|
1387
|
-
currentNode.add(
|
|
2640
|
+
currentNode.add(options.commentPropName, [{ [options.textNodeName]: comment }]);
|
|
1388
2641
|
}
|
|
1389
2642
|
i = endIndex;
|
|
1390
|
-
} else if (
|
|
2643
|
+
} else if (c1 === 33
|
|
2644
|
+
&& xmlData.charCodeAt(i + 2) === 68) { //'!D'
|
|
1391
2645
|
const result = docTypeReader.readDocType(xmlData, i);
|
|
1392
2646
|
this.docTypeEntities = result.entities;
|
|
2647
|
+
this.docTypeEntitiesKeys = Object.keys(this.docTypeEntities) || [];
|
|
1393
2648
|
i = result.i;
|
|
1394
|
-
} else if (
|
|
2649
|
+
} else if (c1 === 33
|
|
2650
|
+
&& xmlData.charCodeAt(i + 2) === 91) { // '!['
|
|
1395
2651
|
const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
|
|
1396
2652
|
const tagExp = xmlData.substring(i + 9, closeIndex);
|
|
1397
2653
|
|
|
1398
|
-
textData = this.saveTextToParentTag(textData, currentNode,
|
|
2654
|
+
textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
|
|
1399
2655
|
|
|
1400
|
-
let val = this.parseTextData(tagExp, currentNode.tagname,
|
|
2656
|
+
let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true);
|
|
1401
2657
|
if (val == undefined) val = "";
|
|
1402
2658
|
|
|
1403
2659
|
//cdata should be set even if it is 0 length string
|
|
1404
|
-
if (
|
|
1405
|
-
currentNode.add(
|
|
2660
|
+
if (options.cdataPropName) {
|
|
2661
|
+
currentNode.add(options.cdataPropName, [{ [options.textNodeName]: tagExp }]);
|
|
1406
2662
|
} else {
|
|
1407
|
-
currentNode.add(
|
|
2663
|
+
currentNode.add(options.textNodeName, val);
|
|
1408
2664
|
}
|
|
1409
2665
|
|
|
1410
2666
|
i = closeIndex + 2;
|
|
1411
2667
|
} else {//Opening tag
|
|
1412
|
-
let result = readTagExp(xmlData, i,
|
|
2668
|
+
let result = readTagExp(xmlData, i, options.removeNSPrefix);
|
|
2669
|
+
|
|
2670
|
+
// Safety check: readTagExp can return undefined
|
|
2671
|
+
if (!result) {
|
|
2672
|
+
// Log context for debugging
|
|
2673
|
+
const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlLen, i + 50));
|
|
2674
|
+
throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
1413
2677
|
let tagName = result.tagName;
|
|
1414
2678
|
const rawTagName = result.rawTagName;
|
|
1415
2679
|
let tagExp = result.tagExp;
|
|
1416
2680
|
let attrExpPresent = result.attrExpPresent;
|
|
1417
2681
|
let closeIndex = result.closeIndex;
|
|
1418
2682
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
2683
|
+
({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options));
|
|
2684
|
+
|
|
2685
|
+
if (options.strictReservedNames &&
|
|
2686
|
+
(tagName === options.commentPropName
|
|
2687
|
+
|| tagName === options.cdataPropName
|
|
2688
|
+
|| tagName === options.textNodeName
|
|
2689
|
+
|| tagName === options.attributesGroupName
|
|
2690
|
+
)) {
|
|
2691
|
+
throw new Error(`Invalid tag name: ${tagName}`);
|
|
1426
2692
|
}
|
|
1427
2693
|
|
|
1428
2694
|
//save text as child node
|
|
1429
2695
|
if (currentNode && textData) {
|
|
1430
2696
|
if (currentNode.tagname !== '!xml') {
|
|
1431
2697
|
//when nested tag is found
|
|
1432
|
-
textData = this.saveTextToParentTag(textData, currentNode,
|
|
2698
|
+
textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false);
|
|
1433
2699
|
}
|
|
1434
2700
|
}
|
|
1435
2701
|
|
|
1436
2702
|
//check if last tag was unpaired tag
|
|
1437
2703
|
const lastTag = currentNode;
|
|
1438
|
-
if (lastTag &&
|
|
2704
|
+
if (lastTag && options.unpairedTagsSet.has(lastTag.tagname)) {
|
|
1439
2705
|
currentNode = this.tagsNodeStack.pop();
|
|
1440
|
-
|
|
2706
|
+
this.matcher.pop();
|
|
1441
2707
|
}
|
|
2708
|
+
|
|
2709
|
+
// Clean up self-closing syntax BEFORE processing attributes
|
|
2710
|
+
// This is where tagExp gets the trailing / removed
|
|
2711
|
+
let isSelfClosing = false;
|
|
2712
|
+
if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
|
|
2713
|
+
isSelfClosing = true;
|
|
2714
|
+
if (tagName[tagName.length - 1] === "/") {
|
|
2715
|
+
tagName = tagName.substr(0, tagName.length - 1);
|
|
2716
|
+
tagExp = tagName;
|
|
2717
|
+
} else {
|
|
2718
|
+
tagExp = tagExp.substr(0, tagExp.length - 1);
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// Re-check attrExpPresent after cleaning
|
|
2722
|
+
attrExpPresent = (tagName !== tagExp);
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
// Now process attributes with CLEAN tagExp (no trailing /)
|
|
2726
|
+
let prefixedAttrs = null;
|
|
2727
|
+
let namespace = undefined;
|
|
2728
|
+
|
|
2729
|
+
// Extract namespace from rawTagName
|
|
2730
|
+
namespace = extractNamespace(rawTagName);
|
|
2731
|
+
|
|
2732
|
+
// Push tag to matcher FIRST (with empty attrs for now) so callbacks see correct path
|
|
1442
2733
|
if (tagName !== xmlObj.tagname) {
|
|
1443
|
-
|
|
2734
|
+
this.matcher.push(tagName, {}, namespace);
|
|
1444
2735
|
}
|
|
2736
|
+
|
|
2737
|
+
// Now build attributes - callbacks will see correct matcher state
|
|
2738
|
+
if (tagName !== tagExp && attrExpPresent) {
|
|
2739
|
+
// Build attributes (returns prefixed attributes for the tree)
|
|
2740
|
+
// Note: buildAttributesMap now internally updates the matcher with raw attributes
|
|
2741
|
+
prefixedAttrs = this.buildAttributesMap(tagExp, this.matcher, tagName);
|
|
2742
|
+
|
|
2743
|
+
if (prefixedAttrs) {
|
|
2744
|
+
// Extract raw attributes (without prefix) for our use
|
|
2745
|
+
extractRawAttributes(prefixedAttrs, options);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
// Now check if this is a stop node (after attributes are set)
|
|
2750
|
+
if (tagName !== xmlObj.tagname) {
|
|
2751
|
+
this.isCurrentNodeStopNode = this.isItStopNode();
|
|
2752
|
+
}
|
|
2753
|
+
|
|
1445
2754
|
const startIndex = i;
|
|
1446
|
-
if (this.
|
|
2755
|
+
if (this.isCurrentNodeStopNode) {
|
|
1447
2756
|
let tagContent = "";
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
tagName = tagName.substr(0, tagName.length - 1);
|
|
1452
|
-
jPath = jPath.substr(0, jPath.length - 1);
|
|
1453
|
-
tagExp = tagName;
|
|
1454
|
-
} else {
|
|
1455
|
-
tagExp = tagExp.substr(0, tagExp.length - 1);
|
|
1456
|
-
}
|
|
2757
|
+
|
|
2758
|
+
// For self-closing tags, content is empty
|
|
2759
|
+
if (isSelfClosing) {
|
|
1457
2760
|
i = result.closeIndex;
|
|
1458
2761
|
}
|
|
1459
2762
|
//unpaired tag
|
|
1460
|
-
else if (
|
|
1461
|
-
|
|
2763
|
+
else if (options.unpairedTagsSet.has(tagName)) {
|
|
1462
2764
|
i = result.closeIndex;
|
|
1463
2765
|
}
|
|
1464
2766
|
//normal tag
|
|
@@ -1472,52 +2774,54 @@ const parseXml = function (xmlData) {
|
|
|
1472
2774
|
|
|
1473
2775
|
const childNode = new XmlNode(tagName);
|
|
1474
2776
|
|
|
1475
|
-
if (
|
|
1476
|
-
childNode[":@"] =
|
|
1477
|
-
}
|
|
1478
|
-
if (tagContent) {
|
|
1479
|
-
tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true);
|
|
2777
|
+
if (prefixedAttrs) {
|
|
2778
|
+
childNode[":@"] = prefixedAttrs;
|
|
1480
2779
|
}
|
|
1481
2780
|
|
|
1482
|
-
|
|
1483
|
-
childNode.add(
|
|
2781
|
+
// For stop nodes, store raw content as-is without any processing
|
|
2782
|
+
childNode.add(options.textNodeName, tagContent);
|
|
2783
|
+
|
|
2784
|
+
this.matcher.pop(); // Pop the stop node tag
|
|
2785
|
+
this.isCurrentNodeStopNode = false; // Reset flag
|
|
1484
2786
|
|
|
1485
|
-
this.addChild(currentNode, childNode,
|
|
2787
|
+
this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
|
|
1486
2788
|
} else {
|
|
1487
2789
|
//selfClosing tag
|
|
1488
|
-
if (
|
|
1489
|
-
|
|
1490
|
-
tagName = tagName.substr(0, tagName.length - 1);
|
|
1491
|
-
jPath = jPath.substr(0, jPath.length - 1);
|
|
1492
|
-
tagExp = tagName;
|
|
1493
|
-
} else {
|
|
1494
|
-
tagExp = tagExp.substr(0, tagExp.length - 1);
|
|
1495
|
-
}
|
|
2790
|
+
if (isSelfClosing) {
|
|
2791
|
+
({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options));
|
|
1496
2792
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
tagExp = newTagName;
|
|
1501
|
-
}
|
|
1502
|
-
tagName = newTagName;
|
|
2793
|
+
const childNode = new XmlNode(tagName);
|
|
2794
|
+
if (prefixedAttrs) {
|
|
2795
|
+
childNode[":@"] = prefixedAttrs;
|
|
1503
2796
|
}
|
|
1504
|
-
|
|
2797
|
+
this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
|
|
2798
|
+
this.matcher.pop(); // Pop self-closing tag
|
|
2799
|
+
this.isCurrentNodeStopNode = false; // Reset flag
|
|
2800
|
+
}
|
|
2801
|
+
else if (options.unpairedTagsSet.has(tagName)) {//unpaired tag
|
|
1505
2802
|
const childNode = new XmlNode(tagName);
|
|
1506
|
-
if (
|
|
1507
|
-
childNode[":@"] =
|
|
2803
|
+
if (prefixedAttrs) {
|
|
2804
|
+
childNode[":@"] = prefixedAttrs;
|
|
1508
2805
|
}
|
|
1509
|
-
this.addChild(currentNode, childNode,
|
|
1510
|
-
|
|
2806
|
+
this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
|
|
2807
|
+
this.matcher.pop(); // Pop unpaired tag
|
|
2808
|
+
this.isCurrentNodeStopNode = false; // Reset flag
|
|
2809
|
+
i = result.closeIndex;
|
|
2810
|
+
// Continue to next iteration without changing currentNode
|
|
2811
|
+
continue;
|
|
1511
2812
|
}
|
|
1512
2813
|
//opening tag
|
|
1513
2814
|
else {
|
|
1514
2815
|
const childNode = new XmlNode(tagName);
|
|
2816
|
+
if (this.tagsNodeStack.length > options.maxNestedTags) {
|
|
2817
|
+
throw new Error("Maximum nested tags exceeded");
|
|
2818
|
+
}
|
|
1515
2819
|
this.tagsNodeStack.push(currentNode);
|
|
1516
2820
|
|
|
1517
|
-
if (
|
|
1518
|
-
childNode[":@"] =
|
|
2821
|
+
if (prefixedAttrs) {
|
|
2822
|
+
childNode[":@"] = prefixedAttrs;
|
|
1519
2823
|
}
|
|
1520
|
-
this.addChild(currentNode, childNode,
|
|
2824
|
+
this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
|
|
1521
2825
|
currentNode = childNode;
|
|
1522
2826
|
}
|
|
1523
2827
|
textData = "";
|
|
@@ -1531,10 +2835,13 @@ const parseXml = function (xmlData) {
|
|
|
1531
2835
|
return xmlObj.child;
|
|
1532
2836
|
};
|
|
1533
2837
|
|
|
1534
|
-
function addChild(currentNode, childNode,
|
|
2838
|
+
function addChild(currentNode, childNode, matcher, startIndex) {
|
|
1535
2839
|
// unset startIndex if not requested
|
|
1536
2840
|
if (!this.options.captureMetaData) startIndex = undefined;
|
|
1537
|
-
|
|
2841
|
+
|
|
2842
|
+
// Pass jPath string or matcher based on options.jPath setting
|
|
2843
|
+
const jPathOrMatcher = this.options.jPath ? matcher.toString() : matcher;
|
|
2844
|
+
const result = this.options.updateTag(childNode.tagname, jPathOrMatcher, childNode[":@"]);
|
|
1538
2845
|
if (result === false) ; else if (typeof result === "string") {
|
|
1539
2846
|
childNode.tagname = result;
|
|
1540
2847
|
currentNode.addChild(childNode, startIndex);
|
|
@@ -1543,33 +2850,40 @@ function addChild(currentNode, childNode, jPath, startIndex) {
|
|
|
1543
2850
|
}
|
|
1544
2851
|
}
|
|
1545
2852
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
2853
|
+
/**
|
|
2854
|
+
* @param {object} val - Entity object with regex and val properties
|
|
2855
|
+
* @param {string} tagName - Tag name
|
|
2856
|
+
* @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
|
|
2857
|
+
*/
|
|
2858
|
+
function replaceEntitiesValue$1(val, tagName, jPath) {
|
|
1552
2859
|
const entityConfig = this.options.processEntities;
|
|
1553
2860
|
|
|
1554
|
-
if (!entityConfig.enabled) {
|
|
2861
|
+
if (!entityConfig || !entityConfig.enabled) {
|
|
1555
2862
|
return val;
|
|
1556
2863
|
}
|
|
1557
2864
|
|
|
1558
|
-
// Check tag
|
|
2865
|
+
// Check if tag is allowed to contain entities
|
|
1559
2866
|
if (entityConfig.allowedTags) {
|
|
1560
|
-
|
|
1561
|
-
|
|
2867
|
+
const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
|
|
2868
|
+
const allowed = Array.isArray(entityConfig.allowedTags)
|
|
2869
|
+
? entityConfig.allowedTags.includes(tagName)
|
|
2870
|
+
: entityConfig.allowedTags(tagName, jPathOrMatcher);
|
|
2871
|
+
|
|
2872
|
+
if (!allowed) {
|
|
2873
|
+
return val;
|
|
1562
2874
|
}
|
|
1563
2875
|
}
|
|
1564
2876
|
|
|
2877
|
+
// Apply custom tag filter if provided
|
|
1565
2878
|
if (entityConfig.tagFilter) {
|
|
1566
|
-
|
|
2879
|
+
const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
|
|
2880
|
+
if (!entityConfig.tagFilter(tagName, jPathOrMatcher)) {
|
|
1567
2881
|
return val; // Skip based on custom filter
|
|
1568
2882
|
}
|
|
1569
2883
|
}
|
|
1570
2884
|
|
|
1571
2885
|
// Replace DOCTYPE entities
|
|
1572
|
-
for (
|
|
2886
|
+
for (const entityName of this.docTypeEntitiesKeys) {
|
|
1573
2887
|
const entity = this.docTypeEntities[entityName];
|
|
1574
2888
|
const matches = val.match(entity.regx);
|
|
1575
2889
|
|
|
@@ -1600,60 +2914,75 @@ const replaceEntitiesValue$1 = function (val, tagName, jPath) {
|
|
|
1600
2914
|
}
|
|
1601
2915
|
}
|
|
1602
2916
|
}
|
|
1603
|
-
}
|
|
1604
|
-
if (val.indexOf('&') === -1) return val;
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
const
|
|
2917
|
+
}
|
|
2918
|
+
if (val.indexOf('&') === -1) return val;
|
|
2919
|
+
// Replace standard entities
|
|
2920
|
+
for (const entityName of this.lastEntitiesKeys) {
|
|
2921
|
+
const entity = this.lastEntities[entityName];
|
|
2922
|
+
const matches = val.match(entity.regex);
|
|
2923
|
+
if (matches) {
|
|
2924
|
+
this.entityExpansionCount += matches.length;
|
|
2925
|
+
if (entityConfig.maxTotalExpansions &&
|
|
2926
|
+
this.entityExpansionCount > entityConfig.maxTotalExpansions) {
|
|
2927
|
+
throw new Error(
|
|
2928
|
+
`Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
1609
2932
|
val = val.replace(entity.regex, entity.val);
|
|
1610
2933
|
}
|
|
1611
|
-
if (val.indexOf('&') === -1) return val;
|
|
2934
|
+
if (val.indexOf('&') === -1) return val;
|
|
1612
2935
|
|
|
1613
2936
|
// Replace HTML entities if enabled
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
2937
|
+
for (const entityName of this.htmlEntitiesKeys) {
|
|
2938
|
+
const entity = this.htmlEntities[entityName];
|
|
2939
|
+
const matches = val.match(entity.regex);
|
|
2940
|
+
if (matches) {
|
|
2941
|
+
//console.log(matches);
|
|
2942
|
+
this.entityExpansionCount += matches.length;
|
|
2943
|
+
if (entityConfig.maxTotalExpansions &&
|
|
2944
|
+
this.entityExpansionCount > entityConfig.maxTotalExpansions) {
|
|
2945
|
+
throw new Error(
|
|
2946
|
+
`Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
1618
2949
|
}
|
|
2950
|
+
val = val.replace(entity.regex, entity.val);
|
|
1619
2951
|
}
|
|
1620
2952
|
|
|
1621
2953
|
// Replace ampersand entity last
|
|
1622
2954
|
val = val.replace(this.ampEntity.regex, this.ampEntity.val);
|
|
1623
2955
|
|
|
1624
2956
|
return val;
|
|
1625
|
-
}
|
|
2957
|
+
}
|
|
1626
2958
|
|
|
1627
2959
|
|
|
1628
|
-
function saveTextToParentTag(textData,
|
|
2960
|
+
function saveTextToParentTag(textData, parentNode, matcher, isLeafNode) {
|
|
1629
2961
|
if (textData) { //store previously collected data as textNode
|
|
1630
|
-
if (isLeafNode === undefined) isLeafNode =
|
|
2962
|
+
if (isLeafNode === undefined) isLeafNode = parentNode.child.length === 0;
|
|
1631
2963
|
|
|
1632
2964
|
textData = this.parseTextData(textData,
|
|
1633
|
-
|
|
1634
|
-
|
|
2965
|
+
parentNode.tagname,
|
|
2966
|
+
matcher,
|
|
1635
2967
|
false,
|
|
1636
|
-
|
|
2968
|
+
parentNode[":@"] ? Object.keys(parentNode[":@"]).length !== 0 : false,
|
|
1637
2969
|
isLeafNode);
|
|
1638
2970
|
|
|
1639
2971
|
if (textData !== undefined && textData !== "")
|
|
1640
|
-
|
|
2972
|
+
parentNode.add(this.options.textNodeName, textData);
|
|
1641
2973
|
textData = "";
|
|
1642
2974
|
}
|
|
1643
2975
|
return textData;
|
|
1644
2976
|
}
|
|
1645
2977
|
|
|
1646
|
-
//TODO: use jPath to simplify the logic
|
|
1647
2978
|
/**
|
|
1648
|
-
* @param {
|
|
1649
|
-
* @param {
|
|
1650
|
-
* @param {string} jPath
|
|
1651
|
-
* @param {string} currentTagName
|
|
2979
|
+
* @param {Array<Expression>} stopNodeExpressions - Array of compiled Expression objects
|
|
2980
|
+
* @param {Matcher} matcher - Current path matcher
|
|
1652
2981
|
*/
|
|
1653
|
-
function isItStopNode(
|
|
1654
|
-
if (
|
|
1655
|
-
|
|
1656
|
-
return
|
|
2982
|
+
function isItStopNode() {
|
|
2983
|
+
if (this.stopNodeExpressionsSet.size === 0) return false;
|
|
2984
|
+
|
|
2985
|
+
return this.matcher.matchesAny(this.stopNodeExpressionsSet);
|
|
1657
2986
|
}
|
|
1658
2987
|
|
|
1659
2988
|
/**
|
|
@@ -1663,32 +2992,33 @@ function isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName)
|
|
|
1663
2992
|
* @returns
|
|
1664
2993
|
*/
|
|
1665
2994
|
function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
|
|
1666
|
-
let attrBoundary;
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
2995
|
+
let attrBoundary = 0;
|
|
2996
|
+
const chars = [];
|
|
2997
|
+
const len = xmlData.length;
|
|
2998
|
+
const closeCode0 = closingChar.charCodeAt(0);
|
|
2999
|
+
const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1;
|
|
3000
|
+
|
|
3001
|
+
for (let index = i; index < len; index++) {
|
|
3002
|
+
const code = xmlData.charCodeAt(index);
|
|
3003
|
+
|
|
1670
3004
|
if (attrBoundary) {
|
|
1671
|
-
if (
|
|
1672
|
-
} else if (
|
|
1673
|
-
attrBoundary =
|
|
1674
|
-
} else if (
|
|
1675
|
-
if (
|
|
1676
|
-
if (xmlData
|
|
1677
|
-
return {
|
|
1678
|
-
data: tagExp,
|
|
1679
|
-
index: index
|
|
1680
|
-
}
|
|
3005
|
+
if (code === attrBoundary) attrBoundary = 0;
|
|
3006
|
+
} else if (code === 34 || code === 39) { // " or '
|
|
3007
|
+
attrBoundary = code;
|
|
3008
|
+
} else if (code === closeCode0) {
|
|
3009
|
+
if (closeCode1 !== -1) {
|
|
3010
|
+
if (xmlData.charCodeAt(index + 1) === closeCode1) {
|
|
3011
|
+
return { data: String.fromCharCode(...chars), index };
|
|
1681
3012
|
}
|
|
1682
3013
|
} else {
|
|
1683
|
-
return {
|
|
1684
|
-
data: tagExp,
|
|
1685
|
-
index: index
|
|
1686
|
-
}
|
|
3014
|
+
return { data: String.fromCharCode(...chars), index };
|
|
1687
3015
|
}
|
|
1688
|
-
} else if (
|
|
1689
|
-
|
|
3016
|
+
} else if (code === 9) { // \t
|
|
3017
|
+
chars.push(32); // space
|
|
3018
|
+
continue;
|
|
1690
3019
|
}
|
|
1691
|
-
|
|
3020
|
+
|
|
3021
|
+
chars.push(code);
|
|
1692
3022
|
}
|
|
1693
3023
|
}
|
|
1694
3024
|
|
|
@@ -1701,6 +3031,12 @@ function findClosingIndex(xmlData, str, i, errMsg) {
|
|
|
1701
3031
|
}
|
|
1702
3032
|
}
|
|
1703
3033
|
|
|
3034
|
+
function findClosingChar(xmlData, char, i, errMsg) {
|
|
3035
|
+
const closingIndex = xmlData.indexOf(char, i);
|
|
3036
|
+
if (closingIndex === -1) throw new Error(errMsg);
|
|
3037
|
+
return closingIndex; // no offset needed
|
|
3038
|
+
}
|
|
3039
|
+
|
|
1704
3040
|
function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") {
|
|
1705
3041
|
const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar);
|
|
1706
3042
|
if (!result) return;
|
|
@@ -1742,10 +3078,12 @@ function readStopNodeData(xmlData, tagName, i) {
|
|
|
1742
3078
|
// Starting at 1 since we already have an open tag
|
|
1743
3079
|
let openTagCount = 1;
|
|
1744
3080
|
|
|
1745
|
-
|
|
3081
|
+
const xmllen = xmlData.length;
|
|
3082
|
+
for (; i < xmllen; i++) {
|
|
1746
3083
|
if (xmlData[i] === "<") {
|
|
1747
|
-
|
|
1748
|
-
|
|
3084
|
+
const c1 = xmlData.charCodeAt(i + 1);
|
|
3085
|
+
if (c1 === 47) {//close tag '/'
|
|
3086
|
+
const closeIndex = findClosingChar(xmlData, ">", i, `${tagName} is not closed`);
|
|
1749
3087
|
let closeTagName = xmlData.substring(i + 2, closeIndex).trim();
|
|
1750
3088
|
if (closeTagName === tagName) {
|
|
1751
3089
|
openTagCount--;
|
|
@@ -1757,13 +3095,16 @@ function readStopNodeData(xmlData, tagName, i) {
|
|
|
1757
3095
|
}
|
|
1758
3096
|
}
|
|
1759
3097
|
i = closeIndex;
|
|
1760
|
-
} else if (
|
|
3098
|
+
} else if (c1 === 63) { //?
|
|
1761
3099
|
const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.");
|
|
1762
3100
|
i = closeIndex;
|
|
1763
|
-
} else if (
|
|
3101
|
+
} else if (c1 === 33
|
|
3102
|
+
&& xmlData.charCodeAt(i + 2) === 45
|
|
3103
|
+
&& xmlData.charCodeAt(i + 3) === 45) { // '!--'
|
|
1764
3104
|
const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.");
|
|
1765
3105
|
i = closeIndex;
|
|
1766
|
-
} else if (
|
|
3106
|
+
} else if (c1 === 33
|
|
3107
|
+
&& xmlData.charCodeAt(i + 2) === 91) { // '!['
|
|
1767
3108
|
const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
|
|
1768
3109
|
i = closeIndex;
|
|
1769
3110
|
} else {
|
|
@@ -1807,97 +3148,173 @@ function fromCodePoint(str, base, prefix) {
|
|
|
1807
3148
|
}
|
|
1808
3149
|
}
|
|
1809
3150
|
|
|
3151
|
+
function transformTagName(fn, tagName, tagExp, options) {
|
|
3152
|
+
if (fn) {
|
|
3153
|
+
const newTagName = fn(tagName);
|
|
3154
|
+
if (tagExp === tagName) {
|
|
3155
|
+
tagExp = newTagName;
|
|
3156
|
+
}
|
|
3157
|
+
tagName = newTagName;
|
|
3158
|
+
}
|
|
3159
|
+
tagName = sanitizeName(tagName, options);
|
|
3160
|
+
return { tagName, tagExp };
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
|
|
3164
|
+
|
|
3165
|
+
function sanitizeName(name, options) {
|
|
3166
|
+
if (criticalProperties.includes(name)) {
|
|
3167
|
+
throw new Error(`[SECURITY] Invalid name: "${name}" is a reserved JavaScript keyword that could cause prototype pollution`);
|
|
3168
|
+
} else if (DANGEROUS_PROPERTY_NAMES.includes(name)) {
|
|
3169
|
+
return options.onDangerousProperty(name);
|
|
3170
|
+
}
|
|
3171
|
+
return name;
|
|
3172
|
+
}
|
|
3173
|
+
|
|
1810
3174
|
const METADATA_SYMBOL = XmlNode.getMetaDataSymbol();
|
|
1811
3175
|
|
|
3176
|
+
/**
|
|
3177
|
+
* Helper function to strip attribute prefix from attribute map
|
|
3178
|
+
* @param {object} attrs - Attributes with prefix (e.g., {"@_class": "code"})
|
|
3179
|
+
* @param {string} prefix - Attribute prefix to remove (e.g., "@_")
|
|
3180
|
+
* @returns {object} Attributes without prefix (e.g., {"class": "code"})
|
|
3181
|
+
*/
|
|
3182
|
+
function stripAttributePrefix(attrs, prefix) {
|
|
3183
|
+
if (!attrs || typeof attrs !== 'object') return {};
|
|
3184
|
+
if (!prefix) return attrs;
|
|
3185
|
+
|
|
3186
|
+
const rawAttrs = {};
|
|
3187
|
+
for (const key in attrs) {
|
|
3188
|
+
if (key.startsWith(prefix)) {
|
|
3189
|
+
const rawName = key.substring(prefix.length);
|
|
3190
|
+
rawAttrs[rawName] = attrs[key];
|
|
3191
|
+
} else {
|
|
3192
|
+
// Attribute without prefix (shouldn't normally happen, but be safe)
|
|
3193
|
+
rawAttrs[key] = attrs[key];
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
return rawAttrs;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
1812
3199
|
/**
|
|
1813
3200
|
*
|
|
1814
3201
|
* @param {array} node
|
|
1815
3202
|
* @param {any} options
|
|
3203
|
+
* @param {Matcher} matcher - Path matcher instance
|
|
1816
3204
|
* @returns
|
|
1817
3205
|
*/
|
|
1818
|
-
function prettify(node, options){
|
|
1819
|
-
return compress(
|
|
3206
|
+
function prettify(node, options, matcher, readonlyMatcher) {
|
|
3207
|
+
return compress(node, options, matcher, readonlyMatcher);
|
|
1820
3208
|
}
|
|
1821
3209
|
|
|
1822
3210
|
/**
|
|
1823
|
-
*
|
|
1824
3211
|
* @param {array} arr
|
|
1825
3212
|
* @param {object} options
|
|
1826
|
-
* @param {
|
|
3213
|
+
* @param {Matcher} matcher - Path matcher instance
|
|
1827
3214
|
* @returns object
|
|
1828
3215
|
*/
|
|
1829
|
-
function compress(arr, options,
|
|
3216
|
+
function compress(arr, options, matcher, readonlyMatcher) {
|
|
1830
3217
|
let text;
|
|
1831
|
-
const compressedObj = {};
|
|
3218
|
+
const compressedObj = {}; //This is intended to be a plain object
|
|
1832
3219
|
for (let i = 0; i < arr.length; i++) {
|
|
1833
3220
|
const tagObj = arr[i];
|
|
1834
3221
|
const property = propName$1(tagObj);
|
|
1835
|
-
let newJpath = "";
|
|
1836
|
-
if(jPath === undefined) newJpath = property;
|
|
1837
|
-
else newJpath = jPath + "." + property;
|
|
1838
3222
|
|
|
1839
|
-
|
|
1840
|
-
|
|
3223
|
+
// Push current property to matcher WITH RAW ATTRIBUTES (no prefix)
|
|
3224
|
+
if (property !== undefined && property !== options.textNodeName) {
|
|
3225
|
+
const rawAttrs = stripAttributePrefix(
|
|
3226
|
+
tagObj[":@"] || {},
|
|
3227
|
+
options.attributeNamePrefix
|
|
3228
|
+
);
|
|
3229
|
+
matcher.push(property, rawAttrs);
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
if (property === options.textNodeName) {
|
|
3233
|
+
if (text === undefined) text = tagObj[property];
|
|
1841
3234
|
else text += "" + tagObj[property];
|
|
1842
|
-
}else if(property === undefined){
|
|
3235
|
+
} else if (property === undefined) {
|
|
1843
3236
|
continue;
|
|
1844
|
-
}else if(tagObj[property]){
|
|
1845
|
-
|
|
1846
|
-
let val = compress(tagObj[property], options,
|
|
3237
|
+
} else if (tagObj[property]) {
|
|
3238
|
+
|
|
3239
|
+
let val = compress(tagObj[property], options, matcher, readonlyMatcher);
|
|
1847
3240
|
const isLeaf = isLeafTag(val, options);
|
|
1848
|
-
if (tagObj[METADATA_SYMBOL] !== undefined) {
|
|
1849
|
-
val[METADATA_SYMBOL] = tagObj[METADATA_SYMBOL]; // copy over metadata
|
|
1850
|
-
}
|
|
1851
3241
|
|
|
1852
|
-
if(tagObj[":@"]){
|
|
1853
|
-
assignAttributes(
|
|
1854
|
-
}else if(Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode){
|
|
3242
|
+
if (tagObj[":@"]) {
|
|
3243
|
+
assignAttributes(val, tagObj[":@"], readonlyMatcher, options);
|
|
3244
|
+
} else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) {
|
|
1855
3245
|
val = val[options.textNodeName];
|
|
1856
|
-
}else if(Object.keys(val).length === 0){
|
|
1857
|
-
if(options.alwaysCreateTextNode) val[options.textNodeName] = "";
|
|
3246
|
+
} else if (Object.keys(val).length === 0) {
|
|
3247
|
+
if (options.alwaysCreateTextNode) val[options.textNodeName] = "";
|
|
1858
3248
|
else val = "";
|
|
1859
3249
|
}
|
|
1860
3250
|
|
|
1861
|
-
if(
|
|
1862
|
-
|
|
1863
|
-
|
|
3251
|
+
if (tagObj[METADATA_SYMBOL] !== undefined && typeof val === "object" && val !== null) {
|
|
3252
|
+
val[METADATA_SYMBOL] = tagObj[METADATA_SYMBOL]; // copy over metadata
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
|
|
3256
|
+
if (compressedObj[property] !== undefined && Object.prototype.hasOwnProperty.call(compressedObj, property)) {
|
|
3257
|
+
if (!Array.isArray(compressedObj[property])) {
|
|
3258
|
+
compressedObj[property] = [compressedObj[property]];
|
|
1864
3259
|
}
|
|
1865
3260
|
compressedObj[property].push(val);
|
|
1866
|
-
}else {
|
|
3261
|
+
} else {
|
|
1867
3262
|
//TODO: if a node is not an array, then check if it should be an array
|
|
1868
3263
|
//also determine if it is a leaf node
|
|
1869
|
-
|
|
3264
|
+
|
|
3265
|
+
// Pass jPath string or readonlyMatcher based on options.jPath setting
|
|
3266
|
+
const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher;
|
|
3267
|
+
if (options.isArray(property, jPathOrMatcher, isLeaf)) {
|
|
1870
3268
|
compressedObj[property] = [val];
|
|
1871
|
-
}else {
|
|
3269
|
+
} else {
|
|
1872
3270
|
compressedObj[property] = val;
|
|
1873
3271
|
}
|
|
1874
3272
|
}
|
|
3273
|
+
|
|
3274
|
+
// Pop property from matcher after processing
|
|
3275
|
+
if (property !== undefined && property !== options.textNodeName) {
|
|
3276
|
+
matcher.pop();
|
|
3277
|
+
}
|
|
1875
3278
|
}
|
|
1876
|
-
|
|
3279
|
+
|
|
1877
3280
|
}
|
|
1878
3281
|
// if(text && text.length > 0) compressedObj[options.textNodeName] = text;
|
|
1879
|
-
if(typeof text === "string"){
|
|
1880
|
-
if(text.length > 0) compressedObj[options.textNodeName] = text;
|
|
1881
|
-
}else if(text !== undefined) compressedObj[options.textNodeName] = text;
|
|
3282
|
+
if (typeof text === "string") {
|
|
3283
|
+
if (text.length > 0) compressedObj[options.textNodeName] = text;
|
|
3284
|
+
} else if (text !== undefined) compressedObj[options.textNodeName] = text;
|
|
3285
|
+
|
|
3286
|
+
|
|
1882
3287
|
return compressedObj;
|
|
1883
3288
|
}
|
|
1884
3289
|
|
|
1885
|
-
function propName$1(obj){
|
|
3290
|
+
function propName$1(obj) {
|
|
1886
3291
|
const keys = Object.keys(obj);
|
|
1887
3292
|
for (let i = 0; i < keys.length; i++) {
|
|
1888
3293
|
const key = keys[i];
|
|
1889
|
-
if(key !== ":@") return key;
|
|
3294
|
+
if (key !== ":@") return key;
|
|
1890
3295
|
}
|
|
1891
3296
|
}
|
|
1892
3297
|
|
|
1893
|
-
function assignAttributes(obj, attrMap,
|
|
3298
|
+
function assignAttributes(obj, attrMap, readonlyMatcher, options) {
|
|
1894
3299
|
if (attrMap) {
|
|
1895
3300
|
const keys = Object.keys(attrMap);
|
|
1896
3301
|
const len = keys.length; //don't make it inline
|
|
1897
3302
|
for (let i = 0; i < len; i++) {
|
|
1898
|
-
const atrrName = keys[i];
|
|
1899
|
-
|
|
1900
|
-
|
|
3303
|
+
const atrrName = keys[i]; // This is the PREFIXED name (e.g., "@_class")
|
|
3304
|
+
|
|
3305
|
+
// Strip prefix for matcher path (for isArray callback)
|
|
3306
|
+
const rawAttrName = atrrName.startsWith(options.attributeNamePrefix)
|
|
3307
|
+
? atrrName.substring(options.attributeNamePrefix.length)
|
|
3308
|
+
: atrrName;
|
|
3309
|
+
|
|
3310
|
+
// For attributes, we need to create a temporary path
|
|
3311
|
+
// Pass jPath string or matcher based on options.jPath setting
|
|
3312
|
+
const jPathOrMatcher = options.jPath
|
|
3313
|
+
? readonlyMatcher.toString() + "." + rawAttrName
|
|
3314
|
+
: readonlyMatcher;
|
|
3315
|
+
|
|
3316
|
+
if (options.isArray(atrrName, jPathOrMatcher, true, true)) {
|
|
3317
|
+
obj[atrrName] = [attrMap[atrrName]];
|
|
1901
3318
|
} else {
|
|
1902
3319
|
obj[atrrName] = attrMap[atrrName];
|
|
1903
3320
|
}
|
|
@@ -1905,10 +3322,10 @@ function assignAttributes(obj, attrMap, jpath, options){
|
|
|
1905
3322
|
}
|
|
1906
3323
|
}
|
|
1907
3324
|
|
|
1908
|
-
function isLeafTag(obj, options){
|
|
3325
|
+
function isLeafTag(obj, options) {
|
|
1909
3326
|
const { textNodeName } = options;
|
|
1910
3327
|
const propCount = Object.keys(obj).length;
|
|
1911
|
-
|
|
3328
|
+
|
|
1912
3329
|
if (propCount === 0) {
|
|
1913
3330
|
return true;
|
|
1914
3331
|
}
|
|
@@ -1923,38 +3340,38 @@ function isLeafTag(obj, options){
|
|
|
1923
3340
|
return false;
|
|
1924
3341
|
}
|
|
1925
3342
|
|
|
1926
|
-
class XMLParser{
|
|
1927
|
-
|
|
1928
|
-
constructor(options){
|
|
3343
|
+
class XMLParser {
|
|
3344
|
+
|
|
3345
|
+
constructor(options) {
|
|
1929
3346
|
this.externalEntities = {};
|
|
1930
3347
|
this.options = buildOptions(options);
|
|
1931
|
-
|
|
3348
|
+
|
|
1932
3349
|
}
|
|
1933
3350
|
/**
|
|
1934
3351
|
* Parse XML dats to JS object
|
|
1935
3352
|
* @param {string|Uint8Array} xmlData
|
|
1936
3353
|
* @param {boolean|Object} validationOption
|
|
1937
3354
|
*/
|
|
1938
|
-
parse(xmlData,validationOption){
|
|
1939
|
-
if(typeof xmlData !== "string" && xmlData.toString){
|
|
3355
|
+
parse(xmlData, validationOption) {
|
|
3356
|
+
if (typeof xmlData !== "string" && xmlData.toString) {
|
|
1940
3357
|
xmlData = xmlData.toString();
|
|
1941
|
-
}else if(typeof xmlData !== "string"){
|
|
3358
|
+
} else if (typeof xmlData !== "string") {
|
|
1942
3359
|
throw new Error("XML data is accepted in String or Bytes[] form.")
|
|
1943
3360
|
}
|
|
1944
|
-
|
|
1945
|
-
if(
|
|
1946
|
-
if(validationOption === true) validationOption = {}; //validate with default options
|
|
1947
|
-
|
|
3361
|
+
|
|
3362
|
+
if (validationOption) {
|
|
3363
|
+
if (validationOption === true) validationOption = {}; //validate with default options
|
|
3364
|
+
|
|
1948
3365
|
const result = validate(xmlData, validationOption);
|
|
1949
3366
|
if (result !== true) {
|
|
1950
|
-
|
|
3367
|
+
throw Error(`${result.err.msg}:${result.err.line}:${result.err.col}`)
|
|
1951
3368
|
}
|
|
1952
|
-
|
|
3369
|
+
}
|
|
1953
3370
|
const orderedObjParser = new OrderedObjParser(this.options);
|
|
1954
3371
|
orderedObjParser.addExternalEntities(this.externalEntities);
|
|
1955
3372
|
const orderedResult = orderedObjParser.parseXml(xmlData);
|
|
1956
|
-
if(this.options.preserveOrder || orderedResult === undefined) return orderedResult;
|
|
1957
|
-
else return prettify(orderedResult, this.options);
|
|
3373
|
+
if (this.options.preserveOrder || orderedResult === undefined) return orderedResult;
|
|
3374
|
+
else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher);
|
|
1958
3375
|
}
|
|
1959
3376
|
|
|
1960
3377
|
/**
|
|
@@ -1962,14 +3379,14 @@ class XMLParser{
|
|
|
1962
3379
|
* @param {string} key
|
|
1963
3380
|
* @param {string} value
|
|
1964
3381
|
*/
|
|
1965
|
-
addEntity(key, value){
|
|
1966
|
-
if(value.indexOf("&") !== -1){
|
|
3382
|
+
addEntity(key, value) {
|
|
3383
|
+
if (value.indexOf("&") !== -1) {
|
|
1967
3384
|
throw new Error("Entity value can't have '&'")
|
|
1968
|
-
}else if(key.indexOf("&") !== -1 || key.indexOf(";") !== -1){
|
|
3385
|
+
} else if (key.indexOf("&") !== -1 || key.indexOf(";") !== -1) {
|
|
1969
3386
|
throw new Error("An entity must be set without '&' and ';'. Eg. use '#xD' for '
'")
|
|
1970
|
-
}else if(value === "&"){
|
|
3387
|
+
} else if (value === "&") {
|
|
1971
3388
|
throw new Error("An entity with value '&' is not permitted");
|
|
1972
|
-
}else {
|
|
3389
|
+
} else {
|
|
1973
3390
|
this.externalEntities[key] = value;
|
|
1974
3391
|
}
|
|
1975
3392
|
}
|
|
@@ -2002,25 +3419,61 @@ function toXml(jArray, options) {
|
|
|
2002
3419
|
if (options.format && options.indentBy.length > 0) {
|
|
2003
3420
|
indentation = EOL;
|
|
2004
3421
|
}
|
|
2005
|
-
|
|
3422
|
+
|
|
3423
|
+
// Pre-compile stopNode expressions for pattern matching
|
|
3424
|
+
const stopNodeExpressions = [];
|
|
3425
|
+
if (options.stopNodes && Array.isArray(options.stopNodes)) {
|
|
3426
|
+
for (let i = 0; i < options.stopNodes.length; i++) {
|
|
3427
|
+
const node = options.stopNodes[i];
|
|
3428
|
+
if (typeof node === 'string') {
|
|
3429
|
+
stopNodeExpressions.push(new Expression(node));
|
|
3430
|
+
} else if (node instanceof Expression) {
|
|
3431
|
+
stopNodeExpressions.push(node);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
// Initialize matcher for path tracking
|
|
3437
|
+
const matcher = new Matcher();
|
|
3438
|
+
|
|
3439
|
+
return arrToStr(jArray, options, indentation, matcher, stopNodeExpressions);
|
|
2006
3440
|
}
|
|
2007
3441
|
|
|
2008
|
-
function arrToStr(arr, options,
|
|
3442
|
+
function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) {
|
|
2009
3443
|
let xmlStr = "";
|
|
2010
3444
|
let isPreviousElementTag = false;
|
|
2011
3445
|
|
|
3446
|
+
if (options.maxNestedTags && matcher.getDepth() > options.maxNestedTags) {
|
|
3447
|
+
throw new Error("Maximum nested tags exceeded");
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
if (!Array.isArray(arr)) {
|
|
3451
|
+
// Non-array values (e.g. string tag values) should be treated as text content
|
|
3452
|
+
if (arr !== undefined && arr !== null) {
|
|
3453
|
+
let text = arr.toString();
|
|
3454
|
+
text = replaceEntitiesValue(text, options);
|
|
3455
|
+
return text;
|
|
3456
|
+
}
|
|
3457
|
+
return "";
|
|
3458
|
+
}
|
|
3459
|
+
|
|
2012
3460
|
for (let i = 0; i < arr.length; i++) {
|
|
2013
3461
|
const tagObj = arr[i];
|
|
2014
3462
|
const tagName = propName(tagObj);
|
|
2015
|
-
if(tagName === undefined) continue;
|
|
3463
|
+
if (tagName === undefined) continue;
|
|
2016
3464
|
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
3465
|
+
// Extract attributes from ":@" property
|
|
3466
|
+
const attrValues = extractAttributeValues(tagObj[":@"], options);
|
|
3467
|
+
|
|
3468
|
+
// Push tag to matcher WITH attributes
|
|
3469
|
+
matcher.push(tagName, attrValues);
|
|
3470
|
+
|
|
3471
|
+
// Check if this is a stop node using Expression matching
|
|
3472
|
+
const isStopNode = checkStopNode(matcher, stopNodeExpressions);
|
|
2020
3473
|
|
|
2021
3474
|
if (tagName === options.textNodeName) {
|
|
2022
3475
|
let tagText = tagObj[tagName];
|
|
2023
|
-
if (!isStopNode
|
|
3476
|
+
if (!isStopNode) {
|
|
2024
3477
|
tagText = options.tagValueProcessor(tagName, tagText);
|
|
2025
3478
|
tagText = replaceEntitiesValue(tagText, options);
|
|
2026
3479
|
}
|
|
@@ -2029,6 +3482,7 @@ function arrToStr(arr, options, jPath, indentation) {
|
|
|
2029
3482
|
}
|
|
2030
3483
|
xmlStr += tagText;
|
|
2031
3484
|
isPreviousElementTag = false;
|
|
3485
|
+
matcher.pop();
|
|
2032
3486
|
continue;
|
|
2033
3487
|
} else if (tagName === options.cdataPropName) {
|
|
2034
3488
|
if (isPreviousElementTag) {
|
|
@@ -2036,27 +3490,42 @@ function arrToStr(arr, options, jPath, indentation) {
|
|
|
2036
3490
|
}
|
|
2037
3491
|
xmlStr += `<![CDATA[${tagObj[tagName][0][options.textNodeName]}]]>`;
|
|
2038
3492
|
isPreviousElementTag = false;
|
|
3493
|
+
matcher.pop();
|
|
2039
3494
|
continue;
|
|
2040
3495
|
} else if (tagName === options.commentPropName) {
|
|
2041
3496
|
xmlStr += indentation + `<!--${tagObj[tagName][0][options.textNodeName]}-->`;
|
|
2042
3497
|
isPreviousElementTag = true;
|
|
3498
|
+
matcher.pop();
|
|
2043
3499
|
continue;
|
|
2044
3500
|
} else if (tagName[0] === "?") {
|
|
2045
|
-
const attStr = attr_to_str(tagObj[":@"], options);
|
|
3501
|
+
const attStr = attr_to_str(tagObj[":@"], options, isStopNode);
|
|
2046
3502
|
const tempInd = tagName === "?xml" ? "" : indentation;
|
|
2047
3503
|
let piTextNodeName = tagObj[tagName][0][options.textNodeName];
|
|
2048
3504
|
piTextNodeName = piTextNodeName.length !== 0 ? " " + piTextNodeName : ""; //remove extra spacing
|
|
2049
3505
|
xmlStr += tempInd + `<${tagName}${piTextNodeName}${attStr}?>`;
|
|
2050
3506
|
isPreviousElementTag = true;
|
|
3507
|
+
matcher.pop();
|
|
2051
3508
|
continue;
|
|
2052
3509
|
}
|
|
3510
|
+
|
|
2053
3511
|
let newIdentation = indentation;
|
|
2054
3512
|
if (newIdentation !== "") {
|
|
2055
3513
|
newIdentation += options.indentBy;
|
|
2056
3514
|
}
|
|
2057
|
-
|
|
3515
|
+
|
|
3516
|
+
// Pass isStopNode to attr_to_str so attributes are also not processed for stopNodes
|
|
3517
|
+
const attStr = attr_to_str(tagObj[":@"], options, isStopNode);
|
|
2058
3518
|
const tagStart = indentation + `<${tagName}${attStr}`;
|
|
2059
|
-
|
|
3519
|
+
|
|
3520
|
+
// If this is a stopNode, get raw content without processing
|
|
3521
|
+
let tagValue;
|
|
3522
|
+
if (isStopNode) {
|
|
3523
|
+
tagValue = getRawContent(tagObj[tagName], options);
|
|
3524
|
+
} else {
|
|
3525
|
+
|
|
3526
|
+
tagValue = arrToStr(tagObj[tagName], options, newIdentation, matcher, stopNodeExpressions);
|
|
3527
|
+
}
|
|
3528
|
+
|
|
2060
3529
|
if (options.unpairedTags.indexOf(tagName) !== -1) {
|
|
2061
3530
|
if (options.suppressUnpairedNode) xmlStr += tagStart + ">";
|
|
2062
3531
|
else xmlStr += tagStart + "/>";
|
|
@@ -2074,27 +3543,129 @@ function arrToStr(arr, options, jPath, indentation) {
|
|
|
2074
3543
|
xmlStr += `</${tagName}>`;
|
|
2075
3544
|
}
|
|
2076
3545
|
isPreviousElementTag = true;
|
|
3546
|
+
|
|
3547
|
+
// Pop tag from matcher
|
|
3548
|
+
matcher.pop();
|
|
2077
3549
|
}
|
|
2078
3550
|
|
|
2079
3551
|
return xmlStr;
|
|
2080
3552
|
}
|
|
2081
3553
|
|
|
3554
|
+
/**
|
|
3555
|
+
* Extract attribute values from the ":@" object and return as plain object
|
|
3556
|
+
* for passing to matcher.push()
|
|
3557
|
+
*/
|
|
3558
|
+
function extractAttributeValues(attrMap, options) {
|
|
3559
|
+
if (!attrMap || options.ignoreAttributes) return null;
|
|
3560
|
+
|
|
3561
|
+
const attrValues = {};
|
|
3562
|
+
let hasAttrs = false;
|
|
3563
|
+
|
|
3564
|
+
for (let attr in attrMap) {
|
|
3565
|
+
if (!Object.prototype.hasOwnProperty.call(attrMap, attr)) continue;
|
|
3566
|
+
// Remove the attribute prefix to get clean attribute name
|
|
3567
|
+
const cleanAttrName = attr.startsWith(options.attributeNamePrefix)
|
|
3568
|
+
? attr.substr(options.attributeNamePrefix.length)
|
|
3569
|
+
: attr;
|
|
3570
|
+
attrValues[cleanAttrName] = attrMap[attr];
|
|
3571
|
+
hasAttrs = true;
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
return hasAttrs ? attrValues : null;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
/**
|
|
3578
|
+
* Extract raw content from a stopNode without any processing
|
|
3579
|
+
* This preserves the content exactly as-is, including special characters
|
|
3580
|
+
*/
|
|
3581
|
+
function getRawContent(arr, options) {
|
|
3582
|
+
if (!Array.isArray(arr)) {
|
|
3583
|
+
// Non-array values return as-is
|
|
3584
|
+
if (arr !== undefined && arr !== null) {
|
|
3585
|
+
return arr.toString();
|
|
3586
|
+
}
|
|
3587
|
+
return "";
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
let content = "";
|
|
3591
|
+
for (let i = 0; i < arr.length; i++) {
|
|
3592
|
+
const item = arr[i];
|
|
3593
|
+
const tagName = propName(item);
|
|
3594
|
+
|
|
3595
|
+
if (tagName === options.textNodeName) {
|
|
3596
|
+
// Raw text content - NO processing, NO entity replacement
|
|
3597
|
+
content += item[tagName];
|
|
3598
|
+
} else if (tagName === options.cdataPropName) {
|
|
3599
|
+
// CDATA content
|
|
3600
|
+
content += item[tagName][0][options.textNodeName];
|
|
3601
|
+
} else if (tagName === options.commentPropName) {
|
|
3602
|
+
// Comment content
|
|
3603
|
+
content += item[tagName][0][options.textNodeName];
|
|
3604
|
+
} else if (tagName && tagName[0] === "?") {
|
|
3605
|
+
// Processing instruction - skip for stopNodes
|
|
3606
|
+
continue;
|
|
3607
|
+
} else if (tagName) {
|
|
3608
|
+
// Nested tags within stopNode
|
|
3609
|
+
// Recursively get raw content and reconstruct the tag
|
|
3610
|
+
// For stopNodes, we don't process attributes either
|
|
3611
|
+
const attStr = attr_to_str_raw(item[":@"], options);
|
|
3612
|
+
const nestedContent = getRawContent(item[tagName], options);
|
|
3613
|
+
|
|
3614
|
+
if (!nestedContent || nestedContent.length === 0) {
|
|
3615
|
+
content += `<${tagName}${attStr}/>`;
|
|
3616
|
+
} else {
|
|
3617
|
+
content += `<${tagName}${attStr}>${nestedContent}</${tagName}>`;
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
return content;
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
/**
|
|
3625
|
+
* Build attribute string for stopNodes - NO entity replacement
|
|
3626
|
+
*/
|
|
3627
|
+
function attr_to_str_raw(attrMap, options) {
|
|
3628
|
+
let attrStr = "";
|
|
3629
|
+
if (attrMap && !options.ignoreAttributes) {
|
|
3630
|
+
for (let attr in attrMap) {
|
|
3631
|
+
if (!Object.prototype.hasOwnProperty.call(attrMap, attr)) continue;
|
|
3632
|
+
// For stopNodes, use raw value without processing
|
|
3633
|
+
let attrVal = attrMap[attr];
|
|
3634
|
+
if (attrVal === true && options.suppressBooleanAttributes) {
|
|
3635
|
+
attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}`;
|
|
3636
|
+
} else {
|
|
3637
|
+
attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}="${attrVal}"`;
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
return attrStr;
|
|
3642
|
+
}
|
|
3643
|
+
|
|
2082
3644
|
function propName(obj) {
|
|
2083
3645
|
const keys = Object.keys(obj);
|
|
2084
3646
|
for (let i = 0; i < keys.length; i++) {
|
|
2085
3647
|
const key = keys[i];
|
|
2086
|
-
if(!
|
|
3648
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
2087
3649
|
if (key !== ":@") return key;
|
|
2088
3650
|
}
|
|
2089
3651
|
}
|
|
2090
3652
|
|
|
2091
|
-
function attr_to_str(attrMap, options) {
|
|
3653
|
+
function attr_to_str(attrMap, options, isStopNode) {
|
|
2092
3654
|
let attrStr = "";
|
|
2093
3655
|
if (attrMap && !options.ignoreAttributes) {
|
|
2094
3656
|
for (let attr in attrMap) {
|
|
2095
|
-
if(!
|
|
2096
|
-
let attrVal
|
|
2097
|
-
|
|
3657
|
+
if (!Object.prototype.hasOwnProperty.call(attrMap, attr)) continue;
|
|
3658
|
+
let attrVal;
|
|
3659
|
+
|
|
3660
|
+
if (isStopNode) {
|
|
3661
|
+
// For stopNodes, use raw value without any processing
|
|
3662
|
+
attrVal = attrMap[attr];
|
|
3663
|
+
} else {
|
|
3664
|
+
// Normal processing: apply attributeValueProcessor and entity replacement
|
|
3665
|
+
attrVal = options.attributeValueProcessor(attr, attrMap[attr]);
|
|
3666
|
+
attrVal = replaceEntitiesValue(attrVal, options);
|
|
3667
|
+
}
|
|
3668
|
+
|
|
2098
3669
|
if (attrVal === true && options.suppressBooleanAttributes) {
|
|
2099
3670
|
attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}`;
|
|
2100
3671
|
} else {
|
|
@@ -2105,11 +3676,13 @@ function attr_to_str(attrMap, options) {
|
|
|
2105
3676
|
return attrStr;
|
|
2106
3677
|
}
|
|
2107
3678
|
|
|
2108
|
-
function
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
for (let
|
|
2112
|
-
if (
|
|
3679
|
+
function checkStopNode(matcher, stopNodeExpressions) {
|
|
3680
|
+
if (!stopNodeExpressions || stopNodeExpressions.length === 0) return false;
|
|
3681
|
+
|
|
3682
|
+
for (let i = 0; i < stopNodeExpressions.length; i++) {
|
|
3683
|
+
if (matcher.matches(stopNodeExpressions[i])) {
|
|
3684
|
+
return true;
|
|
3685
|
+
}
|
|
2113
3686
|
}
|
|
2114
3687
|
return false;
|
|
2115
3688
|
}
|
|
@@ -2124,6 +3697,25 @@ function replaceEntitiesValue(textValue, options) {
|
|
|
2124
3697
|
return textValue;
|
|
2125
3698
|
}
|
|
2126
3699
|
|
|
3700
|
+
function getIgnoreAttributesFn(ignoreAttributes) {
|
|
3701
|
+
if (typeof ignoreAttributes === 'function') {
|
|
3702
|
+
return ignoreAttributes
|
|
3703
|
+
}
|
|
3704
|
+
if (Array.isArray(ignoreAttributes)) {
|
|
3705
|
+
return (attrName) => {
|
|
3706
|
+
for (const pattern of ignoreAttributes) {
|
|
3707
|
+
if (typeof pattern === 'string' && attrName === pattern) {
|
|
3708
|
+
return true
|
|
3709
|
+
}
|
|
3710
|
+
if (pattern instanceof RegExp && pattern.test(attrName)) {
|
|
3711
|
+
return true
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
return () => false
|
|
3717
|
+
}
|
|
3718
|
+
|
|
2127
3719
|
const defaultOptions = {
|
|
2128
3720
|
attributeNamePrefix: '@_',
|
|
2129
3721
|
attributesGroupName: false,
|
|
@@ -2135,10 +3727,10 @@ const defaultOptions = {
|
|
|
2135
3727
|
suppressEmptyNode: false,
|
|
2136
3728
|
suppressUnpairedNode: true,
|
|
2137
3729
|
suppressBooleanAttributes: true,
|
|
2138
|
-
tagValueProcessor: function(key, a) {
|
|
3730
|
+
tagValueProcessor: function (key, a) {
|
|
2139
3731
|
return a;
|
|
2140
3732
|
},
|
|
2141
|
-
attributeValueProcessor: function(attrName, a) {
|
|
3733
|
+
attributeValueProcessor: function (attrName, a) {
|
|
2142
3734
|
return a;
|
|
2143
3735
|
},
|
|
2144
3736
|
preserveOrder: false,
|
|
@@ -2155,13 +3747,42 @@ const defaultOptions = {
|
|
|
2155
3747
|
stopNodes: [],
|
|
2156
3748
|
// transformTagName: false,
|
|
2157
3749
|
// transformAttributeName: false,
|
|
2158
|
-
oneListGroup: false
|
|
3750
|
+
oneListGroup: false,
|
|
3751
|
+
maxNestedTags: 100,
|
|
3752
|
+
jPath: true // When true, callbacks receive string jPath; when false, receive Matcher instance
|
|
2159
3753
|
};
|
|
2160
3754
|
|
|
2161
3755
|
function Builder(options) {
|
|
2162
3756
|
this.options = Object.assign({}, defaultOptions, options);
|
|
3757
|
+
|
|
3758
|
+
// Convert old-style stopNodes for backward compatibility
|
|
3759
|
+
// Old syntax: "*.tag" meant "tag anywhere in tree"
|
|
3760
|
+
// New syntax: "..tag" means "tag anywhere in tree"
|
|
3761
|
+
if (this.options.stopNodes && Array.isArray(this.options.stopNodes)) {
|
|
3762
|
+
this.options.stopNodes = this.options.stopNodes.map(node => {
|
|
3763
|
+
if (typeof node === 'string' && node.startsWith('*.')) {
|
|
3764
|
+
// Convert old wildcard syntax to deep wildcard
|
|
3765
|
+
return '..' + node.substring(2);
|
|
3766
|
+
}
|
|
3767
|
+
return node;
|
|
3768
|
+
});
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
// Pre-compile stopNode expressions for pattern matching
|
|
3772
|
+
this.stopNodeExpressions = [];
|
|
3773
|
+
if (this.options.stopNodes && Array.isArray(this.options.stopNodes)) {
|
|
3774
|
+
for (let i = 0; i < this.options.stopNodes.length; i++) {
|
|
3775
|
+
const node = this.options.stopNodes[i];
|
|
3776
|
+
if (typeof node === 'string') {
|
|
3777
|
+
this.stopNodeExpressions.push(new Expression(node));
|
|
3778
|
+
} else if (node instanceof Expression) {
|
|
3779
|
+
this.stopNodeExpressions.push(node);
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
|
|
2163
3784
|
if (this.options.ignoreAttributes === true || this.options.attributesGroupName) {
|
|
2164
|
-
this.isAttribute = function(/*a*/) {
|
|
3785
|
+
this.isAttribute = function (/*a*/) {
|
|
2165
3786
|
return false;
|
|
2166
3787
|
};
|
|
2167
3788
|
} else {
|
|
@@ -2177,7 +3798,7 @@ function Builder(options) {
|
|
|
2177
3798
|
this.tagEndChar = '>\n';
|
|
2178
3799
|
this.newLine = '\n';
|
|
2179
3800
|
} else {
|
|
2180
|
-
this.indentate = function() {
|
|
3801
|
+
this.indentate = function () {
|
|
2181
3802
|
return '';
|
|
2182
3803
|
};
|
|
2183
3804
|
this.tagEndChar = '>';
|
|
@@ -2185,25 +3806,35 @@ function Builder(options) {
|
|
|
2185
3806
|
}
|
|
2186
3807
|
}
|
|
2187
3808
|
|
|
2188
|
-
Builder.prototype.build = function(jObj) {
|
|
2189
|
-
if(this.options.preserveOrder){
|
|
3809
|
+
Builder.prototype.build = function (jObj) {
|
|
3810
|
+
if (this.options.preserveOrder) {
|
|
2190
3811
|
return toXml(jObj, this.options);
|
|
2191
|
-
}else {
|
|
2192
|
-
if(Array.isArray(jObj) && this.options.arrayNodeName && this.options.arrayNodeName.length > 1){
|
|
3812
|
+
} else {
|
|
3813
|
+
if (Array.isArray(jObj) && this.options.arrayNodeName && this.options.arrayNodeName.length > 1) {
|
|
2193
3814
|
jObj = {
|
|
2194
|
-
[this.options.arrayNodeName]
|
|
3815
|
+
[this.options.arrayNodeName]: jObj
|
|
2195
3816
|
};
|
|
2196
3817
|
}
|
|
2197
|
-
|
|
3818
|
+
// Initialize matcher for path tracking
|
|
3819
|
+
const matcher = new Matcher();
|
|
3820
|
+
return this.j2x(jObj, 0, matcher).val;
|
|
2198
3821
|
}
|
|
2199
3822
|
};
|
|
2200
3823
|
|
|
2201
|
-
Builder.prototype.j2x = function(jObj, level,
|
|
3824
|
+
Builder.prototype.j2x = function (jObj, level, matcher) {
|
|
2202
3825
|
let attrStr = '';
|
|
2203
3826
|
let val = '';
|
|
2204
|
-
|
|
3827
|
+
if (this.options.maxNestedTags && matcher.getDepth() >= this.options.maxNestedTags) {
|
|
3828
|
+
throw new Error("Maximum nested tags exceeded");
|
|
3829
|
+
}
|
|
3830
|
+
// Get jPath based on option: string for backward compatibility, or Matcher for new features
|
|
3831
|
+
const jPath = this.options.jPath ? matcher.toString() : matcher;
|
|
3832
|
+
|
|
3833
|
+
// Check if current node is a stopNode (will be used for attribute encoding)
|
|
3834
|
+
const isCurrentStopNode = this.checkStopNode(matcher);
|
|
3835
|
+
|
|
2205
3836
|
for (let key in jObj) {
|
|
2206
|
-
if(!Object.prototype.hasOwnProperty.call(jObj, key)) continue;
|
|
3837
|
+
if (!Object.prototype.hasOwnProperty.call(jObj, key)) continue;
|
|
2207
3838
|
if (typeof jObj[key] === 'undefined') {
|
|
2208
3839
|
// supress undefined node only if it is not an attribute
|
|
2209
3840
|
if (this.isAttribute(key)) {
|
|
@@ -2222,19 +3853,34 @@ Builder.prototype.j2x = function(jObj, level, ajPath) {
|
|
|
2222
3853
|
}
|
|
2223
3854
|
// val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
|
|
2224
3855
|
} else if (jObj[key] instanceof Date) {
|
|
2225
|
-
val += this.buildTextValNode(jObj[key], key, '', level);
|
|
3856
|
+
val += this.buildTextValNode(jObj[key], key, '', level, matcher);
|
|
2226
3857
|
} else if (typeof jObj[key] !== 'object') {
|
|
2227
3858
|
//premitive type
|
|
2228
3859
|
const attr = this.isAttribute(key);
|
|
2229
3860
|
if (attr && !this.ignoreAttributesFn(attr, jPath)) {
|
|
2230
|
-
attrStr += this.buildAttrPairStr(attr, '' + jObj[key]);
|
|
3861
|
+
attrStr += this.buildAttrPairStr(attr, '' + jObj[key], isCurrentStopNode);
|
|
2231
3862
|
} else if (!attr) {
|
|
2232
3863
|
//tag value
|
|
2233
3864
|
if (key === this.options.textNodeName) {
|
|
2234
3865
|
let newval = this.options.tagValueProcessor(key, '' + jObj[key]);
|
|
2235
3866
|
val += this.replaceEntitiesValue(newval);
|
|
2236
3867
|
} else {
|
|
2237
|
-
|
|
3868
|
+
// Check if this is a stopNode before building
|
|
3869
|
+
matcher.push(key);
|
|
3870
|
+
const isStopNode = this.checkStopNode(matcher);
|
|
3871
|
+
matcher.pop();
|
|
3872
|
+
|
|
3873
|
+
if (isStopNode) {
|
|
3874
|
+
// Build as raw content without encoding
|
|
3875
|
+
const textValue = '' + jObj[key];
|
|
3876
|
+
if (textValue === '') {
|
|
3877
|
+
val += this.indentate(level) + '<' + key + this.closeTag(key) + this.tagEndChar;
|
|
3878
|
+
} else {
|
|
3879
|
+
val += this.indentate(level) + '<' + key + '>' + textValue + '</' + key + this.tagEndChar;
|
|
3880
|
+
}
|
|
3881
|
+
} else {
|
|
3882
|
+
val += this.buildTextValNode(jObj[key], key, '', level, matcher);
|
|
3883
|
+
}
|
|
2238
3884
|
}
|
|
2239
3885
|
}
|
|
2240
3886
|
} else if (Array.isArray(jObj[key])) {
|
|
@@ -2245,18 +3891,23 @@ Builder.prototype.j2x = function(jObj, level, ajPath) {
|
|
|
2245
3891
|
for (let j = 0; j < arrLen; j++) {
|
|
2246
3892
|
const item = jObj[key][j];
|
|
2247
3893
|
if (typeof item === 'undefined') ; else if (item === null) {
|
|
2248
|
-
if(key[0] === "?") val += this.indentate(level) + '<' + key + '?' + this.tagEndChar;
|
|
3894
|
+
if (key[0] === "?") val += this.indentate(level) + '<' + key + '?' + this.tagEndChar;
|
|
2249
3895
|
else val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
|
|
2250
3896
|
// val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
|
|
2251
3897
|
} else if (typeof item === 'object') {
|
|
2252
|
-
if(this.options.oneListGroup){
|
|
2253
|
-
|
|
3898
|
+
if (this.options.oneListGroup) {
|
|
3899
|
+
// Push tag to matcher before recursive call
|
|
3900
|
+
matcher.push(key);
|
|
3901
|
+
const result = this.j2x(item, level + 1, matcher);
|
|
3902
|
+
// Pop tag from matcher after recursive call
|
|
3903
|
+
matcher.pop();
|
|
3904
|
+
|
|
2254
3905
|
listTagVal += result.val;
|
|
2255
3906
|
if (this.options.attributesGroupName && item.hasOwnProperty(this.options.attributesGroupName)) {
|
|
2256
3907
|
listTagAttr += result.attrStr;
|
|
2257
3908
|
}
|
|
2258
|
-
}else {
|
|
2259
|
-
listTagVal += this.processTextOrObjNode(item, key, level,
|
|
3909
|
+
} else {
|
|
3910
|
+
listTagVal += this.processTextOrObjNode(item, key, level, matcher);
|
|
2260
3911
|
}
|
|
2261
3912
|
} else {
|
|
2262
3913
|
if (this.options.oneListGroup) {
|
|
@@ -2264,11 +3915,26 @@ Builder.prototype.j2x = function(jObj, level, ajPath) {
|
|
|
2264
3915
|
textValue = this.replaceEntitiesValue(textValue);
|
|
2265
3916
|
listTagVal += textValue;
|
|
2266
3917
|
} else {
|
|
2267
|
-
|
|
3918
|
+
// Check if this is a stopNode before building
|
|
3919
|
+
matcher.push(key);
|
|
3920
|
+
const isStopNode = this.checkStopNode(matcher);
|
|
3921
|
+
matcher.pop();
|
|
3922
|
+
|
|
3923
|
+
if (isStopNode) {
|
|
3924
|
+
// Build as raw content without encoding
|
|
3925
|
+
const textValue = '' + item;
|
|
3926
|
+
if (textValue === '') {
|
|
3927
|
+
listTagVal += this.indentate(level) + '<' + key + this.closeTag(key) + this.tagEndChar;
|
|
3928
|
+
} else {
|
|
3929
|
+
listTagVal += this.indentate(level) + '<' + key + '>' + textValue + '</' + key + this.tagEndChar;
|
|
3930
|
+
}
|
|
3931
|
+
} else {
|
|
3932
|
+
listTagVal += this.buildTextValNode(item, key, '', level, matcher);
|
|
3933
|
+
}
|
|
2268
3934
|
}
|
|
2269
3935
|
}
|
|
2270
3936
|
}
|
|
2271
|
-
if(this.options.oneListGroup){
|
|
3937
|
+
if (this.options.oneListGroup) {
|
|
2272
3938
|
listTagVal = this.buildObjectNode(listTagVal, key, listTagAttr, level);
|
|
2273
3939
|
}
|
|
2274
3940
|
val += listTagVal;
|
|
@@ -2278,99 +3944,269 @@ Builder.prototype.j2x = function(jObj, level, ajPath) {
|
|
|
2278
3944
|
const Ks = Object.keys(jObj[key]);
|
|
2279
3945
|
const L = Ks.length;
|
|
2280
3946
|
for (let j = 0; j < L; j++) {
|
|
2281
|
-
attrStr += this.buildAttrPairStr(Ks[j], '' + jObj[key][Ks[j]]);
|
|
3947
|
+
attrStr += this.buildAttrPairStr(Ks[j], '' + jObj[key][Ks[j]], isCurrentStopNode);
|
|
2282
3948
|
}
|
|
2283
3949
|
} else {
|
|
2284
|
-
val += this.processTextOrObjNode(jObj[key], key, level,
|
|
3950
|
+
val += this.processTextOrObjNode(jObj[key], key, level, matcher);
|
|
2285
3951
|
}
|
|
2286
3952
|
}
|
|
2287
3953
|
}
|
|
2288
|
-
return {attrStr: attrStr, val: val};
|
|
3954
|
+
return { attrStr: attrStr, val: val };
|
|
2289
3955
|
};
|
|
2290
3956
|
|
|
2291
|
-
Builder.prototype.buildAttrPairStr = function(attrName, val){
|
|
2292
|
-
|
|
2293
|
-
|
|
3957
|
+
Builder.prototype.buildAttrPairStr = function (attrName, val, isStopNode) {
|
|
3958
|
+
if (!isStopNode) {
|
|
3959
|
+
val = this.options.attributeValueProcessor(attrName, '' + val);
|
|
3960
|
+
val = this.replaceEntitiesValue(val);
|
|
3961
|
+
}
|
|
2294
3962
|
if (this.options.suppressBooleanAttributes && val === "true") {
|
|
2295
3963
|
return ' ' + attrName;
|
|
2296
3964
|
} else return ' ' + attrName + '="' + val + '"';
|
|
2297
3965
|
};
|
|
2298
3966
|
|
|
2299
|
-
function processTextOrObjNode
|
|
2300
|
-
|
|
3967
|
+
function processTextOrObjNode(object, key, level, matcher) {
|
|
3968
|
+
// Extract attributes to pass to matcher
|
|
3969
|
+
const attrValues = this.extractAttributes(object);
|
|
3970
|
+
|
|
3971
|
+
// Push tag to matcher before recursion WITH attributes
|
|
3972
|
+
matcher.push(key, attrValues);
|
|
3973
|
+
|
|
3974
|
+
// Check if this entire node is a stopNode
|
|
3975
|
+
const isStopNode = this.checkStopNode(matcher);
|
|
3976
|
+
|
|
3977
|
+
if (isStopNode) {
|
|
3978
|
+
// For stopNodes, build raw content without entity encoding
|
|
3979
|
+
const rawContent = this.buildRawContent(object);
|
|
3980
|
+
const attrStr = this.buildAttributesForStopNode(object);
|
|
3981
|
+
matcher.pop();
|
|
3982
|
+
return this.buildObjectNode(rawContent, key, attrStr, level);
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
const result = this.j2x(object, level + 1, matcher);
|
|
3986
|
+
// Pop tag from matcher after recursion
|
|
3987
|
+
matcher.pop();
|
|
3988
|
+
|
|
2301
3989
|
if (object[this.options.textNodeName] !== undefined && Object.keys(object).length === 1) {
|
|
2302
|
-
return this.buildTextValNode(object[this.options.textNodeName], key, result.attrStr, level);
|
|
3990
|
+
return this.buildTextValNode(object[this.options.textNodeName], key, result.attrStr, level, matcher);
|
|
2303
3991
|
} else {
|
|
2304
3992
|
return this.buildObjectNode(result.val, key, result.attrStr, level);
|
|
2305
3993
|
}
|
|
2306
3994
|
}
|
|
2307
3995
|
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
3996
|
+
// Helper method to extract attributes from an object
|
|
3997
|
+
Builder.prototype.extractAttributes = function (obj) {
|
|
3998
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
3999
|
+
|
|
4000
|
+
const attrValues = {};
|
|
4001
|
+
let hasAttrs = false;
|
|
4002
|
+
|
|
4003
|
+
// Check for attributesGroupName (when attributes are grouped)
|
|
4004
|
+
if (this.options.attributesGroupName && obj[this.options.attributesGroupName]) {
|
|
4005
|
+
const attrGroup = obj[this.options.attributesGroupName];
|
|
4006
|
+
for (let attrKey in attrGroup) {
|
|
4007
|
+
if (!Object.prototype.hasOwnProperty.call(attrGroup, attrKey)) continue;
|
|
4008
|
+
// Remove attribute prefix if present
|
|
4009
|
+
const cleanKey = attrKey.startsWith(this.options.attributeNamePrefix)
|
|
4010
|
+
? attrKey.substring(this.options.attributeNamePrefix.length)
|
|
4011
|
+
: attrKey;
|
|
4012
|
+
attrValues[cleanKey] = attrGroup[attrKey];
|
|
4013
|
+
hasAttrs = true;
|
|
4014
|
+
}
|
|
4015
|
+
} else {
|
|
4016
|
+
// Look for individual attributes (prefixed with attributeNamePrefix)
|
|
4017
|
+
for (let key in obj) {
|
|
4018
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
4019
|
+
const attr = this.isAttribute(key);
|
|
4020
|
+
if (attr) {
|
|
4021
|
+
attrValues[attr] = obj[key];
|
|
4022
|
+
hasAttrs = true;
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
return hasAttrs ? attrValues : null;
|
|
4028
|
+
};
|
|
4029
|
+
|
|
4030
|
+
// Build raw content for stopNode without entity encoding
|
|
4031
|
+
Builder.prototype.buildRawContent = function (obj) {
|
|
4032
|
+
if (typeof obj === 'string') {
|
|
4033
|
+
return obj; // Already a string, return as-is
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
4037
|
+
return String(obj);
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
// Check if this is a stopNode data from parser: { "#text": "raw xml", "@_attr": "val" }
|
|
4041
|
+
if (obj[this.options.textNodeName] !== undefined) {
|
|
4042
|
+
return obj[this.options.textNodeName]; // Return raw text without encoding
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
// Build raw XML from nested structure
|
|
4046
|
+
let content = '';
|
|
4047
|
+
|
|
4048
|
+
for (let key in obj) {
|
|
4049
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
4050
|
+
|
|
4051
|
+
// Skip attributes
|
|
4052
|
+
if (this.isAttribute(key)) continue;
|
|
4053
|
+
if (this.options.attributesGroupName && key === this.options.attributesGroupName) continue;
|
|
4054
|
+
|
|
4055
|
+
const value = obj[key];
|
|
4056
|
+
|
|
4057
|
+
if (key === this.options.textNodeName) {
|
|
4058
|
+
content += value; // Raw text
|
|
4059
|
+
} else if (Array.isArray(value)) {
|
|
4060
|
+
// Array of same tag
|
|
4061
|
+
for (let item of value) {
|
|
4062
|
+
if (typeof item === 'string' || typeof item === 'number') {
|
|
4063
|
+
content += `<${key}>${item}</${key}>`;
|
|
4064
|
+
} else if (typeof item === 'object' && item !== null) {
|
|
4065
|
+
const nestedContent = this.buildRawContent(item);
|
|
4066
|
+
const nestedAttrs = this.buildAttributesForStopNode(item);
|
|
4067
|
+
if (nestedContent === '') {
|
|
4068
|
+
content += `<${key}${nestedAttrs}/>`;
|
|
4069
|
+
} else {
|
|
4070
|
+
content += `<${key}${nestedAttrs}>${nestedContent}</${key}>`;
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
4075
|
+
// Nested object
|
|
4076
|
+
const nestedContent = this.buildRawContent(value);
|
|
4077
|
+
const nestedAttrs = this.buildAttributesForStopNode(value);
|
|
4078
|
+
if (nestedContent === '') {
|
|
4079
|
+
content += `<${key}${nestedAttrs}/>`;
|
|
4080
|
+
} else {
|
|
4081
|
+
content += `<${key}${nestedAttrs}>${nestedContent}</${key}>`;
|
|
4082
|
+
}
|
|
4083
|
+
} else {
|
|
4084
|
+
// Primitive value
|
|
4085
|
+
content += `<${key}>${value}</${key}>`;
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
return content;
|
|
4090
|
+
};
|
|
4091
|
+
|
|
4092
|
+
// Build attribute string for stopNode (no entity encoding)
|
|
4093
|
+
Builder.prototype.buildAttributesForStopNode = function (obj) {
|
|
4094
|
+
if (!obj || typeof obj !== 'object') return '';
|
|
4095
|
+
|
|
4096
|
+
let attrStr = '';
|
|
4097
|
+
|
|
4098
|
+
// Check for attributesGroupName (when attributes are grouped)
|
|
4099
|
+
if (this.options.attributesGroupName && obj[this.options.attributesGroupName]) {
|
|
4100
|
+
const attrGroup = obj[this.options.attributesGroupName];
|
|
4101
|
+
for (let attrKey in attrGroup) {
|
|
4102
|
+
if (!Object.prototype.hasOwnProperty.call(attrGroup, attrKey)) continue;
|
|
4103
|
+
const cleanKey = attrKey.startsWith(this.options.attributeNamePrefix)
|
|
4104
|
+
? attrKey.substring(this.options.attributeNamePrefix.length)
|
|
4105
|
+
: attrKey;
|
|
4106
|
+
const val = attrGroup[attrKey];
|
|
4107
|
+
if (val === true && this.options.suppressBooleanAttributes) {
|
|
4108
|
+
attrStr += ' ' + cleanKey;
|
|
4109
|
+
} else {
|
|
4110
|
+
attrStr += ' ' + cleanKey + '="' + val + '"'; // No encoding for stopNode
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
} else {
|
|
4114
|
+
// Look for individual attributes
|
|
4115
|
+
for (let key in obj) {
|
|
4116
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
4117
|
+
const attr = this.isAttribute(key);
|
|
4118
|
+
if (attr) {
|
|
4119
|
+
const val = obj[key];
|
|
4120
|
+
if (val === true && this.options.suppressBooleanAttributes) {
|
|
4121
|
+
attrStr += ' ' + attr;
|
|
4122
|
+
} else {
|
|
4123
|
+
attrStr += ' ' + attr + '="' + val + '"'; // No encoding for stopNode
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
return attrStr;
|
|
4130
|
+
};
|
|
4131
|
+
|
|
4132
|
+
Builder.prototype.buildObjectNode = function (val, key, attrStr, level) {
|
|
4133
|
+
if (val === "") {
|
|
4134
|
+
if (key[0] === "?") return this.indentate(level) + '<' + key + attrStr + '?' + this.tagEndChar;
|
|
2311
4135
|
else {
|
|
2312
4136
|
return this.indentate(level) + '<' + key + attrStr + this.closeTag(key) + this.tagEndChar;
|
|
2313
4137
|
}
|
|
2314
|
-
}else {
|
|
4138
|
+
} else {
|
|
2315
4139
|
|
|
2316
4140
|
let tagEndExp = '</' + key + this.tagEndChar;
|
|
2317
4141
|
let piClosingChar = "";
|
|
2318
|
-
|
|
2319
|
-
if(key[0] === "?") {
|
|
4142
|
+
|
|
4143
|
+
if (key[0] === "?") {
|
|
2320
4144
|
piClosingChar = "?";
|
|
2321
4145
|
tagEndExp = "";
|
|
2322
4146
|
}
|
|
2323
|
-
|
|
4147
|
+
|
|
2324
4148
|
// attrStr is an empty string in case the attribute came as undefined or null
|
|
2325
4149
|
if ((attrStr || attrStr === '') && val.indexOf('<') === -1) {
|
|
2326
|
-
return (
|
|
4150
|
+
return (this.indentate(level) + '<' + key + attrStr + piClosingChar + '>' + val + tagEndExp);
|
|
2327
4151
|
} else if (this.options.commentPropName !== false && key === this.options.commentPropName && piClosingChar.length === 0) {
|
|
2328
4152
|
return this.indentate(level) + `<!--${val}-->` + this.newLine;
|
|
2329
|
-
}else {
|
|
4153
|
+
} else {
|
|
2330
4154
|
return (
|
|
2331
4155
|
this.indentate(level) + '<' + key + attrStr + piClosingChar + this.tagEndChar +
|
|
2332
4156
|
val +
|
|
2333
|
-
this.indentate(level) + tagEndExp
|
|
4157
|
+
this.indentate(level) + tagEndExp);
|
|
2334
4158
|
}
|
|
2335
4159
|
}
|
|
2336
4160
|
};
|
|
2337
4161
|
|
|
2338
|
-
Builder.prototype.closeTag = function(key){
|
|
4162
|
+
Builder.prototype.closeTag = function (key) {
|
|
2339
4163
|
let closeTag = "";
|
|
2340
|
-
if(this.options.unpairedTags.indexOf(key) !== -1){ //unpaired
|
|
2341
|
-
if(!this.options.suppressUnpairedNode) closeTag = "/";
|
|
2342
|
-
}else if(this.options.suppressEmptyNode){ //empty
|
|
4164
|
+
if (this.options.unpairedTags.indexOf(key) !== -1) { //unpaired
|
|
4165
|
+
if (!this.options.suppressUnpairedNode) closeTag = "/";
|
|
4166
|
+
} else if (this.options.suppressEmptyNode) { //empty
|
|
2343
4167
|
closeTag = "/";
|
|
2344
|
-
}else {
|
|
4168
|
+
} else {
|
|
2345
4169
|
closeTag = `></${key}`;
|
|
2346
4170
|
}
|
|
2347
4171
|
return closeTag;
|
|
2348
4172
|
};
|
|
2349
4173
|
|
|
2350
|
-
Builder.prototype.
|
|
4174
|
+
Builder.prototype.checkStopNode = function (matcher) {
|
|
4175
|
+
if (!this.stopNodeExpressions || this.stopNodeExpressions.length === 0) return false;
|
|
4176
|
+
|
|
4177
|
+
for (let i = 0; i < this.stopNodeExpressions.length; i++) {
|
|
4178
|
+
if (matcher.matches(this.stopNodeExpressions[i])) {
|
|
4179
|
+
return true;
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
return false;
|
|
4183
|
+
};
|
|
4184
|
+
|
|
4185
|
+
Builder.prototype.buildTextValNode = function (val, key, attrStr, level, matcher) {
|
|
2351
4186
|
if (this.options.cdataPropName !== false && key === this.options.cdataPropName) {
|
|
2352
|
-
return this.indentate(level) + `<![CDATA[${val}]]>` +
|
|
2353
|
-
}else if (this.options.commentPropName !== false && key === this.options.commentPropName) {
|
|
2354
|
-
return this.indentate(level) + `<!--${val}-->` +
|
|
2355
|
-
}else if(key[0] === "?") {//PI tag
|
|
2356
|
-
return
|
|
2357
|
-
}else {
|
|
4187
|
+
return this.indentate(level) + `<![CDATA[${val}]]>` + this.newLine;
|
|
4188
|
+
} else if (this.options.commentPropName !== false && key === this.options.commentPropName) {
|
|
4189
|
+
return this.indentate(level) + `<!--${val}-->` + this.newLine;
|
|
4190
|
+
} else if (key[0] === "?") {//PI tag
|
|
4191
|
+
return this.indentate(level) + '<' + key + attrStr + '?' + this.tagEndChar;
|
|
4192
|
+
} else {
|
|
4193
|
+
// Normal processing: apply tagValueProcessor and entity replacement
|
|
2358
4194
|
let textValue = this.options.tagValueProcessor(key, val);
|
|
2359
4195
|
textValue = this.replaceEntitiesValue(textValue);
|
|
2360
|
-
|
|
2361
|
-
if(
|
|
4196
|
+
|
|
4197
|
+
if (textValue === '') {
|
|
2362
4198
|
return this.indentate(level) + '<' + key + attrStr + this.closeTag(key) + this.tagEndChar;
|
|
2363
|
-
}else {
|
|
4199
|
+
} else {
|
|
2364
4200
|
return this.indentate(level) + '<' + key + attrStr + '>' +
|
|
2365
|
-
|
|
4201
|
+
textValue +
|
|
2366
4202
|
'</' + key + this.tagEndChar;
|
|
2367
4203
|
}
|
|
2368
4204
|
}
|
|
2369
4205
|
};
|
|
2370
4206
|
|
|
2371
|
-
Builder.prototype.replaceEntitiesValue = function(textValue){
|
|
2372
|
-
if(textValue && textValue.length > 0 && this.options.processEntities){
|
|
2373
|
-
for (let i=0; i<this.options.entities.length; i++) {
|
|
4207
|
+
Builder.prototype.replaceEntitiesValue = function (textValue) {
|
|
4208
|
+
if (textValue && textValue.length > 0 && this.options.processEntities) {
|
|
4209
|
+
for (let i = 0; i < this.options.entities.length; i++) {
|
|
2374
4210
|
const entity = this.options.entities[i];
|
|
2375
4211
|
textValue = textValue.replace(entity.regex, entity.val);
|
|
2376
4212
|
}
|
|
@@ -2396,10 +4232,7 @@ const ISO20022Messages = {
|
|
|
2396
4232
|
CAMT_005: 'CAMT.005',
|
|
2397
4233
|
CAMT_006: 'CAMT.006',
|
|
2398
4234
|
CAMT_052: 'CAMT.052',
|
|
2399
|
-
CAMT_053: 'CAMT.053'
|
|
2400
|
-
PAIN_001: 'PAIN.001',
|
|
2401
|
-
PAIN_002: 'PAIN.002',
|
|
2402
|
-
};
|
|
4235
|
+
CAMT_053: 'CAMT.053'};
|
|
2403
4236
|
const ISO20022Implementations = new Map();
|
|
2404
4237
|
function registerISO20022Implementation(cl) {
|
|
2405
4238
|
cl.supportedMessages().forEach(msg => {
|
|
@@ -7492,15 +9325,15 @@ const exportAccountIdentification = (accountId) => {
|
|
|
7492
9325
|
};
|
|
7493
9326
|
// TODO: Add both BIC and ABA routing numbers at the same time
|
|
7494
9327
|
const parseAgent = (agent) => {
|
|
7495
|
-
|
|
7496
|
-
if (
|
|
7497
|
-
return {
|
|
7498
|
-
bic: agent.FinInstnId.BIC,
|
|
7499
|
-
};
|
|
9328
|
+
const bic = agent.FinInstnId.BICFI || agent.FinInstnId.BIC;
|
|
9329
|
+
if (bic) {
|
|
9330
|
+
return { bic };
|
|
7500
9331
|
}
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
9332
|
+
const aba = agent.FinInstnId.Othr?.Id || agent.FinInstnId.ClrSysMmbId?.MmbId;
|
|
9333
|
+
if (aba != null) {
|
|
9334
|
+
return { abaRoutingNumber: String(aba) };
|
|
9335
|
+
}
|
|
9336
|
+
throw new Error('Unable to parse agent: no BIC, BICFI, Othr.Id, or ClrSysMmbId.MmbId present');
|
|
7504
9337
|
};
|
|
7505
9338
|
const exportAgent = (agent) => {
|
|
7506
9339
|
const obj = {
|
|
@@ -7643,7 +9476,7 @@ var native = {
|
|
|
7643
9476
|
};
|
|
7644
9477
|
|
|
7645
9478
|
function v4(options, buf, offset) {
|
|
7646
|
-
if (native.randomUUID &&
|
|
9479
|
+
if (native.randomUUID && true && !options) {
|
|
7647
9480
|
return native.randomUUID();
|
|
7648
9481
|
}
|
|
7649
9482
|
options = options || {};
|