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 +36 -21
- package/package.json +8 -7
- package/src/json2xml.js +21 -9
- package/src/nimndata.js +1 -1
- package/src/node2json.js +1 -1
- package/src/node2json_str.js +2 -2
- package/src/parser.d.ts +8 -1
- package/src/parser.js +11 -2
- package/src/util.js +2 -1
- package/src/validator.js +37 -24
- package/src/xmlstr2xmlnode.js +19 -23
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
|
-
|
|
108
|
+
### Installation
|
|
109
|
+
|
|
110
|
+
To use it as an **NPM package**:
|
|
111
|
+
|
|
112
|
+
`npm install fast-xml-parser`
|
|
108
113
|
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
+
const jsonObj = parser.parse(xmlData [,options] );
|
|
122
129
|
```
|
|
123
130
|
|
|
124
131
|
```js
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
const parser = require('fast-xml-parser');
|
|
133
|
+
const he = require('he');
|
|
127
134
|
|
|
128
|
-
|
|
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
|
-
|
|
161
|
+
let jsonObj = parser.parse(xmlData,options);
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
// Intermediate obj
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
+
const result = parser.validate(xmlData);
|
|
223
236
|
if (result !== true) console.log(result.err);
|
|
224
|
-
|
|
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
|
-
|
|
244
|
+
const Parser = require("fast-xml-parser").j2xParser;
|
|
232
245
|
//default options need not to set
|
|
233
|
-
|
|
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})
|
|
257
|
+
attrValueProcessor: a=> he.encode(a, {isAttributeValue: isAttribute, useNamedReferences: true}),// default is a=>a
|
|
258
|
+
rootNodeName: "element"
|
|
245
259
|
};
|
|
246
|
-
|
|
247
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
//
|
|
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
|
|
package/src/node2json_str.js
CHANGED
|
@@ -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
|
-
|
|
22
|
+
const tagname = keys[index];
|
|
23
23
|
if (node.child[tagname] && node.child[tagname].length > 1) {
|
|
24
24
|
jObj += '"' + tagname + '" : [ ';
|
|
25
|
-
for (
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 = "
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
}
|
|
172
|
-
|
|
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
|
-
|
|
192
|
+
const start = i;
|
|
185
193
|
for (; i < xmlData.length; i++) {
|
|
186
194
|
if (xmlData[i] == '?' || xmlData[i] == ' ') {
|
|
187
195
|
//tagname
|
|
188
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
394
|
-
return
|
|
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
|
|
398
|
-
function getPositionFromMatch(
|
|
399
|
-
return
|
|
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
|
}
|
package/src/xmlstr2xmlnode.js
CHANGED
|
@@ -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
|
-
'
|
|
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.
|
|
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,
|
|
108
|
+
function parseValue(val, shouldParse, options) {
|
|
100
109
|
if (shouldParse && typeof val === 'string') {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
147
|
+
options.numParseOptions
|
|
152
148
|
);
|
|
153
149
|
} else if (options.allowBooleanAttributes) {
|
|
154
150
|
attrs[options.attributeNamePrefix + attrName] = true;
|