/**

(function  { // Wrap entire file with anonymous function

var Morebits = {};
window.Morebits = Morebits; // allow global access


/**
Morebits.userIsInGroup = function (group) {
	return mw.config.get('wgUserGroups').indexOf(group) !== -1;
// Used a lot
Morebits.userIsSysop = Morebits.userIsInGroup('sysop');


/**
Morebits.sanitizeIPv6 = function (address) {
	address = address.trim;
	if (address === '') {
		return null;
	if (!mw.util.isIPv6Address(address)) {
		return address; // nothing else to do for IPv4 addresses or invalid ones
	// Remove any whitespaces, convert to upper case
	address = address.toUpperCase;
	// Expand zero abbreviations
	var abbrevPos = address.indexOf('::');
	if (abbrevPos > -1) {
		// We know this is valid IPv6. Find the last index of the
		// address before any CIDR number (e.g. "a:b:c::/24").
		var CIDRStart = address.indexOf('/');
		var addressEnd = CIDRStart > -1 ? CIDRStart - 1 : address.length - 1;
		// If the '::' is at the beginning...
		var repeat, extra, pad;
		if (abbrevPos === 0) {
			repeat = '0:';
			extra = address === '::' ? '0' : ''; // for the address '::'
			pad = 9; // 7+2 (due to '::')
		// If the '::' is at the end...
			repeat = ':0';
			extra = '';
			pad = 9; // 7+2 (due to '::')
		// If the '::' is in the middle...
			repeat = ':0';
			extra = ':';
			pad = 8; // 6+2 (due to '::')
		var replacement = repeat;
		pad -= address.split(':').length - 1;
		for (var i = 1; i < pad; i++) {
			replacement += repeat;
		replacement += extra;
		address = address.replace;
	// Remove leading zeros from each bloc as needed
	address = address.replace(/(^|:)0+/g, '$1$2');

	return address;


/**

/**
Morebits.quickForm = function QuickForm {
	this.root = new Morebits.quickForm.element;

/**
Morebits.quickForm.prototype.render = function QuickFormRender {
	var ret = this.root.render;
	ret.names = {};
	return ret;

/**
Morebits.quickForm.prototype.append = function QuickFormAppend(data) {
	return this.root.append(data);

/**
Morebits.quickForm.element = function QuickFormElement(data) {
	this.data = data;
	this.childs = [];
	this.id = Morebits.quickForm.element.id++;

Morebits.quickForm.element.id = 0;

/**
Morebits.quickForm.element.prototype.append = function QuickFormElementAppend(data) {
	var child;
	if (data instanceof Morebits.quickForm.element) {
		child = data;
		child = new Morebits.quickForm.element(data);
	this.childs.push(child);
	return child;

/**
Morebits.quickForm.element.prototype.render = function QuickFormElementRender(internal_subgroup_id) {
	var currentNode = this.compute;

	for (var i = 0; i < this.childs.length; ++i) {
		// do not pass internal_subgroup_id to recursive calls
		currentNode[1].appendChild(this.childs[i].render);
	return currentNode[0];

Morebits.quickForm.element.prototype.compute = function QuickFormElementCompute {
	var node;
	var childContainder = null;
	var label;
	var id = (in_id ? in_id + '_' : '') + 'node_' + this.id;
	if (data.adminonly && !Morebits.userIsSysop) {
		// hell hack alpha
		data.type = 'hidden';

	var i, current, subnode;
	switch (data.type) {
		case 'form':
			node = document.createElement('form');
			node.className = 'quickform';
			node.setAttribute('action', 'javascript:void(0);');
			if (data.event) {
				node.addEventListener;
			break;
		case 'fragment':
			node = document.createDocumentFragment;
			// fragments can't have any attributes, so just return it straight away
			return [ node, node ];
		case 'select':
			node = document.createElement('div');

			node.setAttribute;
			if (data.label) {
				label = node.appendChild(document.createElement('label'));
				label.setAttribute;
				label.appendChild(document.createTextNode(data.label));
			var select = node.appendChild(document.createElement('select'));
			if (data.event) {
				select.addEventListener;
			if (data.multiple) {
				select.setAttribute;
			if (data.size) {
				select.setAttribute;
			if (data.disabled) {
				select.setAttribute;
			select.setAttribute;

			if (data.list) {
				for (i = 0; i < data.list.length; ++i) {

					current = data.list[i];

					if (current.list) {
						current.type = 'optgroup';
						current.type = 'option';

					subnode = this.compute(current);
					select.appendChild(subnode[0]);
			childContainder = select;
			break;
		case 'option':
			node = document.createElement('option');
			node.values = data.value;
			node.setAttribute;
			if (data.selected) {
				node.setAttribute;
			if (data.disabled) {
				node.setAttribute;
			node.setAttribute;
			node.appendChild(document.createTextNode(data.label));
			break;
		case 'optgroup':
			node = document.createElement('optgroup');
			node.setAttribute;

			if (data.list) {
				for (i = 0; i < data.list.length; ++i) {

					current = data.list[i];
					current.type = 'option'; // must be options here

					subnode = this.compute(current);
					node.appendChild(subnode[0]);
			break;
		case 'field':
			node = document.createElement('fieldset');
			label = node.appendChild(document.createElement('legend'));
			label.appendChild(document.createTextNode(data.label));
			if (data.name) {
				node.setAttribute;
			if (data.disabled) {
				node.setAttribute;
			break;
		case 'checkbox':
		case 'radio':
			node = document.createElement('div');
			if (data.list) {
				for (i = 0; i < data.list.length; ++i) {
					var cur_id = id + '_' + i;
					current = data.list[i];
					var cur_div;
					if (current.type === 'header') {
					// inline hack
						cur_div = node.appendChild(document.createElement('h6'));
						cur_div.appendChild(document.createTextNode(current.label));
						if (current.tooltip) {
							Morebits.quickForm.element.generateTooltip;
						continue;
					cur_div = node.appendChild(document.createElement('div'));
					subnode = cur_div.appendChild(document.createElement('input'));
					subnode.values = current.value;
					subnode.setAttribute;
					subnode.setAttribute;
					subnode.setAttribute;
					subnode.setAttribute;

					// If name is provided on the individual checkbox, add a data-single
					// attribute which indicates it isn't part of a list of checkboxes with
					// same name. Used in getInputData
					if (current.name) {
						subnode.setAttribute;

					if (current.checked) {
						subnode.setAttribute;
					if (current.disabled) {
						subnode.setAttribute;
					label = cur_div.appendChild(document.createElement('label'));
					label.appendChild(document.createTextNode(current.label));
					label.setAttribute;
					if (current.tooltip) {
						Morebits.quickForm.element.generateTooltip;
					// styles go on the label, doesn't make sense to style a checkbox/radio
					if (current.style) {
						label.setAttribute;

					var event;
					if (current.subgroup) {
						var tmpgroup = current.subgroup;

						if (!Array.isArray(tmpgroup)) {
							tmpgroup = [ tmpgroup ];

						var subgroupRaw = new Morebits.quickForm.element({
							type: 'div',
							id: id + '_' + i + '_subgroup'
						$.each(tmpgroup, function {
							var newEl = $.extend;
							if (!newEl.type) {
								newEl.type = data.type;
							newEl.name = (current.name || data.name) + '.' + newEl.name;
							subgroupRaw.append(newEl);

						var subgroup = subgroupRaw.render(cur_id);
						subgroup.className = 'quickformSubgroup';
						subnode.subgroup = subgroup;
						subnode.shown = false;

						event = function(e) {
							if (e.target.checked) {
								e.target.parentNode.appendChild(e.target.subgroup);
								if (e.target.type === 'radio') {
									var name = e.target.name;
									if (e.target.form.names[name] !== undefined) {
										e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup);
									e.target.form.names[name] = e.target;
								e.target.parentNode.removeChild(e.target.subgroup);
						subnode.addEventListener;
						if (current.checked) {
							subnode.parentNode.appendChild(subgroup);
						event = function(e) {
							if (e.target.checked) {
								var name = e.target.name;
								if (e.target.form.names[name] !== undefined) {
									e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup);
								delete e.target.form.names[name];
						subnode.addEventListener;
					// add users' event last, so it can interact with the subgroup
					if (data.event) {
						subnode.addEventListener;
						subnode.addEventListener;
			if (data.shiftClickSupport && data.type === 'checkbox') {
				Morebits.checkboxShiftClickSupport(Morebits.quickForm.getElements);
			break;
		case 'input':
			node = document.createElement('div');
			node.setAttribute;

			if (data.label) {
				label = node.appendChild(document.createElement('label'));
				label.appendChild(document.createTextNode(data.label));
				label.setAttribute;

			subnode = node.appendChild(document.createElement('input'));
			if (data.value) {
				subnode.setAttribute;
			subnode.setAttribute;
			subnode.setAttribute;
			if (data.size) {
				subnode.setAttribute;
			if (data.disabled) {
				subnode.setAttribute;
			if (data.required) {
				subnode.setAttribute;
			if (data.readonly) {
				subnode.setAttribute;
			if (data.maxlength) {
				subnode.setAttribute;
			if (data.event) {
				subnode.addEventListener;
			childContainder = subnode;
			break;
		case 'dyninput':
			var min = data.min || 1;
			var max = data.max || Infinity;

			node = document.createElement('div');

			label = node.appendChild(document.createElement('h5'));
			label.appendChild(document.createTextNode(data.label));

			var listNode = node.appendChild(document.createElement('div'));

			var more = this.compute({
				type: 'button',
				label: '더 보기',
				disabled: min >= max,
				event: function(e) {
					var new_node = new Morebits.quickForm.element(e.target.sublist);
					e.target.area.appendChild(new_node.render);

					if (++e.target.counter >= e.target.max) {
						e.target.setAttribute;
					e.stopPropagation;

			node.appendChild(more[0]);
			var moreButton = more[1];

			var sublist = {
				type: '_dyninput_element',
				label: data.sublabel || data.label,
				name: data.name,
				value: data.value,
				size: data.size,
				remove: false,
				maxlength: data.maxlength,
				event: data.event

			for (i = 0; i < min; ++i) {
				var elem = new Morebits.quickForm.element(sublist);
				listNode.appendChild(elem.render);
			sublist.remove = true;
			sublist.morebutton = moreButton;
			sublist.listnode = listNode;

			moreButton.sublist = sublist;
			moreButton.area = listNode;
			moreButton.max = max - min;
			moreButton.counter = 0;
			break;
		case '_dyninput_element': // Private, similar to normal input
			node = document.createElement('div');

			if (data.label) {
				label = node.appendChild(document.createElement('label'));
				label.appendChild(document.createTextNode(data.label));
				label.setAttribute;

			subnode = node.appendChild(document.createElement('input'));
			if (data.value) {
				subnode.setAttribute;
			subnode.setAttribute;
			subnode.setAttribute;
			if (data.size) {
				subnode.setAttribute;
			if (data.maxlength) {
				subnode.setAttribute;
			if (data.event) {
				subnode.addEventListener;
			if (data.remove) {
				var remove = this.compute({
					type: 'button',
					label: '제거',
					event: function(e) {
						var list = e.target.listnode;
						var node = e.target.inputnode;
						var more = e.target.morebutton;

						list.removeChild(node);
						--more.counter;
						more.removeAttribute('disabled');
						e.stopPropagation;
				node.appendChild(remove[0]);
				var removeButton = remove[1];
				removeButton.inputnode = node;
				removeButton.listnode = data.listnode;
				removeButton.morebutton = data.morebutton;
			break;
		case 'hidden':
			node = document.createElement('input');
			node.setAttribute;
			node.values = data.value;
			node.setAttribute;
			node.setAttribute;
			break;
		case 'header':
			node = document.createElement('h5');
			node.appendChild(document.createTextNode(data.label));
			break;
		case 'div':
			node = document.createElement('div');
			if (data.name) {
				node.setAttribute;
			if (data.label) {
				if (!Array.isArray(data.label)) {
					data.label = [ data.label ];
				var result = document.createElement('span');
				result.className = 'quickformDescription';
				for (i = 0; i < data.label.length; ++i) {
					if (typeof data.label[i] === 'string') {
						result.appendChild(document.createTextNode(data.label[i]));
						result.appendChild(data.label[i]);
				node.appendChild(result);
			break;
		case 'submit':
			node = document.createElement('span');
			childContainder = node.appendChild(document.createElement('input'));
			childContainder.setAttribute;
			if (data.label) {
				childContainder.setAttribute;
			childContainder.setAttribute;
			if (data.disabled) {
				childContainder.setAttribute;
			break;
		case 'button':
			node = document.createElement('span');
			childContainder = node.appendChild(document.createElement('input'));
			childContainder.setAttribute;
			if (data.label) {
				childContainder.setAttribute;
			childContainder.setAttribute;
			if (data.disabled) {
				childContainder.setAttribute;
			if (data.event) {
				childContainder.addEventListener;
			break;
		case 'textarea':
			node = document.createElement('div');
			node.setAttribute;
			if (data.label) {
				label = node.appendChild(document.createElement('h5'));
				var labelElement = document.createElement('label');
				labelElement.textContent = data.label;
				labelElement.setAttribute;
				label.appendChild(labelElement);
			subnode = node.appendChild(document.createElement('textarea'));
			subnode.setAttribute;
			if (data.cols) {
				subnode.setAttribute;
			if (data.rows) {
				subnode.setAttribute;
			if (data.disabled) {
				subnode.setAttribute;
			if (data.required) {
				subnode.setAttribute;
			if (data.readonly) {
				subnode.setAttribute;
			if (data.value) {
				subnode.value = data.value;
			childContainder = subnode;
			break;
		default:
			throw new Error('Morebits.quickForm: 알 수 없는 요소 타입 ' + data.type.toString);

	if (!childContainder) {
		childContainder = node;
	if (data.tooltip) {
		Morebits.quickForm.element.generateTooltip;

	if (data.extra) {
		childContainder.extra = data.extra;
	if (data.style) {
		childContainder.setAttribute;
	if (data.className) {
		childContainder.className = childContainder.className ?
			childContainder.className + ' ' + data.className :
			data.className;
	childContainder.setAttribute;

	return [ node, childContainder ];

Morebits.quickForm.element.autoNWSW = function {
	return $(this).offset.top > ($(document).scrollTop + ($(window).height / 2)) ? 'sw' : 'nw';

/**
Morebits.quickForm.element.generateTooltip = function QuickFormElementGenerateTooltip {
	var tooltipButton = node.appendChild(document.createElement('span'));
	tooltipButton.className = 'morebits-tooltipButton';
	tooltipButton.title = data.tooltip; // Provides the content for jQuery UI
	tooltipButton.appendChild(document.createTextNode('?'));
	$(tooltipButton).tooltip({
		position: { my: 'left top', at: 'center bottom', collision: 'flipfit' },
		// Deprecated in UI 1.12, but MW stuck on 1.9.2 indefinitely; see #398 and T71386
		tooltipClass: 'morebits-ui-tooltip'

// Some utility methods for manipulating quickForms after their creation:
// (None of these work for "dyninput" type fields at present)

/**
Morebits.quickForm.getInputData = function(form) {
	var result = {};

	for (var i = 0; i < form.elements.length; i++) {
		var field = form.elements[i];
		if (field.disabled || !field.name || !field.type ||
			field.type === 'submit' || field.type === 'button') {
			continue;

		// For elements in subgroups, quickform prepends element names with
		// name of the parent group followed by a period, get rid of that.
		var fieldNameNorm = field.name.slice(field.name.indexOf('.') + 1);

		switch (field.type) {
			case 'radio':
				if (field.checked) {
					result[fieldNameNorm] = field.value;
				break;
			case 'checkbox':
				if (field.dataset.single) {
					result[fieldNameNorm] = field.checked; // boolean
					result[fieldNameNorm] = result[fieldNameNorm] || [];
					if (field.checked) {
						result[fieldNameNorm].push(field.value);
				break;
			case 'select-multiple':
				result[fieldNameNorm] = $(field).val; // field.value doesn't work
				break;
			case 'text': // falls through
			case 'textarea':
				result[fieldNameNorm] = field.value.trim;
				break;
			default: // could be select-one, date, number, email, etc
				if (field.value) {
					result[fieldNameNorm] = field.value;
				break;
	return result;

/**
Morebits.quickForm.getElements = function QuickFormGetElements {
	var $form = $(form);
	fieldName = $.escapeSelector(fieldName); // sanitize input
	var $elements = $form.find('[name="' + fieldName + '"]');
	if ($elements.length > 0) {
		return $elements.toArray;
	$elements = $form.find('#' + fieldName);
	return $elements.toArray;

/**
Morebits.quickForm.getCheckboxOrRadio = function QuickFormGetCheckboxOrRadio {
	var found = $.grep(elementArray, function(el) {
		return el.value === value;
	if (found.length > 0) {
		return found[0];
	return null;

/**
Morebits.quickForm.getElementContainer = function QuickFormGetElementContainer(element) {
	// for divs, headings and fieldsets, the container is the element itself
	if (element instanceof HTMLFieldSetElement || element instanceof HTMLDivElement ||
			element instanceof HTMLHeadingElement) {
		return element;

	// for others, just return the parent node
	return element.parentNode;

/**
Morebits.quickForm.getElementLabelObject = function QuickFormGetElementLabelObject(element) {
	// for buttons, divs and headers, the label is on the element itself
	if (element.type === 'button' || element.type === 'submit' ||
			element instanceof HTMLDivElement || element instanceof HTMLHeadingElement) {
		return element;
	// for fieldsets, the label is the child  element
		return element.getElementsByTagName('legend')[0];
	// for textareas, the label is the sibling  element
		return element.parentNode.getElementsByTagName('h5')[0];
	// for others, the label is the sibling  element
	return element.parentNode.getElementsByTagName('label')[0];

/**
Morebits.quickForm.getElementLabel = function QuickFormGetElementLabel(element) {
	var labelElement = Morebits.quickForm.getElementLabelObject(element);

	if (!labelElement) {
		return null;
	return labelElement.firstChild.textContent;

/**
Morebits.quickForm.setElementLabel = function QuickFormSetElementLabel {
	var labelElement = Morebits.quickForm.getElementLabelObject(element);

	if (!labelElement) {
		return false;
	labelElement.firstChild.textContent = labelText;
	return true;

/**
Morebits.quickForm.overrideElementLabel = function QuickFormOverrideElementLabel {
	if (!element.hasAttribute('data-oldlabel')) {
		element.setAttribute('data-oldlabel', Morebits.quickForm.getElementLabel(element));
	return Morebits.quickForm.setElementLabel;

/**
Morebits.quickForm.resetElementLabel = function QuickFormResetElementLabel(element) {
	if (element.hasAttribute('data-oldlabel')) {
		return Morebits.quickForm.setElementLabel(element, element.getAttribute('data-oldlabel'));
	return null;

/**
Morebits.quickForm.setElementVisibility = function QuickFormSetElementVisibility {
	$(element).toggle(visibility);

/**
Morebits.quickForm.setElementTooltipVisibility = function QuickFormSetElementTooltipVisibility {
	$(Morebits.quickForm.getElementContainer(element)).find('.morebits-tooltipButton').toggle(visibility);


/**

/**
HTMLFormElement.prototype.getChecked = function {
	var elements = this.elements[name];
	if (!elements) {
		return [];
	var return_array = [];
	var i;
	if (elements instanceof HTMLSelectElement) {
		var options = elements.options;
		for (i = 0; i < options.length; ++i) {
			if (options[i].selected) {
				if (options[i].values) {
					return_array.push(options[i].values);
					return_array.push(options[i].value);

		if (type && elements.type !== type) {
			return [];
			return [ elements.value ];
		for (i = 0; i < elements.length; ++i) {
			if (elements[i].checked) {
				if (type && elements[i].type !== type) {
					continue;
				if (elements[i].values) {
					return_array.push(elements[i].values);
					return_array.push(elements[i].value);
	return return_array;

/**

HTMLFormElement.prototype.getUnchecked = function {
	var elements = this.elements[name];
	if (!elements) {
		return [];
	var return_array = [];
	var i;
	if (elements instanceof HTMLSelectElement) {
		var options = elements.options;
		for (i = 0; i < options.length; ++i) {
			if (!options[i].selected) {
				if (options[i].values) {
					return_array.push(options[i].values);
					return_array.push(options[i].value);

		if (type && elements.type !== type) {
			return [];
			return [ elements.value ];
		for (i = 0; i < elements.length; ++i) {
			if (!elements[i].checked) {
				if (type && elements[i].type !== type) {
					continue;
				if (elements[i].values) {
					return_array.push(elements[i].values);
					return_array.push(elements[i].value);
	return return_array;

/**
RegExp.escape = function {
	text = mw.util.escapeRegExp(text);

	// Special MediaWiki escape - underscore/space are often equivalent
	if (space_fix) {
		text = text.replace;

	return text;

/**

Morebits.string = {
	// Helper functions to change case of a string
	toUpperCaseFirstChar: function(str) {
		str = str.toString;
		return str.substr.toUpperCase + str.substr(1);
	toLowerCaseFirstChar: function(str) {
		str = str.toString;
		return str.substr.toLowerCase + str.substr(1);

	/**
	splitWeightedByKeys: function {
		if (start.length !== end.length) {
			throw new Error('시작 마커와 끝 마커는 길이가 같아야 합니다');
		var level = 0;
		var initial = null;
		var result = [];
		if (!Array.isArray(skiplist)) {
			if (skiplist === undefined) {
				skiplist = [];
				skiplist = [ skiplist ];
				throw new Error('적용할 수 없는 skiplist 변수');
		for (var i = 0; i < str.length; ++i) {
			for (var j = 0; j < skiplist.length; ++j) {
				if (str.substr === skiplist[j]) {
					i += skiplist[j].length - 1;
					continue;
			if (str.substr === start) {
				if (initial === null) {
					initial = i;
				++level;
				i += start.length - 1;
				--level;
				i += end.length - 1;
			if (!level && initial !== null) {
				result.push(str.substring);
				initial = null;

		return result;

	/**
	formatReasonText: function(str) {
		var result = str.toString.trim;
		var unbinder = new Morebits.unbinder(result);
		unbinder.unbind('');
		unbinder.content = unbinder.content.replace;
		return unbinder.rebind;

	/**
	safeReplace: function morebitsStringSafeReplace {
		return string.replace(pattern, replacement.replace);

	/**
	isInfinity: function morebitsStringIsInfinity(expiry) {
		return ['indefinite', 'infinity', 'infinite', 'never'].indexOf(expiry) !== -1;

/**

Morebits.array = {
	/**
	uniq: function(arr) {
		if (!Array.isArray(arr)) {
			throw 'A non-array object passed to Morebits.array.uniq';
		return arr.filter(function {
			return arr.indexOf(item) === idx;

	/**
	dups: function(arr) {
		if (!Array.isArray(arr)) {
			throw 'A non-array object passed to Morebits.array.dups';
		return arr.filter(function {
			return arr.indexOf(item) !== idx;

	/**
	chunk: function {
		if (!Array.isArray(arr)) {
			throw 'A non-array object passed to Morebits.array.chunk';
		if (typeof size !== 'number' || size <= 0) { // pretty impossible to do anything :)
			return [ arr ]; // we return an array consisting of this array.
		var numChunks = Math.ceil(arr.length / size);
		var result = new Array(numChunks);
		for (var i = 0; i < numChunks; i++) {
			result[i] = arr.slice(i * size, (i + 1) * size);
		return result;

/**
Morebits.select2 = {

	matchers: {
		/**
		optgroupFull: function {
			var originalMatcher = $.fn.select2.defaults.defaults.matcher;
			var result = originalMatcher;

			if (result && params.term &&
				data.text.toUpperCase.indexOf(params.term.toUpperCase) !== -1) {
				result.children = data.children;
			return result;

		/** Custom matcher that matches from the beginning of words only */
		wordBeginning: function {
			var originalMatcher = $.fn.select2.defaults.defaults.matcher;
			var result = originalMatcher;
			if (!params.term || (result &&
				new RegExp('\\b' + mw.util.escapeRegExp(params.term), 'i').test(result.text))) {
				return result;
			return null;

	/** Underline matched part of options */
	highlightSearchMatches: function(data) {
		var searchTerm = Morebits.select2SearchQuery;
		if (!searchTerm || data.loading) {
			return data.text;
		var idx = data.text.toUpperCase.indexOf(searchTerm.toUpperCase);
		if (idx < 0) {
			return data.text;

		return $('').append(
			data.text.slice,
			$('').css.text(data.text.slice),
			data.text.slice(idx + searchTerm.length)
		);

	/** Intercept query as it is happening, for use in highlightSearchMatches */
	queryInterceptor: function(params) {
		Morebits.select2SearchQuery = params && params.term;

	/**
	autoStart: function(ev) {
		if (ev.which < 48) {
			return;
		var target = $(ev.target).closest('.select2-container');
		if (!target.length) {
			return;
		target = target.prev;
		target.select2('open');
		var search = target.data('select2').dropdown.$search ||
			target.data('select2').selection.$search;
		search.focus;


/**
Morebits.pageNameNorm = mw.config.get('wgPageName').replace;

/**
Morebits.pageNameRegex = function(pageName) {
	return '[' + pageName[0].toUpperCase + pageName[0].toLowerCase + ']' + pageName.slice(1);

/**

/**
Morebits.unbinder = function Unbinder(string) {
	if (typeof string !== 'string') {
		throw new Error('문자열이 아닙니다');
	this.content = string;
	this.counter = 0;
	this.history = {};
	this.prefix = '%UNIQ::' + Math.random + '::';
	this.postfix = '::UNIQ%';

Morebits.unbinder.prototype = {
	/**
	unbind: function UnbinderUnbind {
		var re = new RegExp(prefix + '([\\s\\S]*?)' + postfix, 'g');
		this.content = this.content.replace(re, Morebits.unbinder.getCallback(this));

	/** @returns {string} The output */
	rebind: function UnbinderRebind {
		var content = this.content;
		content.self = this;
		for (var current in this.history) {
			if (Object.prototype.hasOwnProperty.call) {
				content = content.replace;
		return content;
	prefix: null, // %UNIQ::0.5955981644938324::
	postfix: null, // ::UNIQ%
	content: null, // string
	counter: null, // 0++
	history: null // {}

Morebits.unbinder.getCallback = function UnbinderGetCallback(self) {
	return function UnbinderCallback(match) {
		var current = self.prefix + self.counter + self.postfix;
		self.history[current] = match;
		++self.counter;
		return current;


/**

/**
Morebits.date = function {
	var args = Array.prototype.slice.call(arguments);

	if (typeof args[0] === 'string') {
		// Attempt to remove a comma and paren-wrapped timezone, to get MediaWiki timestamps to parse
		// Firefox (at least in 75) seems to be okay with the comma, though
		args[0] = args[0].replace(/(\d\d:\d\d),/, '$1').replace(/\(UTC\)/, 'UTC');
	this._d = new (Function.prototype.bind.apply(Date, [Date].concat(args)));

Morebits.date.localeData = {
	months: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
	monthsShort: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
	days: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'],
	daysShort: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'],
	relativeTimes: {
		thisDay: '[오늘] A h:mm',
		prevDay: '[어제] A h:mm',
		nextDay: '[내일] A h:mm',
		thisWeek: 'dddd A h:mm',
		pastWeek: '[지난주] dddd A h:mm',
		other: 'YYYY-MM-DD'

// Allow native Date.prototype methods to be used on Morebits.date objects
Object.getOwnPropertyNames(Date.prototype).forEach(function(func) {
	Morebits.date.prototype[func] = function {
		return this._d[func].apply(this._d, Array.prototype.slice.call(arguments));

$.extend(Morebits.date.prototype, {

	isValid: function {
		return !isNaN(this.getTime);

	/** @param {(Date|Morebits.date)} date */
	isBefore: function(date) {
		return this.getTime < date.getTime;
	isAfter: function(date) {
		return this.getTime > date.getTime;

	/** @return {string} */
	getUTCMonthName: function {
		return Morebits.date.localeData.months[this.getUTCMonth];
	getUTCMonthNameAbbrev: function {
		return Morebits.date.localeData.monthsShort[this.getUTCMonth];
	getMonthName: function {
		return Morebits.date.localeData.months[this.getMonth];
	getMonthNameAbbrev: function {
		return Morebits.date.localeData.monthsShort[this.getMonth];
	getUTCDayName: function {
		return Morebits.date.localeData.days[this.getUTCDay];
	getUTCDayNameAbbrev: function {
		return Morebits.date.localeData.daysShort[this.getUTCDay];
	getDayName: function {
		return Morebits.date.localeData.days[this.getDay];
	getDayNameAbbrev: function {
		return Morebits.date.localeData.daysShort[this.getDay];

	/**
	add: function {
		// mapping time units with getter/setter function names
		var unitMap = {
			seconds: 'Seconds',
			minutes: 'Minutes',
			hours: 'Hours',
			days: 'Date',
			months: 'Month',
			years: 'FullYear'
		var unitNorm = unitMap[unit] || unitMap[unit + 's']; // so that both singular and plural forms work
		if (unitNorm) {
			this['set' + unitNorm](this['get' + unitNorm] + number);
			return this;
		throw new Error('유효하지 않은 단위 "' + unit + '": 오직 ' + Object.keys(unitMap).join + '만 허용됩니다.');

	/**
	subtract: function {
		return this.add;

	/**
	format: function {
		var udate = this;
		// create a new date object that will contain the date to display as system time
		if (zone === 'utc') {
			udate = new Morebits.date(this.getTime).add;
			// convert to utc, then add the utc offset given
			udate = new Morebits.date(this.getTime).add;

		var pad = function(num) {
			return num < 10 ? '0' + num : num;
		var h24 = udate.getHours, m = udate.getMinutes, s = udate.getSeconds;
		var D = udate.getDate, M = udate.getMonth + 1, Y = udate.getFullYear;
		var h12 = h24 % 12 || 12, amOrPm = h24 >= 12 ? '오후' : '오전';
		var replacementMap = {
			'HH': pad(h24), 'H': h24, 'hh': pad(h12), 'h': h12, 'A': amOrPm,
			'mm': pad(m), 'm': m,
			'ss': pad(s), 's': s,
			'dddd': udate.getDayName, 'ddd': udate.getDayNameAbbrev, 'd': udate.getDay,
			'DD': pad(D), 'D': D,
			'MMMM': udate.getMonthName, 'MMM': udate.getMonthNameAbbrev, 'MM': pad(M), 'M': M,
			'YYYY': Y, 'YY': pad(Y % 100), 'Y': Y

		var unbinder = new Morebits.unbinder(formatstr); // escape stuff between [...]
		unbinder.unbind;
		unbinder.content = unbinder.content.replace(
			/* Regex notes:
			/H{1,2}|h{1,2}|m{1,2}|s{1,2}|d?|D{1,2}|M{1,4}|Y{1,2}(Y{2})?|A/g,
			function(match) {
				return replacementMap[match];
		);
		return unbinder.rebind.replace(/\[(.*?)\]/g, '$1');

	/**
	calendar: function(zone) {
		// Zero out the hours, minutes, seconds and milliseconds - keeping only the date;
		// find the difference. Note that setHours returns the same thing as getTime.
		var dateDiff = (new Date.setHours -
			new Date(this).setHours) / 8.64e7;
		switch (true) {
			case dateDiff === 0:
				return this.format;
			case dateDiff === 1:
				return this.format;
			case dateDiff > 0 && dateDiff < 7:
				return this.format;
			case dateDiff === -1:
				return this.format;
			case dateDiff  -7:
				return this.format;
			default:
				return this.format;

	/**
	monthHeaderRegex: function {
		return new RegExp;

	/**
	monthHeader: function(level) {
		level = level || 2;
		var header = Array(level + 1).join('='); // String.prototype.repeat not supported in IE 11
		return header + ' ' + this.getUTCFullYear + '년 ' + this.getUTCMonthName + ' ' + header;


/**
Morebits.wiki = {};

/**
Morebits.wiki.isPageRedirect = function wikipediaIsPageRedirect {
	return !!(mw.config.get('wgIsRedirect') || document.getElementById('softredirect'));


/**

Morebits.wiki.numberOfActionsLeft = 0;
Morebits.wiki.nbrOfCheckpointsLeft = 0;

Morebits.wiki.actionCompleted = function(self) {
	if (--Morebits.wiki.numberOfActionsLeft <= 0 && Morebits.wiki.nbrOfCheckpointsLeft <= 0) {
		Morebits.wiki.actionCompleted.event(self);

// Change per action wanted
Morebits.wiki.actionCompleted.event = function {
	if (Morebits.wiki.actionCompleted.notice) {
		Morebits.status.actionCompleted(Morebits.wiki.actionCompleted.notice);
	if (Morebits.wiki.actionCompleted.redirect) {
		// if it isn't a URL, make it one. TODO: This breaks on the articles 'http://', 'ftp://', and similar ones.
		if (!(/^\w+:\/\//).test(Morebits.wiki.actionCompleted.redirect)) {
			Morebits.wiki.actionCompleted.redirect = mw.util.getUrl(Morebits.wiki.actionCompleted.redirect);
			if (Morebits.wiki.actionCompleted.followRedirect === false) {
				Morebits.wiki.actionCompleted.redirect += '?redirect=no';
		window.setTimeout(function {
			window.location = Morebits.wiki.actionCompleted.redirect;

Morebits.wiki.actionCompleted.timeOut = typeof window.wpActionCompletedTimeOut === 'undefined' ? 5000 : window.wpActionCompletedTimeOut;
Morebits.wiki.actionCompleted.redirect = null;
Morebits.wiki.actionCompleted.notice = null;

Morebits.wiki.addCheckpoint = function {
	++Morebits.wiki.nbrOfCheckpointsLeft;

Morebits.wiki.removeCheckpoint = function {
	if (--Morebits.wiki.nbrOfCheckpointsLeft <= 0 && Morebits.wiki.numberOfActionsLeft <= 0) {
		Morebits.wiki.actionCompleted.event;

/**

/**
Morebits.wiki.api = function {
	this.currentAction = currentAction;
	this.query = query;
	this.query.assert = 'user';
	this.onSuccess = onSuccess;
	this.onError = onError;
	if (statusElement) {
		this.setStatusElement(statusElement);
		this.statelem = new Morebits.status(currentAction);
	if (!query.format) {
		this.query.format = 'xml';
		this.query.formatversion = '2';
		this.statelem.error('Invalid API format: only xml and json are supported.');

Morebits.wiki.api.prototype = {
	currentAction: '',
	onSuccess: null,
	onError: null,
	parent: window, // use global context if there is no parent object
	query: null,
	response: null,
	responseXML: null, // use `response` instead; retained for backwards compatibility
	statelem: null, // this non-standard name kept for backwards compatibility
	statusText: null, // result received from the API, normally "success" or "error"
	errorCode: null, // short text error code, if any, as documented in the MediaWiki API
	errorText: null, // full error description, if any
	badtokenRetry: false, // set to true if this on a retry attempted after a badtoken error

	/**
	setParent: function(parent) {
		this.parent = parent;

	/** @param {Morebits.status} statusElement */
	setStatusElement: function(statusElement) {
		this.statelem = statusElement;
		this.statelem.status(this.currentAction);

	/**
	post: function(callerAjaxParameters) {

		++Morebits.wiki.numberOfActionsLeft;

		var queryString = $.map(this.query, function {
			if (Array.isArray(val)) {
				return encodeURIComponent(i) + '=' + val.map(encodeURIComponent).join('|');
				return encodeURIComponent(i) + '=' + encodeURIComponent(val);
		// token should always be the last item in the query string (bug TW-B-0013)

		var ajaxparams = $.extend({}, {
			context: this,
			type: this.query.action === 'query' ? 'GET' : 'POST',
			url: mw.util.wikiScript('api'),
			data: queryString,
			dataType: this.query.format,
			headers: {
				'Api-User-Agent': morebitsWikiApiUserAgent

		return $.ajax(ajaxparams).then(

			function onAPIsuccess {
				this.statusText = statusText;
				this.response = this.responseXML = response;
				if (this.query.format === 'json') {
					this.errorCode = response.error && response.error.code;
					this.errorText = response.error && response.error.info;
					this.errorCode = $(response).find('error').attr('code');
					this.errorText = $(response).find('error').attr('info');

				if (typeof this.errorCode === 'string') {
					// the API didn't like what we told it, e.g., bad edit token or an error creating a page
					return this.returnError(callerAjaxParameters);

				// invoke success callback if one was supplied
				if (this.onSuccess) {
					// set the callback context to this.parent for new code and supply the API object
					// as the first argument to the callback (for legacy code)
					this.onSuccess.call;
					this.statelem.info('done');

				Morebits.wiki.actionCompleted;

				return $.Deferred.resolveWith;

			// only network and server errors reach here - complaints from the API itself are caught in success
			function onAPIfailure {
				this.statusText = statusText;
				this.errorThrown = errorThrown; // frequently undefined
				this.errorText = statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.';
				return this.returnError;

		);

	returnError: function(callerAjaxParameters) {
		if (this.errorCode === 'badtoken' && !this.badtokenRetry) {
			this.statelem.warn('Invalid token. Getting a new token and retrying...');
			this.badtokenRetry = true;
			// Get a new CSRF token and retry. If the original action needs a different
			// type of action than CSRF, we do one pointless retry before bailing out
			return Morebits.wiki.api.getToken.then(function(token) {
				this.query.token = token;
				return this.post(callerAjaxParameters);

		this.statelem.error(this.errorText);

		// invoke failure callback if one was supplied
		if (this.onError) {

			// set the callback context to this.parent for new code and supply the API object
			// as the first argument to the callback for legacy code
			this.onError.call;
		// don't complete the action so that the error remains displayed

		return $.Deferred.rejectWith;

	getStatusElement: function {
		return this.statelem;

	getErrorCode: function {
		return this.errorCode;

	getErrorText: function {
		return this.errorText;

	getXML: function { // retained for backwards compatibility, use getResponse instead
		return this.responseXML;

	getResponse: function {
		return this.response;

// Custom user agent header, used by WMF for server-side logging
// See https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html
var morebitsWikiApiUserAgent = 'morebits.js (w:WT:TW)';

/**
Morebits.wiki.api.setApiUserAgent = function(ua) {
	morebitsWikiApiUserAgent = (ua ? ua + ' ' : '') + 'morebits.js (w:WT:TW)';

/** Get a new CSRF token on encountering token errors */
Morebits.wiki.api.getToken = function {
	var tokenApi = new Morebits.wiki.api('Getting token', {
		action: 'query',
		meta: 'tokens',
		type: 'csrf'
	return tokenApi.post.then(function(apiobj) {
		return $(apiobj.responseXML).find('tokens').attr('csrftoken');

/**

/**

/**
Morebits.wiki.page = function {

	if (!currentAction) {
		currentAction = 'Opening page "' + pageName + '"';

	/**
	var ctx = {
		// backing fields for public properties
		pageName: pageName,
		pageExists: false,
		editSummary: null,
		callbackParameters: null,
		statusElement: new Morebits.status(currentAction),

		// - edit
		pageText: null,
		editMode: 'all', // save replaces entire contents of the page by default
		appendText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
		prependText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
		createOption: null,
		minorEdit: false,
		botEdit: false,
		pageSection: null,
		maxConflictRetries: 2,
		maxRetries: 2,
		followRedirect: false,
		followCrossNsRedirect: true,
		watchlistOption: 'nochange',
		creator: null,
		timestamp: null,

		// - revert
		revertOldID: null,

		// - move
		moveDestination: null,
		moveTalkPage: false,
		moveSubpages: false,
		moveSuppressRedirect: false,

		// - protect
		protectEdit: null,
		protectMove: null,
		protectCreate: null,
		protectCascade: false,

		// - creation lookup
		lookupNonRedirectCreator: false,

		// - stabilize (FlaggedRevs)
		flaggedRevs: null,

		// internal status
		pageLoaded: false,
		csrfToken: null,
		loadTime: null,
		lastEditTime: null,
		pageID: null,
		revertCurID: null,
		revertUser: null,
		fullyProtected: false,
		suppressProtectWarning: false,
		conflictRetries: 0,
		retries: 0,

		// callbacks
		onLoadSuccess: null,
		onLoadFailure: null,
		onSaveSuccess: null,
		onSaveFailure: null,
		onLookupCreationSuccess: null,
		onMoveSuccess: null,
		onMoveFailure: null,
		onDeleteSuccess: null,
		onDeleteFailure: null,
		onUndeleteSuccess: null,
		onUndeleteFailure: null,
		onProtectSuccess: null,
		onProtectFailure: null,
		onStabilizeSuccess: null,
		onStabilizeFailure: null,

		// internal objects
		loadQuery: null,
		loadApi: null,
		saveApi: null,
		lookupCreationApi: null,
		moveApi: null,
		moveProcessApi: null,
		patrolApi: null,
		patrolProcessApi: null,
		triageApi: null,
		triageProcessApi: null,
		deleteApi: null,
		deleteProcessApi: null,
		undeleteApi: null,
		undeleteProcessApi: null,
		protectApi: null,
		protectProcessApi: null,
		stabilizeApi: null,
		stabilizeProcessApi: null

	var emptyFunction = function { };

	/**
	this.load = function {
		ctx.onLoadSuccess = onSuccess;
		ctx.onLoadFailure = onFailure || emptyFunction;

		// Need to be able to do something after the page loads
		if (!onSuccess) {
			ctx.statusElement.error('Internal error: no onSuccess callback provided to load!');
			ctx.onLoadFailure(this);
			return;

		ctx.loadQuery = {
			action: 'query',
			prop: 'info|revisions',
			curtimestamp: '',
			meta: 'tokens',
			type: 'csrf',
			titles: ctx.pageName
			// don't need rvlimit=1 because we don't need rvstartid here and only one actual rev is returned by default

		if (ctx.editMode === 'all') {
			ctx.loadQuery.rvprop = 'content|timestamp'; // get the page content at the same time, if needed
			ctx.loadQuery.rvprop = 'timestamp';
			ctx.loadQuery.rvlimit = 1;
			ctx.loadQuery.rvstartid = ctx.revertOldID;

		if (ctx.followRedirect) {
			ctx.loadQuery.redirects = ''; // follow all redirects
		if (typeof ctx.pageSection === 'number') {
			ctx.loadQuery.rvsection = ctx.pageSection;
		if (Morebits.userIsSysop) {
			ctx.loadQuery.inprop = 'protection';

		ctx.loadApi = new Morebits.wiki.api;
		ctx.loadApi.setParent(this);
		ctx.loadApi.post;

	/**
	this.save = function {
		ctx.onSaveSuccess = onSuccess;
		ctx.onSaveFailure = onFailure || emptyFunction;

		// are we getting our editing token from mw.user.tokens?
		var canUseMwUserToken = fnCanUseMwUserToken('edit');

		if (!ctx.pageLoaded && !canUseMwUserToken) {
			ctx.statusElement.error('Internal error: attempt to save a page that has not been loaded!');
			ctx.onSaveFailure(this);
			return;
		if (!ctx.editSummary) {
			ctx.statusElement.error('Internal error: edit summary not set before save!');
			ctx.onSaveFailure(this);
			return;

		// shouldn't happen if canUseMwUserToken === true
		if (ctx.fullyProtected && !ctx.suppressProtectWarning &&
			(ctx.fullyProtected === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.date(ctx.fullyProtected).calendar('utc') + ' (UTC))') +
			'. \n\nClick OK to proceed with the edit, or Cancel to skip this edit.')) {
			ctx.statusElement.error('Edit to fully protected page was aborted.');
			ctx.onSaveFailure(this);
			return;

		ctx.retries = 0;

		var query = {
			action: 'edit',
			title: ctx.pageName,
			summary: ctx.editSummary,
			token: canUseMwUserToken ? mw.user.tokens.get('csrfToken') : ctx.csrfToken,
			watchlist: ctx.watchlistOption

		if (typeof ctx.pageSection === 'number') {
			query.section = ctx.pageSection;

		// Set minor edit attribute. If these parameters are present with any value, it is interpreted as true
		if (ctx.minorEdit) {
			query.minor = true;
			query.notminor = true; // force Twinkle config to override user preference setting for "all edits are minor"

		// Set bot edit attribute. If this paramter is present with any value, it is interpreted as true
		if (ctx.botEdit) {
			query.bot = true;

		switch (ctx.editMode) {
			case 'append':
				if (ctx.appendText === null) {
					ctx.statusElement.error('Internal error: append text not set before save!');
					ctx.onSaveFailure(this);
					return;
				query.appendtext = ctx.appendText; // use mode to append to current page contents
				break;
			case 'prepend':
				if (ctx.prependText === null) {
					ctx.statusElement.error('Internal error: prepend text not set before save!');
					ctx.onSaveFailure(this);
					return;
				query.prependtext = ctx.prependText; // use mode to prepend to current page contents
				break;
			case 'revert':
				query.undo = ctx.revertCurID;
				query.undoafter = ctx.revertOldID;
				if (ctx.lastEditTime) {
					query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
				query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
				break;
			default: // 'all'
				query.text = ctx.pageText; // replace entire contents of the page
				if (ctx.lastEditTime) {
					query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
				query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
				break;

		if (['recreate', 'createonly', 'nocreate'].indexOf(ctx.createOption) !== -1) {
			query[ctx.createOption] = '';

		if (canUseMwUserToken && ctx.followRedirect) {
			query.redirect = true;

		ctx.saveApi = new Morebits.wiki.api;
		ctx.saveApi.setParent(this);
		ctx.saveApi.post;

	/**
	this.append = function {
		ctx.editMode = 'append';

		if (fnCanUseMwUserToken('edit')) {
			this.save;
			ctx.onSaveSuccess = onSuccess;
			ctx.onSaveFailure = onFailure || emptyFunction;
			this.load;

	/**
	this.prepend = function {
		ctx.editMode = 'prepend';

		if (fnCanUseMwUserToken('edit')) {
			this.save;
			ctx.onSaveSuccess = onSuccess;
			ctx.onSaveFailure = onFailure || emptyFunction;
			this.load;

	/** @returns {string} string containing the name of the loaded page, including the namespace */
	this.getPageName = function {
		return ctx.pageName;

	/** @returns {string} string containing the text of the page after a successful load */
	this.getPageText = function {
		return ctx.pageText;

	/** @param {string} pageText - updated page text that will be saved when save is called */
	this.setPageText = function(pageText) {
		ctx.editMode = 'all';
		ctx.pageText = pageText;

	/** @param {string} appendText - text that will be appended to the page when append is called */
	this.setAppendText = function(appendText) {
		ctx.editMode = 'append';
		ctx.appendText = appendText;

	/** @param {string} prependText - text that will be prepended to the page when prepend is called */
	this.setPrependText = function(prependText) {
		ctx.editMode = 'prepend';
		ctx.prependText = prependText;


	// Edit-related setter methods:
	/** @param {string} summary - text of the edit summary that will be used when save is called */
	this.setEditSummary = function(summary) {
		ctx.editSummary = summary;

	/**
	this.setCreateOption = function(createOption) {
		ctx.createOption = createOption;

	/** @param {boolean} minorEdit - set true to mark the edit as a minor edit. */
	this.setMinorEdit = function(minorEdit) {
		ctx.minorEdit = minorEdit;

	/** @param {boolean} botEdit - set true to mark the edit as a bot edit */
	this.setBotEdit = function(botEdit) {
		ctx.botEdit = botEdit;

	/**
	this.setPageSection = function(pageSection) {
		ctx.pageSection = pageSection;

	/**
	this.setMaxConflictRetries = function(maxConflictRetries) {
		ctx.maxConflictRetries = maxConflictRetries;

	/**
	this.setMaxRetries = function(maxRetries) {
		ctx.maxRetries = maxRetries;

	/**
	this.setWatchlist = function(watchlistOption) {
		if (watchlistOption) {
			ctx.watchlistOption = 'watch';
			ctx.watchlistOption = 'nochange';

	/**
	this.setWatchlistFromPreferences = function(watchlistOption) {
		if (watchlistOption) {
			ctx.watchlistOption = 'preferences';
			ctx.watchlistOption = 'nochange';

	/**
	this.setFollowRedirect = function {
		if (ctx.pageLoaded) {
			ctx.statusElement.error('Internal error: cannot change redirect setting after the page has been loaded!');
			return;
		ctx.followRedirect = followRedirect;
		ctx.followCrossNsRedirect = typeof followCrossNsRedirect !== 'undefined' ? followCrossNsRedirect : ctx.followCrossNsRedirect;

	// lookup-creation setter function
	/**
	this.setLookupNonRedirectCreator = function(flag) {
		ctx.lookupNonRedirectCreator = flag;

	// Move-related setter functions
	/** @param {string} destination */
	this.setMoveDestination = function(destination) {
		ctx.moveDestination = destination;

	/** @param {boolean} flag */
	this.setMoveTalkPage = function(flag) {
		ctx.moveTalkPage = !!flag;

	/** @param {boolean} flag */
	this.setMoveSubpages = function(flag) {
		ctx.moveSubpages = !!flag;

	/** @param {boolean} flag */
	this.setMoveSuppressRedirect = function(flag) {
		ctx.moveSuppressRedirect = !!flag;

	// Protect-related setter functions
	/**
	this.setEditProtection = function {
		ctx.protectEdit = { level: level, expiry: expiry || 'infinity' };

	this.setMoveProtection = function {
		ctx.protectMove = { level: level, expiry: expiry || 'infinity' };

	this.setCreateProtection = function {
		ctx.protectCreate = { level: level, expiry: expiry || 'infinity' };

	this.setCascadingProtection = function(flag) {
		ctx.protectCascade = !!flag;

	this.suppressProtectWarning = function {
		ctx.suppressProtectWarning = true;

	// Revert-related getters/setters:
	this.setOldID = function(oldID) {
		ctx.revertOldID = oldID;

	/** @returns {string} string containing the current revision ID of the page */
	this.getCurrentID = function {
		return ctx.revertCurID;

	/** @returns {string} last editor of the page */
	this.getRevisionUser = function {
		return ctx.revertUser;

	/** @returns {string} ISO 8601 timestamp at which the page was last edited. */
	this.getLastEditTime = function {
		return ctx.lastEditTime;

	// Miscellaneous getters/setters:

	/**
	this.setCallbackParameters = function(callbackParameters) {
		ctx.callbackParameters = callbackParameters;

	/**
	this.getCallbackParameters = function {
		return ctx.callbackParameters;

	/**
	this.getStatusElement = function {
		return ctx.statusElement;

	/**
	this.setFlaggedRevs = function {
		ctx.flaggedRevs = { level: level, expiry: expiry || 'infinity' };

	/**
	this.exists = function {
		return ctx.pageExists;

	/**
	this.getPageID = function {
		return ctx.pageID;

	/**
	this.getLoadTime = function {
		return ctx.loadTime;

	/**
	this.getCreator = function {
		return ctx.creator;

	/**
	this.getCreationTimestamp = function {
		return ctx.timestamp;

	/**
	this.lookupCreation = function(onSuccess) {
		if (!onSuccess) {
			ctx.statusElement.error('Internal error: no onSuccess callback provided to lookupCreation!');
			return;
		ctx.onLookupCreationSuccess = onSuccess;

		var query = {
			'action': 'query',
			'prop': 'revisions',
			'titles': ctx.pageName,
			'rvlimit': 1,
			'rvprop': 'user|timestamp',
			'rvdir': 'newer'

		// Only the wikitext content model can reliably handle
		// rvsection, others return an error when paired with the
		// content rvprop. Relatedly, non-wikitext models don't
		// understand the #REDIRECT concept, so we shouldn't attempt
		// the redirect resolution in fnLookupCreationSuccess
		if (ctx.lookupNonRedirectCreator) {
			query.rvsection = 0;
			query.rvprop += '|content';

		if (ctx.followRedirect) {
			query.redirects = ''; // follow all redirects

		ctx.lookupCreationApi = new Morebits.wiki.api;
		ctx.lookupCreationApi.setParent(this);
		ctx.lookupCreationApi.post;

	/**
	this.revert = function {
		ctx.onSaveSuccess = onSuccess;
		ctx.onSaveFailure = onFailure || emptyFunction;

		if (!ctx.revertOldID) {
			ctx.statusElement.error('Internal error: revision ID to revert to was not set before revert!');
			ctx.onSaveFailure(this);
			return;

		ctx.editMode = 'revert';
		this.load;

	/**
	this.move = function {
		ctx.onMoveSuccess = onSuccess;
		ctx.onMoveFailure = onFailure || emptyFunction;

		if (!ctx.editSummary) {
			ctx.statusElement.error('Internal error: move reason not set before move (use setEditSummary function)!');
			ctx.onMoveFailure(this);
			return;
		if (!ctx.moveDestination) {
			ctx.statusElement.error('Internal error: destination page name was not set before move!');
			ctx.onMoveFailure(this);
			return;

		if (fnCanUseMwUserToken('move')) {
			fnProcessMove.call;
			var query = fnNeedTokenInfoQuery('move');

			ctx.moveApi = new Morebits.wiki.api;
			ctx.moveApi.setParent(this);
			ctx.moveApi.post;

	/**
	this.patrol = function {
		if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) {
			return;

		// If a link is present, don't need to check if it's patrolled
		if ($('.patrollink').length) {
			var patrolhref = $('.patrollink a').attr('href');
			ctx.rcid = mw.util.getParamValue;
			fnProcessPatrol;
			var patrolQuery = {
				action: 'query',
				prop: 'info',
				meta: 'tokens',
				type: 'patrol', // as long as we're querying, might as well get a token
				list: 'recentchanges', // check if the page is unpatrolled
				titles: ctx.pageName,
				rcprop: 'patrolled',
				rctitle: ctx.pageName,
				rclimit: 1

			ctx.patrolApi = new Morebits.wiki.api;
			ctx.patrolApi.setParent(this);
			ctx.patrolApi.post;

	/**
	this.triage = function {
		// Fall back to patrol if not a valid triage namespace
		if (mw.config.get('pageTriageNamespaces').indexOf(mw.config.get('wgNamespaceNumber')) === -1) {
			this.patrol;
			if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) {
				return;

			// If on the page in question, don't need to query for page ID
			if (new mw.Title(Morebits.pageNameNorm).getPrefixedText === new mw.Title(ctx.pageName).getPrefixedText) {
				ctx.pageID = mw.config.get('wgArticleId');
				fnProcessTriage;
				var query = fnNeedTokenInfoQuery('triage');

				ctx.triageApi = new Morebits.wiki.api;
				ctx.triageApi.setParent(this);
				ctx.triageApi.post;

	// |delete| is a reserved word in some flavours of JS
	/**
	this.deletePage = function {
		ctx.onDeleteSuccess = onSuccess;
		ctx.onDeleteFailure = onFailure || emptyFunction;

		// if a non-admin tries to do this, don't bother
		if (!Morebits.userIsSysop) {
			ctx.statusElement.error('Cannot delete page: only admins can do that');
			ctx.onDeleteFailure(this);
			return;
		if (!ctx.editSummary) {
			ctx.statusElement.error('Internal error: delete reason not set before delete (use setEditSummary function)!');
			ctx.onDeleteFailure(this);
			return;

		if (fnCanUseMwUserToken('delete')) {
			fnProcessDelete.call;
			var query = fnNeedTokenInfoQuery('delete');

			ctx.deleteApi = new Morebits.wiki.api;
			ctx.deleteApi.setParent(this);
			ctx.deleteApi.post;

	/**
	this.undeletePage = function {
		ctx.onUndeleteSuccess = onSuccess;
		ctx.onUndeleteFailure = onFailure || emptyFunction;

		// if a non-admin tries to do this, don't bother
		if (!Morebits.userIsSysop) {
			ctx.statusElement.error('Cannot undelete page: only admins can do that');
			ctx.onUndeleteFailure(this);
			return;
		if (!ctx.editSummary) {
			ctx.statusElement.error('Internal error: undelete reason not set before undelete (use setEditSummary function)!');
			ctx.onUndeleteFailure(this);
			return;

		if (fnCanUseMwUserToken('undelete')) {
			fnProcessUndelete.call;
			var query = fnNeedTokenInfoQuery('undelete');

			ctx.undeleteApi = new Morebits.wiki.api;
			ctx.undeleteApi.setParent(this);
			ctx.undeleteApi.post;

	/**
	this.protect = function {
		ctx.onProtectSuccess = onSuccess;
		ctx.onProtectFailure = onFailure || emptyFunction;

		// if a non-admin tries to do this, don't bother
		if (!Morebits.userIsSysop) {
			ctx.statusElement.error('Cannot protect page: only admins can do that');
			ctx.onProtectFailure(this);
			return;
		if (!ctx.protectEdit && !ctx.protectMove && !ctx.protectCreate) {
			ctx.statusElement.error('Internal error: you must set edit and/or move and/or create protection before calling protect!');
			ctx.onProtectFailure(this);
			return;
		if (!ctx.editSummary) {
			ctx.statusElement.error('Internal error: protection reason not set before protect (use setEditSummary function)!');
			ctx.onProtectFailure(this);
			return;

		// because of the way MW API interprets protection levels
		// , we always need to request
		// protection levels from the server
		var query = fnNeedTokenInfoQuery('protect');

		ctx.protectApi = new Morebits.wiki.api;
		ctx.protectApi.setParent(this);
		ctx.protectApi.post;

	/**
	this.stabilize = function {
		ctx.onStabilizeSuccess = onSuccess;
		ctx.onStabilizeFailure = onFailure || emptyFunction;

		// if a non-admin tries to do this, don't bother
		if (!Morebits.userIsSysop) {
			ctx.statusElement.error('Cannot apply FlaggedRevs settings: only admins can do that');
			ctx.onStabilizeFailure(this);
			return;
		if (!ctx.flaggedRevs) {
			ctx.statusElement.error('Internal error: you must set flaggedRevs before calling stabilize!');
			ctx.onStabilizeFailure(this);
			return;
		if (!ctx.editSummary) {
			ctx.statusElement.error('Internal error: reason not set before calling stabilize (use setEditSummary function)!');
			ctx.onStabilizeFailure(this);
			return;

		if (fnCanUseMwUserToken('stabilize')) {
			fnProcessStabilize.call;
			var query = fnNeedTokenInfoQuery('stabilize');

			ctx.stabilizeApi = new Morebits.wiki.api;
			ctx.stabilizeApi.setParent(this);
			ctx.stabilizeApi.post;

	/*

	/**
	var fnCanUseMwUserToken = function(action) {
		action = typeof action !== 'undefined' ? action : 'edit'; // IE doesn't support default parameters

		// API-based redirect resolution only works for action=query and
		// action=edit in append/prepend modes 
		if (ctx.followRedirect) {
			if (!ctx.followCrossNsRedirect) {
				return false; // must load the page to check for cross namespace redirects
			if (action !== 'edit' || (ctx.editMode !== 'append' && ctx.editMode !== 'prepend')) {
				return false;

		// do we need to fetch the edit protection expiry?
		if (Morebits.userIsSysop && !ctx.suppressProtectWarning) {
			if (new mw.Title(Morebits.pageNameNorm).getPrefixedText === new mw.Title(ctx.pageName).getPrefixedText) {
				return false;

			// wgRestrictionEdit is null on non-existent pages,
			// so this neatly handles nonexistent pages
			var editRestriction = mw.config.get('wgRestrictionEdit');
			if (!editRestriction || editRestriction.indexOf('sysop') !== -1) {
				return false;

		return !!mw.user.tokens.get('csrfToken');

	/**
	var fnNeedTokenInfoQuery = function(action) {
		var query = {
			action: 'query',
			meta: 'tokens',
			type: 'csrf',
			titles: ctx.pageName
		// Protection not checked for flagged-revs or non-sysop moves
		if (action !== 'stabilize' && (action !== 'move' || Morebits.userIsSysop)) {
			query.prop = 'info';
			query.inprop = 'protection';
		if (ctx.followRedirect && action !== 'undelete') {
			query.redirects = ''; // follow all redirects
		return query;

	// callback from loadSuccess for append and prepend threads
	var fnAutoSave = function(pageobj) {
		pageobj.save;

	// callback from loadApi.post
	var fnLoadSuccess = function {
		var xml = ctx.loadApi.getXML;

		if (!fnCheckPageName) {
			return; // abort

		ctx.pageExists = $(xml).find('page').attr('missing') !== '';
		if (ctx.pageExists) {
			ctx.pageText = $(xml).find('rev').text;
			ctx.pageID = $(xml).find('page').attr('pageid');
			ctx.pageText = ''; // allow for concatenation, etc.
			ctx.pageID = 0; // nonexistent in response, matches wgArticleId
		ctx.csrfToken = $(xml).find('tokens').attr('csrftoken');
		if (!ctx.csrfToken) {
			ctx.statusElement.error('Failed to retrieve edit token.');
			ctx.onLoadFailure(this);
			return;
		ctx.loadTime = $(xml).find('api').attr('curtimestamp');
		if (!ctx.loadTime) {
			ctx.statusElement.error('Failed to retrieve current timestamp.');
			ctx.onLoadFailure(this);
			return;

		// extract protection info, to alert admins when they are about to edit a protected page
		if (Morebits.userIsSysop) {
			var editprot = $(xml).find('pr[type="edit"]');
			if (editprot.length > 0 && editprot.attr('level') === 'sysop') {
				ctx.fullyProtected = editprot.attr('expiry');
				ctx.fullyProtected = false;

		ctx.lastEditTime = $(xml).find('rev').attr('timestamp');
		ctx.revertCurID = $(xml).find('page').attr('lastrevid');

		if (ctx.editMode === 'revert') {
			ctx.revertCurID = $(xml).find('rev').attr('revid');
			if (!ctx.revertCurID) {
				ctx.statusElement.error('Failed to retrieve current revision ID.');
				ctx.onLoadFailure(this);
				return;
			ctx.revertUser = $(xml).find('rev').attr('user');
			if (!ctx.revertUser) {
				if ($(xml).find('rev').attr('userhidden') === '') { // username was RevDel'd or oversighted
					ctx.revertUser = '';
					ctx.statusElement.error('Failed to retrieve user who made the revision.');
					ctx.onLoadFailure(this);
					return;
			// set revert edit summary
			ctx.editSummary = 'Reverted to revision ' + ctx.revertOldID + ' by ' + ctx.revertUser + ': ' + ctx.editSummary;

		ctx.pageLoaded = true;

		// alert("Generate edit conflict now"); // for testing edit conflict recovery logic
		ctx.onLoadSuccess(this); // invoke callback

	// helper function to parse the page name returned from the API
	var fnCheckPageName = function {
		if (!onFailure) {
			onFailure = emptyFunction;

		// check for invalid titles
		if ($(xml).find('page').attr('invalid') === '') {
			ctx.statusElement.error('The page title is invalid: ' + ctx.pageName);
			onFailure(this);
			return false; // abort

		// retrieve actual title of the page after normalization and redirects
		if ($(xml).find('page').attr('title')) {
			var resolvedName = $(xml).find('page').attr('title');

			if ($(xml).find('redirects').length > 0) {
				// check for cross-namespace redirect:
				var origNs = new mw.Title(ctx.pageName).namespace;
				var newNs = new mw.Title(resolvedName).namespace;
				if (origNs !== newNs && !ctx.followCrossNsRedirect) {
					ctx.statusElement.error;
					onFailure(this);
					return false;

				// only notify user for redirects, not normalization
				Morebits.status.info;

			ctx.pageName = resolvedName; // update to redirect target or normalized name

			// could be a circular redirect or other problem
			ctx.statusElement.error('Could not resolve redirects for: ' + ctx.pageName);
			onFailure(this);

			// force error to stay on the screen
			++Morebits.wiki.numberOfActionsLeft;
			return false; // abort
		return true; // all OK

	// callback from saveApi.post
	var fnSaveSuccess = function {
		ctx.editMode = 'all'; // cancel append/prepend/revert modes
		var xml = ctx.saveApi.getXML;

		// see if the API thinks we were successful
		if ($(xml).find('edit').attr('result') === 'Success') {

			// real success
			// default on success action - display link for edited page
			var link = document.createElement('a');
			link.setAttribute('href', mw.util.getUrl(ctx.pageName));
			link.appendChild(document.createTextNode(ctx.pageName));
			ctx.statusElement.info(['완료 ']);
			if (ctx.onSaveSuccess) {
				ctx.onSaveSuccess(this); // invoke callback
			return;

		// errors here are only generated by extensions which hook APIEditBeforeSave within MediaWiki,
		// which as of 1.34.0-wmf.23 (Sept 2019) should only encompass captcha messages
		if ($(xml).find('captcha').length > 0) {
			ctx.statusElement.error('Could not save the page because the wiki server wanted you to fill out a CAPTCHA.');
			ctx.statusElement.error('Unknown error received from API while saving page');

		// force error to stay on the screen
		++Morebits.wiki.numberOfActionsLeft;

		ctx.onSaveFailure(this);

	// callback from saveApi.post
	var fnSaveError = function {
		var errorCode = ctx.saveApi.getErrorCode;

		// check for edit conflict
		if (errorCode === 'editconflict' && ctx.conflictRetries++ < ctx.maxConflictRetries) {

			// edit conflicts can occur when the page needs to be purged from the server cache
			var purgeQuery = {
				action: 'purge',
				titles: ctx.pageName // redirects are already resolved

			var purgeApi = new Morebits.wiki.api('Edit conflict detected, purging server cache', purgeQuery, function {
				--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds

				ctx.statusElement.info;
				if (fnCanUseMwUserToken('edit')) {
					ctx.saveApi.post; // necessarily append or prepend, so this should work as desired
					ctx.loadApi.post; // reload the page and reapply the edit
			purgeApi.post;

		// check for network or server error

			// the error might be transient, so try again
			ctx.statusElement.info;
			--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
			ctx.saveApi.post; // give it another go!

		// hard error, give up

			// non-admin attempting to edit a protected page - this gives a friendlier message than the default
			if (errorCode === 'protectedpage') {
				ctx.statusElement.error('Failed to save edit: Page is protected');
			// check for absuefilter hits: disallowed or warning
				var desc = $(ctx.saveApi.getXML).find('abusefilter').attr('description');
				if (errorCode === 'abusefilter-disallowed') {
					ctx.statusElement.error('The edit was disallowed by the edit filter: "' + desc + '".');
					ctx.statusElement.error;
					// We should provide the user with a way to automatically retry the action if they so choose -
					// I can't see how to do this without creating a UI dependency on Morebits.wiki.page though -- TTO
					ctx.statusElement.error('The edit was disallowed by the edit filter.');
			// check for blacklist hits
				// .find('matches') returns an array in case multiple items are blacklisted, we only return the first
				var spam = $(ctx.saveApi.getXML).find('spamblacklist').find('matches').children[0].textContent;
				ctx.statusElement.error('Could not save the page because the URL ' + spam + ' is on the spam blacklist');
				ctx.statusElement.error('Failed to save edit: ' + ctx.saveApi.getErrorText);
			ctx.editMode = 'all'; // cancel append/prepend/revert modes
			if (ctx.onSaveFailure) {
				ctx.onSaveFailure(this); // invoke callback

	var fnLookupCreationSuccess = function {
		var xml = ctx.lookupCreationApi.getXML;

		if (!fnCheckPageName(xml)) {
			return; // abort

		if (!ctx.lookupNonRedirectCreator ||
		(!/^\s*#redirect/i.test($(xml).find('rev').text) && !/^\s*#넘겨주기/.test($(xml).find('rev').text))
		) {

			ctx.creator = $(xml).find('rev').attr('user');
			if (!ctx.creator) {
				ctx.statusElement.error('Could not find name of page creator');
				return;
			ctx.timestamp = $(xml).find('rev').attr('timestamp');
			if (!ctx.timestamp) {
				ctx.statusElement.error('Could not find timestamp of page creation');
				return;
			ctx.onLookupCreationSuccess(this);

			ctx.lookupCreationApi.query.rvlimit = 50; // modify previous query to fetch more revisions
			ctx.lookupCreationApi.query.titles = ctx.pageName; // update pageName if redirect resolution took place in earlier query

			ctx.lookupCreationApi = new Morebits.wiki.api;
			ctx.lookupCreationApi.setParent(this);
			ctx.lookupCreationApi.post;

	var fnLookupNonRedirectCreator = function {
		var xml = ctx.lookupCreationApi.getXML;

		$(xml).find('rev').each(function {
			if (!/^\s*#redirect/i.test(rev.textContent) &&
				ctx.creator = rev.getAttribute('user');
				ctx.timestamp = rev.getAttribute('timestamp');
				return false; // break

		if (!ctx.creator) {
			// fallback to give first revision author if no non-redirect version in the first 50
			ctx.creator = $(xml).find('rev')[0].getAttribute('user');
			ctx.timestamp = $(xml).find('rev')[0].getAttribute('timestamp');
			if (!ctx.creator) {
				ctx.statusElement.error('Could not find name of page creator');
				return;

		if (!ctx.timestamp) {
			ctx.statusElement.error('Could not find timestamp of page creation');
			return;

		ctx.onLookupCreationSuccess(this);

	var fnProcessMove = function {
		var pageTitle, token;

		if (fnCanUseMwUserToken('move')) {
			token = mw.user.tokens.get('csrfToken');
			pageTitle = ctx.pageName;
			var xml = ctx.moveApi.getXML;

			if ($(xml).find('page').attr('missing') === '') {
				ctx.statusElement.error;
				ctx.onMoveFailure(this);
				return;

			// extract protection info
			if (Morebits.userIsSysop) {
				var editprot = $(xml).find('pr[type="edit"]');
				if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !ctx.suppressProtectWarning &&
					(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.date(editprot.attr('expiry')).calendar('utc') + ' (UTC))') +
					'. \n\nClick OK to proceed with the move, or Cancel to skip this move.')) {
					ctx.statusElement.error('Move of fully protected page was aborted.');
					ctx.onMoveFailure(this);
					return;

			token = $(xml).find('tokens').attr('csrftoken');
			if (!token) {
				ctx.statusElement.error('Failed to retrieve move token.');
				ctx.onMoveFailure(this);
				return;

			pageTitle = $(xml).find('page').attr('title');

		var query = {
			'action': 'move',
			'from': pageTitle,
			'to': ctx.moveDestination,
			'token': token,
			'reason': ctx.editSummary,
			'watchlist': ctx.watchlistOption
		if (ctx.moveTalkPage) {
			query.movetalk = 'true';
		if (ctx.moveSubpages) {
			query.movesubpages = 'true';
		if (ctx.moveSuppressRedirect) {
			query.noredirect = 'true';

		ctx.moveProcessApi = new Morebits.wiki.api;
		ctx.moveProcessApi.setParent(this);
		ctx.moveProcessApi.post;

	var fnProcessPatrol = function {
		var query = {
			action: 'patrol'

		// Didn't need to load the page
		if (ctx.rcid) {
			query.rcid = ctx.rcid;
			query.token = mw.user.tokens.get('patrolToken');
			var xml = ctx.patrolApi.getResponse;

			// Don't patrol if not unpatrolled
			if ($(xml).find('rc').attr('unpatrolled') !== '') {
				return;

			var lastrevid = $(xml).find('page').attr('lastrevid');
			if (!lastrevid) {
				return;
			query.revid = lastrevid;

			var token = $(xml).find('tokens').attr('patroltoken');
			if (!token) {
				return;

			query.token = token;

		var patrolStat = new Morebits.status('Marking page as patrolled');

		ctx.patrolProcessApi = new Morebits.wiki.api;
		ctx.patrolProcessApi.setParent(this);
		ctx.patrolProcessApi.post;

	var fnProcessTriage = function {
		var pageID, token;

		if (ctx.pageID) {
			token = mw.user.tokens.get('csrfToken');
			pageID = ctx.pageID;
			var xml = ctx.triageApi.getXML;

			pageID = $(xml).find('page').attr('pageid');
			if (!pageID) {
				return;

			token = $(xml).find('tokens').attr('csrftoken');
			if (!token) {
				return;

		var query = {
			action: 'pagetriageaction',
			pageid: pageID,
			reviewed: 1,
			token: token

		var triageStat = new Morebits.status('Marking page as curated');

		ctx.triageProcessApi = new Morebits.wiki.api;
		ctx.triageProcessApi.setParent(this);
		ctx.triageProcessApi.post;

	// callback from triageProcessApi.post
	var fnProcessTriageError = function {
		// Ignore error if page not in queue, see https://github.com/azatoth/twinkle/pull/930
		if (ctx.triageProcessApi.getErrorCode === 'bad-pagetriage-page') {
			ctx.triageProcessApi.getStatusElement.unlink;

	var fnProcessDelete = function {
		var pageTitle, token;

		if (fnCanUseMwUserToken('delete')) {
			token = mw.user.tokens.get('csrfToken');
			pageTitle = ctx.pageName;
			var xml = ctx.deleteApi.getXML;

			if ($(xml).find('page').attr('missing') === '') {
				ctx.statusElement.error;
				ctx.onDeleteFailure(this);
				return;

			// extract protection info
			var editprot = $(xml).find('pr[type="edit"]');
			if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !ctx.suppressProtectWarning &&
				(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.date(editprot.attr('expiry')).calendar('utc') + ' (UTC))') +
				'. \n\nClick OK to proceed with the deletion, or Cancel to skip this deletion.')) {
				ctx.statusElement.error('Deletion of fully protected page was aborted.');
				ctx.onDeleteFailure(this);
				return;

			token = $(xml).find('tokens').attr('csrftoken');
			if (!token) {
				ctx.statusElement.error('Failed to retrieve delete token.');
				ctx.onDeleteFailure(this);
				return;

			pageTitle = $(xml).find('page').attr('title');

		var query = {
			'action': 'delete',
			'title': pageTitle,
			'token': token,
			'reason': ctx.editSummary,
			'watchlist': ctx.watchlistOption

		ctx.deleteProcessApi = new Morebits.wiki.api;
		ctx.deleteProcessApi.setParent(this);
		ctx.deleteProcessApi.post;

	// callback from deleteProcessApi.post
	var fnProcessDeleteError = function {

		var errorCode = ctx.deleteProcessApi.getErrorCode;

		// check for "Database query error"
		if (errorCode === 'internal_api_error_DBQueryError' && ctx.retries++ < ctx.maxRetries) {
			ctx.statusElement.info;
			--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
			ctx.deleteProcessApi.post; // give it another go!

			ctx.statusElement.error;
			if (ctx.onDeleteFailure) {
				ctx.onDeleteFailure.call; // invoke callback
		// hard error, give up
			ctx.statusElement.error('Failed to delete the page: ' + ctx.deleteProcessApi.getErrorText);
			if (ctx.onDeleteFailure) {
				ctx.onDeleteFailure.call; // invoke callback

	var fnProcessUndelete = function {
		var pageTitle, token;

		if (fnCanUseMwUserToken('undelete')) {
			token = mw.user.tokens.get('csrfToken');
			pageTitle = ctx.pageName;
			var xml = ctx.undeleteApi.getXML;

			if ($(xml).find('page').attr('missing') !== '') {
				ctx.statusElement.error;
				ctx.onUndeleteFailure(this);
				return;

			// extract protection info
			var editprot = $(xml).find('pr[type="create"]');
			if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !ctx.suppressProtectWarning &&
				(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.date(editprot.attr('expiry')).calendar('utc') + ' (UTC))') +
				'. \n\nClick OK to proceed with the undeletion, or Cancel to skip this undeletion.')) {
				ctx.statusElement.error('Undeletion of fully create protected page was aborted.');
				ctx.onUndeleteFailure(this);
				return;

			token = $(xml).find('tokens').attr('csrftoken');
			if (!token) {
				ctx.statusElement.error('Failed to retrieve undelete token.');
				ctx.onUndeleteFailure(this);
				return;

			pageTitle = $(xml).find('page').attr('title');

		var query = {
			'action': 'undelete',
			'title': pageTitle,
			'token': token,
			'reason': ctx.editSummary,
			'watchlist': ctx.watchlistOption

		ctx.undeleteProcessApi = new Morebits.wiki.api;
		ctx.undeleteProcessApi.setParent(this);
		ctx.undeleteProcessApi.post;

	// callback from undeleteProcessApi.post
	var fnProcessUndeleteError = function {

		var errorCode = ctx.undeleteProcessApi.getErrorCode;

		// check for "Database query error"
		if (errorCode === 'internal_api_error_DBQueryError' && ctx.retries++ < ctx.maxRetries) {
			ctx.statusElement.info;
			--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
			ctx.undeleteProcessApi.post; // give it another go!
			ctx.statusElement.error;
			if (ctx.onUndeleteFailure) {
				ctx.onUndeleteFailure.call; // invoke callback
		// hard error, give up
			ctx.statusElement.error('Failed to undelete the page: ' + ctx.undeleteProcessApi.getErrorText);
			if (ctx.onUndeleteFailure) {
				ctx.onUndeleteFailure.call; // invoke callback

	var fnProcessProtect = function {
		var xml = ctx.protectApi.getXML;

		var missing = $(xml).find('page').attr('missing') === '';
		if ((ctx.protectEdit || ctx.protectMove) && missing) {
			ctx.statusElement.error;
			ctx.onProtectFailure(this);
			return;
		if (ctx.protectCreate && !missing) {
			ctx.statusElement.error;
			ctx.onProtectFailure(this);
			return;

		// TODO cascading protection not possible on edit<sysop

		var token = $(xml).find('tokens').attr('csrftoken');
		if (!token) {
			ctx.statusElement.error('Failed to retrieve protect token.');
			ctx.onProtectFailure(this);
			return;

		var pageTitle = $(xml).find('page').attr('title');

		// fetch existing protection levels
		var prs = $(xml).find('pr');
		var editprot = prs.filter('[type="edit"]');
		var moveprot = prs.filter('[type="move"]');
		var createprot = prs.filter('[type="create"]');

		var protections = [], expirys = [];

		// set edit protection level
		if (ctx.protectEdit) {
			protections.push('edit=' + ctx.protectEdit.level);
			expirys.push(ctx.protectEdit.expiry);
			protections.push('edit=' + editprot.attr('level'));
			expirys.push(editprot.attr('expiry').replace);

		if (ctx.protectMove) {
			protections.push('move=' + ctx.protectMove.level);
			expirys.push(ctx.protectMove.expiry);
			protections.push('move=' + moveprot.attr('level'));
			expirys.push(moveprot.attr('expiry').replace);

		if (ctx.protectCreate) {
			protections.push('create=' + ctx.protectCreate.level);
			expirys.push(ctx.protectCreate.expiry);
			protections.push('create=' + createprot.attr('level'));
			expirys.push(createprot.attr('expiry').replace);

		var query = {
			action: 'protect',
			title: pageTitle,
			token: token,
			protections: protections.join('|'),
			expiry: expirys.join('|'),
			reason: ctx.editSummary,
			watchlist: ctx.watchlistOption
		if (ctx.protectCascade) {
			query.cascade = 'true';

		ctx.protectProcessApi = new Morebits.wiki.api;
		ctx.protectProcessApi.setParent(this);
		ctx.protectProcessApi.post;

	var fnProcessStabilize = function {
		var pageTitle, token;

		if (fnCanUseMwUserToken('stabilize')) {
			token = mw.user.tokens.get('csrfToken');
			pageTitle = ctx.pageName;
			var xml = ctx.stabilizeApi.getXML;

			var missing = $(xml).find('page').attr('missing') === '';
			if (missing) {
				ctx.statusElement.error;
				ctx.onStabilizeFailure(this);
				return;

			token = $(xml).find('tokens').attr('csrftoken');
			if (!token) {
				ctx.statusElement.error('Failed to retrieve stabilize token.');
				ctx.onStabilizeFailure(this);
				return;

			pageTitle = $(xml).find('page').attr('title');

		var query = {
			action: 'stabilize',
			title: pageTitle,
			token: token,
			protectlevel: ctx.flaggedRevs.level,
			expiry: ctx.flaggedRevs.expiry,
			reason: ctx.editSummary
		// phab:T247915
		if (ctx.watchlistOption === 'watch') {
			query.watchlist = 'true';

		ctx.stabilizeProcessApi = new Morebits.wiki.api;
		ctx.stabilizeProcessApi.setParent(this);
		ctx.stabilizeProcessApi.post;

/* Morebits.wiki.page TODO: (XXX)


/**

/**
Morebits.wiki.preview = function(previewbox) {
	this.previewbox = previewbox;
	$(previewbox).addClass('morebits-previewbox').hide;

	/**
	this.beginRender = function {
		$(previewbox).show;

		var statusspan = document.createElement('span');
		previewbox.appendChild(statusspan);
		Morebits.status.init(statusspan);

		var query = {
			action: 'parse',
			prop: 'text',
			pst: 'true', // PST = pre-save transform; this makes substitution work properly
			text: wikitext,
			title: pageTitle || mw.config.get('wgPageName')
		var renderApi = new Morebits.wiki.api('loading...', query, fnRenderSuccess, new Morebits.status('Preview'));
		renderApi.post;

	var fnRenderSuccess = function(apiobj) {
		var xml = apiobj.getXML;
		var html = $(xml).find('text').text;
		if (!html) {
			apiobj.statelem.error;
			return;
		previewbox.innerHTML = html;
		$(previewbox).find('a').attr; // this makes links open in new tab

	/** Hides the preview box and clears it. */
	this.closePreview = function {
		$(previewbox).empty.hide;


/**

Morebits.wikitext = {};

Morebits.wikitext.template = {
	parse: function {
		var count = -1;
		var level = -1;
		var equals = -1;
		var current = '';
		var result = {
			name: '',
			parameters: {}
		var key, value;

		for (var i = start; i < text.length; ++i) {
			var test3 = text.substr;
			if (test3 === '}') {
				current += '}}}';
				i += 2;
				--level;
				continue;
			var test2 = text.substr;
			if (test2 === '') {
				current += test2;
				++i;
				--level;

				if (level <= 0) {
					if (count === -1) {
						result.name = current.substring(2).trim;
						++count;
						if (equals !== -1) {
							key = current.substring.trim;
							value = current.substring(equals).trim;
							result.parameters[key] = value;
							equals = -1;
							result.parameters[count] = current;
							++count;
					break;
				continue;

			if (text.charAt(i) === '|' && level <= 0) {
				if (count === -1) {
					result.name = current.substring(2).trim;
					++count;
					if (equals !== -1) {
						key = current.substring.trim;
						value = current.substring(equals + 1).trim;
						result.parameters[key] = value;
						equals = -1;
						result.parameters[count] = current;
						++count;
				current = '';
				equals = current.length;
				current += text.charAt(i);
				current += text.charAt(i);

		return result;

/**
Morebits.wikitext.page = function mediawikiPage(text) {
	this.text = text;

Morebits.wikitext.page.prototype = {
	text: '',

	/**
	removeLink: function(link_target) {
		var first_char = link_target.substr;
		var link_re_string = '[' + first_char.toUpperCase + first_char.toLowerCase + ']' + RegExp.escape(link_target.substr(1), true);

		// Files and Categories become links with a leading colon, e.g. :File:Test.png
		// Otherwise, allow for an optional leading colon, e.g. :User:Test
		var special_ns_re = /^(?:[Ff]ile|[Ii]mage|[Cc]ategory|파일|그림|분류):/;
		var colon = special_ns_re.test(link_target) ? ':' : ':?';

		var link_simple_re = new RegExp('\\[\\[' + colon + '(' + link_re_string + ')\\]\\]', 'g');
		var link_named_re = new RegExp('\\[\\[' + colon + link_re_string + '\\|(.+?)\\]\\]', 'g');
		this.text = this.text.replace.replace;
		return this;

	/**
	commentOutImage: function {
		var unbinder = new Morebits.unbinder(this.text);
		unbinder.unbind('');

		reason = reason ? reason + ': ' : '';
		var first_char = image.substr;
		var image_re_string = '[' + first_char.toUpperCase + first_char.toLowerCase + ']' + RegExp.escape(image.substr(1), true);

		// Check for normal image links, i.e. ...
		// Will eat the whole link
		var links_re = new RegExp('\\[\\[(?:[Ii]mage|[Ff]ile|파일|그림):\\s*' + image_re_string);
		var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys);
		for (var i = 0; i < allLinks.length; ++i) {
			if (links_re.test(allLinks[i])) {
				var replacement = '';
				unbinder.content = unbinder.content.replace;
		// unbind the newly created comments
		unbinder.unbind('');

		// Check for gallery images, i.e. instances that must start on a new line,
		// eventually preceded with some space, and must include File: prefix
		// Will eat the whole line.
		var gallery_image_re = new RegExp('(^\\s*(?:[Ii]mage|[Ff]ile|파일|그림):\\s*' + image_re_string + '.*?$)', 'mg');
		unbinder.content = unbinder.content.replace;

		// unbind the newly created comments
		unbinder.unbind('');

		// Check free image usages, for example as template arguments, might have the File: prefix excluded, but must be preceeded by an |
		// Will only eat the image name and the preceeding bar and an eventual named parameter
		var free_image_re = new RegExp('(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:(?:[Ii]mage|[Ff]ile|파일|그림):\\s*)?' + image_re_string + ')', 'mg');
		unbinder.content = unbinder.content.replace;
		// Rebind the content now, we are done!
		this.text = unbinder.rebind;
		return this;

	/**
	addToImageComment: function {
		var first_char = image.substr;
		var first_char_regex = RegExp.escape;
		if (first_char.toUpperCase !== first_char.toLowerCase) {
			first_char_regex = '[' + RegExp.escape + RegExp.escape + ']';
		var image_re_string = '(?:[Ii]mage|[Ff]ile|파일|그림):\\s*' + first_char_regex + RegExp.escape(image.substr(1), true);
		var links_re = new RegExp('\\[\\[' + image_re_string);
		var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys);
		for (var i = 0; i < allLinks.length; ++i) {
			if (links_re.test(allLinks[i])) {
				var replacement = allLinks[i];
				// just put it at the end?
				replacement = replacement.replace;
				this.text = this.text.replace;
		var gallery_re = new RegExp('^(\\s*' + image_re_string + '.*?)\\|?(.*?)$', 'mg');
		var newtext = '$1|$2 ' + data;
		this.text = this.text.replace;
		return this;

	/**
	removeTemplate: function(template) {
		var first_char = template.substr;
		var template_re_string = '(?:[Tt]emplate:)?\\s*[' + first_char.toUpperCase + first_char.toLowerCase + ']' + RegExp.escape(template.substr(1), true);
		var links_re = new RegExp('\\{\\{' + template_re_string);
		var allTemplates = Morebits.array.uniq(Morebits.string.splitWeightedByKeys);
		for (var i = 0; i < allTemplates.length; ++i) {
			if (links_re.test(allTemplates[i])) {
				this.text = this.text.replace;
		return this;

	/**
	insertAfterTemplates: function {
		if (typeof tag === 'undefined') {
			throw new Error('태그가 지정되지 않았습니다');

		// .length is only a property of strings and arrays so we
		// shouldn't need to check type
		if (typeof regex === 'undefined' || !regex.length) {
			throw new Error('정규 표현식이 지정되지 않았습니다');
			regex = regex.join('|');

		flags = flags || 'i';

		if (!preRegex || !preRegex.length) {
			preRegex = '';
			preRegex = preRegex.join('|');

		// Regex is extra complicated to allow for templates with
		// parameters and to handle whitespace properly
		this.text = this.text.replace(
			new RegExp(
				// leading whitespace
				'^\\s*' +
				// capture template(s)
				'(?:((?:\\s*' +
				// Pre-template regex, such as leading html comments
				preRegex + '|' +
				// begin template format
				'\\{\\{\\s*(?:' +
				// Template regex
				regex +
				// end main template name, optionally with a number
				// Probably remove the (?:) though
				')\\d*\\s*' +
				// template parameters
				'(\\|(?:\\{\\{[^{}]*\\}\\}|[^{}])*)?' +
				// end template format
				'\\}\\})+' +
				// end capture
				'(?:\\s*\\n)?)' +
				// trailing whitespace
				'\\s*)?',
				flags), '$1' + tag
		);
		return this;

	/** @returns {string} */
	getText: function {
		return this.text;

/**

Morebits.userspaceLogger = function(logPageName) {
	if (!logPageName) {
		throw new Error('기록 문서 이름이 지정되지 않았습니다');
	this.initialText = '';
	this.headerLevel = 3;

	this.log = function {
		if (!logText) {
			return;
		var page = new Morebits.wiki.page('User:' + mw.config.get('wgUserName') + '/' + logPageName,
			'Adding entry to userspace log'); // make this '... to ' + logPageName ?
		return page.load(function(pageobj) {
			// add blurb if log page doesn't exist or is blank
			var text = pageobj.getPageText || this.initialText;

			// create monthly header if it doesn't exist already
			var date = new Morebits.date(pageobj.getLoadTime);
			if (!date.monthHeaderRegex.exec(text)) {
				text += '\n\n' + date.monthHeader(this.headerLevel);

			pageobj.setPageText(text + '\n' + logText);
			pageobj.setEditSummary(summaryText);
			pageobj.setCreateOption('recreate');
			pageobj.save;

/**

/**

Morebits.status = function Status {
	this.textRaw = text;
	this.text = this.codify(text);
	this.type = type || 'status';
	this.generate;
	if (stat) {
		this.update;

/**
Morebits.status.init = function(root) {
	if (!(root instanceof Element)) {
		throw new Error('객체가 Element의 인스턴스가 아닙니다');
	while (root.hasChildNodes) {
		root.removeChild(root.firstChild);
	Morebits.status.root = root;
	Morebits.status.errorEvent = null;

Morebits.status.root = null;

/** @param {Function} handler - function to execute on error */
Morebits.status.onError = function(handler) {
	if (typeof handler === 'function') {
		Morebits.status.errorEvent = handler;
		throw 'Morebits.status.onError: handler is not a function';

Morebits.status.prototype = {
	stat: null,
	text: null,
	textRaw: null,
	type: 'status',
	target: null,
	node: null,
	linked: false,

	/** Add the status element node to the DOM */
	link: function {
		if (!this.linked && Morebits.status.root) {
			Morebits.status.root.appendChild(this.node);
			this.linked = true;

	/** Remove the status element node from the DOM */
	unlink: function {
		if (this.linked) {
			Morebits.status.root.removeChild(this.node);
			this.linked = false;

	/**
	codify: function(obj) {
		if (!Array.isArray(obj)) {
			obj = [ obj ];
		var result;
		result = document.createDocumentFragment;
		for (var i = 0; i < obj.length; ++i) {
			if (typeof obj[i] === 'string') {
				result.appendChild(document.createTextNode(obj[i]));
				result.appendChild(obj[i]);
		return result;

	/**
	update: function {
		this.stat = this.codify(status);
		if (type) {
			this.type = type;
			if (type === 'error') {
				// hack to force the page not to reload when an error is output - see also Morebits.status above
				Morebits.wiki.numberOfActionsLeft = 1000;

				// call error callback
				if (Morebits.status.errorEvent) {
					Morebits.status.errorEvent;

				// also log error messages in the browser console
				console.error(this.textRaw + ': ' + status); // eslint-disable-line no-console
		this.render;

	/** Produce the html for first part of the status message */
	generate: function {
		this.node = document.createElement('div');
		this.node.appendChild(document.createElement('span')).appendChild(this.text);
		this.node.appendChild(document.createElement('span')).appendChild(document.createTextNode(': '));
		this.target = this.node.appendChild(document.createElement('span'));
		this.target.appendChild(document.createTextNode('')); // dummy node

	/** Complete the html, for the second part of the status message */
	render: function {
		this.node.className = 'morebits_status_' + this.type;
		while (this.target.hasChildNodes) {
			this.target.removeChild(this.target.firstChild);
		this.target.appendChild(this.stat);
		this.link;
	status: function(status) {
		this.update;
	info: function(status) {
		this.update;
	warn: function(status) {
		this.update;
	error: function(status) {
		this.update;

Morebits.status.info = function {
	return new Morebits.status;

Morebits.status.warn = function {
	return new Morebits.status;

Morebits.status.error = function {
	return new Morebits.status;

/**
Morebits.status.actionCompleted = function(text) {
	var node = document.createElement('div');
	node.appendChild(document.createElement('b')).appendChild(document.createTextNode(text));
	node.className = 'morebits_status_info';
	if (Morebits.status.root) {
		Morebits.status.root.appendChild(node);

/**
Morebits.status.printUserText = function {
	var p = document.createElement('p');
	p.innerHTML = message;
	var div = document.createElement('div');
	div.className = 'toccolours';
	div.style.marginTop = '0';
	div.style.whiteSpace = 'pre-wrap';
	div.textContent = comments;
	p.appendChild(div);
	Morebits.status.root.appendChild(p);


/**
Morebits.htmlNode = function  {
	var node = document.createElement(type);
	if (color) {
		node.style.color = color;
	node.appendChild(document.createTextNode(content));
	return node;


/**
Morebits.checkboxShiftClickSupport = function  {
	var lastCheckbox = null;

	function clickHandler(event) {
		var thisCb = this;
		if (event.shiftKey && lastCheckbox !== null) {
			var cbs = $; // can't cache them, obviously, if we want to support resorting
			var index = -1, lastIndex = -1, i;
			for (i = 0; i < cbs.length; i++) {
				if (cbs[i] === thisCb) {
					index = i;
					if (lastIndex > -1) {
						break;
				if (cbs[i] === lastCheckbox) {
					lastIndex = i;
					if (index > -1) {
						break;

			if (index > -1 && lastIndex > -1) {
				// inspired by wikibits
				var endState = thisCb.checked;
				var start, finish;
				if (index < lastIndex) {
					start = index + 1;
					finish = lastIndex;
					start = lastIndex;
					finish = index - 1;

				for (i = start; i <= finish; i++) {
					if (cbs[i].checked !== endState) {
						cbs[i].click;
		lastCheckbox = thisCb;
		return true;

	$.click(clickHandler);


/** **************** Morebits.batchOperation ****************

/**
Morebits.batchOperation = function(currentAction) {
	var ctx = {
		// backing fields for public properties
		pageList: null,
		options: {
			chunkSize: 50,
			preserveIndividualStatusLines: false

		// internal counters, etc.
		statusElement: new Morebits.status(currentAction || 'Performing batch operation'),
		worker: null, // function that executes for each item in pageList
		postFinish: null, // function that executes when the whole batch has been processed
		countStarted: 0,
		countFinished: 0,
		countFinishedSuccess: 0,
		currentChunkIndex: -1,
		pageChunks: [],
		running: false

	// shouldn't be needed by external users, but provided anyway for maximum flexibility
	this.getStatusElement = function {
		return ctx.statusElement;

	/**
	this.setPageList = function(pageList) {
		ctx.pageList = pageList;

	/**
	this.setOption = function {
		ctx.options[optionName] = optionValue;

	/**
	this.run = function {
		if (ctx.running) {
			ctx.statusElement.error('Batch operation is already running');
			return;
		ctx.running = true;

		ctx.worker = worker;
		ctx.postFinish = postFinish;
		ctx.countStarted = 0;
		ctx.countFinished = 0;
		ctx.countFinishedSuccess = 0;
		ctx.currentChunkIndex = -1;
		ctx.pageChunks = [];

		var total = ctx.pageList.length;
		if (!total) {
			ctx.statusElement.info('no pages specified');
			ctx.running = false;
			if (ctx.postFinish) {
				ctx.postFinish;
			return;

		// chunk page list into more manageable units
		ctx.pageChunks = Morebits.array.chunk;

		// start the process
		Morebits.wiki.addCheckpoint;
		ctx.statusElement.status('0%');
		fnStartNewChunk;

	/**
	this.workerSuccess = function(arg) {

		var createPageLink = function(pageName) {
			var link = document.createElement('a');
			link.setAttribute('href', mw.util.getUrl(pageName));
			link.appendChild(document.createTextNode(pageName));
			return link;

		if (arg instanceof Morebits.wiki.api || arg instanceof Morebits.wiki.page) {
			// update or remove status line
			var statelem = arg.getStatusElement;
			if (ctx.options.preserveIndividualStatusLines) {
				if (arg.getPageName || arg.pageName || (arg.query && arg.query.title)) {
					// we know the page title - display a relevant message
					var pageName = arg.getPageName ? arg.getPageName : arg.pageName || arg.query.title;
					statelem.info(['완료 (', createPageLink(pageName), ')']);
					// we don't know the page title - just display a generic message
					statelem.info('done');
				// remove the status line automatically produced by Morebits.wiki.*
				statelem.unlink;

			new Morebits.status(arg, ['done (', createPageLink(arg), ')']);

		ctx.countFinishedSuccess++;
		fnDoneOne;

	this.workerFailure = function {
		fnDoneOne;

	// private functions

	var thisProxy = this;

	var fnStartNewChunk = function {
		var chunk = ctx.pageChunks[++ctx.currentChunkIndex];
		if (!chunk) {
			return; // done! yay

		// start workers for the current chunk
		ctx.countStarted += chunk.length;
		chunk.forEach(function(page) {
			ctx.worker;

	var fnDoneOne = function {
		ctx.countFinished++;

		// update overall status line
		var total = ctx.pageList.length;
		if (ctx.countFinished === total) {
			var statusString = 'Done (' + ctx.countFinishedSuccess +
				'/' + ctx.countFinished + '개 동작을 성공적으로 완료하였습니다)';
			if (ctx.countFinishedSuccess < ctx.countFinished) {
				ctx.statusElement.warn(statusString);
				ctx.statusElement.info(statusString);
			if (ctx.postFinish) {
				ctx.postFinish;
			Morebits.wiki.removeCheckpoint;
			ctx.running = false;
			return;

		// just for giggles! 
		if (ctx.countFinished > total) {
			ctx.statusElement.warn('Done (overshot by ' + (ctx.countFinished - total) + ')');
			Morebits.wiki.removeCheckpoint;
			ctx.running = false;
			return;

		ctx.statusElement.status(parseInt + '%');

		// start a new chunk if we're close enough to the end of the previous chunk, and
		// we haven't already started the next one
		if (ctx.countFinished >= (ctx.countStarted - Math.max) &&
			Math.floor(ctx.countFinished / ctx.options.chunkSize) > ctx.currentChunkIndex) {
			fnStartNewChunk;


/**

/**
Morebits.simpleWindow = function SimpleWindow {
	var content = document.createElement('div');
	this.content = content;
	content.className = 'morebits-dialog-content';
	content.id = 'morebits-dialog-content-' + Math.round(Math.random * 1e15);

	this.height = height;

	$(this.content).dialog({
		autoOpen: false,
		buttons: { 'Placeholder button': function {} },
		dialogClass: 'morebits-dialog',
		width: Math.min(parseInt, parseInt),
		// give jQuery the given height value (which represents the anticipated height of the dialog) here, so
		// it can position the dialog appropriately
		// the 20 pixels represents adjustment for the extra height of the jQuery dialog "chrome", compared
		// to that of the old SimpleWindow
		height: height + 20,
		close: function(event) {
			// dialogs and their content can be destroyed once closed
			$(event.target).dialog('destroy').remove;
		resizeStart: function {
			this.scrollbox = $(this).find('.morebits-scrollbox')[0];
			if (this.scrollbox) {
				this.scrollbox.style.maxHeight = 'none';
		resizeEnd: function {
			this.scrollbox = null;
		resize: function {
			this.style.maxHeight = '';
			if (this.scrollbox) {
				this.scrollbox.style.width = '';

	var $widget = $(this.content).dialog('widget');

	// delete the placeholder button (it's only there so the buttonpane gets created)
	$widget.find('button').each(function {
		value.parentNode.removeChild(value);

	// add container for the buttons we add, and the footer links (if any)
	var buttonspan = document.createElement('span');
	buttonspan.className = 'morebits-dialog-buttons';
	var linksspan = document.createElement('span');
	linksspan.className = 'morebits-dialog-footerlinks';
	$widget.find('.ui-dialog-buttonpane').append;

	// resize the scrollbox with the dialog, if one is present
	$widget.resizable;

Morebits.simpleWindow.prototype = {
	buttons: [],
	height: 600,
	hasFooterLinks: false,
	scriptName: null,

	/**
	focus: function {
		$(this.content).dialog('moveToTop');
		return this;

	/**
	close: function(event) {
		if (event) {
			event.preventDefault;
		$(this.content).dialog('close');
		return this;

	/**
	display: function {
		if (this.scriptName) {
			var $widget = $(this.content).dialog('widget');
			$widget.find('.morebits-dialog-scriptname').remove;
			var scriptnamespan = document.createElement('span');
			scriptnamespan.className = 'morebits-dialog-scriptname';
			scriptnamespan.textContent = this.scriptName + ' \u00B7 '; // U+00B7 MIDDLE DOT = &middot;
			$widget.find('.ui-dialog-title').prepend(scriptnamespan);

		var dialog = $(this.content).dialog('open');
		if (window.setupTooltips && window.pg && window.pg.re && window.pg.re.diff) { // tie in with NAVPOP
			dialog.parent[0].ranSetupTooltipsAlready = false;
			window.setupTooltips(dialog.parent[0]);
		this.setHeight(this.height); // init height algorithm
		return this;

	/**
	setTitle: function(title) {
		$(this.content).dialog;
		return this;

	/**
	setScriptName: function(name) {
		this.scriptName = name;
		return this;

	/**
	setWidth: function(width) {
		$(this.content).dialog;
		return this;

	/**
	setHeight: function(height) {
		this.height = height;

		// from display time onwards, let the browser determine the optimum height,
		// and instead limit the height at the given value
		// note that the given height will exclude the approx. 20px that the jQuery UI
		// chrome has in height in addition to the height of an equivalent "classic"
		// Morebits.simpleWindow
		if (parseInt(getComputedStyle($(this.content).dialog('widget')[0], null).height, 10) > window.innerHeight) {
			$(this.content).dialog.dialog;
			$(this.content).dialog;
		$(this.content).dialog('widget').find('.morebits-dialog-content')[0].style.maxHeight = parseInt + 'px';
		return this;

	/**
	setContent: function(content) {
		this.purgeContent;
		this.addContent(content);
		return this;

	/**
	addContent: function(content) {
		this.content.appendChild(content);

		// look for submit buttons in the content, hide them, and add a proxy button to the button pane
		var thisproxy = this;
		$(this.content).find.each(function {
			value.style.display = 'none';
			var button = document.createElement('button');
			button.textContent = value.hasAttribute('value') ? value.getAttribute('value') : value.textContent ? value.textContent : '쿼리 제출';
			button.className = value.className || 'submitButtonProxy';
			// here is an instance of cheap coding, probably a memory-usage hit in using a closure here
			button.addEventListener('click', function {
				value.click;
			thisproxy.buttons.push(button);
		// remove all buttons from the button pane and re-add them
		if (this.buttons.length > 0) {
			$(this.content).dialog('widget').find('.morebits-dialog-buttons').empty.append(this.buttons)[0].removeAttribute('data-empty');
			$(this.content).dialog('widget').find('.morebits-dialog-buttons')[0].setAttribute; // used by CSS
		return this;

	/**
	purgeContent: function {
		this.buttons = [];
		// delete all buttons in the buttonpane
		$(this.content).dialog('widget').find('.morebits-dialog-buttons').empty;

		while (this.content.hasChildNodes) {
			this.content.removeChild(this.content.firstChild);
		return this;

	/**
	addFooterLink: function {
		var $footerlinks = $(this.content).dialog('widget').find('.morebits-dialog-footerlinks');
		if (this.hasFooterLinks) {
			var bullet = document.createElement('span');
			bullet.textContent = ' \u2022 '; // U+2022 BULLET
			if (prep) {
				$footerlinks.prepend(bullet);
				$footerlinks.append(bullet);
		var link = document.createElement('a');
		link.setAttribute('href', mw.util.getUrl(wikiPage));
		link.setAttribute;
		link.setAttribute;
		link.textContent = text;
		if (prep) {
			$footerlinks.prepend(link);
			$footerlinks.append(link);
		this.hasFooterLinks = true;
		return this;

	/**
	setModality: function(modal) {
		$(this.content).dialog;
		return this;

/**
Morebits.simpleWindow.setButtonsEnabled = function(enabled) {
	$('.morebits-dialog-buttons button').prop;



/**

if (typeof arguments === 'undefined') { // typeof is here for a reason...
	/* global Morebits */
	window.SimpleWindow = Morebits.simpleWindow;
	window.QuickForm = Morebits.quickForm;
	window.Wikipedia = Morebits.wiki;
	window.Status = Morebits.status;