[FEAT] Extract the Handlebars parser (#1713)

Extracts the parser to `@handlebars/parser`, where it can be shared
between different implementations. This means that e.g. Glimmer/Ember
will be able to iterate on new features without forcing Handlebars to
adopt them immediately, and vice versa. All implementors will be able to
absorb changes as it makes sense for them.
This commit is contained in:
Chris Garrett
2021-02-14 23:56:25 -08:00
committed by GitHub
parent 071003b89c
commit 19bdace85a
34 changed files with 58 additions and 2268 deletions
-1
View File
@@ -12,7 +12,6 @@ node_modules
.nyc_output
# Generated files
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/integration-testing/*/dist/
-1
View File
@@ -13,7 +13,6 @@ node_modules
.nyc_output
# Generated files
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/integration-testing/*/dist/
-1
View File
@@ -12,7 +12,6 @@ node_modules
.nyc_output
# Generated files
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/integration-testing/*/dist/
+2 -7
View File
@@ -3,12 +3,7 @@ module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
clean: [
'tmp',
'dist',
'lib/handlebars/compiler/parser.js',
'integration-testing/**/node_modules'
],
clean: ['tmp', 'dist', 'integration-testing/**/node_modules'],
copy: {
dist: {
@@ -198,7 +193,7 @@ module.exports = function(grunt) {
this.registerTask(
'build',
'Builds a distributable version of the current project',
['parser', 'node', 'globals']
['node', 'globals']
);
this.registerTask('node', ['babel:cjs']);
+7 -6
View File
@@ -1,15 +1,16 @@
import {
parser as Parser,
parse,
parseWithoutProcessing,
Visitor
} from '@handlebars/parser';
import runtime from './handlebars.runtime';
// Compiler imports
import AST from './handlebars/compiler/ast';
import {
parser as Parser,
parse,
parseWithoutProcessing
} from './handlebars/compiler/base';
import { Compiler, compile, precompile } from './handlebars/compiler/compiler';
import JavaScriptCompiler from './handlebars/compiler/javascript-compiler';
import Visitor from './handlebars/compiler/visitor';
import noConflict from './handlebars/no-conflict';
+1 -1
View File
@@ -1,9 +1,9 @@
import { Exception } from '@handlebars/parser';
import * as base from './handlebars/base';
// Each of these augment the Handlebars object. No need to setup here.
// (This is done to easily share code between commonjs and browse envs)
import SafeString from './handlebars/safe-string';
import Exception from './handlebars/exception';
import * as Utils from './handlebars/utils';
import * as runtime from './handlebars/runtime';
+1 -1
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import { createFrame, extend, toString } from './utils';
import Exception from './exception';
import { registerDefaultHelpers } from './helpers';
import { registerDefaultDecorators } from './decorators';
import logger from './logger';
-34
View File
@@ -1,34 +0,0 @@
import parser from './parser';
import WhitespaceControl from './whitespace-control';
import * as Helpers from './helpers';
import { extend } from '../utils';
export { parser };
let yy = {};
extend(yy, Helpers);
export function parseWithoutProcessing(input, options) {
// Just return if an already-compiled AST was passed in.
if (input.type === 'Program') {
return input;
}
parser.yy = yy;
// Altering the shared object here, but this is ok as parser is a sync operation
yy.locInfo = function(locInfo) {
return new yy.SourceLocation(options && options.srcName, locInfo);
};
let ast = parser.parse(input);
return ast;
}
export function parse(input, options) {
let ast = parseWithoutProcessing(input, options);
let strip = new WhitespaceControl(options);
return strip.accept(ast);
}
+1 -1
View File
@@ -1,6 +1,6 @@
/* eslint-disable new-cap */
import Exception from '../exception';
import { Exception } from '@handlebars/parser';
import { isArray, indexOf, extend } from '../utils';
import AST from './ast';
-219
View File
@@ -1,219 +0,0 @@
import Exception from '../exception';
function validateClose(open, close) {
close = close.path ? close.path.original : close;
if (open.path.original !== close) {
let errorNode = { loc: open.path.loc };
throw new Exception(
open.path.original + " doesn't match " + close,
errorNode
);
}
}
export function SourceLocation(source, locInfo) {
this.source = source;
this.start = {
line: locInfo.first_line,
column: locInfo.first_column
};
this.end = {
line: locInfo.last_line,
column: locInfo.last_column
};
}
export function id(token) {
if (/^\[.*\]$/.test(token)) {
return token.substring(1, token.length - 1);
} else {
return token;
}
}
export function stripFlags(open, close) {
return {
open: open.charAt(2) === '~',
close: close.charAt(close.length - 3) === '~'
};
}
export function stripComment(comment) {
return comment.replace(/^\{\{~?!-?-?/, '').replace(/-?-?~?\}\}$/, '');
}
export function preparePath(data, parts, loc) {
loc = this.locInfo(loc);
let original = data ? '@' : '',
dig = [],
depth = 0;
for (let i = 0, l = parts.length; i < l; i++) {
let part = parts[i].part,
// If we have [] syntax then we do not treat path references as operators,
// i.e. foo.[this] resolves to approximately context.foo['this']
isLiteral = parts[i].original !== part;
original += (parts[i].separator || '') + part;
if (!isLiteral && (part === '..' || part === '.' || part === 'this')) {
if (dig.length > 0) {
throw new Exception('Invalid path: ' + original, { loc });
} else if (part === '..') {
depth++;
}
} else {
dig.push(part);
}
}
return {
type: 'PathExpression',
data,
depth,
parts: dig,
original,
loc
};
}
export function prepareMustache(path, params, hash, open, strip, locInfo) {
// Must use charAt to support IE pre-10
let escapeFlag = open.charAt(3) || open.charAt(2),
escaped = escapeFlag !== '{' && escapeFlag !== '&';
let decorator = /\*/.test(open);
return {
type: decorator ? 'Decorator' : 'MustacheStatement',
path,
params,
hash,
escaped,
strip,
loc: this.locInfo(locInfo)
};
}
export function prepareRawBlock(openRawBlock, contents, close, locInfo) {
validateClose(openRawBlock, close);
locInfo = this.locInfo(locInfo);
let program = {
type: 'Program',
body: contents,
strip: {},
loc: locInfo
};
return {
type: 'BlockStatement',
path: openRawBlock.path,
params: openRawBlock.params,
hash: openRawBlock.hash,
program,
openStrip: {},
inverseStrip: {},
closeStrip: {},
loc: locInfo
};
}
export function prepareBlock(
openBlock,
program,
inverseAndProgram,
close,
inverted,
locInfo
) {
if (close && close.path) {
validateClose(openBlock, close);
}
let decorator = /\*/.test(openBlock.open);
program.blockParams = openBlock.blockParams;
let inverse, inverseStrip;
if (inverseAndProgram) {
if (decorator) {
throw new Exception(
'Unexpected inverse block on decorator',
inverseAndProgram
);
}
if (inverseAndProgram.chain) {
inverseAndProgram.program.body[0].closeStrip = close.strip;
}
inverseStrip = inverseAndProgram.strip;
inverse = inverseAndProgram.program;
}
if (inverted) {
inverted = inverse;
inverse = program;
program = inverted;
}
return {
type: decorator ? 'DecoratorBlock' : 'BlockStatement',
path: openBlock.path,
params: openBlock.params,
hash: openBlock.hash,
program,
inverse,
openStrip: openBlock.strip,
inverseStrip,
closeStrip: close && close.strip,
loc: this.locInfo(locInfo)
};
}
export function prepareProgram(statements, loc) {
if (!loc && statements.length) {
const firstLoc = statements[0].loc,
lastLoc = statements[statements.length - 1].loc;
/* istanbul ignore else */
if (firstLoc && lastLoc) {
loc = {
source: firstLoc.source,
start: {
line: firstLoc.start.line,
column: firstLoc.start.column
},
end: {
line: lastLoc.end.line,
column: lastLoc.end.column
}
};
}
}
return {
type: 'Program',
body: statements,
strip: {},
loc: loc
};
}
export function preparePartialBlock(open, program, close, locInfo) {
validateClose(open, close);
return {
type: 'PartialBlockStatement',
name: open.path,
params: open.params,
hash: open.hash,
program,
openStrip: open.strip,
closeStrip: close && close.strip,
loc: this.locInfo(locInfo)
};
}
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import { COMPILER_REVISION, REVISION_CHANGES } from '../base';
import Exception from '../exception';
import { isArray } from '../utils';
import CodeGen from './code-gen';
-178
View File
@@ -1,178 +0,0 @@
/* eslint-disable new-cap */
import Visitor from './visitor';
export function print(ast) {
return new PrintVisitor().accept(ast);
}
export function PrintVisitor() {
this.padding = 0;
}
PrintVisitor.prototype = new Visitor();
PrintVisitor.prototype.pad = function(string) {
let out = '';
for (let i = 0, l = this.padding; i < l; i++) {
out += ' ';
}
out += string + '\n';
return out;
};
PrintVisitor.prototype.Program = function(program) {
let out = '',
body = program.body,
i,
l;
if (program.blockParams) {
let blockParams = 'BLOCK PARAMS: [';
for (i = 0, l = program.blockParams.length; i < l; i++) {
blockParams += ' ' + program.blockParams[i];
}
blockParams += ' ]';
out += this.pad(blockParams);
}
for (i = 0, l = body.length; i < l; i++) {
out += this.accept(body[i]);
}
this.padding--;
return out;
};
PrintVisitor.prototype.MustacheStatement = function(mustache) {
return this.pad('{{ ' + this.SubExpression(mustache) + ' }}');
};
PrintVisitor.prototype.Decorator = function(mustache) {
return this.pad('{{ DIRECTIVE ' + this.SubExpression(mustache) + ' }}');
};
PrintVisitor.prototype.BlockStatement = PrintVisitor.prototype.DecoratorBlock = function(
block
) {
let out = '';
out += this.pad(
(block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:'
);
this.padding++;
out += this.pad(this.SubExpression(block));
if (block.program) {
out += this.pad('PROGRAM:');
this.padding++;
out += this.accept(block.program);
this.padding--;
}
if (block.inverse) {
if (block.program) {
this.padding++;
}
out += this.pad('{{^}}');
this.padding++;
out += this.accept(block.inverse);
this.padding--;
if (block.program) {
this.padding--;
}
}
this.padding--;
return out;
};
PrintVisitor.prototype.PartialStatement = function(partial) {
let content = 'PARTIAL:' + partial.name.original;
if (partial.params[0]) {
content += ' ' + this.accept(partial.params[0]);
}
if (partial.hash) {
content += ' ' + this.accept(partial.hash);
}
return this.pad('{{> ' + content + ' }}');
};
PrintVisitor.prototype.PartialBlockStatement = function(partial) {
let content = 'PARTIAL BLOCK:' + partial.name.original;
if (partial.params[0]) {
content += ' ' + this.accept(partial.params[0]);
}
if (partial.hash) {
content += ' ' + this.accept(partial.hash);
}
content += ' ' + this.pad('PROGRAM:');
this.padding++;
content += this.accept(partial.program);
this.padding--;
return this.pad('{{> ' + content + ' }}');
};
PrintVisitor.prototype.ContentStatement = function(content) {
return this.pad("CONTENT[ '" + content.value + "' ]");
};
PrintVisitor.prototype.CommentStatement = function(comment) {
return this.pad("{{! '" + comment.value + "' }}");
};
PrintVisitor.prototype.SubExpression = function(sexpr) {
let params = sexpr.params,
paramStrings = [],
hash;
for (let i = 0, l = params.length; i < l; i++) {
paramStrings.push(this.accept(params[i]));
}
params = '[' + paramStrings.join(', ') + ']';
hash = sexpr.hash ? ' ' + this.accept(sexpr.hash) : '';
return this.accept(sexpr.path) + ' ' + params + hash;
};
PrintVisitor.prototype.PathExpression = function(id) {
let path = id.parts.join('/');
return (id.data ? '@' : '') + 'PATH:' + path;
};
PrintVisitor.prototype.StringLiteral = function(string) {
return '"' + string.value + '"';
};
PrintVisitor.prototype.NumberLiteral = function(number) {
return 'NUMBER{' + number.value + '}';
};
PrintVisitor.prototype.BooleanLiteral = function(bool) {
return 'BOOLEAN{' + bool.value + '}';
};
PrintVisitor.prototype.UndefinedLiteral = function() {
return 'UNDEFINED';
};
PrintVisitor.prototype.NullLiteral = function() {
return 'NULL';
};
PrintVisitor.prototype.Hash = function(hash) {
let pairs = hash.pairs,
joinedPairs = [];
for (let i = 0, l = pairs.length; i < l; i++) {
joinedPairs.push(this.accept(pairs[i]));
}
return 'HASH{' + joinedPairs.join(', ') + '}';
};
PrintVisitor.prototype.HashPair = function(pair) {
return pair.key + '=' + this.accept(pair.value);
};
/* eslint-enable new-cap */
-136
View File
@@ -1,136 +0,0 @@
import Exception from '../exception';
function Visitor() {
this.parents = [];
}
Visitor.prototype = {
constructor: Visitor,
mutating: false,
// Visits a given value. If mutating, will replace the value if necessary.
acceptKey: function(node, name) {
let value = this.accept(node[name]);
if (this.mutating) {
// Hacky sanity check: This may have a few false positives for type for the helper
// methods but will generally do the right thing without a lot of overhead.
if (value && !Visitor.prototype[value.type]) {
throw new Exception(
'Unexpected node type "' +
value.type +
'" found when accepting ' +
name +
' on ' +
node.type
);
}
node[name] = value;
}
},
// Performs an accept operation with added sanity check to ensure
// required keys are not removed.
acceptRequired: function(node, name) {
this.acceptKey(node, name);
if (!node[name]) {
throw new Exception(node.type + ' requires ' + name);
}
},
// Traverses a given array. If mutating, empty respnses will be removed
// for child elements.
acceptArray: function(array) {
for (let i = 0, l = array.length; i < l; i++) {
this.acceptKey(array, i);
if (!array[i]) {
array.splice(i, 1);
i--;
l--;
}
}
},
accept: function(object) {
if (!object) {
return;
}
/* istanbul ignore next: Sanity code */
if (!this[object.type]) {
throw new Exception('Unknown type: ' + object.type, object);
}
if (this.current) {
this.parents.unshift(this.current);
}
this.current = object;
let ret = this[object.type](object);
this.current = this.parents.shift();
if (!this.mutating || ret) {
return ret;
} else if (ret !== false) {
return object;
}
},
Program: function(program) {
this.acceptArray(program.body);
},
MustacheStatement: visitSubExpression,
Decorator: visitSubExpression,
BlockStatement: visitBlock,
DecoratorBlock: visitBlock,
PartialStatement: visitPartial,
PartialBlockStatement: function(partial) {
visitPartial.call(this, partial);
this.acceptKey(partial, 'program');
},
ContentStatement: function(/* content */) {},
CommentStatement: function(/* comment */) {},
SubExpression: visitSubExpression,
PathExpression: function(/* path */) {},
StringLiteral: function(/* string */) {},
NumberLiteral: function(/* number */) {},
BooleanLiteral: function(/* bool */) {},
UndefinedLiteral: function(/* literal */) {},
NullLiteral: function(/* literal */) {},
Hash: function(hash) {
this.acceptArray(hash.pairs);
},
HashPair: function(pair) {
this.acceptRequired(pair, 'value');
}
};
function visitSubExpression(mustache) {
this.acceptRequired(mustache, 'path');
this.acceptArray(mustache.params);
this.acceptKey(mustache, 'hash');
}
function visitBlock(block) {
visitSubExpression.call(this, block);
this.acceptKey(block, 'program');
this.acceptKey(block, 'inverse');
}
function visitPartial(partial) {
this.acceptRequired(partial, 'name');
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');
}
export default Visitor;
@@ -1,234 +0,0 @@
import Visitor from './visitor';
function WhitespaceControl(options = {}) {
this.options = options;
}
WhitespaceControl.prototype = new Visitor();
WhitespaceControl.prototype.Program = function(program) {
const doStandalone = !this.options.ignoreStandalone;
let isRoot = !this.isRootSeen;
this.isRootSeen = true;
let body = program.body;
for (let i = 0, l = body.length; i < l; i++) {
let current = body[i],
strip = this.accept(current);
if (!strip) {
continue;
}
let _isPrevWhitespace = isPrevWhitespace(body, i, isRoot),
_isNextWhitespace = isNextWhitespace(body, i, isRoot),
openStandalone = strip.openStandalone && _isPrevWhitespace,
closeStandalone = strip.closeStandalone && _isNextWhitespace,
inlineStandalone =
strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace;
if (strip.close) {
omitRight(body, i, true);
}
if (strip.open) {
omitLeft(body, i, true);
}
if (doStandalone && inlineStandalone) {
omitRight(body, i);
if (omitLeft(body, i)) {
// If we are on a standalone node, save the indent info for partials
if (current.type === 'PartialStatement') {
// Pull out the whitespace from the final line
current.indent = /([ \t]+$)/.exec(body[i - 1].original)[1];
}
}
}
if (doStandalone && openStandalone) {
omitRight((current.program || current.inverse).body);
// Strip out the previous content node if it's whitespace only
omitLeft(body, i);
}
if (doStandalone && closeStandalone) {
// Always strip the next node
omitRight(body, i);
omitLeft((current.inverse || current.program).body);
}
}
return program;
};
WhitespaceControl.prototype.BlockStatement = WhitespaceControl.prototype.DecoratorBlock = WhitespaceControl.prototype.PartialBlockStatement = function(
block
) {
this.accept(block.program);
this.accept(block.inverse);
// Find the inverse program that is involed with whitespace stripping.
let program = block.program || block.inverse,
inverse = block.program && block.inverse,
firstInverse = inverse,
lastInverse = inverse;
if (inverse && inverse.chained) {
firstInverse = inverse.body[0].program;
// Walk the inverse chain to find the last inverse that is actually in the chain.
while (lastInverse.chained) {
lastInverse = lastInverse.body[lastInverse.body.length - 1].program;
}
}
let strip = {
open: block.openStrip.open,
close: block.closeStrip.close,
// Determine the standalone candiacy. Basically flag our content as being possibly standalone
// so our parent can determine if we actually are standalone
openStandalone: isNextWhitespace(program.body),
closeStandalone: isPrevWhitespace((firstInverse || program).body)
};
if (block.openStrip.close) {
omitRight(program.body, null, true);
}
if (inverse) {
let inverseStrip = block.inverseStrip;
if (inverseStrip.open) {
omitLeft(program.body, null, true);
}
if (inverseStrip.close) {
omitRight(firstInverse.body, null, true);
}
if (block.closeStrip.open) {
omitLeft(lastInverse.body, null, true);
}
// Find standalone else statments
if (
!this.options.ignoreStandalone &&
isPrevWhitespace(program.body) &&
isNextWhitespace(firstInverse.body)
) {
omitLeft(program.body);
omitRight(firstInverse.body);
}
} else if (block.closeStrip.open) {
omitLeft(program.body, null, true);
}
return strip;
};
WhitespaceControl.prototype.Decorator = WhitespaceControl.prototype.MustacheStatement = function(
mustache
) {
return mustache.strip;
};
WhitespaceControl.prototype.PartialStatement = WhitespaceControl.prototype.CommentStatement = function(
node
) {
/* istanbul ignore next */
let strip = node.strip || {};
return {
inlineStandalone: true,
open: strip.open,
close: strip.close
};
};
function isPrevWhitespace(body, i, isRoot) {
if (i === undefined) {
i = body.length;
}
// Nodes that end with newlines are considered whitespace (but are special
// cased for strip operations)
let prev = body[i - 1],
sibling = body[i - 2];
if (!prev) {
return isRoot;
}
if (prev.type === 'ContentStatement') {
return (sibling || !isRoot ? /\r?\n\s*?$/ : /(^|\r?\n)\s*?$/).test(
prev.original
);
}
}
function isNextWhitespace(body, i, isRoot) {
if (i === undefined) {
i = -1;
}
let next = body[i + 1],
sibling = body[i + 2];
if (!next) {
return isRoot;
}
if (next.type === 'ContentStatement') {
return (sibling || !isRoot ? /^\s*?\r?\n/ : /^\s*?(\r?\n|$)/).test(
next.original
);
}
}
// Marks the node to the right of the position as omitted.
// I.e. {{foo}}' ' will mark the ' ' node as omitted.
//
// If i is undefined, then the first child will be marked as such.
//
// If multiple is truthy then all whitespace will be stripped out until non-whitespace
// content is met.
function omitRight(body, i, multiple) {
let current = body[i == null ? 0 : i + 1];
if (
!current ||
current.type !== 'ContentStatement' ||
(!multiple && current.rightStripped)
) {
return;
}
let original = current.value;
current.value = current.value.replace(
multiple ? /^\s+/ : /^[ \t]*\r?\n?/,
''
);
current.rightStripped = current.value !== original;
}
// Marks the node to the left of the position as omitted.
// I.e. ' '{{foo}} will mark the ' ' node as omitted.
//
// If i is undefined then the last child will be marked as such.
//
// If multiple is truthy then all whitespace will be stripped out until non-whitespace
// content is met.
function omitLeft(body, i, multiple) {
let current = body[i == null ? body.length - 1 : i - 1];
if (
!current ||
current.type !== 'ContentStatement' ||
(!multiple && current.leftStripped)
) {
return;
}
// We omit the last node if it's whitespace only and not preceded by a non-content node.
let original = current.value;
current.value = current.value.replace(multiple ? /\s+$/ : /[ \t]+$/, '');
current.leftStripped = current.value !== original;
return current.leftStripped;
}
export default WhitespaceControl;
-68
View File
@@ -1,68 +0,0 @@
const errorProps = [
'description',
'fileName',
'lineNumber',
'endLineNumber',
'message',
'name',
'number',
'stack'
];
function Exception(message, node) {
let loc = node && node.loc,
line,
endLineNumber,
column,
endColumn;
if (loc) {
line = loc.start.line;
endLineNumber = loc.end.line;
column = loc.start.column;
endColumn = loc.end.column;
message += ' - ' + line + ':' + column;
}
let tmp = Error.prototype.constructor.call(this, message);
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
for (let idx = 0; idx < errorProps.length; idx++) {
this[errorProps[idx]] = tmp[errorProps[idx]];
}
/* istanbul ignore else */
if (Error.captureStackTrace) {
Error.captureStackTrace(this, Exception);
}
try {
if (loc) {
this.lineNumber = line;
this.endLineNumber = endLineNumber;
// Work around issue under safari where we can't directly set the column value
/* istanbul ignore next */
if (Object.defineProperty) {
Object.defineProperty(this, 'column', {
value: column,
enumerable: true
});
Object.defineProperty(this, 'endColumn', {
value: endColumn,
enumerable: true
});
} else {
this.column = column;
this.endColumn = endColumn;
}
}
} catch (nop) {
/* Ignore if the browser is very particular */
}
}
Exception.prototype = new Error();
export default Exception;
+1 -1
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import { createFrame, isArray, isFunction } from '../utils';
import Exception from '../exception';
export default function(instance) {
instance.registerHelper('each', function(context, options) {
+1 -1
View File
@@ -1,4 +1,4 @@
import Exception from '../exception';
import { Exception } from '@handlebars/parser';
export default function(instance) {
instance.registerHelper('helperMissing', function(/* [args, ]options */) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import { isEmpty, isFunction } from '../utils';
import Exception from '../exception';
export default function(instance) {
instance.registerHelper('if', function(conditional, options) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import { isEmpty, isFunction } from '../utils';
import Exception from '../exception';
export default function(instance) {
instance.registerHelper('with', function(context, options) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import * as Utils from './utils';
import Exception from './exception';
import {
COMPILER_REVISION,
createFrame,
+3 -3
View File
@@ -6,9 +6,9 @@
var handlebars = require('../dist/cjs/handlebars')['default'];
var printer = require('../dist/cjs/handlebars/compiler/printer');
handlebars.PrintVisitor = printer.PrintVisitor;
handlebars.print = printer.print;
var parser = require('@handlebars/parser');
handlebars.PrintVisitor = parser.PrintVisitor;
handlebars.print = parser.print;
module.exports = handlebars;
+1 -1
View File
@@ -4,6 +4,6 @@ module.exports = {
lines: 100,
functions: 100,
statements: 100,
exclude: ['**/spec/**', '**/handlebars/compiler/parser.js'],
exclude: ['**/spec/**'],
reporter: 'html'
};
+5
View File
@@ -166,6 +166,11 @@
}
}
},
"@handlebars/parser": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@handlebars/parser/-/parser-1.1.0.tgz",
"integrity": "sha512-rR7tJoSwJ2eooOpYGxGGW95sLq6GXUaS1UtWvN7pei6n2/okYvCGld9vsUTvkl2migxbkszsycwtMf/GEc1k1A=="
},
"@knappi/grunt-saucelabs": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@knappi/grunt-saucelabs/-/grunt-saucelabs-9.0.2.tgz",
+1
View File
@@ -21,6 +21,7 @@
"node": ">=10"
},
"dependencies": {
"@handlebars/parser": "^1.1.0",
"neo-async": "^2.6.0",
"source-map": "^0.6.1",
"yargs": "^15.3.1"
-247
View File
@@ -181,251 +181,4 @@ describe('ast', function() {
testColumns(chainInverseNode.inverse.body[0].inverse, 10, 11, 8, 0);
});
});
describe('whitespace control', function() {
describe('parse', function() {
it('mustache', function() {
var ast = Handlebars.parse(' {{~comment~}} ');
equals(ast.body[0].value, '');
equals(ast.body[2].value, '');
});
it('block statements', function() {
var ast = Handlebars.parse(' {{# comment~}} \nfoo\n {{~/comment}}');
equals(ast.body[0].value, '');
equals(ast.body[1].program.body[0].value, 'foo');
});
});
describe('parseWithoutProcessing', function() {
it('mustache', function() {
var ast = Handlebars.parseWithoutProcessing(' {{~comment~}} ');
equals(ast.body[0].value, ' ');
equals(ast.body[2].value, ' ');
});
it('block statements', function() {
var ast = Handlebars.parseWithoutProcessing(
' {{# comment~}} \nfoo\n {{~/comment}}'
);
equals(ast.body[0].value, ' ');
equals(ast.body[1].program.body[0].value, ' \nfoo\n ');
});
});
});
describe('standalone flags', function() {
describe('mustache', function() {
it('does not mark mustaches as standalone', function() {
var ast = Handlebars.parse(' {{comment}} ');
equals(!!ast.body[0].value, true);
equals(!!ast.body[2].value, true);
});
});
describe('blocks - parseWithoutProcessing', function() {
it('block mustaches', function() {
var ast = Handlebars.parseWithoutProcessing(
' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '
),
block = ast.body[1];
equals(ast.body[0].value, ' ');
equals(block.program.body[0].value, ' \nfoo\n ');
equals(block.inverse.body[0].value, ' \n bar \n ');
equals(ast.body[2].value, ' ');
});
it('initial block mustaches', function() {
var ast = Handlebars.parseWithoutProcessing(
'{{# comment}} \nfoo\n {{/comment}}'
),
block = ast.body[0];
equals(block.program.body[0].value, ' \nfoo\n ');
});
it('mustaches with children', function() {
var ast = Handlebars.parseWithoutProcessing(
'{{# comment}} \n{{foo}}\n {{/comment}}'
),
block = ast.body[0];
equals(block.program.body[0].value, ' \n');
equals(block.program.body[1].path.original, 'foo');
equals(block.program.body[2].value, '\n ');
});
it('nested block mustaches', function() {
var ast = Handlebars.parseWithoutProcessing(
'{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'
),
body = ast.body[0].program.body,
block = body[1];
equals(body[0].value, ' \n');
equals(block.program.body[0].value, ' \nfoo\n ');
equals(block.inverse.body[0].value, ' \n bar \n ');
});
it('column 0 block mustaches', function() {
var ast = Handlebars.parseWithoutProcessing(
'test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '
),
block = ast.body[1];
equals(ast.body[0].omit, undefined);
equals(block.program.body[0].value, ' \nfoo\n ');
equals(block.inverse.body[0].value, ' \n bar \n ');
equals(ast.body[2].value, ' ');
});
});
describe('blocks', function() {
it('marks block mustaches as standalone', function() {
var ast = Handlebars.parse(
' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '
),
block = ast.body[1];
equals(ast.body[0].value, '');
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
equals(ast.body[2].value, '');
});
it('marks initial block mustaches as standalone', function() {
var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'),
block = ast.body[0];
equals(block.program.body[0].value, 'foo\n');
});
it('marks mustaches with children as standalone', function() {
var ast = Handlebars.parse('{{# comment}} \n{{foo}}\n {{/comment}}'),
block = ast.body[0];
equals(block.program.body[0].value, '');
equals(block.program.body[1].path.original, 'foo');
equals(block.program.body[2].value, '\n');
});
it('marks nested block mustaches as standalone', function() {
var ast = Handlebars.parse(
'{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'
),
body = ast.body[0].program.body,
block = body[1];
equals(body[0].value, '');
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
equals(body[0].value, '');
});
it('does not mark nested block mustaches as standalone', function() {
var ast = Handlebars.parse(
'{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}'
),
body = ast.body[0].program.body,
block = body[1];
equals(body[0].omit, undefined);
equals(block.program.body[0].value, ' \nfoo\n');
equals(block.inverse.body[0].value, ' bar \n ');
equals(body[0].omit, undefined);
});
it('does not mark nested initial block mustaches as standalone', function() {
var ast = Handlebars.parse(
'{{#foo}}{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}}{{/foo}}'
),
body = ast.body[0].program.body,
block = body[0];
equals(block.program.body[0].value, ' \nfoo\n');
equals(block.inverse.body[0].value, ' bar \n ');
equals(body[0].omit, undefined);
});
it('marks column 0 block mustaches as standalone', function() {
var ast = Handlebars.parse(
'test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '
),
block = ast.body[1];
equals(ast.body[0].omit, undefined);
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
equals(ast.body[2].value, '');
});
});
describe('partials - parseWithoutProcessing', function() {
it('simple partial', function() {
var ast = Handlebars.parseWithoutProcessing('{{> partial }} ');
equals(ast.body[1].value, ' ');
});
it('indented partial', function() {
var ast = Handlebars.parseWithoutProcessing(' {{> partial }} ');
equals(ast.body[0].value, ' ');
equals(ast.body[1].indent, '');
equals(ast.body[2].value, ' ');
});
});
describe('partials', function() {
it('marks partial as standalone', function() {
var ast = Handlebars.parse('{{> partial }} ');
equals(ast.body[1].value, '');
});
it('marks indented partial as standalone', function() {
var ast = Handlebars.parse(' {{> partial }} ');
equals(ast.body[0].value, '');
equals(ast.body[1].indent, ' ');
equals(ast.body[2].value, '');
});
it('marks those around content as not standalone', function() {
var ast = Handlebars.parse('a{{> partial }}');
equals(ast.body[0].omit, undefined);
ast = Handlebars.parse('{{> partial }}a');
equals(ast.body[1].omit, undefined);
});
});
describe('comments - parseWithoutProcessing', function() {
it('simple comment', function() {
var ast = Handlebars.parseWithoutProcessing('{{! comment }} ');
equals(ast.body[1].value, ' ');
});
it('indented comment', function() {
var ast = Handlebars.parseWithoutProcessing(' {{! comment }} ');
equals(ast.body[0].value, ' ');
equals(ast.body[2].value, ' ');
});
});
describe('comments', function() {
it('marks comment as standalone', function() {
var ast = Handlebars.parse('{{! comment }} ');
equals(ast.body[1].value, '');
});
it('marks indented comment as standalone', function() {
var ast = Handlebars.parse(' {{! comment }} ');
equals(ast.body[0].value, '');
equals(ast.body[2].value, '');
});
it('marks those around content as not standalone', function() {
var ast = Handlebars.parse('a{{! comment }}');
equals(ast.body[0].omit, undefined);
ast = Handlebars.parse('{{! comment }}a');
equals(ast.body[1].omit, undefined);
});
});
});
});
+1 -1
View File
@@ -22,7 +22,7 @@ vm.runInThisContext(
filename
);
var parse = require('../../dist/cjs/handlebars/compiler/base').parse;
var parse = require('@handlebars/parser').parse;
var compiler = require('../../dist/cjs/handlebars/compiler/compiler');
var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler');
-455
View File
@@ -1,455 +0,0 @@
describe('parser', function() {
if (!Handlebars.print) {
return;
}
function astFor(template) {
var ast = Handlebars.parse(template);
return Handlebars.print(ast);
}
it('parses simple mustaches', function() {
equals(astFor('{{123}}'), '{{ NUMBER{123} [] }}\n');
equals(astFor('{{"foo"}}'), '{{ "foo" [] }}\n');
equals(astFor('{{false}}'), '{{ BOOLEAN{false} [] }}\n');
equals(astFor('{{true}}'), '{{ BOOLEAN{true} [] }}\n');
equals(astFor('{{foo}}'), '{{ PATH:foo [] }}\n');
equals(astFor('{{foo?}}'), '{{ PATH:foo? [] }}\n');
equals(astFor('{{foo_}}'), '{{ PATH:foo_ [] }}\n');
equals(astFor('{{foo-}}'), '{{ PATH:foo- [] }}\n');
equals(astFor('{{foo:}}'), '{{ PATH:foo: [] }}\n');
});
it('parses simple mustaches with data', function() {
equals(astFor('{{@foo}}'), '{{ @PATH:foo [] }}\n');
});
it('parses simple mustaches with data paths', function() {
equals(astFor('{{@../foo}}'), '{{ @PATH:foo [] }}\n');
});
it('parses mustaches with paths', function() {
equals(astFor('{{foo/bar}}'), '{{ PATH:foo/bar [] }}\n');
});
it('parses mustaches with this/foo', function() {
equals(astFor('{{this/foo}}'), '{{ PATH:foo [] }}\n');
});
it('parses mustaches with - in a path', function() {
equals(astFor('{{foo-bar}}'), '{{ PATH:foo-bar [] }}\n');
});
it('parses mustaches with escaped [] in a path', function() {
equals(astFor('{{[foo[\\]]}}'), '{{ PATH:foo[] [] }}\n');
});
it('parses escaped \\\\ in path', function() {
equals(astFor('{{[foo\\\\]}}'), '{{ PATH:foo\\ [] }}\n');
});
it('parses mustaches with parameters', function() {
equals(astFor('{{foo bar}}'), '{{ PATH:foo [PATH:bar] }}\n');
});
it('parses mustaches with string parameters', function() {
equals(astFor('{{foo bar "baz" }}'), '{{ PATH:foo [PATH:bar, "baz"] }}\n');
});
it('parses mustaches with NUMBER parameters', function() {
equals(astFor('{{foo 1}}'), '{{ PATH:foo [NUMBER{1}] }}\n');
});
it('parses mustaches with BOOLEAN parameters', function() {
equals(astFor('{{foo true}}'), '{{ PATH:foo [BOOLEAN{true}] }}\n');
equals(astFor('{{foo false}}'), '{{ PATH:foo [BOOLEAN{false}] }}\n');
});
it('parses mustaches with undefined and null paths', function() {
equals(astFor('{{undefined}}'), '{{ UNDEFINED [] }}\n');
equals(astFor('{{null}}'), '{{ NULL [] }}\n');
});
it('parses mustaches with undefined and null parameters', function() {
equals(
astFor('{{foo undefined null}}'),
'{{ PATH:foo [UNDEFINED, NULL] }}\n'
);
});
it('parses mustaches with DATA parameters', function() {
equals(astFor('{{foo @bar}}'), '{{ PATH:foo [@PATH:bar] }}\n');
});
it('parses mustaches with hash arguments', function() {
equals(astFor('{{foo bar=baz}}'), '{{ PATH:foo [] HASH{bar=PATH:baz} }}\n');
equals(astFor('{{foo bar=1}}'), '{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n');
equals(
astFor('{{foo bar=true}}'),
'{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n'
);
equals(
astFor('{{foo bar=false}}'),
'{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n'
);
equals(
astFor('{{foo bar=@baz}}'),
'{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n'
);
equals(
astFor('{{foo bar=baz bat=bam}}'),
'{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n'
);
equals(
astFor('{{foo bar=baz bat="bam"}}'),
'{{ PATH:foo [] HASH{bar=PATH:baz, bat="bam"} }}\n'
);
equals(astFor("{{foo bat='bam'}}"), '{{ PATH:foo [] HASH{bat="bam"} }}\n');
equals(
astFor('{{foo omg bar=baz bat="bam"}}'),
'{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam"} }}\n'
);
equals(
astFor('{{foo omg bar=baz bat="bam" baz=1}}'),
'{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=NUMBER{1}} }}\n'
);
equals(
astFor('{{foo omg bar=baz bat="bam" baz=true}}'),
'{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{true}} }}\n'
);
equals(
astFor('{{foo omg bar=baz bat="bam" baz=false}}'),
'{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n'
);
});
it('parses contents followed by a mustache', function() {
equals(
astFor('foo bar {{baz}}'),
"CONTENT[ 'foo bar ' ]\n{{ PATH:baz [] }}\n"
);
});
it('parses a partial', function() {
equals(astFor('{{> foo }}'), '{{> PARTIAL:foo }}\n');
equals(astFor('{{> "foo" }}'), '{{> PARTIAL:foo }}\n');
equals(astFor('{{> 1 }}'), '{{> PARTIAL:1 }}\n');
});
it('parses a partial with context', function() {
equals(astFor('{{> foo bar}}'), '{{> PARTIAL:foo PATH:bar }}\n');
});
it('parses a partial with hash', function() {
equals(
astFor('{{> foo bar=bat}}'),
'{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n'
);
});
it('parses a partial with context and hash', function() {
equals(
astFor('{{> foo bar bat=baz}}'),
'{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n'
);
});
it('parses a partial with a complex name', function() {
equals(
astFor('{{> shared/partial?.bar}}'),
'{{> PARTIAL:shared/partial?.bar }}\n'
);
});
it('parsers partial blocks', function() {
equals(
astFor('{{#> foo}}bar{{/foo}}'),
"{{> PARTIAL BLOCK:foo PROGRAM:\n CONTENT[ 'bar' ]\n }}\n"
);
});
it('should handle parser block mismatch', function() {
shouldThrow(
function() {
astFor('{{#> goodbyes}}{{/hellos}}');
},
Error,
/goodbyes doesn't match hellos/
);
});
it('parsers partial blocks with arguments', function() {
equals(
astFor('{{#> foo context hash=value}}bar{{/foo}}'),
"{{> PARTIAL BLOCK:foo PATH:context HASH{hash=PATH:value} PROGRAM:\n CONTENT[ 'bar' ]\n }}\n"
);
});
it('parses a comment', function() {
equals(
astFor('{{! this is a comment }}'),
"{{! ' this is a comment ' }}\n"
);
});
it('parses a multi-line comment', function() {
equals(
astFor('{{!\nthis is a multi-line comment\n}}'),
"{{! '\nthis is a multi-line comment\n' }}\n"
);
});
it('parses an inverse section', function() {
equals(
astFor('{{#foo}} bar {{^}} baz {{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"
);
});
it('parses an inverse (else-style) section', function() {
equals(
astFor('{{#foo}} bar {{else}} baz {{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"
);
});
it('parses multiple inverse sections', function() {
equals(
astFor('{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"
);
});
it('parses empty blocks', function() {
equals(astFor('{{#foo}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n');
});
it('parses empty blocks with empty inverse section', function() {
equals(
astFor('{{#foo}}{{^}}{{/foo}}'),
'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n'
);
});
it('parses empty blocks with empty inverse (else-style) section', function() {
equals(
astFor('{{#foo}}{{else}}{{/foo}}'),
'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n'
);
});
it('parses non-empty blocks with empty inverse section', function() {
equals(
astFor('{{#foo}} bar {{^}}{{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"
);
});
it('parses non-empty blocks with empty inverse (else-style) section', function() {
equals(
astFor('{{#foo}} bar {{else}}{{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"
);
});
it('parses empty blocks with non-empty inverse section', function() {
equals(
astFor('{{#foo}}{{^}} bar {{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"
);
});
it('parses empty blocks with non-empty inverse (else-style) section', function() {
equals(
astFor('{{#foo}}{{else}} bar {{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"
);
});
it('parses a standalone inverse section', function() {
equals(
astFor('{{^foo}}bar{{/foo}}'),
"BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"
);
});
it('throws on old inverse section', function() {
shouldThrow(function() {
astFor('{{else foo}}bar{{/foo}}');
}, Error);
});
it('parses block with block params', function() {
equals(
astFor('{{#foo as |bar baz|}}content{{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"
);
});
it('parses inverse block with block params', function() {
equals(
astFor('{{^foo as |bar baz|}}content{{/foo}}'),
"BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"
);
});
it('parses chained inverse block with block params', function() {
equals(
astFor('{{#foo}}{{else foo as |bar baz|}}content{{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"
);
});
it("raises if there's a Parse error", function() {
shouldThrow(
function() {
astFor('foo{{^}}bar');
},
Error,
/Parse error on line 1/
);
shouldThrow(
function() {
astFor('{{foo}');
},
Error,
/Parse error on line 1/
);
shouldThrow(
function() {
astFor('{{foo &}}');
},
Error,
/Parse error on line 1/
);
shouldThrow(
function() {
astFor('{{#goodbyes}}{{/hellos}}');
},
Error,
/goodbyes doesn't match hellos/
);
shouldThrow(
function() {
astFor('{{{{goodbyes}}}} {{{{/hellos}}}}');
},
Error,
/goodbyes doesn't match hellos/
);
});
it('should handle invalid paths', function() {
shouldThrow(
function() {
astFor('{{foo/../bar}}');
},
Error,
/Invalid path: foo\/\.\. - 1:2/
);
shouldThrow(
function() {
astFor('{{foo/./bar}}');
},
Error,
/Invalid path: foo\/\. - 1:2/
);
shouldThrow(
function() {
astFor('{{foo/this/bar}}');
},
Error,
/Invalid path: foo\/this - 1:2/
);
});
it('knows how to report the correct line number in errors', function() {
shouldThrow(
function() {
astFor('hello\nmy\n{{foo}');
},
Error,
/Parse error on line 3/
);
shouldThrow(
function() {
astFor('hello\n\nmy\n\n{{foo}');
},
Error,
/Parse error on line 5/
);
});
it('knows how to report the correct line number in errors when the first character is a newline', function() {
shouldThrow(
function() {
astFor('\n\nhello\n\nmy\n\n{{foo}');
},
Error,
/Parse error on line 7/
);
});
describe('externally compiled AST', function() {
it('can pass through an already-compiled AST', function() {
equals(
astFor({
type: 'Program',
body: [{ type: 'ContentStatement', value: 'Hello' }]
}),
"CONTENT[ 'Hello' ]\n"
);
});
});
describe('directives', function() {
it('should parse block directives', function() {
equals(
astFor('{{#* foo}}{{/foo}}'),
'DIRECTIVE BLOCK:\n PATH:foo []\n PROGRAM:\n'
);
});
it('should parse directives', function() {
equals(astFor('{{* foo}}'), '{{ DIRECTIVE PATH:foo [] }}\n');
});
it('should fail if directives have inverse', function() {
shouldThrow(
function() {
astFor('{{#* foo}}{{^}}{{/foo}}');
},
Error,
/Unexpected inverse/
);
});
});
it('GH1024 - should track program location properly', function() {
var p = Handlebars.parse(
'\n' +
' {{#if foo}}\n' +
' {{bar}}\n' +
' {{else}} {{baz}}\n' +
'\n' +
' {{/if}}\n' +
' '
);
// We really need a deep equals but for now this should be stable...
equals(
JSON.stringify(p.loc),
JSON.stringify({
start: { line: 1, column: 0 },
end: { line: 7, column: 4 }
})
);
equals(
JSON.stringify(p.body[1].program.loc),
JSON.stringify({
start: { line: 2, column: 13 },
end: { line: 4, column: 7 }
})
);
equals(
JSON.stringify(p.body[1].inverse.loc),
JSON.stringify({
start: { line: 4, column: 15 },
end: { line: 6, column: 5 }
})
);
});
});
-164
View File
@@ -1,164 +0,0 @@
describe('Visitor', function() {
if (!Handlebars.Visitor || !Handlebars.print) {
return;
}
it('should provide coverage', function() {
// Simply run the thing and make sure it does not fail and that all of the
// stub methods are executed
var visitor = new Handlebars.Visitor();
visitor.accept(
Handlebars.parse(
'{{foo}}{{#foo (bar 1 "1" true undefined null) foo=@data}}{{!comment}}{{> bar }} {{/foo}}'
)
);
visitor.accept(Handlebars.parse('{{#> bar }} {{/bar}}'));
visitor.accept(Handlebars.parse('{{#* bar }} {{/bar}}'));
visitor.accept(Handlebars.parse('{{* bar }}'));
});
it('should traverse to stubs', function() {
var visitor = new Handlebars.Visitor();
visitor.StringLiteral = function(string) {
equal(string.value, '2');
};
visitor.NumberLiteral = function(number) {
equal(number.value, 1);
};
visitor.BooleanLiteral = function(bool) {
equal(bool.value, true);
equal(this.parents.length, 3);
equal(this.parents[0].type, 'SubExpression');
equal(this.parents[1].type, 'BlockStatement');
equal(this.parents[2].type, 'Program');
};
visitor.PathExpression = function(id) {
equal(/(foo\.)?bar$/.test(id.original), true);
};
visitor.ContentStatement = function(content) {
equal(content.value, ' ');
};
visitor.CommentStatement = function(comment) {
equal(comment.value, 'comment');
};
visitor.accept(
Handlebars.parse(
'{{#foo.bar (foo.bar 1 "2" true) foo=@foo.bar}}{{!comment}}{{> bar }} {{/foo.bar}}'
)
);
});
describe('mutating', function() {
describe('fields', function() {
it('should replace value', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.StringLiteral = function(string) {
return { type: 'NumberLiteral', value: 42, loc: string.loc };
};
var ast = Handlebars.parse('{{foo foo="foo"}}');
visitor.accept(ast);
equals(
Handlebars.print(ast),
'{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'
);
});
it('should treat undefined resonse as identity', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
var ast = Handlebars.parse('{{foo foo=42}}');
visitor.accept(ast);
equals(
Handlebars.print(ast),
'{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'
);
});
it('should remove false responses', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.Hash = function() {
return false;
};
var ast = Handlebars.parse('{{foo foo=42}}');
visitor.accept(ast);
equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n');
});
it('should throw when removing required values', function() {
shouldThrow(
function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.PathExpression = function() {
return false;
};
var ast = Handlebars.parse('{{foo 42}}');
visitor.accept(ast);
},
Handlebars.Exception,
'MustacheStatement requires path'
);
});
it('should throw when returning non-node responses', function() {
shouldThrow(
function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.PathExpression = function() {
return {};
};
var ast = Handlebars.parse('{{foo 42}}');
visitor.accept(ast);
},
Handlebars.Exception,
'Unexpected node type "undefined" found when accepting path on MustacheStatement'
);
});
});
describe('arrays', function() {
it('should replace value', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.StringLiteral = function(string) {
return { type: 'NumberLiteral', value: 42, loc: string.locInfo };
};
var ast = Handlebars.parse('{{foo "foo"}}');
visitor.accept(ast);
equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n');
});
it('should treat undefined resonse as identity', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
var ast = Handlebars.parse('{{foo 42}}');
visitor.accept(ast);
equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n');
});
it('should remove false responses', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.NumberLiteral = function() {
return false;
};
var ast = Handlebars.parse('{{foo 42}}');
visitor.accept(ast);
equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n');
});
});
});
});
-126
View File
@@ -1,126 +0,0 @@
%x mu emu com raw
%{
function strip(start, end) {
return yytext = yytext.substring(start, yyleng - end + start);
}
%}
LEFT_STRIP "~"
RIGHT_STRIP "~"
LOOKAHEAD [=~}\s\/.)|]
LITERAL_LOOKAHEAD [~}\s)]
/*
ID is the inverse of control characters.
Control characters ranges:
[\s] Whitespace
[!"#%-,\./] !, ", #, %, &, ', (, ), *, +, ,, ., /, Exceptions in range: $, -
[;->@] ;, <, =, >, @, Exceptions in range: :, ?
[\[-\^`] [, \, ], ^, `, Exceptions in range: _
[\{-~] {, |, }, ~
*/
ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
%%
[^\x00]*?/("{{") {
if(yytext.slice(-2) === "\\\\") {
strip(0,1);
this.begin("mu");
} else if(yytext.slice(-1) === "\\") {
strip(0,1);
this.begin("emu");
} else {
this.begin("mu");
}
if(yytext) return 'CONTENT';
}
[^\x00]+ return 'CONTENT';
// marks CONTENT up to the next mustache or escaped mustache
<emu>[^\x00]{2,}?/("{{"|"\\{{"|"\\\\{{"|<<EOF>>) {
this.popState();
return 'CONTENT';
}
// nested raw block will create stacked 'raw' condition
<raw>"{{{{"/[^/] this.begin('raw'); return 'CONTENT';
<raw>"{{{{/"[^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]"}}}}" {
this.popState();
// Should be using `this.topState()` below, but it currently
// returns the second top instead of the first top. Opened an
// issue about it at https://github.com/zaach/jison/issues/291
if (this.conditionStack[this.conditionStack.length-1] === 'raw') {
return 'CONTENT';
} else {
strip(5, 9);
return 'END_RAW_BLOCK';
}
}
<raw>[^\x00]+?/("{{{{") { return 'CONTENT'; }
<com>[\s\S]*?"--"{RIGHT_STRIP}?"}}" {
this.popState();
return 'COMMENT';
}
<mu>"(" return 'OPEN_SEXPR';
<mu>")" return 'CLOSE_SEXPR';
<mu>"{{{{" { return 'OPEN_RAW_BLOCK'; }
<mu>"}}}}" {
this.popState();
this.begin('raw');
return 'CLOSE_RAW_BLOCK';
}
<mu>"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL';
<mu>"{{"{LEFT_STRIP}?"#>" return 'OPEN_PARTIAL_BLOCK';
<mu>"{{"{LEFT_STRIP}?"#""*"? return 'OPEN_BLOCK';
<mu>"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK';
<mu>"{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE';
<mu>"{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE';
<mu>"{{"{LEFT_STRIP}?"^" return 'OPEN_INVERSE';
<mu>"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE_CHAIN';
<mu>"{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED';
<mu>"{{"{LEFT_STRIP}?"&" return 'OPEN';
<mu>"{{"{LEFT_STRIP}?"!--" {
this.unput(yytext);
this.popState();
this.begin('com');
}
<mu>"{{"{LEFT_STRIP}?"!"[\s\S]*?"}}" {
this.popState();
return 'COMMENT';
}
<mu>"{{"{LEFT_STRIP}?"*"? return 'OPEN';
<mu>"=" return 'EQUALS';
<mu>".." return 'ID';
<mu>"."/{LOOKAHEAD} return 'ID';
<mu>[\/.] return 'SEP';
<mu>\s+ // ignore whitespace
<mu>"}"{RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE_UNESCAPED';
<mu>{RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE';
<mu>'"'("\\"["]|[^"])*'"' yytext = strip(1,2).replace(/\\"/g,'"'); return 'STRING';
<mu>"'"("\\"[']|[^'])*"'" yytext = strip(1,2).replace(/\\'/g,"'"); return 'STRING';
<mu>"@" return 'DATA';
<mu>"true"/{LITERAL_LOOKAHEAD} return 'BOOLEAN';
<mu>"false"/{LITERAL_LOOKAHEAD} return 'BOOLEAN';
<mu>"undefined"/{LITERAL_LOOKAHEAD} return 'UNDEFINED';
<mu>"null"/{LITERAL_LOOKAHEAD} return 'NULL';
<mu>\-?[0-9]+(?:\.[0-9]+)?/{LITERAL_LOOKAHEAD} return 'NUMBER';
<mu>"as"\s+"|" return 'OPEN_BLOCK_PARAMS';
<mu>"|" return 'CLOSE_BLOCK_PARAMS';
<mu>{ID} return 'ID';
<mu>'['('\\]'|[^\]])*']' yytext = yytext.replace(/\\([\\\]])/g,'$1'); return 'ID';
<mu>. return 'INVALID';
<INITIAL,mu><<EOF>> return 'EOF';
-166
View File
@@ -1,166 +0,0 @@
%start root
%ebnf
%%
root
: program EOF { return $1; }
;
program
: statement* -> yy.prepareProgram($1)
;
statement
: mustache -> $1
| block -> $1
| rawBlock -> $1
| partial -> $1
| partialBlock -> $1
| content -> $1
| COMMENT {
$$ = {
type: 'CommentStatement',
value: yy.stripComment($1),
strip: yy.stripFlags($1, $1),
loc: yy.locInfo(@$)
};
};
content
: CONTENT {
$$ = {
type: 'ContentStatement',
original: $1,
value: $1,
loc: yy.locInfo(@$)
};
};
rawBlock
: openRawBlock content* END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$)
;
openRawBlock
: OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK -> { path: $2, params: $3, hash: $4 }
;
block
: openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$)
| openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$)
;
openBlock
: OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { open: $1, path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) }
;
openInverse
: OPEN_INVERSE helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) }
;
openInverseChain
: OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) }
;
inverseAndProgram
: INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 }
;
inverseChain
: openInverseChain program inverseChain? {
var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$),
program = yy.prepareProgram([inverse], $2.loc);
program.chained = true;
$$ = { strip: $1.strip, program: program, chain: true };
}
| inverseAndProgram -> $1
;
closeBlock
: OPEN_ENDBLOCK helperName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)}
;
mustache
// Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node.
// This also allows for handler unification as all mustache node instances can utilize the same handler
: OPEN helperName param* hash? CLOSE -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$)
| OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$)
;
partial
: OPEN_PARTIAL partialName param* hash? CLOSE {
$$ = {
type: 'PartialStatement',
name: $2,
params: $3,
hash: $4,
indent: '',
strip: yy.stripFlags($1, $5),
loc: yy.locInfo(@$)
};
}
;
partialBlock
: openPartialBlock program closeBlock -> yy.preparePartialBlock($1, $2, $3, @$)
;
openPartialBlock
: OPEN_PARTIAL_BLOCK partialName param* hash? CLOSE -> { path: $2, params: $3, hash: $4, strip: yy.stripFlags($1, $5) }
;
param
: helperName -> $1
| sexpr -> $1
;
sexpr
: OPEN_SEXPR helperName param* hash? CLOSE_SEXPR {
$$ = {
type: 'SubExpression',
path: $2,
params: $3,
hash: $4,
loc: yy.locInfo(@$)
};
};
hash
: hashSegment+ -> {type: 'Hash', pairs: $1, loc: yy.locInfo(@$)}
;
hashSegment
: ID EQUALS param -> {type: 'HashPair', key: yy.id($1), value: $3, loc: yy.locInfo(@$)}
;
blockParams
: OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS -> yy.id($2)
;
helperName
: path -> $1
| dataName -> $1
| STRING -> {type: 'StringLiteral', value: $1, original: $1, loc: yy.locInfo(@$)}
| NUMBER -> {type: 'NumberLiteral', value: Number($1), original: Number($1), loc: yy.locInfo(@$)}
| BOOLEAN -> {type: 'BooleanLiteral', value: $1 === 'true', original: $1 === 'true', loc: yy.locInfo(@$)}
| UNDEFINED -> {type: 'UndefinedLiteral', original: undefined, value: undefined, loc: yy.locInfo(@$)}
| NULL -> {type: 'NullLiteral', original: null, value: null, loc: yy.locInfo(@$)}
;
partialName
: helperName -> $1
| sexpr -> $1
;
dataName
: DATA pathSegments -> yy.preparePath(true, $2, @$)
;
path
: pathSegments -> yy.preparePath(false, $1, @$)
;
pathSegments
: pathSegments SEP ID { $1.push({part: yy.id($3), original: $3, separator: $2}); $$ = $1; }
| ID -> [{part: yy.id($1), original: $1}]
;
-1
View File
@@ -1 +0,0 @@
// File ignored in coverage tests via setting in .istanbul.yml
-1
View File
@@ -1 +0,0 @@
export default handlebars;
-33
View File
@@ -1,33 +0,0 @@
const { execFileWithInheritedOutput } = require('./util/exec-file');
const { createRegisterAsyncTaskFn } = require('./util/async-grunt-task');
const OUTPUT_FILE = 'lib/handlebars/compiler/parser.js';
module.exports = function(grunt) {
const registerAsyncTask = createRegisterAsyncTaskFn(grunt);
registerAsyncTask('parser', async () => {
await runJison();
combineWithPrefixAndSuffix();
grunt.log.writeln(`Parser "${OUTPUT_FILE}" created.`);
});
async function runJison() {
await execFileWithInheritedOutput('jison', [
'-m',
'js',
'src/handlebars.yy',
'src/handlebars.l'
]);
}
function combineWithPrefixAndSuffix() {
const combinedParserSourceCode =
grunt.file.read('src/parser-prefix.js') +
grunt.file.read('handlebars.js') +
grunt.file.read('src/parser-suffix.js');
grunt.file.write(OUTPUT_FILE, combinedParserSourceCode);
grunt.file.delete('handlebars.js');
}
};
+28 -175
View File
@@ -13,6 +13,12 @@
* https://github.com/DefinitelyTyped/DefinitelyTyped/commits/1ce60bdc07f10e0b076778c6c953271c072bc894/types/handlebars/index.d.ts
*/
// TypeScript Version: 2.3
import {
parse,
parseWithoutProcessing,
ParseOptions,
AST
} from '@handlebars/parser';
declare namespace Handlebars {
export interface TemplateDelegate<T = any> {
@@ -50,10 +56,7 @@ declare namespace Handlebars {
[key: string]: HelperDelegate;
}
export interface ParseOptions {
srcName?: string;
ignoreStandalone?: boolean;
}
export { parse, parseWithoutProcessing, ParseOptions };
export function registerHelper(name: string, fn: HelperDelegate): void;
export function registerHelper(name: HelperDeclareSpec): void;
@@ -71,8 +74,7 @@ declare namespace Handlebars {
export function createFrame(object: any): any;
export function blockParams(obj: any[], ids: any[]): any[];
export function log(level: number, obj: any): void;
export function parse(input: string, options?: ParseOptions): hbs.AST.Program;
export function parseWithoutProcessing(input: string, options?: ParseOptions): hbs.AST.Program;
export function compile<T = any>(input: any, options?: CompileOptions): HandlebarsTemplateDelegate<T>;
export function precompile(input: any, options?: PrecompileOptions): TemplateSpecification;
export function template<T = any>(precompilation: TemplateSpecification): HandlebarsTemplateDelegate<T>;
@@ -127,7 +129,7 @@ declare namespace Handlebars {
export const helpers: hbs.AST.helpers;
}
interface ICompiler {
export interface ICompiler {
accept(node: hbs.AST.Node): void;
Program(program: hbs.AST.Program): void;
BlockStatement(block: hbs.AST.BlockStatement): void;
@@ -191,25 +193,25 @@ declare namespace Handlebars {
/**
* Implement this interface on your MVW/MVVM/MVC views such as Backbone.View
**/
interface HandlebarsTemplatable {
export interface HandlebarsTemplatable {
template: HandlebarsTemplateDelegate;
}
// NOTE: for backward compatibility of this typing
type HandlebarsTemplateDelegate<T = any> = Handlebars.TemplateDelegate<T>;
export type HandlebarsTemplateDelegate<T = any> = Handlebars.TemplateDelegate<T>;
interface HandlebarsTemplates {
export interface HandlebarsTemplates {
[index: string]: HandlebarsTemplateDelegate;
}
interface TemplateSpecification {
export interface TemplateSpecification {
}
// for backward compatibility of this typing
type RuntimeOptions = Handlebars.RuntimeOptions;
export type RuntimeOptions = Handlebars.RuntimeOptions;
interface CompileOptions {
export interface CompileOptions {
data?: boolean;
compat?: boolean;
knownHelpers?: KnownHelpers;
@@ -222,11 +224,11 @@ interface CompileOptions {
explicitPartialContext?: boolean;
}
type KnownHelpers = {
export type KnownHelpers = {
[name in BuiltinHelperName | CustomHelperName]: boolean;
};
type BuiltinHelperName =
export type BuiltinHelperName =
"helperMissing"|
"blockHelperMissing"|
"each"|
@@ -236,21 +238,23 @@ type BuiltinHelperName =
"log"|
"lookup";
type CustomHelperName = string;
export type CustomHelperName = string;
interface PrecompileOptions extends CompileOptions {
export interface PrecompileOptions extends CompileOptions {
srcName?: string;
destName?: string;
}
declare namespace hbs {
export namespace hbs {
// for backward compatibility of this typing
type SafeString = Handlebars.SafeString;
export type SafeString = Handlebars.SafeString;
type Utils = typeof Handlebars.Utils;
export type Utils = typeof Handlebars.Utils;
export { AST }
}
interface Logger {
export interface Logger {
DEBUG: number;
INFO: number;
WARN: number;
@@ -262,161 +266,10 @@ interface Logger {
log(level: number, obj: string): void;
}
type CompilerInfo = [number/* revision */, string /* versions */];
declare namespace hbs {
namespace AST {
interface Node {
type: string;
loc: SourceLocation;
}
interface SourceLocation {
source: string;
start: Position;
end: Position;
}
interface Position {
line: number;
column: number;
}
interface Program extends Node {
body: Statement[];
blockParams: string[];
}
interface Statement extends Node {}
interface MustacheStatement extends Statement {
type: 'MustacheStatement';
path: PathExpression | Literal;
params: Expression[];
hash: Hash;
escaped: boolean;
strip: StripFlags;
}
interface Decorator extends MustacheStatement { }
interface BlockStatement extends Statement {
type: 'BlockStatement';
path: PathExpression;
params: Expression[];
hash: Hash;
program: Program;
inverse: Program;
openStrip: StripFlags;
inverseStrip: StripFlags;
closeStrip: StripFlags;
}
interface DecoratorBlock extends BlockStatement { }
interface PartialStatement extends Statement {
type: 'PartialStatement';
name: PathExpression | SubExpression;
params: Expression[];
hash: Hash;
indent: string;
strip: StripFlags;
}
interface PartialBlockStatement extends Statement {
type: 'PartialBlockStatement';
name: PathExpression | SubExpression;
params: Expression[];
hash: Hash;
program: Program;
openStrip: StripFlags;
closeStrip: StripFlags;
}
interface ContentStatement extends Statement {
type: 'ContentStatement';
value: string;
original: StripFlags;
}
interface CommentStatement extends Statement {
type: 'CommentStatement';
value: string;
strip: StripFlags;
}
interface Expression extends Node {}
interface SubExpression extends Expression {
type: 'SubExpression';
path: PathExpression;
params: Expression[];
hash: Hash;
}
interface PathExpression extends Expression {
type: 'PathExpression';
data: boolean;
depth: number;
parts: string[];
original: string;
}
interface Literal extends Expression {}
interface StringLiteral extends Literal {
type: 'StringLiteral';
value: string;
original: string;
}
interface BooleanLiteral extends Literal {
type: 'BooleanLiteral';
value: boolean;
original: boolean;
}
interface NumberLiteral extends Literal {
type: 'NumberLiteral';
value: number;
original: number;
}
interface UndefinedLiteral extends Literal {
type: 'UndefinedLiteral';
}
interface NullLiteral extends Literal {
type: 'NullLiteral';
}
interface Hash extends Node {
type: 'Hash';
pairs: HashPair[];
}
interface HashPair extends Node {
type: 'HashPair';
key: string;
value: Expression;
}
interface StripFlags {
open: boolean;
close: boolean;
}
interface helpers {
helperExpression(node: Node): boolean;
scopeId(path: PathExpression): boolean;
simpleId(path: PathExpression): boolean;
}
}
}
declare module "handlebars" {
export = Handlebars;
}
export type CompilerInfo = [number/* revision */, string /* versions */];
declare module "handlebars/runtime" {
export = Handlebars;
}
export default Handlebars;