fast-xml-parser 3.20.0 → 4.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/fxp.d.ts ADDED
@@ -0,0 +1,71 @@
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
+ cdataTagName: false | string;
13
+ tagValueProcessor: (tagName: string, tagValue: string, jPath: string, hasAttributes: boolean, isLeafNode: boolean) => string;
14
+ attributeValueProcessor: (attrName: string, attrValue: string, jPath: string) => string;
15
+ numberParseOptions: strnumOptions;
16
+ stopNodes: string[];
17
+ alwaysCreateTextNode: boolean;
18
+ isArray: (tagName: string, jPath: string, isLeafNode: boolean, isAttribute: boolean) => boolean;
19
+ };
20
+ type strnumOptions = {
21
+ hex: boolean;
22
+ leadingZeros: boolean,
23
+ skipLike?: RegExp
24
+ }
25
+ type X2jOptionsOptional = Partial<X2jOptions>;
26
+ type validationOptions = {
27
+ allowBooleanAttributes: boolean;
28
+ };
29
+ type validationOptionsOptional = Partial<validationOptions>;
30
+
31
+ type XmlBuilderOptions = {
32
+ attributeNamePrefix: string;
33
+ attributesGroupName: false | string;
34
+ textNodeName: string;
35
+ ignoreAttributes: boolean;
36
+ cdataTagName: false | string;
37
+ format: boolean;
38
+ indentBy: string;
39
+ arrayNodeName: string;
40
+ suppressEmptyNode: boolean;
41
+ preserveOrder: boolean;
42
+ tagValueProcessor: (name: string, value: string) => string;
43
+ attributeValueProcessor: (name: string, value: string) => string;
44
+ };
45
+ type XmlBuilderOptionsOptional = Partial<XmlBuilderOptions>;
46
+
47
+ type ESchema = string | object | Array<string|object>;
48
+
49
+ type ValidationError = {
50
+ err: {
51
+ code: string;
52
+ msg: string,
53
+ line: number,
54
+ col: number
55
+ };
56
+ };
57
+
58
+ export class XMLParser {
59
+ constructor(options?: X2jOptionsOptional);
60
+ parse(xmlData: string | Buffer ,validationOptions?: validationOptionsOptional | boolean);
61
+ }
62
+
63
+ export function XMLValidator(
64
+ xmlData: string,
65
+ options?: validationOptionsOptional
66
+ ): true | ValidationError;
67
+
68
+ export class XMLBuilder {
69
+ constructor(options: XmlBuilderOptionsOptional);
70
+ parse(options: any): any;
71
+ }
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
@@ -25,7 +25,7 @@ exports.validate = function (xmlData, options) {
25
25
  // check for byte order mark (BOM)
26
26
  xmlData = xmlData.substr(1);
27
27
  }
28
-
28
+
29
29
  for (let i = 0; i < xmlData.length; i++) {
30
30
 
31
31
  if (xmlData[i] === '<' && xmlData[i+1] === '?') {
@@ -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 = "There is an unnecessary space between tag name and backward slash '</ ..'.";
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, i - attrStr.length + isValid.err.line));
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, i));
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
- return getErrorObject('InvalidTag', "Closing tag '"+otg+"' is expected inplace of '"+tagName+"'.", getLineNumberForPosition(xmlData, i));
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
  }
@@ -152,6 +156,10 @@ exports.validate = function (xmlData, options) {
152
156
  if (afterAmp == -1)
153
157
  return getErrorObject('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
154
158
  i = afterAmp;
159
+ }else{
160
+ if (reachedRoot === true && !isWhiteSpace(xmlData[i])) {
161
+ return getErrorObject('InvalidXml', "Extra text at the end", getLineNumberForPosition(xmlData, i));
162
+ }
155
163
  }
156
164
  } //end of reading tag text value
157
165
  if (xmlData[i] === '<') {
@@ -159,7 +167,7 @@ exports.validate = function (xmlData, options) {
159
167
  }
160
168
  }
161
169
  } else {
162
- if (xmlData[i] === ' ' || xmlData[i] === '\t' || xmlData[i] === '\n' || xmlData[i] === '\r') {
170
+ if ( isWhiteSpace(xmlData[i])) {
163
171
  continue;
164
172
  }
165
173
  return getErrorObject('InvalidChar', "char '"+xmlData[i]+"' is not expected.", getLineNumberForPosition(xmlData, i));
@@ -168,24 +176,31 @@ exports.validate = function (xmlData, options) {
168
176
 
169
177
  if (!tagFound) {
170
178
  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);
179
+ }else if (tags.length == 1) {
180
+ return getErrorObject('InvalidTag', "Unclosed tag '"+tags[0].tagName+"'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
181
+ }else if (tags.length > 0) {
182
+ return getErrorObject('InvalidXml', "Invalid '"+
183
+ JSON.stringify(tags.map(t => t.tagName), null, 4).replace(/\r?\n/g, '')+
184
+ "' found.", {line: 1, col: 1});
173
185
  }
174
186
 
175
187
  return true;
176
188
  };
177
189
 
190
+ function isWhiteSpace(char){
191
+ return char === ' ' || char === '\t' || char === '\n' || char === '\r';
192
+ }
178
193
  /**
179
194
  * Read Processing insstructions and skip
180
195
  * @param {*} xmlData
181
196
  * @param {*} i
182
197
  */
183
198
  function readPI(xmlData, i) {
184
- var start = i;
199
+ const start = i;
185
200
  for (; i < xmlData.length; i++) {
186
201
  if (xmlData[i] == '?' || xmlData[i] == ' ') {
187
202
  //tagname
188
- var tagname = xmlData.substr(start, i - start);
203
+ const tagname = xmlData.substr(start, i - start);
189
204
  if (i > 5 && tagname === 'xml') {
190
205
  return getErrorObject('InvalidXml', 'XML declaration allowed only at the start of the document.', getLineNumberForPosition(xmlData, i));
191
206
  } else if (xmlData[i] == '?' && xmlData[i + 1] == '>') {
@@ -251,8 +266,8 @@ function readCommentAndCDATA(xmlData, i) {
251
266
  return i;
252
267
  }
253
268
 
254
- var doubleQuote = '"';
255
- var singleQuote = "'";
269
+ const doubleQuote = '"';
270
+ const singleQuote = "'";
256
271
 
257
272
  /**
258
273
  * Keep reading xmlData until '<' is found outside the attribute value.
@@ -269,7 +284,6 @@ function readAttributeStr(xmlData, i) {
269
284
  startChar = xmlData[i];
270
285
  } else if (startChar !== xmlData[i]) {
271
286
  //if vaue is enclosed with double quote then single quotes are allowed inside the value and vice versa
272
- continue;
273
287
  } else {
274
288
  startChar = '';
275
289
  }
@@ -310,23 +324,23 @@ function validateAttributeString(attrStr, options) {
310
324
  for (let i = 0; i < matches.length; i++) {
311
325
  if (matches[i][1].length === 0) {
312
326
  //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]))
327
+ return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' has no space in starting.", getPositionFromMatch(matches[i]))
314
328
  } else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
315
329
  //independent attribute: ab
316
- return getErrorObject('InvalidAttr', "boolean attribute '"+matches[i][2]+"' is not allowed.", getPositionFromMatch(attrStr, matches[i][0]));
330
+ return getErrorObject('InvalidAttr', "boolean attribute '"+matches[i][2]+"' is not allowed.", getPositionFromMatch(matches[i]));
317
331
  }
318
332
  /* else if(matches[i][6] === undefined){//attribute without value: ab=
319
333
  return { err: { code:"InvalidAttr",msg:"attribute " + matches[i][2] + " has no value assigned."}};
320
334
  } */
321
335
  const attrName = matches[i][2];
322
336
  if (!validateAttrName(attrName)) {
323
- return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(attrStr, matches[i][0]));
337
+ return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(matches[i]));
324
338
  }
325
339
  if (!attrNames.hasOwnProperty(attrName)) {
326
340
  //check for duplicate attribute.
327
341
  attrNames[attrName] = 1;
328
342
  } else {
329
- return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(attrStr, matches[i][0]));
343
+ return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(matches[i]));
330
344
  }
331
345
  }
332
346
 
@@ -373,7 +387,8 @@ function getErrorObject(code, message, lineNumber) {
373
387
  err: {
374
388
  code: code,
375
389
  msg: message,
376
- line: lineNumber,
390
+ line: lineNumber.line || lineNumber,
391
+ col: lineNumber.col,
377
392
  },
378
393
  };
379
394
  }
@@ -390,11 +405,16 @@ function validateTagName(tagname) {
390
405
 
391
406
  //this function returns the line number for the character at the given index
392
407
  function getLineNumberForPosition(xmlData, index) {
393
- var lines = xmlData.substring(0, index).split(/\r?\n/);
394
- return lines.length;
408
+ const lines = xmlData.substring(0, index).split(/\r?\n/);
409
+ return {
410
+ line: lines.length,
411
+
412
+ // column number is last line's length + 1, because column numbering starts at 1:
413
+ col: lines[lines.length - 1].length + 1
414
+ };
395
415
  }
396
416
 
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;
417
+ //this function returns the position of the first character of match within attrStr
418
+ function getPositionFromMatch(match) {
419
+ return match.startIndex + match[1].length;
400
420
  }
@@ -1,42 +1,45 @@
1
1
  'use strict';
2
2
  //parse Empty Node as self closing node
3
- const buildOptions = require('./util').buildOptions;
3
+ const buildOptions = require('../util').buildOptions;
4
+ const buildFromOrderedJs = require('./orderedJs2Xml');
4
5
 
5
6
  const defaultOptions = {
6
7
  attributeNamePrefix: '@_',
7
- attrNodeName: false,
8
+ attributesGroupName: false,
8
9
  textNodeName: '#text',
9
10
  ignoreAttributes: true,
10
11
  cdataTagName: false,
11
- cdataPositionChar: '\\c',
12
12
  format: false,
13
13
  indentBy: ' ',
14
- supressEmptyNode: false,
15
- tagValueProcessor: function(a) {
14
+ suppressEmptyNode: false,
15
+ tagValueProcessor: function(key, a) {
16
16
  return a;
17
17
  },
18
- attrValueProcessor: function(a) {
18
+ attributeValueProcessor: function(attrName, a) {
19
19
  return a;
20
20
  },
21
+ preserveOrder: false
21
22
  };
22
23
 
23
24
  const props = [
24
25
  'attributeNamePrefix',
25
- 'attrNodeName',
26
+ 'attributesGroupName',
26
27
  'textNodeName',
27
28
  'ignoreAttributes',
28
29
  'cdataTagName',
29
- 'cdataPositionChar',
30
30
  'format',
31
31
  'indentBy',
32
- 'supressEmptyNode',
32
+ 'suppressEmptyNode',
33
33
  'tagValueProcessor',
34
- 'attrValueProcessor',
34
+ 'attributeValueProcessor',
35
+ 'arrayNodeName', //when array as root
36
+ 'preserveOrder',
37
+ // 'rootNodeName', //when jsObject have multiple properties on root level
35
38
  ];
36
39
 
37
- function Parser(options) {
40
+ function Builder(options) {
38
41
  this.options = buildOptions(options, defaultOptions, props);
39
- if (this.options.ignoreAttributes || this.options.attrNodeName) {
42
+ if (this.options.ignoreAttributes || this.options.attributesGroupName) {
40
43
  this.isAttribute = function(/*a*/) {
41
44
  return false;
42
45
  };
@@ -44,15 +47,8 @@ function Parser(options) {
44
47
  this.attrPrefixLen = this.options.attributeNamePrefix.length;
45
48
  this.isAttribute = isAttribute;
46
49
  }
47
- if (this.options.cdataTagName) {
48
- this.isCDATA = isCDATA;
49
- } else {
50
- this.isCDATA = function(/*a*/) {
51
- return false;
52
- };
53
- }
54
- this.replaceCDATAstr = replaceCDATAstr;
55
- this.replaceCDATAarr = replaceCDATAarr;
50
+
51
+ this.processTextOrObjNode = processTextOrObjNode
56
52
 
57
53
  if (this.options.format) {
58
54
  this.indentate = indentate;
@@ -66,7 +62,7 @@ function Parser(options) {
66
62
  this.newLine = '';
67
63
  }
68
64
 
69
- if (this.options.supressEmptyNode) {
65
+ if (this.options.suppressEmptyNode) {
70
66
  this.buildTextNode = buildEmptyTextNode;
71
67
  this.buildObjNode = buildEmptyObjNode;
72
68
  } else {
@@ -78,17 +74,23 @@ function Parser(options) {
78
74
  this.buildObjectNode = buildObjectNode;
79
75
  }
80
76
 
81
- Parser.prototype.parse = function(jObj) {
82
- return this.j2x(jObj, 0).val;
77
+ Builder.prototype.build = function(jObj) {
78
+ if(this.options.preserveOrder){
79
+ return buildFromOrderedJs(jObj, this.options);
80
+ }else {
81
+ if(Array.isArray(jObj) && this.options.arrayNodeName && this.options.arrayNodeName.length > 1){
82
+ jObj = {
83
+ [this.options.arrayNodeName] : jObj
84
+ }
85
+ }
86
+ return this.j2x(jObj, 0).val;
87
+ }
83
88
  };
84
89
 
85
- Parser.prototype.j2x = function(jObj, level) {
90
+ Builder.prototype.j2x = function(jObj, level) {
86
91
  let attrStr = '';
87
92
  let val = '';
88
- const keys = Object.keys(jObj);
89
- const len = keys.length;
90
- for (let i = 0; i < len; i++) {
91
- const key = keys[i];
93
+ for (let key in jObj) {
92
94
  if (typeof jObj[key] === 'undefined') {
93
95
  // supress undefined node
94
96
  } else if (jObj[key] === null) {
@@ -99,91 +101,57 @@ Parser.prototype.j2x = function(jObj, level) {
99
101
  //premitive type
100
102
  const attr = this.isAttribute(key);
101
103
  if (attr) {
102
- attrStr += ' ' + attr + '="' + this.options.attrValueProcessor('' + jObj[key]) + '"';
103
- } else if (this.isCDATA(key)) {
104
- if (jObj[this.options.textNodeName]) {
105
- val += this.replaceCDATAstr(jObj[this.options.textNodeName], jObj[key]);
106
- } else {
107
- val += this.replaceCDATAstr('', jObj[key]);
108
- }
109
- } else {
104
+ attrStr += ' ' + attr + '="' + this.options.attributeValueProcessor(attr, '' + jObj[key]) + '"';
105
+ }else {
110
106
  //tag value
111
107
  if (key === this.options.textNodeName) {
112
- if (jObj[this.options.cdataTagName]) {
113
- //value will added while processing cdata
114
- } else {
115
- val += this.options.tagValueProcessor('' + jObj[key]);
116
- }
108
+ val += this.options.tagValueProcessor(key, '' + jObj[key]);
117
109
  } else {
118
110
  val += this.buildTextNode(jObj[key], key, '', level);
119
111
  }
120
112
  }
121
113
  } else if (Array.isArray(jObj[key])) {
122
114
  //repeated nodes
123
- if (this.isCDATA(key)) {
124
- val += this.indentate(level);
125
- if (jObj[this.options.textNodeName]) {
126
- val += this.replaceCDATAarr(jObj[this.options.textNodeName], jObj[key]);
115
+ const arrLen = jObj[key].length;
116
+ for (let j = 0; j < arrLen; j++) {
117
+ const item = jObj[key][j];
118
+ if (typeof item === 'undefined') {
119
+ // supress undefined node
120
+ } else if (item === null) {
121
+ val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
122
+ } else if (typeof item === 'object') {
123
+ val += this.processTextOrObjNode(item, key, level)
127
124
  } else {
128
- val += this.replaceCDATAarr('', jObj[key]);
129
- }
130
- } else {
131
- //nested nodes
132
- const arrLen = jObj[key].length;
133
- for (let j = 0; j < arrLen; j++) {
134
- const item = jObj[key][j];
135
- if (typeof item === 'undefined') {
136
- // supress undefined node
137
- } else if (item === null) {
138
- val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
139
- } else if (typeof item === 'object') {
140
- const result = this.j2x(item, level + 1);
141
- val += this.buildObjNode(result.val, key, result.attrStr, level);
142
- } else {
143
- val += this.buildTextNode(item, key, '', level);
144
- }
125
+ val += this.buildTextNode(item, key, '', level);
145
126
  }
146
127
  }
147
128
  } else {
148
129
  //nested node
149
- if (this.options.attrNodeName && key === this.options.attrNodeName) {
130
+ if (this.options.attributesGroupName && key === this.options.attributesGroupName) {
150
131
  const Ks = Object.keys(jObj[key]);
151
132
  const L = Ks.length;
152
133
  for (let j = 0; j < L; j++) {
153
- attrStr += ' ' + Ks[j] + '="' + this.options.attrValueProcessor('' + jObj[key][Ks[j]]) + '"';
134
+ attrStr += ' ' + Ks[j] + '="' + this.options.attributeValueProcessor(Ks[j], '' + jObj[key][Ks[j]]) + '"';
154
135
  }
155
136
  } else {
156
- const result = this.j2x(jObj[key], level + 1);
157
- val += this.buildObjNode(result.val, key, result.attrStr, level);
137
+ val += this.processTextOrObjNode(jObj[key], key, level)
158
138
  }
159
139
  }
160
140
  }
161
141
  return {attrStr: attrStr, val: val};
162
142
  };
163
143
 
164
- function replaceCDATAstr(str, cdata) {
165
- str = this.options.tagValueProcessor('' + str);
166
- if (this.options.cdataPositionChar === '' || str === '') {
167
- return str + '<![CDATA[' + cdata + ']]' + this.tagEndChar;
168
- } else {
169
- return str.replace(this.options.cdataPositionChar, '<![CDATA[' + cdata + ']]' + this.tagEndChar);
170
- }
171
- }
172
-
173
- function replaceCDATAarr(str, cdata) {
174
- str = this.options.tagValueProcessor('' + str);
175
- if (this.options.cdataPositionChar === '' || str === '') {
176
- return str + '<![CDATA[' + cdata.join(']]><![CDATA[') + ']]' + this.tagEndChar;
144
+ function processTextOrObjNode (object, key, level) {
145
+ const result = this.j2x(object, level + 1);
146
+ if (object[this.options.textNodeName] !== undefined && Object.keys(object).length === 1) {
147
+ return this.buildTextNode(result.val, key, result.attrStr, level);
177
148
  } else {
178
- for (let v in cdata) {
179
- str = str.replace(this.options.cdataPositionChar, '<![CDATA[' + cdata[v] + ']]>');
180
- }
181
- return str + this.newLine;
149
+ return this.buildObjNode(result.val, key, result.attrStr, level);
182
150
  }
183
151
  }
184
152
 
185
153
  function buildObjectNode(val, key, attrStr, level) {
186
- if (attrStr && !val.includes('<')) {
154
+ if (attrStr && val.indexOf('<') === -1) {
187
155
  return (
188
156
  this.indentate(level) +
189
157
  '<' +
@@ -230,7 +198,7 @@ function buildTextValNode(val, key, attrStr, level) {
230
198
  key +
231
199
  attrStr +
232
200
  '>' +
233
- this.options.tagValueProcessor(val) +
201
+ this.options.tagValueProcessor(key, val) +
234
202
  '</' +
235
203
  key +
236
204
  this.tagEndChar
@@ -257,12 +225,8 @@ function isAttribute(name /*, options*/) {
257
225
  }
258
226
  }
259
227
 
260
- function isCDATA(name) {
261
- return name === this.options.cdataTagName;
262
- }
263
-
264
228
  //formatting
265
229
  //indentation
266
230
  //\n after each closing or self closing tag
267
231
 
268
- module.exports = Parser;
232
+ module.exports = Builder;
@@ -0,0 +1,58 @@
1
+ const {EOL} = require('os');
2
+
3
+ function toXml(jObj, options){
4
+ return arrToStr( [jObj], options, 0);
5
+ }
6
+
7
+ function arrToStr(arr, options, level){
8
+ let xmlStr = "";
9
+
10
+ let indentation = "";
11
+ if(options.format && options.indentBy.length > 0){//TODO: this logic can be avoided for each call
12
+ indentation = EOL + "" + options.indentBy.repeat(level);
13
+ }
14
+
15
+ for (let i = 0; i < arr.length; i++) {
16
+ const tagObj = arr[i];
17
+ const tagName = propName(tagObj);
18
+
19
+ if(tagName === options.textNodeName){
20
+ xmlStr += indentation + options.tagValueProcessor( tagName, tagObj[tagName]);
21
+ continue;
22
+ }else if( tagName === options.cdataTagName){
23
+ xmlStr += indentation + `<![CDATA[${tagObj[tagName][0][options.textNodeName]}]]>`;
24
+ continue;
25
+ }
26
+ const attStr = attr_to_str(tagObj.attributes, options);
27
+ let tagStart = indentation + `<${tagName}${attStr}`;
28
+ let tagValue = arrToStr(tagObj[tagName], options, level + 1);
29
+ if( (!tagValue || tagValue.length === 0) && options.suppressEmptyNode){
30
+ xmlStr += tagStart + "/>";
31
+ }else{
32
+ //TODO: node with only text value should not parse the text value in next line
33
+ xmlStr += tagStart + `>${tagValue}${indentation}</${tagName}>` ;
34
+ }
35
+ }
36
+
37
+ return xmlStr;
38
+ }
39
+
40
+ function propName(obj){
41
+ const keys = Object.keys(obj);
42
+ for (let i = 0; i < keys.length; i++) {
43
+ const key = keys[i];
44
+ if(key !== "attributes") return key;
45
+ }
46
+ }
47
+
48
+ function attr_to_str(attrMap, options){
49
+ let attrStr = "";
50
+ if(attrMap && !options.ignoreAttributes){
51
+ for( attr in attrMap){
52
+ attrStr+= ` ${attr.substr(options.attributeNamePrefix.length)}="${options.attributeValueProcessor(attr, attrMap[attr])}"`;
53
+ }
54
+ }
55
+ return attrStr;
56
+ }
57
+
58
+ module.exports = toXml;
File without changes