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