User:DarTar/wg system.js

Currently, User:DarTar/wg system.js is a topic that has captured the attention of millions of people around the world. From its origin to its current implications, User:DarTar/wg system.js has been the subject of debates, studies and research that have attempted to decipher its impact on modern society. Whether from a historical, scientific, social or cultural perspective, User:DarTar/wg system.js has aroused the interest of experts in different fields, who have dedicated time and effort to understand its influence on our lives. In this article, we will delve into the fascinating world of User:DarTar/wg system.js and explore its many facets, analyzing its relevance and projection in the current context.
/*!
 * jQuery contextMenu - Plugin for simple contextMenu handling
 *
 * Version: 1.6.6
 *
 * Authors: Rodney Rehm, Addy Osmani (patches for FF)
 * Web: http://medialize.github.com/jQuery-contextMenu/
 *
 * Licensed under
 *   MIT License http://www.opensource.org/licenses/mit-license
 *   GPL v3 http://opensource.org/licenses/GPL-3.0
 *
 */
// <syntaxhighlight lang=javascript>
(function($, undefined){
    
    // TODO: -
        // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio
        // create <menu> structure if $.support and !opt.disableNative

// determine html5 compatibility
$.support.htmlMenuitem = ('HTMLMenuItemElement' in window);
$.support.htmlCommand = ('HTMLCommandElement' in window);
$.support.eventSelectstart = ("onselectstart" in document.documentElement);
/* // should the need arise, test for css user-select
$.support.cssUserSelect = (function(){
    var t = false,
        e = document.createElement('div');
    
    $.each('Moz|Webkit|Khtml|O|ms|Icab|'.split('|'), function(i, prefix) {
        var propCC = prefix + (prefix ? 'U' : 'u') + 'serSelect',
            prop = (prefix ? ('-' + prefix.toLowerCase() + '-') : '') + 'user-select';
            
        e.style.cssText = prop + ': text;';
        if (e.style == 'text') {
            t = true;
            return false;
        }
        
        return true;
    });
    
    return t;
})();
*/

if (!$.ui || !$.ui.widget) {
    // duck punch $.cleanData like jQueryUI does to get that remove event
    // https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js#L16-24
    var _cleanData = $.cleanData;
    $.cleanData = function( elems ) {
        for ( var i = 0, elem; (elem = elems) != null; i++ ) {
            try {
                $( elem ).triggerHandler( "remove" );
                // http://bugs.jquery.com/ticket/8235
            } catch( e ) {}
        }
        _cleanData( elems );
    };
}

var // currently active contextMenu trigger
    $currentTrigger = null,
    // is contextMenu initialized with at least one menu?
    initialized = false,
    // window handle
    $win = $(window),
    // number of registered menus
    counter = 0,
    // mapping selector to namespace
    namespaces = {},
    // mapping namespace to options
    menus = {},
    // custom command type handlers
    types = {},
    // default values
    defaults = {
        // selector of contextMenu trigger
        selector: null,
        // where to append the menu to
        appendTo: null,
        // method to trigger context menu 
        trigger: "right",
        // hide menu when mouse leaves trigger / menu elements
        autoHide: false,
        // ms to wait before showing a hover-triggered context menu
        delay: 200,
        // flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu
        // as long as the trigger happened on one of the trigger-element's child nodes
        reposition: true,
        // determine position to show menu at
        determinePosition: function($menu) {
            // position to the lower middle of the trigger element
            if ($.ui && $.ui.position) {
                // .position() is provided as a jQuery UI utility
                // (...and it won't work on hidden elements)
                $menu.css('display', 'block').position({
                    my: "center top",
                    at: "center bottom",
                    of: this,
                    offset: "0 5",
                    collision: "fit"
                }).css('display', 'none');
            } else {
                // determine contextMenu position
                var offset = this.offset();
                offset.top += this.outerHeight();
                offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;
                $menu.css(offset);
            }
        },
        // position menu
        position: function(opt, x, y) {
            var $this = this,
                offset;
            // determine contextMenu position
            if (!x && !y) {
                opt.determinePosition.call(this, opt.$menu);
                return;
            } else if (x === "maintain" && y === "maintain") {
                // x and y must not be changed (after re-show on command click)
                offset = opt.$menu.position();
            } else {
                // x and y are given (by mouse event)
                offset = {top: y, left: x};
            }
            
            // correct offset if viewport demands it
            var bottom = $win.scrollTop() + $win.height(),
                right = $win.scrollLeft() + $win.width(),
                height = opt.$menu.height(),
                width = opt.$menu.width();
            
            if (offset.top + height > bottom) {
                offset.top -= height;
            }
            
            if (offset.left + width > right) {
                offset.left -= width;
            }
            
            opt.$menu.css(offset);
        },
        // position the sub-menu
        positionSubmenu: function($menu) {
            if ($.ui && $.ui.position) {
                // .position() is provided as a jQuery UI utility
                // (...and it won't work on hidden elements)
                $menu.css('display', 'block').position({
                    my: "left top",
                    at: "right top",
                    of: this,
                    collision: "flipfit fit"
                }).css('display', '');
            } else {
                // determine contextMenu position
                var offset = {
                    top: 0,
                    left: this.outerWidth()
                };
                $menu.css(offset);
            }
        },
        // offset to add to zIndex
        zIndex: 1,
        // show hide animation settings
        animation: {
            duration: 50,
            show: 'slideDown',
            hide: 'slideUp'
        },
        // events
        events: {
            show: $.noop,
            hide: $.noop
        },
        // default callback
        callback: null,
        // list of contextMenu items
        items: {}
    },
    // mouse position for hover activation
    hoveract = {
        timer: null,
        pageX: null,
        pageY: null
    },
    // determine zIndex
    zindex = function($t) {
        var zin = 0,
            $tt = $t;

        while (true) {
            zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);
            $tt = $tt.parent();
            if (!$tt || !$tt.length || "html body".indexOf($tt.prop('nodeName').toLowerCase()) > -1 ) {
                break;
            }
        }
        
        return zin;
    },
    // event handlers
    handle = {
        // abort anything
        abortevent: function(e){
            e.preventDefault();
            e.stopImmediatePropagation();
        },
        
        // contextmenu show dispatcher
        contextmenu: function(e) {
            var $this = $(this);
            
            // disable actual context-menu
            e.preventDefault();
            e.stopImmediatePropagation();
            
            // abort native-triggered events unless we're triggering on right click
            if (e.data.trigger != 'right' && e.originalEvent) {
                return;
            }
            
            // abort event if menu is visible for this trigger
            if ($this.hasClass('context-menu-active')) {
                return;
            }
            
            if (!$this.hasClass('context-menu-disabled')) {
                // theoretically need to fire a show event at <menu>
                // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
                // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this });
                // e.data.$menu.trigger(evt);
                
                $currentTrigger = $this;
                if (e.data.build) {
                    var built = e.data.build($currentTrigger, e);
                    // abort if build() returned false
                    if (built === false) {
                        return;
                    }
                    
                    // dynamically build menu on invocation
                    e.data = $.extend(true, {}, defaults, e.data, built || {});

                    // abort if there are no items to display
                    if (!e.data.items || $.isEmptyObject(e.data.items)) {
                        // Note: jQuery captures and ignores errors from event handlers
                        if (window.console) {
                            (console.error || console.log)("No items specified to show in contextMenu");
                        }
                        
                        throw new Error('No Items specified');
                    }
                    
                    // backreference for custom command type creation
                    e.data.$trigger = $currentTrigger;
                    
                    op.create(e.data);
                }
                // show menu
                op.show.call($this, e.data, e.pageX, e.pageY);
            }
        },
        // contextMenu left-click trigger
        click: function(e) {
            e.preventDefault();
            e.stopImmediatePropagation();
            $(this).trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
        },
        // contextMenu right-click trigger
        mousedown: function(e) {
            // register mouse down
            var $this = $(this);
            
            // hide any previous menus
            if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {
                $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');
            }
            
            // activate on right click
            if (e.button == 2) {
                $currentTrigger = $this.data('contextMenuActive', true);
            }
        },
        // contextMenu right-click trigger
        mouseup: function(e) {
            // show menu
            var $this = $(this);
            if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {
                e.preventDefault();
                e.stopImmediatePropagation();
                $currentTrigger = $this;
                $this.trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
            }
            
            $this.removeData('contextMenuActive');
        },
        // contextMenu hover trigger
        mouseenter: function(e) {
            var $this = $(this),
                $related = $(e.relatedTarget),
                $document = $(document);
            
            // abort if we're coming from a menu
            if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
                return;
            }
            
            // abort if a menu is shown
            if ($currentTrigger && $currentTrigger.length) {
                return;
            }
            
            hoveract.pageX = e.pageX;
            hoveract.pageY = e.pageY;
            hoveract.data = e.data;
            $document.on('mousemove.contextMenuShow', handle.mousemove);
            hoveract.timer = setTimeout(function() {
                hoveract.timer = null;
                $document.off('mousemove.contextMenuShow');
                $currentTrigger = $this;
                $this.trigger($.Event("contextmenu", { data: hoveract.data, pageX: hoveract.pageX, pageY: hoveract.pageY }));
            }, e.data.delay );
        },
        // contextMenu hover trigger
        mousemove: function(e) {
            hoveract.pageX = e.pageX;
            hoveract.pageY = e.pageY;
        },
        // contextMenu hover trigger
        mouseleave: function(e) {
            // abort if we're leaving for a menu
            var $related = $(e.relatedTarget);
            if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
                return;
            }
            
            try {
                clearTimeout(hoveract.timer);
            } catch(e) {}
            
            hoveract.timer = null;
        },
        
        // click on layer to hide contextMenu
        layerClick: function(e) {
            var $this = $(this),
                root = $this.data('contextMenuRoot'),
                mouseup = false,
                button = e.button,
                x = e.pageX,
                y = e.pageY,
                target,
                offset,
                selectors;
                
            e.preventDefault();
            e.stopImmediatePropagation();
            
            setTimeout(function() {
                var $window, hideshow, possibleTarget;
                var triggerAction = ((root.trigger == 'left' && button === 0) || (root.trigger == 'right' && button === 2));
                
                // find the element that would've been clicked, wasn't the layer in the way
                if (document.elementFromPoint) {
                    root.$layer.hide();
                    target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop());
                    root.$layer.show();
                }
                
                if (root.reposition && triggerAction) {
                    if (document.elementFromPoint) {
                        if (root.$trigger.is(target) || root.$trigger.has(target).length) {
                            root.position.call(root.$trigger, root, x, y);
                            return;
                        }
                    } else {
                        offset = root.$trigger.offset();
                        $window = $(window);
                        // while this looks kinda awful, it's the best way to avoid
                        // unnecessarily calculating any positions
                        offset.top += $window.scrollTop();
                        if (offset.top <= e.pageY) {
                            offset.left += $window.scrollLeft();
                            if (offset.left <= e.pageX) {
                                offset.bottom = offset.top + root.$trigger.outerHeight();
                                if (offset.bottom >= e.pageY) {
                                    offset.right = offset.left + root.$trigger.outerWidth();
                                    if (offset.right >= e.pageX) {
                                        // reposition
                                        root.position.call(root.$trigger, root, x, y);
                                        return;
                                    }
                                }
                            }
                        }
                    }
                }
                
                if (target && triggerAction) {
                    root.$trigger.one('contextmenu:hidden', function() {
                        $(target).contextMenu({x: x, y: y});
                    });
                }

                root.$menu.trigger('contextmenu:hide');
            }, 50);
        },
        // key handled :hover
        keyStop: function(e, opt) {
            if (!opt.isInput) {
                e.preventDefault();
            }
            
            e.stopPropagation();
        },
        key: function(e) {
            var opt = $currentTrigger.data('contextMenu') || {};

            switch (e.keyCode) {
                case 9:
                case 38: // up
                    handle.keyStop(e, opt);
                    // if keyCode is  or 
                    if (opt.isInput) {
                        if (e.keyCode == 9 && e.shiftKey) {
                            e.preventDefault();
                            opt.$selected && opt.$selected.find('input, textarea, select').blur();
                            opt.$menu.trigger('prevcommand');
                            return;
                        } else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
                            // checkboxes don't capture this key
                            e.preventDefault();
                            return;
                        }
                    } else if (e.keyCode != 9 || e.shiftKey) {
                        opt.$menu.trigger('prevcommand');
                        return;
                    }
                    // omitting break;
                    
                // case 9: // tab - reached through omitted break;
                case 40: // down
                    handle.keyStop(e, opt);
                    if (opt.isInput) {
                        if (e.keyCode == 9) {
                            e.preventDefault();
                            opt.$selected && opt.$selected.find('input, textarea, select').blur();
                            opt.$menu.trigger('nextcommand');
                            return;
                        } else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
                            // checkboxes don't capture this key
                            e.preventDefault();
                            return;
                        }
                    } else {
                        opt.$menu.trigger('nextcommand');
                        return;
                    }
                    break;
                
                case 37: // left
                    handle.keyStop(e, opt);
                    if (opt.isInput || !opt.$selected || !opt.$selected.length) {
                        break;
                    }
                
                    if (!opt.$selected.parent().hasClass('context-menu-root')) {
                        var $parent = opt.$selected.parent().parent();
                        opt.$selected.trigger('contextmenu:blur');
                        opt.$selected = $parent;
                        return;
                    }
                    break;
                    
                case 39: // right
                    handle.keyStop(e, opt);
                    if (opt.isInput || !opt.$selected || !opt.$selected.length) {
                        break;
                    }
                    
                    var itemdata = opt.$selected.data('contextMenu') || {};
                    if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) {
                        opt.$selected = null;
                        itemdata.$selected = null;
                        itemdata.$menu.trigger('nextcommand');
                        return;
                    }
                    break;
                
                case 35: // end
                case 36: // home
                    if (opt.$selected && opt.$selected.find('input, textarea, select').length) {
                        return;
                    } else {
                        (opt.$selected && opt.$selected.parent() || opt.$menu)
                            .children(':not(.disabled, .not-selectable)')()
                            .trigger('contextmenu:focus');
                        e.preventDefault();
                        return;
                    }
                    break;
                    
                case 13: // enter
                    handle.keyStop(e, opt);
                    if (opt.isInput) {
                        if (opt.$selected && !opt.$selected.is('textarea, select')) {
                            e.preventDefault();
                            return;
                        }
                        break;
                    }
                    opt.$selected && opt.$selected.trigger('mouseup');
                    return;
                    
                case 32: // space
                case 33: // page up
                case 34: // page down
                    // prevent browser from scrolling down while menu is visible
                    handle.keyStop(e, opt);
                    return;
                    
                case 27: // esc
                    handle.keyStop(e, opt);
                    opt.$menu.trigger('contextmenu:hide');
                    return;
                    
                default: // 0-9, a-z
                    var k = (String.fromCharCode(e.keyCode)).toUpperCase();
                    if (opt.accesskeys) {
                        // according to the specs accesskeys must be invoked immediately
                        opt.accesskeys.$node.trigger(opt.accesskeys.$menu
                            ? 'contextmenu:focus'
                            : 'mouseup'
                        );
                        return;
                    }
                    break;
            }
            // pass event to selected item,
            // stop propagation to avoid endless recursion
            e.stopPropagation();
            opt.$selected && opt.$selected.trigger(e);
        },

        // select previous possible command in menu
        prevItem: function(e) {
            e.stopPropagation();
            var opt = $(this).data('contextMenu') || {};

            // obtain currently selected menu
            if (opt.$selected) {
                var $s = opt.$selected;
                opt = opt.$selected.parent().data('contextMenu') || {};
                opt.$selected = $s;
            }
            
            var $children = opt.$menu.children(),
                $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),
                $round = $prev;
            
            // skip disabled
            while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) {
                if ($prev.prev().length) {
                    $prev = $prev.prev();
                } else {
                    $prev = $children.last();
                }
                if ($prev.is($round)) {
                    // break endless loop
                    return;
                }
            }
            
            // leave current
            if (opt.$selected) {
                handle.itemMouseleave.call(opt.$selected.get(0), e);
            }
            
            // activate next
            handle.itemMouseenter.call($prev.get(0), e);
            
            // focus input
            var $input = $prev.find('input, textarea, select');
            if ($input.length) {
                $input.focus();
            }
        },
        // select next possible command in menu
        nextItem: function(e) {
            e.stopPropagation();
            var opt = $(this).data('contextMenu') || {};

            // obtain currently selected menu
            if (opt.$selected) {
                var $s = opt.$selected;
                opt = opt.$selected.parent().data('contextMenu') || {};
                opt.$selected = $s;
            }

            var $children = opt.$menu.children(),
                $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),
                $round = $next;

            // skip disabled
            while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) {
                if ($next.next().length) {
                    $next = $next.next();
                } else {
                    $next = $children.first();
                }
                if ($next.is($round)) {
                    // break endless loop
                    return;
                }
            }
            
            // leave current
            if (opt.$selected) {
                handle.itemMouseleave.call(opt.$selected.get(0), e);
            }
            
            // activate next
            handle.itemMouseenter.call($next.get(0), e);
            
            // focus input
            var $input = $next.find('input, textarea, select');
            if ($input.length) {
                $input.focus();
            }
        },
        
        // flag that we're inside an input so the key handler can act accordingly
        focusInput: function(e) {
            var $this = $(this).closest('.context-menu-item'),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;

            root.$selected = opt.$selected = $this;
            root.isInput = opt.isInput = true;
        },
        // flag that we're inside an input so the key handler can act accordingly
        blurInput: function(e) {
            var $this = $(this).closest('.context-menu-item'),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;

            root.isInput = opt.isInput = false;
        },
        
        // :hover on menu
        menuMouseenter: function(e) {
            var root = $(this).data().contextMenuRoot;
            root.hovering = true;
        },
        // :hover on menu
        menuMouseleave: function(e) {
            var root = $(this).data().contextMenuRoot;
            if (root.$layer && root.$layer.is(e.relatedTarget)) {
                root.hovering = false;
            }
        },
        
        // :hover done manually so key handling is possible
        itemMouseenter: function(e) {
            var $this = $(this),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;
            
            root.hovering = true;

            // abort if we're re-entering
            if (e && root.$layer && root.$layer.is(e.relatedTarget)) {
                e.preventDefault();
                e.stopImmediatePropagation();
            }

            // make sure only one item is selected
            (opt.$menu ? opt : root).$menu
                .children('.hover').trigger('contextmenu:blur');

            if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) {
                opt.$selected = null;
                return;
            }
            
            $this.trigger('contextmenu:focus');
        },
        // :hover done manually so key handling is possible
        itemMouseleave: function(e) {
            var $this = $(this),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;

            if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {
                root.$selected && root.$selected.trigger('contextmenu:blur');
                e.preventDefault();
                e.stopImmediatePropagation();
                root.$selected = opt.$selected = opt.$node;
                return;
            }
            
            $this.trigger('contextmenu:blur');
        },
        // contextMenu item click
        itemClick: function(e) {
            var $this = $(this),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot,
                key = data.contextMenuKey,
                callback;

            // abort if the key is unknown or disabled or is a menu
            if (!opt.items || $this.is('.disabled, .context-menu-submenu, .context-menu-separator, .not-selectable')) {
                return;
            }

            e.preventDefault();
            e.stopImmediatePropagation();

            if ($.isFunction(root.callbacks) && Object.prototype.hasOwnProperty.call(root.callbacks, key)) {
                // item-specific callback
                callback = root.callbacks;
            } else if ($.isFunction(root.callback)) {
                // default callback
                callback = root.callback;
            } else {
                // no callback, no action
                return;
            }

            // hide menu if callback doesn't stop that
            if (callback.call(root.$trigger, key, root) !== false) {
                root.$menu.trigger('contextmenu:hide');
            } else if (root.$menu.parent().length) {
                op.update.call(root.$trigger, root);
            }
        },
        // ignore click events on input elements
        inputClick: function(e) {
            e.stopImmediatePropagation();
        },
        
        // hide <menu>
        hideMenu: function(e, data) {
            var root = $(this).data('contextMenuRoot');
            op.hide.call(root.$trigger, root, data && data.force);
        },
        // focus <command>
        focusItem: function(e) {
            e.stopPropagation();
            var $this = $(this),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;

            $this.addClass('hover')
                .siblings('.hover').trigger('contextmenu:blur');
            
            // remember selected
            opt.$selected = root.$selected = $this;
            
            // position sub-menu - do after show so dumb $.ui.position can keep up
            if (opt.$node) {
                root.positionSubmenu.call(opt.$node, opt.$menu);
            }
        },
        // blur <command>
        blurItem: function(e) {
            e.stopPropagation();
            var $this = $(this),
                data = $this.data(),
                opt = data.contextMenu,
                root = data.contextMenuRoot;
            
            $this.removeClass('hover');
            opt.$selected = null;
        }
    },
    // operations
    op = {
        show: function(opt, x, y) {
            var $trigger = $(this),
                offset,
                css = {};

            // hide any open menus
            $('#context-menu-layer').trigger('mousedown');

            // backreference for callbacks
            opt.$trigger = $trigger;

            // show event
            if (opt.events.show.call($trigger, opt) === false) {
                $currentTrigger = null;
                return;
            }

            // create or update context menu
            op.update.call($trigger, opt);
            
            // position menu
            opt.position.call($trigger, opt, x, y);

            // make sure we're in front
            if (opt.zIndex) {
                css.zIndex = zindex($trigger) + opt.zIndex;
            }
            
            // add layer
            op.layer.call(opt.$menu, opt, css.zIndex);
            
            // adjust sub-menu zIndexes
            opt.$menu.find('ul').css('zIndex', css.zIndex + 1);
            
            // position and show context menu
            opt.$menu.css( css )(opt.animation.duration, function() {
                $trigger.trigger('contextmenu:visible');
            });
            // make options available and set state
            $trigger
                .data('contextMenu', opt)
                .addClass("context-menu-active");
            
            // register key handler
            $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);
            // register autoHide handler
            if (opt.autoHide) {
                // mouse position handler
                $(document).on('mousemove.contextMenuAutoHide', function(e) {
                    // need to capture the offset on mousemove,
                    // since the page might've been scrolled since activation
                    var pos = $trigger.offset();
                    pos.right = pos.left + $trigger.outerWidth();
                    pos.bottom = pos.top + $trigger.outerHeight();
                    
                    if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) {
                        // if mouse in menu...
                        opt.$menu.trigger('contextmenu:hide');
                    }
                });
            }
        },
        hide: function(opt, force) {
            var $trigger = $(this);
            if (!opt) {
                opt = $trigger.data('contextMenu') || {};
            }
            
            // hide event
            if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) {
                return;
            }
            
            // remove options and revert state
            $trigger
                .removeData('contextMenu')
                .removeClass("context-menu-active");
            
            if (opt.$layer) {
                // keep layer for a bit so the contextmenu event can be aborted properly by opera
                setTimeout((function($layer) {
                    return function(){
                        $layer.remove();
                    };
                })(opt.$layer), 10);
                
                try {
                    delete opt.$layer;
                } catch(e) {
                    opt.$layer = null;
                }
            }
            
            // remove handle
            $currentTrigger = null;
            // remove selected
            opt.$menu.find('.hover').trigger('contextmenu:blur');
            opt.$selected = null;
            // unregister key and mouse handlers
            //$(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705
            $(document).off('.contextMenuAutoHide').off('keydown.contextMenu');
            // hide menu
            opt.$menu && opt.$menu(opt.animation.duration, function (){
                // tear down dynamically built menu after animation is completed.
                if (opt.build) {
                    opt.$menu.remove();
                    $.each(opt, function(key, value) {
                        switch (key) {
                            case 'ns':
                            case 'selector':
                            case 'build':
                            case 'trigger':
                                return true;

                            default:
                                opt = undefined;
                                try {
                                    delete opt;
                                } catch (e) {}
                                return true;
                        }
                    });
                }
                
                setTimeout(function() {
                    $trigger.trigger('contextmenu:hidden');
                }, 10);
            });
        },
        create: function(opt, root) {
            if (root === undefined) {
                root = opt;
            }
            // create contextMenu
            opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || "").data({
                'contextMenu': opt,
                'contextMenuRoot': root
            });
            
            $.each(, function(i,k){
                opt = {};
                if (!root) {
                    root = {};
                }
            });
            
            root.accesskeys || (root.accesskeys = {});
            
            // create contextMenu items
            $.each(opt.items, function(key, item){
                var $t = $('<li class="context-menu-item"></li>').addClass(item.className || ""),
                    $label = null,
                    $input = null;
                
                // iOS needs to see a click-event bound to an element to actually
                // have the TouchEvents infrastructure trigger the click event
                $t.on('click', $.noop);
                
                item.$node = $t.data({
                    'contextMenu': opt,
                    'contextMenuRoot': root,
                    'contextMenuKey': key
                });
                
                // register accesskey
                // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that
                if (item.accesskey) {
                    var aks = splitAccesskey(item.accesskey);
                    for (var i=0, ak; ak = aks; i++) {
                        if (!root.accesskeys) {
                            root.accesskeys = item;
                            item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>');
                            break;
                        }
                    }
                }
                
                if (typeof item == "string") {
                    $t.addClass('context-menu-separator not-selectable');
                } else if (item.type && types) {
                    // run custom type handler
                    types.call($t, item, opt, root);
                    // register commands
                    $.each(, function(i,k){
                        k.commands = item;
                        if ($.isFunction(item.callback)) {
                            k.callbacks = item.callback;
                        }
                    });
                } else {
                    // add label for input
                    if (item.type == 'html') {
                        $t.addClass('context-menu-html not-selectable');
                    } else if (item.type) {
                        $label = $('<label></label>').appendTo($t);
                        $('<span></span>').html(item._name || item.name).appendTo($label);
                        $t.addClass('context-menu-input');
                        opt.hasTypes = true;
                        $.each(, function(i,k){
                            k.commands = item;
                            k.inputs = item;
                        });
                    } else if (item.items) {
                        item.type = 'sub';
                    }
                
                    switch (item.type) {
                        case 'text':
                            $input = $('<input type="text" value="1" name="" value="">')
                                .attr('name', 'context-menu-input-' + key)
                                .val(item.value || "")
                                .appendTo($label);
                            break;
                    
                        case 'textarea':
                            $input = $('<textarea name=""></textarea>')
                                .attr('name', 'context-menu-input-' + key)
                                .val(item.value || "")
                                .appendTo($label);

                            if (item.height) {
                                $input.height(item.height);
                            }
                            break;

                        case 'checkbox':
                            $input = $('<input type="checkbox" value="1" name="" value="">')
                                .attr('name', 'context-menu-input-' + key)
                                .val(item.value || "")
                                .prop("checked", !!item.selected)
                                .prependTo($label);
                            break;

                        case 'radio':
                            $input = $('<input type="radio" value="1" name="" value="">')
                                .attr('name', 'context-menu-input-' + item.radio)
                                .val(item.value || "")
                                .prop("checked", !!item.selected)
                                .prependTo($label);
                            break;
                    
                        case 'select':
                            $input = $('<select name="">')
                                .attr('name', 'context-menu-input-' + key)
                                .appendTo($label);
                            if (item.options) {
                                $.each(item.options, function(value, text) {
                                    $('<option></option>').val(value).text(text).appendTo($input);
                                });
                                $input.val(item.selected);
                            }
                            break;
                        
                        case 'sub':
                            // FIXME: shouldn't this .html() be a .text()?
                            $('<span></span>').html(item._name || item.name).appendTo($t);
                            item.appendTo = item.$node;
                            op.create(item, root);
                            $t.data('contextMenu', item).addClass('context-menu-submenu');
                            item.callback = null;
                            break;
                        
                        case 'html':
                            $(item.html).appendTo($t);
                            break;
                        
                        default:
                            $.each(, function(i,k){
                                k.commands = item;
                                if ($.isFunction(item.callback)) {
                                    k.callbacks = item.callback;
                                }
                            });
                            // FIXME: shouldn't this .html() be a .text()?
                            $('<span></span>').html(item._name || item.name || "").appendTo($t);
                            break;
                    }
                    
                    // disable key listener in <input>
                    if (item.type && item.type != 'sub' && item.type != 'html') {
                        $input
                            .on('focus', handle.focusInput)
                            .on('blur', handle.blurInput);
                        
                        if (item.events) {
                            $input.on(item.events, opt);
                        }
                    }
                
                    // add icons
                    if (item.icon) {
                        $t.addClass("icon icon-" + item.icon);
                    }
                }
                
                // cache contained elements
                item.$input = $input;
                item.$label = $label;

                // attach item to menu
                $t.appendTo(opt.$menu);
                
                // Disable text selection
                if (!opt.hasTypes && $.support.eventSelectstart) {
                    // browsers support user-select: none,
                    // IE has a special event for text-selection
                    // browsers supporting neither will not be preventing text-selection
                    $t.on('selectstart.disableTextSelect', handle.abortevent);
                }
            });
            // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)
            if (!opt.$node) {
                opt.$menu.css('display', 'none').addClass('context-menu-root');
            }
            opt.$menu.appendTo(opt.appendTo || document.body);
        },
        resize: function($menu, nested) {
            // determine widths of submenus, as CSS won't grow them automatically
            // position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100;
            // kinda sucks hard...

            // determine width of absolutely positioned element
            $menu.css({position: 'absolute', display: 'block'});
            // don't apply yet, because that would break nested elements' widths
            // add a pixel to circumvent word-break issue in IE9 - #80
            $menu.data('width', Math.ceil($menu.width()) + 1);
            // reset styles so they allow nested elements to grow/shrink naturally
            $menu.css({
                position: 'static',
                minWidth: '0px',
                maxWidth: '100000px'
            });
            // identify width of nested menus
            $menu.find('> li > ul').each(function() {
                op.resize($(this), true);
            });
            // reset and apply changes in the end because nested
            // elements' widths wouldn't be calculatable otherwise
            if (!nested) {
                $menu.find('ul').addBack().css({
                    position: '',
                    display: '',
                    minWidth: '',
                    maxWidth: ''
                }).width(function() {
                    return $(this).data('width');
                });
            }
        },
        update: function(opt, root) {
            var $trigger = this;
            if (root === undefined) {
                root = opt;
                op.resize(opt.$menu);
            }
            // re-check disabled for each item
            opt.$menu.children().each(function(){
                var $item = $(this),
                    key = $item.data('contextMenuKey'),
                    item = opt.items,
                    disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true;

                // dis- / enable item
                $item('disabled');
                
                if (item.type) {
                    // dis- / enable input elements
                    $item.find('input, select, textarea').prop('disabled', disabled);
                    
                    // update input states
                    switch (item.type) {
                        case 'text':
                        case 'textarea':
                            item.$input.val(item.value || "");
                            break;
                            
                        case 'checkbox':
                        case 'radio':
                            item.$input.val(item.value || "").prop('checked', !!item.selected);
                            break;
                            
                        case 'select':
                            item.$input.val(item.selected || "");
                            break;
                    }
                }
                
                if (item.$menu) {
                    // update sub-menu
                    op.update.call($trigger, item, root);
                }
            });
        },
        layer: function(opt, zIndex) {
            // add transparent layer for click area
            // filter and background for Internet Explorer, Issue #23
            var $layer = opt.$layer = $('<div id="context-menu-layer" style="position:fixed; z-index:' + zIndex + '; top:0; left:0; opacity: 0; filter: alpha(opacity=0); background-color: #000;"></div>')
                .css({height: $win.height(), width: $win.width(), display: 'block'})
                .data('contextMenuRoot', opt)
                .insertBefore(this)
                .on('contextmenu', handle.abortevent)
                .on('mousedown', handle.layerClick);
            
            // IE6 doesn't know position:fixed;
            if (!$.support.fixedPosition) {
                $layer.css({
                    'position' : 'absolute',
                    'height' : $(document).height()
                });
            }
            
            return $layer;
        }
    };

// split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key
function splitAccesskey(val) {
    var t = val.split(/\s+/),
        keys = ;
        
    for (var i=0, k; k = t; i++) {
        k = k.toUpperCase(); // first character only
        // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it.
        // a map to look up already used access keys would be nice
        keys.push(k);
    }
    
    return keys;
}

// handle contextMenu triggers
$.fn.contextMenu = function(operation) {
    if (operation === undefined) {
        this.first().trigger('contextmenu');
    } else if (operation.x && operation.y) {
        this.first().trigger($.Event("contextmenu", {pageX: operation.x, pageY: operation.y}));
    } else if (operation === "hide") {
        var $menu = this.data('contextMenu').$menu;
        $menu && $menu.trigger('contextmenu:hide');
    } else if (operation === "destroy") {
        $.contextMenu("destroy", {context: this});
    } else if ($.isPlainObject(operation)) {
        operation.context = this;
        $.contextMenu("create", operation);
    } else if (operation) {
        this.removeClass('context-menu-disabled');
    } else if (!operation) {
        this.addClass('context-menu-disabled');
    }
    
    return this;
};

// manage contextMenu instances
$.contextMenu = function(operation, options) {
    if (typeof operation != 'string') {
        options = operation;
        operation = 'create';
    }
    
    if (typeof options == 'string') {
        options = {selector: options};
    } else if (options === undefined) {
        options = {};
    }
    
    // merge with default options
    var o = $.extend(true, {}, defaults, options || {});
    var $document = $(document);
    var $context = $document;
    var _hasContext = false;
    
    if (!o.context || !o.context.length) {
        o.context = document;
    } else {
        // you never know what they throw at you...
        $context = $(o.context).first();
        o.context = $context.get(0);
        _hasContext = o.context !== document;
    }
    
    switch (operation) {
        case 'create':
            // no selector no joy
            if (!o.selector) {
                throw new Error('No selector specified');
            }
            // make sure internal classes are not bound to
            if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) {
                throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className');
            }
            if (!o.build && (!o.items || $.isEmptyObject(o.items))) {
                throw new Error('No Items specified');
            }
            counter ++;
            o.ns = '.contextMenu' + counter;
            if (!_hasContext) {
                namespaces = o.ns;
            }
            menus = o;
            
            // default to right click
            if (!o.trigger) {
                o.trigger = 'right';
            }
            
            if (!initialized) {
                // make sure item click is registered first
                $document
                    .on({
                        'contextmenu:hide.contextMenu': handle.hideMenu,
                        'prevcommand.contextMenu': handle.prevItem,
                        'nextcommand.contextMenu': handle.nextItem,
                        'contextmenu.contextMenu': handle.abortevent,
                        'mouseenter.contextMenu': handle.menuMouseenter,
                        'mouseleave.contextMenu': handle.menuMouseleave
                    }, '.context-menu-list')
                    .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick)
                    .on({
                        'mouseup.contextMenu': handle.itemClick,
                        'contextmenu:focus.contextMenu': handle.focusItem,
                        'contextmenu:blur.contextMenu': handle.blurItem,
                        'contextmenu.contextMenu': handle.abortevent,
                        'mouseenter.contextMenu': handle.itemMouseenter,
                        'mouseleave.contextMenu': handle.itemMouseleave
                    }, '.context-menu-item');

                initialized = true;
            }
            
            // engage native contextmenu event
            $context
                .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);
            
            if (_hasContext) {
                // add remove hook, just in case
                $context.on('remove' + o.ns, function() {
                    $(this).contextMenu("destroy");
                });
            }
            
            switch (o.trigger) {
                case 'hover':
                        $context
                            .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)
                            .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);
                    break;
                    
                case 'left':
                        $context.on('click' + o.ns, o.selector, o, handle.click);
                    break;
                /*
                default:
                    // http://www.quirksmode.org/dom/events/contextmenu.html
                    $document
                        .on('mousedown' + o.ns, o.selector, o, handle.mousedown)
                        .on('mouseup' + o.ns, o.selector, o, handle.mouseup);
                    break;
                */
            }
            
            // create menu
            if (!o.build) {
                op.create(o);
            }
            break;
        
        case 'destroy':
            var $visibleMenu;
            if (_hasContext) {
                // get proper options
                var context = o.context;
                $.each(menus, function(ns, o) {
                    if (o.context !== context) {
                        return true;
                    }
                    
                    $visibleMenu = $('.context-menu-list').filter(':visible');
                    if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) {
                        $visibleMenu.trigger('contextmenu:hide', {force: true});
                    }

                    try {
                        if (menus.$menu) {
                            menus.$menu.remove();
                        }

                        delete menus;
                    } catch(e) {
                        menus = null;
                    }

                    $(o.context).off(o.ns);
                    
                    return true;
                });
            } else if (!o.selector) {
                $document.off('.contextMenu .contextMenuAutoHide');
                $.each(menus, function(ns, o) {
                    $(o.context).off(o.ns);
                });
                
                namespaces = {};
                menus = {};
                counter = 0;
                initialized = false;
                
                $('#context-menu-layer, .context-menu-list').remove();
            } else if (namespaces) {
                $visibleMenu = $('.context-menu-list').filter(':visible');
                if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) {
                    $visibleMenu.trigger('contextmenu:hide', {force: true});
                }
                
                try {
                    if (menus].$menu) {
                        menus].$menu.remove();
                    }
                    
                    delete menus];
                } catch(e) {
                    menus] = null;
                }
                
                $document.off(namespaces);
            }
            break;
        
        case 'html5':
            // if <command> or <menuitem> are not handled by the browser,
            // or options was a bool true,
            // initialize $.contextMenu for them
            if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options == "boolean" && options)) {
                $('menu').each(function() {
                    if (this.id) {
                        $.contextMenu({
                            selector: '',
                            items: $.contextMenu.fromMenu(this)
                        });
                    }
                }).css('display', 'none');
            }
            break;
        
        default:
            throw new Error('Unknown operation "' + operation + '"');
    }
    
    return this;
};

// import values into <input> commands
$.contextMenu.setInputValues = function(opt, data) {
    if (data === undefined) {
        data = {};
    }
    
    $.each(opt.inputs, function(key, item) {
        switch (item.type) {
            case 'text':
            case 'textarea':
                item.value = data || "";
                break;

            case 'checkbox':
                item.selected = data ? true : false;
                break;
                
            case 'radio':
                item.selected = (data || "") == item.value ? true : false;
                break;
            
            case 'select':
                item.selected = data || "";
                break;
        }
    });
};

// export values from <input> commands
$.contextMenu.getInputValues = function(opt, data) {
    if (data === undefined) {
        data = {};
    }
    
    $.each(opt.inputs, function(key, item) {
        switch (item.type) {
            case 'text':
            case 'textarea':
            case 'select':
                data = item.$input.val();
                break;

            case 'checkbox':
                data = item.$input.prop('checked');
                break;
                
            case 'radio':
                if (item.$input.prop('checked')) {
                    data = item.value;
                }
                break;
        }
    });
    
    return data;
};

// find <label for="xyz">
function inputLabel(node) {
    return (node.id && $('label').val()) || node.name;
}

// convert <menu> to items object
function menuChildren(items, $children, counter) {
    if (!counter) {
        counter = 0;
    }
    
    $children.each(function() {
        var $node = $(this),
            node = this,
            nodeName = this.nodeName.toLowerCase(),
            label,
            item;
        
        // extract <label><input>
        if (nodeName == 'label' && $node.find('input, textarea, select').length) {
            label = $node.text();
            $node = $node.children().first();
            node = $node.get(0);
            nodeName = node.nodeName.toLowerCase();
        }
        
        /*
         * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
         * Not being the sadistic kind, $.contextMenu only accepts:
         * <command>, <menuitem>, <hr>, <span>, <p> <input >, <textarea>, <select> and of course <menu>.
         * Everything else will be imported as an html node, which is not interfaced with contextMenu.
         */
        
        // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
        switch (nodeName) {
            // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
            case 'menu':
                item = {name: $node.attr('label'), items: {}};
                counter = menuChildren(item.items, $node.children(), counter);
                break;
            
            // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
            case 'a':
            // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
            case 'button':
                item = {
                    name: $node.text(),
                    disabled: !!$node.attr('disabled'),
                    callback: (function(){ return function(){ $node.click(); }; })()
                };
                break;
            
            // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command

            case 'menuitem':
            case 'command':
                switch ($node.attr('type')) {
                    case undefined:
                    case 'command':
                    case 'menuitem':
                        item = {
                            name: $node.attr('label'),
                            disabled: !!$node.attr('disabled'),
                            callback: (function(){ return function(){ $node.click(); }; })()
                        };
                        break;
                        
                    case 'checkbox':
                        item = {
                            type: 'checkbox',
                            disabled: !!$node.attr('disabled'),
                            name: $node.attr('label'),
                            selected: !!$node.attr('checked')
                        };
                        break;
                        
                    case 'radio':
                        item = {
                            type: 'radio',
                            disabled: !!$node.attr('disabled'),
                            name: $node.attr('label'),
                            radio: $node.attr('radiogroup'),
                            value: $node.attr('id'),
                            selected: !!$node.attr('checked')
                        };
                        break;
                        
                    default:
                        item = undefined;
                }
                break;
 
            case 'hr':
                item = '-------';
                break;
                
            case 'input':
                switch ($node.attr('type')) {
                    case 'text':
                        item = {
                            type: 'text',
                            name: label || inputLabel(node),
                            disabled: !!$node.attr('disabled'),
                            value: $node.val()
                        };
                        break;
                        
                    case 'checkbox':
                        item = {
                            type: 'checkbox',
                            name: label || inputLabel(node),
                            disabled: !!$node.attr('disabled'),
                            selected: !!$node.attr('checked')
                        };
                        break;
                        
                    case 'radio':
                        item = {
                            type: 'radio',
                            name: label || inputLabel(node),
                            disabled: !!$node.attr('disabled'),
                            radio: !!$node.attr('name'),
                            value: $node.val(),
                            selected: !!$node.attr('checked')
                        };
                        break;
                    
                    default:
                        item = undefined;
                        break;
                }
                break;
                
            case 'select':
                item = {
                    type: 'select',
                    name: label || inputLabel(node),
                    disabled: !!$node.attr('disabled'),
                    selected: $node.val(),
                    options: {}
                };
                $node.children().each(function(){
                    item.options = $(this).text();
                });
                break;
                
            case 'textarea':
                item = {
                    type: 'textarea',
                    name: label || inputLabel(node),
                    disabled: !!$node.attr('disabled'),
                    value: $node.val()
                };
                break;
            
            case 'label':
                break;
            
            default:
                item = {type: 'html', html: $node.clone(true)};
                break;
        }
        
        if (item) {
            counter++;
            items = item;
        }
    });
    
    return counter;
}

// convert html5 menu
$.contextMenu.fromMenu = function(element) {
    var $this = $(element),
        items = {};
        
    menuChildren(items, $this.children());
    
    return items;
};

// make defaults accessible
$.contextMenu.defaults = defaults;
$.contextMenu.types = types;
// export internal functions - undocumented, for hacking only!
$.contextMenu.handle = handle;
$.contextMenu.op = op;
$.contextMenu.menus = menus;

})(jQuery);
/* Simple JavaScript Inheritance
 * By John Resig http://ejohn.org/
 * MIT Licensed.
 */
// Inspired by base2 and Prototype
(function(){
  var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
  // The base Class implementation (does nothing)
  this.Class = function(){};
  
  // Create a new Class that inherits from this class
  Class.extend = function(prop) {
    var _super = this.prototype;
    
    // Instantiate a base class (but only create the instance,
    // don't run the init constructor)
    initializing = true;
    var prototype = new this();
    initializing = false;
    
    // Copy the properties over onto the new prototype
    for (var name in prop) {
      // Check if we're overwriting an existing function
      prototype = typeof prop == "function" && 
        typeof _super == "function" && fnTest.test(prop) ?
        (function(name, fn){
          return function() {
            var tmp = this._super;
            
            // Add a new ._super() method that is the same method
            // but on the super-class
            this._super = _super;
            
            // The method only need to be bound temporarily, so we
            // remove it when we're done executing
            var ret = fn.apply(this, arguments);        
            this._super = tmp;
            
            return ret;
          };
        })(name, prop) :
        prop;
    }
    
    // The dummy class constructor
    function Class() {
      // All construction is actually done in the init method
      if ( !initializing && this.init )
        this.init.apply(this, arguments);
    }
    
    // Populate our constructed prototype object
    Class.prototype = prototype;
    
    // Enforce the constructor to be what we expect
    Class.prototype.constructor = Class;

    // And make this class extendable
    Class.extend = arguments.callee;
    
    return Class;
  };
})();
if(!window.WG){WG = {}}

WG.ConsoleMessage = function(type, prefix, message){
	this.type = type
	this.prefix = prefix
	this.message = message
	
	this.div = $('<div class="message" />')
		.append(
			$('<span class="preflix" />')
				.addClass(type)
				.text(prefix + ": ")
		)
		.append(message)
	
}
	WG.ConsoleMessage.prototype.toString = function(){
		return this.prefix + ": " + this.message
	}

WG.Console = function(){
	this.pane = $('<div class="console"></div>')
	this.info("Console loaded...")
	$(document).ready(function(self){
			return function(){
				self.pane.prependTo($('#content'))
			}
		}(this)
	)
}
WG.Console.prototype.log = function(consoleMessage){
	this.pane.prepend(consoleMessage.div)
}
WG.Console.prototype.info = function(message){
	this.log(new WG.ConsoleMessage("info", "INFO", message))
}
WG.Console.prototype.warning = function(message){
	this.log(new WG.ConsoleMessage("warning", "WARNING", message))
}
WG.Console.prototype.error = function(message){
	this.log(new WG.ConsoleMessage("error", "ERROR", message))
}

WG.HiddenConsole = function(){
	this.messageLog = 
}
WG.HiddenConsole.prototype.log = function(consoleMessage){
	out = '...\n'
	if(consoleMessage.type == "error"){
		for(var i=Math.max(0, this.messageLog.length-5); i < this.messageLog.length;i++){
			message = this.messageLog
			out += message.toString() + "\n"
		}
		out += consoleMessage.toString()
		alert("An error occurred: \n\n" + out)
	}
	this.messageLog.push(consoleMessage)
	if(window.console){
		console.log(consoleMessage.toString())
	}
}
WG.HiddenConsole.prototype.info = function(message){
	this.log(new WG.ConsoleMessage("info", "INFO", message))
}
WG.HiddenConsole.prototype.warning = function(message){
	this.log(new WG.ConsoleMessage("warning", "WARNING", message))
}
WG.HiddenConsole.prototype.error = function(message){
	this.log(new WG.ConsoleMessage("error", "ERROR", message))
}

if(!window.WG){WG = {}}

$.extend(WG, {
	NOTE_TEMPLATE: 'User:EpochFail/Snote'
})

$.extend(WG, {
	NOTE_TEMPLATE_RE: RegExp(
		'\\{\\{*' + 
		WG.NOTE_TEMPLATE.replace(/\:/g, "\\:").replace(/\//g, "\\/") + '*' + 
		'\\|?(*?)(\\|.*)*\\}\\}'
	),
	TOKEN_MAP: {
		white_space:         ' +',
		number:              "+(\\.+)?|(\\.+)",
		word:                '\\w+',
		entity:              '&\\w+;',
		list_item:           '\\n+(\\*|\\#|\\:)+',
		def_item:            '\\n+\\;',
		header:              '(\\n+|^)(=.+?=|==.+?==|===.+?===|====.+?====|=====.+?=====)',
		open_table:          '\\n*\\{\\||<table*>',
		paragraph_break:     '\\n{2,}',
		open_note:           '\\{\\{*' + 
		                     WG.NOTE_TEMPLATE.replace(/\:/g, "\\:").replace(/\//g, "\\/") + 
		                     "*\\|?",
		open_image:          '\\[\\[(Image|File):',
		open_internal_link:  '\\[\\[',
		close_internal_link: '\\]\\]',
		external_link:       '\\\\n]+\\]',
		open_template:       '\\{\\{',
		close_template:      '\\}\\}',
		comment:             '<!--(.|\\n|\\r)*?-->',
		ref:                 '<ref*/>|<ref*>(.|\\n|\\r)*?</ref>',
		math:                '<math*>(.|\\n|\\r)*?</math>',
		pre:                 '<pre*>(.|\\n|\\r)*?</pre>',
		source:              '<source*>(.|\\n|\\r)*?</source>',
		nowiki:              '<nowiki*>(.|\\n|\\r)*?</nowiki>',
		gallery:             '<gallery*>(.|\\n|\\r)*?</gallery>',
		close_table:         '\\|\\}|</table>',
		open_div:            '<div*>',
		close_div:           '</div>',
		table_row:           '\\|-',
		line_break:          '\\n',
		open_markup:         '<*>',
		close_markup:        '<\\/*>',
		bold:                "'''",
		italics:             "''",
		quote:               "'|\\\"",
		ellipsis:            '\\.\\.\\.',
		period:              '\\.',
		comma:               ',',
		exlamation:          '!',
		question:            '\\?',
		colon:               '\\:',
		semicolon:           '\\;',
		bar:                 '\\|',
		other:               '.'
	},
	concatTokens: function(tokens, lower, upper){
		lower = lower||0
		upper = upper||tokens.length
		
		str = ''
		for(var i = lower; i < upper; i++){
			str += tokens.c
		}
		return str
	},
	NOTE_TEMPLATE: 'User:EpochFail/Snote'
})

/**
Converts MediaWiki markup into tokens.  For possible tokens and the regexp 
they match, see `WG.TOKEN_MAP`.  An arbitrary map of tokens can be provided.

:Parameters:
	markup : String
		mediawiki markup to be tokenized
	tokens : Object
		a map from "<token name>" to "<token regexp>" (as a string)
*/
WG.Tokenizer = Class.extend({
	init: function(markup, tokens){
		this.tokens = tokens || WG.TOKEN_MAP
		this.markup = markup
		var expressionParts = 
		for(type in this.tokens){
			expressionParts.push(this.tokens)
		}
		this.tokenRE = RegExp(expressionParts.join("|"), "gi")
		this.lookAhead = this.__nextToken()
	},

	/**
	Returns an array of all tokens found in `markup'.
	*/
	popAll: function(){
		var tokens = 
		while(this.peek()){
			tokens.push(this.pop())
		}
		return tokens
	},
	
	/**
	Returns the next token without removing it.
	*/
	peek: function(){
		if(this.lookAhead){
			return this.lookAhead
		}else{
			return null
		}
	},
	
	/**
	Returns and removes the next token.
	*/
	pop: function(){
		if(this.lookAhead){
			var temp = this.lookAhead
			var lastTime = new Date().getTime()/1000
			this.lookAhead = this.__nextToken()
			WG.WAIT_TIME += (new Date().getTime()/1000) - lastTime
			return temp
		}else{
			return null
		}
	},
	
	__nextToken: function(){
		var timeBefore = new Date().getTime() / 1000
		var match = this.tokenRE.exec(this.markup)
		WG.WAIT_TIME += (new Date().getTime() / 1000) - timeBefore
		if(!match){
			return null
		}else{
			var content = match
			for(type in this.tokens){
				var re = RegExp("^" + this.tokens + "$", "gi")
				if(re.test(content)){
					return {t:type, c:content}
				}
			}
			throw "Unexpected token content '" + content + "' matched no known token types."
		}
	}
})

/**
Converts MediaWiki markup into chunks of content.  Possible chunks to be 
returned include:
- header
- note
- template
- table
- list item
- div
- paragraph break
- line break
- sentence

 All chunks follow the scheme: 
     {t:"<type>", id:<chunkId>, c:"<content>"}

Joining the token content of chunks in order should reproduce the original 
MediaWiki markup.

:Parameters:
	markup : String
		mediawiki markup to be tokenized
	tokens : Object
		a map from "<token name>" to "<token regexp>" (as a string)
 */
WG.Chunker = function(markup, tokens){
	this.tokenizer = new WG.Tokenizer(markup, tokens)
	this.lookAhead = this.__nextChunk()
}
	
	/**
	Returns an array of all chunks found in `markup'.
	*/
	WG.Chunker.prototype.popAll = function(){
		var chunks = 
		while(this.peek()){
			chunks.push(this.pop())
		}
		return chunks
	}
	/**
	Returns the next chunk without removing it.
	*/
	WG.Chunker.prototype.peek = function(){
		if(this.lookAhead){
			return this.lookAhead
		}else{
			return null
		}
	}
	/**
	Returns and removes the next chunk
	*/
	WG.Chunker.prototype.pop = function(){
		if(this.lookAhead){
			var temp = this.lookAhead
			this.lookAhead = this.__nextChunk()
			return temp
		}else{
			return null
		}
	}
	WG.Chunker.prototype.__nextChunk = function(){
		if(!this.chunkId){
			this.chunkId   = 0
		}
		if(this.tokenizer.peek()){ //We have tokens to process
			switch(this.tokenizer.peek().t){
				case "open_template":
					return {
						t: "template", 
						c: WG.concatTokens(this.__template()), 
						id: this.chunkId++
					}
				case "open_note":
					var tokens = this.__note()
					return {
						t: "note", 
						c: WG.concatTokens(tokens), 
						id: this.chunkId++,
						val: WG.concatTokens(tokens, 1,tokens.length-1)
					}
				case "open_table":
					return {
						t: "table", 
						c: WG.concatTokens(this.__table()), 
						id: this.chunkId++
					}
				case "open_div":
					return {
						t: "div", 
						c: WG.concatTokens(this.__div()), 
						id: this.chunkId++
					}
				case "open_image":
					return {
						t: "image", 
						c: WG.concatTokens(this.__image()), 
						id: this.chunkId++
					}
				case "def_item":
					return {
						t: "definition", 
						c: WG.concatTokens(this.__definition()), 
						id: this.chunkId++
					}
				case "list_item":
					return {
						t: "def_list_item", 
						c: WG.concatTokens(),
						id: this.chunkId++
					}
				case "header":
					return {
						t: "header", 
						c: WG.concatTokens(), 
						id: this.chunkId++
					}
				case "paragraph_break":
					return {
						t: "paragraph_break", 
						c: WG.concatTokens(), 
						id: this.chunkId++
					}
				case "line_break":
					return {
						t: "break", 
						c: WG.concatTokens(),
						id: this.chunkId++
					}
				case "ref":
					return {
						t: "ref", 
						c: WG.concatTokens(), 
						id: this.chunkId++
					}
				/*case "math":
					return {
						t: "math", 
						c: WG.concatTokens(), 
						id: this.chunkId++
					}*/
				case "pre":
					return {
						t: "pre", 
						c: WG.concatTokens(), 
						id: this.chunkId++
					}
				case "gallery":
					return {
						t: "gallery", 
						c: WG.concatTokens(), 
						id: this.chunkId++
					}
				case "comment": 
					return {
						t: "comment",
						c: this.tokenizer.pop().c,
						id: this.chunkId++
					}
				case "white_space": 
					return {
						t: "white_space",
						c: this.tokenizer.pop().c,
						id: this.chunkId++
					}
				case "colon": 
					return {
						t: "colon",
						c: this.tokenizer.pop().c,
						id: this.chunkId++
					}
				case "source": 
					return {
						t: "source",
						c: this.tokenizer.pop().c,
						id: this.chunkId++
					}
				case "close_template":
				case "close_table":
				case "close_div":
				case "close_markup":
				case "exclamation":
				case "question":
				case "period":
				case "comma":
				case "table_row":
				case "close_internal_link":
				case "white_space":
				case "colon":
				case "semicolon":
				case "open_markup":
				case "italics":
				case "bold":
				case "quote":
				case "ellipsis":
				case "open_internal_link":
				case "external_link":
				case "entity":
				case "number":
				case "word":
				case "nowiki":
				case "math":
				case "other":
					tokens = this.__sentence()
					return {
						t: 'sentence', 
						c: WG.concatTokens(tokens), 
						id: this.chunkId++,
						tokens: tokens
					}
					break;
				default:
					throw "Unexpected token type '" + this.tokenizer.peek().t + "' found while generating chunks."
			}
		}else{ //No more tokens.  No more chunks. 
			return null
		}
	}
	WG.Chunker.prototype.__template = function(){
		var tokens = 
		var templates = 1
		while(this.tokenizer.peek() && templates > 0){
			switch(this.tokenizer.peek().t){
				case "open_template": //going one deeper!
					templates++
					break;
				case "close_template": //coming out of the templates
					templates--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	WG.Chunker.prototype.__div = function(){
		var tokens = 
		var divs = 1
		while(this.tokenizer.peek() && divs > 0){
			switch(this.tokenizer.peek().t){
				case "open_div": //going one deeper!
					divs++
					break;
				case "close_div": //coming out of the divs
					divs--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	WG.Chunker.prototype.__table = function(){
		var tokens = 
		var tables = 1
		while(this.tokenizer.peek() && tables > 0){
			switch(this.tokenizer.peek().t){
				case "open_table": //going one deeper!
					tables++
					break;
				case "close_table": //coming out of the tables
					tables--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	WG.Chunker.prototype.__definition = function(){
		var tokens = 
		var done = false
		while(this.tokenizer.peek() && !done){
			switch(this.tokenizer.peek().t){
				case "colon":           //---------------
					tokens.push(this.tokenizer.pop())
					tokens.push.apply(tokens, this.__extraDefinitionMatter())
					done = true;
					break;
				case "open_template":   //Suddently, a template.  Eat it and its children.
					tokens.push.apply(tokens, this.__template())
					break;
				case "open_internal_link": //Suddenly, a link.  Eat it and its children.
					tokens.push.apply(tokens, this.__internalLink())
					break;
				case "open_image": //Suddenly, an image.  Eat it and its children.
					tokens.push.apply(tokens, this.__image())
					break;
				case "line_break":
				case "definition_item":
				case "header":
				case "pre":
				case "gallery":
				case "source":
				case "open_note":       //----------------
				case "open_table":      //
				case "open_div":        //End of paragraph
				case "paragraph_break": //----------------
					done = true
					break;
				default:                //Everything else
					tokens.push(this.tokenizer.pop())
			}
		}
		return tokens
	}
	WG.Chunker.prototype.__extraDefinitionMatter = function(){
		var tokens = 
		var done = false
		while(this.tokenizer.peek() && !done){
			switch(this.tokenizer.peek().t){
				case "open_template":   //Suddently, a template.  Eat it and its children.
					tokens.push.apply(tokens, this.__template())
					break;
				case "open_internal_link": //Suddenly, a link.  Eat it and its children.
					tokens.push.apply(tokens, this.__internalLink())
					break;
				case "open_image": //Suddenly, an image.  Eat it and its children.
					tokens.push.apply(tokens, this.__image())
					break;
				case "line_break":
				case "def_item":
				case "header":
				case "pre":
				case "gallery":
				case "source":
				case "open_note":       //----------------
				case "open_table":      //
				case "open_div":        //End of paragraph
				case "paragraph_break": //----------------
					done = true
					break;
				default:                //Everything else
					tokens.push(this.tokenizer.pop())
			}
		}
		return tokens
	}
	WG.Chunker.prototype.__sentence = function(){
		var tokens = 
		var done = false
		while(this.tokenizer.peek() && !done){
			switch(this.tokenizer.peek().t){
				case "exclamation":     //---------------
				case "question":        //End of sentence
				case "period":          //
				case "colon":           //
				case "ellipsis":        //---------------
					tokens.push(this.tokenizer.pop())
					tokens.push.apply(tokens, this.__extraSentenceMatter())
					done = true;
					break;
				case "open_template":   //Suddently, a template.  Eat it and its children.
					tokens.push.apply(tokens, this.__template())
					break;
				case "open_internal_link": //Suddenly, a link.  Eat it and its children.
					tokens.push.apply(tokens, this.__internalLink())
					break;
				case "open_image": //Suddenly, an image.  Eat it and its children.
					tokens.push.apply(tokens, this.__image())
					break;
				case "def_item":
				case "header":
				case "pre":
				case "gallery":
				case "source":
				case "open_note":       //----------------
				case "open_table":      //
				case "open_div":        //End of paragraph
				case "paragraph_break": //----------------
					done = true
					break;
				default:                //Everything else
					tokens.push(this.tokenizer.pop())
			}
		}
		return tokens
	}
	WG.Chunker.prototype.__extraSentenceMatter = function(){
		var tokens = 
		var done = false
		while(this.tokenizer.peek() && !done){
			switch(this.tokenizer.peek().t){
				case "open_template":   //Suddently, a template.  Eat it and its children.
					tokens.push.apply(tokens, this.__template())
					break;
				case "open_internal_link": //----------------
				case "open_note":          //
				case "open_image":         // New element
				case "def_item":      //
				case "list_item":      //
				case "header":             //
				case "source":             //
				case "gallery":            //
				case "pre":                //----------------
				case "open_table":      //----------------
				case "open_div":        //New paragraph
				case "paragraph_break": //----------------
				case "open_markup":         //----------------
				case "italics":             //
				case "bold":                //
				case "open_internal_link":  // New sentence
				case "external_link":       //
				case "entity":              //
				case "word":                //
				case "other":               //----------------
					done = true
					break;
				default:                //Everything else
					tokens.push(this.tokenizer.pop())
			}
		}
		return tokens
	}
	WG.Chunker.prototype.__internalLink = function(){
		var tokens = 
		var links = 1
		while(this.tokenizer.peek() && links > 0){
			switch(this.tokenizer.peek().t){
				case "open_internal_link": //going one deeper!
					links++
					break;
				case "close_internal_link": //coming out of the links
					links--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	WG.Chunker.prototype.__image = function(){
		var tokens = 
		var links = 1
		while(this.tokenizer.peek() && links > 0){
			switch(this.tokenizer.peek().t){
				case "open_internal_link": //going one deeper!
					links++
					break;
				case "close_internal_link": //coming out of the links
					links--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	WG.Chunker.prototype.__note = function(){
		var tokens = 
		var templates = 1
		while(this.tokenizer.peek() && templates > 0){
			switch(this.tokenizer.peek().t){
				case "open_template": //going one deeper!
					templates++
					break;
				case "close_template": //coming out of the links
					templates--
					break;
			}
			tokens.push(this.tokenizer.pop())
		}
		return tokens
	}
	


if(!window.WG){WG = {}}

/**
A simple interface for interacting with a sentence in an article.

:Parameters:
	span : jQuery | DOM element
		the span containing the sentence to be edited

 */
WG.SentenceInteractor = Class.extend({
	init: function(span){
		this.span = $(span)
		if(this.span.hasClass("editing")){
			return
		}
		this.span.addClass("editing")
		var id = parseInt(this.span.attr("id").split("_"))
		this.chunk = WG.chunks.get(id)
		this.currentHTML = this.span.html()
		this.markup = {
			ltrim:   this.chunk.c.match(/^\s*/),
			rtrim:   this.chunk.c.match(/\s*$/),
			trimmed: $.trim(this.chunk.c)
		}
		
		this.div = $('<div />')
			.addClass("sentence_interactor")
			.insertAfter(this.span)
		
		this.pane = $('<div />')
			.addClass("pane")
			.css("position", "absolute")
			.appendTo(this.div)
			.hide()
		
		this.menu   = new WG.SentenceMenu(this)
		this.editor = new WG.SentenceEditor(this)
		this.editor.text(this.markup.trimmed)
		this.editor.summary(
			"Updating sentence starting with \"" + 
			this.markup.trimmed.substring(0, Math.min(50, this.markup.trimmed.length)) + 
			"...\""
		)
		this.show()
		this.resizer = function(interactor){return function(e){
			interactor.resize()
		}}(this)
		
		$(window).resize(this.resizer)
		
		//this.captureClickEvent = function(interactor){return function(e){
		//	if(e.button == 2 && e.ctrlKey){
		//		e.stopPropagation()
		//		return false;
		//	}
		//}}(this)
		//this.span.mousedown(this.captureClickEvent)
	},
	resize: function(){
		this.pane.css('width', $('#bodyContent .mw-content-ltr').innerWidth() - 15)
		this.div.css("height", this.pane.outerHeight())
	},
	preview: function(callback){
		WG.api.pages.preview(
			WG.PAGE_TITLE,
			this.markup.ltrim + 
			$.trim(this.editor.text()) + 
			this.markup.rtrim,
			function(interactor, callback){return function(html){
				html = html
					.replace(/<\/?p>/gi, '')
					.replace(/<br \/>\n<strong class="error">.*?<\/strong>/g, '')
				interactor.span.html(html)
				if(callback){callback(html)}
			}}(this, callback),
			function(interactor, callback){return function(error){
				WG.error(error)
			}}(this, callback)
		)
	},
	save: function(callback){
		this.editor.disable()
		
		//Update chunk
		this.chunk.c = this.markup.ltrim + 
			this.editor.text() + 
			this.markup.rtrim
		
		
		WG.chunks.save(
			this.menu.minor(),
			this.editor.summary() + WG.SUMMARY_SUFFIX,
			function(interactor, callback){return function(html){
				interactor.editor.enable()
				interactor.span.html(html)
				interactor.preview(
					function(interactor, callback){return function(html){
						interactor.currentHTML = html
						interactor.span.html(html)
						interactor.exit()
						if(callback){callback()}
					}}(interactor, callback)
				)
			}}(this, callback),
			function(interactor, callback){return function(error){
				WG.error(error)
				interactor.editor.enable()
			}}(this, callback)
		)
	},
	show: function(){
		this.pane.css('left', $('#bodyContent .mw-content-ltr').position().left - 15)
		this.pane.css('width', $('#bodyContent .mw-content-ltr').innerWidth() - 15)
		this.div.animate(
			{
				height: this.pane.outerHeight()
			},
			{
				duration: 200
			}
		)
		this.pane.slideDown(200)
	},
	hide: function(callback){
		this.pane.slideUp(200)
		this.div.animate(
			{height: 0}, 
			{
				duration: 200,
				complete: function(interactor, callback){return function(){
					interactor.div.hide()
					if(callback){callback()}
				}}(this, callback)
			}
		)
		
	},
	cancel: function(){
		this.span.html(this.currentHTML)
		this.exit()
	},
	exit: function(){
		this.hide(
			function(interactor){return function(){
				interactor.div.remove()
			}}(this)
		)
		this.span.removeClass("editing")
		$(window).unbind(this.resizer)
		//this.unbind('mousedown', this.captureClickEvent)
	}
})

WG.SentenceMenu = Class.extend({
	init: function(interactor){
		this.div = $('<div />')
			.addClass("menu")
			.prependTo(interactor.pane)
		
		
		this.cancel = $('<div />')
			.addClass("button")
			.addClass("cancel")
			.text("cancel")
			.attr("title", "cancel editing")
			.click(
				function(interactor){return function(){
					interactor.cancel()
				}}(interactor)
			)
			.appendTo(this.div)
		
		this.minorCheck = {
			div: $('<div />')
				.addClass("minor")
				.appendTo(this.div),
			label: $('<label />')
				.text("minor")
				.attr('for', "minor_sentence_edit"),
			checkbox: $('<input />')
				.attr('type', "checkbox")
				.attr('id', "minor_sentence_edit")
				.prop('checked', true)
		}
		this.minorCheck.div.append(this.minorCheck.label)
		this.minorCheck.div.append(this.minorCheck.checkbox)
		
		this.save = $('<div />')
			.addClass("button")
			.addClass("save")
			.addClass("primary")
			.text("save")
			.attr("title", "save your changes to the sentence")
			.click(
				function(interactor){return function(){
					interactor.save()
				}}(interactor)
			)
			.appendTo(this.div)
		
		this.preview = $('<div />')
			.addClass("button")
			.addClass("preview")
			.text("preview")
			.attr("title", "preview your change to the sentence")
			.click(
				function(interactor){return function(){
					interactor.preview()
				}}(interactor)
			)
			.appendTo(this.div)
	},
	minor: function(){
		return this.minorCheck.checkbox.is(":checked")
	},
	hide: function(){
		this.div.hide()
	},
	show: function(){
		this.div.show()
	}
})


WG.SentenceEditor = Class.extend({
	init: function(interactor){
		this.div = $('<div/>')
			.addClass('editor')
			.appendTo(interactor.pane)
		
		this.textPane = {
			textarea: $("<textarea />")
				.addClass("text")
				.appendTo(this.div)
		}
			
		this.summaryPane = {
			label: $('<label>')
				.text('Summary: ')
				.attr('for', interactor.chunk.id + "_summary")
				.appendTo(this.div),
			textarea: $("<textarea />")
				.addClass("summary")
				.appendTo(this.div)
				.attr('id', interactor.chunk.id + "_summary")
				.attr('rows', 1)
				
		}
	},
	
	text: function(val){
		if(val){
			this.textPane.textarea
				.attr("rows", Math.max(2, Math.ceil(val.length/80)))
				.val(val)
			
			return this
		}else{
			return this.textPane.textarea.val()
		}
	},
	
	summary: function(val){
		if(val){
			this.summaryPane.textarea.val(val)
			
			return this
		}else{
			return this.summaryPane.textarea.val()
		}
	},
	
	disable: function(){
		this.textPane.textarea.prop('disabled', true)
		this.summaryPane.textarea.prop('disabled', true)
	},
	
	enable: function(){
		this.textPane.textarea.prop('disabled', false)
		this.summaryPane.textarea.prop('disabled', false)
	}
})
if(!window.WG){WG = {}}

/**
Centers a jQuery element(s) horizontally in relation to another element (usually
a containing element).

:Parameters:
	of : DOM element | jQuery
		the element to center around

:Returns:
	this jQuery element
*/
jQuery.fn.center = function(of){
	of = $(of || window)
	this.css("position", "absolute")
	
	
	this.css(
		"left",
		(of.position().left + of.outerWidth()/2) - 
		(this.outerWidth()/2)
	)
	return this
}

/**
Gets the absolute bottom of an element including padding, borders and margin.

:Returns:
	int pixels of position
*/
jQuery.fn.outerBottom = function(){
	return this.position().top + this.outerHeight(true)
}

/**
Positions an element beneath another with a specified offset.

:Parameters:
	of : DOM element | jQuery
		the element to place beneath
	offset : int
		the number of pixels to offset the placement (defualts to zero)

:Returns:
	this jQuery element
*/
jQuery.fn.beneath = function(of, offset){
	offset = parseInt(offset || 0)
	of = $(of || $('body'))
	this.css("position", "absolute")
	this.css(
		"top",
		of.position().top + of.outerHeight(true) + offset
	)
	return this
}


WG.Gbutton = function(displayName){
	var innerSpan = $('<span />')
		.append($('<b />'))
		.append(
			$('<u />')
				.text(displayName)
		)
	
	return $('<button />')
		.attr("type", "button")
		.addClass("btn")
		.append($('<span />').append(innerSpan))

}

WG.lpad = function(number, width, padding){
	padding = padding || 0
	width -= number.toString().length;
	if ( width > 0 ){
		return new Array( width + (/\./.test( number ) ? 2 : 1) ).join( '0' ) + number;
	}
	return number;
}


WG.dumpObj = function(obj){
	str = 'Object: '
	for(thing in obj){
		str += "\n\t" + String(thing) + ": " + String(obj)
	}
	WG.lastDumpedObj = obj
	return str
}

if(!window.WG){WG = {}}

$.extend(WG, {
	NOTE_HANDLE_HEIGHT: 25,
	NOTE_LINK_INIT:          "User:EpochFail/Note_link_init",
	NOTE_REFERENCE_INIT:     "User:EpochFail/Note_reference_init",
	NOTE_REFERENCE_TEMPLATE: "User:EpochFail/Note_reference"
})


/**
Represents a group of notes in a drawer.
*/
WG.NoteDrawerGroup = Class.extend({
	/**
	Constructs a new NoteDrawer note Group.
	*/
	init: function(offset, notes){
		this.div = $('<div class="group" />')
			.css("position", "absolute")
		this.notes = 
		notes = notes || 
		for(i in notes){note = notes
			this.add(note)
		}
	},
	
	/**
	Adds a note to the group.
	*/
	add: function(note){
		//Add node in appropriate location in group
		for(var i in this.notes){var n = this.notes
			if(n.offset() > note.offset()){
				this.notes.splice(i, 0, note)
				note.viewer.div.insertBefore(n.viewer.div)
				this.reposition()
				return
			}
		}
		//Otherwise, add to the end.
		this.notes.push(note)
		this.div.append(note.viewer.div)
		this.reposition()
	},
	
	/**
	Removes a note from the group if it exists in the group.  Otherwise does
	nothing.
	*/
	remove: function(note){
		for(i in this.notes){var n = this.notes
			if(n == note){
				note.viewer.div.detach()
				this.notes.splice(i, 1)
				this.reposition()
			}
		}
	},
	
	/**
	The absolute position top of the drawer including margin, padding and 
	border.
	*/
	top: function(){
		return this.div.position().top
	},
	
	/**
	The absolute position bottom of the drawer including margin, padding and
	border.
	*/
	bottom: function(){
		//return this.div.outerBottom()
		
		//instead, return what the bottom *should* be
		return this.top() + (WG.NOTE_HANDLE_HEIGHT * this.notes.length)
	},
	
	/**
	Detaches notes and removed self from DOM. 
	*/
	del: function(){
		this.div.children().detach()
		this.div.remove()
	},
	
	/**
	
	*/
	reposition: function(){
		if(this.notes.length > 0){
			this.div.css("top", this.notes.offset())
		}
	}
})

/**
Positions and aligns notes on the right side of the screen in an intelligent way.
*/
WG.NoteDrawer = Class.extend({
	/**
	Constructs a new NoteDrawer
	*/
	init: function(){
		this.div = $('<div class="note_drawer" />')
			.appendTo('#bodyContent')
			.css('position', 'absolute')
			.css('top', 0)
			.css(
				'right', 
				-15
			)
			.css('height', $('#bodyContent').height())
		
		this.groups = 
		
		$(window).resize(
			function(drawer){return function(e){
				if(drawer.reloadTimer){
					clearTimeout(
						drawer.resizeTimer
					)
				}
				
				drawer.reloadTimer = setTimeout(
					function(drawer){return function(e){
						drawer.reload()
					}}(drawer)
				)
			}}(this)
		)
	},
	
	/**
	Redraws the note drawer and re-arranges the notes when necessary.  For 
	example, when the window is resized.
	*/
	reload: function(){
		this.clear()
		var oldGroups = this.groups
		this.groups = 
		for(var i in oldGroups){var group = oldGroups
			for(i in group.notes){var note = group.notes
				this.add(note)
			}
		}
	},
	
	/**
	Adds a set of notes to the drawer.
	*/
	load: function(notes){
		this.clear()
		this.groups = 
		for(var i in notes){note = notes
			//make sure the note is hidden so we can align it correctly.
			note.viewer.hide()
			
			//get the location that this note would want its handle positioned.
			
			//If this is the first note or there is no overlap
			if(
				this.groups.length == 0 ||
				this.groups.bottom() < note.offset()
			){
				//Easy case.  We just create a new group at our desired offset
				var group = new WG.NoteDrawerGroup(note.offset())
				group.add(note)
				
				//Add it to the drawer
				this.div.append(group.div)
				
				//Add it to our list of groups
				this.groups.push(group)
			}else{
				//There is currently a group in the way of where
				//we want to put this note's handle
				//Let's just add it to the previous group.
				this.groups.add(note)
			}
		}
		$('div.note_viewer').css('overflow', 'visible')
	},
	
	/**
	Adds a note to the drawer in the most appropriate group
	*/
	add: function(note){
		for(var i in this.groups){var group = this.groups
			if(group.bottom() < note.offset()){
				//Not to the offset yet.
			}else if(group.top() <= note.offset() + WG.NOTE_HANDLE_HEIGHT){
				//We want to be inside of a group that already exists
				group.add(note)
				return
			}else{
				//We passed up the spot we want to put this note.
				//Drop it in it's desired spot.
				var group = new WG.NoteDrawerGroup(note.offset())
				group.add(note)
				this.div.append(group.div)
				
				//Insert in position.  Don't put it *before* the
				//beginning.
				this.groups.splice(Math.max(0, i-1), 0, group)
				return
			}
		}
		//If you get here, that means we are adding a note that is below
		//all previous notes.  We can just add it where we want it. 
		var group = new WG.NoteDrawerGroup(note.offset())
		group.add(note)
		this.div.append(group.div)
		this.groups.push(group)
	},
	
	/**
	Removes a note from the drawer.
	*/
	remove: function(note){
		note.viewer.div.detach()
		for(var i in this.groups){var group = this.groups
			//We don't have to check if the note is in the group
			//since this function does nothing if it isn't.
			group.remove(note)
		}
	},
	
	/**
	Clears all groups from the drawer.
	*/
	clear: function(){
		if(this.groups){
			for(var i in this.groups){var group = this.groups
				group.del()
			}
		}
	}
})

/**
Represents a note in an article.
*/
WG.Note = Class.extend({
	init: function(span){
		this.span  = $(span)
		if(!this.span.attr('id')){
			throw "Span missing id."
		}
		this.id = this.span.attr('id')
		this.viewer = new WG.NoteViewer(this)
		this.editor = new WG.NoteEditor(this)
		this.viewer.div.append(this.editor.div)
		this.hide()
		
		this.span
			.click(
				function(note){return function(e){
					note.toggle()
				}}(this)
			)
			.hover(
				function(note){return function(e){
					note.viewer.handle.div.addClass("hover")
				}}(this),
				function(note){return function(e){
					note.viewer.handle.div.removeClass("hover")
				}}(this)
			)
	},
	chunkId: function(){
		return this.chunk.id
	},
	pageTitle: function(){
		return WG.TALK_PAGE_TITLE + "/" + this.id
	},
	offset: function(){
		return this.span.position().top + this.span.outerHeight(true)+3//fudge
	},
	cancel: function(){
		this.editor.compress()
	},
	
	
	/**
	Toggles between showing and hiding the note viewer/editor
	*/
	toggle: function(){
		if(this.hidden){
			this.show()
		}else{
			this.hide()
		}
	},
	
	/**
	Animated hide operation
	*/
	hide: function(callback){
		this.hidden = true
		this.editor.hide()
		//this.viewer.div.css("overflow", "hidden")
		this.viewer.hide(callback)
	},
	
	/**
	Animated show operation.
	*/
	show: function(callback){
		this.hidden = false
		this.viewer.show(
			function(note, callback){return function(){
				note.editor.show()
				if(callback){callback()}
			}}(this, callback)
		)
		
	},
	
	/**
	Loads a preview of whatever if in the editor into the viewer using the API
	*/
	preview: function(callback){
		WG.api.pages.preview(
			WG.TALK_PAGE_TITLE,
			this.editor.val(),
			function(note, callback){return function(html){
				note.viewer.view(html)
				if(callback){callback(html)}
			}}(this, callback),
			function(error){
				WG.console.error(error)
			}
		)
	},
	
	/**
	Saves a new version of the note based on what is currently in the editor
	*/
	save: function(callback){
		//First disable the editor.  No clicking or typing while I'm saving!
		this.editor.disable()
		
		//Start the call to save the current note.
		WG.api.pages.save(
			this.pageTitle(),
			this.token,
			this.preamble + this.editor.val(),
			"Updating note",
			false,
			function(note, callback){return function(){
				note.preview(
					function(note, callback){return function(html){
						note.savedHTML = html
						if(callback){callback()}
					}}(this, callback)
				)
				//Now you can use the editor again
				note.editor.enable()
				note.hide()
			}}(this, callback),
			function(note, callback){return function(message){
				WG.console.error(message)
				
				//Something bad happened, but you're welcome to try again.
				note.editor.enable()
			}}(this, callback)
		)
	},
	
	/**
	Removes a note placeholder from the chunks.
	*/
	remove: function(callback){
		if(confirm("Are you sure you'd like to remove this note?")){
			this.editor.disable()
			
			var summary = 'Removing note with "'
			summary += this.editor.val().substring(0, 250-(summary.length+4)) + '..."'
			
			WG.chunks.remove(this.chunk)
			WG.chunks.save(
				false,
				summary,
				function(note, callback){return function(){
					note.span.remove()
					//Remove self from drawer
					note.hide(
						function(note){return function(){
							WG.noteDrawer.remove(note)
						}}(note)
					)
				}}(this, callback),
				function(note, callback){return function(error){
					WG.console.error(error)
					this.editor.enable()
				}}(this, callback)
			)
		}
	},
	
	/**
	Refresh with subpage content
	**/
	load: function(){
		WG.api.pages.get(
			this.pageTitle(),
			function(note){return function(markup, page){
				note.__loadMarkup(markup)
				note.preview()
				note.editor.enable()
				note.token = page.edittoken
			}}(this),
			function(note){return function(message){
				WG.console.error("Could not load note markup " + note.id + ": " + message)
			}}(this)
		)
	},
	__loadMarkup: function(markup){
		var noteHeaderRE = RegExp(
			"{{\\s*" + 
			WG.NOTE_REFERENCE_TEMPLATE
				.replace("/", "\\/")
				.replace("_", "(_| )") + 
			"\\s*\\|(\\s|.)*?}}\n*"
		)
		var match = markup.match(noteHeaderRE)
		if(match){
			//Found a note header template.  Yay!
			var parts = markup.split(match)
			this.preamble = parts + match,
			this.editor.val(parts.slice(1).join(match))
		}else{
			//No template :(.  Try to process the first level two header.
			var level2RE = /(^|\n)+==+?==/
			match = markup.match(level2RE)
			if(match){
				var parts = markup.split(match)
				this.preamble = parts + match,
				this.editor.val(parts.slice(1).join(match))
					
			}else{
				this.preamble = '{{' + WG.NOTE_HEADER_TEMPLATE + "|" + this.id + "}}",
				this.editor.val(markup)
			}
		}
	}
})

WG.OldNote = WG.Note.extend({
	init: function(chunk, span){
		this._super(span)
		//var id = parseInt(chunkSpan.attr('id').split("_"))
		if(chunk.t != "note"){
			throw "Non-note chunk(" + chunk.id + ") type '" + chunk.t + "' to be edited.  No can do duder."
		}
		this.chunk = chunk
		this.load()
	}
})

WG.NewNote = WG.Note.extend({
	init: function(previousId, noteClass, id){
		//Creates a human-readable, searchable timestamp
		var id = [
				[
					WG.lpad(d.getUTCFullYear(), 4), 
					WG.lpad(d.getUTCMonth()+1), 
					WG.lpad(d.getUTCDate(), 2)
				].join('-'),
				[
					WG.lpad(d.getUTCHours(), 2), 
					WG.lpad(d.getUTCMinutes(), 2), 
					WG.lpad(d.getUTCSeconds(), 2)
				].join(":")
			].join('_')
		
		var span = $('<span />')
			.addClass(noteClass)
			.css("display", "inline-block;")
			.attr("id", id)
		
		this._super(span)
		this.previousChunk = WG.chunks.get(previousId)
		this.show(function(note){return function(){note.editor.expand()}}(this))
	},
	saved: function(){
		return this.chunk != undefined
	},
	chunkId: function(){
		if(this.saved()) return this._super()
		
		return undefined
	},
	save: function(callback){
		if(this.saved()) return this._super(callback)
		else{this.__createPage(callback)}
	},
	__createPage: function(callback){
		
		//First disable the editor.  No clicking or typing while I'm saving!
		this.editor.disable()
		
		
		//Create the note page with new note content
		WG.api.pages.create(
			this.pageTitle(),
			"== Re. Inline ]==\n" +
			"{{subst:" + WG.NOTE_REFERENCE_INIT + "}}\n" + 
			this.editor.val(),
			"Creating note page for ]",
			function(note, callback){return function(save){
				//Created the note page.  Now update the article with the placeholder
				note.__insertIntoArticle(callback)
				note.__appendToTalkPage()
			}}(this, callback),
			function(note){return function(message){
				WG.console.error("Could not create note subpage: " + message)
				note.editor.enable()
			}}(this)
		)
	},
	__insertIntoArticle: function(callback){
		
		//Update chunks
		this.chunk = {
			id: this.previousChunk.id+1,
			t: "note",
			c: "{{" + WG.NOTE_TEMPLATE + "|" + this.id + "}}",
			val: this.id
		}
		WG.chunks.insert(this.chunk)
		
		//Save a new version
		WG.chunks.save(
			false,
			"Inserting note placeholder for ]",
			function(note, callback){return function(save){
				var summary = 'Inserting ] with "'
				summary += note.editor.val().substring(0, 250-(summary.length+4)) + '..."'
				
				//Preview the new content
				note.preview(
					function(note, callback){return function(html){
						note.savedHTML = html
						if(callback){callback()}
					}}(this, callback)
				)
				
				//Re-enable the editor
				note.editor.enable()
				note.hide()
			}}(this, callback),
			function(note, callback){return function(message){
				WG.console.error("Could not update article with note placeholder: " + message)
				note.editor.enable()
			}}(this, callback)
		)
	},
	__appendToTalkPage: function(){
		WG.api.pages.append(
			WG.TALK_PAGE_TITLE,
			"\n\n{{" + this.pageTitle() + "}}",
			"Adding section for new ]",
			function(save){},
			function(message){
				WG.console.error("Could not append note to talk page: " + message)
			}
		)
	},
	cancel: function(){
		if(this.saved()) return this._super()
		
		//DESTROY EVERYTHING and forget we even started
		this.span.remove()
		
		//Remove self from drawer
		this.hide(
			function(note){return function(){
				WG.noteDrawer.remove(note)
			}}(this)
		)
	}
})



/**
An animated interface for viewing a note embedded in wiki markup.
*/
WG.NoteViewer = Class.extend({
	init: function(note){
		this.div = $('<div class="note_viewer" />')
		
		this.handle = {
			div: $('<div class="handle" />')
				.appendTo(this.div)
				.click(
					function(note){return function(e){
						note.toggle()
					}}(note)
				)
				.hover(
					function(note){return function(e){
						note.span.addClass("hover")
					}}(note),
					function(note){return function(e){
						note.span.removeClass("hover")
					}}(note)
				)
		}
		var paneDiv = $('<div class="pane" />')
		this.pane = {
			div: paneDiv
				.hide()
				.appendTo(this.div),
			view: $('<div class="view" />')
				.appendTo(paneDiv)
		}
		
		this.hidden = true
	},
	
	/**
	Loads new HTML into the note viewer.
	*/
	view: function(html){
		this.pane.view.html(html)
	},
	
	/**
	Animated hide operation
	*/
	hide: function(callback){
		//this.viewer.div.css("overflow", "hidden")
		this.pane.div.slideUp(
			200,
			function(viewer, callback){return function(){
				viewer.div.animate(
					{
						width: 0, 
						right: 0
					},
					{
						complete: function(callback){return function(){
							if(callback){callback()}
						}}(callback)
					}
				)
			}}(this, callback)
		)
	},
	
	/**
	Animated show operation.
	*/
	show: function(callback){
		this.div.animate(
			{width: 500, right: 500},
			{
				complete: function(viewer){return function(){
					viewer.pane.div.slideDown(200)
					if(callback){callback()}
				}}(this)
			}
		)
	}
})

/**
An editor for creating and updating notes.
*/
WG.NoteEditor = Class.extend({
	init: function(note){
		this.note = note
		
		this.div = $('<div class="editor" />')
		
		this.remover = {
			div: $('<div class="button remove"/>')
				.text("remove")
				.appendTo(this.div)
				.click(
					function(editor){return function(e){
						editor.note.remove()
					}}(this)
				)
		}
		
		this.opener = {
			div: $('<div class="button open"/>')
				.text("edit")
				.addClass("primary")
				.appendTo(this.div)
				.click(
					function(editor){return function(e){
						editor.expand()
					}}(this)
				)
		}
		
		this.textarea =  $('<textarea />')
			.hide()
			.appendTo(this.div)
		
		
		this.canceller = {
			div: $('<div class="button cancel"/>')
				.text("cancel")
				.hide()
				.appendTo(this.div)
				.click(
					function(editor){return function(e){
						if(editor.div.hasClass("disabled")){return}
						editor.note.cancel()
					}}(this)
				)
		}
		
		this.saver = {
			div: $('<div class="button save"/>')
				.text("save")
				.addClass("primary")
				.hide()
				.appendTo(this.div)
				.click(
					function(editor){return function(e){
						if(editor.div.hasClass("disabled")){return}
						editor.note.save()	
					}}(this)
				)
		}
		
		this.previewer = {
			div: $('<div class="button preview"/>')
				.text("preview")
				.hide()
				.appendTo(this.div)
				.click(
					function(editor){return function(e){
						if(editor.div.hasClass("disabled")){return}
						editor.note.preview()
					}}(this)
				)
		}
		this.div.append($('<div style="clear:both;"/>').css('height', 0))
			
	},
	
	/**
	Get and sets the value of the editor (what's in the text area)
	*/
	val: function(val){
		if(val){
			//Setting the value
			this.textarea
				.val(val)
				.attr(
					"rows", 
					Math.max(6, Math.floor(val.length/30))
				)
		}else{
			//Asking for the value
			return this.textarea.val()
		}
	},
	
	hide: function(){
		this.div.hide()
	},
	show: function(){
		this.div.show()
	},
	
	/**
	Hides the edit pane with a nice little animation
	*/
	compress: function(){
		this.textarea.slideUp(
			200,
			function(editor){return function(e){
				//Show the other buttons
				editor.saver.div.hide()
				editor.previewer.div.hide()
				editor.canceller.div.hide()
				
				//Hide the edit div
				editor.opener.div.show()
				editor.remover.div.show()
			}}(this)
		)
		
	},
	
	/**
	Shows the edit pane with a nice little animation
	*/
	expand: function(){
		this.div.show()
		//Hide the edit div
		this.opener.div.hide()
		this.remover.div.hide()
		
		//Show the other buttons
		this.saver.div.show()
		this.previewer.div.show()
		this.canceller.div.show()
		
		//Expand the text area
		this.textarea.slideDown(200)
		this.textarea.focus()
	},
	
	/**
	Cancels the editing operation.  Reverts the markup in tghe textarea back
	to the original and closes the editor.
	*/
	cancel: function(){
		
		//hide
		this.compress()
		
		//restore old markup into textarea
		this.textarea.val(this.note.chunk.val)
		
		//Revert the viewer
		this.note.viewer.revert()
	},
	
	/**
	Disables the buttons and text area in the editor.  This is useful when
	new input should be restricted while an operation is being performed.
	*/
	disable: function(){
		//Disable text area
		this.textarea.prop('disabled', true)
		
		//add disabled class
		this.div.addClass("disabled")
	},
	
	/**
	Enables (or re-enables) the buttons and text editor. 
	*/
	enable: function(){
		//Re-enable textarea
		this.textarea.prop('disabled', false)
		
		//Remove the disabled class
		this.div.removeClass("disabled")
	},
	
	/**
	Focuses the cursor in the textarea
	*/
	focus: function(){
		this.textarea.focus()
	}
})
if(!window.WG){WG = {}}


WG.API = Class.extend({
	init: function(){
		this.url = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + "/api.php"
		this.pages = new WG.Pages(this)
	}
})

WG.Pages = Class.extend({
	init: function(api){
		this.api = api
	},
	get: function(title, success, error){
		success = success || function(){}
		error   = error   || function(){}
		
		WG.console.info("API: Requesting the current version of " + title + "...")
		$.ajax({
			url: this.api.url,
			dataType: "json",
			data: {
				action: 'query',
				prop:   'revisions|info',
				titles: title,
				rvprop: 'content|timestamp',
				intoken:'edit',
				format: 'json'
			},
			type: "POST",
			context: this,
			success: function(success, error){return function(data, status){
				//alert(WG.dumpObj(this))
				if(status != "success"){
					error("The API is unavilable: " + status)
				}else if(data.error){
					error("Received an error from the API: " + data.error.code + " - " + data.error.info)
				}else if(!data.query || !data.query.pages){
					error("Received an unexpected response from the API: " + WG.dumpObj(data))
				}else {
					for(key in data.query.pages){
						var page = data.query.pages
					}
					if(page.revisions){
						var markup = page.revisions
						WG.console.info("API: Received revision " + page.lastrevid + " of " + page.title + " with markup of length " + markup.length)
					}else{
						var markup = undefined
						WG.console.info("API: Received info for missing page " + page.title)
					}
					
					success(markup, page)
				}
			}}(success, error),
			error: function(error){return function(jqXHR, status, message){
				//Sometimes an error happens when the request is 
				//interrupted by the user changing pages. 
				if(status != 'error' || message != ''){
					error("An error occurred while contacting Wikipedia's API: " + status + ": " + message)
				}
			}}(error)
		})
	},
	append: function(title, markup, summary, success, error){
		success = success || function(){}
		error   = error   || function(){}
		
		WG.console.info("API: Trying to append to " + title + "...")
		this.get(
			title,
			function(api, title, markup, summary, success, error){return function(___, page){
				WG.console.info("API: Appending markup of length " + markup.length + " to " + title + "...")
				$.ajax({
					url: api.url,
					dataType: "json",
					data: {
						action:     'edit',
						title:      title,
						appendtext: markup,
						token:      page.edittoken,
						summary:    summary,
						format:     'json'
					},
					type: "POST",
					success: function(summary, success, error){return function(data, status){
						if(status != "success"){
							error("The API is unavilable: " + status)
						}else if(data.error){
							error("Received an error from the API: " + data.error.code + " - " + data.error.info)
						}else if(!data.edit || !data.edit.result){
							error("Received an unexpected response from the API: " + WG.dumpObj(data))
						}else if(data.edit.result != "Success"){
							error("Saving the edit failed: " + WG.dumpObj(data.edit))
						}else{
							WG.console.info("API: Successfully appended text in revision " + data.edit.newrevid + " of " + data.edit.title + ": " + summary)
							success(data.edit)
						}
					}}(summary, success, error),
					error: function(error){return function(jqXHR, status, message){
						//Sometimes an error happens when the request is 
						//interrupted by the user changing pages. 
						if(status != 'error' || message != ''){
							error("An error occurred while contacting Wikipedia's API: " + status + ": " + message)
						}
					}}(error)
				})
			}}(this.api, title, markup, summary, success, error),
			function(error){return function(message){
				error(message)
			}}(error)
		)
	},
	save: function(title, token, touched, markup, summary, minor, success, error){
		success = success || function(){}
		error   = error   || function(){}
		
		WG.console.info("API: Saving a new revision of " + title + " with mark of length " + markup.length + "...")
		$.ajax({
			url: this.api.url,
			dataType: "json",
			data: {
				action:          'edit',
				title:           title,
				text:            markup,
				token:           token,
				basetimestamp:   touched,
				summary:         summary,
				minor:           minor,
				format:          'json'
			},
			type: "POST",
			success: function(summary, success, error){return function(data, status){
				if(status != "success"){
					error("The API is unavilable: " + status)
				}else if(data.error){
					error("Received an error from the API: " + data.error.code + " - " + data.error.info)
				}else if(!data.edit || !data.edit.result){
					error("Received an unexpected response from the API: " + WG.dumpObj(data))
				}else if(data.edit.result != "Success"){
					error("Saving the edit failed: " + WG.dumpObj(data.edit))
				}else{
					WG.console.info("API: Successfully saved revision " + data.edit.newrevid + " of " + data.edit.title + ": " + summary)
					success(data.edit)
				}
			}}(summary, success, error),
			error: function(error){return function(jqXHR, status, message){
				//Sometimes an error happens when the request is 
				//interrupted by the user changing pages. 
				if(status != 'error' || message != ''){
					error("An error occurred while contacting Wikipedia's API: " + status + ": " + message)
				}
			}}(error)
		})
	},
	create: function(title, markup, summary, success, error){
		success = success || function(){}
		error   = error   || function(){}
		
		WG.console.info("API: Trying to create " + title + "...")
		this.get(
			title,
			function(api, title, markup, summary, success, error){return function(___, page){
				if(page.missing == undefined){
					throw "Failed to create page " + title + ".  Already exists."
				}
				api.save(title, page.edittoken, markup, summary, false, success, error)
			}}(this, title, markup, summary, success, error),
			function(error){return function(message){
				error(message)
			}}(error)
		)
	},
	preview: function(title, markup, success, error){
		success = success || function(){}
		error   = error   || function(){}
		
		WG.console.info("API: Sending markup of length " + markup.length + " for " + title + " to be parsed...")
		$.ajax({
			url: this.api.url,
			dataType: "json",
			data: {
				action: 'parse',
				title:  title,
				text:   markup,
				prop:   'text',
				pst:    true,
				format: 'json'
			},
			type: "POST",
			context: this,
			success: function(success, failure){return function(data, status){
				if(status != "success"){
					error("The API is unavilable: " + status)
				}else if(data.error){
					error("Received an error from the API: " + data.error.code + " - " + data.error.info)
				}else if(!data.parse || !data.parse.text || !data.parse.text){
					error("Received an unexpected response from the API: " + WG.dumpObj(data))
				}else{
					var html = data.parse.text
						.replace(/<!--(.|\n|\r)*?-->/gi, '')
					success(html)
				}
			}}(success, error),
			error: function(error){return function(jqXHR, status, message){
				//Sometimes an error happens when the request is 
				//interrupted by the user changing pages. 
				if(status != 'error' || message != ''){
					error("An error occurred while contacting Wikipedia's API: " + status + ": " + message)
				}
			}}(error)
		})
		
	}
})
if(!window.WG){WG = {}}

WG.Chunks = Class.extend({
	init: function(chunks){
		this.chunks = chunks
	},
	get: function(id, type){
		id = parseInt(id)
		if(!this.chunks){
			throw "Chunk id " + id + " was not found."
		}else if(type && this.chunks.t != type){
			throw "Chunk id " + id + " is of type '" + this.chunks.t + "', not '" + type + "'"
		}else{
			return this.chunks
		}
	},
	insert: function(chunk){
		this.chunks.splice(chunk.id, 0, chunk)
		
		//Update future chunks and representation
		for(var i=this.chunks.length-1;i>=chunk.id+1;i--){
			var upChunk = this.chunks
			var span = $('#chunk_' + upChunk.id)
			span.attr('id', 'chunk_' + i)
			upChunk.id = i
		}
	},
	remove: function(chunk){
		//Remove chunk from chunk list
		this.chunks.splice(chunk.id, 1)
		
		//Remove id from span
		$('#chunk_' + chunk.id).removeAttr('id')
		
		//Update the affected spans and chunks
		for(var i=chunk.id;i<this.chunks.length;i++){
			var upChunk = this.chunks
			var span = $('#chunk_' + upChunk.id)
			
			span.attr('id', 'chunk_' + i)
			
			upChunk.id = i
		}
	},
	toString: function(){
		var newMarkup = ''
		for(i in this.chunks){
			newMarkup += this.chunks.c
		}
		return newMarkup
	},
	remarkup: function(sentenceClass, noteClass, headerClass){
		markup = ''
		for(var i in this.chunks){
			var chunk = this.chunks
			if(chunk.t == "sentence" || chunk.t == "definition"){
				markup += (
					'<span class="' + sentenceClass + 
					'" id="chunk_' + i + '">' + 
					chunk.c + '</span>'
				)
			}else if(chunk.t == "note"){
				markup += (
					'<span class="' + noteClass + 
					'" id="chunk_' + i + '">' + 
					chunk.c + '</span>'
				)
			}/*else if(chunk.t == "header"){
				markup += (
					'<div class="' + headerClass + 
					'" id="chunk_' + i + '">' + 
					chunk.c + '\n</div>'
				)
			}*/else{
				markup += chunk.c
			}
		}
		return markup
	},
	save: function(minor, summary, success, error){
		WG.api.pages.save(
			WG.PAGE_TITLE,
			WG.token,
			WG.touched,
			this.toString(),
			summary,
			minor,
			success,
			error
		)
	}
})
if(!window.WG){WG = {}}
$.extend(WG, {
	SENTENCE_CLASS: "WG_sentence",
	NOTE_CLASS: "WG_snote",
	HEADER_CLASS: "WG_header",
	MARKUP_CLASS: "WG_markup",
	CONTEXT_MENU: $('<ul style="display:none" class="contextMenu"/>')
		.append($('<li />')
			.addClass('edit')
			.append($('<a />')
				.attr('href', '#edit')
				.text('Edit sentence')
			)
		)
		.append($('<li />')
			.addClass('new_note')
			.append($('<a />')
				.attr('href', '#new_note')
				.text('Insert note')
			)
		)
		.appendTo($('body')),
	SUB_NOTE_CLASS: "note_container",
	WAIT_TIME: 0,
	PAGE_TITLE: wgPageName,
	TALK_PAGE_TITLE: wgFormattedNamespaces + ":" + wgTitle,
	api: new WG.API(),
	SUMMARY_SUFFIX: "(])",
	SUMMARY_MAX_LENGTH: 255
})
$.extend(WG, {
	load: function(){
		WG.api.pages.get(
			WG.PAGE_TITLE,
			function(markup, page){
				WG.token = page.edittoken
				WG.touched = page.touched
				WG.parseAndLoad(markup)
			},
			function(error){
				WG.console.error(error)
			}
		)
	},
	parseAndLoad: function(markup){
		WG.console.info("Parsing article content...")
		WG.chunks = new WG.Chunks((new WG.Chunker(markup)).popAll())
		
		//WG.console.info("Sending new markup of length " + WG.remarkuped.length + " to the API.")
		WG.api.pages.preview(
			WG.PAGE_TITLE,
			WG.chunks.remarkup(WG.SENTENCE_CLASS, WG.NOTE_CLASS, WG.HEADER_CLASS),
			function(html){
				WG.html = html
				$(document).ready(
					function(e){
						$("#bodyContent .mw-content-ltr").html(WG.html)
						/*$.contextMenu(
							{
								menu: WG.CONTEXT_MENU,
								selector: $("span." + WG.SENTENCE_CLASS),
								callback: function(action, el, pos){
									switch(action){
										case "edit":
											WG.loadSentenceInteractor(el)
											break;
										case "new_note":
											WG.loadNoteCreater(el)
											break;
									}
								}
							}
						)*/
						$("span." + WG.SENTENCE_CLASS)
							.hover(
								function(e){
									if(e.ctrlKey){
										$(e.currentTarget).addClass("hover")
									}
								},
								function(e){
									$(e.currentTarget).removeClass("hover")
								}
							)
							.click(
								function(e){
									WG.loadSentenceInteractor(e.currentTarget)
								}
							)
							
						
						/*$(
							"div." + WG.HEADER_CLASS + " h2,"+
							"div." + WG.HEADER_CLASS + " h3,"+
							"div." + WG.HEADER_CLASS + " h4,"+
							"div." + WG.HEADER_CLASS + " h5,"+
							"div." + WG.HEADER_CLASS + " h6,"+
							"div." + WG.HEADER_CLASS + " h7"
							)
							.append(
								$("<div/>")
									.addClass(WG.MARKUP_CLASS)
									.append("+ note")
							)*/
						
						var hash = window.location.hash.substring(1)
						WG.noteDrawer = new WG.NoteDrawer()
						$.each(
							$('span.' + WG.NOTE_CLASS),
							function(i, chunkSpan){
								var id = parseInt($(chunkSpan).attr('id').split("_"))
								var span = $(chunkSpan).children($('span.' + WG.SUB_NOTE_CLASS))
								var note = new WG.OldNote(
									WG.chunks.get(id, 'note'),
									span
								)
								WG.noteDrawer.add(note)
								if(note.id == hash){
									note.show()
								}
							}
						)
						WG.afterLoad()
					}
				)
			},
			function(error){
				WG.console.error(error)
			}
		)
	},
	loadSentenceInteractor: function(e){
		WG.lastSentenceInteractor = new WG.SentenceInteractor(e)
	},
	loadNoteCreater: function(e){
		var previousId = parseInt(e.attr('id').split("_"))
		d = new Date()
		
		var note = new WG.NewNote(previousId, WG.SUB_NOTE_CLASS)
		WG.lastNewNote = note
		note.span.insertAfter(e)
		WG.noteDrawer.add(note)
	},
	console: new WG.HiddenConsole(),
	error: function(message){
		if(confirm(message + "\nWould you like to reload the page?")){
			window.location.reload()
		}
	},
	afterLoad: function(){
		if(window.setupPopups){
			disablePopups()
			setupPopups()
		}
	}
})


WG.load()
// </syntaxhighlight>