Coverage

8%
272
22
250

tally-express.coffee

64%
34
22
12
LineHitsSource
11jsdom = require 'jsdom'
21fs = require 'fs'
3
4# Load the Tally engine.
51tally = (fs.readFileSync __dirname + '/tally.js', 'utf8').toString()
6
7# The Express 3 export.
81exports.__express = (path, data, callback) ->
90 fs.readFile path, 'utf8', (error, template) ->
100 if error
110 return callback(error)
12
130 html = render(template, data)
14
150 callback null, html
16
171render = (template, data) ->
18 # Create the DOM.
197 document = jsdom.jsdom(template, '2')
207 window = document.createWindow()
217 window.console = console
22
23 # Create a private member in the data to communicate with the Tally engine in the DOM.
247 data.__tally = {} unless data.__tally
25
26 # Flag so Tally knows it is running on the server
27 # and will remove nodes that don’t satisfy conditionals
28 # (data-qif) instead of setting them to display: none
29 # like it does when running on the client.
307 data.__tally['server'] = yes;
31
32 # Create Tally in the DOM.
337 window.run tally
34
35 #
36 # Copy over formatters and hooks (if any) from the special __tally namespace
37 # in the data object to the Tally object in the DOM.
38 #
39
40 #
41 # Copy custom formatter(s), if any.
42 # (Use custom formatters to modify values before they are rendered.)
43 #
447 customFormatters = data.__tally['formatters']
457 if customFormatters
460 window.tally.format[customFormatter] = customFormatters[customFormatter] for customFormatter of customFormatters
47
48 #
49 # Copy the attributeWillChange and textWillChange hooks.
50 # (Use these hooks to perform actions before a node is modified—e.g., animate, run debug code.)
51 #
527 window.tally.attributeWillChange = data.__tally['attributeWillChange']
537 window.tally.textWillChange = data.__tally['textWillChange']
54
55 #
56 # Inject Data option: if set, this will result in a copy
57 # of the data being injected into the template at tally.data
58 # (Useful if you want to append to it via Ajax calls, etc.
59 # When rendering a timeline, for example.)
60 #
617 if data.__tally['injectData']
620 head = window.document.getElementsByTagName('head')[0]
630 script = window.document.createElement('script')
640 script.setAttribute('type', 'text/javascript')
650 script.textContent = 'tally.data = ' + JSON.stringify(data, null, 2) + ';'
660 head.appendChild(script)
67
68 #
69 # Static output option: if set, tally will strip the following
70 # from templates rendered on the server:
71 #
72 # 1. All Tally attributes
73 # 2. Any elements with falsy values for data-tally-if
74 #
75 # (Note: any elements marked data-tally-dummy are stripped from
76 # ===== final output regardless of this setting. Also, this setting
77 # has no effect when Tally is used on the client side.)
78 #
797 if data.__tally['renderStatic']
800 window.tally.renderStatic = yes
81
82 #
83 # Save the data on the DOM and run Tally.
84 #
857 window.data = data
86
87 # NB. window.document is tracing out as [ null ] in the function itself
88 # === although window.document.innerHTML works. window.document.documentElement
89 # also works. I’ll be darned if I know why or where the problem is.
907 window.run ('tally(window.document.documentElement, window.data);')
91
927 html = window.document.innerHTML;
93
947 return html
95
961exports.render = render
97

tally.coffee

0%
216
0
216
LineHitsSource
1###
2Copyright 2013 Aral Balkan <aral@aralbalkan.com>
3Copyright 2012 mocking@gmail.com
4
5Licensed under the Apache License, Version 2.0 (the "License");
6you may not use this file except in compliance with the License.
7You may obtain a copy of the License at
8
9http://www.apache.org/licenses/LICENSE-2.0
10
11Unless required by applicable law or agreed to in writing, software
12distributed under the License is distributed on an "AS IS" BASIS,
13WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14See the License for the specific language governing permissions and
15limitations under the License.
16
17Forked from Distal by mocking@gmail.com (https://code.google.com/p/distal/)
18###
19
200tally = (root, obj) ->
210 "use strict"
22
23 # Create a duplicate object which we can add properties to without affecting the original.
240 wrapper = ->
25
260 wrapper:: = obj
270 obj = new wrapper()
280 resolve = tally.resolve
290 node = root
300 doc = root.ownerDocument
310 querySelectorAll = !!root.querySelectorAll
32
33 # Create an empty options object if one was not passed so we don’t have to keep checking for it later.
340 obj.__tally = {} if obj.__tally is undefined
35
36 # Shortcut to flag: are we running on the server?
370 isRunningOnServer = obj.__tally.server
38
39 # Render static option.
400 shouldRenderStatic = isRunningOnServer and obj.__tally.renderStatic
41
42 # Optimize comparison check.
430 innerText = (if "innerText" of root then "innerText" else "textContent")
44
45 # Attributes that don't support setAttribute()
460 altAttr =
47 className: 1
48 class: 1
49 innerHTML: 1
50 style: 1
51 src: 1
52 href: 1
53 id: 1
54 value: 1
55 checked: 1
56 selected: 1
57 label: 1
58 htmlFor: 1
59 text: 1
60 title: 1
61 disabled: 1
62
630 formInputHasBody =
64 BUTTON: 1
65 LABEL: 1
66 LEGEND: 1
67 FIELDSET: 1
68 OPTION: 1
69
70
71 # TAL attributes for querySelectorAll call
720 qdef = tally
730 attributeWillChange = qdef.attributeWillChange
740 textWillChange = qdef.textWillChange
750 qif = qdef.qif or "data-tally-if"
760 qrepeat = qdef.qrepeat or "data-tally-repeat"
770 qattr = qdef.qattr or "data-tally-attribute"
780 qtext = qdef.qtext or "data-tally-text"
790 qdup = qdef.qdup or "data-tally-dummy"
80
81 # Output formatter.
820 format = qdef.format
830 qdef = qdef.qdef or "data-tally-alias"
840 TAL = "*[" + [qdef, qif, qrepeat, qattr, qtext].join("],*[") + "]"
850 html = undefined
860 getProp = (s) ->
870 this[s]
88
89 # There may be generated nodes that are siblings to the root node if the root node
90 # itself was a repeater. Remove them so we don't have to deal with them later.
910 tmpNode = root.parentNode
920 tmpNode.removeChild node while (node = root.nextSibling) and (node.qdup or (node.nodeType is 1 and node.getAttribute(qdup)))
93
94 # If we generate repeat nodes and are dealing with non-live NodeLists, then
95 # we add them to the listStack[] and process them first as they won't appear inline
96 # due to non-live NodeLists when we traverse our tree.
970 listStack = undefined
980 posStack = [0]
990 list = undefined
1000 pos = 0
1010 attr = undefined
1020 attr2 = undefined
1030 `var undefined = {}._`
104
105 # Get a list of concerned nodes within this root node. If querySelectorAll is
106 # supported we use that but it is treated differently because it is a non-live NodeList.
1070 if querySelectorAll
108
109 # Remove all generated nodes (repeats), so we don't have to deal with them later.
110 # Only need to do this for non-live NodeLists.
1110 list = root.querySelectorAll("*[" + qdup + "]")
1120 node.parentNode.removeChild node while (node = list[pos++])
1130 pos = 0
114
1150 listStack = [(if querySelectorAll then root.querySelectorAll(TAL) else root.getElementsByTagName("*"))]
116
1170 list = [root]
118
1190 loop
1200 node = list[pos++]
121
122 # When finished with the current list, there are generated nodes and
123 # their children that need to be processed.
1240 while not node and (list = listStack.pop())
1250 pos = posStack.pop()
1260 node = list[pos++]
1270 break unless node
128
129 # Creates an alias for an object
130 # e.g., <section data-tally-alias='feeds main.sidebar.feeds'>
1310 attr = node.getAttribute(qdef)
1320 if attr
1330 attr = attr.split(" ")
134
135 # Add it to the object as a property.
1360 html = resolve(obj, attr[1])
137
138 # The 3rd parameter, if it exists, is a numerical index into the array.
1390 if attr2 = attr[2]
1400 obj["#"] = parseInt(attr2) + 1
1410 html = html[attr2]
1420 obj[attr[0]] = html
143
144 # Shown if object is truthy.
145 # e.g., <img data-tally-if='item.unread'> <img data-tally-if='item.count isGreaterThan 1'>
1460 attr = node.getAttribute(qif)
1470 if attr
1480 attr = attr.split(" ")
1490 attr = [attr[0].substr(4), "not", 0] if attr[0].indexOf("not:") is 0
1500 obj2 = resolve(obj, attr[0])
151
152 # If obj is empty array it is still truthy, so make it the array length.
1530 obj2 = obj2.length if obj2 and obj2.join and obj2.length > -1
1540 if attr.length > 2
1550 attr[2] = attr.slice(2).join(" ") if attr[3]
1560 attr[2] *= 1 if typeof obj2 is "number"
1570 switch attr[1]
158 when "not"
1590 attr = not obj2
160 when "is" # In Distal, this is eq (equal to)
1610 attr = (obj2 is attr[2])
162 when "isNot" # In Distal, this is ne (not equal to)
1630 attr = (obj2 isnt attr[2])
164 when "isGreaterThan" # In Distal, this is gt (greater than)
1650 attr = (obj2 > attr[2])
166 when "isLessThan" # In Distal, this is lt (less than)
1670 attr = (obj2 < attr[2])
168 when "contains" # In Distal, this is cn (contains)
1690 attr = (obj2 and obj2.indexOf(attr[2]) >= 0)
170 when "doesNotContain" # In Distal this is nc (does not contain)
1710 attr = (obj2 and obj2.indexOf(attr[2]) < 0)
172 else
1730 throw node
174 else
1750 attr = obj2
1760 if attr
1770 if not shouldRenderStatic
1780 if node.style.removeProperty
1790 node.style.removeProperty 'display'
180 else
1810 node.style.removeAttribute 'display'
182 # node.style.display = "" if not shouldRenderStatic
183 else
184
185 # Handle hiding differently based on whether user has flagged that
186 # we should render static HTML from the server. (If so, remove the
187 # nodes instead of hiding them to cut down on traffic.)
188
189 # Skip over all nodes that are children of this node.
1900 if querySelectorAll
1910 pos += node.querySelectorAll(TAL).length
192 else
1930 pos += node.getElementsByTagName("*").length
194
1950 if shouldRenderStatic
1960 node.parentNode.removeChild node
197 else
1980 node.style.display = "none"
199
200 # Stop processing the rest of this node as it is invisible.
2010 continue
202
203 # Duplicate the current node x number of times where x is the length
204 # of the resolved array. Create a shortcut variable for each iteration
205 # of the loop.
206 # e.g., <div data-tally-repeat='item feeds.items'>
2070 attr = node.getAttribute(qrepeat)
208
2090 if attr
2100 attr2 = attr.split(" ")
211
212 #if live NodeList, remove adjacent repeated nodes
2130 unless querySelectorAll
2140 html = node.parentNode
2150 html.removeChild tmpNode while (tmpNode = node.nextSibling) and (tmpNode.qdup or (tmpNode.nodeType is 1 and tmpNode.getAttribute(qdup)))
216
2170 throw attr2 unless attr2[1]
2180 objList = resolve(obj, attr2[1])
219
2200 if objList and objList.length
221
222 # Don’t set the style if on the server (as we don’t on anything)
223 # node.style.display = "" if not shouldRenderStatic
2240 if not shouldRenderStatic
2250 if node.style.removeProperty
2260 node.style.removeProperty 'display'
227 else
2280 node.style.removeAttribute 'display'
229
230
231 # Allow this node to be treated as index zero in the repeat list
232 # we do this by setting the shortcut variable to array[0]
2330 obj[attr2[0]] = objList[0]
2340 obj["#"] = 1
235 else
236
2370 if shouldRenderStatic
238 # Delete the node
2390 if querySelectorAll
2400 pos += node.querySelectorAll(TAL).length
241 else
2420 pos += node.getElementsByTagName("*").length
243
244 # Will this mess up pos?
2450 node.parentNode.removeChild node
246 else
247
248 # Just hide the object and skip its children.
249
250 # We need to hide the repeat node if the object doesn't resolve.
2510 node.style.display = "none"
252
253 # Skip over all nodes that are children of this node.
2540 if querySelectorAll
2550 pos += node.querySelectorAll(TAL).length
256 else
2570 pos += node.getElementsByTagName("*").length
258
259 # Stop processing the rest of this node as it is invisible.
2600 continue
261
2620 if objList.length > 1
263
264 # We need to duplicate this node x number of times. But instead
265 # of calling cloneNode x times, we get the outerHTML and repeat
266 # that x times, then innerHTML it which is faster.
2670 html = new Array(objList.length - 1)
2680 len = html.length
2690 i = len
270
2710 while i > 0
2720 html[len - i] = i
2730 i--
2740 tmpNode = node.cloneNode(true)
2750 tmpNode.checked = false if "form" of tmpNode
2760 tmpNode.setAttribute qdef, attr
2770 tmpNode.removeAttribute qrepeat
2780 tmpNode.setAttribute qdup, "1"
2790 tmpNode = tmpNode.outerHTML or doc.createElement("div").appendChild(tmpNode).parentNode.innerHTML
280
281 # We're doing something like this:
282 # html = "<div data-tally-alias=' + [1,2,3].join('><div data-tally-alias=') + '>'
2830 prefix = tmpNode.indexOf(" " + qdef + "=\"" + attr + "\"")
2840 prefix = tmpNode.indexOf(" " + qdef + "='" + attr + "'") if prefix is -1
2850 prefix = prefix + qdef.length + 3 + attr.length
2860 html = tmpNode.substr(0, prefix) + " " + html.join(tmpNode.substr(prefix) + tmpNode.substr(0, prefix) + " ") + tmpNode.substr(prefix)
2870 tmpNode = doc.createElement("div")
288
289 # Workaround for IE which can't innerHTML tables and selects.
2900 if "cells" of node and not ("tBodies" of node) #TR
2910 tmpNode.innerHTML = "<table>" + html + "</table>"
2920 tmpNode = tmpNode.firstChild.tBodies[0].childNodes
293 else if "cellIndex" of node #TD
2940 tmpNode.innerHTML = "<table><tr>" + html + "</tr></table>"
2950 tmpNode = tmpNode.firstChild.tBodies[0].firstChild.childNodes
296 else if "selected" of node and "text" of node #OPTION, OPTGROUP
2970 tmpNode.innerHTML = "<select>" + html + "</select>"
2980 tmpNode = tmpNode.firstChild.childNodes
299 else
3000 tmpNode.innerHTML = html
3010 tmpNode = tmpNode.childNodes
3020 prefix = node.parentNode
3030 attr2 = node.nextSibling
3040 if querySelectorAll or node is root
305
306 # Push the current list and index to the stack and process the repeated
307 # nodes first. We need to do this inline because some variable may change
308 # value later, if the become redefined.
3090 listStack.push list
3100 posStack.push pos
311
312 # Add this node to the stack so that it is processed right before we pop the
313 # main list off the stack. This will be the last node to be processed and we
314 # use it to assign our repeat variable to array index 0 so that the node's
315 # children, which are also at array index 0, will be processed correctly.
3160 list = getAttribute: getProp
3170 list[qdef] = attr + " 0"
3180 listStack.push [list]
3190 posStack.push 0
320
321 # Clear the current list so that in the next round we grab another list
322 # off the stack.
3230 list = []
3240 i = tmpNode.length - 1
325
3260 while i >= 0
3270 html = tmpNode[i]
328
329 # We need to add the repeated nodes to the listStack because
330 # we are either (1) dealing with a live NodeList and we are still at
331 # the root node so the newly created nodes are adjacent to the root
332 # and so won't appear in the NodeList, or (2) we are dealing with a
333 # non-live NodeList, so we need to add them to the listStack.
3340 listStack.push (if querySelectorAll then html.querySelectorAll(TAL) else html.getElementsByTagName("*"))
3350 posStack.push 0
3360 listStack.push [html]
3370 posStack.push 0
3380 html.qdup = 1
3390 prefix.insertBefore html, attr2
3400 i--
341 else
3420 i = tmpNode.length - 1
343
3440 while i >= 0
3450 html = tmpNode[i]
3460 html.qdup = 1
3470 prefix.insertBefore html, attr2
3480 i--
3490 prefix.selectedIndex = -1
350
351 # Set multiple attributes on the node.
352 # e.g., <div data-tally-attribute='value item.text; disabled item.disabled'>
3530 attr = node.getAttribute(qattr)
3540 if attr
3550 name = undefined
3560 value = undefined
3570 html = attr.split("; ")
3580 i = html.length - 1
359
3600 while i >= 0
3610 attr = html[i].split(" ")
3620 name = attr[0]
3630 throw attr unless attr[1]
3640 value = resolve(obj, attr[1])
3650 value = "" if value is `undefined`
3660 attributeWillChange node, name, value if attributeWillChange
3670 value = attr(value) if attr = attr[2] and format[attr[2]]
3680 if altAttr[name]
3690 switch name
370 when "innerHTML" #should use "qtext"
3710 throw node
372 when "disabled", "checked", "selected"
3730 node[name] = !!value
374 when "style"
3750 node.style.cssText = value
376 when "text" #option.text unstable in IE
3770 node[(if querySelectorAll then name else innerText)] = value
378 when "class"
3790 node["className"] = value
380 else
3810 node[name] = value
382 else
3830 node.setAttribute name, value
3840 i--
385
386 # Sets the innerHTML on the node.
387 # e.g., <div data-tally-text='html item.description'>
3880 attr = node.getAttribute(qtext)
3890 if attr
3900 attr = attr.split(" ")
3910 html = (attr[0] is "html")
3920 attr2 = resolve(obj, attr[(if html then 1 else 0)])
3930 attr2 = "" if attr2 is `undefined`
3940 textWillChange node, attr2 if textWillChange
3950 attr2 = attr(attr2) if (attr = attr[(if html then 2 else 1)]) and (attr = format[attr])
3960 if html
3970 node.innerHTML = attr2
398 else
3990 node[(if "form" of node and not formInputHasBody[node.tagName] then "value" else innerText)] = attr2
400#end while
401
402# Follows the dot notation path to find an object within an object: obj["a"]["b"]["1"] = c;
4030tally.resolve = (obj, seq, x, lastObj) ->
404
405 #if fully qualified path is at top level: obj["a.b.d"] = c
4060 return (if (typeof x is "function") then x.call(obj, seq) else x) if x = obj[seq]
407
4080 seq = seq.split(".")
4090 x = 0
410
4110 while seq[x] and (lastObj = obj) and (obj = obj[seq[x++]])
412 ;
413
4140 (if (typeof obj is "function") then obj.call(lastObj, seq.join(".")) else obj)
415
416
417# Number formatters
4180tally.format = ",.": (v, i) ->
4190 i = v * 1
4200 (if isNaN(i) then v else ((if i % 1 then i.toFixed(2) else parseInt(i, 10) + "")).replace(/(^\d{1,3}|\d{3})(?=(?:\d{3})+(?:$|\.))/g, "$1,"))
421
422
423# Support RequireJS module pattern
4240if typeof define is "function" and define.amd
4250 define "tally", ->
4260 tally
427else
4280 window.tally = tally
429

timer.coffee

0%
22
0
22
LineHitsSource
1#
2# Timer (modified from http://stackoverflow.com/questions/10617070/how-to-measure-execution-time-of-javascript-code-with-callbacks)
3#
40timers = {}
50start = process.hrtime()
6
70reset = ->
80 start = process.hrtime()
9
100elapsedTime = (note) ->
110 precision = 3 # 3 decimal places
120 elapsed = process.hrtime(start)[1] / 1000000; # ms from nanoseconds
13
140 if not (timers[note] and Array.isArray timers[note])
150 timers[note] = []
16
170 times = timers[note]
180 times.push elapsed
19
200 sum = times.reduce (previousValue, currentValue) ->
210 return previousValue + currentValue;
220 sum = sum.toFixed(precision)
230 numTries = times.length
240 average = (sum / numTries).toFixed(precision)
250 min = (Math.min.apply(Math, times)).toFixed(precision)
260 max = (Math.max.apply(Math, times)).toFixed(precision)
27
280 console.log("\n#{note}:\n
29 Elapsed: #{elapsed} ms.\n
30 Average: #{average} ms over #{numTries} tries (min: #{min} ms, max: #{max} ms).")
31
320 start = process.hrtime() # reset the timer
33
340exports.reset = reset
350exports.elapsedTime = elapsedTime
36