import { Mark } from 'prosemirror-model';
import { logger } from "../../logger.js";
import { ProseMirrorTransformer } from "./ProseMirrorTransformer/index.js";
const openSuffix = '_open';
const closeSuffix = '_close';
var TokenType;
(function (TokenType) {
    TokenType["open"] = "open";
    TokenType["close"] = "close";
    TokenType["default"] = "default";
})(TokenType || (TokenType = {}));
/**
 * Remove suffixes from the node name.
 * Crops specified `openSuffix` and `closeSuffix` from the end of the given `tokName`.
 */
function cropNodeName(tokName, openSuffix, closeSuffix) {
    const regex = new RegExp(`(${openSuffix})$|(${closeSuffix})$`);
    return tokName.replace(regex, '');
}
export class MarkdownParser {
    schema;
    stack = [];
    marks;
    tokens;
    tokenizer;
    pmTransformers;
    dynamicModifier;
    logger;
    constructor(schema, tokenizer, tokens, opts) {
        this.schema = schema;
        this.marks = Mark.none;
        this.tokens = tokens;
        this.tokenizer = tokenizer;
        this.pmTransformers = opts.pmTransformers;
        this.dynamicModifier = opts.dynamicModifier ?? null;
        this.logger = opts.logger.nested({ module: 'markdown-parser' });
    }
    validateLink(url) {
        return this.tokenizer.validateLink(url);
    }
    normalizeLink(url) {
        return this.tokenizer.normalizeLink(url);
    }
    normalizeLinkText(url) {
        return this.tokenizer.normalizeLinkText(url);
    }
    matchLinks(text) {
        return this.tokenizer.linkify.match(text);
    }
    parse(text) {
        const time = Date.now();
        try {
            this.stack = [{ type: this.schema.topNodeType, content: [] }];
            let mdItTokens;
            try {
                mdItTokens = this.tokenizer.parse(text, {});
                if (this.dynamicModifier) {
                    mdItTokens = this.dynamicModifier.processTokens(mdItTokens, text);
                }
            }
            catch (err) {
                const e = err;
                e.message = 'Unable to parse your markup. Please check for errors. ' + e.message;
                this.logger.error(e);
                throw e;
            }
            this.parseTokens(mdItTokens);
            let doc;
            // If after parsing there are still unclosed nodes, close them by removing them from the stack.
            do {
                doc = this.closeNode();
            } while (this.stack.length);
            const pmTransformer = new ProseMirrorTransformer(this.pmTransformers);
            return doc ? pmTransformer.transform(doc) : this.schema.topNodeType.createAndFill();
        }
        finally {
            logger.metrics({ component: 'parser', event: 'parse', duration: Date.now() - time });
            this.logger.metrics({ component: 'parser', event: 'parse', duration: Date.now() - time });
        }
    }
    top() {
        return this.stack[this.stack.length - 1];
    }
    push(elt) {
        if (this.stack.length)
            this.top().content.push(elt);
    }
    //#region helpers
    getTokenAttrs(token, tokenSpec, tokenStream, i) {
        let attrs = {};
        if (tokenSpec.getAttrs) {
            attrs = tokenSpec.getAttrs(token, tokenStream, i);
        }
        else if (tokenSpec.attrs instanceof Function) {
            attrs = tokenSpec.attrs(token);
        }
        else {
            attrs = tokenSpec.attrs;
        }
        if (this.dynamicModifier) {
            attrs = this.dynamicModifier.processAttrs(token, attrs ?? {});
        }
        return attrs;
    }
    getTokenSpec(token) {
        // Matching the pm-node meta-attribute which is set in the plugin for markdown-it
        let tokName = token.meta?.['pm-node'] || token.type;
        // Cropping _open and _close from node name end
        // e.g. paragraph_open -> paragraph
        tokName = cropNodeName(tokName, openSuffix, closeSuffix);
        let tokenSpec;
        if (tokName in this.tokens) {
            tokenSpec = this.tokens[tokName];
        }
        if (!tokenSpec) {
            throw new RangeError(`No token spec for token: ${JSON.stringify(token)}`);
        }
        return tokenSpec;
    }
    // Getting token type from its name
    // e.g. "paragraph_open" - is opening token for paragraph
    getTokenType({ type }) {
        if (type.endsWith(openSuffix))
            return TokenType.open;
        if (type.endsWith(closeSuffix))
            return TokenType.close;
        return TokenType.default;
    }
    //#endregion
    //#region token handlers
    // Self-explanatory
    handlePrimitiveToken(token) {
        switch (token.type) {
            case 'text':
                this.addText(withoutTrailingNewline(token.content));
                return true;
            case 'inline':
                this.parseTokens(token.children || []);
                return true;
            case 'softbreak':
                // TODO: move all primitive token's handlers to extensions
                if (this.tokens.softbreak)
                    return false;
                this.addText('\n');
                return true;
        }
        return false;
    }
    handleMark(token, tokenSpec, attrs) {
        const schemaSpec = this.schema.marks[tokenSpec.name];
        if (tokenSpec.noCloseToken) {
            this.openMark(schemaSpec.create(attrs));
            let { content } = token;
            if (!tokenSpec.code)
                content = withoutTrailingNewline(content);
            this.addText(content);
            this.closeMark(schemaSpec);
            return;
        }
        switch (this.getTokenType(token)) {
            case 'open': {
                this.openMark(schemaSpec.create(attrs));
                break;
            }
            case 'close': {
                this.closeMark(schemaSpec);
                break;
            }
        }
    }
    getNodeSchema(tokenSpec) {
        const spec = this.schema.nodes[tokenSpec.name];
        return spec;
    }
    handleNode(_token, tokenSpec, attrs) {
        const schemaSpec = this.getNodeSchema(tokenSpec);
        // Adding node as is, becasuse it doesn't contain content.
        this.addNode(schemaSpec, attrs);
    }
    handleBlock(token, tokenSpec, attrs) {
        const schemaSpec = this.getNodeSchema(tokenSpec);
        if (tokenSpec.noCloseToken) {
            this.openNode(schemaSpec, attrs);
            if (tokenSpec.contentField === 'children' && token.children?.length) {
                this.parseTokens(token.children);
            }
            else {
                let { content } = token;
                if (tokenSpec.prepareContent) {
                    content = tokenSpec.prepareContent(content);
                }
                this.addText(content);
            }
            this.closeNode();
            return;
        }
        switch (this.getTokenType(token)) {
            case 'open': {
                this.openNode(schemaSpec, attrs);
                break;
            }
            case 'close': {
                this.closeNode();
                break;
            }
        }
    }
    handleToken(token, tokenSpec, attrs) {
        switch (tokenSpec.type) {
            case 'mark':
                this.handleMark(token, tokenSpec, attrs);
                break;
            case 'node':
                this.handleNode(token, tokenSpec, attrs);
                break;
            case 'block':
                this.handleBlock(token, tokenSpec, attrs);
                break;
        }
    }
    //#endregion
    //#region state methods
    addNode(type, attrs, content) {
        let node = type.createAndFill(attrs, content, this.marks);
        if (!node) {
            return null;
        }
        if (this.dynamicModifier) {
            node = this.dynamicModifier.processNodes(node);
        }
        this.push(node);
        return node;
    }
    openNode(type, attrs) {
        this.stack.push({ type: type, attrs, content: [] });
    }
    closeNode() {
        // Marks operate within a node. Therefore, when we close the node, we reset the existing marks.
        if (this.marks.length) {
            this.marks = Mark.none;
        }
        const info = this.stack.pop();
        if (info) {
            return this.addNode(info.type, info.attrs, info.content);
        }
        return null;
    }
    addText(text) {
        if (!text)
            return;
        const nodes = this.top().content;
        const last = nodes[nodes.length - 1];
        const node = this.schema.text(text, this.marks);
        let merged;
        if (last && (merged = maybeMerge(last, node)))
            nodes[nodes.length - 1] = merged;
        else
            nodes.push(node);
    }
    // Adding mark to a set of open marks
    openMark(mark) {
        this.marks = mark.addToSet(this.marks);
    }
    closeMark(mark) {
        this.marks = mark.removeFromSet(this.marks);
    }
    //#endregion
    parseTokens(tokens) {
        for (let i = 0; i < tokens.length; i++) {
            const token = tokens[i];
            if (this.handlePrimitiveToken(token))
                continue;
            const tokenSpec = this.getTokenSpec(token);
            if (tokenSpec.ignore)
                continue;
            const attrs = this.getTokenAttrs(token, tokenSpec, tokens, i);
            this.handleToken(token, tokenSpec, attrs);
        }
    }
}
// Checking if these are two text nodes and they have the same marks, then it merges them into one.
function maybeMerge(a, b) {
    if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks)) {
        // Somehow this method is not in typings but it exists.
        // @ts-expect-error
        return a.withText(a.text + b.text);
    }
}
function withoutTrailingNewline(str) {
    return str[str.length - 1] === '\n' || str.endsWith('\\n') ? str.slice(0, str.length - 1) : str;
}
/** @internal */
export class MarkdownParserDynamicModifier {
    elementProcessors;
    constructor(config) {
        this.elementProcessors = new Map(Object.entries(config));
    }
    processTokens(tokens, rawMarkup) {
        return tokens.map((token, index) => {
            const processor = this.elementProcessors.get(cropNodeName(token.type, openSuffix, ''));
            if (!processor || !processor.processToken || processor.processToken.length === 0) {
                return token;
            }
            return processor.processToken.reduce((currentToken, process) => {
                return process(currentToken, index, rawMarkup);
            }, token);
        });
    }
    processAttrs(token, attrs) {
        const processor = this.elementProcessors.get(cropNodeName(token.type, openSuffix, ''));
        if (!processor || !processor.processNodeAttrs || processor.processNodeAttrs.length === 0) {
            return attrs;
        }
        return processor.processNodeAttrs.reduce((currentAttrs, process) => {
            return process(token, currentAttrs);
        }, attrs);
    }
    processNodes(node) {
        const processor = this.elementProcessors.get(node.type.name);
        if (!processor || !processor.processNode || processor.processNode.length === 0) {
            return node;
        }
        return processor.processNode.reduce((currentNode, process) => {
            return process(currentNode);
        }, node);
    }
}
//# sourceMappingURL=MarkdownParser.js.map
