From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.

//<nowiki>

/* jshint esversion: 11, esnext: false */

/******/ (() => { // webpackBootstrap

/******/ 	"use strict";

/******/ 	// The require scope

/******/ 	var __webpack_require__ = {};

/******/ 	

/************************************************************************/

/******/ 	/* webpack/runtime/define property getters */

/******/ 	(() => {

/******/ 		// define getter functions for harmony exports

/******/ 		__webpack_require__.d = (exports, definition) => {

/******/ 			for(var key in definition) {

/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {

/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definitionkey });

/******/ 				}

/******/ 			}

/******/ 		};

/******/ 	})();

/******/ 	

/******/ 	/* webpack/runtime/hasOwnProperty shorthand */

/******/ 	(() => {

/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))

/******/ 	})();

/******/ 	

/******/ 	/* webpack/runtime/make namespace object */

/******/ 	(() => {

/******/ 		// define __esModule on exports

/******/ 		__webpack_require__.r = (exports) => {

/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {

/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });

/******/ 			}

/******/ 			Object.defineProperty(exports, '__esModule', { value: true });

/******/ 		};

/******/ 	})();

/******/ 	

/************************************************************************/

var __webpack_exports__ = {};

// ESM COMPAT FLAG

__webpack_require__.r(__webpack_exports__);



// EXPORTS

__webpack_require__.d(__webpack_exports__, {

  setup: () => (/* binding */ setup)

});



;// CONCATENATED MODULE: ./src/filter.js

class FilterEvaluator {

	constructor(options) {

		let blob = new Blob(['importScripts("https://en.wikipedia.org/?title=User:Suffusion_of_Yellow/fdb-worker.js&action=raw&ctype=text/javascript");'], { type: "text/javascript" });



		this.version = 0;

		this.uid = 0;

		this.callbacks = {};

		this.status = options.status || (() => null);

		this.workers = [];

		this.threads = Math.min(Math.max(options.threads || 1, 1), 16);



		this.status("Starting workers...");



		let channels = [];

		for (let i = 0; i < this.threads - 1; i++)

			channels.push(new MessageChannel());



		for (let i = 0; i < this.threads; i++) {

			this.workersi = new Worker(URL.createObjectURL(blob), { type: 'classic' });



			this.workersi].onmessage = (event) => {

				if (this.status && event.data.status)

					this.status(event.data.status);



				if (event.data.uid && this.callbacksevent.data.uid]) {

					this.callbacksevent.data.uid](event.data);

					delete this.callbacksevent.data.uid];

				}

			};



			if (i == 0) {

				if (this.threads > 1)

					this.workersi].postMessage({

						action: "setsecondaries",

						ports: channels.map(c => c.port1)

					}, channels.map(c => c.port1));

			} else {

				this.workersi].postMessage({

					action: "setprimary",

					port: channelsi - 1].port2

				}, channelsi - 1].port2]);

			}

		}

	}



	work(data, i = 0) {

		return new Promise((resolve) => {

			data.uid = ++this.uid;

			this.callbacksthis.uid = (data) => resolve(data);



			this.workersi].postMessage(data);

		});

	}



	terminate() {

		this.workers.forEach(w => w.terminate());

	}



	async getBatch(params) {

		for (let i = 0; i < this.threads; i++)

			this.work({

				action: "clearallvardumps",

			}, i);



		let response = (await this.work({

			action: "getbatch",

			params: params,

			stash: true

		}));



		this.batch = response.batch || [];

		this.owners = response.owners;



		return this.batch;

	}



	async getVar(name, id) {

		let response = await this.work({

			action: "getvar",

			name: name,

			vardump_id: id

		}, this.ownersid]);



		return response.vardump;

	}

	async getDiff(id) {

		let response = await this.work({

			action: "diff",

			vardump_id: id

		}, this.ownersid]);



		return response.diff;

	}



	async createDownload(fileHandle, compress = true) {

		let encoder = new TextEncoderStream() ;

		let writer = encoder.writable.getWriter();



		(async() => {

			await writer.write("[\n");



			for (let i = 0; i < this.batch.length; i++) {

				let entry = {

					...this.batchi],

					...{

						details: await this.getVar("*", this.batchi].id)

					}

				};

				this.status(`Writing entries... (${i}/${this.batch.length})`);



				await writer.write(JSON.stringify(entry, null, 2).replace(/^/gm, "  "));

				await writer.write(i == this.batch.length - 1 ? "\n]\n" : ",\n");

			}



			await writer.close();

		})();



		let output = encoder.readable;



		if (compress)

			output = output.pipeThrough(new CompressionStream("gzip"));



		if (fileHandle) {

			await output.pipeTo(await fileHandle.createWritable());



			this.status(`Created ${(await fileHandle.getFile()).size} byte file`);

		} else {

			let compressed = await (new Response(output).blob());



			this.status(`Created ${compressed.size} byte file`);



			return URL.createObjectURL(compressed);

		}

	}



	async evalBatch(text, scmode) {

		if (!this.batch)

			return [];



		let version = ++this.version;



		text = text.replaceAll("\r\n", "\n");



		for (let i = 1; i < this.threads; i++)

			this.work({

				action: "setfilter",

				filter_id: 1,

				filter: text,

			}, i);

		let response = await this.work({

			action: "setfilter",

			filter_id: 1,

			filter: text,

		}, 0);



		// Leftover response from last batch

		if (this.version != version)

			return [];



		if (response.error)

			throw response;



		let promises = [], tasks = Array(this.threads).fill().map(() => []);



		for (let entry of this.batch) {

			let task = { entry };



			promises.push(new Promise((resolve) => task.callback = resolve));



			tasksthis.ownersentry.id]].push(task);

		}



		for (let i = 0; i < this.threads; i++)

			(async() => {

				for (let task of tasksi]) {

					let response = await this.work({

						action: "evaluate",

						filter_id: 1,

						vardump_id: task.entry.id,

						scmode: scmode

					}, i);



					if (this.version != version)

						return;



					task.callback(response);

				}

			})();



		return promises;

	}

}







;// CONCATENATED MODULE: ./src/parserdata.js

const parserData = {

	functions: "bool|ccnorm_contains_all|ccnorm_contains_any|ccnorm|contains_all|contains_any|count|equals_to_any|float|get_matches|int|ip_in_range|ip_in_ranges|lcase|length|norm|rcount|rescape|rmdoubles|rmspecials|rmwhitespace|sanitize|set|set_var|specialratio|string|strlen|strpos|str_replace|str_replace_regexp|substr|ucase",

	operators: "==?=?|!==?|!=|\\+|-|/|%|\\*\\*?|<=?|>=?|\\(|\\)|\\[|\\]|&|\\||\\^|!|:=?|\\?|;|,",

	keywords: "contains|in|irlike|like|matches|regex|rlike|if|then|else|end",

	variables: "accountname|action|added_lines|added_lines_pst|added_links|all_links|edit_delta|edit_diff|edit_diff_pst|file_bits_per_channel|file_height|file_mediatype|file_mime|file_sha1|file_size|file_width|global_user_editcount|global_user_groups|moved_from_age|moved_from_first_contributor|moved_from_id|moved_from_last_edit_age|moved_from_namespace|moved_from_prefixedtitle|moved_from_recent_contributors|moved_from_restrictions_create|moved_from_restrictions_edit|moved_from_restrictions_move|moved_from_restrictions_upload|moved_from_title|moved_to_age|moved_to_first_contributor|moved_to_id|moved_to_last_edit_age|moved_to_namespace|moved_to_prefixedtitle|moved_to_recent_contributors|moved_to_restrictions_create|moved_to_restrictions_edit|moved_to_restrictions_move|moved_to_restrictions_upload|moved_to_title|new_content_model|new_html|new_pst|new_size|new_text|new_wikitext|oauth_consumer|old_content_model|old_links|old_size|old_wikitext|page_age|page_first_contributor|page_id|page_last_edit_age|page_namespace|page_prefixedtitle|page_recent_contributors|page_restrictions_create|page_restrictions_edit|page_restrictions_move|page_restrictions_upload|page_title|removed_lines|removed_links|summary|timestamp|tor_exit_node|user_age|user_app|user_blocked|user_editcount|user_emailconfirm|user_groups|user_mobile|user_name|user_rights|user_type|wiki_language|wiki_name",

	deprecated: "article_articleid|article_first_contributor|article_namespace|article_prefixedtext|article_recent_contributors|article_restrictions_create|article_restrictions_edit|article_restrictions_move|article_restrictions_upload|article_text|moved_from_articleid|moved_from_prefixedtext|moved_from_text|moved_to_articleid|moved_to_prefixedtext|moved_to_text",

	disabled: "minor_edit|old_html|old_text"

};



;// CONCATENATED MODULE: ./src/Hit.js

/* globals mw */



function sanitizedSpan(text, classList) {

	let span = document.createElement('span');



	span.textContent = text;



	if (classList)

		span.classList = classList;



	return span.outerHTML;

}



// @vue/component

/* harmony default export */ const Hit = ({

	inject: "shared"],

	props: {

		entry: {

			type: Object,

			required: true

		},

		type: {

			type: String,

			required: true

		},

		matchContext: {

			type: Number,

			default: 10

		},

		diffContext: {

			type: Number,

			default: 25

		},

		header: Boolean

	},

	data() {

		return {

			vars: {},

			diff: []

		};

	},

	computed: {

		id() {

			return this.entry.id;

		},

		difflink() {

			return this.entry.filter_id == 0 ?

				mw.util.getUrl("Special:Diff/" + this.entry.revid) :

				mw.util.getUrl("Special:AbuseLog/" + this.entry.id);

		},

		userlink() {

			return this.entry.filter_id == 0 ?

				mw.util.getUrl("Special:Contribs/" + mw.util.wikiUrlencode(this.entry.user)) :

				new mw.Uri(mw.config.get('wgScript')).extend({

					title: "Special:AbuseLog",

					wpSearchUser: this.entry.user

				});

		},

		pagelink() {

			return this.entry.filter_id == 0 ?

				mw.util.getUrl("Special:PageHistory/" + mw.util.wikiUrlencode(this.entry.title)) :

				new mw.Uri(mw.config.get('wgScript')).extend({

					title: "Special:AbuseLog",

					wpSearchTitle: this.entry.title

				});

		},

		result() {

			return JSON.stringify(this.entry.testresult.result, null, 2);

		},

		vardump() {

			return JSON.stringify(this.vars || null, null, 2);

		},

		vartext() {

			return JSON.stringify(this.vars?.[this.type.slice(4)] ?? null, null, 2);

		},

		matches() {

			let html = "";

			for (let log of this.entry.testresult.log || []) {

				for (let matchinfo of log.details?.matches ?? []) {

					let input = log.details.inputsmatchinfo.arg_haystack];



					let start = Math.max(matchinfo.match0 - this.matchContext, 0);

					let end = Math.min(matchinfo.match1 + this.matchContext, input.length);



					let pre = (start == 0 ? "" : "...") + input.slice(start, matchinfo.match0]);

					let post = input.slice(matchinfo.match1], end) + (end == input.length ? "" : "...");

					let match = input.slice(matchinfo.match0], matchinfo.match1]);



					html += '<div class="fdb-matchresult">' +

						sanitizedSpan(pre) +

						sanitizedSpan(match, "fdb-matchedtext") +

						sanitizedSpan(post) +

						'</div>';

				}

			}

			return html;

		},

		prettydiff() {

			let html = '<div class="fdb-diff">';

			for (let i = 0; i < this.diff.length; i++) {

				let hunk = this.diffi];

				if (hunk0 == -1)

					html += sanitizedSpan(hunk1], "fdb-removed");

				else if (hunk0 == 1)

					html += sanitizedSpan(hunk1], "fdb-added");

				else {

					let common = hunk1];

					if (i == 0) {

						if (common.length > this.diffContext)

							common = "..." + common.slice(-this.diffContext);

					} else if (i == this.diff.length - 1) {

						if (common.length > this.diffContext)

							common = common.slice(0, this.diffContext) + "...";

					} else {

						if (common.length > this.diffContext * 2)

							common = common.slice(0, this.diffContext) + "..." + common.slice(-this.diffContext);

					}

					html += sanitizedSpan(common);

				}

			}

			html += "</div>";

			return html;

		},

		cls() {

			if (!this.header)

				return "";

			if (this.entry.testresult === undefined)

				return 'fdb-undef';

			if (this.entry.testresult.error)

				return 'fdb-error';

			if (this.entry.testresult.result)

				return 'fdb-match';

			return 'fdb-nonmatch';

		}

	},

	watch: {

		id: {

			handler() {

				this.getAsyncData();

			},

			immediate: true

		},

		type: {

			handler() {

				this.getAsyncData();

			},

			immediate: true

		}

	},

	methods: {

		async getAsyncData() {

			if (this.type == "vardump")

				this.vars = await this.shared.evaluator.getVar("*", this.entry.id);

			else if (this.type.slice(0, 4) == "var-")

				this.vars = await this.shared.evaluator.getVar(this.type.slice(4), this.entry.id);

			else {

				this.vars = {};

				if (this.type == "diff")

					this.diff = await this.shared.evaluator.getDiff(this.entry.id);

				else

					this.diff = "";

			}

		}

	},

	template: `

<div class="fdb-hit" :class="cls">

  <div v-if="header"><a :href="difflink">{{entry.time}}</a> | <a :href="userlink">{{entry.user}}</a> | <a :href="pagelink">{{entry.title}}</a></div><div v-if="entry.testresult && entry.testresult.error && (type == 'result' || type == 'matches')">{{entry.testresult.error}}</div>

  <div v-else-if="entry.testresult && type == 'result'">{{result}}</div>

  <div v-else-if="entry.testresult && type == 'matches'" v-html="matches"></div>

  <div v-else-if="type == 'diff'" v-html="prettydiff"></div>

  <div v-else-if="type == 'vardump'">{{vardump}}</div>

  <div v-else-if="type != 'none' && type != 'matches' && type != 'result'">{{vartext}}</div>

</div>`

});





;// CONCATENATED MODULE: ./src/Batch.js





// @vue/component

/* harmony default export */ const Batch = ({

	components: { Hit: Hit },

	props: {

		batch: {

			type: Array,

			required: true

		},

		dategroups: {

			type: Array,

			required: true

		},

		type: {

			type: String,

			required: true

		}

	},

	emits: 'selecthit'],

	data() {

		return {

			selectedHit: 0

		};

	},

	methods: {

		selectHit(hit) {

			this.selectedHit = hit;

			this.$refs"idx-" + this.selectedHit][0].$el.focus();

			this.$emit('selecthit', this.selectedHit);

		},

		nextHit() {

			this.selectHit((this.selectedHit + 1) % this.batch.length);

		},

		prevHit() {

			this.selectHit((this.selectedHit - 1 + this.batch.length) % this.batch.length);

		}

	},

	template: `

<div v-for="dategroup of dategroups" class="fdb-dategroup">

  <div class="fdb-dateheader">{{dategroup.date}}</div>

    <hit v-for="entry of dategroup.batch" tabindex="-1" @focus="selectHit(entry)" @keydown.arrow-down.prevent="nextHit" @keydown.arrow-up.prevent="prevHit" :key="batch[entry].id" :ref="'idx-' + entry" :entry="batch[entry]" :type="type" header></hit>

  </div>

</div>

`

});



;// CONCATENATED MODULE: ./src/Editor.js

/* globals mw, ace */





// @vue/component

/* harmony default export */ const Editor = ({

	props: {

		wrap: Boolean,

		ace: Boolean

	},

	emits: "textchange"],

	data() {

		return {

			session: null,

			timeout: 0,

			text: ""

		};

	},

	watch: {

		wrap() {

			this.session.setOption("wrap", this.wrap);

		},

		ace() {

			if (this.ace)

				this.session.setValue(this.text);

			else

				this.text = this.session.getValue();

		},

		text() {

			clearTimeout(this.timeout);



			this.timeout = setTimeout(() => this.$emit('textchange', this.text), 50);

		}

	},

	async mounted() {

		let config = { ...parserData, aceReadOnly: false };



		mw.config.set("aceConfig", config);

		ace.config.set('basePath', mw.config.get('wgExtensionAssetsPath') + "/CodeEditor/modules/lib/ace");



		let editor = ace.(this.$refs.aceEditor);

		this.session = editor.getSession();



		this.session.setMode("ace/mode/abusefilter");

		this.session.setUseWorker(false);

		ace.require('ace/range');



		let observer = new ResizeObserver(() => editor.resize());

		observer.observe(this.$refs.aceEditor);



		this.session.setValue(this.text);

		this.session.on("change", () => this.text = this.session.getValue());

	},

	methods: {

		async loadFilter(id, revision, overwrite = true, status) {

			if (!overwrite && this.text.trim() !== "")

				return;



			let filterText = "";



			if (/^[0-9]+$/.test(id) && /^[0-9]+$/.test(revision)) {

				try {

					// Why isn't this possible through the API?

					let title = `Special:AbuseFilter/history/${id}/item/${revision}?safemode=1&useskin=fallback&uselang=qqx`;

					let url = mw.config.get('wgArticlePath').replace("$1", title);

					let response = await fetch(url);

					let text = await response.text();

					let html = (new DOMParser()).parseFromString(text, "text/html");

					let exported = html.querySelector('#mw-abusefilter-export textarea').value;

					let parsed = JSON.parse(exported);



					filterText = parsed.data.rules;

				} catch (error) {

					status(`Failed to fetch revision ${revision} of filter ${id}`);

					return false;

				}

			} else {

				try {

					let filter = await (new mw.Api()).get({

						action: "query",

						list: "abusefilters",

						abfstartid: id,

						abflimit: 1,

						abfprop: "pattern"

					});



					filterText = filter.query.abusefilters0].pattern;

				} catch (error) {

					status(`Failed to fetch filter ${id}`);



					return false;

				}

			}



			this.text = filterText;

			if (this.session)

				this.session.setValue(this.text);



			return true;

		},

		getPos(index) {

			let len, pos = { row: 0, column: 0 };



			while (index > (len = this.session.getLine(pos.row).length)) {

				index -= len + 1;

				pos.row++;

			}

			pos.column = index;



			return pos;

		},

		clearAllMarkers() {

			let markers = this.session.getMarkers();

			for (let id of Object.keys(markers))

				if (markersid].clazz.includes("fdb-"))

					this.session.removeMarker(id);

		},

		markRange(start, end, cls) {

			let startPos = this.getPos(start);

			let endPos = this.getPos(end);

			let range = new ace.Range(startPos.row, startPos.column, endPos.row, endPos.column);



			this.session.addMarker(range, cls, "text");

		},

		markRanges(batch) {

			let ranges = {};



			for (let hit of batch) {

				for (let log of hit.testresult?.log ?? []) {

					let key = `${log.start} ${log.end}`;



					if (!rangeskey])

						rangeskey = {

							start: log.start,

							end: log.end,

							total: 0,

							tested: 0,

							matches: 0,

							errors: 0

						};



					rangeskey].total++;

					if (log.error)

						rangeskey].errors++;

					else if (log.result !== undefined)

						rangeskey].tested++;

					if (log.result)

						rangeskey].matches++;



					for (let match of log.details?.matches ?? []) {

						for (let regexRange of match.ranges ?? []) {

							let key = `${regexRange.start} ${regexRange.end}`;



							if (!rangeskey])

								rangeskey = {

									start: regexRange.start,

									end: regexRange.end,

									regexmatch: true

								};

						}

					}

				}

			}



			this.clearAllMarkers();



			for (let range of Object.values(ranges)) {

				let cls = "";



				if (range.regexmatch)

					cls = "fdb-regexmatch";

				else if (range.errors > 0)

					cls = "fdb-evalerror";

				else if (range.tested == 0)

					cls = "fdb-undef";

				else if (range.matches == range.tested)

					cls = "fdb-match";

				else if (range.matches > 0)

					cls = "fdb-match1";

				else

					cls = "fdb-nonmatch";



				this.markRange(range.start, range.end, "fdb-ace-marker " + cls);

			}

		},

		markParseError(error) {

			this.markRange(error.start, error.end, "fdb-ace-marker fdb-parseerror");

		}

	},

	template: `

<div class="fdb-ace-editor mw-abusefilter-editor" v-show="ace" ref="aceEditor"></div>

<textarea class="fdb-textbox-editor" v-show="!ace" v-model="text"></textarea>

`

});



;// CONCATENATED MODULE: ./src/Main.js

/* globals mw, Vue */

















const validURLParams = "mode", "logid", "revids", "filter", "limit", "user",

						 "title", "start", "end", "namespace", "tag", "show"];

const validParams = [...validURLParams, "expensive", "file"];



// @vue/component

/* harmony default export */ const Main = ({

	components: { Hit: Hit, Editor: Editor, Batch: Batch },

	inject: "shared"],

	provide() {

		return {

			shared: this.shared

		};

	},

	data() {

		let state = {

			ace: true,

			wrap: false,

			loadableFilter: "",

			mode: "recentchanges",

			logid: "",

			revids: "",

			filter: "",

			limit: "",

			user: "",

			title: "",

			start: "",

			end: "",

			namespace: "",

			tag: "",

			show: "",

			file: null,

			expensive: false,

			shortCircuit: true,

			showMatches: true,

			showNonMatches: true,

			showErrors: true,

			showUndef: true,

			markAll: true,

			showAdvanced: false,

			threads: navigator.hardwareConcurrency || 2,

			fullscreen: false,

			topSelect: "diff",

			bottomSelect: "matches",

			varnames: [],

			text: "",

			timeout: null,

			batch: [],

			dategroups: [],

			selectedHit: 0,

			status: "",

			statusTimeout: null,

			filterRevisions: [],

			filterRevision: "",

			shared: Vue.shallowRef({ })

		};

		return { ...state, ...this.getParams() };

	},

	watch: {

		fullscreen() {

			if (this.fullscreen)

				this.$refs.wrapper.requestFullscreen();

			else if (document.fullscreenElement)

				document.exitFullscreen();

		},

		markAll() {

			this.markRanges();

		},

		shortCircuit() {

			this.updateText();

		},

		async loadableFilter() {

			let response = await (new mw.Api()).get({

				action: "query",

				list: "logevents",

				letype: "abusefilter",

				letitle: `Special:AbuseFilter/${this.loadableFilter}`,

				leprop: "user|timestamp|details",

				lelimit: 500

			});



			this.filterRevisions = (response?.query?.logevents ?? []).map(item => ({

				timestamp: item.timestamp,

				user: item.user,

				id: item.params.historyId ?? item.params0

			}));

		}

	},

	beforeMount() {

		this.startEvaluator();

	},

	async mounted() {

		this.varnames = parserData.variables.split("|");



		this.getBatch();



		addEventListener("popstate", () => {

			Object.assign(this, this.getParams());

			this.getBatch();

		});

		document.addEventListener("fullscreenchange", () => {

			this.fullscreen = !!document.fullscreenElement;

		});

	},

	methods: {

		getParams() {

			let params = {}, rest = mw.config.get('wgPageName').split('/');



			for (let i = 2; i < rest.length - 1; i += 2)

				if (validURLParams.includes(resti]))

					paramsresti]] = resti + 1];



			for (let param, value of (new URL(window.location)).searchParams)

				if (validURLParams.includes(param))

					paramsparam = value;



			if (!params.mode) {

				if (params.filter || params.logid)

					params.mode = "abuselog";

				else if (params.revid || params.title || params.user)

					params.mode = "revisions";

				else if (Object.keys(params).length > 0)

					params.mode = "recentchanges";

				else {

					// Nothing requested, just show a quick "demo"

					params.mode = "abuselog";

					params.limit = 10;

				}

			}



			return params;

		},

		getURL(params) {

			let url = mw.config.get("wgArticlePath").replace("$1", "Special:BlankPage/FilterDebug");



			for (let param of validURLParams)

				if (paramsparam !== undefined) {

					let encoded = mw.util.wikiUrlencode(paramsparam]).replaceAll("/", "%2F");



					url += `/${param}/${encoded}`;

				}



			return url;

		},

		async getCacheSize() {

			let size = 1000;



			if (typeof window.FilterDebuggerCacheSize == 'number')

				size = window.FilterDebuggerCacheSize;



			// Storing "too much data" migh cause the browser to decide that this site is

			// "abusing" resources and delete EVERYTHING, including data stored by other scripts

			if (size > 5000 && !(await navigator.storage.persist()))

				size = 5000;



			return size;

		},

		async getBatch() {

			let params = {};



			for (let param of validParams) {

				let val = thisparam];



				if (val === undefined || val === "")

					continue;



				paramsparam = val;

			}



			params.cacheSize = await this.getCacheSize();



			if (this.getURL(params) != this.getURL(this.getParams()))

				window.history.pushState(params, "", this.getURL(params));



			if (params.filter && params.filter.match(/^[0-9]+$/))

				this.$refs.editor.loadFilter(params.filter, null, false, this.updateStatus);



			let batch = await this.shared.evaluator.getBatch(params);



			this.batch = [];

			this.dategroups = [];



			for (let i = 0; i < batch.length; i++) {

				let d = new Date(batchi].timestamp);

				let date = `${d.getUTCDate()} ${mw.language.months.namesd.getUTCMonth()]} ${d.getUTCFullYear()}`;

				let time = `${("" + d.getUTCHours()).padStart(2, "0")}:${("" + d.getUTCMinutes()).padStart(2, "0")}`;

				let entry = { ...batchi], date, time };



				if (this.dategroups.length == 0 || date != this.dategroupsthis.dategroups.length - 1].date) {

					this.dategroups.push({

						date,

						batch: i

					});

				} else {

					this.dategroupsthis.dategroups.length - 1].batch.push(i);

				}



				this.batch.push(entry);

			}



			if (params.logid && this.batch.length)

				this.$refs.editor.loadFilter(this.batch0].filter_id, null, false, this.updateStatus);



			this.updateText();

		},

		loadFilter() {

			this.$refs.editor.loadFilter(this.loadableFilter, this.filterRevision, true, this.updateStatus);

		},

		startEvaluator() {

			if (this.shared.evaluator)

				this.shared.evaluator.terminate();

			this.shared.evaluator = new FilterEvaluator({

				threads: this.threads,

				status: this.updateStatus

			});

		},

		updateStatus(status) {

			this.status = status;



			if (this.statusTimeout === null)

				this.statusTimeout = setTimeout(() => {

					this.statusTimeout = null;



					// Vue takes takes waaaay too long to update a simple line of text...

					this.$refs.status.textContent = this.status;

				}, 50);

		},

		async restart() {

			this.startEvaluator();



			await this.getBatch();



			this.updateText();

		},

		async clearCache() {

			try {

				await window.caches.delete("filter-debugger");

				this.updateStatus("Cache cleared");

			} catch (e) {

				this.updateStatus("No cache found");

			}

		},

		selectHit(hit) {

			this.selectedHit = hit;

			this.markAll = false;

			this.markRanges();

		},

		markRanges() {

			this.$refs.editor.markRanges(

				this.markAll ?

					this.batch :

					this.batch.slice(this.selectedHit, this.selectedHit + 1));

		},

		async updateText(text) {

			if (text !== undefined)

				this.text = text;



			this.$refs.editor.clearAllMarkers();



			let promises = [];



			let startTime = performance.now();

			let evaluated = 0;

			let matches = 0;

			let errors = 0;



			try {

				promises = await this.shared.evaluator

					.evalBatch(this.text, this.shortCircuit ? "blank" : "allpaths");

			} catch (error) {

				if (typeof error.start == 'number' && typeof error.end == 'number') {

					this.updateStatus(error.error);



					this.batch.forEach(entry => delete entry.testresult);

					this.$refs.editor.markParseError(error);



					return;

				} else {

					throw error;

				}

			}



			for (let i = 0; i < promises.length; i++)

				promisesi].then(result => {

					this.batchi].testresult = result;



					evaluated++;

					if (result.error)

						errors++;

					else if (result.result)

						matches++;



					this.updateStatus(`${matches}/${evaluated} match, ${errors} errors, ${((performance.now() - startTime) / evaluated).toFixed(2)} ms avg)`);

				});



			await Promise.all(promises);



			this.markRanges();

		},

		setFile(event) {

			if (event.target?.files?.length) {

				this.file = event.target.files0];

				this.getBatch();

			} else {

				this.file = null;

			}

		},

		async download() {

			if (window.showSaveFilePicker) {

				let handle = null;



				try {

					handle = await window.showSaveFilePicker({ suggestedName: "dump.json.gz" });

				} catch (error) {

					this.updateStatus(`Error opening file: ${error.message}`);

					return;

				}



				if (handle)

					this.shared.evaluator.createDownload(handle, /\.gz$/.test(handle.name));

			} else {

				let hidden = this.$refs.hiddenDownload;

				let name = prompt("Filename", "dump.json.gz");



				if (name !== null) {

					hidden.download = name;

					hidden.href = await this.shared.evaluator.createDownload(null, /\.gz$/.test(name));



					hidden.click();

				}

			}

		},

		resize(event, target, dir) {

			let start = dir == 'x' ?

				target.clientWidth + event.clientX :

				target.clientHeight + event.clientY;

			let move = dir == 'x' ?

				((event) => target.style.width = (start - event.clientX) + "px") :

				((event) => target.style.height = (start - event.clientY) + "px");

			let stop = () =>

				document.body.removeEventListener("mousemove", move);



			document.body.addEventListener("mousemove", move);

			document.body.addEventListener("mouseup", stop, { once: true });

			document.body.addEventListener("mouseleave", stop, { once: true });

		}

	},

	template: `

<div class="fdb-wrapper" ref="wrapper">

  <div class="fdb-first-col">

    <div class="fdb-panel fdb-editor">

      <editor ref="editor" :ace="ace" :wrap="wrap" @textchange="updateText"></editor>

    </div>

    <div class="fdb-panel">

      <div class="fdb-status" ref="status">Waiting...</div>

    </div>

    <div class="fdb-panel fdb-controls" ref="controls">

      <div>

        <label><input type="checkbox" v-model="wrap"> Wrap</label>

        <label><input type="checkbox" v-model="ace"> ACE</label>

        <label><input type="checkbox" v-model="fullscreen"> FS</label>

        <input type="text" size="4" v-model.lazy.trim="loadableFilter">

        <select class="fdb-filter-revision" v-model="filterRevision">

          <option value="">(cur)</option>

          <option v-for="rev of filterRevisions" :value="rev.id">{{rev.timestamp}}</option>

        </select>

        <button @click="loadFilter">Load filter</button>

      </div>

      <div>

        <select v-model="mode">

          <option value="abuselog">Abuse log</option>

          <option value="recentchanges">Recent changes</option>

          <option value="revisions">Revisions</option>

          <option value="file">Local file</option>

        </select>

        <button @click="getBatch">Fetch data</button>

        <button @click="download" :disabled="mode == 'file' || !batch.length">Save...</button>

        <a style="display:none;" download="dump.json.gz" ref="hiddenDownload"></a>

        <span v-show="mode == 'recentchanges' || mode == 'revisions'">

          <label><input type="checkbox" v-model="expensive"> Fetch slow vars</label>

        </span>

        <span v-show="mode == 'file'">

        <label>File <input type="file" accept=".json,.json.gz" @change="setFile"></label>

        </span>

      </div>

      <div>

        <label>Limit <input type="text" size="5" placeholder="100" v-model.trim.lazy="limit"></label>

        <span v-show="mode == 'abuselog'">

          <label>Filters <input type="text" size="10" v-model.trim.lazy="filter"></label>

        </span>

        <span v-show="mode == 'recentchanges' || mode == 'revisions'">

          <label>Namespace <input type="text" size="4" v-model.trim.lazy="namespace"></label>

          <label>Tag <input type="text" size="10" v-model.trim.lazy="tag"></label>

        </span>

      </div>

      <div>

        <label>User <input type="text" size="12" v-model.trim.lazy="user"></label>

        <label>Title <input type="text" size="12" v-model.trim.lazy="title"></label>

        <span v-show="mode == 'abuselog'">

          <label>Log ID <input type="text" size="9" v-model.trim.lazy="logid"></label>

        </span>

        <span v-show="mode == 'revisions'">

          <label>Rev ID <input type="text" size="9" v-model.trim.lazy="revids"></label>

        </span>

      </div>

      <div>

        <label>After <input type="text" size="12" v-model.trim.lazy="end"></label>

        <label>Before <input type="text" size="12" v-model.trim.lazy="start"></label>

        <span v-show="mode == 'recentchanges' || mode == 'revisions'">

          <label>Show <input type="text" size="7" v-model.trim.lazy="show"></label>

        </span>

      </div>

      <div>

        <label><input type="checkbox" v-model="showMatches"> Matches</label>

        <label><input type="checkbox" v-model="showNonMatches"> Non-matches</label>

        <label><input type="checkbox" v-model="showUndef"> Untested</label>

        <label><input type="checkbox" v-model="showErrors"> Errors</label>

        <label><input type="checkbox" v-model="markAll"> Mark all</label>

        <a style="float: right;" v-if="!showAdvanced" @click="showAdvanced=true">[more]</a>

      </div>

      <div v-show="showAdvanced">

        <label>Threads <input type="number" min="1" max="16" size="2" v-model="threads"></label>

        <button @click="restart">Restart worker</button>

        <button @click="clearCache">Clear cache</button>

        <label><input type="checkbox" v-model="shortCircuit"> Quick eval</label>

        <a style="float: right;" @click="showAdvanced=false">[less]</a>

      </div>

    </div>

  </div>

  <div class="fdb-column-resizer" @mousedown.prevent="resize($event, $refs.secondCol, 'x')"></div>

  <div class="fdb-second-col" ref="secondCol">

    <div class="fdb-panel fdb-selected-result" v-show="topSelect != 'none'">

       <hit v-if="batch.length" :entry="batch[selectedHit]" :type="topSelect"></hit>

    </div>

    <div class="fdb-row-resizer" @mousedown.prevent="resize($event, $refs.batchPanel, 'y')"></div>

    <div class="fdb-panel">

    &#x2191; <select class="fdb-result-select" v-model="topSelect">

      <option value="none">(none)</option>

      <option value="result">(result)</option>

      <option value="matches">(matches)</option>

      <option value="diff">(diff)</option>

      <option value="vardump">(vardump)</option>

      <option v-for="name of varnames" :value="'var-' + name">{{name}}</option>

    </select>

    &#x2193; <select class="fdb-result-select" v-model="bottomSelect">

      <option value="none">(none)</option>

      <option value="result">(result)</option>

      <option value="diff">(diff)</option>

      <option value="matches">(matches)</option>

      <option v-for="name of varnames" :value="'var-' + name">{{name}}</option>

    </select>

    </div>

    <div class="fdb-row-resizer" @mousedown.prevent="resize($event, $refs.batchPanel, 'y')"></div>

    <div class="fdb-panel fdb-batch-results" ref="batchPanel" :class="{'fdb-show-matches': showMatches, 'fdb-show-nonmatches': showNonMatches, 'fdb-show-errors': showErrors, 'fdb-show-undef': showUndef}" v-show="bottomSelect != 'none'">

      <batch :batch="batch" :dategroups="dategroups" :type="bottomSelect" @selecthit="selectHit"></batch>

    </div>

  </div>

</div>

`

});



;// CONCATENATED MODULE: ./style/ui.css

const ui_namespaceObject = ".fdb-ace-marker {\n    position: absolute;\n}\n.fdb-batch-results .fdb-hit {\n    border-width: 0px 0px 1px 0px;\n    border-style: solid;\n}\n.fdb-batch-results .fdb-hit:focus {\n    outline: 2px inset black;\n    border-style: none;\n}\n.fdb-match {\n    background-color: #DDFFDD;\n}\n.fdb-match1 {\n    background-color: #EEFFEE;\n}\n.fdb-nonmatch {\n    background-color: #FFDDDD;\n}\n.fdb-undef {\n    background-color: #CCCCCC;\n}\n.fdb-error {\n    background-color: #FFBBFF;\n}\n.fdb-regexmatch {\n    background-color: #AAFFAA;\n    outline: 1px solid #00FF00;\n}\n\n.fdb-filter-revision {\n    width: 15em;\n}\n\n.fdb-controls div {\n    padding: 2px;\n}\n\n.fdb-batch-results .fdb-match, .fdb-batch-results .fdb-nonmatch, .fdb-batch-results .fdb-undef, .fdb-batch-results .fdb-error {\n    padding-left: 25px;\n    background-repeat: no-repeat;\n    background-position: left center;\n}\n\n.fdb-batch-results .fdb-match {\n    background-image: url(https://upload.wikimedia.org/wikipedia/en/thumb/f/fb/Yes_check.svg/18px-Yes_check.svg.png);\n}\n\n.fdb-batch-results .fdb-nonmatch {\n    background-image: url(https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Red_x.svg/18px-Red_x.svg.png);\n}\n\n.fdb-batch-results .fdb-undef {\n    background-image: url(https://upload.wikimedia.org/wikipedia/en/thumb/e/e0/Symbol_question.svg/18px-Symbol_question.svg.png);\n}\n\n.fdb-batch-results .fdb-error {\n    background-image: url(https://upload.wikimedia.org/wikipedia/en/thumb/b/b4/Ambox_important.svg/18px-Ambox_important.svg.png);\n}\n\n.fdb-matchedtext {\n    font-weight: bold;\n    background-color: #88FF88;\n}\n\n.fdb-parseerror, .fdb-parseerror {\n    background-color: #FFBBFF;\n    outline: 1px solid #FF00FF;\n}\n\n.fdb-outer {\n    height: 95vh;\n    width: 100%;\n}\n.fdb-wrapper {\n    height: 100%;\n    width: 100%;\n    display: flex;\n    background: #F8F8F8;\n\n}\n.fdb-first-col {\n    display: flex;\n    flex-direction: column;\n    flex: 1;\n    margin: 2px;\n}\n.fdb-column-resizer {\n    display: flex;\n    width: 0px;\n    padding: 0.5em;\n    margin: -0.5em;\n    cursor: col-resize;\n    z-index: 0;\n}\n.fdb-row-resizer {\n    display: flex;\n    height: 0px;\n    padding: 0.5em;\n    margin: -0.5em;\n    cursor: row-resize;\n    z-index: 0;\n}\n\n.fdb-second-col {\n    display: flex;\n    flex-direction: column;\n    width: 45%;\n    height: 100%;\n    margin: 2px;\n}\n.fdb-panel {\n    border: 1px solid black;\n    background: white;\n    padding: 2px;\n    width: 100%;\n    box-sizing: border-box;\n    margin: 2px;\n}\n.fdb-selected-result {\n    overflow: auto;\n    flex: 1;\n    word-wrap: break-word;\n    font-family: monospace;\n    white-space: pre-wrap;\n    word-wrap: break-word;\n}\n.fdb-batch-results {\n    overflow: auto;\n    height: 75%;\n    word-wrap: break-word;\n}\n.fdb-status {\n    float: right;\n    font-style: italic;\n}\n\n.fdb-result-select {\n    display: inline;\n    width: 40%;\n    overflow: hidden;\n}\n.fdb-ace-editor, .fdb-textbox-editor {\n    width: 100%;\n    height: 100%;\n    display: block;\n    resize: none;\n}\n.fdb-editor {\n    flex-basis: 20em;\n    flex-grow: 1;\n}\ndiv.mw-abusefilter-editor {\n    height: 100%;\n}\n.fdb-controls {\n    flex-basis: content;\n}\n.fdb-filtersnippet {\n    background: #DDD;\n}\n.fdb-matchresult {\n    font-family: monospace;\n    font-size: 12px;\n    line-height: 17px;\n}\n.fdb-dateheader {\n    position: sticky;\n    top: 0px;\n    font-weight: bold;\n    background-color: #F0F0F0;\n    border-width: 0px 0px 1px 0px;\n    border-style: solid;\n    border-color: black;\n}\n\n.fdb-diff {\n    background: white;\n}\n.fdb-added {\n    background: #D8ECFF;\n    font-weight: bold;\n}\n.fdb-removed {\n    background: #FEECC8;\n    font-weight: bold;\n}\n\n@supports selector(.fdb-dateheader:has(~ .fdb-match)) {\n    .fdb-dateheader {\n\tdisplay: none;\n    }\n    .fdb-show-matches .fdb-dateheader:has(~ .fdb-match) {\n\tdisplay: block;\n    }\n    .fdb-show-nonmatches .fdb-dateheader:has(~ .fdb-nonmatch) {\n\tdisplay: block;\n    }\n    .fdb-show-errors .fdb-dateheader:has(~ .fdb-error) {\n\tdisplay: block;\n    }\n    .fdb-show-undef .fdb-dateheader:has(~ .fdb-undef) {\n\tdisplay: block;\n    }\n}\n\n.fdb-batch-results .fdb-match {\n    display: none;\n}\n.fdb-batch-results .fdb-nonmatch {\n    display: none;\n}\n.fdb-batch-results .fdb-error {\n    display: none;\n}\n.fdb-batch-results .fdb-undef {\n    display: none;\n}\n\n.fdb-show-matches .fdb-match {\n    display: block;\n}\n.fdb-show-nonmatches .fdb-nonmatch {\n    display: block;\n}\n.fdb-show-errors .fdb-error {\n    display: block;\n}\n.fdb-show-undef .fdb-undef {\n    display: block;\n}\n";

;// CONCATENATED MODULE: ./src/ui.js

/* globals mw, Vue */









function setup() {

	mw.util.addCSS(ui_namespaceObject);



	if (typeof Vue.configureCompat == 'function')

		Vue.configureCompat({ MODE: 3 });



	document.getElementById('firstHeading').innerText = document.title = "Debugging edit filter";

	document.getElementById("mw-content-text").innerHTML = '<div class="fdb-outer"></div>';



	Vue.createApp(Main).mount(".fdb-outer");

}







window.FilterDebugger = __webpack_exports__;

/******/ })()

;//</nowiki>
From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.

//<nowiki>

/* jshint esversion: 11, esnext: false */

/******/ (() => { // webpackBootstrap

/******/ 	"use strict";

/******/ 	// The require scope

/******/ 	var __webpack_require__ = {};

/******/ 	

/************************************************************************/

/******/ 	/* webpack/runtime/define property getters */

/******/ 	(() => {

/******/ 		// define getter functions for harmony exports

/******/ 		__webpack_require__.d = (exports, definition) => {

/******/ 			for(var key in definition) {

/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {

/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definitionkey });

/******/ 				}

/******/ 			}

/******/ 		};

/******/ 	})();

/******/ 	

/******/ 	/* webpack/runtime/hasOwnProperty shorthand */

/******/ 	(() => {

/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))

/******/ 	})();

/******/ 	

/******/ 	/* webpack/runtime/make namespace object */

/******/ 	(() => {

/******/ 		// define __esModule on exports

/******/ 		__webpack_require__.r = (exports) => {

/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {

/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });

/******/ 			}

/******/ 			Object.defineProperty(exports, '__esModule', { value: true });

/******/ 		};

/******/ 	})();

/******/ 	

/************************************************************************/

var __webpack_exports__ = {};

// ESM COMPAT FLAG

__webpack_require__.r(__webpack_exports__);



// EXPORTS

__webpack_require__.d(__webpack_exports__, {

  setup: () => (/* binding */ setup)

});



;// CONCATENATED MODULE: ./src/filter.js

class FilterEvaluator {

	constructor(options) {

		let blob = new Blob(['importScripts("https://en.wikipedia.org/?title=User:Suffusion_of_Yellow/fdb-worker.js&action=raw&ctype=text/javascript");'], { type: "text/javascript" });



		this.version = 0;

		this.uid = 0;

		this.callbacks = {};

		this.status = options.status || (() => null);

		this.workers = [];

		this.threads = Math.min(Math.max(options.threads || 1, 1), 16);



		this.status("Starting workers...");



		let channels = [];

		for (let i = 0; i < this.threads - 1; i++)

			channels.push(new MessageChannel());



		for (let i = 0; i < this.threads; i++) {

			this.workersi = new Worker(URL.createObjectURL(blob), { type: 'classic' });



			this.workersi].onmessage = (event) => {

				if (this.status && event.data.status)

					this.status(event.data.status);



				if (event.data.uid && this.callbacksevent.data.uid]) {

					this.callbacksevent.data.uid](event.data);

					delete this.callbacksevent.data.uid];

				}

			};



			if (i == 0) {

				if (this.threads > 1)

					this.workersi].postMessage({

						action: "setsecondaries",

						ports: channels.map(c => c.port1)

					}, channels.map(c => c.port1));

			} else {

				this.workersi].postMessage({

					action: "setprimary",

					port: channelsi - 1].port2

				}, channelsi - 1].port2]);

			}

		}

	}



	work(data, i = 0) {

		return new Promise((resolve) => {

			data.uid = ++this.uid;

			this.callbacksthis.uid = (data) => resolve(data);



			this.workersi].postMessage(data);

		});

	}



	terminate() {

		this.workers.forEach(w => w.terminate());

	}



	async getBatch(params) {

		for (let i = 0; i < this.threads; i++)

			this.work({

				action: "clearallvardumps",

			}, i);



		let response = (await this.work({

			action: "getbatch",

			params: params,

			stash: true

		}));



		this.batch = response.batch || [];

		this.owners = response.owners;



		return this.batch;

	}



	async getVar(name, id) {

		let response = await this.work({

			action: "getvar",

			name: name,

			vardump_id: id

		}, this.ownersid]);



		return response.vardump;

	}

	async getDiff(id) {

		let response = await this.work({

			action: "diff",

			vardump_id: id

		}, this.ownersid]);



		return response.diff;

	}



	async createDownload(fileHandle, compress = true) {

		let encoder = new TextEncoderStream() ;

		let writer = encoder.writable.getWriter();



		(async() => {

			await writer.write("[\n");



			for (let i = 0; i < this.batch.length; i++) {

				let entry = {

					...this.batchi],

					...{

						details: await this.getVar("*", this.batchi].id)

					}

				};

				this.status(`Writing entries... (${i}/${this.batch.length})`);



				await writer.write(JSON.stringify(entry, null, 2).replace(/^/gm, "  "));

				await writer.write(i == this.batch.length - 1 ? "\n]\n" : ",\n");

			}



			await writer.close();

		})();



		let output = encoder.readable;



		if (compress)

			output = output.pipeThrough(new CompressionStream("gzip"));



		if (fileHandle) {

			await output.pipeTo(await fileHandle.createWritable());



			this.status(`Created ${(await fileHandle.getFile()).size} byte file`);

		} else {

			let compressed = await (new Response(output).blob());



			this.status(`Created ${compressed.size} byte file`);



			return URL.createObjectURL(compressed);

		}

	}



	async evalBatch(text, scmode) {

		if (!this.batch)

			return [];



		let version = ++this.version;



		text = text.replaceAll("\r\n", "\n");



		for (let i = 1; i < this.threads; i++)

			this.work({

				action: "setfilter",

				filter_id: 1,

				filter: text,

			}, i);

		let response = await this.work({

			action: "setfilter",

			filter_id: 1,

			filter: text,

		}, 0);



		// Leftover response from last batch

		if (this.version != version)

			return [];



		if (response.error)

			throw response;



		let promises = [], tasks = Array(this.threads).fill().map(() => []);



		for (let entry of this.batch) {

			let task = { entry };



			promises.push(new Promise((resolve) => task.callback = resolve));



			tasksthis.ownersentry.id]].push(task);

		}



		for (let i = 0; i < this.threads; i++)

			(async() => {

				for (let task of tasksi]) {

					let response = await this.work({

						action: "evaluate",

						filter_id: 1,

						vardump_id: task.entry.id,

						scmode: scmode

					}, i);



					if (this.version != version)

						return;



					task.callback(response);

				}

			})();



		return promises;

	}

}







;// CONCATENATED MODULE: ./src/parserdata.js

const parserData = {

	functions: "bool|ccnorm_contains_all|ccnorm_contains_any|ccnorm|contains_all|contains_any|count|equals_to_any|float|get_matches|int|ip_in_range|ip_in_ranges|lcase|length|norm|rcount|rescape|rmdoubles|rmspecials|rmwhitespace|sanitize|set|set_var|specialratio|string|strlen|strpos|str_replace|str_replace_regexp|substr|ucase",

	operators: "==?=?|!==?|!=|\\+|-|/|%|\\*\\*?|<=?|>=?|\\(|\\)|\\[|\\]|&|\\||\\^|!|:=?|\\?|;|,",

	keywords: "contains|in|irlike|like|matches|regex|rlike|if|then|else|end",

	variables: "accountname|action|added_lines|added_lines_pst|added_links|all_links|edit_delta|edit_diff|edit_diff_pst|file_bits_per_channel|file_height|file_mediatype|file_mime|file_sha1|file_size|file_width|global_user_editcount|global_user_groups|moved_from_age|moved_from_first_contributor|moved_from_id|moved_from_last_edit_age|moved_from_namespace|moved_from_prefixedtitle|moved_from_recent_contributors|moved_from_restrictions_create|moved_from_restrictions_edit|moved_from_restrictions_move|moved_from_restrictions_upload|moved_from_title|moved_to_age|moved_to_first_contributor|moved_to_id|moved_to_last_edit_age|moved_to_namespace|moved_to_prefixedtitle|moved_to_recent_contributors|moved_to_restrictions_create|moved_to_restrictions_edit|moved_to_restrictions_move|moved_to_restrictions_upload|moved_to_title|new_content_model|new_html|new_pst|new_size|new_text|new_wikitext|oauth_consumer|old_content_model|old_links|old_size|old_wikitext|page_age|page_first_contributor|page_id|page_last_edit_age|page_namespace|page_prefixedtitle|page_recent_contributors|page_restrictions_create|page_restrictions_edit|page_restrictions_move|page_restrictions_upload|page_title|removed_lines|removed_links|summary|timestamp|tor_exit_node|user_age|user_app|user_blocked|user_editcount|user_emailconfirm|user_groups|user_mobile|user_name|user_rights|user_type|wiki_language|wiki_name",

	deprecated: "article_articleid|article_first_contributor|article_namespace|article_prefixedtext|article_recent_contributors|article_restrictions_create|article_restrictions_edit|article_restrictions_move|article_restrictions_upload|article_text|moved_from_articleid|moved_from_prefixedtext|moved_from_text|moved_to_articleid|moved_to_prefixedtext|moved_to_text",

	disabled: "minor_edit|old_html|old_text"

};



;// CONCATENATED MODULE: ./src/Hit.js

/* globals mw */



function sanitizedSpan(text, classList) {

	let span = document.createElement('span');



	span.textContent = text;



	if (classList)

		span.classList = classList;



	return span.outerHTML;

}



// @vue/component

/* harmony default export */ const Hit = ({

	inject: "shared"],

	props: {

		entry: {

			type: Object,

			required: true

		},

		type: {

			type: String,

			required: true

		},

		matchContext: {

			type: Number,

			default: 10

		},

		diffContext: {

			type: Number,

			default: 25

		},

		header: Boolean

	},

	data() {

		return {

			vars: {},

			diff: []

		};

	},

	computed: {

		id() {

			return this.entry.id;

		},

		difflink() {

			return this.entry.filter_id == 0 ?

				mw.util.getUrl("Special:Diff/" + this.entry.revid) :

				mw.util.getUrl("Special:AbuseLog/" + this.entry.id);

		},

		userlink() {

			return this.entry.filter_id == 0 ?

				mw.util.getUrl("Special:Contribs/" + mw.util.wikiUrlencode(this.entry.user)) :

				new mw.Uri(mw.config.get('wgScript')).extend({

					title: "Special:AbuseLog",

					wpSearchUser: this.entry.user

				});

		},

		pagelink() {

			return this.entry.filter_id == 0 ?

				mw.util.getUrl("Special:PageHistory/" + mw.util.wikiUrlencode(this.entry.title)) :

				new mw.Uri(mw.config.get('wgScript')).extend({

					title: "Special:AbuseLog",

					wpSearchTitle: this.entry.title

				});

		},

		result() {

			return JSON.stringify(this.entry.testresult.result, null, 2);

		},

		vardump() {

			return JSON.stringify(this.vars || null, null, 2);

		},

		vartext() {

			return JSON.stringify(this.vars?.[this.type.slice(4)] ?? null, null, 2);

		},

		matches() {

			let html = "";

			for (let log of this.entry.testresult.log || []) {

				for (let matchinfo of log.details?.matches ?? []) {

					let input = log.details.inputsmatchinfo.arg_haystack];



					let start = Math.max(matchinfo.match0 - this.matchContext, 0);

					let end = Math.min(matchinfo.match1 + this.matchContext, input.length);



					let pre = (start == 0 ? "" : "...") + input.slice(start, matchinfo.match0]);

					let post = input.slice(matchinfo.match1], end) + (end == input.length ? "" : "...");

					let match = input.slice(matchinfo.match0], matchinfo.match1]);



					html += '<div class="fdb-matchresult">' +

						sanitizedSpan(pre) +

						sanitizedSpan(match, "fdb-matchedtext") +

						sanitizedSpan(post) +

						'</div>';

				}

			}

			return html;

		},

		prettydiff() {

			let html = '<div class="fdb-diff">';

			for (let i = 0; i < this.diff.length; i++) {

				let hunk = this.diffi];

				if (hunk0 == -1)

					html += sanitizedSpan(hunk1], "fdb-removed");

				else if (hunk0 == 1)

					html += sanitizedSpan(hunk1], "fdb-added");

				else {

					let common = hunk1];

					if (i == 0) {

						if (common.length > this.diffContext)

							common = "..." + common.slice(-this.diffContext);

					} else if (i == this.diff.length - 1) {

						if (common.length > this.diffContext)

							common = common.slice(0, this.diffContext) + "...";

					} else {

						if (common.length > this.diffContext * 2)

							common = common.slice(0, this.diffContext) + "..." + common.slice(-this.diffContext);

					}

					html += sanitizedSpan(common);

				}

			}

			html += "</div>";

			return html;

		},

		cls() {

			if (!this.header)

				return "";

			if (this.entry.testresult === undefined)

				return 'fdb-undef';

			if (this.entry.testresult.error)

				return 'fdb-error';

			if (this.entry.testresult.result)

				return 'fdb-match';

			return 'fdb-nonmatch';

		}

	},

	watch: {

		id: {

			handler() {

				this.getAsyncData();

			},

			immediate: true

		},

		type: {

			handler() {

				this.getAsyncData();

			},

			immediate: true

		}

	},

	methods: {

		async getAsyncData() {

			if (this.type == "vardump")

				this.vars = await this.shared.evaluator.getVar("*", this.entry.id);

			else if (this.type.slice(0, 4) == "var-")

				this.vars = await this.shared.evaluator.getVar(this.type.slice(4), this.entry.id);

			else {

				this.vars = {};

				if (this.type == "diff")

					this.diff = await this.shared.evaluator.getDiff(this.entry.id);

				else

					this.diff = "";

			}

		}

	},

	template: `

<div class="fdb-hit" :class="cls">

  <div v-if="header"><a :href="difflink">{{entry.time}}</a> | <a :href="userlink">{{entry.user}}</a> | <a :href="pagelink">{{entry.title}}</a></div><div v-if="entry.testresult && entry.testresult.error && (type == 'result' || type == 'matches')">{{entry.testresult.error}}</div>

  <div v-else-if="entry.testresult && type == 'result'">{{result}}</div>

  <div v-else-if="entry.testresult && type == 'matches'" v-html="matches"></div>

  <div v-else-if="type == 'diff'" v-html="prettydiff"></div>

  <div v-else-if="type == 'vardump'">{{vardump}}</div>

  <div v-else-if="type != 'none' && type != 'matches' && type != 'result'">{{vartext}}</div>

</div>`

});





;// CONCATENATED MODULE: ./src/Batch.js





// @vue/component

/* harmony default export */ const Batch = ({

	components: { Hit: Hit },

	props: {

		batch: {

			type: Array,

			required: true

		},

		dategroups: {

			type: Array,

			required: true

		},

		type: {

			type: String,

			required: true

		}

	},

	emits: 'selecthit'],

	data() {

		return {

			selectedHit: 0

		};

	},

	methods: {

		selectHit(hit) {

			this.selectedHit = hit;

			this.$refs"idx-" + this.selectedHit][0].$el.focus();

			this.$emit('selecthit', this.selectedHit);

		},

		nextHit() {

			this.selectHit((this.selectedHit + 1) % this.batch.length);

		},

		prevHit() {

			this.selectHit((this.selectedHit - 1 + this.batch.length) % this.batch.length);

		}

	},

	template: `

<div v-for="dategroup of dategroups" class="fdb-dategroup">

  <div class="fdb-dateheader">{{dategroup.date}}</div>

    <hit v-for="entry of dategroup.batch" tabindex="-1" @focus="selectHit(entry)" @keydown.arrow-down.prevent="nextHit" @keydown.arrow-up.prevent="prevHit" :key="batch[entry].id" :ref="'idx-' + entry" :entry="batch[entry]" :type="type" header></hit>

  </div>

</div>

`

});



;// CONCATENATED MODULE: ./src/Editor.js

/* globals mw, ace */





// @vue/component

/* harmony default export */ const Editor = ({

	props: {

		wrap: Boolean,

		ace: Boolean

	},

	emits: "textchange"],

	data() {

		return {

			session: null,

			timeout: 0,

			text: ""

		};

	},

	watch: {

		wrap() {

			this.session.setOption("wrap", this.wrap);

		},

		ace() {

			if (this.ace)

				this.session.setValue(this.text);

			else

				this.text = this.session.getValue();

		},

		text() {

			clearTimeout(this.timeout);



			this.timeout = setTimeout(() => this.$emit('textchange', this.text), 50);

		}

	},

	async mounted() {

		let config = { ...parserData, aceReadOnly: false };



		mw.config.set("aceConfig", config);

		ace.config.set('basePath', mw.config.get('wgExtensionAssetsPath') + "/CodeEditor/modules/lib/ace");



		let editor = ace.(this.$refs.aceEditor);

		this.session = editor.getSession();



		this.session.setMode("ace/mode/abusefilter");

		this.session.setUseWorker(false);

		ace.require('ace/range');



		let observer = new ResizeObserver(() => editor.resize());

		observer.observe(this.$refs.aceEditor);



		this.session.setValue(this.text);

		this.session.on("change", () => this.text = this.session.getValue());

	},

	methods: {

		async loadFilter(id, revision, overwrite = true, status) {

			if (!overwrite && this.text.trim() !== "")

				return;



			let filterText = "";



			if (/^[0-9]+$/.test(id) && /^[0-9]+$/.test(revision)) {

				try {

					// Why isn't this possible through the API?

					let title = `Special:AbuseFilter/history/${id}/item/${revision}?safemode=1&useskin=fallback&uselang=qqx`;

					let url = mw.config.get('wgArticlePath').replace("$1", title);

					let response = await fetch(url);

					let text = await response.text();

					let html = (new DOMParser()).parseFromString(text, "text/html");

					let exported = html.querySelector('#mw-abusefilter-export textarea').value;

					let parsed = JSON.parse(exported);



					filterText = parsed.data.rules;

				} catch (error) {

					status(`Failed to fetch revision ${revision} of filter ${id}`);

					return false;

				}

			} else {

				try {

					let filter = await (new mw.Api()).get({

						action: "query",

						list: "abusefilters",

						abfstartid: id,

						abflimit: 1,

						abfprop: "pattern"

					});



					filterText = filter.query.abusefilters0].pattern;

				} catch (error) {

					status(`Failed to fetch filter ${id}`);



					return false;

				}

			}



			this.text = filterText;

			if (this.session)

				this.session.setValue(this.text);



			return true;

		},

		getPos(index) {

			let len, pos = { row: 0, column: 0 };



			while (index > (len = this.session.getLine(pos.row).length)) {

				index -= len + 1;

				pos.row++;

			}

			pos.column = index;



			return pos;

		},

		clearAllMarkers() {

			let markers = this.session.getMarkers();

			for (let id of Object.keys(markers))

				if (markersid].clazz.includes("fdb-"))

					this.session.removeMarker(id);

		},

		markRange(start, end, cls) {

			let startPos = this.getPos(start);

			let endPos = this.getPos(end);

			let range = new ace.Range(startPos.row, startPos.column, endPos.row, endPos.column);



			this.session.addMarker(range, cls, "text");

		},

		markRanges(batch) {

			let ranges = {};



			for (let hit of batch) {

				for (let log of hit.testresult?.log ?? []) {

					let key = `${log.start} ${log.end}`;



					if (!rangeskey])

						rangeskey = {

							start: log.start,

							end: log.end,

							total: 0,

							tested: 0,

							matches: 0,

							errors: 0

						};



					rangeskey].total++;

					if (log.error)

						rangeskey].errors++;

					else if (log.result !== undefined)

						rangeskey].tested++;

					if (log.result)

						rangeskey].matches++;



					for (let match of log.details?.matches ?? []) {

						for (let regexRange of match.ranges ?? []) {

							let key = `${regexRange.start} ${regexRange.end}`;



							if (!rangeskey])

								rangeskey = {

									start: regexRange.start,

									end: regexRange.end,

									regexmatch: true

								};

						}

					}

				}

			}



			this.clearAllMarkers();



			for (let range of Object.values(ranges)) {

				let cls = "";



				if (range.regexmatch)

					cls = "fdb-regexmatch";

				else if (range.errors > 0)

					cls = "fdb-evalerror";

				else if (range.tested == 0)

					cls = "fdb-undef";

				else if (range.matches == range.tested)

					cls = "fdb-match";

				else if (range.matches > 0)

					cls = "fdb-match1";

				else

					cls = "fdb-nonmatch";



				this.markRange(range.start, range.end, "fdb-ace-marker " + cls);

			}

		},

		markParseError(error) {

			this.markRange(error.start, error.end, "fdb-ace-marker fdb-parseerror");

		}

	},

	template: `

<div class="fdb-ace-editor mw-abusefilter-editor" v-show="ace" ref="aceEditor"></div>

<textarea class="fdb-textbox-editor" v-show="!ace" v-model="text"></textarea>

`

});



;// CONCATENATED MODULE: ./src/Main.js

/* globals mw, Vue */

















const validURLParams = "mode", "logid", "revids", "filter", "limit", "user",

						 "title", "start", "end", "namespace", "tag", "show"];

const validParams = [...validURLParams, "expensive", "file"];



// @vue/component

/* harmony default export */ const Main = ({

	components: { Hit: Hit, Editor: Editor, Batch: Batch },

	inject: "shared"],

	provide() {

		return {

			shared: this.shared

		};

	},

	data() {

		let state = {

			ace: true,

			wrap: false,

			loadableFilter: "",

			mode: "recentchanges",

			logid: "",

			revids: "",

			filter: "",

			limit: "",

			user: "",

			title: "",

			start: "",

			end: "",

			namespace: "",

			tag: "",

			show: "",

			file: null,

			expensive: false,

			shortCircuit: true,

			showMatches: true,

			showNonMatches: true,

			showErrors: true,

			showUndef: true,

			markAll: true,

			showAdvanced: false,

			threads: navigator.hardwareConcurrency || 2,

			fullscreen: false,

			topSelect: "diff",

			bottomSelect: "matches",

			varnames: [],

			text: "",

			timeout: null,

			batch: [],

			dategroups: [],

			selectedHit: 0,

			status: "",

			statusTimeout: null,

			filterRevisions: [],

			filterRevision: "",

			shared: Vue.shallowRef({ })

		};

		return { ...state, ...this.getParams() };

	},

	watch: {

		fullscreen() {

			if (this.fullscreen)

				this.$refs.wrapper.requestFullscreen();

			else if (document.fullscreenElement)

				document.exitFullscreen();

		},

		markAll() {

			this.markRanges();

		},

		shortCircuit() {

			this.updateText();

		},

		async loadableFilter() {

			let response = await (new mw.Api()).get({

				action: "query",

				list: "logevents",

				letype: "abusefilter",

				letitle: `Special:AbuseFilter/${this.loadableFilter}`,

				leprop: "user|timestamp|details",

				lelimit: 500

			});



			this.filterRevisions = (response?.query?.logevents ?? []).map(item => ({

				timestamp: item.timestamp,

				user: item.user,

				id: item.params.historyId ?? item.params0

			}));

		}

	},

	beforeMount() {

		this.startEvaluator();

	},

	async mounted() {

		this.varnames = parserData.variables.split("|");



		this.getBatch();



		addEventListener("popstate", () => {

			Object.assign(this, this.getParams());

			this.getBatch();

		});

		document.addEventListener("fullscreenchange", () => {

			this.fullscreen = !!document.fullscreenElement;

		});

	},

	methods: {

		getParams() {

			let params = {}, rest = mw.config.get('wgPageName').split('/');



			for (let i = 2; i < rest.length - 1; i += 2)

				if (validURLParams.includes(resti]))

					paramsresti]] = resti + 1];



			for (let param, value of (new URL(window.location)).searchParams)

				if (validURLParams.includes(param))

					paramsparam = value;



			if (!params.mode) {

				if (params.filter || params.logid)

					params.mode = "abuselog";

				else if (params.revid || params.title || params.user)

					params.mode = "revisions";

				else if (Object.keys(params).length > 0)

					params.mode = "recentchanges";

				else {

					// Nothing requested, just show a quick "demo"

					params.mode = "abuselog";

					params.limit = 10;

				}

			}



			return params;

		},

		getURL(params) {

			let url = mw.config.get("wgArticlePath").replace("$1", "Special:BlankPage/FilterDebug");



			for (let param of validURLParams)

				if (paramsparam !== undefined) {

					let encoded = mw.util.wikiUrlencode(paramsparam]).replaceAll("/", "%2F");



					url += `/${param}/${encoded}`;

				}



			return url;

		},

		async getCacheSize() {

			let size = 1000;



			if (typeof window.FilterDebuggerCacheSize == 'number')

				size = window.FilterDebuggerCacheSize;



			// Storing "too much data" migh cause the browser to decide that this site is

			// "abusing" resources and delete EVERYTHING, including data stored by other scripts

			if (size > 5000 && !(await navigator.storage.persist()))

				size = 5000;



			return size;

		},

		async getBatch() {

			let params = {};



			for (let param of validParams) {

				let val = thisparam];



				if (val === undefined || val === "")

					continue;



				paramsparam = val;

			}



			params.cacheSize = await this.getCacheSize();



			if (this.getURL(params) != this.getURL(this.getParams()))

				window.history.pushState(params, "", this.getURL(params));



			if (params.filter && params.filter.match(/^[0-9]+$/))

				this.$refs.editor.loadFilter(params.filter, null, false, this.updateStatus);



			let batch = await this.shared.evaluator.getBatch(params);



			this.batch = [];

			this.dategroups = [];



			for (let i = 0; i < batch.length; i++) {

				let d = new Date(batchi].timestamp);

				let date = `${d.getUTCDate()} ${mw.language.months.namesd.getUTCMonth()]} ${d.getUTCFullYear()}`;

				let time = `${("" + d.getUTCHours()).padStart(2, "0")}:${("" + d.getUTCMinutes()).padStart(2, "0")}`;

				let entry = { ...batchi], date, time };



				if (this.dategroups.length == 0 || date != this.dategroupsthis.dategroups.length - 1].date) {

					this.dategroups.push({

						date,

						batch: i

					});

				} else {

					this.dategroupsthis.dategroups.length - 1].batch.push(i);

				}



				this.batch.push(entry);

			}



			if (params.logid && this.batch.length)

				this.$refs.editor.loadFilter(this.batch0].filter_id, null, false, this.updateStatus);



			this.updateText();

		},

		loadFilter() {

			this.$refs.editor.loadFilter(this.loadableFilter, this.filterRevision, true, this.updateStatus);

		},

		startEvaluator() {

			if (this.shared.evaluator)

				this.shared.evaluator.terminate();

			this.shared.evaluator = new FilterEvaluator({

				threads: this.threads,

				status: this.updateStatus

			});

		},

		updateStatus(status) {

			this.status = status;



			if (this.statusTimeout === null)

				this.statusTimeout = setTimeout(() => {

					this.statusTimeout = null;



					// Vue takes takes waaaay too long to update a simple line of text...

					this.$refs.status.textContent = this.status;

				}, 50);

		},

		async restart() {

			this.startEvaluator();



			await this.getBatch();



			this.updateText();

		},

		async clearCache() {

			try {

				await window.caches.delete("filter-debugger");

				this.updateStatus("Cache cleared");

			} catch (e) {

				this.updateStatus("No cache found");

			}

		},

		selectHit(hit) {

			this.selectedHit = hit;

			this.markAll = false;

			this.markRanges();

		},

		markRanges() {

			this.$refs.editor.markRanges(

				this.markAll ?

					this.batch :

					this.batch.slice(this.selectedHit, this.selectedHit + 1));

		},

		async updateText(text) {

			if (text !== undefined)

				this.text = text;



			this.$refs.editor.clearAllMarkers();



			let promises = [];



			let startTime = performance.now();

			let evaluated = 0;

			let matches = 0;

			let errors = 0;



			try {

				promises = await this.shared.evaluator

					.evalBatch(this.text, this.shortCircuit ? "blank" : "allpaths");

			} catch (error) {

				if (typeof error.start == 'number' && typeof error.end == 'number') {

					this.updateStatus(error.error);



					this.batch.forEach(entry => delete entry.testresult);

					this.$refs.editor.markParseError(error);



					return;

				} else {

					throw error;

				}

			}



			for (let i = 0; i < promises.length; i++)

				promisesi].then(result => {

					this.batchi].testresult = result;



					evaluated++;

					if (result.error)

						errors++;

					else if (result.result)

						matches++;



					this.updateStatus(`${matches}/${evaluated} match, ${errors} errors, ${((performance.now() - startTime) / evaluated).toFixed(2)} ms avg)`);

				});



			await Promise.all(promises);



			this.markRanges();

		},

		setFile(event) {

			if (event.target?.files?.length) {

				this.file = event.target.files0];

				this.getBatch();

			} else {

				this.file = null;

			}

		},

		async download() {

			if (window.showSaveFilePicker) {

				let handle = null;



				try {

					handle = await window.showSaveFilePicker({ suggestedName: "dump.json.gz" });

				} catch (error) {

					this.updateStatus(`Error opening file: ${error.message}`);

					return;

				}



				if (handle)

					this.shared.evaluator.createDownload(handle, /\.gz$/.test(handle.name));

			} else {

				let hidden = this.$refs.hiddenDownload;

				let name = prompt("Filename", "dump.json.gz");



				if (name !== null) {

					hidden.download = name;

					hidden.href = await this.shared.evaluator.createDownload(null, /\.gz$/.test(name));



					hidden.click();

				}

			}

		},

		resize(event, target, dir) {

			let start = dir == 'x' ?

				target.clientWidth + event.clientX :

				target.clientHeight + event.clientY;

			let move = dir == 'x' ?

				((event) => target.style.width = (start - event.clientX) + "px") :

				((event) => target.style.height = (start - event.clientY) + "px");

			let stop = () =>

				document.body.removeEventListener("mousemove", move);



			document.body.addEventListener("mousemove", move);

			document.body.addEventListener("mouseup", stop, { once: true });

			document.body.addEventListener("mouseleave", stop, { once: true });

		}

	},

	template: `

<div class="fdb-wrapper" ref="wrapper">

  <div class="fdb-first-col">

    <div class="fdb-panel fdb-editor">

      <editor ref="editor" :ace="ace" :wrap="wrap" @textchange="updateText"></editor>

    </div>

    <div class="fdb-panel">

      <div class="fdb-status" ref="status">Waiting...</div>

    </div>

    <div class="fdb-panel fdb-controls" ref="controls">

      <div>

        <label><input type="checkbox" v-model="wrap"> Wrap</label>

        <label><input type="checkbox" v-model="ace"> ACE</label>

        <label><input type="checkbox" v-model="fullscreen"> FS</label>

        <input type="text" size="4" v-model.lazy.trim="loadableFilter">

        <select class="fdb-filter-revision" v-model="filterRevision">

          <option value="">(cur)</option>

          <option v-for="rev of filterRevisions" :value="rev.id">{{rev.timestamp}}</option>

        </select>

        <button @click="loadFilter">Load filter</button>

      </div>

      <div>

        <select v-model="mode">

          <option value="abuselog">Abuse log</option>

          <option value="recentchanges">Recent changes</option>

          <option value="revisions">Revisions</option>

          <option value="file">Local file</option>

        </select>

        <button @click="getBatch">Fetch data</button>

        <button @click="download" :disabled="mode == 'file' || !batch.length">Save...</button>

        <a style="display:none;" download="dump.json.gz" ref="hiddenDownload"></a>

        <span v-show="mode == 'recentchanges' || mode == 'revisions'">

          <label><input type="checkbox" v-model="expensive"> Fetch slow vars</label>

        </span>

        <span v-show="mode == 'file'">

        <label>File <input type="file" accept=".json,.json.gz" @change="setFile"></label>

        </span>

      </div>

      <div>

        <label>Limit <input type="text" size="5" placeholder="100" v-model.trim.lazy="limit"></label>

        <span v-show="mode == 'abuselog'">

          <label>Filters <input type="text" size="10" v-model.trim.lazy="filter"></label>

        </span>

        <span v-show="mode == 'recentchanges' || mode == 'revisions'">

          <label>Namespace <input type="text" size="4" v-model.trim.lazy="namespace"></label>

          <label>Tag <input type="text" size="10" v-model.trim.lazy="tag"></label>

        </span>

      </div>

      <div>

        <label>User <input type="text" size="12" v-model.trim.lazy="user"></label>

        <label>Title <input type="text" size="12" v-model.trim.lazy="title"></label>

        <span v-show="mode == 'abuselog'">

          <label>Log ID <input type="text" size="9" v-model.trim.lazy="logid"></label>

        </span>

        <span v-show="mode == 'revisions'">

          <label>Rev ID <input type="text" size="9" v-model.trim.lazy="revids"></label>

        </span>

      </div>

      <div>

        <label>After <input type="text" size="12" v-model.trim.lazy="end"></label>

        <label>Before <input type="text" size="12" v-model.trim.lazy="start"></label>

        <span v-show="mode == 'recentchanges' || mode == 'revisions'">

          <label>Show <input type="text" size="7" v-model.trim.lazy="show"></label>

        </span>

      </div>

      <div>

        <label><input type="checkbox" v-model="showMatches"> Matches</label>

        <label><input type="checkbox" v-model="showNonMatches"> Non-matches</label>

        <label><input type="checkbox" v-model="showUndef"> Untested</label>

        <label><input type="checkbox" v-model="showErrors"> Errors</label>

        <label><input type="checkbox" v-model="markAll"> Mark all</label>

        <a style="float: right;" v-if="!showAdvanced" @click="showAdvanced=true">[more]</a>

      </div>

      <div v-show="showAdvanced">

        <label>Threads <input type="number" min="1" max="16" size="2" v-model="threads"></label>

        <button @click="restart">Restart worker</button>

        <button @click="clearCache">Clear cache</button>

        <label><input type="checkbox" v-model="shortCircuit"> Quick eval</label>

        <a style="float: right;" @click="showAdvanced=false">[less]</a>

      </div>

    </div>

  </div>

  <div class="fdb-column-resizer" @mousedown.prevent="resize($event, $refs.secondCol, 'x')"></div>

  <div class="fdb-second-col" ref="secondCol">

    <div class="fdb-panel fdb-selected-result" v-show="topSelect != 'none'">

       <hit v-if="batch.length" :entry="batch[selectedHit]" :type="topSelect"></hit>

    </div>

    <div class="fdb-row-resizer" @mousedown.prevent="resize($event, $refs.batchPanel, 'y')"></div>

    <div class="fdb-panel">

    &#x2191; <select class="fdb-result-select" v-model="topSelect">

      <option value="none">(none)</option>

      <option value="result">(result)</option>

      <option value="matches">(matches)</option>

      <option value="diff">(diff)</option>

      <option value="vardump">(vardump)</option>

      <option v-for="name of varnames" :value="'var-' + name">{{name}}</option>

    </select>

    &#x2193; <select class="fdb-result-select" v-model="bottomSelect">

      <option value="none">(none)</option>

      <option value="result">(result)</option>

      <option value="diff">(diff)</option>

      <option value="matches">(matches)</option>

      <option v-for="name of varnames" :value="'var-' + name">{{name}}</option>

    </select>

    </div>

    <div class="fdb-row-resizer" @mousedown.prevent="resize($event, $refs.batchPanel, 'y')"></div>

    <div class="fdb-panel fdb-batch-results" ref="batchPanel" :class="{'fdb-show-matches': showMatches, 'fdb-show-nonmatches': showNonMatches, 'fdb-show-errors': showErrors, 'fdb-show-undef': showUndef}" v-show="bottomSelect != 'none'">

      <batch :batch="batch" :dategroups="dategroups" :type="bottomSelect" @selecthit="selectHit"></batch>

    </div>

  </div>

</div>

`

});



;// CONCATENATED MODULE: ./style/ui.css

const ui_namespaceObject = ".fdb-ace-marker {\n    position: absolute;\n}\n.fdb-batch-results .fdb-hit {\n    border-width: 0px 0px 1px 0px;\n    border-style: solid;\n}\n.fdb-batch-results .fdb-hit:focus {\n    outline: 2px inset black;\n    border-style: none;\n}\n.fdb-match {\n    background-color: #DDFFDD;\n}\n.fdb-match1 {\n    background-color: #EEFFEE;\n}\n.fdb-nonmatch {\n    background-color: #FFDDDD;\n}\n.fdb-undef {\n    background-color: #CCCCCC;\n}\n.fdb-error {\n    background-color: #FFBBFF;\n}\n.fdb-regexmatch {\n    background-color: #AAFFAA;\n    outline: 1px solid #00FF00;\n}\n\n.fdb-filter-revision {\n    width: 15em;\n}\n\n.fdb-controls div {\n    padding: 2px;\n}\n\n.fdb-batch-results .fdb-match, .fdb-batch-results .fdb-nonmatch, .fdb-batch-results .fdb-undef, .fdb-batch-results .fdb-error {\n    padding-left: 25px;\n    background-repeat: no-repeat;\n    background-position: left center;\n}\n\n.fdb-batch-results .fdb-match {\n    background-image: url(https://upload.wikimedia.org/wikipedia/en/thumb/f/fb/Yes_check.svg/18px-Yes_check.svg.png);\n}\n\n.fdb-batch-results .fdb-nonmatch {\n    background-image: url(https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Red_x.svg/18px-Red_x.svg.png);\n}\n\n.fdb-batch-results .fdb-undef {\n    background-image: url(https://upload.wikimedia.org/wikipedia/en/thumb/e/e0/Symbol_question.svg/18px-Symbol_question.svg.png);\n}\n\n.fdb-batch-results .fdb-error {\n    background-image: url(https://upload.wikimedia.org/wikipedia/en/thumb/b/b4/Ambox_important.svg/18px-Ambox_important.svg.png);\n}\n\n.fdb-matchedtext {\n    font-weight: bold;\n    background-color: #88FF88;\n}\n\n.fdb-parseerror, .fdb-parseerror {\n    background-color: #FFBBFF;\n    outline: 1px solid #FF00FF;\n}\n\n.fdb-outer {\n    height: 95vh;\n    width: 100%;\n}\n.fdb-wrapper {\n    height: 100%;\n    width: 100%;\n    display: flex;\n    background: #F8F8F8;\n\n}\n.fdb-first-col {\n    display: flex;\n    flex-direction: column;\n    flex: 1;\n    margin: 2px;\n}\n.fdb-column-resizer {\n    display: flex;\n    width: 0px;\n    padding: 0.5em;\n    margin: -0.5em;\n    cursor: col-resize;\n    z-index: 0;\n}\n.fdb-row-resizer {\n    display: flex;\n    height: 0px;\n    padding: 0.5em;\n    margin: -0.5em;\n    cursor: row-resize;\n    z-index: 0;\n}\n\n.fdb-second-col {\n    display: flex;\n    flex-direction: column;\n    width: 45%;\n    height: 100%;\n    margin: 2px;\n}\n.fdb-panel {\n    border: 1px solid black;\n    background: white;\n    padding: 2px;\n    width: 100%;\n    box-sizing: border-box;\n    margin: 2px;\n}\n.fdb-selected-result {\n    overflow: auto;\n    flex: 1;\n    word-wrap: break-word;\n    font-family: monospace;\n    white-space: pre-wrap;\n    word-wrap: break-word;\n}\n.fdb-batch-results {\n    overflow: auto;\n    height: 75%;\n    word-wrap: break-word;\n}\n.fdb-status {\n    float: right;\n    font-style: italic;\n}\n\n.fdb-result-select {\n    display: inline;\n    width: 40%;\n    overflow: hidden;\n}\n.fdb-ace-editor, .fdb-textbox-editor {\n    width: 100%;\n    height: 100%;\n    display: block;\n    resize: none;\n}\n.fdb-editor {\n    flex-basis: 20em;\n    flex-grow: 1;\n}\ndiv.mw-abusefilter-editor {\n    height: 100%;\n}\n.fdb-controls {\n    flex-basis: content;\n}\n.fdb-filtersnippet {\n    background: #DDD;\n}\n.fdb-matchresult {\n    font-family: monospace;\n    font-size: 12px;\n    line-height: 17px;\n}\n.fdb-dateheader {\n    position: sticky;\n    top: 0px;\n    font-weight: bold;\n    background-color: #F0F0F0;\n    border-width: 0px 0px 1px 0px;\n    border-style: solid;\n    border-color: black;\n}\n\n.fdb-diff {\n    background: white;\n}\n.fdb-added {\n    background: #D8ECFF;\n    font-weight: bold;\n}\n.fdb-removed {\n    background: #FEECC8;\n    font-weight: bold;\n}\n\n@supports selector(.fdb-dateheader:has(~ .fdb-match)) {\n    .fdb-dateheader {\n\tdisplay: none;\n    }\n    .fdb-show-matches .fdb-dateheader:has(~ .fdb-match) {\n\tdisplay: block;\n    }\n    .fdb-show-nonmatches .fdb-dateheader:has(~ .fdb-nonmatch) {\n\tdisplay: block;\n    }\n    .fdb-show-errors .fdb-dateheader:has(~ .fdb-error) {\n\tdisplay: block;\n    }\n    .fdb-show-undef .fdb-dateheader:has(~ .fdb-undef) {\n\tdisplay: block;\n    }\n}\n\n.fdb-batch-results .fdb-match {\n    display: none;\n}\n.fdb-batch-results .fdb-nonmatch {\n    display: none;\n}\n.fdb-batch-results .fdb-error {\n    display: none;\n}\n.fdb-batch-results .fdb-undef {\n    display: none;\n}\n\n.fdb-show-matches .fdb-match {\n    display: block;\n}\n.fdb-show-nonmatches .fdb-nonmatch {\n    display: block;\n}\n.fdb-show-errors .fdb-error {\n    display: block;\n}\n.fdb-show-undef .fdb-undef {\n    display: block;\n}\n";

;// CONCATENATED MODULE: ./src/ui.js

/* globals mw, Vue */









function setup() {

	mw.util.addCSS(ui_namespaceObject);



	if (typeof Vue.configureCompat == 'function')

		Vue.configureCompat({ MODE: 3 });



	document.getElementById('firstHeading').innerText = document.title = "Debugging edit filter";

	document.getElementById("mw-content-text").innerHTML = '<div class="fdb-outer"></div>';



	Vue.createApp(Main).mount(".fdb-outer");

}







window.FilterDebugger = __webpack_exports__;

/******/ })()

;//</nowiki>

Videos

Youtube | Vimeo | Bing

Websites

Google | Yahoo | Bing

Encyclopedia

Google | Yahoo | Bing

Facebook