<commit_before>/**
 * @module wed
 * @desc The main module for wed.
 * @author <NAME>
 */
define(/** @lends module:wed */function (require, exports, module) {
'use strict';

var $ = require("jquery");
var rangy = require("rangy");
var validator = require("./validator");
var Validator = validator.Validator;
var util = require("./util");
var domutil = require("./domutil");
var domlistener = require("./domlistener");
var transformation = require("./transformation");
var validate = require("salve/validate");
var oop = require("./oop");
var undo = require("./undo");
var wundo = require("./wundo");
var jqutil = require("./jqutil");
var key_constants = require("./key_constants");
var SimpleEventEmitter =
        require("./lib/simple_event_emitter").SimpleEventEmitter;
var Conditioned = require("./lib/conditioned").Conditioned;
require("bootstrap");

exports.version = "0.5.0";

var getOriginalName = util.getOriginalName;

function Editor() {
    // Call the constructor for our mixins
    SimpleEventEmitter.call(this);
    Conditioned.call(this);
}

oop.implement(Editor, SimpleEventEmitter);
oop.implement(Editor, Conditioned);

Editor.prototype.init = function (widget, options) {

    if (options === undefined)
        options = module.config();

    // Records whether the first parse has happened.
    this._first_validation_complete = false;

    this._destroyed = false;

    // Marks caret position in places where putting a selection caret
    // is not possible.
    this._$fake_caret = $("<span class='_wed_caret' contenteditable='false'>&nbsp;</span>");

    this.widget = widget;
    this.$widget = $(this.widget);

    // We could be loaded in a frame in which case we should not
    // alter anything outside our frame.
    this.$frame = this.$widget.closest("html");
    this.my_window = this.$frame.get(0).ownerDocument.defaultView;

    this.options = options;

    // $gui_root and root represent the document root in the HTML elements
    // displayed. The top level element of the XML document being
    // edited will be the single child of $gui_root/root.
    this.$gui_root = $("<div class='wed-document'/>");

    this.$widget.wrapInner(this.$gui_root);
    // jQuery does not update this.$gui_root to reflect its position in the
    // DOM tree.
    this.$gui_root = $(this.widget.childNodes[0]);

    this.gui_root = this.$gui_root.get(0);

    // $data_root is the document we are editing, $gui_root will become
    // decorated with all kinds of HTML elements so we keep the two
    // separate.
    this.$data_root = this.$gui_root.clone();
    domutil.linkTrees(this.$data_root.get(0), this.$gui_root.get(0));

    this.$gui_root.wrap('<div class="row-fluid"><div class="span10"/></div>');
    this.$sidebar = $('<div id="sidebar" class="span2"/>');
    this.$widget.find('.row-fluid').first().prepend(this.$sidebar);
    // Needed by Validator
    this.$gui_root.before("<div class='row-fluid'><div class='span12 progress progress-info'><span></span><div id='validation-progress' class='bar' style='width: 0%'></div></div></div>");
    this.$validation_progress = this.$widget.find("#validation-progress");
    this.$validation_message = this.$validation_progress.prev('span');

    this.$sidebar.append('\
<div class="accordion">\
 <div class="accordion-group">\
  <div class="accordion-heading">\
   <a class="accordion-toggle" data-toggle="collapse" href="#sb-nav">Navigation</a>\
  </div>\
 </div>\
 <div id="sb-nav" class="accordion-body collapse in">\
  <div class="accordion-inner">\
   <ul id="navlist" class="nav nav-list">\
    <li class="inactive">A list of navigation links will appear here</li>\
   </ul>\
  </div>\
 </div>\
 <div class="accordion-group">\
  <div class="accordion-heading">\
   <a class="accordion-toggle" data-toggle="collapse" href="#sb-errors">Errors</a>\
  </div>\
 </div>\
 <div id="sb-errors" class="accordion-body collapse in">\
  <div class="accordion-inner">\
   <ul id="sb-errorlist" class="nav nav-list">\
    <li class="inactive"></li>\
   </ul>\
  </div>\
 </div>\
</div>');

    this.$widget.prepend("<div id='menu-layer'>");
    this.menu_layer = this.widget.childNodes[0];

    // tabindex needed to make keyboard stuff work... grumble...
    // https://github.com/twitter/bootstrap/issues/4663
    this.$hyperlink_modal = $(
        '\
<div class="modal hide fade" tabindex="1">\
  <div class="modal-header">\
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>\
    <h3>Insert hyperlink</h3>\
  </div>\
  <div class="modal-body">\
    <p>One fine body…</p>\
  </div>\
  <div class="modal-footer">\
    <a href="#" class="btn btn-primary" data-dismiss="modal">Insert</a>\
    <a href="#" class="btn" data-dismiss="modal">Close</a>\
  </div>\
</div>');
    this.$widget.prepend(this.$hyperlink_modal);

    this._raw_caret = undefined;

    this._selection_stack = [];

    // XXX this should probably go and be replaced by a check to make
    // sure bootstrap.css or a variant has been loaded. (Or
    // configurable to load something else than the minified version?)
    var bootstrap_css = require.toUrl("bootstrap/../../css/bootstrap.min.css");
    this.$frame.children("head").prepend('<link rel="stylesheet" href="' + bootstrap_css + '" type="text/css" />');

    // This domlistener is used only for validation.
    this.validation_domlistener = new domlistener.Listener(this.$data_root);

    // This domlistener is used for everything else
    this.domlistener = new domlistener.Listener(this.gui_root);

    // Setup the cleanup code.
    $(this.my_window).on('unload.wed', { 'editor': this }, unloadHandler);

    this._last_done_shown = 0;
    this.$error_list = this.$widget.find("#sb-errorlist");

    this._undo = new undo.UndoList();

    this.mode_path = options.mode.path;
    this.setMode(this.mode_path, options.mode.options);
};

Editor.prototype.setMode = function (mode_path, options) {
    require([mode_path], function (mode_module) {
        mode_module.Mode.optionResolver(
            options,
            function (resolved_opts) {
            this.onModeChange(new mode_module.Mode(resolved_opts));
        }.bind(this));
    }.bind(this));
};

Editor.prototype.onModeChange = function (mode) {
    if (this._destroyed)
        return;
    this.mode = mode;
    mode.init(this);

    var styles = this.mode.getStylesheets();
    for(var style_ix = 0, style; (style = styles[style_ix]) !== undefined;
        ++style_ix)
        this.$frame.children("head").append(
            '<link rel="stylesheet" href="' + require.toUrl(style) +
                '" type="text/css" />');

    this.$gui_root.css("overflow-y", "auto");
    var $next = this.$widget.next();

    // The height is the inner height of the window:
    // a. minus what appears before it.
    // b. minus what appears after it.
    var height = -5 + this.my_window.innerHeight -
            // This is the space before
            this.$gui_root.offset().top -
            // This is the space after
            (($next.offset().top > 0) ?
             ($(document).height() - $next.offset().top) : 0);

    this.$gui_root.css("max-height", height);
    this.$gui_root.css("min-height", height);

    this.resolver = mode.getAbsoluteResolver();
    this.validator = new Validator(this.options.schema,
                                   this.$data_root.get(0));
    this.validator.addEventListener(
        "state-update", this._onValidatorStateChange.bind(this));
    this.validator.addEventListener(
        "error", this._onValidatorError.bind(this));
    this.validator.addEventListener(
        "reset-errors", this._onResetErrors.bind(this));

    this.validator.initialize(this._postInitialize.bind(this));
};

Editor.prototype._postInitialize = function  () {
    if (this._destroyed)
        return;

    // Make the validator revalidate the structure from the point
    // where a change occurred.
    this.validation_domlistener.addHandler(
        "children-changed",
        "._real, ._phantom_wrap, .wed-document",
        function ($root, $added, $removed, $prev, $next, $target) {
        if ($added.is("._real, ._phantom_wrap") ||
            $removed.is("._real, ._phantom_wrap")) {
            this._last_done_shown = 0;
            this.validator.restartAt($target.get(0));
        }
    }.bind(this));

    this.validation_domlistener.startListening();

    this.copy_domlistener = new domlistener.Listener(this.$data_root);

    this.copy_domlistener.addHandler(
        "text-changed",
        "._real, ._phantom_wrap",
        function ($root, $target) {
        // Don't worry about DOM nodes that are no longer part of the tree.
        if ($target.closest($root).length === 0)
            return;
        var editor = this;
        var path = domutil.nodeToPath($root.get(0), $target.get(0));
        var gui_node = editor.pathToNode(path);
        gui_node.nodeValue = $target.get(0).nodeValue;
    }.bind(this));

    this.copy_domlistener.addHandler(
        "children-changed",
        "._real, ._phantom_wrap, .wed-document",
        function ($root, $added, $removed, $prev, $next, $target) {
        // Don't worry about DOM nodes that are no longer part of the tree.
        if ($target.closest($root).length === 0)
            return;

        var $added_narrow = $added.filter(function () {
            return $(this).is("._real, ._phantom_wrap") ||
                jqutil.textFilter.call(this);
        });
        var $removed_narrow = $removed.filter(function () {
            return $(this).is("._real, ._phantom_wrap") ||
                jqutil.textFilter.call(this);
        });

        // Stupid hoisting
        var i, $node, $node_prev, $node_next, path, $new;
        for(i = 0; i < $added_narrow.length; ++i) {
            $node = $($added_narrow.get(i));
            $new = $node.clone();
            domutil.linkTrees($new.get(0), $node.get(0));
            $node_prev = $node.prev();
            $node_next = $node.next();
            if ($node_prev.length > 0) {
                path = domutil.nodeToPath($root.get(0), $node_prev.get(0));
                $(this.pathToNode(path)).after($new);
            }
            else if ($node_next.length > 0) {
                path = domutil.nodeToPath($root.get(0), $target.get(0));
                $(this.pathToNode(path)).prepend($new);
            }
            else {
                path = domutil.nodeToPath($root.get(0), $target.get(0));
                $(this.pathToNode(path)).append($new);
            }
        }

        // Stupid hoisting
        var $mirror;

        // Do it in reverse
        for(i = $removed_narrow.length - 1; i >= 0; --i) {
            $node = $($removed_narrow.get(i));
            var mirror = $node.data("wed_mirror_node");
            if (mirror !== undefined) {
                $mirror = $(mirror);
                $mirror.remove();
                $mirror.removeData("wed_mirror_node");
                $node.removeData("wed_mirror_node");
            } else if ($node.get(0).nodeType === Node.TEXT_NODE) {
                $mirror = $($target.data("wed_mirror_node"));
                if ($target.get(0).childNodes.length === 0) {
                    // Everything was removed from the target.
                    $mirror.empty();
                }
                else {
                    // We can't do the following because the node is
                    // no longer in its tree.
                    //
                    // path = domutil.nodeToPath($root.get(0),
                    //                           $node.get(0));
                    // $(this.pathToNode(path)).remove();

                    // This is relatively expensive but there is no
                    // easy way currently to locate text nodes that
                    // have been removed from the tree.
                    $mirror = $($target.data("wed_mirror_node"));
                    $new = $target.clone();
                    $mirror.replaceWith($new);
                    domutil.linkTrees($new.get(0), $target.get(0));
                }
            }
            // else: it can happen that a node has no mirror data and
            // is not a text node. Consider the following:
            //
            // - <p><a/><b/></p> is added
            // - <b/> is removed from <p>
            //
            // Then the events are processed by the copy_listener.  By
            // the time the addition of <p> is acted on, <p> is as
            // follows: <p><a/></p> because <b/> has been removed, so
            // <b/> is never linked. Then the removal of <b/> is
            // processed but it has no mirror because it was never
            // linked.
        }

        if ($removed_narrow.length > 0)
            // The caret might have become stale.
            this._caretChangeEmitter();
    }.bind(this));


    this.copy_domlistener.startListening();

    this.decorator = this.mode.makeDecorator(this.domlistener, this);
    this.tr = this.mode.getTransformationRegistry();
    this.decorator.init(this.$gui_root);

    // If a placeholder is edited, delete it.
    this.domlistener.addHandler(
        "children-changed",
        "._placeholder",
        function ($root, $added, $removed, $prev, $next, $target) {
        if ($added.is("._real, ._phantom_wrap") ||
            $removed.is("._real, ._phantom_wrap"))
            $target.children().unwrap();
    });

    // If a placeholder is edited, delete it.
    this.domlistener.addHandler(
        "text-changed",
        "._placeholder",
        function ($root, $target) {
        // Make sure we do no chase after targets that are no
        // longer part of the DOM.
        if ($target.closest(document.documentElement).length > 0) {
            var text = $.trim($target.text());

            // Don't create an empty text node.
            if (text.length > 0) {
                var text_node = document.createTextNode(text);
                $target.parent().replaceWith(text_node);
                this.setCaret(text_node, text_node.nodeValue.length);
            }
        }
    }.bind(this));


    // If an element is edited and contains a placeholder, delete
    // the placeholder
    this.domlistener.addHandler(
        "children-changed",
        "._real, ._phantom_wrap",
        function ($root, $added, $removed, $prev, $next, $target) {
        // Narrow it to the elements we care about.
        var $narrow_added =
            $added.filter("._real, ._phantom_wrap, ._phantom._text").
            add($added.filter(jqutil.textFilter));
        var $narrow_removed =
            $removed.filter("._real, ._phantom_wrap, ._phantom._text").
            add($removed.filter(jqutil.textFilter));

        if ($narrow_added.length + $narrow_removed.length === 0)
            return;

        if ($target.children('._real, ._phantom._text, ._phantom_wrap').
            length === 0 &&
            $target.contents().filter(jqutil.textFilter).length === 0) {
            var nodes =
                    this.mode.nodesAroundEditableContents($target.get(0));
            var ph = this.mode.makePlaceholderFor($target.get(0));
            if (nodes[0] !== null)
                $(nodes[0]).after(ph);
            else if (nodes[1] !== null)
                $(nodes[1]).before(ph);
            else
                $target.append(ph);
        }
        else
            $target.children("._placeholder").remove();
    }.bind(this));

    // We have to listed to included-element rather than
    // added-element because the copy_domlistener may add entire
    // trees.
    this.domlistener.addHandler(
        "included-element",
        "._real, ._phantom_wrap",
        function ($root, $tree, $parent, $prev, $next, $target) {
        if ($target.children(
            '._real, ._phantom._text, ._phantom_wrap').length === 0 &&
            $target.contents().filter(jqutil.textFilter).length === 0) {
            var nodes = this.mode.nodesAroundEditableContents($target.get(0));
            var ph = this.mode.makePlaceholderFor($target.get(0));
            if (nodes[0] !== null)
                $(nodes[0]).after(ph);
            else if (nodes[1] !== null)
                $(nodes[1]).before(ph);
            else
                $target.append(ph);
        }
    }.bind(this));


    // Wed caret has been moved, emit a change
    this.domlistener.addHandler(
        "added-element",
        "._wed_caret",
        function () {
        this._caretChangeEmitter();
    }.bind(this));

    this.copy_domlistener.addHandler(
        "children-changed",
        "._real, ._phantom_wrap",
        function ($root, $added, $removed, $prev, $next, $target) {
        if (this._suppress_undo_recording)
            return;
        // Narrow it to the elements we care about.
        var $narrow_added =
            $added.filter("._real, ._phantom_wrap, ._placeholder").
            add($added.filter(jqutil.textFilter));
        var $narrow_removed =
            $removed.filter("._real, ._phantom_wrap, ._placeholder").
            add($removed.filter(jqutil.textFilter));
        if ($narrow_added.length + $narrow_removed.length === 0)
            return;

        $prev = ($prev.is("._real, ._phantom_wrap") ? $prev :
                 $prev.prevAll("._real, ._phantom_wrap").first());
        $next = ($next.is("._real, ._phantom_wrap") ? $next:
                 $next.nextAll("._real, ._phantom_wrap").first());
        this._undo.record(new wundo.DOMUndo(
            this, $target, $narrow_added, $narrow_removed, $prev, $next));
    }.bind(this));

    this.copy_domlistener.addHandler(
        "text-changed",
        "._real, ._phantom_wrap",
        function ($root, $element, old_value) {
        if (this._suppress_undo_recording)
            return;
        // Narrow it to the elements we care about.
        this._undo.record(new wundo.TextNodeUndo(this, $element, old_value));
    }.bind(this));

    this.domlistener.addHandler(
        "excluded-element",
        "._wed_caret",
        function ($root, $tree, $parent, $prev, $next, $element) {
        // Act only if the caret is being removed because its
        // parent is being removed. Direct action on the caret
        // does not trigger this code.
        if ($element.get(0) !== this._$fake_caret.get(0)) {
            // We must move the caret to a sane position.
            if ($prev.length > 0 && $prev.closest($root).length > 0)
                $prev.after(this._$fake_caret);
            else if ($next.length > 0 && $next.closest($root).length > 0)
                $next.before(this._$fake_caret);
            else if ($parent.closest($root).length > 0)
                $parent.append(this._$fake_caret);
            else {
                // There's nowhere sensible to put it at
                this._$fake_caret.remove();
                this._raw_caret = undefined;
            }
        }
    }.bind(this));

    if (this.gui_root.childNodes.length === 0) {
        var namespaces = this.validator.getNamespaces();

        // Drop the xml namespace, because we don't need to define
        // it. It will appear at most once in the array.
        var xml = namespaces.indexOf(util.XML1_NAMESPACE);
        if (xml >= 0)
            namespaces.splice(xml, 1);

        var mappings = Object.create(null);
        if (namespaces.length === 1)
            // Just make this namespace the default one
            mappings[""] = namespaces[0];
        else { // Ask the user
            var $hyperlink_modal = this.$hyperlink_modal;
            $hyperlink_modal.find("h3").text("Assign names for namespaces");
            var $body = $hyperlink_modal.find('.modal-body');
            $body.empty();
            namespaces.forEach(function (ns) {
                // XXX this is incomplete
                $body.append('<input type="text" name="..."/>');
                $body.append("<label>" + ns + "</label>");
            });
            $hyperlink_modal.modal();
        }

        var attrs = Object.create(null);
        Object.keys(mappings).forEach(function (k) {
            this.resolver.definePrefix(k, mappings[k]);
            if (k === "")
                attrs.xmlns = mappings[k];
            else
                attrs["xmlns:" + k] = mappings[k];
        }.bind(this));

        var evs = this.validator.possibleAt(this.gui_root, 0).toArray();
        if (evs.length === 1 && evs[0].params[0] === "enterStartTag") {
            transformation.insertElement(
                this, this.$data_root.get(0), 0,
                this.resolver.unresolveName(evs[0].params[1],
                                            evs[0].params[2]),
                attrs);
        }

        ///else
        /// XXX log an error somewhere
    }

    this.$gui_root.on('keydown',
                      util.eventHandler(this._keydownHandler.bind(this)));
    this.$gui_root.on('wed-global-keydown',
                      this._globalKeydownHandler.bind(this));

    this.$gui_root.on('keypress',
                      util.eventHandler(this._keypressHandler.bind(this)));
    this.$gui_root.on('wed-global-keypress',
                      this._globalKeypressHandler.bind(this));
    this.$gui_root.on('click.wed', this._mouseHandler.bind(this));
    // No click in the next binding because click does not
    // distinguish left, middle, right mouse buttons.
    this.$gui_root.on('keyup mouseup',
                      this._caretChangeEmitter.bind(this));
    this.$gui_root.on('caretchange',
                      this._caretChangeHandler.bind(this));
    this.$gui_root.on('contextmenu',
                      util.eventHandler(this._contextMenuHandler.bind(this)));

    var nav_links = [];
    this.$gui_root.find(".head").each(function (x, el) {
        nav_links.push("<li><a href='#" + el.id + "'>" + $(el).text() +
                       "</a></li>");
    });
    this.$sidebar.find("#navlist>.inactive").replaceWith(nav_links.join(""));

    this.validator.start();

    this.domlistener.processImmediately();
    // Flush whatever has happened earlier.
    this._undo = new undo.UndoList();
    this._setCondition("initialized", {editor: this});
};

/**
 * This method asks the editor to process any pending operation which
 * may impact the display. This method is meant to be used internally
 * by the editor and for testing/debugging purposes
 * only. Consequently, the various modes that customize wed's behavior
 * must not use this method. If calling this method makes a mode that
 * did not work well work normally, then either the code that was not
 * working well is buggy (and calling _syncDisplay is not the
 * solution) or there is a bug in wed that should be fixed.
 */
Editor.prototype._syncDisplay = function () {
    this.copy_domlistener.processImmediately();
    this.domlistener.processImmediately();
};

/**
 * @param {module:transformation~Transformation} tr The transformation
 * to fire.
 * @param {Node} node The DOM node to fire it on. This can be a node
 * from the GUI tree or from the data tree.
 * @param {String} element_name The element_name to use in the
 * description of the transformation.
 * @param transformation_data Arbitrary data to be passed to the
 * transformation.
 * @param {Array} new_caret_position A new caret position in the data
 * tree.
 */
Editor.prototype.fireTransformation = function(tr, node, element_name,
                                               transformation_data,
                                               new_caret_position) {
    this.dismissContextMenu();

    if (new_caret_position)
        this.setDataCaret(new_caret_position);

    if (this._raw_caret === undefined)
        throw new Error("transformation applied with undefined caret.");
    var current_group = this._undo.getGroup();
    if (current_group !== undefined) {
        if (!(current_group instanceof wundo.TextUndoGroup))
            throw new Error("unexpected group class in _fireTransformation");
        this._undo.endGroup();
    }

    this._undo.startGroup(
        new wundo.UndoGroup("Undo " + tr.getDescriptionFor(element_name),
                            this));
    if (node !== undefined) {
        // Convert the gui node to a data node
        if ($(node).closest(this.$gui_root).length > 0) {
            var path = this.nodeToPath(node);
            node = domutil.pathToNode(this.$data_root.get(0), path);
        }
        else if ($(node).closest(this.$data_root).length === 0) {
            throw new Error("node is neither in the gui tree nor "+
                            "the data tree");
        }
    }
    tr.handler(this, node, element_name, transformation_data);
    // Ensure that all operations that derive from this
    // transformation are done *now*.
    this._syncDisplay();
    this._undo.endGroup();
};

Editor.prototype._fireTransformation = function (e, new_caret_position) {
    this.fireTransformation(e.data.tr, e.data.node, e.data.element_name,
                            undefined, new_caret_position);
};

Editor.prototype.recordUndo = function (undo) {
    this._undo.record(undo);
};

Editor.prototype.undo = function () {
    this._suppress_undo_recording = true;
    this._undo.undo();
    this.copy_domlistener.processImmediately();
    this.domlistener.processImmediately();
    this._suppress_undo_recording = false;
};

Editor.prototype.redo = function () {
    this._suppress_undo_recording = true;
    this._undo.redo();
    this.copy_domlistener.processImmediately();
    this.domlistener.processImmediately();
    this._suppress_undo_recording = false;
};

Editor.prototype._contextMenuHandler = function (e, jQthis) {
    // If the caret is changing due to a click on a placeholder,
    // then put it inside the placeholder.
    var $ph = $(e.target).closest("._placeholder");
    if ($ph.length > 0)
        this.setCaret($ph.get(0).childNodes[0], 0);

    var selection = rangy.getSelection(this.my_window);
    var original_range = domutil.getSelectionRange(this.my_window);
    var range = original_range.cloneRange();
    var start_is_focus = ((selection.focusNode === range.startContainer) &&
                          (selection.focusOffset === range.startOffset));
    range.collapse(start_is_focus);

    var $node_of_interest;
    var offset;
    if (range.startContainer.nodeType === Node.ELEMENT_NODE) {
        $node_of_interest = $(range.startContainer);
        offset = range.startOffset;
    }
    else {
        $node_of_interest = $(range.startContainer.parentNode);
        offset = Array.prototype.indexOf.call(
            range.startContainer.parentNode.childNodes,
            range.startContainer);<commit_msg>undoingOrRedoing method added to Editor.<commit_after>/**
 * @module wed
 * @desc The main module for wed.
 * @author <NAME>
 */
define(/** @lends module:wed */function (require, exports, module) {
'use strict';

var $ = require("jquery");
var rangy = require("rangy");
var validator = require("./validator");
var Validator = validator.Validator;
var util = require("./util");
var domutil = require("./domutil");
var domlistener = require("./domlistener");
var transformation = require("./transformation");
var validate = require("salve/validate");
var oop = require("./oop");
var undo = require("./undo");
var wundo = require("./wundo");
var jqutil = require("./jqutil");
var key_constants = require("./key_constants");
var SimpleEventEmitter =
        require("./lib/simple_event_emitter").SimpleEventEmitter;
var Conditioned = require("./lib/conditioned").Conditioned;
require("bootstrap");

exports.version = "0.5.0";

var getOriginalName = util.getOriginalName;

function Editor() {
    // Call the constructor for our mixins
    SimpleEventEmitter.call(this);
    Conditioned.call(this);
}

oop.implement(Editor, SimpleEventEmitter);
oop.implement(Editor, Conditioned);

Editor.prototype.init = function (widget, options) {

    if (options === undefined)
        options = module.config();

    // Records whether the first parse has happened.
    this._first_validation_complete = false;

    this._destroyed = false;

    // Marks caret position in places where putting a selection caret
    // is not possible.
    this._$fake_caret = $("<span class='_wed_caret' contenteditable='false'>&nbsp;</span>");

    this.widget = widget;
    this.$widget = $(this.widget);

    // We could be loaded in a frame in which case we should not
    // alter anything outside our frame.
    this.$frame = this.$widget.closest("html");
    this.my_window = this.$frame.get(0).ownerDocument.defaultView;

    this.options = options;

    // $gui_root and root represent the document root in the HTML elements
    // displayed. The top level element of the XML document being
    // edited will be the single child of $gui_root/root.
    this.$gui_root = $("<div class='wed-document'/>");

    this.$widget.wrapInner(this.$gui_root);
    // jQuery does not update this.$gui_root to reflect its position in the
    // DOM tree.
    this.$gui_root = $(this.widget.childNodes[0]);

    this.gui_root = this.$gui_root.get(0);

    // $data_root is the document we are editing, $gui_root will become
    // decorated with all kinds of HTML elements so we keep the two
    // separate.
    this.$data_root = this.$gui_root.clone();
    domutil.linkTrees(this.$data_root.get(0), this.$gui_root.get(0));

    this.$gui_root.wrap('<div class="row-fluid"><div class="span10"/></div>');
    this.$sidebar = $('<div id="sidebar" class="span2"/>');
    this.$widget.find('.row-fluid').first().prepend(this.$sidebar);
    // Needed by Validator
    this.$gui_root.before("<div class='row-fluid'><div class='span12 progress progress-info'><span></span><div id='validation-progress' class='bar' style='width: 0%'></div></div></div>");
    this.$validation_progress = this.$widget.find("#validation-progress");
    this.$validation_message = this.$validation_progress.prev('span');

    this.$sidebar.append('\
<div class="accordion">\
 <div class="accordion-group">\
  <div class="accordion-heading">\
   <a class="accordion-toggle" data-toggle="collapse" href="#sb-nav">Navigation</a>\
  </div>\
 </div>\
 <div id="sb-nav" class="accordion-body collapse in">\
  <div class="accordion-inner">\
   <ul id="navlist" class="nav nav-list">\
    <li class="inactive">A list of navigation links will appear here</li>\
   </ul>\
  </div>\
 </div>\
 <div class="accordion-group">\
  <div class="accordion-heading">\
   <a class="accordion-toggle" data-toggle="collapse" href="#sb-errors">Errors</a>\
  </div>\
 </div>\
 <div id="sb-errors" class="accordion-body collapse in">\
  <div class="accordion-inner">\
   <ul id="sb-errorlist" class="nav nav-list">\
    <li class="inactive"></li>\
   </ul>\
  </div>\
 </div>\
</div>');

    this.$widget.prepend("<div id='menu-layer'>");
    this.menu_layer = this.widget.childNodes[0];

    // tabindex needed to make keyboard stuff work... grumble...
    // https://github.com/twitter/bootstrap/issues/4663
    this.$hyperlink_modal = $(
        '\
<div class="modal hide fade" tabindex="1">\
  <div class="modal-header">\
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>\
    <h3>Insert hyperlink</h3>\
  </div>\
  <div class="modal-body">\
    <p>One fine body…</p>\
  </div>\
  <div class="modal-footer">\
    <a href="#" class="btn btn-primary" data-dismiss="modal">Insert</a>\
    <a href="#" class="btn" data-dismiss="modal">Close</a>\
  </div>\
</div>');
    this.$widget.prepend(this.$hyperlink_modal);

    this._raw_caret = undefined;

    this._selection_stack = [];

    // XXX this should probably go and be replaced by a check to make
    // sure bootstrap.css or a variant has been loaded. (Or
    // configurable to load something else than the minified version?)
    var bootstrap_css = require.toUrl("bootstrap/../../css/bootstrap.min.css");
    this.$frame.children("head").prepend('<link rel="stylesheet" href="' + bootstrap_css + '" type="text/css" />');

    // This domlistener is used only for validation.
    this.validation_domlistener = new domlistener.Listener(this.$data_root);

    // This domlistener is used for everything else
    this.domlistener = new domlistener.Listener(this.gui_root);

    // Setup the cleanup code.
    $(this.my_window).on('unload.wed', { 'editor': this }, unloadHandler);

    this._last_done_shown = 0;
    this.$error_list = this.$widget.find("#sb-errorlist");

    this._undo = new undo.UndoList();

    this.mode_path = options.mode.path;
    this.setMode(this.mode_path, options.mode.options);
};

Editor.prototype.setMode = function (mode_path, options) {
    require([mode_path], function (mode_module) {
        mode_module.Mode.optionResolver(
            options,
            function (resolved_opts) {
            this.onModeChange(new mode_module.Mode(resolved_opts));
        }.bind(this));
    }.bind(this));
};

Editor.prototype.onModeChange = function (mode) {
    if (this._destroyed)
        return;
    this.mode = mode;
    mode.init(this);

    var styles = this.mode.getStylesheets();
    for(var style_ix = 0, style; (style = styles[style_ix]) !== undefined;
        ++style_ix)
        this.$frame.children("head").append(
            '<link rel="stylesheet" href="' + require.toUrl(style) +
                '" type="text/css" />');

    this.$gui_root.css("overflow-y", "auto");
    var $next = this.$widget.next();

    // The height is the inner height of the window:
    // a. minus what appears before it.
    // b. minus what appears after it.
    var height = -5 + this.my_window.innerHeight -
            // This is the space before
            this.$gui_root.offset().top -
            // This is the space after
            (($next.offset().top > 0) ?
             ($(document).height() - $next.offset().top) : 0);

    this.$gui_root.css("max-height", height);
    this.$gui_root.css("min-height", height);

    this.resolver = mode.getAbsoluteResolver();
    this.validator = new Validator(this.options.schema,
                                   this.$data_root.get(0));
    this.validator.addEventListener(
        "state-update", this._onValidatorStateChange.bind(this));
    this.validator.addEventListener(
        "error", this._onValidatorError.bind(this));
    this.validator.addEventListener(
        "reset-errors", this._onResetErrors.bind(this));

    this.validator.initialize(this._postInitialize.bind(this));
};

Editor.prototype._postInitialize = function  () {
    if (this._destroyed)
        return;

    // Make the validator revalidate the structure from the point
    // where a change occurred.
    this.validation_domlistener.addHandler(
        "children-changed",
        "._real, ._phantom_wrap, .wed-document",
        function ($root, $added, $removed, $prev, $next, $target) {
        if ($added.is("._real, ._phantom_wrap") ||
            $removed.is("._real, ._phantom_wrap")) {
            this._last_done_shown = 0;
            this.validator.restartAt($target.get(0));
        }
    }.bind(this));

    this.validation_domlistener.startListening();

    this.copy_domlistener = new domlistener.Listener(this.$data_root);

    this.copy_domlistener.addHandler(
        "text-changed",
        "._real, ._phantom_wrap",
        function ($root, $target) {
        // Don't worry about DOM nodes that are no longer part of the tree.
        if ($target.closest($root).length === 0)
            return;
        var editor = this;
        var path = domutil.nodeToPath($root.get(0), $target.get(0));
        var gui_node = editor.pathToNode(path);
        gui_node.nodeValue = $target.get(0).nodeValue;
    }.bind(this));

    this.copy_domlistener.addHandler(
        "children-changed",
        "._real, ._phantom_wrap, .wed-document",
        function ($root, $added, $removed, $prev, $next, $target) {
        // Don't worry about DOM nodes that are no longer part of the tree.
        if ($target.closest($root).length === 0)
            return;

        var $added_narrow = $added.filter(function () {
            return $(this).is("._real, ._phantom_wrap") ||
                jqutil.textFilter.call(this);
        });
        var $removed_narrow = $removed.filter(function () {
            return $(this).is("._real, ._phantom_wrap") ||
                jqutil.textFilter.call(this);
        });

        // Stupid hoisting
        var i, $node, $node_prev, $node_next, path, $new;
        for(i = 0; i < $added_narrow.length; ++i) {
            $node = $($added_narrow.get(i));
            $new = $node.clone();
            domutil.linkTrees($new.get(0), $node.get(0));
            $node_prev = $node.prev();
            $node_next = $node.next();
            if ($node_prev.length > 0) {
                path = domutil.nodeToPath($root.get(0), $node_prev.get(0));
                $(this.pathToNode(path)).after($new);
            }
            else if ($node_next.length > 0) {
                path = domutil.nodeToPath($root.get(0), $target.get(0));
                $(this.pathToNode(path)).prepend($new);
            }
            else {
                path = domutil.nodeToPath($root.get(0), $target.get(0));
                $(this.pathToNode(path)).append($new);
            }
        }

        // Stupid hoisting
        var $mirror;

        // Do it in reverse
        for(i = $removed_narrow.length - 1; i >= 0; --i) {
            $node = $($removed_narrow.get(i));
            var mirror = $node.data("wed_mirror_node");
            if (mirror !== undefined) {
                $mirror = $(mirror);
                $mirror.remove();
                $mirror.removeData("wed_mirror_node");
                $node.removeData("wed_mirror_node");
            } else if ($node.get(0).nodeType === Node.TEXT_NODE) {
                $mirror = $($target.data("wed_mirror_node"));
                if ($target.get(0).childNodes.length === 0) {
                    // Everything was removed from the target.
                    $mirror.empty();
                }
                else {
                    // We can't do the following because the node is
                    // no longer in its tree.
                    //
                    // path = domutil.nodeToPath($root.get(0),
                    //                           $node.get(0));
                    // $(this.pathToNode(path)).remove();

                    // This is relatively expensive but there is no
                    // easy way currently to locate text nodes that
                    // have been removed from the tree.
                    $mirror = $($target.data("wed_mirror_node"));
                    $new = $target.clone();
                    $mirror.replaceWith($new);
                    domutil.linkTrees($new.get(0), $target.get(0));
                }
            }
            // else: it can happen that a node has no mirror data and
            // is not a text node. Consider the following:
            //
            // - <p><a/><b/></p> is added
            // - <b/> is removed from <p>
            //
            // Then the events are processed by the copy_listener.  By
            // the time the addition of <p> is acted on, <p> is as
            // follows: <p><a/></p> because <b/> has been removed, so
            // <b/> is never linked. Then the removal of <b/> is
            // processed but it has no mirror because it was never
            // linked.
        }

        if ($removed_narrow.length > 0)
            // The caret might have become stale.
            this._caretChangeEmitter();
    }.bind(this));


    this.copy_domlistener.startListening();

    this.decorator = this.mode.makeDecorator(this.domlistener, this);
    this.tr = this.mode.getTransformationRegistry();
    this.decorator.init(this.$gui_root);

    // If a placeholder is edited, delete it.
    this.domlistener.addHandler(
        "children-changed",
        "._placeholder",
        function ($root, $added, $removed, $prev, $next, $target) {
        if ($added.is("._real, ._phantom_wrap") ||
            $removed.is("._real, ._phantom_wrap"))
            $target.children().unwrap();
    });

    // If a placeholder is edited, delete it.
    this.domlistener.addHandler(
        "text-changed",
        "._placeholder",
        function ($root, $target) {
        // Make sure we do no chase after targets that are no
        // longer part of the DOM.
        if ($target.closest(document.documentElement).length > 0) {
            var text = $.trim($target.text());

            // Don't create an empty text node.
            if (text.length > 0) {
                var text_node = document.createTextNode(text);
                $target.parent().replaceWith(text_node);
                this.setCaret(text_node, text_node.nodeValue.length);
            }
        }
    }.bind(this));


    // If an element is edited and contains a placeholder, delete
    // the placeholder
    this.domlistener.addHandler(
        "children-changed",
        "._real, ._phantom_wrap",
        function ($root, $added, $removed, $prev, $next, $target) {
        // Narrow it to the elements we care about.
        var $narrow_added =
            $added.filter("._real, ._phantom_wrap, ._phantom._text").
            add($added.filter(jqutil.textFilter));
        var $narrow_removed =
            $removed.filter("._real, ._phantom_wrap, ._phantom._text").
            add($removed.filter(jqutil.textFilter));

        if ($narrow_added.length + $narrow_removed.length === 0)
            return;

        if ($target.children('._real, ._phantom._text, ._phantom_wrap').
            length === 0 &&
            $target.contents().filter(jqutil.textFilter).length === 0) {
            var nodes =
                    this.mode.nodesAroundEditableContents($target.get(0));
            var ph = this.mode.makePlaceholderFor($target.get(0));
            if (nodes[0] !== null)
                $(nodes[0]).after(ph);
            else if (nodes[1] !== null)
                $(nodes[1]).before(ph);
            else
                $target.append(ph);
        }
        else
            $target.children("._placeholder").remove();
    }.bind(this));

    // We have to listed to included-element rather than
    // added-element because the copy_domlistener may add entire
    // trees.
    this.domlistener.addHandler(
        "included-element",
        "._real, ._phantom_wrap",
        function ($root, $tree, $parent, $prev, $next, $target) {
        if ($target.children(
            '._real, ._phantom._text, ._phantom_wrap').length === 0 &&
            $target.contents().filter(jqutil.textFilter).length === 0) {
            var nodes = this.mode.nodesAroundEditableContents($target.get(0));
            var ph = this.mode.makePlaceholderFor($target.get(0));
            if (nodes[0] !== null)
                $(nodes[0]).after(ph);
            else if (nodes[1] !== null)
                $(nodes[1]).before(ph);
            else
                $target.append(ph);
        }
    }.bind(this));


    // Wed caret has been moved, emit a change
    this.domlistener.addHandler(
        "added-element",
        "._wed_caret",
        function () {
        this._caretChangeEmitter();
    }.bind(this));

    this.copy_domlistener.addHandler(
        "children-changed",
        "._real, ._phantom_wrap",
        function ($root, $added, $removed, $prev, $next, $target) {
        if (this._suppress_undo_recording)
            return;
        // Narrow it to the elements we care about.
        var $narrow_added =
            $added.filter("._real, ._phantom_wrap, ._placeholder").
            add($added.filter(jqutil.textFilter));
        var $narrow_removed =
            $removed.filter("._real, ._phantom_wrap, ._placeholder").
            add($removed.filter(jqutil.textFilter));
        if ($narrow_added.length + $narrow_removed.length === 0)
            return;

        $prev = ($prev.is("._real, ._phantom_wrap") ? $prev :
                 $prev.prevAll("._real, ._phantom_wrap").first());
        $next = ($next.is("._real, ._phantom_wrap") ? $next:
                 $next.nextAll("._real, ._phantom_wrap").first());
        this._undo.record(new wundo.DOMUndo(
            this, $target, $narrow_added, $narrow_removed, $prev, $next));
    }.bind(this));

    this.copy_domlistener.addHandler(
        "text-changed",
        "._real, ._phantom_wrap",
        function ($root, $element, old_value) {
        if (this._suppress_undo_recording)
            return;
        // Narrow it to the elements we care about.
        this._undo.record(new wundo.TextNodeUndo(this, $element, old_value));
    }.bind(this));

    this.domlistener.addHandler(
        "excluded-element",
        "._wed_caret",
        function ($root, $tree, $parent, $prev, $next, $element) {
        // Act only if the caret is being removed because its
        // parent is being removed. Direct action on the caret
        // does not trigger this code.
        if ($element.get(0) !== this._$fake_caret.get(0)) {
            // We must move the caret to a sane position.
            if ($prev.length > 0 && $prev.closest($root).length > 0)
                $prev.after(this._$fake_caret);
            else if ($next.length > 0 && $next.closest($root).length > 0)
                $next.before(this._$fake_caret);
            else if ($parent.closest($root).length > 0)
                $parent.append(this._$fake_caret);
            else {
                // There's nowhere sensible to put it at
                this._$fake_caret.remove();
                this._raw_caret = undefined;
            }
        }
    }.bind(this));

    if (this.gui_root.childNodes.length === 0) {
        var namespaces = this.validator.getNamespaces();

        // Drop the xml namespace, because we don't need to define
        // it. It will appear at most once in the array.
        var xml = namespaces.indexOf(util.XML1_NAMESPACE);
        if (xml >= 0)
            namespaces.splice(xml, 1);

        var mappings = Object.create(null);
        if (namespaces.length === 1)
            // Just make this namespace the default one
            mappings[""] = namespaces[0];
        else { // Ask the user
            var $hyperlink_modal = this.$hyperlink_modal;
            $hyperlink_modal.find("h3").text("Assign names for namespaces");
            var $body = $hyperlink_modal.find('.modal-body');
            $body.empty();
            namespaces.forEach(function (ns) {
                // XXX this is incomplete
                $body.append('<input type="text" name="..."/>');
                $body.append("<label>" + ns + "</label>");
            });
            $hyperlink_modal.modal();
        }

        var attrs = Object.create(null);
        Object.keys(mappings).forEach(function (k) {
            this.resolver.definePrefix(k, mappings[k]);
            if (k === "")
                attrs.xmlns = mappings[k];
            else
                attrs["xmlns:" + k] = mappings[k];
        }.bind(this));

        var evs = this.validator.possibleAt(this.gui_root, 0).toArray();
        if (evs.length === 1 && evs[0].params[0] === "enterStartTag") {
            transformation.insertElement(
                this, this.$data_root.get(0), 0,
                this.resolver.unresolveName(evs[0].params[1],
                                            evs[0].params[2]),
                attrs);
        }

        ///else
        /// XXX log an error somewhere
    }

    this.$gui_root.on('keydown',
                      util.eventHandler(this._keydownHandler.bind(this)));
    this.$gui_root.on('wed-global-keydown',
                      this._globalKeydownHandler.bind(this));

    this.$gui_root.on('keypress',
                      util.eventHandler(this._keypressHandler.bind(this)));
    this.$gui_root.on('wed-global-keypress',
                      this._globalKeypressHandler.bind(this));
    this.$gui_root.on('click.wed', this._mouseHandler.bind(this));
    // No click in the next binding because click does not
    // distinguish left, middle, right mouse buttons.
    this.$gui_root.on('keyup mouseup',
                      this._caretChangeEmitter.bind(this));
    this.$gui_root.on('caretchange',
                      this._caretChangeHandler.bind(this));
    this.$gui_root.on('contextmenu',
                      util.eventHandler(this._contextMenuHandler.bind(this)));

    var nav_links = [];
    this.$gui_root.find(".head").each(function (x, el) {
        nav_links.push("<li><a href='#" + el.id + "'>" + $(el).text() +
                       "</a></li>");
    });
    this.$sidebar.find("#navlist>.inactive").replaceWith(nav_links.join(""));

    this.validator.start();

    this.domlistener.processImmediately();
    // Flush whatever has happened earlier.
    this._undo = new undo.UndoList();
    this._setCondition("initialized", {editor: this});
};

/**
 * This method asks the editor to process any pending operation which
 * may impact the display. This method is meant to be used internally
 * by the editor and for testing/debugging purposes
 * only. Consequently, the various modes that customize wed's behavior
 * must not use this method. If calling this method makes a mode that
 * did not work well work normally, then either the code that was not
 * working well is buggy (and calling _syncDisplay is not the
 * solution) or there is a bug in wed that should be fixed.
 */
Editor.prototype._syncDisplay = function () {
    this.copy_domlistener.processImmediately();
    this.domlistener.processImmediately();
};

/**
 * @param {module:transformation~Transformation} tr The transformation
 * to fire.
 * @param {Node} node The DOM node to fire it on. This can be a node
 * from the GUI tree or from the data tree.
 * @param {String} element_name The element_name to use in the
 * description of the transformation.
 * @param transformation_data Arbitrary data to be passed to the
 * transformation.
 * @param {Array} new_caret_position A new caret position in the data
 * tree.
 */
Editor.prototype.fireTransformation = function(tr, node, element_name,
                                               transformation_data,
                                               new_caret_position) {
    this.dismissContextMenu();

    if (new_caret_position)
        this.setDataCaret(new_caret_position);

    if (this._raw_caret === undefined)
        throw new Error("transformation applied with undefined caret.");
    var current_group = this._undo.getGroup();
    if (current_group !== undefined) {
        if (!(current_group instanceof wundo.TextUndoGroup))
            throw new Error("unexpected group class in _fireTransformation");
        this._undo.endGroup();
    }

    this._undo.startGroup(
        new wundo.UndoGroup("Undo " + tr.getDescriptionFor(element_name),
                            this));
    if (node !== undefined) {
        // Convert the gui node to a data node
        if ($(node).closest(this.$gui_root).length > 0) {
            var path = this.nodeToPath(node);
            node = domutil.pathToNode(this.$data_root.get(0), path);
        }
        else if ($(node).closest(this.$data_root).length === 0) {
            throw new Error("node is neither in the gui tree nor "+
                            "the data tree");
        }
    }
    tr.handler(this, node, element_name, transformation_data);
    // Ensure that all operations that derive from this
    // transformation are done *now*.
    this._syncDisplay();
    this._undo.endGroup();
};

Editor.prototype._fireTransformation = function (e, new_caret_position) {
    this.fireTransformation(e.data.tr, e.data.node, e.data.element_name,
                            undefined, new_caret_position);
};

Editor.prototype.recordUndo = function (undo) {
    this._undo.record(undo);
};

Editor.prototype.undo = function () {
    this._suppress_undo_recording = true;
    this._undo.undo();
    this.copy_domlistener.processImmediately();
    this.domlistener.processImmediately();
    this._suppress_undo_recording = false;
};

Editor.prototype.redo = function () {
    this._suppress_undo_recording = true;
    this._undo.redo();
    this.copy_domlistener.processImmediately();
    this.domlistener.processImmediately();
    this._suppress_undo_recording = false;
};

Editor.prototype.undoingOrRedoing = function () {
    return this._undo.undoingOrRedoing();
};

Editor.prototype._contextMenuHandler = function (e, jQthis) {
    // If the caret is changing due to a click on a placeholder,
    // then put it inside the placeholder.
    var $ph = $(e.target).closest("._placeholder");
    if ($ph.length > 0)
        this.setCaret($ph.get(0).childNodes[0], 0);

    var selection = rangy.getSelection(this.my_window);
    var original_range = domutil.getSelectionRange(this.my_window);
    var range = original_range.cloneRange();
    var start_is_focus = ((selection.focusNode === range.startContainer) &&
                          (selection.focusOffset === range.startOffset));
    range.collapse(start_is_focus);

    var $node_of_interest;
    var offset;
    if (range.startContainer.nodeType === Node.ELEMENT_NODE) {
        $node_of_interest = $(range.startContainer);
        offset = range.startOffset;
    }
    else {
        $node_of_interest = $(range.startContainer.parentNode);
        offset = Array.prototype.indexOf.call(
            range.startContainer.parentNode.childNodes,
            range.startContainer);
{'fraction_non_alphanumeric': 0.1447287270824378, 'fraction_numerical': 0.003999476703982656, 'mean_word_length': 2.2804855618907487, 'pattern_counts': {'":': 0, '<': 181, '<?xml version=': 0, '>': 211, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 12}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 18, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5953831', 'n_tokens_mistral': 15892, 'n_tokens_neox': 15385, 'n_words': 3617}