Skip to content

Commit e850643

Browse files
JeanMecheleonsenft
authored andcommitted
feat(compiler): Support comments in html element.
``` <div // comment 0 /* comment 1 */ attr1="value1" /* comment 2 spanning multiple lines */ attr2="value2" ></div> ```
1 parent 74f76d8 commit e850643

File tree

9 files changed

+611
-112
lines changed

9 files changed

+611
-112
lines changed

goldens/vscode-extension/vsix_package_contents.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ syntaxes/inline-styles.json
2323
syntaxes/inline-template.json
2424
syntaxes/let-declaration.json
2525
syntaxes/template-blocks.json
26+
syntaxes/template-tag-language-configuration.json
2627
syntaxes/template-tag.json
2728
syntaxes/template.json

packages/compiler/src/ml_parser/lexer.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,28 @@ class _Tokenizer {
804804
return [prefix, name];
805805
}
806806

807+
private _consumeSingleLineComment() {
808+
this._attemptCharCodeUntilFn((code) => chars.isNewLine(code) || code === chars.$EOF);
809+
this._attemptCharCodeUntilFn(isNotWhitespace);
810+
}
811+
812+
private _consumeMultiLineComment() {
813+
this._attemptCharCodeUntilFn((code) => {
814+
if (code === chars.$EOF) {
815+
return true;
816+
}
817+
if (code === chars.$STAR) {
818+
const next = this._cursor.clone();
819+
next.advance();
820+
return next.peek() === chars.$SLASH;
821+
}
822+
return false;
823+
});
824+
if (this._attemptStr('*/')) {
825+
this._attemptCharCodeUntilFn(isNotWhitespace);
826+
}
827+
}
828+
807829
private _consumeTagOpen(start: CharacterCursor) {
808830
let tagName: string;
809831
let prefix: string;
@@ -840,7 +862,21 @@ class _Tokenizer {
840862
this._attemptCharCodeUntilFn(isNotWhitespace);
841863
}
842864

843-
while (!isAttributeTerminator(this._cursor.peek())) {
865+
while (true) {
866+
if (this._attemptStr('//')) {
867+
this._consumeSingleLineComment();
868+
continue;
869+
}
870+
871+
if (this._attemptStr('/*')) {
872+
this._consumeMultiLineComment();
873+
continue;
874+
}
875+
876+
if (isAttributeTerminator(this._cursor.peek())) {
877+
break;
878+
}
879+
844880
if (this._selectorlessEnabled && this._cursor.peek() === chars.$AT) {
845881
const start = this._cursor.clone();
846882
const nameStart = start.clone();
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import * as html from '../../src/ml_parser/ast';
10+
import {Parser} from '../../src/ml_parser/parser';
11+
import {TagContentType} from '../../src/ml_parser/tags';
12+
13+
describe('Inline comments in attributes', () => {
14+
const parser = new Parser((tagName) => ({
15+
closedByParent: false,
16+
implicitNamespacePrefix: null,
17+
isVoid: false,
18+
ignoreFirstLf: false,
19+
canSelfClose: false,
20+
preventNamespaceInheritance: false,
21+
isClosedByChild: () => false,
22+
getContentType: () => TagContentType.PARSABLE_DATA,
23+
}));
24+
25+
it('should ignore single line comments between attributes', () => {
26+
const source = `
27+
<div
28+
// comment 1
29+
attr1="value1"
30+
// comment 2
31+
attr2="value2"
32+
></div>
33+
`;
34+
const result = parser.parse(source, 'url');
35+
expect(result.errors.length).toBe(0);
36+
const element = result.rootNodes.find((n) => n instanceof html.Element) as html.Element;
37+
expect(element.attrs.length).toBe(2);
38+
expect(element.attrs[0].name).toBe('attr1');
39+
expect(element.attrs[1].name).toBe('attr2');
40+
});
41+
42+
it('should ignore single line comments between inputs and outputs', () => {
43+
const source = `
44+
<div
45+
// comment 1
46+
[input]="value1"
47+
// comment 2
48+
(output)="handler()"
49+
></div>
50+
`;
51+
const result = parser.parse(source, 'url');
52+
expect(result.errors.length).toBe(0);
53+
const element = result.rootNodes.find((n) => n instanceof html.Element) as html.Element;
54+
expect(element.attrs.length).toBe(2);
55+
expect(element.attrs[0].name).toBe('[input]');
56+
expect(element.attrs[1].name).toBe('(output)');
57+
});
58+
59+
it('should ignore single line comments at the end of tag', () => {
60+
const source = `<div attr1="value1" // comment
61+
></div>`;
62+
const result = parser.parse(source, 'url');
63+
expect(result.errors.length).toBe(0);
64+
const element = result.rootNodes.find((n) => n instanceof html.Element) as html.Element;
65+
expect(element.attrs.length).toBe(1);
66+
expect(element.attrs[0].name).toBe('attr1');
67+
});
68+
69+
it('should handle commented out attribute', () => {
70+
const source = `<div /* attr1="value1" */ attr2="value2"></div>`;
71+
const result = parser.parse(source, 'url');
72+
expect(result.errors.length).toBe(0);
73+
const element = result.rootNodes.find((n) => n instanceof html.Element) as html.Element;
74+
expect(element.attrs.length).toBe(1);
75+
expect(element.attrs[0].name).toBe('attr2');
76+
});
77+
78+
it('should comment an attribute with a // on a new line', () => {
79+
const source = `<div
80+
// attr1="value1"
81+
attr2="value2"></div>`;
82+
const result = parser.parse(source, 'url');
83+
expect(result.errors.length).toBe(0);
84+
const element = result.rootNodes.find((n) => n instanceof html.Element) as html.Element;
85+
expect(element.attrs.length).toBe(1);
86+
expect(element.attrs[0].name).toBe('attr2');
87+
});
88+
89+
it('should ignore multi-line comments between attributes', () => {
90+
const source = `
91+
<div
92+
/* comment 1 */
93+
attr1="value1"
94+
/*
95+
comment 2
96+
spanning multiple lines
97+
*/
98+
attr2="value2"
99+
></div>
100+
`;
101+
const result = parser.parse(source, 'url');
102+
expect(result.errors.length).toBe(0);
103+
const element = result.rootNodes.find((n) => n instanceof html.Element) as html.Element;
104+
expect(element.attrs.length).toBe(2);
105+
expect(element.attrs[0].name).toBe('attr1');
106+
expect(element.attrs[1].name).toBe('attr2');
107+
});
108+
109+
it('should ignore multi-line comments at the end of tag', () => {
110+
const source = `<div attr1="value1" /* comment */ ></div>`;
111+
const result = parser.parse(source, 'url');
112+
expect(result.errors.length).toBe(0);
113+
const element = result.rootNodes.find((n) => n instanceof html.Element) as html.Element;
114+
expect(element.attrs.length).toBe(1);
115+
expect(element.attrs[0].name).toBe('attr1');
116+
});
117+
118+
it('should handle * inside multi-line comments', () => {
119+
const source = `<div attr1="value1" /* comment with * inside */ attr2="value2"></div>`;
120+
const result = parser.parse(source, 'url');
121+
expect(result.errors.length).toBe(0);
122+
const element = result.rootNodes.find((n) => n instanceof html.Element) as html.Element;
123+
expect(element.attrs.length).toBe(2);
124+
expect(element.attrs[0].name).toBe('attr1');
125+
expect(element.attrs[1].name).toBe('attr2');
126+
});
127+
128+
it('should maintain correct source spans with comments', () => {
129+
const source = `<div attr1="a" /* comment */ attr2="b"></div>`;
130+
const result = parser.parse(source, 'url');
131+
expect(result.errors.length).toBe(0);
132+
const element = result.rootNodes.find((n) => n instanceof html.Element) as html.Element;
133+
expect(element.attrs.length).toBe(2);
134+
135+
const attr1 = element.attrs[0];
136+
expect(attr1.name).toBe('attr1');
137+
expect(attr1.sourceSpan.start.offset).toBe(5);
138+
expect(attr1.sourceSpan.end.offset).toBe(14);
139+
140+
const attr2 = element.attrs[1];
141+
expect(attr2.name).toBe('attr2');
142+
expect(attr2.sourceSpan.start.offset).toBe(29);
143+
expect(attr2.sourceSpan.end.offset).toBe(38);
144+
});
145+
});

vscode-ng-language-service/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,10 @@
361361
"injectTo": [
362362
"text.html.derivative",
363363
"source.ts"
364-
]
364+
],
365+
"embeddedLanguages": {
366+
"template.tag.ng": "angular-tag-comment"
367+
}
365368
},
366369
{
367370
"path": "./syntaxes/expression.json",
@@ -373,6 +376,12 @@
373376
"fileMatch": "tsconfig*.json",
374377
"url": "./schemas/tsconfig-ng.schema.json"
375378
}
379+
],
380+
"languages": [
381+
{
382+
"id": "angular-tag-comment",
383+
"configuration": "./syntaxes/template-tag-language-configuration.json"
384+
}
376385
]
377386
},
378387
"activationEvents": [

vscode-ng-language-service/syntaxes/src/template-tag.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,34 @@ export const TemplateTag: GrammarDefinition = {
1212
scopeName: 'template.tag.ng',
1313
injectionSelector: 'L:text.html#meta.tag -comment',
1414
patterns: [
15+
{include: '#inlineComments'},
1516
{include: '#twoWayBinding'},
1617
{include: '#propertyBinding'},
1718
{include: '#eventBinding'},
1819
{include: '#templateBinding'},
20+
{include: '#standardAttribute'},
21+
{include: '#other'},
1922
],
2023
repository: {
24+
other: {
25+
match: /\s+/,
26+
name: 'template.tag.ng',
27+
},
28+
standardAttribute: {
29+
begin: /([-_a-zA-Z0-9.$:]+)(=)(["'])/,
30+
beginCaptures: {
31+
1: {name: 'entity.other.attribute-name.html'},
32+
2: {name: 'punctuation.separator.key-value.html'},
33+
3: {name: 'string.quoted.html punctuation.definition.string.begin.html'},
34+
},
35+
// @ts-ignore
36+
end: /\3/,
37+
endCaptures: {
38+
0: {name: 'string.quoted.html punctuation.definition.string.end.html'},
39+
},
40+
name: 'meta.attribute.standard.html',
41+
patterns: [{include: 'expression.ng'}],
42+
},
2143
propertyBinding: {
2244
begin: /(\[\s*@?(?:[-_a-zA-Z0-9.$]+|\[[^\[\]]*]|\([^()]*\))*%?\s*])(=)(["'])/,
2345
beginCaptures: {
@@ -119,5 +141,21 @@ export const TemplateTag: GrammarDefinition = {
119141
},
120142
],
121143
},
144+
inlineComments: {
145+
patterns: [
146+
{
147+
begin: /\/\*/,
148+
captures: {0: {name: 'punctuation.definition.comment.ts'}},
149+
name: 'comment.block.ts',
150+
end: /\*\//,
151+
},
152+
{
153+
begin: /\/\//,
154+
beginCaptures: {0: {name: 'punctuation.definition.comment.ts'}},
155+
name: 'comment.line.double-slash.ts',
156+
end: /(?=$)/,
157+
},
158+
],
159+
},
122160
},
123161
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"comments": {
3+
"blockComment": ["/*", "*/"]
4+
}
5+
}

vscode-ng-language-service/syntaxes/template-tag.json

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"scopeName": "template.tag.ng",
33
"injectionSelector": "L:text.html#meta.tag -comment",
44
"patterns": [
5+
{
6+
"include": "#inlineComments"
7+
},
58
{
69
"include": "#twoWayBinding"
710
},
@@ -13,9 +16,45 @@
1316
},
1417
{
1518
"include": "#templateBinding"
19+
},
20+
{
21+
"include": "#standardAttribute"
22+
},
23+
{
24+
"include": "#other"
1625
}
1726
],
1827
"repository": {
28+
"other": {
29+
"match": "\\s+",
30+
"name": "template.tag.ng"
31+
},
32+
"standardAttribute": {
33+
"begin": "([-_a-zA-Z0-9.$:]+)(=)([\"'])",
34+
"beginCaptures": {
35+
"1": {
36+
"name": "entity.other.attribute-name.html"
37+
},
38+
"2": {
39+
"name": "punctuation.separator.key-value.html"
40+
},
41+
"3": {
42+
"name": "string.quoted.html punctuation.definition.string.begin.html"
43+
}
44+
},
45+
"end": "\\3",
46+
"endCaptures": {
47+
"0": {
48+
"name": "string.quoted.html punctuation.definition.string.end.html"
49+
}
50+
},
51+
"name": "meta.attribute.standard.html",
52+
"patterns": [
53+
{
54+
"include": "expression.ng"
55+
}
56+
]
57+
},
1958
"propertyBinding": {
2059
"begin": "(\\[\\s*@?(?:[-_a-zA-Z0-9.$]+|\\[[^\\[\\]]*]|\\([^()]*\\))*%?\\s*])(=)([\"'])",
2160
"beginCaptures": {
@@ -167,6 +206,30 @@
167206
}
168207
}
169208
]
209+
},
210+
"inlineComments": {
211+
"patterns": [
212+
{
213+
"begin": "\\/\\*",
214+
"captures": {
215+
"0": {
216+
"name": "punctuation.definition.comment.ts"
217+
}
218+
},
219+
"name": "comment.block.ts",
220+
"end": "\\*\\/"
221+
},
222+
{
223+
"begin": "\\/\\/",
224+
"beginCaptures": {
225+
"0": {
226+
"name": "punctuation.definition.comment.ts"
227+
}
228+
},
229+
"name": "comment.line.double-slash.ts",
230+
"end": "(?=$)"
231+
}
232+
]
170233
}
171234
}
172235
}

vscode-ng-language-service/syntaxes/test/data/template-tag.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,15 @@
7777
<div *matHeaderCellDef></div>
7878
<!-- #613 -->
7979
<div *ngIf="x$ | async as a"></div>
80+
81+
<!-- inline comments in the HTML element -->
82+
<div
83+
// comment 0
84+
/* comment 1 */
85+
attr1="value1"
86+
/*
87+
comment 2
88+
spanning multiple lines
89+
*/
90+
attr2="value2"
91+
></div>

0 commit comments

Comments
 (0)