blob: 42b0f391ddf778ea27b85e17fe56739b47f2f1ba [file] [log] [blame]
/*
* 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 + '>\n' + nextIndent +
'<' + n + '>') + '</' + n + '>\n';
// If this is a top-level item, close the top-level containing tag
if (level == 0) {
xml += currentIndent + '</' + inflection.pluralize(n) + '>';
}
}
// 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 += '<![CDATA[' + item + ']]>\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 += '</' + p + '>\n';
}
}
else {
xml += nextIndent;
xml += '</' + p + '>\n';
}
}
// Scalars, just close
else {
xml += '</' + p + '>\n';
}
}
}
// If this is a top-level item, close the top-level containing tag
if (level == 0) {
xml += currentIndent + '</' + n + '>\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 += '<?xml version="1.0" encoding="UTF-8"?>\n';
}
xml += obj2xml(obj, {
name: config.name
, level: config.level
, arrayRoot: config.arrayRoot
});
if (!config.whitespace) {
xml = xml.replace(/>\n/g, '>');
}
return xml;
};
})();