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

NaturalIntelligence / fast-xml-parser / 22607952071

03 Mar 2026 04:10AM UTC coverage: 97.689% (-0.2%) from 97.87%
22607952071

push

github

amitguptagwl
update release info

1029 of 1065 branches covered (96.62%)

8454 of 8654 relevant lines covered (97.69%)

515532.84 hits per line

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

97.53
/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

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

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

5✔
17
export default class OrderedObjParser {
5✔
18
  constructor(options) {
5✔
19
    this.options = options;
990✔
20
    this.currentNode = null;
990✔
21
    this.tagsNodeStack = [];
990✔
22
    this.docTypeEntities = {};
990✔
23
    this.lastEntities = {
990✔
24
      "apos": { regex: /&(apos|#39|#x27);/g, val: "'" },
990✔
25
      "gt": { regex: /&(gt|#62|#x3E);/g, val: ">" },
990✔
26
      "lt": { regex: /&(lt|#60|#x3C);/g, val: "<" },
990✔
27
      "quot": { regex: /&(quot|#34|#x22);/g, val: "\"" },
990✔
28
    };
990✔
29
    this.ampEntity = { regex: /&(amp|#38|#x26);/g, val: "&" };
990✔
30
    this.htmlEntities = {
990✔
31
      "space": { regex: /&(nbsp|#160);/g, val: " " },
990✔
32
      // "lt" : { regex: /&(lt|#60);/g, val: "<" },
990✔
33
      // "gt" : { regex: /&(gt|#62);/g, val: ">" },
990✔
34
      // "amp" : { regex: /&(amp|#38);/g, val: "&" },
990✔
35
      // "quot" : { regex: /&(quot|#34);/g, val: "\"" },
990✔
36
      // "apos" : { regex: /&(apos|#39);/g, val: "'" },
990✔
37
      "cent": { regex: /&(cent|#162);/g, val: "¢" },
990✔
38
      "pound": { regex: /&(pound|#163);/g, val: "£" },
990✔
39
      "yen": { regex: /&(yen|#165);/g, val: "¥" },
990✔
40
      "euro": { regex: /&(euro|#8364);/g, val: "€" },
990✔
41
      "copyright": { regex: /&(copy|#169);/g, val: "©" },
990✔
42
      "reg": { regex: /&(reg|#174);/g, val: "®" },
990✔
43
      "inr": { regex: /&(inr|#8377);/g, val: "₹" },
990✔
44
      "num_dec": { regex: /&#([0-9]{1,7});/g, val: (_, str) => fromCodePoint(str, 10, "&#") },
990✔
45
      "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => fromCodePoint(str, 16, "&#x") },
990✔
46
    };
990✔
47
    this.addExternalEntities = addExternalEntities;
990✔
48
    this.parseXml = parseXml;
990✔
49
    this.parseTextData = parseTextData;
990✔
50
    this.resolveNameSpace = resolveNameSpace;
990✔
51
    this.buildAttributesMap = buildAttributesMap;
990✔
52
    this.isItStopNode = isItStopNode;
990✔
53
    this.replaceEntitiesValue = replaceEntitiesValue;
990✔
54
    this.readStopNodeData = readStopNodeData;
990✔
55
    this.saveTextToParentTag = saveTextToParentTag;
990✔
56
    this.addChild = addChild;
990✔
57
    this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes)
990✔
58
    this.entityExpansionCount = 0;
990✔
59
    this.currentExpandedLength = 0;
990✔
60

990✔
61
    if (this.options.stopNodes && this.options.stopNodes.length > 0) {
990✔
62
      this.stopNodesExact = new Set();
105✔
63
      this.stopNodesWildcard = new Set();
105✔
64
      for (let i = 0; i < this.options.stopNodes.length; i++) {
105✔
65
        const stopNodeExp = this.options.stopNodes[i];
160✔
66
        if (typeof stopNodeExp !== 'string') continue;
160!
67
        if (stopNodeExp.startsWith("*.")) {
160✔
68
          this.stopNodesWildcard.add(stopNodeExp.substring(2));
70✔
69
        } else {
160✔
70
          this.stopNodesExact.add(stopNodeExp);
90✔
71
        }
90✔
72
      }
160✔
73
    }
105✔
74
  }
990✔
75

5✔
76
}
5✔
77

5✔
78
function addExternalEntities(externalEntities) {
990✔
79
  const entKeys = Object.keys(externalEntities);
990✔
80
  for (let i = 0; i < entKeys.length; i++) {
990✔
81
    const ent = entKeys[i];
15✔
82
    const escaped = ent.replace(/[.\-+*:]/g, '\\.');
15✔
83
    this.lastEntities[ent] = {
15✔
84
      regex: new RegExp("&" + escaped + ";", "g"),
15✔
85
      val: externalEntities[ent]
15✔
86
    }
15✔
87
  }
15✔
88
}
990✔
89

5✔
90
/**
5✔
91
 * @param {string} val
5✔
92
 * @param {string} tagName
5✔
93
 * @param {string} jPath
5✔
94
 * @param {boolean} dontTrim
5✔
95
 * @param {boolean} hasAttributes
5✔
96
 * @param {boolean} isLeafNode
5✔
97
 * @param {boolean} escapeEntities
5✔
98
 */
5✔
99
function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) {
4,850✔
100
  if (val !== undefined) {
4,850✔
101
    if (this.options.trimValues && !dontTrim) {
4,850✔
102
      val = val.trim();
4,510✔
103
    }
4,510✔
104
    if (val.length > 0) {
4,850✔
105
      if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);
2,005✔
106

1,975✔
107
      const newval = this.options.tagValueProcessor(tagName, val, jPath, hasAttributes, isLeafNode);
1,975✔
108
      if (newval === null || newval === undefined) {
2,005✔
109
        //don't parse
50✔
110
        return val;
50✔
111
      } else if (typeof newval !== typeof val || newval !== val) {
2,005✔
112
        //overwrite
20✔
113
        return newval;
20✔
114
      } else if (this.options.trimValues) {
1,925✔
115
        return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
1,845✔
116
      } else {
1,905✔
117
        const trimmedVal = val.trim();
60✔
118
        if (trimmedVal === val) {
60✔
119
          return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
15✔
120
        } else {
60✔
121
          return val;
45✔
122
        }
45✔
123
      }
60✔
124
    }
2,005✔
125
  }
4,850✔
126
}
4,850✔
127

5✔
128
function resolveNameSpace(tagname) {
1,360✔
129
  if (this.options.removeNSPrefix) {
1,360✔
130
    const tags = tagname.split(':');
110✔
131
    const prefix = tagname.charAt(0) === '/' ? '/' : '';
110!
132
    if (tags[0] === 'xmlns') {
110✔
133
      return '';
35✔
134
    }
35✔
135
    if (tags.length === 2) {
110✔
136
      tagname = prefix + tags[1];
30✔
137
    }
30✔
138
  }
110✔
139
  return tagname;
1,325✔
140
}
1,360✔
141

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

5✔
146
function buildAttributesMap(attrStr, jPath, tagName) {
1,090✔
147
  if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') {
1,090✔
148
    // attrStr = attrStr.replace(/\r?\n/g, ' ');
925✔
149
    //attrStr = attrStr || attrStr.trim();
925✔
150

925✔
151
    const matches = getAllMatches(attrStr, attrsRegx);
925✔
152
    const len = matches.length; //don't make it inline
925✔
153
    const attrs = {};
925✔
154
    for (let i = 0; i < len; i++) {
925✔
155
      const attrName = this.resolveNameSpace(matches[i][1]);
1,360✔
156
      if (this.ignoreAttributesFn(attrName, jPath)) {
1,360✔
157
        continue
50✔
158
      }
50✔
159
      let oldVal = matches[i][4];
1,310✔
160
      let aName = this.options.attributeNamePrefix + attrName;
1,310✔
161
      if (attrName.length) {
1,360✔
162
        if (this.options.transformAttributeName) {
1,275✔
163
          aName = this.options.transformAttributeName(aName);
20✔
164
        }
20✔
165
        if (aName === "__proto__") aName = "#__proto__";
1,275!
166

1,275✔
167
        if (oldVal !== undefined) {
1,275✔
168
          if (this.options.trimValues) {
1,185✔
169
            oldVal = oldVal.trim();
1,160✔
170
          }
1,160✔
171
          oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath);
1,185✔
172
          const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPath);
1,185✔
173
          if (newVal === null || newVal === undefined) {
1,185!
174
            //don't parse
×
175
            attrs[aName] = oldVal;
×
176
          } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) {
1,185!
177
            //overwrite
×
178
            attrs[aName] = newVal;
×
179
          } else {
1,185✔
180
            //parse
1,185✔
181
            attrs[aName] = parseValue(
1,185✔
182
              oldVal,
1,185✔
183
              this.options.parseAttributeValue,
1,185✔
184
              this.options.numberParseOptions
1,185✔
185
            );
1,185✔
186
          }
1,185✔
187
        } else if (this.options.allowBooleanAttributes) {
1,275✔
188
          attrs[aName] = true;
80✔
189
        }
80✔
190
      }
1,275✔
191
    }
1,360✔
192
    if (!Object.keys(attrs).length) {
925✔
193
      return;
95✔
194
    }
95✔
195
    if (this.options.attributesGroupName) {
925✔
196
      const attrCollection = {};
60✔
197
      attrCollection[this.options.attributesGroupName] = attrs;
60✔
198
      return attrCollection;
60✔
199
    }
60✔
200
    return attrs
770✔
201
  }
770✔
202
}
1,090✔
203

5✔
204
const parseXml = function (xmlData) {
5✔
205
  xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line
990✔
206
  const xmlObj = new xmlNode('!xml');
990✔
207
  let currentNode = xmlObj;
990✔
208
  let textData = "";
990✔
209
  let jPath = "";
990✔
210

990✔
211
  // Reset entity expansion counters for this document
990✔
212
  this.entityExpansionCount = 0;
990✔
213
  this.currentExpandedLength = 0;
990✔
214

990✔
215
  const docTypeReader = new DocTypeReader(this.options.processEntities);
990✔
216
  for (let i = 0; i < xmlData.length; i++) {//for each char in XML data
990✔
217
    const ch = xmlData[i];
296,870✔
218
    if (ch === '<') {
296,870✔
219
      // const nextIndex = i+1;
8,365✔
220
      // const _2ndChar = xmlData[nextIndex];
8,365✔
221
      if (xmlData[i + 1] === '/') {//Closing Tag
8,365✔
222
        const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.")
3,250✔
223
        let tagName = xmlData.substring(i + 2, closeIndex).trim();
3,250✔
224

3,250✔
225
        if (this.options.removeNSPrefix) {
3,250✔
226
          const colonIndex = tagName.indexOf(":");
115✔
227
          if (colonIndex !== -1) {
115✔
228
            tagName = tagName.substr(colonIndex + 1);
50✔
229
          }
50✔
230
        }
115✔
231

3,245✔
232
        if (this.options.transformTagName) {
3,250✔
233
          tagName = this.options.transformTagName(tagName);
75✔
234
        }
75✔
235

3,245✔
236
        if (currentNode) {
3,245✔
237
          textData = this.saveTextToParentTag(textData, currentNode, jPath);
3,245✔
238
        }
3,245✔
239

3,215✔
240
        //check if last tag of nested tag was unpaired tag
3,215✔
241
        const lastTagName = jPath.substring(jPath.lastIndexOf(".") + 1);
3,215✔
242
        if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) {
3,250✔
243
          throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
5✔
244
        }
5✔
245
        let propIndex = 0
3,210✔
246
        if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) {
3,250!
247
          propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.') - 1)
×
248
          this.tagsNodeStack.pop();
×
249
        } else {
3,250✔
250
          propIndex = jPath.lastIndexOf(".");
3,210✔
251
        }
3,210✔
252
        jPath = jPath.substring(0, propIndex);
3,210✔
253

3,210✔
254
        currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
3,210✔
255
        textData = "";
3,210✔
256
        i = closeIndex;
3,210✔
257
      } else if (xmlData[i + 1] === '?') {
8,365✔
258

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

235✔
262
        textData = this.saveTextToParentTag(textData, currentNode, jPath);
235✔
263
        if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) {
245✔
264
          //do nothing
25✔
265
        } else {
245✔
266

210✔
267
          const childNode = new xmlNode(tagData.tagName);
210✔
268
          childNode.add(this.options.textNodeName, "");
210✔
269

210✔
270
          if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
210✔
271
            childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath, tagData.tagName);
200✔
272
          }
200✔
273
          this.addChild(currentNode, childNode, jPath, i);
210✔
274
        }
210✔
275

235✔
276

235✔
277
        i = tagData.closeIndex + 1;
235✔
278
      } else if (xmlData.substr(i + 1, 3) === '!--') {
5,115✔
279
        const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.")
60✔
280
        if (this.options.commentPropName) {
60✔
281
          const comment = xmlData.substring(i + 4, endIndex - 2);
25✔
282

25✔
283
          textData = this.saveTextToParentTag(textData, currentNode, jPath);
25✔
284

25✔
285
          currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]);
25✔
286
        }
25✔
287
        i = endIndex;
55✔
288
      } else if (xmlData.substr(i + 1, 2) === '!D') {
4,870✔
289
        const result = docTypeReader.readDocType(xmlData, i);
220✔
290
        this.docTypeEntities = result.entities;
220✔
291
        i = result.i;
220✔
292
      } else if (xmlData.substr(i + 1, 2) === '![') {
4,810✔
293
        const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
190✔
294
        const tagExp = xmlData.substring(i + 9, closeIndex);
190✔
295

190✔
296
        textData = this.saveTextToParentTag(textData, currentNode, jPath);
190✔
297

190✔
298
        let val = this.parseTextData(tagExp, currentNode.tagname, jPath, true, false, true, true);
190✔
299
        if (val == undefined) val = "";
190✔
300

185✔
301
        //cdata should be set even if it is 0 length string
185✔
302
        if (this.options.cdataPropName) {
190✔
303
          currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]);
35✔
304
        } else {
190✔
305
          currentNode.add(this.options.textNodeName, val);
150✔
306
        }
150✔
307

185✔
308
        i = closeIndex + 2;
185✔
309
      } else {//Opening tag
4,590✔
310
        let result = readTagExp(xmlData, i, this.options.removeNSPrefix);
4,400✔
311
        let tagName = result.tagName;
4,400✔
312
        const rawTagName = result.rawTagName;
4,400✔
313
        let tagExp = result.tagExp;
4,400✔
314
        let attrExpPresent = result.attrExpPresent;
4,400✔
315
        let closeIndex = result.closeIndex;
4,400✔
316

4,400✔
317
        if (this.options.transformTagName) {
4,400✔
318
          //console.log(tagExp, tagName)
80✔
319
          const newTagName = this.options.transformTagName(tagName);
80✔
320
          if (tagExp === tagName) {
80✔
321
            tagExp = newTagName
50✔
322
          }
50✔
323
          tagName = newTagName;
80✔
324
        }
80✔
325

4,400✔
326
        if (this.options.strictReservedNames &&
4,400✔
327
          (tagName === this.options.commentPropName
4,390✔
328
            || tagName === this.options.cdataPropName
4,390✔
329
          )) {
4,400✔
330
          throw new Error(`Invalid tag name: ${tagName}`);
5✔
331
        }
5✔
332

4,395✔
333
        //save text as child node
4,395✔
334
        if (currentNode && textData) {
4,400✔
335
          if (currentNode.tagname !== '!xml') {
2,320✔
336
            //when nested tag is found
2,025✔
337
            textData = this.saveTextToParentTag(textData, currentNode, jPath, false);
2,025✔
338
          }
2,025✔
339
        }
2,320✔
340

4,395✔
341
        //check if last tag was unpaired tag
4,395✔
342
        const lastTag = currentNode;
4,395✔
343
        if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) {
4,400!
344
          currentNode = this.tagsNodeStack.pop();
×
345
          jPath = jPath.substring(0, jPath.lastIndexOf("."));
×
346
        }
×
347
        if (tagName !== xmlObj.tagname) {
4,395✔
348
          jPath += jPath ? "." + tagName : tagName;
4,395✔
349
        }
4,395✔
350
        const startIndex = i;
4,395✔
351
        if (this.isItStopNode(this.stopNodesExact, this.stopNodesWildcard, jPath, tagName)) {
4,400✔
352
          let tagContent = "";
130✔
353
          //self-closing tag
130✔
354
          if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
130✔
355
            if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
10!
356
              tagName = tagName.substr(0, tagName.length - 1);
×
357
              jPath = jPath.substr(0, jPath.length - 1);
×
358
              tagExp = tagName;
×
359
            } else {
10✔
360
              tagExp = tagExp.substr(0, tagExp.length - 1);
10✔
361
            }
10✔
362
            i = result.closeIndex;
10✔
363
          }
10✔
364
          //unpaired tag
120✔
365
          else if (this.options.unpairedTags.indexOf(tagName) !== -1) {
120✔
366

5✔
367
            i = result.closeIndex;
5✔
368
          }
5✔
369
          //normal tag
115✔
370
          else {
115✔
371
            //read until closing tag is found
115✔
372
            const result = this.readStopNodeData(xmlData, rawTagName, closeIndex + 1);
115✔
373
            if (!result) throw new Error(`Unexpected end of ${rawTagName}`);
115!
374
            i = result.i;
115✔
375
            tagContent = result.tagContent;
115✔
376
          }
115✔
377

130✔
378
          const childNode = new xmlNode(tagName);
130✔
379

130✔
380
          if (tagName !== tagExp && attrExpPresent) {
130✔
381
            childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
45✔
382
          }
45✔
383
          if (tagContent) {
130✔
384
            tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true);
95✔
385
          }
95✔
386

130✔
387
          jPath = jPath.substr(0, jPath.lastIndexOf("."));
130✔
388
          childNode.add(this.options.textNodeName, tagContent);
130✔
389

130✔
390
          this.addChild(currentNode, childNode, jPath, startIndex);
130✔
391
        } else {
4,400✔
392
          //selfClosing tag
4,265✔
393
          if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
4,265✔
394
            if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
280✔
395
              tagName = tagName.substr(0, tagName.length - 1);
100✔
396
              jPath = jPath.substr(0, jPath.length - 1);
100✔
397
              tagExp = tagName;
100✔
398
            } else {
280✔
399
              tagExp = tagExp.substr(0, tagExp.length - 1);
180✔
400
            }
180✔
401

280✔
402
            if (this.options.transformTagName) {
280✔
403
              const newTagName = this.options.transformTagName(tagName);
5✔
404
              if (tagExp === tagName) {
5✔
405
                tagExp = newTagName
5✔
406
              }
5✔
407
              tagName = newTagName;
5✔
408
            }
5✔
409

280✔
410
            const childNode = new xmlNode(tagName);
280✔
411
            if (tagName !== tagExp && attrExpPresent) {
280✔
412
              childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
180✔
413
            }
180✔
414
            this.addChild(currentNode, childNode, jPath, startIndex);
280✔
415
            jPath = jPath.substr(0, jPath.lastIndexOf("."));
280✔
416
          }
280✔
417
          else if(this.options.unpairedTags.indexOf(tagName) !== -1){//unpaired tag
3,985✔
418
            const childNode = new xmlNode(tagName);
205✔
419
            if(tagName !== tagExp && attrExpPresent){
205✔
420
              childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
90✔
421
            }
90✔
422
            this.addChild(currentNode, childNode, jPath, startIndex);
205✔
423
            jPath = jPath.substr(0, jPath.lastIndexOf("."));
205✔
424
            i = result.closeIndex;
205✔
425
            // Continue to next iteration without changing currentNode
205✔
426
            continue;
205✔
427
          }
205✔
428
          //opening tag
3,780✔
429
          else {
3,780✔
430
            const childNode = new xmlNode(tagName);
3,780✔
431
            if (this.tagsNodeStack.length > this.options.maxNestedTags) {
3,780✔
432
              throw new Error("Maximum nested tags exceeded");
5✔
433
            }
5✔
434
            this.tagsNodeStack.push(currentNode);
3,775✔
435

3,775✔
436
            if (tagName !== tagExp && attrExpPresent) {
3,780✔
437
              childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
575✔
438
            }
575✔
439
            this.addChild(currentNode, childNode, jPath, startIndex);
3,775✔
440
            currentNode = childNode;
3,775✔
441
          }
3,775✔
442
          textData = "";
4,055✔
443
          i = closeIndex;
4,055✔
444
        }
4,055✔
445
      }
4,400✔
446
    } else {
296,870✔
447
      textData += xmlData[i];
288,505✔
448
    }
288,505✔
449
  }
296,870✔
450
  return xmlObj.child;
895✔
451
}
990✔
452

5✔
453
function addChild(currentNode, childNode, jPath, startIndex) {
4,600✔
454
  // unset startIndex if not requested
4,600✔
455
  if (!this.options.captureMetaData) startIndex = undefined;
4,600✔
456
  const result = this.options.updateTag(childNode.tagname, jPath, childNode[":@"])
4,600✔
457
  if (result === false) {
4,600✔
458
    //do nothing
25✔
459
  } else if (typeof result === "string") {
4,600✔
460
    childNode.tagname = result
4,575✔
461
    currentNode.addChild(childNode, startIndex);
4,575✔
462
  } else {
4,575!
463
    currentNode.addChild(childNode, startIndex);
×
464
  }
×
465
}
4,600✔
466

5✔
467
const replaceEntitiesValue = function (val, tagName, jPath) {
5✔
468
  // Performance optimization: Early return if no entities to replace
2,930✔
469
  if (val.indexOf('&') === -1) {
2,930✔
470
    return val;
2,590✔
471
  }
2,590✔
472

340✔
473
  const entityConfig = this.options.processEntities;
340✔
474

340✔
475
  if (!entityConfig.enabled) {
2,930✔
476
    return val;
25✔
477
  }
25✔
478

315✔
479
  // Check tag-specific filtering
315✔
480
  if (entityConfig.allowedTags) {
2,930✔
481
    if (!entityConfig.allowedTags.includes(tagName)) {
30✔
482
      return val; // Skip entity replacement for current tag as not set
10✔
483
    }
10✔
484
  }
30✔
485

305✔
486
  if (entityConfig.tagFilter) {
2,930✔
487
    if (!entityConfig.tagFilter(tagName, jPath)) {
25✔
488
      return val; // Skip based on custom filter
15✔
489
    }
15✔
490
  }
25✔
491

290✔
492
  // Replace DOCTYPE entities
290✔
493
  for (let entityName in this.docTypeEntities) {
2,930✔
494
    const entity = this.docTypeEntities[entityName];
324✔
495
    const matches = val.match(entity.regx);
324✔
496

324✔
497
    if (matches) {
324✔
498
      // Track expansions
269✔
499
      this.entityExpansionCount += matches.length;
269✔
500

269✔
501
      // Check expansion limit
269✔
502
      if (entityConfig.maxTotalExpansions &&
269✔
503
        this.entityExpansionCount > entityConfig.maxTotalExpansions) {
269✔
504
        throw new Error(
15✔
505
          `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
15✔
506
        );
15✔
507
      }
15✔
508

254✔
509
      // Store length before replacement
254✔
510
      const lengthBefore = val.length;
254✔
511
      val = val.replace(entity.regx, entity.val);
254✔
512

254✔
513
      // Check expanded length immediately after replacement
254✔
514
      if (entityConfig.maxExpandedLength) {
254✔
515
        this.currentExpandedLength += (val.length - lengthBefore);
254✔
516

254✔
517
        if (this.currentExpandedLength > entityConfig.maxExpandedLength) {
254✔
518
          throw new Error(
15✔
519
            `Total expanded content size exceeded: ${this.currentExpandedLength} > ${entityConfig.maxExpandedLength}`
15✔
520
          );
15✔
521
        }
15✔
522
      }
254✔
523
    }
269✔
524
  }
324✔
525
  if (val.indexOf('&') === -1) return val;  // Early exit
2,930✔
526

120✔
527
  // Replace standard entities
120✔
528
  for (let entityName in this.lastEntities) {
2,930✔
529
    const entity = this.lastEntities[entityName];
547✔
530
    val = val.replace(entity.regex, entity.val);
547✔
531
  }
547✔
532
  if (val.indexOf('&') === -1) return val;  // Early exit
2,930✔
533

90✔
534
  // Replace HTML entities if enabled
90✔
535
  if (this.options.htmlEntities) {
2,930✔
536
    for (let entityName in this.htmlEntities) {
40✔
537
      const entity = this.htmlEntities[entityName];
419✔
538
      val = val.replace(entity.regex, entity.val);
419✔
539
    }
419✔
540
  }
40✔
541

90✔
542
  // Replace ampersand entity last
90✔
543
  val = val.replace(this.ampEntity.regex, this.ampEntity.val);
90✔
544

90✔
545
  return val;
90✔
546
}
2,930✔
547

5✔
548

5✔
549
function saveTextToParentTag(textData, parentNode, jPath, isLeafNode) {
5,715✔
550
  if (textData) { //store previously collected data as textNode
5,715✔
551
    if (isLeafNode === undefined) isLeafNode = parentNode.child.length === 0
4,570✔
552

4,570✔
553
    textData = this.parseTextData(textData,
4,570✔
554
      parentNode.tagname,
4,570✔
555
      jPath,
4,570✔
556
      false,
4,570✔
557
      parentNode[":@"] ? Object.keys(parentNode[":@"]).length !== 0 : false,
4,570✔
558
      isLeafNode);
4,570✔
559

4,570✔
560
    if (textData !== undefined && textData !== "")
4,570✔
561
      parentNode.add(this.options.textNodeName, textData);
4,570✔
562
    textData = "";
4,540✔
563
  }
4,540✔
564
  return textData;
5,685✔
565
}
5,715✔
566

5✔
567
//TODO: use jPath to simplify the logic
5✔
568
/**
5✔
569
 * @param {Set} stopNodesExact
5✔
570
 * @param {Set} stopNodesWildcard
5✔
571
 * @param {string} jPath
5✔
572
 * @param {string} currentTagName
5✔
573
 */
5✔
574
function isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName) {
4,395✔
575
  if (stopNodesWildcard && stopNodesWildcard.has(currentTagName)) return true;
4,395✔
576
  if (stopNodesExact && stopNodesExact.has(jPath)) return true;
4,395✔
577
  return false;
4,265✔
578
}
4,395✔
579

5✔
580
/**
5✔
581
 * Returns the tag Expression and where it is ending handling single-double quotes situation
5✔
582
 * @param {string} xmlData 
5✔
583
 * @param {number} i starting index
5✔
584
 * @returns 
5✔
585
 */
5✔
586
function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
4,800✔
587
  let attrBoundary;
4,800✔
588
  let tagExp = "";
4,800✔
589
  for (let index = i; index < xmlData.length; index++) {
4,800✔
590
    let ch = xmlData[index];
54,340✔
591
    if (attrBoundary) {
54,340✔
592
      if (ch === attrBoundary) attrBoundary = "";//reset
14,060✔
593
    } else if (ch === '"' || ch === "'") {
54,340✔
594
      attrBoundary = ch;
1,500✔
595
    } else if (ch === closingChar[0]) {
40,280✔
596
      if (closingChar[1]) {
5,030✔
597
        if (xmlData[index + 1] === closingChar[1]) {
480✔
598
          return {
235✔
599
            data: tagExp,
235✔
600
            index: index
235✔
601
          }
235✔
602
        }
235✔
603
      } else {
5,030✔
604
        return {
4,550✔
605
          data: tagExp,
4,550✔
606
          index: index
4,550✔
607
        }
4,550✔
608
      }
4,550✔
609
    } else if (ch === '\t') {
38,780✔
610
      ch = " "
20✔
611
    }
20✔
612
    tagExp += ch;
49,555✔
613
  }
49,555✔
614
}
4,800✔
615

5✔
616
function findClosingIndex(xmlData, str, i, errMsg) {
3,770✔
617
  const closingIndex = xmlData.indexOf(str, i);
3,770✔
618
  if (closingIndex === -1) {
3,770✔
619
    throw new Error(errMsg)
15✔
620
  } else {
3,770✔
621
    return closingIndex + str.length - 1;
3,755✔
622
  }
3,755✔
623
}
3,770✔
624

5✔
625
function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") {
4,800✔
626
  const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar);
4,800✔
627
  if (!result) return;
4,800✔
628
  let tagExp = result.data;
4,785✔
629
  const closeIndex = result.index;
4,785✔
630
  const separatorIndex = tagExp.search(/\s/);
4,785✔
631
  let tagName = tagExp;
4,785✔
632
  let attrExpPresent = true;
4,785✔
633
  if (separatorIndex !== -1) {//separate tag name and attributes expression
4,800✔
634
    tagName = tagExp.substring(0, separatorIndex);
1,170✔
635
    tagExp = tagExp.substring(separatorIndex + 1).trimStart();
1,170✔
636
  }
1,170✔
637

4,785✔
638
  const rawTagName = tagName;
4,785✔
639
  if (removeNSPrefix) {
4,800✔
640
    const colonIndex = tagName.indexOf(":");
280✔
641
    if (colonIndex !== -1) {
280✔
642
      tagName = tagName.substr(colonIndex + 1);
65✔
643
      attrExpPresent = tagName !== result.data.substr(colonIndex + 1);
65✔
644
    }
65✔
645
  }
280✔
646

4,785✔
647
  return {
4,785✔
648
    tagName: tagName,
4,785✔
649
    tagExp: tagExp,
4,785✔
650
    closeIndex: closeIndex,
4,785✔
651
    attrExpPresent: attrExpPresent,
4,785✔
652
    rawTagName: rawTagName,
4,785✔
653
  }
4,785✔
654
}
4,800✔
655
/**
5✔
656
 * find paired tag for a stop node
5✔
657
 * @param {string} xmlData 
5✔
658
 * @param {string} tagName 
5✔
659
 * @param {number} i 
5✔
660
 */
5✔
661
function readStopNodeData(xmlData, tagName, i) {
115✔
662
  const startIndex = i;
115✔
663
  // Starting at 1 since we already have an open tag
115✔
664
  let openTagCount = 1;
115✔
665

115✔
666
  for (; i < xmlData.length; i++) {
115✔
667
    if (xmlData[i] === "<") {
4,725✔
668
      if (xmlData[i + 1] === "/") {//close tag
425✔
669
        const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
260✔
670
        let closeTagName = xmlData.substring(i + 2, closeIndex).trim();
260✔
671
        if (closeTagName === tagName) {
260✔
672
          openTagCount--;
120✔
673
          if (openTagCount === 0) {
120✔
674
            return {
115✔
675
              tagContent: xmlData.substring(startIndex, i),
115✔
676
              i: closeIndex
115✔
677
            }
115✔
678
          }
115✔
679
        }
120✔
680
        i = closeIndex;
145✔
681
      } else if (xmlData[i + 1] === '?') {
425!
682
        const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.")
×
683
        i = closeIndex;
×
684
      } else if (xmlData.substr(i + 1, 3) === '!--') {
165✔
685
        const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.")
5✔
686
        i = closeIndex;
5✔
687
      } else if (xmlData.substr(i + 1, 2) === '![') {
165✔
688
        const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
5✔
689
        i = closeIndex;
5✔
690
      } else {
160✔
691
        const tagData = readTagExp(xmlData, i, '>')
155✔
692

155✔
693
        if (tagData) {
155✔
694
          const openTagName = tagData && tagData.tagName;
150✔
695
          if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length - 1] !== "/") {
150✔
696
            openTagCount++;
5✔
697
          }
5✔
698
          i = tagData.closeIndex;
150✔
699
        }
150✔
700
      }
155✔
701
    }
425✔
702
  }//end for loop
115✔
703
}
115✔
704

5✔
705
function parseValue(val, shouldParse, options) {
3,045✔
706
  if (shouldParse && typeof val === 'string') {
3,045✔
707
    //console.log(options)
2,080✔
708
    const newval = val.trim();
2,080✔
709
    if (newval === 'true') return true;
2,080✔
710
    else if (newval === 'false') return false;
2,045✔
711
    else return toNumber(val, options);
2,030✔
712
  } else {
3,045✔
713
    if (isExist(val)) {
965✔
714
      return val;
965✔
715
    } else {
965!
716
      return '';
×
717
    }
×
718
  }
965✔
719
}
3,045✔
720

5✔
721
function fromCodePoint(str, base, prefix) {
50✔
722
  const codePoint = Number.parseInt(str, base);
50✔
723

50✔
724
  if (codePoint >= 0 && codePoint <= 0x10FFFF) {
50✔
725
    return String.fromCodePoint(codePoint);
40✔
726
  } else {
50✔
727
    return prefix + str + ";";
10✔
728
  }
10✔
729
}
50✔
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