├── .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 | [](http://badge.fury.io/js/grunt-nunjucks-2-html)
4 | [](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 |
--------------------------------------------------------------------------------