/* * Utilities: A classic collection of JavaScript utilities * Copyright 2112 Matthew Eernisse (mde@fleegix.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ var core = require('./core') , inflection = require('./inflection') /** @name xml @namespace xml */ exports.XML = new (function () { // Default indention level var indentLevel = 4 , tagFromType , obj2xml; tagFromType = function (item, prev) { var ret , type , types; if (item instanceof Array) { ret = 'array'; } else { types = ['string', 'number', 'boolean', 'object']; for (var i = 0, ii = types.length; i < ii; i++) { type = types[i]; if (typeof item == type) { ret = type; } } } if (prev && ret != prev) { return 'record' } else { return ret; } }; obj2xml = function (o, opts) { var name = opts.name , level = opts.level , arrayRoot = opts.arrayRoot , pack , item , n , currentIndent = (new Array(level * indentLevel)).join(' ') , nextIndent = (new Array((level + 1) * indentLevel)).join(' ') , xml = ''; switch (typeof o) { case 'string': case 'number': case 'boolean': xml = o.toString(); break; case 'object': // Arrays if (o instanceof Array) { // Pack the processed version of each item into an array that // can be turned into a tag-list with a `join` method below // As the list gets iterated, if all items are the same type, // that's the tag-name for the individual tags. If the items are // a mixed, the tag-name is 'record' pack = []; for (var i = 0, ii = o.length; i < ii; i++) { item = o[i]; if (!name) { // Pass any previous tag-name, so it's possible to know if // all items are the same type, or it's mixed types n = tagFromType(item, n); } pack.push(obj2xml(item, { name: name , level: level + 1 , arrayRoot: arrayRoot })); } // If this thing is attached to a named property on an object, // use the name for the containing tag-name if (name) { n = name; } // If this is a top-level item, wrap in a top-level containing tag if (level == 0) { xml += currentIndent + '<' + inflection.pluralize(n) + ' type="array">\n' } xml += nextIndent + '<' + n + '>' + pack.join('\n' + nextIndent + '<' + n + '>') + '\n'; // If this is a top-level item, close the top-level containing tag if (level == 0) { xml += currentIndent + ''; } } // Generic objects else { n = name || 'object'; // If this is a top-level item, wrap in a top-level containing tag if (level == 0) { xml += currentIndent + '<' + n; // Lookahead hack to allow tags to have attributes for (var p in o) { if (p.indexOf('attr:') == 0) { xml += ' ' + p.replace(/^attr:/, '') + '="' + o[p] + '"' } } xml += '>\n'; } for (var p in o) { item = o[p]; // Data properties only if (typeof item == 'function') { continue; } // No attr hack properties if (p.indexOf('attr:') == 0) { continue; } xml += nextIndent; if (p == '#cdata') { xml += '\n'; } else { // Complex values, going to have items with multiple tags // inside if (typeof item == 'object') { if (item instanceof Array) { if (arrayRoot) { xml += '<' + p + ' type="array">\n' } } else { xml += '<' + p; // Lookahead hack to allow tags to have attributes for (var q in item) { if (q.indexOf('attr:') == 0) { xml += ' ' + q.replace(/^attr:/, '') + '="' + item[q] + '"' } } xml += '>\n'; } } // Scalars, just a value and closing tag else { xml += '<' + p + '>' } xml += obj2xml(item, { name: p , level: level + 1 , arrayRoot: arrayRoot }); // Objects and Arrays, need indentation before closing tag if (typeof item == 'object') { if (item instanceof Array) { if (arrayRoot) { xml += nextIndent; xml += '\n'; } } else { xml += nextIndent; xml += '\n'; } } // Scalars, just close else { xml += '\n'; } } } // If this is a top-level item, close the top-level containing tag if (level == 0) { xml += currentIndent + '\n'; } } break; default: // No default } return xml; } /* * XML configuration * */ this.config = { whitespace: true , name: null , fragment: false , level: 0 , arrayRoot: true }; /** @name xml#setIndentLevel @public @function @return {Number} Return the given `level` @description SetIndentLevel changes the indent level for XML.stringify and returns it @param {Number} level The indent level to use */ this.setIndentLevel = function (level) { if(!level) { return; } return indentLevel = level; }; /** @name xml#stringify @public @function @return {String} Return the XML entities of the given `obj` @description Stringify returns an XML representation of the given `obj` @param {Object} obj The object containing the XML entities to use @param {Object} opts @param {Boolean} [opts.whitespace=true] Don't insert indents and newlines after xml entities @param {String} [opts.name=typeof obj] Use custom name as global namespace @param {Boolean} [opts.fragment=false] If true no header fragment is added to the top @param {Number} [opts.level=0] Remove this many levels from the output @param {Boolean} [opts.arrayRoot=true] */ this.stringify = function (obj, opts) { var config = core.mixin({}, this.config) , xml = ''; core.mixin(config, (opts || {})); if (!config.whitespace) { indentLevel = 0; } if (!config.fragment) { xml += '\n'; } xml += obj2xml(obj, { name: config.name , level: config.level , arrayRoot: config.arrayRoot }); if (!config.whitespace) { xml = xml.replace(/>\n/g, '>'); } return xml; }; })();