"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.highlightCodeBlock = highlightCodeBlock;
exports.setCodeLineNumbers = setCodeLineNumbers;
const html_entities_1 = require("html-entities");
const cheerio_1 = __importDefault(require("cheerio"));
const has_1 = __importDefault(require("lodash/has"));
const markdown_it_1 = __importDefault(require("../lib/markdown-it"));
const _ = {
    has: has_1.default,
};
/**
 * Traverses a line part and applies highlighting if necessary.
 * @param node The node of the line part to be traversed
 * @param hlStart The highlight start position, relative to the start of the line part
 * @param hlEnd The highlight end position, relative to the start of the line part
 * @param color Optional color for the highlight
 * @returns  An object that contains data to be used by the node's parent.
 */
function traverseLinePart(node, hlStart, hlEnd, color) {
    const resData = {
        numCharsTraversed: 0,
        shouldParentHighlight: false,
        highlightRange: undefined,
    };
    if (hlEnd <= 0) {
        // Highlight end has passed, no need to traverse further
        return resData;
    }
    if (node.type === 'text') {
        /*
         * Node is a text node. It is not an inherent HTML element of its own,
         * so to actually highlight this text, we have to ask to apply at its parent.
         */
        const cleanedText = (0, html_entities_1.decode)(node.data);
        const textLength = cleanedText.length;
        resData.numCharsTraversed = textLength;
        if (hlStart >= textLength) {
            // Highlight start is not in this text
            resData.shouldParentHighlight = false;
            return resData;
        }
        if (hlStart <= 0 && hlEnd >= textLength) {
            // Highlight spans across the entirety of text
            resData.shouldParentHighlight = true;
            return resData;
        }
        // Partial text highlighting
        resData.shouldParentHighlight = true;
        resData.highlightRange = [hlStart, hlEnd];
        return resData;
    }
    if (!node.children) {
        return resData;
    }
    /*
     * The remaining possibility is that node is a tag node.
     * It has at least one child (to contain the text content).
     * It may have more children, such as inner tag nodes.
     */
    const highlightData = node.children.map((child) => {
        const [relativeHlStart, relativeHlEnd] = [hlStart, hlEnd].map(x => x - resData.numCharsTraversed);
        const data = traverseLinePart(child, relativeHlStart, relativeHlEnd, color);
        resData.numCharsTraversed += data.numCharsTraversed;
        return data;
    });
    if (highlightData.every(data => data.shouldParentHighlight && !data.highlightRange)) {
        /*
         * Every child wants highlight to be applied to the whole content at node level.
         * For conciseness, ask for the node's parent to highlight, if possible
         */
        resData.shouldParentHighlight = true;
        return resData;
    }
    /*
     * If node level highlighting is not possible, highlight the individual children as needed.
     * For text nodes, it is trickier, as we have to wrap the text inside a <span> first.
     * Essentially, we have to change the text node to become a tag node.
     */
    node.children.forEach((child, idx) => {
        var _a;
        const data = highlightData[idx];
        if (!data.shouldParentHighlight) {
            return;
        }
        if (child.type === 'tag') {
            child.attribs = (_a = child.attribs) !== null && _a !== void 0 ? _a : {};
            if (color) {
                child.attribs.style = child.attribs.style
                    ? `${child.attribs.style} background-color: ${color};`
                    : `background-color: ${color};`;
                return;
            }
            // Apply the 'highlighted' class if no color is provided
            child.attribs.class = child.attribs.class
                ? `${child.attribs.class} highlighted`
                : 'highlighted';
            return;
        }
        if (!data.highlightRange) {
            if (color) {
                (0, cheerio_1.default)(child).wrap(`<span style='background-color: ${color};'></span>`);
            }
            else if (child.type === 'text') {
                (0, cheerio_1.default)(child).wrap('<span class="highlighted"></span>');
            }
        }
        else {
            const [start, end] = data.highlightRange;
            const cleaned = (0, html_entities_1.decode)(child.data);
            const split = [cleaned.substring(0, start), cleaned.substring(start, end), cleaned.substring(end)];
            const [pre, highlighted, post] = split.map(markdown_it_1.default.utils.escapeHtml);
            if (color) {
                const s = `<span>${pre}<span style='background-color: ${color};'>${highlighted}</span>${post}</span>`;
                const newElement = (0, cheerio_1.default)(s);
                (0, cheerio_1.default)(child).replaceWith(newElement);
            }
            else {
                const str = `<span>${pre}<span class="highlighted">${highlighted}</span>${post}</span>`;
                const newElement = (0, cheerio_1.default)(str);
                (0, cheerio_1.default)(child).replaceWith(newElement);
            }
        }
    });
    resData.shouldParentHighlight = false;
    return resData;
}
/**
 * Applies pending highlighting to the code block.
 * This looks into each line for highlighting data, and if found,
 * traverses over the line and applies the highlight.
 * @param node Root of the code block element, which is the 'pre' node
 */
function highlightCodeBlock(node) {
    if (!node.children) {
        return;
    }
    const codeNode = node.children.find(c => c.name === 'code');
    if (!codeNode || (!codeNode.children)) {
        return;
    }
    codeNode.children.forEach((lineNode) => {
        if ((!lineNode.attribs) || !_.has(lineNode.attribs, 'hl-data')) {
            return;
        }
        const bounds = lineNode.attribs['hl-data']
            .split(',')
            .map((boundStr) => {
            const [range, color] = boundStr.split(':');
            const [start, end] = range.split('-');
            return [start, end, color];
        });
        bounds.forEach(([start, end, color]) => traverseLinePart(lineNode, Number(start), Number(end), color));
        delete lineNode.attribs['hl-data'];
    });
}
/**
 * Adjust the class attribute of code blocks according to the global line numbers setting.
 * Append the 'line-numbers' class if the global setting is true.
 * @param node the code block element, which is the 'code' node
 * @param showCodeLineNumbers true if line numbers should be shown, false otherwise
 */
function setCodeLineNumbers(node, showCodeLineNumbers) {
    var _a;
    const existingClass = node.attribs.class || '';
    const styleClassRegex = /(^|\s)(no-)?line-numbers($|\s)/;
    const hasStyleClass = styleClassRegex.test(existingClass);
    if (hasStyleClass) {
        return;
    }
    if (showCodeLineNumbers) {
        node.attribs = (_a = node.attribs) !== null && _a !== void 0 ? _a : {};
        node.attribs.class = `line-numbers${existingClass === '' ? '' : ` ${existingClass}`}`;
    }
}
//# sourceMappingURL=codeblockProcessor.js.map