mic_none

User:Daniel Quinlan/Scripts/FilterDiff.js Source: en.wikipedia.org/wiki/User:Daniel_Quinlan/Scripts/FilterDiff.js

'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;
				}
			}
		}
	}
});