User:Daniel Quinlan/Scripts/FilterDiff.js

'use strict'; mw.loader.using(['mediawiki.util']).then(function () { if (mw.config.get('wgCanonicalSpecialPageName') != "AbuseFil

'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;
	let initialRun = true;

	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.rows;
		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.cells) {
				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);
	}

	function processTable(table) {
		if (table.dataset.filterDiffProcessed) return;
		table.dataset.filterDiffProcessed = 'true';
		let header = null;
		for (const row of table.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;
				}
			}
		}
	}

	function processDiffs(root) {
		if (root.matches('table.wikitable')) {
			processTable(root);
		} else {
			for (const table of root.querySelectorAll('table.wikitable')) {
				processTable(table);
			}
		}
	}

	function execute($content) {
		if (initialRun) {
			initialRun = false;
			processDiffs(document.body);
		} else if ($content?.length) {
			for (const node of $content) {
				if (node?.nodeType === Node.ELEMENT_NODE) {
					processDiffs(node);
				}
			}
		}
	}

	mw.hook('wikipage.content').add(execute);
});

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.