"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