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

NaturalIntelligence / fast-xml-parser / 20157262184

12 Dec 2025 05:15AM UTC coverage: 97.601% (+0.04%) from 97.564%
20157262184

push

github

amitguptagwl
update publish detail

1106 of 1148 branches covered (96.34%)

8992 of 9213 relevant lines covered (97.6%)

483948.44 hits per line

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

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

850✔
59
    if(this.options.stopNodes && this.options.stopNodes.length > 0){
850✔
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
  }
850✔
73

5✔
74
}
5✔
75

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

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

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

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

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

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

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

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

2,845✔
223
        if(this.options.transformTagName) {
2,850✔
224
          tagName = this.options.transformTagName(tagName);
75✔
225
        }
75✔
226

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

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

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

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

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

230✔
267

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5✔
615
function parseValue(val, shouldParse, options) {
3,210✔
616
  if (shouldParse && typeof val === 'string') {
3,210✔
617
    //console.log(options)
2,190✔
618
    const newval = val.trim();
2,190✔
619
    if(newval === 'true' ) return true;
2,190✔
620
    else if(newval === 'false' ) return false;
2,155✔
621
    else return toNumber(val, options);
2,140✔
622
  } else {
3,210✔
623
    if (isExist(val)) {
1,020✔
624
      return val;
1,020✔
625
    } else {
1,020!
626
      return '';
×
627
    }
×
628
  }
1,020✔
629
}
3,210✔
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