Модуль:Кубковая сетка
Перейти к навигации
Перейти к поиску
Для документации этого модуля может быть создана страница Модуль:Кубковая сетка/doc
--[==[
================================================================================
Модуль:Кубковая сетка
------------------------------------------------------------------------------
Турнирная сетка кубкового (олимпийского) турнира + сводная таблица матчей.
Вызывается из «Шаблон:Кубковая сетка» так: {{#invoke:Кубковая сетка|main}}
(все параметры берутся из вызова шаблона). Внешних зависимостей нет:
ни Модуль:Arguments, ни другие модули не требуются.
СХЕМА ПАРАМЕТРОВ (плоская, имена латиницей, регистр важен)
sKmJ_<поле> поле матча J этапа K s1m3_team1 = BRA
sK_<поле> поле этапа K s1_name = 1/8 финала
third_<поле> поле матча за 3-е место third_score1 = 2
team_<КОД> реестр: название команды team_FRA = [[…|Франция]]
flag_<КОД> реестр: флаг команды flag_FRA = Flag of France.svg
остальное глобальные параметры title, stages, summary, …
Любое поле матча, даже неизвестное модулю, сохраняется в m.f[<поле>].
Поэтому новые поля (ссылки на отчёты, статусы, маркеры хозяев и т. п.)
добавляются без изменения разбора: достаточно начать их где-то использовать.
ГЕОМЕТРИЯ СЕТКИ (CSS Grid)
N этапов, R = 2^(N-1) вертикальных «слотов» (= матчей первого этапа).
Строка 1 — заголовки этапов. Матч J этапа K занимает
строки 2 + (J-1)·2^(K-1) … span 2^(K-1)
колонку 2K-1
и поэтому сам оказывается по центру между своими «предками» — матчами
2J-1 и 2J этапа K-1. Колонки 2K отданы соединительным линиям.
Никаких фиксированных координат: число этапов и матчей — любое (до
CFG.maxStages), вся геометрия выводится из этих двух формул.
СТРУКТУРА ФАЙЛА
1. CFG — настройки, тексты, словари (обычно правят только его)
2. U — утилиты: да/нет, даты, числа, флаги
3. Разбор — плоские параметры → этапы → матчи → поля
4. Модель — команды, счёт, победитель, статус, автозаполнение
5. Сетка — вывод CSS Grid
6. Таблица — сводка матчей (только заполненные)
7. Точки входа
================================================================================
]==]
local p = {}
--------------------------------------------------------------------------------
-- 1. КОНФИГУРАЦИЯ
--------------------------------------------------------------------------------
local CFG = {}
CFG.maxStages = 7 -- 7 этапов = начиная с 1/64 финала
CFG.flagSize = '23x15px' -- размер флага по умолчанию (параметр flagsize)
CFG.fileNamespace = 'File' -- каноническое имя работает в вики на любом языке
CFG.dataPrefix = 'Модуль:Кубковая сетка/данные/' -- для параметра data=
CFG.errorCategory = nil -- напр. 'Страницы с ошибками в шаблоне Кубковая сетка'
CFG.i18n = {
final = 'Финал',
semifinal = 'Полуфиналы',
quarter = 'Четвертьфиналы',
fraction = '1/%d финала', -- для этапов раньше четвертьфинала
penSuffix = 'пен.', -- приписка к победителю в серии пенальти
aetSuffix = 'доп. вр.', -- приписка к победителю в доп. время
shootout = 'Пенальти',
attendance = 'Зрителей: ',
referee = 'Судья: ',
report = 'отчёт',
noScore = '–:–',
scoreSep = ':',
captionSep = ' — ', -- «30 июня — Сочи»
tbd = '', -- текст вместо неизвестной команды
warnPrefix = 'Кубковая сетка: ',
}
CFG.months = { 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря' }
-- Статусы матча. Ключ → CSS-класс cb-status-<ключ>; label выводится в сводке.
CFG.statuses = {
scheduled = { label = 'запланирован' },
live = { label = 'идёт' },
finished = {},
postponed = { label = 'перенесён' },
cancelled = { label = 'отменён' },
}
CFG.statusAliases = {
scheduled = 'scheduled', planned = 'scheduled',
['запланирован'] = 'scheduled',
live = 'live', ['идёт'] = 'live', ['идет'] = 'live',
finished = 'finished', played = 'finished',
['завершён'] = 'finished', ['завершен'] = 'finished', ['сыгран'] = 'finished',
postponed = 'postponed', ['перенесён'] = 'postponed', ['перенесен'] = 'postponed',
cancelled = 'cancelled', canceled = 'cancelled',
['отменён'] = 'cancelled', ['отменен'] = 'cancelled',
}
-- Синонимы имён полей (матча и этапа). Позволяют локализовать параметры,
-- не трогая логику: s1m1_команда1 == s1m1_team1.
CFG.fieldAliases = {
['команда1'] = 'team1', ['команда2'] = 'team2',
['флаг1'] = 'flag1', ['флаг2'] = 'flag2',
['счёт1'] = 'score1', ['счёт2'] = 'score2', ['счет1'] = 'score1', ['счет2'] = 'score2',
['пен1'] = 'pen1', ['пен2'] = 'pen2', pens1 = 'pen1', pens2 = 'pen2',
['дата'] = 'date', ['время'] = 'time', ['город'] = 'city', ['стадион'] = 'stadium',
venue = 'stadium', ['зрители'] = 'attendance', ['судья'] = 'referee',
['отчёт'] = 'report', ['статус'] = 'status', ['победитель'] = 'winner',
et = 'aet', ['доп'] = 'aet', ['название'] = 'name',
}
-- Дополнительные (вне дерева) матчи: выводятся в колонке финала.
-- key — префикс параметров (third_team1 …) и имя выключателя (third = нет)
-- title — заголовок по умолчанию (переопределяется параметром <key>_title)
-- autofill — 'losers': участники = проигравшие полуфиналов
-- beforeFinal — место в сводной таблице (перед финалом или после)
-- Чтобы добавить, например, матч за 5-е место, достаточно ещё одной строки:
-- { key = 'fifth', title = 'Матч за пятое место' },
CFG.extras = {
{ key = 'third', title = 'Матч за третье место', autofill = 'losers', beforeFinal = true },
}
-- Известные глобальные параметры (остальные попадут в предупреждения при debug=да).
CFG.globals = {
title = true, stages = true, summary = true, summary_title = true,
summary_stage = true, summary_sort = true, bracket = true, autofill = true, hide_empty = true,
flagtemplate = true, flagsize = true, theme = true, class = true,
styles = true, data = true, debug = true,
}
-- Служебные поля матча, которые не считаются «данными» (для решения,
-- показывать ли доп. матч и считать ли матч заполненным).
CFG.metaFields = { title = true, position = true, class = true, hide = true, summary = true }
--------------------------------------------------------------------------------
-- 2. УТИЛИТЫ
--------------------------------------------------------------------------------
local U = {}
--- Обрезает пробелы; пустую строку превращает в nil.
function U.clean(v)
if v == nil then return nil end
v = mw.text.trim(tostring(v))
if v == '' then return nil end
return v
end
local YES = { yes = true, y = true, ['true'] = true, ['1'] = true, on = true,
['да'] = true, ['д'] = true, ['вкл'] = true }
local NO = { no = true, n = true, ['false'] = true, ['0'] = true, off = true,
['нет'] = true, ['н'] = true, ['выкл'] = true }
--- Разбор «да/нет». Возвращает default, если значение пустое или непонятное.
function U.yesno(v, default)
v = U.clean(v)
if not v then return default end
v = mw.ustring.lower(v)
if YES[v] then return true end
if NO[v] then return false end
return default
end
function U.pow2(n)
local r = 1
for _ = 1, n do r = r * 2 end
return r
end
--- Наименьшее k, при котором 2^k >= n.
function U.ceilLog2(n)
local k, v = 0, 1
while v < n do v = v * 2; k = k + 1 end
return k
end
--- Оставляет в строке только безопасные для class="" символы.
function U.safeClass(s)
if not s then return nil end
s = s:gsub('[^%w%-_ ]', '')
if s == '' then return nil end
return s
end
--- Дата: 'ГГГГ-ММ-ДД' → '30 июня' (или '30 июня 2018'); любой другой текст — как есть.
function U.formatDate(v, withYear)
if not v then return nil end
local y, mo, d = v:match('^(%d%d%d%d)%-(%d%d?)%-(%d%d?)$')
local month = y and CFG.months[tonumber(mo)]
if month then
local s = tonumber(d) .. ' ' .. month
if withYear then s = s .. ' ' .. y end
return s
end
return v
end
--- Число зрителей: '42873' → '42 873' по правилам языка вики; текст — как есть.
function U.formatNumber(v)
if not v then return nil end
local digits = v:gsub(' ', ''):gsub('\194\160', ''):gsub('%s', '')
if digits:match('^%d+$') then
return mw.getContentLanguage():formatNum(tonumber(digits))
end
return v
end
--- Ссылка на отчёт: URL → внешняя ссылка «отчёт»; викитекст — как есть.
function U.reportLink(v)
if v:match('^https?://') or v:match('^//') then
return '[' .. v .. ' ' .. CFG.i18n.report .. ']'
end
return v
end
local IMAGE_EXT = { svg = true, png = true, jpg = true, jpeg = true, gif = true, webp = true }
--- Создаёт функцию отрисовки флага с учётом реестра и настроек вызова.
-- Значение флага может быть:
-- • кодом из реестра (flag_FRA = …) → берётся значение из реестра;
-- • именем файла (Flag of France.svg) → [[File:…|23x15px|border|link=]];
-- • готовым викитекстом/результатом шаблона → выводится как есть;
-- • коротким кодом при заданном flagtemplate → {{<flagtemplate>|код}}.
function U.makeFlagRenderer(ctx)
local cache = {}
return function (v)
v = U.clean(v)
if not v then return nil end
if cache[v] ~= nil then return cache[v] or nil end
local src = ctx.registry.flag[v] or v
local out
if src:find('[%[{<\127]') then
out = src
else
local ext = src:match('%.(%w+)$')
if ext and IMAGE_EXT[ext:lower()] then
out = string.format('[[%s:%s|%s|border|link=]]', CFG.fileNamespace, src, ctx.flagSize)
elseif ctx.flagTemplate then
out = ctx.frame:expandTemplate{ title = ctx.flagTemplate, args = { src } }
else
out = src -- например, эмодзи-флаг
end
end
cache[v] = out or false
return out
end
end
--------------------------------------------------------------------------------
-- 3. РАЗБОР ПАРАМЕТРОВ
--------------------------------------------------------------------------------
local function newMatch(key)
return { key = key, f = {} }
end
local function newStage()
return { f = {}, matches = {}, maxIndex = 0 }
end
--- Превращает плоский список параметров в иерархию.
-- Возвращает таблицу d:
-- d.g глобальные параметры
-- d.stages[K] { f = поля этапа, matches = { [J] = матч }, maxIndex }
-- d.extras[key] доп. матчи (third …)
-- d.registry { team = {КОД = название}, flag = {КОД = флаг} }
-- d.warnings список сообщений об ошибках данных
local function parse(args)
local d = {
g = {}, stages = {}, extras = {}, warnings = {}, unknown = {},
registry = { team = {}, flag = {} },
}
local extraKeys = {}
for _, spec in ipairs(CFG.extras) do
extraKeys[spec.key] = true
d.extras[spec.key] = newMatch(spec.key)
end
local function field(name) return CFG.fieldAliases[name] or name end
for name, raw in pairs(args) do
local value = U.clean(raw)
if value and type(name) == 'string' then
local k, j, fname = name:match('^s(%d+)m(%d+)_(.+)$')
if k then
k, j = tonumber(k), tonumber(j)
if k < 1 or j < 1 or k > CFG.maxStages then
table.insert(d.warnings, 'параметр «' .. name .. '» вне допустимого диапазона')
else
d.stages[k] = d.stages[k] or newStage()
local s = d.stages[k]
s.matches[j] = s.matches[j] or newMatch('s' .. k .. 'm' .. j)
s.matches[j].f[field(fname)] = value
if j > s.maxIndex then s.maxIndex = j end
end
else
local sk, sfield = name:match('^s(%d+)_(.+)$')
local prefix, rest = name:match('^([%w]+)_(.+)$')
if sk then
sk = tonumber(sk)
if sk >= 1 and sk <= CFG.maxStages then
d.stages[sk] = d.stages[sk] or newStage()
d.stages[sk].f[field(sfield)] = value
end
elseif CFG.globals[name] or extraKeys[name] then
d.g[name] = value
elseif prefix == 'team' then
d.registry.team[rest] = value
elseif prefix == 'flag' then
d.registry.flag[rest] = value
elseif prefix and extraKeys[prefix] then
d.extras[prefix].f[field(rest)] = value
else
d.g[name] = value
table.insert(d.unknown, name)
end
end
end
end
return d
end
--- Сколько этапов рисовать. Явный stages= важнее; иначе — минимум,
-- при котором все заданные матчи помещаются в дерево:
-- для этапа K с J матчами нужно N >= K + ceil(log2 J).
local function computeStages(d)
local need = 1
for k, s in pairs(d.stages) do
local n = k + U.ceilLog2(math.max(s.maxIndex, 1))
if n > need then need = n end
end
local explicit = tonumber(d.g.stages)
if explicit then
explicit = math.floor(explicit)
if explicit < 1 or explicit > CFG.maxStages then
table.insert(d.warnings, 'stages должно быть от 1 до ' .. CFG.maxStages)
explicit = nil
end
end
local N = explicit or need
if N > CFG.maxStages then N = CFG.maxStages end
return N
end
--------------------------------------------------------------------------------
-- 4. МОДЕЛЬ МАТЧА
--------------------------------------------------------------------------------
--- Победитель: явный winner=1|2, иначе по счёту, при равенстве — по пенальти.
-- Нечисловой счёт (например «+:−») требует явного winner.
local function computeWinner(m)
local w = tonumber(m.f.winner)
if w == 1 or w == 2 then return w end
local a, b = tonumber(m.side[1].score), tonumber(m.side[2].score)
if not (a and b) then return nil end
if a ~= b then return a > b and 1 or 2 end
local pa, pb = tonumber(m.side[1].pen), tonumber(m.side[2].pen)
if pa and pb and pa ~= pb then return pa > pb and 1 or 2 end
return nil
end
local function computeStatus(m)
local v = m.f.status
if v then
local key = CFG.statusAliases[mw.ustring.lower(v)]
return key or U.safeClass(mw.ustring.lower(v)) or 'custom'
end
if m.side[1].score or m.side[2].score then return 'finished' end
return 'scheduled'
end
--- Есть ли у матча собственные (введённые, а не служебные) данные.
local function hasOwnData(m)
for k in pairs(m.f) do
if not CFG.metaFields[k] then return true end
end
return false
end
--- Нормализует «сырые» поля матча в удобную для отрисовки структуру m.side[1..2].
local function normalize(m, d)
if m.normalized then return m end
local f = m.f
m.side = {}
for i = 1, 2 do
local raw = f['team' .. i]
local side = { score = f['score' .. i], pen = f['pen' .. i] }
if raw then
side.explicit = true
if d.registry.team[raw] then
side.name, side.code = d.registry.team[raw], raw
elseif d.registry.flag[raw] then
side.name, side.code = raw, raw -- код есть только в реестре флагов
else
side.name = raw
end
end
if f['flag' .. i] then
side.flag = d.flag(f['flag' .. i])
side.flagExplicit = true
elseif side.code and (d.registry.flag[side.code] or d.flagTemplate) then
side.flag = d.flag(side.code)
end
m.side[i] = side
end
m.aet = U.yesno(f.aet, false)
m.hasPens = (m.side[1].pen or m.side[2].pen) ~= nil
m.home = tonumber(f.home)
m.winner = computeWinner(m)
m.status = computeStatus(m)
m.hasData = hasOwnData(m)
m.normalized = true
return m
end
local function slots(d, k)
return U.pow2(d.N - k)
end
--- Возвращает (при необходимости создаёт) нормализованный матч J этапа K.
local function getMatch(d, k, j)
d.stages[k] = d.stages[k] or newStage()
local s = d.stages[k]
s.matches[j] = s.matches[j] or newMatch('s' .. k .. 'm' .. j)
return normalize(s.matches[j], d)
end
--- Копирует команду из матча-источника в свободную сторону.
local function fillSide(target, source)
target.name, target.code, target.auto = source.name, source.code, true
if not target.flagExplicit then target.flag = source.flag end
end
--- Прямой проход по этапам: нормализация всех слотов дерева и
-- автозаполнение: победитель матча 2J-1 этапа K-1 → команда 1 матча J этапа K,
-- победитель матча 2J → команда 2. Явно заданная команда не перезаписывается.
local function buildTree(d)
for k = 1, d.N do
for j = 1, slots(d, k) do
local m = getMatch(d, k, j)
if k > 1 and d.autofill then
for i = 1, 2 do
local src = getMatch(d, k - 1, 2 * (j - 1) + i)
if not m.side[i].explicit and src.winner and src.side[src.winner].name then
fillSide(m.side[i], src.side[src.winner])
end
end
end
end
end
-- матчи, не попавшие в дерево
for k, s in pairs(d.stages) do
for j, m in pairs(s.matches) do
if (k > d.N or j > U.pow2(math.max(d.N - k, 0))) and hasOwnData(m) then
table.insert(d.warnings, 'матч ' .. m.key .. ' не помещается в сетку из ' .. d.N ..
' этапов (увеличьте stages= или проверьте номер)')
end
end
end
-- дополнительные матчи
for _, spec in ipairs(CFG.extras) do
local m = normalize(d.extras[spec.key], d)
local switch = U.yesno(d.g[spec.key], nil)
if switch == nil then m.shown = m.hasData else m.shown = switch end
m.title = m.f.title or spec.title
if m.shown and d.autofill and spec.autofill == 'losers' and d.N >= 2 then
for i = 1, 2 do
local src = getMatch(d, d.N - 1, i)
if not m.side[i].explicit and src.winner then
local loser = src.side[3 - src.winner]
if loser.name then fillSide(m.side[i], loser) end
end
end
end
end
-- скрытие (после автозаполнения, чтобы не прятать заполненные матчи)
local function isEmpty(m)
local a, b = m.side[1], m.side[2]
return not (a.name or b.name or a.score or b.score or m.f.date or m.f.caption)
end
for k = 1, d.N do
for j = 1, slots(d, k) do
local m = getMatch(d, k, j)
m.hidden = U.yesno(m.f.hide, false) or (d.hideEmpty and isEmpty(m))
end
end
end
--------------------------------------------------------------------------------
-- 5. СЕТКА
--------------------------------------------------------------------------------
--- Название этапа: sK_name или значение по умолчанию по расстоянию до финала.
local function stageName(d, k)
local s = d.stages[k]
if s and s.f.name then return s.f.name end
local dist = d.N - k
if dist == 0 then return CFG.i18n.final end
if dist == 1 then return CFG.i18n.semifinal end
if dist == 2 then return CFG.i18n.quarter end
return string.format(CFG.i18n.fraction, U.pow2(dist))
end
--- Подпись над матчем в сетке: «30 июня — Сочи». caption= заменяет её целиком.
local function bracketCaption(m)
if m.f.caption then return m.f.caption end
local date = U.formatDate(m.f.date, false)
local place = m.f.place or m.f.city or m.f.stadium
if date and place then return date .. CFG.i18n.captionSep .. place end
return date or place or ' ' -- пустая подпись всё равно занимает место
end
local function winnerSuffix(m)
if m.hasPens then return CFG.i18n.penSuffix end
if m.aet then return CFG.i18n.aetSuffix end
return nil
end
--- Один матч сетки: подпись + две строки «флаг | команда | счёт».
-- Структура (caption + box + отступ) одинакова у всех матчей: на этом
-- держится выравнивание соединительных линий (см. styles.css, «Геометрия»).
local function renderMatchItem(m, extraClass)
local item = mw.html.create('div')
:addClass('cb-item')
:addClass('cb-status-' .. m.status)
:addClass(U.safeClass(m.f.class))
:addClass(extraClass)
if m.hidden then item:addClass('cb-item--hidden') end
if m.f.report then item:addClass('cb-item--has-report') end
item:tag('div'):addClass('cb-caption'):tag('span'):wikitext(bracketCaption(m))
local box = item:tag('div'):addClass('cb-match')
local suffix = winnerSuffix(m)
for i = 1, 2 do
local s = m.side[i]
local row = box:tag('div'):addClass('cb-team')
if m.winner == i then
row:addClass('cb-team--winner')
elseif m.winner then
row:addClass('cb-team--loser')
end
if m.home == i then row:addClass('cb-team--home') end
if not s.name then row:addClass('cb-team--tbd') end
if s.auto then row:addClass('cb-team--auto') end
local cell = row:tag('div'):addClass('cb-name')
if s.flag then cell:tag('span'):addClass('cb-flag'):wikitext(s.flag) end
local label = cell:tag('span'):addClass('cb-label'):wikitext(s.name or CFG.i18n.tbd)
if suffix and m.winner == i then
label:tag('span'):addClass('cb-suffix'):wikitext(' (' .. suffix .. ')')
end
local score = s.score or ''
if s.score and s.pen then score = score .. ' (' .. s.pen .. ')' end
row:tag('div'):addClass('cb-score'):wikitext(score)
end
return item
end
--- Соединитель от матча J этапа K к его «потомку» в этапе K+1.
-- Верхний предок (J нечётный) рисует линию вниз, нижний — вверх;
-- горизонтальный «вход» в потомка рисует верхний предок (или нижний,
-- если верхний скрыт — например, при свободном проходе).
local function renderConnector(grid, area, col, isTop, drawIn)
local conn = grid:tag('div')
:addClass('cb-conn')
:addClass(isTop and 'cb-conn--top' or 'cb-conn--bottom')
:css('grid-row', area)
:css('grid-column', tostring(col))
conn:tag('div'):addClass('cb-conn-out')
if drawIn then conn:tag('div'):addClass('cb-conn-in') end
end
--- Размещение доп. матчей в колонке финала.
-- 'inside' — в нижней половине колонки финала (как на исходном изображении);
-- 'below' — отдельной строкой под сеткой.
-- inside занимает нижнюю половину слотов (строки R/2+2 … R+1) и потому
-- центрируется на уровне нижнего полуфинала. Это возможно только при N >= 4:
-- в половине колонки должно быть не меньше 4 строк-слотов, иначе блок
-- наедет на финал или растянет строки (а растянутая строка сдвинула бы
-- соединительные линии). При N <= 3 блок всегда выводится под сеткой.
local function extrasArea(d, visible)
local pos
for _, e in ipairs(visible) do pos = pos or e.m.f.position end
local R = U.pow2(d.N - 1)
local inside = d.N >= 4 and pos ~= 'below'
if inside then
return (R / 2 + 2) .. ' / span ' .. (R / 2)
end
return (R + 2) .. ' / span 1'
end
local function renderBracket(d)
local N = d.N
local wrap = mw.html.create('div'):addClass('cb-scroll')
local grid = wrap:tag('div'):addClass('cb-grid')
for k = 1, N do
local col = 2 * k - 1
local sf = d.stages[k] and d.stages[k].f or {}
grid:tag('div')
:addClass('cb-head')
:addClass(U.safeClass(sf.class))
:css('grid-row', '1')
:css('grid-column', tostring(col))
:wikitext(stageName(d, k))
local span = U.pow2(k - 1)
for j = 1, slots(d, k) do
local m = getMatch(d, k, j)
local area = (2 + (j - 1) * span) .. ' / span ' .. span
grid:node(renderMatchItem(m, U.safeClass(sf.class))
:css('grid-row', area)
:css('grid-column', tostring(col)))
if k < N and not m.hidden then
local child = getMatch(d, k + 1, math.ceil(j / 2))
if not child.hidden then
local isTop = (j % 2 == 1)
local sibling = getMatch(d, k, isTop and j + 1 or j - 1)
renderConnector(grid, area, col + 1, isTop, isTop or sibling.hidden)
end
end
end
end
-- доп. матчи (за 3-е место и т. п.)
local visible = {}
for _, spec in ipairs(CFG.extras) do
local m = d.extras[spec.key]
if m.shown then table.insert(visible, { spec = spec, m = m }) end
end
if #visible > 0 then
local box = grid:tag('div')
:addClass('cb-extras')
:css('grid-row', extrasArea(d, visible))
:css('grid-column', tostring(2 * N - 1))
for _, e in ipairs(visible) do
box:tag('div')
:addClass('cb-extra')
:addClass('cb-extra--' .. e.spec.key)
:tag('div'):addClass('cb-head'):addClass('cb-extra-head'):wikitext(e.m.title):done()
:node(renderMatchItem(e.m))
end
end
return wrap
end
--------------------------------------------------------------------------------
-- 6. СВОДНАЯ ТАБЛИЦА
--------------------------------------------------------------------------------
-- Строки информационной колонки (место, зрители, судья, примечание).
-- Каждая функция получает нормализованный матч и возвращает викитекст или nil.
-- Новая строка (например, «Трансляция: …») = ещё одна функция в списке.
CFG.summaryInfo = {
function (m)
local parts = {}
if m.f.stadium then table.insert(parts, m.f.stadium) end
if m.f.city then table.insert(parts, m.f.city) end
if #parts > 0 then return table.concat(parts, ', ') end
end,
function (m)
if m.f.attendance then return CFG.i18n.attendance .. U.formatNumber(m.f.attendance) end
end,
function (m, d)
if m.f.referee then
local flag = d.flag(m.f.referee_flag)
return CFG.i18n.referee ..
(flag and ('<span class="cb-flag">' .. flag .. '</span> ') or '') .. m.f.referee
end
end,
function (m)
if m.f.note then return "''" .. m.f.note .. "''" end
end,
}
--- Попадает ли матч в сводку: summary=да/нет у матча решает сразу;
-- иначе нужны обе команды и хотя бы что-то из счёта/даты/места.
local function inSummary(m)
local ov = U.yesno(m.f.summary, nil)
if ov ~= nil then return ov end
local a, b, f = m.side[1], m.side[2], m.f
if not (a.name and b.name) then return false end
return (a.score or b.score or f.date or f.time or f.stadium or f.city) ~= nil
end
local function scoreLine(m)
local a, b = m.side[1].score, m.side[2].score
if not (a or b) then return CFG.i18n.noScore end
return (a or '?') .. CFG.i18n.scoreSep .. (b or '?')
end
local function renderSummaryTeam(tr, m, i, d)
local s = m.side[i]
local td = tr:tag('td'):addClass('cb-s-team'):addClass('cb-s-team' .. i)
if m.winner == i then td:addClass('cb-s-winner') end
local name = td:tag('div'):addClass('cb-s-name')
local flag = s.flag and ('<span class="cb-flag">' .. s.flag .. '</span>') or nil
-- команда 1: «Франция [флаг]», команда 2: «[флаг] Аргентина» — как на исходном образце
if i == 1 then
name:wikitext(s.name)
if flag then name:wikitext(' ' .. flag) end
else
if flag then name:wikitext(flag .. ' ') end
name:wikitext(s.name)
end
if m.f['goals' .. i] then
td:tag('div'):addClass('cb-s-goals'):wikitext(m.f['goals' .. i])
end
end
local function renderSummaryMatch(tbl, m, d)
local f = m.f
local shootoutRow = m.hasPens and (f.shootout1 or f.shootout2)
local tr = tbl:tag('tr')
:addClass('cb-s-row')
:addClass('cb-status-' .. m.status)
:addClass(U.safeClass(f.class))
if shootoutRow then tr:addClass('cb-s-row--with-shootout') end
-- 1. дата и время
local when = tr:tag('td'):addClass('cb-s-when')
when:tag('div'):addClass('cb-s-date'):wikitext(U.formatDate(f.date, true) or '')
if f.time then when:tag('div'):addClass('cb-s-time'):wikitext(f.time) end
-- 2. команда 1
renderSummaryTeam(tr, m, 1, d)
-- 3. результат
local res = tr:tag('td'):addClass('cb-s-result')
local sc = res:tag('div'):addClass('cb-s-score'):wikitext(scoreLine(m))
if m.aet then
sc:tag('span'):addClass('cb-s-aet'):wikitext(' (' .. CFG.i18n.aetSuffix .. ')')
end
if m.hasPens and not shootoutRow then
res:tag('div'):addClass('cb-s-pens'):wikitext(CFG.i18n.penSuffix .. ' ' ..
(m.side[1].pen or '?') .. CFG.i18n.scoreSep .. (m.side[2].pen or '?'))
end
if f.report then
res:tag('div'):addClass('cb-s-report'):wikitext('(' .. U.reportLink(f.report) .. ')')
end
local st = CFG.statuses[m.status]
if st and st.label then
res:tag('div'):addClass('cb-s-status'):wikitext(st.label)
end
-- 4. команда 2
renderSummaryTeam(tr, m, 2, d)
-- 5. место, зрители, судья…
local info = tr:tag('td'):addClass('cb-s-info')
for _, fn in ipairs(CFG.summaryInfo) do
local line = fn(m, d)
if line then info:tag('div'):wikitext(line) end
end
-- серия пенальти со списками бьющих — отдельной строкой
if shootoutRow then
local tr2 = tbl:tag('tr'):addClass('cb-s-shootout')
tr2:tag('td')
tr2:tag('td'):addClass('cb-s-team1'):wikitext(f.shootout1 or '')
local c = tr2:tag('td'):addClass('cb-s-result')
c:tag('div'):addClass('cb-s-shootout-title'):wikitext(CFG.i18n.shootout)
c:tag('div'):wikitext((m.side[1].pen or '?') .. CFG.i18n.scoreSep .. (m.side[2].pen or '?'))
tr2:tag('td'):addClass('cb-s-team2'):wikitext(f.shootout2 or '')
tr2:tag('td')
end
end
--- Список разделов сводки в хронологическом порядке:
-- этапы 1…N-1, доп. матчи с beforeFinal, финал, остальные доп. матчи.
local function summarySections(d)
local list = {}
local function addExtras(before)
for _, spec in ipairs(CFG.extras) do
local m = d.extras[spec.key]
if m.shown and (spec.beforeFinal or false) == before then
table.insert(list, { key = spec.key, title = m.title, matches = { m } })
end
end
end
for k = 1, d.N do
if k == d.N then addExtras(true) end
local sf = d.stages[k] and d.stages[k].f or {}
if U.yesno(sf.summary, true) then
local ms = {}
for j = 1, slots(d, k) do table.insert(ms, getMatch(d, k, j)) end
table.insert(list, { key = tostring(k), title = stageName(d, k), matches = ms })
end
end
addExtras(false)
return list
end
--- Ключ хронологической сортировки: '2018-06-30 17:00' (дата только в ISO).
local function chronoKey(m)
local date = m.f.date and m.f.date:match('^%d%d%d%d%-%d%d?%-%d%d?$')
if not date then return nil end
local y, mo, dd = date:match('^(%d+)%-(%d+)%-(%d+)$')
local h, mi = (m.f.time or ''):match('^(%d%d?):(%d%d)')
return string.format('%s-%02d-%02d %02d:%s', y, tonumber(mo), tonumber(dd),
tonumber(h or 99), mi or '99')
end
--- Сортирует строки раздела по дате и времени, если у всех матчей дата
-- задана в ISO; иначе оставляет порядок сетки (по номерам матчей).
local function sortRows(rows)
local keys = {}
for i, m in ipairs(rows) do
keys[m] = chronoKey(m)
if not keys[m] then return rows end
m.order = i
end
table.sort(rows, function (a, b)
if keys[a] ~= keys[b] then return keys[a] < keys[b] end
return a.order < b.order -- устойчивость: при равенстве — порядок сетки
end)
return rows
end
local function renderSummary(d)
local sortByDate = U.yesno(d.g.summary_sort, true)
-- summary_stage = 1,2,third — выводить только перечисленные разделы
local only
if d.g.summary_stage then
only = {}
for _, v in ipairs(mw.text.split(d.g.summary_stage, ',')) do
only[mw.text.trim(v)] = true
end
end
local tbl = mw.html.create('table'):addClass('cb-summary')
if d.g.summary_title then tbl:tag('caption'):wikitext(d.g.summary_title) end
local any = false
for _, sec in ipairs(summarySections(d)) do
if not only or only[sec.key] then
local rows = {}
for _, m in ipairs(sec.matches) do
if inSummary(m) then table.insert(rows, m) end
end
if sortByDate then rows = sortRows(rows) end
if #rows > 0 then
any = true
tbl:tag('tr'):addClass('cb-s-stage')
:tag('th'):attr('colspan', '5'):wikitext(sec.title)
for _, m in ipairs(rows) do renderSummaryMatch(tbl, m, d) end
end
end
end
if not any then return nil end
local wrap = mw.html.create('div'):addClass('cb-summary-wrap')
wrap:node(tbl)
return wrap
end
--------------------------------------------------------------------------------
-- 7. ТОЧКИ ВХОДА
--------------------------------------------------------------------------------
--- Собирает модель из плоских аргументов (используется и в тестах, и другими модулями).
function p._build(args, frame)
local d = parse(args)
d.N = computeStages(d)
d.autofill = U.yesno(d.g.autofill, true)
d.hideEmpty = U.yesno(d.g.hide_empty, false)
d.flagTemplate = d.g.flagtemplate
d.flag = U.makeFlagRenderer{
registry = d.registry, frame = frame,
flagTemplate = d.g.flagtemplate, flagSize = d.g.flagsize or CFG.flagSize,
}
buildTree(d)
return d
end
--- Отрисовывает модель в HTML (строкой).
function p._render(d)
local root = mw.html.create('div'):addClass('cb')
:addClass(d.g.theme and U.safeClass('cb--theme-' .. d.g.theme) or nil)
:addClass(U.safeClass(d.g.class))
:attr('data-stages', tostring(d.N))
if d.g.title then root:tag('div'):addClass('cb-title'):wikitext(d.g.title) end
if U.yesno(d.g.bracket, true) then root:node(renderBracket(d)) end
if U.yesno(d.g.summary, true) then
local s = renderSummary(d)
if s then root:node(s) end
end
if U.yesno(d.g.debug, false) and #d.unknown > 0 then
table.sort(d.unknown)
table.insert(d.warnings, 'неизвестные параметры: ' .. table.concat(d.unknown, ', '))
end
local tail = ''
if #d.warnings > 0 then
local w = root:tag('div'):addClass('cb-warnings')
for _, msg in ipairs(d.warnings) do
w:tag('div'):addClass('error'):wikitext(CFG.i18n.warnPrefix .. msg)
end
if CFG.errorCategory then tail = '[[Category:' .. CFG.errorCategory .. ']]' end
end
return tostring(root) .. tail
end
--- Точка входа для {{#invoke:Кубковая сетка|main}}.
-- Приоритет источников: данные из модуля-хранилища (data=) < параметры
-- #invoke < параметры шаблона. Так одну и ту же сетку можно хранить
-- в Модуль:Кубковая сетка/данные/<имя> и выводить в разных местах статьи.
function p.main(frame)
local parent = frame:getParent()
local pargs = parent and parent.args or {}
local args = {}
local dataName = U.clean(pargs.data) or U.clean(frame.args.data)
local warn
if dataName then
local ok, data = pcall(mw.loadData, CFG.dataPrefix .. dataName)
if ok and type(data) == 'table' then
for k, v in pairs(data) do args[k] = v end
else
warn = 'не удалось загрузить данные «' .. CFG.dataPrefix .. dataName .. '»'
end
end
for k, v in pairs(frame.args) do if k ~= 'styles' then args[k] = v end end
for k, v in pairs(pargs) do args[k] = v end
local d = p._build(args, frame)
if warn then table.insert(d.warnings, warn) end
local out = p._render(d)
-- при прямом #invoke можно подключить стили без шаблона: |styles=Шаблон:…/styles.css
local styles = U.clean(frame.args.styles)
if styles then
out = frame:extensionTag('templatestyles', '', { src = styles }) .. out
end
return out
end
return p