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

Модуль:Sports table

Материал из вики «Вече»
Версия от 01:27, 29 сентября 2026; wp>ru.wikipedia (Импорт из русской Википедии, версия 155338716)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)

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

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

-- Набор и оформление столбцов показателей задаёт подмодуль стиля, выбираемый параметром
-- style: [[Модуль:Sports table/WDL]] (по умолчанию), [[Модуль:Sports table/WDLHA]],
-- [[Модуль:Sports table/WDL OT]], [[Модуль:Sports table/WL]], [[Модуль:Sports table/WL OT]].
-- Общие данные и функции семейства лежат в [[Модуль:Sports table/sub]]. Параметры проверяет
-- [[Модуль:Sports table/argcheck]], согласованность чисел — [[Модуль:Sports table/totalscheck]],
-- таблицу из результатов матчей собирает [[Модуль:Sports table/auto]]. Шахматку — внутри
-- таблицы, под ней или вместо неё (show_matches) — строит [[Модуль:Sports results]].

-- Вызывается из шаблонов через #invoke (p.main) и из [[Модуль:Sports group]] (p._main).
-- Документация и примеры вызова: [[Модуль:Sports table/doc]].

require('strict')

local p = {}

local p_sub = require('Модуль:Sports table/sub')

-- [[Модуль:Yesno]] понимает «да» и «нет» наравне с «yes» и «no», а нераспознанное значение
-- превращает в nil, так что мусор в логическом параметре не выдаёт себя за «да».
local yesno = require('Модуль:Yesno')

-- Обращений к mw.ustring много, в том числе в циклах, а локальная переменная в Lua
-- доступна быстрее, чем поле глобальной таблицы.
local ustring = mw.ustring

-- Данные --------------------------------------------------------------------------------

-- Подписи столбца результатов для кодов в res_col_header; любое другое значение выводится в заголовке как есть.
local results_header = {
	Q = 'Квалификация',
	QR = 'Квалификация или выбывание',
	P = 'Повышение',
	PQR = 'Повышение, квалификация или выбывание',
	PR = 'Повышение или выбывание',
	PQ = 'Повышение или квалификация',
	R = 'Выбывание'}

-- Русские буквы статуса, выводимые вместо латинских, если не задано
-- keep_english_status_labels=yes. Буквы, которых здесь нет, выводятся как есть.
local rus_letters = {A = 'С', C = 'Ч', D = 'Д', E = 'В', G = 'Г', H = 'Х', O = 'П/О', P = 'П', Q = 'К', R = 'В', S = 'П/С', T = 'Н'}

-- Разбор параметров ---------------------------------------------------------------------

-- Разбирает параметр-переключатель и возвращает true или false. Нераспознанное значение
-- действует как незаданное — берётся default. Иначе show_positions=скрыть скрывал бы
-- столбец мест, хотя по умолчанию тот показывается, — параметр срабатывал бы вопреки замыслу редактора.
-- Такие значения отмечает отслеживающей категорией [[Модуль:Sports table/argcheck]].

-- У переключателей карточки матча (p.flag в [[Модуль:Football box/base]]) правило другое:
-- там любое непустое нераспознанное значение — истина. Разница намеренная: у aet и подобных
-- нет осмысленного «по умолчанию», а в разметке в них часто пишут пояснение вместо «да».
local function flag(value, default)
	local yn = yesno(value)
	if yn == nil then
		yn = yesno(default)
	end
	return yn or false
end

-- Разбирает режим таблицы. only_pld_pts=yes оставляет из показателей только сыгранные
-- матчи и очки (full_table = false); only_pld_pts=no_hide_class_rules оставляет таблицу
-- полной, но скрывает правила классификации. Возвращает full_table, hide_results,
-- hide_footer, show_class_rules.
local function resolve_table_mode(Args)
	local full_table = true
	local hide_results = yesno(Args['hide_results'] or 'no')
	local hide_footer = yesno(Args['hide_footer'] or 'no')
	local pld_pts_val = string.lower(Args['only_pld_pts'] or 'no')
	local show_class_rules = flag(Args['show_class_rules'], 'yes')
	if yesno(pld_pts_val) then
		full_table = false
	elseif pld_pts_val == 'no_hide_class_rules' then
		full_table = true
		show_class_rules = false
	end
	return full_table, hide_results, hide_footer, show_class_rules
end

-- Раскладывает team_order — коды команд через «;» или «,» — в team1, team2 и далее,
-- начиная с номера team_order_start (по умолчанию 1). Пишет прямо в Args;
-- пустой элемент номер занимает, но ничего не записывает.
local function apply_team_order(Args)
	if not Args['team_order'] or Args['team_order'] == '' then
		return
	end
	local team_order_offset = (tonumber(Args['team_order_start']) or 1) - 1
	local tlist = mw.text.split(Args['team_order'], '%s*[;,]%s*')
	for k, tname in ipairs(tlist) do
		if tname ~= '' then
			Args['team' .. (k + team_order_offset)] = tname
		end
	end
end

-- Собирает team_list из team1, team2… начиная с позиции, следующей за N_teams.
-- Счёт идёт до первого пропущенного номера либо до bottom_pos (lowest_pos), если она не меньше top_pos.
-- При show_totals=yes добавляет в Args и team_list строку итогов — псевдокоманду
-- ppp.total_row_code, суммы в которую записывают подмодули стиля (ppp.add_to_totals).
-- Возвращает N_teams — номер последней позиции, total_row_name и show_totals.
local function build_team_list(Args, team_list, top_pos, bottom_pos, N_teams)
	while Args['team' .. N_teams + 1] ~= nil and (bottom_pos < top_pos or N_teams < bottom_pos) do
		N_teams = N_teams + 1
		-- team_list двусторонний: по номеру позиции — код команды, по коду — номер позиции
		-- (по нему находятся showteam и команда, на примечание которой ссылается другая).
		team_list[N_teams] = Args['team' .. N_teams]
		team_list[Args['team' .. N_teams]] = N_teams
	end

	local total_row_name = p_sub.total_row_code
	local show_totals = flag(Args['show_totals'], 'no')
	if show_totals then
		N_teams = N_teams + 1
		Args['team' .. N_teams] = total_row_name
		Args['name_' .. total_row_name] = 'Всего'
		Args['result' .. N_teams] = total_row_name
		-- Цвет задаётся именем из палитры, а не значением: проверка параметров сверяет цвета
		-- по именам, и шестнадцатеричный код собственного вывода модуля отметила бы как цвет вне палитры.
		Args['col_' .. total_row_name] = 'neutral'
		team_list[N_teams] = Args['team' .. N_teams]
		team_list[Args['team' .. N_teams]] = N_teams
	end

	return N_teams, total_row_name, show_totals
end

-- Дописывает title_source к title либо, если такой пары нет, table_header_source
-- к table_header. Возвращает true, если источник дописан: тогда build_footer не ставит
-- в колонтитул пометку «нет источника».
local function apply_title_source(Args)
	if Args['title'] and Args['title_source'] then
		Args['title'] = Args['title'] .. Args['title_source']
		return true
	elseif Args['table_header'] and Args['table_header_source'] then
		Args['table_header'] = Args['table_header'] .. Args['table_header_source']
		return true
	end
	return false
end

-- Возвращает baselink — страницу, из которой вызван модуль, — и template_name — цель
-- ссылок навбара (build_vte_text). baselink пуст, если вызов идёт через [[Модуль:Excerpt]]
-- или прямо со страницы, на которой выводится таблица. template_name задаётся одноимённым
-- параметром, по умолчанию — «:baselink#якорь таблицы». Ручной baselink применяется после
-- template_name и на него не влияет.

-- p._main берёт только template_name: ссылки на отчёты о матчах, в том числе с учётом
-- ручного baselink, ведёт ppp.resolve_report_links.
local function resolve_baselink(frame, currentTitle, Args, table_anchor)
	local baselink = frame:getParent():getTitle()
	if baselink == 'Модуль:Excerpt' then
		baselink = ''
	end
	if currentTitle.fullText == baselink then
		baselink = ''
	end
	local template_name = Args['template_name'] or (baselink ~= '' and (':' .. baselink .. (table_anchor ~= '' and '#' .. table_anchor or ''))) or ''

	local baselink_override = Args['baselink']
	if baselink_override and baselink_override ~= '' then
		baselink = baselink_override
		if currentTitle.fullText == baselink then
			baselink = ''
		end
	end

	return baselink, template_name
end

-- Выводимые строки ----------------------------------------------------------------------

-- Возвращает первую и последнюю выводимые позиции. Выборка из n_to_show строк (show_limit)
-- строится вокруг команды ii_show (showteam); без ii_show, а также при show_limit не меньше
-- числа позиций, не больше 1 или дробном выводится вся таблица. Команда у края таблицы даёт
-- первые или последние n_to_show строк, в середине — поровну строк выше и ниже неё, то есть
-- 2·⌊n_to_show/2⌋ + 1: при чётном show_limit на строку больше.
local function compute_display_range(top_pos, N_teams, ii_show, n_to_show)
	local ii_start, ii_end
	local check_n = ((n_to_show >= (N_teams - top_pos + 1)) or (n_to_show <= 1) or (n_to_show ~= math.floor(n_to_show)))
	if check_n or (not ii_show) then
		ii_start = top_pos
		ii_end = N_teams
	else
		-- Строк по каждую сторону от ii_show.
		local n_show_side = math.floor(n_to_show / 2)
		if (ii_show - top_pos + 1) <= n_show_side then
			-- У верхнего края: первые n_to_show строк.
			ii_start = top_pos
			ii_end = top_pos + n_to_show - 1
		elseif ii_show >= (N_teams + 1 - n_show_side) then
			-- У нижнего края: последние n_to_show строк.
			ii_start = N_teams + 1 - n_to_show
			ii_end = N_teams
		else
			-- В середине: n_show_side строк выше и ниже.
			ii_start = ii_show - n_show_side
			ii_end = ii_show + n_show_side
		end
	end
	return ii_start, ii_end
end

-- Делит выводимые строки на группы для столбца результатов: подряд идущие строки с одним
-- кодом результата (или без него) объединяются в одну ячейку с rowspan, а splitN обрывает
-- группу после позиции N. Возвращает таблицу по номеру первой строки группы:
-- {n = число строк, result = код результата или ''}.
local function result_groups(Args, result_by_pos, ii_start, ii_end)
	local groups = {}
	local ii = ii_start
	while ii <= ii_end do
		local result_local = result_by_pos[ii] or ''
		local n = 1
		while ii + n <= ii_end and not Args['split' .. (ii + n - 1)]
			and (result_by_pos[ii + n] or '') == result_local do
			n = n + 1
		end
		groups[ii] = {n = n, result = result_local}
		ii = ii + n
	end
	return groups
end

-- Заливка ячеек -------------------------------------------------------------------------

-- Возвращает класс заливки для утверждённого имени палитры — « st-green1» и т. п. —
-- либо пустую строку. Ведущий пробел нужен: имя дописывается к уже имеющемуся классу внутри
-- одного атрибута class, а двух атрибутов class в теге быть не может.

-- Цвет утверждённого имени и его тёмный вариант лежат в таблице стилей, поэтому такое имя
-- выводится классом. Произвольный цвет выводится встроенным стилем (resolve_bg_col) и класса
-- не получает: ссылки в такой ячейке тёмная тема находит по самому атрибуту style.
local function filled_class(Args, respre, result_local)
	if not result_local then
		return ''
	end
	local col_key = Args[respre .. 'col_' .. result_local]
	if not col_key or col_key == '' then
		return ''
	end
	local bg_col, origin = p_sub.palette_color(col_key)
	if not bg_col or bg_col == 'inherit' then
		return ''
	end

	-- Имя класса совпадает с именем цвета, поэтому переименование цвета в палитре требует
	-- правки и таблицы стилей — они и так меняются вместе.
	if origin == 'named' then
		return ' st-' .. mw.ustring.lower(mw.text.trim(col_key))
	end
	return ''
end

-- Возвращает встроенный стиль заливки ячейки по коду результата result_local: без кода —
-- прозрачный фон, без цвета для кода (col_<код>) или с нераспознанным цветом — inherit.
-- Для утверждённого имени палитры возвращает пустую строку: цвет тогда задаёт класс, см. filled_class.

-- С непрозрачным фоном всегда явно задаётся и цвет текста (ppp.text_color). Встроенный style
-- в тёмной теме не инвертируется: фон остаётся светлым, а унаследованный цвет текста становится светлым —
-- выходит светлый текст на светлом фоне. Для transparent и inherit цвет текста остаётся наследуемым:
-- такой фон меняется вместе с темой.
local function resolve_bg_col(Args, respre, result_local)
	if not result_local then
		return 'background-color:transparent; color:inherit;'
	end
	local col_key = Args[respre .. 'col_' .. result_local]
	if not col_key or col_key == '' then
		return 'background-color:inherit; color:inherit;'
	end
	-- Цвет применяется в любом написании, в том числе шестнадцатеричным кодом: так он задан
	-- во множестве статей, и отказ оставил бы их без заливки. Написания вне палитры отмечает
	-- отдельной категорией проверка параметров.
	local bg_col, origin = p_sub.palette_color(col_key)
	if not bg_col or bg_col == 'inherit' then
		return 'background-color:inherit; color:inherit;'
	end

	-- Утверждённое имя палитры выводится классом (filled_class): у класса есть вариант для
	-- тёмной темы, а встроенный стиль правилом CSS не перебить иначе как через !important на каждый цвет.
	if origin == 'named' then
		return ''
	end

	-- Произвольный цвет известен только здесь, и в таблице стилей его держать негде:
	-- он остаётся встроенным и варианта для тёмной темы не получает.
	return 'background-color:' .. bg_col .. '; color:' .. p_sub.text_color .. ';'
end

-- Части вывода --------------------------------------------------------------------------

-- Возвращает навбар шаблона template_name ([[Модуль:Navbar]], _ruwikiNavbar) с отбивкой
-- &nbsp; либо пустую строку, если template_name пуст.
local function build_vte_text(template_name, Args)
	if template_name == '' then
		return ''
	end
	local VTE_text = '&nbsp;' .. require('Модуль:Navbar')._ruwikiNavbar({template_name,})
	-- В сортируемой таблице (sortable_table=yes) ссылки навбара не срабатывают —
	-- ошибка движка, см. обсуждение:
	-- https://en.wikipedia.org/w/index.php?oldid=832717047#Sortable_link_disables_navbar_links?
	-- Обходится она снятием тегов abbr: они заменяются пробелами.
	if flag(Args['sortable_table'], 'no') then
		VTE_text = ustring.gsub(VTE_text, '<%/?abbr[^<>]*>', ' ')
	end
	return VTE_text
end

-- Возвращает respre — префикс полей результата, — текст ячейки заголовка столбца
-- результатов и notes_exist. При result_prefix=X поля результата читаются с префиксом:
-- X_resultN, X_col_<код>, X_text_<код>, X_res_col_header, X_status_<код>.
-- Примечание к заголовку (note_header_res) выводится только в полной таблице и выставляет notes_exist.
local function build_results_header(Args, full_table, frame, notes_exist)
	local respre = (Args['result_prefix'] or '') .. '_'
	respre = (respre == '_') and '' or respre
	local res_col_header_key = Args[respre .. 'res_col_header']
	local local_res_header = results_header[res_col_header_key] or res_col_header_key or ''
	local res_head_note = Args['note_header_res']
	local res_head_note_text = ''
	if full_table and res_head_note then
		notes_exist = true
		res_head_note_text = frame:expandTemplate{title = 'efn',
			args = {group = 'lower-alpha', res_head_note}
		}
	end
	local results_header_txt = '! scope="col" |' .. local_res_header .. res_head_note_text .. '\n'
	return respre, results_header_txt, notes_exist
end

-- Строит шапку таблицы: столбцы стиля (p_style.header) и, если шахматка выводится внутри
-- таблицы, разделительный столбец и столбцы шахматки (p_matches.header).
-- Возвращает массив строк разметки и число столбцов.
local function build_header(p_style, t, Args, pos_label, group_col, VTE_text, full_table, results_header_txt, match_table, p_matches, N_teams, top_pos, team_list, legs)
	local t_return = p_style.header(t, Args, pos_label, group_col, VTE_text, full_table, results_header_txt)
	if match_table then
		-- Ячейка над разделительным столбцом пуста, и атрибута scope у неё нет —
		-- иначе скринридер объявил бы её заголовком.
		t_return.count = t_return.count + 1
		table.insert(t_return.tab_text, '! class="unsortable st-spacer"| \n')

		t_return = p_matches.header(t_return, Args, N_teams, top_pos, team_list, legs)
	end
	return t_return.tab_text, t_return.count
end

-- Строит обозначение статуса команды из букв status_local (например, «PQ»): буквы через
-- запятую, каждая через letter_wrapper, в элементе st-status (полужирный) — в скобках
-- после названия либо с тире перед ним, по t_status.position; перед названием буквы
-- переводятся в строчные (string.lower действует только на латиницу). Знаки, которых нет
-- в t_status.code, пропускаются. Выведенные буквы отмечаются в t_status.called — по этим
-- отметкам build_footer собирает легенду. Без единой известной буквы возвращает пустую строку.
local function build_status_string(t_status, status_local, letter_wrapper)
	local status_string = ''
	if not status_local then
		return status_string
	end
	local status_let_first = true
	for jjj = 1, ustring.len(status_local) do
		local curr_letter = ustring.upper(ustring.sub(status_local, jjj, jjj))
		if t_status.code[curr_letter] then
			if status_let_first then
				status_string = letter_wrapper(curr_letter)
				status_let_first = false
			else
				status_string = status_string .. ', ' .. letter_wrapper(curr_letter)
			end
			t_status.called[curr_letter] = true
		end
	end
	if not status_let_first then
		if t_status.position == 'before' then
			status_string = '<span class="st-status">' .. string.lower(status_string) .. ' &ndash;</span> '
		else
			status_string = ' <span class="st-status">(' .. status_string .. ')</span>'
		end
	end
	return status_string
end

-- Открывает строку таблицы и добавляет в t её первые ячейки: место (при position_col),
-- группу (в полной таблице при group_col) и название команды со статусом и сноской.
-- Строка итогов получает класс sortbottom, который держит её внизу при сортировке; место у неё пусто.

-- Класс заливки ставится каждой ячейке, а не строке. Строкой было бы короче, но при
-- show_matches=yes в ту же строку дописываются клетки шахматки, и заливка по коду результата
-- просвечивала бы через них: клетки матчей оказались бы закрашены цветом квалификации.
-- Снять её там нельзя — прозрачная ячейка как раз показывает фон строки.
local function append_row_lead_cells(t, show_totals, team_code_ii, total_row_name, position_col, ii_fw, bg_col, bg_class, pos_num, full_table, group_col, group_txt, t_status, status_string, team_name, note_string)
	if show_totals and team_code_ii == total_row_name then
		t[#t + 1] = '|- class="sortbottom"\n'
	else
		t[#t + 1] = '|- \n'
	end
	if position_col then
		-- У строки итогов места нет: это не участник, а сумма по таблице.
		local pos_text = (show_totals and team_code_ii == total_row_name) and '' or pos_num
		t[#t + 1] = '| class="st-pos' .. bg_class .. '" style="' .. ii_fw .. bg_col .. '"| ' .. pos_text .. '\n'
	end
	if full_table and group_col then
		t[#t + 1] = '| class="' .. mw.text.trim(bg_class) .. '" style="' .. ii_fw .. bg_col .. '" |' .. group_txt .. '\n'
	end
	local team_string
	if t_status.position == 'before' then
		team_string = status_string .. team_name .. note_string
	else
		team_string = team_name .. note_string .. status_string
	end
	-- Название команды — заголовок строки, поэтому ячейка строится через «!».
	t[#t + 1] = '! scope="row" class="st-team' .. bg_class .. '" style="' .. ii_fw .. bg_col .. '"| ' .. team_string .. '\n'
end

-- Добавляет в t ячейку столбца результатов для группы строк, начинающейся с позиции ii
-- (см. result_groups); на остальных строках группы ничего не делает. res_col —
-- {groups, layout}: группы строк и места разрывов текста в них (ppp.result_layout).
-- Ячейка выводится, только если столбец показывается (results_defined). Возвращает notes_exist.
local function append_results_cell(t, frame, Args, respre, rand_val, res_col, get_result_text, note_id_list, ii, ii_show, notes_exist, results_defined)
	local group = res_col.groups[ii]
	if not group then
		return notes_exist
	end
	local N_rows_res = group.n
	local result_local = group.result
	-- Полужирным — если команда showteam (ii_show) входит в группу.
	local ii_fw
	if ii_show and (ii_show >= ii) and (ii_show <= (ii + N_rows_res - 1)) then
		ii_fw = 'font-weight:bold;'
	else
		ii_fw = 'font-weight:normal;'
	end
	local bg_col = resolve_bg_col(Args, respre, result_local ~= '' and result_local or nil)
	-- Класс заливки считается здесь, а не берётся у строки: ячейка объединяет несколько
	-- строк с одним результатом, и цвет у неё свой — тот, что у первой строки группы.
	local bg_class = filled_class(Args, respre, result_local ~= '' and result_local or nil)
	local note_res_string = ''
	local note_ref, note_text, note_id
	-- Примечание к результату (note_res_<код>) обрабатывается, только если столбец выводится:
	-- шаблон сноски регистрирует примечание уже при вызове expandTemplate,
	-- даже если ячейка не выводится, и в колонтитуле появилась бы пустая секция примечаний.
	if results_defined and Args['note_res_' .. result_local] then
		notes_exist = true
		local note_res_local = Args['note_res_' .. result_local]

		-- Значение — список через запятую, если каждый его элемент — код другого результата
		-- со своим note_res_; иначе оно целиком считается текстом примечания.
		local multiref = 1
		local note_res_local_table = mw.text.split(note_res_local, '%s*,%s*')
		if (#note_res_local_table > 1) then
			for _, note_res_loc in ipairs(note_res_local_table) do
				multiref = multiref * (Args['note_res_' .. note_res_loc] and 1 or 0)
			end
		else
			multiref = 0
		end

		if multiref < 1 then
			note_res_local_table = {note_res_local}
		end
		for _, note_res_item in ipairs(note_res_local_table) do
			if not Args['note_res_' .. note_res_item] then
				-- Собственный текст примечания.
				note_ref = respre .. 'res_' .. result_local
				note_id = '"table_note_res_' .. result_local .. rand_val .. '"'
				note_text = note_res_item
			else
				-- Отсылка к примечанию другого результата: берётся его текст.
				note_ref = respre .. 'res_' .. note_res_item
				note_id = '"table_note_res_' .. note_res_item .. rand_val .. '"'
				note_text = Args['note_res_' .. note_res_item]
			end
			-- Первое упоминание выводит сноску с текстом (efn), повторные — ссылку на неё (ref).
			if not note_id_list[note_ref] then
				note_id_list[note_ref] = note_id
				note_res_string = note_res_string .. frame:expandTemplate{title = 'efn',
					args = {group = 'lower-alpha', name = note_id, note_text}
				}
			else
				note_res_string = note_res_string .. frame:extensionTag{name = 'ref',
					args = {group = 'lower-alpha', name = note_id}
				}
			end
		end
	end
	-- Текст из шаблона fbmulticomp (опознаётся по имени в разметке) занимает ячейку без отступов,
	-- и места переноса в нём модуль не расставляет; fbmulticompefn означает, что внутри есть сноски.
	local text_result = get_result_text(result_local) or ''
	if text_result:match('fbmulticomp') then
		ii_fw = 'padding:0;' .. ii_fw
		if text_result:match('fbmulticompefn') then
			notes_exist = true
		end
	else
		-- Места переноса расставляются по правилам столбца — см. ppp.wrap_result_text.
		text_result = p_sub.wrap_result_text(text_result, res_col.layout[ii])
	end
	local text_field_result = '| class="' .. mw.text.trim(bg_class) .. '" style="' .. ii_fw .. bg_col
		.. '" rowspan="' .. N_rows_res .. '" |' .. text_result .. note_res_string .. '\n'
	if results_defined then
		t[#t + 1] = text_field_result
	end
	return notes_exist
end

-- Дописывает в строку команды ii клетки шахматки: разделительную ячейку и результаты
-- (p_matches.row). Перед этим p_matches.match_notes подставляет в значения match_ХХХ_УУУ
-- знаки сносок к результатам. Возвращает t и notes_exist.
local function append_match_row(t, frame, Args, rand_val, note_id_list, p_matches, top_pos, N_teams, team_list, ii, ii_start, ii_end, legs, ii_show, notes_exist)
	t[#t + 1] = '| class="st-spacer"| \n'
	if p_matches.match_notes(frame, Args, rand_val, note_id_list, top_pos, N_teams, team_list, ii, ii_start, ii_end, legs) then
		notes_exist = true
	end
	t = p_matches.row(t, Args, N_teams, top_pos, team_list, ii, ii_show, legs)
	return t, notes_exist
end

-- Строит нижний колонтитул: дату обновления, источник, правила классификации, пояснения к заливке клеток шахматки,
-- легенду статусов и список примечаний. Возвращает разметку блока reflist, а при notes_exist —
-- ещё и шаблон «Комментарии» со сносками группы lower-alpha.
local function build_footer(frame, Args, t_status, letter_wrapper, full_table, show_class_rules, hide_footer, notes_exist, title_source, p_matches)
	local t_footer = {}
	-- Без source ставится шаблон «нет источника»,
	-- если источник не дописан к заголовку (title_source).
	local update = Args['update'] or '(дата неизвестна)'
	local start_date = Args['start_date'] or '(дата неизвестна)'
	local source = Args['source'] or (title_source == true and '') or frame:expandTemplate{
		title = 'нет источника',
		args = {reason = 'Источник не задан', date = os.date('%B %Y')}
	}
	local class_rules = Args['class_rules'] or nil

	-- Дата обновления. При update=complete или hide_footer строки нет,
	-- при update=future выводится дата начала (start_date).
	local matches_text = Args['matches_text'] or 'матчей'
	local update_mode = string.lower(update)
	if update_mode ~= 'complete' and not hide_footer then
		if update == '' then
			table.insert(t_footer, 'Обновлено для ' .. matches_text .. ', сыгранных на. ')
		elseif update_mode == 'future' then
			table.insert(t_footer, 'Первые матчи пройдут ' .. start_date .. '. ')
		else
			table.insert(t_footer, 'Обновлено для ' .. matches_text .. ', сыгранных на ' .. update .. '. ')
		end
	end

	-- stack_footer=yes ставит источник с новой строки после даты,
	-- а буквы легенды статусов — каждую на свою строку.
	local footer_break = flag(Args['stack_footer'], 'no')

	local stack_string = '<br>'
	if footer_break and string.lower(update) ~= 'complete' and not hide_footer then
		table.insert(t_footer, stack_string)
	end
	if source ~= '' and not hide_footer then
		table.insert(t_footer, 'Источник: ' .. source)
	end
	if class_rules and full_table and show_class_rules and not hide_footer then
		if (#t_footer > 0) then
			table.insert(t_footer, '<br>')
		end
		table.insert(t_footer, 'Правила классификации: ' .. class_rules)
	end

	-- Пояснения к заливке клеток шахматки внутри таблицы (show_matches=yes) —
	-- те же, что под самостоятельной шахматкой: см. p_matches.footer_legend.
	if p_matches and not hide_footer then
		for _, sentence in ipairs(p_matches.footer_legend(Args)) do
			if #t_footer > 0 then
				table.insert(t_footer, '<br>')
			end
			table.insert(t_footer, sentence)
		end
	end

	-- Легенда статусов: буквы, отмеченные в t_status.called, в порядке t_status.letters.
	local status_exist = false
	local status_string = ''
	local curr_letter
	-- Строка перебирается побайтово, в расчёте на латиницу:
	-- буква не латиницей, заданная в status_order, в легенду не попадёт.
	for jjj = 1, #t_status.letters do
		curr_letter = string.upper(t_status.letters:sub(jjj, jjj))
		if t_status.called[curr_letter] then
			if (footer_break and status_exist) then
				status_string = status_string .. stack_string
			end
			if t_status.position == 'before' then
				status_string = status_string .. '<span class="st-status">' .. string.lower(letter_wrapper(curr_letter)) .. ' &ndash;</span> ' .. t_status.code[curr_letter] .. '; '
			else
				status_string = status_string .. '<span class="st-status">(' .. letter_wrapper(curr_letter) .. ')</span> ' .. t_status.code[curr_letter] .. '; '
			end
			status_exist = true
		end
	end
	-- Легенда выводится без завершающего «; ».
	if status_exist and not hide_footer then
		if (#t_footer > 0) then
			table.insert(t_footer, '<br>')
		end
		status_string = ustring.sub(status_string, 1, ustring.len(status_string) - 2)
		table.insert(t_footer, status_string)
	end

	local footer_text
	if notes_exist then
		if (#t_footer > 0) then
			table.insert(t_footer, '<br>')
		end
		table.insert(t_footer, 'Примечания:')
		footer_text = '<div class="reflist">' .. table.concat(t_footer) .. '</div>'
		footer_text = footer_text .. frame:expandTemplate{title = 'Комментарии',
			args = {group = 'lower-alpha'}
		}
	else
		footer_text = '<div class="reflist">' .. table.concat(t_footer) .. '</div>'
	end

	return footer_text
end

-- Дописывает в t замечания предпросмотра и отслеживающие категории: подсказку о ширине столбцов,
-- итоги проверки параметров, команды без показателей, ошибки автосборки, сверку итогов и
-- сверку сокращений с реестром. При notracking=yes после подсказки о ширине
-- ставится только категория «без отслеживания», и остальные проверки не выполняются.

-- На страницах, исключённых ppp.tracking_excluded, категорий нет; замечаний проверки
-- параметров там нет и в предпросмотре — они подставляются на место своих категорий.
-- match_legs — число кругов шахматки внутри таблицы (для подсказок о дерби) или nil.
local function append_tracking_categories(t, frame, Args, currentTitle, team_list, ii_start, ii_end, in_preview, auto_errors, match_legs)
	-- Подсказка о ширине столбцов названий и очков, подобранной модулем.
	-- Нужна, когда турнир разбит на группы: у каждой таблицы ширина своя,
	-- и рядом они выглядят несогласованно. Увидев значения всех групп,
	-- редактор задаёт наибольшее из них в teamwidth и pointswidth каждой — тогда ширина становится единой.

	-- Не выводится, когда ширина задана вручную, и когда подобранное значение не превышает
	-- нижней границы (10em для названий, 2,5em для очков).
	if in_preview() then
		local wide = {}
		if not Args['teamwidth'] then
			local measured = p_sub.measure_team_width(Args)
			if (tonumber(measured:match('^([%d%.]+)')) or 0) > 10 then
				wide[#wide + 1] = 'команд ' .. measured .. ' (|teamwidth=)'
			end
		end

		-- Столбец очков шире, когда очки трёхзначные или к ним приписаны сноски.
		-- Из-за сносок соседние таблицы расходятся по ширине сильнее всего:
		-- в одной группе команды разводили личными встречами, в другой нет.
		if not Args['pointswidth'] then
			local measured = p_sub.measure_points_width(Args)
			if (tonumber(measured:match('^([%d%.]+)')) or 0) > 2.5 then
				wide[#wide + 1] = 'очков ' .. measured .. ' (|pointswidth=)'
			end
		end
		if #wide > 0 then
			t[#t + 1] = p_sub.preview_note('info', 'Столбцы шире обычного: ' .. table.concat(wide, ', ')
				.. '. Чтобы таблицы на странице совпали по ширине, задайте эти параметры '
				.. 'наибольшими из значений во всех таблицах.')
		end
	end

	local exclude_tracking_categories = p_sub.tracking_excluded(currentTitle)
	if flag(Args['notracking'], 'no') then
		if not exclude_tracking_categories then
			table.insert(t, '[[Категория:Страницы, использующие спортивные таблицы без отслеживания]]')
		end
		return
	end
	local getTracking = require('Модуль:Sports table/argcheck').check
	local warning_categories, tracking_categories = getTracking(Args, frame:getParent().args)

	if #warning_categories > 0 then
		if in_preview() then
			for k = 1, #warning_categories do
				warning_categories[k] = p_sub.preview_note('error', p_sub.warning_text(warning_categories[k]))
			end
		end
	end
	if not exclude_tracking_categories then
		t[#t + 1] = p_sub.direct_invoke_category(frame)
		for k = 1, #warning_categories do
			t[#t + 1] = warning_categories[k]
		end
		for k = 1, #tracking_categories do
			t[#t + 1] = tracking_categories[k]
		end
	end

	-- Команда без единого заполненного показателя. Незаполненный показатель модуль считает нулём,
	-- и такая строка неотличима от команды без побед и голов. Обычная причина —
	-- опечатка в коде команды: показатели записаны под одним кодом, а сама она — под другим.

	-- При автосборке проверка не нужна: показатели считает модуль.
	if not flag(Args['auto_generate_standings'], 'no') then
		local blank = {}
		for ii = ii_start, ii_end do
			local code = team_list[ii]
			if code and code ~= p_sub.total_row_code and not p_sub.has_stats(Args, code) then
				blank[#blank + 1] = code
			end
		end
		if #blank > 0 then
			if in_preview() then
				t[#t + 1] = p_sub.preview_note('warning', 'не заполнено ни одного показателя: '
					.. table.concat(blank, ', ') .. '. Пустые показатели считаются нулями — '
					.. 'проверьте, не разошёлся ли код команды с кодом в её показателях')
			end
			if not exclude_tracking_categories then
				t[#t + 1] = '[[Категория:Страницы, использующие спортивные таблицы с командой без показателей]]'
			end
		end
	end

	-- Ошибки автосборки, в том числе стиль без её поддержки. Выводятся красным, в отличие
	-- от зелёной сверки итогов: это не расхождение источников, а неверно заполненная
	-- шахматка — опечатка в коде команды, нечитаемый счёт, повтор пары, нехватка матчей.

	-- Категория ставится и на сохранённой странице: при ошибке автосборка строит таблицу
	-- по тем матчам, которые смогла прочитать, и по виду страницы этого не заметно.
	if auto_errors and #auto_errors > 0 then
		if in_preview() then
			for k = 1, #auto_errors do
				t[#t + 1] = p_sub.preview_note('error', auto_errors[k])
			end
		end
		if not exclude_tracking_categories then
			t[#t + 1] = '[[Категория:Страницы, использующие спортивные таблицы с ошибками автосборки]]'
		end
	end

	-- Сверка итогов ([[Модуль:Sports table/totalscheck]]) не выполняется при showteam (team)
	-- и при автосборке. При автосборке показатели вычислены из тех же матчей, с которыми их
	-- предлагается сверять, и обе проверки становятся тождествами. Осмысленными остаются
	-- проверки самой автосборки — строение шахматки и её комплектность — и сверка счёта
	-- с карточками матча, которая сравнивает два независимо заполненных источника.
	if ((Args['showteam'] or Args['team']) == nil) and not flag(Args['auto_generate_standings'], 'no') then
		local totalscheck = require('Модуль:Sports table/totalscheck')
		local total_warnings = totalscheck.check(Args, team_list, ii_start, ii_end, yesno)
		if #total_warnings > 0 then
			if in_preview() then
				for k = 1, #total_warnings do
					t[#t + 1] = p_sub.preview_note('warning', total_warnings[k])
				end
			end
		end
		-- Показатели команд против результатов матчей match_ХХХ_УУУ, если они заданы.
		local match_warnings, confirmed = totalscheck.check_matches(Args, team_list, ii_start, ii_end)
		if #match_warnings > 0 then
			if in_preview() then
				for k = 1, #match_warnings do
					t[#t + 1] = p_sub.preview_note('warning', match_warnings[k])
				end
			end
		end

		-- Категория — только за расхождения у команд с полностью заполненной шахматкой
		if confirmed > 0 and not exclude_tracking_categories then
			t[#t + 1] = '[[Категория:Страницы, где показатели команд расходятся с результатами матчей]]'
		end
	end

	-- Сверка сокращений команд с реестром — ppp.check_codes. Выполняется только там, где
	-- лежат данные (ppp.data_page): на странице шаблона и его документации либо в статье
	-- с прямым вызовом модуля. Статьи, подключающие шаблон, реестр не загружают вовсе:
	-- сокращение — решение уровня шаблона, и исправляют его там же.
	local data_page = p_sub.data_page(frame)
	local registry = data_page and p_sub.code_registry_data()
	if registry and registry.teams then
		local mismatches, missing, undecided = p_sub.check_codes(Args, registry.teams, registry.pending)
		if in_preview() then
			for _, m in ipairs(mismatches) do
				t[#t + 1] = p_sub.preview_note('warning', 'сокращение ' .. m.shown .. ' — в реестре «'
					.. m.team .. '» записан как ' .. m.want)
			end
			if #undecided > 0 then
				t[#t + 1] = p_sub.preview_note('info', 'сокращение ещё не выбрано в реестре: '
					.. table.concat(undecided, ', ') .. ' — см. раздел pending в [[' .. p_sub.registry_page .. ']]')
			end
			if #missing > 0 then
				t[#t + 1] = p_sub.preview_note('info', 'нет в реестре сокращений: '
					.. table.concat(missing, ', ') .. '. Пополнить реестр можно вызовом '
					.. '{{#invoke:Sports group|codes|…}} — см. [[Модуль:Sports table/doc#Реестр сокращений]]')
			end
		end
		if #mismatches > 0 and not exclude_tracking_categories then
			t[#t + 1] = '[[Категория:Страницы, использующие спортивные таблицы с сокращениями вне реестра]]'
		end

		-- Подсказки о статьях про дерби для шахматки внутри таблицы — ppp.derby_hints.
		-- Самостоятельную шахматку и шахматку под таблицей проверяет [[Модуль:Sports results]].
		if match_legs and in_preview() then
			for _, hint in ipairs(p_sub.derby_hints(Args, team_list, ii_start, ii_end, match_legs, registry.derbies)) do
				t[#t + 1] = p_sub.preview_note('info', 'дерби: ' .. hint)
			end
		end
	end
end

-- Экспортируемые функции ----------------------------------------------------------------

-- Строит турнирную таблицу по готовому набору аргументов и возвращает её разметку вместе
-- с колонтитулом, замечаниями предпросмотра и категориями.

-- Отделена от p.main, чтобы её мог вызвать другой модуль: [[Модуль:Sports group]] собирает
-- таблицу и отчёты о матчах одним #invoke и передаёт сюда уже отобранные аргументы. Args
-- изменяется на месте: автосборка, team_order, строка итогов и источник заголовка записывают в него свои поля.
-- frame нужен для разворачивания шаблонов и для обращения к родительскому вызову.
function p._main(frame, Args)
	local ii_start, ii_end
	local notes_exist = false
	local t = {}
	local t_return
	local team_list = {}
	-- Якорь таблицы при section: «sports-table-<section>», пробелы заменены на «_».
	local table_anchor = ustring.gsub(Args['section'] and 'sports-table-' .. Args['section'] or '', ' ', '_')

	-- Обёртка над ppp.in_preview ради краткости:
	-- вызовов много, в том числев append_tracking_categories, куда она передаётся.
	local function in_preview()
		return p_sub.in_preview(frame)
	end

	local currentTitle = mw.title.getCurrentTitle()

	-- Вызов для другого раздела страницы ничего не выводит — см. ppp.skip_section.
	if p_sub.skip_section(frame) then
		return ''
	end

	-- При show_matches=only вместо турнирной таблицы выводится шахматка:
	-- вызов целиком передаётся p_results._main с теми же аргументами
	-- (team_order, name_ХХХ, match_ХХХ_УУУ и т. д.).
	if string.lower(Args['show_matches'] or '') == 'only' then
		-- Параметры проверяются здесь, набором турнирной таблицы; при notracking=yes — нет.
		-- Шахматка проверяет их сама только при вызове без style — так она отличает
		-- самостоятельный вызов от делегированного.

		-- Без этой проверки путь show_matches=only со style оставался бы непроверенным,
		-- а именно им шахматку выводят в шаблонах групповых этапов.
		local p_results = require('Модуль:Sports results')
		local checks = ''
		if not flag(Args['notracking'], 'no') then
			local warn, track = require('Модуль:Sports table/argcheck').check(Args, frame:getParent().args)
			local exclude = p_sub.tracking_excluded(currentTitle)
			if p_sub.in_preview(frame) then
				for k = 1, #warn do
					checks = checks .. p_sub.preview_note('error', p_sub.warning_text(warn[k]))
				end
			end
			if not exclude then
				checks = checks .. table.concat(warn) .. table.concat(track)
			end
		end
		-- Аргументы передаются уже прочитанные: перечитывать их из frame значило бы при
		-- вызове из Sports group развернуть и данные матчей, которые здесь не нужны.
		return p_results._main(frame, Args) .. checks
	end

	-- Дробные очки и поправки можно записывать по-русски — «0,5», «−3»: они приводятся к виду,
	-- который читают автосборка, стиль и сверки (см. ppp.read_decimals).
	p_sub.read_decimals(Args)

	-- Нужна только цель навбара — см. resolve_baselink.
	local _, template_name = resolve_baselink(frame, currentTitle, Args, table_anchor)

	-- Позиции таблицы: от highest_pos (по умолчанию 1) до lowest_pos; lowest_pos меньше
	-- highest_pos (по умолчанию 0) снимает ограничение. N_teams начинается с позиции перед
	-- первой: teamN с номерами меньше highest_pos не читаются.
	local top_pos = tonumber(Args['highest_pos']) or 1
	local bottom_pos = tonumber(Args['lowest_pos']) or 0
	local N_teams = top_pos - 1

	-- Подмодуль стиля. Имя приходит из параметра, поэтому загрузка защищена: при опечатке
	-- или удалённом стиле такого подмодуля нет, и обычный require уронил бы весь модуль.
	-- Вместо этого берётся стиль по умолчанию: таблица строится, пусть и не тем стилем,
	-- который задавал редактор. Нераспознанный style отмечает отдельной категорией.
	local style_def = Args['style'] or 'WDL'
	local ok, p_style = pcall(require, 'Модуль:Sports table/' .. style_def)
	if not ok then
		p_style = require('Модуль:Sports table/WDL')
	end

	-- Случайное число отличает идентификаторы сносок этой таблицы от сносок других таблиц
	-- на странице. Генератор засевается один раз при загрузке общего подмодуля (p_sub).
	local rand_val = math.random()

	local full_table, hide_results, hide_footer, show_class_rules = resolve_table_mode(Args)

	-- results_defined — выводится ли столбец результатов; решается ниже, после выбора строк.
	local results_defined = false
	local respre, results_header_txt
	respre, results_header_txt, notes_exist = build_results_header(Args, full_table, frame, notes_exist)

	-- Буквы и тексты статусов — от подмодуля стиля (см. ppp.status).
	local t_status = p_style.status(Args)

	-- Автосборка ([[Модуль:Sports table/auto]]): показатели команд вычисляются по
	-- результатам матчей и записываются в Args, после чего таблица строится обычным порядком.

	-- Вызывается до apply_team_order: автосборка сама читает teamN и team_order, убирает их
	-- и записывает итоговый порядок в teamN.
	-- Признаки берутся у самого стиля, а не перечисляются здесь списком: так новый стиль
	-- не требует правок в этом модуле. auto_supported объявляют стили, таблицу которых можно собрать
	-- из результатов матчей. write_ha объявляют стили, читающие раздельные домашние и выездные показатели:
	-- общих полей они не читают вовсе, и проверка параметров отметила бы их как неизвестные. write_ot объявляют стили
	-- со столбцами овертайма и серии: исходы для них автосборка берёт из result_ХХХ_УУУ.
	local auto_errors
	if flag(Args['auto_generate_standings'], 'no') then
		if p_style.auto_supported then
			auto_errors = require('Модуль:Sports table/auto').generate(Args, top_pos,
				p_style.write_ha, p_style.write_ot)
		else
			auto_errors = {'стиль ' .. style_def .. ' не поддерживает |auto_generate_standings='}
		end
	end

	apply_team_order(Args)

	-- В предпросмотре строка итогов включается сама: суммы по столбцам видны рядом
	-- с предупреждениями сверки итогов. В сохранённой статье она выводится только
	-- при show_totals=yes.
	-- Не включается она, когда show_totals задан явно (редактор уже решил), когда
	-- задан no_totals_check=yes (сверка отключена вручную) и когда таблица собрана
	-- из команд разных групп по одной — рейтинг вида «команды, занявшие третьи места»:
	-- там сумма побед не равна сумме поражений, и итоги были бы заведомо неверными.
	-- Последнее условие — то же, по которому пропускается сверка сумм, и берётся из того же
	-- места (totalscheck.is_cross_group), чтобы правила не разошлись.
	local totals_set = Args['show_totals'] ~= nil and Args['show_totals'] ~= ''
	if in_preview() and not totals_set and not flag(Args['no_totals_check'], 'no') then
		local groups = {}
		local ii = 1
		while Args['team' .. ii] do
			groups[#groups + 1] = Args['group_' .. Args['team' .. ii]]
			ii = ii + 1
		end
		if not require('Модуль:Sports table/totalscheck').is_cross_group(groups) then
			Args['show_totals'] = 'yes'
		end
	end

	local total_row_name, show_totals
	N_teams, total_row_name, show_totals = build_team_list(Args, team_list, top_pos, bottom_pos, N_teams)

	local position_col = flag(Args['show_positions'], 'yes')

	local group_col = flag(Args['show_groups'], 'no')

	-- show_matches=below и below_levelN: шахматка выводится не внутри таблицы, а отдельным
	-- блоком под ней, с собственным заголовком раздела. Уровень заголовка задаётся суффиксом:
	-- below_level3 даёт «===»; без суффикса — второй уровень, как у разделов статьи.

	-- Расписание (matches_format=schedule, series, series_split) в строки таблицы не встроить:
	-- у него свои столбцы — туры, а не соперники. Поэтому при нём show_matches=yes тоже выводит
	-- его под таблицей.
	local below_level = nil
	do
		local sm = string.lower(Args['show_matches'] or '')
		local fmt = mw.ustring.lower(mw.text.trim(Args['matches_format'] or ''))
		local schedule = fmt == 'schedule' or fmt == 'series' or fmt == 'series_split'
		if sm == 'below' or (schedule and flag(Args['show_matches'], 'no')) then
			below_level = 2
		else
			local n = sm:match('^below_level(%d)$')
			if n then
				below_level = tonumber(n)
			end
		end
	end

	-- Шахматка внутри таблицы (show_matches=yes); при below она выводится отдельно.
	local match_table = not below_level and flag(Args['show_matches'], 'no')
	local p_matches = match_table and require('Модуль:Sports results')
	-- Число кругов нужно и шапке, и строкам шахматки.
	local legs = tonumber(Args['legs']) or 1

	-- Подпись столбца мест — postitle; без столбца её нет.
	local pos_label = Args['postitle'] or '<abbr title="Позиция">Поз</abbr>'
	if position_col == false then
		pos_label = nil
	end

	local show_status = flag(Args['show_status'], 'yes')

	local VTE_text = build_vte_text(template_name, Args)

	local title_source = apply_title_source(Args)

	if table_anchor ~= '' then
		table.insert(t, '<span class="anchor" id="' .. table_anchor .. '"></span>\n')
	end

	-- Шапка таблицы.
	local N_cols
	t, N_cols = build_header(p_style, t, Args, pos_label, group_col, VTE_text, full_table, results_header_txt, match_table, p_matches, N_teams, top_pos, team_list, legs)

	-- Выводимые позиции — см. compute_display_range.
	local ii_show = team_list[Args['showteam'] or Args['team']]
	local n_to_show = tonumber(Args['show_limit']) or N_teams
	ii_start, ii_end = compute_display_range(top_pos, N_teams, ii_show, n_to_show)

	-- Столбец результатов. result_by_pos — код результата (resultN) каждой выводимой позиции.
	local result_by_pos = {}
	for ii = ii_start, ii_end do
		result_by_pos[ii] = Args[respre .. 'result' .. ii]
	end
	-- Текст результата (text_<код>) по коду, с запоминанием; false в кэше — «текста нет».
	local text_by_result = {}
	local function get_result_text(result_val)
		local cached = text_by_result[result_val]
		if cached == nil then
			cached = Args[respre .. 'text_' .. result_val] or false
			text_by_result[result_val] = cached
		end
		if cached == false then
			return nil
		end
		return cached
	end
	-- Столбец выводится, если хоть у одной выводимой позиции есть текст результата
	-- и он не скрыт параметром hide_results.
	if not hide_results then
		for ii = ii_start, ii_end do
			if result_by_pos[ii] and get_result_text(result_by_pos[ii]) then
				results_defined = true
			end
		end
	end
	-- Группы строк столбца результатов и раскладка их текста. Раскладка считается по всем
	-- ячейкам сразу: ширину столбца задают все они вместе с заголовком.
	local res_col = {groups = result_groups(Args, result_by_pos, ii_start, ii_end), layout = {}}
	if full_table and results_defined then
		local cells, starts = {}, {}
		for ii = ii_start, ii_end do
			local group = res_col.groups[ii]
			if group then
				cells[#cells + 1] = {text = get_result_text(group.result) or '', rows = group.n,
					tail = Args['note_res_' .. group.result] and 2 or 0}
				starts[#starts + 1] = ii
			end
		end
		local layout = p_sub.result_layout(cells, results_header_txt:match('|([^\n]*)') or '')
		for k, ii in ipairs(starts) do
			res_col.layout[ii] = layout[k]
		end
	end

	-- Без столбца результатов из готовой шапки удаляется его заголовок: разметка
	-- сворачивается в одну строку, и заголовок вырезается из неё. Заголовок ищется как простой
	-- текст, а не как шаблон Lua: скобки, «-» и «%» в подписи или в сноске к ней не мешают.
	if full_table and not results_defined then
		local t_str = table.concat(t)
		local cut_from, cut_to = t_str:find(results_header_txt, 1, true)
		if cut_from then
			t_str = t_str:sub(1, cut_from - 1) .. t_str:sub(cut_to + 1)
		end
		N_cols = N_cols - 1
		t = {}
		table.insert(t, t_str)
	end

	-- Строки таблицы.
	local team_name, team_code_ii, pos_num, group_txt
	local note_string, note_local
	local note_id_list = {}
	local hth_id_list = {}
	-- Буквы статуса выводятся русскими (rus_letters), если не задано
	-- keep_english_status_labels=yes.
	local letter_wrapper
	if flag(Args["keep_english_status_labels"], 'no') then
		letter_wrapper = function(x)
			return x
		end
	else
		letter_wrapper = function(x)
			return rus_letters[x] or x
		end
	end
	for ii = ii_start, ii_end do
		team_code_ii = team_list[ii]
		-- pos_ХХХ заменяет выводимый номер места, name_ХХХ — название (без него выводится код).
		pos_num = Args['pos_' .. team_code_ii] or ii
		group_txt = Args['group_' .. team_code_ii] or ' '
		team_name = Args['name_' .. team_code_ii] or team_code_ii
		note_local = Args['note_' .. team_code_ii] or nil

		-- Код результата позиции: по нему берутся заливка ячеек строки (встроенным стилем
		-- или классом, см. resolve_bg_col и filled_class) и текст столбца результатов.
		local result_local = result_by_pos[ii]
		local bg_col = resolve_bg_col(Args, respre, result_local)
		local bg_class = filled_class(Args, respre, result_local)

		-- Полужирным выделяются команда showteam и строка итогов.
		local ii_fw = ii == ii_show and 'font-weight:bold;' or 'font-weight:normal;'
		if show_totals and team_code_ii == total_row_name then
			ii_fw = 'font-weight:bold;'
		end

		if note_local and full_table then
			notes_exist = true
			-- note_ХХХ — собственный текст сноски либо код другой команды, чьё примечание
			-- переиспользуется: при note_ААА=Текст и note_БББ=ААА у обеих одна сноска.
			-- Выведена ли та сноска в показанной части таблицы, решает ppp.build_note.
			local note_local_ref_text = Args['note_' .. note_local]
			local is_own = not note_local_ref_text
			if is_own then
				note_string = p_sub.build_note(frame, rand_val, note_id_list, ii_start, ii_end, team_code_ii, note_local, true)
			else
				note_string = p_sub.build_note(frame, rand_val, note_id_list, ii_start, ii_end, team_code_ii, nil, false, note_local, team_list[note_local], note_local_ref_text)
			end
		else
			note_string = ''
		end

		-- Буквы статуса (status_ХХХ); show_status=no их скрывает.
		local status_local = show_status and Args[respre .. 'status_' .. team_code_ii] or nil
		local status_string = build_status_string(t_status, status_local, letter_wrapper)

		append_row_lead_cells(t, show_totals, team_code_ii, total_row_name, position_col, ii_fw, bg_col, bg_class, pos_num, full_table, group_col, group_txt, t_status, status_string, team_name, note_string)
		-- Ячейки показателей строит подмодуль стиля.
		t_return = p_style.row(frame, t, Args, notes_exist, hth_id_list, full_table, rand_val, team_list, team_code_ii, ii_start, ii_end, ii_fw, bg_col, bg_class, ii)
		t = t_return.t
		notes_exist = t_return.notes_exist
		hth_id_list = t_return.hth_id_list

		if full_table then
			notes_exist = append_results_cell(t, frame, Args, respre, rand_val, res_col, get_result_text, note_id_list, ii, ii_show, notes_exist, results_defined)
		end

		-- Клетки шахматки в той же строке (show_matches=yes).
		if match_table then
			t, notes_exist = append_match_row(t, frame, Args, rand_val, note_id_list, p_matches, top_pos, N_teams, team_list, ii, ii_start, ii_end, legs, ii_show, notes_exist)
		end

		-- splitN — разделительная линия после позиции N, кроме последней выводимой.
		-- Высоту и заливку линии, в том числе в тёмной теме, задаёт класс st-split.
		if Args['split' .. ii] and (ii < ii_end) then
			-- Ширина линии зависит от N_cols: нужно 2·N_cols знаков «|».
			t[#t + 1] = '|- class="st-split"\n'
			t[#t + 1] = string.rep('|', 2 * N_cols) .. '\n'
		end
	end

	table.insert(t, '|}')

	-- Нижний колонтитул.
	local t_footer = build_footer(frame, Args, t_status, letter_wrapper, full_table, show_class_rules, hide_footer, notes_exist, title_source,
		match_table and p_matches or nil)

	table.insert(t, t_footer)

	-- Ссылки со счёта на якоря отчётов о матчах ведутся туда, где эти отчёты лежат;
	-- решает ppp.report_link_resolver, по каждому якорю отдельно.
	p_sub.resolve_report_links(frame, Args, t)

	append_tracking_categories(t, frame, Args, currentTitle, team_list, ii_start, ii_end, in_preview, auto_errors,
		match_table and legs or nil)
	-- Шахматка под таблицей (show_matches=below): p_results._main, как и при
	-- show_matches=only, но с переопределениями. matches_title, matches_team_header
	-- и matches_source (по умолчанию source) заменяют заголовок, подпись угловой ячейки
	-- и источник; незаданные matches_title и matches_team_header ключа в override не дают,
	-- и шахматка берёт title и team_header турнирной таблицы.

	-- matches_note_ХХХ передаётся шахматке как её note_ХХХ; значение вида note_ХХХ заменяется
	-- текстом этого примечания таблицы. Без matches_team_notes=yes остальным выводимым
	-- командам передаётся пустой note_ХХХ: пустое примечание шахматка не выводит, и примечания
	-- таблицы в неё не переходят.
	if below_level then
		local override = {
			['show_matches'] = 'only',
			['title'] = Args['matches_title'],
			['team_header'] = Args['matches_team_header'],
			['source'] = Args['matches_source'] or Args['source'],
		}
		local pass_notes = flag(Args['matches_team_notes'], 'no')
		for ii = ii_start, ii_end do
			local code = team_list[ii]
			if code then
				local own = Args['matches_note_' .. code]
				if own then
					override['note_' .. code] = own:match('^note_') and Args[own] or own
				elseif not pass_notes then
					override['note_' .. code] = ''
				end
			end
		end

		local header = Args['below_results_header'] or 'Результаты'
		local eq = string.rep('=', below_level)
		t[#t + 1] = '\n' .. eq .. ' ' .. header .. ' ' .. eq .. '\n'
		t[#t + 1] = require('Модуль:Sports results')._main(frame, Args, override)
	end

	-- Обёртка горизонтальной прокрутки. Столбцов у таблицы до одиннадцати, и на узком
	-- экране она шире его: без обёртки горизонтально едет вся страница вместе с заголовком
	-- и текстом статьи, а с обёрткой — только сама таблица.
	-- Устроена так же, как у шахматки: внешний блок обрезает, внутренний прокручивает.
	-- Класс noresize нужен, чтобы приложение не пыталось вписать её в экран собственными средствами.
	-- Перевод строки после открывающих блоков нужен: разметка «{|», которой начинается
	-- таблица, разбирается, только когда стоит в начале строки. Перед закрывающими тегами
	-- он тоже нужен — иначе «|}» окажется не в начале своей строки.
	return p_sub.styles()
		.. '<div class="st-scroll-outer"><div class="noresize st-scroll-inner">\n'
		.. table.concat(t)
		.. '\n</div></div>'
end

-- Точки входа для #invoke ---------------------------------------------------------------

-- Читает аргументы вызова — параметры шаблона имеют преимущество перед параметрами самого
-- #invoke (parentFirst) — и передаёт их p._main.
function p.main(frame)
	local getArgs = require('Модуль:Arguments').getArgs
	return p._main(frame, getArgs(frame, {parentFirst = true}))
end

-- Сборщик сокращений команд для реестра — s.codes в [[Модуль:Sports table/source]].
-- Та же точка входа есть у Sports results и Sports group.
function p.codes(frame)
	return require('Модуль:Sports table/source').codes(frame)
end

return p