/Objective-J/CFPropertyList.js

http://github.com/cacaodev/cappuccino · JavaScript · 670 lines · 471 code · 150 blank · 49 comment · 71 complexity · e6eac93e871caccd799c0dc2093d95c1 MD5 · raw file

  1. /*
  2. * CFPropertyList.js
  3. * Objective-J
  4. *
  5. * Created by Francisco Tolmasky.
  6. * Copyright 2008-2010, 280 North, Inc.
  7. *
  8. * This library is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU Lesser General Public
  10. * License as published by the Free Software Foundation; either
  11. * version 2.1 of the License, or (at your option) any later version.
  12. *
  13. * This library is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  16. * Lesser General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Lesser General Public
  19. * License along with this library; if not, write to the Free Software
  20. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  21. */
  22. var OBJECT_COUNT = 0;
  23. GLOBAL(objj_generateObjectUID) = function()
  24. {
  25. return OBJECT_COUNT++;
  26. }
  27. GLOBAL(CFPropertyList) = function()
  28. {
  29. this._UID = objj_generateObjectUID();
  30. }
  31. // We are really liberal when accepting DOCTYPEs.
  32. CFPropertyList.DTDRE = /^\s*(?:<\?\s*xml\s+version\s*=\s*\"1.0\"[^>]*\?>\s*)?(?:<\!DOCTYPE[^>]*>\s*)?/i
  33. CFPropertyList.XMLRE = /^\s*(?:<\?\s*xml\s+version\s*=\s*\"1.0\"[^>]*\?>\s*)?(?:<\!DOCTYPE[^>]*>\s*)?<\s*plist[^>]*\>/i;
  34. 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\">";
  35. CFPropertyList.Format280NorthMagicNumber = "280NPLIST";
  36. // Serialization Formats
  37. CFPropertyList.FormatOpenStep = 1,
  38. CFPropertyList.FormatXML_v1_0 = 100,
  39. CFPropertyList.FormatBinary_v1_0 = 200,
  40. CFPropertyList.Format280North_v1_0 = -1000;
  41. CFPropertyList.sniffedFormatOfString = function(/*String*/ aString)
  42. {
  43. // Check if this is an XML Plist.
  44. if (aString.match(CFPropertyList.XMLRE))
  45. return CFPropertyList.FormatXML_v1_0;
  46. if (aString.substr(0, CFPropertyList.Format280NorthMagicNumber.length) === CFPropertyList.Format280NorthMagicNumber)
  47. return CFPropertyList.Format280North_v1_0;
  48. return NULL;
  49. }
  50. // Serialization
  51. CFPropertyList.dataFromPropertyList = function(/*CFPropertyList*/ aPropertyList, /*Format*/ aFormat)
  52. {
  53. var data = new CFMutableData();
  54. data.setRawString(CFPropertyList.stringFromPropertyList(aPropertyList, aFormat));
  55. return data;
  56. }
  57. CFPropertyList.stringFromPropertyList = function(/*CFPropertyList*/ aPropertyList, /*Format*/ aFormat)
  58. {
  59. if (!aFormat)
  60. aFormat = CFPropertyList.Format280North_v1_0;
  61. var serializers = CFPropertyListSerializers[aFormat];
  62. return serializers["start"]() +
  63. serializePropertyList(aPropertyList, serializers) +
  64. serializers["finish"]();
  65. }
  66. #ifdef COMMONJS
  67. CFPropertyList.readPropertyListFromFile = function(/*String*/ aFilePath)
  68. {
  69. return CFPropertyList.propertyListFromString(FILE.read(aFilePath, { charset:"UTF-8" }));
  70. }
  71. CFPropertyList.writePropertyListToFile = function(/*CFPropertyList*/ aPropertyList, /*String*/ aFilePath, /*Format*/ aFormat)
  72. {
  73. return FILE.write(aFilePath, CFPropertyList.stringFromPropertyList(aPropertyList, aFormat), { charset:"UTF-8" });
  74. }
  75. CFPropertyList.modifyPlist = function(/*String*/ aFilePath, /*Function*/ aCallback, /*String*/ aFormat)
  76. {
  77. var string = FILE.read(aFilePath, { charset:"UTF-8" });
  78. var format = CFPropertyList.sniffedFormatOfString(string);
  79. var plist = CFPropertyList.propertyListFromString(string, format);
  80. aCallback(plist);
  81. CFPropertyList.writePropertyListToFile(plist, aFilePath, aFormat || format);
  82. }
  83. #endif
  84. function serializePropertyList(/*CFPropertyList*/ aPropertyList, /*Object*/ serializers)
  85. {
  86. var type = typeof aPropertyList,
  87. valueOf = aPropertyList.valueOf(),
  88. typeValueOf = typeof valueOf;
  89. if (type !== typeValueOf)
  90. {
  91. type = typeValueOf;
  92. aPropertyList = valueOf;
  93. }
  94. if (aPropertyList === YES || aPropertyList === NO)
  95. type = "boolean";
  96. else if (type === "number")
  97. {
  98. // A number like 3.4028234663852885e+54 should not be written as an integer, even that it is
  99. // an integer - it'd be awfully long if written by expanding it. Worse, if written as an
  100. // integer with scientific notation the parseInt() used to read it back will parse it as
  101. // just 3, thereby wasting many hours of sanity when you're trying to nib2cib a table and
  102. // columns are mysteriously showing up 3 pixels wide.
  103. if (FLOOR(aPropertyList) === aPropertyList && ("" + aPropertyList).indexOf('e') == -1)
  104. type = "integer";
  105. else
  106. type = "real";
  107. }
  108. else if (type !== "string")
  109. {
  110. if (aPropertyList.slice)
  111. type = "array";
  112. else
  113. type = "dictionary";
  114. }
  115. return serializers[type](aPropertyList, serializers);
  116. }
  117. var CFPropertyListSerializers = { };
  118. CFPropertyListSerializers[CFPropertyList.FormatXML_v1_0] =
  119. {
  120. "start": function()
  121. {
  122. return CFPropertyList.FormatXMLDTD + "<plist version = \"1.0\">";
  123. },
  124. "finish": function()
  125. {
  126. return "</plist>";
  127. },
  128. "string": function(/*String*/ aString)
  129. {
  130. return "<string>" + encodeHTMLComponent(aString) + "</string>";
  131. },
  132. "boolean" : function(/*Boolean*/ aBoolean)
  133. {
  134. return aBoolean ? "<true/>" : "<false/>";
  135. },
  136. "integer": function(/*Integer*/ anInteger)
  137. {
  138. return "<integer>" + anInteger + "</integer>";
  139. },
  140. "real": function(/*Float*/ aFloat)
  141. {
  142. return "<real>" + aFloat + "</real>";
  143. },
  144. "array": function(/*Array*/ anArray, /*Object*/ serializers)
  145. {
  146. var index = 0,
  147. count = anArray.length,
  148. string = "<array>";
  149. for (; index < count; ++index)
  150. string += serializePropertyList(anArray[index], serializers);
  151. return string + "</array>";
  152. },
  153. "dictionary": function(/*CFDictionary*/ aDictionary, /*Object*/ serializers)
  154. {
  155. var keys = aDictionary._keys,
  156. index = 0,
  157. count = keys.length,
  158. string = "<dict>";
  159. for (; index < count; ++index)
  160. {
  161. var key = keys[index];
  162. string += "<key>" + key + "</key>";
  163. string += serializePropertyList(aDictionary.valueForKey(key), serializers);
  164. }
  165. return string + "</dict>";
  166. }
  167. }
  168. // 280 North Plist Format
  169. var ARRAY_MARKER = "A",
  170. DICTIONARY_MARKER = "D",
  171. FLOAT_MARKER = "f",
  172. INTEGER_MARKER = "d",
  173. STRING_MARKER = "S",
  174. TRUE_MARKER = "T",
  175. FALSE_MARKER = "F",
  176. KEY_MARKER = "K",
  177. END_MARKER = "E";
  178. CFPropertyListSerializers[CFPropertyList.Format280North_v1_0] =
  179. {
  180. "start": function()
  181. {
  182. return CFPropertyList.Format280NorthMagicNumber + ";1.0;";
  183. },
  184. "finish": function()
  185. {
  186. return "";
  187. },
  188. "string" : function(/*String*/ aString)
  189. {
  190. return STRING_MARKER + ';' + aString.length + ';' + aString;
  191. },
  192. "boolean" : function(/*Boolean*/ aBoolean)
  193. {
  194. return (aBoolean ? TRUE_MARKER : FALSE_MARKER) + ';';
  195. },
  196. "integer": function(/*Integer*/ anInteger)
  197. {
  198. var string = "" + anInteger;
  199. return INTEGER_MARKER + ';' + string.length + ';' + string;
  200. },
  201. "real": function(/*Float*/ aFloat)
  202. {
  203. var string = "" + aFloat;
  204. return FLOAT_MARKER + ';' + string.length + ';' + string;
  205. },
  206. "array": function(/*Array*/ anArray, /*Object*/ serializers)
  207. {
  208. var index = 0,
  209. count = anArray.length,
  210. string = ARRAY_MARKER + ';';
  211. for (; index < count; ++index)
  212. string += serializePropertyList(anArray[index], serializers);
  213. return string + END_MARKER + ';';
  214. },
  215. "dictionary": function(/*CFDictionary*/ aDictionary, /*Object*/ serializers)
  216. {
  217. var keys = aDictionary._keys,
  218. index = 0,
  219. count = keys.length,
  220. string = DICTIONARY_MARKER +';';
  221. for (; index < count; ++index)
  222. {
  223. var key = keys[index];
  224. string += KEY_MARKER + ';' + key.length + ';' + key;
  225. string += serializePropertyList(aDictionary.valueForKey(key), serializers);
  226. }
  227. return string + END_MARKER + ';';
  228. }
  229. }
  230. // Deserialization
  231. var XML_XML = "xml",
  232. XML_DOCUMENT = "#document",
  233. PLIST_PLIST = "plist",
  234. PLIST_KEY = "key",
  235. PLIST_DICTIONARY = "dict",
  236. PLIST_ARRAY = "array",
  237. PLIST_STRING = "string",
  238. PLIST_DATE = "date",
  239. PLIST_BOOLEAN_TRUE = "true",
  240. PLIST_BOOLEAN_FALSE = "false",
  241. PLIST_NUMBER_REAL = "real",
  242. PLIST_NUMBER_INTEGER = "integer",
  243. PLIST_DATA = "data";
  244. #define NODE_NAME(anXMLNode) (String(anXMLNode.nodeName))
  245. #define NODE_TYPE(anXMLNode) (anXMLNode.nodeType)
  246. #define TEXT_CONTENT(anXMLNode) (anXMLNode.textContent || (anXMLNode.textContent !== "" && textContent([anXMLNode])))
  247. #define FIRST_CHILD(anXMLNode) (anXMLNode.firstChild)
  248. #define NEXT_SIBLING(anXMLNode) (anXMLNode.nextSibling)
  249. #define PARENT_NODE(anXMLNode) (anXMLNode.parentNode)
  250. #define DOCUMENT_ELEMENT(aDocument) (aDocument.documentElement)
  251. #define HAS_ATTRIBUTE_VALUE(anXMLNode, anAttributeName, aValue) (anXMLNode.getAttribute(anAttributeName) === aValue)
  252. #define IS_OF_TYPE(anXMLNode, aType) (NODE_NAME(anXMLNode) === aType)
  253. #define IS_PLIST(anXMLNode) IS_OF_TYPE(anXMLNode, PLIST_PLIST)
  254. #define IS_WHITESPACE(anXMLNode) (NODE_TYPE(anXMLNode) === 8 || NODE_TYPE(anXMLNode) === 3)
  255. #define IS_DOCUMENTTYPE(anXMLNode) (NODE_TYPE(anXMLNode) === 10)
  256. #define PLIST_NEXT_SIBLING(anXMLNode) while ((anXMLNode = NEXT_SIBLING(anXMLNode)) && IS_WHITESPACE(anXMLNode));
  257. #define PLIST_FIRST_CHILD(anXMLNode) { anXMLNode = FIRST_CHILD(anXMLNode); if (anXMLNode !== NULL && IS_WHITESPACE(anXMLNode)) PLIST_NEXT_SIBLING(anXMLNode) }
  258. var textContent = function(nodes)
  259. {
  260. var text = "",
  261. index = 0,
  262. count = nodes.length;
  263. for (; index < count; ++index)
  264. {
  265. var node = nodes[index];
  266. if (node.nodeType === 3 || node.nodeType === 4)
  267. text += node.nodeValue;
  268. else if (node.nodeType !== 8)
  269. text += textContent(node.childNodes);
  270. }
  271. return text;
  272. }
  273. var _plist_traverseNextNode = function(anXMLNode, stayWithin, stack)
  274. {
  275. var node = anXMLNode;
  276. PLIST_FIRST_CHILD(node);
  277. // If this element has a child, traverse to it.
  278. if (node)
  279. return node;
  280. // If not, first check if it is a container class (as opposed to a designated leaf).
  281. // If it is, then we have to pop this container off the stack, since it is empty.
  282. if (NODE_NAME(anXMLNode) === PLIST_ARRAY || NODE_NAME(anXMLNode) === PLIST_DICTIONARY)
  283. stack.pop();
  284. // If not, next check whether it has a sibling.
  285. else
  286. {
  287. if (node === stayWithin)
  288. return NULL;
  289. node = anXMLNode;
  290. PLIST_NEXT_SIBLING(node);
  291. if (node)
  292. return node;
  293. }
  294. // If it doesn't, start working our way back up the node tree.
  295. node = anXMLNode;
  296. // While we have a node and it doesn't have a sibling (and we're within our stayWithin),
  297. // keep moving up.
  298. while (node)
  299. {
  300. var next = node;
  301. PLIST_NEXT_SIBLING(next);
  302. // If we have a next sibling, just go to it.
  303. if (next)
  304. return next;
  305. var node = PARENT_NODE(node);
  306. // If we are being asked to move up, and our parent is the stay within, then just
  307. if (stayWithin && node === stayWithin)
  308. return NULL;
  309. // Pop the stack if we have officially "moved up"
  310. stack.pop();
  311. }
  312. return NULL;
  313. }
  314. CFPropertyList.propertyListFromData = function(/*Data*/ aData, /*Format*/ aFormat)
  315. {
  316. return CFPropertyList.propertyListFromString(aData.rawString(), aFormat);
  317. }
  318. CFPropertyList.propertyListFromString = function(/*String*/ aString, /*Format*/ aFormat)
  319. {
  320. if (!aFormat)
  321. aFormat = CFPropertyList.sniffedFormatOfString(aString);
  322. if (aFormat === CFPropertyList.FormatXML_v1_0)
  323. return CFPropertyList.propertyListFromXML(aString);
  324. if (aFormat === CFPropertyList.Format280North_v1_0)
  325. return propertyListFrom280NorthString(aString);
  326. return NULL;
  327. }
  328. // 280 North Plist Format
  329. var ARRAY_MARKER = "A",
  330. DICTIONARY_MARKER = "D",
  331. FLOAT_MARKER = "f",
  332. INTEGER_MARKER = "d",
  333. STRING_MARKER = "S",
  334. TRUE_MARKER = "T",
  335. FALSE_MARKER = "F",
  336. KEY_MARKER = "K",
  337. END_MARKER = "E";
  338. function propertyListFrom280NorthString(/*String*/ aString)
  339. {
  340. var stream = new MarkedStream(aString),
  341. marker = NULL,
  342. key = "",
  343. object = NULL,
  344. plistObject = NULL,
  345. containers = [],
  346. currentContainer = NULL;
  347. while (marker = stream.getMarker())
  348. {
  349. if (marker === END_MARKER)
  350. {
  351. containers.pop();
  352. continue;
  353. }
  354. var count = containers.length;
  355. if (count)
  356. currentContainer = containers[count - 1];
  357. if (marker === KEY_MARKER)
  358. {
  359. key = stream.getString();
  360. marker = stream.getMarker();
  361. }
  362. switch (marker)
  363. {
  364. case ARRAY_MARKER: object = []
  365. containers.push(object);
  366. break;
  367. case DICTIONARY_MARKER: object = new CFMutableDictionary();
  368. containers.push(object);
  369. break;
  370. case FLOAT_MARKER: object = parseFloat(stream.getString());
  371. break;
  372. case INTEGER_MARKER: object = parseInt(stream.getString(), 10);
  373. break;
  374. case STRING_MARKER: object = stream.getString();
  375. break;
  376. case TRUE_MARKER: object = YES;
  377. break;
  378. case FALSE_MARKER: object = NO;
  379. break;
  380. default: throw new Error("*** " + marker + " marker not recognized in Plist.");
  381. }
  382. if (!plistObject)
  383. plistObject = object;
  384. else if (currentContainer)
  385. // If the container is an array...
  386. if (currentContainer.slice)
  387. currentContainer.push(object);
  388. else
  389. currentContainer.setValueForKey(key, object);
  390. }
  391. return plistObject;
  392. }
  393. function encodeHTMLComponent(/*String*/ aString)
  394. {
  395. return aString.replace(/&/g,'&amp;').replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  396. }
  397. function decodeHTMLComponent(/*String*/ aString)
  398. {
  399. return aString.replace(/&quot;/g, '"').replace(/&apos;/g, '\'').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&');
  400. }
  401. function parseXML(/*String*/ aString)
  402. {
  403. if (window.DOMParser)
  404. return DOCUMENT_ELEMENT(new window.DOMParser().parseFromString(aString, "text/xml"));
  405. else if (window.ActiveXObject)
  406. {
  407. XMLNode = new ActiveXObject("Microsoft.XMLDOM");
  408. // Extract the DTD, which confuses IE.
  409. var matches = aString.match(CFPropertyList.DTDRE);
  410. if (matches)
  411. aString = aString.substr(matches[0].length);
  412. XMLNode.loadXML(aString);
  413. return XMLNode
  414. }
  415. return NULL;
  416. }
  417. CFPropertyList.propertyListFromXML = function(/*String | XMLNode*/ aStringOrXMLNode)
  418. {
  419. var XMLNode = aStringOrXMLNode;
  420. if (aStringOrXMLNode.valueOf && typeof aStringOrXMLNode.valueOf() === "string")
  421. XMLNode = parseXML(aStringOrXMLNode);
  422. // Skip over DOCTYPE and so forth.
  423. while (IS_OF_TYPE(XMLNode, XML_DOCUMENT) || IS_OF_TYPE(XMLNode, XML_XML))
  424. PLIST_FIRST_CHILD(XMLNode);
  425. // Skip over the DOCTYPE... see a pattern?
  426. if (IS_DOCUMENTTYPE(XMLNode))
  427. PLIST_NEXT_SIBLING(XMLNode);
  428. // If this is not a PLIST, bail.
  429. if (!IS_PLIST(XMLNode))
  430. return NULL;
  431. var key = "",
  432. object = NULL,
  433. plistObject = NULL,
  434. plistNode = XMLNode,
  435. containers = [],
  436. currentContainer = NULL;
  437. while (XMLNode = _plist_traverseNextNode(XMLNode, plistNode, containers))
  438. {
  439. var count = containers.length;
  440. if (count)
  441. currentContainer = containers[count - 1];
  442. if (NODE_NAME(XMLNode) === PLIST_KEY)
  443. {
  444. key = TEXT_CONTENT(XMLNode);
  445. PLIST_NEXT_SIBLING(XMLNode);
  446. }
  447. switch (String(NODE_NAME(XMLNode)))
  448. {
  449. case PLIST_ARRAY: object = []
  450. containers.push(object);
  451. break;
  452. case PLIST_DICTIONARY: object = new CFMutableDictionary();
  453. containers.push(object);
  454. break;
  455. case PLIST_NUMBER_REAL: object = parseFloat(TEXT_CONTENT(XMLNode));
  456. break;
  457. case PLIST_NUMBER_INTEGER: object = parseInt(TEXT_CONTENT(XMLNode), 10);
  458. break;
  459. case PLIST_STRING: if (HAS_ATTRIBUTE_VALUE(XMLNode, "type", "base64"))
  460. object = FIRST_CHILD(XMLNode) ? CFData.decodeBase64ToString(TEXT_CONTENT(XMLNode)) : "";
  461. else
  462. object = decodeHTMLComponent(FIRST_CHILD(XMLNode) ? TEXT_CONTENT(XMLNode) : "");
  463. break;
  464. case PLIST_DATE: var timestamp = Date.parseISO8601(TEXT_CONTENT(XMLNode));
  465. object = isNaN(timestamp) ? new Date() : new Date(timestamp);
  466. break;
  467. case PLIST_BOOLEAN_TRUE: object = YES;
  468. break;
  469. case PLIST_BOOLEAN_FALSE: object = NO;
  470. break;
  471. case PLIST_DATA: object = new CFMutableData();
  472. var data_bytes = FIRST_CHILD(XMLNode) ? CFData.decodeBase64ToArray(TEXT_CONTENT(XMLNode), YES) : [];
  473. object.setBytes(data_bytes);
  474. break;
  475. default: throw new Error("*** " + NODE_NAME(XMLNode) + " tag not recognized in Plist.");
  476. }
  477. if (!plistObject)
  478. plistObject = object;
  479. else if (currentContainer)
  480. // If the container is an array...
  481. if (currentContainer.slice)
  482. currentContainer.push(object);
  483. else
  484. currentContainer.setValueForKey(key, object);
  485. }
  486. return plistObject;
  487. }
  488. GLOBAL(kCFPropertyListOpenStepFormat) = CFPropertyList.FormatOpenStep;
  489. GLOBAL(kCFPropertyListXMLFormat_v1_0) = CFPropertyList.FormatXML_v1_0;
  490. GLOBAL(kCFPropertyListBinaryFormat_v1_0) = CFPropertyList.FormatBinary_v1_0;
  491. GLOBAL(kCFPropertyList280NorthFormat_v1_0) = CFPropertyList.Format280North_v1_0;
  492. GLOBAL(CFPropertyListCreate) = function()
  493. {
  494. return new CFPropertyList();
  495. }
  496. GLOBAL(CFPropertyListCreateFromXMLData) = function(/*Data*/ data)
  497. {
  498. return CFPropertyList.propertyListFromData(data, CFPropertyList.FormatXML_v1_0);
  499. }
  500. GLOBAL(CFPropertyListCreateXMLData) = function(/*PropertyList*/ aPropertyList)
  501. {
  502. return CFPropertyList.dataFromPropertyList(aPropertyList, CFPropertyList.FormatXML_v1_0);
  503. }
  504. GLOBAL(CFPropertyListCreateFrom280NorthData) = function(/*Data*/ data)
  505. {
  506. return CFPropertyList.propertyListFromData(data, CFPropertyList.Format280North_v1_0);
  507. }
  508. GLOBAL(CFPropertyListCreate280NorthData) = function(/*PropertyList*/ aPropertyList)
  509. {
  510. return CFPropertyList.dataFromPropertyList(aPropertyList, CFPropertyList.Format280North_v1_0);
  511. }
  512. GLOBAL(CPPropertyListCreateFromData) = function(/*CFData*/ data, /*Format*/ aFormat)
  513. {
  514. return CFPropertyList.propertyListFromData(data, aFormat);
  515. }
  516. GLOBAL(CPPropertyListCreateData) = function(/*PropertyList*/ aPropertyList, /*Format*/ aFormat)
  517. {
  518. return CFPropertyList.dataFromPropertyList(aPropertyList, aFormat);
  519. }