Wednesday, September 30, 2009

A DSL for generating HTML with JS

Maybe some people find this interesting. I created a little DSL to generate HTML with JavaScript. Here is an example:
var things = [{text: "Hello"}, {text: "World"}];

with(HTML) {

var html = div({
id: "foo",
content: a({
href: "#",
content: ["test", span("bar")]
})
}) +

div({
className: "baz",
content: map(things, function (thing) {
return p(thing.text)
})
});

alert(html)

document.write(html)
}

Yes, I know, with is very evil but it is cool in this situation.

Here is the full source. I hacked it up in 30 minutes and it is in bad need for a cleanup, but otherwise it seems to work alright.

var HTML = (function () {
function encode(text) {
text = text.replace(/&/g, "&", "g");
text = text.replace(/</g, "&lt;", "g");
text = text.replace(/>/g, "&gt;", "g");
text = text.replace(/"/g, "&quot;", "g");
text = text.replace(/'/g, "&apos;", "g");
return text;
}

function renderAttr(attr) {
if(!attr) return '';
var html = '';
for(var name in attr) {
var value = attr[name];
if(name === "className") name == "class";
if(name === "content") continue;
html += ' '+name+'="'+encode(value)+'"'
}
html.encoded = true;
return html
}

function makeTag (name) {
return function (paras, content) {
if(arguments.length < 2) {
content = [];
}
if(typeof content === "string") content = [content]
var attr = {};
if(typeof paras === "string") {
content = [encode(paras)];
} else if(paras == null) {
content = [];
} else if(paras.content) {
var c = paras.content;
delete paras.content;
return arguments.callee(paras, c);
} else if(paras.length > 0) {
content = paras;
} else {
attr = paras;
}
var empty = content.length === 0 ? true : false;

var html = '<'+name+renderAttr(attr)+(empty ? '/' : '')+'>';
if(!empty) {
html += content.join("");

html += '';
}
return html;
}
}

function map(enumerable, fn) {
var result = [];
for(var i = 0, len = enumerable.length; i < len; ++i) {
result.push(fn(enumerable[i]))
}
return result;
}

var tags = ["div", "a", "p", "span"]

var HTML = {
map: map,
encode: encode
};

map(tags, function (name) {
HTML[name] = makeTag(name)
});

return HTML;
})();

6 comments:

Kris Kowal said...

I've got something similar in Chiron. It looks like:

tag("div", {id:"foo"}, [
tag("a", {href:"#"}, [
"test",
tag("span", {}, "bar")
])
])

Malte said...

Cool, did you put in support for conditionals, loops, etc.?

I'm currently looking for a templating system that I can easily use on the client and the server with the problem that I cannot read non JS files on the server.

Samurai Jack said...

Nifty :)

@Malte Take a look on these 2:

http://jemplate.net/
http://www.kuwata-lab.com/tenjin/

I was using Jemplate for a while, it works fine generally, but has couple of drawbacks like templates should be compiled by external script, and sub-optimal performance because of translating from small internal DSL. For the same reason its pretty feature-full, supports all kinds of loops/conditionals, macroses, etc.

Tenjin uses JavaScript as DSL, so it should be very fast, thats all I know about it though.

Adam said...

Here's one i wrote, it has a few browser bug fixes in it:

http://www.cactusjs.com/browser/trunk/DOM/tag.js

Slim Amamou said...

sometimes you just want to add content to the current page, so you don't need to generate HTML. You just have to generate a DOM element tree like this :

$('myform').appendChild(buildom(
['FORM', {action:'http://www.google.com/search', method:'post'},
['LABEL', 'Search',
['INPUT', {name:'q', type:'text'}],
['BUTTON', {type:'submit'}]]]));

here is buildom() : http://github.com/slim/waraq/blob/master/js/json2dom.js

Malte said...

@Adam: Thanks for the link. Looks interesting

@Slim: Thanks for hint. I was specifically shooting for a solution that is not using the DOM because I use this on the server and on the client.