Merge pull request #182 from mikeric/text-templates
Text content templates
Showing
2 changed files
with
181 additions
and
47 deletions
spec/rivets/text_template_parser.js
0 → 100644
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,54 +186,73 @@ class Rivets.View | ... | @@ -186,54 +186,73 @@ 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 |
191 | for attribute in node.attributes | 214 | if node.nodeType is Node.TEXT_NODE |
192 | if bindingRegExp.test attribute.name | 215 | parser = Rivets.TextTemplateParser |
193 | type = attribute.name.replace bindingRegExp, '' | 216 | |
194 | unless binder = @binders[type] | 217 | if delimiters = @config.templateDelimiters |
195 | for identifier, value of @binders | 218 | if (tokens = parser.parse(node.data, delimiters)).length |
196 | if identifier isnt '*' and identifier.indexOf('*') isnt -1 | 219 | unless tokens.length is 1 and tokens[0].type is parser.types.text |
197 | regexp = new RegExp "^#{identifier.replace('*', '.+')}$" | 220 | [startToken, restTokens...] = tokens |
198 | if regexp.test type | 221 | node.data = startToken.value |
199 | binder = value | 222 | |
200 | 223 | switch startToken.type | |
201 | binder or= @binders['*'] | 224 | when 0 then node.data = startToken.value |
202 | 225 | when 1 then buildBinding node, 'textNode', startToken.value | |
203 | if binder.block | 226 | |
204 | skipNodes.push n for n in node.getElementsByTagName '*' | 227 | for token in restTokens |
205 | attributes = [attribute] | 228 | node.parentNode.appendChild (text = document.createTextNode token.value) |
206 | 229 | buildBinding text, 'textNode', token.value if token.type is 1 | |
207 | for attribute in attributes or node.attributes | 230 | |
208 | if bindingRegExp.test attribute.name | 231 | else if node.attributes? |
209 | options = {} | 232 | for attribute in node.attributes |
210 | type = attribute.name.replace bindingRegExp, '' | 233 | if bindingRegExp.test attribute.name |
211 | pipes = (pipe.trim() for pipe in attribute.value.split '|') | 234 | type = attribute.name.replace bindingRegExp, '' |
212 | context = (ctx.trim() for ctx in pipes.shift().split '<') | 235 | unless binder = @binders[type] |
213 | path = context.shift() | 236 | for identifier, value of @binders |
214 | splitPath = path.split /\.|:/ | 237 | if identifier isnt '*' and identifier.indexOf('*') isnt -1 |
215 | options.formatters = pipes | 238 | regexp = new RegExp "^#{identifier.replace('*', '.+')}$" |
216 | options.bypass = path.indexOf(':') != -1 | 239 | if regexp.test type |
217 | if splitPath[0] | 240 | binder = value |
218 | key = splitPath.shift() | 241 | |
219 | else | 242 | binder or= @binders['*'] |
220 | key = null | 243 | |
221 | splitPath.shift() | 244 | if binder.block |
222 | keypath = splitPath.join '.' | 245 | skipNodes.push n for n in node.childNodes |
223 | 246 | attributes = [attribute] | |
224 | if not key or @models[key]? | 247 | |
225 | if dependencies = context.shift() | 248 | for attribute in attributes or node.attributes |
226 | options.dependencies = dependencies.split /\s+/ | 249 | if bindingRegExp.test attribute.name |
227 | 250 | type = attribute.name.replace bindingRegExp, '' | |
228 | @bindings.push new Rivets.Binding @, node, type, key, keypath, options | 251 | buildBinding node, type, attribute.value |
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 | ... | ... |
-
Please register or sign in to post a comment