├── .gitignore ├── LICENSE.txt ├── README.md ├── bower.json ├── jsonml-dom.js ├── jsonml-html.js ├── jsonml-jbst.js ├── jsonml-utils.js ├── jsonml-xml.js ├── jsonml.xslt ├── lib └── qunit │ ├── MIT-LICENSE.txt │ ├── download.url │ ├── qunit.css │ └── qunit.js ├── package.json └── test ├── unit.html ├── utils.js └── xmlTests.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .classpath 3 | .project 4 | .settings 5 | .iml 6 | .ipr 7 | .iws 8 | *\.sh -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2006-2012 Stephen M. McKamey 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JsonML 2 | ====== 3 | 4 | http://jsonml.org 5 | 6 | The utilities here represent a variety of JsonML-related tools for **losslessly** converting between XML/HTML and JSON. 7 | These are intended for use within a browser context for working with DOM representations of XML / HTML in JavaScript. 8 | A proof of concept XSLT is also included for converting from XML to JsonML. 9 | 10 | Mixed-mode XML 11 | -------------- 12 | 13 | **A common misconception is that JsonML is intended to replace a natural JSON data representation. *This is not the case!* ** 14 | 15 | JsonML-encoding is intended for situations of mixed-mode XML/HTML, i.e., where text nodes sit as siblings to element nodes. 16 | There isn't a natural JSON encoding for mixed-mode documents, and at worst the representation degrades to modeling DOM nodes themselves. 17 | JsonML is a more compact representation of mixed-mode XML/HTML which does not lose the structure of the original markup. 18 | 19 | JSON Data 20 | --------- 21 | 22 | While mixed-mode is very common in markup scenarios (X/HTML) it is actually quite rare in data documents. 23 | In data scenarios, a ["natural" JSON encoding](http://wiki.open311.org/index.php?title=JSON_and_XML_Conversion), such as Badgerfish or Parker Conventions, is 24 | generally more efficient and intuitive to work with in JavaScript. 25 | 26 | That being said, some have found JsonML to be a useful intermediate encoding of XML for manipulation within JavaScript. Your mileage may vary. 27 | 28 | Utilities 29 | --------- 30 | 31 | Here are the various modules available in this repository: 32 | 33 | ### jsonml-html.js *(formerly `jsonml2.js`)* 34 | 35 | Methods for converting from JsonML to HTML: 36 | 37 | - `Element JsonML.toHTML(string|array jml, function filter)` 38 | Converts a JsonML structure to HTML DOM nodes 39 | - `string JsonML.toHTMLText(string|array jml, function filter)` 40 | Converts a JsonML structure to HTML text 41 | 42 | ### jsonml-dom.js 43 | 44 | Methods for converting from HTML to JsonML: 45 | 46 | - `string|array JsonML.fromHTML(Element node, function filter)` 47 | Converts HTML DOM nodes to a JsonML structure 48 | 49 | - `string|array JsonML.fromHTMLText(string xmlText, function filter)` 50 | Converts HTML text to a JsonML structure 51 | 52 | ### jsonml-xml.js 53 | 54 | Methods for converting between JsonML and XML: 55 | 56 | - `Element JsonML.toXML(string|array jml, function filter)` 57 | Converts a JsonML structure to XML DOM nodes 58 | 59 | - `string JsonML.toXMLText(string|array jml, function filter)` 60 | Converts a JsonML structure to XML text 61 | 62 | - `string|array JsonML.fromXML(Element node, function filter)` 63 | Converts XML DOM nodes to a JsonML structure 64 | 65 | - `string|array JsonML.fromXMLText(string xmlText, function filter)` 66 | Converts XML text to a JsonML structure 67 | 68 | ### jsonml-utils.js 69 | 70 | Utility methods for manipulating JsonML structures: 71 | 72 | - `boolean JsonML.isElement(string|array jml)` 73 | Tests if a given object is a valid JsonML element 74 | 75 | - `string JsonML.getTagName(string|array jml)` 76 | Gets the name of a JsonML element 77 | 78 | - `boolean JsonML.isAttributes(string|array jml)` 79 | Tests if a given object is a JsonML attributes collection 80 | 81 | - `boolean JsonML.hasAttributes(string|array jml)` 82 | Tests if a JsonML element has a JsonML attributes collection 83 | 84 | - `object JsonML.getAttributes(string|array jml)` 85 | Gets the attributes collection for a JsonML element 86 | 87 | - `void JsonML.addAttributes(string|array jml, object attr)` 88 | Sets multiple attributes for a JsonML element 89 | 90 | - `object JsonML.getAttribute(string|array jml, string key)` 91 | Gets a single attribute for a JsonML element 92 | 93 | - `void JsonML.setAttribute(string|array jml, string key, string|int|boolean value)` 94 | Sets a single attribute for a JsonML element 95 | 96 | - `void JsonML.appendChild(parent, child)` 97 | Appends a JsonML child node to a parent JsonML element 98 | 99 | - `array JsonML.getChildren(string|array jml)` 100 | Gets an array of the child nodes of a JsonML element 101 | 102 | ### jsonml-jbst.js 103 | 104 | Client-side templating library which executes [JsonML Browser-Side Templates (JBST)](http://www.jsonml.org/bst/). 105 | This is the native runtime format generated by [JsonFx-UI](http://docs.jsonfx.net/jbst). 106 | 107 | // JBST + JSON => DOM 108 | var dom = JsonML.BST(jbst).bind(data); 109 | 110 | // JBST + JSON => JsonML 111 | var jsonml = JsonML.BST(jbst).dataBind(data); 112 | 113 | **NOTE:** JBST has been rebuilt from the ground up as [DUEL](http://duelengine.org), a dual-side template library (client-side & server-side). 114 | 115 | ### jsonml.xslt 116 | 117 | Proof-of-concept XSL transformation from XML directly to JsonML. 118 | 119 | ### jsonml2.js *(deprecated)* 120 | 121 | NOTE: If you want the same functionality of `jsonml2.js`, use `jsonml-html.js` and `jsonml-utils.js` if you used the extra helper methods. 122 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonml", 3 | "version": "2.0.0", 4 | "description": "JsonML-related tools for losslessly converting between XML/HTML and JSON, including mixed-mode XML.", 5 | "main": "jsonml-html.js", 6 | "keywords": [ 7 | "json", 8 | "mixed-mode", 9 | "xml", 10 | "lossless", 11 | "roundtrip", 12 | "conversion", 13 | "jbst" 14 | ], 15 | "authors": [ 16 | "Stephen M. McKamey (http://mck.me)" 17 | ], 18 | "license": "MIT", 19 | "homepage": "http://jsonml.org/", 20 | "ignore": [ 21 | "./!(jsonml*)" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/mckamey/jsonml.git" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /jsonml-dom.js: -------------------------------------------------------------------------------- 1 | /* 2 | jsonml-dom.js 3 | HTML to JsonML utility 4 | 5 | Created: 2007-02-15-2235 6 | Modified: 2012-11-03-2051 7 | 8 | Copyright (c)2006-2012 Stephen M. McKamey 9 | Distributed under The MIT License: http://jsonml.org/license 10 | */ 11 | 12 | var JsonML = JsonML || {}; 13 | 14 | if (typeof module === 'object') { 15 | module.exports = JsonML; 16 | } 17 | 18 | (function(JsonML, document){ 19 | 'use strict'; 20 | 21 | var addChildren = function(/*DOM*/ elem, /*function*/ filter, /*JsonML*/ jml) { 22 | if (elem.hasChildNodes()) { 23 | for (var i=0; i', ''); 98 | jml.push(child); 99 | } else if (elem.hasChildNodes()) { 100 | for (i=0; i', ''); 106 | jml.push(child); 107 | } 108 | } 109 | } 110 | break; 111 | case 'input': 112 | addChildren(elem, filter, jml); 113 | child = (elem.type !== 'password') && elem.value; 114 | if (child) { 115 | if (!hasAttrib) { 116 | // need to add an attribute object 117 | jml.shift(); 118 | props = {}; 119 | jml.unshift(props); 120 | jml.unshift(elem.tagName||''); 121 | } 122 | props.value = child; 123 | } 124 | break; 125 | case 'textarea': 126 | if (!addChildren(elem, filter, jml)) { 127 | child = elem.value || elem.innerHTML; 128 | if (child && 'string' === typeof child) { 129 | jml.push(child); 130 | } 131 | } 132 | break; 133 | default: 134 | addChildren(elem, filter, jml); 135 | break; 136 | } 137 | 138 | // filter result 139 | if ('function' === typeof filter) { 140 | jml = filter(jml, elem); 141 | } 142 | 143 | // free references 144 | elem = null; 145 | return jml; 146 | case 3: // text node 147 | case 4: // CDATA node 148 | var str = String(elem.nodeValue); 149 | // free references 150 | elem = null; 151 | return str; 152 | case 10: // doctype 153 | jml = ['!']; 154 | 155 | var type = ['DOCTYPE', (elem.name || 'html').toLowerCase()]; 156 | 157 | if (elem.publicId) { 158 | type.push('PUBLIC', '"' + elem.publicId + '"'); 159 | } 160 | 161 | if (elem.systemId) { 162 | type.push('"' + elem.systemId + '"'); 163 | } 164 | 165 | jml.push(type.join(' ')); 166 | 167 | // filter result 168 | if ('function' === typeof filter) { 169 | jml = filter(jml, elem); 170 | } 171 | 172 | // free references 173 | elem = null; 174 | return jml; 175 | case 8: // comment node 176 | if ((elem.nodeValue||'').indexOf('DOCTYPE') !== 0) { 177 | // free references 178 | elem = null; 179 | return null; 180 | } 181 | 182 | jml = ['!', 183 | elem.nodeValue]; 184 | 185 | // filter result 186 | if ('function' === typeof filter) { 187 | jml = filter(jml, elem); 188 | } 189 | 190 | // free references 191 | elem = null; 192 | return jml; 193 | default: // etc. 194 | // free references 195 | return (elem = null); 196 | } 197 | }; 198 | 199 | /** 200 | * @param {string} html HTML text 201 | * @param {function} filter 202 | * @return {array} JsonML 203 | */ 204 | JsonML.fromHTMLText = function(html, filter) { 205 | var elem = document.createElement('div'); 206 | elem.innerHTML = html; 207 | 208 | var jml = fromHTML(elem, filter); 209 | 210 | // free references 211 | elem = null; 212 | 213 | if (jml.length === 2) { 214 | return jml[1]; 215 | } 216 | 217 | // make wrapper a document fragment 218 | jml[0] = ''; 219 | return jml; 220 | }; 221 | 222 | })(JsonML, document); 223 | -------------------------------------------------------------------------------- /jsonml-html.js: -------------------------------------------------------------------------------- 1 | /* 2 | jsonml-html.js 3 | JsonML to HTML utility 4 | 5 | Created: 2006-11-09-0116 6 | Modified: 2012-11-24-1051 7 | 8 | Copyright (c)2006-2012 Stephen M. McKamey 9 | Distributed under The MIT License: http://jsonml.org/license 10 | 11 | This file ensures a global JsonML object adding these methods: 12 | 13 | JsonML.toHTML(JsonML, filter) 14 | 15 | This method produces a tree of DOM elements from a JsonML tree. The 16 | array must not contain any cyclical references. 17 | 18 | The optional filter parameter is a function which can filter and 19 | transform the results. It receives each of the DOM nodes, and 20 | its return value is used instead of the original value. If it 21 | returns what it received, then structure is not modified. If it 22 | returns undefined then the member is deleted. 23 | 24 | This is useful for binding unobtrusive JavaScript to the generated 25 | DOM elements. 26 | 27 | Example: 28 | 29 | // Parses the structure. If an element has a specific CSS value then 30 | // takes appropriate action: Remove from results, add special event 31 | // handlers, or bind to a custom component. 32 | 33 | var myUI = JsonML.toHTML(myUITemplate, function (elem) { 34 | if (elem.className.indexOf('Remove-Me') >= 0) { 35 | // this will remove from resulting DOM tree 36 | return null; 37 | } 38 | 39 | if (elem.tagName && elem.tagName.toLowerCase() === 'a' && 40 | elem.className.indexOf('External-Link') >= 0) { 41 | // this is the equivalent of target='_blank' 42 | elem.onclick = function(evt) { 43 | window.open(elem.href); return false; 44 | }; 45 | 46 | } else if (elem.className.indexOf('Fancy-Widgit') >= 0) { 47 | // bind to a custom component 48 | FancyWidgit.bindDOM(elem); 49 | } 50 | return elem; 51 | }); 52 | 53 | JsonML.toHTMLText(JsonML) 54 | Converts JsonML to HTML text 55 | 56 | // Implement onerror to handle any runtime errors while binding: 57 | JsonML.onerror = function (ex, jml, filter) { 58 | // display inline error message 59 | return document.createTextNode('['+ex+']'); 60 | }; 61 | */ 62 | 63 | var JsonML = JsonML || {}; 64 | 65 | if (typeof module === 'object') { 66 | module.exports = JsonML; 67 | } 68 | 69 | (function(JsonML, document) { 70 | 'use strict'; 71 | 72 | /** 73 | * Attribute name map 74 | * 75 | * @private 76 | * @constant 77 | * @type {Object.} 78 | */ 79 | var ATTR_MAP = { 80 | 'accesskey': 'accessKey', 81 | 'bgcolor': 'bgColor', 82 | 'cellpadding': 'cellPadding', 83 | 'cellspacing': 'cellSpacing', 84 | 'checked': 'defaultChecked', 85 | 'class': 'className', 86 | 'colspan': 'colSpan', 87 | 'contenteditable': 'contentEditable', 88 | 'defaultchecked': 'defaultChecked', 89 | 'for': 'htmlFor', 90 | 'formnovalidate': 'formNoValidate', 91 | 'hidefocus': 'hideFocus', 92 | 'ismap': 'isMap', 93 | 'maxlength': 'maxLength', 94 | 'novalidate': 'noValidate', 95 | 'readonly': 'readOnly', 96 | 'rowspan': 'rowSpan', 97 | 'spellcheck': 'spellCheck', 98 | 'tabindex': 'tabIndex', 99 | 'usemap': 'useMap', 100 | 'willvalidate': 'willValidate' 101 | // can add more attributes here as needed 102 | }; 103 | 104 | /** 105 | * Attribute duplicates map 106 | * 107 | * @private 108 | * @constant 109 | * @type {Object.} 110 | */ 111 | var ATTR_DUP = { 112 | 'enctype': 'encoding', 113 | 'onscroll': 'DOMMouseScroll' 114 | // can add more attributes here as needed 115 | }; 116 | 117 | /** 118 | * Attributes to be set via DOM 119 | * 120 | * @private 121 | * @constant 122 | * @type {Object.} 123 | */ 124 | var ATTR_DOM = { 125 | 'autocapitalize': 1, 126 | 'autocomplete': 1, 127 | 'autocorrect': 1 128 | // can add more attributes here as needed 129 | }; 130 | 131 | /** 132 | * Boolean attribute map 133 | * 134 | * @private 135 | * @constant 136 | * @type {Object.} 137 | */ 138 | var ATTR_BOOL = { 139 | 'async': 1, 140 | 'autofocus': 1, 141 | 'checked': 1, 142 | 'defaultchecked': 1, 143 | 'defer': 1, 144 | 'disabled': 1, 145 | 'formnovalidate': 1, 146 | 'hidden': 1, 147 | 'indeterminate': 1, 148 | 'ismap': 1, 149 | 'multiple': 1, 150 | 'novalidate': 1, 151 | 'readonly': 1, 152 | 'required': 1, 153 | 'spellcheck': 1, 154 | 'willvalidate': 1 155 | // can add more attributes here as needed 156 | }; 157 | 158 | /** 159 | * Leading SGML line ending pattern 160 | * 161 | * @private 162 | * @constant 163 | * @type {RegExp} 164 | */ 165 | var LEADING = /^[\r\n]+/; 166 | 167 | /** 168 | * Trailing SGML line ending pattern 169 | * 170 | * @private 171 | * @constant 172 | * @type {RegExp} 173 | */ 174 | var TRAILING = /[\r\n]+$/; 175 | 176 | /** 177 | * @private 178 | * @const 179 | * @type {number} 180 | */ 181 | var NUL = 0; 182 | 183 | /** 184 | * @private 185 | * @const 186 | * @type {number} 187 | */ 188 | var FUN = 1; 189 | 190 | /** 191 | * @private 192 | * @const 193 | * @type {number} 194 | */ 195 | var ARY = 2; 196 | 197 | /** 198 | * @private 199 | * @const 200 | * @type {number} 201 | */ 202 | var OBJ = 3; 203 | 204 | /** 205 | * @private 206 | * @const 207 | * @type {number} 208 | */ 209 | var VAL = 4; 210 | 211 | /** 212 | * @private 213 | * @const 214 | * @type {number} 215 | */ 216 | var RAW = 5; 217 | 218 | /** 219 | * Wraps a data value to maintain as raw markup in output 220 | * 221 | * @private 222 | * @this {Markup} 223 | * @param {string} value The value 224 | * @constructor 225 | */ 226 | function Markup(value) { 227 | /** 228 | * @type {string} 229 | * @const 230 | * @protected 231 | */ 232 | this.value = value; 233 | } 234 | 235 | /** 236 | * Renders the value 237 | * 238 | * @public 239 | * @override 240 | * @this {Markup} 241 | * @return {string} value 242 | */ 243 | Markup.prototype.toString = function() { 244 | return this.value; 245 | }; 246 | 247 | /** 248 | * @param {string} value 249 | * @return {Markup} 250 | */ 251 | JsonML.raw = function(value) { 252 | return new Markup(value); 253 | }; 254 | 255 | /** 256 | * @param {*} value 257 | * @return {boolean} 258 | */ 259 | var isMarkup = JsonML.isRaw = function(value) { 260 | return (value instanceof Markup); 261 | }; 262 | 263 | /** 264 | * Determines if the value is an Array 265 | * 266 | * @private 267 | * @param {*} val the object being tested 268 | * @return {boolean} 269 | */ 270 | var isArray = Array.isArray || function(val) { 271 | return (val instanceof Array); 272 | }; 273 | 274 | /** 275 | * Determines if the value is a function 276 | * 277 | * @private 278 | * @param {*} val the object being tested 279 | * @return {boolean} 280 | */ 281 | function isFunction(val) { 282 | return (typeof val === 'function'); 283 | } 284 | 285 | /** 286 | * Determines the type of the value 287 | * 288 | * @private 289 | * @param {*} val the object being tested 290 | * @return {number} 291 | */ 292 | function getType(val) { 293 | switch (typeof val) { 294 | case 'object': 295 | return !val ? NUL : (isArray(val) ? ARY : (isMarkup(val) ? RAW : ((val instanceof Date) ? VAL : OBJ))); 296 | case 'function': 297 | return FUN; 298 | case 'undefined': 299 | return NUL; 300 | default: 301 | return VAL; 302 | } 303 | } 304 | 305 | /** 306 | * Creates a DOM element 307 | * 308 | * @private 309 | * @param {string} tag The element's tag name 310 | * @return {Node} 311 | */ 312 | var createElement = function(tag) { 313 | if (!tag) { 314 | // create a document fragment to hold multiple-root elements 315 | if (document.createDocumentFragment) { 316 | return document.createDocumentFragment(); 317 | } 318 | 319 | tag = ''; 320 | 321 | } else if (tag.charAt(0) === '!') { 322 | return document.createComment(tag === '!' ? '' : tag.substr(1)+' '); 323 | } 324 | 325 | if (tag.toLowerCase() === 'style' && document.createStyleSheet) { 326 | // IE requires this interface for styles 327 | return document.createStyleSheet(); 328 | } 329 | 330 | return document.createElement(tag); 331 | }; 332 | 333 | /** 334 | * Adds an event handler to an element 335 | * 336 | * @private 337 | * @param {Node} elem The element 338 | * @param {string} name The event name 339 | * @param {function(Event)} handler The event handler 340 | */ 341 | var addHandler = function(elem, name, handler) { 342 | if (name.substr(0,2) === 'on') { 343 | name = name.substr(2); 344 | } 345 | 346 | switch (typeof handler) { 347 | case 'function': 348 | if (elem.addEventListener) { 349 | // DOM Level 2 350 | elem.addEventListener(name, handler, false); 351 | 352 | } else if (elem.attachEvent && getType(elem[name]) !== NUL) { 353 | // IE legacy events 354 | elem.attachEvent('on'+name, handler); 355 | 356 | } else { 357 | // DOM Level 0 358 | var old = elem['on'+name] || elem[name]; 359 | elem['on'+name] = elem[name] = !isFunction(old) ? handler : 360 | function(e) { 361 | return (old.call(this, e) !== false) && (handler.call(this, e) !== false); 362 | }; 363 | } 364 | break; 365 | 366 | case 'string': 367 | // inline functions are DOM Level 0 368 | /*jslint evil:true */ 369 | elem['on'+name] = new Function('event', handler); 370 | /*jslint evil:false */ 371 | break; 372 | } 373 | }; 374 | 375 | /** 376 | * Appends an attribute to an element 377 | * 378 | * @private 379 | * @param {Node} elem The element 380 | * @param {Object} attr Attributes object 381 | * @return {Node} 382 | */ 383 | var addAttributes = function(elem, attr) { 384 | if (attr.name && document.attachEvent && !elem.parentNode) { 385 | try { 386 | // IE fix for not being able to programatically change the name attribute 387 | var alt = createElement('<'+elem.tagName+' name="'+attr.name+'">'); 388 | // fix for Opera 8.5 and Netscape 7.1 creating malformed elements 389 | if (elem.tagName === alt.tagName) { 390 | elem = alt; 391 | } 392 | } catch (ex) { } 393 | } 394 | 395 | // for each attributeName 396 | for (var name in attr) { 397 | if (attr.hasOwnProperty(name)) { 398 | // attributeValue 399 | var value = attr[name], 400 | type = getType(value); 401 | 402 | if (name) { 403 | if (type === NUL) { 404 | value = ''; 405 | type = VAL; 406 | } 407 | 408 | name = ATTR_MAP[name.toLowerCase()] || name; 409 | 410 | if (name === 'style') { 411 | if (getType(elem.style.cssText) !== NUL) { 412 | elem.style.cssText = value; 413 | } else { 414 | elem.style = value; 415 | } 416 | 417 | } else if (name.substr(0,2) === 'on') { 418 | addHandler(elem, name, value); 419 | 420 | // also set duplicated events 421 | name = ATTR_DUP[name]; 422 | if (name) { 423 | addHandler(elem, name, value); 424 | } 425 | 426 | } else if (!ATTR_DOM[name.toLowerCase()] && (type !== VAL || name.charAt(0) === '$' || getType(elem[name]) !== NUL || getType(elem[ATTR_DUP[name]]) !== NUL)) { 427 | // direct setting of existing properties 428 | elem[name] = value; 429 | 430 | // also set duplicated properties 431 | name = ATTR_DUP[name]; 432 | if (name) { 433 | elem[name] = value; 434 | } 435 | 436 | } else if (ATTR_BOOL[name.toLowerCase()]) { 437 | if (value) { 438 | // boolean attributes 439 | elem.setAttribute(name, name); 440 | 441 | // also set duplicated attributes 442 | name = ATTR_DUP[name]; 443 | if (name) { 444 | elem.setAttribute(name, name); 445 | } 446 | } 447 | 448 | } else { 449 | // http://www.quirksmode.org/dom/w3c_core.html#attributes 450 | 451 | // custom and 'data-*' attributes 452 | elem.setAttribute(name, value); 453 | 454 | // also set duplicated attributes 455 | name = ATTR_DUP[name]; 456 | if (name) { 457 | elem.setAttribute(name, value); 458 | } 459 | } 460 | } 461 | } 462 | } 463 | return elem; 464 | }; 465 | 466 | /** 467 | * Appends a child to an element 468 | * 469 | * @private 470 | * @param {Node} elem The parent element 471 | * @param {Node} child The child 472 | */ 473 | var appendDOM = function(elem, child) { 474 | if (child) { 475 | var tag = (elem.tagName||'').toLowerCase(); 476 | if (elem.nodeType === 8) { // comment 477 | if (child.nodeType === 3) { // text node 478 | elem.nodeValue += child.nodeValue; 479 | } 480 | } else if (tag === 'table' && elem.tBodies) { 481 | if (!child.tagName) { 482 | // must unwrap documentFragment for tables 483 | if (child.nodeType === 11) { 484 | while (child.firstChild) { 485 | appendDOM(elem, child.removeChild(child.firstChild)); 486 | } 487 | } 488 | return; 489 | } 490 | 491 | // in IE must explicitly nest TRs in TBODY 492 | var childTag = child.tagName.toLowerCase();// child tagName 493 | if (childTag && childTag !== 'tbody' && childTag !== 'thead') { 494 | // insert in last tbody 495 | var tBody = elem.tBodies.length > 0 ? elem.tBodies[elem.tBodies.length-1] : null; 496 | if (!tBody) { 497 | tBody = createElement(childTag === 'th' ? 'thead' : 'tbody'); 498 | elem.appendChild(tBody); 499 | } 500 | tBody.appendChild(child); 501 | } else if (elem.canHaveChildren !== false) { 502 | elem.appendChild(child); 503 | } 504 | 505 | } else if (tag === 'style' && document.createStyleSheet) { 506 | // IE requires this interface for styles 507 | elem.cssText = child; 508 | 509 | } else if (elem.canHaveChildren !== false) { 510 | elem.appendChild(child); 511 | 512 | } else if (tag === 'object' && 513 | child.tagName && child.tagName.toLowerCase() === 'param') { 514 | // IE-only path 515 | try { 516 | elem.appendChild(child); 517 | } catch (ex1) {} 518 | try { 519 | if (elem.object) { 520 | elem.object[child.name] = child.value; 521 | } 522 | } catch (ex2) {} 523 | } 524 | } 525 | }; 526 | 527 | /** 528 | * Tests a node for whitespace 529 | * 530 | * @private 531 | * @param {Node} node The node 532 | * @return {boolean} 533 | */ 534 | var isWhitespace = function(node) { 535 | return !!node && (node.nodeType === 3) && (!node.nodeValue || !/\S/.exec(node.nodeValue)); 536 | }; 537 | 538 | /** 539 | * Trims whitespace pattern from the text node 540 | * 541 | * @private 542 | * @param {Node} node The node 543 | */ 544 | var trimPattern = function(node, pattern) { 545 | if (!!node && (node.nodeType === 3) && pattern.exec(node.nodeValue)) { 546 | node.nodeValue = node.nodeValue.replace(pattern, ''); 547 | } 548 | }; 549 | 550 | /** 551 | * Removes leading and trailing whitespace nodes 552 | * 553 | * @private 554 | * @param {Node} elem The node 555 | */ 556 | var trimWhitespace = function(elem) { 557 | if (elem) { 558 | while (isWhitespace(elem.firstChild)) { 559 | // trim leading whitespace text nodes 560 | elem.removeChild(elem.firstChild); 561 | } 562 | // trim leading whitespace text 563 | trimPattern(elem.firstChild, LEADING); 564 | while (isWhitespace(elem.lastChild)) { 565 | // trim trailing whitespace text nodes 566 | elem.removeChild(elem.lastChild); 567 | } 568 | // trim trailing whitespace text 569 | trimPattern(elem.lastChild, TRAILING); 570 | } 571 | }; 572 | 573 | /** 574 | * Converts the markup to DOM nodes 575 | * 576 | * @private 577 | * @param {string|Markup} value The node 578 | * @return {Node} 579 | */ 580 | var toDOM = function(value) { 581 | var wrapper = createElement('div'); 582 | wrapper.innerHTML = ''+value; 583 | 584 | // trim extraneous whitespace 585 | trimWhitespace(wrapper); 586 | 587 | // eliminate wrapper for single nodes 588 | if (wrapper.childNodes.length === 1) { 589 | return wrapper.firstChild; 590 | } 591 | 592 | // create a document fragment to hold elements 593 | var frag = createElement(''); 594 | while (wrapper.firstChild) { 595 | frag.appendChild(wrapper.firstChild); 596 | } 597 | return frag; 598 | }; 599 | 600 | /** 601 | * Default error handler 602 | * @param {Error} ex 603 | * @return {Node} 604 | */ 605 | var onError = function(ex) { 606 | return document.createTextNode('['+ex+']'); 607 | }; 608 | 609 | /* override this to perform custom error handling during binding */ 610 | JsonML.onerror = null; 611 | 612 | /** 613 | * also used by JsonML.BST 614 | * @param {Node} elem 615 | * @param {*} jml 616 | * @param {function} filter 617 | * @return {Node} 618 | */ 619 | var patch = JsonML.patch = function(elem, jml, filter) { 620 | 621 | for (var i=1; i DOM 16 | var dom = JsonML.BST(jbst).bind(data); 17 | 18 | // JBST + JSON => JsonML 19 | var jsonml = JsonML.BST(jbst).dataBind(data); 20 | 21 | // implement filter to intercept and perform custom filtering of resulting DOM elements 22 | JsonML.BST.filter = function(elem) { 23 | if (condition) { 24 | // this will prevent insertion into resulting DOM tree 25 | return null; 26 | } 27 | return elem; 28 | }; 29 | 30 | // implement onerror event to handle any runtime errors while binding 31 | JsonML.BST.onerror = function(ex) { 32 | // access the current context via this.data, this.index, etc. 33 | // display custom inline error messages 34 | return '['+ex+']'; 35 | }; 36 | 37 | // implement onbound event to perform custom processing of elements after binding 38 | JsonML.BST.onbound = function(node) { 39 | // access the current context via this.data, this.index, etc. 40 | // watch elements as they are constructed 41 | if (window.console) { 42 | console.log(JSON.stringify(output)); 43 | } 44 | }; 45 | 46 | // implement onappend event to perform custom processing of children before being appended 47 | JsonML.BST.onappend = function(parent, child) { 48 | // access the current context via this.data, this.index, etc. 49 | // watch elements as they are added 50 | if (window.console) { 51 | console.log(JsonML.getTagName(parent)+' > '+JsonML.getTagName(child)); 52 | } 53 | }; 54 | */ 55 | 56 | /* namespace JsonML */ 57 | var JsonML = JsonML || {}; 58 | 59 | if (typeof module === 'object') { 60 | module.exports = JsonML; 61 | } 62 | 63 | JsonML.BST = (function(){ 64 | 'use strict'; 65 | 66 | var SHOW = 'jbst:visible', 67 | INIT = 'jbst:oninit', 68 | LOAD = 'jbst:onload'; 69 | 70 | // ensures attribute key contains method or is removed 71 | // attr: attribute object 72 | // key: method name 73 | /*function*/ function ensureMethod(/*object*/ attr, /*string*/ key) { 74 | var method = attr[key] || null; 75 | if (method) { 76 | // ensure is method 77 | if ('function' !== typeof method) { 78 | try { 79 | /*jslint evil:true */ 80 | method = new Function(String(method)); 81 | /*jslint evil:false */ 82 | } catch (ex) { 83 | // filter 84 | method = null; 85 | } 86 | } 87 | if (method) { 88 | // IE doesn't like colon in property names 89 | attr[key.split(':').join('$')] = method; 90 | } 91 | delete attr[key]; 92 | } 93 | return method; 94 | } 95 | 96 | // default onerror handler, override JsonML.BST.onerror to change 97 | /*JsonML*/ function onError(/*Error*/ ex) { 98 | return '['+ex+']'; 99 | } 100 | 101 | // retrieve and remove method 102 | /*function*/ function popMethod(/*DOM*/ elem, /*string*/ key) { 103 | // IE doesn't like colon in property names 104 | key = key.split(':').join('$'); 105 | 106 | var method = elem[key]; 107 | if (method) { 108 | try { 109 | delete elem[key]; 110 | } catch (ex) { 111 | // sometimes IE doesn't like deleting from DOM 112 | elem[key] = undefined; 113 | } 114 | } 115 | return method; 116 | } 117 | 118 | // JsonML Filter 119 | /*DOM*/ function filter(/*DOM*/ elem) { 120 | 121 | // execute and remove jbst:oninit method 122 | var method = popMethod(elem, INIT); 123 | if ('function' === typeof method) { 124 | // execute in context of element 125 | method.call(elem); 126 | } 127 | 128 | // execute and remove jbst:onload method 129 | method = popMethod(elem, LOAD); 130 | if ('function' === typeof method) { 131 | // queue up to execute after insertion into parentNode 132 | setTimeout(function() { 133 | // execute in context of element 134 | method.call(elem); 135 | method = elem = null; 136 | }, 0); 137 | } 138 | 139 | if (JsonML.BST.filter) { 140 | return JsonML.BST.filter(elem); 141 | } 142 | 143 | return elem; 144 | } 145 | 146 | /*object*/ function callContext( 147 | /*object*/ self, 148 | /*object*/ data, 149 | /*int*/ index, 150 | /*int*/ count, 151 | /*object*/ args, 152 | /*function*/ method, 153 | /*Array*/ methodArgs) { 154 | 155 | try { 156 | // setup context for code block 157 | self.data = ('undefined' !== typeof data) ? data : null; 158 | self.index = isFinite(index) ? Number(index) : NaN; 159 | self.count = isFinite(count) ? Number(count) : NaN; 160 | self.args = ('undefined' !== typeof args) ? args : null; 161 | 162 | // execute node in the context of self as 'this', passing in any parameters 163 | return method.apply(self, methodArgs || []); 164 | 165 | } finally { 166 | // cleanup contextual members 167 | delete self.count; 168 | delete self.index; 169 | delete self.data; 170 | delete self.args; 171 | } 172 | } 173 | 174 | var appendChild = JsonML.appendChild; 175 | 176 | /* ctor */ 177 | function JBST(/*JsonML*/ jbst) { 178 | if ('undefined' === typeof jbst) { 179 | throw new Error('JBST tree is undefined'); 180 | } 181 | 182 | var self = this; 183 | 184 | // recursively applies dataBind to all nodes of the template graph 185 | // NOTE: it is very important to replace each node with a copy, 186 | // otherwise it destroys the original template. 187 | // node: current template node being data bound 188 | // data: current data item being bound 189 | // index: index of current data item 190 | // count: count of current set of data items 191 | // args: state object 192 | // returns: JsonML nodes 193 | /*object*/ function dataBind(/*JsonML*/ node, /*object*/ data, /*int*/ index, /*int*/ count, /*object*/ args) { 194 | try { 195 | // recursively process each node 196 | if (node) { 197 | var output; 198 | 199 | if ('function' === typeof node) { 200 | output = callContext(self, data, index, count, args, node); 201 | 202 | if (output instanceof JBST) { 203 | // allow returned JBSTs to recursively bind 204 | // useful for creating 'switcher' template methods 205 | return output.dataBind(data, index, count, args); 206 | } 207 | 208 | // function result 209 | return output; 210 | } 211 | 212 | if (node instanceof Array) { 213 | var onBound = ('function' === typeof JsonML.BST.onbound) && JsonML.BST.onbound, 214 | onAppend = ('function' === typeof JsonML.BST.onappend) && JsonML.BST.onappend, 215 | appendCB = onAppend && function(parent, child) { 216 | callContext(self, data, index, count, args, onAppend, [parent, child]); 217 | }; 218 | 219 | // JsonML output 220 | output = []; 221 | for (var i=0; i JsonML => DOM */ 316 | /*DOM*/ self.bind = function(/*object*/ data, /*int*/ index, /*int*/ count, /*object*/ args) { 317 | 318 | // databind JSON data to a JBST template, resulting in a JsonML representation 319 | var jml = iterate(jbst, data, index, count, args); 320 | 321 | // hydrate the resulting JsonML, executing callbacks, and user-filter 322 | return JsonML.toHTML(jml, filter); 323 | }; 324 | 325 | // replaces a DOM element with result from binding 326 | /*void*/ self.replace = function(/*DOM*/ elem, /*object*/ data, /*int*/ index, /*int*/ count, /*object*/ args) { 327 | if ('string' === typeof elem) { 328 | elem = document.getElementById(elem); 329 | } 330 | 331 | if (elem && elem.parentNode) { 332 | var jml = self.bind(data, index, count, args); 333 | if (jml) { 334 | elem.parentNode.replaceChild(jml, elem); 335 | } 336 | } 337 | }; 338 | 339 | // displace a DOM element with result from binding JsonML+BST node bound within this context 340 | /*void*/ self.displace = function(/*DOM*/ elem, /*JsonML*/ node, /*object*/ data, /*int*/ index, /*int*/ count, /*object*/ args) { 341 | if ('string' === typeof elem) { 342 | elem = document.getElementById(elem); 343 | } 344 | 345 | if (elem && elem.parentNode) { 346 | // databind JSON data to a JBST template, resulting in a JsonML representation 347 | var jml = iterate(node, data, index, count, args); 348 | 349 | // hydrate the resulting JsonML, executing callbacks, and user-filter 350 | jml = JsonML.toHTML(jml, filter); 351 | if (jml) { 352 | elem.parentNode.replaceChild(jml, elem); 353 | } 354 | } 355 | }; 356 | 357 | // patches a DOM element with JsonML+BST node bound within this context 358 | /*void*/ self.patch = function(/*DOM*/ elem, /*JsonML*/ node, /*object*/ data, /*int*/ index, /*int*/ count, /*object*/ args) { 359 | if ('string' === typeof elem) { 360 | elem = document.getElementById(elem); 361 | } 362 | 363 | if (elem) { 364 | var jml = ['']; 365 | appendChild(jml, dataBind(node, data, index, count, args)); 366 | JsonML.patch(elem, jml, filter); 367 | } 368 | }; 369 | } 370 | 371 | /* factory method */ 372 | return function(/*JBST*/ jbst) { 373 | return (jbst instanceof JBST) ? jbst : new JBST(jbst); 374 | }; 375 | })(); 376 | 377 | /* override to perform default filtering of the resulting DOM tree */ 378 | /*function*/ JsonML.BST.filter = null; 379 | 380 | /* override to perform custom error handling during binding */ 381 | /*function*/ JsonML.BST.onerror = null; 382 | 383 | /* override to perform custom processing of each element after adding to parent */ 384 | /*function*/ JsonML.BST.onappend = null; 385 | 386 | /* override to perform custom processing of each element after binding */ 387 | /*function*/ JsonML.BST.onbound = null; 388 | -------------------------------------------------------------------------------- /jsonml-utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | jsonml-utils.js 3 | JsonML manipulation methods 4 | 5 | Created: 2006-11-09-0116 6 | Modified: 2012-11-03-2051 7 | 8 | Copyright (c)2006-2012 Stephen M. McKamey 9 | Distributed under The MIT License: http://jsonml.org/license 10 | 11 | This file ensures a global JsonML object adding these utility methods for manipulating JsonML elements: 12 | 13 | // tests if a given object is a valid JsonML element 14 | bool JsonML.isElement(jml); 15 | 16 | // gets the name of a JsonML element 17 | string JsonML.getTagName(jml); 18 | 19 | // tests if a given object is a JsonML attributes collection 20 | bool JsonML.isAttributes(jml); 21 | 22 | // tests if a JsonML element has a JsonML attributes collection 23 | bool JsonML.hasAttributes(jml); 24 | 25 | // gets the attributes collection for a JsonML element 26 | object JsonML.getAttributes(jml); 27 | 28 | // sets multiple attributes for a JsonML element 29 | void JsonML.addAttributes(jml, attr); 30 | 31 | // gets a single attribute for a JsonML element 32 | object JsonML.getAttribute(jml, key); 33 | 34 | // sets a single attribute for a JsonML element 35 | void JsonML.setAttribute(jml, key, value); 36 | 37 | // appends a JsonML child node to a parent JsonML element 38 | void JsonML.appendChild(parent, child); 39 | 40 | // gets an array of the child nodes of a JsonML element 41 | array JsonML.getChildren(jml); 42 | */ 43 | 44 | var JsonML = JsonML || {}; 45 | 46 | if (typeof module === 'object') { 47 | module.exports = JsonML; 48 | } 49 | 50 | (function(JsonML) { 51 | 'use strict'; 52 | 53 | /* Utility Methods -------------------------*/ 54 | 55 | /** 56 | * Determines if the value is an Array 57 | * 58 | * @private 59 | * @param {*} val the object being tested 60 | * @return {boolean} 61 | */ 62 | var isArray = Array.isArray || function(val) { 63 | return (val instanceof Array); 64 | }; 65 | 66 | /** 67 | * @param {*} jml 68 | * @return {boolean} 69 | */ 70 | JsonML.isFragment = function(jml) { 71 | return isArray(jml) && (jml[0] === ''); 72 | }; 73 | 74 | /** 75 | * @param {*} jml 76 | * @return {string} 77 | */ 78 | JsonML.getTagName = function(jml) { 79 | return jml[0] || ''; 80 | }; 81 | 82 | /** 83 | * @param {*} jml 84 | * @return {boolean} 85 | */ 86 | var isElement = JsonML.isElement = function(jml) { 87 | return isArray(jml) && ('string' === typeof jml[0]); 88 | }; 89 | 90 | /** 91 | * @param {*} jml 92 | * @return {boolean} 93 | */ 94 | var isAttributes = JsonML.isAttributes = function(jml) { 95 | return !!jml && ('object' === typeof jml) && !isArray(jml); 96 | }; 97 | 98 | /** 99 | * @param {*} jml 100 | * @return {boolean} 101 | */ 102 | var hasAttributes = JsonML.hasAttributes = function(jml) { 103 | if (!isElement(jml)) { 104 | throw new SyntaxError('invalid JsonML'); 105 | } 106 | 107 | return isAttributes(jml[1]); 108 | }; 109 | 110 | /** 111 | * @param {*} jml 112 | * @param {boolean} addIfMissing 113 | * @return {object} 114 | */ 115 | var getAttributes = JsonML.getAttributes = function(jml, addIfMissing) { 116 | if (hasAttributes(jml)) { 117 | return jml[1]; 118 | } 119 | 120 | if (!addIfMissing) { 121 | return undefined; 122 | } 123 | 124 | // need to add an attribute object 125 | var name = jml.shift(); 126 | var attr = {}; 127 | jml.unshift(attr); 128 | jml.unshift(name||''); 129 | return attr; 130 | }; 131 | 132 | /** 133 | * @param {*} jml 134 | * @param {object} attr 135 | */ 136 | var addAttributes = JsonML.addAttributes = function(jml, attr) { 137 | if (!isElement(jml) || !isAttributes(attr)) { 138 | throw new SyntaxError('invalid JsonML'); 139 | } 140 | 141 | if (!isAttributes(jml[1])) { 142 | // just insert attributes 143 | var name = jml.shift(); 144 | jml.unshift(attr); 145 | jml.unshift(name||''); 146 | return; 147 | } 148 | 149 | // merge attribute objects 150 | var old = jml[1]; 151 | for (var key in attr) { 152 | if (attr.hasOwnProperty(key)) { 153 | old[key] = attr[key]; 154 | } 155 | } 156 | }; 157 | 158 | /** 159 | * @param {*} jml 160 | * @param {string} key 161 | * @return {string|number|boolean} 162 | */ 163 | JsonML.getAttribute = function(jml, key) { 164 | if (!hasAttributes(jml)) { 165 | return undefined; 166 | } 167 | return jml[1][key]; 168 | }; 169 | 170 | /** 171 | * @param {*} jml 172 | * @param {string} key 173 | * @param {string|number|boolean} value 174 | */ 175 | JsonML.setAttribute = function(jml, key, value) { 176 | getAttributes(jml, true)[key] = value; 177 | }; 178 | 179 | /** 180 | * @param {*} jml 181 | * @param {array|object|string} child 182 | */ 183 | var appendChild = JsonML.appendChild = function(parent, child) { 184 | if (!isArray(parent)) { 185 | throw new SyntaxError('invalid JsonML'); 186 | } 187 | 188 | if (isArray(child) && child[0] === '') { 189 | // result was multiple JsonML sub-trees (i.e. documentFragment) 190 | child.shift();// remove fragment ident 191 | 192 | // directly append children 193 | while (child.length) { 194 | appendChild(parent, child.shift(), arguments[2]); 195 | } 196 | 197 | } else if (child && 'object' === typeof child) { 198 | if (isArray(child)) { 199 | if (!isElement(child)) { 200 | throw new SyntaxError('invalid JsonML'); 201 | } 202 | 203 | if (typeof arguments[2] === 'function') { 204 | // onAppend callback for JBST use 205 | (arguments[2])(parent, child); 206 | } 207 | 208 | // result was a JsonML node 209 | parent.push(child); 210 | 211 | } else if (JsonML.isRaw(child)) { 212 | 213 | // result was a JsonML node 214 | parent.push(child); 215 | 216 | } else { 217 | // result was JsonML attributes 218 | addAttributes(parent, child); 219 | } 220 | 221 | } else if ('undefined' !== typeof child && child !== null) { 222 | 223 | // must convert to string or JsonML will discard 224 | child = String(child); 225 | 226 | // skip processing empty string literals 227 | if (child && parent.length > 1 && 'string' === typeof parent[parent.length-1]) { 228 | // combine strings 229 | parent[parent.length-1] += child; 230 | } else if (child || !parent.length) { 231 | // append 232 | parent.push(child); 233 | } 234 | } 235 | }; 236 | 237 | /** 238 | * @param {*} jml 239 | * @return {array} 240 | */ 241 | JsonML.getChildren = function(jml) { 242 | if (hasAttributes(jml)) { 243 | return jml.slice(2); 244 | } 245 | 246 | return jml.slice(1); 247 | }; 248 | 249 | })(JsonML); 250 | -------------------------------------------------------------------------------- /jsonml-xml.js: -------------------------------------------------------------------------------- 1 | /*global ActiveXObject */ 2 | /* 3 | jsonml-xml.js 4 | JsonML XML utilities 5 | 6 | Created: 2007-02-15-2235 7 | Modified: 2012-11-03-2051 8 | 9 | Copyright (c)2006-2012 Stephen M. McKamey 10 | Distributed under The MIT License: http://jsonml.org/license 11 | 12 | This file creates a global JsonML object containing these methods: 13 | 14 | JsonML.toXML(string|array, filter) 15 | Converts JsonML to XML nodes 16 | 17 | JsonML.toXMLText(JsonML, filter) 18 | Converts JsonML to XML text 19 | 20 | JsonML.fromXML(node, filter) 21 | Converts XML nodes to JsonML 22 | 23 | JsonML.fromXMLText(xmlText, filter) 24 | Converts XML text to JsonML 25 | */ 26 | 27 | var JsonML = JsonML || {}; 28 | 29 | if (typeof module === 'object') { 30 | module.exports = JsonML; 31 | } 32 | 33 | (function(JsonML, document) { 34 | 'use strict'; 35 | 36 | /** 37 | * Determines if the value is an Array 38 | * 39 | * @private 40 | * @param {*} val the object being tested 41 | * @return {boolean} 42 | */ 43 | var isArray = Array.isArray || function(val) { 44 | return (val instanceof Array); 45 | }; 46 | 47 | /** 48 | * Creates a DOM element 49 | * 50 | * @private 51 | * @param {string} tag The element's tag name 52 | * @return {Node} 53 | */ 54 | var createElement = function(tag) { 55 | if (!tag) { 56 | // create a document fragment to hold multiple-root elements 57 | if (document.createDocumentFragment) { 58 | return document.createDocumentFragment(); 59 | } 60 | 61 | tag = ''; 62 | 63 | } else if (tag.charAt(0) === '!') { 64 | return document.createComment(tag === '!' ? '' : tag.substr(1)+' '); 65 | } 66 | 67 | return document.createElement(tag); 68 | }; 69 | 70 | /** 71 | * Appends an attribute to an element 72 | * 73 | * @private 74 | * @param {Node} elem The element 75 | * @param {Object} attr Attributes object 76 | * @return {Node} 77 | */ 78 | var addAttributes = function(elem, attr) { 79 | // for each attributeName 80 | for (var name in attr) { 81 | if (attr.hasOwnProperty(name)) { 82 | // attributes 83 | elem.setAttribute(name, attr[name]); 84 | } 85 | } 86 | return elem; 87 | }; 88 | 89 | /** 90 | * Appends a child to an element 91 | * 92 | * @private 93 | * @param {Node} elem The parent element 94 | * @param {Node} child The child 95 | */ 96 | var appendDOM = function(elem, child) { 97 | if (child) { 98 | if (elem.nodeType === 8) { // comment 99 | if (child.nodeType === 3) { // text node 100 | elem.nodeValue += child.nodeValue; 101 | } 102 | 103 | } else if (elem.canHaveChildren !== false) { 104 | elem.appendChild(child); 105 | } 106 | } 107 | }; 108 | 109 | /** 110 | * Default error handler 111 | * @param {Error} ex 112 | * @return {Node} 113 | */ 114 | var onError = function (ex) { 115 | return document.createTextNode('['+ex+']'); 116 | }; 117 | 118 | /* override this to perform custom error handling during binding */ 119 | JsonML.onerror = null; 120 | 121 | /** 122 | * @param {Node} elem 123 | * @param {*} jml 124 | * @param {function} filter 125 | * @return {Node} 126 | */ 127 | var patch = function(elem, jml, filter) { 128 | 129 | for (var i=1; i') || document; 411 | 412 | })(JsonML, document); 413 | -------------------------------------------------------------------------------- /jsonml.xslt: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 25 | 26 | 27 | 29 | 30 | 32 | 33 | 35 | 36 | 38 | 39 | 41 | 42 | 44 | 45 | 47 | 48 | 50 | 51 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 155 | 157 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 167 | 169 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 179 | 181 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 191 | 193 | 195 | 196 | 197 | 198 | 199 | 200 | 202 | 204 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 223 | 225 | 226 | 228 | 230 | 232 | 233 | 234 | 235 | 236 | 238 | 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /lib/qunit/MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 John Resig, http://jquery.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/qunit/download.url: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=http://github.com/jquery/qunit/ 3 | IDList= 4 | HotKey=0 5 | [{000214A0-0000-0000-C000-000000000046}] 6 | Prop3=19,2 7 | -------------------------------------------------------------------------------- /lib/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-banner { 58 | height: 5px; 59 | } 60 | 61 | #qunit-testrunner-toolbar { 62 | padding: 0.5em 0 0.5em 2em; 63 | color: #5E740B; 64 | background-color: #eee; 65 | } 66 | 67 | #qunit-userAgent { 68 | padding: 0.5em 0 0.5em 2.5em; 69 | background-color: #2b81af; 70 | color: #fff; 71 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 72 | } 73 | 74 | 75 | /** Tests: Pass/Fail */ 76 | 77 | #qunit-tests { 78 | list-style-position: inside; 79 | } 80 | 81 | #qunit-tests li { 82 | padding: 0.4em 0.5em 0.4em 2.5em; 83 | border-bottom: 1px solid #fff; 84 | list-style-position: inside; 85 | } 86 | 87 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 88 | display: none; 89 | } 90 | 91 | #qunit-tests li strong { 92 | cursor: pointer; 93 | } 94 | 95 | #qunit-tests li a { 96 | padding: 0.5em; 97 | color: #c2ccd1; 98 | text-decoration: none; 99 | } 100 | #qunit-tests li a:hover, 101 | #qunit-tests li a:focus { 102 | color: #000; 103 | } 104 | 105 | #qunit-tests ol { 106 | margin-top: 0.5em; 107 | padding: 0.5em; 108 | 109 | background-color: #fff; 110 | 111 | border-radius: 15px; 112 | -moz-border-radius: 15px; 113 | -webkit-border-radius: 15px; 114 | 115 | box-shadow: inset 0px 2px 13px #999; 116 | -moz-box-shadow: inset 0px 2px 13px #999; 117 | -webkit-box-shadow: inset 0px 2px 13px #999; 118 | } 119 | 120 | #qunit-tests table { 121 | border-collapse: collapse; 122 | margin-top: .2em; 123 | } 124 | 125 | #qunit-tests th { 126 | text-align: right; 127 | vertical-align: top; 128 | padding: 0 .5em 0 0; 129 | } 130 | 131 | #qunit-tests td { 132 | vertical-align: top; 133 | } 134 | 135 | #qunit-tests pre { 136 | margin: 0; 137 | white-space: pre-wrap; 138 | word-wrap: break-word; 139 | } 140 | 141 | #qunit-tests del { 142 | background-color: #e0f2be; 143 | color: #374e0c; 144 | text-decoration: none; 145 | } 146 | 147 | #qunit-tests ins { 148 | background-color: #ffcaca; 149 | color: #500; 150 | text-decoration: none; 151 | } 152 | 153 | /*** Test Counts */ 154 | 155 | #qunit-tests b.counts { color: black; } 156 | #qunit-tests b.passed { color: #5E740B; } 157 | #qunit-tests b.failed { color: #710909; } 158 | 159 | #qunit-tests li li { 160 | margin: 0.5em; 161 | padding: 0.4em 0.5em 0.4em 0.5em; 162 | background-color: #fff; 163 | border-bottom: none; 164 | list-style-position: inside; 165 | } 166 | 167 | /*** Passing Styles */ 168 | 169 | #qunit-tests li li.pass { 170 | color: #5E740B; 171 | background-color: #fff; 172 | border-left: 26px solid #C6E746; 173 | } 174 | 175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 176 | #qunit-tests .pass .test-name { color: #366097; } 177 | 178 | #qunit-tests .pass .test-actual, 179 | #qunit-tests .pass .test-expected { color: #999999; } 180 | 181 | #qunit-banner.qunit-pass { background-color: #C6E746; } 182 | 183 | /*** Failing Styles */ 184 | 185 | #qunit-tests li li.fail { 186 | color: #710909; 187 | background-color: #fff; 188 | border-left: 26px solid #EE5757; 189 | white-space: pre; 190 | } 191 | 192 | #qunit-tests > li:last-child { 193 | border-radius: 0 0 15px 15px; 194 | -moz-border-radius: 0 0 15px 15px; 195 | -webkit-border-bottom-right-radius: 15px; 196 | -webkit-border-bottom-left-radius: 15px; 197 | } 198 | 199 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 200 | #qunit-tests .fail .test-name, 201 | #qunit-tests .fail .module-name { color: #000000; } 202 | 203 | #qunit-tests .fail .test-actual { color: #EE5757; } 204 | #qunit-tests .fail .test-expected { color: green; } 205 | 206 | #qunit-banner.qunit-fail { background-color: #EE5757; } 207 | 208 | 209 | /** Result */ 210 | 211 | #qunit-testresult { 212 | padding: 0.5em 0.5em 0.5em 2.5em; 213 | 214 | color: #2b81af; 215 | background-color: #D2E0E6; 216 | 217 | border-bottom: 1px solid white; 218 | } 219 | 220 | /** Fixture */ 221 | 222 | #qunit-fixture { 223 | position: absolute; 224 | top: -10000px; 225 | left: -10000px; 226 | } 227 | -------------------------------------------------------------------------------- /lib/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | try { 17 | return !!sessionStorage.getItem; 18 | } catch(e) { 19 | return false; 20 | } 21 | })() 22 | }; 23 | 24 | var testId = 0; 25 | 26 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { 27 | this.name = name; 28 | this.testName = testName; 29 | this.expected = expected; 30 | this.testEnvironmentArg = testEnvironmentArg; 31 | this.async = async; 32 | this.callback = callback; 33 | this.assertions = []; 34 | }; 35 | Test.prototype = { 36 | init: function() { 37 | var tests = id("qunit-tests"); 38 | if (tests) { 39 | var b = document.createElement("strong"); 40 | b.innerHTML = "Running " + this.name; 41 | var li = document.createElement("li"); 42 | li.appendChild( b ); 43 | li.className = "running"; 44 | li.id = this.id = "test-output" + testId++; 45 | tests.appendChild( li ); 46 | } 47 | }, 48 | setup: function() { 49 | if (this.module != config.previousModule) { 50 | if ( config.previousModule ) { 51 | runLoggingCallbacks('moduleDone', QUnit, { 52 | name: config.previousModule, 53 | failed: config.moduleStats.bad, 54 | passed: config.moduleStats.all - config.moduleStats.bad, 55 | total: config.moduleStats.all 56 | } ); 57 | } 58 | config.previousModule = this.module; 59 | config.moduleStats = { all: 0, bad: 0 }; 60 | runLoggingCallbacks( 'moduleStart', QUnit, { 61 | name: this.module 62 | } ); 63 | } 64 | 65 | config.current = this; 66 | this.testEnvironment = extend({ 67 | setup: function() {}, 68 | teardown: function() {} 69 | }, this.moduleTestEnvironment); 70 | if (this.testEnvironmentArg) { 71 | extend(this.testEnvironment, this.testEnvironmentArg); 72 | } 73 | 74 | runLoggingCallbacks( 'testStart', QUnit, { 75 | name: this.testName, 76 | module: this.module 77 | }); 78 | 79 | // allow utility functions to access the current test environment 80 | // TODO why?? 81 | QUnit.current_testEnvironment = this.testEnvironment; 82 | 83 | try { 84 | if ( !config.pollution ) { 85 | saveGlobal(); 86 | } 87 | 88 | this.testEnvironment.setup.call(this.testEnvironment); 89 | } catch(e) { 90 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); 91 | } 92 | }, 93 | run: function() { 94 | if ( this.async ) { 95 | QUnit.stop(); 96 | } 97 | 98 | if ( config.notrycatch ) { 99 | this.callback.call(this.testEnvironment); 100 | return; 101 | } 102 | try { 103 | this.callback.call(this.testEnvironment); 104 | } catch(e) { 105 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 106 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); 107 | // else next test will carry the responsibility 108 | saveGlobal(); 109 | 110 | // Restart the tests if they're blocking 111 | if ( config.blocking ) { 112 | start(); 113 | } 114 | } 115 | }, 116 | teardown: function() { 117 | try { 118 | this.testEnvironment.teardown.call(this.testEnvironment); 119 | checkPollution(); 120 | } catch(e) { 121 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); 122 | } 123 | }, 124 | finish: function() { 125 | if ( this.expected && this.expected != this.assertions.length ) { 126 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 127 | } 128 | 129 | var good = 0, bad = 0, 130 | tests = id("qunit-tests"); 131 | 132 | config.stats.all += this.assertions.length; 133 | config.moduleStats.all += this.assertions.length; 134 | 135 | if ( tests ) { 136 | var ol = document.createElement("ol"); 137 | 138 | for ( var i = 0; i < this.assertions.length; i++ ) { 139 | var assertion = this.assertions[i]; 140 | 141 | var li = document.createElement("li"); 142 | li.className = assertion.result ? "pass" : "fail"; 143 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 144 | ol.appendChild( li ); 145 | 146 | if ( assertion.result ) { 147 | good++; 148 | } else { 149 | bad++; 150 | config.stats.bad++; 151 | config.moduleStats.bad++; 152 | } 153 | } 154 | 155 | // store result when possible 156 | if ( QUnit.config.reorder && defined.sessionStorage ) { 157 | if (bad) { 158 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); 159 | } else { 160 | sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); 161 | } 162 | } 163 | 164 | if (bad == 0) { 165 | ol.style.display = "none"; 166 | } 167 | 168 | var b = document.createElement("strong"); 169 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 170 | 171 | var a = document.createElement("a"); 172 | a.innerHTML = "Rerun"; 173 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 174 | 175 | addEvent(b, "click", function() { 176 | var next = b.nextSibling.nextSibling, 177 | display = next.style.display; 178 | next.style.display = display === "none" ? "block" : "none"; 179 | }); 180 | 181 | addEvent(b, "dblclick", function(e) { 182 | var target = e && e.target ? e.target : window.event.srcElement; 183 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 184 | target = target.parentNode; 185 | } 186 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 187 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 188 | } 189 | }); 190 | 191 | var li = id(this.id); 192 | li.className = bad ? "fail" : "pass"; 193 | li.removeChild( li.firstChild ); 194 | li.appendChild( b ); 195 | li.appendChild( a ); 196 | li.appendChild( ol ); 197 | 198 | } else { 199 | for ( var i = 0; i < this.assertions.length; i++ ) { 200 | if ( !this.assertions[i].result ) { 201 | bad++; 202 | config.stats.bad++; 203 | config.moduleStats.bad++; 204 | } 205 | } 206 | } 207 | 208 | try { 209 | QUnit.reset(); 210 | } catch(e) { 211 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 212 | } 213 | 214 | runLoggingCallbacks( 'testDone', QUnit, { 215 | name: this.testName, 216 | module: this.module, 217 | failed: bad, 218 | passed: this.assertions.length - bad, 219 | total: this.assertions.length 220 | } ); 221 | }, 222 | 223 | queue: function() { 224 | var test = this; 225 | synchronize(function() { 226 | test.init(); 227 | }); 228 | function run() { 229 | // each of these can by async 230 | synchronize(function() { 231 | test.setup(); 232 | }); 233 | synchronize(function() { 234 | test.run(); 235 | }); 236 | synchronize(function() { 237 | test.teardown(); 238 | }); 239 | synchronize(function() { 240 | test.finish(); 241 | }); 242 | } 243 | // defer when previous test run passed, if storage is available 244 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); 245 | if (bad) { 246 | run(); 247 | } else { 248 | synchronize(run); 249 | }; 250 | } 251 | 252 | }; 253 | 254 | var QUnit = { 255 | 256 | // call on start of module test to prepend name to all tests 257 | module: function(name, testEnvironment) { 258 | config.currentModule = name; 259 | config.currentModuleTestEnviroment = testEnvironment; 260 | }, 261 | 262 | asyncTest: function(testName, expected, callback) { 263 | if ( arguments.length === 2 ) { 264 | callback = expected; 265 | expected = 0; 266 | } 267 | 268 | QUnit.test(testName, expected, callback, true); 269 | }, 270 | 271 | test: function(testName, expected, callback, async) { 272 | var name = '' + testName + '', testEnvironmentArg; 273 | 274 | if ( arguments.length === 2 ) { 275 | callback = expected; 276 | expected = null; 277 | } 278 | // is 2nd argument a testEnvironment? 279 | if ( expected && typeof expected === 'object') { 280 | testEnvironmentArg = expected; 281 | expected = null; 282 | } 283 | 284 | if ( config.currentModule ) { 285 | name = '' + config.currentModule + ": " + name; 286 | } 287 | 288 | if ( !validTest(config.currentModule + ": " + testName) ) { 289 | return; 290 | } 291 | 292 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); 293 | test.module = config.currentModule; 294 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 295 | test.queue(); 296 | }, 297 | 298 | /** 299 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 300 | */ 301 | expect: function(asserts) { 302 | config.current.expected = asserts; 303 | }, 304 | 305 | /** 306 | * Asserts true. 307 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 308 | */ 309 | ok: function(a, msg) { 310 | a = !!a; 311 | var details = { 312 | result: a, 313 | message: msg 314 | }; 315 | msg = escapeInnerText(msg); 316 | runLoggingCallbacks( 'log', QUnit, details ); 317 | config.current.assertions.push({ 318 | result: a, 319 | message: msg 320 | }); 321 | }, 322 | 323 | /** 324 | * Checks that the first two arguments are equal, with an optional message. 325 | * Prints out both actual and expected values. 326 | * 327 | * Prefered to ok( actual == expected, message ) 328 | * 329 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 330 | * 331 | * @param Object actual 332 | * @param Object expected 333 | * @param String message (optional) 334 | */ 335 | equal: function(actual, expected, message) { 336 | QUnit.push(expected == actual, actual, expected, message); 337 | }, 338 | 339 | notEqual: function(actual, expected, message) { 340 | QUnit.push(expected != actual, actual, expected, message); 341 | }, 342 | 343 | deepEqual: function(actual, expected, message) { 344 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 345 | }, 346 | 347 | notDeepEqual: function(actual, expected, message) { 348 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 349 | }, 350 | 351 | strictEqual: function(actual, expected, message) { 352 | QUnit.push(expected === actual, actual, expected, message); 353 | }, 354 | 355 | notStrictEqual: function(actual, expected, message) { 356 | QUnit.push(expected !== actual, actual, expected, message); 357 | }, 358 | 359 | raises: function(block, expected, message) { 360 | var actual, ok = false; 361 | 362 | if (typeof expected === 'string') { 363 | message = expected; 364 | expected = null; 365 | } 366 | 367 | try { 368 | block(); 369 | } catch (e) { 370 | actual = e; 371 | } 372 | 373 | if (actual) { 374 | // we don't want to validate thrown error 375 | if (!expected) { 376 | ok = true; 377 | // expected is a regexp 378 | } else if (QUnit.objectType(expected) === "regexp") { 379 | ok = expected.test(actual); 380 | // expected is a constructor 381 | } else if (actual instanceof expected) { 382 | ok = true; 383 | // expected is a validation function which returns true is validation passed 384 | } else if (expected.call({}, actual) === true) { 385 | ok = true; 386 | } 387 | } 388 | 389 | QUnit.ok(ok, message); 390 | }, 391 | 392 | start: function() { 393 | config.semaphore--; 394 | if (config.semaphore > 0) { 395 | // don't start until equal number of stop-calls 396 | return; 397 | } 398 | if (config.semaphore < 0) { 399 | // ignore if start is called more often then stop 400 | config.semaphore = 0; 401 | } 402 | // A slight delay, to avoid any current callbacks 403 | if ( defined.setTimeout ) { 404 | window.setTimeout(function() { 405 | if (config.semaphore > 0) { 406 | return; 407 | } 408 | if ( config.timeout ) { 409 | clearTimeout(config.timeout); 410 | } 411 | 412 | config.blocking = false; 413 | process(); 414 | }, 13); 415 | } else { 416 | config.blocking = false; 417 | process(); 418 | } 419 | }, 420 | 421 | stop: function(timeout) { 422 | config.semaphore++; 423 | config.blocking = true; 424 | 425 | if ( timeout && defined.setTimeout ) { 426 | clearTimeout(config.timeout); 427 | config.timeout = window.setTimeout(function() { 428 | QUnit.ok( false, "Test timed out" ); 429 | QUnit.start(); 430 | }, timeout); 431 | } 432 | } 433 | }; 434 | 435 | //We want access to the constructor's prototype 436 | (function() { 437 | function F(){}; 438 | F.prototype = QUnit; 439 | QUnit = new F(); 440 | //Make F QUnit's constructor so that we can add to the prototype later 441 | QUnit.constructor = F; 442 | })(); 443 | 444 | // Backwards compatibility, deprecated 445 | QUnit.equals = QUnit.equal; 446 | QUnit.same = QUnit.deepEqual; 447 | 448 | // Maintain internal state 449 | var config = { 450 | // The queue of tests to run 451 | queue: [], 452 | 453 | // block until document ready 454 | blocking: true, 455 | 456 | // when enabled, show only failing tests 457 | // gets persisted through sessionStorage and can be changed in UI via checkbox 458 | hidepassed: false, 459 | 460 | // by default, run previously failed tests first 461 | // very useful in combination with "Hide passed tests" checked 462 | reorder: true, 463 | 464 | // by default, modify document.title when suite is done 465 | altertitle: true, 466 | 467 | urlConfig: ['noglobals', 'notrycatch'], 468 | 469 | //logging callback queues 470 | begin: [], 471 | done: [], 472 | log: [], 473 | testStart: [], 474 | testDone: [], 475 | moduleStart: [], 476 | moduleDone: [] 477 | }; 478 | 479 | // Load paramaters 480 | (function() { 481 | var location = window.location || { search: "", protocol: "file:" }, 482 | params = location.search.slice( 1 ).split( "&" ), 483 | length = params.length, 484 | urlParams = {}, 485 | current; 486 | 487 | if ( params[ 0 ] ) { 488 | for ( var i = 0; i < length; i++ ) { 489 | current = params[ i ].split( "=" ); 490 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 491 | // allow just a key to turn on a flag, e.g., test.html?noglobals 492 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 493 | urlParams[ current[ 0 ] ] = current[ 1 ]; 494 | } 495 | } 496 | 497 | QUnit.urlParams = urlParams; 498 | config.filter = urlParams.filter; 499 | 500 | // Figure out if we're running the tests from a server or not 501 | QUnit.isLocal = !!(location.protocol === 'file:'); 502 | })(); 503 | 504 | // Expose the API as global variables, unless an 'exports' 505 | // object exists, in that case we assume we're in CommonJS 506 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 507 | extend(window, QUnit); 508 | window.QUnit = QUnit; 509 | } else { 510 | extend(exports, QUnit); 511 | exports.QUnit = QUnit; 512 | } 513 | 514 | // define these after exposing globals to keep them in these QUnit namespace only 515 | extend(QUnit, { 516 | config: config, 517 | 518 | // Initialize the configuration options 519 | init: function() { 520 | extend(config, { 521 | stats: { all: 0, bad: 0 }, 522 | moduleStats: { all: 0, bad: 0 }, 523 | started: +new Date, 524 | updateRate: 1000, 525 | blocking: false, 526 | autostart: true, 527 | autorun: false, 528 | filter: "", 529 | queue: [], 530 | semaphore: 0 531 | }); 532 | 533 | var tests = id( "qunit-tests" ), 534 | banner = id( "qunit-banner" ), 535 | result = id( "qunit-testresult" ); 536 | 537 | if ( tests ) { 538 | tests.innerHTML = ""; 539 | } 540 | 541 | if ( banner ) { 542 | banner.className = ""; 543 | } 544 | 545 | if ( result ) { 546 | result.parentNode.removeChild( result ); 547 | } 548 | 549 | if ( tests ) { 550 | result = document.createElement( "p" ); 551 | result.id = "qunit-testresult"; 552 | result.className = "result"; 553 | tests.parentNode.insertBefore( result, tests ); 554 | result.innerHTML = 'Running...
 '; 555 | } 556 | }, 557 | 558 | /** 559 | * Resets the test setup. Useful for tests that modify the DOM. 560 | * 561 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 562 | */ 563 | reset: function() { 564 | if ( window.jQuery ) { 565 | jQuery( "#qunit-fixture" ).html( config.fixture ); 566 | } else { 567 | var main = id( 'qunit-fixture' ); 568 | if ( main ) { 569 | main.innerHTML = config.fixture; 570 | } 571 | } 572 | }, 573 | 574 | /** 575 | * Trigger an event on an element. 576 | * 577 | * @example triggerEvent( document.body, "click" ); 578 | * 579 | * @param DOMElement elem 580 | * @param String type 581 | */ 582 | triggerEvent: function( elem, type, event ) { 583 | if ( document.createEvent ) { 584 | event = document.createEvent("MouseEvents"); 585 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 586 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 587 | elem.dispatchEvent( event ); 588 | 589 | } else if ( elem.fireEvent ) { 590 | elem.fireEvent("on"+type); 591 | } 592 | }, 593 | 594 | // Safe object type checking 595 | is: function( type, obj ) { 596 | return QUnit.objectType( obj ) == type; 597 | }, 598 | 599 | objectType: function( obj ) { 600 | if (typeof obj === "undefined") { 601 | return "undefined"; 602 | 603 | // consider: typeof null === object 604 | } 605 | if (obj === null) { 606 | return "null"; 607 | } 608 | 609 | var type = Object.prototype.toString.call( obj ) 610 | .match(/^\[object\s(.*)\]$/)[1] || ''; 611 | 612 | switch (type) { 613 | case 'Number': 614 | if (isNaN(obj)) { 615 | return "nan"; 616 | } else { 617 | return "number"; 618 | } 619 | case 'String': 620 | case 'Boolean': 621 | case 'Array': 622 | case 'Date': 623 | case 'RegExp': 624 | case 'Function': 625 | return type.toLowerCase(); 626 | } 627 | if (typeof obj === "object") { 628 | return "object"; 629 | } 630 | return undefined; 631 | }, 632 | 633 | push: function(result, actual, expected, message) { 634 | var details = { 635 | result: result, 636 | message: message, 637 | actual: actual, 638 | expected: expected 639 | }; 640 | 641 | message = escapeInnerText(message) || (result ? "okay" : "failed"); 642 | message = '' + message + ""; 643 | expected = escapeInnerText(QUnit.jsDump.parse(expected)); 644 | actual = escapeInnerText(QUnit.jsDump.parse(actual)); 645 | var output = message + ''; 646 | if (actual != expected) { 647 | output += ''; 648 | output += ''; 649 | } 650 | if (!result) { 651 | var source = sourceFromStacktrace(); 652 | if (source) { 653 | details.source = source; 654 | output += ''; 655 | } 656 | } 657 | output += "
Expected:
' + expected + '
Result:
' + actual + '
Diff:
' + QUnit.diff(expected, actual) +'
Source:
' + escapeInnerText(source) + '
"; 658 | 659 | runLoggingCallbacks( 'log', QUnit, details ); 660 | 661 | config.current.assertions.push({ 662 | result: !!result, 663 | message: output 664 | }); 665 | }, 666 | 667 | url: function( params ) { 668 | params = extend( extend( {}, QUnit.urlParams ), params ); 669 | var querystring = "?", 670 | key; 671 | for ( key in params ) { 672 | querystring += encodeURIComponent( key ) + "=" + 673 | encodeURIComponent( params[ key ] ) + "&"; 674 | } 675 | return window.location.pathname + querystring.slice( 0, -1 ); 676 | }, 677 | 678 | extend: extend, 679 | id: id, 680 | addEvent: addEvent 681 | }); 682 | 683 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later 684 | //Doing this allows us to tell if the following methods have been overwritten on the actual 685 | //QUnit object, which is a deprecated way of using the callbacks. 686 | extend(QUnit.constructor.prototype, { 687 | // Logging callbacks; all receive a single argument with the listed properties 688 | // run test/logs.html for any related changes 689 | begin: registerLoggingCallback('begin'), 690 | // done: { failed, passed, total, runtime } 691 | done: registerLoggingCallback('done'), 692 | // log: { result, actual, expected, message } 693 | log: registerLoggingCallback('log'), 694 | // testStart: { name } 695 | testStart: registerLoggingCallback('testStart'), 696 | // testDone: { name, failed, passed, total } 697 | testDone: registerLoggingCallback('testDone'), 698 | // moduleStart: { name } 699 | moduleStart: registerLoggingCallback('moduleStart'), 700 | // moduleDone: { name, failed, passed, total } 701 | moduleDone: registerLoggingCallback('moduleDone') 702 | }); 703 | 704 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 705 | config.autorun = true; 706 | } 707 | 708 | QUnit.load = function() { 709 | runLoggingCallbacks( 'begin', QUnit, {} ); 710 | 711 | // Initialize the config, saving the execution queue 712 | var oldconfig = extend({}, config); 713 | QUnit.init(); 714 | extend(config, oldconfig); 715 | 716 | config.blocking = false; 717 | 718 | var urlConfigHtml = '', len = config.urlConfig.length; 719 | for ( var i = 0, val; i < len, val = config.urlConfig[i]; i++ ) { 720 | config[val] = QUnit.urlParams[val]; 721 | urlConfigHtml += ''; 722 | } 723 | 724 | var userAgent = id("qunit-userAgent"); 725 | if ( userAgent ) { 726 | userAgent.innerHTML = navigator.userAgent; 727 | } 728 | var banner = id("qunit-header"); 729 | if ( banner ) { 730 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; 731 | addEvent( banner, "change", function( event ) { 732 | var params = {}; 733 | params[ event.target.name ] = event.target.checked ? true : undefined; 734 | window.location = QUnit.url( params ); 735 | }); 736 | } 737 | 738 | var toolbar = id("qunit-testrunner-toolbar"); 739 | if ( toolbar ) { 740 | var filter = document.createElement("input"); 741 | filter.type = "checkbox"; 742 | filter.id = "qunit-filter-pass"; 743 | addEvent( filter, "click", function() { 744 | var ol = document.getElementById("qunit-tests"); 745 | if ( filter.checked ) { 746 | ol.className = ol.className + " hidepass"; 747 | } else { 748 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 749 | ol.className = tmp.replace(/ hidepass /, " "); 750 | } 751 | if ( defined.sessionStorage ) { 752 | if (filter.checked) { 753 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 754 | } else { 755 | sessionStorage.removeItem("qunit-filter-passed-tests"); 756 | } 757 | } 758 | }); 759 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 760 | filter.checked = true; 761 | var ol = document.getElementById("qunit-tests"); 762 | ol.className = ol.className + " hidepass"; 763 | } 764 | toolbar.appendChild( filter ); 765 | 766 | var label = document.createElement("label"); 767 | label.setAttribute("for", "qunit-filter-pass"); 768 | label.innerHTML = "Hide passed tests"; 769 | toolbar.appendChild( label ); 770 | } 771 | 772 | var main = id('qunit-fixture'); 773 | if ( main ) { 774 | config.fixture = main.innerHTML; 775 | } 776 | 777 | if (config.autostart) { 778 | QUnit.start(); 779 | } 780 | }; 781 | 782 | addEvent(window, "load", QUnit.load); 783 | 784 | function done() { 785 | config.autorun = true; 786 | 787 | // Log the last module results 788 | if ( config.currentModule ) { 789 | runLoggingCallbacks( 'moduleDone', QUnit, { 790 | name: config.currentModule, 791 | failed: config.moduleStats.bad, 792 | passed: config.moduleStats.all - config.moduleStats.bad, 793 | total: config.moduleStats.all 794 | } ); 795 | } 796 | 797 | var banner = id("qunit-banner"), 798 | tests = id("qunit-tests"), 799 | runtime = +new Date - config.started, 800 | passed = config.stats.all - config.stats.bad, 801 | html = [ 802 | 'Tests completed in ', 803 | runtime, 804 | ' milliseconds.
', 805 | '', 806 | passed, 807 | ' tests of ', 808 | config.stats.all, 809 | ' passed, ', 810 | config.stats.bad, 811 | ' failed.' 812 | ].join(''); 813 | 814 | if ( banner ) { 815 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 816 | } 817 | 818 | if ( tests ) { 819 | id( "qunit-testresult" ).innerHTML = html; 820 | } 821 | 822 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 823 | // show ✖ for good, ✔ for bad suite result in title 824 | // use escape sequences in case file gets loaded with non-utf-8-charset 825 | document.title = [ 826 | (config.stats.bad ? "\u2716" : "\u2714"), 827 | document.title.replace(/^[\u2714\u2716] /i, "") 828 | ].join(" "); 829 | } 830 | 831 | runLoggingCallbacks( 'done', QUnit, { 832 | failed: config.stats.bad, 833 | passed: passed, 834 | total: config.stats.all, 835 | runtime: runtime 836 | } ); 837 | } 838 | 839 | function validTest( name ) { 840 | var filter = config.filter, 841 | run = false; 842 | 843 | if ( !filter ) { 844 | return true; 845 | } 846 | 847 | var not = filter.charAt( 0 ) === "!"; 848 | if ( not ) { 849 | filter = filter.slice( 1 ); 850 | } 851 | 852 | if ( name.indexOf( filter ) !== -1 ) { 853 | return !not; 854 | } 855 | 856 | if ( not ) { 857 | run = true; 858 | } 859 | 860 | return run; 861 | } 862 | 863 | // so far supports only Firefox, Chrome and Opera (buggy) 864 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 865 | function sourceFromStacktrace() { 866 | try { 867 | throw new Error(); 868 | } catch ( e ) { 869 | if (e.stacktrace) { 870 | // Opera 871 | return e.stacktrace.split("\n")[6]; 872 | } else if (e.stack) { 873 | // Firefox, Chrome 874 | return e.stack.split("\n")[4]; 875 | } else if (e.sourceURL) { 876 | // Safari, PhantomJS 877 | // TODO sourceURL points at the 'throw new Error' line above, useless 878 | //return e.sourceURL + ":" + e.line; 879 | } 880 | } 881 | } 882 | 883 | function escapeInnerText(s) { 884 | if (!s) { 885 | return ""; 886 | } 887 | s = s + ""; 888 | return s.replace(/[\&<>]/g, function(s) { 889 | switch(s) { 890 | case "&": return "&"; 891 | case "<": return "<"; 892 | case ">": return ">"; 893 | default: return s; 894 | } 895 | }); 896 | } 897 | 898 | function synchronize( callback ) { 899 | config.queue.push( callback ); 900 | 901 | if ( config.autorun && !config.blocking ) { 902 | process(); 903 | } 904 | } 905 | 906 | function process() { 907 | var start = (new Date()).getTime(); 908 | 909 | while ( config.queue.length && !config.blocking ) { 910 | if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { 911 | config.queue.shift()(); 912 | } else { 913 | window.setTimeout( process, 13 ); 914 | break; 915 | } 916 | } 917 | if (!config.blocking && !config.queue.length) { 918 | done(); 919 | } 920 | } 921 | 922 | function saveGlobal() { 923 | config.pollution = []; 924 | 925 | if ( config.noglobals ) { 926 | for ( var key in window ) { 927 | config.pollution.push( key ); 928 | } 929 | } 930 | } 931 | 932 | function checkPollution( name ) { 933 | var old = config.pollution; 934 | saveGlobal(); 935 | 936 | var newGlobals = diff( config.pollution, old ); 937 | if ( newGlobals.length > 0 ) { 938 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 939 | } 940 | 941 | var deletedGlobals = diff( old, config.pollution ); 942 | if ( deletedGlobals.length > 0 ) { 943 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 944 | } 945 | } 946 | 947 | // returns a new Array with the elements that are in a but not in b 948 | function diff( a, b ) { 949 | var result = a.slice(); 950 | for ( var i = 0; i < result.length; i++ ) { 951 | for ( var j = 0; j < b.length; j++ ) { 952 | if ( result[i] === b[j] ) { 953 | result.splice(i, 1); 954 | i--; 955 | break; 956 | } 957 | } 958 | } 959 | return result; 960 | } 961 | 962 | function fail(message, exception, callback) { 963 | if ( typeof console !== "undefined" && console.error && console.warn ) { 964 | console.error(message); 965 | console.error(exception); 966 | console.warn(callback.toString()); 967 | 968 | } else if ( window.opera && opera.postError ) { 969 | opera.postError(message, exception, callback.toString); 970 | } 971 | } 972 | 973 | function extend(a, b) { 974 | for ( var prop in b ) { 975 | if ( b[prop] === undefined ) { 976 | delete a[prop]; 977 | } else { 978 | a[prop] = b[prop]; 979 | } 980 | } 981 | 982 | return a; 983 | } 984 | 985 | function addEvent(elem, type, fn) { 986 | if ( elem.addEventListener ) { 987 | elem.addEventListener( type, fn, false ); 988 | } else if ( elem.attachEvent ) { 989 | elem.attachEvent( "on" + type, fn ); 990 | } else { 991 | fn(); 992 | } 993 | } 994 | 995 | function id(name) { 996 | return !!(typeof document !== "undefined" && document && document.getElementById) && 997 | document.getElementById( name ); 998 | } 999 | 1000 | function registerLoggingCallback(key){ 1001 | return function(callback){ 1002 | config[key].push( callback ); 1003 | }; 1004 | } 1005 | 1006 | // Supports deprecated method of completely overwriting logging callbacks 1007 | function runLoggingCallbacks(key, scope, args) { 1008 | //debugger; 1009 | var callbacks; 1010 | if ( QUnit.hasOwnProperty(key) ) { 1011 | QUnit[key].call(scope, args); 1012 | } else { 1013 | callbacks = config[key]; 1014 | for( var i = 0; i < callbacks.length; i++ ) { 1015 | callbacks[i].call( scope, args ); 1016 | } 1017 | } 1018 | } 1019 | 1020 | // Test for equality any JavaScript type. 1021 | // Author: Philippe Rathé 1022 | QUnit.equiv = function () { 1023 | 1024 | var innerEquiv; // the real equiv function 1025 | var callers = []; // stack to decide between skip/abort functions 1026 | var parents = []; // stack to avoiding loops from circular referencing 1027 | 1028 | // Call the o related callback with the given arguments. 1029 | function bindCallbacks(o, callbacks, args) { 1030 | var prop = QUnit.objectType(o); 1031 | if (prop) { 1032 | if (QUnit.objectType(callbacks[prop]) === "function") { 1033 | return callbacks[prop].apply(callbacks, args); 1034 | } else { 1035 | return callbacks[prop]; // or undefined 1036 | } 1037 | } 1038 | } 1039 | 1040 | var callbacks = function () { 1041 | 1042 | // for string, boolean, number and null 1043 | function useStrictEquality(b, a) { 1044 | if (b instanceof a.constructor || a instanceof b.constructor) { 1045 | // to catch short annotaion VS 'new' annotation of a 1046 | // declaration 1047 | // e.g. var i = 1; 1048 | // var j = new Number(1); 1049 | return a == b; 1050 | } else { 1051 | return a === b; 1052 | } 1053 | } 1054 | 1055 | return { 1056 | "string" : useStrictEquality, 1057 | "boolean" : useStrictEquality, 1058 | "number" : useStrictEquality, 1059 | "null" : useStrictEquality, 1060 | "undefined" : useStrictEquality, 1061 | 1062 | "nan" : function(b) { 1063 | return isNaN(b); 1064 | }, 1065 | 1066 | "date" : function(b, a) { 1067 | return QUnit.objectType(b) === "date" 1068 | && a.valueOf() === b.valueOf(); 1069 | }, 1070 | 1071 | "regexp" : function(b, a) { 1072 | return QUnit.objectType(b) === "regexp" 1073 | && a.source === b.source && // the regex itself 1074 | a.global === b.global && // and its modifers 1075 | // (gmi) ... 1076 | a.ignoreCase === b.ignoreCase 1077 | && a.multiline === b.multiline; 1078 | }, 1079 | 1080 | // - skip when the property is a method of an instance (OOP) 1081 | // - abort otherwise, 1082 | // initial === would have catch identical references anyway 1083 | "function" : function() { 1084 | var caller = callers[callers.length - 1]; 1085 | return caller !== Object && typeof caller !== "undefined"; 1086 | }, 1087 | 1088 | "array" : function(b, a) { 1089 | var i, j, loop; 1090 | var len; 1091 | 1092 | // b could be an object literal here 1093 | if (!(QUnit.objectType(b) === "array")) { 1094 | return false; 1095 | } 1096 | 1097 | len = a.length; 1098 | if (len !== b.length) { // safe and faster 1099 | return false; 1100 | } 1101 | 1102 | // track reference to avoid circular references 1103 | parents.push(a); 1104 | for (i = 0; i < len; i++) { 1105 | loop = false; 1106 | for (j = 0; j < parents.length; j++) { 1107 | if (parents[j] === a[i]) { 1108 | loop = true;// dont rewalk array 1109 | } 1110 | } 1111 | if (!loop && !innerEquiv(a[i], b[i])) { 1112 | parents.pop(); 1113 | return false; 1114 | } 1115 | } 1116 | parents.pop(); 1117 | return true; 1118 | }, 1119 | 1120 | "object" : function(b, a) { 1121 | var i, j, loop; 1122 | var eq = true; // unless we can proove it 1123 | var aProperties = [], bProperties = []; // collection of 1124 | // strings 1125 | 1126 | // comparing constructors is more strict than using 1127 | // instanceof 1128 | if (a.constructor !== b.constructor) { 1129 | return false; 1130 | } 1131 | 1132 | // stack constructor before traversing properties 1133 | callers.push(a.constructor); 1134 | // track reference to avoid circular references 1135 | parents.push(a); 1136 | 1137 | for (i in a) { // be strict: don't ensures hasOwnProperty 1138 | // and go deep 1139 | loop = false; 1140 | for (j = 0; j < parents.length; j++) { 1141 | if (parents[j] === a[i]) 1142 | loop = true; // don't go down the same path 1143 | // twice 1144 | } 1145 | aProperties.push(i); // collect a's properties 1146 | 1147 | if (!loop && !innerEquiv(a[i], b[i])) { 1148 | eq = false; 1149 | break; 1150 | } 1151 | } 1152 | 1153 | callers.pop(); // unstack, we are done 1154 | parents.pop(); 1155 | 1156 | for (i in b) { 1157 | bProperties.push(i); // collect b's properties 1158 | } 1159 | 1160 | // Ensures identical properties name 1161 | return eq 1162 | && innerEquiv(aProperties.sort(), bProperties 1163 | .sort()); 1164 | } 1165 | }; 1166 | }(); 1167 | 1168 | innerEquiv = function() { // can take multiple arguments 1169 | var args = Array.prototype.slice.apply(arguments); 1170 | if (args.length < 2) { 1171 | return true; // end transition 1172 | } 1173 | 1174 | return (function(a, b) { 1175 | if (a === b) { 1176 | return true; // catch the most you can 1177 | } else if (a === null || b === null || typeof a === "undefined" 1178 | || typeof b === "undefined" 1179 | || QUnit.objectType(a) !== QUnit.objectType(b)) { 1180 | return false; // don't lose time with error prone cases 1181 | } else { 1182 | return bindCallbacks(a, callbacks, [ b, a ]); 1183 | } 1184 | 1185 | // apply transition with (1..n) arguments 1186 | })(args[0], args[1]) 1187 | && arguments.callee.apply(this, args.splice(1, 1188 | args.length - 1)); 1189 | }; 1190 | 1191 | return innerEquiv; 1192 | 1193 | }(); 1194 | 1195 | /** 1196 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1197 | * http://flesler.blogspot.com Licensed under BSD 1198 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1199 | * 1200 | * @projectDescription Advanced and extensible data dumping for Javascript. 1201 | * @version 1.0.0 1202 | * @author Ariel Flesler 1203 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1204 | */ 1205 | QUnit.jsDump = (function() { 1206 | function quote( str ) { 1207 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1208 | }; 1209 | function literal( o ) { 1210 | return o + ''; 1211 | }; 1212 | function join( pre, arr, post ) { 1213 | var s = jsDump.separator(), 1214 | base = jsDump.indent(), 1215 | inner = jsDump.indent(1); 1216 | if ( arr.join ) 1217 | arr = arr.join( ',' + s + inner ); 1218 | if ( !arr ) 1219 | return pre + post; 1220 | return [ pre, inner + arr, base + post ].join(s); 1221 | }; 1222 | function array( arr, stack ) { 1223 | var i = arr.length, ret = Array(i); 1224 | this.up(); 1225 | while ( i-- ) 1226 | ret[i] = this.parse( arr[i] , undefined , stack); 1227 | this.down(); 1228 | return join( '[', ret, ']' ); 1229 | }; 1230 | 1231 | var reName = /^function (\w+)/; 1232 | 1233 | var jsDump = { 1234 | parse:function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1235 | stack = stack || [ ]; 1236 | var parser = this.parsers[ type || this.typeOf(obj) ]; 1237 | type = typeof parser; 1238 | var inStack = inArray(obj, stack); 1239 | if (inStack != -1) { 1240 | return 'recursion('+(inStack - stack.length)+')'; 1241 | } 1242 | //else 1243 | if (type == 'function') { 1244 | stack.push(obj); 1245 | var res = parser.call( this, obj, stack ); 1246 | stack.pop(); 1247 | return res; 1248 | } 1249 | // else 1250 | return (type == 'string') ? parser : this.parsers.error; 1251 | }, 1252 | typeOf:function( obj ) { 1253 | var type; 1254 | if ( obj === null ) { 1255 | type = "null"; 1256 | } else if (typeof obj === "undefined") { 1257 | type = "undefined"; 1258 | } else if (QUnit.is("RegExp", obj)) { 1259 | type = "regexp"; 1260 | } else if (QUnit.is("Date", obj)) { 1261 | type = "date"; 1262 | } else if (QUnit.is("Function", obj)) { 1263 | type = "function"; 1264 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1265 | type = "window"; 1266 | } else if (obj.nodeType === 9) { 1267 | type = "document"; 1268 | } else if (obj.nodeType) { 1269 | type = "node"; 1270 | } else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) { 1271 | type = "array"; 1272 | } else { 1273 | type = typeof obj; 1274 | } 1275 | return type; 1276 | }, 1277 | separator:function() { 1278 | return this.multiline ? this.HTML ? '
' : '\n' : this.HTML ? ' ' : ' '; 1279 | }, 1280 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1281 | if ( !this.multiline ) 1282 | return ''; 1283 | var chr = this.indentChar; 1284 | if ( this.HTML ) 1285 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1286 | return Array( this._depth_ + (extra||0) ).join(chr); 1287 | }, 1288 | up:function( a ) { 1289 | this._depth_ += a || 1; 1290 | }, 1291 | down:function( a ) { 1292 | this._depth_ -= a || 1; 1293 | }, 1294 | setParser:function( name, parser ) { 1295 | this.parsers[name] = parser; 1296 | }, 1297 | // The next 3 are exposed so you can use them 1298 | quote:quote, 1299 | literal:literal, 1300 | join:join, 1301 | // 1302 | _depth_: 1, 1303 | // This is the list of parsers, to modify them, use jsDump.setParser 1304 | parsers:{ 1305 | window: '[Window]', 1306 | document: '[Document]', 1307 | error:'[ERROR]', //when no parser is found, shouldn't happen 1308 | unknown: '[Unknown]', 1309 | 'null':'null', 1310 | 'undefined':'undefined', 1311 | 'function':function( fn ) { 1312 | var ret = 'function', 1313 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1314 | if ( name ) 1315 | ret += ' ' + name; 1316 | ret += '('; 1317 | 1318 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1319 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1320 | }, 1321 | array: array, 1322 | nodelist: array, 1323 | arguments: array, 1324 | object:function( map, stack ) { 1325 | var ret = [ ]; 1326 | QUnit.jsDump.up(); 1327 | for ( var key in map ) { 1328 | var val = map[key]; 1329 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack)); 1330 | } 1331 | QUnit.jsDump.down(); 1332 | return join( '{', ret, '}' ); 1333 | }, 1334 | node:function( node ) { 1335 | var open = QUnit.jsDump.HTML ? '<' : '<', 1336 | close = QUnit.jsDump.HTML ? '>' : '>'; 1337 | 1338 | var tag = node.nodeName.toLowerCase(), 1339 | ret = open + tag; 1340 | 1341 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1342 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1343 | if ( val ) 1344 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1345 | } 1346 | return ret + close + open + '/' + tag + close; 1347 | }, 1348 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1349 | var l = fn.length; 1350 | if ( !l ) return ''; 1351 | 1352 | var args = Array(l); 1353 | while ( l-- ) 1354 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1355 | return ' ' + args.join(', ') + ' '; 1356 | }, 1357 | key:quote, //object calls it internally, the key part of an item in a map 1358 | functionCode:'[code]', //function calls it internally, it's the content of the function 1359 | attribute:quote, //node calls it internally, it's an html attribute value 1360 | string:quote, 1361 | date:quote, 1362 | regexp:literal, //regex 1363 | number:literal, 1364 | 'boolean':literal 1365 | }, 1366 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1367 | id:'id', 1368 | name:'name', 1369 | 'class':'className' 1370 | }, 1371 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1372 | indentChar:' ',//indentation unit 1373 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1374 | }; 1375 | 1376 | return jsDump; 1377 | })(); 1378 | 1379 | // from Sizzle.js 1380 | function getText( elems ) { 1381 | var ret = "", elem; 1382 | 1383 | for ( var i = 0; elems[i]; i++ ) { 1384 | elem = elems[i]; 1385 | 1386 | // Get the text from text nodes and CDATA nodes 1387 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1388 | ret += elem.nodeValue; 1389 | 1390 | // Traverse everything else, except comment nodes 1391 | } else if ( elem.nodeType !== 8 ) { 1392 | ret += getText( elem.childNodes ); 1393 | } 1394 | } 1395 | 1396 | return ret; 1397 | }; 1398 | 1399 | //from jquery.js 1400 | function inArray( elem, array ) { 1401 | if ( array.indexOf ) { 1402 | return array.indexOf( elem ); 1403 | } 1404 | 1405 | for ( var i = 0, length = array.length; i < length; i++ ) { 1406 | if ( array[ i ] === elem ) { 1407 | return i; 1408 | } 1409 | } 1410 | 1411 | return -1; 1412 | } 1413 | 1414 | /* 1415 | * Javascript Diff Algorithm 1416 | * By John Resig (http://ejohn.org/) 1417 | * Modified by Chu Alan "sprite" 1418 | * 1419 | * Released under the MIT license. 1420 | * 1421 | * More Info: 1422 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1423 | * 1424 | * Usage: QUnit.diff(expected, actual) 1425 | * 1426 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1427 | */ 1428 | QUnit.diff = (function() { 1429 | function diff(o, n) { 1430 | var ns = {}; 1431 | var os = {}; 1432 | 1433 | for (var i = 0; i < n.length; i++) { 1434 | if (ns[n[i]] == null) 1435 | ns[n[i]] = { 1436 | rows: [], 1437 | o: null 1438 | }; 1439 | ns[n[i]].rows.push(i); 1440 | } 1441 | 1442 | for (var i = 0; i < o.length; i++) { 1443 | if (os[o[i]] == null) 1444 | os[o[i]] = { 1445 | rows: [], 1446 | n: null 1447 | }; 1448 | os[o[i]].rows.push(i); 1449 | } 1450 | 1451 | for (var i in ns) { 1452 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1453 | n[ns[i].rows[0]] = { 1454 | text: n[ns[i].rows[0]], 1455 | row: os[i].rows[0] 1456 | }; 1457 | o[os[i].rows[0]] = { 1458 | text: o[os[i].rows[0]], 1459 | row: ns[i].rows[0] 1460 | }; 1461 | } 1462 | } 1463 | 1464 | for (var i = 0; i < n.length - 1; i++) { 1465 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1466 | n[i + 1] == o[n[i].row + 1]) { 1467 | n[i + 1] = { 1468 | text: n[i + 1], 1469 | row: n[i].row + 1 1470 | }; 1471 | o[n[i].row + 1] = { 1472 | text: o[n[i].row + 1], 1473 | row: i + 1 1474 | }; 1475 | } 1476 | } 1477 | 1478 | for (var i = n.length - 1; i > 0; i--) { 1479 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1480 | n[i - 1] == o[n[i].row - 1]) { 1481 | n[i - 1] = { 1482 | text: n[i - 1], 1483 | row: n[i].row - 1 1484 | }; 1485 | o[n[i].row - 1] = { 1486 | text: o[n[i].row - 1], 1487 | row: i - 1 1488 | }; 1489 | } 1490 | } 1491 | 1492 | return { 1493 | o: o, 1494 | n: n 1495 | }; 1496 | } 1497 | 1498 | return function(o, n) { 1499 | o = o.replace(/\s+$/, ''); 1500 | n = n.replace(/\s+$/, ''); 1501 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1502 | 1503 | var str = ""; 1504 | 1505 | var oSpace = o.match(/\s+/g); 1506 | if (oSpace == null) { 1507 | oSpace = [" "]; 1508 | } 1509 | else { 1510 | oSpace.push(" "); 1511 | } 1512 | var nSpace = n.match(/\s+/g); 1513 | if (nSpace == null) { 1514 | nSpace = [" "]; 1515 | } 1516 | else { 1517 | nSpace.push(" "); 1518 | } 1519 | 1520 | if (out.n.length == 0) { 1521 | for (var i = 0; i < out.o.length; i++) { 1522 | str += '' + out.o[i] + oSpace[i] + ""; 1523 | } 1524 | } 1525 | else { 1526 | if (out.n[0].text == null) { 1527 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1528 | str += '' + out.o[n] + oSpace[n] + ""; 1529 | } 1530 | } 1531 | 1532 | for (var i = 0; i < out.n.length; i++) { 1533 | if (out.n[i].text == null) { 1534 | str += '' + out.n[i] + nSpace[i] + ""; 1535 | } 1536 | else { 1537 | var pre = ""; 1538 | 1539 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1540 | pre += '' + out.o[n] + oSpace[n] + ""; 1541 | } 1542 | str += " " + out.n[i].text + nSpace[i] + pre; 1543 | } 1544 | } 1545 | } 1546 | 1547 | return str; 1548 | }; 1549 | })(); 1550 | 1551 | })(this); 1552 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonml-tools", 3 | "version": "2.0.0", 4 | "description": "The original JsonML. JsonML-related tools for losslessly converting between XML/HTML and JSON, including mixed-mode XML.", 5 | "homepage": "http://jsonml.org", 6 | "author": "Stephen McKamey (http://mck.me)", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/mckamey/jsonml.git" 11 | }, 12 | "files": [ 13 | "jsonml-html.js", 14 | "jsonml-utils.js", 15 | "jsonml-dom.js", 16 | "jsonml-jbst.js", 17 | "jsonml-xml.js", 18 | "jsonml.xslt", 19 | "README.md", 20 | "LICENSE.txt" 21 | ], 22 | "main": "jsonml-html.js", 23 | "keywords": [ 24 | "jsonml", 25 | "json", 26 | "mixed-mode", 27 | "xml", 28 | "lossless", 29 | "roundtrip", 30 | "conversion", 31 | "jbst" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /test/unit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JsonML Unit Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

JsonML Unit Tests

16 |

17 |

18 |
    19 | 20 | 21 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | window.onerror = function(msg, url, line) { 2 | alert([msg, url, line].join(', ')); 3 | }; 4 | -------------------------------------------------------------------------------- /test/xmlTests.js: -------------------------------------------------------------------------------- 1 | /*global JsonML, module, test, same */ 2 | try{ 3 | 4 | module('jsonml-xml.js'); 5 | 6 | test('JsonML.parseXML/.renderXML roundtrip', function() { 7 | 8 | var expected = 9 | ''+ 10 | ''+ 11 | 'Gambardella, Matthew'+ 12 | 'XML Developer\'s Guide'+ 13 | ''+ 14 | ''+ 15 | 'Ralls, Kim'+ 16 | 'Midnight Rain'+ 17 | ''+ 18 | ''; 19 | 20 | var dom = JsonML.parseXML(expected); 21 | var actual = JsonML.renderXML(dom); 22 | 23 | same(actual, expected); 24 | }); 25 | 26 | test('JsonML.fromXMLText', function() { 27 | 28 | var expected = 29 | ['catalog', 30 | ['book', { id: 'bk101' }, 31 | ['author', 'Gambardella, Matthew'], 32 | ['title', 'XML Developer\'s Guide'] 33 | ], 34 | ['book', { id: 'bk102' }, 35 | ['author', 'Ralls, Kim'], 36 | ['title', 'Midnight Rain'] 37 | ] 38 | ]; 39 | 40 | var input = 41 | ''+ 42 | ''+ 43 | 'Gambardella, Matthew'+ 44 | 'XML Developer\'s Guide'+ 45 | ''+ 46 | ''+ 47 | 'Ralls, Kim'+ 48 | 'Midnight Rain'+ 49 | ''+ 50 | ''; 51 | 52 | var actual = JsonML.fromXMLText(input); 53 | 54 | same(actual, expected); 55 | }); 56 | 57 | test('JsonML.fromXML', function() { 58 | 59 | var expected = 60 | ['a', 61 | ['b', { b1k: 'b1v' }, 62 | ['c', 'c1'], 63 | ['d', 'd1'] 64 | ], 65 | ['b', { b2k: 'b2v' }, 66 | ['c', 'c2'], 67 | ['d', 'd2'] 68 | ] 69 | ]; 70 | 71 | // hack needed to get an XML document 72 | var xmlDoc = JsonML.parseXML(''); 73 | 74 | var input = xmlDoc.createElement('a'); 75 | input.appendChild(xmlDoc.createElement('b')); 76 | input.firstChild.setAttribute('b1k', 'b1v'); 77 | input.firstChild.appendChild(xmlDoc.createElement('c')); 78 | input.firstChild.firstChild.appendChild(xmlDoc.createTextNode('c1')); 79 | input.firstChild.appendChild(xmlDoc.createElement('d')); 80 | input.firstChild.lastChild.appendChild(xmlDoc.createTextNode('d1')); 81 | input.appendChild(xmlDoc.createElement('b')); 82 | input.lastChild.setAttribute('b2k', 'b2v'); 83 | input.lastChild.appendChild(xmlDoc.createElement('c')); 84 | input.lastChild.firstChild.appendChild(xmlDoc.createTextNode('c2')); 85 | input.lastChild.appendChild(xmlDoc.createElement('d')); 86 | input.lastChild.lastChild.appendChild(xmlDoc.createTextNode('d2')); 87 | 88 | var actual = JsonML.fromXML(input); 89 | 90 | same(actual, expected); 91 | }); 92 | 93 | test('JsonML.fromXML, namespaces', function() { 94 | 95 | var expected = 96 | ['a', { xmlns: 'http://ns.example.com', 'xmlns:foo': 'http://ns.example.com/foo' }, 97 | ['foo:b', { b1k: 'b1v' }, 98 | ['c', 'c1'], 99 | ['d', 'd1'] 100 | ], 101 | ['foo:b', { b2k: 'b2v' }, 102 | ['c', 'c2'], 103 | ['d', 'd2'] 104 | ] 105 | ]; 106 | 107 | // hack needed to get an XML document 108 | var xmlDoc = JsonML.parseXML(''); 109 | 110 | var input = xmlDoc.createElement('a'); 111 | input.setAttribute('xmlns', 'http://ns.example.com'); 112 | input.setAttribute('xmlns:foo', 'http://ns.example.com/foo'); 113 | input.appendChild(xmlDoc.createElement('foo:b')); 114 | input.firstChild.setAttribute('b1k', 'b1v'); 115 | input.firstChild.appendChild(xmlDoc.createElement('c')); 116 | input.firstChild.firstChild.appendChild(xmlDoc.createTextNode('c1')); 117 | input.firstChild.appendChild(xmlDoc.createElement('d')); 118 | input.firstChild.lastChild.appendChild(xmlDoc.createTextNode('d1')); 119 | input.appendChild(xmlDoc.createElement('foo:b')); 120 | input.lastChild.setAttribute('b2k', 'b2v'); 121 | input.lastChild.appendChild(xmlDoc.createElement('c')); 122 | input.lastChild.firstChild.appendChild(xmlDoc.createTextNode('c2')); 123 | input.lastChild.appendChild(xmlDoc.createElement('d')); 124 | input.lastChild.lastChild.appendChild(xmlDoc.createTextNode('d2')); 125 | 126 | var actual = JsonML.fromXML(input); 127 | 128 | same(actual, expected); 129 | }); 130 | 131 | test('JsonML.toXMLText', function() { 132 | 133 | var expected = 134 | ''+ 135 | ''+ 136 | 'Gambardella, Matthew'+ 137 | 'XML Developer\'s Guide'+ 138 | ''+ 139 | ''+ 140 | 'Ralls, Kim'+ 141 | 'Midnight Rain'+ 142 | ''+ 143 | ''; 144 | 145 | var input = 146 | ['catalog', 147 | ['book', { id: 'bk101' }, 148 | ['author', 'Gambardella, Matthew'], 149 | ['title', 'XML Developer\'s Guide'] 150 | ], 151 | ['book', { id: 'bk102' }, 152 | ['author', 'Ralls, Kim'], 153 | ['title', 'Midnight Rain'] 154 | ] 155 | ]; 156 | 157 | var actual = JsonML.toXMLText(input); 158 | 159 | same(actual, expected); 160 | }); 161 | 162 | test('JsonML.toXMLText, namespaces', function() { 163 | 164 | var expected = 165 | ''+ 166 | ''+ 167 | 'Gambardella, Matthew'+ 168 | 'XML Developer\'s Guide'+ 169 | ''+ 170 | ''+ 171 | 'Ralls, Kim'+ 172 | 'Midnight Rain'+ 173 | ''+ 174 | ''; 175 | 176 | var input = 177 | ['catalog', { xmlns: 'http://ns.example.com', 'xmlns:foo': 'http://ns.example.com/foo' }, 178 | ['foo:book', { id: 'bk101' }, 179 | ['author', 'Gambardella, Matthew'], 180 | ['title', 'XML Developer\'s Guide'] 181 | ], 182 | ['foo:book', { id: 'bk102' }, 183 | ['author', 'Ralls, Kim'], 184 | ['title', 'Midnight Rain'] 185 | ] 186 | ]; 187 | 188 | var actual = JsonML.toXMLText(input); 189 | 190 | same(actual, expected); 191 | }); 192 | 193 | test('JsonML.toXML', function() { 194 | 195 | // hack needed to get an XML document 196 | var xmlDoc = JsonML.parseXML(''); 197 | 198 | var expected = xmlDoc.createElement('a'); 199 | expected.appendChild(xmlDoc.createElement('b')); 200 | expected.firstChild.setAttribute('b1k', 'b1v'); 201 | expected.firstChild.appendChild(xmlDoc.createElement('c')); 202 | expected.firstChild.firstChild.appendChild(xmlDoc.createTextNode('c1')); 203 | expected.firstChild.appendChild(xmlDoc.createElement('d')); 204 | expected.firstChild.lastChild.appendChild(xmlDoc.createTextNode('d1')); 205 | expected.appendChild(xmlDoc.createElement('b')); 206 | expected.lastChild.setAttribute('b2k', 'b2v'); 207 | expected.lastChild.appendChild(xmlDoc.createElement('c')); 208 | expected.lastChild.firstChild.appendChild(xmlDoc.createTextNode('c2')); 209 | expected.lastChild.appendChild(xmlDoc.createElement('d')); 210 | expected.lastChild.lastChild.appendChild(xmlDoc.createTextNode('d2')); 211 | 212 | var input = 213 | ['a', 214 | ['b', { b1k: 'b1v' }, 215 | ['c', 'c1'], 216 | ['d', 'd1'] 217 | ], 218 | ['b', { b2k: 'b2v' }, 219 | ['c', 'c2'], 220 | ['d', 'd2'] 221 | ] 222 | ]; 223 | 224 | var actual = JsonML.toXML(input); 225 | 226 | // must compare strings or will get security exception 227 | same(JsonML.renderXML(actual), JsonML.renderXML(expected)); 228 | }); 229 | 230 | test('JsonML.fromXMLText/.toXMLText roundtrip', function() { 231 | 232 | var expected = 233 | ''+ 234 | ''+ 235 | ''+ 236 | 'QWZ5671'+ 237 | '39.95'+ 238 | ''+ 239 | 'Red'+ 240 | 'Burgundy'+ 241 | ''+ 242 | ''+ 243 | 'Red'+ 244 | 'Burgundy'+ 245 | ''+ 246 | ''+ 247 | ''+ 248 | 'RRX9856'+ 249 | 'Dec 25, 1995'+ 250 | '42.50'+ 251 | ''+ 252 | 'Black'+ 253 | ''+ 254 | ''+ 255 | ''+ 256 | ''; 257 | 258 | // JsonML will strip the XML Declaration 259 | var input = '' + expected; 260 | 261 | var jml = JsonML.fromXMLText(input); 262 | var actual = JsonML.toXMLText(jml); 263 | 264 | same(actual, expected); 265 | }); 266 | 267 | test('JsonML.fromXMLText/.toXMLText roundtrip, namespaces', function() { 268 | 269 | var expected = 270 | ''+ 271 | ''+ 272 | ''+ 273 | 'QWZ5671'+ 274 | '39.95'+ 275 | ''+ 276 | 'Red'+ 277 | 'Burgundy'+ 278 | ''+ 279 | ''+ 280 | 'Red'+ 281 | 'Burgundy'+ 282 | ''+ 283 | ''+ 284 | ''+ 285 | 'RRX9856'+ 286 | 'Dec 25, 1995'+ 287 | '42.50'+ 288 | ''+ 289 | 'Black'+ 290 | ''+ 291 | ''+ 292 | ''+ 293 | ''; 294 | 295 | // JsonML will strip the XML Declaration 296 | var input = '' + expected; 297 | 298 | var jml = JsonML.fromXMLText(input); 299 | var actual = JsonML.toXMLText(jml); 300 | 301 | same(actual, expected); 302 | }); 303 | 304 | test('JsonML.fromXMLText/.toXMLText roundtrip, CDATA', function() { 305 | 306 | var expected = 307 | ''+ 308 | ''+ 312 | ''; 313 | 314 | var input = 315 | ''+ 316 | ''+ 320 | ''; 321 | 322 | var jml = JsonML.fromXMLText(input); 323 | var actual = JsonML.toXMLText(jml); 324 | 325 | same(actual, expected); 326 | }); 327 | 328 | test('JsonML.fromXMLText/.toXMLText roundtrip, comments', function() { 329 | 330 | var expected = ''; 331 | 332 | var input = 333 | ''+ 334 | ''+ 335 | ''; 336 | 337 | var jml = JsonML.fromXMLText(input); 338 | var actual = JsonML.toXMLText(jml); 339 | 340 | same(actual, expected); 341 | }); 342 | 343 | }catch(ex){alert(ex);} 344 | --------------------------------------------------------------------------------