odoo.define('web_editor.wysiwyg.plugin.helper', function (require) {
'use strict';

var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
var Dialog = require('web.Dialog');
var registry = require('web_editor.wysiwyg.plugin.registry');

var dom = $.summernote.dom;


var HelperPlugin = AbstractPlugin.extend({
    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * This dictionary contains oft-used regular expressions,
     * for performance and readability purposes. It can be
     * accessed and extended by the getRegex() method.
     */
    regex: {
        char: {
            noflag: /\S|\u00A0|\u200B/,
        },
        emptyElemWithBR: {
            noflag: /^\s*<br\/?>\s*$/,
        },
        endInvisible: {
            noflag: /\u200B$/,
        },
        endNotChar: {
            noflag: /[^\S\u00A0\u200B]+$/,
        },
        endSingleSpace: {
            noflag: /[\S\u00A0\u200B]\s$/,
        },
        endSpace: {
            noflag: /\s+$/,
        },
        invisible: {
            noflag: /\u200B/,
        },
        jinja: {
            noflag: /(^|\n)\s*%\send|%\sset/,
        },
        notWhitespace: {
            noflag: /\S/,
        },
        onlyEmptySpace: {
            noflag: /^[\s\u00A0\u200B]*(<br>)?[\s\u00A0\u200B]*$/,
        },
        semicolon: {
            noflag: / ?; ?/,
        },
        space: {
            noflag: /\s+/,
            g: /\s+/g,
        },
        spaceOrNewline: {
            noflag: /[\s\n\r]+/,
            g: /[\s\n\r]+/g,
        },
        startAndEndInvisible: {
            noflag: /^\u200B|\u200B$/,
            g: /^\u200B|\u200B$/g,
        },
        startAndEndSpace: {
            noflag: /^\s+|\s+$/,
            g: /^\s+|\s+$/g,
        },
        startAndEndSemicolon: {
            noflag: /^ ?;? ?| ?;? ?$/,
        },
        startInvisible: {
            noflag: /^\u200B/,
        },
        startNotChar: {
            noflag: /^[^\S\u00A0\u200B]+/,
        },
        startSingleSpace: {
            noflag: /^\s[\S\u00A0\u200B]/,
        },
        startSpace: {
            noflag: /^\s+/,
        },
    },

    /**
     * Compares two nodes to see if they are similar.
     * "Similar" means that they have the same tag, styles, classes and attributes.
     *
     * @param {Node} node
     * @param {Node} otherNode
     * @returns {Boolean} true if the nodes are similar
     */
    compareNodes: function (node, otherNode) {
        if (!otherNode || !node) {
            return false;
        }
        if (node.tagName !== otherNode.tagName) {
            return false;
        }
        if (dom.isText(node)) {
            return true;
        }
        this.removeBlankAttrs(node);
        this.removeBlankAttrs(otherNode);
        this.orderClass(node);
        this.orderStyle(node);
        this.orderClass(otherNode);
        this.orderStyle(otherNode);
        if (node.attributes.length !== otherNode.attributes.length) {
            return false;
        }
        for (var i = 0; i < node.attributes.length; i++) {
            var attr = node.attributes[i];
            var otherAttr = otherNode.attributes[i];
            if (attr.name !== otherAttr.name || attr.value !== otherAttr.value) {
                return false;
            }
        }
        return true;
    },
    /**
     * Returns the number of leading breakable space in the textNode.
     * Note: returns 0 if the node is not a textNode.
     *
     * @param {Node} node
     */
    countLeadingBreakableSpace: function (node) {
        if (!dom.isText(node)) {
            return 0;
        }
        var clone = $(node).clone()[0];
        var breakableSpace = this.removeExtremeBreakableSpace(clone, 0).start;
        return breakableSpace === 1 ? 0 : breakableSpace;
    },
    /**
     * Returns the number of trailing breakable space in the textNode.
     * Note: returns 0 if the node is not a textNode.
     *
     * @param {Node} node
     */
    countTrailingBreakableSpace: function (node) {
        if (!dom.isText(node)) {
            return 0;
        }
        var clone = $(node).clone()[0];
        var breakableSpace = this.removeExtremeBreakableSpace(clone, 0).end;
        return breakableSpace === 1 ? 0 : breakableSpace;
    },
    /**
     * Remove the dom between 2 points (respecting unbreakable rules).
     * Returns an object::
     *
     *     {
     *         node: pointA.node (updated if necessary),
     *         offset: pointA.offset (updated if necessary),
     *         changed: bool (whether changes were applied)
     *     }
     * 
     * @param {Object} pointA
     * @param {Node} pointA.node
     * @param {number} pointA.offset
     * @param {Object} pointB
     * @param {Node} pointB.node
     * @param {number} pointB.offset
     * @returns {Object} {node, offset, changed}
     */
    deleteBetween: function (pointA, pointB) {
        var self = this;
        if (pointB.node.childNodes[pointB.offset]) {
            var firstLeaf = this.firstLeaf(pointB.node.childNodes[pointB.offset]);
            pointB = this.makePoint(firstLeaf, 0);
        }
        if (pointB.node.tagName && pointB.node.tagName !== 'BR' && pointB.offset >= dom.nodeLength(pointB.node)) {
            pointB = dom.nextPoint(pointB);
        }
        var changed;
        var commonAncestor = dom.commonAncestor(pointA.node, pointB.node);

        var ecAncestor = dom.ancestor(pointB.node, function (node) {
            return node === commonAncestor || self.options.isUnbreakableNode(node.parentNode);
        });
        var next = this.splitTree(ecAncestor, pointB, {
            nextText: true,
        });

        var scAncestor = dom.ancestor(pointA.node, function (node) {
            return node === commonAncestor || self.options.isUnbreakableNode(node.parentNode);
        });
        if (dom.isIcon(pointA.node)) {
            pointA = dom.prevPoint(pointA);
        }
        this.splitTree(scAncestor, pointA, {
            nextText: true,
        });
        pointA.offset = dom.nodeLength(pointA.node);

        var nodes = [];
        dom.nextPointUntil(pointA, function (point) {
            if (point.node === next || !point.node) {
                return true;
            }
            if (dom.isText(point.node) && point.offset) {
                return;
            }
            var target = point.node.childNodes[point.offset] || point.node;
            if (target === pointA.node || $.contains(target, pointA.node) || target === next || $.contains(target, next)) {
                return;
            }
            if (
                nodes.indexOf(target) === -1 && !dom.ancestor(target, function (target) {
                    return nodes.indexOf(target) !== -1;
                })
            ) {
                nodes.push(target);
            }
        });
        $(nodes).remove();

        changed = !!nodes.length;
        var toMerge = changed && pointA.node.parentNode !== next.parentNode;


        var point = this.makePoint(this.firstLeaf(next), 0);
        if (nodes.length > 1 || nodes.length && !dom.isText(nodes[0])) {
            point = this.removeEmptyInlineNodes(point);
        }

        // Remove whole li/ul/ol if deleted all contents of li/ul/ol
        var ul = dom.ancestor(next, function (n) {
            return n.tagName === 'UL' || n.tagName === 'OL';
        });
        if (ul && next[dom.isText(next) ? 'textContent' : 'innerHTML'] === '' && pointA.node !== next.previousSibling) {
            var toRemove = next;
            while (
                toRemove !== ul && toRemove.parentNode &&
                !this.options.isUnbreakableNode(toRemove.parentNode) &&
                this.isBlankNode(toRemove.parentNode)
            ) {
                toRemove = toRemove.parentNode;
            }
            $(toRemove).remove();
        }
        if (!$.contains(this.editable, pointA.node)) {
            pointA = point;
        }

        if (toMerge) {
            pointA = this.deleteEdge(pointA.node, 'next') || pointA;
        }

        return {
            node: pointA.node,
            offset: pointA.offset,
            changed: changed,
        };
    },
    /**
     * Remove the edge between a node and its sibling
     * (= merge the nodes, respecting unbreakable rules).
     *
     * @param {Node} node
     * @param {String('next'|'prev')} direction
     * @param {Boolean} doNotTryNonSim true to not try merging non-similar nodes
     * @returns {Object} {node, offset}
     */
    deleteEdge: function (node, direction, doNotTryNonSim) {
        var prevOrNext = direction === 'prev' ? 'previousSibling' : 'nextSibling';
        var result = false;
        var startN = node;

        if (node.tagName === 'BR' && node.nextSibling && !(dom.isText(node.nextSibling) && !this.isVisibleText(node.nextSibling))) {
            node = node.nextSibling;
            node = this.firstLeaf(node);
        }

        var nodes = [];
        var next;
        while (node && node !== this.editable && !this.options.isUnbreakableNode(node)) {
            nodes.push(node);

            next = node[prevOrNext];
            while (next && !next.tagName) {
                if (!this.getRegex('char').test(next.textContent)) {
                    next = next[prevOrNext];
                    continue;
                }
                break;
            }

            if (next) {
                break;
            }
            node = node.parentNode;
        }

        if (next && next.tagName === 'TABLE') {
            return this.makePoint(node, 0);
        }

        var ifBrRemovedAndMerge = !_.filter(nodes, this.isNodeBlockType.bind(this)).length;
        var brRemoved = false;

        var range = this.context.invoke('editor.createRange');

        var spaceToRemove = [];
        while ((node = nodes.pop())) {
            next = node[prevOrNext];
            while (next && !next.tagName) {
                if (!this.getRegex('char').test(next.textContent)) {
                    spaceToRemove.push(next);
                    next = next[prevOrNext];
                    continue;
                }
                break;
            }
            if (
                !next ||
                !(node.tagName || next.tagName === 'BR') ||
                !next.tagName
            ) {
                continue;
            }

            if (!brRemoved && next.tagName === 'BR' && (!next[prevOrNext] || this.compareNodes(node, next[prevOrNext]))) {
                var newNext = next[prevOrNext];
                $(next).remove();
                next = newNext;
                var offset = (next ? direction === 'prev' : direction === 'next') ? dom.nodeLength(next) : 0;
                result = this.makePoint(next || node, offset);
                if (!ifBrRemovedAndMerge) {
                    continue;
                }
                brRemoved = true;
                ifBrRemovedAndMerge = false;
            }

            if (!this.compareNodes(node, next)) {
                continue;
            }
            _.each(spaceToRemove, function (space) {
                $(space).remove();
            });
            spaceToRemove = [];
            next = node[prevOrNext];
            var $next = $(next);
            if (next.tagName) {
                var textNode;
                var nextTextNode;
                var deep;
                if (direction === 'prev') {
                    textNode = this.firstLeaf(node);
                    if (!textNode.tagName && !dom.ancestor(textNode, dom.isPre)) {
                        this.removeExtremeBreakableSpace(textNode);
                        range.so = range.eo = 0;
                        nextTextNode = this.lastLeaf(next);
                        if (!nextTextNode.tagName && !dom.ancestor(nextTextNode, dom.isPre)) {
                            this.removeExtremeBreakableSpace(nextTextNode);
                        }
                    }
                    deep = this.lastLeaf(next);
                    result = this.makePoint(deep, dom.nodeLength(deep));
                    if (
                        this.getRegex('char').test(node.textContent) || node.childElementCount > 1 ||
                        node.firstElementChild && node.firstElementChild.tagName !== "BR"
                    ) {
                        $next.append($(node).contents());
                    }
                    $(node).remove();
                } else {
                    nextTextNode = this.firstLeaf(next);
                    if (!nextTextNode.tagName && !dom.ancestor(nextTextNode, dom.isPre)) {
                        this.removeExtremeBreakableSpace(nextTextNode);
                        textNode = this.lastLeaf(node);
                        if (!textNode.tagName && !dom.ancestor(textNode, dom.isPre)) {
                            this.removeExtremeBreakableSpace(textNode);
                            range.so = range.eo = dom.nodeLength(node);
                        }
                    }
                    if (node.innerHTML.trim() === '<br>') {
                        $(node).contents().remove();
                    }
                    deep = this.lastLeaf(node);
                    result = this.makePoint(deep, dom.nodeLength(deep));
                    $(node).append($next.contents());
                    $next.remove();
                }
                continue;
            } else if (!this.getRegex('char').test(next.textContent)) {
                result = this.makePoint(node, direction === 'prev' ? 0 : dom.nodeLength(node));
                $next.remove();
                continue;
            }

            break;
        }

        if (!result && startN && !doNotTryNonSim) {
            result = this.deleteNonSimilarEdge(startN, direction);
        }

        return result;
    },
    /**
     * Find and delete the previous/next non-similar edge if possible.
     * "Similar" means that they have the same tag, styles, classes and attributes.
     *
     * @param {Node} node
     * @param {String} direction 'prev' or 'next'
     * @returns {false|Object} {node, offset}
     */
    deleteNonSimilarEdge: function (node, direction) {
        var next = node[direction === 'next' ? 'nextSibling' : 'previousSibling'];
        while (
            next && dom.isText(next) &&
            this.getRegexBlank({
                space: true,
                invisible: true,
            }).test(next.textContent)
        ) {
            next = next[direction === 'next' ? 'nextSibling' : 'previousSibling'];
        }

        if (next) {
            return;
        }

        node = this.firstBlockAncestor(node);

        if (this.options.isUnbreakableNode(node)) {
            return;
        }

        var point = this.makePoint(node, direction === 'prev' ? 0 : dom.nodeLength(node));
        var otherBlock = this.findNextBlockToMerge(point.node, direction);

        if (!otherBlock) {
            return;
        }

        var blockToMergeFrom = direction === 'next' ? otherBlock : point.node;
        var blockToMergeInto = direction === 'next' ? point.node : otherBlock;

        // empty tag are removed
        if (
            this.getRegexBlank({
                space: true,
                newline: true,
            }).test(blockToMergeInto.textContent) &&
            !$(blockToMergeInto).find('.fa').length && $(blockToMergeInto).find('br').length <= 1
        ) {
            $(blockToMergeInto).remove();
            return this.makePoint(this.firstLeaf(blockToMergeFrom), 0);
        }

        return this.mergeNonSimilarBlocks(blockToMergeFrom, blockToMergeInto);
    },
    /**
     * Deletes the contents of the selected DOM.
     *
     * @returns {Boolean} true if there was a selection to delete
     */
    deleteSelection: function () {
        var range = this.context.invoke('editor.createRange');
        if (range.isCollapsed()) {
            return false;
        }
        var point = this.deleteBetween(range.getStartPoint(), range.getEndPoint());
        point = this.fillEmptyNode(point);

        range = this.context.invoke('editor.setRange', point.node, point.offset);
        range = range.select();

        this.editable.normalize();
        this.context.invoke('editor.saveRange');

        // remove tooltip when remove DOM nodes
        $('body > .tooltip').tooltip('hide');

        return true;
    },
    /**
     * Fill up an empty node so as to allow the carret to go inside it.
     * A block node will be filled with a <br>, with the offset before it.
     * An inline node will be filled with two zero-width spaces, with the offset in between the two.
     * Returns the given point (with the completed node and the updated offset).
     *
     * @param {Object} point {node, offset}
     * @returns {Object} {node, offset}
     */
    fillEmptyNode: function (point) {
        if (
            !point.node.tagName && this.getRegexBlank({
                space: true,
                invisible: true,
                nbsp: true,
            }).test(point.node.parentNode.innerHTML)
        ) {
            point.node = point.node.parentNode;
            point.offset = 0;
        }
        if (
            point.node.tagName && point.node.tagName !== 'BR' &&
            this.getRegexBlank({
                space: true,
                invisible: true,
                nbsp: true,
            }).test(point.node.innerHTML)
        ) {
            var text = this.document.createTextNode('');
            point.node.innerHTML = '';
            point.node.appendChild(text);
            point.node = text;
            point.offset = 0;
        }
        if (point.node.parentNode.innerHTML === '') {
            if (this.isNodeBlockType(point.node.parentNode)) {
                var node = point.node.parentNode;
                node.innerHTML = '<br/>';
                point.node = node.firstChild;
                point.offset = 0;
            } else {
                point.node.textContent = '\u200B\u200B';
                point.offset = 1;
            }
        }
        return point;
    },
    /**
     * Get the "format" ancestors list of nodes.
     * In this context, a "format" node is understood as
     * an editable block or an editable element expecting text
     * (eg.: p, h1, span).
     *
     * @param {Node[]} nodes
     * @returns {Node[]}
     */
    filterFormatAncestors: function (nodes) {
        var self = this;
        var selectedNodes = [];
        _.each(this.filterLeafChildren(nodes), function (node) {
            var ancestor = dom.ancestor(node, function (node) {
                return dom.isCell(node) || (
                    !self.options.isUnbreakableNode(node) &&
                    (self.isFormatNode(node) || self.isNodeBlockType(node))
                ) && self.editable !== node;
            });
            if (!ancestor) {
                ancestor = node;
            }
            if (dom.isCell(ancestor)) {
                ancestor = node;
            }
            if (ancestor && selectedNodes.indexOf(ancestor) === -1) {
                selectedNodes.push(ancestor);
            }
        });
        return selectedNodes;
    },
    /**
     * Get the "leaf" children of a list of nodes.
     * In this context, a "leaf" is understood as
     * either a text node or a node that doesn't expect text contents.
     *
     * @param {Node[]} nodes
     * @returns {Node[]}
     */
    filterLeafChildren: function (nodes) {
        var self = this;
        return _.compact(_.map(nodes, function (node) {
            if (node.firstChild) {
                node = node.firstChild;
            }
            if (
                node.tagName === "BR" ||
                self.isVisibleText(node) ||
                dom.isFont(node) ||
                dom.isImg(node) ||
                dom.isDocument(node)
            ) {
                return node;
            }
        }));
    },
    /**
     * Find the previous/next non-similar block to merge with.
     * "Similar" means that they have the same tag, styles, classes and attributes.
     *
     * @param {Node} node
     * @param {String} direction 'prev' or 'next
     * @returns {false|Node}
     */
    findNextBlockToMerge: function (node, direction) {
        var self = this;
        var startNode = node;
        var mergeableTags = this.options.styleTags.join(', ') + ', li';
        var blockToMerge = false;

        var li = dom.ancestor(node, function (n) {
            return n !== node && self.isNodeBlockType(n) || dom.isLi(n);
        });
        li = li && dom.isLi(li) ? li : undefined;
        if (li && direction === 'next') {
            if (li.nextElementSibling) {
                node = li;
            } else {
                node = dom.ancestor(node, function (n) {
                    return ((n.tagName === 'UL' || n.tagName === 'OL') && n.nextElementSibling);
                });
            }
        }

        if (!node || !node[direction === 'next' ? 'nextElementSibling' : 'previousElementSibling']) {
            return false;
        }

        node = node[direction === 'next' ? 'nextElementSibling' : 'previousElementSibling'];

        var ulFoldedSnippetNode = dom.ancestor(node, function (n) {
            return $(n).hasClass('o_ul_folded');
        });
        var ulFoldedSnippetStartNode = dom.ancestor(startNode, function (n) {
            return $(n).hasClass('o_ul_folded');
        });
        if (
            (this.options.isUnbreakableNode(node) && (!ulFoldedSnippetNode || ulFoldedSnippetNode === this.editable)) &&
            this.options.isUnbreakableNode(startNode) && (!ulFoldedSnippetStartNode || ulFoldedSnippetStartNode === this.editable)
        ) {
            return false;
        }

        node = this.firstBlockAncestor(node);

        li = dom.ancestor(node, function (n) {
            return n !== node && self.isNodeBlockType(n) || dom.isLi(n);
        });
        li = li && dom.isLi(li) ? li : undefined;
        node = li || node;

        if (node.tagName === 'UL' || node.tagName === 'OL') {
            node = node[direction === 'next' ? 'firstElementChild' : 'lastElementChild'];
        }

        if (this.options.isUnbreakableNode(node)) {
            return false;
        }

        if (node === startNode || $(node).has(startNode).length || $(startNode).has(node).length) {
            return false;
        }

        var $mergeable = $(node).find('*').addBack()
            .filter(mergeableTags)
            .filter(function (i, n) {
                if (!(n.tagName === 'LI' && $(n).find(mergeableTags).length)) {
                    return n;
                }
            });
        if ($mergeable.length) {
            blockToMerge = $mergeable[direction === 'next' ? 'first' : 'last']()[0] || false;
        }

        return blockToMerge;
    },
    /**
     * Get the first ancestor of a node, that is of block type (or itself).
     *
     * @param {Node} node
     * @returns {Node}
     */
    firstBlockAncestor: function (node) {
        var self = this;
        return dom.ancestor(node, function (n) {
            return self.isNodeBlockType(n);
        });
    },
    /**
     * Get the first leaf of a node, that is editable and not a media.
     * In this context, a leaf node is understood as a childless node.
     *
     * @param {Node} node
     * @returns {Node}
     */
    firstLeaf: function (node) {
        while (node.firstChild && !dom.isMedia(node) && this.options.isEditableNode(node)) {
            node = node.firstChild;
        }
        return node;
    },
    /**
     * Get the first leaf of a node, that is an element and not a BR.
     * In this context, a leaf node is understood as a childless node.
     *
     * @param {Node} node
     * @returns {Node} node
     */
    firstNonBRElementLeaf: function (node) {
        while (node.firstElementChild && node.firstElementChild.tagName !== 'BR') {
            node = node.firstElementChild;
        }
        return node;
    },
    /**
     * Returns the node targeted by a path
     *
     * @param {Object[]} path list of object (tagName, offset)
     * @returns {Node}
     */
    fromPath: function (path) {
        var node = this.editable;
        var to;
        path = path.slice();
        while ((to = path.shift())) {
            node = _.filter(node.childNodes, function (node) {
                return !to.tagName && node.tagName === 'BR' || node.tagName === to.tagName;
            })[to.offset];
        }
        return node;
    },
    /**
     * Returns (and creates if necessary) a regular expression.
     * If a regular expression with the given name exists, simply returns it.
     * Otherwise, creates a new one with the given name, exp and flag.
     *
     * @param {String} name
     * @param {String} [flag] optional
     * @param {String} [exp] optional
     * @returns {RegExp}
     */
    getRegex: function (name, flag, exp) {
        var flagName = flag || 'noflag';
        flag = flag || '';
        // If the regular expression exists, but not with this flag:
        // retrieve whichever version of it and apply the new flag to it,
        // then save that new version in the `regex` object.
        if (this.regex[name] && !this.regex[name][flagName]) {
            if (exp) {
                console.warn("A regular expression already exists with the name: " + name + ". The expression passed will be ignored.");
            }
            var firstVal = this.regex[name][Object.keys(this.regex[name])[0]];
            this.regex[name][flagName] = new RegExp(firstVal, flag);
        } else if (!this.regex[name]) {
            // If the regular expression does not exist:
            // save it into the `regex` object, with the name, expression
            // and flag passed as arguments (if any).
            if (!exp) {
                throw new Error("Cannot find a regular expression with the name " + name + ". Pass an expression to create it.");
            }
            this.regex[name] = {};
            this.regex[name][flagName] = new RegExp(exp, flag);
        }
        return this.regex[name][flagName];
    },
    /**
     * Returns (and creates if necessary) a regular expression
     * targetting a string made ONLY of some combination of the
     * characters enabled with options.
     * If a regular expression with the given options exists, simply returns it.
     * eg: getRegexBlank({space: true, nbsp: true}) => /^[\s\u00A0]*$/
     *
     * @param {Object} [options] optional
     * @param {Boolean} options.not ^ (not all that follows)
     * @param {Boolean} options.space \s (a whitespace)
     * @param {Boolean} options.notspace \S (not a whitespace)
     * @param {Boolean} options.nbsp \u00A0 (a non-breakable space)
     * @param {Boolean} options.invisible \u200B (a zero-width character)
     * @param {Boolean} options.newline \n|\r (a new line or a carriage return)
     * @param {Boolean} options.atLeastOne + (do not target blank strings)
     * @returns {RegExp}
     */
    getRegexBlank: function (options) {
        options = options || {};
        var charMap = {
            notspace: {
                name: 'NotSpace',
                exp: '\\S',
            },
            space: {
                name: 'Space',
                exp: '\\s',
            },
            nbsp: {
                name: 'Nbsp',
                exp: '\\u00A0',
            },
            invisible: {
                name: 'Invisible',
                exp: '\\u200B',
            },
            newline: {
                name: 'Newline', 
                exp: '\\n\\r',
            },
        };
        var name = 'only';
        var exp = '';
        var atLeastOne = options.atLeastOne;
        options.atLeastOne = false;

        // Build the expression and its name
        if (options.not) {
            name += 'Not';
            exp += '^';
            options.not = false;
        }
        _.each(options, function (value, key) {
            if (value && charMap[key]) {
                name += charMap[key].name;
                exp += charMap[key].exp;
            }
        });

        exp = '^[' + exp + ']' + (atLeastOne ? '+' : '*') + '$';
        name += atLeastOne ? 'One' : '';
        return this.getRegex(name, undefined, exp);
    },
    /**
     * Returns a list of all selected nodes in the range.
     *
     * @returns {Node []}
     */
    getSelectedNodes: function () {
        var range = this.context.invoke('editor.createRange');
        if (!range.isCollapsed()) {
            if (range.so && !range.sc.tagName) {
                if (range.sc === range.ec) {
                    range.sc = range.ec = range.sc.splitText(range.so);
                    range.eo -= range.so;
                } else {
                    range.sc = range.sc.splitText(range.so);
                }
                range.so = 0;
            }
            if (!range.ec.tagName && range.eo !== dom.nodeLength(range.ec) && !range.ec.tagName) {
                range.ec.splitText(range.eo);
            }
            range.select();
        }
        var res = [range.sc];
        var point = range.getStartPoint();
        var prevNode = range.sc;
        dom.nextPointUntil(point, function (pt) {
            if (pt.node !== prevNode) {
                var ok = true;
                // Return only the smallest traversed children
                _.each(res, function (n, i) {
                    // If pt.node is a child of res[i], replace it
                    if (dom.listAncestor(pt.node).indexOf(n) !== -1) {
                        res[i] = pt.node;
                        ok = false;
                    } else if (dom.listAncestor(n).indexOf(pt.node) !== -1) {
                        // Inversely, skip parents of res[i]
                        ok = false;
                    }
                });
                if (ok) {
                    res.push(pt.node);
                }
            }
            prevNode = pt.node;
            return pt.node === range.ec;
        });
        return _.uniq(res);
    },
    /**
     * Returns a list of all selected text nodes in the range.
     *
     * @returns {Node []}
     */
    getSelectedText: function () {
        var self = this;
        var selectedText = _.filter(this.getSelectedNodes(), function (node) {
            node = self.firstLeaf(node);
            return self.isVisibleText(node);
        });
        return _.uniq(selectedText);
    },
    /**
     * Returns true if the value contains jinja logic.
     *
     * @returns {Boolean}
     */
    hasJinja: function (value) {
        return this.getRegex('jinja').test(value);
    },
    /**
     * Inserts a block node (respecting the rules of unbreakable nodes).
     * In order to insert the node, the DOM tree is split at the carret position.
     * If there is a selection, it is deleted first.
     *
     * @param {Node} node
     */
    insertBlockNode: function (node) {
        var self = this;
        var range = this.context.invoke('editor.createRange');
        range = range.deleteContents();
        var point = range.getStartPoint();
        var unbreakable = point.node;
        if (!this.options.isUnbreakableNode(point.node)) {
            unbreakable = dom.ancestor(point.node, function (node) {
                return self.options.isUnbreakableNode(node.parentNode) || node === self.editable;
            }) || point.node;
        }

        if (unbreakable === point.node && !point.offset && point.node.tagName !== 'P') {
            if (point.node.innerHTML === '<br>') {
                $(point.node.firstElementChild).remove();
            }
            if (point.node.tagName === "BR") {
                $(point.node).replaceWith(node);
            } else {
                point.node.append(node);
            }
            return;
        }
        if (!this.options.isUnbreakableNode(point.node)) {
            var tree = dom.splitTree(unbreakable, point, {
                isSkipPaddingBlankHTML: true,
                isNotSplitEdgePoint: true,
            });
            if ((!tree || $.contains(tree, range.sc)) && (point.offset || point.node.tagName)) {
                tree = tree || dom.ancestor(point.node, function (node) {
                    return self.options.isUnbreakableNode(node.parentNode);
                });
                $(tree).after(node);
            } else {
                $(tree).before(node);
            }
        } else {
            // prevent unwrapped text in unbreakable
            if (dom.isText(unbreakable)) {
                $(unbreakable).wrap(this.document.createElement('p'));
                unbreakable.splitText(point.offset);
                unbreakable = unbreakable.parentNode;
                point.offset = 1;
            }
            $(unbreakable.childNodes[point.offset]).before(node);
        }
        if (range.sc.innerHTML === '<br>') {
            var clone = range.sc.cloneNode(true);
            if (node.previousSibling === range.sc) {
                $(node).after(clone);
            } else if (node.nextSibling === range.sc) {
                $(node).before(clone);
            }
        }
    },
    /**
     * Inserts a string as a text node in the DOM.
     * If the range is on a text node, splits the text node first.
     * Otherwise just inserts the text node.
     * Wraps it in a P if needed.
     *
     * @param {String} text
     */
    insertTextInline: function (text) {
        if (text === " ") {
            text = "\u00A0";
        }
        this.editable.normalize();
        this.deleteSelection();

        var range = this.context.invoke('editor.createRange');


        if (!range.sc.tagName && range.sc.textContent.match(/\S/)) {
            var before = range.sc.textContent.slice(0, range.so);
            var after = range.sc.textContent.slice(range.so);

            if (
                (before.length || after.length) &&
                (!before.length || before[before.length - 1] === ' ') &&
                (!after.length || after[0] === ' ')
            ) {
                var startSpace = this.getRegex('startSpace');
                var endSpace = this.getRegex('endSpace');
                before = before.replace(endSpace, '\u00A0');
                after = after.replace(startSpace, '\u00A0');
                range.sc.textContent = before + after;
                if (range.so > before.length) {
                    range.so = range.eo = before.length;
                }
                range.select();
            }
        }


        range = range.deleteContents();
        range = this._standardizeRangeOnEdge(range);
        var textNode = this._insertTextNodeInEditableArea(range, text);

        // if the text node can't be inserted in the dom (not editable area) do nothing
        if (!textNode) {
            return;
        }

        this.secureExtremeSingleSpace(range.sc);

        this._wrapTextWithP(textNode);

        range = this.context.invoke('editor.setRange', textNode, text.length);
        range.select();
        this.context.invoke('editor.saveRange');

        // Clean up and make leading/trailing/multiple space visible
        this.normalize();
        range = this.context.invoke('editor.createRange');
        var reStartBlanks = this.getRegex('startBlanks', '', '^([\\s\\u00A0\\u200B]*)');
        var reEndBlanks = this.getRegex('endBlanks', '', '([\\s\\u00A0\\u200B]*)$');
        var reAllNBSP = /\u00A0/g;
        var reMultipleSpace = /(\s){2,}/;

        var startBlanks = range.sc.textContent.match(reStartBlanks)[0] || '';
        var endBlanks = range.sc.textContent.match(reEndBlanks)[0] || '';
        var trimmed = range.sc.textContent.replace(reStartBlanks, '').replace(reEndBlanks, '');
        // Remove the single inner nbsp's and replace the multiple inner spaces with nbsp's
        var cleanContents = trimmed.replace(reAllNBSP, ' ')
            .replace(reMultipleSpace, function (space) {
                return Array(space.length + 1).join('\u00A0');
            });
        // Keep the leading/trailing whitespace, nbsp's and zero-width chars
        range.sc.textContent = startBlanks + cleanContents + endBlanks;

        this._removeInvisibleChar(range);

        range.select();
        this.context.invoke('editor.saveRange');
    },
    /**
     * Returns true if the node is a text node containing nothing
     *
     * @param {Node} node
     * @returns {Boolean}
     */
    isBlankText: function (node) {
        return dom.isText(node) &&
            this.getRegexBlank({
                not: true,
                notspace: true,
                nbsp: true,
                invisible: true,
            })
            .test(node.textContent);
    },
    /**
     * Returns true if the node is blank.
     * In this context, a blank node is understood as
     * a node expecting text contents (or with children expecting text contents)
     * but without any.
     *
     * @param {Node} node
     * @returns {Boolean}
     */
    isBlankNode: function (node) {
        if (dom.isVoid(node) || dom.isIcon(node)) {
            return false;
        }
        if (this.getRegexBlank({
                space: true,
            }).test(node[dom.isText(node) ? 'textContent' : 'innerHTML'])) {
            return true;
        }
        if (node.childNodes.length && _.all(node.childNodes, this.isBlankNode.bind(this))) {
            return true;
        }
        return false;
    },
    /**
     * Returns true if the point is on the left/right edge of the first
     * previous/next point with the given tag name (skips insignificant nodes).
     *
     * @param {Object} point
     * @param {String} tagName
     * @param {String('left'|'right')} side
     * @returns {Boolean}
     */
    isEdgeOfTag: function (point, tagName, side) {
        var method = side === 'left' ? 'isLeftEdgePoint' : 'isRightEdgePoint';
        var prevOrNext = side === 'left' ? 'prev' : 'next';
        var newPt;
        var first = true;
        while (point && point.node.tagName !== tagName) {
            newPt = this.skipNodes(point, prevOrNext, function (pt) {
                return pt.node.tagName === tagName && dom[method](pt);
            });
            if (newPt.node.tagName === tagName || newPt.node.tagName === 'BR') {
                point = newPt;
                break;
            }
            if (newPt === point && (!first || dom.isText(point.node) && !dom[method](point))) {
                break;
            }
            point = dom[prevOrNext + 'Point'](newPt);
            first = false;
        }
        if (!point) {
            return false;
        }
        var ancestor = dom.ancestor(point.node, function (n) {
            return n.tagName === tagName;
        });
        return !!(ancestor && dom[method + 'Of'](point, ancestor));
    },
    /**
     * Returns true if the node is a "format" node.
     * In this context, a "format" node is understood as
     * an editable block or an editable element expecting text
     * (eg.: p, h1, span).
     *
     * @param {Node} node
     * @returns {Boolean}
     */
    isFormatNode: function (node) {
        return node.tagName && this.options.styleTags.indexOf(node.tagName.toLowerCase()) !== -1;
    },
    /**
     * Returns true if the node is within a table.
     *
     * @param {Node} node
     * @returns {Boolean}
     */
    isInTable: function (node) {
        return !!dom.ancestor(node, function (n) {
            return n.tagName === 'TABLE';
        });
    },
    /**
     * Returns true if the point is on the left edge of a block node
     * (skips insignificant nodes).
     *
     * @param {Object} point
     * @returns {Boolean}
     */
    isLeftEdgeOfBlock: function (point) {
        point = this.skipNodes(point, 'prev');
        return dom.isLeftEdgePointOf(point, this.firstBlockAncestor(point.node));
    },
    /**
     * Returns true if the point is on the left edge of the first
     * previous point with the given tag name (skips insignificant nodes).
     *
     * @param {Object} point
     * @param {String} tagName
     * @returns {Boolean}
     */
    isLeftEdgeOfTag: function (point, tagName) {
        return this.isEdgeOfTag(point, tagName, 'left');
    },
    /**
     * Returns true if the node is a block.
     *
     * @param {Node} node
     * @returns {Boolean}
     */
    isNodeBlockType: function (node) {
        if (dom.isText(node)) {
            return false;
        }
        var display = this.window.getComputedStyle(node).display;
        // All inline elements have the word 'inline' in their display value, except 'contents'
        return display.indexOf('inline') === -1 && display !== 'contents';
    },
    /**
     * Returns true if the point is on the right edge of the first
     * next point with the given tag name (skips insignificant nodes).
     *
     * @param {Object} point
     * @param {String} tagName
     * @returns {Boolean}
     */
    isRightEdgeOfTag: function (point, tagName) {
        return this.isEdgeOfTag(point, tagName, 'right');
    },
    /**
     * Returns true if point should be ignored.
     * This is generally used for trying to figure out if the point is an edge point.
     *
     * @param {Object} point
     * @param {String} direction ('prev' or 'next')
     * @param {Object} options
     * @param {Boolean} options.noSkipBlankText true to not skip blank text
     * @param {Boolean} options.noSkipSingleBRs true to not skip single BRs
     * @param {Boolean} options.noSkipExtremeBreakableSpace true to not skip leading/trailing breakable space
     * @param {Boolean} options.noSkipParent true to not skip to leaf nodes or offset 0
     * @param {Boolean} options.noSkipSibling true to not skip if on edge and sibling is skippable
     * @returns {Boolean}
     */
    isSkippable: function (point, direction, options) {
        options = options || {};
        var isEdge = direction === 'prev' ? dom.isLeftEdgePoint(point) : dom.isRightEdgePoint(point);

        // skip blank text nodes
        if (
            !options.noSkipBlankText &&
            this.isBlankText(point.node)
        ) {
            return true;
        }
        // skip single BRs
        if (
            !options.noSkipSingleBRs &&
            point.node.tagName === 'BR' &&
            (!point.node.previousSibling || this.isBlankText(point.node.previousSibling)) &&
            (!point.node.nextSibling || this.isBlankText(point.node.nextSibling))
        ) {
            return true;
        }
        // skip leading/trailing breakable space
        if (
            !options.noSkipExtremeBreakableSpace &&
            (direction === 'prev' && !isEdge && point.offset <= this.countLeadingBreakableSpace(point.node) ||
                direction === 'next' && point.offset > dom.nodeLength(point.node) - this.countTrailingBreakableSpace(point.node))
        ) {
            return true;
        }
        // skip to leaf node or edge
        var node = direction === 'prev' ? point.node.childNodes[0] : point.node.childNodes[point.node.childNodes.length - 1];
        var offset = direction === 'prev' ? 0 : dom.nodeLength(node);
        if (
            !options.noSkipParent &&
            !isEdge && point.node.childNodes.length &&
            this.isSkippable(this.makePoint(node, offset), direction, options)
        ) {
            return true;
        }
        // skip if on edge and sibling is skippable
        var sibling = direction === 'prev' ? point.node.previousSibling : point.node.nextSibling;
        offset = direction === 'prev' ? 0 : dom.nodeLength(sibling);
        if (
            !options.noSkipSibling &&
            isEdge && sibling &&
            this.isSkippable(this.makePoint(sibling, offset), direction, _.defaults({
                noSkipSibling: true,
            }, options))
        ) {
            return true;
        }
        return false;
    },
    /**
     * Returns true if the node is a text node with visible text.
     *
     * @param {Node} node
     * @returns {Boolean}
     */
    isVisibleText: function (node) {
        return !node.tagName && this.getRegex('char').test(node.textContent);
    },
    /**
     * Get the last leaf of a node, that is editable and not a media.
     * In this context, a leaf node is understood as a childless node.
     *
     * @param {Node} node
     * @returns {Node}
     */
    lastLeaf: function (node) {
        while (node.lastChild && !dom.isMedia(node) && this.options.isEditableNode(node)) {
            node = node.lastChild;
        }
        return node;
    },
    /**
     * Returns a Point object from a node and an offset.
     *
     * @param {Node} node
     * @param {Number} offset
     */
    makePoint: function (node, offset) {
        return {
            node: node,
            offset: offset,
        };
    },
    /**
     * Merges mergeFromBlock into mergeIntoBlock, respecting the rules of unbreakable.
     *
     * @param {Node} mergeFromBlock block to merge from
     * @param {Node} mergeIntoBlock block to merge into
     * @returns {Object} {node, offset}
     */
    mergeNonSimilarBlocks: function (mergeFromBlock, mergeIntoBlock) {
        var point;
        var mergeableTags = this.options.styleTags.join(', ') + ', li';
        var $contents = $(mergeFromBlock).find('*').addBack()
            .filter(mergeableTags)
            .filter(function (i, n) {
                if (!(n.tagName === 'LI' && $(n).find(mergeableTags).length)) {
                    return n;
                }
            }).contents();
        var containsUnbreakables = !!$contents.filter(this.options.isUnbreakable).length;

        if ($contents.length && !containsUnbreakables) {
            if (dom.isText($contents[0])) {
                this.removeExtremeBreakableSpace($contents[0]);
            }
            var $lastContents = $(mergeIntoBlock).contents().last();
            if (!($contents.length === 1 && $contents[0].tagName === 'BR')) {
                if (mergeIntoBlock.innerHTML.trim() === '<br>') {
                    $(mergeIntoBlock).contents().remove();
                    $(mergeIntoBlock).append($contents);
                    $lastContents = false;
                } else {
                    $lastContents.after($contents);
                }
            }
            while (mergeFromBlock.parentNode && this.isBlankNode(mergeFromBlock.parentNode)) {
                mergeFromBlock = mergeFromBlock.parentNode;
            }
            $(mergeFromBlock).remove();

            point = {};
            if ($lastContents && $lastContents.length) {
                point = this.makePoint($lastContents[0], dom.nodeLength($lastContents[0]));
            } else {
                point = this.makePoint($contents[0], 0);
            }

            point = this.deleteEdge(point.node, 'next', true) || point;
        }
        return point;
    },
    /**
     * Normalize the DOM and range.
     */
    normalize: function () {
        this.editable.normalize();
        var range = this.context.invoke('editor.createRange');
        var rangeN = range.normalize();

        // summernote's normalize function fails when br in text,
        // and targets the br instead of the point just after br.
        var point = rangeN.getStartPoint();
        if (point.node.tagName === "BR") {
            point = dom.nextPoint(point);
        }
        if (point.node.tagName && point.node.childNodes[point.offset]) {
            point = dom.nextPoint(point);
        }
        if (point.node.tagName === "BR") {
            point = dom.nextPoint(point);
        }
        if (point.node !== range.sc || point.offset !== range.so) {
            range = this.context.invoke('editor.setRange', point.node, point.offset);
            range.select();
        }
    },
    /**
     * Simulate a do_notify by notifying the user through a dialog.
     *
     * @param {String} title
     * @param {String} content
     */
    notify: function (title, content) {
        var $notif = $('<p>' + content + '</p>');
        new Dialog(this, {
            title: title,
            size: 'medium',
            $content: $notif,
        }).open();
    },
    /**
     * Return true if the `container` contains the `contained` and only
     * the `contained` (blank text nodes are ignored).
     *
     * @param {Node} container
     * @param {Node} contained
     * @returns {Boolean}
     */
    onlyContains: function (container, contained) {
        var self = this;
        if (!$.contains(container, contained)) {
            return false;
        }
        var $contents = $(container).contents();
        var otherContents = $contents.filter(function (index, node) {
            if (node === contained || dom.isText(node) && !self.isVisibleText(node)) {
                return false;
            }
            return true;
        });
        return !otherContents.length;
    },
    /**
     * Reorders the classes in the node's class attribute and returns it.
     *
     * @param {Node} node
     * @returns {String}
     */
    orderClass: function (node) {
        var className = node.getAttribute && node.getAttribute('class');
        if (!className) {
            return null;
        }
        className = className.replace(this.getRegex('spaceOrNewline', 'g'), ' ')
            .replace(this.getRegex('startAndEndSpace', 'g'), '')
            .replace(this.getRegex('space', 'g'), ' ');
        className = className.replace('o_default_snippet_text', '')
            .replace('o_checked', '');
        if (!className.length) {
            node.removeAttribute("class");
            return null;
        }
        className = className.split(" ");
        className.sort();
        className = className.join(" ");
        node.setAttribute('class', className);
        return className;
    },
    /**
     * Reorders the styles in the node's style attributes and returns it.
     *
     * @param {Node} node
     * @returns {String}
     */
    orderStyle: function (node) {
        var style = node.getAttribute('style');
        if (!style) {
            return null;
        }
        style = style.replace(this.getRegex('spaceOrNewline'), ' ')
            .replace(this.getRegex('startAndEndSemicolon', 'g'), '')
            .replace(this.getRegex('semicolon', 'g'), ';');
        if (!style.length) {
            node.removeAttribute("style");
            return null;
        }
        style = style.split(";");
        style.sort();
        style = style.join("; ") + ";";
        node.setAttribute('style', style);
        return style;
    },
    /**
     * Returns the path from the editable node to the given node.
     *
     * @param {Node} node
     * @returns {Object[]} list of objects (tagName, offset)
     */
    path: function (node) {
        var path = [];
        while (node && node !== this.editable) {
            var tagName = node.tagName;
            path.unshift({
                tagName: tagName,
                offset: _.filter(node.parentNode.childNodes, function (node) {
                    return node.tagName === tagName;
                }).indexOf(node),
            });
            node = node.parentNode;
        }
        return path;
    },
    /**
     * Removes all attributes without a value from the given node.
     *
     * @param {Node} node
     * @returns {Node}
     */
    removeBlankAttrs: function (node) {
        _.each([].slice.call(node.attributes), function (attr) {
            if (!attr.value) {
                node.removeAttribute(attr.name);
            }
        });
        return node;
    },
    /**
     * Remove a node's direct blank siblings, if any.
     * Eg: Text<i></i>Node<b></b>Text => TextNodeText
     *
     * @param {Node} node
     */
    removeBlankSiblings: function (node) {
        var isAfterBlank = node.previousSibling && this.isBlankNode(node.previousSibling);
        if (isAfterBlank) {
            $(node.previousSibling).remove();
        }
        var isBeforeBlank = node.nextSibling && this.isBlankNode(node.nextSibling)
        if (isBeforeBlank) {
            $(node.nextSibling).remove();
        }
    },
    /**
     * Removes the block target and joins its siblings.
     *
     * @param {Node} target
     * @param {Boolean} doNotInsertP true to NOT fill an empty unbreakable with a p element.
     * @returns {Object} {node, offset}
     */
    removeBlockNode: function (target, doNotInsertP) {
        var self = this;
        var check = function (point) {
            if (point.node === target) {
                return false;
            }
            return !point.node || self.options.isEditableNode(point.node) &&
                (point.node.tagName === "BR" || self.isVisibleText(point.node));
        };
        var parent = target.parentNode;
        var offset = [].indexOf.call(parent.childNodes, target);
        var deleteEdge = 'next';
        var point = dom.prevPointUntil(this.makePoint(target, 0), check);
        if (!point || !point.node) {
            deleteEdge = 'prev';
            point = dom.nextPointUntil(this.makePoint(target, 0), check);
        }

        $(target).remove();

        if (
            point && (deleteEdge === 'prev' && point.offset) ||
            deleteEdge === 'next' && point.offset === dom.nodeLength(point.node)
        ) {
            point = this.deleteEdge(point.node, deleteEdge) || point;
        }

        $(parent).contents().filter(function () {
            return dom.isText(this) && self.getRegexBlank({
                atLeastOne: true,
                invisible: true,
            }).test(this.textContent);
        }).remove();

        var br;
        if (parent.innerHTML === '') {
            br = this.document.createElement('br');
            $(parent).append(br);
            point = this.makePoint(parent, 0);
        }

        if (!doNotInsertP && this.getRegexBlank({
                space: true,
                invisible: true,
            }).test(parent.innerHTML)) {
            br = this.document.createElement('br');
            if (this.options.isUnbreakableNode(parent) && parent.tagName !== "TD") {
                var p = this.document.createElement('p');
                $(p).append(br);
                $(parent).append(p);
            } else {
                $(parent).append(br);
            }
            point = this.makePoint(br.parentNode, 0);
        }

        if (point && point.node.tagName === "BR" && point.node.parentNode) {
            point = {
                node: point.node.parentNode,
                offset: [].indexOf.call(point.node.parentNode.childNodes, point.node),
            };
        }

        return point || this.makePoint(parent, offset);
    },
    /**
     * Removes the empty inline nodes around the point, and joins its siblings.
     *
     * @param {Object} point {node, offset}
     * @returns {Object} {node, offset}
     */
    removeEmptyInlineNodes: function (point) {
        var node = point.node;
        if (!point.node.tagName && !point.node.textContent.length) {
            node = node.parentNode;
            if ($(node).hasClass('o_default_snippet_text')) {
                // for default snippet value
                return point;
            }
        }
        var prev;
        var next;
        while (
            node.tagName !== 'BR' &&
            (node.tagName ? node.innerHTML : node.textContent) === '' &&
            !this.isNodeBlockType(node) &&
            this.options.isEditableNode(node.parentNode) &&
            (!node.attributes || !node.attributes.contenteditable) &&
            !dom.isMedia(node)
        ) {
            prev = node.previousSibling;
            next = node.nextSibling;
            point = {
                node: node.parentNode,
                offset: [].indexOf.call(node.parentNode.childNodes, node)
            };
            $(node).remove();
            node = point.node;
        }
        if (next && !next.tagName) {
            if (/^\s+[^\s<]/.test(next.textContent)) {
                next.textContent = next.textContent.replace(this.getRegex('startSpace'), '\u00A0');
            }
        }
        if (prev) {
            if (!prev.tagName) {
                if (/[^\s>]\s+$/.test(prev.textContent)) {
                    prev.textContent = prev.textContent.replace(this.getRegex('endSpace'), ' ');
                }
            }
            point = this.makePoint(prev, dom.nodeLength(prev));
        }
        return point;
    },
    /**
     * Removes any amount of leading/trailing breakable space from a text node.
     * Returns how many characters were removed at the start
     * and at the end of the text node.
     *
     * @param {Node} textNode
     * @param {Boolean} secureExtremeties (defaults to true)
     * @returns {Object} removed {start, end}
     */
    removeExtremeBreakableSpace: function (textNode, secureExtremeties) {
        if (arguments.length === 1) {
            secureExtremeties = true;
        }
        if (secureExtremeties) {
            this.secureExtremeSingleSpace(textNode);
        }
        var removed = {
            start: 0,
            end: 0,
        };
        textNode.textContent = textNode.textContent.replace(this.getRegex('startNotChar'), function (toRemove) {
            removed.start = toRemove.length;
            return '';
        });
        textNode.textContent = textNode.textContent.replace(this.getRegex('endNotChar'), function (toRemove) {
            removed.end = toRemove.length;
            return '';
        });
        return removed;
    },
    /**
     * Makes the leading/trailing single space of a node non breakable (nbsp).
     *
     * @param {Node} node
     */
    secureExtremeSingleSpace: function (node) {
        if (this.getRegex('endSingleSpace').test(node.textContent)) {
            // if the text ends with a single space, make it insecable
            node.textContent = node.textContent.substr(0, node.textContent.length - 1) + '\u00A0';
        }
        if (this.getRegex('startSingleSpace').test(node.textContent)) {
            // if the text starts with a single space, make it insecable
            node.textContent = '\u00A0' + node.textContent.substr(1, node.textContent.length);
        }
    },
    /**
     * Skips points to ignore (generally for trying to figure out if edge point).
     * Returns the resulting point.
     *
     * @param {Object} point
     * @param {String} direction ('prev' or 'next')
     * @param {function} pred (extra condition to stop at)
     * @param {Object} options
     * @param {Boolean} options.noSkipBlankText true to not skip blank text
     * @param {Boolean} options.noSkipSingleBRs true to not skip single BRs
     * @param {Boolean} options.noSkipExtremeBreakableSpace true to not skip leading/trailing breakable space
     * @param {Boolean} options.noSkipParent true to not skip to leaf nodes or offset 0
     * @param {Boolean} options.noSkipSibling true to not skip if on edge and sibling is skippable
     * @returns {Object} {node, offset}
     */
    skipNodes: function (point, direction, pred, options) {
        var self = this;
        if (arguments.length === 3 && !_.isFunction(arguments[2])) {
            // allow for passing options and no pred function
            options = _.clone(pred);
            pred = null;
        }
        options = options || {};
        return dom[direction + 'PointUntil'](point, function (pt) {
            return !self.isSkippable(pt, direction, options) || pred && pred(pt);
        });
    },
    /**
     * Split the DOM tree at the node's start and end points.
     *
     * @param {Node} node
     */
    splitAtNodeEnds: function (node) {
        if (!node.parentNode) {
            return;
        }
        var startPoint = this.makePoint(node, 0);
        var endPoint = this.makePoint(node, dom.nodeLength(node));
        var splitOptions = {
            isSkipPaddingBlankHTML: true,
        };
        this.splitTree(node.parentNode, startPoint, splitOptions);
        this.splitTree(node.parentNode, endPoint, splitOptions);
        // Splitting at ends may create blank nodes (because of dom.splitTree) so let's clean it up:
        this.removeBlankSiblings(node.parentNode);
    },
    /**
     * Split the DOM tree at the point
     *
     * @param {Node} root - split root
     * @param {BoundaryPoint} point {node, offset}
     * @param {Object} [options]
     * @param {Boolean} [options.nextText] - default: false
     * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
     * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
     * @returns {Node} right node of boundary point
     */
    splitTree: function (root, point, options) {
        var nextText;
        if (options && options.nextText && !point.node.tagName) {
            nextText = point.node.splitText(point.offset);
        }
        var emptyText = false;
        if (!point.node.tagName && point.node.textContent === "") {
            emptyText = true;
            point.node.textContent = '\u200B';
            point.offset = 1;
        }
        var next = dom.splitTree(root, point, options);
        if (emptyText) {
            point.node.textContent = '';
        }
        var result = nextText || next || point.node;
        var att = nextText ? 'textContent' : 'innerHTML';
        if (/^\s+([^\s<])/.test(result[att])) {
            result[att] = result[att].replace(this.getRegex('startSpace'), '\u00A0');
        }
        return result;
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    _insertTextNodeInEditableArea: function (range, text) {
        // try to insert the text node in editable area
        var textNode = this.document.createTextNode(text);
        if (this.options.isEditableNode(range.sc) && $(range.sc).closest('[contenteditable]').attr('contenteditable') === 'true') {
            if (dom.isText(range.sc) && this.isVisibleText(range.sc)) {
                // If range is on visible text: split the text at offset and insert the text node
                range.sc.splitText(range.so);
                range.so = range.eo = dom.nodeLength(range.sc);
                $(range.sc).after(textNode);
            } else if (dom.isBR(range.sc)) {
                if (range.sc.nextSibling && this.isVisibleText(range.sc.nextSibling)) {
                    $(range.sc).before(textNode);
                } else {
                    $(range.sc).replaceWith(textNode);
                }
            } else if (dom.isVoid(range.sc)) {
                $(range.sc).before(textNode);
            } else if (range.sc.childNodes[range.so]) {
                var node = range.sc.childNodes[range.so];
                if (dom.isBR(node)) {
                    $(node).replaceWith(textNode);
                } else {
                    $(node).before(textNode);
                }
            } else if (dom.isText(range.sc)) {
                $(range.sc).after(textNode);
            } else if (this.options.isUnbreakableNode(range.sc)) {
                $(range.sc).append(textNode);
            }
        }
        if (!textNode.parentNode && this.options.isEditableNode(range.sc.parentNode)) {
            $(range.sc).before(textNode);
        }

        return textNode.parentNode && textNode;
    },
    /**
     * Perform operations that are necessary after the insertion of a visible character:
     * adapt range for the presence of zero-width characters, move out of media, rerange.
     *
     * @private
     */
    _removeInvisibleChar: function (range) {
        if (range.sc.tagName || dom.ancestor(range.sc, dom.isAnchor)) {
            return true;
        }
        var needReselect = false;
        var fake = range.sc.parentNode;
        if ((fake.className || '').indexOf('o_fake_editable') !== -1 && dom.isMedia(fake)) {
            var $media = $(fake.parentNode);
            $media[fake.previousElementSibling ? 'after' : 'before'](fake.firstChild);
            needReselect = true;
        }
        if (range.sc.textContent.slice(range.so - 2, range.so - 1) === '\u200B') {
            range.sc.textContent = range.sc.textContent.slice(0, range.so - 2) + range.sc.textContent.slice(range.so - 1);
            range.so = range.eo = range.so - 1;
            needReselect = true;
        }
        if (range.sc.textContent.slice(range.so, range.so + 1) === '\u200B') {
            range.sc.textContent = range.sc.textContent.slice(0, range.so) + range.sc.textContent.slice(range.so + 1);
            needReselect = true;
        }
        if (needReselect) {
            range = range.normalize();
        }
        return range;
    },
    _standardizeRangeOnEdge: function (range) {
        var self = this;

        var invisible = this.document.createTextNode('\u200B');

        if (dom.isText(range.sc) && !this.isVisibleText(range.sc) && range.sc.nextSibling) {
            var firstLeafOfNext = this.firstLeaf(range.sc.nextSibling);
            range = this.context.invoke('editor.setRange', firstLeafOfNext, 0);
        }

        // Create empty text node to have a range into the node
        if (range.sc.tagName && !dom.isVoid(range.sc) && !range.sc.childNodes[range.so]) {
            $(range.sc).append(invisible);
            range = this.context.invoke('editor.setRange', invisible, 0);
        }

        // On left edge of non-empty element: move before
        var siblings = range.sc.parentNode && range.sc.parentNode.childNodes;
        var isInEmptyElem = !siblings || !siblings.length || _.all(siblings, this.isBlankNode.bind(this));
        if (
            !range.so && !isInEmptyElem &&
            !(range.sc.previousSibling && range.sc.previousSibling.tagName === "BR") &&
            !this.options.isEditableNode(range.sc)
        ) {
            var point = range.getStartPoint();
            var newPoint = dom.prevPointUntil(point, function (pt) {
                return pt.node !== range.sc && !pt.node.tagName && !self.isBlankText(pt.node);
            });
            if (!newPoint || this.firstBlockAncestor(newPoint.node) !== this.firstBlockAncestor(point.node)) {
                range = this.context.invoke('editor.setRange', point.node, point.offset);
            } else {
                range = this.context.invoke('editor.setRange', newPoint.node, newPoint.offset);
            }
        }

        return range;
    },
    _wrapTextWithP: function (textNode) {
        if (dom.ancestor(textNode, dom.isAnchor)) {
            return;
        }
        var self = this;
        var isFormatNode = dom.ancestor(textNode, this.isFormatNode.bind(this));
        if (!isFormatNode) {
            var hasInlineParent = dom.ancestor(textNode.parentNode, function (node) {
                return !self.isNodeBlockType(node);
            });
            if (
                !hasInlineParent &&
                (textNode.tagName ||
                    !(textNode.previousSibling && this.isVisibleText(textNode.previousSibling) ||
                      textNode.nextSibling && this.isVisibleText(textNode.nextSibling))
                )
            ) {
                var blankP = this.document.createElement('p');
                $(textNode).after(blankP);
                $(blankP).prepend(textNode);
            }
        }
    },
});

registry.add('HelperPlugin', HelperPlugin);

return HelperPlugin;

});
