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 +25 -9
- package/package.json +8 -9
- package/src/node2json.js +17 -3
- package/src/parser.d.ts +3 -4
- package/src/util.js +15 -10
- package/src/validator.js +122 -47
- package/src/xmlstr2xmlnode.js +8 -9
- package/yarn.lock +564 -860
- package/CHANGELOG.md +0 -315
- package/codecept.json +0 -17
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://
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
- [
|
|
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.
|
|
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.
|
|
74
|
-
"@babel/plugin-transform-runtime": "^7.6
|
|
75
|
-
"@babel/preset-env": "^7.6
|
|
76
|
-
"@babel/register": "^7.
|
|
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.
|
|
85
|
+
"prettier": "^1.19.1",
|
|
86
86
|
"publish-please": "^5.5.1",
|
|
87
|
-
"webpack": "^4.41.
|
|
88
|
-
"webpack-cli": "^3.3.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
19
|
-
const match =
|
|
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
|
-
|
|
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.
|
|
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'
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
121
|
+
const isValid = validateAttributeString(attrStr, options);
|
|
111
122
|
if (isValid !== true) {
|
|
112
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
298
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
314
|
-
|
|
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
|
|
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
|
+
}
|
package/src/xmlstr2xmlnode.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
'<((!\\[CDATA\\[([\\s\\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[
|
|
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[
|
|
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[
|
|
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[
|
|
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
|
}
|