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