User:Suffusion of Yellow/fdb-core.dev.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)

});

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

' +

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.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: `

{{entry.results.main.error}}

{{vardump}}

{{result}}

{{vartext}}

`

});

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

{{dategroup.date}}

`

});

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

`

});

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

Waiting...

{{showAdvanced?"[less]":"[more]"}}

`

});

;// 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(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\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 = '

';

let app = Vue.createApp(Main);

app.mount(".fdb-mountpoint");

}

window.FilterDebugger = __webpack_exports__;

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

;//