Skip to content

Commit ff5d65e

Browse files
authored
(fix) ignore html end-tag-like inside moustache (#671)
#309
1 parent 34903a4 commit ff5d65e

File tree

5 files changed

+172
-14
lines changed

5 files changed

+172
-14
lines changed

packages/language-server/src/lib/documents/Document.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { urlToPath } from '../../utils';
22
import { WritableDocument } from './DocumentBase';
3-
import { extractScriptTags, extractStyleTag, TagInformation, parseHtml } from './utils';
3+
import { extractScriptTags, extractStyleTag, TagInformation } from './utils';
4+
import { parseHtml } from './parseHtml';
45
import { SvelteConfig, loadConfig } from './configLoader';
56
import { HTMLDocument } from 'vscode-html-languageservice';
67

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
getLanguageService,
3+
HTMLDocument,
4+
TokenType,
5+
ScannerState,
6+
Scanner
7+
} from 'vscode-html-languageservice';
8+
import { isInsideMoustacheTag } from './utils';
9+
10+
const parser = getLanguageService();
11+
12+
/**
13+
* Parses text as HTML
14+
*/
15+
export function parseHtml(text: string): HTMLDocument {
16+
const preprocessed = preprocess(text);
17+
18+
// We can safely only set getText because only this is used for parsing
19+
const parsedDoc = parser.parseHTMLDocument(<any>{ getText: () => preprocessed });
20+
21+
return parsedDoc;
22+
}
23+
24+
const createScanner = parser.createScanner as (
25+
input: string,
26+
initialOffset?: number,
27+
initialState?: ScannerState
28+
) => Scanner;
29+
30+
/**
31+
* scan the text and remove any `>` or `<` that cause the tag to end short,
32+
*/
33+
function preprocess(text: string) {
34+
let scanner = createScanner(text);
35+
let token = scanner.scan();
36+
let currentStartTagStart: number | null = null;
37+
38+
while (token !== TokenType.EOS) {
39+
const offset = scanner.getTokenOffset();
40+
41+
if (token === TokenType.StartTagOpen) {
42+
currentStartTagStart = offset;
43+
}
44+
45+
if (token === TokenType.StartTagClose) {
46+
if (shouldBlankStartOrEndTagLike(offset)) {
47+
blankStartOrEndTagLike(offset);
48+
} else {
49+
currentStartTagStart = null;
50+
}
51+
}
52+
53+
if (token === TokenType.StartTagSelfClose) {
54+
currentStartTagStart = null;
55+
}
56+
57+
// <Foo checked={a < 1}>
58+
// https://github.com/microsoft/vscode-html-languageservice/blob/71806ef57be07e1068ee40900ef8b0899c80e68a/src/parser/htmlScanner.ts#L327
59+
if (
60+
token === TokenType.Unknown &&
61+
scanner.getScannerState() === ScannerState.WithinTag &&
62+
scanner.getTokenText() === '<' &&
63+
shouldBlankStartOrEndTagLike(offset)
64+
) {
65+
blankStartOrEndTagLike(offset);
66+
}
67+
68+
token = scanner.scan();
69+
}
70+
71+
return text;
72+
73+
function shouldBlankStartOrEndTagLike(offset: number) {
74+
// not null rather than falsy, otherwise it won't work on first tag(0)
75+
return (
76+
currentStartTagStart !== null &&
77+
isInsideMoustacheTag(text, currentStartTagStart, offset)
78+
);
79+
}
80+
81+
function blankStartOrEndTagLike(offset: number) {
82+
text = text.substring(0, offset) + ' ' + text.substring(offset + 1);
83+
scanner = createScanner(text, offset, ScannerState.WithinTag);
84+
}
85+
}

packages/language-server/src/lib/documents/utils.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { clamp, isInRange, regexLastIndexOf } from '../../utils';
22
import { Position, Range } from 'vscode-languageserver';
3-
import { Node, getLanguageService, HTMLDocument } from 'vscode-html-languageservice';
3+
import { Node, HTMLDocument } from 'vscode-html-languageservice';
44
import * as path from 'path';
5+
import { parseHtml } from './parseHtml';
56

67
export interface TagInformation {
78
content: string;
@@ -38,16 +39,6 @@ function parseAttributes(
3839
}
3940
}
4041

41-
const parser = getLanguageService();
42-
43-
/**
44-
* Parses text as HTML
45-
*/
46-
export function parseHtml(text: string): HTMLDocument {
47-
// We can safely only set getText because only this is used for parsing
48-
return parser.parseHTMLDocument(<any>{ getText: () => text });
49-
}
50-
5142
const regexIf = new RegExp('{#if\\s.*?}', 'gms');
5243
const regexIfElseIf = new RegExp('{:else if\\s.*?}', 'gms');
5344
const regexIfEnd = new RegExp('{/if}', 'gms');
@@ -375,3 +366,8 @@ export function getLangAttribute(...tags: Array<TagInformation | null>): string
375366

376367
return attribute.replace(/^text\//, '');
377368
}
369+
370+
export function isInsideMoustacheTag(html: string, tagStart: number, position: number) {
371+
const charactersInNode = html.substring(tagStart, position);
372+
return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}');
373+
}

packages/language-server/src/plugins/html/HTMLPlugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { LSConfigManager, LSHTMLConfig } from '../../ls-config';
2323
import { svelteHtmlDataProvider } from './dataProvider';
2424
import { HoverProvider, CompletionsProvider } from '../interfaces';
25+
import { isInsideMoustacheTag } from '../../lib/documents/utils';
2526

2627
export class HTMLPlugin implements HoverProvider, CompletionsProvider {
2728
private configManager: LSConfigManager;
@@ -180,8 +181,7 @@ export class HTMLPlugin implements HoverProvider, CompletionsProvider {
180181
private isInsideMoustacheTag(html: HTMLDocument, document: Document, position: Position) {
181182
const offset = document.offsetAt(position);
182183
const node = html.findNodeAt(offset);
183-
const charactersInNode = document.getText().substring(node.start, offset);
184-
return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}');
184+
return isInsideMoustacheTag(document.getText(), node.start, offset);
185185
}
186186

187187
getDocumentSymbols(document: Document): SymbolInformation[] {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import assert from 'assert';
2+
import { HTMLDocument } from 'vscode-html-languageservice';
3+
import { parseHtml } from '../../../src/lib/documents/parseHtml';
4+
5+
describe('parseHtml', () => {
6+
const testRootElements = (document: HTMLDocument) => {
7+
assert.deepStrictEqual(
8+
document.roots.map((r) => r.tag),
9+
['Foo', 'style']
10+
);
11+
};
12+
13+
it('ignore arrow inside moustache', () => {
14+
testRootElements(
15+
parseHtml(
16+
`<Foo on:click={() => console.log('ya!!!')} />
17+
<style></style>`
18+
)
19+
);
20+
});
21+
22+
it('ignore greater than operator inside moustache', () => {
23+
testRootElements(
24+
parseHtml(
25+
`<Foo checked={a > 1} />
26+
<style></style>`
27+
)
28+
);
29+
});
30+
31+
it('ignore less than operator inside moustache', () => {
32+
testRootElements(
33+
parseHtml(
34+
`<Foo checked={a < 1} />
35+
<style></style>`
36+
)
37+
);
38+
});
39+
40+
it('ignore less than operator inside moustache with tag not self closed', () => {
41+
testRootElements(
42+
parseHtml(
43+
`<Foo checked={a < 1}>
44+
</Foo>
45+
<style></style>`
46+
)
47+
);
48+
});
49+
50+
it('parse baseline html', () => {
51+
testRootElements(
52+
parseHtml(
53+
`<Foo checked />
54+
<style></style>`
55+
)
56+
);
57+
});
58+
59+
it('parse baseline html with moustache', () => {
60+
testRootElements(
61+
parseHtml(
62+
`<Foo checked={a} />
63+
<style></style>`
64+
)
65+
);
66+
});
67+
68+
it('parse baseline html with possibly un-closed start tag', () => {
69+
testRootElements(
70+
parseHtml(
71+
`<Foo checked={a}
72+
<style></style>`
73+
)
74+
);
75+
});
76+
});

0 commit comments

Comments
 (0)