Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix source slicing for whitespace-stripping comments #1717

Merged
merged 1 commit into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions packages/@glimmer/compiler/test/compiler-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,88 @@ test('top-level comments', `<!-- {{foo}} -->`, c` {{foo}} `);

test('handlebars comments', `<div>{{! Better not break! }}content</div>`, ['<div>', [s`content`]]);

test('handlebars comments with whitespace removal', '<div> {{~! some comment ~}} content</div>', [
'<div>',
[s`content`],
]);

// Strange handlebars comments
//
// https://github.com/handlebars-lang/handlebars-parser/blob/a095229e292e814ed9d113d88c827f3509534d1a/lib/helpers.js#L44C26-L44C45
//
// These are all the ways I could find to write a comment with a single character 's':
//
// {{!s}}
// {{!s-}}
// {{!s--}}
// {{!-s}}
// {{!-s-}}
// {{!-s--}}
// {{!--s--}}
//
// Unclear if they are meant to work but the parser accepts them, and the compiler shouldn't break.
//
// This is really testing @glimmer/syntax's ASTv2 loc info, but that is currently only exercised by
// the compiler and does not have its own dedicated test suite.
const StrangeComments = [
// empty comments
'!',
'!-',
'!--',
'!---',
'!----',
// comment consisting of '-'
'!-----',
// comment consisting of '--'
'!------',
// comment consisting of '!'
'!!',
'!-!',
'!!-',
'!-!-',
'!-!--',
'!--!--',
// comment consisting of '!-'
'!!---',
'!-!---',
'!--!---',
// comment consisting of '!--'
'!!----',
'!-!----',
'!--!----',
// comment consisting of 's'
'!s',
'!s-',
'!s--',
'!-s',
'!-s-',
'!-s--',
'!--s--',
// notably does not work, probably a bug:
// '!--!',
// '!--!-',
// '!--s',
// '!--s-',
];
for (const c of StrangeComments) {
test(`strange handlebars comments {{${c}}}`, `<div>{{${c}}}content</div>`, [
'<div>',
[s`content`],
]);
test(`strange handlebars comments {{~${c}}}`, `<div> {{~${c}}}content</div>`, [
'<div>',
[s`content`],
]);
test(`strange handlebars comments {{${c}~}}`, `<div>{{${c}~}} content</div>`, [
'<div>',
[s`content`],
]);
test(`strange handlebars comments {{~${c}~}}`, `<div> {{~${c}~}} content</div>`, [
'<div>',
[s`content`],
]);
}

test('namespaced attribute', `<svg xlink:title='svg-title'>content</svg>`, [
'<svg>',
{ 'xlink:title': s`svg-title` },
Expand Down
20 changes: 7 additions & 13 deletions packages/@glimmer/syntax/lib/source/loc/span.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { localAssert } from '@glimmer/debug-util';
import { LOCAL_DEBUG } from '@glimmer/local-debug-flags';
import { assertNever } from '@glimmer/util';

Expand Down Expand Up @@ -206,23 +207,16 @@ export class SourceSpan implements SourceLocation {
}

/**
* Convert this `SourceSpan` into a `SourceSlice`. In debug mode, this method optionally checks
* that the byte offsets represented by this `SourceSpan` actually correspond to the expected
* string.
* Convert this `SourceSpan` into a `SourceSlice`.
*/
toSlice(expected?: string): SourceSlice {
const chars = this.data.asString();

if (import.meta.env.DEV) {
if (expected !== undefined && chars !== expected) {
// eslint-disable-next-line no-console
console.warn(
`unexpectedly found ${JSON.stringify(
chars
)} when slicing source, but expected ${JSON.stringify(expected)}`
);
}
}
localAssert(
expected === undefined || expected === chars,
`unexpectedly found ${JSON.stringify(chars)} when slicing source, ` +
`but expected ${JSON.stringify(expected)}`
);

return new SourceSlice({
loc: this,
Expand Down
48 changes: 43 additions & 5 deletions packages/@glimmer/syntax/lib/v2/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,17 +390,55 @@ class StatementNormalizer {

MustacheCommentStatement(node: ASTv1.MustacheCommentStatement): ASTv2.GlimmerComment {
let loc = this.block.loc(node.loc);
let textLoc: SourceSpan;

if (loc.asString().slice(0, 5) === '{{!--') {
textLoc = loc.slice({ skipStart: 5, skipEnd: 4 });
// If someone cares for these cases to have the right loc, feel free to attempt:
// {{!}} {{~!}} {{!~}} {{~!~}}
// {{!-}} {{~!-}} {{!-~}} {{~!-~}}
// {{!--}} {{~!--}} {{!--~}} {{~!--~}}
// {{!---}} {{~!---}} {{!---~}} {{~!---~}}
// {{!----}} {{~!----}} {{!----~}} {{~!----~}}
if (node.value === '') {
return new ASTv2.GlimmerComment({
loc,
text: SourceSlice.synthetic(''),
});
}

let source = loc.asString();
let span = loc;

if (node.value.startsWith('-')) {
localAssert(
/^\{\{~?!---/u.test(source),
`to start a comment's content with a '-', it must have started with {{!--`
);
span = span.sliceStartChars({
skipStart: source.startsWith('{{~') ? 6 : 5,
chars: node.value.length,
});
} else if (node.value.endsWith('-')) {
localAssert(
/--~?\}\}/u.test(source),
`to end a comment's content with a '-', it must have ended with --}}`
);

const skipEnd = source.endsWith('~}}') ? 5 : 4;
const skipStart = source.length - node.value.length - skipEnd;

span = span.slice({
skipStart,
skipEnd,
});
} else {
textLoc = loc.slice({ skipStart: 3, skipEnd: 2 });
span = span.sliceStartChars({
skipStart: source.lastIndexOf(node.value),
chars: node.value.length,
});
}

return new ASTv2.GlimmerComment({
loc,
text: textLoc.toSlice(node.value),
text: span.toSlice(node.value),
});
}

Expand Down
6 changes: 5 additions & 1 deletion packages/@glimmer/syntax/test/loc-node-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,9 +585,12 @@ test('handlebars comment', () => {
{{!-- derp herky --}}<div></div>
</div>
<span {{! derpy }}></span>

{{~!~}}

`);

let [, div, , span] = ast.body;
let [, div, , span, standaloneComment] = ast.body;
if (assertNodeType(div, 'ElementNode')) {
let [comment1, , comment2, trailingDiv] = div.children;
locEqual(comment1, 2, 9, 2, 39);
Expand All @@ -598,6 +601,7 @@ test('handlebars comment', () => {
locEqual(span, 5, 4, 5, 30);
locEqual(comment3, 5, 10, 5, 22);
}
locEqual(standaloneComment, 7, 6, 7, 13);
}
});

Expand Down
5 changes: 5 additions & 0 deletions packages/@glimmer/syntax/test/parser-node-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,11 @@ test('a Handlebars comment', () => {
);
});

test('a Handlebars comment with whitespace removal', function () {
let t = 'before {{~! some comment ~}} after';
astEqual(t, b.program([b.text('before'), b.mustacheComment(' some comment '), b.text('after')]));
});

test('a Handlebars comment in proper element space', () => {
let t = 'before <div {{! some comment }} data-foo="bar" {{! other comment }}></div> after';
astEqual(
Expand Down
Loading