creatable
Version:
Elegant HTML generation. No templating. Just Javascript.
497 lines (386 loc) • 11.5 kB
text/coffeescript
###
Elegant HTML generation. No templating. Just Javascript.
@author Raine Lourie
@note Created independently from JsonML (http://jsonml.org).
###
if require?
Encoder = require('node-html-encoder').Encoder
encoder = new Encoder('entity')
###
DOM Emulation
###
# Emulated TextNode
class TextNode
constructor: (@textContent)->
@nodeType = 3
toString: ->
require? and encoder.htmlEncode(@textContent) or @textContent
# Emulated Document Fragment
class DocumentFragment
constructor: ->
@nodeType = 11
@childNodes = []
appendChild: (child) ->
@firstChild = child if @childNodes.length is 0
@childNodes.push child
toString: ->
output = ""
i = 0
while i < @childNodes.length
output += @childNodes[i].toString()
i++
output
# Emulated Element
class Element
voidElements: area: 1, base: 1, br: 1, col: 1, command: 1, embed: 1, hr: 1, img: 1, input: 1, keygen: 1, link: 1, meta: 1, param: 1, source: 1, track: 1, wbr: 1
constructor: (@tagName) ->
@attributes = {}
@childNodes = []
@nodeType = 1
hasAttribute: (attrName) ->
attrName of @attributes
getAttribute: (name) ->
@attributes[name]
setAttribute: (name, value) ->
@attributes[name] = value
value
removeChild: (child) ->
console.error "Not implemented."
appendChild: (child) ->
@firstChild = child if @childNodes.length is 0
@childNodes.push child
toString: ->
# opening tag
output = "<" + @tagName
# attributes
for attr of @attributes
output += " " + attr + "=\"" + @attributes[attr] + "\""
# end opening tag
output += ">"
# childNodes
i = 0
while i < @childNodes.length
output += @childNodes[i].toString()
i++
# closing tag
if @tagName not of @voidElements
output += "</" + @tagName + ">"
output
###
A lightweight emulation of the document object. Can be used to render creatable markup as an HTML string instead of a DOM node.
###
emulatedDocument =
createTextNode: (content) ->
new TextNode(content)
createDocumentFragment: ->
new DocumentFragment()
createElement: (tagName) ->
new Element(tagName)
body: new Element("body")
toString: ->
@body.toString()
document = @document || emulatedDocument
setDocument = (doc)->
document = doc
setEmulatedDocument = (doc)->
document = emulatedDocument
###
Regexes
###
regexIdOrClassSeparator = new RegExp("[#.]")
regexIdOrClass = new RegExp("[#.][^#.]+", "g")
###
Private
###
map = (arr, f) ->
output = []
i = 0
while i < arr.length
output.push f(arr[i], i)
i++
output
each = (arr, f) ->
i = 0
while i < arr.length
f arr[i], i
i++
eachObj = (o, f) ->
i = 0
for attr of o
f attr, o[attr], i
i++
filter = (arr, f) ->
output = []
i = 0
while i < arr.length
output.push arr[i] if f(arr[i], i)
i++
output
find = (arr, f) ->
i = 0
while i < arr.length
return arr[i] if f(arr[i])
i++
null
extend = (obj, args...) ->
each args, (source) ->
for prop of source
obj[prop] = source[prop] if source[prop] isnt undefined
obj
toObject = (arr, f) ->
extend({}, (f(x) for x in arr)...)
keyValue = (a, b) ->
o = {}
o[a] = b
o
orderedGroup = (arr, propOrFunc) ->
getGroupKey = (if typeof (propOrFunc) is "function" then propOrFunc else (item) ->
item[propOrFunc]
)
results = []
dict = {}
i = 0
while i < arr.length
key = getGroupKey(arr[i])
unless key of dict
dict[key] = []
results.push
key: key
items: dict[key]
dict[key].push arr[i]
i++
results
###
Indexes into an array, supports negative indices.
###
index = (arr, i) ->
# one modulus to get in range, another to eliminate negative
arr[(i % arr.length + arr.length) % arr.length]
typeOf = (value) ->
s = typeof value
if s is "object"
if value
s = "array" if typeof value.length is "number" and not (value.propertyIsEnumerable("length")) and typeof value.splice is "function"
else
s = "null"
s
curry = (fn, args...)->
(args2...) ->
fn.apply this, args.concat(args2)
splitOnce = (str, delim) ->
components = str.split(delim)
result = [components.shift()]
result.push components.join(delim) if components.length
result
###
Functional, nondestructive version of Array.prototype.splice.
###
splice = (arr, index, howMany, elements...) ->
results = []
len = arr.length
# add starting elements
i = 0
while i < index and i < len
results.push arr[i]
i++
# add inserted elements
i = 0
elementsLen = elements.length
while i < elementsLen
results.push elements[i]
i++
# add ending elements
i = index + howMany
while i < len
results.push arr[i]
i++
results
###
Public
###
###
Creates a DOM element. Supported objects are defined in the types array.
###
create = (arg, doc=document || emulatedDocument) ->
match = find(types, (creatable) -> creatable.isOfType arg)
(if match then match.build(arg, doc) else error("Unbuildable create argument: " + arg, arg))
createHtml = (arg)->
create(arg, emulatedDocument).toString()
###
A list of objects that the create function can create DOM elements from.
In order of most to least common.
###
types = [
# markup array
isOfType: (o) -> o instanceof Array and o.length and typeof(o[0]) is 'string'
build: (o, doc) -> createFromMarkupArray o, doc
,
# content
isOfType: (o) -> typeof o is "string" or typeof o is "number"
build: (o, doc) -> doc.createTextNode o
,
# function
isOfType: (o) -> typeof o is "function"
build: (o, doc) -> o(doc)
,
# null or undefined
isOfType: (o) -> !o?
build: (o) -> o
,
# DOM node
isOfType: (o) -> isDomNode o
build: (o) -> o
,
# jQuery
isOfType: (o) -> typeof (jQuery) isnt "undefined" and o instanceof jQuery
build: (o) -> o[0]
,
# fragment
isOfType: (o) -> o instanceof Array and (!o.length or o[0] instanceof Array)
build: (o, doc) ->
fragment = doc.createDocumentFragment()
fragment.appendChild(create(child, doc)) for child in o
fragment
]
# insert content as unescaped HTML with { html: true }
plugins = html: (el, html) ->
el.innerHTML = el.firstChild.nodeValue if html and el.firstChild
###
Parsing Functions
###
###
Parses the given markup array and returns a newly created element.
###
createFromMarkupArray = (markup, doc) ->
attrsOmitted = typeOf(markup[1]) isnt "object"
tagInput = markup[0]
attrs = (if not attrsOmitted then markup[1] else {})
children = markup[(if attrsOmitted then 1 else 2)]
# split the tag input by spaces to extract descendants
tags = splitOnce(tagInput, " ")
tagNameString = tags[0]
descendantTags = tags[1]
if descendantTags
children = [[descendantTags, attrs, children]]
attrs = {}
# create the element and parse its attributes and children
element = undefined
try
tagName = parseTagName(tagNameString)
element = doc.createElement(tagName)
catch e
error "Invalid tag name: " + tagName, markup
# queue custom attribute plugins. they aren't executed immediately because we need to remove the plugin attributes and add the children and normal attributes
pluginActions = []
eachObj plugins, (pluginAttr, f) ->
if pluginAttr of attrs
pluginActions.push curry(f, element, attrs[pluginAttr]) if attrs[pluginAttr]
delete attrs[pluginAttr]
selectorAttrs = parseSelectorAttributes(tagNameString)
addAttributes element, mergeAttributes(attrs, selectorAttrs)
if children?
addChildren element, children, doc
# exucute the attribute plugins
each pluginActions, (f) ->
f()
element
###
Returns the tag name from a tag name string that could have CSS selector syntax.
###
parseTagName = (tagNameString) ->
tagNameString.split(regexIdOrClassSeparator)[0] or "div"
###
Parses the tagName for CSS selector syntax and returns an object of attribute names and values.
###
parseSelectorAttributes = (tagNameString) ->
attrMap =
"#": "id"
".": "class"
afterSep = tagNameString.substring(tagNameString.indexOf(regexIdOrClassSeparator))
selectors = afterSep.match(regexIdOrClass) or []
# transform the list of selector strings to a list of objects so that they can be grouped by attribute
selObjects = map(selectors, (sel) ->
sep: attrMap[sel[0]]
name: sel.substring(1)
)
# group the same selectors together so that a final attribute value can be determined from multiples, then convert the groups into a single object to be returned as the attribute .
toObject orderedGroup(selObjects, "sep"), (g) ->
# joins duplicate classes with a space, otherwise just uses the last value
keyValue g.key, (if g.key is "class" then (item.name for item in g.items).join(" ") else index(g.items, -1).name)
###
Parses the attributes and adds them to the element.
###
addAttributes = (element, attrs) ->
for attr of attrs
if attr is "checked" or attr is "disabled" or attr is "selected"
element.setAttribute attr, attr if attrs[attr]
else element.setAttribute attr, attrs[attr] if attrs[attr]?
###
Returns true if the given class value string contains the given class.
###
containsClass = (str, className) ->
str and (" " + str + " ").indexOf(" " + className + " ") > -1
###
Adds the given children to the element.
###
addChildren = (node, children, doc) ->
children = [children] if typeof children is "string" or typeof children is "number"
node.appendChild create(child, doc) for child in children when child?
###
Helper Functions
###
###
Returns true if the given object seems to be a DomNode.
###
isDomNode = (node) ->
node and typeof node.nodeType is "number"
# omit further tests for performance.
#arr.length >= 1 && arr.length <= 3 && // 1, 2, or 3 items
#typeof arr[0] === "string" // tagname is a string
mergeAttributes = (a, b) ->
uniqueClass = (singleClass) ->
not containsClass(a["class"], singleClass)
output = {}
for aProp of a
output[aProp] = a[aProp]
for bProp of b
output[bProp] = b[bProp]
# merge class attributes
output["class"] = (if a["class"] and b["class"] then [].concat(a["class"], filter(b["class"].split(" "), uniqueClass)).join(" ") else a["class"] or b["class"])
output
###
Error Handling
###
###
Abstracts the error handling for Creatable so that we can substitute a different handler if necessary.
###
error = (message, params) ->
console.error params if params
throw new Error(message or "ERROR")
###
Render Helper
###
render = (markup) ->
body = document.body
body.removeChild body.firstChild while body.firstChild
body.appendChild create(markup)
# export public interface
extend (exports? and exports or @Creatable = {}),
emulatedDocument: emulatedDocument
setDocument: setDocument
TextNode: TextNode
Element: Element
DocumentFragment: DocumentFragment
create: create
createHtml: createHtml
types: types
plugins: plugins
createFromMarkupArray: createFromMarkupArray
parseTagName: parseTagName
parseSelectorAttributes:parseSelectorAttributes
addAttributes: addAttributes
containsClass: containsClass
isDomNode: isDomNode
mergeAttributes: mergeAttributes
error: error
render: render