afa85e87 by Michael Richards

Merge pull request #182 from mikeric/text-templates

Text content templates
2 parents dba47367 f86541f3
1 describe("Rivets.TextTemplateParser", function() {
2 var Rivets = rivets._
3
4 describe("parse()", function() {
5 it("tokenizes a text template", function() {
6 template = "Hello {{ user.name }}, you have {{ user.messages.unread | length }} unread messages."
7
8 expected = [
9 {type: 0, value: "Hello "},
10 {type: 1, value: "user.name"},
11 {type: 0, value: ", you have "},
12 {type: 1, value: "user.messages.unread | length"},
13 {type: 0, value: " unread messages."}
14 ]
15
16 results = Rivets.TextTemplateParser.parse(template, ['{{', '}}'])
17 expect(results.length).toBe(5)
18
19 for (i = 0; i < results.length; i++) {
20 expect(results[i].type).toBe(expected[i].type)
21 expect(results[i].value).toBe(expected[i].value)
22 }
23 })
24
25 describe("with no binding fragments", function() {
26 it("should return a single text token", function() {
27 template = "Hello World!"
28 expected = [{type: 0, value: "Hello World!"}]
29
30 results = Rivets.TextTemplateParser.parse(template, ['{{', '}}'])
31 expect(results.length).toBe(1)
32
33 for (i = 0; i < results.length; i++) {
34 expect(results[i].type).toBe(expected[i].type)
35 expect(results[i].value).toBe(expected[i].value)
36 }
37 })
38 })
39
40 describe("with only a binding fragment", function() {
41 it("should return a single binding token", function() {
42 template = "{{ user.name }}"
43 expected = [{type: 1, value: "user.name"}]
44
45 results = Rivets.TextTemplateParser.parse(template, ['{{', '}}'])
46 expect(results.length).toBe(1)
47
48 for (i = 0; i < results.length; i++) {
49 expect(results[i].type).toBe(expected[i].type)
50 expect(results[i].value).toBe(expected[i].value)
51 }
52 })
53 })
54 })
55 })
...@@ -24,7 +24,7 @@ class Rivets.Binding ...@@ -24,7 +24,7 @@ class Rivets.Binding
24 # containing view, the DOM node, the type of binding, the model object and the 24 # containing view, the DOM node, the type of binding, the model object and the
25 # keypath at which to listen for changes. 25 # keypath at which to listen for changes.
26 constructor: (@view, @el, @type, @key, @keypath, @options = {}) -> 26 constructor: (@view, @el, @type, @key, @keypath, @options = {}) ->
27 unless @binder = @view.binders[type] 27 unless @binder = Rivets.internalBinders[@type] or @view.binders[type]
28 for identifier, value of @view.binders 28 for identifier, value of @view.binders
29 if identifier isnt '*' and identifier.indexOf('*') isnt -1 29 if identifier isnt '*' and identifier.indexOf('*') isnt -1
30 regexp = new RegExp "^#{identifier.replace('*', '.+')}$" 30 regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
...@@ -186,8 +186,49 @@ class Rivets.View ...@@ -186,8 +186,49 @@ class Rivets.View
186 skipNodes = [] 186 skipNodes = []
187 bindingRegExp = @bindingRegExp() 187 bindingRegExp = @bindingRegExp()
188 188
189 buildBinding = (node, type, declaration) =>
190 options = {}
191
192 pipes = (pipe.trim() for pipe in declaration.split '|')
193 context = (ctx.trim() for ctx in pipes.shift().split '<')
194 path = context.shift()
195 splitPath = path.split /\.|:/
196 options.formatters = pipes
197 options.bypass = path.indexOf(':') != -1
198
199 if splitPath[0]
200 key = splitPath.shift()
201 else
202 key = null
203 splitPath.shift()
204
205 keypath = splitPath.join '.'
206
207 if dependencies = context.shift()
208 options.dependencies = dependencies.split /\s+/
209
210 @bindings.push new Rivets.Binding @, node, type, key, keypath, options
211
189 parse = (node) => 212 parse = (node) =>
190 unless node in skipNodes 213 unless node in skipNodes
214 if node.nodeType is Node.TEXT_NODE
215 parser = Rivets.TextTemplateParser
216
217 if delimiters = @config.templateDelimiters
218 if (tokens = parser.parse(node.data, delimiters)).length
219 unless tokens.length is 1 and tokens[0].type is parser.types.text
220 [startToken, restTokens...] = tokens
221 node.data = startToken.value
222
223 switch startToken.type
224 when 0 then node.data = startToken.value
225 when 1 then buildBinding node, 'textNode', startToken.value
226
227 for token in restTokens
228 node.parentNode.appendChild (text = document.createTextNode token.value)
229 buildBinding text, 'textNode', token.value if token.type is 1
230
231 else if node.attributes?
191 for attribute in node.attributes 232 for attribute in node.attributes
192 if bindingRegExp.test attribute.name 233 if bindingRegExp.test attribute.name
193 type = attribute.name.replace bindingRegExp, '' 234 type = attribute.name.replace bindingRegExp, ''
...@@ -201,39 +242,17 @@ class Rivets.View ...@@ -201,39 +242,17 @@ class Rivets.View
201 binder or= @binders['*'] 242 binder or= @binders['*']
202 243
203 if binder.block 244 if binder.block
204 skipNodes.push n for n in node.getElementsByTagName '*' 245 skipNodes.push n for n in node.childNodes
205 attributes = [attribute] 246 attributes = [attribute]
206 247
207 for attribute in attributes or node.attributes 248 for attribute in attributes or node.attributes
208 if bindingRegExp.test attribute.name 249 if bindingRegExp.test attribute.name
209 options = {}
210 type = attribute.name.replace bindingRegExp, '' 250 type = attribute.name.replace bindingRegExp, ''
211 pipes = (pipe.trim() for pipe in attribute.value.split '|') 251 buildBinding node, type, attribute.value
212 context = (ctx.trim() for ctx in pipes.shift().split '<')
213 path = context.shift()
214 splitPath = path.split /\.|:/
215 options.formatters = pipes
216 options.bypass = path.indexOf(':') != -1
217 if splitPath[0]
218 key = splitPath.shift()
219 else
220 key = null
221 splitPath.shift()
222 keypath = splitPath.join '.'
223
224 if not key or @models[key]?
225 if dependencies = context.shift()
226 options.dependencies = dependencies.split /\s+/
227
228 @bindings.push new Rivets.Binding @, node, type, key, keypath, options
229 252
230 attributes = null if attributes 253 parse childNode for childNode in node.childNodes
231 254
232 return 255 parse el for el in @els
233
234 for el in @els
235 parse el
236 parse node for node in el.getElementsByTagName '*' when node.attributes?
237 256
238 return 257 return
239 258
...@@ -262,6 +281,54 @@ class Rivets.View ...@@ -262,6 +281,54 @@ class Rivets.View
262 @models[key] = model for key, model of models 281 @models[key] = model for key, model of models
263 binding.update models for binding in @bindings 282 binding.update models for binding in @bindings
264 283
284 # Rivets.TextTemplateParser
285 # -------------------------
286
287 # Rivets.js text template parser and tokenizer for mustache-style text content
288 # binding declarations.
289 class Rivets.TextTemplateParser
290 @types:
291 text: 0
292 binding: 1
293
294 # Parses the template and returns a set of tokens, separating static portions
295 # of text from binding declarations.
296 @parse: (template, delimiters) ->
297 tokens = []
298 length = template.length
299 index = 0
300 lastIndex = 0
301
302 while lastIndex < length
303 index = template.indexOf delimiters[0], lastIndex
304
305 if index < 0
306 tokens.push type: @types.text, value: template.slice lastIndex
307 break
308 else
309 if index > 0 and lastIndex < index
310 tokens.push type: @types.text, value: template.slice lastIndex, index
311
312 lastIndex = index + 2
313 index = template.indexOf delimiters[1], lastIndex
314
315 if index < 0
316 substring = template.slice lastIndex - 2
317 lastToken = tokens[tokens.length - 1]
318
319 if lastToken?.type is @types.text
320 lastToken.value += substring
321 else
322 tokens.push type: @types.text, value: substring
323
324 break
325
326 value = template.slice(lastIndex, index).trim()
327 tokens.push type: @types.binding, value: value
328 lastIndex = index + 2
329
330 tokens
331
265 # Rivets.Util 332 # Rivets.Util
266 # ----------- 333 # -----------
267 334
...@@ -515,6 +582,15 @@ Rivets.binders = ...@@ -515,6 +582,15 @@ Rivets.binders =
515 else 582 else
516 el.removeAttribute @type 583 el.removeAttribute @type
517 584
585 # Rivets.internalBinders
586 # ----------------------
587
588 # Contextually sensitive binders that are used outside of the standard attribute
589 # bindings. Put here for fast lookups and to prevent them from being overridden.
590 Rivets.internalBinders =
591 textNode: (node, value) ->
592 node.data = value ? ''
593
518 # Rivets.config 594 # Rivets.config
519 # ------------- 595 # -------------
520 596
...@@ -538,6 +614,9 @@ Rivets.formatters = {} ...@@ -538,6 +614,9 @@ Rivets.formatters = {}
538 614
539 # The Rivets.js module factory. 615 # The Rivets.js module factory.
540 Rivets.factory = (exports) -> 616 Rivets.factory = (exports) ->
617 # Exposes the full Rivets namespace. This is mainly used for isolated testing.
618 exports._ = Rivets
619
541 # Exposes the core binding routines that can be extended or stripped down. 620 # Exposes the core binding routines that can be extended or stripped down.
542 exports.binders = Rivets.binders 621 exports.binders = Rivets.binders
543 622
......