@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/backend.js +718 -0
- package/lib/cache.js +75 -0
- package/lib/dateformatter.js +201 -0
- package/lib/engine.js +414 -0
- package/lib/filters.js +43 -0
- package/lib/index.js +21 -0
- package/lib/ir.js +873 -0
- package/lib/loaders/filesystem.js +59 -0
- package/lib/loaders/index.js +53 -0
- package/lib/loaders/memory.js +63 -0
- package/lib/security.js +25 -0
- package/lib/tokenparser.js +920 -0
- package/lib/tokentypes.js +78 -0
- package/lib/utils.js +184 -0
- package/package.json +26 -0
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
|
+
};
|