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

// Version       : 2.6.7

// Last-modified :  August 07, 2023

// Author        : Alexander Davronov

// Description   : Toolbar for copying diff entries from revision/contributions

//                 pages history on Wikipedia



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

 ***********************************************************************************

 ** HistoryHelper (Wikipedia script)                                              **

 ** Copyright (C) 2021- Alex A. Davronov                                          **

 **                                                                               **

 ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR    **

 ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,      **

 ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE   **

 ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER        **

 ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING       **

 ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER           **

 ** DEALINGS IN THE SOFTWARE.                                                     **

 ***********************************************************************************

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



$(function() {

	"use strict";

	// -----------------------------------------------------------------------------

	// #BROWSER POLYFILLS

	// -----------------------------------------------------------------------------s

	if (!Object.assign) { Object.assign = jQuery.extend; }



	/**

	 * @param {string} message 

	 * @param {string} indent */

	var InvalidArgumentTypeError = class extends TypeError {

		constructor(message, indent) {

			indent = indent instanceof String ? indent : "";

			message = indent + "Invalid Argument: " + message;

			super(message);

		}

	};

	// -----------------------------------------------------------------------------

	// #UTILS

	// -----------------------------------------------------------------------------

	/*

	 * Makes clipboard (temporary buffer) managment easier

	 * @example: new ClipboardBuffer().copy('foo') // copies 'foo' string to the clipboard

	 * Borrowed from Collect tracks v.2.js

	 **/

	let ClipboardBuffer = class {

		static version = "1.0.0";

		constructor(container) {

			this.container = container || document.body;

			this.id = "clipboard-area";

			this.el = this.container.querySelector("#" + this.id);

			if (!this.el) {

				this.el = document.createElement("textarea");

				this.container.appendChild(this.el);

			}



			this.el.style.position = "absolute";

			this.el.style.top = "-9999px";

			this.el.contentEditable = true;

			this.el.id = this.id;

		}

		copy(text) {

			this.el.value = text;

			this.el.select();

			var result = document.execCommand("copy");

			this.el.blur();

			return result;

		}

	};

	/**

	 * Toolbar for buttons.

	 * This class is tasked with book keeping of buttons.

	 * It can retrieve buttons to assing listeners for both pointer and keyboard.

	 * element which you can style.

	 * @since 2.6.0

	 * @example

	 * let toolbar = new Wiki.Toolbar(document.getElementById(`some-panel`))

	 *     toolbar.addMany([ ...htmlElements or oo.UI.ButtonWidgets ])

	 */



	// -----------------------------------------------------------------------------

	// #WIKI TEXT SYNTAX

	// -----------------------------------------------------------------------------

	// Wikipedia Classes NameSpace

	var Wiki = {};

	/**

	 * @since 2.6.0

	 */

	Wiki.Text = class extends String {

		static options = {}

		constructor(rawWikitext, options, C) {

			super(rawWikitext)

			this.C = Object.assign({}, C || {});

			this.options = Object.assign({}, this.constructor.options, options || {});

		}

		/**

		 * https://www.mediawiki.org/wiki/ResourceLoader/Core_modules#mediawiki.api

		 * https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Api

		 * https://www.mediawiki.org/wiki/Special:ApiSandbox#action=parse&text=%7B%7BProject:Sandbox%7D%7D&contentmodel=wikitext

		 * @example render().done((data) => …)

		 * @returns {mw.API}

		 */

		render() {

			// Get rendered wikitext with no miscelanious things

			var api = new mw.Api();

			return api.post({

				action: `parse`,

				format: `json`,

				text: this,

				contentmodel: `wikitext`,

				prop: {

					langlinks: false,

					categories: false,

					categorieshtml: false,

					links: false,

					parsetree: false,

					properties: false

				},

				preview: true

			})

		}

	};

	/** Wikipedia's Template markup as string in the form of {{}}

	 * /info/en/?search=Wikipedia:Anatomy_of_a_template

	 * @return {TemplateTag} */

	Wiki.Text.Tag = class {



		static IATE = InvalidArgumentTypeError;

		/** Basic Tokens */

		static B = "{{";

		static D = "|";

		static E = "}}";



		/**

		 * @param {String} name - Tag name e.g. diff, oldid2

		 * @param {params} params - Params of the template: {diff|param1|paramX|….}

		 */

		constructor(name, params) {

			if (new Object(name).constructor !== String) {

				throw new this.constructor.IATE(

					`Invalid arg: string expected`

				);

			}



			if (!(params instanceof Array)) {

				throw new this.constructor.IATE(

					"params have to be an array"

				);

			}



			let isParamString;

			// Replace non-string by "" (empty) string

			params = params.map((param) => {

				isParamString = new Object(param) instanceof String;

				return isParamString ? param.toString() : "";

			});



			this.name = name;

			this.params = params;

		}

		valueOf() {

			return this.toString();

		}

		toString() {

			// Create `{{name|param0|param1|paramN}}`

			let B = this.constructor.B; // Tag token

			let D = this.constructor.D; // Tag token

			let E = this.constructor.E; // Tag token

			let val = "";

			val += B;

			val += this.name;

			for (var param of this.params) {

				if (param) val += D + param;

			}

			val += E;

			return val;

		}

	};

	/**

	 * A container for Rows. Renders them into a string via toStirng()

	 * @summary Wikipedia table wikitext wrapper

	 */

	Wiki.Text.Table = class extends String {

		static IATE = InvalidArgumentTypeError;

		constructor({ cssClasses, rows }, options) {

			super();



			this.options = Object.assign(

				{

					caption: `Diffs`,

				},

				options || {}

			);



			if (!(rows instanceof Array)) {

				throw new this.constructor.IATE("rows have to be an array");

			}

			this.cssClasses = cssClasses || ``;

			this.rows = rows;

		}



		valueOf() {

			return this.toString();

		}

		toString() {

			let rowsStr = this.rows.join("\r\n");

			let classAttr = this.cssClasses ? `class="${this.cssClasses}"` : ``;

			return `{|${classAttr}\n|+${this.options.caption}\n${rowsStr}\n|}`;

		}

	};

	Wiki.Text.Table.Row = class extends String {

		constructor({ arr, value }, options, C) {

			if (value) {

				throw new Error(`Provide array instead`);

			}

			let rows = arr.join(`||`);



			super(`|-\n|${rows}`);

			this.C = C || {};

		}

	};

	Wiki.Text.Table.Header = class extends String {

		constructor({ arr, value }, options, C) {

			if (value) {

				throw new Error(`Provide array instead`);

			}

			let rows = arr.join(`!!`);



			super(`!${rows}`);

			this.C = C || {};

		}

	};

	Wiki.Text.Table.Def = class extends String {

		constructor(value) {

			if (new Object(value).value != null) {

				value = obj.value

			}

			super(`${value}`);

		}

	};



	// -----------------------------------------------------------------------------

	// #Wikidate

	// -----------------------------------------------------------------------------

	// @summary I convert Wikidate into Date and help to format it

	Wiki.Date = class extends Date {

		constructor(dateStr) {

			let wdate = dateStr.split(`, `);

			super(wdate.slice(1).concat(wdate0]).join(`,`));

			wdate = null;

		}



		// Default

		static dateFormat = {

			dateStyle: `medium`

			, timeStyle: "short"

			, hour12: false

		};

		// @para {object} dateFormat -  Format object, see MDN: Intl/DateTimeFormat

		format(dateFormat) {

			return Intl.DateTimeFormat(undefined, dateFormat || this.constructor.dateFormat).format(this);

		}



	}



	// -----------------------------------------------------------------------------

	// #REVISIONS ENTRIES WRAPPER

	// -----------------------------------------------------------------------------

	/**

	  * @summary Container for elements of Entry class 

	  * @class

	  */

	Wiki.Revisions = class extends Array {

		static IATE = InvalidArgumentTypeError;

		/**

		 * @param {Array<Wiki.Entry>} entries

		 * @param {HTMLElement} parentEl

		 * @param {Object} options

		 * @param {Object} C

		 */

		constructor(entries, parentEl, options, C) {

			super();

			// Context

			this.C = Object.assign({}, C || {});

			this.options = Object.assign({}, options || {});

			this.parentEl = parentEl;

			this.el = parentEl;



			if (entries instanceof Array) {

				// throw new this.constructor.IATE(`Array is expected`);

				// Sieve only Entry-based instances

				this.init(entries);

			}

		}





		/**

		 * @summary Clean up checkboxes left by previous script run

		 * @description Use after revisions.fromEl() call

		 * @param {HTMLElement} rootElement 

		 * @returns {Revisions}

		 */

		static checkboxesCleanUp(rootElement) {

			// Clean up previously created checkboxes

			if (rootElement.querySelector(`input[name="select-diff"]`)) {

				$(rootElement).find(`input[name="select-diff"]`).parent().remove();

			}

			return this

		} // checkboxesCleanUp end

		/**

		 * Helper to map HTMLElement children of revisions into Entries

		 * @param {HTMLElement} rawRevisions - An element whose children are going to be wrapped by Entry

		 * @param {Object} opt - Options for Revisions

		 * @param {Object} C   - Context for Revisions

		 * @param {Wiki.Revisions.Entry} Entry - Entry constructor

		 * @param {Object} Eopt - For Entry    - Options for Entry

		 * @param {Object} EC - Wiki UI native checkbox widget

		 * @returns Revisions

		 */

		static fromEl(rawRevisions, opt, C, Entry, Eopt, EC) {

			if (!(

				rawRevisions

				&& rawRevisions.constructor == Array

				&& rawRevisions.length > 0

			)) {

				throw this.IATE(`${Wiki.HH.NAME}: fromEl() expects an array with elements`);

			}



			if (rawRevisions0].constructor != HTMLLIElement) {

				throw this.IATE(`${Wiki.HH.NAME}: fromEl() expects an array of li elements`);

			}



			EC = Object.assign({ CheckboxInputWidget: OO.ui.CheckboxInputWidget }, EC || {});



			let entries = rawRevisions.map((el) => new Entry(el, Eopt, EC));

			// Invoking this(…) make this portable

			return new this(entries, rawRevisions, opt, C);

		}



		init(entries) {

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

				const entry = entriesi];

				if (entry instanceof this.constructor.Entry) {

					thisi = entry;

					entry.parent = this;

				}

			}

		}



		// Return array of checked entries

		checked() {

			let checked = [];

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

				if (thisi].isChecked()) {

					checked.push(thisi]);

				}

			}



			// BUG: This uncessarily registered new controls listeners if called via

			// built-in Array methods

			return new this.constructor(checked, this.parentEl, this.options, this.C);

		}

	};

	// A single revision entry line container

	Wiki.Revisions.Entry = class extends Object {

		static IATE = InvalidArgumentTypeError;

		constructor(el, options, C) {

			super();

			this.C = Object.assign({}, C || {});

			if (!(el instanceof HTMLLIElement)) {

				throw new this.constructor.IATE(`<li> element expected`);

			}

			this.el = el;

			this.init(el);

		}



		init(el) {

			// Revision link

			let href = el.querySelector(`.mw-changeslist-links > span:nth-child(2) > a`);

			if (href == null) {

				console.warn(`${Wiki.HH.NAME}: Entry()..init(): history 'prev' link isn't found, falling back to default values`);

				this.title = "";

				this.diff = "";

				this.oldid = "";

			} else {

				// TODO: BUG ON MAIN PAGE

				let urlParams = new URL(href).searchParams;

				this.title = urlParams.get(`title`);

				this.diff = urlParams.get(`amp;diff`) || urlParams.get(`diff`);

				this.oldid = urlParams.get(`amp;oldid`) || urlParams.get(`oldid`);

			}



			// Date

			let date = el.querySelector(`li > a`);

			if (date && date.textContent) {

				this.date = new Wiki.Date(date.textContent);

			}



			this.user = el.querySelector("bdi") && (el.querySelector("bdi").textContent ?? "");

			let comment = el.querySelector(".comment") ?? "";



			// Strip comments from backslash

			if (comment && comment.textContent) {

				this.comment = el

					.querySelector(".comment")

					.textContent.replace(/[\(\→]/g, "");

			} else {

				this.comment = ``;

			}

		}



		/**

		 * Insert a given el element before entry's first element

		 * @param {HTMLElement} el - element to be inserted before the first child

		 */

		insertBefore(el) {

			this.el.insertBefore(el, this.el.firstChild);

		}

	};



	/**

	 * @summary Container for elements of EntryCB class

	 * @class

	 * @since 2.6.0

	 */

	Wiki.Revisions2 = class extends Wiki.Revisions {

		/**

		 * @param {Array<Wiki.Revisions2.EntryCB>} entries 

		 * @param {HTMLElement} parentEl

		 * @param {Object} options

		 * @param {Object} C

		 */

		constructor(entries, parentEl, options, C) {

			super(entries, parentEl, options, C);

			this.i = 0;

		}

		init(entries) {

			/** @property {Boolean} - Whether any checkbox is checked*/

			this.checkboxes = [];

			this.checkboxes.parent = this;

			this.checkboxes.lastClicked = [];

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

				const entry = entriesi];

				if (entry instanceof this.constructor.Entry) {

					thisi = entry;

					// entry.parent = this;

					entry.parent = this;

					entry.checkbox.parent = this.checkboxes;

					this.checkboxes.push(entry.checkbox);

				}

			}

		}

		isAnyChecked() {

			return this.some(entry => entry.checkbox.isSelected())

		}

		checked() {

			return this.filter(entry => entry.isChecked());

		}

	};

	/**

	 * The Entry extended with a checkboxk

	 * @class

	 * @since 2.6.0

	 */

	Wiki.Revisions2.EntryCB = class extends Wiki.Revisions.Entry {

		constructor(el, options, C) {

			super(el, options, C);

			if (this.C.CheckboxInputWidget == null) {

				throw new this.constructor.IATE(`CheckboxInputWidget is missing`);

			}

			// The value is expected to be assigned by external entity

			this.parent

			this.init(el);

			this.initCheckBox();

		}



		initCheckBox() {

			this.checkbox = new this.C.CheckboxInputWidget({

				name: `select-diff`,

				value: this.el.getAttribute(`data-mw-revid`),

				selected: false,

			});

			this.checkbox.$element0].style.width = `15px`;

			this.checkbox.$element0].style.height = `15px`;

			this.checkbox.$element.mouseleave(function(e) {

				if (e.buttons === 1) {

					this.setSelected(!this.isSelected());

				}

			}.bind(this.checkbox));

			this.insertBefore(this.checkbox.$element0]);

		}



		/**

		 * @returns {Boolean} - True if checked

		 */

		isChecked() {

			return this.checkbox.isSelected();

		}

	};



	Wiki.Contributions = class extends Wiki.Revisions2 { };

	Wiki.Contributions.EntryCB = class extends Wiki.Revisions2.EntryCB {

		static IATE = InvalidArgumentTypeError;

		static UserName = mw.config.get(`wgRelevantUserName`);



		constructor(el, options, C) {

			super(el, options, C);

			if (!(el instanceof HTMLLIElement)) {

				throw new this.constructor.IATE(`<li> element expected`);

			}

			let context = {}; // The context here stands for imported object

			this.C = Object.assign(context, C || {});

			this.options = Object.assign({}, options || {});

			this.el = el;

			this.init(el);

		}

		init(el) {

			// Revision links

			let diffEl = el.querySelector(`a.mw-changeslist-diff`) || el.querySelector(`a.mw-changeslist-history`);

			if (diffEl == null) {

				throw new Error(`${Wiki.HH.NAME}: can't find diff element on collaboration page.`);

			}

			let href = diffEl.href;



			if (href == null) {

				throw new Error(`${Wiki.HH.NAME}: Entry()..init(): history 'prev' link isn't found, falling back to default values`)

				this.title = "";

				this.diff = "";

			} else {

				let urlParams = new URL(href).searchParams;

				this.title = urlParams.get(`title`);

				this.diff = this.el.dataset"mwRevid"];

			}

			this.oldid = `prev`;



			this.user = this.constructor.UserName;

			// this.user = mw.config.get(`wgRelevantUserName`);



			// Date

			let date = el.querySelector(`li > a`);

			if (date && date.textContent) {

				el.querySelector(`li > a`).textContent;

				this.date = new Wiki.Date(date.textContent);

			} else {

				this.date = new Wiki.Date(date.textContent);

			}

			this.comment = ``;

			let commentEl = this.el.querySelector(`.comment`);

			if (commentEl) {

				this.comment = commentEl.textContent.replace(/[\(\→]/g, "")



			}

		}

	};





	Wiki.Toolbar = class extends Map {

		static IATE = InvalidArgumentTypeError;

		static config = {

			id: `toolbar-default`

		}

		static buttons = {

			`info`: {

				type: `Popup`,

				disabled: true,

				title: `Click buttons on the right`,

				label: `COPY AS`,

				icon: `doubleChevronEnd`,

			},

			`as.diffs`: {

				title: `Copy selected as {{diff|…}} wikitext`,

				id: `as.diffs`,

				label: `{{diff}}`,

				icon: `code`,

				template: `{{tqb|\n%\n}}`

			},

			`as.table`: {

				title: `Copy selected as table wikitext`,

				id: `as.table`,

				label: `<Table/>`,

				icon: `table`,

				template: ``

			},

			`as.links`: {

				title: `Copy selected as raw [1]..[n] links (can be pasted into summary)`,

				id: `as.links`,

				label: `Links`,

				icon: `wikiText`,

				template: ``

			},

		};



		static notice = {

			type: 'info',

			label: 'Nothing to preview. Select checkboxes!',

			title: 'Info',

			inline: true

		}

		/**

		 *

		 * @param {HTMLElement} toolbarEl - Container

		 * @param {Array<Object>} buttons - Arrays of buttons widgets. See add() for supported ones

		 * @param {Object} options -

		 * @param {Object} C - Context

		 */

		constructor(buttons, options, C) {

			super();

			// Options.

			this.arguments = arguments;

			this.arguments1 = Object.assign({}, options || this.constructor.config);

			this.arguments2 = Object.assign({}, OO.ui, C || {});



			// Toolbar widget

			this.buttonsGroup = new this.arguments2].ButtonGroupWidget({ id: this.arguments1].id });

			this.$element = this.buttonsGroup.$element;

			this.$element.css(`z-index`, 2);

			if (buttons) {

				this.addMany(buttons);

			}

		}



		/**

		 * @typedef  {Object} OO.ui.ButtonWidget -

		 * @property {string} id -

		 * @method addItems

		 */



		/**

		 * Add every button to the group, associate buttons with IDs

		 * @example new Toolbar();

		 * @param {HTMLElement | OO.ui.ButtonGroupWidget | OO.ui.PopupButtonWidget} el

		 * @returns {Wiki.Toolbar}

		 */

		add(el) {

			if (el == null) {

				throw new this.constructor.IATE(`first argument is expected`);

			}

			switch (el.constructor) {

				case HTMLElement:

					this.buttonsGroup.$element0].appendChild(el);

					el.id && this.set(el.id, el);

					break;

				case this.arguments2].ButtonWidget:

				case this.arguments2].PopupButtonWidget:

					el.$element0].id && this.set(el.$element0].id, el);

					this.buttonsGroup.addItems([el]);

					break;

					break;

				default:

					console.warn(`toolbar.add(e): unknown e.constructor.`)

			}



			return this;

		}

		/**

		 *

		 * @param {Array<HTMLElement | OO.ui.ButtonGroupWidget>} elements

		 * @returns

		 */

		addMany(elements) {

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

				this.add(elementsi]);

			}

			return this;

		}



		toArray() {

			return Array.from(this.values())

		}

	};





	/**

	 * The HistoryHelper main class used as nameSpace.

	 * It binds provided UI elements (toolbar/revisions) and binds

	 * Pointer (mouse) and Keyboard strokes to actionsM

	 * (e.g. copy revisions to clipboard)

	 */

	Wiki.HH = class extends Object {

		static NAME = `HistoryHelper`;

		static IATE = InvalidArgumentTypeError;



		// TODO: Deprecate in favor of preview copy text field

		static shortcuts = {

			`ctrl+alt+d`: `revisions.as.diffs.to.clipboard`,

			`ctrl+alt+c`: `revisions.as.links.to.clipboard`

		}

		static options = {

			fetchLimit: 64

		}

		/**

		 * Overview of basic HistoryHelper workflows

		 * ##Clipboard workflow

		 * revisions.keyboard -> revisionsTo…(revisions) -> clipboard.copy()

		 * buttons.pointer.click  -> entries.to.markup -> clipboard.copy()

		 * buttons.pointer.hover  -> buttons.popup.showPreview(revisions.as.XYZ)

		 * ##UX workflow

		 * buttons.popup.pointer -> preview.modify()

		 * revisions.pointer -> entries.select

		 * revisions.checkboxes.pointer + keyboard.shift -> entries.select

		 * @param {Wiki.Toolbar}    revisions - Data (revisions container)

		 * @param {Wiki.Revisions}  toolbar   - Input (butttons panel)

		 * @param {ClipboardBuffer} clipboard - Output (clipboard buffer)

		 * @param {Object} options - Configuration object

		 * @param {Object} options.shortcuts - Shortcuts to Action map

		 * @param {Object} C           - Namespace for default class constructors

		 * @param {Object} C.Revisions - Revisions entries container constructor

		 * @param {Object} C.Toolbar   - 

		 * @param {Object} C.Clipboard -

		 * @param {Object} C.Text      - WikiText renderer 

		 * used to build output strings

		 */

		constructor(toolbar, revisions, clipboard, options, C) {

			super();

			this.C = {};

			this.C.Revisions = Wiki.Revisions;

			this.C.Toolbar = Wiki.Toolbar; // Containers

			this.C.Clipboard = ClipboardBuffer;

			this.C.Text = Wiki.Text;

			this.C = Object.assign(this.C, (C || {}));



			this.options = Object.assign({}, this.constructor.options, options || {});



			if (!(toolbar instanceof this.C.Toolbar)) throw new this.constructor.IATE(`toolbar   instance of Toolbar   is expected`);

			if (!(revisions instanceof this.C.Revisions)) throw new this.constructor.IATE(`revisions instance of Revisions is expected`);

			if (!(clipboard instanceof this.C.Clipboard)) throw new this.constructor.IATE(`clipboard instance of Clipboard is expected`);



			this.toolbar = toolbar;

			this.revisions = revisions;

			this.clipboard = clipboard;



			//#ACTIONS MAP

			//------------------------------------------

			// These are intended to be invoked on some user

			// actions such as click or keypress

			// These callbacks are called from multiple places

			// DPRCT: [August 07, 2023] Remove clipboard functionality

			this`revisions.as.diffs.to.clipboard` = function() {

				this.clipboard.copy(this.constructor.revisionsToDIFFS(this.revisions.checked(), undefined, options))

			}.bind(this);

			this`revisions.as.table.to.clipboard` = function() {

				this.clipboard.copy(this.constructor.revisionsToTABLE(this.revisions.checked()))

			}.bind(this);

			this`revisions.as.links.to.clipboard` = function() {

				this.clipboard.copy(this.constructor.revisionsToLINKS(this.revisions.checked()))

			}.bind(this);



			this`revisions.as.diffs.rendered` = function(cb) {

				let selected = this.revisions.checked().slice(0, this.options.fetchLimit);

				let wikitext = this.constructor.revisionsToDIFFS(selected, undefined, options);

				wikitext

					? new this.C.Text(wikitext).render().done(cb)

					: cb({});

			}.bind(this);

			this`revisions.as.table.rendered` = function(cb) {

				let selected = this.revisions.checked().slice(0, this.options.fetchLimit);

				let wikitext = this.constructor.revisionsToTABLE(selected);

				wikitext

					? new this.C.Text(wikitext).render().done(cb)

					: cb({});

			}.bind(this);

			this`revisions.as.links.rendered` = function(cb) {

				let selected = this.revisions.checked().slice(0, this.options.fetchLimit);

				let wikitext = this.constructor.revisionsToLINKS(selected);

				wikitext

					? new this.C.Text(wikitext).render().done(cb)

					: cb({});

			}.bind(this);



			this.buttons = this.toolbar.toArray();

			this.initButtons();

			this.initRevisionsListeners();

			this.initRevisionsSpecialListneners();

		} // CONSTRUCTOR END



		// Associate button clicks with actions

		initButtons() {

			//#POINTER CONTROL - BUTTONS

			//------------------------------------------

			for (let button of this.buttons) {

				button.$element.click(this`revisions.${button.elementId}.to.clipboard`]);



				// Show preview of the selected entries

				button.$element.mouseenter(function(button, e) {

					// Hide all popups

					for (let nextButton of this.buttons) {

						nextButton.popup.toggle(false);

					}

					button.popup.toggle(true);

					let d0 = button.popup.$lable.isVisible();

					if (this.revisions.isAnyChecked()) {

						button.popup.$lable.toggle(false);

						setTimeout(() => {

							this`revisions.${button.elementId}.rendered`]((response) => {

								if (response.parse) {

									button.popup.html(`${response.parse.text`*`}`)

								} else {

									button.popup.html(``);

								}

							});

						}, 300);

					} else {

						button.popup.$lable.toggle(true);

					}



				}.bind(this, button)); // bindEventEnd

			}

		}



		// Associate keyboard hotkeys with actions

		// Only works when pointer is in area of a revisions list element

		initRevisionsListeners() {

			//#KEYBOARD CONTROL

			//------------------------------------------

			if (this.options.shortcuts) {

				const ctrlKey = `ctrl`;

				const shiftKey = `shift`;

				const altKey = `alt`;

				this.revisions.parentEl.tabIndex = 1;

				$(this.revisions.parentEl).bind(`keyup`, (e) => {

					let pressedKeys = ``;

					pressedKeys += e.ctrlKey ? ctrlKey + `+` : ``;

					pressedKeys += e.shiftKey ? shiftKey + `+` : ``;

					pressedKeys += e.altKey ? altKey + `+` : ``;

					pressedKeys += e.key;

					// Match the keystroke into a an action declared above

					let action = thisthis.options.shortcutspressedKeys]];

					if (action) action();



				});

			}

		}

		// Associate keyboard + pointer hotkeys behavior

		// Allows selecting checkboxes range by using shift + checkbox click

		initRevisionsSpecialListneners() {

			//#CHECKBOXES CONTROL

			//------------------------------------------

			this.revisions.checkboxes.lastClicked1 = this.revisions.checkboxes0];

			$(this.revisions.el).click((e) => {

				// Clear up preview data

				for (let button of this.buttons) {

					button.popup.html(``);

				}

				// We need to focuse only on widget's span element

				let focusedCheckbox;

				if (e.target instanceof HTMLInputElement) {

					focusedCheckbox = e.target.parentElement;

				}

				if (e.target instanceof HTMLSpanElement

					&& /oo-ui-checkboxInputWidget/.test(e.target.className)) {

					focusedCheckbox = e.target;

				}

				/**@type Array<CheckboxInputWidgets> */

				let checkboxes = this.revisions.checkboxes;



				if (checkboxes.lastClicked1 !== focusedCheckbox) {

					checkboxes.lastClicked0 = checkboxes.lastClicked1];

					checkboxes.lastClicked1 = focusedCheckbox;

				}



				if (

					e.shiftKey &&

					checkboxes.lastClicked0 &&

					checkboxes.lastClicked1

				) {



					let from = checkboxes.findIndex((widget) => {

						return checkboxes.lastClicked0 === widget.$element0

					});

					let to = checkboxes.findIndex((widget) => {

						return checkboxes.lastClicked1 === widget.$element0

					});

					if (from > to) {

						let mid = to;

						to = from;

						from = mid;

					}

					from++;

					for (; from < to; from++) {

						checkboxesfrom].setSelected(!checkboxesfrom].isSelected())

					}

				}

			});

		}



		// Words to higlight

		static highlights = /competen(t|cy)|IR|bitch|illiterate|fuck(er)?|asshole(ery)?|troll|idiot|dumbass|stupid|blank|subhuman|autis[tm]|(edit)? warring|inept/g;



		/** Convert revisions entries into a Wikitext (diffs)

		 * @since 2.6.0

		 * @param {Wiki.Revisions} revisions - Array that contains Entry instances

		 * @param {Wiki.Text.Tag} Tag 

		 * @returns {String}

		 */

		static revisionsToDIFFS(revisions, Tag, config) {

			Tag = Tag || Wiki.Text.Tag;

			if (!(revisions)) { throw new this.IATE(`Revisions are missing`) }

			if (!revisions.length) { return `` }

			let entry, tag, wikitext = ``;

			let comment;

			let users = new Set();

			// Walk over every entry

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

				entry = revisionsi];





				if (entry && new Object(entry.user).constructor == String) {



					if (entry.user !== mw.config.get(`wgUserName`)) {

						users.add(entry.user);

					}

				}

				if (entry && new Object(entry.date).constructor == Wiki.Date) {

					entry.date = entry.date.format();

				}



				tag = new Tag(`diff`, 

					entry.diff,

					entry.oldid,

					entry.date,

				]);



				// Highlight specified by config words and phrases

				// Highlight incivility

				comment = entry.comment.replace(this.highlights, `{{highlight|$&}}`);

				let highlights = config && new Object(config.highlights);

				if (highlights

					&& highlights.constructor === Array

					&& highlights.length) {

					for (let i = 0, reg; i < highlights.length; i++) {

						reg = highlightsi];

						comment = comment.replace(reg, `{{highlight|$&}}`);

					}

				}

				

				wikitext += `* [[Special:Diff/${entry.oldid}|[${entry.date}]]] - {{User|${entry.user}}} - {{gt|''"${comment}"''}}</br>\n`;

			}

			return wikitext





		}



		/** Convert revisions entries into a Wikitext (Special:Diff/… links)

		 * @since 2.6.0

		 * @param {Wiki.Revisions} revisions - Array that contains Entry instances

		 * @returns {String}

		 */

		static revisionsToLINKS(revisions) {

			if (!(revisions)) { throw new this.IATE(`Revisions are missing`) }

			if (!revisions.length) { return `No revisions selected` }

			let entry, wikitext = ``;

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

				entry = revisionsi];



				if (entry && new Object(entry.date).constructor == Wiki.Date) {

					entry.date = entry.date.format();

				}

				// Omit prev

				let diff = entry.oldid;

				if(diff == "prev") {

					diff = entry.diff

				}

				wikitext += `# [[Special:Diff/${diff}|[${entry.date}]]]</br>\n`

			}

			return wikitext





		}

		/** Convert revisions entries into a Wikitext (tables )

		 * @since 2.6.0

		 * @param {Wiki.Revisions} revisions - Array that contains Entry instances

		 * @param {Wiki.Text.Tag} Tag 

		 * @param {Wiki.Text.Table} Table

		 * @returns {String}

		 */

		static revisionsToTABLE(revisions, Tag, Table) {

			if (!(revisions)) { throw new this.IATE(`Revisions are missing`) }

			if (!revisions.length) { return `` }



			Table = Table || Wiki.Text.Table;

			Tag = Tag || Wiki.Text.Tag;



			// Every entry wrapped into a wiki tag

			// Group of tags into table definitions (colums)

			let entry;

			let anchor, anchLink, diff, oldid, user, tags, entries;

			let defintions = [];

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

				entry = revisionsi];



				anchLink = `hist-${i}-${entry.diff}`;



				anchor = new Tag(`anchor`, anchLink]);

				diff = new Tag(`diff`, entry.oldid, entry.date]);

				oldid = new Tag(`oldid2`, 1, entry.oldid, entry.date]);

				user = new Tag(`u`, entry.user]);



				tags = 

					anchor + `[[#${anchLink}|${i}]]`,

					diff,

					oldid,

					user,

					entry.comment ? `''${entry.comment}''` : ``

				

				defintions.push(tags.map(tag => new Table.Def(tag)));



			}

			// Wrap ever column into a row

			// First row is the head

			let columns;

			let rows = 

				new Table.Header({

					arr: `#`, `DIFF`, `CURRENT`, `USER`, `SUMMARY`],

				})

			];



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

				columns = defintionsi];

				rows.push(new Table.Row({ arr: columns }))

			}



			let wikitext = new Table({

				cssClasses: "wikitable sortable",

				rows: rows,

			}).toString();

			return wikitext;







		}

	};



	//#USER CONFIG

	//------------------------------------------

	// Convert legacy (prior  2.6.0) config version into a 2.6.0 

	if (window.HistoryHelper && window.HistoryHelper.shortcuts) {

		let shortcuts = window.HistoryHelper.shortcuts;

		// 1/2 For every shortcut

		for (const key in shortcuts) {

			if (Object.hasOwnProperty.call(shortcuts, key)) {

				const actionName = shortcutskey];

				// 2/2 if an old action match, replace by a new one

				if (actionName === `copyAsdiffs`) {

					shortcutskey = `revisions.as.links.to.clipboard`;

					console.warn(`${Wiki.HH.NAME}: copyAsdiffs action is deprecated after v2.6.0, update your config`)

				}

			}

		}

	}



	let config = Object.assign(

		{}

		// Turn off default shortcuts

		// ,{ shortcuts: Wiki.HH.shortcuts},

		, window.HistoryHelper || {}

	);



	// ---------------------------------------------------------------------------

	// #MAIN

	// ---------------------------------------------------------------------------

	let main = function main() {

		let contribPageRe = /Special:Contributions/

		let isContributionsPage = contribPageRe.test(window.location.href);

		let isHistoryPage = new URL(window.location).searchParams.get("action") == "history";

		if (!(isContributionsPage || isHistoryPage)) {

			return

		}



		// Initialize toolbar & buttons

		let buttons = Object.values(Wiki.Toolbar.buttons).map((data) => {

			let $lable = new OO.ui.MessageWidget(Wiki.Toolbar.notice);

			$lable.$element.css(`min-width`, `478px`)

			let $content = $(`<div></div>`)

			// .append($notice.$element);

			let popup = new OO.ui.PopupWidget({

				width: null,

				head: true,

				label: $lable.$element,

				$content: $content,

				padded: true,

				autoClose: true,

				autoFlip: false

			});

			popup.$element.css(`z-index`, 32);

			popup.$element.css(`min-width`, `330px`);

			popup.$element.css(`min-height`, `127px`);

			popup.$content = $content;

			popup.$lable = $lable;

			popup.html = function(str) {

				return this.$content.html(str)

			}



			let button = new OO.ui.ButtonWidget({ ...data, content: popup });

			button.popup = popup;

			return button

		})

		//  New toolbar

		windowWiki.Toolbar.config.id && windowWiki.Toolbar.config.id].remove();

		let toolbar = new Wiki.Toolbar(buttons);

		//  Initialize revisions container



		let pagehistory = document.getElementById(`pagehistory`)

			|| document.querySelector(`#mw-content-text section.mw-pager-body`);

		if (!(pagehistory)) {

			throw new Error(

				`${Wiki.HH.NAME}: can't find revisions html element.

        \n\tThis is probably due to Wikipedia changing its HTML ids.

        \n\tContact the script author for help:

        \n\thttps://en.wikipedia.org/?title=User_talk:Alexander_Davronov&action=edit&section=new`

			);

			return

		}

		//  Remove old checkboxes

		Wiki.Revisions2.checkboxesCleanUp(pagehistory);



		let clipboard = new ClipboardBuffer();

		// Article or User history page

		// https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config

		if (isHistoryPage) {



			let rawRevisions = Array.from(pagehistory.querySelectorAll(`ul > li`));

			let revisions = Wiki.Revisions2.fromEl(

				rawRevisions, {}, {}

				, Wiki.Revisions2.EntryCB

			);



			// Adding tools

			let revCompareForm = document.getElementById(`mw-history-compare`);

			let toolbarContainerTarget =

				revCompareForm

				&& revCompareForm.querySelector(`.mw-history-compareselectedversions`);



			$(toolbarContainerTarget).append(toolbar.$element);

			if (toolbar.$element0 && !toolbar.$element0].children.length) {

				throw new Error(`${Wiki.HH.NAME}: Toolbar has no buttons, please fill a bug report!`);

			}



			// Init HistoryHelper controls (button press handlers)

			// over toolbar and revisions

			new Wiki.HH(toolbar, revisions, clipboard, config);

			return

		}

		// User contributions page

		let isViewing = mw.config.get(`wgAction`) === `view`;

		if (isViewing) {

			let rawRevisions = Array.from(pagehistory.querySelectorAll(`ul > li`));

			let revisions = Wiki.Contributions.fromEl(

				rawRevisions, {}, {}

				, Wiki.Contributions.EntryCB

				, { user: mw.config.get(`wgRelevantUserName`) }

			);

			let toolbarContainerTarget = document.getElementById(`mw-content-text`).firstChild;

			toolbar.$element.insertAfter(toolbarContainerTarget);

			new Wiki.HH(toolbar, revisions, clipboard, config);

			return

		}

	}



	mw.loader.using([`oojs-ui.styles.icons-editing-advanced`, `oojs-ui.styles.icons-alerts`], main);





	// From the End comes The Beginning!

	// Something ends, something begins!

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

// Version       : 2.6.7

// Last-modified :  August 07, 2023

// Author        : Alexander Davronov

// Description   : Toolbar for copying diff entries from revision/contributions

//                 pages history on Wikipedia



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

 ***********************************************************************************

 ** HistoryHelper (Wikipedia script)                                              **

 ** Copyright (C) 2021- Alex A. Davronov                                          **

 **                                                                               **

 ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR    **

 ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,      **

 ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE   **

 ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER        **

 ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING       **

 ** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER           **

 ** DEALINGS IN THE SOFTWARE.                                                     **

 ***********************************************************************************

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



$(function() {

	"use strict";

	// -----------------------------------------------------------------------------

	// #BROWSER POLYFILLS

	// -----------------------------------------------------------------------------s

	if (!Object.assign) { Object.assign = jQuery.extend; }



	/**

	 * @param {string} message 

	 * @param {string} indent */

	var InvalidArgumentTypeError = class extends TypeError {

		constructor(message, indent) {

			indent = indent instanceof String ? indent : "";

			message = indent + "Invalid Argument: " + message;

			super(message);

		}

	};

	// -----------------------------------------------------------------------------

	// #UTILS

	// -----------------------------------------------------------------------------

	/*

	 * Makes clipboard (temporary buffer) managment easier

	 * @example: new ClipboardBuffer().copy('foo') // copies 'foo' string to the clipboard

	 * Borrowed from Collect tracks v.2.js

	 **/

	let ClipboardBuffer = class {

		static version = "1.0.0";

		constructor(container) {

			this.container = container || document.body;

			this.id = "clipboard-area";

			this.el = this.container.querySelector("#" + this.id);

			if (!this.el) {

				this.el = document.createElement("textarea");

				this.container.appendChild(this.el);

			}



			this.el.style.position = "absolute";

			this.el.style.top = "-9999px";

			this.el.contentEditable = true;

			this.el.id = this.id;

		}

		copy(text) {

			this.el.value = text;

			this.el.select();

			var result = document.execCommand("copy");

			this.el.blur();

			return result;

		}

	};

	/**

	 * Toolbar for buttons.

	 * This class is tasked with book keeping of buttons.

	 * It can retrieve buttons to assing listeners for both pointer and keyboard.

	 * element which you can style.

	 * @since 2.6.0

	 * @example

	 * let toolbar = new Wiki.Toolbar(document.getElementById(`some-panel`))

	 *     toolbar.addMany([ ...htmlElements or oo.UI.ButtonWidgets ])

	 */



	// -----------------------------------------------------------------------------

	// #WIKI TEXT SYNTAX

	// -----------------------------------------------------------------------------

	// Wikipedia Classes NameSpace

	var Wiki = {};

	/**

	 * @since 2.6.0

	 */

	Wiki.Text = class extends String {

		static options = {}

		constructor(rawWikitext, options, C) {

			super(rawWikitext)

			this.C = Object.assign({}, C || {});

			this.options = Object.assign({}, this.constructor.options, options || {});

		}

		/**

		 * https://www.mediawiki.org/wiki/ResourceLoader/Core_modules#mediawiki.api

		 * https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Api

		 * https://www.mediawiki.org/wiki/Special:ApiSandbox#action=parse&text=%7B%7BProject:Sandbox%7D%7D&contentmodel=wikitext

		 * @example render().done((data) => …)

		 * @returns {mw.API}

		 */

		render() {

			// Get rendered wikitext with no miscelanious things

			var api = new mw.Api();

			return api.post({

				action: `parse`,

				format: `json`,

				text: this,

				contentmodel: `wikitext`,

				prop: {

					langlinks: false,

					categories: false,

					categorieshtml: false,

					links: false,

					parsetree: false,

					properties: false

				},

				preview: true

			})

		}

	};

	/** Wikipedia's Template markup as string in the form of {{}}

	 * /info/en/?search=Wikipedia:Anatomy_of_a_template

	 * @return {TemplateTag} */

	Wiki.Text.Tag = class {



		static IATE = InvalidArgumentTypeError;

		/** Basic Tokens */

		static B = "{{";

		static D = "|";

		static E = "}}";



		/**

		 * @param {String} name - Tag name e.g. diff, oldid2

		 * @param {params} params - Params of the template: {diff|param1|paramX|….}

		 */

		constructor(name, params) {

			if (new Object(name).constructor !== String) {

				throw new this.constructor.IATE(

					`Invalid arg: string expected`

				);

			}



			if (!(params instanceof Array)) {

				throw new this.constructor.IATE(

					"params have to be an array"

				);

			}



			let isParamString;

			// Replace non-string by "" (empty) string

			params = params.map((param) => {

				isParamString = new Object(param) instanceof String;

				return isParamString ? param.toString() : "";

			});



			this.name = name;

			this.params = params;

		}

		valueOf() {

			return this.toString();

		}

		toString() {

			// Create `{{name|param0|param1|paramN}}`

			let B = this.constructor.B; // Tag token

			let D = this.constructor.D; // Tag token

			let E = this.constructor.E; // Tag token

			let val = "";

			val += B;

			val += this.name;

			for (var param of this.params) {

				if (param) val += D + param;

			}

			val += E;

			return val;

		}

	};

	/**

	 * A container for Rows. Renders them into a string via toStirng()

	 * @summary Wikipedia table wikitext wrapper

	 */

	Wiki.Text.Table = class extends String {

		static IATE = InvalidArgumentTypeError;

		constructor({ cssClasses, rows }, options) {

			super();



			this.options = Object.assign(

				{

					caption: `Diffs`,

				},

				options || {}

			);



			if (!(rows instanceof Array)) {

				throw new this.constructor.IATE("rows have to be an array");

			}

			this.cssClasses = cssClasses || ``;

			this.rows = rows;

		}



		valueOf() {

			return this.toString();

		}

		toString() {

			let rowsStr = this.rows.join("\r\n");

			let classAttr = this.cssClasses ? `class="${this.cssClasses}"` : ``;

			return `{|${classAttr}\n|+${this.options.caption}\n${rowsStr}\n|}`;

		}

	};

	Wiki.Text.Table.Row = class extends String {

		constructor({ arr, value }, options, C) {

			if (value) {

				throw new Error(`Provide array instead`);

			}

			let rows = arr.join(`||`);



			super(`|-\n|${rows}`);

			this.C = C || {};

		}

	};

	Wiki.Text.Table.Header = class extends String {

		constructor({ arr, value }, options, C) {

			if (value) {

				throw new Error(`Provide array instead`);

			}

			let rows = arr.join(`!!`);



			super(`!${rows}`);

			this.C = C || {};

		}

	};

	Wiki.Text.Table.Def = class extends String {

		constructor(value) {

			if (new Object(value).value != null) {

				value = obj.value

			}

			super(`${value}`);

		}

	};



	// -----------------------------------------------------------------------------

	// #Wikidate

	// -----------------------------------------------------------------------------

	// @summary I convert Wikidate into Date and help to format it

	Wiki.Date = class extends Date {

		constructor(dateStr) {

			let wdate = dateStr.split(`, `);

			super(wdate.slice(1).concat(wdate0]).join(`,`));

			wdate = null;

		}



		// Default

		static dateFormat = {

			dateStyle: `medium`

			, timeStyle: "short"

			, hour12: false

		};

		// @para {object} dateFormat -  Format object, see MDN: Intl/DateTimeFormat

		format(dateFormat) {

			return Intl.DateTimeFormat(undefined, dateFormat || this.constructor.dateFormat).format(this);

		}



	}



	// -----------------------------------------------------------------------------

	// #REVISIONS ENTRIES WRAPPER

	// -----------------------------------------------------------------------------

	/**

	  * @summary Container for elements of Entry class 

	  * @class

	  */

	Wiki.Revisions = class extends Array {

		static IATE = InvalidArgumentTypeError;

		/**

		 * @param {Array<Wiki.Entry>} entries

		 * @param {HTMLElement} parentEl

		 * @param {Object} options

		 * @param {Object} C

		 */

		constructor(entries, parentEl, options, C) {

			super();

			// Context

			this.C = Object.assign({}, C || {});

			this.options = Object.assign({}, options || {});

			this.parentEl = parentEl;

			this.el = parentEl;



			if (entries instanceof Array) {

				// throw new this.constructor.IATE(`Array is expected`);

				// Sieve only Entry-based instances

				this.init(entries);

			}

		}





		/**

		 * @summary Clean up checkboxes left by previous script run

		 * @description Use after revisions.fromEl() call

		 * @param {HTMLElement} rootElement 

		 * @returns {Revisions}

		 */

		static checkboxesCleanUp(rootElement) {

			// Clean up previously created checkboxes

			if (rootElement.querySelector(`input[name="select-diff"]`)) {

				$(rootElement).find(`input[name="select-diff"]`).parent().remove();

			}

			return this

		} // checkboxesCleanUp end

		/**

		 * Helper to map HTMLElement children of revisions into Entries

		 * @param {HTMLElement} rawRevisions - An element whose children are going to be wrapped by Entry

		 * @param {Object} opt - Options for Revisions

		 * @param {Object} C   - Context for Revisions

		 * @param {Wiki.Revisions.Entry} Entry - Entry constructor

		 * @param {Object} Eopt - For Entry    - Options for Entry

		 * @param {Object} EC - Wiki UI native checkbox widget

		 * @returns Revisions

		 */

		static fromEl(rawRevisions, opt, C, Entry, Eopt, EC) {

			if (!(

				rawRevisions

				&& rawRevisions.constructor == Array

				&& rawRevisions.length > 0

			)) {

				throw this.IATE(`${Wiki.HH.NAME}: fromEl() expects an array with elements`);

			}



			if (rawRevisions0].constructor != HTMLLIElement) {

				throw this.IATE(`${Wiki.HH.NAME}: fromEl() expects an array of li elements`);

			}



			EC = Object.assign({ CheckboxInputWidget: OO.ui.CheckboxInputWidget }, EC || {});



			let entries = rawRevisions.map((el) => new Entry(el, Eopt, EC));

			// Invoking this(…) make this portable

			return new this(entries, rawRevisions, opt, C);

		}



		init(entries) {

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

				const entry = entriesi];

				if (entry instanceof this.constructor.Entry) {

					thisi = entry;

					entry.parent = this;

				}

			}

		}



		// Return array of checked entries

		checked() {

			let checked = [];

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

				if (thisi].isChecked()) {

					checked.push(thisi]);

				}

			}



			// BUG: This uncessarily registered new controls listeners if called via

			// built-in Array methods

			return new this.constructor(checked, this.parentEl, this.options, this.C);

		}

	};

	// A single revision entry line container

	Wiki.Revisions.Entry = class extends Object {

		static IATE = InvalidArgumentTypeError;

		constructor(el, options, C) {

			super();

			this.C = Object.assign({}, C || {});

			if (!(el instanceof HTMLLIElement)) {

				throw new this.constructor.IATE(`<li> element expected`);

			}

			this.el = el;

			this.init(el);

		}



		init(el) {

			// Revision link

			let href = el.querySelector(`.mw-changeslist-links > span:nth-child(2) > a`);

			if (href == null) {

				console.warn(`${Wiki.HH.NAME}: Entry()..init(): history 'prev' link isn't found, falling back to default values`);

				this.title = "";

				this.diff = "";

				this.oldid = "";

			} else {

				// TODO: BUG ON MAIN PAGE

				let urlParams = new URL(href).searchParams;

				this.title = urlParams.get(`title`);

				this.diff = urlParams.get(`amp;diff`) || urlParams.get(`diff`);

				this.oldid = urlParams.get(`amp;oldid`) || urlParams.get(`oldid`);

			}



			// Date

			let date = el.querySelector(`li > a`);

			if (date && date.textContent) {

				this.date = new Wiki.Date(date.textContent);

			}



			this.user = el.querySelector("bdi") && (el.querySelector("bdi").textContent ?? "");

			let comment = el.querySelector(".comment") ?? "";



			// Strip comments from backslash

			if (comment && comment.textContent) {

				this.comment = el

					.querySelector(".comment")

					.textContent.replace(/[\(\→]/g, "");

			} else {

				this.comment = ``;

			}

		}



		/**

		 * Insert a given el element before entry's first element

		 * @param {HTMLElement} el - element to be inserted before the first child

		 */

		insertBefore(el) {

			this.el.insertBefore(el, this.el.firstChild);

		}

	};



	/**

	 * @summary Container for elements of EntryCB class

	 * @class

	 * @since 2.6.0

	 */

	Wiki.Revisions2 = class extends Wiki.Revisions {

		/**

		 * @param {Array<Wiki.Revisions2.EntryCB>} entries 

		 * @param {HTMLElement} parentEl

		 * @param {Object} options

		 * @param {Object} C

		 */

		constructor(entries, parentEl, options, C) {

			super(entries, parentEl, options, C);

			this.i = 0;

		}

		init(entries) {

			/** @property {Boolean} - Whether any checkbox is checked*/

			this.checkboxes = [];

			this.checkboxes.parent = this;

			this.checkboxes.lastClicked = [];

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

				const entry = entriesi];

				if (entry instanceof this.constructor.Entry) {

					thisi = entry;

					// entry.parent = this;

					entry.parent = this;

					entry.checkbox.parent = this.checkboxes;

					this.checkboxes.push(entry.checkbox);

				}

			}

		}

		isAnyChecked() {

			return this.some(entry => entry.checkbox.isSelected())

		}

		checked() {

			return this.filter(entry => entry.isChecked());

		}

	};

	/**

	 * The Entry extended with a checkboxk

	 * @class

	 * @since 2.6.0

	 */

	Wiki.Revisions2.EntryCB = class extends Wiki.Revisions.Entry {

		constructor(el, options, C) {

			super(el, options, C);

			if (this.C.CheckboxInputWidget == null) {

				throw new this.constructor.IATE(`CheckboxInputWidget is missing`);

			}

			// The value is expected to be assigned by external entity

			this.parent

			this.init(el);

			this.initCheckBox();

		}



		initCheckBox() {

			this.checkbox = new this.C.CheckboxInputWidget({

				name: `select-diff`,

				value: this.el.getAttribute(`data-mw-revid`),

				selected: false,

			});

			this.checkbox.$element0].style.width = `15px`;

			this.checkbox.$element0].style.height = `15px`;

			this.checkbox.$element.mouseleave(function(e) {

				if (e.buttons === 1) {

					this.setSelected(!this.isSelected());

				}

			}.bind(this.checkbox));

			this.insertBefore(this.checkbox.$element0]);

		}



		/**

		 * @returns {Boolean} - True if checked

		 */

		isChecked() {

			return this.checkbox.isSelected();

		}

	};



	Wiki.Contributions = class extends Wiki.Revisions2 { };

	Wiki.Contributions.EntryCB = class extends Wiki.Revisions2.EntryCB {

		static IATE = InvalidArgumentTypeError;

		static UserName = mw.config.get(`wgRelevantUserName`);



		constructor(el, options, C) {

			super(el, options, C);

			if (!(el instanceof HTMLLIElement)) {

				throw new this.constructor.IATE(`<li> element expected`);

			}

			let context = {}; // The context here stands for imported object

			this.C = Object.assign(context, C || {});

			this.options = Object.assign({}, options || {});

			this.el = el;

			this.init(el);

		}

		init(el) {

			// Revision links

			let diffEl = el.querySelector(`a.mw-changeslist-diff`) || el.querySelector(`a.mw-changeslist-history`);

			if (diffEl == null) {

				throw new Error(`${Wiki.HH.NAME}: can't find diff element on collaboration page.`);

			}

			let href = diffEl.href;



			if (href == null) {

				throw new Error(`${Wiki.HH.NAME}: Entry()..init(): history 'prev' link isn't found, falling back to default values`)

				this.title = "";

				this.diff = "";

			} else {

				let urlParams = new URL(href).searchParams;

				this.title = urlParams.get(`title`);

				this.diff = this.el.dataset"mwRevid"];

			}

			this.oldid = `prev`;



			this.user = this.constructor.UserName;

			// this.user = mw.config.get(`wgRelevantUserName`);



			// Date

			let date = el.querySelector(`li > a`);

			if (date && date.textContent) {

				el.querySelector(`li > a`).textContent;

				this.date = new Wiki.Date(date.textContent);

			} else {

				this.date = new Wiki.Date(date.textContent);

			}

			this.comment = ``;

			let commentEl = this.el.querySelector(`.comment`);

			if (commentEl) {

				this.comment = commentEl.textContent.replace(/[\(\→]/g, "")



			}

		}

	};





	Wiki.Toolbar = class extends Map {

		static IATE = InvalidArgumentTypeError;

		static config = {

			id: `toolbar-default`

		}

		static buttons = {

			`info`: {

				type: `Popup`,

				disabled: true,

				title: `Click buttons on the right`,

				label: `COPY AS`,

				icon: `doubleChevronEnd`,

			},

			`as.diffs`: {

				title: `Copy selected as {{diff|…}} wikitext`,

				id: `as.diffs`,

				label: `{{diff}}`,

				icon: `code`,

				template: `{{tqb|\n%\n}}`

			},

			`as.table`: {

				title: `Copy selected as table wikitext`,

				id: `as.table`,

				label: `<Table/>`,

				icon: `table`,

				template: ``

			},

			`as.links`: {

				title: `Copy selected as raw [1]..[n] links (can be pasted into summary)`,

				id: `as.links`,

				label: `Links`,

				icon: `wikiText`,

				template: ``

			},

		};



		static notice = {

			type: 'info',

			label: 'Nothing to preview. Select checkboxes!',

			title: 'Info',

			inline: true

		}

		/**

		 *

		 * @param {HTMLElement} toolbarEl - Container

		 * @param {Array<Object>} buttons - Arrays of buttons widgets. See add() for supported ones

		 * @param {Object} options -

		 * @param {Object} C - Context

		 */

		constructor(buttons, options, C) {

			super();

			// Options.

			this.arguments = arguments;

			this.arguments1 = Object.assign({}, options || this.constructor.config);

			this.arguments2 = Object.assign({}, OO.ui, C || {});



			// Toolbar widget

			this.buttonsGroup = new this.arguments2].ButtonGroupWidget({ id: this.arguments1].id });

			this.$element = this.buttonsGroup.$element;

			this.$element.css(`z-index`, 2);

			if (buttons) {

				this.addMany(buttons);

			}

		}



		/**

		 * @typedef  {Object} OO.ui.ButtonWidget -

		 * @property {string} id -

		 * @method addItems

		 */



		/**

		 * Add every button to the group, associate buttons with IDs

		 * @example new Toolbar();

		 * @param {HTMLElement | OO.ui.ButtonGroupWidget | OO.ui.PopupButtonWidget} el

		 * @returns {Wiki.Toolbar}

		 */

		add(el) {

			if (el == null) {

				throw new this.constructor.IATE(`first argument is expected`);

			}

			switch (el.constructor) {

				case HTMLElement:

					this.buttonsGroup.$element0].appendChild(el);

					el.id && this.set(el.id, el);

					break;

				case this.arguments2].ButtonWidget:

				case this.arguments2].PopupButtonWidget:

					el.$element0].id && this.set(el.$element0].id, el);

					this.buttonsGroup.addItems([el]);

					break;

					break;

				default:

					console.warn(`toolbar.add(e): unknown e.constructor.`)

			}



			return this;

		}

		/**

		 *

		 * @param {Array<HTMLElement | OO.ui.ButtonGroupWidget>} elements

		 * @returns

		 */

		addMany(elements) {

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

				this.add(elementsi]);

			}

			return this;

		}



		toArray() {

			return Array.from(this.values())

		}

	};





	/**

	 * The HistoryHelper main class used as nameSpace.

	 * It binds provided UI elements (toolbar/revisions) and binds

	 * Pointer (mouse) and Keyboard strokes to actionsM

	 * (e.g. copy revisions to clipboard)

	 */

	Wiki.HH = class extends Object {

		static NAME = `HistoryHelper`;

		static IATE = InvalidArgumentTypeError;



		// TODO: Deprecate in favor of preview copy text field

		static shortcuts = {

			`ctrl+alt+d`: `revisions.as.diffs.to.clipboard`,

			`ctrl+alt+c`: `revisions.as.links.to.clipboard`

		}

		static options = {

			fetchLimit: 64

		}

		/**

		 * Overview of basic HistoryHelper workflows

		 * ##Clipboard workflow

		 * revisions.keyboard -> revisionsTo…(revisions) -> clipboard.copy()

		 * buttons.pointer.click  -> entries.to.markup -> clipboard.copy()

		 * buttons.pointer.hover  -> buttons.popup.showPreview(revisions.as.XYZ)

		 * ##UX workflow

		 * buttons.popup.pointer -> preview.modify()

		 * revisions.pointer -> entries.select

		 * revisions.checkboxes.pointer + keyboard.shift -> entries.select

		 * @param {Wiki.Toolbar}    revisions - Data (revisions container)

		 * @param {Wiki.Revisions}  toolbar   - Input (butttons panel)

		 * @param {ClipboardBuffer} clipboard - Output (clipboard buffer)

		 * @param {Object} options - Configuration object

		 * @param {Object} options.shortcuts - Shortcuts to Action map

		 * @param {Object} C           - Namespace for default class constructors

		 * @param {Object} C.Revisions - Revisions entries container constructor

		 * @param {Object} C.Toolbar   - 

		 * @param {Object} C.Clipboard -

		 * @param {Object} C.Text      - WikiText renderer 

		 * used to build output strings

		 */

		constructor(toolbar, revisions, clipboard, options, C) {

			super();

			this.C = {};

			this.C.Revisions = Wiki.Revisions;

			this.C.Toolbar = Wiki.Toolbar; // Containers

			this.C.Clipboard = ClipboardBuffer;

			this.C.Text = Wiki.Text;

			this.C = Object.assign(this.C, (C || {}));



			this.options = Object.assign({}, this.constructor.options, options || {});



			if (!(toolbar instanceof this.C.Toolbar)) throw new this.constructor.IATE(`toolbar   instance of Toolbar   is expected`);

			if (!(revisions instanceof this.C.Revisions)) throw new this.constructor.IATE(`revisions instance of Revisions is expected`);

			if (!(clipboard instanceof this.C.Clipboard)) throw new this.constructor.IATE(`clipboard instance of Clipboard is expected`);



			this.toolbar = toolbar;

			this.revisions = revisions;

			this.clipboard = clipboard;



			//#ACTIONS MAP

			//------------------------------------------

			// These are intended to be invoked on some user

			// actions such as click or keypress

			// These callbacks are called from multiple places

			// DPRCT: [August 07, 2023] Remove clipboard functionality

			this`revisions.as.diffs.to.clipboard` = function() {

				this.clipboard.copy(this.constructor.revisionsToDIFFS(this.revisions.checked(), undefined, options))

			}.bind(this);

			this`revisions.as.table.to.clipboard` = function() {

				this.clipboard.copy(this.constructor.revisionsToTABLE(this.revisions.checked()))

			}.bind(this);

			this`revisions.as.links.to.clipboard` = function() {

				this.clipboard.copy(this.constructor.revisionsToLINKS(this.revisions.checked()))

			}.bind(this);



			this`revisions.as.diffs.rendered` = function(cb) {

				let selected = this.revisions.checked().slice(0, this.options.fetchLimit);

				let wikitext = this.constructor.revisionsToDIFFS(selected, undefined, options);

				wikitext

					? new this.C.Text(wikitext).render().done(cb)

					: cb({});

			}.bind(this);

			this`revisions.as.table.rendered` = function(cb) {

				let selected = this.revisions.checked().slice(0, this.options.fetchLimit);

				let wikitext = this.constructor.revisionsToTABLE(selected);

				wikitext

					? new this.C.Text(wikitext).render().done(cb)

					: cb({});

			}.bind(this);

			this`revisions.as.links.rendered` = function(cb) {

				let selected = this.revisions.checked().slice(0, this.options.fetchLimit);

				let wikitext = this.constructor.revisionsToLINKS(selected);

				wikitext

					? new this.C.Text(wikitext).render().done(cb)

					: cb({});

			}.bind(this);



			this.buttons = this.toolbar.toArray();

			this.initButtons();

			this.initRevisionsListeners();

			this.initRevisionsSpecialListneners();

		} // CONSTRUCTOR END



		// Associate button clicks with actions

		initButtons() {

			//#POINTER CONTROL - BUTTONS

			//------------------------------------------

			for (let button of this.buttons) {

				button.$element.click(this`revisions.${button.elementId}.to.clipboard`]);



				// Show preview of the selected entries

				button.$element.mouseenter(function(button, e) {

					// Hide all popups

					for (let nextButton of this.buttons) {

						nextButton.popup.toggle(false);

					}

					button.popup.toggle(true);

					let d0 = button.popup.$lable.isVisible();

					if (this.revisions.isAnyChecked()) {

						button.popup.$lable.toggle(false);

						setTimeout(() => {

							this`revisions.${button.elementId}.rendered`]((response) => {

								if (response.parse) {

									button.popup.html(`${response.parse.text`*`}`)

								} else {

									button.popup.html(``);

								}

							});

						}, 300);

					} else {

						button.popup.$lable.toggle(true);

					}



				}.bind(this, button)); // bindEventEnd

			}

		}



		// Associate keyboard hotkeys with actions

		// Only works when pointer is in area of a revisions list element

		initRevisionsListeners() {

			//#KEYBOARD CONTROL

			//------------------------------------------

			if (this.options.shortcuts) {

				const ctrlKey = `ctrl`;

				const shiftKey = `shift`;

				const altKey = `alt`;

				this.revisions.parentEl.tabIndex = 1;

				$(this.revisions.parentEl).bind(`keyup`, (e) => {

					let pressedKeys = ``;

					pressedKeys += e.ctrlKey ? ctrlKey + `+` : ``;

					pressedKeys += e.shiftKey ? shiftKey + `+` : ``;

					pressedKeys += e.altKey ? altKey + `+` : ``;

					pressedKeys += e.key;

					// Match the keystroke into a an action declared above

					let action = thisthis.options.shortcutspressedKeys]];

					if (action) action();



				});

			}

		}

		// Associate keyboard + pointer hotkeys behavior

		// Allows selecting checkboxes range by using shift + checkbox click

		initRevisionsSpecialListneners() {

			//#CHECKBOXES CONTROL

			//------------------------------------------

			this.revisions.checkboxes.lastClicked1 = this.revisions.checkboxes0];

			$(this.revisions.el).click((e) => {

				// Clear up preview data

				for (let button of this.buttons) {

					button.popup.html(``);

				}

				// We need to focuse only on widget's span element

				let focusedCheckbox;

				if (e.target instanceof HTMLInputElement) {

					focusedCheckbox = e.target.parentElement;

				}

				if (e.target instanceof HTMLSpanElement

					&& /oo-ui-checkboxInputWidget/.test(e.target.className)) {

					focusedCheckbox = e.target;

				}

				/**@type Array<CheckboxInputWidgets> */

				let checkboxes = this.revisions.checkboxes;



				if (checkboxes.lastClicked1 !== focusedCheckbox) {

					checkboxes.lastClicked0 = checkboxes.lastClicked1];

					checkboxes.lastClicked1 = focusedCheckbox;

				}



				if (

					e.shiftKey &&

					checkboxes.lastClicked0 &&

					checkboxes.lastClicked1

				) {



					let from = checkboxes.findIndex((widget) => {

						return checkboxes.lastClicked0 === widget.$element0

					});

					let to = checkboxes.findIndex((widget) => {

						return checkboxes.lastClicked1 === widget.$element0

					});

					if (from > to) {

						let mid = to;

						to = from;

						from = mid;

					}

					from++;

					for (; from < to; from++) {

						checkboxesfrom].setSelected(!checkboxesfrom].isSelected())

					}

				}

			});

		}



		// Words to higlight

		static highlights = /competen(t|cy)|IR|bitch|illiterate|fuck(er)?|asshole(ery)?|troll|idiot|dumbass|stupid|blank|subhuman|autis[tm]|(edit)? warring|inept/g;



		/** Convert revisions entries into a Wikitext (diffs)

		 * @since 2.6.0

		 * @param {Wiki.Revisions} revisions - Array that contains Entry instances

		 * @param {Wiki.Text.Tag} Tag 

		 * @returns {String}

		 */

		static revisionsToDIFFS(revisions, Tag, config) {

			Tag = Tag || Wiki.Text.Tag;

			if (!(revisions)) { throw new this.IATE(`Revisions are missing`) }

			if (!revisions.length) { return `` }

			let entry, tag, wikitext = ``;

			let comment;

			let users = new Set();

			// Walk over every entry

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

				entry = revisionsi];





				if (entry && new Object(entry.user).constructor == String) {



					if (entry.user !== mw.config.get(`wgUserName`)) {

						users.add(entry.user);

					}

				}

				if (entry && new Object(entry.date).constructor == Wiki.Date) {

					entry.date = entry.date.format();

				}



				tag = new Tag(`diff`, 

					entry.diff,

					entry.oldid,

					entry.date,

				]);



				// Highlight specified by config words and phrases

				// Highlight incivility

				comment = entry.comment.replace(this.highlights, `{{highlight|$&}}`);

				let highlights = config && new Object(config.highlights);

				if (highlights

					&& highlights.constructor === Array

					&& highlights.length) {

					for (let i = 0, reg; i < highlights.length; i++) {

						reg = highlightsi];

						comment = comment.replace(reg, `{{highlight|$&}}`);

					}

				}

				

				wikitext += `* [[Special:Diff/${entry.oldid}|[${entry.date}]]] - {{User|${entry.user}}} - {{gt|''"${comment}"''}}</br>\n`;

			}

			return wikitext





		}



		/** Convert revisions entries into a Wikitext (Special:Diff/… links)

		 * @since 2.6.0

		 * @param {Wiki.Revisions} revisions - Array that contains Entry instances

		 * @returns {String}

		 */

		static revisionsToLINKS(revisions) {

			if (!(revisions)) { throw new this.IATE(`Revisions are missing`) }

			if (!revisions.length) { return `No revisions selected` }

			let entry, wikitext = ``;

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

				entry = revisionsi];



				if (entry && new Object(entry.date).constructor == Wiki.Date) {

					entry.date = entry.date.format();

				}

				// Omit prev

				let diff = entry.oldid;

				if(diff == "prev") {

					diff = entry.diff

				}

				wikitext += `# [[Special:Diff/${diff}|[${entry.date}]]]</br>\n`

			}

			return wikitext





		}

		/** Convert revisions entries into a Wikitext (tables )

		 * @since 2.6.0

		 * @param {Wiki.Revisions} revisions - Array that contains Entry instances

		 * @param {Wiki.Text.Tag} Tag 

		 * @param {Wiki.Text.Table} Table

		 * @returns {String}

		 */

		static revisionsToTABLE(revisions, Tag, Table) {

			if (!(revisions)) { throw new this.IATE(`Revisions are missing`) }

			if (!revisions.length) { return `` }



			Table = Table || Wiki.Text.Table;

			Tag = Tag || Wiki.Text.Tag;



			// Every entry wrapped into a wiki tag

			// Group of tags into table definitions (colums)

			let entry;

			let anchor, anchLink, diff, oldid, user, tags, entries;

			let defintions = [];

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

				entry = revisionsi];



				anchLink = `hist-${i}-${entry.diff}`;



				anchor = new Tag(`anchor`, anchLink]);

				diff = new Tag(`diff`, entry.oldid, entry.date]);

				oldid = new Tag(`oldid2`, 1, entry.oldid, entry.date]);

				user = new Tag(`u`, entry.user]);



				tags = 

					anchor + `[[#${anchLink}|${i}]]`,

					diff,

					oldid,

					user,

					entry.comment ? `''${entry.comment}''` : ``

				

				defintions.push(tags.map(tag => new Table.Def(tag)));



			}

			// Wrap ever column into a row

			// First row is the head

			let columns;

			let rows = 

				new Table.Header({

					arr: `#`, `DIFF`, `CURRENT`, `USER`, `SUMMARY`],

				})

			];



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

				columns = defintionsi];

				rows.push(new Table.Row({ arr: columns }))

			}



			let wikitext = new Table({

				cssClasses: "wikitable sortable",

				rows: rows,

			}).toString();

			return wikitext;







		}

	};



	//#USER CONFIG

	//------------------------------------------

	// Convert legacy (prior  2.6.0) config version into a 2.6.0 

	if (window.HistoryHelper && window.HistoryHelper.shortcuts) {

		let shortcuts = window.HistoryHelper.shortcuts;

		// 1/2 For every shortcut

		for (const key in shortcuts) {

			if (Object.hasOwnProperty.call(shortcuts, key)) {

				const actionName = shortcutskey];

				// 2/2 if an old action match, replace by a new one

				if (actionName === `copyAsdiffs`) {

					shortcutskey = `revisions.as.links.to.clipboard`;

					console.warn(`${Wiki.HH.NAME}: copyAsdiffs action is deprecated after v2.6.0, update your config`)

				}

			}

		}

	}



	let config = Object.assign(

		{}

		// Turn off default shortcuts

		// ,{ shortcuts: Wiki.HH.shortcuts},

		, window.HistoryHelper || {}

	);



	// ---------------------------------------------------------------------------

	// #MAIN

	// ---------------------------------------------------------------------------

	let main = function main() {

		let contribPageRe = /Special:Contributions/

		let isContributionsPage = contribPageRe.test(window.location.href);

		let isHistoryPage = new URL(window.location).searchParams.get("action") == "history";

		if (!(isContributionsPage || isHistoryPage)) {

			return

		}



		// Initialize toolbar & buttons

		let buttons = Object.values(Wiki.Toolbar.buttons).map((data) => {

			let $lable = new OO.ui.MessageWidget(Wiki.Toolbar.notice);

			$lable.$element.css(`min-width`, `478px`)

			let $content = $(`<div></div>`)

			// .append($notice.$element);

			let popup = new OO.ui.PopupWidget({

				width: null,

				head: true,

				label: $lable.$element,

				$content: $content,

				padded: true,

				autoClose: true,

				autoFlip: false

			});

			popup.$element.css(`z-index`, 32);

			popup.$element.css(`min-width`, `330px`);

			popup.$element.css(`min-height`, `127px`);

			popup.$content = $content;

			popup.$lable = $lable;

			popup.html = function(str) {

				return this.$content.html(str)

			}



			let button = new OO.ui.ButtonWidget({ ...data, content: popup });

			button.popup = popup;

			return button

		})

		//  New toolbar

		windowWiki.Toolbar.config.id && windowWiki.Toolbar.config.id].remove();

		let toolbar = new Wiki.Toolbar(buttons);

		//  Initialize revisions container



		let pagehistory = document.getElementById(`pagehistory`)

			|| document.querySelector(`#mw-content-text section.mw-pager-body`);

		if (!(pagehistory)) {

			throw new Error(

				`${Wiki.HH.NAME}: can't find revisions html element.

        \n\tThis is probably due to Wikipedia changing its HTML ids.

        \n\tContact the script author for help:

        \n\thttps://en.wikipedia.org/?title=User_talk:Alexander_Davronov&action=edit&section=new`

			);

			return

		}

		//  Remove old checkboxes

		Wiki.Revisions2.checkboxesCleanUp(pagehistory);



		let clipboard = new ClipboardBuffer();

		// Article or User history page

		// https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config

		if (isHistoryPage) {



			let rawRevisions = Array.from(pagehistory.querySelectorAll(`ul > li`));

			let revisions = Wiki.Revisions2.fromEl(

				rawRevisions, {}, {}

				, Wiki.Revisions2.EntryCB

			);



			// Adding tools

			let revCompareForm = document.getElementById(`mw-history-compare`);

			let toolbarContainerTarget =

				revCompareForm

				&& revCompareForm.querySelector(`.mw-history-compareselectedversions`);



			$(toolbarContainerTarget).append(toolbar.$element);

			if (toolbar.$element0 && !toolbar.$element0].children.length) {

				throw new Error(`${Wiki.HH.NAME}: Toolbar has no buttons, please fill a bug report!`);

			}



			// Init HistoryHelper controls (button press handlers)

			// over toolbar and revisions

			new Wiki.HH(toolbar, revisions, clipboard, config);

			return

		}

		// User contributions page

		let isViewing = mw.config.get(`wgAction`) === `view`;

		if (isViewing) {

			let rawRevisions = Array.from(pagehistory.querySelectorAll(`ul > li`));

			let revisions = Wiki.Contributions.fromEl(

				rawRevisions, {}, {}

				, Wiki.Contributions.EntryCB

				, { user: mw.config.get(`wgRelevantUserName`) }

			);

			let toolbarContainerTarget = document.getElementById(`mw-content-text`).firstChild;

			toolbar.$element.insertAfter(toolbarContainerTarget);

			new Wiki.HH(toolbar, revisions, clipboard, config);

			return

		}

	}



	mw.loader.using([`oojs-ui.styles.icons-editing-advanced`, `oojs-ui.styles.icons-alerts`], main);





	// From the End comes The Beginning!

	// Something ends, something begins!

});

Videos

Youtube | Vimeo | Bing

Websites

Google | Yahoo | Bing

Encyclopedia

Google | Yahoo | Bing

Facebook