fast-xml-parser 3.19.0 → 3.21.1

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/README.md CHANGED
@@ -21,9 +21,10 @@ To cover expenses, we're planning to launch [FXP Enterprise](https://github.com/
21
21
  <a href="https://opencollective.com/fast-xml-parser/donate" target="_blank">
22
22
  <img src="https://opencollective.com/fast-xml-parser/donate/button@2x.png?color=blue" width=200 />
23
23
  </a>
24
- <a href="https://www.patreon.com/bePatron?u=9531404" data-patreon-widget-type="become-patron-button"><img src="https://c5.patreon.com/external/logo/become_a_patron_button.png" alt="Become a Patron!" width="200" /></a>
25
24
  <a href="https://paypal.me/naturalintelligence"> <img src="static/img/support_paypal.svg" alt="Stubmatic donate button" width="200"/></a>
26
25
 
26
+ Check [ThankYouBackers](https://github.com/NaturalIntelligence/ThankYouBackers) for our contributors
27
+
27
28
  ## Users
28
29
  List of some applications/projects using Fast XML Parser. (Raise an issue to submit yours)
29
30
 
@@ -104,13 +105,19 @@ The list of users is collected either from the list published by Github, cummuni
104
105
 
105
106
  ## How to use
106
107
 
107
- To use it in **NPM package** install it first
108
+ ### Installation
109
+
110
+ To use it as an **NPM package**:
111
+
112
+ `npm install fast-xml-parser`
108
113
 
109
- `$npm install fast-xml-parser` or using [yarn](https://yarnpkg.com/) `$yarn add fast-xml-parser`
114
+ Or using [yarn](https://yarnpkg.com/):
115
+
116
+ `yarn add fast-xml-parser`
110
117
 
111
118
  To use it from a **CLI** install it globally with the `-g` option.
112
119
 
113
- `$npm install fast-xml-parser -g`
120
+ `npm install fast-xml-parser -g`
114
121
 
115
122
  To use it on a **webpage** include it from a [CDN](https://cdnjs.com/libraries/fast-xml-parser)
116
123
 
@@ -118,14 +125,14 @@ To use it on a **webpage** include it from a [CDN](https://cdnjs.com/libraries/f
118
125
 
119
126
 
120
127
  ```js
121
- var jsonObj = parser.parse(xmlData [,options] );
128
+ const jsonObj = parser.parse(xmlData [,options] );
122
129
  ```
123
130
 
124
131
  ```js
125
- var parser = require('fast-xml-parser');
126
- var he = require('he');
132
+ const parser = require('fast-xml-parser');
133
+ const he = require('he');
127
134
 
128
- var options = {
135
+ const options = {
129
136
  attributeNamePrefix : "@_",
130
137
  attrNodeName: "attr", //default is 'false'
131
138
  textNodeName : "#text",
@@ -138,26 +145,32 @@ var options = {
138
145
  cdataTagName: "__cdata", //default is 'false'
139
146
  cdataPositionChar: "\\c",
140
147
  parseTrueNumberOnly: false,
148
+ numParseOptions:{
149
+ hex: true,
150
+ leadingZeros: true,
151
+ //skipLike: /\+[0-9]{10}/
152
+ },
141
153
  arrayMode: false, //"strict"
142
154
  attrValueProcessor: (val, attrName) => he.decode(val, {isAttributeValue: true}),//default is a=>a
143
155
  tagValueProcessor : (val, tagName) => he.decode(val), //default is a=>a
144
- stopNodes: ["parse-me-as-string"]
156
+ stopNodes: ["parse-me-as-string"],
157
+ alwaysCreateTextNode: false
145
158
  };
146
159
 
147
160
  if( parser.validate(xmlData) === true) { //optional (it'll return an object in case it's not valid)
148
- var jsonObj = parser.parse(xmlData,options);
161
+ let jsonObj = parser.parse(xmlData,options);
149
162
  }
150
163
 
151
164
  // Intermediate obj
152
- var tObj = parser.getTraversalObj(xmlData,options);
153
- var jsonObj = parser.convertToJson(tObj,options);
165
+ const tObj = parser.getTraversalObj(xmlData,options);
166
+ let jsonObj = parser.convertToJson(tObj,options);
154
167
 
155
168
  ```
156
169
  As you can notice in the above code, validator is not embedded with in the parser and expected to be called separately. However, you can pass `true` or validation options as 3rd parameter to the parser to trigger validator internally. It is same as above example.
157
170
 
158
171
  ```js
159
172
  try{
160
- var jsonObj = parser.parse(xmlData,options, true);
173
+ let jsonObj = parser.parse(xmlData,options, true);
161
174
  }catch(error){
162
175
  console.log(error.message)
163
176
  }
@@ -196,7 +209,7 @@ Validator returns the following object in case of error;
196
209
  * **tagValueProcessor** : Process tag value during transformation. Like HTML decoding, word capitalization, etc. Applicable in case of string only.
197
210
  * **attrValueProcessor** : Process attribute value during transformation. Like HTML decoding, word capitalization, etc. Applicable in case of string only.
198
211
  * **stopNodes** : an array of tag names which are not required to be parsed. Instead their values are parsed as string.
199
-
212
+ * **alwaysCreateTextNode** : When `true`, forces the parser always return a property for the `textNodeName` even if there are no attributes or node children.
200
213
  </details>
201
214
 
202
215
  <details>
@@ -219,18 +232,18 @@ $cat xmlfile.xml | xml2js [-ns|-a|-c|-v|-V] [-o outputfile.json]
219
232
  <summary>To use it <b>on webpage</b></summary>
220
233
 
221
234
  ```js
222
- var result = parser.validate(xmlData);
235
+ const result = parser.validate(xmlData);
223
236
  if (result !== true) console.log(result.err);
224
- var jsonObj = parser.parse(xmlData);
237
+ const jsonObj = parser.parse(xmlData);
225
238
  ```
226
239
  </details>
227
240
 
228
241
  ### JSON / JS Object to XML
229
242
 
230
243
  ```js
231
- var Parser = require("fast-xml-parser").j2xParser;
244
+ const Parser = require("fast-xml-parser").j2xParser;
232
245
  //default options need not to set
233
- var defaultOptions = {
246
+ const defaultOptions = {
234
247
  attributeNamePrefix : "@_",
235
248
  attrNodeName: "@", //default is false
236
249
  textNodeName : "#text",
@@ -241,10 +254,11 @@ var defaultOptions = {
241
254
  indentBy: " ",
242
255
  supressEmptyNode: false,
243
256
  tagValueProcessor: a=> he.encode(a, { useNamedReferences: true}),// default is a=>a
244
- attrValueProcessor: a=> he.encode(a, {isAttributeValue: isAttribute, useNamedReferences: true})// default is a=>a
257
+ attrValueProcessor: a=> he.encode(a, {isAttributeValue: isAttribute, useNamedReferences: true}),// default is a=>a
258
+ rootNodeName: "element"
245
259
  };
246
- var parser = new Parser(defaultOptions);
247
- var xml = parser.parse(json_or_js_obj);
260
+ const parser = new Parser(defaultOptions);
261
+ const xml = parser.parse(json_or_js_obj);
248
262
  ```
249
263
 
250
264
  <details>
@@ -263,6 +277,7 @@ With the correct options, you can get the almost original XML without losing any
263
277
  * **supressEmptyNode** : If set to `true`, tags with no value (text or nested tags) are written as self closing tags.
264
278
  * **tagValueProcessor** : Process tag value during transformation. Like HTML encoding, word capitalization, etc. Applicable in case of string only.
265
279
  * **attrValueProcessor** : Process attribute value during transformation. Like HTML encoding, word capitalization, etc. Applicable in case of string only.
280
+ * **rootNodeName** : When input js object is array, parser uses array index by default as tag name. You can set this property for proper response.
266
281
  </details>
267
282
 
268
283
  ## Benchmark
package/package.json CHANGED
@@ -1,19 +1,18 @@
1
1
  {
2
2
  "name": "fast-xml-parser",
3
- "version": "3.19.0",
3
+ "version": "3.21.1",
4
4
  "description": "Validate XML or Parse XML to JS/JSON very fast without C/C++ based libraries",
5
5
  "main": "./src/parser.js",
6
6
  "scripts": {
7
- "test": "jasmine spec/*spec.js",
7
+ "test": "nyc --reporter=lcov --reporter=text jasmine spec/*spec.js",
8
8
  "unit": "jasmine",
9
+ "coverage": "nyc report --reporter html --reporter text -t .nyc_output --report-dir .nyc_output/summary",
9
10
  "perf": "node ./benchmark/perfTest3.js",
10
11
  "lint": "eslint src/*.js spec/*.js",
11
12
  "bundle": "webpack && webpack --config webpack-prod.config.js",
12
- "coverage": "istanbul cover -x \"cli.js\" -x \"spec/*spec.js\" jasmine spec/*spec.js;",
13
- "coverage:check": "istanbul check-coverage --branch 90 --statement 90",
14
13
  "prettier": "prettier --write src/**/*.js",
15
14
  "publish-please": "publish-please",
16
- "prepublishOnly": "publish-please guard"
15
+ "checkReadiness": "publish-please --dry-run"
17
16
  },
18
17
  "bin": {
19
18
  "xml2js": "./cli.js"
@@ -76,10 +75,9 @@
76
75
  "babel-loader": "^8.2.2",
77
76
  "eslint": "^5.16.0",
78
77
  "he": "^1.2.0",
79
- "http-server": "^0.12.3",
80
- "istanbul": "^0.4.5",
81
78
  "jasmine": "^3.6.4",
82
79
  "nimnjs": "^1.3.2",
80
+ "nyc": "^15.1.0",
83
81
  "prettier": "^1.19.1",
84
82
  "publish-please": "^5.5.2",
85
83
  "webpack": "^4.46.0",
@@ -89,5 +87,8 @@
89
87
  "funding": {
90
88
  "type": "paypal",
91
89
  "url": "https://paypal.me/naturalintelligence"
90
+ },
91
+ "dependencies": {
92
+ "strnum": "^1.0.4"
92
93
  }
93
94
  }
package/src/json2xml.js CHANGED
@@ -32,6 +32,7 @@ const props = [
32
32
  'supressEmptyNode',
33
33
  'tagValueProcessor',
34
34
  'attrValueProcessor',
35
+ 'rootNodeName', //when array as root
35
36
  ];
36
37
 
37
38
  function Parser(options) {
@@ -54,6 +55,8 @@ function Parser(options) {
54
55
  this.replaceCDATAstr = replaceCDATAstr;
55
56
  this.replaceCDATAarr = replaceCDATAarr;
56
57
 
58
+ this.processTextOrObjNode = processTextOrObjNode
59
+
57
60
  if (this.options.format) {
58
61
  this.indentate = indentate;
59
62
  this.tagEndChar = '>\n';
@@ -79,16 +82,18 @@ function Parser(options) {
79
82
  }
80
83
 
81
84
  Parser.prototype.parse = function(jObj) {
85
+ if(Array.isArray(jObj) && this.options.rootNodeName && this.options.rootNodeName.length > 1){
86
+ jObj = {
87
+ [this.options.rootNodeName] : jObj
88
+ }
89
+ }
82
90
  return this.j2x(jObj, 0).val;
83
91
  };
84
92
 
85
93
  Parser.prototype.j2x = function(jObj, level) {
86
94
  let attrStr = '';
87
95
  let val = '';
88
- const keys = Object.keys(jObj);
89
- const len = keys.length;
90
- for (let i = 0; i < len; i++) {
91
- const key = keys[i];
96
+ for (let key in jObj) {
92
97
  if (typeof jObj[key] === 'undefined') {
93
98
  // supress undefined node
94
99
  } else if (jObj[key] === null) {
@@ -137,8 +142,7 @@ Parser.prototype.j2x = function(jObj, level) {
137
142
  } else if (item === null) {
138
143
  val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
139
144
  } else if (typeof item === 'object') {
140
- const result = this.j2x(item, level + 1);
141
- val += this.buildObjNode(result.val, key, result.attrStr, level);
145
+ val += this.processTextOrObjNode(item, key, level)
142
146
  } else {
143
147
  val += this.buildTextNode(item, key, '', level);
144
148
  }
@@ -153,14 +157,22 @@ Parser.prototype.j2x = function(jObj, level) {
153
157
  attrStr += ' ' + Ks[j] + '="' + this.options.attrValueProcessor('' + jObj[key][Ks[j]]) + '"';
154
158
  }
155
159
  } else {
156
- const result = this.j2x(jObj[key], level + 1);
157
- val += this.buildObjNode(result.val, key, result.attrStr, level);
160
+ val += this.processTextOrObjNode(jObj[key], key, level)
158
161
  }
159
162
  }
160
163
  }
161
164
  return {attrStr: attrStr, val: val};
162
165
  };
163
166
 
167
+ function processTextOrObjNode (object, key, level) {
168
+ const result = this.j2x(object, level + 1);
169
+ if (object[this.options.textNodeName] !== undefined && Object.keys(object).length === 1) {
170
+ return this.buildTextNode(result.val, key, result.attrStr, level);
171
+ } else {
172
+ return this.buildObjNode(result.val, key, result.attrStr, level);
173
+ }
174
+ }
175
+
164
176
  function replaceCDATAstr(str, cdata) {
165
177
  str = this.options.tagValueProcessor('' + str);
166
178
  if (this.options.cdataPositionChar === '' || str === '') {
@@ -183,7 +195,7 @@ function replaceCDATAarr(str, cdata) {
183
195
  }
184
196
 
185
197
  function buildObjectNode(val, key, attrStr, level) {
186
- if (attrStr && !val.includes('<')) {
198
+ if (attrStr && val.indexOf('<') === -1) {
187
199
  return (
188
200
  this.indentate(level) +
189
201
  '<' +
package/src/nimndata.js CHANGED
@@ -48,7 +48,7 @@ const _e = function(node, e_schema, options) {
48
48
  //attributes can't be repeated. hence check in children tags only
49
49
  str += chars.arrStart;
50
50
  const itemSchema = e_schema[0];
51
- //var itemSchemaType = itemSchema;
51
+ //const itemSchemaType = itemSchema;
52
52
  const arr_len = node.length;
53
53
 
54
54
  if (typeof itemSchema === 'string') {
package/src/node2json.js CHANGED
@@ -6,7 +6,7 @@ const convertToJson = function(node, options, parentTagName) {
6
6
  const jObj = {};
7
7
 
8
8
  // when no child node or attr is present
9
- if ((!node.child || util.isEmptyObject(node.child)) && (!node.attrsMap || util.isEmptyObject(node.attrsMap))) {
9
+ if (!options.alwaysCreateTextNode && (!node.child || util.isEmptyObject(node.child)) && (!node.attrsMap || util.isEmptyObject(node.attrsMap))) {
10
10
  return util.isExist(node.val) ? node.val : '';
11
11
  }
12
12
 
@@ -19,10 +19,10 @@ const _cToJsonStr = function(node, options, level) {
19
19
  const keys = Object.keys(node.child);
20
20
 
21
21
  for (let index = 0; index < keys.length; index++) {
22
- var tagname = keys[index];
22
+ const tagname = keys[index];
23
23
  if (node.child[tagname] && node.child[tagname].length > 1) {
24
24
  jObj += '"' + tagname + '" : [ ';
25
- for (var tag in node.child[tagname]) {
25
+ for (let tag in node.child[tagname]) {
26
26
  jObj += _cToJsonStr(node.child[tagname][tag], options) + ' , ';
27
27
  }
28
28
  jObj = jObj.substr(0, jObj.length - 1) + ' ] '; //remove extra comma in last
package/src/parser.d.ts CHANGED
@@ -12,10 +12,17 @@ type X2jOptions = {
12
12
  cdataTagName: false | string;
13
13
  cdataPositionChar: string;
14
14
  parseTrueNumberOnly: boolean;
15
+ numParseOptions: strnumOptions;
15
16
  tagValueProcessor: (tagValue: string, tagName: string) => string;
16
17
  attrValueProcessor: (attrValue: string, attrName: string) => string;
17
18
  stopNodes: string[];
19
+ alwaysCreateTextNode: boolean;
18
20
  };
21
+ type strnumOptions = {
22
+ hex: boolean;
23
+ leadingZeros: boolean,
24
+ skipLike?: RegExp
25
+ }
19
26
  type X2jOptionsOptional = Partial<X2jOptions>;
20
27
  type validationOptions = {
21
28
  allowBooleanAttributes: boolean;
@@ -39,7 +46,7 @@ type J2xOptionsOptional = Partial<J2xOptions>;
39
46
  type ESchema = string | object | Array<string|object>;
40
47
 
41
48
  type ValidationError = {
42
- err: { code: string; msg: string, line: number };
49
+ err: { code: string; msg: string, line: number, col: number };
43
50
  };
44
51
 
45
52
  export function parse(xmlData: string, options?: X2jOptionsOptional, validationOptions?: validationOptionsOptional | boolean): any;
package/src/parser.js CHANGED
@@ -6,7 +6,7 @@ const x2xmlnode = require('./xmlstr2xmlnode');
6
6
  const buildOptions = require('./util').buildOptions;
7
7
  const validator = require('./validator');
8
8
 
9
- exports.parse = function(xmlData, options, validationOption) {
9
+ exports.parse = function(xmlData, givenOptions = {}, validationOption) {
10
10
  if( validationOption){
11
11
  if(validationOption === true) validationOption = {}
12
12
 
@@ -15,7 +15,16 @@ exports.parse = function(xmlData, options, validationOption) {
15
15
  throw Error( result.err.msg)
16
16
  }
17
17
  }
18
- options = buildOptions(options, x2xmlnode.defaultOptions, x2xmlnode.props);
18
+ if(givenOptions.parseTrueNumberOnly
19
+ && givenOptions.parseNodeValue !== false
20
+ && !givenOptions.numParseOptions){
21
+
22
+ givenOptions.numParseOptions = {
23
+ leadingZeros: false,
24
+ }
25
+ }
26
+ let options = buildOptions(givenOptions, x2xmlnode.defaultOptions, x2xmlnode.props);
27
+
19
28
  const traversableObj = xmlToNodeobj.getTraversalObj(xmlData, options)
20
29
  //print(traversableObj, " ");
21
30
  return nodeToJson.convertToJson(traversableObj, options);
package/src/util.js CHANGED
@@ -10,6 +10,7 @@ const getAllMatches = function(string, regex) {
10
10
  let match = regex.exec(string);
11
11
  while (match) {
12
12
  const allmatches = [];
13
+ allmatches.startIndex = regex.lastIndex - match[0].length;
13
14
  const len = match.length;
14
15
  for (let index = 0; index < len; index++) {
15
16
  allmatches.push(match[index]);
@@ -67,7 +68,7 @@ exports.getValue = function(v) {
67
68
  // const fakeCallNoReturn = function() {};
68
69
 
69
70
  exports.buildOptions = function(options, defaultOptions, props) {
70
- var newOptions = {};
71
+ let newOptions = {};
71
72
  if (!options) {
72
73
  return defaultOptions; //if there are not options
73
74
  }
package/src/validator.js CHANGED
@@ -35,7 +35,7 @@ exports.validate = function (xmlData, options) {
35
35
  }else if (xmlData[i] === '<') {
36
36
  //starting of tag
37
37
  //read until you reach to '>' avoiding any '>' in attribute value
38
-
38
+ let tagStartPos = i;
39
39
  i++;
40
40
 
41
41
  if (xmlData[i] === '!') {
@@ -71,7 +71,7 @@ exports.validate = function (xmlData, options) {
71
71
  if (!validateTagName(tagName)) {
72
72
  let msg;
73
73
  if (tagName.trim().length === 0) {
74
- msg = "There is an unnecessary space between tag name and backward slash '</ ..'.";
74
+ msg = "Invalid space after '<'.";
75
75
  } else {
76
76
  msg = "Tag '"+tagName+"' is an invalid name.";
77
77
  }
@@ -87,6 +87,7 @@ exports.validate = function (xmlData, options) {
87
87
 
88
88
  if (attrStr[attrStr.length - 1] === '/') {
89
89
  //self closing tag
90
+ const attrStrStart = i - attrStr.length;
90
91
  attrStr = attrStr.substring(0, attrStr.length - 1);
91
92
  const isValid = validateAttributeString(attrStr, options);
92
93
  if (isValid === true) {
@@ -96,17 +97,20 @@ exports.validate = function (xmlData, options) {
96
97
  //the result from the nested function returns the position of the error within the attribute
97
98
  //in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute
98
99
  //this gives us the absolute index in the entire xml, which we can use to find the line at last
99
- return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line));
100
+ return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, attrStrStart + isValid.err.line));
100
101
  }
101
102
  } else if (closingTag) {
102
103
  if (!result.tagClosed) {
103
104
  return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
104
105
  } else if (attrStr.trim().length > 0) {
105
- return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, i));
106
+ return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
106
107
  } else {
107
108
  const otg = tags.pop();
108
- if (tagName !== otg) {
109
- return getErrorObject('InvalidTag', "Closing tag '"+otg+"' is expected inplace of '"+tagName+"'.", getLineNumberForPosition(xmlData, i));
109
+ if (tagName !== otg.tagName) {
110
+ let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos);
111
+ return getErrorObject('InvalidTag',
112
+ "Expected closing tag '"+otg.tagName+"' (opened in line "+openPos.line+", col "+openPos.col+") instead of closing tag '"+tagName+"'.",
113
+ getLineNumberForPosition(xmlData, tagStartPos));
110
114
  }
111
115
 
112
116
  //when there are no more tags, we reached the root level.
@@ -127,7 +131,7 @@ exports.validate = function (xmlData, options) {
127
131
  if (reachedRoot === true) {
128
132
  return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
129
133
  } else {
130
- tags.push(tagName);
134
+ tags.push({tagName, tagStartPos});
131
135
  }
132
136
  tagFound = true;
133
137
  }
@@ -168,8 +172,12 @@ exports.validate = function (xmlData, options) {
168
172
 
169
173
  if (!tagFound) {
170
174
  return getErrorObject('InvalidXml', 'Start tag expected.', 1);
171
- } else if (tags.length > 0) {
172
- return getErrorObject('InvalidXml', "Invalid '"+JSON.stringify(tags, null, 4).replace(/\r?\n/g, '')+"' found.", 1);
175
+ }else if (tags.length == 1) {
176
+ return getErrorObject('InvalidTag', "Unclosed tag '"+tags[0].tagName+"'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
177
+ }else if (tags.length > 0) {
178
+ return getErrorObject('InvalidXml', "Invalid '"+
179
+ JSON.stringify(tags.map(t => t.tagName), null, 4).replace(/\r?\n/g, '')+
180
+ "' found.", {line: 1, col: 1});
173
181
  }
174
182
 
175
183
  return true;
@@ -181,11 +189,11 @@ exports.validate = function (xmlData, options) {
181
189
  * @param {*} i
182
190
  */
183
191
  function readPI(xmlData, i) {
184
- var start = i;
192
+ const start = i;
185
193
  for (; i < xmlData.length; i++) {
186
194
  if (xmlData[i] == '?' || xmlData[i] == ' ') {
187
195
  //tagname
188
- var tagname = xmlData.substr(start, i - start);
196
+ const tagname = xmlData.substr(start, i - start);
189
197
  if (i > 5 && tagname === 'xml') {
190
198
  return getErrorObject('InvalidXml', 'XML declaration allowed only at the start of the document.', getLineNumberForPosition(xmlData, i));
191
199
  } else if (xmlData[i] == '?' && xmlData[i + 1] == '>') {
@@ -251,8 +259,8 @@ function readCommentAndCDATA(xmlData, i) {
251
259
  return i;
252
260
  }
253
261
 
254
- var doubleQuote = '"';
255
- var singleQuote = "'";
262
+ const doubleQuote = '"';
263
+ const singleQuote = "'";
256
264
 
257
265
  /**
258
266
  * Keep reading xmlData until '<' is found outside the attribute value.
@@ -269,7 +277,6 @@ function readAttributeStr(xmlData, i) {
269
277
  startChar = xmlData[i];
270
278
  } else if (startChar !== xmlData[i]) {
271
279
  //if vaue is enclosed with double quote then single quotes are allowed inside the value and vice versa
272
- continue;
273
280
  } else {
274
281
  startChar = '';
275
282
  }
@@ -310,23 +317,23 @@ function validateAttributeString(attrStr, options) {
310
317
  for (let i = 0; i < matches.length; i++) {
311
318
  if (matches[i][1].length === 0) {
312
319
  //nospace before attribute name: a="sd"b="saf"
313
- return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' has no space in starting.", getPositionFromMatch(attrStr, matches[i][0]))
320
+ return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' has no space in starting.", getPositionFromMatch(matches[i]))
314
321
  } else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
315
322
  //independent attribute: ab
316
- return getErrorObject('InvalidAttr', "boolean attribute '"+matches[i][2]+"' is not allowed.", getPositionFromMatch(attrStr, matches[i][0]));
323
+ return getErrorObject('InvalidAttr', "boolean attribute '"+matches[i][2]+"' is not allowed.", getPositionFromMatch(matches[i]));
317
324
  }
318
325
  /* else if(matches[i][6] === undefined){//attribute without value: ab=
319
326
  return { err: { code:"InvalidAttr",msg:"attribute " + matches[i][2] + " has no value assigned."}};
320
327
  } */
321
328
  const attrName = matches[i][2];
322
329
  if (!validateAttrName(attrName)) {
323
- return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(attrStr, matches[i][0]));
330
+ return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(matches[i]));
324
331
  }
325
332
  if (!attrNames.hasOwnProperty(attrName)) {
326
333
  //check for duplicate attribute.
327
334
  attrNames[attrName] = 1;
328
335
  } else {
329
- return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(attrStr, matches[i][0]));
336
+ return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(matches[i]));
330
337
  }
331
338
  }
332
339
 
@@ -373,7 +380,8 @@ function getErrorObject(code, message, lineNumber) {
373
380
  err: {
374
381
  code: code,
375
382
  msg: message,
376
- line: lineNumber,
383
+ line: lineNumber.line || lineNumber,
384
+ col: lineNumber.col,
377
385
  },
378
386
  };
379
387
  }
@@ -390,11 +398,16 @@ function validateTagName(tagname) {
390
398
 
391
399
  //this function returns the line number for the character at the given index
392
400
  function getLineNumberForPosition(xmlData, index) {
393
- var lines = xmlData.substring(0, index).split(/\r?\n/);
394
- return lines.length;
401
+ const lines = xmlData.substring(0, index).split(/\r?\n/);
402
+ return {
403
+ line: lines.length,
404
+
405
+ // column number is last line's length + 1, because column numbering starts at 1:
406
+ col: lines[lines.length - 1].length + 1
407
+ };
395
408
  }
396
409
 
397
- //this function returns the position of the last character of match within attrStr
398
- function getPositionFromMatch(attrStr, match) {
399
- return attrStr.indexOf(match) + match.length;
410
+ //this function returns the position of the first character of match within attrStr
411
+ function getPositionFromMatch(match) {
412
+ return match.startIndex + match[1].length;
400
413
  }
@@ -3,6 +3,8 @@
3
3
  const util = require('./util');
4
4
  const buildOptions = require('./util').buildOptions;
5
5
  const xmlNode = require('./xmlNode');
6
+ const toNumber = require("strnum");
7
+
6
8
  const regx =
7
9
  '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
8
10
  .replace(/NAME/g, util.nameRegexp);
@@ -32,13 +34,18 @@ const defaultOptions = {
32
34
  trimValues: true, //Trim string values of tag and attributes
33
35
  cdataTagName: false,
34
36
  cdataPositionChar: '\\c',
37
+ numParseOptions: {
38
+ hex: true,
39
+ leadingZeros: true
40
+ },
35
41
  tagValueProcessor: function(a, tagName) {
36
42
  return a;
37
43
  },
38
44
  attrValueProcessor: function(a, attrName) {
39
45
  return a;
40
46
  },
41
- stopNodes: []
47
+ stopNodes: [],
48
+ alwaysCreateTextNode: false
42
49
  //decodeStrict: false,
43
50
  };
44
51
 
@@ -60,7 +67,9 @@ const props = [
60
67
  'tagValueProcessor',
61
68
  'attrValueProcessor',
62
69
  'parseTrueNumberOnly',
63
- 'stopNodes'
70
+ 'numParseOptions',
71
+ 'stopNodes',
72
+ 'alwaysCreateTextNode'
64
73
  ];
65
74
  exports.props = props;
66
75
 
@@ -76,7 +85,7 @@ function processTagValue(tagName, val, options) {
76
85
  val = val.trim();
77
86
  }
78
87
  val = options.tagValueProcessor(val, tagName);
79
- val = parseValue(val, options.parseNodeValue, options.parseTrueNumberOnly);
88
+ val = parseValue(val, options.parseNodeValue, options.numParseOptions);
80
89
  }
81
90
 
82
91
  return val;
@@ -96,26 +105,13 @@ function resolveNameSpace(tagname, options) {
96
105
  return tagname;
97
106
  }
98
107
 
99
- function parseValue(val, shouldParse, parseTrueNumberOnly) {
108
+ function parseValue(val, shouldParse, options) {
100
109
  if (shouldParse && typeof val === 'string') {
101
- let parsed;
102
- if (val.trim() === '' || isNaN(val)) {
103
- parsed = val === 'true' ? true : val === 'false' ? false : val;
104
- } else {
105
- if (val.indexOf('0x') !== -1) {
106
- //support hexa decimal
107
- parsed = Number.parseInt(val, 16);
108
- } else if (val.indexOf('.') !== -1) {
109
- parsed = Number.parseFloat(val);
110
- val = val.replace(/\.?0+$/, "");
111
- } else {
112
- parsed = Number.parseInt(val, 10);
113
- }
114
- if (parseTrueNumberOnly) {
115
- parsed = String(parsed) === val ? parsed : val;
116
- }
117
- }
118
- return parsed;
110
+ //console.log(options)
111
+ const newval = val.trim();
112
+ if(newval === 'true' ) return true;
113
+ else if(newval === 'false' ) return false;
114
+ else return toNumber(val, options);
119
115
  } else {
120
116
  if (util.isExist(val)) {
121
117
  return val;
@@ -148,7 +144,7 @@ function buildAttributesMap(attrStr, options) {
148
144
  attrs[options.attributeNamePrefix + attrName] = parseValue(
149
145
  matches[i][4],
150
146
  options.parseAttributeValue,
151
- options.parseTrueNumberOnly
147
+ options.numParseOptions
152
148
  );
153
149
  } else if (options.allowBooleanAttributes) {
154
150
  attrs[options.attributeNamePrefix + attrName] = true;