@rhinostone/swig-twig 2.0.1 → 2.2.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/lib/async/pre-walker.js +267 -0
- package/lib/filters.js +23 -20
- package/lib/index.js +155 -1
- package/lib/tags/from.js +31 -2
- package/lib/tags/import.js +22 -2
- package/lib/tags/include.js +10 -1
- package/package.json +2 -2
|
@@ -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-twig'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
|
+
* Twig 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 Twig template source for static <code>{% extends|include|import|from "..." %}</code>
|
|
52
|
+
* targets. Pure function; performs no I/O.
|
|
53
|
+
*
|
|
54
|
+
* The scanner mirrors the Twig 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: 'verbatim',
|
|
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>verbatim</code> for Twig).
|
|
77
|
+
* @param {array} opts.keywords Keywords whose first quoted argument
|
|
78
|
+
* is a template path. Twig:
|
|
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
|
+
};
|
package/lib/filters.js
CHANGED
|
@@ -278,26 +278,33 @@ exports.raw.safe = true;
|
|
|
278
278
|
* @param {string} [type='html'] Pass `'js'` for JavaScript-safe escaping.
|
|
279
279
|
* @return {string}
|
|
280
280
|
*/
|
|
281
|
+
function escapeHtmlRest(ch) {
|
|
282
|
+
return ch === '<' ? '<' : ch === '>' ? '>' : ch === '"' ? '"' : ''';
|
|
283
|
+
}
|
|
284
|
+
|
|
281
285
|
exports.escape = function (input, type) {
|
|
282
|
-
var
|
|
283
|
-
inp = input,
|
|
284
|
-
i = 0,
|
|
285
|
-
code;
|
|
286
|
+
var t, inp, out, i, code;
|
|
286
287
|
|
|
287
|
-
if (
|
|
288
|
-
return
|
|
288
|
+
if (input === null || input === undefined) {
|
|
289
|
+
return input;
|
|
289
290
|
}
|
|
290
291
|
|
|
291
|
-
|
|
292
|
+
t = typeof input;
|
|
293
|
+
|
|
294
|
+
if (t !== 'string') {
|
|
295
|
+
if (t === 'object') {
|
|
296
|
+
out = iterateFilter.apply(exports.escape, arguments);
|
|
297
|
+
if (out !== undefined) {
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
292
301
|
return input;
|
|
293
302
|
}
|
|
294
303
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
inp = inp.replace(/\\/g, '\\u005C');
|
|
300
|
-
for (i; i < inp.length; i += 1) {
|
|
304
|
+
if (type === 'js') {
|
|
305
|
+
inp = input.replace(/\\/g, '\\u005C');
|
|
306
|
+
out = '';
|
|
307
|
+
for (i = 0; i < inp.length; i += 1) {
|
|
301
308
|
code = inp.charCodeAt(i);
|
|
302
309
|
if (code < 32) {
|
|
303
310
|
code = code.toString(16).toUpperCase();
|
|
@@ -315,14 +322,10 @@ exports.escape = function (input, type) {
|
|
|
315
322
|
.replace(/\=/g, '\\u003D')
|
|
316
323
|
.replace(/-/g, '\\u002D')
|
|
317
324
|
.replace(/;/g, '\\u003B');
|
|
318
|
-
|
|
319
|
-
default:
|
|
320
|
-
return inp.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&')
|
|
321
|
-
.replace(/</g, '<')
|
|
322
|
-
.replace(/>/g, '>')
|
|
323
|
-
.replace(/"/g, '"')
|
|
324
|
-
.replace(/'/g, ''');
|
|
325
325
|
}
|
|
326
|
+
|
|
327
|
+
return input.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&')
|
|
328
|
+
.replace(/[<>"']/g, escapeHtmlRest);
|
|
326
329
|
};
|
|
327
330
|
exports.e = exports.escape;
|
|
328
331
|
|
package/lib/index.js
CHANGED
|
@@ -14,7 +14,8 @@ var utils = require('@rhinostone/swig-core/lib/utils'),
|
|
|
14
14
|
parser = require('./parser'),
|
|
15
15
|
_tags = require('./tags'),
|
|
16
16
|
_filters = require('./filters'),
|
|
17
|
-
_tests = require('./tests')
|
|
17
|
+
_tests = require('./tests'),
|
|
18
|
+
preWalker = require('./async/pre-walker');
|
|
18
19
|
|
|
19
20
|
exports.name = 'twig';
|
|
20
21
|
|
|
@@ -163,6 +164,157 @@ exports.Twig = function (opts) {
|
|
|
163
164
|
utils.each(_tests, function (fn, name) {
|
|
164
165
|
self.setExtension('_test_' + name, fn);
|
|
165
166
|
});
|
|
167
|
+
|
|
168
|
+
function buildScanOpts() {
|
|
169
|
+
return {
|
|
170
|
+
varControls: self.options.varControls,
|
|
171
|
+
tagControls: self.options.tagControls,
|
|
172
|
+
cmtControls: self.options.cmtControls,
|
|
173
|
+
rawTag: 'verbatim',
|
|
174
|
+
keywords: ['extends', 'include', 'import', 'from']
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Render a Twig template file asynchronously, supporting async loaders.
|
|
180
|
+
*
|
|
181
|
+
* Pre-walks <code>extends</code> / <code>include</code> /
|
|
182
|
+
* <code>import</code> / <code>from</code> targets in parallel via the
|
|
183
|
+
* user loader, populates an in-memory map, then runs the existing sync
|
|
184
|
+
* render pipeline against the populated map. Dynamic paths
|
|
185
|
+
* (<code>{% extends parent_var %}</code>) are not pre-resolved and will
|
|
186
|
+
* throw at render time as they would on the sync path.
|
|
187
|
+
*
|
|
188
|
+
* @deprecated since 2.2.0 — use {@link Twig#renderFile} with a loader that
|
|
189
|
+
* sets <code>loader.async === true</code>. The async-codegen dispatch
|
|
190
|
+
* handles dynamic include paths the pre-walker cannot. This method will
|
|
191
|
+
* be removed in 3.0.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* twig.setDefaults({ loader: myAsyncLoader });
|
|
195
|
+
* twig.renderFileAsync('page.twig', { name: 'world' }, function (err, output) {
|
|
196
|
+
* if (err) { return done(err); }
|
|
197
|
+
* res.end(output);
|
|
198
|
+
* });
|
|
199
|
+
*
|
|
200
|
+
* @param {string} pathName Template path; resolved via the active loader.
|
|
201
|
+
* @param {object} [locals] Locals to render with.
|
|
202
|
+
* @param {Function} cb Node-style callback <code>(err, output)</code>.
|
|
203
|
+
* @return {undefined}
|
|
204
|
+
*/
|
|
205
|
+
this.renderFileAsync = function (pathName, locals, cb) {
|
|
206
|
+
if (typeof locals === 'function') {
|
|
207
|
+
cb = locals;
|
|
208
|
+
locals = undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
var loader = self.options.loader;
|
|
212
|
+
var entry;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
entry = loader.resolve(pathName);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
cb(e);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
preWalker.walk(entry, loader, buildScanOpts()).then(function (memMap) {
|
|
222
|
+
var memWrapper = preWalker.makeMemoryWrapper(loader, memMap);
|
|
223
|
+
var origLoader = self.options.loader;
|
|
224
|
+
self.options.loader = memWrapper;
|
|
225
|
+
var output, error;
|
|
226
|
+
try {
|
|
227
|
+
output = self.renderFile(entry, locals);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
error = e;
|
|
230
|
+
}
|
|
231
|
+
self.options.loader = origLoader;
|
|
232
|
+
if (error) {
|
|
233
|
+
cb(error);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
cb(null, output);
|
|
237
|
+
}, function (err) {
|
|
238
|
+
cb(err);
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Compile a Twig template file asynchronously, supporting async loaders.
|
|
244
|
+
*
|
|
245
|
+
* Same pre-walk / memory-wrapper / sync-pipeline shape as
|
|
246
|
+
* {@link Twig#renderFileAsync}. Returns the compiled function (via
|
|
247
|
+
* <var>cb</var>) that takes a locals object and yields a rendered
|
|
248
|
+
* string. The returned function captures the pre-walked memory map and
|
|
249
|
+
* temporarily swaps the loader on each call, so subsequent runtime
|
|
250
|
+
* <code>include</code>s resolve correctly without re-running the
|
|
251
|
+
* pre-walk.
|
|
252
|
+
*
|
|
253
|
+
* @deprecated since 2.2.0 — use {@link Twig#compileFile} with
|
|
254
|
+
* <code>options.codegenMode === 'async'</code> on a loader that sets
|
|
255
|
+
* <code>loader.async === true</code>. The returned compiled function
|
|
256
|
+
* yields a <code>Promise<{output, exports}></code> instead of a
|
|
257
|
+
* string. This method will be removed in 3.0.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* twig.compileFileAsync('page.twig', {}, 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 <code>(err, fn)</code>.
|
|
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
|
+
};
|
|
166
318
|
};
|
|
167
319
|
|
|
168
320
|
/*!
|
|
@@ -176,8 +328,10 @@ exports.parseFile = defaultInstance.parseFile;
|
|
|
176
328
|
exports.precompile = defaultInstance.precompile;
|
|
177
329
|
exports.compile = defaultInstance.compile;
|
|
178
330
|
exports.compileFile = defaultInstance.compileFile;
|
|
331
|
+
exports.compileFileAsync = defaultInstance.compileFileAsync;
|
|
179
332
|
exports.render = defaultInstance.render;
|
|
180
333
|
exports.renderFile = defaultInstance.renderFile;
|
|
334
|
+
exports.renderFileAsync = defaultInstance.renderFileAsync;
|
|
181
335
|
exports.run = defaultInstance.run;
|
|
182
336
|
exports.invalidateCache = defaultInstance.invalidateCache;
|
|
183
337
|
|
package/lib/tags/from.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
var utils = require('@rhinostone/swig-core/lib/utils');
|
|
33
|
+
var ir = require('@rhinostone/swig-core/lib/ir');
|
|
33
34
|
var backend = require('@rhinostone/swig-core/lib/backend');
|
|
34
35
|
var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
|
|
35
36
|
|
|
@@ -135,11 +136,20 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
135
136
|
consume();
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
var path = pathTok.match.replace(/^['"]|['"]$/g, '');
|
|
140
|
+
|
|
141
|
+
if (opts && opts.codegenMode === 'async') {
|
|
142
|
+
// Phase 2 (#T22): async mode skips parse-time parseFile + macro
|
|
143
|
+
// pre-render. compile() emits IRFromImportDeferred; runtime resolves
|
|
144
|
+
// the template via _swig.getTemplate and binds each entry on _ctx.
|
|
145
|
+
token.args = [{ path: path, entries: entries }];
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
138
149
|
if (!swig || typeof swig.parseFile !== 'function') {
|
|
139
150
|
utils.throwError('"from" tag requires an engine context with a loader', line, opts.filename);
|
|
140
151
|
}
|
|
141
152
|
|
|
142
|
-
var path = pathTok.match.replace(/^['"]|['"]$/g, '');
|
|
143
153
|
var parseOpts = { resolveFrom: opts.filename };
|
|
144
154
|
var compileOpts = utils.extend({}, opts, parseOpts);
|
|
145
155
|
var parsed = swig.parseFile(path, parseOpts);
|
|
@@ -195,7 +205,26 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
195
205
|
* `_ctx.<aliasName>`. Backend lifts it into
|
|
196
206
|
* `IRLegacyJS`.
|
|
197
207
|
*/
|
|
198
|
-
exports.compile = function (compiler, args) {
|
|
208
|
+
exports.compile = function (compiler, args, content, parents, options) {
|
|
209
|
+
// Phase 2 (#T22): async-codegen branch. Parse stashed a single bundle
|
|
210
|
+
// `[{path, entries: [{origName, aliasName}, ...]}]` in async mode (no
|
|
211
|
+
// macro pre-render); emit IRFromImportDeferred so the backend's
|
|
212
|
+
// `_swig.getTemplate` + per-entry `_ctx.<bind>` assignment happens at
|
|
213
|
+
// runtime.
|
|
214
|
+
if (options && options.codegenMode === 'async') {
|
|
215
|
+
var bundle = args[0];
|
|
216
|
+
var imports = utils.map(bundle.entries, function (e) {
|
|
217
|
+
return {
|
|
218
|
+
name: e.origName,
|
|
219
|
+
alias: e.aliasName === e.origName ? null : e.aliasName
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
return ir.fromImportDeferred(
|
|
223
|
+
ir.literal('string', bundle.path),
|
|
224
|
+
imports,
|
|
225
|
+
options.filename || ''
|
|
226
|
+
);
|
|
227
|
+
}
|
|
199
228
|
var allOrigNames = utils.map(args, function (arg) { return arg.origName; }).join('|');
|
|
200
229
|
var replacements = utils.map(args, function (arg) {
|
|
201
230
|
return {
|
package/lib/tags/import.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
var utils = require('@rhinostone/swig-core/lib/utils');
|
|
34
|
+
var ir = require('@rhinostone/swig-core/lib/ir');
|
|
34
35
|
var backend = require('@rhinostone/swig-core/lib/backend');
|
|
35
36
|
var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
|
|
36
37
|
|
|
@@ -109,11 +110,20 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
109
110
|
utils.throwError('Unexpected token "' + peek().match + '" after alias in "import" tag', line, opts.filename);
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
var path = pathTok.match.replace(/^['"]|['"]$/g, '');
|
|
114
|
+
|
|
115
|
+
if (opts && opts.codegenMode === 'async') {
|
|
116
|
+
// Phase 2 (#T22): async mode skips the parse-time parseFile + macro
|
|
117
|
+
// pre-render. compile() emits IRImportDeferred; runtime resolves the
|
|
118
|
+
// template via _swig.getTemplate and binds .exports under the alias.
|
|
119
|
+
token.args = [path, aliasTok.match];
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
112
123
|
if (!swig || typeof swig.parseFile !== 'function') {
|
|
113
124
|
utils.throwError('"import" tag requires an engine context with a loader', line, opts.filename);
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
var path = pathTok.match.replace(/^['"]|['"]$/g, '');
|
|
117
127
|
var parseOpts = { resolveFrom: opts.filename };
|
|
118
128
|
var compileOpts = utils.extend({}, opts, parseOpts);
|
|
119
129
|
var parsed = swig.parseFile(path, parseOpts);
|
|
@@ -145,7 +155,17 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
|
|
|
145
155
|
* @return {string} JS source that initialises `_ctx.<alias>` and
|
|
146
156
|
* assigns every imported macro into it.
|
|
147
157
|
*/
|
|
148
|
-
exports.compile = function (compiler, args) {
|
|
158
|
+
exports.compile = function (compiler, args, content, parents, options) {
|
|
159
|
+
// Phase 2 (#T22): async-codegen branch. Parse stashed `[path, alias]`
|
|
160
|
+
// in async mode (no macro pre-render); emit IRImportDeferred so the
|
|
161
|
+
// backend's `_swig.getTemplate` + `.exports` bind happens at runtime.
|
|
162
|
+
if (options && options.codegenMode === 'async') {
|
|
163
|
+
return ir.importDeferred(
|
|
164
|
+
ir.literal('string', args[0]),
|
|
165
|
+
args[args.length - 1],
|
|
166
|
+
options.filename || ''
|
|
167
|
+
);
|
|
168
|
+
}
|
|
149
169
|
var ctx = args.pop();
|
|
150
170
|
var allMacros = utils.map(args, function (arg) { return arg.name; }).join('|');
|
|
151
171
|
var out = '_ctx.' + ctx + ' = {};\n var _output = "";\n';
|
package/lib/tags/include.js
CHANGED
|
@@ -163,8 +163,17 @@ function sliceTrim(tokens, start, end, types) {
|
|
|
163
163
|
* context emission, isolated-vs-merged selector, resolveFrom, optional
|
|
164
164
|
* try/catch for ignoreMissing).
|
|
165
165
|
*
|
|
166
|
-
*
|
|
166
|
+
* In async codegen mode (`options.codegenMode === 'async'`), derive an
|
|
167
|
+
* `IRIncludeDeferred` from the same fields so the backend routes through
|
|
168
|
+
* the `_swig.getTemplate` + `await` deferred-resolution path instead of
|
|
169
|
+
* the sync `_swig.compileFile` call.
|
|
170
|
+
*
|
|
171
|
+
* @return {object} IRInclude or IRIncludeDeferred node.
|
|
167
172
|
*/
|
|
168
173
|
exports.compile = function (compiler, args, content, parents, options, blockName, token) {
|
|
174
|
+
if (options && options.codegenMode === 'async') {
|
|
175
|
+
var i = token.irExpr;
|
|
176
|
+
return ir.includeDeferred(i.path, i.context, i.isolated, i.ignoreMissing, i.resolveFrom);
|
|
177
|
+
}
|
|
169
178
|
return token.irExpr;
|
|
170
179
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhinostone/swig-twig",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Twig-syntax frontend for the @rhinostone/swig-core template engine. Part of the @rhinostone/swig multi-flavor family.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"template",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=12"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@rhinostone/swig-core": "2.0
|
|
25
|
+
"@rhinostone/swig-core": "2.2.0"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|