This will be the core thing that takes a source file or string and turns it into a browser-side Javascript function-expression. When run, each such function returns a DOM tree. Some functions might act as templates that take arguments.
I'm calling this submodule compiler
, but it's also the runtime
environment. Because the runtime environment includes the compiler,
obviously. Because it also needs to be able to act as an interpreter.
Probably.
So what's the language we're compiling? Basically it looks like
plain text. With a few things borrowed from or inspired by Markdown.
So, for example, *this*
is emphasized (italicized, or whatever
else the stylesheet wants to do with it).
Unlike Markdown, #{this}
is interpolated CoffeeScript code, with
the result evaluated at the time the text is displayed and inserted
at that point in the document. A paragraph starting with %do
runs CoffeeScript when the text is displayed and ignores any result.
%say
is like %do
but treats the result as one or more paragraphs
or other block-level elements to insert.
I'll add more later, but let me see if I can even get that much working.
exports.quote = quote = (s) ->
s = s
.replace /([\\'])/g, '\\$1'
.replace /\n/g, '\\n'
.replace /\r/g, '\\r'
return "'#{s}'"
exports.stdlib = (imbroglio = {}) ->
imbroglio.elem or= (tag, attrs = {}, children...) ->
result = window.document.createElement tag
result.setAttribute k, v for k, v of attrs
addChild = (child) =>
if not child? or child is '' then return
if child instanceof Array
addChild c for c in child
return
if not child.cloneNode
child = "#{child}"
if @state?.smartQuotes
child = child
.replace /(\s)"/g, '$1\u201c' # ldquo
.replace /^"(\w)/g, '\u201c$1' # ldquo
.replace '"', '\u201d' # rdquo
.replace /(\s)'/g, '$1\u2018' # lsquo
.replace /^'(\w)/g, '\u2018$1' # lsquo
.replace "'", '\u2019' # rsquo
if @state?.smartPunct
child = child
.replace /\s+--\s+/g, '\u2009\u2014\u2009' # thinsp mdash thinsp
.replace /--\s+/g, '\u2014\u2009' # mdash thinsp
.replace /\s+--/g, '\u2009\u2014' # thinsp mdash
.replace /--/g, '\u2014' # mdash
.replace /\.{3}/g, '\u2026' # hellip
child = window.document.createTextNode child
result.appendChild child
return
addChild child for child in children
result
imbroglio
assert = require 'assert'
CoffeeScript = require 'coffee-script'
{Scope} = require 'coffee-script/lib/coffee-script/scope'
nodes = require 'coffee-script/lib/coffee-script/nodes'
class Compiler
constructor: (@opts) ->
@referencedVars = []
@scope = new Scope null, null, null, @referencedVars # XXX remove
refTokens: (tokens) ->
for token in tokens
if token.variable
@referencedVars.push token[1]
return
lit: (x) -> new nodes.Literal x
val: (x, props...) -> new nodes.Value x, props
litval: (x) -> @val @lit x
assign: (k, v) -> new nodes.Assign @val(k), v
field: (lit, field) -> @val lit, new nodes.Access @lit field
string: (s) -> @litval quote s
call: (fn, args...) ->
for arg in args
assert arg.compileToFragments
new nodes.Call fn, args
callname: (name, args...) -> @call @litval(name), args...
block: (children) -> new nodes.Block children
ret: (result) -> new nodes.Return result
blockret: (result) -> @block [@ret result]
wrap: (ast) ->
if not @opts.thisVar then return ast
@blockret @call(@field(new nodes.Parens(@block [new nodes.Code([], ast)]), 'call'), @litval @opts.thisVar)
text: (s) -> @string s
obj: (obj) ->
attrs = for k, v of obj
assert 'string' is typeof v, v
new nodes.Assign @string(k), @string(v), 'object'
@val new nodes.Obj attrs
elem: (tag, attrs = {}, children...) ->
@callname 'imbroglio.elem', @string(tag), @obj(attrs), children...
main: (result) ->
@scope.expressions = @wrap @blockret result
return scope: @scope, ast: @scope.expressions, level: 1, indent: ''
exports.parse = parse = (src, opts = {}) ->
compiler = new Compiler opts
codeBegin = '#{'
codeEnd = '}'
pp = for p in src.split /\n\s*\n/
if not /\S/.test p then continue
pieces = []
idx = 0
loop
found = p.indexOf codeBegin, idx
if found < 0 then found = p.length
pieces.push compiler.text p.substring idx, found
if found == p.length then break
start = found + codeBegin.length
end = start - 1
error = true
loop
end = p.indexOf codeEnd, end + 1
if end < 0
idx = p.length
break
code = p.substring start, end
try
tokens = CoffeeScript.tokens code
ast = CoffeeScript.nodes tokens
catch e
error = e
continue
error = null
compiler.refTokens tokens
pieces.push ast
idx = end + codeEnd.length
break
if error
if opts.handleError then opts.handleError {error}
pieces.push compiler.elem 'span', {class: 'error', title: error.toString()}, compiler.text codeBegin
idx = start
compiler.elem 'p', {}, pieces...
return compiler.main compiler.elem 'div', {class: 'passage'}, pp...
exports.compile = compile = (src, opts) ->
o = parse src, opts
# To get a source map, I'll need to use ast.compileToFragments().
# Look at what CoffeeScript.compile() is doing....
fragments = o.ast.compileWithDeclarations o
(fragment.code for fragment in fragments).join ''
exports.prepare = prepare = (src, opts) ->
code = compile src, opts
varNames = if not opts.vars then [] else (k for k of opts.vars)
argNames = varNames.concat opts.argNames or []
f = new Function argNames..., code
if varNames.length
f = f.bind null, (opts.vars[k] for k in varNames)...
return f
exports.render = render = (src, opts) -> prepare(src, opts)()