Skip to content
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
12 changes: 6 additions & 6 deletions packages/language-server/src/lib/documents/parseHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ function preprocess(text: string) {
const offset = scanner.getTokenOffset();

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

if (token === TokenType.StartTagClose) {
Expand Down Expand Up @@ -74,11 +78,7 @@ function preprocess(text: string) {
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)
);
return isInsideMoustacheTag(text, currentStartTagStart, offset);
}

function blankStartOrEndTagLike(offset: number) {
Expand Down
44 changes: 22 additions & 22 deletions packages/language-server/src/lib/documents/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,31 +40,13 @@ function parseAttributes(
}

const regexIf = new RegExp('{#if\\s.*?}', 'gms');
const regexIfElseIf = new RegExp('{:else if\\s.*?}', 'gms');
const regexIfEnd = new RegExp('{/if}', 'gms');
const regexEach = new RegExp('{#each\\s.*?}', 'gms');
const regexEachEnd = new RegExp('{/each}', 'gms');
const regexAwait = new RegExp('{#await\\s.*?}', 'gms');
const regexAwaitEnd = new RegExp('{/await}', 'gms');
const regexHtml = new RegExp('{@html\\s.*?', 'gms');

/**
* if-blocks can contain the `<` operator, which mistakingly is
* parsed as a "open tag" character by the html parser.
* To prevent this, just replace the whole content inside the if with whitespace.
*/
function blankIfBlocks(text: string): string {
return text
.replace(regexIf, (substr) => {
return '{#if' + substr.replace(/[^\n]/g, ' ').substring(4, substr.length - 1) + '}';
})
.replace(regexIfElseIf, (substr) => {
return (
'{:else if' + substr.replace(/[^\n]/g, ' ').substring(9, substr.length - 1) + '}'
);
});
}

/**
* Extracts a tag (style or script) from the given text
* and returns its start, end and the attributes on that tag.
Expand All @@ -77,7 +59,6 @@ function extractTags(
tag: 'script' | 'style' | 'template',
html?: HTMLDocument
): TagInformation[] {
text = blankIfBlocks(text);
const rootNodes = html?.roots || parseHtml(text).roots;
const matchedNodes = rootNodes
.filter((node) => node.tag === tag)
Expand Down Expand Up @@ -404,7 +385,26 @@ 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('}');
/**
* Checks whether given position is inside a moustache tag (which includes control flow tags)
* using a simple bracket matching heuristic which might fail under conditions like
* `{#if {a: true}.a}`
*/
export function isInsideMoustacheTag(html: string, tagStart: number | null, position: number) {
if (tagStart === null) {
// Not inside <tag ... >
const charactersBeforePosition = html.substring(0, position);
return (
Math.max(
// TODO make this just check for '{'?
// Theoretically, someone could do {a < b} in a simple moustache tag
charactersBeforePosition.lastIndexOf('{#'),
charactersBeforePosition.lastIndexOf('{:')
) > charactersBeforePosition.lastIndexOf('}')
);
} else {
// Inside <tag ... >
const charactersInNode = html.substring(tagStart, position);
return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}');
}
}
29 changes: 29 additions & 0 deletions packages/language-server/test/lib/documents/parseHtml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ describe('parseHtml', () => {
);
});

it('ignore less than operator inside control flow moustache', () => {
testRootElements(
parseHtml(
`<Foo>
{#if 1 < 2 && innWidth <= 700}
<Foo>
<SelfClosing />
</Foo>
<div>hi</div>
{/if}
</Foo>
<style></style>`
)
);
});

it('ignore less than operator inside moustache with tag not self closed', () => {
testRootElements(
parseHtml(
Expand Down Expand Up @@ -65,6 +81,19 @@ describe('parseHtml', () => {
);
});

it('parse baseline html with control flow moustache', () => {
testRootElements(
parseHtml(
`<Foo>
{#if true}
foo
{/if}
</Foo>
<style></style>`
)
);
});

it('parse baseline html with possibly un-closed start tag', () => {
testRootElements(
parseHtml(
Expand Down