User:Chlod/Scripts/Deputy.js: Difference between revisions

Content deleted Content added
[6f254cc0] cci: fix CSRSB
(bot/CD)
 
(71 intermediate revisions by the same user not shown)
Line 28:
*
* https://github.com/ChlodAlejandro/deputy
*
* ------------------------------------------------------------------------
*
* This script compiles with the following dependencies:
* * [https://github.com/Microsoft/tslib tslib] - 0BSD, Microsoft
* * [https://github.com/jakearchibald/idb idb] - ISC, Jake Archibald
* * [https://github.com/JSmith01/broadcastchannel-polyfill broadcastchannel-polyfill] - Unlicense, Joshua Bell
* * [https://github.com/Lusito/tsx-dom tsx-dom] - MIT, Santo Pfingsten
*
*/
// <nowiki>
/*!
* @package tslib
* @version 2.4.0
* @license 0BSD
* @author Microsoft Corp.
* @url https://github.com/Microsoft/tslib
*//*!
* @package idb
* @version 7.0.2
* @license ISC
* @author Jake Archibald
* @url https://github.com/jakearchibald/idb
*//*!
* @package broadcastchannel-polyfill
* @version 1.0.1
* @license Unlicense
* @author Joshua Bell
* @url https://github.com/JSmith01/broadcastchannel-polyfill
*//*!
* @package tsx-dom
* @version 1.4.0
* @license MIT
* @author Santo Pfingsten
* @url https://github.com/Lusito/tsx-dom
*//*!
* @package @chlodalejandro/parsoid
* @version 2.0.0-f08be30
* @license MIT
* @author Chlod Alejandro
* @url https://github.com/ChlodAlejandro/parsoid-document
*/
(function () {
'use strict';
Line 79 ⟶ 56:
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
 
 
function __awaiter(thisArg, _arguments, P, generator) {
Line 89 ⟶ 68:
});
}
 
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
 
const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
Line 114 ⟶ 98:
]));
}
const cursorRequestMap = new WeakMap();
const transactionDoneMap = new WeakMap();
const transactionStoreNamesMap = new WeakMap();
const transformCache = new WeakMap();
const reverseTransformCache = new WeakMap();
Line 136 ⟶ 118:
request.addEventListener('error', error);
});
promise
.then((value) => {
// Since cursoring reuses the IDBRequest (*sigh*), we cache it for later retrieval
// (see wrapFunction).
if (value instanceof IDBCursor) {
cursorRequestMap.set(value, request);
}
// Catching to avoid "Uncaught Promise exceptions"
})
.catch(() => { });
// This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This
// is because we create many promises from a single IDBRequest.
Line 182 ⟶ 154:
if (prop === 'done')
return transactionDoneMap.get(target);
// Polyfill for objectStoreNames because of Edge.
if (prop === 'objectStoreNames') {
return target.objectStoreNames || transactionStoreNamesMap.get(target);
}
// Make tx.store return the only store in the transaction, or undefined if there are many.
if (prop === 'store') {
Line 214 ⟶ 182:
// Due to expected object equality (which is enforced by the caching in `wrap`), we
// only create one new func per func.
// Edge doesn't support objectStoreNames (booo), so we polyfill it here.
if (func === IDBDatabase.prototype.transaction &&
!('objectStoreNames' in IDBTransaction.prototype)) {
return function (storeNames, ...args) {
const tx = func.call(unwrap(this), storeNames, ...args);
transactionStoreNamesMap.set(tx, storeNames.sort ? storeNames.sort() : [storeNames]);
return wrap(tx);
};
}
// Cursor methods are special, as the behaviour is a little more different to standard IDB. In
// IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the
Line 233 ⟶ 192:
// the original object.
func.apply(unwrap(this), args);
return wrap(cursorRequestMap.get(this).request);
};
}
Line 286 ⟶ 245:
if (upgrade) {
request.addEventListener('upgradeneeded', (event) => {
upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
});
}
if (blocked) {
request.addEventListener('blocked', (event) => blocked());
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
event.oldVersion, event.newVersion, event));
}
openPromise
.then((db) => {
if (terminated)
db.addEventListener('close', () => terminated());
if (blocking) {
db.addEventListener('versionchange', (event) => blocking(event.oldVersion, event.newVersion, event));
}
})
.catch(() => { });
Line 346 ⟶ 309:
has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop),
}));
 
const advanceMethodProps = ['continue', 'continuePrimaryKey', 'advance'];
const methodMap = {};
const advanceResults = new WeakMap();
const ittrProxiedCursorToOriginalProxy = new WeakMap();
const cursorIteratorTraps = {
get(target, prop) {
if (!advanceMethodProps.includes(prop))
return target[prop];
let cachedFunc = methodMap[prop];
if (!cachedFunc) {
cachedFunc = methodMap[prop] = function (...args) {
advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
};
}
return cachedFunc;
},
};
async function* iterate(...args) {
// tslint:disable-next-line:no-this-assignment
let cursor = this;
if (!(cursor instanceof IDBCursor)) {
cursor = await cursor.openCursor(...args);
}
if (!cursor)
return;
cursor = cursor;
const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
// Map this double-proxy back to the original, so other cursor methods work.
reverseTransformCache.set(proxiedCursor, unwrap(cursor));
while (cursor) {
yield proxiedCursor;
// If one of the advancing methods was not called, call continue().
cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
advanceResults.delete(proxiedCursor);
}
}
function isIteratorProp(target, prop) {
return ((prop === Symbol.asyncIterator &&
instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor])) ||
(prop === 'iterate' && instanceOfAny(target, [IDBIndex, IDBObjectStore])));
}
replaceTraps((oldTraps) => ({
...oldTraps,
get(target, prop, receiver) {
if (isIteratorProp(target, prop))
return iterate;
return oldTraps.get(target, prop, receiver);
},
has(target, prop) {
return isIteratorProp(target, prop) || oldTraps.has(target, prop);
},
}));
 
var version = "0.9.0";
var gitAbbrevHash = "317b503";
var gitBranch = "HEAD";
var gitDate = "Wed, 19 Feb 2025 00:13:53 +0800";
var gitVersion = "0.9.0+g317b503";
 
/**
Line 357 ⟶ 380:
var _a;
return (_a = this._action) !== null && _a !== void 0 ? _a : (this._action = new mw.Api({
ajax: {
headers: {
'Api-User-Agent': `Deputy/${version} (https://w.wiki/7NWR; User:Chlod; wiki@chlod.net)`
}
},
parameters: {
format: 'json',
Line 374 ⟶ 402:
return (_a = this._rest) !== null && _a !== void 0 ? _a : (this._rest = new mw.Rest());
}
}
MwApi.USER_AGENT = `Deputy/${version} (https://w.wiki/7NWR; User:Chlod; wiki@chlod.net)`;
 
/**
* Log to the console.
*
* @param {...any} data
*/
function log(...data) {
console.log('[Deputy]', ...data);
}
 
Line 390 ⟶ 428:
upgrade(db, oldVersion, newVersion) {
let currentVersion = oldVersion;
// Adding new stores? Make sure to also add it in `reset()`!
const upgrader = {
0: () => {
Line 414 ⟶ 453:
while (currentVersion < newVersion) {
upgrader[`${currentVersion}`]();
console.log(`[deputy] upgradedUpgraded database from ${currentVersion} to ${currentVersion + 1}`);
currentVersion++;
}
Line 477 ⟶ 516:
}
}
});
}
/**
* Reset the Deputy database. Very dangerous!
*/
reset() {
return __awaiter(this, void 0, void 0, function* () {
yield this.db.clear('keyval');
yield this.db.clear('casePageCache');
yield this.db.clear('diffCache');
yield this.db.clear('diffStatus');
yield this.db.clear('pageStatus');
yield this.db.clear('tagCache');
});
}
Line 590 ⟶ 642:
revisionStatusUpdate: 'acknowledge',
pageNextRevisionRequest: 'pageNextRevisionResponse',
pageNextRevisionResponse: 'pageNextRevisionRequest',
userConfigUpdate: 'userConfigUpdate',
wikiConfigUpdate: 'wikiConfigUpdate'
};
// TODO: debug
Line 608 ⟶ 662:
this.broadcastChannel.addEventListener('message', (event) => {
// TODO: debug
console.log(Date.now() - start, '[deputycomms comms]in: in', event.data);
if (event.data && typeof event.data === 'object' && event.data._deputy) {
this.dispatchEvent(Object.assign(new Event(event.data.type), {
Line 627 ⟶ 681:
this.broadcastChannel.postMessage(message);
// TODO: debug
console.log(Date.now() - start, '[deputy comms]: out:', data);
return message;
}
Line 661 ⟶ 715:
};
handlers.listener = ((event) => {
console.log(event);
if (event.data._deputyRespondsTo === message._deputyMessageId &&
event.data.type === OneWayDeputyMessageMap[data.type]) {
Line 692 ⟶ 746:
*
* @param title The title to normalize. Default is current page.
* @return {mw.Title} A mw.Title object. `null` if not a valid title.
* @private
*/
Line 728 ⟶ 782:
titles: normalizeTitle(page).getPrefixedText()
})), { rvprop: 'ids|content', rvslots: 'main', rvlimit: '1' }), extraOptions)).then((data) => {
const fallbackText = extraOptions.fallbacktext;
if (data.query.pages[0].revisions == null) {
returnif null;(fallbackText) {
return Object.assign(fallbackText, {
page: data.query.pages[0]
});
}
else {
return null;
}
}
return Object.assign(data.query.pages[0].revisions[0].slots.main.content, {
Line 759 ⟶ 821:
return (_a = this.content) !== null && _a !== void 0 ? _a : (this.content = yield getPageContent(this.casePage.pageId));
});
}
/**
* Removes the cached wikitext for this page.
*/
resetCachedWikitext() {
this.content = undefined;
}
/**
Line 766 ⟶ 834:
*
* @param section The section to edit
* @param n If the section heading appears multiple times in the page and n is
* provided, this function extracts the nth occurrence of that section heading.
*/
getSectionWikitext(section, n = 1) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof section === 'number') {
return getPageContent(this.casePage.pageId, { rvsection: section }).then((v) => v.toString());{
return Object.assign(v.toString(), {
revid: v.revid
});
});
}
else {
Line 777 ⟶ 851:
let capturing = false;
let captureLevel = 0;
let currentN = 1;
const sectionLines = [];
for (let i = 0; i < wikitextLines.length; i++) {
const line = wikitextLines[i];
const headerCheck = /^(=={1,5})\s*(.+?)\s*=={1,5}$/.exec(line);
if (!capturing &&
headerCheck != null &&
headerCheck[2] === section) {
sectionLines.pushif (linecurrentN < n); {
capturing = true currentN++;
captureLevel = headerCheck[1].length;}
else {
sectionLines.push(line);
capturing = true;
captureLevel = headerCheck[1].length;
}
}
else if (capturing) {
if (headerCheck != null && headerCheck[1].length <= captureLevel) {
capturing = false;
break;
}
else {
Line 795 ⟶ 878:
}
}
return Object.assign(sectionLines.join('\n');, {
revid: wikitext.revid
});
}
});
}
}
 
/**
* @param element The element to get the name of
* @return the name of a section from its section heading.
*/
function sectionHeadingName(element) {
var _a, _b;
return (_b = (_a = element.querySelector('.mw-headline')) === null || _a === void 0 ? void 0 : _a.innerText) !== null && _b !== void 0 ? _b : element.innerText;
}
 
Line 832 ⟶ 908:
*/
class DeputyCase {
/**
* @param pageId The page ID of the case page.
* @param title The title of the case page.
*/
constructor(pageId, title) {
this.pageId = pageId;
this.title = title;
}
/**
* @return the title of the case page
Line 885 ⟶ 953:
return new DeputyCase(pageId, title);
});
}
/**
* @param pageId The page ID of the case page.
* @param title The title of the case page.
*/
constructor(pageId, title) {
this.pageId = pageId;
this.title = title;
}
/**
Line 894 ⟶ 970:
return DeputyCase.getCaseName(this.title);
}
}
 
/**
* Returns the last item of an array.
*
* @param array The array to get the last element from
* @return The last element of the array
*/
function last(array) {
return array[array.length - 1];
}
 
/**
* Each WikiHeadingType implies specific fields in {@link WikiHeading}:
*
* - `PARSOID` implies that there is no headline element, and that the `h`
* element is the root heading element. This means `h.innerText` will be
* "Section title".
* - `OLD` implies that there is a headline element and possibly an editsection
* element, and that the `h` is the root heading element. This means that
* `h.innerText` will be "Section title[edit | edit source]" or similar.
* - `NEW` implies that there is a headline element and possibly an editsection
* element, and that a `div` is the root heading element. This means that
* `h.innerText` will be "Section title".
*/
var WikiHeadingType;
(function (WikiHeadingType) {
WikiHeadingType[WikiHeadingType["PARSOID"] = 0] = "PARSOID";
WikiHeadingType[WikiHeadingType["OLD"] = 1] = "OLD";
WikiHeadingType[WikiHeadingType["NEW"] = 2] = "NEW";
})(WikiHeadingType || (WikiHeadingType = {}));
/**
* Get relevant information from an H* element in a section heading.
*
* @param headingElement The heading element
* @return An object containing the relevant {@link WikiHeading} fields.
*/
function getHeadingElementInfo(headingElement) {
return {
h: headingElement,
id: headingElement.id,
title: headingElement.innerText,
level: +last(headingElement.tagName)
};
}
/**
* Annoyingly, there are many different ways that a heading can be parsed
* into depending on the version and the parser used for given wikitext.
*
* In order to properly perform such wiki heading checks, we need to identify
* if a given element is part of a wiki heading, and perform a normalization
* if so.
*
* Since this function needs to check many things before deciding if a given
* HTML element is part of a section heading or not, this also acts as an
* `isWikiHeading` check.
*
* The layout for a heading differs depending on the MediaWiki version:
*
* <b>On 1.43+ (Parser)</b>
* ```html
* <div class="mw-heading mw-heading2">
* <h2 id="Parsed_wikitext...">Parsed <i>wikitext</i>...</h2>
* <span class="mw-editsection>...</span>
* </div>
* ```
*
* <b>On Parsoid</b>
* ```html
* <h2 id="Parsed_wikitext...">Parsed <i>wikitext</i>...</h2>
* ```
*
* <b>On pre-1.43</b>
* ```html
* <h2>
* <span class="mw-headline" id="Parsed_wikitext...">Parsed <i>wikitext</i>...</span>
* <span class="mw-editsection">...</span>
* </h2>
* ```
*
* <b>Worst case execution time</b> would be if this was run with an element which was
* outside a heading and deeply nested within the page.
*
* Backwards-compatibility support may be removed in the future. This function does not
* support Parsoid specification versions lower than 2.0.
*
* @param node The node to check for
* @param ceiling An element which `node` must be in to be a valid heading.
* This is set to the `.mw-parser-output` element by default.
* @return The root heading element (can be an &lt;h2&gt; or &lt;div&gt;),
* or `null` if it is not a valid heading.
*/
function normalizeWikiHeading(node, ceiling) {
var _a;
if (node == null) {
// Not valid input, obviously.
return null;
}
const rootNode = node.getRootNode();
// Break out of text nodes until we hit an element node.
while (node.nodeType !== node.ELEMENT_NODE) {
node = node.parentNode;
if (node === rootNode) {
// We've gone too far and hit the root. This is not a wiki heading.
return null;
}
}
// node is now surely an element.
let elementNode = node;
// If this node is the 1.43+ heading root, return it immediately.
if (elementNode.classList.contains('mw-heading')) {
return Object.assign({ type: WikiHeadingType.NEW, root: elementNode }, getHeadingElementInfo(Array.from(elementNode.children)
.find(v => /^H[123456]$/.test(v.tagName))));
}
// Otherwise, we're either inside or outside a mw-heading.
// To determine if we are inside or outside, we keep climbing up until
// we either hit an <hN> or a given stop point.
// The default stop point differs on Parsoid and standard parser:
// - On Parsoid, `<body>` will be `.mw-body-content.mw-parser-output`.
// - On standard parser, we want `div.mw-body-content > div.mw-parser.output`.
// If such an element doesn't
// exist in this document, we just stop at the root element.
ceiling = (_a = ceiling !== null && ceiling !== void 0 ? ceiling : elementNode.ownerDocument.querySelector('.mw-body-content > .mw-parser-output, .mw-body-content.mw-parser-output')) !== null && _a !== void 0 ? _a : elementNode.ownerDocument.documentElement;
// While we haven't hit a heading, keep going up.
while (elementNode !== ceiling) {
if (/^H[123456]$/.test(elementNode.tagName)) {
// This element is a heading!
// Now determine if this is a MediaWiki heading.
if (elementNode.parentElement.classList.contains('mw-heading')) {
// This element's parent is a `div.mw-heading`!
return Object.assign({ type: WikiHeadingType.NEW, root: elementNode.parentElement }, getHeadingElementInfo(elementNode));
}
else {
const headline = elementNode.querySelector(':scope > .mw-headline');
if (headline != null) {
// This element has a `.mw-headline` child!
return {
type: WikiHeadingType.OLD,
root: elementNode,
h: elementNode,
id: headline.id,
title: headline.innerText,
level: +last(elementNode.tagName)
};
}
else if (elementNode.parentElement.tagName === 'SECTION' &&
elementNode.parentElement.firstElementChild === elementNode) {
// A <section> element is directly above this element, and it is the
// first element of that section!
// This is a specific format followed by the 2.8.0 MediaWiki Parsoid spec.
// https://www.mediawiki.org/wiki/Specs/HTML/2.8.0#Headings_and_Sections
return {
type: WikiHeadingType.PARSOID,
root: elementNode,
h: elementNode,
id: elementNode.id,
title: elementNode.innerText,
level: +last(elementNode.tagName)
};
}
else {
// This is a heading, but we can't figure out how it works.
// This usually means something inserted an <h2> into the DOM, and we
// accidentally picked it up.
// In that case, discard it.
return null;
}
}
}
else if (elementNode.classList.contains('mw-heading')) {
// This element is the `div.mw-heading`!
// This usually happens when we selected an element from inside the
// `span.mw-editsection` span.
return Object.assign({ type: WikiHeadingType.NEW, root: elementNode }, getHeadingElementInfo(Array.from(elementNode.children)
.find(v => /^H[123456]$/.test(v.tagName))));
}
else {
// Haven't reached the top part of a heading yet, or we are not
// in a heading. Keep climbing up the tree until we hit the ceiling.
elementNode = elementNode.parentElement;
}
}
// We hit the ceiling. This is not a wiki heading.
return null;
}
 
/**
* Check if a given parameter is a wikitext heading parsed into HTML.
*
* Alias for `normalizeWikiHeading( el ) != null`.
*
* @param el The element to check
* @return `true` if the element is a heading, `false` otherwise
*/
function isWikiHeading(el) {
return normalizeWikiHeading(el) != null;
}
 
/**
* Finds section elements from a given section heading (and optionally a predicate)
*
* @param sectionHeading
* @param sectionHeadingPredicate A function which returns `true` if the section should stop here
* @return Section headings.
*/
function getSectionElements(sectionHeading, sectionHeadingPredicate = isWikiHeading) {
const sectionMembers = [];
let nextSibling = sectionHeading.nextElementSibling;
while (nextSibling != null && !sectionHeadingPredicate(nextSibling)) {
sectionMembers.push(nextSibling);
nextSibling = nextSibling.nextElementSibling;
}
return sectionMembers;
}
 
Line 902 ⟶ 1,191:
*/
class DeputyCasePage extends DeputyCase {
/**
* @param pageId The page ID of the case page.
* @param title The title of the page being accessed
* @param document The document to be used as a reference.
* @param parsoid Whether this is a Parsoid document or not.
*/
static build(pageId, title, document, parsoid) {
return __awaiter(this, void 0, void 0, function* () {
const cachedInfo = yield window.deputy.storage.db.get('casePageCache', pageId !== null && pageId !== void 0 ? pageId : window.deputy.currentPageId);
if (cachedInfo != null) {
if (pageId != null) {
// Title might be out of date. Recheck for safety.
title = yield getPageTitle(pageId);
}
// Fix for old data (moved from section name to IDs as of c5251642)
const oldSections = cachedInfo.lastActiveSections.some((v) => v.indexOf(' ') !== -1);
if (oldSections) {
cachedInfo.lastActiveSections =
cachedInfo.lastActiveSections.map((v) => v.replace(/ /g, '_'));
}
const casePage = new DeputyCasePage(pageId, title, document, parsoid, cachedInfo.lastActive, cachedInfo.lastActiveSections);
if (oldSections) {
// Save to fix the data in storage
yield casePage.saveToCache();
}
return casePage;
}
else {
return new DeputyCasePage(pageId, title, document, parsoid);
}
});
}
/**
* @param pageId The page ID of the case page.
Line 925 ⟶ 1,246:
this.lastActive = lastActive !== null && lastActive !== void 0 ? lastActive : Date.now();
this.lastActiveSections = lastActiveSessions !== null && lastActiveSessions !== void 0 ? lastActiveSessions : [];
}
/**
* @param pageId The page ID of the case page.
* @param title The title of the page being accessed
* @param document The document to be used as a reference.
* @param parsoid Whether this is a Parsoid document or not.
*/
static build(pageId, title, document, parsoid) {
return __awaiter(this, void 0, void 0, function* () {
const cachedInfo = yield window.deputy.storage.db.get('casePageCache', pageId !== null && pageId !== void 0 ? pageId : window.deputy.currentPageId);
if (cachedInfo != null) {
if (pageId != null) {
// Title might be out of date. Recheck for safety.
title = yield getPageTitle(pageId);
}
return new DeputyCasePage(pageId, title, document, parsoid, cachedInfo.lastActive, cachedInfo.lastActiveSections);
}
else {
return new DeputyCasePage(pageId, title, document, parsoid);
}
});
}
/**
Line 954 ⟶ 1,254:
*/
isContributionSurveyHeading(el) {
// All headingsif (h1,!(el h2,instanceof h3,HTMLElement)) h4, h5, h6){
// TODO: l10n return false;
}
const headlineElement = this.parsoid ? el : el.querySelector('.mw-headline');
returnconst /^H\d$/.testheading = normalizeWikiHeading(el.tagName) &&;
return headlineElementheading != null &&
/(Page|Article|Local/ file|File)s?Require \d+that (to|through)this \d+$/.test(headlineElementheading is already normalized.innerText);
// TODO: Remove at some point.
// This shouldn't be required if double-normalization wasn't a thing.
el === heading.h &&
// eslint-disable-next-line security/detect-non-literal-regexp
new RegExp(window.deputy.wikiConfig.cci.headingMatch.get()).test(heading.title);
}
/**
Line 967 ⟶ 1,272:
* @return The <h*> element of the heading.
*/
findFirstContributionSurveyHeadingfindFirstContributionSurveyHeadingElement() {
return this.findContributionSurveyHeadings()[0];
}
Line 973 ⟶ 1,278:
* Find a contribution survey heading by section name.
*
* @param sectionNamesectionIdentifier The section nameidentifier to look for, usually the section
* name unless `useId` is set to true.
* @param useId Whether to use the section name instead of the ID
* @return The <h*> element of the heading.
*/
findContributionSurveyHeading(sectionNamesectionIdentifier, useId = false) {
// No need to perform .mw-headline existence check here, already
// done by `findContributionSurveyHeadings`
return this.findContributionSurveyHeadings()
.find((v) => sectionHeadingNamenormalizeWikiHeading(v)[useId ? 'id' : 'title'] === sectionNamesectionIdentifier);
}
/**
Line 990 ⟶ 1,295:
findContributionSurveyHeadings() {
if (!DeputyCasePage.isCasePage()) {
throw new Error('Current page is not a case page. Expected subpage of '); +
DeputyCasePage.rootPage.getPrefixedText());
}
else {
Line 1,017 ⟶ 1,323:
*/
getContributionSurveySection(sectionHeading) {
//const Normalizeheading "= normalizeWikiHeading(sectionHeading" to use the h* element and not the .mw-heading span.);
const ceiling = heading.root.parentElement;
if (!this.isContributionSurveyHeading(sectionHeading)) {
return getSectionElements(heading.root, (el) => {
if (!this.isContributionSurveyHeading(sectionHeading.parentElement)) {
var _a, _b;
throw new Error('Provided section heading is not a valid section heading.');
}// TODO: Avoid double normalization
elseconst {norm = normalizeWikiHeading(el, ceiling);
return (heading.level >= ((_a = norm === null || norm === void 0 ? void 0 : norm.level) !== null && _a !== void 0 ? _a : Infinity)) ||
sectionHeading = sectionHeading.parentElement;
this.isContributionSurveyHeading((_b = norm === null || norm === void 0 ? void 0 : norm.h) !== null && _b !== void 0 ? _b : el);
}
});
const sectionMembers = [];
let nextSibling = sectionHeading.nextElementSibling;
while (nextSibling != null && !this.isContributionSurveyHeading(nextSibling)) {
sectionMembers.push(nextSibling);
nextSibling = nextSibling.nextElementSibling;
}
return sectionMembers;
}
/**
Line 1,076 ⟶ 1,375:
* and for one-click continuation of past active sessions.
*
* @param sectionId The ID of the section to add.
*/
addActiveSection(sectionsectionId) {
return __awaiter(this, void 0, void 0, function* () {
const lastActiveSection = this.lastActiveSections.indexOf(sectionsectionId);
if (lastActiveSection === -1) {
this.lastActiveSections.push(sectionsectionId);
yield this.saveToCache();
}
Line 1,091 ⟶ 1,390:
* for this section.
*
* @param sectionId ID of the section to remove
*/
removeActiveSection(sectionsectionId) {
return __awaiter(this, void 0, void 0, function* () {
const lastActiveSection = this.lastActiveSections.indexOf(sectionsectionId);
if (lastActiveSection !== -1) {
this.lastActiveSections.splice(lastActiveSection, 1);
Line 1,189 ⟶ 1,488:
h_1("a", { onClick: () => __awaiter(this, void 0, void 0, function* () {
if (casePage && casePage.lastActiveSections.length > 0) {
const headingNameheadingId = sectionHeadingName(heading).id;
if (casePagewindow.lastActiveSectionsdeputy.indexOfconfig.cci.openOldOnContinue.get(headingName) === -1) {
yieldif (casePage.addActiveSectionlastActiveSections.indexOf(headingNameheadingId); === -1) {
yield casePage.addActiveSection(headingId);
}
yield window.deputy.session.DeputyRootSession.continueSession(casePage);
}
else {
yield window.deputy.session.DeputyRootSession.continueSession(casePage, [headingId]);
}
yield window.deputy.session.DeputyRootSession.continueSession(casePage);
}
else {
yield window.deputy.session.DeputyRootSession.startSession(heading.h);
}
}) }, mw.message(casePage && casePage.lastActiveSections.length > 0 ?
Line 1,202 ⟶ 1,506:
'deputy.session.start').text()),
h_1("span", { class: "dp-sessionStarter-bracket" }, "]"));
}
 
/**
* Displayed when a user has previously worked on a case page and is now visiting
* the page again (with no active session). Displayed inside an OOUI message box.
*
* @param props
* @param props.casePage
* @return HTML element
*/
function DeputyCCISessionContinueMessage (props) {
return h_1("span", null,
h_1("b", null, mw.message('deputy.session.continue.head', new Date().toLocaleString(mw.config.get('wgUserLanguage'), { dateStyle: 'long', timeStyle: 'medium' })).text()),
h_1("br", null),
mw.message('deputy.session.continue.help', props.casePage.lastActiveSections[0]).text(),
h_1("br", null),
h_1("span", { class: "dp-cs-session-continue" }));
}
 
Line 1,230 ⟶ 1,517:
var _a;
return (_a = element === null || element === void 0 ? void 0 : element.parentElement) === null || _a === void 0 ? void 0 : _a.removeChild(element);
}
 
/**
* Log errors to the console.
*
* @param {...any} data
*/
function error(...data) {
console.error('[Deputy]', ...data);
}
 
Line 1,241 ⟶ 1,537:
function unwrapWidget (el) {
if (el.$element == null) {
console.error(el);
throw new Error('Element is not aan OOUI WidgetElement!');
}
return el.$element[0];
}
 
/**
* Swaps two elements in the DOM. Element 1 will be removed from the DOM, Element 2 will
* be added in its place.
*
* @param element1 The element to remove
* @param element2 The element to insert
* @return `element2`, for chaining
*/
function swapElements (element1, element2) {
try {
element1.insertAdjacentElement('afterend', element2);
element1.parentElement.removeChild(element1);
return element2;
}
catch (e) {
console.error(e, { element1, element2 });
// Caught for debug only. Rethrow.
throw e;
}
}
 
/**
* Displayed when a user is currently working on a case page from a different
* tab. Displayed inside an OOUI message box.
*
* @return HTML element
*/
function DeputyCCISessionTabActiveMessage () {
return h_1("span", null,
h_1("b", null, mw.msg('deputy.session.tabActive.head')),
h_1("br", null),
mw.msg('deputy.session.tabActive.help'));
}
 
Line 1,288 ⟶ 1,550:
* @param props.casePage
* @param props.heading
* @param props.height
* @return HTML element
*/
Line 1,298 ⟶ 1,561:
flags: ['primary', 'progressive']
});
const element = h_1("div", { style: { height: props.height + 'px' }, class: "dp-cs-section-add" }, unwrapWidget(startButton));
startButton.on('click', () => {
// This element is automatically appended to the UL of the section, which is a no-no
Line 1,336 ⟶ 1,599:
}
}
 
/**
* Data that constructs a raw contribution survey row.
*/
/**
* Parser for {@link ContributionSurveyRow}s.
*
* This is used directly in unit tests. Do not import unnecessary
* dependencies, as they may indirectly import the entire Deputy
* codebase outside a browser environment.
*/
class ContributionSurveyRowParser {
/**
*
* @param wikitext
*/
constructor(wikitext) {
this.wikitext = wikitext;
this.current = wikitext;
}
/**
* Parses a wikitext contribution survey row into a {@link RawContributionSurveyRow}.
* If invalid, an Error is thrown with relevant information.
*
* @return Components of a parsed contribution survey row.
*/
parse() {
var _a, _b;
this.current = this.wikitext;
const bullet = this.eatUntil(/^[^*\s]/g);
if (!bullet) {
throw new Error('dp-malformed-row');
}
const creation = this.eatExpression(/^\s*'''N'''\s*/g) != null;
const page = this.eatExpression(/\[\[([^\]|]+)(?:\|.*)?]]/g, 1);
if (!page) {
// Malformed or unparsable listing.
throw new Error('dp-undetectable-page-name');
}
let extras =
// [[Special:Diff/12345|6789]]
(_a = this.eatUntil(/^(?:'''?)?\[\[Special:Diff\/\d+/, true)) !== null && _a !== void 0 ? _a :
// {{dif|12345|6789}}
this.eatUntil(/^(?:'''?)?{{dif\|\d+/, true);
let diffsBolded = false;
// At this point, `extras` is either a string or `null`. If it's a string,
// extras exist, and we should add them. If not, there's likely no more
// revisions to be processed here, and can assume that the rest is user comments.
const revids = [];
const revidText = {};
let diffs = null, comments, diffTemplate = '[[Special:Diff/$1|($2)]]';
if (extras) {
const starting = this.current;
let diff = true;
while (diff) {
const diffMatch =
// [[Special:Diff/12345|6789]]
(_b = this.eatExpressionMatch(/\s*(?:'''?)?\[\[Special:Diff\/(\d+)(?:\|([^\]]*))?]](?:'''?)?/g)) !== null && _b !== void 0 ? _b :
// {{dif|12345|6789}}
this.eatExpressionMatch(/\s*(?:'''?)?{{dif\|(\d+)\|([^}]+)}}(?:'''?)?/g);
diff = diffMatch === null || diffMatch === void 0 ? void 0 : diffMatch[1];
if (diff != null) {
revids.push(+diff);
revidText[+diff] = diffMatch[2].replace(/^\(|\)$/g, '');
}
}
// All diff links removed. Get diff of starting and current to get entire diff part.
diffs = starting.slice(0, starting.length - this.current.length);
// Bolded diffs support.
if (diffs.slice(0, 3) === "'''" &&
diffs.slice(-3) === "'''" &&
!diffs.slice(3, -3).includes("'''")) {
diffsBolded = true;
}
// Pre-2014 style support.
if ((diffs !== null && diffs !== void 0 ? diffs : '').includes('{{dif')) {
diffsBolded = true;
diffTemplate = '{{dif|$1|($2)}}';
}
// Comments should be empty, but just in case we do come across comments.
comments = this.isEmpty() ? null : this.eatRemaining();
}
else {
// Try to grab extras. This is done by detecting any form of parentheses and
// matching them, including any possible included colon. If that doesn't work,
// try pulling out just the colon.
const maybeExtras = this.eatExpression(/\s*(?::\s*)?\(.+?\)(?:\s*:)?\s*/) || this.eatExpression(/\s*:\s*/g);
if (maybeExtras) {
extras = maybeExtras;
}
// Only comments probably remain. Eat out whitespaces and the rest is a comment.
extras = (extras || '') + (this.eatUntil(/^\S/g, true) || '');
if (extras === '') {
extras = null;
}
comments = this.getCurrentLength() > 0 ? this.eatRemaining() : null;
}
// "{bullet}{creation}[[{page}]]{extras}{diffs}{comments}"
return {
type: (extras || comments || diffs) == null ? 'pageonly' : 'detailed',
bullet,
creation,
page,
extras,
diffs,
comments,
revids,
revidText,
diffTemplate,
diffsTemplate: diffsBolded ? "'''$1'''" : '$1'
};
}
/**
* Returns `true` if the working string is empty.
*
* @return `true` if the length of `current` is zero. `false` if otherwise.
*/
isEmpty() {
return this.current.length === 0;
}
/**
* @return the length of the working string.
*/
getCurrentLength() {
return this.current.length;
}
/**
* Views the next character to {@link ContributionSurveyRowParser#eat}.
*
* @return The first character of the working string.
*/
peek() {
return this.current[0];
}
/**
* Pops the first character off the working string and returns it.
*
* @return First character of the working string, pre-mutation.
*/
eat() {
const first = this.current[0];
this.current = this.current.slice(1);
return first;
}
/**
* Continue eating from the string until a string or regular expression
* is matched. Unlike {@link eatExpression}, passed regular expressions
* will not be re-wrapped with `^(?:)`. These must be added on your own if
* you wish to match the start of the string.
*
* @param pattern The string or regular expression to match.
* @param noFinish If set to `true`, `null` will be returned instead if the
* pattern is never matched. The working string will be reset to its original
* state if this occurs. This prevents the function from being too greedy.
* @return The consumed characters.
*/
eatUntil(pattern, noFinish) {
const starting = this.current;
let consumed = '';
while (this.current.length > 0) {
if (typeof pattern === 'string') {
if (this.current.startsWith(pattern)) {
return consumed;
}
}
else {
if (cloneRegex$1(pattern).test(this.current)) {
return consumed;
}
}
consumed += this.eat();
}
if (noFinish && this.current.length === 0) {
// We finished the string! Reset.
this.current = starting;
return null;
}
else {
return consumed;
}
}
/**
* Eats a given expression from the start of the working string. If the working
* string does not contain the given expression, `null` is returned (and not a
* blank string). Only eats once, so any expression must be greedy if different
* behavior is expected.
*
* The regular expression passed into this function is automatically re-wrapped
* with `^(?:<source>)`. Avoid adding these expressions on your own.
*
* @param pattern The pattern to match.
* @param n The capture group to return (returns the entire string (`0`) by default)
* @return The consumed characters.
*/
eatExpression(pattern, n = 0) {
const expression = new RegExp(`^(?:${pattern.source})`,
// Ban global and multiline, useless since this only matches once and to
// ensure that the reading remains 'flat'.
pattern.flags.replace(/[gm]/g, ''));
const match = expression.exec(this.current);
if (match) {
this.current = this.current.slice(match[0].length);
return match[n];
}
else {
return null;
}
}
/**
* Eats a given expression from the start of the working string. If the working
* string does not contain the given expression, `null` is returned (and not a
* blank string). Only eats once, so any expression must be greedy if different
* behavior is expected.
*
* The regular expression passed into this function is automatically re-wrapped
* with `^(?:<source>)`. Avoid adding these expressions on your own.
*
* @param pattern The pattern to match.
* @return A {@link RegExpExecArray}.
*/
eatExpressionMatch(pattern) {
const expression = new RegExp(`^(?:${pattern.source})`,
// Ban global and multiline, useless since this only matches once and to
// ensure that the reading remains 'flat'.
pattern.flags.replace(/[gm]/g, ''));
const match = expression.exec(this.current);
if (match) {
this.current = this.current.slice(match[0].length);
return match;
}
else {
return null;
}
}
/**
* Consumes the rest of the working string.
*
* @return The remaining characters in the working string.
*/
eatRemaining() {
const remaining = this.current;
this.current = '';
return remaining;
}
}
 
var ContributionSurveyRowSort;
(function (ContributionSurveyRowSort) {
// Chronological
ContributionSurveyRowSort[ContributionSurveyRowSort["Date"] = 0] = "Date";
// Reverse chronological
ContributionSurveyRowSort[ContributionSurveyRowSort["DateReverse"] = 1] = "DateReverse";
// New size - old size
ContributionSurveyRowSort[ContributionSurveyRowSort["Bytes"] = 2] = "Bytes";
})(ContributionSurveyRowSort || (ContributionSurveyRowSort = {}));
 
/**
* Sleep for an specified amount of time.
*
* @param ms Milliseconds to sleep for.
*/
function sleep(ms) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((res) => {
setTimeout(res, ms);
});
});
}
 
/**
* Handles requests that might get hit by a rate limit. Wraps around
* `fetch` and ensures that all users of the Requester only request
* a single time per 100 ms on top of the time it takes to load
* previous requests. Also runs on four "threads", allowing at
* least a certain level of asynchronicity.
*
* Particularly used when a multitude of requests have a chance to
* DoS a service.
*/
class Requester {
/**
* Processes things in the fetchQueue.
*/
static processFetch() {
return __awaiter(this, void 0, void 0, function* () {
if (Requester.fetchActive >= Requester.maxThreads) {
return;
}
Requester.fetchActive++;
const next = Requester.fetchQueue.shift();
if (next) {
const data =
// eslint-disable-next-line prefer-spread
yield fetch.apply(null, next[1])
.then((res) => {
// Return false for survivable cases. In this case, we'll re-queue
// the request.
if (res.status === 429 || res.status === 502) {
return res.status;
}
else {
return res;
}
}, next[0][1]);
if (data instanceof Response) {
next[0][0](data);
}
else if (typeof data === 'number') {
Requester.fetchQueue.push(next);
}
}
yield sleep(Requester.minTime);
Requester.fetchActive--;
setTimeout(Requester.processFetch, 0);
});
}
}
/**
* Maximum number of requests to be processed simultaneously.
*/
Requester.maxThreads = 4;
/**
* Minimum amount of milliseconds to wait between each request.
*/
Requester.minTime = 100;
/**
* Requests to be performed. Takes tuples containing a resolve-reject pair and arguments
* to be passed into the fetch function.
*/
Requester.fetchQueue = [];
/**
* Number of requests currently being processed. Must be lower than
* {@link maxThreads}.
*/
Requester.fetchActive = 0;
Requester.fetch = (...args) => {
let res, rej;
const fakePromise = new Promise((_res, _rej) => {
res = _res;
rej = _rej;
});
Requester.fetchQueue.push([[res, rej], args]);
setTimeout(Requester.processFetch, 0);
return fakePromise;
};
 
/**
* Transforms the `redirects` object returned by MediaWiki's `query` action into an
* object instead of an array.
*
* @param redirects
* @param normalized
* @return Redirects as an object
*/
function toRedirectsObject(redirects, normalized) {
var _a;
if (redirects == null) {
return {};
}
const out = {};
for (const redirect of redirects) {
out[redirect.from] = redirect.to;
}
// Single-level redirect-normalize loop check
for (const normal of normalized) {
out[normal.from] = (_a = out[normal.to]) !== null && _a !== void 0 ? _a : normal.to;
}
return out;
}
 
/**
* A configuration. Defines settings and setting groups.
*/
class ConfigurationBase {
// eslint-disable-next-line jsdoc/require-returns-check
/**
* @return the configuration from the current wiki.
*/
static load() {
throw new Error('Unimplemented method.');
}
/**
* Creates a new Configuration.
*/
constructor() { }
/**
* Deserializes a JSON configuration into this configuration. This WILL overwrite
* past settings.
*
* @param serializedData
*/
deserialize(serializedData) {
var _a;
for (const group in this.all) {
const groupObject = this.all[group];
for (const key in this.all[group]) {
const setting = groupObject[key];
if (((_a = serializedData === null || serializedData === void 0 ? void 0 : serializedData[group]) === null || _a === void 0 ? void 0 : _a[key]) !== undefined) {
setting.set(setting.deserialize ?
// Type-checked upon declaration, just trust it to skip errors.
setting.deserialize(serializedData[group][key]) :
serializedData[group][key]);
}
}
}
}
/**
* @return the serialized version of the configuration. All `undefined` values are stripped
* from output. If a category remains unchanged from defaults, it is skipped. If the entire
* configuration remains unchanged, `null` is returned.
*/
serialize() {
const config = {};
for (const group of Object.keys(this.all)) {
const groupConfig = {};
const groupObject = this.all[group];
for (const key in this.all[group]) {
const setting = groupObject[key];
if (setting.get() === setting.defaultValue && !setting.alwaysSave) {
continue;
}
const serialized = setting.serialize ?
// Type-checked upon declaration, just trust it to skip errors.
setting.serialize(setting.get()) : setting.get();
if (serialized !== undefined) {
groupConfig[key] = serialized;
}
}
if (Object.keys(groupConfig).length > 0) {
config[group] = groupConfig;
}
}
if (Object.keys(config).length > 0) {
return config;
}
else {
return null;
}
}
}
 
/**
* Works like `Object.values`.
*
* @param obj The object to get the values of.
* @return The values of the given object as an array
*/
function getObjectValues(obj) {
return Object.keys(obj).map((key) => obj[key]);
}
 
/**
* Log warnings to the console.
*
* @param {...any} data
*/
function warn(...data) {
console.warn('[Deputy]', ...data);
}
 
/**
* Refers to a specific setting on the configuration. Should be initialized with
* a raw (serialized) type and an actual (deserialized) type.
*
* This is used for both client and wiki-wide configuration.
*/
class Setting {
/**
* @param options
* @param options.serialize Serialization function. See {@link Setting#serialize}
* @param options.deserialize Deserialization function. See {@link Setting#deserialize}
* @param options.alwaysSave See {@link Setting#alwaysSave}.
* @param options.defaultValue Default value. If not supplied, `undefined` is used.
* @param options.displayOptions See {@link Setting#displayOptions}
* @param options.allowedValues See {@link Setting#allowedValues}
*/
constructor(options) {
var _a, _b;
this.serialize = options.serialize;
this.deserialize = options.deserialize;
this.displayOptions = options.displayOptions;
this.allowedValues = options.allowedValues;
this.value = this.defaultValue = options.defaultValue;
this.alwaysSave = options.alwaysSave;
this.isDisabled = ((_a = options.displayOptions) === null || _a === void 0 ? void 0 : _a.disabled) != null ?
(typeof options.displayOptions.disabled === 'function' ?
options.displayOptions.disabled.bind(this) :
() => options.displayOptions.disabled) : () => false;
this.isHidden = ((_b = options.displayOptions) === null || _b === void 0 ? void 0 : _b.hidden) != null ?
(typeof options.displayOptions.hidden === 'function' ?
options.displayOptions.hidden.bind(this) :
() => options.displayOptions.hidden) : () => false;
}
/**
* @return `true` if `this.value` is not null or undefined.
*/
ok() {
return this.value != null;
}
/**
* @return The current value of this setting.
*/
get() {
return this.value;
}
/**
* Sets the value and performs validation. If the input is an invalid value, and
* `throwOnInvalid` is false, the value will be reset to default.
*
* @param v
* @param throwOnInvalid
*/
set(v, throwOnInvalid = false) {
if (this.locked) {
warn('Attempted to modify locked setting.');
return;
}
if (this.allowedValues) {
const keys = Array.isArray(this.allowedValues) ?
this.allowedValues : getObjectValues(this.allowedValues);
if (Array.isArray(v)) {
if (v.some((v1) => keys.indexOf(v1) === -1)) {
if (throwOnInvalid) {
throw new Error('Invalid value');
}
v = this.value;
}
}
else {
if (this.allowedValues && keys.indexOf(v) === -1) {
if (throwOnInvalid) {
throw new Error('Invalid value');
}
v = this.value;
}
}
}
this.value = v;
}
/**
* Resets this setting to its original value.
*/
reset() {
this.set(this.defaultValue);
}
/**
* Parses a given raw value and mutates the setting.
*
* @param raw The raw value to parse.
* @return The new value.
*/
load(raw) {
return (this.value = this.deserialize(raw));
}
/**
* Prevents the value of the setting from being changed. Used for debugging.
*/
lock() {
this.locked = true;
}
/**
* Allows the value of the setting to be changed. Used for debugging.
*/
unlock() {
this.locked = false;
}
}
Setting.basicSerializers = {
serialize: (value) => value,
deserialize: (value) => value
};
 
/**
* Checks if two MediaWiki page titles are equal.
*
* @param title1
* @param title2
* @return `true` if `title1` and `title2` refer to the same page
*/
function equalTitle(title1, title2) {
return normalizeTitle(title1).getPrefixedDb() === normalizeTitle(title2).getPrefixedDb();
}
 
var deputySettingsStyles = ".deputy-setting {margin-bottom: 1em;}.deputy-setting > .oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-header .oo-ui-labelElement-label {font-weight: bold;}.dp-mb {margin-bottom: 1em;}.deputy-about {display: flex;}.deputy-about > :first-child {flex: 0;}.deputy-about > :first-child > img {height: 5em;width: auto;}.ltr .deputy-about > :first-child {margin-right: 1em;}.rtl .deputy-about > :first-child {margin-left: 1em;}.deputy-about > :nth-child(2) {flex: 1;}.deputy-about > :nth-child(2) > :first-child > * {display: inline;}.deputy-about > :nth-child(2) > :first-child > :first-child {font-weight: bold;font-size: 2em;}.deputy-about > :nth-child(2) > :first-child > :nth-child(2) {color: gray;vertical-align: bottom;margin-left: 0.4em;}.deputy-about > :nth-child(2) > :not(:first-child) {margin-top: 0.5em;}.ltr .deputy-about + div > :not(:last-child) {margin-right: 0.5em;}.rtl .deputy-about + div > :not(:last-child) {margin-left: 0.5em;}.ltr .deputy-about + div {text-align: right;}.rtl .deputy-about + div {text-align: left;}";
 
/**
* Works like `Object.fromEntries`
*
* @param obj The object to get the values of.
* @return The values of the given object as an array
*/
function fromObjectEntries(obj) {
const i = {};
for (const [key, value] of obj) {
i[key] = value;
}
return i;
}
 
/**
* Generates serializer and deserializer for serialized <b>string</b> enums.
*
* Trying to use anything that isn't a string enum here (union enum, numeral enum)
* will likely cause serialization/deserialization failures.
*
* @param _enum
* @param defaultValue
* @return An object containing a `serializer` and `deserializer`.
*/
function generateEnumSerializers(_enum, defaultValue) {
return {
serialize: (value) => value === defaultValue ? undefined : value,
deserialize: (value) => value
};
}
/**
* Generates configuration properties for serialized <b>string</b> enums.
*
* Trying to use anything that isn't a string enum here (union enum, numeral enum)
* will likely cause serialization/deserialization failures.
*
* @param _enum
* @param defaultValue
* @return Setting properties.
*/
function generateEnumConfigurationProperties(_enum, defaultValue) {
return Object.assign(Object.assign({}, generateEnumSerializers(_enum, defaultValue)), { displayOptions: {
type: 'radio'
}, allowedValues: fromObjectEntries(Array.from(new Set(Object.keys(_enum)).values())
.map((v) => [_enum[v], _enum[v]])), defaultValue: defaultValue });
}
var PortletNameView;
(function (PortletNameView) {
PortletNameView["Full"] = "full";
PortletNameView["Short"] = "short";
PortletNameView["Acronym"] = "acronym";
})(PortletNameView || (PortletNameView = {}));
 
var CompletionAction;
(function (CompletionAction) {
CompletionAction["Nothing"] = "nothing";
CompletionAction["Reload"] = "reload";
})(CompletionAction || (CompletionAction = {}));
var TripleCompletionAction;
(function (TripleCompletionAction) {
TripleCompletionAction["Nothing"] = "nothing";
TripleCompletionAction["Reload"] = "reload";
TripleCompletionAction["Redirect"] = "redirect";
})(TripleCompletionAction || (TripleCompletionAction = {}));
 
var ContributionSurveyRowSigningBehavior;
(function (ContributionSurveyRowSigningBehavior) {
ContributionSurveyRowSigningBehavior["Always"] = "always";
ContributionSurveyRowSigningBehavior["AlwaysTrace"] = "alwaysTrace";
ContributionSurveyRowSigningBehavior["AlwaysTraceLastOnly"] = "alwaysTraceLastOnly";
ContributionSurveyRowSigningBehavior["LastOnly"] = "lastOnly";
ContributionSurveyRowSigningBehavior["Never"] = "never";
})(ContributionSurveyRowSigningBehavior || (ContributionSurveyRowSigningBehavior = {}));
 
var DeputyPageToolbarState;
(function (DeputyPageToolbarState) {
DeputyPageToolbarState[DeputyPageToolbarState["Open"] = 0] = "Open";
DeputyPageToolbarState[DeputyPageToolbarState["Collapsed"] = 1] = "Collapsed";
DeputyPageToolbarState[DeputyPageToolbarState["Hidden"] = 2] = "Hidden";
})(DeputyPageToolbarState || (DeputyPageToolbarState = {}));
 
/**
* A button that performs an action when clicked. Shown in the preferences screen,
* and acts exactly like a setting, but always holds a value of 'null'.
*/
class Action extends Setting {
/**
* @param onClick
* @param displayOptions
*/
constructor(onClick, displayOptions = {}) {
super({
serialize: () => undefined,
deserialize: () => undefined,
displayOptions: Object.assign({}, displayOptions, { type: 'button' })
});
this.onClick = onClick;
}
}
 
/**
* A configuration. Defines settings and setting groups.
*/
class UserConfiguration extends ConfigurationBase {
/**
* @return the configuration from the current wiki.
*/
static load() {
const config = new UserConfiguration();
try {
if (mw.user.options.get(UserConfiguration.optionKey)) {
const decodedOptions = JSON.parse(mw.user.options.get(UserConfiguration.optionKey));
config.deserialize(decodedOptions);
}
}
catch (e) {
error(e, mw.user.options.get(UserConfiguration.optionKey));
mw.hook('deputy.i18nDone').add(function notifyConfigFailure() {
mw.notify(mw.msg('deputy.loadError.userConfig'), {
type: 'error'
});
mw.hook('deputy.i18nDone').remove(notifyConfigFailure);
});
config.save();
}
return config;
}
/**
* Creates a new Configuration.
*
* @param serializedData
*/
constructor(serializedData = {}) {
var _a;
super();
this.core = {
/**
* Numerical code that identifies this config version. Increments for every breaking
* configuration file change.
*/
configVersion: new Setting({
defaultValue: UserConfiguration.configVersion,
displayOptions: { hidden: true },
alwaysSave: true
}),
language: new Setting({
defaultValue: mw.config.get('wgUserLanguage'),
displayOptions: { type: 'select' }
}),
modules: new Setting({
defaultValue: ['cci', 'ante', 'ia'],
displayOptions: { type: 'checkboxes' },
allowedValues: ['cci', 'ante', 'ia']
}),
portletNames: new Setting(generateEnumConfigurationProperties(PortletNameView, PortletNameView.Full)),
seenAnnouncements: new Setting({
defaultValue: [],
displayOptions: { hidden: true }
}),
dangerMode: new Setting({
defaultValue: false,
displayOptions: {
type: 'checkbox'
}
}),
resetDatabase: new Action(() => __awaiter(this, void 0, void 0, function* () {
yield window.deputy.storage.reset();
}), {
disabled: () => !window.deputy,
extraOptions: {
flags: ['destructive']
}
}),
resetPreferences: new Action(() => __awaiter(this, void 0, void 0, function* () {
yield MwApi.action.saveOption(UserConfiguration.optionKey, null);
}), {
extraOptions: {
flags: ['destructive']
}
})
};
this.cci = {
enablePageToolbar: new Setting({
defaultValue: true,
displayOptions: {
type: 'checkbox'
}
}),
showCvLink: new Setting({
defaultValue: true,
displayOptions: {
type: 'checkbox'
}
}),
showUsername: new Setting({
defaultValue: false,
displayOptions: {
type: 'checkbox'
}
}),
autoCollapseRows: new Setting({
defaultValue: false,
displayOptions: {
type: 'checkbox'
}
}),
autoShowDiff: new Setting({
defaultValue: false,
displayOptions: {
type: 'checkbox'
}
}),
maxRevisionsToAutoShowDiff: new Setting({
defaultValue: 2,
displayOptions: {
type: 'number',
// Force any due to self-reference
disabled: (config) => !config.cci.autoShowDiff.get(),
extraOptions: {
min: 1
}
}
}),
maxSizeToAutoShowDiff: new Setting({
defaultValue: 500,
displayOptions: {
type: 'number',
// Force any due to self-reference
disabled: (config) => !config.cci.autoShowDiff.get(),
extraOptions: {
min: -1
}
}
}),
forceUtc: new Setting({
defaultValue: false,
displayOptions: {
type: 'checkbox'
}
}),
signingBehavior: new Setting(generateEnumConfigurationProperties(ContributionSurveyRowSigningBehavior, ContributionSurveyRowSigningBehavior.Always)),
signSectionArchive: new Setting({
defaultValue: true,
displayOptions: {
type: 'checkbox'
}
}),
openOldOnContinue: new Setting({
defaultValue: false,
displayOptions: {
type: 'checkbox'
}
}),
toolbarInitialState: new Setting(Object.assign(Object.assign({}, generateEnumSerializers(DeputyPageToolbarState, DeputyPageToolbarState.Open)), { defaultValue: DeputyPageToolbarState.Open, displayOptions: { hidden: true } }))
};
this.ante = {
enableAutoMerge: new Setting({
defaultValue: false,
displayOptions: {
type: 'checkbox',
disabled: 'unimplemented'
}
}),
onSubmit: new Setting(generateEnumConfigurationProperties(CompletionAction, CompletionAction.Reload))
};
this.ia = {
responses: new Setting(Object.assign(Object.assign({}, Setting.basicSerializers), { defaultValue: null, displayOptions: {
disabled: 'unimplemented',
type: 'unimplemented'
} })),
enablePageToolbar: new Setting({
defaultValue: true,
displayOptions: {
type: 'checkbox',
disabled: 'unimplemented'
}
}),
defaultEntirePage: new Setting({
defaultValue: true,
displayOptions: {
type: 'checkbox'
}
}),
defaultFromUrls: new Setting({
defaultValue: true,
displayOptions: {
type: 'checkbox'
}
}),
onHide: new Setting(generateEnumConfigurationProperties(TripleCompletionAction, TripleCompletionAction.Reload)),
onSubmit: new Setting(generateEnumConfigurationProperties(TripleCompletionAction, TripleCompletionAction.Reload)),
onBatchSubmit: new Setting(generateEnumConfigurationProperties(CompletionAction, CompletionAction.Reload))
};
this.type = 'user';
this.all = { core: this.core, cci: this.cci, ante: this.ante, ia: this.ia };
if (serializedData) {
this.deserialize(serializedData);
}
if (mw.storage.get(`mw-${UserConfiguration.optionKey}-lastVersion`) !== version) ;
mw.storage.set(`mw-${UserConfiguration.optionKey}-lastVersion`, version);
if ((_a = window.deputy) === null || _a === void 0 ? void 0 : _a.comms) {
window.deputy.comms.addEventListener('userConfigUpdate', (e) => {
// Update the configuration based on another tab's message.
this.deserialize(e.data.config);
});
}
}
/**
* Saves the configuration.
*/
save() {
return __awaiter(this, void 0, void 0, function* () {
yield MwApi.action.saveOption(UserConfiguration.optionKey, JSON.stringify(this.serialize()));
});
}
}
UserConfiguration.configVersion = 1;
UserConfiguration.optionKey = 'userjs-deputy';
 
let InternalConfigurationGroupTabPanel$1;
/**
* Initializes the process element.
*/
function initConfigurationGroupTabPanel$1() {
InternalConfigurationGroupTabPanel$1 = class ConfigurationGroupTabPanel extends OO.ui.TabPanelLayout {
/**
* @return The {@Link Setting}s for this group.
*/
get settings() {
return this.config.config.all[this.config.group];
}
/**
* @param config Configuration to be passed to the element.
*/
constructor(config) {
super(`configurationGroupPage_${config.group}`);
this.config = config;
this.mode = config.config instanceof UserConfiguration ? 'user' : 'wiki';
if (this.mode === 'wiki') {
this.$element.append(new OO.ui.MessageWidget({
classes: [
'deputy', 'dp-mb'
],
type: 'warning',
label: mw.msg('deputy.settings.dialog.wikiConfigWarning')
}).$element);
}
for (const settingKey of Object.keys(this.settings)) {
const setting = this.settings[settingKey];
if (setting.isHidden(this.config.config)) {
continue;
}
switch (setting.displayOptions.type) {
case 'checkbox':
this.$element.append(this.newCheckboxField(settingKey, setting));
break;
case 'checkboxes':
this.$element.append(this.newCheckboxesField(settingKey, setting));
break;
case 'radio':
this.$element.append(this.newRadioField(settingKey, setting));
break;
case 'text':
this.$element.append(this.newStringField(settingKey, setting, setting.displayOptions.extraOptions));
break;
case 'number':
this.$element.append(this.newNumberField(settingKey, setting, setting.displayOptions.extraOptions));
break;
case 'page':
this.$element.append(this.newPageField(settingKey, setting, setting.displayOptions.extraOptions));
break;
case 'code':
this.$element.append(this.newCodeField(settingKey, setting, setting.displayOptions.extraOptions));
break;
case 'button':
this.$element.append(this.newButtonField(settingKey, setting, setting.displayOptions.extraOptions));
break;
default:
this.$element.append(this.newUnimplementedField(settingKey));
break;
}
}
}
/**
* Sets up the tab item
*/
setupTabItem() {
this.tabItem.setLabel(this.getMsg(this.config.group));
return this;
}
/**
* @return the i18n message for this setting tab.
*
* @param messageKey
*/
getMsg(messageKey) {
return mw.msg(`deputy.setting.${this.mode}.${messageKey}`);
}
/**
* Gets the i18n message for a given setting.
*
* @param settingKey
* @param key
* @return A localized string
*/
getSettingMsg(settingKey, key) {
return this.getMsg(`${this.config.group}.${settingKey}.${key}`);
}
/**
* @param settingKey
* @param allowedValues
* @return a tuple array of allowed values that can be used in OOUI `items` parameters.
*/
getAllowedValuesArray(settingKey, allowedValues) {
const items = [];
if (Array.isArray(allowedValues)) {
for (const key of allowedValues) {
const message = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.${key}`);
items.push([key, message.exists() ? message.text() : key]);
}
}
else {
for (const key of Object.keys(allowedValues)) {
const message = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.${key}`);
items.push([key, message.exists() ? message.text() : key]);
}
}
return items;
}
/**
* Creates an unimplemented setting notice.
*
* @param settingKey
* @return An HTMLElement of the given setting's field.
*/
newUnimplementedField(settingKey) {
const desc = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.description`);
return h_1("div", { class: "deputy-setting" },
h_1("b", null, this.getSettingMsg(settingKey, 'name')),
desc.exists() ? h_1("p", { style: { fontSize: '0.925em', color: '#54595d' } }, desc.text()) : '',
h_1("p", null, mw.msg('deputy.settings.dialog.unimplemented')));
}
/**
* Creates a checkbox field.
*
* @param settingKey
* @param setting
* @return An HTMLElement of the given setting's field.
*/
newCheckboxField(settingKey, setting) {
const isDisabled = setting.isDisabled(this.config.config);
const desc = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.description`);
const field = new OO.ui.CheckboxInputWidget({
selected: setting.get(),
disabled: isDisabled !== undefined && isDisabled !== false
});
const layout = new OO.ui.FieldLayout(field, {
align: 'inline',
label: this.getSettingMsg(settingKey, 'name'),
help: typeof isDisabled === 'string' ?
this.getSettingMsg(settingKey, isDisabled) :
desc.exists() ? desc.text() : undefined,
helpInline: true
});
field.on('change', () => {
setting.set(field.isSelected());
this.emit('change');
});
// Attach disabled re-checker
this.on('change', () => {
field.setDisabled(!!setting.isDisabled(this.config.config));
});
return h_1("div", { class: "deputy-setting" }, unwrapWidget(layout));
}
/**
* Creates a new checkbox set field.
*
* @param settingKey
* @param setting
* @return An HTMLElement of the given setting's field.
*/
newCheckboxesField(settingKey, setting) {
const isDisabled = setting.isDisabled(this.config.config);
const desc = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.description`);
const field = new OO.ui.CheckboxMultiselectInputWidget({
value: setting.get(),
disabled: isDisabled !== undefined && isDisabled !== false,
options: this.getAllowedValuesArray(settingKey, setting.allowedValues)
.map(([key, label]) => ({ data: key, label }))
});
const layout = new OO.ui.FieldLayout(field, {
align: 'top',
label: this.getSettingMsg(settingKey, 'name'),
help: typeof isDisabled === 'string' ?
this.getSettingMsg(settingKey, isDisabled) :
desc.exists() ? desc.text() : undefined,
helpInline: true
});
// TODO: @types/oojs-ui limitation
field.on('change', (items) => {
const finalData = Array.isArray(setting.allowedValues) ?
items :
field.getValue().map((v) => setting.allowedValues[v]);
setting.set(finalData);
this.emit('change');
});
// Attach disabled re-checker
this.on('change', () => {
field.setDisabled(!!setting.isDisabled(this.config.config));
});
return h_1("div", { class: "deputy-setting" }, unwrapWidget(layout));
}
/**
* Creates a new radio set field.
*
* @param settingKey
* @param setting
* @return An HTMLElement of the given setting's field.
*/
newRadioField(settingKey, setting) {
var _a;
const isDisabled = setting.isDisabled(this.config.config);
const desc = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.description`);
const field = new OO.ui.RadioSelectWidget({
disabled: isDisabled !== undefined && isDisabled !== false &&
!((_a = setting.displayOptions.readOnly) !== null && _a !== void 0 ? _a : false),
items: this.getAllowedValuesArray(settingKey, setting.allowedValues)
.map(([key, label]) => new OO.ui.RadioOptionWidget({
data: key,
label: label,
selected: setting.get() === key
})),
multiselect: false
});
const layout = new OO.ui.FieldLayout(field, {
align: 'top',
label: this.getSettingMsg(settingKey, 'name'),
help: typeof isDisabled === 'string' ?
this.getSettingMsg(settingKey, isDisabled) :
desc.exists() ? desc.text() : undefined,
helpInline: true
});
// OOUIRadioInputWidget
field.on('select', (items) => {
const finalData = Array.isArray(setting.allowedValues) ?
items.data :
setting.allowedValues[items.data];
setting.set(finalData);
this.emit('change');
});
// Attach disabled re-checker
this.on('change', () => {
field.setDisabled(!!setting.isDisabled(this.config.config));
});
return h_1("div", { class: "deputy-setting" }, unwrapWidget(layout));
}
/**
* Creates a new field that acts like a string field.
*
* @param FieldClass
* @param settingKey
* @param setting
* @param extraFieldOptions
* @return A Deputy setting field
*/
newStringLikeField(FieldClass, settingKey, setting, extraFieldOptions = {}) {
var _a, _b, _c;
const isDisabled = setting.isDisabled(this.config.config);
const desc = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.description`);
const field = new FieldClass(Object.assign({ readOnly: (_a = setting.displayOptions.readOnly) !== null && _a !== void 0 ? _a : false, value: (_c = (_b = setting.serialize) === null || _b === void 0 ? void 0 : _b.call(setting, setting.get())) !== null && _c !== void 0 ? _c : setting.get(), disabled: isDisabled !== undefined && isDisabled !== false }, extraFieldOptions));
const layout = new OO.ui.FieldLayout(field, {
align: 'top',
label: this.getSettingMsg(settingKey, 'name'),
help: typeof isDisabled === 'string' ?
this.getSettingMsg(settingKey, isDisabled) :
desc.exists() ? desc.text() : undefined,
helpInline: true
});
if (FieldClass === OO.ui.NumberInputWidget) {
field.on('change', (value) => {
setting.set(+value);
this.emit('change');
});
}
else {
field.on('change', (value) => {
setting.set(value);
this.emit('change');
});
}
// Attach disabled re-checker
this.on('change', () => {
field.setDisabled(setting.isDisabled(this.config.config));
});
return h_1("div", { class: "deputy-setting" }, unwrapWidget(layout));
}
/**
* Creates a new string setting field.
*
* @param settingKey
* @param setting
* @param extraFieldOptions
* @return An HTMLElement of the given setting's field.
*/
newStringField(settingKey, setting, extraFieldOptions) {
return this.newStringLikeField(OO.ui.TextInputWidget, settingKey, setting, extraFieldOptions);
}
/**
* Creates a new number setting field.
*
* @param settingKey
* @param setting
* @param extraFieldOptions
* @return An HTMLElement of the given setting's field.
*/
newNumberField(settingKey, setting, extraFieldOptions) {
return this.newStringLikeField(OO.ui.NumberInputWidget, settingKey, setting, extraFieldOptions);
}
/**
* Creates a new page title setting field.
*
* @param settingKey
* @param setting
* @param extraFieldOptions
* @return An HTMLElement of the given setting's field.
*/
newPageField(settingKey, setting, extraFieldOptions) {
return this.newStringLikeField(mw.widgets.TitleInputWidget, settingKey, setting, extraFieldOptions);
}
/**
* Creates a new code setting field.
*
* @param settingKey
* @param setting
* @param extraFieldOptions
* @return An HTMLElement of the given setting's field.
*/
newCodeField(settingKey, setting, extraFieldOptions) {
return this.newStringLikeField(OO.ui.MultilineTextInputWidget, settingKey, setting, extraFieldOptions);
}
/**
* Creates a new button setting field.
*
* @param settingKey
* @param setting
* @param extraFieldOptions
* @return An HTMLElement of the given setting's field.
*/
newButtonField(settingKey, setting, extraFieldOptions) {
const isDisabled = setting.isDisabled(this.config.config);
const msgPrefix = `deputy.setting.${this.mode}.${this.config.group}.${settingKey}`;
const desc = mw.message(`${msgPrefix}.description`);
const field = new OO.ui.ButtonWidget(Object.assign({ label: this.getSettingMsg(settingKey, 'name'), disabled: isDisabled !== undefined && isDisabled !== false }, extraFieldOptions));
const layout = new OO.ui.FieldLayout(field, {
align: 'top',
label: this.getSettingMsg(settingKey, 'name'),
help: typeof isDisabled === 'string' ?
this.getSettingMsg(settingKey, isDisabled) :
desc.exists() ? desc.text() : undefined,
helpInline: true
});
field.on('click', () => __awaiter(this, void 0, void 0, function* () {
try {
if (yield OO.ui.confirm(mw.msg(`${msgPrefix}.confirm`))) {
yield setting.onClick();
OO.ui.alert(mw.msg(`${msgPrefix}.success`));
}
}
catch (e) {
OO.ui.alert(mw.msg(`${msgPrefix}.failed`));
}
}));
return h_1("div", { class: "deputy-setting" }, unwrapWidget(layout));
}
};
}
/**
* Creates a new ConfigurationGroupTabPanel.
*
* @param config Configuration to be passed to the element.
* @return A ConfigurationGroupTabPanel object
*/
function ConfigurationGroupTabPanel (config) {
if (!InternalConfigurationGroupTabPanel$1) {
initConfigurationGroupTabPanel$1();
}
return new InternalConfigurationGroupTabPanel$1(config);
}
 
let windowManager;
/**
* Opens a temporary window. Use this for dialogs that are immediately destroyed
* after running. Do NOT use this for re-openable dialogs, such as the main ANTE
* dialog.
*
* @param window
* @return A promise. Resolves when the window is closed.
*/
function openWindow(window) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((res) => {
var _a;
if (!windowManager) {
windowManager = new OO.ui.WindowManager();
const parent = (_a = document.getElementById('mw-teleport-target')) !== null && _a !== void 0 ? _a : document.getElementsByTagName('body')[0];
parent.appendChild(unwrapWidget(windowManager));
}
windowManager.addWindows([window]);
windowManager.openWindow(window);
windowManager.on('closing', (win, closed) => {
closed.then(() => {
if (windowManager) {
const _wm = windowManager;
windowManager = null;
removeElement(unwrapWidget(_wm));
_wm.destroy();
res();
}
});
});
});
});
}
 
var deputySettingsEnglish = {
"deputy.about.version": "v$1 ($2)",
"deputy.about": "About",
"deputy.about.homepage": "Homepage",
"deputy.about.openSource": "Source",
"deputy.about.contact": "Contact",
"deputy.about.credit": "Deputy was made with the help of the English Wikipedia Copyright Cleanup WikiProject and the Wikimedia Foundation.",
"deputy.about.license": "Deputy is licensed under the [$1 Apache License 2.0]. The source code for Deputy is available on [$2 GitHub], and is free for everyone to view and suggest changes.",
"deputy.about.thirdParty": "Deputy is bundled with third party libraries to make development easier. All libraries have been vetted for user security and license compatibility. For more information, see the [$1 Licensing] section on Deputy's README.",
"deputy.about.buildInfo": "Deputy v$1 ($2), committed $3.",
"deputy.about.footer": "Made with love, coffee, and the tears of copyright editors.",
"deputy.settings.portlet": "Deputy preferences",
"deputy.settings.portlet.tooltip": "Opens a dialog to modify Deputy preferences",
"deputy.settings.wikiEditIntro.title": "This is a Deputy configuration page",
"deputy.settings.wikiEditIntro.current": "Deputy's active configuration comes from this page. Changing this page will affect the settings of all Deputy users on this wiki. Edit responsibly, and avoid making significant changes without prior discussion.",
"deputy.settings.wikiEditIntro.other": "This is a valid Deputy configuration page, but the configuration is currently being loaded from [[$1]]. If this becomes the active configuration page, changing it will affect the settings of all Deputy users on this wiki. Edit responsibly, and avoid making significant changes without prior discussion.",
"deputy.settings.wikiEditIntro.edit.current": "Modify configuration",
"deputy.settings.wikiEditIntro.edit.other": "Modify this configuration",
"deputy.settings.wikiEditIntro.edit.otherCurrent": "Modify the active configuration",
"deputy.settings.wikiEditIntro.edit.protected": "This page's protection settings do not allow you to edit the page.",
"deputy.settings.wikiOutdated": "Outdated configuration",
"deputy.settings.wikiOutdated.help": "Deputy has detected a change in this wiki's configuration for all Deputy users. We've automatically downloaded the changes for you, but you have to reload to apply the changes.",
"deputy.settings.wikiOutdated.reload": "Reload",
"deputy.settings.dialog.title": "Deputy Preferences",
"deputy.settings.dialog.unimplemented": "A way to modify this setting has not yet been implemented. Check back later!",
"deputy.settings.saved": "Preferences saved. Please refresh the page to see changes.",
"deputy.settings.dialog.wikiConfigWarning": "You are currently editing a wiki-wide Deputy configuration page. Changes made to this page may affect the settings of all Deputy users on this wiki. Edit responsibly, and avoid making significant changes without prior discussion.",
"deputy.setting.user.core": "Deputy",
"deputy.setting.user.core.language.name": "Language",
"deputy.setting.user.core.language.description": "Deputy's interface language. English (US) is used by default, and is used as a fallback if no translations are available. If the content of the wiki you work on is in a different language from the interface language, Deputy will need to load additional data to ensure edit summaries, text, etc., saved on-wiki match the wiki's content language. For this reason, we suggest keeping the interface language the same as the wiki's content language.",
"deputy.setting.user.core.modules.name": "Modules",
"deputy.setting.user.core.modules.description": "Choose the enabled Deputy modules. By default, all modules are enabled.\nDisabling specific modules won't make Deputy load faster, but it can remove\nUI features added by Deputy which may act as clutter when unused.",
"deputy.setting.user.core.modules.cci": "Contributor Copyright Investigations",
"deputy.setting.user.core.modules.ante": "{{int:deputy.ante}}",
"deputy.setting.user.core.modules.ia": "{{int:deputy.ia}}",
"deputy.setting.user.core.portletNames.name": "Portlet names",
"deputy.setting.user.core.portletNames.description": "Choose which names appear in the Deputy portlet (toolbox) links.",
"deputy.setting.user.core.portletNames.full": "Full names (e.g. Attribution Notice Template Editor)",
"deputy.setting.user.core.portletNames.short": "Shortened names (e.g. Attrib. Template Editor)",
"deputy.setting.user.core.portletNames.acronym": "Acronyms (e.g. ANTE)",
"deputy.setting.user.core.dangerMode.name": "Danger mode",
"deputy.setting.user.core.dangerMode.description": "Live on the edge. This disables most confirmations and warnings given by Deputy, only leaving potentially catastrophic actions, such as page edits which break templates. It also adds extra buttons meant for rapid case processing. Intended for clerk use; use with extreme caution.",
"deputy.setting.user.core.resetDatabase.name": "Reset database",
"deputy.setting.user.core.resetDatabase.description": "Resets the Deputy internal database. This clears all cached data, including saved page and revision statuses that haven't been saved on-wiki. This does not clear your Deputy preferences.",
"deputy.setting.user.core.resetDatabase.confirm": "Are you sure you want to reset the Deputy database? This action cannot be undone.",
"deputy.setting.user.core.resetDatabase.success": "Database reset successfully. Please refresh the page to see changes.",
"deputy.setting.user.core.resetDatabase.failed": "Could not reset the database. If this error persists, please contact the Deputy maintainers.",
"deputy.setting.user.core.resetPreferences.name": "Reset preferences",
"deputy.setting.user.core.resetPreferences.description": "Resets your Deputy preferences. This clears all settings you have saved in Deputy across all browsers.",
"deputy.setting.user.core.resetPreferences.confirm": "Are you sure you want to reset your Deputy preferences? This action cannot be undone.",
"deputy.setting.user.core.resetPreferences.success": "Preferences reset successfully. Please refresh the page to see changes.",
"deputy.setting.user.core.resetPreferences.failed": "Could not reset preferences. If this error persists, please contact the Deputy maintainers.",
"deputy.setting.user.cci": "CCI",
"deputy.setting.user.cci.enablePageToolbar.name": "Enable page toolbar",
"deputy.setting.user.cci.enablePageToolbar.description": "Enables the page toolbar, which is used to quickly show tools, analysis options, and related case information on a page that is the subject of a CCI investigation.",
"deputy.setting.user.cci.showCvLink.name": "Show \"cv\" (\"copyvios\") link for revisions",
"deputy.setting.user.cci.showCvLink.description": "Show a \"cv\" link next to \"cur\" and \"prev\" on revision rows. This link will only appear if this wiki is configured to use Earwig's Copyvio Detector.",
"deputy.setting.user.cci.showUsername.name": "Show username",
"deputy.setting.user.cci.showUsername.description": "Show the username of the user who made the edit on revision rows. This may be redundant for cases which only have one editor.",
"deputy.setting.user.cci.autoCollapseRows.name": "Automatically collapse rows",
"deputy.setting.user.cci.autoCollapseRows.description": "Automatically collapse rows when the page is loaded. This is useful for cases where each row has many revisions, but may be annoying for cases where each row has few revisions.",
"deputy.setting.user.cci.autoShowDiff.name": "Automatically show diffs",
"deputy.setting.user.cci.autoShowDiff.description": "Enabling automatic loading of diffs. Configurable with two additional options to avoid loading too much content.",
"deputy.setting.user.cci.maxRevisionsToAutoShowDiff.name": "Maximum revisions to automatically show diff",
"deputy.setting.user.cci.maxRevisionsToAutoShowDiff.description": "The maximum number of revisions for a given page to automatically show the diff for each revision in the main interface.",
"deputy.setting.user.cci.maxSizeToAutoShowDiff.name": "Maximum size to automatically show diff",
"deputy.setting.user.cci.maxSizeToAutoShowDiff.description": "The maximum size of a diff to be automatically shown, if the diff will be automatically shown (see \"Maximum revisions to automatically show diff\"). Prevents extremely large diffs from opening. Set to -1 to show regardless of size.",
"deputy.setting.user.cci.forceUtc.name": "Force UTC time",
"deputy.setting.user.cci.forceUtc.description": "Forces Deputy to use UTC time whenever displaying dates and times, irregardless of your system's timezone or your MediaWiki time settings.",
"deputy.setting.user.cci.signingBehavior.name": "Row signing behavior",
"deputy.setting.user.cci.signingBehavior.description": "Choose how Deputy should behave when signing rows. By default, all rows are always signed with your signature (~~~~). You may configure Deputy to only sign the last row or never sign. You can also configure Deputy to leave a hidden trace behind (<!-- User:Example|2016-05-28T14:32:12Z -->), which helps Deputy (for other users) determine who assessed a row.",
"deputy.setting.user.cci.signingBehavior.always": "Always sign rows",
"deputy.setting.user.cci.signingBehavior.alwaysTrace": "Always leave a trace",
"deputy.setting.user.cci.signingBehavior.alwaysTraceLastOnly": "Always leave a trace, but sign the last row modified",
"deputy.setting.user.cci.signingBehavior.lastOnly": "Only sign the last row modified (prevents assessor recognition)",
"deputy.setting.user.cci.signingBehavior.never": "Never sign rows (prevents assessor recognition)",
"deputy.setting.user.cci.signSectionArchive.name": "Sign by default when archiving CCI sections",
"deputy.setting.user.cci.signSectionArchive.description": "If enabled, Deputy will enable the \"include my signature\" checkbox by default when archiving a CCI section.",
"deputy.setting.user.cci.openOldOnContinue.name": "Open old versions on continue",
"deputy.setting.user.cci.openOldOnContinue.description": "If enabled, all previously-open sections of a given case page will also be opened alongside the section where the \"continue CCI session\" link was clicked.",
"deputy.setting.user.ante": "ANTE",
"deputy.setting.user.ante.enableAutoMerge.name": "Merge automatically on run",
"deputy.setting.user.ante.enableAutoMerge.description": "If enabled, templates that can be merged will automatically be merged when the dialog opens.",
"deputy.setting.user.ante.enableAutoMerge.unimplemented": "This feature has not yet been implemented.",
"deputy.setting.user.ante.onSubmit.name": "Action on submit",
"deputy.setting.user.ante.onSubmit.description": "Choose what to do after editing attribution notice templates.",
"deputy.setting.user.ante.onSubmit.nothing": "Do nothing",
"deputy.setting.user.ante.onSubmit.reload": "Reload the page",
"deputy.setting.user.ia": "IA",
"deputy.setting.user.ia.responses.name": "Custom responses",
"deputy.setting.user.ia.responses.description": "A custom set of responses, or overrides for existing responses. If an entry\nwith the same key on both the wiki-wide configuration and the user configuration\nexists, the user configuration will override the wiki-wide configuration. Wiki-wide configuration responses can also be disabled locally. If this setting is empty, no overrides are made.",
"deputy.setting.user.ia.enablePageToolbar.name": "Enable page toolbar",
"deputy.setting.user.ia.enablePageToolbar.description": "If enabled, the page toolbar will be shown when dealing with CP cases. The IA page toolbar works slightly differently from the CCI page toolbar. Namely, it shows a button for responding instead of a status dropdown.",
"deputy.setting.user.ia.enablePageToolbar.unimplemented": "This feature has not yet been implemented.",
"deputy.setting.user.ia.defaultEntirePage.name": "Hide entire page by default",
"deputy.setting.user.ia.defaultEntirePage.description": "If enabled, the Infringement Assistant reporting window will hide the entire page by default.",
"deputy.setting.user.ia.defaultFromUrls.name": "Use URLs by default",
"deputy.setting.user.ia.defaultFromUrls.description": "If enabled, the Infringement Assistant reporting window will use URL inputs by default.",
"deputy.setting.user.ia.onHide.name": "Action on hide",
"deputy.setting.user.ia.onHide.description": "Choose what to do after the \"Hide content only\" button is selected and the relevant content is hidden from the page.",
"deputy.setting.user.ia.onHide.nothing": "Do nothing",
"deputy.setting.user.ia.onHide.reload": "Reload the page",
"deputy.setting.user.ia.onHide.redirect": "Redirect to the noticeboard page",
"deputy.setting.user.ia.onSubmit.name": "Action on submit",
"deputy.setting.user.ia.onSubmit.description": "Choose what to do after the \"Submit\" button is selected, the relevant content is hidden from the page, and the page is reported.",
"deputy.setting.user.ia.onSubmit.nothing": "Do nothing",
"deputy.setting.user.ia.onSubmit.reload": "Reload the page",
"deputy.setting.user.ia.onSubmit.redirect": "Redirect to the noticeboard page",
"deputy.setting.user.ia.onBatchSubmit.name": "Action on batch listing submit",
"deputy.setting.user.ia.onBatchSubmit.description": "When reporting a batch of pages, choose what to do after the \"Report\" button is selected and the pages are reported.",
"deputy.setting.user.ia.onBatchSubmit.nothing": "Do nothing",
"deputy.setting.user.ia.onBatchSubmit.reload": "Reload the noticeboard page",
"deputy.setting.wiki.core": "Core",
"deputy.setting.wiki.core.lastEdited.name": "Configuration last edited",
"deputy.setting.wiki.core.lastEdited.description": "The last time that this configuration was edited, as a timestamp. This is a way to ensure that all users are on the correct wiki-wide configuration version before changes are made. Checks are performed on every page load with Deputy.",
"deputy.setting.wiki.core.dispatchRoot.name": "Deputy Dispatch root URL",
"deputy.setting.wiki.core.dispatchRoot.description": "The URL to a Deputy Dispatch instance that can handle this wiki. Deputy Dispatch is a webserver responsible for centralizing and optimizing data used by Deputy, and can be used to reduce load on wikis. More information can be found at https://github.com/ChlodAlejandro/deputy-dispatch.",
"deputy.setting.wiki.core.changeTag.name": "Change tag",
"deputy.setting.wiki.core.changeTag.description": "Tag to use for all Deputy edits.",
"deputy.setting.wiki.cci": "CCI",
"deputy.setting.wiki.cci.enabled.name": "Enable contributor copyright investigations assistant",
"deputy.setting.wiki.cci.enabled.description": "Enables the CCI workflow assistant. This allows Deputy to replace the contribution survey found on CCI case pages with a graphical interface which works with other tabs to make the CCI workflow easier.",
"deputy.setting.wiki.cci.rootPage.name": "Root page",
"deputy.setting.wiki.cci.rootPage.description": "The main page that holds all subpages containing valid contribution copyright investigation cases.",
"deputy.setting.wiki.cci.headingMatch.name": "Heading title regular expression",
"deputy.setting.wiki.cci.headingMatch.description": "A regular expression that will be used to detect valid contribution surveyor heading. Since its usage is rather technical, this value should be edited by someone with technical knowledge of regular expressions.",
"deputy.setting.wiki.cci.collapseTop.name": "Collapsible wikitext (top)",
"deputy.setting.wiki.cci.collapseTop.description": "Placed just below a section heading when closing a contributor survey section. Use \"$1\" to denote user comments and signature. On the English Wikipedia, this is {{Template:collapse top}}. Other wikis may have an equivalent template. This should go hand in hand with \"{{int:deputy.setting.wiki.cci.collapseBottom.name}}\", as they are used as a pair.",
"deputy.setting.wiki.cci.collapseBottom.name": "Collapsible wikitext (bottom)",
"deputy.setting.wiki.cci.collapseBottom.description": "Placed at the end of a section when closing a contributor survey section. On the English Wikipedia, this is {{Template:collapse bottom}}. Other wikis may have an equivalent template.",
"deputy.setting.wiki.cci.earwigRoot.name": "Earwig's Copyvio Detector root URL",
"deputy.setting.wiki.cci.earwigRoot.description": "The URL to an instance of Earwig's Copyvio Detector that can handle this wiki. The official copyvio detector (copyvios.toolforge.org) can only handle Wikimedia wikis — you may change this behavior by specifying a custom instance that can process this wiki here.",
"deputy.setting.wiki.cci.resortRows.name": "Resort rows",
"deputy.setting.wiki.cci.resortRows.description": "Resort rows when saving the page. This is useful for cases where rows are added out of order, or when rows are added in a different order than they should be displayed.",
"deputy.setting.wiki.ante": "ANTE",
"deputy.setting.wiki.ante.enabled.name": "Enable the Attribution Notice Template Editor",
"deputy.setting.wiki.ante.enabled.description": "Enables ANTE for all users. ANTE is currently the least-optimized module for localization, and may not work for all wikis.",
"deputy.setting.wiki.ia": "IA",
"deputy.setting.wiki.ia.enabled.name": "Enable the Infringement Assistant",
"deputy.setting.wiki.ia.enabled.description": "Enables IA for all users. IA allows users to easily and graphically report pages with suspected or complicated copyright infringements.",
"deputy.setting.wiki.ia.rootPage.name": "Root page",
"deputy.setting.wiki.ia.rootPage.description": "The root page for Infringement Assistant. This should be the copyright problems noticeboard for this specific wiki. IA will only show quick response links for the root page and its subpages.",
"deputy.setting.wiki.ia.subpageFormat.name": "Subpage format",
"deputy.setting.wiki.ia.subpageFormat.description": "The format to use for subpages of the root page. This is a moment.js format string.",
"deputy.setting.wiki.ia.preload.name": "Preload",
"deputy.setting.wiki.ia.preload.description": "Defines the page content to preload the page with if a given subpage does not exist yet. This should be an existing page on-wiki. Leave blank to avoid using a preload entirely.",
"deputy.setting.wiki.ia.allowPresumptive.name": "Allow presumptive deletions",
"deputy.setting.wiki.ia.allowPresumptive.description": "Allows users to file listings for presumptive deletions. Note that the CCI setting \"Root page\" must be set for this to work, even if the \"CCI\" module is disabled entirely.",
"deputy.setting.wiki.ia.listingWikitext.name": "Listing wikitext",
"deputy.setting.wiki.ia.listingWikitext.description": "Defines the wikitext that will be used when adding listings to a noticeboard page. You may use \"$1\" to denote the page being reported, and \"$2\" for user comments (which shouldn't contain the signature).",
"deputy.setting.wiki.ia.listingWikitextMatch.name": "Regular expression for listings",
"deputy.setting.wiki.ia.listingWikitextMatch.description": "A regular expression that will be used to parse and detect listings on a given noticeboard page. Since its usage is rather technical, this value should be edited by someone with technical knowledge of regular expressions. This regular expression must provide three captured groups: group \"$1\" will catch any bullet point, space, or prefix, \"$2\" will catch the page title ONLY if the given page matches \"{{int:deputy.setting.wiki.ia.listingWikitext.name}}\" or \"{{int:deputy.setting.wiki.ia.batchListingWikitext.name}}\", and \"$3\" will catch the page title ONLY IF the page wasn't caught in \"$2\" (such as in cases where there is only a bare link to the page).",
"deputy.setting.wiki.ia.batchListingWikitext.name": "Batch listing wikitext",
"deputy.setting.wiki.ia.batchListingWikitext.description": "Defines the wikitext that will be used when adding batch listings to a noticeboard page. You may use \"$1\" to denote the page being reported, and \"$2\" for the list of pages (as determined by \"{{int:deputy.setting.wiki.ia.batchListingPageWikitext.name}}\") and \"$3\" for user comments (which doesn't contain the signature).",
"deputy.setting.wiki.ia.batchListingPageWikitext.name": "Batch listing page wikitext",
"deputy.setting.wiki.ia.batchListingPageWikitext.description": "Wikitext to use for every row of text in \"{{int:deputy.setting.wiki.ia.batchListingWikitext.name}}\". No line breaks are automatically added; these must be added into this string.",
"deputy.setting.wiki.ia.hideTemplate.name": "Content hiding wikitext (top)",
"deputy.setting.wiki.ia.hideTemplate.description": "Wikitext to hide offending content with. On the English Wikipedia, this is a usage of {{Template:copyvio}}. Other wikis may have an equivalent template. This should go hand in hand with \"{{int:deputy.setting.wiki.ia.hideTemplateBottom.name}}\", as they are used as a pair.",
"deputy.setting.wiki.ia.hideTemplateBottom.name": "Content hiding wikitext (bottom)",
"deputy.setting.wiki.ia.hideTemplateBottom.description": "Placed at the end of hidden content to hide only part of a page. On the English Wikipedia, this is {{Template:copyvio/bottom}}. Other wikis may have an equivalent template.",
"deputy.setting.wiki.ia.entirePageAppendBottom.name": "Append content hiding wikitext (bottom) when hiding an entire page",
"deputy.setting.wiki.ia.entirePageAppendBottom.description": "If enabled, the content hiding wikitext (bottom) will be appended to the end of the page when hiding the entire page. This avoids the \"missing end tag\" lint error, if the template is properly formatted.",
"deputy.setting.wiki.ia.responses.name": "Responses",
"deputy.setting.wiki.ia.responses.description": "Quick responses for copyright problems listings. Used by clerks to resolve specific listings or provide more information about the progress of a given listing."
};
 
/**
* Handles resource fetching operations.
*/
class DeputyResources {
/**
* Loads a resource from the provided resource root.
*
* @param path A path relative to the resource root.
* @return A Promise that resolves to the resource's content as a UTF-8 string.
*/
static loadResource(path) {
return __awaiter(this, void 0, void 0, function* () {
switch (this.root.type) {
case 'url': {
const headers = new Headers();
headers.set('Origin', window.___location.origin);
return fetch((new URL(path, this.root.url)).href, {
method: 'GET',
headers
}).then((r) => r.text());
}
case 'wiki': {
this.assertApi();
return getPageContent(this.root.prefix.replace(/\/$/, '') + '/' + path, {}, this.api);
}
}
});
}
/**
* Ensures that `this.api` is a valid ForeignApi.
*/
static assertApi() {
if (this.root.type !== 'wiki') {
return;
}
if (!this.api) {
this.api = new mw.ForeignApi(this.root.wiki.toString(), {
// Force anonymous mode. Deputy doesn't need user data anyway,
// so this should be fine.
anonymous: true
});
}
}
}
/**
* The root of all Deputy resources. This should serve static data that Deputy will
* use to load resources such as language files.
*/
DeputyResources.root = {
type: 'url',
url: new URL('https://tools-static.wmflabs.org/deputy/')
};
 
var _a;
const USER_LOCALE = (_a = window.deputyLang) !== null && _a !== void 0 ? _a : mw.config.get('wgUserLanguage');
 
/**
* Handles internationalization and localization for Deputy and sub-modules.
*/
class DeputyLanguage {
/**
* Loads the language for this Deputy interface.
*
* @param module The module to load a language pack for.
* @param fallback A fallback language pack to load. Since this is an actual
* `Record`, this relies on the language being bundled with the userscript. This ensures
* that a language pack is always available, even if a language file could not be loaded.
*/
static load(module, fallback) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const lang = (_a = window.deputyLang) !== null && _a !== void 0 ? _a : 'en';
// The loaded language resource file. Forced to `null` if using English, since English
// is our fallback language.
const langResource = lang === 'en' ? null :
yield DeputyResources.loadResource(`i18n/${module}/${lang}.json`)
.catch(() => {
// Could not find requested language file.
return null;
});
if (!langResource) {
// Fall back.
for (const key in fallback) {
mw.messages.set(key, fallback[key]);
}
return;
}
try {
const langData = JSON.parse(langResource);
for (const key in langData) {
mw.messages.set(key, langData[key]);
}
}
catch (e) {
error(e);
mw.notify(
// No languages to fall back on. Do not translate this string.
'Deputy: Requested language page is not a valid JSON file.', { type: 'error' });
// Fall back.
for (const key in fallback) {
mw.messages.set(key, fallback[key]);
}
}
if (lang !== mw.config.get('wgUserLanguage')) {
yield DeputyLanguage.loadSecondary();
}
});
}
/**
* Loads a specific moment.js locale. It's possible for nothing to be loaded (e.g. if the
* locale is not supported by moment.js), in which case nothing happens and English is
* likely used.
*
* @param locale The locale to load. `window.deputyLang` by default.
*/
static loadMomentLocale(locale = USER_LOCALE) {
return __awaiter(this, void 0, void 0, function* () {
if (locale === 'en') {
// Always loaded.
return;
}
if (mw.loader.getState('moment') !== 'ready') {
// moment.js is not yet loaded.
warn('Deputy tried loading moment.js locales but moment.js is not yet ready.');
return;
}
if (window.moment.locales().indexOf(locale) !== -1) {
// Already loaded.
return;
}
yield mw.loader.using('moment')
.then(() => true, () => null);
yield mw.loader.getScript(new URL(`resources/lib/moment/locale/${locale}.js`, new URL(mw.util.wikiScript('index'), window.___location.href)).href).then(() => true, () => null);
});
}
/**
* There are times when the user interface language do not match the wiki content
* language. Since Deputy's edit summary and content strings are found in the
* i18n files, however, there are cases where the wrong language would be used.
*
* This solves this problem by manually overriding content-specific i18n keys with
* the correct language. By default, all keys that match `deputy.*.content.**` get
* overridden.
*
* There are no fallbacks for this. If it fails, the user interface language is
* used anyway. In the event that the user interface language is not English,
* this will cause awkward situations. Whether or not something should be done to
* catch this specific edge case will depend on how frequent it happens.
*
* @param locale
* @param match
*/
static loadSecondary(locale = mw.config.get('wgContentLanguage'), match = /^deputy\.(?:[^.]+)?\.content\./g) {
return __awaiter(this, void 0, void 0, function* () {
// The loaded language resource file. Forced to `null` if using English, since English
// is our fallback language.
const langResource = locale === 'en' ? null :
yield DeputyResources.loadResource(`i18n/${module}/${locale}.json`)
.catch(() => {
// Could not find requested language file.
return null;
});
if (!langResource) {
return;
}
try {
const langData = JSON.parse(langResource);
for (const key in langData) {
if (cloneRegex$1(match).test(key)) {
mw.messages.set(key, langData[key]);
}
}
}
catch (e) {
// Silent failure.
error('Deputy: Requested language page is not a valid JSON file.', e);
}
});
}
}
 
/**
* Get the nodes from a JQuery object and wraps it in an element.
*
* @param element The element to add the children into
* @param $j The JQuery object
* @return The original element, now with children
*/
function unwrapJQ(element = h_1("span", null), $j) {
$j.each((i, e) => element.append(e));
return element;
}
 
let InternalConfigurationGroupTabPanel;
/**
* Initializes the process element.
*/
function initConfigurationGroupTabPanel() {
var _a;
InternalConfigurationGroupTabPanel = (_a = class ConfigurationGroupTabPanel extends OO.ui.TabPanelLayout {
/**
* @return The {@Link Setting}s for this group.
*/
get settings() {
return this.config.config.all[this.config.group];
}
/**
*/
constructor() {
super('configurationGroupPage_About');
this.$element.append(h_1("div", null,
h_1("div", { class: "deputy-about" },
h_1("div", { style: "flex: 0" },
h_1("img", { src: ConfigurationGroupTabPanel.logoUrl, alt: "Deputy logo" })),
h_1("div", { style: "flex: 1" },
h_1("div", null,
h_1("div", null, mw.msg('deputy.name')),
h_1("div", null, mw.msg('deputy.about.version', version, gitAbbrevHash))),
h_1("div", null, mw.msg('deputy.description')))),
h_1("div", null,
h_1("a", { href: "https://w.wiki/7NWR", target: "_blank" }, unwrapWidget(new OO.ui.ButtonWidget({
label: mw.msg('deputy.about.homepage'),
flags: ['progressive']
}))),
h_1("a", { href: "https://github.com/ChlodAlejandro/deputy", target: "_blank" }, unwrapWidget(new OO.ui.ButtonWidget({
label: mw.msg('deputy.about.openSource'),
flags: ['progressive']
}))),
h_1("a", { href: "https://w.wiki/7NWS", target: "_blank" }, unwrapWidget(new OO.ui.ButtonWidget({
label: mw.msg('deputy.about.contact'),
flags: ['progressive']
})))),
unwrapJQ(h_1("p", null), mw.message('deputy.about.credit').parseDom()),
unwrapJQ(h_1("p", null), mw.message('deputy.about.license', 'https://www.apache.org/licenses/LICENSE-2.0', 'https://github.com/ChlodAlejandro/deputy').parseDom()),
unwrapJQ(h_1("p", null), mw.message('deputy.about.thirdParty', 'https://github.com/ChlodAlejandro/deputy#licensing').parseDom()),
unwrapJQ(h_1("p", { style: { fontSize: '0.9em', color: 'darkgray' } }), mw.message('deputy.about.buildInfo', gitVersion, gitBranch, new Date(gitDate).toLocaleString()).parseDom()),
unwrapJQ(h_1("p", { style: { fontSize: '0.9em', color: 'darkgray' } }), mw.message('deputy.about.footer').parseDom())));
}
/**
* Sets up the tab item
*/
setupTabItem() {
this.tabItem.setLabel(mw.msg('deputy.about'));
return this;
}
},
_a.logoUrl = 'https://upload.wikimedia.org/wikipedia/commons/2/2b/Deputy_logo.svg',
_a);
}
/**
* Creates a new ConfigurationGroupTabPanel.
*
* @return A ConfigurationGroupTabPanel object
*/
function ConfigurationAboutTabPanel () {
if (!InternalConfigurationGroupTabPanel) {
initConfigurationGroupTabPanel();
}
return new InternalConfigurationGroupTabPanel();
}
 
let InternalConfigurationDialog;
/**
* Initializes the process element.
*/
function initConfigurationDialog() {
var _a;
InternalConfigurationDialog = (_a = class ConfigurationDialog extends OO.ui.ProcessDialog {
/**
* @param data
*/
constructor(data) {
super();
this.config = data.config;
}
/**
* @return The body height of this dialog.
*/
getBodyHeight() {
return 900;
}
/**
* Initializes the dialog.
*/
initialize() {
super.initialize();
this.layout = new OO.ui.IndexLayout();
this.layout.addTabPanels(this.generateGroupLayouts());
if (this.config instanceof UserConfiguration) {
this.layout.addTabPanels([ConfigurationAboutTabPanel()]);
}
this.$body.append(this.layout.$element);
return this;
}
/**
* Generate TabPanelLayouts for each configuration group.
*
* @return An array of TabPanelLayouts
*/
generateGroupLayouts() {
return Object.keys(this.config.all).map((group) => ConfigurationGroupTabPanel({
$overlay: this.$overlay,
config: this.config,
group
}));
}
/**
* @param action
* @return An OOUI Process.
*/
getActionProcess(action) {
const process = super.getActionProcess();
if (action === 'save') {
process.next(this.config.save());
process.next(() => {
var _a, _b;
mw.notify(mw.msg('deputy.settings.saved'), {
type: 'success'
});
if (this.config.type === 'user') {
// Override local Deputy option, just in case the user wishes to
// change the configuration again.
mw.user.options.set(UserConfiguration.optionKey, this.config.serialize());
if ((_a = window.deputy) === null || _a === void 0 ? void 0 : _a.comms) {
window.deputy.comms.send({
type: 'userConfigUpdate',
config: this.config.serialize()
});
}
}
else if (this.config.type === 'wiki') {
// We know it is a WikiConfiguration, the instanceof check here
// is just for type safety.
if ((_b = window.deputy) === null || _b === void 0 ? void 0 : _b.comms) {
window.deputy.comms.send({
type: 'wikiConfigUpdate',
config: {
title: this.config.sourcePage.getPrefixedText(),
editable: this.config.editable,
wt: this.config.serialize()
}
});
}
// Reload the page.
window.___location.reload();
}
});
}
process.next(() => {
this.close();
});
return process;
}
},
_a.static = Object.assign(Object.assign({}, OO.ui.ProcessDialog.static), { name: 'configurationDialog', title: mw.msg('deputy.settings.dialog.title'), size: 'large', actions: [
{
flags: ['safe', 'close'],
icon: 'close',
label: mw.msg('deputy.ante.close'),
title: mw.msg('deputy.ante.close'),
invisibleLabel: true,
action: 'close'
},
{
action: 'save',
label: mw.msg('deputy.save'),
flags: ['progressive', 'primary']
}
] }),
_a);
}
/**
* Creates a new ConfigurationDialog.
*
* @param data
* @return A ConfigurationDialog object
*/
function ConfigurationDialogBuilder(data) {
if (!InternalConfigurationDialog) {
initConfigurationDialog();
}
return new InternalConfigurationDialog(data);
}
let attached = false;
/**
* Spawns a new configuration dialog.
*
* @param config
*/
function spawnConfigurationDialog(config) {
mw.loader.using([
'oojs-ui-core', 'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets'
], () => {
const dialog = ConfigurationDialogBuilder({ config });
openWindow(dialog);
});
}
/**
* Attaches the "Deputy preferences" portlet link in the toolbox. Ensures that it doesn't
* get attached twice.
*/
function attachConfigurationDialogPortletLink() {
return __awaiter(this, void 0, void 0, function* () {
if (document.querySelector('#p-deputy-config') || attached) {
return;
}
attached = true;
mw.util.addCSS(deputySettingsStyles);
yield DeputyLanguage.load('settings', deputySettingsEnglish);
mw.util.addPortletLink('p-tb', '#', mw.msg('deputy.settings.portlet'), 'deputy-config', mw.msg('deputy.settings.portlet.tooltip')).addEventListener('click', () => {
// Load a fresh version of the configuration - this way we can make
// modifications live to the configuration without actually affecting
// tool usage.
spawnConfigurationDialog(UserConfiguration.load());
});
});
}
 
let InternalDeputyMessageWidget;
/**
* Initializes the process element.
*/
function initDeputyMessageWidget() {
InternalDeputyMessageWidget = class DeputyMessageWidget extends OO.ui.MessageWidget {
/**
* @param config Configuration to be passed to the element.
*/
constructor(config) {
var _a;
super(config);
this.$element.addClass('dp-messageWidget');
const elLabel = this.$label[0];
if (!config.label) {
if (config.title) {
elLabel.appendChild(h_1("b", { style: { display: 'block' } }, config.title));
}
if (config.message) {
elLabel.appendChild(h_1("p", { class: "dp-messageWidget-message" }, config.message));
}
}
if (config.actions || config.closable) {
const actionContainer = h_1("div", { class: "dp-messageWidget-actions" });
for (const action of ((_a = config.actions) !== null && _a !== void 0 ? _a : [])) {
if (action instanceof OO.ui.Element) {
actionContainer.appendChild(unwrapWidget(action));
}
else {
actionContainer.appendChild(action);
}
}
if (config.closable) {
const closeButton = new OO.ui.ButtonWidget({
label: mw.msg('deputy.dismiss')
});
closeButton.on('click', () => {
removeElement(unwrapWidget(this));
this.emit('close');
});
actionContainer.appendChild(unwrapWidget(closeButton));
}
elLabel.appendChild(actionContainer);
}
}
};
}
/**
* Creates a new DeputyMessageWidget. This is an extension of the default
* OOUI MessageWidget. It includes support for a title, a message, and button
* actions.
*
* @param config Configuration to be passed to the element.
* @return A DeputyMessageWidget object
*/
function DeputyMessageWidget (config) {
if (!InternalDeputyMessageWidget) {
initDeputyMessageWidget();
}
return new InternalDeputyMessageWidget(config);
}
 
/**
* @param config The current configuration (actively loaded, not the one being viewed)
* @return An HTML element consisting of an OOUI MessageWidget
*/
function WikiConfigurationEditIntro(config) {
const current = config.onConfigurationPage();
let buttons;
if (current) {
const editCurrent = new OO.ui.ButtonWidget({
flags: ['progressive', 'primary'],
label: mw.msg('deputy.settings.wikiEditIntro.edit.current'),
disabled: !mw.config.get('wgIsProbablyEditable'),
title: mw.config.get('wgIsProbablyEditable') ?
undefined : mw.msg('deputy.settings.wikiEditIntro.edit.protected')
});
editCurrent.on('click', () => {
spawnConfigurationDialog(config);
});
buttons = [editCurrent];
}
else {
const editCurrent = new OO.ui.ButtonWidget({
flags: ['progressive', 'primary'],
label: mw.msg('deputy.settings.wikiEditIntro.edit.otherCurrent'),
disabled: !config.editable,
title: config.editable ?
undefined : mw.msg('deputy.settings.wikiEditIntro.edit.protected')
});
editCurrent.on('click', () => __awaiter(this, void 0, void 0, function* () {
spawnConfigurationDialog(config);
}));
const editOther = new OO.ui.ButtonWidget({
flags: ['progressive'],
label: mw.msg('deputy.settings.wikiEditIntro.edit.other'),
disabled: !mw.config.get('wgIsProbablyEditable'),
title: mw.config.get('wgIsProbablyEditable') ?
undefined : mw.msg('deputy.settings.wikiEditIntro.edit.protected')
});
editOther.on('click', () => __awaiter(this, void 0, void 0, function* () {
spawnConfigurationDialog(yield config.static.load(normalizeTitle()));
}));
buttons = [editCurrent, editOther];
}
const messageBox = DeputyMessageWidget({
classes: [
'deputy', 'dp-mb'
],
type: 'notice',
title: mw.msg('deputy.settings.wikiEditIntro.title'),
message: current ?
mw.msg('deputy.settings.wikiEditIntro.current') :
unwrapJQ(h_1("span", null), mw.message('deputy.settings.wikiEditIntro.other', config.sourcePage.getPrefixedText()).parseDom()),
actions: buttons
});
const box = unwrapWidget(messageBox);
box.classList.add('deputy', 'deputy-wikiConfig-intro');
return box;
}
 
/* eslint-disable max-len */
/*
* Replacement polyfills for wikis that have no configured templates.
* Used in WikiConfiguration, to permit a seamless OOB experience.
*/
/** `{{collapse top}}` equivalent */
const collapseTop = `
{| class="mw-collapsible mw-collapsed" style="border:1px solid #C0C0C0;width:100%"
! <div style="background:#CCFFCC;">$1</div>
|-
|
`.trimStart();
/** `{{collapse bottom}}` equivalent */
const collapseBottom = `
|}`;
/** `* {{subst:article-cv|1=$1}} $2 ~~~~` equivalent */
const listingWikitext = '* [[$1]] $2 ~~~~';
/**
* Polyfill for the following:
* `; {{anchor|1={{delink|$1}}}} $1
* $2
* $3 ~~~~`
*/
const batchListingWikitext = `*; <span style="display: none;" id="$1"></span> $1
$2
$3`;
/**
* Inserted and chained as part of $2 in `batchListingWikitext`.
* Equivalent of `* {{subst:article-cv|1=$1}}\n`. Newline is intentional.
*/
const batchListingPageWikitext = '* [[$1]]\n';
/**
* `{{subst:copyvio|url=$1|fullpage=$2}}` equivalent
*/
const copyvioTop = `<div style="padding: 8px; border: 4px solid #0298b1;">
<div style="font-size: 1.2rem"><b>{{int:deputy.ia.content.copyvio}}</b></div>
<div>{{int:deputy.ia.content.copyvio.help}}</div>
{{if:$1|<div>{{if:$presumptive|{{int:deputy.ia.content.copyvio.from.pd}} $1|{{int:deputy.ia.content.copyvio.from}} $1}}</div>}}
</div>
<!-- {{int:deputy.ia.content.copyvio.content}} -->
<div class="copyvio" style="display: none">`;
/**
* `{{subst:copyvio/bottom}}` equivalent.
*/
const copyvioBottom = `
</div>`;
 
/**
* @return A MessageWidget for reloading a page with an outdated configuration.
*/
function ConfigurationReloadBanner() {
const reloadButton = new OO.ui.ButtonWidget({
flags: ['progressive', 'primary'],
label: mw.msg('deputy.settings.wikiOutdated.reload')
});
const messageBox = DeputyMessageWidget({
classes: [
'deputy', 'dp-mb', 'dp-wikiConfigUpdateMessage'
],
type: 'notice',
title: mw.msg('deputy.settings.wikiOutdated'),
message: mw.msg('deputy.settings.wikiOutdated.help'),
actions: [reloadButton]
});
reloadButton.on('click', () => __awaiter(this, void 0, void 0, function* () {
window.___location.reload();
}));
const box = unwrapWidget(messageBox);
box.style.fontSize = 'calc(1em * 0.875)';
return box;
}
 
var WikiConfigurationLocations = [
'MediaWiki:Deputy-config.json',
// Prioritize interface protected page over Project namespace
'User:Chlod/Scripts/Deputy/configuration.json',
'Project:Deputy/configuration.json'
];
 
/**
* Automatically applies a change tag to edits made by the user if
* a change tag was provided in the configuration.
*
* @param config
* @return A spreadable Object containing the `tags` parameter for `action=edit`.
*/
function changeTag(config) {
return config.core.changeTag.get() ?
{ tags: config.core.changeTag.get() } :
{};
}
 
/**
* Wiki-wide configuration. This is applied to all users of the wiki, and has
* the potential to break things for EVERYONE if not set to proper values.
*
* As much as possible, the correct configuration ___location should be protected
* to avoid vandalism or bad-faith changes.
*
* This configuration works if specific settings are set. In other words, some
* features of Deputy are disabled unless Deputy has been configured. This is
* to avoid messing with existing on-wiki processes.
*/
class WikiConfiguration extends ConfigurationBase {
/**
* Loads the configuration from a set of possible sources.
*
* @param sourcePage The specific page to load from
* @return A WikiConfiguration object
*/
static load(sourcePage) {
return __awaiter(this, void 0, void 0, function* () {
if (sourcePage) {
// Explicit source given. Do not load from local cache.
return this.loadFromWiki(sourcePage);
}
else {
return this.loadFromLocal();
}
});
}
/**
* Loads the wiki configuration from localStorage and/or MediaWiki
* settings. This allows for faster loads at the expense of a (small)
* chance of outdated configuration.
*
* The localStorage layer allows fast browser-based caching. If a user
* is logging in again on another device, the user configuration
* will automatically be sent to the client, lessening turnaround time.
* If all else fails, the configuration will be loaded from the wiki.
*
* @return A WikiConfiguration object.
*/
static loadFromLocal() {
return __awaiter(this, void 0, void 0, function* () {
let configInfo;
// If `mw.storage.get` returns `false` or `null`, it'll be thrown up.
let rawConfigInfo = mw.storage.get(WikiConfiguration.optionKey);
// Try to grab it from user options, if it exists.
if (!rawConfigInfo) {
rawConfigInfo = mw.user.options.get(WikiConfiguration.optionKey);
}
if (typeof rawConfigInfo === 'string') {
try {
configInfo = JSON.parse(rawConfigInfo);
}
catch (e) {
// Bad local! Switch to non-local.
error('Failed to get Deputy wiki configuration', e);
return this.loadFromWiki();
}
}
else {
log('No locally-cached Deputy configuration, pulling from wiki.');
return this.loadFromWiki();
}
if (configInfo) {
return new WikiConfiguration(new mw.Title(configInfo.title.title, configInfo.title.namespace), JSON.parse(configInfo.wt), configInfo.editable);
}
else {
return this.loadFromWiki();
}
});
}
/**
* Loads the configuration from the current wiki.
*
* @param sourcePage The specific page to load from
* @return A WikiConfiguration object
*/
static loadFromWiki(sourcePage) {
return __awaiter(this, void 0, void 0, function* () {
const configPage = sourcePage ? Object.assign({ title: sourcePage }, yield (() => __awaiter(this, void 0, void 0, function* () {
const content = yield getPageContent(sourcePage, {
prop: 'revisions|info',
intestactions: 'edit',
fallbacktext: '{}'
});
return {
wt: content,
editable: content.page.actions.edit
};
}))()) : yield this.loadConfigurationWikitext();
try {
// Attempt save of configuration to local options (if not explicitly loaded)
if (sourcePage == null) {
mw.storage.set(WikiConfiguration.optionKey, JSON.stringify(configPage));
}
return new WikiConfiguration(configPage.title, JSON.parse(configPage.wt), configPage.editable);
}
catch (e) {
error(e, configPage);
mw.hook('deputy.i18nDone').add(function notifyConfigFailure() {
mw.notify(mw.msg('deputy.loadError.wikiConfig'), {
type: 'error'
});
mw.hook('deputy.i18nDone').remove(notifyConfigFailure);
});
return null;
}
});
}
/**
* Loads the wiki-wide configuration from a set of predefined locations.
* See {@link WikiConfiguration#configLocations} for a full list.
*
* @return The string text of the raw configuration, or `null` if a configuration was not found.
*/
static loadConfigurationWikitext() {
return __awaiter(this, void 0, void 0, function* () {
const response = yield MwApi.action.get({
action: 'query',
prop: 'revisions|info',
rvprop: 'content',
rvslots: 'main',
rvlimit: 1,
intestactions: 'edit',
redirects: true,
titles: WikiConfiguration.configLocations.join('|')
});
const redirects = toRedirectsObject(response.query.redirects, response.query.normalized);
for (const page of WikiConfiguration.configLocations) {
const title = normalizeTitle(redirects[page] || page).getPrefixedText();
const pageInfo = response.query.pages.find((p) => p.title === title);
if (!pageInfo.missing) {
return {
title: normalizeTitle(pageInfo.title),
wt: pageInfo.revisions[0].slots.main.content,
editable: pageInfo.actions.edit
};
}
}
return null;
});
}
/**
* Check if the current page being viewed is a valid configuration page.
*
* @param page
* @return `true` if the current page is a valid configuration page.
*/
static isConfigurationPage(page) {
if (page == null) {
page = new mw.Title(mw.config.get('wgPageName'));
}
return this.configLocations.some((v) => equalTitle(page, normalizeTitle(v)));
}
/**
* @param sourcePage
* @param serializedData
* @param editable Whether the configuration is editable by the current user or not.
*/
constructor(sourcePage, serializedData, editable) {
var _a;
super();
this.sourcePage = sourcePage;
this.serializedData = serializedData;
this.editable = editable;
// Used to avoid circular dependencies.
this.static = WikiConfiguration;
this.core = {
/**
* Numerical code that identifies this config version. Increments for every breaking
* configuration file change.
*/
configVersion: new Setting({
defaultValue: WikiConfiguration.configVersion,
displayOptions: { hidden: true },
alwaysSave: true
}),
lastEdited: new Setting({
defaultValue: 0,
displayOptions: { hidden: true },
alwaysSave: true
}),
dispatchRoot: new Setting({
serialize: (v) => v.href,
deserialize: (v) => new URL(v),
defaultValue: new URL('https://deputy.toolforge.org/'),
displayOptions: { type: 'text' },
alwaysSave: true
}),
changeTag: new Setting({
defaultValue: null,
displayOptions: { type: 'text' }
})
};
this.cci = {
enabled: new Setting({
defaultValue: false,
displayOptions: { type: 'checkbox' }
}),
rootPage: new Setting({
serialize: (v) => v === null || v === void 0 ? void 0 : v.getPrefixedText(),
deserialize: (v) => new mw.Title(v),
defaultValue: null,
displayOptions: { type: 'page' }
}),
headingMatch: new Setting({
defaultValue: '(Page|Article|Local file|File)s? \\d+ (to|through) \\d+',
displayOptions: { type: 'text' }
}),
collapseTop: new Setting({
defaultValue: collapseTop,
displayOptions: { type: 'code' }
}),
collapseBottom: new Setting({
defaultValue: collapseBottom,
displayOptions: { type: 'code' }
}),
earwigRoot: new Setting({
serialize: (v) => v.href,
deserialize: (v) => new URL(v),
defaultValue: new URL('https://copyvios.toolforge.org/'),
displayOptions: { type: 'text' },
alwaysSave: true
}),
resortRows: new Setting({
defaultValue: true,
displayOptions: { type: 'checkbox' }
})
};
this.ante = {
enabled: new Setting({
defaultValue: false,
displayOptions: { type: 'checkbox' }
})
};
this.ia = {
enabled: new Setting({
defaultValue: false,
displayOptions: { type: 'checkbox' }
}),
rootPage: new Setting({
serialize: (v) => v === null || v === void 0 ? void 0 : v.getPrefixedText(),
deserialize: (v) => new mw.Title(v),
defaultValue: null,
displayOptions: { type: 'page' }
}),
subpageFormat: new Setting({
defaultValue: 'YYYY MMMM D',
displayOptions: { type: 'text' }
}),
preload: new Setting({
serialize: (v) => { var _a, _b; return ((_b = (_a = v === null || v === void 0 ? void 0 : v.trim()) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) === 0 ? null : v.trim(); },
defaultValue: null,
displayOptions: { type: 'page' }
}),
allowPresumptive: new Setting({
defaultValue: true,
displayOptions: { type: 'checkbox' }
}),
listingWikitext: new Setting({
defaultValue: listingWikitext,
displayOptions: { type: 'code' }
}),
/**
* $1 - Title of the batch
* $2 - List of pages (newlines should be added in batchListingPageWikitext).
* $3 - User comment
*/
batchListingWikitext: new Setting({
defaultValue: batchListingWikitext,
displayOptions: { type: 'code' }
}),
/**
* $1 - Page to include
*/
batchListingPageWikitext: new Setting({
defaultValue: batchListingPageWikitext,
displayOptions: { type: 'code' }
}),
/**
* @see {@link CopyrightProblemsListing#articleCvRegex}
*
* This should match both normal and batch listings.
*/
listingWikitextMatch: new Setting({
defaultValue: '(\\*\\s*)?\\[\\[([^\\]]+)\\]\\]',
displayOptions: { type: 'code' }
}),
hideTemplate: new Setting({
defaultValue: copyvioTop,
displayOptions: { type: 'code' }
}),
hideTemplateBottom: new Setting({
defaultValue: copyvioBottom,
displayOptions: { type: 'code' }
}),
entirePageAppendBottom: new Setting({
defaultValue: true,
displayOptions: { type: 'checkbox' }
}),
responses: new Setting(Object.assign(Object.assign({}, Setting.basicSerializers), { defaultValue: null, displayOptions: { type: 'unimplemented' } }))
};
this.type = 'wiki';
this.all = { core: this.core, cci: this.cci, ante: this.ante, ia: this.ia };
/**
* Set to true when this configuration is outdated based on latest data. Usually adds banners
* to UI interfaces saying a new version of the configuration is available, and that it should
* be used whenever possible.
*
* TODO: This doesn't do what the documentations says yet.
*/
this.outdated = false;
if (serializedData) {
this.deserialize(serializedData);
}
if ((_a = window.deputy) === null || _a === void 0 ? void 0 : _a.comms) {
// Communications is available. Register a listener.
window.deputy.comms.addEventListener('wikiConfigUpdate', (e) => {
this.update(Object.assign({}, e.data.config, {
title: normalizeTitle(e.data.config.title)
}));
});
}
}
/**
* Check for local updates, and update the local configuration as needed.
*
* @param sourceConfig A serialized version of the configuration based on a wiki
* page configuration load.
*/
update(sourceConfig) {
return __awaiter(this, void 0, void 0, function* () {
// Asynchronously load from the wiki.
let fromWiki;
if (sourceConfig) {
fromWiki = sourceConfig;
}
else {
// Asynchronously load from the wiki.
fromWiki = yield WikiConfiguration.loadConfigurationWikitext();
if (fromWiki == null) {
// No configuration found on the wiki.
return;
}
}
const liveWikiConfig = JSON.parse(fromWiki.wt);
// Attempt save if on-wiki config found and doesn't match local.
// Doesn't need to be from the same config page, since this usually means a new config
// page was made, and we need to switch to it.
if (this.core.lastEdited.get() < liveWikiConfig.core.lastEdited) {
if (liveWikiConfig.core.configVersion > WikiConfiguration.configVersion) {
// Don't update if the config version is higher than ours. We don't want
// to load in the config of a newer version, as it may break things.
// Deputy should load in the newer version of the script soon enough,
// and the config will be parsed by a version that supports it.
warn(`Expected wiki configuration version ${this.core.configVersion.get()}, but found ${liveWikiConfig.core.configVersion}. New configuration will not be loaded.`);
return;
}
else if (liveWikiConfig.core.configVersion < WikiConfiguration.configVersion) {
// Version change detected.
// Do nothing... for now.
// HINT: Update configuration
warn(`Expected wiki configuration version ${this.core.configVersion.get()}, but found ${liveWikiConfig.core.configVersion}. Proceeding anyway...`);
}
const onSuccess = () => {
var _a;
// Only mark outdated after saving, so we don't indirectly cause a save operation
// to cancel.
this.outdated = true;
// Attempt to add site notice.
if (document.querySelector('.dp-wikiConfigUpdateMessage') == null) {
(_a = document.getElementById('siteNotice')) === null || _a === void 0 ? void 0 : _a.insertAdjacentElement('afterend', ConfigurationReloadBanner());
}
};
// If updated from a source config (other Deputy tab), do not attempt to save
// to MediaWiki settings. This is most likely already saved by the original tab
// that sent the comms message.
if (!sourceConfig) {
// Use `liveWikiConfig`, since this contains the compressed version and is more
// bandwidth-friendly.
const rawConfigInfo = JSON.stringify({
title: fromWiki.title,
editable: fromWiki.editable,
wt: JSON.stringify(liveWikiConfig)
});
// Save to local storage.
mw.storage.set(WikiConfiguration.optionKey, rawConfigInfo);
// Save to user options (for faster first-load times).
yield MwApi.action.saveOption(WikiConfiguration.optionKey, rawConfigInfo).then(() => {
var _a;
if ((_a = window.deputy) === null || _a === void 0 ? void 0 : _a.comms) {
// Broadcast the update to other tabs.
window.deputy.comms.send({
type: 'wikiConfigUpdate',
config: {
title: fromWiki.title.getPrefixedText(),
editable: fromWiki.editable,
wt: liveWikiConfig
}
});
}
onSuccess();
}).catch(() => {
// silently fail
});
}
else {
onSuccess();
}
}
});
}
/**
* Saves the configuration on-wiki. Does not automatically generate overrides.
*/
save() {
return __awaiter(this, void 0, void 0, function* () {
// Update last edited number
this.core.lastEdited.set(Date.now());
yield MwApi.action.postWithEditToken(Object.assign(Object.assign({}, changeTag(yield window.deputy.getWikiConfig())), { action: 'edit', title: this.sourcePage.getPrefixedText(), text: JSON.stringify(this.serialize()) }));
});
}
/**
* Check if the current page being viewed is the active configuration page.
*
* @param page
* @return `true` if the current page is the active configuration page.
*/
onConfigurationPage(page) {
return equalTitle(page !== null && page !== void 0 ? page : mw.config.get('wgPageName'), this.sourcePage);
}
/**
* Actually displays the banner which allows an editor to modify the configuration.
*/
displayEditBanner() {
return __awaiter(this, void 0, void 0, function* () {
mw.loader.using(['oojs', 'oojs-ui'], () => {
if (document.getElementsByClassName('deputy-wikiConfig-intro').length > 0) {
return;
}
document.getElementById('mw-content-text').insertAdjacentElement('afterbegin', WikiConfigurationEditIntro(this));
});
});
}
/**
* Shows the configuration edit intro banner, if applicable on this page.
*
* @return void
*/
prepareEditBanners() {
return __awaiter(this, void 0, void 0, function* () {
if (['view', 'diff'].indexOf(mw.config.get('wgAction')) === -1) {
return;
}
if (document.getElementsByClassName('deputy-wikiConfig-intro').length > 0) {
return;
}
if (this.onConfigurationPage()) {
return this.displayEditBanner();
}
else if (WikiConfiguration.isConfigurationPage()) {
return this.displayEditBanner();
}
});
}
}
WikiConfiguration.configVersion = 2;
WikiConfiguration.optionKey = 'userjs-deputy-wiki';
WikiConfiguration.configLocations = WikiConfigurationLocations;
 
/**
* API communication class
*/
class Dispatch {
/**
* Creates a Deputy API instance.
*/
constructor() { }
/**
* Logs the user out of the API.
*/
logout() {
return __awaiter(this, void 0, void 0, function* () {
// TODO: Make logout API request
yield window.deputy.storage.setKV('api-token', null);
});
}
/**
* Logs in the user. Optional: only used for getting data on deleted revisions.
*/
login() {
return __awaiter(this, void 0, void 0, function* () {
Dispatch.token = yield window.deputy.storage.getKV('api-token');
// TODO: If token, set token
// TODO: If no token, start OAuth flow and make login API request
throw new Error('Unimplemented method.');
});
}
/**
* Returns a fully-formed HTTP URL from a given endpoint. This uses the wiki's
* set Dispatch endpoint and a given target (such as `/v1/revisions`) to get
* the full URL.
*
* @param endpoint The endpoint to get
*/
getEndpoint(endpoint) {
return __awaiter(this, void 0, void 0, function* () {
return new URL(endpoint.replace(/^\/+/, ''), (yield WikiConfiguration.load()).core.dispatchRoot.get()
.href
.replace(/\/+$/, ''));
});
}
}
/**
* Singleton instance.
*/
Dispatch.i = new Dispatch();
 
/**
*
*/
class DispatchRevisions {
/**
*
*/
constructor() { }
/**
* Gets expanded revision data from the API. This returns a response similar to the
* `revisions` object provided by action=query, but also includes additional information
* relevant (such as the parsed (HTML) comment, diff size, etc.)
*
* @param revisions The revisions to get the data for
* @return An object of expanded revision data mapped by revision IDs
*/
get(revisions) {
return __awaiter(this, void 0, void 0, function* () {
return Requester.fetch(yield Dispatch.i.getEndpoint(`v1/revisions/${mw.config.get('wgWikiID')}`), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Api-User-Agent': `Deputy/${version} (${window.___location.hostname})`
},
body: 'revisions=' + revisions.join('|')
})
.then((r) => r.json())
.then((j) => {
if (j.error) {
throw new Error(j.error.info);
}
return j;
})
.then((j) => j.revisions);
});
}
}
/**
* Singleton instance
*/
DispatchRevisions.i = new DispatchRevisions();
 
var ContributionSurveyRowStatus;
Line 1,352 ⟶ 4,281:
ContributionSurveyRowStatus[ContributionSurveyRowStatus["PresumptiveRemoval"] = 5] = "PresumptiveRemoval";
})(ContributionSurveyRowStatus || (ContributionSurveyRowStatus = {}));
 
/**
* Represents a contribution survey row. This is an abstraction of the row that can
Line 1,359 ⟶ 4,289:
class ContributionSurveyRow {
/**
* CreatesIdentifies a newrow's contributioncurrent surveystatus rowbased fromon MediaWikithe parsercomment's outputcontents.
*
* @param casePagecomment The casecomment pageto of this rowprocess
* @param wikitextreturn The wikitextstatus of the row
*/
static identifyCommentStatus(comment) {
constructor(casePage, wikitext) {
for (const rowExecstatus =in cloneRegex$1(ContributionSurveyRow.rowWikitextRegexcommentMatchRegex).exec(wikitext); {
if (cloneRegex$1(ContributionSurveyRow.commentMatchRegex[+status]).test(comment)) {
this.casePage = casePage;
this.wikitext = wikitext return +status;
this.title = new mw.Title(rowExec[1]);
this.extras = rowExec[2];
this.comment = rowExec[4];
this.status = this.originalStatus = rowExec[4] == null ?
ContributionSurveyRowStatus.Unfinished :
ContributionSurveyRow.identifyCommentStatus(rowExec[4]);
if (ContributionSurveyRow.commentMatchRegex[this.status] != null) {
if (cloneRegex$1((ContributionSurveyRow.commentMatchRegex)[this.status], { pre: '^' }).test(this.comment)) {
this.statusIsolated = 'start';
}
else if (cloneRegex$1((ContributionSurveyRow.commentMatchRegex)[this.status], { post: '$' }).test(this.comment)) {
this.statusIsolated = 'end';
}
else {
this.statusIsolated = false;
}
}
return ContributionSurveyRowStatus.Unknown;
}
/**
* DeterminesGuesses ifthe asort givenorder wikitext line isfor a validgiven contributionset surveyof rowrevisions.
*
* @param textdiffs The wikitextdiffs to checkguess from.
* @return WhetherThe thesort provided wikitext is a contribution survey row or notorder
*/
static isContributionSurveyRowTextguessSortOrder(textdiffs) {
let last = null;
return cloneRegex$1(ContributionSurveyRow.rowWikitextRegex).test(text);
let dateScore = 1;
let dateReverseScore = 1;
let byteScore = 1;
let dateStreak = 0;
let dateReverseStreak = 0;
let byteStreak = 0;
for (const diff of diffs) {
if (last == null) {
last = diff;
}
else {
const diffTimestamp = new Date(diff.timestamp).getTime();
const lastTimestamp = new Date(last.timestamp).getTime();
// The use of the OR operator here has a specific purpose:
// * On the first iteration, we want all streak values to be 1
// * On any other iteration, we want it to increment the streak by 1 if a streak
// exists, or set it to 1 if a streak was broken.
dateStreak =
diffTimestamp > lastTimestamp ? dateStreak + 1 : 0;
dateReverseStreak =
diffTimestamp < lastTimestamp ? dateReverseStreak + 1 : 0;
byteStreak =
diff.diffsize <= last.diffsize ? byteStreak + 1 : 0;
dateScore = (dateScore + ((diffTimestamp > lastTimestamp ? 1 : 0) * (1 + dateStreak * 0.3))) / 2;
dateReverseScore = (dateReverseScore + ((diffTimestamp < lastTimestamp ? 1 : 0) * (1 + dateReverseStreak * 0.3))) / 2;
byteScore = (byteScore + ((diff.diffsize <= last.diffsize ? 1 : 0) * (1 + byteStreak * 0.3))) / 2;
last = diff;
}
}
// Multiply by weights to remove ties
dateScore *= 1.05;
dateReverseScore *= 1.025;
switch (Math.max(dateScore, dateReverseScore, byteScore)) {
case byteScore:
return ContributionSurveyRowSort.Bytes;
case dateScore:
return ContributionSurveyRowSort.Date;
case dateReverseScore:
return ContributionSurveyRowSort.DateReverse;
}
}
/**
* IdentifiesGets the sorter function which will sort a row'sset currentof statusdiffs based on the comment'sa contents.given
* sort order.
*
* @param comment The comment to processsort
* @returnparam mode The statussort ofmode to use. If `array`, the rowreturned function sorts an
* array of revisions. If `key`, the returned function sorts entries with the first
* entry element (`entry[0]`) being a revision. If `value`, the returned function
* sorts values with the second entry element (`entry[1]`) being a revision.
* @return The sorted array
*/
static identifyCommentStatusgetSorterFunction(commentsort, mode = 'array') {
forreturn (const_a, status_b) in ContributionSurveyRow.commentMatchRegex)=> {
let a, b;
if (cloneRegex$1(ContributionSurveyRow.commentMatchRegex[+status]).test(comment)) {
switch (mode) return +status;{
case 'array':
a = _a;
b = _b;
break;
case 'key':
a = _a[0];
b = _b[0];
break;
case 'value':
a = _a[1];
b = _b[1];
break;
}
} switch (sort) {
return ContributionSurveyRowStatus case ContributionSurveyRowSort.Unknown;Date:
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
case ContributionSurveyRowSort.DateReverse:
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
case ContributionSurveyRowSort.Bytes:
return b.diffsize - a.diffsize;
}
};
}
/**
Line 1,422 ⟶ 4,402:
return this.status !== ContributionSurveyRowStatus.Unfinished &&
this.diffs.size === 0;
}
/**
* Creates a new contribution survey row from MediaWiki parser output.
*
* @param casePage The case page of this row
* @param wikitext The wikitext of the row
*/
constructor(casePage, wikitext) {
this.data = new ContributionSurveyRowParser(wikitext).parse();
this.type = this.data.type;
this.casePage = casePage;
this.wikitext = wikitext;
this.title = new mw.Title(this.data.page);
this.extras = this.data.extras;
this.comment = this.data.comments;
this.status = this.originalStatus = this.data.comments == null ?
ContributionSurveyRowStatus.Unfinished :
ContributionSurveyRow.identifyCommentStatus(this.data.comments);
if (ContributionSurveyRow.commentMatchRegex[this.status] != null) {
if (cloneRegex$1((ContributionSurveyRow.commentMatchRegex)[this.status], { pre: '^' }).test(this.comment)) {
this.statusIsolated = 'start';
}
else if (cloneRegex$1((ContributionSurveyRow.commentMatchRegex)[this.status], { post: '$' }).test(this.comment)) {
this.statusIsolated = 'end';
}
else {
this.statusIsolated = false;
}
}
}
/**
Line 1,433 ⟶ 4,442:
return this.diffs;
}
const rowExec = cloneRegex$1(ContributionSurveyRow.rowWikitextRegex).exec(this.wikitext);
const revisionData = new Map();
const diffsrevids = []this.data.revids;
// Load revision information
ifconst (rowExec[3]toCache !== null && rowExec.length !== 0) {[];
for (const diffRegexrevisionID =of cloneRegex$1(/Special:Diff\/(\d+revids)/g); {
letconst diffMatchcachedDiff = diffRegexyield window.execdeputy.storage.db.get(rowExec[0]'diffCache', revisionID);
whileif (diffMatch != nullcachedDiff) {
if revisionData.set(diffMatch[1]revisionID, ==new null)ContributionSurveyRevision(this, {cachedDiff));
console.warn('Could not parse revision ID: ' + diffMatch[0]);
}
else {
diffs.push(+diffMatch[1]);
}
diffMatch = diffRegex.exec(rowExec[0]);
}
constelse toCache = [];{
for (const revisionID of diffstoCache.push(revisionID) {;
const cachedDiff = yield window.deputy.storage.db.get('diffCache', revisionID);
if (cachedDiff) {
revisionData.set(revisionID, new ContributionSurveyRevision(this, cachedDiff));
}
else {
toCache.push(revisionID);
}
}
}
if (toCache.length > 0) {
const expandedData = yield window.deputyDispatchRevisions.apii.getExpandedRevisionDataget(toCache);
for (const revisionID in expandedData) {
revisionData.set(+revisionID, new ContributionSurveyRevision(this, expandedData[revisionID]));
}
for (const revisionID in expandedData) {
yield window.deputy.storage.db.put('diffCache', expandedData[revisionID]);
}
}
}
// Load tag messages
// First gather all tags mentioned, and then load messages.
const tags = Array.from(revisionData.values()).reduce((acc, cur) => {
forif (const tag of cur.tags) {
iffor (acc.indexOf(const tag) ===of -1cur.tags) {
if (acc.pushindexOf(tag); === -1) {
acc.push(tag);
}
}
}
Line 1,481 ⟶ 4,479:
amenableparser: true
});
// Sort fromthe mostrows bytes(if torearranging least.is enabled)
if (window.deputy.wikiConfig.cci.resortRows.get()) {
return this.diffs = new Map([...revisionData.entries()].sort((a, b) => b[1].diffsize - a[1].diffsize));
const sortOrder = ContributionSurveyRow.guessSortOrder(revisionData.values());
return this.diffs = new Map([...revisionData.entries()].sort(ContributionSurveyRow.getSorterFunction(sortOrder, 'value')));
}
else {
return this.diffs = new Map([...revisionData.entries()]);
}
});
}
Line 1,504 ⟶ 4,508:
return this.comment.replace(cloneRegex$1((ContributionSurveyRow.commentMatchRegex)[this.originalStatus], { post: '$' }), '').trim();
}
return '';
}
}
ContributionSurveyRow.Parser = ContributionSurveyRowParser;
/**
* Wikitext for checking if a given row is a contribution survey row.
* $1 is the page name. $2 is the ID of the first revision. If $2 is undefined, the
* page has been cleared and commented on by a user.
*/
ContributionSurveyRow.rowWikitextRegex = /\*?(?:'''.''' )?\[\[:?(.+?)]] ?([^:]*) ?(?:: ?)?(?:(?:\[\[Special:Diff\/(\d+)\|.+?]])+|(.+$))/gm;
/**
* A set of regular expressions that will match a specific contribution survey row
Line 1,523 ⟶ 4,523:
[ContributionSurveyRowStatus.PresumptiveRemoval]: /\{\{x}}/gi
};
 
/**
* Swaps two elements in the DOM. Element 1 will be removed from the DOM, Element 2 will
* be added in its place.
*
* @param element1 The element to remove
* @param element2 The element to insert
* @return `element2`, for chaining
*/
function swapElements (element1, element2) {
try {
element1.insertAdjacentElement('afterend', element2);
element1.parentElement.removeChild(element1);
return element2;
}
catch (e) {
error(e, { element1, element2 });
// Caught for debug only. Rethrow.
throw e;
}
}
 
/**
Line 1,601 ⟶ 4,622:
function nsId(namespace) {
return mw.config.get('wgNamespaceIds')[namespace.toLowerCase().replace(/ /g, '_')];
}
 
/**
* Evaluates any string using `mw.msg`. This handles internationalization of strings
* that are loaded outside the script or asynchronously.
*
* @param string The string to evaluate
* @param {...any} parameters Parameters to pass, if any
* @return A mw.Message
*/
function msgEval(string, ...parameters) {
// Named parameters
let named = {};
if (typeof parameters[0] === 'object') {
named = parameters.shift();
}
const m = new mw.Map();
for (const [from, to] of Object.entries(named)) {
string = string.replace(new RegExp(`\\$${from}`, 'g'), to);
}
m.set('msg', string);
return new mw.Message(m, 'msg', parameters);
}
 
/**
* Mixes values together into a string for the `class` attribute.
*
* @param {...any} classes
* @return string
*/
function classMix (...classes) {
const processedClasses = [];
for (const _class of classes) {
if (Array.isArray(_class)) {
processedClasses.push(..._class);
}
else {
processedClasses.push(_class);
}
}
return processedClasses.filter((v) => v != null && !!v).join(' ');
}
 
Line 1,607 ⟶ 4,669:
* @param root0.revid
* @param root0.parentid
* @param root0.missing
* @return HTML element
*/
function ChangesListLinks({ revid: _revid, parentid: _parentid, missing }) {
const cur = getRevisionDiffURL(_revid, 'cur');
const prev = missing ?
getRevisionDiffURL(_revid, 'prev') :
getRevisionDiffURL(_parentid, _revid);
let cv;
if (window.deputy &&
window.deputy.config.cci.showCvLink &&
window.deputy.wikiConfig.cci.earwigRoot) {
cv = new URL('', window.deputy.wikiConfig.cci.earwigRoot.get());
const selfUrl = new URL(window.___location.href);
const urlSplit = selfUrl.hostname.split('.').reverse();
const proj = urlSplit[1]; // wikipedia
const lang = urlSplit[2]; // en
// Cases where the project/lang is unsupported (e.g. proj = "facebook", for example)
// should be handled by Earwig's.
cv.searchParams.set('action', 'search');
cv.searchParams.set('lang', lang);
cv.searchParams.set('project', proj);
cv.searchParams.set('oldid', `${_revid}`);
cv.searchParams.set('use_engine', '0');
cv.searchParams.set('use_links', '1');
}
return h_1("span", { class: "mw-changeslist-links" },
h_1("span", null,
h_1("a", { rel: "noopener", href: getRevisionDiffURL(revid, 0)cur, title: mw.msg('deputy.session.revision.cur.tooltip'), target: "_blank" }, mw.msg('deputy.session.revision.cur'))),
h_1("span", null, (!parentid_parentid && !missing) ?
mw.msg('deputy.session.revision.prev') :
h_1("a", { rel: "noopener", href: !parentidprev, ?title: mw.msg('deputy.session.revision.prev.tooltip'), target: "_blank" }, mw.msg('deputy.revision.prev'))),
!!window.deputy.config.cci.showCvLink &&
null :
cv &&
getRevisionDiffURL(parentid, revid), title: mw.msg('deputy.session.revision.prev.tooltip'), target: "_blank" }, mw.msg('deputy.session.revision.prev'))));
h_1("span", null,
h_1("a", { rel: "noopener", href: cv.toString(), title: mw.msg('deputy.session.revision.cv.tooltip'), target: "_blank" }, mw.msg('deputy.session.revision.cv'))));
}
/**
Line 1,623 ⟶ 4,710:
*/
function NewPageIndicator() {
return h_1("abbr", { class: "newpage", title: mw.msg('deputy.session.revision.new.tooltip') }, mw.msg('deputy.session.revision.new'));
}
/**
Line 1,632 ⟶ 4,719:
function ChangesListTime({ timestamp }) {
const time = new Date(timestamp);
const formattedTime = time.toLocaleTimeString(window.deputyLangUSER_LOCALE, {
hourCycle: 'h24',
timeStyle: mw.user.options.get('date') === 'ISO 8601' ? 'long' : 'short'
Line 1,641 ⟶ 4,728:
* @param root0
* @param root0.revision
* @param root0.link
* @return HTML element
*/
function ChangesListDate({ revision, link }) {
var _a;
// `texthidden` would be indeterminate if the `{timestamp}` type was used
if (revision.texthidden) {
// Don't give out a link if the revision was deleted
link = false;
}
const time = new Date(revision.timestamp);
let now = window.moment(time);
if (window.deputy && window.deputy.config.cci.forceUtc.get()) {
now = now.utc();
}
const formattedTime = time.toLocaleTimeString(window.deputyLangUSER_LOCALE, {
hourCycle: 'h24',
timeStyle: mw.user.options.get('date') === 'ISO 8601' ? 'long' : 'short',
timeZone: ((_a = window.deputy) === null || _a === void 0 ? void 0 : _a.config.cci.forceUtc.get()) ? 'UTC' : undefined
});
const formattedDate = now.locale(window.deputyLangUSER_LOCALE).format({
dmy: 'D MMMM YYYY',
mdy: 'MMMM D, Y',
Line 1,660 ⟶ 4,755:
}[mw.user.options.get('date')]);
const comma = mw.msg('comma-separator');
return link !== false ?
h_1("a", { class: "mw-changeslist-date", href: getRevisionURL(revision.revid, revision.page.title) },
formattedTime,
comma,
formattedDate); :
h_1("span", { class: classMix('mw-changeslist-date', revision.texthidden && 'history-deleted') },
formattedTime,
comma,
formattedDate);
}
/**
* @param root0
* @param root0.userrevision
* @return HTML element
*/
function ChangesListUser({ userrevision }) {
const { user, userhidden } = revision;
if (userhidden) {
return h_1("span", { class: "history-user" },
h_1("span", { class: "history-deleted mw-userlink" }, mw.msg('deputy.revision.removed.user')));
}
const userPage = new mw.Title(user, nsId('user'));
const userTalkPage = new mw.Title(user, nsId('user_talk'));
const userContribsPage = new mw.Title('Special:Contributions/' + user);
return h_1("span", { class: "history-user" },
h_1("a", { class: "mw-userlink", target: "_blank", rel: "noopener", href: mw.format(mw.config.get('wgArticlePath'), userPage.getPrefixedDb()), title: userPage.getPrefixedText() }, userPage.getPrefixedTextgetMainText()),
" ",
h_1("span", { class: "mw-usertoollinks mw-changeslist-links" },
h_1("span", null,
h_1("a", { class: "mw-usertoollinks-talk", target: "_blank", rel: "noopener", href: mw.format(mw.config.get('wgArticlePath'), userTalkPage.getPrefixedDb()), title: userTalkPage.getPrefixedText() }, mw.msg('deputy.session.revision.talk'))),
" ",
h_1("span", null,
h_1("a", { class: "mw-usertoollinks-contribs", target: "_blank", rel: "noopener", href: mw.format(mw.config.get('wgArticlePath'), userContribsPage.getPrefixedDb()), title: userContribsPage.getPrefixedText() }, mw.msg('deputy.session.revision.contribs')))));
}
/**
Line 1,690 ⟶ 4,795:
*/
function ChangesListBytes({ size }) {
return h_1("span", { class: "history-size mw-diff-bytes", "data-mw-bytes": size }, mw.message('deputy.session.revision.bytes', size.toString()).text());
}
/**
Line 1,702 ⟶ 4,807:
'strong' :
'span');
return h_1(DiffTag, { class: `mw-plusminus-${!diffsize === 0 ? 'null' :
(diffsize > 0 ? 'pos' : 'neg')} mw-diff-bytes`, title: mw.message('deputy.session.revision.byteChange', size.toString()).text() }, mw.message(`deputy.${diffsize <== 0null ? 'negative' : 'positive'}Diff`, diffsize.toString()).text());
mw.msg('deputy.brokenDiff.explain') :
mw.message('deputy.revision.byteChange', size.toString()).text() }, diffsize == null ?
mw.msg('deputy.brokenDiff') :
// Messages that can be used here:
// * deputy.negativeDiff
// * deputy.positiveDiff
// * deputy.zeroDiff
mw.message(`deputy.${{
'-1': 'negative',
1: 'positive',
0: 'zero'
}[Math.sign(diffsize)]}Diff`, diffsize.toString()).text());
}
/**
* @param root0
* @param root0.page
* @param root0.page.title
* @param root0.page.ns
* @return HTML element
*/
function ChangesListPage({ page }) {
const pageTitle = new mw.Title(page.title, page.ns).getPrefixedText();
return h_1("a", { class: "mw-contributions-title", href: mw.util.getUrl(pageTitle), title: pageTitle }, pageTitle);
}
/**
Line 1,712 ⟶ 4,840:
function ChangesListTags({ tags }) {
return h_1("span", { class: "mw-tag-markers" },
h_1("a", { rel: "noopener", href: mw.format(mw.config.get('wgArticlePath'), 'Special:Tags'), title: "Special:Tags", target: "_blank" }, mw.message('deputy.session.revision.tags', tags.length.toString()).text()),
tags.map((v) => {
// eslint-disable-next-line mediawiki/msg-doc
const tagMessage = mw.message(`tag-${v}`).parseparseDom();
return [
return tagMessage !== '-' && h_1("span", { class: `mw-tag-marker mw-tag-marker-${v}`, dangerouslySetInnerHTML: tagMessage });
' ',
tagMessage.text() !== '-' && unwrapJQ(h_1("span", { class: `mw-tag-marker mw-tag-marker-${v}` }), tagMessage)
];
}));
}
/**
*
* @param root0
* @param root0.revision
*/
function ChangesListMissingRow({ revision }) {
return h_1("span", null,
h_1(ChangesListLinks, { revid: revision.revid, parentid: revision.parentid, missing: true }),
' ',
h_1("i", { dangerouslySetInnerHTML: mw.message('deputy.session.revision.missing', revision.revid).parse() }));
}
/**
* @param root0
* @param root0.revision
* @param root0.format
* @return A changes list row.
*/
function ChangesListRow({ revision, format }) {
var _a, _b;
if (!format) {
format = 'history';
}
let commentElement = '';
if (revision.commenthidden) {
commentElement = h_1("span", { class: "history-deleted comment" }, mw.msg('deputy.revision.removed.comment'));
}
else if (revision.parsedcomment) {
commentElement = h_1("span", { class: "comment comment--without-parentheses",
/** Stranger danger! Yes. */
dangerouslySetInnerHTML: revision.parsedcomment });
}
else if (revision.comment) {
const comment = revision.comment
// Insert Word-Joiner to avoid parsing "templates".
.replace(/{/g, '{\u2060')
.replace(/}/g, '\u2060}');
commentElement = unwrapJQ(h_1("span", { class: "comment comment--without-parentheses" }), msgEval(comment).parseDom());
}
return h_1("span", null,
h_1(ChangesListLinks, { revid: revision.revid, parentid: revision.parentid }),
" ",
!revision.parentid && h_1(NewPageIndicator, null),
h_1(ChangesListTime, { timestamp: revision.timestamp }),
h_1(ChangesListDate, { revision: revision }),
" ",
format === 'history' && h_1(ChangesListUser, { revision: revision }),
" ",
h_1("span", { class: "mw-changeslist-separator" }),
" ",
format === 'history' && h_1(ChangesListBytes, { size: revision.size }),
" ",
h_1(ChangesListDiff, { size: revision.size, diffsize: revision.diffsize }),
" ",
h_1("span", { class: "mw-changeslist-separator" }),
" ",
format === 'contribs' && h_1(ChangesListPage, { page: revision.page }),
" ",
commentElement,
" ",
((_b = (_a = revision.tags) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : -1) > 0 &&
h_1(ChangesListTags, { tags: revision.tags }));
}
 
/**
* Get the API error text from an API response.
*
* @param errorData
* @param n Get the `n`th error. Defaults to 0 (first error).
*/
function getApiErrorText(errorData, n = 0) {
var _a, _b, _c, _d, _e, _f, _g;
// errorformat=html
return ((_b = (_a = errorData.errors) === null || _a === void 0 ? void 0 : _a[n]) === null || _b === void 0 ? void 0 : _b.html) ?
h_1("span", { dangerouslySetInnerHTML: (_d = (_c = errorData.errors) === null || _c === void 0 ? void 0 : _c[n]) === null || _d === void 0 ? void 0 : _d.html }) :
(
// errorformat=plaintext/wikitext
(_g = (_f = (_e = errorData.errors) === null || _e === void 0 ? void 0 : _e[n]) === null || _f === void 0 ? void 0 : _f.text) !== null && _g !== void 0 ? _g :
// errorformat=bc
errorData.info);
}
 
Line 1,723 ⟶ 4,934:
* A specific revision for a section row.
*/
class DeputyContributionSurveyRevision extends OO.EventEmitterEventTarget {
/**
* @param revision
* @param row
*/
constructor(revision, row) {
super();
this.revisionStatusUpdateListener = this.onRevisionStatusUpdate.bind(this);
this.revision = revision;
this.uiRow = row;
if (this.statusAutosaveFunction == null) {
this.statusAutosaveFunction = mw.util.throttle(() => __awaiter(this, void 0, void 0, function* () {
yield this.saveStatus();
}), 500);
}
}
/**
* @return `true` the current revision has been checked by the user or `false` if not.
Line 1,760 ⟶ 4,956:
get autosaveHash() {
return `CASE--${this.uiRow.row.casePage.title.getPrefixedDb()}+PAGE--${this.uiRow.row.title.getPrefixedDb()}+REVISION--${this.revision.revid}`;
}
/**
* @param revision
* @param row
* @param options
* @param options.expanded
*/
constructor(revision, row, options = {}) {
var _a;
super();
this.revisionStatusUpdateListener = this.onRevisionStatusUpdate.bind(this);
/**
* The diff view of the given revision. May also be "loading" text, or
* null if the diff view has not yet been set.
*
* @private
*/
this.diff = null;
this.revision = revision;
this.uiRow = row;
this.autoExpanded = (_a = options.expanded) !== null && _a !== void 0 ? _a : false;
if (this.statusAutosaveFunction == null) {
// TODO: types-mediawiki limitation
this.statusAutosaveFunction = mw.util.throttle(() => __awaiter(this, void 0, void 0, function* () {
yield this.saveStatus();
}), 500);
}
}
/**
Line 1,814 ⟶ 5,037:
return __awaiter(this, void 0, void 0, function* () {
this.completedCheckbox = new OO.ui.CheckboxInputWidget({
labeltitle: mw.msg('deputy.session.revision.assessed'),
selected: yield this.getSavedStatus(),
classes: ['dp-cs-rev-checkbox']
});
this.completedCheckbox.on('change', (checked) => {
var _a, _b, _c;
this.emitdispatchEvent(new CustomEvent('update', checked, this.revision);{
detail: {
checked: checked,
revision: this.revision
}
}));
window.deputy.comms.send({
type: 'revisionStatusUpdate',
Line 1,826 ⟶ 5,055:
revision: this.revision.revid,
status: checked,
nextRevision: (_c = (_b = (_a = this.uiRow.revisions) === null || _a === void 0 ? void 0 : _a.find((revision) => !revision.completed &&
revision.revision.revid !== this.revision.revid)) === null || _b === void 0 ? void 0 : _b.revision.revid) !== null && _c !== void 0 ? _c : null
});
this.statusAutosaveFunction();
});
this.diffToggle = new OO.ui.ToggleButtonWidget({
label: mw.msg('deputy.session.revision.diff.toggle'),
invisibleLabel: true,
indicator: 'down',
framed: false,
classes: ['dp-cs-rev-toggleDiff'],
value: this.autoExpanded
});
this.diff = h_1("div", { class: "dp-cs-rev-diff" });
let loaded = false;
const handleDiffToggle = (active) => {
this.diffToggle.setIndicator(active ? 'up' : 'down');
if (!active) {
this.diff.classList.toggle('dp-cs-rev-diff--hidden', true);
return;
}
if (this.diff.classList.contains('dp-cs-rev-diff--errored')) {
// Error occurred previously, remake diff panel
this.diff = swapElements(this.diff, h_1("div", { class: "dp-cs-rev-diff" }));
}
else if (loaded) {
this.diff.classList.toggle('dp-cs-rev-diff--hidden', false);
}
if (active && !loaded) {
// Going active, clear the element out
Array.from(this.diff.children).forEach((child) => this.diff.removeChild(child));
this.diff.setAttribute('class', 'dp-cs-rev-diff');
this.diff.appendChild(h_1(DeputyLoadingDots, null));
const comparePromise = MwApi.action.get({
action: 'compare',
fromrev: this.revision.revid,
torelative: 'prev',
prop: 'diff'
});
const stylePromise = mw.loader.using('mediawiki.diff.styles');
// Promise.all not used here since we need to use JQuery.Promise#then
// if we want to access the underlying error response.
$.when([comparePromise, stylePromise])
.then((results) => results[0])
.then((data) => {
unwrapWidget(this.diffToggle).classList.add('dp-cs-rev-toggleDiff--loaded');
// Clear element out again
Array.from(this.diff.children).forEach((child) => this.diff.removeChild(child));
// https://youtrack.jetbrains.com/issue/WEB-61047
// noinspection JSXDomNesting
const diffTable = h_1("table", { class: classMix('diff', `diff-editfont-${mw.user.options.get('editfont')}`) },
h_1("colgroup", null,
h_1("col", { class: "diff-marker" }),
h_1("col", { class: "diff-content" }),
h_1("col", { class: "diff-marker" }),
h_1("col", { class: "diff-content" })));
// Trusted .innerHTML (data always comes from MediaWiki Action API)
diffTable.innerHTML += data.compare.body;
diffTable.querySelectorAll('tr').forEach((tr) => {
// Delete all header rows
if (tr.querySelector('.diff-lineno')) {
removeElement(tr);
return;
}
// Delete all no-change rows (gray rows)
if (tr.querySelector('td.diff-context')) {
removeElement(tr);
}
});
this.diff.classList.toggle('dp-cs-rev-diff--loaded', true);
this.diff.classList.toggle('dp-cs-rev-diff--errored', false);
this.diff.appendChild(diffTable);
loaded = true;
}, (_error, errorData) => {
// Clear element out again
Array.from(this.diff.children).map((child) => this.diff.removeChild(child));
this.diff.classList.toggle('dp-cs-rev-diff--loaded', true);
this.diff.classList.toggle('dp-cs-rev-diff--errored', true);
this.diff.appendChild(unwrapWidget(DeputyMessageWidget({
type: 'error',
message: mw.msg('deputy.session.revision.diff.error', errorData ?
getApiErrorText(errorData) :
_error.message)
})));
});
}
};
this.diffToggle.on('change', (checked) => {
handleDiffToggle(checked);
});
if (this.autoExpanded) {
handleDiffToggle(true);
}
});
}
Line 1,836 ⟶ 5,154:
*/
render() {
var _a, _b, _c;
const commentElement = h_1("span", { class: "comment comment--without-parentheses",
/** Stranger danger! Yes. */
dangerouslySetInnerHTML: this.revision.parsedcomment });
window.deputy.comms.addEventListener('revisionStatusUpdate', this.revisionStatusUpdateListener);
// Be wary of the spaces between tags.
return this.element = h_1("div", { class: ((_a = this.revision.tags) !== null && _a !== void 0 ? _a : []).map((v) => 'mw-tag-' + v).join(' ') },
.replace(/[^A-Z0-9-]/gi, '')
.replace(/\s/g, '_')).join(' ') },
unwrapWidget(this.completedCheckbox),
h_1unwrapWidget(ChangesListLinks, { revid: this.revision.revid, parentid: this.revision.parentid }diffToggle),
"this.revision.missing ",?
!this.revision.parentid && h_1(NewPageIndicatorChangesListMissingRow, null{ revision: this.revision }), :
h_1(ChangesListTimeChangesListRow, { timestamprevision: this.revision.timestamp }),
h_1(ChangesListDate, { revision: this.revision }diff),;
" ",
h_1(ChangesListUser, { user: this.revision.user }),
" ",
h_1("span", { class: "mw-changeslist-separator" }),
" ",
h_1(ChangesListBytes, { size: this.revision.size }),
" ",
h_1(ChangesListDiff, { size: this.revision.size, diffsize: this.revision.diffsize }),
" ",
h_1("span", { class: "mw-changeslist-separator" }),
" ",
commentElement,
" ",
((_c = (_b = this.revision.tags) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : -1) > 0 &&
h_1(ChangesListTags, { tags: this.revision.tags }));
}
/**
Line 1,957 ⟶ 5,259:
const parser = window.deputy.session.rootSession.parser;
// Use DiscussionTools to identify the user and timestamp.
constlet parsedComment;
try {
parsedComment = (_b = (_a = parser.parse(props.originalElement)) === null || _a === void 0 ? void 0 : _a.commentItems) === null || _b === void 0 ? void 0 : _b[0];
}
catch (e) {
warn('Failed to parse user signature.', e);
}
if (!parsedComment) {
// See if the Deputy trace exists.
Line 1,987 ⟶ 5,295:
h_1("span", { class: "mw-usertoollinks mw-changeslist-links" },
h_1("span", null,
h_1("a", { class: "mw-usertoollinks-talk", target: "_blank", rel: "noopener", href: mw.util.getUrl(talkPage.getPrefixedDb()), title: talkPage.getPrefixedText() }, mw.msg('deputy.session.revision.talk'))),
h_1("span", null,
h_1("a", { class: "mw-usertoollinks-contribs", target: "_blank", rel: "noopener", href: mw.util.getUrl(contribsPage.getPrefixedDb()), title: contribsPage.getPrefixedText() }, mw.msg('deputy.session.revision.contribs')))))).outerHTML
];
if (this.timestamp) {
Line 1,995 ⟶ 5,303:
.toLocaleString('en-US', { dateStyle: 'long', timeStyle: 'short' }), this.timestamp.toNow(true));
}
return unwrapJQ(h_1("i", { dangerouslySetInnerHTML:null), mw.message(this.timestamp ?
'deputy.session.row.checkedComplete' :
'deputy.session.row.checked', ...params).textparseDom() });
}
else {
Line 2,006 ⟶ 5,314:
 
/**
* Mixes values together into a string for the `class` attribute.
*
* @param {...any} classes
* @return string
*/
class DeputyCCIStatusDropdown extends EventTarget {
function classMix (...classes) {
const processedClasses = [];/**
* @return The currently-selected status of this dropdown.
for (const _class of classes) {
if (Array.isArray(_class)) {*/
get processedClasses.pushstatus(..._class); {
}var _a, _b;
return (_b = (_a = this.dropdown.getMenu().findSelectedItem()) === null || _a === void 0 ? void 0 : _a.getData()) !== null && _b !== void 0 ? _b : null;
else {
}
processedClasses.push(_class);
}/**
* Sets the currently-selected status of this dropdown.
*/
set status(status) {
this.dropdown.getMenu().selectItemByData(status);
this.setOptionDisabled(ContributionSurveyRowStatus.Unknown, status !== ContributionSurveyRowStatus.Unknown, false);
this.refresh();
}
return processedClasses.filter((v) => v != null && v != false).join(' ');
}
 
/**
*
*/
class DeputyCCIStatusDropdown extends EventTarget {
/**
* Create a new DeputyCCIStatusDropdown object.
Line 2,039 ⟶ 5,343:
* @param row.title The title of the row (page) that this dropdown accesses
* @param options Additional construction options, usually used by the root session.
* @param row.type
*/
constructor(row, options = {}) {
Line 2,152 ⟶ 5,457:
unwrapWidget(this.dropdown.getMenu()).style.width = '20em';
});
}
/**
* @return The currently-selected status of this dropdown.
*/
get status() {
var _a, _b;
return (_b = (_a = this.dropdown.getMenu().findSelectedItem()) === null || _a === void 0 ? void 0 : _a.getData()) !== null && _b !== void 0 ? _b : null;
}
/**
* Sets the currently-selected status of this dropdown.
*/
set status(status) {
this.dropdown.getMenu().selectItemByData(status);
this.setOptionDisabled(ContributionSurveyRowStatus.Unknown, status !== ContributionSurveyRowStatus.Unknown, false);
this.refresh();
}
/**
Line 2,186 ⟶ 5,476:
refresh() {
const icon = DeputyCCIStatusDropdown.menuOptionIcons[this.status];
this.dropdown.setIcon(icon === false ? null : icon);
}
/**
Line 2,264 ⟶ 5,554:
*
* For
* - Unfinished: WithoutViolations, unless it's `pageonly`, on which it'll be kept as is.
* - Unknown: Unfinished
* - WithViolations: _usually not disabled, kept as is_
Line 2,275 ⟶ 5,565:
selectNextBestValue(status) {
if (status === ContributionSurveyRowStatus.Unfinished) {
if (this.row.type === 'pageonly') {
// Leave it alone.
return;
}
this.status = ContributionSurveyRowStatus.WithoutViolations;
}
Line 2,283 ⟶ 5,577:
}
DeputyCCIStatusDropdown.menuOptionIcons = {
[ContributionSurveyRowStatus.Unfinished]: falsenull,
[ContributionSurveyRowStatus.Unknown]: 'alert',
[ContributionSurveyRowStatus.WithViolations]: 'check',
Line 2,290 ⟶ 5,584:
[ContributionSurveyRowStatus.PresumptiveRemoval]: 'trash'
};
 
/**
* Shows a confirmation dialog, if the user does not have danger mode enabled.
* If the user has danger mode enabled, this immediately resolves to true, letting
* the action run immediately.
*
* Do not use this with any action that can potentially break templates, user data,
* or cause irreversible data loss.
*
* @param config The user's configuration
* @param message See {@link OO.ui.MessageDialog}'s parameters.
* @param options See {@link OO.ui.MessageDialog}'s parameters.
* @return Promise resolving to a true/false boolean.
*/
function dangerModeConfirm(config, message, options) {
if (config.all.core.dangerMode.get()) {
return $.Deferred().resolve(true);
}
else {
return OO.ui.confirm(message, options);
}
}
 
var DeputyContributionSurveyRowState;
Line 2,318 ⟶ 5,634:
* (g) closing comments
*/
class DeputyContributionSurveyRow extends EventTarget {
/**
* Creates a new DeputyContributionSurveyRow object.
*
* @param row The contribution survey row data
* @param originalElement
* @param originalWikitext
* @param section The section that this row belongs to
*/
constructor(row, originalElement, originalWikitext, section) {
/**
* The state of this element.
*/
this.state = DeputyContributionSurveyRowState.Loading;
/**
* Responder for session requests.
*/
this.statusRequestResponder = this.sendStatusResponse.bind(this);
this.nextRevisionRequestResponder = this.sendNextRevisionResponse.bind(this);
this.row = row;
this.originalElement = originalElement;
this.originalWikitext = originalWikitext;
this.section = section;
}
/**
* @return `true` if:
Line 2,414 ⟶ 5,707:
*/
get wikitext() {
var _a, _b, _c, _d;
// Broken, loading, or closed. Just return the original wikitext.
if (this.state !== DeputyContributionSurveyRowState.Ready) {
Line 2,420 ⟶ 5,713:
}
if (this.wasFinished == null) {
console.warn('Could not determine if this is an originally-finished or ' +
'originally-unfinished row. Assuming unfinished and moving on...');
}
const finished = (_a = this.wasFinished) !== null && _a !== void 0 ? _a : false;
const wikitext = this.row.wikitext;
// "* "
let result = /\*\s*/gthis.exec(wikitext)[0]row.data.bullet;
if (/'''N'''/this.test(wikitext)row.data.creation) {
// '''N'''
result += "'''N''' ";
}
// [[:Example]]
result += `[[:${this.row.titledata.getPrefixedText()page}]]`;
// "{bullet}{creation}[[{page}]]{extras}{diffs}{comments}"
if (this.row.extras) {
result += ` ${this.row.extras}`;
}
const unfinishedDiffs = (_c = (_b = (_a = this.revisions) === null || _a === void 0 ? void 0 : _a.filter((v) => !v.completed)) === null || _b === void 0 ? void 0 : _b.sort((a, b) => ContributionSurveyRow.getSorterFunction(this.sortOrder)(a.revision, b.revision))) !== null && _c !== void 0 ? _c : [];
result += ': ';
let diffsText = '';
const unfinishedDiffs = (_c = (_b = this.revisions) === null || _b === void 0 ? void 0 : _b.filter((v) => !v.completed)) !== null && _c !== void 0 ? _c : [];
if (unfinishedDiffs.length > 0) {
resultdiffsText += unfinishedDiffs.map((v) => {
return `[[Special:Diff/${vmw.revisionformat(this.revid}|row.data.diffTemplate, String(${v.revision.diffsize > 0 ? '+' +revid), v.revision.diffsize :== v.revision.diffsize})]]`;null ?
// For whatever reason, diffsize is missing. Fall back to the text we had
// previously.
v.uiRow.row.data.revidText[v.revision.revid] :
String(v.revision.diffsize > 0 ?
'+' + v.revision.diffsize : v.revision.diffsize));
}).join('');
result += mw.format(this.row.data.diffsTemplate, diffsText);
if (this.row.data.comments) {
// Comments existed despite not being finished yet. Allow anyway.
result += this.row.data.comments;
}
}
else {
Line 2,486 ⟶ 5,787:
result += ' ~~~~';
};
if (finishedthis.statusModified) {
if// (thisModified.statusModified) {Use user data.
// Modified. Use user data.
useUserData();
}
else {
// No changes. Just append original closure comments.
result += this.row.comment;
}
}
else {
useUserData();
}
else if ((_d = this.wasFinished) !== null && _d !== void 0 ? _d : false) {
// No changes. Just append original closure comments.
result += this.row.comment;
}
// Otherwise, leave this row unchanged.
}
return result;
}
/**
* @return The hash used for autosave keys
*/
get autosaveHash() {
return `CASE--${this.row.casePage.title.getPrefixedDb()}+H--${this.section.headingName}-${this.section.headingN}+PAGE--${this.row.title.getPrefixedDb()}`;
}
/**
* Creates a new DeputyContributionSurveyRow object.
*
* @param row The contribution survey row data
* @param originalElement
* @param originalWikitext
* @param section The section that this row belongs to
*/
constructor(row, originalElement, originalWikitext, section) {
super();
/**
* The state of this element.
*/
this.state = DeputyContributionSurveyRowState.Loading;
/**
* Responder for session requests.
*/
this.statusRequestResponder = this.sendStatusResponse.bind(this);
this.nextRevisionRequestResponder = this.sendNextRevisionResponse.bind(this);
this.row = row;
this.originalElement = originalElement;
this.additionalComments = this.extractAdditionalComments();
this.originalWikitext = originalWikitext;
this.section = section;
}
/**
* Extracts HTML elements which may be additional comments left by others.
* The general qualification for this is that it has to be a list block
* element that comes after the main line (in this case, it's detected after
* the last .
* This appears in the following form in wikitext:
*
* ```
* * [[Page]] (...) [[Special:Diff/...|...]]
* *: Hello! <-- definition list block
* ** What!? <-- sub ul
* *# Yes. <-- sub ol
* * [[Page]] (...) [[Special:Diff/...|...]]<div>...</div> <-- inline div
* ```
*
* Everything else (`*<div>...`, `*'''...`, `*<span>`, etc.) is considered
* not to be an additional comment.
*
* If no elements were found, this returns an empty array.
*
* @return An array of HTMLElements
*/
extractAdditionalComments() {
// COMPAT: Specific to MER-C contribution surveyor
// Initialize to first successive diff link.
let lastSuccessiveDiffLink = this.originalElement.querySelector('a[href^="/wiki/Special:Diff/"]');
const elements = [];
if (!lastSuccessiveDiffLink) {
// No diff links. Get last element, check if block element, and crawl backwards.
let nextDiscussionElement = this.originalElement.lastElementChild;
while (nextDiscussionElement &&
window.getComputedStyle(nextDiscussionElement, '').display === 'block') {
elements.push(nextDiscussionElement);
nextDiscussionElement = nextDiscussionElement.previousElementSibling;
}
}
else {
while (lastSuccessiveDiffLink.nextElementSibling &&
lastSuccessiveDiffLink.nextElementSibling.tagName === 'A' &&
lastSuccessiveDiffLink
.nextElementSibling
.getAttribute('href')
.startsWith('/wiki/Special:Diff')) {
lastSuccessiveDiffLink = lastSuccessiveDiffLink.nextElementSibling;
}
// The first block element after `lastSuccessiveDiffLink` is likely discussion,
// and everything after it is likely part of such discussion.
let pushing = false;
let nextDiscussionElement = lastSuccessiveDiffLink.nextElementSibling;
while (nextDiscussionElement != null) {
if (!pushing &&
window.getComputedStyle(nextDiscussionElement).display === 'block') {
pushing = true;
elements.push(nextDiscussionElement);
}
else if (pushing) {
elements.push(nextDiscussionElement);
}
nextDiscussionElement = nextDiscussionElement.nextElementSibling;
}
}
return elements;
}
/**
Line 2,509 ⟶ 5,900:
try {
const diffs = yield this.row.getDiffs();
this.sortOrder = ContributionSurveyRow.guessSortOrder(diffs.values());
this.wasFinished = this.row.completed;
if (this.row.completed) {
Line 2,528 ⟶ 5,920:
}
catch (e) {
console.error('Caught exception while loading data', e);
this.state = DeputyContributionSurveyRowState.Broken;
this.renderRow(null, unwrapWidget(new OO.ui.MessageWidget({
Line 2,537 ⟶ 5,929:
}
});
}
/**
* @return The hash used for autosave keys
*/
get autosaveHash() {
return `CASE--${this.row.casePage.title.getPrefixedDb()}+PAGE--${this.row.title.getPrefixedDb()}`;
}
/**
Line 2,549 ⟶ 5,935:
onUpdate() {
if (this.statusAutosaveFunction == null) {
// TODO: types-mediawiki limitation
this.statusAutosaveFunction = mw.util.throttle(() => __awaiter(this, void 0, void 0, function* () {
yield this.saveStatus();
Line 2,554 ⟶ 5,941:
}
if (this.revisions && this.statusDropdown) {
if (this.row.type !== 'pageonly') {
this.statusDropdown.setOptionDisabled(ContributionSurveyRowStatus.Unfinished, this.completed, true);
// Only disable this option if the row isn't already finished.
this.statusDropdown.setOptionDisabled(ContributionSurveyRowStatus.Unfinished, this.completed, true);
}
const unfinishedWithStatus = this.statusModified && !this.completed;
if (this.unfinishedMessageBox) {
this.unfinishedMessageBox.toggle(unfinishedWithStatus);
// If using danger mode, this should always be enabled.
!window.deputy.config.core.dangerMode.get() &&
unfinishedWithStatus);
}
this.statusAutosaveFunction();
Line 2,571 ⟶ 5,964:
this.commentsField.setNotices([]);
}
// Emit "update" event
this.dispatchEvent(new CustomEvent('update'));
}
/**
Line 2,577 ⟶ 5,972:
*/
getSavedStatus() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
return (_a = yield window.deputy.storage.db.get('pageStatus', this.autosaveHash);) !== null && _a !== void 0 ? _a :
// Old hash (< v0.9.0)
yield window.deputy.storage.db.get('pageStatus', `CASE--${this.row.casePage.title.getPrefixedDb()}+PAGE--${this.row.title.getPrefixedDb()}`);
});
}
Line 2,594 ⟶ 5,992:
}
});
}
/**
* Mark all revisions of this section as finished.
*/
markAllAsFinished() {
if (!this.revisions) {
// If `renderUnfinished` was never called, this will be undefined.
// We want to skip over instead.
return;
}
this.revisions.forEach((revision) => {
revision.completed = true;
});
this.onUpdate();
}
/**
Line 2,605 ⟶ 6,017:
classes: ['dp-cs-row-closeComments'],
placeholder: mw.msg('deputy.session.row.closeComments'),
value: value !== null && value !== void 0 ? value : '',
autosize: true,
rows: 1
Line 2,647 ⟶ 6,059:
this.unfinishedMessageBox = new OO.ui.MessageWidget({
classes: ['dp-cs-row-unfinishedWarning'],
type: 'warnwarning',
label: mw.msg('deputy.session.row.unfinishedWarning')
});
this.unfinishedMessageBox.toggle(false);
revisionList.appendChild(unwrapWidget(this.unfinishedMessageBox));
revisionList.appendChild(unwrapWidget(this.renderCommentsTextInput(this.row.comment)));
forif (constthis.row.type revision=== of diffs.values()'pageonly') {
revisionList.appendChild(h_1("div", { class: "dp-cs-row-pageonly" },
const revisionUIEl = new DeputyContributionSurveyRevision(revision, this);
revisionUIEl.on h_1('update'"i", null, mw.msg('deputy.session.row.pageonly') => {)));
}
// Recheck options first to avoid "Unfinished" being selected when done.
else this.onUpdate();{
})const cciConfig = window.deputy.config.cci;
yieldconst revisionUIElmaxSize = cciConfig.maxSizeToAutoShowDiff.prepareget();
revisionList.appendChildfor (revisionUIElconst revision of diffs.rendervalues()); {
this.revisions.push( const revisionUIEl); = new DeputyContributionSurveyRevision(revision, this, {
expanded: cciConfig.autoShowDiff.get() &&
diffs.size < cciConfig.maxRevisionsToAutoShowDiff.get() &&
(maxSize === -1 || Math.abs(revision.diffsize) < maxSize)
});
revisionUIEl.addEventListener('update', () => {
// Recheck options first to avoid "Unfinished" being selected when done.
this.onUpdate();
});
yield revisionUIEl.prepare();
revisionList.appendChild(revisionUIEl.render());
this.revisions.push(revisionUIEl);
}
}
return revisionList;
Line 2,704 ⟶ 6,128:
renderDetails(diffs) {
const parts = [];
// Timestamp is always found in a non-missing diff, suppressed or not.
if (diffs.size > 0) {
const validDiffs = Array.from(diffs.values()).filter((v) => v.timestamp);
if (validDiffs.length > 0) {
const diffArray = Array.from(diffs.values());
if (diffArray.some((v) => !v.parentid)) {
Line 2,710 ⟶ 6,136:
}
// Number of edits
parts.push(mw.message('deputy.session.row.details.edits', diffs.size.toString()).text());
{
parts.push(mw.message('deputy.session.row.details.edits', diffs.size.toString()).text());
}
// Identify largest diff
const largestDiff = diffs.get(Array.from(diffs.values())
.sort(ContributionSurveyRow.getSorterFunction(a, b) => bContributionSurveyRowSort.diffsize - a.diffsizeBytes))[0]
.revid);
parts.push(
// eslint-disable-next-lineMessages mediawiki/msg-docthat can be used here:
// * deputy.negativeDiff
mw.message(`deputy.${largestDiff.diffsize < 0 ? 'negative' : 'positive'}Diff`, largestDiff.diffsize.toString()).text());
// * deputy.positiveDiff
// * deputy.zeroDiff
mw.message(`deputy.${{
'-1': 'negative',
1: 'positive',
0: 'zero'
}[Math.sign(largestDiff.diffsize)]}Diff`, largestDiff.diffsize.toString()).text());
}
const spliced = [];
Line 2,747 ⟶ 6,178:
requireAcknowledge: false
});
if ((diffs && diffsthis.sizerow.type =!== 0)'pageonly' || this.wasFinished) {&&
((diffs && diffs.size === 0) || this.wasFinished)) {
// If there are no diffs found or `this.wasFinished` is set (both meaning there are
// no diffs and this is an already-assessed row), then the "Unfinished" option will
// be disabled. This does not apply for page-only rows, which never have diffs.
this.statusDropdown.setOptionDisabled(ContributionSurveyRowStatus.Unfinished, true);
}
Line 2,766 ⟶ 6,198:
});
this.checkAllButton.on('click', () => {
OOdangerModeConfirm(window.uideputy.confirm(config, mw.msg('deputy.session.row.checkAll.confirm')).done((confirmed) => {
if (confirmed) {
this.revisions.forEachmarkAllAsFinished((revision) => {;
revision.completed = true;
});
this.onUpdate();
}
});
Line 2,783 ⟶ 6,212:
framed: false
});
let contentToggled = !window.deputy.prefsconfig.get('cci.contentDefault'autoCollapseRows.get();
/**
* Toggles the content.
Line 2,798 ⟶ 6,227:
'deputy.session.row.content.open').text());
contentContainer.style.display = show ? 'block' : 'none';
contentToggled = !contentToggledshow;
};
toggleContent(contentToggled);
Line 2,814 ⟶ 6,243:
}
/**
* Renders additional comments that became part of this row.
*
* @return An HTML element.
*/
renderAdditionalComments() {
const additionalComments = h_1("div", { class: "dp-cs-row-comments" },
h_1("b", null, mw.msg('deputy.session.row.additionalComments')),
h_1("hr", null),
h_1("div", { class: "dp-cs-row-comments-content", dangerouslySetInnerHTML: this.additionalComments.map(e => e.innerHTML).join('') }));
// Open all links in new tabs.
additionalComments.querySelectorAll('.dp-cs-row-comments-content a')
.forEach(a => a.setAttribute('target', '_blank'));
return additionalComments;
}
/**
* @param diffs
* @param content
*/
renderRow(diffs, content) {
var _a;
const contentContainer = h_1("div", { class: classMix([
'dp-cs-row-content',
Line 2,825 ⟶ 6,269:
this.element = swapElements(this.element, h_1("div", null,
this.renderHead(diffs, contentContainer),
((_a = this.additionalComments) === null || _a === void 0 ? void 0 : _a.length) > 0 && this.renderAdditionalComments(),
contentContainer));
}
Line 2,833 ⟶ 6,278:
this.element = h_1(DeputyLoadingDots, null);
this.rootElement = h_1("div", { class: "dp-cs-row" }, this.element);
this.loadData();
return this.rootElement;
}
Line 2,868 ⟶ 6,312:
sendStatusResponse(event) {
var _a, _b, _c, _d;
const rev = (_a = this.revisions) === null || _a === void 0 ? void 0 : _a.find((r) => r.revision.revid === event.data.revision);
// Handles the cases:
// * Page title and revision ID (if supplied) match
// * Page title matches
// * Page revision ID (if supplied) matches
if (event.data.page === this.row.title.getPrefixedText() ||
this.revisions.some((r) => r.revision.revidrev ===&& event.data.revision)) {
window.deputy.comms.reply(event.data, {
type: 'pageStatusResponse',
Line 2,877 ⟶ 6,326:
status: this.status,
enabledStatuses: this.statusDropdown.getEnabledOptions(),
rowType: this.row.type,
revisionStatus: event.data.revision ? (_a = this.revisions.find((r) => r.revision.revid === event.data.revision)) === null || _a === void 0 ? void 0 : _a.completed : undefined,
revisionStatus: rev ? rev.completed : undefined,
nextRevision: (_d = (_c = (_b = this.revisions) === null || _b === void 0 ? void 0 : _b.find((revision) => !revision.completed)) === null || _c === void 0 ? void 0 : _c.revision.revid) !== null && _d !== void 0 ? _d : null
revision: event.data.revision,
nextRevision: (_d = (_c = (_b = this.revisions) === null || _b === void 0 ? void 0 : _b.find((revision) => !revision.completed &&
revision.revision.revid !== event.data.revision)) === null || _c === void 0 ? void 0 : _c.revision.revid) !== null && _d !== void 0 ? _d : null
});
}
}
/**
*
* @param event
*/
Line 2,902 ⟶ 6,353:
const baseRevisionIndex = baseRevision == null ?
0 : this.revisions.indexOf(baseRevision);
const// exactRevisionFind =the this.revisions.find((r,next i)revision =>that iis >not baseRevisionIndex && !rcompleted.completed);
const exactRevision = event.data.reverse ?
last(this.revisions.filter((r, i) => i < baseRevisionIndex && !r.completed)) :
this.revisions.find((r, i) => i > baseRevisionIndex && !r.completed);
const firstRevision = exactRevision == null ?
this.revisions.find((r) => !r.completed) : null;
Line 2,934 ⟶ 6,388:
* @param closingComments Closing comments for this section
* @param wikitext The original wikitext of this section
* @param revid The revision ID of the wikitext attached to this section.
*/
constructor(casePage, name, closed, closingComments, wikitext, revid) {
this.casePage = casePage;
this.name = name;
Line 2,942 ⟶ 6,397:
this.originalWikitext = wikitext;
this.originallyClosed = closed;
this.revid = revid;
}
}
Line 2,987 ⟶ 6,443:
unwrapWidget(this.content).appendChild(this.element);
this.$body.append(this.content.$element);
return this;
}
/**
Line 3,017 ⟶ 6,474:
});
if (compareRequest.error) {
swapElements(this.element, unwrapWidget(new OO.ui.MessageWidget({
type: 'error',
label: mw.msg('deputy.diff.error')
})));
}
const diffHTML = compareRequest.compare.bodies.main;
Line 3,027 ⟶ 6,484:
}
else {
// noinspection JSXDomNesting
this.element = swapElements(this.element, h_1("table", { class: "diff" },
h_1("colgroup", null,
Line 3,053 ⟶ 6,511:
}
},
_a.static = Object.assign(Object.assign({}, OO.ui.ProcessDialog.static), { name: 'deputyReviewDialog', title: mw.msg('deputy.diff'), actions: [
_a.static = {
name: 'deputyReviewDialog',
title: mw.msg('deputy.diff'),
actions: [
{
flags: ['safe', 'close'],
Line 3,065 ⟶ 6,520:
action: 'close'
}
] }),
},
_a);
}
Line 3,087 ⟶ 6,541:
* @param page The page to check for
* @param sectionName The section name to get the ID of
* @param n The `n`th occurrence of a section with the same name
*/
function getSectionId (page, sectionName, n = 1) {
return __awaiter(this, void 0, void 0, function* () {
const parseRequest = yield MwApi.action.get({
Line 3,098 ⟶ 6,553:
throw new Error('Error finding section ID: ' + parseRequest.error.info);
}
constlet indexSection = parseRequest.parse.sections;
let currentN .find((section) => section.line === sectionName)1;
for (const section of parseRequest.parse.sections) {
if (section.line === sectionName) {
if (currentN < n) {
currentN++;
}
else {
indexSection = section;
break;
}
}
}
if (indexSection) {
return isNaN(+indexSection.index) ? null : +indexSection.index;
Line 3,122 ⟶ 6,588:
section = yield getSectionId(page, section);
}
return MwApi.action.get(Object.assign({ action: 'parse', prop: 'text|wikitext|revid', page: normalizeTitle(page).getPrefixedText(), section: section, disablelimitreport: true }, extraOptions)).then((data) => {
const temp = document.createElement('span');
temp.innerHTML = data.parse.text;
return {
element: temp.children[0],
wikitext: data.parse.wikitext,
revid: data.parse.revid
};
});
Line 3,134 ⟶ 6,601:
 
/**
* Appends extra information to an edit summary (also known as the "advert").
* Deputy's current version, exported as a string.
*
* @param editSummary The edit summary
* Why is this in its own file? Multiple modules can be run standalone (without
* @param config The user's configuration. Used to get the "danger mode" setting.
* Deputy), but they are still part of Deputy and hence use the same
* @return The decorated edit summary (in wikitext)
* `decorateEditSummary` function. However, Deputy (core) may not be available
*/
* at the moment, leading to a reference error if `window.deputy.version` were
function decorateEditSummary (editSummary, config) {
* to be used.
var _a;
const dangerMode = (_a = config === null || config === void 0 ? void 0 : config.core.dangerMode.get()) !== null && _a !== void 0 ? _a : false;
return `${editSummary} ([[Wikipedia:Deputy|Deputy]] v${version}${dangerMode ? '!' : ''})`;
}
 
/**
* Checks the n of a given element, that is to say the `n`th occurrence of a section
* with this exact heading name in the entire page.
*
* This ensuresis thatpurely thestring- versionand is availableelement-based, evenwith ifno theadditional core ismetadata notor loaded.parsing
* information required.
* It also keeps standalone versions lightweight to avoid too much additional code.
*
* This function detects the `n` using the following conditions:
* This file is automatically modified by npm when running `npm version ...`. Avoid
* - If the heading ID does not have an n suffix, the n is always 1.
* modifying it manually.
* - If the heading ID does have an n suffix, and the detected heading name does not end
* with a number, the n is always the last number on the ID.
* - If the heading ID and heading name both end with a number,
* - The n is 1 if the ID has an equal number of ending number patterns (sequences of "_n",
* e.g. "_20_30_40" has three) with the heading name.
* - Otherwise, the n is the last number on the ID if the ID than the heading name.
*
* @param heading The heading to check
* @return The n, a number
*/
function sectionHeadingN(heading) {
var deputyVersion = /* v */ '0.1.1' /* v */;
try {
const headingNameEndPattern = /(?:\s|_)(\d+)/g;
const headingIdEndPattern = /_(\d+)/g;
const headingId = heading.id;
const headingIdMatches = headingId.match(headingIdEndPattern);
const headingNameMatches = heading.title.match(headingNameEndPattern);
if (headingIdMatches == null) {
return 1;
}
else if (headingNameMatches == null) {
// Last number of the ID
return +(headingIdEndPattern.exec(last(headingIdMatches))[1]);
}
else if (headingIdMatches.length === headingNameMatches.length) {
return 1;
}
else {
// Last number of the ID
return +(headingIdEndPattern.exec(last(headingIdMatches))[1]);
}
}
catch (e) {
error('Error getting section number', e, heading);
throw e;
}
}
 
/**
* Wraps a set of nodes in a div.dp-cs-extraneous element.
* Appends extra information to an edit summary (also known as the "advert").
*
* @param editSummarychildren The editnodes summaryto wrap
* @return The decorated edit summary (in wikitext)
*/
function decorateEditSummary DeputyExtraneousElement(editSummarychildren) {
const container = document.createElement('div');
return `${editSummary} ([[User:Chlod/Scripts/Deputy|Deputy]] v${deputyVersion})`;
container.classList.add('dp-cs-extraneous');
children = Array.isArray(children) ? children : [children];
children.forEach(child => container.appendChild(child.cloneNode(true)));
return container;
}
 
/**
* What it says on the tin. Attempt to parse out a `title`, `diff`,
* Evaluates any string using `mw.msg`. This handles internationalization of strings
* or `oldid` from a URL. This is useful for converting diff URLs into actual
* that are loaded outside the script or asynchronously.
* diff information, and especially useful for {{copied}} templates.
*
* If diff parameters were not found (no `diff` or `oldid`), they will be `null`.
* @param string The string to evaluate
*
* @param {...any} parameters Parameters to pass, if any
* @returnparam Aurl mw.MessageThe URL to parse
* @return Parsed info: `diff` or `oldid` revision IDs, and/or the page title.
*/
function msgEvalparseDiffUrl(string, ...parametersurl) {
constif m(typeof url === new mw.Map('string'); {
m.set('msg', string url = new URL(url);
}
return new mw.Message(m, 'msg', parameters);
// Attempt to get values from URL parameters (when using `/w/index.php?action=diff`)
let oldid = url.searchParams.get('oldid');
let diff = url.searchParams.get('diff');
let title = url.searchParams.get('title');
// Attempt to get information from this URL.
tryConvert: {
if (title && oldid && diff) {
// Skip if there's nothing else we need to get.
break tryConvert;
}
// Attempt to get values from Special:Diff short-link
const diffSpecialPageCheck =
// eslint-disable-next-line security/detect-unsafe-regex
/\/wiki\/Special:Diff\/(prev|next|\d+)(?:\/(prev|next|\d+))?/i.exec(url.pathname);
if (diffSpecialPageCheck != null) {
if (diffSpecialPageCheck[1] != null &&
diffSpecialPageCheck[2] == null) {
// Special:Diff/diff
diff = diffSpecialPageCheck[1];
}
else if (diffSpecialPageCheck[1] != null &&
diffSpecialPageCheck[2] != null) {
// Special:Diff/oldid/diff
oldid = diffSpecialPageCheck[1];
diff = diffSpecialPageCheck[2];
}
break tryConvert;
}
// Attempt to get values from Special:PermanentLink short-link
const permanentLinkCheck = /\/wiki\/Special:Perma(nent)?link\/(\d+)/i.exec(url.pathname);
if (permanentLinkCheck != null) {
oldid = permanentLinkCheck[2];
break tryConvert;
}
// Attempt to get values from article path with ?oldid or ?diff
// eslint-disable-next-line security/detect-non-literal-regexp
const articlePathRegex = new RegExp(mw.util.getUrl('(.*)'))
.exec(url.pathname);
if (articlePathRegex != null) {
title = decodeURIComponent(articlePathRegex[1]);
break tryConvert;
}
}
// Convert numbers to numbers
if (oldid != null && !isNaN(+oldid)) {
oldid = +oldid;
}
if (diff != null && !isNaN(+diff)) {
diff = +diff;
}
// Try to convert a page title
try {
title = new mw.Title(title).getPrefixedText();
}
catch (e) {
warn('Failed to normalize page title during diff URL conversion.');
}
return {
diff: diff,
oldid: oldid,
title: title
};
}
 
var ContributionSurveyRowSigningBehavior;
(function (ContributionSurveyRowSigningBehavior) {
ContributionSurveyRowSigningBehavior["Always"] = "always";
ContributionSurveyRowSigningBehavior["AlwaysTrace"] = "alwaysTrace";
ContributionSurveyRowSigningBehavior["AlwaysTraceLastOnly"] = "alwaysTraceLastOnly";
ContributionSurveyRowSigningBehavior["LastOnly"] = "lastOnly";
ContributionSurveyRowSigningBehavior["Never"] = "never";
})(ContributionSurveyRowSigningBehavior || (ContributionSurveyRowSigningBehavior = {}));
 
/**
Line 3,190 ⟶ 6,758:
*/
class DeputyContributionSurveySection {
/**
* Creates a DeputyContributionSurveySection from a given heading.
*
* @param casePage
* @param heading
*/
constructor(casePage, heading) {
this.casePage = casePage;
this.heading = heading;
this.headingName = sectionHeadingName(this.heading);
this.sectionElements = casePage.getContributionSurveySection(heading);
}
/**
* @return `true` if this section has been modified
Line 3,302 ⟶ 6,858:
}
if (this.closed) {
if (!this._section.originallyClosed) {
final.splice(1, 0, msgEval(window.deputy.wikiConfig.cci.collapseTop.get(), (((_a = this.comments) !== null && _a !== void 0 ? _a : '') + ' ~~~~').trim()).plain());
if let closingComments = (final[final.length(_a -= 1]this.trim(comments).length !== null && _a !== void 0) {? _a : '').trim();
finalif (this.popclosingCommentsSign.isSelected();) {
closingComments += ' ~~~~';
}
final.splice(1, 0, msgEval(window.deputy.wikiConfig.cci.collapseTop.get(), closingComments).plain());
if (final[final.length - 1].trim().length === 0) {
final.pop();
}
final.push(window.deputy.wikiConfig.cci.collapseBottom.get());
}
// If the section was originally closed, don't allow the archiving
final.push(window.deputy.wikiConfig.cci.collapseBottom.get());
// message to be edited.
}
return final.join('\n');
Line 3,343 ⟶ 6,907:
message.push(mw.msg('deputy.content.assessed.reworked', `${reworked}`));
}
ifconst nowClosed = (!this._section.originallyClosed && this.closed) {;
if (nowClosed) // Now closed.{
message.push(mw.msg('deputy.content.assessed.sectionClosed'));
}
const m = message.join(mw.msg('deputy.content.assessed.comma'));
returnif (m[0].toUpperCase()length +=== m.slice(10); {
return mw.msg('deputy.content.reformat');
}
const summary = mw.msg(nowClosed ?
'deputy.content.summary.sectionClosed' :
(finished === 0 && assessed > 0 ?
'deputy.content.summary.partial' :
'deputy.content.summary'), this.headingName, finished);
return summary + m[0].toUpperCase() + m.slice(1);
}
else {
return mw.msg('deputy.content.reformat');
}
}
/**
* @return the name of the section heading.
*/
get headingName() {
return this.heading.title;
}
/**
* @return the `n` of the section heading, if applicable.
*/
get headingN() {
return sectionHeadingN(this.heading);
}
/**
* Creates a DeputyContributionSurveySection from a given heading.
*
* @param casePage
* @param heading
*/
constructor(casePage, heading) {
this.casePage = casePage;
this.heading = normalizeWikiHeading(heading);
this.sectionNodes = casePage.getContributionSurveySection(heading);
}
/**
Line 3,358 ⟶ 6,953:
*
* @param wikitext Internal use only. Used to skip section loading using existing wikitext.
* @return The ContributionSurveySection for this section
*/
getSection(wikitext) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const collapsible = (_b = (_a = this.sectionElementssectionNodes.find((v) => v instanceof HTMLElement && v.querySelector('.mw-collapsible'))) === null || _a === void 0 ? void 0 : _a.querySelector('.mw-collapsible')) !== null && _b !== void 0 ? _b : null;
const sectionWikitext = yield this.casePage.wikitext.getSectionWikitext(this.headingName, this.headingN);
return (_c = this._section) !== null && _c !== void 0 ? _c : (this._section = new ContributionSurveySection(this.casePage, this.headingName, collapsible != null, collapsible === null || collapsible === void 0 ? void 0 : collapsible.querySelector('th > div').innerText, wikitext !== null && wikitext !== void 0 ? wikitext : yieldsectionWikitext, wikitext ? this.casePage.wikitext.getSectionWikitext(thisrevid : sectionWikitext.headingName)revid));
});
}
Line 3,373 ⟶ 6,970:
*/
prepare() {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
constlet firstListtargetSectionNodes = this.sectionElements.find((el) => el.tagName === 'UL')sectionNodes;
let listElements = this.sectionNodes.filter((el) => el instanceof HTMLElement && el.tagName === 'UL');
if (firstList == null) {
if (listElements.length === 0) // Not a valid section! Might be closed already.{
return// false;No list found ! Is this a valid section?
// Check for a collapsible section.
const collapsible = (_b = (_a = this.sectionNodes.find((v) => v instanceof HTMLElement && v.querySelector('.mw-collapsible'))) === null || _a === void 0 ? void 0 : _a.querySelector('.mw-collapsible')) !== null && _b !== void 0 ? _b : null;
if (collapsible) {
// This section has a collapsible. It's possible that it's a closed section.
// From here, use a different `sectionNodes` (specifically targeting all nodes
// inside that collapsible), and then locate all ULs inside that collapsible.
targetSectionNodes = Array.from(collapsible.childNodes);
listElements = Array.from(collapsible.querySelectorAll('ul'));
}
else {
// No collapsible found. Give up.
warn('Could not find valid ULs in CCI section.', targetSectionNodes);
return false;
}
}
this.originalList = firstList.parentElement.removeChild(firstList);
const rowElements = {};
for (letconst ilistElement =of 0; i < this.originalList.children.length; i++listElements) {
constfor li(let i = this.originalList0; i < listElement.children.item(length; i++); {
if ( const li.tagName !== 'LI'listElement.children.item(i) {;
returnif false;(li.tagName !== 'LI') {
} // Skip this element.
const anchor = li.querySelector('a:first-of-type') continue;
// Avoid enlisting if the anchor can't be found (invalid row).}
if ( const anchor) {= li.querySelector('a:first-of-type');
rowElements[new// Avoid enlisting if the mw.Title(anchor.innerText).getPrefixedText can't be found ()]invalid =row).
if (anchor) li;{
const anchorLinkTarget = parseDiffUrl(new URL(anchor.getAttribute('href'), window.___location.href)).title;
if (!anchorLinkTarget) {
warn('Could not parse target of anchor', anchor);
}
else {
rowElements[new mw.Title(anchorLinkTarget).getPrefixedText()] =
li;
}
}
}
}
const sectionWikitextsection = (yield this.getSection()).originalWikitext;
const sectionWikitext = section.originalWikitext;
this.revid = section.revid;
const wikitextLines = sectionWikitext.split('\n');
this.rows = [];
this.rowElements = [];
this.wikitextLines = [];
let rowElement;
for (let i = 0; i < wikitextLines.length; i++) {
const line = wikitextLines[i];
lettry rowElement;{
if (ContributionSurveyRow.isContributionSurveyRowText(line)) {
const csr = new ContributionSurveyRow(this.casePage, line);
rowElementconst originalElement = new DeputyContributionSurveyRow(csr, rowElements[csr.title.getPrefixedText()], line, this);
if (originalElement) {
rowElement = new DeputyContributionSurveyRow(csr, originalElement, line, this);
}
else {
// Element somehow not in list. Just keep line as-is.
warn(`Could not find row element for "${csr.title.getPrefixedText()}"`);
rowElement = line;
}
}
elsecatch (e) {
rowElement// =This line;is not a contribution surveyor row.
if (/^\*[^*:]+/.test(line)) {
// Only trigger on actual bulleted lists.
warn('Could not parse row.', line, e);
// For debugging and tests.
mw.hook('deputy.errors.cciRowParse').fire({
line, error: e.toString()
});
}
if (rowElement instanceof DeputyContributionSurveyRow &&
rowElement.originalElement.nextSibling == null &&
rowElement.originalElement.parentNode.nextSibling != null &&
// Just a blank line. Don't try to do anything else.
line !== '') {
// The previous row element was the last in the list. The
// list probably broke somewhere. (comment with wrong
// bullet?)
// In any case, let's try show it anyway. The user might
// miss some context otherwise.
// We'll only begin reading proper section data once we hit
// another bullet. So let's grab all nodes from the erring
// one until the next bullet list.
const extraneousNodes = [];
let lastNode = rowElement.originalElement.parentElement.nextSibling;
while (
// Another node exists next
lastNode != null &&
// The node is part of this section
targetSectionNodes.includes(lastNode) &&
(
// The node is not an element
!(lastNode instanceof HTMLElement) ||
// The element is not a bullet list
lastNode.tagName !== 'UL')) {
extraneousNodes.push(lastNode);
lastNode = lastNode.nextSibling;
}
rowElement = extraneousNodes;
}
else {
rowElement = line;
}
}
if (typeof rowElement !==instanceof 'string'DeputyContributionSurveyRow) {
this.rows.push(rowElement);
this.rowElements.push(rowElement);
this.wikitextLines.push(rowElement);
}
else if (Array.isArray(rowElement)) {
// Array of Nodes
this.wikitextLines.push(line);
if (rowElement.length !== 0) {
// Only append the row element if it has contents.
// Otherwise, there will be a blank blue box.
this.rowElements.push(DeputyExtraneousElement(rowElement));
}
}
else if (typeof rowElement === 'string') {
this.wikitextLines.push(rowElement);
}
this.wikitextLines.push(rowElement);
}
// Hide all section elements
this.toggleSectionElements(false);
return true;
});
}
/**
* Toggle section elements. Removes the section elements (but preservers them in
* `this.sectionElements`) if `false`, re-appends them to the DOM if `true`.
*
* @param toggle
*/
toggleSectionElements(toggle) {
var _a;
const bottom = (_a = this.heading.root.nextSibling) !== null && _a !== void 0 ? _a : null;
for (const sectionElement of this.sectionNodes) {
if (toggle) {
this.heading.root.parentNode.insertBefore(sectionElement, bottom);
}
else {
removeElement(sectionElement);
}
}
}
/**
Line 3,422 ⟶ 7,127:
*/
close() {
swapElementsremoveElement(this.container, this.originalList);
this.toggleSectionElements(true);
// Detach listeners to stop listening to events.
this.rows.forEach((row) => {
Line 3,429 ⟶ 7,135:
}
/**
* Toggles the closing comments input box. Thisand willsignature disable the input box ANDcheckbox.
* This will disable the input box AND hide the element from view.
*
* @param show
*/
toggleClosingCommentstoggleClosingElements(show) {
this.closingComments.setDisabled(!show);
this.closingComments.toggle(show);
this.closingCommentsSign.setDisabled(!show);
this.closingCommentsSign.toggle(show);
}
/**
Line 3,444 ⟶ 7,152:
*/
setDisabled(disabled) {
var _a, _b, _c, _d, _e, _f, _g;
(_a = this.closeButton) === null || _a === void 0 ? void 0 : _a.setDisabled(disabled);
(_b = this.reviewButton) === null || _b === void 0 ? void 0 : _b.setDisabled(disabled);
Line 3,450 ⟶ 7,158:
(_d = this.closingCheckbox) === null || _d === void 0 ? void 0 : _d.setDisabled(disabled);
(_e = this.closingComments) === null || _e === void 0 ? void 0 : _e.setDisabled(disabled);
(_f = this.rowsclosingCommentsSign) === null || _f === void 0 ? void 0 : _f.forEach((row) => row.setDisabled(disabled));
(_g = this.rows) === null || _g === void 0 ? void 0 : _g.forEach((row) => row.setDisabled(disabled));
this.disabled = disabled;
}
Line 3,457 ⟶ 7,166:
*
* @param sectionId
* @return Save data, or `false` if the save hit an error
*/
save(sectionId) {
return __awaiter(this, void 0, void 0, function* () {
if (sectionId == null) {
mw.notifythrow new Error(mw.msg('deputy.session.section.missingSection'), {);
autoHide: false,
title: mw.msg('deputy.session.section.failed'),
type: 'error'
});
}
returnif MwApi.action.postWithEditToken({this.closed &&
action:!this._section.originallyClosed 'edit',&&
pageid: this!window.casePagedeputy.pageId,config.core.dangerMode.get() &&
section:this.rows.some(r sectionId,=> !r.completed)) {
text:throw thisnew Error(mw.msg('deputy.session.section.wikitext,sectionIncomplete'));
summary: decorateEditSummary(this.editSummary)}
return MwApi.action.postWithEditToken(Object.assign(Object.assign({}, changeTag(yield window.deputy.getWikiConfig())), { action: 'edit', pageid: this.casePage.pageId, section: sectionId, text: this.wikitext, baserevid: this.revid, summary: decorateEditSummary(this.editSummary, window.deputy.config) })).then(function (data) {
}).then(function (data) {
return data;
}, function (code, data) => {
if (code === 'editconflict') {
// Wipe cache.
this.casePage.wikitext.resetCachedWikitext();
OO.ui.alert(mw.msg('deputy.session.section.conflict.help'), {
title: mw.msg('deputy.session.section.conflict.title')
}).then(() => {
window.deputy.session.rootSession.restartSession();
});
return false;
}
mw.notify(h_1("span", { dangerouslySetInnerHTML: data.errors[0].html }), {
autoHide: false,
Line 3,483 ⟶ 7,199:
return false;
});
});
}
/**
* Makes all rows of this section being loading data.
*
* @return A Promise that resolves when all rows have finished loading data.
*/
loadData() {
return __awaiter(this, void 0, void 0, function* () {
// For debugging and tests.
// noinspection JSUnresolvedReference
if (window.deputy.NO_ROW_LOADING !== true) {
yield Promise.all(this.rows.map(row => row.loadData()));
}
});
}
Line 3,489 ⟶ 7,219:
*/
render() {
this.closingCheckboxconst dangerMode = new OOwindow.deputy.config.core.uidangerMode.CheckboxInputWidgetget();
this.closingCheckbox = new OO.ui.CheckboxInputWidget({
selected: this._section.originallyClosed,
disabled: this._section.originallyClosed
});
this.closingComments = new OO.ui.TextInputWidget({
placeholder: mw.msg('deputy.session.section.closeComments'),
value: this._section.closingComments,
disabled: true
});
this.closeButtonclosingCommentsSign = new OO.ui.ButtonWidgetCheckboxInputWidget({
labelselected: mwwindow.msg('deputy.close'config.cci.signSectionArchive.get(),
}); disabled: true
this.reviewButton = new OO.ui.ButtonWidget({
label: mw.msg('deputy.review')
});
this.closeButton = new OO.ui.ButtonWidget(Object.assign({ label: mw.msg('deputy.session.section.stop'), title: mw.msg('deputy.session.section.stop.title') }, (dangerMode ? { invisibleLabel: true, icon: 'pause' } : {})));
this.reviewButton = new OO.ui.ButtonWidget(Object.assign({ label: mw.msg('deputy.review'), title: mw.msg('deputy.review.title') }, (dangerMode ? { invisibleLabel: true, icon: 'eye' } : {})));
this.saveButton = new OO.ui.ButtonWidget({
label: mw.msg('deputy.save'),
Line 3,509 ⟶ 7,244:
this.closeButton.on('click', () => __awaiter(this, void 0, void 0, function* () {
if (this.wikitext !== (yield this.getSection()).originalWikitext) {
OOdangerModeConfirm(window.uideputy.confirm(config, mw.msg('deputy.session.section.closeWarn')).done((confirmed) => {
if (confirmed) {
this.close();
Line 3,528 ⟶ 7,263:
});
window.deputy.windowManager.addWindows([reviewDialog]);
yield window.deputy.windowManager.openWindow(reviewDialog).opened;
}));
this.saveButton.on('click', () => __awaiter(this, void 0, void 0, function* () {
this.setDisabled(true);
saveContainer.classList.add('active');
const sectionId = yield getSectionId(this.casePage.title, this.headingName, this.headingN);
yield this.save(sectionId).then((result) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
if (result) {
mw.notify(mw.msg('deputy.session.section.saved'));
// Rebuild the entire section to HTML, and then reopen.
const { element, wikitext, revid } = yield getSectionHTML(this.casePage.title, sectionId);
removeElement(this.container);
const// sectionElementsRemove =whatever this.casePage.getContributionSurveySection(thissection elements are still there.heading);
// They may have been greatly modified by the save.
const sectionElements = this.casePage.getContributionSurveySection(this.heading.root);
sectionElements.forEach((el) => removeElement(el));
this.sectionElements// =Clear [];out section elements and re-append new ones to the DOM.
const oldHeadingthis.sectionNodes = this.heading[];
for// (constHeading childis ofpreserved Array.from(element.children))to {avoid messing with IDs.
const heading = oldHeadingthis.insertAdjacentElement('beforebegin', child)heading.root;
const insertRef = (_a this= heading.sectionElements.push(childnextSibling) !== null && _a !== void 0 ? _a : null;
for (const child of if Array.from(thiselement.casePage.isContributionSurveyHeading(childchildNodes)) {
if (!this.headingcasePage.isContributionSurveyHeading((_b = normalizeWikiHeading(child;,
// We're using elements this.headingNamethat =aren't currently appended to the
// DOM, so we have to manually set sectionHeadingName(child);the ceiling. Otherwise, we'll
// get the wrong element and ceiling checks will always be false.
element)) === null || _b === void 0 ? void 0 : _b.h)) {
heading.parentNode.insertBefore(child, insertRef);
this.sectionNodes.push(child);
// noinspection JSUnresolvedReference
$(child).children('.mw-collapsible').makeCollapsible();
}
}
if (!this._section.closed) {
this._section = null;
yield this.getSection(Object.assign(wikitext, { revid }));
yield this.prepare();
oldHeadingheading.insertAdjacentElement('afterend', this.render());
// Run this asynchronously.
setTimeout(this.loadData.bind(this), 0);
}
else {
this.close();
yield window.deputy.session.rootSession.closeSection(this);
}
removeElement(oldHeading);
}
}), (errorerr) => {
consoleOO.errorui.alert(error);err.message, {
title: mw.msg('deputy.session.section.failed')
});
error(err);
saveContainer.classList.remove('active');
this.setDisabled(false);
Line 3,569 ⟶ 7,321:
this.setDisabled(false);
}));
// Section closing (archive/ctop) elements
const closingWarning = DeputyMessageWidget({
classes: ['dp-cs-section-unfinishedWarning'],
type: 'error',
label: mw.msg('deputy.session.section.closeError')
});
closingWarning.toggle(false);
const updateClosingWarning = (() => {
const incomplete = this.rows.some((row) => !row.completed);
if (window.deputy.config.core.dangerMode.get()) {
this.saveButton.setDisabled(false);
closingWarning.setLabel(mw.msg('deputy.session.section.closeError.danger'));
}
else {
closingWarning.setLabel(mw.msg('deputy.session.section.closeError'));
this.saveButton.setDisabled(incomplete);
}
closingWarning.toggle(incomplete);
});
const closingCommentsField = new OO.ui.FieldLayout(this.closingComments, {
align: 'top',
label: mw.msg('Closing commentsdeputy.session.section.closeComments'),
invisibleLabel: true,
help: mw.msg('deputy.session.section.closeHelp'),
helpInline: true,
classes: ['dp-cs-section-closingCommentsField']
});
const closingCommentsSignField = new OO.ui.FieldLayout(this.closingCommentsSign, {
// Hide by default.
closingCommentsField.toggle(false); align: 'inline',
label: mw.msg('deputy.session.section.closeCommentsSign')
closingCommentsField.on('change', (v) => {
this.comments = v;
});
const closingFields = h_1("div", { class: "dp-cs-section-closing", style: { display: 'none' } },
this.toggleClosingComments(false);
unwrapWidget(closingWarning),
this.closingCheckbox.on('change', (v) => {
unwrapWidget(closingCommentsField),
unwrapWidget(closingCommentsSignField));
const updateClosingFields = (v) => {
this.closed = v;
closingCommentsField.toggleif (vthis._section.originallyClosed); {
// This section was originally closed. Hide everything.
this.toggleClosingComments(v);
}) v = false;
}
closingFields.style.display = v ? '' : 'none';
this.toggleClosingElements(v);
if (v) {
updateClosingWarning();
this.rows.forEach((row) => {
row.addEventListener('update', updateClosingWarning);
});
}
else {
closingWarning.toggle(false);
this.saveButton.setDisabled(false);
this.rows.forEach((row) => {
row.removeEventListener('update', updateClosingWarning);
});
}
};
this.closingCheckbox.on('change', updateClosingFields);
updateClosingFields(this.closed);
this.closingComments.on('change', (v) => {
this.comments = v;
});
window.test// =Danger this;mode buttons
const dangerModeElements = [];
return this.container = h_1("div", { class: "deputy dp-cs-section" },
if h_1("div", null, this.rows.map((rowdangerMode) => row.render())),{
const markAllFinishedButton = new OO.ui.ButtonWidget({
flags: ['destructive'],
icon: 'checkAll',
label: mw.msg('deputy.session.section.markAllFinished'),
title: mw.msg('deputy.session.section.markAllFinished'),
invisibleLabel: true
});
markAllFinishedButton.on('click', () => {
this.rows.forEach(v => v.markAllAsFinished());
});
const instantArchiveButton = new OO.ui.ButtonWidget({
flags: ['destructive', 'primary'],
label: mw.msg('deputy.session.section.instantArchive'),
title: mw.msg('deputy.session.section.instantArchive.title')
});
instantArchiveButton.on('click', () => {
this.closingCheckbox.setSelected(true);
this.saveButton.emit('click');
});
const dangerModeButtons = [
unwrapWidget(markAllFinishedButton),
unwrapWidget(instantArchiveButton)
];
dangerModeElements.push(h_1("div", { class: "dp-cs-section-danger--separator" }, mw.msg('deputy.session.section.danger')), dangerModeButtons);
// Remove spacing from save button
unwrapWidget(this.saveButton).style.marginRight = '0';
}
// Actual element
return this.container = h_1("div", { class: classMix('deputy', 'dp-cs-section', this._section.originallyClosed && 'dp-cs-section-archived') },
this._section.originallyClosed && h_1("div", { class: "dp-cs-section-archived-warn" }, unwrapWidget(new OO.ui.MessageWidget({
type: 'warning',
label: mw.msg('deputy.session.section.closed')
}))),
h_1("div", null, this.rowElements.map((row) => row instanceof HTMLElement ? row : row.render())),
h_1("div", { class: "dp-cs-section-footer" },
h_1("div", { style: { display: 'flex' } },
h_1("div", { style: {
flex: '1 1 100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
} },
unwrapWidget(new OO.ui.FieldLayout(this.closingCheckbox, {
Line 3,606 ⟶ 7,430:
label: mw.msg('deputy.session.section.close')
})),
unwrapWidget(closingCommentsField)closingFields),
h_1("div", { style: { display: 'flex', alignItems: 'end' } },
display: 'flex',
alignContent: 'end',
justifyContent: 'end',
flexWrap: dangerMode ? 'wrap' : 'nowrap',
maxWidth: '320px'
} },
unwrapWidget(this.closeButton),
unwrapWidget(this.reviewButton),
unwrapWidget(this.saveButton))),
dangerModeElements)),
saveContainer));
}
Line 3,616 ⟶ 7,447:
 
/**
* Displayed when a user has previously worked on a case page and is now visiting
* the page again (with no active session). Displayed inside an OOUI message box.
*
* @return HTMLparam element
*/
function DeputyCCISessionOverwriteMessage findNextSiblingElement(element) {
returnif h_1("span",element == null,) {
h_1("b",return null, mw.msg('deputy.session.otherActive.head')),;
h_1("br", null),}
let anchor = element.nextSibling;
mw.msg('deputy.session.otherActive.help'),
while (anchor && h_1!("br",anchor nullinstanceof Element),) {
h_1("span",anchor {= class: "dp-cs-session-stop" }))anchor.nextSibling;
}
return anchor;
}
 
Line 3,636 ⟶ 7,467:
*/
class DeputyRootSession {
/*
* =========================================================================
* INSTANCE AND ACTIVE SESSION FUNCTIONS
* =========================================================================
*/
/**
* @param session
* @param casePage
*/
constructor(session, casePage) {
/**
* Responder for session requests.
*/
this.sessionRequestResponder = this.sendSessionResponse.bind(this);
this.sessionStopResponder = this.handleStopRequest.bind(this);
this.session = session;
this.casePage = casePage;
}
/*
* =========================================================================
Line 3,672 ⟶ 7,485:
casePage.findContributionSurveyHeadings()
.forEach((heading) => {
const linknormalizedHeading = DeputyCCISessionStartLinknormalizeWikiHeading(heading, casePage);
const link = DeputyCCISessionStartLink(normalizedHeading, casePage);
startLink.push(link);
headingnormalizedHeading.root.appendChild(link);
});
window.deputy.comms.addEventListener('sessionStarted', () => {
Line 3,686 ⟶ 7,500:
}
/**
* Shows the interface for continuingoverwriting aan previousexisting session. ThisThe includesprovided
* theaction `[continuebutton CCIwill session]`close noticethe atother thesection. topThis ofdoes eachnot CCIstart page sectiona headingnew
* andsession; athe singleuser messagemust box showing whenstart the pagesession wason lastthis workedpage on ontheir top of theown.
* first CCI heading found.
*
* @param casePage The case page to continue with
Line 3,696 ⟶ 7,509:
return __awaiter(this, void 0, void 0, function* () {
yield mw.loader.using(['oojs-ui-core', 'oojs-ui.styles.icons-content'], () => {
const firstHeading = casePage.findContributionSurveyHeadingsfindFirstContributionSurveyHeadingElement()[0];
if (firstHeading) {
//const InsertstopButton element= directly into widgetnew OO.ui.ButtonWidget(not as text, or else event{
// handlers will be destroyed)label: mw.msg('deputy.session.otherActive.button'),
const messageBox = new OO.ui.MessageWidget({flags: ['primary', 'destructive']
});
const messageBox = DeputyMessageWidget({
classes: [
'deputy', 'dp-cs-session-notice', 'dp-cs-session-otherActive'
Line 3,706 ⟶ 7,521:
type: 'notice',
icon: 'alert',
labeltitle: new OOmw.uimsg('deputy.HtmlSnippet(DeputyCCISessionOverwriteMessage()session.otherActive.innerHTMLhead'),
} message: mw.msg('deputy.session.otherActive.help');,
const stopButton = new OO.ui.ButtonWidget({actions: [stopButton],
classesclosable: ['dp-cs-session-stop'],true
label: mw.msg('deputy.session.otherActive.button'),
flags: ['primary', 'destructive']
});
stopButton.on('click', () => __awaiter(this, void 0, void 0, function* () {
Line 3,730 ⟶ 7,543:
window.deputy.session.init();
});
swapElementsnormalizeWikiHeading(firstHeading).root.insertAdjacentElement('beforebegin', unwrapWidget(messageBox));
.querySelector('.dp-cs-session-stop'), unwrapWidget(stopButton));
firstHeading.insertAdjacentElement('beforebegin', unwrapWidget(messageBox));
}
});
Line 3,740 ⟶ 7,551:
* Shows the interface for continuing a previous session. This includes
* the `[continue CCI session]` notice at the top of each CCI page section heading
* and a single message box showing when the page was last worked on on top of the
* first CCI heading found.
*
Line 3,750 ⟶ 7,561:
DeputyRootSession.initEntryInterface(),
mw.loader.using(['oojs-ui-core', 'oojs-ui.styles.icons-content'], () => {
const firstHeadinglastActiveSection = casePageDeputyRootSession.findContributionSurveyHeadingsfindFirstLastActiveSection(casePage)[0];
ifconst firstSection = normalizeWikiHeading(casePage.findFirstContributionSurveyHeadingElement(firstHeading) {);
// Insert element directly into widget (not as text, or else event
// handlers will be destroyed).
const messageBoxcontinueButton = new OO.ui.MessageWidgetButtonWidget({
classeslabel: [mw.msg('deputy.session.continue.button'),
flags: 'deputy', ['dp-cs-session-noticeprimary', 'dp-cs-session-lastActiveprogressive']
],});
const messageBox = type: 'notice',DeputyMessageWidget({
iconclasses: 'history',[
label:'deputy', new'dp-cs-session-notice', OO.ui.HtmlSnippet(DeputyCCISessionContinueMessage({'dp-cs-session-lastActive'
casePage: casePage],
type: }).innerHTML)'notice',
});icon: 'history',
consttitle: continueButton =mw.msg('deputy.session.continue.head', new OODate().uitoLocaleString(mw.ButtonWidgetconfig.get('wgUserLanguage'), { dateStyle: 'long', timeStyle: 'medium' })),
message: mw.msg(lastActiveSection classes: ['dp-cs-session-continue'],?
label: mw.msg('deputy.session.continue.buttonhelp'), :
flags: ['primarydeputy.session.continue.help.fromStart', 'progressive']lastActiveSection ?
} normalizeWikiHeading(lastActiveSection);.title :
const sessionStartListener = () => __awaiter(this, void casePage.lastActiveSections[0, void 0, function* () {]
removeElement .replace(unwrapWidget(messageBox/_/g, ' '), firstSection.title);,
actions: yield this.initTabActiveInterface();[continueButton],
});closable: true
continueButton.on('click', (}) => {;
const sessionStartListener = () => __awaiter(this, void 0, void 0, function* removeElement(unwrapWidget(messageBox)); {
removeElement(unwrapWidget(messageBox));
yield this.initTabActiveInterface();
});
continueButton.on('click', () => {
removeElement(unwrapWidget(messageBox));
if (lastActiveSection) {
DeputyRootSession.continueSession(casePage);
window.deputy.comms.removeEventListener('sessionStarted', sessionStartListener);}
});else {
swapElements DeputyRootSession.continueSession(unwrapWidget(messageBox)casePage, [
.querySelector('.dp-cs-session-continue'), unwrapWidget(continueButton)); firstSection.id
firstHeading.insertAdjacentElement('beforebegin', unwrapWidget(messageBox) ]);
window.deputy.comms.addEventListener('sessionStarted', sessionStartListener, { once: true });
} window.deputy.comms.removeEventListener('sessionStarted', sessionStartListener);
});
firstSection.root.insertAdjacentElement('beforebegin', unwrapWidget(messageBox));
window.deputy.comms.addEventListener('sessionStarted', sessionStartListener, { once: true });
})
]);
Line 3,797 ⟶ 7,617:
return __awaiter(this, void 0, void 0, function* () {
const casePage = _casePage !== null && _casePage !== void 0 ? _casePage : yield DeputyCasePage.build();
returnyield mw.loader.using(['oojs-ui-core', 'oojs-ui.styles.icons-content'], () => {
const firstHeading = casePage.findContributionSurveyHeadingsfindFirstContributionSurveyHeadingElement()[0];
if (firstHeading) {
const messageBox = new OO.ui.MessageWidgetDeputyMessageWidget({
classes: [
'deputy', 'dp-cs-session-notice', 'dp-cs-session-tabActive'
],
type: 'infonotice',
labeltitle: new OOmw.uimsg('deputy.HtmlSnippet(DeputyCCISessionTabActiveMessage()session.tabActive.innerHTMLhead'),
message: mw.msg('deputy.session.tabActive.help'),
closable: true
});
normalizeWikiHeading(firstHeading).root.insertAdjacentElement('beforebegin', unwrapWidget(messageBox));
window.deputy.comms.addEventListener('sessionClosed', () => __awaiter(this, void 0, void 0, function* () {
removeElement(unwrapWidget(messageBox));
Line 3,815 ⟶ 7,637:
});
});
}
/**
* Finds the first last active section that exists on the page.
* If a last active section that still exists on the page could not be found,
* `null` is returned.
*
* @param casePage The case page to use
* @return The last active session's heading element.
*/
static findFirstLastActiveSection(casePage) {
const csHeadings = casePage.findContributionSurveyHeadings();
for (const lastActiveSection of casePage.lastActiveSections) {
for (const heading of csHeadings) {
if (normalizeWikiHeading(heading).id === lastActiveSection) {
return heading;
}
}
}
return null;
}
/**
Line 3,824 ⟶ 7,665:
static startSession(section, _casePage) {
return __awaiter(this, void 0, void 0, function* () {
const sectionNamesectionIds = sectionHeadingName(Array.isArray(section) ? section : [section]).map((_section) => normalizeWikiHeading(_section).id);
// Save session to storage
const casePage = _casePage !== null && _casePage !== void 0 ? _casePage : yield DeputyCasePage.build();
const session = yield this.setSession({
casePageId: casePage.pageId,
caseSections: [sectionName]sectionIds
});
const rootSession = window.deputy.session.rootSession =
Line 3,841 ⟶ 7,682:
*
* @param casePage The case page to continue with
* @param sectionIds The section IDs to load on startup. If not provided, this will be
* taken from the cache. If provided, this overrides the cache, discarding any
* sections cached previously.
*/
static continueSession(casePage, sectionIds) {
return __awaiter(this, void 0, void 0, function* () {
// Save session to storage
if (sectionIds) {
casePage.lastActiveSections = sectionIds;
}
const session = yield this.setSession({
casePageId: casePage.pageId,
Line 3,866 ⟶ 7,713:
return (yield window.deputy.storage.setKV('session', session)) ? session : null;
});
}
/*
* =========================================================================
* INSTANCE AND ACTIVE SESSION FUNCTIONS
* =========================================================================
*/
/**
* @param session
* @param casePage
*/
constructor(session, casePage) {
/**
* Responder for session requests.
*/
this.sessionRequestResponder = this.sendSessionResponse.bind(this);
this.sessionStopResponder = this.handleStopRequest.bind(this);
this.session = session;
this.casePage = casePage;
}
/**
Line 3,899 ⟶ 7,764:
'oojs-ui-core',
'oojs-ui-windows',
'oojs-ui-widgets',
'oojs-ui.styles.icons-alerts',
'oojs-ui.styles.icons-content',
Line 3,905 ⟶ 7,771:
'oojs-ui.styles.icons-media',
'oojs-ui.styles.icons-movement',
'ext.discussionTools.init',
'jquery.makeCollapsible'
], (require) => __awaiter(this, void 0, void 0, function* () {
// Instantiate the parser
Line 3,916 ⟶ 7,783:
const activeSectionPromises = [];
for (const heading of this.casePage.findContributionSurveyHeadings()) {
const headingNameheadingId = sectionHeadingNamenormalizeWikiHeading(heading).id;
if (this.session.caseSections.indexOf(headingNameheadingId) !== -1) {
activeSectionPromises.push(this.activateSection(this.casePage, heading)
.then(v => v ? headingNameheadingId : null));
}
else {
Line 3,934 ⟶ 7,801:
yield this.closeSession();
}
mw.hook('deputy.load.cci.root').fire();
res();
}));
Line 3,973 ⟶ 7,841:
*/
addSectionOverlay(casePage, heading) {
constvar section_a, =_b, casePage.getContributionSurveySection(heading)_c;
const listnormalizedHeading = section.findnormalizeWikiHeading((vheading) => v.tagName === 'UL')root;
const section = casePage.getContributionSurveySection(normalizedHeading);
const list = section.find((v) => v instanceof HTMLElement && v.tagName === 'UL');
const headingTop = window.scrollY +
normalizedHeading.getBoundingClientRect().bottom;
const sectionBottom = window.scrollY + ((_c = (_b = (_a = findNextSiblingElement(last(section))) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.top) !== null && _c !== void 0 ? _c : normalizedHeading.parentElement.getBoundingClientRect().bottom);
const overlayHeight = sectionBottom - headingTop;
if (list != null) {
list.style.position = 'relative';
list.appendChild(DeputyCCISessionAddSection({
casePage, heading,
height: overlayHeight
}));
}
Line 4,025 ⟶ 7,900:
return false;
}
const sectionNamesectionId = sectionHeadingNamenormalizeWikiHeading(heading).id;
this.sections.push(el);
const lastActiveSession = this.session.caseSections.indexOf(sectionNamesectionId);
if (lastActiveSession === -1) {
this.session.caseSections.push(sectionNamesectionId);
yield DeputyRootSession.setSession(this.session);
}
yield casePage.addActiveSection(sectionNamesectionId);
normalizeWikiHeading(heading).root.insertAdjacentElement('afterend', el.render());
yield el.loadData();
mw.hook('deputy.load.cci.session').fire();
return true;
});
Line 4,051 ⟶ 7,928:
e0.casePage : e0;
const heading = e0 instanceof DeputyContributionSurveySection ?
e0.heading : normalizeWikiHeading(e1);
const sectionNamesectionId = sectionHeadingName(heading).id;
const sectionListIndex = this.sections.indexOf(el);
if (el != null && sectionListIndex !== -1) {
this.sections.splice(sectionListIndex, 1);
}
const lastActiveSessionlastActiveSection = this.session.caseSections.indexOf(sectionNamesectionId);
if (lastActiveSessionlastActiveSection !== -1) {
this.session.caseSections.splice(lastActiveSessionlastActiveSection, 1);
// If no sections remain, clear the session.
if (this.session.caseSections.length === 0) {
Line 4,068 ⟶ 7,945:
else {
yield DeputyRootSession.setSession(this.session);
yield casePage.removeActiveSection(sectionNamesectionId);
this.addSectionOverlay(casePage, heading.h);
}
}
});
}
/**
* Restarts the section. This rebuilds *everything* from the ground up, which may
* be required when there's an edit conflict.
*/
restartSession() {
return __awaiter(this, void 0, void 0, function* () {
const casePage = this.casePage;
yield this.closeSession();
yield window.deputy.session.DeputyRootSession.continueSession(casePage);
});
}
Line 4,081 ⟶ 7,969:
*/
class FakeDocument {
/**
* @param data Data to include in the iframe
*/
constructor(data) {
this.ready = false;
this.iframe = document.createElement('iframe');
this.iframe.style.display = 'none';
this.iframe.addEventListener('load', () => {
this.ready = true;
});
this.iframe.src = URL.createObjectURL(data instanceof Blob ? data : new Blob(data));
// Disables JavaScript, modals, popups, etc., but allows same-origin access.
this.iframe.setAttribute('sandbox', 'allow-same-origin');
document.getElementsByTagName('body')[0].appendChild(this.iframe);
}
/**
* Creates a fake document and waits for the `document` to be ready.
Line 4,113 ⟶ 7,986:
get document() {
return this.iframe.contentDocument;
}
/**
* @param data Data to include in the iframe
*/
constructor(data) {
this.ready = false;
this.iframe = document.createElement('iframe');
this.iframe.style.display = 'none';
this.iframe.addEventListener('load', () => {
this.ready = true;
});
this.iframe.src = URL.createObjectURL(data instanceof Blob ? data : new Blob(data));
// Disables JavaScript, modals, popups, etc., but allows same-origin access.
this.iframe.setAttribute('sandbox', 'allow-same-origin');
document.getElementsByTagName('body')[0].appendChild(this.iframe);
}
/**
Line 4,172 ⟶ 8,060:
swapElements(contentText, newContentText);
document.querySelectorAll('#ca-edit a, #ca-ve-edit a').forEach((e) => {
const newEditUrl = new URL(e.getAttribute('href'), window.___location.href);
newEditUrl.searchParams.set('oldid', `${diff}`);
e.setAttribute('href', newEditUrl.href);
Line 4,232 ⟶ 8,120:
this.supportedProjects = cachedSupported.projects;
}
const sites = yield fetch('https://copyvios`${(yield window.deputy.getWikiConfig()).cci.toolforgeearwigRoot.orgget()}/api.json?action=sites&version=1'`)
.then((r) => r.json());
this.supportedLanguages = [];
Line 4,276 ⟶ 8,164:
}
const { project, language } = this.guessProject(options.project, options.language);
return `https://copyvios${(yield window.toolforgedeputy.org/getWikiConfig()).cci.earwigRoot.get()}?action=search&lang=${language}&project=${project}&${typeof target === 'number' ?
'oldid=' + target :
'title=' + target.getPrefixedText()}&use_engine=${((_a = options.useEngine) !== null && _a !== void 0 ? _a : true) ? 1 : 0}&use_links=${((_b = options.useLinks) !== null && _b !== void 0 ? _b : true) ? 1 : 0}&turnitin=${((_c = options.turnItIn) !== null && _c !== void 0 ? _c : false) ? 1 : 0}`;
Line 4,321 ⟶ 8,209:
});
menuSelectWidget.on('select', () => {
// Not a multiselect MenuSelectWidget
const selected = menuSelectWidget.findSelectedItem();
if (selected) {
Line 4,423 ⟶ 8,312:
*/
constructor(options) {
var _a;
this.state = DeputyPageToolbarState.Open;
this.instanceId = generateId();
this.revisionStatusUpdateListener = this.onRevisionStatusUpdate.bind(this);
this.options = options;
if (options.revisionStatus != null) {
this.revision = (_a = options.revision) !== null && _a !== void 0 ? _a : mw.config.get('wgRevisionId');
}
this.state = window.deputy.config.cci.toolbarInitialState.get();
this.runAsyncJobs();
}
Line 4,447 ⟶ 8,339:
this.row = {
casePage: yield DeputyCase.build(this.options.caseId, normalizeTitle(this.options.caseTitle)),
title: normalizeTitle(this.options.title),
type: this.options.rowType
};
});
Line 4,484 ⟶ 8,377:
var _a;
if (this.revision == null) {
if ((_a = this.options.forceRevision) !== null && _a !== void 0 ? _a : true) {
// Show if forced, or if we're not looking at the latest revision.
mw.config.get('wgRevisionId') !== mw.config.get('wgCurRevisionId') ||
((_a = this.options.forceRevision) !== null && _a !== void 0 ? _a : true)) {
return this.renderMissingRevisionInfo();
}
Line 4,492 ⟶ 8,388:
}
this.revisionCheckbox = new OO.ui.CheckboxInputWidget({
labeltitle: mw.msg('deputy.session.revision.assessed'),
selected: this.options.revisionStatus
});
let lastStatus = this.revisionCheckbox.isSelected();
// State variables
let processing = false;
let incommunicable = false;
Line 4,566 ⟶ 8,463:
* @return The OOUI ButtonWidget element.
*/
renderNextRevisionButtonrenderRevisionNavigationButtons() {
if (this.nextRevisionButtonrow.type === new'pageonly') OO.ui.ButtonWidget({
return h_1("div", { class: "dp-pt-section" }, unwrapWidget(new OO.ui.PopupButtonWidget({
invisibleLabel: true,
label icon: mw.msg('deputy.session.page.diff.nextinfo'),
title framed: mw.msg('deputy.session.page.diff.next')false,
icon: this.revision == null ? 'play' label: mw.msg('nextdeputy.session.page.pageonly.title'),
}); popup: {
head: true,
this.nextRevisionButton.on('click', () => __awaiter(this, void 0, void 0, function* () {
this.setDisabled(true); icon: 'infoFilled',
if label: mw.msg(this'deputy.optionssession.nextRevisionpage.pageonly.title') {,
// No need to worry$content: about$(h_1("p", swappingnull, elements heremw.msg('deputy.session.page.pageonly.help'))), since `loadNewDiff`
// will fire the `wikipage.diff`padded: MW hook. This means this element willtrue
// be rebuilt from scratch anyway.}
try {})));
}
const nextRevisionData = yield window.deputy.comms.sendAndWait({
const getButtonClickHandler = (button, reverse) => {
type: 'pageNextRevisionRequest',
return () => __awaiter(this, void 0, void 0, function* () caseId: this.options.caseId,{
page: this.row.title.getPrefixedTextsetDisabled(true),;
if after: (this.revisionoptions.nextRevision) {
});// No need to worry about swapping elements here, since `loadNewDiff`
if// (nextRevisionDatawill ==fire null)the {`wikipage.diff` MW hook. This means this element will
// be rebuilt from OO.ui.alert(mw.msg('deputy.session.pagescratch anyway.incommunicable'));
try this.setDisabled(false);{
const nextRevisionData = yield window.deputy.comms.sendAndWait({
type: 'pageNextRevisionRequest',
caseId: this.options.caseId,
page: this.row.title.getPrefixedText(),
after: this.revision,
reverse
});
if (nextRevisionData == null) {
OO.ui.alert(mw.msg('deputy.session.page.incommunicable'));
this.setDisabled(false);
}
else if (nextRevisionData.revid != null) {
yield DiffPage.loadNewDiff(nextRevisionData.revid);
}
else {
this.setDisabled(false);
button.setDisabled(true);
}
}
else ifcatch (nextRevisionData.revid != nulle) {
yield DiffPage.loadNewDifferror(nextRevisionData.revide);
}
else {
this.setDisabled(false);
this.nextRevisionButton.setDisabled(true);
}
}
catchelse if (ethis.options.nextRevision !== false) {
console// Sets disabled to false if the value is null.error(e);
this.setDisabled(false);
}
});
};
else if (this.options.nextRevision !== false) {
this.previousRevisionButton = new OO.ui.ButtonWidget({
// Sets disabled to false if the value is null.
invisibleLabel: this.setDisabled(false);true,
}label: mw.msg('deputy.session.page.diff.previous'),
title: mw.msg('deputy.session.page.diff.previous'),
}));
if (this.options.nextRevision == null) {icon: 'previous',
disabled: this.nextRevisionButtonoptions.setDisabled(true);nextRevision == null
});
this.previousRevisionButton.on('click', getButtonClickHandler(this.nextRevisionButton, true));
this.nextRevisionButton = new OO.ui.ButtonWidget({
invisibleLabel: true,
label: mw.msg('deputy.session.page.diff.next'),
title: mw.msg('deputy.session.page.diff.next'),
icon: this.revision == null ? 'play' : 'next',
disabled: this.options.nextRevision == null
});
this.nextRevisionButton.on('click', getButtonClickHandler(this.nextRevisionButton, false));
return h_1("div", { class: "dp-pt-section" },
h_1("div", { class: "dp-pt-section-content" }, unwrapWidget(this.nextRevisionButton)));
this.revision != null && unwrapWidget(this.previousRevisionButton),
unwrapWidget(this.nextRevisionButton)));
}
/**
Line 4,643 ⟶ 8,566:
this.renderMenu(mw.msg('deputy.session.page.analysis'), deputyPageAnalysisOptions()),
this.renderMenu(mw.msg('deputy.session.page.tools'), deputyPageTools())));
}
/**
* Rends the page toolbar actions and main section, if the dropdown is open.
*/
renderOpen() {
return [
h_1("div", { class: "dp-pageToolbar-actions" },
h_1("div", { class: "dp-pageToolbar-close", role: "button", title: mw.msg('deputy.session.page.close'), onClick: () => this.setState(DeputyPageToolbarState.Hidden) }),
h_1("div", { class: "dp-pageToolbar-collapse", role: "button", title: mw.msg('deputy.session.page.collapse'), onClick: () => this.setState(DeputyPageToolbarState.Collapsed) })),
h_1("div", { class: "dp-pageToolbar-main" },
this.renderStatusDropdown(),
this.renderCaseInfo(),
this.renderRevisionInfo(),
this.revisionNavigationSection =
this.renderRevisionNavigationButtons(),
this.renderMenus())
];
}
/**
* Renders the collapsed toolbar button.
*
* @return The render button, to be included in the main toolbar.
*/
renderCollapsed() {
return h_1("div", { class: "dp-pageToolbar-collapsed", role: "button", title: mw.msg('deputy.session.page.expand'), onClick: () => this.setState(DeputyPageToolbarState.Open) });
}
/**
Line 4,648 ⟶ 8,596:
*/
render() {
console.log(this.state);
if (this.state === DeputyPageToolbarState.Hidden) {
const portletLink = mw.util.addPortletLink('p-tb', '#', mw.msg('deputy.session.page.open'), 'pt-dp-pt', mw.msg('deputy.session.page.open.tooltip'));
portletLink.querySelector('a').addEventListener('click', (event) => {
event.preventDefault();
this.setState(DeputyPageToolbarState.Open);
return false;
});
// Placeholder element
return this.element = h_1("div", { class: "deputy" });
}
else {
const toolbar = document.getElementById('pt-dp-pt');
if (toolbar) {
removeElement(toolbar);
}
}
return this.element = h_1("div", { class: "deputy dp-pageToolbar" },
this.renderStatusDropdownstate === DeputyPageToolbarState.Open && this.renderOpen(),
this.renderCaseInfostate === DeputyPageToolbarState.Collapsed && this.renderCollapsed(),);
this.renderRevisionInfo(),
this.nextRevisionSection = this.renderNextRevisionButton(),
this.renderMenus());
}
/**
Line 4,661 ⟶ 8,623:
*/
setDisabled(disabled) {
var _a, _b, _c, _d;
(_a = this.statusDropdown) === null || _a === void 0 ? void 0 : _a.setDisabled(disabled);
(_b = this.revisionCheckbox) === null || _b === void 0 ? void 0 : _b.setDisabled(disabled);
(_c = this.nextRevisionButtonpreviousRevisionButton) === null || _c === void 0 ? void 0 : _c.setDisabled(disabled);
(_d = this.nextRevisionButton) === null || _d === void 0 ? void 0 : _d.setDisabled(disabled);
}
/**
* Sets the display state of the toolbar. This will also set the
* initial state configuration option for the user.
*
* @param state
*/
setState(state) {
this.state = state;
window.deputy.config.cci.toolbarInitialState.set(state);
window.deputy.config.save();
swapElements(this.element, this.render());
}
/**
Line 4,689 ⟶ 8,664:
this.options.nextRevision = data.nextRevision;
// Re-render button.
swapElements(this.nextRevisionSectionrevisionNavigationSection, this.nextRevisionSectionrevisionNavigationSection = this.renderNextRevisionButton());
this.renderRevisionNavigationButtons());
}
}
Line 4,711 ⟶ 8,687:
* used instead of the revision-specific toolbar.
* @param title The title of the page to get information for. Defaults to current.
* @param timeout Timeout for the page detail request.
*/
static getPageDetails(revision = mw.config.get('wgRevisionId'), title = window.deputy.currentPage, timeout = 500) {
return __awaiter(this, void 0, void 0, function* () {
return window.deputy.comms.sendAndWait({
Line 4,718 ⟶ 8,695:
page: title.getPrefixedText(),
revision: revision
}, timeout);
});
}
/**
*
* @param data
*/
Line 4,732 ⟶ 8,708:
'oojs-ui-core',
'oojs-ui-windows',
'oojs-ui-widgets',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-movement',
'oojs-ui.styles.icons-moderation',
'oojs-ui.styles.icons-media',
'oojs-ui.styles.icons-editing-advanced',
'oojs-ui.styles.icons-editing-citation'
], () => {
if (mw.hookconfig.get('wikipage.diffwgDiffNewId').add(() =>== __awaiter(this, void 0, void 0, function* (null) {
// AttemptNot toon geta newdiff revisionpage, databut *withwgRevisionId revisionis ID*populated nonetheless.
data = yield DeputyPageSessionthis.getPageDetailsinitInterface(mw.config.get('wgDiffNewId'data) ||;
mw.config.get('wgRevisionId'));}
else {
const openPromise = this.appendToolbar(Object.assign(Object.assign({}, data), { forceRevision: this.toolbar != null ||
mw.hook('wikipage.diff').add(() => __awaiter(this, void 0, void 0, function* //() Is a diff page.{
yield mwthis.config.getinitInterface('wgDiffNewId') != null })data);
if (this.toolbar &&}));
this.toolbar.revision !== mw.config.get('wgRevisionId')) {}
const oldToolbar = this.toolbar;
openPromise.then(() => {
oldToolbar.close();
});
}
this.toolbar = yield openPromise;
}));
});
}
}
/**
* Initialize the interface.
*
* @param data
*/
initInterface(data) {
return __awaiter(this, void 0, void 0, function* () {
// Attempt to get new revision data *with revision ID*.
const isCurrentDiff = /[?&]diff=0+(\D|$)/.test(window.___location.search);
data = yield DeputyPageSession.getPageDetails((isCurrentDiff ?
// On a "cur" diff page
mw.config.get('wgDiffOldId') :
// On a "prev" diff page
mw.config.get('wgDiffNewId')) ||
mw.config.get('wgRevisionId'), window.deputy.currentPage,
// Relatively low-stakes branch, we can handle a bit of a delay.
2000);
const openPromise = this.appendToolbar(Object.assign(Object.assign({}, data), { forceRevision: this.toolbar != null ||
// Is a diff page.
mw.config.get('wgDiffNewId') != null }));
if (
// Previous toolbar exists. Close it before moving on.
this.toolbar &&
this.toolbar.revision !== mw.config.get('wgRevisionId')) {
const oldToolbar = this.toolbar;
openPromise.then(() => {
oldToolbar.close();
});
}
this.toolbar = yield openPromise;
});
}
/**
Line 4,800 ⟶ 8,803:
constructor() {
this.DeputyRootSession = DeputyRootSession;
this.DeputyPageSession = DeputyPageSession;
}
/**
Line 4,844 ⟶ 8,848:
}
else if (DeputyCasePage.isCasePage()) {
if (mw.config.get('wgCurRevisionId') !==
mw.config.get('wgRevisionId')) {
// This is an old revision. Don't show the interface.
return;
}
const casePage = yield DeputyCasePage.build();
yield DeputyRootSession.initOverwriteMessage(casePage);
}
else if (mw.config.get('wgAction') === 'view') {
yield this.normalPageInitialization();
window.deputy.comms.addEventListener('sessionStarted', () => {
Line 4,895 ⟶ 8,904:
// Normal page. Determine if this is being worked on, and then
// start a new session if it is.
const pageSession = yield DeputyPageSession.getPageDetails(mw.config.get('wgDiffNewId') ||;
mw.config.get('wgRevisionId'));
if (pageSession) {
// This page is being worked on, create a session.
Line 4,935 ⟶ 8,943:
 
/**
* MediaWiki core contains a lot of quirks in the code. Other extensions
* API communication class
* also have their own quirks. To prevent these quirks from affecting Deputy's
* functionality, we need to perform a few hacks.
*/
classfunction DeputyAPIperformHacks () {
var _a;
const HtmlEmitter = (_a = mw.jqueryMsg.HtmlEmitter) !== null && _a !== void 0 ? _a : {
prototype: Object.getPrototypeOf(new mw.jqueryMsg.Parser().emitter)
};
// This applies the {{int:message}} parser function with "MediaWiki:". This
// is due to VisualEditor using "MediaWiki:" in message values instead of "int:"
HtmlEmitter.prototype.mediawiki =
HtmlEmitter.prototype.int;
/**
* Performs a simple if check. Works just like the Extension:ParserFunctions
* Creates a Deputy API instance.
* version; it checks if the first parameter is blank and returns the second
* parameter if true. The latter parameter is passed if false.
*
* UNLIKE the Extension:ParserFunctions version, this version does not trim
* the parameters.
*
* @see https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#if
* @param nodes
* @return see function description
*/
constructorHtmlEmitter.prototype.if = function (nodes) { }
var _a, _b;
return (nodes[0].trim() ? ((_a = nodes[1]) !== null && _a !== void 0 ? _a : '') : ((_b = nodes[2]) !== null && _b !== void 0 ? _b : ''));
};
// "#if" is unsupported due to the parsing done by jqueryMsg.
/**
* Simple function to avoid parsing errors during message expansion. Drops the "Template:"
* Logs the user out of the API.
* prefix before a link.
*
* @param nodes
* @return `{{text}}`
*/
logoutHtmlEmitter.prototype.template = function (nodes) {
return __awaiter(this, void 0, void 0, function* `{{${nodes.join('|') {}}}`;
};
// TODO: Make logout API request
window.deputy.storage.setKV('api-token', null);
});
}
/**
* Allows `{{subst:...}}` to work. Does not actually change anything.
* Logs in the user. Optional: only used for getting data on deleted revisions.
*
* @param nodes
* @return `{{text}}`
*/
loginHtmlEmitter.prototype.subst = function (nodes) {
return __awaiter`{{subst:${nodes.map(this,(v) void=> 0,typeof voidv 0,=== function*'string' ? v : v.text() {).join('|')}}}`;
};
this.token = yield window.deputy.storage.getKV('api-token');
// TODO: If token, set token
// TODO: If no token, start OAuth flow and make login API request
throw new Error('Unimplemented method.');
});
}
/**
* GetsWorks expandedexactly revisionlike datathe fromlocalurl themagic APIword. ThisReturns returnsthe alocal response similarhref to thea page.
* `revisions`Also objectadds provided by action=query, but also includesstrings additionalif informationgiven.
* relevant (such as the parsed (HTML) comment, diff size, etc.)
*
* @see https://www.mediawiki.org/wiki/Help:Magic_words#URL_data
* @param revisions The revisions to get the data for
* @param nodes
* @return An object of expanded revision data mapped by revision IDs
* @return `/wiki/{page}?{query}`
*/
HtmlEmitter.prototype.localurl = function (nodes) {
getExpandedRevisionData(revisions) {
return __awaitermw.util.getUrl(this, void nodes[0,]) void+ 0,'?' function* ()+ {nodes[1];
return fetch(`https://zoomiebot.toolforge.org/bot/api/deputy/v1/revisions/${mw.config.get('wgWikiID')}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'revisions=' + revisions.join('|')
})
.then((r) => r.json())
.then((j) => {
if (j.error) {
throw new Error(j.error.info);
}
return j;
})
.then((j) => j.revisions);
});
}
}
 
/**
*
*/
class DeputyPreferences {
constructor() {
this.preferences = DeputyPreferences.default;
}
/**
* @param id
* @return value
*/
get(id) {
var _a;
const idParts = id.split('.');
let current = this.preferences;
for (const part of idParts) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
current = (_a = current === null || current === void 0 ? void 0 : current[part]) !== null && _a !== void 0 ? _a : null;
}
return current;
}
}
DeputyPreferences.default = {
configVersion: 0,
lastVersion: '0.1.0',
cci: {
contentDefault: 'expanded'
}
};
 
/**
* MediaWiki core contains a lot of quirks in the code. Other extensions
* also have their own quirks. To prevent these quirks from affecting Deputy's
* functionality, we need to perform a few hacks.
*/
function performHacks () {
// This applies the {{int:message}} parser function with "MediaWiki:". This
// is due to VisualEditor using "MediaWiki:" in message values instead of "int:"
mw.jqueryMsg.HtmlEmitter.prototype.mediawiki =
mw.jqueryMsg.HtmlEmitter.prototype.mediaWiki =
mw.jqueryMsg.HtmlEmitter.prototype.Mediawiki =
mw.jqueryMsg.HtmlEmitter.prototype.MediaWiki =
mw.jqueryMsg.HtmlEmitter.prototype.int;
mw.jqueryMsg.HtmlEmitter.prototype.if = function (nodes) {
return nodes[0] ? nodes[1] : nodes[2];
};
}
 
/**
* Works like `Object.values`.
*
* @param obj The object to get the values of.
* @return The values of the given object as an array
*/
function getObjectValues(obj) {
return Object.keys(obj).map((key) => obj[key]);
}
 
/**
* Transforms the `redirects` object returned by MediaWiki's `query` action into an
* object instead of an array.
*
* @param redirects
* @param normalized
* @return Redirects as an object
*/
function toRedirectsObject(redirects, normalized) {
var _a;
if (redirects == null) {
return {};
}
const out = {};
for (const redirect of redirects) {
out[redirect.from] = redirect.to;
}
// Single-level redirect-normalize loop check
for (const normal of normalized) {
out[normal.from] = (_a = out[normal.to]) !== null && _a !== void 0 ? _a : normal.to;
}
return out;
}
 
Line 5,125 ⟶ 9,056:
}
 
let InternalRevisionDateGetButton;
/**
* Initializes the process element.
* Checks if two MediaWiki page titles are equal.
*/
function initRevisionDateGetButton() {
InternalRevisionDateGetButton = class RevisionDateGetButton extends OO.ui.ButtonWidget {
/**
* @param config Configuration to be passed to the element.
*/
constructor(config) {
super(Object.assign({
icon: 'download',
invisibleLabel: true,
disabled: true
}, config));
this.revisionInputWidget = config.revisionInputWidget;
this.dateInputWidget = config.dateInputWidget;
this.revisionInputWidget.on('change', this.updateButton.bind(this));
this.dateInputWidget.on('change', this.updateButton.bind(this));
this.on('click', this.setDateFromRevision.bind(this));
this.updateButton();
}
/**
* Update the disabled state of the button.
*/
updateButton() {
this.setDisabled(isNaN(+this.revisionInputWidget.getValue()) ||
!!this.dateInputWidget.getValue());
}
/**
* Set the date from the revision ID provided in the value of
* `this.revisionInputWidget`.
*/
setDateFromRevision() {
return __awaiter(this, void 0, void 0, function* () {
const revid = this.revisionInputWidget.getValue();
if (isNaN(+revid)) {
mw.notify(mw.msg('deputy.ante.dateAuto.invalid'), { type: 'error' });
this.updateButton();
return;
}
this
.setIcon('ellipsis')
.setDisabled(true);
this.dateInputWidget.setDisabled(true);
yield MwApi.action.get({
action: 'query',
prop: 'revisions',
revids: revid,
rvprop: 'timestamp'
}).then((data) => {
if (data.query.badrevids != null) {
mw.notify(mw.msg('deputy.ante.dateAuto.missing', revid), { type: 'error' });
this.updateButton();
return;
}
this.dateInputWidget.setValue(
// ISO-format date
data.query.pages[0].revisions[0].timestamp.split('T')[0]);
this.dateInputWidget.setDisabled(false);
this.setIcon('download');
this.updateButton();
}, (_error, errorData) => {
mw.notify(mw.msg('deputy.ante.dateAuto.failed', getApiErrorText(errorData)), { type: 'error' });
this.dateInputWidget.setDisabled(false);
this.setIcon('download');
this.updateButton();
});
});
}
};
}
/**
* Creates a new RevisionDateGetButton.
*
* @param title1config Configuration to be passed to the element.
* @return A RevisionDateGetButton object
* @param title2
* @return `true` if `title1` and `title2` refer to the same page
*/
function equalTitle(title1,RevisionDateGetButton title2(config) {
if (!InternalRevisionDateGetButton) {
return normalizeTitle(title1).getPrefixedDb() === normalizeTitle(title2).getPrefixedDb();
initRevisionDateGetButton();
}
return new InternalRevisionDateGetButton(config);
}
 
let InternalSmartTitleInputWidget;
/**
* Initializes the process element.
*/
function initSmartTitleInputWidget() {
InternalSmartTitleInputWidget = class SmartTitleInputWidget extends mw.widgets.TitleInputWidget {
/**
* @param config Configuration to be passed to the element.
*/
constructor(config) {
super(Object.assign(config, {
// Force this to be true
allowSuggestionsWhenEmpty: true
}));
}
/**
* @inheritDoc
*/
getRequestQuery() {
const v = super.getRequestQuery();
return v || normalizeTitle().getSubjectPage().getPrefixedText();
}
/**
* @inheritDoc
*/
getQueryValue() {
const v = super.getQueryValue();
return v || normalizeTitle().getSubjectPage().getPrefixedText();
}
};
}
/**
* Creates a new SmartTitleInputWidget.
*
* @param config Configuration to be passed to the element.
* @return A SmartTitleInputWidget object
*/
function SmartTitleInputWidget (config) {
if (!InternalSmartTitleInputWidget) {
initSmartTitleInputWidget();
}
return new InternalSmartTitleInputWidget(config);
}
 
let InternalPageLatestRevisionGetButton;
/**
* Initializes the process element.
*/
function initPageLatestRevisionGetButton() {
InternalPageLatestRevisionGetButton = class PageLatestRevisionGetButton extends OO.ui.ButtonWidget {
/**
* @param config Configuration to be passed to the element.
*/
constructor(config) {
super(Object.assign({
icon: 'download',
invisibleLabel: true,
disabled: true
}, config));
this.titleInputWidget = config.titleInputWidget;
this.revisionInputWidget = config.revisionInputWidget;
this.titleInputWidget.on('change', this.updateButton.bind(this));
this.revisionInputWidget.on('change', this.updateButton.bind(this));
this.on('click', this.setRevisionFromPageLatestRevision.bind(this));
this.updateButton();
}
/**
* Update the disabled state of the button.
*/
updateButton() {
this.setDisabled(this.titleInputWidget.getValue().trim().length === 0 ||
this.revisionInputWidget.getValue().trim().length !== 0 ||
!this.titleInputWidget.isQueryValid());
}
/**
* Set the revision ID from the page provided in the value of
* `this.titleInputWidget`.
*/
setRevisionFromPageLatestRevision() {
return __awaiter(this, void 0, void 0, function* () {
this
.setIcon('ellipsis')
.setDisabled(true);
this.revisionInputWidget.setDisabled(true);
const title = this.titleInputWidget.getValue();
yield MwApi.action.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'ids'
}).then((data) => {
if (data.query.pages[0].missing) {
mw.notify(mw.msg('deputy.ante.revisionAuto.missing', title), { type: 'error' });
this.updateButton();
return;
}
this.revisionInputWidget.setValue(data.query.pages[0].revisions[0].revid);
this.revisionInputWidget.setDisabled(false);
this.setIcon('download');
this.updateButton();
}, (_error, errorData) => {
mw.notify(mw.msg('deputy.ante.revisionAuto.failed', getApiErrorText(errorData)), { type: 'error' });
this.revisionInputWidget.setDisabled(false);
this.setIcon('download');
this.updateButton();
});
});
}
};
}
/**
* Creates a new PageLatestRevisionGetButton.
*
* @param config Configuration to be passed to the element.
* @return A PageLatestRevisionGetButton object
*/
function PageLatestRevisionGetButton (config) {
if (!InternalPageLatestRevisionGetButton) {
initPageLatestRevisionGetButton();
}
return new InternalPageLatestRevisionGetButton(config);
}
 
Line 5,176 ⟶ 9,304:
*/
refreshLabel() {
if (this.copiedTemplateRow.from && equalTitle(this.copiedTemplateRow.from, normalizeTitle(this.copiedTemplateRow.parent.parsoid.getPage())
.getSubjectPage())) {
this.label = mw.message('deputy.ante.copied.entry.shortTo', this.copiedTemplateRow.to || '???').text();
}
else if (this.copiedTemplateRow.to && equalTitle(this.copiedTemplateRow.to, normalizeTitle(this.copiedTemplateRow.parent.parsoid.getPage())
.getSubjectPage())) {
this.label = mw.message('deputy.ante.copied.entry.shortFrom', this.copiedTemplateRow.from || '???').text();
Line 5,287 ⟶ 9,415:
new Date(copiedTemplateRow.date.trim()) : null));
this.inputs = {
from: new mw.widgets.TitleInputWidgetSmartTitleInputWidget({
$overlay: this.parent.$overlay,
placeholder: mw.msg('deputy.ante.copied.from.placeholder'),
Line 5,298 ⟶ 9,426:
validate: /^\d*$/
}),
to: new mw.widgets.TitleInputWidgetSmartTitleInputWidget({
$overlay: this.parent.$overlay,
placeholder: mw.msg('deputy.ante.copied.to.placeholder'),
Line 5,319 ⟶ 9,447:
}),
merge: new OO.ui.CheckboxInputWidget({
valueselected: yesNo(copiedTemplateRow.merge)
}),
afd: new OO.ui.TextInputWidget({
Line 5,328 ⟶ 9,456:
validate: /^((?!W(iki)?p(edia)?:(A(rticles)?[ _]?f(or)?[ _]?d(eletion)?\/)).+|$)/gi
}),
date: new mw.widgets.datetime.DateTimeInputWidgetDateInputWidget({
// calendar$overlay: {this.parent.$overlay,
// $overlay: parent["$overlay"]
// },
calendar: null,
icon: 'calendar',
value: parsedDate ? `${parsedDate.getUTCFullYear()}-${parsedDate.getUTCMonth() + 1}-${parsedDate.getUTCDate()}` : undefined,
clearable: true,
valueplaceholder: parsedDatemw.msg('deputy.ante.copied.date.placeholder'),
calendar: {
verticalPosition: 'above'
}
}),
toggle: new OO.ui.ToggleSwitchWidget()
};
const diffConvert = new OO.ui.ButtonWidget({
label: mw.msg('Convertdeputy.ante.copied.convert')
});
const dateAuto = RevisionDateGetButton({
label: mw.msg('deputy.ante.dateAuto', 'to_diff'),
revisionInputWidget: this.inputs.to_diff,
dateInputWidget: this.inputs.date
});
const revisionAutoFrom = PageLatestRevisionGetButton({
invisibleLabel: false,
label: mw.msg('deputy.ante.revisionAuto'),
title: mw.msg('deputy.ante.revisionAuto.title', 'from'),
titleInputWidget: this.inputs.from,
revisionInputWidget: this.inputs.from_oldid
});
const revisionAutoTo = PageLatestRevisionGetButton({
invisibleLabel: false,
label: mw.msg('deputy.ante.revisionAuto'),
title: mw.msg('deputy.ante.revisionAuto.title', 'to'),
titleInputWidget: this.inputs.to,
revisionInputWidget: this.inputs.to_diff
});
// const dateButton = new OO.ui.PopupButtonWidget({
// icon: "calendar",
// title: "Select a date"
// });
this.fieldLayouts = {
from: new OO.ui.FieldLayout(this.inputs.from, {
Line 5,353 ⟶ 9,496:
help: mw.msg('deputy.ante.copied.from.help')
}),
from_oldid: new OO.ui.FieldLayoutActionFieldLayout(this.inputs.from_oldid, revisionAutoFrom, {
$overlay: this.parent.$overlay,
label: mw.msg('deputy.ante.copied.from_oldid.label'),
Line 5,365 ⟶ 9,508:
help: mw.msg('deputy.ante.copied.to.help')
}),
to_diff: new OO.ui.FieldLayoutActionFieldLayout(this.inputs.to_diff, revisionAutoTo, {
$overlay: this.parent.$overlay,
label: mw.msg('deputy.ante.copied.to_diff.label'),
Line 5,396 ⟶ 9,539:
help: mw.msg('deputy.ante.copied.afd.help')
}),
date: new OO.ui.FieldLayoutActionFieldLayout(this.inputs.date, dateAuto, {
align: 'inline',
classes: ['cte-fieldset-date']
Line 5,487 ⟶ 9,630:
this.copiedTemplateRow[field] = value ? 'yes' : '';
}
else if (input instanceof mw.widgets.datetime.DateTimeInputWidgetDateInputWidget) {
this.copiedTemplateRow[field] = value ?
new Datewindow.moment(value).toLocaleDateString(, 'enYYYY-GBMM-DD', {)
year: .locale(mw.config.get('numeric', month: 'long', day: 'numericwgContentLanguage'))
} .format('D MMMM Y') : undefined;
if (value.length > 0) {
this.fieldLayouts[field].setWarnings([]);
Line 5,516 ⟶ 9,659:
*/
convertDeprecatedDiff() {
constreturn value__awaiter(this, =void 0, void 0, function* this.inputs.diff.getValue(); {
try { const value = this.inputs.diff.getValue();
try {
const url = new URL(value, window.___location.href);
if (!value) {
return;
}
if (url.host === window.___location.host) {
console.warn('Attempted to convert a diff URL from another wiki.');
}
// From the same wiki, accept deprecation
// Attempt to get values from URL parameters (when using `/w/index.php?action=diff`)
let oldid = url.searchParams.get('oldid');
let diff = url.searchParams.get('diff');
const title = url.searchParams.get('title');
// Attempt to get values from Special:Diff short-link
const diffSpecialPageCheck = /\/wiki\/Special:Diff\/(prev|next|\d+)(?:\/(prev|next|\d+))?/.exec(url.pathname);
if (diffSpecialPageCheck != null) {
if (diffSpecialPageCheck[1] != null &&
diffSpecialPageCheck[2] == null) {
// Special:Diff/diff
diff = diffSpecialPageCheck[1];
}
else if (diffSpecialPageCheck[1]url.host !== nullwindow.___location.host) &&{
diffSpecialPageCheck[2]if (!=(yield nullOO.ui.confirm(mw.msg('deputy.ante.copied.diffDeprecate.warnHost')))) {
// Special:Diff/oldid/diff return;
oldid = diffSpecialPageCheck[1];}
diff = diffSpecialPageCheck[2];
}
} // From the same wiki, accept deprecation immediately.
const confirmProcess = new OO.ui.Process();// Parse out info from this diff URL
for (const [_rowName, newValue] ofconst [parseInfo = parseDiffUrl(url);
['to_oldid'let { diff, oldid], } = parseInfo;
['to_diff',const diff],{ title } = parseInfo;
['to'// If only an oldid was provided, title]and no diff
] if (oldid && !diff) {
const rowName diff = _rowNameoldid;
if (newValue == null) {oldid = undefined;
continue;
}
ifconst confirmProcess = new OO.ui.Process();
// FieldLooping hasover anthe existingrow name and the value that will replace it.
this.copiedTemplateRowfor (const [rowName_rowName, newValue] !=of null &&[
this.copiedTemplateRow[rowName'to_oldid', oldid].length > 0 &&,
this.copiedTemplateRow[rowName'to_diff', diff] !== newValue) {,
confirmProcess.next(() => __awaiter(this['to', void 0, void 0, function* () {title]
]) {
const confirmPromise = OO.ui.confirm(mw.message('deputy.ante.copied.diffDeprecate.replace', rowName, this.copiedTemplateRow[rowName], newValue).text());
const confirmPromise.done((confirmed)rowName => {_rowName;
if (newValue == if (confirmednull) {
this.inputs[rowName].setValue(newValue)continue;
}
if });(
// Field has an returnexisting confirmPromise;value
}));this.copiedTemplateRow[rowName] != null &&
} this.copiedTemplateRow[rowName].length > 0 &&
else this.copiedTemplateRow[rowName] !== newValue) {
this confirmProcess.inputs[rowName].setValuenext(() => __awaiter(this, void 0, void 0, function* (newValue); {
const confirmPromise = dangerModeConfirm(window.CopiedTemplateEditor.config, mw.message('deputy.ante.copied.diffDeprecate.replace', rowName, this.copiedTemplateRow[rowName], newValue).text());
confirmPromise.done((confirmed) => {
if (confirmed) {
this.inputs[rowName].setValue(newValue);
this.fieldLayouts[rowName].toggle(true);
}
});
return confirmPromise;
}));
}
else {
this.inputs[rowName].setValue(newValue);
this.fieldLayouts[rowName].toggle(true);
}
}
confirmProcess.next(() => {
this.copiedTemplateRow.parent.save();
this.inputs.diff.setValue('');
if (!this.inputs.toggle.getValue()) {
this.fieldLayouts.diff.toggle(false);
}
});
confirmProcess.execute();
}
confirmProcess.next(catch (e) => {
thiserror('Cannot convert `diff` parameter to URL.copiedTemplateRow.parent.save(', e);
thisOO.inputsui.diffalert(mw.setValuemsg('deputy.ante.copied.diffDeprecate.failed'));
if (!this.inputs.toggle.getValue()) {}
this.fieldLayouts.diff.toggle(false});
}
});
confirmProcess.execute();
}
catch (e) {
console.error('Cannot convert `diff` parameter to URL.', e);
OO.ui.alert(mw.msg('deputy.ante.copied.diffDeprecate.failed'));
}
}
/**
Line 5,592 ⟶ 9,732:
*/
setupOutlineItem() {
/** @member any */
if (this.outlineItem !== undefined) {
/** @member any */
this.outlineItem
.setMovable(true)
Line 5,622 ⟶ 9,760:
*/
class AttributionNoticeRow {
/**
*
* @param parent
*/
constructor(parent) {
this._parent = parent;
const r = btoa((Math.random() * 10000).toString()).slice(0, 6);
this.name = this.parent.name + '#' + r;
this.id = btoa(parent.node.getTarget().wt) + '-' + this.name;
}
/**
* @return The parent of this attribution notice row.
Line 5,648 ⟶ 9,776:
newParent.addRow(this);
this._parent = newParent;
}
/**
*
* @param parent
*/
constructor(parent) {
this._parent = parent;
const r = window.btoa((Math.random() * 10000).toString()).slice(0, 6);
this.name = this.parent.name + '#' + r;
this.id = window.btoa(parent.node.getTarget().wt) + '-' + this.name;
}
/**
Line 5,656 ⟶ 9,794:
*/
clone(parent) {
// Odd constructor usage here allows cloning from subclasses without having
// to re-implement the cloning function.
// noinspection JSCheckFunctionSignatures
return new this.constructor(this, parent);
Line 5,720 ⟶ 9,860:
* in the list will be used.
*/
static copiedmerge(templateList, pivot) {
pivot = pivot !== null && pivot !== void 0 ? pivot : templateList[0];
while (templateList.length > 0) {
const template = templateList[0];
if (template !== pivot) {
if (template.node.getTarget().href !== pivot.node.getTarget().href) {
throw new Error("Attempted to merge incompatible templates.");
}
pivot.merge(template, { delete: true });
}
Line 5,754 ⟶ 9,897:
const mergeTarget = new OO.ui.DropdownInputWidget({
$overlay: true,
labeltitle: mw.msg('deputy.ante.merge.from.select')
});
const mergeTargetButton = new OO.ui.ButtonWidget({
Line 5,760 ⟶ 9,903:
});
mergeTargetButton.on('click', () => {
const template = parentTemplate.parsoid.findNoticeType(type).find((v) => v.name === mergeTarget.valuegetValue());
if (template) {
// If template found, merge and reset panel
Line 5,782 ⟶ 9,925:
const notices = parentTemplate.parsoid.findNoticeType(type);
// Confirm before merging.
OOdangerModeConfirm(window.uiCopiedTemplateEditor.confirm(config, mw.message('deputy.ante.merge.all.confirm', `${notices.length - 1}`).text()).done((confirmed) => {
if (confirmed) {
// Recursively merge all templates
TemplateMerger.copiedmerge(notices, parentTemplate);
mergeTarget.setValue(null);
mergePanel.toggle(false);
Line 5,839 ⟶ 9,982:
function renderPreviewPanel(template) {
const previewPanel = h_1("div", { class: "cte-preview" });
// TODO: types-mediawiki limitation
const updatePreview = mw.util.throttle(() => __awaiter(this, void 0, void 0, function* () {
if (!previewPanel) {
Line 5,963 ⟶ 10,107:
deleteButton.on('click', () => {
if (this.copiedTemplate.rows.length > 0) {
OOdangerModeConfirm(window.uiCopiedTemplateEditor.confirm(config, mw.message('deputy.ante.copied.remove.confirm', `${this.copiedTemplate.rows.length}`).text()).done((confirmed) => {
if (confirmed) {
this.copiedTemplate.destroy();
Line 6,038 ⟶ 10,182:
*/
setupOutlineItem() {
/** @member any */
if (this.outlineItem !== undefined) {
/** @member any */
this.outlineItem
.setMovable(true)
Line 6,094 ⟶ 10,236:
*/
class AttributionNotice extends EventTarget {
/**
* Super constructor for AttributionNotice subclasses.
*
* @param node
* The ParsoidTransclusionTemplateNode of this notice.
*/
constructor(node) {
super();
this.node = node;
this.name = this.element.getAttribute('about')
.replace(/^#mwt/, '') + '-' + this.i;
this.id = btoa(node.getTarget().wt) + '-' + this.name;
this.parse();
}
/**
* @return The ParsoidDocument handling this notice (specifically its node).
Line 6,126 ⟶ 10,254:
get i() {
return this.node.i;
}
/**
* Super constructor for AttributionNotice subclasses.
*
* @param node
* The ParsoidTransclusionTemplateNode of this notice.
*/
constructor(node) {
super();
this.node = node;
this.name = this.element.getAttribute('about')
.replace(/^#mwt/, '') + '-' + this.i;
this.id = window.btoa(node.getTarget().wt) + '-' + this.name;
this.parse();
}
/**
Line 6,541 ⟶ 10,683:
new Date(rowDate.trim()) : null));
const inputs = {
to: new mw.widgets.TitleInputWidgetSmartTitleInputWidget({
$overlay: this.parent.$overlay,
required: true,
Line 6,549 ⟶ 10,691:
// eslint-disable-next-line camelcase
from_oldid: new OO.ui.TextInputWidget({
$overlay: this.parent.$overlay,
value: this.splitArticleTemplateRow.from_oldid || '',
placeholder: mw.msg('deputy.ante.splitArticle.from_oldid.placeholder')
}),
date: new mw.widgets.datetime.DateTimeInputWidget({
$overlay: this.parent.$overlay,
required: true,
calendar: null,
icon: 'calendar',
clearable: true,
value: parsedDate
}),
diff: new OO.ui.TextInputWidget({
$overlay: this.parent.$overlay,
value: this.splitArticleTemplateRow.from_oldid || '',
placeholder: mw.msg('deputy.ante.splitArticle.diff.placeholder'),
Line 6,579 ⟶ 10,711:
return false;
}
}
}),
date: new mw.widgets.DateInputWidget({
$overlay: this.parent.$overlay,
required: true,
icon: 'calendar',
value: parsedDate ? `${parsedDate.getUTCFullYear()}-${parsedDate.getUTCMonth() + 1}-${parsedDate.getUTCDate()}` : undefined,
placeholder: mw.msg('deputy.ante.copied.date.placeholder'),
calendar: {
verticalPosition: 'above'
}
})
};
const dateAuto = RevisionDateGetButton({
label: mw.msg('deputy.ante.dateAuto', 'diff'),
revisionInputWidget: inputs.diff,
dateInputWidget: inputs.date
});
const fieldLayouts = {
to: new OO.ui.FieldLayout(inputs.to, {
Line 6,596 ⟶ 10,743:
help: mw.msg('deputy.ante.splitArticle.from_oldid.help')
}),
datediff: new OO.ui.FieldLayout(inputs.datediff, {
$overlay: this.parent.$overlay,
align: 'left',
label: mw.msg('deputy.ante.splitArticle.datediff.label'),
help: mw.msg('deputy.ante.splitArticle.datediff.help')
}),
diffdate: new OO.ui.FieldLayoutActionFieldLayout(inputs.diffdate, dateAuto, {
$overlay: this.parent.$overlay,
align: 'left',
label: mw.msg('deputy.ante.splitArticle.diffdate.label'),
help: mw.msg('deputy.ante.splitArticle.diffdate.help')
})
};
Line 6,614 ⟶ 10,761:
// Attach the change listener
input.on('change', (value) => {
if (input instanceof mw.widgets.datetime.DateTimeInputWidgetDateInputWidget) {
this.splitArticleTemplateRow[field] = value ?
new Datewindow.moment(value).toLocaleDateString(, 'enYYYY-GBMM-DD', {)
year: .locale(mw.config.get('numeric', month: 'long', day: 'numericwgContentLanguage'))
} .format('D MMMM Y') : undefined;
if (value.length > 0) {
fieldLayouts[field].setWarnings([]);
Line 6,645 ⟶ 10,792:
*/
setupOutlineItem() {
/** @member any */
if (this.outlineItem !== undefined) {
/** @member any */
this.outlineItem
.setMovable(true)
Line 6,793 ⟶ 10,938:
deleteButton.on('click', () => {
if (this.splitArticleTemplate.rows.length > 0) {
OOdangerModeConfirm(window.uiCopiedTemplateEditor.confirm(config, mw.message('deputy.ante.splitArticle.remove.confirm', `${this.splitArticleTemplate.rows.length}`).text()).done((confirmed) => {
if (confirmed) {
this.splitArticleTemplate.destroy();
Line 6,838 ⟶ 10,983:
yesNo(this.splitArticleTemplate.collapse) : false
});
const from = new mw.widgets.TitleInputWidgetSmartTitleInputWidget({
$overlay: this.parent.$overlay,
value: this.splitArticleTemplate.from || '',
Line 6,872 ⟶ 11,017:
*/
setupOutlineItem() {
/** @member any */
if (this.outlineItem !== undefined) {
/** @member any */
this.outlineItem
.setMovable(true)
Line 7,058 ⟶ 11,201:
new Date(rowDate.trim()) : null));
const inputs = {
article: new mw.widgets.TitleInputWidgetSmartTitleInputWidget({
$overlay: this.parent.$overlay,
required: true,
Line 7,064 ⟶ 11,207:
placeholder: mw.msg('deputy.ante.mergedFrom.article.placeholder')
}),
date: new mw.widgets.datetime.DateTimeInputWidgetDateInputWidget({
$overlay: this.parent.$overlay,
required: true,
calendar: null,
icon: 'calendar',
value: parsedDate ? `${parsedDate.getUTCFullYear()}-${parsedDate.getUTCMonth() + 1}-${parsedDate.getUTCDate()}` : undefined,
clearable: true,
valueplaceholder: parsedDatemw.msg('deputy.ante.copied.date.placeholder')
}),
target: new mw.widgets.TitleInputWidgetSmartTitleInputWidget({
$overlay: this.parent.$overlay,
value: this.mergedFromTemplate.target || '',
Line 7,088 ⟶ 11,230:
}),
talk: new OO.ui.CheckboxInputWidget({
$overlay: this.parent.$overlay,
selected: yesNo(this.mergedFromTemplate.target)
})
Line 7,132 ⟶ 11,273:
this.mergedFromTemplate[field] = value ? 'yes' : 'no';
}
else if (input instanceof mw.widgets.datetime.DateTimeInputWidgetDateInputWidget) {
this.mergedFromTemplate[field] = value ?
new Datewindow.moment(value).toLocaleDateString(, 'enYYYY-GBMM-DD', {)
year: .locale(mw.config.get('numeric', month: 'long', day: 'numericwgContentLanguage'))
} .format('D MMMM Y') : undefined;
if (value.length > 0) {
fieldLayouts[field].setWarnings([]);
Line 7,161 ⟶ 11,302:
*/
setupOutlineItem() {
/** @member any */
if (this.outlineItem !== undefined) {
/** @member any */
this.outlineItem
.setMovable(true)
Line 7,324 ⟶ 11,463:
new Date(rowDate.trim()) : null));
const inputs = {
to: new mw.widgets.TitleInputWidgetSmartTitleInputWidget({
$overlay: this.parent.$overlay,
required: true,
Line 7,330 ⟶ 11,469:
placeholder: mw.msg('deputy.ante.mergedTo.to.placeholder')
}),
date: new mw.widgets.datetime.DateTimeInputWidgetDateInputWidget({
$overlay: this.parent.$overlay,
required: true,
calendar: null,
icon: 'calendar',
value: parsedDate ? `${parsedDate.getUTCFullYear()}-${parsedDate.getUTCMonth() + 1}-${parsedDate.getUTCDate()}` : undefined,
clearable: true,
valueplaceholder: parsedDatemw.msg('deputy.ante.copied.date.placeholder')
}),
small: new OO.ui.CheckboxInputWidget({
$overlay: this.parent.$overlay,
selected: yesNo(this.mergedToTemplate.small, false)
})
Line 7,371 ⟶ 11,508:
this.mergedToTemplate[field] = value ? 'yes' : 'no';
}
else if (input instanceof mw.widgets.datetime.DateTimeInputWidgetDateInputWidget) {
this.mergedToTemplate[field] = value ?
new Datewindow.moment(value).toLocaleDateString(, 'enYYYY-GBMM-DD', {)
year: .locale(mw.config.get('numeric', month: 'long', day: 'numericwgContentLanguage'))
} .format('D MMMM Y') : undefined;
if (value.length > 0) {
fieldLayouts[field].setWarnings([]);
Line 7,400 ⟶ 11,537:
*/
setupOutlineItem() {
/** @member any */
if (this.outlineItem !== undefined) {
/** @member any */
this.outlineItem
.setMovable(true)
Line 7,489 ⟶ 11,624:
* @param _regex The regular expression to exec with
* @param string The string to exec against
* @return The matches found
*/
function matchAll(_regex, string) {
Line 7,593 ⟶ 11,729:
*/
renderFields() {
var _a, _b, _c, _d;
// Use order: `date`, `monthday` + `year`, `year`
const rowDate = (_a = this.backwardsCopyTemplateRow.date) !== null && _a !== void 0 ? _a : (this.backwardsCopyTemplateRow.monthday ?
Line 7,611 ⟶ 11,747:
value: rowDate
}),
author: new OO.ui.TagMultiselectWidgetTextInputWidget({
allowArbitrary: true,
placeholder: mw.msg('deputy.ante.backwardsCopy.entry.author.placeholder'),
selectedvalue: (_d = authors[0]) !== null && _d !== void 0 ? _d : this.backwardsCopyTemplateRow.author
}),
url: new OO.ui.TextInputWidget({
Line 7,671 ⟶ 11,806:
const field = _field;
const input = inputs[field];
ifinput.on('change', (fieldvalue) === 'author')> {
inputthis.on('change', (value)backwardsCopyTemplateRow[field] => {value;
if (valuethis.length === 0backwardsCopyTemplateRow.parent.save() {;
this.backwardsCopyTemplateRow.author = null});
this.backwardsCopyTemplateRow.authorlist = null;
}
else if (value.length > 1) {
this.backwardsCopyTemplateRow.author = null;
this.backwardsCopyTemplateRow.authorlist =
// TODO: ANTE l10n
value.map((v) => v.data).join('; ');
}
else {
this.backwardsCopyTemplateRow.authorlist = null;
this.backwardsCopyTemplateRow.author =
value[0].data;
}
this.backwardsCopyTemplateRow.parent.save();
});
}
else {
// Attach the change listener
input.on('change', (value) => {
this.backwardsCopyTemplateRow[field] = value;
this.backwardsCopyTemplateRow.parent.save();
});
}
if (input instanceof OO.ui.TextInputWidget) {
// Rechecks the validity of the field.
Line 7,709 ⟶ 11,821:
*/
setupOutlineItem() {
/** @member any */
if (this.outlineItem !== undefined) {
/** @member any */
this.outlineItem
.setMovable(true)
Line 7,790 ⟶ 11,900:
function DemoTemplateMessage (nocat = false) {
return h_1("span", null,
h_1("b", null, mw.message(nocat ? 'deputy.cciante.nocat.head' : 'deputy.cciante.demo.head').parseDom().get()),
h_1("br", null),
mw.message(nocat ? 'deputy.cciante.nocat.help' : 'deputy.cciante.demo.help').parseDom().get(),
h_1("br", null),
h_1("span", { class: "cte-message-button" }));
Line 7,889 ⟶ 11,999:
deleteButton.on('click', () => {
if (this.backwardsCopyTemplate.rows.length > 0) {
OOdangerModeConfirm(window.uiCopiedTemplateEditor.confirm(config, mw.message('deputy.ante.copied.remove.confirm', `${this.backwardsCopyTemplate.rows.length}`).text()).done((confirmed) => {
if (confirmed) {
this.backwardsCopyTemplate.destroy();
Line 7,920 ⟶ 12,030:
if (this.backwardsCopyTemplate.node.hasParameter('bot')) {
const bot = this.backwardsCopyTemplate.node.getParameter('bot');
return unwrapWidget(new OO.ui.MessageWidgetDeputyMessageWidget({
type: 'notice',
icon: 'robot',
label: new OO.ui.HtmlSnippet(mw.message('deputy.ante.backwardsCopy.bot', bot).parse()),
closable: true
}));
}
Line 7,939 ⟶ 12,050:
// Insert element directly into widget (not as text, or else event
// handlers will be destroyed).
const messageBox = new OO.ui.MessageWidgetDeputyMessageWidget({
type: 'notice',
icon: 'alert',
label: new OO.ui.HtmlSnippet(DemoTemplateMessage().innerHTML),
closable: true
});
const clearButton = new OO.ui.ButtonWidget({
Line 8,016 ⟶ 12,128:
*/
setupOutlineItem() {
/** @member any */
if (this.outlineItem !== undefined) {
/** @member any */
Line 8,316 ⟶ 12,427:
value: this.translatedPageTemplate.version,
placeholder: mw.msg('deputy.ante.translatedPage.version.placeholder'),
validate: /^\d+*$/gi
}),
insertversion: new OO.ui.TextInputWidget({
value: this.translatedPageTemplate.insertversion,
placeholder: mw.msg('deputy.ante.translatedPage.insertversion.placeholder'),
validate: /^[\d/]+*$/gi
}),
section: new OO.ui.TextInputWidget({
Line 8,425 ⟶ 12,536:
*/
setupOutlineItem() {
/** @member any */
if (this.outlineItem !== undefined) {
/** @member any */
this.outlineItem
.setMovable(true)
Line 8,651 ⟶ 12,760:
});
menuSelectWidget.on('select', () => {
// Not a multiselect menu; cast the result to OptionWidget.
const selected = menuSelectWidget.findSelectedItem();
if (selected) {
Line 8,684 ⟶ 12,794:
const addListener = this.parent.layout.on('add', () => {
for (const name of Object.keys(this.parent.layout.pages)) {
/** @member any */
if (name !== 'cte-no-templates' && this.outlineItem !== null) {
// Pop this page out if a page exists.
Line 8,708 ⟶ 12,817:
*/
setupOutlineItem() {
/** @member any */
if (this.outlineItem !== undefined) {
/** @member any */
this.outlineItem.toggle(false);
}
Line 8,740 ⟶ 12,847:
* Encodes text for an API parameter. This performs both an encodeURIComponent
* and a string replace to change spaces into underscores.
*
* @param {string} text
* @returnreturns {string}
*/
function encodeAPIComponent(text) {
Line 8,749 ⟶ 12,855:
/**
* Clones a regular expression.
*
* @param regex The regular expression to clone.
* @returnreturns A new regular expression object.
*/
function cloneRegex(regex) {
Line 8,761 ⟶ 12,866:
*/
class ParsoidTransclusionTemplateNode {
/**
* Create a new ParsoidTransclusionTemplateNode.
*
* @param {ParsoidDocument} parsoidDocument
* The document handling this transclusion node.
* @param {HTMLElement} originalElement
* The original element where the `data-mw` of this node is found.
* @param {*} data
* The `data-mw` `part.template` of this node.
* @param {number} i
* The `i` property of this node.
* @param {boolean} autosave
* Whether to automatically save parameter and target changes or not.
*/
constructor(parsoidDocument, originalElement, data, i, autosave = true) {
this.parsoidDocument = parsoidDocument;
this.element = originalElement;
this.data = data;
this.i = i;
this.autosave = autosave;
}
/**
* Creates a new ParsoidTransclusionTemplateNode. Can be used later on to add a template
* into wikitext. To have this node show up in wikitext, append the node's element (using
* {@link ParsoidTransclusionTemplateNode.element}) to the document of a ParsoidDocument.
*
* @param document The document used to generate this node.
* @param template The template to create. If you wish to generate wikitext as a block-type
Line 8,793 ⟶ 12,876:
* @param parameters The parameters to the template.
* @param autosave
* @returnreturns A new ParsoidTransclusionTemplateNode.
*/
static fromNew(document, template, parameters, autosave) {
Line 8,820 ⟶ 12,903:
}));
return new ParsoidTransclusionTemplateNode(document, el, data, data.i, autosave);
}
/**
* Create a new ParsoidTransclusionTemplateNode.
* @param {ParsoidDocument} parsoidDocument
* The document handling this transclusion node.
* @param {HTMLElement} originalElement
* The original element where the `data-mw` of this node is found.
* @param {*} data
* The `data-mw` `part.template` of this node.
* @param {number} i
* The `i` property of this node.
* @param {boolean} autosave
* Whether to automatically save parameter and target changes or not.
*/
constructor(parsoidDocument, originalElement, data, i, autosave = true) {
this.parsoidDocument = parsoidDocument;
this.element = originalElement;
this.data = data;
this.i = i;
this.autosave = autosave;
}
/**
* Gets the target of this node.
* @returns {object} The target of this node, in wikitext and href (for links).
*
* @return {Object} The target of this node, in wikitext and href (for links).
*/
getTarget() {
Line 8,831 ⟶ 12,933:
/**
* Sets the target of this template (in wikitext).
*
* @param {string} wikitext
* The target template (in wikitext, e.g. `Test/{{FULLPAGENAME}}`).
Line 8,851 ⟶ 12,952:
/**
* Gets the parameters of this node.
* @returns {{[key:string]:{wt:string}}} The parameters of this node, in wikitext.
*
* @return {Object.<string, {wt: string}>} The parameters of this node, in wikitext.
*/
getParameters() {
Line 8,859 ⟶ 12,959:
/**
* Checks if a template has a parameter.
*
* @param {string} key The key of the parameter to check.
* @returnreturns {boolean} `true` if the template has the given parameter
*/
hasParameter(key) {
Line 8,868 ⟶ 12,967:
/**
* Gets the value of a parameter.
*
* @param {string} key The key of the parameter to check.
* @returnreturns {string} The parameter value.
*/
getParameter(key) {
Line 8,879 ⟶ 12,977:
* Sets the value for a specific parameter. If `value` is null or undefined,
* the parameter is removed.
*
* @param {string} key The parameter key to set.
* @param {string} value The new value of the parameter.
Line 8,896 ⟶ 12,993:
/**
* Removes a parameter from the template.
*
* @param key The parameter key to remove.
*/
Line 8,923 ⟶ 13,019:
* Removes this node from its element. This will prevent the node from being saved
* again.
*
* @param eraseLine For block templates. Setting this to `true` will also erase a newline
* that immediately succeeds this template, if one exists. This is useful in ensuring that
Line 8,987 ⟶ 13,082:
*/
class ParsoidDocument extends EventTarget {
/**
* Create a new ParsoidDocument instance.
*/
constructor() {
super();
this.iframe = document.createElement('iframe');
Object.assign(this.iframe.style, {
width: '0',
height: '0',
border: '0',
position: 'fixed',
top: '0',
left: '0'
});
this.iframe.addEventListener('load', () => {
if (this.iframe.contentWindow.document.___URL === 'about:blank') {
// Blank document loaded. Ignore.
return;
}
/**
* The document of this ParsoidDocument's IFrame.
*
* @type {Document}
* @protected
*/
this.document = this.iframe.contentWindow.document;
this.$document = $(this.document);
this.setupJquery(this.$document);
this.buildIndex();
if (this.observer) {
// This very much assumes that the MutationObserver is still connected.
// Yes, this is quite an assumption, but should not be a problem during normal use.
// If only MutationObserver had a `.connected` field...
this.observer.disconnect();
}
this.observer = new MutationObserver(() => {
this.buildIndex();
});
this.observer.observe(this.document.getElementsByTagName('body')[0], {
// Listen for ALL DOM mutations.
attributes: true,
childList: true,
subtree: true
});
// Replace the page title. Handles redirects.
if (this.document.title) {
this.page = (mw === null || mw === void 0 ? void 0 : mw.Title) ?
new mw.Title(this.document.title).getPrefixedText() :
this.document.title;
}
});
document.getElementsByTagName('body')[0].appendChild(this.iframe);
}
/**
* Create a new ParsoidDocument instance from a page on-wiki.
*
* @param {string} page The page to load.
* @param {Objectobject} options Options for frame loading.
* @param {boolean} options.reload
* Whether the current page should be discarded and reloaded.
Line 9,057 ⟶ 13,098:
/**
* Create a new ParsoidDocument instance from plain HTML.
*
* @param {string} page The name of the page.
* @param {string} html The HTML to use.
Line 9,073 ⟶ 13,113:
/**
* Creates a new ParsoidDocument from a blank page.
*
* @param {string} page The name of the page.
* @param restBaseUri
Line 9,084 ⟶ 13,123:
/**
* Creates a new ParsoidDocument from wikitext.
*
* @param {string} page The page of the document.
* @param {string} wikitext The wikitext to load.
Line 9,095 ⟶ 13,133:
}
/**
* @returnGet `true`additional ifrequest theoptions pageto isbe apatched redirect.onto `false`RESTBase ifAPI otherwisecalls.
* Extend this class to modify this.
* @protected
*/
getRequestOptions() {
return {
headers: {
'Api-User-Agent': 'parsoid-document/2.0.0 (https://github.com/ChlodAlejandro/parsoid-document; chlod@chlod.net)'
}
};
}
/**
* @returns `true` if the page is a redirect. `false` if otherwise.
*/
get redirect() {
return this.document &&
this.document.querySelector("[rel='mw:PageProp/redirect']") !== null;
}
/**
* Create a new ParsoidDocument instance.
*/
constructor() {
super();
this.iframe = document.createElement('iframe');
Object.assign(this.iframe.style, {
width: '0',
height: '0',
border: '0',
position: 'fixed',
top: '0',
left: '0'
});
this.iframe.addEventListener('load', () => {
if (this.iframe.contentWindow.document.___URL === 'about:blank') {
// Blank document loaded. Ignore.
return;
}
/**
* The document of this ParsoidDocument's IFrame.
* @type {Document}
* @protected
*/
this.document = this.iframe.contentWindow.document;
this.$document = $(this.document);
this.setupJquery(this.$document);
this.buildIndex();
if (this.observer) {
// This very much assumes that the MutationObserver is still connected.
// Yes, this is quite an assumption, but should not be a problem during normal use.
// If only MutationObserver had a `.connected` field...
this.observer.disconnect();
}
this.observer = new MutationObserver(() => {
this.buildIndex();
});
this.observer.observe(this.document.getElementsByTagName('body')[0], {
// Listen for ALL DOM mutations.
attributes: true,
childList: true,
subtree: true
});
// Replace the page title. Handles redirects.
if (this.document.title) {
this.page = (mw === null || mw === void 0 ? void 0 : mw.Title) ?
new mw.Title(this.document.title).getPrefixedText() :
this.document.title;
}
});
document.getElementsByTagName('body')[0].appendChild(this.iframe);
}
/**
* Set up a JQuery object for this window.
*
* @param $doc The JQuery object to set up.
* @returnreturns The JQuery object.
*/
setupJquery($doc) {
Line 9,123 ⟶ 13,224:
/**
* Processes an element and extracts its transclusion parts.
*
* @param {HTMLElement} element Element to process.
* @returnreturns The transclusion parts.
*/
function process(element) {
Line 9,150 ⟶ 13,250:
/**
* Notify the user of a document loading error.
*
* @param {Error} error An error object.
*/
Line 9,175 ⟶ 13,274:
/**
* Loads a wiki page with this ParsoidDocument.
*
* @param {string} page The page to load.
* @param {object} options Options for frame loading.
*
* @param {Object} options Options for frame loading.
* @param {boolean} options.reload
* Whether the current page should be discarded and reloaded.
Line 9,185 ⟶ 13,282:
* @param options.restBaseUri
* A relative or absolute URI to the wiki's RESTBase root. This is
* `/api/rest_` by default, though the `window.restBaseRoot` variable
* can modify it.
* @param options.requestOptions
* Options to pass to the `fetch` request.
Line 9,191 ⟶ 13,290:
*/
async loadPage(page, options = {}) {
var _a, _b, _c;
if (this.document && options.reload !== true) {
throw new Error('Attempted to reload an existing frame.');
}
this.restBaseUri = (_a = options.restBaseUri) !== null && _a !== void 0 ? _a : restBaseRoot;
return fetch(`${this.restBaseUri}v1/page/html/${encodeAPIComponent(page)}?stash=true&redirect=${options.followRedirects !== false ? 'true' : 'false'}&t=${Date.now()}`, Object.assign({}, {
cache: 'no-cache'
}, (_b = optionsthis.requestOptionsgetRequestOptions()) !== null && _b !== void 0 ? _b : {}, (_c = options.requestOptions) !== null && _c !== void 0 ? _c : {}))
.then((data) => {
/**
* The ETag of this iframe's content.
*
* @type {string}
*/
Line 9,222 ⟶ 13,320:
/**
* Load a document from wikitext.
*
* @param {string} page The page title of this document.
* @param {string} wikitext The wikitext to load.
Line 9,228 ⟶ 13,325:
*/
async loadWikitext(page, wikitext, restBaseUri) {
var _a;
this.restBaseUri = restBaseUri !== null && restBaseUri !== void 0 ? restBaseUri : restBaseRoot;
return fetch(`${this.restBaseUri}v1/transform/wikitext/to/html/${encodeAPIComponent(page)}?t=${Date.now()}`, Object.assign((_a = this.getRequestOptions()) !== null && _a !== void 0 ? _a : {}, {
cache: 'no-cache',
method: 'POST',
Line 9,238 ⟶ 13,336:
return formData;
})()
}))
.then((data) => {
/**
* The ETag of this iframe's content.
*
* @type {string}
*/
Line 9,254 ⟶ 13,351:
/**
* Load a document from HTML.
*
* @param {string} page The loaded page's name.
* @param {string} html The page's HTML.
Line 9,333 ⟶ 13,429:
/**
* Gets the `<section>` HTMLElement given a section ID.
*
* @param id The ID of the section
* @returnreturns The HTMLElement of the section. If the section cannot be found, `null`.
*/
getSection(id) {
Line 9,342 ⟶ 13,437:
/**
* Finds a template in the loaded document.
*
* @param {string|RegExp} templateName The name of the template to look for.
* @param {boolean} hrefMode Use the href instead of the wikitext to search for templates.
* @returnreturns {HTMLElement} A list of elements.
*/
findTemplate(templateName, hrefMode = false) {
Line 9,388 ⟶ 13,482:
* Finds the element with the "data-mw" attribute containing the element
* passed into the function.
*
* @param {HTMLElement} element
* The element to find the parent of. This must be a member of the
* ParsoidDocument's document.
* @returnreturns {HTMLElement} The element responsible for showing the given element.
*/
findParsoidNode(element) {
Line 9,408 ⟶ 13,501:
* Get HTML elements that are associated to a specific Parsoid node using its
* `about` attribute.
*
* @param node The node to get the elements of
* @returnreturns All elements that match the `about` of the given node.
*/
getNodeElements(node) {
Line 9,420 ⟶ 13,512:
* This effectively deletes an element, be it a transclusion set, file, section,
* or otherwise.
*
* @param element
*/
Line 9,436 ⟶ 13,527:
/**
* Converts the contents of this document to wikitext.
* @returns {Promise<string>} The wikitext of this document.
*
* @return {Promise<string>} The wikitext of this document.
*/
async toWikitext() {
var _a;
// this.restBaseUri should be set.
let target = `${this.restBaseUri}v1/transform/html/to/wikitext/${encodeAPIComponent(this.page)}`;
Line 9,445 ⟶ 13,536:
target += `/${+(/(\d+)$/.exec(this.document.documentElement.getAttribute('about'))[1])}`;
}
returnconst fetch(target,requestOptions {= this.getRequestOptions();
return fetch(target, Object.assign(requestOptions, {
method: 'POST',
headers: Object.assign((_a = requestOptions.headers) !== null && _a !== void 0 ? _a : {}, { 'If-Match': this.fromExisting ? this.etag : undefined }),
headers: {
'If-Match': this.fromExisting ? this.etag : undefined
},
body: (() => {
const data = new FormData();
Line 9,457 ⟶ 13,547:
return data;
})()
})).then((data) => data.text());
}
/**
* Get the {@link Document} object of this ParsoidDocument.
* @returns {Document} {@link ParsoidDocument#document}
*
* @return {Document} {@link ParsoidDocument#document}
*/
getDocument() {
Line 9,469 ⟶ 13,558:
/**
* Get the JQuery object associated with this ParsoidDocument.
* @returns {*} {@link ParsoidDocument#$document}
*
* @return {*} {@link ParsoidDocument#$document}
*/
getJQuery() {
Line 9,477 ⟶ 13,565:
/**
* Get the IFrame element of this ParsoidDocument.
* @returns {HTMLIFrameElement} {@link ParsoidDocument#iframe}
*
* @return {HTMLIFrameElement} {@link ParsoidDocument#iframe}
*/
getIframe() {
Line 9,485 ⟶ 13,572:
/**
* Get the page name of the currently-loaded page.
* @returns {string} {@link ParsoidDocument#page}
*
* @return {string} {@link ParsoidDocument#page}
*/
getPage() {
Line 9,493 ⟶ 13,579:
/**
* Get the element index of this ParsoidDocument.
* @returns {{ [p: string]: HTMLElement[] }} {@link ParsoidDocument#elementIndex}
*
* @return {Object.<string, HTMLElement[]>} {@link ParsoidDocument#elementIndex}
*/
getElementIndex() {
Line 9,501 ⟶ 13,586:
/**
* Check if this element exists on-wiki or not.
* @returns {boolean} {@link ParsoidDocument#fromExisting}
*
* @return {boolean} {@link ParsoidDocument#fromExisting}
*/
isFromExisting() {
Line 9,511 ⟶ 13,595:
/**
* A blank Parsoid document, with a section 0.
*
* @type {string}
*/
Line 9,517 ⟶ 13,600:
/**
* The default document to create if a page was not found.
*
* @type {string}
*/
Line 9,523 ⟶ 13,605:
// ParsoidDocument:end
var _default = ParsoidDocument_module.default = ParsoidDocument;
 
/**
* Returns the last item of an array.
*
* @param array The array to get the last element from
* @return The last element of the array
*/
function last(array) {
return array[array.length - 1];
}
 
/**
Line 9,586 ⟶ 13,658:
nsId('template') ?
// If in the "Template" namespace, "Copied"
WikiAttributionNotices.attributionNoticeTemplates[type].getNameTextgetMainText() :
// If not in the "Template" namespace, "Namespace:Copied"
WikiAttributionNotices.attributionNoticeTemplates[type].getPrefixedText();
Line 9,698 ⟶ 13,770:
array.splice(0, 0, el);
return array;
}
 
function organize(objects, keyer) {
const finalObj = {};
for (const obj of objects) {
const key = keyer(obj);
if (!finalObj[key]) {
finalObj[key] = [];
}
finalObj[key].push(obj);
}
return finalObj;
}
 
Line 9,740 ⟶ 13,824:
}
});
}
/**
* @inheritDoc
* @protected
*/
getRequestOptions() {
var _a, _b;
const ro = super.getRequestOptions();
return {
headers: {
'Api-User-Agent': `${MwApi.USER_AGENT} ${(_b = (_a = ro.headers) === null || _a === void 0 ? void 0 : _a['Api-User-Agent']) !== null && _b !== void 0 ? _b : ''}`
}
};
}
/**
Line 9,770 ⟶ 13,867:
}
return notices;
}
/**
* Find all notices which have rows using their 'href' fields.
*
* @return All found {@link RowedAttributionNotice}s
*/
findRowedNoticesByHref() {
return organize(this.findNotices().filter(v => v instanceof RowedAttributionNotice), (v) => v.node.getTarget().href);
}
/**
Line 9,986 ⟶ 14,091:
}
 
var blockExit$1 = /*#__PURE__*/Object.freeze({
/**
__proto__: null,
* Opens a temporary window. Use this for dialogs that are immediately destroyed
blockExit: blockExit,
* after running. Do NOT use this for re-openable dialogs, such as the main ANTE
unblockExit: unblockExit
* dialog.
*});
* @param window
* @return A promise. Resolves when the window is closed.
*/
function openWindow(window) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((res) => {
let wm = new OO.ui.WindowManager();
document.getElementsByTagName('body')[0].appendChild(unwrapWidget(wm));
wm.addWindows([window]);
wm.openWindow(window);
wm.on('closing', (win, closed) => {
closed.then(() => {
if (wm) {
const _wm = wm;
wm = null;
removeElement(unwrapWidget(_wm));
_wm.destroy();
res();
}
});
});
});
});
}
 
let InternalCopiedTemplateEditorDialog;
Line 10,072 ⟶ 14,153:
this.renderMenuActions();
this.$body.append(this.layout.$element);
return this;
}
/**
Line 10,148 ⟶ 14,230:
invisibleLabel: true,
label: mw.msg('deputy.ante.mergeAll'),
title: mw.msg('deputy.ante.mergeAll'),
disabled: true
});
// TODO: Repair mergeButton
this.mergeButton.on('click', () => {
const notices = this.parsoid.findNoticeTypefindRowedNoticesByHref('copied');
ifconst noticeCount = Object.values(notices.length > 1) {
return OO.ui.confirmfilter(mw.message('deputy.ante.mergeAll.confirm',v `${notices=> v.length}`).text()).done((confirmed) => {1)
.reduce((p, n) => p + n.length, 0);
return noticeCount ?
dangerModeConfirm(window.CopiedTemplateEditor.config, mw.message('deputy.ante.mergeAll.confirm', `${noticeCount}`).text()).done((confirmed) => {
if (!confirmed) {
return;
}
TemplateMergerfor (const noticeSet of Object.copiedvalues(notices);) {
} TemplateMerger.merge(noticeSet);
}
else { }) :
return// TODO: i18n
OO.ui.alert('There are no templates to merge.');
}
});
const resetButton = new OO.ui.ButtonWidget({
Line 10,174 ⟶ 14,257:
});
resetButton.on('click', () => {
return OOdangerModeConfirm(window.uiCopiedTemplateEditor.confirm(config, mw.msg('deputy.ante.reset.confirm')).done((confirmed) => {
if (confirmed) {
this.loadTalkPage().then(() => {
Line 10,194 ⟶ 14,277:
// Original copied notice count.
const notices = this.parsoid.findNotices();
return OOdangerModeConfirm(window.uiCopiedTemplateEditor.confirm(config, mw.message('deputy.ante.delete.confirm', `${notices.length}`).text()).done((confirmed) => {
if (confirmed) {
for (const notice of notices) {
Line 10,212 ⟶ 14,295:
previewButton.on('click', () => __awaiter(this, void 0, void 0, function* () {
previewButton.setDisabled(true);
yield openWindow(DeputyReviewDialog({
title: normalizeTitle(this.parsoid.getPage()),
from: yield getPageContent(this.parsoid.getPage()),
Line 10,220 ⟶ 14,303:
}));
this.layout.on('remove', () => {
const notices = this.mergeButton.setDisabled(!Object.values(this.parsoid.findNoticesfindRowedNoticesByHref();)
// TODO: Repair mergeButton .some(v => v.length > 1));
// deleteButton.setDisabled(this.mergeButtonparsoid.setDisabledfindNotices( notices).length < 2=== 0);
deleteButton.setDisabled(notices.length === 0);
});
this.parsoid.addEventListener('templateInsert', () => {
const notices = this.mergeButton.setDisabled(!Object.values(this.parsoid.findNoticesfindRowedNoticesByHref();)
// TODO: Repair mergeButton .some(v => v.length > 1));
// deleteButton.setDisabled(this.mergeButtonparsoid.setDisabledfindNotices( notices).length < 2=== 0);
deleteButton.setDisabled(notices.length === 0);
});
this.$overlay.append(new AttributionNoticeAddMenu(this.parsoid, addButton).render());
Line 10,255 ⟶ 14,336:
if (this.parsoid.getPage() !== talkPage) {
// Ask for user confirmation.
yield OOdangerModeConfirm(window.uiCopiedTemplateEditor.confirm(config, mw.message('deputy.ante.loadRedirect.message', talkPage, this.parsoid.getPage()).text(), {
title: mw.msg('deputy.ante.loadRedirect.title'),
actions: [
Line 10,307 ⟶ 14,388:
*/
getReadyProcess() {
var _a;
const process = super.getReadyProcess();
// Recheck state of merge button
this.mergeButton.setDisabled(!Object.values((_a = this.parsoid.findNoticeTypefindRowedNoticesByHref('copied').length) !== null && _a !== void 0 ? _a : 0) < 2);
.some(v => v.length > 1));
process.next(() => {
for (const page of getObjectValues(this.layout.pages)) {
Line 10,334 ⟶ 14,415:
if (unwrapWidget(this.layout)
.querySelector('.oo-ui-flaggedElement-invalid') != null) {
return new OO.ui.Processalert(mw.msg('deputy.ante.invalid')) => {;
return OO.ui.alert(mw.msg('deputy.ante.invalid'))process;
});
}
// Saves the page.
process.next(() => __awaiter(this, void 0, void 0, function* () {
return new mw.Api().postWithEditToken(Object.assign(Object.assign({}, changeTag(yield window.CopiedTemplateEditor.getWikiConfig())), { action: 'edit', format: 'json', formatversion: '2', utf8: 'true', title: this.parsoid.getPage(), text: yield this.parsoid.toWikitext(), summary: decorateEditSummary(mw.msg(this.parsoid.originalCount > 0 ?
return new mw.Api().postWithEditToken({
action: 'edit',
format: 'json',
formatversion: '2',
utf8: 'true',
title: this.parsoid.getPage(),
text: yield this.parsoid.toWikitext(),
summary: decorateEditSummary(mw.msg(this.parsoid.originalCount > 0 ?
'deputy.ante.content.modify' :
'deputy.ante.content.add'), window.CopiedTemplateEditor.config) })).catch((e, c) => {
}).catch( throw errorToOO(e, c);
});
}), this);
// Page redirect
Line 10,386 ⟶ 14,460:
}
},
_a.static = Object.assign(Object.assign({}, OO.ui.ProcessDialog.static), { name: 'copiedTemplateEditorDialog', title: mw.msg('deputy.ante'), size: 'huge', actions: [
_a.static = {
name: 'copiedTemplateEditorDialog',
title: mw.msg('deputy.ante'),
size: 'huge',
actions: [
{
flags: ['primary', 'progressive'],
Line 10,405 ⟶ 14,475:
action: 'close'
}
] }),
},
_a);
}
Line 10,428 ⟶ 14,497:
"deputy.ante.add": "Add a notice",
"deputy.ante.mergeAll": "Merge all notices",
"deputy.ante.mergeAll.confirm": "You are about to merge $1 {{PLURAL:$1|notice|notices}} which support rows. Continue?",
"deputy.ante.reset": "Reset all changes",
"deputy.ante.reset.confirm": "This will reset all changes. Proceed?",
Line 10,462 ⟶ 14,531:
"deputy.ante.merge.button": "Merge",
"deputy.ante.templateOptions": "Template options",
"deputy.ante.dateAuto": "Pull the date from the provided revision ID (`$1` parameter)",
"deputy.ante.dateAuto.invalid": "Parameter does not appear to be a valid revision ID.",
"deputy.ante.dateAuto.failed": "Could not pull date from revision: $1",
"deputy.ante.dateAuto.missing": "The revision $1 could not be found. Its page may have been deleted.",
"deputy.ante.revisionAuto": "Latest",
"deputy.ante.revisionAuto.title": "Pull the revision ID from the latest (current) revision of the page in `$1`.",
"deputy.ante.revisionAuto.failed": "Could not pull revision ID from page: $1",
"deputy.ante.revisionAuto.missing": "The page $1 could not be found. It may have been deleted.",
"deputy.ante.copied.label": "Copied $1",
"deputy.ante.copied.remove": "Remove notice",
Line 10,476 ⟶ 14,553:
"deputy.ante.copied.collapse": "Collapse",
"deputy.ante.copied.small": "Small",
"deputy.ante.copied.convert": "Convert",
"deputy.ante.copied.from.placeholder": "Page A",
"deputy.ante.copied.from.label": "Page copied from",
Line 10,499 ⟶ 14,577:
"deputy.ante.copied.afd.label": "AfD page",
"deputy.ante.copied.afd.help": "The AfD page if the copy was made due to an AfD closed as \"merge\".",
"deputy.ante.copied.date.placeholder": "Date (YYYY-MM-DD)",
"deputy.ante.copied.advanced": "Advanced",
"deputy.ante.copied.dateInvalid": "The previous date value, \"$1\", was not a valid date.",
"deputy.ante.copied.diffDeprecate": "The <code>to_diff</code> and <code>to_oldid</code> parameters are preferred in favor of the <code>diff</code> parameter.",
"deputy.ante.copied.diffDeprecate.warnHost": "The URL in this parameter is not the same as the wiki you're currently editing on. Continue?",
"deputy.ante.copied.diffDeprecate.replace": "The current value of '$1', \"$2\", will be replaced with \"$3\". Continue?",
"deputy.ante.copied.diffDeprecate.failed": "Cannot convert `diff` parameter to URL. See your browser console for more details.",
Line 10,568 ⟶ 14,648:
"deputy.ante.backwardsCopy.entry.date.help": "This is the date on which the article was first published.",
"deputy.ante.backwardsCopy.entry.author.placeholder": "Add author",
"deputy.ante.backwardsCopy.entry.author.label": "Author(s)",
"deputy.ante.backwardsCopy.entry.author.help": "A list of theThe article's authorsauthor.",
"deputy.ante.backwardsCopy.entry.url.placeholder": "https://example.com/news/a-news-article-that-copies-from-wikipedia",
"deputy.ante.backwardsCopy.entry.url.label": "URL",
Line 10,605 ⟶ 14,685:
};
 
var deputySharedEnglish = {
/**
"deputy.name": "Deputy",
* Handles resource fetching operations.
"deputy.description": "Copyright cleanup and case processing tool for Wikipedia.",
*/
"deputy.ia": "Infringement Assistant",
class DeputyResources {
"deputy.ia.short": "I. Assistant",
/**
"deputy.ia.acronym": "Deputy: IA",
* Loads a resource from the provided resource root.
"deputy.ante": "Attribution Notice Template Editor",
*
"deputy.ante.short": "Attrib. Template Editor",
* @param path A path relative to the resource root.
"deputy.ante.acronym": "Deputy: ANTE",
* @return A Promise that resolves to the resource's content as a UTF-8 string.
"deputy.cancel": "Cancel",
*/
"deputy.review": "Review",
static loadResource(path) {
"deputy.review.title": "Review a diff of the changes to be made to the page",
return __awaiter(this, void 0, void 0, function* () {
"deputy.save": "Save",
switch (this.root.type) {
"deputy.close": "Close",
case 'url': {
"deputy.positiveDiff": "+{{FORMATNUM:$1}}",
const headers = new Headers();
"deputy.negativeDiff": "-{{FORMATNUM:$1}}",
headers.set('Origin', window.___location.origin);
"deputy.zeroDiff": "0",
return fetch((new URL(path, this.root.url)).href, {
"deputy.brokenDiff": "?",
method: 'GET',
"deputy.brokenDiff.explain": "The internal parent revision ID for this diff points to a non-existent revision. [[phab:T186280]] has more information.",
headers
"deputy.moreInfo": "More information",
}).then((r) => r.text());
"deputy.dismiss": "Dismiss",
}
"deputy.revision.cur": "cur",
case 'wiki': {
"deputy.revision.prev": "prev",
this.assertApi();
"deputy.revision.cur.tooltip": "Difference with latest revision",
return getPageContent(this.root.prefix.replace(/\/$/, '') + '/' + path, {}, this.api);
"deputy.revision.prev.tooltip": "Difference with preceding revision",
}
"deputy.revision.talk": "talk",
}
"deputy.revision.contribs": "contribs",
});
"deputy.revision.bytes": "{{FORMATNUM:$1}} bytes",
}
"deputy.revision.byteChange": "{{FORMATNUM:$1}} bytes after change of this size",
/**
"deputy.revision.tags": "{{PLURAL:$1|Tag|Tags}}:",
* Ensures that `this.api` is a valid ForeignApi.
"deputy.revision.new": "N",
*/
"deputy.revision.new.tooltip": "This edit created a new page.",
static assertApi() {
"deputy.comma-separator": ", ",
if (this.root.type !== 'wiki') {
"deputy.diff": "Review your changes",
return;
"deputy.diff.load": "Loading changes...",
}
"deputy.diff.no-changes": "No difference",
if (!this.api) {
"deputy.diff.error": "An error occurred while trying to get the comparison.",
this.api = new mw.ForeignApi(this.root.wiki.toString(), {
"deputy.loadError.userConfig": "Due to an error, your Deputy configuration has been reset.",
// Force anonymous mode. Deputy doesn't need user data anyway,
"deputy.loadError.wikiConfig": "An error occurred while loading this wiki's Deputy configuration. Please report this to the Deputy maintainers for this wiki."
// so this should be fine.
anonymous: true
});
}
}
}
/**
* The root of all Deputy resources. This should serve static data that Deputy will
* use to load resources such as language files.
*/
DeputyResources.root = {
type: 'url',
url: new URL('https://zoomiebot.toolforge.org/deputy/')
};
 
/**
* Handles internationalization and localization for Deputy and sub-modules.
*/
class DeputyLanguage {
/**
* Loads the language for this Deputy interface.
*
* @param module The module to load a language pack for.
* @param fallback A fallback language pack to load. Since this is an actual
* `Record`, this relies on the language being bundled with the userscript. This ensures
* that a language pack is always available, even if a language file could not be loaded.
*/
static load(module, fallback) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const lang = (_a = window.deputyLang) !== null && _a !== void 0 ? _a : 'en';
// The loaded language resource file. Forced to `null` if using English, since English
// is our fallback language.
const langResource = lang === 'en' ? null :
yield DeputyResources.loadResource(`i18n/${module}/${lang}.json`)
.catch(() => {
// Could not find requested language file.
return null;
});
if (!langResource) {
// Fall back.
for (const key in fallback) {
mw.messages.set(key, fallback[key]);
}
return;
}
try {
const langData = JSON.parse(langResource);
for (const key in langData) {
mw.messages.set(key, langData[key]);
}
}
catch (e) {
console.error(e);
mw.notify(
// No languages to fall back on. Do not translate this string.
'Deputy: Requested language page is not a valid JSON file.', { type: 'error' });
// Fall back.
for (const key in fallback) {
mw.messages.set(key, fallback[key]);
}
}
if (lang !== mw.config.get('wgUserLanguage')) {
yield DeputyLanguage.loadSecondary();
}
});
}
/**
* Loads a specific moment.js locale. It's possible for nothing to be loaded (e.g. if the
* locale is not supported by moment.js), in which case nothing happens and English is
* likely used.
*
* @param locale The locale to load. `window.deputyLang` by default.
*/
static loadMomentLocale(locale = window.deputyLang) {
return __awaiter(this, void 0, void 0, function* () {
if (locale === 'en') {
// Always loaded.
return;
}
if (window.moment.locales().indexOf(locale) !== -1) {
// Already loaded.
return;
}
yield mw.loader.using('moment')
.then(() => true, () => null);
yield mw.loader.getScript(new URL(`resources/lib/moment/locale/${locale}.js`, new URL(mw.util.wikiScript('index'), window.___location.href)).toString()).then(() => true, () => null);
});
}
/**
* There are times when the user interface language do not match the wiki content
* language. Since Deputy's edit summary and content strings are found in the
* i18n files, however, there are cases where the wrong language would be used.
*
* This solves this problem by manually overriding content-specific i18n keys with
* the correct language. By default, all keys that match `deputy.*.content.**` get
* overridden.
*
* There are no fallbacks for this. If it fails, the user interface language is
* used anyway. In the event that the user interface language is not English,
* this will cause awkward situations. Whether or not something should be done to
* catch this specific edge case will depend on how frequent it happens.
*
* @param locale
* @param match
*/
static loadSecondary(locale = mw.config.get('wgContentLanguage'), match = /^deputy\.(?:[^.]+)?\.content\./g) {
return __awaiter(this, void 0, void 0, function* () {
// The loaded language resource file. Forced to `null` if using English, since English
// is our fallback language.
const langResource = locale === 'en' ? null :
yield DeputyResources.loadResource(`i18n/${module}/${locale}.json`)
.catch(() => {
// Could not find requested language file.
return null;
});
if (!langResource) {
return;
}
try {
const langData = JSON.parse(langResource);
for (const key in langData) {
if (cloneRegex$1(match).test(key)) {
mw.messages.set(key, langData[key]);
}
}
}
catch (e) {
// Silent failure.
console.error('Deputy: Requested language page is not a valid JSON file.', e);
}
});
}
}
 
/**
* Refers to a specific setting on the configuration. Should be initialized with
* a raw (serialized) type and an actual (deserialized) type.
*
* This is used for both client and wiki-wide configuration.
*/
class Setting {
/**
*
* @param options
* @param options.serialize Serialization function. See {@link Setting#serialize}
* @param options.deserialize Deserialization function. See {@link Setting#deserialize}
* @param options.alwaysSave See {@link Setting#alwaysSave}.
* @param options.defaultValue Default value. If not supplied, `undefined` is used.
* @param options.displayOptions See {@link Setting#displayOptions}
* @param options.allowedValues See {@link Setting#allowedValues}
*/
constructor(options) {
this.serialize = options.serialize;
this.deserialize = options.deserialize;
this.displayOptions = options.displayOptions;
this.allowedValues = options.allowedValues;
this.value = this.defaultValue = options.defaultValue;
this.alwaysSave = options.alwaysSave;
}
/**
* @return if this option is disabled or not.
*/
get disabled() {
return typeof this.displayOptions.disabled !== 'function' ?
this.displayOptions.disabled :
this.displayOptions.disabled.call(this);
}
/**
* @return if this option is hidden or not.
*/
get hidden() {
return typeof this.displayOptions.hidden !== 'function' ?
this.displayOptions.hidden :
this.displayOptions.hidden.call(this);
}
/**
* @return `true` if `this.value` is not null or undefined.
*/
ok() {
return this.value != null;
}
/**
* @return The current value of this setting.
*/
get() {
return this.value;
}
/**
* Sets the value and performs validation. If the input is an invalid value, and
* `throwOnInvalid` is false, the value will be reset to default.
*
* @param v
* @param throwOnInvalid
*/
set(v, throwOnInvalid = false) {
if (this.allowedValues) {
const keys = Array.isArray(this.allowedValues) ?
this.allowedValues : getObjectValues(this.allowedValues);
if (Array.isArray(v)) {
if (v.some((v1) => keys.indexOf(v1) === -1)) {
if (throwOnInvalid) {
throw new Error('Invalid value');
}
v = this.value;
}
}
else {
if (this.allowedValues && keys.indexOf(v) === -1) {
if (throwOnInvalid) {
throw new Error('Invalid value');
}
v = this.value;
}
}
}
this.value = v;
}
/**
* Resets this setting to its original value.
*/
reset() {
this.set(this.defaultValue);
}
/**
* Parses a given raw value and mutates the setting.
*
* @param raw The raw value to parse.
* @return The new value.
*/
load(raw) {
return (this.value = this.deserialize(raw));
}
}
Setting.basicSerializers = {
serialize: (value) => value,
deserialize: (value) => value
};
 
/**
* Works like `Object.fromEntries`
*
* @param obj The object to get the values of.
* @return The values of the given object as an array
*/
function fromObjectEntries(obj) {
const i = {};
for (const [key, value] of obj) {
i[key] = value;
}
return i;
}
 
/**
* Generates configuration properties for serialized <b>string</b> enums.
*
* Trying to use anything that isn't a string enum here (union enum, numeral enum)
* will likely cause serialization/deserialization failures.
*
* @param _enum
* @param defaultValue
* @return Setting properties.
*/
function generateEnumConfigurationProperties(_enum, defaultValue) {
return {
serialize: (value) => value === defaultValue ? undefined : value,
deserialize: (value) => value,
displayOptions: {
type: 'radio'
},
allowedValues: fromObjectEntries(Array.from(new Set(Object.keys(_enum)).values())
.map((v) => [_enum[v], _enum[v]])),
defaultValue: defaultValue
};
}
var PortletNameView;
(function (PortletNameView) {
PortletNameView["Full"] = "full";
PortletNameView["Short"] = "short";
PortletNameView["Acronym"] = "acronym";
})(PortletNameView || (PortletNameView = {}));
 
var CompletionAction;
(function (CompletionAction) {
CompletionAction["Nothing"] = "nothing";
CompletionAction["Reload"] = "reload";
})(CompletionAction || (CompletionAction = {}));
var TripleCompletionAction;
(function (TripleCompletionAction) {
TripleCompletionAction["Nothing"] = "nothing";
TripleCompletionAction["Reload"] = "reload";
TripleCompletionAction["Redirect"] = "redirect";
})(TripleCompletionAction || (TripleCompletionAction = {}));
 
/**
* A configuration. Defines settings and setting groups.
*/
class ConfigurationBase {
/**
* Creates a new Configuration.
*/
constructor() { }
// eslint-disable-next-line jsdoc/require-returns-check
/**
* @return the configuration from the current wiki.
*/
static load() {
throw new Error('Unimplemented method.');
}
/**
* Deserializes a JSON configuration into this configuration. This WILL overwrite
* past settings.
*
* @param serializedData
*/
deserialize(serializedData) {
var _a;
for (const group in this.all) {
const groupObject = this.all[group];
for (const key in this.all[group]) {
const setting = groupObject[key];
if (((_a = serializedData === null || serializedData === void 0 ? void 0 : serializedData[group]) === null || _a === void 0 ? void 0 : _a[key]) !== undefined) {
setting.set(setting.deserialize ?
// Type-checked upon declaration, just trust it to skip errors.
setting.deserialize(serializedData[group][key]) :
serializedData[group][key]);
}
}
}
}
/**
* @return the serialized version of the configuration. All `undefined` values are stripped
* from output. If a category remains unchanged from defaults, it is skipped. If the entire
* configuration remains unchanged, `null` is returned.
*/
serialize() {
const config = {};
for (const group of Object.keys(this.all)) {
const groupConfig = {};
const groupObject = this.all[group];
for (const key in this.all[group]) {
const setting = groupObject[key];
if (setting.get() === setting.defaultValue && !setting.alwaysSave) {
continue;
}
const serialized = setting.serialize ?
// Type-checked upon declaration, just trust it to skip errors.
setting.serialize(setting.get()) : setting.get();
if (serialized !== undefined) {
groupConfig[key] = serialized;
}
}
if (Object.keys(groupConfig).length > 0) {
config[group] = groupConfig;
}
}
if (Object.keys(config).length > 0) {
return config;
}
else {
return null;
}
}
}
ConfigurationBase.configVersion = 1;
 
/**
* A configuration. Defines settings and setting groups.
*/
class UserConfiguration extends ConfigurationBase {
/**
* Creates a new Configuration.
*
* @param serializedData
*/
constructor(serializedData = {}) {
super();
this.core = {
/**
* Numerical code that identifies this config version. Increments for every breaking
* configuration file change.
*/
configVersion: new Setting({
defaultValue: UserConfiguration.configVersion,
displayOptions: { hidden: true },
alwaysSave: true
}),
language: new Setting({
defaultValue: mw.config.get('wgUserLanguage'),
displayOptions: { type: 'select' }
}),
modules: new Setting({
defaultValue: ['cci', 'ante', 'ia'],
displayOptions: { type: 'checkboxes' },
allowedValues: ['cci', 'ante', 'ia']
}),
portletNames: new Setting(generateEnumConfigurationProperties(PortletNameView, PortletNameView.Full))
};
this.cci = {
enablePageToolbar: new Setting({
defaultValue: true,
displayOptions: {
type: 'checkbox'
}
}),
forceUtc: new Setting({
defaultValue: false,
displayOptions: {
type: 'checkbox'
}
}),
signingBehavior: new Setting(generateEnumConfigurationProperties(ContributionSurveyRowSigningBehavior, ContributionSurveyRowSigningBehavior.Always))
};
this.ante = {
enableAutoMerge: new Setting({
defaultValue: false,
displayOptions: {
type: 'checkbox',
disabled: 'unimplemented'
}
}),
onSubmit: new Setting(generateEnumConfigurationProperties(CompletionAction, CompletionAction.Reload))
};
this.ia = {
responses: new Setting(Object.assign(Object.assign({}, Setting.basicSerializers), { defaultValue: null, displayOptions: {
disabled: 'unimplemented',
type: 'unimplemented'
} })),
enablePageToolbar: new Setting({
defaultValue: true,
displayOptions: {
type: 'checkbox',
disabled: 'unimplemented'
}
}),
defaultEntirePage: new Setting({
defaultValue: true,
displayOptions: {
type: 'checkbox'
}
}),
defaultFromUrls: new Setting({
defaultValue: true,
displayOptions: {
type: 'checkbox'
}
}),
onHide: new Setting(generateEnumConfigurationProperties(TripleCompletionAction, TripleCompletionAction.Reload)),
onSubmit: new Setting(generateEnumConfigurationProperties(TripleCompletionAction, TripleCompletionAction.Reload)),
onBatchSubmit: new Setting(generateEnumConfigurationProperties(CompletionAction, CompletionAction.Reload))
};
this.all = { core: this.core, cci: this.cci, ante: this.ante, ia: this.ia };
if (serializedData) {
this.deserialize(serializedData);
}
if (mw.storage.get(`mw-${UserConfiguration.optionKey}-lastVersion`) !== deputyVersion) ;
mw.storage.set(`mw-${UserConfiguration.optionKey}-lastVersion`, deputyVersion);
}
/**
* @return the configuration from the current wiki.
*/
static load() {
const config = new UserConfiguration();
try {
if (mw.user.options.get(UserConfiguration.optionKey)) {
const decodedOptions = JSON.parse(mw.user.options.get(UserConfiguration.optionKey));
config.deserialize(decodedOptions);
}
}
catch (e) {
console.error(e, mw.user.options.get(UserConfiguration.optionKey));
mw.hook('deputy.i18nDone').add(function notifyConfigFailure() {
mw.notify(mw.msg('deputy.loadError.userConfig'), {
type: 'error'
});
mw.hook('deputy.i18nDone').remove(notifyConfigFailure);
});
config.save();
}
return config;
}
/**
* Saves the configuration.
*/
save() {
return __awaiter(this, void 0, void 0, function* () {
yield MwApi.action.saveOption(UserConfiguration.optionKey, JSON.stringify(this.serialize()));
});
}
}
UserConfiguration.configVersion = 1;
UserConfiguration.optionKey = 'userjs-deputy';
 
var deputySettingsStyles = ".deputy-setting {margin-bottom: 1em;}.deputy-setting > .oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-header .oo-ui-labelElement-label {font-weight: bold;}.dp-mb {margin-bottom: 1em;}.deputy-about {display: flex;}.deputy-about > :first-child {flex: 0;}.deputy-about > :first-child > img {height: 5em;width: auto;}.ltr .deputy-about > :first-child {margin-right: 1em;}.rtl .deputy-about > :first-child {margin-left: 1em;}.deputy-about > :nth-child(2) {flex: 1;}.deputy-about > :nth-child(2) > :first-child {font-weight: bold;font-size: 2em;}.deputy-about > :nth-child(2) > :not(:first-child) {margin-top: 0.5em;}.ltr .deputy-about > :nth-child(2) > :last-child > :not(:last-child) {margin-right: 0.5em;}.rtl .deputy-about > :nth-child(2) > :last-child > :not(:last-child) {margin-left: 0.5em;}.ltr .deputy-about > :nth-child(2) > :last-child {text-align: right;}.rtl .deputy-about > :nth-child(2) > :last-child {text-align: left;}";
 
/* eslint-disable mediawiki/msg-doc */
let InternalConfigurationGroupTabPanel$1;
/**
* Initializes the process element.
*/
function initConfigurationGroupTabPanel$1() {
InternalConfigurationGroupTabPanel$1 = class ConfigurationGroupTabPanel extends OO.ui.TabPanelLayout {
/**
* @param config Configuration to be passed to the element.
*/
constructor(config) {
super(`configurationGroupPage_${config.group}`);
this.config = config;
this.mode = config.config instanceof UserConfiguration ? 'user' : 'wiki';
if (this.mode === 'wiki') {
this.$element.append(new OO.ui.MessageWidget({
classes: [
'deputy', 'dp-mb'
],
type: 'warning',
label: mw.msg('deputy.settings.dialog.wikiConfigWarning')
}).$element);
}
for (const settingKey of Object.keys(this.settings)) {
const setting = this.settings[settingKey];
if (setting.hidden) {
continue;
}
switch (setting.displayOptions.type) {
case 'checkbox':
this.$element.append(this.newCheckboxField(settingKey, setting));
break;
case 'checkboxes':
this.$element.append(this.newCheckboxesField(settingKey, setting));
break;
case 'radio':
this.$element.append(this.newRadioField(settingKey, setting));
break;
case 'text':
this.$element.append(this.newStringField(settingKey, setting));
break;
case 'page':
this.$element.append(this.newPageField(settingKey, setting));
break;
case 'code':
this.$element.append(this.newCodeField(settingKey, setting));
break;
default:
this.$element.append(this.newUnimplementedField(settingKey));
break;
}
}
this.on('change', () => {
console.log(this.config.config);
console.log(this.config.config.serialize());
});
}
/**
* @return The {@Link Setting}s for this group.
*/
get settings() {
return this.config.config.all[this.config.group];
}
/**
* Sets up the tab item
*/
setupTabItem() {
this.tabItem.setLabel(this.getMsg(this.config.group));
}
/**
* @return the i18n message for this setting tab.
*
* @param messageKey
*/
getMsg(messageKey) {
return mw.msg(`deputy.setting.${this.mode}.${messageKey}`);
}
/**
* Gets the i18n message for a given setting.
*
* @param settingKey
* @param key
* @return A localized string
*/
getSettingMsg(settingKey, key) {
return this.getMsg(`${this.config.group}.${settingKey}.${key}`);
}
/**
* @param settingKey
* @param allowedValues
* @return a tuple array of allowed values that can be used in OOUI `items` parameters.
*/
getAllowedValuesArray(settingKey, allowedValues) {
const items = [];
if (Array.isArray(allowedValues)) {
for (const key of allowedValues) {
const message = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.${key}`);
items.push([key, message.exists() ? message.text() : key]);
}
}
else {
for (const key of Object.keys(allowedValues)) {
const message = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.${key}`);
items.push([key, message.exists() ? message.text() : key]);
}
}
return items;
}
/**
* Creates an unimplemented setting notice.
*
* @param settingKey
* @return An HTMLElement of the given setting's field.
*/
newUnimplementedField(settingKey) {
const desc = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.description`);
return h_1("div", { class: "deputy-setting" },
h_1("b", null, this.getSettingMsg(settingKey, 'name')),
desc.exists() ? h_1("p", { style: { fontSize: '0.925em', color: '#54595d' } }, desc.text()) : '',
h_1("p", null, mw.msg('deputy.settings.dialog.unimplemented')));
}
/**
* Creates a checkbox field.
*
* @param settingKey
* @param setting
* @return An HTMLElement of the given setting's field.
*/
newCheckboxField(settingKey, setting) {
const isDisabled = setting.disabled;
const desc = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.description`);
const field = new OO.ui.CheckboxInputWidget({
selected: setting.get(),
disabled: isDisabled !== undefined && isDisabled !== false
});
const layout = new OO.ui.FieldLayout(field, {
align: 'inline',
label: this.getSettingMsg(settingKey, 'name'),
help: typeof isDisabled === 'string' ?
this.getSettingMsg(settingKey, isDisabled) :
desc.exists() ? desc.text() : undefined,
helpInline: true
});
field.on('change', () => {
setting.set(field.isSelected());
this.emit('change');
});
// Attach disabled re-checker
this.on('change', () => {
field.setDisabled(setting.disabled);
});
return h_1("div", { class: "deputy-setting" }, unwrapWidget(layout));
}
/**
* Creates a new checkbox set field.
*
* @param settingKey
* @param setting
* @return An HTMLElement of the given setting's field.
*/
newCheckboxesField(settingKey, setting) {
const isDisabled = setting.disabled;
const desc = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.description`);
const field = new OO.ui.CheckboxMultiselectInputWidget({
value: setting.get(),
disabled: isDisabled !== undefined && isDisabled !== false,
options: this.getAllowedValuesArray(settingKey, setting.allowedValues)
.map(([key, label]) => ({ data: key, label }))
});
const layout = new OO.ui.FieldLayout(field, {
align: 'top',
label: this.getSettingMsg(settingKey, 'name'),
help: typeof isDisabled === 'string' ?
this.getSettingMsg(settingKey, isDisabled) :
desc.exists() ? desc.text() : undefined,
helpInline: true
});
field.on('change', (items) => {
const finalData = Array.isArray(setting.allowedValues) ?
items :
field.findSelectedItemsData().map((v) => setting.allowedValues[v]);
setting.set(finalData);
this.emit('change');
});
// Attach disabled re-checker
this.on('select', () => {
field.setDisabled(setting.disabled);
});
return h_1("div", { class: "deputy-setting" }, unwrapWidget(layout));
}
/**
* Creates a new radio set field.
*
* @param settingKey
* @param setting
* @return An HTMLElement of the given setting's field.
*/
newRadioField(settingKey, setting) {
const isDisabled = setting.disabled;
const desc = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.description`);
const field = new OO.ui.RadioSelectWidget({
disabled: isDisabled !== undefined && isDisabled !== false,
items: this.getAllowedValuesArray(settingKey, setting.allowedValues)
.map(([key, label]) => new OO.ui.RadioOptionWidget({
data: key,
label: label,
selected: setting.get() === key
})),
multiselect: false
});
const layout = new OO.ui.FieldLayout(field, {
align: 'top',
label: this.getSettingMsg(settingKey, 'name'),
help: typeof isDisabled === 'string' ?
this.getSettingMsg(settingKey, isDisabled) :
desc.exists() ? desc.text() : undefined,
helpInline: true
});
// OOUIRadioInputWidget
field.on('select', (items) => {
const finalData = Array.isArray(setting.allowedValues) ?
items.data :
setting.allowedValues[items.data];
setting.set(finalData);
this.emit('change');
});
// Attach disabled re-checker
this.on('change', () => {
field.setDisabled(setting.disabled);
});
return h_1("div", { class: "deputy-setting" }, unwrapWidget(layout));
}
/**
* Creates a new field that acts like a string field.
*
* @param FieldClass
* @param settingKey
* @param setting
* @param extraFieldOptions
* @return A Deputy setting field
*/
newStringLikeField(FieldClass, settingKey, setting, extraFieldOptions = {}) {
var _a, _b;
const isDisabled = setting.disabled;
const desc = mw.message(`deputy.setting.${this.mode}.${this.config.group}.${settingKey}.description`);
const field = new FieldClass(Object.assign({ value: (_b = (_a = setting.serialize) === null || _a === void 0 ? void 0 : _a.call(setting, setting.get())) !== null && _b !== void 0 ? _b : setting.get(), disabled: isDisabled !== undefined && isDisabled !== false }, extraFieldOptions));
const layout = new OO.ui.FieldLayout(field, {
align: 'top',
label: this.getSettingMsg(settingKey, 'name'),
help: typeof isDisabled === 'string' ?
this.getSettingMsg(settingKey, isDisabled) :
desc.exists() ? desc.text() : undefined,
helpInline: true
});
field.on('change', (value) => {
setting.set(value);
this.emit('change');
});
// Attach disabled re-checker
this.on('change', () => {
field.setDisabled(setting.disabled);
});
return h_1("div", { class: "deputy-setting" }, unwrapWidget(layout));
}
/**
* Creates a new string setting field.
*
* @param settingKey
* @param setting
* @return An HTMLElement of the given setting's field.
*/
newStringField(settingKey, setting) {
return this.newStringLikeField(OO.ui.TextInputWidget, settingKey, setting);
}
/**
* Creates a new page title setting field.
*
* @param settingKey
* @param setting
* @return An HTMLElement of the given setting's field.
*/
newPageField(settingKey, setting) {
return this.newStringLikeField(mw.widgets.TitleInputWidget, settingKey, setting);
}
/**
* Creates a new code setting field.
*
* @param settingKey
* @param setting
* @return An HTMLElement of the given setting's field.
*/
newCodeField(settingKey, setting) {
return this.newStringLikeField(OO.ui.MultilineTextInputWidget, settingKey, setting);
}
};
}
/**
* Creates a new ConfigurationGroupTabPanel.
*
* @param config Configuration to be passed to the element.
* @return A ConfigurationGroupTabPanel object
*/
function ConfigurationGroupTabPanel (config) {
if (!InternalConfigurationGroupTabPanel$1) {
initConfigurationGroupTabPanel$1();
}
return new InternalConfigurationGroupTabPanel$1(config);
}
 
var deputySettingsEnglish = {
"deputy.about": "About",
"deputy.about.homepage": "Homepage",
"deputy.about.openSource": "Source",
"deputy.about.contact": "Contact",
"deputy.about.credit": "Deputy was made with the help of the English Wikipedia Copyright Cleanup WikiProject and the Wikimedia Foundation.",
"deputy.about.license": "Deputy is licensed under the <a href=\"$1\">Apache License 2.0</a>. The source code for Deputy is available on <a href=\"$2\">GitHub</a>, and is free for everyone to view and suggest changes.",
"deputy.about.thirdParty": "Deputy is bundled with third party libraries to make development easier. All libraries have been vetted for user security and license compatibility. For more information, see the <a href=\"$1\">\"Licensing\"</a> section on Deputy's README.",
"deputy.about.footer": "Made with love, coffee, and the tears of copyright editors.",
"deputy.settings.portlet": "Deputy preferences",
"deputy.settings.portlet.tooltip": "Opens a dialog to modify Deputy preferences",
"deputy.settings.wikiEditIntro.title": "This is a Deputy configuration page",
"deputy.settings.wikiEditIntro.current": "Deputy's active configuration comes from this page. Changing this page will affect the settings of all Deputy users on this wiki. Edit responsibly, and avoid making significant changes without prior discussion.",
"deputy.settings.wikiEditIntro.other": "This is a valid Deputy configuration page, but the configuration is currently being loaded from {{wikilink:$1}}. If this becomes the active configuration page, changing it will affect the settings of all Deputy users on this wiki. Edit responsibly, and avoid making significant changes without prior discussion.",
"deputy.settings.wikiEditIntro.edit.current": "Modify configuration",
"deputy.settings.wikiEditIntro.edit.other": "Modify this configuration",
"deputy.settings.wikiEditIntro.edit.otherCurrent": "Modify the active configuration",
"deputy.settings.wikiEditIntro.edit.protected": "This page's protection settings do not allow you to edit the page.",
"deputy.settings.wikiOutdated": "Outdated configuration",
"deputy.settings.wikiOutdated.help": "Deputy has detected a change in this wiki's configuration for all Deputy users. We've automatically downloaded the changes for you, but you have to reload to apply the changes.",
"deputy.settings.wikiOutdated.reload": "Reload",
"deputy.settings.dialog.title": "Deputy Preferences",
"deputy.settings.dialog.unimplemented": "A way to modify this setting has not yet been implemented. Check back later!",
"deputy.settings.saved": "Preferences saved. Please refresh the page to see changes.",
"deputy.settings.dialog.wikiConfigWarning": "You are currently editing a wiki-wide Deputy configuration page. Changes made to this page may affect the settings of all Deputy users on this wiki. Edit responsibly, and avoid making significant changes without prior discussion.",
"deputy.setting.user.core": "Deputy",
"deputy.setting.user.core.language.name": "Language",
"deputy.setting.user.core.language.description": "Deputy's interface language. English (US) is used by default, and is used as a fallback if no translations are available. If the content of the wiki you work on is in a different language from the interface language, Deputy will need to load additional data to ensure edit summaries, text, etc., saved on-wiki match the wiki's content language. For this reason, we suggest keeping the interface language the same as the wiki's content language.",
"deputy.setting.user.core.modules.name": "Modules",
"deputy.setting.user.core.modules.description": "Choose the enabled Deputy modules. By default, all modules are enabled.\nDisabling specific modules won't make Deputy load faster, but it can remove\nUI features added by Deputy which may act as clutter when unused.",
"deputy.setting.user.core.modules.cci": "Contributor Copyright Investigations",
"deputy.setting.user.core.modules.ante": "{{int:deputy.ante}}",
"deputy.setting.user.core.modules.ia": "{{int:deputy.ia}}",
"deputy.setting.user.core.portletNames.name": "Portlet names",
"deputy.setting.user.core.portletNames.description": "Choose which names appear in the Deputy portlet (toolbox) links.",
"deputy.setting.user.core.portletNames.full": "Full names (e.g. Attribution Notice Template Editor)",
"deputy.setting.user.core.portletNames.short": "Shortened names (e.g. Attrib. Template Editor)",
"deputy.setting.user.core.portletNames.acronym": "Acronyms (e.g. ANTE)",
"deputy.setting.user.cci": "CCI",
"deputy.setting.user.cci.enablePageToolbar.name": "Enable page toolbar",
"deputy.setting.user.cci.enablePageToolbar.description": "Enables the page toolbar, which is used to quickly show tools, analysis options, and related case information on a page that is the subject of a CCI investigation.",
"deputy.setting.user.cci.enablePageToolbar.unimplemented": "This feature has not yet been implemented.",
"deputy.setting.user.cci.forceUtc.name": "Force UTC time",
"deputy.setting.user.cci.forceUtc.description": "Forces Deputy to use UTC time whenever displaying dates and times, irregardless of your system's timezone or your MediaWiki time settings.",
"deputy.setting.user.cci.signingBehavior.name": "Row signing behavior",
"deputy.setting.user.cci.signingBehavior.description": "Choose how Deputy should behave when signing rows. By default, all rows are always signed with your signature (~~~~). You may configure Deputy to only sign the last row or never sign. You can also configure Deputy to leave a hidden trace behind (<!-- User:Example|2016-05-28T14:32:12Z -->), which helps Deputy (for other users) determine who assessed a row.",
"deputy.setting.user.cci.signingBehavior.always": "Always sign rows",
"deputy.setting.user.cci.signingBehavior.alwaysTrace": "Always leave a trace",
"deputy.setting.user.cci.signingBehavior.alwaysTraceLastOnly": "Always leave a trace, but sign the last row modified",
"deputy.setting.user.cci.signingBehavior.lastOnly": "Only sign the last row modified (prevents assessor recognition)",
"deputy.setting.user.cci.signingBehavior.never": "Never sign rows (prevents assessor recognition)",
"deputy.setting.user.ante": "ANTE",
"deputy.setting.user.ante.enableAutoMerge.name": "Merge automatically on run",
"deputy.setting.user.ante.enableAutoMerge.description": "If enabled, templates that can be merged will automatically be merged when the dialog opens.",
"deputy.setting.user.ante.enableAutoMerge.unimplemented": "This feature has not yet been implemented.",
"deputy.setting.user.ante.onSubmit.name": "Action on submit",
"deputy.setting.user.ante.onSubmit.description": "Choose what to do after editing attribution notice templates.",
"deputy.setting.user.ante.onSubmit.nothing": "Do nothing",
"deputy.setting.user.ante.onSubmit.reload": "Reload the page",
"deputy.setting.user.ia": "IA",
"deputy.setting.user.ia.responses.name": "Custom responses",
"deputy.setting.user.ia.responses.description": "A custom set of responses, or overrides for existing responses. If an entry\nwith the same key on both the wiki-wide configuration and the user configuration\nexists, the user configuration will override the wiki-wide configuration. Wiki-wide configuration responses can also be disabled locally. If this setting is empty, no overrides are made.",
"deputy.setting.user.ia.enablePageToolbar.name": "Enable page toolbar",
"deputy.setting.user.ia.enablePageToolbar.description": "If enabled, the page toolbar will be shown when dealing with CP cases. The IA page toolbar works slightly differently from the CCI page toolbar. Namely, it shows a button for responding instead of a status dropdown.",
"deputy.setting.user.ia.enablePageToolbar.unimplemented": "This feature has not yet been implemented.",
"deputy.setting.user.ia.defaultEntirePage.name": "Hide entire page by default",
"deputy.setting.user.ia.defaultEntirePage.description": "If enabled, the Infringement Assistant reporting window will hide the entire page by default.",
"deputy.setting.user.ia.defaultFromUrls.name": "Use URLs by default",
"deputy.setting.user.ia.defaultFromUrls.description": "If enabled, the Infringement Assistant reporting window will use URL inputs by default.",
"deputy.setting.user.ia.onHide.name": "Action on hide",
"deputy.setting.user.ia.onHide.description": "Choose what to do after the \"Hide content only\" button is selected and the relevant content is hidden from the page.",
"deputy.setting.user.ia.onHide.nothing": "Do nothing",
"deputy.setting.user.ia.onHide.reload": "Reload the page",
"deputy.setting.user.ia.onHide.redirect": "Redirect to the noticeboard page",
"deputy.setting.user.ia.onSubmit.name": "Action on submit",
"deputy.setting.user.ia.onSubmit.description": "Choose what to do after the \"Submit\" button is selected, the relevant content is hidden from the page, and the page is reported.",
"deputy.setting.user.ia.onSubmit.nothing": "Do nothing",
"deputy.setting.user.ia.onSubmit.reload": "Reload the page",
"deputy.setting.user.ia.onSubmit.redirect": "Redirect to the noticeboard page",
"deputy.setting.user.ia.onBatchSubmit.name": "Action on batch listing submit",
"deputy.setting.user.ia.onBatchSubmit.description": "When reporting a batch of pages, choose what to do after the \"Report\" button is selected and the pages are reported.",
"deputy.setting.user.ia.onBatchSubmit.nothing": "Do nothing",
"deputy.setting.user.ia.onBatchSubmit.reload": "Reload the noticeboard page",
"deputy.setting.wiki.cci": "CCI",
"deputy.setting.wiki.cci.enabled.name": "Enable contributor copyright investigations assistant",
"deputy.setting.wiki.cci.enabled.description": "Enables the CCI workflow assistant. This allows Deputy to replace the contribution survey found on CCI case pages with a graphical interface which works with other tabs to make the CCI workflow easier.",
"deputy.setting.wiki.cci.rootPage.name": "Root page",
"deputy.setting.wiki.cci.rootPage.description": "The main page that holds all subpages containing valid contribution copyright investigation cases.",
"deputy.setting.wiki.cci.collapseTop.name": "Collapsible wikitext (top)",
"deputy.setting.wiki.cci.collapseTop.description": "Placed just below a section heading when closing a contributor survey section. Use \"$1\" to denote user comments and signature. On the English Wikipedia, this is {{collapse top}}. Other wikis may have an equivalent template. This should go hand in hand with \"{{int:deputy.setting.wiki.cci.collapseBottom.name}}\", as they are used as a pair.",
"deputy.setting.wiki.cci.collapseBottom.name": "Collapsible wikitext (bottom)",
"deputy.setting.wiki.cci.collapseBottom.description": "Placed at the end of a section when closing a contributor survey section. On the English Wikipedia, this is {{collapse bottom}}. Other wikis may have an equivalent template.",
"deputy.setting.wiki.ante": "ANTE",
"deputy.setting.wiki.ante.enabled.name": "Enable the Attribution Notice Template Editor",
"deputy.setting.wiki.ante.enabled.description": "Enables ANTE for all users. ANTE is currently the least-optimized module for localization, and may not work for all wikis.",
"deputy.setting.wiki.ia": "IA",
"deputy.setting.wiki.ia.enabled.name": "Enable the Infringement Assistant",
"deputy.setting.wiki.ia.enabled.description": "Enables IA for all users. IA allows users to easily and graphically report pages with suspected or complicated copyright infringements.",
"deputy.setting.wiki.ia.rootPage.name": "Root page",
"deputy.setting.wiki.ia.rootPage.description": "The root page for Infringement Assistant. This should be the copyright problems noticeboard for this specific wiki. IA will only show quick response links for the root page and its subpages.",
"deputy.setting.wiki.ia.subpageFormat.name": "Subpage format",
"deputy.setting.wiki.ia.subpageFormat.description": "The format to use for subpages of the root page. This is a moment.js format string.",
"deputy.setting.wiki.ia.preload.name": "Preload",
"deputy.setting.wiki.ia.preload.description": "Defines the page content to preload the page with if a given subpage does not exist yet. This should be an existing page on-wiki. Leave blank to avoid using a preload entirely.",
"deputy.setting.wiki.ia.listingWikitext": "Listing wikitext",
"deputy.setting.wiki.ia.listingWikitext.description": "Defines the wikitext that will be used when adding listings to a noticeboard page. You may use \"$1\" to denote the page being reported, and \"$2\" for user comments (which shouldn't contain the signature).",
"deputy.setting.wiki.ia.listingWikitextMatch.name": "Regular expression for listings",
"deputy.setting.wiki.ia.listingWikitextMatch.description": "A regular expression that will be used to parse and detect listings on a given noticeboard page. Since its usage is rather technical, this value should be edited by someone with technical knowledge of regular expressions. This regular expression must provide three captured groups: group \"$1\" will catch any bullet point, space, or prefix, \"$2\" will catch the page title ONLY if the given page matches \"{{int:deputy.setting.wiki.ia.listingWikitext.name}}\" or \"{{int:deputy.setting.wiki.ia.batchListingWikitext.name}}\", and \"$3\" will catch the page title ONLY IF the page wasn't caught in \"$2\" (such as in cases where there is only a bare link to the page).",
"deputy.setting.wiki.ia.batchListingWikitext.name": "Batch listing wikitext",
"deputy.setting.wiki.ia.batchListingWikitext.description": "Defines the wikitext that will be used when adding batch listings to a noticeboard page. You may use \"$1\" to denote the page being reported, and \"$2\" for the list of pages (as determined by \"{{int:deputy.setting.wiki.ia.batchListingPageWikitext.name}}\") and \"$3\" for user comments (which doesn't contain the signature).",
"deputy.setting.wiki.ia.batchListingPageWikitext.name": "Batch listing page wikitext",
"deputy.setting.wiki.ia.batchListingPageWikitext.description": "Wikitext to use for every row of text in \"{{int:deputy.setting.wiki.ia.batchListingWikitext.name}}\". No line breaks are automatically added; these must be added into this string.",
"deputy.setting.wiki.ia.hideTemplate.name": "Content hiding wikitext (top)",
"deputy.setting.wiki.ia.hideTemplate.description": "Wikitext to hide offending content with. On the English Wikipedia, this is a usage of {{copyvio}}. Other wikis may have an equivalent template. This should go hand in hand with \"{{int:deputy.setting.wiki.ia.hideTemplateBottom.name}}\", as they are used as a pair.",
"deputy.setting.wiki.ia.hideTemplateBottom.name": "Content hiding wikitext (bottom)",
"deputy.setting.wiki.ia.hideTemplateBottom.description": "Placed at the end of hidden content to hide only part of a page. On the English Wikipedia, this is {{copyvio/bottom}}. Other wikis may have an equivalent template.",
"deputy.setting.wiki.ia.responses.name": "Responses",
"deputy.setting.wiki.ia.responses.description": "Quick responses for copyright problems listings. Used by clerks to resolve specific listings or provide more information about the progress of a given listing."
};
 
let InternalConfigurationGroupTabPanel;
/**
* Initializes the process element.
*/
function initConfigurationGroupTabPanel() {
var _a;
InternalConfigurationGroupTabPanel = (_a = class ConfigurationGroupTabPanel extends OO.ui.TabPanelLayout {
/**
*/
constructor() {
super('configurationGroupPage_About');
this.$element.append(h_1("div", null,
h_1("div", { class: "deputy-about" },
h_1("div", { style: "flex: 0" },
h_1("img", { src: ConfigurationGroupTabPanel.logoUrl, alt: "Deputy logo" })),
h_1("div", { style: "flex: 1" },
h_1("div", null, mw.msg('deputy.name')),
h_1("div", null, mw.msg('deputy.description')),
h_1("div", null,
h_1("a", { href: "https://w.wiki/5k$q", target: "_blank" }, unwrapWidget(new OO.ui.ButtonWidget({
label: mw.msg('deputy.about.homepage'),
flags: ['progressive']
}))),
h_1("a", { href: "https://github.com/ChlodAlejandro/deputy", target: "_blank" }, unwrapWidget(new OO.ui.ButtonWidget({
label: mw.msg('deputy.about.openSource'),
flags: ['progressive']
}))),
h_1("a", { href: "https://w.wiki/5k$p", target: "_blank" }, unwrapWidget(new OO.ui.ButtonWidget({
label: mw.msg('deputy.about.contact'),
flags: ['progressive']
})))))),
h_1("p", { dangerouslySetInnerHTML: mw.msg('deputy.about.credit') }),
h_1("p", { dangerouslySetInnerHTML: mw.msg('deputy.about.license', 'https://www.apache.org/licenses/LICENSE-2.0', 'https://github.com/ChlodAlejandro/deputy') }),
h_1("p", { dangerouslySetInnerHTML: mw.msg('deputy.about.thirdParty', 'https://github.com/ChlodAlejandro/deputy#licensing') }),
h_1("p", { style: { fontSize: '0.9em', color: 'darkgray' }, dangerouslySetInnerHTML: mw.msg('deputy.about.footer') })));
}
/**
* @return The {@Link Setting}s for this group.
*/
get settings() {
return this.config.config.all[this.config.group];
}
/**
* Sets up the tab item
*/
setupTabItem() {
this.tabItem.setLabel(mw.msg('deputy.about'));
}
},
_a.logoUrl = 'https://upload.wikimedia.org/wikipedia/commons/2/2b/Deputy_logo.svg',
_a);
}
/**
* Creates a new ConfigurationGroupTabPanel.
*
* @return A ConfigurationGroupTabPanel object
*/
function ConfigurationAboutTabPanel () {
if (!InternalConfigurationGroupTabPanel) {
initConfigurationGroupTabPanel();
}
return new InternalConfigurationGroupTabPanel();
}
 
let InternalConfigurationDialog;
/**
* Initializes the process element.
*/
function initConfigurationDialog() {
var _a;
InternalConfigurationDialog = (_a = class ConfigurationDialog extends OO.ui.ProcessDialog {
/**
*
* @param data
*/
constructor(data) {
super();
this.config = data.config;
}
/**
* @return The body height of this dialog.
*/
getBodyHeight() {
return 900;
}
/**
* Initializes the dialog.
*/
initialize() {
super.initialize();
this.layout = new OO.ui.IndexLayout();
this.layout.addTabPanels(this.generateGroupLayouts());
if (this.config instanceof UserConfiguration) {
this.layout.addTabPanels([ConfigurationAboutTabPanel()]);
}
this.$body.append(this.layout.$element);
}
/**
* Generate TabPanelLayouts for each configuration group.
*
* @return An array of TabPanelLayouts
*/
generateGroupLayouts() {
return Object.keys(this.config.all).map((group) => ConfigurationGroupTabPanel({
config: this.config,
group
}));
}
/**
*
* @param action
* @return An OOUI Process.
*/
getActionProcess(action) {
const process = super.getActionProcess();
if (action === 'save') {
process.next(this.config.save());
process.next(() => {
mw.notify(mw.msg('deputy.settings.saved'), {
type: 'success'
});
if (this.config instanceof UserConfiguration) {
// Override local Deputy option, just in case the user wishes to
// change the configuration again.
mw.user.options.set(UserConfiguration.optionKey, this.config.serialize());
}
});
}
process.next(() => {
this.close();
});
return process;
}
},
_a.static = {
name: 'configurationDialog',
title: mw.msg('deputy.settings.dialog.title'),
size: 'large',
actions: [
{
flags: ['safe', 'close'],
icon: 'close',
label: mw.msg('deputy.ante.close'),
title: mw.msg('deputy.ante.close'),
invisibleLabel: true,
action: 'close'
},
{
action: 'save',
label: mw.msg('deputy.save'),
flags: ['progressive', 'primary']
}
]
},
_a);
}
/**
* Creates a new ConfigurationDialog.
*
* @param data
* @return A ConfigurationDialog object
*/
function ConfigurationDialogBuilder(data) {
if (!InternalConfigurationDialog) {
initConfigurationDialog();
}
return new InternalConfigurationDialog(data);
}
let attached = false;
/**
* Spawns a new configuration dialog.
*
* @param config
*/
function spawnConfigurationDialog(config) {
mw.loader.using([
'oojs-ui-core', 'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.widgets'
], () => {
const dialog = ConfigurationDialogBuilder({ config });
openWindow(dialog);
});
}
/**
* Attaches the "Deputy preferences" portlet link in the toolbox. Ensures that it doesn't
* get attached twice.
*/
function attachConfigurationDialogPortletLink() {
return __awaiter(this, void 0, void 0, function* () {
if (document.querySelector('#p-deputy-config') || attached) {
return;
}
attached = true;
mw.util.addCSS(deputySettingsStyles);
yield DeputyLanguage.load('settings', deputySettingsEnglish);
mw.util.addPortletLink('p-tb', '#', mw.msg('deputy.settings.portlet'), 'deputy-config', mw.msg('deputy.settings.portlet.tooltip')).addEventListener('click', () => {
// Load a fresh version of the configuration - this way we can make
// modifications live to the configuration without actually affecting
// tool usage.
spawnConfigurationDialog(UserConfiguration.load());
});
});
}
 
/**
* @param config The current configuration
* @return An HTML element consisting of an OOUI MessageWidget
*/
function WikiConfigurationEditIntro(config) {
const r = 'deputy-' + Math.random().toString().slice(2);
const current = config.onConfigurationPage();
const messageBox = new OO.ui.MessageWidget({
classes: [
'deputy', 'dp-mb'
],
type: 'info',
label: new OO.ui.HtmlSnippet((h_1("span", null,
h_1("b", null, mw.msg('deputy.settings.wikiEditIntro.title')),
h_1("br", null),
current ?
mw.msg('deputy.settings.wikiEditIntro.current') :
h_1("span", { dangerouslySetInnerHTML: mw.message('deputy.settings.wikiEditIntro.other', config.sourcePage.getPrefixedText()).parse() }),
h_1("br", null),
h_1("span", { id: r }))).innerHTML)
});
let buttons;
if (current) {
const editCurrent = new OO.ui.ButtonWidget({
flags: ['progressive', 'primary'],
label: mw.msg('deputy.settings.wikiEditIntro.edit.current'),
disabled: !mw.config.get('wgIsProbablyEditable'),
title: mw.config.get('wgIsProbablyEditable') ?
undefined : mw.msg('deputy.settings.wikiEditIntro.edit.protected')
});
editCurrent.on('click', () => {
spawnConfigurationDialog(config);
});
buttons = [editCurrent];
}
else {
const editCurrent = new OO.ui.ButtonWidget({
flags: ['progressive', 'primary'],
label: mw.msg('deputy.settings.wikiEditIntro.edit.otherCurrent'),
disabled: config.editable,
title: config.editable ?
undefined : mw.msg('deputy.settings.wikiEditIntro.edit.protected')
});
editCurrent.on('click', () => __awaiter(this, void 0, void 0, function* () {
spawnConfigurationDialog(config);
}));
const editOther = new OO.ui.ButtonWidget({
flags: ['progressive'],
label: mw.msg('deputy.settings.wikiEditIntro.edit.other'),
disabled: !mw.config.get('wgIsProbablyEditable'),
title: mw.config.get('wgIsProbablyEditable') ?
undefined : mw.msg('deputy.settings.wikiEditIntro.edit.protected')
});
editOther.on('click', () => __awaiter(this, void 0, void 0, function* () {
spawnConfigurationDialog(yield config.static.load(normalizeTitle()));
}));
buttons = [editCurrent, editOther];
}
const box = unwrapWidget(messageBox);
swapElements(box.querySelector(`#${r}`), h_1("span", null, buttons.map(b => unwrapWidget(b))));
box.classList.add('deputy', 'deputy-wikiConfig-intro');
return box;
}
 
/*
* Replacement polyfills for wikis that have no configured templates.
* Used in WikiConfiguration, to permit a seamless OOB experience.
*/
/** `{{collapse top}}` equivalent */
const collapseTop = `
{| class="mw-collapsible mw-collapsed" style="border:1px solid #C0C0C0;width:100%"
! <div style="background:#CCFFCC;">$1</div>
|-
|
`.trimStart();
/** `{{collapse bottom}}` equivalent */
const collapseBottom = `
|}`;
/** `* {{subst:article-cv|1=$1}} $2 ~~~~` equivalent */
const listingWikitext = '* [[$1]] $2 ~~~~';
/**
* Polyfill for the following:
* `; {{anchor|1={{delink|$1}}}} $1
* $2
* $3 ~~~~`
*/
const batchListingWikitext = `*; <span style="display: none;" id="$1"></span> $1
$2
$3`;
/**
* Inserted and chained as part of $2 in `batchListingWikitext`.
* Equivalent of `* {{subst:article-cv|1=$1}}\n`. Newline is intentional.
*/
const batchListingPageWikitext = '* [[$1]]\n';
/**
* `{{subst:copyvio|url=$1|fullpage=$2}}` equivalent
*/
const copyvioTop = `<div style="padding: 8px; border: 4px solid #0298b1;">
<div style="font-size: 1.2rem"><b>{{int:deputy.ia.content.copyvio}}</b></div>
<div>{{int:deputy.ia.content.copyvio.help}}</div>
</div>
<!-- {{int:deputy.ia.content.copyvio.content}} -->
<div style="display: none" data-copyvio>`;
/**
* `{{subst:copyvio/bottom}}` equivalent.
*/
const copyvioBottom = `
</div>`;
 
/**
* @return A MessageWidget for reloading a page with an outdated configuration.
*/
function ConfigurationReloadBanner() {
const r = 'deputy-' + Math.random().toString().slice(2);
const messageBox = new OO.ui.MessageWidget({
classes: [
'deputy', 'dp-mb'
],
type: 'info',
label: new OO.ui.HtmlSnippet((h_1("span", null,
h_1("b", null, mw.msg('deputy.settings.wikiOutdated')),
h_1("br", null),
mw.msg('deputy.settings.wikiOutdated.help'),
h_1("br", null),
h_1("span", { id: r }))).innerHTML)
});
const reloadButton = new OO.ui.ButtonWidget({
flags: ['progressive', 'primary'],
label: mw.msg('deputy.settings.wikiOutdated.reload')
});
reloadButton.on('click', () => __awaiter(this, void 0, void 0, function* () {
window.___location.reload();
}));
const box = unwrapWidget(messageBox);
swapElements(box.querySelector(`#${r}`), unwrapWidget(reloadButton));
box.style.fontSize = 'calc(1em * 0.875)';
return box;
}
 
/**
* Wiki-wide configuration. This is applied to all users of the wiki, and has
* the potential to break things for EVERYONE if not set to proper values.
*
* As much as possible, the correct configuration ___location should be protected
* to avoid vandalism or bad-faith changes.
*
* This configuration works if specific settings are set. In other words, some
* features of Deputy are disabled unless Deputy has been configured. This is
* to avoid messing with existing on-wiki processes.
*/
class WikiConfiguration extends ConfigurationBase {
/**
*
* @param sourcePage
* @param serializedData
* @param editable Whether the configuration is editable by the current user or not.
*/
constructor(sourcePage, serializedData, editable) {
super();
this.sourcePage = sourcePage;
this.serializedData = serializedData;
this.editable = editable;
// uUed to avoid circular dependencies.
this.static = WikiConfiguration;
this.core = {
/**
* Numerical code that identifies this config version. Increments for every breaking
* configuration file change.
*/
configVersion: new Setting({
defaultValue: WikiConfiguration.configVersion,
displayOptions: { hidden: true },
alwaysSave: true
})
};
this.cci = {
enabled: new Setting({
defaultValue: false,
displayOptions: { type: 'checkbox' }
}),
rootPage: new Setting({
serialize: (v) => v.getPrefixedText(),
deserialize: (v) => new mw.Title(v),
defaultValue: null,
displayOptions: { type: 'page' }
}),
collapseTop: new Setting({
defaultValue: collapseTop,
displayOptions: { type: 'code' }
}),
collapseBottom: new Setting({
defaultValue: collapseBottom,
displayOptions: { type: 'code' }
})
};
this.ante = {
enabled: new Setting({
defaultValue: false,
displayOptions: { type: 'checkbox' }
})
};
this.ia = {
enabled: new Setting({
defaultValue: false,
displayOptions: { type: 'checkbox' }
}),
rootPage: new Setting({
serialize: (v) => v.getPrefixedText(),
deserialize: (v) => new mw.Title(v),
defaultValue: null,
displayOptions: { type: 'page' }
}),
subpageFormat: new Setting({
defaultValue: 'YYYY MMMM D',
displayOptions: { type: 'text' }
}),
preload: new Setting({
serialize: (v) => v.trim().length === 0 ? null : v.trim(),
defaultValue: null,
displayOptions: { type: 'page' }
}),
listingWikitext: new Setting({
defaultValue: listingWikitext,
displayOptions: { type: 'code' }
}),
/**
* $1 - Title of the batch
* $2 - List of pages (newlines should be added in batchListingPageWikitext).
* $3 - User comment
*/
batchListingWikitext: new Setting({
defaultValue: batchListingWikitext,
displayOptions: { type: 'code' }
}),
/**
* $1 - Page to include
*/
batchListingPageWikitext: new Setting({
defaultValue: batchListingPageWikitext,
displayOptions: { type: 'code' }
}),
/**
* @see {@link CopyrightProblemsListing#articleCvRegex}
*
* This should match both normal and batch listings.
*/
listingWikitextMatch: new Setting({
defaultValue: '(\\*\\s*)?\\[\\[([^\\]]+)\\]\\]',
displayOptions: { type: 'code' }
}),
hideTemplate: new Setting({
defaultValue: copyvioTop,
displayOptions: { type: 'code' }
}),
hideTemplateBottom: new Setting({
defaultValue: copyvioBottom,
displayOptions: { type: 'code' }
}),
responses: new Setting(Object.assign(Object.assign({}, Setting.basicSerializers), { defaultValue: null, displayOptions: { type: 'unimplemented' } }))
};
this.all = { cci: this.cci, ante: this.ante, ia: this.ia };
/**
* Set to true when this configuration is outdated based on latest data. Usually adds banners
* to UI interfaces saying a new version of the configuration is available, and that it should
* be used whenever possible.
*
* TODO: This doesn't do what the documentations says yet.
*/
this.outdated = false;
if (serializedData) {
this.deserialize(serializedData);
}
}
/**
* Loads the configuration from a set of possible sources.
*
* @param sourcePage The specific page to load from
*/
static load(sourcePage) {
return __awaiter(this, void 0, void 0, function* () {
if (sourcePage) {
// Explicit source given. Do not load from local cache.
return this.loadFromWiki(sourcePage);
}
else {
return this.loadFromLocal();
}
});
}
/**
* loads the wiki configuration from localStorage. This allows for faster loads at
* the expense of a (small) chance of outdated configuration.
*/
static loadFromLocal() {
return __awaiter(this, void 0, void 0, function* () {
let configPage;
try {
configPage = JSON.parse(mw.user.options.get(WikiConfiguration.optionKey));
}
catch (e) {
// Bad local! Switch to non-local.
console.error(e);
return this.loadFromWiki();
}
if (configPage) {
return new WikiConfiguration(configPage.title, JSON.parse(configPage.wt), configPage.editable);
}
else {
return this.loadFromWiki();
}
});
}
/**
* Loads the configuration from the current wiki.
*
* @param sourcePage The specific page to load from
*/
static loadFromWiki(sourcePage) {
return __awaiter(this, void 0, void 0, function* () {
const configPage = sourcePage ? Object.assign({ title: sourcePage }, yield (() => __awaiter(this, void 0, void 0, function* () {
const content = yield getPageContent(sourcePage, {
prop: 'revisions|info',
intestactions: 'edit'
});
return {
wt: content,
editable: content.page.actions.edit
};
}))()) : yield this.loadConfigurationWikitext();
try {
// Attempt save of configuration to local options (if not explicitly loaded)
if (sourcePage == null) {
MwApi.action.saveOption(WikiConfiguration.optionKey, JSON.stringify(configPage)).catch(() => { });
}
return new WikiConfiguration(configPage.title, JSON.parse(configPage.wt), configPage.editable);
}
catch (e) {
console.error(e, configPage);
mw.hook('deputy.i18nDone').add(function notifyConfigFailure() {
mw.notify(mw.msg('deputy.loadError.wikiConfig'), {
type: 'error'
});
mw.hook('deputy.i18nDone').remove(notifyConfigFailure);
});
return null;
}
});
}
/**
* Loads the wiki-wide configuration from a set of predefined locations.
* See {@link WikiConfiguration#configLocations} for a full list.
*
* @return The string text of the raw configuration, or `null` if a configuration was not found.
*/
static loadConfigurationWikitext() {
return __awaiter(this, void 0, void 0, function* () {
const response = yield MwApi.action.get({
action: 'query',
prop: 'revisions|info',
rvprop: 'content',
rvslots: 'main',
rvlimit: 1,
intestactions: 'edit',
redirects: true,
titles: WikiConfiguration.configLocations.join('|')
});
const redirects = toRedirectsObject(response.query.redirects, response.query.normalized);
for (const page of WikiConfiguration.configLocations) {
const title = normalizeTitle(redirects[page] || page).getPrefixedText();
const pageInfo = response.query.pages.find((p) => p.title === title);
if (!pageInfo.missing) {
return {
title: normalizeTitle(pageInfo.title),
wt: pageInfo.revisions[0].slots.main.content,
editable: pageInfo.actions.edit
};
}
}
return null;
});
}
/**
* Check if the current page being viewed is a valid configuration page.
*
* @param page
* @return `true` if the current page is a valid configuration page.
*/
static isConfigurationPage(page) {
if (page == null) {
page = new mw.Title(mw.config.get('wgPageName'));
}
return this.configLocations.some((v) => equalTitle(page, normalizeTitle(v)));
}
/**
* Check for local updates, and update the local configuration as needed.
*/
update() {
return __awaiter(this, void 0, void 0, function* () {
// Asynchronously load from the wiki.
const fromWiki = yield WikiConfiguration.loadConfigurationWikitext();
if (fromWiki == null) {
// No configuration found on the wiki.
return;
}
const liveWikiConfig = JSON.stringify(JSON.parse(fromWiki.wt));
// Attempt save if on-wiki config found and doesn't match local.
// Doesn't need to be from the same config page, since this usually means a new config
// page was made, and we need to switch to it.
if (JSON.stringify(this.serializedData) !== liveWikiConfig) {
MwApi.action.saveOption(WikiConfiguration.optionKey,
// Use `liveWikiConfig`, since this contains the compressed version and is more
// bandwidth-friendly.
JSON.stringify({
title: fromWiki.title,
editable: fromWiki.editable,
wt: liveWikiConfig
})).then(() => {
var _a;
// Only mark outdated after saving, so we don't indirectly cause a save operation
// to cancel.
this.outdated = true;
// Attempt to add site notice.
(_a = document.getElementById('siteNotice')) === null || _a === void 0 ? void 0 : _a.insertAdjacentElement('afterend', ConfigurationReloadBanner());
}, () => { });
}
});
}
/**
* Saves the configuration on-wiki. Does not automatically generate overrides.
*/
save() {
return __awaiter(this, void 0, void 0, function* () {
yield MwApi.action.postWithEditToken({
action: 'edit',
title: this.sourcePage.getPrefixedText(),
text: JSON.stringify(this.all, null, '\t')
});
});
}
/**
* Check if the current page being viewed is the active configuration page.
*
* @param page
* @return `true` if the current page is the active configuration page.
*/
onConfigurationPage(page) {
return equalTitle(page !== null && page !== void 0 ? page : mw.config.get('wgPageName'), this.sourcePage);
}
/**
* Actually displays the banner which allows an editor to modify the configuration.
*/
displayEditBanner() {
return __awaiter(this, void 0, void 0, function* () {
mw.loader.using(['oojs', 'oojs-ui'], () => {
if (document.getElementsByClassName('deputy-wikiConfig-intro').length > 0) {
return;
}
document.getElementById('mw-content-text').insertAdjacentElement('afterbegin', WikiConfigurationEditIntro(this));
});
});
}
/**
* Shows the configuration edit intro banner, if applicable on this page.
*/
prepareEditBanners() {
return __awaiter(this, void 0, void 0, function* () {
if (['view', 'diff'].indexOf(mw.config.get('wgAction')) === -1) {
return;
}
if (document.getElementsByClassName('deputy-wikiConfig-intro').length > 0) {
return;
}
if (this.onConfigurationPage()) {
return this.displayEditBanner();
}
else if (WikiConfiguration.isConfigurationPage()) {
return this.displayEditBanner();
}
});
}
}
WikiConfiguration.configVersion = 1;
WikiConfiguration.optionKey = 'userjs-deputy-wiki';
WikiConfiguration.configLocations = [
'MediaWiki:Deputy-config.json',
// Prioritize interface protected page over Project namespace
'User:Chlod/Scripts/Deputy/configuration.json',
'Project:Deputy/configuration.json'
];
 
/**
Line 12,265 ⟶ 14,731:
*/
class DeputyModule {
/**
*
* @param deputy
*/
constructor(deputy) {
this.deputy = deputy;
}
/**
* @return The responsible window manager for this class.
Line 12,308 ⟶ 14,767:
get wikiConfig() {
return this.deputy ? this.deputy.wikiConfig : this._wikiConfig;
}
/**
*
* @param deputy
*/
constructor(deputy) {
this.deputy = deputy;
}
/**
* Get the module key for this module. Allows modules to be identified with a different
* configuration key.
*
* @return The module key. the module name by default.
*/
getModuleKey() {
return this.getName();
}
/**
Line 12,319 ⟶ 14,794:
yield Promise.all([
DeputyLanguage.load(this.getName(), fallback),
DeputyLanguage.load('shared', deputySharedEnglish),
DeputyLanguage.loadMomentLocale()
]);
Line 12,333 ⟶ 14,809:
return __awaiter(this, void 0, void 0, function* () {
yield this.getWikiConfig();
if (((_a = this.wikiConfig[this.getNamegetModuleKey()]) === null || _a === void 0 ? void 0 : _a.enabled.get()) !== true) {
// Stop loading here.
console.warn(`[Deputy] Preinit for ${this.getName()} cancelled; module is disabled.`);
return false;
}
Line 12,362 ⟶ 14,838:
}
 
var cteStyles = ".copied-template-editor .oo-ui-window-frame {width: 1000px !important;}.copied-template-editor .oo-ui-menuLayout > .oo-ui-menuLayout-menu {height: 20em;width: 20em;}.copied-template-editor .oo-ui-menuLayout > .oo-ui-menuLayout-content {left: 20em;}.cte-preview .copiednotice {margin-left: 0;margin-right: 0;}.cte-merge-panel {padding: 16px;z-index: 20;border: 1px solid lightgray;margin-bottom: 8px;}.copied-template-editor .oo-ui-bookletLayout-outlinePanel {bottom: 32px;}.cte-actionPanel {height: 32px;width: 100%;position: absolute;bottom: 0;z-index: 1;background-color: white;border-top: 1px solid #c8ccd1;}.cte-actionPanel > .oo-ui-buttonElement {display: inline-block;margin: 0 !important;}.cte-templateOptions {margin: 8px;display: flex;}.cte-templateOptions > * {flex: 1;}.cte-fieldset {border: 1px solid gray;background-color: #ddf7ff;padding: 16px;min-width: 200px;clear: both;}.cte-fieldset-date {float: left;margin-top: 10px !important;}.cte-fieldset-advswitch {float: right;}.cte-fieldset-advswitch .oo-ui-fieldLayout-field,.cte-fieldset-date .oo-ui-fieldLayout-field {display: inline-block !important;}.cte-fieldset-advswitch .oo-ui-fieldLayout-header {display: inline-block !important;margin-right: 16px;}.copied-template-editor .mw-widgets-datetime-dateTimeInputWidget-handle .oo-ui-iconElement-icon {left: 0.5em;width: 1em;height: 1em;top: 0.4em;}.cte-fieldset-date .mwoo-widgetsui-datetimefieldLayout-dateTimeInputWidget-editFieldfield {min-width: 2.5ch !important18em;}.cte-fieldset-date :not(.mw-widgetswidget-datetime-dateTimeInputWidget-empty) > .mw-widgets-datetime-dateTimeInputWidget-handledateInputWidget {paddingmax-rightwidth: 0unset;}.cte-page-row:not(:last-child),.cte-page-template:not(:last-child),.cte-fieldset-date.oo-ui-actionFieldLayout.oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-header {padding-bottom: 0 !important;}.cte-page-template + .cte-page-row {padding-top: 0 !important;}.copied-template-editor .oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-fieldsetLayout-header {position: relative;}.oo-ui-actionFieldLayout.oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-header {padding-bottom: 6px !important;}.deputy.oo-ui-windowManager-modalwindow >{/** Place below default .oo-ui-window.oo-ui-dialog.oo-ui-messageDialog {manager */z-index: 200199 !important;}";
 
var deputySharedEnglish = {
"deputy.name": "Deputy",
"deputy.description": "Copyright cleanup and case processing tool for Wikipedia.",
"deputy.ia": "Infringement Assistant",
"deputy.ia.short": "I. Assistant",
"deputy.ia.acronym": "Deputy: IA",
"deputy.ante": "Attribution Notice Template Editor",
"deputy.ante.short": "Attrib. Template Editor",
"deputy.ante.acronym": "Deputy: ANTE",
"deputy.cancel": "Cancel",
"deputy.review": "Review",
"deputy.save": "Save",
"deputy.close": "Close",
"deputy.positiveDiff": "+{{FORMATNUM:$1}}",
"deputy.negativeDiff": "-{{FORMATNUM:$1}}",
"deputy.moreInfo": "More information",
"deputy.comma-separator": ", ",
"deputy.diff": "Review your changes",
"deputy.diff.load": "Loading changes...",
"deputy.diff.no-changes": "No difference",
"deputy.diff.error": "An error occurred while trying to get the comparison.",
"deputy.loadError.userConfig": "Due to an error, your Deputy configuration has been reset.",
"deputy.loadError.wikiConfig": "An error occurred while loading this wiki's Deputy configuration. Please report this to the Deputy maintainers for this wiki."
};
 
/**
Line 12,425 ⟶ 14,876:
return false;
}
yield Promise.all([
DeputyLanguage.load('shared', deputySharedEnglish),
DeputyLanguage.loadMomentLocale()
]);
if (
// Button not yet appended
Line 12,499 ⟶ 14,946:
openEditDialog() {
mw.loader.using(CopiedTemplateEditor.dependencies, () => __awaiter(this, void 0, void 0, function* () {
yield DeputyLanguage.loadMomentLocale();
OO.ui.WindowManager.static.sizes.huge = {
width: 1100
Line 12,518 ⟶ 14,966:
this.windowManager.addWindows([this.dialog]);
}
yield this.windowManager.openWindow(this.dialog).opened;
}));
}
Line 12,549 ⟶ 14,997:
'mediawiki.Title',
'mediawiki.widgets',
'mediawiki.widgets.datetimeDateInputWidget',
'jquery.makeCollapsible'
];
 
var deputyStyles = "/*=============================================================================== GLOBAL DEPUTY CLASSES===============================================================================*/* > .deputy.dp-heading {position: absolute;opacity: 0;pointer-events: none;}*:hover > .deputy.dp-heading:not(.dp-heading--active) {opacity: 1;pointer-events: all;}.dp-loadingDots-1, .dp-loadingDots-2, .dp-loadingDots-3 {display: inline-block;margin: 0.1em 0.6em 0.1em 0.1em;width: 0.8em;height: 0.8em;background-color: rgba(0, 0, 0, 50%);animation: dp-loadingDots linear 3s infinite;border-radius: 50%;}@keyframes dp-loadingDots {0% {background-color: rgba(0, 0, 0, 10%);}16% {background-color: rgba(0, 0, 0, 40%);}32% {background-color: rgba(0, 0, 0, 10%);}100% {background-color: rgba(0, 0, 0, 10%);}}.dp-loadingDots-1 {animation-delay: -1s;}.dp-loadingDots-2 {animation-delay: -0.5s;}#mw-content-text.dp-reloading {opacity: 0.2;pointer-events: none;}p.dp-messageWidget-message {margin: 0 0 0.5em 0;}.dp-messageWidget-actions .oo-ui-buttonElement {margin-top: 0;}.oo-ui-image-destructive.oo-ui-icon-checkAll, .oo-ui-image-destructive.mw-ui-icon-checkAll::before {background-image: url(\"data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2220%22 height=%2220%22 viewBox=%220 0 20 20%22%3E%3Ctitle%3E check all %3C/title%3E%3Cpath fill=%22%23d73333%22 d=%22m.29 12.71 1.42-1.42 2.22 2.22 8.3-10.14 1.54 1.26-9.7 11.86zM12 10h5v2h-5zm-3 4h5v2H9zm6-8h5v2h-5z%22/%3E%3C/svg%3E\");}/*=============================================================================== DEPUTY REVIEW DIALOG (DeputyReviewDialog)===============================================================================*/.dp-review-progress {flex: 1;width: 60%;min-width: 300px;}/*=============================================================================== DEPUTY ENTRY POINTS (DeputyCCISessionStartLink, etc.)===============================================================================*/.deputy.dp-sessionStarter {font-size: small;font-weight: normal;margin-left: 0.25em;vertical-align: baseline;line-height: 1em;font-family: sans-serif;}.deputy.dp-sessionStarter::before {content: '\\200B';}.mw-content-ltr .deputy.dp-sessionStarter .dp-sessionStarter-bracket:first-of-type,.mw-content-rtl .deputy.dp-sessionStarter .dp-sessionStarter-bracket:not(:first-of-type) {margin-right: 0.25em;color: #54595d;}.client-js .deputy.dp-sessionStarter .dp-sessionStarter-bracket:first-of-type,.client-js .deputy.dp-sessionStarter .dp-sessionStarter-bracket:not(:first-of-type) {margin-left: 0.25em;color: #54595d}.dp-cs-session-continue {margin-top: 8px;}.dp-cs-section-add {position: absolute;top: 0;/* -1.6em derived from MediaWiki list margins. */left: -1.6em;width: calc(100% + 1.6em);height: 100%;background-color: rgba(255, 255, 255, 75%);display: flex;justify-content: center;align-items: center;}.dp-cs-section-add .dp-cs-section-addButton {opacity: 0;transition: opacity 0.2s ease-in-out;}.dp-cs-section-add:hover .dp-cs-section-addButton {opacity: 1;}/*=============================================================================== DEPUTY CONTRIBUTION SURVEY SECTION===============================================================================*/.dp-cs-section-archived .dp-cs-row-content {background-color: rgba(255, 0, 0, 6%);}.dp-cs-session-notice {margin-top: 8px;position: sticky;top: 8px;z-index: 50;}.skin-vector-2022.vector-sticky-header-visible .dp-cs-session-notice {top: calc(3.125rem + 8px);}.dp-cs-section-footer {position: relative;padding: 8px;}.dp-cs-section-danger--separator {flex-basis: 100%;margin: 8px 0;border-bottom: 1px solid #d73333;color: #d73333;font-weight: bold;font-size: 0.7em;text-align: right;text-transform: uppercase;line-height: 0.7em;padding-bottom: 0.2em;}.dp-cs-section-closing {margin: 1em 1.75em;}.dp-cs-section-progress {margin-top: 8px;max-height: 0;transition: max-height 0.2s ease-in-out;display: flex;justify-content: center;align-items: center;overflow: hidden;}.dp-cs-section-progress.active {max-height: 50px;}.dp-cs-section-progress .oo-ui-progressBarWidget {flex: 1}.dp-cs-section-closingCommentsField {margin-top: 8px;}.dp-cs-extraneous {border: 1px solid rgba(0, 159, 255, 40%);background-color: rgba(0, 159, 255, 10%);margin-bottom: 8px;padding: 16px;}.dp-cs-extraneous > dl {margin-left: -1.6em;}.dp-cs-extraneous > :first-child {margin-top: 0 !important;}.dp-cs-extraneous > :last-child {margin-bottom: 0 !important;}.dp-cs-section-archived-warn, .dp-cs-row, .dp-cs-extraneous {margin-bottom: 8px;}.dp-cs-row .dp--loadingDots {display: flex;align-items: center;justify-content: center;padding: 0.4em;}.dp-cs-row-status {max-width: 5.4em;}.dp-cs-row-status .oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {width: 0;opacity: 0;}.dp-cs-row-status .dp-cs-row-status--unknown:not(.oo-ui-optionWidget-selected) {display: none;}.dp-cs-row-head > * {vertical-align: middle;}.dp-cs-row-comments {padding: 16px;background-color: rgba(0, 159, 255, 10%);margin: 4px 0;}.dp-cs-row-comments > b {letter-spacing: 0.1em;font-weight: bold;text-transform: uppercase;color: rgba(0, 0, 0, 0.5);}.dp-cs-row-comments hr {border-color: rgb(0, 31, 51);}body.mediawiki.ltr .dp-cs-row-head > :not(:first-child):not(:last-child),body.mediawiki.ltr .dp-cs-row-head > :not(:first-child):not(:last-child) {margin-right: 16px;}body.mediawiki.rtl .dp-cs-row-head > :not(:first-child):not(:last-child),body.mediawiki.rtl .dp-cs-row-head > :not(:first-child):not(:last-child) {margin-left: 16px;}.dp-cs-row-links {margin-right: 0 !important;}.dp-cs-row-links > :not(:last-child) {margin-right: 8px !important;}.dp-cs-row-title {font-weight: bold;font-size: 1.2em;vertical-align: middle;}.dp-cs-row-details {color: #4a5054;font-weight: bold;}.dp-cs-row-toggle .oo-ui-iconElement-icon {background-size: 1em;}.dp-cs-row-toggle .oo-ui-buttonElement-button {border-radius: 50%;}.dp-cs-row .history-user,.dp-cs-row :not(.newpage) + .mw-changeslist-date {margin-left: 0.4em;margin-right: 0.2em;}.dp-cs-row .newpage {margin-left: 0.4em;}.dp-cs-row-content {padding: 16px;background-color: rgba(0, 0, 0, 46%);margin: 4px 0;}.dp-cs-row-content.dp-cs-row-content-empty {display: none !important;}.dp-cs-row-unfinishedWarning {margin-bottom: 8px;}.dp-cs-section-unfinishedWarning {margin-top: 8px;}.dp-cs-row-closeComments {font-family: monospace, monospace;font-size: small;}.dp-cs-row-closeComments:not(:last-child) {margin-bottom: 8px;}.dp-cs-row-finished .oo-ui-fieldLayout:first-child {margin-top: 0;}.dp-cs-row-finished .oo-ui-fieldLayout {margin-top: 8px;}.dp-cs-row-revisions .mw-tag-markers .mw-tag-marker:not(:first-child),.dp-cs-row-detail:not(:first-child) {margin-left: 0.2em;}.dp-cs-rev-checkbox {margin-right: 4px;}.dp-cs-rev-toggleDiff {vertical-align: baseline;margin-right: 4px;}.dp-cs-rev-diff {background-color: white;position: relative;}.dp-cs-rev-diff--loaded {margin: 4px 0;padding: 8px 14px;}.dp-cs-rev-diff--hidden {display: none;}.dp-cs-rev-toggleDiff > .oo-ui-buttonElement-button {padding: 0;min-height: 1em;background-color: unset !important;}.dp-cs-rev-toggleDiff .oo-ui-indicatorElement-indicator {top: -1px;}/*=============================================================================== DEPUTY PAGE TOOLBAR===============================================================================*/.dp-pageToolbar {position: fixed;bottom: 8px;left: 8px;paddingz-index: 8px100;background-color: #fff;border: 1px solid gray;font-size: 0.9rem;display: flex;}.dp-pageToolbar .dp-pageToolbar-main {padding: 8px;display: flex;align-items: center;}.dp-pageToolbar-actions {width: 12px;display: flex;flex-direction: column;font-size: 12px;line-height: 1em;}.dp-pageToolbar-close {cursor: pointer;height: 12px;text-align: center;background-color: rgba(0, 0, 0, 0.25);}.dp-pageToolbar-close:hover {transition: background-color 0.1s ease-in-out;background-color: rgba(0, 0, 0, 0.4);}.dp-pageToolbar-close::before {content: '×';vertical-align: middle;position: relative;right: 1px;}.dp-pageToolbar-collapse {cursor: pointer;flex: 1;background-color: rgba(0, 0, 0, 0.125);text-align: center;writing-mode: vertical-rl;position: relative;}.dp-pageToolbar-collapse:hover {transition: background-color 0.1s ease-in-out;background-color: rgba(0, 0, 0, 0.25);}.dp-pageToolbar-collapse::before {content: '»';position: absolute;vertical-align: middle;width: 12px;left: 0;bottom: 2px;}.dp-pageToolbar-collapsed {cursor: pointer;width: 32px;height: 32px;/* logo-white.svg */background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDgwIDEwODAiIHdpZHRoPSIxMDgwIiBoZWlnaHQ9IjEwODAiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojMDI5N2IxO308L3N0eWxlPjwvZGVmcz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik04NzcuNTQsNDM4LjY5YTQzNy41NCw0MzcuNTQsMCwwLDEtODQuMzgsMjU4Ljc4bDI2Ny4wNiwyNjcuMjJhNjcuNTYsNjcuNTYsMCwxLDEtOTUuNTYsOTUuNTRMNjk3LjYsNzkzYTQzNi4yNyw0MzYuMjcsMCwwLDEtMjU4LjgzLDg0LjM2QzE5Ni4zOSw4NzcuMzcsMCw2ODEsMCw0MzguNjlTMTk2LjM5LDAsNDM4Ljc3LDAsODc3LjU0LDE5Ni4zNSw4NzcuNTQsNDM4LjY5Wk00MzguNzcsNzQyLjM5YzE2Ny43LDAsMzAzLjc3LTEzNiwzMDMuNzctMzAzLjdTNjA2LjQ3LDEzNSw0MzguNzcsMTM1LDEzNSwyNzEsMTM1LDQzOC42OSwyNzEuMDcsNzQyLjM5LDQzOC43Nyw3NDIuMzlaIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNTI0LjgzLDUxNS4yOGExMDQuODUsMTA0Ljg1LDAsMCwxLTE0OS40MSwwYy00MS41LTQxLjg3LTQxLjUtMTEwLDAtMTUxLjk1YTEwNC45MiwxMDQuOTIsMCwwLDEsMTQ5LjUxLjA4QTUwLjc2LDUwLjc2LDAsMCwwLDU5NywyOTEuOWEyMDYuNDQsMjA2LjQ0LDAsMCwwLTI5My41NSwwYy04MC41Nyw4MS4yNy04MC41NywyMTMuNTMsMCwyOTQuODNBMjA1LjI1LDIwNS4yNSwwLDAsMCw0NTAuMTgsNjQ4aDBBMjA1LjMsMjA1LjMsMCwwLDAsNTk3LDU4Ni43MWE1MC43Nyw1MC43NywwLDAsMC03Mi4xNi03MS40M1oiLz48L3N2Zz4=') no-repeat center;background-size: 24px;}@media only screen and (max-width: 768px) {.dp-pageToolbar {flex-wrap: wrap;bottom: 0;left: 0;border-left: 0;border-bottom: 0;border-right: 0;width: 100%;}}.dp-pt-section {display: inline-block;white-space: nowrap;}.dp-pt-section .oo-ui-popupWidget-popup {/** Avoid preventing line breaks in popups */white-space: initial;}.dp-pt-section + .dp-pt-section {/* TODO: Recheck RTL compatibility */margin-left: 16px;padding-left: 16px;border-left: 1px solid gray;}.dp-pt-section:last-child {/* TODO: Recheck RTL compatibility */margin-right: 8px;}.dp-pt-section-label {font-weight: bold;font-size: 0.6rem;color: #4a5054;text-transform: uppercase;}.dp-pt-section-content .oo-ui-buttonElement:last-child {margin-right: 0;}.dp-pt-caseInfo {font-weight: bold;font-size: 1.3rem;pointer-events: none;}.dp-pt-missingRevision {white-space: normal;}.dp-pageToolbar .dp-cs-row-status {width: 5.4em;}.dp-pt-menu .oo-ui-menuSelectWidget {min-width: 300px;}.dp-pt-menu .oo-ui-menuOptionWidget {padding-top: 8px;padding-bottom: 8px;}";
 
var deputyCoreEnglish = {
"deputy.content.assessedsummary": "Assessed/* $1 revisions across*/ -$2) pages(",
"deputy.content.summary.partial": "/* $1 */ - partial) (",
"deputy.content.summary.sectionClosed": "/* $1 */ -$2, section done) (",
"deputy.content.assessed": "Assessed $1 {{PLURAL:$1|revision|revisions}} across $2 pages",
"deputy.content.assessed.comma": ", ",
"deputy.content.assessed.finished": "$1 finished",
Line 12,567 ⟶ 15,018:
"deputy.session.continue.head": "You last worked on this page on $1.",
"deputy.session.continue.help": "Continue working on \"$1\" and pick up where you left off.",
"deputy.session.continue.help.fromStart": "The section \"$1\" might have been archived already. Not to worry, you can being working on \"$2\".",
"deputy.session.tabActive.head": "You are working on this case page from another tab.",
"deputy.session.tabActive.help": "Deputy can only run on one case page and tab at a time. Navigate to the other tab to continue working.",
Line 12,573 ⟶ 15,025:
"deputy.session.otherActive.button": "Stop session",
"deputy.session.add": "Start working on this section",
"deputy.session.section.close": "CloseArchive section",
"deputy.session.section.closeComments": "ClosingArchiving comments",
"deputy.session.section.closeHelpcloseCommentsSign": "YourInclude my signature will automatically be included.",
"deputy.session.section.closeError": "Some revisions remain unassessed. You must mark these revisions as assessed before archiving this section.",
"deputy.session.section.closeError.danger": "Some revisions remain unassessed, but Deputy will allow archiving while danger mode is enabled.",
"deputy.session.section.closeWarn": "You have unsaved changes. Close the section without saving?",
"deputy.session.section.closed": "This section has been archived. You can edit its contents, but you cannot un-archive it.",
"deputy.session.section.stop": "Stop session",
"deputy.session.section.stop.title": "Stop then current session, closing all sections and saving changes for later.",
"deputy.session.section.saved": "Section saved",
"deputy.session.section.failed": "Failed to save section",
"deputy.session.section.missingSection": "The target section is missing from the case page.",
"deputy.session.section.sectionIncomplete": "The target section still has unreviewed rows.",
"deputy.session.section.conflict.title": "Edit conflict",
"deputy.session.section.conflict.help": "Someone else edited the page before you. Deputy will restart to load the new case content. Your changes will be preserved.",
"deputy.session.section.danger": "Danger mode",
"deputy.session.section.markAllFinished": "Mark all revisions in all sections as finished",
"deputy.session.section.instantArchive": "Archive",
"deputy.session.section.instantArchive.title": "Archive and save this section immediately. Revisions will not be marked as finished.",
"deputy.session.row.status": "Current page status",
"deputy.session.row.status.unfinished": "Unfinished",
Line 12,597 ⟶ 15,061:
"deputy.session.row.checkAll": "Mark all revisions as finished",
"deputy.session.row.checkAll.confirm": "Mark all revisions as finished?",
"deputy.session.row.additionalComments": "Discussion",
"deputy.session.row.closeComments": "Closing comments",
"deputy.session.row.close.sigFound": "The closing comment had a signature. It will not be automatically removed when saved.",
Line 12,605 ⟶ 15,070:
"deputy.session.row.checked.talk": "talk",
"deputy.session.row.checked.contribs": "contribs",
"deputy.session.row.pageonly": "This row does not contain any diffs. Please assess the page history manually.",
"deputy.session.revision.assessed": "Mark as assessed",
"deputy.session.revision.diff.toggle": "Toggle comparison (diff) view",
"deputy.session.revision.diff.error": "Failed to load comparison: $1",
"deputy.session.revision.cur": "cur",
"deputy.session.revision.prev": "prev",
"deputy.session.revision.cv": "cv",
"deputy.session.revision.cur.tooltip": "Difference with latest revision",
"deputy.session.revision.prev.tooltip": "Difference with preceding revision",
"deputy.session.revision.cv.tooltip": "Run through Earwig's Copyvio Detector",
"deputy.session.revision.talk": "talk",
"deputy.session.revision.contribs": "contribs",
Line 12,616 ⟶ 15,087:
"deputy.session.revision.new": "N",
"deputy.session.revision.new.tooltip": "This edit created a new page.",
"deputy.session.revision.missing": "The revision [[Special:Diff/$1|$1]] could not be found. It may have been deleted or suppressed.",
"deputy.session.page.close": "Minimize the toolbar to the sidebar",
"deputy.session.page.collapse": "Collapse the toolbar",
"deputy.session.page.expand": "Expand the toolbar",
"deputy.session.page.open": "Open Deputy toolbar",
"deputy.session.page.open.tooltip": "Open the Deputy page toolbar",
"deputy.session.page.diff.previous": "Navigate to the previous unassessed revision",
"deputy.session.page.diff.next": "Navigate to the next unassessed revision",
"deputy.session.page.diff.loadFail": "Failed to load diff. Please check your internet connection and try again.",
Line 12,625 ⟶ 15,103:
"deputy.session.page.caseInfo.assessed": "Assessed?",
"deputy.session.page.caseInfo.next": "Navigate to the next unassessed revision",
"deputy.session.page.pageonly.title": "No revisions",
"deputy.session.page.pageonly.help": "This row does not contain any revisions with it. Please assess the page history manually before making an assessment.",
"deputy.session.page.analysis": "Analysis",
"deputy.session.page.earwigLatest": "Earwig's Copyvio Detector (latest)",
Line 12,638 ⟶ 15,118:
"deputy.ia.content.close": "-1) (Responding to [[$1#$2|$2]]",
"deputy.ia.content.listing": "Adding listing for [[$1#$2|$2]]",
"deputy.ia.content.listingComment": "from $1. $2",
"deputy.ia.content.batchListing": "Adding batch listing: \"[[$1#$2|$2]]\"",
"deputy.ia.content.listingComment": "from $1. $2",
"deputy.ia.content.hideAll": "Hiding page content due to a suspected or complicated copyright issue",
"deputy.ia.content.hide": "Hiding sections [[$1#$2|$3]] to [[$1#$4|$5]] for suspected or complicated copyright issues",
"deputy.ia.content.listing.pd": "Adding listing for [[$1#$2|$2]] (presumptive deletion)",
"deputy.ia.content.batchListing.pd": "Adding batch listing: \"[[$1#$2|$2]]\" (presumptive deletion)",
"deputy.ia.content.listingComment.pd": "presumptive deletion from [[$1/$2|$2]]. $3",
"deputy.ia.content.batchListingComment.pd": "Presumptive deletion from [[$1/$2|$2]]. $3",
"deputy.ia.content.hideAll.pd": "Hiding page content for presumptive deletion; see [[$1/$2]]",
"deputy.ia.content.hide.pd": "Hiding sections [[$1#$2|$3]] to [[$1#$4|$5]] for presumptive deletion; see [[$6/$7]]",
"deputy.ia.content.copyvio": "⛔ Content on this page has been temporarily hidden due to a suspected copyright violation",
"deputy.ia.content.copyvio.help": "Please see this wiki's noticeboard for copyright problems for more information.",
"deputy.ia.content.copyvio.from": "The following reason/source was provided:",
"deputy.ia.content.copyvio.from.pd": "The content was presumptively removed based on the following contributor copyright investigation:",
"deputy.ia.content.copyvio.content": "The following content may be a copyright violation. Please do not unhide it unless you have determined that it is compatible with this wiki's copyright license.",
"deputy.ia.listing.new": "New listing",
Line 12,656 ⟶ 15,144:
"deputy.ia.listing.new.title.label": "Batch title",
"deputy.ia.listing.new.title.placeholder": "Articles from ...",
"deputy.ia.listing.new.presumptive.label": "This is for presumptive deletion",
"deputy.ia.listing.new.presumptive.help": "Presumptive deletions are content removals where the actual source of copied content cannot be determined, but due to the history of the user, it is most likely a copyright violation. Enabling this will change related edit summaries and listing text.",
"deputy.ia.listing.new.presumptiveCase.label": "Case title",
"deputy.ia.listing.new.presumptiveCase.help": "The title of the case on a list of contributor copyright investigations. This is used to link to the case from the listing.",
"deputy.ia.listing.new.comments.label": "Batch listing comments",
"deputy.ia.listing.new.comments.placeholder": "Comments for each article",
Line 12,677 ⟶ 15,169:
"deputy.ia.listing.re.where": "No vio found, claim cannot be validated. Tag removed from article.",
"deputy.ia.listing.re.unsure": "No source found; copy-paste tag removed and cv-unsure tag placed at article talk.",
"deputy.ia.listing.re.deletedcup": "Copyright concerns remain. Article deleted, left {{Template:Cup}} notice.",
"deputy.ia.listing.re.relist": "Permission plausible. Article relisted under today.",
"deputy.ia.listing.re.resolved": "Issue resolved.",
Line 12,694 ⟶ 15,186:
"deputy.ia.listing.re.unverified": "Permission unverified as of this tagging; article will need to be deleted if that does not change.",
"deputy.ia.listing.re.viable": "Viable rewrite proposed; rewrite on temp page can be merged into the article.",
"deputy.ia.report.intro": "You are reporting to <b>{{wikilink:[[$1}}]]</b>",
"deputy.ia.report.page": "Currently reporting <b>{{wikilink:[[$1}}]]</b>",
"deputy.ia.report.lead": "Lead section",
"deputy.ia.report.end": "End of page",
Line 12,706 ⟶ 15,198:
"deputy.ia.report.endSection.label": "Ending section",
"deputy.ia.report.endSection.help": "This setting is inclusive, meaning it will also hide the section indicated.",
"deputy.ia.report.presumptive.label": "This is for presumptive deletion",
"deputy.ia.report.presumptive.help": "Presumptive deletions are content removals where the actual source of copied content cannot be determined, but due to the history of the user, it is most likely a copyright violation. Enabling this will change related edit summaries and listing text.",
"deputy.ia.report.presumptiveCase.label": "Case title",
"deputy.ia.report.presumptiveCase.help": "The title of the case on a list of contributor copyright investigations. This is used to link to the case from the listing.",
"deputy.ia.report.fromUrls.label": "Content copied from online sources",
"deputy.ia.report.fromUrls.help": "URLs will automatically be wrapped with brackets to shorten the external link. Disabling this option will present the text as is.",
Line 12,715 ⟶ 15,211:
"deputy.ia.report.submit": "Submit",
"deputy.ia.report.hide": "Hide content only",
"deputy.ia.report.hide.confirm": "This will insert the {{Template:copyvio}} template and hide page content as set, but will not post a listing for this page on the noticeboard. Are you sure you don't want to list this page on the noticeboard?",
"deputy.ia.report.success": "Page content hidden and reported",
"deputy.ia.report.success.hide": "Page content hidden",
"deputy.ia.report.success.report": "Page reported",
"deputy.ia.report.error.report": "An error occurred while trying to save the entry to today's noticeboard listings. Please visit the noticeboard page and select \"Add listing\" or file the listing manually.",
"deputy.ia.report.error.shadow": "An error occurred while trying to append the {{Template:copyvio}} template on the page. Please manually insert the template.",
"deputy.ia.hiddenVio": "A user has marked content on this page as a suspected copyright violation. It is currently hidden from normal viewers of this page while awaiting further action.",
"deputy.ia.hiddenVio.show": "Show hidden content",
"deputy.ia.hiddenVio.hide": "Hide hidden content"
};
 
Line 12,733 ⟶ 15,232:
*/
class CopyrightProblemsPage {
/**
* Private constructor. Use `get` instead to avoid cache misses.
*
* @param listingPage
* @param revid
*/
constructor(listingPage, revid) {
this.title = listingPage;
this.main = CopyrightProblemsPage.rootPage.getPrefixedText() ===
listingPage.getPrefixedText();
this.revid = revid;
}
/**
* @return See {@link WikiConfiguration#ia}.rootPage.
Line 12,794 ⟶ 15,281:
return page;
}
}
/**
* Private constructor. Use `get` instead to avoid cache misses.
*
* @param listingPage
* @param revid
*/
constructor(listingPage, revid) {
this.title = listingPage;
this.main = CopyrightProblemsPage.rootPage.getPrefixedText() ===
listingPage.getPrefixedText();
this.revid = revid;
}
/**
Line 12,846 ⟶ 15,345:
}
const config = yield window.InfringementAssistant.getWikiConfig();
const preloadText = config.ia.preload.get() ? `{{subst:${config.ia.preload.get()}}}` : '';
// Only trim last newline, if any.
config.ia.preload.get().replace(/\n$/, '')}}}\n` : '';
const textParameters = appendMode ? {
appendtext: '\n' + content,
nocreate: true
} : {
Line 12,856 ⟶ 15,357:
// The `catch` statement here can theoretically create an infinite loop given
// enough race conditions. Don't worry about it too much, though.
yield MwApi.action.postWithEditToken(Object.assign(Object.assign(Object.assign(Object.assign({}, changeTag(yield window.InfringementAssistant.getWikiConfig())), { action: 'edit', title: listingPage.getPrefixedText() }), textParameters), { summary })).catchthen((code) => {
// Purge the main listing page.
return MwApi.action.post({
action: 'purge',
titles: CopyrightProblemsPage.rootPage.getPrefixedText()
});
}).catch((code) => {
if (code === 'articleexists') {
// Article exists on non-append mode. Attempt a normal append.
Line 12,884 ⟶ 15,391:
* @param page
* @param comments
* @param presumptive
*/
postListing(page, comments, presumptive) {
return __awaiter(this, void 0, void 0, function* () {
const listingPage = this.main ? CopyrightProblemsPage.getCurrentListingPage() : this.title;
yield this.tryListingAppend(this.getListingWikitext(page, comments), decorateEditSummary(mw.msg(presumptive ?
'deputy.ia.content.listing.pd' :
'deputy.ia.content.listing', listingPage.getPrefixedText(), page.getPrefixedText()), window.InfringementAssistant.config));
});
}
Line 12,915 ⟶ 15,425:
* @param title
* @param comments
* @param presumptive
*/
postListings(page, title, comments, presumptive) {
return __awaiter(this, void 0, void 0, function* () {
const listingPage = this.main ? CopyrightProblemsPage.getCurrentListingPage() : this.title;
yield this.tryListingAppend(this.getBatchListingWikitext(page, title, comments), decorateEditSummary(mw.msg(presumptive ?
'deputy.ia.content.batchListing', listingPage.getPrefixedText(),pd' title)));:
'deputy.ia.content.batchListing', listingPage.getPrefixedText(), title), window.InfringementAssistant.config));
});
}
Line 12,940 ⟶ 15,453:
 
/**
* Extracts a page title from a MediaWiki anchor`<a>`. If the anchorlink does not validly linkpoint
* to a MediaWiki page, `false` is returned.
*
* The part of the anchorlink used to determine the page title depends on how trustworthy
* the data is in telling the correct title. If the anchorlink does not have an `href`, only
* two routes are available: the selflink check and the `title` attribute check.
*
Line 12,957 ⟶ 15,470:
* @return the page linked to
*/
function anchorToTitlepagelinkToTitle(el) {
const href = el.getAttribute('href');
const articlePathRegex = new RegExp(mw.util.getUrl('(.*)'));
Line 13,009 ⟶ 15,522:
}
 
/**
* Check if a given copyright problems listing is full.
*
* @param data
* @return `true` if the listing is a {@link FullCopyrightProblemsListingData}
*/
function isFullCopyrightProblemsListing(data) {
return data.basic === false;
}
/**
* Represents an <b>existing</b> copyright problems listing. To add or create new
Line 13,014 ⟶ 15,536:
*/
class CopyrightProblemsListing {
/**
* Creates a new listing object.
*
* @param data Additional data about the page
* @param listingPage The page that this listing is on. This is not necessarily the page that
* the listing's wikitext is on, nor is it necessarily the root page.
* @param i A discriminator used to avoid collisions when a page is listed multiple times.
*/
constructor(data, listingPage, i = 1) {
this.listingPage = listingPage !== null && listingPage !== void 0 ? listingPage : CopyrightProblemsPage.get(data.listingPage);
this.i = Math.max(1, i); // Ensures no value below 1.
this.basic = data.basic;
this.title = data.title;
this.element = data.element;
if (data.basic === false) {
this.anchor = data.anchor;
this.plainlinks = data.plainlinks;
}
}
/**
* Responsible for determining listings on a page. This method allows for full-metadata
Line 13,039 ⟶ 15,542:
* This regular expression must catch three groups:
* - $1 - The initial `* `, used to keep the correct number of whitespace between parts.
* - $2 - The page title in the `id="..."`, ONLY IF the page is listed with an
* `article-cv`-like template.
* - $3 - The page title in the wikilink, ONLY IF the page is a bare link to another page and doeslisted notwith usean
* `article-cv`-like template.
* - $4 - The page title, ONLY IF the page is a bare link to another page and does not use
* `article-cv`.
*
*
* @return A regular expression.
*/
static get articleCvRegex() {
// Acceptable level of danger; global configuration is found only in trusted
// places (see WikiConfiguration documentation).
// eslint-disable-next-line security/detect-non-literal-regexp
return new RegExp(window.InfringementAssistant.wikiConfig.ia.listingWikitextMatch.get());
}
Line 13,062 ⟶ 15,570:
*/
static getListingHeader(el) {
var _a;
let listingPage = null;
let previousPivot = (
// Target the ol/ul element itself if a list, target the <p> if not a list.
el.parentElement.tagName === 'LI' ? el.parentElement.parentElement : el.parentElement).previousElementSibling;
let heading;
while (previousPivot != null && previousPivot.tagName !== 'H4') {
// Search for a level 4 heading backwards.
while (previousPivot != null &&
// Set the ceiling to be immediately above for efficiency.
((_a = (heading = normalizeWikiHeading(previousPivot, previousPivot.parentElement))) === null || _a === void 0 ? void 0 : _a.level) !== 4) {
previousPivot = previousPivot.previousElementSibling;
}
Line 13,072 ⟶ 15,585:
return false;
}
if// At this point, (previousPivot.querySelector('.mw-headline') !=is null)likely {a MediaWiki level 4 heading.
const h4Anchor = heading.h.querySelector('a');
// At this point, previousPivot is likely a MediaWiki level 4 heading.
if const (h4Anchor) = previousPivot.querySelector('.mw-headline a');{
listingPage = anchorToTitlepagelinkToTitle(h4Anchor);
// Identify if the page is a proper listing page (within the root page's
// pagespace)
Line 13,112 ⟶ 15,625:
// This ensures we're always using the prefixedDb version of the title (as
// provided by the anchor) for stability.
const prefixedDbid = anchor.getAttribute('id');
const title = anchorToTitlepagelinkToTitle(el);
if (title === false || id == null) {
// Not a valid link.
return false;
}
else if (title.getPrefixedText() !== new mw.Title(prefixedDbid).getPrefixedText()) {
// Anchor and link mismatch. Someone tampered with the template?
// In this case, rely on the link instead, as the anchor is merely invisible.
console.warn(`Anchor and link mismatch for "${title.getPrefixedText()}".`, title, prefixedDbid);
}
// Checks for the <span class="plainlinks"> element.
// This ensures that the listing came from {{article-cv}} and isn't just a
// link with an anchor.
const plainlinkselSiblings = Array.from(el.nextElementSiblingparentElement.children);
const elIndex = elSiblings.indexOf(el);
const plainlinks = el.parentElement.querySelector(`:nth-child(${elIndex}) ~ span.plainlinks`);
if (plainlinks == null ||
(plainlinks.tagName// !==`~` 'SPAN'never &&gets !plainlinks.classList.contains(an earlier element, so just check if it'plainlinks')))s more than 2 {elements
// away.
elSiblings.indexOf(plainlinks) - elIndex > 2) {
return false;
}
Line 13,142 ⟶ 15,659:
return {
basic: false,
id,
title,
listingPage,
Line 13,150 ⟶ 15,668:
}
catch (e) {
console.warn("Couldn't parse listing. Might be malformed?", e, el);
return false;
}
}
/**
* A much more loose version of {@link CopyrightProblemsListing#getListing}, which only checks if a given
* which only checks if a given page is a link at the start of a paragraph or `<[uo]l>` list. Metadata is
* `<[uo]l>` list. Metadata is unavailable with this method.
*
* @param el
Line 13,179 ⟶ 15,697:
}
// Attempt to extract page title.
const title = anchorToTitlepagelinkToTitle(el);
if (!title) {
return false;
Line 13,200 ⟶ 15,718:
}
catch (e) {
console.warn("Couldn't parse listing. Might be malformed?", e, el);
return false;
}
Line 13,208 ⟶ 15,726:
* wikitext.
*/
get idanchorId() {
return this.title.getPrefixedDb()id + (this.i > 1 ? `-${this.i}` : '');
}
/**
* Creates a new listing object.
*
* @param data Additional data about the page
* @param listingPage The page that this listing is on. This is not necessarily the page that
* the listing's wikitext is on, nor is it necessarily the root page.
* @param i A discriminator used to avoid collisions when a page is listed multiple times.
*/
constructor(data, listingPage, i = 1) {
this.listingPage = listingPage !== null && listingPage !== void 0 ? listingPage : CopyrightProblemsPage.get(data.listingPage);
this.i = Math.max(1, i); // Ensures no value below 1.
this.basic = data.basic;
this.title = data.title;
this.element = data.element;
if (data.basic === false) {
this.id = data.id;
this.anchor = data.anchor;
this.plainlinks = data.plainlinks;
}
}
/**
Line 13,218 ⟶ 15,756:
* the line on which the listing appears, the `end` denotes the last line
* where there is a comment on that specific listing.
*
* Use in conjunction with `listingPage.getWikitext()` to get the lines in wikitext.
*
* @return See documentation body.
*/
getListingWikitextLines() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const lines = (yield this.listingPage.getWikitext()).split('\n');
Line 13,228 ⟶ 15,769:
let endLine = null;
let bulletList;
const normalizedId = normalizeTitle((_a = this.id) !== null && _a !== void 0 ? _a : this.title).getPrefixedText();
const idMalformed = normalizedId !== this.title.getPrefixedText();
for (let line = 0; line < lines.length; line++) {
const lineText = lines[line];
Line 13,235 ⟶ 15,778:
if (startLine != null) {
if (bulletList ?
!/^(\*[*:]+|:)/g.test(lineText) :
/^[^:*]/.test(lineText)) {
return { start: startLine, end: endLine !== null && endLine !== void 0 ? endLine : startLine };
Line 13,247 ⟶ 15,790:
.exec(lineText);
if (match != null) {
// Check if this(normalizeTitle(match[2] is|| thematch[4]).getPrefixedText() page we're looking for.!==
if (normalizeTitle(match[2] || match[3]).getPrefixedText( normalizedId) !=={
this.title.getPrefixedText()) {
continue;
}
Line 13,257 ⟶ 15,799:
skipCounter++;
continue;
}
if (idMalformed && match[2] === match[3]) {
throw new Error(`Expected malformed listing with ID "${normalizedId}" and title "${this.title.getPrefixedText()}" but got normal listing.`);
}
bulletList = /[*:]/.test((match[1] || '').trim());
Line 13,263 ⟶ 15,808:
}
}
if// (startLineWe've ===reached lines.lengththe -end 1)of {the document.
// `startLine` is only //ever Lastset lineif only.the IDs match, so we can safely assume
// that if `startLine` returnand {`endLine` start:is startLine,set end:or if `startLine` is the last };line
// in the page, then we've found the listing (and it is the last listing on the
// page, where `endLine` would have been set if it had comments).
if ((startLine != null && endLine != null) ||
(startLine != null && startLine === lines.length - 1)) {
return { start: startLine, end: endLine !== null && endLine !== void 0 ? endLine : startLine };
}
// Couldn't find an ending. Malformed listing?
// It should be nearly impossible to hit this condition.
// Gracefully handle this.
throw new Error("Couldn'Listingt isdetect missinglisting from wikitext or(edit malformedconflict/is listing'it missing?)");
});
}
Line 13,284 ⟶ 15,835:
const range = yield this.getListingWikitextLines();
if (indent) {
// This usually isn't needed. {{CPC}} handles the bullet.
message = (this.element.parentElement.tagName === 'LI' ?
'*:' :
Line 13,303 ⟶ 15,855:
return __awaiter(this, void 0, void 0, function* () {
const newWikitext = yield this.addComment(message, indent);
yield MwApi.action.postWithEditToken(Object.assign(Object.assign({}, changeTag(yield window.InfringementAssistant.getWikiConfig())), { action: 'edit', format: 'json', formatversion: '2', utf8: 'true', title: this.listingPage.title.getPrefixedText(), text: newWikitext, summary: decorateEditSummary(summary !== null && summary !== void 0 ? summary : mw.msg('deputy.ia.content.respond', this.listingPage.title.getPrefixedText(), this.title.getPrefixedText()), window.InfringementAssistant.config) }));
yield MwApi.action.postWithEditToken({
action: 'edit',
format: 'json',
formatversion: '2',
utf8: 'true',
title: this.listingPage.title.getPrefixedText(),
text: newWikitext,
summary: decorateEditSummary(summary !== null && summary !== void 0 ? summary : mw.msg('deputy.ia.content.respond', this.listingPage.title.getPrefixedText(), this.title.getPrefixedText()))
});
yield this.listingPage.getWikitext(true);
});
}
/**
* Serialize this listing. Used for tests.
*/
serialize() {
return __awaiter(this, void 0, void 0, function* () {
return {
basic: this.basic,
i: this.i,
id: this.id,
title: {
namespace: this.title.getNamespaceId(),
title: this.title.getMainText(),
fragment: this.title.getFragment()
},
listingPage: {
namespace: this.listingPage.title.getNamespaceId(),
title: this.listingPage.title.getMainText(),
fragment: this.listingPage.title.getFragment()
},
lines: yield this.getListingWikitextLines()
};
});
}
Line 13,321 ⟶ 15,888:
*/
class ListingResponsePanel extends EventTarget {
/**
*
* @param originLink
* @param listing
*/
constructor(originLink, listing) {
super();
this.reloadPreviewThrottled = mw.util.throttle(this.reloadPreview, 500);
this.originLink = originLink;
this.listing = listing;
}
/**
* @return A set of possible copyright problems responses.
Line 13,339 ⟶ 15,895:
}
/**
*
* @param response
* @param locale
Line 13,353 ⟶ 15,908:
response.label :
((_c = (_b = response.label[locale]) !== null && _b !== void 0 ? _b : response.label[locale1]) !== null && _c !== void 0 ? _c : response.label[0]);
}
/**
* @param originLink
* @param listing
*/
constructor(originLink, listing) {
super();
// TODO: types-mediawiki limitation
this.reloadPreviewThrottled = mw.util.throttle(this.reloadPreview, 500);
this.originLink = originLink;
this.listing = listing;
}
/**
Line 13,396 ⟶ 15,962:
*/
renderAdditionalCommentsField() {
this.commentsField = new OO.ui.TextInputWidgetMultilineTextInputWidget({
multiline: true,
placeholder: mw.msg('deputy.ia.listing.re.extras'),
autosize: true,
Line 13,453 ⟶ 16,018:
}
catch (e) {
console.error(e);
OO.ui.alert(mw.msg('deputy.ia.listing.re.error', e.message));
this.dropdown.setDisabled(false);
Line 13,528 ⟶ 16,093:
}
return this.prefill ?
mw.format(this.prefill.template, this.listing.title.getPrefixedText(), (_c = this.comments) !== null && _c !== void 0 ? _c : '') :
this.comments;
}
Line 13,576 ⟶ 16,141:
h_1("span", { class: "ia-listing-action--bracket" }, mw.msg('deputy.ia.listing.respondPost')));
return element;
}
 
let InternalCCICaseInputWidget;
/**
* Initializes the process element.
*/
function initCCICaseInputWidget() {
InternalCCICaseInputWidget = class CCICaseInputWidget extends mw.widgets.TitleInputWidget {
/**
*
* @param config
*/
constructor(config) {
super(Object.assign(Object.assign({}, config), { inputFilter: (value) => {
const prefix = window.InfringementAssistant.wikiConfig
.cci.rootPage.get().getPrefixedText() + '/';
// Simple replace, only 1 replacement made anyway.
const trimmed = value.replace(prefix, '').trimStart();
if (config.inputFilter) {
return config.inputFilter(trimmed);
}
else {
return trimmed;
}
} }));
this.getQueryValue = function () {
return `${window.InfringementAssistant.wikiConfig.cci.rootPage.get()
.getPrefixedText()}/${this.getValue().trimEnd()}`;
};
}
};
}
/**
* Creates a new CCICaseInputWidget.
*
* @param config Configuration to be passed to the element.
* @return A CCICaseInputWidget object
*/
function CCICaseInputWidget (config) {
if (!InternalCCICaseInputWidget) {
initCCICaseInputWidget();
}
return new InternalCCICaseInputWidget(config);
}
 
Line 13,611 ⟶ 16,219:
initialize() {
super.initialize();
const intro = unwrapJQ(h_1("div", { class: "ia-report-intro" }), dangerouslySetInnerHTML: mw.message('deputy.ia.report.intro', CopyrightProblemsPage.getCurrentListingPage().getPrefixedText()).parseparseDom() });
intro.querySelector('a').setAttribute('target', '_blank');
const page = unwrapJQ(h_1("div", { class: "ia-report-intro" }), dangerouslySetInnerHTML: mw.message('deputy.ia.report.page', this.page.getPrefixedText()).parseparseDom() });
page.querySelector('a').setAttribute('target', '_blank');
this.fieldsetLayout = new OO.ui.FieldsetLayout({
Line 13,629 ⟶ 16,237:
]
}).$element);
return this;
}
/**
Line 13,657 ⟶ 16,266:
* Render OOUI FieldLayouts to be appended to the fieldset layout.
*
* @return An array of OOUI `FieldsetLayoutFieldLayout`s
*/
renderFields() {
Line 13,668 ⟶ 16,277:
$overlay: this.$overlay,
disabled: entirePageByDefault,
placeholdertitle: mw.msg('deputy.ia.report.startSection.placeholder')
}),
endSection: new OO.ui.DropdownInputWidget({
$overlay: this.$overlay,
disabled: entirePageByDefault,
placeholdertitle: mw.msg('deputy.ia.report.endSection.placeholder')
}),
presumptive: new OO.ui.CheckboxInputWidget({
selected: false
}),
presumptiveCase: CCICaseInputWidget({
allowArbitrary: false,
required: true,
showMissing: false,
validateTitle: true,
excludeDynamicNamespaces: true
}),
fromUrls: new OO.ui.CheckboxInputWidget({
Line 13,682 ⟶ 16,301:
allowArbitrary: true,
inputPosition: 'outline',
indicatorsindicator: ['required'],
placeholder: mw.msg('deputy.ia.report.sourceUrls.placeholder')
}),
Line 13,710 ⟶ 16,329:
label: mw.msg('deputy.ia.report.endSection.label'),
help: mw.msg('deputy.ia.report.endSection.help')
}),
presumptive: new OO.ui.FieldLayout(this.inputs.presumptive, {
align: 'inline',
label: mw.msg('deputy.ia.report.presumptive.label'),
help: mw.msg('deputy.ia.report.presumptive.help')
}),
presumptiveCase: new OO.ui.FieldLayout(this.inputs.presumptiveCase, {
align: 'top',
label: mw.msg('deputy.ia.report.presumptiveCase.label'),
help: mw.msg('deputy.ia.report.presumptiveCase.help')
}),
fromUrls: new OO.ui.FieldLayout(this.inputs.fromUrls, {
Line 13,792 ⟶ 16,421:
}
entirePageHiddenCheck();
});
const enablePresumptive = window.InfringementAssistant.wikiConfig.ia.allowPresumptive.get() &&
!!window.InfringementAssistant.wikiConfig.cci.rootPage.get();
fields.presumptive.toggle(enablePresumptive);
fields.presumptiveCase.toggle(false);
this.inputs.presumptive.on('change', (selected) => {
var _a;
this.data.presumptive = selected;
fields.presumptiveCase.toggle(selected);
fields.fromUrls.toggle(!selected);
if (!selected) {
if ((_a = this.data.fromUrls) !== null && _a !== void 0 ? _a : window.InfringementAssistant.config.ia.defaultFromUrls.get()) {
fields.sourceUrls.toggle(true);
// No need to toggle sourceText, assume it is already hidden.
}
else {
fields.sourceText.toggle(true);
// No need to toggle sourceText, assume it is already hidden.
}
}
else {
fields.sourceUrls.toggle(false);
fields.sourceText.toggle(false);
}
});
this.inputs.presumptiveCase.on('change', (text) => {
this.data.presumptiveCase = text.replace(window.InfringementAssistant.wikiConfig.cci.rootPage.get().getPrefixedText(), '');
});
this.inputs.fromUrls.on('change', (selected = this.data.fromUrls) => {
Line 13,808 ⟶ 16,464:
this.data.sourceText = text.replace(/\.\s*$/, '');
});
// Presumptive deletion is default false, so no need to check for its state here.
if (window.InfringementAssistant.config.ia.defaultFromUrls.get()) {
fields.sourceText.toggle(false);
Line 13,818 ⟶ 16,475:
});
return this.shadow ? getObjectValues(fields) : [
fields.presumptive, fields.presumptiveCase,
fields.fromUrls, fields.sourceUrls, fields.sourceText,
fields.additionalNotes
Line 13,884 ⟶ 16,542:
let finalPageContent;
const wikiConfig = (yield window.InfringementAssistant.getWikiConfig()).ia;
const copyvioWikitext = msgEval(wikiConfig.hideTemplate.get(), this.data.fromUrls ?{
presumptive: this.data.presumptive ? 'true' : '',
presumptiveCase: this.data.presumptiveCase ? 'true' : '',
fromUrls: this.data.fromUrls ? 'true' : '',
sourceUrls: this.data.sourceUrls ? 'true' : '',
sourceText: this.data.sourceText ? 'true' : '',
entirePage: this.data.entirePage ? 'true' : ''
}, this.data.presumptive ?
`[[${window.deputy.wikiConfig.cci.rootPage.get().getPrefixedText()}/${this.data.presumptiveCase}]]` : (this.data.fromUrls ?
(_b = ((_a = this.data.sourceUrls) !== null && _a !== void 0 ? _a : [])[0]) !== null && _b !== void 0 ? _b : '' :
this.data.sourceText), this.data.entirePage ? 'true' : 'false').text();
if (this.data.entirePage) {
finalPageContent = copyvioWikitext + '\n' + this.wikitext;
if (wikiConfig.entirePageAppendBottom.get()) {
finalPageContent += '\n' + wikiConfig.hideTemplateBottom.get();
}
}
else {
Line 13,898 ⟶ 16,567:
this.wikitext.slice(this.data.endOffset);
}
yield MwApi.action.postWithEditToken(Object.assign(Object.assign({}, changeTag(yield window.InfringementAssistant.getWikiConfig())), { action: 'edit', title: this.page.getPrefixedText(), text: finalPageContent, summary: decorateEditSummary(this.data.entirePage ?
action: 'edit', mw.msg(this.data.presumptive ?
title: this 'deputy.pageia.getPrefixedText(),content.hideAll.pd' :
text: finalPageContent 'deputy.ia.content.hideAll',
summary: decorateEditSummary(this.data.entirePage ? // Only ever used if presumptive is set.
mw.msg('deputy.ia.content(this.hideAll')data.presumptive ? :[
window.InfringementAssistant.wikiConfig
mw.msg('deputy.ia.content.hide', this.page.getPrefixedText(), (_c = this.data.startSection) === null || _c === void 0 ? void 0 : _c.anchor, (_d = this.data.startSection) === null || _d === void 0 ? void 0 : _d.line, (_e = this.data.endSection) === null || _e === void 0 ? void 0 : _e.anchor, (_f = this.data.endSection) === null || _f === void 0 ? void 0 : _f.line))
} .cci.rootPage.get();.getPrefixedText(),
this.data.presumptiveCase
] : [])) :
mw.msg(this.data.presumptive ?
'deputy.ia.content.hideAll.pd' :
'deputy.ia.content.hide', this.page.getPrefixedText(), (_c = this.data.startSection) === null || _c === void 0 ? void 0 : _c.anchor, (_d = this.data.startSection) === null || _d === void 0 ? void 0 : _d.line, (_e = this.data.endSection) === null || _e === void 0 ? void 0 : _e.anchor, (_f = this.data.endSection) === null || _f === void 0 ? void 0 : _f.line, ...(this.data.presumptive ? [
window.InfringementAssistant.wikiConfig
.cci.rootPage.get().getPrefixedText(),
this.data.presumptiveCase
] : [])), window.InfringementAssistant.config) }));
});
}
Line 13,912 ⟶ 16,590:
*/
postListing() {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const sourceUrls = (_a = this.data.sourceUrls) !== null && _a !== void 0 ? _a : [];
Line 13,922 ⟶ 16,600:
' ') :
this.data.sourceText;
const comments = (from || '').trim().length !== 0 || this.data.presumptive ?
mw.format(mw.msg('deputy.ia.content.listingComment', from, this.data.notes))presumptive :?
(_b = this 'deputy.dataia.notes) !== null && _b !== void 0 ? _bcontent.listingComment.pd' : '';
'deputy.ia.content.listingComment', ...(this.data.presumptive ? [
window.InfringementAssistant.wikiConfig
.cci.rootPage.get().getPrefixedText(),
this.data.presumptiveCase
] : [from]), (_b = this.data.notes) !== null && _b !== void 0 ? _b : '')) :
(_c = this.data.notes) !== null && _c !== void 0 ? _c : '';
yield CopyrightProblemsPage.getCurrent()
.postListing(this.page, comments, this.data.presumptive);
});
}
Line 13,943 ⟶ 16,627:
}
process.next(() => {
mw.notify(!this.shadow ?
mw.msg('deputy.ia.report.success.report') :
(action === 'hide' ?
Line 13,969 ⟶ 16,653:
}, this);
return process;
}
/**
* @param data
* @return An OOUI Process
*/
getTeardownProcess(data) {
/** @member any */
return super.getTeardownProcess.call(this, data);
}
},
// For dialogs. Remove if not a dialog.
_a.static = Object.assign(Object.assign({}, OO.ui.ProcessDialog.static), { name: 'iaSinglePageWorkflowDialog', title: mw.msg('deputy.ia'), actions: [
_a.static = {
name: 'iaSinglePageWorkflowDialog',
title: mw.msg('deputy.ia'),
actions: [
{
flags: ['safe', 'close'],
Line 13,992 ⟶ 16,665:
action: 'close'
}
] }),
},
_a);
}
Line 14,107 ⟶ 16,779:
titleMultiselect: new mw.widgets.TitlesMultiselectWidget({
inputPosition: 'outline',
allowArbitrary: false,
required: true,
showMissing: false,
validateTitle: true,
excludeDynamicNamespaces: true
}),
presumptive: new OO.ui.CheckboxInputWidget({
selected: false
}),
presumptiveCase: CCICaseInputWidget({
allowArbitrary: false,
required: true,
Line 14,129 ⟶ 16,811:
align: 'top',
label: mw.msg('deputy.ia.listing.new.comments.label')
}),
presumptive: new OO.ui.FieldLayout(inputs.presumptive, {
align: 'inline',
label: mw.msg('deputy.ia.listing.new.presumptive.label'),
help: mw.msg('deputy.ia.listing.new.presumptive.help')
}),
presumptiveCase: new OO.ui.FieldLayout(inputs.presumptiveCase, {
align: 'top',
label: mw.msg('deputy.ia.listing.new.presumptiveCase.label'),
help: mw.msg('deputy.ia.listing.new.presumptiveCase.help')
})
};
const getData = (listingPage) => {
return {
wikitext: listingPage.getBatchListingWikitext(inputs.titleMultiselect.items.map((v) => new mw.Title(v.data)), inputs.title.getValue(), inputs.commentspresumptive.getValue()), ?
summary: mw.msg('deputy.ia.content.batchListingbatchListingComment.pd', listingPagewindow.titleInfringementAssistant.getPrefixedText(), delink(inputs.title.getValue()))wikiConfig
.cci.rootPage.get().getPrefixedText(), inputs.presumptiveCase.getValue(), inputs.comments.getValue()) :
inputs.comments.getValue()),
summary: mw.msg(inputs.presumptive.getValue() ?
'deputy.ia.content.batchListing.pd' :
'deputy.ia.content.batchListing', listingPage.title.getPrefixedText(), delink(inputs.title.getValue()))
};
};
const currentListingPage = CopyrightProblemsPage.getCurrent();
const previewPanel = h_1("div", { class: "ia-listing--preview", "data-label": mw.msg('deputy.ia.listing.new.preview') });
// TODO: types-mediawiki limitation
const reloadPreview = mw.util.throttle(() => __awaiter(this, void 0, void 0, function* () {
const data = getData(currentListingPage);
Line 14,306 ⟶ 17,004:
headingSets[listingPageTitle] = {};
}
const prefixedDbid = listingData.title.getPrefixedDbnormalizeTitle(isFullCopyrightProblemsListing(listingData); ?
listingData.id :
listingData.title).getPrefixedDb();
const pageSet = headingSets[listingPageTitle];
if (pageSet[prefixedDbid] != null) {
pageSet[prefixedDbid]++;
}
else {
pageSet[prefixedDbid] = 1;
}
this.listingMap.set(link, new CopyrightProblemsListing(listingData, this.main ? null : this, pageSet[prefixedDbid]));
links.push(link);
}
Line 14,343 ⟶ 17,043:
}
/**
* Adds a panel containing the "new listing" buttons (single and multiple)
*
* and the panel container (when filing a multiple-page listing) to the proper
* ___location: either at the end of the copyright problems section or replacing
* the redlink to the blank copyright problems page.
*/
addNewListingsPanel() {
document.querySelectorAll('.mw-headline >a, .mw-heading a, a.external, a.redlink').forEach((el) => {
const href = el.getAttribute('href');
const url = new URL(href, window.___location.href);
if (equalTitle(url.searchParams.get('title'), CopyrightProblemsPage.getCurrentListingPage()) ||
url.pathname === mw.util.getUrl(CopyrightProblemsPage.getCurrentListingPage().getPrefixedText())) {
if (el.classList.contains('external') || el.classList.contains('redlink')) {
// Crawl backwards, avoiding common inline elements, to see if this is a standalone
// lineKeep withincrawling up and find the renderedparent of this element that is text.directly
let currentPivot = el // below the parser root or the current section.parentElement;
while ( let currentPivot !== null &&el;
['I',while 'B', 'SPAN', 'EM', 'STRONG'].indexOf(currentPivot.tagName) !== -1)null {&&
currentPivot = !currentPivot.parentElement;classList.contains('mw-parser-output') &&
} ['A', 'I', 'B', 'SPAN', 'EM', 'STRONG']
// By this point, current pivot will be a <div>, <p>, or other.indexOf(currentPivot.tagName) usable!== element.-1) {
if (!el currentPivot = currentPivot.parentElement.classList.contains('mw-headline') &&;
(currentPivot == null ||
currentPivot.children.length > 1)) {
return;
}
else if (el.parentElement.classList.contains('mw-headline')) {
// "Edit source" button of an existing section heading.
let headingBottom = el.parentElement.parentElement.nextElementSibling;
let pos = 'beforebegin';
while (headingBottom != null &&
!/^H[123456]$/.test(headingBottom.tagName)) {
headingBottom = headingBottom.nextElementSibling;
}
if// (headingBottomWe're ==now null)at {the <p> or <div> or whatever.
// Check if it headingBottomonly =has el.parentElement.parentElement.parentElement;one child (the tree that contains this element)
// and if so, posreplace =the 'beforeend';links.
if (currentPivot.children.length > 1) {
return;
}
// Add below today's section header.
mw.loader.using([
'oojs-ui-core',
Line 14,383 ⟶ 17,076:
'mediawiki.widgets.TitlesMultiselectWidget'
], () => {
//swapElements(currentPivot, H4NewCopyrightProblemsListing());
headingBottom.insertAdjacentElement(pos, NewCopyrightProblemsListing());
});
}
else {
// This is in a heading. Let's place it after the section heading.
const heading = normalizeWikiHeading(el);
if (heading.root.classList.contains('dp-ia-upgraded')) {
return;
}
heading.root.classList.add('dp-ia-upgraded');
mw.loader.using([
'oojs-ui-core',
Line 14,394 ⟶ 17,092:
'mediawiki.widgets.TitlesMultiselectWidget'
], () => {
swapElementsheading.root.insertAdjacentElement(el'afterend', NewCopyrightProblemsListing());
});
}
Line 14,402 ⟶ 17,100:
}
 
var iaStyles = ".ia-listing-action {display: inline-block;}body.ltr .ia-listing-action {margin-left: 0.5em;}body.ltr .ia-listing-action--bracket:first-child,body.rtl .ia-listing-action--bracket:first-child {margin-right: 0.2em;}body.rtl .ia-listing-action {margin-right: 0.5em;}body.ltr .ia-listing-action--bracket:last-child,body.rtl .ia-listing-action--bracket:last-child {margin-left: 0.2em;}.ia-listing-action--link[disabled] {color: gray;pointer-events: none;}@keyframes ia-newResponse {from { background-color: #ffe29e }to { background-color: rgba( 0, 0, 0, 0 ); }}.ia-newResponse {animation: ia-newResponse 2s ease-out;}.ia-listing-response, .ia-listing-new {max-width: 50em;}.ia-listing-response {margin-top: 0.4em;margin-bottom: 0.4em;}.mw-content-ltr .ia-listing-response, .mw-content-rtl .mw-content-ltr .ia-listing-response {margin-left: 1.6em;margin-right: 0;}.mw-content-rtl .ia-listing-response, .mw-content-ltr .mw-content-rtl .ia-listing-response {margin-left: 0;margin-right: 1.6em;}.ia-listing-response > div {margin-bottom: 8px;}.ia-listing--preview {box-sizing: border-box;background: #f6f6f6;padding: 0.5em 1em;overflow: hidden;}/** \"Preview\" */.ia-listing--preview::before {content: attr(data-label);color: #808080;display: block;margin-bottom: 0.2em;}.ia-listing-response--submit {text-align: right;}/** * NEW LISTINGS */.ia-listing-newPanel {margin-top: 0.5em;}.ia-listing-new {display: flex;align-items: end;margin-top: 0.5em;padding: 1em;}.ia-listing-new--field {flex: 1;}.ia-listing-new--cancel {margin-left: 0.5em;}.ia-batchListing-new {padding: 1em;max-width: 50em;}.ia-batchListing-new--buttons {display: flex;justify-content: end;margin-top: 12px;}.ia-batchListing-new .ia-listing--preview {margin-top: 12px;}/** * REPORTING DIALOG */.ia-report-intro {font-size: 0.8rem;padding-bottom: 12px;border-bottom: 1px solid gray;margin-bottom: 12px;}.ia-report-intro b {display: block;font-size: 1rem;}.ia-report-submit {padding-top: 12px;display: flex;justify-content: flex-end;}/** * COPYVIO PREVIEWS */.copyvio.deputy-show {display: inherit !important;border: 0.2em solid #f88;padding: 1em;}.dp-hiddenVio {display: flex;flex-direction: row;margin: 1em 0;}.dp-hiddenVio-message {flex: 1;}.dp-hiddenVio-actions {flex: 0;margin-left: 1em;display: flex;flex-direction: column;justify-content: center;}";
 
/**
*
*/
class HiddenViolationUI {
/**
* @param el
*/
constructor(el) {
if (!el.classList.contains('copyvio') && !el.hasAttribute('data-copyvio')) {
throw new Error('Attempted to create HiddenViolationUI on non-copyvio element.');
}
this.vioElement = el;
}
/**
*
*/
attach() {
this.vioElement.insertAdjacentElement('beforebegin', h_1("div", { class: "deputy dp-hiddenVio" },
h_1("div", { class: "dp-hiddenVio-message" }, this.renderMessage()),
h_1("div", { class: "dp-hiddenVio-actions" }, this.renderButton())));
this.vioElement.classList.add('deputy-upgraded');
}
/**
* @return A message widget.
*/
renderMessage() {
return unwrapWidget(DeputyMessageWidget({
type: 'warning',
label: mw.msg('deputy.ia.hiddenVio')
}));
}
/**
* @return A button.
*/
renderButton() {
const button = new OO.ui.ToggleButtonWidget({
icon: 'eye',
label: mw.msg('deputy.ia.hiddenVio.show')
});
button.on('change', (shown) => {
button.setLabel(shown ? mw.msg('deputy.ia.hiddenVio.hide') : mw.msg('deputy.ia.hiddenVio.show'));
button.setIcon(shown ? 'eyeClosed' : 'eye');
this.vioElement.appendChild(h_1("div", { style: "clear: both;" }));
this.vioElement.classList.toggle('deputy-show', shown);
});
return unwrapWidget(button);
}
}
 
/**
Line 14,436 ⟶ 17,183:
return false;
}
yield Promise.all([
DeputyLanguage.load('shared', deputySharedEnglish),
DeputyLanguage.loadMomentLocale()
]);
mw.hook('ia.preload').fire();
mw.util.addCSS(iaStyles);
Line 14,467 ⟶ 17,210:
// Query parameter-based autostart disable (i.e. don't start if param exists)
if (!/[?&]ia-autostart(=(0|no|false|off)?(&|$)|$)/.test(window.___location.search)) {
yield thismw.initloader.using(InfringementAssistant.dependencies, () => __awaiter(this, void 0, void 0, function* (); {
yield this.init();
}));
return true;
}
return true;
Line 14,482 ⟶ 17,228:
this.wikiConfig.ia.listingWikitextMatch.get() != null &&
this.wikiConfig.ia.responses.get() != null) {
yield DeputyLanguage.loadMomentLocale();
this.session = new CopyrightProblemsSession();
mw.hook('wikipage.content').add((el) => {
Line 14,494 ⟶ 17,241:
});
}
mw.hook('wikipage.content').add(() => {
mw.loader.using([
'oojs-ui-core',
'oojs-ui-widgets',
'oojs-ui.styles.icons-alerts',
'oojs-ui.styles.icons-accessibility'
], () => {
document.querySelectorAll('.copyvio:not(.deputy-upgraded), [data-copyvio]:not(.deputy-upgraded)').forEach((el) => {
new HiddenViolationUI(el).attach();
});
});
});
});
}
/**
* Opens the workflow dialog.
*/
openWorkflowDialog() {
return __awaiter(this, void 0, void 0, function* () {
returnyield mw.loader.using(InfringementAssistant.dependencies, () => __awaiter(this, void 0, void 0, function* () {
yield DeputyLanguage.loadMomentLocale();
if (!this.dialog) {
yield DeputyLanguage.loadMomentLocale();
this.dialog = SinglePageWorkflowDialog({
page: new mw.Title(mw.config.get('wgPageName')),
Line 14,509 ⟶ 17,270:
this.windowManager.addWindows([this.dialog]);
}
yield this.windowManager.openWindow(this.dialog).opened;
}));
});
Line 14,521 ⟶ 17,282:
'mediawiki.util',
'mediawiki.api',
'mediawiki.Title',
'mediawiki.widgets'
];
 
Line 14,537 ⟶ 17,299:
var _a;
const page = normalizeTitle();
if (page.namespacegetNamespaceId() === nsId('special') ||
page.namespacegetNamespaceId() === nsId('media')) {
// Don't save virtual namespaces.
return;
Line 14,565 ⟶ 17,327:
}
Recents.key = 'mw-userjs-recents';
 
/**
* Applies configuration overrides. This takes two objects, A and B.
* A's keys will be respected and will remain unchanged. Object
* values of A that also exist in B will be overwritten with its
* values in B.
*
* @param data
* @param overrides
* @param logger
*/
function applyOverrides(data, overrides, logger) {
if (overrides) {
for (const category of Object.keys(data)) {
if (!overrides[category]) {
continue; // Category does not exist.
}
for (const categoryKey of Object.keys(overrides[category])) {
if (logger) {
logger(`${category}.${categoryKey}`, data[category][categoryKey], overrides[category][categoryKey]);
}
data[category][categoryKey] =
overrides[category][categoryKey];
}
}
for (const category of Object.keys(overrides)) {
if (!data[category]) {
data[category] = overrides[category];
if (logger) {
logger(`${category}`, data[category], overrides[category]);
}
}
}
}
}
 
/**
* Runs a clean operation. If `option` is false or null, the operation will not be run.
*
* @param obj
* @param option
* @param callback
*/
function onOption(obj, option, callback) {
if (option == null || option === false) {
return;
}
for (const key of Object.keys(obj)) {
if (option === true ||
option === key ||
(Array.isArray(option) && option.indexOf(key) !== -1)) {
const result = callback(obj[key]);
if (result === undefined) {
delete obj[key];
}
else {
obj[key] = result;
}
}
}
}
/**
* Cleans a parameter list. By default, this performs the following:
* - Removes all undefined, null, or empty values
* - Trims all strings
* - Removes newly undefined, null, or empty values
*
* This mutates the original object and also returns it for chaining.
*
* @param obj
* @param _options
* @return The cleaned parameter list.
*/
function cleanParams(obj, _options = {}) {
const defaultOptions = {
trim: true,
filter: true,
removeYes: false,
removeNo: false,
filter2: true
};
const options = Object.assign({}, defaultOptions, _options);
// First clean pass
onOption(obj, options.filter, (v) => !v || v.length === 0 ? undefined : v);
onOption(obj, options.trim, (v) => v.trim ? v.trim() : v);
onOption(obj, options.removeYes, (v) => yesNo(v, false) ? undefined : v);
onOption(obj, options.removeNo, (v) => yesNo(v, true) ? undefined : v);
// Second clean pass
onOption(obj, options.filter, (v) => !v || v.length === 0 ? undefined : v);
return obj;
}
 
/**
* Iterates over an array and returns an Iterator which checks each element
* of the array sequentially for a given condition (predicated by `condition`)
* and returns another array, containing an element where `true` was returned,
* and every subsequent element where the check returns `false`.
*
* @param arr
* @param condition
* @yield The found sequence
*/
function* pickSequence(arr, condition) {
let currentValues = null;
let shouldReturnValues = false;
for (const val of arr) {
if (condition(val)) {
shouldReturnValues = true;
if (currentValues != null) {
yield currentValues;
}
currentValues = [val];
continue;
}
if (shouldReturnValues) {
currentValues.push(val);
}
}
if (currentValues.length > 0) {
yield currentValues;
}
}
 
/**
* Unwraps an element into its child elements. This entirely discards
* the parent element.
*
* @param el The element to unwrap.
* @return The unwrapped element.
*/
function unwrapElement (el) {
return Array.from(el.childNodes).map(v => v instanceof HTMLElement ? v :
(v instanceof Text ? v.textContent : undefined)).filter(v => v !== undefined);
}
 
var util = {
applyOverrides: applyOverrides,
blockExit: blockExit$1,
classMix: classMix,
cleanParams: cleanParams,
cloneRegex: cloneRegex$1,
copyToClipboard: copyToClipboard,
dangerModeConfirm: dangerModeConfirm,
equalTitle: equalTitle,
error: error,
findNextSiblingElement: findNextSiblingElement,
fromObjectEntries: fromObjectEntries,
generateId: generateId,
getObjectValues: getObjectValues,
last: last,
log: log,
matchAll: matchAll,
moveToStart: moveToStart,
organize: organize,
pickSequence: pickSequence,
removeElement: removeElement,
Requester: Requester,
sleep: sleep,
swapElements: swapElements,
unwrapElement: unwrapElement,
unwrapJQ: unwrapJQ,
unwrapWidget: unwrapWidget,
warn: warn,
yesNo: yesNo
};
 
/**
* Finds a MediaWiki section heading from the current DOM using its title.
*
* @param sectionHeadingName The name of the section to find.
* @param n The `n` of the section. Starts at 1.
* @return The found section heading. `null` if not found.
*/
function findSectionHeading(sectionHeadingName, n = 1) {
let currentN = 1;
const headlines = Array.from(document.querySelectorAll(
// Old style headings
[1, 2, 3, 4, 5, 6].map(v => `h${v} > .mw-headline`).join(',') +
',' +
// New style headings
[1, 2, 3, 4, 5, 6].map(v => `mw-heading > h${v}`).join(',')));
for (const el of headlines) {
if (el instanceof HTMLElement && el.innerText === sectionHeadingName) {
if (currentN >= n) {
return el.parentElement;
}
else {
currentN++;
}
}
}
return null;
}
 
/**
* Converts a range-like Object into a native Range object.
*
* @param rangeLike The range to convert
* @param rangeLike.startContainer
* @param rangeLike.startOffset
* @param rangeLike.endContainer
* @param rangeLike.endOffset
* @return A {@link Range} object.
*/
function getNativeRange (rangeLike) {
const doc = rangeLike.startContainer.ownerDocument;
const nativeRange = doc.createRange();
nativeRange.setStart(rangeLike.startContainer, rangeLike.startOffset);
nativeRange.setEnd(rangeLike.endContainer, rangeLike.endOffset);
return nativeRange;
}
 
/**
* From a list of page titles, get which pages exist.
*
* @param pages The pages to search for
* @return An array of pages which exist, ordered by input order.
*/
function getPageExists (pages) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
if (!Array.isArray(pages)) {
pages = [pages];
}
const pageNames = pages
.map(p => p instanceof mw.Title ? p.getPrefixedText() : p);
const pageRequest = (yield MwApi.action.get({
action: 'query',
titles: pageNames.join('|')
})).query;
const existingPages = [];
if (pageRequest.pages.length > 0) {
const redirects = toRedirectsObject(pageRequest.redirects, pageRequest.normalized);
const pageMap = Object.fromEntries(pageRequest.pages.map((v) => [v.title, !v.missing]));
// Use `pages` to retain client order (assume MW response can be tampered with)
for (const loc of pageNames) {
const actualLocation = (_a = redirects[loc]) !== null && _a !== void 0 ? _a : loc;
if (pageMap[actualLocation]) {
existingPages.push(actualLocation);
}
}
}
return existingPages;
});
}
 
/**
* Get the content of a revision on-wiki.
*
* @param revision The revision ID of the revision to get the content of
* @param extraOptions Extra options to pass to the request
* @param api The API object to use
* @return A promise resolving to the page content
*/
function getRevisionContent (revision, extraOptions = {}, api = MwApi.action) {
return api.get(Object.assign({ action: 'query', revids: revision, rvprop: 'content', rvslots: 'main', rvlimit: '1' }, extraOptions)).then((data) => {
return Object.assign(data.query.pages[0].revisions[0].slots.main.content, { contentFormat: data.query.pages[0].revisions[0].slots.main.contentformat });
});
}
 
var wikiUtil = {
decorateEditSummary: decorateEditSummary,
delink: delink,
errorToOO: errorToOO,
findSectionHeading: findSectionHeading,
getApiErrorText: getApiErrorText,
getNativeRange: getNativeRange,
getPageContent: getPageContent,
getPageExists: getPageExists,
getPageTitle: getPageTitle,
getRevisionContent: getRevisionContent,
getRevisionDiffURL: getRevisionDiffURL,
getRevisionURL: getRevisionURL,
getSectionElements: getSectionElements,
getSectionHTML: getSectionHTML,
getSectionId: getSectionId,
guessAuthor: guessAuthor,
isWikiHeading: isWikiHeading,
msgEval: msgEval,
normalizeTitle: normalizeTitle,
normalizeWikiHeading: normalizeWikiHeading,
nsId: nsId,
openWindow: openWindow,
pagelinkToTitle: pagelinkToTitle,
parseDiffUrl: parseDiffUrl,
performHacks: performHacks,
purge: purge,
renderWikitext: renderWikitext,
sectionHeadingN: sectionHeadingN,
toRedirectsObject: toRedirectsObject
};
 
var deputyAnnouncementsEnglish = {
"deputy.announcement.template.title": "Announcement title",
"deputy.announcement.template.message": "Announcement message",
"deputy.announcement.template.actionButton.label": "Button label",
"deputy.announcement.template.actionButton.title": "Button title"
};
 
/**
*
* Deputy announcements
*
* This will be loaded on all standalone modules and on main Deputy.
* Be conservative with what you load!
*
*/
class DeputyAnnouncements {
/**
* Initialize announcements.
* @param config
*/
static init(config) {
return __awaiter(this, void 0, void 0, function* () {
yield Promise.all([
DeputyLanguage.load('shared', deputySharedEnglish),
DeputyLanguage.load('announcements', deputyAnnouncementsEnglish)
]);
mw.util.addCSS('#siteNotice .deputy { text-align: left; }');
for (const [id, announcements] of Object.entries(this.knownAnnouncements)) {
if (config.core.seenAnnouncements.get().includes(id)) {
continue;
}
if (announcements.expiry && (announcements.expiry < new Date())) {
// Announcement has expired. Skip it.
continue;
}
this.showAnnouncement(config, id, announcements);
}
});
}
/**
*
* @param config
* @param announcementId
* @param announcement
*/
static showAnnouncement(config, announcementId, announcement) {
mw.loader.using([
'oojs-ui-core',
'oojs-ui.styles.icons-interactions'
], () => {
const messageWidget = DeputyMessageWidget({
classes: ['deputy'],
icon: 'feedback',
// Messages that can be used here:
// * deputy.announcement.<id>.title
title: mw.msg(`deputy.announcement.${announcementId}.title`),
// Messages that can be used here:
// * deputy.announcement.<id>.message
message: mw.msg(`deputy.announcement.${announcementId}.message`),
closable: true,
actions: announcement.actions.map(action => {
var _a;
const button = new OO.ui.ButtonWidget({
// Messages that can be used here:
// * deputy.announcement.<id>.<action id>.message
label: mw.msg(`deputy.announcement.${announcementId}.${action.id}.label`),
// Messages that can be used here:
// * deputy.announcement.<id>.<action id>.title
title: mw.msg(`deputy.announcement.${announcementId}.${action.id}.title`),
flags: (_a = action.flags) !== null && _a !== void 0 ? _a : []
});
button.on('click', action.action);
return button;
})
});
messageWidget.on('close', () => {
config.core.seenAnnouncements.set([...config.core.seenAnnouncements.get(), announcementId]);
config.save();
});
document.getElementById('siteNotice').appendChild(unwrapWidget(messageWidget));
});
}
}
DeputyAnnouncements.knownAnnouncements = {
// No active announcements
// 'announcementId': {
// actions: [
// {
// id: 'actionButton',
// flags: [ 'primary', 'progressive' ],
// action: () => { /* do something */ }
// }
// ]
// }
};
 
/**
Line 14,573 ⟶ 17,722:
*/
class Deputy {
/**
* @return An OOUI window manager
*/
get windowManager() {
if (!this._windowManager) {
this._windowManager = new OO.ui.WindowManager();
document.body.appendChild(unwrapWidget(this._windowManager));
}
return this._windowManager;
}
/**
* Initialize Deputy. This static function attaches Deputy to the `window.deputy`
* object and initializes that instance.
*/
static init() {
return __awaiter(this, void 0, void 0, function* () {
Deputy.instance = new Deputy();
window.deputy = Deputy.instance;
return window.deputy.init();
});
}
/**
* Private constructor. To access Deputy, use `window.deputy` or Deputy.instance.
*/
constructor() {
this.DeputyAPIDeputyDispatch = DeputyAPIDispatch;
this.DeputyStorage = DeputyStorage;
this.DeputySession = DeputySession;
this.DeputyPreferences = DeputyPreferences;
this.DeputyCommunications = DeputyCommunications;
this.DeputyCase = DeputyCase;
Line 14,587 ⟶ 17,756:
ContributionSurveyRow: ContributionSurveyRow
};
this.util = {util;
this.wikiUtil = cloneRegex: cloneRegex$1,wikiUtil;
getPageContent: getPageContent,
normalizeTitle: normalizeTitle,
sectionHeadingName: sectionHeadingName
};
this.modules = {
CopiedTemplateEditor: CopiedTemplateEditor,
Line 14,599 ⟶ 17,764:
/**
* This version of Deputy.
*
* @type {string}
*/
this.version = deputyVersionversion;
/**
* The current page as an mw.Title.
Line 14,618 ⟶ 17,781:
this.ia = new InfringementAssistant(this);
/* ignored */
}
/**
* @return An OOUI window manager
*/
get windowManager() {
if (!this._windowManager) {
this._windowManager = new OO.ui.WindowManager();
document.body.appendChild(unwrapWidget(this._windowManager));
}
return this._windowManager;
}
/**
Line 14,636 ⟶ 17,789:
init() {
return __awaiter(this, void 0, void 0, function* () {
// Attach modules to respective names
window.CopiedTemplateEditor = this.ante;
window.InfringementAssistant = this.ia;
mw.hook('deputy.preload').fire(this);
// Initialize the configuration
this.config = yield UserConfiguration.load();
window.deputyLang = this.config.core.language.get();
// Inject CSS
Line 14,656 ⟶ 17,810:
yield this.storage.init();
// Initialize the Deputy API interface
this.apidispatch = new DeputyAPI()Dispatch.i;
// Initialize the Deputy preferences instance
this.prefs = new DeputyPreferences();
// Initialize communications
this.comms = new DeputyCommunications();
Line 14,675 ⟶ 17,827:
}
yield this.wikiConfig.prepareEditBanners();
console.log('Loaded!');
mw.hook('deputy.load').fire(this);
// AsynchronouslyPerform reloadpost-load wiki configurationtasks.
thisyield Promise.wikiConfig.updateall().catch(() => { });[
// Show announcements (if any)
yield DeputyAnnouncements.init(this.config),
// Asynchronously reload wiki configuration.
this.wikiConfig.update().catch(() => { })
]);
});
}
Line 14,693 ⟶ 17,850:
}
}
/**
* Singleton for this class.
*
* @private
*/
Deputy.instance = new Deputy();
mw.loader.using([
'mediawiki.api',
'mediawiki.jqueryMsg',
'mediawiki.Title',
'mediawiki.util',
Line 14,708 ⟶ 17,860:
Recents.save();
performHacks();
window.deputy = Deputy.instanceinit();
window.deputy.init();
});