UNPKG

objj-runtime

Version:

JavaScript (ECMAScript) and Objective-J runtime

672 lines (520 loc) 22.1 kB
/* * CFPropertyList.js * Objective-J * * Created by Francisco Tolmasky. * Copyright 2008-2010, 280 North, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ var OBJECT_COUNT = 0; GLOBAL(objj_generateObjectUID) = function() { return OBJECT_COUNT++; } GLOBAL(CFPropertyList) = function() { this._UID = objj_generateObjectUID(); } // We are really liberal when accepting DOCTYPEs. CFPropertyList.DTDRE = /^\s*(?:<\?\s*xml\s+version\s*=\s*\"1.0\"[^>]*\?>\s*)?(?:<\!DOCTYPE[^>]*>\s*)?/i CFPropertyList.XMLRE = /^\s*(?:<\?\s*xml\s+version\s*=\s*\"1.0\"[^>]*\?>\s*)?(?:<\!DOCTYPE[^>]*>\s*)?<\s*plist[^>]*\>/i; CFPropertyList.FormatXMLDTD = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"; CFPropertyList.Format280NorthMagicNumber = "280NPLIST"; // Serialization Formats CFPropertyList.FormatOpenStep = 1, CFPropertyList.FormatXML_v1_0 = 100, CFPropertyList.FormatBinary_v1_0 = 200, CFPropertyList.Format280North_v1_0 = -1000; CFPropertyList.sniffedFormatOfString = function(/*String*/ aString) { // Check if this is an XML Plist. if (aString.match(CFPropertyList.XMLRE)) return CFPropertyList.FormatXML_v1_0; if (aString.substr(0, CFPropertyList.Format280NorthMagicNumber.length) === CFPropertyList.Format280NorthMagicNumber) return CFPropertyList.Format280North_v1_0; return NULL; } // Serialization CFPropertyList.dataFromPropertyList = function(/*CFPropertyList*/ aPropertyList, /*Format*/ aFormat) { var data = new CFMutableData(); data.setRawString(CFPropertyList.stringFromPropertyList(aPropertyList, aFormat)); return data; } CFPropertyList.stringFromPropertyList = function(/*CFPropertyList*/ aPropertyList, /*Format*/ aFormat) { if (!aFormat) aFormat = CFPropertyList.Format280North_v1_0; var serializers = CFPropertyListSerializers[aFormat]; return serializers["start"]() + serializePropertyList(aPropertyList, serializers) + serializers["finish"](); } #ifdef COMMONJS CFPropertyList.readPropertyListFromFile = function(/*String*/ aFilePath) { return CFPropertyList.propertyListFromString(FILE.read(aFilePath, { charset:"UTF-8" })); } CFPropertyList.writePropertyListToFile = function(/*CFPropertyList*/ aPropertyList, /*String*/ aFilePath, /*Format*/ aFormat) { return FILE.write(aFilePath, CFPropertyList.stringFromPropertyList(aPropertyList, aFormat), { charset:"UTF-8" }); } CFPropertyList.modifyPlist = function(/*String*/ aFilePath, /*Function*/ aCallback, /*String*/ aFormat) { var string = FILE.read(aFilePath, { charset:"UTF-8" }); var format = CFPropertyList.sniffedFormatOfString(string); var plist = CFPropertyList.propertyListFromString(string, format); aCallback(plist); CFPropertyList.writePropertyListToFile(plist, aFilePath, aFormat || format); } #endif function serializePropertyList(/*CFPropertyList*/ aPropertyList, /*Object*/ serializers) { var type = typeof aPropertyList, valueOf = aPropertyList.valueOf(), typeValueOf = typeof valueOf; if (type !== typeValueOf) { type = typeValueOf; aPropertyList = valueOf; } if (aPropertyList === YES || aPropertyList === NO) type = "boolean"; else if (type === "number") { // A number like 3.4028234663852885e+54 should not be written as an integer, even that it is // an integer - it'd be awfully long if written by expanding it. Worse, if written as an // integer with scientific notation the parseInt() used to read it back will parse it as // just 3, thereby wasting many hours of sanity when you're trying to nib2cib a table and // columns are mysteriously showing up 3 pixels wide. if (FLOOR(aPropertyList) === aPropertyList && ("" + aPropertyList).indexOf('e') == -1) type = "integer"; else type = "real"; } else if (type !== "string") { if (aPropertyList.slice) type = "array"; else type = "dictionary"; } return serializers[type](aPropertyList, serializers); } var CFPropertyListSerializers = { }; CFPropertyListSerializers[CFPropertyList.FormatXML_v1_0] = { "start": function() { return CFPropertyList.FormatXMLDTD + "<plist version = \"1.0\">"; }, "finish": function() { return "</plist>"; }, "string": function(/*String*/ aString) { return "<string>" + encodeHTMLComponent(aString) + "</string>"; }, "boolean" : function(/*Boolean*/ aBoolean) { return aBoolean ? "<true/>" : "<false/>"; }, "integer": function(/*Integer*/ anInteger) { return "<integer>" + anInteger + "</integer>"; }, "real": function(/*Float*/ aFloat) { return "<real>" + aFloat + "</real>"; }, "array": function(/*Array*/ anArray, /*Object*/ serializers) { var index = 0, count = anArray.length, string = "<array>"; for (; index < count; ++index) string += serializePropertyList(anArray[index], serializers); return string + "</array>"; }, "dictionary": function(/*CFDictionary*/ aDictionary, /*Object*/ serializers) { var keys = aDictionary._keys, index = 0, count = keys.length, string = "<dict>"; for (; index < count; ++index) { var key = keys[index]; string += "<key>" + key + "</key>"; string += serializePropertyList(aDictionary.valueForKey(key), serializers); } return string + "</dict>"; } } // 280 North Plist Format var ARRAY_MARKER = "A", DICTIONARY_MARKER = "D", FLOAT_MARKER = "f", INTEGER_MARKER = "d", STRING_MARKER = "S", TRUE_MARKER = "T", FALSE_MARKER = "F", KEY_MARKER = "K", END_MARKER = "E"; CFPropertyListSerializers[CFPropertyList.Format280North_v1_0] = { "start": function() { return CFPropertyList.Format280NorthMagicNumber + ";1.0;"; }, "finish": function() { return ""; }, "string" : function(/*String*/ aString) { return STRING_MARKER + ';' + aString.length + ';' + aString; }, "boolean" : function(/*Boolean*/ aBoolean) { return (aBoolean ? TRUE_MARKER : FALSE_MARKER) + ';'; }, "integer": function(/*Integer*/ anInteger) { var string = "" + anInteger; return INTEGER_MARKER + ';' + string.length + ';' + string; }, "real": function(/*Float*/ aFloat) { var string = "" + aFloat; return FLOAT_MARKER + ';' + string.length + ';' + string; }, "array": function(/*Array*/ anArray, /*Object*/ serializers) { var index = 0, count = anArray.length, string = ARRAY_MARKER + ';'; for (; index < count; ++index) string += serializePropertyList(anArray[index], serializers); return string + END_MARKER + ';'; }, "dictionary": function(/*CFDictionary*/ aDictionary, /*Object*/ serializers) { var keys = aDictionary._keys, index = 0, count = keys.length, string = DICTIONARY_MARKER +';'; for (; index < count; ++index) { var key = keys[index]; string += KEY_MARKER + ';' + key.length + ';' + key; string += serializePropertyList(aDictionary.valueForKey(key), serializers); } return string + END_MARKER + ';'; } } // Deserialization var XML_XML = "xml", XML_DOCUMENT = "#document", PLIST_PLIST = "plist", PLIST_KEY = "key", PLIST_DICTIONARY = "dict", PLIST_ARRAY = "array", PLIST_STRING = "string", PLIST_DATE = "date", PLIST_BOOLEAN_TRUE = "true", PLIST_BOOLEAN_FALSE = "false", PLIST_NUMBER_REAL = "real", PLIST_NUMBER_INTEGER = "integer", PLIST_DATA = "data"; #define NODE_NAME(anXMLNode) (String(anXMLNode.nodeName)) #define NODE_TYPE(anXMLNode) (anXMLNode.nodeType) #define TEXT_CONTENT(anXMLNode) (anXMLNode.textContent || (anXMLNode.textContent !== "" && textContent([anXMLNode]))) #define FIRST_CHILD(anXMLNode) (anXMLNode.firstChild) #define NEXT_SIBLING(anXMLNode) (anXMLNode.nextSibling) #define PARENT_NODE(anXMLNode) (anXMLNode.parentNode) #define DOCUMENT_ELEMENT(aDocument) (aDocument && aDocument.documentElement) #define HAS_ATTRIBUTE_VALUE(anXMLNode, anAttributeName, aValue) (anXMLNode.getAttribute(anAttributeName) === aValue) #define IS_OF_TYPE(anXMLNode, aType) (NODE_NAME(anXMLNode) === aType) #define IS_PLIST(anXMLNode) IS_OF_TYPE(anXMLNode, PLIST_PLIST) #define IS_WHITESPACE(anXMLNode) (NODE_TYPE(anXMLNode) === 8 || NODE_TYPE(anXMLNode) === 3 || NODE_TYPE(anXMLNode) === 7) #define IS_DOCUMENTTYPE(anXMLNode) (NODE_TYPE(anXMLNode) === 10) #define PLIST_NEXT_SIBLING(anXMLNode) while ((anXMLNode = NEXT_SIBLING(anXMLNode)) && IS_WHITESPACE(anXMLNode)); #define PLIST_FIRST_CHILD(anXMLNode) { anXMLNode = FIRST_CHILD(anXMLNode); if (anXMLNode != NULL && IS_WHITESPACE(anXMLNode)) PLIST_NEXT_SIBLING(anXMLNode) } var textContent = function(nodes) { var text = "", index = 0, count = nodes.length; for (; index < count; ++index) { var node = nodes[index]; if (node.nodeType === 3 || node.nodeType === 4) text += node.nodeValue; else if (node.nodeType !== 8) text += textContent(node.childNodes); } return text; } var _plist_traverseNextNode = function(anXMLNode, stayWithin, stack) { var node = anXMLNode; PLIST_FIRST_CHILD(node); // If this element has a child, traverse to it. if (node) return node; // If not, first check if it is a container class (as opposed to a designated leaf). // If it is, then we have to pop this container off the stack, since it is empty. if (NODE_NAME(anXMLNode) === PLIST_ARRAY || NODE_NAME(anXMLNode) === PLIST_DICTIONARY) stack.pop(); // If not, next check whether it has a sibling. else { if (node === stayWithin) return NULL; node = anXMLNode; PLIST_NEXT_SIBLING(node); if (node) return node; } // If it doesn't, start working our way back up the node tree. node = anXMLNode; // While we have a node and it doesn't have a sibling (and we're within our stayWithin), // keep moving up. while (node) { var next = node; PLIST_NEXT_SIBLING(next); // If we have a next sibling, just go to it. if (next) return next; var node = PARENT_NODE(node); // If we are being asked to move up, and our parent is the stay within, then just if (stayWithin && node === stayWithin) return NULL; // Pop the stack if we have officially "moved up" stack.pop(); } return NULL; } CFPropertyList.propertyListFromData = function(/*Data*/ aData, /*Format*/ aFormat) { return CFPropertyList.propertyListFromString(aData.rawString(), aFormat); } CFPropertyList.propertyListFromString = function(/*String*/ aString, /*Format*/ aFormat) { if (!aFormat) aFormat = CFPropertyList.sniffedFormatOfString(aString); if (aFormat === CFPropertyList.FormatXML_v1_0) return CFPropertyList.propertyListFromXML(aString); if (aFormat === CFPropertyList.Format280North_v1_0) return propertyListFrom280NorthString(aString); return NULL; } // 280 North Plist Format var ARRAY_MARKER = "A", DICTIONARY_MARKER = "D", FLOAT_MARKER = "f", INTEGER_MARKER = "d", STRING_MARKER = "S", TRUE_MARKER = "T", FALSE_MARKER = "F", KEY_MARKER = "K", END_MARKER = "E"; function propertyListFrom280NorthString(/*String*/ aString) { var stream = new MarkedStream(aString), marker = NULL, key = "", object = NULL, plistObject = NULL, containers = [], currentContainer = NULL; while (marker = stream.getMarker()) { if (marker === END_MARKER) { containers.pop(); continue; } var count = containers.length; if (count) currentContainer = containers[count - 1]; if (marker === KEY_MARKER) { key = stream.getString(); marker = stream.getMarker(); } switch (marker) { case ARRAY_MARKER: object = [] containers.push(object); break; case DICTIONARY_MARKER: object = new CFMutableDictionary(); containers.push(object); break; case FLOAT_MARKER: object = parseFloat(stream.getString()); break; case INTEGER_MARKER: object = parseInt(stream.getString(), 10); break; case STRING_MARKER: object = stream.getString(); break; case TRUE_MARKER: object = YES; break; case FALSE_MARKER: object = NO; break; default: throw new Error("*** " + marker + " marker not recognized in Plist."); } if (!plistObject) plistObject = object; else if (currentContainer) // If the container is an array... if (currentContainer.slice) currentContainer.push(object); else currentContainer.setValueForKey(key, object); } return plistObject; } function encodeHTMLComponent(/*String*/ aString) { return aString.replace(/&/g,'&amp;').replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); } function decodeHTMLComponent(/*String*/ aString) { return aString.replace(/&quot;/g, '"').replace(/&apos;/g, '\'').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&'); } function parseXML(/*String*/ aString) { if (window.DOMParser) return DOCUMENT_ELEMENT(new window.DOMParser().parseFromString(aString, "text/xml")); else if (window.ActiveXObject) { XMLNode = new ActiveXObject("Microsoft.XMLDOM"); // Extract the DTD, which confuses IE. var matches = aString.match(CFPropertyList.DTDRE); if (matches) aString = aString.substr(matches[0].length); XMLNode.loadXML(aString); return XMLNode } return NULL; } CFPropertyList.propertyListFromXML = function(/*String | XMLNode*/ aStringOrXMLNode) { var XMLNode = aStringOrXMLNode; if (aStringOrXMLNode.valueOf && typeof aStringOrXMLNode.valueOf() === "string") XMLNode = parseXML(aStringOrXMLNode); // Skip over DOCTYPE and so forth. while (XMLNode && (IS_OF_TYPE(XMLNode, XML_DOCUMENT) || IS_OF_TYPE(XMLNode, XML_XML))) { PLIST_FIRST_CHILD(XMLNode); } // Skip over the DOCTYPE... see a pattern? if (XMLNode && IS_DOCUMENTTYPE(XMLNode)) PLIST_NEXT_SIBLING(XMLNode); // If this is not a PLIST, bail. if (!XMLNode || !IS_PLIST(XMLNode)) return NULL; var key = "", object = NULL, plistObject = NULL, plistNode = XMLNode, containers = [], currentContainer = NULL; while (XMLNode = _plist_traverseNextNode(XMLNode, plistNode, containers)) { var count = containers.length; if (count) currentContainer = containers[count - 1]; if (NODE_NAME(XMLNode) === PLIST_KEY) { key = TEXT_CONTENT(XMLNode); PLIST_NEXT_SIBLING(XMLNode); } switch (String(NODE_NAME(XMLNode))) { case PLIST_ARRAY: object = [] containers.push(object); break; case PLIST_DICTIONARY: object = new CFMutableDictionary(); containers.push(object); break; case PLIST_NUMBER_REAL: object = parseFloat(TEXT_CONTENT(XMLNode)); break; case PLIST_NUMBER_INTEGER: object = parseInt(TEXT_CONTENT(XMLNode), 10); break; case PLIST_STRING: if (HAS_ATTRIBUTE_VALUE(XMLNode, "type", "base64")) object = FIRST_CHILD(XMLNode) ? CFData.decodeBase64ToString(TEXT_CONTENT(XMLNode)) : ""; else object = decodeHTMLComponent(FIRST_CHILD(XMLNode) ? TEXT_CONTENT(XMLNode) : ""); break; case PLIST_DATE: var timestamp = Date.parseISO8601(TEXT_CONTENT(XMLNode)); object = isNaN(timestamp) ? new Date() : new Date(timestamp); break; case PLIST_BOOLEAN_TRUE: object = YES; break; case PLIST_BOOLEAN_FALSE: object = NO; break; case PLIST_DATA: object = new CFMutableData(); var data_bytes = FIRST_CHILD(XMLNode) ? CFData.decodeBase64ToArray(TEXT_CONTENT(XMLNode), YES) : []; object.setBytes(data_bytes); break; default: throw new Error("*** " + NODE_NAME(XMLNode) + " tag not recognized in Plist."); } if (!plistObject) plistObject = object; else if (currentContainer) // If the container is an array... if (currentContainer.slice) currentContainer.push(object); else currentContainer.setValueForKey(key, object); } return plistObject; } GLOBAL(kCFPropertyListOpenStepFormat) = CFPropertyList.FormatOpenStep; GLOBAL(kCFPropertyListXMLFormat_v1_0) = CFPropertyList.FormatXML_v1_0; GLOBAL(kCFPropertyListBinaryFormat_v1_0) = CFPropertyList.FormatBinary_v1_0; GLOBAL(kCFPropertyList280NorthFormat_v1_0) = CFPropertyList.Format280North_v1_0; GLOBAL(CFPropertyListCreate) = function() { return new CFPropertyList(); } GLOBAL(CFPropertyListCreateFromXMLData) = function(/*Data*/ data) { return CFPropertyList.propertyListFromData(data, CFPropertyList.FormatXML_v1_0); } GLOBAL(CFPropertyListCreateXMLData) = function(/*PropertyList*/ aPropertyList) { return CFPropertyList.dataFromPropertyList(aPropertyList, CFPropertyList.FormatXML_v1_0); } GLOBAL(CFPropertyListCreateFrom280NorthData) = function(/*Data*/ data) { return CFPropertyList.propertyListFromData(data, CFPropertyList.Format280North_v1_0); } GLOBAL(CFPropertyListCreate280NorthData) = function(/*PropertyList*/ aPropertyList) { return CFPropertyList.dataFromPropertyList(aPropertyList, CFPropertyList.Format280North_v1_0); } GLOBAL(CPPropertyListCreateFromData) = function(/*CFData*/ data, /*Format*/ aFormat) { return CFPropertyList.propertyListFromData(data, aFormat); } GLOBAL(CPPropertyListCreateData) = function(/*PropertyList*/ aPropertyList, /*Format*/ aFormat) { return CFPropertyList.dataFromPropertyList(aPropertyList, aFormat); };