afa85e87 by Michael Richards

Merge pull request #182 from mikeric/text-templates

Text content templates
2 parents dba47367 f86541f3
describe("Rivets.TextTemplateParser", function() {
var Rivets = rivets._
describe("parse()", function() {
it("tokenizes a text template", function() {
template = "Hello {{ user.name }}, you have {{ user.messages.unread | length }} unread messages."
expected = [
{type: 0, value: "Hello "},
{type: 1, value: "user.name"},
{type: 0, value: ", you have "},
{type: 1, value: "user.messages.unread | length"},
{type: 0, value: " unread messages."}
]
results = Rivets.TextTemplateParser.parse(template, ['{{', '}}'])
expect(results.length).toBe(5)
for (i = 0; i < results.length; i++) {
expect(results[i].type).toBe(expected[i].type)
expect(results[i].value).toBe(expected[i].value)
}
})
describe("with no binding fragments", function() {
it("should return a single text token", function() {
template = "Hello World!"
expected = [{type: 0, value: "Hello World!"}]
results = Rivets.TextTemplateParser.parse(template, ['{{', '}}'])
expect(results.length).toBe(1)
for (i = 0; i < results.length; i++) {
expect(results[i].type).toBe(expected[i].type)
expect(results[i].value).toBe(expected[i].value)
}
})
})
describe("with only a binding fragment", function() {
it("should return a single binding token", function() {
template = "{{ user.name }}"
expected = [{type: 1, value: "user.name"}]
results = Rivets.TextTemplateParser.parse(template, ['{{', '}}'])
expect(results.length).toBe(1)
for (i = 0; i < results.length; i++) {
expect(results[i].type).toBe(expected[i].type)
expect(results[i].value).toBe(expected[i].value)
}
})
})
})
})
......@@ -24,7 +24,7 @@ class Rivets.Binding
# containing view, the DOM node, the type of binding, the model object and the
# keypath at which to listen for changes.
constructor: (@view, @el, @type, @key, @keypath, @options = {}) ->
unless @binder = @view.binders[type]
unless @binder = Rivets.internalBinders[@type] or @view.binders[type]
for identifier, value of @view.binders
if identifier isnt '*' and identifier.indexOf('*') isnt -1
regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
......@@ -186,54 +186,73 @@ class Rivets.View
skipNodes = []
bindingRegExp = @bindingRegExp()
buildBinding = (node, type, declaration) =>
options = {}
pipes = (pipe.trim() for pipe in declaration.split '|')
context = (ctx.trim() for ctx in pipes.shift().split '<')
path = context.shift()
splitPath = path.split /\.|:/
options.formatters = pipes
options.bypass = path.indexOf(':') != -1
if splitPath[0]
key = splitPath.shift()
else
key = null
splitPath.shift()
keypath = splitPath.join '.'
if dependencies = context.shift()
options.dependencies = dependencies.split /\s+/
@bindings.push new Rivets.Binding @, node, type, key, keypath, options
parse = (node) =>
unless node in skipNodes
for attribute in node.attributes
if bindingRegExp.test attribute.name
type = attribute.name.replace bindingRegExp, ''
unless binder = @binders[type]
for identifier, value of @binders
if identifier isnt '*' and identifier.indexOf('*') isnt -1
regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
if regexp.test type
binder = value
binder or= @binders['*']
if binder.block
skipNodes.push n for n in node.getElementsByTagName '*'
attributes = [attribute]
for attribute in attributes or node.attributes
if bindingRegExp.test attribute.name
options = {}
type = attribute.name.replace bindingRegExp, ''
pipes = (pipe.trim() for pipe in attribute.value.split '|')
context = (ctx.trim() for ctx in pipes.shift().split '<')
path = context.shift()
splitPath = path.split /\.|:/
options.formatters = pipes
options.bypass = path.indexOf(':') != -1
if splitPath[0]
key = splitPath.shift()
else
key = null
splitPath.shift()
keypath = splitPath.join '.'
if not key or @models[key]?
if dependencies = context.shift()
options.dependencies = dependencies.split /\s+/
@bindings.push new Rivets.Binding @, node, type, key, keypath, options
attributes = null if attributes
return
for el in @els
parse el
parse node for node in el.getElementsByTagName '*' when node.attributes?
if node.nodeType is Node.TEXT_NODE
parser = Rivets.TextTemplateParser
if delimiters = @config.templateDelimiters
if (tokens = parser.parse(node.data, delimiters)).length
unless tokens.length is 1 and tokens[0].type is parser.types.text
[startToken, restTokens...] = tokens
node.data = startToken.value
switch startToken.type
when 0 then node.data = startToken.value
when 1 then buildBinding node, 'textNode', startToken.value
for token in restTokens
node.parentNode.appendChild (text = document.createTextNode token.value)
buildBinding text, 'textNode', token.value if token.type is 1
else if node.attributes?
for attribute in node.attributes
if bindingRegExp.test attribute.name
type = attribute.name.replace bindingRegExp, ''
unless binder = @binders[type]
for identifier, value of @binders
if identifier isnt '*' and identifier.indexOf('*') isnt -1
regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
if regexp.test type
binder = value
binder or= @binders['*']
if binder.block
skipNodes.push n for n in node.childNodes
attributes = [attribute]
for attribute in attributes or node.attributes
if bindingRegExp.test attribute.name
type = attribute.name.replace bindingRegExp, ''
buildBinding node, type, attribute.value
parse childNode for childNode in node.childNodes
parse el for el in @els
return
......@@ -262,6 +281,54 @@ class Rivets.View
@models[key] = model for key, model of models
binding.update models for binding in @bindings
# Rivets.TextTemplateParser
# -------------------------
# Rivets.js text template parser and tokenizer for mustache-style text content
# binding declarations.
class Rivets.TextTemplateParser
@types:
text: 0
binding: 1
# Parses the template and returns a set of tokens, separating static portions
# of text from binding declarations.
@parse: (template, delimiters) ->
tokens = []
length = template.length
index = 0
lastIndex = 0
while lastIndex < length
index = template.indexOf delimiters[0], lastIndex
if index < 0
tokens.push type: @types.text, value: template.slice lastIndex
break
else
if index > 0 and lastIndex < index
tokens.push type: @types.text, value: template.slice lastIndex, index
lastIndex = index + 2
index = template.indexOf delimiters[1], lastIndex
if index < 0
substring = template.slice lastIndex - 2
lastToken = tokens[tokens.length - 1]
if lastToken?.type is @types.text
lastToken.value += substring
else
tokens.push type: @types.text, value: substring
break
value = template.slice(lastIndex, index).trim()
tokens.push type: @types.binding, value: value
lastIndex = index + 2
tokens
# Rivets.Util
# -----------
......@@ -515,6 +582,15 @@ Rivets.binders =
else
el.removeAttribute @type
# Rivets.internalBinders
# ----------------------
# Contextually sensitive binders that are used outside of the standard attribute
# bindings. Put here for fast lookups and to prevent them from being overridden.
Rivets.internalBinders =
textNode: (node, value) ->
node.data = value ? ''
# Rivets.config
# -------------
......@@ -538,6 +614,9 @@ Rivets.formatters = {}
# The Rivets.js module factory.
Rivets.factory = (exports) ->
# Exposes the full Rivets namespace. This is mainly used for isolated testing.
exports._ = Rivets
# Exposes the core binding routines that can be extended or stripped down.
exports.binders = Rivets.binders
......