User:Alexander Davronov/HistoryHelper.js

Source: 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 {{}}
	 * https://en.wikipedia.org/wiki/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(wdate[0]).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 (rawRevisions[0].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 = entries[i];
				if (entry instanceof this.constructor.Entry) {
					this[i] = entry;
					entry.parent = this;
				}
			}
		}

		// Return array of checked entries
		checked() {
			let checked = [];
			for (let i = 0; i < this.length; i++) {
				if (this[i].isChecked()) {
					checked.push(this[i]);
				}
			}

			// 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 = entries[i];
				if (entry instanceof this.constructor.Entry) {
					this[i] = 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.$element[0].style.width = `15px`;
			this.checkbox.$element[0].style.height = `15px`;
			this.checkbox.$element.mouseleave(function(e) {
				if (e.buttons === 1) {
					this.setSelected(!this.isSelected());
				}
			}.bind(this.checkbox));
			this.insertBefore(this.checkbox.$element[0]);
		}

		/**
		 * @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.arguments[1] = Object.assign({}, options || this.constructor.config);
			this.arguments[2] = Object.assign({}, OO.ui, C || {});

			// Toolbar widget
			this.buttonsGroup = new this.arguments[2].ButtonGroupWidget({ id: this.arguments[1].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.$element[0].appendChild(el);
					el.id && this.set(el.id, el);
					break;
				case this.arguments[2].ButtonWidget:
				case this.arguments[2].PopupButtonWidget:
					el.$element[0].id && this.set(el.$element[0].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(elements[i]);
			}
			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 = this[this.options.shortcuts[pressedKeys]];
					if (action) action();

				});
			}
		}
		// Associate keyboard + pointer hotkeys behavior
		// Allows selecting checkboxes range by using shift + checkbox click
		initRevisionsSpecialListneners() {
			//#CHECKBOXES CONTROL
			//------------------------------------------
			this.revisions.checkboxes.lastClicked[1] = this.revisions.checkboxes[0];
			$(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.lastClicked[1] !== focusedCheckbox) {
					checkboxes.lastClicked[0] = checkboxes.lastClicked[1];
					checkboxes.lastClicked[1] = focusedCheckbox;
				}

				if (
					e.shiftKey &&
					checkboxes.lastClicked[0] &&
					checkboxes.lastClicked[1]
				) {

					let from = checkboxes.findIndex((widget) => {
						return checkboxes.lastClicked[0] === widget.$element[0]
					});
					let to = checkboxes.findIndex((widget) => {
						return checkboxes.lastClicked[1] === widget.$element[0]
					});
					if (from > to) {
						let mid = to;
						to = from;
						from = mid;
					}
					from++;
					for (; from < to; from++) {
						checkboxes[from].setSelected(!checkboxes[from].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 = revisions[i];


				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 = highlights[i];
						comment = comment.replace(reg, `{{highlight|$&}}`);
					}
				}
				comment = comment ? `- ''«${comment}»''` : ``;
				wikitext += `${tag.toString()} ${comment}<br/>\n`;
			}

			if (users.size) {
				wikitext += ':';
				wikitext += new Tag(`re`, Array.from(users));
			}
			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 = revisions[i];

				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}]]]`
			}
			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 = revisions[i];

				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 = defintions[i];
				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 = shortcuts[key];
				// 2/2 if an old action match, replace by a new one
				if (actionName === `copyAsdiffs`) {
					shortcuts[key] = `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
		window[Wiki.Toolbar.config.id] && window[Wiki.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/w/index.php?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.$element[0] && !toolbar.$element[0].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!
});