fast-xml-parser 5.3.5 → 5.3.7

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.
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
  ///@ts-check
3
3
 
4
- import {getAllMatches, isExist} from '../util.js';
4
+ import { getAllMatches, isExist } from '../util.js';
5
5
  import xmlNode from './xmlNode.js';
6
6
  import DocTypeReader from './DocTypeReader.js';
7
7
  import toNumber from "strnum";
@@ -14,19 +14,19 @@ import getIgnoreAttributesFn from "../ignoreAttributes.js";
14
14
  //const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g");
15
15
  //const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g");
16
16
 
17
- export default class OrderedObjParser{
18
- constructor(options){
17
+ export default 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 @@ export default 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) => fromCodePoint(str, 10, "&#") },
45
- "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val : (_, str) => fromCodePoint(str, 16, "&#x") },
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,16 +55,18 @@ export default 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;
58
60
 
59
- if(this.options.stopNodes && this.options.stopNodes.length > 0){
61
+ if (this.options.stopNodes && this.options.stopNodes.length > 0) {
60
62
  this.stopNodesExact = new Set();
61
63
  this.stopNodesWildcard = new Set();
62
- for(let i = 0; i < this.options.stopNodes.length; i++){
64
+ for (let i = 0; i < this.options.stopNodes.length; i++) {
63
65
  const stopNodeExp = this.options.stopNodes[i];
64
- if(typeof stopNodeExp !== 'string') continue;
65
- if(stopNodeExp.startsWith("*.")){
66
+ if (typeof stopNodeExp !== 'string') continue;
67
+ if (stopNodeExp.startsWith("*.")) {
66
68
  this.stopNodesWildcard.add(stopNodeExp.substring(2));
67
- }else{
69
+ } else {
68
70
  this.stopNodesExact.add(stopNodeExp);
69
71
  }
70
72
  }
@@ -73,14 +75,14 @@ export default class OrderedObjParser{
73
75
 
74
76
  }
75
77
 
76
- function addExternalEntities(externalEntities){
78
+ function addExternalEntities(externalEntities) {
77
79
  const entKeys = Object.keys(externalEntities);
78
80
  for (let i = 0; i < entKeys.length; i++) {
79
81
  const ent = entKeys[i];
80
82
  const escaped = ent.replace(/[.\-+*:]/g, '\\.');
81
83
  this.lastEntities[ent] = {
82
- regex: new RegExp("&"+escaped+";","g"),
83
- val : externalEntities[ent]
84
+ regex: new RegExp("&" + escaped + ";", "g"),
85
+ val: externalEntities[ent]
84
86
  }
85
87
  }
86
88
  }
@@ -99,23 +101,23 @@ function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode,
99
101
  if (this.options.trimValues && !dontTrim) {
100
102
  val = val.trim();
101
103
  }
102
- if(val.length > 0){
103
- if(!escapeEntities) val = this.replaceEntitiesValue(val);
104
-
104
+ if (val.length > 0) {
105
+ if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);
106
+
105
107
  const newval = this.options.tagValueProcessor(tagName, val, jPath, hasAttributes, isLeafNode);
106
- if(newval === null || newval === undefined){
108
+ if (newval === null || newval === undefined) {
107
109
  //don't parse
108
110
  return val;
109
- }else if(typeof newval !== typeof val || newval !== val){
111
+ } else if (typeof newval !== typeof val || newval !== val) {
110
112
  //overwrite
111
113
  return newval;
112
- }else if(this.options.trimValues){
114
+ } else if (this.options.trimValues) {
113
115
  return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
114
- }else{
116
+ } else {
115
117
  const trimmedVal = val.trim();
116
- if(trimmedVal === val){
118
+ if (trimmedVal === val) {
117
119
  return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
118
- }else{
120
+ } else {
119
121
  return val;
120
122
  }
121
123
  }
@@ -141,7 +143,7 @@ function resolveNameSpace(tagname) {
141
143
  //const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm");
142
144
  const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm');
143
145
 
144
- function buildAttributesMap(attrStr, jPath) {
146
+ function buildAttributesMap(attrStr, jPath, tagName) {
145
147
  if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') {
146
148
  // attrStr = attrStr.replace(/\r?\n/g, ' ');
147
149
  //attrStr = attrStr || attrStr.trim();
@@ -160,20 +162,20 @@ function buildAttributesMap(attrStr, jPath) {
160
162
  if (this.options.transformAttributeName) {
161
163
  aName = this.options.transformAttributeName(aName);
162
164
  }
163
- if(aName === "__proto__") aName = "#__proto__";
165
+ if (aName === "__proto__") aName = "#__proto__";
164
166
  if (oldVal !== undefined) {
165
167
  if (this.options.trimValues) {
166
168
  oldVal = oldVal.trim();
167
169
  }
168
- oldVal = this.replaceEntitiesValue(oldVal);
170
+ oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath);
169
171
  const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPath);
170
- if(newVal === null || newVal === undefined){
172
+ if (newVal === null || newVal === undefined) {
171
173
  //don't parse
172
174
  attrs[aName] = oldVal;
173
- }else if(typeof newVal !== typeof oldVal || newVal !== oldVal){
175
+ } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) {
174
176
  //overwrite
175
177
  attrs[aName] = newVal;
176
- }else{
178
+ } else {
177
179
  //parse
178
180
  attrs[aName] = parseValue(
179
181
  oldVal,
@@ -198,47 +200,52 @@ function buildAttributesMap(attrStr, jPath) {
198
200
  }
199
201
  }
200
202
 
201
- const parseXml = function(xmlData) {
203
+ const parseXml = function (xmlData) {
202
204
  xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line
203
205
  const xmlObj = new xmlNode('!xml');
204
206
  let currentNode = xmlObj;
205
207
  let textData = "";
206
208
  let jPath = "";
209
+
210
+ // Reset entity expansion counters for this document
211
+ this.entityExpansionCount = 0;
212
+ this.currentExpandedLength = 0;
213
+
207
214
  const docTypeReader = new DocTypeReader(this.options.processEntities);
208
- for(let i=0; i< xmlData.length; i++){//for each char in XML data
215
+ for (let i = 0; i < xmlData.length; i++) {//for each char in XML data
209
216
  const ch = xmlData[i];
210
- if(ch === '<'){
217
+ if (ch === '<') {
211
218
  // const nextIndex = i+1;
212
219
  // const _2ndChar = xmlData[nextIndex];
213
- if( xmlData[i+1] === '/') {//Closing Tag
220
+ if (xmlData[i + 1] === '/') {//Closing Tag
214
221
  const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.")
215
- let tagName = xmlData.substring(i+2,closeIndex).trim();
222
+ let tagName = xmlData.substring(i + 2, closeIndex).trim();
216
223
 
217
- if(this.options.removeNSPrefix){
224
+ if (this.options.removeNSPrefix) {
218
225
  const colonIndex = tagName.indexOf(":");
219
- if(colonIndex !== -1){
220
- tagName = tagName.substr(colonIndex+1);
226
+ if (colonIndex !== -1) {
227
+ tagName = tagName.substr(colonIndex + 1);
221
228
  }
222
229
  }
223
230
 
224
- if(this.options.transformTagName) {
231
+ if (this.options.transformTagName) {
225
232
  tagName = this.options.transformTagName(tagName);
226
233
  }
227
234
 
228
- if(currentNode){
235
+ if (currentNode) {
229
236
  textData = this.saveTextToParentTag(textData, currentNode, jPath);
230
237
  }
231
238
 
232
239
  //check if last tag of nested tag was unpaired tag
233
- const lastTagName = jPath.substring(jPath.lastIndexOf(".")+1);
234
- 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) {
235
242
  throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
236
243
  }
237
244
  let propIndex = 0
238
- if(lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1 ){
239
- propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.')-1)
245
+ if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) {
246
+ propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.') - 1)
240
247
  this.tagsNodeStack.pop();
241
- }else{
248
+ } else {
242
249
  propIndex = jPath.lastIndexOf(".");
243
250
  }
244
251
  jPath = jPath.substring(0, propIndex);
@@ -246,61 +253,61 @@ const parseXml = function(xmlData) {
246
253
  currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
247
254
  textData = "";
248
255
  i = closeIndex;
249
- } else if( xmlData[i+1] === '?') {
256
+ } else if (xmlData[i + 1] === '?') {
250
257
 
251
- let tagData = readTagExp(xmlData,i, false, "?>");
252
- 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.");
253
260
 
254
261
  textData = this.saveTextToParentTag(textData, currentNode, jPath);
255
- if( (this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags){
262
+ if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) {
256
263
  //do nothing
257
- }else{
258
-
264
+ } else {
265
+
259
266
  const childNode = new xmlNode(tagData.tagName);
260
267
  childNode.add(this.options.textNodeName, "");
261
-
262
- if(tagData.tagName !== tagData.tagExp && tagData.attrExpPresent){
263
- childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath);
268
+
269
+ if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
270
+ childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath, tagData.tagName);
264
271
  }
265
272
  this.addChild(currentNode, childNode, jPath, i);
266
273
  }
267
274
 
268
275
 
269
276
  i = tagData.closeIndex + 1;
270
- } else if(xmlData.substr(i + 1, 3) === '!--') {
271
- const endIndex = findClosingIndex(xmlData, "-->", i+4, "Comment is not closed.")
272
- 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) {
273
280
  const comment = xmlData.substring(i + 4, endIndex - 2);
274
281
 
275
282
  textData = this.saveTextToParentTag(textData, currentNode, jPath);
276
283
 
277
- currentNode.add(this.options.commentPropName, [ { [this.options.textNodeName] : comment } ]);
284
+ currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]);
278
285
  }
279
286
  i = endIndex;
280
- } else if( xmlData.substr(i + 1, 2) === '!D') {
287
+ } else if (xmlData.substr(i + 1, 2) === '!D') {
281
288
  const result = docTypeReader.readDocType(xmlData, i);
282
289
  this.docTypeEntities = result.entities;
283
290
  i = result.i;
284
- }else if(xmlData.substr(i + 1, 2) === '![') {
291
+ } else if (xmlData.substr(i + 1, 2) === '![') {
285
292
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
286
- const tagExp = xmlData.substring(i + 9,closeIndex);
293
+ const tagExp = xmlData.substring(i + 9, closeIndex);
287
294
 
288
295
  textData = this.saveTextToParentTag(textData, currentNode, jPath);
289
296
 
290
297
  let val = this.parseTextData(tagExp, currentNode.tagname, jPath, true, false, true, true);
291
- if(val == undefined) val = "";
298
+ if (val == undefined) val = "";
292
299
 
293
300
  //cdata should be set even if it is 0 length string
294
- if(this.options.cdataPropName){
295
- currentNode.add(this.options.cdataPropName, [ { [this.options.textNodeName] : tagExp } ]);
296
- }else{
301
+ if (this.options.cdataPropName) {
302
+ currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]);
303
+ } else {
297
304
  currentNode.add(this.options.textNodeName, val);
298
305
  }
299
-
306
+
300
307
  i = closeIndex + 2;
301
- }else {//Opening tag
302
- let result = readTagExp(xmlData,i, this.options.removeNSPrefix);
303
- let tagName= result.tagName;
308
+ } else {//Opening tag
309
+ let result = readTagExp(xmlData, i, this.options.removeNSPrefix);
310
+ let tagName = result.tagName;
304
311
  const rawTagName = result.rawTagName;
305
312
  let tagExp = result.tagExp;
306
313
  let attrExpPresent = result.attrExpPresent;
@@ -309,15 +316,15 @@ const parseXml = function(xmlData) {
309
316
  if (this.options.transformTagName) {
310
317
  //console.log(tagExp, tagName)
311
318
  const newTagName = this.options.transformTagName(tagName);
312
- if(tagExp === tagName) {
319
+ if (tagExp === tagName) {
313
320
  tagExp = newTagName
314
321
  }
315
322
  tagName = newTagName;
316
323
  }
317
-
324
+
318
325
  //save text as child node
319
326
  if (currentNode && textData) {
320
- if(currentNode.tagname !== '!xml'){
327
+ if (currentNode.tagname !== '!xml') {
321
328
  //when nested tag is found
322
329
  textData = this.saveTextToParentTag(textData, currentNode, jPath, false);
323
330
  }
@@ -325,88 +332,87 @@ const parseXml = function(xmlData) {
325
332
 
326
333
  //check if last tag was unpaired tag
327
334
  const lastTag = currentNode;
328
- if(lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1 ){
335
+ if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) {
329
336
  currentNode = this.tagsNodeStack.pop();
330
337
  jPath = jPath.substring(0, jPath.lastIndexOf("."));
331
338
  }
332
- if(tagName !== xmlObj.tagname){
339
+ if (tagName !== xmlObj.tagname) {
333
340
  jPath += jPath ? "." + tagName : tagName;
334
341
  }
335
342
  const startIndex = i;
336
343
  if (this.isItStopNode(this.stopNodesExact, this.stopNodesWildcard, jPath, tagName)) {
337
344
  let tagContent = "";
338
345
  //self-closing tag
339
- if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){
340
- if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
346
+ if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
347
+ if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
341
348
  tagName = tagName.substr(0, tagName.length - 1);
342
349
  jPath = jPath.substr(0, jPath.length - 1);
343
350
  tagExp = tagName;
344
- }else{
351
+ } else {
345
352
  tagExp = tagExp.substr(0, tagExp.length - 1);
346
353
  }
347
354
  i = result.closeIndex;
348
355
  }
349
356
  //unpaired tag
350
- else if(this.options.unpairedTags.indexOf(tagName) !== -1){
351
-
357
+ else if (this.options.unpairedTags.indexOf(tagName) !== -1) {
358
+
352
359
  i = result.closeIndex;
353
360
  }
354
361
  //normal tag
355
- else{
362
+ else {
356
363
  //read until closing tag is found
357
364
  const result = this.readStopNodeData(xmlData, rawTagName, closeIndex + 1);
358
- if(!result) throw new Error(`Unexpected end of ${rawTagName}`);
365
+ if (!result) throw new Error(`Unexpected end of ${rawTagName}`);
359
366
  i = result.i;
360
367
  tagContent = result.tagContent;
361
368
  }
362
369
 
363
370
  const childNode = new xmlNode(tagName);
364
371
 
365
- if(tagName !== tagExp && attrExpPresent){
366
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath
367
- );
372
+ if (tagName !== tagExp && attrExpPresent) {
373
+ childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
368
374
  }
369
- if(tagContent) {
375
+ if (tagContent) {
370
376
  tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true);
371
377
  }
372
-
378
+
373
379
  jPath = jPath.substr(0, jPath.lastIndexOf("."));
374
380
  childNode.add(this.options.textNodeName, tagContent);
375
-
381
+
376
382
  this.addChild(currentNode, childNode, jPath, startIndex);
377
- }else{
378
- //selfClosing tag
379
- if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){
380
- if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
383
+ } else {
384
+ //selfClosing tag
385
+ if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
386
+ if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
381
387
  tagName = tagName.substr(0, tagName.length - 1);
382
388
  jPath = jPath.substr(0, jPath.length - 1);
383
389
  tagExp = tagName;
384
- }else{
390
+ } else {
385
391
  tagExp = tagExp.substr(0, tagExp.length - 1);
386
392
  }
387
-
388
- if(this.options.transformTagName) {
393
+
394
+ if (this.options.transformTagName) {
389
395
  const newTagName = this.options.transformTagName(tagName);
390
- if(tagExp === tagName) {
396
+ if (tagExp === tagName) {
391
397
  tagExp = newTagName
392
398
  }
393
399
  tagName = newTagName;
394
400
  }
395
401
 
396
402
  const childNode = new xmlNode(tagName);
397
- if(tagName !== tagExp && attrExpPresent){
398
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
403
+ if (tagName !== tagExp && attrExpPresent) {
404
+ childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
399
405
  }
400
406
  this.addChild(currentNode, childNode, jPath, startIndex);
401
407
  jPath = jPath.substr(0, jPath.lastIndexOf("."));
402
408
  }
403
- //opening tag
404
- else{
405
- const childNode = new xmlNode( tagName);
409
+ //opening tag
410
+ else {
411
+ const childNode = new xmlNode(tagName);
406
412
  this.tagsNodeStack.push(currentNode);
407
-
408
- if(tagName !== tagExp && attrExpPresent){
409
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
413
+
414
+ if (tagName !== tagExp && attrExpPresent) {
415
+ childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
410
416
  }
411
417
  this.addChild(currentNode, childNode, jPath, startIndex);
412
418
  currentNode = childNode;
@@ -415,52 +421,113 @@ const parseXml = function(xmlData) {
415
421
  i = closeIndex;
416
422
  }
417
423
  }
418
- }else{
424
+ } else {
419
425
  textData += xmlData[i];
420
426
  }
421
427
  }
422
428
  return xmlObj.child;
423
429
  }
424
430
 
425
- function addChild(currentNode, childNode, jPath, startIndex){
431
+ function addChild(currentNode, childNode, jPath, startIndex) {
426
432
  // unset startIndex if not requested
427
433
  if (!this.options.captureMetaData) startIndex = undefined;
428
434
  const result = this.options.updateTag(childNode.tagname, jPath, childNode[":@"])
429
- if(result === false){
435
+ if (result === false) {
430
436
  //do nothing
431
- } else if(typeof result === "string"){
437
+ } else if (typeof result === "string") {
432
438
  childNode.tagname = result
433
439
  currentNode.addChild(childNode, startIndex);
434
- }else{
440
+ } else {
435
441
  currentNode.addChild(childNode, startIndex);
436
442
  }
437
443
  }
438
444
 
439
- const replaceEntitiesValue = function(val){
445
+ const replaceEntitiesValue = function (val, tagName, jPath) {
446
+ // Performance optimization: Early return if no entities to replace
447
+ if (val.indexOf('&') === -1) {
448
+ return val;
449
+ }
450
+
451
+ const entityConfig = this.options.processEntities;
440
452
 
441
- if(this.options.processEntities){
442
- for(let entityName in this.docTypeEntities){
443
- const entity = this.docTypeEntities[entityName];
444
- val = val.replace( entity.regx, entity.val);
453
+ if (!entityConfig.enabled) {
454
+ return val;
455
+ }
456
+
457
+ // Check tag-specific filtering
458
+ if (entityConfig.allowedTags) {
459
+ if (!entityConfig.allowedTags.includes(tagName)) {
460
+ return val; // Skip entity replacement for current tag as not set
445
461
  }
446
- for(let entityName in this.lastEntities){
447
- const entity = this.lastEntities[entityName];
448
- val = val.replace( entity.regex, entity.val);
462
+ }
463
+
464
+ if (entityConfig.tagFilter) {
465
+ if (!entityConfig.tagFilter(tagName, jPath)) {
466
+ return val; // Skip based on custom filter
449
467
  }
450
- if(this.options.htmlEntities){
451
- for(let entityName in this.htmlEntities){
452
- const entity = this.htmlEntities[entityName];
453
- val = val.replace( entity.regex, entity.val);
468
+ }
469
+
470
+ // Replace DOCTYPE entities
471
+ for (let entityName in this.docTypeEntities) {
472
+ const entity = this.docTypeEntities[entityName];
473
+ const matches = val.match(entity.regx);
474
+
475
+ if (matches) {
476
+ // Track expansions
477
+ this.entityExpansionCount += matches.length;
478
+
479
+ // Check expansion limit
480
+ if (entityConfig.maxTotalExpansions &&
481
+ this.entityExpansionCount > entityConfig.maxTotalExpansions) {
482
+ throw new Error(
483
+ `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
484
+ );
485
+ }
486
+
487
+ // Store length before replacement
488
+ const lengthBefore = val.length;
489
+ val = val.replace(entity.regx, entity.val);
490
+
491
+ // Check expanded length immediately after replacement
492
+ if (entityConfig.maxExpandedLength) {
493
+ this.currentExpandedLength += (val.length - lengthBefore);
494
+
495
+ if (this.currentExpandedLength > entityConfig.maxExpandedLength) {
496
+ throw new Error(
497
+ `Total expanded content size exceeded: ${this.currentExpandedLength} > ${entityConfig.maxExpandedLength}`
498
+ );
499
+ }
454
500
  }
455
501
  }
456
- val = val.replace( this.ampEntity.regex, this.ampEntity.val);
457
502
  }
503
+ if (val.indexOf('&') === -1) return val; // Early exit
504
+
505
+ // Replace standard entities
506
+ for (let entityName in this.lastEntities) {
507
+ const entity = this.lastEntities[entityName];
508
+ val = val.replace(entity.regex, entity.val);
509
+ }
510
+ if (val.indexOf('&') === -1) return val; // Early exit
511
+
512
+ // Replace HTML entities if enabled
513
+ if (this.options.htmlEntities) {
514
+ for (let entityName in this.htmlEntities) {
515
+ const entity = this.htmlEntities[entityName];
516
+ val = val.replace(entity.regex, entity.val);
517
+ }
518
+ }
519
+
520
+ // Replace ampersand entity last
521
+ val = val.replace(this.ampEntity.regex, this.ampEntity.val);
522
+
458
523
  return val;
459
524
  }
525
+
526
+
460
527
  function saveTextToParentTag(textData, currentNode, jPath, isLeafNode) {
461
528
  if (textData) { //store previously collected data as textNode
462
- if(isLeafNode === undefined) isLeafNode = currentNode.child.length === 0
463
-
529
+ if (isLeafNode === undefined) isLeafNode = currentNode.child.length === 0
530
+
464
531
  textData = this.parseTextData(textData,
465
532
  currentNode.tagname,
466
533
  jPath,
@@ -482,9 +549,9 @@ function saveTextToParentTag(textData, currentNode, jPath, isLeafNode) {
482
549
  * @param {string} jPath
483
550
  * @param {string} currentTagName
484
551
  */
485
- function isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName){
486
- if(stopNodesWildcard && stopNodesWildcard.has(currentTagName)) return true;
487
- if(stopNodesExact && stopNodesExact.has(jPath)) return true;
552
+ function isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName) {
553
+ if (stopNodesWildcard && stopNodesWildcard.has(currentTagName)) return true;
554
+ if (stopNodesExact && stopNodesExact.has(jPath)) return true;
488
555
  return false;
489
556
  }
490
557
 
@@ -494,24 +561,24 @@ function isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName){
494
561
  * @param {number} i starting index
495
562
  * @returns
496
563
  */
497
- function tagExpWithClosingIndex(xmlData, i, closingChar = ">"){
564
+ function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
498
565
  let attrBoundary;
499
566
  let tagExp = "";
500
567
  for (let index = i; index < xmlData.length; index++) {
501
568
  let ch = xmlData[index];
502
569
  if (attrBoundary) {
503
- if (ch === attrBoundary) attrBoundary = "";//reset
570
+ if (ch === attrBoundary) attrBoundary = "";//reset
504
571
  } else if (ch === '"' || ch === "'") {
505
- attrBoundary = ch;
572
+ attrBoundary = ch;
506
573
  } else if (ch === closingChar[0]) {
507
- if(closingChar[1]){
508
- if(xmlData[index + 1] === closingChar[1]){
574
+ if (closingChar[1]) {
575
+ if (xmlData[index + 1] === closingChar[1]) {
509
576
  return {
510
577
  data: tagExp,
511
578
  index: index
512
579
  }
513
580
  }
514
- }else{
581
+ } else {
515
582
  return {
516
583
  data: tagExp,
517
584
  index: index
@@ -524,33 +591,33 @@ function tagExpWithClosingIndex(xmlData, i, closingChar = ">"){
524
591
  }
525
592
  }
526
593
 
527
- function findClosingIndex(xmlData, str, i, errMsg){
594
+ function findClosingIndex(xmlData, str, i, errMsg) {
528
595
  const closingIndex = xmlData.indexOf(str, i);
529
- if(closingIndex === -1){
596
+ if (closingIndex === -1) {
530
597
  throw new Error(errMsg)
531
- }else{
598
+ } else {
532
599
  return closingIndex + str.length - 1;
533
600
  }
534
601
  }
535
602
 
536
- function readTagExp(xmlData,i, removeNSPrefix, closingChar = ">"){
537
- const result = tagExpWithClosingIndex(xmlData, i+1, closingChar);
538
- if(!result) return;
603
+ function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") {
604
+ const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar);
605
+ if (!result) return;
539
606
  let tagExp = result.data;
540
607
  const closeIndex = result.index;
541
608
  const separatorIndex = tagExp.search(/\s/);
542
609
  let tagName = tagExp;
543
610
  let attrExpPresent = true;
544
- if(separatorIndex !== -1){//separate tag name and attributes expression
611
+ if (separatorIndex !== -1) {//separate tag name and attributes expression
545
612
  tagName = tagExp.substring(0, separatorIndex);
546
613
  tagExp = tagExp.substring(separatorIndex + 1).trimStart();
547
614
  }
548
615
 
549
616
  const rawTagName = tagName;
550
- if(removeNSPrefix){
617
+ if (removeNSPrefix) {
551
618
  const colonIndex = tagName.indexOf(":");
552
- if(colonIndex !== -1){
553
- tagName = tagName.substr(colonIndex+1);
619
+ if (colonIndex !== -1) {
620
+ tagName = tagName.substr(colonIndex + 1);
554
621
  attrExpPresent = tagName !== result.data.substr(colonIndex + 1);
555
622
  }
556
623
  }
@@ -569,47 +636,47 @@ function readTagExp(xmlData,i, removeNSPrefix, closingChar = ">"){
569
636
  * @param {string} tagName
570
637
  * @param {number} i
571
638
  */
572
- function readStopNodeData(xmlData, tagName, i){
639
+ function readStopNodeData(xmlData, tagName, i) {
573
640
  const startIndex = i;
574
641
  // Starting at 1 since we already have an open tag
575
642
  let openTagCount = 1;
576
643
 
577
644
  for (; i < xmlData.length; i++) {
578
- if( xmlData[i] === "<"){
579
- if (xmlData[i+1] === "/") {//close tag
580
- const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
581
- let closeTagName = xmlData.substring(i+2,closeIndex).trim();
582
- if(closeTagName === tagName){
583
- openTagCount--;
584
- if (openTagCount === 0) {
585
- return {
586
- tagContent: xmlData.substring(startIndex, i),
587
- i : closeIndex
588
- }
645
+ if (xmlData[i] === "<") {
646
+ if (xmlData[i + 1] === "/") {//close tag
647
+ const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
648
+ let closeTagName = xmlData.substring(i + 2, closeIndex).trim();
649
+ if (closeTagName === tagName) {
650
+ openTagCount--;
651
+ if (openTagCount === 0) {
652
+ return {
653
+ tagContent: xmlData.substring(startIndex, i),
654
+ i: closeIndex
589
655
  }
590
656
  }
591
- i=closeIndex;
592
- } else if(xmlData[i+1] === '?') {
593
- const closeIndex = findClosingIndex(xmlData, "?>", i+1, "StopNode is not closed.")
594
- i=closeIndex;
595
- } else if(xmlData.substr(i + 1, 3) === '!--') {
596
- const closeIndex = findClosingIndex(xmlData, "-->", i+3, "StopNode is not closed.")
597
- i=closeIndex;
598
- } else if(xmlData.substr(i + 1, 2) === '![') {
599
- const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
600
- i=closeIndex;
601
- } else {
602
- const tagData = readTagExp(xmlData, i, '>')
657
+ }
658
+ i = closeIndex;
659
+ } else if (xmlData[i + 1] === '?') {
660
+ const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.")
661
+ i = closeIndex;
662
+ } else if (xmlData.substr(i + 1, 3) === '!--') {
663
+ const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.")
664
+ i = closeIndex;
665
+ } else if (xmlData.substr(i + 1, 2) === '![') {
666
+ const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
667
+ i = closeIndex;
668
+ } else {
669
+ const tagData = readTagExp(xmlData, i, '>')
603
670
 
604
- if (tagData) {
605
- const openTagName = tagData && tagData.tagName;
606
- if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length-1] !== "/") {
607
- openTagCount++;
608
- }
609
- i=tagData.closeIndex;
671
+ if (tagData) {
672
+ const openTagName = tagData && tagData.tagName;
673
+ if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length - 1] !== "/") {
674
+ openTagCount++;
610
675
  }
676
+ i = tagData.closeIndex;
611
677
  }
612
678
  }
679
+ }
613
680
  }//end for loop
614
681
  }
615
682
 
@@ -617,8 +684,8 @@ function parseValue(val, shouldParse, options) {
617
684
  if (shouldParse && typeof val === 'string') {
618
685
  //console.log(options)
619
686
  const newval = val.trim();
620
- if(newval === 'true' ) return true;
621
- else if(newval === 'false' ) return false;
687
+ if (newval === 'true') return true;
688
+ else if (newval === 'false') return false;
622
689
  else return toNumber(val, options);
623
690
  } else {
624
691
  if (isExist(val)) {
@@ -629,12 +696,12 @@ function parseValue(val, shouldParse, options) {
629
696
  }
630
697
  }
631
698
 
632
- function fromCodePoint(str, base, prefix){
699
+ function fromCodePoint(str, base, prefix) {
633
700
  const codePoint = Number.parseInt(str, base);
634
701
 
635
702
  if (codePoint >= 0 && codePoint <= 0x10FFFF) {
636
- return String.fromCodePoint(codePoint);
703
+ return String.fromCodePoint(codePoint);
637
704
  } else {
638
- return prefix +str + ";";
705
+ return prefix + str + ";";
639
706
  }
640
707
  }