svelte-eslint-parser
Version:
Svelte parser for ESLint
664 lines (663 loc) • 24.7 kB
JavaScript
import { convertAwaitBlock, convertEachBlock, convertIfBlock, convertKeyBlock, convertSnippetBlock, } from "./block.js";
import { getWithLoc, indexOf } from "./common.js";
import { convertMustacheTag, convertDebugTag, convertRawMustacheTag, } from "./mustache.js";
import { convertText } from "./text.js";
import { convertAttributes } from "./attr.js";
import { convertConstTag } from "./const.js";
import { sortNodes } from "../sort.js";
import { ParseError } from "../../index.js";
import { convertRenderTag } from "./render.js";
import { getChildren, getFragment } from "../compat.js";
/** Convert for Fragment or Element or ... */
export function* convertChildren(fragment, parent, ctx) {
const children = getChildren(fragment);
if (!children)
return;
for (const child of children) {
if (child.type === "Comment") {
yield convertComment(child, parent, ctx);
continue;
}
if (child.type === "Text") {
if (!child.data && child.start === child.end) {
continue;
}
yield convertText(child, parent, ctx);
continue;
}
if (child.type === "RegularElement") {
yield convertHTMLElement(child, parent, ctx);
continue;
}
if (child.type === "Element") {
if (child.name.includes(":")) {
yield convertSpecialElement(child, parent, ctx);
}
else {
yield convertHTMLElement(child, parent, ctx);
}
continue;
}
if (child.type === "Component") {
yield convertComponentElement(child, parent, ctx);
continue;
}
if (child.type === "InlineComponent") {
if (child.name.includes(":")) {
yield convertSpecialElement(child, parent, ctx);
}
else {
yield convertComponentElement(child, parent, ctx);
}
continue;
}
if (child.type === "SvelteComponent" ||
child.type === "SvelteElement" ||
child.type === "SvelteSelf") {
yield convertSpecialElement(child, parent, ctx);
continue;
}
if (child.type === "SlotElement" || child.type === "Slot") {
yield convertSlotElement(child, parent, ctx);
continue;
}
if (child.type === "ExpressionTag" || child.type === "MustacheTag") {
yield convertMustacheTag(child, parent, null, ctx);
continue;
}
if (child.type === "HtmlTag" || child.type === "RawMustacheTag") {
yield convertRawMustacheTag(child, parent, ctx);
continue;
}
if (child.type === "IfBlock") {
// {#if expr} {/if}
yield convertIfBlock(child, parent, ctx);
continue;
}
if (child.type === "EachBlock") {
// {#each expr as item, index (key)} {/each}
yield convertEachBlock(child, parent, ctx);
continue;
}
if (child.type === "AwaitBlock") {
// {#await promise} {:then number} {:catch error} {/await}
yield convertAwaitBlock(child, parent, ctx);
continue;
}
if (child.type === "KeyBlock") {
// {#key expression}...{/key}
yield convertKeyBlock(child, parent, ctx);
continue;
}
if (child.type === "SnippetBlock") {
// {#snippet x(args)}...{/snippet}
yield convertSnippetBlock(child, parent, ctx);
continue;
}
if (child.type === "SvelteWindow" || child.type === "Window") {
yield convertWindowElement(child, parent, ctx);
continue;
}
if (child.type === "SvelteBody" || child.type === "Body") {
yield convertBodyElement(child, parent, ctx);
continue;
}
if (child.type === "SvelteHead" || child.type === "Head") {
yield convertHeadElement(child, parent, ctx);
continue;
}
if (child.type === "TitleElement" || child.type === "Title") {
yield convertTitleElement(child, parent, ctx);
continue;
}
if (child.type === "SvelteOptions" || child.type === "Options") {
yield convertOptionsElement(child, parent, ctx);
continue;
}
if (child.type === "SvelteFragment" || child.type === "SlotTemplate") {
yield convertSlotTemplateElement(child, parent, ctx);
continue;
}
if (child.type === "DebugTag") {
yield convertDebugTag(child, parent, ctx);
continue;
}
if (child.type === "ConstTag") {
yield convertConstTag(child, parent, ctx);
continue;
}
if (child.type === "RenderTag") {
yield convertRenderTag(child, parent, ctx);
continue;
}
if (child.type === "SvelteDocument" || child.type === "Document") {
yield convertDocumentElement(child, parent, ctx);
continue;
}
if (child.type === "SvelteBoundary") {
yield convertSvelteBoundaryElement(child, parent, ctx);
continue;
}
throw new Error(`Unknown type:${child.type}`);
}
}
/** Extract `let:` directives. */
function extractLetDirectives(fragment) {
const letDirectives = [];
const attributes = [];
for (const attr of fragment.attributes) {
if (attr.type === "LetDirective" || attr.type === "Let") {
letDirectives.push(attr);
}
else {
attributes.push(attr);
}
}
return { letDirectives, attributes };
}
/** Check if children needs a scope. */
function needScopeByChildren(fragment) {
const children = getChildren(fragment);
if (!children)
return false;
for (const child of children) {
if (child.type === "ConstTag") {
return true;
}
if (child.type === "SnippetBlock") {
return true;
}
}
return false;
}
/** Convert for HTML Comment */
function convertComment(node, parent, ctx) {
const comment = {
type: "SvelteHTMLComment",
value: node.data,
parent,
...ctx.getConvertLocation(node),
};
ctx.addToken("HTMLComment", node);
return comment;
}
/** Convert for HTMLElement */
function convertHTMLElement(node, parent, ctx) {
const locs = ctx.getConvertLocation(node);
const element = {
type: "SvelteElement",
kind: "html",
name: null,
startTag: {
type: "SvelteStartTag",
attributes: [],
selfClosing: false,
parent: null,
range: [locs.range[0], null],
loc: {
start: {
line: locs.loc.start.line,
column: locs.loc.start.column,
},
end: null,
},
},
children: [],
endTag: null,
parent,
...locs,
};
ctx.elements.set(element, node);
element.startTag.parent = element;
const elementName = node.name;
const { letDirectives, attributes } = extractLetDirectives(node);
const letParams = [];
if (letDirectives.length) {
ctx.letDirCollections.beginExtract();
element.startTag.attributes.push(...convertAttributes(letDirectives, element.startTag, ctx));
letParams.push(...ctx.letDirCollections.extract().getLetParams());
}
const fragment = getFragment(node);
if (!letParams.length && !needScopeByChildren(fragment)) {
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
element.children.push(...convertChildren(fragment, element, ctx));
}
else {
ctx.scriptLet.nestBlock(element, letParams);
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
sortNodes(element.startTag.attributes);
element.children.push(...convertChildren(fragment, element, ctx));
ctx.scriptLet.closeScope();
}
extractElementTags(element, ctx, {
buildNameNode: (openTokenRange) => {
ctx.addToken("HTMLIdentifier", openTokenRange);
const name = {
type: "SvelteName",
name: elementName,
parent: element,
...ctx.getConvertLocation(openTokenRange),
};
return name;
},
});
if (element.name.name === "script" ||
element.name.name === "style" ||
(element.name.name === "template" && ctx.findBlock(element))) {
// Restore the block-like element.
for (const child of element.children) {
if (child.type === "SvelteText") {
child.value = ctx.code.slice(...child.range);
}
}
if (element.name.name === "script") {
ctx.stripScriptCode(element.startTag.range[1], element.endTag?.range[0] ?? element.range[1]);
}
}
if (element.startTag.selfClosing && element.name.name.endsWith("-")) {
// Restore the self-closing block.
const selfClosingBlock = /^[a-z]-+$/iu.test(element.name.name) &&
ctx.findSelfClosingBlock(element);
if (selfClosingBlock) {
element.name.name = selfClosingBlock.originalTag;
}
}
return element;
}
/** Convert for Special element. e.g. <svelte:self> */
function convertSpecialElement(node, parent, ctx) {
const locs = ctx.getConvertLocation(node);
const element = {
type: "SvelteElement",
kind: "special",
name: null,
startTag: {
type: "SvelteStartTag",
attributes: [],
selfClosing: false,
parent: null,
range: [locs.range[0], null],
loc: {
start: {
line: locs.loc.start.line,
column: locs.loc.start.column,
},
end: null,
},
},
children: [],
endTag: null,
parent,
...locs,
};
ctx.elements.set(element, node);
element.startTag.parent = element;
const elementName = node.name;
const { letDirectives, attributes } = extractLetDirectives(node);
const letParams = [];
if (letDirectives.length) {
ctx.letDirCollections.beginExtract();
element.startTag.attributes.push(...convertAttributes(letDirectives, element.startTag, ctx));
letParams.push(...ctx.letDirCollections.extract().getLetParams());
}
const fragment = getFragment(node);
if (!letParams.length && !needScopeByChildren(fragment)) {
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
element.children.push(...convertChildren(fragment, element, ctx));
}
else {
ctx.scriptLet.nestBlock(element, letParams);
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
sortNodes(element.startTag.attributes);
element.children.push(...convertChildren(fragment, element, ctx));
ctx.scriptLet.closeScope();
}
const thisExpression = (node.type === "SvelteComponent" && node.expression) ||
(node.type === "SvelteElement" && node.tag) ||
(node.type === "InlineComponent" &&
elementName === "svelte:component" &&
node.expression) ||
(node.type === "Element" && elementName === "svelte:element" && node.tag);
if (thisExpression) {
processThisAttribute(node, thisExpression, element, ctx);
}
extractElementTags(element, ctx, {
buildNameNode: (openTokenRange) => {
ctx.addToken("HTMLIdentifier", openTokenRange);
const name = {
type: "SvelteName",
name: elementName,
parent: element,
...ctx.getConvertLocation(openTokenRange),
};
return name;
},
});
return element;
}
/** process `this=` */
function processThisAttribute(node, thisValue, element, ctx) {
const startIndex = findStartIndexOfThis(node, ctx);
const eqIndex = ctx.code.indexOf("=", startIndex + 4 /* t,h,i,s */);
let thisNode;
if (typeof thisValue === "string") {
// Svelte v4
// this="..."
thisNode = createSvelteAttribute(startIndex, eqIndex, thisValue);
}
else {
// this={...}
const valueStartIndex = indexOf(ctx.code, (c) => Boolean(c.trim()), eqIndex + 1);
if (thisValue.type === "Literal" &&
typeof thisValue.value === "string" &&
ctx.code[valueStartIndex] !== "{") {
thisNode = createSvelteAttribute(startIndex, eqIndex, thisValue.value);
}
else {
thisNode = createSvelteSpecialDirective(startIndex, eqIndex, thisValue);
}
}
const targetIndex = element.startTag.attributes.findIndex((attr) => thisNode.range[1] <= attr.range[0]);
if (targetIndex === -1) {
element.startTag.attributes.push(thisNode);
}
else {
element.startTag.attributes.splice(targetIndex, 0, thisNode);
}
/** Create SvelteAttribute */
function createSvelteAttribute(startIndex, eqIndex, thisValue) {
const valueStartIndex = indexOf(ctx.code, (c) => Boolean(c.trim()), eqIndex + 1);
const quote = ctx.code.startsWith(thisValue, valueStartIndex)
? null
: ctx.code[valueStartIndex];
const literalStartIndex = quote
? valueStartIndex + quote.length
: valueStartIndex;
const literalEndIndex = literalStartIndex + thisValue.length;
const endIndex = quote ? literalEndIndex + quote.length : literalEndIndex;
const thisAttr = {
type: "SvelteAttribute",
key: null,
boolean: false,
value: [],
parent: element.startTag,
...ctx.getConvertLocation({ start: startIndex, end: endIndex }),
};
thisAttr.key = {
type: "SvelteName",
name: "this",
parent: thisAttr,
...ctx.getConvertLocation({ start: startIndex, end: eqIndex }),
};
thisAttr.value.push({
type: "SvelteLiteral",
value: thisValue,
parent: thisAttr,
...ctx.getConvertLocation({
start: literalStartIndex,
end: literalEndIndex,
}),
});
// this
ctx.addToken("HTMLIdentifier", {
start: startIndex,
end: startIndex + 4,
});
// =
ctx.addToken("Punctuator", {
start: eqIndex,
end: eqIndex + 1,
});
if (quote) {
// "
ctx.addToken("Punctuator", {
start: valueStartIndex,
end: literalStartIndex,
});
}
ctx.addToken("HTMLText", {
start: literalStartIndex,
end: literalEndIndex,
});
if (quote) {
// "
ctx.addToken("Punctuator", {
start: literalEndIndex,
end: endIndex,
});
}
return thisAttr;
}
/** Create SvelteSpecialDirective */
function createSvelteSpecialDirective(startIndex, eqIndex, expression) {
const closeIndex = ctx.code.indexOf("}", getWithLoc(expression).end);
const endIndex = indexOf(ctx.code, (c) => c === ">" || !c.trim(), closeIndex);
const thisDir = {
type: "SvelteSpecialDirective",
kind: "this",
key: null,
expression: null,
parent: element.startTag,
...ctx.getConvertLocation({ start: startIndex, end: endIndex }),
};
thisDir.key = {
type: "SvelteSpecialDirectiveKey",
parent: thisDir,
...ctx.getConvertLocation({ start: startIndex, end: eqIndex }),
};
// this
ctx.addToken("HTMLIdentifier", {
start: startIndex,
end: startIndex + 4,
});
// =
ctx.addToken("Punctuator", {
start: eqIndex,
end: eqIndex + 1,
});
ctx.scriptLet.addExpression(expression, thisDir, null, (es) => {
thisDir.expression = es;
});
return thisDir;
}
}
/** Find the start index of `this` */
function findStartIndexOfThis(node, ctx) {
// Get the end index of `svelte:element`
const startIndex = ctx.code.indexOf(node.name, node.start) + node.name.length;
const sortedAttrs = [...node.attributes].sort((a, b) => a.start - b.start);
// Find the start index of `this` from the end index of `svelte:element`.
// However, it only seeks to the start index of the first attribute (or the end index of element node).
let thisIndex = indexOf(ctx.code, (_c, index) => ctx.code.startsWith("this", index), startIndex, sortedAttrs[0]?.start ?? node.end);
while (thisIndex < 0) {
if (sortedAttrs.length === 0)
throw new ParseError("Cannot resolved `this` attribute.", thisIndex, ctx);
// Step3: Find the start index of `this` from the end index of attribute.
// However, it only seeks to the start index of the first attribute (or the end index of element node).
const nextStartIndex = sortedAttrs.shift().end;
thisIndex = indexOf(ctx.code, (_c, index) => ctx.code.startsWith("this", index), nextStartIndex, sortedAttrs[0]?.start ?? node.end);
}
return thisIndex;
}
/** Convert for ComponentElement */
function convertComponentElement(node, parent, ctx) {
const locs = ctx.getConvertLocation(node);
const element = {
type: "SvelteElement",
kind: "component",
name: null,
startTag: {
type: "SvelteStartTag",
attributes: [],
selfClosing: false,
parent: null,
range: [locs.range[0], null],
loc: {
start: {
line: locs.loc.start.line,
column: locs.loc.start.column,
},
end: null,
},
},
children: [],
endTag: null,
parent,
...locs,
};
ctx.elements.set(element, node);
element.startTag.parent = element;
const elementName = node.name;
const { letDirectives, attributes } = extractLetDirectives(node);
const letParams = [];
if (letDirectives.length) {
ctx.letDirCollections.beginExtract();
element.startTag.attributes.push(...convertAttributes(letDirectives, element.startTag, ctx));
letParams.push(...ctx.letDirCollections.extract().getLetParams());
}
const fragment = getFragment(node);
if (!letParams.length && !needScopeByChildren(fragment)) {
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
element.children.push(...convertChildren(fragment, element, ctx));
}
else {
ctx.scriptLet.nestBlock(element, letParams);
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
sortNodes(element.startTag.attributes);
element.children.push(...convertChildren(fragment, element, ctx));
ctx.scriptLet.closeScope();
}
extractElementTags(element, ctx, {
buildNameNode: (openTokenRange) => {
const chains = elementName.split(".");
const id = chains.shift();
const idRange = {
start: openTokenRange.start,
end: openTokenRange.start + id.length,
};
// ctx.addToken("Identifier", idRange)
const identifier = {
type: "Identifier",
name: id,
// @ts-expect-error -- ignore
parent: element,
...ctx.getConvertLocation(idRange),
};
let object = identifier;
// eslint-disable-next-line func-style -- var
let esCallback = (es) => {
element.name = es;
};
let start = idRange.end + 1;
for (const name of chains) {
const range = { start, end: start + name.length };
ctx.addToken("HTMLIdentifier", range);
const mem = {
type: "SvelteMemberExpressionName",
object,
property: {
type: "SvelteName",
name,
parent: null,
...ctx.getConvertLocation(range),
},
parent: element,
...ctx.getConvertLocation({
start: openTokenRange.start,
end: range.end,
}),
};
mem.property.parent = mem;
object.parent = mem;
object = mem;
start = range.end + 1;
if (mem.object === identifier) {
esCallback = (es) => {
mem.object = es;
};
}
}
ctx.scriptLet.addExpression(identifier, identifier.parent, null, esCallback);
return object;
},
});
return element;
}
/** Convert for Slot */
function convertSlotElement(node, parent, ctx) {
// Slot translates to SvelteHTMLElement.
const element = convertHTMLElement(node, parent, ctx);
ctx.slots.add(element);
return element;
}
/** Convert for window element. e.g. <svelte:window> */
function convertWindowElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for document element. e.g. <svelte:document> */
function convertDocumentElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for body element. e.g. <svelte:body> */
function convertBodyElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for head element. e.g. <svelte:head> */
function convertHeadElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for title element. e.g. <title> */
function convertTitleElement(node, parent, ctx) {
return convertHTMLElement(node, parent, ctx);
}
/** Convert for options element. e.g. <svelte:options> */
function convertOptionsElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for <svelte:fragment> element. */
function convertSlotTemplateElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for <svelte:boundary> element. */
function convertSvelteBoundaryElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Extract element tag and tokens */
export function extractElementTags(element, ctx, options) {
const startTagNameEnd = indexOf(ctx.code, (c) => c === "/" || c === ">" || !c.trim(), element.range[0] + 1);
const openTokenRange = {
start: element.range[0] + 1,
end: startTagNameEnd,
};
element.name = options.buildNameNode(openTokenRange);
const startTagEnd = ctx.code.indexOf(">", element.startTag.attributes[element.startTag.attributes.length - 1]
?.range[1] ?? openTokenRange.end) + 1;
element.startTag.range[1] = startTagEnd;
element.startTag.loc.end = ctx.getLocFromIndex(startTagEnd);
if (ctx.code[element.range[1] - 1] !== ">") {
// Have not end tag
return;
}
if (ctx.code[element.range[1] - 2] === "/") {
// self close
element.startTag.selfClosing = true;
return;
}
const endTagOpen = ctx.code.lastIndexOf("<", element.range[1] - 1);
if (endTagOpen <= startTagEnd - 1) {
// void element
return;
}
const endTagNameStart = endTagOpen + 2;
const endTagNameEnd = indexOf(ctx.code, (c) => c === ">" || !c.trim(), endTagNameStart);
const endTagClose = ctx.code.indexOf(">", endTagNameEnd);
element.endTag = {
type: "SvelteEndTag",
parent: element,
...ctx.getConvertLocation({ start: endTagOpen, end: endTagClose + 1 }),
};
ctx.addToken("HTMLIdentifier", {
start: endTagNameStart,
end: endTagNameEnd,
});
}