User:Karelklic/editor.js

From Wiktionary, the free dictionary
Jump to navigation Jump to search

Note – after saving, you may have to bypass your browser’s cache to see the changes.

  • Mozilla / Firefox / Safari: hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Command-R on a Macintosh);
  • Konqueror and Chrome: click Reload or press F5;
  • Opera: clear the cache in Tools → Preferences;
  • Internet Explorer: hold Ctrl while clicking Refresh, or press Ctrl-F5.

/** Code written by Conrad Irwin **/

/* DOM abbreviation function */
function newNode(tagname){

  var node = document.createElement(tagname);
  
  for( var i=1;i<arguments.length;i++ ){
    
    if(typeof arguments[i] == 'string'){ //Text
      node.appendChild( document.createTextNode(arguments[i]) );
      
    }else if(typeof arguments[i] == 'object'){ 
      
      if(arguments[i].nodeName){ //If it is a DOM Node
        node.appendChild(arguments[i]);
        
      }else{ //Attributes (hopefully)
        for(var j in arguments[i]){
          if(j == 'class'){ //Classname different because...
            node.className = arguments[i][j];
            
          }else if(j == 'style'){ //Style is special
            node.style.cssText = arguments[i][j];
            
          }else if(typeof arguments[i][j] == 'function'){ //Basic event handlers
            try{ node.addEventListener(j,arguments[i][j],false); //W3C
            }catch(e){try{ node.attachEvent('on'+j,arguments[i][j],"Language"); //MSIE
            }catch(e){ node['on'+j]=arguments[i][j]; }}; //Legacy
          
          }else{
            node.setAttribute(j,arguments[i][j]); //Normal attributes
          
          }
        }
      }
    }
  }
  
  return node;
}

/* Wrapper around API */
function API() {

    function request (query, callback)
    {
        var xhr = sajax_init_object();

        xhr.open('POST', '/w/api.php?format=json', true);
        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");                  
        xhr.send(query);
        xhr.onreadystatechange = function ()
        {
            if (xhr.readyState == 4)
            {
                callback(eval("("+xhr.responseText+")"));
            }
        }
    }

    function encode_array (arg, args)
    {
        if (arg instanceof Array)
            args = arg;

        return encodeURIComponent(Array.prototype.join.call(args,"|"));
    }

    function Query (what)
    {
        return function (props, callback)
        {
            request('action=query&' + what + "&" + props, callback); 
        }
    }

    var query = {

        titles: function (arg) {
            return Query('titles=' + encode_array(arg, arguments))
        },

        pageids: function (arg) {
            return Query('pageids=' + encode_array(arg, arguments))
        },

        revids: function (arg) {
            return Query('revids=' + encode_array(arg, arguments))
        },
        page: function (title)
        {
            //self is this without the interference from javascript
            var self = {
                query: query.titles(title),
                title: title,

                edit: function (callback, section)
                {
                    var q = 'prop=info|revisions&intoken=edit&rvprop=content|timestamp';

                    if (section != null)
                        section = '&rvsection=' + section;
                    else
                        section = '';

                    q += section;

                    self.query(q, function (res)
                    {
                        // should only be one pageid
                        for (var pageid in res.query.pages)
                        {
                            var page = res.query.pages[pageid];
                            var text = '';

                            if (page.revisions)
                                text = page.revisions[0]['*']

                            //the "save" function
                            callback(text, function (ntext, summary, postsave)
                            {
                                if (text == ntext || !ntext)
                                    return;

                                if(!summary)
                                    summary = "";

                                request('action=edit&title=' + encodeURIComponent(self.title) +
                                        '&text=' + encodeURIComponent(ntext) + section +
                                        '&summary=' + encodeURIComponent(summary) +
                                        '&token=' + encodeURIComponent(page.edittoken) +
                                        '&starttimestamp=' + encodeURIComponent(page.revisions[0].timestamp),
                                        postsave
                                    )
                            });
                        }
                    });
                },

                create: function (text, summary, minor)
                {
                    self.edit(function(otext, save)
                    {
                        save(text, summary, minor);
                    });
                },

                parse: function (text, callback)
                {
                    request('action=parse&title=' + encodeURIComponent(self.title) + '&text=' + encodeURIComponent(text.replace('subst:','')), callback)
                },

                parseFragment: function (text, callback) //To prevent <p>'s being added
                {
                    self.parse('<div>' + text + '</div>', function (res)
                    {
                        res.parse.text['*'] = res.parse.text['*'].replace(/^<div>/,'').replace(/<\/div>$/,'');
                        callback(res);
                    });
                } 
            }
            return self;
        }
    };

    return query;
}

//Oh, why did I use that self nonsense above. let's use this properly...
//A class to make settings persistant, and to store changes to them - yay
function Preferences (context)
{
    //Repeated calls with the same context should get the same preferences object.
    if (arguments.callee[context])
        return arguments.callee[context];
    else
        arguments.callee[context] = this;

    /**
     * Subscribe to changes to a preference.
     *
     * This will call the callback each time the preference is changed. And, if
     * the currentvalue that you pass differs from the value in the storage,
     * then it will call the callback immediately with the true value. If you
     * pass in a currentvalue, and it has not yet got a value for this pref,
     * then it will use the value you pass.
     *
     * @param {string} name  The name of this preference.
     * @param {function(string):void}  The callback to call when it changes.
     * @param {string} currentvalue  The optional currentvalue.
     */
    this.subscribe = function (name, callback, currentvalue)
    {
        if (storage[name] === null && currentvalue !== null)
            storage[name] == currentvalue;

        if (!callbacks[name])
            callbacks[name] = [];

        callbacks[name].push(callback);

        if (storage[name] !== null && storage[name] !== currentvalue)
            callback(storage[name])
    }

    /**
     * Change the value of a preference.
     *
     * This will cause all the people subscribed to the function to recieve an
     * update.
     *
     * @param {string} name  The name of the preference
     * @param {string} value  The new value of the preference.
     */
    this.set = function (name, value)
    {
        if (value === null || storage[name] === value)
            return;

        storage[name] = value;

        if (callbacks[name])
            for (var i=0; i < callbacks[name].length; i++)
                callbacks[name][i](value);

        updateCookie();
    }

    this.get = function (name, def)
    {
        if (storage[name])
            return storage[name];
        else
            return def;
    }

    var storage = {};
    var callbacks = {};

    // Save storage into the cookie.
    function updateCookie ()
    {
        var value = "";
        for (var name in storage)
        {
            value += '&' + encodeURIComponent(name) + "=" + encodeURIComponent(storage[name]);
        }

        setCookie('preferences' + context, value)
    }
    
    // Load storage from the cookie. 
    function updateStorage ()
    {
        var value = getCookie('preferences' + context, value);
        var pairs = value.split('&');

        for (var i=1; i < pairs.length; i++)
        {
            var val = pairs[i].split('=');
            //alert("get"+val[0]);

            if (storage[val[0]] === val[1])
                continue;

            if (callbacks[val[0]])
            {
                for (var j=0; j < callbacks[val[0]].length; j++)
                    callbacks[val[0]][j](val[1]);
            }

            storage[val[0]] = val[1];
        }
    }

    //__init__
    updateStorage();
}
/**
 * A generic page editor, three points of intersection,
 *
 * This is a singleton and it displays a small interface in the top left after
 * the first edit has been registered.
 *
 * @public
 * this.page
 * this.addEdit
 * this.error
 *
 */
function Editor ()
{
    //Singleton
    if (arguments.callee.instance)
        return arguments.callee.instance
    else
        arguments.callee.instance = this;

    /**
     * Get the API page object associated with this editor
     */
    this.page = API().page(wgPageName);

    /**
     * Add the specific edit to the page.
     *
     * If the node is specified it will be highlighted now, and unhighlighted
     * when the change is saved.
     *
     * @param {edit}  The edit {redo:, undo:, edit:, summary:}
     * @param {*node}  The node to highlight.
     */
    this.addEdit = function (edit, node, fromRedo)
    {
        if (node)
        {
            nodestack.push(node);
            node.style.cssText = "border: 2px #00FF00 dashed;"
        }

        if(! fromRedo)
            redostack = [];

        if(! (loaded && !loading))
            load()
        
        else
        {
            var ntext = false;
            try
            {
                ntext = edit.edit(currentText);
            }
            catch (e)
            {
                this.error("ERROR:" + e);
            }

            if (ntext)
            {
                currentText = ntext;
                edit.redo();
                fixButtons();
            }
            else
                return false;
        }
        editstack.push(edit);
    }

    /**
     * Display an error message to the user.
     *
     * @param {string}  The message.
     */
    this.error = function (message)
    { 
        
        if (!errorlog)
        {
            errorlog = newNode('ul',{'style': "background-color: #FFDDDD; margin: 0px -10px -10px -10px; padding: 10px;"});
            presence.appendChild(errorlog);
        }
        errorlog.appendChild(newNode('li', message));
    }

    var thiz = this; // this is set incorrectly when private functions are used as callbacks.

    var editstack = []; // A list of the edits that have been applied to get currentText
    var redostack = []; // A list of the edits that have been recently undone.
    var nodestack = []; // A lst of nodes to which we have added highlighting

    var loaded = false; // Is the page-text loaded?
    var loading = false; // Is the page-text loading?

    var originalText = ""; // What was the contents of the page before we fiddled?
    var currentText = ""; // What is the contents now?

    var savebutton;
    var undobutton;
    var redobutton;

    var saveCallback; // The callback returned by the api's edit function to save.

    var presence; // The HTML element in the top-left
    var errorlog; // The ul for sticking errors in.

    // Disable useless buttons, enable useful ones.
    function fixButtons () 
    {
        if(! loaded)
            return;
        undobutton.disabled = (editstack.length ? false : true);
        savebutton.disabled = undobutton.disabled;
        redobutton.disabled = (redostack.length ? false : true);
    }

    function updateCurrentText ()
    {
        var text = originalText;
        for (var i=0; i < editstack.length; i++)
        {
            var ntext = false;
            try
            {
                ntext = editstack[i].edit(text);
            }
            catch (e)
            {
                thiz.error("ERROR:" + e);
            }
            if (ntext && ntext != text)
            {
                text = ntext;
                editstack[i].redo();
            }
            else
            {
                editstack = editstack.splice(0, i);
                break;
            }
        }
        currentText = text;
        fixButtons();
    }

    function undo ()
    {
        if (editstack.length == 0)
            return;
        var edit = editstack.pop();
        redostack.push(edit);
        edit.undo();

        updateCurrentText();
        fixButtons();
    }

    function redo ()
    {
        if (redostack.length == 0)
            return;
        var edit = redostack.pop();
        thiz.addEdit(edit, null, true);
        fixButtons();
    }

    function load ()
    {
        loading = true;
        thiz.page.edit(function (text, _save)
        {
            originalText = text;
            saveCallback = _save;
            
            loading = false;
            loaded = true;

            savebutton = newNode('button',"Save Changes", {'click': save});
            undobutton = newNode('button',"Undo", {'click': undo});
            redobutton = newNode('button', "Redo", {'click':redo});

            if (!presence)
                presence = newNode('div',{'style':"position: fixed; top:0px; left: 0px; background-color: #CCCCFF; z-index: 10;padding: 10px;"})
            else
                presence.innerHTML = ""

            presence.appendChild(newNode('p',
                newNode('b', "Page Editing"), newNode('br'),
                savebutton, newNode('br'), undobutton, redobutton))

           document.body.insertBefore(presence, document.body.firstChild);

            updateCurrentText();
        });
    }

    function save (summary)
    {
        if (loading)
            window.setTimeout(save, 500);

        if (!loaded)
            throw "Not loaded..."; 

        var sum = {};
        for (var i=0; i<editstack.length; i++)
        {
            sum[editstack[i].summary] = true;
        }
        var summary = "";
        for (var name in sum)
        {
            summary += name + " ";
        }
        editstack = [];
        redostack = [];
        loaded = false;
        presence.innerHTML = "Saving: " + summary;
        originalText = currentText;
        saveCallback(currentText, summary + "([[User_talk:Conrad.Irwin/editor.js|Assisted]])", function (res)
        {
            try {
                presence.innerHTML = "";
                presence.appendChild(newNode('p', newNode('b', "Saved"), ": "+ summary,
                    newNode('a', {'href': wgScript + 
                    '?title=' + encodeURIComponent(mw.config.get('wgPageName')) + 
                    '&diff=' + encodeURIComponent(res.edit.newrevid) +
                    '&oldid=' + encodeURIComponent(res.edit.oldrevid)}, "(Show changes)")));
            }catch(e){
                presence.innerHTML = ""
                presence.appendChild(newNode('p',String(e)))
            }

            var node;
            var nst = []
            while (node = nodestack.pop())
            {
                node.style.cssText = "background-color: #0F0;border: 2px #0F0 solid;";
                nst.push(node);
            }
            window.setTimeout(function () {
                var node;
                while (node = nst.pop())
                    node.style.cssText = "";
            }, 400);
        
        });
    }
}

function AdderProtocol(editor, adders)
{
    for(var i=0; i < adders.length; i++)
    {
        var adder = adders[i](editor);

        adder.registerFormPositions(function (id, node, nextChild)
        {
            var context = adder.getFormContext(id, node, nextChild);
            var form = adder.createForm(context);

            if (nextChild)
                node.insertBefore(form, nextChild);
            else
                node.appendChild(form);

            form.status = newNode('p');
            form.appendChild(form.status);
            form.adder = adder;

            form.onsubmit = function ()
            {
                try
                {
                var values = {};
                var form = this;
                var adder = form.adder;
                form.status.innerHTML = "";
                var submit = true;
                for (var i=0; i < form.elements.length; i++) //IE7 fix.
                {
                    if(!form.elements[form.elements[i].name])
                        form.elements[form.elements[i].name] = form.elements[i];
                }
                for (var name in adder.fields)
                {
                    if (adder.fields[name] == 'checkbox')
                    {
                        values[name] = form.elements[name].checked ? name : false;
                    }
                    else
                    {
                        values[name] = adder.fields[name](form.elements[name].value || '', function (msg) { form.status.innerHTML += msg + "<br/>"; return false}, form.elements);
                        
                        if (values[name] === false)
                        {
                            submit = false;
                        }
                    }
                }
                if (!submit)
                    return false;

                if (adder.resetFields)
                    adder.resetFields(form);
                else
                    for (var name in adder.fields)
                        form[name].value = "";

                form.status.innerHTML = 'Loading...';
                
                adder.registerWikitext(context, values, function (text, type)
                {
                    if (text)
                    {
                        editor.page.parseFragment(text, function (res)
                        {
                            adder.registerEdits(context, values, text, res.parse.text['*'], type, editor.addEdit);
                            form.status.innerHTML = "";
                        });
                    }
                    else
                    {
                        adder.registerEdits(context, values, text, text, type, editor.addEdit);
                        form.status.innerHTML = "";
                    }
                });   
                } catch(e) {throw(e);form.status.innerHTML = "ERROR:" + e.description; return false;}

                return false;
            }
        });

    }
}

var util = {
    
    getVanillaIndexOf: function (str, text, pos)
    {
        if (!pos)
            pos = 0;
        var cpos = 0, tpos = 0, wpos = 0, spos = 0;
        do
        {
            cpos = text.indexOf('<!--', pos);
            tpos = text.indexOf('{'+'{', pos);
            wpos = text.indexOf('<nowiki>', pos);
            spos = text.indexOf(str, pos);

            pos = Math.min(
                Math.min(
                    cpos == -1 ? Infinity : cpos , 
                    tpos == -1 ? Infinity : tpos
                ), 
                Math.min(
                    wpos == -1 ? Infinity : wpos,
                    spos == -1 ? Infinity : spos
                )
            )

            if (pos == spos)
                return pos == Infinity ? -1 : pos;

            else if (pos == cpos)
                pos = text.indexOf('-->', pos) + 3;

            else if (pos == wpos)
                pos = text.indexOf('</nowiki>', pos) + 9;

            else if (pos == tpos) //FIXME
                pos = text.indexOf('}}', pos) + 2;


        } while (pos < Infinity)
        return -1;
    },

    validateNoWikisyntax: function(field, nonempty)
    {
        return function(txt, error)
        {
            if(/[\[\{\|#\}\]]/.test(txt))
                return error("Please don't use wiki markup ([]{}#|) in the " + field +".");
            if(nonempty && !txt)
                return error("Please specify a " + field + ".");
            return txt;
        }
    },

    escapeRe: function(txt)
    {
        return txt.replace(/([\\{}(\|)[\].?*+])/g, "\\$1");
    },

    getTransTable: function (text, gloss)
    {
        var pos = 0;
        var transect = [];
        while(pos > -1)
        {
            pos = util.getVanillaIndexOf('{'+'{trans-top', text, pos+1)
            if (util.matchGloss(text.substr(pos, text.indexOf('\n', pos)-pos), gloss))
            {
                transect.push(pos);
            }
        }
        if (transect.length == 1)
        {
            pos = transect[0];
            pos = text.indexOf('}}\n', pos) + 3;
            var endpos = text.indexOf('{'+'{trans-bottom}}', pos); 
            if (endpos > -1 && pos > -1)
                return [pos, endpos];
        }

        return false;
    },

    matchGloss: function (line, gloss)
    {
        line = line.replace('{'+'{trans-top}}','{'+'{trans-top|Translations}}');
        var words = gloss.split(' ');
        var pos = 0;
        for (var i=0; i < words.length; i++)
        {
            pos = line.indexOf(words[i], pos);
            if (pos == -1)
                return false;
        }
        return pos > -1;
    },

    getTransGlossText: function (node) {
      var ret = '';
      var children = node.childNodes;
      for (var i=0; i<children.length; i++)
      {
        if (children[i].nodeType == 3)
          ret += children[i].nodeValue;
        else if (children[i].tagName.match(/^(i|b)$/i))
          ret += util.getTransGlossText(children[i]);
      }
      // all characters except a-zA-Z0-9 are changed to spaces
      return ret.replace(/\W/g, ' '); 
    },

    getTransGloss: function (ul)
    {
        var node = ul;
        while (node && node.className.indexOf('NavFrame') == -1)
            node = node.parentNode;

        if (!node) return ''; 

        var children = node.childNodes;
        for (var i=0; i< children.length; i++)
        {
            if(children[i].className && children[i].className.indexOf('NavHead') > -1)
                return util.getTransGlossText(children[i]);
        }
        return '';
    }
};

/**
 * An editor has the following:
 *  Callbacks are used to allow generator-like code in the absense of generators
 *
 * fields {object[string] = function(string, function(string))}
 *   An object with the keys equal to the names of the input elements to extract
 *   data from. (Must be added by create form).
 *   The function is used to validate and preprocess, it can return a string
 *   or false, the error function passed in displays an error message and returns
 *   false. (you can display an error and return a value if you want).
 *
 * registerFormPositions {function(callback)} 
 *   A function that calls the callback(id, parentNode, ?nextChild) to
 *   specify where forms should be created.
 *
 * getFormContext {function(id, parentNode, ?nextChild):context}
 *   A function that should return a custom object containing any context to
 *   be passed to the createForm function.
 *
 * createForm {function(context):form}
 *   A function to create an edit form based on the given context
 *
 * registerWikitext {context, values, callback(str, type)}
 *   This should callback() for every string of wikitext that the
 *   edit needs. These will then be parsed and passed into 
 *   registerEdits. If multiple strings are calledback, the type
 *   parameneter to the callback can identify them. Use '' if no wikitext is needed.
 *
 *  registerEdits {context, values, wikitext, renderedwikitext, type, callback(edit)}
 *   Return an actual edit, as described below. It should do as much work as possible,
 *   making the undo and redo functions very simple; and should use editor.error() to
 *   interact with the user.
 *
 * An edit has the following:
 *  summary {string}
 *   A summary to be added to the edit summary
 *
 * redo {function()}
 *   A function that can be used to create a preview of the edit. This function may
 *   be called multiple times (though each will be interspersed with undo).
 *
 * undo {function()}
 *   A function that exactly undoes the effect of redo(), with no side-effects.
 *
 * edit {function(text):text}
 *   A function that given an entries text can edit it and return a new text.
 *   It should return null or false to signal and error and abort the edit, it
 *   should also use editor.error() to alert the user to errors.
 *
 */
function TranslationAdder (editor, page)
{
    var prefs = new Preferences('TranslationAdder');
    var self = {
        fields: {
            'lang': function (txt, error)
            {
                if (/^[a-z][a-z][a-z]?$/.test(txt)) return txt;
                return error("Please use a language code. (en, fr, aaa)")
            },
            'word': util.validateNoWikisyntax('translation', true),
            'qual': util.validateNoWikisyntax('qualifier'),
            'tr': util.validateNoWikisyntax('transcription'),
            'alt': util.validateNoWikisyntax('display name'),
            'sc': function (txt, error, fields)
            {
                if (txt && !/^([A-Z][a-z]{3}|[a-z]{2}-Arab|polytonic|unicode)$/.test(txt))
                    return error("Please use a script template. (e.g. fa-Arab, Deva, polytonic)")

                    if (!txt) 
                        txt = prefs.get('script-' + fields.lang.value, guessScript(fields.lang.value) || '');
                    if (txt == 'Latn')
                        txt = '';
                    return txt;
            },
            'm': 'checkbox', 
            'f': 'checkbox',
            'n': 'checkbox',
            'c': 'checkbox',
            'p': 'checkbox'
//            'language': util.validateNoWikisyntax('langauge')
        },

        resetFields: function (form)
        {
            prefs.set('more-display', form.lang.extras.style.display);
            form.word.value = form.tr.value = form.alt.value = '';
            form.m.checked = form.f.checked = form.n.checked = form.c.checked = form.p.checked = false;
            prefs.set('curlang', form.lang.value);
            if (form.sc.value)
            {
                prefs.set('script-'+form.lang.value, form.sc.value); 
                form.lang.update();
            }
            form.sc.value = '';
        },

        registerFormPositions: function (callback) 
        {
            var tables = document.getElementsByTagName('table');
            for (var i=0; i<tables.length; i++)
            {
                if (tables[i].className.indexOf('translations') > -1)
                {
                    var _lists = tables[i].getElementsByTagName('ul');
                    var lists = [];
                    for (var j=0; j<_lists.length; j++)
                        if (_lists[j].parentNode.nodeName.toLowerCase() == 'td')
                            lists.push(_lists[j]);

                    if (lists.length == 0)
                    {
                        tables[i].getElementsByTagName('td')[0].appendChild(newNode('ul'));
                        lists = tables[i].getElementsByTagName('ul');
                    }
                    if (lists.length == 1)
                    {
                        var table = tables[i].getElementsByTagName('td')[2]
                        if (table)
                        {
                            table.appendChild(newNode('ul'));
                            lists = tables[i].getElementsByTagName('ul');
                        }
                    }
                    var li = newNode('li');
                    lists[lists.length - 1].appendChild(li);
                    callback(i, li);
                }
            }
        },

        getFormContext: function (id, node, nextNode) {return {'id': id, 'ul':node.parentNode}},

        createForm: function (context)
        {
            var scriptGuess = newNode('span');
            var extras = newNode('p', {'style': 'display: ' + prefs.get('more-display', 'none')},
//                "Gender: ", newNode('input', {'size': 4, 'name': 'gender'}), newNode('br'), 
                newNode('a', {href: '/wiki/Wiktionary:Transliteration'},"Transliteration"),": ", newNode('input', {'name': 'tr','title':"The word transliterated into the Latin alphabet."}), " (e.g. ázbuka for азбука)", newNode('br'),
                "Qualifier: ", newNode('input', {'name':'qual', 'title':"A qualifier for the word"}), " (e.g. literally, formally, slang)", newNode('br'),
                "Display form: ", newNode('input', {'name': 'alt','title':"The word with all of the dictionary-only diacritics."}), " (e.g. amō for amo)", newNode('br'),
                "Override ", newNode('a', {href: '/wiki/Category:Script_templates'},"script"),": ", newNode('input', {'name':'sc','size':8,'title':"The script template to render this word in."}), scriptGuess, newNode('br')
               /* "Language name: ", newNode('input', {'name':'language'}) */)

            function updateScriptGuess(){
                this.value = cleanLangCode(this.value);
                var guess = prefs.get('script-' + this.value, guessScript(this.value));
                if (guess)
                    scriptGuess.innerHTML = " (using "+guess+")";
                else
                    scriptGuess.innerHTML = "";
            }
            context.langInput = newNode('input', {'size': 4, 'name': 'lang', 'value': prefs.get('curlang',''),'title':'The two or three letter language code','change':updateScriptGuess})
            context.langInput.update = updateScriptGuess;
            context.langInput.update();
            context.langInput.extras = extras;
            var showButton = newNode('span',{'click': function ()
            {
                if (extras.style.display != "block")
                {
                    extras.style.display = "block";
                    showButton.innerHTML = " Less";
                }
                else
                {
                    extras.style.display = "none";
                    showButton.innerHTML = " More";
                }
            }, 'style':"color: #0000FF;cursor: pointer;"}, prefs.get('more-display', 'none') == 'none' ? " More" : " Less");
            context.form = newNode('form', {'context': context},
                        newNode('p', newNode('b', 'Add',newNode('a',{href:"/wiki/User_talk:Conrad.Irwin/editor.js"},"?"),': '),
                            context.langInput, ' ', newNode('input', {'name': 'word'}),
                            newNode('input',{'type': 'submit', 'value':'Preview'}), showButton
                        ), 
                newNode('input', {type: 'checkbox', name: 'm'}), 'male ', 
                newNode('input', {type: 'checkbox', name: 'f'}), 'female ',
                newNode('input', {type: 'checkbox', name: 'n'}), 'neuter ',
                newNode('input', {type: 'checkbox', name: 'c'}), 'common gender ',
                newNode('input', {type: 'checkbox', name: 'p'}), 'plural ', newNode('br'),
                    extras
                    )
            return context.form;
        },

        registerEdits: function (context, values, wikitext, content, type, register)
        {
            var li = newNode('li');
            li.innerHTML = content;
            var lang = getLangName(li);

            if (lang)
            {
                //Get all li's in this table row. - TODO: sort out nesting
                var lis = context.ul.parentNode.parentNode.getElementsByTagName('li');
                for (var j=0; j < lis.length; j++)
                {
                    var ln = getLangName(lis[j]);
                    if (ln == lang)
                    {
                        //FIXME: don't do this if we have nested languages.
                        var span = newNode('span');
                        span.innerHTML = ", " + content.substr(content.indexOf(':') + 1);
                        var parent = lis[j];
                        register({
                            'redo': function () { parent.appendChild(span) },
                            'undo': function () { parent.removeChild(span) },
                            'edit': self.getEditFunction(context, values, wikitext, ln, values.lang, function (text, ipos)
                                    {
                                         var lineend = text.indexOf('\n', ipos);
                                         wikitext = wikitext.replace('subst:','');
                                         wikitext = wikitext.substr(wikitext.indexOf(':') + 1);
                                         return text.substr(0, lineend) + ", " + wikitext + text.substr(lineend);
                                    }),
                            'summary': 't+'+values.lang+':[['+values.word+']]'
                        }, span);
                        return;
                    }
                    else if (ln && ln > lang)
                    {
                        var parent = lis[j];
                        
                        register({
                            'redo': function () {parent.parentNode.insertBefore(li, lis[j]);},
                            'undo': function () {parent.parentNode.removeChild(li)},
                            'edit': self.getEditFunction(context, values, wikitext, ln, getLangCode(lis[j]), function (text, ipos)
                                    {
                                        var lineend = text.lastIndexOf('\n', ipos);
                                        return text.substr(0, lineend) + "\n* " + wikitext + text.substr(lineend);
                                    }),
                            'summary': 't+'+values.lang+':[['+values.word+']]'
                        }, li);
                        return;
                    }
                }
            }
            register ({
                'redo': function () {context.ul.insertBefore(li, context.ul.lastChild);},
                'undo': function () {context.ul.removeChild(li)},
                'edit': self.getEditFunction(context, values, wikitext),
                'summary': 't+'+values.lang+':[['+values.word+']]'
            }, li);

        },

        registerWikitext: function (context, values, callback)
        {
            callback( '{'+'{subst:' + values.lang + '}}: ' + 
            (values.qual? '{'+'{italbrac|' + values.qual + '}} ' : '') +  
            '{'+'{t' + (hasWiktionary(values.lang) ? '' : 'ø') +
            '|' + values.lang + '|' + values.word + 
            (values.m ? '|m' : '') +
            (values.f ? '|f' : '') +
            (values.n ? '|n' : '') +
            (values.c ? '|c' : '') +
            (values.p ? '|p' : '') +
            (values.tr ? '|tr=' + values.tr : '') + 
            (values.alt ? '|alt=' + values.alt : '') +
            (values.sc ? '|sc=' + values.sc  : '') + '}}');
        },

        getEditFunction: function (context, values, wikitext, findLanguage, findLangCode, callback)
        {
            return function(text)
            {
                var p = util.getTransTable(text, util.getTransGloss(context.ul));

                if (!p)
                    return editor.error("Could not find translation table for '" + values.lang + ":" + values.word + "'. Please improve glosses DEBUG");

                var stapos = p[0];
                var endpos = p[1];

                if (findLanguage)
                {
                    var ipos = text.indexOf(findLanguage + ":", stapos);
                    if (ipos < 0)
                        ipos = text.indexOf(findLanguage + "]]:", stapos);
                    if (ipos < 0)
                        ipos = text.indexOf('{'+'{subst:'+findLangCode+'}}:', stapos);
                    if (ipos > stapos && ipos < endpos)
                    {
                        return callback(text, ipos);
                    }
                    else
                    {
                        return editor.error("Could not find translation entry for '" + values.lang + ":" +values.word + "'. Please reformat");
                    }
                }

                return text.substr(0, endpos) + "* " + wikitext + "\n" + text.substr(endpos);
            };
        }
    }

    function hasWiktionary(lang)
    {
        var val = ",en,fr,tr,vi,ru,io,lt,el,pl,zh,fi,hu,it,ta,sv,de,ko,lo,pt,nl,ku,es,ja,id,te,gl,bg,ro,vo,ar,et,no,li,ca,sr,is,fa,af,uk,scn,br,th,fy,oc,he,sl,simple,hy,sq,tt,cs,la,zh-min-nan,da,sw,ast,ur,kk,hsb,ky,ml,hr,ang,eo,hi,gn,ia,az,co,ga,sk,csb,st,ms,nds,kl,wo,sd,ug,ti,mk,tl,an,my,gu,km,ka,cy,ts,qu,bs,fo,rw,am,mr,kn,eu,tk,su,chr,lv,wa,mn,nah,ie,yi,be,om,gd,mg,zu,iu,pa,bn,nn,si,mt,mi,tpi,dv,ps,jv,so,tg,roa-rup,ik,ha,gv,sh,ss,kw,sa,ay,uz,na,ne,jbo,tn,as,sg,lb,ks,fj,ln,mo,sm,za,pi,ba,xh,mh,bh,sn,or,ak,yo,bi,rn,av,bm,ab,to,aa,tw,dz,als,bo,rm,sc,ch,cr,tokipona,".indexOf(","+lang+",") > -1;

        return val;
    }

    function guessScript(lang)
    {
        var dict = {ar:'Arab',hy:'Armn',xcr:'Cari',be:'Cyrl',bg:'Cyrl',mk:'Cyrl',ru:'Cyrl',uk:'Cyrl',cu:'Cyrs',bhb:'Deva',hi:'Deva',ne:'Deva',ma:'Deva',sa:'Deva',am:'Ethi',gez:'Ethi',har:'Ethi',ti:'Ethi',tig:'Ethi',xst:'Ethi',fa:'fa-Arab',ka:'Geor',got:'Goth',el:'Grek',he:'Hebr',iw:'Hebr',tmr:'Hebr',yi:'Hebr',ett:'Ital',ims:'Ital',osc:'Ital',spx:'Ital',xae:'Ital',xfa:'Ital',xrr:'Ital',xum:'Ital',xve:'Ital',xvo:'Ital',ja:'Jpan',ku:'ku-Arab',km:'Khmr',ko:'Kore',lo:'Laoo',gmy:'Linb',xlc:'Lyci',xld:'Lydi',phn:'Phnx',grc:'polytonic',si:'Sinh',syr:'Syrc',uga:'Ugar',ta:'Taml',te:'Telu',th:'Thai',peo:'Xpeo',akk:'Xsux',hit:'Xsux',sux:'Xsux',xlu:'Xsux'}

        if (dict[lang])
            return dict[lang];

        return false;
    }

    function cleanLangCode(lang)
    {
        //This is ISO 639-1, it serves the purposes of mapping ISO 639-3 to ISO 639-1 and of converting some language names to ISO 639-1 codes.
        var key = lang.toLowerCase().replace(' ','');
var dict = {aar:"aa",afar:"aa",abk:"ab",abkhazian:"ab",afr:"af",afrikaans:"af",aka:"ak",akan:"ak",amh:"am",amharic:"am",ara:"ar",arabic:"ar",arg:"an",aragonese:"an",asm:"as",assamese:"as",ava:"av",avaric:"av",ave:"ae",avestan:"ae",aym:"ay",aymara:"ay",aze:"az",azerbaijani:"az",bak:"ba",bashkir:"ba",bam:"bm",bambara:"bm",bel:"be",belarusian:"be",ben:"bn",bengali:"bn",bis:"bi",bislama:"bi",bod:"bo",tibetan:"bo",bos:"bs",bosnian:"bs",bre:"br",breton:"br",bul:"bg",bulgarian:"bg",cat:"ca",catalan:"ca",ces:"cs",czech:"cs",cha:"ch",chamorro:"ch",che:"ce",chechen:"ce",chu:"cu",churchslavic:"cu",chv:"cv",chuvash:"cv",cor:"kw",cornish:"kw",cos:"co",corsican:"co",cre:"cr",cree:"cr",cym:"cy",welsh:"cy",dan:"da",danish:"da",deu:"de",german:"de",div:"dv",dhivehi:"dv",dzo:"dz",dzongkha:"dz",ell:"el",greek:"el",eng:"en",english:"en",epo:"eo",esperanto:"eo",est:"et",estonian:"et",eus:"eu",basque:"eu",ewe:"ee",fao:"fo",faroese:"fo",fas:"fa",persian:"fa",fij:"fj",fijian:"fj",fin:"fi",finnish:"fi",fra:"fr",french:"fr",fry:"fy",westernfrisian:"fy",ful:"ff",fulah:"ff",gla:"gd",scottishgaelic:"gd",gle:"ga",irish:"ga",glg:"gl",galician:"gl",glv:"gv",manx:"gv",grn:"gn",guarani:"gn",guj:"gu",gujarati:"gu",hat:"ht",haitian:"ht",hau:"ha",hausa:"ha",heb:"he",hebrew:"he",her:"hz",herero:"hz",hin:"hi",hindi:"hi",hmo:"ho",hirimotu:"ho",hrv:"hr",croatian:"hr",hun:"hu",hungarian:"hu",hye:"hy",armenian:"hy",ibo:"ig",igbo:"ig",ido:"io",iii:"ii",sichuanyi:"ii",iku:"iu",inuktitut:"iu",ile:"ie",interlingue:"ie",ina:"ia",interlingua:"ia",ind:"id",indonesian:"id",ipk:"ik",inupiaq:"ik",isl:"is",icelandic:"is",ita:"it",italian:"it",jav:"jv",javanese:"jv",jpn:"ja",japanese:"ja",kal:"kl",kalaallisut:"kl",kan:"kn",kannada:"kn",kas:"ks",kashmiri:"ks",kat:"ka",georgian:"ka",kau:"kr",kanuri:"kr",kaz:"kk",kazakh:"kk",khm:"km",centralkhmer:"km",kik:"ki",kikuyu:"ki",kin:"rw",kinyarwanda:"rw",kir:"ky",kirghiz:"ky",kom:"kv",komi:"kv",kon:"kg",kongo:"kg",kor:"ko",korean:"ko",kua:"kj",kuanyama:"kj",kur:"ku",kurdish:"ku",lao:"lo",lat:"la",latin:"la",lav:"lv",latvian:"lv",lim:"li",limburgan:"li",lin:"ln",lingala:"ln",lit:"lt",lithuanian:"lt",ltz:"lb",luxembourgish:"lb",lub:"lu",lubakatanga:"lu",lug:"lg",ganda:"lg",mah:"mh",marshallese:"mh",mal:"ml",malayalam:"ml",mar:"mr",marathi:"mr",mkd:"mk",macedonian:"mk",mlg:"mg",malagasy:"mg",mlt:"mt",maltese:"mt",mon:"mn",mongolian:"mn",mri:"mi",maori:"mi",msa:"ms",malay:"ms",mya:"my",burmese:"my",nau:"na",nauru:"na",nav:"nv",navajo:"nv",nbl:"nr",southndebele:"nr",nde:"nd",northndebele:"nd",ndo:"ng",ndonga:"ng",nep:"ne",nepali:"ne",nld:"nl",dutch:"nl",nno:"nn",norwegiannynorsk:"nn",nob:"nb",norwegianbokmal:"nb",nor:"no",norwegian:"no",nya:"ny",nyanja:"ny",oci:"oc",occitan:"oc",oji:"oj",ojibwa:"oj",ori:"or",oriya:"or",orm:"om",oromo:"om",oss:"os",ossetian:"os",pan:"pa",panjabi:"pa",pli:"pi",pali:"pi",pol:"pl",polish:"pl",por:"pt",portuguese:"pt",pus:"ps",pushto:"ps",que:"qu",quechua:"qu",roh:"rm",romansh:"rm",ron:"ro",romanian:"ro",run:"rn",rundi:"rn",rus:"ru",russian:"ru",sag:"sg",sango:"sg",san:"sa",sanskrit:"sa",sin:"si",sinhala:"si",slk:"sk",slovak:"sk",slv:"sl",slovenian:"sl",sme:"se",northernsami:"se",smo:"sm",samoan:"sm",sna:"sn",shona:"sn",snd:"sd",sindhi:"sd",som:"so",somali:"so",sot:"st",southernsotho:"st",spa:"es",spanish:"es",sqi:"sq",albanian:"sq",srd:"sc",sardinian:"sc",srp:"sr",serbian:"sr",ssw:"ss",swati:"ss",sun:"su",sundanese:"su",swa:"sw",swahili:"sw",swe:"sv",swedish:"sv",tah:"ty",tahitian:"ty",tam:"ta",tamil:"ta",tat:"tt",tatar:"tt",tel:"te",telugu:"te",tgk:"tg",tajik:"tg",tgl:"tl",tagalog:"tl",tha:"th",thai:"th",tir:"ti",tigrinya:"ti",ton:"to",tonga:"to",tsn:"tn",tswana:"tn",tso:"ts",tsonga:"ts",tuk:"tk",turkmen:"tk",tur:"tr",turkish:"tr",twi:"tw",uig:"ug",uighur:"ug",ukr:"uk",ukrainian:"uk",urd:"ur",urdu:"ur",uzb:"uz",uzbek:"uz",ven:"ve",venda:"ve",vie:"vi",vietnamese:"vi",vol:"vo",volapuk:"vo",wln:"wa",walloon:"wa",wol:"wo",wolof:"wo",xho:"xh",xhosa:"xh",yid:"yi",yiddish:"yi",yor:"yo",yoruba:"yo",zha:"za",zhuang:"za",zho:"zh",chinese:"zh",zul:"zu",zulu:"zu"};
    if (dict[key])
        return dict[key];
    else
        return lang;
    }

    function getLangName(li)
    {
        var guess = li.textContent.substr(0, li.textContent.indexOf(':'));
        if (guess == 'Template')
            return false;
        return guess;
    }

    function getLangCode(li)
    {
        var spans = li.getElementsByTagName('span');
        for (var i=0; i < spans.length; i++)
        {
            if (spans[i].className == "tlc")
                return spans[i].innerHTML;
        }
        return false;
    }

    return self;
}

function TranslationBalancers(editor)
{
    function TranslationBalancer (insertTd)
    {
        var left;
        var right;
        var moveLeft;
        var moveRight;

        function init ()
        {
            var cns = insertTd.parentNode.childNodes;
            var tds = []; 
            for (var i=0; i<cns.length; i++)
            {
                if (cns[i].nodeName.toUpperCase() == 'TD')
                    tds.push(cns[i])
            }

            left = tds[0].getElementsByTagName('ul');
            if (left.length > 0)
                left = left[0];
            else
            {
                left = newNode('ul');
                tds[0].appendChild(left);
            }

            right = tds[2].getElementsByTagName('ul');
            if (right.length > 0)
                right = right[0];
            else
            {
                right = newNode('ul');
                tds[2].appendChild(right);
            }

            moveLeft = newNode('input',{'type':'submit','name':'ml', 'value':'←', 'click': function(){return getEditObject('←')}});
            moveRight = newNode('input',{'type':'submit','name':'mr', 'value':'→', 'click': function(){return getEditObject('→')}});

            var form = newNode('form', moveLeft, newNode('br'), moveRight);
            insertTd.appendChild(form);
            form.onsubmit = function () { return false; }
        }
        function getEditObject(move)
        {
            if (move == '→')
            {
                var li = left.lastChild;
                while (li && li.nodeName.toLowerCase() != 'li')
                    li = li.previousSibling;

                if (li && li.childNodes.length > li.getElementsByTagName('form').length)
                {
                    editor.addEdit({
                        'redo': function () {left.removeChild(li); right.insertBefore(li, right.firstChild);},
                        'undo': function () {right.removeChild(li); left.appendChild(li);},
                        'edit': getEdit(util.getTransGloss(moveRight.parentNode), true),
                        'summary': 't-balance'
                    });
                }
            }
            else if (move == '←')
            {
                var li = right.firstChild;
                while (li && li.nodeName.toLowerCase() != 'li')
                    li = li.nextSibling;

                if (li && li.childNodes.length > li.getElementsByTagName('form').length)
                {
                    editor.addEdit({
                        'redo': function () {right.removeChild(li); left.appendChild(li);},
                        'undo': function () {left.removeChild(li); right.insertBefore(li, right.firstChild);},
                        'edit': getEdit(util.getTransGloss(moveLeft.parentNode), false),
                        'summary': 't-balance'
                    });
                }
            }
        }

        function getEdit(gloss, moveRight)
        {
            return function (text)
            {
                var p = util.getTransTable(text, gloss);

                if (!p)
                    return editor.error("Could not find translation table, please improve glosses.");

                var stapos = p[0];
                var endpos = p[1];

                var midpos = text.indexOf('{'+'{trans-mid}}', stapos);

                if (midpos < stapos || midpos > endpos)
                    return editor.error("Could not find {"+"{trans-mid}}, please correct page.");

                var midstart = text.lastIndexOf("\n", midpos);
                var midend = text.indexOf("\n", midpos);

                if (moveRight)
                {
                    var linestart = text.lastIndexOf("\n", midstart - 3);
                    return text.substr(0, linestart) + text.substr(midstart, midend - midstart)
                           + text.substr(linestart, midstart - linestart) + text.substr(midend);
                }
                else
                {
                    var lineend = text.indexOf("\n", midend + 3);
                    return text.substr(0, midstart) + text.substr(midend, lineend - midend)
                            + text.substr(midstart, midend - midstart) + text.substr(lineend);
                }
            }
        }
        init();
    }

    var tables = document.getElementsByTagName('table');
    for (var i=0; i<tables.length; i++)
    {
        if (tables[i].className.indexOf('translations') > -1)
        {
            var tr = tables[i].getElementsByTagName('tr')[0];

            var passed = 0;
            for (var j=0; j<tr.childNodes.length; j++)
            {
                if (tr.childNodes[j].nodeName.toUpperCase() == 'TD')
                {
                    if (passed == 1)
                    {
                        TranslationBalancer(tr.childNodes[j]);
                    }
                    passed ++;
                }
            }
        }
    }

}

$(function () {
    var editor = new Editor();
    AdderProtocol(editor, [TranslationAdder]);
    TranslationBalancers(editor);
})