User:Rusalkii/RfDInfo.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. |
![]() | Documentation for this user script can be added at User:Rusalkii/RfDInfo. |
// <nowiki>
(function() {
'use strict';
const API_BASE = 'https://en.wikipedia.org/w/api.php';
async function fetchAPI(params) {
const url = `${API_BASE}?format=json&origin=*&${params}`;
const response = await fetch(url);
return response.json();
}
async function getPageInfo(title) {
const data = await fetchAPI(`action=query&prop=info|linkshere|revisions&inprop=&lhlimit=500&rvlimit=1&rvdir=newer&rvprop=timestamp&titles=${encodeURIComponent(title)}`);
const pageId = Object.keys(data.query.pages)[0];
return data.query.pages[pageId];
}
async function getPageContent(title) {
const data = await fetchAPI(`action=query&prop=revisions&rvprop=content&titles=${encodeURIComponent(title)}`);
const page = Object.values(data.query.pages)[0];
return page.revisions?.[0]?.['*'] || '';
}
async function getRedirectTarget(content) {
const match = content.match(/#REDIRECT\s*\[\[(.*?)\]\]/i);
return match ? match[1] : null;
}
function formatCreationDate(timestamp) {
const creationDate = new Date(timestamp);
const ageYears = (new Date() - creationDate) / (365.25 * 24 * 60 * 60 * 1000);
let formatted = ageYears < 1
? creationDate.toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric' })
: creationDate.getFullYear().toString();
if (ageYears > 10) {
formatted = `<span style="color: red;">${formatted}</span>`;
}
return formatted;
}
async function checkTextMentioned(targetTitle, redirectTitle) {
const content = await getPageContent(targetTitle);
const cleanContent = content.replace(/<ref[^>]*>.*?<\/ref>|<ref[^>]*\/>/g, '');
return cleanContent.includes(redirectTitle);
}
async function getRedirectHistory(title) {
const data = await fetchAPI(`action=query&prop=revisions&titles=${encodeURIComponent(title)}&rvprop=content|timestamp|ids&rvlimit=max`);
const page = Object.values(data.query.pages)[0];
if (!page?.revisions) return {};
const targets = new Map();
let lastNonRedirectRev = null;
let firstRedirectYear = null;
let firstRedirectRev = null;
// Revisions are newest first, so iterate backwards
for (let i = page.revisions.length - 1; i >= 0; i--) {
const rev = page.revisions[i];
const isRedirect = /#REDIRECT/i.test(rev['*']);
if (isRedirect) {
const redirectMatch = rev['*'].match(/#REDIRECT\s*\[\[(.*?)\]\]/i);
if (redirectMatch) {
const year = new Date(rev.timestamp).getFullYear();
targets.set(redirectMatch[1], year);
if (lastNonRedirectRev && !firstRedirectYear) {
firstRedirectRev = rev.revid;
firstRedirectYear = year;
}
}
} else {
lastNonRedirectRev = rev.revid;
}
}
return {
history: targets.size > 1 ? Array.from(targets.entries())
.map(([target, year]) => `${target} (${year})`)
.join(' → ') : null,
significantHistory: firstRedirectRev && firstRedirectYear ? {
revid: firstRedirectRev,
redirectYear: firstRedirectYear
} : null
};
}
function extractRedirectTemplates(content) {
const matches = content.match(/\{\{(?:R(?:edir(?:ect)?)?[ _](?!category\s*shell)|Redirect[ _]from)[^}|]*(?:\|[^}]*)?\}\}/gi) || [];
return matches.map(template => template.replace(/\{\{([^}|]+).*?\}\}/i, '$1').trim());
}
async function getTalkPageInfo(title) {
const talkContent = await getPageContent(`Talk:${title}`);
if (!talkContent) return {};
const rfdMatches = talkContent.match(/\{\{old ?rfd[^\}]*?\|\s*result\s*=\s*'''(.*?)'''.*}}/gi) || [];
const afdMatches = talkContent.match(/\{\{old\s*[ax]fd(?:\s+multi|\s*full)?[^\}]*?\|\s*result\s*=\s*'''(.*?)'''.*}}/gi) || [];
const mergeMatches = talkContent.match(/\{\{merged[-_ ]?(from|to)[^\}]*?\}\}/gi) || [];
return {
rfdHistory: rfdMatches.map(match => {
const pageMatch = match.match(/\|\s*page\s*=\s*([^|}\n]+)/i);
const resultMatch = match.match(/\|\s*result\s*=\s*'''(.*?)'''/i);
const page = pageMatch[1].trim();
const result = resultMatch[1].trim();
return `<a href="https://en.wikipedia.org/wiki/Wikipedia:Redirects_for_discussion/Log/${page}" target="_blank">${result}</a>`;
}),
afdHistory: afdMatches.map(match => {
const pageMatch = match.match(/\|\s*page\s*=\s*([^|}\n]+)/i);
const resultMatch = match.match(/\|\s*result\s*=\s*'''(.*?)'''/i);
const page = pageMatch ? pageMatch[1].trim() : title;
const result = resultMatch[1].trim();
return `<a href="https://en.wikipedia.org/wiki/Wikipedia:Articles_for_deletion/${page}" target="_blank">${result}</a>`;
}),
mergeProposals: mergeMatches.length
};
}
async function getMoveHistory(title) {
const data = await fetchAPI(`action=query&list=logevents&letype=move&letitle=${encodeURIComponent(title)}&leprop=title|details|timestamp`);
const events = data.query.logevents;
if (!events?.length) return null;
const moves = events.map(event =>
`${event.params.target_title} (${new Date(event.timestamp).getFullYear()})`
);
const firstYear = new Date(events[events.length - 1].timestamp).getFullYear();
moves.unshift(`${events[events.length - 1].title} (${firstYear})`);
return moves.join(' → ');
}
async function getPageViews(title) {
const end = new Date();
const start = new Date();
start.setDate(start.getDate() - 30);
const formatDate = date => date.toISOString().slice(0, 10).replace(/-/g, '');
const url = `/media/api/rest_v1/metrics/pageviews/per-article/en.wikipedia/all-access/user/${encodeURIComponent(title)}/daily/${formatDate(start)}/${formatDate(end)}`;
const response = await fetch(url);
if (!response.ok) return 0;
const data = await response.json();
return data.items?.reduce((sum, item) => sum + item.views, 0) || 0;
}
async function fetchRedirectInfo(title) {
const info = { errors: [] };
try {
const pageInfo = await getPageInfo(title);
if (pageInfo.missing) {
info.errors.push('Page not found');
return info;
}
// Creation date
if (pageInfo.revisions?.[0]?.timestamp) {
info.created = formatCreationDate(pageInfo.revisions[0].timestamp);
}
// Incoming links
info.incomingLinks = pageInfo.linkshere
? pageInfo.linkshere.filter(link => [0, 12, 100, 126].includes(link.ns)).length // Main, help, portal, MOS
: 0;
// Redirect content analysis
const content = await getPageContent(title);
const targetTitle = await getRedirectTarget(content);
if (targetTitle) {
info.targetTitle = targetTitle;
info.textMentioned = await checkTextMentioned(targetTitle, title);
}
// Redirect history
const { history, significantHistory } = await getRedirectHistory(title);
info.redirectHistory = history;
info.significantHistory = significantHistory;
// Redirect templates
info.redirectTemplates = extractRedirectTemplates(content);
// Talk page info (RfDs, AfDs, merge proposals)
Object.assign(info, await getTalkPageInfo(title));
// Move history
info.moveHistory = await getMoveHistory(title);
// Page views
info.views = await getPageViews(title);
} catch (error) {
info.errors.push(error.message);
}
return info;
}
function createInfoButton(redirectLink) {
const button = document.createElement('button');
button.textContent = 'ℹ️';
button.style.cssText = 'margin-left: 5px; font-size: 12px; padding: 2px 5px; cursor: pointer;';
const title = redirectLink.title;
button.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
button.disabled = true;
button.textContent = '⌛';
const info = await fetchRedirectInfo(title);
let infoText = `<strong>${title}</strong>`;
infoText += ` <small>(<a href="https://en.wikipedia.org/w/index.php?fulltext=1&search=%22${encodeURIComponent(title)}%22&title=Special%3ASearch&ns0=1" target="_blank">Wikipedia</a> | <a href="https://www.google.com/search?q=%22${encodeURIComponent(title)}%22" target="_blank">Google</a>)</small><div style="margin-bottom: 8px;"></div>`;
infoText += info.created ? `<b>Created:</b> ${info.created}<br>` : '';
infoText += info.incomingLinks > 0 ? `<b>Incoming links:</b> ${info.incomingLinks}<br>` : '';
infoText += info.views !== undefined ? `<b>Views (30d):</b> ${info.views}<br>` : '';
infoText += info.textMentioned !== undefined ? `<b>Mentioned in target:</b> ${info.textMentioned ? `<a href="https://en.wikipedia.org/wiki/${encodeURIComponent(info.targetTitle)}#:~:text=${title}" target="_blank">Yes</a>` : 'No'}<br>` : '';
infoText += info.redirectTemplates?.length ? `<b>Tags:</b> ${info.redirectTemplates.join(', ')}<br>` : '';
infoText += info.rfdHistory?.length ? `<b>Past RfDs:</b> ${info.rfdHistory.join(', ')}<br>` : '';
infoText += info.afdHistory?.length ? `<b>Past AfDs:</b> ${info.afdHistory.join(', ')}<br>` : '';
infoText += info.mergeProposals ? `<b>Merge proposals:</b> ${info.mergeProposals}<br>` : '';
infoText += info.moveHistory ? `<b>Move history:</b> ${info.moveHistory}<br>` : '';
infoText += info.redirectHistory ? `<b>Target history:</b> ${info.redirectHistory}<br>` : '';
infoText += info.significantHistory ? `<b>BLARed:</b> <a href="https://en.wikipedia.org/w/index.php?diff=${info.significantHistory.revid}" target="_blank">Last non-redirect revision</a> (${info.significantHistory.redirectYear})<br>` : '';
if (info.errors.length > 0) {
infoText += `Errors: ${info.errors.join(', ')}`;
}
const existingPopup = document.querySelector('.rfd-info-popup');
if (existingPopup) existingPopup.remove();
const popup = document.createElement('div');
popup.className = 'rfd-info-popup';
popup.style.cssText = 'position: fixed; background: #fff; border: 1px solid #aaa; padding: 15px; z-index: 999999; box-shadow: 2px 2px 5px rgba(0,0,0,0.2); font-size: 13px; line-height: 1.5; left: 50%; top: 50%; transform: translate(-50%, -50%); max-width: 400px; max-height: 80vh; overflow-y: auto;';
popup.innerHTML = infoText;
document.body.appendChild(popup);
const closePopup = (e) => {
if (!popup.contains(e.target) && e.target !== button) {
popup.remove();
document.removeEventListener('click', closePopup);
}
};
setTimeout(() => {
document.addEventListener('click', closePopup);
}, 100);
button.disabled = false;
button.textContent = 'ℹ️';
});
return button;
}
function addButtons() {
const redirectLinks = document.querySelectorAll('a.deletion');
redirectLinks.forEach((link) => {
if (!link.previousElementSibling || !link.previousElementSibling.classList.contains('rfd-info-button')) {
const infoButton = createInfoButton(link);
infoButton.classList.add('rfd-info-button');
link.insertAdjacentElement('beforebegin', infoButton);
}
});
}
// Do not run anywhere except RfD pages
if (!mw.config.get('wgPageName').match(/Wikipedia:Redirects_for_discussion/)) {
return;
}
// Initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', addButtons);
} else {
setTimeout(addButtons, 1000);
}
// Watch for dynamic content
const observer = new MutationObserver(() => {
addButtons();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
})();
// </nowiki>