jot.coffee | |
---|---|
This file contains the Jot "engine" for authoring markup. | |
TODO:
| |
Require scope | {scope, COFFEESCRIPT_HELPERS } = require './scope' |
Helpers | |
isFunction, isArray, isString are borrowed from underscorejs | isFunction = (x) -> !!(x and x.constructor and x.call and x.apply)
isString = (x) -> !!(x is '' or (x and x.charCodeAt and x.substr))
isArray = (x) -> x and x.slice and x.join
isNumber = (x) -> x? and x.toPrecision and x.toExponential |
Escape HTML entities | escapeXml = (x) ->
x = x.replace /&/g, '&'
x = x.replace />/g, '>'
x = x.replace /</g, '<' |
Remove values from an array. Modifies the array. | remove = (arr, vals...) ->
i = arr.length;
while i--
arr.splice i, 1 if arr[i] in vals |
jQuery's extend: copy attributes from one object to another. | extend = (x, more...) -> x[k] = v for k,v of o for o in more; x
|
Given a level of indentation, return the whitespace needed to indent. | indent = (level) -> (INDENT for i in [0...level]).join '' |
The default indent is two spaces | INDENT = ' ' |
Class NodeA node in the markup tree. | exports.Node = class Node |
Constructor takes the tagname | constructor: (@tag) ->
@attrs = {}
@children = []
|
Get or set this node's tagname | tagname: (newtag) ->
if newtag then @tag = newtag; @
else @tag
|
Adds children to this node | append: (childs...) -> @children.push c for c in childs; @ |
Sets attributes on this node | attr: (k, v) ->
if isString k then @attrs[k] = v
else @attr k2, v for k2, v of k
@ |
Returns an xml-like string representation of this node. | toString: (format, level = 0) ->
@streamString arr = [], format, level
arr.join ''
|
Use an array like a stream for efficient string append. | streamString: (stream, format, level) ->
newline = if format then "\n#{indent level}" else ''
s = (s) -> stream.push s
s "#{newline}<#{@tag}"
s "#{(" #{k}=\"#{v}\"" for k, v of @attrs).join ''}"
if @children.length is 0
s '/>'
else
s '>'
for c in @children
c.streamString?(stream, format, level + 1) or s c
if c instanceof Node then heavy = true
s newline if heavy
s "</#{@tag}>"
true
|
Class ContentA context for building markup fragments. All tags and macros added to this context become instance properties that can be called outside of a scoped function (for convenience). | exports.Context = class Context
REG_NOESCAPE = /^\s*`([\s\S]*)/ # A backtick means "do not escape text"
constructor: ({@tags}) ->
@tags ?= []
@format = true
|
Keeps track of various aspects of the context While tags and macros are being called. | @state = |
The current value of 'this' inside a macro or mixin. | self: null, |
The current node being operated on. | node: null, |
The current list of nodes without parents. | orphans: []
|
Keep track of scoped functions | @scoped = [] |
Setup the initial local scope | @locals = {} |
Declare all tags | @declareTag tag for tag in @tags |
Add CoffeeScript helpers in if macros or scoped functions are written in coffee-script | @local COFFEESCRIPT_HELPERS |
Other local functions | @local
node: => @state.node
$: => @inlineTemplate arguments...
@delegateToNode 'append', 'attr', 'addClass', 'tagname'
@alias text: 'append'
|
Push a state change, call f, then pop the change | withState: (o, f) -> |
push state | old = {}; for k of o
old[k] = @state[k]; @state[k] = o[k]
ret = f()
|
pop state | @state[k] = old[k] for k of o
ret
|
functions that delegate to the current node made available in lexical scope | delegateToNode: (names...) ->
for name in names then do (name) =>
@local name, @[name] = => @state.node[name] arguments...
|
Public functions for modifying the local scope. | |
Add variable(s) to local scope. | local: (nameOrHash, value) ->
if isString nameOrHash
if value? then @locals[nameOrHash] = value
else @locals[nameOrHash]
else @local k, v for k, v of nameOrHash
|
Remove variable(s) from local scope | removeLocal: (names...) -> delete @local[name] for name in names
|
Alias a variable in local scope | alias: (o) ->
@local name, @local dest for name, dest of o
|
create a tag with the given name and args This is a helper function. Assumes all function arguments are compiled. | callTag: (tag, args) =>
@nodeCall n = new @Node(tag), args |
Add new node to orphans until we can find it a parent. | @state.orphans.push n; n
|
Temporary | declare: (names...) ->
@local name, 0 for name in names
|
Add a simple tag function to the local scope | declareTag: (tag, tagname = tag) ->
@local tag, => @callTag tagname, arguments
@[tag] = (args...) =>
@callTag tagname,
isFunction(x) and @scope(x) or x for x in args
@render [@state.orphans.pop()]
|
The Node class to instantiate | Node: Node
|
Call fn with current state.self as 'this' Append any emitted orphans to current node. | mixin: (fn) ->
ret = null
nodes = @trapOrphans true, => ret = fn.call @state.self |
template | if isString ret then @processArgString ret |
mixin | else @append nodes...
|
Process an argument passed to a tag call | processArg: (arg) ->
arg = arg.toString() if isNumber arg
|
The case when an emitting function is called in an argument to this node. | if isArray arg then @processArg a for a in arg
else if (node = arg) instanceof @Node
@append node; @state.orphans =
(n for n in @state.orphans when n isnt node)
|
Call the function and append the emitted nodes f is already scoped. | else if isFunction arg then @mixin arg
else if isString arg then @processArgString arg
else @attr arg |
Process a string argument | processArgString: (s) ->
@append if m = s.match REG_NOESCAPE then m[1] else escapeXml s
nodeCall: (n, args) ->
@withState node: n, => @processArg arg for arg in args
trapOrphans: (remove, f) ->
len = @state.orphans.length
f()
ret = @state.orphans[len..]
@state.orphans = @state.orphans[...len] if remove
ret
|
The external representation of an array of nodes. The default is to stringify them | render: (nodes) -> @stringify nodes
|
Return a single string of all nodes combined. | stringify: (nodes) ->
(n.toString @format for n in nodes).join('').substr 1 #\n
|
Allow flexible calling of function
If a name is detected, result will be
added to local scope
obj arguments are is a hash | @flexargs: (needsRender, impl) ->
externify = (intern) ->
if needsRender
(args...) =>
@render @trapOrphans true, ->
intern.apply null, args
else intern
f = (name, locals, fn) -> |
case: (name, locals, fn) or (name, fn) | if isString name
[locals, fn] = [null, locals] if not fn
intern = @local name, impl.call @, locals, fn
intern._name = name
@[name] = externify.call @, intern
|
Eliminate possibility of obj case: (locals, fn) or (fn, locals) | else if isFunction(name) or b = isFunction locals
[locals, fn] =
if b
[name, locals]
else
[locals, name]
intern = impl.call @, locals, fn
externify.call @, intern
|
We are dealing with an obj | else
|
case: (locals, obj) | if (obj = locals) then locals = name
|
case: (obj) | else obj = name
f.call @, name, locals, fn for name, fn of obj
scope:
@flexargs false, (locals, fn) ->
fn = scope extend({}, @locals, locals), fn
fn.moreLocals = locals
@scoped.push fn; {state} = @
(args...) -> fn.apply state.self, args
|
The value of 'this' is the first argument passed to the macro when called. macros look and behave like tags, only they generate more than a single tag of content. Macros get wrapped and return an array of top-level nodes created in its scope. | macro:
@flexargs true, (locals, fn) ->
@inlineMacro @scope locals, fn;
|
an 'inline' function is one that is defined inside a scoped function. | inlineMacro: (fn) ->
{state} = @
(args...) =>
call = -> fn.apply state.self, args
@MacroResult \
@trapOrphans false, =>
if args.length then @withState self: args[0], call
else call()
MacroResult: (nodes) ->
nodes.call = => @nodeCall n, arguments for n in nodes; nodes
nodes.toString = => @stringify nodes
nodes
|
if a macro expands to a list of nodes, a template expands to a string. It's like an inside-out macro: the nodes are embedded in the string, rather than vice-versa. | template:
@flexargs false, (locals, fn) ->
@inlineTemplate @scope locals, fn;
inlineTemplate: (fn) ->
{state} = @
(args...) =>
str = null
call = -> str = fn.apply state.self, args |
Remove orphans... they are embedded in the string | @trapOrphans true, =>
if args.length then @withState self: args[0], call
else call()
str |
Update scoped functions with current locals | rescope: ->
for fn in @scoped
fn.scope extend({}, @locals, fn.moreLocals)
|
Compile and call the function | run: (locals, fn, args...) ->
if isFunction locals
args.unshift fn; fn = locals; locals = null
@macro(locals, fn) args... |
An HTML Context | ADD_DEFAULT_CSS_CLASS = true
exports.HtmlNode = class HtmlNode extends Node
addClass: (c) ->
cls = (@attrs.class or '') + ' ' + c
@attrs.class = (c for c in cls.split ' ' when c).join ' '
@
exports.HtmlContext = class HtmlContext extends Context
|
mined from http://www.w3schools.com/html5/html5_reference.asp | HTMLTAGS = "a,abbr,acronym,address,applet,area,article,aside,audio,b,base,basefont,bdo,big,blockquote,body,br,button,canvas,caption,center,cite,code,col,colgroup,command,datalist,dd,del,details,dfn,dir,div,dl,dt,em,embed,fieldset,figcaption,figure,font,footer,form,frame,frameset,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,i,iframe,img,input,ins,keygen,kbd,label,legend,li,link,map,mark,menu,meta,meter,nav,noframes,noscript,object,ol,optgroup,option,output,p,param,pre,progress,q,rp,rt,ruby,s,samp,script,section,select,small,source,span,strike,strong,style,sub,summary,sup,table,tbody,td,textarea,tfoot,th,thead,time,title,tr,tt,u,ul,video,wbr,xmp".split ','
REG_CLASS = /^\.\w/
REG_ID = /^#(\w*)/
constructor: ->
super tags: HTMLTAGS
@alias
_: 'div'
o: 'div'
@delegateToNode 'addClass'
@macro
nav: -> _ '.nav', @
@declareTag 'DOCTYPE', '!DOCTYPE'
@declareTag 'var_', 'var'
Node: HtmlNode
processArgString: (s) ->
if s.match REG_CLASS then @addClass s.split('.').join ' '
else if m = s.match REG_ID then @attr 'id', m[1]
else super s
|