Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial Commit

  • Loading branch information...
commit 58601b35c592ec8b1ade777dcbfc2696b4c8ae41 0 parents
Malte Ubl authored
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2009 Malte Ubl
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
125 README.md
@@ -0,0 +1,125 @@
+# node-syncEJS - 0.0.1
+
+> An asynchronous ERB-like templating system for node.js
+
+node-asyncEJS implements a templating language for embedding JavaScript into
+other text documents such as HTML. It adds new features to the classic ERB
+syntax to enable asynchronous execution of the template.
+
+## When to use
+You can always use the synchronous features. So use it whenever you need a
+templating solution.
+
+The asyncronous features come in handy when you
+1. Want to flush the output to the client as early as possible
+2. Need to stream your generated content
+
+## Usage
+
+### Template
+
+ <html>
+ <head>
+ <% ctx.hello = "World"; %>
+ <title><%= "Hello " + ctx.hello %></title>
+ </head>
+ <body>
+
+ <h1><%? setTimeout(function () { res.print("Async Header"); res.finish(); }, 2000) %></h1>
+ <p><%? setTimeout(function () { res.print("Body"); res.finish(); }, 1000) %></p>
+
+ </body>
+ </html>
+
+### JavaScript
+
+ var te = require("../lib/asyncEJS").Engine();
+
+ var template = te.template("template.t.html");
+
+ var templateResponse = template(paras);
+
+ templateResponse.addListener("body", function (chunk) {
+ sys.print(chunk);
+ });
+
+ templateResponse.addListener("complete", function () {
+ sys.puts("COMPLETE")
+ });
+
+## Templates
+
+### Special Variables
+
+`ctx` contains the parameter that was passed to the template function
+
+`res` represents the output of the template.
+
+`res.print("string")` prints more output into the template
+
+`res.finish()` tells the template that the current asynchronous blocks has finished execution
+
+### Basic Template Commands
+
+`<% var javascript = "code" %>` executes arbitrary JavaScript
+
+`<%= "Hello " + ctx.hello %>` outputs the statement result into the template
+
+### Asynchronous Templates
+
+ <%? setTimeout(function () { res.print("Async Header"); res.finish(); }, 2000) %>
+
+`<%?` introduces a block that will be expected to execute asynchronously with respect
+to the rest of the template. The rest of the template will continue executing but no
+output will be returned until the `res.finish()` method will be called.
+
+### Escaping
+
+There is currently no escaping of output but this will change.
+
+## JavaScript Usage
+
+### Template Engine
+
+Construct a template engine with
+
+ var te = require("../lib/asyncEJS").Engine({
+ autoUpdate: false
+ });
+
+If `autoUpdate` is true, asyncEJS will continously look for changes in templates and automatically
+update them when they change on disk.
+
+### Loading Templates
+
+After you instatiated a template engine use the `template` method to create a template
+function.
+
+ var template = te.template("template.t.html");
+
+Executing the template function with `paras` will return a templateResponse. The `paras` are
+accessible as `ctx` variable inside the template.
+
+ var templateResponse = template(paras);
+
+### Template Responses
+
+The template response emits two events `body` and `complete`
+
+ templateResponse.addListener("body", function (chunk) {
+ sys.print(chunk);
+ });
+
+ templateResponse.addListener("complete", function () {
+ sys.puts("COMPLETE")
+ });
+
+`body` is emitted whenever a part of the template has been completed.
+
+`complete` will fire once when the template has fully executed.
+
+## Examples
+
+See `examples/` and `test/`
+
+
8 examples/async.js.html
@@ -0,0 +1,8 @@
+<html>
+ <body>
+
+ <h1><%? setTimeout(function () { res.print("Async Header"); res.finish(); }, 2000) %></h1>
+ <p><%? setTimeout(function () { res.print("Body"); res.finish(); }, 1000) %></p>
+
+ </body>
+</html>
20 examples/run.js
@@ -0,0 +1,20 @@
+var sys = require("sys"),
+ posix = require("posix");
+
+var filename = process.ARGV[2];
+var paras = JSON.parse(process.ARGV[3] || "{}")
+
+// The template Engine
+var te = require("../lib/asyncEJS").Engine();
+
+var template = te.template(filename);
+
+var templateResponse = template(paras);
+
+templateResponse.addListener("body", function (chunk) {
+ sys.print(chunk);
+});
+
+templateResponse.addListener("complete", function () {
+ sys.puts("COMPLETE")
+});
1  examples/simple.js.html
@@ -0,0 +1 @@
+<%= "Hello World" %>
12 examples/usage.js.html
@@ -0,0 +1,12 @@
+<html>
+ <head>
+ <% ctx.hello = "World"; %>
+ <title><%= "Hello " + ctx.hello %></title>
+ </head>
+ <body>
+
+ <h1><%? setTimeout(function () { res.print("Async Header"); res.finish(); }, 2000) %></h1>
+ <p><%? setTimeout(function () { res.print("Body"); res.finish(); }, 1000) %></p>
+
+ </body>
+</html>
249 lib/asyncEJS.js
@@ -0,0 +1,249 @@
+var posix = require('posix'),
+ sys = require('sys');
+
+var templateCache = {};
+
+// A TemplateEngine holds template factories
+// plus a config for all templates
+var TemplateEngine = function (config) {
+ config = config || {};
+ this.autoUpdate = config.autoUpdate
+}
+
+exports.Engine = function (config) {
+ return new TemplateEngine(config);
+}
+
+TemplateEngine.prototype = {
+
+ // Synchronous template constructor
+ template: function (filename) {
+ return this.templateAsync(filename).wait();
+ },
+
+ // Asynchronous template constructor
+ // Returns a promise for a template
+ templateAsync: function (filename) {
+ var self = this;
+ var templatePromise = new process.Promise();
+
+ if(templateCache[filename]) {
+ setTimeout(function () {
+ templatePromise.emitSuccess(templateCache[filename]);
+ }, 0);
+ return templatePromise;
+ }
+
+ load(filename, function (fn) {
+
+ var t = function (ctx) {
+ var res = new TemplateResponse(self, filename);
+ fn(ctx, res);
+ setTimeout(function () {
+ res.partFinished();
+ }, 0)
+ return res;
+ };
+
+ templateCache[filename] = t;
+
+ if(self.autoUpdate) {
+ process.watchFile(filename, {
+ persistent: true,
+ interval: 2000
+ }, function () {
+ load(filename, function (newFn) {
+ fn = newFn;
+ })
+ });
+ }
+
+ templatePromise.emitSuccess(t);
+ }, function (e) {
+ templatePromise.emitError(e)
+ });
+ return templatePromise;
+ },
+
+ // Should not be public :)
+ partial: function (filename, res, ctx) {
+ var partialPromise = new process.Promise();
+ var p = this.templateAsync(filename);
+
+ var partRes;
+
+ if(res) {
+ res.schedule(function (res) {
+ partRes = res;
+ })
+ }
+
+ p.addCallback(function (t) {
+ var partialResponse = t(ctx);
+ //sys.puts("Partial "+partialResponse.filename)
+ partialResponse.addListener("body", function (chunk) {
+ //sys.puts("PartialChunk"+partialResponse.filename)
+ partRes.print(chunk);
+ });
+ partialResponse.addListener("complete", function () {
+ //sys.error("Partial done "+partialResponse.filename);
+ partialResponse.finished = true;
+ partRes.finish();
+ res.partFinished();
+ partialPromise.emitSuccess();
+ });
+ })
+ return partialPromise;
+ }
+};
+
+function load(filename, cb, errb) {
+ posix.cat(filename).addCallback(function (str) {
+ //sys.puts(str.toString());
+
+ var src = parseAndGenerate(str);
+
+ //sys.puts(src);
+ var fn = process.compile(src, filename);
+ cb(fn);
+ });
+}
+
+
+function TemplateResponse(engine, filename) {
+ var self = this;
+ process.EventEmitter.call(this);
+ this.engine = engine;
+ this.filename = filename;
+ this.p = [];
+ this.returned = 0;
+ this.finished = false;
+}
+
+process.inherits(TemplateResponse, process.EventEmitter);
+process.mixin(TemplateResponse.prototype, {
+ print: function () {
+ this.p.push.apply(this.p, arguments);
+ },
+
+ toString: function () {
+ return "<a TemplateResponse for "+this.filename+" "+this.finished+">"
+ },
+
+ finish: function () {
+ this.emit("complete");
+ },
+
+ schedule: function (fn) {
+ var part = this._schedule(fn);
+ this.p.push(part);
+ },
+
+ _schedule: function (fn) {
+ var self = this;
+ var part = new TemplateResponse(this.engine, this.filename+"->async");
+ part.addListener("complete", function () {
+ part.finished = true;
+ self.partFinished();
+ })
+ fn.call(part, part);
+ return part;
+ },
+
+ partial: function (filename, ctx) {
+ return this.engine.partial(filename, this, ctx);
+ },
+
+ partFinished: function () {
+ var self = this;
+ if(this.returned < this.p.length) {
+ var done = this._partFinished(function (chunk) {
+ self.emit("body", chunk);
+ //sys.error("EMIT "+self.filename+" -> "+JSON.stringify(chunk))
+ });
+
+ if(done === this.p.length) {
+ this.emit("complete");
+ }
+ }
+ },
+
+ _partFinished: function (cb, start) {
+ var p = this.p;
+ //sys.puts(this.filename + " -> "+JSON.stringify(this.p))
+ if(start != null) {
+ this.returned = start
+ }
+ for(var i = this.returned, len = p.length; i < len; ++i) {
+ var part = p[i];
+ if(!(part instanceof TemplateResponse)) {
+ cb(part);
+ this.returned++;
+ }
+ else if(part.finished) {
+ part._partFinished(cb, 0)
+ this.returned++;
+ } else {
+ break;
+ }
+ }
+ return i;
+ }
+});
+
+function parseAndGenerate(str) {
+ var open = "<%";
+ var close = "%>";
+ var index;
+ var dynPart = [];
+ // TODO tweak the second replace so that the first can go away (and the later which reverse it)
+ var staticParts = str.replace(/[\n\r]/g, "<;;;;>").replace(/<%(.*?)%>/g, function (part, code) {
+ dynPart.push(code.replace(/<;;;;>/g, "\n"));
+ return "<%%>"
+ }).replace(/<;;;;>/g, "\n").replace(/'/g, "\\'").replace(/[\n\r]/g, "\\n\\\n").split(/<%%>/);
+
+ var src = 'function __template__ (ctx, res) {';
+
+ src = staticParts.reduce(function(src, part, index){
+ src = src + "res.p.push('" + part + "');";
+ var code = dynPart[index];
+ if(code != null) {
+ if(code.charAt(0) === "=") {
+ code = code.substr(1);
+ src = src + "res.p.push("+code+");";
+ }
+ else if(code.charAt(0) === "?") {
+ code = code.substr(1);
+ src = src + "res.p.push(res._schedule(function (res) {"+code+"}));"
+ }
+ else {
+ src = src + code
+ }
+ }
+ return src;
+ }, src) + "return res}__template__;";
+ return src;
+}
+
+function parseJohnResig(str) {
+ // Stolen from underscore.js http://documentcloud.github.com/underscore/underscore.js
+ // JavaScript templating a-la ERB, pilfered from John Resig's
+ // "Secrets of the JavaScript Ninja", page 83.
+ var src =
+ 'function __template__ (ctx, res) {' +
+ 'res.p.push(\'' +
+ str
+ .replace(/[\t]/g, " ")
+ .replace(/[\n\r]/g, "<%\n%>\\n")
+ .split("<%").join("\t")
+ .replace(/((^|%>)[^\t]*)'/g, "$1\n")
+ .replace(/\t=(.*?)%>/g, "',$1,'")
+ .replace(/\t\?(.*?)%>/g, "',res._schedule(function (res) {$1}),'")
+ .split("\t").join("');")
+ .split("%>").join("res.p.push('")
+ .split("\r").join("\\'")
+ + "');return res}__template__;";
+
+ return src;
+}
+
3  test/fixtures/partial.js.html
@@ -0,0 +1,3 @@
+<p>Hello: <%? setTimeout(function () { res.print(ctx.hello); res.finish(); }, 1000); %></p>
+
+<em><% res.partial("fixtures/partialStatic.js.html"); %></em>
1  test/fixtures/partialStatic.js.html
@@ -0,0 +1 @@
+Static
18 test/fixtures/reference.html
@@ -0,0 +1,18 @@
+<html>
+ <body>
+
+ <h2>{"hello":[],"test":1}</h2>
+
+ <h1>Async1</h1>
+ <p>Async2</p>
+
+ <h2>Async3</h2>
+
+ <p>Hello: world</p>
+
+<em>Static</em>
+
+ <h6>Async4Static</h6>
+
+ </body>
+</html>
22 test/fixtures/template.js.html
@@ -0,0 +1,22 @@
+<html>
+ <body>
+ <% ctx.test = 1; %>
+ <h2><%= JSON.stringify(ctx) %></h2>
+
+ <h1><%? setTimeout(function () { res.print("Async1"); res.finish(); }, 400) %></h1>
+ <p><%? setTimeout(function () { res.print("Async2"); res.finish(); }, 100) %></p>
+
+ <h2><% res.schedule(function (res) {setTimeout(function () { %>Async3<% res.finish(); }, 300)}); %></h2>
+
+ <% res.partial("fixtures/partial.js.html", { hello: "world" }); %>
+
+ <h6><%? setTimeout(function () {
+ res.print("Async4");
+ res.partial("fixtures/partialStatic.js.html")
+ .addCallback(function () {
+ res.finish();
+ })
+ }, 200) %></h6>
+
+ </body>
+</html>
25 test/test.js
@@ -0,0 +1,25 @@
+var sys = require("sys"),
+ posix = require("posix");
+
+var reference = posix.cat("fixtures/reference.html").wait();
+
+var te = require("../lib/asyncEJS").Engine();
+
+var t1 = te.template("fixtures/template.js.html");
+
+var templateResponse = t1({
+ hello: []
+});
+var body = ""
+templateResponse.addListener("body", function (chunk) {
+ body += chunk;
+ sys.print(chunk);
+});
+
+templateResponse.addListener("complete", function () {
+ if(body === reference) {
+ sys.puts("OK");
+ } else {
+ sys.puts("NOT OK")
+ }
+});
Please sign in to comment.
Something went wrong with that request. Please try again.