Last active
August 29, 2015 14:08
-
-
Save kjaquier/e4eb623da6449d6bfba4 to your computer and use it in GitHub Desktop.
JSON Templates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Simple template system with plain JS objects, using CSS selectors | |
// Two versions : | |
// * JSON | |
// - based on JSON objects, each element is a property | |
// * JSON-ML : | |
// - inspired from the JSON-ML markup language (http://www.jsonml.org/) | |
// - each element is a list | |
// - element type can contains id and classes like the first version (so, not standard json-ml) | |
// | |
// Try it: http://jsfiddle.net/ynakLqv8/ | |
// | |
(function() { | |
/*** HELPERS ***/ | |
if (typeof String.prototype.startsWith != 'function') { | |
String.prototype.startsWith = function (str){ | |
return this.slice(0, str.length) == str; | |
}; | |
} | |
if (typeof String.prototype.endsWith != 'function') { | |
String.prototype.endsWith = function (str){ | |
return this.slice(-str.length) == str; | |
}; | |
} | |
function str(obj) { | |
return JSON.stringify(obj, undefined, 2); | |
} | |
function isObject(value) { | |
return typeof value == 'object' && value.length === undefined; | |
} | |
function isList(value) { | |
return typeof value == 'object' && value.length !== undefined; | |
} | |
/*** COMMON FUNCTIONS ***/ | |
// Parse a string containing CSS keys (id, classes, etc.) and | |
// return a DOM object definition | |
// Example : | |
// parseCSSKeys("input #name-input .text-field .big-input required") | |
// => { type: "input", id: "name-input", classes: ["text-field", "big-input"], flags: { required: true } } | |
function parseCSSKeys(keyString) { | |
var elem = { | |
type: "div", | |
id: undefined, | |
subId: undefined, | |
classes: [], | |
flags: {} | |
}; | |
var tokens = keyString.split(" "); | |
elem.type = tokens[0]; // TODO check that's a type, else make it a div | |
for (var i=1; i < tokens.length; i++) { | |
token = tokens[i]; | |
if (token.startsWith(".")) { | |
elem.classes.push(token.slice(-token.length+1)); | |
} | |
else if (token.startsWith("#")) { | |
elem.id = token.slice(-token.length+1); | |
} | |
else if (token.startsWith("$")) { | |
elem.subId = token.slice(-token.length+1); | |
} else { | |
elem.flags[token] = true; | |
} | |
} | |
return elem; | |
} | |
function addClasses(props, classes) { | |
if (classes.length > 0) { | |
if (props.classes === undefined) { | |
props.classes = []; | |
} | |
Array.prototype.push.apply(props.classes, classes); | |
} | |
} | |
function mergeProps(targetProps, sourceProps) { | |
for (var propName in sourceProps) { | |
if (propName == 'classes') { | |
addClasses(targetProps, sourceProps[propName]); | |
} else { | |
targetProps[propName] = sourceProps[propName]; | |
} | |
} | |
} | |
/*** PARSERS AND GENERATORS ***/ | |
// Parse the given template with JSON format (see example) | |
// and gives an AST | |
function parseJSONTemplate(tree) { | |
var nodeChilds = []; | |
for (var elKey in tree) { | |
if (tree.hasOwnProperty(elKey)) { | |
var element = tree[elKey]; | |
// Parse key to have type, id, classes | |
var elKeys = parseCSSKeys(elKey); | |
var elType = elKeys.type; | |
var elId = elKeys.id; | |
var elClasses = elKeys.classes; | |
var elProps = elKeys.flags; | |
if (elId) { | |
elProps.id = elId; | |
} | |
addClasses(elProps, elClasses); | |
// Parse value to have subtree or leaf | |
var elChilds = null; | |
if (isObject(element)) { // subtree | |
for (var attrName in element) { | |
var attrValue = element[attrName]; | |
// element's attribute begins with "_" to distinguish them | |
// from childs | |
if (attrName.startsWith("_")) { | |
// if it's the "props" attribute, it contains additionnal attributes | |
// (or properties, or props) to be merged | |
if (attrName == '_props') { | |
mergeProps(elProps, attrValue); | |
} | |
// if (attrName == '_style') { | |
// for (var propName in attrValue) { | |
// elProps.styles[propName] = attrValue[propName]; | |
// } | |
// } | |
} | |
} | |
// deletes special attributes so they are ignored on further parsing | |
delete element._props; | |
// delete element._style; | |
// parse the childs | |
elChilds = parseJSONTemplate(element); | |
} else { // leaf | |
elChilds = element; | |
} | |
nodeChilds.push({ type: elType, props: elProps, childs: elChilds }); | |
} | |
} | |
return nodeChilds; | |
} | |
// Parse the given template with JSON-ML format (see example) | |
// and gives an AST | |
function parseJSONMLTemplate(node) { | |
if (isList(node)) { | |
var elKeys = parseCSSKeys(node[0]); | |
var elem = { | |
type: elKeys.type, | |
props: elKeys.flags, | |
childs: [] | |
}; | |
if (elKeys.id) { | |
elem.props.id = elKeys.id; | |
} | |
addClasses(elem.props, elKeys.classes); | |
// If 2nd element of the list is an object, it contains | |
// additional properties to be merged | |
var firstChildIndex = 1; | |
if (isObject(node[1])) { | |
mergeProps(elem.props, node[1]); | |
firstChildIndex++; | |
} | |
for (var i = firstChildIndex; i < node.length; i++) { | |
elem.childs.push(parseJSONMLTemplate(node[i])); | |
} | |
return elem; | |
} else if (typeof node == 'string') { | |
return node; | |
} | |
} | |
// Generate HTML code for the given tree node | |
function generateHtml(elem) { | |
if (isObject(elem)) { // if it's a node | |
var type = elem.type; | |
var props = elem.props; | |
var childs = elem.childs; | |
var openingTagStr = [type]; | |
if (props.classes !== undefined) { | |
if (props.classes.length > 0) { | |
props['class'] = props.classes.join(" "); | |
} | |
delete props.classes; | |
} | |
for (var propKey in props) { | |
if (props.hasOwnProperty(propKey)) { | |
if (typeof props[propKey] == 'boolean' && props[propKey]) { | |
openingTagStr.push(propKey); | |
} else { | |
openingTagStr.push(propKey + "=\"" + props[propKey] + "\""); | |
} | |
} | |
} | |
var s = ""; | |
s += "<" + openingTagStr.join(" ") + ">"; | |
for (var i=0; i < childs.length; i++) { | |
s += generateHtml(childs[i]); | |
} | |
s += "</" + type + ">"; | |
return s; | |
} else if (isList(elem)) { // if it's a list | |
var elems = []; | |
for (var i = 0; i < elem.length; i++) { | |
var current = generateHtml(elem[i]); | |
elems.push(current); | |
} | |
return elems.join(" "); | |
} else { // not a object, so it's a leaf | |
return elem; | |
} | |
} | |
/****************************** EXAMPLE ***********************************/ | |
////////// | |
// HTML // | |
////////// | |
// <h1>JSON Template</h1><div id="test-json"></div> | |
// <h1>JSON-ML Template</h1><div id="test-jsonml"></div> | |
///////// | |
// CSS // | |
///////// | |
// .red-bg { | |
// background: red; | |
// } | |
// .centered { | |
// margin: auto; | |
// width: 500px; | |
// } | |
// .green-bg { | |
// background: green; | |
// } | |
// div { | |
// margin: 5px; | |
// padding: 5px; | |
// border: 2px; | |
// } | |
// .round-border { | |
// border-radius: 5px; | |
// -webkit-border-radius: 5px; | |
// -moz-border-radius: 5px; | |
// } | |
//////// | |
// JS // | |
//////// | |
var jsonTemplate = { | |
"section": { | |
"div #container .red-bg .centered" : { | |
_props: { // _define additional attributes (or properties, or props) in _props | |
style: "text: gray; font-family: Arial", | |
classes: ["round-border"] // you can add classes in props too (useful for dynamic classes) | |
}, | |
// _style: { text: "gray", fontFamily: "Arial" } // this is for the future ;) | |
"h2": "Foo", // put simple string for simple text (not recommanded though) | |
"p": "Hello", | |
"p $1": "Beautiful", | |
"p $2": "World", // use "$" identifiers so you can use the same tag more than once | |
"div .green-bg": { | |
"h3": "Hahaha" | |
} | |
}, | |
"div .green-bg" : { | |
"h2": "Bar", | |
"p": "Hi!" | |
} | |
} | |
}; | |
var jsonmlTemplate = ( // more "lisp-like" style | |
["section", | |
["div #container .red-bg .centered", // first element is the css keys | |
{ // second element is the props (optional) | |
style: "text: gray; font-family: Arial", | |
classes: ["round-border"] | |
}, | |
["h2", "Foo"], // then all following elements are childs | |
["p", "Hello"], | |
["p", "Beautiful"], | |
["p", "World"], // note that since we're using lists, we don't need to make the keys unique anymore! | |
["div .green-bg", | |
["h3", "Hahaha"] | |
] | |
], | |
["div .green-bg", | |
["h2", "Bar"], | |
["p", "Hi!"] | |
] | |
] | |
); | |
var ast1 = parseJSONTemplate(jsonTemplate); | |
console.log("[JSON] Parsed : " + str(ast1)); | |
var generatedHtml1 = generateHtml(ast1); | |
console.log("[JSON] Generated : " + generatedHtml1); | |
document.getElementById("test-json").innerHTML = generatedHtml1; | |
var ast2 = parseJSONMLTemplate(jsonmlTemplate); | |
console.log("[JSON-ML] Parsed : " + str(ast2)); | |
var generatedHtml2 = generateHtml(ast2); | |
console.log("[JSON-ML] Generated : " + generatedHtml2); | |
document.getElementById("test-jsonml").innerHTML = generatedHtml2; | |
console.log("Same result? " + (generatedHtml1 == generatedHtml2)); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Je tatillone, mais pour le cas des attributs boolean. C'est W3C de mettre que le nom de l'attribut, mais les navigateurs l'implémentent faux la plupart du temps. Il faut préférer répéter le nom de l'attribut en valeur.
L.83