Skip to content
This repository has been archived by the owner on Jun 10, 2018. It is now read-only.

Async Eco #11

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
65 changes: 54 additions & 11 deletions src/eco/compiler.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,65 @@ 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

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);
Expand Down Expand Up @@ -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) ->
Expand Down
23 changes: 16 additions & 7 deletions src/eco/preprocessor.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}"
Expand All @@ -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
Expand Down
11 changes: 7 additions & 4 deletions src/eco/scanner.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<%%"
Expand All @@ -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"
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/eco/util.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/blocks-async.coffee
Original file line number Diff line number Diff line change
@@ -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'
10 changes: 10 additions & 0 deletions test/fixtures/blocks.coffee
Original file line number Diff line number Diff line change
@@ -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'
5 changes: 5 additions & 0 deletions test/fixtures/blocks.eco
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<%= @emit => %>
<%- @emit (data) -> %>
<%= "&" %>
<% end %>
<% end %>
5 changes: 5 additions & 0 deletions test/fixtures/blocks.out.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@


&amp;


1 change: 1 addition & 0 deletions test/fixtures/error.eco
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= @emit => %><% end %>
8 changes: 8 additions & 0 deletions test/test_compile.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions test/test_preprocessor.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions test/test_render.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<[email protected]>"
Expand Down
28 changes: 13 additions & 15 deletions test/test_scanner.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,59 @@

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()

"': %>' 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()

"'-> %>' 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()

"'=> %>' 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()

"'<% 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()
Expand All @@ -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()
Expand All @@ -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()