@rhinostone/swig 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/.changes/v2.1.0.md +8 -0
- package/.changes/v2.2.0.md +8 -0
- package/.eslintrc.json +8 -1
- package/HISTORY.md +18 -0
- package/README.md +13 -1
- package/ROADMAP.md +13 -0
- package/dist/swig.js +609 -67
- package/dist/swig.min.js +24 -6
- package/dist/swig.min.js.map +1 -1
- package/lib/async/pre-walker.js +279 -0
- package/lib/filters.js +23 -20
- package/lib/swig.js +157 -2
- package/lib/tags/import.js +42 -21
- package/lib/tags/include.js +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
var utils = require('../utils');
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Makes a string safe for a regular expression. Mirrors lib/parser.js.
|
|
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 that
|
|
13
|
+
* parser.parse() builds at parse-time so the pre-walker chunks the same
|
|
14
|
+
* way 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. `extends "x.html"`).
|
|
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 template source for static `{% extends|include|import|from "..." %}`
|
|
52
|
+
* targets. Pure function; performs no I/O.
|
|
53
|
+
*
|
|
54
|
+
* The scanner mirrors the real parser's chunk-splitter so it agrees on
|
|
55
|
+
* chunk boundaries even under non-default control characters. Dynamic
|
|
56
|
+
* paths (`{% extends parent_var %}`) and tag bodies whose first token
|
|
57
|
+
* isn't a string literal are silently skipped — they remain on the sync
|
|
58
|
+
* path, which throws appropriately at parse time.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* preWalker.scan('{% extends "layout.html" %}{% include "x" %}', {
|
|
62
|
+
* varControls: ['{{', '}}'],
|
|
63
|
+
* tagControls: ['{%', '%}'],
|
|
64
|
+
* cmtControls: ['{#', '#}'],
|
|
65
|
+
* rawTag: 'raw',
|
|
66
|
+
* keywords: ['extends', 'include', 'import']
|
|
67
|
+
* });
|
|
68
|
+
* // => [
|
|
69
|
+
* // { kind: 'extends', path: 'layout.html' },
|
|
70
|
+
* // { kind: 'include', path: 'x' }
|
|
71
|
+
* // ]
|
|
72
|
+
*
|
|
73
|
+
* @param {string} source
|
|
74
|
+
* @param {object} opts
|
|
75
|
+
* @param {array} opts.varControls e.g. <code>['{{', '}}']</code>.
|
|
76
|
+
* @param {array} opts.tagControls e.g. <code>['{%', '%}']</code>.
|
|
77
|
+
* @param {array} opts.cmtControls e.g. <code>['{#', '#}']</code>.
|
|
78
|
+
* @param {string} opts.rawTag Tag name that opens verbatim regions
|
|
79
|
+
* (<code>raw</code> for native swig).
|
|
80
|
+
* @param {array} opts.keywords Keywords whose first quoted argument is
|
|
81
|
+
* a template path. Native swig:
|
|
82
|
+
* <code>['extends', 'include', 'import']</code>.
|
|
83
|
+
* @return {array} List of <code>{ kind, path }</code> entries.
|
|
84
|
+
*/
|
|
85
|
+
exports.scan = function (source, opts) {
|
|
86
|
+
source = source.replace(/\r\n/g, '\n');
|
|
87
|
+
|
|
88
|
+
var splitter = buildSplitter(opts),
|
|
89
|
+
tagOpen = opts.tagControls[0],
|
|
90
|
+
tagClose = opts.tagControls[1],
|
|
91
|
+
rawTag = opts.rawTag,
|
|
92
|
+
endRawTag = 'end' + rawTag,
|
|
93
|
+
keywordRegex = new RegExp(
|
|
94
|
+
'^(' + opts.keywords.join('|') + ')\\s+["\\\']([^"\\\']+)["\\\']'
|
|
95
|
+
),
|
|
96
|
+
chunks = source.split(splitter),
|
|
97
|
+
results = [],
|
|
98
|
+
inRaw = false,
|
|
99
|
+
i,
|
|
100
|
+
chunk,
|
|
101
|
+
body,
|
|
102
|
+
name,
|
|
103
|
+
m;
|
|
104
|
+
|
|
105
|
+
for (i = 0; i < chunks.length; i += 1) {
|
|
106
|
+
chunk = chunks[i];
|
|
107
|
+
if (typeof chunk !== 'string' || !chunk) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!utils.startsWith(chunk, tagOpen) || !utils.endsWith(chunk, tagClose)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
body = stripTagBody(chunk, tagOpen, tagClose);
|
|
116
|
+
name = body.split(/\s+/)[0];
|
|
117
|
+
|
|
118
|
+
if (name === rawTag) {
|
|
119
|
+
inRaw = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (name === endRawTag) {
|
|
123
|
+
inRaw = false;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (inRaw) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
m = keywordRegex.exec(body);
|
|
131
|
+
if (m) {
|
|
132
|
+
results.push({ kind: m[1], path: m[2] });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return results;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Walk the dependency graph asynchronously starting from <var>entryPath</var>.
|
|
141
|
+
*
|
|
142
|
+
* Repeatedly loads, scans, and resolves child template paths in parallel
|
|
143
|
+
* via the user's async loader, until the dep graph closes. Returns a
|
|
144
|
+
* Promise resolving to a populated <code>{ resolvedPath: source }</code>
|
|
145
|
+
* map suitable for backing a memory loader.
|
|
146
|
+
*
|
|
147
|
+
* Cycles in the graph are tolerated — once a path is in the map or
|
|
148
|
+
* pending, subsequent enqueue requests are dropped. The synchronous
|
|
149
|
+
* renderer's existing circular-extends guard handles cycles at parse
|
|
150
|
+
* time on the second pass.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* preWalker.walk('/abs/entry.html', userLoader, scanOpts).then(function (memMap) {
|
|
154
|
+
* // memMap = { '/abs/entry.html': '...', '/abs/layout.html': '...', ... }
|
|
155
|
+
* });
|
|
156
|
+
*
|
|
157
|
+
* @param {string} entryPath Resolved path of the entry template.
|
|
158
|
+
* @param {object} loader User loader. Must expose:
|
|
159
|
+
* <code>resolve(to, from)</code> (sync, returns
|
|
160
|
+
* string) and
|
|
161
|
+
* <code>load(id, cb)</code> (async, calls
|
|
162
|
+
* <code>cb(err, source)</code>).
|
|
163
|
+
* @param {object} scanOpts Pass-through to {@link scan}.
|
|
164
|
+
* @return {Promise} Resolves to the populated memory map.
|
|
165
|
+
*/
|
|
166
|
+
exports.walk = function (entryPath, loader, scanOpts) {
|
|
167
|
+
var memMap = {};
|
|
168
|
+
var pending = {};
|
|
169
|
+
|
|
170
|
+
return new Promise(function (resolve, reject) {
|
|
171
|
+
var inFlight = 0;
|
|
172
|
+
var queue = [];
|
|
173
|
+
var hasError = false;
|
|
174
|
+
|
|
175
|
+
function enqueue(path) {
|
|
176
|
+
if (memMap.hasOwnProperty(path) || pending[path]) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
pending[path] = true;
|
|
180
|
+
queue.push(path);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function drain() {
|
|
184
|
+
while (queue.length > 0 && !hasError) {
|
|
185
|
+
var path = queue.shift();
|
|
186
|
+
inFlight += 1;
|
|
187
|
+
startLoad(path);
|
|
188
|
+
}
|
|
189
|
+
if (inFlight === 0 && !hasError && queue.length === 0) {
|
|
190
|
+
resolve(memMap);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function startLoad(resolvedPath) {
|
|
195
|
+
loader.load(resolvedPath, function (err, src) {
|
|
196
|
+
if (hasError) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (err) {
|
|
200
|
+
hasError = true;
|
|
201
|
+
reject(err);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (typeof src !== 'string') {
|
|
205
|
+
hasError = true;
|
|
206
|
+
reject(new Error('Async loader returned non-string source for "' + resolvedPath + '"'));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
memMap[resolvedPath] = src;
|
|
210
|
+
|
|
211
|
+
var targets;
|
|
212
|
+
try {
|
|
213
|
+
targets = exports.scan(src, scanOpts);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
hasError = true;
|
|
216
|
+
reject(e);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
var i, resolvedChild;
|
|
221
|
+
for (i = 0; i < targets.length; i += 1) {
|
|
222
|
+
try {
|
|
223
|
+
resolvedChild = loader.resolve(targets[i].path, resolvedPath);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
hasError = true;
|
|
226
|
+
reject(e);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
enqueue(resolvedChild);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
inFlight -= 1;
|
|
233
|
+
drain();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
enqueue(entryPath);
|
|
238
|
+
drain();
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Build a sync memory wrapper around a pre-populated
|
|
244
|
+
* <code>{ resolvedPath: source }</code> map. Delegates <code>resolve</code>
|
|
245
|
+
* to the user loader so cache keys match what the pre-walker produced.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* var mem = preWalker.makeMemoryWrapper(userLoader, memMap);
|
|
249
|
+
* swig.options.loader = mem;
|
|
250
|
+
* swig.renderFile('/abs/entry.html', locals); // sync, hits memMap
|
|
251
|
+
*
|
|
252
|
+
* @param {object} userLoader Original async loader (used for resolve).
|
|
253
|
+
* @param {object} memMap Pre-populated source map.
|
|
254
|
+
* @return {object} A loader exposing <code>resolve</code> and
|
|
255
|
+
* <code>load</code>.
|
|
256
|
+
*/
|
|
257
|
+
exports.makeMemoryWrapper = function (userLoader, memMap) {
|
|
258
|
+
return {
|
|
259
|
+
resolve: function (to, from) {
|
|
260
|
+
return userLoader.resolve(to, from);
|
|
261
|
+
},
|
|
262
|
+
load: function (id, cb) {
|
|
263
|
+
var src = memMap[id];
|
|
264
|
+
if (typeof src !== 'string') {
|
|
265
|
+
var err = new Error('Pre-walked map missing path: "' + id + '"');
|
|
266
|
+
if (cb) {
|
|
267
|
+
cb(err);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
if (cb) {
|
|
273
|
+
cb(null, src);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
return src;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
};
|
package/lib/filters.js
CHANGED
|
@@ -117,26 +117,33 @@ exports["default"] = function (input, def) {
|
|
|
117
117
|
* @param {string} [type='html'] If you pass the string js in as the type, output will be escaped so that it is safe for JavaScript execution.
|
|
118
118
|
* @return {string} Escaped string.
|
|
119
119
|
*/
|
|
120
|
+
function escapeHtmlRest(ch) {
|
|
121
|
+
return ch === '<' ? '<' : ch === '>' ? '>' : ch === '"' ? '"' : ''';
|
|
122
|
+
}
|
|
123
|
+
|
|
120
124
|
exports.escape = function (input, type) {
|
|
121
|
-
var
|
|
122
|
-
inp = input,
|
|
123
|
-
i = 0,
|
|
124
|
-
code;
|
|
125
|
+
var t, inp, out, i, code;
|
|
125
126
|
|
|
126
|
-
if (
|
|
127
|
-
return
|
|
127
|
+
if (input === null || input === undefined) {
|
|
128
|
+
return input;
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
t = typeof input;
|
|
132
|
+
|
|
133
|
+
if (t !== 'string') {
|
|
134
|
+
if (t === 'object') {
|
|
135
|
+
out = iterateFilter.apply(exports.escape, arguments);
|
|
136
|
+
if (out !== undefined) {
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
131
140
|
return input;
|
|
132
141
|
}
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
inp = inp.replace(/\\/g, '\\u005C');
|
|
139
|
-
for (i; i < inp.length; i += 1) {
|
|
143
|
+
if (type === 'js') {
|
|
144
|
+
inp = input.replace(/\\/g, '\\u005C');
|
|
145
|
+
out = '';
|
|
146
|
+
for (i = 0; i < inp.length; i += 1) {
|
|
140
147
|
code = inp.charCodeAt(i);
|
|
141
148
|
if (code < 32) {
|
|
142
149
|
code = code.toString(16).toUpperCase();
|
|
@@ -154,14 +161,10 @@ exports.escape = function (input, type) {
|
|
|
154
161
|
.replace(/\=/g, '\\u003D')
|
|
155
162
|
.replace(/-/g, '\\u002D')
|
|
156
163
|
.replace(/;/g, '\\u003B');
|
|
157
|
-
|
|
158
|
-
default:
|
|
159
|
-
return inp.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&')
|
|
160
|
-
.replace(/</g, '<')
|
|
161
|
-
.replace(/>/g, '>')
|
|
162
|
-
.replace(/"/g, '"')
|
|
163
|
-
.replace(/'/g, ''');
|
|
164
164
|
}
|
|
165
|
+
|
|
166
|
+
return input.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&')
|
|
167
|
+
.replace(/[<>"']/g, escapeHtmlRest);
|
|
165
168
|
};
|
|
166
169
|
exports.e = exports.escape;
|
|
167
170
|
|
package/lib/swig.js
CHANGED
|
@@ -4,16 +4,17 @@ var utils = require('./utils'),
|
|
|
4
4
|
parser = require('./parser'),
|
|
5
5
|
dateformatter = require('./dateformatter'),
|
|
6
6
|
loaders = require('./loaders'),
|
|
7
|
+
preWalker = require('./async/pre-walker'),
|
|
7
8
|
engine = require('@rhinostone/swig-core/lib/engine');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Swig version number as a string.
|
|
11
12
|
* @example
|
|
12
|
-
* if (swig.version === "2.0
|
|
13
|
+
* if (swig.version === "2.2.0") { ... }
|
|
13
14
|
*
|
|
14
15
|
* @type {String}
|
|
15
16
|
*/
|
|
16
|
-
exports.version = "2.0
|
|
17
|
+
exports.version = "2.2.0";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Swig Options Object. This object can be passed to many of the API-level Swig methods to control various aspects of the engine. All keys are optional.
|
|
@@ -177,6 +178,158 @@ exports.Swig = function (opts) {
|
|
|
177
178
|
utils.throwError(err, null, options.filename);
|
|
178
179
|
}
|
|
179
180
|
});
|
|
181
|
+
|
|
182
|
+
var self = this;
|
|
183
|
+
|
|
184
|
+
function buildScanOpts() {
|
|
185
|
+
return {
|
|
186
|
+
varControls: self.options.varControls,
|
|
187
|
+
tagControls: self.options.tagControls,
|
|
188
|
+
cmtControls: self.options.cmtControls,
|
|
189
|
+
rawTag: 'raw',
|
|
190
|
+
keywords: ['extends', 'include', 'import']
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Render a template file asynchronously, supporting async loaders.
|
|
196
|
+
*
|
|
197
|
+
* Pre-walks <var>extends</var> / <var>include</var> / <var>import</var>
|
|
198
|
+
* targets in parallel via the user loader, populates an in-memory map,
|
|
199
|
+
* then runs the existing sync render pipeline against the populated map.
|
|
200
|
+
* Dynamic paths (e.g. <code>{% extends parent_var %}</code>) are not
|
|
201
|
+
* pre-resolved and will throw at render time as they would on the sync
|
|
202
|
+
* path.
|
|
203
|
+
*
|
|
204
|
+
* @deprecated since 2.2.0 — use {@link Swig#renderFile} with a loader that
|
|
205
|
+
* sets <code>loader.async === true</code>. The async-codegen dispatch
|
|
206
|
+
* handles dynamic include paths the pre-walker cannot. This method will
|
|
207
|
+
* be removed in 3.0.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* swig.setDefaults({ loader: myAsyncLoader });
|
|
211
|
+
* swig.renderFileAsync('page.html', { name: 'world' }, function (err, output) {
|
|
212
|
+
* if (err) { return done(err); }
|
|
213
|
+
* res.end(output);
|
|
214
|
+
* });
|
|
215
|
+
*
|
|
216
|
+
* @param {string} pathName Template path; resolved via the active loader.
|
|
217
|
+
* @param {object} [locals] Locals to render with.
|
|
218
|
+
* @param {Function} cb Node-style callback <code>(err, output)</code>.
|
|
219
|
+
* @return {undefined}
|
|
220
|
+
*/
|
|
221
|
+
this.renderFileAsync = function (pathName, locals, cb) {
|
|
222
|
+
if (typeof locals === 'function') {
|
|
223
|
+
cb = locals;
|
|
224
|
+
locals = undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
var loader = self.options.loader;
|
|
228
|
+
var entry;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
entry = loader.resolve(pathName);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
cb(e);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
preWalker.walk(entry, loader, buildScanOpts()).then(function (memMap) {
|
|
238
|
+
var memWrapper = preWalker.makeMemoryWrapper(loader, memMap);
|
|
239
|
+
var origLoader = self.options.loader;
|
|
240
|
+
self.options.loader = memWrapper;
|
|
241
|
+
var output, error;
|
|
242
|
+
try {
|
|
243
|
+
output = self.renderFile(entry, locals);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
error = e;
|
|
246
|
+
}
|
|
247
|
+
self.options.loader = origLoader;
|
|
248
|
+
if (error) {
|
|
249
|
+
cb(error);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
cb(null, output);
|
|
253
|
+
}, function (err) {
|
|
254
|
+
cb(err);
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Compile a template file asynchronously, supporting async loaders.
|
|
260
|
+
*
|
|
261
|
+
* Same pre-walk / memory-wrapper / sync-pipeline shape as
|
|
262
|
+
* {@link Swig#renderFileAsync}. Returns the compiled function (via
|
|
263
|
+
* <var>cb</var>) that takes a locals object and yields a rendered
|
|
264
|
+
* string. The returned function captures the pre-walked memory map and
|
|
265
|
+
* temporarily swaps the loader on each call, so subsequent runtime
|
|
266
|
+
* <var>include</var>s resolve correctly without re-running the pre-walk.
|
|
267
|
+
*
|
|
268
|
+
* @deprecated since 2.2.0 — use {@link Swig#compileFile} with
|
|
269
|
+
* <code>options.codegenMode === 'async'</code> on a loader that sets
|
|
270
|
+
* <code>loader.async === true</code>. The returned compiled function
|
|
271
|
+
* yields a <code>Promise<{output, exports}></code> instead of a
|
|
272
|
+
* string. This method will be removed in 3.0.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* swig.compileFileAsync('page.html', {}, function (err, fn) {
|
|
276
|
+
* if (err) { return done(err); }
|
|
277
|
+
* res.end(fn({ name: 'world' }));
|
|
278
|
+
* });
|
|
279
|
+
*
|
|
280
|
+
* @param {string} pathName Template path.
|
|
281
|
+
* @param {object} [options] Compilation options.
|
|
282
|
+
* @param {Function} cb Node-style callback <code>(err, fn)</code>.
|
|
283
|
+
* @return {undefined}
|
|
284
|
+
*/
|
|
285
|
+
this.compileFileAsync = function (pathName, options, cb) {
|
|
286
|
+
if (typeof options === 'function') {
|
|
287
|
+
cb = options;
|
|
288
|
+
options = {};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
var loader = self.options.loader;
|
|
292
|
+
var entry;
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
entry = loader.resolve(pathName);
|
|
296
|
+
} catch (e) {
|
|
297
|
+
cb(e);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
preWalker.walk(entry, loader, buildScanOpts()).then(function (memMap) {
|
|
302
|
+
var memWrapper = preWalker.makeMemoryWrapper(loader, memMap);
|
|
303
|
+
var origLoader = self.options.loader;
|
|
304
|
+
self.options.loader = memWrapper;
|
|
305
|
+
var compiled, error;
|
|
306
|
+
try {
|
|
307
|
+
compiled = self.compileFile(entry, options);
|
|
308
|
+
} catch (e) {
|
|
309
|
+
error = e;
|
|
310
|
+
}
|
|
311
|
+
self.options.loader = origLoader;
|
|
312
|
+
if (error) {
|
|
313
|
+
cb(error);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
var wrapped = function (locals) {
|
|
317
|
+
var origInner = self.options.loader;
|
|
318
|
+
self.options.loader = memWrapper;
|
|
319
|
+
try {
|
|
320
|
+
var output = compiled(locals);
|
|
321
|
+
self.options.loader = origInner;
|
|
322
|
+
return output;
|
|
323
|
+
} catch (e) {
|
|
324
|
+
self.options.loader = origInner;
|
|
325
|
+
throw e;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
cb(null, wrapped);
|
|
329
|
+
}, function (err) {
|
|
330
|
+
cb(err);
|
|
331
|
+
});
|
|
332
|
+
};
|
|
180
333
|
};
|
|
181
334
|
|
|
182
335
|
/*!
|
|
@@ -190,8 +343,10 @@ exports.parseFile = defaultInstance.parseFile;
|
|
|
190
343
|
exports.precompile = defaultInstance.precompile;
|
|
191
344
|
exports.compile = defaultInstance.compile;
|
|
192
345
|
exports.compileFile = defaultInstance.compileFile;
|
|
346
|
+
exports.compileFileAsync = defaultInstance.compileFileAsync;
|
|
193
347
|
exports.render = defaultInstance.render;
|
|
194
348
|
exports.renderFile = defaultInstance.renderFile;
|
|
349
|
+
exports.renderFileAsync = defaultInstance.renderFileAsync;
|
|
195
350
|
exports.run = defaultInstance.run;
|
|
196
351
|
exports.invalidateCache = defaultInstance.invalidateCache;
|
|
197
352
|
exports.loaders = loaders;
|
package/lib/tags/import.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
var utils = require('../utils'),
|
|
2
|
+
ir = require('@rhinostone/swig-core/lib/ir'),
|
|
2
3
|
backend = require('@rhinostone/swig-core/lib/backend');
|
|
3
4
|
|
|
4
5
|
// CVE-2023-25345: prototype-chain properties that must not be used as import
|
|
@@ -26,7 +27,19 @@ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousPro
|
|
|
26
27
|
* @param {literal} as Literally, "as".
|
|
27
28
|
* @param {literal} varname Local-accessible object name to assign the macros to.
|
|
28
29
|
*/
|
|
29
|
-
exports.compile = function (compiler, args) {
|
|
30
|
+
exports.compile = function (compiler, args, content, parents, options) {
|
|
31
|
+
// Phase 2 (#T22): async-codegen branch. Parse stashed `[{path}, alias]`
|
|
32
|
+
// (no macro pre-render in async mode); emit IRImportDeferred so the
|
|
33
|
+
// backend's `_swig.getTemplate` + `.exports` bind happens at runtime.
|
|
34
|
+
if (options && options.codegenMode === 'async') {
|
|
35
|
+
var asyncAlias = args[args.length - 1];
|
|
36
|
+
var asyncPath = args[0].path;
|
|
37
|
+
return ir.importDeferred(
|
|
38
|
+
ir.literal('string', asyncPath),
|
|
39
|
+
asyncAlias,
|
|
40
|
+
options.filename || ''
|
|
41
|
+
);
|
|
42
|
+
}
|
|
30
43
|
var ctx = args.pop(),
|
|
31
44
|
allMacros = utils.map(args, function (arg) {
|
|
32
45
|
return arg.name;
|
|
@@ -56,37 +69,45 @@ exports.parse = function (str, line, parser, types, stack, opts, swig) {
|
|
|
56
69
|
var compiler = require('../parser').compile,
|
|
57
70
|
parseOpts = { resolveFrom: opts.filename },
|
|
58
71
|
compileOpts = utils.extend({}, opts, parseOpts),
|
|
59
|
-
|
|
72
|
+
isAsync = !!(opts && opts.codegenMode === 'async'),
|
|
73
|
+
importPath,
|
|
60
74
|
ctx;
|
|
61
75
|
|
|
62
76
|
parser.on(types.STRING, function (token) {
|
|
63
77
|
var self = this;
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Phase 2 (#T15): macro.compile now returns an IRMacro node
|
|
74
|
-
// rather than a JS source string. Render it through the shared
|
|
75
|
-
// backend so import.js still gets the JS source it performs
|
|
76
|
-
// regex-surgery on for namespace-prefixing. The +'\n' trailing
|
|
77
|
-
// newline matches the pre-Phase-2 compile output exactly.
|
|
78
|
-
out += backend.compile([token.compile(compiler, token.args, token.content, [], compileOpts)], [], compileOpts) + '\n';
|
|
79
|
-
self.out.push({compiled: out, name: macroName});
|
|
80
|
-
});
|
|
78
|
+
if (importPath !== undefined) {
|
|
79
|
+
throw new Error('Unexpected string ' + token.match + ' on line ' + line + '.');
|
|
80
|
+
}
|
|
81
|
+
importPath = token.match.replace(/^("|')|("|')$/g, '');
|
|
82
|
+
|
|
83
|
+
if (isAsync) {
|
|
84
|
+
// Async mode: skip the sync parseFile + macro pre-render. Stash
|
|
85
|
+
// just the path; compile() emits IRImportDeferred.
|
|
86
|
+
self.out.push({ path: importPath });
|
|
81
87
|
return;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
|
-
|
|
90
|
+
var tokens = swig.parseFile(importPath, parseOpts).tokens;
|
|
91
|
+
utils.each(tokens, function (token) {
|
|
92
|
+
var out = '',
|
|
93
|
+
macroName;
|
|
94
|
+
if (!token || token.name !== 'macro' || !token.compile) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
macroName = token.args[0];
|
|
98
|
+
// Phase 2 (#T15): macro.compile now returns an IRMacro node
|
|
99
|
+
// rather than a JS source string. Render it through the shared
|
|
100
|
+
// backend so import.js still gets the JS source it performs
|
|
101
|
+
// regex-surgery on for namespace-prefixing. The +'\n' trailing
|
|
102
|
+
// newline matches the pre-Phase-2 compile output exactly.
|
|
103
|
+
out += backend.compile([token.compile(compiler, token.args, token.content, [], compileOpts)], [], compileOpts) + '\n';
|
|
104
|
+
self.out.push({compiled: out, name: macroName});
|
|
105
|
+
});
|
|
85
106
|
});
|
|
86
107
|
|
|
87
108
|
parser.on(types.VAR, function (token) {
|
|
88
109
|
var self = this;
|
|
89
|
-
if (
|
|
110
|
+
if (importPath === undefined || ctx) {
|
|
90
111
|
throw new Error('Unexpected variable "' + token.match + '" on line ' + line + '.');
|
|
91
112
|
}
|
|
92
113
|
|
package/lib/tags/include.js
CHANGED
|
@@ -48,6 +48,10 @@ exports.compile = function (compiler, args, content, parents, options, blockName
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
if (options && options.codegenMode === 'async') {
|
|
52
|
+
return ir.includeDeferred(file, w || undefined, !!onlyCtx, !!ignoreMissing, parentFile);
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
return ir.include(file, w || undefined, !!onlyCtx, !!ignoreMissing, parentFile);
|
|
52
56
|
};
|
|
53
57
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhinostone/swig",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A simple, powerful, and extendable templating engine for node.js and browsers, similar to Django, Jinja2, and Twig.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"template",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"Rhinostone <contact@gina.io>"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@rhinostone/swig-core": "2.0
|
|
24
|
+
"@rhinostone/swig-core": "2.2.0",
|
|
25
25
|
"terser": "^5.46.1",
|
|
26
26
|
"yargs": "^17.7.2"
|
|
27
27
|
},
|