MediaWiki:Gadget-AlignTemplateParameters.js

ХӀара гӀирс Википеди чуьра бу — маьрша энциклопеди

Билгалдаккхар: Ӏалашйинчул тӀаьхьа хийцамаш га браузеран кэш цӀанйан йезаш хила мега.

  • Firefox / Safari: Shift тӀетаӀийна йитина, гӀирсийн панелан тӀера тӀетаӀайе Карлайаккха йа Ctrl+F5 йа Ctrl+R (⌘+R Mac тӀехь)
  • Google Chrome: тӀетаӀайе Ctrl+Shift+R (⌘+Shift+R Mac тӀехь)
  • Internet Explorer / Edge: тӀетаӀийна йитина Ctrl, тӀетаӀайе Карлайаккха йа тӀетаӀайе Ctrl+F5
  • Opera: тӀетаӀайе Ctrl+F5.
// <nowiki>
(function () {

// String.prototype.includes polyfill
if (!String.prototype.includes) {
	Object.defineProperty(String.prototype, 'includes', {
		value: function(search, start) {
			if (typeof start !== 'number') {
				start = 0;
			}
			
			if (start + search.length > this.length) {
				return false;
			} else {
				return this.indexOf(search, start) !== -1;
			}
		}
	});
}

var pageIsProbablyTemplate = [2, 10].includes(mw.config.get('wgNamespaceNumber'));

function alignTemplateParameters(settings) {
	settings = settings || {};
	var askOptions = settings.askOptions;
	var hardMode = settings.hardMode;
	var templateExpanderMode = settings.templateExpanderMode;
	var codeStylerMode = settings.codeStylerMode;

	var txt = '';
	var hidden = [];
	var $textbox = $('#wpTextbox1');
	var $CodeMirrorVscrollbar = $('.CodeMirror-vscrollbar');
	var winScroll = document.documentElement.scrollTop;
	var i;
	var selectionMode = false;
	
	if (typeof atpMaxWidth === 'undefined') window.atpMaxWidth = 30;
	if (typeof atpPreserveBasicSpacing === 'undefined') window.atpPreserveBasicSpacing = true;
	if (typeof atpPreserveParameterSpacing === 'undefined') window.atpPreserveParameterSpacing = true;
	if (typeof atpStandardExternalLeftSpacing === 'undefined') window.atpStandardExternalLeftSpacing = 1;
	if (typeof atpStandardInternalLeftSpacing === 'undefined') window.atpStandardInternalLeftSpacing = 1;
	if (typeof atpParserFunctionExternalLeftSpacing === 'undefined') window.atpParserFunctionExternalLeftSpacing = 0;
	if (typeof atpParserFunctionInternalLeftSpacing === 'undefined') window.atpParserFunctionInternalLeftSpacing = 1;
	if (typeof atpAutoSummary === 'undefined') window.atpAutoSummary = true;
	
	if (atpParserFunctionExternalLeftSpacing > 1) atpParserFunctionExternalLeftSpacing = 1;
	if (atpParserFunctionInternalLeftSpacing > 1) atpParserFunctionInternalLeftSpacing = 1;
	if (atpStandardExternalLeftSpacing > 1) atpStandardExternalLeftSpacing = 1;
	if (atpStandardInternalLeftSpacing > 1) atpStandardInternalLeftSpacing = 1;
	
	function isMultiline(s) {
		return s.includes('\n');
	}

	function r(r1, r2) {
		txt = txt.replace(r1, r2);
	}

	function hide(re) {
		r(re, function (s) {
			return '\x01' + (isMultiline(s) ? '_' : '') + hidden.push(s) + '\x02';
		});
	}
	
	function hideTag(tag) {
		hide(new RegExp('<' + tag + '( [^>]+)?>[\\s\\S]+?<\\/' + tag + '>', 'gi'));
	}
	
	function prettifyTemplates() {
		function prettifyTemplate() {
			function localR(r1, r2) {
				newTpl = newTpl.replace(r1, r2);
			}

			function localHide(re) {
				localR(re, function (s) {
					return '\x03' + localHidden.push(s) + '\x04';
				});
			}
			
			function localUnhide() {
				while (newTpl.match(/\x03\d+\x04/)) {
					newTpl = newTpl.replace(/\x03(\d+)\x04/g, function (s, num) {
						return localHidden[num - 1];
					});
				}
			}
			
			function isNamed(param) {
				return param.includes('=');
			}
			
			function removeEndingNewlines(s) {
				return s.replace(/\n+([ \t]*)$/, '');
			}
			
			function generateSpaces(num) {
				return ' '.repeat(Math.max(0, num));
			}
			
			function lcFirst(s) {
				if (!s) return s;
				return s[0].toLowerCase() + s.substr(1);
			}

			function calculateOneValueDominance(arr) {
				var spread = arr.reduce((obj, item) => {
					obj[item] = (obj[item] || 0) + 1;
					return obj;
				}, {});
				var topValueCount = Object.entries(spread)
					.map(([, value]) => value)
					.sort((a, b) => b - a)[0];
				return topValueCount / arr.length;
			}
			
			
			var newTpl = tpl;
			var multiline = isMultiline(newTpl);
			newTpl = newTpl.replace(/^\{\{\s+(?!\x01)/, '{{');
			if (!newTpl.match(/\{\{[a-zA-Z][^\|]*:/)) {  // например, {{fullurl:...|action=edit}}
				newTpl = newTpl.replace(/^([^\|]+?)\s+\}\}$/, '$1}}');
			}
			if (!multiline && !atpCodeStyler && !atpTemplateExpander) return newTpl;
			
			var re, m, m2, tplName, start;
			var isParserFunction = false;
			
			var anythingBeforeTplName = '(\\x01\\d+\\x02|(?:<includeonly>)?(?:safe)?subst:(?:<\\/includeonly>|<(?:includeonly|noinclude) ?\\/>))?';
			re = new RegExp('^\\{\\{' + anythingBeforeTplName + '(#\\w*)?:[ \\t]*(?=(\\n?))');
			m = newTpl.match(re);
			tplName = m && m[2];
			if (tplName) {
				isParserFunction = true;
				if (codeStylerMode && tplName !== '#invoke') {
					newTpl = newTpl.replace(re, '{{' + (m[1] || '') + tplName + ':' + (m[3] ? '' : ' '));
				}
			} else {
				if (!multiline && !atpTemplateExpander) {
					return newTpl;
				}
				m = newTpl.match(/^\{\{([^|}\n]+)/);
				tplName = m && m[1].trim();
			}
			
			var localHidden = [];
			var params;
			if (isParserFunction && atpCodeStyler && codeStylerMode) {
				localHide(/\[\[[^\]]+\]\]/g);  // [[Вики-ссылки]]
				
				params = newTpl.split('|');
				params[0] = params[0].replace(/^\{\{/, '');
				params[params.length - 1] = params[params.length - 1].replace(/\}\}$/, '');
				
				var safeToSpace = false;  // С неименованными пробелами отбивать пробелами небезопасно
				var unnamedCount = 0;
				if (tplName === '#invoke' && codeStylerMode === 'newlines') {
					safeToSpace = true;
					for (i = 2; i < params.length; i++) {
						if (!isNamed(params[i])) {
							safeToSpace = false;
							break;
						}
					}
				}

				// Make #if, #ifexpr, #ifeq, #ifexist, #iferror multiline only if both arguments are non-empty, or if
				// it contains nested templates / parser functions
				var makeIfMultiline = codeStylerMode === 'newlines' &&
					(tplName.substr(0, 3) === '#if' &&
						params[1] !== undefined &&
						(isMultiline(params[1]) ||
							tplName === '#ifeq' ||
							(params[2] !== undefined &&
								isMultiline(params[2])
							) ||
							((params[1].trim() !== '' &&
								params[2] !== undefined &&
								params[2].trim() !== ''
							) ||
								params[1].includes('\x01_') ||
								(params[2] !== undefined &&
									params[2].includes('\x01_')
								)
							)
						)
					);
				
				var isMultipleAlternativesSwitch = false;
				var isOneOfSwitchAlternatives;
				// FIXME: это надо рефакторить, конечно.
				// Выше, видимо, safeToSpace должен мочь стать true не только при codeStylerMode === 'newlines'
				if (params.length > 1) {
					for (i = 1; i < params.length; i++) {
						// FIXME: криво, да, что анализируется предыдущий параметр, но связано с логикой ниже
						if (tplName === '#switch' && i - 1 !== 0 && !params[i - 1].includes('=')) {
							isMultipleAlternativesSwitch = true;
							isOneOfSwitchAlternatives = true;
						} else {
							isOneOfSwitchAlternatives = false;
						}
						if (tplName === '#switch' ||
							(tplName === '#invoke' &&
								(safeToSpace ||
									params[i].match(/\n[ \t]*$/) &&
									params[i - 1].match(/\n[ \t]*$/)
								)
							)
						) {
							params[i] = params[i].replace(/([ \t]*)=[ \t]*/, function (s, m1) {
								var sp;
								if (tplName === '#invoke' &&
									codeStylerMode === 'newlines' &&
									!isMultipleAlternativesSwitch
								) {
									sp = generateSpaces(i);
								} else if (m1.length < 1 ||
									codeStylerMode === 'newlines-collapse' ||
									!(params[i].match(/\n[ \t]*$/) &&
										params[i - 1].match(/\n[ \t]*$/)
									) ||
									isMultipleAlternativesSwitch
								) {
									sp = ' ';
								} else {
									sp = m1;
								}
								return sp + '= ';
							});
						} else if (tplName === '#invoke') {
							params[i] = params[i]
								.replace(/[ \t]+=/, ' =')
								.replace(/=[ \t]+/, '= ');
						}
						if (codeStylerMode === 'newlines-collapse' && !(tplName === '#invoke' && !multiline)) {
							params[i - 1] = params[i - 1].replace(/\s*$/, ' ');
							// {{#if: smth |\nsmth\n}}
							params[i] = params[i].replace(/^[ \t]*(?=(\n)?)/, function (s, m1) {
								return m1 ? '' : ' ';
							});
							if (i === params.length - 1) {
								params[i] = params[i].replace(/\s*$/, ' ');
							}
						} else if (!isMultiline(params[i - 1]) ||
							params[i - 1].match(/\S[ \t]*$/) ||
							(tplName === '#switch' && isOneOfSwitchAlternatives)
						) {
							if (codeStylerMode === 'newlines' &&
								((tplName.substr(0, 3) === '#if' && makeIfMultiline) ||
									(tplName === '#switch' && !isOneOfSwitchAlternatives) ||
									(tplName === '#invoke' && safeToSpace && i !== 1)
								)
							) {
								params[i - 1] = params[i - 1].replace(/[ \t]*$/, '\n');
								multiline = true;
							} else if (tplName === '#switch' && isOneOfSwitchAlternatives) {
								params[i - 1] = ' ' + params[i - 1].trim() + ' ';
							} else {
								if (!(tplName === '#invoke' && !safeToSpace)) {
									params[i - 1] = params[i - 1].replace(/[ \t]*$/, ' ');
								} else if (isNamed(params[i - 1]) || i - 1 <= 1) {
									params[i - 1] = params[i - 1].replace(/[ \t]+$/, ' ');
								}
							}
							if (!(tplName === '#invoke' && !safeToSpace)) {
								// {{#if: smth |\nsmth\n}}
								params[i] = params[i].replace(/^[ \t]*(?=(\n)?)/, function (s, m1) {
									return m1 ? '' : ' ';
								});
							} else if (isNamed(params[i]) || i === 1) {
								params[i] = params[i].replace(/^[ \t]+/, ' ');
							}
							if (i === params.length - 1) {
								if (!(tplName === '#invoke' && !safeToSpace)) {
									params[i] = params[i].replace(/(\S)[ \t]*$/, '$1 ');
								} else if (isNamed(params[i])) {
									params[i] = params[i].replace(/(\S)[ \t]+$/, '$1 ');
								}
							}
						}
					}
				} else {
					params[0] = params[0].replace(/\s*$/, ' ');
				}
				if (tplName === '#expr' || tplName === '#ifexpr') {
					params[0] = params[0]
						// Не трогать +30 * -7
						.replace(/: /, ':')
						.replace(/[ \t]*(\+|-|\*|\/|\^|<>|!=|>=|<=|>|<|=)[ \t]*/g, ' $1 ')
						.replace(/([+\-*/^=<>:])[ \t]*(\+|-) /g, '$1 $2')
						.replace(/:[ \t]*/, ': ');
				}
				newTpl = '{{' + params.join('|') + '}}';
				
				if (params.length > 1) {
					if (tplName !== '#invoke') {
						newTpl = newTpl.replace(/\|[ \t]\}\}/, '}}');
					}
					if (tplName === '#invoke' && (safeToSpace || codeStylerMode === 'newlines-collapse')) {
						newTpl = newTpl.replace(
							new RegExp('^\\{\\{' + anythingBeforeTplName + '#invoke:[ \\t]*'),
							'{{$1#invoke: '
						);
					} else {
						newTpl = newTpl.replace(
							new RegExp('^\\{\\{' + anythingBeforeTplName + '#invoke:[ \\t]+'),
							'{{$1#invoke: '
						);
					}
					if (tplName !== '#switch') {
						newTpl = newTpl.replace(/\|  ?\|/g, '||');
					}
				}
				
				localUnhide();
				//console.log(newTpl);
				if (!multiline) {
					return newTpl;
				}
			}
			
			if (!isParserFunction && atpTemplateExpander && templateExpanderMode) {
				localHide(/\[\[[^\]]+\]\]/g);  // [[Вики-ссылки]]
				
				params = newTpl.split('|');
				params[0] = params[0].replace(/^\{\{/, '');
				params[params.length - 1] = params[params.length - 1].replace(/\}\}$/, '');
				
				var namedCount = 0, unnamedCount = 0;
				var hasCollapsed = false;
				var isFullyCollapsible = true;
				for (i = 1; i < params.length; i++) {
					if (isNamed(params[i])) {
						namedCount++;
						if (!isMultiline(params[i - 1])) {
							hasCollapsed = true;
						}
					} else {
						unnamedCount++;
					}
					if ((isNamed(params[i]) &&
							isMultiline(params[i].trim())
						) ||
						(!isNamed(params[i]) &&
							isMultiline(params[i]) &&
							params[i + 1] &&
							!isNamed(params[i + 1]) &&
							isMultiline(params[i + 1])
						)
					) {
						isFullyCollapsible = false;  // Можно ли свернуть в одну строчку этот шаблон
					}
				}
				
				if (namedCount > 1 && !(templateExpanderMode === 'expand' && !hasCollapsed)) {
					if (templateExpanderMode === 'expand') {
						for (i = 0; i < params.length; i++) {
							if (i === 0) {
								params[i] = params[i].replace(/\s*$/, '');
							}
							if ((params[i + 1] &&
									isNamed(params[i + 1])
								) ||
								(i === 0 &&
									(params[i + 1] &&
										params[i + 2] &&
										!isNamed(params[i + 1]) &&
										!isNamed(params[i + 2]) &&
										isMultiline(params[i + 1])
									)
								) ||
								(i !== 0 &&
									(isNamed(params[i]) ||
										lcFirst(tplName) === 'публикация'
									)
								)
							) {
								if (isNamed(params[i]) || lcFirst(tplName) === 'публикация') {
									params[i] = params[i].replace(/\s*$/, '');
								} else {
									params[i] = removeEndingNewlines(params[i]);
								}
								params[i] = params[i] + '\n' + generateSpaces(stack.length * 2 + selectionSpacing) +
									' ';
								if (unnamedCount <= 1 && params[i + 1] && isNamed(params[i + 1])) {
									params[i + 1] = ' ' + params[i + 1].trim();
								}
								if (i !== 0) {
									// Для последующего выравнивания
									params[i] = params[i].replace('=', generateSpaces(i) + '=');
								}
							}
						}
						hasCollapsed = false;
					} else if (templateExpanderMode === 'collapse') {
						for (i = 0; i < params.length; i++) {
							if (i === 0 ||
								isNamed(params[i]) ||
								(params[i + 1] &&
									(isNamed(params[i + 1]) ||
										!isMultiline(params[i + 1])
									)
								) ||
								(i === params.length - 1 &&
									isFullyCollapsible
								)
							) {
								if (i === 0 || isNamed(params[i])) {
									if (isFullyCollapsible ||
										(i !== params.length - 1 &&
											!(params[i + 1] &&
												!isNamed(params[i + 1]) &&
												isMultiline(params[i + 1]) &&
												params[i + 2] &&
												!isNamed(params[i + 2]) &&
												isMultiline(params[i + 2])
											)
										)
									) {
										params[i] = params[i].trim();
									} else {
										params[i] = params[i]
											.replace(/^\s*/, '')
											.replace(/\s*?(\n*)$/, '$1');
									}
								// Иногда неименованные параметры вытягиваются в стопку
								} else if (i !== 0 && !isNamed(params[i]) && isMultiline(params[i]) &&
									(isFullyCollapsible ||
										params[i + 1] &&
										(isNamed(params[i + 1]) ||
											!isMultiline(params[i + 1])
										) &&
										(i === 1 ||
											isNamed(params[i - 1]) ||
											!isMultiline(params[i - 1])
										)
									)
								) {
									params[i] = removeEndingNewlines(params[i]);
								}
							}
							if (i !== 0) {
								params[i] = params[i].replace(/[ \t]*=[ \t]*/, '=');
							}
							if (!pageIsProbablyTemplate &&
								(!unnamedCount &&
									isNamed(params[Math.max(1, i)]) ||
									(lcFirst(tplName) === 'публикация')
								)  // && i !== params.length - 1
							) {
								params[i] = params[i].replace(/ *$/, ' ');
							}
						}
						hasCollapsed = true;
					}
					
					newTpl = '{{' + params.join('|') + '}}';
				}
				
				localUnhide();
				//console.log(newTpl);
				if (hasCollapsed) {
					return newTpl;
				}
			}
			
			m = newTpl.match(/^(\{\{[\s\S]*?\n\s*)(\||\}\})/);
			if (!m) return newTpl;
			
			start = m[1];
			if (!templateExpanderMode) {
				start = start.trim();
			} else {
				start = removeEndingNewlines(start);
			}
			
			var indentSpaces = ' '.repeat(basicLevel);
			if (!refTagBefore) {
				// Вставляем то или иное число пробелов в зависимости от уровня вложенности
				if (isParserFunction) {
					indentSpaces += generateSpaces(stack.length);  // Знак «|»
					indentSpaces += generateSpaces(stack.length * atpParserFunctionExternalLeftSpacing);
					indentSpaces += generateSpaces(stack.length * atpParserFunctionInternalLeftSpacing);
				} else {
					indentSpaces += generateSpaces(stack.length * 2);
				}
				indentSpaces += generateSpaces(selectionSpacing);
			}
			
			var noEndingBrackets = false;
			if (!newTpl.includes('}}', newTpl.length - 2)) {
				newTpl += '}}';
				noEndingBrackets = true;
			}
			
			if (tplName === '#switch') {
				// Для правильного выравнивания «=» в «| a | b | c = value»
				re = /\n([ \t]*)\|([ \t]*)(?:([^=\n]+?)([ \t]*)=([ \t]*))?([\s\S]*?)(?=(\n[ \t]*\||\}\}))/g;
			} else {
				re = /\n([ \t]*)\|([ \t]*)(?:([^=|\n]+?)([ \t]*)=([ \t]*))?([\s\S]*?)(?=(\n[ \t]*\||\}\}))/g;
			}
			params = [];
			var longestParameterLength = 0;
			var standardWidth = 0;
			var length;
			var basicExternalLeftSpacing;
			var basicInternalLeftSpacing;
			var oneSpaceCount = 0;
			var zeroSpacesCount = 0;
			var paramLeftLengths = [];
			var namedParametersCount = 0;
			i = 0;
			while (m = re.exec(newTpl)) {
				i++;
				if (i === 1) {
					basicExternalLeftSpacing = isParserFunction
						? generateSpaces(indentSpaces.length + atpParserFunctionExternalLeftSpacing)
						: m[1];
					basicInternalLeftSpacing = isParserFunction
						? generateSpaces(atpParserFunctionInternalLeftSpacing)
						: m[2];
				}
				if (thisPreserveBasicSpacing) {
					/* Этот код гарантирует, что:
					   1) внешняя левая отбивка не будет размножаться во вложенных шаблонах
					   2) не будет строк с меньшей внешней левой отбивкой, чем у первой строки
					   3) не будет внешней левой отбивки из более чем одного пробела на ровном месте */
					if (i === 1) {
						m[1] = '';
					} else {
						// Случаи, когда параметры отбиваются по отношению к базовому уровню специально.
						// Но у всех аргументов парсерных функций отступ одинаковый
						if (isParserFunction || !thisPreserveParameterSpacing) {
							m[1] = '';
						} else if (thisPreserveParameterSpacing) {
							m[1] = generateSpaces(m[1].length - basicExternalLeftSpacing.length);
						}
					}
					if (basicExternalLeftSpacing.length > indentSpaces.length) {
						m[1] += ' ';
					}
				} else {
					if (isParserFunction) {
						m[1] = '';
						// FIXME: криво, что стандартный отступ парсерных функций формируется через
						// basicExternalLeftSpacing, а не напрямую через atpParserFunctionExternalLeftSpacing (также выше)
						if (basicExternalLeftSpacing.length > indentSpaces.length) {
							m[1] += ' ';
						}
					} else if (thisPreserveParameterSpacing) {
						m[1] = generateSpaces(
							atpStandardExternalLeftSpacing + m[1].length - basicExternalLeftSpacing.length
						);
					} else {
						m[1] = generateSpaces(atpStandardExternalLeftSpacing);
					}
				}
				
				if (thisPreserveBasicSpacing) {
					// Аналогично для внутренней левой отбивки
					if (i === 1) {
						m[2] = '';
					} else {
						if (isParserFunction || !thisPreserveParameterSpacing) {
							m[2] = '';
						} else if (thisPreserveParameterSpacing) {
							m[2] = generateSpaces(m[2].length - basicInternalLeftSpacing.length);
						}
					}
					if (basicInternalLeftSpacing.length > 0) {
						m[2] += ' ';
					}
				} else {
					if (thisPreserveParameterSpacing) {
						m[2] = generateSpaces(
							atpStandardInternalLeftSpacing + m[2].length - basicInternalLeftSpacing.length
						);
					} else {
						m[2] = generateSpaces(atpStandardInternalLeftSpacing);
					}
				}
				
				if (!hardMode || codeStylerMode || templateExpanderMode) {
					paramLeftLengths.push((m[1] + m[2] + m[3] + m[4]).length);
					if (m[4] === ' ') {
						oneSpaceCount++;
					}
					if (m[4] === '' && m[5] === '') {
						zeroSpacesCount++;
					}
				}
				
				if (m[3] !== undefined) {
					length = m[1].length + m[2].length + m[3].length;
					if (length > longestParameterLength) {
						longestParameterLength = length;
					}
					
					if (!hardMode || codeStylerMode || templateExpanderMode) {
						namedParametersCount++;
					}
				}
				
				if (m[3] === undefined && m[6].match(/^[ \t]*\n/)) {
					m[6] = m[6].replace(/^\n\s*\x01/, '\x01');
				}
				
				m[6] = m[6]
					.replace(/\n\s*\n\s*$/, '\n')  // В сумме 3+ переноса строки
					.replace(/^[ \t]+$/, '');  // Пустой параметр (но нельзя обрезать, если там переносы)
				
				if (m[3] !== undefined) {
					m[6] = m[6]
						// Оканчивающие пробелы во всех завершающих (состоящих только из пробелов) строках
						.replace(/[ \t]*(\n?)[ \t]*$/, '$1')
						// Оканчивающий пробел в параметрах lat_*, lon_* и т. п.
						.replace(/(\|.*=)(\n?)$/, '$1 $2');
				}
				
				params.push([m[1], m[2], m[3], m[6]]);
			}
			if (!params[0]) {
				return newTpl;
			}
			
			standardWidth = Math.min(thisMaxWidth, longestParameterLength);
			if (!hardMode || codeStylerMode || templateExpanderMode) {
				if (namedParametersCount !== 0 && calculateOneValueDominance(paramLeftLengths) <= 0.75) {
					if (oneSpaceCount / namedParametersCount > 0.75) {
						standardWidth = 0;
					}
					if (zeroSpacesCount / namedParametersCount > 0.75) {
						standardWidth = -1;
					}
				}
			}

			newTpl = '';
			if (isMultiline(trailingSpaceBefore)) {
				m = trailingSpaceBefore.match(/ *$/);
				var trailingNormalSpacesLength = m ? m[0].length : 0;
				if (trailingNormalSpacesLength < indentSpaces.length) {
					newTpl += generateSpaces(indentSpaces.length - trailingNormalSpacesLength);
				} else {
					newLeft -= trailingNormalSpacesLength - indentSpaces.length;
				}				
			}
			newTpl += start + '\n';
			//console.log(params);
			
			if (params.length !== 1 && params[params.length - 1][2] && params[params.length - 1][3]) {
				params[params.length - 1][3] = params[params.length - 1][3]
					.replace(/\s*$/, '')
					// Ещё раз оканчивающий пробел в параметрах lat_*, lon_* и т. п.
					.replace(/(\|.*=)(\n?)$/, '$1 $2');
			} else if (isParserFunction) {
				params[params.length - 1][3] = params[params.length - 1][3].replace(/\s*$/, '');
			} else {
				params[params.length - 1][3] = removeEndingNewlines(params[params.length - 1][3]);
			}
			
			var line;
			var NUMBERED_PARAMS = [
				'заголовок',
				'метка',
				'текст',
				'викиданные',
				'группа', 'group',
				'список', 'list',
				'блок',
				'класс',
				'класс_заголовка', 'класс заголовка',
				'класс_метки', 'класс метки',
				'класс_текста', 'класс текста',
				'класс_списка', 'класс списка', 'listclass',
				'стиль_заголовка', 'стиль заголовка',
				'стиль_метки', 'стиль метки',
				'стиль_текста', 'стиль текста',
				'стиль_группы', 'стиль группы', 'groupstyle',
				'стиль_списка', 'стиль списка', 'liststyle',
			];
			var paramName, numberMatches, number, numberedParamCurrent, numberedParamCounter, paramNameCount,
				paramGroupDuplicatesCount, isDuplicate;
			var paramNamesMet = [];
			var paramGroupDuplicateNumbersMet = [];
			for (i = 0; i < params.length; i++) {
				if (params[i][2]) {
					paramName = params[i][2];
					if (!NUMBERED_PARAMS.includes(paramName) &&
						NUMBERED_PARAMS.includes(paramName.replace(/\d+/, ''))
					) {
						numberMatches = paramName.match(/\d+/);
						number = Number(numberMatches[0]);
						
						isDuplicate = paramNamesMet.includes(paramName);
						if (isDuplicate) {
							paramNameCount = paramNamesMet.filter(function (value) {
								return value === paramName;
							}).length;
							paramGroupDuplicatesCount = paramGroupDuplicateNumbersMet.filter(function (value) {
								return value === number;
							}).length;
						}
						
						if (numberedParamCurrent === undefined) {
							numberedParamCounter = 1;
						} else if (
							// В numberedParamCurrent предыдущий номер.
							number !== numberedParamCurrent ||
							// Это условие позволяет адекватно разбирать многочисленные дубли имён параметров. Оно
							// идентифицирует случаи, когда некий параметр повторяется больше раз, чем уже
							// существует групп для этого номера, чтобы повысить счётчик на 1.
							(isDuplicate && paramNameCount > paramGroupDuplicatesCount)
						) {
							numberedParamCounter++;
							if (isDuplicate) {
								paramGroupDuplicateNumbersMet.push(number);
							}
						}
						
						numberedParamCurrent = number;
						paramNamesMet.push(paramName);
						
						paramName = paramName.replace(/\d+/, numberedParamCounter);
						params[i][2] = paramName;
					}
				}
				
				line = indentSpaces + params[i][0] + '|' + params[i][1] + (params[i][2] || '');
				if (params[i][2]) {
					if (!params[i][2].match(/^(lat|lon)_/) || !params[i][3].includes('|')) {
						line += generateSpaces(
							standardWidth - (params[i][2].length + params[i][0].length + params[i][1].length)
						);
					}
					if (standardWidth !== -1) {
						line += ' ';
					}
					line += '=';
					if (standardWidth !== -1 && (!params[i][3].match(/^\n(?!<!--)/) || params[i][3].trim() === '')) {
						line += ' ';
					}
				}
				line += params[i][3] + '\n';
				newTpl += line;
			}
			if (!noEndingBrackets) {
				newTpl += indentSpaces + '}}';
			}
			
			return newTpl;
		}
		
		var initialTxt = txt;
		
		
		var thisMaxWidth, thisPreserveBasicSpacing, thisPreserveParameterSpacing, selectionSpacing = 0, basicLevel = 0;
		if (askOptions) {
			if (!templateExpanderMode && !codeStylerMode) {
				if (selectionMode) {
					selectionSpacing = parseInt(prompt('Базовая отбивка значений параметров в пробелах:', '0'));
					if (isNaN(selectionSpacing)) {
						selectionSpacing = 0;
					}
				}
				
				thisMaxWidth = parseInt(prompt('Максимально допустимая стандартная ширина колонки названий параметров, в символах. (Значение по умолчанию для этой настройки можно указать в своём common.js в переменной atpMaxWidth.)', atpMaxWidth));
				if (isNaN(thisMaxWidth)) {
					thisMaxWidth = atpMaxWidth;
				}
				
				thisPreserveBasicSpacing = confirm('Сохранять отбивку вокруг знака «|» в начале строки? (Значение по умолчанию для этой настройки [true/false, сейчас ' + atpPreserveBasicSpacing + '] можно указать в своём common.js в переменной atpPreserveBasicSpacing.)');
				thisPreserveParameterSpacing = confirm('Сохранять отбивку вокруг знака «|» в начале строки у отдельных параметров по отношению к базовому уровню (параметры некоторых шаблонов визуально группируются таким образом)? (Значение по умолчанию для этой настройки [true/false, сейчас ' + atpPreserveParameterSpacing + '] можно указать в своём common.js в переменной atpPreserveParameterSpacing.)');
			} else {
				thisMaxWidth = atpMaxWidth;
				thisPreserveBasicSpacing = atpPreserveBasicSpacing;
				thisPreserveParameterSpacing = atpPreserveParameterSpacing;
			}
			basicLevel = parseInt(prompt('Базовая отбивка строк', '0'));
			if (isNaN(basicLevel) || basicLevel < 0) {
				basicLevel = 0;
			}
		} else {
			thisMaxWidth = atpMaxWidth;
			thisPreserveBasicSpacing = atpPreserveBasicSpacing;
			thisPreserveParameterSpacing = atpPreserveParameterSpacing;
		}
		
		
		if (!pageIsProbablyTemplate) {  // заготовки карточек
			hideTag('pre');
		}
		hideTag('nowiki');
		hideTag('source');
		hideTag('syntaxhighlight');
		hideTag('templatedata');

		hideTag('code');
		hideTag('kbd');
		hideTag('tt');

		hideTag('graph');
		hideTag('hiero');
		hideTag('math');
		hideTag('timeline');
		hideTag('chem');
		hideTag('score');
		hideTag('categorytree');
		hide(/<!--[\s\S]*?-->/g);
		
		if (atpCodeStyler && codeStylerMode) {
			r(/^;(?=[^ ])/mg, '; ');
			
			r(/(\sstyle=)([\'"])([^<>\2]+?)\2([^<>]*?)/g, function (s, m1, m2, m3, m4) {
				if (/\{\{ *#/.test(s)) {
 					return s;
				}
				m3 = m3
					.replace(/;+/g, ';')
					.replace(/; */g, '; ')
					.replace(/ *$/, '')
					.replace(/([^;}])$/, '$1;')
					.replace(/#[0-9A-F]{6}/, function (m) {
						return m.toLowerCase();
					})
					.replace(/#[0-9A-F]{3}/, function (m) {
						return m.toLowerCase();
					});
				return m1 + '"' + m3 + '"' + m4;
			});
		}
		
		var stack = [],
			tpl,
			left,
			right,
			trailingSpaceBefore,
			refTagBefore,
			leftRe = /\{\{(?:\{(?!\{|!)|(?!\{!))/g,  // учитываем {{{{{|subst:}}}template}}, {{{!}} (чтобы получить {|).
			                                         // жертвуем тем, что {{{!}}} будет считаться за { + {{!}} + }
			leftM,
			bracketsLength = 0,
			lastStackBrackets,
			stackElem,
			newLeft;
		while (true) {
			lastStackBrackets = stack.length && stack[stack.length - 1][1] === 3 ? '}}}' : '}}';
			right = txt.indexOf(lastStackBrackets, leftRe.lastIndex);
			
			leftM = leftRe.exec(txt);
			if (leftM) {
				bracketsLength = leftM[0].length;
				left = leftM.index;
			} else {
				left = -1;
			}
			
			if (left === -1 && right === -1 && !stack.length) {
				break;
			}
			if (left !== -1 && (left < right || right === -1)) {
				stack.push([left, bracketsLength]);
			} else {
				stackElem = stack.pop();
				if (stackElem) {
					left = stackElem[0];
					bracketsLength = stackElem[1];
				} else {
					if (right === -1) {
						continue;
					} else {
						left = 0;
						bracketsLength = 2;
					}
				}
				if (right === -1) {
					right = txt.length;
				}
				right += bracketsLength;
				tpl = txt.substring(left, right);
				trailingSpaceBefore = '';
				if (isMultiline(tpl) || templateExpanderMode) {
					var m = txt.substring(0, left).match(/(\S)\s*$/);
					if (m && m[1] === '=') {
						trailingSpaceBefore = m[0].substr(1);
					}
					refTagBefore = txt.substring(0, left).match(/\n<ref( [\w ]+?=[^<>]+?| *\/?)>\s*$/);
				}
				newLeft = left;
				var index;
				var isParserFunction = false;
				var isMultilineTemplate = isMultiline(tpl);
				if (bracketsLength === 2) {
					isParserFunction = /^\{\{\s*#/.test(tpl);
					index = hidden.push(prettifyTemplate());
				} else {
					index = hidden.push(
						tpl.replace(/^\{\{\{\s*([^}|]*?)\s*(?:\|\s*([^}]*?)\s*)?\}\}\}$/, function (s, m1, m2) {
							return '{{{' + m1 + (m2 === undefined ? '' : '|' + m2) + '}}}';
						})
					);
				}
				// Add "_" to the indexes of templates that should be taken into account when determining the
				// makeIfMultiline variable value.
				txt = txt.substring(0, newLeft) + '\x01' + (isParserFunction || isMultilineTemplate ? '_' : '') +
					index + '\x02' + txt.substr(right);
				leftRe.lastIndex = right - tpl.length;
			}
		}
		//console.log(hidden);
		
		while (txt.match(/\x01_?\d+\x02/)) {
			txt = txt.replace(/\x01_?(\d+)\x02/g, function (s, num) {
				return hidden[num - 1];
			});
		}
		
		if (atpAutoSummary && txt !== initialTxt) {
			var summaryValue = $('#wpSummary').val();
			if (!/Код кечйар/.test(summaryValue)) {
				$('#wpSummary').val(summaryValue + (/[^,; \/]$/.test(summaryValue) ? ', ' : '') + 'Скриптан гӀоьнца код кечйар');
			}
		}
	}

	function processAllText() {
		if (templateExpanderMode && !selectionMode) {
			var expandOrCollapseText = templateExpanderMode === 'expand' ? 'развернуть' : 'свернуть';
			if (!confirm('Вы уверены, что хотите ' + expandOrCollapseText + ' все шаблоны на странице? Чтобы ' + expandOrCollapseText + ' один шаблон или группу шаблонов, выделите только их.')) return;
		}
		txt = $textbox.textSelection('getContents').replace(/^\s+/, '');
		prettifyTemplates();

		// 2017 wikitext editor adds an empty line to the end with every text replacement. Remove
		// the following block when [[phab:T198010]] is fixed.
		if (window.ve && ve.init && ve.init.target && ve.init.target.active) {
			r(/[\n\r]+$/, '');
		}

		$textbox.textSelection('setContents', txt);
		if (caretPosition) {
			$textbox.textSelection('setSelection', {
				start: caretPosition[0] > txt.length ? txt.length : caretPosition[0]
			});
		}
	}
	
	
	$textbox.focus();

	var caretPosition = $textbox.textSelection('getCaretPosition', { startAndEnd: true });
	if (caretPosition) {
		var textScroll = ($CodeMirrorVscrollbar.length ? $CodeMirrorVscrollbar : $textbox).scrollTop();
		if (caretPosition[0] === caretPosition[1]) {
			processAllText();
		} else {
			txt = $textbox.textSelection('getSelection');
			prettifyTemplates();
			$textbox
				.textSelection('replaceSelection', txt)
				.textSelection('setSelection', {
					start: caretPosition[0],
					end: caretPosition[0] + txt.length
				});
		}
		($CodeMirrorVscrollbar.length ? $CodeMirrorVscrollbar : $textbox).scrollTop(textScroll);
	// If something went wrong
	} else if (confirm('Скрипт обработает ВЕСЬ текст на этой странице. Продолжить?')) {
		processAllText();
	}

	// scroll back, for 2017 wikitext editor, IE, Opera
	document.documentElement.scrollTop = winScroll;
}


mw.loader.using( [ 'ext.gadget.registerTool', 'jquery.textSelection' ] ).done( function () {
	if (typeof atpTemplateExpander === 'undefined') window.atpTemplateExpander = true;
	if (typeof atpCodeStyler === 'undefined') window.atpCodeStyler = false;
	
	registerTool( {
		name: 'atp',
		position: 150,
		title: 'Выровнять параметры шаблонов',
		label: 'Выровнять параметры шаблонов. Выделите шаблон(ы), чтобы выровнять только их. Ctrl+клик, чтобы выровнять параметры даже тех шаблонов, где выравнивание не используется. Alt+клик, чтобы вызвать настройки',
		callback: function () {},
		classic: {
			icon: '//upload.wikimedia.org/wikipedia/commons/5/59/AlignTemplateParameters.js_icon.png',
			addCallback: function () {
				 $( '.tool-button[rel="atp"]' )
					.off('click')
					.on('click', function (e) {
						alignTemplateParameters({
							askOptions: e.altKey,
							hardMode: e.ctrlKey || e.metaKey,
						});
						e.preventDefault();
					});
			},
		},
		visual: {
			icon: '//upload.wikimedia.org/wikipedia/commons/thumb/1/14/AlignTemplateParameters.js_VE_icon.svg/20px-AlignTemplateParameters.js_VE_icon.svg.png',
			modes: [ 'source' ],
			callback: alignTemplateParameters,
		},
	} );
	
	if (atpTemplateExpander) {
		if (typeof atpSwapExpandCollapseTemplates === 'undefined') window.atpSwapExpandCollapseTemplates = false;
		
		registerTool( {
			name: 'atpTemplateExpander',
			position: 151,
			label: !atpSwapExpandCollapseTemplates
				? 'Развернуть шаблоны в многострочные и выровнять параметры шаблонов. Shift+клик, чтобы свернуть'
				: 'Свернуть шаблоны в однострочные и выровнять параметры шаблонов. Shift+клик, чтобы развернуть',
			icon: '//upload.wikimedia.org/wikipedia/commons/8/8b/AlignTemplateParameters.js_templateExpander.png',
			callback: function () {},
			classic: {
				addCallback: function () {
					$( '.tool-button[rel="atpTemplateExpander"]' )
						.off('click')
						.on('click', function (e) {
							alignTemplateParameters({
								askOptions: e.altKey,
								templateExpanderMode: e.shiftKey ^ atpSwapExpandCollapseTemplates ? 'collapse' : 'expand',
							});
							e.preventDefault();
						});
				},
			},
		} );
	}
	
	if (atpCodeStyler) {
		registerTool( {
			name: 'atpCodeStyler',
			position: 152,
			label: 'Оформить функции парсера и выровнять параметры шаблонов. Ctrl+клик, чтобы развернуть их в многострочные. Ctrl+Shift+клик, чтобы свернуть',
			icon: '//upload.wikimedia.org/wikipedia/commons/6/68/AlignTemplateParameters.js_codeStyler.png',
			callback: function () {},
			classic: {
				addCallback: function () {
					$( '.tool-button[rel="atpCodeStyler"]' )
						.off('click')
						.on('click', function (e) {
							alignTemplateParameters({
								askOptions: e.altKey,
								codeStylerMode: (e.ctrlKey || e.metaKey) ? (e.shiftKey ? 'newlines-collapse' : 'newlines') : 'spaces',
							});
							e.preventDefault();
						});
				},
			},
		} );
	}
} );

}());
// </nowiki>