Skip to content

Instantly share code, notes, and snippets.

@mnshcodie
Created July 22, 2015 16:18
Show Gist options
  • Select an option

  • Save mnshcodie/3cec7266272c4d3bf784 to your computer and use it in GitHub Desktop.

Select an option

Save mnshcodie/3cec7266272c4d3bf784 to your computer and use it in GitHub Desktop.
JSON: Big JSON
//
// 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