@rhinostone/swig-jinja2 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/lib/async/pre-walker.js +267 -0
- package/lib/filters.js +1369 -0
- package/lib/index.js +344 -0
- package/lib/lexer.js +305 -0
- package/lib/parser.js +763 -0
- package/lib/tags/autoescape.js +75 -0
- package/lib/tags/block.js +82 -0
- package/lib/tags/elif.js +33 -0
- package/lib/tags/else.js +27 -0
- package/lib/tags/extends.js +77 -0
- package/lib/tags/filter.js +205 -0
- package/lib/tags/for.js +154 -0
- package/lib/tags/from.js +305 -0
- package/lib/tags/if.js +82 -0
- package/lib/tags/import.js +250 -0
- package/lib/tags/include.js +154 -0
- package/lib/tags/index.js +32 -0
- package/lib/tags/macro.js +174 -0
- package/lib/tags/raw.js +51 -0
- package/lib/tags/set.js +164 -0
- package/lib/tags/with.js +121 -0
- package/lib/tests/index.js +213 -0
- package/lib/tokentypes.js +89 -0
- package/package.json +31 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rhinostone/swig-jinja2 — Jinja2 frontend for the @rhinostone/swig family.
|
|
3
|
+
*
|
|
4
|
+
* End-to-end render wiring (Path A): the package exposes a Jinja2
|
|
5
|
+
* constructor + default instance via `engine.install(self, frontend)` from
|
|
6
|
+
* @rhinostone/swig-core, so callers can `render(source, locals)` /
|
|
7
|
+
* `renderFile(path, locals, cb)` directly against Jinja2 syntax.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var utils = require('@rhinostone/swig-core/lib/utils'),
|
|
11
|
+
engine = require('@rhinostone/swig-core/lib/engine'),
|
|
12
|
+
loaders = require('@rhinostone/swig-core/lib/loaders'),
|
|
13
|
+
dateformatter = require('@rhinostone/swig-core/lib/dateformatter'),
|
|
14
|
+
parser = require('./parser'),
|
|
15
|
+
_tags = require('./tags'),
|
|
16
|
+
_filters = require('./filters'),
|
|
17
|
+
_tests = require('./tests'),
|
|
18
|
+
preWalker = require('./async/pre-walker');
|
|
19
|
+
|
|
20
|
+
exports.name = 'jinja2';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Expression-level parser — Pratt-style recursive descent that consumes
|
|
24
|
+
* Jinja2 lexer tokens and returns swig-core IRExpr nodes, plus the
|
|
25
|
+
* top-level `parse(swig, source, opts, tags, filters)` splitter.
|
|
26
|
+
*
|
|
27
|
+
* @type {object}
|
|
28
|
+
*/
|
|
29
|
+
exports.parser = parser;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Built-in Jinja2 tag registry.
|
|
33
|
+
*
|
|
34
|
+
* @type {object}
|
|
35
|
+
*/
|
|
36
|
+
exports.tags = _tags;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Built-in Jinja2 filter catalog.
|
|
40
|
+
*
|
|
41
|
+
* @type {object}
|
|
42
|
+
*/
|
|
43
|
+
exports.filters = _filters;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Template loaders re-exported from swig-core.
|
|
47
|
+
*
|
|
48
|
+
* @type {object}
|
|
49
|
+
*/
|
|
50
|
+
exports.loaders = loaders;
|
|
51
|
+
|
|
52
|
+
var defaultOptions = {
|
|
53
|
+
autoescape: true,
|
|
54
|
+
varControls: ['{{', '}}'],
|
|
55
|
+
tagControls: ['{%', '%}'],
|
|
56
|
+
cmtControls: ['{#', '#}'],
|
|
57
|
+
locals: {},
|
|
58
|
+
cache: 'memory',
|
|
59
|
+
loader: loaders.fs()
|
|
60
|
+
},
|
|
61
|
+
defaultInstance;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate the Jinja2 options object.
|
|
65
|
+
*
|
|
66
|
+
* @param {?object} options Jinja2 options object.
|
|
67
|
+
* @return {undefined} Throws on malformed input.
|
|
68
|
+
* @private
|
|
69
|
+
*/
|
|
70
|
+
function validateOptions(options) {
|
|
71
|
+
if (!options) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
utils.each(['varControls', 'tagControls', 'cmtControls'], function (key) {
|
|
76
|
+
if (!options.hasOwnProperty(key)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!utils.isArray(options[key]) || options[key].length !== 2) {
|
|
80
|
+
throw new Error('Option "' + key + '" must be an array containing 2 different control strings.');
|
|
81
|
+
}
|
|
82
|
+
if (options[key][0] === options[key][1]) {
|
|
83
|
+
throw new Error('Option "' + key + '" open and close controls must not be the same.');
|
|
84
|
+
}
|
|
85
|
+
utils.each(options[key], function (a, i) {
|
|
86
|
+
if (a.length < 2) {
|
|
87
|
+
throw new Error('Option "' + key + '" ' + ((i) ? 'open ' : 'close ') + 'control must be at least 2 characters. Saw "' + a + '" instead.');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (options.hasOwnProperty('cache')) {
|
|
93
|
+
if (options.cache && options.cache !== 'memory') {
|
|
94
|
+
if (!options.cache.get || !options.cache.set) {
|
|
95
|
+
throw new Error('Invalid cache option ' + JSON.stringify(options.cache) + ' found. Expected "memory" or { get: function (key) { ... }, set: function (key, value) { ... } }.');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (options.hasOwnProperty('loader')) {
|
|
100
|
+
if (options.loader) {
|
|
101
|
+
if (!options.loader.load || !options.loader.resolve) {
|
|
102
|
+
throw new Error('Invalid loader option ' + JSON.stringify(options.loader) + ' found. Expected { load: function (pathname, cb) { ... }, resolve: function (to, from) { ... } }.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Set defaults for the base and all new Jinja2 environments.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} [options={}] Jinja2 options object.
|
|
112
|
+
* @return {undefined}
|
|
113
|
+
*/
|
|
114
|
+
exports.setDefaults = function (options) {
|
|
115
|
+
validateOptions(options);
|
|
116
|
+
defaultInstance.options = utils.extend(defaultInstance.options, options);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Set the default TimeZone offset for date formatting via the date filter.
|
|
121
|
+
* Mutates the shared dateformatter's tzOffset — affects every frontend
|
|
122
|
+
* (native swig + swig-jinja2) because both consume the same module instance.
|
|
123
|
+
*
|
|
124
|
+
* @param {number} offset Offset from GMT, in minutes (west of GMT).
|
|
125
|
+
* @return {undefined}
|
|
126
|
+
*/
|
|
127
|
+
exports.setDefaultTZOffset = function (offset) {
|
|
128
|
+
dateformatter.tzOffset = offset;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a new, separate Jinja2 compile/render environment.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* var jinja2 = require('@rhinostone/swig-jinja2');
|
|
136
|
+
* var myjinja2 = new jinja2.Jinja2({ autoescape: false });
|
|
137
|
+
* myjinja2.render('Hello {{ name }}', { locals: { name: 'world' }});
|
|
138
|
+
*
|
|
139
|
+
* @param {object} [opts={}] Jinja2 options object.
|
|
140
|
+
* @return {object} New Jinja2 environment.
|
|
141
|
+
*/
|
|
142
|
+
exports.Jinja2 = function (opts) {
|
|
143
|
+
var self = this;
|
|
144
|
+
validateOptions(opts);
|
|
145
|
+
this.options = utils.extend({}, defaultOptions, opts || {});
|
|
146
|
+
this.cache = {};
|
|
147
|
+
this.extensions = {};
|
|
148
|
+
|
|
149
|
+
engine.install(this, {
|
|
150
|
+
parser: parser,
|
|
151
|
+
tags: _tags,
|
|
152
|
+
filters: _filters,
|
|
153
|
+
validateOptions: validateOptions,
|
|
154
|
+
onCompileError: function (err, options) {
|
|
155
|
+
utils.throwError(err, null, options.filename);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Register Jinja2 `is <name>` runtime helpers as `_ext._test_<name>`.
|
|
160
|
+
// setExtension attaches them to this instance's `extensions` map, so a
|
|
161
|
+
// consumer can override any test per-instance with their own
|
|
162
|
+
// `setExtension('_test_<name>', fn)` after construction.
|
|
163
|
+
utils.each(_tests, function (fn, name) {
|
|
164
|
+
self.setExtension('_test_' + name, fn);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
function buildScanOpts() {
|
|
168
|
+
return {
|
|
169
|
+
varControls: self.options.varControls,
|
|
170
|
+
tagControls: self.options.tagControls,
|
|
171
|
+
cmtControls: self.options.cmtControls,
|
|
172
|
+
rawTag: 'raw',
|
|
173
|
+
keywords: ['extends', 'include', 'import', 'from']
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Render a Jinja2 template file asynchronously, supporting async loaders.
|
|
179
|
+
*
|
|
180
|
+
* Pre-walks <code>extends</code> / <code>include</code> /
|
|
181
|
+
* <code>import</code> / <code>from</code> targets in parallel via the
|
|
182
|
+
* user loader, populates an in-memory map, then runs the existing sync
|
|
183
|
+
* render pipeline against the populated map. Dynamic paths
|
|
184
|
+
* (<code>{% extends parent_var %}</code>) are not pre-resolved and will
|
|
185
|
+
* throw at render time as they would on the sync path.
|
|
186
|
+
*
|
|
187
|
+
* @deprecated since 2.5.0 — use {@link Jinja2#renderFile} with a loader
|
|
188
|
+
* that sets <code>loader.async === true</code>. The async-codegen
|
|
189
|
+
* dispatch handles dynamic include paths the pre-walker cannot. This
|
|
190
|
+
* method will be removed in 3.0.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* jinja2.setDefaults({ loader: myAsyncLoader });
|
|
194
|
+
* jinja2.renderFileAsync('page.html', { name: 'world' }, function (err, output) {
|
|
195
|
+
* if (err) { return done(err); }
|
|
196
|
+
* res.end(output);
|
|
197
|
+
* });
|
|
198
|
+
*
|
|
199
|
+
* @param {string} pathName Template path; resolved via the active loader.
|
|
200
|
+
* @param {object} [locals] Locals to render with.
|
|
201
|
+
* @param {Function} cb Node-style callback <code>(err, output)</code>.
|
|
202
|
+
* @return {undefined}
|
|
203
|
+
*/
|
|
204
|
+
this.renderFileAsync = function (pathName, locals, cb) {
|
|
205
|
+
if (typeof locals === 'function') {
|
|
206
|
+
cb = locals;
|
|
207
|
+
locals = undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
var loader = self.options.loader;
|
|
211
|
+
var entry;
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
entry = loader.resolve(pathName);
|
|
215
|
+
} catch (e) {
|
|
216
|
+
cb(e);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
preWalker.walk(entry, loader, buildScanOpts()).then(function (memMap) {
|
|
221
|
+
var memWrapper = preWalker.makeMemoryWrapper(loader, memMap);
|
|
222
|
+
var origLoader = self.options.loader;
|
|
223
|
+
self.options.loader = memWrapper;
|
|
224
|
+
var output, error;
|
|
225
|
+
try {
|
|
226
|
+
output = self.renderFile(entry, locals);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
error = e;
|
|
229
|
+
}
|
|
230
|
+
self.options.loader = origLoader;
|
|
231
|
+
if (error) {
|
|
232
|
+
cb(error);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
cb(null, output);
|
|
236
|
+
}, function (err) {
|
|
237
|
+
cb(err);
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Compile a Jinja2 template file asynchronously, supporting async loaders.
|
|
243
|
+
*
|
|
244
|
+
* Same pre-walk / memory-wrapper / sync-pipeline shape as
|
|
245
|
+
* {@link Jinja2#renderFileAsync}. Returns the compiled function (via
|
|
246
|
+
* <var>cb</var>) that takes a locals object and yields a rendered
|
|
247
|
+
* string. The returned function captures the pre-walked memory map and
|
|
248
|
+
* temporarily swaps the loader on each call, so subsequent runtime
|
|
249
|
+
* <code>include</code>s resolve correctly without re-running the
|
|
250
|
+
* pre-walk.
|
|
251
|
+
*
|
|
252
|
+
* @deprecated since 2.5.0 — use {@link Jinja2#compileFile} with
|
|
253
|
+
* <code>options.codegenMode === 'async'</code> on a loader that sets
|
|
254
|
+
* <code>loader.async === true</code>. The returned compiled function
|
|
255
|
+
* yields a <code>Promise<{output, exports}></code> instead of a
|
|
256
|
+
* string. This method will be removed in 3.0.
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* jinja2.compileFileAsync('page.html', {}, function (err, fn) {
|
|
260
|
+
* if (err) { return done(err); }
|
|
261
|
+
* res.end(fn({ name: 'world' }));
|
|
262
|
+
* });
|
|
263
|
+
*
|
|
264
|
+
* @param {string} pathName Template path.
|
|
265
|
+
* @param {object} [options] Compilation options.
|
|
266
|
+
* @param {Function} cb Node-style callback <code>(err, fn)</code>.
|
|
267
|
+
* @return {undefined}
|
|
268
|
+
*/
|
|
269
|
+
this.compileFileAsync = function (pathName, options, cb) {
|
|
270
|
+
if (typeof options === 'function') {
|
|
271
|
+
cb = options;
|
|
272
|
+
options = {};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
var loader = self.options.loader;
|
|
276
|
+
var entry;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
entry = loader.resolve(pathName);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
cb(e);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
preWalker.walk(entry, loader, buildScanOpts()).then(function (memMap) {
|
|
286
|
+
var memWrapper = preWalker.makeMemoryWrapper(loader, memMap);
|
|
287
|
+
var origLoader = self.options.loader;
|
|
288
|
+
self.options.loader = memWrapper;
|
|
289
|
+
var compiled, error;
|
|
290
|
+
try {
|
|
291
|
+
compiled = self.compileFile(entry, options);
|
|
292
|
+
} catch (e) {
|
|
293
|
+
error = e;
|
|
294
|
+
}
|
|
295
|
+
self.options.loader = origLoader;
|
|
296
|
+
if (error) {
|
|
297
|
+
cb(error);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
var wrapped = function (locals) {
|
|
301
|
+
var origInner = self.options.loader;
|
|
302
|
+
self.options.loader = memWrapper;
|
|
303
|
+
try {
|
|
304
|
+
var output = compiled(locals);
|
|
305
|
+
self.options.loader = origInner;
|
|
306
|
+
return output;
|
|
307
|
+
} catch (e) {
|
|
308
|
+
self.options.loader = origInner;
|
|
309
|
+
throw e;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
cb(null, wrapped);
|
|
313
|
+
}, function (err) {
|
|
314
|
+
cb(err);
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/*!
|
|
320
|
+
* Export methods publicly via the default instance.
|
|
321
|
+
*/
|
|
322
|
+
defaultInstance = new exports.Jinja2();
|
|
323
|
+
exports.setFilter = defaultInstance.setFilter;
|
|
324
|
+
exports.setTag = defaultInstance.setTag;
|
|
325
|
+
exports.setExtension = defaultInstance.setExtension;
|
|
326
|
+
exports.parseFile = defaultInstance.parseFile;
|
|
327
|
+
exports.precompile = defaultInstance.precompile;
|
|
328
|
+
exports.compile = defaultInstance.compile;
|
|
329
|
+
exports.compileFile = defaultInstance.compileFile;
|
|
330
|
+
exports.compileFileAsync = defaultInstance.compileFileAsync;
|
|
331
|
+
exports.render = defaultInstance.render;
|
|
332
|
+
exports.renderFile = defaultInstance.renderFile;
|
|
333
|
+
exports.renderFileAsync = defaultInstance.renderFileAsync;
|
|
334
|
+
exports.run = defaultInstance.run;
|
|
335
|
+
exports.invalidateCache = defaultInstance.invalidateCache;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Express 3/4 compatibility alias.
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* app.engine('jinja2', require('@rhinostone/swig-jinja2').__express);
|
|
342
|
+
* app.set('view engine', 'jinja2');
|
|
343
|
+
*/
|
|
344
|
+
exports.__express = defaultInstance.renderFile;
|
package/lib/lexer.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
var utils = require('@rhinostone/swig-core/lib/utils');
|
|
2
|
+
var TYPES = require('./tokentypes');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A Jinja2 lexer token.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {object} LexerToken
|
|
8
|
+
* @property {string} match The string that was matched (post-replace).
|
|
9
|
+
* @property {number} type Jinja2 token type enum value.
|
|
10
|
+
* @property {number} length Length of the input chunk consumed.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/*!
|
|
14
|
+
* Jinja2 lexer rule table — the swig-shared token subset.
|
|
15
|
+
*
|
|
16
|
+
* The Jinja2-only operators (`~` concat, `**` power, `//` floor-division,
|
|
17
|
+
* `is` / `is not` tests) land in subsequent commits; until then they fall
|
|
18
|
+
* through to the unknown-token throw (fail-close). Jinja2 has no range
|
|
19
|
+
* (`..`), null-coalescing (`??`), ternary (`?:`), or `#{}` string-
|
|
20
|
+
* interpolation operators, so this table will never grow those rules.
|
|
21
|
+
*
|
|
22
|
+
* Rule ordering constraints worth the call-out:
|
|
23
|
+
*
|
|
24
|
+
* - FILTER / FILTEREMPTY / FUNCTION above VAR — `|name(` and `name(`
|
|
25
|
+
* must be consumed as filter / function tokens before VAR's
|
|
26
|
+
* `^[a-zA-Z_$]\w*` pattern gobbles the bare identifier.
|
|
27
|
+
* - LOGIC (`and` / `or`), NOT (`not`), BOOL (`true` / `false`), and the
|
|
28
|
+
* COMPARATOR `in\s` keyword above VAR — same reason: bake the keyword
|
|
29
|
+
* sequence into the operator token rather than emit an identifier.
|
|
30
|
+
*
|
|
31
|
+
* Rules are tried in order; first match wins. Patterns are anchored at
|
|
32
|
+
* start-of-string because the consumer slices `str` before each dispatch.
|
|
33
|
+
*/
|
|
34
|
+
var rules = [
|
|
35
|
+
{
|
|
36
|
+
type: TYPES.WHITESPACE,
|
|
37
|
+
regex: [
|
|
38
|
+
/^\s+/
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: TYPES.STRING,
|
|
43
|
+
regex: [
|
|
44
|
+
/^""/,
|
|
45
|
+
/^".*?[^\\]"/,
|
|
46
|
+
/^''/,
|
|
47
|
+
/^'.*?[^\\]'/
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: TYPES.FILTER,
|
|
52
|
+
regex: [
|
|
53
|
+
/^\|\s*(\w+)\(/
|
|
54
|
+
],
|
|
55
|
+
idx: 1
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: TYPES.FILTEREMPTY,
|
|
59
|
+
regex: [
|
|
60
|
+
/^\|\s*(\w+)/
|
|
61
|
+
],
|
|
62
|
+
idx: 1
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: TYPES.FUNCTIONEMPTY,
|
|
66
|
+
regex: [
|
|
67
|
+
/^\s*(\w+)\(\)/
|
|
68
|
+
],
|
|
69
|
+
idx: 1
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: TYPES.FUNCTION,
|
|
73
|
+
regex: [
|
|
74
|
+
/^\s*(\w+)\(/
|
|
75
|
+
],
|
|
76
|
+
idx: 1
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: TYPES.PARENOPEN,
|
|
80
|
+
regex: [
|
|
81
|
+
/^\(/
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: TYPES.PARENCLOSE,
|
|
86
|
+
regex: [
|
|
87
|
+
/^\)/
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: TYPES.COMMA,
|
|
92
|
+
regex: [
|
|
93
|
+
/^,/
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
type: TYPES.LOGIC,
|
|
98
|
+
regex: [
|
|
99
|
+
/^(&&|\|\|)\s*/,
|
|
100
|
+
/^(and|or)\s+/
|
|
101
|
+
],
|
|
102
|
+
idx: 1,
|
|
103
|
+
replace: {
|
|
104
|
+
'and': '&&',
|
|
105
|
+
'or': '||'
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: TYPES.COMPARATOR,
|
|
110
|
+
regex: [
|
|
111
|
+
/^(===|==|\!==|\!=|<=|<|>=|>|in\s)\s*/
|
|
112
|
+
],
|
|
113
|
+
idx: 1
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: TYPES.ASSIGNMENT,
|
|
117
|
+
regex: [
|
|
118
|
+
/^(=|\+=|-=|\*=|\/=)/
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: TYPES.NOT,
|
|
123
|
+
regex: [
|
|
124
|
+
/^\!\s*/,
|
|
125
|
+
/^not\s+/
|
|
126
|
+
],
|
|
127
|
+
replace: {
|
|
128
|
+
'not': '!'
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: TYPES.BOOL,
|
|
133
|
+
regex: [
|
|
134
|
+
/^(true|false)\s+/,
|
|
135
|
+
/^(true|false)$/
|
|
136
|
+
],
|
|
137
|
+
idx: 1
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
// ISNOT above IS above VAR — the `is` keyword would otherwise be
|
|
141
|
+
// gobbled by VAR's `^[a-zA-Z_$]\w*` pattern. ISNOT above IS because
|
|
142
|
+
// `is not` must be consumed as a single token, not IS + NOT. The `\b`
|
|
143
|
+
// word boundary keeps identifiers like `island` from matching.
|
|
144
|
+
type: TYPES.ISNOT,
|
|
145
|
+
regex: [
|
|
146
|
+
/^is\s+not\b/
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
type: TYPES.IS,
|
|
151
|
+
regex: [
|
|
152
|
+
/^is\b/
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
// The dotted-path interior segment uses `\w+` (not `\w*`) so a future
|
|
157
|
+
// operator whose first char is `.` cannot be absorbed as a zero-width
|
|
158
|
+
// interior segment. Jinja2 has no `..` range today, but the tighter
|
|
159
|
+
// form is the defensive default and matches the Twig sibling lexer.
|
|
160
|
+
type: TYPES.VAR,
|
|
161
|
+
regex: [
|
|
162
|
+
/^[a-zA-Z_$]\w*((\.\$?\w+)+)?/,
|
|
163
|
+
/^[a-zA-Z_$]\w*/
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: TYPES.BRACKETOPEN,
|
|
168
|
+
regex: [
|
|
169
|
+
/^\[/
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: TYPES.BRACKETCLOSE,
|
|
174
|
+
regex: [
|
|
175
|
+
/^\]/
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: TYPES.CURLYOPEN,
|
|
180
|
+
regex: [
|
|
181
|
+
/^\{/
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: TYPES.COLON,
|
|
186
|
+
regex: [
|
|
187
|
+
/^\:/
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: TYPES.CURLYCLOSE,
|
|
192
|
+
regex: [
|
|
193
|
+
/^\}/
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
type: TYPES.DOTKEY,
|
|
198
|
+
regex: [
|
|
199
|
+
/^\.(\w+)/
|
|
200
|
+
],
|
|
201
|
+
idx: 1
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
type: TYPES.NUMBER,
|
|
205
|
+
regex: [
|
|
206
|
+
/^\d+(\.\d+)?/
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
type: TYPES.TILDE,
|
|
211
|
+
regex: [
|
|
212
|
+
/^~/
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
// Above OPERATOR so `**` wins over a bare `*`; first-match-wins would
|
|
217
|
+
// otherwise lex `**` as two OPERATOR tokens.
|
|
218
|
+
type: TYPES.POWER,
|
|
219
|
+
regex: [
|
|
220
|
+
/^\*\*/
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
// Above OPERATOR so `//` wins over a bare `/`. Note JS has no
|
|
225
|
+
// floor-division operator (`a // b` is a comment), so the parser
|
|
226
|
+
// lowers FLOORDIV to Math.floor(a / b) rather than emitting `//`.
|
|
227
|
+
type: TYPES.FLOORDIV,
|
|
228
|
+
regex: [
|
|
229
|
+
/^\/\//
|
|
230
|
+
]
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
type: TYPES.OPERATOR,
|
|
234
|
+
regex: [
|
|
235
|
+
/^(\+|\-|\/|\*|%)/
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
exports.types = TYPES;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Match the next token at the start of `str`.
|
|
244
|
+
*
|
|
245
|
+
* Throws via utils.throwError when no rule matches — including every
|
|
246
|
+
* Jinja2-only operator until its rule lands. The throw is opaque (no
|
|
247
|
+
* line / file info); the Jinja2 frontend's onCompileError callback
|
|
248
|
+
* attaches filename + line per the swig-core / frontend seam rule.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} str Input slice starting at the unconsumed offset.
|
|
251
|
+
* @return {LexerToken} Matched token.
|
|
252
|
+
* @throws {Error} When no rule matches.
|
|
253
|
+
* @private
|
|
254
|
+
*/
|
|
255
|
+
function reader(str) {
|
|
256
|
+
var matched;
|
|
257
|
+
|
|
258
|
+
utils.some(rules, function (rule) {
|
|
259
|
+
return utils.some(rule.regex, function (regex) {
|
|
260
|
+
var match = str.match(regex),
|
|
261
|
+
normalized;
|
|
262
|
+
|
|
263
|
+
if (!match) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
normalized = match[rule.idx || 0].replace(/\s*$/, '');
|
|
268
|
+
normalized = (rule.hasOwnProperty('replace') && rule.replace.hasOwnProperty(normalized)) ? rule.replace[normalized] : normalized;
|
|
269
|
+
|
|
270
|
+
matched = {
|
|
271
|
+
match: normalized,
|
|
272
|
+
type: rule.type,
|
|
273
|
+
length: match[0].length
|
|
274
|
+
};
|
|
275
|
+
return true;
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (!matched) {
|
|
280
|
+
utils.throwError('Unexpected token "' + str.charAt(0) + '" in Jinja2 expression');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return matched;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Tokenize a Jinja2 expression string.
|
|
288
|
+
*
|
|
289
|
+
* @param {string} str Expression source (the contents of
|
|
290
|
+
* `{{ … }}` or `{% … %}` minus the
|
|
291
|
+
* control delimiters and tag name).
|
|
292
|
+
* @return {Array.<LexerToken>} Sequence of matched tokens.
|
|
293
|
+
* @throws {Error} On the first unrecognised character.
|
|
294
|
+
*/
|
|
295
|
+
exports.read = function (str) {
|
|
296
|
+
var offset = 0,
|
|
297
|
+
tokens = [],
|
|
298
|
+
match;
|
|
299
|
+
while (offset < str.length) {
|
|
300
|
+
match = reader(str.substring(offset));
|
|
301
|
+
offset += match.length;
|
|
302
|
+
tokens.push(match);
|
|
303
|
+
}
|
|
304
|
+
return tokens;
|
|
305
|
+
};
|