/*
jsonml-html.js
JsonML to HTML utility
Created: 2006-11-09-0116
Modified: 2012-11-24-1051
Copyright (c)2006-2012 Stephen M. McKamey
Distributed under The MIT License: http://jsonml.org/license
This file ensures a global JsonML object adding these methods:
JsonML.toHTML(JsonML, filter)
This method produces a tree of DOM elements from a JsonML tree. The
array must not contain any cyclical references.
The optional filter parameter is a function which can filter and
transform the results. It receives each of the DOM nodes, and
its return value is used instead of the original value. If it
returns what it received, then structure is not modified. If it
returns undefined then the member is deleted.
This is useful for binding unobtrusive JavaScript to the generated
DOM elements.
Example:
// Parses the structure. If an element has a specific CSS value then
// takes appropriate action: Remove from results, add special event
// handlers, or bind to a custom component.
var myUI = JsonML.toHTML(myUITemplate, function (elem) {
if (elem.className.indexOf('Remove-Me') >= 0) {
// this will remove from resulting DOM tree
return null;
}
if (elem.tagName && elem.tagName.toLowerCase() === 'a' &&
elem.className.indexOf('External-Link') >= 0) {
// this is the equivalent of target='_blank'
elem.onclick = function(evt) {
window.open(elem.href); return false;
};
} else if (elem.className.indexOf('Fancy-Widgit') >= 0) {
// bind to a custom component
FancyWidgit.bindDOM(elem);
}
return elem;
});
JsonML.toHTMLText(JsonML)
Converts JsonML to HTML text
// Implement onerror to handle any runtime errors while binding:
JsonML.onerror = function (ex, jml, filter) {
// display inline error message
return document.createTextNode('['+ex+']');
};
*/
var JsonML = JsonML || {};
if (typeof module === 'object') {
module.exports = JsonML;
}
(function(JsonML, document) {
'use strict';
/**
* Attribute name map
*
* @private
* @constant
* @type {Object.}
*/
var ATTR_MAP = {
'accesskey': 'accessKey',
'bgcolor': 'bgColor',
'cellpadding': 'cellPadding',
'cellspacing': 'cellSpacing',
'checked': 'defaultChecked',
'class': 'className',
'colspan': 'colSpan',
'contenteditable': 'contentEditable',
'defaultchecked': 'defaultChecked',
'for': 'htmlFor',
'formnovalidate': 'formNoValidate',
'hidefocus': 'hideFocus',
'ismap': 'isMap',
'maxlength': 'maxLength',
'novalidate': 'noValidate',
'readonly': 'readOnly',
'rowspan': 'rowSpan',
'spellcheck': 'spellCheck',
'tabindex': 'tabIndex',
'usemap': 'useMap',
'willvalidate': 'willValidate'
// can add more attributes here as needed
};
/**
* Attribute duplicates map
*
* @private
* @constant
* @type {Object.}
*/
var ATTR_DUP = {
'enctype': 'encoding',
'onscroll': 'DOMMouseScroll'
// can add more attributes here as needed
};
/**
* Attributes to be set via DOM
*
* @private
* @constant
* @type {Object.}
*/
var ATTR_DOM = {
'autocapitalize': 1,
'autocomplete': 1,
'autocorrect': 1
// can add more attributes here as needed
};
/**
* Boolean attribute map
*
* @private
* @constant
* @type {Object.}
*/
var ATTR_BOOL = {
'async': 1,
'autofocus': 1,
'checked': 1,
'defaultchecked': 1,
'defer': 1,
'disabled': 1,
'formnovalidate': 1,
'hidden': 1,
'indeterminate': 1,
'ismap': 1,
'multiple': 1,
'novalidate': 1,
'readonly': 1,
'required': 1,
'spellcheck': 1,
'willvalidate': 1
// can add more attributes here as needed
};
/**
* Leading SGML line ending pattern
*
* @private
* @constant
* @type {RegExp}
*/
var LEADING = /^[\r\n]+/;
/**
* Trailing SGML line ending pattern
*
* @private
* @constant
* @type {RegExp}
*/
var TRAILING = /[\r\n]+$/;
/**
* @private
* @const
* @type {number}
*/
var NUL = 0;
/**
* @private
* @const
* @type {number}
*/
var FUN = 1;
/**
* @private
* @const
* @type {number}
*/
var ARY = 2;
/**
* @private
* @const
* @type {number}
*/
var OBJ = 3;
/**
* @private
* @const
* @type {number}
*/
var VAL = 4;
/**
* @private
* @const
* @type {number}
*/
var RAW = 5;
/**
* Wraps a data value to maintain as raw markup in output
*
* @private
* @this {Markup}
* @param {string} value The value
* @constructor
*/
function Markup(value) {
/**
* @type {string}
* @const
* @protected
*/
this.value = value;
}
/**
* Renders the value
*
* @public
* @override
* @this {Markup}
* @return {string} value
*/
Markup.prototype.toString = function() {
return this.value;
};
/**
* @param {string} value
* @return {Markup}
*/
JsonML.raw = function(value) {
return new Markup(value);
};
/**
* @param {*} value
* @return {boolean}
*/
var isMarkup = JsonML.isRaw = function(value) {
return (value instanceof Markup);
};
/**
* Determines if the value is an Array
*
* @private
* @param {*} val the object being tested
* @return {boolean}
*/
var isArray = Array.isArray || function(val) {
return (val instanceof Array);
};
/**
* Determines if the value is a function
*
* @private
* @param {*} val the object being tested
* @return {boolean}
*/
function isFunction(val) {
return (typeof val === 'function');
}
/**
* Determines the type of the value
*
* @private
* @param {*} val the object being tested
* @return {number}
*/
function getType(val) {
switch (typeof val) {
case 'object':
return !val ? NUL : (isArray(val) ? ARY : (isMarkup(val) ? RAW : ((val instanceof Date) ? VAL : OBJ)));
case 'function':
return FUN;
case 'undefined':
return NUL;
default:
return VAL;
}
}
/**
* Creates a DOM element
*
* @private
* @param {string} tag The element's tag name
* @return {Node}
*/
var createElement = function(tag) {
if (!tag) {
// create a document fragment to hold multiple-root elements
if (document.createDocumentFragment) {
return document.createDocumentFragment();
}
tag = '';
} else if (tag.charAt(0) === '!') {
return document.createComment(tag === '!' ? '' : tag.substr(1)+' ');
}
if (tag.toLowerCase() === 'style' && document.createStyleSheet) {
// IE requires this interface for styles
return document.createStyleSheet();
}
return document.createElement(tag);
};
/**
* Adds an event handler to an element
*
* @private
* @param {Node} elem The element
* @param {string} name The event name
* @param {function(Event)} handler The event handler
*/
var addHandler = function(elem, name, handler) {
if (name.substr(0,2) === 'on') {
name = name.substr(2);
}
switch (typeof handler) {
case 'function':
if (elem.addEventListener) {
// DOM Level 2
elem.addEventListener(name, handler, false);
} else if (elem.attachEvent && getType(elem[name]) !== NUL) {
// IE legacy events
elem.attachEvent('on'+name, handler);
} else {
// DOM Level 0
var old = elem['on'+name] || elem[name];
elem['on'+name] = elem[name] = !isFunction(old) ? handler :
function(e) {
return (old.call(this, e) !== false) && (handler.call(this, e) !== false);
};
}
break;
case 'string':
// inline functions are DOM Level 0
/*jslint evil:true */
elem['on'+name] = new Function('event', handler);
/*jslint evil:false */
break;
}
};
/**
* Appends an attribute to an element
*
* @private
* @param {Node} elem The element
* @param {Object} attr Attributes object
* @return {Node}
*/
var addAttributes = function(elem, attr) {
if (attr.name && document.attachEvent && !elem.parentNode) {
try {
// IE fix for not being able to programatically change the name attribute
var alt = createElement('<'+elem.tagName+' name="'+attr.name+'">');
// fix for Opera 8.5 and Netscape 7.1 creating malformed elements
if (elem.tagName === alt.tagName) {
elem = alt;
}
} catch (ex) { }
}
// for each attributeName
for (var name in attr) {
if (attr.hasOwnProperty(name)) {
// attributeValue
var value = attr[name],
type = getType(value);
if (name) {
if (type === NUL) {
value = '';
type = VAL;
}
name = ATTR_MAP[name.toLowerCase()] || name;
if (name === 'style') {
if (getType(elem.style.cssText) !== NUL) {
elem.style.cssText = value;
} else {
elem.style = value;
}
} else if (name.substr(0,2) === 'on') {
addHandler(elem, name, value);
// also set duplicated events
name = ATTR_DUP[name];
if (name) {
addHandler(elem, name, value);
}
} else if (!ATTR_DOM[name.toLowerCase()] && (type !== VAL || name.charAt(0) === '$' || getType(elem[name]) !== NUL || getType(elem[ATTR_DUP[name]]) !== NUL)) {
// direct setting of existing properties
elem[name] = value;
// also set duplicated properties
name = ATTR_DUP[name];
if (name) {
elem[name] = value;
}
} else if (ATTR_BOOL[name.toLowerCase()]) {
if (value) {
// boolean attributes
elem.setAttribute(name, name);
// also set duplicated attributes
name = ATTR_DUP[name];
if (name) {
elem.setAttribute(name, name);
}
}
} else {
// http://www.quirksmode.org/dom/w3c_core.html#attributes
// custom and 'data-*' attributes
elem.setAttribute(name, value);
// also set duplicated attributes
name = ATTR_DUP[name];
if (name) {
elem.setAttribute(name, value);
}
}
}
}
}
return elem;
};
/**
* Appends a child to an element
*
* @private
* @param {Node} elem The parent element
* @param {Node} child The child
*/
var appendDOM = function(elem, child) {
if (child) {
var tag = (elem.tagName||'').toLowerCase();
if (elem.nodeType === 8) { // comment
if (child.nodeType === 3) { // text node
elem.nodeValue += child.nodeValue;
}
} else if (tag === 'table' && elem.tBodies) {
if (!child.tagName) {
// must unwrap documentFragment for tables
if (child.nodeType === 11) {
while (child.firstChild) {
appendDOM(elem, child.removeChild(child.firstChild));
}
}
return;
}
// in IE must explicitly nest TRs in TBODY
var childTag = child.tagName.toLowerCase();// child tagName
if (childTag && childTag !== 'tbody' && childTag !== 'thead') {
// insert in last tbody
var tBody = elem.tBodies.length > 0 ? elem.tBodies[elem.tBodies.length-1] : null;
if (!tBody) {
tBody = createElement(childTag === 'th' ? 'thead' : 'tbody');
elem.appendChild(tBody);
}
tBody.appendChild(child);
} else if (elem.canHaveChildren !== false) {
elem.appendChild(child);
}
} else if (tag === 'style' && document.createStyleSheet) {
// IE requires this interface for styles
elem.cssText = child;
} else if (elem.canHaveChildren !== false) {
elem.appendChild(child);
} else if (tag === 'object' &&
child.tagName && child.tagName.toLowerCase() === 'param') {
// IE-only path
try {
elem.appendChild(child);
} catch (ex1) {}
try {
if (elem.object) {
elem.object[child.name] = child.value;
}
} catch (ex2) {}
}
}
};
/**
* Tests a node for whitespace
*
* @private
* @param {Node} node The node
* @return {boolean}
*/
var isWhitespace = function(node) {
return !!node && (node.nodeType === 3) && (!node.nodeValue || !/\S/.exec(node.nodeValue));
};
/**
* Trims whitespace pattern from the text node
*
* @private
* @param {Node} node The node
*/
var trimPattern = function(node, pattern) {
if (!!node && (node.nodeType === 3) && pattern.exec(node.nodeValue)) {
node.nodeValue = node.nodeValue.replace(pattern, '');
}
};
/**
* Removes leading and trailing whitespace nodes
*
* @private
* @param {Node} elem The node
*/
var trimWhitespace = function(elem) {
if (elem) {
while (isWhitespace(elem.firstChild)) {
// trim leading whitespace text nodes
elem.removeChild(elem.firstChild);
}
// trim leading whitespace text
trimPattern(elem.firstChild, LEADING);
while (isWhitespace(elem.lastChild)) {
// trim trailing whitespace text nodes
elem.removeChild(elem.lastChild);
}
// trim trailing whitespace text
trimPattern(elem.lastChild, TRAILING);
}
};
/**
* Converts the markup to DOM nodes
*
* @private
* @param {string|Markup} value The node
* @return {Node}
*/
var toDOM = function(value) {
var wrapper = createElement('div');
wrapper.innerHTML = ''+value;
// trim extraneous whitespace
trimWhitespace(wrapper);
// eliminate wrapper for single nodes
if (wrapper.childNodes.length === 1) {
return wrapper.firstChild;
}
// create a document fragment to hold elements
var frag = createElement('');
while (wrapper.firstChild) {
frag.appendChild(wrapper.firstChild);
}
return frag;
};
/**
* Default error handler
* @param {Error} ex
* @return {Node}
*/
var onError = function(ex) {
return document.createTextNode('['+ex+']');
};
/* override this to perform custom error handling during binding */
JsonML.onerror = null;
/**
* also used by JsonML.BST
* @param {Node} elem
* @param {*} jml
* @param {function} filter
* @return {Node}
*/
var patch = JsonML.patch = function(elem, jml, filter) {
for (var i=1; i