Jump to content

User:Uglemat/RefMan.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*
  Copyright (C) 2017 Mattias Ugelvik
  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

$(function () {

var run = (function($, mw) {
    Number.prototype.mod = function(n) {
        return ((this%n)+n)%n; // Stupid % no work properly like in Python!
    };

    function excise_tough_stuff(wikitext) {
        /*
          This function is used to remove difficult stuff from wikitext, specifically <pre> tags, <nowiki> tags,
          and <!-- html comments -->. If they are not removed, then the regex looking for references might find
          references there which it should not find. The excised regions are added back in with `add_tough_stuff`
          later on.
          
          Returns [replaced, excised]. `replaced` is a string without any difficult <nowiki>/<pre> stuff; removed
          regions are replaced with a unicode ellipsis character. `excised` is a list of objects of the form
          {location, content}, where `content` is the removed text which belongs to the ellipsis found in `replaced`
          at index `location`. `excised` is sorted in increasing order of the `location`.
        */
        var tough_stuff = /(<\s*(pre|nowiki)[^\/]*>[\s\S]*?<\s*\/\s*\2\s*>|<!--[\s\S]*?-->)/ig,
            offset = 0,
            excised = [];

        return [wikitext.replace(tough_stuff, function (match, content, _tag, index) {
            excised.push({location: index + offset, content: content});
            offset = (offset - content.length) + 1; // + 1 because ellipsis character

            return "…";
        }), excised];
    }

    function add_tough_stuff(startindex, substring, excised) {
        /*
          Used to add back in tough stuff previously removed. `substring` a substring of the
          sanitized string (= the content of a <ref> tag); `startindex` is the index of
          `substring` in the sanitized string.
          
          All the regions in `excised` has a `.location` >= `startindex`; this has been taken
          care of before calling this function.
          
          Also, `excised` is sorted, so there is no need for searching through the array.
          It must be the first element, or else it's a false positive: someone actually
          used the unicode ellipsis character.
        */
        return substring.replace(/…/g, function(_, offset) {
            if (excised.length && excised[0].location === startindex + offset) {
                return excised.shift().content;
            }
            else {
                return "…"; // False positive, move along
            }
        });
    }

    function replace_sections(oldstring, sections) {
        /*
          Given a list `sections` of [startindex, endindex, replacement], this function
          will replace each string section [startindex, endindex) in `oldstring` with
          `replacement` simultaneously. These sections cannot overlap, obviously.
        */
        var substrings = [],
            cursor = 0;
        sections = sections.slice().sort((a, b) => a[0] - b[0]); // in increasing order
        
        sections.forEach(function(section) {
            var startidx = section[0],
                endidx = section[1],
                replacement = section[2];
            
            substrings.push(oldstring.slice(cursor, startidx));
            substrings.push(replacement);
            cursor = endidx;
        });
        substrings.push(oldstring.slice(cursor, oldstring.length));
        
        return substrings.join("");
    }

    function replace_refs(oldstring, ref_sections) {
        /*
          Utility function for using reference objects with `replace_sections`
        */
        return replace_sections(oldstring, ref_sections.map(
            ([ref, replacement]) => [ref.index, ref.index_end, replacement]
        ));
    }

    function sorted_refs(reflist) {
        /* Sort a list of reference objects in the order they appear in the text */
        return reflist.slice().sort((a, b) => a.index - b.index);
    }
    function ref_to_str(ref) {
        var maybe_name = "";
        
        if (ref.name) {
            var maybe_quote = ref.name.match(/^[a-z0-9]+$/i) ? "" : "\"";
            maybe_name = ` name=${maybe_quote}${ref.name}${maybe_quote}`;
        }
        return ((ref.type === "reused") ?
                `<ref${maybe_name}/>` :
                `<ref${maybe_name}>${ref.content}</ref>`);
    }

    function find_references(wikitext) {
        /*
          Return a list of all references found in `wikitext`. Every reference is represented as a
          "reference object". It's just a regular object, but it must at a minimum have these keys:
          {
          name: [the reference name, `undefined` if it has no name],
          index: [the index in `wikitext` of where it was found. The code for this is a bit tricky,
          because the regex which searches for references doesn't actually run on `wikitext`,
          it runs on a sanitized version of `wikitext`.],
          string: [the full text of the reference, including the <ref> tag(s), and including any
          <nowiki>, <pre> tags or <!-- html comments --> which may be inside the reference
          content.],
          index_end: index + string.length
          type: ["ref" if it is an actual reference definition of the form <ref [name=...]>...</ref>
          "reused" if it is of the form <ref name=... />]
          }
          Reference definitions also have a `content` key, which is the stuff inside the <ref> tags, and
          when they are collected together they also get a `number` key, which is the order in which the
          ref appears to the user.
        */
        var refs = [],
            match,
            regex = new RegExp("("
                               + "(<\\s*ref" // This part matches <ref [name=...]>...</ref>
                               +   "(\\s+name\\s*=\\s*("
                               +     "([\"'])(((?!\\5).)+)\\5" /* This matches a name in quotes (single or double).
                                                                  The backreference is used to allow single quotes inside double quotes and vice versa */
                               +     "|([^\\s'\">\\/]+))" // Matches an unquoted name
                               +   ")?" // Names are optional
                               + "\\s*>)([\\s\\S]*?)(<\\s*\\/\\s*ref\\s*>)" // [\s\S]* instead of .* to match newlines
                               + "|" // The part below matches <ref name=... />. It's very similar.
                               + /<\s*ref\s+name\s*=\s*((["'])(((?!\12).)+)\12|([^\s'">\/]+))\s*\/\s*>/.source
                               + ")", "ig");
        
        var [sanitized_wikitext, excised] = excise_tough_stuff(wikitext),
            offset = 0; // Keeping track of index offsets as tough stuff is added back in
        
        while (match = regex.exec(sanitized_wikitext)) {
            while (excised.length && excised[0].location < match.index) {
                offset += excised.shift().content.length - 1; // - 1 because ellipsis character
                // Dumping irrelevant excised regions. We only care about regions that are
                // actually *inside* reference contents. This must be done before calling
                // `add_tough_stuff`.
            }

            if (match[11]) { // True if and only if it matched <ref name=... />
                refs.push({"name": match[13] || match[15],
                           "index": match.index + offset,
                           "index_end": match.index + offset + match[0].length,
                           "string": match[0],
                           "type": "reused"
                          });
            }
            else { // Else it must have matched <ref [name=...]>...</ref>
                var tag_length = match[2].length;
                // ^ Length of the opening tag. The tag content (match[9]) starts at `match.index` + `tag_length`.
                // This information is needed to know exactly where the `excised` stuff should be inserted.
                var with_stuff = add_tough_stuff(match.index + tag_length,
                                                 match[9], excised),
                    string = match[2] + with_stuff + match[10];

                refs.push({"name": match[6] || match[8], // Either a quoted name (6) or unquoted (8)
                           "index": match.index + offset,
                           "index_end": match.index + offset + string.length,
                           "string": string,
                           "content": with_stuff,
                           "type": "ref"
                          });
                
                offset += with_stuff.length - match[9].length;
            }
        }
        return refs;
    }

    function new_ref_state(text, cache, prevent_textarea_refresh) {
        /*
          Every time a change happens, this function is called, and a new `RefState` is created. The
          only input is `text`, which is the new content of the wikitext textarea. The text is parsed
          anew every time a change happens. This is a deliberate design decision. It hopefully makes
          bugs more visible and obvious. The alternative would be to keep track of the indices of all
          the references as changes are made, but that would require bug-prone housekeeping.
          
          For some transformations, the old `cache` is remembered.
        */
        function RefState() {
            this.refs = find_references(text);
            // Parsing the text for references. All the reference objects returned are sorted in the order
            // they appear in the document.
            
            this.text = text;

            this.cache = cache || {}; /* Storing changes made by the user. The key is either the index,
                                         in the case of individual references, or a string of the form
                                         "index|index" for caches of reference merges. The first index
                                         is the lowest index.
                                      */
            this.selected = undefined; /* May be a reference, or a list [index, index], when a merge is
                                          going on. When merging, the first index is the lowest index.
                                       */
            
            this.names = {}; /* An object {name: reused_refs} where `reused_refs` is a list of all the
                                reference objects of type "reused" with `name`                            
                             */
            this.name_to_ref = {}; // Mapping from a name to the actual reference object
            
            var proper_ref_index = (ref) => {
                /*
                  The references are sorted by wikipedia according to when it was first used *or reused*.
                  This function is used to sort `this.proper_refs` to replicate that behaviour.
                */
                
                // we assume (correctly) that `this.names[ref.name]` is sorted in the order they appear
                let reused = ref.name && this.names[ref.name];
                if (reused && reused[0].index < ref.index) {
                    return reused[0].index;
                } else {
                    return ref.index;
                }
            };
            
            this.proper_refs = this.refs.filter((ref) => {
                if (ref.type === "reused") {
                    this.names[ref.name] = this.names[ref.name] || [];
                    this.names[ref.name].push(ref);
                    return false;
                }
                return true;
            });
            this.proper_refs.sort((a, b) => proper_ref_index(a) - proper_ref_index(b));
            this.proper_refs.forEach((ref, index) => {
                ref.number = index;
                if (Object.keys(this.name_to_ref).includes(ref.name)) {
                    alert(`RefMan: The name "${ref.name}" is used more than once! Aborting ship!`);
                    throw new Error(`"${ref.name}" used more than once`);
                } else {
                    this.name_to_ref[ref.name] = ref;
                }
            });
            
            $("#refman").html("");
            this.proper_refs.forEach((ref, number) => {
                var name = ref.name ? `<span class=refman-name>${ref.name}</span> ` : "";
                var reused = ref.name ?
                    `[<span class=refman-reused>${this.names[ref.name] ? this.names[ref.name].length : 0}</span>] ` : "";
                var refdiv = $(`<div class="refman-ref refman-refnumber${number}" draggable=true>${name}${reused}`+
                               `<span class=refman-index>${number + 1}</span><span class=refman-cachenotice>(modified)</span></div>`)
                    .click(() => {
                        this.select_ref((this.selected && this.selected.number === number) ? undefined : ref);
                    }),
                    vanilla = refdiv.get(0);
                if (this.cache.hasOwnProperty(number)) {
                    refdiv.addClass("refman-cached");
                }

                vanilla.ondragstart = (ev) => {
                    let textbox = $("#wpTextbox1").get(0)
                    textbox.setSelectionRange(textbox.selectionStart, textbox.selectionStart);
                    /* ^ Clearing the selection, otherwise the selected text will be deleted when
                       a reference is dragged into the text box.
                       
                       Couldn't find a better way to clear the selection...
                     */
                    ev.dataTransfer.setData("RefManIndex", number.toString());
                    ev.dataTransfer.setData("text/plain", ref.name
                                            ? ref_to_str({name: ref.name, type: "reused"})
                                            : ref_to_str({content: ref.content, type: "ref"}));
                };
                vanilla.ondragover = (ev) => {
                    ev.preventDefault();
                    var other_number = parseInt(ev.dataTransfer.getData("RefManIndex"));
                    ev.dataTransfer.dropEffect = (other_number === number) ? "none" : "link";
                };
                
                vanilla.ondrop = (ev) => {
                    ev.preventDefault();
                    var other_number = parseInt(ev.dataTransfer.getData("RefManIndex")),
                        other_ref = this.get_ref(other_number);
                    if (other_number !== number) {
                        /*
                          Set up the edit area for merging two references. This area is usually used for
                          updating a reference, so some things have to be changed, like having different
                          buttons available (CSS is delegated that particular responsability).
                          
                          `other_ref` is the reference that was "dragged onto" `ref`.
                        */

                        $("#refman-editref").addClass("refman-merging").removeClass("refman-updating refman-hidden");
                        
                        $("#refman").find(".refman-selected").removeClass("refman-selected");
                        $("#refman").find(`.refman-refnumber${number}, .refman-refnumber${other_number}`).addClass("refman-selected");
                        
                        var reuse_list = $("#refman-editref").find("#refman-reuses ol");
                        reuse_list.empty();
                        var allrefs = this.reuses(ref.name).concat(this.reuses(other_ref.name)).concat([ref, other_ref]);
                        
                        sorted_refs(allrefs).forEach((r) => {
                            var elm;
                            if (r.type === "ref") {
                                elm = $(`<li class="refman-deflink" data-showname=${r === ref ? "merger" : "mergee"}></li>`);
                            } else {
                                elm = $(`<li></li>`);
                            }
                            elm.click(() => select_ref_range(r, true)).appendTo(reuse_list);
                        });
                        
                        this.selected = (other_number < number) ? [other_number, number] : [number, other_number];
                        let cached = this.get_cache(this.selected),
                            newcontent;

                        if (cached) {
                            newcontent = cached.content;
                            $("#refman-inputname").val(cached.name);
                        } else {
                            newcontent = [ref.content, other_ref.content].join("\n\n-- MERGE --\n\n");
                            $("#refman-inputname").val([ref.name, other_ref.name].filter((id) => id).join(" | "));
                        }
                        $("#refman-refcontent").val(newcontent);
                        this.show_links(newcontent);

                        $("#refman-inputname").attr("required", "required");
                    }
                };

                refdiv.appendTo($("#refman"));
                $(document.createTextNode(" ")).appendTo($("#refman")); // needed for text-align: justify
            });
            
            if (!prevent_textarea_refresh) {
                $("#wpTextbox1").val(text);
            }
            if (!document.forms.editform.wpSummary.value.includes("RefMan")) {
                document.forms.editform.wpSummary.value = 
                    $.trim($.trim(document.forms.editform.wpSummary.value) + " Merging references with [[User:Uglemat/RefMan|RefMan]]");
            }
        }
        RefState.prototype.get_ref = function(index) {
            return this.proper_refs[index];
        };
        RefState.prototype.move_selected = function(movement) {
            this.select_ref(this.get_ref(
                (this.selected.number + movement).mod(this.proper_refs.length)
            ));
        };
        RefState.prototype.reuses = function(name) {
            return this.names[name] || [];
        };
        RefState.prototype.add_cache = function(what, name, content) {
            /* `what` is either a reference object or a two-element list of reference
               numbers (= `this.proper_refs` indices) in the case of a merge cache.
            */
            var is_merge = Array.isArray(what),
                cashable = is_merge ? what.join("|") : what.number;
            this.cache[cashable] = { "name": name, "content": content };

            if (!is_merge) {
                if (name === this.get_ref(what.number).name &&
                    content === this.get_ref(what.number).content) {
                    $(`.refman-refnumber${what.number}`).removeClass("refman-cached");
                    $("#refman-update").removeClass("refman-needsupdate");
                } else {
                    $(`.refman-refnumber${what.number}`).addClass("refman-cached");
                    $("#refman-update").addClass("refman-needsupdate");
                }
            }
        };
        RefState.prototype.delete_cache = function(what) {
            var is_merge = Array.isArray(what),
                cashable = is_merge ? what.join("|") : what.number;

            if (this.cache[cashable] !== undefined) {
                delete this.cache[cashable];

                if (!is_merge) {
                    $(`.refman-refnumber${what.number}`).removeClass("refman-cached");
                    $("#refman-update").removeClass("refman-needsupdate");
                }
            }
        };
        RefState.prototype.get_cache = function(what) {
            var cashable = (Array.isArray(what)) ? what.join("|") : what.number;
            return this.cache[cashable];
        };
        RefState.prototype.transform_cache = function(func) {
            /*
              Transform the cache indices to conform to a new reality. `func` is mapped over all indices,
              and should return the new index.
            */
            var new_cache = {}, item;
            for (key in this.cache) {
                if (typeof key === "string") {
                    new_cache[key.split("|").map((n) => parseInt(n)).map(func).join("|")] =
                        this.cache[key];
                } else {
                    new_cache[key] = this.cache[key];
                }
            }
            return new_cache;
        };
        RefState.prototype.remove_ref = function(ref) {
            this.delete_cache(ref);
            
            var removed = [ref].concat(this.reuses(ref.name));

            this.select_ref();
            refman_state = new_ref_state(replace_refs(this.text, removed.map(
                (r) => [r, '']
            )), this.transform_cache(
                (i) => i - Number(i > ref.number) // Later cached indices must be reduced by one
            ));
        };
        RefState.prototype.update_ref = function(ref, name, content) {
            if (this.reuses(ref.name).length && name === "") {
                alert("This reference is used other places. You need to supply a name!");
                return;
            }
            this.delete_cache(ref);

            refman_state = new_ref_state(replace_refs(this.text, this.reuses(ref.name).map(
                (r) => [r, ref_to_str({type: "reused", name: name})]
            ).concat([ [ref, ref_to_str({type: "ref", name: name, content: content})] ])), this.cache);
            refman_state.select_ref(refman_state.get_ref(ref.number));
        };
        RefState.prototype.merge_refs = function([fst_index, snd_index], name, content) {
            var fst = this.get_ref(fst_index),
                snd = this.get_ref(snd_index);
            
            this.delete_cache([fst_index, snd_index]);
            this.delete_cache(fst);
            this.delete_cache(snd);

            var indices = new Set([fst.index, fst.index_end]);
            // ^ Used to remove unnecessary references.

            var reuses = this.reuses(fst.name).concat(this.reuses(snd.name)).concat([snd]);
            
            refman_state = new_ref_state(replace_refs(this.text, reuses.map((r) => {
                var not_needed = indices.has(r.index) || indices.has(r.index_end),
                    replacement = not_needed ? "" : ref_to_str({type: "reused", name: name});
                
                indices.add(r.index).add(r.index_end);

                return [r, replacement];
            }).concat(
                [ [fst, ref_to_str({type: "ref", name: name, content: content})] ]
            )), this.transform_cache(
                (i) => i - Number(i > snd_index)
            ));
            refman_state.select_ref(refman_state.get_ref(fst_index));
        };
        RefState.prototype.select_ref = function(ref) {
            if (ref === undefined) {
                // Select nothing
                $("#refman-editref").addClass("refman-hidden");
                this.selected = undefined;
                $("#refman").find(".refman-selected").removeClass("refman-selected");
                return;
            }

            this.selected = ref;

            var reuse_list = $("#refman-editref").find("#refman-reuses ol");
            reuse_list.empty();
            sorted_refs([ref].concat(this.reuses(ref.name)))
                .forEach((r) => {
                    $(`<li ${r.type === "ref" ? "class=refman-deflink data-showname=def" : ""}></li>`).click(
                        () => select_ref_range(r, true)
                    ).appendTo(reuse_list);
                });
            
            if (this.get_cache(ref)) {
                $("#refman-refcontent").val(this.get_cache(ref).content);
                $("#refman-inputname").val(this.get_cache(ref).name);
                $("#refman-update").addClass("refman-needsupdate");
            } else {
                $("#refman-refcontent").val(ref.content);
                $("#refman-inputname").val(ref.name || "");
                $("#refman-update").removeClass("refman-needsupdate");
            }
            

            $("#refman-inputname").attr("required", this.reuses(ref.name).length ? "required" : null);
            this.show_links($("#refman-refcontent").val());

            var ref_elem = $("#refman").find(`.refman-refnumber${ref.number}`);
            $("#refman").find(".refman-selected").removeClass("refman-selected");
            ref_elem.addClass("refman-selected");
            ref_elem.get(0).scrollIntoView(false);

            $("#refman-editref").addClass("refman-updating").removeClass("refman-merging refman-hidden");

            select_ref_range(ref, true);
        };
        RefState.prototype.show_links = function(text) {
            var link = /\bhttps?:\/\/(www\.)?[-a-z0-9@:%_+.~#=]{2,256}\.[a-z]{2,6}\b(\/[-a-z0-9@:%_+.~#?&\/=()]*)?/gi,
                ul = $("<ul></ul>"),
                match;
            
            while (match = link.exec(text)) {
                let start = match.index,
                    end = match.index + match[0].length;
                
                let li = $(`<li><a href="${match[0]}" data-start=${start} data-end=${end}>${match[0]}</a></li>`);
                // The data is used to highlight the relevant text when the control key is pressed while hovering the link
                
                ul.append(li);
            }
            $("#refman-showlinks").empty().append(ul);
        };
        RefState.prototype.ref_at_point = function(index) {
            /*
              Returns the reference `index` of `this.text` is inside of, if any
            */
            let binary_search = (array) => {
                if (!array.length) {
                    return false;
                } else {
                    let array_index = Math.floor(array.length/2),
                        ref = array[array_index];
                    
                    if (ref.index_end <= index) {
                        return binary_search(array.slice(array_index + 1));   
                    } else if (ref.index > index) {
                        return binary_search(array.slice(0, array_index));
                    } else { // Found it!
                        return (ref.type === "reused") ? this.name_to_ref[ref.name] : ref;
                    }
                }
            }
            return binary_search(this.refs);
        };
        RefState.prototype.highlight_ref_at_cursor = function() {
            let ref = refman_state.ref_at_point($("#wpTextbox1").get(0).selectionStart);
            $(".refman-ref-at-point").removeClass("refman-ref-at-point");
            if (ref) {
                $(`.refman-refnumber${ref.number}`).addClass("refman-ref-at-point").get(0).scrollIntoView(false);
            }  
        };
        
        return new RefState();
    }

    function select_range(start, end, doselect) {
        if (doselect) {
            $("#wpTextbox1").get(0).select();
        }
        $("#wpTextbox1").get(0).setSelectionRange(start, end);
    }
    function select_ref_range(ref, doselect) {
        select_range(ref.index, ref.index_end, doselect);
    }

    function load_stuff() {
        $(`<style type='text/css'>
          .refman-ref {
              cursor: pointer;
              color: #333;
              margin: 3px 2px;
              display: inline-block;
              padding: 2px 4px;
              background: beige;
              border: 1px solid black;
          }
          .refman-ref.refman-selected {
              box-shadow: inset 0 0 2px black;
              background: #67f367;
          }
          .refman-ref.refman-ref-at-point:not(.refman-selected) {
              box-shadow: inset 0 0 2px red;
              background: pink;
          }
          .refman-ref .refman-cachenotice {
              display: none
          }
          .refman-ref.refman-cached .refman-cachenotice {
              display: inline;
              color: red;
              margin-left: .2em;
              font-size: .8em;
          }
          .refman-name {
              font-weight: bold;
              color: #000;
          }
          .refman-reused {
              font-weight: bold;
              color: #0645ad;
          }
          .refman-index {
              color: #353;
              font-family: monospace;
              font-size: 1.1em;
          }
          .refman-index::before {
              content: "#";   
          }
          #refman-refcontent {
              width: 100%;
              resize: vertical;
              height: 120px;
              min-height: 140px;
              padding: 4px;
          }
          #refman-actions input {
              margin-right: 7px;
          }
          #refman-left {
              flex: auto;
          }
          #refman-right {
              width: 400px;
              vertical-align: bottom;
              align-self: end;
          }
          #refman-right > div {
              padding: 0 20px;
              height: 150px;
              display: table-cell;
              vertical-align: middle;
          }
          #refman-right input {
              margin-bottom: 5px;
          }
          #refman-right label {
              display: inline-block;
              width: 70px;
          }
          #refman-right a {
              margin-right: 10px;
          }
          .refman-hidden {
              display: none !important;
          }
          #refman-reflinks {
              margin-top: 5px; 
          }
          #refman-reflinks a {
              padding: 4px;
          }
          #refman-reflinks a:hover {
              border: 1px solid green;
              text-decoration: none;
              padding: 3px;
          }
          #refman-reuses > span {
              display: inline-block;
              width: 70px;
          }
          #refman-reuses ol {
              display: inline-block;
              margin: 0;
          }
          #refman-reuses li {
              display: inline-block;
              counter-increment: item;
              margin-right: 2px;
              padding: 2px;
          }
          #refman-reuses li:hover {
              border: 1px solid green;
              padding: 1px;
          }
          #refman-reuses li::before {
              font-weight: bold;
              color: #0645ad;
              cursor: pointer;
              content: counter(item, lower-alpha);
          }
          #refman-reuses li.refman-deflink::before {
              content: attr(data-showname) "[" counter(item, lower-alpha) "]";
              color: black;
          }
          .refman-needsupdate {
              color: red;
              font-style: italic;
              font-weight: bold;
          }
          .refman-merging .refman-whenupdating {
              display: none;
          }
          .refman-updating .refman-whenmerging {
              display: none;
          }
          #refman-showlinks {
              font-size: .8em;
          }
          #refman-editref {
              margin-bottom: 5px;
              position: fixed;
              bottom: 150px;
              right: 0;
              left: 0;
              background: white;
              border-top: 5px solid black;
              box-shadow: 0 5px 5px #3f3f3f99;
              z-index: 10;
              display: flex;
              flex-direction: row;
          }
          #refman {
              text-align: justify;
              position: fixed;
              bottom: 0;
              right: 0;
              left: 0;
              padding-top: 5px;
              overflow-y: auto;
              height: 150px;
              background: #a6bc7c;
              border-top: 5px solid black;
          }
          .refman-small-textarea {
              height: 250px;
          }
          </style>`).appendTo("head");

        var container = $("<div id=refman-container></div>"),
            main_div = $("<div id=refman></div>"),
            editref_div = $("<div id=refman-editref class=refman-hidden></div>");

        $("<div id=refman-left><div id=refman-showlinks></div><textarea id=refman-refcontent></textarea></div>"+
          "<div id=refman-right><div><label for=refman-inputname>Name:</label><input id=refman-inputname type=text/>"+
          "<div id=refman-actions><label for=refman-update>Actions:</label>"+
          "<input type=button id=refman-update class=refman-whenupdating value='Update'/>"+
          "<input type=button id=refman-remove class=refman-whenupdating value='Remove'/>"+
          "<input type=button id=refman-merge  class=refman-whenmerging  value='Merge References'/>"+
          "<input type=button id=refman-cancel value='Cancel'/></div>"+
          "<div id=refman-reuses><span>Uses:</span><ol></ol></div>"+
          "<div id=refman-reflinks class=refman-whenupdating><a id=refman-previous title='Previous Reference'>⇦ Previous</a>"+
          "<a id=refman-next title='Next Reference'>Next ⇨</a></div></div></div>").appendTo(editref_div);

        editref_div.find("#refman-previous").click(() => {
            refman_state.move_selected(-1);
        });
        editref_div.find("#refman-next").click(() => {
            refman_state.move_selected(+1);
        });
        editref_div.find("#refman-remove").click(() => {
            refman_state.remove_ref(refman_state.selected);
        });
        editref_div.find("#refman-update").click(() => {
            refman_state.update_ref(
                refman_state.selected,
                $("#refman-inputname").val(),
                $("#refman-refcontent").val()
            );
        });
        editref_div.find("#refman-merge").click(() => {
            if ($("#refman-inputname").val() === "") {
                alert("You need to provide a name! How else to merge them?");
            } else {
                refman_state.merge_refs(
                    refman_state.selected,
                    $("#refman-inputname").val(),
                    $("#refman-refcontent").val()
                )
            }
        });
        editref_div.find("#refman-cancel").click(() => {
            refman_state.delete_cache(refman_state.selected);
            refman_state.select_ref();
        });

        editref_div.find("#refman-inputname, #refman-refcontent").on("input", () => {
            refman_state.add_cache(
                refman_state.selected,
                $("#refman-inputname").val() || undefined,
                $("#refman-refcontent").val()
            ); 
        });
        editref_div.find("#refman-refcontent").on("input", function() {
            refman_state.show_links($(this).val());
        });

        $(document).keydown(function(e) {
            let link = $("#refman-showlinks a:hover");
            
            // 17 is ctrl. e.ctrlKey doesn't work when input elements are selected, for some reason
            if (e.which === 17 && link.length) {
                var refcontent = $("#refman-refcontent").get(0);
                refcontent.select();
                refcontent.setSelectionRange(
                    link.data("start"),
                    link.data("end")
                );
            }
        });


        container.append(editref_div).append(main_div).appendTo("body");

        var input_count = 0; // Used to rate-limit parsing
        $("#wpTextbox1").on('input', function() {
            input_count++;
            setTimeout(function() {
                input_count--;
                if (input_count === 0) {
                    if ((Object.keys(refman_state.cache).length === 0) ||
                        confirm("You have modified the main text; your changes in the RefMan interface will disappear. Do you want to proceed even so?")) {
                        refman_state.select_ref();
                        refman_state = new_ref_state($("#wpTextbox1").val(), undefined, true);
                        $("#wpTextbox1").focus();
                    } else {
                        $("#wpTextbox1").val(refman_state.text);
                    }
                }
            }, 500); // Parse .5 seconds after change
        });
        $("#wpTextbox1").on("keydown click focus", function() {
            setTimeout(function() { // Adding delay to get the new cursor position
                refman_state.highlight_ref_at_cursor();
            }, 20);
        });
        $("#wpTextbox1").on("blur", function() {
            $(".refman-ref-at-point").removeClass("refman-ref-at-point");
        });
    }

    var refman_state; // The current RefState object should always be assigned to this global variable

    function toggle_refman() {
        if (refman_state === undefined) {
            load_stuff();
            refman_state = new_ref_state($("#wpTextbox1").val(), undefined, true);
            $("#wpTextbox1").addClass("refman-small-textarea");
        } else {
            $("#refman-container").toggleClass("refman-hidden");
            $("#wpTextbox1").toggleClass("refman-small-textarea");
        }
        refman_state.highlight_ref_at_cursor();
    }

    if (['edit', 'submit'].includes(mw.config.get('wgAction'))) {
        mw.loader.using('user.options').then(function () {
            if (!mw.user.options.get('usebetatoolbar') === 1)
                throw "hands in the air";
            
	    $.when(mw.loader.using( 'ext.wikiEditor' ), $.ready).then(() => {
                $("#wpTextbox1").wikiEditor('addToToolbar', {
                    'section': 'main',
                    'group': 'insert',
                    'tools': {
                        'refman': {
                            'label': 'RefMan',
                            'type': 'button',
                            'icon': '//upload.wikimedia.org/wikipedia/commons/1/15/RefMan.png',
                            'action': {
                                'type': 'callback',
                                'execute': toggle_refman
                            }}}});
            });
        });
    }
});
  run($, mw);
});