mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
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:
@@ -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) {
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
240
packages/spacebars-compiler/templatetag.js
Normal file
240
packages/spacebars-compiler/templatetag.js
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user