diff --git a/packages/@glimmer/syntax/lib/builders.ts b/packages/@glimmer/syntax/lib/builders.ts index b5a66329cc..dc5d70530d 100644 --- a/packages/@glimmer/syntax/lib/builders.ts +++ b/packages/@glimmer/syntax/lib/builders.ts @@ -14,7 +14,8 @@ function buildMustache( params?: AST.Expression[], hash?: AST.Hash, raw?: boolean, - loc?: AST.SourceLocation + loc?: AST.SourceLocation, + strip?: AST.StripFlags ): AST.MustacheStatement { if (typeof path === 'string') { path = buildPath(path); @@ -27,6 +28,7 @@ function buildMustache( hash: hash || buildHash([]), escaped: !raw, loc: buildLoc(loc || null), + strip: strip || { open: false, close: false }, }; } @@ -36,7 +38,10 @@ function buildBlock( hash: Option, _defaultBlock: AST.PossiblyDeprecatedBlock, _elseBlock?: Option, - loc?: AST.SourceLocation + loc?: AST.SourceLocation, + openStrip?: AST.StripFlags, + inverseStrip?: AST.StripFlags, + closeStrip?: AST.StripFlags ): AST.BlockStatement { let defaultBlock: AST.Block; let elseBlock: Option | undefined; @@ -69,6 +74,9 @@ function buildBlock( program: defaultBlock || null, inverse: elseBlock || null, loc: buildLoc(loc || null), + openStrip: openStrip || { open: false, close: false }, + inverseStrip: inverseStrip || { open: false, close: false }, + closeStrip: closeStrip || { open: false, close: false }, }; } diff --git a/packages/@glimmer/syntax/lib/generation/print.ts b/packages/@glimmer/syntax/lib/generation/print.ts index 0cba885bdf..b1414ccd74 100644 --- a/packages/@glimmer/syntax/lib/generation/print.ts +++ b/packages/@glimmer/syntax/lib/generation/print.ts @@ -57,11 +57,26 @@ export default function build( } function openBlock(block: AST.BlockStatement): string { - return ['{{#', pathParams(block), blockParams(block), '}}'].join(''); + return compactJoin([ + '{{', + block.openStrip.open ? '~' : null, + '#', + pathParams(block), + blockParams(block), + block.openStrip.close ? '~' : null, + '}}', + ]); } - function closeBlock(block: any): string { - return ['{{/', build(block.path, options), '}}'].join(''); + function closeBlock(block: AST.BlockStatement): string { + return compactJoin([ + '{{', + block.closeStrip.open ? '~' : null, + '/', + build(block.path, options), + block.closeStrip.close ? '~' : null, + '}}', + ]); } const output: string[] = []; @@ -144,7 +159,13 @@ export default function build( case 'MustacheStatement': { output.push( - compactJoin([ast.escaped ? '{{' : '{{{', pathParams(ast), ast.escaped ? '}}' : '}}}']) + compactJoin([ + ast.escaped ? '{{' : '{{{', + ast.strip.open ? '~' : null, + pathParams(ast), + ast.strip.close ? '~' : null, + ast.escaped ? '}}' : '}}}', + ]) ); } break; @@ -174,7 +195,16 @@ export default function build( const lines: string[] = []; if (ast.chained) { - lines.push(['{{else ', pathParams(ast), '}}'].join('')); + lines.push( + compactJoin([ + '{{', + ast.inverseStrip.open ? '~' : null, + 'else ', + pathParams(ast), + ast.inverseStrip.close ? '~' : null, + '}}', + ]) + ); } else { lines.push(openBlock(ast)); } @@ -183,7 +213,15 @@ export default function build( if (ast.inverse) { if (!ast.inverse.chained) { - lines.push('{{else}}'); + lines.push( + compactJoin([ + '{{', + ast.inverseStrip.open ? '~' : null, + 'else', + ast.inverseStrip.close ? '~' : null, + '}}', + ]) + ); } lines.push(build(ast.inverse, options)); } diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index a2b5877ea2..7993715264 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -90,7 +90,17 @@ export abstract class HandlebarsNodeVisitors extends Parser { hash = addInElementHash(this.cursor(), hash, block.loc); } - let node = b.block(path, params, hash, program, inverse, block.loc); + let node = b.block( + path, + params, + hash, + program, + inverse, + block.loc, + block.openStrip, + block.inverseStrip, + block.closeStrip + ); let parentProgram = this.currentElement(); @@ -106,7 +116,7 @@ export abstract class HandlebarsNodeVisitors extends Parser { } let mustache: AST.MustacheStatement; - let { escaped, loc } = rawMustache; + let { escaped, loc, strip } = rawMustache; if (isLiteral(rawMustache.path)) { mustache = { @@ -116,12 +126,13 @@ export abstract class HandlebarsNodeVisitors extends Parser { hash: b.hash(), escaped, loc, + strip, }; } else { let { path, params, hash } = acceptCallNodes(this, rawMustache as HBS.MustacheStatement & { path: HBS.PathExpression; }); - mustache = b.mustache(path, params, hash, !escaped, loc); + mustache = b.mustache(path, params, hash, !escaped, loc, strip); } switch (tokenizer.state) { diff --git a/packages/@glimmer/syntax/lib/types/nodes.ts b/packages/@glimmer/syntax/lib/types/nodes.ts index dda5c32b48..c2bb00b4d4 100644 --- a/packages/@glimmer/syntax/lib/types/nodes.ts +++ b/packages/@glimmer/syntax/lib/types/nodes.ts @@ -100,6 +100,7 @@ export interface MustacheStatement extends BaseNode { params: Expression[]; hash: Hash; escaped: boolean; + strip: StripFlags; } export interface BlockStatement extends BaseNode { @@ -109,6 +110,9 @@ export interface BlockStatement extends BaseNode { hash: Hash; program: Block; inverse?: Option; + openStrip: StripFlags; + inverseStrip: StripFlags; + closeStrip: StripFlags; // Glimmer extensions chained?: boolean; diff --git a/packages/@glimmer/syntax/test/generation/print-test.ts b/packages/@glimmer/syntax/test/generation/print-test.ts index 836722a610..7646a9e30f 100644 --- a/packages/@glimmer/syntax/test/generation/print-test.ts +++ b/packages/@glimmer/syntax/test/generation/print-test.ts @@ -101,8 +101,19 @@ QUnit.module('[glimmer-syntax] Code generation - source -> source', function() { // newlines after opening block '{{#each}}\n
  • foo
  • \n{{/each}}', - - // TODO: fix whitespace control in codemod mode - // '\n{{~#foo-bar~}} {{~/foo-bar~}} ', ].forEach(buildTest); + + test('whitespace control is preserved', function(assert) { + let before = '\n{{~var~}} '; + let after = '{{~var~}}'; + + assert.equal(printTransform(before), after); + }); + + test('block whitespace control is preserved', function(assert) { + let before = '\n{{~#foo-bar~}} {{~else if x~}} {{~else~}} {{~/foo-bar~}} '; + let after = '{{~#foo-bar~}}{{~else if x~}}{{~else~}}{{~/foo-bar~}}'; + + assert.equal(printTransform(before), after); + }); }); diff --git a/packages/@glimmer/syntax/test/parser-node-test.ts b/packages/@glimmer/syntax/test/parser-node-test.ts index eae8937b25..82dedb9797 100644 --- a/packages/@glimmer/syntax/test/parser-node-test.ts +++ b/packages/@glimmer/syntax/test/parser-node-test.ts @@ -281,10 +281,30 @@ test('Tokenizer: MustacheStatement encountered in afterAttributeValueQuoted stat test('Stripping - mustaches', function() { let t = 'foo {{~content}} bar'; - astEqual(t, b.program([b.text('foo'), b.mustache(b.path('content')), b.text(' bar')])); + astEqual( + t, + b.program([ + b.text('foo'), + b.mustache(b.path('content'), undefined, undefined, undefined, undefined, { + open: true, + close: false, + }), + b.text(' bar'), + ]) + ); t = 'foo {{content~}} bar'; - astEqual(t, b.program([b.text('foo '), b.mustache(b.path('content')), b.text('bar')])); + astEqual( + t, + b.program([ + b.text('foo '), + b.mustache(b.path('content'), undefined, undefined, undefined, undefined, { + open: false, + close: true, + }), + b.text('bar'), + ]) + ); }); test('Stripping - blocks', function() { @@ -293,7 +313,10 @@ test('Stripping - blocks', function() { t, b.program([ b.text('foo'), - b.block(b.path('wat'), [], b.hash(), b.blockItself()), + b.block(b.path('wat'), [], b.hash(), b.blockItself(), undefined, undefined, { + open: true, + close: false, + }), b.text(' bar'), ]) ); @@ -303,7 +326,17 @@ test('Stripping - blocks', function() { t, b.program([ b.text('foo '), - b.block(b.path('wat'), [], b.hash(), b.blockItself()), + b.block( + b.path('wat'), + [], + b.hash(), + b.blockItself(), + undefined, + undefined, + undefined, + undefined, + { open: false, close: true } + ), b.text('bar'), ]) ); @@ -314,7 +347,15 @@ test('Stripping - programs', function() { astEqual( t, b.program([ - b.block(b.path('wat'), [], b.hash(), b.blockItself([b.text('foo ')]), b.blockItself()), + b.block( + b.path('wat'), + [], + b.hash(), + b.blockItself([b.text('foo ')]), + b.blockItself(), + undefined, + { open: false, close: true } + ), ]) ); @@ -322,7 +363,16 @@ test('Stripping - programs', function() { astEqual( t, b.program([ - b.block(b.path('wat'), [], b.hash(), b.blockItself([b.text(' foo')]), b.blockItself()), + b.block( + b.path('wat'), + [], + b.hash(), + b.blockItself([b.text(' foo')]), + b.blockItself(), + undefined, + undefined, + { open: true, close: false } + ), ]) ); @@ -330,7 +380,16 @@ test('Stripping - programs', function() { astEqual( t, b.program([ - b.block(b.path('wat'), [], b.hash(), b.blockItself(), b.blockItself([b.text('foo ')])), + b.block( + b.path('wat'), + [], + b.hash(), + b.blockItself(), + b.blockItself([b.text('foo ')]), + undefined, + undefined, + { open: false, close: true } + ), ]) ); @@ -338,7 +397,17 @@ test('Stripping - programs', function() { astEqual( t, b.program([ - b.block(b.path('wat'), [], b.hash(), b.blockItself(), b.blockItself([b.text(' foo')])), + b.block( + b.path('wat'), + [], + b.hash(), + b.blockItself(), + b.blockItself([b.text(' foo')]), + undefined, + undefined, + undefined, + { open: true, close: false } + ), ]) ); }); @@ -354,7 +423,11 @@ test('Stripping - removes unnecessary text nodes', function() { [], b.hash(), b.blockItself([b.element('li', ['body', b.text(' foo ')])]), - null + null, + undefined, + { open: false, close: true }, + undefined, + { open: true, close: false } ), ]) );