diff --git a/lib/extend/tag.js b/lib/extend/tag.js index ba83da9d33..e3b9a48d9e 100644 --- a/lib/extend/tag.js +++ b/lib/extend/tag.js @@ -2,7 +2,7 @@ const { stripIndent } = require('hexo-util'); const { cyan } = require('chalk'); -const { Environment } = require('hexo-renderer-nunjucks'); +const { Environment } = require('nunjucks'); const Promise = require('bluebird'); const placeholder = '\uFFFC'; const rPlaceholder = /(?:<|<)!--\uFFFC(\d+)--(?:>|>)/g; diff --git a/lib/plugins/renderer/index.js b/lib/plugins/renderer/index.js index 356cf0c211..d9f03fe718 100644 --- a/lib/plugins/renderer/index.js +++ b/lib/plugins/renderer/index.js @@ -12,11 +12,13 @@ module.exports = ctx => { renderer.register('json', 'json', require('./json'), true); - const nunjucks = require('hexo-renderer-nunjucks'); - nunjucks.register(ctx); - const yaml = require('./yaml'); renderer.register('yml', 'json', yaml, true); renderer.register('yaml', 'json', yaml, true); + + const nunjucks = require('./nunjucks'); + + renderer.register('njk', 'html', nunjucks, true); + renderer.register('j2', 'html', nunjucks, true); }; diff --git a/lib/plugins/renderer/nunjucks.js b/lib/plugins/renderer/nunjucks.js new file mode 100644 index 0000000000..b6760e0e00 --- /dev/null +++ b/lib/plugins/renderer/nunjucks.js @@ -0,0 +1,70 @@ + +'use strict'; + +const nunjucks = require('nunjucks'); +const { readFileSync } = require('hexo-fs'); +const { dirname } = require('path'); + +function toArray(value) { + if (Array.isArray(value)) { + // Return if given value is an Array + return value; + } else if (typeof value.toArray === 'function') { + return value.toArray(); + } else if (value instanceof Map) { + const arr = []; + value.forEach(v => arr.push(v)); + return arr; + } else if (value instanceof Set || typeof value === 'string') { + return [...value]; + } else if (typeof value === 'object' && value instanceof Object && Boolean(value)) { + return Object.values(value); + } + + return []; +} + +function safeJsonStringify(json, spacer = undefined) { + if (typeof json !== 'undefined' && json !== null) { + return JSON.stringify(json, null, spacer); + } + + return '""'; +} + +const nunjucksCfg = { + autoescape: false, + throwOnUndefined: false, + trimBlocks: false, + lstripBlocks: false +}; + +const nunjucksAddFilter = env => { + env.addFilter('toarray', toArray); + env.addFilter('safedump', safeJsonStringify); +}; + +function njkCompile(data) { + let env; + if (data.path) { + env = nunjucks.configure(dirname(data.path), nunjucksCfg); + } else { + env = nunjucks.configure(nunjucksCfg); + } + nunjucksAddFilter(env); + + const text = 'text' in data ? data.text : readFileSync(data.path); + + return nunjucks.compile(text, env, data.path); +} + +function njkRenderer(data, locals) { + return njkCompile(data).render(locals); +} + +njkRenderer.compile = data => { + // Need a closure to keep the compiled template. + return locals => njkCompile(data).render(locals); +}; + +module.exports = njkRenderer; diff --git a/package.json b/package.json index cf3edc8a97..778aa0208a 100644 --- a/package.json +++ b/package.json @@ -48,12 +48,12 @@ "hexo-fs": "^3.1.0", "hexo-i18n": "^1.0.0", "hexo-log": "^2.0.0", - "hexo-renderer-nunjucks": "^2.0.0", "hexo-util": "^2.0.0", "js-yaml": "^3.12.0", "micromatch": "^4.0.2", "moment": "^2.22.2", "moment-timezone": "^0.5.21", + "nunjucks": "^3.2.1", "pretty-hrtime": "^1.0.3", "resolve": "^1.8.1", "strip-ansi": "^6.0.0", diff --git a/test/fixtures/hello.njk b/test/fixtures/hello.njk new file mode 100644 index 0000000000..4ce626e9be --- /dev/null +++ b/test/fixtures/hello.njk @@ -0,0 +1 @@ +Hello {{ name }}! diff --git a/test/scripts/renderers/index.js b/test/scripts/renderers/index.js index b79059d7f3..30808e80a1 100644 --- a/test/scripts/renderers/index.js +++ b/test/scripts/renderers/index.js @@ -4,4 +4,5 @@ describe('Renderers', () => { require('./json'); require('./plain'); require('./yaml'); + require('./nunjucks'); }); diff --git a/test/scripts/renderers/nunjucks.js b/test/scripts/renderers/nunjucks.js new file mode 100644 index 0000000000..bc8717d5b3 --- /dev/null +++ b/test/scripts/renderers/nunjucks.js @@ -0,0 +1,226 @@ +'use strict'; + +require('chai').should(); +const r = require('../../../lib/plugins/renderer/nunjucks'); +const { dirname, join } = require('path'); + +describe('nunjucks', () => { + const fixturePath = join(dirname(dirname(__dirname)), 'fixtures', 'hello.njk'); + + it('render from string', () => { + const body = [ + 'Hello {{ name }}!' + ].join('\n'); + + r({ text: body }, { + name: 'world' + }).should.eql('Hello world!'); + }); + + it('render from path', () => { + r({ path: fixturePath }, { + name: 'world' + }).should.matches(/^Hello world!\s*$/); + }); + + it('compile from text', () => { + const body = [ + 'Hello {{ name }}!' + ].join('\n'); + + const render = r.compile({ + text: body + }); + + render({ + name: 'world' + }).should.eql('Hello world!'); + }); + + it('compile from an .njk file', () => { + const render = r.compile({ + path: fixturePath + }); + + render({ + name: 'world' + }).should.eql('Hello world!\n'); + }); + + describe('nunjucks filters', () => { + const forLoop = [ + '{% for x in arr | toarray %}', + '{{ x }}', + '{% endfor %}' + ].join(''); + + it('toarray can iterate on Warehouse collections', () => { + const data = { + arr: { + toArray() { + return [1, 2, 3]; + } + } + }; + + r({ text: forLoop }, data).should.eql('123'); + }); + + it('toarray can iterate on plain array', () => { + const data = { + arr: [1, 2, 3] + }; + + r({ text: forLoop }, data).should.eql('123'); + }); + + it('toarray can iterate on string', () => { + const data = { + arr: '123' + }; + + r({ text: forLoop }, data).should.eql('123'); + }); + + // https://github.com/lodash/lodash/blob/master/test/toarray.test.js + it('toarray can iterate on objects', () => { + const data = { + arr: { a: '1', b: '2', c: '3' } + }; + + r({ text: forLoop }, data).should.eql('123'); + }); + + it('toarray can iterate on object string', () => { + const data = { + arr: Object('123') + }; + + r({ text: forLoop }, data).should.eql('123'); + }); + + it('toarray can iterate on Map', () => { + const data = { + arr: new Map() + }; + + data.arr.set('a', 1); + data.arr.set('b', 2); + data.arr.set('c', 3); + + r({ text: forLoop }, data).should.eql('123'); + }); + + it('toarray can iterate on Set', () => { + const data = { + arr: new Set() + }; + + data.arr.add(1); + data.arr.add(2); + data.arr.add(3); + + r({ text: forLoop }, data).should.eql('123'); + }); + + it('safedump undefined', () => { + const text = [ + '{{ items | safedump }}' + ].join('\n'); + + r({ text }).should.eql('""'); + }); + + it('safedump null', () => { + const text = [ + '{% set items = null %}', + '{{ items | safedump }}' + ].join('\n'); + + r({ text }).should.eql('\n""'); + }); + + // Adapt from nunjucks test cases + // https://github.com/mozilla/nunjucks/blob/9a0ce364effd28fcdb3ab922fcffa9343b7b3630/tests/filters.js#L98 + it('safedump default', () => { + const text = [ + '{% set items = ["a", 1, { b : true}] %}', + '{{ items | safedump }}' + ].join('\n'); + + r({ text }).should.eql('\n["a",1,{"b":true}]'); + }); + + it('safedump spacer - 2', () => { + const text = [ + '{% set items = ["a", 1, { b : true}] %}', + '{{ items | safedump(2) }}' + ].join('\n'); + + r({ text }).should.eql([ + '', + '[', + ' "a",', + ' 1,', + ' {', + ' "b": true', + ' }', + ']' + ].join('\n')); + }); + + it('safedump spacer - 2', () => { + const text = [ + '{% set items = ["a", 1, { b : true}] %}', + '{{ items | safedump(2) }}' + ].join('\n'); + + r({ text }).should.eql([ + '', + '[', + ' "a",', + ' 1,', + ' {', + ' "b": true', + ' }', + ']' + ].join('\n')); + }); + + it('safedump spacer - 4', () => { + const text = [ + '{% set items = ["a", 1, { b : true}] %}', + '{{ items | safedump(4) }}' + ].join('\n'); + + r({ text }).should.eql([ + '', + '[', + ' "a",', + ' 1,', + ' {', + ' "b": true', + ' }', + ']' + ].join('\n')); + }); + + it('safedump spacer - \\t', () => { + const text = [ + '{% set items = ["a", 1, { b : true}] %}', + '{{ items | safedump(\'\t\') }}' + ].join('\n'); + + r({ text }).should.eql([ + '', + '[', + '\t"a",', + '\t1,', + '\t{', + '\t\t"b": true', + '\t}', + ']' + ].join('\n')); + }); + }); +});