1 | 'use strict';
|
2 |
|
3 | const stripIndent = require('strip-indent');
|
4 | const { cyan } = require('chalk');
|
5 | const nunjucks = require('nunjucks');
|
6 | const { inherits } = require('util');
|
7 | const Promise = require('bluebird');
|
8 |
|
9 | function Tag() {
|
10 | this.env = new nunjucks.Environment(null, {
|
11 | autoescape: false
|
12 | });
|
13 | }
|
14 |
|
15 | Tag.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 |
|
46 | const placeholder = '\uFFFC';
|
47 | const rPlaceholder = /(?:<|<)!--\uFFFC(\d+)--(?:>|>)/g;
|
48 |
|
49 | function 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 |
|
57 | const LINES_OF_CONTEXT = 5;
|
58 | function 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 |
|
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 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 | function 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 |
|
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 |
|
108 | Tag.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 |
|
125 | function NunjucksTag(name, fn) {
|
126 | this.tags = [name];
|
127 | this.fn = fn;
|
128 | }
|
129 |
|
130 | NunjucksTag.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 |
|
136 | NunjucksTag.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 |
|
165 | NunjucksTag.prototype.run = function(context, args) {
|
166 | return this._run(context, args, '');
|
167 | };
|
168 |
|
169 | NunjucksTag.prototype._run = function(context, args, body) {
|
170 | return Reflect.apply(this.fn, context.ctx, [args, body]);
|
171 | };
|
172 |
|
173 | function NunjucksBlock(name, fn) {
|
174 | Reflect.apply(NunjucksTag, this, [name, fn]);
|
175 | }
|
176 |
|
177 | inherits(NunjucksBlock, NunjucksTag);
|
178 |
|
179 | NunjucksBlock.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 |
|
186 | NunjucksBlock.prototype._parseBody = function(parser, nodes, lexer) {
|
187 | const body = parser.parseUntilBlocks(`end${this.tags[0]}`);
|
188 |
|
189 | parser.advanceAfterBlockEnd();
|
190 | return body;
|
191 | };
|
192 |
|
193 | NunjucksBlock.prototype.run = function(context, args, body) {
|
194 | return this._run(context, args, trimBody(body));
|
195 | };
|
196 |
|
197 | function trimBody(body) {
|
198 | return stripIndent(body()).replace(/^\n?|\n?$/g, '');
|
199 | }
|
200 |
|
201 | function NunjucksAsyncTag(name, fn) {
|
202 | Reflect.apply(NunjucksTag, this, [name, fn]);
|
203 | }
|
204 |
|
205 | inherits(NunjucksAsyncTag, NunjucksTag);
|
206 |
|
207 | NunjucksAsyncTag.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 |
|
213 | NunjucksAsyncTag.prototype.run = function(context, args, callback) {
|
214 | return this._run(context, args, '').then(result => {
|
215 | callback(null, result);
|
216 | }, callback);
|
217 | };
|
218 |
|
219 | function NunjucksAsyncBlock(name, fn) {
|
220 | Reflect.apply(NunjucksBlock, this, [name, fn]);
|
221 | }
|
222 |
|
223 | inherits(NunjucksAsyncBlock, NunjucksBlock);
|
224 |
|
225 | NunjucksAsyncBlock.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 |
|
232 | NunjucksAsyncBlock.prototype.run = function(context, args, body, callback) {
|
233 |
|
234 | body((err, result) => {
|
235 |
|
236 |
|
237 | body = () => result || '';
|
238 |
|
239 | this._run(context, args, trimBody(body)).then(result => {
|
240 | callback(err, result);
|
241 | });
|
242 | });
|
243 | };
|
244 |
|
245 | module.exports = Tag;
|