1 | const ARRAY = 'array';
|
2 | const BOOLEAN = 'boolean';
|
3 | const DATE = 'date';
|
4 | const NULL = 'null';
|
5 | const NUMBER = 'number';
|
6 | const OBJECT = 'object';
|
7 | const SPECIAL_OBJECT = 'special-object';
|
8 | const STRING = 'string';
|
9 |
|
10 | const PRIVATE_VARS = ['_selfCloseTag', '_attrs'];
|
11 | const PRIVATE_VARS_REGEXP = new RegExp(PRIVATE_VARS.join('|'), 'g');
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | const getIndentStr = (indent = '', depth = 0) => indent.repeat(depth);
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | const getType = val =>
|
26 | Array.isArray(val) && ARRAY ||
|
27 | (typeof val === OBJECT && val !== null && val._name && SPECIAL_OBJECT) ||
|
28 | (val instanceof Date && DATE) ||
|
29 | val === null && NULL ||
|
30 | typeof val;
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 | const filterStr = (inputStr = '', filter = {}) => {
|
39 | const regexp = new RegExp(`(${ Object.keys(filter).join('|') })`, 'g');
|
40 |
|
41 | return String(inputStr).replace(regexp, (str, entity) => filter[entity] || '');
|
42 | };
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 | const getAttributeKeyVals = (attributes = {}, filter) => {
|
51 | const isArray = Array.isArray(attributes);
|
52 |
|
53 | let keyVals = [];
|
54 | if (isArray) {
|
55 |
|
56 | keyVals = attributes.map(attr => {
|
57 | const key = Object.keys(attr)[0];
|
58 | const val = attr[key];
|
59 |
|
60 | const filteredVal = (filter) ? filterStr(val, filter) : val;
|
61 | const valStr = (filteredVal === true) ? '' : `="${filteredVal}"`;
|
62 | return `${key}${valStr}`;
|
63 | });
|
64 | } else {
|
65 | const keys = Object.keys(attributes);
|
66 | keyVals = keys.map(key => {
|
67 |
|
68 |
|
69 |
|
70 | const filteredVal = (filter) ? filterStr(attributes[key], filter) : attributes[key];
|
71 | const valStr = (attributes[key] === true) ? '' : `="${filteredVal}"`;
|
72 |
|
73 | return `${key}${valStr}`;
|
74 | });
|
75 | }
|
76 |
|
77 | return keyVals;
|
78 | };
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 | const formatAttributes = (attributes = {}, filter) => {
|
87 | const keyVals = getAttributeKeyVals(attributes, filter);
|
88 | if (keyVals.length === 0) return '';
|
89 |
|
90 | const keysValsJoined = keyVals.join(' ');
|
91 | return ` ${keysValsJoined}`;
|
92 | };
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 | const objToArray = (obj = {}) => (Object.keys(obj).map(key => {
|
111 | return {
|
112 | _name: key,
|
113 | _content: obj[key]
|
114 | };
|
115 | }));
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 | const PRIMITIVE_TYPES = [STRING, NUMBER, BOOLEAN];
|
124 | const isPrimitive = val => PRIMITIVE_TYPES.includes(getType(val));
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 | const SIMPLE_TYPES = [...PRIMITIVE_TYPES, DATE, SPECIAL_OBJECT];
|
134 | const isSimpleType = val => SIMPLE_TYPES.includes(getType(val));
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 | const isSimpleXML = xmlStr => !xmlStr.match('<');
|
142 |
|
143 |
|
144 |
|
145 |
|
146 | const DEFAULT_XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>';
|
147 | const getHeaderString = ({ header, indent, depth, isOutputStart }) => {
|
148 | const shouldOutputHeader = header && isOutputStart;
|
149 |
|
150 | if (!shouldOutputHeader) return '';
|
151 |
|
152 | const shouldUseDefaultHeader = typeof header === BOOLEAN;
|
153 | return `${ (shouldUseDefaultHeader) ? DEFAULT_XML_HEADER : header }${ indent ? '\n' : '' }`;
|
154 | };
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 | export const toXML = (
|
163 | obj = {},
|
164 | config = {}
|
165 | ) => {
|
166 | const {
|
167 |
|
168 | depth = 0,
|
169 | indent,
|
170 | _isFirstItem,
|
171 | _isLastItem,
|
172 | attributesFilter,
|
173 | header,
|
174 | filter
|
175 | } = config;
|
176 |
|
177 |
|
178 | const indentStr = getIndentStr(indent, depth);
|
179 |
|
180 |
|
181 | const valType = getType(obj);
|
182 | const isSimple = isSimpleType(obj);
|
183 |
|
184 |
|
185 | const isOutputStart = depth === 0 && (isSimple || (!isSimple && _isFirstItem));
|
186 |
|
187 | let outputStr = '';
|
188 | switch (valType) {
|
189 | case 'special-object': {
|
190 |
|
191 |
|
192 | const { _name, _content } = obj;
|
193 |
|
194 |
|
195 | if (_content === null) {
|
196 | outputStr = _name;
|
197 | break;
|
198 | }
|
199 |
|
200 |
|
201 | if (Array.isArray(_content) && _content.every(isPrimitive)) {
|
202 | return _content.map(a => {
|
203 | return toXML({
|
204 | _name,
|
205 | _content: a
|
206 | },
|
207 | {
|
208 | ...config,
|
209 | depth
|
210 | });
|
211 | }).join('');
|
212 | }
|
213 |
|
214 |
|
215 | if (_name.match(PRIVATE_VARS_REGEXP)) break;
|
216 |
|
217 |
|
218 | const newVal = toXML(_content, { ...config, depth: depth + 1 });
|
219 | const newValType = getType(newVal);
|
220 | const isNewValSimple = isSimpleXML(newVal);
|
221 |
|
222 |
|
223 | const preIndentStr = (indent && !isOutputStart) ? '\n' : '';
|
224 | const preTag = `${preIndentStr}${indentStr}`;
|
225 |
|
226 |
|
227 | const valIsEmpty = newValType === 'undefined' || newVal === '';
|
228 | const shouldSelfClose = (typeof obj._selfCloseTag === BOOLEAN) ?
|
229 | (valIsEmpty && obj._selfCloseTag) :
|
230 | valIsEmpty;
|
231 | const selfCloseStr = (shouldSelfClose) ? '/' : '';
|
232 | const attributesString = formatAttributes(obj._attrs, attributesFilter);
|
233 | const tag = `<${_name}${attributesString}${selfCloseStr}>`;
|
234 |
|
235 |
|
236 | const preTagCloseStr = (indent && !isNewValSimple) ? `\n${indentStr}` : '';
|
237 | const postTag = (!shouldSelfClose) ? `${newVal}${preTagCloseStr}</${_name}>` : '';
|
238 |
|
239 | outputStr = `${preTag}${tag}${postTag}`;
|
240 | break;
|
241 | }
|
242 |
|
243 | case 'object': {
|
244 |
|
245 |
|
246 | const keys = Object.keys(obj);
|
247 | const outputArr = keys.map((key, index) => {
|
248 | const newConfig = {
|
249 | ...config,
|
250 | _isFirstItem: index === 0,
|
251 | _isLastItem: ((index + 1) === keys.length)
|
252 | };
|
253 |
|
254 | const outputObj = { _name: key };
|
255 |
|
256 | if (getType(obj[key]) === 'object') {
|
257 |
|
258 |
|
259 |
|
260 |
|
261 | PRIVATE_VARS.forEach(privateVar => {
|
262 | const val = obj[key][privateVar];
|
263 | if (typeof val !== 'undefined') {
|
264 | outputObj[privateVar] = val;
|
265 | delete obj[key][privateVar];
|
266 | }
|
267 | });
|
268 |
|
269 | const hasContent = typeof obj[key]._content !== 'undefined';
|
270 | if (hasContent) {
|
271 |
|
272 |
|
273 | if (Object.keys(obj[key]).length > 1) {
|
274 | const newContentObj = Object.assign({}, obj[key]);
|
275 | delete newContentObj._content;
|
276 |
|
277 | outputObj._content = [
|
278 | ...objToArray(newContentObj),
|
279 | obj[key]._content
|
280 | ];
|
281 | }
|
282 | }
|
283 | }
|
284 |
|
285 |
|
286 | if (typeof outputObj._content === 'undefined') outputObj._content = obj[key];
|
287 |
|
288 | const xml = toXML(outputObj, newConfig, key);
|
289 |
|
290 | return xml;
|
291 | }, config);
|
292 |
|
293 | outputStr = outputArr.join('');
|
294 | break;
|
295 | }
|
296 |
|
297 | case 'function': {
|
298 |
|
299 |
|
300 | const fnResult = obj(config);
|
301 |
|
302 | outputStr = toXML(fnResult, config);
|
303 | break;
|
304 | }
|
305 |
|
306 | case 'array': {
|
307 |
|
308 |
|
309 | const outputArr = obj.map((singleVal, index) => {
|
310 | const newConfig = {
|
311 | ...config,
|
312 | _isFirstItem: index === 0,
|
313 | _isLastItem: ((index + 1) === obj.length)
|
314 | };
|
315 | return toXML(singleVal, newConfig);
|
316 | });
|
317 |
|
318 | outputStr = outputArr.join('');
|
319 |
|
320 | break;
|
321 | }
|
322 |
|
323 |
|
324 | default: {
|
325 | outputStr = filterStr(obj, filter);
|
326 | break;
|
327 | }
|
328 | }
|
329 |
|
330 | const headerStr = getHeaderString({ header, indent, depth, isOutputStart });
|
331 |
|
332 | return `${headerStr}${outputStr}`;
|
333 | };
|
334 |
|
335 | export default {
|
336 | toXML
|
337 | };
|
338 |
|