fix(language-server): don't self-close tags inside quoted attribute values
This commit is contained in:
@@ -77,12 +77,37 @@ export function htmlFoldingRanges(
|
|||||||
* start tag, since plain HTML has no such elements outside its fixed void-element
|
* start tag, since plain HTML has no such elements outside its fixed void-element
|
||||||
* list. WRNexus components (`<Card />`) are exactly that case, so we complete
|
* list. WRNexus components (`<Card />`) are exactly that case, so we complete
|
||||||
* it ourselves rather than relying on the library.
|
* it ourselves rather than relying on the library.
|
||||||
|
*
|
||||||
|
* The scan tracks quote state from the tag's opening `<` up to `offset` (quotes
|
||||||
|
* are only meaningful inside a tag) so a `/` inside an attribute value — e.g. the
|
||||||
|
* first slash of `href="https://..."` — never misfires as a self-close: the
|
||||||
|
* library already returns `null` there on purpose, because the cursor sits in an
|
||||||
|
* attribute value, not a tag-close position.
|
||||||
*/
|
*/
|
||||||
function selfClosingTagCompletion(text: string, offset: number): string | null {
|
function selfClosingTagCompletion(text: string, offset: number): string | null {
|
||||||
if (text.charAt(offset - 1) !== "/") return null;
|
if (text.charAt(offset - 1) !== "/") return null;
|
||||||
if (text.charAt(offset) === ">") return null;
|
if (text.charAt(offset) === ">") return null;
|
||||||
const before = text.slice(0, offset);
|
|
||||||
return /<[A-Za-z][\w-]*(?:\s[^<>]*)?\/$/.test(before) ? ">" : null;
|
const tagStart = text.lastIndexOf("<", offset - 1);
|
||||||
|
if (tagStart < 0) return null;
|
||||||
|
if (!/^<[A-Za-z][\w-]*/.test(text.slice(tagStart))) return null;
|
||||||
|
|
||||||
|
let quote: '"' | "'" | null = null;
|
||||||
|
for (let i = tagStart + 1; i < offset - 1; i++) {
|
||||||
|
const ch = text[i];
|
||||||
|
if (quote) {
|
||||||
|
if (ch === quote) quote = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '"' || ch === "'") {
|
||||||
|
quote = ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === "<" || ch === ">") return null;
|
||||||
|
}
|
||||||
|
if (quote) return null;
|
||||||
|
|
||||||
|
return ">";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -96,6 +96,36 @@ test("completes a self-closing component tag", () => {
|
|||||||
expect(htmlTagComplete(doc(text), positionOf(text, "<Card /"))).toBe(">");
|
expect(htmlTagComplete(doc(text), positionOf(text, "<Card /"))).toBe(">");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("does not misfire inside a quoted attribute value containing a slash", () => {
|
||||||
|
const doubleQuoted = `page A {
|
||||||
|
view {
|
||||||
|
<a href="https:/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
expect(htmlTagComplete(doc(doubleQuoted), positionOf(doubleQuoted, `href="https:/`))).toBeNull();
|
||||||
|
|
||||||
|
const singleQuoted = `page A {
|
||||||
|
view {
|
||||||
|
<img src='/assets/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
expect(
|
||||||
|
htmlTagComplete(doc(singleQuoted), positionOf(singleQuoted, `src='/assets/`)),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("still completes a self-close after a preceding attribute", () => {
|
||||||
|
const text = `page A {
|
||||||
|
view {
|
||||||
|
<Card title="x" /
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
expect(htmlTagComplete(doc(text), positionOf(text, `title="x" /`))).toBe(">");
|
||||||
|
});
|
||||||
|
|
||||||
test("returns no tag completion outside a view block", () => {
|
test("returns no tag completion outside a view block", () => {
|
||||||
const text = `page A {
|
const text = `page A {
|
||||||
functions {
|
functions {
|
||||||
|
|||||||
Reference in New Issue
Block a user