├── test ├── specs │ ├── stream │ │ ├── output.html │ │ ├── input.ajs │ │ └── input.js │ ├── render │ │ ├── simple │ │ │ ├── output.html │ │ │ ├── input.ajs │ │ │ └── input.js │ │ ├── encode │ │ │ ├── input.ajs │ │ │ ├── output.html │ │ │ └── input.js │ │ ├── json-stringify │ │ │ ├── input.js │ │ │ ├── output.html │ │ │ └── input.ajs │ │ ├── async │ │ │ ├── output.html │ │ │ ├── input.ajs │ │ │ └── input.js │ │ └── for-loop │ │ │ ├── input.js │ │ │ ├── output.html │ │ │ └── input.ajs │ └── compile │ │ ├── runtime-errors-in-partials │ │ ├── error.txt │ │ ├── input.js │ │ ├── partials │ │ │ ├── foo │ │ │ │ └── bar.ajs │ │ │ └── partial.ajs │ │ └── input.ajs │ │ ├── simple-include │ │ ├── partial.ajs │ │ ├── output.html │ │ ├── input.js │ │ └── input.ajs │ │ ├── partials-in-dirs │ │ ├── partials │ │ │ └── partial.ajs │ │ ├── output.html │ │ ├── input.js │ │ └── input.ajs │ │ └── partials-in-partials │ │ ├── partials │ │ ├── foo │ │ │ └── bar.ajs │ │ └── partial.ajs │ │ ├── input.js │ │ ├── input.ajs │ │ └── output.html └── index.js ├── examples ├── from-file │ ├── input.ajs │ └── index.js ├── server │ ├── public │ │ ├── partials │ │ │ ├── footer.ajs │ │ │ ├── post.ajs │ │ │ └── header.ajs │ │ ├── css │ │ │ └── site.css │ │ └── index.ajs │ ├── server.js │ └── context.js ├── async-from-file │ ├── input.ajs │ └── index.js ├── simple │ └── example.js ├── middleware │ ├── public │ │ └── css │ │ │ └── site.css │ ├── views │ │ └── index.ajs │ └── server.js └── index.js ├── .gitignore ├── .github └── FUNDING.yml ├── LICENSE ├── lib ├── cache.js ├── util.js ├── index.js ├── lexer.js ├── template.js ├── grammar.js ├── compiler.js └── parser.js ├── CONTRIBUTING.md ├── bin └── cli.js ├── package.json ├── DOCUMENTATION.md └── README.md /test/specs/stream/output.html: -------------------------------------------------------------------------------- 1 | Welcome to Foo! 2 | -------------------------------------------------------------------------------- /test/specs/render/simple/output.html: -------------------------------------------------------------------------------- 1 | Welcome to Foo! 2 | -------------------------------------------------------------------------------- /test/specs/stream/input.ajs: -------------------------------------------------------------------------------- 1 | Welcome to <%= title %>! 2 | -------------------------------------------------------------------------------- /test/specs/render/simple/input.ajs: -------------------------------------------------------------------------------- 1 | Welcome to <%= title %>! 2 | -------------------------------------------------------------------------------- /test/specs/render/encode/input.ajs: -------------------------------------------------------------------------------- 1 | <%= title %> 2 | <%- html %> 3 | -------------------------------------------------------------------------------- /test/specs/render/json-stringify/input.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/specs/compile/runtime-errors-in-partials/error.txt: -------------------------------------------------------------------------------- 1 | bar.ajs:3 2 | -------------------------------------------------------------------------------- /test/specs/compile/simple-include/partial.ajs: -------------------------------------------------------------------------------- 1 | Content from partial. 2 | -------------------------------------------------------------------------------- /test/specs/compile/partials-in-dirs/partials/partial.ajs: -------------------------------------------------------------------------------- 1 | Content from partial. 2 | -------------------------------------------------------------------------------- /test/specs/compile/partials-in-partials/partials/foo/bar.ajs: -------------------------------------------------------------------------------- 1 | Another partial. 2 | -------------------------------------------------------------------------------- /test/specs/stream/input.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "title": "Foo" 3 | }; 4 | -------------------------------------------------------------------------------- /test/specs/render/simple/input.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "title": "Foo" 3 | }; 4 | -------------------------------------------------------------------------------- /test/specs/compile/partials-in-dirs/output.html: -------------------------------------------------------------------------------- 1 | Welcome to Foo! 2 | Content from partial. 3 | -------------------------------------------------------------------------------- /test/specs/compile/simple-include/output.html: -------------------------------------------------------------------------------- 1 | Welcome to Foo! 2 | Content from partial. 3 | -------------------------------------------------------------------------------- /test/specs/compile/partials-in-dirs/input.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "title": "Foo" 3 | }; 4 | -------------------------------------------------------------------------------- /test/specs/compile/simple-include/input.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "title": "Foo" 3 | }; 4 | -------------------------------------------------------------------------------- /test/specs/compile/partials-in-partials/input.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "title": "Foo" 3 | }; 4 | -------------------------------------------------------------------------------- /test/specs/compile/simple-include/input.ajs: -------------------------------------------------------------------------------- 1 | Welcome to <%= title %>! 2 | <% include('partial') -%> 3 | -------------------------------------------------------------------------------- /test/specs/render/encode/output.html: -------------------------------------------------------------------------------- 1 | <script>alert('evil')</script> 2 |

hey

3 | -------------------------------------------------------------------------------- /test/specs/compile/runtime-errors-in-partials/input.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "title": "Foo" 3 | }; 4 | -------------------------------------------------------------------------------- /test/specs/render/async/output.html: -------------------------------------------------------------------------------- 1 | - Apples are great 2 | - Pears are great 3 | - Oranges are great 4 | -------------------------------------------------------------------------------- /examples/from-file/input.ajs: -------------------------------------------------------------------------------- 1 | <% fruits.forEach(function (c) { -%> 2 | <%= c %>s are great 3 | <% }) %> 4 | -------------------------------------------------------------------------------- /examples/server/public/partials/footer.ajs: -------------------------------------------------------------------------------- 1 |
2 |

© Copyright 2011

3 | 4 | 5 | -------------------------------------------------------------------------------- /test/specs/compile/partials-in-dirs/input.ajs: -------------------------------------------------------------------------------- 1 | Welcome to <%= title %>! 2 | <% include('partials/partial') -%> 3 | -------------------------------------------------------------------------------- /test/specs/render/for-loop/input.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "posts": ["Apple", "Pear", "Orange"] 3 | } 4 | -------------------------------------------------------------------------------- /test/specs/render/for-loop/output.html: -------------------------------------------------------------------------------- 1 | - Apples are great 2 | - Pears are great 3 | - Oranges are great 4 | -------------------------------------------------------------------------------- /test/specs/compile/partials-in-partials/input.ajs: -------------------------------------------------------------------------------- 1 | Welcome to <%= title %>! 2 | <% include('partials/partial') -%> 3 | -------------------------------------------------------------------------------- /test/specs/compile/partials-in-partials/output.html: -------------------------------------------------------------------------------- 1 | Welcome to Foo! 2 | Content from partial. 3 | Another partial. 4 | -------------------------------------------------------------------------------- /test/specs/compile/partials-in-partials/partials/partial.ajs: -------------------------------------------------------------------------------- 1 | Content from partial. 2 | <% include('foo/bar') -%> 3 | -------------------------------------------------------------------------------- /test/specs/compile/runtime-errors-in-partials/partials/foo/bar.ajs: -------------------------------------------------------------------------------- 1 | Another partial. 2 | 3 | <% undefined.foo %> 4 | -------------------------------------------------------------------------------- /test/specs/render/for-loop/input.ajs: -------------------------------------------------------------------------------- 1 | <% posts.forEach(function (c) { -%> 2 | - <%= c -%>s are great 3 | <% }) %> 4 | -------------------------------------------------------------------------------- /test/specs/compile/runtime-errors-in-partials/input.ajs: -------------------------------------------------------------------------------- 1 | Welcome to <%= title %>! 2 | <% include('partials/partial') -%> 3 | -------------------------------------------------------------------------------- /test/specs/compile/runtime-errors-in-partials/partials/partial.ajs: -------------------------------------------------------------------------------- 1 | Content from partial. 2 | <% include('foo/bar') -%> 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *~ 4 | *.log 5 | node_modules 6 | *.env 7 | .DS_Store 8 | package-lock.json 9 | .bloggify/* 10 | -------------------------------------------------------------------------------- /test/specs/render/encode/input.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: "" 3 | , html: "

hey

" 4 | }; 5 | -------------------------------------------------------------------------------- /test/specs/render/json-stringify/output.html: -------------------------------------------------------------------------------- 1 | 2 | {"name":{"first":"Johnny","last":"B."},"evil":"<script>alert()</script>"} 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ionicabizau 2 | patreon: ionicabizau 3 | open_collective: ionicabizau 4 | custom: https://www.buymeacoffee.com/h96wwchmy -------------------------------------------------------------------------------- /examples/server/public/partials/post.ajs: -------------------------------------------------------------------------------- 1 |
2 |

<%= title %>

3 | <%- body %> 4 |
-------------------------------------------------------------------------------- /examples/server/public/partials/header.ajs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/specs/render/async/input.ajs: -------------------------------------------------------------------------------- 1 | <% 2 | getPosts(function (fruits) { 3 | fruits.forEach(function (c) { -%> 4 | - <%= c -%>s are great 5 | <% 6 | }); 7 | }); 8 | %> 9 | -------------------------------------------------------------------------------- /examples/async-from-file/input.ajs: -------------------------------------------------------------------------------- 1 | <% 2 | getFruits(function (fruits) { 3 | fruits.forEach(function (c) { -%> 4 | - <%= c -%>s are great 5 | <% 6 | }); 7 | }); 8 | %> 9 | -------------------------------------------------------------------------------- /examples/from-file/index.js: -------------------------------------------------------------------------------- 1 | const ajs = require('../..'); 2 | 3 | ajs.renderFile("input.ajs", { 4 | fruits: ["Apple", "Pear", "Orange"] 5 | }, (err, data) => { 6 | console.log(err || data); 7 | }); 8 | -------------------------------------------------------------------------------- /test/specs/render/async/input.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getPosts: function (cb) { 3 | setTimeout(function() { 4 | cb(["Apple", "Pear", "Orange"]); 5 | }, 10); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /test/specs/render/json-stringify/input.ajs: -------------------------------------------------------------------------------- 1 | <% 2 | user = { 3 | name: { 4 | first: "Johnny", 5 | last: "B.", 6 | } 7 | , evil: "" 8 | }; 9 | %> 10 | <%= JSON.stringify(user) %> 11 | -------------------------------------------------------------------------------- /examples/async-from-file/index.js: -------------------------------------------------------------------------------- 1 | const ajs = require('../..'); 2 | 3 | ajs.renderFile("input.ajs", { 4 | getFruits (cb) { 5 | setTimeout(function () { 6 | cb(["Apple", "Pear", "Orange"]); 7 | }, 100); 8 | } 9 | }, (err, data) => { 10 | console.log(err || data); 11 | }); 12 | -------------------------------------------------------------------------------- /examples/simple/example.js: -------------------------------------------------------------------------------- 1 | const ajs = require('../..'); 2 | var str = `<% posts.forEach(function (c) { -%> 3 | <%= c -%> 4 | s are great 5 | <% }) %>`; 6 | 7 | ajs.render(str, { 8 | filename: "foo.js", 9 | locals: { 10 | posts: ["Apple", "Pear", "Orange"] 11 | } 12 | }, function (err, data) { 13 | console.log(err || data); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/server/public/css/site.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, h1, h2, h3, h4, h5, h6, p {margin: 0;padding: 0;border: 0;outline: 0;} 2 | 3 | a { color: #222; text-decoration:none; } 4 | 5 | body { 6 | background:#fafafa; 7 | padding:30px; 8 | width:500px; 9 | } 10 | 11 | h1 { 12 | margin-bottom: 10px; 13 | } 14 | 15 | p { 16 | margin-bottom:10px; 17 | } 18 | 19 | .post { 20 | margin-bottom:30px; 21 | padding-top:10px; 22 | border-top:5px solid #333; 23 | } -------------------------------------------------------------------------------- /examples/middleware/public/css/site.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, h1, h2, h3, h4, h5, h6, p {margin: 0;padding: 0;border: 0;outline: 0;} 2 | 3 | a { color: #222; text-decoration:none; } 4 | 5 | body { 6 | background:#fafafa; 7 | padding:30px; 8 | width:500px; 9 | } 10 | 11 | h1 { 12 | margin-bottom: 10px; 13 | } 14 | 15 | p { 16 | margin-bottom:10px; 17 | } 18 | 19 | .post { 20 | margin-bottom:30px; 21 | padding-top:10px; 22 | border-top:5px solid #333; 23 | } -------------------------------------------------------------------------------- /examples/server/server.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect') 2 | , ajs = require('../../lib/ajs') 3 | , context = require('./context'); 4 | 5 | var server = connect.createServer() 6 | .use(ajs.serve('./public', context)) 7 | .use(connect.static('./public')); 8 | 9 | if (!module.parent) { 10 | var port = process.argv[2] || 3000; 11 | server.listen(port); 12 | console.log("Server running at http://127.0.0.1:" + port + "/"); 13 | } else { 14 | module.exports = server; 15 | } -------------------------------------------------------------------------------- /examples/server/public/index.ajs: -------------------------------------------------------------------------------- 1 | <% var title = "Blog Home"; 2 | include('partials/header', {title: title}) %> 3 | 4 |

<%= title %>

5 | 6 |
7 | <% // this could be any asyncronous data source 8 | // (in this case we've defined a mock db in './context.js') %> 9 | <% mysqlMock.query("select * from posts", function(err, posts) { %> 10 | <% posts.forEach(function(post){ %> 11 | <% include('partials/post', post) %> 12 | <% }) %> 13 | <% }) %> 14 |
15 | 16 | <% include('partials/footer') %> -------------------------------------------------------------------------------- /examples/middleware/views/index.ajs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= title %> 4 | 5 | 6 | 7 |

Blog Home

8 |
9 | <% getPosts(function(err, posts) { 10 | if(posts) { 11 | posts.forEach(function(post) { %> 12 |
13 |

<%= post.title %>

14 | <%- post.body %> 15 |
16 | <%}); 17 | } else { %> 18 | An error occured while trying to load the posts. 19 | <% } 20 | }) %> 21 |
22 | 23 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ajs = require(".."); 4 | 5 | ajs.render( 6 | `<% fruits.forEach(function (c) { -%> 7 | <%= c %>s are great 8 | <% }) %>`, { 9 | locals: { 10 | fruits: ["Apple", "Pear", "Orange"] 11 | } 12 | }, (err, data) => { 13 | console.log(err || data); 14 | // => 15 | // Apples are great 16 | // Pears are great 17 | // Oranges are great 18 | }); 19 | 20 | // Do some async stuff 21 | ajs.render( 22 | `<% fetchData(function (err, msg) { 23 | if (err) { %> 24 | Error: <%= err.message %> 25 | <% } else { 26 | <%= msg %> 27 | <% } %> 28 | <% }) %>`, { 29 | locals: { 30 | fetchData: cb => setTimeout( 31 | () => cb(null, "Hey there!") 32 | , 1000 33 | ) 34 | } 35 | }, (err, data) => { 36 | console.log(err || data); 37 | // => 38 | // Hey there! 39 | }); 40 | -------------------------------------------------------------------------------- /examples/server/context.js: -------------------------------------------------------------------------------- 1 | // var mysql = new (require('mysql').Client) 2 | // , node = {}; 3 | // 4 | // mysql.user = 'dbuser'; 5 | // mysql.password = 'passwd'; 6 | // mysql.connect(); 7 | // mysql.query('USE blog'); 8 | // 9 | // for(name in process.binding('natives')) { 10 | // node[name] = require(name); 11 | // } 12 | // 13 | // module.exports.mysql = mysql; 14 | // module.exports.node = node; 15 | 16 | var mysqlMock = { 17 | query: function(query, viewCallback) { 18 | setTimeout(function() { 19 | viewCallback(null, [ 20 | {id: 1, title: '10 Quick Photography Tips', body: '

Some Sample Text

'} 21 | , {id: 1, title: 'Some Post Title', body: '

With Sample Content

'} 22 | , {id: 1, title: 'Another Interesting Post', body: '

Welcome to our blog!

'} 23 | ]); 24 | }, 10); 25 | } 26 | } 27 | 28 | module.exports.mysqlMock = mysqlMock -------------------------------------------------------------------------------- /examples/middleware/server.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect') 2 | , ajs = require('../../lib/ajs'); 3 | 4 | 5 | var getPosts = function(viewCallback) { 6 | setTimeout(function() { 7 | viewCallback(null, [ 8 | {id: 1, title: '10 Quick Photography Tips', body: '

Some Sample Text

'} 9 | , {id: 1, title: 'Some Post Title', body: '

With Sample Content

'} 10 | , {id: 1, title: 'Another Interesting Post', body: '

Welcome to our blog!

'} 11 | ]); 12 | }, 50); 13 | } 14 | 15 | var server = connect.createServer() 16 | .use(ajs({dir: './views'})) 17 | .use(connect.static('./public')) 18 | .use(function(req, res) { 19 | res.render('index', {title: "Hello World!", getPosts: getPosts}); 20 | }); 21 | 22 | if (!module.parent) { 23 | var port = process.argv[2] || 3000; 24 | server.listen(port); 25 | console.log("Server running at http://127.0.0.1:" + port + "/"); 26 | } else { 27 | module.exports = server; 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-25 Ionică Bizău (https://ionicabizau.net) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | // [« Back to Index](index.html) 2 | 3 | var fs = require('fs'); 4 | 5 | // A very simple singleton memory cache to store compiled template functions for 6 | // extremely fast retrieval. We use a file's mtime (modified time) to determine 7 | // when the cache is stale and needs to be refreshed. 8 | module.exports = new (function() { 9 | this._store = {}; 10 | 11 | this.get = function(filename, callback) { 12 | var cached = this._store[filename]; 13 | if(!cached) return callback(null, null); 14 | 15 | fs.stat(filename, function(err, stat) { 16 | if(err) return callback(err); 17 | if(cached.mtime.toString() != stat.mtime.toString()) 18 | callback(null, null); 19 | else callback(null, cached.template); 20 | }); 21 | } 22 | 23 | this.getSync = function(filename) { 24 | var cached = this._store[filename]; 25 | if(!cached) return null; 26 | 27 | var stat = fs.statSync(filename); 28 | if(cached.mtime.toString() != stat.mtime.toString()) 29 | return null; 30 | else return cached.template; 31 | } 32 | 33 | this.set = function(filename, template) { 34 | var self = this; 35 | fs.stat(filename, function(err, stat) { 36 | if(stat) { 37 | self._store[filename] = { 38 | template: template 39 | , mtime: stat.mtime 40 | }; 41 | }; 42 | }); 43 | } 44 | })(); 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 🌟 Contributing 2 | 3 | Want to contribute to this project? Great! Please read these quick steps to streamline the process and avoid unnecessary tasks. ✨ 4 | 5 | ## 💬 Discuss Changes 6 | Start by opening an issue in the repository using the [bug tracker][1]. Describe your proposed contribution or the bug you've found. If relevant, include platform info and screenshots. 🖼️ 7 | 8 | Wait for feedback before proceeding unless the fix is straightforward, like a typo. 📝 9 | 10 | ## 🔧 Fixing Issues 11 | 12 | Fork the project and create a branch for your fix, naming it `some-great-feature` or `some-issue-fix`. Commit changes while following the [code style][2]. If the project has tests, add one. ✅ 13 | 14 | If a `package.json` or `bower.json` exists, add yourself to the `contributors` array; create it if it doesn't. 🙌 15 | 16 | ```json 17 | { 18 | "contributors": [ 19 | "Your Name (http://your.website)" 20 | ] 21 | } 22 | ``` 23 | 24 | ## 📬 Creating a Pull Request 25 | Open a pull request and reference the initial issue (e.g., *fixes #*). Provide a clear title and consider adding visual aids for clarity. 📊 26 | 27 | ## ⏳ Wait for Feedback 28 | Your contributions will be reviewed. If feedback is given, update your branch as needed, and the pull request will auto-update. 🔄 29 | 30 | ## 🎉 Everyone Is Happy! 31 | Your contributions will be merged, and everyone will appreciate your effort! 😄❤️ 32 | 33 | Thanks! 🤩 34 | 35 | [1]: /issues 36 | [2]: https://github.com/IonicaBizau/code-style -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | const fs = require("fs") 6 | , path = require("path") 7 | , util = require("util") 8 | , ajs = require("..") 9 | , Tilda = require("tilda") 10 | ; 11 | 12 | new Tilda(`${__dirname}/../package.json`, { 13 | options: [ 14 | { 15 | opts: ["tree", "t"] 16 | , desc: "Output the abstract syntax tree" 17 | } 18 | , { 19 | opts: ["source", "s"] 20 | , desc: "Output the raw VM source" 21 | } 22 | , { 23 | name: "locals" 24 | , opts: ["locals", "l"] 25 | , desc: "The template data as JSON." 26 | } 27 | ] 28 | , examples: [ 29 | "ajs template.ajs" 30 | , "ajs -t template.ajs" 31 | , "ajs -s template.ajs" 32 | ] 33 | }).main(app => { 34 | 35 | let filename = app.argv[0]; 36 | 37 | if (!filename) { 38 | return app.exit(new Error("Please provide a template path.")); 39 | } 40 | 41 | debugger 42 | let locals = {}; 43 | try { 44 | locals = JSON.parse(app.options.locals.value); 45 | } catch (e) { 46 | return app.exit(e); 47 | } 48 | 49 | ajs._load(filename, {}, function(err, template) { 50 | if(err) return console.error(err.stack); 51 | 52 | if(app.options.tree.value) 53 | return console.log(util.inspect(template.toString(), false, 100) + "\n"); 54 | else if(app.options.source.value) 55 | return console.log(template.toString() + "\n"); 56 | 57 | template(locals).on("data", function(data) { 58 | console.log(data); 59 | }).on("error", function(err) { 60 | console.error(); 61 | console.error(err.stack); 62 | }).on("end", function() { 63 | console.log(); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require('path'); 4 | 5 | const filenameCache = {} 6 | , dirnameCache = {} 7 | ; 8 | 9 | module.exports = { 10 | /** 11 | * resolveFilename 12 | * Resolves and caches filename paths. 13 | * 14 | * @name resolveFilename 15 | * @function 16 | * @param {String} filename The filename to resolve. 17 | * @returns {String} The resolved filename. 18 | */ 19 | resolveFilename: filename => { 20 | let cached = filenameCache[filename]; 21 | 22 | if (cached) { 23 | return cached; 24 | } 25 | 26 | return filenameCache[filename] = path.resolve(process.cwd(), filename); 27 | } 28 | 29 | 30 | /** 31 | * resolveDirname 32 | * Resolves and caches folder paths. 33 | * 34 | * @name resolveDirname 35 | * @function 36 | * @param {} filename 37 | * @returns {String} The resolved dirname path. 38 | */ 39 | , resolveDirname: filename => { 40 | let cached = dirnameCache[filename]; 41 | 42 | if (cached) { 43 | return cached; 44 | } 45 | 46 | return dirnameCache[filename] = path.dirname(filename); 47 | } 48 | 49 | 50 | /** 51 | * escape 52 | * Escapes the HTML entities. 53 | * 54 | * @name escape 55 | * @function 56 | * @param {String} expr The input HTML code. 57 | * @returns {String} The escaped result. 58 | */ 59 | , escape: expr => { 60 | return String(expr) 61 | .replace(/&(?!\w+;)/g, '&') 62 | .replace(//g, '>'); 64 | } 65 | 66 | /** 67 | * extend 68 | * Merges two objects. 69 | * 70 | * @name extend 71 | * @function 72 | * @param {Object} target The first object. 73 | * @param {Object} source The second object. 74 | * @returns {Object} The merged objects. 75 | */ 76 | , extend: (target, source, clone) => { 77 | let _target = clone ? {} : target; 78 | let props = Object.getOwnPropertyNames(source); 79 | props.forEach(name => { 80 | _target[name] = (clone ? target[name] : null) || source[name]; 81 | }); 82 | return _target; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "node test/index.js" 4 | }, 5 | "name": "ajs", 6 | "description": "Asynchronous templating in Node.js", 7 | "keywords": [ 8 | "ajs", 9 | "ejs", 10 | "template", 11 | "view", 12 | "asynchronous" 13 | ], 14 | "version": "1.3.4", 15 | "author": "Ionică Bizău (https://ionicabizau.net)", 16 | "contributors": [ 17 | "Evan Owen " 18 | ], 19 | "main": "./lib/index.js", 20 | "dependencies": { 21 | "idy": "^1.2.3", 22 | "is-there": "^4.3.3", 23 | "read-utf8": "^1.2.3", 24 | "stream-data": "^1.0.0", 25 | "tilda": "^4.3.1" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+ssh://git@github.com/IonicaBizau/ajs.git" 30 | }, 31 | "bin": { 32 | "ajs": "./bin/cli.js" 33 | }, 34 | "devDependencies": { 35 | "ejs": "^2.4.2", 36 | "fs-file-tree": "^1.0.0", 37 | "iterate-object": "^1.3.2", 38 | "r-json": "^1.2.3", 39 | "read-utf8": "^1.2.3", 40 | "tester": "^1.3.4" 41 | }, 42 | "blah": { 43 | "generateLicense": false, 44 | "description": [ 45 | { 46 | "h2": "Features" 47 | }, 48 | { 49 | "ul": [ 50 | "Control flow with `<% %>`", 51 | "Escaped output with `<%= %>` (escape function configurable)", 52 | "Unescaped raw output with `<%- %>`", 53 | "Newline-trim mode ('newline slurping') with `-%>` ending tag", 54 | "Custom delimiters (e.g., use `` instead of `<% %>`)", 55 | "Includes", 56 | "Static caching of intermediate JavaScript", 57 | "Static caching of templates", 58 | "Complies with the [Express](http://expressjs.com) view system" 59 | ] 60 | } 61 | ], 62 | "thanks": { 63 | "p": "Big thanks to [Evan Owen](https://github.com/kainosnoema) who created the initial versions of the project! Amazing stuff! :cake:" 64 | }, 65 | "h_img": "http://i.imgur.com/nQiOz0E.png" 66 | }, 67 | "license": "MIT", 68 | "bugs": { 69 | "url": "https://github.com/IonicaBizau/ajs/issues" 70 | }, 71 | "homepage": "https://github.com/IonicaBizau/ajs#readme", 72 | "directories": { 73 | "example": "examples", 74 | "test": "test" 75 | }, 76 | "files": [ 77 | "bin/", 78 | "app/", 79 | "lib/", 80 | "dist/", 81 | "src/", 82 | "scripts/", 83 | "resources/", 84 | "menu/", 85 | "cli.js", 86 | "index.js", 87 | "index.d.ts", 88 | "package-lock.json", 89 | "bloggify.js", 90 | "bloggify.json", 91 | "bloggify/" 92 | ] 93 | } -------------------------------------------------------------------------------- /DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | ## Documentation 2 | 3 | You can see below the API reference of this module. 4 | 5 | ### `ajs(opts)` 6 | The main `ajs` export is a Connect middleware function. By adding 7 | `ajs()` to your stack, any middleware down the line will have a 8 | `res.render("/path", )` function that accepts a template 9 | path and context object. 10 | 11 | #### Params 12 | 13 | - **Object** `opts`: An object containing the following fields: 14 | - `dir` (String): The path to the views directory (default: `./views`). 15 | 16 | #### Return 17 | - **Function** The middleware function. 18 | 19 | ### `serve(rootDir, locals, opts)` 20 | If you're looking for a simpler way to build a quick templated site, 21 | you can use the `ajs.serve("dir", )` middleware and ajs will map request URLs 22 | directly to file and directory paths. Simply create a context containing 23 | a data source and any utilities, and your entire app can live in your templates! 24 | If this reminds of you PHP, just remember you're asyncronous now. 25 | 26 | #### Params 27 | 28 | - **String** `rootDir`: The views directory. 29 | - **Object** `locals`: The data to pass. 30 | - **Object** `opts`: The render options. 31 | 32 | #### Return 33 | - **Function** The middleware function. 34 | 35 | ### `compile(str, opts)` 36 | While we can't support ExpressJS yet due to its syncronous handling of 37 | [template engines](https://github.com/visionmedia/express/blob/master/lib/view.js#L421) 38 | and [responses](https://github.com/visionmedia/express/blob/master/lib/response.js#L115), 39 | we can still support a similar API. 40 | 41 | #### Params 42 | 43 | - **String** `str`: The content to compile. 44 | - **Object** `opts`: An object containing the following fields: 45 | - `filename` (String): The filename of the compiled file. By default a random filename. 46 | - 47 | 48 | #### Return 49 | - **Template** An ajs `Template` object. 50 | 51 | ### `render(str, opts, callback)` 52 | Render the template content. 53 | 54 | #### Params 55 | 56 | - **String** `str`: The template content. 57 | - **Object** `opts`: The compile options. 58 | - **Function** `callback`: The callback function. 59 | 60 | #### Return 61 | - **EventEmitter** The event emitter you can use to listen to `'data'`, `'end'`, and `'error'` events. 62 | 63 | ### `renderFile(path, opts, callback)` 64 | Renders a file. 65 | 66 | #### Params 67 | 68 | - **String** `path`: The path to the template file. 69 | - **Object** `opts`: The compile options. 70 | - **Function** `callback`: The callback function. 71 | 72 | ### `compileFile(filename, opts, callback)` 73 | Return a template function compiled from the requested file. 74 | If a cached object is found and the file hasn't been updated, return that. 75 | Otherwise, attempt to read and compile the file asyncronously, calling back 76 | with a compiled template function if successful or an error if not. 77 | 78 | #### Params 79 | 80 | - **String** `filename`: The path to the file. 81 | - **Object** `opts`: The compile options. 82 | - **Function** `callback`: The callback function. 83 | 84 | ### `compileFileSync(filename, opts)` 85 | Synchronous version of `ajs.compileFile`, used for `require()` support. 86 | 87 | #### Params 88 | 89 | - **String** `filename`: The path to the file. 90 | - **Object** `opts`: The compile options. 91 | 92 | #### Return 93 | - **Template** The ajs template object. 94 | 95 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const tester = require("tester") 4 | , ajs = require("..") 5 | , fsTree = require("fs-file-tree") 6 | , iterateObject = require("iterate-object") 7 | , _readFile = require("read-utf8") 8 | , readJson = require("r-json") 9 | , isThere = require("is-there") 10 | ; 11 | 12 | function readFile (path) { 13 | return _readFile(path).replace(/\r/g, ""); 14 | } 15 | 16 | tester.describe("ajs", t => { 17 | let tree = fsTree.sync(`${__dirname}/specs`, { camelCase: true }) 18 | iterateObject(tree.render, (files, name) => { 19 | if (name.startsWith("_")) { return; } 20 | t.should(`render ${name} templates`, cb => { 21 | ajs.render(readFile(files.inputAjs.path), { 22 | locals: require(files.inputJs.path) 23 | }, (err, data) => { 24 | err && console.log(err.stack); 25 | t.expect(err).toBe(null); 26 | t.expect(data).toBe(readFile(files.outputHtml.path)); 27 | cb(); 28 | }); 29 | }); 30 | t.should("render " + name + " templates (renderFile)", function (cb) { 31 | ajs.renderFile(files.inputAjs.path, require(files.inputJs.path), function (err, data) { 32 | err && console.log(err.stack); 33 | t.expect(err).toBe(null); 34 | t.expect(data).toBe(readFile(files.outputHtml.path)); 35 | cb(); 36 | }); 37 | }); 38 | }); 39 | 40 | iterateObject(tree.compile, (files, name) => { 41 | if (name.startsWith("_")) { return; } 42 | t.should(`compile ${name} cases`, cb => { 43 | ajs.compileFile(files.inputAjs.path, (err, templ) => { 44 | err && console.log(err.stack); 45 | t.expect(err).toBe(null); 46 | t.expect(templ).toBeA("function"); 47 | templ(require(files.inputJs.path), (err, data) => { 48 | if (files.errorTxt) { 49 | t.expect(err).toNotBe(null); 50 | t.expect(err.message.includes(readFile(files.errorTxt.path).trim())).toBe(true); 51 | } else { 52 | err && console.log(err.stack); 53 | t.expect(err).toBe(null); 54 | t.expect(data).toBe(readFile(files.outputHtml.path)); 55 | } 56 | cb(); 57 | }); 58 | }); 59 | }); 60 | }); 61 | 62 | let templ = null; 63 | t.it("compile the file", cb => { 64 | ajs.compileFile(tree.stream.inputAjs.path, (err, template) => { 65 | debugger 66 | err && console.log(err.stack); 67 | templ = template; 68 | cb(); 69 | }); 70 | }); 71 | 72 | t.it("reuse the compiled result", cb => { 73 | templ(require(tree.stream.inputJs.path), (err, data) => { 74 | err && console.log(err.stack); 75 | t.expect(data).toBe(readFile(tree.stream.outputHtml.path)); 76 | cb(); 77 | }); 78 | }); 79 | 80 | t.it("reuse the compiled result again", cb => { 81 | templ(require(tree.stream.inputJs.path), (err, data) => { 82 | err && console.log(err.stack); 83 | t.expect(data).toBe(readFile(tree.stream.outputHtml.path)); 84 | cb(); 85 | }); 86 | }); 87 | 88 | t.it("reuse the compiled result again", cb => { 89 | templ(require(tree.stream.inputJs.path), (err, data) => { 90 | err && console.log(err.stack); 91 | t.expect(data).toBe(readFile(tree.stream.outputHtml.path)); 92 | cb(); 93 | }); 94 | }); 95 | 96 | t.it("handle streams", cb => { 97 | templ(require(tree.stream.inputJs.path)).on("data", chunk => { 98 | t.expect(chunk).toBeA("string"); 99 | }).on("end", cb); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | [![ajs](http://i.imgur.com/nQiOz0E.png)](#) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | # `$ ajs` 23 | 24 | [![Support me on Patreon][badge_patreon]][patreon] [![Buy me a book][badge_amazon]][amazon] [![PayPal][badge_paypal_donate]][paypal-donations] [![Ask me anything](https://img.shields.io/badge/ask%20me-anything-1abc9c.svg)](https://github.com/IonicaBizau/ama) [![Version](https://img.shields.io/npm/v/ajs.svg)](https://www.npmjs.com/package/ajs) [![Downloads](https://img.shields.io/npm/dt/ajs.svg)](https://www.npmjs.com/package/ajs) [![Get help on Codementor](https://cdn.codementor.io/badges/get_help_github.svg)](https://www.codementor.io/@johnnyb?utm_source=github&utm_medium=button&utm_term=johnnyb&utm_campaign=github) 25 | 26 | Buy Me A Coffee 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | > Asynchronous templating in Node.js 35 | 36 | 37 | 38 | 39 | 40 | 41 | ## Features 42 | 43 | 44 | - Control flow with `<% %>` 45 | - Escaped output with `<%= %>` (escape function configurable) 46 | - Unescaped raw output with `<%- %>` 47 | - Newline-trim mode ('newline slurping') with `-%>` ending tag 48 | - Custom delimiters (e.g., use `` instead of `<% %>`) 49 | - Includes 50 | - Static caching of intermediate JavaScript 51 | - Static caching of templates 52 | - Complies with the [Express](http://expressjs.com) view system 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ## :cloud: Installation 67 | 68 | You can install the package globally and use it as command line tool: 69 | 70 | 71 | ```sh 72 | # Using npm 73 | npm install --global ajs 74 | 75 | # Using yarn 76 | yarn global add ajs 77 | ``` 78 | 79 | 80 | Then, run `ajs --help` and see what the CLI tool can do. 81 | 82 | 83 | ``` 84 | $ ajs --help 85 | Usage: ajs [options] 86 | 87 | Asynchronous templating in Node.js 88 | 89 | Options: 90 | -l, --locals The template data as JSON. 91 | -s, --source Output the raw VM source 92 | -t, --tree Output the abstract syntax tree 93 | -h, --help Displays this help. 94 | -v, --version Displays version information. 95 | 96 | Examples: 97 | $ ajs template.ajs 98 | $ ajs -t template.ajs 99 | $ ajs -s template.ajs 100 | 101 | Documentation can be found at https://github.com/IonicaBizau/ajs#readme. 102 | ``` 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | ## :clipboard: Example 117 | 118 | 119 | 120 | Here is an example how to use this package as library. To install it locally, as library, you can use `npm install ajs` (or `yarn add ajs`): 121 | 122 | 123 | 124 | ```js 125 | const ajs = require("ajs"); 126 | 127 | ajs.render( 128 | `<% fruits.forEach(function (c) { -%> 129 | <%= c %>s are great 130 | <% }) %>`, { 131 | locals: { 132 | fruits: ["Apple", "Pear", "Orange"] 133 | } 134 | }, (err, data) => { 135 | console.log(err || data); 136 | // => 137 | // Apples are great 138 | // Pears are great 139 | // Oranges are great 140 | }); 141 | 142 | // Do some async stuff 143 | ajs.render( 144 | `<% fetchData(function (err, msg) { 145 | if (err) { %> 146 | Error: <%= err.message %> 147 | <% } else { 148 | <%= msg %> 149 | <% } %> 150 | <% }) %>`, { 151 | locals: { 152 | fetchData: cb => setTimeout( 153 | () => cb(null, "Hey there!") 154 | , 1000 155 | ) 156 | } 157 | }, (err, data) => { 158 | console.log(err || data); 159 | // => 160 | // Hey there! 161 | }); 162 | ``` 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | ## :question: Get Help 176 | 177 | There are few ways to get help: 178 | 179 | 180 | 181 | 1. Please [post questions on Stack Overflow](https://stackoverflow.com/questions/ask). You can open issues with questions, as long you add a link to your Stack Overflow question. 182 | 2. For bug reports and feature requests, open issues. :bug: 183 | 3. For direct and quick help, you can [use Codementor](https://www.codementor.io/johnnyb). :rocket: 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | ## :memo: Documentation 192 | 193 | For full API reference, see the [DOCUMENTATION.md][docs] file. 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | ## :yum: How to contribute 207 | Have an idea? Found a bug? See [how to contribute][contributing]. 208 | 209 | 210 | ## :sparkling_heart: Support my projects 211 | I open-source almost everything I can, and I try to reply to everyone needing help using these projects. Obviously, 212 | this takes time. You can integrate and use these projects in your applications *for free*! You can even change the source code and redistribute (even resell it). 213 | 214 | However, if you get some profit from this or just want to encourage me to continue creating stuff, there are few ways you can do it: 215 | 216 | 217 | - Starring and sharing the projects you like :rocket: 218 | - [![Buy me a book][badge_amazon]][amazon]—I love books! I will remember you after years if you buy me one. :grin: :book: 219 | - [![PayPal][badge_paypal]][paypal-donations]—You can make one-time donations via PayPal. I'll probably buy a ~~coffee~~ tea. :tea: 220 | - [![Support me on Patreon][badge_patreon]][patreon]—Set up a recurring monthly donation and you will get interesting news about what I'm doing (things that I don't share with everyone). 221 | - **Bitcoin**—You can send me bitcoins at this address (or scanning the code below): `1P9BRsmazNQcuyTxEqveUsnf5CERdq35V6` 222 | 223 | ![](https://i.imgur.com/z6OQI95.png) 224 | 225 | 226 | Thanks! :heart: 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | ## :cake: Thanks 237 | 238 | Big thanks to [Evan Owen](https://github.com/kainosnoema) who created the initial versions of the project! Amazing stuff! :cake: 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | ## :dizzy: Where is this library used? 249 | If you are using this library in one of your projects, add it in this list. :sparkles: 250 | 251 | - `ajs-xgettext` 252 | - `blah` 253 | - `bloggify-ajs-renderer` 254 | - `bloggify-icons` 255 | - `bloggify-renderer-ajs` 256 | - `express-ajs` 257 | - `git-stats-html` 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | ## :scroll: License 270 | 271 | [MIT][license] © [Ionică Bizău][website] 272 | 273 | 274 | 275 | 276 | 277 | 278 | [license]: /LICENSE 279 | [website]: https://ionicabizau.net 280 | [contributing]: /CONTRIBUTING.md 281 | [docs]: /DOCUMENTATION.md 282 | [badge_patreon]: https://ionicabizau.github.io/badges/patreon.svg 283 | [badge_amazon]: https://ionicabizau.github.io/badges/amazon.svg 284 | [badge_paypal]: https://ionicabizau.github.io/badges/paypal.svg 285 | [badge_paypal_donate]: https://ionicabizau.github.io/badges/paypal_donate.svg 286 | [patreon]: https://www.patreon.com/ionicabizau 287 | [amazon]: http://amzn.eu/hRo9sIZ 288 | [paypal-donations]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=RVXDDLKKLQRJW 289 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs") 4 | , path = require("path") 5 | , Template = require("./template") 6 | , Cache = require("./cache") 7 | , idy = require("idy") 8 | ; 9 | 10 | // If you need lower-level access to an ajs template, simply require it, call it 11 | // with a locals object `template()`, and bind to its `data`, 12 | // `error` and `end` events. 13 | require.extensions[".ajs"] = (module, filename) => { 14 | module.exports = ajs._loadSync(filename); 15 | return module; 16 | }; 17 | 18 | /** 19 | * ajs 20 | * The main `ajs` export is a Connect middleware function. By adding 21 | * `ajs()` to your stack, any middleware down the line will have a 22 | * `res.render("/path", )` function that accepts a template 23 | * path and context object. 24 | * 25 | * @name ajs 26 | * @function 27 | * @param {Object} opts An object containing the following fields: 28 | * 29 | * - `dir` (String): The path to the views directory (default: `./views`). 30 | * 31 | * @returns {Function} The middleware function. 32 | */ 33 | function ajs (opts) { 34 | opts = opts || {}; 35 | 36 | const templateDir = opts.dir || "./views"; 37 | 38 | return (req, res, next) => { 39 | res.render = (filename, locals, opts) => { 40 | filename = normalizeFilename(path.join(templateDir, filename)); 41 | 42 | ajs._load(filename, opts, (err, template) => { 43 | if (err) { 44 | if (err.code == "ENOENT" || err.code == "EBADF") { 45 | res.statusCode = 500; 46 | res.end("Template not found: " + filename); 47 | } else { 48 | next(err); 49 | } 50 | return; 51 | } 52 | 53 | // We make sure to set the content-type and transfer-encoding headers 54 | // to take full advantage of HTTP's streaming ability. 55 | res.statusCode = 200; 56 | res.setHeader("Content-Type", "text/html; charset=UTF-8"); 57 | res.setHeader("Transfer-Encoding", "chunked"); 58 | 59 | // As data becomes available from the template, we pass it on to the client immediately. 60 | template(locals) 61 | .on("data", data => { 62 | res.write(data); 63 | }).on("error", e => { 64 | console.error(e.stack); 65 | res.statusCode = 500; 66 | res.end("Internal server error"); 67 | }).on("end", () => { 68 | res.end(); 69 | }); 70 | 71 | next(); 72 | }); 73 | }; 74 | }; 75 | } 76 | 77 | /** 78 | * serve 79 | * If you're looking for a simpler way to build a quick templated site, 80 | * you can use the `ajs.serve("dir", )` middleware and ajs will map request URLs 81 | * directly to file and directory paths. Simply create a context containing 82 | * a data source and any utilities, and your entire app can live in your templates! 83 | * If this reminds of you PHP, just remember you're asyncronous now. 84 | * 85 | * @name serve 86 | * @function 87 | * @param {String} rootDir The views directory. 88 | * @param {Object} locals The data to pass. 89 | * @param {Object} opts The render options. 90 | * @returns {Function} The middleware function. 91 | */ 92 | ajs.serve = (rootDir, locals, opts) => { 93 | return (req, res, next) => { 94 | let path = normalizeFilename(req.url) 95 | , filename = rootDir + path 96 | ; 97 | 98 | ajs._load(filename, opts, (err, template) => { 99 | if (err) { 100 | return next(err); 101 | } 102 | 103 | locals.request = req; 104 | 105 | template(locals) 106 | .on("data", data => { 107 | res.write(data); 108 | }).on("error", e => { 109 | console.error(e.stack); 110 | res.statusCode = 500; 111 | res.end("Internal server error"); 112 | }).on("end", () => { 113 | res.end(); 114 | }); 115 | }); 116 | }; 117 | }; 118 | 119 | 120 | /** 121 | * compile 122 | * While we can't support ExpressJS yet due to its syncronous handling of 123 | * [template engines](https://github.com/visionmedia/express/blob/master/lib/view.js#L421) 124 | * and [responses](https://github.com/visionmedia/express/blob/master/lib/response.js#L115), 125 | * we can still support a similar API. 126 | * 127 | * @name compile 128 | * @function 129 | * @param {String} str The content to compile. 130 | * @param {Object} opts An object containing the following fields: 131 | * 132 | * - `filename` (String): The filename of the compiled file. By default a random filename. 133 | * - 134 | * @returns {Template} An ajs `Template` object. 135 | */ 136 | ajs.compile = (str, opts) => { 137 | opts = opts || {}; 138 | opts.filename = opts.filename || idy(); 139 | 140 | let key = JSON.stringify(opts.filename + opts.bare) 141 | , template 142 | ; 143 | 144 | if (!(template = Cache._store[key])) 145 | template = Cache._store[key] = new Template(str, opts); 146 | 147 | return template; 148 | }; 149 | 150 | /** 151 | * render 152 | * Render the template content. 153 | * 154 | * @name render 155 | * @function 156 | * @param {String} str The template content. 157 | * @param {Object} opts The compile options. 158 | * @param {Function} callback The callback function. 159 | * @returns {EventEmitter} The event emitter you can use to listen to `'data'`, 160 | * `'end'`, and `'error'` events. 161 | */ 162 | ajs.render = (str, opts, callback) => { 163 | let buffer = [] 164 | , template = ajs.compile(str, opts) 165 | , ev = template(opts.locals) 166 | ; 167 | 168 | if (callback) { 169 | ev.on("data", data => { 170 | buffer.push(data); 171 | }).on("error", err => { 172 | callback(err); 173 | }).on("end", () => { 174 | callback(null, buffer.join("")); 175 | }); 176 | } 177 | 178 | 179 | return ev; 180 | }; 181 | 182 | /** 183 | * renderFile 184 | * Renders a file. 185 | * 186 | * @name renderFile 187 | * @function 188 | * @param {String} path The path to the template file. 189 | * @param {Object} opts The compile options. 190 | * @param {Function} callback The callback function. 191 | */ 192 | ajs.renderFile = function (path, data, opts, callback) { 193 | 194 | if (typeof data === "function") { 195 | callback = data; 196 | opts = {}; 197 | } else if (typeof opts === "function") { 198 | callback = opts; 199 | opts = {}; 200 | } 201 | 202 | opts = opts || {}; 203 | opts.locals = data; 204 | 205 | ajs.compileFile(path, opts, function (err, template) { 206 | if (err) { 207 | return callback(err); 208 | } 209 | template(opts.locals, callback); 210 | }); 211 | }; 212 | 213 | // Alias for express rendering 214 | ajs.__express = ajs.renderFile; 215 | 216 | /** 217 | * compileFile 218 | * Return a template function compiled from the requested file. 219 | * If a cached object is found and the file hasn't been updated, return that. 220 | * Otherwise, attempt to read and compile the file asyncronously, calling back 221 | * with a compiled template function if successful or an error if not. 222 | * 223 | * @name compileFile 224 | * @function 225 | * @param {String} filename The path to the file. 226 | * @param {Object} opts The compile options. 227 | * @param {Function} callback The callback function. 228 | */ 229 | ajs.compileFile = ajs._load = (filename, opts, callback) => { 230 | 231 | if (typeof opts === "function") { 232 | callback = opts; 233 | opts = {}; 234 | } 235 | 236 | opts = opts || {}; 237 | 238 | let template 239 | , cache = (typeof opts.cache != "undefined") ? opts.cache : true 240 | ; 241 | 242 | Cache.get(filename, (err, cached) => { 243 | if(err) return callback(err); 244 | 245 | if(cache && cached) { 246 | callback(null, cached); 247 | } else { 248 | fs.readFile(filename, "utf-8", (err, source) => { 249 | if(err) return callback(err); 250 | try { 251 | opts.filename = filename; 252 | template = new Template(source, opts); 253 | } catch(e) { 254 | e.message = "In " + filename + ", " + e.message; 255 | return callback(e); 256 | } 257 | Cache.set(filename, template); 258 | callback(null, template); 259 | }); 260 | } 261 | }); 262 | }; 263 | 264 | /** 265 | * compileFileSync 266 | * Synchronous version of `ajs.compileFile`, used for `require()` support. 267 | * 268 | * @name compileFileSync 269 | * @function 270 | * @param {String} filename The path to the file. 271 | * @param {Object} opts The compile options. 272 | * @returns {Template} The ajs template object. 273 | */ 274 | ajs.compileFileSync = ajs._loadSync = (filename, opts) => { 275 | opts = opts || {}; 276 | 277 | let template 278 | , cache = (typeof opts.cache != "undefined") ? opts.cache : true 279 | ; 280 | 281 | try { 282 | if (cache && (cached = Cache.getSync(filename))) { 283 | return cached; 284 | } else { 285 | opts.filename = filename; 286 | template = new Template(fs.readFileSync(filename, "utf8"), opts); 287 | Cache.set(filename, template); 288 | return template; 289 | } 290 | } catch(e) { 291 | e.message = "In " + filename + ", " + e.message; 292 | throw e; 293 | } 294 | }; 295 | 296 | function normalizeFilename(path) { 297 | if(path.slice(-1) == "/") 298 | path += "index"; 299 | if(path.slice(-4) != ".ajs") 300 | path += ".ajs"; 301 | return path; 302 | } 303 | 304 | module.exports = ajs; 305 | -------------------------------------------------------------------------------- /lib/lexer.js: -------------------------------------------------------------------------------- 1 | // Thanks to mishoo/uglifyjs for most of this! 2 | 3 | // [« Back to Index](index.html) 4 | 5 | var g = require('./grammar') 6 | , util = require('util'); 7 | 8 | // AJS Lexer 9 | // ------------- 10 | 11 | // The lexer accepts raw AJS source and processes it character-by-character, 12 | // creating token objects that can be interpreted by the parser to 13 | // form an AST (abstract syntax tree). 14 | var Lexer = module.exports = function Lexer(source, opts) { 15 | opts = opts || {}; 16 | 17 | this.source = source; 18 | this.length = source.length; 19 | 20 | this.tokens = []; 21 | 22 | this._curToken = null; 23 | this._line = 1; 24 | this._col = 1; 25 | this._pos = 0; 26 | 27 | this._embedChar = opts.embedChar || "%"; 28 | this._includeComments = opts.includeComments == true; 29 | this._newLineBefore = false; 30 | this._commentsBefore = []; 31 | this._regexpAllowed = false; 32 | } 33 | 34 | Lexer.prototype.tokenize = function() { 35 | this.tokens = []; 36 | 37 | this._curToken = null; 38 | this._line = 1; 39 | this._col = 1; 40 | this._pos = 0; 41 | 42 | this._inEmbed = false; 43 | this._stripNewLine = false; 44 | 45 | this.nextToken(); 46 | while(this._curToken.type != Token.EOF) { 47 | this.nextToken(); 48 | } 49 | return this.tokens; 50 | } 51 | 52 | Lexer.prototype.nextToken = function() { 53 | if(this._inEmbed) this._skipWhitespace(); 54 | if(this._stripNewLine) this._skipNewline(); 55 | 56 | var ch = this._peek(); 57 | if (!ch) return this._token(Token.EOF); 58 | 59 | if(ch == '<' && this._peek(1) == this._embedChar) 60 | return this._embed(); 61 | else if(this._inEmbed) { 62 | if (g.is_digit(ch)) 63 | return this._number(); 64 | if (ch == '"' || ch == "'") 65 | return this._string(); 66 | if (g.is_punctuation(ch)) 67 | return this._punctuation(); 68 | if (ch == ".") 69 | return this._dot(); 70 | if (ch == "/") 71 | return this._slash(); 72 | if (g.is_operator(ch)) { 73 | if (ch === "-" && this._peek(1) == this._embedChar && this._peek(2) == ">") { 74 | return this._embed(); 75 | } else if (ch == this._embedChar && this._peek(1) == ">") 76 | return this._embed(); 77 | else 78 | return this._operator(); 79 | } 80 | if (ch == "\\" || g.is_identifier_start(ch)) 81 | return this._word(); 82 | 83 | this._error("Unexpected character '" + ch + "'"); 84 | } else { 85 | return this._output(); 86 | } 87 | } 88 | 89 | Lexer.prototype._peek = function(i) { 90 | i = i || 0; 91 | return this.source.charAt(this._pos + i); 92 | } 93 | 94 | Lexer.prototype._next = function(throwEof) { 95 | var ch = this.source.charAt(this._pos++); 96 | if (throwEof && !ch) throw EX_EOF; 97 | if (ch == "\n") { 98 | this._newLineBefore = true; 99 | this._line++; 100 | this._col = 0; 101 | } else { 102 | this._col++; 103 | } 104 | return ch; 105 | } 106 | 107 | Lexer.prototype._skipWhitespace = function() { 108 | while (g.is_whitespace(this._peek())) this._next(); 109 | }; 110 | 111 | Lexer.prototype._skipNewline = function() { 112 | // TODO Handle other new line characters 113 | while (this._peek() === '\n') this._next(); 114 | }; 115 | 116 | Lexer.prototype._readWhile = function(pred) { 117 | var ret = "", ch = this._peek(), i = 0; 118 | while (ch && pred(ch, i++)) { 119 | ret += this._next(); 120 | ch = this._peek(); 121 | } 122 | return ret; 123 | }; 124 | 125 | 126 | Lexer.prototype._eof = function() { 127 | return !this._peek(); 128 | }; 129 | 130 | Lexer.prototype._find = function(ch, throwEof) { 131 | var pos = this.source.indexOf(ch, this._pos); 132 | if (throwEof && pos == -1) throw EX_EOF; 133 | return pos; 134 | }; 135 | 136 | Lexer.prototype._output = function() { 137 | var i = this._find("<" + this._embedChar), text; 138 | if (i == -1) { 139 | text = this.source.substr(this._pos); 140 | this._pos = this.source.length; 141 | } else { 142 | text = this.source.substring(this._pos, i); 143 | this._pos = i; 144 | } 145 | 146 | this._line += text.split("\n").length - 1; 147 | this._newlineBefore = text.indexOf("\n") >= 0; 148 | return this._token(Token.OUTPUT, text, true); 149 | } 150 | 151 | Lexer.prototype._embed = function() { 152 | var tag = this._next() + this._next() 153 | , ch = this._peek() 154 | ; 155 | 156 | if(tag == '<' + this._embedChar) { 157 | this._inEmbed = true; 158 | if(ch == '=' || ch == '-') return this._token(Token.EMBED, this._next()); 159 | else return this.nextToken(); 160 | } else if(tag == this._embedChar + '>') { 161 | this._inEmbed = false; 162 | return this.nextToken(); 163 | } else if(tag + ch == "-" + this._embedChar + '>') { 164 | this._inEmbed = false; 165 | this._stripNewLine = true; 166 | this._next(); 167 | return this.nextToken(); 168 | } 169 | 170 | else this._error('invalid embed token "'+ tag + '"'); 171 | } 172 | 173 | Lexer.prototype._punctuation = function() { 174 | return this._token(Token.PUNCTUATION, this._next()); 175 | } 176 | 177 | Lexer.prototype._number = function(prefix) { 178 | var hasE = false, afterE = false, hasX = false, hasDot = (prefix == "."); 179 | var num = this._readWhile(function(ch, i){ 180 | 181 | if (ch == "x" || ch == "X") { 182 | if (hasX) return false; 183 | return hasX = true; 184 | } 185 | 186 | if (!hasX && (ch == "E" || ch == "e")) { 187 | if (hasE) return false; 188 | return hasE = afterE = true; 189 | } 190 | 191 | if (ch == "-") { 192 | if (afterE || (i == 0 && !prefix)) return true; 193 | return false; 194 | } 195 | 196 | if (ch == "+") return afterE; 197 | 198 | afterE = false; 199 | if (ch == ".") { 200 | if (!hasDot && !hasX) 201 | return hasDot = true; 202 | return false; 203 | } 204 | 205 | return g.is_alphanumeric(ch); 206 | }); 207 | 208 | if (prefix) num = prefix + num; 209 | 210 | var valid = parseJSNumber(num); 211 | if (!isNaN(valid)) return this._token(Token.NUMBER, valid); 212 | else this._error("Invalid syntax: " + num); 213 | } 214 | 215 | Lexer.prototype._escapedChar = function() { 216 | var ch = this._next(true); 217 | switch (ch) { 218 | case "n" : return "\n"; 219 | case "r" : return "\r"; 220 | case "t" : return "\t"; 221 | case "b" : return "\b"; 222 | case "v" : return "\v"; 223 | case "f" : return "\f"; 224 | case "0" : return "\0"; 225 | case "x" : return String.fromCharCode(this._hexBytes(2)); 226 | case "u" : return String.fromCharCode(this._hexBytes(4)); 227 | default : return ch; 228 | } 229 | }; 230 | 231 | Lexer.prototype._hexBytes = function(n) { 232 | var num = 0; 233 | for (; n > 0; --n) { 234 | var digit = parseInt(this._next(true), 16); 235 | if (isNaN(digit)) 236 | this._error("Invalid hex-character pattern in string"); 237 | num = (num << 4) | digit; 238 | } 239 | return num; 240 | } 241 | 242 | Lexer.prototype._string = function() { 243 | var self = this; 244 | return this._withEofError("Unterminated string constant", function(){ 245 | var quote = self._next(), ret = ""; 246 | for (;;) { 247 | var ch = self._next(true); 248 | if (ch == "\\") ch = self._escapedChar(); 249 | else if (ch == quote) break; 250 | ret += ch; 251 | } 252 | return self._token(Token.STRING, ret); 253 | }); 254 | } 255 | 256 | Lexer.prototype._comment = function() { 257 | this._next(); 258 | 259 | var i = this._find("\n"), ret; 260 | if (i == -1) { 261 | ret = this.source.substr(this._pos); 262 | this._pos = this.source.length; 263 | } else { 264 | var j = this._find(this._embedChar + ">"); 265 | if (j > -1 && j < i) i = j; 266 | ret = this.source.substring(this._pos, i); 267 | this._pos = i; 268 | } 269 | return this._token(Token.COMMENT, ret, true); 270 | }; 271 | 272 | Lexer.prototype._commentBlock = function() { 273 | this._next(); 274 | 275 | var self = this; 276 | return this._withEofError("Unterminated multiline comment", function() { 277 | var i = self._find("*/", true) 278 | , text = self.source.substring(self._pos, i) 279 | , tok = self._token(Token.COMMENT_BLOCK, text, true); 280 | self._pos = i + 2; 281 | self._line += text.split("\n").length - 1; 282 | self._newlineBefore = text.indexOf("\n") >= 0; 283 | return tok; 284 | }); 285 | }; 286 | 287 | Lexer.prototype._name = function() { 288 | var backslash = false 289 | , name = "" 290 | , ch; 291 | 292 | while ((ch = this._peek()) != null) { 293 | if (!backslash) { 294 | if (ch == "\\") backslash = true, this._next(); 295 | else if (g.is_identifier_char(ch)) name += this._next(); 296 | else break; 297 | } else { 298 | if (ch != "u") this._error("Expecting UnicodeEscapeSequence -- uXXXX"); 299 | ch = this._escapedChar(); 300 | if (!g.is_identifier_char(ch)) this._error("Unicode char: " + ch.charCodeAt(0) + " is not valid in identifier"); 301 | name += ch; 302 | backslash = false; 303 | } 304 | } 305 | return name; 306 | }; 307 | 308 | Lexer.prototype._regexp = function() { 309 | var self = this; 310 | return this._withEofError("Unterminated regular expression", function() { 311 | var ch 312 | , regexp = "" 313 | , inClass = false 314 | , prevBackslash = false; 315 | 316 | while ((ch = self._next(true))) if (prevBackslash) { 317 | regexp += "\\" + ch; 318 | prevBackslash = false; 319 | } else if (ch == "[") { 320 | inClass = true; 321 | regexp += ch; 322 | } else if (ch == "]" && inClass) { 323 | inClass = false; 324 | regexp += ch; 325 | } else if (ch == "/" && !inClass) { 326 | break; 327 | } else if (ch == "\\") { 328 | prevBackslash = true; 329 | } else { 330 | regexp += ch; 331 | } 332 | var mods = self._name(); 333 | return self._token(Token.REGEXP, [ regexp, mods ]); 334 | }); 335 | }; 336 | 337 | Lexer.prototype._operator = function(prefix) { 338 | var self = this; 339 | function grow(op) { 340 | if (!self._peek()) return op; 341 | var bigger = op + self._peek(); 342 | if (g.is_operator(bigger)) { 343 | self._next(); 344 | return grow(bigger); 345 | } else return op; 346 | }; 347 | return this._token(Token.OPERATOR, grow(prefix || this._next())); 348 | }; 349 | 350 | Lexer.prototype._slash = function() { 351 | this._next(); 352 | var regexpAllowed = this._regexpAllowed; 353 | switch (this._peek()) { 354 | case "/": 355 | var comment = this._comment(); 356 | if(comment) this._commentsBefore.push(comment); 357 | this._regexpAllowed = regexpAllowed; 358 | return this.nextToken(); 359 | case "*": 360 | var comment = this._commentBlock(); 361 | if(comment) this._commentsBefore.push(comment); 362 | this._regexpAllowed = regexpAllowed; 363 | return this.nextToken(); 364 | } 365 | return this._regexpAllowed ? this._regexp() : this._operator("/"); 366 | }; 367 | 368 | Lexer.prototype._dot = function() { 369 | this._next(); 370 | return g.is_digit(this._peek()) ? this._number(".") : this._token(Token.PUNCTUATION, "."); 371 | }; 372 | 373 | Lexer.prototype._word = function() { 374 | var word = this._name(); 375 | if(g.is_keyword(word)) { 376 | if(g.is_operator(word)) 377 | return this._token(Token.OPERATOR, word) 378 | else if (g.is_keyword_atom(word)) 379 | return this._token(Token.ATOM, word) 380 | else 381 | return this._token(Token.KEYWORD, word); 382 | } else return this._token(Token.NAME, word); 383 | }; 384 | 385 | Lexer.prototype._withEofError = function(message, cont) { 386 | try { 387 | return cont(); 388 | } catch(ex) { 389 | if (ex === EX_EOF) this._error(message); 390 | else throw ex; 391 | } 392 | }; 393 | 394 | Lexer.prototype._token = function(type, value, isComment) { 395 | this._regexpAllowed = ((type == Token.OPERATOR && !g.is_unary_postfix(value)) || 396 | (type == Token.KEYWORD && g.is_keyword_before_expression(value)) || 397 | (type == Token.PUNCTUATION && g.is_punctuation_before_expression(value))); 398 | 399 | this._curToken = new Token(type, value, this._line, this._col, this._pos); 400 | 401 | if (!isComment && this._commentsBefore.length) { 402 | this._curToken.commentsBefore = this._commentsBefore; 403 | this._commentsBefore = []; 404 | } 405 | 406 | if(this._newLineBefore) { 407 | this._curToken.newLineBefore = this._newLineBefore; 408 | this._newLineBefore = false; 409 | } 410 | 411 | if(!this._curToken.isComment() || this._includeComments) 412 | this.tokens.push(this._curToken); 413 | 414 | return this._curToken; 415 | }; 416 | 417 | Lexer.prototype._error = function(message) { 418 | throw new Error(message); // should be ParseError 419 | }; 420 | 421 | var Token = module.exports.Token = function(type, value, line, col, pos) { 422 | if(typeof type == 'undefined') throw new Error('undefined token type'); 423 | 424 | this.type = type; 425 | this.name = Token[type]; 426 | this.value = value; 427 | this.line = line; 428 | this.col = col; 429 | this.pos = pos; 430 | }; 431 | 432 | Token.prototype.isComment = function() { 433 | return this.type == Token.COMMENT || this.type == Token.COMMENT_BLOCK; 434 | } 435 | 436 | Token.prototype.toString = function() { 437 | return '[' + this.type + ', ' + this.value + ']'; 438 | }; 439 | 440 | var tokenTypes = [ 441 | 'output' 442 | , 'embed' 443 | , 'operator' 444 | , 'keyword' 445 | , 'atom' 446 | , 'name' 447 | , 'punctuation' 448 | , 'string' 449 | , 'number' 450 | , 'regexp' 451 | , 'comment' 452 | , 'comment_block' 453 | , 'eof' 454 | ]; 455 | 456 | tokenTypes.forEach(function(name) { 457 | name = name.toUpperCase() 458 | Token[name] = 'T_' + name; 459 | Token[Token[name]] = name; 460 | }); 461 | 462 | var ParseError = module.exports.ParseError = function(message, line, col, pos) { 463 | this.message = message; 464 | this.line = line; 465 | this.col = col; 466 | this.pos = pos; 467 | try { 468 | ({})(); 469 | } catch(ex) { 470 | this.stack = ex.stack; 471 | }; 472 | }; 473 | 474 | /* utilities */ 475 | 476 | var EX_EOF = {}; 477 | 478 | var RE_HEX_NUMBER = /^0x[0-9a-f]+$/i; 479 | var RE_OCT_NUMBER = /^0[0-7]+$/; 480 | var RE_DEC_NUMBER = /^\d*\.?\d*(?:e[+-]?\d*(?:\d\.?|\.?\d)\d*)?$/i; 481 | 482 | function parseJSNumber(num) { 483 | if (RE_HEX_NUMBER.test(num)) { 484 | return parseInt(num.substr(2), 16); 485 | } else if (RE_OCT_NUMBER.test(num)) { 486 | return parseInt(num.substr(1), 8); 487 | } else if (RE_DEC_NUMBER.test(num)) { 488 | return parseFloat(num); 489 | } 490 | }; 491 | -------------------------------------------------------------------------------- /lib/template.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require('fs') 4 | , path = require('path') 5 | , util = require('./util') 6 | , EventEmitter = require('events').EventEmitter 7 | , Compiler = require('./compiler') 8 | , Cache = require('./cache') 9 | , streamData = require("stream-data") 10 | ; 11 | 12 | 13 | /** 14 | * Template 15 | * Creates a new `Template` instance by receiving the AJS source 16 | * and wrapping it in a function that gives it a new 17 | * VM to run in on each call. 18 | * 19 | * @name Template 20 | * @function 21 | * @param {String} source The template source. 22 | * @param {Object} opts The compile options. 23 | * @returns {Function} The template function. 24 | */ 25 | function Template (source, opts) { 26 | let fn = new Compiler(source, opts).compile(); 27 | return (locals, cb) => { 28 | return new VM(fn, opts).render(locals, cb); 29 | }; 30 | } 31 | 32 | /** 33 | * loadInclude 34 | * When we include templates from a running VM, we specificy the `bare` option 35 | * so the compiler doesn't wrap the template in a new VM. 36 | * 37 | * @name loadInclude 38 | * @function 39 | * @param {String} filename The filename to include. 40 | * @param {Objec} opts The compile options. 41 | * @returns {Template} The new template. 42 | */ 43 | Template.loadInclude = function (filename, opts) { 44 | opts = opts || {}; 45 | opts.bare = true; 46 | 47 | let template = null 48 | , cached = null 49 | , cache = (typeof opts.cache != 'undefined') 50 | ? opts.cache 51 | : true 52 | ; 53 | 54 | try { 55 | if (cache && (cached = Cache.getSync(filename))) { 56 | return cached; 57 | } else { 58 | opts.filename = filename; 59 | template = new Compiler(fs.readFileSync(filename, 'utf8'), opts).compile(); 60 | Cache.set(filename, template); 61 | return template; 62 | } 63 | } catch (e) { 64 | e.message = "In " + filename + ", " + e.message; 65 | throw e; 66 | } 67 | }; 68 | 69 | class VM extends EventEmitter { 70 | 71 | /** 72 | * VM 73 | * This is the AJS Virtual Machine. 74 | * In the VM we execute the compiled AJS code in a context that captures 75 | * and buffers output until callbacks return and we're ready to flush. 76 | * 77 | * @name VM 78 | * @function 79 | * @param {Function} func The function returned by the compiler. 80 | * @param {Object} opts An object containing the following fields: 81 | * 82 | * - `filename` (String): The path to the ajs template. 83 | * 84 | * @returns {VM} The `VM` instance. 85 | */ 86 | constructor (func, opts) { 87 | super(); 88 | this.filename = util.resolveFilename(opts.filename); 89 | this.dirname = util.resolveDirname(this.filename); 90 | 91 | this.line = 1; 92 | this.error = null; 93 | 94 | this._function = func; 95 | this._locals = null; 96 | this._vmContext_ = opts._vmContext_; 97 | if (this._vmContext_) { 98 | this._vmContext_.inc = this._include.bind(this); 99 | this._vmContext_.modInclude = this._moduleInclude.bind(this); 100 | } 101 | 102 | this._depth = 0; 103 | this._cbDepth = 0; 104 | this._cbs = []; 105 | this._inCb = null; 106 | this._cbBuffer = []; 107 | this._buffer = []; 108 | } 109 | 110 | /** 111 | * render 112 | * We delay the actual execution of the template function by a tick 113 | * to give us time to bind to the `data`, `error` and `end` events. 114 | * 115 | * @name render 116 | * @function 117 | * @param {Object} locals The template data. 118 | * @param {Function} cb The callback function. 119 | */ 120 | render (locals, cb) { 121 | 122 | if (cb) { 123 | streamData(this, (err, data) => { 124 | cb(err, data); 125 | }); 126 | } 127 | 128 | this._locals = locals || {}; 129 | process.nextTick(() => this._execute()); 130 | return this; 131 | } 132 | 133 | /** 134 | * _execute 135 | * We kick off the VM by calling the compiled template function, 136 | * passing it our own vm context (for writes and callback handling), 137 | * as well as the locals passed in for the current request. 138 | * 139 | * @name _execute 140 | * @function 141 | */ 142 | _execute () { 143 | this._depth++; 144 | this._function.call(this, this._vmContext(), this._runLocals()); 145 | } 146 | 147 | /** 148 | * _include 149 | * When you call `include` in a template, we use `Loader` to find 150 | * the appropriate template (using a cached copy if available), 151 | * pass it the context you provide, and execute it under this VM. 152 | * 153 | * @name _include 154 | * @function 155 | * @param {String} request The path to the included ajs file. 156 | * @param {Object} locals The template data. 157 | * @param {Object} pLocals The parent template data. 158 | */ 159 | _include (request, locals, pLocals) { 160 | if (!request.endsWith(".ajs")) { 161 | request += ".ajs"; 162 | } 163 | let filename = path.resolve(pLocals.__dirname, request) 164 | let template = null; 165 | if(filename == this.filename) throw new Error('self include'); 166 | 167 | try { 168 | template = Template.loadInclude(filename); 169 | } catch(e) { 170 | if(e.code == 'ENOENT' || e.code == 'EBADF') 171 | throw new Error("Can't find include: '" + request + "'"); 172 | else throw e; 173 | } 174 | 175 | let includeLocals = util.extend(this._runLocals(), locals || {}); 176 | 177 | this._depth++; 178 | template.call(this, this._vmContext(), util.extend({ 179 | __filename: template.filename 180 | , __dirname: template.dirname 181 | }, includeLocals, true)); 182 | } 183 | 184 | /** 185 | * _moduleInclude 186 | * Include an ajs template from node_modules 187 | * 188 | * @name _moduleInclude 189 | * @function 190 | * @param {String} request The path to the included ajs file. 191 | * @param {Object} locals The template data. 192 | * @param {Object} pLocals The parent template data. 193 | */ 194 | _moduleInclude (request, locals, pLocals) { 195 | 196 | if (!request.endsWith(".ajs")) { 197 | request += ".ajs"; 198 | } 199 | 200 | let filename = require.resolve(request) 201 | return this._include(filename, locals, pLocals) 202 | } 203 | 204 | /** 205 | * _render 206 | * Renders the template. 207 | * 208 | * @name _render 209 | * @function 210 | * @param {String} str The string to render. 211 | * @param {Object} locals The template data. 212 | * @param {Object} opts The compile options. 213 | */ 214 | _render (str, locals, opts) { 215 | opts = opts || {}; 216 | let template; 217 | if(opts.filename) { 218 | let key = JSON.stringify(opts.filename); 219 | if(!(template = Cache._store[key])) 220 | template = Cache._store[key] = new Template(str, opts); 221 | } else template = new Compiler(str, opts).compile(); 222 | 223 | let renderLocals = util.extend(this._runLocals(), locals || {}); 224 | 225 | this._depth++; 226 | template.call(this, this._vmContext(), renderLocals); 227 | } 228 | 229 | /** 230 | * _wrapCb 231 | * This is where the magic happens. The compiler wraps any arguments 232 | * that look like callbacks with this function, enabling us to keep 233 | * track of when a callback returns and when its completed. 234 | * 235 | * @name _wrapCb 236 | * @function 237 | * @param {Function} func The callback function. 238 | * @returns {Function} The wrapping function. 239 | */ 240 | _wrapCb (func) { 241 | if(typeof func != 'function') return func; 242 | 243 | if(this._inCb != null) throw new Error('nested callback'); 244 | 245 | let id = this._cbBuffer.length 246 | , cb = { data: [], done: false }; 247 | this._cbBuffer[id] = cb; 248 | this._cbDepth++; 249 | this._flush(); 250 | let self = this; 251 | return function() { 252 | self._cbStart(id); 253 | func.apply(this, arguments); 254 | self._cbEnd(id); 255 | }; 256 | } 257 | 258 | /** 259 | * _cbStart 260 | * Mark the callback state as *started*. 261 | * 262 | * @name _cbStart 263 | * @function 264 | * @param {String} id The callback id. 265 | */ 266 | _cbStart (id) { 267 | this._cbDepth--; 268 | this._cbBuffer[id].done = false; 269 | this._inCb = id; 270 | } 271 | 272 | /** 273 | * _cbEnd 274 | * Mark the callback state as *done*. 275 | * 276 | * @name _cbEnd 277 | * @function 278 | * @param {String} id The callback id. 279 | */ 280 | _cbEnd (id) { 281 | this._cbBuffer[id].done = true; 282 | this._inCb = null; 283 | this._write(); 284 | } 285 | 286 | /** 287 | * _write 288 | * Write data in the buffers. 289 | * 290 | * @name _write 291 | * @function 292 | * @param {String} data The data to write. 293 | */ 294 | _write (data) { 295 | let include; 296 | 297 | if(data) { 298 | // We're not waiting on any callbacks, so write directly to the main buffer. 299 | if(this._cbDepth == 0) return this._buffer.push(data); 300 | 301 | // If we're currently writing _inside_ a callback, we make sure to write 302 | // to its own buffer. Otherwise we write to the cb buffer so we stay in order. 303 | if(this._inCb != null) 304 | this._cbBuffer[this._inCb].data.push(data); 305 | else 306 | this._cbBuffer.push(data); 307 | } 308 | 309 | // Each time we write, check to see if any callbacks have been completed. 310 | // If so, we can dump its buffer into the main buffer and continue until 311 | // we hit the next incomplete callback. 312 | for (let i in this._cbBuffer) { 313 | let cb = null; 314 | if(typeof (cb = this._cbBuffer[i]).done != 'undefined') { 315 | if(cb.done) { 316 | if(cb.data.length) this._buffer.push(cb.data.join('')); 317 | } else return; 318 | } else { 319 | this._buffer.push(this._cbBuffer[i]); 320 | } 321 | delete this._cbBuffer[i]; 322 | } 323 | 324 | // We don't want to overload the socket with too many writes, so we only 325 | // flush on two occasions: (1) when we start waiting on a callback, and 326 | // (2) when the template has finished rendering. 327 | if(this.isComplete()) { 328 | this._flush(); 329 | } 330 | } 331 | 332 | /** 333 | * _flush 334 | * Ends the buffer writing and emits the *end* event. 335 | * 336 | * @name _flush 337 | * @function 338 | */ 339 | _flush () { 340 | // If there's anything in the buffer, emit a `data` event 341 | // with the contents of the buffer as a `String`. 342 | if(this._buffer.length) { 343 | this.emit('data', this._buffer.join('')); 344 | this._buffer = []; 345 | } 346 | 347 | // If we're done executing, emit an `end` event. 348 | if(this.isComplete()) { 349 | setTimeout(() => { 350 | this.emit('end'); 351 | }, 10); 352 | } 353 | } 354 | 355 | /** 356 | * _line 357 | * Our compiled AJS is instrumented with calls so we can keep track of 358 | * corresponding line numbers in the original AJS source. 359 | * 360 | * @name _line 361 | * @function 362 | * @param {String} i The line number. 363 | */ 364 | _line (i) { 365 | this.line = i; 366 | } 367 | 368 | /** 369 | * _error 370 | * When an error occurs during template execution, we handle it here so 371 | * we can add additional information and emit an `error` event. 372 | * 373 | * @name _error 374 | * @function 375 | * @param {Error} e The error object. 376 | */ 377 | _error (e, filename) { 378 | e.filename = filename; 379 | e.line = this.line; 380 | e.message = e.message + ' at (' + e.filename + ":" + e.line + ')'; 381 | this.error = e; 382 | this.emit('error', this.error); 383 | this._flush(); 384 | } 385 | 386 | /** 387 | * _end 388 | * All templates call `_end()` when they're finished. Since some templates 389 | * are actually nested `include`s, we use `_depth` to let us know when 390 | * we're actually done. 391 | * 392 | * @name _end 393 | * @function 394 | */ 395 | _end () { 396 | this._depth--; 397 | this._write(); 398 | } 399 | 400 | /** 401 | * isComplete 402 | * We know we're done when the parent template is complete and all callbacks have returned. 403 | * 404 | * @name isComplete 405 | * @function 406 | * @returns {Boolean} `true` if the rendering is complete, `false` otherwise. 407 | */ 408 | isComplete () { 409 | return this.error || (this._depth == 0 && this._cbDepth == 0); 410 | } 411 | 412 | /** 413 | * _vmContext 414 | * Here we build the actual VM context that's passed to the compiled AJS code. 415 | * The calls to these functions are added automatically by the compiler. 416 | * 417 | * @name _vmContext 418 | * @function 419 | * @returns {Object} The vm context. 420 | */ 421 | _vmContext () { 422 | if(this._vmContext_) return this._vmContext_; 423 | 424 | this._vmContext_ = { 425 | cb: this._wrapCb.bind(this) 426 | , out: this._write.bind(this) 427 | , ln: this._line.bind(this) 428 | , end: this._end.bind(this) 429 | , err: this._error.bind(this) 430 | , inc: this._include.bind(this) 431 | , modInclude: this._moduleInclude.bind(this) 432 | , ren: this._render.bind(this) 433 | , flush: this._flush.bind(this) 434 | , esc: util.escape 435 | }; 436 | 437 | return this._vmContext_; 438 | } 439 | 440 | /** 441 | * _runLocals 442 | * Here we extend the context passed into the template by you, 443 | * adding a few helpful global properties along the way. 444 | * 445 | * @name _runLocals 446 | * @function 447 | * @returns {Object} The locals object. 448 | */ 449 | _runLocals () { 450 | if(this._runLocals_) return this._runLocals_; 451 | 452 | this._runLocals_ = util.extend({}, this._locals); 453 | 454 | this._runLocals_.__filename = this.filename; 455 | this._runLocals_.__dirname = this.dirname; 456 | 457 | return this._runLocals_; 458 | } 459 | } 460 | 461 | VM.ROOT = process.cwd() 462 | Template.VM = VM 463 | 464 | module.exports = Template; 465 | -------------------------------------------------------------------------------- /lib/grammar.js: -------------------------------------------------------------------------------- 1 | // Thanks to mishoo/uglifyjs for most of this! 2 | 3 | // [« Back to Index](index.html) 4 | 5 | // The lexer and parsers refer here to determine how 6 | // to interpret AJS source code. Most of it is identical to JS grammer. 7 | var g = exports.grammar = { 8 | 9 | WHITESPACE: chars_to_hash(" \u00a0\n\r\t\f\v\u200b") 10 | 11 | , UNICODE: { // regexps adapted from http://xregexp.com/plugins/#unicode 12 | letter: new RegExp("[\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0523\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0621-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971\\u0972\\u097B-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D28\\u0D2A-\\u0D39\\u0D3D\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC\\u0EDD\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8B\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10D0-\\u10FA\\u10FC\\u1100-\\u1159\\u115F-\\u11A2\\u11A8-\\u11F9\\u1200-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u1676\\u1681-\\u169A\\u16A0-\\u16EA\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19A9\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u2094\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2C6F\\u2C71-\\u2C7D\\u2C80-\\u2CE4\\u2D00-\\u2D25\\u2D30-\\u2D65\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31B7\\u31F0-\\u31FF\\u3400\\u4DB5\\u4E00\\u9FC3\\uA000-\\uA48C\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA65F\\uA662-\\uA66E\\uA67F-\\uA697\\uA717-\\uA71F\\uA722-\\uA788\\uA78B\\uA78C\\uA7FB-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA90A-\\uA925\\uA930-\\uA946\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAC00\\uD7A3\\uF900-\\uFA2D\\uFA30-\\uFA6A\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC]") 13 | , non_spacing_mark: new RegExp("[\\u0300-\\u036F\\u0483-\\u0487\\u0591-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065E\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u0711\\u0730-\\u074A\\u07A6-\\u07B0\\u07EB-\\u07F3\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u082D\\u0900-\\u0902\\u093C\\u0941-\\u0948\\u094D\\u0951-\\u0955\\u0962\\u0963\\u0981\\u09BC\\u09C1-\\u09C4\\u09CD\\u09E2\\u09E3\\u0A01\\u0A02\\u0A3C\\u0A41\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A51\\u0A70\\u0A71\\u0A75\\u0A81\\u0A82\\u0ABC\\u0AC1-\\u0AC5\\u0AC7\\u0AC8\\u0ACD\\u0AE2\\u0AE3\\u0B01\\u0B3C\\u0B3F\\u0B41-\\u0B44\\u0B4D\\u0B56\\u0B62\\u0B63\\u0B82\\u0BC0\\u0BCD\\u0C3E-\\u0C40\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C62\\u0C63\\u0CBC\\u0CBF\\u0CC6\\u0CCC\\u0CCD\\u0CE2\\u0CE3\\u0D41-\\u0D44\\u0D4D\\u0D62\\u0D63\\u0DCA\\u0DD2-\\u0DD4\\u0DD6\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EB9\\u0EBB\\u0EBC\\u0EC8-\\u0ECD\\u0F18\\u0F19\\u0F35\\u0F37\\u0F39\\u0F71-\\u0F7E\\u0F80-\\u0F84\\u0F86\\u0F87\\u0F90-\\u0F97\\u0F99-\\u0FBC\\u0FC6\\u102D-\\u1030\\u1032-\\u1037\\u1039\\u103A\\u103D\\u103E\\u1058\\u1059\\u105E-\\u1060\\u1071-\\u1074\\u1082\\u1085\\u1086\\u108D\\u109D\\u135F\\u1712-\\u1714\\u1732-\\u1734\\u1752\\u1753\\u1772\\u1773\\u17B7-\\u17BD\\u17C6\\u17C9-\\u17D3\\u17DD\\u180B-\\u180D\\u18A9\\u1920-\\u1922\\u1927\\u1928\\u1932\\u1939-\\u193B\\u1A17\\u1A18\\u1A56\\u1A58-\\u1A5E\\u1A60\\u1A62\\u1A65-\\u1A6C\\u1A73-\\u1A7C\\u1A7F\\u1B00-\\u1B03\\u1B34\\u1B36-\\u1B3A\\u1B3C\\u1B42\\u1B6B-\\u1B73\\u1B80\\u1B81\\u1BA2-\\u1BA5\\u1BA8\\u1BA9\\u1C2C-\\u1C33\\u1C36\\u1C37\\u1CD0-\\u1CD2\\u1CD4-\\u1CE0\\u1CE2-\\u1CE8\\u1CED\\u1DC0-\\u1DE6\\u1DFD-\\u1DFF\\u20D0-\\u20DC\\u20E1\\u20E5-\\u20F0\\u2CEF-\\u2CF1\\u2DE0-\\u2DFF\\u302A-\\u302F\\u3099\\u309A\\uA66F\\uA67C\\uA67D\\uA6F0\\uA6F1\\uA802\\uA806\\uA80B\\uA825\\uA826\\uA8C4\\uA8E0-\\uA8F1\\uA926-\\uA92D\\uA947-\\uA951\\uA980-\\uA982\\uA9B3\\uA9B6-\\uA9B9\\uA9BC\\uAA29-\\uAA2E\\uAA31\\uAA32\\uAA35\\uAA36\\uAA43\\uAA4C\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uABE5\\uABE8\\uABED\\uFB1E\\uFE00-\\uFE0F\\uFE20-\\uFE26]") 14 | , space_combining_mark: new RegExp("[\\u0903\\u093E-\\u0940\\u0949-\\u094C\\u094E\\u0982\\u0983\\u09BE-\\u09C0\\u09C7\\u09C8\\u09CB\\u09CC\\u09D7\\u0A03\\u0A3E-\\u0A40\\u0A83\\u0ABE-\\u0AC0\\u0AC9\\u0ACB\\u0ACC\\u0B02\\u0B03\\u0B3E\\u0B40\\u0B47\\u0B48\\u0B4B\\u0B4C\\u0B57\\u0BBE\\u0BBF\\u0BC1\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCC\\u0BD7\\u0C01-\\u0C03\\u0C41-\\u0C44\\u0C82\\u0C83\\u0CBE\\u0CC0-\\u0CC4\\u0CC7\\u0CC8\\u0CCA\\u0CCB\\u0CD5\\u0CD6\\u0D02\\u0D03\\u0D3E-\\u0D40\\u0D46-\\u0D48\\u0D4A-\\u0D4C\\u0D57\\u0D82\\u0D83\\u0DCF-\\u0DD1\\u0DD8-\\u0DDF\\u0DF2\\u0DF3\\u0F3E\\u0F3F\\u0F7F\\u102B\\u102C\\u1031\\u1038\\u103B\\u103C\\u1056\\u1057\\u1062-\\u1064\\u1067-\\u106D\\u1083\\u1084\\u1087-\\u108C\\u108F\\u109A-\\u109C\\u17B6\\u17BE-\\u17C5\\u17C7\\u17C8\\u1923-\\u1926\\u1929-\\u192B\\u1930\\u1931\\u1933-\\u1938\\u19B0-\\u19C0\\u19C8\\u19C9\\u1A19-\\u1A1B\\u1A55\\u1A57\\u1A61\\u1A63\\u1A64\\u1A6D-\\u1A72\\u1B04\\u1B35\\u1B3B\\u1B3D-\\u1B41\\u1B43\\u1B44\\u1B82\\u1BA1\\u1BA6\\u1BA7\\u1BAA\\u1C24-\\u1C2B\\u1C34\\u1C35\\u1CE1\\u1CF2\\uA823\\uA824\\uA827\\uA880\\uA881\\uA8B4-\\uA8C3\\uA952\\uA953\\uA983\\uA9B4\\uA9B5\\uA9BA\\uA9BB\\uA9BD-\\uA9C0\\uAA2F\\uAA30\\uAA33\\uAA34\\uAA4D\\uAA7B\\uABE3\\uABE4\\uABE6\\uABE7\\uABE9\\uABEA\\uABEC]") 15 | , connector_punc: new RegExp("[\\u005F\\u203F\\u2040\\u2054\\uFE33\\uFE34\\uFE4D-\\uFE4F\\uFF3F]") 16 | } 17 | 18 | , KEYWORDS: array_to_hash([ 19 | "break" 20 | , "case" 21 | , "catch" 22 | , "const" 23 | , "continue" 24 | , "default" 25 | , "delete" 26 | , "do" 27 | , "else" 28 | , "finally" 29 | , "for" 30 | , "function" 31 | , "if" 32 | , "in" 33 | , "instanceof" 34 | , "new" 35 | , "return" 36 | , "switch" 37 | , "throw" 38 | , "try" 39 | , "typeof" 40 | , "var" 41 | , "void" 42 | , "while" 43 | , "with" 44 | ]) 45 | 46 | , KEYWORDS_ATOM: array_to_hash([ 47 | "false" 48 | , "null" 49 | , "true" 50 | , "undefined" 51 | ]) 52 | 53 | , RESERVED_WORDS: array_to_hash([ 54 | "abstract" 55 | , "boolean" 56 | , "byte" 57 | , "char" 58 | , "class" 59 | , "debugger" 60 | , "double" 61 | , "enum" 62 | , "export" 63 | , "extends" 64 | , "final" 65 | , "float" 66 | , "goto" 67 | , "implements" 68 | , "import" 69 | , "int" 70 | , "interface" 71 | , "long" 72 | , "native" 73 | , "package" 74 | , "private" 75 | , "protected" 76 | , "public" 77 | , "short" 78 | , "static" 79 | , "super" 80 | , "synchronized" 81 | , "throws" 82 | , "transient" 83 | , "volatile" 84 | ]) 85 | 86 | , KEYWORDS_BEFORE_EXPRESSION: array_to_hash([ 87 | "return" 88 | , "new" 89 | , "delete" 90 | , "throw" 91 | , "else" 92 | , "case" 93 | ]) 94 | 95 | , OPERATOR_START: chars_to_hash("+-*&%=<>!?|~^") // OPERATOR_CHARS 96 | 97 | , OPERATORS: array_to_hash([ 98 | "in" 99 | , "instanceof" 100 | , "typeof" 101 | , "new" 102 | , "void" 103 | , "delete" 104 | , "++" 105 | , "--" 106 | , "+" 107 | , "-" 108 | , "!" 109 | , "~" 110 | , "&" 111 | , "|" 112 | , "^" 113 | , "*" 114 | , "/" 115 | , "%" 116 | , ">>" 117 | , "<<" 118 | , ">>>" 119 | , "<" 120 | , ">" 121 | , "<=" 122 | , ">=" 123 | , "==" 124 | , "===" 125 | , "!=" 126 | , "!==" 127 | , "?" 128 | , "=" 129 | , "+=" 130 | , "-=" 131 | , "/=" 132 | , "*=" 133 | , "%=" 134 | , ">>=" 135 | , "<<=" 136 | , ">>>=" 137 | , "|=" 138 | , "^=" 139 | , "&=" 140 | , "&&" 141 | , "||" 142 | ]) 143 | 144 | , UNARY_PREFIX: array_to_hash([ 145 | "typeof" 146 | , "void" 147 | , "delete" 148 | , "--" 149 | , "++" 150 | , "!" 151 | , "~" 152 | , "-" 153 | , "+" 154 | ]) 155 | 156 | , UNARY_POSTFIX: array_to_hash([ "--", "++" ]) 157 | 158 | , ASSIGNMENTS: (function(a, ret, i){ 159 | while (i < a.length) { 160 | ret[a[i]] = a[i].substr(0, a[i].length - 1); 161 | i++; 162 | } 163 | return ret; 164 | })( 165 | ["+=", "-=", "/=", "*=", "%=", ">>=", "<<=", ">>>=", "|=", "^=", "&="], 166 | { "=": true }, 167 | 0 168 | ) 169 | 170 | , PRECEDENCE: (function(a, ret){ 171 | for (var i = 0, n = 1; i < a.length; ++i, ++n) { 172 | var b = a[i]; 173 | for (var j = 0; j < b.length; ++j) { 174 | ret[b[j]] = n; 175 | } 176 | } 177 | return ret; 178 | })( 179 | [ 180 | ["||"], 181 | ["&&"], 182 | ["|"], 183 | ["^"], 184 | ["&"], 185 | ["==", "===", "!=", "!=="], 186 | ["<", ">", "<=", ">=", "in", "instanceof"], 187 | [">>", "<<", ">>>"], 188 | ["+", "-"], 189 | ["*", "/", "%"] 190 | ], 191 | {} 192 | ) 193 | 194 | , ATOMIC_START_TOKENS: array_to_hash([ "T_ATOM", "T_NUMBER", "T_STRING", "T_REGEXP", "T_NAME" ]) 195 | 196 | , STATEMENTS_WITH_LABELS: array_to_hash([ "N_FOR", "N_DO", "N_WHILE", "N_SWITCH" ]) 197 | 198 | , PUNCTUATION: chars_to_hash("[]{}(),;:") 199 | , PUNCTUATION_BEFORE_EXPRESSION: chars_to_hash("[{}(,.;:") 200 | 201 | , REGEXP_MODIFIERS: chars_to_hash("gmsiy") 202 | 203 | , SYNCRONOUS_CALLS: array_to_hash([ 204 | "forEach" 205 | , "map" 206 | , "reduce" 207 | , "reduceRight" 208 | , "filter" 209 | , "every" 210 | , "some" 211 | ]) 212 | } 213 | 214 | /* exports */ 215 | 216 | exports.is_whitespace = function(ch) { 217 | return (ch in g.WHITESPACE); 218 | } 219 | 220 | exports.is_digit = function(ch) { 221 | ch = ch.charCodeAt(0); // XXX: find out if "UnicodeDigit" means something else than 0..9 222 | return ch >= 48 && ch <= 57; 223 | } 224 | 225 | exports.is_letter = function(ch) { 226 | return g.UNICODE.letter.test(ch); 227 | }; 228 | 229 | exports.is_alphanumeric = function(ch) { 230 | return exports.is_letter(ch) || exports.is_digit(ch); 231 | }; 232 | 233 | exports.is_unicode_combining_mark = function(ch) { 234 | return g.UNICODE.non_spacing_mark.test(ch) || g.UNICODE.space_combining_mark.test(ch); 235 | }; 236 | 237 | exports.is_unicode_connector_punc = function(ch) { 238 | return g.UNICODE.connector_punc.test(ch); 239 | }; 240 | 241 | exports.is_identifier = function(word) { 242 | return /^[a-z_$][a-z0-9_$]*$/i.test(word) 243 | && word != "this" 244 | && !exports.is_keyword_atom(word) 245 | && !exports.is_reserved_word(word) 246 | && !exports.is_keyword(word); 247 | }; 248 | 249 | exports.is_identifier_start = function(ch) { 250 | return ch == "$" || ch == "_" || exports.is_letter(ch); 251 | }; 252 | 253 | exports.is_identifier_char = function(ch) { 254 | return exports.is_identifier_start(ch) 255 | || exports.is_digit(ch) 256 | || exports.is_unicode_combining_mark(ch) 257 | || exports.is_unicode_connector_punc(ch) 258 | || ch == "\u200c" // zero-width non-joiner 259 | || ch == "\u200d" // zero-width joiner (in my ECMA-262 PDF, this is also 200c) 260 | ; 261 | }; 262 | 263 | exports.is_keyword = function (word) { 264 | return g.KEYWORDS.hasOwnProperty(word); 265 | }; 266 | 267 | exports.is_keyword_atom = function(word) { 268 | return g.KEYWORDS_ATOM.hasOwnProperty(word); 269 | }; 270 | 271 | exports.is_atomic_start_token = function(name) { 272 | return g.ATOMIC_START_TOKENS.hasOwnProperty(name); 273 | } 274 | 275 | exports.is_keyword_before_expression = function(word) { 276 | return g.KEYWORDS_BEFORE_EXPRESSION.hasOwnProperty(word); 277 | }; 278 | 279 | exports.is_reserved_word = function(word) { 280 | return g.RESERVED_WORDS.hasOwnProperty(word); 281 | }; 282 | 283 | exports.is_operator = function(word) { 284 | return g.OPERATORS.hasOwnProperty(word); 285 | }; 286 | 287 | exports.is_operator_start = function(ch) { 288 | return (ch in g.OPERATOR_START); 289 | }; 290 | 291 | exports.is_punctuation = function(ch) { 292 | return (ch in g.PUNCTUATION); 293 | }; 294 | 295 | exports.is_punctuation_before_expression = function(ch) { 296 | return (ch in g.PUNCTUATION_BEFORE_EXPRESSION); 297 | }; 298 | 299 | exports.is_unary_prefix = function(ch) { 300 | return (ch in g.UNARY_PREFIX); 301 | }; 302 | 303 | exports.is_unary_postfix = function(ch) { 304 | return (ch in g.UNARY_POSTFIX); 305 | }; 306 | 307 | exports.is_regexp_modifier = function(ch) { 308 | return (ch in g.REGEXP_MODIFIERS); 309 | }; 310 | 311 | exports.precedence = function(op) { 312 | return g.PRECEDENCE[op]; 313 | } 314 | 315 | exports.is_assignment = function(op) { 316 | return g.ASSIGNMENTS.hasOwnProperty(op); 317 | } 318 | 319 | exports.assignment = function(op) { 320 | return g.ASSIGNMENTS[op]; 321 | } 322 | 323 | exports.is_syncronous_call = function(name) { 324 | return g.SYNCRONOUS_CALLS.hasOwnProperty(name); 325 | } 326 | 327 | /* utilities */ 328 | 329 | function array_to_hash(a) { 330 | var ret = {}; 331 | for (var i = 0; i < a.length; ++i) 332 | ret[a[i]] = true; 333 | return ret; 334 | }; 335 | 336 | exports.array_to_hash = array_to_hash; 337 | 338 | function chars_to_hash(str) { 339 | return array_to_hash(str.split("")); 340 | }; 341 | 342 | -------------------------------------------------------------------------------- /lib/compiler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Thanks to mishoo/uglifyjs for most of this! 4 | 5 | // [« Back to Index](index.html) 6 | 7 | var util = require('util') 8 | , g = require('./grammar') 9 | , Lexer = require('./lexer') 10 | , Parser = require('./parser') 11 | , Node = Parser.Node 12 | , path = require("path") 13 | 14 | var Compiler = module.exports = function Compiler(source, opts) { 15 | opts = opts || {}; 16 | source = source.replace(/\r/g, ''); 17 | 18 | this._onlyTree = (typeof opts.tree != 'undefined') ? opts.tree : false; 19 | this._onlySource = (typeof opts.source != 'undefined') ? opts.source : false; 20 | this._bareFunc = (typeof opts.bare != 'undefined') ? opts.bare : false; 21 | this._optimize = false; 22 | 23 | this._cbFunc = "__ajs.cb"; 24 | this._outFunc = "__ajs.out"; 25 | this._escFunc = "__ajs.esc"; 26 | this._lineFunc = "__ajs.ln"; 27 | this._endFunc = "__ajs.end"; 28 | this._errFunc = "__ajs.err"; 29 | 30 | this.filename = opts.filename; 31 | this.dirname = path.dirname(this.filename); 32 | this.source = source; 33 | this.line = 1; 34 | this.lineCount = 1; 35 | this.stack = []; 36 | this.compiled = ""; 37 | } 38 | 39 | Compiler.prototype.compile = function() { 40 | var lexer = new Lexer(this.source, {includeComments: false}) 41 | , self = this; 42 | 43 | this.tree = new Parser(lexer).parse(); 44 | this.lineCount = lexer._line; 45 | 46 | if(this._onlyTree) return this.tree; 47 | 48 | this.compiled = "var include = function (request, customLocals) {\n" + 49 | " __ajs.inc(request, customLocals, __locals)\n" + 50 | " }" + 51 | " , mod = function (request, customLocals) {\n" + 52 | " __ajs.modInclude(request, customLocals, __locals)\n" + 53 | " }" + 54 | " , render = __ajs.ren\n" + 55 | " , print = __ajs.out;\n" + 56 | "with(__locals) {\n" + 57 | "try {\n" + 58 | this._make(this.tree) + ";\n" + this._endFunc + "();\n" + 59 | "} catch(e) { " + this._errFunc +"(e, __filename) }\n" + 60 | "}"; 61 | 62 | var fn = new Function('__ajs, __locals', this.compiled); 63 | 64 | if(this._onlySource) return fn.toString(); 65 | fn.filename = this.filename; 66 | fn.dirname = this.dirname; 67 | 68 | return fn; 69 | } 70 | 71 | Compiler.prototype._make = function(node) { 72 | var type = node.type; 73 | var gen = this[type]; 74 | if (!gen) throw new Error("Can't find generator for \"" + type + "\""); 75 | this.stack.push(node); 76 | var compiled = this[type].apply(this, node.children.slice()); 77 | this.stack.pop(); 78 | return compiled; 79 | } 80 | 81 | var proto = Compiler.prototype; 82 | 83 | proto[Node.ROOT] = function(statements) { 84 | return this._blockStatements(statements).join("; \n"); 85 | }; 86 | 87 | proto[Node.OUTPUT] = function(output) { 88 | return this._format([this._outFunc + "('" + formatOutput(output).replace(/[\n]+/g, '\\n') + "')"]); 89 | }; 90 | 91 | proto[Node.ESCAPED] = function(statement) { 92 | return this._format([this._escFunc + "(" + formatEmbed(this[Node.STATEMENT](statement)) + ")"]); 93 | }; 94 | 95 | proto[Node.EMBED] = function(statement) { 96 | return this._format([this._outFunc + "(" + this._escFunc + "(" + formatEmbed(this[Node.STATEMENT](statement)) + "))"]); 97 | }; 98 | 99 | proto[Node.EMBED_RAW] = function(statement) { 100 | return this._format([this._outFunc + "(" + formatEmbed(this[Node.STATEMENT](statement)) + ")"]); 101 | }; 102 | 103 | proto[Node.BLOCK] = function(statements) { 104 | if (!statements) return ";"; 105 | if (statements.length == 0) return "{}"; 106 | var statements = this._optimize ? this._optimizeOutputNodes(statements) : statements; 107 | return "{" + this._blockStatements(statements).join('; ') + "}"; 108 | }; 109 | 110 | proto[Node.STRING] = function(str) { 111 | var dq = 0, sq = 0; 112 | str = str.replace(/[\\\b\f\n\r\t\x22\x27\u2028\u2029]/g, function(s){ 113 | switch (s) { 114 | case "\\": return "\\\\"; 115 | case "\b": return "\\b"; 116 | case "\f": return "\\f"; 117 | case "\n": return "\\n"; 118 | case "\r": return "\\r"; 119 | case "\t": return "\\t"; 120 | case "\u2028": return "\\u2028"; 121 | case "\u2029": return "\\u2029"; 122 | case '"': ++dq; return '"'; 123 | case "'": ++sq; return "'"; 124 | } 125 | return s; 126 | }); 127 | if (this.asciiOnly) str = toAscii(str); 128 | if (dq > sq) return "'" + str.replace(/\x27/g, "\\'") + "'"; 129 | else return '"' + str.replace(/\x22/g, '\\"') + '"'; 130 | } 131 | 132 | proto[Node.NUMBER] = function(num) { 133 | var str = num.toString(10), a = [ str.replace(/^0\./, ".") ], m; 134 | if (Math.floor(num) === num) { 135 | a.push("0x" + num.toString(16).toLowerCase(), // probably pointless 136 | "0" + num.toString(8)); // same. 137 | if ((m = /^(.*?)(0+)$/.exec(num))) { 138 | a.push(m[1] + "e" + m[2].length); 139 | } 140 | } else if ((m = /^0?\.(0+)(.*)$/.exec(num))) { 141 | a.push(m[2] + "e-" + (m[1].length + m[2].length), 142 | str.substr(str.indexOf("."))); 143 | } 144 | return bestOf(a); 145 | } 146 | 147 | proto[Node.NAME] = function(name) { 148 | name = name.toString(); 149 | if (this.asciiOnly) name = toAscii(name); 150 | return name; 151 | } 152 | 153 | proto[Node.VAR] = function(defs) { 154 | return "var " + addCommas(MAP(defs, this._make.bind(this))) + ";"; 155 | } 156 | 157 | proto[Node.VAR_DEF] = function(name, val) { 158 | if (val != null) name = this._format([ this[Node.NAME](name), "=", this._parenthesize(val, Node.SEQUENCE) ]); 159 | return name; 160 | } 161 | 162 | proto[Node.CONST] = function(defs) { 163 | return "const " + addCommas(MAP(defs, this._make.bind(this))) + ";"; 164 | } 165 | 166 | proto[Node.TRY] = function(tr, ca, fi) { 167 | var out = [ "try", this[Node.BLOCK](tr) ]; 168 | if (ca) out.push("catch", "(" + ca[0] + ")", this[Node.BLOCK](ca[1])); 169 | if (fi) out.push("finally", this[Node.BLOCK](fi)); 170 | return this._format(out); 171 | } 172 | 173 | proto[Node.THROW] = function(expr) { 174 | return this._format([ "throw", this._make(expr) ]) + ";"; 175 | } 176 | 177 | proto[Node.NEW] = function(ctor, args) { 178 | args = args.length > 0 ? "(" + addCommas(MAP(args, this._make.bind(this))) + ")" : ""; 179 | return this._format([ "new", this._parenthesize(ctor, Node.SEQUENCE, Node.BINARY, Node.TERNARY, Node.ASSIGN, function(expr){ 180 | var w = this._astWalker(), has_call = {}; 181 | try { 182 | w.withWalkers({ 183 | "N_CALL": function() { throw has_call }, 184 | "N_FUNCTION": function() { return this } 185 | }, function(){ w.walk(expr); }); 186 | } catch(ex) { 187 | if (ex === has_call) 188 | return true; 189 | throw ex; 190 | } 191 | }) + args ]); 192 | } 193 | 194 | proto[Node.SWITCH] = function(expr, body) { 195 | return this._format([ "switch", "(" + this._make(expr) + ")", this._switchBlock(body) ]); 196 | } 197 | 198 | proto[Node.BREAK] = function(label) { 199 | var out = "break"; 200 | if (label != null) 201 | out += " " + this[Node.NAME](label); 202 | return out + ";"; 203 | } 204 | 205 | proto[Node.CONTINUE] = function(label) { 206 | var out = "continue"; 207 | if (label != null) 208 | out += " " + this[Node.NAME](label); 209 | return out + ";"; 210 | } 211 | 212 | proto[Node.TERNARY] = function(co, th, el) { 213 | return this._format([ this._parenthesize(co, Node.ASSIGN, Node.SEQUENCE, Node.TERNARY), "?", 214 | this._parenthesize(th, Node.SEQUENCE), ":", 215 | this._parenthesize(el, Node.SEQUENCE) ]); 216 | } 217 | 218 | proto[Node.ASSIGN] = function(op, lvalue, rvalue) { 219 | if (op && op !== true) op += "="; 220 | else op = "="; 221 | return this._format([ this._make(lvalue), op, this._parenthesize(rvalue, Node.SEQUENCE) ]); 222 | } 223 | 224 | proto[Node.DOT] = function(expr) { 225 | var out = this._make(expr), i = 1; 226 | if (expr.type == Node.NUMBER) { 227 | if (!/\./.test(expr.children[0])) 228 | out += "."; 229 | } else if (this._needsParens(expr)) 230 | out = "(" + out + ")"; 231 | while (i < arguments.length) 232 | out += "." + this[Node.NAME](arguments[i++]); 233 | return out; 234 | } 235 | 236 | proto[Node.CALL] = function(func, args, node) { 237 | var f = this._make(func); 238 | if (this._needsParens(func)) 239 | f = "(" + f + ")"; 240 | var self = this; 241 | return f + "(" + addCommas(MAP(args, function(expr){ 242 | var val = self._parenthesize(expr, Node.SEQUENCE); 243 | if(expr.type == Node.NAME || (expr.type == Node.FUNCTION && needsCbWrap(func))) { 244 | val = self._wrapCb(val); 245 | } 246 | return val; 247 | })) + ")"; 248 | } 249 | 250 | proto[Node.FUNCTION] = proto[Node.DEFUN] = function(name, args, body, keyword) { 251 | var out = keyword || "function"; 252 | if (name) out += " " + this[Node.NAME](name); 253 | out += "(" + addCommas(MAP(args, this[Node.NAME])) + ")"; 254 | return this._format([ out, this[Node.BLOCK](body) ]); 255 | } 256 | 257 | proto[Node.IF] = function(co, th, el) { 258 | var out = [ "if", "(" + this._make(co) + ")", el ? this._then(th) : this._make(th) ]; 259 | if (el) { 260 | out.push("else", this._make(el)); 261 | } 262 | return this._format(out); 263 | } 264 | 265 | proto[Node.FOR] = function(init, cond, step, block) { 266 | var out = [ "for" ]; 267 | init = (init != null ? this._make(init) : "").replace(/;*\s*$/, ";"); 268 | cond = (cond != null ? this._make(cond) : "").replace(/;*\s*$/, ";"); 269 | step = (step != null ? this._make(step) : "").replace(/;*\s*$/, ""); 270 | var args = init + cond + step; 271 | if (args == "; ; ") args = ";;"; 272 | out.push("(" + args + ")", this._make(block)); 273 | return this._format(out); 274 | } 275 | 276 | proto[Node.FOR_IN] = function(vvar, key, hash, block) { 277 | return this._format([ "for", "(" + 278 | (vvar ? this._make(vvar).replace(/;+$/, "") : this._make(key)), 279 | "in", 280 | this._make(hash) + ")", this._make(block) ]); 281 | } 282 | 283 | proto[Node.WHILE] = function(condition, block) { 284 | return this._format([ "while", "(" + this._make(condition) + ")", this._make(block) ]); 285 | } 286 | 287 | proto[Node.DO] = function(condition, block) { 288 | return this._format([ "do", this._make(block), "while", "(" + this._make(condition) + ")" ]) + ";"; 289 | } 290 | 291 | proto[Node.RETURN] = function(expr) { 292 | var out = [ "return" ]; 293 | if (expr != null) out.push(this._make(expr)); 294 | return this._format(out) + ";"; 295 | } 296 | 297 | proto[Node.BINARY] = function(op, lvalue, rvalue) { 298 | var left = this._make(lvalue), right = this._make(rvalue); 299 | if (member(lvalue.type, [ Node.ASSIGN, Node.TERNARY, Node.SEQUENCE ]) || 300 | lvalue.type == Node.BINARY && g.precedence(op) > g.precedence(lvalue.children[0])) { 301 | left = "(" + left + ")"; 302 | } 303 | if (member(rvalue.type, [ Node.ASSIGN, Node.TERNARY, Node.SEQUENCE ]) || 304 | rvalue.type == Node.BINARY && g.precedence(op) >= g.precedence(rvalue.children[0]) && 305 | !(rvalue.children[0] == op && member(op, [ "&&", "||", "*" ]))) { 306 | right = "(" + right + ")"; 307 | } 308 | return this._format([ left, op, right ]); 309 | } 310 | 311 | proto[Node.UNARY_PREFIX] = function(op, expr) { 312 | var val = this._make(expr); 313 | if (!(expr.type == Node.NUMBER || (expr.type == Node.UNARY_PREFIX && !g.is_operator(op + expr.children[0])) || !this._needsParens(expr))) 314 | val = "(" + val + ")"; 315 | return op + (g.is_alphanumeric(op.charAt(0)) ? " " : "") + val; 316 | } 317 | 318 | proto[Node.UNARY_POSTFIX] = function(op, expr) { 319 | var val = this._make(expr); 320 | if (!(expr.type == Node.NUMBER || (expr.type == Node.UNARY_POSTFIX && !g.is_operator(op + expr.children[0])) || !this._needsParens(expr))) 321 | val = "(" + val + ")"; 322 | return val + op; 323 | } 324 | 325 | proto[Node.SUBSCRIPT] = function(expr, subscript) { 326 | var hash = this._make(expr); 327 | if (this._needsParens(expr)) 328 | hash = "(" + hash + ")"; 329 | return hash + "[" + this._make(subscript) + "]"; 330 | } 331 | 332 | proto[Node.OBJECT] = function(exprList) { 333 | if (exprList.children.length == 0) 334 | return "{}"; 335 | var self = this; 336 | return "{" + MAP(exprList.children, function(p){ 337 | if (p.length == 3) { 338 | // getter/setter. The name is in p[0], the arg.list in p[1][2], the 339 | // body in p[1][3] and type ("get" / "set") in p[2]. 340 | return self[Node.FUNCTION](p[0], p[1][2], p[1][3], p[2]); 341 | } 342 | var key = p[0], val = self._make(p[1]); 343 | if ((typeof key == "number" || +key + "" == key) 344 | && parseFloat(key) >= 0) { 345 | key = self[Node.NUMBER](+key); 346 | } else if (!g.is_identifier(key)) { 347 | key = self[Node.STRING](key); 348 | } 349 | return self._format([ key + ":", val ]); 350 | }).join(",") + "}"; 351 | } 352 | 353 | proto[Node.REGEXP] = function(rx, mods) { 354 | return "/" + rx + "/" + mods; 355 | } 356 | 357 | proto[Node.ARRAY] = function(elements) { 358 | if (elements.length == 0) return "[]"; 359 | var self = this; 360 | return "[" + addCommas(MAP(elements, function(expr){ 361 | if (expr.type == Node.ATOM && expr.children[0] == "undefined") return ""; 362 | return self._parenthesize(expr, Node.SEQUENCE); 363 | })) + "]"; 364 | } 365 | proto[Node.STATEMENT] = function(stmt) { 366 | return this._make(stmt).replace(/;*\s*$/, ""); 367 | } 368 | 369 | proto[Node.SEQUENCE] = function() { 370 | return addCommas(MAP(slice(arguments), this._make.bind(this))); 371 | } 372 | 373 | proto[Node.LABEL] = function(name, block) { 374 | return this._format([ this[Node.NAME](name), ":", this._make(block) ]); 375 | } 376 | 377 | proto[Node.WITH] = function(expr, block) { 378 | return this._format([ "with", "(" + this._make(expr) + ")", this._make(block) ]); 379 | } 380 | 381 | proto[Node.ATOM] = function(name) { 382 | return this[Node.NAME](name); 383 | } 384 | 385 | // other generators not matching a node type 386 | 387 | Compiler.prototype._blockStatements = function (statements) { 388 | for (var a = [], last = statements.length - 1, i = 0; i <= last; ++i) { 389 | var stat = statements[i]; 390 | var code = this._make(stat); 391 | 392 | if(this._lineFunc && stat.line && stat.line != this.line && (!stat.children[0] || !member(stat.children[0].type, [Node.NAME, Node.STRING]))) { 393 | code = this._lineFunc + "(" + stat.line + "); " + code; 394 | this.line = stat.line; 395 | } 396 | 397 | if (code != ";") { 398 | if (i == last) { 399 | if ((stat.type == Node.WHILE && emptyBlock(stat.children[1])) || 400 | (member(stat.type, [ Node.FOR, Node.FOR_IN] ) && emptyBlock(stat.children[3])) || 401 | (stat.type == Node.IF && emptyBlock(stat.children[1]) && !stat.children[2]) || 402 | (stat.type == Node.IF && stat.children[2] && emptyBlock(stat.children[2]))) { 403 | code = code.replace(/;*\s*$/, ";"); 404 | } else { 405 | code = code.replace(/;+\s*$/, ""); 406 | } 407 | } 408 | a.push(code); 409 | } 410 | } 411 | return a; 412 | }; 413 | 414 | Compiler.prototype._switchBlock = function(body) { 415 | var n = body.length; 416 | if (n == 0) return "{}"; 417 | return "{" + MAP(body, function(branch, i){ 418 | var has_body = branch[1].length > 0, 419 | code = (branch[0] 420 | ? this._format([ "case", this._make(branch[0]) + ":" ]) 421 | : "default:") + (has_body ? this._blockStatements(branch[1]).join('') : "") 422 | if (has_body && i < n - 1) code += ";"; 423 | return code; 424 | }).join('') + "}"; 425 | } 426 | 427 | Compiler.prototype._then = function(th) { 428 | if (th.type == Node.DO) { 429 | return this._make(new Node(Node.BLOCK, th)); 430 | } 431 | var b = th; 432 | while (true) { 433 | var type = b.type; 434 | if (type == Node.IF) { 435 | if (!b.children[2]) 436 | // no else, we must add the block 437 | return this._make(new Node(Node.BLOCK, th)); 438 | b = b.children[2]; 439 | } 440 | else if (type == Node.WHILE || type == Node.DO) b = b.children[1]; 441 | else if (type == Node.FOR || type == Node.FOR_IN) b = b.children[3]; 442 | else break; 443 | } 444 | return this._make(th); 445 | }; 446 | 447 | Compiler.prototype._parenthesize = function(expr) { 448 | var gen = this._make(expr); 449 | for (var i = 1; i < arguments.length; ++i) { 450 | var el = arguments[i]; 451 | if ((el instanceof Function && el.apply(this, [expr])) || expr.type == el) 452 | return "(" + gen + ")"; 453 | } 454 | return gen; 455 | } 456 | 457 | Compiler.prototype._needsParens = function(node) { 458 | if (node.type == Node.FUNCTION || node.type == Node.OBJECT) { 459 | var a = slice(this.stack), self = a.pop(), p = a.pop(); 460 | while (p) { 461 | if (p.type == Node.STATEMENT) return true; 462 | if ( 463 | ((member(p.type, [Node.SEQUENCE, Node.CALL, Node.DOT, Node.SUBSCRIPT, Node.TERNARY])) && p.children[0] === self) || 464 | ((member(p.type, [Node.BINARY, Node.ASSIGN, Node.UNARY_POSTFIX])) && p.children[1] === self) 465 | ) { 466 | self = p; 467 | p = a.pop(); 468 | } else { 469 | return false; 470 | } 471 | } 472 | } 473 | return needsParens(node.type); 474 | } 475 | 476 | Compiler.prototype._optimizeOutputNodes = function(statements) { 477 | if(statements.length < 2) return statements; 478 | 479 | var newStats = [] 480 | , binStat = new Node(Node.BINARY, '+'); 481 | 482 | statements.forEach(function(stat, i, list) { 483 | switch(stat.type) { 484 | case Node.OUTPUT: 485 | addStat(new Node(Node.STRING, formatOutput(stat.children[0]))); 486 | break; 487 | case Node.EMBED: 488 | addStat(new Node(Node.ESCAPED, stat.children[0])); 489 | break; 490 | case Node.EMBED_RAW: 491 | addStat(stat.children[0]); 492 | break; 493 | default: 494 | if(i < list.length - 1) { 495 | nextEmbed(stat); 496 | } 497 | } 498 | }); 499 | nextEmbed(); 500 | 501 | return newStats.length ? newStats : statements; 502 | 503 | function addStat(stat) { 504 | if(binStat.children.length < 3) 505 | binStat.push(stat); 506 | else binStat = new Node(Node.BINARY, '+', binStat, stat); 507 | } 508 | 509 | function nextEmbed(stat) { 510 | if(binStat.children.length < 3) { 511 | if(stat) newStats.push(stat); 512 | return; 513 | } 514 | var node = new Node(Node.EMBED_RAW, new Node(Node.STATEMENT, binStat)); 515 | if(stat) node.line = stat.line; 516 | newStats.push(node); 517 | binStat = new Node(Node.BINARY, '+'); 518 | } 519 | } 520 | 521 | Compiler.prototype._wrapCb = function(arg) { 522 | return this._cbFunc + "(" + arg + ")"; 523 | } 524 | 525 | Compiler.prototype._format = function(args) { 526 | return args.join(' '); 527 | } 528 | 529 | 530 | Compiler.prototype._astWalker = function(ast) { 531 | var user = {} 532 | , stack = [] 533 | , self = this 534 | , walkers = { 535 | "N_NAME": function(name) { 536 | return [ this.type, name ]; // maybe needs a new node? 537 | } 538 | , "N_DOT": function(expr) { 539 | return [ this[0], walk(expr) ].concat(slice(arguments, 1)); 540 | } 541 | }; 542 | 543 | function walk(ast) { 544 | if (ast == null) return null; 545 | try { 546 | stack.push(ast); 547 | var type = ast.type; 548 | var gen = user[type]; 549 | if (gen) { 550 | var ret = gen.apply(ast, ast.children); 551 | if (ret != null) return ret; 552 | } 553 | gen = walkers[type]; 554 | return gen.apply(ast, ast.children); 555 | } finally { 556 | stack.pop(); 557 | } 558 | }; 559 | 560 | function withWalkers(walkers, cont){ 561 | var save = {}, i; 562 | for (i in walkers) if (HOP(walkers, i)) { 563 | save[i] = user[i]; 564 | user[i] = walkers[i]; 565 | } 566 | var ret = cont.call(self); 567 | for (i in save) if (HOP(save, i)) { 568 | if (!save[i]) delete user[i]; 569 | else user[i] = save[i]; 570 | } 571 | return ret; 572 | }; 573 | 574 | return { 575 | walk: walk, 576 | withWalkers: withWalkers, 577 | parent: function() { 578 | return stack[stack.length - 2]; // last one is current node 579 | }, 580 | stack: function() { 581 | return stack; 582 | } 583 | }; 584 | } 585 | 586 | // utilities 587 | 588 | var DOT_CALL_NO_PARENS = g.array_to_hash([ 589 | Node.NAME, 590 | Node.ARRAY, 591 | Node.OBJECT, 592 | Node.STRING, 593 | Node.DOT, 594 | Node.SUBSCRIPT, 595 | Node.CALL, 596 | Node.REGEXP 597 | ]); 598 | 599 | function needsCbWrap(func) { 600 | if(g.is_syncronous_call(func.children.slice(-1)[0])) 601 | return false; 602 | 603 | if (func.children[0].children && func.children[0].children[0] == '_') 604 | return false; 605 | 606 | return true; 607 | } 608 | 609 | function needsParens(type) { 610 | !HOP(DOT_CALL_NO_PARENS, type); 611 | } 612 | 613 | function emptyBlock(b) { 614 | return !b || (b.type == Node.BLOCK && (!b.children[0] || b.children[0].length == 0)); 615 | }; 616 | 617 | function toAscii(str) { 618 | return str.replace(/[\u0080-\uffff]/g, function(ch) { 619 | var code = ch.charCodeAt(0).toString(16); 620 | while (code.length < 4) code = "0" + code; 621 | return "\\u" + code; 622 | }); 623 | }; 624 | 625 | function addCommas(args) { 626 | return args.join(","); 627 | }; 628 | 629 | function formatOutput(str) { 630 | return str.replace(/\'/g, "\\'"); 631 | } 632 | 633 | function formatEmbed(str) { 634 | return str.replace(/[\n]+/g, '\\n') 635 | } 636 | 637 | function bestOf(a) { 638 | if (a.length == 1) { 639 | return a[0]; 640 | } 641 | if (a.length == 2) { 642 | var b = a[1]; 643 | a = a[0]; 644 | return a.length <= b.length ? a : b; 645 | } 646 | return bestOf([ a[0], bestOf(a.slice(1)) ]); 647 | }; 648 | 649 | function member(name, array) { 650 | for (var i = array.length; --i >= 0;) 651 | if (array[i] === name) return true; 652 | return false; 653 | }; 654 | 655 | function HOP(obj, prop) { 656 | return Object.prototype.hasOwnProperty.call(obj, prop); 657 | }; 658 | 659 | var MAP; 660 | 661 | (function(){ 662 | MAP = function(a, f, o) { 663 | o = o || {}; 664 | var ret = []; 665 | for (var i = 0; i < a.length; ++i) { 666 | var val = f.call(o, a[i], i); 667 | if (val instanceof AtTop) ret.unshift(val.v); 668 | else ret.push(val); 669 | } 670 | return ret; 671 | }; 672 | MAP.at_top = function(val) { return new AtTop(val) }; 673 | function AtTop(val) { this.v = val }; 674 | })(); 675 | 676 | function slice(a, start) { 677 | return Array.prototype.slice.call(a, start == null ? 0 : start); 678 | }; 679 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | // Thanks to mishoo/uglifyjs for most of this! 2 | 3 | // [« Back to Index](index.html) 4 | 5 | var util = require('util') 6 | , g = require('./grammar') 7 | , Token = require('./lexer').Token 8 | 9 | // AJS Parser 10 | // ------------- 11 | 12 | // The parser takes raw token output from the lexer and construct 13 | // an AST (abstract syntax tree) according to Javascript and AJS syntax rules. 14 | var Parser = module.exports = function Parser(lexer, opts) { 15 | opts = opts || {}; 16 | 17 | this.lexer = lexer; 18 | this.tree = []; 19 | 20 | this.exigentMode = (opts.exigentMode == true); 21 | this.embedTokens = (opts.embedTokens == true); 22 | 23 | this._token = null; 24 | this._peeked = null; 25 | this._prevToken = null; 26 | this._inFunction = 0; 27 | this._inLoop = 0; 28 | this._labels = []; 29 | } 30 | 31 | 32 | // We use a recursive look-ahead algorithm to build up the AST. Parse a statement 33 | // at a time until we reach the end of the file. 34 | Parser.prototype.parse = function() { 35 | var self = this 36 | 37 | this._pos = 0; 38 | 39 | var self = this; 40 | this.tree = (function(){ 41 | self._next(); 42 | var statements = []; 43 | while (!self._tokenIs(Token.EOF)) 44 | statements.push(self._statement()); 45 | return self._node(Node.ROOT, statements); 46 | })(); 47 | 48 | return this.tree; 49 | } 50 | 51 | Parser.prototype._peek = function() { 52 | return this._peeked || (this._peeked = this.lexer.nextToken()); 53 | }; 54 | 55 | Parser.prototype._next = function() { 56 | this._prevToken = this._token; 57 | if (this._peeked) { 58 | this._token = this._peeked; 59 | this._peeked = null; 60 | } else { 61 | this._token = this.lexer.nextToken(); 62 | } 63 | if(!this._token) 64 | throw new Error('unexpected eof, after:' + this._prev() + this._prev().line); 65 | return this._token; 66 | }; 67 | 68 | Parser.prototype._prev = function() { 69 | return this._prevToken; 70 | }; 71 | 72 | 73 | Parser.prototype._tokenIs = function (type, value) { 74 | return tokenIs(this._token, type, value); 75 | }; 76 | 77 | Parser.prototype._tokenIsPunc = function (value) { 78 | return tokenIs(this._token, Token.PUNCTUATION, value); 79 | }; 80 | 81 | Parser.prototype._error = function(msg, line, col) { 82 | throw new Error(msg + " at line " + (line || this.lexer._line) + ", column " + (col || this.lexer._col)); 83 | }; 84 | 85 | Parser.prototype._tokenError = function(token, msg) { 86 | this._error(msg, token.line, token.col); 87 | }; 88 | Parser.prototype._unexpected = function(token) { 89 | if (token == null) token = this._token; 90 | this._tokenError(token, "Unexpected token: " + token.type + " (" + token.value + ")"); 91 | }; 92 | 93 | Parser.prototype._expectToken = function(type, val) { 94 | if (this._tokenIs(type, val)) return this._next(); 95 | this._tokenError(this._token, "Unexpected token " + this._token.type + ", expected " + type); 96 | }; 97 | 98 | Parser.prototype._expect = function(punc) { 99 | return this._expectToken(Token.PUNCTUATION, punc); 100 | }; 101 | 102 | Parser.prototype._canInsertSemicolon = function() { 103 | return !this.exigentMode && ( 104 | this._token.newLineBefore || this._tokenIs(Token.EOF) || this._tokenIsPunc("}") 105 | ); 106 | }; 107 | 108 | Parser.prototype._semicolon = function() { 109 | if (this._tokenIsPunc(";")) this._next(); 110 | else if (!this._canInsertSemicolon() && !this._tokenIs(Token.OUTPUT)) this._unexpected(); 111 | }; 112 | 113 | Parser.prototype._parenthesised = function() { 114 | this._expect("("); 115 | var ex = this._expression(); 116 | this._expect(")"); 117 | return ex; 118 | }; 119 | 120 | Parser.prototype._statement = function() { 121 | if (this._tokenIs(Token.OPERATOR, "/")) { 122 | this._peeked = null; 123 | this._token = this.lexer.nextToken(true); // force regexp 124 | } 125 | 126 | switch (this._token.type) { 127 | case Token.OUTPUT: 128 | return this._node(Node.OUTPUT, this._prog1(this._token.value, this._next)); 129 | case Token.EMBED: 130 | return this._embed(); 131 | case Token.NUMBER: 132 | case Token.STRING: 133 | case Token.REGEXP: 134 | case Token.OPERATOR: 135 | case Token.ATOM: 136 | return this._simpleStatement(); 137 | 138 | case Token.NAME: 139 | return tokenIs(this._peek(), Token.PUNCTUATION, ":") 140 | ? this._labeledStatement(this._prog1(this._token.value, this._next, this._next)) 141 | : this._simpleStatement(); 142 | 143 | case Token.PUNCTUATION: 144 | switch (this._token.value) { 145 | case "{": 146 | return this._node(Node.BLOCK, this._block()); 147 | case "[": 148 | case "(": 149 | return this._simpleStatement(); 150 | case ";": 151 | this._next(); 152 | return this._node(Node.BLOCK); 153 | default: 154 | this._unexpected(); 155 | } 156 | 157 | case Token.KEYWORD: 158 | switch (this._prog1(this._token.value, this._next)) { 159 | case "break": 160 | return this._breakCont(Node.BREAK); 161 | case "continue": 162 | return this._breakCont(Node.CONTINUE); 163 | case "debugger": 164 | this._semicolon(); 165 | return this._node(Node.DEBUGGER); 166 | case "do": 167 | var self = this; 168 | return (function(body){ 169 | self._expectToken(Token.KEYWORD, "while"); 170 | return self._node(Node.DO, self._prog1(self._parenthesised, self._semicolon), body); 171 | })(this._loop(this._statement)); 172 | case "for": 173 | return this._for(); 174 | case "function": 175 | return this._function(true); 176 | case "if": 177 | return this._if(); 178 | case "return": 179 | if (!this._inFunction) 180 | this._error("'return' outside of function"); 181 | return this._node(Node.RETURN, 182 | this._tokenIsPunc(";") 183 | ? (this._next(), null) 184 | : this._canInsertSemicolon() 185 | ? null 186 | : this._prog1(this._expression, this._semicolon)); 187 | case "switch": 188 | return this._node(Node.SWITCH, this._parenthesised(), this._switchBlock()); 189 | case "throw": 190 | return this._node(Node.THROW, this._prog1(this._expression, this._semicolon)); 191 | case "try": 192 | return this._try(); 193 | case "var": 194 | return this._prog1(this._var, this._semicolon); 195 | case "const": 196 | return this._prog1(this._const, this._semicolon); 197 | case "while": 198 | return this._node(Node.WHILE, this._parenthesised(), this._loop(this._statement)); 199 | case "with": 200 | return this._node(Node.WITH, this._parenthesised(), this._statement()); 201 | default: 202 | this._unexpected(); 203 | } 204 | break; 205 | } 206 | } 207 | 208 | Parser.prototype._embed = function() { 209 | var type = this._token.value; 210 | this._next() 211 | switch (type) { 212 | case "=": 213 | return this._node(Node.EMBED, this._statement()); 214 | case "-": 215 | return this._node(Node.EMBED_RAW, this._statement()); 216 | default: this._unexpected(); 217 | } 218 | } 219 | 220 | Parser.prototype._labeledStatement = function(label) { 221 | this._labels.push(label); 222 | var start = this._token 223 | , statNode = this._statement(); 224 | if (this.exigentMode && !g.is_statement_with_label(statNode.type)) 225 | this._unexpected(start); 226 | this._labels.pop(); 227 | return this._node(Node.LABEL, label, statNode); 228 | }; 229 | 230 | Parser.prototype._simpleStatement = function() { 231 | return this._node(Node.STATEMENT, this._prog1(this._expression, this._semicolon)); 232 | }; 233 | 234 | Parser.prototype._breakCont = function(type) { 235 | var name = this._tokenIs(Token.NAME) ? this._token.value : null; 236 | if (name != null) { 237 | this._next(); 238 | if (!member(name, this._labels)) 239 | this._error("Label " + name + " without matching loop or statement"); 240 | } else if (!this._inLoop) 241 | this._error(type + " not inside a loop or switch"); 242 | this._semicolon(); 243 | return this._node(type, name); 244 | }; 245 | 246 | Parser.prototype._for = function() { 247 | this._expect("("); 248 | var init = null; 249 | if (!this._tokenIsPunc(";")) { 250 | init = this._tokenIs(Token.KEYWORD, "var") 251 | ? (this._next(), this._var(true)) 252 | : this._expression(true, true); 253 | if (this._tokenIs(Token.OPERATOR, "in")) 254 | return this._forIn(init); 255 | } 256 | return this._regularFor(init); 257 | }; 258 | 259 | Parser.prototype._regularFor = function(init) { 260 | this._expect(";"); 261 | var test = this._tokenIsPunc(";") ? null : this._expression(); 262 | this._expect(";"); 263 | var step = this._tokenIsPunc(")") ? null : this._expression(); 264 | this._expect(")"); 265 | return this._node(Node.FOR, init, test, step, this._loop(this._statement)); 266 | }; 267 | 268 | Parser.prototype._forIn = function(init) { 269 | var lhs = init.type == Node.VAR ? this._node(Node.NAME, init.children[0][0].children[0]) : init; 270 | this._next(); 271 | var obj = this._expression(); 272 | this._expect(")"); 273 | return this._node(Node.FOR_IN, init, lhs, obj, this._loop(this._statement)); 274 | }; 275 | 276 | Parser.prototype._function = function(inStatement) { 277 | var name = this._tokenIs(Token.NAME) ? this._prog1(this._token.value, this._next) : null; 278 | if (inStatement && !name) this._unexpected(); 279 | this._expect("("); 280 | var self = this; 281 | return this._node(inStatement ? Node.DEFUN : Node.FUNCTION, 282 | name, 283 | (function(first, a){ // arguments 284 | while (!self._tokenIsPunc(")")) { 285 | if (first) first = false; else self._expect(","); 286 | if (!self._tokenIs(Token.NAME)) self._unexpected(); 287 | a.push(self._token.value); 288 | self._next(); 289 | } 290 | self._next(); 291 | return a; 292 | })(true, []), 293 | (function(){ // body 294 | ++self._inFunction; 295 | var loop = self._inLoop; 296 | self._inLoop = 0; 297 | var node = self._block(); 298 | --self._inFunction; 299 | self._inLoop = loop; 300 | return node; 301 | })()); 302 | }; 303 | 304 | Parser.prototype._if = function() { 305 | var cond = this._parenthesised() 306 | , body = this._statement() 307 | , belse; 308 | if (this._tokenIs(Token.KEYWORD, "else")) { 309 | this._next(); 310 | belse = this._statement(); 311 | } 312 | return this._node(Node.IF, cond, body, belse); 313 | }; 314 | 315 | Parser.prototype._block = function() { 316 | this._expect("{"); 317 | var a = []; 318 | while (!this._tokenIsPunc("}")) { 319 | if (this._tokenIs(Token.EOF)) this._unexpected(); 320 | a.push(this._statement()); 321 | } 322 | this._next(); 323 | return a; 324 | }; 325 | 326 | Parser.prototype._switchBlock = function() { 327 | var self = this; 328 | this._curry(self._loop, function(){ 329 | self._expect("{"); 330 | var node = this._node(Node.BLOCK) 331 | , cur = null; 332 | while (!self._tokenIsPunc("}")) { 333 | if (self._tokenIs(Token.EOF)) self._unexpected(); 334 | if (self._tokenIs(Token.KEYWORD, "case")) { 335 | self._next(); 336 | cur = this._node(Node.STATEMENT); 337 | node.push([ self._expression(), cur ]); 338 | self._expect(":"); 339 | } else if (self._tokenIs(Token.KEYWORD, "default")) { 340 | self._next(); 341 | self._expect(":"); 342 | cur = this._node(Node.STATEMENT); 343 | node.push([ null, cur ]); 344 | } else { 345 | if (!cur) self._unexpected(); 346 | cur.push(self._statement()); 347 | } 348 | } 349 | self._next(); 350 | return node; 351 | })(); 352 | }; 353 | 354 | Parser.prototype._try = function() { 355 | var body = this._block() 356 | , bcatch 357 | , bfinally; 358 | 359 | if (this._tokenIs(Token.KEYWORD, "catch")) { 360 | this._next(); 361 | this._expect("("); 362 | if (!this._tokenIs(Token.NAME)) this._error("Name expected"); 363 | var name = this._token.value; 364 | this._next(); 365 | this._expect(")"); 366 | bcatch = this._node(Node.BLOCK, name, this._block()); 367 | } 368 | if (this._tokenIs(Token.KEYWORD, "finally")) { 369 | this._next(); 370 | bfinally = this._block(); 371 | } 372 | if (!bcatch && !bfinally) this._error("Missing catch/finally blocks"); 373 | return this._node(Node.TRY, body, bcatch, bfinally); 374 | }; 375 | 376 | Parser.prototype._vardefs = function(noIn) { 377 | var a = []; 378 | for (;;) { 379 | if (!this._tokenIs(Token.NAME)) this._unexpected(); 380 | var name = this._token.value; 381 | this._next(); 382 | if (this._tokenIs(Token.OPERATOR, "=")) { 383 | this._next(); 384 | a.push(this._node(Node.VAR_DEF, name, this._expression(false, noIn))); 385 | } else { 386 | a.push(this._node(Node.VAR_DEF, name)); 387 | } 388 | if (!this._tokenIsPunc(",")) 389 | break; 390 | this._next(); 391 | } 392 | return a; 393 | }; 394 | 395 | Parser.prototype._var = function(noIn) { 396 | return this._node(Node.VAR, this._vardefs(noIn)); 397 | }; 398 | 399 | Parser.prototype._const = function(noIn) { 400 | return this._node(Node.CONST, this._vardefs(noIn)); 401 | }; 402 | 403 | Parser.prototype._new = function() { 404 | var newexp = this._exprAtom(false) 405 | , args; 406 | if (this._tokenIsPunc("(")) { 407 | this._next(); 408 | args = this._exprList(")"); 409 | } else { 410 | args = []; 411 | } 412 | return this._subscripts(this._node(Node.NEW, newexp, args), true); 413 | }; 414 | 415 | Parser.prototype._exprAtom = function(allowCalls) { 416 | if (this._tokenIs(Token.OPERATOR, "new")) { 417 | this._next(); 418 | return this._new(); 419 | } 420 | if (this._tokenIs(Token.OPERATOR) && g.is_unary_prefix(this._token.value)) { 421 | return this._makeUnary(Node.UNARY_PREFIX, 422 | this._prog1(this._token.value, this._next), 423 | this._exprAtom(allowCalls)); 424 | } 425 | if (this._tokenIs(Token.PUNCTUATION)) { 426 | switch (this._token.value) { 427 | case "(": 428 | this._next(); 429 | return this._subscripts(this._prog1(this._expression, this._curry(this._expect, ")")), allowCalls); 430 | case "[": 431 | this._next(); 432 | return this._subscripts(this._array(), allowCalls); 433 | case "{": 434 | this._next(); 435 | return this._subscripts(this._object(), allowCalls); 436 | } 437 | return this._unexpected(); 438 | } 439 | if (this._tokenIs(Token.KEYWORD, "function")) { 440 | this._next(); 441 | return this._subscripts(this._function(false), allowCalls); 442 | } 443 | if (g.is_atomic_start_token(this._token.type)) { 444 | var atom = this._tokenIs(Token.REGEXP) 445 | ? this._node(Node.REGEXP, this._token.value[0], this._token.value[1]) 446 | : this._node(Node[this._token.name], this._token.value); 447 | return this._subscripts(this._prog1(atom, this._next), allowCalls); 448 | } 449 | this._unexpected(); 450 | }; 451 | 452 | Parser.prototype._exprList = function(closing, allowTrailingComma, allowEmpty) { 453 | var first = true 454 | , a = []; 455 | while (!this._tokenIsPunc(closing)) { 456 | if (first) first = false; else this._expect(","); 457 | if (allowTrailingComma && this._tokenIsPunc(closing)) break; 458 | if (this._tokenIsPunc(",") && allowEmpty) { 459 | a.push([ "atom", "undefined" ]); 460 | } else { 461 | a.push(this._expression(false)); 462 | } 463 | } 464 | this._next(); 465 | return a; 466 | }; 467 | 468 | Parser.prototype._array = function() { 469 | return this._node(Node.ARRAY, this._exprList("]", !this._exigentMode, true)); 470 | }; 471 | 472 | Parser.prototype._object = function() { 473 | var first = true 474 | , node = this._node(Node.EXPRESSION_LIST); 475 | while (!this._tokenIsPunc("}")) { 476 | if (first) first = false; else this._expect(","); 477 | if (!this._exigentMode && this._tokenIsPunc("}")) 478 | break; // allow trailing comma 479 | var type = this._token.type; 480 | var name = this._propertyName(); 481 | if (type == Token.NAME && (name == "get" || name == "set") && !this._tokenIsPunc(":")) { 482 | node.push([ this._name(), this._function(false), name ]); 483 | } else { 484 | this._expect(":"); 485 | node.push([ name, this._expression(false) ]); 486 | } 487 | } 488 | this._next(); 489 | return this._node(Node.OBJECT, node); 490 | }; 491 | 492 | Parser.prototype._propertyName = function() { 493 | switch (this._token.type) { 494 | case Token.NUMBER: 495 | case Token.STRING: 496 | return this._prog1(this._token.value, this._next); 497 | } 498 | return this._name(); 499 | }; 500 | 501 | Parser.prototype._name = function() { 502 | switch (this._token.type) { 503 | case Token.NAME: 504 | case Token.OPERATOR: 505 | case Token.KEYWORD: 506 | case Token.ATOM: 507 | return this._prog1(this._token.value, this._next); 508 | default: 509 | this._unexpected(); 510 | } 511 | }; 512 | 513 | Parser.prototype._subscripts = function(expr, allowCalls) { 514 | 515 | if (this._tokenIsPunc(".")) { 516 | this._next(); 517 | return this._subscripts(this._node(Node.DOT, expr, this._name()), allowCalls); 518 | } 519 | if (this._tokenIsPunc("[")) { 520 | this._next(); 521 | return this._subscripts(this._node(Node.SUBSCRIPT, expr, this._prog1(this._expression, this._curry(this._expect, "]"))), allowCalls); 522 | } 523 | if (allowCalls && this._tokenIsPunc("(")) { 524 | this._next(); 525 | return this._subscripts(this._node(Node.CALL, expr, this._exprList(")")), true); 526 | } 527 | if (allowCalls && this._tokenIs(Token.OPERATOR) && g.is_unary_postfix(this._token.value)) { 528 | return this._prog1(this._curry(this._makeUnary, Node.UNARY_POSTFIX, this._token.value, expr), 529 | this._next); 530 | } 531 | return expr; 532 | }; 533 | 534 | Parser.prototype._makeUnary = function(name, op, expr) { 535 | if ((op == "++" || op == "--") && !this._isAssignable(expr)) 536 | this._error("Invalid use of " + op + " operator"); 537 | return this._node(name, op, expr); 538 | }; 539 | 540 | Parser.prototype._exprOp = function(left, minPrec, noIn) { 541 | var op = this._tokenIs(Token.OPERATOR) ? this._token.value : null; 542 | if (op && op == "in" && noIn) op = null; 543 | var prec = op != null ? g.precedence(op) : null; 544 | if (prec != null && prec > minPrec) { 545 | this._next(); 546 | var right = this._exprOp(this._exprAtom(true), prec, noIn); 547 | return this._exprOp(this._node(Node.BINARY, op, left, right), minPrec, noIn); 548 | } 549 | return left; 550 | }; 551 | 552 | Parser.prototype._exprOps = function(noIn) { 553 | return this._exprOp(this._exprAtom(true), 0, noIn); 554 | }; 555 | 556 | Parser.prototype._maybeTernary = function(noIn) { 557 | var expr = this._exprOps(noIn); 558 | if (this._tokenIs(Token.OPERATOR, "?")) { 559 | this._next(); 560 | var yes = this._expression(false); 561 | this._expect(":"); 562 | return this._node(Node.TERNARY, expr, yes, this._expression(false, noIn)); 563 | } 564 | return expr; 565 | }; 566 | 567 | Parser.prototype._isAssignable = function(expr) { 568 | if (!this.exigentMode) return true; 569 | switch (expr.type) { 570 | case Node.DOT: 571 | case Node.SUBSCRIPT: 572 | case Node.NEW: 573 | case Node.CALL: 574 | return true; 575 | case Node.NAME: 576 | return expr.value != "this"; 577 | } 578 | }; 579 | 580 | Parser.prototype._maybeAssign = function(noIn) { 581 | var left = this._maybeTernary(noIn) 582 | , val = this._token.value; 583 | if (this._tokenIs(Token.OPERATOR) && g.is_assignment(val)) { 584 | if (this._isAssignable(left)) { 585 | this._next(); 586 | return this._node(Node.ASSIGN, g.assignment(val), left, this._maybeAssign(noIn)); 587 | } 588 | this._error("Invalid assignment"); 589 | } 590 | return left; 591 | }; 592 | 593 | Parser.prototype._expression = function(commas, noIn) { 594 | if (arguments.length == 0) 595 | commas = true; 596 | var expr = this._maybeAssign(noIn); 597 | if (commas && this._tokenIsPunc(",")) { 598 | this._next(); 599 | return this._node(Node.SEQUENCE, expr, this._expression(true, noIn)); 600 | } 601 | return expr; 602 | }; 603 | 604 | Parser.prototype._loop = function(cont) { 605 | try { 606 | ++this._inLoop; 607 | return cont.call(this); 608 | } finally { 609 | --this._inLoop; 610 | } 611 | }; 612 | 613 | Parser.prototype._curry = function(f) { 614 | var args = slice(arguments, 1) 615 | , self = this; 616 | return function() { return f.apply(self, args.concat(slice(arguments))); }; 617 | }; 618 | 619 | Parser.prototype._prog1 = function(ret) { 620 | if (ret instanceof Function) 621 | ret = ret.call(this); 622 | for (var i = 1, n = arguments.length; --n > 0; ++i) 623 | arguments[i].call(this); 624 | return ret; 625 | }; 626 | 627 | Parser.prototype._node = function(type) { 628 | var node = new Node(type); 629 | node.children = Array.prototype.slice.call(arguments, 1); 630 | node.line = (this._prevToken && this._prevToken.line) || 0; 631 | return node; 632 | } 633 | 634 | var Node = module.exports.Node = function(type) { 635 | if(typeof type == 'undefined') throw new Error('undefined node type'); 636 | 637 | this.type = type; 638 | this.line = undefined; 639 | this.children = Array.prototype.slice.call(arguments, 1); 640 | } 641 | 642 | Node.prototype.push = function(child) { 643 | this.children.push(child); 644 | } 645 | 646 | Node.prototype.toString = function() { 647 | return '[' + this.type + ', ' + util.inspect(this.children, false, 10) + ']'; 648 | }; 649 | 650 | var nodeTypes = [ 651 | 'arguments' 652 | , 'array' 653 | , 'assign' 654 | , 'atom' 655 | , 'binary' 656 | , 'block' 657 | , 'break' 658 | , 'call' 659 | , 'const' 660 | , 'continue' 661 | , 'debugger' 662 | , 'defun' 663 | , 'do' 664 | , 'dot' 665 | , 'embed' 666 | , 'embed_raw' 667 | , 'escaped' 668 | , 'expression_list' 669 | , 'for' 670 | , 'for_in' 671 | , 'function' 672 | , 'if' 673 | , 'label' 674 | , 'name' 675 | , 'new' 676 | , 'number' 677 | , 'object' 678 | , 'output' 679 | , 'regexp' 680 | , 'return' 681 | , 'root' 682 | , 'sequence' 683 | , 'statement' 684 | , 'string' 685 | , 'switch' 686 | , 'subscript' 687 | , 'ternary' 688 | , 'throw' 689 | , 'try' 690 | , 'unary_postfix' 691 | , 'unary_prefix' 692 | , 'var' 693 | , 'var_def' 694 | , 'while' 695 | , 'with' 696 | ]; 697 | 698 | nodeTypes.forEach(function(name) { 699 | name = name.toUpperCase() 700 | Node[name] = 'N_' + name; 701 | }); 702 | 703 | // utilities 704 | 705 | function tokenIs(token, type, value) { 706 | return token.type == type && (value == null || token.value == value); 707 | } 708 | 709 | function slice(a, start) { 710 | return Array.prototype.slice.call(a, start == null ? 0 : start); 711 | }; 712 | 713 | function member(name, array) { 714 | for (var i = array.length; --i >= 0;) 715 | if (array[i] === name) return true; 716 | return false; 717 | }; 718 | 719 | --------------------------------------------------------------------------------