fast-xml-parser 3.13.0 → 3.16.0

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
@@ -27,14 +27,18 @@ List of some applications/projects using Fast XML Parser. (Raise an issue to sub
27
27
  <a href="https://nevatrip.ru/" title="nevatrip" > <img src="https://avatars2.githubusercontent.com/u/35730984" width="80px" ></a>
28
28
  <a href="http://www.smartbear.com" title="SmartBear Software" > <img src="https://avatars2.githubusercontent.com/u/1644671" width="80px" ></a>
29
29
  <a href="http://eosnavigator.com/" title="nevatrip" > <img src="https://avatars1.githubusercontent.com/u/40260563" width="80px" ></a>
30
- <a href="http://pds.nasa.gov/" title="NASA-PDS" > <img src="https://avatars2.githubusercontent.com/u/26313833" width="80px" ></a>
30
+ <a href="http://nasa.github.io/" title="NASA" > <img src="https://avatars0.githubusercontent.com/u/848102" width="80px" ></a>
31
31
  <a href="http://qgis.org/" title="QGIS" > <img src="https://avatars2.githubusercontent.com/u/483444" width="80px" ></a>
32
32
  <a href="http://www.craft.ai/" title="craft ai" > <img src="https://avatars1.githubusercontent.com/u/12046764" width="80px" ></a>
33
33
  <a href="http://brownspace.org/" title="Brown Space Engineering" > <img src="https://avatars2.githubusercontent.com/u/5504507" width="80px" ></a>
34
34
  <a href="http://www.appcelerator.com/" title="Team Appcelerator" > <img src="https://avatars1.githubusercontent.com/u/82188" width="80px" ></a>
35
+ <a href="https://xmllint.com/" title="XML Lint" > <img src="https://xmllint.com/assets/logo.png" width="80px" ></a>
36
+ <a href="https://github.com/prettier" title="Prettier" > <img src="https://avatars0.githubusercontent.com/u/25822731" width="80px" ></a>
37
+ <a href="https://github.com/dolanmiu/docx" title="docx" > <img src="https://i.imgur.com/37uBGhO.gif" width="80px" ></a>
35
38
  <a href="http://orange-opensource.github.io/" title="Open Source by Orange" > <img src="https://avatars3.githubusercontent.com/u/1506386" width="80px" ></a>
36
39
  <a href="http://www.ybrain.com/" title="YBRAIN Inc." > <img src="https://avatars2.githubusercontent.com/u/38232440" width="80px" ></a>
37
40
  <a href="http://99bitcoins.com/" title="99 bitcoins" > <img src="https://avatars0.githubusercontent.com/u/9527779" width="80px" ></a>
41
+ <a href="https://wechaty.github.io/wechaty/" title="Wechaty" > <img src="https://avatars0.githubusercontent.com/u/21285357" width="80px" ></a>
38
42
  <a href="https://opendatakit.org" title="Open Data Kit" > <img src="https://avatars0.githubusercontent.com/u/6222985" width="80px" ></a>
39
43
  <a href="https://ridibooks.com" title="RIDI Books" > <img src="https://avatars1.githubusercontent.com/u/24955411" width="80px" ></a>
40
44
  <a href="http://signalk.org" title="Signal K" > <img src="https://avatars1.githubusercontent.com/u/7126740" width="80px" ></a>
@@ -131,8 +135,8 @@ var options = {
131
135
  trimValues: true,
132
136
  cdataTagName: "__cdata", //default is 'false'
133
137
  cdataPositionChar: "\\c",
134
- localeRange: "", //To support non english character in tag/attribute values.
135
138
  parseTrueNumberOnly: false,
139
+ arrayMode: false, //"strict"
136
140
  attrValueProcessor: (val, attrName) => he.decode(val, {isAttributeValue: true}),//default is a=>a
137
141
  tagValueProcessor : (val, tagName) => he.decode(val), //default is a=>a
138
142
  stopNodes: ["parse-me-as-string"]
@@ -147,7 +151,7 @@ var tObj = parser.getTraversalObj(xmlData,options);
147
151
  var jsonObj = parser.convertToJson(tObj,options);
148
152
 
149
153
  ```
150
- You can pass `true` or validation option as 3rd parameter to validate along with parsing which is same as above example.
154
+ As you can notice in above code, validator is not embeded 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.
151
155
 
152
156
  ```js
153
157
  try{
@@ -157,6 +161,18 @@ try{
157
161
  }
158
162
  ```
159
163
 
164
+ Validator returns the following object in case of error;
165
+ ```js
166
+ {
167
+ err: {
168
+ code: code,
169
+ msg: message,
170
+ line: lineNumber,
171
+ },
172
+ };
173
+ ```
174
+
175
+
160
176
  #### Note: [he](https://www.npmjs.com/package/he) library is used in this example
161
177
 
162
178
  <details>
@@ -173,8 +189,8 @@ try{
173
189
  * **decodeHTMLchar** : This options has been removed from 3.3.4. Instead, use tagValueProcessor, and attrValueProcessor. See above example.
174
190
  * **cdataTagName** : If specified, parser parse CDATA as nested tag instead of adding it's value to parent tag.
175
191
  * **cdataPositionChar** : It'll help to covert JSON back to XML without losing CDATA position.
176
- * **localeRange**: Parser will accept non-English character in tag or attribute name. Check #87 for more detail. Eg `localeRange: "a-zA-Zа-яёА-ЯЁ"`
177
192
  * **parseTrueNumberOnly**: if true then values like "+123", or "0123" will not be parsed as number.
193
+ * **arrayMode** : When `false`, a tag with single occurence is parsed as an object but as an array in case of multiple occurences. When `true`, a tag will be parsed as an array always excluding leaf nodes. When `strict`, all the tags will be parsed as array only.
178
194
  * **tagValueProcessor** : Process tag value during transformation. Like HTML decoding, word capitalization, etc. Applicable in case of string only.
179
195
  * **attrValueProcessor** : Process attribute value during transformation. Like HTML decoding, word capitalization, etc. Applicable in case of string only.
180
196
  * **stopNodes** : an array of tag names which are not required to be parsed. Instead their values are parsed as string.
@@ -285,15 +301,15 @@ With the correct options, you can get the almost original XML without losing any
285
301
  </details>
286
302
 
287
303
  ### Limitations
288
- Currently FXP fails to parse XML with attributes has ">" in the value. This problem is left open as change in regex for its fix is degrading the performance. And the parser become very slow in case of long attrbute names.
304
+ Currently FXP fails to parse XML with attributes has ">" in the value. This problem is left open as change in regex for its fix is degrading the performance. And the parser become very slow in case of long attrbute names. Hoever, It is not ignored and we're working on the fix.
289
305
 
290
306
  ### Worth to mention
291
307
 
292
- - **[BigBit standard)](https://github.com/amitguptagwl/bigbit)** : A standard to reprent any number in the universe in comparitively less space and without precision loss. A standard to save space to represent any text string in comparision of UTF encoding.
293
- - **[imglab](https://github.com/NaturalIntelligence/imglab)** : Speedup and simplify image labeling / annotation. Supports multiple formats, one click annotation, easy interface and much more. There are more than 20k images are annotated every month.
308
+ - **[BigBit standard)](https://github.com/amitguptagwl/bigbit)** : A standard to represent any number in the universe in comparitively less space and without precision loss. A standard to save memory to represent any text string in comparision of UTF encodings.
309
+ - **[imglab](https://github.com/NaturalIntelligence/imglab)** : Speedup and simplify image labeling / annotation. Supports multiple formats, one click annotation, easy interface and much more. There are more than half million images are being annotated every month using this tool.
310
+ - [stubmatic](https://github.com/NaturalIntelligence/Stubmatic) : Create fake webservices, DynamoDB or S3 servers, Manage fake/mock stub data, Or fake any HTTP(s) call.
294
311
  - **[अनुमार्गक (anumargak)](https://github.com/NaturalIntelligence/anumargak)** : The fastest and simple router for node js web frameworks with many unique features.
295
- - [stubmatic](https://github.com/NaturalIntelligence/Stubmatic) : A stub server to mock behaviour of HTTP(s) / REST / SOAP services, incuding DynamoDB calls. You can also mock binary formats.
296
- - [मुनीम (Muneem)](https://github.com/muneem4node/muneem) : A webframework made for all team members. Faster tha fastify, express, koa, hapi and others.
312
+ - [मुनीम (Muneem)](https://github.com/muneem4node/muneem) : A webframework made for all team members. Fast and Featured.
297
313
  - [शब्दावली (shabdawali)](https://github.com/amitguptagwl/shabdawali) : Amazing human like typing effects beyond your imagination.
298
314
 
299
315
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-xml-parser",
3
- "version": "3.13.0",
3
+ "version": "3.16.0",
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": {
@@ -70,10 +70,10 @@
70
70
  ],
71
71
  "license": "MIT",
72
72
  "devDependencies": {
73
- "@babel/core": "^7.6.2",
74
- "@babel/plugin-transform-runtime": "^7.6.2",
75
- "@babel/preset-env": "^7.6.2",
76
- "@babel/register": "^7.6.2",
73
+ "@babel/core": "^7.7.5",
74
+ "@babel/plugin-transform-runtime": "^7.7.6",
75
+ "@babel/preset-env": "^7.7.6",
76
+ "@babel/register": "^7.7.4",
77
77
  "babel-loader": "^8.0.6",
78
78
  "benchmark": "^2.1.4",
79
79
  "eslint": "^5.16.0",
@@ -82,11 +82,10 @@
82
82
  "istanbul": "^0.4.5",
83
83
  "jasmine": "^3.5.0",
84
84
  "nimnjs": "^1.3.2",
85
- "prettier": "^1.18.2",
85
+ "prettier": "^1.19.1",
86
86
  "publish-please": "^5.5.1",
87
- "webpack": "^4.41.0",
88
- "webpack-cli": "^3.3.9",
89
- "xml2js": "^0.4.22"
87
+ "webpack": "^4.41.2",
88
+ "webpack-cli": "^3.3.10"
90
89
  },
91
90
  "typings": "src/parser.d.ts"
92
91
  }
package/src/node2json.js CHANGED
@@ -12,12 +12,16 @@ const convertToJson = function(node, options) {
12
12
  //otherwise create a textnode if node has some text
13
13
  if (util.isExist(node.val)) {
14
14
  if (!(typeof node.val === 'string' && (node.val === '' || node.val === options.cdataPositionChar))) {
15
- jObj[options.textNodeName] = node.val;
15
+ if(options.arrayMode === "strict"){
16
+ jObj[options.textNodeName] = [ node.val ];
17
+ }else{
18
+ jObj[options.textNodeName] = node.val;
19
+ }
16
20
  }
17
21
  }
18
22
  }
19
23
 
20
- util.merge(jObj, node.attrsMap);
24
+ util.merge(jObj, node.attrsMap, options.arrayMode);
21
25
 
22
26
  const keys = Object.keys(node.child);
23
27
  for (let index = 0; index < keys.length; index++) {
@@ -28,7 +32,17 @@ const convertToJson = function(node, options) {
28
32
  jObj[tagname].push(convertToJson(node.child[tagname][tag], options));
29
33
  }
30
34
  } else {
31
- jObj[tagname] = convertToJson(node.child[tagname][0], options);
35
+ if(options.arrayMode === true){
36
+ const result = convertToJson(node.child[tagname][0], options)
37
+ if(typeof result === 'object')
38
+ jObj[tagname] = [ result ];
39
+ else
40
+ jObj[tagname] = result;
41
+ }else if(options.arrayMode === "strict"){
42
+ jObj[tagname] = [convertToJson(node.child[tagname][0], options) ];
43
+ }else{
44
+ jObj[tagname] = convertToJson(node.child[tagname][0], options);
45
+ }
32
46
  }
33
47
  }
34
48
 
package/src/parser.d.ts CHANGED
@@ -7,19 +7,18 @@ type X2jOptions = {
7
7
  allowBooleanAttributes: boolean;
8
8
  parseNodeValue: boolean;
9
9
  parseAttributeValue: boolean;
10
- arrayMode: boolean;
10
+ arrayMode: boolean | 'strict';
11
11
  trimValues: boolean;
12
12
  cdataTagName: false | string;
13
13
  cdataPositionChar: string;
14
- localeRange: string;
15
14
  parseTrueNumberOnly: boolean;
16
15
  tagValueProcessor: (tagValue: string, tagName: string) => string;
17
16
  attrValueProcessor: (attrValue: string, attrName: string) => string;
17
+ stopNodes: string[];
18
18
  };
19
19
  type X2jOptionsOptional = Partial<X2jOptions>;
20
20
  type validationOptions = {
21
21
  allowBooleanAttributes: boolean;
22
- localeRange: string;
23
22
  };
24
23
  type validationOptionsOptional = Partial<validationOptions>;
25
24
  type J2xOptions = {
@@ -40,7 +39,7 @@ type J2xOptionsOptional = Partial<J2xOptions>;
40
39
  type ESchema = string | object | Array<string|object>;
41
40
 
42
41
  type ValidationError = {
43
- err: { code: string; msg: string };
42
+ err: { code: string; msg: string, line: number };
44
43
  };
45
44
 
46
45
  export function parse(xmlData: string, options?: X2jOptionsOptional, validationOptions?: validationOptionsOptional | boolean): any;
package/src/util.js CHANGED
@@ -1,5 +1,10 @@
1
1
  'use strict';
2
2
 
3
+ const nameStartChar = ':A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD';
4
+ const nameChar = nameStartChar + '\\-.\\d\\u00B7\\u0300-\\u036F\\u203F-\\u2040';
5
+ const nameRegexp = '[' + nameStartChar + '][' + nameChar + ']*'
6
+ const regexName = new RegExp('^' + nameRegexp + '$');
7
+
3
8
  const getAllMatches = function(string, regex) {
4
9
  const matches = [];
5
10
  let match = regex.exec(string);
@@ -15,15 +20,11 @@ const getAllMatches = function(string, regex) {
15
20
  return matches;
16
21
  };
17
22
 
18
- const doesMatch = function(string, regex) {
19
- const match = regex.exec(string);
23
+ const isName = function(string) {
24
+ const match = regexName.exec(string);
20
25
  return !(match === null || typeof match === 'undefined');
21
26
  };
22
27
 
23
- const doesNotMatch = function(string, regex) {
24
- return !doesMatch(string, regex);
25
- };
26
-
27
28
  exports.isExist = function(v) {
28
29
  return typeof v !== 'undefined';
29
30
  };
@@ -37,12 +38,16 @@ exports.isEmptyObject = function(obj) {
37
38
  * @param {*} target
38
39
  * @param {*} a
39
40
  */
40
- exports.merge = function(target, a) {
41
+ exports.merge = function(target, a, arrayMode) {
41
42
  if (a) {
42
43
  const keys = Object.keys(a); // will return an array of own properties
43
44
  const len = keys.length; //don't make it inline
44
45
  for (let i = 0; i < len; i++) {
45
- target[keys[i]] = a[keys[i]];
46
+ if(arrayMode === 'strict'){
47
+ target[keys[i]] = [ a[keys[i]] ];
48
+ }else{
49
+ target[keys[i]] = a[keys[i]];
50
+ }
46
51
  }
47
52
  }
48
53
  };
@@ -77,6 +82,6 @@ exports.buildOptions = function(options, defaultOptions, props) {
77
82
  return newOptions;
78
83
  };
79
84
 
80
- exports.doesMatch = doesMatch;
81
- exports.doesNotMatch = doesNotMatch;
85
+ exports.isName = isName;
82
86
  exports.getAllMatches = getAllMatches;
87
+ exports.nameRegexp = nameRegexp;
package/src/validator.js CHANGED
@@ -4,27 +4,28 @@ const util = require('./util');
4
4
 
5
5
  const defaultOptions = {
6
6
  allowBooleanAttributes: false, //A tag can have attributes without any value
7
- localeRange: 'a-zA-Z',
8
7
  };
9
8
 
10
- const props = ['allowBooleanAttributes', 'localeRange'];
9
+ const props = ['allowBooleanAttributes'];
11
10
 
12
11
  //const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g");
13
- exports.validate = function(xmlData, options) {
12
+ exports.validate = function (xmlData, options) {
14
13
  options = util.buildOptions(options, defaultOptions, props);
15
14
 
16
15
  //xmlData = xmlData.replace(/(\r\n|\n|\r)/gm,"");//make it single line
17
16
  //xmlData = xmlData.replace(/(^\s*<\?xml.*?\?>)/g,"");//Remove XML starting tag
18
17
  //xmlData = xmlData.replace(/(<!DOCTYPE[\s\w\"\.\/\-\:]+(\[.*\])*\s*>)/g,"");//Remove DOCTYPE
19
-
20
18
  const tags = [];
21
19
  let tagFound = false;
20
+
21
+ //indicates that the root tag has been closed (aka. depth 0 has been reached)
22
+ let reachedRoot = false;
23
+
22
24
  if (xmlData[0] === '\ufeff') {
23
25
  // check for byte order mark (BOM)
24
26
  xmlData = xmlData.substr(1);
25
27
  }
26
- const regxAttrName = new RegExp('^[_w][\\w\\-.:]*$'.replace('_w', '_' + options.localeRange));
27
- const regxTagName = new RegExp('^([w]|_)[\\w.\\-_:]*'.replace('([w', '([' + options.localeRange));
28
+
28
29
  for (let i = 0; i < xmlData.length; i++) {
29
30
  if (xmlData[i] === '<') {
30
31
  //starting of tag
@@ -66,15 +67,22 @@ exports.validate = function(xmlData, options) {
66
67
  if (tagName[tagName.length - 1] === '/') {
67
68
  //self closing tag without attributes
68
69
  tagName = tagName.substring(0, tagName.length - 1);
69
- continue;
70
+ //continue;
71
+ i--;
70
72
  }
71
- if (!validateTagName(tagName, regxTagName)) {
72
- return {err: {code: 'InvalidTag', msg: 'Tag ' + tagName + ' is an invalid name.'}};
73
+ if (!validateTagName(tagName)) {
74
+ let msg;
75
+ if(tagName.trim().length === 0) {
76
+ msg = "There is an unnecessary space between tag name and backward slash '</ ..'.";
77
+ }else{
78
+ msg = `Tag '${tagName}' is an invalid name.`;
79
+ }
80
+ return getErrorObject('InvalidTag', msg, getLineNumberForPosition(xmlData, i));
73
81
  }
74
82
 
75
83
  const result = readAttributeStr(xmlData, i);
76
84
  if (result === false) {
77
- return {err: {code: 'InvalidAttr', msg: 'Attributes for "' + tagName + '" have open quote.'}};
85
+ return getErrorObject('InvalidAttr', `Attributes for '${tagName}' have open quote.`, getLineNumberForPosition(xmlData, i));
78
86
  }
79
87
  let attrStr = result.value;
80
88
  i = result.index;
@@ -82,36 +90,48 @@ exports.validate = function(xmlData, options) {
82
90
  if (attrStr[attrStr.length - 1] === '/') {
83
91
  //self closing tag
84
92
  attrStr = attrStr.substring(0, attrStr.length - 1);
85
- const isValid = validateAttributeString(attrStr, options, regxAttrName);
93
+ const isValid = validateAttributeString(attrStr, options);
86
94
  if (isValid === true) {
87
95
  tagFound = true;
88
96
  //continue; //text may presents after self closing tag
89
97
  } else {
90
- return isValid;
98
+ //the result from the nested function returns the position of the error within the attribute
99
+ //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
100
+ //this gives us the absolute index in the entire xml, which we can use to find the line at last
101
+ return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line));
91
102
  }
92
103
  } else if (closingTag) {
93
- if(!result.tagClosed){
94
- return {
95
- err: {code: 'InvalidTag', msg: 'closing tag "' + tagName + "\" don't have proper closing."},
96
- };
97
- }else if (attrStr.trim().length > 0) {
98
- return {
99
- err: {code: 'InvalidTag', msg: 'closing tag "' + tagName + "\" can't have attributes or invalid starting."},
100
- };
104
+ if (!result.tagClosed) {
105
+ return getErrorObject('InvalidTag', `Closing tag '${tagName}' doesn't have proper closing.`, getLineNumberForPosition(xmlData, i));
106
+ } else if (attrStr.trim().length > 0) {
107
+ return getErrorObject('InvalidTag', `Closing tag '${tagName}' can't have attributes or invalid starting.`, getLineNumberForPosition(xmlData, i));
101
108
  } else {
102
109
  const otg = tags.pop();
103
110
  if (tagName !== otg) {
104
- return {
105
- err: {code: 'InvalidTag', msg: 'closing tag ' + otg + ' is expected inplace of ' + tagName + '.'},
106
- };
111
+ return getErrorObject('InvalidTag', `Closing tag '${otg}' is expected inplace of '${tagName}'.`, getLineNumberForPosition(xmlData, i));
112
+ }
113
+
114
+ //when there are no more tags, we reached the root level.
115
+ if(tags.length == 0)
116
+ {
117
+ reachedRoot = true;
107
118
  }
108
119
  }
109
120
  } else {
110
- const isValid = validateAttributeString(attrStr, options, regxAttrName);
121
+ const isValid = validateAttributeString(attrStr, options);
111
122
  if (isValid !== true) {
112
- return isValid;
123
+ //the result from the nested function returns the position of the error within the attribute
124
+ //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
125
+ //this gives us the absolute index in the entire xml, which we can use to find the line at last
126
+ return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line));
127
+ }
128
+
129
+ //if the root level has been reached before ...
130
+ if(reachedRoot === true) {
131
+ return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
132
+ } else {
133
+ tags.push(tagName);
113
134
  }
114
- tags.push(tagName);
115
135
  tagFound = true;
116
136
  }
117
137
 
@@ -127,6 +147,11 @@ exports.validate = function(xmlData, options) {
127
147
  } else {
128
148
  break;
129
149
  }
150
+ } else if (xmlData[i] === '&') {
151
+ const afterAmp = validateAmpersand(xmlData, i);
152
+ if (afterAmp == -1)
153
+ return getErrorObject('InvalidChar', `char '&' is not expected.`, getLineNumberForPosition(xmlData, i));
154
+ i = afterAmp;
130
155
  }
131
156
  } //end of reading tag text value
132
157
  if (xmlData[i] === '<') {
@@ -137,16 +162,14 @@ exports.validate = function(xmlData, options) {
137
162
  if (xmlData[i] === ' ' || xmlData[i] === '\t' || xmlData[i] === '\n' || xmlData[i] === '\r') {
138
163
  continue;
139
164
  }
140
- return {err: {code: 'InvalidChar', msg: 'char ' + xmlData[i] + ' is not expected .'}};
165
+ return getErrorObject('InvalidChar', `char '${xmlData[i]}' is not expected.`, getLineNumberForPosition(xmlData, i));
141
166
  }
142
167
  }
143
168
 
144
169
  if (!tagFound) {
145
- return {err: {code: 'InvalidXml', msg: 'Start tag expected.'}};
170
+ return getErrorObject('InvalidXml', 'Start tag expected.', 1);
146
171
  } else if (tags.length > 0) {
147
- return {
148
- err: {code: 'InvalidXml', msg: 'Invalid ' + JSON.stringify(tags, null, 4).replace(/\r?\n/g, '') + ' found.'},
149
- };
172
+ return getErrorObject('InvalidXml', `Invalid '${JSON.stringify(tags, null, 4).replace(/\r?\n/g, '')}' found.`, 1);
150
173
  }
151
174
 
152
175
  return true;
@@ -164,7 +187,7 @@ function readPI(xmlData, i) {
164
187
  //tagname
165
188
  var tagname = xmlData.substr(start, i - start);
166
189
  if (i > 5 && tagname === 'xml') {
167
- return {err: {code: 'InvalidXml', msg: 'XML declaration allowed only at the start of the document.'}};
190
+ return getErrorObject('InvalidXml', 'XML declaration allowed only at the start of the document.', getLineNumberForPosition(xmlData, i));
168
191
  } else if (xmlData[i] == '?' && xmlData[i + 1] == '>') {
169
192
  //check if valid attribut string
170
193
  i++;
@@ -262,7 +285,7 @@ function readAttributeStr(xmlData, i) {
262
285
  return false;
263
286
  }
264
287
 
265
- return {value: attrStr, index: i, tagClosed: tagClosed};
288
+ return { value: attrStr, index: i, tagClosed: tagClosed };
266
289
  }
267
290
 
268
291
  /**
@@ -272,7 +295,7 @@ const validAttrStrRegxp = new RegExp('(\\s*)([^\\s=]+)(\\s*=)?(\\s*([\'"])(([\\s
272
295
 
273
296
  //attr, ="sd", a="amit's", a="sd"b="saf", ab cd=""
274
297
 
275
- function validateAttributeString(attrStr, options, regxAttrName) {
298
+ function validateAttributeString(attrStr, options) {
276
299
  //console.log("start:"+attrStr+":end");
277
300
 
278
301
  //if(attrStr.trim().length === 0) return true; //empty string
@@ -281,45 +304,97 @@ function validateAttributeString(attrStr, options, regxAttrName) {
281
304
  const attrNames = {};
282
305
 
283
306
  for (let i = 0; i < matches.length; i++) {
284
- //console.log(matches[i]);
285
-
286
307
  if (matches[i][1].length === 0) {
287
308
  //nospace before attribute name: a="sd"b="saf"
288
- return {err: {code: 'InvalidAttr', msg: 'attribute ' + matches[i][2] + ' has no space in starting.'}};
309
+ return getErrorObject('InvalidAttr', `Attribute '${matches[i][2]}' has no space in starting.`, getPositionFromMatch(attrStr, matches[i][0]))
289
310
  } else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
290
311
  //independent attribute: ab
291
- return {err: {code: 'InvalidAttr', msg: 'boolean attribute ' + matches[i][2] + ' is not allowed.'}};
312
+ return getErrorObject('InvalidAttr', `boolean attribute '${matches[i][2]}' is not allowed.`, getPositionFromMatch(attrStr, matches[i][0]));
292
313
  }
293
314
  /* else if(matches[i][6] === undefined){//attribute without value: ab=
294
315
  return { err: { code:"InvalidAttr",msg:"attribute " + matches[i][2] + " has no value assigned."}};
295
316
  } */
296
317
  const attrName = matches[i][2];
297
- if (!validateAttrName(attrName, regxAttrName)) {
298
- return {err: {code: 'InvalidAttr', msg: 'attribute ' + attrName + ' is an invalid name.'}};
318
+ if (!validateAttrName(attrName)) {
319
+ return getErrorObject('InvalidAttr', `Attribute '${attrName}' is an invalid name.`, getPositionFromMatch(attrStr, matches[i][0]));
299
320
  }
300
321
  if (!attrNames.hasOwnProperty(attrName)) {
301
322
  //check for duplicate attribute.
302
323
  attrNames[attrName] = 1;
303
324
  } else {
304
- return {err: {code: 'InvalidAttr', msg: 'attribute ' + attrName + ' is repeated.'}};
325
+ return getErrorObject('InvalidAttr', `Attribute '${attrName}' is repeated.`, getPositionFromMatch(attrStr, matches[i][0]));
305
326
  }
306
327
  }
307
328
 
308
329
  return true;
309
330
  }
310
331
 
311
- // const validAttrRegxp = /^[_a-zA-Z][\w\-.:]*$/;
332
+ function validateNumberAmpersand(xmlData, i) {
333
+ let re = /\d/;
334
+ if (xmlData[i] === 'x') {
335
+ i++;
336
+ re = /[\da-fA-F]/;
337
+ }
338
+ for (; i < xmlData.length; i++) {
339
+ if (xmlData[i] === ';')
340
+ return i;
341
+ if (!xmlData[i].match(re))
342
+ break;
343
+ }
344
+ return -1;
345
+ }
346
+
347
+ function validateAmpersand(xmlData, i) {
348
+ // https://www.w3.org/TR/xml/#dt-charref
349
+ i++;
350
+ if (xmlData[i] === ';')
351
+ return -1;
352
+ if (xmlData[i] === '#') {
353
+ i++;
354
+ return validateNumberAmpersand(xmlData, i);
355
+ }
356
+ let count = 0;
357
+ for (; i < xmlData.length; i++, count++) {
358
+ if (xmlData[i].match(/\w/) && count < 20)
359
+ continue;
360
+ if (xmlData[i] === ';')
361
+ break;
362
+ return -1;
363
+ }
364
+ return i;
365
+ }
366
+
367
+ function getErrorObject(code, message, lineNumber) {
368
+ return {
369
+ err: {
370
+ code: code,
371
+ msg: message,
372
+ line: lineNumber,
373
+ },
374
+ };
375
+ }
312
376
 
313
- function validateAttrName(attrName, regxAttrName) {
314
- // const validAttrRegxp = new RegExp(regxAttrName);
315
- return util.doesMatch(attrName, regxAttrName);
377
+ function validateAttrName(attrName) {
378
+ return util.isName(attrName);
316
379
  }
317
380
 
318
381
  //const startsWithXML = new RegExp("^[Xx][Mm][Ll]");
319
382
  // startsWith = /^([a-zA-Z]|_)[\w.\-_:]*/;
320
383
 
321
- function validateTagName(tagname, regxTagName) {
384
+ function validateTagName(tagname) {
322
385
  /*if(util.doesMatch(tagname,startsWithXML)) return false;
323
386
  else*/
324
- return !util.doesNotMatch(tagname, regxTagName);
387
+ //return !tagname.toLowerCase().startsWith("xml") || !util.doesNotMatch(tagname, regxTagName);
388
+ return util.isName(tagname);
325
389
  }
390
+
391
+ //this function returns the line number for the character at the given index
392
+ function getLineNumberForPosition(xmlData, index) {
393
+ var lines = xmlData.substring(0, index).split(/\r?\n/);
394
+ return lines.length;
395
+ }
396
+
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;
400
+ }
@@ -4,8 +4,9 @@ const util = require('./util');
4
4
  const buildOptions = require('./util').buildOptions;
5
5
  const xmlNode = require('./xmlNode');
6
6
  const TagType = {OPENING: 1, CLOSING: 2, SELF: 3, CDATA: 4};
7
- let regx =
8
- '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|(([\\w:\\-._]*:)?([\\w:\\-._]+))([^>]*)>|((\\/)(([\\w:\\-._]*:)?([\\w:\\-._]+))\\s*>))([^<]*)';
7
+ const regx =
8
+ '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
9
+ .replace(/NAME/g, util.nameRegexp);
9
10
 
10
11
  //const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g");
11
12
  //const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g");
@@ -32,7 +33,6 @@ const defaultOptions = {
32
33
  trimValues: true, //Trim string values of tag and attributes
33
34
  cdataTagName: false,
34
35
  cdataPositionChar: '\\c',
35
- localeRange: '',
36
36
  tagValueProcessor: function(a, tagName) {
37
37
  return a;
38
38
  },
@@ -58,7 +58,6 @@ const props = [
58
58
  'trimValues',
59
59
  'cdataTagName',
60
60
  'cdataPositionChar',
61
- 'localeRange',
62
61
  'tagValueProcessor',
63
62
  'attrValueProcessor',
64
63
  'parseTrueNumberOnly',
@@ -74,7 +73,6 @@ const getTraversalObj = function(xmlData, options) {
74
73
  const xmlObj = new xmlNode('!xml');
75
74
  let currentNode = xmlObj;
76
75
 
77
- regx = regx.replace(/\[\\w/g, '[' + options.localeRange + '\\w');
78
76
  const tagsRegx = new RegExp(regx, 'g');
79
77
  let tag = tagsRegx.exec(xmlData);
80
78
  let nextTag = tagsRegx.exec(xmlData);
@@ -83,7 +81,7 @@ const getTraversalObj = function(xmlData, options) {
83
81
 
84
82
  if (tagType === TagType.CLOSING) {
85
83
  //add parsed data to parent node
86
- if (currentNode.parent && tag[14]) {
84
+ if (currentNode.parent && tag[12]) {
87
85
  currentNode.parent.val = util.getValue(currentNode.parent.val) + '' + processTagValue(tag, options, currentNode.parent.tagname);
88
86
  }
89
87
  if (options.stopNodes.length && options.stopNodes.includes(currentNode.tagname)) {
@@ -101,14 +99,14 @@ const getTraversalObj = function(xmlData, options) {
101
99
  //for backtracking
102
100
  currentNode.val = util.getValue(currentNode.val) + options.cdataPositionChar;
103
101
  //add rest value to parent node
104
- if (tag[14]) {
102
+ if (tag[12]) {
105
103
  currentNode.val += processTagValue(tag, options);
106
104
  }
107
105
  } else {
108
106
  currentNode.val = (currentNode.val || '') + (tag[3] || '') + processTagValue(tag, options);
109
107
  }
110
108
  } else if (tagType === TagType.SELF) {
111
- if (currentNode && tag[14]) {
109
+ if (currentNode && tag[12]) {
112
110
  currentNode.val = util.getValue(currentNode.val) + '' + processTagValue(tag, options);
113
111
  }
114
112
 
@@ -142,7 +140,7 @@ const getTraversalObj = function(xmlData, options) {
142
140
 
143
141
  function processTagValue(parsedTags, options, parentTagName) {
144
142
  const tagName = parsedTags[7] || parentTagName;
145
- let val = parsedTags[14];
143
+ let val = parsedTags[12];
146
144
  if (val) {
147
145
  if (options.trimValues) {
148
146
  val = val.trim();
@@ -191,6 +189,7 @@ function parseValue(val, shouldParse, parseTrueNumberOnly) {
191
189
  parsed = Number.parseInt(val, 16);
192
190
  } else if (val.indexOf('.') !== -1) {
193
191
  parsed = Number.parseFloat(val);
192
+ val = val.replace(/0+$/,"");
194
193
  } else {
195
194
  parsed = Number.parseInt(val, 10);
196
195
  }