"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeBlockHighlight = void 0;
const prosemirror_state_1 = require("prosemirror-state");
// @ts-ignore // TODO: fix cjs build
const prosemirror_utils_1 = require("prosemirror-utils");
const prosemirror_view_1 = require("prosemirror-view");
const lodash_1 = require("../../../../lodash.js");
const logger_1 = require("../../../../logger.js");
const CodeBlockSpecs_1 = require("../CodeBlockSpecs/index.js");
const CodeBlockNodeView_1 = require("./CodeBlockNodeView.js");
const TooltipPlugin_1 = require("./TooltipPlugin/index.js");
const const_1 = require("./const.js");
const codeBlockLineNumbersPlugin_1 = require("./plugins/codeBlockLineNumbersPlugin.js");
const codeBlockLineWrappingPlugin_1 = require("./plugins/codeBlockLineWrappingPlugin.js");
require("./CodeBlockHighlight.css");
const key = new prosemirror_state_1.PluginKey('code_block_highlight');
const CodeBlockHighlight = (builder, opts) => {
    let langs;
    let lowlight;
    let hljs;
    const loadModules = async () => {
        try {
            hljs = (await import('highlight.js/lib/core')).default;
            const low = await import('lowlight');
            const all = low.all;
            const create = low.createLowlight;
            langs = { ...all, ...opts.langs };
            lowlight = create(langs);
            return true;
        }
        catch (e) {
            logger_1.globalLogger.info('Skip code_block highlighting');
            builder.logger.log('Skip code_block highlighting');
            return false;
        }
    };
    if (opts.lineWrapping?.enabled)
        builder.addPlugin(codeBlockLineWrappingPlugin_1.codeBlockLineWrappingPlugin);
    if (opts.lineNumbers?.enabled)
        builder.addPlugin(codeBlockLineNumbersPlugin_1.codeBlockLineNumbersPlugin);
    builder.addPlugin(() => {
        let modulesLoaded = false;
        let view = null;
        // empty array by default, but is filled after loading modules
        const selectItems = [];
        const mapping = {};
        // TODO: add TAB key handler
        // TODO: Remove constant selection of block
        return new prosemirror_state_1.Plugin({
            key,
            state: {
                init: (_, state) => {
                    loadModules().then((loaded) => {
                        modulesLoaded = loaded;
                        if (modulesLoaded) {
                            for (const lang of Object.keys(langs)) {
                                const defs = langs[lang](hljs);
                                selectItems.push({
                                    value: lang,
                                    content: defs.name || (0, lodash_1.capitalize)(lang),
                                });
                                if (defs.aliases) {
                                    for (const alias of defs.aliases) {
                                        mapping[alias] = lang;
                                    }
                                }
                            }
                            selectItems.sort(sortLangs);
                            if (view && !view.isDestroyed) {
                                view.dispatch(view.state.tr.setMeta(key, { modulesLoaded }));
                            }
                        }
                    });
                    return getDecorations(state.doc);
                },
                apply: (tr, decos, oldState, newState) => {
                    if (!modulesLoaded) {
                        return prosemirror_view_1.DecorationSet.empty;
                    }
                    if (tr.getMeta(key)?.modulesLoaded) {
                        return getDecorations(tr.doc);
                    }
                    if (tr.docChanged) {
                        const oldNodeName = oldState.selection.$head.parent.type.name;
                        const newNodeName = newState.selection.$head.parent.type.name;
                        // Apply decorations if:
                        if (oldNodeName === CodeBlockSpecs_1.codeBlockNodeName ||
                            newNodeName === CodeBlockSpecs_1.codeBlockNodeName) {
                            // selection includes codeblock node,
                            return getDecorations(tr.doc);
                        }
                        else {
                            const oldNodes = (0, prosemirror_utils_1.findChildrenByType)(oldState.doc, (0, CodeBlockSpecs_1.codeBlockType)(oldState.schema));
                            const newNodes = (0, prosemirror_utils_1.findChildrenByType)(newState.doc, (0, CodeBlockSpecs_1.codeBlockType)(newState.schema));
                            if (
                            // OR transaction adds/removes codeblock nodes,
                            newNodes.length !== oldNodes.length ||
                                // OR transaction has changes that completely encapsulate a node
                                // (for example, a transaction that affects the entire document).
                                // Such transactions can happen during collab syncing via y-prosemirror, for example.
                                tr.steps.some((step) => {
                                    return (stepHasFromTo(step) &&
                                        oldNodes.some((node) => node.pos >= step.from &&
                                            node.pos + node.node.nodeSize <= step.to));
                                })) {
                                return getDecorations(tr.doc);
                            }
                        }
                    }
                    return decos.map(tr.mapping, tr.doc);
                },
            },
            view: (v) => {
                view = v;
                return (0, TooltipPlugin_1.codeLangSelectTooltipViewCreator)(view, selectItems, mapping, {
                    showCodeWrapping: Boolean(opts.lineWrapping?.enabled),
                    showLineNumbers: Boolean(opts.lineNumbers?.enabled),
                });
            },
            props: {
                decorations: (state) => {
                    return key.getState(state);
                },
                nodeViews: {
                    [CodeBlockSpecs_1.codeBlockNodeName]: CodeBlockNodeView_1.CodeBlockNodeView.withOpts(opts),
                },
            },
        });
    });
    function getDecorations(doc) {
        const decos = [];
        if (!lowlight) {
            return prosemirror_view_1.DecorationSet.empty;
        }
        for (const { node, pos } of (0, prosemirror_utils_1.findChildrenByType)(doc, (0, CodeBlockSpecs_1.codeBlockType)(doc.type.schema), true)) {
            let from = pos + 1;
            let nodes;
            const lang = node.attrs[CodeBlockSpecs_1.CodeBlockNodeAttr.Lang];
            if (lang && lowlight.registered(lang)) {
                nodes = lowlight.highlight(lang, node.textContent).children;
            }
            else {
                continue;
            }
            for (const { text, classes } of parseNodes(nodes)) {
                const to = from + text.length;
                if (classes.length) {
                    decos.push(prosemirror_view_1.Decoration.inline(from, to, {
                        class: classes.join(' '),
                    }));
                }
                from = to;
            }
        }
        return prosemirror_view_1.DecorationSet.create(doc, decos);
    }
};
exports.CodeBlockHighlight = CodeBlockHighlight;
function parseNodes(nodes, className = []) {
    return nodes
        .map((node) => {
        let classes = className;
        if (node.type === 'element') {
            classes = classes.concat(node.properties.className ?? []);
            return parseNodes(node.children, classes);
        }
        return {
            text: node.type === 'comment' || node.type === 'text' ? node.value : '',
            classes,
        };
    })
        .flat();
}
function stepHasFromTo(step) {
    // @ts-expect-error
    return typeof step.from === 'number' && typeof step.to === 'number';
}
function sortLangs(a, b) {
    // plaintext always goes first
    if (a.value === const_1.PlainTextLang)
        return -1;
    if (b.value === const_1.PlainTextLang)
        return 1;
    return 0;
}
//# sourceMappingURL=CodeBlockHighlight.js.map
