Start of spacebars compiler reorg

In advance of changing how {{> }} and {{# }} are compiled.

Goals for this reorg:
- "stache tags" are instanceof Spacebars.TemplateTag
- Phase out the word "stache"
- Better separate parsing, template tag expansion, and overall codegen and optimization using multiple files, and additional methods and properties on TemplateTag as appropriate

In particular, the things that happen to a template tag are it is parsed; validated (i.e. errors are thrown); and expanded (into code).  Validation and expansion can be methods on TemplateTag, as long as they also know the TEMPLATE_TAG_POSITION where the tag occurred.  They can be done either while parsing or while walking the AST for code generation, but if validation is done during parsing then it's easy to throw good error messages.  If expansion is done during code generation, then it's easier to inspect the parse tree for testing and debugging.

Also:
- Get rid of the word "Special" in html-tools (HTML.Special, getSpecialTag) in favor of TemplateTag.
This commit is contained in:
David Greenspan
2014-01-15 10:17:41 -08:00
parent 3a99ea672b
commit cf9bc604b8
4 changed files with 261 additions and 251 deletions

View File

@@ -15,7 +15,8 @@ Package.on_use(function (api) {
api.use('underscore');
api.use('ui');
api.use('minifiers', ['server']);
api.add_files(['tokens.js', 'tojs.js', 'spacebars-compiler.js']);
api.add_files(['tokens.js', 'tojs.js', 'templatetag.js',
'spacebars-compiler.js']);
});
Package.on_test(function (api) {

View File

@@ -1,247 +1,6 @@
var makeStacheTagStartRegex = function (r) {
return new RegExp(r.source + /(?![{>!#/])/.source,
r.ignoreCase ? 'i' : '');
};
var starts = {
ELSE: makeStacheTagStartRegex(/^\{\{\s*else(?=[\s}])/i),
DOUBLE: makeStacheTagStartRegex(/^\{\{\s*(?!\s)/),
TRIPLE: makeStacheTagStartRegex(/^\{\{\{\s*(?!\s)/),
COMMENT: makeStacheTagStartRegex(/^\{\{\s*!/),
INCLUSION: makeStacheTagStartRegex(/^\{\{\s*>\s*(?!\s)/),
BLOCKOPEN: makeStacheTagStartRegex(/^\{\{\s*#\s*(?!\s)/),
BLOCKCLOSE: makeStacheTagStartRegex(/^\{\{\s*\/\s*(?!\s)/)
};
var ends = {
DOUBLE: /^\s*\}\}/,
TRIPLE: /^\s*\}\}\}/
};
// Parse a tag at `pos` in `inputString`. Succeeds or errors.
Spacebars.parseStacheTag = function (scannerOrString, options) {
var scanner = scannerOrString;
if (typeof scanner === 'string')
scanner = new HTML.Scanner(scannerOrString);
var run = function (regex) {
// regex is assumed to start with `^`
var result = regex.exec(scanner.rest());
if (! result)
return null;
var ret = result[0];
scanner.pos += ret.length;
return ret;
};
var advance = function (amount) {
scanner.pos += amount;
};
var scanIdentifier = function (isFirstInPath) {
var id = parseIdentifierName(scanner);
if (! id)
expected('IDENTIFIER');
if (isFirstInPath &&
(id === 'null' || id === 'true' || id === 'false'))
scanner.fatal("Can't use null, true, or false, as an identifier at start of path");
return id;
};
var scanPath = function () {
var segments = [];
// handle initial `.`, `..`, `./`, `../`, `../..`, `../../`, etc
var dots;
if ((dots = run(/^[\.\/]+/))) {
var ancestorStr = '.'; // eg `../../..` maps to `....`
var endsWithSlash = /\/$/.test(dots);
if (endsWithSlash)
dots = dots.slice(0, -1);
_.each(dots.split('/'), function(dotClause, index) {
if (index === 0) {
if (dotClause !== '.' && dotClause !== '..')
expected("`.`, `..`, `./` or `../`");
} else {
if (dotClause !== '..')
expected("`..` or `../`");
}
if (dotClause === '..')
ancestorStr += '.';
});
segments.push(ancestorStr);
if (!endsWithSlash)
return segments;
}
while (true) {
// scan a path segment
if (run(/^\[/)) {
var seg = run(/^[\s\S]*?\]/);
if (! seg)
error("Unterminated path segment");
seg = seg.slice(0, -1);
if (! seg && ! segments.length)
error("Path can't start with empty string");
segments.push(seg);
} else {
var id = scanIdentifier(! segments.length);
if (id === 'this' && ! segments.length) {
// initial `this`
segments.push('.');
} else {
segments.push(id);
}
}
var sep = run(/^[\.\/]/);
if (! sep)
break;
}
return segments;
};
// scan an argument; succeeds or errors
var scanArg = function (notKeyword) {
var startPos = scanner.pos;
var result;
if ((result = parseNumber(scanner))) {
return ['NUMBER', result.value];
} else if ((result = parseStringLiteral(scanner))) {
return ['STRING', result.value];
} else if (/^[\.\[]/.test(scanner.peek())) {
return ['PATH', scanPath()];
} else if ((result = parseIdentifierName(scanner))) {
var id = result;
if (id === 'null') {
return ['NULL', null];
} else if (id === 'true' || id === 'false') {
return ['BOOLEAN', id === 'true'];
} else {
if ((! notKeyword) &&
/^\s*=/.test(scanner.rest())) {
// it's a keyword argument!
run(/^\s*=\s*/);
// recurse to scan value, disallowing a second `=`.
var arg = scanArg(true);
arg.push(id); // add third element for key
return arg;
} else {
scanner.pos = startPos; // unconsume `id`
return ['PATH', scanPath()];
}
}
} else {
expected('identifier, number, string, boolean, or null');
}
};
var type;
var error = function (msg) {
scanner.fatal(msg);
};
var expected = function (what) {
error('Expected ' + what);
};
// must do ELSE first; order of others doesn't matter
if (run(starts.ELSE)) type = 'ELSE';
else if (run(starts.DOUBLE)) type = 'DOUBLE';
else if (run(starts.TRIPLE)) type = 'TRIPLE';
else if (run(starts.COMMENT)) type = 'COMMENT';
else if (run(starts.INCLUSION)) type = 'INCLUSION';
else if (run(starts.BLOCKOPEN)) type = 'BLOCKOPEN';
else if (run(starts.BLOCKCLOSE)) type = 'BLOCKCLOSE';
else
error('Unknown stache tag');
var tag = { type: type };
if (type === 'COMMENT') {
var result = run(/^[\s\S]*?\}\}/);
if (! result)
error("Unclosed comment");
tag.value = result.slice(0, -2);
} else if (type === 'BLOCKCLOSE') {
tag.path = scanPath();
if (! run(ends.DOUBLE))
expected('`}}`');
} else if (type === 'ELSE') {
if (! run(ends.DOUBLE))
expected('`}}`');
} else {
// DOUBLE, TRIPLE, BLOCKOPEN, INCLUSION
tag.path = scanPath();
tag.args = [];
while (true) {
run(/^\s*/);
if (type === 'TRIPLE') {
if (run(ends.TRIPLE))
break;
else if (scanner.peek() === '}')
expected('`}}}`');
} else {
if (run(ends.DOUBLE))
break;
else if (scanner.peek() === '}')
expected('`}}`');
}
tag.args.push(scanArg());
if (run(/^(?=[\s}])/) !== '')
expected('space');
}
}
var checkTag = function (tag) {
if (tag.type === 'INCLUSION') {
// throw error on >1 positional arguments
var numPosArgs = 0;
var args = tag.args;
for (var i = 0; i < args.length; i++)
if (args[i].length === 2)
numPosArgs++;
if (numPosArgs > 1)
error("Only one positional argument is allowed in {{> ... }}");
}
};
checkTag(tag);
return tag;
};
Spacebars.peekStacheTag = function (scanner, options) {
var startPos = scanner.pos;
var result = Spacebars.parseStacheTag(scanner, options);
scanner.pos = startPos;
return result;
};
var makeObjectLiteral = function (obj) {
var parts = [];
for (var k in obj)
parts.push(toObjectLiteralKey(k) + ': ' + obj[k]);
return '{' + parts.join(', ') + '}';
};
//////////////////////////////////////////////////
Spacebars.parse = function (input) {
// This implementation of `getSpecialTag` looks for "{{" and if it
// finds it, it will parse a stache tag or fail fatally trying.
@@ -254,9 +13,9 @@ Spacebars.parse = function (input) {
scanner.rest().slice(0, 2) === '{{'))
return null;
// `parseStacheTag` will succeed or die trying.
// `TemplateTag.parse` will succeed or die trying.
var lastPos = scanner.pos;
var stache = Spacebars.parseStacheTag(scanner);
var stache = TemplateTag.parse(scanner);
// kill any `args: []` cluttering up the object
if (stache.args && ! stache.args.length)
delete stache.args;
@@ -316,7 +75,7 @@ Spacebars.parse = function (input) {
scanner.fatal("Expected {{else}} or block close for " + blockName);
lastPos = scanner.pos;
var stache2 = Spacebars.parseStacheTag(scanner);
var stache2 = TemplateTag.parse(scanner);
if (stache2.type === 'ELSE') {
stache.elseContent = HTML.parseFragment(scanner, parserOptions);
@@ -325,7 +84,7 @@ Spacebars.parse = function (input) {
scanner.fatal("Expected block close for " + blockName);
lastPos = scanner.pos;
stache2 = Spacebars.parseStacheTag(scanner);
stache2 = TemplateTag.parse(scanner);
}
if (stache2.type === 'BLOCKCLOSE') {
@@ -346,13 +105,15 @@ Spacebars.parse = function (input) {
};
var isAtBlockCloseOrElse = function (scanner) {
// we could just call parseStacheTag, but this function is called
// for every token in the input stream, so we add some shortcuts.
// Because this function may be called for every token in the input
// stream, we try to make it reasonably efficient in the false case.
// We also have to screen for `{{` before calling TemplateTag.peek
// to avoid throwing an error.
var rest, type;
return (scanner.peek() === '{' &&
(rest = scanner.rest()).slice(0, 2) === '{{' &&
/^\{\{\s*(\/|else\b)/.test(rest) &&
(type = Spacebars.peekStacheTag(scanner).type) &&
(type = TemplateTag.peek(scanner).type) &&
(type === 'BLOCKCLOSE' || type === 'ELSE'));
};
@@ -560,6 +321,14 @@ var replaceSpecials = function (node) {
}
};
var makeObjectLiteral = function (obj) {
var parts = [];
for (var k in obj)
parts.push(toObjectLiteralKey(k) + ': ' + obj[k]);
return '{' + parts.join(', ') + '}';
};
var codeGenInclusionArgs = function (tag) {
var args = null;
var posArgs = [];

View File

@@ -6,7 +6,7 @@ Tinytest.add("spacebars - stache tags", function (test) {
var msg = '';
test.throws(function () {
try {
Spacebars.parseStacheTag(input);
Spacebars.TemplateTag.parse(input);
} catch (e) {
msg = e.message;
throw e;
@@ -14,7 +14,7 @@ Tinytest.add("spacebars - stache tags", function (test) {
});
test.equal(msg.slice(0, expected.length), expected);
} else {
var result = Spacebars.parseStacheTag(input);
var result = Spacebars.TemplateTag.parse(input);
test.equal(result, expected);
}
};

View File

@@ -0,0 +1,240 @@
TemplateTag = Spacebars.TemplateTag = function () {};
var makeStacheTagStartRegex = function (r) {
return new RegExp(r.source + /(?![{>!#/])/.source,
r.ignoreCase ? 'i' : '');
};
var starts = {
ELSE: makeStacheTagStartRegex(/^\{\{\s*else(?=[\s}])/i),
DOUBLE: makeStacheTagStartRegex(/^\{\{\s*(?!\s)/),
TRIPLE: makeStacheTagStartRegex(/^\{\{\{\s*(?!\s)/),
COMMENT: makeStacheTagStartRegex(/^\{\{\s*!/),
INCLUSION: makeStacheTagStartRegex(/^\{\{\s*>\s*(?!\s)/),
BLOCKOPEN: makeStacheTagStartRegex(/^\{\{\s*#\s*(?!\s)/),
BLOCKCLOSE: makeStacheTagStartRegex(/^\{\{\s*\/\s*(?!\s)/)
};
var ends = {
DOUBLE: /^\s*\}\}/,
TRIPLE: /^\s*\}\}\}/
};
// Parse a tag from the provided scanner or string. Either succeeds
// and returns a Spacebars.TemplateTag, or throws an error (using
// `scanner.fatal` if a scanner is provided).
TemplateTag.parse = function (scannerOrString) {
var scanner = scannerOrString;
if (typeof scanner === 'string')
scanner = new HTML.Scanner(scannerOrString);
var run = function (regex) {
// regex is assumed to start with `^`
var result = regex.exec(scanner.rest());
if (! result)
return null;
var ret = result[0];
scanner.pos += ret.length;
return ret;
};
var advance = function (amount) {
scanner.pos += amount;
};
var scanIdentifier = function (isFirstInPath) {
var id = parseIdentifierName(scanner);
if (! id)
expected('IDENTIFIER');
if (isFirstInPath &&
(id === 'null' || id === 'true' || id === 'false'))
scanner.fatal("Can't use null, true, or false, as an identifier at start of path");
return id;
};
var scanPath = function () {
var segments = [];
// handle initial `.`, `..`, `./`, `../`, `../..`, `../../`, etc
var dots;
if ((dots = run(/^[\.\/]+/))) {
var ancestorStr = '.'; // eg `../../..` maps to `....`
var endsWithSlash = /\/$/.test(dots);
if (endsWithSlash)
dots = dots.slice(0, -1);
_.each(dots.split('/'), function(dotClause, index) {
if (index === 0) {
if (dotClause !== '.' && dotClause !== '..')
expected("`.`, `..`, `./` or `../`");
} else {
if (dotClause !== '..')
expected("`..` or `../`");
}
if (dotClause === '..')
ancestorStr += '.';
});
segments.push(ancestorStr);
if (!endsWithSlash)
return segments;
}
while (true) {
// scan a path segment
if (run(/^\[/)) {
var seg = run(/^[\s\S]*?\]/);
if (! seg)
error("Unterminated path segment");
seg = seg.slice(0, -1);
if (! seg && ! segments.length)
error("Path can't start with empty string");
segments.push(seg);
} else {
var id = scanIdentifier(! segments.length);
if (id === 'this' && ! segments.length) {
// initial `this`
segments.push('.');
} else {
segments.push(id);
}
}
var sep = run(/^[\.\/]/);
if (! sep)
break;
}
return segments;
};
// scan an argument; succeeds or errors
var scanArg = function (notKeyword) {
var startPos = scanner.pos;
var result;
if ((result = parseNumber(scanner))) {
return ['NUMBER', result.value];
} else if ((result = parseStringLiteral(scanner))) {
return ['STRING', result.value];
} else if (/^[\.\[]/.test(scanner.peek())) {
return ['PATH', scanPath()];
} else if ((result = parseIdentifierName(scanner))) {
var id = result;
if (id === 'null') {
return ['NULL', null];
} else if (id === 'true' || id === 'false') {
return ['BOOLEAN', id === 'true'];
} else {
if ((! notKeyword) &&
/^\s*=/.test(scanner.rest())) {
// it's a keyword argument!
run(/^\s*=\s*/);
// recurse to scan value, disallowing a second `=`.
var arg = scanArg(true);
arg.push(id); // add third element for key
return arg;
} else {
scanner.pos = startPos; // unconsume `id`
return ['PATH', scanPath()];
}
}
} else {
expected('identifier, number, string, boolean, or null');
}
};
var type;
var error = function (msg) {
scanner.fatal(msg);
};
var expected = function (what) {
error('Expected ' + what);
};
// must do ELSE first; order of others doesn't matter
if (run(starts.ELSE)) type = 'ELSE';
else if (run(starts.DOUBLE)) type = 'DOUBLE';
else if (run(starts.TRIPLE)) type = 'TRIPLE';
else if (run(starts.COMMENT)) type = 'COMMENT';
else if (run(starts.INCLUSION)) type = 'INCLUSION';
else if (run(starts.BLOCKOPEN)) type = 'BLOCKOPEN';
else if (run(starts.BLOCKCLOSE)) type = 'BLOCKCLOSE';
else
error('Unknown stache tag');
var tag = new TemplateTag;
tag.type = type;
if (type === 'COMMENT') {
var result = run(/^[\s\S]*?\}\}/);
if (! result)
error("Unclosed comment");
tag.value = result.slice(0, -2);
} else if (type === 'BLOCKCLOSE') {
tag.path = scanPath();
if (! run(ends.DOUBLE))
expected('`}}`');
} else if (type === 'ELSE') {
if (! run(ends.DOUBLE))
expected('`}}`');
} else {
// DOUBLE, TRIPLE, BLOCKOPEN, INCLUSION
tag.path = scanPath();
tag.args = [];
while (true) {
run(/^\s*/);
if (type === 'TRIPLE') {
if (run(ends.TRIPLE))
break;
else if (scanner.peek() === '}')
expected('`}}}`');
} else {
if (run(ends.DOUBLE))
break;
else if (scanner.peek() === '}')
expected('`}}`');
}
tag.args.push(scanArg());
if (run(/^(?=[\s}])/) !== '')
expected('space');
}
}
var checkTag = function (tag) {
if (tag.type === 'INCLUSION') {
// throw error on >1 positional arguments
var numPosArgs = 0;
var args = tag.args;
for (var i = 0; i < args.length; i++)
if (args[i].length === 2)
numPosArgs++;
if (numPosArgs > 1)
error("Only one positional argument is allowed in {{> ... }}");
}
};
checkTag(tag);
return tag;
};
// Returns a Spacebars.TemplateTag parsed from `scanner`, leaving scanner
// at its original position.
//
// An error will still be thrown if there is not a valid template tag at
// the current position.
TemplateTag.peek = function (scanner) {
var startPos = scanner.pos;
var result = TemplateTag.parse(scanner);
scanner.pos = startPos;
return result;
};