/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Eswara Rajesh Pinapala (edited Chris Pollett)
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2014-2021
 * @filesource
 */
/*
 * This script adds buttons to textareas on a page which when clicked insert
 * the relevant way to format something using wiki syntax.
 *
 * The editor automatically renders the editor buttons using the following list
 * of button names for configuration. These can be set on the data-buttons
 * attribute of the textarea
 * {
 * "wikibtn-bold"
 * "wikibtn-italic"
 * "wikibtn-underline"
 *" wikibtn-strike"
 * "wikibtn-nowiki"
 * "wikibtn-hyperlink"
 * "wikibtn-bullets"
 * "wikibtn-numbers"
 * "wikibtn-hr"
 * "wikibtn-heading"
 * "wikibtn-search"
 * "wikibtn-table"
 * "wikibtn-slide"
 * "wikibtn-definitionlist"
 * }
 */
/**
 * Used to contain formatting info about all buttons on the wiki editor
 * @var Array
 */
var editor_all_buttons = [];
/**
 * Used to store formatting info buttons on a particular textarea
 * @var Array
 */
var editor_buttons = [];
/**
 * Object that buffers selection information.
 * @var Object
 */
var editor_buffer = {};
/**
 * Object containing all icons for editor toolbar
 * @var Object
 */
var editor_toolbar_icons = {
    "wikibtn-bold": "<b>B</b>",
    "wikibtn-italic": "<i>I</i>",
    "wikibtn-underline": "<u>U</u>",
    "wikibtn-strike": "<s>S</s>",
    "wikibtn-nowiki": `<div class='wiki-icon-nowiki'>W</div>`,
    "wikibtn-hyperlink": "🔗",
    "wikibtn-bullets": `<div class="wikibtn-icon-bullets"
        ><div>&vellip;</div></div>&equiv;`,
    "wikibtn-numbers": `<div class='wikibtn-icon-numbers'
        ><div>1</div><div>2</div><div>3</div></div>&equiv;`,
    "wikibtn-hr": "⎯",
    "wikibtn-heading": "H",
    "wikibtn-search-widget": "🔎",
    "wikibtn-more-syntax": "…",
    "wikibtn-table": "⊞",
    "wikibtn-slide": "🧑‍🏫",
    "wikibtn-definitionlist": "≝",
    "wikibtn-leftaligned": `<div class="wikibtn-icon-aligned
        wikibtn-icon-leftaligned"><span></span><span></span><span
        ></span><span></span></div>`,
    "wikibtn-centeraligned": `<div class="wikibtn-icon-aligned
        wikibtn-icon-centeraligned"><span></span><span></span><span
        ></span><span></span></div>`,
    "wikibtn-rightaligned": `<div class="wikibtn-icon-aligned
        wikibtn-icon-rightaligned"><span></span><span></span><span
        ></span><span></span></div>`,
};

/*
 * Add buttons for common wiki operation before the textareas on a page.
 */
function editorizeAll()
{
    var text_areas = tag("textarea");
    var len = text_areas.length;
    var ids = new Array();
    for (i = 0; i < len; i++)
    {
        if (text_areas[i].id){
            editorize(text_areas[i].id);
        }
    }
    initResourceUndo();
}
/**
 * Adds Wiki editor buttons to a textarea with the given id
 *
 * @param String id identifier of the text area to have wiki buttons added to
 */
function editorize(id)
{
    /*
     check if the editor div exists.
     */
    var node = elt(id);
    if (node === null) {
        /*
         If the div to render wiki_editor is not found, do nothing.
         */
        alert("No textarea found with id = " + id);
        return false;
    }else{
        node.addEventListener("focus", function(event)
        {
            enableKBShortcuts(id);
        }, true);
    }
    var button_string = "";
    /*
        Initialize tool bar wiki_buttons object
     */
    editor_buttons[id] = {};
    /*
        filter the buttons according to the data-buttons
        attribute.
     */
    filterButtons(id);
    /*
        Buttons are rendered below.
     */
    for (var prop in editor_buttons[id]) {
        var no_buttons = ['wikibtn-search', 'wikibtn-table', 'wikibtn-heading'];
        if (editor_buttons[id].hasOwnProperty(prop)
            && (no_buttons.indexOf(prop) === -1)) {
            translate_prop = tl[prop.replace(/wikibtn-/, 'wiki_js_')];
            button_string +=
                '<div class="' + prop +' icon-button-container">' +
                '<a class="icon-anchor-button">' +
                '<span role="img" ' +
                'aria-label="' + translate_prop +
                '" onclick="wikifySelection(\'' +prop + '\', \''+ id +'\');">'+
                editor_toolbar_icons[prop] +
                '</span></a></div>';
        }
    }
    /*
        Render the wiki-popup-prompt div used to prompt
        user input.
     */
    var editor_toolbar = '<div id="wiki-popup-prompt-' + id +
        '" class="wiki-popup-prompt" style="display: none;"></div>' +
        '<div class="wiki-buttons">' + button_string;
    /*
        check if heading was desired and render if was.
     */
    if (editor_buttons[id].hasOwnProperty('wikibtn-heading')) {
        translate_prop = tl['wiki_js_heading'];
        editor_toolbar += '<select id="wiki-heading-' +id +
            '" title="' + translate_prop +
            '" value="heading" ' +
            'onchange="addWikiHeading(\''+ id +'\');">' +
            '<option selected="" disabled="">'+
            tl['wiki_js_heading'] + '</option>' +
            '<option value="1">H1</option>' +
            '<option value="2">H2</option>' +
            '<option value="3">H3</option>' +
            '<option value="4">H4</option>' +
            '</select>';
    }
    /*
     check if table was desired and render if was.
     */
    if (editor_buttons[id].hasOwnProperty('wikibtn-table')) {
        translate_prop = tl['wiki_js_add_wiki_table'];
        editor_toolbar += '<div class="wikibtn-table icon-button-container">' +
            '<a class="icon-anchor-button">' +
            '<span role = "img" ' +
            'aria-label = "' +translate_prop +
            '" onclick = "addWikiTable(\''+ id +'\');">'+
            editor_toolbar_icons["wikibtn-table"] +
            '</span></a></div>';
    }
    /*
     check if search was desired and render if was.
     */
    if (editor_buttons[id].hasOwnProperty('wikibtn-search')) {
        translate_prop = tl['wiki_js_add_search'];
        editor_toolbar += '<div class="wikibtn-search-widget ' +
            'icon-button-container">' +
            '<a class="icon-anchor-button">' +
            '<span role="img" aria-label="' + translate_prop +
            '" onclick = "addWikiSearch(\''+ id +'\');">'+
            editor_toolbar_icons["wikibtn-search-widget"] +
            '</span></a></div>';
    }
    /*
     The marks above cover the markup a writer reaches for most. The rest
     of it is offered from a list, so that everything the parser reads can
     be put in without remembering how it is spelt.
     */
    if (editor_buttons[id].hasOwnProperty('wikibtn-search')) {
        translate_prop = tl['wiki_js_more_syntax'];
        editor_toolbar += '<div class="wikibtn-more-syntax ' +
            'icon-button-container" id="more-syntax-holder-' + id + '">' +
            '<a class="icon-anchor-button">' +
            '<span role="img" aria-label="' + translate_prop +
            '" aria-haspopup="true" aria-expanded="false" ' +
            'id="more-syntax-mark-' + id + '"' +
            ' onclick = "addWikiMoreSyntax(\''+ id +'\');">'+
            editor_toolbar_icons["wikibtn-more-syntax"] +
            '</span></a></div>';
    }
    /*
     A page that can be saved is offered saving on its own. A field with
     no buttons of its own is a form to be filled in and sent, such as a
     page's settings, so the keeping is not offered there and the save
     button stays where it is.
     */
    if (elt("save-container") && elt(id) && elt(id).form &&
        ((editor_buttons[id] &&
        Object.keys(editor_buttons[id]).length > 0) ||
        (elt("resource-save-controls") && id == "wiki-page"))) {
        editor_toolbar += '<div class="wikibtn-autosave ' +
            'icon-button-container">' +
            '<a class="icon-anchor-button">' +
            '<button type="button" id="autosave-button-' + id + '" ' +
            'class="autosave-on" aria-label="' + tl['wiki_js_autosave'] +
            '" title="' + tl['wiki_js_autosave'] + '" ' +
            'onclick="toggleWikiAutoSave(\'' + id + '\');">' +
            WIKI_AUTOSAVE_ICON + '</button></a></div>' +
            '<span id="autosave-saying-' + id +
            '" class="autosave-saying"></span>';
    }
    editor_toolbar += '</div>';
    /*
        Insert the toolbar div before the textarea
     */
    node.insertAdjacentHTML("beforebegin", editor_toolbar);
    let save_by_hand = elt("wiki-save-button");
    let keeping = elt("autosave-button-" + id);
    if (save_by_hand && keeping && keeping.parentNode &&
        keeping.parentNode.parentNode &&
        !elt("resource-save-controls")) {
        keeping.parentNode.parentNode.appendChild(save_by_hand);
    }
    let beside_name = elt("resource-save-controls");
    if (beside_name && id == "wiki-page" && keeping && keeping.parentNode &&
        keeping.parentNode.parentNode) {
        beside_name.appendChild(keeping.parentNode.parentNode);
        let saying = elt("autosave-saying-" + id);
        if (saying) {
            beside_name.appendChild(saying);
        }
    }
    startWikiAutoSave(id);
    return;
}
/*
 * How long the writer must leave the page alone before it is saved for
 * them, in milliseconds. Long enough that a save does not land in the
 * middle of a sentence, short enough that little is ever at risk.
 */
var WIKI_AUTOSAVE_WAIT = 3000;
/*
 * The two arrows going round that stand for a page keeping itself. They
 * are drawn rather than written so the control sits in the toolbar the
 * size the other buttons are, and the line across them is drawn here too
 * and shown only while the keeping is off.
 */
var WIKI_AUTOSAVE_ICON = '<svg viewBox="0 0 24 24" width="100%" ' +
    'height="100%" fill="none" stroke="currentColor" ' +
    'stroke-width="2.4" stroke-linecap="butt" ' +
    'xmlns="http://www.w3.org/2000/svg">' +
    '<path d="M3.6 12A8.4 8.4 0 0 1 18 6.1"/>' +
    '<polygon points="21.4 2.2 21.4 9.4 14.2 9.4" ' +
    'fill="currentColor" stroke="none"/>' +
    '<path d="M20.4 12A8.4 8.4 0 0 1 6 17.9"/>' +
    '<polygon points="2.6 21.8 2.6 14.6 9.8 14.6" ' +
    'fill="currentColor" stroke="none"/>' +
    '<line class="autosave-bar" x1="0.9" y1="0.9" x2="23.1" ' +
    'y2="23.1" stroke-width="1.6"/></svg>';
/*
 * How long the word that a save is happening stays on screen, in
 * milliseconds. A save of a page this size is over in a moment, so
 * without a floor the word would come and go unread and a writer would
 * never know their work had been kept.
 */
var WIKI_AUTOSAVE_SHOWN = 1200;
/*
 * The timer counting down to a save, kept so a further change can push
 * it back rather than let two saves run.
 */
var wiki_autosave_waiting = null;
/*
 * Whether the page is being saved for the writer without their asking.
 * It starts on, so a page is kept without anyone having to remember.
 */
var wiki_autosave_on = true;
/*
 * The name under which the writer's choice is remembered for as long as
 * they are working, so turning it off stays off from one page to the
 * next rather than coming back on at every save.
 */
var WIKI_AUTOSAVE_CHOICE = "wiki-autosave";
/*
 * Which field's control is the one on screen. A page can hold more than
 * one editor, and each gets a control of its own, so the one belonging
 * to the field being written in is the one to speak through.
 */
var wiki_autosave_field = "";
/*
 * What the mark guarding a save is called in a form. The page carries
 * it, so it is read from there rather than written in twice.
 */
var wiki_token_name = "YIOOP_TOKEN";
/*
 * Takes the mark the site sends back and puts it in the form in place
 * of the one that was there. The mark carries the time the page was
 * fetched, and the site refuses a save whose mark is older than the
 * page's last change. Without this, the first save of a sitting would
 * land and every one after it would be turned away as though someone
 * else had been editing.
 *
 * @param Object form the form the page is saved through
 * @param String page what the site sent back, holding a fresh mark
 */
function takeFreshWikiToken(form, page)
{
    if (!form || !page) {
        return;
    }
    var field = form.elements[wiki_token_name];
    if (!field) {
        return;
    }
    var pattern = new RegExp('name=["\']' + wiki_token_name +
        '["\'][^>]*value=["\']([^"\']+)["\']');
    var found = page.match(pattern);
    if (!found) {
        pattern = new RegExp('value=["\']([^"\']+)["\'][^>]*name=["\']' +
            wiki_token_name + '["\']');
        found = page.match(pattern);
    }
    if (found) {
        field.value = found[1];
    }
}
/*
 * Writes down where the writer was in the page, so that when the page
 * comes back after a save it comes back at the same place with the
 * cursor where they left it. The form used to do this itself as it was
 * submitted, which left anything sent another way, such as a file added
 * to the page, coming back at the top.
 */
function noteWikiPlace()
{
    var caret_at = elt("caret-pos");
    var scrolled_to = elt("scroll-top");
    var field = elt("wiki-page");
    if (!caret_at || !scrolled_to || !field) {
        return;
    }
    caret_at.value = field.selectionStart ? field.selectionStart : 0;
    scrolled_to.value = field.scrollTop ? field.scrollTop : 0;
}
/*
 * Takes the page's own id from what the site sends back and puts it on
 * the field. A page written for the first time has no id until it is
 * saved, and a page that saves itself never reloads, so without this the
 * preview went on asking the site for a file belonging to page nothing
 * and drew a picture that could not be found.
 *
 * @param Object field the text area holding the page's source
 * @param String page what the site sent back
 */
function takeFreshWikiPageId(field, page)
{
    if (!field || !page || field.getAttribute("data-page-id")) {
        return;
    }
    var found = page.match(/data-page-id=['"](\d+)['"]/);
    if (found && found[1] !== "0") {
        field.setAttribute("data-page-id", found[1]);
    }
}
/*
 * Which parts of a page's settings each kind of page uses. A kind not
 * named here is treated as a standard page. Anything not listed for the
 * kind chosen is put away, so a writer sees only what applies: an alias
 * page has nothing but the page it stands for, a shortened address has
 * no icon, and a front page has no resource path.
 */
var PAGE_TYPE_SHOWS = {
    "standard": ["non-alias-type", "page-border-setting",
        "page-theme-setting", "page-toc-setting", "public-source-setting",
        "page-title-setting", "meta-author-setting",
        "meta-robots-setting", "meta-description-setting",
        "meta-properties-setting", "alt-path-setting",
        "page-header-setting", "page-footer-setting"],
    "page_and_feedback": ["non-alias-type", "page-border-setting",
        "page-theme-setting", "page-toc-setting", "public-source-setting",
        "page-title-setting", "meta-author-setting",
        "meta-robots-setting", "meta-description-setting",
        "meta-properties-setting", "alt-path-setting",
        "page-header-setting", "page-footer-setting"],
    "page_alias": ["alias-type"],
    "media_list": ["non-alias-type", "page-border-setting",
        "page-theme-setting", "static-html-folder-setting",
        "public-source-setting", "page-title-setting",
        "meta-author-setting", "meta-robots-setting",
        "meta-description-setting", "meta-properties-setting",
        "alt-path-setting", "update-description-setting",
        "page-header-setting", "page-footer-setting"],
    "presentation": ["non-alias-type", "page-border-setting",
        "page-theme-setting", "public-source-setting",
        "page-title-setting", "meta-author-setting",
        "meta-robots-setting", "meta-description-setting",
        "meta-properties-setting", "alt-path-setting"],
    "url_shortener": ["non-alias-type", "shortener-container",
        "short-url-label",
        "public-source-setting", "page-title-setting",
        "meta-author-setting", "meta-robots-setting",
        "meta-description-setting"],
    "share": ["share-container", "non-alias-type", "page-border-setting",
        "page-theme-setting", "public-source-setting",
        "page-title-setting", "meta-author-setting",
        "meta-robots-setting", "meta-description-setting",
        "meta-properties-setting", "alt-path-setting",
        "page-header-setting", "page-footer-setting"],
    "git_repository": ["non-alias-type", "page-border-setting",
        "page-theme-setting", "public-source-setting",
        "page-title-setting", "meta-author-setting",
        "meta-robots-setting", "meta-description-setting",
        "meta-properties-setting", "alt-path-setting",
        "page-header-setting", "page-footer-setting",
        "anonymous-issue-setting"],
    "news_article": ["non-alias-type", "page-border-setting",
        "page-theme-setting", "page-toc-setting", "public-source-setting",
        "meta-robots-setting", "page-header-setting",
        "page-footer-setting", "page-icon-setting", "page-title-setting",
        "meta-author-setting", "meta-description-setting",
        "article-categories-setting", "article-date-setting"],
    "front_page": ["non-alias-type", "front-page-settings",
        "page-title-setting", "meta-author-setting",
        "meta-robots-setting", "meta-description-setting",
        "meta-properties-setting", "page-header-setting",
        "page-footer-setting"]
};
/*
 * Shows the controls a front page needs and hides the ones it does not,
 * as soon as the kind of page is chosen rather than after a save.
 */
/*
 * Where each control that an article moves lives when the page is not an
 * article, so it can be put back. The place is remembered the first time
 * a control is moved, since after that its neighbors have changed.
 */
var ARTICLE_MOVED_HOME = {};
/*
 * The controls an article shows above its writing area, in the order a
 * writer meets them: the thumbnail, the title, the author, then the
 * abstract.
 */
var ARTICLE_MOVES = ["page-icon-setting", "page-title-setting",
    "meta-author-setting", "meta-description-setting",
    "article-date-setting"];
/*
 * Which part of the article block each control is put into, so the title
 * and the author stand together beside the thumbnail rather than each
 * taking a line of its own.
 */
var ARTICLE_AREAS = {
    "page-icon-setting": "article-header-thumb",
    "page-title-setting": "article-header-fields",
    "meta-author-setting": "article-header-fields",
    "meta-description-setting": "article-header-fields",
    "article-date-setting": "article-header-fields"
};
/*
 * Reads the label a control should carry, keeping the one it arrived
 * with so it can be given back when the page stops being an article.
 *
 * @param Element part the control being relabeled
 * @param String wanted what the label should say now, or "" for the one
 *      it arrived with
 */
function setArticleLabel(part, wanted)
{
    var label = part ? part.getElementsByTagName("label")[0] : null;
    var bold = label ? label.getElementsByTagName("b")[0] : null;
    if (!bold) {
        return;
    }
    if (bold.getAttribute("data-first-label") === null) {
        bold.setAttribute("data-first-label", bold.textContent);
    }
    bold.textContent = (wanted === "") ?
        bold.getAttribute("data-first-label") : wanted;
}
/*
 * Puts the thumbnail, title, author and abstract above the writing area
 * when an article is being written, and back in the settings panel when
 * it is not. The controls are moved rather than drawn a second time, so
 * the form never holds two fields of one name; a control put away with a
 * style still sends its value, and two would disagree.
 *
 * @param Boolean is_article whether the kind chosen is a news article
 */
/*
 * Says whether the kind of page being made uses one of the named parts of
 * the settings.
 *
 * @param String name which part
 * @return bool whether this kind of page uses it
 */
/*
 * The parts of the settings a writer only wants now and then, kept out of
 * the way until they ask for them. A page's name, who wrote it and how it
 * is laid out are what most writing needs; the rest describe the page to
 * something other than a reader.
 */
var ADVANCED_SETTINGS = ["meta-robots-setting", "meta-description-setting",
    "meta-properties-setting", "page-header-setting",
    "page-footer-setting"];
/*
 * Whether the settings only wanted now and then are being shown.
 */
var showing_advanced = false;
/*
 * Turns the settings only wanted now and then on or off, and says on the
 * link which way it will go next.
 */
function showAdvancedSettings()
{
    showing_advanced = !showing_advanced;
    var link = elt("advanced-settings-link");
    if (link) {
        link.innerHTML = showing_advanced ?
            tl["wiki_element_basic_settings"] :
            tl["wiki_element_advanced_settings"];
    }
    applyAdvancedSettings();
}
/*
 * Shows or hides each of the settings only wanted now and then, leaving
 * alone any this kind of page does not use at all.
 *
 * Only a front page tucks these away. Every other kind of page shows what
 * it always showed, so a writer of an ordinary page does not have to go
 * looking for a control that was there yesterday.
 */
function applyAdvancedSettings()
{
    var chooser = elt("page-type");
    var tucks_away = chooser && chooser.value == "front_page";
    var line = elt("advanced-settings-line");
    if (line) {
        line.style.display = tucks_away ? "" : "none";
    }
    for (var index = 0; index < ADVANCED_SETTINGS.length; index++) {
        var part = elt(ADVANCED_SETTINGS[index]);
        if (!part || !pageTypeUsesSetting(ADVANCED_SETTINGS[index])) {
            continue;
        }
        part.style.display = (!tucks_away || showing_advanced) ?
            "" : "none";
    }
}
/*
 * Wires the button that sets when an article was written. Pressing it
 * writes the moment it was pressed onto the button itself and into the
 * field the form sends, so a writer never types a date and can put the
 * moment forward by pressing again.
 *
 * @param String id identifier of the text area holding the page
 */
function initArticleDate(id)
{
    var button = elt("article-date-button");
    var field = elt("article-date");
    if (!button || !field) {
        return;
    }
    button.addEventListener("click", function ()
    {
        var now = new Date();
        var said = now.getFullYear() + "-" +
            twoFigures(now.getMonth() + 1) + "-" +
            twoFigures(now.getDate()) + " " +
            twoFigures(now.getHours()) + ":" +
            twoFigures(now.getMinutes());
        button.textContent = said;
        field.value = said;
        wikiChangeMade(id);
    });
}
/*
 * Gives a number as two figures, so a month or an hour reads the same
 * width whatever it is.
 *
 * @param int number the number
 * @return String it as two figures
 */
function twoFigures(number)
{
    return (number < 10) ? "0" + number : "" + number;
}
function pageTypeUsesSetting(name)
{
    var chooser = elt("page-type");
    var shows = chooser ? PAGE_TYPE_SHOWS[chooser.value] : undefined;
    if (shows === undefined) {
        shows = PAGE_TYPE_SHOWS["standard"];
    }
    return shows.indexOf(name) > -1;
}
function placeArticleHeader(is_article)
{
    var header = elt("article-header");
    for (var index = 0; index < ARTICLE_MOVES.length; index++) {
        var name = ARTICLE_MOVES[index];
        var part = elt(name);
        if (!part) {
            continue;
        }
        if (!header) {
            /* A screen showing the settings on their own has nowhere to
               put these, and an article does not want them there, so
               they are put away rather than moved. A part this kind of
               page does not use stays put away: showing it again here
               undid the hiding done a moment before, which left a front
               page offering a thumbnail it never uses. */
            part.style.display = (is_article || !pageTypeUsesSetting(name)) ?
                "none" : "";
            continue;
        }
        if (ARTICLE_MOVED_HOME[name] === undefined) {
            ARTICLE_MOVED_HOME[name] = {"parent": part.parentNode,
                "before": part.nextSibling};
        }
        if (is_article) {
            elt(ARTICLE_AREAS[name]).appendChild(part);
        } else {
            var home = ARTICLE_MOVED_HOME[name];
            home.parent.insertBefore(part, home.before);
        }
    }
    if (!header) {
        return;
    }
    /* What a banner shows and who holds it belong to an article and to
       nothing else, so the block holding them is put away with the rest
       of the article's own header. They had been left standing on every
       kind of page. */
    header.style.display = is_article ? "" : "none";
    setArticleLabel(elt("page-icon-setting"),
        is_article ? header.getAttribute("data-thumbnail-label") : "");
    setArticleLabel(elt("meta-description-setting"),
        is_article ? header.getAttribute("data-abstract-label") : "");
}
/*
 * Rebuilds the one line with commas between that the hidden field carries
 * for an article's categories, from the rows now standing in the list.
 */
function syncArticleCategories()
{
    var hidden = elt("article-categories");
    if (!hidden) {
        return;
    }
    var texts = sel("#article-category-list .tag-text");
    var names = [];
    for (var index = 0; index < texts.length; index++) {
        names.push(texts[index].textContent);
    }
    hidden.value = names.join(",");
    /* A field set by script fires nothing, so the change is reported
       here: filing an article under a category is a change to the page
       and is kept like any other. */
    if (wiki_autosave_field) {
        wikiChangeMade(wiki_autosave_field);
    }
}
/*
 * Wires the picker that files an article. Choosing a name from the
 * group's own list puts a row in; the mark on a row takes it out. Either
 * way the hidden field is rebuilt, and that field is what the save reads.
 * Removal is listened for on the list rather than on each mark, so a row
 * added after the page loaded behaves like one that arrived with it.
 */
function initArticleCategories()
{
    var add_button = elt("article-category-add-button");
    var chooser = elt("article-category-add");
    var list = elt("article-category-list");
    if (!add_button || !chooser || !list) {
        return;
    }
    var remove_label = add_button.getAttribute("data-remove-label") || "";
    listen(add_button, "click", function () {
        var name = chooser.value;
        if (!name) {
            return;
        }
        var texts = list.querySelectorAll(".tag-text");
        for (var index = 0; index < texts.length; index++) {
            if (texts[index].textContent === name) {
                chooser.value = "";
                return;
            }
        }
        var row = ce("li");
        row.className = "tag-item";
        var text = ce("span");
        text.className = "tag-text";
        text.textContent = name;
        var remove = ce("button");
        remove.type = "button";
        remove.className = "tag-remove article-category-remove";
        remove.title = remove_label;
        remove.innerHTML = "&times;";
        row.appendChild(text);
        row.appendChild(remove);
        list.appendChild(row);
        chooser.value = "";
        syncArticleCategories();
    });
    listen(list, "click", function (event) {
        var pressed = event.target;
        if (!pressed ||
            !pressed.classList.contains("article-category-remove")) {
            return;
        }
        var row = pressed.parentNode;
        if (row && row.parentNode) {
            row.parentNode.removeChild(row);
            syncArticleCategories();
        }
    });
}
function showFrontPageSettings()
{
    var chooser = elt("page-type");
    if (!chooser) {
        return;
    }
    /* Only the front page's own block wants that block to be there. The
       showing and hiding belongs to every kind of page, so requiring it
       here left every screen without one showing every control. */
    var shows = PAGE_TYPE_SHOWS[chooser.value];
    if (shows === undefined) {
        shows = PAGE_TYPE_SHOWS["standard"];
    }
    /* Every named part of the settings is put away, then the ones this
       kind of page uses are brought back, so the form always shows what
       belongs to what is being made. */
    for (var name in PAGE_TYPE_SHOWS) {
        var list = PAGE_TYPE_SHOWS[name];
        for (var index = 0; index < list.length; index++) {
            var part = elt(list[index]);
            if (part) {
                part.style.display = "none";
            }
        }
    }
    for (var wanted = 0; wanted < shows.length; wanted++) {
        var showing = elt(shows[wanted]);
        if (showing) {
            showing.style.display = "";
        }
    }
    /* The article's own controls are moved after the showing and hiding,
       since moving one first would leave it hidden in its new place. */
    placeArticleHeader(chooser.value == "news_article");
    /* The settings only wanted now and then stay put away when a kind of
       page is chosen, since choosing a kind is not asking for them. */
    applyAdvancedSettings();
    showFrontPageSlots();
}
/*
 * Shows the places belonging to the shape a writer has chosen and puts
 * the others away, so the fields on screen always match the picture
 * beside them.
 */
function showFrontPageSlots()
{
    var picked = document.querySelector(
        "input[name='front_page_layout']:checked");
    var sets = document.querySelectorAll(".front-page-slots");
    for (var index = 0; index < sets.length; index++) {
        var set = sets[index];
        set.style.display = (picked &&
            set.getAttribute("data-layout") === picked.value) ? "" : "none";
    }
}
/*
 * Sets each shape to show its own places as it is chosen.
 */
function watchFrontPageLayout()
{
    var buttons = document.querySelectorAll(
        "input[name='front_page_layout']");
    for (var index = 0; index < buttons.length; index++) {
        listen(buttons[index], "change", showFrontPageSlots);
    }
    var kinds = document.querySelectorAll(".front-page-kind");
    for (var which = 0; which < kinds.length; which++) {
        listen(kinds[which], "change", function ()
        {
            showFrontPageSlotKind(this.getAttribute("data-slot"));
        });
        showFrontPageSlotKind(kinds[which].getAttribute("data-slot"));
    }
}
/*
 * Shows the control a place needs for what it holds: a page to name
 * where it holds one article, a category to pick where it holds a whole
 * category of them.
 *
 * @param String slot which place within its layout
 */
function showFrontPageSlotKind(slot)
{
    var chooser = elt("front-kind-" + slot);
    var page = elt("front-slot-" + slot);
    var category = elt("front-category-" + slot);
    if (!chooser) {
        return;
    }
    var wants_page = (chooser.value === "article");
    if (page) {
        page.style.display = wants_page ? "" : "none";
    }
    /* How much to show belongs to the kind: paragraphs of a story, items
       and a heading for a category's list. A place holding the names of a
       group's categories asks for nothing further, so none of these
       belong beside it. */
    var wants_names = (chooser.value === "category_names");
    var much = document.querySelectorAll(
        "[data-slot='" + slot + "'][data-for]");
    for (var index = 0; index < much.length; index++) {
        much[index].style.display = (!wants_names &&
            (much[index].getAttribute("data-for") === "article") ===
            wants_page) ? "" : "none";
    }
    var paragraphs = elt("front-paragraphs-" + slot);
    if (paragraphs) {
        paragraphs.style.display = (wants_page && !wants_names) ? "" : "none";
    }
    var items = elt("front-items-" + slot);
    if (items) {
        items.style.display = (wants_page || wants_names) ? "none" : "";
    }
    if (category) {
        category.style.display = (wants_page || wants_names) ?
            "none" : "";
    }
}
/*
 * Sets the settings form to follow the kind of page chosen, so the
 * controls on show match what is being made.
 */
function watchPageTypeChoice()
{
    var chooser = elt("page-type");
    if (!chooser) {
        return;
    }
    listen(chooser, "change", showFrontPageSettings);
    watchFrontPageLayout();
    initArticleCategories();
    showFrontPageSettings();
}
/*
 * Says whether the page is keeping itself, so work elsewhere can tell
 * whether there will be a save to carry something along with.
 *
 * @return bool true when the page is being saved without asking
 */
function wikiAutoSaveIsOn()
{
    return wiki_autosave_on;
}
/*
 * Gives back the control belonging to one editor. A page can hold more
 * than one, so each is named after the field it saves rather than all
 * sharing one name, which left two of the three without a place on the
 * page at all.
 *
 * @param String id identifier of the text area holding the page
 * @return Object the control, or nothing where there is none
 */
function wikiAutoSaveButton(id)
{
    return elt("autosave-button-" + id);
}
/*
 * Gives back what the writer last chose about the page saving itself,
 * which is remembered for as long as they are working so the choice is
 * not undone by a save or by moving to another page.
 *
 * @return String "off" when they turned it off, otherwise an empty
 *      answer, which leaves it on
 */
function rememberedWikiAutoSave()
{
    try {
        return window.sessionStorage.getItem(WIKI_AUTOSAVE_CHOICE) || "";
    } catch (problem) {
        return "";
    }
}
/*
 * Remembers what the writer chose about the page saving itself. A
 * browser that will not remember is no reason to refuse the choice, so
 * nothing is made of being turned away.
 *
 * @param String choice "on" or "off"
 */
function rememberWikiAutoSave(choice)
{
    try {
        window.sessionStorage.setItem(WIKI_AUTOSAVE_CHOICE, choice);
    } catch (problem) {
        return;
    }
}
/*
 * Sets the page up to save itself: the controls a writer would have used
 * to save by hand are put away while it does, and every change to the
 * source starts the wait over.
 *
 * @param String id identifier of the text area holding the page
 */
function startWikiAutoSave(id)
{
    var field = elt(id);
    if (!field || !wikiAutoSaveButton(id)) {
        return;
    }
    wiki_autosave_field = id;
    wiki_autosave_on = (rememberedWikiAutoSave() !== "off");
    showWikiSaveControls();
    field.addEventListener("input", function ()
    {
        wikiChangeMade(id);
    });
    watchKeptFields(id);
    initArticleDate(id);
}
/*
 * Offers keeping and saving for a file shown as a grid rather than as
 * text. Such a file has no writing area to hang them on, but the grid
 * keeps what it holds in a hidden field the form sends, so that field is
 * watched in place of one. The two controls stand beside the file's name,
 * where a writing area's would stand beside its marks.
 */
/*
 * Fills in each place a front page holds, by asking the site to draw it.
 * A place naming a story or a category is answered from what the group
 * holds at the moment the page is read, so a preview cannot draw it here
 * and asks for the drawn place instead.
 *
 * @param Object target the box the preview was drawn into
 */
function fillFrontPagePlaces(target)
{
    var waiting = target.querySelectorAll(".front-page-waiting");
    for (var index = 0; index < waiting.length; index++) {
        askForFrontPagePlace(waiting[index]);
    }
}
/*
 * Asks the site to draw one place and puts what comes back where the box
 * was standing. A place that cannot be drawn leaves the box empty rather
 * than showing the mark, which is not what a reader would see.
 *
 * @param Object box the empty box carrying the mark
 */
function askForFrontPagePlace(box)
{
    var mark = box.getAttribute("data-mark");
    if (!mark) {
        return;
    }
    /* The place is drawn by the part of the site that shows a page, not
       the part that edits one, so the address is the reading of this page
       rather than its editing. Asking at the editing address brought back
       the whole editing page instead of the place. */
    var address = window.location.href.split("#")[0]
        .replace(/&arg=[^&]*/g, "")
        .replace(/&settings=[^&]*/g, "")
        .replace(/&resources=[^&]*/g, "") +
        "&preview_mark=" + encodeURIComponent(mark);
    fetch(address, {"credentials": "same-origin"}).then(function (got) {
        return got.text();
    }).then(function (said) {
        /* What comes back is the place itself, and it stands where the
           box stood rather than inside it: a box around a place is a
           block of its own, so a row of cards read as a column in the
           preview while reading as a row on the page. */
        var holder = document.createElement("div");
        holder.innerHTML = said;
        var parent = box.parentNode;
        while (holder.firstChild) {
            parent.insertBefore(holder.firstChild, box);
        }
        parent.removeChild(box);
        /* The preview wraps each block of a page in a box of its own,
           which is a block and so put every card of a row on a line of
           its own. Where such a box holds nothing but the place, it is
           let fall away. */
        if (parent.tagName === "DIV" && parent.className === "" &&
            parent.parentNode) {
            var above = parent.parentNode;
            while (parent.firstChild) {
                above.insertBefore(parent.firstChild, parent);
            }
            above.removeChild(parent);
        }
    }).catch(function () {
        box.classList.remove("front-page-waiting");
    });
}
function initSpreadsheetKeeping()
{
    var beside_name = elt("resource-save-controls");
    var field = elt("spreadsheet-data");
    if (!beside_name || !field || !field.form) {
        return;
    }
    var id = field.id;
    var says = tl['wiki_js_autosave'];
    beside_name.insertAdjacentHTML("beforeend",
        '<div class="wikibtn-autosave ' +
        'icon-button-container"><a class="icon-anchor-button">' +
        '<button type="button" id="autosave-button-' + id + '" ' +
        'class="autosave-on" aria-label="' + says + '" title="' + says +
        '" onclick="toggleWikiAutoSave(\'' + id + '\');">' +
        WIKI_AUTOSAVE_ICON + '</button></a></div>' +
        '<span id="autosave-saying-' + id + '" class="autosave-saying">' +
        '</span>');
    startWikiAutoSave(id);
}
/*
 * Opens the box asking how large a new picture should be. The file
 * list goes straight to an address when a choice is made, so this
 * catches that first, holds the address the choice would have gone to,
 * and shows the box instead.
 *
 * @param Object event the click on the file list actions
 */
function askNewImageSize(event)
{
    let mark = event.target.closest ?
        event.target.closest("a[href*='new-image-file']") : null;
    let box = elt("new-image-dialog");
    if (!mark || !box) {
        return;
    }
    event.stopPropagation();
    event.preventDefault();
    new_image_url = mark.getAttribute("href");
    box.style.display = "";
    elt("new-image-wide").focus();
}
/*
 * Makes the new picture at the width and height the box holds, by going
 * to the address the file list would have used with the two sizes added.
 * A size left empty or not a number is taken as the ordinary size.
 */
function makeNewImage()
{
    let wide = parseInt(elt("new-image-wide").value, 10) ||
        NEW_IMAGE_DEFAULT_SIDE;
    let tall = parseInt(elt("new-image-tall").value, 10) ||
        NEW_IMAGE_DEFAULT_SIDE;
    window.location = new_image_url + "&image_wide=" + wide +
        "&image_tall=" + tall;
}
/*
 * Closes the box asking how large a new picture should be without
 * making one, which is what the cancel button does.
 */
function dropNewImage()
{
    elt("new-image-dialog").style.display = "none";
}
/*
 * Holds the address the file list would have gone to when a new picture
 * was chosen, so the box can go there once a size is given.
 */
let new_image_url = "";
/*
 * How large each side of a new picture is where a writer gives no size,
 * in pixels. Big enough to draw on, small enough to show whole on a
 * phone.
 */
const NEW_IMAGE_DEFAULT_SIDE = 400;
/*
 * How many earlier states of a file's text are kept so a writer may step
 * back through them with the undo keys.
 */
const RESOURCE_UNDO_DEPTH = 40;
/*
 * How long typing must pause, in thousandths of a second, before the
 * text is kept as a state an undo can return to. Long enough that a run
 * of typing is one step back, short enough that little is lost.
 */
const RESOURCE_UNDO_PAUSE = 400;
/*
 * Holds the earlier states of the text being edited, newest last. One
 * screen edits one file, so one stack is enough.
 */
let resource_earlier = [];
/*
 * Keeps the text as it stands so a later undo can put it back. The copy
 * is taken before a run of typing rather than on every letter, so one
 * undo steps back over a word rather than a character.
 *
 * @param Object field the writing box whose text is kept
 */
function rememberResourceText(field)
{
    let last = resource_earlier[resource_earlier.length - 1];
    if (last === field.value) {
        return;
    }
    resource_earlier.push(field.value);
    if (resource_earlier.length > RESOURCE_UNDO_DEPTH) {
        resource_earlier.shift();
    }
}
/*
 * Puts the text back as it stood before the last run of typing, where a
 * writer presses the control or command key with z. The browser's own
 * stepping back is left alone on a screen this does not set up.
 *
 * @param Object event the key event to read
 */
function undoResourceText(event)
{
    if (!(event.ctrlKey || event.metaKey) || event.shiftKey ||
        event.key.toLowerCase() != "z") {
        return;
    }
    let field = elt("wiki-page");
    if (!field || resource_earlier.length < 1) {
        return;
    }
    event.preventDefault();
    let earlier = resource_earlier.pop();
    if (earlier === field.value && resource_earlier.length > 0) {
        earlier = resource_earlier.pop();
    }
    field.value = earlier;
    field.dispatchEvent(new Event("input", {bubbles: true}));
}
/*
 * Sets up stepping back over the text of a file being edited. The text
 * as it first stands is kept, then a copy is taken whenever typing
 * pauses, and the undo keys walk back through those copies.
 */
function initResourceUndo()
{
    let field = elt("wiki-page");
    if (!field) {
        return;
    }
    resource_earlier = [field.value];
    let waiting = null;
    listen(field, "input", function () {
        clearTimeout(waiting);
        waiting = setTimeout(function () {
            rememberResourceText(field);
        }, RESOURCE_UNDO_PAUSE);
    });
    listen(field, "keydown", undoResourceText);
}
document.addEventListener("click", askNewImageSize, true);
listen(window, "load", function () {
    let make = elt("new-image-make");
    let drop = elt("new-image-drop");
    if (make && drop) {
        listen(make, "click", makeNewImage);
        listen(drop, "click", dropNewImage);
    }
});
/*
 * Asks the writer what to call the file and saves what is on screen
 * under that name, leaving the file that was open as it was.
 *
 * The Save As mark beside Save calls this. The name the file has now is
 * offered to start from, since a writer keeping one version beside
 * another changes only part of it. A writer who cancels, or who gives
 * nothing but spaces, saves nothing at all. A name carrying a folder
 * mark or a step up out of the folder is turned away with a word about
 * why and the box is asked for again, rather than the server quietly
 * flattening it into a name the writer did not choose. The name that
 * passes is put in the field the form carries and the plain save is
 * pressed, so the save takes the same path any other save takes.
 */
function saveWikiResourceAs()
{
    let carries = elt("save-as-name");
    let save_by_hand = elt("wiki-save-button");
    if (!carries || !save_by_hand) {
        return;
    }
    let offer = carries.value;
    let ask = tl['wiki_js_save_as_name'];
    while (true) {
        let typed = window.prompt(ask, offer);
        if (typed === null || typed.trim() === "") {
            return;
        }
        typed = typed.trim();
        if (typed.indexOf("/") >= 0 || typed.indexOf("\\") >= 0 ||
            typed.indexOf("..") >= 0) {
            /* The prompt is a modal that covers any message shown on
               the page behind it, so the reason a name was turned away
               and how to fix it go at the top of the next prompt, where
               the writer will see them, with the name they gave kept to
               correct. */
            ask = tl['wiki_js_save_as_relative'] + "\n\n" +
                tl['wiki_js_save_as_name'];
            offer = typed;
            continue;
        }
        carries.value = typed;
        save_by_hand.click();
        return;
    }
}
/*
 * Turns saving on its own on or off, and says which it is: the button
 * stands green while the page is being kept, and red while it is not.
 *
 * @param String id identifier of the text area holding the page
 */
function toggleWikiAutoSave(id)
{
    wiki_autosave_on = !wiki_autosave_on;
    rememberWikiAutoSave(wiki_autosave_on ? "on" : "off");
    if (!wiki_autosave_on && wiki_autosave_waiting !== null) {
        clearTimeout(wiki_autosave_waiting);
        wiki_autosave_waiting = null;
    }
    showWikiSaveControls();
    if (wiki_autosave_on) {
        wikiChangeMade(id);
    }
}
/*
 * Puts the button in the state saving is in, and shows or hides the
 * controls for saving by hand. While the page keeps itself there is
 * nothing for a writer to press, so the reason for an edit, the save
 * button and the way to see the page are left out.
 */
function showWikiSaveControls()
{
    var button = wikiAutoSaveButton(wiki_autosave_field);
    if (!button) {
        return;
    }
    button.className = wiki_autosave_on ? "autosave-on" : "autosave-off";
    var says = wiki_autosave_on ? tl['wiki_js_autosave'] :
        tl['wiki_js_autosave_off'];
    button.setAttribute("aria-label", says);
    button.setAttribute("title", says);
    var saying = elt("autosave-saying-" + wiki_autosave_field);
    if (saying) {
        saying.textContent = "";
    }
    /* Choosing a thumbnail by hand is a saving of its own, so its button
       belongs with the other by-hand controls: while the page keeps
       itself, a picture dropped on the thumbnail is kept with it. */
    var by_hand = [elt("edit-reason-container"), elt("save-container"),
        elt("wiki-save-button"), elt("wiki-save-as-button"),
        elt("page-icon")];
    for (var index = 0; index < by_hand.length; index++) {
        if (by_hand[index]) {
            by_hand[index].style.display = wiki_autosave_on ? "none" : "";
        }
    }
}
/*
 * The fields beside the writing area that belong to the page as much as
 * its words do: what it is called, who wrote it, what it is about, and
 * the picture that stands for it. A change to any of them is a change to
 * the page, so the page keeps itself after one just as it does after a
 * sentence.
 */
var WIKI_KEPT_FIELDS = ["page-title", "meta-author", "meta-description",
    "page-icon", "banner-caption", "banner-rights",
    "article-date-kind"];
/*
 * Watches the fields beside the writing area, so that renaming a page or
 * changing its picture is kept without a writer having to save by hand.
 *
 * @param String id identifier of the text area holding the page
 */
function watchKeptFields(id)
{
    for (var index = 0; index < WIKI_KEPT_FIELDS.length; index++) {
        var field = elt(WIKI_KEPT_FIELDS[index]);
        if (!field) {
            continue;
        }
        field.addEventListener("input", function ()
        {
            wikiChangeMade(id);
        });
        field.addEventListener("change", function ()
        {
            wikiChangeMade(id);
        });
    }
}
/*
 * Says a change has been made, which is what makes a save possible. The
 * wait starts over on every change, so the page is kept once the writer
 * has stopped rather than in the middle of a sentence.
 *
 * @param String id identifier of the text area holding the page
 */
function wikiChangeMade(id)
{
    if (!wiki_autosave_on) {
        return;
    }
    if (wiki_autosave_waiting !== null) {
        clearTimeout(wiki_autosave_waiting);
    }
    wiki_autosave_waiting = setTimeout(function ()
    {
        wiki_autosave_waiting = null;
        saveWikiPageQuietly(id);
    }, WIKI_AUTOSAVE_WAIT);
}
/*
 * Saves the page where the writer cannot see it happening, saying so on
 * the button while it goes. Nothing further is possible to save until
 * another change is made.
 *
 * @param String id identifier of the text area holding the page
 */
function saveWikiPageQuietly(id)
{
    var field = elt(id);
    var button = wikiAutoSaveButton(id);
    if (!field || !field.form) {
        return;
    }
    var began = Date.now();
    var saying = elt("autosave-saying-" + id);
    if (saying) {
        saying.textContent = tl['wiki_js_saving'];
    }
    if (button) {
        button.setAttribute("aria-label", tl['wiki_js_saving']);
    }
    var settled = function ()
    {
        var left = WIKI_AUTOSAVE_SHOWN - (Date.now() - began);
        setTimeout(showWikiSaveControls, (left > 0) ? left : 0);
    };
    noteWikiPlace();
    try {
        var sending = new FormData(field.form);
        /* A save made this way has no one to ask why, so it says so
           itself and the history reads as it should. */
        sending.set("edit_reason", tl['wiki_js_autosave_reason']);
        fetch(field.form.action || window.location.href, {method: "POST",
            body: sending, credentials: "same-origin"})
            .then(function (answer)
            {
                return answer.text();
            })
            .then(function (page)
            {
                takeFreshWikiToken(field.form, page);
                takeFreshWikiPageId(field, page);
                settled();
            })
            .catch(settled);
    } catch (problem) {
        settled();
    }
}
/**
 * Method to return standard buttons as an object based on the current
 * render engine (mediawiki, markdown, etc).
 *
 * @return Object containing button configurations with their markup syntax
 */
function getStandardButtonsObject()
{
    if (tl?.wiki_js_render_engine === "markdown") {
        return {
            'wikibtn-bold': ['**', '**'],
            'wikibtn-italic': ['*', '*'],
            'wikibtn-strike': ['~~', '~~'],
            'wikibtn-nowiki': ['```', '```'],
            'wikibtn-hyperlink': ['[', '](url)'],
            'wikibtn-bullets': ['* ' + tl['wiki_js_bullet'] + ' \n'],
            'wikibtn-numbers': ['1. ' + tl['wiki_js_enum'] + '. \n'],
            'wikibtn-hr': ['---\n'],
            'wikibtn-heading': '', // Handled separately
            'wikibtn-table': '',
            'wikibtn-slide': ['=' + tl['wiki_js_slide_sample_title'] + '=\n'
                + '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
                + '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
                + '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
                + '....' + '\n'],
            'wikibtn-definitionlist': ['**Term**: Definition\n'],
        };
    }
    return {
        'wikibtn-bold': ['---', '---'],
        'wikibtn-italic': ['--', '--'],
        'wikibtn-underline': ['<u>', '</u>'],
        'wikibtn-strike': ['<s>', '</s>'],
        'wikibtn-nowiki': ['<nowiki>', '</nowiki>'],
        'wikibtn-hyperlink': ['[[', ']]'],
        'wikibtn-slide': ['=' + tl['wiki_js_slide_sample_title'] + '=\n'
        + '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
        + '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
        + '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
        + '....' + '\n'],
        'wikibtn-bullets': ['* ' + tl['wiki_js_bullet'] + ' \n'],
        'wikibtn-numbers': ['# ' + tl['wiki_js_enum'] + ' \n'],
        'wikibtn-definitionlist': [
            '; ' + tl['wiki_js_definitionlist_item']
            + ' : ' + tl['wiki_js_definitionlist_definition']
            + '' + '\n'
            + '; ' + tl['wiki_js_definitionlist_item']
            + ' : ' + tl['wiki_js_definitionlist_definition']
            + '' + '\n'],
        'wikibtn-leftaligned': ['{{left|','}}'],
        'wikibtn-centeraligned': ['{{center|','}}'],
        'wikibtn-rightaligned': ['{{right|','}}'],
        'wikibtn-hr': ['---- \n']
    };
}
/**
 * Filters the buttons according to exclusion/inclusion rules.
 * If the 'data-buttons' attribute value starts with 'all' - Only the buttons
 * need to be excluded should be followed, with their name specified with a '!'
 * prefix.
 * Any buttons with no '!' prefix will eb ignored
 * example : all,!bol,!italic will render editor with all buttns excluding bold
 * and italic
 *
 * If the 'data-buttons' attribute value does not starts with 'all', any button
 * names followed will only be included in the editor.
 * Any buttons with '!' prefix will be ignored.
 * example : bol,italic will render editor only with bold and italic buttons.
 *
 * @param String id identifier of the textarea that we want editor buttons on
 */
function filterButtons(id)
{
    editor_all_buttons[id] = getStandardButtonsObject();
    editor_all_buttons[id]['wikibtn-heading'] = '';
    editor_all_buttons[id]['wikibtn-search'] = '';
    editor_all_buttons[id]['wikibtn-table'] = '';
    var wiki_text_div = elt(id);
    var wiki_buttons = wiki_text_div.getAttribute("data-buttons");
    if (wiki_buttons) {
        var wiki_buttons_array = wiki_buttons.split(',');
        var buttons_array_length = wiki_buttons_array.length;
        var included_buttons = new Array();
        var excluded_buttons = new Array();
        var exc = false;
        if (wiki_buttons_array[0].trim() === 'all') {
            exc = true;
        }
        if (wiki_buttons_array[0].trim() === 'none') {
            editor_buttons[id] = [];
            return;
        }
        for (var i = 0; i < buttons_array_length; i++) {
            wiki_buttons_array[i] = wiki_buttons_array[i].trim();
            var firstChar = wiki_buttons_array[i].charAt(0);
            if (wiki_buttons_array[i] && (exc === true) && firstChar === '!') {
                wiki_buttons_array[i] = wiki_buttons_array[i].substr(1);
                if (editor_all_buttons[id].hasOwnProperty(
                    wiki_buttons_array[i])) {
                    excluded_buttons.push(wiki_buttons_array[i]);
                    delete editor_all_buttons[id][
                        wiki_buttons_array[i]];
                }
            } else if (wiki_buttons_array[i] && exc === false) {
                if (editor_all_buttons[id].hasOwnProperty(
                    wiki_buttons_array[i])) {
                    included_buttons.push(wiki_buttons_array[i]);
                    editor_buttons[id][wiki_buttons_array[i]] =
                        editor_all_buttons[id][wiki_buttons_array[i]];
                }
            }
        }
    } else {
        editor_buttons[id] = editor_all_buttons[id];
    }
    if (Object.keys(editor_buttons[id]).length === 0) {
        editor_buttons[id] = editor_all_buttons[id];
    }
}
/**
 * This function returns the selected text from text_area.
 * The returned Object has properties for
 * selected text, entire prefix and entire suffix strings to the
 * selected text.
 *
 * @param String id the identifier for the textarea to get the selected text for
 * @return Object with properties of the selected text set
 */
function getSelection(id)
{
    /*
     Select the DOM element
     */
    var text_area = elt(id);
    /*
     IE?
     */
    if (document.selection) {
        var selection_bookmark =
            document.selection.createRange().getBookmark();
        var sel = text_area.createTextRange();
        sel.moveToBookmark(selection_bookmark);
        var sleft = text_area.createTextRange();
        sleft.collapse(true);
        sleft.setEndPoint("EndToStart", sel);
        text_area.selectionStart = sleft.text.length;
        text_area.selectionEnd = sleft.text.length + sel.text.length;
        text_area.selectedText = sel.text;
        var selected_text_prefix = text_area.value.substring(0,
            text_area.selectionStart);
        var selection = sel.text;
        var selected_text_suffix = text_area.value.substring(
            text_area.selectionEnd, text_area.textLength);
    } else if (typeof (text_area.selectionStart) !== "undefined") {
        /**
         Things are pretty straightforward in Mozilla based
         browsers & IE > 10.
         We can get the selectionStart & selectionEnd character
         position directly.
         Then compute the prefix and suffix using substr method.
         */
        var selected_text_prefix = text_area.value.substr(0,
            text_area.selectionStart);
        var selection = text_area.value.substr(
            text_area.selectionStart,
            text_area.selectionEnd - text_area.selectionStart);
        var selected_text_suffix = text_area.value.substr(
            text_area.selectionEnd);
    }
    var obj = {};
    obj.selection = selection;
    obj.selected_text_prefix = selected_text_prefix;
    obj.selected_text_suffix = selected_text_suffix;
    setCaretPosition(text_area, text_area.selectionEnd);
    return obj;
}

/**
 * This function can be used to set
 * the caret position in a text field object like a text area.
 *
 * @param element text_field in which the caret is to be set.
 * @param int pos position of the caret to be set.
 * @return undefined
 */
function setCaretPosition(text_field, pos)
{
    if (text_field.setSelectionRange) {
        text_field.focus();
        text_field.setSelectionRange(pos, pos);
    } else if (text_field.createTextRange) {
        var range = text_field.createTextRange();
        range.collapse(true);
        range.moveEnd('character', pos);
        range.moveStart('character', pos);
        range.select();
    }
}
/**
 * Looks up the name'd wiki formatting task, then uses it type to
 * render it in the textarea with identifier id
 *
 * @param String name identifier of the wiki task to be performed
 * @param String id identifier of the textarea to add wiki code for the given
 *     task
 */
function wikifySelection(name, id)
{
    var length = 0;
    length = editor_buttons[id][name].length;
    if (length === 2) {
        wikify(editor_buttons[id][name][0].replace(new RegExp('-', 'g'), "\'"),
           editor_buttons[id][name][1].replace(new RegExp('-', 'g'), "\'"),
           name, id);
    } else if (length === 1) {
        insertTextAtCursor(editor_buttons[id][name][0], id);
    }
}

/**
 * This creates a wiki string from wiki_prefix, wiki_suffix, task_name or
 * currently selected text and then replaces the currently selected text in the
 * DOM element id with this string. Currently, selected text is computed using
 * getSelection(id).
 *
 * @param String wiki_prefix prefix to be added to wikify the selected text.
 * @param String wiki_suffix suffix to be added to wikify the selected text.
 * @param String task_name name of the task to render default selection text.
 * @param String id of textarea to wikify
 */
function wikify(wiki_prefix, wiki_suffix, task_name, id)
{
    var br = '';
    /*
     Select the DOM element - wikiText
     */
    var text_area = elt(id);
    var obj = getSelection(id);
    var selection = obj.selection;
    var selected_text_prefix = obj.selected_text_prefix;
    var selected_text_suffix = obj.selected_text_suffix;
    if (!selection && wiki_prefix === '[[') {
        wiki_popup_prompt = elt('wiki-popup-prompt-'+id);
        if (wiki_popup_prompt.hasChildNodes()) {
            /*
             Remove childnodes, if any exist.
             */
            while (wiki_popup_prompt.hasChildNodes()) {
                wiki_popup_prompt.removeChild(wiki_popup_prompt.lastChild);
            }
        }
        wiki_popup_prompt.appendChild(createHyperlinkForm(id));
        editor_buffer = obj;
        toggleDisplay('wiki-popup-prompt-' + id);
    }
    if (!selection && wiki_prefix !== '[[') {
        br = '\n';
        selection = tl[task_name.replace('wikibtn-', 'wiki_js_')];
    }
    if (!selection && wiki_prefix !== '[[') {
        selection = task_name;
    }
    /*
     Now Add the wrap the selected text between the wiki stuff,
     and then wrap the selected
     text between actual selected text's prefix and suffix.
     Replace the text_area contents with the result.
     */
    if (selection) {
        var written = wiki_prefix + selection + wiki_suffix + br;
        writeIntoWikiSource(text_area, selected_text_prefix.length,
            text_area.value.length - selected_text_suffix.length, written);
    }
}
/*
 * Puts words into a page's source in place of the stretch between two
 * points, the way a person's own typing would, so what the browser
 * remembers of the writing is kept and the change can be taken back with
 * the undo key. Where the browser will not write for us, the words are
 * put in by hand and the source is told it has changed, since a browser
 * raises nothing when a script writes into a field.
 *
 * @param Object field the text area holding the page's source
 * @param int start where the stretch being replaced begins
 * @param int end where it ends
 * @param String words what to put there
 */
function writeIntoWikiSource(field, start, end, words)
{
    if (!field) {
        return;
    }
    field.focus();
    field.setSelectionRange(start, end);
    var written = false;
    try {
        written = document.execCommand("insertText", false, words);
    } catch (problem) {
        written = false;
    }
    if (!written) {
        var whole = field.value;
        field.value = whole.slice(0, start) + words + whole.slice(end);
        var after = start + words.length;
        field.setSelectionRange(after, after);
        wikiSourceChanged(field);
    }
}
/*
 * Says that the source of a page has been changed by the editor rather
 * than by someone typing. A browser raises nothing when a script writes
 * into a field, so whatever watches the source, the preview above all,
 * would go on showing what was there before the markup was put in.
 *
 * @param Object field the text area holding the page's source
 */
function wikiSourceChanged(field)
{
    if (!field) {
        return;
    }
    field.dispatchEvent(new Event("input", {bubbles: true}));
}
/**
 * This is a special function for processing user input
 * when the user clicks on hyperlink, and inserts a hyper link.
 *
 * @param String id of textarea the wiki editor instance is associated with
 * @return String
 */
function addWikiHyperlink(id)
{
    toggleDisplay('wiki-popup-prompt-' + id);
    var title = elt('wikify-link-text-' + id).value;
    var link = elt('wikify-link-url-' + id).value;
    if (!link && !title) {
        return false;
    }
    if (!link || !title) {
        out = "" + link + title;
    } else {
        out = link + "|"  + title;
    }
    insertTextAtCursor("[[" + out + "]]" + '\n', id);
}
/**
 * This function takes in number of rows, columns and
 * header_text(if desired) and constructs a table in wiki markup.
 *
 * @param int rows number of rows intended in the table.
 * @param int cols number of cols intended in the table.
 * @param string example_text placeholder text to be inserted in each
 * table field.
 * @param string header_text if header text is intended, placeholder
 * text for column header.
 * @return string
 */
function createWikiTable(rows, cols, example_text, header_text)
{
    var br = "\n";
    var table = "{|" + br;
    for (i = 0; i < rows; i++) {
        if (header_text && i === 0) {
            table = table + "|- " + br;
            table = table + "! ";
            for (j = 0; j < cols; j++) {
                table = table + header_text + " ";
                if (j < (cols - 1)) {
                    table = table + "!!" + " ";
                } else {
                    table = table + br;
                }
            }
        }
        table = table + "|- " + br;
        table = table + "| ";
        for (j = 0; j < cols; j++) {
            table = table + example_text + " ";
            if (j < (cols - 1)) {
                table = table + "||" + " ";
            } else {
                table = table + br;
            }
        }
    }
    table = table + "|}";
    return table;
}
/**
 * Inserts wiki search widget in textarea identified by id
 *
 * @param String id of textarea to insert wiki search widget
 */
/*
 * The markup this list offers, by the words a writer sees and the shape
 * that is put in for them. A shape carries an example of each part it
 * takes, so what goes in reads as something to edit rather than something
 * to look up. Only markup with no mark of its own in the row above is
 * here, since anything with one is a press away already.
 */
/*
 * What is listening for a press elsewhere or for the escape key while the
 * list of markup is open, kept so both can be taken off again when it
 * closes.
 */
var wiki_more_syntax_away = null;
var wiki_more_syntax_key = null;
var WIKI_MORE_SYNTAX = [
    ["wiki_js_syntax_textfield", "{{textfield|Label|field_name}}"],
    ["wiki_js_syntax_required_textfield",
        "{{r-textfield|Label|field_name}}"],
    ["wiki_js_syntax_textarea", "{{textarea|Label|field_name}}"],
    ["wiki_js_syntax_required_textarea",
        "{{r-textarea|Label|field_name}}"],
    ["wiki_js_syntax_checkbox", "{{checkbox|Label|field_name}}"],
    ["wiki_js_syntax_left_checkbox", "{{lcheckbox|Label|field_name}}"],
    ["wiki_js_syntax_radio", "{{radio|Label|field_name}}"],
    ["wiki_js_syntax_left_radio", "{{lradio|Label|field_name}}"],
    ["wiki_js_syntax_dropdown",
        "{{dropdown|field_name}}\nFirst\nSecond\n{{end-dropdown}}"],
    ["wiki_js_syntax_submit", "{{submit|Send|submit_button}}"],
    ["wiki_js_syntax_keyword_captcha",
        "{{keyword-captcha|Question to answer|expected answer}}"],
    ["wiki_js_syntax_require_signin",
        "{{require-signin|Shown to anyone not signed in}}"],
    ["wiki_js_syntax_username", "{{username|field_name}}"],
    ["wiki_js_syntax_date", "{{date|field_name}}"],
    ["wiki_js_syntax_timestamp", "{{timestamp|field_name}}"],
    ["wiki_js_syntax_presubmit",
        "{{presubmit|Shown before the form is sent}}"],
    ["wiki_js_syntax_same_unit",
        "{{same-unit Several lines held together as one thing. }}"],
    ["wiki_js_syntax_category", "{{category|Name}}"],
    ["wiki_js_syntax_category_list", "{{category-list|Name|classes}}"],
    ["wiki_js_syntax_class", "{{class=\"a-class\" Held text}}"],
    ["wiki_js_syntax_no_contents", "<notoc>\nHeld text\n</notoc>"],
    ["wiki_js_syntax_preformatted", "<pre>\nHeld text\n</pre>"],
    ["wiki_js_syntax_math", "<math>\n\\sum_{i=1}^{n} i\n</math>"],
    ["wiki_js_syntax_indent", ": Indented text"],
    ["wiki_js_syntax_recent_places", "[{recent_places}]"],
    ["wiki_js_syntax_resource", "((resource:my_photo.jpg|What it shows))"],
    ["wiki_js_syntax_resource_link",
        "((resource-link:my_document.pdf|What it is))"],
    ["wiki_js_syntax_resource_thumb",
        "((resource-thumb:my_photo.jpg|What it shows))"],
    ["wiki_js_syntax_resource_qr",
        "((resource-qr:https://example.com/|What it leads to))"],
    ["wiki_js_syntax_resource_bargraph",
        "((resource-bargraph:my_data.csv##B1#B4#C1#C4|What it shows))"],
    ["wiki_js_syntax_resource_data",
        "((resource-data:image/jpeg;base64,PUT THE DATA HERE|What it shows))"],
    ["wiki_js_syntax_text_field_placeholder",
        "{{text|field_name|What to type here}}", true],
    ["wiki_js_syntax_text_area_placeholder",
        "{{area|field_name|What to type here}}", true],
    ["wiki_js_syntax_option", "{{option|What it says|value_sent}}"],
    ["wiki_js_syntax_required_dropdown",
        "{{r-dropdown|field_name}}\n{{option|First|first}}\n" +
        "{{end-r-dropdown}}"],
    ["wiki_js_syntax_after_submit",
        "{{onsubmit|Shown once the form has been sent}}"],
    ["wiki_js_syntax_checkbox_short", "{{s-ck|Label|field_name}}"],
    ["wiki_js_syntax_required_checkbox_short",
        "{{r-ck|Label|field_name}}"],
    ["wiki_js_syntax_abbreviation", "{{a|short}}"],
    ["wiki_js_syntax_abbreviation_meaning",
        "{{a-def|short|what it stands for}}"],
    ["wiki_js_syntax_alias", "{{alias|short}}"],
    ["wiki_js_syntax_alias_meaning",
        "{{alias-def|short|what it stands for}}"],
    ["wiki_js_syntax_block", "{{block-class=\"my-class\" Held text}}"],
    ["wiki_js_syntax_inline_block",
        "{{inblock}}Held text{{end-inblock}}"],
    ["wiki_js_syntax_data_block", "{{data-block|block_name}}"],
    ["wiki_js_syntax_toggle",
        "{{toggle|id_of_thing_to_show|What the link says}}"],
    ["wiki_js_syntax_session_set", "{{session-set|field_name|value}}"],
    ["wiki_js_syntax_session_check",
        "{{session-check|field_name|one,two|Shown if in|Shown if not}}"],
    ["wiki_js_syntax_request_check",
        "{{request-check|field_name|one,two|Shown if in|Shown if not}}"],
    ["wiki_js_syntax_secret_ballot",
        "{{secret-ballot|first_witness|second_witness}}"],
    ["wiki_js_syntax_one_row_per_user", "{{user-record-form}}"],
    ["wiki_js_syntax_one_row_per_hash", "{{hash-record-form}}"],
    ["wiki_js_syntax_one_row_shared", "{{share-edited-form}}"]
];
/*
 * Opens the list of markup that has no mark of its own, and puts in
 * whichever shape is chosen. The list scrolls, since there is more of it
 * than fits above a page being written.
 *
 * @param String id identifier of the text area holding the page
 */
function addWikiMoreSyntax(id)
{
    var holder = elt('more-syntax-holder-' + id);
    if (!holder) {
        return;
    }
    var standing = elt('more-syntax-menu-' + id);
    if (standing) {
        closeWikiMoreSyntax(id);
        return;
    }
    var box = ce("div");
    box.className = "wiki-syntax-menu";
    box.id = 'more-syntax-menu-' + id;
    var listing = ce("ul");
    listing.className = "wiki-syntax-menu-list";
    /* The list is put in the order of the words a writer reads rather
       than the order it is written in here, and those words differ by
       language, so the sorting is done as the list is built. */
    /* Some markup only means anything on a template, so it is offered
       only where a writer is on one. A page is a template when its name
       says so. */
    var field = elt("wiki-page");
    var page_name = field ?
        (field.getAttribute("data-page-name") || "") : "";
    var on_a_template =
        page_name.toLowerCase().indexOf("template:") === 0;
    var offered = [];
    for (var index = 0; index < WIKI_MORE_SYNTAX.length; index++) {
        if (WIKI_MORE_SYNTAX[index][2] && !on_a_template) {
            continue;
        }
        offered.push({"says": tl[WIKI_MORE_SYNTAX[index][0]] ||
            WIKI_MORE_SYNTAX[index][0],
            "shape": WIKI_MORE_SYNTAX[index][1]});
    }
    /* Sorted in the language the page is written in, which the page
       declares on itself, rather than in whichever language the browser
       happens to be set to. */
    var reading_in = document.documentElement.lang || undefined;
    offered.sort(function (one, other) {
        return one.says.localeCompare(other.says, reading_in);
    });
    for (var place = 0; place < offered.length; place++) {
        var row = ce("li");
        var choice = ce("button");
        choice.type = "button";
        choice.className = "wiki-syntax-menu-choice";
        choice.textContent = offered[place].says;
        choice.setAttribute("data-shape", offered[place].shape);
        listen(choice, "click", function () {
            useWikiMoreSyntax(id, this.getAttribute("data-shape"));
        });
        row.appendChild(choice);
        listing.appendChild(row);
    }
    box.appendChild(listing);
    holder.appendChild(box);
    var mark = elt('more-syntax-mark-' + id);
    if (mark) {
        mark.setAttribute("aria-expanded", "true");
    }
    /* Pressing anywhere else puts the list away, which is what a reader
       expects of a list that drops from a mark, and the escape key does
       the same for anyone working from the keyboard. */
    wiki_more_syntax_away = function (event) {
        if (!holder.contains(event.target)) {
            closeWikiMoreSyntax(id);
        }
    };
    wiki_more_syntax_key = function (event) {
        if (event.key === "Escape") {
            closeWikiMoreSyntax(id);
            if (mark) {
                mark.focus();
            }
        }
    };
    document.addEventListener("mousedown", wiki_more_syntax_away);
    document.addEventListener("keydown", wiki_more_syntax_key);
    var first = box.querySelector(".wiki-syntax-menu-choice");
    if (first) {
        first.focus();
    }
}
/*
 * Puts the list away and stops it listening for a press elsewhere.
 *
 * @param String id identifier of the text area holding the page
 */
function closeWikiMoreSyntax(id)
{
    var box = elt('more-syntax-menu-' + id);
    if (box && box.parentNode) {
        box.parentNode.removeChild(box);
    }
    var mark = elt('more-syntax-mark-' + id);
    if (mark) {
        mark.setAttribute("aria-expanded", "false");
    }
    if (wiki_more_syntax_away) {
        document.removeEventListener("mousedown", wiki_more_syntax_away);
        wiki_more_syntax_away = null;
    }
    if (wiki_more_syntax_key) {
        document.removeEventListener("keydown", wiki_more_syntax_key);
        wiki_more_syntax_key = null;
    }
}
/*
 * Puts one shape of markup where the writer was, and closes the list.
 *
 * @param String id identifier of the text area holding the page
 * @param String shape the markup to put in
 */
function useWikiMoreSyntax(id, shape)
{
    var field = elt(id);
    if (!field) {
        return;
    }
    var start = field.selectionStart;
    var end = field.selectionEnd;
    closeWikiMoreSyntax(id);
    writeIntoWikiSource(field, start, end, shape);
}
function addWikiSearch(id)
{
    wiki_popup_prompt = elt('wiki-popup-prompt-'+id);
    if (wiki_popup_prompt.hasChildNodes()) {
        /*
         Remove child nodes, if any exist.
         */
        while (wiki_popup_prompt.hasChildNodes()) {
            wiki_popup_prompt.removeChild(wiki_popup_prompt.lastChild);
        }
    }
    wiki_popup_prompt.appendChild(createSearchWidgetForm(id));
    toggleDisplay('wiki-popup-prompt-'+id);
}

/**
 * Gets the size of the search widget to load.
 *
 * @param String id identifier of the textarea to put search form on
 */
function useInputForSearch(id)
{
    toggleDisplay('wiki-popup-prompt-'+id);
    size_elt = elt('wiki-search-size-'+id);
    var size = size_elt.options[size_elt.selectedIndex].value;
    insertTextAtCursor( "{{search:default|size:" + size+ "|placeholder:" +
        tl['wiki_js_placeholder'] + "}}\n", id);
}
/**
 * Util function to Stringify an JS Object
 *
 * @param Object js_object the javascript object to be converted to string.
 * @return String
 */
function objToString(js_object)
{
    var json_string = [];
    for (var property in js_object) {
        if (js_object.hasOwnProperty(property)) {
            json_string.push('"' + property + '"' + ':' + js_object[property] );
        }
    }
    json_string.push();
    return '{' + json_string.join(',') + '}';
}
/**
 * This is invoked by the editor to insert a wiki table.
 * This functions takes care okf the user input for rows/columns/etc
 * and leverages createWikiTable function to construct the table.
 *
 * @param String id
 */
function addWikiTable(id)
{
    wiki_popup_prompt = elt('wiki-popup-prompt-'+id);

    if (wiki_popup_prompt.hasChildNodes()) {
        /*
         Remove childnodes, if any exist.
         */
        while (wiki_popup_prompt.hasChildNodes()) {
            wiki_popup_prompt.removeChild(wiki_popup_prompt.lastChild);
        }
    }
    wiki_popup_prompt.appendChild(createTableForm(id));
    toggleDisplay('wiki-popup-prompt-'+id);
}
/**
 * Prompt/Draws a form to get input from a user, then uses that input to draw
 * a wiki table in the textarea with identifier id
 *
 * @param String id string identifier of the textarea to draw table in
 */
function useInputForTable(id)
{
    var rows = elt('wiki-rows-'+id).value;
    var cols = elt('wiki-cols-'+id).value;
    var head_checked = elt('wiki-insert-heading-'+id).checked;
    addWikiTableFromInput(rows, cols, head_checked, id);
}
/**
 * Using the input from the user, like number of rows,cols etc.
 * builds a table in wiki markup and inserts at cursor.
 * @param int rows number of rows desired
 * @param int cols number of cols desired
 * @param Boolean head_checked heading desired or not.
 */
function addWikiTableFromInput(rows, cols, head_checked, id)
{
    toggleDisplay('wiki-popup-prompt-' + id);
    if (!cols || !rows) {
        return;
    }
    if (head_checked) {
        var table = createWikiTable(rows, cols,
            tl['wiki_js_example'], tl['wiki_js_table_title']);
    } else {
        table = createWikiTable(rows, cols, tl['wiki_js_example']);
    }
    insertTextAtCursor(table + '\n', id);
}
/**
 * Accepts text as parameter to be inserted at caret's
 * current position.
 *
 * @param string text string that is intended to be placed at current cursor.
 */
function insertTextAtCursor(text, textarea_id)
{
    var field = elt(textarea_id);
    if (document.selection) {
        var range = document.selection.createRange();
        if (!range || range.parentElement() !== field) {
            field.focus();
            range = field.createTextRange();
            range.collapse(false);
        }
        range.text = text;
        range.collapse(false);
        range.select();
    } else {
        writeIntoWikiSource(field, field.selectionStart,
            field.selectionEnd, text);
    }
}

/**
 * Used to mark-up selected text in textarea given by id with a heading whose
 * size is decided by a select drop down
 *
 * @param String id of a text area that will mark
 */
function addWikiHeading(id)
{
    select_object = elt("wiki-heading-" + id);
    var heading_size = select_object.value;
    var markup_text;
    if (tl?.wiki_js_render_engine === "markdown") {
        markup_text = fillChars("#", heading_size) + " ";
        wikify(markup_text, "\n", "wikibtn-heading" + heading_size, id);
        select_object.selectedIndex = 0;
        return;
    }
    markup_text = fillChars("=", heading_size);
    select_object.selectedIndex = 0;
    wikify(markup_text, markup_text, "wikibtn-heading" + heading_size , id);
}

/**
 * Fills a character array and returns as a string consisting given count
 * of a given character.
 *
 * @param String c character to be filled into an array.
 * @param int n number of times the character to be repeated.
 * @return String consistring of n many c's
 */
function fillChars(c, n)
{
    for (var e = ""; e.length < n; ) {
        e += c;
    }
    return e;
}

/**
 * Created an input prompt HTML element with 2 text fields and a checkbox.
 * @param String id
 * @return HTMLFormElement
 */
function createTableForm(id)
{
    var wiki_prompt_for_table = ce("div");
    var table_form =
        '<div class="wiki-popup-content">' +
        '<h2 class="center">'+tl['wiki_js_add_wiki_table']+'</h2>' +
        '<form onsubmit="useInputForTable(\'' + id
            +'\');return false;"><table><tr>'+
        '<th class="float-opposite"><label for="wiki-rows-' + id + '" >' +
             tl['wiki_js_for_table_rows'] + '</label></th>' +
        '<td><input id="wiki-rows-' + id +
            '" name="wiki-prompt-rows" '+ 'type="text"></td></tr>'+
        '<tr><th class="float-opposite"><label for="wiki-cols-' + id + '">' +
            tl['wiki_js_for_table_cols'] + '</label></th>'+
        '<td><input id="wiki-cols-' + id +'" name="wiki-prompt-cols" '+
            'type="text"></td></tr>'+
        '<tr><th  class="float-opposite"><label for="wiki-insert-heading-' +
            id + '" >' + tl['wiki_js_prompt_heading'] + '</label></th>' +
        '<td><input id="wiki-insert-heading-' + id + '"' +
            'name="wiki-insert-heading" type="checkbox"></td></tr>' +
        '</table>' +
        '<div class="center"><button onmousedown="useInputForTable(\'' + id
            +'\')" name="submit" class="button-box">'+ tl['wiki_js_submit'] +
        '</button> &nbsp;' +
        '<button onmousedown="toggleDisplay(\'wiki-popup-prompt-' + id +
            '\')" name="close" class="button-box" >' + tl['wiki_js_cancel'] +
        '</button></form></div>';
    wiki_prompt_for_table.innerHTML = table_form;
    return wiki_prompt_for_table;
}

/**
 * Creates an HTMLFormElement with two text fields to get the URL and text of
 * a link
 * @param String id identifier of the text area that we want to add a wiki link
 *     to
 * @return HTMLFormElement containing form that we will use to get info from
 *     the user so that we can later add a wiki link
 */
function createHyperlinkForm(id)
{
    var wiki_prompt_for_hyperlink = ce("div");
    wiki_prompt_for_hyperlink.innerHTML =
        '<div class="wiki-popup-content">' +
        '<h2 class="center">'+tl['wiki_js_add_hyperlink']+'</h2>' +
        '<form onsubmit="addWikiHyperlink(\''+
            id +'\');return false;" '+
            '><table><tr><th><label for="wikify-link-text-'+id+'">' +
            tl['wiki_js_link_text'] + '</label></th>' +
        '<td><input id="wikify-link-text-'+id+'" '+
            'name="wikify-link-text" type="text"></td></tr>' +
        '<tr><th><label for="wiki-link-url-'+id+'">' +
            tl['wiki_js_link_url'] + '</label></th>' +
        '<td><input id="wikify-link-url-'+id+'" ' +
            'name="wikify-enter-link" type="text"></td></tr></table>' +
        '<div class="center"><button onmousedown="addWikiHyperlink(\''+
            id +'\')" ' + 'name="submit" class="button-box">'+
            tl['wiki_js_submit'] +'</button> &nbsp;' +
        '<button onmousedown="toggleDisplay(\'wiki-popup-prompt-' + id +
            '\')" name="close" class="button-box">' + tl['wiki_js_cancel'] +
        '</button></div></form></div>';
    return wiki_prompt_for_hyperlink;
}
/**
 * Creates an an HTMLFormElement with a form to get the kind of search bar
 * that is desired to be inserted into a wiki document
 * @param String id identifier of the textarea used to insert wiki text
 */
function createSearchWidgetForm(id)
{
    var search_elt = ce("div");
    search_form = '<div class="wiki-popup-content">'+
        '<h2 class="center">' + tl['wiki_js_add_search'] + '</h2>' +
        '<div class="center"><form onsubmit="useInputForSearch(\'' + id +
            '\');return false;">' +
        '<select name="wiki_search_size" id="wiki-search-size-' + id + '" >' +
        '<option disabled="" selected="" >' + tl['wiki_js_search_size'] +
        '</option>'+
        '<option value="small">' + tl['wiki_js_small'] + '</option>' +
        '<option value="medium">' + tl['wiki_js_medium'] + '</option>' +
        '<option value="large">' + tl['wiki_js_large'] + '</option>'+
        '</select></div>' +
        '<div class="center"><button onmousedown="useInputForSearch(\'' + id +
            '\');"'+ 'name="submit"  class="button-box">' +
            tl['wiki_js_submit'] + '</button> &nbsp;' +
        '<button onmousedown="toggleDisplay(\'wiki-popup-prompt-' + id +
        '\')" name="close"  class="button-box">' + tl['wiki_js_cancel'] +
        '</button></form></div></div>';
    search_elt.innerHTML = search_form;
    return search_elt;
}
/**
 * Sets up keyboard events for a wiki textarea after it is in focus
 * @param String id identifier of the textarea used to handle keyboard events
 *      for
 */
function enableKBShortcuts(id)
{
    var ctrl_down = false;
    /**
     * onkeyup handler: clears the Ctrl-down flag when the user
     * releases the Control key.
     *
     * @param {KeyboardEvent} e
     */
    document.onkeyup = function keyUp(e)
    {
        if (e.which == 17) ctrl_down = false;
    };
    /**
     * onkeydown handler: tracks Ctrl-down state and routes
     * Ctrl-B / Ctrl-I / Ctrl-U into the bold / italic /
     * underline wikify helpers.
     *
     * @param {KeyboardEvent} e
     */
    document.onkeydown = function keyDown(e)
    {
        if (e.which == 17) {
            ctrl_down = true;
        }
        if (e.which == 66 && ctrl_down == true) {
            wikifySelection('wikibtn-bold', id);
            return false;
        }
        if (e.which == 73 && ctrl_down == true) {
            wikifySelection('wikibtn-italic', id);
            return false;
        }
        if (e.which == 85 && ctrl_down == true) {
            wikifySelection('wikibtn-underline', id);
            return false;
        }
    };
}
/**
 * Used to add the wiki text for including a resource into a wiki page
 * @param String resource_name string name of resource to be added
 * @param String id identifier of the textarea used to insert wiki text
 * @param String sub_path path in resource folder to reasource
 */
function addToPage(resource_name, textarea_id, sub_path)
{
    /* The word before the file's name and the name itself ran
       together, giving Descriptionmyphoto.webp. */
    var described = tl['wiki_js_resource_description'] + " " +
        resource_name.replace('&quot;', '"');
    if (typeof sub_path === 'undefined' || sub_path == '') {
        wikify("((resource:", "|" + described + "))",
            resource_name.replace('&quot;', '"'), textarea_id);
    } else {
        wikify("((resource:", "|" + sub_path + "|" + described + "))",
            resource_name.replace('&quot;', '"'), textarea_id);
    }
}
/*
 * Set up event listeners to manage column splitter resizes when we have
 * tabular output such as in media lists
 */
 document.addEventListener('DOMContentLoaded', function () {
     const direction =
         document.getElementsByTagName('html')[0].getAttribute('dir');
     const direction_sign = (direction == 'rtl') ? -1 : 1;
     let split_adjusters = document.getElementsByClassName('split-adjuster');
     // Query the element
     for (const split_adjuster of split_adjusters) {
         let left_elt = split_adjuster.previousElementSibling;
         let right_elt = split_adjuster.nextElementSibling;
         // The current position of mouse
         let x = 0;
         let y = 0;
         let left_width = 0;
         /**
          * mousedown handler on a column split adjuster: records
          * the starting cursor position and the left column's
          * current width, then arms the mousemove / mouseup
          * handlers on the document.
          *
          * @param {MouseEvent} e
          */
         let mouseDownHandler = function (e) {
             x = e.clientX;
             y = e.clientY;
             left_width = left_elt.getBoundingClientRect().width;
             document.addEventListener('mousemove', mouseMoveHandler);
             document.addEventListener('mouseup', mouseUpHandler);
         };
         /**
          * mousemove handler bound during a split-adjuster drag:
          * resizes the left column's width based on cursor delta,
          * clamped to a minimum of 10%.
          *
          * @param {MouseEvent} e
          */
         let mouseMoveHandler = function (e) {
             const dx = direction_sign*(e.clientX - x);
             const dy = e.clientY - y;
             const new_left_width = Math.max(((left_width + dx) * 100) /
                 split_adjuster.parentNode.getBoundingClientRect().width, 10);
             left_elt.style.width = `${new_left_width}%`;
             split_adjuster.style.cursor = 'col-resize';
             document.body.style.cursor = 'col-resize';
             left_elt.style.userSelect = 'none';
             left_elt.style.pointerEvents = 'none';
             right_elt.style.userSelect = 'none';
             right_elt.style.pointerEvents = 'none';
         };
         /**
          * mouseup handler bound during a split-adjuster drag:
          * resets cursor styles, restores text selection, and
          * detaches itself plus the mousemove handler from the
          * document.
          */
         let mouseUpHandler = function () {
             split_adjuster.style.removeProperty('cursor');
             document.body.style.removeProperty('cursor');
             left_elt.style.removeProperty('user-select');
             left_elt.style.removeProperty('pointer-events');
             right_elt.style.removeProperty('user-select');
             right_elt.style.removeProperty('pointer-events');
             document.removeEventListener('mousemove', mouseMoveHandler);
             document.removeEventListener('mouseup', mouseUpHandler);
         };
         split_adjuster.addEventListener('mousedown', mouseDownHandler);
     }
 });
 /**
  * Toggles the visibility of a wiki-page AI summary box. The
  * first time it's shown, the summary is fetched asynchronously
  * via the api/summarize endpoint and inserted on completion.
  *
  * @param {string} csrf_token CSRF token for the API call
  * @param {string} base_url controller base URL ("?" suffix as
  *      appropriate)
  * @param {string|number} summary_id the wiki page / thread id
  *      whose summary is being toggled
  */
 function toggleAISummary(csrf_token, base_url, summary_id)
 {
    var summary_box = document.getElementById("ai-summary-" + summary_id);
    if (!summary_box) return;
    if (summary_box.style.display === "none" ||
        summary_box.style.display === "") {
        summary_box.style.display = "block";
        summary_box.innerHTML = "<p>" + tl['wiki_js_summarizing'] + "</p>";
        var url = base_url + "?thread_id=" + encodeURIComponent(summary_id) +
            "&c=api&a=summarize&YIOOP_TOKEN=" +
            encodeURIComponent(csrf_token);
        fetch(url, { method: "POST",
            headers: {
                "User-Agent": "Yioop-Web-Client/1.0",
                "Content-Type": "application/x-www-form-urlencoded"
            },
            credentials: "include"
        })
        .then(function(response) {
            if (!response.ok) {
                throw new Error("HTTP error! Status: " + response.status);
            }
            return response.json();
        })
        .then(function(data) {
            if (data.SUMMARY) {
                summary_box.innerHTML = "<p>" + data.SUMMARY + "</p>";
            } else if (data.ERRORS) {
                summary_box.innerHTML = "<p>" + data.ERRORS.join(", ") + "</p>";
            } else {
                summary_box.innerHTML = "<p>" +
                    tl['wiki_js_summary_unavailable'] + "</p>";
            }
        })
        .catch(function() {
            summary_box.innerHTML = "<p>" +
                tl['wiki_js_summary_unavailable'] + "</p>";
        });
    } else {
        summary_box.style.display = "none";
    }
  }
/*
 * The preview beside the editor: what opens and closes it, what keeps
 * the two panes in step, and what remembers whether a writer had it out.
 * The reading of a page into html lives with the parsers rather than
 * here.
 */
/*
 * How long typing must pause, in milliseconds, before the preview is
 * rendered again. Long enough that a run of keystrokes renders once,
 * short enough that a pause to read shows the result straight away.
 */
var WIKI_PREVIEW_DELAY = 400;
/*
 * The name under which having the preview out is remembered for as long
 * as the writer is working.
 */
var WIKI_PREVIEW_CHOICE = "wiki-preview-out";
/*
 * Whether the site serves pretty addresses. A file kept with a page is
 * asked for as a path where they are on and as a query where they are
 * off, and the two are not interchangeable.
 */
var wiki_pretty_addresses = false;
/*
 * Where the site is rooted, so a file's address points at the site's own
 * file route rather than under the page being edited.
 */
var wiki_base_address = "/";
/*
 * The mark under which a file kept with the page is asked for, which for
 * a group anyone may join is a row of zeros rather than the reader's own
 * mark.
 */
var wiki_resource_mark = "";
/*
 * The call that redraws the preview once it is set going, kept so that
 * opening the pane can redraw at once rather than waiting on a keystroke.
 */
var wiki_preview_render = null;
/*
 * How many letters a picked-out stretch must carry before the other pane
 * is asked to find it. A word or two of letters is enough to land in one
 * place; a handful would match all over the page.
 */
var WIKI_SELECTION_LEAST = 6;
/*
 * Keeps a pane showing what the wiki source in a textarea will look like,
 * rendering it in the browser with the parser ported above so that typing
 * costs no request and no work on the server. The render waits for a short
 * pause in typing rather than running on every keystroke, since parsing a
 * long page on each letter would be felt on the keyboard.
 *
 * While the preview is closed nothing is rendered at all, so a writer who
 * never opens it pays nothing for it. The two panes are kept at the same
 * place in the document as either is scrolled, so what is being edited is
 * what is being looked at.
 *
 * A page that names a resource is rendered without resolving it: the files
 * of a group live on the server and the browser cannot reach them, so such
 * a reference is left showing its markup and comes out right once the page
 * is saved.
 *
 * @param String source_id id of the textarea holding the wiki source
 * @param String target_id id of the element the rendering is placed into
 * @param String group_id id of the group the page belongs to
 * @param String page_id id of the page, used for resource addresses
 * @param String controller_name the controller a page link should target
 * @param String csrf_token_key the query variable name for the csrf token
 * @param String csrf_token_value the csrf token value
 */
function initWikiPreview(source_id, target_id, group_id, page_id,
    controller_name, csrf_token_key, csrf_token_value)
{
    var source = elt(source_id);
    var target = elt(target_id);
    var pair = elt("wiki-edit-pair");
    if (!source || !target || typeof parseWikiContent !== "function") {
        return;
    }
    /* The page's own group and id are carried on the field, since the
       page being edited knows them and whoever starts the preview may
       not. A resource is drawn by asking the site for it, and the ask
       has to say which page's files it wants. */
    if (source.dataset) {
        if (source.dataset.groupId) {
            group_id = source.dataset.groupId;
        }
        if (source.dataset.pageId) {
            page_id = source.dataset.pageId;
        }
    }
    var waiting = null;
    var render = function ()
    {
        waiting = null;
        if (pair && pair.classList.contains("wiki-preview-closed")) {
            return;
        }
        wiki_preview_marks_lines = true;
        /* Read afresh each time rather than once at the start: a page
           written for the first time has no id until it is saved, and
           the field is told the id as soon as the site gives one. */
        if (source.dataset) {
            if (source.dataset.groupId) {
                group_id = source.dataset.groupId;
            }
            if (source.dataset.pageId) {
                page_id = source.dataset.pageId;
            }
            wiki_pretty_addresses = (source.dataset.prettyAddresses === "1");
            if (source.dataset.baseAddress) {
                wiki_base_address = source.dataset.baseAddress;
            }
            /* A file kept with the page is asked for under a mark of its
               own, which is not always the reader's own mark. */
            if (source.dataset.resourceMark !== undefined) {
                wiki_resource_mark = source.dataset.resourceMark;
            }
        }
        if (typeof parseMarkdownContent === "function" &&
            tl['wiki_js_render_engine'] === "markdown") {
            /* A page written in markdown is read by the reader written
               for it, so such a page shows a preview of its own rather
               than the words the writer typed. */
            target.innerHTML = parseMarkdownContent(source.value,
                group_id, page_id, csrf_token_key, wiki_resource_mark,
                wiki_pretty_addresses, wiki_base_address);
        } else {
            /* Whether the page draws a table of contents is a setting,
               and the settings are not in the words being typed, so it is
               read from the editing field where the page wrote it. */
            var draws_toc = source.getAttribute("data-draw-toc") != "0";
            target.innerHTML = fillWikiSearchBoxes(parseWikiContent(
                source.value, group_id, page_id, controller_name,
                csrf_token_key, wiki_resource_mark,
                wiki_pretty_addresses, wiki_base_address, draws_toc));
        }
        wiki_preview_marks_lines = false;
        /* A list written into a page takes its choices from a block of
           kept words elsewhere on the page, which a page fills in as it
           opens. The preview draws afresh each time, so it fills them
           again over what it has just drawn. */
        if (typeof fillCsvBlockUsers === "function") {
            fillCsvBlockUsers(target);
        }
        fillFrontPagePlaces(target);
        /* Whatever goes wrong drawing a chart, the words of the page are
           already on screen and picking text out of them must keep
           working. */
        try {
            drawWikiPreviewExtras(target);
            setWikiPreviewMath(target);
        } catch (problem) {
            wiki_pane_moved = null;
        }
    };
    var schedule = function ()
    {
        if (waiting !== null) {
            clearTimeout(waiting);
        }
        waiting = setTimeout(render, WIKI_PREVIEW_DELAY);
    };
    listen(source, "input", schedule);
    /* Scrolling one pane does not move the other. It used to put each at
       the same fraction of its own scrollable length as the other, which
       lines the two up at the top and the bottom of a page and nowhere
       between: a line of markup and the block it draws to are different
       heights, so half way down the source is not half way down the
       rendering. The preview follows the writer instead, at the line the
       caret is on, which initWikiSelectionSync below does.
     */
    initWikiSelectionSync(source, target);
    wiki_preview_render = render;
    /* The writer had the preview out when last they looked at a page, so
       it comes back out. A save reloads the page, and without this a
       writer who was reading the preview lost it at every save. */
    if (rememberedWikiPreview() === "open" && pair &&
        pair.classList.contains("wiki-preview-closed")) {
        toggleWikiPreview();
    }
    render();
}
/*
 * Opens or closes the preview pane beside the wiki editor. Opening it
 * renders what is written so far, since nothing is rendered while it is
 * closed.
 */
function toggleWikiPreview()
{
    var pair = elt("wiki-edit-pair");
    if (!pair) {
        return;
    }
    /* The editor's own row of controls is put in just before the textarea
       by wiki.js, which leaves it inside the source column and starts the
       writing area lower than the preview beside it. Moving it above the
       two columns puts both at the same place on the screen. It is done
       here rather than at page load because the controls are built by a
       call the controller adds to the page, which may not have run yet
       when the page finishes loading. */
    var moved = ["wiki-popup-prompt", "wiki-buttons"];
    for (var which = 0; which < moved.length; which++) {
        var row = pair.querySelector("." + moved[which]);
        if (row && pair.parentNode) {
            pair.parentNode.insertBefore(row, pair);
        }
    }
    pair.classList.toggle("wiki-preview-closed");
    var closed = pair.classList.contains("wiki-preview-closed");
    /* The book says what pressing it will do: shut while the preview is
       out of sight, open while it is being read. */
    var book = elt("preview-book");
    if (book) {
        book.innerHTML = closed ? "&#128213;" : "&#128214;";
    }
    rememberWikiPreview(closed ? "closed" : "open");
    if (!closed && wiki_preview_render !== null) {
        wiki_preview_render();
    }
}
/*
 * Remembers whether the writer had the preview out, for as long as they
 * are working, so a page that comes back after a save comes back as they
 * left it. A browser that will not remember is no reason to refuse the
 * choice, so nothing is made of being turned away.
 *
 * @param String choice "open" or "closed"
 */
function rememberWikiPreview(choice)
{
    try {
        window.sessionStorage.setItem(WIKI_PREVIEW_CHOICE, choice);
    } catch (problem) {
        return;
    }
}
/*
 * Gives back whether the writer had the preview out when last they
 * looked at a page.
 *
 * @return String "open" when they had it out, otherwise an empty answer
 */
function rememberedWikiPreview()
{
    try {
        return window.sessionStorage.getItem(WIKI_PREVIEW_CHOICE) || "";
    } catch (problem) {
        return "";
    }
}
/*
 * Reduces a piece of text to its letters and digits, lower-cased, and
 * remembers where each of those came from. Markup and spacing differ
 * between what is written and what is drawn from it, so the two are
 * compared on their letters alone and the answer is carried back through
 * the places recorded here.
 *
 * @param String text the text to reduce
 * @return Object the reduced text under "letters" and, under "places", the
 *      position in the original of each letter kept
 */
function wikiLettersOnly(text)
{
    let letters = "";
    let places = [];
    for (let index = 0; index < text.length; index++) {
        let one = text.charAt(index).toLowerCase();
        if ((one >= "a" && one <= "z") || (one >= "0" && one <= "9")) {
            letters += one;
            places.push(index);
        }
    }
    return {letters: letters, places: places};
}
/*
 * Gathers the pieces of text inside an element in the order they are read,
 * so a stretch of the whole can be found and then traced back to the piece
 * it falls in.
 *
 * @param Object root the element to gather from
 * @return Array each piece as its node, the text of it, and where that
 *      text begins within the whole
 */
function wikiTextPieces(root)
{
    let pieces = [];
    let along = 0;
    let walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    let node = walker.nextNode();
    while (node) {
        pieces.push({node: node, text: node.nodeValue, begins: along});
        along += node.nodeValue.length;
        node = walker.nextNode();
    }
    return pieces;
}
/*
 * Takes away any marks a previous match left in the preview, putting the
 * pieces of text it split back together.
 *
 * @param Object target the element the preview is drawn into
 */
function clearWikiPreviewMarks(target)
{
    let marks = target.querySelectorAll("." + WIKI_PREVIEW_MARK_CLASS);
    for (let index = 0; index < marks.length; index++) {
        let mark = marks[index];
        while (mark.firstChild) {
            mark.parentNode.insertBefore(mark.firstChild, mark);
        }
        mark.parentNode.removeChild(mark);
    }
    target.normalize();
}
/*
 * Marks the stretch of the preview between two places in its text, and
 * brings the first of it into view.
 *
 * @param Object target the element the preview is drawn into
 * @param int from where the stretch begins in the preview's whole text
 * @param int to where it ends
 */
function markWikiPreview(target, from, to)
{
    clearWikiPreviewMarks(target);
    let pieces = wikiTextPieces(target);
    let first = null;
    for (let index = 0; index < pieces.length; index++) {
        let piece = pieces[index];
        let ends = piece.begins + piece.text.length;
        if (ends <= from || piece.begins >= to) {
            continue;
        }
        let starts_at = Math.max(0, from - piece.begins);
        let stops_at = Math.min(piece.text.length, to - piece.begins);
        let range = document.createRange();
        range.setStart(piece.node, starts_at);
        range.setEnd(piece.node, stops_at);
        let mark = ce("span");
        mark.className = WIKI_PREVIEW_MARK_CLASS;
        range.surroundContents(mark);
        if (first === null) {
            first = mark;
        }
    }
    if (first !== null) {
        first.scrollIntoView({block: "nearest"});
    }
}
/*
 * Lets a stretch of text picked out in one pane show up in the other. What
 * is written and what is drawn from it do not read the same, since the
 * markup is in one and not the other, so the two are matched on their
 * letters and digits alone and the answer is carried back to where those
 * letters sit in each.
 *
 * Picking something out in the preview selects the same words in the
 * source, ready to be edited; picking something out in the source marks
 * the same words in the preview, which leaves the writing where it is.
 *
 * @param Object source the textarea holding the wiki source
 * @param Object target the element the preview is drawn into
 */
function initWikiSelectionSync(source, target)
{
    let syncing = false;
    /* From the source to the preview: the letters of what is picked out
       are looked for in the letters of the preview, and the stretch they
       fall in is marked. */
    let fromSource = function ()
    {
        /* The pane being worked in never moves. Whatever else may scroll
           it while this runs, focusing or the browser bringing a caret
           into view, it is put back exactly where it was. */
        let stay = source.scrollTop;
        if (syncing) {
            return;
        }
        if (source.selectionStart == source.selectionEnd) {
            /* Nothing is picked out, so the caret's own line is what the
               preview is brought to. */
            syncing = true;
            wikiShowSourceLine(source, target);
            syncing = false;
            return;
        }
        syncing = true;
        let picked = wikiLettersOnly(source.value.substring(
            source.selectionStart, source.selectionEnd));
        let drawn = wikiLettersOnly(target.textContent);
        /* The search begins at the block the picked-out line came from,
           so words that also appear earlier, in a table of contents most
           of all, are not what gets marked. */
        let line = source.value.substring(0,
            source.selectionStart).split("\n").length;
        let from_letter = wikiPreviewLineLetter(target, drawn, line);
        let at = (picked.letters.length < WIKI_SELECTION_LEAST) ? -1 :
            drawn.letters.indexOf(picked.letters, from_letter);
        if (at < 0 && from_letter > 0) {
            at = drawn.letters.indexOf(picked.letters);
        }
        if (at >= 0) {
            markWikiPreview(target, drawn.places[at],
                drawn.places[at + picked.letters.length - 1] + 1);
            /* Where the answering text sits comes from the line stamps
               the parser left, which say outright which block came from
               which line, rather than from measuring the mark. */
            let line = source.value.substring(0,
                source.selectionStart).split("\n").length;
            /* The marked words themselves say where the answering text
               is, which is what has to come into view. The line stamps
               answer only when nothing was marked. */
            let mark = target.querySelector("." + WIKI_PREVIEW_MARK_CLASS);
            let answering = -1;
            if (mark) {
                answering = mark.getBoundingClientRect().top -
                    target.getBoundingClientRect().top + target.scrollTop;
            } else {
                answering = wikiPreviewLineTop(target, line);
            }
            if (answering >= 0) {
                matchWikiPaneFraction(source,
                    wikiTextAreaOffset(source, source.selectionStart),
                    target, answering);
            }
            if (source.scrollTop !== stay) {
                moveWikiPane(source, stay);
            }
        } else {
            clearWikiPreviewMarks(target);
        }
        syncing = false;
    };
    /* From the preview to the source: the letters of what is picked out
       are looked for in the letters of the source, and the words they came
       from are selected there. */
    let fromPreview = function ()
    {
        let stay = target.scrollTop;
        if (syncing) {
            return;
        }
        /* wiki.js declares a getSelection of its own at the top level,
           which stands in front of the window's, so the document's is
           asked for the reader's selection instead. */
        let choice = document.getSelection();
        if (!choice || choice.rangeCount == 0 || choice.isCollapsed) {
            return;
        }
        if (!target.contains(choice.getRangeAt(0).commonAncestorContainer)) {
            return;
        }
        syncing = true;
        /* Where the picked-out words begin, counted in letters from the
           start of the preview. Reading it before anything is marked keeps
           it exact, and lets the same words be marked again afterwards:
           putting the caret in the source takes the browser's own
           highlight away, and without this an older mark elsewhere would
           be the only thing still showing. */
        let reach = document.createRange();
        reach.setStart(target, 0);
        reach.setEnd(choice.getRangeAt(0).startContainer,
            choice.getRangeAt(0).startOffset);
        let ahead = wikiLettersOnly(reach.toString()).letters.length;
        let picked = wikiLettersOnly(choice.toString());
        let written = wikiLettersOnly(source.value);
        /* The stamps say which stretch of the source the picked-out
           words can have come from, so they are looked for within that
           stretch. Looking through the whole source found whichever copy
           came first, which for words that repeat is the wrong one. */
        let span = wikiPreviewLineSpan(target, choice.getRangeAt(0));
        let lines = source.value.split("\n");
        let from_letter = 0;
        let to_letter = written.letters.length;
        if (span.first > 0) {
            from_letter = wikiLettersOnly(lines.slice(0,
                span.first - 1).join("\n")).letters.length;
        }
        if (span.last > 0) {
            to_letter = wikiLettersOnly(lines.slice(0,
                span.last - 1).join("\n")).letters.length;
        }
        let at = -1;
        if (picked.letters.length >= WIKI_SELECTION_LEAST) {
            at = written.letters.indexOf(picked.letters, from_letter);
            /* A match that begins past the stretch belongs to another
               block, so it is not this selection. */
            if (at >= 0 && to_letter > from_letter &&
                at >= to_letter) {
                at = -1;
            }
            /* Where the stamps say nothing useful, the old way of
               looking through the whole source still answers. */
            if (at < 0 && from_letter > 0) {
                at = written.letters.indexOf(picked.letters);
            }
        }
        if (at >= 0) {
            let from = written.places[at];
            let to = written.places[at + picked.letters.length - 1] + 1;
            let mark = choice.getRangeAt(0).getBoundingClientRect();
            let seen = target.getBoundingClientRect();
            let where = mark.top - seen.top + target.scrollTop;
            source.focus();
            source.setSelectionRange(from, to);
            let drawn = wikiLettersOnly(target.textContent);
            if (ahead + picked.letters.length <= drawn.places.length) {
                markWikiPreview(target, drawn.places[ahead],
                    drawn.places[ahead + picked.letters.length - 1] + 1);
            }
            /* The line the picked-out text came from is read off the
               stamp before it, and the source is put at that line. */
            let line = wikiPreviewLineAt(target, where);
            /* The stamp says which line of the source the words came
               from; where that line sits on screen is measured the same
               way, so a wrapped paragraph counts for all the height it
               actually takes. */
            let answering = wikiTextAreaOffset(source, from);
            if (line > 0) {
                let upto = source.value.split("\n").slice(0,
                    line - 1).join("\n").length;
                answering = wikiTextAreaOffset(source, upto);
            }
            matchWikiPaneFraction(target, where, source, answering);
            if (target.scrollTop !== stay) {
                moveWikiPane(target, stay);
            }
        }
        syncing = false;
    };
    listen(source, "mouseup", fromSource);
    listen(source, "keyup", fromSource);
    /* Typing moves the caret too, and a writer watching the preview
       wants what they are writing in view. */
    listen(source, "input", fromSource);
    listen(source, "click", fromSource);
    /* From the preview back to the editor, only a stretch picked out
       with the mouse says anything about where the writer is looking. */
    listen(target, "mouseup", fromPreview);
}
/*
 * Which pane was last moved to answer the other, and to what. A pane moved
 * this way reports a scroll of its own; recognizing that report by the
 * position it carries is what keeps the two from chasing each other, and
 * it needs no waiting on a clock to do it.
 */
/*
 * The hidden element a textarea's text is laid out in again, to find where
 * a place in it sits on screen.
 */
var WIKI_MIRROR_ID = "wiki-text-mirror";
/*
 * How much of a pane's height is kept clear at the top and bottom when
 * bringing answering text into view, so it is never flush against an edge.
 */
var WIKI_PANE_EDGE = 0.15;
var wiki_pane_moved = null;
/*
 * Where that pane was put.
 */
var wiki_pane_moved_to = -1;
/*
 * Moves a pane, remembering where it was put so the scroll this causes can
 * be told from one the reader made.
 *
 * @param Object pane the pane to move
 * @param int wanted where to put it, before its own limits are applied
 */
function moveWikiPane(pane, wanted)
{
    let furthest = pane.scrollHeight - pane.clientHeight;
    let put = Math.max(0, Math.min(furthest, wanted));
    if (Math.abs(pane.scrollTop - put) < 1) {
        return;
    }
    wiki_pane_moved = pane;
    wiki_pane_moved_to = put;
    pane.scrollTop = put;
}
/*
 * Whether a pane's scroll is the one its own move just caused rather than
 * one the reader made. Asking clears the record, so the next scroll of
 * that pane is read as the reader's.
 *
 * @param Object pane the pane that reported a scroll
 * @return bool whether this scroll was one we caused
 */
function wikiPaneEcho(pane)
{
    if (pane !== wiki_pane_moved) {
        return false;
    }
    if (Math.abs(pane.scrollTop - wiki_pane_moved_to) > 1) {
        return false;
    }
    wiki_pane_moved = null;
    return true;
}
/*
 * Puts the preview at the line of the source the caret sits on. Each
 * line of the preview carries a stamp whose id is that line number, so
 * the line is looked up rather than searched for. A line that carries
 * no stamp of its own, such as one inside a table, is answered by the
 * nearest line above it that does.
 *
 * @param Object source the textarea being written in
 * @param Object target the element the preview is drawn into
 */
function wikiShowSourceLine(source, target)
{
    let line = source.value.slice(0, source.selectionStart).split(
        "\n").length;
    let found = null;
    while (line > 0 && !found) {
        found = elt("wiki-line-" + line);
        line--;
    }
    if (!found) {
        return;
    }
    moveWikiPane(target, found.getBoundingClientRect().top -
        target.getBoundingClientRect().top + target.scrollTop -
        target.clientHeight / 2);
}
/*
 * Which letter of the preview the block that began on a given line of the
 * source starts at, counting only the letters and digits the two panes are
 * matched on. Searching from there keeps words that also appear earlier,
 * in a table of contents above all, from being the ones marked.
 *
 * @param Object target the element the preview is drawn into
 * @param Object drawn the preview's letters and where each came from
 * @param int line the line of the source being looked for
 * @return int the letter to search from, zero when it cannot be told
 */
function wikiPreviewLineLetter(target, drawn, line)
{
    let stamps = target.querySelectorAll(".wiki-line");
    let found = null;
    for (let index = 0; index < stamps.length; index++) {
        if (parseInt(attr(stamps[index], "data-line"), 10) <= line) {
            found = stamps[index];
        }
    }
    if (found === null) {
        return 0;
    }
    /* How much text stands before the block, in the whole of what the
       preview reads as, turned into a count of letters alone. */
    let range = document.createRange();
    range.setStart(target, 0);
    range.setEndBefore(found);
    let before = wikiLettersOnly(range.toString()).letters.length;
    return Math.min(before, drawn.letters.length);
}
/*
 * Where in the preview the block that began on a given line of the source
 * was drawn, measured within everything the preview can scroll through.
 * The block a line falls in is the last one stamped at or before it.
 *
 * @param Object target the element the preview is drawn into
 * @param int line the line of the source being looked for
 * @return int how far down the preview that block sits, or -1 when the
 *      preview holds no stamps
 */
function wikiPreviewLineTop(target, line)
{
    let stamps = target.querySelectorAll(".wiki-line");
    if (stamps.length == 0) {
        return -1;
    }
    let found = null;
    for (let index = 0; index < stamps.length; index++) {
        if (parseInt(attr(stamps[index], "data-line"), 10) <= line) {
            found = stamps[index];
        }
    }
    if (found === null) {
        found = stamps[0];
    }
    let seen = target.getBoundingClientRect();
    return found.getBoundingClientRect().top - seen.top + target.scrollTop;
}
/*
 * The stretch of the source a picked-out piece of the preview can have
 * come from, as a first and last line. The parser stamps each block with
 * the line it began on, so the block the selection starts in is the last
 * one stamped at or before it, and the stretch ends where the next
 * stamped block begins. Searching only within those lines is what keeps
 * words that appear more than once from matching the wrong one.
 *
 * @param Object target the element the preview is drawn into
 * @param Object range the reader's picked-out piece of the preview
 * @return Object first and last line of the source it can have come
 *      from, both -1 when the preview holds no stamps
 */
function wikiPreviewLineSpan(target, range)
{
    let stamps = target.querySelectorAll(".wiki-line");
    if (stamps.length == 0) {
        return {first: -1, last: -1};
    }
    let begins = null;
    let ends = null;
    for (let index = 0; index < stamps.length; index++) {
        let where = range.comparePoint(stamps[index], 0);
        /* A stamp before the selection begins is a candidate for where
           it started; the first one after the selection ends says where
           the stretch stops. */
        if (where <= 0) {
            begins = stamps[index];
        } else if (ends === null) {
            ends = stamps[index];
            break;
        }
    }
    if (begins === null) {
        begins = stamps[0];
    }
    let first = parseInt(attr(begins, "data-line"), 10);
    /* Where the next block begins is one line past what this stretch can
       hold; with no next block the stretch runs to the end. */
    let last = (ends === null) ? -1 :
        parseInt(attr(ends, "data-line"), 10);
    return {first: first, last: last};
}
/*
 * Which line of the source the block holding a place in the preview began
 * on, by reading the stamp the parser left before it.
 *
 * @param Object target the element the preview is drawn into
 * @param int at how far down the preview the place is, measured within
 *      everything it can scroll through
 * @return int the line, or -1 when the preview holds no stamps
 */
function wikiPreviewLineAt(target, at)
{
    let stamps = target.querySelectorAll(".wiki-line");
    if (stamps.length == 0) {
        return -1;
    }
    let seen = target.getBoundingClientRect();
    let line = parseInt(attr(stamps[0], "data-line"), 10);
    for (let index = 0; index < stamps.length; index++) {
        let top = stamps[index].getBoundingClientRect().top - seen.top +
            target.scrollTop;
        if (top > at) {
            break;
        }
        line = parseInt(attr(stamps[index], "data-line"), 10);
    }
    return line;
}
/*
 * Puts the marked stretch of one pane at the same place down its visible
 * height as the picked-out text sits down the other, so what is being
 * looked at in one is at eye level in the other. The pane the text was
 * picked out in is never moved.
 *
 * @param Object leader the pane the text was picked out in
 * @param int leader_top where the picked-out text begins within the
 *      leader's whole scrollable height
 * @param Object follower the pane to move
 * @param int follower_top where the answering text begins within the
 *      follower's whole scrollable height
 */
function matchWikiPaneFraction(leader, leader_top, follower, follower_top)
{
    let visible = leader.clientHeight;
    if (visible < 1) {
        return;
    }
    let along = (leader_top - leader.scrollTop) / visible;
    /* Held away from either edge. The words being answered have to be
       read, and at the very top or bottom they sit half out of the pane;
       when the picked-out text is off screen in the pane being worked in
       there is no fraction to match anyway, so this is where it lands. */
    if (along < WIKI_PANE_EDGE) {
        along = WIKI_PANE_EDGE;
    }
    if (along > 1 - WIKI_PANE_EDGE) {
        along = 1 - WIKI_PANE_EDGE;
    }
    moveWikiPane(follower, follower_top - along * follower.clientHeight);
}
/*
 * Where a place in a textarea's text sits down its whole scrollable
 * height, in pixels. Counting newlines will not do: a paragraph too long
 * for the width is one line of text but several lines on screen, and the
 * two answers drift further apart the more of them a page has.
 *
 * The text up to that place is laid out again in a hidden element wearing
 * the textarea's own font, width and padding, and the height it reaches is
 * where that place sits.
 *
 * @param Object area the textarea to look in
 * @param int at the position in its text
 * @return int how far down its scrollable height that position is
 */
function wikiTextAreaOffset(area, at)
{
    let mirror = elt(WIKI_MIRROR_ID);
    if (!mirror) {
        mirror = ce("div");
        mirror.id = WIKI_MIRROR_ID;
        document.body.appendChild(mirror);
    }
    let worn = getComputedStyle(area);
    let copied = ["fontFamily", "fontSize", "fontWeight", "fontStyle",
        "letterSpacing", "lineHeight", "textTransform", "wordSpacing",
        "paddingTop", "paddingBottom", "paddingLeft", "paddingRight",
        "borderTopWidth", "borderBottomWidth", "borderLeftWidth",
        "borderRightWidth", "boxSizing"];
    for (let index = 0; index < copied.length; index++) {
        mirror.style[copied[index]] = worn[copied[index]];
    }
    mirror.style.position = "absolute";
    mirror.style.visibility = "hidden";
    mirror.style.top = "0";
    mirror.style.left = "-9999px";
    mirror.style.whiteSpace = "pre-wrap";
    mirror.style.overflowWrap = "break-word";
    mirror.style.width = area.clientWidth + "px";
    mirror.textContent = area.value.substring(0, at);
    let here = ce("span");
    here.textContent = "\u200b";
    mirror.appendChild(here);
    return here.offsetTop;
}

/**
 * toggleSpreadsheetView swaps a table of figures between its rows and
 * bars showing how those figures fall, and changes the mark on the
 * control that did the swapping so it shows what pressing it again would
 * give. The controls above a resource being edited call this where that
 * resource holds figures separated by commas.
 */
function toggleSpreadsheetView()
{
    spreadsheet.toggleHistograms({has_column_headers: true,
        ignore_columns: ["user_captcha_text"]});
    let control = elt('histogram-toggle');
    if (!control) {
        return;
    }
    let mark = control.querySelector('[aria-label]');
    if (!mark) {
        return;
    }
    /* No data-showing attribute means the page is on the rows, so the
       first press turns the bars on. */
    let was_showing_bars = attr(control, 'data-showing') === 'bars';
    let now_showing_bars = !was_showing_bars;
    control.setAttribute('data-showing',
        now_showing_bars ? 'bars' : 'rows');
    let offers = now_showing_bars ? 'rows' : 'bars';
    mark.textContent = window.spreadsheet_marks[offers];
    mark.setAttribute('aria-label', window.spreadsheet_says[offers]);
    control.setAttribute('title', window.spreadsheet_says[offers]);
}
