@rhinostone/swig-core 2.0.0-alpha.3

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/cache.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Template cache primitives — shared across @rhinostone/swig-family engines.
3
+ *
4
+ * Each helper is pure: state (the memory cache object, the engine's options)
5
+ * is passed in explicitly so the same helpers can serve multiple frontends
6
+ * without closure state. The native Swig constructor wires its inline
7
+ * `self.cache` + `self.options.cache` through these at each call site.
8
+ *
9
+ * See .claude/architecture/multi-flavor-ir.md — cache keys are opaque
10
+ * strings; the cache layer has no filename awareness.
11
+ */
12
+
13
+ /**
14
+ * Determine whether caching is disabled via the per-call options or the
15
+ * engine's default options.
16
+ *
17
+ * Match semantics of the previous inline closure in lib/swig.js:
18
+ * return (options.hasOwnProperty('cache') && !options.cache) ||
19
+ * !engineCache;
20
+ *
21
+ * @param {object} [options] Per-call Swig options. May have a `cache` key.
22
+ * @param {*} engineCache The engine's default `options.cache` value.
23
+ * @return {boolean} True if caching should be skipped.
24
+ */
25
+ exports.shouldCache = function (options, engineCache) {
26
+ options = options || {};
27
+ return (options.hasOwnProperty('cache') && !options.cache) || !engineCache;
28
+ };
29
+
30
+ /**
31
+ * Read a compiled template from the cache.
32
+ *
33
+ * @param {string} key Resolved template identifier.
34
+ * @param {object} [options] Per-call Swig options.
35
+ * @param {*} engineCache The engine's default `options.cache` value
36
+ * (`'memory'`, `false`, or a `{ get, set }` object).
37
+ * @param {object} memoryStore The engine's in-memory cache map
38
+ * (used only when engineCache === 'memory').
39
+ * @return {object|undefined} Cached compiled template, or undefined on miss.
40
+ */
41
+ exports.cacheGet = function (key, options, engineCache, memoryStore) {
42
+ if (exports.shouldCache(options, engineCache)) {
43
+ return;
44
+ }
45
+
46
+ if (engineCache === 'memory') {
47
+ return memoryStore[key];
48
+ }
49
+
50
+ return engineCache.get(key);
51
+ };
52
+
53
+ /**
54
+ * Store a compiled template in the cache.
55
+ *
56
+ * @param {string} key Resolved template identifier.
57
+ * @param {object} [options] Per-call Swig options.
58
+ * @param {*} val Compiled template to cache.
59
+ * @param {*} engineCache The engine's default `options.cache` value.
60
+ * @param {object} memoryStore The engine's in-memory cache map
61
+ * (used only when engineCache === 'memory').
62
+ * @return {undefined}
63
+ */
64
+ exports.cacheSet = function (key, options, val, engineCache, memoryStore) {
65
+ if (exports.shouldCache(options, engineCache)) {
66
+ return;
67
+ }
68
+
69
+ if (engineCache === 'memory') {
70
+ memoryStore[key] = val;
71
+ return;
72
+ }
73
+
74
+ engineCache.set(key, val);
75
+ };
@@ -0,0 +1,201 @@
1
+ var utils = require('./utils');
2
+
3
+ var _months = {
4
+ full: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
5
+ abbr: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
6
+ },
7
+ _days = {
8
+ full: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
9
+ abbr: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
10
+ alt: {'-1': 'Yesterday', 0: 'Today', 1: 'Tomorrow'}
11
+ };
12
+
13
+ /*
14
+ DateZ is licensed under the MIT License:
15
+ Copyright (c) 2011 Tomo Universalis (http://tomouniversalis.com)
16
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
17
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+ */
20
+ exports.tzOffset = 0;
21
+ // Offset sign convention: minutes WEST of GMT, matching JS
22
+ // Date#getTimezoneOffset() and PHP date(). Counterintuitive at
23
+ // first glance: -240 renders as GMT+4, +240 renders as GMT-4.
24
+ exports.DateZ = function () {
25
+ var members = {
26
+ 'default': ['getUTCDate', 'getUTCDay', 'getUTCFullYear', 'getUTCHours', 'getUTCMilliseconds', 'getUTCMinutes', 'getUTCMonth', 'getUTCSeconds', 'toISOString', 'toGMTString', 'toUTCString', 'valueOf', 'getTime'],
27
+ z: ['getDate', 'getDay', 'getFullYear', 'getHours', 'getMilliseconds', 'getMinutes', 'getMonth', 'getSeconds', 'getYear', 'toDateString', 'toLocaleDateString', 'toLocaleTimeString']
28
+ },
29
+ d = this;
30
+
31
+ d.date = d.dateZ = (arguments.length > 1) ? new Date(Date.UTC.apply(Date, arguments) + ((new Date()).getTimezoneOffset() * 60000)) : (arguments.length === 1) ? new Date(new Date(arguments['0'])) : new Date();
32
+
33
+ d.timezoneOffset = d.dateZ.getTimezoneOffset();
34
+
35
+ utils.each(members.z, function (name) {
36
+ d[name] = function () {
37
+ return d.dateZ[name]();
38
+ };
39
+ });
40
+ utils.each(members['default'], function (name) {
41
+ d[name] = function () {
42
+ return d.date[name]();
43
+ };
44
+ });
45
+
46
+ this.setTimezoneOffset(exports.tzOffset);
47
+ };
48
+ exports.DateZ.prototype = {
49
+ getTimezoneOffset: function () {
50
+ return this.timezoneOffset;
51
+ },
52
+ setTimezoneOffset: function (offset) {
53
+ this.timezoneOffset = offset;
54
+ this.dateZ = new Date(this.date.getTime() + this.date.getTimezoneOffset() * 60000 - this.timezoneOffset * 60000);
55
+ return this;
56
+ }
57
+ };
58
+
59
+ // Day
60
+ exports.d = function (input) {
61
+ return (input.getDate() < 10 ? '0' : '') + input.getDate();
62
+ };
63
+ exports.D = function (input) {
64
+ return _days.abbr[input.getDay()];
65
+ };
66
+ exports.j = function (input) {
67
+ return input.getDate();
68
+ };
69
+ exports.l = function (input) {
70
+ return _days.full[input.getDay()];
71
+ };
72
+ exports.N = function (input) {
73
+ var d = input.getDay();
74
+ return (d >= 1) ? d : 7;
75
+ };
76
+ exports.S = function (input) {
77
+ var d = input.getDate();
78
+ return (d % 10 === 1 && d !== 11 ? 'st' : (d % 10 === 2 && d !== 12 ? 'nd' : (d % 10 === 3 && d !== 13 ? 'rd' : 'th')));
79
+ };
80
+ exports.w = function (input) {
81
+ return input.getDay();
82
+ };
83
+ exports.z = function (input, offset, abbr) {
84
+ var year = input.getFullYear(),
85
+ e = new exports.DateZ(year, input.getMonth(), input.getDate(), 12, 0, 0),
86
+ d = new exports.DateZ(year, 0, 1, 12, 0, 0);
87
+
88
+ e.setTimezoneOffset(offset, abbr);
89
+ d.setTimezoneOffset(offset, abbr);
90
+ return Math.round((e - d) / 86400000);
91
+ };
92
+
93
+ // Week
94
+ exports.W = function (input) {
95
+ var target = new Date(input.valueOf()),
96
+ dayNr = (input.getDay() + 6) % 7,
97
+ fThurs;
98
+
99
+ target.setDate(target.getDate() - dayNr + 3);
100
+ fThurs = target.valueOf();
101
+ target.setMonth(0, 1);
102
+ if (target.getDay() !== 4) {
103
+ target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7);
104
+ }
105
+
106
+ return 1 + Math.ceil((fThurs - target) / 604800000);
107
+ };
108
+
109
+ // Month
110
+ exports.F = function (input) {
111
+ return _months.full[input.getMonth()];
112
+ };
113
+ exports.m = function (input) {
114
+ return (input.getMonth() < 9 ? '0' : '') + (input.getMonth() + 1);
115
+ };
116
+ exports.M = function (input) {
117
+ return _months.abbr[input.getMonth()];
118
+ };
119
+ exports.n = function (input) {
120
+ return input.getMonth() + 1;
121
+ };
122
+ exports.t = function (input) {
123
+ return 32 - (new Date(input.getFullYear(), input.getMonth(), 32).getDate());
124
+ };
125
+
126
+ // Year
127
+ exports.L = function (input) {
128
+ return new Date(input.getFullYear(), 1, 29).getDate() === 29;
129
+ };
130
+ exports.o = function (input) {
131
+ var target = new Date(input.valueOf());
132
+ target.setDate(target.getDate() - ((input.getDay() + 6) % 7) + 3);
133
+ return target.getFullYear();
134
+ };
135
+ exports.Y = function (input) {
136
+ return input.getFullYear();
137
+ };
138
+ exports.y = function (input) {
139
+ return (input.getFullYear().toString()).substr(2);
140
+ };
141
+
142
+ // Time
143
+ exports.a = function (input) {
144
+ return input.getHours() < 12 ? 'am' : 'pm';
145
+ };
146
+ exports.A = function (input) {
147
+ return input.getHours() < 12 ? 'AM' : 'PM';
148
+ };
149
+ exports.B = function (input) {
150
+ var hours = input.getUTCHours(), beats;
151
+ hours = (hours === 23) ? 0 : hours + 1;
152
+ beats = Math.abs(((((hours * 60) + input.getUTCMinutes()) * 60) + input.getUTCSeconds()) / 86.4).toFixed(0);
153
+ return ('000'.concat(beats).slice(beats.length));
154
+ };
155
+ exports.g = function (input) {
156
+ var h = input.getHours();
157
+ return h === 0 ? 12 : (h > 12 ? h - 12 : h);
158
+ };
159
+ exports.G = function (input) {
160
+ return input.getHours();
161
+ };
162
+ exports.h = function (input) {
163
+ var h = input.getHours();
164
+ return ((h < 10 || (12 < h && 22 > h)) ? '0' : '') + ((h < 12) ? h : h - 12);
165
+ };
166
+ exports.H = function (input) {
167
+ var h = input.getHours();
168
+ return (h < 10 ? '0' : '') + h;
169
+ };
170
+ exports.i = function (input) {
171
+ var m = input.getMinutes();
172
+ return (m < 10 ? '0' : '') + m;
173
+ };
174
+ exports.s = function (input) {
175
+ var s = input.getSeconds();
176
+ return (s < 10 ? '0' : '') + s;
177
+ };
178
+ //u = function () { return ''; },
179
+
180
+ // Timezone
181
+ //e = function () { return ''; },
182
+ //I = function () { return ''; },
183
+ exports.O = function (input) {
184
+ var tz = input.getTimezoneOffset();
185
+ return (tz < 0 ? '-' : '+') + (tz / 60 < 10 ? '0' : '') + Math.abs((tz / 60)) + '00';
186
+ };
187
+ //T = function () { return ''; },
188
+ exports.Z = function (input) {
189
+ return input.getTimezoneOffset() * 60;
190
+ };
191
+
192
+ // Full Date/Time
193
+ exports.c = function (input) {
194
+ return input.toISOString();
195
+ };
196
+ exports.r = function (input) {
197
+ return input.toUTCString();
198
+ };
199
+ exports.U = function (input) {
200
+ return input.getTime() / 1000;
201
+ };
package/lib/engine.js ADDED
@@ -0,0 +1,414 @@
1
+ var utils = require('./utils'),
2
+ backend = require('./backend'),
3
+ cache = require('./cache');
4
+
5
+ /**
6
+ * Empty function used as a fallback in compiled template code.
7
+ * @return {string} Empty string.
8
+ * @private
9
+ */
10
+ function efn() { return ''; }
11
+
12
+ /**
13
+ * Runtime engine plumbing shared across @rhinostone/swig-family frontends.
14
+ *
15
+ * Phase 1 carve — owns the `extends`-chain walker and block-remap helpers.
16
+ * The native Swig constructor in lib/swig.js delegates here so each frontend
17
+ * inherits the loader-walk + circular-extends detection + block merge logic
18
+ * for free.
19
+ *
20
+ * Each helper is pure: frontend-specific state (the resolving loader, the
21
+ * source-to-tokens parseFile, the cache lookup, the parent template cache)
22
+ * is injected via a `deps` parameter rather than captured through a
23
+ * closure. Helpers treat template identifiers as opaque strings — they do
24
+ * not know whether the loader's `resolve` output is a file path, a URL, or
25
+ * a Memcached key.
26
+ *
27
+ * See .claude/architecture/multi-flavor-ir.md — filename-aware code
28
+ * (utils.throwError wrapping, the engine's try/catch that attaches a
29
+ * filename to compile errors) stays in the frontend.
30
+ */
31
+
32
+ /**
33
+ * Re-map block tags inside a parent token list to the child template's
34
+ * overriding block tags.
35
+ *
36
+ * @param {object} blocks Map of block name → overriding block token.
37
+ * @param {array} tokens Parent token list.
38
+ * @return {array} Remapped token list.
39
+ */
40
+ exports.remapBlocks = function remapBlocks(blocks, tokens) {
41
+ return utils.map(tokens, function (token) {
42
+ var args = token.args ? token.args.join('') : '';
43
+ if (token.name === 'block' && blocks[args]) {
44
+ token = blocks[args];
45
+ }
46
+ if (token.content && token.content.length) {
47
+ token.content = remapBlocks(blocks, token.content);
48
+ }
49
+ return token;
50
+ });
51
+ };
52
+
53
+ /**
54
+ * Inject the child template's non-`block` block-level tags (e.g. `set`,
55
+ * `import`) onto the top of the rendered parent's token list so they run
56
+ * before the parent's body.
57
+ *
58
+ * @param {object} blocks Child template's block-level token map.
59
+ * @param {array} tokens Token list to prepend onto.
60
+ * @return {undefined}
61
+ */
62
+ exports.importNonBlocks = function (blocks, tokens) {
63
+ var temp = [];
64
+ utils.each(blocks, function (block) { temp.push(block); });
65
+ utils.each(temp.reverse(), function (block) {
66
+ if (block.name !== 'block') {
67
+ tokens.unshift(block);
68
+ }
69
+ });
70
+ };
71
+
72
+ /**
73
+ * Walk a template's `extends` chain and build the parent token tree.
74
+ * Detects circular inheritance.
75
+ *
76
+ * Deps are injected so each frontend can plug in its own loader and
77
+ * parse-to-tokens implementation:
78
+ *
79
+ * deps.resolve(to, from) → string (loader.resolve)
80
+ * deps.parseFile(pathname, opts) → token tree (frontend-specific)
81
+ * deps.cacheGet(key, options) → token tree | undefined
82
+ *
83
+ * @param {object} tokens Parsed token tree for the child template.
84
+ * Must expose `.parent` (or falsy).
85
+ * @param {object} [options] Per-call Swig options. `options.filename` is
86
+ * required when `tokens.parent` is set.
87
+ * @param {object} deps Injected frontend helpers.
88
+ * @return {array} Parent templates, outermost-first.
89
+ */
90
+ exports.getParents = function (tokens, options, deps) {
91
+ var parentName = tokens.parent,
92
+ parentFiles = [],
93
+ parents = [],
94
+ parentFile,
95
+ parent,
96
+ l;
97
+
98
+ while (parentName) {
99
+ if (!options || !options.filename) {
100
+ throw new Error('Cannot extend "' + parentName + '" because current template has no filename.');
101
+ }
102
+
103
+ parentFile = parentFile || options.filename;
104
+ parentFile = deps.resolve(parentName, parentFile);
105
+ parent = deps.cacheGet(parentFile, options) || deps.parseFile(parentFile, utils.extend({}, options, { filename: parentFile }));
106
+ parentName = parent.parent;
107
+
108
+ if (parentFiles.indexOf(parentFile) !== -1) {
109
+ throw new Error('Illegal circular extends of "' + parentFile + '".');
110
+ }
111
+ parentFiles.push(parentFile);
112
+
113
+ parents.push(parent);
114
+ }
115
+
116
+ // Remap each parent's(1) blocks onto its own parent(2), receiving the full
117
+ // token list for rendering the original parent(1) on its own.
118
+ l = parents.length;
119
+ for (l = parents.length - 2; l >= 0; l -= 1) {
120
+ parents[l].tokens = exports.remapBlocks(parents[l].blocks, parents[l + 1].tokens);
121
+ exports.importNonBlocks(parents[l].blocks, parents[l].tokens);
122
+ }
123
+
124
+ return parents;
125
+ };
126
+
127
+ /**
128
+ * Build a renderable template Function from a parsed token tree.
129
+ *
130
+ * Walks tokens via the swig-core backend codegen, wraps the emitted body
131
+ * in the `new Function('_swig', '_ctx', '_filters', '_utils', '_fn', ...)`
132
+ * construction, and returns the function. The argument list is the
133
+ * contract every tag's compile() output depends on — `_output`, `_ext`,
134
+ * `_ctx`, etc. are referenced by name in emitted code.
135
+ *
136
+ * Filename attribution on compile-time failures lives on the frontend
137
+ * per the seam rule (the caller knows which template the body came from
138
+ * and can attach that via options.filename in its own try/catch). This
139
+ * helper only throws whatever `new Function(...)` throws — the caller
140
+ * can catch and rewrap.
141
+ *
142
+ * @param {object|array} tokens Parsed token tree.
143
+ * @param {array} [parents] Parent tokens from getParents().
144
+ * @param {object} [options] Swig options object.
145
+ * @return {Function} Template function.
146
+ */
147
+ exports.buildTemplateFunction = function (tokens, parents, options) {
148
+ return new Function('_swig', '_ctx', '_filters', '_utils', '_fn',
149
+ ' var _ext = _swig.extensions,\n' +
150
+ ' _output = "";\n' +
151
+ backend.compile(tokens, parents, options) + '\n' +
152
+ ' return _output;\n'
153
+ );
154
+ };
155
+
156
+ /**
157
+ * Install the swig-family runtime API on a Swig instance. Called by each
158
+ * frontend's constructor after state init (options, cache, extensions).
159
+ *
160
+ * The frontend supplies its per-flavor parser, tag/filter maps, option
161
+ * validator, and a filename-attribution error wrap. Install then attaches
162
+ * the full instance API — setFilter, setTag, setExtension, parse,
163
+ * parseFile, precompile, compile, compileFile, render, renderFile, run,
164
+ * invalidateCache — to `self`.
165
+ *
166
+ * Frontend contract:
167
+ * frontend.parser Module with a `.parse(swig, src, opts, tags, filters)` method.
168
+ * frontend.tags Tag map mutated by setTag.
169
+ * frontend.filters Filter map mutated by setFilter.
170
+ * frontend.validateOptions Per-flavor options validator. Called at every parse/setDefaults entry point.
171
+ * frontend.onCompileError Invoked with (err, options) when `new Function(body)` throws. Owns filename attribution per the seam rule.
172
+ *
173
+ * @param {object} self Swig instance. Must already have `options`,
174
+ * `cache`, and `extensions` populated.
175
+ * @param {object} frontend Per-flavor wiring. See above.
176
+ * @return {undefined}
177
+ */
178
+ exports.install = function (self, frontend) {
179
+ var parser = frontend.parser,
180
+ tags = frontend.tags,
181
+ filters = frontend.filters,
182
+ validateOptions = frontend.validateOptions,
183
+ onCompileError = frontend.onCompileError;
184
+
185
+ function getLocals(options) {
186
+ if (!options || !options.locals) {
187
+ return self.options.locals;
188
+ }
189
+ return utils.extend({}, self.options.locals, options.locals);
190
+ }
191
+
192
+ function cacheGet(key, options) {
193
+ return cache.cacheGet(key, options, self.options.cache, self.cache);
194
+ }
195
+
196
+ function cacheSet(key, options, val) {
197
+ cache.cacheSet(key, options, val, self.options.cache, self.cache);
198
+ }
199
+
200
+ function getParentsInternal(tokens, options) {
201
+ return exports.getParents(tokens, options, {
202
+ resolve: function (to, from) { return self.options.loader.resolve(to, from); },
203
+ parseFile: self.parseFile,
204
+ cacheGet: cacheGet
205
+ });
206
+ }
207
+
208
+ self.invalidateCache = function () {
209
+ if (self.options.cache === 'memory') {
210
+ self.cache = {};
211
+ }
212
+ };
213
+
214
+ self.setFilter = function (name, method) {
215
+ if (typeof method !== "function") {
216
+ throw new Error('Filter "' + name + '" is not a valid function.');
217
+ }
218
+ filters[name] = method;
219
+ };
220
+
221
+ self.setTag = function (name, parse, compile, ends, blockLevel) {
222
+ if (typeof parse !== 'function') {
223
+ throw new Error('Tag "' + name + '" parse method is not a valid function.');
224
+ }
225
+ if (typeof compile !== 'function') {
226
+ throw new Error('Tag "' + name + '" compile method is not a valid function.');
227
+ }
228
+ tags[name] = {
229
+ parse: parse,
230
+ compile: compile,
231
+ ends: ends || false,
232
+ block: !!blockLevel
233
+ };
234
+ };
235
+
236
+ self.setExtension = function (name, object) {
237
+ self.extensions[name] = object;
238
+ };
239
+
240
+ self.parse = function (source, options) {
241
+ validateOptions(options);
242
+
243
+ var locals = getLocals(options),
244
+ opts = {},
245
+ k;
246
+
247
+ for (k in options) {
248
+ if (options.hasOwnProperty(k) && k !== 'locals') {
249
+ opts[k] = options[k];
250
+ }
251
+ }
252
+
253
+ options = utils.extend({}, self.options, opts);
254
+ options.locals = locals;
255
+
256
+ return parser.parse(self, source, options, tags, filters);
257
+ };
258
+
259
+ self.parseFile = function (pathname, options) {
260
+ var src;
261
+
262
+ if (!options) {
263
+ options = {};
264
+ }
265
+
266
+ pathname = self.options.loader.resolve(pathname, options.resolveFrom);
267
+ src = self.options.loader.load(pathname);
268
+
269
+ if (!options.filename) {
270
+ options = utils.extend({ filename: pathname }, options);
271
+ }
272
+
273
+ return self.parse(src, options);
274
+ };
275
+
276
+ self.precompile = function (source, options) {
277
+ var tokens = self.parse(source, options),
278
+ parents = getParentsInternal(tokens, options),
279
+ tpl;
280
+
281
+ if (parents.length) {
282
+ tokens.tokens = exports.remapBlocks(tokens.blocks, parents[0].tokens);
283
+ exports.importNonBlocks(tokens.blocks, tokens.tokens);
284
+ }
285
+
286
+ try {
287
+ tpl = exports.buildTemplateFunction(tokens, parents, options);
288
+ } catch (e) {
289
+ onCompileError(e, options);
290
+ }
291
+
292
+ return { tpl: tpl, tokens: tokens };
293
+ };
294
+
295
+ self.compile = function (source, options) {
296
+ var key = options ? options.filename : null,
297
+ cached = key ? cacheGet(key, options) : null,
298
+ context,
299
+ contextLength,
300
+ pre;
301
+
302
+ if (cached) {
303
+ return cached;
304
+ }
305
+
306
+ context = getLocals(options);
307
+ contextLength = utils.keys(context).length;
308
+ pre = self.precompile(source, options);
309
+
310
+ function compiled(locals) {
311
+ var lcls;
312
+ if (locals && contextLength) {
313
+ lcls = utils.extend({}, context, locals);
314
+ } else if (locals && !contextLength) {
315
+ lcls = locals;
316
+ } else if (!locals && contextLength) {
317
+ lcls = context;
318
+ } else {
319
+ lcls = {};
320
+ }
321
+ return pre.tpl(self, lcls, filters, utils, efn);
322
+ }
323
+
324
+ utils.extend(compiled, pre.tokens);
325
+
326
+ if (key) {
327
+ cacheSet(key, options, compiled);
328
+ }
329
+
330
+ return compiled;
331
+ };
332
+
333
+ self.compileFile = function (pathname, options, cb) {
334
+ var src, cached;
335
+
336
+ if (!options) {
337
+ options = {};
338
+ }
339
+
340
+ pathname = self.options.loader.resolve(pathname, options.resolveFrom);
341
+ if (!options.filename) {
342
+ options = utils.extend({ filename: pathname }, options);
343
+ }
344
+ cached = cacheGet(pathname, options);
345
+
346
+ if (cached) {
347
+ if (cb) {
348
+ cb(null, cached);
349
+ return;
350
+ }
351
+ return cached;
352
+ }
353
+
354
+ if (cb) {
355
+ self.options.loader.load(pathname, function (err, src) {
356
+ if (err) {
357
+ cb(err);
358
+ return;
359
+ }
360
+ var compiled;
361
+
362
+ try {
363
+ compiled = self.compile(src, options);
364
+ } catch (err2) {
365
+ cb(err2);
366
+ return;
367
+ }
368
+
369
+ cb(err, compiled);
370
+ });
371
+ return;
372
+ }
373
+
374
+ src = self.options.loader.load(pathname);
375
+ return self.compile(src, options);
376
+ };
377
+
378
+ self.render = function (source, options) {
379
+ return self.compile(source, options)();
380
+ };
381
+
382
+ self.renderFile = function (pathName, locals, cb) {
383
+ if (cb) {
384
+ self.compileFile(pathName, {}, function (err, fn) {
385
+ var result;
386
+
387
+ if (err) {
388
+ cb(err);
389
+ return;
390
+ }
391
+
392
+ try {
393
+ result = fn(locals);
394
+ } catch (err2) {
395
+ cb(err2);
396
+ return;
397
+ }
398
+
399
+ cb(null, result);
400
+ });
401
+ return;
402
+ }
403
+
404
+ return self.compileFile(pathName)(locals);
405
+ };
406
+
407
+ self.run = function (tpl, locals, filepath) {
408
+ var context = getLocals({ locals: locals });
409
+ if (filepath) {
410
+ cacheSet(filepath, {}, tpl);
411
+ }
412
+ return tpl(self, context, filters, utils, efn);
413
+ };
414
+ };