diff --git a/lib/plist.js b/lib/plist.js index 60165e7..334f955 100644 --- a/lib/plist.js +++ b/lib/plist.js @@ -208,72 +208,96 @@ var parser = new Parser(); parser.parseFile(filename, callback); } - - exports.build = function (obj) { - var XMLHDR = { 'version': '1.0','encoding': 'UTF-8'} + + 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. + // 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 (toString.call(obj) == '[object Date]'); + } + + function isBoolean(obj) { + return (obj === true || obj === false || toString.call(obj) == '[object Boolean]'); + } + + function isNumber(obj) { + return (toString.call(obj) == '[object Number]'); + } + + function isObject(obj) { + return (toString.call(obj) == '[object Object]'); + } + + function isString(obj) { + return (toString.call(obj) == '[object String]'); + } + + 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').ele('dict') - , stack = []; + , child = doc.begin('plist', XMLHDR, XMLDTD).att('version', '1.0') + , next, tag_type, i, prop; - reflect(obj); - return child.end({pretty: true }); - - 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' - } + walk_obj(obj, child); + return child.end({pretty: true }); + } + + function walk_obj(next, next_child) { + var tag_type, i, prop; - function reflect(o) { - prev = null; - var name; - for (name in o) { - if (typeof o[name]!== 'function') { - if (typeof o[name]=== 'object') { - if (o[name] instanceof Date) { - child = child.ele('key').txt(name).up().ele('date').raw(ISODateString(new Date(o[name]))).up(); - } else { - if (o[name] instanceof Array) { - prev = 'array'; - child = child.ele('key').txt(name).up().ele('array'); - } else { - if (stack[stack.length-1] instanceof Array) { - child = child.ele('dict'); - } else { - child = child.ele('key').txt(name).up().ele('dict'); - } - } - stack.push(o[name]); - reflect(o[name]); - } - } else { - child.ele('key').raw(name); - if ((typeof o[name]) === 'boolean') { - child.ele(o[name]); - } else if ((typeof o[name]) === 'number') { - child.ele((o[name] % 1 === 0) ? 'integer' : 'real').txt(o[name]); - } else { - var tok = o[name]; - // TODO this is sloppy base64 detection, use Buffer - var base64Matcher = new RegExp("^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$"); - if (base64Matcher.test(tok.replace(/\s/g,'')) && (tok.length>125)) { - child.ele('data').raw(tok); - } else { - child.ele('string').raw(tok); - } - } - } - } + 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)) { + 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); } - stack.pop(); - child = child.up(); } - } + } + 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 = ""; + // TODO this is sloppy base64 detection, use Buffer + var base64Matcher = new RegExp("^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$"); + if (base64Matcher.test(next.replace(/\s/g,'')) && (next.length>125)) { + next_child.ele('data').raw(next); + } else { + next_child.ele('string').raw(next); + } + } + }; + })(typeof exports === 'undefined' ? plist = {} : exports, typeof window === "undefined" ? require('sax') : sax, 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. diff --git a/tests/Cordova.plist b/tests/Cordova.plist new file mode 100644 index 0000000..94a4e01 --- /dev/null +++ b/tests/Cordova.plist @@ -0,0 +1,87 @@ + + + + + + UIWebViewBounce + + TopActivityIndicator + gray + EnableLocation + + EnableViewportScale + + AutoHideSplashScreen + + ShowSplashScreenSpinner + + MediaPlaybackRequiresUserAction + + AllowInlineMediaPlayback + + OpenAllWhitelistURLsInWebView + + BackupWebStorage + + ExternalHosts + + * + + Plugins + + Device + CDVDevice + Logger + CDVLogger + Compass + CDVLocation + Accelerometer + CDVAccelerometer + Camera + CDVCamera + NetworkStatus + CDVConnection + Contacts + CDVContacts + Debug Console + CDVDebugConsole + Echo + CDVEcho + File + CDVFile + FileTransfer + CDVFileTransfer + Geolocation + CDVLocation + Notification + CDVNotification + Media + CDVSound + Capture + CDVCapture + SplashScreen + CDVSplashScreen + Battery + CDVBattery + + + \ No newline at end of file diff --git a/tests/test-build.js b/tests/test-build.js index a49e594..cf766f7 100644 --- a/tests/test-build.js +++ b/tests/test-build.js @@ -2,7 +2,6 @@ var path = require('path') , fs = require('fs') , plist = require('../'); - /* // TODO These assertions fail because CDATA entities get converted in the process exports.testBuildFromPlistFile = function(test) { @@ -39,7 +38,6 @@ exports.testBuildFromSmallItunesXML = function(test) { }); } - exports.testBuildAirplayXML = function(test) { var file = path.join(__dirname, 'airplay.xml'); @@ -56,16 +54,36 @@ exports.testBuildAirplayXML = function(test) { }); } +exports.testCordovaPlist = function(test) { + var file = path.join(__dirname, 'Cordova.plist'); + + plist.parseFile(file, function(err, dicts) { + var dict = dicts[0]; + test.ifError(err); + test.equal(dict['TopActivityIndicator'], 'gray'); + test.equal(dict['Plugins']['Device'], 'CDVDevice'); + + // Try re-stringifying and re-parsing + plist.parseString(plist.build(dict), function(err, dicts2) { + test.ifError(err); + test.deepEqual(dicts,dicts2); + test.done(); + }); + }); +} + exports.testBuildPhoneGapPlist = function(test) { var file = path.join(__dirname, 'Xcode-PhoneGap.plist'); plist.parseFile(file, function(err, dicts) { var dict = dicts[0]; test.ifError(err); - + test.equal(dict['ExternalHosts'][0], "*"); test.equal(dict['Plugins']['com.phonegap.accelerometer'], "PGAccelerometer"); + //console.log('like they were', dict); + //console.log('hmm', plist.build(dict)); // Try re-stringifying and re-parsing plist.parseString(plist.build(dict), function(err, dicts2) { test.ifError(err); @@ -95,6 +113,7 @@ exports.testBuildXcodeInfoPlist = function(test) { }); } + // this code does a string to string comparison. It's not very useful right // now because CDATA sections arent supported. save for later I guess /*