Skip to content

Commit

Permalink
(fix) ignore html end-tag-like inside moustache (#671)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonlyu123 authored Nov 19, 2020
1 parent 34903a4 commit ff5d65e
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 14 deletions.
3 changes: 2 additions & 1 deletion packages/language-server/src/lib/documents/Document.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { urlToPath } from '../../utils';
import { WritableDocument } from './DocumentBase';
import { extractScriptTags, extractStyleTag, TagInformation, parseHtml } from './utils';
import { extractScriptTags, extractStyleTag, TagInformation } from './utils';
import { parseHtml } from './parseHtml';
import { SvelteConfig, loadConfig } from './configLoader';
import { HTMLDocument } from 'vscode-html-languageservice';

Expand Down
85 changes: 85 additions & 0 deletions packages/language-server/src/lib/documents/parseHtml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
getLanguageService,
HTMLDocument,
TokenType,
ScannerState,
Scanner
} from 'vscode-html-languageservice';
import { isInsideMoustacheTag } from './utils';

const parser = getLanguageService();

/**
* Parses text as HTML
*/
export function parseHtml(text: string): HTMLDocument {
const preprocessed = preprocess(text);

// We can safely only set getText because only this is used for parsing
const parsedDoc = parser.parseHTMLDocument(<any>{ getText: () => preprocessed });

return parsedDoc;
}

const createScanner = parser.createScanner as (
input: string,
initialOffset?: number,
initialState?: ScannerState
) => Scanner;

/**
* scan the text and remove any `>` or `<` that cause the tag to end short,
*/
function preprocess(text: string) {
let scanner = createScanner(text);
let token = scanner.scan();
let currentStartTagStart: number | null = null;

while (token !== TokenType.EOS) {
const offset = scanner.getTokenOffset();

if (token === TokenType.StartTagOpen) {
currentStartTagStart = offset;
}

if (token === TokenType.StartTagClose) {
if (shouldBlankStartOrEndTagLike(offset)) {
blankStartOrEndTagLike(offset);
} else {
currentStartTagStart = null;
}
}

if (token === TokenType.StartTagSelfClose) {
currentStartTagStart = null;
}

// <Foo checked={a < 1}>
// https://github.com/microsoft/vscode-html-languageservice/blob/71806ef57be07e1068ee40900ef8b0899c80e68a/src/parser/htmlScanner.ts#L327
if (
token === TokenType.Unknown &&
scanner.getScannerState() === ScannerState.WithinTag &&
scanner.getTokenText() === '<' &&
shouldBlankStartOrEndTagLike(offset)
) {
blankStartOrEndTagLike(offset);
}

token = scanner.scan();
}

return text;

function shouldBlankStartOrEndTagLike(offset: number) {
// not null rather than falsy, otherwise it won't work on first tag(0)
return (
currentStartTagStart !== null &&
isInsideMoustacheTag(text, currentStartTagStart, offset)
);
}

function blankStartOrEndTagLike(offset: number) {
text = text.substring(0, offset) + ' ' + text.substring(offset + 1);
scanner = createScanner(text, offset, ScannerState.WithinTag);
}
}
18 changes: 7 additions & 11 deletions packages/language-server/src/lib/documents/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { clamp, isInRange, regexLastIndexOf } from '../../utils';
import { Position, Range } from 'vscode-languageserver';
import { Node, getLanguageService, HTMLDocument } from 'vscode-html-languageservice';
import { Node, HTMLDocument } from 'vscode-html-languageservice';
import * as path from 'path';
import { parseHtml } from './parseHtml';

export interface TagInformation {
content: string;
Expand Down Expand Up @@ -38,16 +39,6 @@ function parseAttributes(
}
}

const parser = getLanguageService();

/**
* Parses text as HTML
*/
export function parseHtml(text: string): HTMLDocument {
// We can safely only set getText because only this is used for parsing
return parser.parseHTMLDocument(<any>{ getText: () => text });
}

const regexIf = new RegExp('{#if\\s.*?}', 'gms');
const regexIfElseIf = new RegExp('{:else if\\s.*?}', 'gms');
const regexIfEnd = new RegExp('{/if}', 'gms');
Expand Down Expand Up @@ -375,3 +366,8 @@ export function getLangAttribute(...tags: Array<TagInformation | null>): string

return attribute.replace(/^text\//, '');
}

export function isInsideMoustacheTag(html: string, tagStart: number, position: number) {
const charactersInNode = html.substring(tagStart, position);
return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}');
}
4 changes: 2 additions & 2 deletions packages/language-server/src/plugins/html/HTMLPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { LSConfigManager, LSHTMLConfig } from '../../ls-config';
import { svelteHtmlDataProvider } from './dataProvider';
import { HoverProvider, CompletionsProvider } from '../interfaces';
import { isInsideMoustacheTag } from '../../lib/documents/utils';

export class HTMLPlugin implements HoverProvider, CompletionsProvider {
private configManager: LSConfigManager;
Expand Down Expand Up @@ -180,8 +181,7 @@ export class HTMLPlugin implements HoverProvider, CompletionsProvider {
private isInsideMoustacheTag(html: HTMLDocument, document: Document, position: Position) {
const offset = document.offsetAt(position);
const node = html.findNodeAt(offset);
const charactersInNode = document.getText().substring(node.start, offset);
return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}');
return isInsideMoustacheTag(document.getText(), node.start, offset);
}

getDocumentSymbols(document: Document): SymbolInformation[] {
Expand Down
76 changes: 76 additions & 0 deletions packages/language-server/test/lib/documents/parseHtml.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import assert from 'assert';
import { HTMLDocument } from 'vscode-html-languageservice';
import { parseHtml } from '../../../src/lib/documents/parseHtml';

describe('parseHtml', () => {
const testRootElements = (document: HTMLDocument) => {
assert.deepStrictEqual(
document.roots.map((r) => r.tag),
['Foo', 'style']
);
};

it('ignore arrow inside moustache', () => {
testRootElements(
parseHtml(
`<Foo on:click={() => console.log('ya!!!')} />
<style></style>`
)
);
});

it('ignore greater than operator inside moustache', () => {
testRootElements(
parseHtml(
`<Foo checked={a > 1} />
<style></style>`
)
);
});

it('ignore less than operator inside moustache', () => {
testRootElements(
parseHtml(
`<Foo checked={a < 1} />
<style></style>`
)
);
});

it('ignore less than operator inside moustache with tag not self closed', () => {
testRootElements(
parseHtml(
`<Foo checked={a < 1}>
</Foo>
<style></style>`
)
);
});

it('parse baseline html', () => {
testRootElements(
parseHtml(
`<Foo checked />
<style></style>`
)
);
});

it('parse baseline html with moustache', () => {
testRootElements(
parseHtml(
`<Foo checked={a} />
<style></style>`
)
);
});

it('parse baseline html with possibly un-closed start tag', () => {
testRootElements(
parseHtml(
`<Foo checked={a}
<style></style>`
)
);
});
});

0 comments on commit ff5d65e

Please sign in to comment.