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/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
- exports.buildOptions = function(options, defaultOptions, props) {
70
- var newOptions = {};
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 = ['allowBooleanAttributes'];
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 = "There is an unnecessary space between tag name and backward slash '</ ..'.";
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, i - attrStr.length + isValid.err.line));
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, i));
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
- return getErrorObject('InvalidTag', "Closing tag '"+otg+"' is expected inplace of '"+tagName+"'.", getLineNumberForPosition(xmlData, i));
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 (xmlData[i] === ' ' || xmlData[i] === '\t' || xmlData[i] === '\n' || xmlData[i] === '\r') {
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
- } else if (tags.length > 0) {
172
- return getErrorObject('InvalidXml', "Invalid '"+JSON.stringify(tags, null, 4).replace(/\r?\n/g, '')+"' found.", 1);
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
- var start = i;
205
+ const start = i;
185
206
  for (; i < xmlData.length; i++) {
186
207
  if (xmlData[i] == '?' || xmlData[i] == ' ') {
187
208
  //tagname
188
- var tagname = xmlData.substr(start, i - start);
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
- var doubleQuote = '"';
255
- var singleQuote = "'";
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(attrStr, matches[i][0]))
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(attrStr, matches[i][0]));
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(attrStr, matches[i][0]));
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(attrStr, matches[i][0]));
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
- var lines = xmlData.substring(0, index).split(/\r?\n/);
394
- return lines.length;
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 last character of match within attrStr
398
- function getPositionFromMatch(attrStr, match) {
399
- return attrStr.indexOf(match) + match.length;
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