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