-
-
Save nzakas/1202328 to your computer and use it in GitHub Desktop.
<!DOCTYPE html> | |
<!-- | |
This is a simple experiment relying on ECMAScript 6 Proxies. To try this out, | |
use Aurora (http://www.mozilla.org/en-US/firefox/channel/). | |
The goal was to create a HTML writer where the method names were really just | |
the HTML tags names, but without manually creating each method. This uses | |
a Proxy to create a shell to an underlying writer object that checks each | |
method name to see if it's in a list of known tags. | |
--> | |
<html> | |
<body> | |
<script> | |
/* | |
* The constructor name I want is HTMLWriter. | |
*/ | |
var HTMLWriter = (function(){ | |
/* | |
* Lazily-incomplete list of HTML tags. | |
*/ | |
var tags = [ | |
"a", "abbr", "acronym", "address", "applet", "area", | |
"b", "base", "basefont", "bdo", "big", "blockquote", | |
"body", "br", "button", | |
"caption", "center", "cite", "code", "col", "colgroup", | |
"dd", "del", "dir", "div", "dfn", "dl", "dt", | |
"em", | |
"fieldset", "font", "form", "frame", "frameset", | |
"h1", "h2", "h3", "h4", "h5", "h6", "head", "hr", "html", | |
"i", "iframe", "img", "input", "ins", "isindex", | |
"kbd", | |
"label", "legend", "li", "link", | |
"map", "menu", "meta", | |
"noframes", "noscript", | |
"object", "ol", "optgroup", "option", | |
"p", "param", "pre", | |
"q", | |
"s", "samp", "script", "select", "small", "span", "strike", | |
"strong", "style", "sub", "sup", | |
"table", "tbody", "td", "textarea", "tfoot", "th", "thead", | |
"title", "tr", "tt", | |
"u", "ul", | |
"var" | |
]; | |
/* | |
* Define an internal-only type. Code taken from: | |
* http://www.nczonline.net/blog/2009/02/17/mozilla-javascript-extension-nosuchmethod/ | |
*/ | |
function InternalHTMLWriter(){ | |
this._work = []; | |
} | |
InternalHTMLWriter.prototype = { | |
escape: function (text){ | |
return text.replace(/[><"&]/g, function(c){ | |
switch(c){ | |
case ">": return ">"; | |
case "<": return "<"; | |
case "\"": return """; | |
case "&": return "&"; | |
} | |
}); | |
}, | |
startTag: function(tagName, attributes){ | |
this._work.push("<" + tagName); | |
if (attributes){ | |
var name, value; | |
for (name in attributes){ | |
if (attributes.hasOwnProperty(name)){ | |
value = this.escape(attributes[name]); | |
this._work.push(" " + name + "=\"" + value + "\""); | |
} | |
} | |
} | |
this._work.push(">"); | |
return this; | |
}, | |
text: function(text){ | |
this._work.push(this.escape(text)); | |
return this; | |
}, | |
endTag: function(tagName){ | |
this._work.push("</" + tagName + ">"); | |
return this; | |
}, | |
toString: function(){ | |
return this._work.join(""); | |
} | |
}; | |
/* | |
* Output a pseudo-constructor. It's not a real constructor, | |
* since it just returns the proxy object, but I like the | |
* "new" pattern vs. factory functions. | |
*/ | |
return function(){ | |
var writer = new InternalHTMLWriter(), | |
proxy = Proxy.create({ | |
/* | |
* Only really need getter, don't want anything else going on. | |
*/ | |
get: function(receiver, name){ | |
var tagName, | |
closeTag = false; | |
if (name in writer){ | |
return writer[name]; | |
} else { | |
if (tags.indexOf(name) > -1){ | |
tagName = name; | |
} else if (name.charAt(0) == "x" && tags.indexOf(name.substring(1)) > -1){ | |
tagName = name.substring(1); | |
closeTag = true; | |
} | |
if (tagName){ | |
return function(){ | |
if (!closeTag){ | |
writer.startTag(tagName, arguments[0]); | |
} else { | |
writer.endTag(tagName); | |
} | |
return receiver; | |
}; | |
} | |
} | |
}, | |
/* | |
* Don't allow fixing. | |
*/ | |
fix: function(){ | |
return undefined; | |
} | |
}); | |
return proxy; | |
}; | |
})(); | |
//hmmm...doesn't look like magic way down here | |
var w = new HTMLWriter(); | |
w.html() | |
.head().title().text("Example & Test").xtitle().xhead() | |
.body().text("Hello world!").xbody() | |
.xhtml(); | |
console.log(w); | |
</script> | |
</body> | |
</html> |
http://wiki.ecmascript.org/doku.php?id=strawman:proxy_drop_receiver is also relevant in the Proxy API change
Here is a small experiment of mine which is slightly related to your approach: https://github.com/DavidBruant/DeclO
Instead of reusing the same proxy as you do, I generate one on the fly as a property is "get-ed". I'm not sure it's relevant in your context, but for insipration.
Thanks for the feedback. I'm not really planning on maintaining this, it's just more of an exploration than anything else. With regards to "x", it actually does work with tag names beginning with x, such as xmp, which would end up as xxmp(). Perhaps not the best naming convention, but works for example purposes.
Great point regarding closing tags as functions...hadn't even thought of that. And also thanks for the tip on the API change. I thought the first argument should be the name since receiver may not ever be used.
Brilliant idea!
"This is a simple experiment relying on ECMAScript 6 Proxies. To try this out, use Aurora (http://www.mozilla.org/en-US/firefox/channel/)."
=> It works on Firefox 6 as well (current stable)
It is somewhat necessary for starting tags to be functions (to pass attributes as arguments), but ending tags have no need.
After line 131, instead of returning a function, if closeTag===true, then "writer.endTag(tagName);" could be called right away and receiver returned directly (without returning a function to be called to return it).
Marking closing tags with "x" may not be future-proof since one day, there might be a tag with a name starting with an "x". "$" instead, maybe?
I don't know if you plan on supporting this in the long term of if it's just an experiment, but the proxy API may change especially regarding the get trap: http://wiki.ecmascript.org/doku.php?id=strawman:handler_access_to_proxy
Basically, the get trap would look like: get(name, proxy). But this is not spec'ed yet and not implemented yet.