blob: 379c5d1b6c2e55859506cbadc16d92be3eb98acf [file] [log] [blame]
;(function (exports, DOMParser, xmlbuilder) {
// Checks if running in a non-browser environment
var inNode = typeof window === 'undefined' ? true : false
, utf8_to_b64
, b64_to_utf8;
// this library runs in browsers and nodejs, set up functions accordingly
if (inNode) {
exports.parseFile = function (filename, callback) {
console.warn('parseFile is deprecated. Please use parseFileSync instead.');
var fs = require('fs');
var inxml = fs.readFileSync(filename, 'utf8');
exports.parseString(inxml, callback);
}
exports.parseFileSync = function (filename) {
var fs = require('fs');
var inxml = fs.readFileSync(filename, 'utf8');
return exports.parseStringSync(inxml);
}
// set up base64 encode/decode functions
utf8_to_b64= function ( str ) {
return new Buffer(str).toString('base64');
}
b64_to_utf8 = function ( str ) {
return new Buffer(str, 'base64').toString('utf8');
}
} else {
// we're in a browser context, find the DOMParser
if (typeof window.DOMParser != 'undefined') {
DOMParser = function(xmlStr) {
return ( new window.DOMParser() ).parseFromString(xmlStr, 'text/xml');
};
} else if (typeof window.ActiveXObject != 'undefined' &&
new window.ActiveXObject('Microsoft.XMLDOM')) {
DOMParser = function(xmlStr) {
var xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM');
xmlDoc.async = 'false';
xmlDoc.loadXML(xmlStr);
return xmlDoc;
};
} else {
throw new Error('No XML parser found');
}
// set up base64 encode/decode functions
if (typeof window.btoa == 'undefined') {
throw new Error('No base64 support found');
// TODO: shim btoa and atob if not present (ie < 10)
//http://stackoverflow.com/questions/246801/how-can-you-encode-to-base64-using-javascript/247261#247261
} else {
utf8_to_b64= function ( str ) {
return window.btoa(unescape(encodeURIComponent( str )));
}
b64_to_utf8 = function ( str ) {
return decodeURIComponent(escape(window.atob( str )));
}
}
}
exports.parseString = function (xml, callback) {
console.warn('parseString is deprecated. Please use parseStringSync instead.');
var doc, error, plist;
try {
doc = new DOMParser().parseFromString(xml);
plist = parsePlistXML(doc.documentElement);
} catch(e) {
error = e;
}
callback(error, plist);
}
exports.parseStringSync = function (xml) {
var doc = new DOMParser().parseFromString(xml);
var plist;
if (doc.documentElement.nodeName !== 'plist') {
throw new Error('malformed document. First element should be <plist>');
}
plist = parsePlistXML(doc.documentElement);
// if the plist is an array with 1 element, pull it out of the array
if(isArray(plist) && plist.length == 1) {
plist = plist[0];
}
return plist;
}
/**
* convert an XML based plist document into a JSON representation
*
* @param object xml_node current XML node in the plist
* @return built up JSON object
*/
function parsePlistXML(node) {
var i, new_obj, key, val, new_arr;
if (!node)
return null;
if (node.nodeName === 'plist') {
new_arr = [];
for (i=0;i < node.childNodes.length;i++) {
// ignore comment nodes (text)
if (node.childNodes[i].nodeType !== 3) {
new_arr.push( parsePlistXML(node.childNodes[i]));
}
}
return new_arr;
}
else if(node.nodeName === 'dict') {
new_obj = {};
key = null;
for (i=0;i < node.childNodes.length;i++) {
// ignore comment nodes (text)
if (node.childNodes[i].nodeType !== 3) {
if (key === null) {
key = parsePlistXML(node.childNodes[i]);
} else {
new_obj[key] = parsePlistXML(node.childNodes[i]);
key = null;
}
}
}
return new_obj;
}
else if(node.nodeName === 'array') {
new_arr = [];
for (i=0;i < node.childNodes.length;i++) {
// ignore comment nodes (text)
if (node.childNodes[i].nodeType !== 3) {
res = parsePlistXML(node.childNodes[i]);
if (res) new_arr.push( res );
}
}
return new_arr;
}
else if(node.nodeName === '#text') {
// TODO: what should we do with text types? (CDATA sections)
}
else if(node.nodeName === 'key') {
return node.childNodes[0].nodeValue;
}
else if(node.nodeName === 'string') {
var res = '';
for (var d=0; d < node.childNodes.length; d++)
{
res += node.childNodes[d].nodeValue;
}
return res;
}
else if(node.nodeName === 'integer') {
// parse as base 10 integer
return parseInt(node.childNodes[0].nodeValue, 10);
}
else if(node.nodeName === 'real') {
var res = '';
for (var d=0; d < node.childNodes.length; d++)
{
if(node.childNodes[d].nodeType === 3) {
res += node.childNodes[d].nodeValue;
}
}
return parseFloat(res);
}
else if(node.nodeName === 'data') {
var res = '';
for (var d=0; d < node.childNodes.length; d++)
{
if(node.childNodes[d].nodeType === 3) {
res += node.childNodes[d].nodeValue;
}
}
// validate that the string is encoded as base64
var base64Matcher = new RegExp("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$");
if (!base64Matcher.test(res.replace(/\s/g,'')) ) {
throw new Error('malformed document. <data> element is not base64 encoded');
}
// decode base64 data as utf8 string
return b64_to_utf8(res);
}
else if(node.nodeName === 'date') {
return new Date(node.childNodes[0].nodeValue);
}
else if(node.nodeName === 'true') {
return true;
}
else if(node.nodeName === 'false') {
return false;
}
}
function ISODateString(d){
function pad(n){return n<10 ? '0'+n : n}
return d.getUTCFullYear()+'-'
+ pad(d.getUTCMonth()+1)+'-'
+ pad(d.getUTCDate())+'T'
+ pad(d.getUTCHours())+':'
+ pad(d.getUTCMinutes())+':'
+ pad(d.getUTCSeconds())+'Z'
}
// instanceof is horribly unreliable so we use these hackish but safer checks
// http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray
function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
function isDate(obj) {
return Object.prototype.toString.call(obj) === '[object Date]';
}
function isBoolean(obj) {
return (obj === true || obj === false || toString.call(obj) == '[object Boolean]');
}
function isNumber(obj) {
return Object.prototype.toString.call(obj) === '[object Number]';
}
function isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
function isString(obj) {
return Object.prototype.toString.call(obj) === '[object String]';
}
/**
* generate an XML plist string from the input object
*
* @param object obj the object to convert
* @return string converted plist
*/
exports.build = function(obj) {
var XMLHDR = { 'version': '1.0','encoding': 'UTF-8'}
, XMLDTD = { 'ext': 'PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"'}
, doc = xmlbuilder.create()
, child = doc.begin('plist', XMLHDR, XMLDTD).att('version', '1.0');
walk_obj(obj, child);
return child.end({pretty: true });
}
// depth first, recursive traversal of a javascript object. when complete,
// next_child contains a reference to the build XML object.
function walk_obj(next, next_child) {
var tag_type, i, prop;
if(isArray(next)) {
next_child = next_child.ele('array');
for(i=0 ;i < next.length;i++) {
walk_obj(next[i], next_child);
}
}
else if (isObject(next)) {
if (inNode && next instanceof Buffer) {
next_child.ele('data').raw(next.toString('base64'));
} else {
next_child = next_child.ele('dict');
for(prop in next) {
if(next.hasOwnProperty(prop)) {
next_child.ele('key').txt(prop);
walk_obj(next[prop], next_child);
}
}
}
}
else if(isNumber(next)) {
// detect if this is an integer or real
tag_type =(next % 1 === 0) ? 'integer' : 'real';
next_child.ele(tag_type).txt(next.toString());
}
else if(isDate(next)) {
next_child.ele('date').raw(ISODateString(new Date(next)));
}
else if(isBoolean(next)) {
val = next ? 'true' : 'false';
next_child.ele(val);
}
else if(isString(next)) {
//if (str!=obj || str.indexOf("\n")>=0) str = "<![CDATA["+str+"]]>";
var base64Matcher = new RegExp("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$");
if (!inNode && base64Matcher.test(next.replace(/\s/g,''))) {
// data is base 64 encoded so assume it's a <data> node
next_child.ele('data').raw(utf8_to_b64(next));
} else {
// it's not base 64 encoded, assume it's a <string> node
next_child.ele('string').raw(next);
}
}
};
})(typeof exports === 'undefined' ? plist = {} : exports, typeof window === 'undefined' ? require('xmldom').DOMParser : null, typeof window === 'undefined' ? require('xmlbuilder') : xmlbuilder)
// the above line checks for exports (defined in node) and uses it, or creates
// a global variable and exports to that. also, if in node, require DOMParser
// node-style, in browser it should already be present