Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80713 views
1
import Exception from "../exception";
2
import {isArray, indexOf} from "../utils";
3
import AST from "./ast";
4
5
var slice = [].slice;
6
7
8
export function Compiler() {}
9
10
// the foundHelper register will disambiguate helper lookup from finding a
11
// function in a context. This is necessary for mustache compatibility, which
12
// requires that context functions in blocks are evaluated by blockHelperMissing,
13
// and then proceed as if the resulting value was provided to blockHelperMissing.
14
15
Compiler.prototype = {
16
compiler: Compiler,
17
18
equals: function(other) {
19
var len = this.opcodes.length;
20
if (other.opcodes.length !== len) {
21
return false;
22
}
23
24
for (var i = 0; i < len; i++) {
25
var opcode = this.opcodes[i],
26
otherOpcode = other.opcodes[i];
27
if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) {
28
return false;
29
}
30
}
31
32
// We know that length is the same between the two arrays because they are directly tied
33
// to the opcode behavior above.
34
len = this.children.length;
35
for (i = 0; i < len; i++) {
36
if (!this.children[i].equals(other.children[i])) {
37
return false;
38
}
39
}
40
41
return true;
42
},
43
44
guid: 0,
45
46
compile: function(program, options) {
47
this.sourceNode = [];
48
this.opcodes = [];
49
this.children = [];
50
this.options = options;
51
this.stringParams = options.stringParams;
52
this.trackIds = options.trackIds;
53
54
options.blockParams = options.blockParams || [];
55
56
// These changes will propagate to the other compiler components
57
var knownHelpers = options.knownHelpers;
58
options.knownHelpers = {
59
'helperMissing': true,
60
'blockHelperMissing': true,
61
'each': true,
62
'if': true,
63
'unless': true,
64
'with': true,
65
'log': true,
66
'lookup': true
67
};
68
if (knownHelpers) {
69
for (var name in knownHelpers) {
70
options.knownHelpers[name] = knownHelpers[name];
71
}
72
}
73
74
return this.accept(program);
75
},
76
77
compileProgram: function(program) {
78
var result = new this.compiler().compile(program, this.options);
79
var guid = this.guid++;
80
81
this.usePartial = this.usePartial || result.usePartial;
82
83
this.children[guid] = result;
84
this.useDepths = this.useDepths || result.useDepths;
85
86
return guid;
87
},
88
89
accept: function(node) {
90
this.sourceNode.unshift(node);
91
var ret = this[node.type](node);
92
this.sourceNode.shift();
93
return ret;
94
},
95
96
Program: function(program) {
97
this.options.blockParams.unshift(program.blockParams);
98
99
var body = program.body;
100
for(var i=0, l=body.length; i<l; i++) {
101
this.accept(body[i]);
102
}
103
104
this.options.blockParams.shift();
105
106
this.isSimple = l === 1;
107
this.blockParams = program.blockParams ? program.blockParams.length : 0;
108
109
return this;
110
},
111
112
BlockStatement: function(block) {
113
transformLiteralToPath(block);
114
115
var program = block.program,
116
inverse = block.inverse;
117
118
program = program && this.compileProgram(program);
119
inverse = inverse && this.compileProgram(inverse);
120
121
var type = this.classifySexpr(block);
122
123
if (type === 'helper') {
124
this.helperSexpr(block, program, inverse);
125
} else if (type === 'simple') {
126
this.simpleSexpr(block);
127
128
// now that the simple mustache is resolved, we need to
129
// evaluate it by executing `blockHelperMissing`
130
this.opcode('pushProgram', program);
131
this.opcode('pushProgram', inverse);
132
this.opcode('emptyHash');
133
this.opcode('blockValue', block.path.original);
134
} else {
135
this.ambiguousSexpr(block, program, inverse);
136
137
// now that the simple mustache is resolved, we need to
138
// evaluate it by executing `blockHelperMissing`
139
this.opcode('pushProgram', program);
140
this.opcode('pushProgram', inverse);
141
this.opcode('emptyHash');
142
this.opcode('ambiguousBlockValue');
143
}
144
145
this.opcode('append');
146
},
147
148
PartialStatement: function(partial) {
149
this.usePartial = true;
150
151
var params = partial.params;
152
if (params.length > 1) {
153
throw new Exception('Unsupported number of partial arguments: ' + params.length, partial);
154
} else if (!params.length) {
155
params.push({type: 'PathExpression', parts: [], depth: 0});
156
}
157
158
var partialName = partial.name.original,
159
isDynamic = partial.name.type === 'SubExpression';
160
if (isDynamic) {
161
this.accept(partial.name);
162
}
163
164
this.setupFullMustacheParams(partial, undefined, undefined, true);
165
166
var indent = partial.indent || '';
167
if (this.options.preventIndent && indent) {
168
this.opcode('appendContent', indent);
169
indent = '';
170
}
171
172
this.opcode('invokePartial', isDynamic, partialName, indent);
173
this.opcode('append');
174
},
175
176
MustacheStatement: function(mustache) {
177
this.SubExpression(mustache);
178
179
if(mustache.escaped && !this.options.noEscape) {
180
this.opcode('appendEscaped');
181
} else {
182
this.opcode('append');
183
}
184
},
185
186
ContentStatement: function(content) {
187
if (content.value) {
188
this.opcode('appendContent', content.value);
189
}
190
},
191
192
CommentStatement: function() {},
193
194
SubExpression: function(sexpr) {
195
transformLiteralToPath(sexpr);
196
var type = this.classifySexpr(sexpr);
197
198
if (type === 'simple') {
199
this.simpleSexpr(sexpr);
200
} else if (type === 'helper') {
201
this.helperSexpr(sexpr);
202
} else {
203
this.ambiguousSexpr(sexpr);
204
}
205
},
206
ambiguousSexpr: function(sexpr, program, inverse) {
207
var path = sexpr.path,
208
name = path.parts[0],
209
isBlock = program != null || inverse != null;
210
211
this.opcode('getContext', path.depth);
212
213
this.opcode('pushProgram', program);
214
this.opcode('pushProgram', inverse);
215
216
this.accept(path);
217
218
this.opcode('invokeAmbiguous', name, isBlock);
219
},
220
221
simpleSexpr: function(sexpr) {
222
this.accept(sexpr.path);
223
this.opcode('resolvePossibleLambda');
224
},
225
226
helperSexpr: function(sexpr, program, inverse) {
227
var params = this.setupFullMustacheParams(sexpr, program, inverse),
228
path = sexpr.path,
229
name = path.parts[0];
230
231
if (this.options.knownHelpers[name]) {
232
this.opcode('invokeKnownHelper', params.length, name);
233
} else if (this.options.knownHelpersOnly) {
234
throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
235
} else {
236
path.falsy = true;
237
238
this.accept(path);
239
this.opcode('invokeHelper', params.length, path.original, AST.helpers.simpleId(path));
240
}
241
},
242
243
PathExpression: function(path) {
244
this.addDepth(path.depth);
245
this.opcode('getContext', path.depth);
246
247
var name = path.parts[0],
248
scoped = AST.helpers.scopedId(path),
249
blockParamId = !path.depth && !scoped && this.blockParamIndex(name);
250
251
if (blockParamId) {
252
this.opcode('lookupBlockParam', blockParamId, path.parts);
253
} else if (!name) {
254
// Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
255
this.opcode('pushContext');
256
} else if (path.data) {
257
this.options.data = true;
258
this.opcode('lookupData', path.depth, path.parts);
259
} else {
260
this.opcode('lookupOnContext', path.parts, path.falsy, scoped);
261
}
262
},
263
264
StringLiteral: function(string) {
265
this.opcode('pushString', string.value);
266
},
267
268
NumberLiteral: function(number) {
269
this.opcode('pushLiteral', number.value);
270
},
271
272
BooleanLiteral: function(bool) {
273
this.opcode('pushLiteral', bool.value);
274
},
275
276
Hash: function(hash) {
277
var pairs = hash.pairs, i, l;
278
279
this.opcode('pushHash');
280
281
for (i=0, l=pairs.length; i<l; i++) {
282
this.pushParam(pairs[i].value);
283
}
284
while (i--) {
285
this.opcode('assignToHash', pairs[i].key);
286
}
287
this.opcode('popHash');
288
},
289
290
// HELPERS
291
opcode: function(name) {
292
this.opcodes.push({ opcode: name, args: slice.call(arguments, 1), loc: this.sourceNode[0].loc });
293
},
294
295
addDepth: function(depth) {
296
if (!depth) {
297
return;
298
}
299
300
this.useDepths = true;
301
},
302
303
classifySexpr: function(sexpr) {
304
var isSimple = AST.helpers.simpleId(sexpr.path);
305
306
var isBlockParam = isSimple && !!this.blockParamIndex(sexpr.path.parts[0]);
307
308
// a mustache is an eligible helper if:
309
// * its id is simple (a single part, not `this` or `..`)
310
var isHelper = !isBlockParam && AST.helpers.helperExpression(sexpr);
311
312
// if a mustache is an eligible helper but not a definite
313
// helper, it is ambiguous, and will be resolved in a later
314
// pass or at runtime.
315
var isEligible = !isBlockParam && (isHelper || isSimple);
316
317
var options = this.options;
318
319
// if ambiguous, we can possibly resolve the ambiguity now
320
// An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc.
321
if (isEligible && !isHelper) {
322
var name = sexpr.path.parts[0];
323
324
if (options.knownHelpers[name]) {
325
isHelper = true;
326
} else if (options.knownHelpersOnly) {
327
isEligible = false;
328
}
329
}
330
331
if (isHelper) { return 'helper'; }
332
else if (isEligible) { return 'ambiguous'; }
333
else { return 'simple'; }
334
},
335
336
pushParams: function(params) {
337
for(var i=0, l=params.length; i<l; i++) {
338
this.pushParam(params[i]);
339
}
340
},
341
342
pushParam: function(val) {
343
var value = val.value != null ? val.value : val.original || '';
344
345
if (this.stringParams) {
346
if (value.replace) {
347
value = value
348
.replace(/^(\.?\.\/)*/g, '')
349
.replace(/\//g, '.');
350
}
351
352
if(val.depth) {
353
this.addDepth(val.depth);
354
}
355
this.opcode('getContext', val.depth || 0);
356
this.opcode('pushStringParam', value, val.type);
357
358
if (val.type === 'SubExpression') {
359
// SubExpressions get evaluated and passed in
360
// in string params mode.
361
this.accept(val);
362
}
363
} else {
364
if (this.trackIds) {
365
var blockParamIndex;
366
if (val.parts && !AST.helpers.scopedId(val) && !val.depth) {
367
blockParamIndex = this.blockParamIndex(val.parts[0]);
368
}
369
if (blockParamIndex) {
370
var blockParamChild = val.parts.slice(1).join('.');
371
this.opcode('pushId', 'BlockParam', blockParamIndex, blockParamChild);
372
} else {
373
value = val.original || value;
374
if (value.replace) {
375
value = value
376
.replace(/^\.\//g, '')
377
.replace(/^\.$/g, '');
378
}
379
380
this.opcode('pushId', val.type, value);
381
}
382
}
383
this.accept(val);
384
}
385
},
386
387
setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) {
388
var params = sexpr.params;
389
this.pushParams(params);
390
391
this.opcode('pushProgram', program);
392
this.opcode('pushProgram', inverse);
393
394
if (sexpr.hash) {
395
this.accept(sexpr.hash);
396
} else {
397
this.opcode('emptyHash', omitEmpty);
398
}
399
400
return params;
401
},
402
403
blockParamIndex: function(name) {
404
for (var depth = 0, len = this.options.blockParams.length; depth < len; depth++) {
405
var blockParams = this.options.blockParams[depth],
406
param = blockParams && indexOf(blockParams, name);
407
if (blockParams && param >= 0) {
408
return [depth, param];
409
}
410
}
411
}
412
};
413
414
export function precompile(input, options, env) {
415
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
416
throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
417
}
418
419
options = options || {};
420
if (!('data' in options)) {
421
options.data = true;
422
}
423
if (options.compat) {
424
options.useDepths = true;
425
}
426
427
var ast = env.parse(input, options);
428
var environment = new env.Compiler().compile(ast, options);
429
return new env.JavaScriptCompiler().compile(environment, options);
430
}
431
432
export function compile(input, options, env) {
433
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
434
throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
435
}
436
437
options = options || {};
438
439
if (!('data' in options)) {
440
options.data = true;
441
}
442
if (options.compat) {
443
options.useDepths = true;
444
}
445
446
var compiled;
447
448
function compileInput() {
449
var ast = env.parse(input, options);
450
var environment = new env.Compiler().compile(ast, options);
451
var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
452
return env.template(templateSpec);
453
}
454
455
// Template is only compiled on first use and cached after that point.
456
var ret = function(context, options) {
457
if (!compiled) {
458
compiled = compileInput();
459
}
460
return compiled.call(this, context, options);
461
};
462
ret._setup = function(options) {
463
if (!compiled) {
464
compiled = compileInput();
465
}
466
return compiled._setup(options);
467
};
468
ret._child = function(i, data, blockParams, depths) {
469
if (!compiled) {
470
compiled = compileInput();
471
}
472
return compiled._child(i, data, blockParams, depths);
473
};
474
return ret;
475
}
476
477
function argEquals(a, b) {
478
if (a === b) {
479
return true;
480
}
481
482
if (isArray(a) && isArray(b) && a.length === b.length) {
483
for (var i = 0; i < a.length; i++) {
484
if (!argEquals(a[i], b[i])) {
485
return false;
486
}
487
}
488
return true;
489
}
490
}
491
492
function transformLiteralToPath(sexpr) {
493
if (!sexpr.path.parts) {
494
var literal = sexpr.path;
495
// Casting to string here to make false and 0 literal values play nicely with the rest
496
// of the system.
497
sexpr.path = new AST.PathExpression(false, 0, [literal.original+''], literal.original+'', literal.log);
498
}
499
}
500
501