├── .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 |
6 | - age: {{me.profile.age}}
7 |
- height: {{me.profile.height}}
8 |
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 = '';
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 |
277 | {{#if foo}}
278 | - Hi!
279 | {{#else}}
280 | - Hi!
281 | {{/if}}
282 |
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 |
303 | {{#unless foo}}
304 | - Hi!
305 | {{#else}}
306 | - Hi!
307 | {{/if}}
308 |
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 |
329 | - Hi!
330 | {{#if foo}}
331 | {{#else}}
332 | - Hi!
333 | {{/if}}
334 | - There
335 |
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 |
355 | - Hi!
356 | {{#if (not (not foo ))}}
357 | {{#else}}
358 | - Hi!
359 | {{/if}}
360 | - There
361 |
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 |
--------------------------------------------------------------------------------