/**
 * Transformation framework.
 * @author Louis-Dominique Dubeau
 * @license MPL 2.0
 * @copyright Mangalam Research Center for Buddhist Languages
 */
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
define(["require", "exports", "rxjs", "./action", "./domtypeguards", "./domutil", "./exceptions", "./gui/icon"], function (require, exports, rxjs_1, action_1, domtypeguards_1, domutil_1, exceptions_1, icon) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    icon = __importStar(icon);
    const TYPE_TO_KIND = {
        // Ideally the next line would be uncommented but TS goes off the rails
        // if that line is there. (Type inference gets stupid).
        // __proto__: null,
        // These are not actually type names. It is possible to use a kind name as a
        // type name if the transformation is not more specific. In this case the kind
        // === type.
        add: "add",
        delete: "delete",
        transform: "transform",
        insert: "add",
        "delete-element": "delete",
        "delete-parent": "delete",
        wrap: "wrap",
        "wrap-content": "wrap",
        "merge-with-next": "transform",
        "merge-with-previous": "transform",
        "swap-with-next": "transform",
        "swap-with-previous": "transform",
        split: "transform",
        append: "add",
        prepend: "add",
        unwrap: "unwrap",
        "add-attribute": "add",
        "delete-attribute": "delete",
    };
    const TYPE_TO_NODE_TYPE = {
        // Ideally the next line would be uncommented but TS goes off the rails
        // if that line is there. (Type inference gets stupid).
        // __proto__: null,
        // These are not actually type names. These are here to handle the
        // case where the type is actually a kind name. Since they are not
        // more specific, the node type is set to "other". Note that
        // "wrap" and "unwrap" are always about elements so there is no
        // way to have a "wrap/unwrap" which has "other" for the node
        // type.
        add: "other",
        delete: "other",
        transform: "other",
        insert: "element",
        "delete-element": "element",
        "delete-parent": "element",
        wrap: "element",
        "wrap-content": "element",
        "merge-with-next": "element",
        "merge-with-previous": "element",
        "swap-with-next": "element",
        "swap-with-previous": "element",
        split: "element",
        append: "element",
        prepend: "element",
        unwrap: "element",
        "add-attribute": "attribute",
        "delete-attribute": "attribute",
    };
    function computeIconHtml(iconHtml, transformationType) {
        if (iconHtml !== undefined) {
            return iconHtml;
        }
        const kind = TYPE_TO_KIND[transformationType];
        if (kind !== undefined) {
            return icon.makeHTML(kind);
        }
        return undefined;
    }
    /**
     * An operation that transforms the data tree.
     */
    class Transformation extends action_1.Action {
        /**
         * @param editor The editor for which this transformation is created.
         *
         * @param transformationType The type of transformation.
         *
         * @param desc The description of this transformation. A transformation's
         * [[getDescriptionFor]] method will replace ``<name>`` with the name of the
         * node actually being processed. So a string like ``Remove <name>`` would
         * become ``Remove foo`` when the transformation is called for the element
         * ``foo``.
         *
         * @param handler The handler to call when this transformation is executed.
         *
         * @param options Additional options.
         */
        constructor(editor, transformationType, desc, handler, options) {
            const actualOpts = options !== undefined ? options : {};
            super(editor, desc, actualOpts.abbreviatedDesc, computeIconHtml(actualOpts.iconHtml, transformationType), actualOpts.needsInput);
            if (handler === undefined) {
                throw new Error("did not specify a handler");
            }
            this.handler = handler;
            this.transformationType = transformationType;
            this.kind = TYPE_TO_KIND[transformationType];
            this.nodeType = TYPE_TO_NODE_TYPE[transformationType];
            this.treatAsTextInput = actualOpts.treatAsTextInput !== undefined ?
                actualOpts.treatAsTextInput : false;
        }
        getDescriptionFor(data) {
            if (data.name === undefined) {
                return this.desc;
            }
            return this.desc.replace(/<name>/, data.name);
        }
        /**
         * Calls the ``fireTransformation`` method on this transformation's editor.
         *
         * @param data The data object to pass.
         */
        execute(data) {
            this.editor.fireTransformation(this, data);
        }
    }
    exports.Transformation = Transformation;
    /**
     * Transformation events are generated by an editor before and after a
     * transformation is executed. The ``StartTransformation`` event is generated
     * before, and the ``EndTransformation`` is generated after. These events allow
     * modes to perform additional processing before or after a transformation, or
     * to abort a transformation while it is being processed.
     */
    class TransformationEvent {
        /**
         * @param name The name of the event.
         * @param transformation The transformation to which the event pertains.
         */
        constructor(name, transformation) {
            this.name = name;
            this.transformation = transformation;
            this._aborted = false;
        }
        /** Whether the transformation is aborted. */
        get aborted() {
            return this._aborted;
        }
        /**
         * Mark the transformation as aborted. Once aborted, a transformation cannot
         * be unaborted.
         */
        abort(message) {
            this._aborted = true;
            this._abortMessage = message;
        }
        /**
         * Raise an [[AbortTransformationException]] if the event was marked as
         * aborted.
         */
        throwIfAborted() {
            if (this.aborted) {
                throw new exceptions_1.AbortTransformationException(this._abortMessage);
            }
        }
    }
    exports.TransformationEvent = TransformationEvent;
    /**
     * A subject that emits [[TransformationEvent]] objects and immediately stops
     * calling subscribers when the [[TransformationEvent]] object it is processing
     * is aborted.
     */
    class TransformationEventSubject extends rxjs_1.Subject {
        next(value) {
            if (this.closed) {
                throw new rxjs_1.ObjectUnsubscribedError();
            }
            if (this.isStopped || value.aborted) {
                return;
            }
            for (const observer of this.observers.slice()) {
                observer.next(value);
                if (value.aborted) {
                    break;
                }
            }
        }
    }
    exports.TransformationEventSubject = TransformationEventSubject;
    /**
     * Makes an element appropriate for a wed data tree.
     *
     * @param doc The document for which to make the element.
     *
     * @param ns The URI of the namespace to use for the new element.
     *
     * @param name The name of the new element.
     *
     * @param attrs An object whose fields will become attributes for the new
     * element.
     *
     * @returns The new element.
     */
    function makeElement(doc, ns, name, attrs) {
        const e = doc.createElementNS(ns, name);
        if (attrs !== undefined) {
            // Create attributes
            const keys = Object.keys(attrs).sort();
            for (const key of keys) {
                e.setAttribute(key, attrs[key]);
            }
        }
        return e;
    }
    exports.makeElement = makeElement;
    /**
     * Insert an element in a wed data tree.
     *
     * @param dataUpdater A tree updater through which to update the DOM tree.
     *
     * @param parent The parent of the new node.
     *
     * @param index Offset in the parent where to insert the new node.
     *
     * @param ns The URI of the namespace to use for the new element.
     *
     * @param name The name of the new element.
     *
     * @param attrs An object whose fields will become attributes for the new
     * element.
     *
     * @returns The new element.
     */
    function insertElement(dataUpdater, parent, index, ns, name, attrs) {
        const ownerDocument = domtypeguards_1.isDocument(parent) ? parent : parent.ownerDocument;
        const el = makeElement(ownerDocument, ns, name, attrs);
        dataUpdater.insertAt(parent, index, el);
        return el;
    }
    exports.insertElement = insertElement;
    /**
     * Wraps a span of text in a new element.
     *
     * @param dataUpdater A tree updater through which to update the DOM tree.
     *
     * @param node The DOM node where to wrap. Must be a text node.
     *
     * @param offset The offset in the node. This parameter specifies where to start
     * wrapping.
     *
     * @param endOffset Offset in the node. This parameter specifies where to end
     * wrapping.
     *
     * @param ns The URI of the namespace to use for the new element.
     *
     * @param name The name of the wrapping element.
     *
     * @param attrs An object whose fields will become attributes for the new
     * element.
     *
     * @returns The new element.
     */
    function wrapTextInElement(dataUpdater, node, offset, endOffset, ns, name, attrs) {
        const textToWrap = node.data.slice(offset, endOffset);
        const parent = node.parentNode;
        if (parent === null) {
            throw new Error("detached node");
        }
        const nodeOffset = domutil_1.indexOf(parent.childNodes, node);
        dataUpdater.deleteText(node, offset, textToWrap.length);
        const newElement = makeElement(node.ownerDocument, ns, name, attrs);
        if (textToWrap !== "") {
            // It is okay to manipulate the DOM directly as long as the DOM tree being
            // manipulated is not *yet* inserted into the data tree. That is the case
            // here.
            newElement.appendChild(node.ownerDocument.createTextNode(textToWrap));
        }
        if (node.parentNode === null) {
            // The entire node was removed.
            dataUpdater.insertAt(parent, nodeOffset, newElement);
        }
        else {
            dataUpdater.insertAt(node, offset, newElement);
        }
        return newElement;
    }
    exports.wrapTextInElement = wrapTextInElement;
    /**
     * Utility function for [[wrapInElement]].
     *
     * @param dataUpdater A tree updater through which to update the DOM tree.
     *
     * @param container The text node to split.
     *
     * @param offset Where to split the node
     *
     * @returns A caret location marking where the split occurred.
     */
    function _wie_splitTextNode(dataUpdater, container, offset) {
        const parent = container.parentNode;
        if (parent === null) {
            throw new Error("detached node");
        }
        const containerOffset = domutil_1.indexOf(parent.childNodes, container);
        // The first two cases here just return a caret outside of the text node
        // rather than make a split that will create a useless empty text node.
        if (offset === 0) {
            offset = containerOffset;
        }
        else if (offset >= container.length) {
            offset = containerOffset + 1;
        }
        else {
            const text = container.data.slice(offset);
            dataUpdater.setTextNode(container, container.data.slice(0, offset));
            dataUpdater.insertNodeAt(parent, containerOffset + 1, container.ownerDocument.createTextNode(text));
            offset = containerOffset + 1;
        }
        return [parent, offset];
    }
    /**
     * Wraps a well-formed span in a new element. This span can contain text and
     * element nodes.
     *
     * @param dataUpdater A tree updater through which to update the DOM tree.
     *
     * @param startContainer The node where to start wrapping.
     *
     * @param startOffset The offset where to start wrapping.
     *
     * @param endContainer The node where to end wrapping.
     *
     * @param endOffset The offset where to end wrapping.
     *
     * @param ns The URI of the namespace to use for the new element.
     *
     * @param name The name of the new element.
     *
     * @param [attrs] An object whose fields will become attributes for the new
     * element.
     *
     * @returns The new element.
     *
     * @throws {Error} If the range is malformed or if there is an internal error.
     */
    function wrapInElement(dataUpdater, startContainer, startOffset, endContainer, endOffset, ns, name, attrs) {
        if (!domutil_1.isWellFormedRange({ startContainer, startOffset, endContainer,
            endOffset })) {
            throw new Error("malformed range");
        }
        if (domtypeguards_1.isText(startContainer)) {
            // We already have an algorithm for this case.
            if (startContainer === endContainer) {
                return wrapTextInElement(dataUpdater, startContainer, startOffset, endOffset, ns, name, attrs);
            }
            [startContainer, startOffset] =
                _wie_splitTextNode(dataUpdater, startContainer, startOffset);
        }
        if (domtypeguards_1.isText(endContainer)) {
            [endContainer, endOffset] =
                _wie_splitTextNode(dataUpdater, endContainer, endOffset);
        }
        if (startContainer !== endContainer) {
            throw new Error("startContainer and endContainer are not the same;" +
                "probably due to an algorithmic mistake");
        }
        const newElement = makeElement(startContainer.ownerDocument, ns, name, attrs);
        while (--endOffset >= startOffset) {
            const endNode = endContainer.childNodes[endOffset];
            dataUpdater.deleteNode(endNode);
            // Okay to change a tree which is not yet connected to the data tree.
            newElement.insertBefore(endNode, newElement.firstChild);
        }
        dataUpdater.insertAt(startContainer, startOffset, newElement);
        return newElement;
    }
    exports.wrapInElement = wrapInElement;
    /**
     * Replaces an element with its contents.
     *
     * @param dataUpdater A tree updater through which to update the DOM tree.
     *
     * @param node The element to unwrap.
     *
     * @returns The contents of the element.
     */
    function unwrap(dataUpdater, node) {
        const parent = node.parentNode;
        if (parent === null) {
            throw new Error("detached node");
        }
        const children = Array.prototype.slice.call(node.childNodes);
        const prev = node.previousSibling;
        const next = node.nextSibling;
        // This does not merge text nodes, which is what we want. We also want to
        // remove it first so that we don't generate so many update events.
        dataUpdater.deleteNode(node);
        // We want to calculate this index *after* removal.
        let nextIx = (next !== null) ? domutil_1.indexOf(parent.childNodes, next) :
            parent.childNodes.length;
        const lastChild = node.lastChild;
        // This also does not merge text nodes.
        while (node.firstChild != null) {
            dataUpdater.insertNodeAt(parent, nextIx++, node.firstChild);
        }
        // The order of the next two calls is important. We start at the end because
        // going the other way around could cause lastChild to leave the DOM tree.
        // Merge possible adjacent text nodes: the last child of the node that was
        // removed in the unwrapping and the node that was after the node that was
        // removed in the unwrapping.
        if (lastChild !== null) {
            dataUpdater.mergeTextNodes(lastChild);
        }
        // Merge the possible adjacent text nodes: the one before the start of the
        // children we unwrapped and the first child that was unwrapped. There may not
        // be a prev so we use the NF form of the call.
        dataUpdater.mergeTextNodesNF(prev);
        return children;
    }
    exports.unwrap = unwrap;
    /**
     * This function splits a node at the position of the caret. If the caret is not
     * inside the node or its descendants, an exception is raised.
     *
     * @param editor The editor on which we are to perform the transformation.
     *
     * @param node The node to split.
     *
     * @throws {Error} If the caret is not inside the node or its descendants.
     */
    function splitNode(editor, node) {
        const caret = editor.caretManager.getDataCaret();
        if (caret === undefined) {
            throw new Error("no caret");
        }
        if (!node.contains(caret.node)) {
            throw new Error("caret outside node");
        }
        const pair = editor.dataUpdater.splitAt(node, caret);
        // Find the deepest location at the start of the 2nd element.
        editor.caretManager.setCaret(domutil_1.firstDescendantOrSelf(pair[1]), 0);
    }
    exports.splitNode = splitNode;
    /**
     * This function merges an element with a previous element of the same name. For
     * the operation to go forward, the element must have a previous sibling and
     * this sibling must have the same name as the element being merged.
     *
     * @param editor The editor on which we are to perform the transformation.
     *
     * @param node The element to merge with previous.
     */
    function mergeWithPreviousHomogeneousSibling(editor, node) {
        const prev = node.previousElementSibling;
        if (prev === null) {
            return;
        }
        if (prev.localName !== node.localName ||
            prev.namespaceURI !== node.namespaceURI) {
            return;
        }
        // We need to record these to set the caret to a good position.
        const caretPos = prev.childNodes.length;
        const lastChild = prev.lastChild;
        const wasText = domtypeguards_1.isText(lastChild);
        // We need to record this *now* for future use, because it is possible that
        // the next loop could modify lastChild in place.
        const textLen = wasText ? lastChild.length : 0;
        const insertionPoint = prev.childNodes.length;
        // Reverse order
        for (let i = node.childNodes.length - 1; i >= 0; --i) {
            editor.dataUpdater.insertAt(prev, insertionPoint, node.childNodes[i].cloneNode(true));
        }
        if (wasText) {
            // If wasText is true, lastChild cannot be null.
            editor.dataUpdater.mergeTextNodes(lastChild);
            editor.caretManager.setCaret(prev.childNodes[caretPos - 1], textLen);
        }
        else {
            editor.caretManager.setCaret(prev, caretPos);
        }
        editor.dataUpdater.removeNode(node);
    }
    exports.mergeWithPreviousHomogeneousSibling = mergeWithPreviousHomogeneousSibling;
    /**
     * This function merges an element with a next element of the same name. For the
     * operation to go forward, the element must have a next sibling and this
     * sibling must have the same name as the element being merged.
     *
     * @param editor The editor on which we are to perform the transformation.
     *
     * @param node The element to merge with next.
     */
    function mergeWithNextHomogeneousSibling(editor, node) {
        const next = node.nextElementSibling;
        if (next === null) {
            return;
        }
        mergeWithPreviousHomogeneousSibling(editor, next);
    }
    exports.mergeWithNextHomogeneousSibling = mergeWithNextHomogeneousSibling;
    /**
     * This function swaps an element with a previous element of the same name. For
     * the operation to go forward, the element must have a previous sibling and
     * this sibling must have the same name as the element being merged.
     *
     * @param editor The editor on which we are to perform the transformation.
     *
     * @param node The element to swap with previous.
     */
    function swapWithPreviousHomogeneousSibling(editor, node) {
        const prev = node.previousElementSibling;
        if (prev === null) {
            return;
        }
        if (prev.localName !== node.localName ||
            prev.namespaceURI !== node.namespaceURI) {
            return;
        }
        const parent = prev.parentNode;
        if (parent === null) {
            throw new Error("detached node");
        }
        editor.dataUpdater.removeNode(node);
        editor.dataUpdater.insertBefore(parent, node, prev);
        editor.caretManager.setCaret(node);
    }
    exports.swapWithPreviousHomogeneousSibling = swapWithPreviousHomogeneousSibling;
    /**
     * This function swaps an element with a next element of the same name. For the
     * operation to go forward, the element must have a next sibling and this
     * sibling must have the same name as the element being merged.
     *
     * @param editor The editor on which we are to perform the transformation.
     *
     * @param node The element to swap with next.
     */
    function swapWithNextHomogeneousSibling(editor, node) {
        const next = node.nextElementSibling;
        if (next === null) {
            return;
        }
        swapWithPreviousHomogeneousSibling(editor, next);
    }
    exports.swapWithNextHomogeneousSibling = swapWithNextHomogeneousSibling;
    /**
     * Remove markup from the current selection. This turns mixed content into pure
     * text. The selection must be well-formed, otherwise the transformation is
     * aborted.
     *
     * @param editor The editor for which we are doing the transformation.
     */
    function removeMarkup(editor) {
        const selection = editor.caretManager.sel;
        // Do nothing if we don't have a selection.
        if (selection === undefined || selection.collapsed) {
            return;
        }
        if (!selection.wellFormed) {
            editor.modals.getModal("straddling").modal();
            throw new exceptions_1.AbortTransformationException("selection is not well-formed");
        }
        const [start, end] = selection.asDataCarets();
        const cutRet = editor.dataUpdater.cut(start, end);
        let newText = "";
        const cutNodes = cutRet[1];
        for (const el of cutNodes) {
            newText += el.textContent;
        }
        const insertRet = editor.dataUpdater.insertText(cutRet[0], newText);
        editor.caretManager.setRange(start.make(insertRet.node, insertRet.isNew ? cutRet[0].offset : 0), insertRet.caret);
    }
    exports.removeMarkup = removeMarkup;
});
//  LocalWords:  wasText endOffset prepend endContainer startOffset html DOM
//  LocalWords:  startContainer Mangalam Dubeau previousSibling nextSibling MPL
//  LocalWords:  insertNodeAt deleteNode mergeTextNodes lastChild prev Prepend
//  LocalWords:  deleteText domutil
//# sourceMappingURL=transformation.js.map