diff --git a/src/eco/compiler.coffee b/src/eco/compiler.coffee index bccb881..d32a5cd 100644 --- a/src/eco/compiler.coffee +++ b/src/eco/compiler.coffee @@ -2,8 +2,8 @@ CoffeeScript = require "coffee-script" {preprocess} = require "./preprocessor" {indent} = require "./util" -module.exports = eco = (source) -> - (new Function "module", compile source) module = {} +module.exports = eco = (source, async) -> + (new Function "module", compile source, { async }) module = {} module.exports eco.preprocess = preprocess @@ -11,12 +11,56 @@ eco.preprocess = preprocess eco.compile = compile = (source, options) -> identifier = options?.identifier ? "module.exports" identifier = "var #{identifier}" unless identifier.match(/\./) - script = CoffeeScript.compile preprocess(source), noWrap: true - + script = CoffeeScript.compile preprocess(source, options?.async), noWrap: true + if options?.async + parameters = "__obj, __callback" + asyncAssertions = """ + if (!__callback) { + throw new Error("Callback required."); + } + """ + asyncHelpers = """ + __async = function(callback) { + return function(done) { + try { + callback([], function(out) { + done(out); + }); + } catch (error) { + __callback(error); + } + } + }, + """ + join = """ + var index = 0; + function expand() { + while (index < __out.length) { + if (typeof __out[index] == "function") { + __out[index](function (out) { + Array.prototype.splice.apply(__out, [ index, 1 ].concat(out)); + expand(); + }); + return; + } + index++; + } + __callback(null, __out.join("")); + } + expand(); + """ + else + parameters = "__obj" + asyncAssertions = "" + asyncHelpers = "" + join = """ + return __out.join(""); + """ """ - #{identifier} = function(__obj) { + #{identifier} = function(#{parameters}) { if (!__obj) __obj = {}; - var __out = [], __capture = function(callback) { + #{indent asyncAssertions, 2} + var __out = [], #{indent asyncHelpers, 2}__capture = function(callback) { var out = __out, result; __out = []; callback.call(this); @@ -52,15 +96,14 @@ eco.compile = compile = (source, options) -> }; } (function() { - #{indent script, 4} + #{indent script, 4} }).call(__obj); __obj.safe = __objSafe, __obj.escape = __escape; - return __out.join(''); + #{indent join, 2} }; """ - -eco.render = (source, data) -> - (eco source) data +eco.render = (source, data, callback) -> + (eco source, typeof callback is "function") data, callback if require.extensions require.extensions[".eco"] = (module, filename) -> diff --git a/src/eco/preprocessor.coffee b/src/eco/preprocessor.coffee index f698dd1..7dd1602 100644 --- a/src/eco/preprocessor.coffee +++ b/src/eco/preprocessor.coffee @@ -2,16 +2,17 @@ Scanner = require "./scanner" util = require "./util" module.exports = class Preprocessor - @preprocess: (source) -> - preprocessor = new Preprocessor source + @preprocess: (source, async) -> + preprocessor = new Preprocessor source, async preprocessor.preprocess() - constructor: (source) -> + constructor: (source, @async) -> @scanner = new Scanner source @output = "" @level = 0 @options = {} @captures = [] + @asyncs = [] preprocess: -> until @scanner.done @@ -33,7 +34,9 @@ module.exports = class Preprocessor recordCode: (code) -> if code isnt "end" if @options.print - if @options.safe + if @async and not @options.inline + @record "__out.push __async (__out, __done) => #{code}" + else if @options.safe @record "__out.push #{code}" else @record "__out.push __sanitize #{code}" @@ -43,11 +46,17 @@ module.exports = class Preprocessor indent: (capture) -> @level++ if capture - @record "__capture #{capture}" - @captures.unshift @level - @indent() + if @async and @options.print + @asyncs.unshift @level + else + @record "__capture #{capture}" + @captures.unshift @level + @indent() dedent: -> + if @asyncs[0] is @level + @asyncs.shift() + @record "__done(__out)" @level-- @fail "unexpected dedent" if @level < 0 if @captures[0] is @level diff --git a/src/eco/scanner.coffee b/src/eco/scanner.coffee index 48a4cfe..f63f36e 100644 --- a/src/eco/scanner.coffee +++ b/src/eco/scanner.coffee @@ -48,8 +48,11 @@ module.exports = class Scanner @scanner.scanUntil Scanner.modePatterns[@mode] @buffer += @scanner.getCapture 0 @tail = @scanner.getCapture 1 - @directive = @scanner.getCapture 3 - @arrow = @scanner.getCapture 4 + if @mode is "data" + @capture = @scanner.getCapture 3 + else + @colon = @scanner.getCapture 3 + @arrow = @scanner.getCapture 4 scanData: (callback) -> if @tail is "<%%" @@ -64,7 +67,6 @@ module.exports = class Scanner else if @tail @mode = "code" callback ["printString", @flush()] - callback ["beginCode", print: @directive?, safe: @directive is "-"] scanCode: (callback) -> if @tail is "\n" @@ -75,9 +77,10 @@ module.exports = class Scanner code = trim @flush() code += " #{@arrow}" if @arrow + callback ["beginCode", print: @capture?, safe: @capture is "-", inline: not @arrow] callback ["dedent"] if @isDedentable code callback ["recordCode", code] - callback ["indent", @arrow] if @directive + callback ["indent", @arrow] if @colon flush: -> buffer = @buffer diff --git a/src/eco/util.coffee b/src/eco/util.coffee index 9f40cf6..48765e3 100644 --- a/src/eco/util.coffee +++ b/src/eco/util.coffee @@ -4,6 +4,7 @@ exports.repeat = repeat = (string, count) -> exports.indent = (string, width) -> space = repeat " ", width lines = (space + line for line in string.split "\n") + lines[0] = lines[0].substring(width) lines.join "\n" exports.trim = (string) -> diff --git a/test/fixtures/blocks-async.coffee b/test/fixtures/blocks-async.coffee new file mode 100644 index 0000000..7687321 --- /dev/null +++ b/test/fixtures/blocks-async.coffee @@ -0,0 +1,10 @@ +__out.push __async (__out, __done) => @emit => + __out.push '\n ' + __out.push __async (__out, __done) => @emit (data) -> + __out.push '\n ' + __out.push __sanitize "&" + __out.push '\n ' + __done(__out) + __out.push '\n' + __done(__out) +__out.push '\n' diff --git a/test/fixtures/blocks.coffee b/test/fixtures/blocks.coffee new file mode 100644 index 0000000..97441b7 --- /dev/null +++ b/test/fixtures/blocks.coffee @@ -0,0 +1,10 @@ +__out.push __sanitize @emit => + __capture => + __out.push '\n ' + __out.push @emit (data) -> + __capture -> + __out.push '\n ' + __out.push __sanitize "&" + __out.push '\n ' + __out.push '\n' +__out.push '\n' diff --git a/test/fixtures/blocks.eco b/test/fixtures/blocks.eco new file mode 100644 index 0000000..117affc --- /dev/null +++ b/test/fixtures/blocks.eco @@ -0,0 +1,5 @@ +<%= @emit => %> + <%- @emit (data) -> %> + <%= "&" %> + <% end %> +<% end %> diff --git a/test/fixtures/blocks.out.1 b/test/fixtures/blocks.out.1 new file mode 100644 index 0000000..3fdd67d --- /dev/null +++ b/test/fixtures/blocks.out.1 @@ -0,0 +1,5 @@ + + + & + + diff --git a/test/fixtures/error.eco b/test/fixtures/error.eco new file mode 100644 index 0000000..afaf744 --- /dev/null +++ b/test/fixtures/error.eco @@ -0,0 +1 @@ +<%= @emit => %><% end %> diff --git a/test/test_compile.coffee b/test/test_compile.coffee index b692981..ab96e4e 100644 --- a/test/test_compile.coffee +++ b/test/test_compile.coffee @@ -14,6 +14,14 @@ module.exports = test.ok eco.compile fixture("helpers.eco") test.done() + "compiling fixtures/block.eco": (test) -> + test.ok eco.compile fixture("helpers.eco") + test.done() + + "compiling fixtures/block.eco with async": (test) -> + test.ok eco.compile fixture("helpers.eco"), { async: true } + test.done() + "parse error throws exception": (test) -> test.expect 1 try diff --git a/test/test_preprocessor.coffee b/test/test_preprocessor.coffee index 27bf51a..589ba9b 100644 --- a/test/test_preprocessor.coffee +++ b/test/test_preprocessor.coffee @@ -18,6 +18,22 @@ module.exports = test.same fixture("helpers.coffee"), preprocess fixture("helpers.eco") test.done() + "preprocessing fixtures/blocks.eco": (test) -> + test.same fixture("blocks.coffee"), preprocess fixture("blocks.eco") + test.done() + + "preprocessing fixtures/capture.eco": (test) -> + test.same fixture("capture.coffee"), preprocess fixture("capture.eco") + test.done() + + "preprocessing fixtures/capture.eco with async": (test) -> + test.same fixture("capture.coffee"), preprocess fixture("capture.eco"), true + test.done() + + "preprocessing fixtures/blocks.eco with async": (test) -> + test.same fixture("blocks-async.coffee"), preprocess fixture("blocks.eco"), true + test.done() + "unexpected dedent": (test) -> test.expect 1 try diff --git a/test/test_render.coffee b/test/test_render.coffee index 7cb45e7..3a9df39 100644 --- a/test/test_render.coffee +++ b/test/test_render.coffee @@ -60,6 +60,22 @@ module.exports = test.same fixture("capture.out.1"), output test.done() + "rendering blocks.eco": (test) -> + output = eco.render fixture("blocks.eco"), emit: (yield) -> yield() + test.same fixture("blocks.out.1"), output + test.done() + + "rendering blocks.eco async": (test) -> + output = eco.render fixture("blocks.eco"), { emit: (yield) -> yield() }, (error, output) -> + test.same fixture("blocks.out.1"), output + test.done() + + "rendering error.eco async": (test) -> + output = eco.render fixture("error.eco"), { emit: () -> throw new Error("catch me") }, (error, output) -> + test.ok error + test.same "catch me", error.message + test.done() + "HTML is escaped by default": (test) -> output = eco.render "<%= @emailAddress %>", emailAddress: "" diff --git a/test/test_scanner.coffee b/test/test_scanner.coffee index fd4b0eb..335f0cc 100644 --- a/test/test_scanner.coffee +++ b/test/test_scanner.coffee @@ -2,27 +2,27 @@ module.exports = "'<%' begins a code block": (test) -> - tokens = scan "<%" + tokens = scan "<% hello() %>" test.same ["printString", ""], tokens.shift() - test.same ["beginCode", print: false, safe: false], tokens.shift() + test.same ["beginCode", print: false, safe: false, inline: true], tokens.shift() test.done() "'<%=' begins a print block": (test) -> - tokens = scan "<%=" + tokens = scan "<%= hello %>" test.same ["printString", ""], tokens.shift() - test.same ["beginCode", print: true, safe: false], tokens.shift() + test.same ["beginCode", print: true, safe: false, inline: true], tokens.shift() test.done() "'<%-' begins a safe print block": (test) -> - tokens = scan "<%-" + tokens = scan "<%- hello %>" test.same ["printString", ""], tokens.shift() - test.same ["beginCode", print: true, safe: true], tokens.shift() + test.same ["beginCode", print: true, safe: true, inline: true], tokens.shift() test.done() "'%>' ends a code block": (test) -> tokens = scan "<% code goes here %>" test.same ["printString", ""], tokens.shift() - test.same ["beginCode", print: false, safe: false], tokens.shift() + test.same ["beginCode", print: false, safe: false, inline: true], tokens.shift() test.same ["recordCode", "code goes here"], tokens.shift() test.same ["printString", ""], tokens.shift() test.done() @@ -30,7 +30,7 @@ module.exports = "': %>' ends a code block and indents": (test) -> tokens = scan "<% for project in @projects: %>" test.same ["printString", ""], tokens.shift() - test.same ["beginCode", print: false, safe: false], tokens.shift() + test.same ["beginCode", print: false, safe: false, inline: true], tokens.shift() test.same ["recordCode", "for project in @projects"], tokens.shift() test.same ["indent", undefined], tokens.shift() test.done() @@ -38,7 +38,7 @@ module.exports = "'-> %>' ends a code block and indents": (test) -> tokens = scan "<%= @render 'layout', -> %>" test.same ["printString", ""], tokens.shift() - test.same ["beginCode", print: true, safe: false], tokens.shift() + test.same ["beginCode", print: true, safe: false, inline: false], tokens.shift() test.same ["recordCode", "@render 'layout', ->"], tokens.shift() test.same ["indent", "->"], tokens.shift() test.done() @@ -46,7 +46,7 @@ module.exports = "'=> %>' ends a code block and indents": (test) -> tokens = scan "<%= @render 'layout', => %>" test.same ["printString", ""], tokens.shift() - test.same ["beginCode", print: true, safe: false], tokens.shift() + test.same ["beginCode", print: true, safe: false, inline: false], tokens.shift() test.same ["recordCode", "@render 'layout', =>"], tokens.shift() test.same ["indent", "=>"], tokens.shift() test.done() @@ -54,7 +54,7 @@ module.exports = "'<% else: %>' dedents, begins a code block, and indents": (test) -> tokens = scan "<% else: %>" test.same ["printString", ""], tokens.shift() - test.same ["beginCode", print: false, safe: false], tokens.shift() + test.same ["beginCode", print: false, safe: false, inline: true], tokens.shift() test.same ["dedent"], tokens.shift() test.same ["recordCode", "else"], tokens.shift() test.same ["indent", undefined], tokens.shift() @@ -63,7 +63,7 @@ module.exports = "'<% else if ...: %>' dedents, begins a code block, and indents": (test) -> tokens = scan "<% else if @projects: %>" test.same ["printString", ""], tokens.shift() - test.same ["beginCode", print: false, safe: false], tokens.shift() + test.same ["beginCode", print: false, safe: false, inline: true], tokens.shift() test.same ["dedent"], tokens.shift() test.same ["recordCode", "else if @projects"], tokens.shift() test.same ["indent", undefined], tokens.shift() @@ -72,21 +72,19 @@ module.exports = "<%% prints an escaped <% in data mode": (test) -> tokens = scan "a <%% b <%= '<%%' %>" test.same ["printString", "a <% b "], tokens.shift() - test.same ["beginCode", print: true, safe: false], tokens.shift() + test.same ["beginCode", print: true, safe: false, inline: true], tokens.shift() test.same ["recordCode", "'<%%'"], tokens.shift() test.done() "unexpected newline in code block": (test) -> tokens = scan "foo\nhello <% do 'thing'\n %>" test.same ["printString", "foo\nhello "], tokens.shift() - test.same ["beginCode", print: false, safe: false], tokens.shift() test.same ["fail", "unexpected newline in code block"], tokens.shift() test.done() "unexpected end of template": (test) -> tokens = scan "foo\nhello <% do 'thing'" test.same ["printString", "foo\nhello "], tokens.shift() - test.same ["beginCode", print: false, safe: false], tokens.shift() test.same ["fail", "unexpected end of template"], tokens.shift() test.done()