Skip to content

Commit 62ed3c2

Browse files
rwjbluenknapp
authored andcommitted
Add Handlebars.parseWithoutProcessing (#1584)
When authoring tooling that parses Handlebars files and emits Handlebars files, you often want to preserve the **exact** formatting of the input. The changes in this commit add a new method to the `Handlebars` namespace: `parseWithoutProcessing`. Unlike, `Handlebars.parse` (which will mutate the parsed AST to apply whitespace control) this method will parse the template and return it directly (**without** processing :wink:). For example, parsing the following template: ```hbs {{#foo}} {{~bar~}} {{baz~}} {{/foo}} ``` Using `Handlebars.parse`, the AST returned would have truncated the following whitespace: * The whitespace prior to the `{{#foo}}` * The newline following `{{#foo}}` * The leading whitespace before `{{~bar~}}` * The whitespace between `{{~bar~}}` and `{{baz~}}` * The newline after `{{baz~}}` * The whitespace prior to the `{{/foo}}` When `Handlebars.parse` is used from `Handlebars.precompile` or `Handlebars.compile`, this whitespace stripping is **very** important (these behaviors are intentional, and generally lead to better rendered output). When the same template is parsed with `Handlebars.parseWithoutProcessing` none of those modifications to the AST are made. This enables "codemod tooling" (e.g. `prettier` and `ember-template-recast`) to preserve the **exact** initial formatting. Prior to these changes, those tools would have to _manually_ reconstruct the whitespace that is lost prior to emitting source.
1 parent 7fcf9d2 commit 62ed3c2

File tree

6 files changed

+157
-6
lines changed

6 files changed

+157
-6
lines changed

docs/compiler-api.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,34 @@ var ast = Handlebars.parse(myTemplate);
1616
Handlebars.precompile(ast);
1717
```
1818

19+
### Parsing
20+
21+
There are two primary APIs that are used to parse an existing template into the AST:
22+
23+
#### parseWithoutProcessing
24+
25+
`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.
26+
27+
Example:
28+
29+
```js
30+
let ast = Handlebars.parseWithoutProcessing(myTemplate);
31+
```
32+
33+
#### parse
34+
35+
`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:
36+
37+
* Removes whitespace around dynamic statements that are on a line by themselves (aka "stand alone")
38+
* 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 `''`)
39+
40+
`Handlebars.parse` is used internally by `Handlebars.precompile` and `Handlebars.compile`.
41+
42+
Example:
43+
44+
```js
45+
let ast = Handlebars.parse(myTemplate);
46+
```
1947

2048
### Basic
2149

lib/handlebars.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import runtime from './handlebars.runtime';
22

33
// Compiler imports
44
import AST from './handlebars/compiler/ast';
5-
import { parser as Parser, parse } from './handlebars/compiler/base';
5+
import { parser as Parser, parse, parseWithoutProcessing } from './handlebars/compiler/base';
66
import { Compiler, compile, precompile } from './handlebars/compiler/compiler';
77
import JavaScriptCompiler from './handlebars/compiler/javascript-compiler';
88
import Visitor from './handlebars/compiler/visitor';
@@ -25,6 +25,7 @@ function create() {
2525
hb.JavaScriptCompiler = JavaScriptCompiler;
2626
hb.Parser = Parser;
2727
hb.parse = parse;
28+
hb.parseWithoutProcessing = parseWithoutProcessing;
2829

2930
return hb;
3031
}

lib/handlebars/compiler/base.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export { parser };
88
let yy = {};
99
extend(yy, Helpers);
1010

11-
export function parse(input, options) {
11+
export function parseWithoutProcessing(input, options) {
1212
// Just return if an already-compiled AST was passed in.
1313
if (input.type === 'Program') { return input; }
1414

@@ -19,6 +19,14 @@ export function parse(input, options) {
1919
return new yy.SourceLocation(options && options.srcName, locInfo);
2020
};
2121

22+
let ast = parser.parse(input);
23+
24+
return ast;
25+
}
26+
27+
export function parse(input, options) {
28+
let ast = parseWithoutProcessing(input, options);
2229
let strip = new WhitespaceControl(options);
23-
return strip.accept(parser.parse(input));
30+
31+
return strip.accept(ast);
2432
}

spec/ast.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,40 @@ describe('ast', function() {
123123
});
124124
});
125125

126+
describe('whitespace control', function() {
127+
describe('parse', function() {
128+
it('mustache', function() {
129+
let ast = Handlebars.parse(' {{~comment~}} ');
130+
131+
equals(ast.body[0].value, '');
132+
equals(ast.body[2].value, '');
133+
});
134+
135+
it('block statements', function() {
136+
var ast = Handlebars.parse(' {{# comment~}} \nfoo\n {{~/comment}}');
137+
138+
equals(ast.body[0].value, '');
139+
equals(ast.body[1].program.body[0].value, 'foo');
140+
});
141+
});
142+
143+
describe('parseWithoutProcessing', function() {
144+
it('mustache', function() {
145+
let ast = Handlebars.parseWithoutProcessing(' {{~comment~}} ');
146+
147+
equals(ast.body[0].value, ' ');
148+
equals(ast.body[2].value, ' ');
149+
});
150+
151+
it('block statements', function() {
152+
var ast = Handlebars.parseWithoutProcessing(' {{# comment~}} \nfoo\n {{~/comment}}');
153+
154+
equals(ast.body[0].value, ' ');
155+
equals(ast.body[1].program.body[0].value, ' \nfoo\n ');
156+
});
157+
});
158+
});
159+
126160
describe('standalone flags', function() {
127161
describe('mustache', function() {
128162
it('does not mark mustaches as standalone', function() {
@@ -131,6 +165,54 @@ describe('ast', function() {
131165
equals(!!ast.body[2].value, true);
132166
});
133167
});
168+
describe('blocks - parseWithoutProcessing', function() {
169+
it('block mustaches', function() {
170+
var ast = Handlebars.parseWithoutProcessing(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
171+
block = ast.body[1];
172+
173+
equals(ast.body[0].value, ' ');
174+
175+
equals(block.program.body[0].value, ' \nfoo\n ');
176+
equals(block.inverse.body[0].value, ' \n bar \n ');
177+
178+
equals(ast.body[2].value, ' ');
179+
});
180+
it('initial block mustaches', function() {
181+
var ast = Handlebars.parseWithoutProcessing('{{# comment}} \nfoo\n {{/comment}}'),
182+
block = ast.body[0];
183+
184+
equals(block.program.body[0].value, ' \nfoo\n ');
185+
});
186+
it('mustaches with children', function() {
187+
var ast = Handlebars.parseWithoutProcessing('{{# comment}} \n{{foo}}\n {{/comment}}'),
188+
block = ast.body[0];
189+
190+
equals(block.program.body[0].value, ' \n');
191+
equals(block.program.body[1].path.original, 'foo');
192+
equals(block.program.body[2].value, '\n ');
193+
});
194+
it('nested block mustaches', function() {
195+
var ast = Handlebars.parseWithoutProcessing('{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'),
196+
body = ast.body[0].program.body,
197+
block = body[1];
198+
199+
equals(body[0].value, ' \n');
200+
201+
equals(block.program.body[0].value, ' \nfoo\n ');
202+
equals(block.inverse.body[0].value, ' \n bar \n ');
203+
});
204+
it('column 0 block mustaches', function() {
205+
var ast = Handlebars.parseWithoutProcessing('test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
206+
block = ast.body[1];
207+
208+
equals(ast.body[0].omit, undefined);
209+
210+
equals(block.program.body[0].value, ' \nfoo\n ');
211+
equals(block.inverse.body[0].value, ' \n bar \n ');
212+
213+
equals(ast.body[2].value, ' ');
214+
});
215+
});
134216
describe('blocks', function() {
135217
it('marks block mustaches as standalone', function() {
136218
var ast = Handlebars.parse(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
@@ -204,6 +286,18 @@ describe('ast', function() {
204286
equals(ast.body[2].value, '');
205287
});
206288
});
289+
describe('partials - parseWithoutProcessing', function() {
290+
it('simple partial', function() {
291+
var ast = Handlebars.parseWithoutProcessing('{{> partial }} ');
292+
equals(ast.body[1].value, ' ');
293+
});
294+
it('indented partial', function() {
295+
var ast = Handlebars.parseWithoutProcessing(' {{> partial }} ');
296+
equals(ast.body[0].value, ' ');
297+
equals(ast.body[1].indent, '');
298+
equals(ast.body[2].value, ' ');
299+
});
300+
});
207301
describe('partials', function() {
208302
it('marks partial as standalone', function() {
209303
var ast = Handlebars.parse('{{> partial }} ');
@@ -223,6 +317,17 @@ describe('ast', function() {
223317
equals(ast.body[1].omit, undefined);
224318
});
225319
});
320+
describe('comments - parseWithoutProcessing', function() {
321+
it('simple comment', function() {
322+
var ast = Handlebars.parseWithoutProcessing('{{! comment }} ');
323+
equals(ast.body[1].value, ' ');
324+
});
325+
it('indented comment', function() {
326+
var ast = Handlebars.parseWithoutProcessing(' {{! comment }} ');
327+
equals(ast.body[0].value, ' ');
328+
equals(ast.body[2].value, ' ');
329+
});
330+
});
226331
describe('comments', function() {
227332
it('marks comment as standalone', function() {
228333
var ast = Handlebars.parse('{{! comment }} ');

types/index.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ declare namespace Handlebars {
4747
}
4848

4949
export interface ParseOptions {
50-
srcName?: string,
51-
ignoreStandalone?: boolean
50+
srcName?: string;
51+
ignoreStandalone?: boolean;
5252
}
5353

5454
export function registerHelper(name: string, fn: HelperDelegate): void;
@@ -69,6 +69,7 @@ declare namespace Handlebars {
6969
export function Exception(message: string): void;
7070
export function log(level: number, obj: any): void;
7171
export function parse(input: string, options?: ParseOptions): hbs.AST.Program;
72+
export function parseWithoutProcessing(input: string, options?: ParseOptions): hbs.AST.Program;
7273
export function compile<T = any>(input: any, options?: CompileOptions): HandlebarsTemplateDelegate<T>;
7374
export function precompile(input: any, options?: PrecompileOptions): TemplateSpecification;
7475
export function template<T = any>(precompilation: TemplateSpecification): HandlebarsTemplateDelegate<T>;

types/test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,12 @@ switch(allthings.type) {
192192
break;
193193
default:
194194
break;
195-
}
195+
}
196+
197+
function testParseWithoutProcessing() {
198+
const parsedTemplate: hbs.AST.Program = Handlebars.parseWithoutProcessing('<p>Hello, my name is {{name}}.</p>', {
199+
srcName: "/foo/bar/baz.hbs",
200+
});
201+
202+
const parsedTemplateWithoutOptions: hbs.AST.Program = Handlebars.parseWithoutProcessing('<p>Hello, my name is {{name}}.</p>');
203+
}

0 commit comments

Comments
 (0)