User:Suffusion of Yellow/fdb-core.js

// /* 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: definition[key] }); /******/ 				} /******/ 			} /******/ 		}; /******/ 	}); /******/ 	/******/ 	/* 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) });

class FilterEvaluator { constructor(options) { let blob = new Blob(['importScripts("https://en.wikipedia.org/w/index.php?title=User:Suffusion_of_Yellow/fdb-worker.js&action=raw&ctype=text/javascript");'], { type: "text/javascript" });
 * // CONCATENATED MODULE: ./src/filter.js

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.workers[i] = new Worker(URL.createObjectURL(blob), { type: 'classic' });

this.workers[i].onmessage = (event) => { if (this.status && event.data.status) this.status(event.data.status);

if (event.data.uid && this.callbacks[event.data.uid]) { this.callbacks[event.data.uid](event.data); delete this.callbacks[event.data.uid]; }			};

if (i == 0) { if (this.threads > 1) this.workers[i].postMessage({						action: "setsecondaries",						ports: channels.map(c => c.port1)					}, channels.map(c => c.port1)); } else { this.workers[i].postMessage({					action: "setprimary",					port: channels[i - 1].port2				}, [channels[i - 1].port2]); }		}	}

work(data, i = 0) { return new Promise((resolve) => {			data.uid = ++this.uid;			this.callbacks[this.uid] = (data) => resolve(data);

this.workers[i].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.owners[id]);

return response.vardump; }	async getDiff(id) { let response = await this.work({			action: "diff",			vardump_id: id		}, this.owners[id]);

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.batch[i], ...{						details: await this.getVar("*", this.batch[i].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));

tasks[this.owners[entry.id]].push(task); }

for (let i = 0; i < this.threads; i++) (async => {				for (let task of tasks[i]) {					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; } }

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/parserdata.js

/* globals mw */
 * // CONCATENATED MODULE: ./src/Hit.js

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.inputs[matchinfo.arg_haystack];

let start = Math.max(matchinfo.match[0] - this.matchContext, 0); let end = Math.min(matchinfo.match[1] + this.matchContext, input.length);

let pre = (start == 0 ? "" : "...") + input.slice(start, matchinfo.match[0]); let post = input.slice(matchinfo.match[1], end) + (end == input.length ? "" : "..."); let match = input.slice(matchinfo.match[0], matchinfo.match[1]);

html += ' ' + sanitizedSpan(pre) + sanitizedSpan(match, "fdb-matchedtext") + sanitizedSpan(post) + ' ';				}			}			return html; },		prettydiff { let html = ' '; for (let i = 0; i < this.diff.length; i++) { let hunk = this.diff[i]; if (hunk[0] == -1) html += sanitizedSpan(hunk[1], "fdb-removed"); else if (hunk[0] == 1) html += sanitizedSpan(hunk[1], "fdb-added"); else { let common = hunk[1]; 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 += " "; 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: `  |  |      ` });


 * // 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: `    ` });

/* globals mw, ace */
 * // CONCATENATED MODULE: ./src/Editor.js

// @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.edit(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.abusefilters[0].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 (markers[id].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 (!ranges[key]) ranges[key] = { start: log.start, end: log.end, total: 0, tested: 0, matches: 0, errors: 0 };

ranges[key].total++; if (log.error) ranges[key].errors++; else if (log.result !== undefined) ranges[key].tested++; if (log.result) ranges[key].matches++;

for (let match of log.details?.matches ?? []) { for (let regexRange of match.ranges ?? []) { let key = `${regexRange.start} ${regexRange.end}`;

if (!ranges[key]) ranges[key] = { 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: `  ` });

/* globals mw, Vue */
 * // CONCATENATED MODULE: ./src/Main.js

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.params[0] }));		}	},	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(rest[i])) params[rest[i]] = rest[i + 1];

for (let [param, value] of (new URL(window.location)).searchParams) if (validURLParams.includes(param)) params[param] = 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 (params[param] !== undefined) { let encoded = mw.util.wikiUrlencode(params[param]).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 = this[param];

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

params[param] = 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(batch[i].timestamp); let date = `${d.getUTCDate} ${mw.language.months.names[d.getUTCMonth]} ${d.getUTCFullYear}`; let time = `${("" + d.getUTCHours).padStart(2, "0")}:${("" + d.getUTCMinutes).padStart(2, "0")}`; let entry = { ...batch[i], date, time };

if (this.dategroups.length == 0 || date != this.dategroups[this.dategroups.length - 1].date) { this.dategroups.push({						date,						batch: [i]					}); } else { this.dategroups[this.dategroups.length - 1].batch.push(i); }

this.batch.push(entry); }

if (params.logid && this.batch.length) this.$refs.editor.loadFilter(this.batch[0].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++) promises[i].then(result => {					this.batch[i].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.files[0]; 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: `  Waiting...         Wrap  ACE  FS        <input type="text" size="4" v-model.lazy.trim="loadableFilter"> <select class="fdb-filter-revision" v-model="filterRevision"> (cur) <option v-for="rev of filterRevisions" :value="rev.id"> <button @click="loadFilter">Load filter <select v-model="mode"> Abuse log <option value="recentchanges">Recent changes Revisions Local file <button @click="getBatch">Fetch data <button @click="download" :disabled="mode == 'file' || !batch.length">Save...        <a style="display:none;" download="dump.json.gz" ref="hiddenDownload"></a> <span v-show="mode == 'recentchanges' || mode == 'revisions'"> <input type="checkbox" v-model="expensive"> Fetch slow vars File <input type="file" accept=".json,.json.gz" @change="setFile"> Limit <input type="text" size="5" placeholder="100" v-model.trim.lazy="limit"> Filters <input type="text" size="10" v-model.trim.lazy="filter"> <span v-show="mode == 'recentchanges' || mode == 'revisions'"> Namespace <input type="text" size="4" v-model.trim.lazy="namespace"> Tag <input type="text" size="10" v-model.trim.lazy="tag"> User <input type="text" size="12" v-model.trim.lazy="user"> Title <input type="text" size="12" v-model.trim.lazy="title"> Log ID <input type="text" size="9" v-model.trim.lazy="logid"> Rev ID <input type="text" size="9" v-model.trim.lazy="revids"> After <input type="text" size="12" v-model.trim.lazy="end"> Before <input type="text" size="12" v-model.trim.lazy="start"> <span v-show="mode == 'recentchanges' || mode == 'revisions'"> Show <input type="text" size="7" v-model.trim.lazy="show"> <input type="checkbox" v-model="showMatches"> Matches <input type="checkbox" v-model="showNonMatches"> Non-matches <input type="checkbox" v-model="showUndef"> Untested <input type="checkbox" v-model="showErrors"> Errors <input type="checkbox" v-model="markAll"> Mark all <a style="float: right;" v-if="!showAdvanced" @click="showAdvanced=true">[more]</a> Threads <input type="number" min="1" max="16" size="2" v-model="threads"> <button @click="restart">Restart worker <button @click="clearCache">Clear cache <input type="checkbox" v-model="shortCircuit"> Quick eval <a style="float: right;" @click="showAdvanced=false">[less]</a> <div class="fdb-column-resizer" @mousedown.prevent="resize($event, $refs.secondCol, 'x')"> <div class="fdb-panel fdb-selected-result" v-show="topSelect != 'none'"> <hit v-if="batch.length" :entry="batch[selectedHit]" :type="topSelect"> <div class="fdb-row-resizer" @mousedown.prevent="resize($event, $refs.batchPanel, 'y')"> &#x2191; <select class="fdb-result-select" v-model="topSelect"> (none) (result) (matches) (diff) (vardump) <option v-for="name of varnames" :value="'var-' + name"> &#x2193; <select class="fdb-result-select" v-model="bottomSelect"> (none) (result) (diff) (matches) <option v-for="name of varnames" :value="'var-' + name"> <div class="fdb-row-resizer" @mousedown.prevent="resize($event, $refs.batchPanel, 'y')"> <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"> ` });

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"; /* globals mw, Vue */
 * // CONCATENATED MODULE: ./style/ui.css
 * // CONCATENATED MODULE: ./src/ui.js

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 = ' ';

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

window.FilterDebugger = __webpack_exports__; /******/ })