MediaWiki:Gadget-AcceleratedFormCreation.js: Difference between revisions

From Linguifex
Jump to navigation Jump to search
No edit summary
m 1 revision imported
 
(2 intermediate revisions by 2 users not shown)
Line 1: Line 1:
// <nowiki>
// <nowiki>
/* globals mw, $ */
// jshint maxerr:500
// FIXME: This gadget relies to an excessive degree on OrangeLinks to function. These gadgets should be made independent.
// FIXME: This gadget relies to an excessive degree on OrangeLinks to function. These gadgets should be made independent.


Line 12: Line 15:


mw.loader.using(["mediawiki.util"]).done(function() {
mw.loader.using(["mediawiki.util"]).done(function() {
    var pageName = mw.config.get("wgPageName");
var pageName = mw.config.get("wgPageName");
// Don't do anything unless the current page is in the main namespace.
// Set window.accelEverywhere = true to test this gadget elsewhere.
if (window.accelEverywhere || (mw.config.get("wgAction") === "view" && (mw.config.get("wgNamespaceNumber") === 0 || pageName == "Wiktionary:Sandbox"))) {
// Stores all accelerated data, by language, by target pagename.
// Sub-arrays are in HTML order.
var accelParamsByPagename = {};
var getTargetPagename = function(link) {
var targetPagename = mw.util.getParamValue("title", link.href);
if (targetPagename === null) {
var match = link.href.match(/^(.*)\/wiki\/([^#]+)(?:#.+)?$/);
if (match) {
targetPagename = decodeURIComponent(match[2]);
}
}
return targetPagename;
};
var getLang = function (element) {
if (element.accelLang !== undefined) {
return element.accelLang;
}
var hasLang = $(element).closest("[lang]")[0];
var lang = hasLang ? hasLang.getAttribute("lang") : null;
element.accelLang = lang;
return lang;
};
var getPartOfSpeech = function(link) {
// Acceleration can be added to inflection tables too.
// This tells the search script to skip headers with these names.
var skipheaders = [
"alternative forms",
"antonyms",
"conjugation",
"declension",
"derived terms",
"inflection",
"mutation",
"related terms",
"synonyms",
"translations",
"usage notes"
];
for (var node = link; node !== null; node = node.previousSibling || node.parentNode) {
if (node.nodeType == 1 && (node.nodeName.match(/^H[3-6]$/) || (node.nodeName === 'DIV' && node.className.indexOf('mw-heading') !== -1))) {
var header = $(node).find(".mw-headline, h3, h4, h5, h6");
if (!header) {
continue;
}
header = header.text().replace(/^[1-9.]* /, "").toLowerCase();
if (skipheaders.indexOf(header) == -1) {
return header;
}
}
}
throw new Error("This entry seems to be formatted incorrectly. Does it have a language and part-of-speech header?");
};
var createAccelParam = function(link) {
var classNames = Array.prototype.filter.call($(link).closest(".form-of")[0].classList,
function (className) {
return (/^(gender|origin|origin_transliteration|pos|target|transliteration)-.+|.+-form-of$/.test(className));
});
var accelParam = classNames.join(" ");
var targetPagename = getTargetPagename(link);
var targetHead = (link.innerText || link.textContent).replace(/ /g, "_");
if (targetPagename != targetHead) {
accelParam = "target-" + targetHead + " " + accelParam;
}
return "pos-" + getPartOfSpeech(link).replace(/ /g, "_") + " " + accelParam;
};
var storeAccelParam = function(link) {
// Extract the targeted pagename from the URL,
// and language code from the nearest element with a lang attribute
var lang = getLang(link);
var targetPagename = getTargetPagename(link);
// Add page name to the list
if (accelParamsByPagename[lang] === undefined) {
accelParamsByPagename[lang] = {};
}
if (accelParamsByPagename[lang][targetPagename] === undefined) {
accelParamsByPagename[lang][targetPagename] = [];
}
var accelParam = createAccelParam(link);
if (accelParamsByPagename[lang][targetPagename].indexOf(accelParam) === -1) {
accelParamsByPagename[lang][targetPagename].push(accelParam);
}
};
var processLink = function(link) {
// Extract the targeted pagename from the URL,
// and language code from the nearest element with a lang attribute
var lang = getLang(link);
var targetPagename = getTargetPagename(link);
// Fetch the acceleration parameters from the store
var accelParam = accelParamsByPagename[lang][targetPagename]
.map(function (accel, i) {
return "accel" + (i + 1).toString() + "=" + encodeURIComponent(accel);
})
.join("&");
// Convert a non-edit link into an edit link
if (link.href.indexOf("action=edit") < 0) {
link.href = link.href.replace(/^(.*)\/wiki\/([^#]+)(?:#.+)?$/, "$1/w/index.php?title=$2&action=edit");
}
// use URL to ensure we append to the query string, not e.g. the hash.
var targetUrl = new URL(link.href);
// Now build a new "green link" URL to replace the original red link with
targetUrl.search +=
"&editintro=MediaWiki:Gadget-AcceleratedFormCreation.js/intro" +
"&accel_lang=" + encodeURIComponent(lang) +
"&accel_lemma=" + encodeURIComponent(pageName.replace(/_/g, " ")) +
"&" + accelParam +
"&veswitched=1";
link.href = targetUrl.href;
link.classList.add("accelerated");
link.processedLink = true;
};
// Mutation observer to respond when OrangeLinks modifies links
var mutobs = new MutationObserver(function(mutations, observer) {
mutations.forEach(function(mutation) {
var link = mutation.target;
if (!(mutation.attributeName == "class" && link.tagName === "A")) {
return;
}
// Don't process a link we've already been to
if (link.processedLink) {
return;
}
if (!$(link).hasClass("orange-link")) {
return;
}
// Process
processLink(link);
});
});
// First generate and store all the parameters
var oldtable = null;  // Were we previously inside a table?
var columns = [];
$(".form-of a").each(function() {
// Are we currently inside a table?
var $this = $(this);
var table = $this.closest("table");
if (table.length > 0) {
table = table[0];
} else {
table = null;
}
// Was a column number specified on the current table cell?
// jQuery.fn.data automatically converts an integer-like string
// to a number.
var col = $this.closest("td[data-accel-col]").first().data("accel-col");
if (typeof col !== "number") {
col = null;
}
// If we were in a table, and we changed to another table or are no longer in one,
// or if there is no column number attribute, flush the column lists.
if (oldtable && (oldtable !== table || col === null)) {
for (var i = 0; i < columns.length; ++i) {
for (var j = 0; j < columns[i].length; ++j) {
storeAccelParam(columns[i][j]);
}
}
columns = [];
}
oldtable = table;
// The nostore parameter causes the link to not be stored,
// but it is processed later. The effect is that this link has no
// effect on the ordering of forms.
if ($(this).closest(".form-of").first().hasClass("form-of-nostore")) {
return;
}
// If there is a column number attribute, defer storing the link,
// put it in the columns array instead.
if (col !== null) {
--col;  // Column attributes are 1-based, JS arrays are 0-based
// Expand the columns list to fit the number of columns
while (columns.length <= col) {
columns.push([]);
}
// Save the link in the columns list
columns[col].push(this);
} else {
// Store the link directly
storeAccelParam(this);
}
});
// Flush column lists
for (var i = 0; i < columns.length; ++i) {
for (var j = 0; j < columns[i].length; ++j) {
storeAccelParam(columns[i][j]);
}
}
// Then add them onto the links, or add a mutation observer
$(".form-of a").each(function() {
var $this = $(this);
if ($this.hasClass("new") || $this.hasClass("orange-link")) {
processLink(this);
} else {
// FIXME: There's a small window for a race condition here.
// If the "orange-link" class is added by OrangeLinks after the above if-statement is evaluated,
// but before the observer is added, then the link won't be processed.
mutobs.observe(this, {attributes : true});
}
});
// A function that force-processes all links and adds a class to those that would not have otherwise been processed.
var forceProcessAllLinks = function() {
$(".form-of a").each(function() {
var $this = $(this);
if (!($this.hasClass("new") || $this.hasClass("orange-link"))) {
this.classList.add("accelerated-forced");
}
processLink(this);
});
};
window.accelForceProcessAllLinks = forceProcessAllLinks;
} else if (mw.config.get("wgAction") === "edit") {
// Get the parameters from the URL
var getAccelParams = function() {
var accelParams = [];
var i = 1;
while (true) {
var acceldata = mw.util.getParamValue("accel" + i.toString());
if (!acceldata) {
break;
}
// Default values
var params = {
pos: null,
form: null,
gender: null,
transliteration: null,
origin: mw.util.getParamValue("accel_lemma"),
origin_transliteration: null,
target: pageName,
};
// Go over each part and add it
var parts = acceldata.split(" ");
for (var j = 0; j < parts.length; ++j) {
var part = parts[j];
var paramMatch = part.match(/^(gender|origin|origin_transliteration|pos|target|transliteration)-(.+)$/);
if (paramMatch) {
params[paramMatch[1]] = paramMatch[2].replace(/_/g, " ").replace(/\uFFF0/g, "_");
} else {
var formMatch = part.match(/^(.+)-form-of$/);
if (formMatch) {
params.form = formMatch[1].replace(/_/g, " ").replace(/\uFFF0/g, "_");
}
}
}
accelParams.push(params);
++i;
}
return accelParams;
};
// Generates entries from the information
var printArgs = function(accelParams) {
var args = [
"lang=" + mw.util.getParamValue("accel_lang"),
"origin_pagename=" + mw.util.getParamValue("accel_lemma"),
"target_pagename=" + pageName,
"num=" + accelParams.length,
];
for (var i = 0; i < accelParams.length; ++i) {
for (var key in accelParams[i]) {
if (accelParams[i][key] !== null) {
args.push(key + (i + 1) + "=" + accelParams[i][key].replace(/\|/g, "&#124;"));
}
}
}
return args.join("|");
};
var showModuleError = function(errorText) {
// Attempt to link to the line of the module in which the error occurred.
errorText = errorText.replace(
/(Module:[^#<>\[\]|{}_]+)(?: at line |:)(\d+)/,
function (wholeMatch, moduleName, lineNumber) {
var link = document.createElement('a');
link.href = mw.util.getUrl(moduleName, {action: "edit"}) + "#mw-ce-l" + lineNumber;
link.innerHTML = moduleName + " at line " + lineNumber;
return "Lua error in " + link.outerHTML;
});
var errorBox =
"<div id=\"accel-error\">" +
"<p><big>An error occurred while generating the entry:</big></p>" +
"<p>" + errorText + "</p>" +
"</div>";
wikipreview.insertAdjacentHTML("beforebegin", errorBox);
};
var receiveModuleResponse = function(response) {
var newtext, result;
try {
result = JSON.parse(response.expandtemplates.wikitext);
} catch (err) { // JSON parse error should not happen.
mw.notify(err.msg);
return;
}
if (result.error) { // module error
showModuleError(result.error);
} else { // successfully generated entries
newtext = result.entries;
}
for (i = 0; i < result.messages.length; ++i) {
mw.notify(result.messages[i]);
}
if (!newtext) {
return;
}
var newValue;


    // Don't do anything unless the current page is in the Contionary namespace.
var langsection_regex = /^==([^=\n]+)==$/mg;
    // Set window.accelEverywhere = true to test this gadget elsewhere.
var match = langsection_regex.exec(newtext);
    if (window.accelEverywhere || (mw.config.get("wgAction") === "view" && (mw.config.get("wgNamespaceNumber") === 120))) {
        var accelParamsByPagename = {};
if (!match) {
 
showModuleError("No language section was found in the returned text.");
        var getTargetPagename = function(link) {
}
            var targetPagename = mw.util.getParamValue("title", link.href);
            if (targetPagename === null) {
var langname = match[1];
                var match = link.href.match(/^(.*)\/wiki\/([^#]+)(?:#.+)?$/);
                if (match) {
// Does the page already exist?
                    targetPagename = decodeURIComponent(match[2]);
if (textbox.value) {
                }
var resultInBox = false;
            }
            return targetPagename;
// Reset position at which regex starts its search.
        };
// Otherwise, regex starts matching after the index where it
 
// found the language header in newtext.
        var getLang = function(element) {
langsection_regex.lastIndex = 0;
            if (element.accelLang !== undefined) {
                return element.accelLang;
// Go over language sections to find where to insert our new one
            }
while ((match = langsection_regex.exec(textbox.value)) !== null) {
            var closest = $(element).closest("[lang]");
if (match[1] == langname) {
            var lang = closest.length ? closest[0].getAttribute("lang") : null;
// There already exists a section for our language, display text in a separate box.
            element.accelLang = lang;
resultInBox = true;
            return lang;
break;
        };
} else if (match[1] == "Translingual" || match[1] == "English" || (langname != "English" && match[1] < langname)) {
 
// Skip past English and Translingual, or if the language sorts higher
        var getPartOfSpeech = function(link) {
continue;
            var skipheaders = [
} else {
                "alternative forms",
// We found the first match that sorts lower than our language, great.
                "antonyms",
break;
                "conjugation",
}
                "declension",
}
                "derived terms",
                "inflection",
var scrollIndex;
                "mutation",
newValue = textbox.value;
                "related terms",
                "synonyms",
if (resultInBox) {
                "translations",
// Display the result in a separate box.
                "usage notes"
var insertTextBoxIn = document.getElementById("accel-form-conflict-textbox-here");
            ];
if (insertTextBoxIn) {
 
var newElement = document.createElement("div");
            for (var node = link; node !== null; node = node.previousSibling || node.parentNode) {
newElement.id = insertTextBoxIn.id;
                if (node.nodeType == 1 && (node.nodeName.match(/^H[3-6]$/) || (node.nodeName === 'DIV' && node.className.indexOf('mw-heading') !== -1))) {
var warning = document.createElement("p");
                    var header = $(node).find(".mw-headline, h3, h4, h5, h6");
warning.textContent = "A section for this language already exists. Please combine the new text manually:";
                    if (!header.length) {
var textBox = document.createElement("textarea");
                        continue;
textBox.setAttribute("readonly", true);
                    }
textBox.setAttribute("rows", 10);
                    header = header.text().replace(/^[1-9.]* /, "").toLowerCase();
textBox.textContent = newtext;
                    if (skipheaders.indexOf(header) == -1) {
newElement.appendChild(warning);
                        return header;
newElement.appendChild(textBox);
                    }
insertTextBoxIn.replaceWith(newElement);
                }
}
            }
            throw new Error("This entry seems to be formatted incorrectly. Does it have a language and part-of-speech header?");
scrollIndex = match !== null ? match.index : newValue.length;
        };
} else if (match === null) {
 
// We found no language that our section should go before, so insert it at the end.
        var createAccelParam = function(link) {
newValue = newValue.trimEnd() + "\n\n";
            var classList = $(link).closest(".form-of")[0].classList;
scrollIndex = newValue.length;
            var classNames = [];
newValue = newValue + newtext;
            for (var i = 0; i < classList.length; i++) {
} else {
                if (/^(gender|origin|origin_transliteration|pos|target|transliteration)-.+|.+-form-of$/.test(classList[i])) {
// We found a language to insert before, so do that.
                    classNames.push(classList[i]);
newValue = newValue.substring(0, match.index) + newtext + "\n\n" + newValue.substring(match.index);
                }
scrollIndex = match.index;
            }
}
// Scroll the textbox to the newly added section. First scroll all the way down,
// then set the cursor to the start of the new section, which scrolls back up
// to the new section's language header.
textbox.scrollTop = textbox.scrollHeight;
textbox.selectionStart = scrollIndex;
textbox.selectionEnd = scrollIndex;
summary.value = "Adding forms of " + langname + " [[" + lemma + "]] ([[WT:ACCEL|Accelerated]])";
} else {
newValue = newtext;
summary.value = "Creating forms of " + langname + " [[" + lemma + "]] ([[WT:ACCEL|Accelerated]])";
}
// Set textbox text. Setting textbox.value is unreliable.
$(textbox).val(newValue);
};
var wikipreview = document.getElementById("wikiPreview");
var textbox = document.getElementById("wpTextbox1");
var summary = document.getElementById("wpSummary");
var lang = mw.util.getParamValue("accel_lang");
var lemma = mw.util.getParamValue("accel_lemma");
if (!(wikipreview && textbox && summary && lang && lemma)) {
return;
}
// Gather all the information that was given in the URL
var accelParams = getAccelParams();
if (!accelParams) {
return;
}
var module = "accel", funcName = "generate_JSON";
mw.loader.using("mediawiki.api", function() {
new mw.Api().get({
"action": "expandtemplates",
"format": "json",
"text": "{{#invoke:" + module + "|" + funcName + "|" + printArgs(accelParams) + "}}",
"prop": "wikitext"
}).done(receiveModuleResponse);
});
}
});


            var accelParam = classNames.join(" ");
            var targetPagename = getTargetPagename(link);
            var targetHead = (link.innerText || link.textContent).replace(/ /g, "_");
            if (targetPagename != targetHead) {
                accelParam = "target-" + targetHead + " " + accelParam;
            }
            return "pos-" + getPartOfSpeech(link).replace(/ /g, "_") + " " + accelParam;
        };
        var storeAccelParam = function(link) {
            var lang = getLang(link);
            var targetPagename = getTargetPagename(link);
            if (accelParamsByPagename[lang] === undefined) {
                accelParamsByPagename[lang] = {};
            }
            if (accelParamsByPagename[lang][targetPagename] === undefined) {
                accelParamsByPagename[lang][targetPagename] = [];
            }
            var accelParam = createAccelParam(link);
            if (accelParamsByPagename[lang][targetPagename].indexOf(accelParam) === -1) {
                accelParamsByPagename[lang][targetPagename].push(accelParam);
            }
        };
        var processLink = function(link) {
            var lang = getLang(link);
            var targetPagename = getTargetPagename(link);
            var accelParamArr = accelParamsByPagename[lang][targetPagename];
            var accelParam = "";
            for (var i = 0; i < accelParamArr.length; i++) {
                if (i > 0) accelParam += "&";
                accelParam += "accel" + (i+1) + "=" + encodeURIComponent(accelParamArr[i]);
            }
            if (link.href.indexOf("action=edit") < 0) {
                link.href = link.href.replace(/^(.*)\/wiki\/([^#]+)(?:#.+)?$/, "$1/w/index.php?title=$2&action=edit");
            }
            link.href += "&editintro=MediaWiki:Gadget-AcceleratedFormCreation.js/intro" +
                        "&accel_lang=" + encodeURIComponent(lang) +
                        "&accel_lemma=" + encodeURIComponent(pageName.replace(/_/g, " ")) +
                        "&" + accelParam +
                        "&veswitched=1";
            link.classList.add("accelerated");
            link.processedLink = true;
        };
        var mutobs = new MutationObserver(function(mutations) {
            for (var m = 0; m < mutations.length; m++) {
                var mutation = mutations[m];
                var link = mutation.target;
                if (!(mutation.attributeName == "class" && link.tagName === "A")) {
                    continue;
                }
                if (link.processedLink) continue;
                if (!$(link).hasClass("orange-link")) continue;
                processLink(link);
            }
        });
        var oldtable = null;
        var columns = [];
        $(".form-of a").each(function() {
            var $this = $(this);
            var table = $this.closest("table");
            table = table.length ? table[0] : null;
            var col = $this.closest("td[data-accel-col]").first().data("accel-col");
            if (typeof col !== "number") col = null;
            if (oldtable && (oldtable !== table || col === null)) {
                for (var i = 0; i < columns.length; i++) {
                    for (var j = 0; j < columns[i].length; j++) {
                        storeAccelParam(columns[i][j]);
                    }
                }
                columns = [];
            }
            oldtable = table;
            if ($this.closest(".form-of").first().hasClass("form-of-nostore")) return;
            if (col !== null) {
                col--;
                while (columns.length <= col) columns.push([]);
                columns[col].push(this);
            } else {
                storeAccelParam(this);
            }
        });
        for (var i = 0; i < columns.length; i++) {
            for (var j = 0; j < columns[i].length; j++) {
                storeAccelParam(columns[i][j]);
            }
        }
        $(".form-of a").each(function() {
            var $this = $(this);
            if ($this.hasClass("new") || $this.hasClass("orange-link")) {
                processLink(this);
            } else {
                mutobs.observe(this, {attributes: true});
            }
        });
        window.accelForceProcessAllLinks = function() {
            $(".form-of a").each(function() {
                var $this = $(this);
                if (!($this.hasClass("new") || $this.hasClass("orange-link"))) {
                    this.classList.add("accelerated-forced");
                }
                processLink(this);
            });
        };
    } else if (mw.config.get("wgAction") === "edit") {
        var getAccelParams = function() {
            var accelParams = [];
            var i = 1;
            while (true) {
                var acceldata = mw.util.getParamValue("accel" + i);
                if (!acceldata) break;
                var params = {
                    pos: null,
                    form: null,
                    gender: null,
                    transliteration: null,
                    origin: mw.util.getParamValue("accel_lemma"),
                    origin_transliteration: null,
                    target: pageName
                };
                var parts = acceldata.split(" ");
                for (var j = 0; j < parts.length; j++) {
                    var part = parts[j];
                    var paramMatch = part.match(/^(gender|origin|origin_transliteration|pos|target|transliteration)-(.+)$/);
                    if (paramMatch) {
                        params[paramMatch[1]] = paramMatch[2].replace(/_/g, " ").replace(/\uFFF0/g, "_");
                    } else {
                        var formMatch = part.match(/^(.+)-form-of$/);
                        if (formMatch) {
                            params.form = formMatch[1].replace(/_/g, " ").replace(/\uFFF0/g, "_");
                        }
                    }
                }
                accelParams.push(params);
                i++;
            }
            return accelParams;
        };
        var printArgs = function(accelParams) {
            var args = [
                "lang=" + mw.util.getParamValue("accel_lang"),
                "origin_pagename=" + mw.util.getParamValue("accel_lemma"),
                "target_pagename=" + pageName,
                "num=" + accelParams.length
            ];
            for (var i = 0; i < accelParams.length; i++) {
                for (var key in accelParams[i]) {
                    if (accelParams[i].hasOwnProperty(key) && accelParams[i][key] !== null) {
                        args.push(key + (i + 1) + "=" + accelParams[i][key].replace(/\|/g, "&#124;"));
                    }
                }
            }
            return args.join("|");
        };
        var showModuleError = function(errorText) {
            errorText = errorText.replace(
                /(Module:[^#<>\[\]|{}_]+)(?: at line |:)(\d+)/,
                function (wholeMatch, moduleName, lineNumber) {
                    var link = document.createElement('a');
                    link.href = mw.util.getUrl(moduleName, {action: "edit"}) + "#mw-ce-l" + lineNumber;
                    link.innerHTML = moduleName + " at line " + lineNumber;
                    return "Lua error in " + link.outerHTML;
                }
            );
            var errorBox = "<div id=\"accel-error\">" +
                          "<p><big>An error occurred while generating the entry:</big></p>" +
                          "<p>" + errorText + "</p>" +
                          "</div>";
            wikipreview.insertAdjacentHTML("beforebegin", errorBox);
        };
        var receiveModuleResponse = function(response) {
            var newtext, result;
            try {
                result = JSON.parse(response.expandtemplates.wikitext);
            } catch (err) {
                mw.notify(err.msg);
                return;
            }
            if (result.error) {
                showModuleError(result.error);
            } else {
                newtext = result.entries;
            }
            for (var i = 0; i < result.messages.length; i++) {
                mw.notify(result.messages[i]);
            }
            if (!newtext) return;
            var newValue;
            var langsection_regex = /^==([^=\n]+)==$/mg;
            var match = langsection_regex.exec(newtext);
            if (!match) {
                showModuleError("No language section was found in the returned text.");
            }
            var langname = match[1];
            if (textbox.value) {
                var resultInBox = false;
                langsection_regex.lastIndex = 0;
                while ((match = langsection_regex.exec(textbox.value)) !== null) {
                    if (match[1] == langname) {
                        resultInBox = true;
                        break;
                    } else if (match[1] == "Translingual" || match[1] == "English" || (langname != "English" && match[1] < langname)) {
                        continue;
                    } else {
                        break;
                    }
                }
                var scrollIndex;
                newValue = textbox.value;
                if (resultInBox) {
                    var insertTextBoxIn = document.getElementById("accel-form-conflict-textbox-here");
                    if (insertTextBoxIn) {
                        var newElement = document.createElement("div");
                        newElement.id = insertTextBoxIn.id;
                        var warning = document.createElement("p");
                        warning.textContent = "A section for this language already exists. Please combine the new text manually:";
                        var textBox = document.createElement("textarea");
                        textBox.setAttribute("readonly", true);
                        textBox.setAttribute("rows", 10);
                        textBox.textContent = newtext;
                        newElement.appendChild(warning);
                        newElement.appendChild(textBox);
                        insertTextBoxIn.parentNode.replaceChild(newElement, insertTextBoxIn);
                    }
                    scrollIndex = match !== null ? match.index : newValue.length;
                } else if (match === null) {
                    newValue = newValue.trimEnd() + "\n\n" + newtext;
                    scrollIndex = newValue.length;
                } else {
                    newValue = newValue.substring(0, match.index) + newtext + "\n\n" + newValue.substring(match.index);
                    scrollIndex = match.index;
                }
                textbox.scrollTop = textbox.scrollHeight;
                textbox.selectionStart = scrollIndex;
                textbox.selectionEnd = scrollIndex;
                summary.value = "Adding forms of " + langname + " [[" + lemma + "]] ([[WT:ACCEL|Accelerated]])";
            } else {
                newValue = newtext;
                summary.value = "Creating forms of " + langname + " [[" + lemma + "]] ([[WT:ACCEL|Accelerated]])";
            }
            $(textbox).val(newValue);
        };
        var wikipreview = document.getElementById("wikiPreview");
        var textbox = document.getElementById("wpTextbox1");
        var summary = document.getElementById("wpSummary");
        var lang = mw.util.getParamValue("accel_lang");
        var lemma = mw.util.getParamValue("accel_lemma");
        if (!(wikipreview && textbox && summary && lang && lemma)) return;
        var accelParams = getAccelParams();
        if (!accelParams) return;
        var module = "accel", funcName = "generate_JSON";
        mw.loader.using("mediawiki.api", function() {
            new mw.Api().get({
                "action": "expandtemplates",
                "format": "json",
                "text": "{{#invoke:" + module + "|" + funcName + "|" + printArgs(accelParams) + "}}",
                "prop": "wikitext"
            }).done(receiveModuleResponse);
        });
    }
});
// </nowiki>
// </nowiki>

Latest revision as of 17:52, 4 November 2025

// <nowiki>
/* globals mw, $ */
// jshint maxerr:500

// FIXME: This gadget relies to an excessive degree on OrangeLinks to function. These gadgets should be made independent.

/*
 * The starting point of the whole script.
 * 
 * This adds a hook to the page load event so that the script runs
 * adds the generated text to the edit window once the page is done loading.
 */

"use strict";

mw.loader.using(["mediawiki.util"]).done(function() {
	var pageName = mw.config.get("wgPageName");
	
	// Don't do anything unless the current page is in the main namespace.
	// Set window.accelEverywhere = true to test this gadget elsewhere.
	if (window.accelEverywhere || (mw.config.get("wgAction") === "view" && (mw.config.get("wgNamespaceNumber") === 0 || pageName == "Wiktionary:Sandbox"))) {
		// Stores all accelerated data, by language, by target pagename.
		// Sub-arrays are in HTML order.
		var accelParamsByPagename = {};
		
		var getTargetPagename = function(link) {
			var targetPagename = mw.util.getParamValue("title", link.href);
			
			if (targetPagename === null) {
				var match = link.href.match(/^(.*)\/wiki\/([^#]+)(?:#.+)?$/);
				
				if (match) {
					targetPagename = decodeURIComponent(match[2]);
				}
			}
			
			return targetPagename;
		};
		
		var getLang = function (element) {
			if (element.accelLang !== undefined) {
				return element.accelLang;
			}
			var hasLang = $(element).closest("[lang]")[0];
			var lang = hasLang ? hasLang.getAttribute("lang") : null;
			element.accelLang = lang;
			return lang;
		};
		
		var getPartOfSpeech = function(link) {
			// Acceleration can be added to inflection tables too.
			// This tells the search script to skip headers with these names.
			var skipheaders = [
				"alternative forms",
				"antonyms",
				"conjugation",
				"declension",
				"derived terms", 
				"inflection",
				"mutation",
				"related terms",
				"synonyms",
				"translations",
				"usage notes"
			];
			
			for (var node = link; node !== null; node = node.previousSibling || node.parentNode) {
				if (node.nodeType == 1 && (node.nodeName.match(/^H[3-6]$/) || (node.nodeName === 'DIV' && node.className.indexOf('mw-heading') !== -1))) {
					var header = $(node).find(".mw-headline, h3, h4, h5, h6");
					if (!header) {
						continue;
					}
					header = header.text().replace(/^[1-9.]* /, "").toLowerCase();
					
					if (skipheaders.indexOf(header) == -1) {
						return header;
					}
				}
			}
			
			throw new Error("This entry seems to be formatted incorrectly. Does it have a language and part-of-speech header?");
		};
		
		var createAccelParam = function(link) {
			var classNames = Array.prototype.filter.call($(link).closest(".form-of")[0].classList,
				function (className) {
					return (/^(gender|origin|origin_transliteration|pos|target|transliteration)-.+|.+-form-of$/.test(className));
				});
			
			var accelParam = classNames.join(" ");
			
			var targetPagename = getTargetPagename(link);
			var targetHead = (link.innerText || link.textContent).replace(/ /g, "_");
			
			if (targetPagename != targetHead) {
				accelParam = "target-" + targetHead + " " + accelParam;
			}
			
			return "pos-" + getPartOfSpeech(link).replace(/ /g, "_") + " " + accelParam;
		};
		
		var storeAccelParam = function(link) {
			// Extract the targeted pagename from the URL,
			// and language code from the nearest element with a lang attribute
			var lang = getLang(link);
			var targetPagename = getTargetPagename(link);
			
			// Add page name to the list
			if (accelParamsByPagename[lang] === undefined) {
				accelParamsByPagename[lang] = {};
			}
			
			if (accelParamsByPagename[lang][targetPagename] === undefined) {
				accelParamsByPagename[lang][targetPagename] = [];
			}
			
			var accelParam = createAccelParam(link);
			
			if (accelParamsByPagename[lang][targetPagename].indexOf(accelParam) === -1) {
				accelParamsByPagename[lang][targetPagename].push(accelParam);
			}
		};
		
		var processLink = function(link) {
			// Extract the targeted pagename from the URL,
			// and language code from the nearest element with a lang attribute
			var lang = getLang(link);
			var targetPagename = getTargetPagename(link);
			
			// Fetch the acceleration parameters from the store
			var accelParam = accelParamsByPagename[lang][targetPagename]
				.map(function (accel, i) {
					return "accel" + (i + 1).toString() + "=" + encodeURIComponent(accel);
				})
				.join("&");
			
			// Convert a non-edit link into an edit link
			if (link.href.indexOf("action=edit") < 0) {
				link.href = link.href.replace(/^(.*)\/wiki\/([^#]+)(?:#.+)?$/, "$1/w/index.php?title=$2&action=edit");
			}
			
			// use URL to ensure we append to the query string, not e.g. the hash.
			var targetUrl = new URL(link.href);
			// Now build a new "green link" URL to replace the original red link with
			targetUrl.search +=
				"&editintro=MediaWiki:Gadget-AcceleratedFormCreation.js/intro" +
				"&accel_lang=" + encodeURIComponent(lang) +
				"&accel_lemma=" + encodeURIComponent(pageName.replace(/_/g, " ")) +
				"&" + accelParam + 
				"&veswitched=1";
			link.href = targetUrl.href;
				
			link.classList.add("accelerated");
			link.processedLink = true;
		};
		
		// Mutation observer to respond when OrangeLinks modifies links
		var mutobs = new MutationObserver(function(mutations, observer) {
			mutations.forEach(function(mutation) {
				var link = mutation.target;
				if (!(mutation.attributeName == "class" && link.tagName === "A")) {
					return;
				}
				
				// Don't process a link we've already been to
				if (link.processedLink) {
					return;
				}
				
				if (!$(link).hasClass("orange-link")) {
					return;
				}
				
				// Process
				processLink(link);
			});
		});
		
		// First generate and store all the parameters
		var oldtable = null;  // Were we previously inside a table?
		var columns = [];
		
		$(".form-of a").each(function() {
			// Are we currently inside a table?
			var $this = $(this);
			var table = $this.closest("table");
			
			if (table.length > 0) {
				table = table[0];
			} else {
				table = null;
			}
			
			// Was a column number specified on the current table cell?
			// jQuery.fn.data automatically converts an integer-like string
			// to a number.
			var col = $this.closest("td[data-accel-col]").first().data("accel-col");
			
			if (typeof col !== "number") {
				col = null;
			}
			
			// If we were in a table, and we changed to another table or are no longer in one,
			// or if there is no column number attribute, flush the column lists.
			if (oldtable && (oldtable !== table || col === null)) {
				for (var i = 0; i < columns.length; ++i) {
					for (var j = 0; j < columns[i].length; ++j) {
						storeAccelParam(columns[i][j]);
					}
				}
				
				columns = [];
			}
			
			oldtable = table;
			
			// The nostore parameter causes the link to not be stored,
			// but it is processed later. The effect is that this link has no
			// effect on the ordering of forms.
			if ($(this).closest(".form-of").first().hasClass("form-of-nostore")) {
				return;
			}
			
			// If there is a column number attribute, defer storing the link,
			// put it in the columns array instead.
			if (col !== null) {
				--col;  // Column attributes are 1-based, JS arrays are 0-based
				
				// Expand the columns list to fit the number of columns
				while (columns.length <= col) {
					columns.push([]);
				}
				
				// Save the link in the columns list
				columns[col].push(this);
			} else {
				// Store the link directly
				storeAccelParam(this);
			}
		});
		
		// Flush column lists
		for (var i = 0; i < columns.length; ++i) {
			for (var j = 0; j < columns[i].length; ++j) {
				storeAccelParam(columns[i][j]);
			}
		}
		
		// Then add them onto the links, or add a mutation observer
		$(".form-of a").each(function() {
			var $this = $(this);
			if ($this.hasClass("new") || $this.hasClass("orange-link")) {
				processLink(this);
			} else {
				// FIXME: There's a small window for a race condition here.
				// If the "orange-link" class is added by OrangeLinks after the above if-statement is evaluated,
				// but before the observer is added, then the link won't be processed.
				mutobs.observe(this, {attributes : true});
			}
		});
		
		// A function that force-processes all links and adds a class to those that would not have otherwise been processed.
		var forceProcessAllLinks = function() {
			$(".form-of a").each(function() {
				var $this = $(this);
				if (!($this.hasClass("new") || $this.hasClass("orange-link"))) {
					this.classList.add("accelerated-forced");
				}
				processLink(this);
			});
		};
		
		window.accelForceProcessAllLinks = forceProcessAllLinks;
	} else if (mw.config.get("wgAction") === "edit") {
		// Get the parameters from the URL
		var getAccelParams = function() {
			var accelParams = [];
			var i = 1;
			
			while (true) {
				var acceldata = mw.util.getParamValue("accel" + i.toString());
				
				if (!acceldata) {
					break;
				}
				
				// Default values
				var params = {
					pos: null,
					form: null,
					gender: null,
					transliteration: null,
					origin: mw.util.getParamValue("accel_lemma"),
					origin_transliteration: null,
					target: pageName,
				};
				
				// Go over each part and add it
				var parts = acceldata.split(" ");
				
				for (var j = 0; j < parts.length; ++j) {
					var part = parts[j];
					
					var paramMatch = part.match(/^(gender|origin|origin_transliteration|pos|target|transliteration)-(.+)$/);
					if (paramMatch) {
						params[paramMatch[1]] = paramMatch[2].replace(/_/g, " ").replace(/\uFFF0/g, "_");
					} else {
						var formMatch = part.match(/^(.+)-form-of$/);
						if (formMatch) {
							params.form = formMatch[1].replace(/_/g, " ").replace(/\uFFF0/g, "_");
						}
					}
				}
				
				accelParams.push(params);
				++i;
			}
			
			return accelParams;
		};
		
		// Generates entries from the information
		var printArgs = function(accelParams) {
			var args = [
				"lang=" + mw.util.getParamValue("accel_lang"),
				"origin_pagename=" + mw.util.getParamValue("accel_lemma"),
				"target_pagename=" + pageName,
				"num=" + accelParams.length,
			];
			
			for (var i = 0; i < accelParams.length; ++i) {
				for (var key in accelParams[i]) {
					if (accelParams[i][key] !== null) {
						args.push(key + (i + 1) + "=" + accelParams[i][key].replace(/\|/g, "&#124;"));
					}
				}
			}
			
			return args.join("|");
		};
		
		var showModuleError = function(errorText) {
			// Attempt to link to the line of the module in which the error occurred.
			errorText = errorText.replace(
				/(Module:[^#<>\[\]|{}_]+)(?: at line |:)(\d+)/,
				function (wholeMatch, moduleName, lineNumber) {
					var link = document.createElement('a');
					link.href = mw.util.getUrl(moduleName, {action: "edit"}) + "#mw-ce-l" + lineNumber;
					link.innerHTML = moduleName + " at line " + lineNumber;
					return "Lua error in " + link.outerHTML;
				});
			
			var errorBox =
				"<div id=\"accel-error\">" +
				"<p><big>An error occurred while generating the entry:</big></p>" +
				"<p>" + errorText + "</p>" +
				"</div>";
			
			wikipreview.insertAdjacentHTML("beforebegin", errorBox);
		};
		
		var receiveModuleResponse = function(response) {
			var newtext, result;
			
			try {
				result = JSON.parse(response.expandtemplates.wikitext);
			} catch (err) { // JSON parse error should not happen.
				mw.notify(err.msg);
				return;
			}
			
			if (result.error) { // module error
				showModuleError(result.error);
			} else { // successfully generated entries
				newtext = result.entries;
			}
			
			for (i = 0; i < result.messages.length; ++i) {
				mw.notify(result.messages[i]);
			}
			
			if (!newtext) {
				return;
			}
			
			var newValue;

			var langsection_regex = /^==([^=\n]+)==$/mg;
			var match = langsection_regex.exec(newtext);
			
			if (!match) {
				showModuleError("No language section was found in the returned text.");
			}
			
			var langname = match[1];
			
			// Does the page already exist?
			if (textbox.value) {
				var resultInBox = false;
				
				// Reset position at which regex starts its search.
				// Otherwise, regex starts matching after the index where it
				// found the language header in newtext.
				langsection_regex.lastIndex = 0;
				
				// Go over language sections to find where to insert our new one
				while ((match = langsection_regex.exec(textbox.value)) !== null) {
					if (match[1] == langname) {
						// There already exists a section for our language, display text in a separate box.
						resultInBox = true;
						break;
					} else if (match[1] == "Translingual" || match[1] == "English" || (langname != "English" && match[1] < langname)) {
						// Skip past English and Translingual, or if the language sorts higher
						continue;
					} else {
						// We found the first match that sorts lower than our language, great.
						break;
					}
				}
				
				var scrollIndex;
				newValue = textbox.value;
				
				if (resultInBox) {
					// Display the result in a separate box.
					var insertTextBoxIn = document.getElementById("accel-form-conflict-textbox-here");
					if (insertTextBoxIn) {
						var newElement = document.createElement("div");
						newElement.id = insertTextBoxIn.id;
						var warning = document.createElement("p");
						warning.textContent = "A section for this language already exists. Please combine the new text manually:";
						var textBox = document.createElement("textarea");
						textBox.setAttribute("readonly", true);
						textBox.setAttribute("rows", 10);
						textBox.textContent = newtext;
						newElement.appendChild(warning);
						newElement.appendChild(textBox);
						insertTextBoxIn.replaceWith(newElement);
					}
					
					scrollIndex = match !== null ? match.index : newValue.length;
				} else if (match === null) {
					// We found no language that our section should go before, so insert it at the end.
					newValue = newValue.trimEnd() + "\n\n";
					scrollIndex = newValue.length;
					newValue = newValue + newtext;
				} else {
					// We found a language to insert before, so do that.
					newValue = newValue.substring(0, match.index) + newtext + "\n\n" + newValue.substring(match.index);
					scrollIndex = match.index;
				}
				
				// Scroll the textbox to the newly added section. First scroll all the way down,
				// then set the cursor to the start of the new section, which scrolls back up
				// to the new section's language header.
				textbox.scrollTop = textbox.scrollHeight;
				textbox.selectionStart = scrollIndex;
				textbox.selectionEnd = scrollIndex;
				
				summary.value = "Adding forms of " + langname + " [[" + lemma + "]] ([[WT:ACCEL|Accelerated]])";
			} else {
				newValue = newtext;
				summary.value = "Creating forms of " + langname + " [[" + lemma + "]] ([[WT:ACCEL|Accelerated]])";
			}
			
			// Set textbox text. Setting textbox.value is unreliable.
			$(textbox).val(newValue);
		};
		
		var wikipreview = document.getElementById("wikiPreview");
		var textbox = document.getElementById("wpTextbox1");
		var summary = document.getElementById("wpSummary");
		var lang = mw.util.getParamValue("accel_lang");
		var lemma = mw.util.getParamValue("accel_lemma");
		
		if (!(wikipreview && textbox && summary && lang && lemma)) {
			return;
		}
		
		// Gather all the information that was given in the URL
		var accelParams = getAccelParams();
		
		if (!accelParams) {
			return;
		}
		
		var module = "accel", funcName = "generate_JSON";
		mw.loader.using("mediawiki.api", function() {
			new mw.Api().get({
				"action": "expandtemplates",
				"format": "json",
				"text": "{{#invoke:" + module + "|" + funcName + "|" + printArgs(accelParams) + "}}",
				"prop": "wikitext"
			}).done(receiveModuleResponse);
			
		});
	}
});

// </nowiki>