UNPKG

svelte-language-server

Version:
428 lines 18.8 kB
"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