svelte-language-server
Version:
A language server for Svelte
428 lines • 18.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HTMLNode = void 0;
exports.parseHtml = parseHtml;
exports.getAttributeContextAtPosition = getAttributeContextAtPosition;
exports.scanCommentWithinTextOrComment = scanCommentWithinTextOrComment;
const vscode_html_languageservice_1 = require("vscode-html-languageservice");
const utils_1 = require("./utils");
const voidElements = new Set((0, vscode_html_languageservice_1.getDefaultHTMLDataProvider)()
.provideTags()
.filter((tag) => tag.void)
.map((tag) => tag.name));
const createScanner = (0, vscode_html_languageservice_1.getLanguageService)()
.createScanner;
const braceStartCode = '{'.charCodeAt(0);
const singleQuoteCode = "'".charCodeAt(0);
const doubleQuoteCode = '"'.charCodeAt(0);
/**
* adopted from https://github.com/microsoft/vscode-html-languageservice/blob/10daf45dc16b4f4228987cf7cddf3a7dbbdc7570/src/parser/htmlParser.ts
* differences:
*
* 1. parse expression tag in Whitespace state
* 2. parse attribute with interpolation in AttributeValue state
* 3. detect svelte blocks/tags in Content state
*/
function parseHtml(text) {
let scanner = createScanner(text, undefined, undefined, true);
const htmlDocument = new HTMLNode(0, text.length, [], undefined);
let curr = htmlDocument;
let endTagStart = -1;
let endTagName = undefined;
let pendingAttribute = null;
let token = scanner.scan();
while (token !== vscode_html_languageservice_1.TokenType.EOS) {
switch (token) {
case vscode_html_languageservice_1.TokenType.StartTagOpen:
const child = new HTMLNode(scanner.getTokenOffset(), text.length, [], curr);
curr.children.push(child);
curr = child;
break;
case vscode_html_languageservice_1.TokenType.StartTag:
curr.tag = scanner.getTokenText();
break;
case vscode_html_languageservice_1.TokenType.StartTagClose:
if (curr.parent) {
curr.end = scanner.getTokenEnd(); // might be later set to end tag position
if (scanner.getTokenLength()) {
curr.startTagEnd = scanner.getTokenEnd();
if (curr.tag && voidElements.has(curr.tag)) {
curr.closed = true;
curr = curr.parent;
}
}
else {
// pseudo close token from an incomplete start tag
curr = curr.parent;
}
}
break;
case vscode_html_languageservice_1.TokenType.StartTagSelfClose:
if (curr.parent) {
curr.closed = true;
curr.end = scanner.getTokenEnd();
curr.startTagEnd = scanner.getTokenEnd();
curr = curr.parent;
}
break;
case vscode_html_languageservice_1.TokenType.EndTagOpen:
endTagStart = scanner.getTokenOffset();
endTagName = undefined;
break;
case vscode_html_languageservice_1.TokenType.EndTag:
endTagName = scanner.getTokenText().toLowerCase();
break;
case vscode_html_languageservice_1.TokenType.EndTagClose:
let node = curr;
// see if we can find a matching tag
while (!node.isSameTag(endTagName) && node.parent) {
node = node.parent;
}
if (node.parent) {
while (curr !== node) {
curr.end = endTagStart;
curr.closed = false;
curr = curr.parent;
}
curr.closed = true;
curr.endTagStart = endTagStart;
curr.end = scanner.getTokenEnd();
curr = curr.parent;
}
break;
case vscode_html_languageservice_1.TokenType.AttributeName: {
pendingAttribute = scanner.getTokenText();
let attributes = curr.attributes;
if (!attributes) {
curr.attributes = attributes = {};
}
attributes[pendingAttribute] = null;
break;
}
case vscode_html_languageservice_1.TokenType.DelimiterAssign: {
const afterAssign = scanner.getTokenEnd();
if (text.charCodeAt(afterAssign) === braceStartCode) {
const result = (0, utils_1.scanMatchingBraces)(text, afterAssign);
restartScannerAt(result.endOffset, vscode_html_languageservice_1.ScannerState.WithinTag);
finishAttribute(afterAssign, result.endOffset);
}
break;
}
case vscode_html_languageservice_1.TokenType.Whitespace: {
const afterWhitespace = scanner.getTokenEnd();
if (text.charCodeAt(afterWhitespace) === braceStartCode) {
// <div a = {...}
if (scanner.getScannerState() === vscode_html_languageservice_1.ScannerState.BeforeAttributeValue) {
const result = (0, utils_1.scanMatchingBraces)(text, afterWhitespace);
restartScannerAt(result.endOffset, vscode_html_languageservice_1.ScannerState.WithinTag);
finishAttribute(afterWhitespace, result.endOffset);
}
else {
// spread or attribute short-hand
parseSpreadOrShorthandAttribute(afterWhitespace);
}
}
break;
}
case vscode_html_languageservice_1.TokenType.AttributeValue:
parseAttributeValue();
break;
case vscode_html_languageservice_1.TokenType.Unknown: {
const tokenOffset = scanner.getTokenOffset();
if (isInsideTagScannerState() &&
text.charCodeAt(tokenOffset) === '/'.charCodeAt(0)) {
const nextCharCode = text.charCodeAt(tokenOffset + 1);
if (nextCharCode === '/'.charCodeAt(0)) {
const newlineOffset = text.indexOf('\n', tokenOffset + 2);
const commentEndOffset = newlineOffset === -1 ? text.length : newlineOffset;
restartScannerAt(commentEndOffset, vscode_html_languageservice_1.ScannerState.WithinTag);
}
else if (nextCharCode === '*'.charCodeAt(0)) {
const blockCommentEnd = text.indexOf('*/', tokenOffset + 2);
const commentEndOffset = blockCommentEnd === -1 ? text.length : blockCommentEnd + 2;
restartScannerAt(commentEndOffset, vscode_html_languageservice_1.ScannerState.WithinTag);
}
}
break;
}
case vscode_html_languageservice_1.TokenType.Content: {
const expressionEnd = skipExpressionInCurrentRange();
if (expressionEnd > scanner.getTokenEnd()) {
restartScannerAt(expressionEnd, vscode_html_languageservice_1.ScannerState.WithinContent);
}
break;
}
}
token = scanner.scan();
}
while (curr.parent) {
curr.end = text.length;
curr.closed = false;
curr = curr.parent;
}
return {
roots: htmlDocument.children,
findNodeBefore: htmlDocument.findNodeBefore.bind(htmlDocument),
findNodeAt: htmlDocument.findNodeAt.bind(htmlDocument)
};
function skipExpressionInCurrentRange() {
const start = scanner.getTokenOffset();
const end = scanner.getTokenEnd();
return skipExpressionInRange(text, start, end);
}
function restartScannerAt(offset, scannerState) {
if (offset <= scanner.getTokenEnd()) {
return;
}
scanner = createScanner(text, offset, scannerState, /* emitPseudoCloseTags*/ true);
}
function finishAttribute(start, end) {
if (!pendingAttribute || !curr.attributes) {
return;
}
curr.attributes[pendingAttribute] = text.substring(start, end);
pendingAttribute = null;
}
function parseSpreadOrShorthandAttribute(startOffset) {
const scanResult = (0, utils_1.scanMatchingBraces)(text, startOffset);
const end = scanResult.endOffset;
restartScannerAt(end, vscode_html_languageservice_1.ScannerState.WithinTag);
const expressionStart = startOffset + 1;
const expressionEnd = end - 1;
const expression = text.substring(expressionStart, expressionEnd).trim();
if (text.substring(expressionStart).startsWith('...')) {
return;
}
curr.attributes ??= {};
curr.attributes[expression] = text.substring(startOffset, end);
}
function parseAttributeValue() {
const quote = text.charCodeAt(scanner.getTokenOffset());
// <a href=a >
if (!isQuote(quote)) {
finishAttribute(scanner.getTokenOffset(), scanner.getTokenEnd());
return;
}
const start = scanner.getTokenOffset();
const tokenEnd = scanner.getTokenEnd();
let expressionTagEnd = skipExpressionInCurrentRange();
if (expressionTagEnd > tokenEnd) {
const indexOfQuote = text.indexOf(String.fromCharCode(quote), expressionTagEnd);
expressionTagEnd = indexOfQuote !== -1 ? indexOfQuote + 1 : text.length;
restartScannerAt(expressionTagEnd, vscode_html_languageservice_1.ScannerState.WithinTag);
}
finishAttribute(start, expressionTagEnd);
}
function isInsideTagScannerState() {
const scannerState = scanner.getScannerState();
return (scannerState === vscode_html_languageservice_1.ScannerState.WithinTag ||
scannerState === vscode_html_languageservice_1.ScannerState.AfterAttributeName ||
scannerState === vscode_html_languageservice_1.ScannerState.BeforeAttributeValue ||
scannerState === vscode_html_languageservice_1.ScannerState.AfterOpeningStartTag);
}
}
function getAttributeContextAtPosition(document, position) {
const offset = document.offsetAt(position);
const { html } = document;
const tag = html.findNodeAt(offset);
if (!inStartTag(offset, tag) || !tag.attributes) {
return null;
}
const text = document.getText();
const beforeStartTagEnd = text.substring(0, tag.startTagEnd);
let scanner = createScanner(beforeStartTagEnd, tag.start);
let token = scanner.scan();
let currentAttributeName;
const inTokenRange = () => scanner.getTokenOffset() <= offset && offset <= scanner.getTokenEnd();
while (token != vscode_html_languageservice_1.TokenType.EOS) {
// adopted from https://github.com/microsoft/vscode-html-languageservice/blob/2f7ae4df298ac2c299a40e9024d118f4a9dc0c68/src/services/htmlCompletion.ts#L402
if (token === vscode_html_languageservice_1.TokenType.AttributeName) {
currentAttributeName = scanner.getTokenText();
if (inTokenRange()) {
return {
elementTag: tag,
name: currentAttributeName,
inValue: false
};
}
}
else if (token === vscode_html_languageservice_1.TokenType.DelimiterAssign) {
const afterAssign = scanner.getTokenEnd();
if (afterAssign === offset && currentAttributeName) {
const nextToken = scanner.scan();
return {
elementTag: tag,
name: currentAttributeName,
inValue: true,
valueRange: [
offset,
nextToken === vscode_html_languageservice_1.TokenType.AttributeValue ? scanner.getTokenEnd() : offset
]
};
}
if (text.charCodeAt(afterAssign) === braceStartCode) {
const scanResult = (0, utils_1.scanMatchingBraces)(text, afterAssign);
restartScannerAt(scanResult.endOffset, vscode_html_languageservice_1.ScannerState.WithinTag);
}
}
else if (token === vscode_html_languageservice_1.TokenType.AttributeValue) {
if (inTokenRange() && currentAttributeName) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenEnd();
if (isQuote(text.charCodeAt(start))) {
start++;
end--;
}
return {
elementTag: tag,
name: currentAttributeName,
inValue: true,
valueRange: [start, end]
};
}
currentAttributeName = undefined;
}
else if (token === vscode_html_languageservice_1.TokenType.Whitespace) {
const afterWhitespace = scanner.getTokenEnd();
if (text.charCodeAt(afterWhitespace) === braceStartCode) {
// <div a = {...}
if (scanner.getScannerState() === vscode_html_languageservice_1.ScannerState.BeforeAttributeValue) {
const scanResult = (0, utils_1.scanMatchingBraces)(text, afterWhitespace);
restartScannerAt(scanResult.endOffset, vscode_html_languageservice_1.ScannerState.WithinTag);
}
else {
// spread or attribute short-hand
parseSpreadOrShorthandAttribute(afterWhitespace);
}
}
}
token = scanner.scan();
function parseSpreadOrShorthandAttribute(startOffset) {
const scanResult = (0, utils_1.scanMatchingBraces)(text, startOffset);
const end = scanResult.endOffset;
restartScannerAt(end, vscode_html_languageservice_1.ScannerState.WithinTag);
}
}
return null;
function restartScannerAt(offset, scannerState) {
if (offset <= scanner.getTokenEnd()) {
return;
}
scanner = createScanner(beforeStartTagEnd, offset, scannerState);
}
}
function isQuote(charCode) {
return charCode === singleQuoteCode || charCode === doubleQuoteCode;
}
function inStartTag(offset, node) {
return offset > node.start && node.startTagEnd != undefined && offset < node.startTagEnd;
}
/**
* adopted from https://github.com/microsoft/vscode-html-languageservice/blob/10daf45dc16b4f4228987cf7cddf3a7dbbdc7570/src/parser/htmlParser.ts
*/
class HTMLNode {
get attributeNames() {
return this.attributes ? Object.keys(this.attributes) : [];
}
constructor(start, end, children, parent) {
this.start = start;
this.end = end;
this.children = children;
this.parent = parent;
this.closed = false;
}
isSameTag(tagInLowerCase) {
if (this.tag === undefined) {
return tagInLowerCase === undefined;
}
else {
return (tagInLowerCase !== undefined &&
this.tag.length === tagInLowerCase.length &&
this.tag.toLowerCase() === tagInLowerCase);
}
}
get firstChild() {
return this.children[0];
}
get lastChild() {
return this.children.length ? this.children[this.children.length - 1] : void 0;
}
findNodeBefore(offset) {
const idx = HTMLNode.findFirst(this.children, (c) => offset <= c.start) - 1;
if (idx >= 0) {
const child = this.children[idx];
if (offset > child.start) {
if (offset < child.end) {
return child.findNodeBefore(offset);
}
const lastChild = child.lastChild;
if (lastChild && lastChild.end === child.end) {
return child.findNodeBefore(offset);
}
return child;
}
}
return this;
}
findNodeAt(offset) {
const idx = HTMLNode.findFirst(this.children, (c) => offset <= c.start) - 1;
if (idx >= 0) {
const child = this.children[idx];
if (offset > child.start && offset <= child.end) {
return child.findNodeAt(offset);
}
}
return this;
}
static findFirst(array, p) {
let low = 0, high = array.length;
if (high === 0) {
return 0; // no children
}
while (low < high) {
let mid = Math.floor((low + high) / 2);
if (p(array[mid])) {
high = mid;
}
else {
low = mid + 1;
}
}
return low;
}
}
exports.HTMLNode = HTMLNode;
function skipExpressionInRange(text, start, end) {
let index = start;
while (index < end) {
if (text.charCodeAt(index) !== braceStartCode) {
index++;
continue;
}
const matchResult = (0, utils_1.scanMatchingBraces)(text, index);
index = matchResult.endOffset;
}
return Math.max(index, end);
}
function scanCommentWithinTextOrComment(text, startOffset, endOffset) {
let scanner = createScanner(text, startOffset, vscode_html_languageservice_1.ScannerState.WithinContent);
let token = scanner.scan();
const results = [];
while (token !== vscode_html_languageservice_1.TokenType.EOS && scanner.getTokenOffset() < endOffset) {
if (token === vscode_html_languageservice_1.TokenType.Comment) {
results.push({ start: scanner.getTokenOffset(), end: scanner.getTokenEnd() });
}
else if (token === vscode_html_languageservice_1.TokenType.Content) {
const expressionEnd = skipExpressionInRange(text, scanner.getTokenOffset(), scanner.getTokenEnd());
if (expressionEnd > scanner.getTokenEnd()) {
scanner = createScanner(text, expressionEnd, vscode_html_languageservice_1.ScannerState.WithinContent);
}
}
else if (token !== vscode_html_languageservice_1.TokenType.StartCommentTag && token !== vscode_html_languageservice_1.TokenType.EndCommentTag) {
break;
}
token = scanner.scan();
}
return results;
}
//# sourceMappingURL=parseHtml.js.map