UNPKG

svelte-language-server

Version:
410 lines 15.5 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.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.getNodeIfIsInStartTag = getNodeIfIsInStartTag; exports.isInHTMLTagRange = isInHTMLTagRange; exports.getWordRangeAt = getWordRangeAt; exports.getWordAt = getWordAt; exports.toRange = toRange; exports.getLangAttribute = getLangAttribute; exports.isInsideMoustacheTag = isInsideMoustacheTag; exports.inStyleOrScript = inStyleOrScript; 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(transformToTagInfo); /** * 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) { 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 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 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\//, ''); } /** * Checks whether given position is inside a moustache tag (which includes control flow tags) * using a simple bracket matching heuristic which might fail under conditions like * `{#if {a: true}.a}` */ function isInsideMoustacheTag(html, tagStart, position) { if (tagStart === null) { // Not inside <tag ... > const charactersBeforePosition = html.substring(0, position); return (Math.max( // TODO make this just check for '{'? // Theoretically, someone could do {a < b} in a simple moustache tag charactersBeforePosition.lastIndexOf('{#'), charactersBeforePosition.lastIndexOf('{:'), charactersBeforePosition.lastIndexOf('{@')) > charactersBeforePosition.lastIndexOf('}')); } else { // Inside <tag ... > const charactersInNode = html.substring(tagStart, position); return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}'); } } function inStyleOrScript(document, position) { return (isInTag(position, document.styleInfo) || isInTag(position, document.scriptInfo) || isInTag(position, document.moduleScriptInfo)); } //# sourceMappingURL=utils.js.map