Skip to content
This repository was archived by the owner on Jan 6, 2026. It is now read-only.
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