User:Suffusion of Yellow/fdb-core.dev.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | Documentation for this user script can be added at User:Suffusion of Yellow/fdb-core.dev. |
//<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: 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)
});
;// CONCATENATED MODULE: ./src/filter.js
class FilterEvaluator {
constructor(options) {
let blob = new Blob(['importScripts("https://en.wikipedia.org/w/index.php?title=User:Suffusion_of_Yellow/fdb-worker.dev.js&action=raw&ctype=text/javascript");'], { type: "text/javascript" });
this.version = {};
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(name, text, options = {}) {
if (!this.batch)
return [];
if (typeof this.version[name] == 'undefined')
this.version[name] = 1;
let version = ++this.version[name];
text = text.replaceAll("\r\n", "\n");
for (let i = 1; i < this.threads; i++)
this.work({
action: "setfilter",
filter_id: name,
filter: text,
}, i);
let response = await this.work({
action: "setfilter",
filter_id: name,
filter: text,
}, 0);
// Leftover response from last batch
if (this.version[name] != 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++) {
let taskGroup = tasks[i];
if (options.priority) {
let first = new Set(options.priority);
taskGroup = [
...taskGroup.filter(task => first.has(task.entry.id)),
...taskGroup.filter(task => !first.has(task.entry.id))
];
}
(async() => {
for (let task of taskGroup) {
let response = await this.work({
action: "evaluate",
filter_id: name,
vardump_id: task.entry.id,
scmode: options.scmode ?? "fast",
stash: options.stash,
usestash: options.usestash
}, i);
if (this.version[name] != version)
return;
response.version = version;
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|sfs_blocked|summary|timestamp|tor_exit_node|translate_source_text|translate_target_language|user_age|user_app|user_blocked|user_editcount|user_emailconfirm|user_groups|user_mobile|user_name|user_rights|user_type|user_unnamed_ip|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;
},
selectedResult() {
return this.type.slice(0, 7) == "result-" ? this.type.slice(7) : null;
},
selectedVar() {
return this.type.slice(0, 4) == "var-" ? this.type.slice(4) : null;
},
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)) :
mw.util.getUrl("Special:AbuseLog", {
wpSearchUser: this.entry.user
});
},
pagelink() {
return this.entry.filter_id == 0 ?
mw.util.getUrl("Special:PageHistory/" + mw.util.wikiUrlencode(this.entry.title)) :
mw.util.getUrl("Special:AbuseLog", {
wpSearchTitle: this.entry.title
});
},
result() {
return this.entry.results[this.selectedResult].error ??
JSON.stringify(this.entry.results[this.selectedResult].result, null, 2);
},
vardump() {
return JSON.stringify(this.vars ?? null, null, 2);
},
vartext() {
return JSON.stringify(this.vars?.[this.selectedVar] ?? null, null, 2);
},
matches() {
let html = "";
for (let log of this.entry.results.main.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 += '<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.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 += "</div>";
return html;
},
cls() {
if (!this.header)
return "";
if (this.entry.results.main === undefined)
return 'fdb-undef';
if (this.entry.results.main.error)
return 'fdb-error';
if (this.entry.results.main.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.results.main && entry.results.main.error && (selectedResult || type == 'matches')">{{entry.results.main.error}}</div>
<div v-else-if="type == 'matches' && entry.results.main" 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="selectedResult && entry.results[selectedResult]">{{result}}</div>
<div v-else-if="selectedVar">{{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
},
diffContext: {
type: Number,
default: 25
},
matchContext: {
type: Number,
default: 10
}
},
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 :diffContext="diffContext" :matchContext="matchContext"></hit>
</div>
</div>
`
});
;// CONCATENATED MODULE: ./src/Editor.js
/* globals mw, ace */
// @vue/component
/* harmony default export */ const Editor = ({
props: {
wrap: Boolean,
ace: Boolean,
simple: Boolean,
darkMode: Boolean,
modelValue: String
},
emits: ["textchange", "update:modelValue"],
data() {
return {
editor: Vue.shallowRef(null),
session: Vue.shallowRef(null),
lightModeTheme: "ace/theme/textmate",
darkModeTheme: "ace/theme/monokai",
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();
},
darkMode(newVal, oldVal) {
if (oldVal)
this.darkModeTheme = this.editor.getOption("theme");
else
this.lightModeTheme = this.editor.getOption("theme");
this.editor.setOption("theme", newVal ? this.darkModeTheme : this.lightModeTheme);
},
modelValue() {
this.text = this.modelValue;
},
text() {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.$emit('update:modelValue', 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");
this.editor = ace.edit(this.$refs.aceEditor);
this.session = this.editor.getSession();
this.session.setMode("ace/mode/abusefilter");
this.session.setUseWorker(false);
this.session.setOption("wrap", this.wrap);
if (this.simple) {
this.editor.setOptions({
highlightActiveLine: false,
showGutter: false,
showLineNumbers: false,
minLines: 1,
maxLines: 10
});
}
this.editor.setOption("theme", this.darkMode ? this.darkModeTheme : this.lightModeTheme);
ace.require('ace/range');
let observer = new ResizeObserver(() => this.editor.resize());
observer.observe(this.$refs.aceEditor);
this.text = this.modelValue;
this.session.setValue(this.text);
this.session.on("change", () => this.text = this.session.getValue());
},
methods: {
async loadFilter(id, revision, status) {
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 results of batch) {
for (let log of results?.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: `
<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"];
const localSettingsParams = ["wrap", "ace", "threads", "shortCircuit", "showAdvanced",
"topSelect", "bottomSelect", "showMatches", "showNonMatches",
"showUndef", "showErrors", "rememberSettings", "matchContext",
"diffContext" ];
// @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,
allPaths: false,
showMatches: true,
showNonMatches: true,
showErrors: true,
showUndef: true,
allHits: true,
showAdvanced: false,
threads: navigator.hardwareConcurrency || 2,
rememberSettings: false,
fullscreen: false,
diffContext: 25,
matchContext: 10,
topSelect: "diff",
bottomSelect: "matches",
topExpression: "",
bottomExpression: "",
varnames: [],
text: "",
timeout: 0,
batch: [],
dategroups: [],
selectedHit: 0,
status: "",
statusTimeout: null,
filterRevisions: [],
filterRevision: "",
canViewDeleted: false,
darkMode: false,
shared: Vue.shallowRef({ }),
help: {
wrap: "Wrap long lines",
ace: "Use the ACE editor. Required for highlighting matches in the filter",
fullscreen: "Fullscreen mode",
loadableFilter: "Load the filter with this ID into the editor",
filterRevision: "Load the filter revision with this timestamp. Might be unreliable.",
mode: "Fetch the log from this source",
modeAbuselog: "Fetch the log from one or more filters",
modeRecentchanges: "Generate the log from recent changes. Limited to the last 30 days, but 'Tag' and 'Show' will work even if no user or title is specified.",
modeRevisions: "Generate the log from any revisions. 'Show' option requires 'User'. 'Tag' option requires 'User' or 'Title'.",
modeDeleted: "Generate the log from deleted revisions. Requires 'User', 'Title', or 'Rev ID'.",
modeMixed: "Generate the log from a mix of deleted and live revisions. Requires 'User', 'Title', or 'Rev ID'.",
modeFile: "Fetch the filter log from a saved file",
download: "Save this batch to your computer. Use .gz extension to compress.",
expensive: "Generate 'expensive' variables requiring many slow queries. Required for these variables: new_html, new_text, all_links, old_links, added_links, removed_links, page_recent_contributors, page_first_contributor, page_age, global_user_groups, global_user_editcount",
file: "Name of local file. Must be either a JSON or gzip-compressed JSON file.",
limit: "Fetch up to this up this many entries",
filters: "Fetch only log entries matching these filter IDs. Separate with pipes.",
namespace: "Namespace number",
tag: "Fetch entries matching this edit tag. Ignored unless user or title is specified.",
user: "Fetch entries match this username, IP, or range. Ranges are not supported in 'abuselog' mode",
title: "Fetch entries matching this page title",
logid: "Fetch this AbuseLog ID",
revids: "Fetch entries from these revision IDs. Separate with pipes.",
end: "Fetch entries from on or after this timestamp (YYYY-MM-DDThh:mm:ssZ)",
start: "Fetch entries from on or before this timestamp (YYYY-MM-DDThh:mm:ssZ)",
showRecentChanges: "Any of !anon, !autopatrolled, !bot, !minor, !oresreview, !patrolled, !redirect, anon, autopatrolled, bot, minor, oresreview, patrolled, redirect, unpatrolled. Separate multiple options with pipes.",
showRevisions: "Ignored unless user is specified. Any of !autopatrolled, !minor, !new, !oresreview, !patrolled, !top, autopatrolled, minor, new, oresreview, patrolled, top. Separate multiple options with pipes.",
showMatches: "Show entries matching the filter",
showNonMatches: "Show entries NOT matching the filter",
showUndef: "Show entries which have not been tested yet",
showErrors: "Show entries triggering evaluation errors",
allHits: "Highlight all matches in the filter editor, not just the selected one",
threads: "Number of worker threads. Click 'Restart worker' for this to take effect.",
restart: "Restart all worker threads",
allPaths: "Evaluate all paths in the filter. Slower, but shows matches on the 'path not taken'. Does not affect final result.",
clearCache: "Delete all cached variable dumps",
diffContext: "Number of characters to display before and after changes",
matchContext: "Number of characters to display before and after matches",
rememberSettings: "Save some settings in local storage. Uncheck then refresh the page to restore all settings.",
selectResult: "Show filter evaluation result",
selectMatches: "Show strings matching regular expressions",
selectDiff: "Show an inline diff of the changes",
selectVardump: "Show all variables",
selectExpression: "Evaluate a second filter, re-using any variables",
selectVar: "Show variable: "
}
};
return { ...state, ...this.getParams() };
},
watch: {
fullscreen() {
if (this.fullscreen)
this.$refs.wrapper.requestFullscreen();
else if (document.fullscreenElement)
document.exitFullscreen();
},
allHits() {
this.markRanges("main", this.allHits);
},
allPaths() {
this.evalMain();
},
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]
}));
},
text() {
this.evalMain();
},
topExpression() {
this.maybeEvalTopExpression()
},
bottomExpression() {
this.maybeEvalBottomExpression()
},
topSelect() {
this.maybeEvalTopExpression()
},
bottomSelect() {
this.maybeEvalBottomExpression()
}
},
beforeMount() {
let localSettings = mw.storage.getObject("filterdebugger-settings");
for (let setting of localSettingsParams) {
if (localSettings?.[setting] !== undefined)
this[setting] = localSettings[setting];
this.$watch(setting, this.updateSettings);
}
this.startEvaluator();
},
async mounted() {
let localSettings = mw.storage.getObject("filterdebugger-settings");
if (localSettings?.outerHeight?.length)
this.$refs.outer.style.height = localSettings.outerHeight;
if (localSettings?.secondColWidth?.length)
this.$refs.secondCol.style.width = localSettings.secondColWidth;
if (localSettings?.resultPanelHeight?.length)
this.$refs.resultPanel.style.height = localSettings.resultPanelHeight;
this.varnames = parserData.variables.split("|");
(new mw.Api()).get(
{ action: "query",
meta: "userinfo",
uiprop: "rights"
}).then((r) => {
if (r.query.userinfo.rights.includes("deletedtext"))
this.canViewDeleted = true;
});
this.getBatch();
addEventListener("popstate", () => {
Object.assign(this, this.getParams());
this.getBatch();
});
document.addEventListener("fullscreenchange", () => {
this.fullscreen = !!document.fullscreenElement;
});
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', this.darkModeSwitch);
new MutationObserver(this.darkModeSwitch)
.observe(document.documentElement, { attributes: true });
this.darkModeSwitch();
},
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 = new URL(mw.util.getUrl("Special:BlankPage/FilterDebug"), document.location.href);
let badtitle = validURLParams.some(p => params[p]?.match?.(/[#<>[\]|{}]|&.*;|~~~/));
for (let param of validURLParams.filter(p => params[p])) {
if (!badtitle)
url.pathname += `/${param}/${mw.util.wikiUrlencode(params[param])}`;
else
url.searchParams.set(param, params[param]);
}
return url.href;
},
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.loadFilter(params.filter, true);
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, results: {} };
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 == 1)
this.loadFilter(this.batch[0].filter_id, true);
this.evalMain();
},
updateSettings() {
if (this.rememberSettings) {
let localSettings = {};
for(let setting of localSettingsParams)
localSettings[setting] = this[setting];
localSettings.outerHeight = this.$refs.outer.style.height;
localSettings.secondColWidth = this.$refs.secondCol.style.width;
localSettings.resultPanelHeight = this.$refs.resultPanel.style.height;
mw.storage.setObject("filterdebugger-settings", localSettings);
} else {
mw.storage.remove("filterdebugger-settings");
}
},
loadFilter(filter, keep) {
if (keep && this.text.trim().length)
return;
if (typeof filter != 'undefined') {
this.loadableFilter = filter;
this.filterRevision = "";
}
this.$refs.mainEditor.loadFilter(this.loadableFilter, this.filterRevision, 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.evalMain();
},
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.allHits = false;
this.markRanges("main", false);
this.markRanges("top", false);
},
markRanges(name, markAll) {
let batch = markAll ?
this.batch :
this.batch.slice(this.selectedHit, this.selectedHit + 1);
this.$refs[name + "Editor"]?.markRanges?.(batch.map(entry => entry.results?.[name]));
},
async doEval(name, text, stash, usestash, markAll, showStatus) {
this.$refs[name + "Editor"]?.clearAllMarkers?.();
let promises = [];
let startTime = performance.now();
let evaluated = 0;
let matches = 0;
let errors = 0;
try {
promises = await this.shared.evaluator.evalBatch(name, text, {
scmode: this.allPaths ? "allpaths" : "blank",
stash,
usestash,
priority: [this.batch[this.selectedHit]?.id]
});
} catch (error) {
if (typeof error.start == 'number' && typeof error.end == 'number') {
if (showStatus)
this.updateStatus(error.error);
this.batch.forEach(entry => delete entry.results[name]);
this.$refs[name + "Editor"]?.markParseError?.(error);
return;
} else {
throw error;
}
}
for (let i = 0; i < promises.length; i++)
promises[i].then(result => {
this.batch[i].results[name] = result;
if (!markAll && i == this.selectedHit)
this.markRanges(name, false);
if (showStatus) {
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);
if (markAll)
this.markRanges(name, true);
},
async evalMain() {
await this.doEval("main", this.text, "main", null, this.allHits, true);
this.maybeEvalTopExpression();
this.maybeEvalBottomExpression();
},
maybeEvalTopExpression() {
if (this.topSelect == "result-top")
this.doEval("top", this.topExpression, null, "main", false, false);
},
maybeEvalBottomExpression() {
if (this.bottomSelect == "result-bottom")
this.doEval("bottom", this.bottomExpression, null, "main", true, false);
},
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, axis, dir = 1, suffix = "%", min = .05, max = .95) {
let clientSize = axis == 'x' ? "clientWidth" : "clientHeight";
let clientPos = axis == 'x' ? "clientX" : "clientY";
let style = axis == 'x' ? "width" : "height";
let start = target[clientSize] + dir * event[clientPos];
let move = (event) => {
let parent = suffix == "vh" || suffix == "vw" ?
document.documentElement : target.parentElement;
let fraction = (start - dir * event[clientPos]) / parent[clientSize];
fraction = Math.min(Math.max(min, fraction), max);
target.style[style] = (100 * fraction) + suffix;
}
let stop = () => {
document.body.removeEventListener("mousemove", move);
this.updateSettings();
}
document.body.addEventListener("mousemove", move);
document.body.addEventListener("mouseup", stop, { once: true });
document.body.addEventListener("mouseleave", stop, { once: true });
},
darkModeSwitch() {
let classList = document.documentElement.classList;
this.darkMode =
classList.contains("skin-theme-clientpref-night") ||
(classList.contains("skin-theme-clientpref-os") &&
matchMedia("(prefers-color-scheme: dark)").matches);
}
},
template: `
<div class="fdb-outer" ref="outer">
<div class="fdb-wrapper" ref="wrapper" :class="{'fdb-dark-mode':darkMode}">
<div class="fdb-first-col">
<div class="fdb-panel fdb-editor">
<editor ref="mainEditor" :ace="ace" :wrap="wrap" :darkMode="darkMode" v-model="text"></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 :title="help.loadableFilter">Filter <input type="text" v-model.lazy.trim="loadableFilter" v-on:keyup.enter="loadFilter()"></label>
<label class="fdb-large"><select :title="help.filterRevision" class="fdb-filter-revision" v-model="filterRevision">
<option value="">(cur)</option>
<option v-for="rev of filterRevisions" :value="rev.id">{{rev.id}} - {{rev.timestamp}} - {{rev.user}}</option>
</select></label>
<button @click="loadFilter()">Load</button>
<label :title="help.allPaths"><input type="checkbox" v-model="allPaths"> All paths</label>
<label :title="help.allHits"><input type="checkbox" v-model="allHits"> All hits</label>
<label :title="help.ace"><input type="checkbox" v-model="ace"> ACE</label>
<label :title="help.wrap"><input type="checkbox" v-model="wrap"> Wrap</label>
</div>
<div>
<label :title="help.mode">Source <select v-model="mode">
<option :title="help.modeAbuselog" value="abuselog">Abuse log</option>
<option :title="help.modeRecentchanges" value="recentchanges">Recent changes</option>
<option :title="help.modeRevisions" value="revisions">Revisions</option>
<option :title="help.modeDeleted" v-show="canViewDeleted" value="deletedrevisions">Deleted</option>
<option :title="help.modeMixed" v-show="canViewDeleted" value="revisions|deletedrevisions">Live + deleted</option>
<option :title="help.modeFile" value="file">Local file</option>
</select></label>
<label :title="help.limit">Limit <input type="text" placeholder="100" v-model.trim.lazy="limit" v-on:keyup.enter="getBatch"></label>
<label class="fdb-large" :title="help.logid" v-show="mode == 'abuselog'">Log ID <input type="text" v-model.trim.lazy="logid" v-on:keyup.enter="getBatch"></label>
<label class="fdb-large" :title="help.revids" v-show="mode.includes('revisions')">Rev ID <input type="text" v-model.trim.lazy="revids" v-on:keyup.enter="getBatch"></label>
</div>
<div>
<label class="fdb-large" :title="help.filters" v-show="mode == 'abuselog'">Filters <input type="text" v-model.trim.lazy="filter" v-on:keyup.enter="getBatch"></label>
<label :title="help.namespace" v-show="mode == 'recentchanges' || mode.includes('revisions')">Namespace <input type="text" v-model.trim.lazy="namespace" v-on:keyup.enter="getBatch"></label>
<label class="fdb-large" :title="help.tag" v-show="mode == 'recentchanges' || mode.includes('revisions')">Tag <input type="text" v-model.trim.lazy="tag" v-on:keyup.enter="getBatch"></label>
<label class="fdb-large" :title="mode == 'recentchanges' ? help.showRecentChanges : help.showRevisions" v-show="mode == 'recentchanges' || mode == 'revisions'">Show <input type="text" v-model.trim.lazy="show" v-on:keyup.enter="getBatch"></label>
<label class="fdb-large" :title="help.file" v-show="mode == 'file'">File <input type="file" accept=".json,.json.gz" @change="setFile"></label>
</div>
<div>
<label class="fdb-large" :title="help.user" v-on:keyup.enter="getBatch">User <input type="text" v-model.trim.lazy="user"></label>
<label class="fdb-large" :title="help.title" v-on:keyup.enter="getBatch">Title <input type="text" v-model.trim.lazy="title"></label>
<label :title="help.expensive" v-show="mode == 'recentchanges' || mode.includes('revisions')"><input type="checkbox" v-model="expensive"> Fetch all variables</label>
</div>
<div>
<label class="fdb-large" :title="help.end" v-on:keyup.enter="getBatch">After <input type="text" placeholder="YYYY-MM-DDThh:mm:ssZ" v-model.trim.lazy="end"></label>
<label class="fdb-large" :title="help.start" placeholder="YYYY-MM-DDThh:mm:ssZ" v-on:keyup.enter="getBatch">Before <input type="text" placeholder="YYYY-MM-DDThh:mm:ssZ" v-model.trim.lazy="start"></label>
<button @click="getBatch">Fetch data</button>
<a class="fdb-more" @click="showAdvanced=!showAdvanced">{{showAdvanced?"[less]":"[more]"}}</a>
</div>
<div v-show="showAdvanced">
<label :title="help.threads">Threads <input type="number" min="1" max="16" v-model="threads"></label>
<button :title="help.restart" @click="restart">Restart workers</button>
<button :title="help.clearCache" @click="clearCache">Clear cache</button>
<button :title="help.download" @click="download" :disabled="mode == 'file' || !batch.length">Save...</button><a style="display:none;" download="dump.json.gz" ref="hiddenDownload"></a>
</div>
<div v-show="showAdvanced">
<label class="fdb-large" :title="help.diffContext">Diff context <input type="number" min="0" v-model="diffContext"></label>
<label class="fdb-large" :title="help.matchContext">Match context <input type="number" min="0" v-model="matchContext"></label>
<label :title="help.rememberSettings"><input type="checkbox" v-model="rememberSettings"> Remember settings</label>
</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'" ref="resultPanel">
<hit v-if="batch.length" :entry="batch[selectedHit]" :type="topSelect" :diffContext="diffContext" :matchContext="matchContext"></hit>
</div>
<div class="fdb-row-resizer" v-show="topSelect != 'none' && bottomSelect != 'none'" @mousedown.prevent="resize($event, $refs.resultPanel, 'y', -1)"></div>
<div class="fdb-panel fdb-mini-editor" :darkMode="darkMode" v-show="topSelect =='result-top'">
<editor ref="topEditor" :ace="ace" wrap simple v-model="topExpression"></editor>
</div>
<div class="fdb-panel fdb-controls fdb-batch-controls">
<div>
<label class="fdb-large">↑ <select class="fdb-result-select" v-model="topSelect">
<option value="none">(none)</option>
<option :title="help.selectResult" value="result-main">(result)</option>
<option :title="help.selectMatches" value="matches">(matches)</option>
<option :title="help.selectDiff" value="diff">(diff)</option>
<option :title="help.selectVardump" value="vardump">(vardump)</option>
<option :title="help.selectExpression" value="result-top">(expression)</option>
<option :title="help.selectVar + name" v-for="name of varnames" :value="'var-' + name">{{name}}</option>
</select></label>
<label class="fdb-large">↓ <select class="fdb-result-select" v-model="bottomSelect">
<option :title="help.selectResult" value="result-main">(result)</option>
<option :title="help.selectMatches" value="matches">(matches)</option>
<option :title="help.selectDiff" value="diff">(diff)</option>
<option :title="help.selectExpression" value="result-bottom">(expression)</option>
<option :title="help.selectVar + name" v-for="name of varnames" :value="'var-' + name">{{name}}</option>
</select></label>
</div>
</div>
<div class="fdb-panel fdb-mini-editor" :darkMode="darkMode" v-show="bottomSelect =='result-bottom'">
<editor ref="bottomEditor" :ace="ace" wrap simple v-model="bottomExpression"></editor>
</div>
<div class="fdb-row-resizer" v-show="topSelect != 'none' && bottomSelect != 'none'" @mousedown.prevent="resize($event, $refs.resultPanel, 'y', -1)"></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" :diffContext="diffContext" :matchContext="matchContext"></batch>
</div>
<div class="fdb-panel fdb-controls" v-show="bottomSelect != 'none'">
<div v-show="bottomSelect != 'none'">
<label class="fdb-match" :title="help.showMatches"><input type="checkbox" v-model="showMatches"> Matches</label>
<label class="fdb-nonmatch" :title="help.showNonMatches"><input type="checkbox" v-model="showNonMatches"> Non-matches</label>
<label class="fdb-undef" :title="help.showUndef"><input type="checkbox" v-model="showUndef"> Untested</label>
<label class="fdb-error" :title="help.showErrors"><input type="checkbox" v-model="showErrors"> Errors</label>
<label :title="help.fullscreen" class="fdb-fullscreen"><input type="checkbox" v-model="fullscreen">⛶</label>
</div>
</div>
</div>
</div>
<div class="fdb-row-resizer" @mousedown.prevent="resize($event, $refs.outer, 'y', -1, 'vh', 0.5, 1.0)"></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-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(/media/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(/media/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(/media/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(/media/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\n.fdb-wrapper {\n height: 100%;\n width: 100%;\n display: flex;\n gap: 4px;\n background: #F8F8F8;\n}\n.fdb-first-col {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 4px;\n height: 100%;\n}\n.fdb-column-resizer {\n width: 0px;\n height: 100%;\n padding: 0.5em;\n margin: calc(-2px - 0.5em);\n cursor: col-resize;\n z-index: 0;\n}\n.fdb-row-resizer {\n height: 0px;\n width: 100%;\n padding: 0.5em;\n margin: calc(-2px - 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 gap: 4px;\n}\n.fdb-panel {\n border: 1px solid black;\n background: white;\n padding: 2px;\n width: 100%;\n box-sizing: border-box;\n}\n\n.fdb-selected-result {\n overflow: auto;\n height: 20%;\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 flex: 1;\n word-wrap: break-word;\n}\n\n.fdb-status {\n float: right;\n font-style: italic;\n}\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-mini-editor {\n min-height: 1.5em;\n}\n\n.fdb-controls {\n flex-basis: content;\n font-size: 90%;\n}\n\n.fdb-controls > div {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n text-wrap: nowrap;\n padding: 2px;\n gap: 2px;\n}\n\n.fdb-controls > div > * {\n display: block;\n flex-basis: content;\n}\n\n.fdb-controls .fdb-large {\n display: flex;\n gap: 2px;\n flex: 1;\n align-items: center;\n}\n\n.fdb-controls .fdb-fullscreen {\n margin-left: auto;\n}\n\n.fdb-controls .fdb-fullscreen checkbox {\n display: none;\n}\n\n.fdb-controls input:not([type=\"checkbox\"]) {\n width: 4em;\n flex-basis: content;\n}\n\n.fdb-controls .fdb-large input, .fdb-controls .fdb-large select {\n display: block;\n width: 4em;\n flex: 1;\n}\n\n.fdb-batch-controls {\n flex-basis: content;\n}\n\n.fdb-fullscreen {\n font-weight: bold;\n margin-left: auto;\n}\n.fdb-fullscreen input {\n display: none;\n}\n.fdb-more {\n margin-left: auto;\n}\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\n/* Vector-2022 fixes */\n.skin-vector-2022 .fdb-outer {\n height: calc(100vh - 75px); /* Make room for sticky header that apparently some people have */\n}\nhtml.client-js.vector-sticky-header-enabled {\n scroll-padding-top: 0px; /* Stop scroll position from jumping when typing */\n}\n\n/* Timeless fixes */\n.skin-timeless .fdb-outer {\n height: calc(100vh - 75px); /* Make room for sticky header */\n}\n.skin-timeless button, .skin-timeless select {\n padding: unset;\n}\n\n/* Dark mode, courtesy [[User:Daniel Quinlan]] */\n.fdb-dark-mode .fdb-match {\n color: #DDFFDD;\n background-color: var(--background-color-base);\n}\n.fdb-dark-mode .fdb-match1 {\n color: #EEFFEE;\n background-color: var(--background-color-base);\n}\n.fdb-dark-mode .fdb-nonmatch {\n color: #FFDDDD;\n background-color: var(--background-color-warning-subtle);\n}\n.fdb-dark-mode .fdb-undef {\n color: #CCCCCC;\n background-color: var(--background-color-base);\n}\n.fdb-dark-mode .fdb-error {\n color: #FFBBFF;\n background-color: var(--background-color-base);\n}\n.fdb-dark-mode .fdb-regexmatch {\n color: #AAFFAA;\n outline: 1px solid #00FF00;\n background-color: var(--background-color-base);\n}\n.fdb-dark-mode .fdb-matchedtext {\n color: #88FF88;\n background-color: var(--background-color-base);\n}\n.fdb-dark-mode .fdb-parseerror {\n color: #FFBBFF;\n outline: 1px solid #FF00FF;\n background-color: var(--background-color-base);\n}\n.fdb-wrapper.fdb-dark-mode {\n background-color: var(--background-color-base);\n}\n.fdb-dark-mode .fdb-panel {\n border: 1px solid var(--border-color-interactive);\n background: var(--background-color-neutral);\n}\n.fdb-dark-mode .fdb-filtersnippet {\n color: #DDD;\n background: var(--background-color-base);\n}\n.fdb-dark-mode .fdb-dateheader {\n color: var(--background-color-notice-subtle);\n border-color: var(--color-base);\n}\n.fdb-dark-mode .fdb-diff {\n background: var(--background-color-base);\n}\n.fdb-dark-mode .fdb-added {\n color: #22A622;\n background: var(--background-color-base);\n}\n.fdb-dark-mode .fdb-removed {\n color: #C62222;\n background: var(--background-color-base);\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-mountpoint"></div>';
let app = Vue.createApp(Main);
app.mount(".fdb-mountpoint");
}
window.FilterDebugger = __webpack_exports__;
/******/ })()
;//</nowiki>