├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── README.md ├── lib └── estemplate.js ├── package.json └── test ├── custom_options.ast.json └── estemplate_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "evil": true, 5 | "immed": true, 6 | "latedef": true, 7 | "newcap": true, 8 | "noarg": true, 9 | "sub": true, 10 | "undef": true, 11 | "unused": true, 12 | "boss": true, 13 | "eqnull": true, 14 | "node": true 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - npm install -g grunt-cli 6 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | // Show elapsed time at the end 5 | require('time-grunt')(grunt); 6 | // Load all grunt tasks 7 | require('load-grunt-tasks')(grunt); 8 | 9 | // Project configuration. 10 | grunt.initConfig({ 11 | nodeunit: { 12 | files: ['test/**/*_test.js'] 13 | }, 14 | jshint: { 15 | options: { 16 | jshintrc: '.jshintrc', 17 | reporter: require('jshint-stylish') 18 | }, 19 | gruntfile: { 20 | src: 'Gruntfile.js' 21 | }, 22 | lib: { 23 | src: ['lib/**/*.js'] 24 | }, 25 | test: { 26 | src: ['test/**/*.js'] 27 | } 28 | }, 29 | watch: { 30 | gruntfile: { 31 | files: '<%= jshint.gruntfile.src %>', 32 | tasks: ['jshint:gruntfile'] 33 | }, 34 | lib: { 35 | files: '<%= jshint.lib.src %>', 36 | tasks: ['jshint:lib', 'nodeunit'] 37 | }, 38 | test: { 39 | files: '<%= jshint.test.src %>', 40 | tasks: ['jshint:test', 'nodeunit'] 41 | } 42 | } 43 | }); 44 | 45 | // Default task. 46 | grunt.registerTask('default', ['jshint', 'nodeunit']); 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # estemplate [![Build Status](https://secure.travis-ci.org/estools/estemplate.png?branch=master)](http://travis-ci.org/estools/estemplate) 2 | 3 | > Proper JavaScript code templating with source maps support. 4 | 5 | This module allows to generate JavaScript AST from code template and AST nodes as substitutions. 6 | 7 | This is more proper way of code templating since it works on AST not on code string, and thus preserves locations which allow to generate source maps in future. 8 | 9 | ## Getting Started 10 | Install the module with: `npm install estemplate` and require it: 11 | 12 | ```shell 13 | npm i estemplate --save 14 | ``` 15 | 16 | ```javascript 17 | var estemplate = require('estemplate'); 18 | ``` 19 | 20 | ## API 21 | 22 | ### estemplate(tmplString, [options], data) 23 | 24 | Generates [SpiderMonkey AST](https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API) from given template string, optional [esprima](http://esprima.org/doc/index.html) options and data. 25 | 26 | Supported template substitution markers: 27 | 28 | * Compile-time execution block: `<% var localCounter = 0; %>` 29 | * Node substitution: `var x = <%= expr %> + 1;` 30 | * Array elements: `var a = [%= elements %];` 31 | * Function parameters: `function f(%= params %) {}` 32 | * Call arguments: `var x = f(%= args %);` 33 | * Block statements: `define(function () {%= body %});` 34 | * Literals: `var x = "%= 'alpha' + 'beta' %";` 35 | 36 | You can combine list substitutions with inline elements like: 37 | * `var a = [0, %= numbers %, Infinity];` 38 | * `function f(%= params %, callback) {}` 39 | * `define(function () { console.time('Module'); %= body %; console.timeEnd('Module'); });` 40 | 41 | From template, you can access entire data object via `it` and estemplate itself via `estemplate`. 42 | 43 | If you set `options.fast` to true, then passed data will be available only via `it` variable, but template function in general will be significantly faster. 44 | 45 | ### estemplate.compile(tmplString, [options]) 46 | 47 | Same as above but returns function that can be reused for AST generation (just save result and call with `data` as argument whenever needed). 48 | 49 | ## Examples 50 | 51 | ### Simple generation 52 | 53 | ```javascript 54 | var ast = estemplate('var <%= varName %> = <%= value %> + 1;', { 55 | varName: {type: 'Identifier', name: 'myVar'}, 56 | value: {type: 'Literal', value: 123} 57 | }); 58 | 59 | console.log(escodegen.generate(ast)); 60 | // > var myVar = 123 + 1; 61 | ``` 62 | 63 | ### Advanced generation (with source map) 64 | 65 | > template.jst 66 | 67 | ```javascript 68 | define(function (require, exports, module) {% = body %}); 69 | ``` 70 | 71 | > index.js 72 | 73 | ```javascript 74 | var dependency1 = require('dependency1'), 75 | dependency2 = require('dependency2'); 76 | 77 | module.exports = function () { 78 | return dependency1() + dependency2(); 79 | }; 80 | ``` 81 | 82 | > main code 83 | 84 | ```javascript 85 | var templateCode = fs.readFileSync('template.jst', 'utf-8'); 86 | var template = estemplate.compile(templateCode, {attachComment: true}); 87 | 88 | var program = esprima.parse(fs.readFileSync('index.js', 'utf-8'), { 89 | loc: true, 90 | source: 'index.js' 91 | }); 92 | 93 | var ast = template({body: program.body}); 94 | 95 | var output = escodegen.generate(ast, { 96 | sourceMap: true, 97 | sourceMapWithCode: true 98 | }); 99 | 100 | console.log(output.code); 101 | ``` 102 | 103 | > output 104 | 105 | ```javascript 106 | define(function (require, exports, module) { 107 | var dependency1 = require('dependency1'), dependency2 = require('dependency2'); 108 | module.exports = function () { 109 | return dependency1() + dependency2(); 110 | }; 111 | }); 112 | ``` 113 | 114 | ## Contributing 115 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [Grunt](http://gruntjs.com/). 116 | 117 | ## License 118 | Copyright (c) 2014 Ingvar Stepanyan. Licensed under the MIT license. 119 | -------------------------------------------------------------------------------- /lib/estemplate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * estemplate 3 | * https://github.com/RReverser/estemplate 4 | * 5 | * Copyright (c) 2014 Ingvar Stepanyan 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var parse = require('esprima').parse; 12 | var estraverse = require('estraverse'); 13 | var reCode = /([^\s,;]?)\s*?%(=?)\s*([\s\S]+?)\s*%\s*?([^\s,;]?)/g; 14 | var reInternalVar = /^__ASTER_DATA_\d+$/; 15 | var reInternalMarker = /\"(__ASTER_DATA_\d+)\"/g; 16 | 17 | function tmpl(str, options, data) { 18 | if (!data) { 19 | data = options; 20 | options = undefined; 21 | } 22 | return tmpl.compile(str, options)(data); 23 | } 24 | 25 | function isInternalVar(node) { 26 | return node.type === 'Identifier' && reInternalVar.test(node.name); 27 | } 28 | 29 | function isInternalStmt(node) { 30 | return node.type === 'ExpressionStatement' && typeof node.expression === 'string'; 31 | } 32 | 33 | var brackets = { 34 | '<': '>', 35 | '[': ']', 36 | '(': ')', 37 | '{': '}', 38 | "'": "'", 39 | '"': '"' 40 | }; 41 | 42 | var spread = { 43 | 'ArrayExpression': 'elements', 44 | 'CallExpression': 'arguments', 45 | 'BlockStatement': 'body', 46 | 'FunctionExpression': 'params', 47 | 'FunctionDeclaration': 'params', 48 | 'Program': 'body' 49 | }; 50 | 51 | tmpl.fixAST = function (ast) { 52 | estraverse.traverse(ast, { 53 | leave: function (node, parent) { 54 | if (node.type !== '...') { 55 | return; 56 | } 57 | var itemsKey = spread[parent.type]; 58 | if (!itemsKey) { 59 | throw new TypeError('Unknown substitution in ' + parent.type); 60 | } 61 | parent[itemsKey] = parent[itemsKey].reduce(function (items, item) { 62 | if (item.type === '...') { 63 | return items.concat(item.argument); 64 | } 65 | items.push(item); 66 | return items; 67 | }, []); 68 | }, 69 | keys: { 70 | '...': ['argument'] 71 | } 72 | }); 73 | return ast; 74 | }; 75 | 76 | tmpl.compile = function (str, options) { 77 | var code = [], 78 | index = 0; 79 | 80 | str = str.replace(reCode, function (match, open, isEval, codePart, close) { 81 | if (open) { 82 | var expectedClose = brackets[open]; 83 | if (!expectedClose || close && expectedClose !== close) { 84 | return match; 85 | } 86 | } 87 | if (isEval) { 88 | var varName = '__ASTER_DATA_' + (index++); 89 | var isSpread = open !== '<' && open !== "'" && open !== '"'; 90 | if (isSpread) { 91 | codePart = '{type: "...", argument: ' + codePart + '}'; 92 | } else if (open === "'" || open === '"') { 93 | codePart = '{type: "Literal", value: ' + codePart + '}'; 94 | } 95 | code.push('\t\tvar ' + varName + ' = ' + codePart); 96 | return isSpread ? (open + varName + close) : varName; 97 | } else { 98 | if (open !== '<') { 99 | return match; 100 | } 101 | code.push(codePart); 102 | return ''; 103 | } 104 | }); 105 | 106 | var ast = parse(str, options); 107 | 108 | ast = estraverse.replace(ast, { 109 | leave: function (node) { 110 | if (isInternalVar(node)) { 111 | return node.name; 112 | } 113 | 114 | if (isInternalStmt(node)) { 115 | return node.expression; 116 | } 117 | } 118 | }); 119 | 120 | if (!(options && options.fast)) { 121 | code.unshift('\twith (it) {'); 122 | code.push('\t}'); 123 | } 124 | 125 | code.unshift('return function template(it) {'); 126 | 127 | code.push( 128 | '\treturn estemplate.fixAST(' + JSON.stringify(ast).replace(reInternalMarker, '$1') + ')', 129 | '}' 130 | ); 131 | 132 | return new Function('estemplate', code.join('\n'))(tmpl); 133 | }; 134 | 135 | module.exports = tmpl; 136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "estemplate", 3 | "version": "0.5.1", 4 | "main": "lib/estemplate.js", 5 | "description": "Proper JavaScript code templating with source maps support.", 6 | "keywords": [ 7 | "javascript", 8 | "spidermonkey", 9 | "mozilla", 10 | "ast", 11 | "code", 12 | "template", 13 | "wrap", 14 | "substitute" 15 | ], 16 | "homepage": "https://github.com/RReverser/estemplate", 17 | "bugs": "https://github.com/RReverser/estemplate/issues", 18 | "author": { 19 | "name": "Ingvar Stepanyan", 20 | "email": "me@rreverser.com", 21 | "url": "https://github.com/RReverser" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/RReverser/estemplate" 26 | }, 27 | "license": "MIT", 28 | "files": [ 29 | "lib" 30 | ], 31 | "devDependencies": { 32 | "escodegen": "^1.3.3", 33 | "grunt": "^0.4.5", 34 | "grunt-cli": "^0.1.13", 35 | "grunt-contrib-jshint": "^0.10.0", 36 | "grunt-contrib-nodeunit": "^0.3.3", 37 | "grunt-contrib-watch": "^0.6.1", 38 | "jshint-stylish": "^0.2.0", 39 | "load-grunt-tasks": "^0.4.0", 40 | "time-grunt": "^0.3.1" 41 | }, 42 | "scripts": { 43 | "test": "grunt" 44 | }, 45 | "dependencies": { 46 | "esprima": "^2.7.2", 47 | "estraverse": "^4.1.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/custom_options.ast.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "sourceType": "script", 4 | "body": [ 5 | { 6 | "type": "ExpressionStatement", 7 | "expression": { 8 | "type": "CallExpression", 9 | "callee": { 10 | "type": "Identifier", 11 | "name": "define", 12 | "loc": { 13 | "start": { 14 | "line": 1, 15 | "column": 0 16 | }, 17 | "end": { 18 | "line": 1, 19 | "column": 6 20 | }, 21 | "source": "template.jst" 22 | } 23 | }, 24 | "arguments": [ 25 | { 26 | "type": "FunctionExpression", 27 | "id": null, 28 | "params": [], 29 | "defaults": [], 30 | "body": { 31 | "type": "BlockStatement", 32 | "body": [ 33 | { 34 | "type": "ExpressionStatement", 35 | "expression": { 36 | "type": "AssignmentExpression", 37 | "operator": "=", 38 | "left": { 39 | "type": "MemberExpression", 40 | "computed": false, 41 | "object": { 42 | "type": "Identifier", 43 | "name": "module", 44 | "loc": { 45 | "start": { 46 | "line": 1, 47 | "column": 0 48 | }, 49 | "end": { 50 | "line": 1, 51 | "column": 6 52 | }, 53 | "source": "source.js" 54 | } 55 | }, 56 | "property": { 57 | "type": "Identifier", 58 | "name": "exports", 59 | "loc": { 60 | "start": { 61 | "line": 1, 62 | "column": 7 63 | }, 64 | "end": { 65 | "line": 1, 66 | "column": 14 67 | }, 68 | "source": "source.js" 69 | } 70 | }, 71 | "loc": { 72 | "start": { 73 | "line": 1, 74 | "column": 0 75 | }, 76 | "end": { 77 | "line": 1, 78 | "column": 14 79 | }, 80 | "source": "source.js" 81 | } 82 | }, 83 | "right": { 84 | "type": "MemberExpression", 85 | "computed": false, 86 | "object": { 87 | "type": "CallExpression", 88 | "callee": { 89 | "type": "Identifier", 90 | "name": "require", 91 | "loc": { 92 | "start": { 93 | "line": 1, 94 | "column": 17 95 | }, 96 | "end": { 97 | "line": 1, 98 | "column": 24 99 | }, 100 | "source": "source.js" 101 | } 102 | }, 103 | "arguments": [ 104 | { 105 | "type": "Literal", 106 | "value": "./module", 107 | "raw": "\"./module\"", 108 | "loc": { 109 | "start": { 110 | "line": 1, 111 | "column": 25 112 | }, 113 | "end": { 114 | "line": 1, 115 | "column": 35 116 | }, 117 | "source": "source.js" 118 | } 119 | } 120 | ], 121 | "loc": { 122 | "start": { 123 | "line": 1, 124 | "column": 17 125 | }, 126 | "end": { 127 | "line": 1, 128 | "column": 36 129 | }, 130 | "source": "source.js" 131 | } 132 | }, 133 | "property": { 134 | "type": "Identifier", 135 | "name": "property", 136 | "loc": { 137 | "start": { 138 | "line": 1, 139 | "column": 37 140 | }, 141 | "end": { 142 | "line": 1, 143 | "column": 45 144 | }, 145 | "source": "source.js" 146 | } 147 | }, 148 | "loc": { 149 | "start": { 150 | "line": 1, 151 | "column": 17 152 | }, 153 | "end": { 154 | "line": 1, 155 | "column": 45 156 | }, 157 | "source": "source.js" 158 | } 159 | }, 160 | "loc": { 161 | "start": { 162 | "line": 1, 163 | "column": 0 164 | }, 165 | "end": { 166 | "line": 1, 167 | "column": 45 168 | }, 169 | "source": "source.js" 170 | } 171 | }, 172 | "loc": { 173 | "start": { 174 | "line": 1, 175 | "column": 0 176 | }, 177 | "end": { 178 | "line": 1, 179 | "column": 46 180 | }, 181 | "source": "source.js" 182 | } 183 | } 184 | ], 185 | "loc": { 186 | "start": { 187 | "line": 1, 188 | "column": 19 189 | }, 190 | "end": { 191 | "line": 1, 192 | "column": 37 193 | }, 194 | "source": "template.jst" 195 | } 196 | }, 197 | "generator": false, 198 | "expression": false, 199 | "loc": { 200 | "start": { 201 | "line": 1, 202 | "column": 7 203 | }, 204 | "end": { 205 | "line": 1, 206 | "column": 37 207 | }, 208 | "source": "template.jst" 209 | } 210 | } 211 | ], 212 | "loc": { 213 | "start": { 214 | "line": 1, 215 | "column": 0 216 | }, 217 | "end": { 218 | "line": 1, 219 | "column": 38 220 | }, 221 | "source": "template.jst" 222 | } 223 | }, 224 | "loc": { 225 | "start": { 226 | "line": 1, 227 | "column": 0 228 | }, 229 | "end": { 230 | "line": 1, 231 | "column": 39 232 | }, 233 | "source": "template.jst" 234 | } 235 | } 236 | ], 237 | "loc": { 238 | "start": { 239 | "line": 1, 240 | "column": 0 241 | }, 242 | "end": { 243 | "line": 1, 244 | "column": 39 245 | }, 246 | "source": "template.jst" 247 | } 248 | } -------------------------------------------------------------------------------- /test/estemplate_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var estemplate = require('../lib/estemplate.js'); 4 | var parse = require('esprima').parse; 5 | var generate = require('escodegen').generate; 6 | var genOpts = { 7 | format: { 8 | indent: { 9 | style: '' 10 | }, 11 | newline: ' ', 12 | quotes: 'double' 13 | } 14 | }; 15 | var readFile = require('fs').readFile; 16 | 17 | function tmplTest(tmpl, data, code) { 18 | return function (test) { 19 | var ast = estemplate(tmpl, data); 20 | test.equal(generate(ast, genOpts), code); 21 | test.done(); 22 | }; 23 | } 24 | 25 | exports.estemplate = { 26 | 'ast locations': function (test) { 27 | var ast = estemplate('define(function () { <%= stmt %> });', {loc: true, source: 'template.jst'}, { 28 | stmt: parse('module.exports = require("./module").property;', {loc: true, source: 'source.js'}).body[0] 29 | }); 30 | 31 | readFile(__dirname + '/custom_options.ast.json', 'utf-8', function (err, expectedAstJson) { 32 | if (err) { 33 | throw err; 34 | } 35 | 36 | var expectedAst = JSON.parse(expectedAstJson); 37 | 38 | test.deepEqual(ast, expectedAst); 39 | test.done(); 40 | }); 41 | }, 42 | 43 | 'simple substitution': tmplTest('var <%= varName %> = <%= value %> + 1;', { 44 | varName: {type: 'Identifier', name: 'myVar'}, 45 | value: {type: 'Literal', value: 123} 46 | }, 'var myVar = 123 + 1;'), 47 | 48 | spread: { 49 | 'array elements': tmplTest('var a = [%= items %];', { 50 | items: [{type: 'Literal', value: 123}, {type: 'Literal', value: 456}] 51 | }, 'var a = [ 123, 456 ];'), 52 | 53 | 'call arguments': tmplTest('var x = f(%= items %);', { 54 | items: [{type: 'Literal', value: 123}, {type: 'Literal', value: 456}] 55 | }, 'var x = f(123, 456);'), 56 | 57 | 'function params': tmplTest('function f(%= params %) {}', { 58 | params: [{type: 'Identifier', name: 'a'}, {type: 'Identifier', name: 'b'}] 59 | }, 'function f(a, b) { }'), 60 | 61 | 'block statements': tmplTest('define(function () {%= body %});', { 62 | body: parse('module.exports = require("./module").property;').body 63 | }, 'define(function () { module.exports = require("./module").property; });'), 64 | 65 | 'program root': tmplTest('var x = 42; %= body %', { 66 | body: parse('module.exports = require("./module").property;').body 67 | }, 'var x = 42; module.exports = require("./module").property;'), 68 | 69 | 'literals': tmplTest('var a = "%= x %"; var b = \'%= y %\';', { 70 | x: 'alpha', 71 | y: 'beta' 72 | }, 'var a = "alpha"; var b = "beta";'), 73 | 74 | 'concatenate with inline elements': { 75 | 'in the beginning': tmplTest('var a = [123, %= items %];', { 76 | items: [{type: 'Literal', value: 456}, {type: 'Literal', value: 789}] 77 | }, 'var a = [ 123, 456, 789 ];'), 78 | 79 | 'in the end': tmplTest('function f(%= params %, callback) {}', { 80 | params: [{type: 'Identifier', name: 'a'}, {type: 'Identifier', name: 'b'}] 81 | }, 'function f(a, b, callback) { }'), 82 | 83 | 'around': tmplTest('function f() { console.time("module"); %= body %; console.timeEnd("module"); }', { 84 | body: parse('init(); doSmth(); finalize();').body 85 | }, 'function f() { console.time("module"); init(); doSmth(); finalize(); console.timeEnd("module"); }'), 86 | 87 | 'in between': tmplTest('function f() { %= init %; doSmth(); %= finalize %; }', { 88 | init: parse('console.time("module"); init();').body, 89 | finalize: parse('finalize(); console.timeEnd("module");').body 90 | }, 'function f() { console.time("module"); init(); doSmth(); finalize(); console.timeEnd("module"); }') 91 | } 92 | } 93 | }; 94 | --------------------------------------------------------------------------------