Jump To …

jot.coffee

This file contains the Jot "engine" for authoring markup.

TODO:

  • Import util dependencies.
  • Import scope dependency.
  • Move Node::toString implementation into subclasses

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, '&lt;'

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 Node

A 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 Content

A 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 Options: - (name, fn) - (name, locals, fn) - (fn) - (locals, fn) - (fn, locals) - (locals, obj) - (obj)

  @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