fast-xml-parser 5.5.10 → 5.5.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-xml-parser",
3
- "version": "5.5.10",
3
+ "version": "5.5.11",
4
4
  "description": "Validate XML, Parse XML, Build XML without C/C++ based libraries",
5
5
  "main": "./lib/fxp.cjs",
6
6
  "type": "module",
@@ -88,7 +88,7 @@
88
88
  ],
89
89
  "dependencies": {
90
90
  "fast-xml-builder": "^1.1.4",
91
- "path-expression-matcher": "^1.2.1",
92
- "strnum": "^2.2.2"
91
+ "path-expression-matcher": "^1.4.0",
92
+ "strnum": "^2.2.3"
93
93
  }
94
94
  }
@@ -142,7 +142,7 @@ export const buildOptions = function (options) {
142
142
 
143
143
  // Always normalize processEntities for backward compatibility and validation
144
144
  built.processEntities = normalizeProcessEntities(built.processEntities);
145
-
145
+ built.unpairedTagsSet = new Set(built.unpairedTags);
146
146
  // Convert old-style stopNodes for backward compatibility
147
147
  if (built.stopNodes && Array.isArray(built.stopNodes)) {
148
148
  built.stopNodes = built.stopNodes.map(node => {
@@ -7,6 +7,7 @@ import DocTypeReader from './DocTypeReader.js';
7
7
  import toNumber from "strnum";
8
8
  import getIgnoreAttributesFn from "../ignoreAttributes.js";
9
9
  import { Expression, Matcher } from 'path-expression-matcher';
10
+ import { ExpressionSet } from 'path-expression-matcher';
10
11
 
11
12
  // const regx =
12
13
  // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
@@ -121,18 +122,20 @@ export default class OrderedObjParser {
121
122
  this.isCurrentNodeStopNode = false;
122
123
 
123
124
  // Pre-compile stopNodes expressions
124
- if (this.options.stopNodes && this.options.stopNodes.length > 0) {
125
- this.stopNodeExpressions = [];
126
- for (let i = 0; i < this.options.stopNodes.length; i++) {
127
- const stopNodeExp = this.options.stopNodes[i];
125
+ this.stopNodeExpressionsSet = new ExpressionSet();
126
+ const stopNodesOpts = this.options.stopNodes;
127
+ if (stopNodesOpts && stopNodesOpts.length > 0) {
128
+ for (let i = 0; i < stopNodesOpts.length; i++) {
129
+ const stopNodeExp = stopNodesOpts[i];
128
130
  if (typeof stopNodeExp === 'string') {
129
131
  // Convert string to Expression object
130
- this.stopNodeExpressions.push(new Expression(stopNodeExp));
132
+ this.stopNodeExpressionsSet.add(new Expression(stopNodeExp));
131
133
  } else if (stopNodeExp instanceof Expression) {
132
134
  // Already an Expression object
133
- this.stopNodeExpressions.push(stopNodeExp);
135
+ this.stopNodeExpressionsSet.add(stopNodeExp);
134
136
  }
135
137
  }
138
+ this.stopNodeExpressionsSet.seal();
136
139
  }
137
140
  }
138
141
 
@@ -160,28 +163,29 @@ function addExternalEntities(externalEntities) {
160
163
  * @param {boolean} escapeEntities
161
164
  */
162
165
  function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) {
166
+ const options = this.options;
163
167
  if (val !== undefined) {
164
- if (this.options.trimValues && !dontTrim) {
168
+ if (options.trimValues && !dontTrim) {
165
169
  val = val.trim();
166
170
  }
167
171
  if (val.length > 0) {
168
172
  if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);
169
173
 
170
174
  // Pass jPath string or matcher based on options.jPath setting
171
- const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
172
- const newval = this.options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode);
175
+ const jPathOrMatcher = options.jPath ? jPath.toString() : jPath;
176
+ const newval = options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode);
173
177
  if (newval === null || newval === undefined) {
174
178
  //don't parse
175
179
  return val;
176
180
  } else if (typeof newval !== typeof val || newval !== val) {
177
181
  //overwrite
178
182
  return newval;
179
- } else if (this.options.trimValues) {
180
- return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
183
+ } else if (options.trimValues) {
184
+ return parseValue(val, options.parseTagValue, options.numberParseOptions);
181
185
  } else {
182
186
  const trimmedVal = val.trim();
183
187
  if (trimmedVal === val) {
184
- return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
188
+ return parseValue(val, options.parseTagValue, options.numberParseOptions);
185
189
  } else {
186
190
  return val;
187
191
  }
@@ -209,7 +213,8 @@ function resolveNameSpace(tagname) {
209
213
  const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm');
210
214
 
211
215
  function buildAttributesMap(attrStr, jPath, tagName) {
212
- if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') {
216
+ const options = this.options;
217
+ if (options.ignoreAttributes !== true && typeof attrStr === 'string') {
213
218
  // attrStr = attrStr.replace(/\r?\n/g, ' ');
214
219
  //attrStr = attrStr || attrStr.trim();
215
220
 
@@ -229,7 +234,7 @@ function buildAttributesMap(attrStr, jPath, tagName) {
229
234
 
230
235
  if (attrName.length && oldVal !== undefined) {
231
236
  let val = oldVal;
232
- if (this.options.trimValues) val = val.trim();
237
+ if (options.trimValues) val = val.trim();
233
238
  val = this.replaceEntitiesValue(val, tagName, this.readonlyMatcher);
234
239
  processedVals[i] = val;
235
240
 
@@ -244,7 +249,7 @@ function buildAttributesMap(attrStr, jPath, tagName) {
244
249
  }
245
250
 
246
251
  // Hoist toString() once — path doesn't change during attribute processing
247
- const jPathStr = this.options.jPath ? jPath.toString() : this.readonlyMatcher;
252
+ const jPathStr = options.jPath ? jPath.toString() : this.readonlyMatcher;
248
253
 
249
254
  // Second pass: apply processors, build final attrs
250
255
  let hasAttrs = false;
@@ -253,28 +258,28 @@ function buildAttributesMap(attrStr, jPath, tagName) {
253
258
 
254
259
  if (this.ignoreAttributesFn(attrName, jPathStr)) continue;
255
260
 
256
- let aName = this.options.attributeNamePrefix + attrName;
261
+ let aName = options.attributeNamePrefix + attrName;
257
262
 
258
263
  if (attrName.length) {
259
- if (this.options.transformAttributeName) {
260
- aName = this.options.transformAttributeName(aName);
264
+ if (options.transformAttributeName) {
265
+ aName = options.transformAttributeName(aName);
261
266
  }
262
- aName = sanitizeName(aName, this.options);
267
+ aName = sanitizeName(aName, options);
263
268
 
264
269
  if (matches[i][4] !== undefined) {
265
270
  // Reuse already-processed value — no double entity replacement
266
271
  const oldVal = processedVals[i];
267
272
 
268
- const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPathStr);
273
+ const newVal = options.attributeValueProcessor(attrName, oldVal, jPathStr);
269
274
  if (newVal === null || newVal === undefined) {
270
275
  attrs[aName] = oldVal;
271
276
  } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) {
272
277
  attrs[aName] = newVal;
273
278
  } else {
274
- attrs[aName] = parseValue(oldVal, this.options.parseAttributeValue, this.options.numberParseOptions);
279
+ attrs[aName] = parseValue(oldVal, options.parseAttributeValue, options.numberParseOptions);
275
280
  }
276
281
  hasAttrs = true;
277
- } else if (this.options.allowBooleanAttributes) {
282
+ } else if (options.allowBooleanAttributes) {
278
283
  attrs[aName] = true;
279
284
  hasAttrs = true;
280
285
  }
@@ -283,9 +288,9 @@ function buildAttributesMap(attrStr, jPath, tagName) {
283
288
 
284
289
  if (!hasAttrs) return;
285
290
 
286
- if (this.options.attributesGroupName) {
291
+ if (options.attributesGroupName) {
287
292
  const attrCollection = {};
288
- attrCollection[this.options.attributesGroupName] = attrs;
293
+ attrCollection[options.attributesGroupName] = attrs;
289
294
  return attrCollection;
290
295
  }
291
296
  return attrs;
@@ -303,25 +308,30 @@ const parseXml = function (xmlData) {
303
308
  // Reset entity expansion counters for this document
304
309
  this.entityExpansionCount = 0;
305
310
  this.currentExpandedLength = 0;
306
-
307
- const docTypeReader = new DocTypeReader(this.options.processEntities);
308
- for (let i = 0; i < xmlData.length; i++) {//for each char in XML data
311
+ this.docTypeEntitiesKeys = [];
312
+ this.lastEntitiesKeys = Object.keys(this.lastEntities);
313
+ this.htmlEntitiesKeys = this.options.htmlEntities ? Object.keys(this.htmlEntities) : [];
314
+ const options = this.options;
315
+ const docTypeReader = new DocTypeReader(options.processEntities);
316
+ const xmlLen = xmlData.length;
317
+ for (let i = 0; i < xmlLen; i++) {//for each char in XML data
309
318
  const ch = xmlData[i];
310
319
  if (ch === '<') {
311
320
  // const nextIndex = i+1;
312
321
  // const _2ndChar = xmlData[nextIndex];
313
- if (xmlData[i + 1] === '/') {//Closing Tag
322
+ const c1 = xmlData.charCodeAt(i + 1);
323
+ if (c1 === 47) {//Closing Tag '/'
314
324
  const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.")
315
325
  let tagName = xmlData.substring(i + 2, closeIndex).trim();
316
326
 
317
- if (this.options.removeNSPrefix) {
327
+ if (options.removeNSPrefix) {
318
328
  const colonIndex = tagName.indexOf(":");
319
329
  if (colonIndex !== -1) {
320
330
  tagName = tagName.substr(colonIndex + 1);
321
331
  }
322
332
  }
323
333
 
324
- tagName = transformTagName(this.options.transformTagName, tagName, "", this.options).tagName;
334
+ tagName = transformTagName(options.transformTagName, tagName, "", options).tagName;
325
335
 
326
336
  if (currentNode) {
327
337
  textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
@@ -329,10 +339,10 @@ const parseXml = function (xmlData) {
329
339
 
330
340
  //check if last tag of nested tag was unpaired tag
331
341
  const lastTagName = this.matcher.getCurrentTag();
332
- if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) {
342
+ if (tagName && options.unpairedTagsSet.has(tagName)) {
333
343
  throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
334
344
  }
335
- if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) {
345
+ if (lastTagName && options.unpairedTagsSet.has(lastTagName)) {
336
346
  // Pop the unpaired tag
337
347
  this.matcher.pop();
338
348
  this.tagsNodeStack.pop();
@@ -344,18 +354,18 @@ const parseXml = function (xmlData) {
344
354
  currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
345
355
  textData = "";
346
356
  i = closeIndex;
347
- } else if (xmlData[i + 1] === '?') {
357
+ } else if (c1 === 63) { //'?'
348
358
 
349
359
  let tagData = readTagExp(xmlData, i, false, "?>");
350
360
  if (!tagData) throw new Error("Pi Tag is not closed.");
351
361
 
352
362
  textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
353
- if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) {
363
+ if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) {
354
364
  //do nothing
355
365
  } else {
356
366
 
357
367
  const childNode = new xmlNode(tagData.tagName);
358
- childNode.add(this.options.textNodeName, "");
368
+ childNode.add(options.textNodeName, "");
359
369
 
360
370
  if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
361
371
  childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName);
@@ -365,21 +375,26 @@ const parseXml = function (xmlData) {
365
375
 
366
376
 
367
377
  i = tagData.closeIndex + 1;
368
- } else if (xmlData.substr(i + 1, 3) === '!--') {
378
+ } else if (c1 === 33
379
+ && xmlData.charCodeAt(i + 2) === 45
380
+ && xmlData.charCodeAt(i + 3) === 45) { //'!--'
369
381
  const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.")
370
- if (this.options.commentPropName) {
382
+ if (options.commentPropName) {
371
383
  const comment = xmlData.substring(i + 4, endIndex - 2);
372
384
 
373
385
  textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
374
386
 
375
- currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]);
387
+ currentNode.add(options.commentPropName, [{ [options.textNodeName]: comment }]);
376
388
  }
377
389
  i = endIndex;
378
- } else if (xmlData.substr(i + 1, 2) === '!D') {
390
+ } else if (c1 === 33
391
+ && xmlData.charCodeAt(i + 2) === 68) { //'!D'
379
392
  const result = docTypeReader.readDocType(xmlData, i);
380
393
  this.docTypeEntities = result.entities;
394
+ this.docTypeEntitiesKeys = Object.keys(this.docTypeEntities) || []
381
395
  i = result.i;
382
- } else if (xmlData.substr(i + 1, 2) === '![') {
396
+ } else if (c1 === 33
397
+ && xmlData.charCodeAt(i + 2) === 91) { // '!['
383
398
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
384
399
  const tagExp = xmlData.substring(i + 9, closeIndex);
385
400
 
@@ -389,20 +404,20 @@ const parseXml = function (xmlData) {
389
404
  if (val == undefined) val = "";
390
405
 
391
406
  //cdata should be set even if it is 0 length string
392
- if (this.options.cdataPropName) {
393
- currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]);
407
+ if (options.cdataPropName) {
408
+ currentNode.add(options.cdataPropName, [{ [options.textNodeName]: tagExp }]);
394
409
  } else {
395
- currentNode.add(this.options.textNodeName, val);
410
+ currentNode.add(options.textNodeName, val);
396
411
  }
397
412
 
398
413
  i = closeIndex + 2;
399
414
  } else {//Opening tag
400
- let result = readTagExp(xmlData, i, this.options.removeNSPrefix);
415
+ let result = readTagExp(xmlData, i, options.removeNSPrefix);
401
416
 
402
417
  // Safety check: readTagExp can return undefined
403
418
  if (!result) {
404
419
  // Log context for debugging
405
- const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlData.length, i + 50));
420
+ const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlLen, i + 50));
406
421
  throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`);
407
422
  }
408
423
 
@@ -412,13 +427,13 @@ const parseXml = function (xmlData) {
412
427
  let attrExpPresent = result.attrExpPresent;
413
428
  let closeIndex = result.closeIndex;
414
429
 
415
- ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options));
430
+ ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options));
416
431
 
417
- if (this.options.strictReservedNames &&
418
- (tagName === this.options.commentPropName
419
- || tagName === this.options.cdataPropName
420
- || tagName === this.options.textNodeName
421
- || tagName === this.options.attributesGroupName
432
+ if (options.strictReservedNames &&
433
+ (tagName === options.commentPropName
434
+ || tagName === options.cdataPropName
435
+ || tagName === options.textNodeName
436
+ || tagName === options.attributesGroupName
422
437
  )) {
423
438
  throw new Error(`Invalid tag name: ${tagName}`);
424
439
  }
@@ -433,7 +448,7 @@ const parseXml = function (xmlData) {
433
448
 
434
449
  //check if last tag was unpaired tag
435
450
  const lastTag = currentNode;
436
- if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) {
451
+ if (lastTag && options.unpairedTagsSet.has(lastTag.tagname)) {
437
452
  currentNode = this.tagsNodeStack.pop();
438
453
  this.matcher.pop();
439
454
  }
@@ -475,13 +490,13 @@ const parseXml = function (xmlData) {
475
490
 
476
491
  if (prefixedAttrs) {
477
492
  // Extract raw attributes (without prefix) for our use
478
- rawAttrs = extractRawAttributes(prefixedAttrs, this.options);
493
+ rawAttrs = extractRawAttributes(prefixedAttrs, options);
479
494
  }
480
495
  }
481
496
 
482
497
  // Now check if this is a stop node (after attributes are set)
483
498
  if (tagName !== xmlObj.tagname) {
484
- this.isCurrentNodeStopNode = this.isItStopNode(this.stopNodeExpressions, this.matcher);
499
+ this.isCurrentNodeStopNode = this.isItStopNode();
485
500
  }
486
501
 
487
502
  const startIndex = i;
@@ -493,7 +508,7 @@ const parseXml = function (xmlData) {
493
508
  i = result.closeIndex;
494
509
  }
495
510
  //unpaired tag
496
- else if (this.options.unpairedTags.indexOf(tagName) !== -1) {
511
+ else if (options.unpairedTagsSet.has(tagName)) {
497
512
  i = result.closeIndex;
498
513
  }
499
514
  //normal tag
@@ -512,7 +527,7 @@ const parseXml = function (xmlData) {
512
527
  }
513
528
 
514
529
  // For stop nodes, store raw content as-is without any processing
515
- childNode.add(this.options.textNodeName, tagContent);
530
+ childNode.add(options.textNodeName, tagContent);
516
531
 
517
532
  this.matcher.pop(); // Pop the stop node tag
518
533
  this.isCurrentNodeStopNode = false; // Reset flag
@@ -521,7 +536,7 @@ const parseXml = function (xmlData) {
521
536
  } else {
522
537
  //selfClosing tag
523
538
  if (isSelfClosing) {
524
- ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options));
539
+ ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options));
525
540
 
526
541
  const childNode = new xmlNode(tagName);
527
542
  if (prefixedAttrs) {
@@ -531,7 +546,7 @@ const parseXml = function (xmlData) {
531
546
  this.matcher.pop(); // Pop self-closing tag
532
547
  this.isCurrentNodeStopNode = false; // Reset flag
533
548
  }
534
- else if (this.options.unpairedTags.indexOf(tagName) !== -1) {//unpaired tag
549
+ else if (options.unpairedTagsSet.has(tagName)) {//unpaired tag
535
550
  const childNode = new xmlNode(tagName);
536
551
  if (prefixedAttrs) {
537
552
  childNode[":@"] = prefixedAttrs;
@@ -546,7 +561,7 @@ const parseXml = function (xmlData) {
546
561
  //opening tag
547
562
  else {
548
563
  const childNode = new xmlNode(tagName);
549
- if (this.tagsNodeStack.length > this.options.maxNestedTags) {
564
+ if (this.tagsNodeStack.length > options.maxNestedTags) {
550
565
  throw new Error("Maximum nested tags exceeded");
551
566
  }
552
567
  this.tagsNodeStack.push(currentNode);
@@ -618,7 +633,7 @@ function replaceEntitiesValue(val, tagName, jPath) {
618
633
  }
619
634
 
620
635
  // Replace DOCTYPE entities
621
- for (const entityName of Object.keys(this.docTypeEntities)) {
636
+ for (const entityName of this.docTypeEntitiesKeys) {
622
637
  const entity = this.docTypeEntities[entityName];
623
638
  const matches = val.match(entity.regx);
624
639
 
@@ -652,7 +667,7 @@ function replaceEntitiesValue(val, tagName, jPath) {
652
667
  }
653
668
  if (val.indexOf('&') === -1) return val;
654
669
  // Replace standard entities
655
- for (const entityName of Object.keys(this.lastEntities)) {
670
+ for (const entityName of this.lastEntitiesKeys) {
656
671
  const entity = this.lastEntities[entityName];
657
672
  const matches = val.match(entity.regex);
658
673
  if (matches) {
@@ -669,22 +684,20 @@ function replaceEntitiesValue(val, tagName, jPath) {
669
684
  if (val.indexOf('&') === -1) return val;
670
685
 
671
686
  // Replace HTML entities if enabled
672
- if (this.options.htmlEntities) {
673
- for (const entityName of Object.keys(this.htmlEntities)) {
674
- const entity = this.htmlEntities[entityName];
675
- const matches = val.match(entity.regex);
676
- if (matches) {
677
- //console.log(matches);
678
- this.entityExpansionCount += matches.length;
679
- if (entityConfig.maxTotalExpansions &&
680
- this.entityExpansionCount > entityConfig.maxTotalExpansions) {
681
- throw new Error(
682
- `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
683
- );
684
- }
687
+ for (const entityName of this.htmlEntitiesKeys) {
688
+ const entity = this.htmlEntities[entityName];
689
+ const matches = val.match(entity.regex);
690
+ if (matches) {
691
+ //console.log(matches);
692
+ this.entityExpansionCount += matches.length;
693
+ if (entityConfig.maxTotalExpansions &&
694
+ this.entityExpansionCount > entityConfig.maxTotalExpansions) {
695
+ throw new Error(
696
+ `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
697
+ );
685
698
  }
686
- val = val.replace(entity.regex, entity.val);
687
699
  }
700
+ val = val.replace(entity.regex, entity.val);
688
701
  }
689
702
 
690
703
  // Replace ampersand entity last
@@ -712,20 +725,14 @@ function saveTextToParentTag(textData, parentNode, matcher, isLeafNode) {
712
725
  return textData;
713
726
  }
714
727
 
715
- //TODO: use jPath to simplify the logic
716
728
  /**
717
729
  * @param {Array<Expression>} stopNodeExpressions - Array of compiled Expression objects
718
730
  * @param {Matcher} matcher - Current path matcher
719
731
  */
720
- function isItStopNode(stopNodeExpressions, matcher) {
721
- if (!stopNodeExpressions || stopNodeExpressions.length === 0) return false;
732
+ function isItStopNode() {
733
+ if (this.stopNodeExpressionsSet.size === 0) return false;
722
734
 
723
- for (let i = 0; i < stopNodeExpressions.length; i++) {
724
- if (matcher.matches(stopNodeExpressions[i])) {
725
- return true;
726
- }
727
- }
728
- return false;
735
+ return this.matcher.matchesAny(this.stopNodeExpressionsSet);
729
736
  }
730
737
 
731
738
  /**
@@ -735,32 +742,33 @@ function isItStopNode(stopNodeExpressions, matcher) {
735
742
  * @returns
736
743
  */
737
744
  function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
738
- let attrBoundary;
739
- let tagExp = "";
740
- for (let index = i; index < xmlData.length; index++) {
741
- let ch = xmlData[index];
745
+ let attrBoundary = 0;
746
+ const chars = [];
747
+ const len = xmlData.length;
748
+ const closeCode0 = closingChar.charCodeAt(0);
749
+ const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1;
750
+
751
+ for (let index = i; index < len; index++) {
752
+ const code = xmlData.charCodeAt(index);
753
+
742
754
  if (attrBoundary) {
743
- if (ch === attrBoundary) attrBoundary = "";//reset
744
- } else if (ch === '"' || ch === "'") {
745
- attrBoundary = ch;
746
- } else if (ch === closingChar[0]) {
747
- if (closingChar[1]) {
748
- if (xmlData[index + 1] === closingChar[1]) {
749
- return {
750
- data: tagExp,
751
- index: index
752
- }
755
+ if (code === attrBoundary) attrBoundary = 0;
756
+ } else if (code === 34 || code === 39) { // " or '
757
+ attrBoundary = code;
758
+ } else if (code === closeCode0) {
759
+ if (closeCode1 !== -1) {
760
+ if (xmlData.charCodeAt(index + 1) === closeCode1) {
761
+ return { data: String.fromCharCode(...chars), index };
753
762
  }
754
763
  } else {
755
- return {
756
- data: tagExp,
757
- index: index
758
- }
764
+ return { data: String.fromCharCode(...chars), index };
759
765
  }
760
- } else if (ch === '\t') {
761
- ch = " "
766
+ } else if (code === 9) { // \t
767
+ chars.push(32); // space
768
+ continue;
762
769
  }
763
- tagExp += ch;
770
+
771
+ chars.push(code);
764
772
  }
765
773
  }
766
774
 
@@ -773,6 +781,12 @@ function findClosingIndex(xmlData, str, i, errMsg) {
773
781
  }
774
782
  }
775
783
 
784
+ function findClosingChar(xmlData, char, i, errMsg) {
785
+ const closingIndex = xmlData.indexOf(char, i);
786
+ if (closingIndex === -1) throw new Error(errMsg);
787
+ return closingIndex; // no offset needed
788
+ }
789
+
776
790
  function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") {
777
791
  const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar);
778
792
  if (!result) return;
@@ -814,10 +828,12 @@ function readStopNodeData(xmlData, tagName, i) {
814
828
  // Starting at 1 since we already have an open tag
815
829
  let openTagCount = 1;
816
830
 
817
- for (; i < xmlData.length; i++) {
831
+ const xmllen = xmlData.length;
832
+ for (; i < xmllen; i++) {
818
833
  if (xmlData[i] === "<") {
819
- if (xmlData[i + 1] === "/") {//close tag
820
- const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
834
+ const c1 = xmlData.charCodeAt(i + 1);
835
+ if (c1 === 47) {//close tag '/'
836
+ const closeIndex = findClosingChar(xmlData, ">", i, `${tagName} is not closed`);
821
837
  let closeTagName = xmlData.substring(i + 2, closeIndex).trim();
822
838
  if (closeTagName === tagName) {
823
839
  openTagCount--;
@@ -829,13 +845,16 @@ function readStopNodeData(xmlData, tagName, i) {
829
845
  }
830
846
  }
831
847
  i = closeIndex;
832
- } else if (xmlData[i + 1] === '?') {
848
+ } else if (c1 === 63) { //?
833
849
  const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.")
834
850
  i = closeIndex;
835
- } else if (xmlData.substr(i + 1, 3) === '!--') {
851
+ } else if (c1 === 33
852
+ && xmlData.charCodeAt(i + 2) === 45
853
+ && xmlData.charCodeAt(i + 3) === 45) { // '!--'
836
854
  const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.")
837
855
  i = closeIndex;
838
- } else if (xmlData.substr(i + 1, 2) === '![') {
856
+ } else if (c1 === 33
857
+ && xmlData.charCodeAt(i + 2) === 91) { // '!['
839
858
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
840
859
  i = closeIndex;
841
860
  } else {