@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/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
@rhinostone/swig-jinja2
|
|
2
|
+
=======================
|
|
3
|
+
|
|
4
|
+
[](https://www.npmjs.com/package/@rhinostone/swig-jinja2) [](https://socket.dev/npm/package/@rhinostone/swig-jinja2)
|
|
5
|
+
|
|
6
|
+
Jinja2-syntax frontend for the [@rhinostone/swig-core](https://www.npmjs.com/package/@rhinostone/swig-core) template engine. Part of the multi-flavor architecture introduced in `2.0.0` — see [ROADMAP.md](https://github.com/gina-io/swig/blob/develop/ROADMAP.md) for the release narrative.
|
|
7
|
+
|
|
8
|
+
Installation
|
|
9
|
+
------------
|
|
10
|
+
|
|
11
|
+
npm install @rhinostone/swig-jinja2
|
|
12
|
+
|
|
13
|
+
This pulls in `@rhinostone/swig-core` as a peer dependency, pinned to the matching version. Frontends and the core release in lockstep — do not mix versions.
|
|
14
|
+
|
|
15
|
+
Basic example
|
|
16
|
+
-------------
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
var swig = require('@rhinostone/swig-jinja2');
|
|
20
|
+
|
|
21
|
+
var out = swig.render('Hello, {{ name|upper }}!', {
|
|
22
|
+
locals: { name: 'world' }
|
|
23
|
+
});
|
|
24
|
+
// => Hello, WORLD!
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Supported surface (as of 2.5.0)
|
|
28
|
+
-------------------------------
|
|
29
|
+
|
|
30
|
+
A near-subset of Python Jinja2 — everything below was cross-checked against Jinja2 3.x.
|
|
31
|
+
|
|
32
|
+
* **Tags** — `set`, `if` / `elif` / `else`, `for` (with `else`), `block`, `extends`, `include`, `macro`, `import`, `from`, `raw`, `filter`, `with`, `autoescape`.
|
|
33
|
+
* **Operators** — `**` power, `//` floor-division, `~` string concat, inline-if (`a if c else b`), Python slicing (`seq[start:stop:step]`), `is <test>` / `is not <test>`, plus `{{- … -}}` / `{%- … -%}` whitespace control.
|
|
34
|
+
* **Built-in `is` tests** — `defined`, `undefined`, `none`, `even`, `odd`, `divisibleby`, `iterable`, `mapping`, `sequence`, `string`, `number`, `boolean`, `callable`, `lower`, `upper`, `sameas`.
|
|
35
|
+
* **Filters** — 39 built-ins (`upper`, `lower`, `capitalize`, `title`, `trim`, `truncate`, `replace`, `striptags`, `format`, `wordcount`, `wordwrap`, `indent`, `center`, `urlencode`, `escape` / `e`, `safe`, `first`, `last`, `join`, `reverse`, `sort`, `length` / `count`, `list`, `unique`, `batch`, `slice`, `dictsort`, `groupby`, `min`, `max`, `sum`, `random`, `abs`, `round`, `int`, `float`, `default` / `d`, `tojson`, `date`). See `lib/filters.js` for the full list.
|
|
36
|
+
* **Async** — `renderFileAsync` / `compileFileAsync` for async loaders.
|
|
37
|
+
|
|
38
|
+
Explicitly unsupported (parse-time throw or absent)
|
|
39
|
+
---------------------------------------------------
|
|
40
|
+
|
|
41
|
+
* No sandboxed-rendering mode — template source is trusted.
|
|
42
|
+
* `{% call %}`, `{% do %}`, `{% trans %}` — deferred / Jinja2 extensions.
|
|
43
|
+
* `map` / `select` / `reject` / `selectattr` / `rejectattr` filters — deferred.
|
|
44
|
+
* Macro kwargs — use positional args or an object literal.
|
|
45
|
+
* Dynamic `{% extends %}` / `{% import %}` / `{% from %}` — string-literal paths only.
|
|
46
|
+
* Bracket-notation `{% set foo["x"] = … %}` — use dot-path notation.
|
|
47
|
+
|
|
48
|
+
Repository
|
|
49
|
+
----------
|
|
50
|
+
|
|
51
|
+
Source: [gina-io/swig/packages/swig-jinja2](https://github.com/gina-io/swig/tree/develop/packages/swig-jinja2). File issues and PRs at [gina-io/swig](https://github.com/gina-io/swig).
|
|
52
|
+
|
|
53
|
+
License
|
|
54
|
+
-------
|
|
55
|
+
|
|
56
|
+
MIT. See [LICENSE](https://github.com/gina-io/swig/blob/develop/LICENSE) in the monorepo root.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
var utils = require('@rhinostone/swig-core/lib/utils');
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Makes a string safe for a regular expression. Mirrors swig-jinja2's parser.
|
|
5
|
+
* @private
|
|
6
|
+
*/
|
|
7
|
+
function escapeRegExp(str) {
|
|
8
|
+
return str.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/*!
|
|
12
|
+
* Build the splitter regex from the controls trio. Mirrors the regex the
|
|
13
|
+
* Jinja2 parser builds at parse-time so the pre-walker chunks the same way
|
|
14
|
+
* the real parser would.
|
|
15
|
+
* @private
|
|
16
|
+
*/
|
|
17
|
+
function buildSplitter(controls) {
|
|
18
|
+
var anyChar = '[\\s\\S]*?',
|
|
19
|
+
varOpen = escapeRegExp(controls.varControls[0]),
|
|
20
|
+
varClose = escapeRegExp(controls.varControls[1]),
|
|
21
|
+
tagOpen = escapeRegExp(controls.tagControls[0]),
|
|
22
|
+
tagClose = escapeRegExp(controls.tagControls[1]),
|
|
23
|
+
cmtOpen = escapeRegExp(controls.cmtControls[0]),
|
|
24
|
+
cmtClose = escapeRegExp(controls.cmtControls[1]);
|
|
25
|
+
return new RegExp(
|
|
26
|
+
'(' +
|
|
27
|
+
tagOpen + anyChar + tagClose + '|' +
|
|
28
|
+
varOpen + anyChar + varClose + '|' +
|
|
29
|
+
cmtOpen + anyChar + cmtClose +
|
|
30
|
+
')'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/*!
|
|
35
|
+
* Strip tag controls and optional whitespace-control markers from a tag
|
|
36
|
+
* chunk, returning the trimmed tag body (e.g. <code>extends "x.html"</code>).
|
|
37
|
+
* @private
|
|
38
|
+
*/
|
|
39
|
+
function stripTagBody(chunk, tagOpen, tagClose) {
|
|
40
|
+
var body = chunk.substr(tagOpen.length, chunk.length - tagOpen.length - tagClose.length);
|
|
41
|
+
if (body.charAt(0) === '-') {
|
|
42
|
+
body = body.substr(1);
|
|
43
|
+
}
|
|
44
|
+
if (body.charAt(body.length - 1) === '-') {
|
|
45
|
+
body = body.substr(0, body.length - 1);
|
|
46
|
+
}
|
|
47
|
+
return body.replace(/^\s+|\s+$/g, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Scan Jinja2 template source for static <code>{% extends|include|import|from "..." %}</code>
|
|
52
|
+
* targets. Pure function; performs no I/O.
|
|
53
|
+
*
|
|
54
|
+
* The scanner mirrors the Jinja2 parser's chunk-splitter so it agrees on
|
|
55
|
+
* chunk boundaries even under non-default control characters. Dynamic
|
|
56
|
+
* paths (<code>{% extends parent_var %}</code>) and tag bodies whose
|
|
57
|
+
* first token isn't a string literal are silently skipped — they remain
|
|
58
|
+
* on the sync path, which throws appropriately at parse time.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* preWalker.scan('{% from "macros.html" import foo %}', {
|
|
62
|
+
* varControls: ['{{', '}}'],
|
|
63
|
+
* tagControls: ['{%', '%}'],
|
|
64
|
+
* cmtControls: ['{#', '#}'],
|
|
65
|
+
* rawTag: 'raw',
|
|
66
|
+
* keywords: ['extends', 'include', 'import', 'from']
|
|
67
|
+
* });
|
|
68
|
+
* // => [{ kind: 'from', path: 'macros.html' }]
|
|
69
|
+
*
|
|
70
|
+
* @param {string} source
|
|
71
|
+
* @param {object} opts
|
|
72
|
+
* @param {array} opts.varControls
|
|
73
|
+
* @param {array} opts.tagControls
|
|
74
|
+
* @param {array} opts.cmtControls
|
|
75
|
+
* @param {string} opts.rawTag Tag name that opens verbatim regions
|
|
76
|
+
* (<code>raw</code> for Jinja2).
|
|
77
|
+
* @param {array} opts.keywords Keywords whose first quoted argument
|
|
78
|
+
* is a template path. Jinja2:
|
|
79
|
+
* <code>['extends', 'include', 'import',
|
|
80
|
+
* 'from']</code>.
|
|
81
|
+
* @return {array} List of <code>{ kind, path }</code> entries.
|
|
82
|
+
*/
|
|
83
|
+
exports.scan = function (source, opts) {
|
|
84
|
+
source = source.replace(/\r\n/g, '\n');
|
|
85
|
+
|
|
86
|
+
var splitter = buildSplitter(opts),
|
|
87
|
+
tagOpen = opts.tagControls[0],
|
|
88
|
+
tagClose = opts.tagControls[1],
|
|
89
|
+
rawTag = opts.rawTag,
|
|
90
|
+
endRawTag = 'end' + rawTag,
|
|
91
|
+
keywordRegex = new RegExp(
|
|
92
|
+
'^(' + opts.keywords.join('|') + ')\\s+["\\\']([^"\\\']+)["\\\']'
|
|
93
|
+
),
|
|
94
|
+
chunks = source.split(splitter),
|
|
95
|
+
results = [],
|
|
96
|
+
inRaw = false,
|
|
97
|
+
i,
|
|
98
|
+
chunk,
|
|
99
|
+
body,
|
|
100
|
+
name,
|
|
101
|
+
m;
|
|
102
|
+
|
|
103
|
+
for (i = 0; i < chunks.length; i += 1) {
|
|
104
|
+
chunk = chunks[i];
|
|
105
|
+
if (typeof chunk !== 'string' || !chunk) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!utils.startsWith(chunk, tagOpen) || !utils.endsWith(chunk, tagClose)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
body = stripTagBody(chunk, tagOpen, tagClose);
|
|
114
|
+
name = body.split(/\s+/)[0];
|
|
115
|
+
|
|
116
|
+
if (name === rawTag) {
|
|
117
|
+
inRaw = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (name === endRawTag) {
|
|
121
|
+
inRaw = false;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (inRaw) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
m = keywordRegex.exec(body);
|
|
129
|
+
if (m) {
|
|
130
|
+
results.push({ kind: m[1], path: m[2] });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return results;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Walk the dependency graph asynchronously starting from <var>entryPath</var>.
|
|
139
|
+
*
|
|
140
|
+
* Repeatedly loads, scans, and resolves child template paths in parallel
|
|
141
|
+
* via the user's async loader, until the dep graph closes. Returns a
|
|
142
|
+
* Promise resolving to a populated <code>{ resolvedPath: source }</code>
|
|
143
|
+
* map suitable for backing a memory loader.
|
|
144
|
+
*
|
|
145
|
+
* Cycles in the graph are tolerated — once a path is in the map or
|
|
146
|
+
* pending, subsequent enqueue requests are dropped. The synchronous
|
|
147
|
+
* renderer's existing circular-extends guard handles cycles at parse
|
|
148
|
+
* time on the second pass.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} entryPath Resolved path of the entry template.
|
|
151
|
+
* @param {object} loader User loader. Must expose:
|
|
152
|
+
* <code>resolve(to, from)</code> (sync, returns
|
|
153
|
+
* string) and
|
|
154
|
+
* <code>load(id, cb)</code> (async, calls
|
|
155
|
+
* <code>cb(err, source)</code>).
|
|
156
|
+
* @param {object} scanOpts Pass-through to {@link scan}.
|
|
157
|
+
* @return {Promise} Resolves to the populated memory map.
|
|
158
|
+
*/
|
|
159
|
+
exports.walk = function (entryPath, loader, scanOpts) {
|
|
160
|
+
var memMap = {};
|
|
161
|
+
var pending = {};
|
|
162
|
+
|
|
163
|
+
return new Promise(function (resolve, reject) {
|
|
164
|
+
var inFlight = 0;
|
|
165
|
+
var queue = [];
|
|
166
|
+
var hasError = false;
|
|
167
|
+
|
|
168
|
+
function enqueue(path) {
|
|
169
|
+
if (memMap.hasOwnProperty(path) || pending[path]) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
pending[path] = true;
|
|
173
|
+
queue.push(path);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function drain() {
|
|
177
|
+
while (queue.length > 0 && !hasError) {
|
|
178
|
+
var path = queue.shift();
|
|
179
|
+
inFlight += 1;
|
|
180
|
+
startLoad(path);
|
|
181
|
+
}
|
|
182
|
+
if (inFlight === 0 && !hasError && queue.length === 0) {
|
|
183
|
+
resolve(memMap);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function startLoad(resolvedPath) {
|
|
188
|
+
loader.load(resolvedPath, function (err, src) {
|
|
189
|
+
if (hasError) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (err) {
|
|
193
|
+
hasError = true;
|
|
194
|
+
reject(err);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (typeof src !== 'string') {
|
|
198
|
+
hasError = true;
|
|
199
|
+
reject(new Error('Async loader returned non-string source for "' + resolvedPath + '"'));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
memMap[resolvedPath] = src;
|
|
203
|
+
|
|
204
|
+
var targets;
|
|
205
|
+
try {
|
|
206
|
+
targets = exports.scan(src, scanOpts);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
hasError = true;
|
|
209
|
+
reject(e);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
var i, resolvedChild;
|
|
214
|
+
for (i = 0; i < targets.length; i += 1) {
|
|
215
|
+
try {
|
|
216
|
+
resolvedChild = loader.resolve(targets[i].path, resolvedPath);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
hasError = true;
|
|
219
|
+
reject(e);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
enqueue(resolvedChild);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
inFlight -= 1;
|
|
226
|
+
drain();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
enqueue(entryPath);
|
|
231
|
+
drain();
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Build a sync memory wrapper around a pre-populated
|
|
237
|
+
* <code>{ resolvedPath: source }</code> map. Delegates <code>resolve</code>
|
|
238
|
+
* to the user loader so cache keys match what the pre-walker produced.
|
|
239
|
+
*
|
|
240
|
+
* @param {object} userLoader Original async loader (used for resolve).
|
|
241
|
+
* @param {object} memMap Pre-populated source map.
|
|
242
|
+
* @return {object} A loader exposing <code>resolve</code> and
|
|
243
|
+
* <code>load</code>.
|
|
244
|
+
*/
|
|
245
|
+
exports.makeMemoryWrapper = function (userLoader, memMap) {
|
|
246
|
+
return {
|
|
247
|
+
resolve: function (to, from) {
|
|
248
|
+
return userLoader.resolve(to, from);
|
|
249
|
+
},
|
|
250
|
+
load: function (id, cb) {
|
|
251
|
+
var src = memMap[id];
|
|
252
|
+
if (typeof src !== 'string') {
|
|
253
|
+
var err = new Error('Pre-walked map missing path: "' + id + '"');
|
|
254
|
+
if (cb) {
|
|
255
|
+
cb(err);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
if (cb) {
|
|
261
|
+
cb(null, src);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
return src;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
};
|