跳转到内容

User:SuperGrey/gadgets/refOrganizer/helper.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Main page: [[User:SuperGrey/gadgets/refOrganizer]]

(function (mw, $) {

	var refcon = {

		/**
		 * This variable holds edit textbox text that is modified throughout the script
		 *
		 * @type {string}
		 */
		textBoxText: '',

		/**
		 * This array holds reference template groups in the order that they appear in article
		 *
		 * @type {array}
		 */
		templateGroups: [],

		/**
		 * This array holds reference templates in the order that they appear in article
		 *
		 * @type {array}
		 */

		refTemplates: [],

		/**
		 * This array holds article text parts that are between reference templates
		 *
		 * @type {array}
		 */

		textParts: [],

		/**
		 * Object for user selectable sort options
		 *
		 * @type {object}
		 */

		userOptions: {},

		/**
		 * Convenience method to get a RefCon option
		 *
		 * @param {string} option key without the "refcon-" prefix
		 * @return {string} option value
		 */
		getOption: function (key) {
			return mw.config.get('refcon-' + key);
		},

		/**
		 * Convenience method to get a RefCon message
		 *
		 * @param {string} message key without the "refcon-" prefix
		 * @param {array} array of replacements
		 * @return {string} message value
		 */
		getMessage: function (key, param) {
			return new mw.Message(mw.messages, 'refcon-' + key, param).text();
		},

		/**
		 * Convenience method to get the edit textbox
		 *
		 * @return {jQuery} edit textbox
		 */
		getTextbox: function () {
			return $('#wpTextbox1');
		},

		/**
		 * Initialization. Sets up script execution link. If the link is clicked, calls main function
		 *
		 * @return {void}
		 */
		init: function () {

			$([refcon.getOption('image-yes'),
			refcon.getOption('image-no')
			]).each(function () {
				$('<img/>')[0].src = this;
			});

			var linkname = refcon.getOption('linkname'),
				linkhover = refcon.getOption('linkhover');

			// Add portlet link to the script
			if (document.getElementById('ca-edit')) {
				var url = mw.util.getUrl(mw.config.get('wgPageName'), { action: 'edit', RefCon: 'true' });
				var portletlink = $(mw.util.addPortletLink(
					'p-cactions',
					url,
					linkname,
					'ca-RefCon',
					linkhover,
					'',
					document.getElementById('ca-move')
				));
				// If the portlet link is clicked while on edit page, run the function and do stuff, don't load new page
				if (typeof document.forms.editform !== 'undefined') {
					portletlink.on('click', function (e) {
						e.preventDefault();
						refcon.main();
					});
				}
			}

			// Only load when editing
			var action = mw.config.get('wgAction');

			if (action === 'edit' || action === 'submit') {
				// Only if the portlet link was clicked
				if (mw.util.getParamValue('RefCon')) {
					// Only if there is wpTextbox1 on the page
					if (document.getElementById('wpTextbox1')) {
						refcon.main();
					}
				}
			}
		},

		/**
		 * Main function. Calls specific subfunctions
		 *
		 * @return {void}
		 */
		main: function () {
			// This is a container function that calls subfunctions and passes their return values to other subfunctions

			// First, get indexes of reference templates in article, if there are any
			var indexes = refcon.parseIndexes(), i;

			if (indexes.length > 0) {

				var templateDataList = [], templatesString = '';

				// Go through indexes array
				for (i = 0; i < indexes.length; i++) {
					var refStartIndex = indexes[i];
					var nextRefStartIndex = indexes[i + 1] ? indexes[i + 1] : refcon.textBoxText.length;

					var templateData = refcon.getTemplateContent(refStartIndex, nextRefStartIndex, i);

					// don't do anything with the reference template if it is not closed
					if (templateData['refEndIndex'] !== null) {
						templatesString += templateData['templateContent'];
						templateDataList.push(templateData);
					}
				}

				// Use mw.API to get reflist templates parameter pairs
				var paramPairsList = refcon.getTemplateParams(templatesString);

				for (i = 0; i < templateDataList.length; i++) {
					var paramsPair = typeof paramPairsList[i] !== 'undefined' ? paramPairsList[i] : {};
					var refTemplate = refcon.getTemplateObject(templateDataList[i], paramsPair);
					refcon.parseTemplateRefs(refTemplate);
				}

				// Go through refTemplates array (refTemplates determine the boundaries) and create an array of TextPart objects
				// These are text parts of an article that are located between reference templates

				refcon.storeTextParts();

				// Process references in reference templates, remove duplicate keys and values

				for (i = 0; i < refcon.refTemplates.length; i++) {
					refcon.refTemplates[i].processDuplicates();
				}

				// Find and store references and citations in each textPart object

				for (i = 0; i < refcon.textParts.length; i++) {
					refcon.parseTextParts(refcon.textParts[i]);
				}

				// Compare references to the ones in reference template(s). Add text part references into reference template.
				// Create citations to references.

				for (i = 0; i < refcon.textParts.length; i++) {
					refcon.processTextPartRefs(refcon.textParts[i]);
				}

				// Link textPart citations to references

				for (i = 0; i < refcon.textParts.length; i++) {
					refcon.linkCitations(refcon.textParts[i]);
				}

				// Show form with references
				refcon.showForm();

			} else {
				refcon.showDifferenceView();
			}
		},

		/**
		 * Continue processing after form. Commit changes and show the differences view
		 *
		 * @return {void}
		 */
		commit: function () {

			// Recreate indexes (because names could have been changed in the form)
			for (i = 0; i < refcon.refTemplates.length; i++) {
				refcon.refTemplates[i].reIndex();
			}

			// Replace references inside text part strings with citations
			for (i = 0; i < refcon.textParts.length; i++) {
				refcon.replaceTextPartRefs(refcon.textParts[i]);
			}
			// Build reference templates
			for (i = 0; i < refcon.refTemplates.length; i++) {
				refcon.buildRefTemplates(refcon.refTemplates[i]);
			}
			var newText = refcon.writeTextBoxText();
			var textbox = refcon.getTextbox();
			var oldText = textbox.val();

			if (oldText != newText) {
				// Update textbox
				textbox.val(newText);
				// Add summary
				refcon.addSummary();
			}
			refcon.showDifferenceView();
		},

		/**
		 * Show form with references
		 *
		 * @return {void}
		 */
		showForm: function () {

			// Define basic elements
			var gui = $('<div>').attr('id', 'refcon'),
				container = $('<div>').attr('id', 'refcon-container'),
				header = $('<div>').attr('id', 'refcon-header'),
				title = $('<span>').attr('id', 'refcon-title').text(refcon.getOption('gadgetname')),
				closer = $('<div>').attr('id', 'refcon-close').addClass('refcon-abort').html('&times;').attr('title', refcon.getMessage('closetitle')),
				content = $('<div>').attr('id', 'refcon-content'),
				form = $('<form>').attr('id', 'refcon-form'),
				table = $('<table>').attr('id', 'refcon-table');

			// Put everything together and add it to DOM
			header.append(title, closer);
			content.append(form).append(table);
			container.append(header, content);
			gui.append(container);
			$('body').prepend(gui);

			// Make GUI draggable
			container.draggable({
				handle: header
			});

			// Set GUI width and height to 80% of user's window size (fallback is CSS-predefined values, if this fails)
			var width = $(window).width();
			var height = $(window).height();
			if ((Number.isInteger(width) && width > 0) && (Number.isInteger(height) && height > 0)) {
				content.css("width", Math.floor(width * 0.8));
				content.css("height", Math.floor(height * 0.8));
			}

			// Build table and fill it with reference data
			table.append('<tr>\
						<th></th>\
						<th class="refcon-sortable refcon-asc"><span>#</span></th>\
						<th class="refcon-sortable"><span>'+ refcon.getMessage('name') + '</span></th>\
						<th class="refcon-sortable"><span>'+ refcon.getMessage('reference') + '</span></th>\
						<th class="refcon-sortable"><span>'+ refcon.getMessage('referenceuses') + '</span></th>\
						<th></th>\
						</tr>');

			var i;
			for (i = 0; i < refcon.refTemplates.length; i++) {
				var refTemplate = refcon.refTemplates[i];
				table.append('<tr id="templateheader' + i + '"><td class="refcon-templategroup" colspan="5" align="center">'
					+ refcon.getMessage('refstemplateno') + ' ' + (i + 1)
					+ (refcon.templateGroups[i].length > 0 ? ' (' + refcon.getMessage('referencegroup') + ': ' + refcon.templateGroups[i] + ')' : '')
					+ '</td></tr>');
				var j, k = 0;
				for (j = 0; j < refTemplate.references.length; j++) {
					var reference = refTemplate.references[j];
					if (reference) {
						k++;
						var cssClass = k % 2 == 0 ? 'refcon-even' : 'refcon-odd';
						table.append(
							'<tr template="' + i + '">'
							+ '<td class="' + cssClass + '"><img src="' + refcon.getOption('image-yes') + '"></td>'
							+ '<td class="' + cssClass + '" align="center">' + k + '</td>'
							+ '<td class="' + cssClass + '"><input class="refcon-refname" type="text" template_id="' + i + '" name="' + j + '" value="' + reference.name + '"></td>'
							+ '<td class="' + cssClass + ' refcontent">' + reference.content + '</td>'
							+ '<td class="' + cssClass + '" align="center">' + reference.citations.length + '</td>'
							+ '<td class="' + cssClass + '"><input class="refcon-refplace" type="checkbox" name="' + j + '" value="' + reference.citations.length + '"' + (reference.inRefTemplate === true ? 'checked' : '') + '></td>'
							+ '</tr>');
					}
				}
			}
			table.append('<tr><td colspan="5"><table id="refcon-table-options">\
						<tr><td><span class="refcon-option-header">' + refcon.getMessage('optionsheaderreflocation') + '</span></td><td width="20"></td><td><span class="refcon-option-header">' + refcon.getMessage('optionsheaderother') + '</span></td></tr>\
						<tr><td><span class="refcon-option-point"><input class="refcon-refplacement" type="radio" name="reference-place" value="template"> ' + refcon.getMessage('optionlocation1') + '</span></td><td width="20"></td><td><span class="refcon-option-point"><input type="checkbox" id="refcon-savesorted" name="sort" value="yes">' + refcon.getMessage('checkboxsortorder') + '</span></td></tr>\
						<tr><td><span class="refcon-option-point"><input class="refcon-refplacement" type="radio" name="reference-place" value="text"> ' + refcon.getMessage('optionlocation2') + '</span></td><td width="20"></td><td><span class="refcon-option-point"><input type="checkbox" id="refcon-keepnames" name="names" value="yes">' + refcon.getMessage('checkboxkeepnames') + '</span></td></tr>\
						<tr><td><span class="refcon-option-point"><input class="refcon-refplacement" type="radio" name="reference-place" value="usage"> ' + refcon.getMessage('optionlocation3', ['<input id="refcon-table-options-uses" type="text" name="min_uses" size="2" value="2">']) + '</span></td><td width="20"></td><td><span class="refcon-option-point"><input type="checkbox" id="refcon-makecopies" name="copies" value="yes">' + refcon.getMessage('checkboxmakecopies') + '</span></td></tr>\
						</table></td></tr>');
			table.append('<tr id="refcon-buttons"><td colspan="5" align="center"><button type="button" id="refcon-abort-button" class="refcon-abort">'
				+ refcon.getMessage('buttonabort') + '</button><button type="button" id="refcon-continue-button">'
				+ refcon.getMessage('buttoncontinue') + '</button></td></tr>');

			container.css('display', 'block');

			// Bind events

			// Close window when user clicks on 'x'
			$('.refcon-abort').on('click', function () {
				gui.remove();
				refcon.cleanUp();
			});

			// Activate 'Continue' button when user changes some reference name
			$('#refcon-table .refcon-refname').on('input', function () {
				$('#refcon-continue-button').removeAttr('disabled');
			});

			// Validate reference names when user clicks 'Continue'. If there are errors, disable 'Continue' button
			$('#refcon-continue-button').on('click', function (event) {
				refcon.validateInput();
				if (table.find('[data-invalid]').length === 0) {
					refcon.afterScreenSave();
				} else {
					$('#refcon-continue-button').attr('disabled', true);
				}
			});

			// Sort table if user clicks on sortable table header
			$(".refcon-sortable").on('click', function () {
				refcon.sortTable($(this));
			});

			$("#refcon-table .refcon-refplacement").on('change', function () {
				switch ($(this).val()) {
					case 'template':
						$('#refcon-table .refcon-refplace').prop('checked', true);
						break;
					case 'text':
						$('#refcon-table .refcon-refplace').prop('checked', false);
						break;
					case 'usage':
						refcon.selectReferencesByUsage();
						break;
				}
			});
			// When user clicks on uses input field, select the third radio checkbox
			$("#refcon-table-options-uses").on('focus', function () {
				$('#refcon-table-options input:radio[name=reference-place]:nth(2)').trigger("click");
			});

			$("#refcon-table-options-uses").on('input', function () {
				refcon.selectReferencesByUsage();
			});

		},

		sortTable: function (columnHeader) {
			var order = $(columnHeader).hasClass('refcon-asc') ? 'refcon-desc' : 'refcon-asc';
			$('.refcon-sortable').removeClass('refcon-asc').removeClass('refcon-desc');
			$(columnHeader).addClass(order);

			var colIndex = $(columnHeader).prevAll().length;
			var tbod = $(columnHeader).closest("table").find("tbody");

			var i;
			for (i = 0; i < refcon.templateGroups.length; i++) {
				var rows = $(tbod).children("tr[template='" + i + "']");
				rows.sort(function (a, b) {
					var A = $(a).children("td").eq(colIndex).has("input").length ? $(a).children("td").eq(colIndex).children("input").val() : $(a).children("td").eq(colIndex).text();
					var B = $(b).children("td").eq(colIndex).has("input").length ? $(b).children("td").eq(colIndex).children("input").val() : $(b).children("td").eq(colIndex).text();

					if (colIndex === 1 || colIndex === 4) {
						A = Number(A);
						B = Number(B);
						return order === 'refcon-asc' ? A - B : B - A;
					} else {
						if (order === 'refcon-asc') {
							return A.localeCompare(B, mw.config.get('wgContentLanguage'));
						} else {
							return B.localeCompare(A, mw.config.get('wgContentLanguage'));
						}
					}
				});
				$(rows).each(function (index) {
					$(this).children("td").removeClass('refcon-even').removeClass('refcon-odd');
					$(this).children("td").addClass(index % 2 == 0 ? 'refcon-odd' : 'refcon-even');
				});

				$(columnHeader).closest("table").find("tbody").children("tr[template='" + i + "']").remove();
				$(columnHeader).closest("table").find("#templateheader" + i).after(rows);
			}

			// Activate 'Continue' button when user changes some reference name
			$('#refcon-table .refcon-refname').on('input', function () {
				$('#refcon-continue-button').removeAttr('disabled');
			});
		},

		selectReferencesByUsage: function () {
			var usage = $("#refcon-table-options-uses").val();
			if (usage.length > 0) {
				var regex = /[^0-9]+/;
				if (!usage.match(regex)) {
					usage = Number(usage);
					$('#refcon-table .refcon-refplace').each(function () {
						if ($(this).attr('value') >= usage)
							$(this).prop('checked', true);
						else
							$(this).prop('checked', false);
					});
				}
			}
		},

		validateInput: function () {
			var names = {}, duplicateNames = {}, i;

			for (i = 0; i < refcon.templateGroups.length; i++) {
				names[i] = {};
				duplicateNames[i] = {};
			}

			$('#refcon-table .refcon-refname').each(function () {
				if ($(this).val() in names[$(this).attr('template_id')]) {
					duplicateNames[$(this).attr('template_id')][$(this).val()] = 1;
				} else {
					names[$(this).attr('template_id')][$(this).val()] = 1;
				}
			});

			$('#refcon-table .refcon-refname').each(function () {
				if ($(this).val() in duplicateNames[$(this).attr('template_id')]) {
					refcon.markFieldAsInvalid($(this));
				} else if ($(this).val() === '') {
					refcon.markFieldAsInvalid($(this));
				} else if ($(this).val().match(/[<>"]/) !== null) {
					refcon.markFieldAsInvalid($(this));
				} else {
					refcon.markFieldAsValid($(this));
				}
			});
		},

		markFieldAsValid: function (inputField) {
			$(inputField).removeAttr('data-invalid');
			$(inputField).closest('tr').find('img').attr('src', refcon.getOption('image-yes'));
		},

		markFieldAsInvalid: function (inputField) {
			$(inputField).attr('data-invalid', 1);
			$(inputField).closest('tr').find('img').attr('src', refcon.getOption('image-no'));
		},

		/**
		 * Process form after the Save button was pressed
		 *
		 * @return {void}
		 */

		afterScreenSave: function () {
			$('#refcon-table tr[template]').each(function () {
				var refName = $(this).find('.refcon-refname');
				var name = refName.val();
				var templateId = refName.attr('template_id');
				var refId = refName.attr('name');
				// change reference names to the ones from the form, in case some name was changed
				refcon.refTemplates[templateId].references[refId].changeName(name);
				// save reference location preference from the form into reference object
				var refPlace = $(this).find('.refcon-refplace');
				refcon.refTemplates[templateId].references[refId].inRefTemplate = refPlace.prop('checked') ? true : false;
			});

			// If user has checked "save sorted" checkbox
			if ($('#refcon-savesorted').prop('checked')) {
				var sortOptions = {};
				if ($('.refcon-asc').prevAll().length) {
					sortOptions['column'] = $('.refcon-asc').prevAll().length;
					sortOptions['order'] = 'asc';
				} else if ($('.refcon-desc').prevAll().length) {
					sortOptions['column'] = $('.refcon-desc').prevAll().length;
					sortOptions['order'] = 'desc';
				}
				refcon.userOptions['sort'] = sortOptions;
			}
			// If user has checked "keep names" checkbox
			if ($('#refcon-keepnames').prop('checked'))
				refcon.userOptions['keepnames'] = true;
			else
				refcon.userOptions['keepnames'] = false;

			// If user has checked "separate copies" checkbox
			if ($('#refcon-makecopies').prop('checked'))
				refcon.userOptions['makecopies'] = true;
			else
				refcon.userOptions['makecopies'] = false;

			refcon.commit();
		},

		/**
		 * Parse article text and find all reference templates indexes
		 *
		 * @return {array} Start indexes of all reference templates
		 */

		parseIndexes: function () {

			var refTemplateNames = refcon.getOption('reftemplatenames');

			var wikitext = refcon.getTextbox().val(),
				i, name, re, refTemplateIndexes = [];

			// Make all appearances of the reference templates in article text uniform
			if (Array.isArray(refTemplateNames)) {
				var refTemplateName = refTemplateNames[0];

				for (i = 0; i < refTemplateNames.length; i++) {
					name = refTemplateNames[i];
					re = new RegExp('{{\s*' + name, 'gi');
					wikitext = wikitext.replace(re, '{{' + refTemplateName);
				}

				// Find all indexes of the reference template in the article and put them into array
				// Index is the place in article text where references template starts
				var pos = wikitext.indexOf('{{' + refTemplateName);

				if (pos !== -1)
					refTemplateIndexes.push(pos);

				while (pos !== -1) {
					pos = wikitext.indexOf('{{' + refTemplateName, pos + 1);
					if (pos !== -1)
						refTemplateIndexes.push(pos);
				}
			} else {
				// call some error handling function and halt
			}

			// Set the refcon variable with modified wikitext
			refcon.textBoxText = wikitext;

			return (refTemplateIndexes);

		},

		/**
		 * Get reference template's content and end index
		 *
		 * @param {integer} reference template's index in article text
		 * @param {integer} next reference template's index in article text
		 *
		 * @return {object} reference template's content string, start and end indexes
		 */

		getTemplateContent: function (templateIndex, nextTemplateIndex) {

			var textPart = refcon.textBoxText.substring(templateIndex, nextTemplateIndex);
			var i, depth = 1, prevChar = '', templateEndIndex = 0, templateAbsEndIndex = null, templateContent = '';

			// Go through the textPart and find the template's end code '}}'
			// @todo: could use ProveIt's alternative code here
			for (i = 2; i < textPart.length; i++) {
				if (textPart.charAt(i) === "{" && prevChar === "{") {
					++depth;
					prevChar = '';  // reset prevChar to avoid double counting for '{{{'
				} else if (textPart.charAt(i) === "}" && prevChar === "}") {
					--depth;
					prevChar = '';  // reset prevChar to avoid double counting for '}}}'
				} else {
					prevChar = textPart.charAt(i);
				}

				if (depth === 0) {
					templateEndIndex = i + 1;
					break;
				}
			}

			// If templateEndIndex is 0, reference template's ending '}}' is missing in the textPart

			if (templateEndIndex > 0) {
				templateContent = textPart.substring(0, templateEndIndex);
				templateAbsEndIndex = templateIndex + templateEndIndex;
			}

			return ({
				'templateContent': templateContent,
				'refStartIndex': templateIndex,
				'refEndIndex': templateAbsEndIndex
			});

		},

		/**
		 * Get all reference templates' name and value pairs using a single mw.Api call
		 *
		 * @param {string} String that contains all article's reflist templates
		 *
		 * @return {array} List of reference template objects with parameter names and values
		 */

		getTemplateParams: function (templatesString) {

			var paramPairsList = [];
			var refTemplateNames = refcon.getOption('reftemplatenames');

			if (Array.isArray(refTemplateNames)) {
				var mainRefTemplateName = refTemplateNames[0];
			} else {
				// call some error handling function and halt
			}

			// We will do a single API call to get all reflist templates parameter pairs
			new mw.Api().post({
				'action': 'expandtemplates',
				'text': templatesString,
				'prop': 'parsetree'
			}, { async: false }).done(function (data) {
				var parsetree = data.expandtemplates.parsetree;
				var result = xmlToJSON.parseString(parsetree);
				var i, templateRoot = result.root[0].template;

				//@todo: could rewrite the code to use JSON.parse
				for (i = 0; i < templateRoot.length; i++) {
					if (templateRoot[i].title[0]['_text'] === mainRefTemplateName) {
						var paramPairs = {};
						var part = templateRoot[i].part;
						if (typeof part !== 'undefined') {
							var j, name, value, ext;
							for (j = 0; j < part.length; j++) {
								if (typeof part[j].equals !== 'undefined') {
									name = part[j].name[0]['_text'];
								} else {
									name = part[j].name[0]['_attr']['index']['_value'];
								}
								name = typeof name === 'string' ? name.trim() : name;
								// By checking 'ext' first, '_text' second,
								// if the parameter value is a list of references that contains some text between the reference tags, the text is lost.
								// But at least we get the references and not the text instead
								if (typeof part[j].value[0]['ext'] !== 'undefined') {
									ext = part[j].value[0]['ext'];
									if (Array.isArray(ext)) {
										var k, attr, inner;
										value = [];
										for (k = 0; k < ext.length; k++) {
											if (typeof ext[k]['name'][0]['_text'] !== 'undefined' && ext[k]['name'][0]['_text'].toLowerCase() === 'ref'
												&& typeof ext[k]['close'][0]['_text'] !== 'undefined' && ext[k]['close'][0]['_text'].toLowerCase() === '</ref>') {
												if (typeof ext[k]['attr'][0]['_text'] !== 'undefined' && typeof ext[k]['inner'][0]['_text'] !== 'undefined') {
													value.push({
														'attr': ext[k]['attr'][0]['_text'],
														'inner': ext[k]['inner'][0]['_text']
													});
												}
											}
										}
									}
								} else if (typeof part[j].value[0]['_text'] !== 'undefined') {
									value = part[j].value[0]['_text'];
								}
								value = typeof value === 'string' ? value.trim() : value;
								paramPairs[name] = value;
							}
							paramPairsList.push(paramPairs);
						}
					}
				}
			});
			return (paramPairsList);
		},

		/**
		 * Get reference template object from paramPairs and templateData objects
		 *
		 * @param {object} reference template data object with indexes and template content
		 * @param {object} reference template parameter pairs object with param names and values
		 *
		 * @return {object} reference template object
		 */

		getTemplateObject: function (templateData, paramPairs) {

			var name, i, groupName;
			var refGroupNames = refcon.getOption('reftemplategroupnames');

			// Go through paramPairs and see if there is a configuration defined group name in parameter names. Get it's value
			if (Array.isArray(refGroupNames)) {
				if (typeof paramPairs === 'object') {
					for (i = 0; i < refGroupNames.length; i++) {
						var name = refGroupNames[i];
						if (typeof paramPairs[name] !== 'undefined') {
							groupName = paramPairs[name];
							break;
						}
					}
				}
			} else {
				// call some error handling function and halt
			}

			if (typeof groupName === 'undefined') {
				groupName = '';
			}

			refcon.templateGroups.push(groupName);

			// Build basic reference template
			var refTemplate = new refcon.RefTemplate({
				'group': groupName,
				'string': templateData['templateContent'],
				'start': templateData['refStartIndex'],
				'end': templateData['refEndIndex'],
				'params': paramPairs
			});

			return (refTemplate);
		},

		/**
		 * Parse references in reference template's refs field (using mw.Api)
		 *
		 * @param {object} refTemplate object
		 *
		  * @return {void} 
		 */

		parseTemplateRefs: function (refTemplate) {

			var refsNames = refcon.getOption('reftemplaterefsnames');
			var refsArray, refsName, i;

			if (Array.isArray(refsNames)) {
				if (typeof refTemplate.params === 'object') {
					for (i = 0; i < refsNames.length; i++) {
						refsName = refsNames[i];
						if (typeof refTemplate.params[refsName] !== 'undefined') {
							refsArray = refTemplate.params[refsName];
							break;
						}
					}
				}
			} else {
				// call some error handling function and halt
			}

			// Look for references inside the reference template's refs parameter

			if (typeof refsArray !== 'undefined' && refsArray.length > 0) {
				for (i = 0; i < refsArray.length; i++) {

					// Turn all matches into reference objects
					reference = refcon.parseReference(['', refsArray[i].attr, refsArray[i].inner], 'reference');

					// Only add references that have name
					if (reference['name'].length > 0) {
						refTemplate.addRef(reference);
					}
				}
			}
			refcon.refTemplates.push(refTemplate);
		},

		/**
		 * Make a reference object out of a reference string
		 *
		 * @param {array} match array produced by regexp
		 * @param {string} type. can be either "reference" or "citation"
		 *
		 * @return {object} returns either reference object or citation object based on type
		 */

		parseReference: function (data, type) {
			var params = {}, referenceName, referenceGroup,
				referenceString = data[0], refParamString = data[1],
				referenceContent = data[2], referenceIndex = data.index;

			if (typeof refParamString !== 'undefined') {
				refParamString = refParamString.trim();

				if (refParamString.length > 0) {
					//Examples of strings to extract name and group from
					//group="arvuti" name="refname1"
					//name="refname2" group="arvuti str"
					//group="arvuti"
					//name="refname1 blah"

					var re = /(?:(name|group)\s*=\s*(?:"([^"]+)"|'([^']+)'|([^ ]+)))(?:\s+(name|group)\s*=\s*(?:"([^"]+)"|'([^']+)'|([^ ]+)))?/i;

					var match = refParamString.match(re);

					try {
						if (typeof match[1] !== 'undefined' && (typeof match[2] !== 'undefined' || typeof match[3] !== 'undefined' || typeof match[4] !== 'undefined')) {
							if (typeof match[2] !== 'undefined') {
								params[match[1]] = match[2];
							} else if (typeof match[3] !== 'undefined') {
								params[match[1]] = match[3];
							} else {
								params[match[1]] = match[4];
							}
						}

						if (typeof match[5] !== 'undefined' && (typeof match[6] !== 'undefined' || typeof match[7] !== 'undefined' || typeof match[8] !== 'undefined')) {
							if (typeof match[6] !== 'undefined') {
								params[match[5]] = match[6];
							} else if (typeof match[7] !== 'undefined') {
								params[match[5]] = match[7];
							} else {
								params[match[5]] = match[8];
							}
						}
					} catch (e) {
						refcon.throwReferenceError(referenceString, refcon.getMessage('parsereferror', [referenceString]), e);
					}

					referenceName = params['name'] ? params['name'] : '';
					referenceGroup = params['group'] ? params['group'] : '';
				}
			}

			if (typeof referenceGroup === 'undefined')
				referenceGroup = '';

			if (typeof referenceName === 'undefined')
				referenceName = '';

			var found = referenceName.match(/[<>"]/);
			if (found !== null) {
				refcon.throwReferenceError(referenceString, refcon.getMessage('parserefforbidden', [found[0], referenceString]));
			}

			// Clean reference name and content of newlines, double spaces, leading or trailing whitespace and more

			referenceName = refcon.cleanString(referenceName, 'name');

			if (typeof referenceContent !== 'undefined')
				referenceContent = refcon.cleanString(referenceContent, 'content');

			if (type === 'reference') {
				// Build the basic reference
				var reference = new refcon.Reference({
					'group': referenceGroup,
					'name': referenceName,
					'content': referenceContent,
					'index': referenceIndex,
					'string': referenceString
				});
			} else if (type === 'citation') {
				// Build the basic citation
				var reference = new refcon.Citation({
					'group': referenceGroup,
					'name': referenceName,
					'index': referenceIndex,
					'string': referenceString
				});
			}
			return reference;
		},

		throwReferenceError: function (referenceString, message, error) {
			var found = refcon.getTextbox().val().match(refcon.escapeRegExp(referenceString));
			refcon.highlight(found.index, referenceString);
			window.alert(message);
			refcon.cleanUp();
			throw new Error(error);
		},

		/**
		 * Clean reference name and content of newlines, double spaces, leading or trailing whitespace, etc
		 *
		 * @param {string} reference name or reference content string
		 * @param (string) whether the string is name or content
		 *
		 * @return {string} cleaned reference name and content
		 */

		cleanString: function (str, type) {

			// get rid of newlines and trailing/leading space
			str = str.replace(/(\r\n|\n|\r)/gm, ' ').trim();

			// get rid of double whitespace inside string
			str = str.replace(/\s\s+/g, ' ');

			// if the str is content, get rid of extra space before template closing / after template opening
			if (type === 'content') {
				str = str.replace(/ }}/g, '}}');
				str = str.replace(/{{ /g, '{{');
			}

			return (str);
		},

		escapeRegExp: function (str) {
			return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
		},

		/**
		 * Highlight string in the textbox and scroll it to view
		 *
		 * @return {void}
		 */
		highlight: function (index, string) {
			var textbox = refcon.getTextbox()[0],
				text = textbox.value;

			// Scroll to the string
			textbox.value = text.substring(0, index);
			textbox.focus();
			textbox.scrollTop = 99999999; // Larger than any real textarea (hopefully)
			var currentScrollTop = textbox.scrollTop;
			textbox.value += text.substring(index);
			if (currentScrollTop > 0) {
				textbox.scrollTop = currentScrollTop + 300;
			}

			// Highlight the string
			var start = index,
				end = start + string.length;
			$(textbox).focus().textSelection('setSelection', { 'start': start, 'end': end });
		},

		/**
		 * Turn all article text parts – parts that are between reference templates – into objects and save into array
		 *
		 * @return {void}
		 */

		storeTextParts: function () {
			var i, text, refEnd, from, to, textPart;

			for (i = 0; i < refcon.refTemplates.length; i++) {

				from = refEnd ? refEnd : 0;

				to = refcon.refTemplates[i]['start'];
				refEnd = refcon.refTemplates[i]['end'];

				if (to === 0) {
					continue;
				}

				text = refcon.textBoxText.substring(from, to);

				// Textpart's references can only be in templates that come after the textpart in article text
				var j, groupName, groupNames = {};

				for (j = i; j < refcon.refTemplates.length; j++) {
					groupName = refcon.templateGroups[j];
					// Only add the first instance of template group
					if (typeof groupNames[groupName] === 'undefined') {
						groupNames[groupName] = j;
					}
				}

				// @todo: check what happens if a reference template follows another reference template without any space.
				// Does textpart still get correct inTemplate sequence?

				// Create new TextPart object and store it

				textPart = new refcon.TextPart({
					'start': from,
					'end': to,
					'string': text,
					'inTemplates': groupNames
				});

				refcon.textParts.push(textPart);
			}

			// Add the last text part after the last reference template
			if (typeof refEnd === 'number' && refEnd > 0) {
				if (refcon.textBoxText.length > refEnd) {

					text = refcon.textBoxText.substring(refEnd, refcon.textBoxText.length);

					textPart = new refcon.TextPart({
						'start': refEnd,
						'end': refcon.textBoxText.length,
						'string': text
					});

					refcon.textParts.push(textPart);
				}
			}
		},

		/**
		 * Parses the inside of a simple template into a key/value object. Complex templates need to be handled with mw.Api instead.
		 * 
		 * @param {string} templateContent The content of the template, excluding the template name and braces.
		 * 
		 * @return {object} An object with the template parameters as keys and their values as values.
		 */
		parseTemplate: function (templateContent) {
			var parts = templateContent.split('|');
			var params = {};
			var unnamedIndex = 1;

			parts.forEach(function (part) {
				part = part.trim();
				var eqIndex = part.indexOf('=');
				if (eqIndex !== -1) {
					// Named parameter (e.g. p1=page1)
					var key = part.substring(0, eqIndex).trim();
					var value = part.substring(eqIndex + 1).trim();
					params[key] = value;
				} else {
					// Unnamed parameter: assign a sequential numeric key (as string)
					while (params[unnamedIndex.toString()]) {
						unnamedIndex++;
					}
					params[unnamedIndex.toString()] = part;
					unnamedIndex++;
				}
			});
			return params;
		},

		parseRTemplate: function (data) {
			var referenceString = data[0], refParamString = data[1],
				referenceIndex = data.index;
			var params = refcon.parseTemplate(refParamString);

			// Buckets will hold the per-citation info (for chained citations)
			var buckets = {};
			var groupValue = '';

			// Iterate over each key/value pair from the template.
			for (var key in params) {
				// If it's a group parameter (g, group, or grp), store it for later cloning.
				if (/^(g|group|grp)$/i.test(key)) {
					groupValue = params[key];
					continue;
				}

				// Handle page parameters (e.g. p, page, pp, pages) and their numbered variants (p1, p2, …)
				var pageMatch = key.match(/^(p|page|pp|pages)(\d+)?$/i);
				if (pageMatch) {
					var bucketIndex = pageMatch[2] ? parseInt(pageMatch[2], 10) : 1;
					buckets[bucketIndex] = buckets[bucketIndex] || {};
					buckets[bucketIndex].page = params[key];
					continue;
				}

				// Handle quote parameters (e.g. q, quote) and their numbered variants
				var quoteMatch = key.match(/^(q|quote)(\d+)?$/i);
				if (quoteMatch) {
					var bucketIndex = quoteMatch[2] ? parseInt(quoteMatch[2], 10) : 1;
					buckets[bucketIndex] = buckets[bucketIndex] || {};
					buckets[bucketIndex].quote = params[key];
					continue;
				}

				// Handle numeric keys (e.g. "1", "2") which are used for citation names.
				if (/^\d+$/.test(key)) {
					var bucketIndex = parseInt(key, 10);
					buckets[bucketIndex] = buckets[bucketIndex] || {};
					buckets[bucketIndex].name = params[key];
				}
			}

			var bucketsArray = [];
			for (var idx in buckets) {
				if (buckets[idx].name) {  // Ignore unnamed citations
					bucketsArray.push(buckets[idx]);
				}
			}
			if (bucketsArray.length === 0) {
				return [];
			}

			// Cut the reference string into pieces based on the citation names
			for (var i = 0; i < bucketsArray.length; i++) {
				bucketsArray[i].index = referenceString.indexOf(bucketsArray[i].name);
				if (i > 0) {
					bucketsArray[i - 1].string = referenceString.substring(bucketsArray[i - 1].index, bucketsArray[i].index);
				}
			}
			bucketsArray[bucketsArray.length - 1].string = referenceString.substring(bucketsArray[bucketsArray.length - 1].index);
			bucketsArray[0].string = referenceString.substring(0, bucketsArray[0].index) + bucketsArray[0].string;

			var citations = [], citation;
			for (var i = 0; i < bucketsArray.length; i++) {
				citation = new refcon.Citation({
					'group': groupValue,
					'name': bucketsArray[i].name,
					'index': bucketsArray[i].index + referenceIndex,
					'string': bucketsArray[i].string,
					'page': bucketsArray[i].page,
					'quote': bucketsArray[i].quote
				});
				citations.push(citation);
			}
			return citations;
		},

		/**
		 * Find all references and citations in a TextPart object and store them in the object.
		 *
		 * @param {object} TextPart object
		 */

		parseTextParts: function (textPart) {

			if (typeof textPart.string !== 'undefined' && textPart.string.length > 0) {

				// Look for all citations
				// Citations come in two forms:
				// 1. <ref name="CV Kontrollikoda"/>
				// 2. <ref name="pm"></ref>
				// Ref label can have optional group parameter:
				// <ref group="blah" name="CV Kontrollikoda"/> or <ref name="CV Kontrollikoda" group="blah"/>
				// Group and name parameter values can be between '' or "", or bare (if value has no whitespaces)

				var citations = [],
					citationsRegExp = /<ref(\s+[^/>]+)(?:\/\s*>|><\/ref>)/ig,
					rTemplateRegExp = /\{\{\s*[Rr]\|([^}]+)\}\}/ig,
					match,
					citation;

				while ((match = citationsRegExp.exec(textPart.string))) {

					// Turn all the matches into citation objects
					citation = refcon.parseReference(match, 'citation');

					if (typeof citation === 'object' && typeof citation.name !== 'undefined') {
						citations.push(citation);
					}
				}

				while ((match = rTemplateRegExp.exec(textPart.string))) {
					rTemplateCitations = refcon.parseRTemplate(match);
					citations = citations.concat(rTemplateCitations);
				}

				textPart.citations = citations;

				// Look for all references

				var references = [],
					referencesRegExp = /<ref(\s+[^\/]+?)?>([\s\S]*?)<\/ref>/ig,
					match,
					reference;

				while ((match = referencesRegExp.exec(textPart.string))) {
					// Avoid further processing of citations like <ref name="pm"></ref>
					if (match[2] === '') {
						continue;
					}

					// Turn all the matches into reference objects
					reference = refcon.parseReference(match, 'reference');

					references.push(reference);
				}

				textPart.references = references;
			}
		},

		/**
		 * Compare references in a TextPart object to the references in reference template (if there are any). Add references into
		 * reference template. Update indexes. For each reference create citation object and link it with reflist template reference.
		 *
		 * @param {object} TextPart object
		 */
		processTextPartRefs: function (textPart) {
			var i, reference, refTemplate, templateRef,
				createdCitations = [];

			for (i = 0; i < textPart.references.length; i++) {
				reference = textPart.references[i];

				refTemplate = refcon.refTemplates[textPart.inTemplates[reference.group]];

				// First add named references, because otherwise we could create new records (and names) 
				// for already existing text part defined references
				if (reference.content.length > 0 && reference.name.length > 0) {

					// First check if this a complete duplicate reference (name and value are the same)
					templateRef = refcon.getRefByIndex(refTemplate, 'keyValues', reference.name + '_' + reference.content);

					if (typeof templateRef === 'object') {
						if (templateRef.name === reference.name && templateRef.content === reference.content) {
							// found exact duplicate
							var citation = new refcon.Citation({
								'group': reference.group,
								'name': reference.name,
								'index': reference.index,
								'string': reference.string
							});
							templateRef.citations.push(citation);
							createdCitations.push(citation);
							continue;
						}
					}
					// Check if the reference has the same name but different content than template reference
					templateRef = refcon.getRefByIndex(refTemplate, 'keys', reference.name);

					if (typeof templateRef === 'object') {
						if (templateRef.name === reference.name && templateRef.content !== reference.content) {
							// found reference with the same name but different content

							// add reference content to template references under new name
							var newName = refTemplate.getNewName(reference);
							var newRef = new refcon.Reference({
								'group': reference.group,
								'name': newName,
								'content': reference.content,
								'inRefTemplate': false
							});
							var citation = new refcon.Citation({
								'group': reference.group,
								'name': newName,
								'index': reference.index,
								'string': reference.string
							});
							newRef.citations.push(citation);
							refTemplate.addRef(newRef);
							createdCitations.push(citation);
							// add names into replacements object, so we can replace all citation names that use the old name
							refTemplate.replacements[reference.name] = newName;
							continue;
						}
					}
					// Check if the reference has the same content but different name than template reference
					templateRef = refcon.getRefByIndex(refTemplate, 'values', reference.content);

					if (typeof templateRef === 'object') {
						if (templateRef.content === reference.content && templateRef.name !== reference.name) {
							// Found reference with the same content but different name.
							// Drop reference name, use reflist template reference name as citation name
							var citation = new refcon.Citation({
								'group': reference.group,
								'name': templateRef.name,
								'index': reference.index,
								'string': reference.string
							});
							templateRef.citations.push(citation);
							createdCitations.push(citation);
							// add names into replacements object, so we can replace all citation names that use the old name
							refTemplate.replacements[reference.name] = templateRef.name;
							continue;
						}
					}
					// If we get here, it means we've got a named reference that has not yet been described in reflist template.

					var refName = reference.name;
					// If the reference name is bad (starts with ":"), replace it with a new name
					if (refName.charAt(0) == ':') {
						refName = refTemplate.getNewName(reference);
						refTemplate.replacements[reference.name] = refName;
					}

					// Add the reference to reflist references
					var newRef = new refcon.Reference({
						'group': reference.group,
						'name': refName,
						'content': reference.content,
						'inRefTemplate': false
					});
					var citation = new refcon.Citation({
						'group': reference.group,
						'name': refName,
						'index': reference.index,
						'string': reference.string
					});
					newRef.citations.push(citation);
					refTemplate.addRef(newRef);
					createdCitations.push(citation);
				}
			}
			// Now we go through unnamed references
			for (i = 0; i < textPart.references.length; i++) {
				reference = textPart.references[i];

				refTemplate = refcon.refTemplates[textPart.inTemplates[reference.group]];
				if (refTemplate === undefined) {  // 例如 {{NoteTag}}
					continue;
				}

				if (reference.content.length > 0 && reference.name.length === 0) {
					templateRef = refcon.getRefByIndex(refTemplate, 'values', reference.content);
					if (typeof templateRef === 'object') {
						if (templateRef.content === reference.content) {
							// found reference with the same content
							var citation = new refcon.Citation({
								'group': reference.group,
								'name': templateRef.name,
								'index': reference.index,
								'string': reference.string
							});
							templateRef.citations.push(citation);
							createdCitations.push(citation);
							continue;
						}
					}
					// If we get here, we have a completely new unnamed reference
					// add the reference to template references
					var newName = refTemplate.getNewName(reference);
					var newRef = new refcon.Reference({
						'group': reference.group,
						'name': newName,
						'content': reference.content,
						'inRefTemplate': false
					});
					var citation = new refcon.Citation({
						'group': reference.group,
						'name': newName,
						'index': reference.index,
						'string': reference.string
					});
					newRef.citations.push(citation);
					refTemplate.addRef(newRef);
					createdCitations.push(citation);
				}
			}
			textPart.linkedCitations = createdCitations;
		},

		/**
		 * Link citations to their reflist template references
		 *
		 * @param {object} TextPart object
		 *
		 * @return {void}
		 */
		linkCitations: function (textPart) {

			var citation, refTemplate, replaceName, templateRef,
				i;

			for (i = 0; i < textPart.citations.length; i++) {
				citation = textPart.citations[i];

				refTemplate = refcon.refTemplates[textPart.inTemplates[citation.group]];

				if (citation.name.length > 0) {

					// If there is replacement name in replacements object, replace the citation name
					replaceName = refTemplate.replacements[citation.name];

					if (typeof replaceName !== 'undefined') {
						citation.name = replaceName;
					}

					// For each citation try to find its reference
					templateRef = refcon.getRefByIndex(refTemplate, 'keys', citation.name);
					if (typeof templateRef === 'object') {
						if (templateRef.name === citation.name) {
							templateRef.citations.push(citation);
							textPart.linkedCitations.push(citation);
						}
					}
				}
			}
		},

		/**
		 * Replace all references in TextPart object string with citations. Also replace citation names that were changed in previous steps
		 *
		 * @param {object} TextPart object
		 *
		 * @return {void}
		 */
		replaceTextPartRefs: function (textPart) {
			var i, citation, refTemplate, templateRef;
			for (i = 0; i < textPart.linkedCitations.length; i++) {
				citation = textPart.linkedCitations[i];
				if (citation.name.length > 0) {
					refTemplate = refcon.refTemplates[textPart.inTemplates[citation.group]];
					templateRef = refcon.getRefByIndex(refTemplate, 'keys', citation.name);

					// For the references that are marked as "in reference list template" replace all instances with citation
					if (templateRef.inRefTemplate === true) {
						textPart.string = textPart.string.replace(citation.string, citation.toString());
						// For the references that are marked as "in the body of article"...
					} else {
						// if the reference has just one use, output the reference string w/o name (unless user options "keep names" was selected)
						if (templateRef.citations.length == 1) {
							textPart.string = textPart.string.replace(citation.string, templateRef.toStringText(refcon.userOptions.keepnames));
							// if the reference has more uses...
						} else {
							// if user has requested every article body reference to be separate copy...
							if (refcon.userOptions.makecopies === true) {
								textPart.string = textPart.string.replace(citation.string, templateRef.toStringText(refcon.userOptions.keepnames));
								// if copies option was not requested...
							} else {
								// if the reference has not been output yet, output named reference
								if (templateRef.wasPrinted === false) {
									textPart.string = textPart.string.replace(citation.string, templateRef.toStringText(true));
									// mark reference as printed
									templateRef.wasPrinted = true;
									// if the reference has already been printed, output citation
								} else {
									textPart.string = textPart.string.replace(citation.string, citation.toString());
								}
							}
						}
					}
				}
			}
		},

		/**
		 * Build reference templates
		 *
		 * @param {object} RefTemplate object
		 *
		 * @return {void}
		 */
		buildRefTemplates: function (refTemplate) {
			var i, reference, referencesString = '', refsAdded = false;

			// sort references if user has checked the checkbox
			if (typeof refcon.userOptions.sort === 'object' && Object.keys(refcon.userOptions.sort).length > 0) {
				refcon.sortReferences(refTemplate);
			}

			// turn reference data into reflist parameter value string
			for (i = 0; i < refTemplate.references.length; i++) {
				reference = refTemplate.references[i];
				if (typeof reference === 'object' && reference.inRefTemplate === true) {
					referencesString += reference.toString() + "\n";
				}
			}
			// Cut the last newline
			referencesString = referencesString.substr(0, referencesString.length - 1);

			var refTemplateNames = refcon.getOption('reftemplatenames');

			if (Array.isArray(refTemplateNames)) {
				var refTemplateName = refTemplateNames[0];
			} else {
				// call some error handling function and halt
			}

			var refsNames = refcon.getOption('reftemplaterefsnames');

			if (Array.isArray(refsNames)) {
				var refsName = refsNames[0];
			} else {
				// call some error handling function and halt
			}

			var templateString = '{{' + refTemplateName;

			// Build references template string
			if (Object.keys(refTemplate.params).length > 0) {
				// Go through params object
				for (var name in refTemplate.params) {
					var value = refTemplate.params[name];
					// If param name matches with config name for reference list template refs param...
					if (refsNames.indexOf(name) > -1) {
						// ... only if there are references in reflist template
						if (referencesString.length > 0) {
							// ... add refstring to reflist params
							templateString += '|' + refsName + '=' + "\n" + referencesString;
							refsAdded = true;
						}
						continue;
					} else if (typeof value !== 'string' && typeof value !== 'number') {
						// If value is anything other than string or number, skip it. 
						// Value is array if, for example, references are incorrectly defined inside unnamed parameter.
						continue;
					}
					templateString += '|' + name + '=' + value;
				}
			}
			// if the reflist template was without any parameters, add parameter and references here
			if (refsAdded === false && referencesString.length > 0) {
				templateString += '|' + refsName + "=\n" + referencesString;
			}
			if (referencesString.length > 0)
				templateString += "\n}}";
			else
				templateString += "}}";

			refTemplate.string = templateString;
		},

		/**
		 * Sort references inside reflist template according to user preferences
		 *
		 * @param {object} Reftemplate object
		 *
		 * @return {void}
		 */
		sortReferences: function (refTemplate) {

			if (refcon.userOptions.sort.column === 1) {
				refTemplate.references = refcon.userOptions.sort.order === 'desc' ? refTemplate.references.reverse() : refTemplate.references;
			} else {
				refTemplate.references.sort(function (a, b) {
					// order by reference name
					if (refcon.userOptions.sort.column === 2) {
						return refcon.userOptions.sort.order === 'asc' ? a.name.localeCompare(b.name, mw.config.get('wgContentLanguage')) : b.name.localeCompare(a.name, mw.config.get('wgContentLanguage'));
						// order by reference content
					} else if (refcon.userOptions.sort.column === 3) {
						return refcon.userOptions.sort.order === 'asc' ? a.content.localeCompare(b.content, mw.config.get('wgContentLanguage')) : b.content.localeCompare(a.content, mw.config.get('wgContentLanguage'));
						// order by citations count
					} else if (refcon.userOptions.sort.column === 4) {
						return refcon.userOptions.sort.order === 'asc' ? a.citations.length - b.citations.length : b.citations.length - a.citations.length;
					}
				});
			}
		},

		/**
		 * Verify if configuration option should be used. Return true or false
		 * @param {string} Refcon option as returned by refcon.getOption method
	
		 * @param {string} User configuration variable content
		 *
		 * @return {boolean} True of false
		 */
		useConfigOption: function (configOptionValue, userConfigOptionName) {
			var result = false;
			switch (configOptionValue) {
				case 'yes':
					result = true;
					break;
				case 'no':
					result = false;
					break;
				case 'user':
					if (typeof refConsolidateConfig === 'object' && typeof refConsolidateConfig[userConfigOptionName] !== 'undefined' && refConsolidateConfig[userConfigOptionName] === true) {
						result = true;
						break;
					}
					if (userConfigOptionName === 'usetemplateR') { // 只有 useTemplateR 預設為 true
						result = true;
						break;
					}
					break;
				default:
					result = false;
			}
			return (result);
		},

		/**
		 * Write text parts and reference templates into textbox variable
		 *
		 * @return {string} String that contains article text
		 */

		writeTextBoxText: function () {

			var textBoxString = '';

			for (i = 0; i < refcon.textParts.length; i++) {
				textPart = refcon.textParts[i];

				textBoxString += textPart.string;

				if (typeof refcon.refTemplates[i] === 'object') {
					textBoxString += refcon.refTemplates[i].string;
				}
			}

			// Deal with introduced symbols for R templates
			textBoxString = textBoxString.replace(/⬗\}\}\{\{[Rr]p\|([^\}=]+)\}\}/g, '|p=$1⬗}}⬙');
			textBoxString = textBoxString.replace(/⬗\}\}\{\{⬖r\|/g, '|');
			textBoxString = textBoxString.replace(/[⬘⬙⬗⬖]/g, '');

			return (textBoxString);
		},

		/**
		 * Index into reference template template objects and return template object
		 *
		 * @param {object} reference template object
		 * @param {string} index name
		 * @param {integer} key to index into
		 *
		 * @return {object} reference template object 
		 */

		getRefByIndex: function (refTemplate, dictname, key) {
			var templateRef;
			var refDict = refTemplate[dictname];

			if (key in refDict && Array.isArray(refDict[key])) {
				var refKey = refDict[key][0];
				templateRef = refTemplate.getRef(refKey);
			}

			return (templateRef);
		},

		/**
		 * Add the RefCon edit summary
		 *
		 * @return {void}
		 */
		addSummary: function () {
			var currentSummary = $('#wpSummary').val();
			var refconSummary = refcon.getOption('summary');
			var summarySeparator = refcon.getOption('summaryseparator');

			if (!refconSummary) {
				return; // No summary defined
			}
			if (currentSummary.indexOf(refconSummary) > -1) {
				return; // Don't add it twice
			}
			$('#wpSummary').val(currentSummary ? currentSummary + summarySeparator + refconSummary : refconSummary);
		},

		/**
		 * Set minor edit checkbox and click View Differences button
		 *
		 * @return {void}
		 */
		showDifferenceView: function () {
			document.forms.editform.wpMinoredit.checked = true;
			document.forms.editform.wpDiff.click();
		},

		/**
		 * Produces random string with a given length
		 *
		 * @param {integer} string length
		 * @param {string} charset (optional)
		 *
		 * @return {string} random string
		 */

		randomString: function (len, charSet) {
			charSet = charSet || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
			var randomString = '';
			for (var i = 0; i < len; i++) {
				var randomPoz = Math.floor(Math.random() * charSet.length);
				randomString += charSet.substring(randomPoz, randomPoz + 1);
			}
			return randomString;
		},

		/**
		 * Produces suggested ref name from url
		 * 
		 * @param {object} reference reference object
		 * 
		 * @return {string} suggested ref name
		 */
		extractRefName: function (reference) {
			var refContent = reference.content;

			var urlMatch = refContent.match(/https?:\/\/[^ ]+/);
			var urlString = urlMatch ? urlMatch[0] : '';
			var refName = '';

			if (urlString.length > 0) {
				var url = new URL(urlString);
				var domain = url.hostname;
				var domainParts = domain.split(".");
				if (domainParts.length > 2) {
					domainParts = domainParts.slice(-3);
					if (['com', 'org', 'edu', 'gov', 'net'].indexOf(domainParts[1]) == -1 && domainParts[1].length > 2) {
						domainParts = domainParts.slice(-2);
					}
				}
				refName = domainParts[0];
			} else {
				// Match Cite templates
				var templateMatch = refContent.match(/\{\{\s*[Cc]ite\s+([^|]+)/);
				var templateName = templateMatch ? templateMatch[1].trim() : '';

				if (templateName.length > 0) {
					refName = templateName.replace(/ /g, '-');
				} else {
					// If no URL or template name, generate random string
					refName = this.randomString(5);
				}
			}
			return refName;
		},

		/**
		 * Empty refcon arrays before script exit
		 *
		 * @return {void}
		 */
		cleanUp: function () {
			refcon.refTemplates = [];
			refcon.templateGroups = [];
			refcon.textParts = [];
			refcon.textBoxText = [];
		},

		/**
		 * TextPart class
		 *
		 * @param {object} data for constructing the object
		 */
		TextPart: function (data) {

			/**
			 * Article text start index
			 */
			this.start = typeof data.start === 'number' ? data.start : null;

			/**
			 * Article text end index
			 */
			this.end = typeof data.end === 'number' ? data.end : null;

			/**
			 * Article text content string
			 */
			this.string = data.string ? data.string : '';

			/**
			 * Array that has indexes of reference templates that apply to this text part
			 */
			this.inTemplates = data.inTemplates ? data.inTemplates : {};

			/**
			 * Temporary holding array for reference objects
			 */
			this.references = [];

			/**
			 * Temporary holding array for citation objects
			 */
			this.citations = [];

			/**
			 * Array that hold citation objects that are linked to reflist template references
			 */
			this.linkedCitations = [];
		},


		/**
		 * Citation class
		 *
		 * @param {object} data for constructing the object
		 */

		Citation: function (data) {

			/**
			 * Citation group
			 */
			this.group = data.group ? data.group : '';

			/**
			 * Citation name
			 */
			this.name = data.name ? data.name : '';

			/**
			 * Citation pages
			 */
			this.page = data.page ? data.page : '';

			/**
			 * Citation location in the edit textbox
			 */
			this.index = data.index ? data.index : 0;

			/**
			 * Citation wikitext
			 *
			 * Example: <ref name="abc" />
			 */
			this.string = data.string ? data.string : '';

			/**
			 * Convert this citation to wikitext
			 */
			this.toString = function () {
				var useTemplateR = true;
				// check if we should use template {{R}} for shorter citation format
				useTemplateR = refcon.useConfigOption(refcon.getOption('usetemplateR'), 'usetemplateR');

				var startString = useTemplateR ? '{{⬖r' : '<ref';
				var groupString = useTemplateR ? '|g=' + this.group : ' group="' + this.group + '"';
				var nameString = useTemplateR ? '|' + this.name : ' name="' + this.name + '"';
				var pageString = useTemplateR ? '|p=' + this.page : '{{rp|' + this.page + '}}';
				var endString = useTemplateR ? '⬗}}' : ' />';

				var finalString = '';
				if (this.page || this.group) {
					finalString += '⬘';
				}
				finalString += startString;
				if (this.group) {
					finalString += groupString;
				}
				if (this.name) {
					finalString += nameString;
				}
				if (this.page) {
					if (useTemplateR) {
						finalString += pageString + endString;
					} else {
						finalString += endString + pageString;
					}
				} else {
					finalString += endString;
				}
				if (this.page || this.group) {
					finalString += '⬙';
				}

				return (finalString);
			};
		},

		/**
		 * Reference class
		 *
		 * @param {object} Data for constructing the object
		 */
		Reference: function (data) {

			/**
			 * Extend the Citation class
			 */
			refcon.Citation.call(this, data);

			/**
			 * Reference content (without the <ref> tags)
			 *
			 * Example: Second chapter of {{Cite book |first=Charles |last=Darwin |title=On the Origin of Species}}
			 */
			this.content = data.content ? data.content : '';

			/**
			 * Array that contains citations to this reference
			 */
			this.citations = [];

			/**
			 * Boolean for reference location. True (the default) means in reference list template. False means in article text
			 */
			this.inRefTemplate = typeof data.inRefTemplate !== 'undefined' ? data.inRefTemplate : true;

			/**
			 * Boolean for reference output. False (the default) means the reference has not been printed yet. True means it has been printed.
			 */
			this.wasPrinted = false;

			/**
			 * Convert this reference to wikitext (inside reference list template)
			 */
			this.toString = function () {
				var string = '<ref name="' + this.name + '">' + this.content + '</ref>';
				return string;
			};

			/**
			 * Convert this reference to wikitext (in article text)
			 */
			this.toStringText = function (named) {
				var string = '<ref';
				if (this.group)
					string += ' group="' + this.group + '"';
				if (named)
					string += ' name="' + this.name + '"';
				string += '>' + this.content + '</ref>';

				return string;
			};

			/**
			 * Change reference's name and it's citations' names
			 */
			this.changeName = function (newName) {
				this.name = newName;
				var i;
				for (i = 0; i < this.citations.length; i++) {
					this.citations[i].name = newName;
				}
			};
		},

		/**
		 * Reftemplate class
		 *
		 * @param {object} Data for constructing the object
		 */
		RefTemplate: function (data) {

			/**
			 * Template group
			 */
			this.group = data.group ? data.group : '';

			/**
			 * Template wikitext
			 *
			 */
			this.string = data.string ? data.string : '';

			/**
			 * Template start position in the edit textbox
			 */
			this.start = data.start ? data.start : 0;

			/**
			 * Template end position in the edit textbox
			 */
			this.end = data.end ? data.end : 0;

			/**
			 * Template parameters object that holds name-value pairs
			 */
			this.params = data.params ? data.params : {};

			/**
			 * Array of reference objects of this template
			 */
			this.references = [];

			/**
			 * Reference index dicts
			 */

			this.keys = {};
			this.values = {};
			this.keyValues = {};

			/**
			 * Helper dicts to keep track of duplicate reference keys, values key/values
			 */

			this.dupKeys = {};
			this.dupValues = {};
			this.dupKeyValues = {};

			/**
			 * Dict that holds citation name replacements
			 */

			this.replacements = {};

			/**
			 * Populate reference template's index dicts
			 * @param {string} reference name
			 * @param (string) reference content
			 * @param (integer) reference order number in template
			 *
			 * @return {void}
			 */
			this.createIndexes = function (key, value, ix) {

				if (key in this.keys) {
					this.keys[key].push(ix);
					this.dupKeys[key] = this.keys[key];
				} else {
					this.keys[key] = [ix];
				}

				if (value in this.values) {
					this.values[value].push(ix);
					this.dupValues[value] = this.values[value];
				} else {
					this.values[value] = [ix];
				}

				if (key + '_' + value in this.keyValues) {
					this.keyValues[key + '_' + value].push(ix);
					this.dupKeyValues[key + '_' + value] = this.keyValues[key + '_' + value];
				} else {
					this.keyValues[key + '_' + value] = [ix];
				}
			};

			/**
			 * Recreate reference list template indexes
			 *
			 * @return {void}
			 */
			this.reIndex = function () {
				var i, reference;
				this.keys = {};
				this.values = {};
				this.keyValues = {};

				for (i = 0; i < this.references.length; i++) {
					reference = this.references[i];
					if (typeof reference === 'object') {
						this.keys[reference.name] = [i];
						this.values[reference.content] = [i];
						this.keyValues[reference.name + '_' + reference.content] = [i];
					}
				}
			};

			/**
			 * Process references indexes, remove duplicate 
			 *
			 * @return {void}
			 */

			this.processDuplicates = function () {
				this.processIndex(this.dupKeyValues, this.processDupKeyValues, this);
				this.processIndex(this.dupKeys, this.processDupKeys, this);
				this.processIndex(this.dupValues, this.processDupValues, this);
			};

			this.processIndex = function (indexObj, callBack, callbackObj) {
				// returnObj and dataObj are a bit of a hack for dupValues index. We need to get back the refIndex of the first duplicate value
				// to add it into the replacements array with the duplicate values that were deleted
				var returnObj, dataObj;
				for (var key in indexObj) {
					if (indexObj.hasOwnProperty(key)) {
						indexObj[key].forEach(function (refIndex, ix) {
							returnObj = callBack.call(callbackObj, refIndex, ix, dataObj);
							if (typeof returnObj === 'object') {
								dataObj = returnObj;
							}
						});
					}
				}
			};

			this.processDupKeyValues = function (refIndex, ix, dataObj) {
				if (ix > 0) {
					var refData = this.delRef(refIndex);
					this.changeEveryIndex(refData['name'], refData['content'], refIndex);
				}
			};

			this.processDupKeys = function (refIndex, ix, dataObj) {
				if (ix > 0) {
					var refData = this.changeRefName(refIndex);
					this.changeIndex(refData['oldName'], refIndex, this.keys);
					this.addIndex(refData['newName'], refIndex, this.keys);
					this.removeIndex(refData['oldName'] + '_' + refData['content'], this.keyValues);
					this.addIndex(refData['newName'] + '_' + refData['content'], refIndex, this.keyValues);
				}
			};

			this.processDupValues = function (refIndex, ix, dataObj) {
				if (ix == 0) {
					// get TemplateReference object
					var refData = this.getRef(refIndex);
					return (refData);
				} else {
					var delrefData = this.delRef(refIndex);
					this.removeIndex(delrefData['name'], this.keys);
					this.changeIndex(delrefData['content'], refIndex, this.values);
					this.removeIndex(delrefData['name'] + '_' + delrefData['content'], this.keyValues);
					// add old and new reference name into replacements array
					this.replacements[delrefData['name']] = dataObj['name'];
				}
			};

			this.delRef = function (refIndex) {
				var name = this.references[refIndex].name;
				var content = this.references[refIndex].content;
				this.references[refIndex] = null;
				return ({
					'name': name,
					'content': content
				});
			};

			this.changeRefName = function (refIndex) {
				var oldName = this.references[refIndex].name;
				var content = this.references[refIndex].content;
				var newName = this.getNewName(this.references[refIndex]);
				this.references[refIndex].name = newName;
				return ({
					'oldName': oldName,
					'content': content,
					'newName': newName
				});
			};

			// Creates new reference name while making sure it is unique per template
			this.getNewName = function (reference) {
				var oldName = reference.name;
				var refName, idx;
				if (oldName.length == 0 || oldName.charAt(0) == ':') {
					refName = refcon.extractRefName(reference);
					idx = 1;
				} else {
					// If oldName matches "[a-z]+\-[0-9]+", break down to refName & idx
					var match = oldName.match(/([a-z]+)\-([0-9]+)/);
					refName = match ? match[1] : oldName;
					idx = match ? parseInt(match[2]) : 1;
				}
				var newName = refName + '-' + idx.toString().padStart(2, '0');

				while (newName in this.keys) {
					idx++;
					newName = refName + '-' + idx.toString().padStart(2, '0');
				}
				return (newName);
			}

			this.changeIndex = function (key, refIndex, obj) {
				var ix = obj[key].indexOf(refIndex);
				if (ix > -1)
					obj[key].splice(ix, 1);
			};

			this.addIndex = function (key, value, obj) {
				obj[key] = [];
				obj[key].push(value);
			};

			this.removeIndex = function (key, obj) {
				delete obj[key];
			};

			this.getRef = function (refIndex) {
				return this.references[refIndex];
			};

			this.addRef = function (reference) {
				var count = this.references.push(reference);
				this.createIndexes(reference['name'], reference['content'], count - 1);
			}

			this.delRef = function (refIndex) {
				var name = this.references[refIndex].name;
				var content = this.references[refIndex].content;
				this.references[refIndex] = null;
				return ({
					'name': name,
					'content': content
				});
			};

			this.changeEveryIndex = function (key, value, refIndex) {
				this.changeIndex(key, refIndex, this.keys);
				this.changeIndex(value, refIndex, this.values);
				this.changeIndex(key + '_' + value, refIndex, this.keyValues);
				// dupKeys, dupValues and dupKeyValues get changed by reference
			};
		}
	};

	$(refcon.init);

}(mw, jQuery));