├── .gitignore ├── Makefile ├── README.md ├── TODO.md ├── bin └── domthing ├── demo ├── demo.js ├── templates.js └── templates │ ├── person.dom │ └── test.dom ├── domthing.js ├── lib ├── AST.js ├── compiler.js ├── evented-property.js ├── eventify-fn.js ├── file-writer.js ├── is-boolean-attribute.js ├── parser.js ├── reduce-keypath.js ├── relative-keypath.js ├── runtime │ ├── expressions.js │ ├── helpers.js │ ├── hooks.js │ ├── safe-string.js │ └── template.js ├── sexp-parser.js ├── sexp-parser.pegjs └── split-and-keep-splitter.js ├── package.json ├── runtime.js ├── test ├── client │ ├── compiler-test.js │ ├── dom-bindings-parity.js │ └── security-test.js ├── file-writer-test.js ├── helpers │ └── parsePrecompileAndAppend.js ├── is-boolean-attribute-test.js ├── parser-test.js ├── sexp-parser-test.js └── split-and-keep-splitter-test.js └── testem.json /.gitignore: -------------------------------------------------------------------------------- 1 | runtime.bundle.js 2 | bundle.js 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PATH := node_modules/.bin:$(PATH) 2 | 3 | .PHONY: test 4 | .PHONY: force 5 | 6 | all: lib/sexp-parser.js runtime.bundle.js demo/templates.js 7 | 8 | runtime.bundle.js: force 9 | browserify runtime.js -s RUNTIME > runtime.bundle.js 10 | 11 | demo/templates.js: force 12 | ./bin/domthing --no-runtime demo/templates > demo/templates.js 13 | 14 | serve-demo: force demo/templates.js 15 | beefy demo/demo.js --open 16 | 17 | test: runtime.bundle.js 18 | faucet 19 | 20 | lib/sexp-parser.js: lib/sexp-parser.pegjs 21 | ./node_modules/.bin/pegjs lib/sexp-parser.pegjs 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Deprecation notice 2 | 3 | Due to the growth in popularity of react, and my own enjoyment using it, domthing is effectively being unused and unmaintained by me at this point. 4 | 5 | If you are interested in picking it up and developing it let me know, otherwise, sorry. 6 | 7 | --- 8 | 9 | # domthing 10 | 11 | ## What is this? 12 | 13 | A DOM-aware, mustache/handlebars-like, templating engine. Heavily inspired by HTMLBars. 14 | 15 | ## How do i I use it? 16 | 17 | Check out the demo repo at http://github.com/latentflip/domthing-demo 18 | 19 | ```bash 20 | npm install -g domthing 21 | ``` 22 | 23 | Now create a directory of templates, ending in `.dom`, like 'foo.dom': 24 | 25 | ```html 26 | 27 |

{{ greeting }}

28 | ``` 29 | 30 | ```javascript 31 | domthing path/to/templates > path/to/templates.js 32 | ``` 33 | 34 | now in your app you can do: 35 | ```javascript 36 | var templates = require('./path/to/templates.js'); 37 | 38 | document.addEventListener('DOMContentLoaded', function () { 39 | 40 | //render the template (returns a dom node); 41 | var rendered = templates.foo({ greeting: 'Hello!' }); 42 | 43 | //append the node 44 | document.body.appendChild(rendered); 45 | 46 | //trigger updates to context options: 47 | setInterval(function () { 48 | //or whatever 49 | rendered.update('greeting', 'Goodbye!'); 50 | }, 5000); 51 | }); 52 | ``` 53 | 54 | # Why? 55 | 56 | Most templating engines are just string interpolation, precompiling this: 57 | 58 | ```html 59 | My profile 60 | ``` 61 | 62 | generates a function like this: 63 | 64 | ```js 65 | function template(context) { 66 | return 'My profile'; 67 | } 68 | ``` 69 | 70 | which you call like this: 71 | 72 | ```js 73 | someElement.innerHTML = template({ me: { url: 'twitter.com/philip_roberts' } }); 74 | ``` 75 | 76 | This works, but it's not very smart. If you want to update your page if the context data changes you have to rerender the template (slow), or you have to insert css selector hooks everywhere so that you can update specific elements, a la: `My Profile` and then `$('[role=profile-link]').text(me.url)`. 77 | 78 | You've also now split the knowledge of where data goes into the dom in the template into two places, once in the template, and once somewhere in JavaScript land. Or you just do it in JavaScript land and your templates look a little empty. You also better hope nobody changes your html in a way that breaks your css selector code, or you'll be sad :(. _Also_ you've now made it harder for frontend devs who might be comfortable editing templates & styling, but less so tweaking css selector bindings, to actually edit where data goes in your templates. 79 | 80 | So, what if your template engine actually understood how the dom worked, and actually returned DOM elements: 81 | 82 | ```js 83 | //warning, code for illustrative purposes only: 84 | function template(context) { 85 | var element = document.createElement('a'); 86 | element.setAttribute('href', context.me.url); 87 | element.appendChild(document.createTextNode('My Profile')); 88 | return element; 89 | } 90 | ``` 91 | 92 | And now that you had actual references to real elements, you could just bind them to data changes directly, no css selectors required: 93 | 94 | ```js 95 | //warning, code for illustrative purposes only: 96 | function template(context) { 97 | var element = document.createElement('a'); 98 | bindAttribute(element, 'href', 'context.me.url'); //updates href when context changes 99 | element.appendChild(document.createTextNode('My Profile')); 100 | return element; 101 | } 102 | ``` 103 | 104 | If you had that you could trivially write templates that did this: 105 | 106 | ```html 107 | Buy Stuff! 108 | ``` 109 | 110 | and the classes would all just work, and update with the data, or this: 111 | 112 | ```html 113 | 114 | {{#if user.hasBought }} 115 | BUY MORE! 116 | {{#else }} 117 | BUY SOMETHING! 118 | {{/if }} 119 | 120 | ``` 121 | 122 | and the output would update as soon as user.hasBought changed. 123 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [x] Basic compilation 2 | - [x] Basic bindings 3 | - [x] Ampersand Integration 4 | - [x] Handling {{{ html }}} 5 | - [x] Handling weird things like 6 | - [x] Securing attributes etc 7 | - [ ] Documenting {{ (expressions foo "bar") }} 8 | - [ ] ~~`each`~~ 9 | - [ ] ~~Subviews?~~ 10 | - [ ] Improve parser error messages for broken expressions 11 | - [x] Can we handle both ' and " inside expressions? 12 | - [x] Can we handle unquoted bindings? 13 | -------------------------------------------------------------------------------- /bin/domthing: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | //Config CLI 4 | var path = require('path'); 5 | var program = require('commander'); 6 | 7 | program 8 | .version(require(path.join(__dirname, '..', 'package.json')).version) 9 | .option('--no-runtime', 'Omit Runtime') 10 | .parse(process.argv); 11 | 12 | var fs = require('fs'); 13 | var glob = require('glob'); 14 | var async = require('async'); 15 | var domthing = require('../domthing'); 16 | 17 | var root = path.join(process.cwd(), program.args[0]); 18 | var match = path.join(root, '**', '*.dom'); 19 | 20 | glob(match, function (err, paths) { 21 | if (err) throw err; 22 | 23 | async.map( 24 | paths, 25 | compileTemplate, 26 | function (err, outputs) { 27 | if (err) throw err; 28 | 29 | var file = [ 30 | "var templates = {};", 31 | (program.runtime ? "templates._runtime = require('domthing/runtime');" : "") 32 | ].concat(outputs).concat([ 33 | "module.exports = templates;" 34 | ]); 35 | 36 | process.stdout.write(file.join('\n')); 37 | } 38 | ); 39 | }); 40 | 41 | function compileTemplate(p, next) { 42 | var tmpl = fs.readFileSync(p).toString(); 43 | domthing.parser(tmpl, function (err, ast) { 44 | if (err) return next(err); 45 | var compiled = domthing.compiler.compile(ast); 46 | 47 | var parts = path.relative(root, p).split(path.sep); 48 | var name = parts[parts.length - 1].split('.')[0]; 49 | 50 | next(null, "templates['" + name + "'] = " + compiled + '.bind(templates);'); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | var runtime = require('../runtime'); 2 | var templates = require('./templates'); 3 | templates._runtime = runtime; 4 | 5 | var data = { 6 | foo: true, 7 | bar: true, 8 | aString: "hello", 9 | aModel: { 10 | foo: 'foo' 11 | }, 12 | active: true, 13 | things: [1,2,3], 14 | joinArgs: [" | "] 15 | }; 16 | 17 | document.addEventListener('DOMContentLoaded', function () { 18 | var template = templates.test(data, runtime); 19 | document.body.appendChild(template); 20 | setInterval(function () { 21 | template.update('aString', "hello" + Date.now()); 22 | }, 500); 23 | 24 | setInterval(function () { 25 | data.bar = !data.bar; 26 | template.update('bar', data.bar); 27 | }, 100); 28 | 29 | setInterval(function () { 30 | data.foo = !data.foo; 31 | template.update('foo', data.foo); 32 | }, 750); 33 | 34 | setInterval(function () { 35 | template.update('aModel.foo', "a string: " + Date.now()); 36 | }, 250); 37 | 38 | var i = 0; 39 | setInterval(function () { 40 | i++; 41 | template.update('joinArgs', i % 2 === 0 ? [" / "] : [" | "]); 42 | }, 600); 43 | }); 44 | -------------------------------------------------------------------------------- /demo/templates.js: -------------------------------------------------------------------------------- 1 | var templates = {}; 2 | 3 | templates['person'] = function (context, runtime) { 4 | runtime = runtime || this._runtime; 5 | var template = new runtime.Template(); 6 | 7 | (function (parent) { 8 | (function (parent) { 9 | var element = document.createElement('h1'); 10 | var expr; 11 | element.setAttribute('class', 'foo'); 12 | expr = ( 13 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'me.leftStyle') 14 | ); 15 | element.setAttribute('style', expr.value ? runtime.hooks.ESCAPE_FOR_ATTRIBUTE('style', expr.value) : ''); 16 | expr.on('change', function (v) { 17 | element.setAttribute('style', v ? runtime.hooks.ESCAPE_FOR_ATTRIBUTE('style', v) : ''); 18 | }); 19 | (function (parent) { 20 | (function (parent) { 21 | var expr = ( 22 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'me.name') 23 | ); 24 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 25 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 26 | parent.appendChild(node); 27 | })(parent); 28 | })(element); 29 | parent.appendChild(element); 30 | })(parent); 31 | (function (parent) { 32 | var expr = ( 33 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 34 | ); 35 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 36 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 37 | parent.appendChild(node); 38 | })(parent); 39 | runtime.hooks.HELPER('if', [ 40 | parent, 41 | context, 42 | ( 43 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'me.profile') 44 | ), 45 | function (parent) { 46 | (function (parent) { 47 | var expr = ( 48 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 49 | ); 50 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 51 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 52 | parent.appendChild(node); 53 | })(parent); 54 | (function (parent) { 55 | var element = document.createElement('h2'); 56 | var expr; 57 | (function (parent) { 58 | (function (parent) { 59 | var expr = ( 60 | runtime.hooks.EVENTIFY_LITERAL.call(template, "Profile") 61 | ); 62 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 63 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 64 | parent.appendChild(node); 65 | })(parent); 66 | })(element); 67 | parent.appendChild(element); 68 | })(parent); 69 | (function (parent) { 70 | var expr = ( 71 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 72 | ); 73 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 74 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 75 | parent.appendChild(node); 76 | })(parent); 77 | (function (parent) { 78 | var element = document.createElement('ul'); 79 | var expr; 80 | expr = ( 81 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'me.profile.style') 82 | ); 83 | element.setAttribute('style', expr.value ? runtime.hooks.ESCAPE_FOR_ATTRIBUTE('style', expr.value) : ''); 84 | expr.on('change', function (v) { 85 | element.setAttribute('style', v ? runtime.hooks.ESCAPE_FOR_ATTRIBUTE('style', v) : ''); 86 | }); 87 | (function (parent) { 88 | (function (parent) { 89 | var expr = ( 90 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 91 | ); 92 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 93 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 94 | parent.appendChild(node); 95 | })(parent); 96 | (function (parent) { 97 | var element = document.createElement('li'); 98 | var expr; 99 | (function (parent) { 100 | (function (parent) { 101 | var expr = ( 102 | runtime.hooks.EVENTIFY_LITERAL.call(template, "age: ") 103 | ); 104 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 105 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 106 | parent.appendChild(node); 107 | })(parent); 108 | (function (parent) { 109 | var expr = ( 110 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'me.profile.age') 111 | ); 112 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 113 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 114 | parent.appendChild(node); 115 | })(parent); 116 | (function (parent) { 117 | var expr = ( 118 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 119 | ); 120 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 121 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 122 | parent.appendChild(node); 123 | })(parent); 124 | })(element); 125 | parent.appendChild(element); 126 | })(parent); 127 | (function (parent) { 128 | var element = document.createElement('li'); 129 | var expr; 130 | (function (parent) { 131 | (function (parent) { 132 | var expr = ( 133 | runtime.hooks.EVENTIFY_LITERAL.call(template, "height: ") 134 | ); 135 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 136 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 137 | parent.appendChild(node); 138 | })(parent); 139 | (function (parent) { 140 | var expr = ( 141 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'me.profile.height') 142 | ); 143 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 144 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 145 | parent.appendChild(node); 146 | })(parent); 147 | (function (parent) { 148 | var expr = ( 149 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 150 | ); 151 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 152 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 153 | parent.appendChild(node); 154 | })(parent); 155 | })(element); 156 | parent.appendChild(element); 157 | })(parent); 158 | })(element); 159 | parent.appendChild(element); 160 | })(parent); 161 | (function (parent) { 162 | var expr = ( 163 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 164 | ); 165 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 166 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 167 | parent.appendChild(node); 168 | })(parent); 169 | }, 170 | function (parent) { 171 | }]); 172 | (function (parent) { 173 | var expr = ( 174 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 175 | ); 176 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 177 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 178 | parent.appendChild(node); 179 | })(parent); 180 | (function (parent) { 181 | var element = document.createElement('div'); 182 | var expr; 183 | element.setAttribute('role', 'collection'); 184 | parent.appendChild(element); 185 | })(parent); 186 | })(template.html); 187 | var firstChild = template.html.firstChild; 188 | firstChild.update = template.update.bind(template); 189 | return firstChild; 190 | }.bind(templates); 191 | templates['test'] = function (context, runtime) { 192 | runtime = runtime || this._runtime; 193 | var template = new runtime.Template(); 194 | 195 | (function (parent) { 196 | (function (parent) { 197 | var element = document.createElement('div'); 198 | var expr; 199 | (function (parent) { 200 | (function (parent) { 201 | var expr = ( 202 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 203 | ); 204 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 205 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 206 | parent.appendChild(node); 207 | })(parent); 208 | (function (parent) { 209 | var element = document.createElement('input'); 210 | var expr; 211 | element.setAttribute('type', 'checkbox'); 212 | expr = ( 213 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'foo') 214 | ); 215 | element[ expr.value ? 'setAttribute' : 'removeAttribute']('checked', ''); 216 | expr.on('change', function (v) { 217 | element[ v ? 'setAttribute' : 'removeAttribute']('checked', ''); 218 | }); 219 | parent.appendChild(element); 220 | })(parent); 221 | (function (parent) { 222 | var expr = ( 223 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 224 | ); 225 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 226 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 227 | parent.appendChild(node); 228 | })(parent); 229 | (function (parent) { 230 | var element = document.createElement('div'); 231 | var expr; 232 | expr = ( 233 | runtime.hooks.EXPRESSION('concat', [ 234 | runtime.hooks.EVENTIFY_LITERAL.call(template, "foo"), 235 | runtime.hooks.EVENTIFY_LITERAL.call(template, " "), 236 | runtime.hooks.EXPRESSION('if', [ 237 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'foo'), 238 | runtime.hooks.EVENTIFY_LITERAL.call(template, "a"), 239 | runtime.hooks.EVENTIFY_LITERAL.call(template, "b"), 240 | ]), 241 | ]) 242 | ); 243 | element.setAttribute('class', expr.value ? runtime.hooks.ESCAPE_FOR_ATTRIBUTE('class', expr.value) : ''); 244 | expr.on('change', function (v) { 245 | element.setAttribute('class', v ? runtime.hooks.ESCAPE_FOR_ATTRIBUTE('class', v) : ''); 246 | }); 247 | (function (parent) { 248 | (function (parent) { 249 | var expr = ( 250 | runtime.hooks.EVENTIFY_LITERAL.call(template, "A") 251 | ); 252 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 253 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 254 | parent.appendChild(node); 255 | })(parent); 256 | })(element); 257 | parent.appendChild(element); 258 | })(parent); 259 | (function (parent) { 260 | var expr = ( 261 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 262 | ); 263 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 264 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 265 | parent.appendChild(node); 266 | })(parent); 267 | (function (parent) { 268 | var element = document.createElement('div'); 269 | var expr; 270 | expr = ( 271 | runtime.hooks.EXPRESSION('concat', [ 272 | runtime.hooks.EVENTIFY_LITERAL.call(template, "foo"), 273 | runtime.hooks.EVENTIFY_LITERAL.call(template, " "), 274 | runtime.hooks.EXPRESSION('if', [ 275 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'foo'), 276 | runtime.hooks.EVENTIFY_LITERAL.call(template, "a"), 277 | runtime.hooks.EVENTIFY_LITERAL.call(template, "b"), 278 | ]), 279 | ]) 280 | ); 281 | element.setAttribute('class', expr.value ? runtime.hooks.ESCAPE_FOR_ATTRIBUTE('class', expr.value) : ''); 282 | expr.on('change', function (v) { 283 | element.setAttribute('class', v ? runtime.hooks.ESCAPE_FOR_ATTRIBUTE('class', v) : ''); 284 | }); 285 | (function (parent) { 286 | (function (parent) { 287 | var expr = ( 288 | runtime.hooks.EVENTIFY_LITERAL.call(template, "A") 289 | ); 290 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 291 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 292 | parent.appendChild(node); 293 | })(parent); 294 | })(element); 295 | parent.appendChild(element); 296 | })(parent); 297 | (function (parent) { 298 | var expr = ( 299 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 300 | ); 301 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 302 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 303 | parent.appendChild(node); 304 | })(parent); 305 | (function (parent) { 306 | var expr = ( 307 | runtime.hooks.EXPRESSION('call', [ 308 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'aModel.foo'), 309 | runtime.hooks.EVENTIFY_LITERAL.call(template, "toUpperCase"), 310 | ]) 311 | ); 312 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 313 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 314 | parent.appendChild(node); 315 | })(parent); 316 | (function (parent) { 317 | var expr = ( 318 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 319 | ); 320 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 321 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 322 | parent.appendChild(node); 323 | })(parent); 324 | (function (parent) { 325 | var expr = ( 326 | runtime.hooks.EXPRESSION('call', [ 327 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'things'), 328 | runtime.hooks.EVENTIFY_LITERAL.call(template, "join"), 329 | ]) 330 | ); 331 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 332 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 333 | parent.appendChild(node); 334 | })(parent); 335 | (function (parent) { 336 | var expr = ( 337 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 338 | ); 339 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 340 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 341 | parent.appendChild(node); 342 | })(parent); 343 | (function (parent) { 344 | var expr = ( 345 | runtime.hooks.EXPRESSION('apply', [ 346 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'things'), 347 | runtime.hooks.EVENTIFY_LITERAL.call(template, "join"), 348 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'joinArgs'), 349 | ]) 350 | ); 351 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 352 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 353 | parent.appendChild(node); 354 | })(parent); 355 | (function (parent) { 356 | var expr = ( 357 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 358 | ); 359 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 360 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 361 | parent.appendChild(node); 362 | })(parent); 363 | runtime.hooks.HELPER('if', [ 364 | parent, 365 | context, 366 | ( 367 | runtime.hooks.EXPRESSION('not', [ 368 | runtime.hooks.EXPRESSION('not', [ 369 | runtime.hooks.EVENTIFY_BINDING.call(template, context, 'foo'), 370 | ]), 371 | ]) 372 | ), 373 | function (parent) { 374 | (function (parent) { 375 | var expr = ( 376 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 377 | ); 378 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 379 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 380 | parent.appendChild(node); 381 | })(parent); 382 | (function (parent) { 383 | var element = document.createElement('a'); 384 | var expr; 385 | (function (parent) { 386 | (function (parent) { 387 | var expr = ( 388 | runtime.hooks.EVENTIFY_LITERAL.call(template, "hi") 389 | ); 390 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 391 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 392 | parent.appendChild(node); 393 | })(parent); 394 | })(element); 395 | parent.appendChild(element); 396 | })(parent); 397 | (function (parent) { 398 | var expr = ( 399 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 400 | ); 401 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 402 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 403 | parent.appendChild(node); 404 | })(parent); 405 | }, 406 | function (parent) { 407 | (function (parent) { 408 | var expr = ( 409 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 410 | ); 411 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 412 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 413 | parent.appendChild(node); 414 | })(parent); 415 | (function (parent) { 416 | var element = document.createElement('b'); 417 | var expr; 418 | (function (parent) { 419 | (function (parent) { 420 | var expr = ( 421 | runtime.hooks.EVENTIFY_LITERAL.call(template, "there") 422 | ); 423 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 424 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 425 | parent.appendChild(node); 426 | })(parent); 427 | })(element); 428 | parent.appendChild(element); 429 | })(parent); 430 | (function (parent) { 431 | var expr = ( 432 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 433 | ); 434 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 435 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 436 | parent.appendChild(node); 437 | })(parent); 438 | }]); 439 | (function (parent) { 440 | var expr = ( 441 | runtime.hooks.EVENTIFY_LITERAL.call(template, " ") 442 | ); 443 | var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : ''); 444 | expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; }); 445 | parent.appendChild(node); 446 | })(parent); 447 | })(element); 448 | parent.appendChild(element); 449 | })(parent); 450 | })(template.html); 451 | var firstChild = template.html.firstChild; 452 | firstChild.update = template.update.bind(template); 453 | return firstChild; 454 | }.bind(templates); 455 | module.exports = templates; -------------------------------------------------------------------------------- /demo/templates/person.dom: -------------------------------------------------------------------------------- 1 |

{{ me.name }}

2 | 3 | {{#if me.profile }} 4 |

Profile

5 | 9 | {{/if}} 10 | 11 |
12 | -------------------------------------------------------------------------------- /demo/templates/test.dom: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
A
5 |
A
6 | 7 | {{ (call aModel.foo 'toUpperCase') }} 8 | {{ (call things 'join') }} 9 | {{ (apply things 'join' joinArgs) }} 10 | 11 | {{#if (not (not foo))}} 12 | hi 13 | {{#else}} 14 | there 15 | {{/if}} 16 |
17 | -------------------------------------------------------------------------------- /domthing.js: -------------------------------------------------------------------------------- 1 | module.exports.parser = require('./lib/parser'); 2 | module.exports.compiler = require('./lib/compiler'); 3 | -------------------------------------------------------------------------------- /lib/AST.js: -------------------------------------------------------------------------------- 1 | var AST = module.exports; 2 | 3 | module.exports.Template = function (children) { 4 | return { 5 | type: 'Template', 6 | children: children || [] 7 | }; 8 | }; 9 | 10 | //class='42' 11 | module.exports.Literal = function (literal) { 12 | return { 13 | type: 'Literal', 14 | value: literal 15 | }; 16 | }; 17 | 18 | //class='{{foo}}' 19 | module.exports.Binding = function (keypath) { 20 | return { 21 | type: 'Binding', 22 | keypath: keypath 23 | }; 24 | }; 25 | 26 | module.exports.SEXP = function (body) { 27 | return { 28 | type: "SEXP", 29 | body: body || [] 30 | }; 31 | }; 32 | 33 | //class='{{ (toggle "is-active" (invert active)) }}' 34 | //class='foo {{bar}} baz' => {{ (concat 'foo' bar 'baz') }} 35 | module.exports.Expression = function (name, args) { 36 | return { 37 | type: 'Expression', 38 | name: name, 39 | arguments: args 40 | }; 41 | }; 42 | 43 | module.exports.Element = function (tagName, attributes, children) { 44 | if (!children && Array.isArray(attributes)) { 45 | children = attributes; 46 | attributes = undefined; 47 | } 48 | attributes = attributes || {}; 49 | children = children || []; 50 | 51 | return { 52 | type: 'Element', 53 | tagName: tagName, 54 | attributes: attributes, 55 | children: children 56 | }; 57 | }; 58 | 59 | module.exports.TextNode = function (contents) { 60 | contents = contents || AST.Literal(''); 61 | return { 62 | type: 'TextNode', 63 | content: contents 64 | }; 65 | }; 66 | 67 | module.exports.DocumentFragment = function (contents) { 68 | contents = contents || AST.Literal(''); 69 | return { 70 | type: 'DocumentFragment', 71 | content: contents 72 | }; 73 | }; 74 | 75 | module.exports.BlockStatement = function (type, expression, body, alternate) { 76 | return { 77 | type: 'BlockStatement', 78 | blockType: type, 79 | blockExpression: expression, 80 | body: body || [], 81 | alternate: alternate || [] 82 | }; 83 | }; 84 | module.exports.BlockStart = function (blockType, blockExpression) { 85 | return { 86 | type: 'BlockStart', 87 | blockType: blockType, 88 | blockExpression: blockExpression 89 | }; 90 | }; 91 | module.exports.BlockElse = function () { 92 | return { 93 | type: 'BlockElse', 94 | }; 95 | }; 96 | module.exports.BlockEnd = function (blockType) { 97 | return { 98 | type: 'BlockEnd', 99 | blockType: blockType, 100 | }; 101 | }; 102 | -------------------------------------------------------------------------------- /lib/compiler.js: -------------------------------------------------------------------------------- 1 | var isBooleanAttribute = require('./is-boolean-attribute'); 2 | var FileWriter = require('./file-writer'); 3 | 4 | function compile(ast) { 5 | var compiler = new Compiler(); 6 | return compiler.compile(ast); 7 | } 8 | 9 | function Compiler () { 10 | this.writer = new FileWriter(); 11 | } 12 | 13 | Compiler.prototype.write = function () { 14 | this.writer.write.apply(this.writer, arguments); 15 | }; 16 | Compiler.prototype.indent = function () { 17 | this.writer.indent.apply(this.writer, arguments); 18 | }; 19 | Compiler.prototype.outdent = function () { 20 | this.writer.outdent.apply(this.writer, arguments); 21 | }; 22 | 23 | Compiler.prototype.compile = function (ast) { 24 | this.write( 25 | 'function (context, runtime) {', 26 | ' runtime = runtime || this._runtime;', 27 | ' var template = new runtime.Template();', 28 | '', 29 | ' (function (parent) {' 30 | ); 31 | 32 | this.indent(2); 33 | ast.children.forEach(this.compileNode.bind(this)); 34 | this.outdent(2); 35 | 36 | this.write( 37 | ' })(template.html);', 38 | ' var firstChild = template.html.firstChild;', 39 | ' firstChild.update = template.update.bind(template);', 40 | ' return firstChild;', 41 | '}' 42 | ); 43 | 44 | return this.writer.toString(); 45 | }; 46 | 47 | Compiler.prototype.compileNode = function (node) { 48 | if (node.type === 'Element') return this.compileElement(node); 49 | if (node.type === 'TextNode') return this.compileTextNode(node); 50 | if (node.type === 'BlockStatement') return this.compileBlock(node); 51 | if (node.type === 'DocumentFragment') return this.compileDocumentFragment(node); 52 | }; 53 | 54 | Compiler.prototype.compileExpression = function (ast) { 55 | if (ast.type === 'Literal') { 56 | this.write( 57 | "runtime.hooks.EVENTIFY_LITERAL.call(template, " + JSON.stringify(ast.value) + ")" 58 | ); 59 | } 60 | 61 | if (ast.type === 'Binding') { 62 | this.write( 63 | "runtime.hooks.EVENTIFY_BINDING.call(template, context, '" + ast.keypath + "')" 64 | ); 65 | } 66 | 67 | if (ast.type === 'Expression') { 68 | var name = ast.name; 69 | 70 | this.write( 71 | "runtime.hooks.EXPRESSION('" + name + "', [" 72 | ); 73 | 74 | this.indent(1); 75 | ast.arguments.map(function (expr) { 76 | this.compileExpression(expr); 77 | this.writer.appendToLastLine(','); 78 | }.bind(this)); 79 | this.outdent(1); 80 | 81 | this.write("])"); 82 | } 83 | }; 84 | 85 | Compiler.prototype.compileTextNode = function(element, o) { 86 | o = o || {}; 87 | 88 | this.write( 89 | "(function (parent) {", 90 | " var expr = (" 91 | ); 92 | 93 | this.indent(2); 94 | this.compileExpression(element.content); 95 | this.outdent(2); 96 | 97 | this.write( 98 | " );", 99 | " var node = document.createTextNode((expr.value||expr.value===0) ? expr.value : '');", 100 | " expr.on('change', function (text) { node.data = (text||text===0) ? text : ''; });", 101 | " parent.appendChild(node);", 102 | "})(parent);" 103 | ); 104 | }; 105 | 106 | Compiler.prototype.compileElement = function(element, o) { 107 | o = o || {}; 108 | 109 | this.write( 110 | "(function (parent) {", 111 | " var element = document.createElement('" + element.tagName + "');", 112 | " var expr;" 113 | ); 114 | 115 | this.indent(1); 116 | this.compileElementAttributes(element); 117 | this.outdent(1); 118 | 119 | this.indent(1); 120 | if (element.children.length) { 121 | this.write( 122 | "(function (parent) {" 123 | ); 124 | 125 | this.indent(1); 126 | element.children.forEach(this.compileNode.bind(this)); 127 | this.outdent(1); 128 | 129 | this.write( 130 | "})(element);" 131 | ); 132 | } 133 | this.outdent(1); 134 | 135 | this.write( 136 | " parent.appendChild(element);", 137 | "})(parent);" 138 | ); 139 | }; 140 | 141 | Compiler.prototype.compileElementAttributes = function (element) { 142 | Object.keys(element.attributes).forEach(function (attrName) { 143 | var attr = element.attributes[attrName]; 144 | 145 | this.compileAttribute(attrName, attr); 146 | 147 | }.bind(this)); 148 | }; 149 | 150 | Compiler.prototype.compileAttribute = function (attrName, attr) { 151 | if (attr.type === 'Literal') { 152 | this.write( 153 | "element.setAttribute('" + attrName + "', '" + attr.value.replace(/\n/g, '') + "');" 154 | ); 155 | return; 156 | } 157 | 158 | this.write("expr = ("); 159 | this.indent(1); 160 | this.compileExpression(attr); 161 | this.outdent(1); 162 | this.write(");"); 163 | 164 | if (isBooleanAttribute(attrName)) { 165 | this.compileBooleanAttribute(attrName); 166 | } else { 167 | this.compileStandardAttribute(attrName); 168 | } 169 | }; 170 | 171 | Compiler.prototype.compileBooleanAttribute = function (attrName) { 172 | this.write( 173 | "element[ expr.value ? 'setAttribute' : 'removeAttribute']('" + attrName + "', '');", 174 | "expr.on('change', function (v) {", 175 | " element[ v ? 'setAttribute' : 'removeAttribute']('" + attrName + "', '');", 176 | "});" 177 | ); 178 | }; 179 | 180 | Compiler.prototype.compileStandardAttribute = function (attrName) { 181 | this.write( 182 | "element.setAttribute('" + attrName + "', expr.value ? runtime.hooks.ESCAPE_FOR_ATTRIBUTE('" + attrName + "', expr.value) : '');", 183 | "expr.on('change', function (v) {", 184 | " element.setAttribute('" + attrName + "', v ? runtime.hooks.ESCAPE_FOR_ATTRIBUTE('" + attrName + "', v) : '');", 185 | "});" 186 | ); 187 | }; 188 | 189 | Compiler.prototype.compileDocumentFragment = function (node) { 190 | this.compileBlock({ 191 | blockType: 'documentFragment', 192 | blockExpression: node.content, 193 | body: [], 194 | alternate: [] 195 | }); 196 | }; 197 | 198 | Compiler.prototype.compileBlock = function (node) { 199 | this.write( 200 | "runtime.hooks.HELPER('" + node.blockType + "', [", 201 | " parent,", 202 | " context,", 203 | " (" 204 | ); 205 | 206 | this.indent(2); 207 | this.compileExpression(node.blockExpression); 208 | this.outdent(2); 209 | 210 | this.write( 211 | " ),", 212 | " function (parent) {" 213 | ); 214 | 215 | this.indent(2); 216 | node.body.forEach(this.compileNode.bind(this)); 217 | this.outdent(2); 218 | 219 | this.write( 220 | " },", 221 | " function (parent) {" 222 | ); 223 | 224 | this.indent(2); 225 | node.alternate.forEach(this.compileNode.bind(this)); 226 | this.outdent(2); 227 | 228 | this.write( 229 | "}]);" 230 | ); 231 | }; 232 | 233 | module.exports.compile = compile; 234 | -------------------------------------------------------------------------------- /lib/evented-property.js: -------------------------------------------------------------------------------- 1 | var Events = require('backbone-events-standalone'); 2 | 3 | /* A really simple evented property 4 | * 5 | * var prop = property(10); 6 | * 7 | * prop.on('change', function (newValue) { console.log('got:', newValue); }); 8 | * prop.value = 100 //=> logs "got: 100" 9 | * prop.value //=> 100 10 | */ 11 | 12 | function property(value) { 13 | var prop = Events.mixin({}); 14 | var _value = value; 15 | 16 | Object.defineProperty(prop, 'value', { 17 | get: function () { 18 | return _value; 19 | }, 20 | set: function (newValue) { 21 | var oldValue = _value; 22 | _value = newValue; 23 | if (_value !== oldValue) { 24 | prop.trigger('change', _value, { previous: oldValue }); 25 | } 26 | } 27 | }); 28 | 29 | return prop; 30 | } 31 | 32 | module.exports = property; 33 | -------------------------------------------------------------------------------- /lib/eventify-fn.js: -------------------------------------------------------------------------------- 1 | var property = require('./evented-property'); 2 | 3 | module.exports = function (fn) { 4 | return function (/*args...*/) { 5 | var args = [].slice.call(arguments); 6 | 7 | var getNewValueFromFn = function () { 8 | return fn.apply(fn, args.map(function (a) { return a.value; })); 9 | }; 10 | 11 | var prop = property(getNewValueFromFn()); 12 | 13 | args.forEach(function (a) { 14 | a.on('change', function () { 15 | prop.value = getNewValueFromFn(); 16 | }); 17 | }); 18 | 19 | return prop; 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/file-writer.js: -------------------------------------------------------------------------------- 1 | function FileWriter () { 2 | this.content = []; 3 | this.depth = 1; 4 | } 5 | 6 | FileWriter.prototype.appendToLastLine = function (str) { 7 | this.content[this.content.length - 1] += str; 8 | }; 9 | 10 | FileWriter.prototype.write = function (lines) { 11 | if (arguments.length > 1) { 12 | lines = [].slice.call(arguments); 13 | } 14 | 15 | if (!Array.isArray(lines)) lines = [lines]; 16 | 17 | lines = lines.map(this.indentString.bind(this)); 18 | this.content = this.content.concat(lines); 19 | }; 20 | 21 | FileWriter.prototype.indent = function (depth) { 22 | depth = depth || 1; 23 | this.depth += depth; 24 | }; 25 | 26 | FileWriter.prototype.outdent = function (depth) { 27 | depth = depth || 1; 28 | this.depth -= depth; 29 | }; 30 | 31 | FileWriter.prototype.toString = function () { 32 | return this.content.join('\n'); 33 | }; 34 | 35 | FileWriter.prototype.indentString = function (string) { 36 | return Array(this.depth).join(' ') + string; 37 | }; 38 | 39 | module.exports = FileWriter; 40 | -------------------------------------------------------------------------------- /lib/is-boolean-attribute.js: -------------------------------------------------------------------------------- 1 | var booleanAttributes = [ 2 | "async", 3 | "autofocus", 4 | "autoplay", 5 | "badInput", 6 | "bufferingThrottled", 7 | "checked", 8 | "closed", 9 | "compact", 10 | "complete", 11 | "controls", 12 | "cookieEnabled", 13 | "customError", 14 | "declare", 15 | "default", 16 | "defaultChecked", 17 | "defaultMuted", 18 | "defaultSelected", 19 | "defer", 20 | "disabled", 21 | "draggable", 22 | "enabled", 23 | "ended", 24 | "formNoValidate", 25 | "hidden", 26 | "indeterminate", 27 | "isContentEditable", 28 | "isMap", 29 | "javaEnabled", 30 | "loop", 31 | "multiple", 32 | "muted", 33 | "noHref", 34 | "noResize", 35 | "noShade", 36 | "noValidate", 37 | "noWrap", 38 | "onLine", 39 | "patternMismatch", 40 | "pauseOnExit", 41 | "paused", 42 | "persisted", 43 | "rangeOverflow", 44 | "rangeUnderflow", 45 | "readOnly", 46 | "required", 47 | "reversed", 48 | "seeking", 49 | "selected", 50 | "spellcheck", 51 | "stepMismatch", 52 | "tooLong", 53 | "tooShort", 54 | "translate", 55 | "trueSpeed", 56 | "typeMismatch", 57 | "typeMustMatch", 58 | "valid", 59 | "valueMissing", 60 | "visible", 61 | "willValidate", 62 | ]; 63 | 64 | module.exports = function isbooleanAttribute (attr) { 65 | return booleanAttributes.indexOf(attr) !== -1; 66 | }; 67 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | var AST = require('./AST'); 2 | var DomHandler = require('domhandler'); 3 | var htmlparser = require('htmlparser2'); 4 | var splitter = require('./split-and-keep-splitter'); 5 | var sexpParser = require('./sexp-parser'); 6 | 7 | var REGEXES = { 8 | block: /{{{?\s*(#|\/)?\s*([^}]*)}?}}/, 9 | curlyId: /{{(c\d+)}}/g, 10 | splitter: /{{{?([^}]*)}?}}/g, 11 | simpleExpression: /{{{?\s*([^}]*)\s*}?}}/, 12 | safeExpression: /{{{\s*([^}]*)\s*}}}/, 13 | }; 14 | 15 | /* Parsing: 16 | * - Given a template of html + {{ }} bindings 17 | * - First parse the html with an html parser into a dom 18 | * - Iterate over the dom, building a JSON AST of nodes 19 | * - For each node, check if it's attributes have {{ }} bindings, and build an AST for those expressions 20 | * - For each node with content, check if it's contents contain {{# }} bindings, and build an AST of those expressions too 21 | */ 22 | function parse(tmpl, cb) { 23 | preParse(tmpl, function (err, tmpl, curlies) { 24 | parseHTML(tmpl, function (err, dom) { 25 | var ast = AST.Template(); 26 | 27 | if (err) return cb(err); 28 | 29 | dom.forEach(function (node) { 30 | ast.children = ast.children.concat(parseNode(node, curlies)); 31 | }); 32 | collectBlocks(ast); 33 | cb(null, ast); 34 | }); 35 | }); 36 | } 37 | 38 | function preParse(tmpl, cb) { 39 | var curlies = {}; 40 | var curlyId = 0; 41 | 42 | tmpl = tmpl.replace(new RegExp(REGEXES.block.source, 'g'), function (match) { 43 | curlyId++; 44 | curlies['c' + curlyId] = match; 45 | return "{{c" + curlyId + "}}"; 46 | }); 47 | 48 | cb(null, tmpl, curlies); 49 | } 50 | 51 | //Parse template into a dom tree 52 | function parseHTML(tmpl, cb) { 53 | tmpl = tmpl.trim(); 54 | var handler = new DomHandler(cb, { normalizeWhitespace: true }); 55 | var htmlParser = new htmlparser.Parser(handler); 56 | 57 | htmlParser.write(tmpl.trim()); 58 | htmlParser.done(); 59 | } 60 | 61 | function parseNode(node, curlies) { 62 | var NODE_PARSERS = { 63 | tag: parseElement, 64 | text: parseTextNode 65 | }; 66 | var parsed; 67 | 68 | if (!NODE_PARSERS[node.type]) return []; 69 | 70 | parsed = NODE_PARSERS[node.type](node, curlies); 71 | 72 | if (!Array.isArray(parsed)) parsed = [parsed]; 73 | return parsed; 74 | } 75 | 76 | //build AST node from element 77 | function parseElement(el, curlies) { 78 | var attributes = {}; 79 | var children = []; 80 | 81 | Object.keys(el.attribs).forEach(function (attrName) { 82 | var attr = el.attribs[attrName].replace(REGEXES.curlyId, function (match, id) { 83 | return curlies[id]; 84 | }); 85 | attributes[attrName] = parseSimpleExpression(attr); 86 | }); 87 | 88 | el.children.forEach(function (node) { 89 | children = children.concat(parseNode(node, curlies)); 90 | }); 91 | 92 | return AST.Element(el.name, attributes, children); 93 | } 94 | 95 | function parseSimpleExpression(string) { 96 | string = string.trim(); 97 | var match = string.match(REGEXES.simpleExpression); 98 | 99 | //class="foo" 100 | if (!match) return AST.Literal(string); 101 | 102 | //class="{{foo}}" 103 | if (match[0] === string) { 104 | var expr; 105 | 106 | if (match[1].indexOf('(') > -1) { 107 | expr = sexpParser.parse(match[1]); 108 | } else { 109 | expr = AST.Binding(match[1].trim()); 110 | } 111 | 112 | if (match[0].match(REGEXES.safeExpression)) { 113 | return AST.Expression('safe', [ expr ]); 114 | } else { 115 | return expr; 116 | } 117 | } 118 | 119 | //class="baz {{foo}} bar" 120 | var args = splitter( 121 | string, 122 | REGEXES.splitter, 123 | function (str) { 124 | return parseSimpleExpression(str); 125 | }, 126 | function (str) { 127 | return AST.Literal(str); 128 | } 129 | ); 130 | 131 | return AST.Expression('concat', args); 132 | } 133 | 134 | function parseTextNode(node, curlies) { 135 | var data = node.data.replace(REGEXES.curlyId, function (match, id) { 136 | return curlies[id]; 137 | }); 138 | 139 | return splitter( 140 | data, 141 | REGEXES.splitter, 142 | function (str) { 143 | return parseMustaches(str); 144 | }, 145 | function (str) { 146 | if (str.trim() === '') return AST.TextNode( AST.Literal(' ') ); 147 | return AST.TextNode( AST.Literal(str) ); 148 | } 149 | ); 150 | } 151 | 152 | function parseSafeExpression(string) { 153 | var match = string.match(REGEXES.safeExpression); 154 | return AST.DocumentFragment(parseBlockExpression(match[1].trim())); 155 | } 156 | 157 | function parseMustaches(string) { 158 | if (string.match(REGEXES.safeExpression)) { 159 | return parseSafeExpression(string); 160 | } 161 | var match = string.match(REGEXES.block); 162 | var tagType = match[1]; 163 | var blockType; 164 | var blockExpression = match[2].trim(); 165 | 166 | switch(tagType) { 167 | case undefined: 168 | return AST.TextNode( parseBlockExpression(blockExpression) ); 169 | case "#": 170 | var parts = blockExpression.split(' '); 171 | blockType = parts.shift(); 172 | blockExpression = parseBlockExpression(parts.join(' ')); 173 | if (blockType === 'else') { 174 | return AST.BlockElse(); 175 | } else { 176 | return AST.BlockStart(blockType, blockExpression); 177 | } 178 | break; 179 | case "/": 180 | blockType = blockExpression.split(' ')[0].trim(); 181 | return AST.BlockEnd(blockType); 182 | } 183 | } 184 | 185 | function parseBlockExpression(string) { 186 | if (string.indexOf('(') > -1) { 187 | return sexpParser.parse(string); 188 | } else { 189 | return AST.Binding(string); 190 | } 191 | } 192 | 193 | function collectBlocks(node) { 194 | if (!node.children) return node; 195 | 196 | var newChildren = []; 197 | var blockStack = []; 198 | 199 | node.children.forEach(function (node) { 200 | var block; 201 | 202 | if (node.type === 'BlockStart') { 203 | block = AST.BlockStatement(node.blockType, node.blockExpression); 204 | block.state = 'body'; 205 | blockStack.push(block); 206 | } 207 | else if (node.type === 'BlockElse') { 208 | last(blockStack).state = 'alternate'; 209 | } 210 | else { 211 | if (node.type === 'BlockEnd') { 212 | node = blockStack.pop(); 213 | node.body = node.body.map(collectBlocks); 214 | node.alternate = node.alternate.map(collectBlocks); 215 | delete node.state; 216 | } 217 | 218 | var currentNodeList; 219 | if (blockStack.length) { 220 | block = last(blockStack); 221 | currentNodeList = block[block.state]; 222 | } 223 | else { 224 | currentNodeList = newChildren; 225 | } 226 | 227 | currentNodeList.push(node); 228 | } 229 | }); 230 | 231 | node.children = newChildren.map(collectBlocks); 232 | return node; 233 | } 234 | 235 | function last(arr) { 236 | return arr[arr.length - 1]; 237 | } 238 | module.exports = parse; 239 | module.exports.parseHTML = parseHTML; 240 | -------------------------------------------------------------------------------- /lib/reduce-keypath.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Given a context object { a: { 3 | * b: { 4 | * c: 'hello' 5 | * }, 6 | * d: { 7 | * e: 'goodbye' 8 | * } 9 | * } 10 | * 11 | * And keypaths, like "a.b.c" or "d.e", or "a.b" 12 | * 13 | * This function returns the value at the keypath: 14 | * reduceKeypath(context, "a.b.c") //=> 'hello' 15 | * reduceKeypath(context, "d.e") //=> 'goodbye' 16 | * reduceKeypath(context, "a.b") //=> { c: 'hello' } 17 | * 18 | * Will return undefined if the keypath doesn't exist. 19 | */ 20 | 21 | module.exports = function reduceKeypath(context, keypath) { 22 | var path = keypath.trim().split('.'); 23 | 24 | return path.reduce(function (obj, path) { 25 | return obj && obj[path]; 26 | }, context); 27 | }; 28 | -------------------------------------------------------------------------------- /lib/relative-keypath.js: -------------------------------------------------------------------------------- 1 | module.exports = function relativeKeypath(from, to) { 2 | from = from.trim(); 3 | to = to.trim(); 4 | 5 | if (to.indexOf(from + '.') !== 0) { 6 | throw new Error('Cannot get to "' + to + '" from "' + from + '"'); 7 | } 8 | return to.substr(from.length + 1); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/runtime/expressions.js: -------------------------------------------------------------------------------- 1 | /* jshint evil: true */ 2 | var eventifyFn = require('../eventify-fn'); 3 | var SafeString = require('./safe-string'); 4 | 5 | var EXPRESSIONS = {}; 6 | 7 | function registerExpression (name, fn) { 8 | EXPRESSIONS[name] = eventifyFn(fn); 9 | } 10 | 11 | function lookupExpression (name) { 12 | if (!EXPRESSIONS[name]) throw new Error('Cannot find expression ' + name); 13 | return EXPRESSIONS[name]; 14 | } 15 | 16 | module.exports = { 17 | lookupExpression: lookupExpression, 18 | registerExpression: registerExpression 19 | }; 20 | 21 | 22 | //Builtin expressions: 23 | 24 | function makeBinaryOperatorFunction(op) { 25 | registerExpression(op, new Function("a", "b", "return a " + op + " b")); 26 | } 27 | function aliasBinaryOperatorFunction(name, op) { 28 | registerExpression(name, new Function("a", "b", "return a " + op + " b")); 29 | } 30 | 31 | ['*', '+', '-', '/', '%', '<', '==', '===', '>'].forEach(makeBinaryOperatorFunction); 32 | 33 | aliasBinaryOperatorFunction("leq", "<="); 34 | aliasBinaryOperatorFunction("lteq", "<="); 35 | aliasBinaryOperatorFunction("geq", ">="); 36 | aliasBinaryOperatorFunction("gteq", ">="); 37 | 38 | registerExpression('!', function (inp) { return !inp; }); 39 | registerExpression('not', function (inp) { return !inp; }); 40 | 41 | 42 | registerExpression('if', function (cond, yes, no) { 43 | if (cond) { 44 | return yes; 45 | } else { 46 | return no || ''; 47 | } 48 | }); 49 | 50 | registerExpression('concat', function (/*args...*/) { 51 | var truthy = function (a) { return !!a; }; 52 | return [].slice.call(arguments).filter(truthy).join(''); 53 | }); 54 | 55 | registerExpression('safe', function (value) { 56 | return new SafeString(value); 57 | }); 58 | 59 | registerExpression('call', function (object, fn/*, args...*/) { 60 | return object[fn].apply(object, [].slice.call(arguments, 2)); 61 | }); 62 | 63 | registerExpression('apply', function (object, fn, argArray) { 64 | return object[fn].apply(object, argArray); 65 | }); 66 | -------------------------------------------------------------------------------- /lib/runtime/helpers.js: -------------------------------------------------------------------------------- 1 | var HELPERS = {}; 2 | 3 | function registerHelper(name, fn) { 4 | HELPERS[name] = fn; 5 | } 6 | 7 | function lookupHelper(name) { 8 | if (!HELPERS[name]) throw new Error('Cannot find helper ' + name); 9 | return HELPERS[name]; 10 | } 11 | 12 | module.exports = { 13 | registerHelper: registerHelper, 14 | lookupHelper: lookupHelper 15 | }; 16 | 17 | //Builtin helpers: 18 | registerHelper('if', function (parent, context, expression, body, alternate) { 19 | var anchor = document.createComment('if placeholder'); 20 | var elements, newElements; 21 | //FIXME: need to wrap in a div, ugh 22 | 23 | var trueDiv = document.createElement('div'); 24 | var falseDiv = document.createElement('div'); 25 | 26 | parent.appendChild(anchor); 27 | 28 | body(trueDiv); 29 | alternate(falseDiv); 30 | 31 | var trueEls = [].slice.call(trueDiv.childNodes); 32 | var falseEls = [].slice.call(falseDiv.childNodes); 33 | 34 | var render = function (value, force) { 35 | var first = false; 36 | if (value) { 37 | if (!first) { 38 | falseEls.forEach(function (el) { 39 | if (el.parentNode) el.parentNode.removeChild(el); 40 | }); 41 | } 42 | 43 | insertNodesAfterAnchor(anchor, trueEls); 44 | } else { 45 | if (!first) { 46 | trueEls.forEach(function (el) { 47 | if (el.parentNode) el.parentNode.removeChild(el); 48 | }); 49 | } 50 | insertNodesAfterAnchor(anchor, falseEls); 51 | } 52 | }; 53 | 54 | render(expression.value, true); 55 | expression.on('change', render); 56 | }); 57 | 58 | registerHelper('unless', function (parent, context, expression, body, alternate) { 59 | lookupHelper('if')(parent, context, expression, alternate, body); 60 | }); 61 | 62 | registerHelper('documentFragment', function (parent, context, expression) { 63 | var anchor = document.createComment('document fragment placeholder'); 64 | var fragment = document.createElement('div'); 65 | var currentNodes; 66 | 67 | parent.appendChild(anchor); 68 | 69 | var render = function () { 70 | if (currentNodes) { 71 | currentNodes.forEach(function (node) { 72 | if (node.parentNode) node.parentNode.removeChild(node); 73 | }); 74 | } 75 | 76 | fragment.innerHTML = expression.value; 77 | 78 | currentNodes = [].slice.call(fragment.childNodes); 79 | insertNodesAfterAnchor(anchor, currentNodes); 80 | }; 81 | 82 | render(); 83 | expression.on('change', render); 84 | }); 85 | 86 | function insertNodesAfterAnchor(anchor, nodes) { 87 | if (!nodes) return; 88 | 89 | var parent = anchor.parentNode; 90 | var nextEl = anchor.nextSibling; 91 | 92 | if (!Array.isArray(nodes)) nodes = [].slice.call(nodes); 93 | nodes.forEach(function (node) { 94 | parent.insertBefore(node, nextEl); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /lib/runtime/hooks.js: -------------------------------------------------------------------------------- 1 | var helpers = require('./helpers'); 2 | var expressions = require('./expressions'); 3 | var property = require('../evented-property'); 4 | var reduceKeypath = require('../reduce-keypath'); 5 | 6 | 7 | module.exports.EVENTIFY_LITERAL = function (value) { 8 | return property(value); 9 | }; 10 | 11 | module.exports.EVENTIFY_BINDING = function (context, keypath) { 12 | var value = reduceKeypath(context, keypath); 13 | var s = property(value); 14 | 15 | this.addCallback(keypath, function (value) { 16 | s.value = value; 17 | }); 18 | 19 | return s; 20 | }; 21 | 22 | module.exports.EXPRESSION = function (name, args) { 23 | var expression = expressions.lookupExpression(name); 24 | return expression.apply(expression, args); 25 | }; 26 | 27 | module.exports.HELPER = function (name, args) { 28 | var helper = helpers.lookupHelper(name); 29 | return helper.apply(helper, args); 30 | }; 31 | 32 | module.exports.ESCAPE_FOR_ATTRIBUTE = function (attrName, value) { 33 | if (value.isSafeString) return value; 34 | 35 | var sanitationNode, normalisedValue; 36 | var protocolRegex = /^\s*(https?|ftp|mailto):/; 37 | 38 | if (attrName === 'href') { 39 | sanitationNode = document.createElement('a'); 40 | sanitationNode.setAttribute('href', value); 41 | normalisedValue = sanitationNode.href; 42 | 43 | if (normalisedValue.match(protocolRegex)) { 44 | return normalisedValue; 45 | } else { 46 | return 'unsafe:' + normalisedValue; 47 | } 48 | } 49 | 50 | if (attrName === 'src') { 51 | sanitationNode = document.createElement('a'); 52 | sanitationNode.setAttribute('href', value); 53 | normalisedValue = sanitationNode.href; 54 | 55 | if (normalisedValue.match(protocolRegex)) { 56 | return normalisedValue; 57 | } else { 58 | return 'unsafe:' + normalisedValue; 59 | } 60 | } 61 | 62 | return value; 63 | }; 64 | -------------------------------------------------------------------------------- /lib/runtime/safe-string.js: -------------------------------------------------------------------------------- 1 | function SafeString(value) { 2 | this.value = value.toString(); 3 | } 4 | 5 | SafeString.prototype.isSafeString = true; 6 | SafeString.prototype.toString = function () { 7 | return this.value; 8 | }; 9 | 10 | module.exports = SafeString; 11 | 12 | -------------------------------------------------------------------------------- /lib/runtime/template.js: -------------------------------------------------------------------------------- 1 | var reduceKeypath = require('../reduce-keypath'); 2 | var requestAnimationFrame = require('raf'); 3 | var KeyTreeStore = require('key-tree-store'); 4 | var relativeKeypath = require('../relative-keypath'); 5 | 6 | KeyTreeStore.prototype.keys = function (keypath) { 7 | var keys = Object.keys(this.storage); 8 | return keys.filter(function (k) { 9 | return (k.indexOf(keypath) === 0); 10 | }); 11 | }; 12 | 13 | function Template () { 14 | this._callbacks = new KeyTreeStore(); 15 | this._changes = {}; 16 | this.html = document.createDocumentFragment(); 17 | this.isRenderQueued = false; 18 | } 19 | 20 | Template.prototype.update = function (keypath, value) { 21 | var keys = this._callbacks.keys(keypath); 22 | var self = this; 23 | 24 | keys.forEach(function (key) { 25 | if (key === keypath) { 26 | self._changes[key] = value; 27 | } else { 28 | self._changes[key] = reduceKeypath(value, relativeKeypath(keypath, key)); 29 | } 30 | }); 31 | 32 | if (!this.isRenderQueued) this.queueRender(); 33 | }; 34 | 35 | Template.prototype.queueRender = function () { 36 | requestAnimationFrame(this.doRender.bind(this)); 37 | this.isRenderQueued = true; 38 | }; 39 | 40 | Template.prototype._update = function (keypath, value) { 41 | if (this._callbacks.storage[keypath]) { 42 | this._callbacks.storage[keypath].forEach(function (cb) { 43 | cb(value); 44 | }); 45 | } 46 | }; 47 | 48 | Template.prototype.doRender = function () { 49 | var keypaths = Object.keys(this._changes); 50 | for (var i=0, len = keypaths.length; i < len; i++) { 51 | this._update(keypaths[i], this._changes[keypaths[i]]); 52 | } 53 | this._changes = {}; 54 | this.isRenderQueued = false; 55 | }; 56 | 57 | Template.prototype.addCallback = function(keypath, cb) { 58 | this._callbacks.add(keypath, cb); 59 | }; 60 | 61 | module.exports = Template; 62 | -------------------------------------------------------------------------------- /lib/sexp-parser.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | /* 3 | * Generated by PEG.js 0.8.0. 4 | * 5 | * http://pegjs.majda.cz/ 6 | */ 7 | 8 | function peg$subclass(child, parent) { 9 | function ctor() { this.constructor = child; } 10 | ctor.prototype = parent.prototype; 11 | child.prototype = new ctor(); 12 | } 13 | 14 | function SyntaxError(message, expected, found, offset, line, column) { 15 | this.message = message; 16 | this.expected = expected; 17 | this.found = found; 18 | this.offset = offset; 19 | this.line = line; 20 | this.column = column; 21 | 22 | this.name = "SyntaxError"; 23 | } 24 | 25 | peg$subclass(SyntaxError, Error); 26 | 27 | function parse(input) { 28 | var options = arguments.length > 1 ? arguments[1] : {}, 29 | 30 | peg$FAILED = {}, 31 | 32 | peg$startRuleFunctions = { start: peg$parsestart }, 33 | peg$startRuleFunction = peg$parsestart, 34 | 35 | peg$c0 = { type: "other", description: "integer" }, 36 | peg$c1 = [], 37 | peg$c2 = peg$FAILED, 38 | peg$c3 = /^[0-9]/, 39 | peg$c4 = { type: "class", value: "[0-9]", description: "[0-9]" }, 40 | peg$c5 = function(digits) { 41 | 42 | return { 43 | type: 'Literal', 44 | value: parseInt(digits.join(""), 10) 45 | } 46 | }, 47 | peg$c6 = null, 48 | peg$c7 = "(", 49 | peg$c8 = { type: "literal", value: "(", description: "\"(\"" }, 50 | peg$c9 = ")", 51 | peg$c10 = { type: "literal", value: ")", description: "\")\"" }, 52 | peg$c11 = function(body) { 53 | 54 | return { 55 | type: 'Expression', 56 | name: body.identifier, 57 | arguments: body.args 58 | }; 59 | }, 60 | peg$c12 = function(id, args) { 61 | 62 | return { 63 | identifier: id, 64 | args: args.filter(function(a) { return (a != ""); }) 65 | }; 66 | }, 67 | peg$c13 = { type: "other", description: "string" }, 68 | peg$c14 = "\"", 69 | peg$c15 = { type: "literal", value: "\"", description: "\"\\\"\"" }, 70 | peg$c16 = function(chars) { 71 | return { type: "Literal", value: chars.join("") }; 72 | }, 73 | peg$c17 = "'", 74 | peg$c18 = { type: "literal", value: "'", description: "\"'\"" }, 75 | peg$c19 = /^[0-9a-f]/i, 76 | peg$c20 = { type: "class", value: "[0-9a-f]i", description: "[0-9a-f]i" }, 77 | peg$c21 = void 0, 78 | peg$c22 = "\\", 79 | peg$c23 = { type: "literal", value: "\\", description: "\"\\\\\"" }, 80 | peg$c24 = { type: "any", description: "any character" }, 81 | peg$c25 = function() { return text(); }, 82 | peg$c26 = function(sequence) { return sequence; }, 83 | peg$c27 = "0", 84 | peg$c28 = { type: "literal", value: "0", description: "\"0\"" }, 85 | peg$c29 = function() { return "\0"; }, 86 | peg$c30 = "b", 87 | peg$c31 = { type: "literal", value: "b", description: "\"b\"" }, 88 | peg$c32 = function() { return "\b"; }, 89 | peg$c33 = "f", 90 | peg$c34 = { type: "literal", value: "f", description: "\"f\"" }, 91 | peg$c35 = function() { return "\f"; }, 92 | peg$c36 = "n", 93 | peg$c37 = { type: "literal", value: "n", description: "\"n\"" }, 94 | peg$c38 = function() { return "\n"; }, 95 | peg$c39 = "r", 96 | peg$c40 = { type: "literal", value: "r", description: "\"r\"" }, 97 | peg$c41 = function() { return "\r"; }, 98 | peg$c42 = "t", 99 | peg$c43 = { type: "literal", value: "t", description: "\"t\"" }, 100 | peg$c44 = function() { return "\t"; }, 101 | peg$c45 = "v", 102 | peg$c46 = { type: "literal", value: "v", description: "\"v\"" }, 103 | peg$c47 = function() { return "\x0B"; }, 104 | peg$c48 = "x", 105 | peg$c49 = { type: "literal", value: "x", description: "\"x\"" }, 106 | peg$c50 = "u", 107 | peg$c51 = { type: "literal", value: "u", description: "\"u\"" }, 108 | peg$c52 = function(digits) { 109 | return String.fromCharCode(parseInt(digits, 16)); 110 | }, 111 | peg$c53 = "true", 112 | peg$c54 = { type: "literal", value: "true", description: "\"true\"" }, 113 | peg$c55 = "false", 114 | peg$c56 = { type: "literal", value: "false", description: "\"false\"" }, 115 | peg$c57 = function(bool) { 116 | 117 | if (bool === 'true') return { type: 'Literal', value: true }; 118 | if (bool === 'false') return { type: 'Literal', value: false }; 119 | }, 120 | peg$c58 = /^[a-zA-Z=*:]/, 121 | peg$c59 = { type: "class", value: "[a-zA-Z=*:]", description: "[a-zA-Z=*:]" }, 122 | peg$c60 = /^[a-zA-Z0-9_=*-:]/, 123 | peg$c61 = { type: "class", value: "[a-zA-Z0-9_=*-:]", description: "[a-zA-Z0-9_=*-:]" }, 124 | peg$c62 = ".", 125 | peg$c63 = { type: "literal", value: ".", description: "\".\"" }, 126 | peg$c64 = function(binding) { 127 | 128 | 129 | return { 130 | type: 'Binding', 131 | keypath: flatten(binding).join('') 132 | }; 133 | }, 134 | peg$c65 = /^[a-zA-Z=*:\/+<>\-%]/, 135 | peg$c66 = { type: "class", value: "[a-zA-Z=*:\\/+<>\\-%]", description: "[a-zA-Z=*:\\/+<>\\-%]" }, 136 | peg$c67 = /^[a-zA-Z0-9_=*-:.]/, 137 | peg$c68 = { type: "class", value: "[a-zA-Z0-9_=*-:.]", description: "[a-zA-Z0-9_=*-:.]" }, 138 | peg$c69 = function(identifier) { 139 | 140 | return flatten(identifier).join(''); 141 | }, 142 | peg$c70 = /^[\t\r\n ]/, 143 | peg$c71 = { type: "class", value: "[\\t\\r\\n ]", description: "[\\t\\r\\n ]" }, 144 | peg$c72 = function() { return ""; }, 145 | peg$c73 = "#", 146 | peg$c74 = { type: "literal", value: "#", description: "\"#\"" }, 147 | 148 | peg$currPos = 0, 149 | peg$reportedPos = 0, 150 | peg$cachedPos = 0, 151 | peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }, 152 | peg$maxFailPos = 0, 153 | peg$maxFailExpected = [], 154 | peg$silentFails = 0, 155 | 156 | peg$result; 157 | 158 | if ("startRule" in options) { 159 | if (!(options.startRule in peg$startRuleFunctions)) { 160 | throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); 161 | } 162 | 163 | peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; 164 | } 165 | 166 | function text() { 167 | return input.substring(peg$reportedPos, peg$currPos); 168 | } 169 | 170 | function offset() { 171 | return peg$reportedPos; 172 | } 173 | 174 | function line() { 175 | return peg$computePosDetails(peg$reportedPos).line; 176 | } 177 | 178 | function column() { 179 | return peg$computePosDetails(peg$reportedPos).column; 180 | } 181 | 182 | function expected(description) { 183 | throw peg$buildException( 184 | null, 185 | [{ type: "other", description: description }], 186 | peg$reportedPos 187 | ); 188 | } 189 | 190 | function error(message) { 191 | throw peg$buildException(message, null, peg$reportedPos); 192 | } 193 | 194 | function peg$computePosDetails(pos) { 195 | function advance(details, startPos, endPos) { 196 | var p, ch; 197 | 198 | for (p = startPos; p < endPos; p++) { 199 | ch = input.charAt(p); 200 | if (ch === "\n") { 201 | if (!details.seenCR) { details.line++; } 202 | details.column = 1; 203 | details.seenCR = false; 204 | } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { 205 | details.line++; 206 | details.column = 1; 207 | details.seenCR = true; 208 | } else { 209 | details.column++; 210 | details.seenCR = false; 211 | } 212 | } 213 | } 214 | 215 | if (peg$cachedPos !== pos) { 216 | if (peg$cachedPos > pos) { 217 | peg$cachedPos = 0; 218 | peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }; 219 | } 220 | advance(peg$cachedPosDetails, peg$cachedPos, pos); 221 | peg$cachedPos = pos; 222 | } 223 | 224 | return peg$cachedPosDetails; 225 | } 226 | 227 | function peg$fail(expected) { 228 | if (peg$currPos < peg$maxFailPos) { return; } 229 | 230 | if (peg$currPos > peg$maxFailPos) { 231 | peg$maxFailPos = peg$currPos; 232 | peg$maxFailExpected = []; 233 | } 234 | 235 | peg$maxFailExpected.push(expected); 236 | } 237 | 238 | function peg$buildException(message, expected, pos) { 239 | function cleanupExpected(expected) { 240 | var i = 1; 241 | 242 | expected.sort(function(a, b) { 243 | if (a.description < b.description) { 244 | return -1; 245 | } else if (a.description > b.description) { 246 | return 1; 247 | } else { 248 | return 0; 249 | } 250 | }); 251 | 252 | while (i < expected.length) { 253 | if (expected[i - 1] === expected[i]) { 254 | expected.splice(i, 1); 255 | } else { 256 | i++; 257 | } 258 | } 259 | } 260 | 261 | function buildMessage(expected, found) { 262 | function stringEscape(s) { 263 | function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); } 264 | 265 | return s 266 | .replace(/\\/g, '\\\\') 267 | .replace(/"/g, '\\"') 268 | .replace(/\x08/g, '\\b') 269 | .replace(/\t/g, '\\t') 270 | .replace(/\n/g, '\\n') 271 | .replace(/\f/g, '\\f') 272 | .replace(/\r/g, '\\r') 273 | .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) 274 | .replace(/[\x10-\x1F\x80-\xFF]/g, function(ch) { return '\\x' + hex(ch); }) 275 | .replace(/[\u0180-\u0FFF]/g, function(ch) { return '\\u0' + hex(ch); }) 276 | .replace(/[\u1080-\uFFFF]/g, function(ch) { return '\\u' + hex(ch); }); 277 | } 278 | 279 | var expectedDescs = new Array(expected.length), 280 | expectedDesc, foundDesc, i; 281 | 282 | for (i = 0; i < expected.length; i++) { 283 | expectedDescs[i] = expected[i].description; 284 | } 285 | 286 | expectedDesc = expected.length > 1 287 | ? expectedDescs.slice(0, -1).join(", ") 288 | + " or " 289 | + expectedDescs[expected.length - 1] 290 | : expectedDescs[0]; 291 | 292 | foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input"; 293 | 294 | return "Expected " + expectedDesc + " but " + foundDesc + " found."; 295 | } 296 | 297 | var posDetails = peg$computePosDetails(pos), 298 | found = pos < input.length ? input.charAt(pos) : null; 299 | 300 | if (expected !== null) { 301 | cleanupExpected(expected); 302 | } 303 | 304 | return new SyntaxError( 305 | message !== null ? message : buildMessage(expected, found), 306 | expected, 307 | found, 308 | pos, 309 | posDetails.line, 310 | posDetails.column 311 | ); 312 | } 313 | 314 | function peg$parsestart() { 315 | var s0; 316 | 317 | s0 = peg$parseexpression(); 318 | 319 | return s0; 320 | } 321 | 322 | function peg$parseinteger() { 323 | var s0, s1, s2; 324 | 325 | peg$silentFails++; 326 | s0 = peg$currPos; 327 | s1 = []; 328 | if (peg$c3.test(input.charAt(peg$currPos))) { 329 | s2 = input.charAt(peg$currPos); 330 | peg$currPos++; 331 | } else { 332 | s2 = peg$FAILED; 333 | if (peg$silentFails === 0) { peg$fail(peg$c4); } 334 | } 335 | if (s2 !== peg$FAILED) { 336 | while (s2 !== peg$FAILED) { 337 | s1.push(s2); 338 | if (peg$c3.test(input.charAt(peg$currPos))) { 339 | s2 = input.charAt(peg$currPos); 340 | peg$currPos++; 341 | } else { 342 | s2 = peg$FAILED; 343 | if (peg$silentFails === 0) { peg$fail(peg$c4); } 344 | } 345 | } 346 | } else { 347 | s1 = peg$c2; 348 | } 349 | if (s1 !== peg$FAILED) { 350 | peg$reportedPos = s0; 351 | s1 = peg$c5(s1); 352 | } 353 | s0 = s1; 354 | peg$silentFails--; 355 | if (s0 === peg$FAILED) { 356 | s1 = peg$FAILED; 357 | if (peg$silentFails === 0) { peg$fail(peg$c0); } 358 | } 359 | 360 | return s0; 361 | } 362 | 363 | function peg$parseexpression() { 364 | var s0, s1, s2, s3, s4, s5; 365 | 366 | s0 = peg$currPos; 367 | s1 = peg$parsespace(); 368 | if (s1 === peg$FAILED) { 369 | s1 = peg$c6; 370 | } 371 | if (s1 !== peg$FAILED) { 372 | if (input.charCodeAt(peg$currPos) === 40) { 373 | s2 = peg$c7; 374 | peg$currPos++; 375 | } else { 376 | s2 = peg$FAILED; 377 | if (peg$silentFails === 0) { peg$fail(peg$c8); } 378 | } 379 | if (s2 !== peg$FAILED) { 380 | s3 = peg$parsebody(); 381 | if (s3 !== peg$FAILED) { 382 | if (input.charCodeAt(peg$currPos) === 41) { 383 | s4 = peg$c9; 384 | peg$currPos++; 385 | } else { 386 | s4 = peg$FAILED; 387 | if (peg$silentFails === 0) { peg$fail(peg$c10); } 388 | } 389 | if (s4 !== peg$FAILED) { 390 | s5 = peg$parsespace(); 391 | if (s5 === peg$FAILED) { 392 | s5 = peg$c6; 393 | } 394 | if (s5 !== peg$FAILED) { 395 | peg$reportedPos = s0; 396 | s1 = peg$c11(s3); 397 | s0 = s1; 398 | } else { 399 | peg$currPos = s0; 400 | s0 = peg$c2; 401 | } 402 | } else { 403 | peg$currPos = s0; 404 | s0 = peg$c2; 405 | } 406 | } else { 407 | peg$currPos = s0; 408 | s0 = peg$c2; 409 | } 410 | } else { 411 | peg$currPos = s0; 412 | s0 = peg$c2; 413 | } 414 | } else { 415 | peg$currPos = s0; 416 | s0 = peg$c2; 417 | } 418 | 419 | return s0; 420 | } 421 | 422 | function peg$parsebody() { 423 | var s0, s1, s2, s3, s4, s5; 424 | 425 | s0 = peg$currPos; 426 | s1 = peg$parsespace(); 427 | if (s1 === peg$FAILED) { 428 | s1 = peg$c6; 429 | } 430 | if (s1 !== peg$FAILED) { 431 | s2 = peg$parseidentifier(); 432 | if (s2 !== peg$FAILED) { 433 | s3 = peg$parsespace(); 434 | if (s3 !== peg$FAILED) { 435 | s4 = []; 436 | s5 = peg$parseboolean(); 437 | if (s5 === peg$FAILED) { 438 | s5 = peg$parseexpression(); 439 | if (s5 === peg$FAILED) { 440 | s5 = peg$parsebinding(); 441 | if (s5 === peg$FAILED) { 442 | s5 = peg$parseinteger(); 443 | if (s5 === peg$FAILED) { 444 | s5 = peg$parsestring(); 445 | if (s5 === peg$FAILED) { 446 | s5 = peg$parsespace(); 447 | } 448 | } 449 | } 450 | } 451 | } 452 | while (s5 !== peg$FAILED) { 453 | s4.push(s5); 454 | s5 = peg$parseboolean(); 455 | if (s5 === peg$FAILED) { 456 | s5 = peg$parseexpression(); 457 | if (s5 === peg$FAILED) { 458 | s5 = peg$parsebinding(); 459 | if (s5 === peg$FAILED) { 460 | s5 = peg$parseinteger(); 461 | if (s5 === peg$FAILED) { 462 | s5 = peg$parsestring(); 463 | if (s5 === peg$FAILED) { 464 | s5 = peg$parsespace(); 465 | } 466 | } 467 | } 468 | } 469 | } 470 | } 471 | if (s4 !== peg$FAILED) { 472 | peg$reportedPos = s0; 473 | s1 = peg$c12(s2, s4); 474 | s0 = s1; 475 | } else { 476 | peg$currPos = s0; 477 | s0 = peg$c2; 478 | } 479 | } else { 480 | peg$currPos = s0; 481 | s0 = peg$c2; 482 | } 483 | } else { 484 | peg$currPos = s0; 485 | s0 = peg$c2; 486 | } 487 | } else { 488 | peg$currPos = s0; 489 | s0 = peg$c2; 490 | } 491 | 492 | return s0; 493 | } 494 | 495 | function peg$parsestring() { 496 | var s0, s1, s2, s3; 497 | 498 | peg$silentFails++; 499 | s0 = peg$currPos; 500 | if (input.charCodeAt(peg$currPos) === 34) { 501 | s1 = peg$c14; 502 | peg$currPos++; 503 | } else { 504 | s1 = peg$FAILED; 505 | if (peg$silentFails === 0) { peg$fail(peg$c15); } 506 | } 507 | if (s1 !== peg$FAILED) { 508 | s2 = []; 509 | s3 = peg$parseDoubleStringCharacter(); 510 | while (s3 !== peg$FAILED) { 511 | s2.push(s3); 512 | s3 = peg$parseDoubleStringCharacter(); 513 | } 514 | if (s2 !== peg$FAILED) { 515 | if (input.charCodeAt(peg$currPos) === 34) { 516 | s3 = peg$c14; 517 | peg$currPos++; 518 | } else { 519 | s3 = peg$FAILED; 520 | if (peg$silentFails === 0) { peg$fail(peg$c15); } 521 | } 522 | if (s3 !== peg$FAILED) { 523 | peg$reportedPos = s0; 524 | s1 = peg$c16(s2); 525 | s0 = s1; 526 | } else { 527 | peg$currPos = s0; 528 | s0 = peg$c2; 529 | } 530 | } else { 531 | peg$currPos = s0; 532 | s0 = peg$c2; 533 | } 534 | } else { 535 | peg$currPos = s0; 536 | s0 = peg$c2; 537 | } 538 | if (s0 === peg$FAILED) { 539 | s0 = peg$currPos; 540 | if (input.charCodeAt(peg$currPos) === 39) { 541 | s1 = peg$c17; 542 | peg$currPos++; 543 | } else { 544 | s1 = peg$FAILED; 545 | if (peg$silentFails === 0) { peg$fail(peg$c18); } 546 | } 547 | if (s1 !== peg$FAILED) { 548 | s2 = []; 549 | s3 = peg$parseSingleStringCharacter(); 550 | while (s3 !== peg$FAILED) { 551 | s2.push(s3); 552 | s3 = peg$parseSingleStringCharacter(); 553 | } 554 | if (s2 !== peg$FAILED) { 555 | if (input.charCodeAt(peg$currPos) === 39) { 556 | s3 = peg$c17; 557 | peg$currPos++; 558 | } else { 559 | s3 = peg$FAILED; 560 | if (peg$silentFails === 0) { peg$fail(peg$c18); } 561 | } 562 | if (s3 !== peg$FAILED) { 563 | peg$reportedPos = s0; 564 | s1 = peg$c16(s2); 565 | s0 = s1; 566 | } else { 567 | peg$currPos = s0; 568 | s0 = peg$c2; 569 | } 570 | } else { 571 | peg$currPos = s0; 572 | s0 = peg$c2; 573 | } 574 | } else { 575 | peg$currPos = s0; 576 | s0 = peg$c2; 577 | } 578 | } 579 | peg$silentFails--; 580 | if (s0 === peg$FAILED) { 581 | s1 = peg$FAILED; 582 | if (peg$silentFails === 0) { peg$fail(peg$c13); } 583 | } 584 | 585 | return s0; 586 | } 587 | 588 | function peg$parseDecimalDigit() { 589 | var s0; 590 | 591 | if (peg$c3.test(input.charAt(peg$currPos))) { 592 | s0 = input.charAt(peg$currPos); 593 | peg$currPos++; 594 | } else { 595 | s0 = peg$FAILED; 596 | if (peg$silentFails === 0) { peg$fail(peg$c4); } 597 | } 598 | 599 | return s0; 600 | } 601 | 602 | function peg$parseHexDigit() { 603 | var s0; 604 | 605 | if (peg$c19.test(input.charAt(peg$currPos))) { 606 | s0 = input.charAt(peg$currPos); 607 | peg$currPos++; 608 | } else { 609 | s0 = peg$FAILED; 610 | if (peg$silentFails === 0) { peg$fail(peg$c20); } 611 | } 612 | 613 | return s0; 614 | } 615 | 616 | function peg$parseDoubleStringCharacter() { 617 | var s0, s1, s2; 618 | 619 | s0 = peg$currPos; 620 | s1 = peg$currPos; 621 | peg$silentFails++; 622 | if (input.charCodeAt(peg$currPos) === 34) { 623 | s2 = peg$c14; 624 | peg$currPos++; 625 | } else { 626 | s2 = peg$FAILED; 627 | if (peg$silentFails === 0) { peg$fail(peg$c15); } 628 | } 629 | if (s2 === peg$FAILED) { 630 | if (input.charCodeAt(peg$currPos) === 92) { 631 | s2 = peg$c22; 632 | peg$currPos++; 633 | } else { 634 | s2 = peg$FAILED; 635 | if (peg$silentFails === 0) { peg$fail(peg$c23); } 636 | } 637 | } 638 | peg$silentFails--; 639 | if (s2 === peg$FAILED) { 640 | s1 = peg$c21; 641 | } else { 642 | peg$currPos = s1; 643 | s1 = peg$c2; 644 | } 645 | if (s1 !== peg$FAILED) { 646 | if (input.length > peg$currPos) { 647 | s2 = input.charAt(peg$currPos); 648 | peg$currPos++; 649 | } else { 650 | s2 = peg$FAILED; 651 | if (peg$silentFails === 0) { peg$fail(peg$c24); } 652 | } 653 | if (s2 !== peg$FAILED) { 654 | peg$reportedPos = s0; 655 | s1 = peg$c25(); 656 | s0 = s1; 657 | } else { 658 | peg$currPos = s0; 659 | s0 = peg$c2; 660 | } 661 | } else { 662 | peg$currPos = s0; 663 | s0 = peg$c2; 664 | } 665 | if (s0 === peg$FAILED) { 666 | s0 = peg$currPos; 667 | if (input.charCodeAt(peg$currPos) === 92) { 668 | s1 = peg$c22; 669 | peg$currPos++; 670 | } else { 671 | s1 = peg$FAILED; 672 | if (peg$silentFails === 0) { peg$fail(peg$c23); } 673 | } 674 | if (s1 !== peg$FAILED) { 675 | s2 = peg$parseEscapeSequence(); 676 | if (s2 !== peg$FAILED) { 677 | peg$reportedPos = s0; 678 | s1 = peg$c26(s2); 679 | s0 = s1; 680 | } else { 681 | peg$currPos = s0; 682 | s0 = peg$c2; 683 | } 684 | } else { 685 | peg$currPos = s0; 686 | s0 = peg$c2; 687 | } 688 | } 689 | 690 | return s0; 691 | } 692 | 693 | function peg$parseSingleStringCharacter() { 694 | var s0, s1, s2; 695 | 696 | s0 = peg$currPos; 697 | s1 = peg$currPos; 698 | peg$silentFails++; 699 | if (input.charCodeAt(peg$currPos) === 39) { 700 | s2 = peg$c17; 701 | peg$currPos++; 702 | } else { 703 | s2 = peg$FAILED; 704 | if (peg$silentFails === 0) { peg$fail(peg$c18); } 705 | } 706 | if (s2 === peg$FAILED) { 707 | if (input.charCodeAt(peg$currPos) === 92) { 708 | s2 = peg$c22; 709 | peg$currPos++; 710 | } else { 711 | s2 = peg$FAILED; 712 | if (peg$silentFails === 0) { peg$fail(peg$c23); } 713 | } 714 | } 715 | peg$silentFails--; 716 | if (s2 === peg$FAILED) { 717 | s1 = peg$c21; 718 | } else { 719 | peg$currPos = s1; 720 | s1 = peg$c2; 721 | } 722 | if (s1 !== peg$FAILED) { 723 | if (input.length > peg$currPos) { 724 | s2 = input.charAt(peg$currPos); 725 | peg$currPos++; 726 | } else { 727 | s2 = peg$FAILED; 728 | if (peg$silentFails === 0) { peg$fail(peg$c24); } 729 | } 730 | if (s2 !== peg$FAILED) { 731 | peg$reportedPos = s0; 732 | s1 = peg$c25(); 733 | s0 = s1; 734 | } else { 735 | peg$currPos = s0; 736 | s0 = peg$c2; 737 | } 738 | } else { 739 | peg$currPos = s0; 740 | s0 = peg$c2; 741 | } 742 | if (s0 === peg$FAILED) { 743 | s0 = peg$currPos; 744 | if (input.charCodeAt(peg$currPos) === 92) { 745 | s1 = peg$c22; 746 | peg$currPos++; 747 | } else { 748 | s1 = peg$FAILED; 749 | if (peg$silentFails === 0) { peg$fail(peg$c23); } 750 | } 751 | if (s1 !== peg$FAILED) { 752 | s2 = peg$parseEscapeSequence(); 753 | if (s2 !== peg$FAILED) { 754 | peg$reportedPos = s0; 755 | s1 = peg$c26(s2); 756 | s0 = s1; 757 | } else { 758 | peg$currPos = s0; 759 | s0 = peg$c2; 760 | } 761 | } else { 762 | peg$currPos = s0; 763 | s0 = peg$c2; 764 | } 765 | } 766 | 767 | return s0; 768 | } 769 | 770 | function peg$parseEscapeSequence() { 771 | var s0, s1, s2, s3; 772 | 773 | s0 = peg$parseCharacterEscapeSequence(); 774 | if (s0 === peg$FAILED) { 775 | s0 = peg$currPos; 776 | if (input.charCodeAt(peg$currPos) === 48) { 777 | s1 = peg$c27; 778 | peg$currPos++; 779 | } else { 780 | s1 = peg$FAILED; 781 | if (peg$silentFails === 0) { peg$fail(peg$c28); } 782 | } 783 | if (s1 !== peg$FAILED) { 784 | s2 = peg$currPos; 785 | peg$silentFails++; 786 | s3 = peg$parseDecimalDigit(); 787 | peg$silentFails--; 788 | if (s3 === peg$FAILED) { 789 | s2 = peg$c21; 790 | } else { 791 | peg$currPos = s2; 792 | s2 = peg$c2; 793 | } 794 | if (s2 !== peg$FAILED) { 795 | peg$reportedPos = s0; 796 | s1 = peg$c29(); 797 | s0 = s1; 798 | } else { 799 | peg$currPos = s0; 800 | s0 = peg$c2; 801 | } 802 | } else { 803 | peg$currPos = s0; 804 | s0 = peg$c2; 805 | } 806 | if (s0 === peg$FAILED) { 807 | s0 = peg$parseHexEscapeSequence(); 808 | if (s0 === peg$FAILED) { 809 | s0 = peg$parseUnicodeEscapeSequence(); 810 | } 811 | } 812 | } 813 | 814 | return s0; 815 | } 816 | 817 | function peg$parseCharacterEscapeSequence() { 818 | var s0; 819 | 820 | s0 = peg$parseSingleEscapeCharacter(); 821 | if (s0 === peg$FAILED) { 822 | s0 = peg$parseNonEscapeCharacter(); 823 | } 824 | 825 | return s0; 826 | } 827 | 828 | function peg$parseSingleEscapeCharacter() { 829 | var s0, s1; 830 | 831 | if (input.charCodeAt(peg$currPos) === 39) { 832 | s0 = peg$c17; 833 | peg$currPos++; 834 | } else { 835 | s0 = peg$FAILED; 836 | if (peg$silentFails === 0) { peg$fail(peg$c18); } 837 | } 838 | if (s0 === peg$FAILED) { 839 | if (input.charCodeAt(peg$currPos) === 34) { 840 | s0 = peg$c14; 841 | peg$currPos++; 842 | } else { 843 | s0 = peg$FAILED; 844 | if (peg$silentFails === 0) { peg$fail(peg$c15); } 845 | } 846 | if (s0 === peg$FAILED) { 847 | if (input.charCodeAt(peg$currPos) === 92) { 848 | s0 = peg$c22; 849 | peg$currPos++; 850 | } else { 851 | s0 = peg$FAILED; 852 | if (peg$silentFails === 0) { peg$fail(peg$c23); } 853 | } 854 | if (s0 === peg$FAILED) { 855 | s0 = peg$currPos; 856 | if (input.charCodeAt(peg$currPos) === 98) { 857 | s1 = peg$c30; 858 | peg$currPos++; 859 | } else { 860 | s1 = peg$FAILED; 861 | if (peg$silentFails === 0) { peg$fail(peg$c31); } 862 | } 863 | if (s1 !== peg$FAILED) { 864 | peg$reportedPos = s0; 865 | s1 = peg$c32(); 866 | } 867 | s0 = s1; 868 | if (s0 === peg$FAILED) { 869 | s0 = peg$currPos; 870 | if (input.charCodeAt(peg$currPos) === 102) { 871 | s1 = peg$c33; 872 | peg$currPos++; 873 | } else { 874 | s1 = peg$FAILED; 875 | if (peg$silentFails === 0) { peg$fail(peg$c34); } 876 | } 877 | if (s1 !== peg$FAILED) { 878 | peg$reportedPos = s0; 879 | s1 = peg$c35(); 880 | } 881 | s0 = s1; 882 | if (s0 === peg$FAILED) { 883 | s0 = peg$currPos; 884 | if (input.charCodeAt(peg$currPos) === 110) { 885 | s1 = peg$c36; 886 | peg$currPos++; 887 | } else { 888 | s1 = peg$FAILED; 889 | if (peg$silentFails === 0) { peg$fail(peg$c37); } 890 | } 891 | if (s1 !== peg$FAILED) { 892 | peg$reportedPos = s0; 893 | s1 = peg$c38(); 894 | } 895 | s0 = s1; 896 | if (s0 === peg$FAILED) { 897 | s0 = peg$currPos; 898 | if (input.charCodeAt(peg$currPos) === 114) { 899 | s1 = peg$c39; 900 | peg$currPos++; 901 | } else { 902 | s1 = peg$FAILED; 903 | if (peg$silentFails === 0) { peg$fail(peg$c40); } 904 | } 905 | if (s1 !== peg$FAILED) { 906 | peg$reportedPos = s0; 907 | s1 = peg$c41(); 908 | } 909 | s0 = s1; 910 | if (s0 === peg$FAILED) { 911 | s0 = peg$currPos; 912 | if (input.charCodeAt(peg$currPos) === 116) { 913 | s1 = peg$c42; 914 | peg$currPos++; 915 | } else { 916 | s1 = peg$FAILED; 917 | if (peg$silentFails === 0) { peg$fail(peg$c43); } 918 | } 919 | if (s1 !== peg$FAILED) { 920 | peg$reportedPos = s0; 921 | s1 = peg$c44(); 922 | } 923 | s0 = s1; 924 | if (s0 === peg$FAILED) { 925 | s0 = peg$currPos; 926 | if (input.charCodeAt(peg$currPos) === 118) { 927 | s1 = peg$c45; 928 | peg$currPos++; 929 | } else { 930 | s1 = peg$FAILED; 931 | if (peg$silentFails === 0) { peg$fail(peg$c46); } 932 | } 933 | if (s1 !== peg$FAILED) { 934 | peg$reportedPos = s0; 935 | s1 = peg$c47(); 936 | } 937 | s0 = s1; 938 | } 939 | } 940 | } 941 | } 942 | } 943 | } 944 | } 945 | } 946 | 947 | return s0; 948 | } 949 | 950 | function peg$parseNonEscapeCharacter() { 951 | var s0, s1, s2; 952 | 953 | s0 = peg$currPos; 954 | s1 = peg$currPos; 955 | peg$silentFails++; 956 | s2 = peg$parseEscapeCharacter(); 957 | peg$silentFails--; 958 | if (s2 === peg$FAILED) { 959 | s1 = peg$c21; 960 | } else { 961 | peg$currPos = s1; 962 | s1 = peg$c2; 963 | } 964 | if (s1 !== peg$FAILED) { 965 | if (input.length > peg$currPos) { 966 | s2 = input.charAt(peg$currPos); 967 | peg$currPos++; 968 | } else { 969 | s2 = peg$FAILED; 970 | if (peg$silentFails === 0) { peg$fail(peg$c24); } 971 | } 972 | if (s2 !== peg$FAILED) { 973 | peg$reportedPos = s0; 974 | s1 = peg$c25(); 975 | s0 = s1; 976 | } else { 977 | peg$currPos = s0; 978 | s0 = peg$c2; 979 | } 980 | } else { 981 | peg$currPos = s0; 982 | s0 = peg$c2; 983 | } 984 | 985 | return s0; 986 | } 987 | 988 | function peg$parseEscapeCharacter() { 989 | var s0; 990 | 991 | s0 = peg$parseSingleEscapeCharacter(); 992 | if (s0 === peg$FAILED) { 993 | s0 = peg$parseDecimalDigit(); 994 | if (s0 === peg$FAILED) { 995 | if (input.charCodeAt(peg$currPos) === 120) { 996 | s0 = peg$c48; 997 | peg$currPos++; 998 | } else { 999 | s0 = peg$FAILED; 1000 | if (peg$silentFails === 0) { peg$fail(peg$c49); } 1001 | } 1002 | if (s0 === peg$FAILED) { 1003 | if (input.charCodeAt(peg$currPos) === 117) { 1004 | s0 = peg$c50; 1005 | peg$currPos++; 1006 | } else { 1007 | s0 = peg$FAILED; 1008 | if (peg$silentFails === 0) { peg$fail(peg$c51); } 1009 | } 1010 | } 1011 | } 1012 | } 1013 | 1014 | return s0; 1015 | } 1016 | 1017 | function peg$parseHexEscapeSequence() { 1018 | var s0, s1, s2, s3, s4, s5; 1019 | 1020 | s0 = peg$currPos; 1021 | if (input.charCodeAt(peg$currPos) === 120) { 1022 | s1 = peg$c48; 1023 | peg$currPos++; 1024 | } else { 1025 | s1 = peg$FAILED; 1026 | if (peg$silentFails === 0) { peg$fail(peg$c49); } 1027 | } 1028 | if (s1 !== peg$FAILED) { 1029 | s2 = peg$currPos; 1030 | s3 = peg$currPos; 1031 | s4 = peg$parseHexDigit(); 1032 | if (s4 !== peg$FAILED) { 1033 | s5 = peg$parseHexDigit(); 1034 | if (s5 !== peg$FAILED) { 1035 | s4 = [s4, s5]; 1036 | s3 = s4; 1037 | } else { 1038 | peg$currPos = s3; 1039 | s3 = peg$c2; 1040 | } 1041 | } else { 1042 | peg$currPos = s3; 1043 | s3 = peg$c2; 1044 | } 1045 | if (s3 !== peg$FAILED) { 1046 | s3 = input.substring(s2, peg$currPos); 1047 | } 1048 | s2 = s3; 1049 | if (s2 !== peg$FAILED) { 1050 | peg$reportedPos = s0; 1051 | s1 = peg$c52(s2); 1052 | s0 = s1; 1053 | } else { 1054 | peg$currPos = s0; 1055 | s0 = peg$c2; 1056 | } 1057 | } else { 1058 | peg$currPos = s0; 1059 | s0 = peg$c2; 1060 | } 1061 | 1062 | return s0; 1063 | } 1064 | 1065 | function peg$parseUnicodeEscapeSequence() { 1066 | var s0, s1, s2, s3, s4, s5, s6, s7; 1067 | 1068 | s0 = peg$currPos; 1069 | if (input.charCodeAt(peg$currPos) === 117) { 1070 | s1 = peg$c50; 1071 | peg$currPos++; 1072 | } else { 1073 | s1 = peg$FAILED; 1074 | if (peg$silentFails === 0) { peg$fail(peg$c51); } 1075 | } 1076 | if (s1 !== peg$FAILED) { 1077 | s2 = peg$currPos; 1078 | s3 = peg$currPos; 1079 | s4 = peg$parseHexDigit(); 1080 | if (s4 !== peg$FAILED) { 1081 | s5 = peg$parseHexDigit(); 1082 | if (s5 !== peg$FAILED) { 1083 | s6 = peg$parseHexDigit(); 1084 | if (s6 !== peg$FAILED) { 1085 | s7 = peg$parseHexDigit(); 1086 | if (s7 !== peg$FAILED) { 1087 | s4 = [s4, s5, s6, s7]; 1088 | s3 = s4; 1089 | } else { 1090 | peg$currPos = s3; 1091 | s3 = peg$c2; 1092 | } 1093 | } else { 1094 | peg$currPos = s3; 1095 | s3 = peg$c2; 1096 | } 1097 | } else { 1098 | peg$currPos = s3; 1099 | s3 = peg$c2; 1100 | } 1101 | } else { 1102 | peg$currPos = s3; 1103 | s3 = peg$c2; 1104 | } 1105 | if (s3 !== peg$FAILED) { 1106 | s3 = input.substring(s2, peg$currPos); 1107 | } 1108 | s2 = s3; 1109 | if (s2 !== peg$FAILED) { 1110 | peg$reportedPos = s0; 1111 | s1 = peg$c52(s2); 1112 | s0 = s1; 1113 | } else { 1114 | peg$currPos = s0; 1115 | s0 = peg$c2; 1116 | } 1117 | } else { 1118 | peg$currPos = s0; 1119 | s0 = peg$c2; 1120 | } 1121 | 1122 | return s0; 1123 | } 1124 | 1125 | function peg$parseboolean() { 1126 | var s0, s1; 1127 | 1128 | s0 = peg$currPos; 1129 | if (input.substr(peg$currPos, 4) === peg$c53) { 1130 | s1 = peg$c53; 1131 | peg$currPos += 4; 1132 | } else { 1133 | s1 = peg$FAILED; 1134 | if (peg$silentFails === 0) { peg$fail(peg$c54); } 1135 | } 1136 | if (s1 === peg$FAILED) { 1137 | if (input.substr(peg$currPos, 5) === peg$c55) { 1138 | s1 = peg$c55; 1139 | peg$currPos += 5; 1140 | } else { 1141 | s1 = peg$FAILED; 1142 | if (peg$silentFails === 0) { peg$fail(peg$c56); } 1143 | } 1144 | } 1145 | if (s1 !== peg$FAILED) { 1146 | peg$reportedPos = s0; 1147 | s1 = peg$c57(s1); 1148 | } 1149 | s0 = s1; 1150 | 1151 | return s0; 1152 | } 1153 | 1154 | function peg$parsebindingpart() { 1155 | var s0, s1, s2, s3; 1156 | 1157 | s0 = peg$currPos; 1158 | if (peg$c58.test(input.charAt(peg$currPos))) { 1159 | s1 = input.charAt(peg$currPos); 1160 | peg$currPos++; 1161 | } else { 1162 | s1 = peg$FAILED; 1163 | if (peg$silentFails === 0) { peg$fail(peg$c59); } 1164 | } 1165 | if (s1 !== peg$FAILED) { 1166 | s2 = []; 1167 | if (peg$c60.test(input.charAt(peg$currPos))) { 1168 | s3 = input.charAt(peg$currPos); 1169 | peg$currPos++; 1170 | } else { 1171 | s3 = peg$FAILED; 1172 | if (peg$silentFails === 0) { peg$fail(peg$c61); } 1173 | } 1174 | while (s3 !== peg$FAILED) { 1175 | s2.push(s3); 1176 | if (peg$c60.test(input.charAt(peg$currPos))) { 1177 | s3 = input.charAt(peg$currPos); 1178 | peg$currPos++; 1179 | } else { 1180 | s3 = peg$FAILED; 1181 | if (peg$silentFails === 0) { peg$fail(peg$c61); } 1182 | } 1183 | } 1184 | if (s2 !== peg$FAILED) { 1185 | s1 = [s1, s2]; 1186 | s0 = s1; 1187 | } else { 1188 | peg$currPos = s0; 1189 | s0 = peg$c2; 1190 | } 1191 | } else { 1192 | peg$currPos = s0; 1193 | s0 = peg$c2; 1194 | } 1195 | 1196 | return s0; 1197 | } 1198 | 1199 | function peg$parsebinding() { 1200 | var s0, s1, s2, s3, s4, s5, s6; 1201 | 1202 | s0 = peg$currPos; 1203 | s1 = peg$currPos; 1204 | s2 = peg$parsebindingpart(); 1205 | if (s2 !== peg$FAILED) { 1206 | s3 = []; 1207 | s4 = peg$currPos; 1208 | if (input.charCodeAt(peg$currPos) === 46) { 1209 | s5 = peg$c62; 1210 | peg$currPos++; 1211 | } else { 1212 | s5 = peg$FAILED; 1213 | if (peg$silentFails === 0) { peg$fail(peg$c63); } 1214 | } 1215 | if (s5 !== peg$FAILED) { 1216 | s6 = peg$parsebindingpart(); 1217 | if (s6 !== peg$FAILED) { 1218 | s5 = [s5, s6]; 1219 | s4 = s5; 1220 | } else { 1221 | peg$currPos = s4; 1222 | s4 = peg$c2; 1223 | } 1224 | } else { 1225 | peg$currPos = s4; 1226 | s4 = peg$c2; 1227 | } 1228 | while (s4 !== peg$FAILED) { 1229 | s3.push(s4); 1230 | s4 = peg$currPos; 1231 | if (input.charCodeAt(peg$currPos) === 46) { 1232 | s5 = peg$c62; 1233 | peg$currPos++; 1234 | } else { 1235 | s5 = peg$FAILED; 1236 | if (peg$silentFails === 0) { peg$fail(peg$c63); } 1237 | } 1238 | if (s5 !== peg$FAILED) { 1239 | s6 = peg$parsebindingpart(); 1240 | if (s6 !== peg$FAILED) { 1241 | s5 = [s5, s6]; 1242 | s4 = s5; 1243 | } else { 1244 | peg$currPos = s4; 1245 | s4 = peg$c2; 1246 | } 1247 | } else { 1248 | peg$currPos = s4; 1249 | s4 = peg$c2; 1250 | } 1251 | } 1252 | if (s3 !== peg$FAILED) { 1253 | s2 = [s2, s3]; 1254 | s1 = s2; 1255 | } else { 1256 | peg$currPos = s1; 1257 | s1 = peg$c2; 1258 | } 1259 | } else { 1260 | peg$currPos = s1; 1261 | s1 = peg$c2; 1262 | } 1263 | if (s1 !== peg$FAILED) { 1264 | peg$reportedPos = s0; 1265 | s1 = peg$c64(s1); 1266 | } 1267 | s0 = s1; 1268 | 1269 | return s0; 1270 | } 1271 | 1272 | function peg$parseidentifier() { 1273 | var s0, s1, s2, s3, s4; 1274 | 1275 | s0 = peg$currPos; 1276 | s1 = peg$currPos; 1277 | if (peg$c65.test(input.charAt(peg$currPos))) { 1278 | s2 = input.charAt(peg$currPos); 1279 | peg$currPos++; 1280 | } else { 1281 | s2 = peg$FAILED; 1282 | if (peg$silentFails === 0) { peg$fail(peg$c66); } 1283 | } 1284 | if (s2 !== peg$FAILED) { 1285 | s3 = []; 1286 | if (peg$c67.test(input.charAt(peg$currPos))) { 1287 | s4 = input.charAt(peg$currPos); 1288 | peg$currPos++; 1289 | } else { 1290 | s4 = peg$FAILED; 1291 | if (peg$silentFails === 0) { peg$fail(peg$c68); } 1292 | } 1293 | while (s4 !== peg$FAILED) { 1294 | s3.push(s4); 1295 | if (peg$c67.test(input.charAt(peg$currPos))) { 1296 | s4 = input.charAt(peg$currPos); 1297 | peg$currPos++; 1298 | } else { 1299 | s4 = peg$FAILED; 1300 | if (peg$silentFails === 0) { peg$fail(peg$c68); } 1301 | } 1302 | } 1303 | if (s3 !== peg$FAILED) { 1304 | s2 = [s2, s3]; 1305 | s1 = s2; 1306 | } else { 1307 | peg$currPos = s1; 1308 | s1 = peg$c2; 1309 | } 1310 | } else { 1311 | peg$currPos = s1; 1312 | s1 = peg$c2; 1313 | } 1314 | if (s1 !== peg$FAILED) { 1315 | peg$reportedPos = s0; 1316 | s1 = peg$c69(s1); 1317 | } 1318 | s0 = s1; 1319 | 1320 | return s0; 1321 | } 1322 | 1323 | function peg$parsespace() { 1324 | var s0, s1, s2; 1325 | 1326 | s0 = peg$currPos; 1327 | s1 = []; 1328 | if (peg$c70.test(input.charAt(peg$currPos))) { 1329 | s2 = input.charAt(peg$currPos); 1330 | peg$currPos++; 1331 | } else { 1332 | s2 = peg$FAILED; 1333 | if (peg$silentFails === 0) { peg$fail(peg$c71); } 1334 | } 1335 | if (s2 !== peg$FAILED) { 1336 | while (s2 !== peg$FAILED) { 1337 | s1.push(s2); 1338 | if (peg$c70.test(input.charAt(peg$currPos))) { 1339 | s2 = input.charAt(peg$currPos); 1340 | peg$currPos++; 1341 | } else { 1342 | s2 = peg$FAILED; 1343 | if (peg$silentFails === 0) { peg$fail(peg$c71); } 1344 | } 1345 | } 1346 | } else { 1347 | s1 = peg$c2; 1348 | } 1349 | if (s1 !== peg$FAILED) { 1350 | peg$reportedPos = s0; 1351 | s1 = peg$c72(); 1352 | } 1353 | s0 = s1; 1354 | 1355 | return s0; 1356 | } 1357 | 1358 | function peg$parsecomment() { 1359 | var s0, s1, s2, s3; 1360 | 1361 | s0 = peg$currPos; 1362 | if (input.charCodeAt(peg$currPos) === 35) { 1363 | s1 = peg$c73; 1364 | peg$currPos++; 1365 | } else { 1366 | s1 = peg$FAILED; 1367 | if (peg$silentFails === 0) { peg$fail(peg$c74); } 1368 | } 1369 | if (s1 !== peg$FAILED) { 1370 | s2 = []; 1371 | if (input.length > peg$currPos) { 1372 | s3 = input.charAt(peg$currPos); 1373 | peg$currPos++; 1374 | } else { 1375 | s3 = peg$FAILED; 1376 | if (peg$silentFails === 0) { peg$fail(peg$c24); } 1377 | } 1378 | while (s3 !== peg$FAILED) { 1379 | s2.push(s3); 1380 | if (input.length > peg$currPos) { 1381 | s3 = input.charAt(peg$currPos); 1382 | peg$currPos++; 1383 | } else { 1384 | s3 = peg$FAILED; 1385 | if (peg$silentFails === 0) { peg$fail(peg$c24); } 1386 | } 1387 | } 1388 | if (s2 !== peg$FAILED) { 1389 | s1 = [s1, s2]; 1390 | s0 = s1; 1391 | } else { 1392 | peg$currPos = s0; 1393 | s0 = peg$c2; 1394 | } 1395 | } else { 1396 | peg$currPos = s0; 1397 | s0 = peg$c2; 1398 | } 1399 | 1400 | return s0; 1401 | } 1402 | 1403 | 1404 | function flatten(arr) { 1405 | var res = [], item; 1406 | for (var i in arr) { 1407 | item = arr[i]; 1408 | if (Array.isArray(item)) { 1409 | res = res.concat(flatten(item)); 1410 | } else { 1411 | res.push(item); 1412 | } 1413 | } 1414 | return res; 1415 | } 1416 | 1417 | 1418 | peg$result = peg$startRuleFunction(); 1419 | 1420 | if (peg$result !== peg$FAILED && peg$currPos === input.length) { 1421 | return peg$result; 1422 | } else { 1423 | if (peg$result !== peg$FAILED && peg$currPos < input.length) { 1424 | peg$fail({ type: "end", description: "end of input" }); 1425 | } 1426 | 1427 | throw peg$buildException(null, peg$maxFailExpected, peg$maxFailPos); 1428 | } 1429 | } 1430 | 1431 | return { 1432 | SyntaxError: SyntaxError, 1433 | parse: parse 1434 | }; 1435 | })(); 1436 | -------------------------------------------------------------------------------- /lib/sexp-parser.pegjs: -------------------------------------------------------------------------------- 1 | { 2 | function flatten(arr) { 3 | var res = [], item; 4 | for (var i in arr) { 5 | item = arr[i]; 6 | if (Array.isArray(item)) { 7 | res = res.concat(flatten(item)); 8 | } else { 9 | res.push(item); 10 | } 11 | } 12 | return res; 13 | } 14 | } 15 | 16 | start 17 | = expression 18 | 19 | integer "integer" 20 | = digits:[0-9]+ { 21 | 22 | return { 23 | type: 'Literal', 24 | value: parseInt(digits.join(""), 10) 25 | } 26 | } 27 | 28 | expression 29 | = (space? '(' body:body ')' space?) { 30 | 31 | return { 32 | type: 'Expression', 33 | name: body.identifier, 34 | arguments: body.args 35 | }; 36 | } 37 | 38 | body 39 | = ( space? id:identifier space args:(boolean / expression / binding / integer / string / space )* ) { 40 | 41 | return { 42 | identifier: id, 43 | args: args.filter(function(a) { return (a != ""); }) 44 | }; 45 | } 46 | 47 | 48 | //float 49 | // = float:(('+' / '-')? [0-9]+ (('.' [0-9]+) / ('e' [0-9]+))) { 50 | // 51 | // return float; 52 | //} 53 | 54 | 55 | // from https://github.com/dmajda/pegjs/blob/master/examples/javascript.pegjs#L297 56 | string "string" 57 | = '"' chars:DoubleStringCharacter* '"' { 58 | return { type: "Literal", value: chars.join("") }; 59 | } 60 | / "'" chars:SingleStringCharacter* "'" { 61 | return { type: "Literal", value: chars.join("") }; 62 | } 63 | 64 | DecimalDigit = [0-9] 65 | HexDigit = [0-9a-f]i 66 | 67 | DoubleStringCharacter 68 | = !('"' / "\\") . { return text(); } 69 | / "\\" sequence:EscapeSequence { return sequence; } 70 | 71 | SingleStringCharacter 72 | = !("'" / "\\") . { return text(); } 73 | / "\\" sequence:EscapeSequence { return sequence; } 74 | 75 | EscapeSequence 76 | = CharacterEscapeSequence 77 | / "0" !DecimalDigit { return "\0"; } 78 | / HexEscapeSequence 79 | / UnicodeEscapeSequence 80 | 81 | CharacterEscapeSequence 82 | = SingleEscapeCharacter 83 | / NonEscapeCharacter 84 | 85 | SingleEscapeCharacter 86 | = "'" 87 | / '"' 88 | / "\\" 89 | / "b" { return "\b"; } 90 | / "f" { return "\f"; } 91 | / "n" { return "\n"; } 92 | / "r" { return "\r"; } 93 | / "t" { return "\t"; } 94 | / "v" { return "\x0B"; } // IE does not recognize "\v". 95 | 96 | NonEscapeCharacter 97 | = !(EscapeCharacter) . { return text(); } 98 | 99 | EscapeCharacter 100 | = SingleEscapeCharacter 101 | / DecimalDigit 102 | / "x" 103 | / "u" 104 | 105 | HexEscapeSequence 106 | = "x" digits:$(HexDigit HexDigit) { 107 | return String.fromCharCode(parseInt(digits, 16)); 108 | } 109 | 110 | UnicodeEscapeSequence 111 | = "u" digits:$(HexDigit HexDigit HexDigit HexDigit) { 112 | return String.fromCharCode(parseInt(digits, 16)); 113 | } 114 | 115 | boolean 116 | = bool:( 'true' / 'false') { 117 | 118 | if (bool === 'true') return { type: 'Literal', value: true }; 119 | if (bool === 'false') return { type: 'Literal', value: false }; 120 | } 121 | 122 | bindingpart 123 | = ([a-zA-Z\=\*:] [a-zA-Z0-9_\=\*-:]*) 124 | 125 | binding 126 | = binding:(bindingpart ( '.' bindingpart)*) { 127 | 128 | 129 | return { 130 | type: 'Binding', 131 | keypath: flatten(binding).join('') 132 | }; 133 | } 134 | 135 | 136 | identifier 137 | = identifier:([a-zA-Z\=\*:\/\+\<\>\-\%] [a-zA-Z0-9_\=\*-:.]*) { 138 | 139 | return flatten(identifier).join(''); 140 | } 141 | 142 | space 143 | = [\t\r\n ]+ { return ""; } 144 | 145 | comment 146 | = "#" .* 147 | -------------------------------------------------------------------------------- /lib/split-and-keep-splitter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Given a string: "hello {name}, how are you this {dayOfWeek}" 3 | * 4 | * And a regex to split it on: /{[^}]*}/g 5 | * 6 | * And two functions, withMatch and withSpace 7 | * 8 | * This function will return an array of parts like so: 9 | * => [ 10 | * withSpace("hello "), 11 | * withMatch("{name}") 12 | * withSpace(", how are you this "), 13 | * withMatch("{dayOfWeek}") 14 | * ] 15 | */ 16 | 17 | module.exports = function (string, regex, withMatch, withSpace) { 18 | withSpace = withSpace || function (s) { return s; }; 19 | withMatch = withMatch || function (s) { return s; }; 20 | 21 | regex = new RegExp(regex); 22 | 23 | var result = []; 24 | var pos = 0; 25 | var match; 26 | var prior; 27 | var substr; 28 | var transformed; 29 | 30 | while(match = regex.exec(string)) { 31 | if (match.index > pos) { 32 | substr = string.substr(pos, match.index - pos); 33 | if (substr !== '') { 34 | transformed = withSpace(substr); 35 | if (transformed) result.push(transformed); 36 | } 37 | } 38 | 39 | if (string.substr(match.index, match[0].length).trim() !== '') { 40 | substr = string.substr(match.index, match[0].length); 41 | if (substr !== '') { 42 | transformed = withMatch(substr); 43 | if (transformed) result.push(transformed); 44 | } 45 | } 46 | pos = match.index + match[0].length; 47 | } 48 | 49 | if (pos < string.length) { 50 | substr = string.substr(pos, string.length - pos); 51 | if (substr !== '') result.push(withSpace(substr)); 52 | } 53 | return result; 54 | }; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "domthing", 3 | "description": "A simple, fast & safe, mustache/handlebars like templating engine with data-binding hooks built in.", 4 | "version": "0.4.0", 5 | "bin": "bin/domthing", 6 | "bugs": { 7 | "url": "https://github.com/latentflip/domthing/issues" 8 | }, 9 | "dependencies": { 10 | "async": "^0.9.0", 11 | "backbone-events-standalone": "^0.2.1", 12 | "commander": "^2.2.0", 13 | "domhandler": "^2.2.0", 14 | "glob": "^4.0.2", 15 | "htmlparser2": "^3.7.2", 16 | "key-tree-store": "^0.1.1", 17 | "raf": "^2.0.1" 18 | }, 19 | "devDependencies": { 20 | "browserify": "^4.1.7", 21 | "deval": "^0.1.1", 22 | "difflet": "^0.2.6", 23 | "faucet": "0.0.1", 24 | "jsdom": "^0.10.6", 25 | "multiline": "^0.3.4", 26 | "pegjs": "^0.8.0", 27 | "tape": "^2.13.1", 28 | "testem": "^0.6.23" 29 | }, 30 | "directories": { 31 | "test": "test" 32 | }, 33 | "homepage": "https://github.com/latentflip/domthing", 34 | "license": "ISC", 35 | "main": "domthing.js", 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/latentflip/domthing" 39 | }, 40 | "scripts": { 41 | "test": "faucet && testem ci", 42 | "start": "make && beefy demo/demo.js" 43 | }, 44 | "browser": "./runtime" 45 | } 46 | -------------------------------------------------------------------------------- /runtime.js: -------------------------------------------------------------------------------- 1 | var Template = require('./lib/runtime/template'); 2 | var hooks = require('./lib/runtime/hooks'); 3 | var helpers = require('./lib/runtime/helpers'); 4 | var expressions = require('./lib/runtime/expressions'); 5 | 6 | module.exports = { 7 | Template: Template, 8 | hooks: hooks, 9 | 10 | registerHelper: helpers.registerHelper, 11 | registerExpression: expressions.registerExpression 12 | }; 13 | -------------------------------------------------------------------------------- /test/client/compiler-test.js: -------------------------------------------------------------------------------- 1 | var compiler = require('../../lib/compiler'); 2 | var parser = require('../../lib/parser'); 3 | var test = require('tape'); 4 | var s = require('multiline'); 5 | var compile = compiler.compile; 6 | var deval = require('deval'); 7 | //var jsdom = require('jsdom'); 8 | var builtinHelpers = require('../../lib/runtime/helpers'); 9 | var fs = require('fs'); 10 | 11 | var visible = function (el) { 12 | return el; 13 | }; 14 | var wait = function (fn) { 15 | setTimeout(fn, 25); 16 | }; 17 | 18 | var parsePrecompileAndAppend = require('../helpers/parsePrecompileAndAppend'); 19 | 20 | test('compiles simple nodes', function (t) { 21 | parsePrecompileAndAppend('', function (err, window) { 22 | t.equal(window.document.querySelectorAll('a').length, 1); 23 | t.end(); 24 | }); 25 | }); 26 | 27 | test('compiles attributes', function (t) { 28 | var template = ''; 29 | parsePrecompileAndAppend(template, function (err, window) { 30 | var el = window.document.querySelector('#output a'); 31 | console.log('!' + el.outerHTML + '!'); 32 | t.equal(el.getAttribute('href'), 'foo'); 33 | t.equal(el.getAttribute('class'), 'bar'); 34 | t.equal(el.getAttribute('id'), 'baz'); 35 | t.end(); 36 | }); 37 | }); 38 | 39 | test('compiles multiline attributes', function (t) { 40 | var template = s(function () {/* 41 | 45 | */}); 46 | 47 | parsePrecompileAndAppend(template, function (err, window) { 48 | var el = window.document.querySelector('a'); 49 | t.equal(el.style.color, 'red'); 50 | t.equal(el.style.border, '1px solid red'); 51 | t.equal(el.getAttribute('href'), 'foo'); 52 | t.end(); 53 | }); 54 | }); 55 | 56 | test('compiles textNodes', function (t) { 57 | parsePrecompileAndAppend('foo', function (err, window) { 58 | var el = window.document.querySelector('a'); 59 | t.equal(el.innerHTML, 'foo'); 60 | t.end(); 61 | }); 62 | }); 63 | 64 | test('compiles raw html', function (t) { 65 | var tmpl = s(function () {/* 66 |
67 | 68 | {{{ html }}} 69 | 70 |
71 | */}); 72 | 73 | var context = { 74 | html: "" 75 | }; 76 | 77 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 78 | var qs = window.document.querySelector.bind(window.document); 79 | var div = qs('.container'); 80 | t.equal(div.children[0].getAttribute('class'), 'before'); 81 | t.equal(div.children[1].outerHTML, ''); 82 | t.equal(div.children[2].outerHTML, ''); 83 | t.equal(div.children[3].getAttribute('class'), 'after'); 84 | 85 | window.templateUnderTest.update('html', ''); 86 | wait(function () { 87 | var div = qs('.container'); 88 | t.equal(div.children[0].getAttribute('class'), 'before'); 89 | t.equal(div.children[1].getAttribute('class'), 'after'); 90 | t.notOk(qs('a')); 91 | t.notOk(qs('b')); 92 | 93 | window.templateUnderTest.update('html', ''); 94 | wait(function () { 95 | var div = qs('.container'); 96 | t.equal(div.children[0].getAttribute('class'), 'before'); 97 | t.equal(div.children[1].outerHTML, ''); 98 | t.equal(div.children[2].getAttribute('class'), 'after'); 99 | t.notOk(qs('a')); 100 | t.notOk(qs('b')); 101 | t.end(); 102 | }); 103 | }); 104 | }); 105 | }); 106 | 107 | test('compiles textNode with simple bindings', function (t) { 108 | var template = '{{foo}}'; 109 | var context = { foo: 'hello' }; 110 | 111 | parsePrecompileAndAppend(template, context, builtinHelpers, function (err, window) { 112 | var el = window.document.querySelector('a'); 113 | t.equal(el.innerHTML, 'hello'); 114 | window.templateUnderTest.update('foo', 'goodbye'); 115 | wait(function () { 116 | t.equal(el.innerHTML, 'goodbye'); 117 | t.end(); 118 | }); 119 | }); 120 | }); 121 | 122 | test('compiles attributes with bindings', function (t) { 123 | var template = 'a link'; 124 | var context = { url: 'http://google.com' }; 125 | 126 | parsePrecompileAndAppend(template, context, builtinHelpers, function (err, window) { 127 | var el = window.document.querySelector('a'); 128 | t.equal(el.getAttribute('href'), 'http://google.com/'); 129 | window.templateUnderTest.update('url', 'http://yahoo.com'); 130 | wait(function () { 131 | t.equal(el.getAttribute('href'), 'http://yahoo.com/'); 132 | t.end(); 133 | }); 134 | }); 135 | }); 136 | 137 | 138 | test('compiles expressions', function (t) { 139 | var template = 'foo {{bar}} baz'; 140 | parsePrecompileAndAppend(template, { bar: 'hello!' }, builtinHelpers, function (err, window) { 141 | var el = window.document.querySelector('a'); 142 | t.equal(el.innerHTML, 'foo hello! baz'); 143 | window.templateUnderTest.update('bar', 'goodbye!'); 144 | wait(function () { 145 | t.equal(el.innerHTML, 'foo goodbye! baz'); 146 | t.end(); 147 | }); 148 | }); 149 | }); 150 | 151 | test('compiles siblings', function (t) { 152 | var tmpl = '
foobar
'; 153 | parsePrecompileAndAppend(tmpl, function (err, window) { 154 | var el1 = window.document.querySelector('#one'); 155 | t.equal(el1.innerHTML, 'foo'); 156 | 157 | var el2 = window.document.querySelector('#two'); 158 | t.equal(el2.innerHTML, 'bar'); 159 | 160 | t.end(); 161 | }); 162 | }); 163 | 164 | test('compiles nested', function (t) { 165 | var tmpl = 'foo'; 166 | parsePrecompileAndAppend(tmpl, function (err, window) { 167 | var span = window.document.querySelector('a span'); 168 | t.equal(span.innerHTML, 'foo'); 169 | t.end(); 170 | }); 171 | }); 172 | 173 | 174 | test('updates magical class bindings', function (t) { 175 | var tmpl = ''; 176 | var context = { foo: 'hello', bar: 'there' }; 177 | 178 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 179 | var el = window.document.querySelector('a'); 180 | t.equal(el.getAttribute('class'), 'static hello there'); 181 | 182 | window.templateUnderTest.update('foo', 'goodbye'); 183 | wait(function () { 184 | t.equal(el.getAttribute('class'), 'static goodbye there'); 185 | t.end(); 186 | }); 187 | }); 188 | }); 189 | 190 | test('spaces things properly', function (t) { 191 | var tmpl = s(function () {/* 192 |
  • 193 | hello 194 | {{foo}} 195 | there 196 |
  • 197 | */}); 198 | 199 | t.plan(2); 200 | 201 | parsePrecompileAndAppend(tmpl, {foo: 'binding'}, builtinHelpers, function (err, window) { 202 | t.equal(window.document.querySelector('li').textContent, ' hello binding there '); 203 | }); 204 | 205 | var tmpl2 = "
  • hello{{foo}}there
  • "; 206 | parsePrecompileAndAppend(tmpl2, {foo: 'binding'}, builtinHelpers, function (err, window) { 207 | t.equal(window.document.querySelector('li').textContent, 'hellobindingthere'); 208 | }); 209 | }); 210 | 211 | test('compiles simple if statements', function (t) { 212 | var tmpl = s(function () {/* 213 |
    214 | {{#if foo}} 215 | 216 | {{#else}} 217 | 218 | {{/if}} 219 |
    220 | */}); 221 | var context = { foo: true }; 222 | 223 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 224 | t.ok(visible(window.document.querySelector('a'))); 225 | t.notOk(visible(window.document.querySelector('b'))); 226 | 227 | window.templateUnderTest.update('foo', false); 228 | 229 | wait(function () { 230 | t.notOk(visible(window.document.querySelector('a'))); 231 | t.ok(visible(window.document.querySelector('b'))); 232 | t.end(); 233 | }); 234 | }); 235 | }); 236 | 237 | test('compiles complex if statements', function (t) { 238 | var tmpl = s(function () {/* 239 |
    240 |
    
    241 |             {{#if foo}}
    242 |                 
    243 |                 
    244 |             {{#else}}
    245 |                 
    246 |                 
    247 |             {{/if}}
    248 |             
    249 |         
    250 | */}); 251 | var context = { foo: true }; 252 | 253 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 254 | var qs = window.document.querySelector.bind(window.document); 255 | var div = qs('.if-wrap'); 256 | t.equal(div.children[0].tagName, 'PRE'); 257 | t.equal(div.children[1].tagName, 'A'); 258 | t.equal(div.children[2].tagName, 'B'); 259 | t.equal(div.children[3].tagName, 'POST'); 260 | 261 | window.templateUnderTest.update('foo', false); 262 | wait(function () { 263 | var div = qs('.if-wrap'); 264 | console.log(div.outerHTML); 265 | t.equal(div.children[0].tagName, 'PRE'); 266 | t.equal(div.children[1].tagName, 'C'); 267 | t.equal(div.children[2].tagName, 'D'); 268 | t.equal(div.children[3].tagName, 'POST'); 269 | t.end(); 270 | }); 271 | }); 272 | }); 273 | 274 | test('compiles if statements without wrappers', function (t) { 275 | var tmpl = s(function () {/* 276 | 283 | */}); 284 | 285 | var context = { foo: true }; 286 | 287 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 288 | t.ok(visible(window.document.querySelector('ul > li.yes'))); 289 | t.notOk(visible(window.document.querySelector('ul > li.no'))); 290 | 291 | window.templateUnderTest.update('foo', false); 292 | wait(function () { 293 | t.notOk(visible(window.document.querySelector('ul > li.yes'))); 294 | t.ok(visible(window.document.querySelector('ul > li.no'))); 295 | t.end(); 296 | }); 297 | }); 298 | }); 299 | 300 | test('compiles unless statements without wrappers', function (t) { 301 | var tmpl = s(function () {/* 302 | 309 | */}); 310 | 311 | var context = { foo: true }; 312 | 313 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 314 | t.notOk(visible(window.document.querySelector('ul > li.yes'))); 315 | t.ok(visible(window.document.querySelector('ul > li.no'))); 316 | 317 | window.templateUnderTest.update('foo', false); 318 | wait(function () { 319 | t.ok(visible(window.document.querySelector('ul > li.yes'))); 320 | t.notOk(visible(window.document.querySelector('ul > li.no'))); 321 | t.end(); 322 | }); 323 | }); 324 | }); 325 | 326 | test('if statements dont die if only one sided', function (t) { 327 | var tmpl = s(function () {/* 328 | 336 | */}); 337 | 338 | var context = { foo: true }; 339 | 340 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 341 | t.notOk(visible(window.document.querySelector('ul > li.no'))); 342 | 343 | window.templateUnderTest.update('foo', false); 344 | wait(function () { 345 | t.notOk(visible(window.document.querySelector('ul > li.yes'))); 346 | t.ok(visible(window.document.querySelector('ul > li.no'))); 347 | t.end(); 348 | }); 349 | }); 350 | }); 351 | 352 | test('compiles sub-expressions', function (t) { 353 | var tmpl = s(function () {/* 354 | 362 | */}); 363 | 364 | var context = { foo: true }; 365 | 366 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 367 | t.notOk(visible(window.document.querySelector('ul > li.no'))); 368 | 369 | window.templateUnderTest.update('foo', false); 370 | 371 | wait(function () { 372 | t.notOk(visible(window.document.querySelector('ul > li.yes'))); 373 | t.ok(visible(window.document.querySelector('ul > li.no'))); 374 | t.end(); 375 | }); 376 | }); 377 | }); 378 | 379 | 380 | test('compiles booleans', function (t) { 381 | var tmpl = s(function () {/* 382 |
    383 | {{#if (not false)}} 384 | 385 | {{#else}} 386 | 387 | {{/if}} 388 |
    389 | */}); 390 | 391 | var context = {}; 392 | 393 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 394 | t.ok(visible(window.document.querySelector('a'))); 395 | t.notOk(visible(window.document.querySelector('b'))); 396 | t.end(); 397 | }); 398 | }); 399 | 400 | test('handles boolean attributes:', function (t) { 401 | var tmpl = ""; 402 | var context = { 403 | model: { 404 | active: false 405 | } 406 | }; 407 | 408 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 409 | var el = window.document.querySelector('input'); 410 | 411 | t.notOk(el.checked, 'Initially unchecked'); 412 | 413 | el.update('model.active', true); 414 | wait(function () { 415 | t.ok(el.checked, 'Check with a boolean'); 416 | 417 | el.update('model.active', undefined); 418 | wait(function () { 419 | t.notOk(el.checked); 420 | 421 | el.update('model.active', 'farce'); 422 | wait(function () { 423 | t.ok(el.checked); 424 | 425 | t.end(); 426 | }); 427 | }); 428 | }); 429 | }); 430 | }); 431 | 432 | test('compiles this:', function (t) { 433 | var tmpl = s(function () {/* 434 |
    435 | {{model.src}} 436 | {{#if model.src }} 437 | 438 | 439 | {{#else }} 440 | No image, upload one. 441 | {{/if}} 442 | 443 |
    444 | */}); 445 | 446 | var context = { 447 | model: { 448 | src: undefined 449 | } 450 | }; 451 | 452 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 453 | var qs = window.document.querySelector.bind(window.document); 454 | 455 | //No source 456 | t.equal(qs('a').innerHTML, ''); 457 | t.notOk(qs('img')); 458 | t.ok(qs('b')); 459 | 460 | var f = window.templateUnderTest; 461 | //Set source 462 | window.templateUnderTest.update('model.src', 'http://foo.com/a'); 463 | 464 | wait(function () { 465 | t.ok(qs('img')); 466 | t.equal(qs('img').getAttribute('src'), 'http://foo.com/a'); 467 | t.equal(qs('a').innerHTML, 'http://foo.com/a'); 468 | t.notOk(qs('b')); 469 | 470 | window.templateUnderTest.update('model.src', 'http://bar.com/a'); 471 | wait(function () { 472 | t.ok(qs('img')); 473 | t.equal(qs('img').getAttribute('src'), 'http://bar.com/a'); 474 | t.equal(qs('a').innerHTML, 'http://bar.com/a'); 475 | t.notOk(qs('b')); 476 | 477 | window.templateUnderTest.update('model.src', undefined); 478 | 479 | wait(function () { 480 | t.equal(qs('a').innerHTML, ''); 481 | t.notOk(qs('img')); 482 | t.ok(qs('b')); 483 | 484 | t.end(); 485 | }); 486 | }); 487 | }); 488 | 489 | return; 490 | }); 491 | }); 492 | 493 | 494 | test('compile expressions', function (t) { 495 | var tmpl = s(function () {/* 496 |
    497 | {{ (* foo 2) }} 498 | {{ (/ foo 2) }} 499 | {{ (+ foo 2) }} 500 | {{ (- foo 2) }} 501 | {{ (% foo 2) }} 502 | 503 | {{ (if (< foo 2) "true" "false") }} 504 | {{ (if (> foo 2) "true" "false") }} 505 | 506 | {{ (if (leq foo 4) "true" "false") }} 507 | {{ (if (geq foo 4) "true" "false") }} 508 | 509 | {{ (if (not foo) "true" "false") }} 510 | 511 |
    512 | */}); 513 | 514 | var context = { 515 | foo: 4 516 | }; 517 | 518 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 519 | var qs = window.document.querySelector.bind(window.document); 520 | 521 | //No source 522 | t.equal(qs('.mult').innerHTML, '8'); 523 | t.equal(qs('.div').innerHTML, '2'); 524 | t.equal(qs('.add').innerHTML, '6'); 525 | t.equal(qs('.sub').innerHTML, '2'); 526 | t.equal(qs('.mod').innerHTML, '0'); 527 | 528 | t.equal(qs('.less').innerHTML, 'false'); 529 | t.equal(qs('.more').innerHTML, 'true'); 530 | 531 | t.equal(qs('.less-eq').innerHTML, 'true'); 532 | t.equal(qs('.more-eq').innerHTML, 'true'); 533 | 534 | t.equal(qs('.not').innerHTML, 'false'); 535 | 536 | t.end(); 537 | }); 538 | }); 539 | -------------------------------------------------------------------------------- /test/client/dom-bindings-parity.js: -------------------------------------------------------------------------------- 1 | var compiler = require('../../lib/compiler'); 2 | var parser = require('../../lib/parser'); 3 | var test = require('tape'); 4 | var s = require('multiline'); 5 | var compile = compiler.compile; 6 | var builtinHelpers = require('../../lib/runtime/helpers'); 7 | var fs = require('fs'); 8 | var parsePrecompileAndAppend = require('../helpers/parsePrecompileAndAppend'); 9 | var async = require('async'); 10 | 11 | 12 | test('text bindings', function (t) { 13 | var tmpl = '{{ foo }}'; 14 | 15 | testBinding(tmpl) 16 | .context({ foo: 'hello' }).assert(innerHTMLEqual('hello')) 17 | .update('foo', '').assert(innerHTMLEqual('')) 18 | .update('foo', 'there').assert(innerHTMLEqual('there')) 19 | .update('foo', NaN).assert(innerHTMLEqual('')) 20 | .update('foo', undefined).assert(innerHTMLEqual('')) 21 | .run(t); 22 | }); 23 | 24 | test('class', function (t) { 25 | var tmpl = ''; 26 | 27 | testBinding(tmpl) 28 | .context({ foo: 'active' }).assert(hasClasses('bar', 'active', 'baz')) 29 | .update('foo', '').assert(hasClasses('bar', 'baz')) 30 | .update('foo', 'blah').assert(hasClasses('bar', 'blah', 'baz')) 31 | .update('foo', undefined).assert(hasClasses('bar', 'baz')) 32 | .update('foo', NaN).assert(hasClasses('bar', 'baz')) 33 | .update('foo', null).assert(hasClasses('bar', 'baz')) 34 | .run(t); 35 | }); 36 | 37 | test('set attribute', function (t) { 38 | var hasId = function (id) { 39 | return function (el) { 40 | var elId = el.id; 41 | if (elId !== id) console.log(elId, '!==', id); 42 | return el.getAttribute('id') === id; 43 | }; 44 | }; 45 | testBinding('') 46 | .context({ foo: 'my-span' }).assert(hasId('my-span')) 47 | .update('foo', 'bar').assert(hasId('bar')) 48 | .update('foo', NaN).assert(hasId('')) 49 | .update('foo', undefined).assert(hasId('')) 50 | .update('foo', null).assert(hasId('')) 51 | .run(t); 52 | }); 53 | 54 | test('booleanClass', function (t) { 55 | var tmpl = ""; 56 | 57 | testBinding(tmpl) 58 | .context({ foo: true, no: "b" }).assert(hasClasses('bar', 'a', 'baz')) 59 | .update('foo', false).assert(hasClasses('bar', 'b', 'baz')) 60 | .update('foo', 'yes').assert(hasClasses('bar', 'a', 'baz')) 61 | .update('foo', null).assert(hasClasses('bar', 'b', 'baz')) 62 | //Can change bound expression argument 63 | .update('no', 'altered').assert(hasClasses('bar', 'altered', 'baz')) 64 | .run(t); 65 | }); 66 | 67 | 68 | test('booleanAttribute - initially true', function (t) { 69 | var tmpl = ""; 70 | var isChecked = function (el) { return el.checked; }; 71 | var isNotChecked = function (el) { return !el.checked; }; 72 | 73 | testBinding(tmpl) 74 | .context({ foo: true }).assert(isChecked) 75 | .update('foo', false).assert(isNotChecked) 76 | .update('foo', 'checked').assert(isChecked) 77 | //FIXME should '' be truthy or falsy in this context? 78 | .update('foo', '').assert(isNotChecked) 79 | .update('foo', null).assert(isNotChecked) 80 | .update('foo', undefined).assert(isNotChecked) 81 | .update('foo', NaN).assert(isNotChecked) 82 | .run(t); 83 | }); 84 | 85 | test('booleanAttribute - initially false', function (t) { 86 | var tmpl = ""; 87 | var isChecked = function (el) { return el.checked; }; 88 | var isNotChecked = function (el) { return !el.checked; }; 89 | 90 | testBinding(tmpl) 91 | .context({ foo: false }).assert(isNotChecked) 92 | .update('foo', true).assert(isChecked) 93 | .update('foo', false).assert(isNotChecked) 94 | .update('foo', 'checked').assert(isChecked) 95 | //FIXME should '' be truthy or falsy in this context? 96 | .update('foo', '').assert(isNotChecked) 97 | .update('foo', null).assert(isNotChecked) 98 | .update('foo', undefined).assert(isNotChecked) 99 | .update('foo', NaN).assert(isNotChecked) 100 | .run(t); 101 | }); 102 | 103 | 104 | test('switch (equivalent)', function (t) { 105 | var tmpl = s(function () {/* 106 | 107 | {{#if (=== foo "a")}} a content {{/if}} 108 | {{#if (=== foo "b")}} b content {{/if}} 109 | {{#if (=== foo "c")}} c content {{/if}} 110 | 111 | */}); 112 | 113 | testBinding(tmpl) 114 | .context({ foo: "a" }).assert(textContentEqual('a content', true)) 115 | .update('foo', 'b').assert(textContentEqual('b content', true)) 116 | .update('foo', 'c').assert(textContentEqual('c content', true)) 117 | .update('foo', 'b').assert(textContentEqual('b content', true)) 118 | .update('foo', 'a').assert(textContentEqual('a content', true)) 119 | .run(t); 120 | }); 121 | 122 | /// helpers 123 | 124 | function wait(fn) { 125 | setTimeout(fn, 25); 126 | } 127 | 128 | function hasClasses() { 129 | var expected = [].slice.call(arguments); 130 | 131 | return function (el) { 132 | var classes = el.getAttribute('class') || ''; 133 | classes = classes.split(' ').filter(function (s) { return s !== ''; }); 134 | expected = JSON.stringify(expected); 135 | var actual = JSON.stringify(classes); 136 | 137 | if (actual !== expected) { console.log(actual, '!=', expected); } 138 | return actual === expected; 139 | }; 140 | } 141 | 142 | function testBinding(tmpl) { 143 | return { 144 | _template: tmpl, 145 | _steps: [], 146 | _context: {}, 147 | context: function (context) { 148 | this._context = context; 149 | return this; 150 | }, 151 | assert: function (cb) { 152 | this._steps.push(['assert', cb]); 153 | return this; 154 | }, 155 | update: function (key, val) { 156 | this._steps.push(['update', key, val]); 157 | return this; 158 | }, 159 | log: function () { 160 | this._steps.push(['log']); 161 | return this; 162 | }, 163 | run: function (t) { 164 | parsePrecompileAndAppend(this._template, this._context, builtinHelpers, function (err, window) { 165 | t.notOk(err); 166 | var tut = window.templateUnderTest; 167 | var el = window.document.querySelector('#output > *'); 168 | 169 | async.eachSeries(this._steps, function (step, next) { 170 | if (step[0] === 'assert') { 171 | wait(function () { 172 | t.ok(step[1](el)); 173 | next(); 174 | }); 175 | } else if (step[0] === 'update') { 176 | tut.update(step[1], step[2]); 177 | next(); 178 | } else if (step[0] === 'log') { 179 | console.log(window._console); 180 | next(); 181 | } 182 | }, function (err) { 183 | t.notOk(err); 184 | t.end(); 185 | }); 186 | }.bind(this)); 187 | } 188 | }; 189 | } 190 | 191 | function innerHTMLEqual(str) { 192 | return function (el) { 193 | if (el.innerHTML !== str) console.log(el.innerHTML, '!==', str); 194 | return el.innerHTML === str; 195 | }; 196 | } 197 | 198 | function textContentEqual(expect, doTrim) { 199 | return function (el) { 200 | var actual = el.textContent; 201 | if (doTrim) actual = actual.trim(); 202 | if (actual !== expect) console.log(actual, '!==', expect); 203 | return actual === expect; 204 | }; 205 | } 206 | -------------------------------------------------------------------------------- /test/client/security-test.js: -------------------------------------------------------------------------------- 1 | /*jshint scripturl:true*/ 2 | 3 | var compiler = require('../../lib/compiler'); 4 | var parser = require('../../lib/parser'); 5 | var test = require('tape'); 6 | var s = require('multiline'); 7 | var compile = compiler.compile; 8 | var builtinHelpers = require('../../lib/runtime/helpers'); 9 | var fs = require('fs'); 10 | var parsePrecompileAndAppend = require('../helpers/parsePrecompileAndAppend'); 11 | var async = require('async'); 12 | 13 | test('html is escaped with double-curlies', function (t) { 14 | var tmpl = '
    {{ foo }}
    '; 15 | var context = { 16 | foo: "" 17 | }; 18 | 19 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 20 | var el = window.document.querySelector('#output'); 21 | t.notOk(el.querySelector('b')); 22 | t.end(); 23 | }); 24 | }); 25 | 26 | test('html is unescaped with triple-curlies', function (t) { 27 | var tmpl = '
    {{{ foo }}}
    '; 28 | var context = { 29 | foo: "" 30 | }; 31 | 32 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 33 | var el = window.document.querySelector('#output'); 34 | t.ok(el.querySelector('b')); 35 | t.end(); 36 | }); 37 | }); 38 | 39 | test('href is escaped with double-curlies', function (t) { 40 | var tmpl = ''; 41 | var context = { 42 | // done like this because jshint doesnt like it 43 | foo: 'javascript' + ':' + 'alert(1)' 44 | }; 45 | 46 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 47 | var el = window.document.querySelector('a'); 48 | t.equal(el.getAttribute('href'), 'unsafe:javascript:alert(1)'); 49 | t.end(); 50 | }); 51 | }); 52 | 53 | test('href is unescaped with triple-curlies', function (t) { 54 | var tmpl = ''; 55 | var context = { 56 | // done like this because jshint doesnt like it 57 | foo: 'javascript' + ':' + 'alert(1)' 58 | }; 59 | 60 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 61 | var el = window.document.querySelector('a'); 62 | t.equal(el.getAttribute('href'), 'javascript:alert(1)'); 63 | t.end(); 64 | }); 65 | }); 66 | 67 | test('src is escaped with double-curlies', function (t) { 68 | var tmpl = ''; 69 | var context = { 70 | // done like this because jshint doesnt like it 71 | foo: 'javascript' + ':' + 'alert(1)' 72 | }; 73 | 74 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 75 | var el = window.document.querySelector('img'); 76 | t.equal(el.getAttribute('src'), 'unsafe:javascript:alert(1)'); 77 | t.end(); 78 | }); 79 | }); 80 | 81 | test('src is unescaped with double-curlies', function (t) { 82 | var tmpl = ''; 83 | var context = { 84 | // done like this because jshint doesnt like it 85 | foo: 'javascript' + ':' + 'alert(1)' 86 | }; 87 | 88 | parsePrecompileAndAppend(tmpl, context, builtinHelpers, function (err, window) { 89 | var el = window.document.querySelector('img'); 90 | t.equal(el.getAttribute('src'), 'javascript:alert(1)'); 91 | t.end(); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/file-writer-test.js: -------------------------------------------------------------------------------- 1 | var FileWriter = require('../lib/file-writer'); 2 | 3 | var test = require('tape'); 4 | 5 | test('works correctly', function (t) { 6 | var fw = new FileWriter(); 7 | 8 | fw.write('function (a, b) {'); 9 | fw.indent(); 10 | fw.write(['console.log(a);', 'console.log(b);']); 11 | fw.outdent(); 12 | fw.write('}'); 13 | 14 | var expected = [ 15 | 'function (a, b) {', 16 | ' console.log(a);', 17 | ' console.log(b);', 18 | '}' 19 | ]; 20 | 21 | t.equal(fw.toString(), expected.join('\n')); 22 | t.end(); 23 | }); 24 | -------------------------------------------------------------------------------- /test/helpers/parsePrecompileAndAppend.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var compiler = require('../../lib/compiler'); 3 | var compile = compiler.compile; 4 | var parser = require('../../lib/parser'); 5 | //var jsdom = require('jsdom'); 6 | var deval = require('deval'); 7 | 8 | 9 | var precompileAndAppend = function (ast, context, helpers, cb) { 10 | if (!cb && !helpers && typeof context === 'function') { 11 | cb = context; 12 | context = {}; 13 | } 14 | var strFn = compile(ast); 15 | 16 | window.RUNTIME = require('../../runtime'); 17 | 18 | eval("window.TEMPLATE = " + strFn); 19 | 20 | var output = document.querySelector('#output'); 21 | 22 | if (!output) { 23 | output = document.createElement('div'); 24 | output.setAttribute('id', 'output'); 25 | document.body.appendChild(output); 26 | } else { 27 | output.innerHTML = ''; 28 | } 29 | 30 | var fragment = window.TEMPLATE(context, require('../../runtime')); 31 | output.appendChild(fragment); 32 | window.templateUnderTest = fragment; 33 | 34 | cb(null, window); 35 | 36 | //useful for debug 37 | //console.log(JSON.stringify(ast, null, 2)); 38 | //console.log(strFn); 39 | 40 | //var window = deval(function () { 41 | // window._console = []; 42 | // window.console = { 43 | // log: function () { 44 | // window._console.push([].slice.call(arguments)); 45 | // } 46 | // }; 47 | // window.requestAnimationFrame = function (cb) { setTimeout(cb, 0); }; 48 | //}); 49 | 50 | //var inject = deval(function (strFn, context) { 51 | // var tmpl = $strFn$; 52 | // var fragment = tmpl($context$, window.RUNTIME); 53 | // document.querySelector('#output').appendChild(fragment); 54 | // window.templateUnderTest = fragment; 55 | //}, strFn, JSON.stringify(context)); 56 | 57 | //jsdom.env({ 58 | // html: '
    ', 59 | // src: [ 60 | // window + ';' + fs.readFileSync(__dirname + '/../../runtime.bundle.js').toString() + ';' + inject 61 | // ], 62 | // done: function (err, window) { 63 | // if (err) { 64 | // console.log('JSDOM Errors:', err); 65 | // console.log(err[0].data.error); 66 | // throw err; 67 | // } 68 | // cb(null, window); 69 | // } 70 | //}); 71 | }; 72 | 73 | var parsePrecompileAndAppend = function (template, context, helpers, cb) { 74 | parser(template, function (err, ast) { 75 | precompileAndAppend(ast, context, helpers, cb); 76 | }); 77 | }; 78 | 79 | module.exports = parsePrecompileAndAppend; 80 | -------------------------------------------------------------------------------- /test/is-boolean-attribute-test.js: -------------------------------------------------------------------------------- 1 | var isBooleanAttribute = require('../lib/is-boolean-attribute'); 2 | 3 | var test = require('tape'); 4 | 5 | test('returns correctly', function (t) { 6 | t.ok(isBooleanAttribute('checked')); 7 | t.notOk(isBooleanAttribute('href')); 8 | 9 | t.end(); 10 | }); 11 | -------------------------------------------------------------------------------- /test/parser-test.js: -------------------------------------------------------------------------------- 1 | var parse = require('../lib/parser'); 2 | var test = require('tape'); 3 | var AST = require('../lib/AST'); 4 | 5 | var s = require('multiline'); 6 | 7 | test.Test.prototype.astEqual = function (tmpl, expected, noplan) { 8 | var t = this; 9 | if (!noplan) { 10 | t.plan(2); 11 | } 12 | parse(tmpl, function (err, ast) { 13 | //console.log(JSON.stringify(expected, null, 2)); 14 | //console.log(JSON.stringify(ast, null, 2)); 15 | t.notOk(err); 16 | t.deepEqual(ast, expected); 17 | }); 18 | }; 19 | 20 | test('parses a template', function (t) { 21 | t.astEqual('', AST.Template()); 22 | }); 23 | 24 | 25 | test('parses simple tags', function (t) { 26 | t.astEqual('', AST.Template([ 27 | AST.Element('a') 28 | ])); 29 | }); 30 | 31 | test('parses spaces', function (t) { 32 | var template = [ 33 | "
    ", 34 | " message: {{ model.payload }} ", 35 | "
    " 36 | ].join('\n'); 37 | 38 | t.astEqual(template, AST.Template([ 39 | AST.Element('div', [ 40 | AST.TextNode(AST.Literal(' ')), 41 | AST.Element('strong'), 42 | AST.TextNode(AST.Literal(' message: ')), 43 | AST.TextNode(AST.Binding('model.payload')), 44 | AST.TextNode(AST.Literal(' ')), 45 | ]), 46 | ])); 47 | }); 48 | 49 | test('parses this properly', function (t) { 50 | var tmpl = [ 51 | "
  • ", 52 | " hello", 53 | " {{foo}}", 54 | " there", 55 | "
  • ", 56 | ].join('\n'); 57 | 58 | t.astEqual(tmpl, AST.Template([ 59 | AST.Element('li', [ 60 | AST.TextNode(AST.Literal(' ')), 61 | 62 | AST.Element('a', [ 63 | AST.TextNode(AST.Literal('hello')) 64 | ]), 65 | AST.TextNode(AST.Literal(' ')), 66 | AST.TextNode(AST.Binding('foo')), 67 | AST.TextNode(AST.Literal(' ')), 68 | AST.Element('a', [ 69 | AST.TextNode(AST.Literal('there')) 70 | ]), 71 | 72 | AST.TextNode(AST.Literal(' ')), 73 | ]), 74 | ])); 75 | }); 76 | 77 | test('parses attributes', function (t) { 78 | t.astEqual("", AST.Template([ 79 | AST.Element('a', { 80 | href: AST.Literal('foo'), 81 | class: AST.Literal('bar'), 82 | id: AST.Literal('baz'), 83 | }) 84 | ])); 85 | }); 86 | 87 | test('parses raw html bindings', function (t) { 88 | t.astEqual('
    {{{ foo }}}
    ', AST.Template([ 89 | AST.Element('div', [ 90 | AST.DocumentFragment(AST.Binding('foo')) 91 | ]) 92 | ])); 93 | }); 94 | 95 | test('parses raw expressions in attributes', function (t) { 96 | t.astEqual("", AST.Template([ 97 | AST.Element('a', { 98 | href: AST.Expression('safe', [ 99 | AST.Expression('if', [ 100 | AST.Binding('model.foo'), 101 | AST.Literal('a'), 102 | AST.Literal('b') 103 | ]) 104 | ]) 105 | }) 106 | ])); 107 | }); 108 | 109 | test('parses expressions in attributes', function (t) { 110 | t.astEqual("", AST.Template([ 111 | AST.Element('a', { 112 | href: AST.Binding('foo'), 113 | id: AST.Binding('baz'), 114 | class: AST.Literal('bar') 115 | }) 116 | ])); 117 | }); 118 | 119 | test('parses concat expressions in attributes', function (t) { 120 | t.astEqual("", AST.Template([ 121 | AST.Element('a', { 122 | class: AST.Expression('concat', [ 123 | AST.Literal('foo '), 124 | AST.Binding('foo'), 125 | AST.Literal(' bar '), 126 | AST.Binding('bar'), 127 | AST.Literal(' '), 128 | AST.Binding('baz') 129 | ]) 130 | }) 131 | ])); 132 | }); 133 | 134 | test('parses text nodes', function (t) { 135 | t.astEqual("foo", AST.Template([ 136 | AST.Element('a', [ 137 | AST.TextNode( AST.Literal('foo') ) 138 | ]) 139 | ])); 140 | }); 141 | 142 | test('parses simple bindings in text nodes', function (t) { 143 | t.astEqual('foo {{bar}} baz', AST.Template([ 144 | AST.Element('a', [ 145 | AST.TextNode( AST.Literal('foo ') ), 146 | AST.TextNode( AST.Binding('bar') ), 147 | AST.TextNode( AST.Literal(' baz') ), 148 | ]) 149 | ])); 150 | }); 151 | 152 | test('parses child nodes', function (t) { 153 | t.astEqual('foo', AST.Template([ 154 | AST.Element('a', [ 155 | AST.Element('em', [ 156 | AST.TextNode( AST.Literal('foo') ) 157 | ]) 158 | ]) 159 | ])); 160 | }); 161 | 162 | test('parses siblings', function (t) { 163 | t.astEqual('foobarbaz', AST.Template([ 164 | AST.Element('a', [ 165 | AST.Element('em', [ 166 | AST.TextNode( AST.Literal('foo') ) 167 | ]), 168 | AST.Element('strong', [ 169 | AST.TextNode( AST.Literal('bar') ) 170 | ]), 171 | AST.TextNode( AST.Literal('baz') ) 172 | ]) 173 | ])); 174 | }); 175 | 176 | 177 | test('parses if statements', function (t) { 178 | t.astEqual([ 179 | "{{#if foo }}", 180 | " ", 181 | "{{/if}}", 182 | ].join('\n'), AST.Template([ 183 | AST.BlockStatement('if', AST.Binding('foo'), [ 184 | AST.TextNode(AST.Literal(' ')), 185 | AST.Element('a'), 186 | AST.Element('b'), 187 | AST.TextNode(AST.Literal(' ')), 188 | ]), 189 | ])); 190 | }); 191 | 192 | test('parses if/else statements', function (t) { 193 | var tmpl = [ 194 | "{{#if foo }}", 195 | " ", 196 | "{{#else }}", 197 | " ", 198 | "{{/if}}", 199 | ].join('\n'); 200 | 201 | t.astEqual(tmpl, AST.Template([ 202 | AST.BlockStatement('if', AST.Binding('foo'), [ 203 | AST.TextNode(AST.Literal(' ')), 204 | AST.Element('a'), 205 | AST.TextNode(AST.Literal(' ')), 206 | ], [ 207 | AST.TextNode(AST.Literal(' ')), 208 | AST.Element('b'), 209 | AST.TextNode(AST.Literal(' ')), 210 | ]) 211 | ])); 212 | }); 213 | 214 | test('handles blocks in text', function (t) { 215 | var tmpl = [ 216 | "

    ", 217 | " {{#if foo }}", 218 | " hello", 219 | " {{#else }}", 220 | " goodbye", 221 | " {{/if}}", 222 | "

    ", 223 | ].join('\n'); 224 | 225 | t.astEqual(tmpl, AST.Template([ 226 | AST.Element('p', [ 227 | AST.TextNode(AST.Literal(' ')), 228 | AST.BlockStatement('if', AST.Binding('foo'), [ 229 | AST.TextNode( AST.Literal(' hello ') ) 230 | ], [ 231 | AST.TextNode( AST.Literal(' goodbye ') ) 232 | ]), 233 | AST.TextNode(AST.Literal(' ')), 234 | ]) 235 | ])); 236 | }); 237 | 238 | test('parses nested if/else statements', function (t) { 239 | var tmpl = [ 240 | "{{#if foo }}", 241 | "
    ", 242 | " {{#if bar }}", 243 | " ", 244 | " {{#else }}", 245 | " ", 246 | " {{/if}}", 247 | "
    ", 248 | "{{#else }}", 249 | " ", 250 | "{{/if}}", 251 | ].join('\n'); 252 | 253 | t.astEqual(tmpl, AST.Template([ 254 | AST.BlockStatement('if', AST.Binding('foo'), [ 255 | AST.TextNode(AST.Literal(' ')), 256 | AST.Element('div', [ 257 | AST.TextNode(AST.Literal(' ')), 258 | AST.BlockStatement('if', AST.Binding('bar'), [ 259 | AST.TextNode(AST.Literal(' ')), 260 | AST.Element('a'), 261 | AST.TextNode(AST.Literal(' ')), 262 | ], [ 263 | AST.TextNode(AST.Literal(' ')), 264 | AST.Element('c'), 265 | AST.TextNode(AST.Literal(' ')), 266 | ]), 267 | AST.TextNode(AST.Literal(' ')), 268 | ]), 269 | AST.TextNode(AST.Literal(' ')), 270 | ], [ 271 | AST.TextNode(AST.Literal(' ')), 272 | AST.Element('b'), 273 | AST.TextNode(AST.Literal(' ')), 274 | ]) 275 | ])); 276 | }); 277 | 278 | test('parses unless statements', function (t) { 279 | var tmpl = [ 280 | "{{#unless foo }}", 281 | " ", 282 | "{{#else }}", 283 | " ", 284 | "{{/if}}", 285 | ].join('\n'); 286 | 287 | t.astEqual(tmpl, AST.Template([ 288 | AST.BlockStatement('unless', AST.Binding('foo'), [ 289 | AST.TextNode(AST.Literal(' ')), 290 | AST.Element('a'), 291 | AST.TextNode(AST.Literal(' ')), 292 | ], [ 293 | AST.TextNode(AST.Literal(' ')), 294 | AST.Element('b'), 295 | AST.TextNode(AST.Literal(' ')), 296 | ]) 297 | ])); 298 | }); 299 | 300 | test('parses sub-expressions', function (t) { 301 | var tmpl = [ 302 | "{{#if (not foo)}}", 303 | " ", 304 | "{{/if}}", 305 | ].join('\n'); 306 | 307 | t.astEqual(tmpl, AST.Template([ 308 | AST.BlockStatement( 309 | 'if', 310 | AST.Expression('not', [AST.Binding('foo')]), 311 | [ 312 | AST.TextNode(AST.Literal(' ')), 313 | AST.Element('a'), 314 | AST.TextNode(AST.Literal(' ')), 315 | ] 316 | ) 317 | ])); 318 | }); 319 | 320 | test('parses expressions in bindings', function (t) { 321 | var tmpl = ""; 322 | 323 | t.astEqual(tmpl, AST.Template([ 324 | AST.Element('span', { 325 | class: AST.Expression('sw', [AST.Binding('foo'), AST.Literal("bar"), AST.Binding("baz")]) 326 | }) 327 | ])); 328 | }); 329 | 330 | test('parses expressions in text nodes', function (t) { 331 | var tmpl = '{{ (if foo "bar" baz) }}'; 332 | 333 | t.astEqual(tmpl, AST.Template([ 334 | AST.Element('span', [ 335 | AST.TextNode( 336 | AST.Expression('if', [AST.Binding('foo'), AST.Literal("bar"), AST.Binding("baz")]) 337 | ) 338 | ]) 339 | ])); 340 | }); 341 | 342 | test('parses log expressions in bindings', function (t) { 343 | var tmpl = ""; 344 | 345 | t.astEqual(tmpl, AST.Template([ 346 | AST.Element('span', { 347 | class: AST.Expression('log', [AST.Binding('foo.bar')]) 348 | }) 349 | ])); 350 | }); 351 | 352 | test('handles hyphens somehow in bindings', function (t) { 353 | var tmpl = [ 354 | '
  • ' 355 | ].join('\n'); 356 | 357 | t.astEqual(tmpl, AST.Template([ 358 | AST.Element('li', [ 359 | AST.Element('input', { 360 | 'data-grid': AST.Binding('data-grid'), 361 | type: AST.Literal('checkbox') 362 | }) 363 | ]) 364 | ])); 365 | }); 366 | 367 | test('parses bindings without quotes', function (t) { 368 | t.plan(6); 369 | 370 | t.astEqual('', AST.Template([ 371 | AST.Element('a', { 372 | href: AST.Binding('foo') 373 | }) 374 | ]), true); 375 | 376 | t.astEqual('', AST.Template([ 377 | AST.Element('a', { 378 | href: AST.Expression('foo', [ 379 | AST.Literal('foo'), 380 | AST.Binding('bar') 381 | ]) 382 | }) 383 | ]), true); 384 | 385 | t.astEqual('{{foo}}', AST.Template([ 386 | AST.Element('a', { 387 | href: AST.Literal('/thing'), 388 | class: AST.Expression('if', [ 389 | AST.Expression('eq', [ 390 | AST.Binding('aString'), 391 | AST.Literal('HELLO"') 392 | ]), 393 | AST.Literal('active'), 394 | AST.Literal('foo'), 395 | AST.Binding('bar') 396 | ]), 397 | bar: AST.Literal("baz") 398 | }, [ 399 | AST.TextNode(AST.Binding('foo')) 400 | ]) 401 | ]), true); 402 | }); 403 | -------------------------------------------------------------------------------- /test/sexp-parser-test.js: -------------------------------------------------------------------------------- 1 | var parse = require('../lib/sexp-parser').parse; 2 | var test = require('tape'); 3 | var AST = require('../lib/ast'); 4 | 5 | test('simple functions', function (t) { 6 | t.deepEqual( 7 | parse('(concat "foo" "bar")'), 8 | AST.Expression('concat', [ 9 | AST.Literal('foo'), 10 | AST.Literal('bar') 11 | ]) 12 | ); 13 | 14 | t.deepEqual( 15 | parse("(concat 'foo' 'bar')"), 16 | AST.Expression('concat', [ 17 | AST.Literal('foo'), 18 | AST.Literal('bar') 19 | ]) 20 | ); 21 | 22 | t.deepEqual( 23 | parse('(concat "fo\'o" "ba\\"r")'), 24 | AST.Expression('concat', [ 25 | AST.Literal('fo\'o'), 26 | AST.Literal('ba"r') 27 | ]) 28 | ); 29 | 30 | t.deepEqual( 31 | parse("(concat 'fo\\'o' 'ba\"r')"), 32 | AST.Expression('concat', [ 33 | AST.Literal('fo\'o'), 34 | AST.Literal('ba"r') 35 | ]) 36 | ); 37 | 38 | t.deepEqual( 39 | parse('(sw foo "bar" baz)'), 40 | AST.Expression('sw', [ 41 | AST.Binding('foo'), 42 | AST.Literal('bar'), 43 | AST.Binding('baz') 44 | ]) 45 | ); 46 | 47 | t.deepEqual( 48 | parse('(not true false)'), 49 | AST.Expression('not', [ 50 | AST.Literal(true), 51 | AST.Literal(false) 52 | ]) 53 | ); 54 | 55 | t.deepEqual( 56 | parse('(concat "foo" bar)'), 57 | AST.Expression('concat', [ 58 | AST.Literal('foo'), 59 | AST.Binding('bar') 60 | ]) 61 | ); 62 | 63 | t.deepEqual( 64 | parse('( concat "foo" bar )'), 65 | AST.Expression('concat', [ 66 | AST.Literal('foo'), 67 | AST.Binding('bar') 68 | ]) 69 | ); 70 | 71 | t.deepEqual( 72 | parse('(concat "foo" bar.baz.bux)'), 73 | AST.Expression('concat', [ 74 | AST.Literal('foo'), 75 | AST.Binding('bar.baz.bux') 76 | ]) 77 | ); 78 | 79 | t.deepEqual( 80 | parse('(concat "foo" (not foo) (toStr (mult 2 foo.bar)))'), 81 | AST.Expression('concat', [ 82 | AST.Literal('foo'), 83 | AST.Expression('not', [ 84 | AST.Binding('foo') 85 | ]), 86 | AST.Expression('toStr', [ 87 | AST.Expression('mult', [ 88 | AST.Literal(2), 89 | AST.Binding('foo.bar') 90 | ]) 91 | ]) 92 | ]) 93 | ); 94 | 95 | t.end(); 96 | }); 97 | -------------------------------------------------------------------------------- /test/split-and-keep-splitter-test.js: -------------------------------------------------------------------------------- 1 | var splitter = require('../lib/split-and-keep-splitter'); 2 | var test = require('tape'); 3 | 4 | var regex = /{[^}]*}/g; 5 | 6 | test('splits and keeps the bits', function (t) { 7 | var parts = splitter("foo {bar} baz {bux} qux", regex); 8 | 9 | t.deepEqual(parts, ['foo ', '{bar}', ' baz ', '{bux}', ' qux']); 10 | t.end(); 11 | }); 12 | 13 | 14 | test('works with neighbouring things', function (t) { 15 | var parts = splitter("foo {bar} {bux}{qux}", regex); 16 | 17 | t.deepEqual(parts, ['foo ', '{bar}', ' ', '{bux}', '{qux}']); 18 | t.end(); 19 | }); 20 | 21 | test('splits even at the start', function (t) { 22 | var parts = splitter("{bar} baz {bux}", regex); 23 | 24 | t.deepEqual(parts, ['{bar}', ' baz ', '{bux}']); 25 | t.end(); 26 | }); 27 | 28 | test('it calls first function on matches and second on spaces', function (t) { 29 | var questionIt = function (s) { 30 | return '??' + s + '??'; 31 | }; 32 | 33 | var exclaimIt = function (s) { 34 | return '!!' + s + '!!'; 35 | }; 36 | 37 | var parts = splitter("foo {bar} baz {bux} qux", regex, exclaimIt, questionIt); 38 | 39 | t.deepEqual(parts, ['??foo ??', '!!{bar}!!', '?? baz ??', '!!{bux}!!', '?? qux??']); 40 | t.end(); 41 | }); 42 | 43 | test('it splits this properly', function (t) { 44 | var regex =/{{*([^}]*)}}/g; 45 | var withStache = function (s) { return '{}'; }; 46 | var withSpace = function (s) { return 's'; }; 47 | 48 | var parts = splitter(' {{foo}} ', regex, withStache, withSpace); 49 | t.deepEqual(parts, ['s', '{}', 's']); 50 | t.end(); 51 | }); 52 | -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "tap", 3 | "src_files": [ 4 | "lib/**/*.js", 5 | "test/**/*.js" 6 | ], 7 | "serve_files": [ 8 | "bundle.js" 9 | ], 10 | "before_tests": "browserify test/client/* -o bundle.js", 11 | "after_tests": "rm bundle.js", 12 | "launch_in_dev": ["Chrome", "Firefox"], 13 | "launch_in_ci": ["Firefox"] 14 | } 15 | --------------------------------------------------------------------------------