• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NaturalIntelligence / fast-xml-parser / 22910077419

10 Mar 2026 03:23PM UTC coverage: 97.837% (+0.1%) from 97.689%
22910077419

push

github

amitguptagwl
update dependency

1110 of 1154 branches covered (96.19%)

9228 of 9432 relevant lines covered (97.84%)

473155.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.84
/src/xmlparser/OrderedObjParser.js
1
'use strict';
5✔
2
///@ts-check
5✔
3

5✔
4
import { getAllMatches, isExist } from '../util.js';
5✔
5
import xmlNode from './xmlNode.js';
5✔
6
import DocTypeReader from './DocTypeReader.js';
5✔
7
import toNumber from "strnum";
5✔
8
import getIgnoreAttributesFn from "../ignoreAttributes.js";
5✔
9
import { Expression, Matcher } from 'path-expression-matcher';
5✔
10

5✔
11

5✔
12
// const regx =
5✔
13
//   '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
5✔
14
//   .replace(/NAME/g, util.nameRegexp);
5✔
15

5✔
16
//const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g");
5✔
17
//const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g");
5✔
18

5✔
19
// Helper functions for attribute and namespace handling
5✔
20

5✔
21
/**
5✔
22
 * Extract raw attributes (without prefix) from prefixed attribute map
5✔
23
 * @param {object} prefixedAttrs - Attributes with prefix from buildAttributesMap
5✔
24
 * @param {object} options - Parser options containing attributeNamePrefix
5✔
25
 * @returns {object} Raw attributes for matcher
5✔
26
 */
5✔
27
function extractRawAttributes(prefixedAttrs, options) {
760✔
28
  if (!prefixedAttrs) return {};
760!
29

760✔
30
  // Handle attributesGroupName option
760✔
31
  const attrs = options.attributesGroupName
760✔
32
    ? prefixedAttrs[options.attributesGroupName]
760✔
33
    : prefixedAttrs;
760✔
34

760✔
35
  if (!attrs) return {};
760!
36

760✔
37
  const rawAttrs = {};
760✔
38
  for (const key in attrs) {
760✔
39
    // Remove the attribute prefix to get raw name
1,501✔
40
    if (key.startsWith(options.attributeNamePrefix)) {
1,501✔
41
      const rawName = key.substring(options.attributeNamePrefix.length);
1,227✔
42
      rawAttrs[rawName] = attrs[key];
1,227✔
43
    } else {
1,501✔
44
      // Attribute without prefix (shouldn't normally happen, but be safe)
274✔
45
      rawAttrs[key] = attrs[key];
274✔
46
    }
274✔
47
  }
1,501✔
48
  return rawAttrs;
760✔
49
}
760✔
50

5✔
51
/**
5✔
52
 * Extract namespace from raw tag name
5✔
53
 * @param {string} rawTagName - Tag name possibly with namespace (e.g., "soap:Envelope")
5✔
54
 * @returns {string|undefined} Namespace or undefined
5✔
55
 */
5✔
56
function extractNamespace(rawTagName) {
6,275✔
57
  if (!rawTagName || typeof rawTagName !== 'string') return undefined;
6,275!
58

6,275✔
59
  const colonIndex = rawTagName.indexOf(':');
6,275✔
60
  if (colonIndex !== -1 && colonIndex > 0) {
6,275✔
61
    const ns = rawTagName.substring(0, colonIndex);
150✔
62
    // Don't treat xmlns as a namespace
150✔
63
    if (ns !== 'xmlns') {
150✔
64
      return ns;
150✔
65
    }
150✔
66
  }
150✔
67
  return undefined;
6,125✔
68
}
6,275✔
69

5✔
70
export default class OrderedObjParser {
5✔
71
  constructor(options) {
5✔
72
    this.options = options;
1,595✔
73
    this.currentNode = null;
1,595✔
74
    this.tagsNodeStack = [];
1,595✔
75
    this.docTypeEntities = {};
1,595✔
76
    this.lastEntities = {
1,595✔
77
      "apos": { regex: /&(apos|#39|#x27);/g, val: "'" },
1,595✔
78
      "gt": { regex: /&(gt|#62|#x3E);/g, val: ">" },
1,595✔
79
      "lt": { regex: /&(lt|#60|#x3C);/g, val: "<" },
1,595✔
80
      "quot": { regex: /&(quot|#34|#x22);/g, val: "\"" },
1,595✔
81
    };
1,595✔
82
    this.ampEntity = { regex: /&(amp|#38|#x26);/g, val: "&" };
1,595✔
83
    this.htmlEntities = {
1,595✔
84
      "space": { regex: /&(nbsp|#160);/g, val: " " },
1,595✔
85
      // "lt" : { regex: /&(lt|#60);/g, val: "<" },
1,595✔
86
      // "gt" : { regex: /&(gt|#62);/g, val: ">" },
1,595✔
87
      // "amp" : { regex: /&(amp|#38);/g, val: "&" },
1,595✔
88
      // "quot" : { regex: /&(quot|#34);/g, val: "\"" },
1,595✔
89
      // "apos" : { regex: /&(apos|#39);/g, val: "'" },
1,595✔
90
      "cent": { regex: /&(cent|#162);/g, val: "¢" },
1,595✔
91
      "pound": { regex: /&(pound|#163);/g, val: "£" },
1,595✔
92
      "yen": { regex: /&(yen|#165);/g, val: "¥" },
1,595✔
93
      "euro": { regex: /&(euro|#8364);/g, val: "€" },
1,595✔
94
      "copyright": { regex: /&(copy|#169);/g, val: "©" },
1,595✔
95
      "reg": { regex: /&(reg|#174);/g, val: "®" },
1,595✔
96
      "inr": { regex: /&(inr|#8377);/g, val: "₹" },
1,595✔
97
      "num_dec": { regex: /&#([0-9]{1,7});/g, val: (_, str) => fromCodePoint(str, 10, "&#") },
1,595✔
98
      "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => fromCodePoint(str, 16, "&#x") },
1,595✔
99
    };
1,595✔
100
    this.addExternalEntities = addExternalEntities;
1,595✔
101
    this.parseXml = parseXml;
1,595✔
102
    this.parseTextData = parseTextData;
1,595✔
103
    this.resolveNameSpace = resolveNameSpace;
1,595✔
104
    this.buildAttributesMap = buildAttributesMap;
1,595✔
105
    this.isItStopNode = isItStopNode;
1,595✔
106
    this.replaceEntitiesValue = replaceEntitiesValue;
1,595✔
107
    this.readStopNodeData = readStopNodeData;
1,595✔
108
    this.saveTextToParentTag = saveTextToParentTag;
1,595✔
109
    this.addChild = addChild;
1,595✔
110
    this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes)
1,595✔
111
    this.entityExpansionCount = 0;
1,595✔
112
    this.currentExpandedLength = 0;
1,595✔
113

1,595✔
114
    // Initialize path matcher for path-expression-matcher
1,595✔
115
    this.matcher = new Matcher();
1,595✔
116

1,595✔
117
    // Flag to track if current node is a stop node (optimization)
1,595✔
118
    this.isCurrentNodeStopNode = false;
1,595✔
119

1,595✔
120
    // Pre-compile stopNodes expressions
1,595✔
121
    if (this.options.stopNodes && this.options.stopNodes.length > 0) {
1,595✔
122
      this.stopNodeExpressions = [];
670✔
123
      for (let i = 0; i < this.options.stopNodes.length; i++) {
670✔
124
        const stopNodeExp = this.options.stopNodes[i];
1,255✔
125
        if (typeof stopNodeExp === 'string') {
1,255✔
126
          // Convert string to Expression object
200✔
127
          this.stopNodeExpressions.push(new Expression(stopNodeExp));
200✔
128
        } else if (stopNodeExp instanceof Expression) {
1,255✔
129
          // Already an Expression object
1,055✔
130
          this.stopNodeExpressions.push(stopNodeExp);
1,055✔
131
        }
1,055✔
132
      }
1,255✔
133
    }
670✔
134
  }
1,595✔
135

5✔
136
}
5✔
137

5✔
138
function addExternalEntities(externalEntities) {
1,595✔
139
  const entKeys = Object.keys(externalEntities);
1,595✔
140
  for (let i = 0; i < entKeys.length; i++) {
1,595✔
141
    const ent = entKeys[i];
15✔
142
    const escaped = ent.replace(/[.\-+*:]/g, '\\.');
15✔
143
    this.lastEntities[ent] = {
15✔
144
      regex: new RegExp("&" + escaped + ";", "g"),
15✔
145
      val: externalEntities[ent]
15✔
146
    }
15✔
147
  }
15✔
148
}
1,595✔
149

5✔
150
/**
5✔
151
 * @param {string} val
5✔
152
 * @param {string} tagName
5✔
153
 * @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
5✔
154
 * @param {boolean} dontTrim
5✔
155
 * @param {boolean} hasAttributes
5✔
156
 * @param {boolean} isLeafNode
5✔
157
 * @param {boolean} escapeEntities
5✔
158
 */
5✔
159
function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) {
6,690✔
160
  if (val !== undefined) {
6,690✔
161
    if (this.options.trimValues && !dontTrim) {
6,690✔
162
      val = val.trim();
6,445✔
163
    }
6,445✔
164
    if (val.length > 0) {
6,690✔
165
      if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);
1,985✔
166

1,955✔
167
      // Pass jPath string or matcher based on options.jPath setting
1,955✔
168
      const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
1,985✔
169
      const newval = this.options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode);
1,985✔
170
      if (newval === null || newval === undefined) {
1,985✔
171
        //don't parse
50✔
172
        return val;
50✔
173
      } else if (typeof newval !== typeof val || newval !== val) {
1,985✔
174
        //overwrite
20✔
175
        return newval;
20✔
176
      } else if (this.options.trimValues) {
1,905✔
177
        return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
1,825✔
178
      } else {
1,885✔
179
        const trimmedVal = val.trim();
60✔
180
        if (trimmedVal === val) {
60✔
181
          return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
15✔
182
        } else {
60✔
183
          return val;
45✔
184
        }
45✔
185
      }
60✔
186
    }
1,985✔
187
  }
6,690✔
188
}
6,690✔
189

5✔
190
function resolveNameSpace(tagname) {
2,850✔
191
  if (this.options.removeNSPrefix) {
2,850✔
192
    const tags = tagname.split(':');
220✔
193
    const prefix = tagname.charAt(0) === '/' ? '/' : '';
220!
194
    if (tags[0] === 'xmlns') {
220✔
195
      return '';
70✔
196
    }
70✔
197
    if (tags.length === 2) {
220✔
198
      tagname = prefix + tags[1];
60✔
199
    }
60✔
200
  }
220✔
201
  return tagname;
2,780✔
202
}
2,850✔
203

5✔
204
//TODO: change regex to capture NS
5✔
205
//const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm");
5✔
206
const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm');
5✔
207

5✔
208
function buildAttributesMap(attrStr, jPath, tagName) {
1,140✔
209
  if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') {
1,140✔
210
    // attrStr = attrStr.replace(/\r?\n/g, ' ');
975✔
211
    //attrStr = attrStr || attrStr.trim();
975✔
212

975✔
213
    const matches = getAllMatches(attrStr, attrsRegx);
975✔
214
    const len = matches.length; //don't make it inline
975✔
215
    const attrs = {};
975✔
216

975✔
217
    // First pass: parse all attributes and update matcher with raw values
975✔
218
    // This ensures the matcher has all attribute values when processors run
975✔
219
    const rawAttrsForMatcher = {};
975✔
220
    for (let i = 0; i < len; i++) {
975✔
221
      const attrName = this.resolveNameSpace(matches[i][1]);
1,425✔
222
      const oldVal = matches[i][4];
1,425✔
223

1,425✔
224
      if (attrName.length && oldVal !== undefined) {
1,425✔
225
        let parsedVal = oldVal;
1,300✔
226
        if (this.options.trimValues) {
1,300✔
227
          parsedVal = parsedVal.trim();
1,275✔
228
        }
1,275✔
229
        parsedVal = this.replaceEntitiesValue(parsedVal, tagName, jPath);
1,300✔
230
        rawAttrsForMatcher[attrName] = parsedVal;
1,300✔
231
      }
1,300✔
232
    }
1,425✔
233

975✔
234
    // Update matcher with raw attribute values BEFORE running processors
975✔
235
    if (Object.keys(rawAttrsForMatcher).length > 0 && typeof jPath === 'object' && jPath.updateCurrent) {
975✔
236
      jPath.updateCurrent(rawAttrsForMatcher);
855✔
237
    }
855✔
238

975✔
239
    // Second pass: now process attributes with matcher having full attribute context
975✔
240
    for (let i = 0; i < len; i++) {
975✔
241
      const attrName = this.resolveNameSpace(matches[i][1]);
1,425✔
242

1,425✔
243
      // Convert jPath to string if needed for ignoreAttributesFn
1,425✔
244
      const jPathStr = this.options.jPath ? jPath.toString() : jPath;
1,425✔
245
      if (this.ignoreAttributesFn(attrName, jPathStr)) {
1,425✔
246
        continue
55✔
247
      }
55✔
248

1,370✔
249
      let oldVal = matches[i][4];
1,370✔
250
      let aName = this.options.attributeNamePrefix + attrName;
1,370✔
251

1,370✔
252
      if (attrName.length) {
1,425✔
253
        if (this.options.transformAttributeName) {
1,335✔
254
          aName = this.options.transformAttributeName(aName);
20✔
255
        }
20✔
256
        if (aName === "__proto__") aName = "#__proto__";
1,335!
257

1,335✔
258
        if (oldVal !== undefined) {
1,335✔
259
          if (this.options.trimValues) {
1,245✔
260
            oldVal = oldVal.trim();
1,220✔
261
          }
1,220✔
262
          oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath);
1,245✔
263

1,245✔
264
          // Pass jPath string or matcher based on options.jPath setting
1,245✔
265
          const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
1,245✔
266
          const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPathOrMatcher);
1,245✔
267
          if (newVal === null || newVal === undefined) {
1,245!
268
            //don't parse
×
269
            attrs[aName] = oldVal;
×
270
          } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) {
1,245✔
271
            //overwrite
5✔
272
            attrs[aName] = newVal;
5✔
273
          } else {
1,245✔
274
            //parse
1,240✔
275
            attrs[aName] = parseValue(
1,240✔
276
              oldVal,
1,240✔
277
              this.options.parseAttributeValue,
1,240✔
278
              this.options.numberParseOptions
1,240✔
279
            );
1,240✔
280
          }
1,240✔
281
        } else if (this.options.allowBooleanAttributes) {
1,335✔
282
          attrs[aName] = true;
80✔
283
        }
80✔
284
      }
1,335✔
285
    }
1,425✔
286

975✔
287
    if (!Object.keys(attrs).length) {
975✔
288
      return;
95✔
289
    }
95✔
290
    if (this.options.attributesGroupName) {
975✔
291
      const attrCollection = {};
60✔
292
      attrCollection[this.options.attributesGroupName] = attrs;
60✔
293
      return attrCollection;
60✔
294
    }
60✔
295
    return attrs
820✔
296
  }
820✔
297
}
1,140✔
298

5✔
299
const parseXml = function (xmlData) {
5✔
300
  xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line
1,595✔
301
  const xmlObj = new xmlNode('!xml');
1,595✔
302
  let currentNode = xmlObj;
1,595✔
303
  let textData = "";
1,595✔
304

1,595✔
305
  // Reset matcher for new document
1,595✔
306
  this.matcher.reset();
1,595✔
307

1,595✔
308
  // Reset entity expansion counters for this document
1,595✔
309
  this.entityExpansionCount = 0;
1,595✔
310
  this.currentExpandedLength = 0;
1,595✔
311

1,595✔
312
  const docTypeReader = new DocTypeReader(this.options.processEntities);
1,595✔
313
  for (let i = 0; i < xmlData.length; i++) {//for each char in XML data
1,595✔
314
    const ch = xmlData[i];
324,800✔
315
    if (ch === '<') {
324,800✔
316
      // const nextIndex = i+1;
11,025✔
317
      // const _2ndChar = xmlData[nextIndex];
11,025✔
318
      if (xmlData[i + 1] === '/') {//Closing Tag
11,025✔
319
        const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.")
4,030✔
320
        let tagName = xmlData.substring(i + 2, closeIndex).trim();
4,030✔
321

4,030✔
322
        if (this.options.removeNSPrefix) {
4,030✔
323
          const colonIndex = tagName.indexOf(":");
115✔
324
          if (colonIndex !== -1) {
115✔
325
            tagName = tagName.substr(colonIndex + 1);
50✔
326
          }
50✔
327
        }
115✔
328

4,025✔
329
        if (this.options.transformTagName) {
4,030✔
330
          tagName = this.options.transformTagName(tagName);
75✔
331
        }
75✔
332

4,025✔
333
        if (currentNode) {
4,025✔
334
          textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
4,025✔
335
        }
4,025✔
336

3,995✔
337
        //check if last tag of nested tag was unpaired tag
3,995✔
338
        const lastTagName = this.matcher.getCurrentTag();
3,995✔
339
        if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) {
4,030✔
340
          throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
5✔
341
        }
5✔
342
        if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) {
4,030!
343
          // Pop the unpaired tag
×
344
          this.matcher.pop();
×
345
          this.tagsNodeStack.pop();
×
346
        }
×
347
        // Pop the closing tag
3,990✔
348
        this.matcher.pop();
3,990✔
349
        this.isCurrentNodeStopNode = false; // Reset flag when closing tag
3,990✔
350

3,990✔
351
        currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
3,990✔
352
        textData = "";
3,990✔
353
        i = closeIndex;
3,990✔
354
      } else if (xmlData[i + 1] === '?') {
11,025✔
355

245✔
356
        let tagData = readTagExp(xmlData, i, false, "?>");
245✔
357
        if (!tagData) throw new Error("Pi Tag is not closed.");
245✔
358

235✔
359
        textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
235✔
360
        if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) {
245✔
361
          //do nothing
25✔
362
        } else {
245✔
363

210✔
364
          const childNode = new xmlNode(tagData.tagName);
210✔
365
          childNode.add(this.options.textNodeName, "");
210✔
366

210✔
367
          if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
210✔
368
            childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName);
200✔
369
          }
200✔
370
          this.addChild(currentNode, childNode, this.matcher, i);
210✔
371
        }
210✔
372

235✔
373

235✔
374
        i = tagData.closeIndex + 1;
235✔
375
      } else if (xmlData.substr(i + 1, 3) === '!--') {
6,995✔
376
        const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.")
60✔
377
        if (this.options.commentPropName) {
60✔
378
          const comment = xmlData.substring(i + 4, endIndex - 2);
25✔
379

25✔
380
          textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
25✔
381

25✔
382
          currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]);
25✔
383
        }
25✔
384
        i = endIndex;
55✔
385
      } else if (xmlData.substr(i + 1, 2) === '!D') {
6,750✔
386
        const result = docTypeReader.readDocType(xmlData, i);
220✔
387
        this.docTypeEntities = result.entities;
220✔
388
        i = result.i;
220✔
389
      } else if (xmlData.substr(i + 1, 2) === '![') {
6,690✔
390
        const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
190✔
391
        const tagExp = xmlData.substring(i + 9, closeIndex);
190✔
392

190✔
393
        textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
190✔
394

190✔
395
        let val = this.parseTextData(tagExp, currentNode.tagname, this.matcher, true, false, true, true);
190✔
396
        if (val == undefined) val = "";
190✔
397

185✔
398
        //cdata should be set even if it is 0 length string
185✔
399
        if (this.options.cdataPropName) {
190✔
400
          currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]);
35✔
401
        } else {
190✔
402
          currentNode.add(this.options.textNodeName, val);
150✔
403
        }
150✔
404

185✔
405
        i = closeIndex + 2;
185✔
406
      } else {//Opening tag
6,470✔
407
        let result = readTagExp(xmlData, i, this.options.removeNSPrefix);
6,280✔
408

6,280✔
409
        // Safety check: readTagExp can return undefined
6,280✔
410
        if (!result) {
6,280!
411
          // Log context for debugging
×
412
          const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlData.length, i + 50));
×
413
          throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`);
×
414
        }
×
415

6,280✔
416
        let tagName = result.tagName;
6,280✔
417
        const rawTagName = result.rawTagName;
6,280✔
418
        let tagExp = result.tagExp;
6,280✔
419
        let attrExpPresent = result.attrExpPresent;
6,280✔
420
        let closeIndex = result.closeIndex;
6,280✔
421

6,280✔
422
        if (this.options.transformTagName) {
6,280✔
423
          //console.log(tagExp, tagName)
80✔
424
          const newTagName = this.options.transformTagName(tagName);
80✔
425
          if (tagExp === tagName) {
80✔
426
            tagExp = newTagName
50✔
427
          }
50✔
428
          tagName = newTagName;
80✔
429
        }
80✔
430

6,280✔
431
        if (this.options.strictReservedNames &&
6,280✔
432
          (tagName === this.options.commentPropName
6,270✔
433
            || tagName === this.options.cdataPropName
6,270✔
434
          )) {
6,280✔
435
          throw new Error(`Invalid tag name: ${tagName}`);
5✔
436
        }
5✔
437

6,275✔
438
        //save text as child node
6,275✔
439
        if (currentNode && textData) {
6,280✔
440
          if (currentNode.tagname !== '!xml') {
4,125✔
441
            //when nested tag is found
3,240✔
442
            textData = this.saveTextToParentTag(textData, currentNode, this.matcher, false);
3,240✔
443
          }
3,240✔
444
        }
4,125✔
445

6,275✔
446
        //check if last tag was unpaired tag
6,275✔
447
        const lastTag = currentNode;
6,275✔
448
        if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) {
6,280!
449
          currentNode = this.tagsNodeStack.pop();
×
450
          this.matcher.pop();
×
451
        }
×
452

6,275✔
453
        // Clean up self-closing syntax BEFORE processing attributes
6,275✔
454
        // This is where tagExp gets the trailing / removed
6,275✔
455
        let isSelfClosing = false;
6,275✔
456
        if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
6,280✔
457
          isSelfClosing = true;
290✔
458
          if (tagName[tagName.length - 1] === "/") {
290✔
459
            tagName = tagName.substr(0, tagName.length - 1);
100✔
460
            tagExp = tagName;
100✔
461
          } else {
290✔
462
            tagExp = tagExp.substr(0, tagExp.length - 1);
190✔
463
          }
190✔
464

290✔
465
          // Re-check attrExpPresent after cleaning
290✔
466
          attrExpPresent = (tagName !== tagExp);
290✔
467
        }
290✔
468

6,275✔
469
        // Now process attributes with CLEAN tagExp (no trailing /)
6,275✔
470
        let prefixedAttrs = null;
6,275✔
471
        let rawAttrs = {};
6,275✔
472
        let namespace = undefined;
6,275✔
473

6,275✔
474
        // Extract namespace from rawTagName
6,275✔
475
        namespace = extractNamespace(rawTagName);
6,275✔
476

6,275✔
477
        // Push tag to matcher FIRST (with empty attrs for now) so callbacks see correct path
6,275✔
478
        if (tagName !== xmlObj.tagname) {
6,275✔
479
          this.matcher.push(tagName, {}, namespace);
6,275✔
480
        }
6,275✔
481

6,275✔
482
        // Now build attributes - callbacks will see correct matcher state
6,275✔
483
        if (tagName !== tagExp && attrExpPresent) {
6,280✔
484
          // Build attributes (returns prefixed attributes for the tree)
940✔
485
          // Note: buildAttributesMap now internally updates the matcher with raw attributes
940✔
486
          prefixedAttrs = this.buildAttributesMap(tagExp, this.matcher, tagName);
940✔
487

940✔
488
          if (prefixedAttrs) {
940✔
489
            // Extract raw attributes (without prefix) for our use
760✔
490
            rawAttrs = extractRawAttributes(prefixedAttrs, this.options);
760✔
491
          }
760✔
492
        }
940✔
493

6,275✔
494
        // Now check if this is a stop node (after attributes are set)
6,275✔
495
        if (tagName !== xmlObj.tagname) {
6,275✔
496
          this.isCurrentNodeStopNode = this.isItStopNode(this.stopNodeExpressions, this.matcher);
6,275✔
497
        }
6,275✔
498

6,275✔
499
        const startIndex = i;
6,275✔
500
        if (this.isCurrentNodeStopNode) {
6,280✔
501
          let tagContent = "";
1,225✔
502

1,225✔
503
          // For self-closing tags, content is empty
1,225✔
504
          if (isSelfClosing) {
1,225✔
505
            i = result.closeIndex;
10✔
506
          }
10✔
507
          //unpaired tag
1,215✔
508
          else if (this.options.unpairedTags.indexOf(tagName) !== -1) {
1,215✔
509
            i = result.closeIndex;
5✔
510
          }
5✔
511
          //normal tag
1,210✔
512
          else {
1,210✔
513
            //read until closing tag is found
1,210✔
514
            const result = this.readStopNodeData(xmlData, rawTagName, closeIndex + 1);
1,210✔
515
            if (!result) throw new Error(`Unexpected end of ${rawTagName}`);
1,210!
516
            i = result.i;
1,210✔
517
            tagContent = result.tagContent;
1,210✔
518
          }
1,210✔
519

1,225✔
520
          const childNode = new xmlNode(tagName);
1,225✔
521

1,225✔
522
          if (prefixedAttrs) {
1,225✔
523
            childNode[":@"] = prefixedAttrs;
20✔
524
          }
20✔
525

1,225✔
526
          // For stop nodes, store raw content as-is without any processing
1,225✔
527
          childNode.add(this.options.textNodeName, tagContent);
1,225✔
528

1,225✔
529
          this.matcher.pop(); // Pop the stop node tag
1,225✔
530
          this.isCurrentNodeStopNode = false; // Reset flag
1,225✔
531

1,225✔
532
          this.addChild(currentNode, childNode, this.matcher, startIndex);
1,225✔
533
        } else {
6,280✔
534
          //selfClosing tag
5,050✔
535
          if (isSelfClosing) {
5,050✔
536
            if (this.options.transformTagName) {
280✔
537
              const newTagName = this.options.transformTagName(tagName);
5✔
538
              if (tagExp === tagName) {
5✔
539
                tagExp = newTagName
5✔
540
              }
5✔
541
              tagName = newTagName;
5✔
542
            }
5✔
543

280✔
544
            const childNode = new xmlNode(tagName);
280✔
545
            if (prefixedAttrs) {
280✔
546
              childNode[":@"] = prefixedAttrs;
90✔
547
            }
90✔
548
            this.addChild(currentNode, childNode, this.matcher, startIndex);
280✔
549
            this.matcher.pop(); // Pop self-closing tag
280✔
550
            this.isCurrentNodeStopNode = false; // Reset flag
280✔
551
          }
280✔
552
          else if (this.options.unpairedTags.indexOf(tagName) !== -1) {//unpaired tag
4,770✔
553
            const childNode = new xmlNode(tagName);
210✔
554
            if (prefixedAttrs) {
210✔
555
              childNode[":@"] = prefixedAttrs;
90✔
556
            }
90✔
557
            this.addChild(currentNode, childNode, this.matcher, startIndex);
210✔
558
            this.matcher.pop(); // Pop unpaired tag
210✔
559
            this.isCurrentNodeStopNode = false; // Reset flag
210✔
560
            i = result.closeIndex;
210✔
561
            // Continue to next iteration without changing currentNode
210✔
562
            continue;
210✔
563
          }
210✔
564
          //opening tag
4,560✔
565
          else {
4,560✔
566
            const childNode = new xmlNode(tagName);
4,560✔
567
            if (this.tagsNodeStack.length > this.options.maxNestedTags) {
4,560✔
568
              throw new Error("Maximum nested tags exceeded");
5✔
569
            }
5✔
570
            this.tagsNodeStack.push(currentNode);
4,555✔
571

4,555✔
572
            if (prefixedAttrs) {
4,560✔
573
              childNode[":@"] = prefixedAttrs;
560✔
574
            }
560✔
575
            this.addChild(currentNode, childNode, this.matcher, startIndex);
4,555✔
576
            currentNode = childNode;
4,555✔
577
          }
4,555✔
578
          textData = "";
4,835✔
579
          i = closeIndex;
4,835✔
580
        }
4,835✔
581
      }
6,280✔
582
    } else {
324,800✔
583
      textData += xmlData[i];
313,775✔
584
    }
313,775✔
585
  }
324,800✔
586
  return xmlObj.child;
1,500✔
587
}
1,595✔
588

5✔
589
function addChild(currentNode, childNode, matcher, startIndex) {
6,480✔
590
  // unset startIndex if not requested
6,480✔
591
  if (!this.options.captureMetaData) startIndex = undefined;
6,480✔
592

6,480✔
593
  // Pass jPath string or matcher based on options.jPath setting
6,480✔
594
  const jPathOrMatcher = this.options.jPath ? matcher.toString() : matcher;
6,480✔
595
  const result = this.options.updateTag(childNode.tagname, jPathOrMatcher, childNode[":@"])
6,480✔
596
  if (result === false) {
6,480✔
597
    //do nothing
25✔
598
  } else if (typeof result === "string") {
6,480✔
599
    childNode.tagname = result
6,455✔
600
    currentNode.addChild(childNode, startIndex);
6,455✔
601
  } else {
6,455!
602
    currentNode.addChild(childNode, startIndex);
×
603
  }
×
604
}
6,480✔
605

5✔
606
/**
5✔
607
 * @param {object} val - Entity object with regex and val properties
5✔
608
 * @param {string} tagName - Tag name
5✔
609
 * @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
5✔
610
 */
5✔
611
function replaceEntitiesValue(val, tagName, jPath) {
4,365✔
612
  const entityConfig = this.options.processEntities;
4,365✔
613

4,365✔
614
  if (!entityConfig || !entityConfig.enabled) {
4,365✔
615
    return val;
80✔
616
  }
80✔
617

4,285✔
618
  // Check if tag is allowed to contain entities
4,285✔
619
  if (entityConfig.allowedTags) {
4,365✔
620
    const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
30!
621
    const allowed = Array.isArray(entityConfig.allowedTags)
30✔
622
      ? entityConfig.allowedTags.includes(tagName)
30✔
623
      : entityConfig.allowedTags(tagName, jPathOrMatcher);
30!
624

30✔
625
    if (!allowed) {
30✔
626
      return val;
10✔
627
    }
10✔
628
  }
30✔
629

4,275✔
630
  // Apply custom tag filter if provided
4,275✔
631
  if (entityConfig.tagFilter) {
4,365✔
632
    const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
25!
633
    if (!entityConfig.tagFilter(tagName, jPathOrMatcher)) {
25✔
634
      return val; // Skip based on custom filter
15✔
635
    }
15✔
636
  }
25✔
637

4,260✔
638
  // Replace DOCTYPE entities
4,260✔
639
  for (let entityName in this.docTypeEntities) {
4,365✔
640
    const entity = this.docTypeEntities[entityName];
2,499✔
641
    const matches = val.match(entity.regx);
2,499✔
642

2,499✔
643
    if (matches) {
2,499✔
644
      // Track expansions
2,254✔
645
      this.entityExpansionCount += matches.length;
2,254✔
646

2,254✔
647
      // Check expansion limit
2,254✔
648
      if (entityConfig.maxTotalExpansions &&
2,254✔
649
        this.entityExpansionCount > entityConfig.maxTotalExpansions) {
2,254✔
650
        throw new Error(
15✔
651
          `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
15✔
652
        );
15✔
653
      }
15✔
654

2,239✔
655
      // Store length before replacement
2,239✔
656
      const lengthBefore = val.length;
2,239✔
657
      val = val.replace(entity.regx, entity.val);
2,239✔
658

2,239✔
659
      // Check expanded length immediately after replacement
2,239✔
660
      if (entityConfig.maxExpandedLength) {
2,239✔
661
        this.currentExpandedLength += (val.length - lengthBefore);
2,239✔
662

2,239✔
663
        if (this.currentExpandedLength > entityConfig.maxExpandedLength) {
2,239✔
664
          throw new Error(
15✔
665
            `Total expanded content size exceeded: ${this.currentExpandedLength} > ${entityConfig.maxExpandedLength}`
15✔
666
          );
15✔
667
        }
15✔
668
      }
2,239✔
669
    }
2,254✔
670
  }
2,499✔
671
  if (val.indexOf('&') === -1) return val;  // Early exit
4,365✔
672

150✔
673
  // Replace standard entities
150✔
674
  for (let entityName in this.lastEntities) {
4,365✔
675
    const entity = this.lastEntities[entityName];
666✔
676
    val = val.replace(entity.regex, entity.val);
666✔
677
  }
666✔
678
  if (val.indexOf('&') === -1) return val;  // Early exit
4,365✔
679

115✔
680
  // Replace HTML entities if enabled
115✔
681
  if (this.options.htmlEntities) {
4,365✔
682
    for (let entityName in this.htmlEntities) {
55✔
683
      const entity = this.htmlEntities[entityName];
575✔
684
      val = val.replace(entity.regex, entity.val);
575✔
685
    }
575✔
686
  }
55✔
687

115✔
688
  // Replace ampersand entity last
115✔
689
  val = val.replace(this.ampEntity.regex, this.ampEntity.val);
115✔
690

115✔
691
  return val;
115✔
692
}
4,365✔
693

5✔
694

5✔
695
function saveTextToParentTag(textData, parentNode, matcher, isLeafNode) {
7,710✔
696
  if (textData) { //store previously collected data as textNode
7,710✔
697
    if (isLeafNode === undefined) isLeafNode = parentNode.child.length === 0
6,505✔
698

6,505✔
699
    textData = this.parseTextData(textData,
6,505✔
700
      parentNode.tagname,
6,505✔
701
      matcher,
6,505✔
702
      false,
6,505✔
703
      parentNode[":@"] ? Object.keys(parentNode[":@"]).length !== 0 : false,
6,505!
704
      isLeafNode);
6,505✔
705

6,505✔
706
    if (textData !== undefined && textData !== "")
6,505✔
707
      parentNode.add(this.options.textNodeName, textData);
6,505✔
708
    textData = "";
6,475✔
709
  }
6,475✔
710
  return textData;
7,680✔
711
}
7,710✔
712

5✔
713
//TODO: use jPath to simplify the logic
5✔
714
/**
5✔
715
 * @param {Array<Expression>} stopNodeExpressions - Array of compiled Expression objects
5✔
716
 * @param {Matcher} matcher - Current path matcher
5✔
717
 */
5✔
718
function isItStopNode(stopNodeExpressions, matcher) {
6,275✔
719
  if (!stopNodeExpressions || stopNodeExpressions.length === 0) return false;
6,275✔
720

2,220✔
721
  for (let i = 0; i < stopNodeExpressions.length; i++) {
6,275✔
722
    if (matcher.matches(stopNodeExpressions[i])) {
3,505✔
723
      return true;
1,225✔
724
    }
1,225✔
725
  }
3,505✔
726
  return false;
995✔
727
}
6,275✔
728

5✔
729
/**
5✔
730
 * Returns the tag Expression and where it is ending handling single-double quotes situation
5✔
731
 * @param {string} xmlData 
5✔
732
 * @param {number} i starting index
5✔
733
 * @returns 
5✔
734
 */
5✔
735
function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
6,700✔
736
  let attrBoundary;
6,700✔
737
  let tagExp = "";
6,700✔
738
  for (let index = i; index < xmlData.length; index++) {
6,700✔
739
    let ch = xmlData[index];
66,000✔
740
    if (attrBoundary) {
66,000✔
741
      if (ch === attrBoundary) attrBoundary = "";//reset
14,350✔
742
    } else if (ch === '"' || ch === "'") {
66,000✔
743
      attrBoundary = ch;
1,565✔
744
    } else if (ch === closingChar[0]) {
51,650✔
745
      if (closingChar[1]) {
6,930✔
746
        if (xmlData[index + 1] === closingChar[1]) {
480✔
747
          return {
235✔
748
            data: tagExp,
235✔
749
            index: index
235✔
750
          }
235✔
751
        }
235✔
752
      } else {
6,930✔
753
        return {
6,450✔
754
          data: tagExp,
6,450✔
755
          index: index
6,450✔
756
        }
6,450✔
757
      }
6,450✔
758
    } else if (ch === '\t') {
50,085✔
759
      ch = " "
20✔
760
    }
20✔
761
    tagExp += ch;
59,315✔
762
  }
59,315✔
763
}
6,700✔
764

5✔
765
function findClosingIndex(xmlData, str, i, errMsg) {
5,660✔
766
  const closingIndex = xmlData.indexOf(str, i);
5,660✔
767
  if (closingIndex === -1) {
5,660✔
768
    throw new Error(errMsg)
15✔
769
  } else {
5,660✔
770
    return closingIndex + str.length - 1;
5,645✔
771
  }
5,645✔
772
}
5,660✔
773

5✔
774
function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") {
6,700✔
775
  const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar);
6,700✔
776
  if (!result) return;
6,700✔
777
  let tagExp = result.data;
6,685✔
778
  const closeIndex = result.index;
6,685✔
779
  const separatorIndex = tagExp.search(/\s/);
6,685✔
780
  let tagName = tagExp;
6,685✔
781
  let attrExpPresent = true;
6,685✔
782
  if (separatorIndex !== -1) {//separate tag name and attributes expression
6,700✔
783
    tagName = tagExp.substring(0, separatorIndex);
1,225✔
784
    tagExp = tagExp.substring(separatorIndex + 1).trimStart();
1,225✔
785
  }
1,225✔
786

6,685✔
787
  const rawTagName = tagName;
6,685✔
788
  if (removeNSPrefix) {
6,700✔
789
    const colonIndex = tagName.indexOf(":");
300✔
790
    if (colonIndex !== -1) {
300✔
791
      tagName = tagName.substr(colonIndex + 1);
65✔
792
      attrExpPresent = tagName !== result.data.substr(colonIndex + 1);
65✔
793
    }
65✔
794
  }
300✔
795

6,685✔
796
  return {
6,685✔
797
    tagName: tagName,
6,685✔
798
    tagExp: tagExp,
6,685✔
799
    closeIndex: closeIndex,
6,685✔
800
    attrExpPresent: attrExpPresent,
6,685✔
801
    rawTagName: rawTagName,
6,685✔
802
  }
6,685✔
803
}
6,700✔
804
/**
5✔
805
 * find paired tag for a stop node
5✔
806
 * @param {string} xmlData 
5✔
807
 * @param {string} tagName 
5✔
808
 * @param {number} i 
5✔
809
 */
5✔
810
function readStopNodeData(xmlData, tagName, i) {
1,210✔
811
  const startIndex = i;
1,210✔
812
  // Starting at 1 since we already have an open tag
1,210✔
813
  let openTagCount = 1;
1,210✔
814

1,210✔
815
  for (; i < xmlData.length; i++) {
1,210✔
816
    if (xmlData[i] === "<") {
15,985✔
817
      if (xmlData[i + 1] === "/") {//close tag
1,555✔
818
        const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
1,370✔
819
        let closeTagName = xmlData.substring(i + 2, closeIndex).trim();
1,370✔
820
        if (closeTagName === tagName) {
1,370✔
821
          openTagCount--;
1,215✔
822
          if (openTagCount === 0) {
1,215✔
823
            return {
1,210✔
824
              tagContent: xmlData.substring(startIndex, i),
1,210✔
825
              i: closeIndex
1,210✔
826
            }
1,210✔
827
          }
1,210✔
828
        }
1,215✔
829
        i = closeIndex;
160✔
830
      } else if (xmlData[i + 1] === '?') {
1,555!
831
        const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.")
×
832
        i = closeIndex;
×
833
      } else if (xmlData.substr(i + 1, 3) === '!--') {
185✔
834
        const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.")
5✔
835
        i = closeIndex;
5✔
836
      } else if (xmlData.substr(i + 1, 2) === '![') {
185✔
837
        const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
5✔
838
        i = closeIndex;
5✔
839
      } else {
180✔
840
        const tagData = readTagExp(xmlData, i, '>')
175✔
841

175✔
842
        if (tagData) {
175✔
843
          const openTagName = tagData && tagData.tagName;
170✔
844
          if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length - 1] !== "/") {
170✔
845
            openTagCount++;
5✔
846
          }
5✔
847
          i = tagData.closeIndex;
170✔
848
        }
170✔
849
      }
175✔
850
    }
1,555✔
851
  }//end for loop
1,210✔
852
}
1,210✔
853

5✔
854
function parseValue(val, shouldParse, options) {
3,080✔
855
  if (shouldParse && typeof val === 'string') {
3,080✔
856
    //console.log(options)
2,060✔
857
    const newval = val.trim();
2,060✔
858
    if (newval === 'true') return true;
2,060✔
859
    else if (newval === 'false') return false;
2,025✔
860
    else return toNumber(val, options);
2,010✔
861
  } else {
3,080✔
862
    if (isExist(val)) {
1,020✔
863
      return val;
1,020✔
864
    } else {
1,020!
865
      return '';
×
866
    }
×
867
  }
1,020✔
868
}
3,080✔
869

5✔
870
function fromCodePoint(str, base, prefix) {
90✔
871
  const codePoint = Number.parseInt(str, base);
90✔
872

90✔
873
  if (codePoint >= 0 && codePoint <= 0x10FFFF) {
90✔
874
    return String.fromCodePoint(codePoint);
75✔
875
  } else {
90✔
876
    return prefix + str + ";";
15✔
877
  }
15✔
878
}
90✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc