fast-xml-parser 4.3.4 → 4.3.6

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,11 @@
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.3.6 / 2024-03-16**
4
+ * Add support for parsing HTML numeric entities (#645) (By [Jonas Schade ](https://github.com/DerZade))
5
+
6
+ **4.3.5 / 2024-02-24**
7
+ * code for v5 is added for experimental use
8
+
3
9
  **4.3.4 / 2024-01-10**
4
10
  * fix: Don't escape entities in CDATA sections (#633) (By [wackbyte](https://github.com/wackbyte))
5
11
 
package/README.md CHANGED
@@ -11,7 +11,8 @@
11
11
 
12
12
  Validate XML, Parse XML to JS Object, or Build XML from JS Object without C/C++ based libraries and no callback.
13
13
 
14
- <font size="6">I need a Career advice. I've posted the query on my <a href="github.com/amitguptagwl">profile</a>. Your support would be appreciable.</font>
14
+ > XML Parser v5 is added for experimental use
15
+ > https://solothought.com
15
16
 
16
17
  Sponsor this project 👉
17
18
  <a href="https://github.com/sponsors/NaturalIntelligence">
@@ -91,6 +92,11 @@ If you want to be an anonymous user of this application and don't want to be hig
91
92
  * Supports parsing of PI (Processing Instruction) tags with XML declaration tags
92
93
  * And many more other features.
93
94
 
95
+ ## v5
96
+ I developed v5 in Apr 2023. And I didn't get the chance to complete all the features. I've ensured that new features don't impact performance. With v5, you have more control on parsing output. Check [docs](./docs/v5) for syntax help and basic understanding.
97
+
98
+ Please leave a comment in discussion forum for your suggestions and if you really need v5.
99
+
94
100
  ## How to use
95
101
 
96
102
  To use as package dependency
@@ -174,19 +180,6 @@ Check lib folder for different browser bundles
174
180
 
175
181
  [![](static/img/ni_ads_ads.gif)](https://github.com/NaturalIntelligence/ads/)
176
182
 
177
- ## Our other projects and research you must try
178
-
179
- * **[BigBit standard](https://github.com/amitguptagwl/bigbit)** :
180
- * Single text encoding to replace UTF-8, UTF-16, UTF-32 and more with less memory.
181
- * Single Numeric datatype alternative of integer, float, double, long, decimal and more without precision loss.
182
- * **[Cytorus](https://github.com/NaturalIntelligence/cytorus)**: Be specific and flexible while running E2E tests.
183
- * Run tests only for a particular User Story
184
- * Run tests for a route or from a route
185
- * Customizable reporting
186
- * Central dashboard for better monitoring
187
- * Options to integrate E2E tests with Jira, Github etc using Central dashboard `Tian`.
188
- * **[Stubmatic](https://github.com/NaturalIntelligence/Stubmatic)** : Create fake webservices, DynamoDB or S3 servers, Manage fake/mock stub data, Or fake any HTTP(s) call.
189
-
190
183
 
191
184
  ## Supporters
192
185
  ### Contributors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-xml-parser",
3
- "version": "4.3.4",
3
+ "version": "4.3.6",
4
4
  "description": "Validate XML, Parse XML, Build XML without C/C++ based libraries",
5
5
  "main": "./src/fxp.js",
6
6
  "scripts": {
@@ -0,0 +1,16 @@
1
+ modules.export = {
2
+ "<" : "<", //tag start
3
+ ">" : ">", //tag end
4
+ "/" : "/", //close tag
5
+ "!" : "!", //comment or docttype
6
+ "!--" : "!--", //comment
7
+ "-->" : "-->", //comment end
8
+ "?" : "?", //pi
9
+ "?>" : "?>", //pi end
10
+ "?xml" : "?xml", //pi end
11
+ "![" : "![", //cdata
12
+ "]]>" : "]]>", //cdata end
13
+ "[" : "[",
14
+ "-" : "-",
15
+ "D" : "D",
16
+ }
@@ -0,0 +1,107 @@
1
+ const ampEntity = { regex: /&(amp|#38|#x26);/g, val : "&"};
2
+ const htmlEntities = {
3
+ "space": { regex: /&(nbsp|#160);/g, val: " " },
4
+ // "lt" : { regex: /&(lt|#60);/g, val: "<" },
5
+ // "gt" : { regex: /&(gt|#62);/g, val: ">" },
6
+ // "amp" : { regex: /&(amp|#38);/g, val: "&" },
7
+ // "quot" : { regex: /&(quot|#34);/g, val: "\"" },
8
+ // "apos" : { regex: /&(apos|#39);/g, val: "'" },
9
+ "cent" : { regex: /&(cent|#162);/g, val: "¢" },
10
+ "pound" : { regex: /&(pound|#163);/g, val: "£" },
11
+ "yen" : { regex: /&(yen|#165);/g, val: "¥" },
12
+ "euro" : { regex: /&(euro|#8364);/g, val: "€" },
13
+ "copyright" : { regex: /&(copy|#169);/g, val: "©" },
14
+ "reg" : { regex: /&(reg|#174);/g, val: "®" },
15
+ "inr" : { regex: /&(inr|#8377);/g, val: "₹" },
16
+ "num_dec": { regex: /&#([0-9]{1,7});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 10)) },
17
+ "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 16)) },
18
+ };
19
+
20
+ class EntitiesParser{
21
+ constructor(replaceHtmlEntities) {
22
+ this.replaceHtmlEntities = replaceHtmlEntities;
23
+ this.docTypeEntities = {};
24
+ this.lastEntities = {
25
+ "apos" : { regex: /&(apos|#39|#x27);/g, val : "'"},
26
+ "gt" : { regex: /&(gt|#62|#x3E);/g, val : ">"},
27
+ "lt" : { regex: /&(lt|#60|#x3C);/g, val : "<"},
28
+ "quot" : { regex: /&(quot|#34|#x22);/g, val : "\""},
29
+ };
30
+ }
31
+
32
+ addExternalEntities(externalEntities){
33
+ const entKeys = Object.keys(externalEntities);
34
+ for (let i = 0; i < entKeys.length; i++) {
35
+ const ent = entKeys[i];
36
+ this.addExternalEntity(ent,externalEntities[ent])
37
+ }
38
+ }
39
+ addExternalEntity(key,val){
40
+ validateEntityName(key);
41
+ if(val.indexOf("&") !== -1) {
42
+ reportWarning(`Entity ${key} is not added as '&' is found in value;`)
43
+ return;
44
+ }else{
45
+ this.lastEntities[ent] = {
46
+ regex: new RegExp("&"+key+";","g"),
47
+ val : val
48
+ }
49
+ }
50
+ }
51
+
52
+ addDocTypeEntities(entities){
53
+ const entKeys = Object.keys(entities);
54
+ for (let i = 0; i < entKeys.length; i++) {
55
+ const ent = entKeys[i];
56
+ this.docTypeEntities[ent] = {
57
+ regex: new RegExp("&"+ent+";","g"),
58
+ val : entities[ent]
59
+ }
60
+ }
61
+ }
62
+
63
+ parse(val){
64
+ return this.replaceEntitiesValue(val)
65
+ }
66
+
67
+ /**
68
+ * 1. Replace DOCTYPE entities
69
+ * 2. Replace external entities
70
+ * 3. Replace HTML entities if asked
71
+ * @param {string} val
72
+ */
73
+ replaceEntitiesValue(val){
74
+ if(typeof val === "string" && val.length > 0){
75
+ for(let entityName in this.docTypeEntities){
76
+ const entity = this.docTypeEntities[entityName];
77
+ val = val.replace( entity.regx, entity.val);
78
+ }
79
+ for(let entityName in this.lastEntities){
80
+ const entity = this.lastEntities[entityName];
81
+ val = val.replace( entity.regex, entity.val);
82
+ }
83
+ if(this.replaceHtmlEntities){
84
+ for(let entityName in htmlEntities){
85
+ const entity = htmlEntities[entityName];
86
+ val = val.replace( entity.regex, entity.val);
87
+ }
88
+ }
89
+ val = val.replace( ampEntity.regex, ampEntity.val);
90
+ }
91
+ return val;
92
+ }
93
+ };
94
+
95
+ //an entity name should not contains special characters that may be used in regex
96
+ //Eg !?\\\/[]$%{}^&*()<>
97
+ const specialChar = "!?\\\/[]$%{}^&*()<>|+";
98
+
99
+ function validateEntityName(name){
100
+ for (let i = 0; i < specialChar.length; i++) {
101
+ const ch = specialChar[i];
102
+ if(name.indexOf(ch) !== -1) throw new Error(`Invalid character ${ch} in entity name`);
103
+ }
104
+ return name;
105
+ }
106
+
107
+ module.exports = EntitiesParser;
@@ -0,0 +1,64 @@
1
+
2
+ const JsObjOutputBuilder = require("./OutputBuilders/JsObjBuilder");
3
+
4
+ const defaultOptions = {
5
+ preserveOrder: false,
6
+ removeNSPrefix: false, // remove NS from tag name or attribute name if true
7
+ //ignoreRootElement : false,
8
+ stopNodes: [], //nested tags will not be parsed even for errors
9
+ // isArray: () => false, //User will set it
10
+ htmlEntities: false,
11
+ // skipEmptyListItem: false
12
+ tags:{
13
+ unpaired: [],
14
+ nameFor:{
15
+ cdata: false,
16
+ comment: false,
17
+ text: '#text'
18
+ },
19
+ separateTextProperty: false,
20
+ },
21
+ attributes:{
22
+ ignore: false,
23
+ booleanType: true,
24
+ entities: true,
25
+ },
26
+
27
+ // select: ["img[src]"],
28
+ // stop: ["anim", "[ads]"]
29
+ only: [], // rest tags will be skipped. It will result in flat array
30
+ hierarchy: false, //will be used when a particular tag is set to be parsed.
31
+ skip: [], // will be skipped from parse result. on('skip') will be triggered
32
+
33
+ select: [], // on('select', tag => tag ) will be called if match
34
+ stop: [], //given tagPath will not be parsed. innerXML will be set as string value
35
+ OutputBuilder: new JsObjOutputBuilder(),
36
+ };
37
+
38
+ const buildOptions = function(options) {
39
+ const finalOptions = { ... defaultOptions};
40
+ copyProperties(finalOptions,options)
41
+ return finalOptions;
42
+ };
43
+
44
+ function copyProperties(target, source) {
45
+ for (let key in source) {
46
+ if (source.hasOwnProperty(key)) {
47
+ if (key === 'OutputBuilder') {
48
+ target[key] = source[key];
49
+ }else if (typeof source[key] === 'object' && !Array.isArray(source[key])) {
50
+ // Recursively copy nested properties
51
+ if (typeof target[key] === 'undefined') {
52
+ target[key] = {};
53
+ }
54
+ copyProperties(target[key], source[key]);
55
+ } else {
56
+ // Copy non-nested properties
57
+ target[key] = source[key];
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ exports.buildOptions = buildOptions;
64
+ exports.defaultOptions = defaultOptions;
@@ -0,0 +1,71 @@
1
+ class BaseOutputBuilder{
2
+ constructor(){
3
+ // this.attributes = {};
4
+ }
5
+
6
+ addAttribute(name, value){
7
+ if(this.options.onAttribute){
8
+ //TODO: better to pass tag path
9
+ const v = this.options.onAttribute(name, value, this.tagName);
10
+ if(v) this.attributes[v.name] = v.value;
11
+ }else{
12
+ name = this.options.attributes.prefix + name + this.options.attributes.suffix;
13
+ this.attributes[name] = this.parseValue(value, this.options.attributes.valueParsers);
14
+ }
15
+ }
16
+
17
+ /**
18
+ * parse value by chain of parsers
19
+ * @param {string} val
20
+ * @returns {any} parsed value if matching parser found
21
+ */
22
+ parseValue = function(val, valParsers){
23
+ for (let i = 0; i < valParsers.length; i++) {
24
+ let valParser = valParsers[i];
25
+ if(typeof valParser === "string"){
26
+ valParser = this.registeredParsers[valParser];
27
+ }
28
+ if(valParser){
29
+ val = valParser.parse(val);
30
+ }
31
+ }
32
+ return val;
33
+ }
34
+
35
+ /**
36
+ * To add a nested empty tag.
37
+ * @param {string} key
38
+ * @param {any} val
39
+ */
40
+ _addChild(key, val){}
41
+
42
+ /**
43
+ * skip the comment if property is not set
44
+ */
45
+ addComment(text){
46
+ if(this.options.nameFor.comment)
47
+ this._addChild(this.options.nameFor.comment, text);
48
+ }
49
+
50
+ //store CDATA separately if property is set
51
+ //otherwise add to tag's value
52
+ addCdata(text){
53
+ if (this.options.nameFor.cdata) {
54
+ this._addChild(this.options.nameFor.cdata, text);
55
+ } else {
56
+ this.addRawValue(text || "");
57
+ }
58
+ }
59
+
60
+ addRawValue = text => this.addValue(text);
61
+
62
+ addDeclaration(){
63
+ if(!this.options.declaration){
64
+ }else{
65
+ this.addPi("?xml");
66
+ }
67
+ this.attributes = {}
68
+ }
69
+ }
70
+
71
+ module.exports = BaseOutputBuilder;
@@ -0,0 +1,103 @@
1
+ const {buildOptions,registerCommonValueParsers} = require("./ParserOptionsBuilder");
2
+
3
+ class OutputBuilder{
4
+ constructor(options){
5
+ this.options = buildOptions(options);
6
+ this.registeredParsers = registerCommonValueParsers();
7
+ }
8
+
9
+ registerValueParser(name,parserInstance){//existing name will override the parser without warning
10
+ this.registeredParsers[name] = parserInstance;
11
+ }
12
+
13
+ getInstance(parserOptions){
14
+ return new JsArrBuilder(parserOptions, this.options, this.registeredParsers);
15
+ }
16
+ }
17
+
18
+ const rootName = '!js_arr';
19
+ const BaseOutputBuilder = require("./BaseOutputBuilder");
20
+
21
+ class JsArrBuilder extends BaseOutputBuilder{
22
+
23
+ constructor(parserOptions, options,registeredParsers) {
24
+ super();
25
+ this.tagsStack = [];
26
+ this.parserOptions = parserOptions;
27
+ this.options = options;
28
+ this.registeredParsers = registeredParsers;
29
+
30
+ this.root = new Node(rootName);
31
+ this.currentNode = this.root;
32
+ this.attributes = {};
33
+ }
34
+
35
+ addTag(tag){
36
+ //when a new tag is added, it should be added as child of current node
37
+ //TODO: shift this check to the parser
38
+ if(tag.name === "__proto__") tag.name = "#__proto__";
39
+
40
+ this.tagsStack.push(this.currentNode);
41
+ this.currentNode = new Node(tag.name, this.attributes);
42
+ this.attributes = {};
43
+ }
44
+
45
+ /**
46
+ * Check if the node should be added by checking user's preference
47
+ * @param {Node} node
48
+ * @returns boolean: true if the node should not be added
49
+ */
50
+ closeTag(){
51
+ const node = this.currentNode;
52
+ this.currentNode = this.tagsStack.pop(); //set parent node in scope
53
+ if(this.options.onClose !== undefined){
54
+ //TODO TagPathMatcher
55
+ const resultTag = this.options.onClose(node,
56
+ new TagPathMatcher(this.tagsStack,node));
57
+
58
+ if(resultTag) return;
59
+ }
60
+ this.currentNode.child.push(node); //to parent node
61
+ }
62
+
63
+ //Called by parent class methods
64
+ _addChild(key, val){
65
+ // if(key === "__proto__") tagName = "#__proto__";
66
+ this.currentNode.child.push( {[key]: val });
67
+ // this.currentNode.leafType = false;
68
+ }
69
+
70
+ /**
71
+ * Add text value child node
72
+ * @param {string} text
73
+ */
74
+ addValue(text){
75
+ this.currentNode.child.push( {[this.options.nameFor.text]: this.parseValue(text, this.options.tags.valueParsers) });
76
+ }
77
+
78
+ addPi(name){
79
+ //TODO: set pi flag
80
+ if(!this.options.ignorePiTags){
81
+ const node = new Node(name, this.attributes);
82
+ this.currentNode[":@"] = this.attributes;
83
+ this.currentNode.child.push(node);
84
+ }
85
+ this.attributes = {};
86
+ }
87
+ getOutput(){
88
+ return this.root.child[0];
89
+ }
90
+ }
91
+
92
+
93
+
94
+ class Node{
95
+ constructor(tagname, attributes){
96
+ this.tagname = tagname;
97
+ this.child = []; //nested tags, text, cdata, comments
98
+ if(attributes && Object.keys(attributes).length > 0)
99
+ this[":@"] = attributes;
100
+ }
101
+ }
102
+
103
+ module.exports = OutputBuilder;
@@ -0,0 +1,102 @@
1
+ const {buildOptions,registerCommonValueParsers} = require("./ParserOptionsBuilder");
2
+
3
+ class OutputBuilder{
4
+ constructor(options){
5
+ this.options = buildOptions(options);
6
+ this.registeredParsers = registerCommonValueParsers();
7
+ }
8
+
9
+ registerValueParser(name,parserInstance){//existing name will override the parser without warning
10
+ this.registeredParsers[name] = parserInstance;
11
+ }
12
+
13
+ getInstance(parserOptions){
14
+ return new JsMinArrBuilder(parserOptions, this.options, this.registeredParsers);
15
+ }
16
+ }
17
+
18
+ const BaseOutputBuilder = require("./BaseOutputBuilder");
19
+ const rootName = '^';
20
+
21
+ class JsMinArrBuilder extends BaseOutputBuilder{
22
+
23
+ constructor(parserOptions, options,registeredParsers) {
24
+ super();
25
+ this.tagsStack = [];
26
+ this.parserOptions = parserOptions;
27
+ this.options = options;
28
+ this.registeredParsers = registeredParsers;
29
+
30
+ this.root = {[rootName]: []};
31
+ this.currentNode = this.root;
32
+ this.currentNodeTagName = rootName;
33
+ this.attributes = {};
34
+ }
35
+
36
+ addTag(tag){
37
+ //when a new tag is added, it should be added as child of current node
38
+ //TODO: shift this check to the parser
39
+ if(tag.name === "__proto__") tag.name = "#__proto__";
40
+
41
+ this.tagsStack.push([this.currentNodeTagName,this.currentNode]); //this.currentNode is parent node here
42
+ this.currentNodeTagName = tag.name;
43
+ this.currentNode = { [tag.name]:[]}
44
+ if(Object.keys(this.attributes).length > 0){
45
+ this.currentNode[":@"] = this.attributes;
46
+ this.attributes = {};
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Check if the node should be added by checking user's preference
52
+ * @param {Node} node
53
+ * @returns boolean: true if the node should not be added
54
+ */
55
+ closeTag(){
56
+ const node = this.currentNode;
57
+ const nodeName = this.currentNodeTagName;
58
+ const arr = this.tagsStack.pop(); //set parent node in scope
59
+ this.currentNodeTagName = arr[0];
60
+ this.currentNode = arr[1];
61
+
62
+ if(this.options.onClose !== undefined){
63
+ //TODO TagPathMatcher
64
+ const resultTag = this.options.onClose(node,
65
+ new TagPathMatcher(this.tagsStack,node));
66
+
67
+ if(resultTag) return;
68
+ }
69
+ this.currentNode[this.currentNodeTagName].push(node); //to parent node
70
+ }
71
+
72
+ //Called by parent class methods
73
+ _addChild(key, val){
74
+ // if(key === "__proto__") tagName = "#__proto__";
75
+ this.currentNode.push( {[key]: val });
76
+ // this.currentNode.leafType = false;
77
+ }
78
+
79
+ /**
80
+ * Add text value child node
81
+ * @param {string} text
82
+ */
83
+ addValue(text){
84
+ this.currentNode[this.currentNodeTagName].push( {[this.options.nameFor.text]: this.parseValue(text, this.options.tags.valueParsers) });
85
+ }
86
+
87
+ addPi(name){
88
+ if(!this.options.ignorePiTags){
89
+ const node = { [name]:[]}
90
+ if(this.attributes){
91
+ node[":@"] = this.attributes;
92
+ }
93
+ this.currentNode.push(node);
94
+ }
95
+ this.attributes = {};
96
+ }
97
+ getOutput(){
98
+ return this.root[rootName];
99
+ }
100
+ }
101
+
102
+ module.exports = OutputBuilder;
@@ -0,0 +1,156 @@
1
+
2
+
3
+ const {buildOptions,registerCommonValueParsers} = require("./ParserOptionsBuilder");
4
+
5
+ class OutputBuilder{
6
+ constructor(builderOptions){
7
+ this.options = buildOptions(builderOptions);
8
+ this.registeredParsers = registerCommonValueParsers();
9
+ }
10
+
11
+ registerValueParser(name,parserInstance){//existing name will override the parser without warning
12
+ this.registeredParsers[name] = parserInstance;
13
+ }
14
+
15
+ getInstance(parserOptions){
16
+ return new JsObjBuilder(parserOptions, this.options, this.registeredParsers);
17
+ }
18
+ }
19
+
20
+ const BaseOutputBuilder = require("./BaseOutputBuilder");
21
+ const rootName = '^';
22
+
23
+ class JsObjBuilder extends BaseOutputBuilder{
24
+
25
+ constructor(parserOptions, builderOptions,registeredParsers) {
26
+ super();
27
+ //hold the raw detail of a tag and sequence with reference to the output
28
+ this.tagsStack = [];
29
+ this.parserOptions = parserOptions;
30
+ this.options = builderOptions;
31
+ this.registeredParsers = registeredParsers;
32
+
33
+ this.root = {};
34
+ this.parent = this.root;
35
+ this.tagName = rootName;
36
+ this.value = {};
37
+ this.textValue = "";
38
+ this.attributes = {};
39
+ }
40
+
41
+ addTag(tag){
42
+
43
+ let value = "";
44
+ if( !isEmpty(this.attributes)){
45
+ value = {};
46
+ if(this.options.attributes.groupBy){
47
+ value[this.options.attributes.groupBy] = this.attributes;
48
+ }else{
49
+ value = this.attributes;
50
+ }
51
+ }
52
+
53
+ this.tagsStack.push([this.tagName, this.textValue, this.value]); //parent tag, parent text value, parent tag value (jsobj)
54
+ this.tagName = tag.name;
55
+ this.value = value;
56
+ this.textValue = "";
57
+ this.attributes = {};
58
+ }
59
+
60
+ /**
61
+ * Check if the node should be added by checking user's preference
62
+ * @param {Node} node
63
+ * @returns boolean: true if the node should not be added
64
+ */
65
+ closeTag(){
66
+ const tagName = this.tagName;
67
+ let value = this.value;
68
+ let textValue = this.textValue;
69
+
70
+ //update tag text value
71
+ if(typeof value !== "object" && !Array.isArray(value)){
72
+ value = this.parseValue(textValue.trim(), this.options.tags.valueParsers);
73
+ }else if(textValue.length > 0){
74
+ value[this.options.nameFor.text] = this.parseValue(textValue.trim(), this.options.tags.valueParsers);
75
+ }
76
+
77
+
78
+ let resultTag= {
79
+ tagName: tagName,
80
+ value: value
81
+ };
82
+
83
+ if(this.options.onTagClose !== undefined){
84
+ //TODO TagPathMatcher
85
+ resultTag = this.options.onClose(tagName, value, this.textValue, new TagPathMatcher(this.tagsStack,node));
86
+
87
+ if(!resultTag) return;
88
+ }
89
+
90
+ //set parent node in scope
91
+ let arr = this.tagsStack.pop();
92
+ let parentTag = arr[2];
93
+ parentTag=this._addChildTo(resultTag.tagName, resultTag.value, parentTag);
94
+
95
+ this.tagName = arr[0];
96
+ this.textValue = arr[1];
97
+ this.value = parentTag;
98
+ }
99
+
100
+ _addChild(key, val){
101
+ if(typeof this.value === "string"){
102
+ this.value = { [this.options.nameFor.text] : this.value };
103
+ }
104
+
105
+ this._addChildTo(key, val, this.value);
106
+ // this.currentNode.leafType = false;
107
+ this.attributes = {};
108
+ }
109
+
110
+ _addChildTo(key, val, node){
111
+ if(typeof node === 'string') node = {};
112
+ if(!node[key]){
113
+ node[key] = val;
114
+ }else{ //Repeated
115
+ if(!Array.isArray(node[key])){ //but not stored as array
116
+ node[key] = [node[key]];
117
+ }
118
+ node[key].push(val);
119
+ }
120
+ return node;
121
+ }
122
+
123
+
124
+ /**
125
+ * Add text value child node
126
+ * @param {string} text
127
+ */
128
+ addValue(text){
129
+ //TODO: use bytes join
130
+ if(this.textValue.length > 0) this.textValue += " " + text;
131
+ else this.textValue = text;
132
+ }
133
+
134
+ addPi(name){
135
+ let value = "";
136
+ if( !isEmpty(this.attributes)){
137
+ value = {};
138
+ if(this.options.attributes.groupBy){
139
+ value[this.options.attributes.groupBy] = this.attributes;
140
+ }else{
141
+ value = this.attributes;
142
+ }
143
+ }
144
+ this._addChild(name, value);
145
+
146
+ }
147
+ getOutput(){
148
+ return this.value;
149
+ }
150
+ }
151
+
152
+ function isEmpty(obj) {
153
+ return Object.keys(obj).length === 0;
154
+ }
155
+
156
+ module.exports = OutputBuilder;