User:Daniel Quinlan/Scripts/FilterDiff.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 user script seems to have a documentation page at User:Daniel Quinlan/Scripts/FilterDiff. |
'use strict';
mw.loader.using(['mediawiki.util']).then(function () {
if (mw.config.get('wgCanonicalSpecialPageName') != "AbuseFilter")
return;
const page = mw.config.get('wgPageName');
const diffMatch = page.match(/\/history\/\d+\/diff\/\w+\/\w+/);
if (!diffMatch)
return;
const context = 3;
let displayFullContext = false;
mw.loader.addStyleTag(`
table.wikitable .diff-toggle-button-wrapper {
position: relative;
}
table.wikitable .diff-toggle-button {
position: absolute;
right: 0em;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
color: var(--color-subtle, gray);
display: flex;
padding: 0px;
cursor: pointer;
}
table.wikitable .diff-toggle-button:hover,
table.wikitable .diff-toggle-button:focus-visible {
color: var(--color-base--hover, gray);
}
table.wikitable tr:not(.mw-abusefilter-diff-header) > th:first-child {
display: none;
}
.diff col.diff-line-number { width: 3.5%; }
.diff col.diff-marker { width: 1.5%; }
.diff col.diff-content { width: 45%; }
.diff td.diff-line-number { position: relative; }
.diff td.diff-line-number::after {
content: attr(data-line-number);
position: absolute;
right: 0.3em;
top: 50%;
transform: translateY(-50%);
font-size: smaller;
font-family: monospace;
color: gray;
}
.diff td.diff-marker { font-size: 1em; }
.diff:not(.diff-full-context) .context-separator-above td {
border-bottom: 3px dotted var(--border-color-disabled, gray);
}
.diff:not(.diff-full-context) .context-separator-below td {
border-top: 3px dotted var(--border-color-disabled, gray);
}
.diff:not(.diff-full-context) tr.context-distant { display: none; }
`);
function processDiffTable(diffTable, lineNumbering) {
const rows = diffTable.querySelectorAll('tr');
const types = [];
// add columns
const colgroup = diffTable.querySelector('colgroup');
if (colgroup) {
const markerCol = colgroup.querySelector('.diff-marker');
const contentCol = colgroup.querySelector('.diff-content');
if (markerCol) {
const newCol = document.createElement('col');
newCol.className = 'diff-line-number';
colgroup.insertBefore(newCol, markerCol);
}
if (contentCol) {
const newCol = document.createElement('col');
newCol.className = 'diff-line-number';
contentCol.after(newCol);
}
}
// line numbering
let lineNumberDeleted = 0;
let lineNumberAdded = 0;
for (const row of rows) {
// add first line number as first column
const tdDel = document.createElement('td');
tdDel.className = 'diff-line-number';
row.insertBefore(tdDel, row.firstChild);
// add second line number as fourth column
const tdAdd = document.createElement('td');
let colCount = 0;
for (const td of row.querySelectorAll('td')) {
const colspan = parseInt(td.getAttribute('colspan') || '1', 10);
colCount += colspan;
if (colCount >= 3) {
tdAdd.className = 'diff-line-number';
td.after(tdAdd);
break;
}
}
// add line number attributes
if (lineNumbering) {
const deleted = row.querySelector('.diff-side-deleted');
const added = row.querySelector('.diff-side-added');
if (deleted && deleted.children.length > 0)
tdDel.setAttribute('data-line-number', ++lineNumberDeleted);
if (added && added.children.length > 0)
tdAdd.setAttribute('data-line-number', ++lineNumberAdded);
}
// classify row
let type = 'other';
const sides = row.querySelectorAll('td.diff-side-deleted, td.diff-side-added');
if (sides.length === 2) {
const oldCell = sides[0];
const newCell = sides[1];
const oldExists = oldCell.children.length > 0;
const newExists = newCell.children.length > 0;
if (oldExists && newExists && oldCell.textContent === newCell.textContent)
type = 'identical';
else
type = 'changed';
}
types.push(type);
}
// compute distances
const n = rows.length;
const distances = Array(n).fill(Infinity);
let lastChanged = -Infinity;
// forward pass
for (let i = 0; i < n; i++) {
if (types[i] === 'changed')
lastChanged = i;
else if (types[i] === 'identical')
distances[i] = i - lastChanged;
}
// backward pass
lastChanged = Infinity;
for (let i = n - 1; i >= 0; i--) {
if (types[i] === 'changed')
lastChanged = i;
else if (types[i] === 'identical')
distances[i] = Math.min(distances[i], lastChanged - i);
}
// apply classes
for (let i = 0; i < n; i++) {
const row = rows[i];
const distance = distances[i];
if (types[i] === 'identical') {
// add border classes as separators
if (distance === context) {
const prev = i > 0 ? distances[i - 1] : null;
const next = i + 1 < n ? distances[i + 1] : null;
if (prev === context + 1) row.classList.add('context-separator-below');
if (next === context + 1) row.classList.add('context-separator-above');
}
// hide distant rows
if (distance > context)
row.classList.add('context-distant');
}
}
}
function addHeaderToggle(header, diffTable) {
const expandIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="currentColor"><circle cx="2" cy="12" r="1"/><circle cx="6" cy="12" r="1"/><circle cx="10" cy="12" r="1"/><circle cx="14" cy="12" r="1"/><circle cx="18" cy="12" r="1"/><circle cx="22" cy="12" r="1"/></g><path d="M12 9V1M12 1L9 5M12 1L15 5M12 15V23M12 23L9 19M12 23L15 19" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"/></svg>';
const collapseIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="currentColor"><circle cx="2" cy="1" r="1"/><circle cx="6" cy="1" r="1"/><circle cx="10" cy="1" r="1"/><circle cx="14" cy="1" r="1"/><circle cx="18" cy="1" r="1"/><circle cx="22" cy="1" r="1"/><circle cx="2" cy="23" r="1"/><circle cx="6" cy="23" r="1"/><circle cx="10" cy="23" r="1"/><circle cx="14" cy="23" r="1"/><circle cx="18" cy="23" r="1"/><circle cx="22" cy="23" r="1"/></g><path d="M12 3.25V10.5M12 10.5L9 6.5M12 10.5L15 6.5M12 20.75V13.5M12 13.5L9 17.5M12 13.5L15 17.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"/></svg>';
const wrapper = document.createElement('div');
wrapper.className = 'diff-toggle-button-wrapper';
while (header.firstChild)
wrapper.appendChild(header.firstChild);
header.appendChild(wrapper);
const button = document.createElement('button');
button.className = 'diff-toggle-button';
button.innerHTML = expandIcon;
button.title = 'Expand';
button.addEventListener('click', (e) => {
e.stopPropagation();
const expanded = diffTable.classList.toggle('diff-full-context');
button.innerHTML = expanded ? collapseIcon : expandIcon;
button.title = expanded ? 'Collapse' : 'Expand';
});
wrapper.appendChild(button);
}
for (const wikiTable of document.querySelectorAll('table.wikitable')) {
const rows = wikiTable.querySelectorAll('tr');
let header = null;
for (const row of rows) {
if (row.classList.contains('mw-abusefilter-diff-header')) {
header = row.querySelector('th');
}
const diff = row.querySelector('table.diff');
if (diff) {
const labelText = row.querySelector('th:first-child')?.textContent || '';
const lineNumbering = !/^(?:actions|description|flags)\b/i.test(labelText);
processDiffTable(diff, lineNumbering);
if (header && diff.querySelector('tr.context-distant')) {
addHeaderToggle(header, diff);
header = null;
}
}
}
}
});