├── .travis.yml ├── tests ├── data.json ├── autoescape │ ├── input.html │ ├── output.html │ └── index.js ├── leaking-vars │ ├── input2.html │ ├── input1.html │ ├── output2.html │ ├── base.html │ ├── output1.html │ └── index.js ├── all.js └── base │ ├── output.html │ ├── input.html │ └── index.js ├── .gitignore ├── .editorconfig ├── Gruntfile.js ├── package.json ├── LICENSE ├── tasks └── nunjucks.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_script: npm run test 3 | node_js: 4 | - 4.0 5 | notifications: 6 | email: false -------------------------------------------------------------------------------- /tests/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Hello, world", 3 | "content": "The world is mine!", 4 | "html": "Hello, world" 5 | } 6 | -------------------------------------------------------------------------------- /tests/autoescape/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ html }} 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/leaking-vars/input2.html: -------------------------------------------------------------------------------- 1 | {% extends "tests/leaking-vars/base.html" %} 2 | 3 | {% set testVar = "testVar from page2" %} 4 | 5 | {% block main %}main block from page2{% endblock %} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | .DS_Store 10 | 11 | pids 12 | logs 13 | results 14 | 15 | npm-debug.log 16 | node_modules 17 | 18 | tests/**/_* -------------------------------------------------------------------------------- /tests/autoescape/output.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <span>Hello, world</span> 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/all.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const tests = ['base', 'leaking-vars', 'autoescape'] 5 | 6 | tests.forEach(function (folder) { 7 | require(path.join(__dirname, folder))() 8 | }) 9 | -------------------------------------------------------------------------------- /tests/leaking-vars/input1.html: -------------------------------------------------------------------------------- 1 | {% extends "tests/leaking-vars/base.html" %} 2 | 3 | {% set testVar = "testVar from page1" %} 4 | {% set testVar2 = "testVar2 from page1" %} 5 | 6 | {% block main %}main block from page1{% endblock %} -------------------------------------------------------------------------------- /tests/base/output.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello, world 6 | 7 | 8 | Hello, I'm a global var bar 9 | input 10 | The world is mine! 11 | 12 | -------------------------------------------------------------------------------- /tests/leaking-vars/output2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

testVar: testVar from page2

8 |

testVar2:

9 |

MainBlock: main block from page2

10 | 11 | -------------------------------------------------------------------------------- /tests/base/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | Hello, I'm a global var {{ foo }} 9 | {{ page }} 10 | {{ content }} 11 | 12 | -------------------------------------------------------------------------------- /tests/leaking-vars/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

testVar: {{ testVar }}

8 |

testVar2: {{ testVar2 }}

9 |

MainBlock: {% block main %}{% endblock %}

10 | 11 | -------------------------------------------------------------------------------- /tests/leaking-vars/output1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

testVar: testVar from page1

8 |

testVar2: testVar2 from page1

9 |

MainBlock: main block from page1

10 | 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = spaces 13 | indent_size = 4 14 | 15 | [*.json] 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /tests/base/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const expect = require('expect.js') 4 | const fs = require('fs') 5 | const util = require('util') 6 | const path = require('path') 7 | 8 | let expected = fs.readFileSync(path.join(__dirname, 'output.html')).toString() 9 | let generated = fs.readFileSync(path.join(__dirname, '_output.html')).toString() 10 | 11 | module.exports = function () { 12 | try { 13 | expect(expected).to.eql(generated) 14 | } catch (e) { 15 | console.log(util.inspect(e, {colors: true})) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/autoescape/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const expect = require('expect.js') 4 | const fs = require('fs') 5 | const util = require('util') 6 | const path = require('path') 7 | 8 | let expected = fs.readFileSync(path.join(__dirname, 'output.html')).toString() 9 | let generated = fs.readFileSync(path.join(__dirname, '_output.html')).toString() 10 | 11 | module.exports = function () { 12 | try { 13 | expect(expected).to.eql(generated) 14 | } catch (e) { 15 | console.log(util.inspect(e, {colors: true})) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/leaking-vars/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const expect = require('expect.js') 4 | const fs = require('fs') 5 | const util = require('util') 6 | const path = require('path') 7 | 8 | let expected = [ 9 | fs.readFileSync(path.join(__dirname, 'output1.html')).toString(), 10 | fs.readFileSync(path.join(__dirname, 'output2.html')).toString() 11 | ] 12 | let generated = [ 13 | fs.readFileSync(path.join(__dirname, '_output1.html')).toString(), 14 | fs.readFileSync(path.join(__dirname, '_output2.html')).toString() 15 | ] 16 | 17 | module.exports = function () { 18 | expected.forEach((input, i) => { 19 | try { 20 | expect(input).to.eql(generated[i]) 21 | } catch (e) { 22 | console.log(util.inspect(e, {colors: true})) 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | module.exports = function (grunt) { 4 | grunt.initConfig({ 5 | nunjucks: { 6 | options: { 7 | fooName: 'foo', 8 | data: grunt.file.readJSON('tests/data.json'), 9 | preprocessData: function (data) { 10 | data.page = path.basename(this.src[0], '.html') 11 | return data 12 | }, 13 | configureEnvironment: function (env) { 14 | var options = this.options() 15 | env.addGlobal(options.fooName, 'bar') 16 | } 17 | }, 18 | render: { 19 | files: { 20 | 'tests/base/_output.html': ['tests/base/input.html'], 21 | 'tests/autoescape/_output.html': ['tests/autoescape/input.html'], 22 | 'tests/leaking-vars/_output1.html': ['tests/leaking-vars/input1.html'], 23 | 'tests/leaking-vars/_output2.html': ['tests/leaking-vars/input2.html'] 24 | } 25 | } 26 | } 27 | }) 28 | 29 | grunt.loadTasks('tasks/') 30 | 31 | grunt.registerTask('test', ['nunjucks']) 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-nunjucks-2-html", 3 | "version": "3.1.0", 4 | "description": "Grunt task for rendering nunjucks` templates to HTML", 5 | "homepage": "https://github.com/vitkarpov/grunt-nunjucks-2-html", 6 | "author": "Viktor Karpov ", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/vitkarpov/grunt-nunjucks-2-html.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/vitkarpov/grunt-nunjucks-2-html/issues" 13 | }, 14 | "license": "MIT", 15 | "keywords": [ 16 | "gruntplugin", 17 | "nunjucks", 18 | "compile" 19 | ], 20 | "engines": { 21 | "node": ">=4.0.0" 22 | }, 23 | "dependencies": { 24 | "chalk": "^1.1.1", 25 | "nunjucks": "^3.0.0" 26 | }, 27 | "devDependencies": { 28 | "babel-eslint": "^7.2.3", 29 | "expect.js": "^0.3.1", 30 | "grunt": "^1.0.1", 31 | "snazzy": "^7.0.0", 32 | "standard": "^10.0.2" 33 | }, 34 | "scripts": { 35 | "test": "standard | snazzy && grunt test && node tests/all.js" 36 | }, 37 | "standard": { 38 | "parser": "babel-eslint" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Victor Karpov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tasks/nunjucks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt-nunjucks-2-html 3 | * https://github.com/vitkarpov/grunt-nunjucks-2-html 4 | * 5 | * Copyright (c) 2014 Vit Karpov 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict' 10 | 11 | const nunjucks = require('nunjucks') 12 | const chalk = require('chalk') 13 | const path = require('path') 14 | 15 | module.exports = function (grunt) { 16 | grunt.registerMultiTask('nunjucks', `Renders Nunjucks' templates to HTML`, function () { 17 | // Declare async task 18 | const completeTask = this.async() 19 | 20 | // Get options and set defaults 21 | const options = this.options({ 22 | watch: false, 23 | paths: '', 24 | configureEnvironment: false, 25 | data: false, 26 | preprocessData: false, 27 | noCache: true 28 | }) 29 | 30 | // Finish task if no files specified 31 | if (!this.files.length) { 32 | grunt.log.error('No files specified.') 33 | 34 | // Finish task — nothing we can do without specified files 35 | return completeTask() 36 | } 37 | 38 | // Warn in case of undefined data 39 | if (!options.data) { 40 | grunt.log.error(`Template's data is empty. Guess you've forget to specify data option.`) 41 | } 42 | 43 | // Arm Nunjucks 44 | const env = nunjucks.configure(options.paths, options) 45 | 46 | // Pass configuration to Nunjucks if specified 47 | if (typeof options.configureEnvironment === 'function') { 48 | options.configureEnvironment.call(this, env, nunjucks) 49 | } 50 | 51 | // Get number of files 52 | const totalFiles = this.files.length 53 | // Start counter for number of compiled files 54 | let countCompiled = 0 55 | 56 | // Run compilation asynchronously, wait for finish, then print results and complete task 57 | const task = new Promise((resolve, reject) => { 58 | // Iterate over all files' groups 59 | this.files.forEach(file => { 60 | // Set destination 61 | let filedest = file.dest 62 | 63 | // Check whether there are any source files 64 | if (!file.src.length) { 65 | grunt.log.error(`No source files specified for ${chalk.cyan(filedest)}.`) 66 | 67 | // Skip to next file — nothing we can do without specified source files 68 | return reject(new Error('For some destinations were not specified source files.')) 69 | } 70 | 71 | // Iterate over files' sources 72 | file.src.forEach(src => { 73 | // Construct absolute path to file for Nunjucks 74 | let filepath = path.join(process.cwd(), src) 75 | 76 | let data = {} 77 | // Clone data 78 | for (let i in options.data) { 79 | if (options.data.hasOwnProperty(i)) { 80 | data[i] = options.data[i] 81 | } 82 | } 83 | 84 | // Preprocess data 85 | if (options.data && typeof options.preprocessData === 'function') { 86 | data = options.preprocessData.call(file, data) 87 | } 88 | 89 | // Asynchronously render templates with configurated Nunjucks environment 90 | // and write to destination 91 | env.render(filepath, data, (error, result) => { 92 | // Catch errors, warn 93 | if (error) { 94 | grunt.log.error(error) 95 | grunt.fail.warn('Failed to compile one of the source files.') 96 | grunt.log.writeln() 97 | 98 | // Prevent writing of failed to compile file, skip to next file 99 | return reject(new Error('Failed to compile some source files.')) 100 | } 101 | 102 | // Write rendered template to destination 103 | grunt.file.write(filedest, result) 104 | 105 | // Debug process 106 | grunt.verbose.ok(`File ${chalk.cyan(filedest)} created.`) 107 | grunt.verbose.writeln() 108 | 109 | countCompiled++ 110 | }) 111 | }) 112 | }) 113 | 114 | // Finish Promise 115 | resolve() 116 | }) 117 | 118 | // Print any errors from rejects 119 | task.catch(error => { 120 | if (error) { 121 | grunt.log.writeln() 122 | grunt.log.error(error) 123 | grunt.log.writeln() 124 | } 125 | }) 126 | 127 | // Log number of processed templates 128 | task.then(success => { 129 | // Log number of processed templates 130 | let logType = (countCompiled === totalFiles) ? 'ok' : 'error' 131 | let countCompiledColor = (logType === 'ok') ? 'green' : 'red' 132 | grunt.log[logType](`${chalk[countCompiledColor](countCompiled)}/${chalk.cyan(totalFiles)} ${grunt.util.pluralize(totalFiles, 'file/files')} compiled.`) 133 | }) 134 | 135 | // Finish async task 136 | completeTask() 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grunt task for rendering nunjucks` templates to HTML 2 | 3 | [![NPM version](https://badge.fury.io/js/grunt-nunjucks-2-html.png)](http://badge.fury.io/js/grunt-nunjucks-2-html) 4 | [![Build Status](https://travis-ci.org/vitkarpov/grunt-nunjucks-2-html.svg?branch=master)](https://travis-ci.org/vitkarpov/grunt-nunjucks-2-html) 5 | 6 | ## Getting start 7 | 8 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide. 9 | 10 | Once plugin has been installed include it in your `Gruntfile.js` 11 | 12 | ```javascript 13 | grunt.loadNpmTasks('grunt-nunjucks-2-html'); 14 | ``` 15 | 16 | ## Usage examples 17 | 18 | Task targets and options may be specified according to the grunt [Configuring tasks](http://gruntjs.com/configuring-tasks) guide. 19 | 20 | ```javascript 21 | nunjucks: { 22 | options: { 23 | data: grunt.file.readJSON('data.json'), 24 | paths: 'templates' 25 | }, 26 | render: { 27 | files: { 28 | 'index.html' : ['index.html'] 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | `templates/index.html` (relative to the gruntfile) is now compiled with `data.json`! 35 | 36 | ```javascipt 37 | nunjucks: { 38 | options: { 39 | data: grunt.file.readJSON('data.json') 40 | }, 41 | render: { 42 | files: [ 43 | { 44 | expand: true, 45 | cwd: "bundles/", 46 | src: "*.html", 47 | dest: "build/", 48 | ext: ".html" 49 | } 50 | ] 51 | } 52 | } 53 | ``` 54 | 55 | You'll get a set of html files in `build` folder. 56 | 57 | ## Tests 58 | 59 | ```bash 60 | $ npm test 61 | ``` 62 | 63 | ## Options 64 | 65 | ### Data 66 | 67 | Read JSON from file using `grunt.file.readJSON` or specify object just inside your `Gruntfile`. 68 | 69 | ### preprocessData 70 | 71 | You should specify a function to construct each data object for every of your templates. Execution context for the function would be a [grunt file object](http://gruntjs.com/api/inside-tasks#this.files). If you specify a data option it would be passed inside the function as an argument. 72 | 73 | For instance, you could include name of the file inside an every data object 74 | 75 | ```js 76 | nunjucks: { 77 | options: { 78 | preprocessData: function(data) { 79 | var page = require('path').basename(this.src[0], '.html'); 80 | var result = { 81 | page: page, 82 | data: data 83 | }; 84 | return result; 85 | }, 86 | data: grunt.file.readJSON('data.json') 87 | }, 88 | render: { 89 | files: [ 90 | { 91 | expand: true, 92 | cwd: "bundles/", 93 | src: "*.html", 94 | dest: "build/", 95 | ext: ".html" 96 | } 97 | ] 98 | } 99 | } 100 | ``` 101 | 102 | ### paths 103 | 104 | You could specify root path for your templates, `paths` would be set for [nunjucks' configure](http://mozilla.github.io/nunjucks/api#configure) 105 | 106 | ### configureEnvironment 107 | 108 | You could use nunjucks' environment API to set some global options. Use `configureEnvironment` function the same way as `preprocessData`. 109 | 110 | As the second argument for the function you have nunjucks` instance, so you can do some extra work before rendering. For instance, you can pre-render some string in custom filter or extension. 111 | 112 | ```js 113 | nunjucks: { 114 | options: { 115 | configureEnvironment: function(env, nunjucks) { 116 | // for instance, let's set a global variable across all templates 117 | env.addGlobal('foo', 'bar'); 118 | } 119 | }, 120 | render: { 121 | files: [ 122 | { 123 | expand: true, 124 | cwd: "bundles/", 125 | src: "*.html", 126 | dest: "build/", 127 | ext: ".html" 128 | } 129 | ] 130 | } 131 | } 132 | ``` 133 | 134 | Check out [nunjucks' API](http://mozilla.github.io/nunjucks/api.html#environment) to know a list of available methods for environment object. 135 | 136 | ### Nunjucks' configure API 137 | 138 | You can use [nunjucks' configure API](http://mozilla.github.io/nunjucks/api#configure) as options for plugin. 139 | 140 | ### tags 141 | 142 | If you want different tokens than {{ and the rest for variables, blocks, and comments, you can specify different tokens as the tags option: 143 | 144 | ```js 145 | nunjucks: { 146 | options: { 147 | tags: { 148 | blockStart: '<%', 149 | blockEnd: '%>', 150 | variableStart: '<$', 151 | variableEnd: '$>', 152 | commentStart: '<#', 153 | commentEnd: '#>' 154 | }, 155 | data: grunt.file.readJSON('data.json') 156 | }, 157 | render: { 158 | files: [ 159 | { 160 | expand: true, 161 | cwd: "bundles/", 162 | src: "*.html", 163 | dest: "build/", 164 | ext: ".html" 165 | } 166 | ] 167 | } 168 | } 169 | ``` 170 | 171 | #### autoescape 172 | 173 | By default, nunjucks escapes all output. [Details](http://mozilla.github.io/nunjucks/api#autoescaping) 174 | 175 | #### throwOnUndefined 176 | 177 | Throw errors when outputting a null/undefined value 178 | 179 | #### trimBlocks 180 | 181 | Automatically remove trailing newlines from a block/tag 182 | 183 | #### lstripBlocks 184 | 185 | Automatically remove leading whitespace from a block/tag 186 | --------------------------------------------------------------------------------