"use strict";
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.serializeSelector = exports.parseCSS = void 0;
const css = require("./cssTokenizer");
function parseCSS(selector) {
    let tokens;
    try {
        tokens = css.tokenize(selector);
        if (!(tokens[tokens.length - 1] instanceof css.EOFToken))
            tokens.push(new css.EOFToken());
    }
    catch (e) {
        const newMessage = e.message + ` while parsing selector "${selector}"`;
        const index = (e.stack || '').indexOf(e.message);
        if (index !== -1)
            e.stack = e.stack.substring(0, index) + newMessage + e.stack.substring(index + e.message.length);
        e.message = newMessage;
        throw e;
    }
    const unsupportedToken = tokens.find(token => {
        return (token instanceof css.AtKeywordToken) ||
            (token instanceof css.BadStringToken) ||
            (token instanceof css.BadURLToken) ||
            (token instanceof css.ColumnToken) ||
            (token instanceof css.CDOToken) ||
            (token instanceof css.CDCToken) ||
            (token instanceof css.SemicolonToken) ||
            // TODO: Consider using these for something, e.g. to escape complex strings.
            // For example :xpath{ (//div/bar[@attr="foo"])[2]/baz }
            // Or this way :xpath( {complex-xpath-goes-here("hello")} )
            (token instanceof css.OpenCurlyToken) ||
            (token instanceof css.CloseCurlyToken) ||
            // TODO: Consider treating these as strings?
            (token instanceof css.URLToken) ||
            (token instanceof css.PercentageToken);
    });
    if (unsupportedToken)
        throw new Error(`Unsupported token "${unsupportedToken.toSource()}" while parsing selector "${selector}"`);
    let pos = 0;
    function unexpected() {
        return new Error(`Unexpected token "${tokens[pos].toSource()}" while parsing selector "${selector}"`);
    }
    function skipWhitespace() {
        while (tokens[pos] instanceof css.WhitespaceToken)
            pos++;
    }
    function isIdent(p = pos) {
        return tokens[p] instanceof css.IdentToken;
    }
    function isString(p = pos) {
        return tokens[p] instanceof css.StringToken;
    }
    function isNumber(p = pos) {
        return tokens[p] instanceof css.NumberToken;
    }
    function isComma(p = pos) {
        return tokens[p] instanceof css.CommaToken;
    }
    function isCloseParen(p = pos) {
        return tokens[p] instanceof css.CloseParenToken;
    }
    function isStar(p = pos) {
        return (tokens[p] instanceof css.DelimToken) && tokens[p].value === '*';
    }
    function isEOF(p = pos) {
        return tokens[p] instanceof css.EOFToken;
    }
    function isClauseCombinator(p = pos) {
        return (tokens[p] instanceof css.DelimToken) && (['>', '+', '~'].includes(tokens[p].value));
    }
    function isSelectorClauseEnd(p = pos) {
        return isComma(p) || isCloseParen(p) || isEOF(p) || isClauseCombinator(p) || (tokens[p] instanceof css.WhitespaceToken);
    }
    function consumeSelectorList() {
        const result = [consumeSelector()];
        while (true) {
            skipWhitespace();
            if (!isComma())
                break;
            pos++;
            result.push(consumeSelector());
        }
        return result;
    }
    function consumeSelector() {
        skipWhitespace();
        const result = { clauses: [{ clause: consumeSelectorClause(), combinator: '' }] };
        while (true) {
            skipWhitespace();
            if (isClauseCombinator()) {
                result.clauses[result.clauses.length - 1].combinator = tokens[pos++].value;
                skipWhitespace();
            }
            else if (isSelectorClauseEnd()) {
                break;
            }
            result.clauses.push({ combinator: '', clause: consumeSelectorClause() });
        }
        if (result.clauses.length === 1 && typeof result.clauses[0].clause === 'string')
            return result.clauses[0].clause;
        return result;
    }
    function consumeSelectorClause() {
        // TODO: Consider symbols like `=`, `|=`, `~=`, `*=`, `/` and convert them to strings.
        if ((isNumber() || isString() || isStar() || isIdent()) && isSelectorClauseEnd(pos + 1))
            return isString() ? tokens[pos++].value : tokens[pos++].toSource();
        let rawCSSString = '';
        const funcs = [];
        while (!isSelectorClauseEnd()) {
            if (isIdent() || isStar()) {
                rawCSSString += tokens[pos++].toSource();
            }
            else if (tokens[pos] instanceof css.HashToken) {
                rawCSSString += tokens[pos++].toSource();
            }
            else if ((tokens[pos] instanceof css.DelimToken) && tokens[pos].value === '.') {
                pos++;
                if (isIdent())
                    rawCSSString += '.' + tokens[pos++].toSource();
                else
                    throw unexpected();
            }
            else if (tokens[pos] instanceof css.ColonToken) {
                pos++;
                if (isIdent()) {
                    if (cssFilters.has(tokens[pos].value))
                        rawCSSString += ':' + tokens[pos++].toSource();
                    else
                        funcs.push({ name: tokens[pos++].value, args: [] });
                }
                else if (tokens[pos] instanceof css.FunctionToken) {
                    const name = tokens[pos++].value;
                    if (cssFunctions.has(name))
                        rawCSSString += `:${name}(${consumeCSSFunctionArgs()})`;
                    else
                        funcs.push({ name, args: consumeSelectorList() });
                    skipWhitespace();
                    if (!isCloseParen())
                        throw unexpected();
                    pos++;
                }
                else {
                    throw unexpected();
                }
            }
            else if (tokens[pos] instanceof css.OpenSquareToken) {
                rawCSSString += '[';
                pos++;
                while (!(tokens[pos] instanceof css.CloseSquareToken) && !isEOF())
                    rawCSSString += tokens[pos++].toSource();
                if (!(tokens[pos] instanceof css.CloseSquareToken))
                    throw unexpected();
                rawCSSString += ']';
                pos++;
            }
            else {
                throw unexpected();
            }
        }
        if (!rawCSSString && !funcs.length)
            throw unexpected();
        return { css: rawCSSString || undefined, funcs };
    }
    function consumeCSSFunctionArgs() {
        let s = '';
        while (!isCloseParen() && !isEOF())
            s += tokens[pos++].toSource();
        return s;
    }
    const result = consumeSelectorList();
    if (!isEOF())
        throw new Error(`Error while parsing selector "${selector}"`);
    return result;
}
exports.parseCSS = parseCSS;
function serializeSelector(selectorList) {
    return selectorList.map(selector => {
        if (typeof selector === 'string')
            return selector;
        return selector.clauses.map(({ clause, combinator }) => {
            let s = '';
            if (typeof clause === 'string') {
                s = clause;
            }
            else {
                if (clause.css)
                    s = clause.css;
                s = s + clause.funcs.map(func => `:${func.name}(${serializeSelector(func.args)})`).join('');
            }
            if (combinator)
                s += ' ' + combinator;
            return s;
        }).join(' ');
    }).join(', ');
}
exports.serializeSelector = serializeSelector;
const cssFilters = new Set([
    'active', 'any-link', 'checked', 'blank', 'default', 'defined',
    'disabled', 'empty', 'enabled', 'first', 'first-child', 'first-of-type',
    'fullscreen', 'focus', 'focus-visible', 'focus-within', 'hover',
    'indeterminate', 'in-range', 'invalid', 'last-child', 'last-of-type',
    'link', 'only-child', 'only-of-type', 'optional', 'out-of-range', 'placeholder-shown',
    'read-only', 'read-write', 'required', 'root', 'target', 'valid', 'visited',
]);
const cssFunctions = new Set([
    'dir', 'lang', 'nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type',
]);
//# sourceMappingURL=cssParser.js.map