Jump to content

User:FenrisAureus/HistoryHelper.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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|$&}}`);
					}
				}
				
				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 = 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}]]]</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 = 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!
});