"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