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