Skip to content

Instantly share code, notes, and snippets.

@cognitom
Created July 31, 2015 11:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cognitom/b185a7a1abfc3b876005 to your computer and use it in GitHub Desktop.
Save cognitom/b185a7a1abfc3b876005 to your computer and use it in GitHub Desktop.
Riot.js WIP
/* Riot WIP, @license MIT, (c) 2015 Muut Inc. + contributors */
;(function(window, undefined) {
'use strict'
var riot = { version: 'WIP', settings: {} }
// This globals 'const' helps code size reduction
// for typeof == '' comparisons
var T_STRING = 'string',
T_OBJECT = 'object',
T_UNDEF = 'undefined'
// for IE8 and rest of the world
/* istanbul ignore next */
var isArray = Array.isArray || (function () {
var _ts = Object.prototype.toString
return function (v) { return _ts.call(v) === '[object Array]' }
})()
// Version# for IE 8-11, 0 for others
var ieVersion = (function (win) {
return (window && window.document || {}).documentMode | 0
})()
riot.observable = function(el) {
el = el || {}
var callbacks = {},
_id = 0
el.on = function(events, fn) {
if (isFunction(fn)) {
if (typeof fn.id === T_UNDEF) fn._id = _id++
events.replace(/\S+/g, function(name, pos) {
(callbacks[name] = callbacks[name] || []).push(fn)
fn.typed = pos > 0
})
}
return el
}
el.off = function(events, fn) {
if (events == '*') callbacks = {}
else {
events.replace(/\S+/g, function(name) {
if (fn) {
var arr = callbacks[name]
for (var i = 0, cb; (cb = arr && arr[i]); ++i) {
if (cb._id == fn._id) arr.splice(i--, 1)
}
} else {
callbacks[name] = []
}
})
}
return el
}
// only single event supported
el.one = function(name, fn) {
function on() {
el.off(name, on)
fn.apply(el, arguments)
}
return el.on(name, on)
}
el.trigger = function(name) {
var args = [].slice.call(arguments, 1),
fns = callbacks[name] || []
for (var i = 0, fn; (fn = fns[i]); ++i) {
if (!fn.busy) {
fn.busy = 1
fn.apply(el, fn.typed ? [name].concat(args) : args)
if (fns[i] !== fn) { i-- }
fn.busy = 0
}
}
if (callbacks.all && name != 'all') {
el.trigger.apply(el, ['all', name].concat(args))
}
return el
}
return el
}
riot.mixin = (function() {
var mixins = {}
return function(name, mixin) {
if (!mixin) return mixins[name]
mixins[name] = mixin
}
})()
;(function(riot, evt, win) {
// browsers only
if (!win) return
var loc = win.location,
fns = riot.observable(),
started = false,
current
function hash() {
return loc.href.split('#')[1] || ''
}
function parser(path) {
return path.split('/')
}
function emit(path) {
if (path.type) path = hash()
if (path != current) {
fns.trigger.apply(null, ['H'].concat(parser(path)))
current = path
}
}
var r = riot.route = function(arg) {
// string
if (arg[0]) {
loc.hash = arg
emit(arg)
// function
} else {
fns.on('H', arg)
}
}
r.exec = function(fn) {
fn.apply(null, parser(hash()))
}
r.parser = function(fn) {
parser = fn
}
r.stop = function () {
if (!started) return
win.removeEventListener ? win.removeEventListener(evt, emit, false) : win.detachEvent('on' + evt, emit)
fns.off('*')
started = false
}
r.start = function () {
if (started) return
win.addEventListener ? win.addEventListener(evt, emit, false) : win.attachEvent('on' + evt, emit)
started = true
}
// autostart the router
r.start()
})(riot, 'hashchange', window)
/*
//// How it works?
Three ways:
1. Expressions: tmpl('{ value }', data).
Returns the result of evaluated expression as a raw object.
2. Templates: tmpl('Hi { name } { surname }', data).
Returns a string with evaluated expressions.
3. Filters: tmpl('{ show: !done, highlight: active }', data).
Returns a space separated list of trueish keys (mainly
used for setting html classes), e.g. "show highlight".
// Template examples
tmpl('{ title || "Untitled" }', data)
tmpl('Results are { results ? "ready" : "loading" }', data)
tmpl('Today is { new Date() }', data)
tmpl('{ message.length > 140 && "Message is too long" }', data)
tmpl('This item got { Math.round(rating) } stars', data)
tmpl('<h1>{ title }</h1>{ body }', data)
// Falsy expressions in templates
In templates (as opposed to single expressions) all falsy values
except zero (undefined/null/false) will default to empty string:
tmpl('{ undefined } - { false } - { null } - { 0 }', {})
// will return: " - - - 0"
*/
var brackets = (function(orig) {
var cachedBrackets,
r,
b,
re = /[{}]/g
return function(x) {
// make sure we use the current setting
var s = riot.settings.brackets || orig
// recreate cached vars if needed
if (cachedBrackets !== s) {
cachedBrackets = s
b = s.split(' ')
r = b.map(function (e) { return e.replace(/(?=.)/g, '\\') })
}
// if regexp given, rewrite it with current brackets (only if differ from default)
return x instanceof RegExp ? (
s === orig ? x :
new RegExp(x.source.replace(re, function(b) { return r[~~(b === '}')] }), x.global ? 'g' : '')
) :
// else, get specific bracket
b[x]
}
})('{ }')
var tmpl = (function() {
var cache = {},
reVars = /(['"\/]).*?[^\\]\1|\.\w*|\w*:|\b(?:(?:new|typeof|in|instanceof) |(?:this|true|false|null|undefined)\b|function *\()|([a-z_$]\w*)/gi
// [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ]
// find variable names:
// 1. skip quoted strings and regexps: "a b", 'a b', 'a \'b\'', /a b/
// 2. skip object properties: .name
// 3. skip object literals: name:
// 4. skip javascript keywords
// 5. match var name
// build a template (or get it from cache), render with data
return function(str, data) {
return str && (cache[str] = cache[str] || tmpl(str))(data)
}
// create a template instance
function tmpl(s, p) {
// default template string to {}
s = (s || (brackets(0) + brackets(1)))
// temporarily convert \{ and \} to a non-character
.replace(brackets(/\\{/g), '\uFFF0')
.replace(brackets(/\\}/g), '\uFFF1')
// split string to expression and non-expresion parts
p = split(s, extract(s, brackets(/{/), brackets(/}/)))
return new Function('d', 'return ' + (
// is it a single expression or a template? i.e. {x} or <b>{x}</b>
!p[0] && !p[2] && !p[3]
// if expression, evaluate it
? expr(p[1])
// if template, evaluate all expressions in it
: '[' + p.map(function(s, i) {
// is it an expression or a string (every second part is an expression)
return i % 2
// evaluate the expressions
? expr(s, true)
// process string parts of the template:
: '"' + s
// preserve new lines
.replace(/\n/g, '\\n')
// escape quotes
.replace(/"/g, '\\"')
+ '"'
}).join(',') + '].join("")'
)
// bring escaped { and } back
.replace(/\uFFF0/g, brackets(0))
.replace(/\uFFF1/g, brackets(1))
+ ';')
}
// parse { ... } expression
function expr(s, n) {
s = s
// convert new lines to spaces
.replace(/\n/g, ' ')
// trim whitespace, brackets, strip comments
.replace(brackets(/^[{ ]+|[ }]+$|\/\*.+?\*\//g), '')
// is it an object literal? i.e. { key : value }
return /^\s*[\w- "']+ *:/.test(s)
// if object literal, return trueish keys
// e.g.: { show: isOpen(), done: item.done } -> "show done"
? '[' +
// extract key:val pairs, ignoring any nested objects
extract(s,
// name part: name:, "name":, 'name':, name :
/["' ]*[\w- ]+["' ]*:/,
// expression part: everything upto a comma followed by a name (see above) or end of line
/,(?=["' ]*[\w- ]+["' ]*:)|}|$/
).map(function(pair) {
// get key, val parts
return pair.replace(/^[ "']*(.+?)[ "']*: *(.+?),? *$/, function(_, k, v) {
// wrap all conditional parts to ignore errors
return v.replace(/[^&|=!><]+/g, wrap) + '?"' + k + '":"",'
})
}).join('')
+ '].join(" ").trim()'
// if js expression, evaluate as javascript
: wrap(s, n)
}
// execute js w/o breaking on errors or undefined vars
function wrap(s, nonull) {
s = s.trim()
return !s ? '' : '(function(v){try{v='
// prefix vars (name => data.name)
+ (s.replace(reVars, function(s, _, v) { return v ? '(d.'+v+'===undefined?'+(typeof window == 'undefined' ? 'global.' : 'window.')+v+':d.'+v+')' : s })
// break the expression if its empty (resulting in undefined value)
|| 'x')
+ '}catch(e){'
+ '}finally{return '
// default to empty string for falsy values except zero
+ (nonull === true ? '!v&&v!==0?"":v' : 'v')
+ '}}).call(d)'
}
// split string by an array of substrings
function split(str, substrings) {
var parts = []
substrings.map(function(sub, i) {
// push matched expression and part before it
i = str.indexOf(sub)
parts.push(str.slice(0, i), sub)
str = str.slice(i + sub.length)
})
// push the remaining part
return parts.concat(str)
}
// match strings between opening and closing regexp, skipping any inner/nested matches
function extract(str, open, close) {
var start,
level = 0,
matches = [],
re = new RegExp('('+open.source+')|('+close.source+')', 'g')
str.replace(re, function(_, open, close, pos) {
// if outer inner bracket, mark position
if (!level && open) start = pos
// in(de)crease bracket level
level += open ? 1 : -1
// if outer closing bracket, grab the match
if (!level && close != null) matches.push(str.slice(start, pos+close.length))
})
return matches
}
})()
// { key, i in items} -> { key, i, items }
function loopKeys(expr) {
var b0 = brackets(0),
els = expr.trim().slice(b0.length).match(/^\s*(\S+?)\s*(?:,\s*(\S+))?\s+in\s+(.+)$/)
return els ? { key: els[1], pos: els[2], val: b0 + els[3] } : { val: expr }
}
function mkitem(expr, key, val) {
var item = {}
item[expr.key] = key
if (expr.pos) item[expr.pos] = val
return item
}
/* Beware: heavy stuff */
function _each(dom, parent, expr) {
remAttr(dom, 'each')
var tagName = getTagName(dom),
template = dom.outerHTML,
hasImpl = !!tagImpl[tagName],
impl = tagImpl[tagName] || {
tmpl: template
},
root = dom.parentNode,
placeholder = document.createComment('riot placeholder'),
tags = [],
child = getTag(dom),
checksum
root.insertBefore(placeholder, dom)
expr = loopKeys(expr)
// clean template code
parent
.one('premount', function () {
if (root.stub) root = parent.root
// remove the original DOM node
dom.parentNode.removeChild(dom)
})
.on('update', function () {
var items = tmpl(expr.val, parent)
// object loop. any changes cause full redraw
if (!isArray(items)) {
checksum = items ? JSON.stringify(items) : ''
items = !items ? [] :
Object.keys(items).map(function (key) {
return mkitem(expr, key, items[key])
})
}
var frag = document.createDocumentFragment(),
i = tags.length,
j = items.length
// unmount leftover items
while (i > j) {
tags[--i].unmount()
tags.splice(i, 1)
}
for (i = 0; i < j; ++i) {
var _item = !checksum && !!expr.key ? mkitem(expr, items[i], i) : items[i]
if (!tags[i]) {
// mount new
(tags[i] = new Tag(impl, {
parent: parent,
isLoop: true,
hasImpl: hasImpl,
root: hasImpl ? dom.cloneNode() : root,
item: _item
}, dom.innerHTML)
).mount()
frag.appendChild(tags[i].root)
} else
tags[i].update(_item)
tags[i]._item = _item
}
root.insertBefore(frag, placeholder)
if (child) parent.tags[tagName] = tags
}).one('updated', function() {
var keys = Object.keys(parent)// only set new values
walk(root, function(node) {
// only set element node and not isLoop
if (node.nodeType == 1 && !node.isLoop && !node._looped) {
node._visited = false // reset _visited for loop node
node._looped = true // avoid set multiple each
setNamed(node, parent, keys)
}
})
})
}
function parseNamedElements(root, parent, childTags) {
walk(root, function(dom) {
if (dom.nodeType == 1) {
dom.isLoop = dom.isLoop || (dom.parentNode && dom.parentNode.isLoop || dom.getAttribute('each')) ? 1 : 0
// custom child tag
var child = getTag(dom)
if (child && !dom.isLoop) {
var tag = new Tag(child, { root: dom, parent: parent }, dom.innerHTML),
tagName = getTagName(dom),
ptag = parent,
cachedTag
while (!getTag(ptag.root)) {
if (!ptag.parent) break
ptag = ptag.parent
}
// fix for the parent attribute in the looped elements
tag.parent = ptag
cachedTag = ptag.tags[tagName]
// if there are multiple children tags having the same name
if (cachedTag) {
// if the parent tags property is not yet an array
// create it adding the first cached tag
if (!isArray(cachedTag))
ptag.tags[tagName] = [cachedTag]
// add the new nested tag to the array
ptag.tags[tagName].push(tag)
} else {
ptag.tags[tagName] = tag
}
// empty the child node once we got its template
// to avoid that its children get compiled multiple times
dom.innerHTML = ''
childTags.push(tag)
}
if (!dom.isLoop)
setNamed(dom, parent, [])
}
})
}
function parseExpressions(root, tag, expressions) {
function addExpr(dom, val, extra) {
if (val.indexOf(brackets(0)) >= 0) {
var expr = { dom: dom, expr: val }
expressions.push(extend(expr, extra))
}
}
walk(root, function(dom) {
var type = dom.nodeType
// text node
if (type == 3 && dom.parentNode.tagName != 'STYLE') addExpr(dom, dom.nodeValue)
if (type != 1) return
/* element */
// loop
var attr = dom.getAttribute('each')
if (attr) { _each(dom, tag, attr); return false }
// attribute expressions
each(dom.attributes, function(attr) {
var name = attr.name,
bool = name.split('__')[1]
addExpr(dom, attr.value, { attr: bool || name, bool: bool })
if (bool) { remAttr(dom, name); return false }
})
// skip custom tags
if (getTag(dom)) return false
})
}
function Tag(impl, conf, innerHTML) {
var self = riot.observable(this),
opts = inherit(conf.opts) || {},
dom = mkdom(impl.tmpl),
parent = conf.parent,
isLoop = conf.isLoop,
hasImpl = conf.hasImpl,
item = cleanUpData(conf.item),
expressions = [],
childTags = [],
root = conf.root,
fn = impl.fn,
tagName = root.tagName.toLowerCase(),
attr = {},
propsInSyncWithParent = [],
loopDom,
TAG_ATTRIBUTES = /([\w\-]+)\s?=\s?['"]([^'"]+)["']/gim
if (fn && root._tag) {
root._tag.unmount(true)
}
// not yet mounted
this.isMounted = false
root.isLoop = isLoop
if (impl.attrs) {
var attrs = impl.attrs.match(TAG_ATTRIBUTES)
each(attrs, function(a) {
var kv = a.split(/\s?=\s?/)
root.setAttribute(kv[0], kv[1].replace(/['"]/g, ''))
})
}
// keep a reference to the tag just created
// so we will be able to mount this tag multiple times
root._tag = this
// create a unique id to this tag
// it could be handy to use it also to improve the virtual dom rendering speed
this._id = fastAbs(~~(Date.now() * Math.random()))
extend(this, { parent: parent, root: root, opts: opts, tags: {} }, item)
// grab attributes
each(root.attributes, function(el) {
var val = el.value
// remember attributes with expressions only
if (brackets(/\{.*\}/).test(val)) attr[el.name] = val
})
if (dom.innerHTML && !/select|optgroup|tr/.test(tagName))
// replace all the yield tags with the tag inner html
dom.innerHTML = replaceYield(dom.innerHTML, innerHTML)
// options
function updateOpts() {
var ctx = hasImpl && isLoop ? self : parent || self
// update opts from current DOM attributes
each(root.attributes, function(el) {
opts[el.name] = tmpl(el.value, ctx)
})
// recover those with expressions
each(Object.keys(attr), function(name) {
opts[name] = tmpl(attr[name], ctx)
})
}
function normalizeData(data) {
for (var key in item) {
if (typeof self[key] !== T_UNDEF)
self[key] = data[key]
}
}
function inheritFromParent () {
if (!self.parent || !isLoop) return
each(Object.keys(self.parent), function(k) {
// some properties must be always in sync with the parent tag
var mustSync = ~propsInSyncWithParent.indexOf(k)
if (typeof self[k] === T_UNDEF || mustSync) {
// track the property to keep in sync
// so we can keep it updated
if (!mustSync) propsInSyncWithParent.push(k)
self[k] = self.parent[k]
}
})
}
this.update = function(data) {
// make sure the data passed will not override
// the component core methods
data = cleanUpData(data)
// inherit properties from the parent
inheritFromParent()
// normalize the tag properties in case an item object was initially passed
if (data && typeof item === T_OBJECT || isArray(item)) {
normalizeData(data)
item = data
}
extend(self, data)
updateOpts()
self.trigger('update', data)
update(expressions, self)
self.trigger('updated')
}
this.mixin = function() {
each(arguments, function(mix) {
mix = typeof mix === T_STRING ? riot.mixin(mix) : mix
each(Object.keys(mix), function(key) {
// bind methods to self
if (key != 'init')
self[key] = isFunction(mix[key]) ? mix[key].bind(self) : mix[key]
})
// init method will be called automatically
if (mix.init) mix.init.bind(self)()
})
}
this.mount = function() {
updateOpts()
// initialiation
fn && fn.call(self, opts)
toggle(true)
// parse layout after init. fn may calculate args for nested custom tags
parseExpressions(dom, self, expressions)
if (!self.parent || hasImpl) parseExpressions(self.root, self, expressions) // top level before update, empty root
if (!self.parent || isLoop) self.update(item)
// internal use only, fixes #403
self.trigger('premount')
if (isLoop && !hasImpl) {
// update the root attribute for the looped elements
self.root = root = loopDom = dom.firstChild
} else {
while (dom.firstChild) root.appendChild(dom.firstChild)
if (root.stub) self.root = root = parent.root
}
// if it's not a child tag we can trigger its mount event
if (!self.parent || self.parent.isMounted) {
self.isMounted = true
self.trigger('mount')
}
// otherwise we need to wait that the parent event gets triggered
else self.parent.one('mount', function() {
// avoid to trigger the `mount` event for the tags
// not visible included in an if statement
if (!isInStub(self.root)) {
self.parent.isMounted = self.isMounted = true
self.trigger('mount')
}
})
}
this.unmount = function(keepRootTag) {
var el = loopDom || root,
p = el.parentNode
if (p) {
if (parent)
// remove this tag from the parent tags object
// if there are multiple nested tags with same name..
// remove this element form the array
if (isArray(parent.tags[tagName]))
each(parent.tags[tagName], function(tag, i) {
if (tag._id == self._id)
parent.tags[tagName].splice(i, 1)
})
else
// otherwise just delete the tag instance
parent.tags[tagName] = undefined
else
while (el.firstChild) el.removeChild(el.firstChild)
if (!keepRootTag)
p.removeChild(el)
}
self.trigger('unmount')
toggle()
self.off('*')
// somehow ie8 does not like `delete root._tag`
root._tag = null
}
function toggle(isMount) {
// mount/unmount children
each(childTags, function(child) { child[isMount ? 'mount' : 'unmount']() })
// listen/unlisten parent (events flow one way from parent to children)
if (parent) {
var evt = isMount ? 'on' : 'off'
// the loop tags will be always in sync with the parent automatically
if (isLoop)
parent[evt]('unmount', self.unmount)
else
parent[evt]('update', self.update)[evt]('unmount', self.unmount)
}
}
// named elements available for fn
parseNamedElements(dom, this, childTags)
}
function setEventHandler(name, handler, dom, tag) {
dom[name] = function(e) {
var item = tag._item,
ptag = tag.parent
if (!item)
while (ptag) {
item = ptag._item
ptag = item ? false : ptag.parent
}
// cross browser event fix
e = e || window.event
// ignore error on some browsers
try {
e.currentTarget = dom
if (!e.target) e.target = e.srcElement
if (!e.which) e.which = e.charCode || e.keyCode
} catch (ignored) { '' }
e.item = item
// prevent default behaviour (by default)
if (handler.call(tag, e) !== true && !/radio|check/.test(dom.type)) {
e.preventDefault && e.preventDefault()
e.returnValue = false
}
if (!e.preventUpdate) {
var el = item ? tag.parent : tag
el.update()
}
}
}
// used by if- attribute
function insertTo(root, node, before) {
if (root) {
root.insertBefore(before, node)
root.removeChild(node)
}
}
function update(expressions, tag) {
each(expressions, function(expr, i) {
var dom = expr.dom,
attrName = expr.attr,
value = tmpl(expr.expr, tag),
parent = expr.dom.parentNode
if (value == null) value = ''
// leave out riot- prefixes from strings inside textarea
if (parent && parent.tagName == 'TEXTAREA') value = value.replace(/riot-/g, '')
// no change
if (expr.value === value) return
expr.value = value
// text node
if (!attrName) return dom.nodeValue = value.toString()
// remove original attribute
remAttr(dom, attrName)
// event handler
if (isFunction(value)) {
setEventHandler(attrName, value, dom, tag)
// if- conditional
} else if (attrName == 'if') {
var stub = expr.stub
// add to DOM
if (value) {
if (stub) {
insertTo(stub.parentNode, stub, dom)
dom.inStub = false
// avoid to trigger the mount event if the tags is not visible yet
// maybe we can optimize this avoiding to mount the tag at all
if (!isInStub(dom)) {
walk(dom, function(el) {
if (el._tag && !el._tag.isMounted) el._tag.isMounted = !!el._tag.trigger('mount')
})
}
}
// remove from DOM
} else {
stub = expr.stub = stub || document.createTextNode('')
insertTo(dom.parentNode, dom, stub)
dom.inStub = true
}
// show / hide
} else if (/^(show|hide)$/.test(attrName)) {
if (attrName == 'hide') value = !value
dom.style.display = value ? '' : 'none'
// field value
} else if (attrName == 'value') {
dom.value = value
// <img src="{ expr }">
} else if (attrName.slice(0, 5) == 'riot-' && attrName != 'riot-tag') {
attrName = attrName.slice(5)
value ? dom.setAttribute(attrName, value) : remAttr(dom, attrName)
} else {
if (expr.bool) {
dom[attrName] = value
if (!value) return
value = attrName
}
if (typeof value !== T_OBJECT) dom.setAttribute(attrName, value)
}
})
}
function each(els, fn) {
for (var i = 0, len = (els || []).length, el; i < len; i++) {
el = els[i]
// return false -> remove current item during loop
if (el != null && fn(el, i) === false) i--
}
return els
}
function isFunction(v) {
return typeof v === 'function' || false // avoid IE problems
}
function remAttr(dom, name) {
dom.removeAttribute(name)
}
function fastAbs(nr) {
return (nr ^ (nr >> 31)) - (nr >> 31)
}
function getTag(dom) {
var tagName = dom.tagName.toLowerCase()
return tagImpl[dom.getAttribute(RIOT_TAG) || tagName]
}
function getTagName(dom) {
var child = getTag(dom),
namedTag = dom.getAttribute('name'),
tagName = namedTag && namedTag.indexOf(brackets(0)) < 0 ? namedTag : child ? child.name : dom.tagName.toLowerCase()
return tagName
}
function extend(src) {
var obj, args = arguments
for (var i = 1; i < args.length; ++i) {
if ((obj = args[i])) {
for (var key in obj) { // eslint-disable-line guard-for-in
src[key] = obj[key]
}
}
}
return src
}
// with this function we avoid that the current Tag methods get overridden
function cleanUpData(data) {
if (!(data instanceof Tag)) return data
var o = {},
blackList = ['update', 'root', 'mount', 'unmount', 'mixin', 'isMounted', 'isloop', 'tags', 'parent', 'opts']
for (var key in data) {
if (!~blackList.indexOf(key))
o[key] = data[key]
}
return o
}
function mkdom(template) {
var checkie = ieVersion && ieVersion < 10,
matches = /^\s*<([\w-]+)/.exec(template),
tagName = matches ? matches[1].toLowerCase() : '',
rootTag = (tagName === 'th' || tagName === 'td') ? 'tr' :
(tagName === 'tr' ? 'tbody' : 'div'),
el = mkEl(rootTag)
el.stub = true
if (checkie) {
if (tagName === 'optgroup')
optgroupInnerHTML(el, template)
else if (tagName === 'option')
optionInnerHTML(el, template)
else if (rootTag !== 'div')
tbodyInnerHTML(el, template, tagName)
else
checkie = 0
}
if (!checkie) el.innerHTML = template
return el
}
function walk(dom, fn) {
if (dom) {
if (fn(dom) === false) walk(dom.nextSibling, fn)
else {
dom = dom.firstChild
while (dom) {
walk(dom, fn)
dom = dom.nextSibling
}
}
}
}
function isInStub(dom) {
while (dom) {
if (dom.inStub) return true
dom = dom.parentNode
}
return false
}
function mkEl(name) {
return document.createElement(name)
}
function replaceYield (tmpl, innerHTML) {
return tmpl.replace(/<(yield)\/?>(<\/\1>)?/gim, innerHTML || '')
}
function $$(selector, ctx) {
return (ctx || document).querySelectorAll(selector)
}
function $(selector, ctx) {
return (ctx || document).querySelector(selector)
}
function inherit(parent) {
function Child() {}
Child.prototype = parent
return new Child()
}
function setNamed(dom, parent, keys) {
each(dom.attributes, function(attr) {
if (dom._visited) return
if (attr.name === 'id' || attr.name === 'name') {
dom._visited = true
var p, v = attr.value
if (~keys.indexOf(v)) return
p = parent[v]
if (!p)
parent[v] = dom
else
isArray(p) ? p.push(dom) : (parent[v] = [p, dom])
}
})
}
/**
*
* Hacks needed for the old internet explorer versions [lower than IE10]
*
*/
/* istanbul ignore next */
function tbodyInnerHTML(el, html, tagName) {
var div = mkEl('div'),
loops = /td|th/.test(tagName) ? 3 : 2,
child
div.innerHTML = '<table>' + html + '</table>'
child = div.firstChild
while (loops--) child = child.firstChild
el.appendChild(child)
}
/* istanbul ignore next */
function optionInnerHTML(el, html) {
var opt = mkEl('option'),
valRegx = /value=[\"'](.+?)[\"']/,
selRegx = /selected=[\"'](.+?)[\"']/,
eachRegx = /each=[\"'](.+?)[\"']/,
ifRegx = /if=[\"'](.+?)[\"']/,
innerRegx = />([^<]*)</,
valuesMatch = html.match(valRegx),
selectedMatch = html.match(selRegx),
innerValue = html.match(innerRegx),
eachMatch = html.match(eachRegx),
ifMatch = html.match(ifRegx)
if (innerValue) opt.innerHTML = innerValue[1]
else opt.innerHTML = html
if (valuesMatch) opt.value = valuesMatch[1]
if (selectedMatch) opt.setAttribute('riot-selected', selectedMatch[1])
if (eachMatch) opt.setAttribute('each', eachMatch[1])
if (ifMatch) opt.setAttribute('if', ifMatch[1])
el.appendChild(opt)
}
/* istanbul ignore next */
function optgroupInnerHTML(el, html) {
var opt = mkEl('optgroup'),
labelRegx = /label=[\"'](.+?)[\"']/,
elementRegx = /^<([^>]*)>/,
tagRegx = /^<([^ \>]*)/,
labelMatch = html.match(labelRegx),
elementMatch = html.match(elementRegx),
tagMatch = html.match(tagRegx),
innerContent = html
if (elementMatch) {
var options = html.slice(elementMatch[1].length+2, -tagMatch[1].length-3).trim()
innerContent = options
}
if (labelMatch) opt.setAttribute('riot-label', labelMatch[1])
if (innerContent) {
var innerOpt = mkEl('div')
optionInnerHTML(innerOpt, innerContent)
opt.appendChild(innerOpt.firstChild)
}
el.appendChild(opt)
}
/*
Virtual dom is an array of custom tags on the document.
Updates and unmounts propagate downwards from parent to children.
*/
var virtualDom = [],
tagImpl = {},
styleNode
var RIOT_TAG = 'riot-tag'
function injectStyle(css) {
if (riot.render) return // skip injection on the server
if (!styleNode) {
styleNode = mkEl('style')
styleNode.setAttribute('type', 'text/css')
}
var head = document.head || document.getElementsByTagName('head')[0]
if (styleNode.styleSheet)
styleNode.styleSheet.cssText += css
else
styleNode.innerHTML += css
if (!styleNode._rendered)
if (styleNode.styleSheet) {
document.body.appendChild(styleNode)
} else {
var rs = $('style[type=riot]')
if (rs) {
rs.parentNode.insertBefore(styleNode, rs)
rs.parentNode.removeChild(rs)
} else head.appendChild(styleNode)
}
styleNode._rendered = true
}
function mountTo(root, tagName, opts) {
var tag = tagImpl[tagName],
// cache the inner HTML to fix #855
innerHTML = root._innerHTML = root._innerHTML || root.innerHTML
// clear the inner html
root.innerHTML = ''
if (tag && root) tag = new Tag(tag, { root: root, opts: opts }, innerHTML)
if (tag && tag.mount) {
tag.mount()
virtualDom.push(tag)
return tag.on('unmount', function() {
virtualDom.splice(virtualDom.indexOf(tag), 1)
})
}
}
riot.tag = function(name, html, css, attrs, fn) {
if (isFunction(attrs)) {
fn = attrs
if (/^[\w\-]+\s?=/.test(css)) {
attrs = css
css = ''
} else attrs = ''
}
if (css) {
if (isFunction(css)) fn = css
else injectStyle(css)
}
tagImpl[name] = { name: name, tmpl: html, attrs: attrs, fn: fn }
return name
}
riot.mount = function(selector, tagName, opts) {
var els,
allTags,
tags = []
// helper functions
function addRiotTags(arr) {
var list = ''
each(arr, function (e) {
list += ', *[riot-tag="'+ e.trim() + '"]'
})
return list
}
function selectAllTags() {
var keys = Object.keys(tagImpl)
return keys + addRiotTags(keys)
}
function pushTags(root) {
if (root.tagName) {
if (tagName && !root.getAttribute(RIOT_TAG))
root.setAttribute(RIOT_TAG, tagName)
var tag = mountTo(root,
tagName || root.getAttribute(RIOT_TAG) || root.tagName.toLowerCase(), opts)
if (tag) tags.push(tag)
}
else if (root.length) {
each(root, pushTags) // assume nodeList
}
}
// ----- mount code -----
if (typeof tagName === T_OBJECT) {
opts = tagName
tagName = 0
}
// crawl the DOM to find the tag
if (typeof selector === T_STRING) {
if (selector === '*')
// select all the tags registered
// and also the tags found with the riot-tag attribute set
selector = allTags = selectAllTags()
else
// or just the ones named like the selector
selector += addRiotTags(selector.split(','))
els = $$(selector)
}
else
// probably you have passed already a tag or a NodeList
els = selector
// select all the registered and mount them inside their root elements
if (tagName === '*') {
// get all custom tags
tagName = allTags || selectAllTags()
// if the root els it's just a single tag
if (els.tagName)
els = $$(tagName, els)
else {
// select all the children for all the different root elements
var nodeList = []
each(els, function (_el) {
nodeList.push($$(tagName, _el))
})
els = nodeList
}
// get rid of the tagName
tagName = 0
}
if (els.tagName)
pushTags(els)
else
each(els, pushTags)
return tags
}
// update everything
riot.update = function() {
return each(virtualDom, function(tag) {
tag.update()
})
}
// @deprecated
riot.mountTo = riot.mount
var parsers = {
html: {},
css: {},
js: {
coffee: function(js) {
return CoffeeScript.compile(js, { bare: true })
},
es6: function(js) {
return babel.transform(js, { blacklist: ['useStrict'] }).code
},
none: function(js) {
return js
}
}
}
// fix 913
parsers.js.javascript = parsers.js.none
// 4 the nostalgics
parsers.js.coffeescript = parsers.js.coffee
riot.parsers = parsers
var BOOL_ATTR = ('allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,'+
'defaultchecked,defaultmuted,defaultselected,defer,disabled,draggable,enabled,formnovalidate,hidden,'+
'indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,'+
'pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,spellcheck,translate,truespeed,'+
'typemustmatch,visible').split(','),
// these cannot be auto-closed
VOID_TAGS = 'area,base,br,col,command,embed,hr,img,input,keygen,link,meta,param,source,track,wbr'.split(','),
/*
Following attributes give error when parsed on browser with { exrp_values }
'd' describes the SVG <path>, Chrome gives error if the value is not valid format
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
*/
PREFIX_ATTR = ['style', 'src', 'd'],
LINE_TAG = /^<([\w\-]+)>(.*)<\/\1>/gim,
QUOTE = /=({[^}]+})([\s\/\>]|$)/g,
SET_ATTR = /([\w\-]+)=(["'])([^\2]+?)\2/g,
EXPR = /{\s*([^}]+)\s*}/g,
// (tagname) (html) (javascript) endtag
CUSTOM_TAG = /^<([\w\-]+)\s?([^>]*)>([^\x00]*[\w\/}"']>$)?([^\x00]*?)^<\/\1>/gim,
SCRIPT = /<script(\s+type=['"]?([^>'"]+)['"]?)?>([^\x00]*?)<\/script>/gm,
STYLE = /<style(\s+type=['"]?([^>'"]+)['"]?|\s+scoped)?>([^\x00]*?)<\/style>/gm,
CSS_SELECTOR = /(^|\}|\{)\s*([^\{\}]+)\s*(?=\{)/g,
CSS_COMMENT = /\/\*[^\x00]*?\*\//gm,
HTML_COMMENT = /<!--.*?-->/g,
CLOSED_TAG = /<([\w\-]+)([^>]*)\/\s*>/g,
LINE_COMMENT = /^\s*\/\/.*$/gm,
JS_COMMENT = /\/\*[^\x00]*?\*\//gm,
INPUT_NUMBER = /(<input\s[^>]*?)type=['"]number['"]/gm
function mktag(name, html, css, attrs, js) {
return 'riot.tag(\''
+ name + '\', \''
+ html + '\''
+ (css ? ', \'' + css + '\'' : '')
+ (attrs ? ', \'' + attrs.replace(/'/g, "\\'") + '\'' : '')
+ ', function(opts) {' + js + '\n});'
}
function compileHTML(html, opts, type) {
var brackets = riot.util.brackets
// foo={ bar } --> foo="{ bar }"
html = html.replace(brackets(QUOTE), '="$1"$2')
// whitespace
html = opts.whitespace ? html.replace(/\n/g, '\\n') : html.replace(/\s+/g, ' ')
// strip comments
html = html.trim().replace(HTML_COMMENT, '')
// input type=numbr
html = html.replace(INPUT_NUMBER, '$1riot-type='+brackets(0)+'"number"'+brackets(1)) // fake expression
// alter special attribute names
html = html.replace(SET_ATTR, function(full, name, _, expr) {
if (expr.indexOf(brackets(0)) >= 0) {
name = name.toLowerCase()
if (PREFIX_ATTR.indexOf(name) >= 0) name = 'riot-' + name
// IE8 looses boolean attr values: `checked={ expr }` --> `__checked={ expr }`
else if (BOOL_ATTR.indexOf(name) >= 0) name = '__' + name
}
return name + '="' + expr + '"'
})
// run expressions trough parser
if (opts.expr) {
html = html.replace(brackets(EXPR), function(_, expr) {
var ret = compileJS(expr, opts, type).trim().replace(/\r?\n|\r/g, '').trim()
if (ret.slice(-1) == ';') ret = ret.slice(0, -1)
return brackets(0) + ret + brackets(1)
})
}
// <foo/> -> <foo></foo>
html = html.replace(CLOSED_TAG, function(_, name, attr) {
var tag = '<' + name + (attr ? ' ' + attr.trim() : '') + '>'
// Do not self-close HTML5 void tags
if (VOID_TAGS.indexOf(name.toLowerCase()) == -1) tag += '</' + name + '>'
return tag
})
// escape single quotes
html = html.replace(/'/g, "\\'")
// \{ jotain \} --> \\{ jotain \\}
html = html.replace(brackets(/\\{|\\}/g), '\\$&')
// compact: no whitespace between tags
if (opts.compact) html = html.replace(/> </g, '><')
return html
}
function riotjs(js) {
// strip comments
js = js.replace(LINE_COMMENT, '').replace(JS_COMMENT, '')
// ES6 method signatures
var lines = js.split('\n'),
es6Ident = ''
lines.forEach(function(line, i) {
var l = line.trim()
// method start
if (l[0] != '}' && l.indexOf('(') > 0 && l.indexOf('function') == -1) {
var end = /[{}]/.exec(l.slice(-1)),
m = end && /(\s+)([\w]+)\s*\(([\w,\s]*)\)\s*\{/.exec(line)
if (m && !/^(if|while|switch|for|catch)$/.test(m[2])) {
lines[i] = m[1] + 'this.' + m[2] + ' = function(' + m[3] + ') {'
// foo() { }
if (end[0] == '}') {
lines[i] += ' ' + l.slice(m[0].length - 1, -1) + '}.bind(this)'
} else {
es6Ident = m[1]
}
}
}
// method end
if (line.slice(0, es6Ident.length + 1) == es6Ident + '}') {
lines[i] = es6Ident + '}.bind(this);'
es6Ident = ''
}
})
return lines.join('\n')
}
function scopedCSS (tag, style, type) {
// 1. Remove CSS comments
// 2. Find selectors and separate them by conmma
// 3. keep special selectors as is
// 4. prepend tag and [riot-tag]
return style.replace(CSS_COMMENT, '').replace(CSS_SELECTOR, function (m, p1, p2) {
return p1 + ' ' + p2.split(/\s*,\s*/g).map(function(sel) {
var s = sel.trim()
var t = (/:scope/.test(s) ? '' : ' ') + s.replace(/:scope/, '')
return s[0] == '@' || s == 'from' || s == 'to' || /%$/.test(s) ? s :
tag + t + ', [riot-tag="' + tag + '"]' + t
}).join(',')
}).trim()
}
function compileJS(js, opts, type) {
var parser = opts.parser || (type ? riot.parsers.js[type] : riotjs)
if (!parser) throw new Error('Parser not found "' + type + '"')
return parser(js, opts)
}
function compileTemplate(lang, html) {
var parser = riot.parsers.html[lang]
if (!parser) throw new Error('Template parser not found "' + lang + '"')
return parser(html)
}
function compileCSS(style, tag, type) {
if (type == 'scoped-css') style = scopedCSS(tag, style)
else if (riot.parsers.css[type]) style = riot.parsers.css[type](tag, style)
return style.replace(/\s+/g, ' ').replace(/\\/g, '\\\\').replace(/'/g, "\\'").trim()
}
function compile(src, opts) {
opts = opts || {}
if (opts.brackets) riot.settings.brackets = opts.brackets
if (opts.template) src = compileTemplate(opts.template, src)
src = src.replace(LINE_TAG, function(_, tagName, html) {
return mktag(tagName, compileHTML(html, opts), '', '', '')
})
return src.replace(CUSTOM_TAG, function(_, tagName, attrs, html, js) {
html = html || ''
attrs = compileHTML(attrs, '', '')
// js wrapped inside <script> tag
var type = opts.type
if (!js.trim()) {
html = html.replace(SCRIPT, function(_, fullType, _type, script) {
if (_type) type = _type.replace('text/', '')
js = script
return ''
})
}
// styles in <style> tag
var styleType = 'css',
style = ''
html = html.replace(STYLE, function(_, fullType, _type, _style) {
if (fullType && fullType.trim() == 'scoped') styleType = 'scoped-css'
else if (_type) styleType = _type.replace('text/', '')
style = _style
return ''
})
return mktag(
tagName,
compileHTML(html, opts, type),
compileCSS(style, tagName, styleType),
attrs,
compileJS(js, opts, type)
)
})
}
var doc = window.document,
promise,
ready
function GET(url, fn) {
var req = new XMLHttpRequest()
req.onreadystatechange = function() {
if (req.readyState == 4 && req.status == 200) fn(req.responseText)
}
req.open('GET', url, true)
req.send('')
}
function unindent(src) {
var ident = /[ \t]+/.exec(src)
if (ident) src = src.replace(new RegExp('^' + ident[0], 'gm'), '')
return src
}
function globalEval(js) {
var node = doc.createElement('script'),
root = doc.documentElement
node.text = compile(js)
root.appendChild(node)
root.removeChild(node)
}
function compileScripts(fn) {
var scripts = doc.querySelectorAll('script[type="riot/tag"]'),
scriptsAmount = scripts.length
function done() {
promise.trigger('ready')
ready = true
fn && fn()
}
if (!scriptsAmount) {
done()
} else {
[].map.call(scripts, function(script) {
var url = script.getAttribute('src')
function compileTag(source) {
globalEval(source)
scriptsAmount--
if (!scriptsAmount) {
done()
}
}
return url ? GET(url, compileTag) : compileTag(unindent(script.innerHTML))
})
}
}
riot.compile = function(arg, fn) {
// string
if (typeof arg === T_STRING) {
// compile & return
if (arg.trim()[0] == '<') {
var js = unindent(compile(arg))
if (!fn) globalEval(js)
return js
// URL
} else {
return GET(arg, function(str) {
var js = unindent(compile(str))
globalEval(js)
fn && fn(js, str)
})
}
}
// must be a function
if (typeof arg !== 'function') arg = undefined
// all compiled
if (ready) return arg && arg()
// add to queue
if (promise) {
arg && promise.on('ready', arg)
// grab riot/tag elements + load & execute them
} else {
promise = riot.observable()
compileScripts(arg)
}
}
// reassign mount methods
var mount = riot.mount
riot.mount = function(a, b, c) {
var ret
riot.compile(function() { ret = mount(a, b, c) })
return ret
}
// @deprecated
riot.mountTo = riot.mount
// share methods for other riot parts, e.g. compiler
riot.util = { brackets: brackets, tmpl: tmpl }
// support CommonJS, AMD & browser
/* istanbul ignore next */
if (typeof exports === T_OBJECT)
module.exports = riot
else if (typeof define === 'function' && define.amd)
define(function() { return window.riot = riot })
else
window.riot = riot
})(typeof window != 'undefined' ? window : void 0);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment