fast-xml-parser 4.0.0-beta.4 → 4.0.0-beta.8

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 CHANGED
@@ -1,5 +1,22 @@
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.8 / 2021-12-13**
4
+ * call tagValueProcessor for stop nodes
5
+
6
+ ** 4.0.0-beta.7 / 2021-12-09**
7
+ * fix Validator bug when an attribute has no value but '=' only
8
+ * XML Builder should suppress unpaired tags by default.
9
+ * documents update for missing features
10
+ * refactoring to use Object.assign
11
+ * refactoring to remove repeated code
12
+
13
+ ** 4.0.0-beta.6 / 2021-12-05**
14
+ * Support PI Tags processing
15
+ * Support `suppressBooleanAttributes` by XML Builder for attributes with value `true`.
16
+
17
+ ** 4.0.0-beta.5 / 2021-12-04**
18
+ * fix: when a tag with name "attributes"
19
+
3
20
  ** 4.0.0-beta.4 / 2021-12-02**
4
21
  * Support HTML document parsing
5
22
  * skip stop nodes parsing when building the XML from JS object
package/README.md CHANGED
@@ -55,6 +55,7 @@ Check the list of all known users [here](./USERs.md);
55
55
  * Supports comments
56
56
  * It can preserve Order of tags in JS object
57
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
58
59
  * And many more other features.
59
60
 
60
61
  ## How to use
@@ -108,6 +109,7 @@ In a HTML page
108
109
  4. [XML Validator](./docs/v4/4.XMLValidator.md)
109
110
  5. [Entites](./docs/5.Entities.md)
110
111
  6. [HTML Document Parsing](./docs/6.HTMLParsing.md)
112
+ 7. [PI Tag processing](./docs/7.PITags.md)
111
113
  ## Performance
112
114
 
113
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.4",
3
+ "version": "4.0.0-beta.8",
4
4
  "description": "Validate XML, Parse XML, Build XML without C/C++ based libraries",
5
5
  "main": "./src/fxp.js",
6
6
  "scripts": {
@@ -47,6 +47,7 @@
47
47
  "@babel/preset-env": "^7.13.10",
48
48
  "@babel/register": "^7.13.8",
49
49
  "babel-loader": "^8.2.2",
50
+ "cytorus": "^0.2.9",
50
51
  "eslint": "^8.3.0",
51
52
  "he": "^1.2.0",
52
53
  "jasmine": "^3.6.4",
package/src/fxp.d.ts CHANGED
@@ -44,6 +44,7 @@ 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[];
49
50
  stopNodes: string[];
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 = util.buildOptions(options, defaultOptions, props);
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
  },
@@ -31,29 +31,8 @@ const defaultOptions = {
31
31
  stopNodes: []
32
32
  };
33
33
 
34
- const props = [
35
- 'attributeNamePrefix',
36
- 'attributesGroupName',
37
- 'textNodeName',
38
- 'ignoreAttributes',
39
- 'cdataPropName',
40
- 'format',
41
- 'indentBy',
42
- 'suppressEmptyNode',
43
- 'tagValueProcessor',
44
- 'attributeValueProcessor',
45
- 'arrayNodeName', //when array as root
46
- 'preserveOrder',
47
- "commentPropName",
48
- "unpairedTags",
49
- "entities",
50
- "processEntities",
51
- "stopNodes",
52
- // 'rootNodeName', //when jsObject have multiple properties on root level
53
- ];
54
-
55
34
  function Builder(options) {
56
- this.options = buildOptions(options, defaultOptions, props);
35
+ this.options = Object.assign({}, defaultOptions, options);
57
36
  if (this.options.ignoreAttributes || this.options.attributesGroupName) {
58
37
  this.isAttribute = function(/*a*/) {
59
38
  return false;
@@ -39,16 +39,18 @@ function arrToStr(arr, options, jPath, level){
39
39
  }else if( tagName === options.commentPropName){
40
40
  xmlStr += indentation + `<!--${tagObj[tagName][0][options.textNodeName]}-->`;
41
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;
42
46
  }
43
- const attStr = attr_to_str(tagObj.attributes, options);
47
+ const attStr = attr_to_str(tagObj[":@"], options);
44
48
  let tagStart = indentation + `<${tagName}${attStr}`;
45
49
  let tagValue = arrToStr(tagObj[tagName], options, newJPath, level + 1);
46
- if( (!tagValue || tagValue.length === 0) && options.suppressEmptyNode){
47
- if(options.unpairedTags.indexOf(tagName) !== -1){
48
- xmlStr += tagStart + ">";
49
- }else{
50
- xmlStr += tagStart + "/>";
51
- }
50
+ if(options.unpairedTags.indexOf(tagName) !== -1){
51
+ xmlStr += tagStart + ">";
52
+ }else if( (!tagValue || tagValue.length === 0) && options.suppressEmptyNode){
53
+ xmlStr += tagStart + "/>";
52
54
  }else{
53
55
  //TODO: node with only text value should not parse the text value in next line
54
56
  xmlStr += tagStart + `>${tagValue}${indentation}</${tagName}>` ;
@@ -62,7 +64,7 @@ function propName(obj){
62
64
  const keys = Object.keys(obj);
63
65
  for (let i = 0; i < keys.length; i++) {
64
66
  const key = keys[i];
65
- if(key !== "attributes") return key;
67
+ if(key !== ":@") return key;
66
68
  }
67
69
  }
68
70
 
@@ -72,7 +74,11 @@ function attr_to_str(attrMap, options){
72
74
  for( attr in attrMap){
73
75
  let attrVal = options.attributeValueProcessor(attr, attrMap[attr]);
74
76
  attrVal = replaceEntitiesValue(attrVal, options);
75
- attrStr+= ` ${attr.substr(options.attributeNamePrefix.length)}="${attrVal}"`;
77
+ if(attrVal === true && options.suppressBooleanAttributes){
78
+ attrStr+= ` ${attr.substr(options.attributeNamePrefix.length)}`;
79
+ }else{
80
+ attrStr+= ` ${attr.substr(options.attributeNamePrefix.length)}="${attrVal}"`;
81
+ }
76
82
  }
77
83
  }
78
84
  return attrStr;
@@ -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 util.buildOptions(options, defaultOptions, props);
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');
@@ -47,8 +48,8 @@ class OrderedObjParser{
47
48
  this.buildAttributesMap = buildAttributesMap;
48
49
  this.isItStopNode = isItStopNode;
49
50
  this.replaceEntitiesValue = replaceEntitiesValue;
50
- this.readTagExp = readTagExp;
51
51
  this.readStopNodeData = readStopNodeData;
52
+ this.saveTextToParentTag = saveTextToParentTag;
52
53
  }
53
54
 
54
55
  }
@@ -71,14 +72,15 @@ function addExternalEntities(externalEntities){
71
72
  * @param {boolean} dontTrim
72
73
  * @param {boolean} hasAttributes
73
74
  * @param {boolean} isLeafNode
75
+ * @param {boolean} escapeEntities
74
76
  */
75
- function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode) {
77
+ function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) {
76
78
  if (val !== undefined) {
77
79
  if (this.options.trimValues && !dontTrim) {
78
80
  val = val.trim();
79
81
  }
80
82
  if(val.length > 0){
81
- val = this.replaceEntitiesValue(val);
83
+ if(!escapeEntities) val = this.replaceEntitiesValue(val);
82
84
 
83
85
  const newval = this.options.tagValueProcessor(tagName, val, jPath, hasAttributes, isLeafNode);
84
86
  if(newval === null || newval === undefined){
@@ -192,14 +194,7 @@ const parseXml = function(xmlData) {
192
194
  }
193
195
 
194
196
  if(currentNode){
195
- textData = this.parseTextData(textData
196
- , currentNode.tagname
197
- , jPath
198
- ,false
199
- , currentNode.attributes ? Object.keys(currentNode.attributes).length !== 0 : false
200
- , Object.keys(currentNode.child).length === 0);
201
- if(textData !== undefined && textData !== "") currentNode.add(this.options.textNodeName, textData);
202
- textData = "";
197
+ textData = this.saveTextToParentTag(textData, currentNode, jPath);
203
198
  }
204
199
 
205
200
  jPath = jPath.substr(0, jPath.lastIndexOf("."));
@@ -208,24 +203,26 @@ const parseXml = function(xmlData) {
208
203
  textData = "";
209
204
  i = closeIndex;
210
205
  } else if( xmlData[i+1] === '?') {
211
- i = findClosingIndex(xmlData, "?>", i, "Pi Tag is not closed.")
206
+ let tagData = readTagExp(xmlData,i, false, "?>");
207
+ if(!tagData) throw new Error("Pi Tag is not closed.");
208
+ textData = this.saveTextToParentTag(textData, currentNode, jPath);
209
+
210
+ const childNode = new xmlNode(tagData.tagName);
211
+ childNode.add(this.options.textNodeName, "");
212
+
213
+ if(tagData.tagName !== tagData.tagExp && tagData.attrExpPresent){
214
+ childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath);
215
+ }
216
+ currentNode.addChild(childNode);
217
+
218
+ i = tagData.closeIndex + 1;
212
219
  } else if(xmlData.substr(i + 1, 3) === '!--') {
213
220
  const endIndex = findClosingIndex(xmlData, "-->", i, "Comment is not closed.")
214
221
  if(this.options.commentPropName){
215
222
  const comment = xmlData.substring(i + 4, endIndex - 2);
216
223
 
217
- //TODO: remove repeated code
218
- if(textData){ //store previously collected data as textNode
219
- textData = this.parseTextData(textData
220
- , currentNode.tagname
221
- , jPath
222
- ,false
223
- , currentNode.attributes ? Object.keys(currentNode.attributes).length !== 0 : false
224
- , Object.keys(currentNode.child).length === 0);
225
-
226
- if(textData !== undefined && textData !== "") currentNode.add(this.options.textNodeName, textData);
227
- textData = "";
228
- }
224
+ textData = this.saveTextToParentTag(textData, currentNode, jPath);
225
+
229
226
  currentNode.add(this.options.commentPropName, [ { [this.options.textNodeName] : comment } ]);
230
227
  }
231
228
  i = endIndex;
@@ -237,17 +234,7 @@ const parseXml = function(xmlData) {
237
234
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
238
235
  const tagExp = xmlData.substring(i + 9,closeIndex);
239
236
 
240
- if(textData){ //store previously collected data as textNode
241
- textData = this.parseTextData(textData
242
- , currentNode.tagname
243
- , jPath
244
- ,false
245
- , currentNode.attributes ? Object.keys(currentNode.attributes).length !== 0 : false
246
- , Object.keys(currentNode.child).length === 0);
247
-
248
- if(textData !== undefined && textData !== "") currentNode.add(this.options.textNodeName, textData);
249
- textData = "";
250
- }
237
+ textData = this.saveTextToParentTag(textData, currentNode, jPath);
251
238
 
252
239
  //cdata should be set even if it is 0 length string
253
240
  if(this.options.cdataPropName){
@@ -263,7 +250,7 @@ const parseXml = function(xmlData) {
263
250
  i = closeIndex + 2;
264
251
  }else {//Opening tag
265
252
 
266
- let result = this.readTagExp(xmlData,i);
253
+ let result = readTagExp(xmlData,i, this. options.removeNSPrefix);
267
254
  let tagName= result.tagName;
268
255
  let tagExp = result.tagExp;
269
256
  let attrExpPresent = result.attrExpPresent;
@@ -273,14 +260,7 @@ const parseXml = function(xmlData) {
273
260
  if (currentNode && textData) {
274
261
  if(currentNode.tagname !== '!xml'){
275
262
  //when nested tag is found
276
- textData = this.parseTextData(textData
277
- , currentNode.tagname
278
- , jPath
279
- , false
280
- , currentNode.attributes ? Object.keys(currentNode.attributes).length !== 0 : false
281
- , false);
282
- if(textData !== undefined && textData !== "") currentNode.add(this.options.textNodeName, textData);
283
- textData = "";
263
+ textData = this.saveTextToParentTag(textData, currentNode, jPath, false);
284
264
  }
285
265
  }
286
266
 
@@ -311,8 +291,12 @@ const parseXml = function(xmlData) {
311
291
 
312
292
  const childNode = new xmlNode(tagName);
313
293
  if(tagName !== tagExp && attrExpPresent){
314
- childNode.attributes = this.buildAttributesMap(tagExp, jPath);
294
+ childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
315
295
  }
296
+ if(tagContent) {
297
+ tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true);
298
+ }
299
+
316
300
  jPath = jPath.substr(0, jPath.lastIndexOf("."));
317
301
  childNode.add(this.options.textNodeName, tagContent);
318
302
 
@@ -330,7 +314,7 @@ const parseXml = function(xmlData) {
330
314
 
331
315
  const childNode = new xmlNode(tagName);
332
316
  if(tagName !== tagExp && attrExpPresent){
333
- childNode.attributes = this.buildAttributesMap(tagExp, jPath);
317
+ childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
334
318
  }
335
319
  jPath = jPath.substr(0, jPath.lastIndexOf("."));
336
320
  currentNode.addChild(childNode);
@@ -341,7 +325,7 @@ const parseXml = function(xmlData) {
341
325
  this.tagsNodeStack.push(currentNode);
342
326
 
343
327
  if(tagName !== tagExp && attrExpPresent){
344
- childNode.attributes = this.buildAttributesMap(tagExp, jPath);
328
+ childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
345
329
  }
346
330
  currentNode.addChild(childNode);
347
331
  currentNode = childNode;
@@ -376,6 +360,24 @@ const replaceEntitiesValue = function(val){
376
360
  }
377
361
  return val;
378
362
  }
363
+ function saveTextToParentTag(textData, currentNode, jPath, isLeafNode) {
364
+ if (textData) { //store previously collected data as textNode
365
+ if(isLeafNode === undefined) isLeafNode = Object.keys(currentNode.child).length === 0
366
+
367
+ textData = this.parseTextData(textData,
368
+ currentNode.tagname,
369
+ jPath,
370
+ false,
371
+ currentNode[":@"] ? Object.keys(currentNode[":@"]).length !== 0 : false,
372
+ isLeafNode);
373
+
374
+ if (textData !== undefined && textData !== "")
375
+ currentNode.add(this.options.textNodeName, textData);
376
+ textData = "";
377
+ }
378
+ return textData;
379
+ }
380
+
379
381
  //TODO: use jPath to simplify the logic
380
382
  /**
381
383
  *
@@ -398,7 +400,7 @@ function isItStopNode(stopNodes, jPath, currentTagName){
398
400
  * @param {number} i starting index
399
401
  * @returns
400
402
  */
401
- function tagExpWithClosingIndex(xmlData, i){
403
+ function tagExpWithClosingIndex(xmlData, i, closingChar = ">"){
402
404
  let attrBoundary;
403
405
  let tagExp = "";
404
406
  for (let index = i; index < xmlData.length; index++) {
@@ -407,11 +409,20 @@ function tagExpWithClosingIndex(xmlData, i){
407
409
  if (ch === attrBoundary) attrBoundary = "";//reset
408
410
  } else if (ch === '"' || ch === "'") {
409
411
  attrBoundary = ch;
410
- } else if (ch === '>') {
412
+ } else if (ch === closingChar[0]) {
413
+ if(closingChar[1]){
414
+ if(xmlData[index + 1] === closingChar[1]){
415
+ return {
416
+ data: tagExp,
417
+ index: index
418
+ }
419
+ }
420
+ }else{
411
421
  return {
412
422
  data: tagExp,
413
423
  index: index
414
424
  }
425
+ }
415
426
  } else if (ch === '\t') {
416
427
  ch = " "
417
428
  }
@@ -428,8 +439,9 @@ function findClosingIndex(xmlData, str, i, errMsg){
428
439
  }
429
440
  }
430
441
 
431
- function readTagExp(xmlData,i){
432
- const result = tagExpWithClosingIndex(xmlData, i+1);
442
+ function readTagExp(xmlData,i, removeNSPrefix, closingChar = ">"){
443
+ const result = tagExpWithClosingIndex(xmlData, i+1, closingChar);
444
+ if(!result) return;
433
445
  let tagExp = result.data;
434
446
  const closeIndex = result.index;
435
447
  const separatorIndex = tagExp.search(/\s/);
@@ -440,7 +452,7 @@ function readTagExp(xmlData,i){
440
452
  tagExp = tagExp.substr(separatorIndex + 1);
441
453
  }
442
454
 
443
- if(this. options.removeNSPrefix){
455
+ if(removeNSPrefix){
444
456
  const colonIndex = tagName.indexOf(":");
445
457
  if(colonIndex !== -1){
446
458
  tagName = tagName.substr(colonIndex+1);
@@ -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.attributes){
41
- assignAttributes( val, tagObj.attributes, newJpath, options);
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 !== "attributes") return key;
77
+ if(key !== ":@") return key;
78
78
  }
79
79
  }
80
80
 
@@ -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.attributes = {}; //attributes map
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.attributes && Object.keys(node.attributes).length > 0){
15
- this.child.push( { [node.tagname]: node.child, attributes: node.attributes });
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
  }