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

NaturalIntelligence / fast-xml-parser / 21798451348

08 Feb 2026 11:00AM UTC coverage: 97.617% (+0.007%) from 97.61%
21798451348

push

github

amitguptagwl
update strnum and release detail

1111 of 1153 branches covered (96.36%)

25 of 25 new or added lines in 1 file covered. (100.0%)

6 existing lines in 1 file now uncovered.

9052 of 9273 relevant lines covered (97.62%)

480818.73 hits per line

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

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

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

5✔
74
}
5✔
75

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

5✔
88
/**
5✔
89
 * @param {string} val
5✔
90
 * @param {string} tagName
5✔
91
 * @param {string} jPath
5✔
92
 * @param {boolean} dontTrim
5✔
93
 * @param {boolean} hasAttributes
5✔
94
 * @param {boolean} isLeafNode
5✔
95
 * @param {boolean} escapeEntities
5✔
96
 */
5✔
97
function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) {
5,295✔
98
  if (val !== undefined) {
5,295✔
99
    if (this.options.trimValues && !dontTrim) {
5,295✔
100
      val = val.trim();
4,885✔
101
    }
4,885✔
102
    if(val.length > 0){
5,295✔
103
      if(!escapeEntities) val = this.replaceEntitiesValue(val);
2,095✔
104
      
2,095✔
105
      const newval = this.options.tagValueProcessor(tagName, val, jPath, hasAttributes, isLeafNode);
2,095✔
106
      if(newval === null || newval === undefined){
2,095✔
107
        //don't parse
50✔
108
        return val;
50✔
109
      }else if(typeof newval !== typeof val || newval !== val){
2,095✔
110
        //overwrite
20✔
111
        return newval;
20✔
112
      }else if(this.options.trimValues){
2,045✔
113
        return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
1,965✔
114
      }else{
2,025✔
115
        const trimmedVal = val.trim();
60✔
116
        if(trimmedVal === val){
60✔
117
          return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
15✔
118
        }else{
60✔
119
          return val;
45✔
120
        }
45✔
121
      }
60✔
122
    }
2,095✔
123
  }
5,295✔
124
}
5,295✔
125

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

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

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

970✔
149
    const matches = getAllMatches(attrStr, attrsRegx);
970✔
150
    const len = matches.length; //don't make it inline
970✔
151
    const attrs = {};
970✔
152
    for (let i = 0; i < len; i++) {
970✔
153
      const attrName = this.resolveNameSpace(matches[i][1]);
1,420✔
154
      if (this.ignoreAttributesFn(attrName, jPath)) {
1,420✔
155
        continue
50✔
156
      }
50✔
157
      let oldVal = matches[i][4];
1,370✔
158
      let aName = this.options.attributeNamePrefix + attrName;
1,370✔
159
      if (attrName.length) {
1,420✔
160
        if (this.options.transformAttributeName) {
1,335✔
161
          aName = this.options.transformAttributeName(aName);
20✔
162
        }
20✔
163
        if(aName === "__proto__") aName  = "#__proto__";
1,335!
164
        if (oldVal !== undefined) {
1,335✔
165
          if (this.options.trimValues) {
1,245✔
166
            oldVal = oldVal.trim();
1,220✔
167
          }
1,220✔
168
          oldVal = this.replaceEntitiesValue(oldVal);
1,245✔
169
          const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPath);
1,245✔
170
          if(newVal === null || newVal === undefined){
1,245!
171
            //don't parse
×
UNCOV
172
            attrs[aName] = oldVal;
×
173
          }else if(typeof newVal !== typeof oldVal || newVal !== oldVal){
1,245!
174
            //overwrite
×
UNCOV
175
            attrs[aName] = newVal;
×
176
          }else{
1,245✔
177
            //parse
1,245✔
178
            attrs[aName] = parseValue(
1,245✔
179
              oldVal,
1,245✔
180
              this.options.parseAttributeValue,
1,245✔
181
              this.options.numberParseOptions
1,245✔
182
            );
1,245✔
183
          }
1,245✔
184
        } else if (this.options.allowBooleanAttributes) {
1,335✔
185
          attrs[aName] = true;
80✔
186
        }
80✔
187
      }
1,335✔
188
    }
1,420✔
189
    if (!Object.keys(attrs).length) {
970✔
190
      return;
95✔
191
    }
95✔
192
    if (this.options.attributesGroupName) {
970✔
193
      const attrCollection = {};
60✔
194
      attrCollection[this.options.attributesGroupName] = attrs;
60✔
195
      return attrCollection;
60✔
196
    }
60✔
197
    return attrs
815✔
198
  }
815✔
199
}
1,135✔
200

5✔
201
const parseXml = function(xmlData) {
5✔
202
  xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line
860✔
203
  const xmlObj = new xmlNode('!xml');
860✔
204
  let currentNode = xmlObj;
860✔
205
  let textData = "";
860✔
206
  let jPath = "";
860✔
207
  const docTypeReader = new DocTypeReader(this.options.processEntities);
860✔
208
  for(let i=0; i< xmlData.length; i++){//for each char in XML data
860✔
209
    const ch = xmlData[i];
61,820✔
210
    if(ch === '<'){
61,820✔
211
      // const nextIndex = i+1;
7,000✔
212
      // const _2ndChar = xmlData[nextIndex];
7,000✔
213
      if( xmlData[i+1] === '/') {//Closing Tag
7,000✔
214
        const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.")
2,865✔
215
        let tagName = xmlData.substring(i+2,closeIndex).trim();
2,865✔
216

2,865✔
217
        if(this.options.removeNSPrefix){
2,865✔
218
          const colonIndex = tagName.indexOf(":");
115✔
219
          if(colonIndex !== -1){
115✔
220
            tagName = tagName.substr(colonIndex+1);
50✔
221
          }
50✔
222
        }
115✔
223

2,860✔
224
        if(this.options.transformTagName) {
2,865✔
225
          tagName = this.options.transformTagName(tagName);
75✔
226
        }
75✔
227

2,860✔
228
        if(currentNode){
2,860✔
229
          textData = this.saveTextToParentTag(textData, currentNode, jPath);
2,860✔
230
        }
2,860✔
231

2,860✔
232
        //check if last tag of nested tag was unpaired tag
2,860✔
233
        const lastTagName = jPath.substring(jPath.lastIndexOf(".")+1);
2,860✔
234
        if(tagName && this.options.unpairedTags.indexOf(tagName) !== -1 ){
2,865✔
235
          throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
5✔
236
        }
5✔
237
        let propIndex = 0
2,855✔
238
        if(lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1 ){
2,865✔
239
          propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.')-1)
80✔
240
          this.tagsNodeStack.pop();
80✔
241
        }else{
2,865✔
242
          propIndex = jPath.lastIndexOf(".");
2,775✔
243
        }
2,775✔
244
        jPath = jPath.substring(0, propIndex);
2,855✔
245

2,855✔
246
        currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
2,855✔
247
        textData = "";
2,855✔
248
        i = closeIndex;
2,855✔
249
      } else if( xmlData[i+1] === '?') {
7,000✔
250

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

235✔
254
        textData = this.saveTextToParentTag(textData, currentNode, jPath);
235✔
255
        if( (this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags){
245✔
256
          //do nothing
25✔
257
        }else{
245✔
258
  
210✔
259
          const childNode = new xmlNode(tagData.tagName);
210✔
260
          childNode.add(this.options.textNodeName, "");
210✔
261
          
210✔
262
          if(tagData.tagName !== tagData.tagExp && tagData.attrExpPresent){
210✔
263
            childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath);
200✔
264
          }
200✔
265
          this.addChild(currentNode, childNode, jPath, i);
210✔
266
        }
210✔
267

235✔
268

235✔
269
        i = tagData.closeIndex + 1;
235✔
270
      } else if(xmlData.substr(i + 1, 3) === '!--') {
4,135✔
271
        const endIndex = findClosingIndex(xmlData, "-->", i+4, "Comment is not closed.")
60✔
272
        if(this.options.commentPropName){
60✔
273
          const comment = xmlData.substring(i + 4, endIndex - 2);
25✔
274

25✔
275
          textData = this.saveTextToParentTag(textData, currentNode, jPath);
25✔
276

25✔
277
          currentNode.add(this.options.commentPropName, [ { [this.options.textNodeName] : comment } ]);
25✔
278
        }
25✔
279
        i = endIndex;
55✔
280
      } else if( xmlData.substr(i + 1, 2) === '!D') {
3,890✔
281
        const result = docTypeReader.readDocType(xmlData, i);
75✔
282
        this.docTypeEntities = result.entities;
75✔
283
        i = result.i;
75✔
284
      }else if(xmlData.substr(i + 1, 2) === '![') {
3,830✔
285
        const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
260✔
286
        const tagExp = xmlData.substring(i + 9,closeIndex);
260✔
287

260✔
288
        textData = this.saveTextToParentTag(textData, currentNode, jPath);
260✔
289

260✔
290
        let val = this.parseTextData(tagExp, currentNode.tagname, jPath, true, false, true, true);
260✔
291
        if(val == undefined) val = "";
260✔
292

255✔
293
        //cdata should be set even if it is 0 length string
255✔
294
        if(this.options.cdataPropName){
260✔
295
          currentNode.add(this.options.cdataPropName, [ { [this.options.textNodeName] : tagExp } ]);
65✔
296
        }else{
260✔
297
          currentNode.add(this.options.textNodeName, val);
190✔
298
        }
190✔
299
        
255✔
300
        i = closeIndex + 2;
255✔
301
      }else {//Opening tag
3,755✔
302
        let result = readTagExp(xmlData,i, this.options.removeNSPrefix);
3,495✔
303
        let tagName= result.tagName;
3,495✔
304
        const rawTagName = result.rawTagName;
3,495✔
305
        let tagExp = result.tagExp;
3,495✔
306
        let attrExpPresent = result.attrExpPresent;
3,495✔
307
        let closeIndex = result.closeIndex;
3,495✔
308

3,495✔
309
        if (this.options.transformTagName) {
3,495✔
310
          //console.log(tagExp, tagName)
80✔
311
          const newTagName = this.options.transformTagName(tagName);
80✔
312
          if(tagExp === tagName) {
80✔
313
            tagExp = newTagName
50✔
314
          }
50✔
315
          tagName = newTagName;
80✔
316
        }
80✔
317
        
3,495✔
318
        //save text as child node
3,495✔
319
        if (currentNode && textData) {
3,495✔
320
          if(currentNode.tagname !== '!xml'){
2,480✔
321
            //when nested tag is found
2,235✔
322
            textData = this.saveTextToParentTag(textData, currentNode, jPath, false);
2,235✔
323
          }
2,235✔
324
        }
2,480✔
325

3,495✔
326
        //check if last tag was unpaired tag
3,495✔
327
        const lastTag = currentNode;
3,495✔
328
        if(lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1 ){
3,495✔
329
          currentNode = this.tagsNodeStack.pop();
120✔
330
          jPath = jPath.substring(0, jPath.lastIndexOf("."));
120✔
331
        }
120✔
332
        if(tagName !== xmlObj.tagname){
3,495✔
333
          jPath += jPath ? "." + tagName : tagName;
3,495✔
334
        }
3,495✔
335
        const startIndex = i;
3,495✔
336
        if (this.isItStopNode(this.stopNodesExact, this.stopNodesWildcard, jPath, tagName)) {
3,495✔
337
          let tagContent = "";
130✔
338
          //self-closing tag
130✔
339
          if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){
130✔
340
            if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
10!
341
              tagName = tagName.substr(0, tagName.length - 1);
×
342
              jPath = jPath.substr(0, jPath.length - 1);
×
UNCOV
343
              tagExp = tagName;
×
344
            }else{
10✔
345
              tagExp = tagExp.substr(0, tagExp.length - 1);
10✔
346
            }
10✔
347
            i = result.closeIndex;
10✔
348
          }
10✔
349
          //unpaired tag
120✔
350
          else if(this.options.unpairedTags.indexOf(tagName) !== -1){
120✔
351
            
5✔
352
            i = result.closeIndex;
5✔
353
          }
5✔
354
          //normal tag
115✔
355
          else{
115✔
356
            //read until closing tag is found
115✔
357
            const result = this.readStopNodeData(xmlData, rawTagName, closeIndex + 1);
115✔
358
            if(!result) throw new Error(`Unexpected end of ${rawTagName}`);
115!
359
            i = result.i;
115✔
360
            tagContent = result.tagContent;
115✔
361
          }
115✔
362

130✔
363
          const childNode = new xmlNode(tagName);
130✔
364

130✔
365
          if(tagName !== tagExp && attrExpPresent){
130✔
366
            childNode[":@"] = this.buildAttributesMap(tagExp, jPath
45✔
367
            );
45✔
368
          }
45✔
369
          if(tagContent) {
130✔
370
            tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true);
95✔
371
          }
95✔
372
          
130✔
373
          jPath = jPath.substr(0, jPath.lastIndexOf("."));
130✔
374
          childNode.add(this.options.textNodeName, tagContent);
130✔
375
          
130✔
376
          this.addChild(currentNode, childNode, jPath, startIndex);
130✔
377
        }else{
3,495✔
378
  //selfClosing tag
3,365✔
379
          if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){
3,365✔
380
            if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
290✔
381
              tagName = tagName.substr(0, tagName.length - 1);
105✔
382
              jPath = jPath.substr(0, jPath.length - 1);
105✔
383
              tagExp = tagName;
105✔
384
            }else{
290✔
385
              tagExp = tagExp.substr(0, tagExp.length - 1);
185✔
386
            }
185✔
387
            
290✔
388
            if(this.options.transformTagName) {
290✔
389
              const newTagName = this.options.transformTagName(tagName);
5✔
390
              if(tagExp === tagName) {
5✔
391
                tagExp = newTagName
5✔
392
              }
5✔
393
              tagName = newTagName;
5✔
394
            }
5✔
395

290✔
396
            const childNode = new xmlNode(tagName);
290✔
397
            if(tagName !== tagExp && attrExpPresent){
290✔
398
              childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
185✔
399
            }
185✔
400
            this.addChild(currentNode, childNode, jPath, startIndex);
290✔
401
            jPath = jPath.substr(0, jPath.lastIndexOf("."));
290✔
402
          }
290✔
403
    //opening tag
3,075✔
404
          else{
3,075✔
405
            const childNode = new xmlNode( tagName);
3,075✔
406
            this.tagsNodeStack.push(currentNode);
3,075✔
407
            
3,075✔
408
            if(tagName !== tagExp && attrExpPresent){
3,075✔
409
              childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
705✔
410
            }
705✔
411
            this.addChild(currentNode, childNode, jPath, startIndex);
3,075✔
412
            currentNode = childNode;
3,075✔
413
          }
3,075✔
414
          textData = "";
3,365✔
415
          i = closeIndex;
3,365✔
416
        }
3,365✔
417
      }
3,495✔
418
    }else{
61,820✔
419
      textData += xmlData[i];
54,820✔
420
    }
54,820✔
421
  }
61,820✔
422
  return xmlObj.child;
820✔
423
}
860✔
424

5✔
425
function addChild(currentNode, childNode, jPath, startIndex){
3,705✔
426
  // unset startIndex if not requested
3,705✔
427
  if (!this.options.captureMetaData) startIndex = undefined;
3,705✔
428
  const result = this.options.updateTag(childNode.tagname, jPath, childNode[":@"])
3,705✔
429
  if(result === false){
3,705✔
430
    //do nothing
25✔
431
  } else if(typeof result === "string"){
3,705✔
432
    childNode.tagname = result
3,680✔
433
    currentNode.addChild(childNode, startIndex);
3,680✔
434
  }else{
3,680!
435
    currentNode.addChild(childNode, startIndex);
×
UNCOV
436
  }
×
437
}
3,705✔
438

5✔
439
const replaceEntitiesValue = function(val){
5✔
440

3,010✔
441
  if(this.options.processEntities){
3,010✔
442
    for(let entityName in this.docTypeEntities){
2,960✔
443
      const entity = this.docTypeEntities[entityName];
2,597✔
444
      val = val.replace( entity.regx, entity.val);
2,597✔
445
    }
2,597✔
446
    for(let entityName in this.lastEntities){
2,960✔
447
      const entity = this.lastEntities[entityName];
14,192✔
448
      val = val.replace( entity.regex, entity.val);
14,192✔
449
    }
14,192✔
450
    if(this.options.htmlEntities){
2,960✔
451
      for(let entityName in this.htmlEntities){
185✔
452
        const entity = this.htmlEntities[entityName];
2,016✔
453
        val = val.replace( entity.regex, entity.val);
2,016✔
454
      }
2,016✔
455
    }
185✔
456
    val = val.replace( this.ampEntity.regex, this.ampEntity.val);
2,960✔
457
  }
2,960✔
458
  return val;
3,010✔
459
}
3,010✔
460
function saveTextToParentTag(textData, currentNode, jPath, isLeafNode) {
5,610✔
461
  if (textData) { //store previously collected data as textNode
5,610✔
462
    if(isLeafNode === undefined) isLeafNode = currentNode.child.length === 0
4,945✔
463
    
4,945✔
464
    textData = this.parseTextData(textData,
4,945✔
465
      currentNode.tagname,
4,945✔
466
      jPath,
4,945✔
467
      false,
4,945✔
468
      currentNode[":@"] ? Object.keys(currentNode[":@"]).length !== 0 : false,
4,945✔
469
      isLeafNode);
4,945✔
470

4,945✔
471
    if (textData !== undefined && textData !== "")
4,945✔
472
      currentNode.add(this.options.textNodeName, textData);
4,945✔
473
    textData = "";
4,945✔
474
  }
4,945✔
475
  return textData;
5,610✔
476
}
5,610✔
477

5✔
478
//TODO: use jPath to simplify the logic
5✔
479
/**
5✔
480
 * @param {Set} stopNodesExact
5✔
481
 * @param {Set} stopNodesWildcard
5✔
482
 * @param {string} jPath
5✔
483
 * @param {string} currentTagName
5✔
484
 */
5✔
485
function isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName){
3,495✔
486
  if(stopNodesWildcard && stopNodesWildcard.has(currentTagName)) return true;
3,495✔
487
  if(stopNodesExact && stopNodesExact.has(jPath)) return true;
3,495✔
488
  return false;
3,365✔
489
}
3,495✔
490

5✔
491
/**
5✔
492
 * Returns the tag Expression and where it is ending handling single-double quotes situation
5✔
493
 * @param {string} xmlData 
5✔
494
 * @param {number} i starting index
5✔
495
 * @returns 
5✔
496
 */
5✔
497
function tagExpWithClosingIndex(xmlData, i, closingChar = ">"){
3,895✔
498
  let attrBoundary;
3,895✔
499
  let tagExp = "";
3,895✔
500
  for (let index = i; index < xmlData.length; index++) {
3,895✔
501
    let ch = xmlData[index];
53,630✔
502
    if (attrBoundary) {
53,630✔
503
        if (ch === attrBoundary) attrBoundary = "";//reset
13,970✔
504
    } else if (ch === '"' || ch === "'") {
53,630✔
505
        attrBoundary = ch;
1,560✔
506
    } else if (ch === closingChar[0]) {
39,660✔
507
      if(closingChar[1]){
4,125✔
508
        if(xmlData[index + 1] === closingChar[1]){
480✔
509
          return {
235✔
510
            data: tagExp,
235✔
511
            index: index
235✔
512
          }
235✔
513
        }
235✔
514
      }else{
4,125✔
515
        return {
3,645✔
516
          data: tagExp,
3,645✔
517
          index: index
3,645✔
518
        }
3,645✔
519
      }
3,645✔
520
    } else if (ch === '\t') {
38,100✔
521
      ch = " "
20✔
522
    }
20✔
523
    tagExp += ch;
49,750✔
524
  }
49,750✔
525
}
3,895✔
526

5✔
527
function findClosingIndex(xmlData, str, i, errMsg){
3,455✔
528
  const closingIndex = xmlData.indexOf(str, i);
3,455✔
529
  if(closingIndex === -1){
3,455✔
530
    throw new Error(errMsg)
15✔
531
  }else{
3,455✔
532
    return closingIndex + str.length - 1;
3,440✔
533
  }
3,440✔
534
}
3,455✔
535

5✔
536
function readTagExp(xmlData,i, removeNSPrefix, closingChar = ">"){
3,895✔
537
  const result = tagExpWithClosingIndex(xmlData, i+1, closingChar);
3,895✔
538
  if(!result) return;
3,895✔
539
  let tagExp = result.data;
3,880✔
540
  const closeIndex = result.index;
3,880✔
541
  const separatorIndex = tagExp.search(/\s/);
3,880✔
542
  let tagName = tagExp;
3,880✔
543
  let attrExpPresent = true;
3,880✔
544
  if(separatorIndex !== -1){//separate tag name and attributes expression
3,895✔
545
    tagName = tagExp.substring(0, separatorIndex);
1,215✔
546
    tagExp = tagExp.substring(separatorIndex + 1).trimStart();
1,215✔
547
  }
1,215✔
548

3,880✔
549
  const rawTagName = tagName;
3,880✔
550
  if(removeNSPrefix){
3,895✔
551
    const colonIndex = tagName.indexOf(":");
280✔
552
    if(colonIndex !== -1){
280✔
553
      tagName = tagName.substr(colonIndex+1);
65✔
554
      attrExpPresent = tagName !== result.data.substr(colonIndex + 1);
65✔
555
    }
65✔
556
  }
280✔
557

3,880✔
558
  return {
3,880✔
559
    tagName: tagName,
3,880✔
560
    tagExp: tagExp,
3,880✔
561
    closeIndex: closeIndex,
3,880✔
562
    attrExpPresent: attrExpPresent,
3,880✔
563
    rawTagName: rawTagName,
3,880✔
564
  }
3,880✔
565
}
3,895✔
566
/**
5✔
567
 * find paired tag for a stop node
5✔
568
 * @param {string} xmlData 
5✔
569
 * @param {string} tagName 
5✔
570
 * @param {number} i 
5✔
571
 */
5✔
572
function readStopNodeData(xmlData, tagName, i){
115✔
573
  const startIndex = i;
115✔
574
  // Starting at 1 since we already have an open tag
115✔
575
  let openTagCount = 1;
115✔
576

115✔
577
  for (; i < xmlData.length; i++) {
115✔
578
    if( xmlData[i] === "<"){ 
4,725✔
579
      if (xmlData[i+1] === "/") {//close tag
425✔
580
          const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
260✔
581
          let closeTagName = xmlData.substring(i+2,closeIndex).trim();
260✔
582
          if(closeTagName === tagName){
260✔
583
            openTagCount--;
120✔
584
            if (openTagCount === 0) {
120✔
585
              return {
115✔
586
                tagContent: xmlData.substring(startIndex, i),
115✔
587
                i : closeIndex
115✔
588
              }
115✔
589
            }
115✔
590
          }
120✔
591
          i=closeIndex;
145✔
592
        } else if(xmlData[i+1] === '?') { 
425!
593
          const closeIndex = findClosingIndex(xmlData, "?>", i+1, "StopNode is not closed.")
×
UNCOV
594
          i=closeIndex;
×
595
        } else if(xmlData.substr(i + 1, 3) === '!--') { 
165✔
596
          const closeIndex = findClosingIndex(xmlData, "-->", i+3, "StopNode is not closed.")
5✔
597
          i=closeIndex;
5✔
598
        } else if(xmlData.substr(i + 1, 2) === '![') { 
165✔
599
          const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
5✔
600
          i=closeIndex;
5✔
601
        } else {
160✔
602
          const tagData = readTagExp(xmlData, i, '>')
155✔
603

155✔
604
          if (tagData) {
155✔
605
            const openTagName = tagData && tagData.tagName;
150✔
606
            if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length-1] !== "/") {
150✔
607
              openTagCount++;
5✔
608
            }
5✔
609
            i=tagData.closeIndex;
150✔
610
          }
150✔
611
        }
155✔
612
      }
425✔
613
  }//end for loop
115✔
614
}
115✔
615

5✔
616
function parseValue(val, shouldParse, options) {
3,225✔
617
  if (shouldParse && typeof val === 'string') {
3,225✔
618
    //console.log(options)
2,200✔
619
    const newval = val.trim();
2,200✔
620
    if(newval === 'true' ) return true;
2,200✔
621
    else if(newval === 'false' ) return false;
2,165✔
622
    else return toNumber(val, options);
2,150✔
623
  } else {
3,225✔
624
    if (isExist(val)) {
1,025✔
625
      return val;
1,025✔
626
    } else {
1,025!
627
      return '';
×
UNCOV
628
    }
×
629
  }
1,025✔
630
}
3,225✔
631

5✔
632
function fromCodePoint(str, base, prefix){
50✔
633
  const codePoint = Number.parseInt(str, base);
50✔
634

50✔
635
  if (codePoint >= 0 && codePoint <= 0x10FFFF) {
50✔
636
      return String.fromCodePoint(codePoint);
40✔
637
  } else {
50✔
638
      return prefix +str + ";";
10✔
639
  }
10✔
640
}
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