User:Argenti Aertheri/RefConsolidate alt.js

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.
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></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, save sorting preferences
		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, save name keeping preferences
		if ( $('#refcon-keepnames').prop('checked') )
			refcon.userOptions['keepnames'] = true;
		else
			refcon.userOptions['keepnames'] = 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 < nextTemplateIndex; i++ ) {
			if (textPart.charAt(i) === "{" && prevChar === "{")
				++depth;
			if (textPart.charAt(i) === "}" && prevChar === "}")
				--depth;
			if (depth === 0) {
				templateEndIndex = i + 1;
				break;
			}
			prevChar = textPart.charAt(i);
		}

		// 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 );
			}
		}
	},

	/**
	 * 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,
				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 );
				}
			}

			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.name );
						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.
				// Add the reference to reflist references
				var newRef = new refcon.Reference({
					'group': reference.group,
					'name': reference.name,
					'content': reference.content,
					'inRefTemplate': false
				});
				var citation = new refcon.Citation({
					'group': reference.group,
					'name': reference.name,
					'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 ( 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();
				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 article text"...
				} 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 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;
		  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;
			}
		}

		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];
			var 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 || '0123456789';
		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;
	},

	/**
	 * 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 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 = false;
			// 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 endString = useTemplateR ? '}}' : ' />';

			return ( startString + ( this.group ? groupString : '' ) + ( this.name ? nameString : '' ) + endString );
		};
	},

	/**
	 * 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 ( oldName );
			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 ( oldName ) {
			var prefix, randomValue, newName;

			randomValue = refcon.randomString( 6 );
			prefix = typeof oldName !== 'undefined' ? oldName + '_' : ':';
			newName = prefix + randomValue;

			while ( newName in this.keys ) {
				randomValue = refcon.randomString( 6 );
				newName = prefix + randomValue;
			}
			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 );