1191 lines
108 KiB
JavaScript
1191 lines
108 KiB
JavaScript
|
(function () {
|
||
|
|
||
|
/* Imports */
|
||
|
var Meteor = Package.meteor.Meteor;
|
||
|
var HTML = Package.htmljs.HTML;
|
||
|
var HTMLTools = Package['html-tools'].HTMLTools;
|
||
|
var BlazeTools = Package['blaze-tools'].BlazeTools;
|
||
|
var _ = Package.underscore._;
|
||
|
|
||
|
/* Package-scope variables */
|
||
|
var SpacebarsCompiler, TemplateTag;
|
||
|
|
||
|
(function () {
|
||
|
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
// //
|
||
|
// packages/spacebars-compiler/templatetag.js //
|
||
|
// //
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
//
|
||
|
SpacebarsCompiler = {}; // 1
|
||
|
// 2
|
||
|
// A TemplateTag is the result of parsing a single `{{...}}` tag. // 3
|
||
|
// // 4
|
||
|
// The `.type` of a TemplateTag is one of: // 5
|
||
|
// // 6
|
||
|
// - `"DOUBLE"` - `{{foo}}` // 7
|
||
|
// - `"TRIPLE"` - `{{{foo}}}` // 8
|
||
|
// - `"COMMENT"` - `{{! foo}}` // 9
|
||
|
// - `"BLOCKCOMMENT" - `{{!-- foo--}}` // 10
|
||
|
// - `"INCLUSION"` - `{{> foo}}` // 11
|
||
|
// - `"BLOCKOPEN"` - `{{#foo}}` // 12
|
||
|
// - `"BLOCKCLOSE"` - `{{/foo}}` // 13
|
||
|
// - `"ELSE"` - `{{else}}` // 14
|
||
|
// - `"ESCAPE"` - `{{|`, `{{{|`, `{{{{|` and so on // 15
|
||
|
// // 16
|
||
|
// Besides `type`, the mandatory properties of a TemplateTag are: // 17
|
||
|
// // 18
|
||
|
// - `path` - An array of one or more strings. The path of `{{foo.bar}}` // 19
|
||
|
// is `["foo", "bar"]`. Applies to DOUBLE, TRIPLE, INCLUSION, BLOCKOPEN, // 20
|
||
|
// and BLOCKCLOSE. // 21
|
||
|
// // 22
|
||
|
// - `args` - An array of zero or more argument specs. An argument spec // 23
|
||
|
// is a two or three element array, consisting of a type, value, and // 24
|
||
|
// optional keyword name. For example, the `args` of `{{foo "bar" x=3}}` // 25
|
||
|
// are `[["STRING", "bar"], ["NUMBER", 3, "x"]]`. Applies to DOUBLE, // 26
|
||
|
// TRIPLE, INCLUSION, and BLOCKOPEN. // 27
|
||
|
// // 28
|
||
|
// - `value` - A string of the comment's text. Applies to COMMENT and // 29
|
||
|
// BLOCKCOMMENT. // 30
|
||
|
// // 31
|
||
|
// These additional are typically set during parsing: // 32
|
||
|
// // 33
|
||
|
// - `position` - The HTMLTools.TEMPLATE_TAG_POSITION specifying at what sort // 34
|
||
|
// of site the TemplateTag was encountered (e.g. at element level or as // 35
|
||
|
// part of an attribute value). Its absence implies // 36
|
||
|
// TEMPLATE_TAG_POSITION.ELEMENT. // 37
|
||
|
// // 38
|
||
|
// - `content` and `elseContent` - When a BLOCKOPEN tag's contents are // 39
|
||
|
// parsed, they are put here. `elseContent` will only be present if // 40
|
||
|
// an `{{else}}` was found. // 41
|
||
|
// 42
|
||
|
var TEMPLATE_TAG_POSITION = HTMLTools.TEMPLATE_TAG_POSITION; // 43
|
||
|
// 44
|
||
|
TemplateTag = SpacebarsCompiler.TemplateTag = function () { // 45
|
||
|
HTMLTools.TemplateTag.apply(this, arguments); // 46
|
||
|
}; // 47
|
||
|
TemplateTag.prototype = new HTMLTools.TemplateTag; // 48
|
||
|
TemplateTag.prototype.constructorName = 'SpacebarsCompiler.TemplateTag'; // 49
|
||
|
// 50
|
||
|
var makeStacheTagStartRegex = function (r) { // 51
|
||
|
return new RegExp(r.source + /(?![{>!#/])/.source, // 52
|
||
|
r.ignoreCase ? 'i' : ''); // 53
|
||
|
}; // 54
|
||
|
// 55
|
||
|
// "starts" regexes are used to see what type of template // 56
|
||
|
// tag the parser is looking at. They must match a non-empty // 57
|
||
|
// result, but not the interesting part of the tag. // 58
|
||
|
var starts = { // 59
|
||
|
ESCAPE: /^\{\{(?=\{*\|)/, // 60
|
||
|
ELSE: makeStacheTagStartRegex(/^\{\{\s*else(?=[\s}])/i), // 61
|
||
|
DOUBLE: makeStacheTagStartRegex(/^\{\{\s*(?!\s)/), // 62
|
||
|
TRIPLE: makeStacheTagStartRegex(/^\{\{\{\s*(?!\s)/), // 63
|
||
|
BLOCKCOMMENT: makeStacheTagStartRegex(/^\{\{\s*!--/), // 64
|
||
|
COMMENT: makeStacheTagStartRegex(/^\{\{\s*!/), // 65
|
||
|
INCLUSION: makeStacheTagStartRegex(/^\{\{\s*>\s*(?!\s)/), // 66
|
||
|
BLOCKOPEN: makeStacheTagStartRegex(/^\{\{\s*#\s*(?!\s)/), // 67
|
||
|
BLOCKCLOSE: makeStacheTagStartRegex(/^\{\{\s*\/\s*(?!\s)/) // 68
|
||
|
}; // 69
|
||
|
// 70
|
||
|
var ends = { // 71
|
||
|
DOUBLE: /^\s*\}\}/, // 72
|
||
|
TRIPLE: /^\s*\}\}\}/ // 73
|
||
|
}; // 74
|
||
|
// 75
|
||
|
// Parse a tag from the provided scanner or string. If the input // 76
|
||
|
// doesn't start with `{{`, returns null. Otherwise, either succeeds // 77
|
||
|
// and returns a SpacebarsCompiler.TemplateTag, or throws an error (using // 78
|
||
|
// `scanner.fatal` if a scanner is provided). // 79
|
||
|
TemplateTag.parse = function (scannerOrString) { // 80
|
||
|
var scanner = scannerOrString; // 81
|
||
|
if (typeof scanner === 'string') // 82
|
||
|
scanner = new HTMLTools.Scanner(scannerOrString); // 83
|
||
|
// 84
|
||
|
if (! (scanner.peek() === '{' && // 85
|
||
|
(scanner.rest()).slice(0, 2) === '{{')) // 86
|
||
|
return null; // 87
|
||
|
// 88
|
||
|
var run = function (regex) { // 89
|
||
|
// regex is assumed to start with `^` // 90
|
||
|
var result = regex.exec(scanner.rest()); // 91
|
||
|
if (! result) // 92
|
||
|
return null; // 93
|
||
|
var ret = result[0]; // 94
|
||
|
scanner.pos += ret.length; // 95
|
||
|
return ret; // 96
|
||
|
}; // 97
|
||
|
// 98
|
||
|
var advance = function (amount) { // 99
|
||
|
scanner.pos += amount; // 100
|
||
|
}; // 101
|
||
|
// 102
|
||
|
var scanIdentifier = function (isFirstInPath) { // 103
|
||
|
var id = BlazeTools.parseIdentifierName(scanner); // 104
|
||
|
if (! id) // 105
|
||
|
expected('IDENTIFIER'); // 106
|
||
|
if (isFirstInPath && // 107
|
||
|
(id === 'null' || id === 'true' || id === 'false')) // 108
|
||
|
scanner.fatal("Can't use null, true, or false, as an identifier at start of path"); // 109
|
||
|
// 110
|
||
|
return id; // 111
|
||
|
}; // 112
|
||
|
// 113
|
||
|
var scanPath = function () { // 114
|
||
|
var segments = []; // 115
|
||
|
// 116
|
||
|
// handle initial `.`, `..`, `./`, `../`, `../..`, `../../`, etc // 117
|
||
|
var dots; // 118
|
||
|
if ((dots = run(/^[\.\/]+/))) { // 119
|
||
|
var ancestorStr = '.'; // eg `../../..` maps to `....` // 120
|
||
|
var endsWithSlash = /\/$/.test(dots); // 121
|
||
|
// 122
|
||
|
if (endsWithSlash) // 123
|
||
|
dots = dots.slice(0, -1); // 124
|
||
|
// 125
|
||
|
_.each(dots.split('/'), function(dotClause, index) { // 126
|
||
|
if (index === 0) { // 127
|
||
|
if (dotClause !== '.' && dotClause !== '..') // 128
|
||
|
expected("`.`, `..`, `./` or `../`"); // 129
|
||
|
} else { // 130
|
||
|
if (dotClause !== '..') // 131
|
||
|
expected("`..` or `../`"); // 132
|
||
|
} // 133
|
||
|
// 134
|
||
|
if (dotClause === '..') // 135
|
||
|
ancestorStr += '.'; // 136
|
||
|
}); // 137
|
||
|
// 138
|
||
|
segments.push(ancestorStr); // 139
|
||
|
// 140
|
||
|
if (!endsWithSlash) // 141
|
||
|
return segments; // 142
|
||
|
} // 143
|
||
|
// 144
|
||
|
while (true) { // 145
|
||
|
// scan a path segment // 146
|
||
|
// 147
|
||
|
if (run(/^\[/)) { // 148
|
||
|
var seg = run(/^[\s\S]*?\]/); // 149
|
||
|
if (! seg) // 150
|
||
|
error("Unterminated path segment"); // 151
|
||
|
seg = seg.slice(0, -1); // 152
|
||
|
if (! seg && ! segments.length) // 153
|
||
|
error("Path can't start with empty string"); // 154
|
||
|
segments.push(seg); // 155
|
||
|
} else { // 156
|
||
|
var id = scanIdentifier(! segments.length); // 157
|
||
|
if (id === 'this') { // 158
|
||
|
if (! segments.length) { // 159
|
||
|
// initial `this` // 160
|
||
|
segments.push('.'); // 161
|
||
|
} else { // 162
|
||
|
error("Can only use `this` at the beginning of a path.\nInstead of `foo.this` or `../this`, just write `foo` or `..`.");
|
||
|
} // 164
|
||
|
} else { // 165
|
||
|
segments.push(id); // 166
|
||
|
} // 167
|
||
|
} // 168
|
||
|
// 169
|
||
|
var sep = run(/^[\.\/]/); // 170
|
||
|
if (! sep) // 171
|
||
|
break; // 172
|
||
|
} // 173
|
||
|
// 174
|
||
|
return segments; // 175
|
||
|
}; // 176
|
||
|
// 177
|
||
|
// scan the keyword portion of a keyword argument // 178
|
||
|
// (the "foo" portion in "foo=bar"). // 179
|
||
|
// Result is either the keyword matched, or null // 180
|
||
|
// if we're not at a keyword argument position. // 181
|
||
|
var scanArgKeyword = function () { // 182
|
||
|
var match = /^([^\{\}\(\)\>#=\s"'\[\]]+)\s*=\s*/.exec(scanner.rest()); // 183
|
||
|
if (match) { // 184
|
||
|
scanner.pos += match[0].length; // 185
|
||
|
return match[1]; // 186
|
||
|
} else { // 187
|
||
|
return null; // 188
|
||
|
} // 189
|
||
|
}; // 190
|
||
|
// 191
|
||
|
// scan an argument; succeeds or errors. // 192
|
||
|
// Result is an array of two or three items: // 193
|
||
|
// type , value, and (indicating a keyword argument) // 194
|
||
|
// keyword name. // 195
|
||
|
var scanArg = function () { // 196
|
||
|
var keyword = scanArgKeyword(); // null if not parsing a kwarg // 197
|
||
|
var value = scanArgValue(); // 198
|
||
|
return keyword ? value.concat(keyword) : value; // 199
|
||
|
}; // 200
|
||
|
// 201
|
||
|
// scan an argument value (for keyword or positional arguments); // 202
|
||
|
// succeeds or errors. Result is an array of type, value. // 203
|
||
|
var scanArgValue = function () { // 204
|
||
|
var startPos = scanner.pos; // 205
|
||
|
var result; // 206
|
||
|
if ((result = BlazeTools.parseNumber(scanner))) { // 207
|
||
|
return ['NUMBER', result.value]; // 208
|
||
|
} else if ((result = BlazeTools.parseStringLiteral(scanner))) { // 209
|
||
|
return ['STRING', result.value]; // 210
|
||
|
} else if (/^[\.\[]/.test(scanner.peek())) { // 211
|
||
|
return ['PATH', scanPath()]; // 212
|
||
|
} else if ((result = BlazeTools.parseIdentifierName(scanner))) { // 213
|
||
|
var id = result; // 214
|
||
|
if (id === 'null') { // 215
|
||
|
return ['NULL', null]; // 216
|
||
|
} else if (id === 'true' || id === 'false') { // 217
|
||
|
return ['BOOLEAN', id === 'true']; // 218
|
||
|
} else { // 219
|
||
|
scanner.pos = startPos; // unconsume `id` // 220
|
||
|
return ['PATH', scanPath()]; // 221
|
||
|
} // 222
|
||
|
} else { // 223
|
||
|
expected('identifier, number, string, boolean, or null'); // 224
|
||
|
} // 225
|
||
|
}; // 226
|
||
|
// 227
|
||
|
var type; // 228
|
||
|
// 229
|
||
|
var error = function (msg) { // 230
|
||
|
scanner.fatal(msg); // 231
|
||
|
}; // 232
|
||
|
// 233
|
||
|
var expected = function (what) { // 234
|
||
|
error('Expected ' + what); // 235
|
||
|
}; // 236
|
||
|
// 237
|
||
|
// must do ESCAPE first, immediately followed by ELSE // 238
|
||
|
// order of others doesn't matter // 239
|
||
|
if (run(starts.ESCAPE)) type = 'ESCAPE'; // 240
|
||
|
else if (run(starts.ELSE)) type = 'ELSE'; // 241
|
||
|
else if (run(starts.DOUBLE)) type = 'DOUBLE'; // 242
|
||
|
else if (run(starts.TRIPLE)) type = 'TRIPLE'; // 243
|
||
|
else if (run(starts.BLOCKCOMMENT)) type = 'BLOCKCOMMENT'; // 244
|
||
|
else if (run(starts.COMMENT)) type = 'COMMENT'; // 245
|
||
|
else if (run(starts.INCLUSION)) type = 'INCLUSION'; // 246
|
||
|
else if (run(starts.BLOCKOPEN)) type = 'BLOCKOPEN'; // 247
|
||
|
else if (run(starts.BLOCKCLOSE)) type = 'BLOCKCLOSE'; // 248
|
||
|
else // 249
|
||
|
error('Unknown stache tag'); // 250
|
||
|
// 251
|
||
|
var tag = new TemplateTag; // 252
|
||
|
tag.type = type; // 253
|
||
|
// 254
|
||
|
if (type === 'BLOCKCOMMENT') { // 255
|
||
|
var result = run(/^[\s\S]*?--\s*?\}\}/); // 256
|
||
|
if (! result) // 257
|
||
|
error("Unclosed block comment"); // 258
|
||
|
tag.value = result.slice(0, result.lastIndexOf('--')); // 259
|
||
|
} else if (type === 'COMMENT') { // 260
|
||
|
var result = run(/^[\s\S]*?\}\}/); // 261
|
||
|
if (! result) // 262
|
||
|
error("Unclosed comment"); // 263
|
||
|
tag.value = result.slice(0, -2); // 264
|
||
|
} else if (type === 'BLOCKCLOSE') { // 265
|
||
|
tag.path = scanPath(); // 266
|
||
|
if (! run(ends.DOUBLE)) // 267
|
||
|
expected('`}}`'); // 268
|
||
|
} else if (type === 'ELSE') { // 269
|
||
|
if (! run(ends.DOUBLE)) // 270
|
||
|
expected('`}}`'); // 271
|
||
|
} else if (type === 'ESCAPE') { // 272
|
||
|
var result = run(/^\{*\|/); // 273
|
||
|
tag.value = '{{' + result.slice(0, -1); // 274
|
||
|
} else { // 275
|
||
|
// DOUBLE, TRIPLE, BLOCKOPEN, INCLUSION // 276
|
||
|
tag.path = scanPath(); // 277
|
||
|
tag.args = []; // 278
|
||
|
var foundKwArg = false; // 279
|
||
|
while (true) { // 280
|
||
|
run(/^\s*/); // 281
|
||
|
if (type === 'TRIPLE') { // 282
|
||
|
if (run(ends.TRIPLE)) // 283
|
||
|
break; // 284
|
||
|
else if (scanner.peek() === '}') // 285
|
||
|
expected('`}}}`'); // 286
|
||
|
} else { // 287
|
||
|
if (run(ends.DOUBLE)) // 288
|
||
|
break; // 289
|
||
|
else if (scanner.peek() === '}') // 290
|
||
|
expected('`}}`'); // 291
|
||
|
} // 292
|
||
|
var newArg = scanArg(); // 293
|
||
|
if (newArg.length === 3) { // 294
|
||
|
foundKwArg = true; // 295
|
||
|
} else { // 296
|
||
|
if (foundKwArg) // 297
|
||
|
error("Can't have a non-keyword argument after a keyword argument"); // 298
|
||
|
} // 299
|
||
|
tag.args.push(newArg); // 300
|
||
|
// 301
|
||
|
if (run(/^(?=[\s}])/) !== '') // 302
|
||
|
expected('space'); // 303
|
||
|
} // 304
|
||
|
} // 305
|
||
|
// 306
|
||
|
return tag; // 307
|
||
|
}; // 308
|
||
|
// 309
|
||
|
// Returns a SpacebarsCompiler.TemplateTag parsed from `scanner`, leaving scanner // 310
|
||
|
// at its original position. // 311
|
||
|
// // 312
|
||
|
// An error will still be thrown if there is not a valid template tag at // 313
|
||
|
// the current position. // 314
|
||
|
TemplateTag.peek = function (scanner) { // 315
|
||
|
var startPos = scanner.pos; // 316
|
||
|
var result = TemplateTag.parse(scanner); // 317
|
||
|
scanner.pos = startPos; // 318
|
||
|
return result; // 319
|
||
|
}; // 320
|
||
|
// 321
|
||
|
// Like `TemplateTag.parse`, but in the case of blocks, parse the complete // 322
|
||
|
// `{{#foo}}...{{/foo}}` with `content` and possible `elseContent`, rather // 323
|
||
|
// than just the BLOCKOPEN tag. // 324
|
||
|
// // 325
|
||
|
// In addition: // 326
|
||
|
// // 327
|
||
|
// - Throws an error if `{{else}}` or `{{/foo}}` tag is encountered. // 328
|
||
|
// // 329
|
||
|
// - Returns `null` for a COMMENT. (This case is distinguishable from // 330
|
||
|
// parsing no tag by the fact that the scanner is advanced.) // 331
|
||
|
// // 332
|
||
|
// - Takes an HTMLTools.TEMPLATE_TAG_POSITION `position` and sets it as the // 333
|
||
|
// TemplateTag's `.position` property. // 334
|
||
|
// // 335
|
||
|
// - Validates the tag's well-formedness and legality at in its position. // 336
|
||
|
TemplateTag.parseCompleteTag = function (scannerOrString, position) { // 337
|
||
|
var scanner = scannerOrString; // 338
|
||
|
if (typeof scanner === 'string') // 339
|
||
|
scanner = new HTMLTools.Scanner(scannerOrString); // 340
|
||
|
// 341
|
||
|
var startPos = scanner.pos; // for error messages // 342
|
||
|
var result = TemplateTag.parse(scannerOrString); // 343
|
||
|
if (! result) // 344
|
||
|
return result; // 345
|
||
|
// 346
|
||
|
if (result.type === 'BLOCKCOMMENT') // 347
|
||
|
return null; // 348
|
||
|
// 349
|
||
|
if (result.type === 'COMMENT') // 350
|
||
|
return null; // 351
|
||
|
// 352
|
||
|
if (result.type === 'ELSE') // 353
|
||
|
scanner.fatal("Unexpected {{else}}"); // 354
|
||
|
// 355
|
||
|
if (result.type === 'BLOCKCLOSE') // 356
|
||
|
scanner.fatal("Unexpected closing template tag"); // 357
|
||
|
// 358
|
||
|
position = (position || TEMPLATE_TAG_POSITION.ELEMENT); // 359
|
||
|
if (position !== TEMPLATE_TAG_POSITION.ELEMENT) // 360
|
||
|
result.position = position; // 361
|
||
|
// 362
|
||
|
if (result.type === 'BLOCKOPEN') { // 363
|
||
|
// parse block contents // 364
|
||
|
// 365
|
||
|
// Construct a string version of `.path` for comparing start and // 366
|
||
|
// end tags. For example, `foo/[0]` was parsed into `["foo", "0"]` // 367
|
||
|
// and now becomes `foo,0`. This form may also show up in error // 368
|
||
|
// messages. // 369
|
||
|
var blockName = result.path.join(','); // 370
|
||
|
// 371
|
||
|
var textMode = null; // 372
|
||
|
if (blockName === 'markdown' || // 373
|
||
|
position === TEMPLATE_TAG_POSITION.IN_RAWTEXT) { // 374
|
||
|
textMode = HTML.TEXTMODE.STRING; // 375
|
||
|
} else if (position === TEMPLATE_TAG_POSITION.IN_RCDATA || // 376
|
||
|
position === TEMPLATE_TAG_POSITION.IN_ATTRIBUTE) { // 377
|
||
|
textMode = HTML.TEXTMODE.RCDATA; // 378
|
||
|
} // 379
|
||
|
var parserOptions = { // 380
|
||
|
getTemplateTag: TemplateTag.parseCompleteTag, // 381
|
||
|
shouldStop: isAtBlockCloseOrElse, // 382
|
||
|
textMode: textMode // 383
|
||
|
}; // 384
|
||
|
result.content = HTMLTools.parseFragment(scanner, parserOptions); // 385
|
||
|
// 386
|
||
|
if (scanner.rest().slice(0, 2) !== '{{') // 387
|
||
|
scanner.fatal("Expected {{else}} or block close for " + blockName); // 388
|
||
|
// 389
|
||
|
var lastPos = scanner.pos; // save for error messages // 390
|
||
|
var tmplTag = TemplateTag.parse(scanner); // {{else}} or {{/foo}} // 391
|
||
|
// 392
|
||
|
if (tmplTag.type === 'ELSE') { // 393
|
||
|
// parse {{else}} and content up to close tag // 394
|
||
|
result.elseContent = HTMLTools.parseFragment(scanner, parserOptions); // 395
|
||
|
// 396
|
||
|
if (scanner.rest().slice(0, 2) !== '{{') // 397
|
||
|
scanner.fatal("Expected block close for " + blockName); // 398
|
||
|
// 399
|
||
|
lastPos = scanner.pos; // 400
|
||
|
tmplTag = TemplateTag.parse(scanner); // 401
|
||
|
} // 402
|
||
|
// 403
|
||
|
if (tmplTag.type === 'BLOCKCLOSE') { // 404
|
||
|
var blockName2 = tmplTag.path.join(','); // 405
|
||
|
if (blockName !== blockName2) { // 406
|
||
|
scanner.pos = lastPos; // 407
|
||
|
scanner.fatal('Expected tag to close ' + blockName + ', found ' + // 408
|
||
|
blockName2); // 409
|
||
|
} // 410
|
||
|
} else { // 411
|
||
|
scanner.pos = lastPos; // 412
|
||
|
scanner.fatal('Expected tag to close ' + blockName + ', found ' + // 413
|
||
|
tmplTag.type); // 414
|
||
|
} // 415
|
||
|
} // 416
|
||
|
// 417
|
||
|
var finalPos = scanner.pos; // 418
|
||
|
scanner.pos = startPos; // 419
|
||
|
validateTag(result, scanner); // 420
|
||
|
scanner.pos = finalPos; // 421
|
||
|
// 422
|
||
|
return result; // 423
|
||
|
}; // 424
|
||
|
// 425
|
||
|
var isAtBlockCloseOrElse = function (scanner) { // 426
|
||
|
// Detect `{{else}}` or `{{/foo}}`. // 427
|
||
|
// // 428
|
||
|
// We do as much work ourselves before deferring to `TemplateTag.peek`, // 429
|
||
|
// for efficiency (we're called for every input token) and to be // 430
|
||
|
// less obtrusive, because `TemplateTag.peek` will throw an error if it // 431
|
||
|
// sees `{{` followed by a malformed tag. // 432
|
||
|
var rest, type; // 433
|
||
|
return (scanner.peek() === '{' && // 434
|
||
|
(rest = scanner.rest()).slice(0, 2) === '{{' && // 435
|
||
|
/^\{\{\s*(\/|else\b)/.test(rest) && // 436
|
||
|
(type = TemplateTag.peek(scanner).type) && // 437
|
||
|
(type === 'BLOCKCLOSE' || type === 'ELSE')); // 438
|
||
|
}; // 439
|
||
|
// 440
|
||
|
// Validate that `templateTag` is correctly formed and legal for its // 441
|
||
|
// HTML position. Use `scanner` to report errors. On success, does // 442
|
||
|
// nothing. // 443
|
||
|
var validateTag = function (ttag, scanner) { // 444
|
||
|
// 445
|
||
|
if (ttag.type === 'INCLUSION' || ttag.type === 'BLOCKOPEN') { // 446
|
||
|
var args = ttag.args; // 447
|
||
|
if (args.length > 1 && args[0].length === 2 && args[0][0] !== 'PATH') { // 448
|
||
|
// we have a positional argument that is not a PATH followed by // 449
|
||
|
// other arguments // 450
|
||
|
scanner.fatal("First argument must be a function, to be called on the rest of the arguments; found " + args[0][0]);
|
||
|
} // 452
|
||
|
} // 453
|
||
|
// 454
|
||
|
var position = ttag.position || TEMPLATE_TAG_POSITION.ELEMENT; // 455
|
||
|
if (position === TEMPLATE_TAG_POSITION.IN_ATTRIBUTE) { // 456
|
||
|
if (ttag.type === 'DOUBLE' || ttag.type === 'ESCAPE') { // 457
|
||
|
return; // 458
|
||
|
} else if (ttag.type === 'BLOCKOPEN') { // 459
|
||
|
var path = ttag.path; // 460
|
||
|
var path0 = path[0]; // 461
|
||
|
if (! (path.length === 1 && (path0 === 'if' || // 462
|
||
|
path0 === 'unless' || // 463
|
||
|
path0 === 'with' || // 464
|
||
|
path0 === 'each'))) { // 465
|
||
|
scanner.fatal("Custom block helpers are not allowed in an HTML attribute, only built-in ones like #each and #if");
|
||
|
} // 467
|
||
|
} else { // 468
|
||
|
scanner.fatal(ttag.type + " template tag is not allowed in an HTML attribute"); // 469
|
||
|
} // 470
|
||
|
} else if (position === TEMPLATE_TAG_POSITION.IN_START_TAG) { // 471
|
||
|
if (! (ttag.type === 'DOUBLE')) { // 472
|
||
|
scanner.fatal("Reactive HTML attributes must either have a constant name or consist of a single {{helper}} providing a dictionary of names and values. A template tag of type " + ttag.type + " is not allowed here.");
|
||
|
} // 474
|
||
|
if (scanner.peek() === '=') { // 475
|
||
|
scanner.fatal("Template tags are not allowed in attribute names, only in attribute values or in the form of a single {{helper}} that evaluates to a dictionary of name=value pairs.");
|
||
|
} // 477
|
||
|
} // 478
|
||
|
// 479
|
||
|
}; // 480
|
||
|
// 481
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
}).call(this);
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
(function () {
|
||
|
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
// //
|
||
|
// packages/spacebars-compiler/optimizer.js //
|
||
|
// //
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
//
|
||
|
// Optimize parts of an HTMLjs tree into raw HTML strings when they don't // 1
|
||
|
// contain template tags. // 2
|
||
|
// 3
|
||
|
var constant = function (value) { // 4
|
||
|
return function () { return value; }; // 5
|
||
|
}; // 6
|
||
|
// 7
|
||
|
var OPTIMIZABLE = { // 8
|
||
|
NONE: 0, // 9
|
||
|
PARTS: 1, // 10
|
||
|
FULL: 2 // 11
|
||
|
}; // 12
|
||
|
// 13
|
||
|
// We can only turn content into an HTML string if it contains no template // 14
|
||
|
// tags and no "tricky" HTML tags. If we can optimize the entire content // 15
|
||
|
// into a string, we return OPTIMIZABLE.FULL. If the we are given an // 16
|
||
|
// unoptimizable node, we return OPTIMIZABLE.NONE. If we are given a tree // 17
|
||
|
// that contains an unoptimizable node somewhere, we return OPTIMIZABLE.PARTS. // 18
|
||
|
// // 19
|
||
|
// For example, we always create SVG elements programmatically, since SVG // 20
|
||
|
// doesn't have innerHTML. If we are given an SVG element, we return NONE. // 21
|
||
|
// However, if we are given a big tree that contains SVG somewhere, we // 22
|
||
|
// return PARTS so that the optimizer can descend into the tree and optimize // 23
|
||
|
// other parts of it. // 24
|
||
|
var CanOptimizeVisitor = HTML.Visitor.extend(); // 25
|
||
|
CanOptimizeVisitor.def({ // 26
|
||
|
visitNull: constant(OPTIMIZABLE.FULL), // 27
|
||
|
visitPrimitive: constant(OPTIMIZABLE.FULL), // 28
|
||
|
visitComment: constant(OPTIMIZABLE.FULL), // 29
|
||
|
visitCharRef: constant(OPTIMIZABLE.FULL), // 30
|
||
|
visitRaw: constant(OPTIMIZABLE.FULL), // 31
|
||
|
visitObject: constant(OPTIMIZABLE.NONE), // 32
|
||
|
visitFunction: constant(OPTIMIZABLE.NONE), // 33
|
||
|
visitArray: function (x) { // 34
|
||
|
for (var i = 0; i < x.length; i++) // 35
|
||
|
if (this.visit(x[i]) !== OPTIMIZABLE.FULL) // 36
|
||
|
return OPTIMIZABLE.PARTS; // 37
|
||
|
return OPTIMIZABLE.FULL; // 38
|
||
|
}, // 39
|
||
|
visitTag: function (tag) { // 40
|
||
|
var tagName = tag.tagName; // 41
|
||
|
if (tagName === 'textarea') { // 42
|
||
|
// optimizing into a TEXTAREA's RCDATA would require being a little // 43
|
||
|
// more clever. // 44
|
||
|
return OPTIMIZABLE.NONE; // 45
|
||
|
} else if (! (HTML.isKnownElement(tagName) && // 46
|
||
|
! HTML.isKnownSVGElement(tagName))) { // 47
|
||
|
// foreign elements like SVG can't be stringified for innerHTML. // 48
|
||
|
return OPTIMIZABLE.NONE; // 49
|
||
|
} else if (tagName === 'table') { // 50
|
||
|
// Avoid ever producing HTML containing `<table><tr>...`, because the // 51
|
||
|
// browser will insert a TBODY. If we just `createElement("table")` and // 52
|
||
|
// `createElement("tr")`, on the other hand, no TBODY is necessary // 53
|
||
|
// (assuming IE 8+). // 54
|
||
|
return OPTIMIZABLE.NONE; // 55
|
||
|
} // 56
|
||
|
// 57
|
||
|
var children = tag.children; // 58
|
||
|
for (var i = 0; i < children.length; i++) // 59
|
||
|
if (this.visit(children[i]) !== OPTIMIZABLE.FULL) // 60
|
||
|
return OPTIMIZABLE.PARTS; // 61
|
||
|
// 62
|
||
|
if (this.visitAttributes(tag.attrs) !== OPTIMIZABLE.FULL) // 63
|
||
|
return OPTIMIZABLE.PARTS; // 64
|
||
|
// 65
|
||
|
return OPTIMIZABLE.FULL; // 66
|
||
|
}, // 67
|
||
|
visitAttributes: function (attrs) { // 68
|
||
|
if (attrs) { // 69
|
||
|
var isArray = HTML.isArray(attrs); // 70
|
||
|
for (var i = 0; i < (isArray ? attrs.length : 1); i++) { // 71
|
||
|
var a = (isArray ? attrs[i] : attrs); // 72
|
||
|
if ((typeof a !== 'object') || (a instanceof HTMLTools.TemplateTag)) // 73
|
||
|
return OPTIMIZABLE.PARTS; // 74
|
||
|
for (var k in a) // 75
|
||
|
if (this.visit(a[k]) !== OPTIMIZABLE.FULL) // 76
|
||
|
return OPTIMIZABLE.PARTS; // 77
|
||
|
} // 78
|
||
|
} // 79
|
||
|
return OPTIMIZABLE.FULL; // 80
|
||
|
} // 81
|
||
|
}); // 82
|
||
|
// 83
|
||
|
var getOptimizability = function (content) { // 84
|
||
|
return (new CanOptimizeVisitor).visit(content); // 85
|
||
|
}; // 86
|
||
|
// 87
|
||
|
var toRaw = function (x) { // 88
|
||
|
return HTML.Raw(HTML.toHTML(x)); // 89
|
||
|
}; // 90
|
||
|
// 91
|
||
|
var TreeTransformer = HTML.TransformingVisitor.extend(); // 92
|
||
|
TreeTransformer.def({ // 93
|
||
|
visitAttributes: function (attrs/*, ...*/) { // 94
|
||
|
// pass template tags through by default // 95
|
||
|
if (attrs instanceof HTMLTools.TemplateTag) // 96
|
||
|
return attrs; // 97
|
||
|
// 98
|
||
|
return HTML.TransformingVisitor.prototype.visitAttributes.apply( // 99
|
||
|
this, arguments); // 100
|
||
|
} // 101
|
||
|
}); // 102
|
||
|
// 103
|
||
|
// Replace parts of the HTMLjs tree that have no template tags (or // 104
|
||
|
// tricky HTML tags) with HTML.Raw objects containing raw HTML. // 105
|
||
|
var OptimizingVisitor = TreeTransformer.extend(); // 106
|
||
|
OptimizingVisitor.def({ // 107
|
||
|
visitNull: toRaw, // 108
|
||
|
visitPrimitive: toRaw, // 109
|
||
|
visitComment: toRaw, // 110
|
||
|
visitCharRef: toRaw, // 111
|
||
|
visitArray: function (array) { // 112
|
||
|
var optimizability = getOptimizability(array); // 113
|
||
|
if (optimizability === OPTIMIZABLE.FULL) { // 114
|
||
|
return toRaw(array); // 115
|
||
|
} else if (optimizability === OPTIMIZABLE.PARTS) { // 116
|
||
|
return TreeTransformer.prototype.visitArray.call(this, array); // 117
|
||
|
} else { // 118
|
||
|
return array; // 119
|
||
|
} // 120
|
||
|
}, // 121
|
||
|
visitTag: function (tag) { // 122
|
||
|
var optimizability = getOptimizability(tag); // 123
|
||
|
if (optimizability === OPTIMIZABLE.FULL) { // 124
|
||
|
return toRaw(tag); // 125
|
||
|
} else if (optimizability === OPTIMIZABLE.PARTS) { // 126
|
||
|
return TreeTransformer.prototype.visitTag.call(this, tag); // 127
|
||
|
} else { // 128
|
||
|
return tag; // 129
|
||
|
} // 130
|
||
|
}, // 131
|
||
|
visitChildren: function (children) { // 132
|
||
|
// don't optimize the children array into a Raw object! // 133
|
||
|
return TreeTransformer.prototype.visitArray.call(this, children); // 134
|
||
|
}, // 135
|
||
|
visitAttributes: function (attrs) { // 136
|
||
|
return attrs; // 137
|
||
|
} // 138
|
||
|
}); // 139
|
||
|
// 140
|
||
|
// Combine consecutive HTML.Raws. Remove empty ones. // 141
|
||
|
var RawCompactingVisitor = TreeTransformer.extend(); // 142
|
||
|
RawCompactingVisitor.def({ // 143
|
||
|
visitArray: function (array) { // 144
|
||
|
var result = []; // 145
|
||
|
for (var i = 0; i < array.length; i++) { // 146
|
||
|
var item = array[i]; // 147
|
||
|
if ((item instanceof HTML.Raw) && // 148
|
||
|
((! item.value) || // 149
|
||
|
(result.length && // 150
|
||
|
(result[result.length - 1] instanceof HTML.Raw)))) { // 151
|
||
|
// two cases: item is an empty Raw, or previous item is // 152
|
||
|
// a Raw as well. In the latter case, replace the previous // 153
|
||
|
// Raw with a longer one that includes the new Raw. // 154
|
||
|
if (item.value) { // 155
|
||
|
result[result.length - 1] = HTML.Raw( // 156
|
||
|
result[result.length - 1].value + item.value); // 157
|
||
|
} // 158
|
||
|
} else { // 159
|
||
|
result.push(item); // 160
|
||
|
} // 161
|
||
|
} // 162
|
||
|
return result; // 163
|
||
|
} // 164
|
||
|
}); // 165
|
||
|
// 166
|
||
|
// Replace pointless Raws like `HTMl.Raw('foo')` that contain no special // 167
|
||
|
// characters with simple strings. // 168
|
||
|
var RawReplacingVisitor = TreeTransformer.extend(); // 169
|
||
|
RawReplacingVisitor.def({ // 170
|
||
|
visitRaw: function (raw) { // 171
|
||
|
var html = raw.value; // 172
|
||
|
if (html.indexOf('&') < 0 && html.indexOf('<') < 0) { // 173
|
||
|
return html; // 174
|
||
|
} else { // 175
|
||
|
return raw; // 176
|
||
|
} // 177
|
||
|
} // 178
|
||
|
}); // 179
|
||
|
// 180
|
||
|
SpacebarsCompiler.optimize = function (tree) { // 181
|
||
|
tree = (new OptimizingVisitor).visit(tree); // 182
|
||
|
tree = (new RawCompactingVisitor).visit(tree); // 183
|
||
|
tree = (new RawReplacingVisitor).visit(tree); // 184
|
||
|
return tree; // 185
|
||
|
}; // 186
|
||
|
// 187
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
}).call(this);
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
(function () {
|
||
|
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
// //
|
||
|
// packages/spacebars-compiler/codegen.js //
|
||
|
// //
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
//
|
||
|
// ============================================================ // 1
|
||
|
// Code-generation of template tags // 2
|
||
|
// 3
|
||
|
// The `CodeGen` class currently has no instance state, but in theory // 4
|
||
|
// it could be useful to track per-function state, like whether we // 5
|
||
|
// need to emit `var self = this` or not. // 6
|
||
|
var CodeGen = SpacebarsCompiler.CodeGen = function () {}; // 7
|
||
|
// 8
|
||
|
var builtInBlockHelpers = SpacebarsCompiler._builtInBlockHelpers = { // 9
|
||
|
'if': 'Blaze.If', // 10
|
||
|
'unless': 'Blaze.Unless', // 11
|
||
|
'with': 'Spacebars.With', // 12
|
||
|
'each': 'Blaze.Each' // 13
|
||
|
}; // 14
|
||
|
// 15
|
||
|
// 16
|
||
|
// Mapping of "macros" which, when preceded by `Template.`, expand // 17
|
||
|
// to special code rather than following the lookup rules for dotted // 18
|
||
|
// symbols. // 19
|
||
|
var builtInTemplateMacros = { // 20
|
||
|
// `view` is a local variable defined in the generated render // 21
|
||
|
// function for the template in which `Template.contentBlock` or // 22
|
||
|
// `Template.elseBlock` is invoked. // 23
|
||
|
'contentBlock': 'view.templateContentBlock', // 24
|
||
|
'elseBlock': 'view.templateElseBlock', // 25
|
||
|
// 26
|
||
|
// Confusingly, this makes `{{> Template.dynamic}}` an alias // 27
|
||
|
// for `{{> __dynamic}}`, where "__dynamic" is the template that // 28
|
||
|
// implements the dynamic template feature. // 29
|
||
|
'dynamic': 'Template.__dynamic', // 30
|
||
|
// 31
|
||
|
'subscriptionsReady': 'view.templateInstance().subscriptionsReady()' // 32
|
||
|
}; // 33
|
||
|
// 34
|
||
|
// A "reserved name" can't be used as a <template> name. This // 35
|
||
|
// function is used by the template file scanner. // 36
|
||
|
// // 37
|
||
|
// Note that the runtime imposes additional restrictions, for example // 38
|
||
|
// banning the name "body" and names of built-in object properties // 39
|
||
|
// like "toString". // 40
|
||
|
SpacebarsCompiler.isReservedName = function (name) { // 41
|
||
|
return builtInBlockHelpers.hasOwnProperty(name) || // 42
|
||
|
builtInTemplateMacros.hasOwnProperty(name); // 43
|
||
|
}; // 44
|
||
|
// 45
|
||
|
var makeObjectLiteral = function (obj) { // 46
|
||
|
var parts = []; // 47
|
||
|
for (var k in obj) // 48
|
||
|
parts.push(BlazeTools.toObjectLiteralKey(k) + ': ' + obj[k]); // 49
|
||
|
return '{' + parts.join(', ') + '}'; // 50
|
||
|
}; // 51
|
||
|
// 52
|
||
|
_.extend(CodeGen.prototype, { // 53
|
||
|
codeGenTemplateTag: function (tag) { // 54
|
||
|
var self = this; // 55
|
||
|
if (tag.position === HTMLTools.TEMPLATE_TAG_POSITION.IN_START_TAG) { // 56
|
||
|
// Special dynamic attributes: `<div {{attrs}}>...` // 57
|
||
|
// only `tag.type === 'DOUBLE'` allowed (by earlier validation) // 58
|
||
|
return BlazeTools.EmitCode('function () { return ' + // 59
|
||
|
self.codeGenMustache(tag.path, tag.args, 'attrMustache') // 60
|
||
|
+ '; }'); // 61
|
||
|
} else { // 62
|
||
|
if (tag.type === 'DOUBLE' || tag.type === 'TRIPLE') { // 63
|
||
|
var code = self.codeGenMustache(tag.path, tag.args); // 64
|
||
|
if (tag.type === 'TRIPLE') { // 65
|
||
|
code = 'Spacebars.makeRaw(' + code + ')'; // 66
|
||
|
} // 67
|
||
|
if (tag.position !== HTMLTools.TEMPLATE_TAG_POSITION.IN_ATTRIBUTE) { // 68
|
||
|
// Reactive attributes are already wrapped in a function, // 69
|
||
|
// and there's no fine-grained reactivity. // 70
|
||
|
// Anywhere else, we need to create a View. // 71
|
||
|
code = 'Blaze.View("lookup:' + tag.path.join('.') + '", ' + // 72
|
||
|
'function () { return ' + code + '; })'; // 73
|
||
|
} // 74
|
||
|
return BlazeTools.EmitCode(code); // 75
|
||
|
} else if (tag.type === 'INCLUSION' || tag.type === 'BLOCKOPEN') { // 76
|
||
|
var path = tag.path; // 77
|
||
|
// 78
|
||
|
if (tag.type === 'BLOCKOPEN' && // 79
|
||
|
builtInBlockHelpers.hasOwnProperty(path[0])) { // 80
|
||
|
// if, unless, with, each. // 81
|
||
|
// // 82
|
||
|
// If someone tries to do `{{> if}}`, we don't // 83
|
||
|
// get here, but an error is thrown when we try to codegen the path. // 84
|
||
|
// 85
|
||
|
// Note: If we caught these errors earlier, while scanning, we'd be able to // 86
|
||
|
// provide nice line numbers. // 87
|
||
|
if (path.length > 1) // 88
|
||
|
throw new Error("Unexpected dotted path beginning with " + path[0]); // 89
|
||
|
if (! tag.args.length) // 90
|
||
|
throw new Error("#" + path[0] + " requires an argument"); // 91
|
||
|
// 92
|
||
|
// `args` must exist (tag.args.length > 0) // 93
|
||
|
var dataCode = self.codeGenInclusionDataFunc(tag.args) || 'null'; // 94
|
||
|
// `content` must exist // 95
|
||
|
var contentBlock = (('content' in tag) ? // 96
|
||
|
self.codeGenBlock(tag.content) : null); // 97
|
||
|
// `elseContent` may not exist // 98
|
||
|
var elseContentBlock = (('elseContent' in tag) ? // 99
|
||
|
self.codeGenBlock(tag.elseContent) : null); // 100
|
||
|
// 101
|
||
|
var callArgs = [dataCode, contentBlock]; // 102
|
||
|
if (elseContentBlock) // 103
|
||
|
callArgs.push(elseContentBlock); // 104
|
||
|
// 105
|
||
|
return BlazeTools.EmitCode( // 106
|
||
|
builtInBlockHelpers[path[0]] + '(' + callArgs.join(', ') + ')'); // 107
|
||
|
// 108
|
||
|
} else { // 109
|
||
|
var compCode = self.codeGenPath(path, {lookupTemplate: true}); // 110
|
||
|
if (path.length > 1) { // 111
|
||
|
// capture reactivity // 112
|
||
|
compCode = 'function () { return Spacebars.call(' + compCode + // 113
|
||
|
'); }'; // 114
|
||
|
} // 115
|
||
|
// 116
|
||
|
var dataCode = self.codeGenInclusionDataFunc(tag.args); // 117
|
||
|
var content = (('content' in tag) ? // 118
|
||
|
self.codeGenBlock(tag.content) : null); // 119
|
||
|
var elseContent = (('elseContent' in tag) ? // 120
|
||
|
self.codeGenBlock(tag.elseContent) : null); // 121
|
||
|
// 122
|
||
|
var includeArgs = [compCode]; // 123
|
||
|
if (content) { // 124
|
||
|
includeArgs.push(content); // 125
|
||
|
if (elseContent) // 126
|
||
|
includeArgs.push(elseContent); // 127
|
||
|
} // 128
|
||
|
// 129
|
||
|
var includeCode = // 130
|
||
|
'Spacebars.include(' + includeArgs.join(', ') + ')'; // 131
|
||
|
// 132
|
||
|
// calling convention compat -- set the data context around the // 133
|
||
|
// entire inclusion, so that if the name of the inclusion is // 134
|
||
|
// a helper function, it gets the data context in `this`. // 135
|
||
|
// This makes for a pretty confusing calling convention -- // 136
|
||
|
// In `{{#foo bar}}`, `foo` is evaluated in the context of `bar` // 137
|
||
|
// -- but it's what we shipped for 0.8.0. The rationale is that // 138
|
||
|
// `{{#foo bar}}` is sugar for `{{#with bar}}{{#foo}}...`. // 139
|
||
|
if (dataCode) { // 140
|
||
|
includeCode = // 141
|
||
|
'Blaze._TemplateWith(' + dataCode + ', function () { return ' + // 142
|
||
|
includeCode + '; })'; // 143
|
||
|
} // 144
|
||
|
// 145
|
||
|
// XXX BACK COMPAT - UI is the old name, Template is the new // 146
|
||
|
if ((path[0] === 'UI' || path[0] === 'Template') && // 147
|
||
|
(path[1] === 'contentBlock' || path[1] === 'elseBlock')) { // 148
|
||
|
// Call contentBlock and elseBlock in the appropriate scope // 149
|
||
|
includeCode = 'Blaze._InOuterTemplateScope(view, function () { return ' // 150
|
||
|
+ includeCode + '; })'; // 151
|
||
|
} // 152
|
||
|
// 153
|
||
|
return BlazeTools.EmitCode(includeCode); // 154
|
||
|
} // 155
|
||
|
} else if (tag.type === 'ESCAPE') { // 156
|
||
|
return tag.value; // 157
|
||
|
} else { // 158
|
||
|
// Can't get here; TemplateTag validation should catch any // 159
|
||
|
// inappropriate tag types that might come out of the parser. // 160
|
||
|
throw new Error("Unexpected template tag type: " + tag.type); // 161
|
||
|
} // 162
|
||
|
} // 163
|
||
|
}, // 164
|
||
|
// 165
|
||
|
// `path` is an array of at least one string. // 166
|
||
|
// // 167
|
||
|
// If `path.length > 1`, the generated code may be reactive // 168
|
||
|
// (i.e. it may invalidate the current computation). // 169
|
||
|
// // 170
|
||
|
// No code is generated to call the result if it's a function. // 171
|
||
|
// // 172
|
||
|
// Options: // 173
|
||
|
// // 174
|
||
|
// - lookupTemplate {Boolean} If true, generated code also looks in // 175
|
||
|
// the list of templates. (After helpers, before data context). // 176
|
||
|
// Used when generating code for `{{> foo}}` or `{{#foo}}`. Only // 177
|
||
|
// used for non-dotted paths. // 178
|
||
|
codeGenPath: function (path, opts) { // 179
|
||
|
if (builtInBlockHelpers.hasOwnProperty(path[0])) // 180
|
||
|
throw new Error("Can't use the built-in '" + path[0] + "' here"); // 181
|
||
|
// Let `{{#if Template.contentBlock}}` check whether this template was // 182
|
||
|
// invoked via inclusion or as a block helper, in addition to supporting // 183
|
||
|
// `{{> Template.contentBlock}}`. // 184
|
||
|
// XXX BACK COMPAT - UI is the old name, Template is the new // 185
|
||
|
if (path.length >= 2 && // 186
|
||
|
(path[0] === 'UI' || path[0] === 'Template') // 187
|
||
|
&& builtInTemplateMacros.hasOwnProperty(path[1])) { // 188
|
||
|
if (path.length > 2) // 189
|
||
|
throw new Error("Unexpected dotted path beginning with " + // 190
|
||
|
path[0] + '.' + path[1]); // 191
|
||
|
return builtInTemplateMacros[path[1]]; // 192
|
||
|
} // 193
|
||
|
// 194
|
||
|
var firstPathItem = BlazeTools.toJSLiteral(path[0]); // 195
|
||
|
var lookupMethod = 'lookup'; // 196
|
||
|
if (opts && opts.lookupTemplate && path.length === 1) // 197
|
||
|
lookupMethod = 'lookupTemplate'; // 198
|
||
|
var code = 'view.' + lookupMethod + '(' + firstPathItem + ')'; // 199
|
||
|
// 200
|
||
|
if (path.length > 1) { // 201
|
||
|
code = 'Spacebars.dot(' + code + ', ' + // 202
|
||
|
_.map(path.slice(1), BlazeTools.toJSLiteral).join(', ') + ')'; // 203
|
||
|
} // 204
|
||
|
// 205
|
||
|
return code; // 206
|
||
|
}, // 207
|
||
|
// 208
|
||
|
// Generates code for an `[argType, argValue]` argument spec, // 209
|
||
|
// ignoring the third element (keyword argument name) if present. // 210
|
||
|
// // 211
|
||
|
// The resulting code may be reactive (in the case of a PATH of // 212
|
||
|
// more than one element) and is not wrapped in a closure. // 213
|
||
|
codeGenArgValue: function (arg) { // 214
|
||
|
var self = this; // 215
|
||
|
// 216
|
||
|
var argType = arg[0]; // 217
|
||
|
var argValue = arg[1]; // 218
|
||
|
// 219
|
||
|
var argCode; // 220
|
||
|
switch (argType) { // 221
|
||
|
case 'STRING': // 222
|
||
|
case 'NUMBER': // 223
|
||
|
case 'BOOLEAN': // 224
|
||
|
case 'NULL': // 225
|
||
|
argCode = BlazeTools.toJSLiteral(argValue); // 226
|
||
|
break; // 227
|
||
|
case 'PATH': // 228
|
||
|
argCode = self.codeGenPath(argValue); // 229
|
||
|
break; // 230
|
||
|
default: // 231
|
||
|
// can't get here // 232
|
||
|
throw new Error("Unexpected arg type: " + argType); // 233
|
||
|
} // 234
|
||
|
// 235
|
||
|
return argCode; // 236
|
||
|
}, // 237
|
||
|
// 238
|
||
|
// Generates a call to `Spacebars.fooMustache` on evaluated arguments. // 239
|
||
|
// The resulting code has no function literals and must be wrapped in // 240
|
||
|
// one for fine-grained reactivity. // 241
|
||
|
codeGenMustache: function (path, args, mustacheType) { // 242
|
||
|
var self = this; // 243
|
||
|
// 244
|
||
|
var nameCode = self.codeGenPath(path); // 245
|
||
|
var argCode = self.codeGenMustacheArgs(args); // 246
|
||
|
var mustache = (mustacheType || 'mustache'); // 247
|
||
|
// 248
|
||
|
return 'Spacebars.' + mustache + '(' + nameCode + // 249
|
||
|
(argCode ? ', ' + argCode.join(', ') : '') + ')'; // 250
|
||
|
}, // 251
|
||
|
// 252
|
||
|
// returns: array of source strings, or null if no // 253
|
||
|
// args at all. // 254
|
||
|
codeGenMustacheArgs: function (tagArgs) { // 255
|
||
|
var self = this; // 256
|
||
|
// 257
|
||
|
var kwArgs = null; // source -> source // 258
|
||
|
var args = null; // [source] // 259
|
||
|
// 260
|
||
|
// tagArgs may be null // 261
|
||
|
_.each(tagArgs, function (arg) { // 262
|
||
|
var argCode = self.codeGenArgValue(arg); // 263
|
||
|
// 264
|
||
|
if (arg.length > 2) { // 265
|
||
|
// keyword argument (represented as [type, value, name]) // 266
|
||
|
kwArgs = (kwArgs || {}); // 267
|
||
|
kwArgs[arg[2]] = argCode; // 268
|
||
|
} else { // 269
|
||
|
// positional argument // 270
|
||
|
args = (args || []); // 271
|
||
|
args.push(argCode); // 272
|
||
|
} // 273
|
||
|
}); // 274
|
||
|
// 275
|
||
|
// put kwArgs in options dictionary at end of args // 276
|
||
|
if (kwArgs) { // 277
|
||
|
args = (args || []); // 278
|
||
|
args.push('Spacebars.kw(' + makeObjectLiteral(kwArgs) + ')'); // 279
|
||
|
} // 280
|
||
|
// 281
|
||
|
return args; // 282
|
||
|
}, // 283
|
||
|
// 284
|
||
|
codeGenBlock: function (content) { // 285
|
||
|
return SpacebarsCompiler.codeGen(content); // 286
|
||
|
}, // 287
|
||
|
// 288
|
||
|
codeGenInclusionDataFunc: function (args) { // 289
|
||
|
var self = this; // 290
|
||
|
// 291
|
||
|
var dataFuncCode = null; // 292
|
||
|
// 293
|
||
|
if (! args.length) { // 294
|
||
|
// e.g. `{{#foo}}` // 295
|
||
|
return null; // 296
|
||
|
} else if (args[0].length === 3) { // 297
|
||
|
// keyword arguments only, e.g. `{{> point x=1 y=2}}` // 298
|
||
|
var dataProps = {}; // 299
|
||
|
_.each(args, function (arg) { // 300
|
||
|
var argKey = arg[2]; // 301
|
||
|
dataProps[argKey] = 'Spacebars.call(' + self.codeGenArgValue(arg) + ')'; // 302
|
||
|
}); // 303
|
||
|
dataFuncCode = makeObjectLiteral(dataProps); // 304
|
||
|
} else if (args[0][0] !== 'PATH') { // 305
|
||
|
// literal first argument, e.g. `{{> foo "blah"}}` // 306
|
||
|
// // 307
|
||
|
// tag validation has confirmed, in this case, that there is only // 308
|
||
|
// one argument (`args.length === 1`) // 309
|
||
|
dataFuncCode = self.codeGenArgValue(args[0]); // 310
|
||
|
} else if (args.length === 1) { // 311
|
||
|
// one argument, must be a PATH // 312
|
||
|
dataFuncCode = 'Spacebars.call(' + self.codeGenPath(args[0][1]) + ')'; // 313
|
||
|
} else { // 314
|
||
|
// Multiple positional arguments; treat them as a nested // 315
|
||
|
// "data mustache" // 316
|
||
|
dataFuncCode = self.codeGenMustache(args[0][1], args.slice(1), // 317
|
||
|
'dataMustache'); // 318
|
||
|
} // 319
|
||
|
// 320
|
||
|
return 'function () { return ' + dataFuncCode + '; }'; // 321
|
||
|
} // 322
|
||
|
// 323
|
||
|
}); // 324
|
||
|
// 325
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
}).call(this);
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
(function () {
|
||
|
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
// //
|
||
|
// packages/spacebars-compiler/compiler.js //
|
||
|
// //
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
//
|
||
|
// 1
|
||
|
SpacebarsCompiler.parse = function (input) { // 2
|
||
|
// 3
|
||
|
var tree = HTMLTools.parseFragment( // 4
|
||
|
input, // 5
|
||
|
{ getTemplateTag: TemplateTag.parseCompleteTag }); // 6
|
||
|
// 7
|
||
|
return tree; // 8
|
||
|
}; // 9
|
||
|
// 10
|
||
|
SpacebarsCompiler.compile = function (input, options) { // 11
|
||
|
var tree = SpacebarsCompiler.parse(input); // 12
|
||
|
return SpacebarsCompiler.codeGen(tree, options); // 13
|
||
|
}; // 14
|
||
|
// 15
|
||
|
SpacebarsCompiler._TemplateTagReplacer = HTML.TransformingVisitor.extend(); // 16
|
||
|
SpacebarsCompiler._TemplateTagReplacer.def({ // 17
|
||
|
visitObject: function (x) { // 18
|
||
|
if (x instanceof HTMLTools.TemplateTag) { // 19
|
||
|
// 20
|
||
|
// Make sure all TemplateTags in attributes have the right // 21
|
||
|
// `.position` set on them. This is a bit of a hack // 22
|
||
|
// (we shouldn't be mutating that here), but it allows // 23
|
||
|
// cleaner codegen of "synthetic" attributes like TEXTAREA's // 24
|
||
|
// "value", where the template tags were originally not // 25
|
||
|
// in an attribute. // 26
|
||
|
if (this.inAttributeValue) // 27
|
||
|
x.position = HTMLTools.TEMPLATE_TAG_POSITION.IN_ATTRIBUTE; // 28
|
||
|
// 29
|
||
|
return this.codegen.codeGenTemplateTag(x); // 30
|
||
|
} // 31
|
||
|
// 32
|
||
|
return HTML.TransformingVisitor.prototype.visitObject.call(this, x); // 33
|
||
|
}, // 34
|
||
|
visitAttributes: function (attrs) { // 35
|
||
|
if (attrs instanceof HTMLTools.TemplateTag) // 36
|
||
|
return this.codegen.codeGenTemplateTag(attrs); // 37
|
||
|
// 38
|
||
|
// call super (e.g. for case where `attrs` is an array) // 39
|
||
|
return HTML.TransformingVisitor.prototype.visitAttributes.call(this, attrs); // 40
|
||
|
}, // 41
|
||
|
visitAttribute: function (name, value, tag) { // 42
|
||
|
this.inAttributeValue = true; // 43
|
||
|
var result = this.visit(value); // 44
|
||
|
this.inAttributeValue = false; // 45
|
||
|
// 46
|
||
|
if (result !== value) { // 47
|
||
|
// some template tags must have been replaced, because otherwise // 48
|
||
|
// we try to keep things `===` when transforming. Wrap the code // 49
|
||
|
// in a function as per the rules. You can't have // 50
|
||
|
// `{id: Blaze.View(...)}` as an attributes dict because the View // 51
|
||
|
// would be rendered more than once; you need to wrap it in a function // 52
|
||
|
// so that it's a different View each time. // 53
|
||
|
return BlazeTools.EmitCode(this.codegen.codeGenBlock(result)); // 54
|
||
|
} // 55
|
||
|
return result; // 56
|
||
|
} // 57
|
||
|
}); // 58
|
||
|
// 59
|
||
|
SpacebarsCompiler.codeGen = function (parseTree, options) { // 60
|
||
|
// is this a template, rather than a block passed to // 61
|
||
|
// a block helper, say // 62
|
||
|
var isTemplate = (options && options.isTemplate); // 63
|
||
|
var isBody = (options && options.isBody); // 64
|
||
|
// 65
|
||
|
var tree = parseTree; // 66
|
||
|
// 67
|
||
|
// The flags `isTemplate` and `isBody` are kind of a hack. // 68
|
||
|
if (isTemplate || isBody) { // 69
|
||
|
// optimizing fragments would require being smarter about whether we are // 70
|
||
|
// in a TEXTAREA, say. // 71
|
||
|
tree = SpacebarsCompiler.optimize(tree); // 72
|
||
|
} // 73
|
||
|
// 74
|
||
|
var codegen = new SpacebarsCompiler.CodeGen; // 75
|
||
|
tree = (new SpacebarsCompiler._TemplateTagReplacer( // 76
|
||
|
{codegen: codegen})).visit(tree); // 77
|
||
|
// 78
|
||
|
var code = '(function () { '; // 79
|
||
|
if (isTemplate || isBody) { // 80
|
||
|
code += 'var view = this; '; // 81
|
||
|
} // 82
|
||
|
code += 'return '; // 83
|
||
|
code += BlazeTools.toJS(tree); // 84
|
||
|
code += '; })'; // 85
|
||
|
// 86
|
||
|
code = SpacebarsCompiler._beautify(code); // 87
|
||
|
// 88
|
||
|
return code; // 89
|
||
|
}; // 90
|
||
|
// 91
|
||
|
SpacebarsCompiler._beautify = function (code) { // 92
|
||
|
if (Package.minifiers && Package.minifiers.UglifyJSMinify) { // 93
|
||
|
var result = Package.minifiers.UglifyJSMinify( // 94
|
||
|
code, // 95
|
||
|
{ fromString: true, // 96
|
||
|
mangle: false, // 97
|
||
|
compress: false, // 98
|
||
|
output: { beautify: true, // 99
|
||
|
indent_level: 2, // 100
|
||
|
width: 80 } }); // 101
|
||
|
var output = result.code; // 102
|
||
|
// Uglify interprets our expression as a statement and may add a semicolon. // 103
|
||
|
// Strip trailing semicolon. // 104
|
||
|
output = output.replace(/;$/, ''); // 105
|
||
|
return output; // 106
|
||
|
} else { // 107
|
||
|
// don't actually beautify; no UglifyJS // 108
|
||
|
return code; // 109
|
||
|
} // 110
|
||
|
}; // 111
|
||
|
// 112
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
}).call(this);
|
||
|
|
||
|
|
||
|
/* Exports */
|
||
|
if (typeof Package === 'undefined') Package = {};
|
||
|
Package['spacebars-compiler'] = {
|
||
|
SpacebarsCompiler: SpacebarsCompiler
|
||
|
};
|
||
|
|
||
|
})();
|
||
|
|
||
|
//# sourceMappingURL=spacebars-compiler.js.map
|