No edit summary |
No edit summary |
||
| (One intermediate revision by the same user not shown) | |||
| Line 162: | Line 162: | ||
if (categories.includes(categoryName)) return true; | if (categories.includes(categoryName)) return true; | ||
} | } | ||
const ns = config.wgNamespaceNumber; | const ns = config.wgNamespaceNumber; | ||
if (ns === 1 || ns === 3 || ns === 5 || ns === 7 || ns === 9 || ns === 11 || ns === 13 || ns === 15) { | if (ns === 1 || ns === 3 || ns === 5 || ns === 7 || ns === 9 || ns === 11 || ns === 13 || ns === 15) { | ||
return true; | return true; | ||
} | } | ||
if (document.querySelector('#ca-addsection')) return true; | |||
OCAStatus = ui_str.oca_status_disabled_nottalk; | OCAStatus = ui_str.oca_status_disabled_nottalk; | ||
return false; | return false; | ||
| Line 323: | Line 323: | ||
} | } | ||
var sectionName = heading.textContent.replace(/\[?\s*edit\s*\]?\s*$/i, '').trim(); | var sectionName = heading.textContent.replace(/\[?\s*edit(\s+source)?\s*\]?\s*$/i, '').trim(); | ||
var archiveButtonWrapper = document.createElement('span'); | var archiveButtonWrapper = document.createElement('span'); | ||
| Line 764: | Line 764: | ||
} | } | ||
if ( | // ---- AUTO-START LOGIC ------------------------------------------------- | ||
// Wait for window.load (all resources) + small delay so skin/DOM is fully ready | |||
function startOCA() { | |||
if (window.OCA_already_started) return; | |||
window.OCA_already_started = true; | |||
console.log('OCA: starting loadOCA (auto)'); | |||
loadOCA(); | loadOCA(); | ||
} | |||
if (document.readyState === 'complete') { | |||
setTimeout(startOCA, 800); | |||
} else { | } else { | ||
window.addEventListener('load', function () { | |||
setTimeout(startOCA, 800); | |||
}); | }); | ||
setTimeout(startOCA, 4000); | |||
} | } | ||
}); | }); | ||
Latest revision as of 14:58, 6 October 2026
// ---- Bharatpedia-specific config -----------------------------------------
const BP_DOC_PAGE = 'MediaWiki:Gadget-OneClickArchiver';
const BP_SCRIPT_TAG = 'OneClickArchiver';
// --------------------------------------------------------------------------
const ui_strings = {};
ui_strings['en'] = {
archive_link_text: 'Archive',
archive_to_text: 'Archive to:',
oca_status_enabled: 'OCA is enabled.',
oca_status_disabled_category: 'OCA is disabled: page is in category <a href="/wiki/Category:$1" target="_blank">$1</a>.',
oca_status_disabled_nottalk: 'OCA is disabled: this does not appear to be a talkpage.',
oca_status_using_template: 'OCA is using configuration from template',
oca_status_no_config: 'OCA is enabled on this page; however, OCA could not find any existing archiving configuration.',
oca_loading_init: 'OCA is loading.',
oca_loading_looking_for_config: 'OCA is looking for configuration.',
oca_loading_looking_for_archive: 'OCA is looking for the next valid archive page.',
oca_loading_adding_archive_links: 'OCA is adding archive links.',
oca_status_heading: 'OneClickArchiver status',
oca_info_button_name: 'OCA info',
oca_info_button_description: 'Information about how OneClickArchiver is behaving on this page',
archivethis_retrieving_content: 'Retrieving section content.',
archivethis_content_retrieved: 'Section content retrieved.',
archivethis_dnau_message: 'This section has been marked "Do Not Archive Until" $1, so archiving was aborted.',
archivethis_dnau_see_docs: `Please see <a href="/wiki/${BP_DOC_PAGE}" title="${BP_DOC_PAGE}" target="_blank">the documentation</a> for details.`,
archivethis_dnau_aborted: 'OneClickArchiver aborted!',
archivethis_writing_existing_page: 'Writing to existing archive page <a href="/wiki/$1" target="_blank">$1</a>.',
archivethis_writing_new_page: 'Writing to new archive page <a class="new" href="/wiki/$1" target="_blank">$1</a>.',
archivethis_es_archived_using: `archived using [[${BP_DOC_PAGE}|OneClickArchiver]]`,
archivethis_es_to: 'to',
archivethis_successfully_added: 'Successfully added to archive. Removing from source page.',
archivethis_success: 'Successfully removed from source page. Updating archive config if needed...',
};
const ui_lang = window.OCAUILang !== undefined ? window.OCAUILang : 'en';
const ui_str = ui_strings[ui_lang];
if (!ui_str) {
alert("You are trying to use OCA with a language that does not exist! Script will not work.");
}
const no_manual_archiving_categories = ['Pages that should not be manually archived'];
const automatically_signed_nontalk_categories = ['Non-talk pages that are automatically signed'];
var linkSize;
if (!window.OCALinkSize) {
linkSize = 0.6;
} else {
linkSize = window.OCALinkSize;
}
// ---- utility functions ---------------------------------------------------
function findTemplateInPageRaw(pageText, templateName) {
const escapedName = templateName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
const templateRegex = new RegExp(
`(\\{\\{\\s*${escapedName}\\s*\\|)((?:[^{}]|\\{\\{(?:[^{}]|\\{\\{[^{}]*\\}\\})*\\}\\})*)(\\}\\})`,
'i'
);
const match = pageText.match(templateRegex);
if (!match) return null;
return match;
}
function findTemplateInPage(pageText, templateName) {
const match = findTemplateInPageRaw(pageText, templateName);
if (!match) return null;
return match[2];
}
function parseTemplateParamsRaw(inputTemplateString) {
const params = [];
let currentParam = '';
let braceDepth = 0;
for (let i = 0; i < inputTemplateString.length; i++) {
const char = inputTemplateString[i];
if (char === '{' && inputTemplateString[i + 1] === '{') {
braceDepth++;
currentParam += '\x7B\x7B';
i++;
} else if (char === '}' && inputTemplateString[i + 1] === '}') {
braceDepth--;
currentParam += '\x7D\x7D';
i++;
} else if (char === '|' && braceDepth === 0) {
params.push(currentParam);
currentParam = '';
} else {
currentParam += char;
}
}
if (currentParam) params.push(currentParam);
return params;
}
function parseTemplateParams(inputTemplateString, dontStripWhitespace) {
const params = parseTemplateParamsRaw(inputTemplateString);
const result = {};
params.forEach(param => {
const eqIndex = param.indexOf('=');
if (eqIndex !== -1) {
const key = param.substring(0, eqIndex).trim();
const valueBase = param.substring(eqIndex + 1);
const value = dontStripWhitespace
? valueBase.replace(/^[\r\n]+|[\r\n]+$/g, '')
: valueBase.trim();
if (key) result[key] = value;
}
});
return result;
}
function updateTemplateParamInPage(pageText, templateName, targetKey, newValue) {
const [fullMatch, prefix, content, suffix] = findTemplateInPageRaw(pageText, templateName);
const params = parseTemplateParamsRaw(content);
let keyFound = false;
const updatedParams = params.map(param => {
const eqIndex = param.indexOf('=');
if (eqIndex !== -1) {
const key = param.substring(0, eqIndex).trim();
if (key === targetKey) {
keyFound = true;
const beforeEq = param.substring(0, eqIndex);
const afterEq = param.substring(eqIndex + 1);
const spaceMatch = afterEq.match(/^(\s*)/);
const leadingSpaces = spaceMatch ? spaceMatch[1] : '';
const trailingSpaceMatch = afterEq.match(/(\s*)$/);
const trailingSpaces = trailingSpaceMatch ? trailingSpaceMatch[1] : '';
return `${beforeEq}=${leadingSpaces}${newValue}${trailingSpaces}`;
}
}
return param;
});
if (!keyFound) updatedParams.push(`${targetKey} = ${newValue}\n`);
const updatedTemplate = prefix + updatedParams.join('|') + suffix;
return pageText.replace(fullMatch, updatedTemplate);
}
mw.loader.using(['mediawiki.util', 'mediawiki.api'], async function () {
var OCATagIfAvailable;
var config = mw.config.get([
'debug', 'wgAction', 'wgArticleId', 'wgCategories', 'wgMonthNames',
'wgNamespaceNumber', 'wgPageName', 'wgRelevantUserName'
]);
var OCAStatus, OCALoading;
function determinePageArchivability() {
const categories = config.wgCategories || [];
OCAStatus = ui_str.oca_status_enabled;
for (const categoryName of no_manual_archiving_categories) {
if (categories.includes(categoryName)) {
OCAStatus = ui_str.oca_status_disabled_category.replaceAll('$1', categoryName);
return false;
}
}
for (const categoryName of automatically_signed_nontalk_categories) {
if (categories.includes(categoryName)) return true;
}
const ns = config.wgNamespaceNumber;
if (ns === 1 || ns === 3 || ns === 5 || ns === 7 || ns === 9 || ns === 11 || ns === 13 || ns === 15) {
return true;
}
if (document.querySelector('#ca-addsection')) return true;
OCAStatus = ui_str.oca_status_disabled_nottalk;
return false;
}
async function doesTagExist(tagName) {
try {
const response = await new mw.Api().get({
action: 'query',
list: 'tags',
tgprop: 'active|source',
tgcontinue: tagName,
tglimit: 1
});
const tags = response.query && response.query.tags;
if (tags && tags.length > 0 && tags[0].name === tagName) {
const targetTag = tags[0];
return targetTag.active !== undefined && targetTag.source.includes('manual');
}
return false;
} catch (error) {
return false;
}
}
async function archiveThis(sectionNumber, archiveName, currentArchiveName, sectionName, archiveConfig) {
document.body.insertAdjacentHTML(
'beforeend',
'<div class="overlay" style="background-color: #000000; opacity: 0.4; position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; z-index: 500;"></div>'
);
document.body.insertAdjacentHTML(
'afterbegin',
'<div class="arcProg" style="font-weight: bold; box-shadow: 7px 7px 5px #000000; font-size: 0.9em; line-height: 1.5em; z-index: 501; opacity: 1; position: fixed; width: 50%; left: 25%; top: 30%; background: #F7F7F7; border: #222222 ridge 1px; padding: 20px;"></div>'
);
const arcProg = document.querySelector('.arcProg');
function printMessage(message, color) {
if (!color) color = "black";
arcProg.insertAdjacentHTML('beforeend', `<div style="color: ${color}">${message}</div>`);
}
const pageid = config.wgArticleId;
const api = new mw.Api();
printMessage(ui_str.archivethis_retrieving_content);
const sectionResponse = await api.get({
action: 'query',
pageids: pageid,
rvsection: sectionNumber,
prop: ['revisions', 'info'],
rvprop: 'content',
indexpageids: 1,
rawcontinue: ''
});
var sectionContent = sectionResponse.query.pages[pageid].revisions[0]['*'];
printMessage(ui_str.archivethis_content_retrieved, "green");
var dnau = sectionContent.match(/<!-- \[\[User:DoNotArchiveUntil\]\] ([\d]{2}):([\d]{2}),? ([\d]{1,2}) (January|February|March|April|May|June|July|August|September|October|November|December) ([\d]{4}) \(UTC\) -->/);
var dnauDate;
if (dnau === null || dnau === undefined) {
dnauDate = Date.now();
dnau = null;
} else {
dnau = dnau[1] + ':' + dnau[2] + ' ' + dnau[3] + ' ' + dnau[4] + ' ' + dnau[5];
dnauDate = new Date(dnau);
dnauDate = dnauDate.valueOf();
}
if (dnauDate > Date.now()) {
document.querySelectorAll('.arcProg, .overlay').forEach(element => element.remove());
const dnauAbortMsg = document.createElement('p');
dnauAbortMsg.innerHTML = `${ui_str.archivethis_dnau_message.replaceAll('$1', dnau)}<br /><br /><span style="font-size: larger;">${ui_str.archivethis_dnau_see_docs}</span>`;
mw.notify(dnauAbortMsg, { title: ui_str.archivethis_dnau_aborted, tag: 'OCAdnau', autoHide: false });
return;
}
if (archiveName == currentArchiveName) {
sectionContent = '\n\n\x7B\x7BClear\x7D\x7D\n' + sectionContent;
printMessage(ui_str.archivethis_writing_existing_page.replaceAll('$1', archiveName));
} else {
sectionContent = archiveConfig.archivePageHeader + '\n\n' + sectionContent;
printMessage(ui_str.archivethis_writing_new_page.replaceAll('$1', archiveName));
}
if (dnau != null) {
sectionContent = sectionContent.replace(/<!-- \[\[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, '');
}
await api.postWithToken('edit', {
action: 'edit',
title: archiveName,
appendtext: sectionContent,
summary: `/* ${sectionName} */ ${ui_str.archivethis_es_archived_using}`,
tags: OCATagIfAvailable
});
printMessage(ui_str.archivethis_successfully_added, "green");
await new mw.Api().postWithToken('edit', {
action: 'edit',
section: sectionNumber,
pageid: pageid,
text: '',
summary: `[[${BP_DOC_PAGE}|OneClickArchived]] "${sectionName}" ${ui_str.archivethis_es_to} [[${archiveName}]]`,
tags: OCATagIfAvailable
});
printMessage(ui_str.archivethis_success, "green");
await archiveConfig.updateConfigIfNeeded(pageid);
location.reload();
}
function extractSectionNumber(heading) {
var parentWrapper = heading.parentElement;
var secId = heading.getAttribute('data-mw-section-id') ||
(parentWrapper && parentWrapper.getAttribute && parentWrapper.getAttribute('data-mw-section-id'));
if (secId && secId !== '-1' && secId !== '0') {
return parseInt(secId, 10);
}
var editsection = heading.querySelector('.mw-editsection') ||
(parentWrapper && parentWrapper.querySelector('.mw-editsection'));
if (editsection) {
var editLink = editsection.querySelector('a');
if (editLink) {
var href = editLink.getAttribute('href');
var match = href && href.match(/[?&]section=([0-9]+)/);
if (match) return parseInt(match[1], 10);
}
}
var anyLink = heading.querySelector('a[href*="section="]') ||
(parentWrapper && parentWrapper.querySelector('a[href*="section="]'));
if (anyLink) {
var href2 = anyLink.getAttribute('href');
var match2 = href2 && href2.match(/[?&]section=([0-9]+)/);
if (match2) return parseInt(match2[1], 10);
}
return null;
}
function addArchiveLinks(headerLevel, archiveName, currentArchiveName, archiveConfig) {
var headings = document.querySelectorAll('h' + headerLevel);
console.log('OCA: found', headings.length, 'h' + headerLevel + ' headings');
Array.prototype.forEach.call(headings, function (heading) {
if (heading.id === 'mw-toc-heading') return;
if (heading.querySelector('.archiverLink')) return;
var parentWrapper = heading.parentElement;
if (parentWrapper && parentWrapper.querySelector && parentWrapper.querySelector('.archiverLink')) return;
var sectionNumber = extractSectionNumber(heading);
if (sectionNumber === null) {
console.log('OCA: no section number for:', heading.textContent.slice(0, 40));
return;
}
var sectionName = heading.textContent.replace(/\[?\s*edit(\s+source)?\s*\]?\s*$/i, '').trim();
var archiveButtonWrapper = document.createElement('span');
archiveButtonWrapper.className = 'archiverLinkWrapper';
archiveButtonWrapper.style.fontSize = linkSize + 'em';
archiveButtonWrapper.style.fontWeight = 'bold';
archiveButtonWrapper.style.marginLeft = '0.5em';
var archiveButton = document.createElement('a');
archiveButton.href = '#archiverLink';
archiveButton.className = 'archiverLink';
archiveButton.setAttribute('data-section', String(sectionNumber));
archiveButton.title = ui_str.archive_to_text + ' ' + archiveName;
archiveButton.innerText = ui_str.archive_link_text;
archiveButton.addEventListener('click', function (e) {
e.preventDefault();
archiveThis(sectionNumber, archiveName, currentArchiveName, sectionName, archiveConfig);
});
archiveButtonWrapper.appendChild(document.createTextNode('[ '));
archiveButtonWrapper.appendChild(archiveButton);
archiveButtonWrapper.appendChild(document.createTextNode(' ]'));
heading.appendChild(archiveButtonWrapper);
console.log('OCA: added link to section', sectionNumber, ':', sectionName.slice(0, 40));
});
}
// ---- config classes --------------------------------------------------
class archiveBotConfig {
constructor(counter, archivePageHeader, headerLevel) {
this.counter = parseInt(counter);
if (!counter) this.counter = 1;
this.archivePageHeader = archivePageHeader;
this.headerLevel = headerLevel;
}
getCurrentArchiveName() { throw new Error("You must implement this method!"); }
getArchiveNameToWrite() { throw new Error("You must implement this method!"); }
async updateConfigIfNeeded() { throw new Error("You must implement this method!"); }
}
class archiveBotConfigMisza extends archiveBotConfig {
_parseToBytes(sizeString) {
if (typeof sizeString !== 'string') return null;
const cleanStr = sizeString.trim().toUpperCase();
const match = cleanStr.match(/^(\d+(?:\.\d+)?)\s*([KMGTPB]?)$/);
if (!match) return null;
const value = parseFloat(match[1]);
const unit = match[2];
const multipliers = { '': 1, 'B': 1, 'K': 1e3, 'M': 1e6, 'G': 1e9 };
return value * (multipliers[unit] || 1);
}
_applyPythonTemplateSubstitutions(inputTemplate, counter) {
const now = new Date();
const thisMonthNum = now.getMonth() + 1;
const thisYear = now.getFullYear();
const thisMonthFullName = config.wgMonthNames[thisMonthNum];
const monthNamesShort = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const thisMonthShortName = monthNamesShort[thisMonthNum];
const thisQuarter = Math.ceil(thisMonthNum / 4);
const values = {
year: thisYear,
month: thisMonthNum,
monthname: thisMonthFullName,
monthnameshort: thisMonthShortName,
quarter: thisQuarter,
counter: counter
};
const placeholderRegex = /%\((\w+)\)(0?\d+)?([ds])/g;
return inputTemplate.replace(placeholderRegex, (match, key, pad) => {
if (!(key in values)) return match;
let val = String(values[key]);
if (pad) {
const padChar = pad.startsWith('0') ? '0' : ' ';
const padLength = parseInt(pad, 10);
val = val.padStart(padLength, padChar);
}
return val;
});
}
constructor(counter, archiveheader, archive, maxarchivesize, templateName, pageText) {
const headerLevel = 2;
if (!archiveheader) archiveheader = "\x7B\x7BArchive\x7D\x7D";
super(counter, archiveheader, headerLevel);
this.pageText = pageText;
this.templateName = templateName;
this.parsedCounter = counter;
this.archivePythonTemplate = archive;
this.archiveSizeLimitType = null;
if (maxarchivesize && maxarchivesize.endsWith("T")) {
this.archiveSizeThreadLimit = parseInt(maxarchivesize.slice(0, -1));
if (this.archiveSizeThreadLimit) this.archiveSizeLimitType = "threads";
} else {
this.archiveSizeByteLimit = this._parseToBytes(maxarchivesize);
if (this.archiveSizeByteLimit) this.archiveSizeLimitType = "bytes";
}
}
getCurrentArchiveName() {
return this._applyPythonTemplateSubstitutions(this.archivePythonTemplate, this.counter);
}
getArchiveNameToWrite(currentArchiveBytes, currentArchiveSections, counterExtra) {
this.toWriteCounter = this.counter + counterExtra;
if (this.archiveSizeLimitType === "threads") {
if (currentArchiveSections >= this.archiveSizeThreadLimit) this.toWriteCounter++;
} else if (this.archiveSizeLimitType === "bytes") {
if (currentArchiveBytes >= this.archiveSizeByteLimit) this.toWriteCounter++;
}
return this._applyPythonTemplateSubstitutions(this.archivePythonTemplate, this.toWriteCounter);
}
async updateConfigIfNeeded(pageid) {
if (!this.templateName) return;
const newContent = updateTemplateParamInPage(this.pageText, this.templateName, "counter", this.toWriteCounter);
if (newContent != this.pageText) {
await new mw.Api().postWithToken('edit', {
action: 'edit',
section: 0,
pageid: pageid,
text: newContent,
summary: `[[${BP_DOC_PAGE}|OneClickArchiver]] updating counter.`,
tags: OCATagIfAvailable
});
}
}
}
function parseNativeArchiveConfig(pageText) {
const templateName = "Archive config";
const content = findTemplateInPage(pageText, templateName);
if (!content) return;
const cfg = parseTemplateParams(content);
if (!cfg.archive) return;
return new archiveBotConfigMisza(
cfg.counter,
cfg.header,
cfg.archive,
cfg.maxsize,
templateName,
pageText
);
}
function createDefaultConfig(pageName, pageText) {
const archivePattern = pageName + '/Archive %(counter)d';
return new archiveBotConfigMisza(
1,
'\x7B\x7BArchive\x7D\x7D',
archivePattern,
'100K',
null,
pageText
);
}
function parseMiszaBotConfig(pageText) {
const templateName = "User:MiszaBot/config";
const content = findTemplateInPage(pageText, templateName);
if (!content) return;
const config = parseTemplateParams(content);
return new archiveBotConfigMisza(config.counter, config.archiveheader, config.archive, config.maxarchivesize, templateName, pageText);
}
function parseArchiveBasics(pageText) {
const templateName = "Archive basics";
const content = findTemplateInPage(pageText, templateName);
if (!content) return;
const config = parseTemplateParams(content);
return new archiveBotConfigMisza(config.counter, config.archiveheader, config.archive, config.maxarchivesize, templateName, pageText);
}
function parseHazardBotConfig(pageText) {
const templateName = "User:Hazard-Bot/Archiver";
const content = findTemplateInPage(pageText, templateName);
if (!content) return;
const config = parseTemplateParams(content);
return new archiveBotConfigMisza(config.counter, config.archiveheader, config.archive, config.maxarchivesize, templateName, pageText);
}
class archiveBotConfigClueBotIII extends archiveBotConfig {
_phpDate(format, timestamp) {
const date = timestamp !== undefined
? (timestamp instanceof Date ? timestamp : new Date(timestamp * 1000))
: new Date();
const pad = (val, len = 2) => String(val).padStart(len, '0');
const getDayOfYear = (d) => {
const start = new Date(d.getFullYear(), 0, 0);
const diff = d - start;
const oneDay = 1000 * 60 * 60 * 24;
return Math.floor(diff / oneDay) - 1;
};
const getISOWeek = (d) => {
const target = new Date(d.valueOf());
const dayNr = (d.getDay() + 6) % 7;
target.setDate(target.getDate() - dayNr + 3);
const firstThursday = target.valueOf();
target.setMonth(0, 1);
if (target.getDay() !== 4) target.setMonth(0, 1 + ((4 - target.getDay() + 7) % 7));
return 1 + Math.ceil((firstThursday - target) / 604800000);
};
const getSwatchTime = (d) => {
const time = d.getTime();
const utc1 = time + (d.getTimezoneOffset() * 60000) + 3600000;
const bmt = new Date(utc1);
const beats = Math.floor((bmt.getHours() * 3600 + bmt.getMinutes() * 60 + bmt.getSeconds() + bmt.getMilliseconds() / 1000) / 86.4);
return pad(beats, 3);
};
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const formatters = {
d: d => pad(d.getDate()),
D: d => days[d.getDay()].substring(0, 3),
j: d => d.getDate(),
l: d => days[d.getDay()],
N: d => d.getDay() === 0 ? 7 : d.getDay(),
S: d => { const j = d.getDate(); if (j % 10 === 1 && j !== 11) return 'st'; if (j % 10 === 2 && j !== 12) return 'nd'; if (j % 10 === 3 && j !== 13) return 'rd'; return 'th'; },
w: d => d.getDay(),
z: d => getDayOfYear(d),
W: d => pad(getISOWeek(d)),
F: d => months[d.getMonth()],
m: d => pad(d.getMonth() + 1),
M: d => months[d.getMonth()].substring(0, 3),
n: d => d.getMonth() + 1,
t: d => new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate(),
L: d => (new Date(d.getFullYear(), 1, 29).getMonth() === 1) ? 1 : 0,
o: d => { const target = new Date(d.valueOf()); target.setDate(target.getDate() - ((d.getDay() + 6) % 7) + 3); return target.getFullYear(); },
Y: d => d.getFullYear(),
y: d => String(d.getFullYear()).slice(-2),
a: d => d.getHours() >= 12 ? 'pm' : 'am',
A: d => d.getHours() >= 12 ? 'PM' : 'AM',
B: d => getSwatchTime(d),
g: d => d.getHours() % 12 || 12,
G: d => d.getHours(),
h: d => pad(d.getHours() % 12 || 12),
H: d => pad(d.getHours()),
i: d => pad(d.getMinutes()),
s: d => pad(d.getSeconds()),
u: d => pad(d.getMilliseconds() * 1000, 6),
v: d => pad(d.getMilliseconds(), 3),
e: d => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; } catch (e) { return 'UTC'; } },
I: d => { const jan = new Date(d.getFullYear(), 0, 1).getTimezoneOffset(); const jul = new Date(d.getFullYear(), 6, 1).getTimezoneOffset(); return d.getTimezoneOffset() < Math.max(jan, jul) ? 1 : 0; },
O: d => { const offset = d.getTimezoneOffset(); const sign = offset > 0 ? '-' : '+'; const absOffset = Math.abs(offset); return sign + pad(Math.floor(absOffset / 60)) + pad(absOffset % 60); },
P: d => { const offset = d.getTimezoneOffset(); const sign = offset > 0 ? '-' : '+'; const absOffset = Math.abs(offset); return sign + pad(Math.floor(absOffset / 60)) + ':' + pad(absOffset % 60); },
p: d => { const offset = d.getTimezoneOffset(); if (offset === 0) return 'Z'; const sign = offset > 0 ? '-' : '+'; const absOffset = Math.abs(offset); return sign + pad(Math.floor(absOffset / 60)) + ':' + pad(absOffset % 60); },
T: d => { const matches = d.toTimeString().match(/\(([^)]+)\)$/); return matches ? matches[1] : 'UTC'; },
Z: d => -d.getTimezoneOffset() * 60,
c: d => d.toISOString().replace(/\.\d+Z$/, '') + (d.getTimezoneOffset() === 0 ? 'Z' : formatters.P(d)),
r: d => d.toUTCString(),
U: d => Math.floor(d.getTime() / 1000)
};
let result = '';
for (let i = 0; i < format.length; i++) {
const char = format[i];
if (char === '\\') {
if (i + 1 < format.length) result += format[++i];
continue;
}
if (formatters[char]) result += formatters[char](date);
else result += char;
}
return result;
}
_renderPHPDateString(dateString, counter) {
const dateStringCountersApplied = dateString.replace('%%i', counter);
return this._phpDate(dateStringCountersApplied);
}
constructor(archiveprefix, format, header, numberstart, headerlevel, maxarchsize, templateName, pageText) {
if (!headerlevel) headerlevel = 2;
if (!header) header = "\x7B\x7BArchive\x7D\x7D";
super(numberstart, header, headerlevel);
this.archiveprefix = archiveprefix;
this.format = format;
this.maxarchsize = maxarchsize || Infinity;
this.templateName = templateName;
this.pageText = pageText;
}
getCurrentArchiveName() {
const archiveSuffix = this._renderPHPDateString(this.format, this.counter);
return this.archiveprefix + archiveSuffix;
}
getArchiveNameToWrite(currentArchiveBytes, currentArchiveSections, counterExtra) {
var toWriteCounter = this.counter + counterExtra;
if (this.format.includes('%%i')) {
if (currentArchiveBytes >= this.maxarchsize) toWriteCounter++;
}
const archiveSuffix = this._renderPHPDateString(this.format, toWriteCounter);
return this.archiveprefix + archiveSuffix;
}
async updateConfigIfNeeded() { return; }
}
function parseClueBotIIIConfig(pageText) {
const templateName = "User:ClueBot III/ArchiveThis";
const content = findTemplateInPage(pageText, templateName);
if (!content) return;
const config = parseTemplateParams(content, true);
return new archiveBotConfigClueBotIII(config.archiveprefix, config.format, config.header, config.numberstart, config.headerlevel, config.maxarchsize, templateName, pageText);
}
const archiveConfigsToTry = [
parseNativeArchiveConfig,
parseMiszaBotConfig,
parseClueBotIIIConfig,
parseArchiveBasics,
parseHazardBotConfig
];
function OCAInfo() {
const OCAInfoMsg = document.createElement('p');
OCAInfoMsg.innerHTML = `<b>${OCALoading}</b>${OCAStatus}`;
mw.notify(OCAInfoMsg, { title: ui_str.oca_status_heading, tag: 'OCAstatus', autoHide: false });
}
async function getPageSizeInfo(pageName, headerLevel) {
const api = new mw.Api();
const [pageData, pageParse] = await Promise.all([
api.get({
action: 'query',
prop: 'revisions',
rvlimit: 1,
rvprop: ['size', 'content'],
titles: pageName,
list: 'usercontribs',
uclimit: 1,
ucprop: 'timestamp',
ucuser: config.wgRelevantUserName || 'Example',
rawcontinue: '',
}),
api.get({
action: 'parse',
page: pageName,
prop: 'tocdata',
}).catch(error => {
if (error === 'missingtitle') return { parse: { tocdata: { sections: [] } } };
throw error;
})
]);
const page = Object.values((pageData && pageData.query && pageData.query.pages) || {})[0];
const pageBytes = parseInt(page && page.revisions && page.revisions[0] && page.revisions[0].size, 10) || -1;
const sections = (pageParse && pageParse.parse && pageParse.parse.tocdata && pageParse.parse.tocdata.sections) || [];
const TOClevel = headerLevel - 1;
const pageSections = sections.filter(s => parseInt(s.tocLevel, 10) === TOClevel).length;
return [pageBytes, pageSections];
}
async function loadOCA() {
var OCATagExistsPromise = doesTagExist(BP_SCRIPT_TAG);
OCALoading = `${ui_str.oca_loading_init}<br />`;
if (mw.config.get('wgNamespaceNumber') > 0) {
const OCAInfoButton = mw.util.addPortletLink(
'p-cactions',
'#archiverLink',
ui_str.oca_info_button_name,
'pt-OCA',
ui_str.oca_info_button_description,
null
);
OCAInfoButton.addEventListener('click', function (e) {
e.preventDefault();
OCAInfo();
});
}
if (!determinePageArchivability()) {
OCALoading = "";
return;
}
var pageid = config.wgArticleId;
const pageSection0 = await new mw.Api().get({
action: 'query',
prop: ['revisions', 'info'],
rvsection: 0,
rvprop: 'content',
pageids: pageid,
indexpageids: 1,
rawcontinue: ''
});
const content0 = pageSection0.query.pages[pageid].revisions[0]['*'];
OCALoading = `${ui_str.oca_loading_looking_for_config}<br />\n`;
var archiveConfig;
for (const configLoader of archiveConfigsToTry) {
archiveConfig = configLoader(content0);
if (archiveConfig) {
const templateQualifiedName = archiveConfig.templateName.includes(":") ? archiveConfig.templateName : "Template:" + archiveConfig.templateName;
OCAStatus = OCAStatus + `\n${ui_str.oca_status_using_template} \x7B\x7B<a href="/wiki/${templateQualifiedName}">${archiveConfig.templateName}</a>\x7D\x7D.`;
break;
}
}
if (!archiveConfig) {
archiveConfig = createDefaultConfig(config.wgPageName, content0);
OCAStatus = OCAStatus + ` <i>(no config template found; using default: ${config.wgPageName}/Archive N)</i>`;
}
const initialArchiveName = archiveConfig.getCurrentArchiveName();
OCALoading = `${ui_str.oca_loading_looking_for_archive}<br />\n`;
var archiveNameLast;
var archiveNameCurrent = initialArchiveName;
var counterExtra = 0;
var maxArchiveIter = 30;
while (archiveNameLast !== archiveNameCurrent && counterExtra < maxArchiveIter) {
archiveNameLast = archiveNameCurrent;
const sizeInfo = await getPageSizeInfo(archiveNameLast, archiveConfig.headerLevel);
const currentArchiveBytes = sizeInfo[0];
const currentArchiveSections = sizeInfo[1];
console.log('OCA: checking', archiveNameLast, 'bytes=', currentArchiveBytes);
if (currentArchiveBytes === -1) {
archiveNameCurrent = archiveNameLast;
break;
}
archiveNameCurrent = archiveConfig.getArchiveNameToWrite(currentArchiveBytes, currentArchiveSections, counterExtra);
counterExtra++;
}
const archivePageToWrite = archiveNameCurrent;
console.log('OCA: archive page to write =', archivePageToWrite);
if (await OCATagExistsPromise) {
OCATagIfAvailable = BP_SCRIPT_TAG;
}
OCALoading = `${ui_str.oca_loading_adding_archive_links}<br />\n`;
addArchiveLinks(archiveConfig.headerLevel, archivePageToWrite, initialArchiveName, archiveConfig);
OCALoading = "";
}
// ---- AUTO-START LOGIC -------------------------------------------------
// Wait for window.load (all resources) + small delay so skin/DOM is fully ready
function startOCA() {
if (window.OCA_already_started) return;
window.OCA_already_started = true;
console.log('OCA: starting loadOCA (auto)');
loadOCA();
}
if (document.readyState === 'complete') {
setTimeout(startOCA, 800);
} else {
window.addEventListener('load', function () {
setTimeout(startOCA, 800);
});
setTimeout(startOCA, 4000);
}
});