fast-xml-parser 4.5.3 → 4.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
 
4
4
  const util = require('../util');
5
5
  const xmlNode = require('./xmlNode');
6
- const readDocType = require("./DocTypeReader");
6
+ const DocTypeReader = require('./DocTypeReader');
7
7
  const toNumber = require("strnum");
8
8
  const getIgnoreAttributesFn = require('../ignoreAttributes')
9
9
 
@@ -14,19 +14,19 @@ const getIgnoreAttributesFn = require('../ignoreAttributes')
14
14
  //const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g");
15
15
  //const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g");
16
16
 
17
- class OrderedObjParser{
18
- constructor(options){
17
+ class OrderedObjParser {
18
+ constructor(options) {
19
19
  this.options = options;
20
20
  this.currentNode = null;
21
21
  this.tagsNodeStack = [];
22
22
  this.docTypeEntities = {};
23
23
  this.lastEntities = {
24
- "apos" : { regex: /&(apos|#39|#x27);/g, val : "'"},
25
- "gt" : { regex: /&(gt|#62|#x3E);/g, val : ">"},
26
- "lt" : { regex: /&(lt|#60|#x3C);/g, val : "<"},
27
- "quot" : { regex: /&(quot|#34|#x22);/g, val : "\""},
24
+ "apos": { regex: /&(apos|#39|#x27);/g, val: "'" },
25
+ "gt": { regex: /&(gt|#62|#x3E);/g, val: ">" },
26
+ "lt": { regex: /&(lt|#60|#x3C);/g, val: "<" },
27
+ "quot": { regex: /&(quot|#34|#x22);/g, val: "\"" },
28
28
  };
29
- this.ampEntity = { regex: /&(amp|#38|#x26);/g, val : "&"};
29
+ this.ampEntity = { regex: /&(amp|#38|#x26);/g, val: "&" };
30
30
  this.htmlEntities = {
31
31
  "space": { regex: /&(nbsp|#160);/g, val: " " },
32
32
  // "lt" : { regex: /&(lt|#60);/g, val: "<" },
@@ -34,15 +34,15 @@ class OrderedObjParser{
34
34
  // "amp" : { regex: /&(amp|#38);/g, val: "&" },
35
35
  // "quot" : { regex: /&(quot|#34);/g, val: "\"" },
36
36
  // "apos" : { regex: /&(apos|#39);/g, val: "'" },
37
- "cent" : { regex: /&(cent|#162);/g, val: "¢" },
38
- "pound" : { regex: /&(pound|#163);/g, val: "£" },
39
- "yen" : { regex: /&(yen|#165);/g, val: "¥" },
40
- "euro" : { regex: /&(euro|#8364);/g, val: "€" },
41
- "copyright" : { regex: /&(copy|#169);/g, val: "©" },
42
- "reg" : { regex: /&(reg|#174);/g, val: "®" },
43
- "inr" : { regex: /&(inr|#8377);/g, val: "₹" },
44
- "num_dec": { regex: /&#([0-9]{1,7});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 10)) },
45
- "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 16)) },
37
+ "cent": { regex: /&(cent|#162);/g, val: "¢" },
38
+ "pound": { regex: /&(pound|#163);/g, val: "£" },
39
+ "yen": { regex: /&(yen|#165);/g, val: "¥" },
40
+ "euro": { regex: /&(euro|#8364);/g, val: "€" },
41
+ "copyright": { regex: /&(copy|#169);/g, val: "©" },
42
+ "reg": { regex: /&(reg|#174);/g, val: "®" },
43
+ "inr": { regex: /&(inr|#8377);/g, val: "₹" },
44
+ "num_dec": { regex: /&#([0-9]{1,7});/g, val: (_, str) => fromCodePoint(str, 10, "&#") },
45
+ "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => fromCodePoint(str, 16, "&#x") },
46
46
  };
47
47
  this.addExternalEntities = addExternalEntities;
48
48
  this.parseXml = parseXml;
@@ -55,17 +55,34 @@ class OrderedObjParser{
55
55
  this.saveTextToParentTag = saveTextToParentTag;
56
56
  this.addChild = addChild;
57
57
  this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes)
58
+ this.entityExpansionCount = 0;
59
+ this.currentExpandedLength = 0;
60
+
61
+ if (this.options.stopNodes && this.options.stopNodes.length > 0) {
62
+ this.stopNodesExact = new Set();
63
+ this.stopNodesWildcard = new Set();
64
+ for (let i = 0; i < this.options.stopNodes.length; i++) {
65
+ const stopNodeExp = this.options.stopNodes[i];
66
+ if (typeof stopNodeExp !== 'string') continue;
67
+ if (stopNodeExp.startsWith("*.")) {
68
+ this.stopNodesWildcard.add(stopNodeExp.substring(2));
69
+ } else {
70
+ this.stopNodesExact.add(stopNodeExp);
71
+ }
72
+ }
73
+ }
58
74
  }
59
75
 
60
76
  }
61
77
 
62
- function addExternalEntities(externalEntities){
78
+ function addExternalEntities(externalEntities) {
63
79
  const entKeys = Object.keys(externalEntities);
64
80
  for (let i = 0; i < entKeys.length; i++) {
65
81
  const ent = entKeys[i];
82
+ const escaped = ent.replace(/[.\-+*:]/g, '\\.');
66
83
  this.lastEntities[ent] = {
67
- regex: new RegExp("&"+ent+";","g"),
68
- val : externalEntities[ent]
84
+ regex: new RegExp("&" + escaped + ";", "g"),
85
+ val: externalEntities[ent]
69
86
  }
70
87
  }
71
88
  }
@@ -84,23 +101,23 @@ function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode,
84
101
  if (this.options.trimValues && !dontTrim) {
85
102
  val = val.trim();
86
103
  }
87
- if(val.length > 0){
88
- if(!escapeEntities) val = this.replaceEntitiesValue(val);
89
-
104
+ if (val.length > 0) {
105
+ if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);
106
+
90
107
  const newval = this.options.tagValueProcessor(tagName, val, jPath, hasAttributes, isLeafNode);
91
- if(newval === null || newval === undefined){
108
+ if (newval === null || newval === undefined) {
92
109
  //don't parse
93
110
  return val;
94
- }else if(typeof newval !== typeof val || newval !== val){
111
+ } else if (typeof newval !== typeof val || newval !== val) {
95
112
  //overwrite
96
113
  return newval;
97
- }else if(this.options.trimValues){
114
+ } else if (this.options.trimValues) {
98
115
  return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
99
- }else{
116
+ } else {
100
117
  const trimmedVal = val.trim();
101
- if(trimmedVal === val){
118
+ if (trimmedVal === val) {
102
119
  return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
103
- }else{
120
+ } else {
104
121
  return val;
105
122
  }
106
123
  }
@@ -145,20 +162,20 @@ function buildAttributesMap(attrStr, jPath, tagName) {
145
162
  if (this.options.transformAttributeName) {
146
163
  aName = this.options.transformAttributeName(aName);
147
164
  }
148
- if(aName === "__proto__") aName = "#__proto__";
165
+ aName = sanitizeName(aName, this.options);
149
166
  if (oldVal !== undefined) {
150
167
  if (this.options.trimValues) {
151
168
  oldVal = oldVal.trim();
152
169
  }
153
- oldVal = this.replaceEntitiesValue(oldVal);
170
+ oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath);
154
171
  const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPath);
155
- if(newVal === null || newVal === undefined){
172
+ if (newVal === null || newVal === undefined) {
156
173
  //don't parse
157
174
  attrs[aName] = oldVal;
158
- }else if(typeof newVal !== typeof oldVal || newVal !== oldVal){
175
+ } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) {
159
176
  //overwrite
160
177
  attrs[aName] = newVal;
161
- }else{
178
+ } else {
162
179
  //parse
163
180
  attrs[aName] = parseValue(
164
181
  oldVal,
@@ -183,46 +200,52 @@ function buildAttributesMap(attrStr, jPath, tagName) {
183
200
  }
184
201
  }
185
202
 
186
- const parseXml = function(xmlData) {
203
+ const parseXml = function (xmlData) {
187
204
  xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line
188
205
  const xmlObj = new xmlNode('!xml');
189
206
  let currentNode = xmlObj;
190
207
  let textData = "";
191
208
  let jPath = "";
192
- for(let i=0; i< xmlData.length; i++){//for each char in XML data
209
+
210
+ // Reset entity expansion counters for this document
211
+ this.entityExpansionCount = 0;
212
+ this.currentExpandedLength = 0;
213
+
214
+ const docTypeReader = new DocTypeReader(this.options.processEntities);
215
+ for (let i = 0; i < xmlData.length; i++) {//for each char in XML data
193
216
  const ch = xmlData[i];
194
- if(ch === '<'){
217
+ if (ch === '<') {
195
218
  // const nextIndex = i+1;
196
219
  // const _2ndChar = xmlData[nextIndex];
197
- if( xmlData[i+1] === '/') {//Closing Tag
220
+ if (xmlData[i + 1] === '/') {//Closing Tag
198
221
  const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.")
199
- let tagName = xmlData.substring(i+2,closeIndex).trim();
222
+ let tagName = xmlData.substring(i + 2, closeIndex).trim();
200
223
 
201
- if(this.options.removeNSPrefix){
224
+ if (this.options.removeNSPrefix) {
202
225
  const colonIndex = tagName.indexOf(":");
203
- if(colonIndex !== -1){
204
- tagName = tagName.substr(colonIndex+1);
226
+ if (colonIndex !== -1) {
227
+ tagName = tagName.substr(colonIndex + 1);
205
228
  }
206
229
  }
207
230
 
208
- if(this.options.transformTagName) {
231
+ if (this.options.transformTagName) {
209
232
  tagName = this.options.transformTagName(tagName);
210
233
  }
211
234
 
212
- if(currentNode){
235
+ if (currentNode) {
213
236
  textData = this.saveTextToParentTag(textData, currentNode, jPath);
214
237
  }
215
238
 
216
239
  //check if last tag of nested tag was unpaired tag
217
- const lastTagName = jPath.substring(jPath.lastIndexOf(".")+1);
218
- if(tagName && this.options.unpairedTags.indexOf(tagName) !== -1 ){
240
+ const lastTagName = jPath.substring(jPath.lastIndexOf(".") + 1);
241
+ if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) {
219
242
  throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
220
243
  }
221
244
  let propIndex = 0
222
- if(lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1 ){
223
- propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.')-1)
245
+ if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) {
246
+ propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.') - 1)
224
247
  this.tagsNodeStack.pop();
225
- }else{
248
+ } else {
226
249
  propIndex = jPath.lastIndexOf(".");
227
250
  }
228
251
  jPath = jPath.substring(0, propIndex);
@@ -230,74 +253,87 @@ const parseXml = function(xmlData) {
230
253
  currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
231
254
  textData = "";
232
255
  i = closeIndex;
233
- } else if( xmlData[i+1] === '?') {
256
+ } else if (xmlData[i + 1] === '?') {
234
257
 
235
- let tagData = readTagExp(xmlData,i, false, "?>");
236
- if(!tagData) throw new Error("Pi Tag is not closed.");
258
+ let tagData = readTagExp(xmlData, i, false, "?>");
259
+ if (!tagData) throw new Error("Pi Tag is not closed.");
237
260
 
238
261
  textData = this.saveTextToParentTag(textData, currentNode, jPath);
239
- if( (this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags){
262
+ if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) {
263
+ //do nothing
264
+ } else {
240
265
 
241
- }else{
242
-
243
266
  const childNode = new xmlNode(tagData.tagName);
244
267
  childNode.add(this.options.textNodeName, "");
245
-
246
- if(tagData.tagName !== tagData.tagExp && tagData.attrExpPresent){
268
+
269
+ if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
247
270
  childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath, tagData.tagName);
248
271
  }
249
- this.addChild(currentNode, childNode, jPath)
250
-
272
+ this.addChild(currentNode, childNode, jPath, i);
251
273
  }
252
274
 
253
275
 
254
276
  i = tagData.closeIndex + 1;
255
- } else if(xmlData.substr(i + 1, 3) === '!--') {
256
- const endIndex = findClosingIndex(xmlData, "-->", i+4, "Comment is not closed.")
257
- if(this.options.commentPropName){
277
+ } else if (xmlData.substr(i + 1, 3) === '!--') {
278
+ const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.")
279
+ if (this.options.commentPropName) {
258
280
  const comment = xmlData.substring(i + 4, endIndex - 2);
259
281
 
260
282
  textData = this.saveTextToParentTag(textData, currentNode, jPath);
261
283
 
262
- currentNode.add(this.options.commentPropName, [ { [this.options.textNodeName] : comment } ]);
284
+ currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]);
263
285
  }
264
286
  i = endIndex;
265
- } else if( xmlData.substr(i + 1, 2) === '!D') {
266
- const result = readDocType(xmlData, i);
287
+ } else if (xmlData.substr(i + 1, 2) === '!D') {
288
+ const result = docTypeReader.readDocType(xmlData, i);
267
289
  this.docTypeEntities = result.entities;
268
290
  i = result.i;
269
- }else if(xmlData.substr(i + 1, 2) === '![') {
291
+ } else if (xmlData.substr(i + 1, 2) === '![') {
270
292
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
271
- const tagExp = xmlData.substring(i + 9,closeIndex);
293
+ const tagExp = xmlData.substring(i + 9, closeIndex);
272
294
 
273
295
  textData = this.saveTextToParentTag(textData, currentNode, jPath);
274
296
 
275
297
  let val = this.parseTextData(tagExp, currentNode.tagname, jPath, true, false, true, true);
276
- if(val == undefined) val = "";
298
+ if (val == undefined) val = "";
277
299
 
278
300
  //cdata should be set even if it is 0 length string
279
- if(this.options.cdataPropName){
280
- currentNode.add(this.options.cdataPropName, [ { [this.options.textNodeName] : tagExp } ]);
281
- }else{
301
+ if (this.options.cdataPropName) {
302
+ currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]);
303
+ } else {
282
304
  currentNode.add(this.options.textNodeName, val);
283
305
  }
284
-
306
+
285
307
  i = closeIndex + 2;
286
- }else {//Opening tag
287
- let result = readTagExp(xmlData,i, this.options.removeNSPrefix);
288
- let tagName= result.tagName;
308
+ } else {//Opening tag
309
+ let result = readTagExp(xmlData, i, this.options.removeNSPrefix);
310
+ let tagName = result.tagName;
289
311
  const rawTagName = result.rawTagName;
290
312
  let tagExp = result.tagExp;
291
313
  let attrExpPresent = result.attrExpPresent;
292
314
  let closeIndex = result.closeIndex;
293
315
 
294
316
  if (this.options.transformTagName) {
295
- tagName = this.options.transformTagName(tagName);
317
+ //console.log(tagExp, tagName)
318
+ const newTagName = this.options.transformTagName(tagName);
319
+ if (tagExp === tagName) {
320
+ tagExp = newTagName
321
+ }
322
+ tagName = newTagName;
296
323
  }
297
-
324
+
325
+ if (this.options.strictReservedNames &&
326
+ (tagName === this.options.commentPropName
327
+ || tagName === this.options.cdataPropName
328
+ || tagName === this.options.textNodeName
329
+ || tagName === this.options.attributesGroupName
330
+ )) {
331
+ throw new Error(`Invalid tag name: ${tagName}`);
332
+ }
333
+
298
334
  //save text as child node
299
335
  if (currentNode && textData) {
300
- if(currentNode.tagname !== '!xml'){
336
+ if (currentNode.tagname !== '!xml') {
301
337
  //when nested tag is found
302
338
  textData = this.saveTextToParentTag(textData, currentNode, jPath, false);
303
339
  }
@@ -305,80 +341,99 @@ const parseXml = function(xmlData) {
305
341
 
306
342
  //check if last tag was unpaired tag
307
343
  const lastTag = currentNode;
308
- if(lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1 ){
344
+ if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) {
309
345
  currentNode = this.tagsNodeStack.pop();
310
346
  jPath = jPath.substring(0, jPath.lastIndexOf("."));
311
347
  }
312
- if(tagName !== xmlObj.tagname){
348
+ if (tagName !== xmlObj.tagname) {
313
349
  jPath += jPath ? "." + tagName : tagName;
314
350
  }
315
- if (this.isItStopNode(this.options.stopNodes, jPath, tagName)) {
351
+ const startIndex = i;
352
+ if (this.isItStopNode(this.stopNodesExact, this.stopNodesWildcard, jPath, tagName)) {
316
353
  let tagContent = "";
317
354
  //self-closing tag
318
- if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){
319
- if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
355
+ if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
356
+ if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
320
357
  tagName = tagName.substr(0, tagName.length - 1);
321
358
  jPath = jPath.substr(0, jPath.length - 1);
322
359
  tagExp = tagName;
323
- }else{
360
+ } else {
324
361
  tagExp = tagExp.substr(0, tagExp.length - 1);
325
362
  }
326
363
  i = result.closeIndex;
327
364
  }
328
365
  //unpaired tag
329
- else if(this.options.unpairedTags.indexOf(tagName) !== -1){
330
-
366
+ else if (this.options.unpairedTags.indexOf(tagName) !== -1) {
367
+
331
368
  i = result.closeIndex;
332
369
  }
333
370
  //normal tag
334
- else{
371
+ else {
335
372
  //read until closing tag is found
336
373
  const result = this.readStopNodeData(xmlData, rawTagName, closeIndex + 1);
337
- if(!result) throw new Error(`Unexpected end of ${rawTagName}`);
374
+ if (!result) throw new Error(`Unexpected end of ${rawTagName}`);
338
375
  i = result.i;
339
376
  tagContent = result.tagContent;
340
377
  }
341
378
 
342
379
  const childNode = new xmlNode(tagName);
343
- if(tagName !== tagExp && attrExpPresent){
380
+ if (tagName !== tagExp && attrExpPresent) {
344
381
  childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
345
382
  }
346
- if(tagContent) {
383
+ if (tagContent) {
347
384
  tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true);
348
385
  }
349
-
386
+
350
387
  jPath = jPath.substr(0, jPath.lastIndexOf("."));
351
388
  childNode.add(this.options.textNodeName, tagContent);
352
-
353
- this.addChild(currentNode, childNode, jPath)
354
- }else{
355
- //selfClosing tag
356
- if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){
357
- if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
389
+
390
+ this.addChild(currentNode, childNode, jPath, startIndex);
391
+ } else {
392
+ //selfClosing tag
393
+ if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
394
+ if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
358
395
  tagName = tagName.substr(0, tagName.length - 1);
359
396
  jPath = jPath.substr(0, jPath.length - 1);
360
397
  tagExp = tagName;
361
- }else{
398
+ } else {
362
399
  tagExp = tagExp.substr(0, tagExp.length - 1);
363
400
  }
364
-
365
- if(this.options.transformTagName) {
366
- tagName = this.options.transformTagName(tagName);
401
+
402
+ if (this.options.transformTagName) {
403
+ const newTagName = this.options.transformTagName(tagName);
404
+ if (tagExp === tagName) {
405
+ tagExp = newTagName
406
+ }
407
+ tagName = newTagName;
367
408
  }
368
409
 
369
410
  const childNode = new xmlNode(tagName);
370
- if(tagName !== tagExp && attrExpPresent){
411
+ if (tagName !== tagExp && attrExpPresent) {
371
412
  childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
372
413
  }
373
- this.addChild(currentNode, childNode, jPath)
414
+ this.addChild(currentNode, childNode, jPath, startIndex);
415
+ jPath = jPath.substr(0, jPath.lastIndexOf("."));
416
+ }
417
+ else if (this.options.unpairedTags.indexOf(tagName) !== -1) {//unpaired tag
418
+ const childNode = new xmlNode(tagName);
419
+ if (tagName !== tagExp && attrExpPresent) {
420
+ childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
421
+ }
422
+ this.addChild(currentNode, childNode, jPath, startIndex);
374
423
  jPath = jPath.substr(0, jPath.lastIndexOf("."));
424
+ i = result.closeIndex;
425
+ // Continue to next iteration without changing currentNode
426
+ continue;
375
427
  }
376
- //opening tag
377
- else{
378
- const childNode = new xmlNode( tagName);
428
+ //opening tag
429
+ else {
430
+ const childNode = new xmlNode(tagName);
431
+ if (this.tagsNodeStack.length > this.options.maxNestedTags) {
432
+ throw new Error("Maximum nested tags exceeded");
433
+ }
379
434
  this.tagsNodeStack.push(currentNode);
380
-
381
- if(tagName !== tagExp && attrExpPresent){
435
+
436
+ if (tagName !== tagExp && attrExpPresent) {
382
437
  childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
383
438
  }
384
439
  this.addChild(currentNode, childNode, jPath)
@@ -388,58 +443,142 @@ const parseXml = function(xmlData) {
388
443
  i = closeIndex;
389
444
  }
390
445
  }
391
- }else{
446
+ } else {
392
447
  textData += xmlData[i];
393
448
  }
394
449
  }
395
450
  return xmlObj.child;
396
451
  }
397
452
 
398
- function addChild(currentNode, childNode, jPath){
453
+ function addChild(currentNode, childNode, jPath, startIndex) {
454
+ // unset startIndex if not requested
455
+ if (!this.options.captureMetaData) startIndex = undefined;
399
456
  const result = this.options.updateTag(childNode.tagname, jPath, childNode[":@"])
400
- if(result === false){
401
- }else if(typeof result === "string"){
457
+ if (result === false) {
458
+ //do nothing
459
+ } else if (typeof result === "string") {
402
460
  childNode.tagname = result
403
- currentNode.addChild(childNode);
404
- }else{
405
- currentNode.addChild(childNode);
461
+ currentNode.addChild(childNode, startIndex);
462
+ } else {
463
+ currentNode.addChild(childNode, startIndex);
406
464
  }
407
465
  }
408
466
 
409
- const replaceEntitiesValue = function(val){
467
+ const replaceEntitiesValue = function (val, tagName, jPath) {
468
+ // Performance optimization: Early return if no entities to replace
469
+ if (val.indexOf('&') === -1) {
470
+ return val;
471
+ }
472
+
473
+ const entityConfig = this.options.processEntities;
474
+
475
+ if (!entityConfig.enabled) {
476
+ return val;
477
+ }
478
+
479
+ // Check tag-specific filtering
480
+ if (entityConfig.allowedTags) {
481
+ if (!entityConfig.allowedTags.includes(tagName)) {
482
+ return val; // Skip entity replacement for current tag as not set
483
+ }
484
+ }
485
+
486
+ if (entityConfig.tagFilter) {
487
+ if (!entityConfig.tagFilter(tagName, jPath)) {
488
+ return val; // Skip based on custom filter
489
+ }
490
+ }
491
+
492
+ // Replace DOCTYPE entities
493
+ for (let entityName in this.docTypeEntities) {
494
+ const entity = this.docTypeEntities[entityName];
495
+ const matches = val.match(entity.regx);
496
+
497
+ if (matches) {
498
+ // Track expansions
499
+ this.entityExpansionCount += matches.length;
500
+
501
+ // Check expansion limit
502
+ if (entityConfig.maxTotalExpansions &&
503
+ this.entityExpansionCount > entityConfig.maxTotalExpansions) {
504
+ throw new Error(
505
+ `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
506
+ );
507
+ }
508
+
509
+ // Store length before replacement
510
+ const lengthBefore = val.length;
511
+ val = val.replace(entity.regx, entity.val);
410
512
 
411
- if(this.options.processEntities){
412
- for(let entityName in this.docTypeEntities){
413
- const entity = this.docTypeEntities[entityName];
414
- val = val.replace( entity.regx, entity.val);
513
+ // Check expanded length immediately after replacement
514
+ if (entityConfig.maxExpandedLength) {
515
+ this.currentExpandedLength += (val.length - lengthBefore);
516
+
517
+ if (this.currentExpandedLength > entityConfig.maxExpandedLength) {
518
+ throw new Error(
519
+ `Total expanded content size exceeded: ${this.currentExpandedLength} > ${entityConfig.maxExpandedLength}`
520
+ );
521
+ }
522
+ }
415
523
  }
416
- for(let entityName in this.lastEntities){
417
- const entity = this.lastEntities[entityName];
418
- val = val.replace( entity.regex, entity.val);
524
+ }
525
+ if (val.indexOf('&') === -1) return val; // Early exit
526
+
527
+ // Replace standard entities
528
+ for (const entityName of Object.keys(this.lastEntities)) {
529
+ const entity = this.lastEntities[entityName];
530
+ const matches = val.match(entity.regex);
531
+ if (matches) {
532
+ this.entityExpansionCount += matches.length;
533
+ if (entityConfig.maxTotalExpansions &&
534
+ this.entityExpansionCount > entityConfig.maxTotalExpansions) {
535
+ throw new Error(
536
+ `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
537
+ );
538
+ }
419
539
  }
420
- if(this.options.htmlEntities){
421
- for(let entityName in this.htmlEntities){
422
- const entity = this.htmlEntities[entityName];
423
- val = val.replace( entity.regex, entity.val);
540
+ val = val.replace(entity.regex, entity.val);
541
+ }
542
+ if (val.indexOf('&') === -1) return val; // Early exit
543
+
544
+ // Replace HTML entities if enabled
545
+ if (this.options.htmlEntities) {
546
+ for (const entityName of Object.keys(this.htmlEntities)) {
547
+ const entity = this.htmlEntities[entityName];
548
+ const matches = val.match(entity.regex);
549
+ if (matches) {
550
+ //console.log(matches);
551
+ this.entityExpansionCount += matches.length;
552
+ if (entityConfig.maxTotalExpansions &&
553
+ this.entityExpansionCount > entityConfig.maxTotalExpansions) {
554
+ throw new Error(
555
+ `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
556
+ );
557
+ }
424
558
  }
559
+ val = val.replace(entity.regex, entity.val);
425
560
  }
426
- val = val.replace( this.ampEntity.regex, this.ampEntity.val);
427
561
  }
562
+
563
+ // Replace ampersand entity last
564
+ val = val.replace(this.ampEntity.regex, this.ampEntity.val);
565
+
428
566
  return val;
429
567
  }
430
- function saveTextToParentTag(textData, currentNode, jPath, isLeafNode) {
568
+
569
+ function saveTextToParentTag(textData, parentNode, jPath, isLeafNode) {
431
570
  if (textData) { //store previously collected data as textNode
432
- if(isLeafNode === undefined) isLeafNode = currentNode.child.length === 0
433
-
571
+ if (isLeafNode === undefined) isLeafNode = parentNode.child.length === 0
572
+
434
573
  textData = this.parseTextData(textData,
435
- currentNode.tagname,
574
+ parentNode.tagname,
436
575
  jPath,
437
576
  false,
438
- currentNode[":@"] ? Object.keys(currentNode[":@"]).length !== 0 : false,
577
+ parentNode[":@"] ? Object.keys(parentNode[":@"]).length !== 0 : false,
439
578
  isLeafNode);
440
579
 
441
580
  if (textData !== undefined && textData !== "")
442
- currentNode.add(this.options.textNodeName, textData);
581
+ parentNode.add(this.options.textNodeName, textData);
443
582
  textData = "";
444
583
  }
445
584
  return textData;
@@ -447,17 +586,14 @@ function saveTextToParentTag(textData, currentNode, jPath, isLeafNode) {
447
586
 
448
587
  //TODO: use jPath to simplify the logic
449
588
  /**
450
- *
451
- * @param {string[]} stopNodes
589
+ * @param {Set} stopNodesExact
590
+ * @param {Set} stopNodesWildcard
452
591
  * @param {string} jPath
453
- * @param {string} currentTagName
592
+ * @param {string} currentTagName
454
593
  */
455
- function isItStopNode(stopNodes, jPath, currentTagName){
456
- const allNodesExp = "*." + currentTagName;
457
- for (const stopNodePath in stopNodes) {
458
- const stopNodeExp = stopNodes[stopNodePath];
459
- if( allNodesExp === stopNodeExp || jPath === stopNodeExp ) return true;
460
- }
594
+ function isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName) {
595
+ if (stopNodesWildcard && stopNodesWildcard.has(currentTagName)) return true;
596
+ if (stopNodesExact && stopNodesExact.has(jPath)) return true;
461
597
  return false;
462
598
  }
463
599
 
@@ -467,24 +603,24 @@ function isItStopNode(stopNodes, jPath, currentTagName){
467
603
  * @param {number} i starting index
468
604
  * @returns
469
605
  */
470
- function tagExpWithClosingIndex(xmlData, i, closingChar = ">"){
606
+ function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
471
607
  let attrBoundary;
472
608
  let tagExp = "";
473
609
  for (let index = i; index < xmlData.length; index++) {
474
610
  let ch = xmlData[index];
475
611
  if (attrBoundary) {
476
- if (ch === attrBoundary) attrBoundary = "";//reset
612
+ if (ch === attrBoundary) attrBoundary = "";//reset
477
613
  } else if (ch === '"' || ch === "'") {
478
- attrBoundary = ch;
614
+ attrBoundary = ch;
479
615
  } else if (ch === closingChar[0]) {
480
- if(closingChar[1]){
481
- if(xmlData[index + 1] === closingChar[1]){
616
+ if (closingChar[1]) {
617
+ if (xmlData[index + 1] === closingChar[1]) {
482
618
  return {
483
619
  data: tagExp,
484
620
  index: index
485
621
  }
486
622
  }
487
- }else{
623
+ } else {
488
624
  return {
489
625
  data: tagExp,
490
626
  index: index
@@ -497,33 +633,33 @@ function tagExpWithClosingIndex(xmlData, i, closingChar = ">"){
497
633
  }
498
634
  }
499
635
 
500
- function findClosingIndex(xmlData, str, i, errMsg){
636
+ function findClosingIndex(xmlData, str, i, errMsg) {
501
637
  const closingIndex = xmlData.indexOf(str, i);
502
- if(closingIndex === -1){
638
+ if (closingIndex === -1) {
503
639
  throw new Error(errMsg)
504
- }else{
640
+ } else {
505
641
  return closingIndex + str.length - 1;
506
642
  }
507
643
  }
508
644
 
509
- function readTagExp(xmlData,i, removeNSPrefix, closingChar = ">"){
510
- const result = tagExpWithClosingIndex(xmlData, i+1, closingChar);
511
- if(!result) return;
645
+ function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") {
646
+ const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar);
647
+ if (!result) return;
512
648
  let tagExp = result.data;
513
649
  const closeIndex = result.index;
514
650
  const separatorIndex = tagExp.search(/\s/);
515
651
  let tagName = tagExp;
516
652
  let attrExpPresent = true;
517
- if(separatorIndex !== -1){//separate tag name and attributes expression
653
+ if (separatorIndex !== -1) {//separate tag name and attributes expression
518
654
  tagName = tagExp.substring(0, separatorIndex);
519
655
  tagExp = tagExp.substring(separatorIndex + 1).trimStart();
520
656
  }
521
657
 
522
658
  const rawTagName = tagName;
523
- if(removeNSPrefix){
659
+ if (removeNSPrefix) {
524
660
  const colonIndex = tagName.indexOf(":");
525
- if(colonIndex !== -1){
526
- tagName = tagName.substr(colonIndex+1);
661
+ if (colonIndex !== -1) {
662
+ tagName = tagName.substr(colonIndex + 1);
527
663
  attrExpPresent = tagName !== result.data.substr(colonIndex + 1);
528
664
  }
529
665
  }
@@ -542,47 +678,47 @@ function readTagExp(xmlData,i, removeNSPrefix, closingChar = ">"){
542
678
  * @param {string} tagName
543
679
  * @param {number} i
544
680
  */
545
- function readStopNodeData(xmlData, tagName, i){
681
+ function readStopNodeData(xmlData, tagName, i) {
546
682
  const startIndex = i;
547
683
  // Starting at 1 since we already have an open tag
548
684
  let openTagCount = 1;
549
685
 
550
686
  for (; i < xmlData.length; i++) {
551
- if( xmlData[i] === "<"){
552
- if (xmlData[i+1] === "/") {//close tag
553
- const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
554
- let closeTagName = xmlData.substring(i+2,closeIndex).trim();
555
- if(closeTagName === tagName){
556
- openTagCount--;
557
- if (openTagCount === 0) {
558
- return {
559
- tagContent: xmlData.substring(startIndex, i),
560
- i : closeIndex
561
- }
687
+ if (xmlData[i] === "<") {
688
+ if (xmlData[i + 1] === "/") {//close tag
689
+ const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
690
+ let closeTagName = xmlData.substring(i + 2, closeIndex).trim();
691
+ if (closeTagName === tagName) {
692
+ openTagCount--;
693
+ if (openTagCount === 0) {
694
+ return {
695
+ tagContent: xmlData.substring(startIndex, i),
696
+ i: closeIndex
562
697
  }
563
698
  }
564
- i=closeIndex;
565
- } else if(xmlData[i+1] === '?') {
566
- const closeIndex = findClosingIndex(xmlData, "?>", i+1, "StopNode is not closed.")
567
- i=closeIndex;
568
- } else if(xmlData.substr(i + 1, 3) === '!--') {
569
- const closeIndex = findClosingIndex(xmlData, "-->", i+3, "StopNode is not closed.")
570
- i=closeIndex;
571
- } else if(xmlData.substr(i + 1, 2) === '![') {
572
- const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
573
- i=closeIndex;
574
- } else {
575
- const tagData = readTagExp(xmlData, i, '>')
699
+ }
700
+ i = closeIndex;
701
+ } else if (xmlData[i + 1] === '?') {
702
+ const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.")
703
+ i = closeIndex;
704
+ } else if (xmlData.substr(i + 1, 3) === '!--') {
705
+ const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.")
706
+ i = closeIndex;
707
+ } else if (xmlData.substr(i + 1, 2) === '![') {
708
+ const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
709
+ i = closeIndex;
710
+ } else {
711
+ const tagData = readTagExp(xmlData, i, '>')
576
712
 
577
- if (tagData) {
578
- const openTagName = tagData && tagData.tagName;
579
- if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length-1] !== "/") {
580
- openTagCount++;
581
- }
582
- i=tagData.closeIndex;
713
+ if (tagData) {
714
+ const openTagName = tagData && tagData.tagName;
715
+ if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length - 1] !== "/") {
716
+ openTagCount++;
583
717
  }
718
+ i = tagData.closeIndex;
584
719
  }
585
720
  }
721
+ }
586
722
  }//end for loop
587
723
  }
588
724
 
@@ -590,8 +726,8 @@ function parseValue(val, shouldParse, options) {
590
726
  if (shouldParse && typeof val === 'string') {
591
727
  //console.log(options)
592
728
  const newval = val.trim();
593
- if(newval === 'true' ) return true;
594
- else if(newval === 'false' ) return false;
729
+ if (newval === 'true') return true;
730
+ else if (newval === 'false') return false;
595
731
  else return toNumber(val, options);
596
732
  } else {
597
733
  if (util.isExist(val)) {
@@ -602,5 +738,24 @@ function parseValue(val, shouldParse, options) {
602
738
  }
603
739
  }
604
740
 
741
+ function fromCodePoint(str, base, prefix) {
742
+ const codePoint = Number.parseInt(str, base);
743
+
744
+ if (codePoint >= 0 && codePoint <= 0x10FFFF) {
745
+ return String.fromCodePoint(codePoint);
746
+ } else {
747
+ return prefix + str + ";";
748
+ }
749
+ }
750
+
751
+ function sanitizeName(name, options) {
752
+ if (util.criticalProperties.includes(name)) {
753
+ throw new Error(`[SECURITY] Invalid name: "${name}" is a reserved JavaScript keyword that could cause prototype pollution`);
754
+ } else if (util.DANGEROUS_PROPERTY_NAMES.includes(name)) {
755
+ return options.onDangerousProperty(name);
756
+ }
757
+ return name;
758
+ }
605
759
 
606
760
  module.exports = OrderedObjParser;
761
+