User:StefenTower/ArticleLinkDiffs.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.
This user script seems to have a documentation page at User:StefenTower/ArticleLinkDiffs.
/*** Article Link Diffs ***/
// Automatically summarizes article links added or removed when viewing a diff page.
// Also, incomplete links will be caught and shown as errors.
// Documentation at [[en:w:User:StefenTower/ArticleLinkDiffs]]
// By [[en:w:User:StefenTower]]
(function() {
'use strict';
if (!mw.config.get('wgDiffNewId') && !window.location.search.includes('diff=')) {
return;
}
// Exit if it is a JavaScript code page diff
if (mw.config.get('wgPageName').endsWith('.js')) return;
mw.loader.using(['mediawiki.util'], function() {
var namespaceIds = mw.config.get('wgNamespaceIds') || {};
var brokenLinks = []; // Track unclosed link snippets globally within instance
// Fortified CSS selectors targeting dark, desaturated link block baselines natively
mw.util.addCSS(
'div#article-link-diff-summary a.alschip { display: inline-flex; align-items: center; background: var(--background-color-neutral-subtle, #f8f9fa) !important; border: 1px solid var(--border-color-muted, #c8ccd1) !important; border-radius: 4px !important; padding: 2px 6px !important; margin: 3px 6px 3px 0 !important; font-size: 0.9em !important; white-space: nowrap !important; color: var(--color-progressive, #36c) !important; font-weight: 500 !important; text-decoration: none !important; text-decoration-line: none !important; background-image: none !important; box-shadow: 0 1px 2px rgba(0,0,0,0.04) !important; transition: background 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; }' +
'div#article-link-diff-summary a.alschip-added:hover { background: var(--background-color-success-subtle, #d4eed8) !important; border-color: var(--border-color-success, #a3cfad) !important; box-shadow: 0 1px 0 0 var(--color-success, #137333), 0 2px 4px rgba(0,0,0,0.05) !important; color: var(--color-success, #137333) !important; text-decoration: none !important; }' +
'div#article-link-diff-summary a.alschip-removed:hover { background: var(--background-color-error-subtle, #fadbd8) !important; border-color: var(--border-color-error, #e6b0aa) !important; box-shadow: 0 1px 0 0 var(--color-error, #c5221f), 0 2px 4px rgba(0,0,0,0.05) !important; color: var(--color-error, #c5221f) !important; text-decoration: none !important; }' +
// Base Light Mode style configuration for error chips (spans)
'div#article-link-diff-summary span.alschip-error { display: inline-flex; align-items: center; background: var(--background-color-error-subtle, #fdf2f2) !important; border: 1px solid var(--border-color-error, #f8b4b4) !important; border-radius: 3px !important; padding: 1px 5px !important; margin: 1px 3px 1px 0 !important; font-family: monospace !important; font-size: 0.85em !important; color: var(--color-error, #9c1a1a) !important; white-space: nowrap !important; }' +
// --- DARK MODE COMPONENT OVERRIDES ---
// Tones down the main card summary block row container
'.mw-theme-client-dark div#article-link-diff-summary { background: #1a1b1c !important; border-color: #3a3d40 !important; }' +
'.mw-theme-client-dark div#article-link-diff-summary div { border-color: #2a2b2c !important; }' +
'.mw-theme-client-dark div#article-link-diff-summary span.alschip-error { background: #351716 !important; border-color: #54211e !important; color: #f2a6a1 !important; }'
);
function getLinkCounts(text) {
// Strip raw "File:Name.jpg|" prefixes from <gallery> rows so their captions parse like normal text
var processedText = text.replace(/^\s*(File|Image):[^|\n]+\|/gim, '');
// 1. Pre-process text to isolate inner links from File/Image captions
var extractedInnerLinks = [];
var fileRegex = /\[\[(File|Image):[^[\]]*(?:\[\[[^[\]]*\]\][^[\]]*)*\]\]/gi;
var fileMatch;
// Extract links nested inside file captions and save them
while ((fileMatch = fileRegex.exec(processedText)) !== null) {
var fileMarkup = fileMatch[0];
var innerLinks = fileMarkup.match(/\[\[(?!File:|Image:)[^\]]+\]\]/gi);
if (innerLinks) {
extractedInnerLinks = extractedInnerLinks.concat(innerLinks);
}
}
// Remove the outer File/Image blocks entirely so they don't get double-counted
var baseTextWithoutFiles = processedText.replace(fileRegex, '');
// Combine the file-free text with our single copy of the extracted caption links
var cleanText = baseTextWithoutFiles + ' \n ' + extractedInnerLinks.join(' ');
// Standard link extraction loop running on the deduplicated text pool
var linkRegex = /\[\[([^\]|]{1,150})(\|[^\]]{1,150})?\]\]/g;
var counts = {};
var match;
while ((match = linkRegex.exec(cleanText)) !== null) {
var rawTitle = match[1].trim();
// Remove leading colon (used for inline category/file links)
var cleanTitle = rawTitle.startsWith(':') ? rawTitle.slice(1) : rawTitle;
var parts = cleanTitle.split(':');
if (parts.length > 1) {
var prefix = parts[0].toLowerCase().replace(/ /g, '_');
// If the prefix is a known namespace (Category, File, User, etc.), skip it
if (namespaceIds.hasOwnProperty(prefix)) {
continue;
}
}
// Normalize: Capitalize first letter and underscores to spaces
var title = cleanTitle.charAt(0).toUpperCase() + cleanTitle.slice(1).replace(/_/g, ' ');
var display = match[2] ? match[2].substring(1).trim() : null;
var key = display ? title + "|||" + display : title;
counts[key] = (counts[key] || 0) + 1;
}
// 2. Independent scanner to parse and report broken runaway links
var idx = 0;
while ((idx = text.indexOf('[[', idx)) !== -1) {
// Lookahead check to skip media assets entirely from errors panel
var preview = text.substring(idx + 2, idx + 15).trim().toLowerCase();
if (preview.startsWith('file:') || preview.startsWith('image:')) {
idx += 2;
continue;
}
var nextClose = text.indexOf(']]', idx);
var nextOpen = text.indexOf('[[', idx + 2);
if (nextClose === -1 || (nextOpen !== -1 && nextOpen < nextClose)) {
var endPos = (nextOpen !== -1) ? nextOpen : idx + 40;
var snippet = text.substring(idx, endPos).trim();
if (snippet.length > 40) {
snippet = snippet.substring(0, 40) + '...';
}
if (!brokenLinks.includes(snippet)) {
brokenLinks.push(snippet);
}
}
idx += 2;
}
return counts;
}
// Ensure the diff table exists before proceeding
var diffTable = document.querySelector('table.diff');
if (!diffTable) return;
var oldText = Array.from(document.querySelectorAll('.diff-deletedline')).map(td => td.textContent).join(' \n ');
var newText = Array.from(document.querySelectorAll('.diff-addedline')).map(td => td.textContent).join(' \n ');
var oldCounts = getLinkCounts(oldText);
var oldErrors = [].concat(brokenLinks);
brokenLinks = [];
var newCounts = getLinkCounts(newText);
var freshErrors = brokenLinks.filter(function(err) { return !oldErrors.includes(err); });
function getDifference(source, target) {
var diff = [];
for (var key in source) {
var sourceCount = source[key];
var targetCount = target[key] || 0;
if (sourceCount > targetCount) {
var p = key.split('|||');
diff.push({ title: p[0], display: p[1] || null, count: sourceCount - targetCount });
}
}
return diff.sort((a, b) => a.title.localeCompare(b.title));
}
var added = getDifference(newCounts, oldCounts);
var removed = getDifference(oldCounts, newCounts);
if (added.length === 0 && removed.length === 0 && freshErrors.length === 0) return;
function renderList(list, typeClass) {
if (list.length === 0) return '<span style="color: var(--color-subtle, #72777d); font-style: italic; font-size: 0.9em; margin-left: 4px; align-self: center;">None</span>';
var chipsHTML = list.map(item => {
var html = '<a class="alschip ' + typeClass + '" href="' + mw.util.getUrl(item.title) + '">';
html += '<span>' + item.title + '</span>';
if (item.display && item.display !== item.title) {
html += '<span style="color: var(--color-subtle, #72777d); font-size: 0.85em; margin-left: 3px; font-weight: normal;">(as <b>“</b>' + item.display + '<b>”</b>)</span>';
}
if (item.count > 1) {
html += '<span style="background: var(--background-color-neutral, #eaecf0); color: var(--color-base, #202122); font-weight: bold; font-size: 0.8em; padding: 0 3px; border-radius: 2px; margin-left: 4px;">×' + item.count + '</span>';
}
html += '</a>';
return html;
}).join('');
return '<div style="display: inline-flex; flex-wrap: wrap; align-items: center; vertical-align: middle; flex: 1;">' + chipsHTML + '</div>';
}
var priorReport = document.getElementById('article-link-diff-summary');
if (priorReport) priorReport.remove();
var report = document.createElement('div');
report.id = 'article-link-diff-summary';
var cardStyles = 'margin: 8px 0; padding: 8px 12px; background: var(--background-color-neutral-subtle, #ffffff); border: 1px solid var(--border-color-base, #a2a9b1); border-left: 4px solid #36c; border-radius: 4px; font-family: sans-serif; box-sizing: border-box; width: 100%; line-height: 1.4; color: var(--color-base, #202122);';
report.style.cssText = cardStyles;
var totalAdded = added.reduce((sum, i) => sum + i.count, 0);
var totalRemoved = removed.reduce((sum, i) => sum + i.count, 0);
// Lightweight, uniform link emblem using style filters to ensure a sleek monochrome look
var reportHTML = '<div style="font-size: 0.95em; font-weight: bold; color: var(--color-base, #202122); margin-bottom: 4px; padding-bottom: 6px; border-bottom: 1px solid var(--border-color-subtle, #f0f1f2); display: flex; align-items: center; gap: 5px;">' +
'<span style="font-size: 1.15em; display: inline-flex; align-items: center; justify-content: center; filter: grayscale(1) brightness(1.2); opacity: 0.75; transform: rotate(-15deg); line-height: 1;">🔗</span>' +
'<span>Article Link Diff Summary</span>' +
'</div>';
reportHTML += '<div style="display: flex; margin-bottom: 3px; align-items: flex-start;">' +
'<div style="display: inline-block; width: 95px; flex-shrink: 0; background: var(--background-color-success-subtle, #e6f4ea); color: var(--color-success, #137333); font-weight: bold; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.3px; padding: 1px 4px; border-radius: 3px; text-align: center; margin-right: 6px; margin-top: 2px; white-space: nowrap;">Added (' + totalAdded + ')</div>' +
renderList(added, 'alschip-added') +
'</div>';
reportHTML += '<div style="display: flex; margin-bottom: 3px; align-items: flex-start;">' +
'<div style="display: inline-block; width: 95px; flex-shrink: 0; background: var(--background-color-error-subtle, #fce8e6); color: var(--color-error, #c5221f); font-weight: bold; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.3px; padding: 1px 4px; border-radius: 3px; text-align: center; margin-right: 6px; margin-top: 2px; white-space: nowrap;">Removed (' + totalRemoved + ')</div>' +
renderList(removed, 'alschip-removed') +
'</div>';
if (freshErrors.length > 0) {
report.style.borderLeftColor = '#d33';
var errorChips = freshErrors.map(function(e) {
return '<span class="alschip-error">' + mw.html.escape(e) + '</span>';
}).join('');
reportHTML += '<div style="display: flex; margin-top: 4px; padding-top: 4px; border-top: 1px dashed #eaecf0; align-items: flex-start;">' +
'<div style="display: inline-block; width: 95px; flex-shrink: 0; background: var(--background-color-notice-subtle, #feefe3); color: var(--color-notice, #b06000); font-weight: bold; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.3px; padding: 1px 4px; border-radius: 3px; text-align: center; margin-right: 6px; margin-top: 2px; white-space: nowrap;">⚠️ Errors (' + freshErrors.length + ')</div>' +
'<div style="display: inline-flex; flex-wrap: wrap; align-items: center; vertical-align: middle; flex: 1;">' + errorChips + '</div>' +
'</div>';
}
report.innerHTML = reportHTML;
diffTable.parentNode.insertBefore(report, diffTable);
mw.hook('wikipage.content').fire($(report));
});
})();