UNPKG

svelte-language-server

Version:
559 lines 20.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.extractScriptTags = extractScriptTags; exports.extractStyleTag = extractStyleTag; exports.extractStyleTagAtOffset = extractStyleTagAtOffset; exports.extractTemplateTag = extractTemplateTag; exports.positionAt = positionAt; exports.offsetAt = offsetAt; exports.getLineOffsets = getLineOffsets; exports.isInTag = isInTag; exports.isRangeInTag = isRangeInTag; exports.getTextInRange = getTextInRange; exports.getLineAtPosition = getLineAtPosition; exports.isAtEndOfLine = isAtEndOfLine; exports.updateRelativeImport = updateRelativeImport; exports.getNodeIfIsInComponentStartTag = getNodeIfIsInComponentStartTag; exports.getNodeIfIsInHTMLStartTag = getNodeIfIsInHTMLStartTag; exports.getNodeIfIsInTagName = getNodeIfIsInTagName; exports.getNodeIfIsInStartTag = getNodeIfIsInStartTag; exports.isInHTMLTagRange = isInHTMLTagRange; exports.getWordRangeAt = getWordRangeAt; exports.getWordAt = getWordAt; exports.toRange = toRange; exports.getLangAttribute = getLangAttribute; exports.inStyleOrScript = inStyleOrScript; exports.scanMatchingBraces = scanMatchingBraces; exports.isInsideMoustacheTag = isInsideMoustacheTag; const utils_1 = require("../../utils"); const vscode_languageserver_1 = require("vscode-languageserver"); const path = __importStar(require("path")); const parseHtml_1 = require("./parseHtml"); function parseAttributes(rawAttrs) { const attrs = {}; if (!rawAttrs) { return attrs; } Object.keys(rawAttrs).forEach((attrName) => { const attrValue = rawAttrs[attrName]; attrs[attrName] = attrValue === null ? attrName : removeOuterQuotes(attrValue); }); return attrs; function removeOuterQuotes(attrValue) { if ((attrValue.startsWith('"') && attrValue.endsWith('"')) || (attrValue.startsWith("'") && attrValue.endsWith("'"))) { return attrValue.slice(1, attrValue.length - 1); } return attrValue; } } const regexIf = new RegExp('{\\s*#if\\s.*?}', 'gms'); const regexIfEnd = new RegExp('{\\s*/if}', 'gms'); const regexEach = new RegExp('{\\s*#each\\s.*?}', 'gms'); const regexEachEnd = new RegExp('{\\s*/each}', 'gms'); const regexAwait = new RegExp('{\\s*#await\\s.*?}', 'gms'); const regexAwaitEnd = new RegExp('{\\s*/await}', 'gms'); const regexHtml = new RegExp('{\\s*@html\\s.*?', 'gms'); /** * Extracts a tag (style or script) from the given text * and returns its start, end and the attributes on that tag. * @param text text content to extract tag from * @param tag the tag to extract */ function extractTags(text, tag, html) { const rootNodes = html?.roots || (0, parseHtml_1.parseHtml)(text).roots; const matchedNodes = rootNodes .filter((node) => node.tag === tag) .filter((tag) => { return isNotInsideControlFlowTag(tag) && isNotInsideHtmlTag(tag); }); return matchedNodes.map((node) => transformToTagInfo(node, text)); /** * For every match AFTER the tag do a search for `{/X`. * If that is BEFORE `{#X`, we are inside a moustache tag. */ function isNotInsideControlFlowTag(tag) { const tagIndex = rootNodes.indexOf(tag); // Quick check: if the tag has nothing before it, it can't be inside a control flow tag // This also works around a case where the tag is treated as under a control flow tag when vscode-html-languageservice parses something wrong if (tagIndex === 0) { const startContent = text.substring(0, tag.start); if (startContent.trim() === '') { return true; } } const nodes = rootNodes.slice(tagIndex); const rootContentAfterTag = nodes .map((node, idx) => { const start = node.startTagEnd ? node.end : node.start + (node.tag?.length || 0); return text.substring(start, nodes[idx + 1]?.start); }) .join(''); return ![ [regexIf, regexIfEnd], [regexEach, regexEachEnd], [regexAwait, regexAwaitEnd] ].some((pair) => { pair[0].lastIndex = 0; pair[1].lastIndex = 0; const start = pair[0].exec(rootContentAfterTag); const end = pair[1].exec(rootContentAfterTag); return (end?.index ?? text.length) < (start?.index ?? text.length); }); } /** * For every match BEFORE the tag do a search for `{@html`. * If that is BEFORE `}`, we are inside a moustache tag. */ function isNotInsideHtmlTag(tag) { const nodes = rootNodes.slice(0, rootNodes.indexOf(tag)); const rootContentBeforeTag = [{ start: 0, end: 0 }, ...nodes] .map((node, idx) => { return text.substring(node.end, nodes[idx]?.start); }) .join(''); return !((0, utils_1.regexLastIndexOf)(rootContentBeforeTag, regexHtml) > rootContentBeforeTag.lastIndexOf('}')); } } function transformToTagInfo(matchedNode, text) { const start = matchedNode.startTagEnd ?? matchedNode.start; const end = matchedNode.endTagStart ?? matchedNode.end; const startPos = positionAt(start, text); const endPos = positionAt(end, text); const container = { start: matchedNode.start, end: matchedNode.end }; const content = text.substring(start, end); return { content, attributes: parseAttributes(matchedNode.attributes), start, end, startPos, endPos, container }; } function extractScriptTags(source, html) { const scripts = extractTags(source, 'script', html); if (!scripts.length) { return null; } const script = scripts.find((s) => s.attributes['context'] !== 'module' && !('module' in s.attributes)); const moduleScript = scripts.find((s) => s.attributes['context'] === 'module' || 'module' in s.attributes); return { script, moduleScript }; } function extractStyleTag(source, html) { const styles = extractTags(source, 'style', html); if (!styles.length) { return null; } // There can only be one style tag return styles[0]; } function extractStyleTagAtOffset(source, offset, html) { const node = html.findNodeAt(offset); if (node.tag !== 'style') { return null; } const styleInfo = transformToTagInfo(node, source); if (offset < styleInfo.start || offset > styleInfo.end) { return null; } return styleInfo; } function extractTemplateTag(source, html) { const templates = extractTags(source, 'template', html); if (!templates.length) { return null; } // There should only be one style tag return templates[0]; } /** * Get the line and character based on the offset * @param offset The index of the position * @param text The text for which the position should be retrived * @param lineOffsets number Array with offsets for each line. Computed if not given */ function positionAt(offset, text, lineOffsets = getLineOffsets(text)) { offset = (0, utils_1.clamp)(offset, 0, text.length); let low = 0; let high = lineOffsets.length; if (high === 0) { return vscode_languageserver_1.Position.create(0, offset); } while (low <= high) { const mid = Math.floor((low + high) / 2); const lineOffset = lineOffsets[mid]; if (lineOffset === offset) { return vscode_languageserver_1.Position.create(mid, 0); } else if (offset > lineOffset) { low = mid + 1; } else { high = mid - 1; } } // low is the least x for which the line offset is larger than the current offset // or array.length if no line offset is larger than the current offset const line = low - 1; return vscode_languageserver_1.Position.create(line, offset - lineOffsets[line]); } /** * Get the offset of the line and character position * @param position Line and character position * @param text The text for which the offset should be retrived * @param lineOffsets number Array with offsets for each line. Computed if not given */ function offsetAt(position, text, lineOffsets = getLineOffsets(text)) { if (position.line >= lineOffsets.length) { return text.length; } else if (position.line < 0) { return 0; } const lineOffset = lineOffsets[position.line]; const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length; return (0, utils_1.clamp)(nextLineOffset, lineOffset, lineOffset + position.character); } function getLineOffsets(text) { const lineOffsets = []; let isLineStart = true; for (let i = 0; i < text.length; i++) { if (isLineStart) { lineOffsets.push(i); isLineStart = false; } const ch = text.charAt(i); isLineStart = ch === '\r' || ch === '\n'; if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') { i++; } } if (isLineStart && text.length > 0) { lineOffsets.push(text.length); } return lineOffsets; } function isInTag(position, tagInfo) { return !!tagInfo && (0, utils_1.isInRange)(vscode_languageserver_1.Range.create(tagInfo.startPos, tagInfo.endPos), position); } function isRangeInTag(range, tagInfo) { return isInTag(range.start, tagInfo) && isInTag(range.end, tagInfo); } function getTextInRange(range, text) { return text.substring(offsetAt(range.start, text), offsetAt(range.end, text)); } function getLineAtPosition(position, text) { return text.substring(offsetAt({ line: position.line, character: 0 }, text), offsetAt({ line: position.line, character: Number.MAX_VALUE }, text)); } /** * Assumption: Is called with a line. A line does only contain line break characters * at its end. */ function isAtEndOfLine(line, offset) { return [undefined, '\r', '\n'].includes(line[offset]); } /** * Updates a relative import * * @param oldPath Old absolute path * @param newPath New absolute path * @param relativeImportPath Import relative to the old path */ function updateRelativeImport(oldPath, newPath, relativeImportPath) { let newImportPath = path .join(path.relative(newPath, oldPath), relativeImportPath) .replace(/\\/g, '/'); if (!newImportPath.startsWith('.')) { newImportPath = './' + newImportPath; } return newImportPath; } /** * Returns the node if offset is inside a component's starttag */ function getNodeIfIsInComponentStartTag(html, document, offset) { const node = html.findNodeAt(offset); if (!!node.tag && (node.tag[0] === node.tag[0].toUpperCase() || (document.isSvelte5 && node.tag.includes('.'))) && (!node.startTagEnd || offset < node.startTagEnd)) { return node; } } /** * Returns the node if offset is inside a HTML starttag */ function getNodeIfIsInHTMLStartTag(html, offset) { const node = html.findNodeAt(offset); if (!!node.tag && node.tag[0] === node.tag[0].toLowerCase() && (!node.startTagEnd || offset < node.startTagEnd)) { return node; } } /** * Returns the node if offset is on the actual tag name (start or end) */ function getNodeIfIsInTagName(html, offset) { const node = html.findNodeAt(offset); if (!!node.tag && node.tag[0] === node.tag[0].toLowerCase() && (offset <= node.start + 1 + (node.tag.length ?? 0) || (node.endTagStart && offset >= node.endTagStart && offset <= node.end))) { return node; } } /** * Returns the node if offset is inside a starttag (HTML or component) */ function getNodeIfIsInStartTag(html, offset) { const node = html.findNodeAt(offset); if (!!node.tag && (!node.startTagEnd || offset < node.startTagEnd)) { return node; } } /** * Returns `true` if `offset` is a html tag and within the name of the start tag or end tag */ function isInHTMLTagRange(html, offset) { const node = html.findNodeAt(offset); return (!!node.tag && node.tag[0] === node.tag[0].toLowerCase() && (node.start + node.tag.length + 1 >= offset || (!!node.endTagStart && node.endTagStart <= offset))); } /** * Gets word range at position. * Delimiter is by default a whitespace, but can be adjusted. */ function getWordRangeAt(str, pos, delimiterRegex = { left: /\S+$/, right: /\s/ }) { let start = str.slice(0, pos).search(delimiterRegex.left); if (start < 0) { start = pos; } let end = str.slice(pos).search(delimiterRegex.right); if (end < 0) { end = str.length; } else { end = end + pos; } return { start, end }; } /** * Gets word at position. * Delimiter is by default a whitespace, but can be adjusted. */ function getWordAt(str, pos, delimiterRegex = { left: /\S+$/, right: /\s/ }) { const { start, end } = getWordRangeAt(str, pos, delimiterRegex); return str.slice(start, end); } function toRange(str, start, end) { if (typeof str === 'string') { return vscode_languageserver_1.Range.create(positionAt(start, str), positionAt(end, str)); } return vscode_languageserver_1.Range.create(str.positionAt(start), str.positionAt(end)); } /** * Returns the language from the given tags, return the first from which a language is found. * Searches inside lang and type and removes leading 'text/' */ function getLangAttribute(...tags) { const tag = tags.find((tag) => tag?.attributes.lang || tag?.attributes.type); if (!tag) { return null; } const attribute = tag.attributes.lang || tag.attributes.type; if (!attribute) { return null; } return attribute.replace(/^text\//, ''); } function inStyleOrScript(document, position) { return (isInTag(position, document.styleInfo) || isInTag(position, document.scriptInfo) || isInTag(position, document.moduleScriptInfo)); } const backtickCode = '`'.charCodeAt(0); const braceStartCode = '{'.charCodeAt(0); const braceEndCode = '}'.charCodeAt(0); const singleQuoteCode = "'".charCodeAt(0); const doubleQuoteCode = '"'.charCodeAt(0); const forwardSlashCode = '/'.charCodeAt(0); const starCode = '*'.charCodeAt(0); const crCode = '\r'.charCodeAt(0); const lfCode = '\n'.charCodeAt(0); const escapeCode = 92; // '\' const dollarCode = '$'.charCodeAt(0); /** * Matches until braces are balanced using a simple parsing logic. * Should only be called when positioned at an opening brace. */ function scanMatchingBraces(html, startOffset) { if (html.charCodeAt(startOffset) !== braceStartCode) { return { terminated: true, endOffset: startOffset }; } let depth = 0; let templateStack; let index = startOffset; while (index < html.length) { const char = html.charCodeAt(index); switch (char) { case braceStartCode: depth++; break; case braceEndCode: if (depth > 0) { depth--; } if (depth === 0 && templateStack !== undefined && templateStack.length > 0) { depth = templateStack.pop() || 0; scanTemplateString(); } break; case singleQuoteCode: case doubleQuoteCode: { index++; scanString(char); break; } case backtickCode: index++; scanTemplateString(); break; case forwardSlashCode: { // / const nextChar = html.charCodeAt(index + 1); if (nextChar === forwardSlashCode) { skipToNewLine(); } else if (nextChar === starCode) { index += 2; // skip /* skipToEndOfMultiLineComment(); } // Theoretically it could be a regex here. But it clashes with an end block and self-close tag. // There is also /[/]/ that makes it hard to do a simple scan. So we skip regex handling for now. break; } } index++; if (depth === 0 && (templateStack === undefined || templateStack.length === 0)) { return { terminated: true, endOffset: index }; } } return { terminated: false, endOffset: index }; function scanString(quote) { while (index < html.length) { const char = html.charCodeAt(index); if (char === quote || char == lfCode) { return; } if (char === escapeCode) { index += 2; continue; } index++; } } function scanTemplateString() { while (index < html.length) { const char = html.charCodeAt(index); switch (char) { case backtickCode: return; case dollarCode: // $ if (html.charCodeAt(index + 1) === braceStartCode) { templateStack = templateStack || []; templateStack.push(depth); depth = 0; return; } break; case escapeCode: // \ // skip next character index++; break; } index++; } return; } function skipToNewLine() { while (index < html.length) { const char = html.charCodeAt(index); if (char === crCode || char === lfCode) { return; } index++; } } function skipToEndOfMultiLineComment() { while (index < html.length) { const char = html.charCodeAt(index); if (char === starCode && html.charCodeAt(index + 1) === forwardSlashCode) { index += 2; return; } index++; } } } function isInsideMoustacheTag(text, tagStart, offset) { const firstBraceIndex = text.indexOf('{', tagStart); if (firstBraceIndex > offset) { return false; } let index = firstBraceIndex; while (index < offset) { if (text.charCodeAt(index) !== braceStartCode) { index++; continue; } const result = scanMatchingBraces(text, index); if (!result.terminated) { return true; } index = result.endOffset; } return index > offset; } //# sourceMappingURL=utils.js.map