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

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,84 @@
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
+ processEntities: boolean;
22
+ htmlEntities: boolean;
23
+ };
24
+ type strnumOptions = {
25
+ hex: boolean;
26
+ leadingZeros: boolean,
27
+ skipLike?: RegExp
28
+ }
29
+ type X2jOptionsOptional = Partial<X2jOptions>;
30
+ type validationOptions = {
31
+ allowBooleanAttributes: boolean;
32
+ unpairedTags: string[];
33
+ };
34
+ type validationOptionsOptional = Partial<validationOptions>;
35
+
36
+ type XmlBuilderOptions = {
37
+ attributeNamePrefix: string;
38
+ attributesGroupName: false | string;
39
+ textNodeName: string;
40
+ ignoreAttributes: boolean;
41
+ cdataPropName: false | string;
42
+ commentPropName: false | string;
43
+ format: boolean;
44
+ indentBy: string;
45
+ arrayNodeName: string;
46
+ suppressEmptyNode: boolean;
47
+ preserveOrder: boolean;
48
+ unpairedTags: string[];
49
+ stopNodes: string[];
50
+ tagValueProcessor: (name: string, value: string) => string;
51
+ attributeValueProcessor: (name: string, value: string) => string;
52
+ processEntities: boolean;
53
+ };
54
+ type XmlBuilderOptionsOptional = Partial<XmlBuilderOptions>;
55
+
56
+ type ESchema = string | object | Array<string|object>;
57
+
58
+ type ValidationError = {
59
+ err: {
60
+ code: string;
61
+ msg: string,
62
+ line: number,
63
+ col: number
64
+ };
65
+ };
66
+
67
+ export class XMLParser {
68
+ constructor(options?: X2jOptionsOptional);
69
+ parse(xmlData: string | Buffer ,validationOptions?: validationOptionsOptional | boolean): any;
70
+ /**
71
+ * Add Entity which is not by default supported by this library
72
+ * @param entityIndentifier {string} Eg: 'ent' for &ent;
73
+ * @param entityValue {string} Eg: '\r'
74
+ */
75
+ addEntity(entityIndentifier: string, entityValue: string): void;
76
+ }
77
+
78
+ export class XMLValidator{
79
+ static validate( xmlData: string, options?: validationOptionsOptional): true | ValidationError;
80
+ }
81
+ export class XMLBuilder {
82
+ constructor(options: XmlBuilderOptionsOptional);
83
+ build(jObj: any): any;
84
+ }
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
@@ -67,7 +67,7 @@ exports.getValue = function(v) {
67
67
  // const fakeCall = function(a) {return a;};
68
68
  // const fakeCallNoReturn = function() {};
69
69
 
70
- exports.buildOptions = function(options, defaultOptions, props) {
70
+ const buildOptions = function(options, defaultOptions, props) {
71
71
  let newOptions = {};
72
72
  if (!options) {
73
73
  return defaultOptions; //if there are not options
@@ -83,26 +83,7 @@ exports.buildOptions = function(options, defaultOptions, props) {
83
83
  return newOptions;
84
84
  };
85
85
 
86
- /**
87
- * Check if a tag name should be treated as array
88
- *
89
- * @param tagName the node tagname
90
- * @param arrayMode the array mode option
91
- * @param parentTagName the parent tag name
92
- * @returns {boolean} true if node should be parsed as array
93
- */
94
- exports.isTagNameInArrayMode = function (tagName, arrayMode, parentTagName) {
95
- if (arrayMode === false) {
96
- return false;
97
- } else if (arrayMode instanceof RegExp) {
98
- return arrayMode.test(tagName);
99
- } else if (typeof arrayMode === 'function') {
100
- return !!arrayMode(tagName, parentTagName);
101
- }
102
-
103
- return arrayMode === "strict";
104
- }
105
-
86
+ exports.buildOptions = buildOptions;
106
87
  exports.isName = isName;
107
88
  exports.getAllMatches = getAllMatches;
108
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] === '?') {
@@ -130,6 +134,8 @@ exports.validate = function (xmlData, options) {
130
134
  //if the root level has been reached before ...
131
135
  if (reachedRoot === true) {
132
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
133
139
  } else {
134
140
  tags.push({tagName, tagStartPos});
135
141
  }
@@ -156,6 +162,10 @@ exports.validate = function (xmlData, options) {
156
162
  if (afterAmp == -1)
157
163
  return getErrorObject('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
158
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
+ }
159
169
  }
160
170
  } //end of reading tag text value
161
171
  if (xmlData[i] === '<') {
@@ -163,7 +173,7 @@ exports.validate = function (xmlData, options) {
163
173
  }
164
174
  }
165
175
  } else {
166
- if (xmlData[i] === ' ' || xmlData[i] === '\t' || xmlData[i] === '\n' || xmlData[i] === '\r') {
176
+ if ( isWhiteSpace(xmlData[i])) {
167
177
  continue;
168
178
  }
169
179
  return getErrorObject('InvalidChar', "char '"+xmlData[i]+"' is not expected.", getLineNumberForPosition(xmlData, i));
@@ -183,6 +193,9 @@ exports.validate = function (xmlData, options) {
183
193
  return true;
184
194
  };
185
195
 
196
+ function isWhiteSpace(char){
197
+ return char === ' ' || char === '\t' || char === '\n' || char === '\r';
198
+ }
186
199
  /**
187
200
  * Read Processing insstructions and skip
188
201
  * @param {*} xmlData
@@ -1,43 +1,60 @@
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
- cdataTagName: false,
11
- cdataPositionChar: '\\c',
11
+ cdataPropName: false,
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,
22
+ commentPropName: false,
23
+ unpairedTags: [],
24
+ entities: {
25
+ ">" : { regex: new RegExp(">", "g"), val: "&gt;" },
26
+ "<" : { regex: new RegExp("<", "g"), val: "&lt;" },
27
+ "sQuot" : { regex: new RegExp("\'", "g"), val: "&apos;" },
28
+ "dQuot" : { regex: new RegExp("\"", "g"), val: "&quot;" }
29
+ },
30
+ processEntities: true,
31
+ stopNodes: []
21
32
  };
22
33
 
23
34
  const props = [
24
35
  'attributeNamePrefix',
25
- 'attrNodeName',
36
+ 'attributesGroupName',
26
37
  'textNodeName',
27
38
  'ignoreAttributes',
28
- 'cdataTagName',
29
- 'cdataPositionChar',
39
+ 'cdataPropName',
30
40
  'format',
31
41
  'indentBy',
32
- 'supressEmptyNode',
42
+ 'suppressEmptyNode',
33
43
  'tagValueProcessor',
34
- 'attrValueProcessor',
35
- 'rootNodeName', //when array as root
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
36
53
  ];
37
54
 
38
- function Parser(options) {
55
+ function Builder(options) {
39
56
  this.options = buildOptions(options, defaultOptions, props);
40
- if (this.options.ignoreAttributes || this.options.attrNodeName) {
57
+ if (this.options.ignoreAttributes || this.options.attributesGroupName) {
41
58
  this.isAttribute = function(/*a*/) {
42
59
  return false;
43
60
  };
@@ -45,15 +62,6 @@ function Parser(options) {
45
62
  this.attrPrefixLen = this.options.attributeNamePrefix.length;
46
63
  this.isAttribute = isAttribute;
47
64
  }
48
- if (this.options.cdataTagName) {
49
- this.isCDATA = isCDATA;
50
- } else {
51
- this.isCDATA = function(/*a*/) {
52
- return false;
53
- };
54
- }
55
- this.replaceCDATAstr = replaceCDATAstr;
56
- this.replaceCDATAarr = replaceCDATAarr;
57
65
 
58
66
  this.processTextOrObjNode = processTextOrObjNode
59
67
 
@@ -69,7 +77,7 @@ function Parser(options) {
69
77
  this.newLine = '';
70
78
  }
71
79
 
72
- if (this.options.supressEmptyNode) {
80
+ if (this.options.suppressEmptyNode) {
73
81
  this.buildTextNode = buildEmptyTextNode;
74
82
  this.buildObjNode = buildEmptyObjNode;
75
83
  } else {
@@ -79,18 +87,24 @@ function Parser(options) {
79
87
 
80
88
  this.buildTextValNode = buildTextValNode;
81
89
  this.buildObjectNode = buildObjectNode;
90
+
91
+ this.replaceEntitiesValue = replaceEntitiesValue;
82
92
  }
83
93
 
84
- Parser.prototype.parse = function(jObj) {
85
- if(Array.isArray(jObj) && this.options.rootNodeName && this.options.rootNodeName.length > 1){
86
- jObj = {
87
- [this.options.rootNodeName] : jObj
94
+ Builder.prototype.build = function(jObj) {
95
+ if(this.options.preserveOrder){
96
+ return buildFromOrderedJs(jObj, this.options);
97
+ }else {
98
+ if(Array.isArray(jObj) && this.options.arrayNodeName && this.options.arrayNodeName.length > 1){
99
+ jObj = {
100
+ [this.options.arrayNodeName] : jObj
101
+ }
88
102
  }
103
+ return this.j2x(jObj, 0).val;
89
104
  }
90
- return this.j2x(jObj, 0).val;
91
105
  };
92
106
 
93
- Parser.prototype.j2x = function(jObj, level) {
107
+ Builder.prototype.j2x = function(jObj, level) {
94
108
  let attrStr = '';
95
109
  let val = '';
96
110
  for (let key in jObj) {
@@ -104,57 +118,42 @@ Parser.prototype.j2x = function(jObj, level) {
104
118
  //premitive type
105
119
  const attr = this.isAttribute(key);
106
120
  if (attr) {
107
- attrStr += ' ' + attr + '="' + this.options.attrValueProcessor('' + jObj[key]) + '"';
108
- } else if (this.isCDATA(key)) {
109
- if (jObj[this.options.textNodeName]) {
110
- val += this.replaceCDATAstr(jObj[this.options.textNodeName], jObj[key]);
111
- } else {
112
- val += this.replaceCDATAstr('', jObj[key]);
113
- }
114
- } else {
121
+ let val = this.options.attributeValueProcessor(attr, '' + jObj[key]);
122
+ val = this.replaceEntitiesValue(val);
123
+ attrStr += ' ' + attr + '="' + val + '"';
124
+ }else {
115
125
  //tag value
116
126
  if (key === this.options.textNodeName) {
117
- if (jObj[this.options.cdataTagName]) {
118
- //value will added while processing cdata
119
- } else {
120
- val += this.options.tagValueProcessor('' + jObj[key]);
121
- }
127
+ let newval = this.options.tagValueProcessor(key, '' + jObj[key]);
128
+ val += this.replaceEntitiesValue(newval);
122
129
  } else {
123
130
  val += this.buildTextNode(jObj[key], key, '', level);
124
131
  }
125
132
  }
126
133
  } else if (Array.isArray(jObj[key])) {
127
134
  //repeated nodes
128
- if (this.isCDATA(key)) {
129
- val += this.indentate(level);
130
- if (jObj[this.options.textNodeName]) {
131
- val += this.replaceCDATAarr(jObj[this.options.textNodeName], jObj[key]);
135
+ const arrLen = jObj[key].length;
136
+ for (let j = 0; j < arrLen; j++) {
137
+ const item = jObj[key][j];
138
+ if (typeof item === 'undefined') {
139
+ // supress undefined node
140
+ } else if (item === null) {
141
+ val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
142
+ } else if (typeof item === 'object') {
143
+ val += this.processTextOrObjNode(item, key, level)
132
144
  } else {
133
- val += this.replaceCDATAarr('', jObj[key]);
134
- }
135
- } else {
136
- //nested nodes
137
- const arrLen = jObj[key].length;
138
- for (let j = 0; j < arrLen; j++) {
139
- const item = jObj[key][j];
140
- if (typeof item === 'undefined') {
141
- // supress undefined node
142
- } else if (item === null) {
143
- val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
144
- } else if (typeof item === 'object') {
145
- val += this.processTextOrObjNode(item, key, level)
146
- } else {
147
- val += this.buildTextNode(item, key, '', level);
148
- }
145
+ val += this.buildTextNode(item, key, '', level);
149
146
  }
150
147
  }
151
148
  } else {
152
149
  //nested node
153
- if (this.options.attrNodeName && key === this.options.attrNodeName) {
150
+ if (this.options.attributesGroupName && key === this.options.attributesGroupName) {
154
151
  const Ks = Object.keys(jObj[key]);
155
152
  const L = Ks.length;
156
153
  for (let j = 0; j < L; j++) {
157
- attrStr += ' ' + Ks[j] + '="' + this.options.attrValueProcessor('' + jObj[key][Ks[j]]) + '"';
154
+ let val = this.options.attributeValueProcessor(Ks[j], '' + jObj[key][Ks[j]]);
155
+ val = this.replaceEntitiesValue(val);
156
+ attrStr += ' ' + Ks[j] + '="' + val + '"';
158
157
  }
159
158
  } else {
160
159
  val += this.processTextOrObjNode(jObj[key], key, level)
@@ -173,27 +172,6 @@ function processTextOrObjNode (object, key, level) {
173
172
  }
174
173
  }
175
174
 
176
- function replaceCDATAstr(str, cdata) {
177
- str = this.options.tagValueProcessor('' + str);
178
- if (this.options.cdataPositionChar === '' || str === '') {
179
- return str + '<![CDATA[' + cdata + ']]' + this.tagEndChar;
180
- } else {
181
- return str.replace(this.options.cdataPositionChar, '<![CDATA[' + cdata + ']]' + this.tagEndChar);
182
- }
183
- }
184
-
185
- function replaceCDATAarr(str, cdata) {
186
- str = this.options.tagValueProcessor('' + str);
187
- if (this.options.cdataPositionChar === '' || str === '') {
188
- return str + '<![CDATA[' + cdata.join(']]><![CDATA[') + ']]' + this.tagEndChar;
189
- } else {
190
- for (let v in cdata) {
191
- str = str.replace(this.options.cdataPositionChar, '<![CDATA[' + cdata[v] + ']]>');
192
- }
193
- return str + this.newLine;
194
- }
195
- }
196
-
197
175
  function buildObjectNode(val, key, attrStr, level) {
198
176
  if (attrStr && val.indexOf('<') === -1) {
199
177
  return (
@@ -236,21 +214,36 @@ function buildEmptyObjNode(val, key, attrStr, level) {
236
214
  }
237
215
 
238
216
  function buildTextValNode(val, key, attrStr, level) {
217
+ let textValue = this.options.tagValueProcessor(key, val);
218
+ textValue = this.replaceEntitiesValue(textValue);
219
+
239
220
  return (
240
221
  this.indentate(level) +
241
222
  '<' +
242
223
  key +
243
224
  attrStr +
244
225
  '>' +
245
- this.options.tagValueProcessor(val) +
226
+ textValue +
246
227
  '</' +
247
228
  key +
248
229
  this.tagEndChar
249
230
  );
250
231
  }
251
232
 
233
+ function replaceEntitiesValue(textValue){
234
+ if(textValue && textValue.length > 0 && this.options.processEntities){
235
+ for (const entityName in this.options.entities) {
236
+ const entity = this.options.entities[entityName];
237
+ textValue = textValue.replace(entity.regex, entity.val);
238
+ }
239
+ }
240
+ return textValue;
241
+ }
242
+
252
243
  function buildEmptyTextNode(val, key, attrStr, level) {
253
- if (val !== '') {
244
+ if( val === '' && this.options.unpairedTags.indexOf(key) !== -1){
245
+ return this.indentate(level) + '<' + key + attrStr + this.tagEndChar;
246
+ }else if (val !== '') {
254
247
  return this.buildTextValNode(val, key, attrStr, level);
255
248
  } else {
256
249
  return this.indentate(level) + '<' + key + attrStr + '/' + this.tagEndChar;
@@ -269,12 +262,4 @@ function isAttribute(name /*, options*/) {
269
262
  }
270
263
  }
271
264
 
272
- function isCDATA(name) {
273
- return name === this.options.cdataTagName;
274
- }
275
-
276
- //formatting
277
- //indentation
278
- //\n after each closing or self closing tag
279
-
280
- module.exports = Parser;
265
+ module.exports = Builder;
@@ -0,0 +1,99 @@
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, jPath, 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
+ let newJPath = "";
25
+ if(jPath.length === 0) newJPath = tagName
26
+ else newJPath = `${jPath}.${tagName}`;
27
+
28
+ if(tagName === options.textNodeName){
29
+ let tagText = tagObj[tagName];
30
+ if(!isStopNode(newJPath, options)){
31
+ tagText = options.tagValueProcessor( tagName, tagText);
32
+ tagText = replaceEntitiesValue(tagText, options);
33
+ }
34
+ xmlStr += indentation + tagText;
35
+ continue;
36
+ }else if( tagName === options.cdataPropName){
37
+ xmlStr += indentation + `<![CDATA[${tagObj[tagName][0][options.textNodeName]}]]>`;
38
+ continue;
39
+ }else if( tagName === options.commentPropName){
40
+ xmlStr += indentation + `<!--${tagObj[tagName][0][options.textNodeName]}-->`;
41
+ continue;
42
+ }
43
+ const attStr = attr_to_str(tagObj.attributes, options);
44
+ let tagStart = indentation + `<${tagName}${attStr}`;
45
+ 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
+ }
52
+ }else{
53
+ //TODO: node with only text value should not parse the text value in next line
54
+ xmlStr += tagStart + `>${tagValue}${indentation}</${tagName}>` ;
55
+ }
56
+ }
57
+
58
+ return xmlStr;
59
+ }
60
+
61
+ function propName(obj){
62
+ const keys = Object.keys(obj);
63
+ for (let i = 0; i < keys.length; i++) {
64
+ const key = keys[i];
65
+ if(key !== "attributes") return key;
66
+ }
67
+ }
68
+
69
+ function attr_to_str(attrMap, options){
70
+ let attrStr = "";
71
+ if(attrMap && !options.ignoreAttributes){
72
+ for( attr in attrMap){
73
+ let attrVal = options.attributeValueProcessor(attr, attrMap[attr]);
74
+ attrVal = replaceEntitiesValue(attrVal, options);
75
+ attrStr+= ` ${attr.substr(options.attributeNamePrefix.length)}="${attrVal}"`;
76
+ }
77
+ }
78
+ return attrStr;
79
+ }
80
+
81
+ function isStopNode(jPath, options){
82
+ jPath = jPath.substr(0,jPath.length - options.textNodeName.length - 1);
83
+ let tagName = jPath.substr(jPath.lastIndexOf(".") + 1);
84
+ for(let index in options.stopNodes){
85
+ if(options.stopNodes[index] === jPath || options.stopNodes[index] === "*."+tagName) return true;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ function replaceEntitiesValue(textValue, options){
91
+ if(textValue && textValue.length > 0 && options.processEntities){
92
+ for (const entityName in options.entities) {
93
+ const entity = options.entities[entityName];
94
+ textValue = textValue.replace(entity.regex, entity.val);
95
+ }
96
+ }
97
+ return textValue;
98
+ }
99
+ module.exports = toXml;
File without changes
@@ -0,0 +1,92 @@
1
+ //TODO: handle comments
2
+ function readDocType(xmlData, i){
3
+
4
+ const entities = {};
5
+ if( xmlData[i + 3] === 'O' &&
6
+ xmlData[i + 4] === 'C' &&
7
+ xmlData[i + 5] === 'T' &&
8
+ xmlData[i + 6] === 'Y' &&
9
+ xmlData[i + 7] === 'P' &&
10
+ xmlData[i + 8] === 'E')
11
+ {
12
+ i = i+9;
13
+ let angleBracketsCount = 1;
14
+ let hasBody = false, entity = false, comment = false;
15
+ let exp = "";
16
+ for(;i<xmlData.length;i++){
17
+ if (xmlData[i] === '<') {
18
+ if( hasBody &&
19
+ xmlData[i+1] === '!' &&
20
+ xmlData[i+2] === 'E' &&
21
+ xmlData[i+3] === 'N' &&
22
+ xmlData[i+4] === 'T' &&
23
+ xmlData[i+5] === 'I' &&
24
+ xmlData[i+6] === 'T' &&
25
+ xmlData[i+7] === 'Y'
26
+ ){
27
+ i += 7;
28
+ entity = true;
29
+ }else if( hasBody &&
30
+ xmlData[i+1] === '!' &&
31
+ xmlData[i+2] === 'E' &&
32
+ xmlData[i+3] === 'L' &&
33
+ xmlData[i+4] === 'E' &&
34
+ xmlData[i+5] === 'M' &&
35
+ xmlData[i+6] === 'E' &&
36
+ xmlData[i+7] === 'N' &&
37
+ xmlData[i+8] === 'T'
38
+ ){
39
+ //Not supported
40
+ i += 8;
41
+ }else if( //comment
42
+ xmlData[i+1] === '!' &&
43
+ xmlData[i+2] === '-' &&
44
+ xmlData[i+3] === '-'
45
+ ){
46
+ comment = true;
47
+ }else{
48
+ throw new Error("Invalid DOCTYPE");
49
+ }
50
+ angleBracketsCount++;
51
+ exp = "";
52
+ } else if (xmlData[i] === '>') {
53
+ if(comment){
54
+ if( xmlData[i - 1] === "-" && xmlData[i - 2] === "-"){
55
+ comment = false;
56
+ }else{
57
+ throw new Error(`Invalid XML comment in DOCTYPE`);
58
+ }
59
+ }else if(entity){
60
+ parseEntityExp(exp, entities);
61
+ entity = false;
62
+ }
63
+ angleBracketsCount--;
64
+ if (angleBracketsCount === 0) {
65
+ break;
66
+ }
67
+ }else if( xmlData[i] === '['){
68
+ hasBody = true;
69
+ }else{
70
+ exp += xmlData[i];
71
+ }
72
+ }
73
+ if(angleBracketsCount !== 0){
74
+ throw new Error(`Unclosed DOCTYPE`);
75
+ }
76
+ }else{
77
+ throw new Error(`Invalid Tag instead of DOCTYPE`);
78
+ }
79
+ return {entities, i};
80
+ }
81
+
82
+ const entityRegex = RegExp("^\\s([a-zA-z0-0]+)[ \t](['\"])([^&]+)\\2");
83
+ function parseEntityExp(exp, entities){
84
+ const match = entityRegex.exec(exp);
85
+ if(match){
86
+ entities[ match[1] ] = {
87
+ regx : RegExp( `&${match[1]};`,"g"),
88
+ val: match[3]
89
+ };
90
+ }
91
+ }
92
+ module.exports = readDocType;