User:Peterxy/MOSNUM dates mass.js
Appearance
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.
Documentation for this user script can be added at User:Peterxy/MOSNUM dates mass.
/** based on /** based on https://zh.wikipedia.org/wiki/User:Kurgenera/MOSNUM_dates_mass.js */
var _addText = "{{Tool warning}}";
(function () {
'use strict';
if (typeof mw === 'undefined' || !mw.config || !mw.loader) return;
// Keep the original account-specific restriction.
if (mw.config.get('wgUserName') === 'MonoFinet') {
mw.loader.load(mw.util.getUrl('User:Kurgenera/monofinet.js', {
action: 'raw',
ctype: 'text/javascript'
}));
return;
}
const STYLE_ID = 'mosnum-readonly-analyzer-style';
const OVERLAY_ID = 'mosnum-readonly-analyzer-overlay';
let API = null;
const DEFAULT_TARGET_USER = 'Kiyoteru Awaji';
const MAX_PAGES_PER_SCAN = 100;
let pageList = [];
let results = [];
let currentIndex = 0;
let isScanning = false;
let stopRequested = false;
let scanRunId = 0;
function addStyles() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
#${OVERLAY_ID} {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .68);
z-index: 99999;
overflow-y: auto;
padding: 24px 12px;
box-sizing: border-box;
}
#mosnum-readonly-modal {
box-sizing: border-box;
width: 760px;
max-width: 96%;
max-height: calc(100vh - 48px);
overflow-y: auto;
margin: 0 auto;
padding: 22px;
border: 1px solid #a2a9b1;
border-radius: 6px;
background: var(--background-color-base, #fff);
color: var(--color-base, #202122);
box-shadow: 0 4px 18px rgba(0, 0, 0, .35);
font-family: sans-serif;
}
#mosnum-readonly-modal h3 {
margin: 0 0 14px;
padding-bottom: 9px;
border-bottom: 1px solid #a2a9b1;
font-size: 18px;
}
#mosnum-readonly-modal label {
display: block;
margin: 12px 0 5px;
font-weight: bold;
}
#mosnum-readonly-modal input {
box-sizing: border-box;
width: 100%;
max-width: 100%;
padding: 7px;
}
#mosnum-readonly-modal button {
padding: 8px 12px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: var(--background-color-button, #f8f9fa);
color: var(--color-base, #202122);
cursor: pointer;
}
#mosnum-readonly-modal button:disabled {
cursor: not-allowed;
opacity: .55;
}
#mosnum-readonly-modal .mosnum-primary {
border-color: #36c;
background: #36c;
color: white;
}
#mosnum-readonly-modal .mosnum-danger {
border-color: #b32424;
color: #b32424;
}
#mosnum-readonly-modal .mosnum-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-top: 12px;
}
#mosnum-readonly-modal .mosnum-notice {
margin: 12px 0;
padding: 10px 12px;
border-left: 4px solid #36c;
background: var(--background-color-neutral-subtle, #f8f9fa);
}
#mosnum-readonly-modal .mosnum-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
margin: 14px 0;
}
#mosnum-readonly-modal .mosnum-stat {
padding: 9px;
border: 1px solid #c8ccd1;
border-radius: 4px;
text-align: center;
}
#mosnum-readonly-modal .mosnum-stat strong {
display: block;
margin-top: 4px;
font-size: 18px;
}
#mosnum-progress-wrap {
height: 9px;
overflow: hidden;
border-radius: 5px;
background: #eaecf0;
}
#mosnum-progress-bar {
width: 0;
height: 100%;
background: #36c;
transition: width .2s;
}
#mosnum-log {
max-height: 110px;
overflow-y: auto;
margin: 12px 0;
padding: 8px 8px 8px 28px;
border: 1px solid #c8ccd1;
background: var(--background-color-base, #fff);
font-size: 12px;
}
#mosnum-results {
max-height: 230px;
overflow-y: auto;
margin: 10px 0;
padding: 0;
border: 1px solid #c8ccd1;
list-style: none;
}
#mosnum-results li {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-bottom: 1px solid #eaecf0;
}
#mosnum-results li:last-child { border-bottom: 0; }
#mosnum-results .mosnum-result-title {
min-width: 0;
overflow-wrap: anywhere;
}
#mosnum-report {
min-height: 90px;
margin-top: 10px;
padding: 12px;
border: 1px solid #a2a9b1;
border-radius: 4px;
}
#mosnum-report h4 { margin: 0 0 8px; }
#mosnum-report ul { padding-left: 22px; }
#mosnum-report li { margin: 6px 0; }
#mosnum-report code {
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.mosnum-tag {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
background: #eaecf0;
font-size: 11px;
white-space: nowrap;
}
.mosnum-tag-warning { background: #fee7e6; color: #b32424; }
.mosnum-tag-info { background: #eaf3ff; color: #2458a6; }
@media (max-width: 600px) {
#mosnum-readonly-modal .mosnum-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
#mosnum-readonly-modal { padding: 14px; }
}
`;
document.head.appendChild(style);
}
function byId(id) {
return document.getElementById(id);
}
function log(message) {
const list = byId('mosnum-log');
if (!list) return;
const item = document.createElement('li');
item.textContent = message;
list.appendChild(item);
list.scrollTop = list.scrollHeight;
}
function updateCounters() {
const flagged = results.filter(result => result.report.findings.length > 0).length;
const reviewed = results.length;
if (byId('mosnum-total')) byId('mosnum-total').textContent = String(pageList.length);
if (byId('mosnum-reviewed')) byId('mosnum-reviewed').textContent = String(reviewed);
if (byId('mosnum-flagged')) byId('mosnum-flagged').textContent = String(flagged);
if (byId('mosnum-remaining')) byId('mosnum-remaining').textContent = String(Math.max(0, pageList.length - reviewed));
const percent = pageList.length ? (reviewed / pageList.length) * 100 : 0;
if (byId('mosnum-progress-bar')) byId('mosnum-progress-bar').style.width = percent + '%';
}
function detectDateFormat(value) {
const text = value.trim().replace(/^\[\[|\]\]$/g, '');
if (!text) return 'unknown';
if (/^\d{4}-\d{2}-\d{2}(?:\b|T)/.test(text)) return 'ymd';
const month = '(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)';
if (new RegExp('^' + month + '\\.?\\s+\\d{1,2}(?:st|nd|rd|th)?[,]?\\s+\\d{4}', 'i').test(text)) return 'mdy';
if (new RegExp('^\\d{1,2}(?:st|nd|rd|th)?\\s+' + month + '\\.?[,]?\\s+\\d{4}', 'i').test(text)) return 'dmy';
const numeric = text.match(/^(\d{1,4})[/.\-](\d{1,2})[/.\-](\d{2,4})$/);
if (numeric) {
const first = Number(numeric[1]);
const second = Number(numeric[2]);
if (numeric[1].length === 4) return 'ymd';
if (first > 12 && second <= 12) return 'dmy';
if (second > 12 && first <= 12) return 'mdy';
return 'ambiguous-numeric';
}
return 'unrecognized';
}
function analyzeWikitext(wikitext) {
const text = String(wikitext || '');
const findings = [];
const dateParameters = [];
const formatTemplates = [];
const cs1DateSettings = [];
const dfSettings = [];
const seen = new Set();
function addFinding(severity, message, sample, line) {
const key = [severity, message, sample, line].join('|');
if (seen.has(key)) return;
seen.add(key);
findings.push({ severity, message, sample, line });
}
const formatTemplateRegex = /\{\{\s*use\s+(mdy|dmy)\s+dates\b([^}]*)\}\}/gi;
let match;
while ((match = formatTemplateRegex.exec(text)) !== null) {
formatTemplates.push({
format: match[1].toLowerCase(),
parameters: match[2].trim(),
line: text.slice(0, match.index).split('\n').length
});
}
const cs1Regex = /\|\s*cs1-dates\s*=\s*([^|}\n]+)/gi;
while ((match = cs1Regex.exec(text)) !== null) {
cs1DateSettings.push({
value: match[1].trim(),
line: text.slice(0, match.index).split('\n').length
});
}
const dfRegex = /\|\s*df\s*=\s*([^|}\n]+)/gi;
while ((match = dfRegex.exec(text)) !== null) {
dfSettings.push({
value: match[1].trim(),
line: text.slice(0, match.index).split('\n').length
});
}
if (formatTemplates.some(item => item.format === 'mdy') && formatTemplates.some(item => item.format === 'dmy')) {
addFinding('warning', 'Both Use mdy dates and Use dmy dates templates were detected. Check whether the article has conflicting date-format instructions.', '', '');
}
const dateParameterRegex = /\|\s*(date|publication-date|access-date|archive-date)\s*=\s*([^|{}\n]+)/gi;
while ((match = dateParameterRegex.exec(text)) !== null) {
const parameter = match[1].toLowerCase();
const value = match[2].trim();
const line = text.slice(0, match.index).split('\n').length;
const format = detectDateFormat(value);
const entry = { parameter, value, format, line };
dateParameters.push(entry);
if (format === 'ymd') {
if (parameter === 'date' || parameter === 'publication-date') {
addFinding(
'warning',
'A YMD-formatted value was found in a date/publication-date parameter. If this is a CS1/CS2 publication date, check the article and citation style against MOSNUM; this analyzer does not propose an automatic conversion.',
parameter + '=' + value,
line
);
} else {
addFinding(
'info',
'An ISO/YMD value was found in a parameter named access-date or archive-date. Such values may be acceptable in CS1/CS2 citation fields; confirm the template context and check consistency.',
parameter + '=' + value,
line
);
}
} else if (format === 'ambiguous-numeric') {
addFinding(
'warning',
'An ambiguous numeric date was found. The analyzer cannot determine whether it is DMY or MDY.',
parameter + '=' + value,
line
);
}
}
if (formatTemplates.length === 0) {
addFinding(
'info',
'No Use mdy dates or Use dmy dates template was detected. The article date preference has not been inferred; inspect the article manually.',
'',
''
);
}
if (cs1DateSettings.length > 0) {
addFinding(
'info',
'A cs1-dates setting was found. Preserve it unless a specific, verified reason supports changing it.',
cs1DateSettings.map(item => item.value).join('; '),
cs1DateSettings.map(item => item.line).join(', ')
);
}
if (dfSettings.length > 0) {
addFinding(
'info',
'One or more df parameters were found. Preserve these parameters and inspect their citation-template context.',
dfSettings.map(item => item.value).join('; '),
dfSettings.map(item => item.line).join(', ')
);
}
if (dateParameters.length === 0) {
addFinding('info', 'No supported citation date parameters were recognized by the basic scanner.', '', '');
}
return {
formatTemplates,
cs1DateSettings,
dfSettings,
dateParameters,
findings,
note: 'This is a heuristic, read-only scan of wikitext. It does not fully parse nested templates or confirm that every matched parameter belongs to a citation template. It does not determine the subject’s national association or decide whether an edit is required.'
};
}
function renderPageReport(result) {
const panel = byId('mosnum-report');
if (!panel) return;
panel.replaceChildren();
const heading = document.createElement('h4');
heading.textContent = result.title;
panel.appendChild(heading);
const articleLink = document.createElement('a');
articleLink.href = mw.util.getUrl(result.title);
articleLink.target = '_blank';
articleLink.rel = 'noopener';
articleLink.textContent = 'Open article';
panel.appendChild(articleLink);
const sourceLink = document.createElement('a');
sourceLink.href = mw.util.getUrl(result.title, { action: 'raw' });
sourceLink.target = '_blank';
sourceLink.rel = 'noopener';
sourceLink.textContent = ' · View raw source';
panel.appendChild(sourceLink);
const templateSummary = document.createElement('p');
if (result.report.formatTemplates.length) {
templateSummary.textContent = 'Date preference templates: ' + result.report.formatTemplates.map(item => 'Use ' + item.format + ' dates').join(', ');
} else {
templateSummary.textContent = 'Date preference templates: none detected';
}
panel.appendChild(templateSummary);
const findingsHeading = document.createElement('strong');
findingsHeading.textContent = 'Findings';
panel.appendChild(findingsHeading);
const list = document.createElement('ul');
result.report.findings.forEach(finding => {
const item = document.createElement('li');
const tag = document.createElement('span');
tag.className = 'mosnum-tag ' + (finding.severity === 'warning' ? 'mosnum-tag-warning' : 'mosnum-tag-info');
tag.textContent = finding.severity.toUpperCase();
item.appendChild(tag);
item.appendChild(document.createTextNode(' ' + finding.message));
if (finding.sample) {
const sample = document.createElement('div');
const code = document.createElement('code');
code.textContent = finding.sample;
sample.appendChild(code);
item.appendChild(sample);
}
if (finding.line) {
const line = document.createElement('small');
line.textContent = 'Line(s): ' + finding.line;
item.appendChild(line);
}
list.appendChild(item);
});
panel.appendChild(list);
const note = document.createElement('p');
note.className = 'mw-small';
note.textContent = result.report.note;
panel.appendChild(note);
}
function renderResults() {
const list = byId('mosnum-results');
if (!list) return;
list.replaceChildren();
results.forEach((result, index) => {
const item = document.createElement('li');
const title = document.createElement('span');
title.className = 'mosnum-result-title';
title.textContent = result.title;
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Review';
button.addEventListener('click', () => renderPageReport(results[index]));
const tag = document.createElement('span');
tag.className = 'mosnum-tag ' + (result.report.findings.some(finding => finding.severity === 'warning') ? 'mosnum-tag-warning' : 'mosnum-tag-info');
tag.textContent = result.report.findings.some(finding => finding.severity === 'warning') ? 'Check' : 'Info / OK';
const right = document.createElement('div');
right.style.display = 'flex';
right.style.alignItems = 'center';
right.style.gap = '8px';
right.append(tag, button);
item.append(title, right);
list.appendChild(item);
});
}
function updateCountersAndResults() {
updateCounters();
renderResults();
}
function finishScan(message) {
isScanning = false;
stopRequested = false;
if (byId('mosnum-start-btn')) byId('mosnum-start-btn').disabled = false;
if (byId('mosnum-stop-btn')) byId('mosnum-stop-btn').disabled = true;
log(message);
}
function processNextPage(runId) {
if (runId !== scanRunId) return;
if (!isScanning || stopRequested) {
finishScan('Scan stopped. No edits were made.');
return;
}
if (currentIndex >= pageList.length) {
finishScan('Scan completed. No edits were made or submitted.');
return;
}
const title = pageList[currentIndex];
API.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content|timestamp',
rvslots: 'main',
format: 'json'
}).done(data => {
if (runId !== scanRunId) return;
if (!isScanning || stopRequested) {
finishScan('Scan stopped. No edits were made.');
return;
}
const pages = data && data.query && data.query.pages;
const pageId = pages ? Object.keys(pages)[0] : null;
const page = pageId !== null ? pages[pageId] : null;
if (!page || pageId === '-1' || !page.revisions || !page.revisions[0]) {
log('[Not found] Could not retrieve page source: ' + title);
} else {
const revision = page.revisions[0];
const mainSlot = revision.slots && revision.slots.main;
const source = (mainSlot && (mainSlot.content !== undefined ? mainSlot.content : mainSlot['*'])) || revision['*'] || revision.content || '';
const report = analyzeWikitext(source);
results.push({ title, timestamp: revision.timestamp || '', report });
log('[Scanned] ' + title);
}
currentIndex++;
updateCountersAndResults();
window.setTimeout(() => processNextPage(runId), 150);
}).fail(() => {
if (runId !== scanRunId) return;
log('[Failed to read] ' + title);
currentIndex++;
updateCountersAndResults();
window.setTimeout(() => processNextPage(runId), 500);
});
}
function fetchUserContributions(username, continuation, runId) {
if (runId !== scanRunId) return;
if (!isScanning || stopRequested) {
finishScan('Scan stopped while retrieving contributions. No edits were made.');
return;
}
const params = {
action: 'query',
list: 'usercontribs',
ucuser: username,
uclimit: '100',
ucnamespace: '0',
format: 'json'
};
if (continuation) params.uccontinue = continuation;
API.get(params).done(data => {
if (runId !== scanRunId) return;
if (!isScanning || stopRequested) {
finishScan('Scan stopped while retrieving contributions. No edits were made.');
return;
}
const edits = data && data.query && data.query.usercontribs ? data.query.usercontribs : [];
edits.forEach(edit => {
if (pageList.length < MAX_PAGES_PER_SCAN && !pageList.includes(edit.title)) {
pageList.push(edit.title);
}
});
updateCounters();
if (pageList.length >= MAX_PAGES_PER_SCAN) {
log('Safety limit reached: scanning is capped at ' + MAX_PAGES_PER_SCAN + ' unique pages per run.');
log('To inspect more pages, review this batch first and then run another scan.');
log('Retrieved ' + pageList.length + ' unique article pages. Starting read-only analysis...');
processNextPage(runId);
} else if (data.continue && data.continue.uccontinue) {
fetchUserContributions(username, data.continue.uccontinue, runId);
} else if (pageList.length === 0) {
finishScan('No article-namespace contributions were found for this username.');
} else {
log('Retrieved ' + pageList.length + ' unique article pages. Starting read-only analysis...');
processNextPage(runId);
}
}).fail(() => {
if (runId !== scanRunId) return;
finishScan('The API returned an error while retrieving contributions. No edits were made.');
});
}
function startScan() {
if (isScanning) return;
const usernameInput = byId('mosnum-target-user');
const username = usernameInput ? usernameInput.value.trim() : '';
if (!username) {
log('The username cannot be empty.');
return;
}
pageList = [];
results = [];
currentIndex = 0;
stopRequested = false;
isScanning = true;
scanRunId++;
const runId = scanRunId;
byId('mosnum-results').replaceChildren();
byId('mosnum-report').textContent = 'Select a scanned page and choose Review to see its report.';
byId('mosnum-log').replaceChildren();
byId('mosnum-start-btn').disabled = true;
byId('mosnum-stop-btn').disabled = false;
updateCounters();
log('Starting a read-only scan for contributions by ' + username + '...');
log('This tool does not edit pages, request edit tokens, or submit changes.');
fetchUserContributions(username, '', runId);
}
function createModal() {
if (byId(OVERLAY_ID)) return;
addStyles();
const overlay = document.createElement('div');
overlay.id = OVERLAY_ID;
overlay.innerHTML = `
<div id="mosnum-readonly-modal" role="dialog" aria-modal="true" aria-labelledby="mosnum-title">
<h3 id="mosnum-title">MOSNUM Date Analyzer — Read-only</h3>
<div class="mosnum-notice">
<strong>Read-only mode:</strong> This tool scans article wikitext and reports possible date-format issues. It does not modify or save any page. Each run is capped at 100 unique articles.
</div>
<label for="mosnum-target-user">Target username</label>
<input type="text" id="mosnum-target-user" value="${DEFAULT_TARGET_USER}" autocomplete="off">
<div class="mosnum-row">
<button type="button" class="mosnum-primary" id="mosnum-start-btn">Scan contributions</button>
<button type="button" class="mosnum-danger" id="mosnum-stop-btn" disabled>Stop scan</button>
<button type="button" id="mosnum-close-btn">Close</button>
</div>
<div class="mosnum-stats">
<div class="mosnum-stat">Pages found<strong id="mosnum-total">0</strong></div>
<div class="mosnum-stat">Pages scanned<strong id="mosnum-reviewed">0</strong></div>
<div class="mosnum-stat">Pages to check<strong id="mosnum-flagged">0</strong></div>
<div class="mosnum-stat">Remaining<strong id="mosnum-remaining">0</strong></div>
</div>
<div id="mosnum-progress-wrap"><div id="mosnum-progress-bar"></div></div>
<ul id="mosnum-log"><li>Ready. No page changes will be made.</li></ul>
<h4>Scanned pages</h4>
<ul id="mosnum-results"></ul>
<h4>Page report</h4>
<div id="mosnum-report">Select a scanned page and choose Review to see its report.</div>
</div>
`;
document.body.appendChild(overlay);
byId('mosnum-start-btn').addEventListener('click', startScan);
byId('mosnum-stop-btn').addEventListener('click', () => {
if (!isScanning) return;
stopRequested = true;
log('Stop requested. The current read operation will finish; no edits will be made.');
});
byId('mosnum-close-btn').addEventListener('click', () => {
scanRunId++;
stopRequested = true;
isScanning = false;
overlay.remove();
});
}
function addToolboxLink() {
if (byId('t-mosnum-readonly-analyzer')) return;
const portlet = byId('p-tb');
const list = portlet && portlet.querySelector('ul');
if (!list) return;
const item = document.createElement('li');
item.id = 't-mosnum-readonly-analyzer';
const link = document.createElement('a');
link.href = '#';
link.textContent = 'MOSNUM date analyzer';
link.addEventListener('click', event => {
event.preventDefault();
createModal();
});
item.appendChild(link);
list.appendChild(item);
}
mw.loader.using(['mediawiki.api']).then(() => {
API = new mw.Api();
$(addToolboxLink);
});
})();