_ _    _ _____  ___   __                       
 __      _(_) | _(_)___ / ( _ ) / /_   ___ ___  _ __ ___  
 \ \ /\ / / | |/ / | |_ \ / _ \| '_ \ / __/ _ \| '_ ` _ \ 
  \ V  V /| |   <| |___) | (_) | (_) | (_| (_) | | | | | |
   \_/\_/ |_|_|\_\_|____/ \___/ \___(_)___\___/|_| |_| |_|

User:GeneralNotability/edit-filter-hit-analyzer.js

In the modern world, User:GeneralNotability/edit-filter-hit-analyzer.js has become a topic of great interest and debate. From its origins to its impact on today's society, User:GeneralNotability/edit-filter-hit-analyzer.js has been the subject of study and analysis by experts in various fields. Its relevance transcends borders and cultures, since its influence is felt in all areas of daily life. In this article, we will explore the different aspects related to User:GeneralNotability/edit-filter-hit-analyzer.js, from its origins to its evolution today. Through a rigorous and detailed analysis, we will seek to better understand the importance of User:GeneralNotability/edit-filter-hit-analyzer.js in today's society and its projection in the future.
// <nowiki>
// @ts-check
// More information on how an edit filter was tripped

importStylesheet('w:en:User:GeneralNotability/edit-filter-hit-analyzer.css' );
/**
 * @typedef EditFilterLine
 * @type {Object}
 * @property {string} text Text of the line
 * @property {string} normedText Text with some modifications applied for parsing
 * @property {string} variables Variables found in the line
 * @property {number} indentation how far to indent the line
 */

const efa_knownVars = {};

const efa_PAGE_NAME_RE = /Special:AbuseLog\/\d+/;
const efa_FILTER_PAGE_RE = /\/wiki\/(Special:AbuseFilter\/(\d+))/;
// Vars in this list shouldn't have their full content displayed because they're usually really big
const efa_HIDDEN_VARS = ;

// Parser regexes
const efa_REGEX_ASSIGNMENT_RE = /(\w+)\s*:=\s*"(.*)"/;
const efa_RLIKE_RE = /\b(.*)\s*(i?rlike|regex)\s*(\w+|".*")/;

// Future reference: if we want to do ccnorm ourselves, we can pull the conversion list
// from https://phab.wmfusercontent.org/file/data/jcoued3dziiwwwdr53lp/PHID-FILE-lkxia6juxnhqt263dbrj/equivset.json

async function efa_main() {
	// populate knownVars with built-in values
	Object.entries(mw.config.get('wgAbuseFilterVariables')).forEach(() => {
		efa_knownVars = value;
	});

	const $actionParams = $('h3:contains("Action parameters")', document);
	$('<h3>').text('Filter rule analysis').insertBefore($actionParams);
	const $ruleAnchor = $('<ul>').attr('id', 'efa-anchor').insertBefore($actionParams);
	// Find the link which goes to Special:AbuseFilter, then pull out the wikilink part of it
	const filterId = $('a', document).filter(function () {
		return efa_FILTER_PAGE_RE.test(this.getAttribute('href'));
	}).attr('href').match(efa_FILTER_PAGE_RE);
	const filterPattern = await efa_getFilter(filterId);
	if (!filterPattern) {
		// Something went wrong (or we can't access the filter),
		// bail out
		return;
	}
	const filterRules = efa_parseRules(filterPattern);
	filterRules.forEach((rule) => {
		const $bullet = $('<li>').attr('style', 'margin-left:' + (10 * rule.indentation + 10) + 'px;');
		$('<span>').addClass('efa-rule').text(rule.text).appendTo($bullet);
		rule.variables.forEach((variable) => {
			const $efaData = $('<span>').addClass('efa-data');
			if (efa_HIDDEN_VARS.includes(variable)) {
				$efaData.append(variable + ': (not shown)');
			} else {
				$efaData.append(variable + ': ' + efa_knownVars);
			}
			$efaData.appendTo($bullet);
		});

		const rlikeMatch = rule.normedText.match(efa_RLIKE_RE);
		if (rlikeMatch) {
			// If this is a regex, try to expand it and generate a link
			let reText = rlikeMatch;
			const matchType = rlikeMatch;
			let re = rlikeMatch;
			// Whether to apply substitution on the regex side (don't if )
			let subRe = true;
			const reQuoteSearch = re.match(/.*?"(.*)"/);
			if (reQuoteSearch) {
				// Remove the quotes around a literal regex
				re = reQuoteSearch;
				// Don't attempt substitution since this is a literal
				subRe = false;
			}
			// Expand variables (or possibly function calls on a variable)
			// TODO: this is really simplistic (obviously) - strip function calls and get
			// an exact match
			for (const entry of Object.entries(efa_knownVars)) {
				if (reText.includes(entry)) {
					reText = entry.toString();
				}
				if (re.includes(entry) && subRe) {
					re = entry.toString();
				}
			}
			// abusefilter entries are PCRE and by default use the 'u' flag.
			// if irlike is being used, add the i flag as well.
			let flags = 'u';
			if (matchType === 'irlike') {
				flags += 'i';
			}
			const re101url = `https://regex101.com/?regex=${encodeURIComponent(re)}&testString=${encodeURIComponent(reText)}&flags=${flags}`;
			$bullet.append(' ').append($('<a>').attr('href', re101url).text('(view at regex101)'));
		}
		$bullet.appendTo($ruleAnchor);
	});
}

/**
 * Turn a filter's pattern into a list of rules
 *
 * @param {string} pattern Original text pattern
 *
 * @return {EditFilterLine} List of rules
 */
function efa_parseRules(pattern) {
	// Strip all newline characters and split by statement
	// The second part is taken from https://stackoverflow.com/questions/11502598/how-to-match-something-with-regex-that-is-not-between-two-special-characters
	// It matches all split characters (&, ;, &) as long as they are _not_ between quotes
	const filterLines = pattern.replace(/(\r|\n)/g, '').split(/((?=(?:*"*")**$))/g);
	/** @type {EditFilterLine} */
	const annotatedFilterLines = ;
	filterLines.forEach((line) => {
		// Trim, then replace long whitespaces with a single space
		const cleanedUpLine = line.trim().replace(/\s+/, ' ');
		const annotatedLine = { text: cleanedUpLine, normedText: cleanedUpLine,
			variables: , indentation: 0 };
		annotatedFilterLines.push(annotatedLine);
	});

	// Indentation pass: figure out how deep each statement is nested in parens,
	// then create a "normed" version which strips the extra paren(s)
	// While we're in there, save variable assignments
	let indent = 0;
	annotatedFilterLines.forEach((line) => {
		const openParens = line.text.split(/\(/).length;
		const closeParens = line.text.split(/\)/).length;
		// Because of how we split the strings, a block of indented text will
		// always start with an extra open paren on the starting rule, and close
		// with an extra one on the ending rule (but we want both of those lines)
		// indented
		const deltaParens = openParens - closeParens;
		if (deltaParens > 0) {
			indent += deltaParens;
			line.indentation = indent;
			// Remove the extra paren from the normed text
			line.normedText = line.text.replace('(', '');
		} else if (deltaParens < 0) {
			line.indentation = indent;
			indent += deltaParens; // Remember, deltaparens is negative here, so add it
			// Remove the extra paren from the normed text
			line.normedText = line.text.replace(/\)(?=*$)/, '');
		} else {
			line.indentation = indent;
		}
		const varAssignment = line.normedText.match(efa_REGEX_ASSIGNMENT_RE);
		if (varAssignment) {
			efa_knownVars] = varAssignment;
		}
	});

	// Annotate by going through and identifying variables used in the lines
	Object.keys(efa_knownVars).forEach((varName) => {
		const varRe = new RegExp('\\b' + varName + '\\b');
		annotatedFilterLines.forEach((line) => {
			if (line.text.match(varRe)) {
				const assignmentMatch = line.text.match(efa_REGEX_ASSIGNMENT_RE);
				if (assignmentMatch && assignmentMatch === varName) {
					// Don't list the variable on the line that assigns
					return;
				}
				line.variables.push(varName);
			}
		});
	});
	return annotatedFilterLines;
}

async function efa_getFilter(filterId) {
	try {
		const api = new mw.Api();
		const response = await api.get({
			action: 'query',
			list: 'abusefilters',
			abfstartid: filterId,
			abfendid: filterId,
			abfprop: 'pattern'
		});
		if (response.query.abusefilters.length < 1) {
			// No match?
			return '';
		}
		return response.query.abusefilters.pattern;

	} catch (error) {
		console.log(error);
		return '';
	}
}

// On document load, check if this page is a edit filter hit - if so,
// load the EF stuff
$(function () {
	if (efa_PAGE_NAME_RE.test(mw.config.get('wgPageName'))) {
		efa_main();
	}
});
// </nowiki>