diff --git a/docs/compiler-api.md b/docs/compiler-api.md index f419fbcd2..99b950693 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -16,6 +16,34 @@ var ast = Handlebars.parse(myTemplate); Handlebars.precompile(ast); ``` +### Parsing + +There are two primary APIs that are used to parse an existing template into the AST: + +#### parseWithoutProcessing + +`Handlebars.parseWithoutProcessing` is the primary mechanism to turn a raw template string into the Handlebars AST described in this document. No processing is done on the resulting AST which makes this ideal for codemod (for source to source transformation) tooling. + +Example: + +```js +let ast = Handlebars.parseWithoutProcessing(myTemplate); +``` + +#### parse + +`Handlebars.parse` will parse the template with `parseWithoutProcessing` (see above) then it will update the AST to strip extraneous whitespace. The whitespace stripping functionality handles two distinct situations: + +* Removes whitespace around dynamic statements that are on a line by themselves (aka "stand alone") +* Applies "whitespace control" characters (i.e. `~`) by truncating the `ContentStatement` `value` property appropriately (e.g. `\n\n{{~foo}}` would have a `ContentStatement` with a `value` of `''`) + +`Handlebars.parse` is used internally by `Handlebars.precompile` and `Handlebars.compile`. + +Example: + +```js +let ast = Handlebars.parse(myTemplate); +``` ### Basic diff --git a/lib/handlebars.js b/lib/handlebars.js index f11495905..69bcb410b 100644 --- a/lib/handlebars.js +++ b/lib/handlebars.js @@ -2,7 +2,7 @@ import runtime from './handlebars.runtime'; // Compiler imports import AST from './handlebars/compiler/ast'; -import { parser as Parser, parse } from './handlebars/compiler/base'; +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'; @@ -25,6 +25,7 @@ function create() { hb.JavaScriptCompiler = JavaScriptCompiler; hb.Parser = Parser; hb.parse = parse; + hb.parseWithoutProcessing = parseWithoutProcessing; return hb; } diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index c6871d399..19989ca5e 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -8,7 +8,7 @@ export { parser }; let yy = {}; extend(yy, Helpers); -export function parse(input, options) { +export function parseWithoutProcessing(input, options) { // Just return if an already-compiled AST was passed in. if (input.type === 'Program') { return input; } @@ -19,6 +19,14 @@ export function parse(input, options) { 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(parser.parse(input)); + + return strip.accept(ast); } diff --git a/spec/ast.js b/spec/ast.js index e43438be7..09034739f 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -123,6 +123,40 @@ describe('ast', function() { }); }); + describe('whitespace control', function() { + describe('parse', function() { + it('mustache', function() { + let 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() { + let 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() { @@ -131,6 +165,54 @@ describe('ast', function() { 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}} '), @@ -204,6 +286,18 @@ describe('ast', function() { 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 }} '); @@ -223,6 +317,17 @@ describe('ast', function() { 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 }} '); diff --git a/types/index.d.ts b/types/index.d.ts index 6fdaf0f9c..00e04d427 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -47,8 +47,8 @@ declare namespace Handlebars { } export interface ParseOptions { - srcName?: string, - ignoreStandalone?: boolean + srcName?: string; + ignoreStandalone?: boolean; } export function registerHelper(name: string, fn: HelperDelegate): void; @@ -69,6 +69,7 @@ declare namespace Handlebars { export function Exception(message: string): void; 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(input: any, options?: CompileOptions): HandlebarsTemplateDelegate; export function precompile(input: any, options?: PrecompileOptions): TemplateSpecification; export function template(precompilation: TemplateSpecification): HandlebarsTemplateDelegate; diff --git a/types/test.ts b/types/test.ts index ff435916e..cef0d5981 100644 --- a/types/test.ts +++ b/types/test.ts @@ -192,4 +192,12 @@ switch(allthings.type) { break; default: break; -} \ No newline at end of file +} + +function testParseWithoutProcessing() { + const parsedTemplate: hbs.AST.Program = Handlebars.parseWithoutProcessing('

Hello, my name is {{name}}.

', { + srcName: "/foo/bar/baz.hbs", + }); + + const parsedTemplateWithoutOptions: hbs.AST.Program = Handlebars.parseWithoutProcessing('

Hello, my name is {{name}}.

'); +}