fast-xml-parser 4.0.0-beta.5 → 4.0.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/CHANGELOG.md CHANGED
@@ -1,15 +1,33 @@
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.5 / 2021-12-04**
3
+ **4.0.0 / 2022-01-06**
4
+ * Generating different combined, parser only, builder only, validator only browser bundles
5
+ * Keeping cjs modules as they can be imported in cjs and esm modules both. Otherwise refer `esm` branch.
6
+
7
+ **4.0.0-beta.8 / 2021-12-13**
8
+ * call tagValueProcessor for stop nodes
9
+
10
+ **4.0.0-beta.7 / 2021-12-09**
11
+ * fix Validator bug when an attribute has no value but '=' only
12
+ * XML Builder should suppress unpaired tags by default.
13
+ * documents update for missing features
14
+ * refactoring to use Object.assign
15
+ * refactoring to remove repeated code
16
+
17
+ **4.0.0-beta.6 / 2021-12-05**
18
+ * Support PI Tags processing
19
+ * Support `suppressBooleanAttributes` by XML Builder for attributes with value `true`.
20
+
21
+ **4.0.0-beta.5 / 2021-12-04**
4
22
  * fix: when a tag with name "attributes"
5
23
 
6
- ** 4.0.0-beta.4 / 2021-12-02**
24
+ **4.0.0-beta.4 / 2021-12-02**
7
25
  * Support HTML document parsing
8
26
  * skip stop nodes parsing when building the XML from JS object
9
27
  * Support external entites without DOCTYPE
10
28
  * update dev dependency: strnum v1.0.5 to fix long number issue
11
29
 
12
- ** 4.0.0-beta.3 / 2021-11-30**
30
+ **4.0.0-beta.3 / 2021-11-30**
13
31
  * support global stopNodes expression like "*.stop"
14
32
  * support self-closing and paired unpaired tags
15
33
  * fix: CDATA should not be parsed.
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
@@ -89,7 +90,7 @@ const xmlContent = builder.build(jObj);
89
90
 
90
91
  In a HTML page
91
92
  ```html
92
- <script src="path/to/fxparser.js"></script>
93
+ <script src="path/to/fxp.min.js"></script>
93
94
  :
94
95
  <script>
95
96
  const parser = new fxparser.XMLParser();
@@ -97,6 +98,16 @@ In a HTML page
97
98
  </script>
98
99
  ```
99
100
 
101
+ Check lib folder for different browser bundles
102
+
103
+ | Bundle Name | Size |
104
+ | -- | -- |
105
+ | fxbuilder.min.js | 5.2K |
106
+ | fxparser.js | 50K |
107
+ | fxparser.min.js | 17K |
108
+ | fxp.min.js | 22K |
109
+ | fxvalidator.min.js | 5.7K |
110
+
100
111
  ### Documents
101
112
  **v3**
102
113
  * [documents](./docs/v3/docs.md)
@@ -108,6 +119,7 @@ In a HTML page
108
119
  4. [XML Validator](./docs/v4/4.XMLValidator.md)
109
120
  5. [Entites](./docs/5.Entities.md)
110
121
  6. [HTML Document Parsing](./docs/6.HTMLParsing.md)
122
+ 7. [PI Tag processing](./docs/7.PITags.md)
111
123
  ## Performance
112
124
 
113
125
  ### XML Parser
@@ -133,6 +145,7 @@ In a HTML page
133
145
  * Run tests for a route or from a route
134
146
  * Customizable reporting
135
147
  * Central dashboard for better monitoring
148
+ * Options to integrate E2E tests with Jira, Github etc using Central dashboard `Tian`.
136
149
  * **[Stubmatic](https://github.com/NaturalIntelligence/Stubmatic)** : Create fake webservices, DynamoDB or S3 servers, Manage fake/mock stub data, Or fake any HTTP(s) call.
137
150
 
138
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-xml-parser",
3
- "version": "4.0.0-beta.5",
3
+ "version": "4.0.0",
4
4
  "description": "Validate XML, Parse XML, Build XML without C/C++ based libraries",
5
5
  "main": "./src/fxp.js",
6
6
  "scripts": {
@@ -9,7 +9,7 @@
9
9
  "coverage": "nyc report --reporter html --reporter text -t .nyc_output --report-dir .nyc_output/summary",
10
10
  "perf": "node ./benchmark/perfTest3.js",
11
11
  "lint": "eslint src/*.js spec/*.js",
12
- "bundle": "webpack && webpack --config webpack-prod.config.js",
12
+ "bundle": "webpack --config webpack-prod.config.js",
13
13
  "prettier": "prettier --write src/**/*.js",
14
14
  "publish-please": "publish-please",
15
15
  "checkReadiness": "publish-please --dry-run"
@@ -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/fxp_cjs ADDED
@@ -0,0 +1,8 @@
1
+
2
+
3
+ const XMLValidator = require('./validator.js');
4
+ const XMLParser = require('./xmlparser/XMLParser.js');
5
+ const XMLBuilder = require('./xmlbuilder/json2xml.js');
6
+
7
+ module.exports = {XMLParser,XMLValidator, XMLBuilder};
8
+
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;
@@ -1,4 +1,4 @@
1
- const {EOL} = require('os');
1
+ const EOL = "\n";
2
2
 
3
3
  /**
4
4
  *
@@ -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
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}>` ;
@@ -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[":@"] ? Object.keys(currentNode[":@"]).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[":@"] ? Object.keys(currentNode[":@"]).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[":@"] ? Object.keys(currentNode[":@"]).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[":@"] ? Object.keys(currentNode[":@"]).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
 
@@ -313,6 +293,10 @@ const parseXml = function(xmlData) {
313
293
  if(tagName !== tagExp && attrExpPresent){
314
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
 
@@ -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);