(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