fast-xml-parser 4.0.0-beta.3 → 4.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  Note: If you find missing information about particular minor version, that version must have been changed without any functional change in this library.
2
2
 
3
+ ** 4.0.0-beta.7 / 2021-12-09**
4
+ * fix Validator bug when an attribute has no value but '=' only
5
+ * XML Builder should suppress unpaired tags by default.
6
+ * documents update for missing features
7
+ * refactoring to use Object.assign
8
+ * refactoring to remove repeated code
9
+
10
+ ** 4.0.0-beta.6 / 2021-12-05**
11
+ * Support PI Tags processing
12
+ * Support `suppressBooleanAttributes` by XML Builder for attributes with value `true`.
13
+
14
+ ** 4.0.0-beta.5 / 2021-12-04**
15
+ * fix: when a tag with name "attributes"
16
+
17
+ ** 4.0.0-beta.4 / 2021-12-02**
18
+ * Support HTML document parsing
19
+ * skip stop nodes parsing when building the XML from JS object
20
+ * Support external entites without DOCTYPE
21
+ * update dev dependency: strnum v1.0.5 to fix long number issue
22
+
3
23
  ** 4.0.0-beta.3 / 2021-11-30**
4
24
  * support global stopNodes expression like "*.stop"
5
25
  * support self-closing and paired unpaired tags
package/README.md CHANGED
@@ -29,6 +29,7 @@ Check [ThankYouBackers](https://github.com/NaturalIntelligence/ThankYouBackers)
29
29
  <a href="http://nasa.github.io/" title="NASA" > <img src="https://avatars0.githubusercontent.com/u/848102" width="60px" ></a>
30
30
  <a href="https://github.com/prettier" title="Prettier" > <img src="https://avatars0.githubusercontent.com/u/25822731" width="60px" ></a>
31
31
  <a href="http://brain.js.org/" title="brain.js" > <img src="https://avatars2.githubusercontent.com/u/23732838" width="60px" ></a>
32
+ <a href="https://github.com/aws" title="AWS SDK" > <img src="https://avatars.githubusercontent.com/u/2232217" width="60px" ></a>
32
33
  <a href="#" title="NHS Connect" > <img src="https://avatars3.githubusercontent.com/u/20316669" width="60px" ></a>
33
34
  <a href="http://www.fda.gov/" title="Food and Drug Administration " > <img src="https://avatars2.githubusercontent.com/u/6471964" width="60px" ></a>
34
35
  <a href="http://www.magento.com/" title="Magento" > <img src="https://avatars2.githubusercontent.com/u/168457" width="60px" ></a>
@@ -54,6 +55,7 @@ Check the list of all known users [here](./USERs.md);
54
55
  * Supports comments
55
56
  * It can preserve Order of tags in JS object
56
57
  * You can control if a single tag should be parsed into array.
58
+ * Supports parsing of PI (Processing Instruction) tags with XML declaration tags
57
59
  * And many more other features.
58
60
 
59
61
  ## How to use
@@ -106,7 +108,8 @@ In a HTML page
106
108
  3. [XML Builder](./docs/v4/3.XMLBuilder.md)
107
109
  4. [XML Validator](./docs/v4/4.XMLValidator.md)
108
110
  5. [Entites](./docs/5.Entities.md)
109
-
111
+ 6. [HTML Document Parsing](./docs/6.HTMLParsing.md)
112
+ 7. [PI Tag processing](./docs/7.PITags.md)
110
113
  ## Performance
111
114
 
112
115
  ### XML Parser
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-xml-parser",
3
- "version": "4.0.0-beta.3",
3
+ "version": "4.0.0-beta.7",
4
4
  "description": "Validate XML, Parse XML, Build XML without C/C++ based libraries",
5
5
  "main": "./src/fxp.js",
6
6
  "scripts": {
@@ -47,14 +47,14 @@
47
47
  "@babel/preset-env": "^7.13.10",
48
48
  "@babel/register": "^7.13.8",
49
49
  "babel-loader": "^8.2.2",
50
- "eslint": "^5.16.0",
50
+ "eslint": "^8.3.0",
51
51
  "he": "^1.2.0",
52
52
  "jasmine": "^3.6.4",
53
53
  "nyc": "^15.1.0",
54
54
  "prettier": "^1.19.1",
55
55
  "publish-please": "^5.5.2",
56
- "webpack": "^4.46.0",
57
- "webpack-cli": "^3.3.12"
56
+ "webpack": "^5.64.4",
57
+ "webpack-cli": "^4.9.1"
58
58
  },
59
59
  "typings": "src/fxp.d.ts",
60
60
  "funding": {
@@ -62,6 +62,6 @@
62
62
  "url": "https://paypal.me/naturalintelligence"
63
63
  },
64
64
  "dependencies": {
65
- "strnum": "^1.0.4"
65
+ "strnum": "^1.0.5"
66
66
  }
67
67
  }
package/src/fxp.d.ts CHANGED
@@ -44,8 +44,10 @@ type XmlBuilderOptions = {
44
44
  indentBy: string;
45
45
  arrayNodeName: string;
46
46
  suppressEmptyNode: boolean;
47
+ suppressBooleanAttributes: boolean;
47
48
  preserveOrder: boolean;
48
49
  unpairedTags: string[];
50
+ stopNodes: string[];
49
51
  tagValueProcessor: (name: string, value: string) => string;
50
52
  attributeValueProcessor: (name: string, value: string) => string;
51
53
  processEntities: boolean;
@@ -66,6 +68,12 @@ type ValidationError = {
66
68
  export class XMLParser {
67
69
  constructor(options?: X2jOptionsOptional);
68
70
  parse(xmlData: string | Buffer ,validationOptions?: validationOptionsOptional | boolean): any;
71
+ /**
72
+ * Add Entity which is not by default supported by this library
73
+ * @param entityIndentifier {string} Eg: 'ent' for &ent;
74
+ * @param entityValue {string} Eg: '\r'
75
+ */
76
+ addEntity(entityIndentifier: string, entityValue: string): void;
69
77
  }
70
78
 
71
79
  export class XMLValidator{
package/src/util.js CHANGED
@@ -67,23 +67,6 @@ exports.getValue = function(v) {
67
67
  // const fakeCall = function(a) {return a;};
68
68
  // const fakeCallNoReturn = function() {};
69
69
 
70
- const buildOptions = function(options, defaultOptions, props) {
71
- let newOptions = {};
72
- if (!options) {
73
- return defaultOptions; //if there are not options
74
- }
75
-
76
- for (let i = 0; i < props.length; i++) {
77
- if (options[props[i]] !== undefined) {
78
- newOptions[props[i]] = options[props[i]];
79
- } else {
80
- newOptions[props[i]] = defaultOptions[props[i]];
81
- }
82
- }
83
- return newOptions;
84
- };
85
-
86
- exports.buildOptions = buildOptions;
87
70
  exports.isName = isName;
88
71
  exports.getAllMatches = getAllMatches;
89
72
  exports.nameRegexp = nameRegexp;
package/src/validator.js CHANGED
@@ -7,14 +7,9 @@ const defaultOptions = {
7
7
  unpairedTags: []
8
8
  };
9
9
 
10
- const props = [
11
- 'allowBooleanAttributes',
12
- 'unpairedTags'
13
- ];
14
-
15
10
  //const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g");
16
11
  exports.validate = function (xmlData, options) {
17
- options = util.buildOptions(options, defaultOptions, props);
12
+ options = Object.assign({}, defaultOptions, options);
18
13
 
19
14
  //xmlData = xmlData.replace(/(\r\n|\n|\r)/gm,"");//make it single line
20
15
  //xmlData = xmlData.replace(/(^\s*<\?xml.*?\?>)/g,"");//Remove XML starting tag
@@ -331,6 +326,8 @@ function validateAttributeString(attrStr, options) {
331
326
  if (matches[i][1].length === 0) {
332
327
  //nospace before attribute name: a="sd"b="saf"
333
328
  return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' has no space in starting.", getPositionFromMatch(matches[i]))
329
+ } else if (matches[i][3] !== undefined && matches[i][4] === undefined) {
330
+ return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' is without value.", getPositionFromMatch(matches[i]));
334
331
  } else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
335
332
  //independent attribute: ab
336
333
  return getErrorObject('InvalidAttr', "boolean attribute '"+matches[i][2]+"' is not allowed.", getPositionFromMatch(matches[i]));
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
  //parse Empty Node as self closing node
3
- const buildOptions = require('../util').buildOptions;
4
3
  const buildFromOrderedJs = require('./orderedJs2Xml');
5
4
 
6
5
  const defaultOptions = {
@@ -12,6 +11,7 @@ const defaultOptions = {
12
11
  format: false,
13
12
  indentBy: ' ',
14
13
  suppressEmptyNode: false,
14
+ suppressBooleanAttributes: true,
15
15
  tagValueProcessor: function(key, a) {
16
16
  return a;
17
17
  },
@@ -27,31 +27,12 @@ const defaultOptions = {
27
27
  "sQuot" : { regex: new RegExp("\'", "g"), val: "&apos;" },
28
28
  "dQuot" : { regex: new RegExp("\"", "g"), val: "&quot;" }
29
29
  },
30
- processEntities: true
30
+ processEntities: true,
31
+ stopNodes: []
31
32
  };
32
33
 
33
- const props = [
34
- 'attributeNamePrefix',
35
- 'attributesGroupName',
36
- 'textNodeName',
37
- 'ignoreAttributes',
38
- 'cdataPropName',
39
- 'format',
40
- 'indentBy',
41
- 'suppressEmptyNode',
42
- 'tagValueProcessor',
43
- 'attributeValueProcessor',
44
- 'arrayNodeName', //when array as root
45
- 'preserveOrder',
46
- "commentPropName",
47
- "unpairedTags",
48
- "entities",
49
- "processEntities",
50
- // 'rootNodeName', //when jsObject have multiple properties on root level
51
- ];
52
-
53
34
  function Builder(options) {
54
- this.options = buildOptions(options, defaultOptions, props);
35
+ this.options = Object.assign({}, defaultOptions, options);
55
36
  if (this.options.ignoreAttributes || this.options.attributesGroupName) {
56
37
  this.isAttribute = function(/*a*/) {
57
38
  return false;
@@ -7,10 +7,10 @@ const {EOL} = require('os');
7
7
  * @returns
8
8
  */
9
9
  function toXml(jArray, options){
10
- return arrToStr( jArray, options, 0);
10
+ return arrToStr( jArray, options, "", 0);
11
11
  }
12
12
 
13
- function arrToStr(arr, options, level){
13
+ function arrToStr(arr, options, jPath, level){
14
14
  let xmlStr = "";
15
15
 
16
16
  let indentation = "";
@@ -21,10 +21,16 @@ function arrToStr(arr, options, level){
21
21
  for (let i = 0; i < arr.length; i++) {
22
22
  const tagObj = arr[i];
23
23
  const tagName = propName(tagObj);
24
+ let newJPath = "";
25
+ if(jPath.length === 0) newJPath = tagName
26
+ else newJPath = `${jPath}.${tagName}`;
24
27
 
25
28
  if(tagName === options.textNodeName){
26
- let tagText = options.tagValueProcessor( tagName, tagObj[tagName]);
27
- tagText = replaceEntitiesValue(tagText, options);
29
+ let tagText = tagObj[tagName];
30
+ if(!isStopNode(newJPath, options)){
31
+ tagText = options.tagValueProcessor( tagName, tagText);
32
+ tagText = replaceEntitiesValue(tagText, options);
33
+ }
28
34
  xmlStr += indentation + tagText;
29
35
  continue;
30
36
  }else if( tagName === options.cdataPropName){
@@ -33,16 +39,18 @@ function arrToStr(arr, options, level){
33
39
  }else if( tagName === options.commentPropName){
34
40
  xmlStr += indentation + `<!--${tagObj[tagName][0][options.textNodeName]}-->`;
35
41
  continue;
42
+ }else if( tagName[0] === "?"){
43
+ const attStr = attr_to_str(tagObj[":@"], options);
44
+ xmlStr += indentation + `<${tagName} ${tagObj[tagName][0][options.textNodeName]} ${attStr}?>`;
45
+ continue;
36
46
  }
37
- const attStr = attr_to_str(tagObj.attributes, options);
47
+ const attStr = attr_to_str(tagObj[":@"], options);
38
48
  let tagStart = indentation + `<${tagName}${attStr}`;
39
- let tagValue = arrToStr(tagObj[tagName], options, level + 1);
40
- if( (!tagValue || tagValue.length === 0) && options.suppressEmptyNode){
41
- if(options.unpairedTags.indexOf(tagName) !== -1){
42
- xmlStr += tagStart + ">";
43
- }else{
44
- xmlStr += tagStart + "/>";
45
- }
49
+ let tagValue = arrToStr(tagObj[tagName], options, newJPath, level + 1);
50
+ if(options.unpairedTags.indexOf(tagName) !== -1){
51
+ xmlStr += tagStart + ">";
52
+ }else if( (!tagValue || tagValue.length === 0) && options.suppressEmptyNode){
53
+ xmlStr += tagStart + "/>";
46
54
  }else{
47
55
  //TODO: node with only text value should not parse the text value in next line
48
56
  xmlStr += tagStart + `>${tagValue}${indentation}</${tagName}>` ;
@@ -56,7 +64,7 @@ function propName(obj){
56
64
  const keys = Object.keys(obj);
57
65
  for (let i = 0; i < keys.length; i++) {
58
66
  const key = keys[i];
59
- if(key !== "attributes") return key;
67
+ if(key !== ":@") return key;
60
68
  }
61
69
  }
62
70
 
@@ -66,12 +74,25 @@ function attr_to_str(attrMap, options){
66
74
  for( attr in attrMap){
67
75
  let attrVal = options.attributeValueProcessor(attr, attrMap[attr]);
68
76
  attrVal = replaceEntitiesValue(attrVal, options);
69
- attrStr+= ` ${attr.substr(options.attributeNamePrefix.length)}="${attrVal}"`;
77
+ if(attrVal === true && options.suppressBooleanAttributes){
78
+ attrStr+= ` ${attr.substr(options.attributeNamePrefix.length)}`;
79
+ }else{
80
+ attrStr+= ` ${attr.substr(options.attributeNamePrefix.length)}="${attrVal}"`;
81
+ }
70
82
  }
71
83
  }
72
84
  return attrStr;
73
85
  }
74
86
 
87
+ function isStopNode(jPath, options){
88
+ jPath = jPath.substr(0,jPath.length - options.textNodeName.length - 1);
89
+ let tagName = jPath.substr(jPath.lastIndexOf(".") + 1);
90
+ for(let index in options.stopNodes){
91
+ if(options.stopNodes[index] === jPath || options.stopNodes[index] === "*."+tagName) return true;
92
+ }
93
+ return false;
94
+ }
95
+
75
96
  function replaceEntitiesValue(textValue, options){
76
97
  if(textValue && textValue.length > 0 && options.processEntities){
77
98
  for (const entityName in options.entities) {
@@ -31,36 +31,9 @@ const defaultOptions = {
31
31
  htmlEntities: false,
32
32
  };
33
33
 
34
- const props = [
35
- 'preserveOrder',
36
- 'attributeNamePrefix',
37
- 'attributesGroupName',
38
- 'textNodeName',
39
- 'ignoreAttributes',
40
- 'removeNSPrefix',
41
- 'allowBooleanAttributes',
42
- 'parseTagValue',
43
- 'parseAttributeValue',
44
- 'trimValues',
45
- 'cdataPropName',
46
- 'tagValueProcessor',
47
- 'attributeValueProcessor',
48
- 'numberParseOptions',
49
- 'stopNodes',
50
- 'alwaysCreateTextNode',
51
- 'isArray',
52
- 'commentPropName',
53
- 'unpairedTags',
54
- 'processEntities',
55
- 'htmlEntities'
56
- ];
57
-
58
- const util = require('../util');
59
-
60
34
  const buildOptions = function(options) {
61
- return util.buildOptions(options, defaultOptions, props);
35
+ return Object.assign({}, defaultOptions, options);
62
36
  };
63
37
 
64
38
  exports.buildOptions = buildOptions;
65
- exports.defaultOptions = defaultOptions;
66
- exports.props = props;
39
+ exports.defaultOptions = defaultOptions;
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ ///@ts-check
2
3
 
3
4
  const util = require('../util');
4
5
  const xmlNode = require('./xmlNode');
@@ -40,18 +41,30 @@ class OrderedObjParser{
40
41
  "reg" : { regex: /&(reg|#174);/g, val: "®" },
41
42
  "inr" : { regex: /&(inr|#8377);/g, val: "₹" },
42
43
  };
44
+ this.addExternalEntities = addExternalEntities;
43
45
  this.parseXml = parseXml;
44
46
  this.parseTextData = parseTextData;
45
47
  this.resolveNameSpace = resolveNameSpace;
46
48
  this.buildAttributesMap = buildAttributesMap;
47
49
  this.isItStopNode = isItStopNode;
48
50
  this.replaceEntitiesValue = replaceEntitiesValue;
49
- this.readTagExp = readTagExp;
50
51
  this.readStopNodeData = readStopNodeData;
52
+ this.saveTextToParentTag = saveTextToParentTag;
51
53
  }
52
54
 
53
55
  }
54
56
 
57
+ function addExternalEntities(externalEntities){
58
+ const entKeys = Object.keys(externalEntities);
59
+ for (let i = 0; i < entKeys.length; i++) {
60
+ const ent = entKeys[i];
61
+ this.lastEntities[ent] = {
62
+ regex: new RegExp("&"+ent+";","g"),
63
+ val : externalEntities[ent]
64
+ }
65
+ }
66
+ }
67
+
55
68
  /**
56
69
  * @param {string} val
57
70
  * @param {string} tagName
@@ -184,7 +197,7 @@ const parseXml = function(xmlData) {
184
197
  , currentNode.tagname
185
198
  , jPath
186
199
  ,false
187
- , currentNode.attributes ? Object.keys(currentNode.attributes).length !== 0 : false
200
+ , currentNode[":@"] ? Object.keys(currentNode[":@"]).length !== 0 : false
188
201
  , Object.keys(currentNode.child).length === 0);
189
202
  if(textData !== undefined && textData !== "") currentNode.add(this.options.textNodeName, textData);
190
203
  textData = "";
@@ -196,24 +209,26 @@ const parseXml = function(xmlData) {
196
209
  textData = "";
197
210
  i = closeIndex;
198
211
  } else if( xmlData[i+1] === '?') {
199
- i = findClosingIndex(xmlData, "?>", i, "Pi Tag is not closed.")
212
+ let tagData = readTagExp(xmlData,i, false, "?>");
213
+ if(!tagData) throw new Error("Pi Tag is not closed.");
214
+ textData = this.saveTextToParentTag(textData, currentNode, jPath);
215
+
216
+ const childNode = new xmlNode(tagData.tagName);
217
+ childNode.add(this.options.textNodeName, "");
218
+
219
+ if(tagData.tagName !== tagData.tagExp && tagData.attrExpPresent){
220
+ childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath);
221
+ }
222
+ currentNode.addChild(childNode);
223
+
224
+ i = tagData.closeIndex + 1;
200
225
  } else if(xmlData.substr(i + 1, 3) === '!--') {
201
226
  const endIndex = findClosingIndex(xmlData, "-->", i, "Comment is not closed.")
202
227
  if(this.options.commentPropName){
203
228
  const comment = xmlData.substring(i + 4, endIndex - 2);
204
229
 
205
- //TODO: remove repeated code
206
- if(textData){ //store previously collected data as textNode
207
- textData = this.parseTextData(textData
208
- , currentNode.tagname
209
- , jPath
210
- ,false
211
- , currentNode.attributes ? Object.keys(currentNode.attributes).length !== 0 : false
212
- , Object.keys(currentNode.child).length === 0);
213
-
214
- if(textData !== undefined && textData !== "") currentNode.add(this.options.textNodeName, textData);
215
- textData = "";
216
- }
230
+ textData = this.saveTextToParentTag(textData, currentNode, jPath);
231
+
217
232
  currentNode.add(this.options.commentPropName, [ { [this.options.textNodeName] : comment } ]);
218
233
  }
219
234
  i = endIndex;
@@ -225,17 +240,7 @@ const parseXml = function(xmlData) {
225
240
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
226
241
  const tagExp = xmlData.substring(i + 9,closeIndex);
227
242
 
228
- if(textData){ //store previously collected data as textNode
229
- textData = this.parseTextData(textData
230
- , currentNode.tagname
231
- , jPath
232
- ,false
233
- , currentNode.attributes ? Object.keys(currentNode.attributes).length !== 0 : false
234
- , Object.keys(currentNode.child).length === 0);
235
-
236
- if(textData !== undefined && textData !== "") currentNode.add(this.options.textNodeName, textData);
237
- textData = "";
238
- }
243
+ textData = this.saveTextToParentTag(textData, currentNode, jPath);
239
244
 
240
245
  //cdata should be set even if it is 0 length string
241
246
  if(this.options.cdataPropName){
@@ -251,7 +256,7 @@ const parseXml = function(xmlData) {
251
256
  i = closeIndex + 2;
252
257
  }else {//Opening tag
253
258
 
254
- let result = this.readTagExp(xmlData,i);
259
+ let result = readTagExp(xmlData,i, this. options.removeNSPrefix);
255
260
  let tagName= result.tagName;
256
261
  let tagExp = result.tagExp;
257
262
  let attrExpPresent = result.attrExpPresent;
@@ -265,7 +270,7 @@ const parseXml = function(xmlData) {
265
270
  , currentNode.tagname
266
271
  , jPath
267
272
  , false
268
- , currentNode.attributes ? Object.keys(currentNode.attributes).length !== 0 : false
273
+ , currentNode[":@"] ? Object.keys(currentNode[":@"]).length !== 0 : false
269
274
  , false);
270
275
  if(textData !== undefined && textData !== "") currentNode.add(this.options.textNodeName, textData);
271
276
  textData = "";
@@ -299,7 +304,7 @@ const parseXml = function(xmlData) {
299
304
 
300
305
  const childNode = new xmlNode(tagName);
301
306
  if(tagName !== tagExp && attrExpPresent){
302
- childNode.attributes = this.buildAttributesMap(tagExp, jPath);
307
+ childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
303
308
  }
304
309
  jPath = jPath.substr(0, jPath.lastIndexOf("."));
305
310
  childNode.add(this.options.textNodeName, tagContent);
@@ -318,7 +323,7 @@ const parseXml = function(xmlData) {
318
323
 
319
324
  const childNode = new xmlNode(tagName);
320
325
  if(tagName !== tagExp && attrExpPresent){
321
- childNode.attributes = this.buildAttributesMap(tagExp, jPath);
326
+ childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
322
327
  }
323
328
  jPath = jPath.substr(0, jPath.lastIndexOf("."));
324
329
  currentNode.addChild(childNode);
@@ -329,7 +334,7 @@ const parseXml = function(xmlData) {
329
334
  this.tagsNodeStack.push(currentNode);
330
335
 
331
336
  if(tagName !== tagExp && attrExpPresent){
332
- childNode.attributes = this.buildAttributesMap(tagExp, jPath);
337
+ childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
333
338
  }
334
339
  currentNode.addChild(childNode);
335
340
  currentNode = childNode;
@@ -364,6 +369,22 @@ const replaceEntitiesValue = function(val){
364
369
  }
365
370
  return val;
366
371
  }
372
+ function saveTextToParentTag(textData, currentNode, jPath) {
373
+ if (textData) { //store previously collected data as textNode
374
+ textData = this.parseTextData(textData,
375
+ currentNode.tagname,
376
+ jPath,
377
+ false,
378
+ currentNode[":@"] ? Object.keys(currentNode[":@"]).length !== 0 : false,
379
+ Object.keys(currentNode.child).length === 0);
380
+
381
+ if (textData !== undefined && textData !== "")
382
+ currentNode.add(this.options.textNodeName, textData);
383
+ textData = "";
384
+ }
385
+ return textData;
386
+ }
387
+
367
388
  //TODO: use jPath to simplify the logic
368
389
  /**
369
390
  *
@@ -386,7 +407,7 @@ function isItStopNode(stopNodes, jPath, currentTagName){
386
407
  * @param {number} i starting index
387
408
  * @returns
388
409
  */
389
- function tagExpWithClosingIndex(xmlData, i){
410
+ function tagExpWithClosingIndex(xmlData, i, closingChar = ">"){
390
411
  let attrBoundary;
391
412
  let tagExp = "";
392
413
  for (let index = i; index < xmlData.length; index++) {
@@ -395,11 +416,20 @@ function tagExpWithClosingIndex(xmlData, i){
395
416
  if (ch === attrBoundary) attrBoundary = "";//reset
396
417
  } else if (ch === '"' || ch === "'") {
397
418
  attrBoundary = ch;
398
- } else if (ch === '>') {
419
+ } else if (ch === closingChar[0]) {
420
+ if(closingChar[1]){
421
+ if(xmlData[index + 1] === closingChar[1]){
422
+ return {
423
+ data: tagExp,
424
+ index: index
425
+ }
426
+ }
427
+ }else{
399
428
  return {
400
429
  data: tagExp,
401
430
  index: index
402
431
  }
432
+ }
403
433
  } else if (ch === '\t') {
404
434
  ch = " "
405
435
  }
@@ -416,8 +446,9 @@ function findClosingIndex(xmlData, str, i, errMsg){
416
446
  }
417
447
  }
418
448
 
419
- function readTagExp(xmlData,i){
420
- const result = tagExpWithClosingIndex(xmlData, i+1);
449
+ function readTagExp(xmlData,i, removeNSPrefix, closingChar = ">"){
450
+ const result = tagExpWithClosingIndex(xmlData, i+1, closingChar);
451
+ if(!result) return;
421
452
  let tagExp = result.data;
422
453
  const closeIndex = result.index;
423
454
  const separatorIndex = tagExp.search(/\s/);
@@ -428,7 +459,7 @@ function readTagExp(xmlData,i){
428
459
  tagExp = tagExp.substr(separatorIndex + 1);
429
460
  }
430
461
 
431
- if(this. options.removeNSPrefix){
462
+ if(removeNSPrefix){
432
463
  const colonIndex = tagName.indexOf(":");
433
464
  if(colonIndex !== -1){
434
465
  tagName = tagName.substr(colonIndex+1);
@@ -4,8 +4,11 @@ const { prettify} = require("./node2json");
4
4
  const validator = require('../validator');
5
5
 
6
6
  class XMLParser{
7
+
7
8
  constructor(options){
9
+ this.externalEntities = {};
8
10
  this.options = buildOptions(options);
11
+
9
12
  }
10
13
  /**
11
14
  * Parse XML dats to JS object
@@ -28,10 +31,26 @@ class XMLParser{
28
31
  }
29
32
  }
30
33
  const orderedObjParser = new OrderedObjParser(this.options);
34
+ orderedObjParser.addExternalEntities(this.externalEntities);
31
35
  const orderedResult = orderedObjParser.parseXml(xmlData);
32
36
  if(this.options.preserveOrder || orderedResult === undefined) return orderedResult;
33
37
  else return prettify(orderedResult, this.options);
34
38
  }
39
+
40
+ /**
41
+ * Add Entity which is not by default supported by this library
42
+ * @param {string} key
43
+ * @param {string} value
44
+ */
45
+ addEntity(key, value){
46
+ if(value.indexOf("&") !== -1){
47
+ throw new Error("Entity value can't have '&'")
48
+ }else if(key.indexOf("&") !== -1 || key.indexOf(";") !== -1){
49
+ throw new Error("An entity must be set without '&' and ';'. Eg. use '#xD' for '&#xD;'")
50
+ }else{
51
+ this.externalEntities[key] = value;
52
+ }
53
+ }
35
54
  }
36
55
 
37
56
  module.exports = XMLParser;
@@ -37,8 +37,8 @@ function compress(arr, options, jPath){
37
37
  let val = compress(tagObj[property], options, newJpath);
38
38
  const isLeaf = isLeafTag(val, options);
39
39
 
40
- if(tagObj.attributes){
41
- assignAttributes( val, tagObj.attributes, newJpath, options);
40
+ if(tagObj[":@"]){
41
+ assignAttributes( val, tagObj[":@"], newJpath, options);
42
42
  }else if(Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode){
43
43
  val = val[options.textNodeName];
44
44
  }else if(Object.keys(val).length === 0){
@@ -74,7 +74,7 @@ function propName(obj){
74
74
  const keys = Object.keys(obj);
75
75
  for (let i = 0; i < keys.length; i++) {
76
76
  const key = keys[i];
77
- if(key !== "attributes") return key;
77
+ if(key !== ":@") return key;
78
78
  }
79
79
  }
80
80
 
@@ -4,15 +4,15 @@ class XmlNode{
4
4
  constructor(tagname) {
5
5
  this.tagname = tagname;
6
6
  this.child = []; //nested tags, text, cdata, comments in order
7
- this.attributes = {}; //attributes map
7
+ this[":@"] = {}; //attributes map
8
8
  }
9
9
  add(key,val){
10
10
  // this.child.push( {name : key, val: val, isCdata: isCdata });
11
11
  this.child.push( {[key]: val });
12
12
  }
13
13
  addChild(node) {
14
- if(node.attributes && Object.keys(node.attributes).length > 0){
15
- this.child.push( { [node.tagname]: node.child, attributes: node.attributes });
14
+ if(node[":@"] && Object.keys(node[":@"]).length > 0){
15
+ this.child.push( { [node.tagname]: node.child, [":@"]: node[":@"] });
16
16
  }else{
17
17
  this.child.push( { [node.tagname]: node.child });
18
18
  }