fast-xml-parser 3.20.3 → 4.0.0-beta.2
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 +421 -0
- package/README.md +79 -267
- package/package.json +9 -38
- package/{cli.js → src/cli/cli.js} +16 -24
- package/src/cli/man.js +12 -0
- package/src/{read.js → cli/read.js} +0 -0
- package/src/fxp.d.ts +74 -0
- package/src/fxp.js +11 -0
- package/src/util.js +4 -22
- package/src/validator.js +53 -27
- package/src/xmlbuilder/json2xml.js +234 -0
- package/src/xmlbuilder/orderedJs2Xml.js +71 -0
- package/src/xmlbuilder/prettifyJs2Xml.js +0 -0
- package/src/xmlparser/OptionsBuilder.js +62 -0
- package/src/xmlparser/OrderedObjParser.js +407 -0
- package/src/xmlparser/XMLParser.js +36 -0
- package/src/xmlparser/node2json.js +101 -0
- package/src/xmlparser/xmlNode.js +23 -0
- package/src/json2xml.js +0 -268
- package/src/nimndata.js +0 -144
- package/src/node2json.js +0 -42
- package/src/node2json_str.js +0 -63
- package/src/parser.d.ts +0 -78
- package/src/parser.js +0 -76
- package/src/xmlNode.js +0 -17
- package/src/xmlstr2xmlnode.js +0 -337
package/src/fxp.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
type X2jOptions = {
|
|
2
|
+
preserveOrder: boolean;
|
|
3
|
+
attributeNamePrefix: string;
|
|
4
|
+
attributesGroupName: false | string;
|
|
5
|
+
textNodeName: string;
|
|
6
|
+
ignoreAttributes: boolean;
|
|
7
|
+
removeNSPrefix: boolean;
|
|
8
|
+
allowBooleanAttributes: boolean;
|
|
9
|
+
parseTagValue: boolean;
|
|
10
|
+
parseAttributeValue: boolean;
|
|
11
|
+
trimValues: boolean;
|
|
12
|
+
cdataPropName: false | string;
|
|
13
|
+
commentPropName: false | string;
|
|
14
|
+
tagValueProcessor: (tagName: string, tagValue: string, jPath: string, hasAttributes: boolean, isLeafNode: boolean) => string;
|
|
15
|
+
attributeValueProcessor: (attrName: string, attrValue: string, jPath: string) => string;
|
|
16
|
+
numberParseOptions: strnumOptions;
|
|
17
|
+
stopNodes: string[];
|
|
18
|
+
unpairedTags: string[];
|
|
19
|
+
alwaysCreateTextNode: boolean;
|
|
20
|
+
isArray: (tagName: string, jPath: string, isLeafNode: boolean, isAttribute: boolean) => boolean;
|
|
21
|
+
};
|
|
22
|
+
type strnumOptions = {
|
|
23
|
+
hex: boolean;
|
|
24
|
+
leadingZeros: boolean,
|
|
25
|
+
skipLike?: RegExp
|
|
26
|
+
}
|
|
27
|
+
type X2jOptionsOptional = Partial<X2jOptions>;
|
|
28
|
+
type validationOptions = {
|
|
29
|
+
allowBooleanAttributes: boolean;
|
|
30
|
+
unpairedTags: string[];
|
|
31
|
+
};
|
|
32
|
+
type validationOptionsOptional = Partial<validationOptions>;
|
|
33
|
+
|
|
34
|
+
type XmlBuilderOptions = {
|
|
35
|
+
attributeNamePrefix: string;
|
|
36
|
+
attributesGroupName: false | string;
|
|
37
|
+
textNodeName: string;
|
|
38
|
+
ignoreAttributes: boolean;
|
|
39
|
+
cdataPropName: false | string;
|
|
40
|
+
commentPropName: false | string;
|
|
41
|
+
format: boolean;
|
|
42
|
+
indentBy: string;
|
|
43
|
+
arrayNodeName: string;
|
|
44
|
+
suppressEmptyNode: boolean;
|
|
45
|
+
preserveOrder: boolean;
|
|
46
|
+
unpairedTags: string[];
|
|
47
|
+
tagValueProcessor: (name: string, value: string) => string;
|
|
48
|
+
attributeValueProcessor: (name: string, value: string) => string;
|
|
49
|
+
};
|
|
50
|
+
type XmlBuilderOptionsOptional = Partial<XmlBuilderOptions>;
|
|
51
|
+
|
|
52
|
+
type ESchema = string | object | Array<string|object>;
|
|
53
|
+
|
|
54
|
+
type ValidationError = {
|
|
55
|
+
err: {
|
|
56
|
+
code: string;
|
|
57
|
+
msg: string,
|
|
58
|
+
line: number,
|
|
59
|
+
col: number
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export class XMLParser {
|
|
64
|
+
constructor(options?: X2jOptionsOptional);
|
|
65
|
+
parse(xmlData: string | Buffer ,validationOptions?: validationOptionsOptional | boolean): any;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class XMLValidator{
|
|
69
|
+
static validate( xmlData: string, options?: validationOptionsOptional): true | ValidationError;
|
|
70
|
+
}
|
|
71
|
+
export class XMLBuilder {
|
|
72
|
+
constructor(options: XmlBuilderOptionsOptional);
|
|
73
|
+
parse(options: any): any;
|
|
74
|
+
}
|
package/src/fxp.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const validator = require('./validator');
|
|
4
|
+
const XMLParser = require('./xmlparser/XMLParser');
|
|
5
|
+
const XMLBuilder = require('./xmlbuilder/json2xml');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
XMLParser: XMLParser,
|
|
9
|
+
XMLValidator: validator,
|
|
10
|
+
XMLBuilder: XMLBuilder
|
|
11
|
+
}
|
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]);
|
|
@@ -66,8 +67,8 @@ exports.getValue = function(v) {
|
|
|
66
67
|
// const fakeCall = function(a) {return a;};
|
|
67
68
|
// const fakeCallNoReturn = function() {};
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
const buildOptions = function(options, defaultOptions, props) {
|
|
71
|
+
let newOptions = {};
|
|
71
72
|
if (!options) {
|
|
72
73
|
return defaultOptions; //if there are not options
|
|
73
74
|
}
|
|
@@ -82,26 +83,7 @@ exports.buildOptions = function(options, defaultOptions, props) {
|
|
|
82
83
|
return newOptions;
|
|
83
84
|
};
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
* Check if a tag name should be treated as array
|
|
87
|
-
*
|
|
88
|
-
* @param tagName the node tagname
|
|
89
|
-
* @param arrayMode the array mode option
|
|
90
|
-
* @param parentTagName the parent tag name
|
|
91
|
-
* @returns {boolean} true if node should be parsed as array
|
|
92
|
-
*/
|
|
93
|
-
exports.isTagNameInArrayMode = function (tagName, arrayMode, parentTagName) {
|
|
94
|
-
if (arrayMode === false) {
|
|
95
|
-
return false;
|
|
96
|
-
} else if (arrayMode instanceof RegExp) {
|
|
97
|
-
return arrayMode.test(tagName);
|
|
98
|
-
} else if (typeof arrayMode === 'function') {
|
|
99
|
-
return !!arrayMode(tagName, parentTagName);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return arrayMode === "strict";
|
|
103
|
-
}
|
|
104
|
-
|
|
86
|
+
exports.buildOptions = buildOptions;
|
|
105
87
|
exports.isName = isName;
|
|
106
88
|
exports.getAllMatches = getAllMatches;
|
|
107
89
|
exports.nameRegexp = nameRegexp;
|
package/src/validator.js
CHANGED
|
@@ -4,9 +4,13 @@ const util = require('./util');
|
|
|
4
4
|
|
|
5
5
|
const defaultOptions = {
|
|
6
6
|
allowBooleanAttributes: false, //A tag can have attributes without any value
|
|
7
|
+
unpairedTags: []
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
const props = [
|
|
10
|
+
const props = [
|
|
11
|
+
'allowBooleanAttributes',
|
|
12
|
+
'unpairedTags'
|
|
13
|
+
];
|
|
10
14
|
|
|
11
15
|
//const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g");
|
|
12
16
|
exports.validate = function (xmlData, options) {
|
|
@@ -25,7 +29,7 @@ exports.validate = function (xmlData, options) {
|
|
|
25
29
|
// check for byte order mark (BOM)
|
|
26
30
|
xmlData = xmlData.substr(1);
|
|
27
31
|
}
|
|
28
|
-
|
|
32
|
+
|
|
29
33
|
for (let i = 0; i < xmlData.length; i++) {
|
|
30
34
|
|
|
31
35
|
if (xmlData[i] === '<' && xmlData[i+1] === '?') {
|
|
@@ -35,7 +39,7 @@ exports.validate = function (xmlData, options) {
|
|
|
35
39
|
}else if (xmlData[i] === '<') {
|
|
36
40
|
//starting of tag
|
|
37
41
|
//read until you reach to '>' avoiding any '>' in attribute value
|
|
38
|
-
|
|
42
|
+
let tagStartPos = i;
|
|
39
43
|
i++;
|
|
40
44
|
|
|
41
45
|
if (xmlData[i] === '!') {
|
|
@@ -71,7 +75,7 @@ exports.validate = function (xmlData, options) {
|
|
|
71
75
|
if (!validateTagName(tagName)) {
|
|
72
76
|
let msg;
|
|
73
77
|
if (tagName.trim().length === 0) {
|
|
74
|
-
msg = "
|
|
78
|
+
msg = "Invalid space after '<'.";
|
|
75
79
|
} else {
|
|
76
80
|
msg = "Tag '"+tagName+"' is an invalid name.";
|
|
77
81
|
}
|
|
@@ -87,6 +91,7 @@ exports.validate = function (xmlData, options) {
|
|
|
87
91
|
|
|
88
92
|
if (attrStr[attrStr.length - 1] === '/') {
|
|
89
93
|
//self closing tag
|
|
94
|
+
const attrStrStart = i - attrStr.length;
|
|
90
95
|
attrStr = attrStr.substring(0, attrStr.length - 1);
|
|
91
96
|
const isValid = validateAttributeString(attrStr, options);
|
|
92
97
|
if (isValid === true) {
|
|
@@ -96,17 +101,20 @@ exports.validate = function (xmlData, options) {
|
|
|
96
101
|
//the result from the nested function returns the position of the error within the attribute
|
|
97
102
|
//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
103
|
//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,
|
|
104
|
+
return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, attrStrStart + isValid.err.line));
|
|
100
105
|
}
|
|
101
106
|
} else if (closingTag) {
|
|
102
107
|
if (!result.tagClosed) {
|
|
103
108
|
return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
|
|
104
109
|
} else if (attrStr.trim().length > 0) {
|
|
105
|
-
return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData,
|
|
110
|
+
return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
|
|
106
111
|
} else {
|
|
107
112
|
const otg = tags.pop();
|
|
108
|
-
if (tagName !== otg) {
|
|
109
|
-
|
|
113
|
+
if (tagName !== otg.tagName) {
|
|
114
|
+
let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos);
|
|
115
|
+
return getErrorObject('InvalidTag',
|
|
116
|
+
"Expected closing tag '"+otg.tagName+"' (opened in line "+openPos.line+", col "+openPos.col+") instead of closing tag '"+tagName+"'.",
|
|
117
|
+
getLineNumberForPosition(xmlData, tagStartPos));
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
//when there are no more tags, we reached the root level.
|
|
@@ -126,8 +134,10 @@ exports.validate = function (xmlData, options) {
|
|
|
126
134
|
//if the root level has been reached before ...
|
|
127
135
|
if (reachedRoot === true) {
|
|
128
136
|
return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
|
|
137
|
+
} else if(options.unpairedTags.indexOf(tagName) !== -1){
|
|
138
|
+
//don't push into stack
|
|
129
139
|
} else {
|
|
130
|
-
tags.push(tagName);
|
|
140
|
+
tags.push({tagName, tagStartPos});
|
|
131
141
|
}
|
|
132
142
|
tagFound = true;
|
|
133
143
|
}
|
|
@@ -152,6 +162,10 @@ exports.validate = function (xmlData, options) {
|
|
|
152
162
|
if (afterAmp == -1)
|
|
153
163
|
return getErrorObject('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
|
|
154
164
|
i = afterAmp;
|
|
165
|
+
}else{
|
|
166
|
+
if (reachedRoot === true && !isWhiteSpace(xmlData[i])) {
|
|
167
|
+
return getErrorObject('InvalidXml', "Extra text at the end", getLineNumberForPosition(xmlData, i));
|
|
168
|
+
}
|
|
155
169
|
}
|
|
156
170
|
} //end of reading tag text value
|
|
157
171
|
if (xmlData[i] === '<') {
|
|
@@ -159,7 +173,7 @@ exports.validate = function (xmlData, options) {
|
|
|
159
173
|
}
|
|
160
174
|
}
|
|
161
175
|
} else {
|
|
162
|
-
if (
|
|
176
|
+
if ( isWhiteSpace(xmlData[i])) {
|
|
163
177
|
continue;
|
|
164
178
|
}
|
|
165
179
|
return getErrorObject('InvalidChar', "char '"+xmlData[i]+"' is not expected.", getLineNumberForPosition(xmlData, i));
|
|
@@ -168,24 +182,31 @@ exports.validate = function (xmlData, options) {
|
|
|
168
182
|
|
|
169
183
|
if (!tagFound) {
|
|
170
184
|
return getErrorObject('InvalidXml', 'Start tag expected.', 1);
|
|
171
|
-
}
|
|
172
|
-
|
|
185
|
+
}else if (tags.length == 1) {
|
|
186
|
+
return getErrorObject('InvalidTag', "Unclosed tag '"+tags[0].tagName+"'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
|
|
187
|
+
}else if (tags.length > 0) {
|
|
188
|
+
return getErrorObject('InvalidXml', "Invalid '"+
|
|
189
|
+
JSON.stringify(tags.map(t => t.tagName), null, 4).replace(/\r?\n/g, '')+
|
|
190
|
+
"' found.", {line: 1, col: 1});
|
|
173
191
|
}
|
|
174
192
|
|
|
175
193
|
return true;
|
|
176
194
|
};
|
|
177
195
|
|
|
196
|
+
function isWhiteSpace(char){
|
|
197
|
+
return char === ' ' || char === '\t' || char === '\n' || char === '\r';
|
|
198
|
+
}
|
|
178
199
|
/**
|
|
179
200
|
* Read Processing insstructions and skip
|
|
180
201
|
* @param {*} xmlData
|
|
181
202
|
* @param {*} i
|
|
182
203
|
*/
|
|
183
204
|
function readPI(xmlData, i) {
|
|
184
|
-
|
|
205
|
+
const start = i;
|
|
185
206
|
for (; i < xmlData.length; i++) {
|
|
186
207
|
if (xmlData[i] == '?' || xmlData[i] == ' ') {
|
|
187
208
|
//tagname
|
|
188
|
-
|
|
209
|
+
const tagname = xmlData.substr(start, i - start);
|
|
189
210
|
if (i > 5 && tagname === 'xml') {
|
|
190
211
|
return getErrorObject('InvalidXml', 'XML declaration allowed only at the start of the document.', getLineNumberForPosition(xmlData, i));
|
|
191
212
|
} else if (xmlData[i] == '?' && xmlData[i + 1] == '>') {
|
|
@@ -251,8 +272,8 @@ function readCommentAndCDATA(xmlData, i) {
|
|
|
251
272
|
return i;
|
|
252
273
|
}
|
|
253
274
|
|
|
254
|
-
|
|
255
|
-
|
|
275
|
+
const doubleQuote = '"';
|
|
276
|
+
const singleQuote = "'";
|
|
256
277
|
|
|
257
278
|
/**
|
|
258
279
|
* Keep reading xmlData until '<' is found outside the attribute value.
|
|
@@ -269,7 +290,6 @@ function readAttributeStr(xmlData, i) {
|
|
|
269
290
|
startChar = xmlData[i];
|
|
270
291
|
} else if (startChar !== xmlData[i]) {
|
|
271
292
|
//if vaue is enclosed with double quote then single quotes are allowed inside the value and vice versa
|
|
272
|
-
continue;
|
|
273
293
|
} else {
|
|
274
294
|
startChar = '';
|
|
275
295
|
}
|
|
@@ -310,23 +330,23 @@ function validateAttributeString(attrStr, options) {
|
|
|
310
330
|
for (let i = 0; i < matches.length; i++) {
|
|
311
331
|
if (matches[i][1].length === 0) {
|
|
312
332
|
//nospace before attribute name: a="sd"b="saf"
|
|
313
|
-
return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' has no space in starting.", getPositionFromMatch(
|
|
333
|
+
return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' has no space in starting.", getPositionFromMatch(matches[i]))
|
|
314
334
|
} else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
|
|
315
335
|
//independent attribute: ab
|
|
316
|
-
return getErrorObject('InvalidAttr', "boolean attribute '"+matches[i][2]+"' is not allowed.", getPositionFromMatch(
|
|
336
|
+
return getErrorObject('InvalidAttr', "boolean attribute '"+matches[i][2]+"' is not allowed.", getPositionFromMatch(matches[i]));
|
|
317
337
|
}
|
|
318
338
|
/* else if(matches[i][6] === undefined){//attribute without value: ab=
|
|
319
339
|
return { err: { code:"InvalidAttr",msg:"attribute " + matches[i][2] + " has no value assigned."}};
|
|
320
340
|
} */
|
|
321
341
|
const attrName = matches[i][2];
|
|
322
342
|
if (!validateAttrName(attrName)) {
|
|
323
|
-
return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(
|
|
343
|
+
return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(matches[i]));
|
|
324
344
|
}
|
|
325
345
|
if (!attrNames.hasOwnProperty(attrName)) {
|
|
326
346
|
//check for duplicate attribute.
|
|
327
347
|
attrNames[attrName] = 1;
|
|
328
348
|
} else {
|
|
329
|
-
return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(
|
|
349
|
+
return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(matches[i]));
|
|
330
350
|
}
|
|
331
351
|
}
|
|
332
352
|
|
|
@@ -373,7 +393,8 @@ function getErrorObject(code, message, lineNumber) {
|
|
|
373
393
|
err: {
|
|
374
394
|
code: code,
|
|
375
395
|
msg: message,
|
|
376
|
-
line: lineNumber,
|
|
396
|
+
line: lineNumber.line || lineNumber,
|
|
397
|
+
col: lineNumber.col,
|
|
377
398
|
},
|
|
378
399
|
};
|
|
379
400
|
}
|
|
@@ -390,11 +411,16 @@ function validateTagName(tagname) {
|
|
|
390
411
|
|
|
391
412
|
//this function returns the line number for the character at the given index
|
|
392
413
|
function getLineNumberForPosition(xmlData, index) {
|
|
393
|
-
|
|
394
|
-
return
|
|
414
|
+
const lines = xmlData.substring(0, index).split(/\r?\n/);
|
|
415
|
+
return {
|
|
416
|
+
line: lines.length,
|
|
417
|
+
|
|
418
|
+
// column number is last line's length + 1, because column numbering starts at 1:
|
|
419
|
+
col: lines[lines.length - 1].length + 1
|
|
420
|
+
};
|
|
395
421
|
}
|
|
396
422
|
|
|
397
|
-
//this function returns the position of the
|
|
398
|
-
function getPositionFromMatch(
|
|
399
|
-
return
|
|
423
|
+
//this function returns the position of the first character of match within attrStr
|
|
424
|
+
function getPositionFromMatch(match) {
|
|
425
|
+
return match.startIndex + match[1].length;
|
|
400
426
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
//parse Empty Node as self closing node
|
|
3
|
+
const buildOptions = require('../util').buildOptions;
|
|
4
|
+
const buildFromOrderedJs = require('./orderedJs2Xml');
|
|
5
|
+
|
|
6
|
+
const defaultOptions = {
|
|
7
|
+
attributeNamePrefix: '@_',
|
|
8
|
+
attributesGroupName: false,
|
|
9
|
+
textNodeName: '#text',
|
|
10
|
+
ignoreAttributes: true,
|
|
11
|
+
cdataPropName: false,
|
|
12
|
+
format: false,
|
|
13
|
+
indentBy: ' ',
|
|
14
|
+
suppressEmptyNode: false,
|
|
15
|
+
tagValueProcessor: function(key, a) {
|
|
16
|
+
return a;
|
|
17
|
+
},
|
|
18
|
+
attributeValueProcessor: function(attrName, a) {
|
|
19
|
+
return a;
|
|
20
|
+
},
|
|
21
|
+
preserveOrder: false,
|
|
22
|
+
commentPropName: false,
|
|
23
|
+
unpairedTags: [],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const props = [
|
|
27
|
+
'attributeNamePrefix',
|
|
28
|
+
'attributesGroupName',
|
|
29
|
+
'textNodeName',
|
|
30
|
+
'ignoreAttributes',
|
|
31
|
+
'cdataPropName',
|
|
32
|
+
'format',
|
|
33
|
+
'indentBy',
|
|
34
|
+
'suppressEmptyNode',
|
|
35
|
+
'tagValueProcessor',
|
|
36
|
+
'attributeValueProcessor',
|
|
37
|
+
'arrayNodeName', //when array as root
|
|
38
|
+
'preserveOrder',
|
|
39
|
+
"commentPropName",
|
|
40
|
+
"unpairedTags",
|
|
41
|
+
// 'rootNodeName', //when jsObject have multiple properties on root level
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function Builder(options) {
|
|
45
|
+
this.options = buildOptions(options, defaultOptions, props);
|
|
46
|
+
if (this.options.ignoreAttributes || this.options.attributesGroupName) {
|
|
47
|
+
this.isAttribute = function(/*a*/) {
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
} else {
|
|
51
|
+
this.attrPrefixLen = this.options.attributeNamePrefix.length;
|
|
52
|
+
this.isAttribute = isAttribute;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.processTextOrObjNode = processTextOrObjNode
|
|
56
|
+
|
|
57
|
+
if (this.options.format) {
|
|
58
|
+
this.indentate = indentate;
|
|
59
|
+
this.tagEndChar = '>\n';
|
|
60
|
+
this.newLine = '\n';
|
|
61
|
+
} else {
|
|
62
|
+
this.indentate = function() {
|
|
63
|
+
return '';
|
|
64
|
+
};
|
|
65
|
+
this.tagEndChar = '>';
|
|
66
|
+
this.newLine = '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (this.options.suppressEmptyNode) {
|
|
70
|
+
this.buildTextNode = buildEmptyTextNode;
|
|
71
|
+
this.buildObjNode = buildEmptyObjNode;
|
|
72
|
+
} else {
|
|
73
|
+
this.buildTextNode = buildTextValNode;
|
|
74
|
+
this.buildObjNode = buildObjectNode;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.buildTextValNode = buildTextValNode;
|
|
78
|
+
this.buildObjectNode = buildObjectNode;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Builder.prototype.build = function(jObj) {
|
|
82
|
+
if(this.options.preserveOrder){
|
|
83
|
+
return buildFromOrderedJs(jObj, this.options);
|
|
84
|
+
}else {
|
|
85
|
+
if(Array.isArray(jObj) && this.options.arrayNodeName && this.options.arrayNodeName.length > 1){
|
|
86
|
+
jObj = {
|
|
87
|
+
[this.options.arrayNodeName] : jObj
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return this.j2x(jObj, 0).val;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
Builder.prototype.j2x = function(jObj, level) {
|
|
95
|
+
let attrStr = '';
|
|
96
|
+
let val = '';
|
|
97
|
+
for (let key in jObj) {
|
|
98
|
+
if (typeof jObj[key] === 'undefined') {
|
|
99
|
+
// supress undefined node
|
|
100
|
+
} else if (jObj[key] === null) {
|
|
101
|
+
val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
|
|
102
|
+
} else if (jObj[key] instanceof Date) {
|
|
103
|
+
val += this.buildTextNode(jObj[key], key, '', level);
|
|
104
|
+
} else if (typeof jObj[key] !== 'object') {
|
|
105
|
+
//premitive type
|
|
106
|
+
const attr = this.isAttribute(key);
|
|
107
|
+
if (attr) {
|
|
108
|
+
attrStr += ' ' + attr + '="' + this.options.attributeValueProcessor(attr, '' + jObj[key]) + '"';
|
|
109
|
+
}else {
|
|
110
|
+
//tag value
|
|
111
|
+
if (key === this.options.textNodeName) {
|
|
112
|
+
val += this.options.tagValueProcessor(key, '' + jObj[key]);
|
|
113
|
+
} else {
|
|
114
|
+
val += this.buildTextNode(jObj[key], key, '', level);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else if (Array.isArray(jObj[key])) {
|
|
118
|
+
//repeated nodes
|
|
119
|
+
const arrLen = jObj[key].length;
|
|
120
|
+
for (let j = 0; j < arrLen; j++) {
|
|
121
|
+
const item = jObj[key][j];
|
|
122
|
+
if (typeof item === 'undefined') {
|
|
123
|
+
// supress undefined node
|
|
124
|
+
} else if (item === null) {
|
|
125
|
+
val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
|
|
126
|
+
} else if (typeof item === 'object') {
|
|
127
|
+
val += this.processTextOrObjNode(item, key, level)
|
|
128
|
+
} else {
|
|
129
|
+
val += this.buildTextNode(item, key, '', level);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
//nested node
|
|
134
|
+
if (this.options.attributesGroupName && key === this.options.attributesGroupName) {
|
|
135
|
+
const Ks = Object.keys(jObj[key]);
|
|
136
|
+
const L = Ks.length;
|
|
137
|
+
for (let j = 0; j < L; j++) {
|
|
138
|
+
attrStr += ' ' + Ks[j] + '="' + this.options.attributeValueProcessor(Ks[j], '' + jObj[key][Ks[j]]) + '"';
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
val += this.processTextOrObjNode(jObj[key], key, level)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {attrStr: attrStr, val: val};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function processTextOrObjNode (object, key, level) {
|
|
149
|
+
const result = this.j2x(object, level + 1);
|
|
150
|
+
if (object[this.options.textNodeName] !== undefined && Object.keys(object).length === 1) {
|
|
151
|
+
return this.buildTextNode(result.val, key, result.attrStr, level);
|
|
152
|
+
} else {
|
|
153
|
+
return this.buildObjNode(result.val, key, result.attrStr, level);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildObjectNode(val, key, attrStr, level) {
|
|
158
|
+
if (attrStr && val.indexOf('<') === -1) {
|
|
159
|
+
return (
|
|
160
|
+
this.indentate(level) +
|
|
161
|
+
'<' +
|
|
162
|
+
key +
|
|
163
|
+
attrStr +
|
|
164
|
+
'>' +
|
|
165
|
+
val +
|
|
166
|
+
//+ this.newLine
|
|
167
|
+
// + this.indentate(level)
|
|
168
|
+
'</' +
|
|
169
|
+
key +
|
|
170
|
+
this.tagEndChar
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
return (
|
|
174
|
+
this.indentate(level) +
|
|
175
|
+
'<' +
|
|
176
|
+
key +
|
|
177
|
+
attrStr +
|
|
178
|
+
this.tagEndChar +
|
|
179
|
+
val +
|
|
180
|
+
//+ this.newLine
|
|
181
|
+
this.indentate(level) +
|
|
182
|
+
'</' +
|
|
183
|
+
key +
|
|
184
|
+
this.tagEndChar
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildEmptyObjNode(val, key, attrStr, level) {
|
|
190
|
+
if (val !== '') {
|
|
191
|
+
return this.buildObjectNode(val, key, attrStr, level);
|
|
192
|
+
} else {
|
|
193
|
+
return this.indentate(level) + '<' + key + attrStr + '/' + this.tagEndChar;
|
|
194
|
+
//+ this.newLine
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildTextValNode(val, key, attrStr, level) {
|
|
199
|
+
return (
|
|
200
|
+
this.indentate(level) +
|
|
201
|
+
'<' +
|
|
202
|
+
key +
|
|
203
|
+
attrStr +
|
|
204
|
+
'>' +
|
|
205
|
+
this.options.tagValueProcessor(key, val) +
|
|
206
|
+
'</' +
|
|
207
|
+
key +
|
|
208
|
+
this.tagEndChar
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildEmptyTextNode(val, key, attrStr, level) {
|
|
213
|
+
if( val === '' && this.options.unpairedTags.indexOf(key) !== -1){
|
|
214
|
+
return this.indentate(level) + '<' + key + attrStr + this.tagEndChar;
|
|
215
|
+
}else if (val !== '') {
|
|
216
|
+
return this.buildTextValNode(val, key, attrStr, level);
|
|
217
|
+
} else {
|
|
218
|
+
return this.indentate(level) + '<' + key + attrStr + '/' + this.tagEndChar;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function indentate(level) {
|
|
223
|
+
return this.options.indentBy.repeat(level);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isAttribute(name /*, options*/) {
|
|
227
|
+
if (name.startsWith(this.options.attributeNamePrefix)) {
|
|
228
|
+
return name.substr(this.attrPrefixLen);
|
|
229
|
+
} else {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = Builder;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const {EOL} = require('os');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param {array} jArray
|
|
6
|
+
* @param {any} options
|
|
7
|
+
* @returns
|
|
8
|
+
*/
|
|
9
|
+
function toXml(jArray, options){
|
|
10
|
+
return arrToStr( jArray, options, 0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function arrToStr(arr, options, level){
|
|
14
|
+
let xmlStr = "";
|
|
15
|
+
|
|
16
|
+
let indentation = "";
|
|
17
|
+
if(options.format && options.indentBy.length > 0){//TODO: this logic can be avoided for each call
|
|
18
|
+
indentation = EOL + "" + options.indentBy.repeat(level);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < arr.length; i++) {
|
|
22
|
+
const tagObj = arr[i];
|
|
23
|
+
const tagName = propName(tagObj);
|
|
24
|
+
|
|
25
|
+
if(tagName === options.textNodeName){
|
|
26
|
+
xmlStr += indentation + options.tagValueProcessor( tagName, tagObj[tagName]);
|
|
27
|
+
continue;
|
|
28
|
+
}else if( tagName === options.cdataPropName){
|
|
29
|
+
xmlStr += indentation + `<![CDATA[${tagObj[tagName][0][options.textNodeName]}]]>`;
|
|
30
|
+
continue;
|
|
31
|
+
}else if( tagName === options.commentPropName){
|
|
32
|
+
xmlStr += indentation + `<!--${tagObj[tagName][0][options.textNodeName]}-->`;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const attStr = attr_to_str(tagObj.attributes, options);
|
|
36
|
+
let tagStart = indentation + `<${tagName}${attStr}`;
|
|
37
|
+
let tagValue = arrToStr(tagObj[tagName], options, level + 1);
|
|
38
|
+
if( (!tagValue || tagValue.length === 0) && options.suppressEmptyNode){
|
|
39
|
+
if(options.unpairedTags.indexOf(tagName) !== -1){
|
|
40
|
+
xmlStr += tagStart + ">";
|
|
41
|
+
}else{
|
|
42
|
+
xmlStr += tagStart + "/>";
|
|
43
|
+
}
|
|
44
|
+
}else{
|
|
45
|
+
//TODO: node with only text value should not parse the text value in next line
|
|
46
|
+
xmlStr += tagStart + `>${tagValue}${indentation}</${tagName}>` ;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return xmlStr;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function propName(obj){
|
|
54
|
+
const keys = Object.keys(obj);
|
|
55
|
+
for (let i = 0; i < keys.length; i++) {
|
|
56
|
+
const key = keys[i];
|
|
57
|
+
if(key !== "attributes") return key;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function attr_to_str(attrMap, options){
|
|
62
|
+
let attrStr = "";
|
|
63
|
+
if(attrMap && !options.ignoreAttributes){
|
|
64
|
+
for( attr in attrMap){
|
|
65
|
+
attrStr+= ` ${attr.substr(options.attributeNamePrefix.length)}="${options.attributeValueProcessor(attr, attrMap[attr])}"`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return attrStr;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = toXml;
|
|
File without changes
|