UNPKG

6.69 kBJavaScriptView Raw
1'use strict';
2
3const stripIndent = require('strip-indent');
4const { cyan } = require('chalk');
5const nunjucks = require('nunjucks');
6const { inherits } = require('util');
7const Promise = require('bluebird');
8
9function Tag() {
10 this.env = new nunjucks.Environment(null, {
11 autoescape: false
12 });
13}
14
15Tag.prototype.register = function(name, fn, options) {
16 if (!name) throw new TypeError('name is required');
17 if (typeof fn !== 'function') throw new TypeError('fn must be a function');
18
19 if (options == null || typeof options === 'boolean') {
20 options = {ends: options};
21 }
22
23 let tag;
24
25 if (options.async) {
26 if (fn.length > 2) {
27 fn = Promise.promisify(fn);
28 } else {
29 fn = Promise.method(fn);
30 }
31
32 if (options.ends) {
33 tag = new NunjucksAsyncBlock(name, fn);
34 } else {
35 tag = new NunjucksAsyncTag(name, fn);
36 }
37 } else if (options.ends) {
38 tag = new NunjucksBlock(name, fn);
39 } else {
40 tag = new NunjucksTag(name, fn);
41 }
42
43 this.env.addExtension(name, tag);
44};
45
46const placeholder = '\uFFFC';
47const rPlaceholder = /(?:<|&lt;)!--\uFFFC(\d+)--(?:>|&gt;)/g;
48
49function getContextLineNums(min, max, center, amplitude) {
50 const result = [];
51 let lbound = Math.max(min, center - amplitude);
52 const hbound = Math.min(max, center + amplitude);
53 while (lbound <= hbound) result.push(lbound++);
54 return result;
55}
56
57const LINES_OF_CONTEXT = 5;
58function getContext(lines, errLine, location, type) {
59 const colorize = cyan;
60 const message = [
61 location + ' ' + type,
62 colorize(' ===== Context Dump ====='),
63 colorize(' === (line number probably different from source) ===')
64 ];
65
66 Array.prototype.push.apply(message,
67 // get LINES_OF_CONTEXT lines surrounding `errLine`
68 getContextLineNums(1, lines.length, errLine, LINES_OF_CONTEXT)
69 .map(lnNum => {
70 const line = ' ' + lnNum + ' | ' + lines[lnNum - 1];
71 if (lnNum === errLine) {
72 return colorize.bold(line);
73 }
74
75 return colorize(line);
76 })
77 );
78 message.push(colorize(
79 ' ===== Context Dump Ends ====='));
80
81 return message;
82}
83
84/**
85 * Provide context for Nunjucks error
86 * @param {Error} err Nunjucks error
87 * @param {string} str string input for Nunjucks
88 * @return {Error} New error object with embedded context
89 */
90function formatNunjucksError(err, input) {
91 const match = err.message.match(/Line (\d+), Column \d+/);
92 if (!match) return err;
93 const errLine = parseInt(match[1], 10);
94 if (isNaN(errLine)) return err;
95
96 // trim useless info from Nunjucks Error
97 const splited = err.message.replace('(unknown path)', '').split('\n');
98
99 const e = new Error();
100 e.name = 'Nunjucks Error';
101 e.line = errLine;
102 e.location = splited[0];
103 e.type = splited[1].trim();
104 e.message = getContext(input.split(/\r?\n/), errLine, e.location, e.type).join('\n');
105 return e;
106}
107
108Tag.prototype.render = function(str, options, callback) {
109 if (!callback && typeof options === 'function') {
110 callback = options;
111 options = {};
112 }
113
114 const cache = [];
115
116 const escapeContent = str => `<!--${placeholder}${cache.push(str) - 1}-->`;
117
118 str = str.replace(/<pre><code.*>[\s\S]*?<\/code><\/pre>/gm, escapeContent);
119
120 return Promise.fromCallback(cb => { this.env.renderString(str, options, cb); })
121 .catch(err => Promise.reject(formatNunjucksError(err, str)))
122 .then(result => result.replace(rPlaceholder, (_, index) => cache[index]));
123};
124
125function NunjucksTag(name, fn) {
126 this.tags = [name];
127 this.fn = fn;
128}
129
130NunjucksTag.prototype.parse = function(parser, nodes, lexer) {
131 const node = this._parseArgs(parser, nodes, lexer);
132
133 return new nodes.CallExtension(this, 'run', node, []);
134};
135
136NunjucksTag.prototype._parseArgs = (parser, nodes, lexer) => {
137 const tag = parser.nextToken();
138 const node = new nodes.NodeList(tag.lineno, tag.colno);
139 const argarray = new nodes.Array(tag.lineno, tag.colno);
140
141 let token;
142 let argitem = '';
143
144 while ((token = parser.nextToken(true))) {
145 if (token.type === lexer.TOKEN_WHITESPACE || token.type === lexer.TOKEN_BLOCK_END) {
146 if (argitem !== '') {
147 const argnode = new nodes.Literal(tag.lineno, tag.colno, argitem.trim());
148 argarray.addChild(argnode);
149 argitem = '';
150 }
151
152 if (token.type === lexer.TOKEN_BLOCK_END) {
153 break;
154 }
155 } else {
156 argitem += token.value;
157 }
158 }
159
160 node.addChild(argarray);
161
162 return node;
163};
164
165NunjucksTag.prototype.run = function(context, args) {
166 return this._run(context, args, '');
167};
168
169NunjucksTag.prototype._run = function(context, args, body) {
170 return Reflect.apply(this.fn, context.ctx, [args, body]);
171};
172
173function NunjucksBlock(name, fn) {
174 Reflect.apply(NunjucksTag, this, [name, fn]);
175}
176
177inherits(NunjucksBlock, NunjucksTag);
178
179NunjucksBlock.prototype.parse = function(parser, nodes, lexer) {
180 const node = this._parseArgs(parser, nodes, lexer);
181 const body = this._parseBody(parser, nodes, lexer);
182
183 return new nodes.CallExtension(this, 'run', node, [body]);
184};
185
186NunjucksBlock.prototype._parseBody = function(parser, nodes, lexer) {
187 const body = parser.parseUntilBlocks(`end${this.tags[0]}`);
188
189 parser.advanceAfterBlockEnd();
190 return body;
191};
192
193NunjucksBlock.prototype.run = function(context, args, body) {
194 return this._run(context, args, trimBody(body));
195};
196
197function trimBody(body) {
198 return stripIndent(body()).replace(/^\n?|\n?$/g, '');
199}
200
201function NunjucksAsyncTag(name, fn) {
202 Reflect.apply(NunjucksTag, this, [name, fn]);
203}
204
205inherits(NunjucksAsyncTag, NunjucksTag);
206
207NunjucksAsyncTag.prototype.parse = function(parser, nodes, lexer) {
208 const node = this._parseArgs(parser, nodes, lexer);
209
210 return new nodes.CallExtensionAsync(this, 'run', node, []);
211};
212
213NunjucksAsyncTag.prototype.run = function(context, args, callback) {
214 return this._run(context, args, '').then(result => {
215 callback(null, result);
216 }, callback);
217};
218
219function NunjucksAsyncBlock(name, fn) {
220 Reflect.apply(NunjucksBlock, this, [name, fn]);
221}
222
223inherits(NunjucksAsyncBlock, NunjucksBlock);
224
225NunjucksAsyncBlock.prototype.parse = function(parser, nodes, lexer) {
226 const node = this._parseArgs(parser, nodes, lexer);
227 const body = this._parseBody(parser, nodes, lexer);
228
229 return new nodes.CallExtensionAsync(this, 'run', node, [body]);
230};
231
232NunjucksAsyncBlock.prototype.run = function(context, args, body, callback) {
233 // enable async tag nesting
234 body((err, result) => {
235 // wrapper for trimBody expecting
236 // body to be a function
237 body = () => result || '';
238
239 this._run(context, args, trimBody(body)).then(result => {
240 callback(err, result);
241 });
242 });
243};
244
245module.exports = Tag;