Перейти к содержимому страницы
Открыть меню
Переключить меню настроек
Открыть персональное меню
Вы не представились системе
Ваш IP-адрес будет виден всем, если вы внесёте какие-либо изменения.

Модуль:Sports table/argcheck

Материал из вики «Вече»

Для документации этого модуля может быть создана страница Модуль:Sports table/argcheck/doc

-- Проверка параметров турнирной таблицы и самостоятельной шахматки.

-- Ищет неизвестные и недействующие параметры, ссылки на необъявленные команды, названия,
-- примечания и результаты без владельца, а также неканонические логические значения,
-- нестандартную запись счёта, цвета вне палитры, ручное оформление,
-- непереведённые сокращения, ручные ширины столбцов и незавершённый турнир.

-- Сам модуль ничего не выводит: a.check возвращает два списка ссылок на категории.
-- Первый — предупреждения, их вызывающий модуль в предпросмотре показывает текстом;
-- второй — только отслеживающие категории. Вызывают его [[Модуль:Sports table]] — для таблицы
-- и перед передачей вызова шахматке при show_matches=only — и [[Модуль:Sports results]]
-- для самостоятельной шахматки. Ставить ли категории на странице, решает вызывающий модуль.

-- Перечень категорий и условия попадания в них:
-- [[Модуль:Sports table/doc#Отслеживающие категории]].

local a = {}

-- Из [[Модуль:Sports table/sub]] берутся разбор цвета палитры, поиск ручного оформления,
-- проверка логических значений и код строки итогов.
local p_sub = require('Модуль:Sports table/sub')

-- Категории -----------------------------------------------------------------------------

-- Категории по родам находок; несколько родов могут делить одну категорию.
local categories = {
	ignored_parameters = 'Категория:Страницы, использующие спортивные таблицы с недействующими параметрами',
	missing_results = 'Категория:Страницы, использующие спортивные таблицы с ошибками в описании квалификационного раунда',
	missing_teams = 'Категория:Страницы, использующие спортивные таблицы с отсутствующими командами',
	orphaned_match_notes = 'Категория:Страницы, использующие спортивные таблицы с некорректными параметрами',
	orphaned_names = 'Категория:Страницы, использующие спортивные таблицы с некорректными параметрами',
	orphaned_notes = 'Категория:Страницы, использующие спортивные таблицы с некорректными параметрами',
	orphaned_results = 'Категория:Страницы, использующие спортивные таблицы с ошибками в описании квалификационного раунда',
	possible_incorrect_winpoints = 'Категория:Страницы, использующие спортивные таблицы с возможно неверным количеством очков',
	possibly_ignored_parameters = 'Категория:Страницы, использующие спортивные таблицы с возможно игнорируемыми параметрами',
	unknown_parameters = 'Категория:Страницы, использующие спортивные таблицы с неизвестными параметрами',
	bad_boolean = 'Категория:Страницы, использующие спортивные таблицы с нестандартными логическими значениями',
	bad_score = 'Категория:Страницы, использующие спортивные таблицы с нестандартной записью счёта',
	useless_params = 'Категория:Страницы, использующие спортивные таблицы с параметрами без эффекта',
	bad_color = 'Категория:Страницы, использующие спортивные таблицы с цветами вне палитры',
	manual_format = 'Категория:Страницы с ручным оформлением в спортивных шаблонах',
	unrecognised_style = 'Категория:Страницы, использующие спортивные таблицы с нераспознанным стилем',
	untranslated_short = 'Категория:Страницы, использующие спортивные таблицы с непереведёнными сокращениями',
	manual_teamwidth = 'Категория:Страницы, использующие спортивные таблицы с ручной шириной столбцов',
	unfinished_update = 'Категория:Страницы, использующие спортивные таблицы с незавершённым турниром',
	adjust_and_start = 'Категория:Страницы, использующие спортивные таблицы с настройками и начальными точками'
}

-- Допустимые параметры ------------------------------------------------------------------

-- Допустимые параметры турнирной таблицы, общие для всех стилей, вместе с параметрами
-- соседних модулей, которые приходят в тот же вызов. a.check накладывает на копию набора
-- надстройку стиля и параметры шахматки; сам набор не меняется.
local good_args = {
	['away_goals_header'] = 1,
	['baselink'] = 1,
	['bonus_header'] = 1,
	['class_rules'] = 1,
	['digitpad'] = 1,
	['draw_header'] = 1,
	['drawpoints'] = 1,
	['for_against_style'] = 1,
	['GB_team'] = 1,
	['generate_links'] = 1,
	['goalpoints'] = 1,
	['group_header'] = 1,
	['keep_english_status_labels'] = 1,
	['notracking'] = 1,
	['show_box'] = 1,
	['collapsible'] = 1,
	['team_reports'] = 1,
	['below_results_header'] = 1,
	['matches_source'] = 1,
	['matches_team_header'] = 1,
	['matches_team_notes'] = 1,
	['matches_title'] = 1,
	['auto_generate_standings'] = 1,
	['default_order'] = 1,
	['external_teams'] = 1,
	['h2h_after_complete'] = 1,
	['matches_format'] = 1,
	['ranking_criteria'] = 1,
	['show_tied_positions'] = 1,
	['hide_footer'] = 1,
	['hide_for_against_columns'] = 1,
	['hide_played'] = 1,
	['hide_results'] = 1,
	['highest_pos'] = 1,
	['legs'] = 1,
	['loss_before_draw'] = 1,
	['loss_before_tie'] = 1,
	['losspoints'] = 1,
	['lowest_pos'] = 1,
	['matches_style'] = 1,
	['matches_text'] = 1,
	['match_col_width'] = 1,
	['note_header_res'] = 1,
	['only_pld_pts'] = 1,
	['only_totals'] = 1,
	['only_fa_totals'] = 1,
	['param1_valid'] = 1,
	['param2_valid'] = 1,
	['param3_valid'] = 1,
	['pct_style'] = 1,
	['pld_header'] = 1,
	['postitle'] = 1,
	['perc_first'] = 1,
	['pts_first'] = 1,
	['ranking_style'] = 1,
	['res_col_header'] = 1,
	['result_prefix'] = 1,
	['rwpoints'] = 1,
	['rounds_won'] = 1,
	['rw_header'] = 1,
	['section'] = 1,
	['short_style'] = 1,
	['show_GB'] = 1,
	['show_away_goals'] = 1,
	['show_bonus_points'] = 1,
	['show_class_rules'] = 1,
	['show_draw'] = 1,
	['show_groups'] = 1,
	['show_hth'] = 1,
	['show_limit'] = 1,
	['show_loss'] = 1,
	['show_matches'] = 1,
	['show_positions'] = 1,
	['show_rw'] = 1,
	['show_status'] = 1,
	['no_totals_check'] = 1,
	['show_totals'] = 1,
	['show_win'] = 1,
	['sortable_table'] = 1,
	['source'] = 1,
	['start_date'] = 1,
	['stack_footer'] = 1,
	['status_order'] = 1,
	['status_pos'] = 1,
	['style'] = 1,
	['table_header'] = 1,
	['table_header_source'] = 1,
	['team_header'] = 1,
	['team_order'] = 1,
	['team_order_start'] = 1,
	['teamwidth'] = 1, ['pointswidth'] = 1,
	['template_name'] = 1,
	['title'] = 1,
	['title_source'] = 1,
	['transcludesection'] = 1,
	['update'] = 1,
	['use_against_percentage'] = 1,
	['use_goal_average'] = 1,
	['use_goal_diff'] = 1,
	['use_goal_percentage'] = 1,
	['use_goal_ratio'] = 1,
	['use_goals_scored'] = 1,
	['use_point_percentage'] = 1,
	['use_goals_per_match'] = 1,
	['use_points_per_match'] = 1,
	['use_tie'] = 1,
	['winpoints'] = 1,
	['win_header'] = 1,
	['loss_header'] = 1,
}

-- Надстройка для стиля WDLHA — таблицы с разделением на дом и выезд.
local good_args_HA = {
	['ha_side'] = 1,
	['hwinpoints'] = 1,
	['awinpoints'] = 1
}

-- Надстройка для стилей WDL OT и WL OT:
-- очки, столбцы и заголовки для исходов в овертайме, по буллитам и в серии пенальти.
local good_args_PKOT = {
	['showOTloss'] = 1,
	['showOTwin'] = 1,
	['showPKloss'] = 1,
	['showSOloss'] = 1,
	['showPKwin'] = 1,
	['showSOwin'] = 1,
	['show_tiebr'] = 1,
	['PKlosspoints'] = 1,
	['SOlosspoints'] = 1,
	['OTlosspoints'] = 1,
	['PKwinpoints'] = 1,
	['SOwinpoints'] = 1,
	['OTwinpoints'] = 1,
	['PKloss_after_loss'] = 1,
	['SOloss_after_loss'] = 1,
	['OTloss_after_loss'] = 1,
	['tiebr_header'] = 1,
	['OTwin_header'] = 1,
	['OTloss_header'] = 1,
	['PKwin_header'] = 1,
	['PKloss_header'] = 1,
	['SOwin_header'] = 1,
	['SOloss_header'] = 1,
	['percentage_format'] = 1,
}

-- Параметры шахматки [[Модуль:Sports results]], которые a.check добавляет к набору таблицы при любом стиле,
-- а не только при заданном show_matches. Шаблон обычно держит и данные таблицы, и данные шахматки и вызывается дважды,
-- причём show_matches=only задаётся только во втором вызове: привязка к этому параметру пометила бы верную разметку первого вызова.
local good_args_matches = {
	['a_note'] = 1,
	['font_size'] = 1,
	['highest_row'] = 1,
	['lowest_row'] = 1,
	['multirowlegs'] = 1,
	['ot_note'] = 1,
	['ot_sup'] = 1,
	['pk_note'] = 1,
	['team_align'] = 1,
	['team_header_note'] = 1,
	['team_nowrap'] = 1
}

-- Набор для самостоятельной шахматки (a.check с mode = 'results'): применяется вместо набора таблицы,
-- префиксов поколоночных параметров у него нет. Параметры таблицы (ranking_style, win_ХХХ и прочие)
-- в самостоятельной шахматке ничего не делают, и предупредить о них полезнее, чем молча принять.

-- team_order_start здесь нет намеренно:
-- Sports results его не читает и раскладывает team_order всегда с team1.
local good_args_results = {
	['a_note'] = 1,
	['baselink'] = 1,
	['teamwidth'] = 1,
	['font_size'] = 1,
	['generate_links'] = 1,
	['highest_pos'] = 1,
	['highest_row'] = 1,
	['legs'] = 1,
	['loss_before_draw'] = 1,
	['loss_before_tie'] = 1,
	['lowest_pos'] = 1,
	['lowest_row'] = 1,
	['match_col_width'] = 1,
	['matches_style'] = 1,
	['multirowlegs'] = 1,
	['ot_note'] = 1,
	['ot_sup'] = 1,
	['pk_note'] = 1,
	['section'] = 1,
	['short_style'] = 1,
	['source'] = 1,
	['start_date'] = 1,
	['team_align'] = 1,
	['team_header'] = 1,
	['team_header_note'] = 1,
	['team_nowrap'] = 1,
	['team_order'] = 1,
	['title'] = 1,
	['transcludesection'] = 1,
	['update'] = 1
}

-- Набор для расписания ([[Модуль:Sports results/schedule]], matches_format=schedule, series,
-- series_split): при самостоятельном вызове (a.check с mode = 'schedule') применяется вместо
-- набора таблицы, а в вызове турнирной таблицы с таким matches_format добавляется к её набору.
-- Параметры по номеру тура и блока (roundN_matches, roundN_header, roundN_date, blockN,
-- blockN_title) узнаются по образцу — см. schedule_patterns. legs здесь есть: о том, что он
-- в расписании не действует, сообщает само расписание, и второе сообщение не нужно.
local good_args_schedule = {
	['matches_format'] = 1, ['all_neutral'] = 1, ['external_teams'] = 1,
	['external_teams_label'] = 1, ['default_order'] = 1, ['team_order'] = 1,
	['highest_pos'] = 1, ['lowest_pos'] = 1, ['legs'] = 1,
	['title'] = 1, ['block_columns'] = 1,
	['team_header'] = 1, ['team_header_note'] = 1, ['team_align'] = 1, ['team_nowrap'] = 1,
	['teamwidth'] = 1, ['match_col_width'] = 1, ['font_size'] = 1,
	['round_type'] = 1, ['round_abbreviation'] = 1, ['round_name'] = 1, ['round_span_header'] = 1,
	['display_codes'] = 1, ['opponent_inline_codes'] = 1, ['short_style'] = 1,
	['legend_divider'] = 1, ['abbreviation_after'] = 1, ['header_legend'] = 1,
	['legend_teams_per_line'] = 1, ['venue_display'] = 1, ['home_first'] = 1,
	['solid_cell'] = 1, ['null_text'] = 1, ['row_dividers'] = 1,
	['matches_style'] = 1, ['loss_before_draw'] = 1, ['loss_before_tie'] = 1, ['use_tie'] = 1,
	['generate_links'] = 1, ['baselink'] = 1,
	['update'] = 1, ['source'] = 1, ['start_date'] = 1,
	['a_note'] = 1, ['ot_note'] = 1, ['pk_note'] = 1, ['ot_sup'] = 1,
	['series_matches_style'] = 1, ['series_team1'] = 1, ['series_team2'] = 1,
	['series_columns'] = 1, ['bold_winner'] = 1,
	['section'] = 1, ['transcludesection'] = 1, ['notracking'] = 1,
}

-- Образцы имён параметров расписания с номером тура или блока.
local schedule_patterns = {
	'^round%d+_matches$', '^round%d+_header$', '^round%d+_date$', '^block%d+$', '^block%d+_title$',
}

-- Виды matches_format, которые выводит расписание.
local schedule_formats = {schedule = true, series = true, series_split = true}

-- Префиксы поколоночных параметров вида win_КОД, общие для всех стилей; надстройка стиля
-- накладывается так же, как на good_args. a.scan узнаёт префикс по части имени до первого
-- подчёркивания, а составной (adjust_points, matches_note) — до второго.
local col_names = {
	['adjust_points'] = 1,
	['away_goals'] = 1,
	['schedule'] = 1,
	['matches_note'] = 1,
	['draw'] = 1,
	['loss'] = 1,
	['gf'] = 1,
	['ga'] = 1,
	['group'] = 1,
	['pf'] = 1,
	['pa'] = 1,
	['matches'] = 1,
	['pos'] = 1,
	['rw'] = 1,
	['short'] = 1,
	['startpoints'] = 1,
	['status'] = 1,
	['win'] = 1,
}

-- Надстройка префиксов для стиля WDLHA: 0 убирает префикс из набора, 1 добавляет. Общие
-- draw, loss, gf, ga, pf, pa и rw заменяются парами «дома»/«на выезде» с приставками h и a;
-- win остаётся, к нему добавляются hwin и awin.
local col_names_HA = {
	['draw'] = 0,
	['loss'] = 0,
	['gf'] = 0,
	['ga'] = 0,
	['pf'] = 0,
	['pa'] = 0,
	['rw'] = 0,
	['hdraw'] = 1,
	['adraw'] = 1,
	['hloss'] = 1,
	['aloss'] = 1,
	['hwin'] = 1,
	['awin'] = 1,
	['hga'] = 1,
	['aga'] = 1,
	['hgf'] = 1,
	['agf'] = 1,
	['hpa'] = 1,
	['apa'] = 1,
	['hpf'] = 1,
	['apf'] = 1,
	['hrw'] = 1,
	['arw'] = 1,
}

-- Надстройка префиксов для стилей WDL OT и WL OT.
local col_names_PKOT = {
	['PKloss'] = 1,
	['SOloss'] = 1,
	['OTloss'] = 1,
	['PKwin'] = 1,
	['SOwin'] = 1,
	['OTwin'] = 1,
	['tiebr'] = 1
}

-- Проверяемые стили и надстройка, которую a.check применяет к каждому ('WDL' — без надстройки).
-- style сравнивается с учётом регистра, после замены пробелов и подчёркиваний одним пробелом.
-- Незаданный style и стиль не из списка проверки не получают: a.check возвращает пустые списки,
-- а для заданного, но нераспознанного стиля — одну категорию unrecognised_style.
local tracked_styles = {
	['WDL'] = 'WDL',
	['WL'] = 'WDL',
	['WDLHA'] = 'HA',
	['WDL OT'] = 'PKOT',
	['WL OT'] = 'PKOT'
}

-- Параметры с особыми правилами ---------------------------------------------------------

-- Параметры, которые распознаются, но ни на что не влияют: страница с ними попадает
-- в категорию недействующих параметров, как и с параметрами на att_ (их a.scan узнаёт
-- по приставке). hdrawpoints/adrawpoints и hlosspoints/alosspoints выглядят парой
-- к действующим hwinpoints/awinpoints, но стиль WDLHA их не читает: очки за ничью
-- и поражение он берёт только из общих drawpoints и losspoints.
local ignored_args = {
	['date'] = 1,
	['ptsfirst'] = 1,
	['solid_cell'] = 1,
	['hdrawpoints'] = 1,
	['adrawpoints'] = 1,
	['hlosspoints'] = 1,
	['alosspoints'] = 1
}

-- Логические параметры, запись которых сверяется с каноническими значениями
-- (ppp.is_canonical_boolean, набор общий с карточками матча). Список один для таблицы и самостоятельной шахматки.
--
-- show_matches сюда не входит и входить не должен: кроме логических значений он принимает
-- only, below и below_levelN, и каждое из них отмечалось бы как неканоническое.
-- По той же причине здесь нет ot_note (принимает ещё ot, pk, so) и ot_sup (принимает подписи).
local boolean_args = {
	['generate_links'] = 1, ['ha_side'] = 1, ['hide_footer'] = 1,
	['hide_for_against_columns'] = 1, ['hide_played'] = 1, ['hide_results'] = 1,
	['loss_before_draw'] = 1, ['loss_before_tie'] = 1, ['multirowlegs'] = 1,
	['auto_generate_standings'] = 1, ['collapsible'] = 1, ['matches_team_notes'] = 1, ['team_reports'] = 1,
	['show_tied_positions'] = 1,
	['no_totals_check'] = 1, ['notracking'] = 1,
	['only_fa_totals'] = 1, ['only_totals'] = 1,
	['a_note'] = 1, ['pk_note'] = 1, ['OTloss_after_loss'] = 1,
	['all_neutral'] = 1, ['header_legend'] = 1, ['home_first'] = 1, ['row_dividers'] = 1,
	['abbreviation_after'] = 1, ['bold_winner'] = 1,
	['PKloss_after_loss'] = 1, ['SOloss_after_loss'] = 1, ['perc_first'] = 1,
	['pts_first'] = 1, ['rounds_won'] = 1, ['showOTloss'] = 1, ['showOTwin'] = 1,
	['showPKloss'] = 1, ['showPKwin'] = 1, ['showSOloss'] = 1, ['showSOwin'] = 1,
	['show_GB'] = 1, ['show_away_goals'] = 1, ['show_bonus_points'] = 1,
	['show_class_rules'] = 1, ['show_draw'] = 1, ['show_groups'] = 1,
	['show_hth'] = 1, ['show_loss'] = 1, ['show_positions'] = 1, ['show_rw'] = 1,
	['show_status'] = 1, ['show_tiebr'] = 1, ['show_totals'] = 1, ['show_win'] = 1,
	['sortable_table'] = 1, ['stack_footer'] = 1, ['use_against_percentage'] = 1,
	['use_goal_average'] = 1, ['use_goal_diff'] = 1, ['use_goal_percentage'] = 1,
	['use_goal_ratio'] = 1, ['use_goals_per_match'] = 1, ['use_goals_scored'] = 1,
	['use_point_percentage'] = 1, ['use_points_per_match'] = 1, ['use_tie'] = 1
}

-- Параметры, которые действуют только вместе с другим. Заданные в одиночку они ничего не меняют:
-- разметка выглядит осмысленной, а результат не тот, которого ждал редактор.
-- Ключ — зависимый параметр, значение — тот, без которого он бесполезен.
local requires_param = {
	['GB_team'] = 'show_GB',
	['team_order_start'] = 'team_order'
}

-- Вспомогательные функции ---------------------------------------------------------------

-- Возвращает ссылку на категорию с value в роли ключа сортировки:
-- по нему в категории видно, что именно вызвало отметку.
local function get_tracking_category(category, value)
	return '[[' .. category .. '|' .. value .. ']]'
end

-- Возвращает поверхностную копию таблицы. a.check копирует ею аргументы вызова и базовые наборы:
-- a.scan меняет аргументы, а наборы — данные модуля, общие для всех вызовов a.check,
-- и надстройка одного вызова не должна в них оставаться.
local function shallow_copy(source)
	local copy = {}
	for k, v in pairs(source) do
		copy[k] = v
	end
	return copy
end

-- Добавляет по ссылке на категорию для каждого значения из args: в warn (предупреждения)
-- либо, при is_track, в track (только категории). Знаки из набора bad_char
-- в значении заменяются на «?», пустое значение — на пробел.
local function validate_table_values(warn, track, args, category, bad_char, is_track)
	for _, v in ipairs(args) do
		v = mw.ustring.gsub(v, bad_char, '?')
		if v == '' then
			v = ' '
		end
		if is_track then
			table.insert(track, get_tracking_category(category, v))
		else
			table.insert(warn, get_tracking_category(category, v))
		end
	end
end

-- Проверка вызова -----------------------------------------------------------------------

-- Помечает разобранными (обнуляет в targs) параметры расписания с номером тура или блока —
-- их набор допустимых параметров перечислить не может.
local function clear_schedule_patterns(targs)
	for k in pairs(targs) do
		if type(k) == 'string' then
			for _, pattern in ipairs(schedule_patterns) do
				if k:find(pattern) then
					targs[k] = ''
					break
				end
			end
		end
	end
end

-- Возвращает true, если вызов выводит расписание (matches_format=schedule, series, series_split).
local function is_schedule(targs)
	local f = targs['matches_format']
	return type(f) == 'string' and schedule_formats[mw.ustring.lower(mw.text.trim(f))] == true
end

-- Проверяет параметры вызова. targs — аргументы модуля, pargs — аргументы вызова шаблона,
-- mode — nil для турнирной таблицы, 'results' для самостоятельной шахматки
-- или 'schedule' для самостоятельного расписания. Выбирает набор допустимых параметров
-- по режиму и стилю и передаёт разбор в a.scan. Возвращает два массива ссылок на категории:
-- предупреждения и отслеживающие категории.

-- Разбор идёт по копии: a.scan обнуляет разобранные параметры и дописывает позиции teamN,
-- а вызывающий модуль продолжает работать с теми же аргументами — при show_matches=only и below
-- по ним после проверки строится шахматка.
function a.check(targs, pargs, mode)
	targs = shallow_copy(targs)
	local warn, track = {}, {}

	-- Самостоятельная шахматка: свой набор параметров и пустой набор префиксов — name_,
	-- short_, note_, hth_, match_ и result_ a.scan узнаёт по образцам имён.
	if mode == 'results' then
		local good_args_local = shallow_copy(good_args_results)
		return a.scan(targs, pargs, good_args_local, {}, warn, track)
	end

	-- Самостоятельное расписание: свой набор, из префиксов — только schedule_КОД. Соперникам вне турнира
	-- (external_teams) строки и названия не нужны: матчи с ними не отмечаются как матчи отсутствующих команд.
	if mode == 'schedule' then
		clear_schedule_patterns(targs)
		local known = {}
		for _, c in ipairs(mw.text.split(targs['external_teams'] or '', '%s*[;,]%s*')) do
			c = mw.text.trim(c)
			if c ~= '' then known[c] = 1 end
		end
		return a.scan(targs, pargs, shallow_copy(good_args_schedule), {schedule = 1}, warn, track, known)
	end

	-- Незаданный style — стиль по умолчанию, как и в самой турнирной таблице.
	local raw_style = mw.ustring.gsub(targs['style'] or '', '[%s_][%s_]*', ' ')
	if raw_style == '' then
		raw_style = 'WDL'
	end
	local mstyle = tracked_styles[raw_style]
	if mstyle == nil then
		if raw_style ~= '' then
			table.insert(track, get_tracking_category(categories.unrecognised_style, raw_style))
		end
		return warn, track
	end

	-- Надстройка стиля накладывается на копии наборов. При заданном only_fa_totals стиль
	-- WDLHA читает и общие gf, ga, pf, pa, поэтому они возвращаются в набор префиксов.
	local good_args_local = shallow_copy(good_args)
	local col_names_local = shallow_copy(col_names)
	if mstyle == 'HA' then
		for k, v in pairs(good_args_HA) do
			good_args_local[k] = v ~= 0 and v or nil
		end
		for k, v in pairs(col_names_HA) do
			col_names_local[k] = v ~= 0 and v or nil
		end
		if targs['only_fa_totals'] then
			col_names_local['pf'] = 1
			col_names_local['gf'] = 1
			col_names_local['pa'] = 1
			col_names_local['ga'] = 1
		end
	end
	if mstyle == 'PKOT' then
		for k, v in pairs(good_args_PKOT) do
			good_args_local[k] = v ~= 0 and v or nil
		end
		for k, v in pairs(col_names_PKOT) do
			col_names_local[k] = v ~= 0 and v or nil
		end
	end

	-- Параметры шахматки допустимы при любом стиле — см. good_args_matches.
	for k, v in pairs(good_args_matches) do
		good_args_local[k] = v
	end

	-- При matches_format=schedule и подобных шаблон держит и параметры расписания:
	-- его выводит та же таблица при show_matches=only или below.
	if is_schedule(targs) then
		for k, v in pairs(good_args_schedule) do
			good_args_local[k] = v
		end
		clear_schedule_patterns(targs)
	end

	-- Команды, у которых заданы и поправка очков, и начальные очки. Стили складывают их
	-- по-разному: WDL и WDLHA — суммой, стили с овертаймом берут только поправку, — и такую
	-- запись стоит проверить. Проверяется до a.scan: тот стирает в копии аргументов teamN.
	-- Строка итогов не в счёт: у неё обе величины — суммы по командам.
	local both = {}
	for k, v in pairs(targs) do
		if type(k) == 'string' and k:find('^team%d+$') and v ~= p_sub.total_row_code
			and tonumber(targs['adjust_points_' .. v]) and tonumber(targs['startpoints_' .. v]) then
			both[#both + 1] = v
		end
	end
	table.sort(both)
	if #both > 0 then
		table.insert(track, get_tracking_category(categories.adjust_and_start, both[1]))
	end

	return a.scan(targs, pargs, good_args_local, col_names_local, warn, track)
end

-- Разбирает аргументы targs по набору допустимых параметров good_args_local и префиксов
-- col_names_local, дописывает ссылки на категории в warn и track и возвращает их же.
-- Общая для турнирной таблицы и самостоятельной шахматки: наборы выбирает a.check. pargs —
-- аргументы вызова шаблона: нераспознанный параметр, который задан и там, считается возможно
-- игнорируемым (только категория), остальные — неизвестными (предупреждение). known_teams —
-- коды, которые считаются командами без teamN и name_КОД (соперники вне турнира в расписании).

-- Разбор меняет targs — обнуляет разобранные параметры и дописывает позиции teamN, —
-- поэтому передаётся копия. Сначала идут проверки, которым нужны нетронутые значения,
-- затем два прохода: первый строит списки команд и кодов результатов и помечает связанные
-- с ними параметры, второй разбирает всё оставшееся; в конце — логические значения,
-- парные параметры и запись счёта.
function a.scan(targs, pargs, good_args_local, col_names_local, warn, track, known_teams)
	local teams = shallow_copy(known_teams or {})
	local missing_teams = {}
	local missing_results = {}
	local orphaned_match_notes = {}
	local orphaned_results = {}
	local orphaned_names = {}
	local orphaned_notes = {}
	local ignored = {}
	local possibly_ignored = {}
	local unknown = {}

	-- Турнир, не помеченный завершённым: update не равен complete (регистр не важен).
	-- Ключ сортировки — род случая, а не само значение: цифра впереди задаёт порядок групп:
	-- 0 not set — параметр не заполняли вовсе;
	-- 1 date    — дата стоит, но её надо либо обновлять, либо заменить на complete;
	-- 2 future  — турнир ещё не начался.
	local unfinished_update = {}
	do
		local upd = targs['update']
		local low = type(upd) == 'string' and mw.ustring.lower(mw.text.trim(upd)) or ''
		if low ~= 'complete' then
			table.insert(unfinished_update,
				(low == '' and '0 not set') or (low == 'future' and '2 future') or '1 date')
		end
	end

	-- Ручные ширины столбцов: teamwidth (столбец команд), pointswidth (столбец очков)
	-- и match_col_width (клетки шахматки). Ширины подбираются сами; вручную их задают,
	-- чтобы несколько таблиц на странице совпали по ширине, и категория показывает где.
	local manual_teamwidth = {}
	for _, key in ipairs({'teamwidth', 'pointswidth', 'match_col_width'}) do
		if type(targs[key]) == 'string' and targs[key] ~= '' then
			table.insert(manual_teamwidth, key)
		end
	end

	-- Сокращения, оставшиеся на латинице при названии команды на кириллице.
	local untranslated = {}
	for k, v in pairs(targs) do
		local code = type(k) == 'string' and type(v) == 'string' and k:match('^name_(.+)$')
		-- Строка итогов — не команда: её код служебный и в заголовок столбца не попадает.
		if code and code ~= p_sub.total_row_code and mw.ustring.find(v, '[А-Яа-яЁё]') then
			local short = targs['short_' .. code]
			local shown, param
			if type(short) == 'string' and short ~= '' then
				-- Берётся видимое: подпись ссылки, если она есть, иначе цель; шаблоны и теги снимаются.
				shown = mw.ustring.gsub(short, '%[%[[^%[%]|]*|([^%[%]]*)%]%]', '%1')
				shown = mw.ustring.gsub(shown, '%[%[([^%[%]]*)%]%]', '%1')
				shown = mw.ustring.gsub(shown, '%b{}', '')
				shown = mw.ustring.gsub(shown, '<[^<>]*>', '')
				shown = mw.text.trim(shown)
				param = 'short_' .. code
			else
				-- Сокращение не задано: в заголовок столбца шахматки попадает сам код команды,
				-- и латинский код оставляет столбец непереведённым так же.
				shown = code
				param = 'name_' .. code
			end
			-- Отмечается только сокращение с буквами, среди которых нет ни одной кириллической:
			-- сокращение из одних цифр или знаков ошибкой не считается.
			if mw.ustring.find(shown, '%a') and not mw.ustring.find(shown, '[А-Яа-яЁё]')
				and mw.ustring.find(shown, '^[%w%s%p]*$') then
				table.insert(untranslated, param)
			end
		end
	end

	-- Параметры col_ХХХ, заданные не утверждённым именем палитры (ppp.palette_color
	-- возвращает происхождение, отличное от 'named'): шестнадцатеричным кодом, названием
	-- цвета CSS, именем оттенка без номера уровня, именем без назначенного смысла или
	-- с опечаткой. Всё, что похоже на цвет, при этом применяется — категория нужна, чтобы
	-- привести разметку к именам или дополнить палитру.

	-- Проверка идёт до обоих проходов: первый обнуляет разобранные параметры,
	-- включая col_ХХХ, и после него сверять уже нечего.
	local bad_color = {}
	for k, v in pairs(targs) do
		if type(k) == 'string' and type(v) == 'string' and v ~= '' and k:find('^col_') then
			local _, origin = p_sub.palette_color(v)
			if origin ~= 'named' then
				table.insert(bad_color, k)
			end
		end
	end

	-- Заливка отдельной клетки шахматки задаётся не параметром, а тегом прямо в значении матча:
	-- Проверка по col_ХХХ такую запись не видит, а имя палитры там столь же уместно.
	-- Находки идут в ту же категорию: случай тот же — цвет вне утверждённых имён.
	for k, v in pairs(targs) do
		if type(k) == 'string' and type(v) == 'string' and v ~= ''
			and k:find('^match[%d]*_.*_.*$') and not k:find('_note$') and k ~= 'match_col_width'
			and v:find('<', 1, true) then
			local inline = mw.ustring.match(v,
				'^%s*<span%s+style%s*=["\'%s]*background[%-colr]*%s*:([^\'";<>]*)')
			if inline then
				local _, origin = p_sub.palette_color(inline)
				if origin ~= 'named' then
					table.insert(bad_color, k)
				end
			end
		end
	end

	-- Ручное оформление в значениях (ppp.manual_format): запрет переноса,
	-- принудительный перенос строки, размер шрифта, ширина, отступы, выравнивание.
	-- Оформление семейства рассчитано на узкие экраны, и такие вставки ему мешают:
	-- запрет переноса выталкивает длинную строку за край,
	-- а уменьшение шрифта поверх уже уменьшенного нарушает [[ВП:РТ]].

	-- Проверка идёт по всем именованным параметрам и до обоих проходов —
	-- первый обнуляет разобранные параметры. Вид приёма идёт в ключ сортировки:
	-- в предпросмотре он виден рядом с именем параметра.

	-- Источник (source, matches_source) не проверяется: это ссылка на литературу, и размер шрифта в ней
	-- задают шаблоны цитирования — {{книга}}, пометка языка «(англ.)», — а не редактор таблицы.
	local manual_format = {}
	local citation_args = {source = true, matches_source = true}
	for k, v in pairs(targs) do
		local kind = type(k) == 'string' and not citation_args[k] and p_sub.manual_format(v)
		if kind then
			table.insert(manual_format, k .. ' (' .. kind .. ')')
		end
	end

	-- team_order раскладывается по teamN с тем же смещением team_order_start,
	-- что и в Sports table: иначе проверка видела бы команды не на тех позициях.
	if targs['team_order'] and targs['team_order'] ~= '' then
		local team_order_offset = (tonumber(targs['team_order_start']) or 1) - 1
		local tlist = mw.text.split(targs['team_order'], '%s*[;,]%s*')
		for k, tname in ipairs(tlist) do
			if tname ~= '' then
				targs['team' .. (k + team_order_offset)] = tname
			end
		end
	end

	-- Вызов с выделенной командой (showteam или team) не проверяется, если эта команда стоит
	-- на одной из показываемых позиций — от highest_pos до lowest_pos, а без lowest_pos до
	-- последней команды: функция возвращает warn и track нетронутыми, и собранные выше списки
	-- тоже не выводятся. showteam обычно передают из статьи о клубе, а данные таблицы
	-- проверяет вызов без него. Команду, которой на этих позициях нет, второй проход
	-- отмечает как отсутствующую.
	if targs['showteam'] or targs['team'] then
		local top_pos = tonumber(targs['highest_pos']) or 1
		local bottom_pos = tonumber(targs['lowest_pos']) or 0
		local N = top_pos - 1
		while targs['team' .. N + 1] ~= nil and (bottom_pos < top_pos or N < bottom_pos) do
			N = N + 1
			teams[targs['team' .. N]] = 1
			if teams[targs['showteam'] or targs['team']] then
				return warn, track
			end
		end
	end

	-- Позиции команд, не заданные вручную (нет team1). Автосборка записывает вычисленный
	-- порядок в teamN, но при show_matches=only Sports table проверяет аргументы до неё
	-- и передаёт вызов шахматке, так и не дойдя до автосборки.

	-- Без позиций первый проход не распознаёт ни одного поимённого параметра — ни примечания,
	-- ни названия, ни сокращения, — и все они попадают в некорректные. Поэтому список
	-- достраивается из самих названий: для разбора важен набор кодов, а не их порядок.
	-- Проверка осиротевших названий от этого не притупляется: она ловит name_КОД у команды,
	-- которой нет в таблице, а когда позиций не задано вовсе, таблицу и составляют сами названия.
	-- Если team1 задан, достройка не выполняется.

	-- Коды сначала собираются, а позиции записываются потом: добавлять новые ключи в таблицу
	-- во время её обхода pairs() нельзя — обход после этого может пропустить часть ключей,
	-- и команды молча терялись бы.
	if targs['team1'] == nil then
		local codes = {}
		for k, _ in pairs(targs) do
			local code = type(k) == 'string' and k:match('^name_(.+)$')
			if code then
				codes[#codes + 1] = code
			end
		end
		table.sort(codes)
		for i, code in ipairs(codes) do
			if targs['team' .. i] == nil then
				targs['team' .. i] = code
			end
		end
	end

	-- Первый проход: собирает команды по teamN и коды результатов по resultN и помечает
	-- связанные с ними параметры — обнуляет их в targs, чтобы второй проход не счёл их
	-- неизвестными. Здесь же отмечаются команды без name_КОД, коды результатов без
	-- оформления и примечания к матчам, которых нет.
	for k, v in pairs(targs) do
		if type(k) == 'string' then
			if k:find('^team%d%d*$') then
				teams[v] = 1
				targs[k] = ''
				if targs['name_' .. v] then
					targs['name_' .. v] = ''
				else
					table.insert(missing_teams, v)
				end
				-- short_КОД — сокращение для заголовка столбца шахматки. Помечается здесь,
				-- наравне с названием и примечанием: в самостоятельной шахматке префиксов
				-- нет, и распознать его иначе нечем.
				if targs['short_' .. v] then
					targs['short_' .. v] = ''
				end
				if targs['note_' .. v] then
					targs['note_' .. v] = ''
				end
				-- hth_КОД — примечание о личных встречах: собственный текст либо отсылка
				-- к hth другой команды — одним кодом или списком кодов через запятую.
				-- Список засчитывается, только если есть примечания всех его кодов;
				-- примечания, на которые идёт отсылка, помечаются разобранными.
				if targs['hth_' .. v] then
					local multiref = 1
					local hth_local_table = mw.text.split(targs['hth_' .. v], '%s*,%s*')
					if (#hth_local_table > 1) then
						for _, hth_loc in ipairs(hth_local_table) do
							multiref = multiref * (targs['hth_' .. hth_loc] and 1 or 0)
						end
					else
						multiref = 0
					end
					if multiref > 0 then
						for _, hth_loc in ipairs(hth_local_table) do
							targs['hth_' .. hth_loc] = ''
						end
					elseif targs['hth_' .. targs['hth_' .. v]] then
						targs['hth_' .. targs['hth_' .. v]] = ''
					end
					targs['hth_' .. v] = ''
				end
			elseif k:find('^result%d%d*$') or k:find('^.*_result%d%d*$') then
				-- resultN (или ПРЕФИКС_resultN) — код результата команды. Коду нужен хотя бы один
				-- из col_КОД, text_КОД, note_res_КОД с тем же префиксом; note_res_КОД
				-- отсылает к другим примечаниям так же, как hth_КОД.
				local pre = mw.ustring.gsub(k, '^(.*)result%d%d*$', '%1')
				targs[k] = ''
				if targs[pre .. 'col_' .. v] or targs[pre .. 'text_' .. v] or targs[pre .. 'note_res_' .. v] then
					if targs[pre .. 'col_' .. v] then
						targs[pre .. 'col_' .. v] = ''
					end
					if targs[pre .. 'text_' .. v] then
						targs[pre .. 'text_' .. v] = ''
					end
					if targs[pre .. 'note_res_' .. v] then
						local multiref = 1
						local note_res_local_table = mw.text.split(targs[pre .. 'note_res_' .. v], '%s*,%s*')
						if (#note_res_local_table > 1) then
							for _, note_res_loc in ipairs(note_res_local_table) do
								multiref = multiref * (targs[pre .. 'note_res_' .. note_res_loc] and 1 or 0)
							end
						else
							multiref = 0
						end
						if multiref > 0 then
							for _, note_res_loc in ipairs(note_res_local_table) do
								targs[pre .. 'note_res_' .. note_res_loc] = ''
							end
						elseif targs[pre .. 'note_res_' .. targs[pre .. 'note_res_' .. v]] then
							targs[pre .. 'note_res_' .. targs[pre .. 'note_res_' .. v]] = ''
						end
						targs[pre .. 'note_res_' .. v] = ''
					end
					if targs[pre .. 'res_col_header'] then
						targs[pre .. 'res_col_header'] = ''
					end
				else
					table.insert(missing_results, v)
				end
			elseif k:find('^matchn[%d]*_.*_.*$') or k:find('^resultn[%d]*_.*_.*$') then
				-- matchn_ААА_БББ — матч на нейтральном поле, resultn_ААА_БББ — код его исхода.
				-- Их читает только автосборка: в шахматке у клетки есть хозяева и гости,
				-- и такой матч она не показывает. Здесь они лишь помечаются разобранными,
				-- вместе с примечанием к матчу.
				targs[k] = ''
			elseif k:find('^match[%d]*_.*_.*_note$') then
				-- Примечание к матчу требует самого матча. Значение примечания может быть
				-- отсылкой к note_КОД — тогда и это примечание помечается разобранным.
				local m = mw.ustring.gsub(k, '^(match[%d]*_.*_.*)_note$', '%1')
				targs[k] = ''
				if targs[m] then
					if targs['note_' .. v] then
						targs['note_' .. v] = ''
					end
				else
					table.insert(orphaned_match_notes, m)
				end
			end
		end
	end

	-- Второй проход: каждый оставшийся непустой параметр сверяется с известными видами
	-- в порядке проверок ниже. Не подошедший ни под один вид считается возможно игнорируемым,
	-- если он задан и в вызове шаблона (pargs), иначе — неизвестным.
	local possible_missing_winpoints = 0
	for k, v in pairs(targs) do
		if v and v ~= '' then
			local found_arg = 0
			if type(k) == 'string' then
				-- Счётчик для категории possible_incorrect_winpoints: ненулевая поправка
				-- adjust_points_КОД, в точности гасящая число побед win_КОД, прибавляет единицу,
				-- любая другая ненулевая — вычитает. Сам параметр распознаётся ниже, по префиксу.
				if k:find('^adjust_points_') then
					local p = mw.ustring.gsub(k, '^adjust_points_', '')
					if (tonumber(v) or 0) ~= 0 then
						local pdiff = (tonumber(targs['win_' .. p] or 0) or 0) + tonumber(v)
						if pdiff == 0 then
							possible_missing_winpoints = possible_missing_winpoints + 1
						else
							possible_missing_winpoints = possible_missing_winpoints - 1
						end
					end
				end
				if found_arg == 0 and (good_args_local[k] or k:find('^split[%d][%d]*$')) then
					found_arg = 1
				end
				if found_arg == 0 and (k == 'showteam' or k == 'team') then
					found_arg = 1
					if teams[v] ~= 1 then
						table.insert(missing_teams, v)
					end
				end
				if found_arg == 0 and (ignored_args[k] or k:find('^att_')) then
					found_arg = 1
					table.insert(ignored, k)
				end
				if found_arg == 0 and k:find('^[A-Za-z3-5]*_.*$') then
					local p = mw.ustring.gsub(k, '^([A-Za-z3-5]*)_.*$', '%1')
					if col_names_local[p] then
						found_arg = 1
					end
				end
				if found_arg == 0 and k:find('^[A-Za-z]*_[A-Za-z]*_.*$') then
					local p = mw.ustring.gsub(k, '^([A-Za-z]*_[A-Za-z]*)_.*$', '%1')
					if col_names_local[p] then
						found_arg = 1
					end
				end
				-- Имя матча или кода исхода делится на коды команд по известным кодам
				-- (ppp.split_pair): подчёркивание бывает и внутри кода, и деление по последнему
				-- отметило бы «B_A» и «1» вместо команд «B» и «A_1».
				if found_arg == 0 and k:find('^match[%d]*_.*_.*$') then
					local t1, t2 = p_sub.split_pair(k:match('^match%d*_(.+)$'), teams)
					found_arg = 1
					if teams[t1] == nil or teams[t2] == nil then
						if teams[t1] == nil then
							table.insert(missing_teams, t1)
						end
						if teams[t2] == nil then
							table.insert(missing_teams, t2)
						end
					end
				end
				if found_arg == 0 and k:find('^result[%d]*_.*_.*$') then
					local t1, t2 = p_sub.split_pair(k:match('^result%d*_(.+)$'), teams)
					found_arg = 1
					if teams[t1] == nil or teams[t2] == nil then
						if teams[t1] == nil then
							table.insert(missing_teams, t1)
						end
						if teams[t2] == nil then
							table.insert(missing_teams, t2)
						end
					end
				end
				if found_arg == 0 and (k:find('^name_') or k:find('^short_')) then
					found_arg = 1
					table.insert(orphaned_names, k)
				end
				if found_arg == 0 and (k:find('^note_') or k:find('^hth_')) then
					found_arg = 1
					-- Нетронутые образцы из документации шаблона (hth_ABC=H2H note и т. п.)
					-- идут в недействующие параметры, а не в некорректные.
					if (k == 'hth_ABC' and v == 'H2H note') then
						table.insert(warn, get_tracking_category(categories.ignored_parameters, 'hth_ABC'))
					elseif (k == 'note_ABC' and v == 'Team note') then
						table.insert(warn, get_tracking_category(categories.ignored_parameters, 'note_ABC'))
					elseif (k == 'note_res_AAA' and v == 'Result note') then
						table.insert(warn, get_tracking_category(categories.ignored_parameters, 'note_res_AAA'))
					else
						table.insert(orphaned_notes, k)
					end
				end
				if found_arg == 0 and (k:find('^col_') or k:find('^text_')) then
					found_arg = 1
					table.insert(orphaned_results, k)
				end
			end
			-- Позиционные аргументы шаблона-переключателя: первый обычно выбирает ветку
			-- #switch, второй — что она выводит (таблицу или отчёты о матчах), третий —
			-- команду, матчами которой ограничен вывод. Модулю они не предназначены,
			-- но попадают в его аргументы через parentFirst; param1_valid–param3_valid
			-- разрешают их, и без этих признаков они отмечались бы как лишние.
			if found_arg == 0 and tonumber(k) == 1 and targs['param1_valid'] then
				found_arg = 1
			end
			if found_arg == 0 and tonumber(k) == 2 and targs['param2_valid'] then
				found_arg = 1
			end
			if found_arg == 0 and tonumber(k) == 3 and targs['param3_valid'] then
				found_arg = 1
			end
			if found_arg == 0 then
				if pargs[k] and pargs[k] ~= '' then
					if tonumber(k) then
						k = k .. ' = ' .. v
					end
					table.insert(possibly_ignored, k)
				else
					if tonumber(k) then
						k = k .. ' = ' .. v
					end
					table.insert(unknown, k)
				end
			end
		end
	end

	-- Логические параметры, записанные не в каноническом виде: и заведомо сломанные
	-- значения, и рабочие, но разнобойные. Разбираются они по наполнению категории,
	-- поэтому в один список попадают и те и другие.
	local bad_boolean = {}
	for k in pairs(boolean_args) do
		local v = targs[k]
		if v ~= nil and v ~= '' and not p_sub.is_canonical_boolean(v) then
			table.insert(bad_boolean, k)
		end
	end

	-- Параметры, заданные без того, с которым они работают в паре (requires_param).
	local useless_params = {}
	for k, needed in pairs(requires_param) do
		if targs[k] ~= nil and targs[k] ~= '' and (targs[needed] == nil or targs[needed] == '') then
			table.insert(useless_params, k)
		end
	end

	-- Результаты матчей, записанные не так, как принято. Собираются три рода случаев.

	-- Счёт через дефис или тире вместо двоеточия: модуль такую запись понимает
	-- и приводит к общему виду, это разнобой, а не поломка.

	-- Счёт с припиской вроде «3:0<br>возд.». Оформление снимается перед проверкой, поэтому
	-- «[[Отчёт|2:1]]» и «'''2:1'''» проходят, а приписка — нет: после снятия там остаётся
	-- не только счёт. Пояснению место в match_ААА_БББ_note, а не рядом со счётом.

	-- Значение, не распознанное ни как счёт, ни как дата будущего матча или пояснение.
	-- Так выглядят опечатки вроде «-:0» или «2;1»: счётом они не считаются, в таблицу
	-- не попадают, и заметить их можно только по пустой клетке в шахматке.

	-- Что не отмечается:
	-- «2:1» и «10:0» — счёт, в том числе обёрнутый ссылкой или шаблоном;
	-- «+:−» и «−:+» — техническая победа и поражение;
	-- «null» — матч намеренно не показывается;
	-- пустое значение после снятия оформления — счёт целиком внутри шаблона;
	-- текст, где есть две буквы подряд, — дата будущего матча, «перенесён», «отменён».
	local bad_score = {}
	for k, v in pairs(targs) do
		if type(k) == 'string' and type(v) == 'string' and v ~= ''
			and k:find('^match[%d]*_.*_.*$') and not k:find('_note$') and k ~= 'match_col_width'
			and not v:find('^%d+:%d+$') then
			-- Обычный счёт вида «2:1» пропускается сразу, остальное разбирается.
			-- Оформление снимается: счёт часто обёрнут ссылкой на отчёт, шаблоном
			-- уменьшенного шрифта или полужирным. Сноска через ref убирается вместе
			-- с содержимым — она к счёту не относится. Так же убирается служебная метка сноски:
			-- её оставляет и ref, развёрнутый до вызова модуля, и сноска к матчу (match_ХХХ_УУУ_note),
			-- которую шахматка дописывает к счёту в Args раньше, чем идёт эта проверка.
			local s = mw.ustring.gsub(v, '\127[^\127]*\127', '')
			s = mw.ustring.gsub(s, '<ref[^<>]*>.-</ref>', '')
			s = mw.ustring.gsub(s, '<ref[^<>]*/>', '')
			s = mw.ustring.gsub(s, '%[%[[^%[%]|]*|([^%[%]]*)%]%]', '%1')
			s = mw.ustring.gsub(s, '%[%[([^%[%]]*)%]%]', '%1')
			s = mw.ustring.gsub(s, '%b{}', '')
			s = mw.ustring.gsub(s, '</?%w+[^<>]*>', '')
			-- Скобки вокруг gsub обязательны: без них вторым аргументом trim получил бы
			-- число замен и принял бы его за набор обрезаемых знаков — «2:0» теряло бы «0».
			s = mw.text.trim((mw.ustring.gsub(s, "'+", '')))

			if mw.ustring.match(s, '^%s*%d+%s*[%-–—−]%s*%d+%s*$') then
				table.insert(bad_score, k)
			elseif mw.ustring.match(s, '^%s*%d+%s*:%s*%d+') then
				if not mw.ustring.match(s, '^%s*%d+%s*:%s*%d+%s*$') then
					table.insert(bad_score, k)
				end
			elseif not mw.ustring.match(s, '^%s*[%+%-−]%s*:%s*[%+%-−]%s*$')
				and mw.ustring.lower(s) ~= 'null'
				and s ~= ''
				and not mw.ustring.find(s, '%a%a') then
				table.insert(bad_score, k)
			end
		end
	end

	-- Списки превращаются в ссылки на категории. Значение идёт в ключ сортировки,
	-- и опасны там только знаки разметки: квадратная скобка закрывает ссылку посреди ключа,
	-- вертикальная черта делит её на части, фигурные открывают шаблон, перевод строки разрывает ссылку.

	-- Список запрещённых знаков, а не разрешённых: разрешающий список из латиницы и цифр
	-- превратил бы кириллицу в вопросительные знаки, ключи вида match_БАВ_МЮ стали бы
	-- нечитаемыми, и записи в категории перестали бы различаться.
	local bad_char = '[%[%]|{}\n\r]'
	validate_table_values(warn, track, unknown, categories.unknown_parameters, bad_char, false)
	validate_table_values(warn, track, bad_boolean, categories.bad_boolean, bad_char, false)
	validate_table_values(warn, track, bad_score, categories.bad_score, bad_char, true)
	validate_table_values(warn, track, useless_params, categories.useless_params, bad_char, false)
	validate_table_values(warn, track, bad_color, categories.bad_color, bad_char, false)
	validate_table_values(warn, track, manual_format, categories.manual_format, bad_char, false)
	validate_table_values(warn, track, untranslated, categories.untranslated_short, bad_char, true)
	validate_table_values(warn, track, manual_teamwidth, categories.manual_teamwidth, bad_char, true)
	validate_table_values(warn, track, unfinished_update, categories.unfinished_update, bad_char, true)
	validate_table_values(warn, track, missing_teams, categories.missing_teams, bad_char, false)
	validate_table_values(warn, track, missing_results, categories.missing_results, bad_char, false)
	validate_table_values(warn, track, orphaned_results, categories.orphaned_results, bad_char, false)
	validate_table_values(warn, track, orphaned_names, categories.orphaned_names, bad_char, false)
	validate_table_values(warn, track, orphaned_notes, categories.orphaned_notes, bad_char, false)
	validate_table_values(warn, track, orphaned_match_notes, categories.orphaned_match_notes, bad_char, false)
	validate_table_values(warn, track, ignored, categories.ignored_parameters, bad_char, false)
	validate_table_values(warn, track, possibly_ignored, categories.possibly_ignored_parameters, bad_char, true)

	if possible_missing_winpoints > 1 then
		table.insert(track, get_tracking_category(categories.possible_incorrect_winpoints, possible_missing_winpoints))
	end

	return warn, track
end

return a