Модуль:Призёры
Перейти к навигации
Перейти к поиску
Для документации этого модуля может быть создана страница Модуль:Призёры/doc
--[[
Модуль:Призёры
Из одного списка сезонов строит две таблицы:
* призёры по сезонам — вид = сезоны;
* сводная таблица медалей или кубков — вид = зачёт (по умолчанию).
Данные: каждый сезон — с новой строки, ячейки через «|»:
| Сезон | 1-е место | 2-е место | 3-е место | доп. столбцы…
Если строк с данными нет, модуль ищет таблицу призёров в тексте статьи
(параметр «источник», по умолчанию — текущая страница).
Подробно — на странице «Шаблон:Призёры/doc».
]]
local p = {}
local CLUBS_MODULE = 'Module:Призёры/Клубы'
-- Столбцы для каждого типа турнира.
local PRESETS = {
medals = {
labels = { 'Золото', 'Серебро', 'Бронза' }, -- сводная таблица
places = { '1-е место', '2-е место', '3-е место' }, -- таблица по сезонам
colors = { 'gold', 'silver', '#CC9966' },
total = 'Всего медалей',
},
cup = {
labels = { 'Победитель', 'Финалист' },
places = { 'Победитель', 'Финалист' },
colors = { 'gold', 'silver' },
total = 'Всего кубков',
},
supercup = {
labels = { 'Победитель', 'Финалист' },
places = { 'Победитель', 'Финалист' },
colors = { 'gold', 'silver' },
total = 'Всего суперкубков',
},
}
-- Добавляется к кубку при «малый кубок = да».
local SMALL_CUP = {
labels = { 'Малый кубок', 'Финалист МК' },
colors = { 'gold', 'silver' },
}
local TYPES = {
['медали'] = 'medals', ['чемпионат'] = 'medals', ['первенство'] = 'medals',
['кубок'] = 'cup', ['кубки'] = 'cup',
['суперкубок'] = 'supercup', ['суперкубки'] = 'supercup',
}
-- Ключи записаны так, как их возвращает norm(): строчные, «ё» → «е».
local VIEWS = {
['зачет'] = 'standings', ['итоги'] = 'standings', ['итог'] = 'standings',
['сезоны'] = 'seasons', ['сезон'] = 'seasons', ['по сезонам'] = 'seasons', ['призеры'] = 'seasons',
}
local PARAMS = {
'вид', 'тип', 'малый кубок', 'доп', 'названия', 'названия мест', 'цвета',
'всего', 'столбец сезона', 'столбец клуба', 'сортировка', 'равные',
'учтены', 'жирный', 'клубы', 'источник', 'номер таблицы',
}
local KNOWN = {}
for _, name in ipairs(PARAMS) do
KNOWN[name] = true
end
-- Ячейка места с таким текстом — награда не вручалась или пока неизвестна.
local DASHES = { ['—'] = true, ['–'] = true, ['-'] = true, ['−'] = true, ['?'] = true }
-- Ссылки в эти пространства имён — не клубы (флаги, категории).
local SKIP_NS = {
['файл'] = true, ['file'] = true, ['image'] = true, ['изображение'] = true,
['категория'] = true, ['category'] = true, ['media'] = true, ['медиа'] = true,
}
------------------------------------------------------------------------
-- Строки
------------------------------------------------------------------------
local function trim(s)
if s == nil then
return ''
end
return mw.text.trim(s)
end
-- Для сравнения слов: без пробелов по краям, строчные, «ё» → «е».
local function norm(s)
return (mw.ustring.lower(trim(s)):gsub('ё', 'е'))
end
local function isYes(s)
local v = norm(s)
return v == 'да' or v == 'yes' or v == '1' or v == 'true' or v == 'вкл'
end
local function isNo(s)
local v = norm(s)
return v == 'нет' or v == 'no' or v == '0' or v == 'false' or v == 'выкл'
end
-- «a; b; c» → { 'a', 'b', 'c' }; точку с запятой внутри и т. п. не трогает.
local function splitList(s)
local list = {}
if not s then
return list
end
s = s:gsub('&(#?%w+);', '&%1\1')
for part in mw.text.gsplit(s, ';', true) do
table.insert(list, trim((part:gsub('\1', ';'))))
end
return list
end
-- Все strip-маркеры строки (так в аргументы попадают <ref> и подобные теги).
local function markersOf(s)
local found = {}
for marker in s:gmatch('\127[^\127]*\127') do
table.insert(found, marker)
end
return table.concat(found)
end
-- Текст без маркеров, вики-разметки выделения и HTML-тегов.
local function stripFormatting(s)
s = mw.text.killMarkers(s or '')
s = s:gsub("'''?", '')
s = s:gsub('<[^>]*>', '')
return trim(s)
end
-- Видимый текст: ссылки заменены их подписями.
local function plainText(s)
s = mw.text.killMarkers(s or '')
s = s:gsub('%[%[([^%]]-)%]%]', function (inner)
local label = inner:match('^[^|]*|(.*)$')
return label or inner
end)
s = s:gsub('%[%a+://[^%s%]]+%s+([^%]]+)%]', '%1')
return stripFormatting(s)
end
-- Название страницы в каноническом виде: пробелы вместо «_», первая буква заглавная.
local function normTitle(t)
t = t:gsub('_', ' ')
t = t:gsub('#.*$', '')
t = t:gsub('%s+', ' ')
t = trim(t)
t = t:gsub('^:%s*', '')
if t == '' then
return t
end
return mw.ustring.upper(mw.ustring.sub(t, 1, 1)) .. mw.ustring.sub(t, 2)
end
-- Первая ссылка на статью в тексте: цель и подпись.
local function firstLink(s)
for inner in s:gmatch('%[%[(.-)%]%]') do
local target, label = inner:match('^([^|]*)|(.*)$')
target = target or inner
local ns = target:match('^%s*:?%s*([^:]+):')
if trim(target) ~= '' and not (ns and SKIP_NS[norm(ns)]) then
if label == nil then
label = (target:gsub('#.*$', ''):gsub('^%s*:', ''))
elseif trim(label) == '' then
label = (target:gsub('%s*%(.-%)%s*$', '')) -- «трюк с вертикальной чертой»
end
return target, stripFormatting(label)
end
end
end
local function makeLink(target, label)
if target == label then
return '[[' .. target .. ']]'
end
return '[[' .. target .. '|' .. label .. ']]'
end
------------------------------------------------------------------------
-- Список клубов
------------------------------------------------------------------------
local function loadClubs(name, warnings)
name = name or CLUBS_MODULE
if not name:find(':', 1, true) then
name = 'Module:' .. name
end
local ok, data = pcall(mw.loadData, name)
if ok and type(data) == 'table' then
return data
end
if not ok and not tostring(data):find('not found', 1, true) then
table.insert(warnings, 'не удалось прочитать список клубов «' .. name .. '»: ' .. tostring(data))
end
return {}
end
------------------------------------------------------------------------
-- Аргументы
------------------------------------------------------------------------
-- Ячейки по строкам: строка заканчивается там, где ячейка заканчивается переводом строки.
local function splitRows(positional)
local rows, current = {}, {}
for _, raw in ipairs(positional) do
local endsLine = raw:find('\n[ \t]*$') ~= nil
table.insert(current, trim((raw:gsub('%s*\n%s*', ' '))))
if endsLine then
table.insert(rows, current)
current = {}
end
end
if #current > 0 then
table.insert(rows, current)
end
local filled = {}
for _, row in ipairs(rows) do
for _, cell in ipairs(row) do
if cell ~= '' then
table.insert(filled, row)
break
end
end
end
return filled
end
local function viewOf(s)
if s == nil or trim(s) == '' then
return nil
end
return VIEWS[norm(s)]
end
local function collectArgs(frame, forcedView, warnings)
local fargs = frame.args
local parent = frame:getParent()
local pargs = parent and parent.args or {}
-- Параметр из вызова на странице важнее того, что записан в шаблоне.
local function get(name)
local v = pargs[name]
if v == nil or trim(v) == '' then
v = fargs[name]
end
v = trim(v)
return v ~= '' and v or nil
end
local settings = {}
for _, name in ipairs(PARAMS) do
settings[name] = get(name)
end
local fromTemplate = fargs[1] ~= nil
local source = fromTemplate and fargs or pargs
local positional, i = {}, 1
while source[i] ~= nil do
positional[i] = source[i]
i = i + 1
end
-- «сезоны» / «зачёт» самой первой ячейкой — это вид таблицы, а не данные. Убираем его
-- до разбивки на строки: если следом на той же строке идёт именованный параметр
-- ({{Призёры|сезоны|доп=…), перевод строки достаётся параметру, а не этому слову.
local leadingView = viewOf(positional[1])
if leadingView then
table.remove(positional, 1)
end
local rows = splitRows(positional)
local wrongView = settings['вид']
if not wrongView and fromTemplate and trim(pargs[1]) ~= '' then
wrongView = trim(pargs[1]) -- {{Призёры/Чемпионат 7х7|…}}: безымянный параметр — только вид
end
if wrongView and not viewOf(wrongView) then
table.insert(warnings, 'неизвестный вид «' .. wrongView .. '»: ожидается «зачёт» или «сезоны».')
end
local pageView = viewOf(pargs['вид'])
if fromTemplate then
pageView = pageView or viewOf(pargs[1])
else
pageView = pageView or leadingView
end
local templateView = viewOf(fargs['вид']) or (fromTemplate and leadingView or nil)
settings.view = forcedView or pageView or templateView or 'standings'
local function scan(args)
for name in pairs(args) do
if type(name) == 'string' and not KNOWN[name] then
table.insert(warnings, 'неизвестный параметр «' .. name .. '». Если это ячейка, '
.. 'в которой есть знак «=», запишите его как {{=}} или &#61;.')
end
end
end
scan(fargs)
if parent then
scan(pargs)
end
return rows, settings
end
local function makeConfig(settings, warnings)
local typeName = 'medals'
if settings['тип'] then
typeName = TYPES[norm(settings['тип'])]
if not typeName then
table.insert(warnings, 'неизвестный тип «' .. settings['тип']
.. '»: ожидается «медали», «кубок» или «суперкубок».')
typeName = 'medals'
end
end
local preset = PRESETS[typeName]
local cfg = { labels = {}, places = {}, colors = {} }
for i = 1, #preset.labels do
cfg.labels[i] = preset.labels[i]
cfg.places[i] = preset.places[i]
cfg.colors[i] = preset.colors[i]
end
if typeName ~= 'medals' and settings['малый кубок'] and isYes(settings['малый кубок']) then
for i = 1, #SMALL_CUP.labels do
table.insert(cfg.labels, SMALL_CUP.labels[i])
table.insert(cfg.places, SMALL_CUP.labels[i])
table.insert(cfg.colors, SMALL_CUP.colors[i])
end
end
cfg.n = #cfg.labels
local function override(list, param)
for i, value in ipairs(splitList(settings[param])) do
if i <= cfg.n and value ~= '' then
list[i] = value
end
end
end
override(cfg.labels, 'названия')
override(cfg.places, 'названия мест')
override(cfg.colors, 'цвета')
cfg.total = settings['всего'] or preset.total
cfg.seasonHeader = settings['столбец сезона'] or 'Сезон'
cfg.clubHeader = settings['столбец клуба'] or 'Клуб'
cfg.extras = splitList(settings['доп'])
cfg.extrasGiven = settings['доп'] ~= nil
cfg.bold = not (settings['жирный'] and isNo(settings['жирный']))
cfg.note = settings['учтены'] ~= nil and isYes(settings['учтены'])
local order = norm(settings['сортировка'] or '')
if order == '' or order:find('сумм') then
cfg.olympic = false
elseif order:find('олимп') or order:find('золот') or order:find('мест') or order:find('побед') then
cfg.olympic = true
else
table.insert(warnings, 'неизвестная сортировка «' .. settings['сортировка']
.. '»: ожидается «по сумме» или «олимпийская».')
cfg.olympic = false
end
local equal = norm(settings['равные'] or '')
cfg.alphabetical = equal:find('алфав') ~= nil
if equal ~= '' and not cfg.alphabetical and not equal:find('поряд') then
table.insert(warnings, 'неизвестное значение «равные = ' .. settings['равные']
.. '»: ожидается «по алфавиту» или «по порядку».')
end
return cfg
end
------------------------------------------------------------------------
-- Таблица из текста статьи
------------------------------------------------------------------------
-- Делит строку по разделителям, не заглядывая внутрь [[…]] и {{…}}.
local function splitTop(s, seps)
local parts, start, i, n = {}, 1, 1, #s
local links, templates = 0, 0
while i <= n do
local two = s:sub(i, i + 1)
if two == '[[' then
links = links + 1
i = i + 2
elseif two == ']]' and links > 0 then
links = links - 1
i = i + 2
elseif two == '{{' then
templates = templates + 1
i = i + 2
elseif two == '}}' and templates > 0 then
templates = templates - 1
i = i + 2
else
local sep
if links == 0 and templates == 0 then
for _, candidate in ipairs(seps) do
if s:sub(i, i + #candidate - 1) == candidate then
sep = candidate
break
end
end
end
if sep then
table.insert(parts, s:sub(start, i - 1))
i = i + #sep
start = i
else
i = i + 1
end
end
end
table.insert(parts, s:sub(start))
return parts
end
-- Вики-таблицы верхнего уровня: строки с сырыми ячейками.
local function parseTables(text)
local tables, current, depth = {}, nil, 0
for line in (text .. '\n'):gmatch('([^\n]*)\n') do
local l = trim(line)
if l:sub(1, 2) == '{|' then
depth = depth + 1
if depth == 1 then
current = { rows = {}, newRow = true }
end
elseif depth > 0 and l:sub(1, 2) == '|}' then
depth = depth - 1
if depth == 0 then
table.insert(tables, current)
current = nil
end
elseif depth == 1 then
local row = current.rows[#current.rows]
local first = l:sub(1, 1)
if l:sub(1, 2) == '|-' then
current.newRow = true
elseif l:sub(1, 2) == '|+' then
-- подпись таблицы
elseif first == '|' or first == '!' then
if current.newRow or not row then
row = { cells = {}, header = true }
table.insert(current.rows, row)
current.newRow = false
end
local isHeader = first == '!'
for _, cell in ipairs(splitTop(l:sub(2), isHeader and { '!!', '||' } or { '||' })) do
table.insert(row.cells, cell)
if not isHeader then
row.header = false
end
end
elseif row and #row.cells > 0 then
row.cells[#row.cells] = row.cells[#row.cells] .. '\n' .. l
end
end
end
return tables
end
local function attrNumber(attrs, name)
return tonumber(attrs:lower():match(name .. '%s*=%s*["\']?%s*(%d+)'))
end
-- Раскладывает ячейки по столбцам с учётом rowspan и colspan.
local function tableGrid(tbl)
local pending = {}
for _, row in ipairs(tbl.rows) do
local out, col = {}, 1
local function skipSpanned()
while pending[col] do
local span = pending[col]
out[col] = span.text
span.left = span.left - 1
if span.left == 0 then
pending[col] = nil
end
col = col + 1
end
end
for _, cellText in ipairs(row.cells) do
skipSpanned()
local parts = splitTop(cellText, { '|' })
local attrs, content = '', cellText
if #parts > 1 then
attrs = parts[1]
content = table.concat(parts, '|', 2)
end
content = trim(content)
local rowspan = attrNumber(attrs, 'rowspan') or 1
local colspan = attrNumber(attrs, 'colspan') or 1
for k = 0, colspan - 1 do
local text = content
if colspan > 1 and not row.header then
-- Объединённая по горизонтали ячейка («не проводился») — пояснение, а не клуб.
text = (k == 0 and content ~= '') and ('(' .. content .. ')') or ''
end
out[col + k] = text
if rowspan > 1 then
pending[col + k] = { text = text, left = rowspan - 1 }
end
end
col = col + colspan
end
skipSpanned()
row.grid = out
row.width = col - 1
end
end
local function readSourceTable(frame, settings, cfg, warnings)
local name = settings['источник']
local title = name and mw.title.new(name) or mw.title.getCurrentTitle()
if not title then
return nil, nil, 'не удалось разобрать название страницы «' .. name .. '».'
end
local text = title:getContent()
if not text then
return nil, nil, 'страница «' .. title.prefixedText .. '» не найдена.'
end
text = text:gsub('<!%-%-.-%-%->', '')
text = text:gsub('<[Rr][Ee][Ff][^>]-/>', '')
text = text:gsub('<[Rr][Ee][Ff][%s>].-</[Rr][Ee][Ff]%s*>', '')
local wanted = tonumber(settings['номер таблицы'] or '')
local chosen
for i, tbl in ipairs(parseTables(text)) do
tableGrid(tbl)
if wanted then
if i == wanted then
chosen = tbl
end
else
for _, row in ipairs(tbl.rows) do
local second = row.header and row.grid[2]
if second then
second = norm(stripFormatting(second))
if second == norm(cfg.places[1]) or second == norm(cfg.labels[1]) then
chosen = tbl
break
end
end
end
end
if chosen then
break
end
end
if not chosen then
if wanted then
return nil, nil, 'на странице «' .. title.prefixedText .. '» нет таблицы № ' .. wanted .. '.'
end
return nil, nil, 'на странице «' .. title.prefixedText .. '» не найдена таблица призёров '
.. '(во втором столбце заголовка ожидается «' .. cfg.places[1] .. '»). '
.. 'Укажите «номер таблицы» или впишите данные строками.'
end
local rows, header = {}, nil
for _, row in ipairs(chosen.rows) do
if row.header then
header = row -- при двухэтажной шапке нужен нижний этаж
else
local cells, filled = {}, false
for c = 1, row.width do
local value = row.grid[c] or ''
if value:find('{{', 1, true) then
value = frame:preprocess(value)
end
cells[c] = trim(value)
filled = filled or cells[c] ~= ''
end
if filled then
table.insert(rows, cells)
end
end
end
local extras = {}
if header then
-- Столбцы мест должны называться так, как ждёт «тип»: иначе, например, «Счёт»
-- молча засчитался бы как «Малый кубок».
local wrong = {}
for j = 1, cfg.n do
local h = stripFormatting(header.grid[1 + j] or '')
if h ~= '' and norm(h) ~= norm(cfg.places[j]) and norm(h) ~= norm(cfg.labels[j]) then
table.insert(wrong, (1 + j) .. '-й столбец называется «' .. h .. '», а ожидается «'
.. cfg.places[j] .. '»')
end
end
if #wrong > 0 then
table.insert(warnings, 'в таблице на странице «' .. title.prefixedText .. '» '
.. table.concat(wrong, '; ') .. '. Проверьте «тип» и «малый кубок» '
.. 'или задайте «названия мест» как в шапке таблицы.')
end
for c = cfg.n + 2, header.width do
table.insert(extras, header.grid[c] or '')
end
end
return rows, extras
end
------------------------------------------------------------------------
-- Сезоны и клубы
------------------------------------------------------------------------
local function parseTeam(part, clubs)
local clean = trim(mw.text.killMarkers(part))
if clean == '' or DASHES[clean] then
return nil
end
local team = { raw = part, markers = markersOf(part) }
local target, label = firstLink(clean)
if target then
team.target = normTitle(target)
team.label = label ~= '' and label or team.target
team.linked = true
else
team.label = stripFormatting(clean)
if team.label == '' or DASHES[team.label] then
return nil
end
local known = clubs[team.label]
if type(known) == 'string' and trim(known) ~= '' then
team.target = normTitle(known)
end
end
return team
end
local BR = '<[Bb][Rr]%s*/?%s*>'
local function parseCell(raw, clubs)
local cell = { raw = raw, parts = {} }
local inner = stripFormatting(raw):match('^%((.*)%)$')
if inner then
cell.note = trim(inner) .. markersOf(raw)
return cell
end
local pos = 1
while true do
local a, b = raw:find(BR, pos)
local piece = raw:sub(pos, a and a - 1 or nil)
table.insert(cell.parts, { raw = piece, team = parseTeam(piece, clubs) })
if not a then
break
end
pos = b + 1
end
return cell
end
local function buildSeasons(rows, cfg, clubs)
local seasons = {}
for _, cells in ipairs(rows) do
local season = { label = cells[1] or '', places = {}, extras = {} }
for j = 1, cfg.n do
season.places[j] = parseCell(cells[1 + j] or '', clubs)
end
for e = 1, #cfg.extras do
season.extras[e] = cells[1 + cfg.n + e] or ''
end
table.insert(seasons, season)
end
return seasons
end
local function eachTeam(seasons, n, fn)
for _, season in ipairs(seasons) do
for j = 1, n do
for _, part in ipairs(season.places[j].parts) do
if part.team then
fn(part.team, season, j)
end
end
end
end
end
-- Клуб, записанный текстом, получает ссылку, если в данных он уже встречался ссылкой
-- с той же подписью (и только с одной целью). Затем — ключи и вид ячеек.
local function resolveTeams(seasons, n)
local byLabel = {}
eachTeam(seasons, n, function (team)
if team.linked then
byLabel[team.label] = byLabel[team.label] or {}
byLabel[team.label][team.target] = true
end
end)
eachTeam(seasons, n, function (team)
if not team.target and byLabel[team.label] then
local only, count = nil, 0
for target in pairs(byLabel[team.label]) do
only = target
count = count + 1
end
if count == 1 then
team.target = only
end
end
team.key = team.target or ('\1' .. team.label)
if team.linked or not team.target then
team.display = team.raw
else
local link = makeLink(team.target, team.label)
local a, b = team.raw:find(team.label, 1, true)
if a then
team.display = team.raw:sub(1, a - 1) .. link .. team.raw:sub(b + 1)
else
team.display = link .. team.markers
end
end
end)
end
local function tally(seasons, n)
local byKey, list = {}, {}
eachTeam(seasons, n, function (team, season, j)
local s = byKey[team.key]
if not s then
s = { counts = {}, total = 0, first = #list + 1 }
for c = 1, n do
s.counts[c] = 0
end
byKey[team.key] = s
table.insert(list, s)
end
s.counts[j] = s.counts[j] + 1
s.total = s.total + 1
s.label = team.label -- подпись — из последнего упоминания
s.target = team.target or s.target
season.counted = true
end)
return list
end
local function rank(list, cfg)
table.sort(list, function (a, b)
if not cfg.olympic and a.total ~= b.total then
return a.total > b.total
end
for c = 1, cfg.n do
if a.counts[c] ~= b.counts[c] then
return a.counts[c] > b.counts[c]
end
end
if cfg.alphabetical then
local x, y = norm(a.label), norm(b.label)
if x ~= y then
return x < y
end
end
return a.first < b.first
end)
local place = 0
for i, s in ipairs(list) do
local same = i > 1
if same then
for c = 1, cfg.n do
if list[i - 1].counts[c] ~= s.counts[c] then
same = false
break
end
end
end
if not same then
place = place + 1
end
s.place = place
end
end
local function seasonsNote(seasons)
local first, last
for _, season in ipairs(seasons) do
if season.counted then
local label = plainText(season.label)
if label ~= '' then
first = first or label
last = label
end
end
end
if not first then
return nil
end
local y1, y2 = tonumber(first:match('%d%d%d%d')), tonumber(last:match('%d%d%d%d'))
if y1 and y2 and y1 > y2 then
first, last = last, first
end
if first == last then
return 'Учтён сезон ' .. first
end
return 'Учтены сезоны с ' .. first .. ' по ' .. last
end
------------------------------------------------------------------------
-- Вывод
------------------------------------------------------------------------
local function newTable()
return mw.html.create('table')
:addClass('wikitable')
:css('text-align', 'center')
end
local function colorHeader(row, text, color)
local th = row:tag('th'):wikitext(text)
if color and color ~= '' then
th:css('background-color', color)
end
return th
end
local function renderStandings(list, cfg)
local root = newTable()
local head = root:tag('tr')
head:tag('th')
head:tag('th'):css('width', '18em'):css('height', '2em'):wikitext(cfg.clubHeader)
for j = 1, cfg.n do
colorHeader(head, cfg.labels[j], cfg.colors[j])
end
head:tag('th'):css('width', '8em'):css('height', '2em'):wikitext(cfg.total)
local function number(v)
return cfg.bold and ("'''" .. v .. "'''") or tostring(v)
end
if #list == 0 then
root:tag('tr'):tag('td'):attr('colspan', cfg.n + 3):wikitext('Призёров пока нет')
end
local i = 1
while i <= #list do
local j = i
while j < #list and list[j + 1].place == list[i].place do
j = j + 1
end
for k = i, j do
local s = list[k]
local tr = root:tag('tr')
if k == i then
local td = tr:tag('td'):wikitext(s.place)
if j > i then
td:attr('rowspan', j - i + 1)
end
end
local name = s.target and makeLink(s.target, s.label) or s.label
tr:tag('td'):css('text-align', 'left'):wikitext(name)
for c = 1, cfg.n do
tr:tag('td'):wikitext(number(s.counts[c]))
end
tr:tag('td'):wikitext(number(s.total))
end
i = j + 1
end
return tostring(root)
end
local function cellText(cell)
if cell.note then
return cell.note
end
local out = {}
for _, part in ipairs(cell.parts) do
table.insert(out, part.team and part.team.display or part.raw)
end
return table.concat(out, '<br>')
end
local function renderSeasons(seasons, cfg)
local root = newTable()
local head = root:tag('tr')
head:tag('th'):wikitext(cfg.seasonHeader)
for j = 1, cfg.n do
colorHeader(head, cfg.places[j], cfg.colors[j])
end
for e = 1, #cfg.extras do
head:tag('th'):wikitext(cfg.extras[e])
end
for _, season in ipairs(seasons) do
local tr = root:tag('tr')
tr:tag('td'):wikitext(season.label)
local first = season.places[1]
local merged = first and first.note ~= nil
for j = 2, cfg.n do
if season.places[j].raw ~= '' then
merged = false
end
end
if merged then
tr:tag('td'):attr('colspan', cfg.n):wikitext(first.note)
else
for j = 1, cfg.n do
tr:tag('td'):wikitext(cellText(season.places[j]))
end
end
for e = 1, #cfg.extras do
tr:tag('td'):wikitext(season.extras[e])
end
end
return tostring(root)
end
local function messages(list)
local out = {}
for _, text in ipairs(list) do
if mw.addWarning then
mw.addWarning('Призёры: ' .. text)
end
table.insert(out, '<div class="error">Призёры: ' .. text .. '</div>')
end
return #out > 0 and (table.concat(out, '\n') .. '\n') or ''
end
------------------------------------------------------------------------
-- Точки входа
------------------------------------------------------------------------
local function main(frame, forcedView)
local warnings = {}
local rows, settings = collectArgs(frame, forcedView, warnings)
local cfg = makeConfig(settings, warnings)
local clubs = loadClubs(settings['клубы'], warnings)
if #rows > 0 then
for _, cells in ipairs(rows) do
if #cells > 1 + cfg.n + #cfg.extras then
table.insert(warnings, 'в сезоне «' .. plainText(cells[1]) .. '» лишние ячейки (всего '
.. #cells .. ', а столбцов ' .. (1 + cfg.n + #cfg.extras) .. '). '
.. 'Каждый сезон пишите с новой строки, дополнительные столбцы объявите в «доп».')
end
end
else
local extras, err
rows, extras, err = readSourceTable(frame, settings, cfg, warnings)
if err then
return messages(warnings) .. '<strong class="error">Призёры: ' .. err .. '</strong>'
end
if not cfg.extrasGiven then
cfg.extras = extras
end
end
if #rows == 0 then
return messages(warnings) .. '<strong class="error">Призёры: нет данных — '
.. 'впишите сезоны (каждый с новой строки) или укажите «источник».</strong>'
end
local seasons = buildSeasons(rows, cfg, clubs)
resolveTeams(seasons, cfg.n)
local list = tally(seasons, cfg.n)
-- Один клуб, записанный ссылками на разные статьи, попал бы в зачёт двумя строками.
local byName = {}
for _, s in ipairs(list) do
local name = norm(s.label)
local other = byName[name]
if other then
table.insert(warnings, 'в зачёте две строки «' .. s.label .. '»: ссылки ведут на «'
.. (other.target or 'без ссылки') .. '» и «' .. (s.target or 'без ссылки')
.. '». Если это один клуб, запишите его везде одинаково.')
else
byName[name] = s
end
end
if settings.view == 'seasons' then
return messages(warnings) .. renderSeasons(seasons, cfg)
end
rank(list, cfg)
local out = renderStandings(list, cfg)
if cfg.note then
local note = seasonsNote(seasons)
if note then
out = out .. '\n<small>' .. note .. '</small>'
end
end
return messages(warnings) .. out
end
function p.main(frame)
return main(frame)
end
function p.seasons(frame)
return main(frame, 'seasons')
end
function p.standings(frame)
return main(frame, 'standings')
end
-- {{#invoke:Призёры|клуб|ДСК}} → [[ДСК (футбольный клуб, Наро-Фоминск)|ДСК]]
-- Второй параметр — другая подпись. Пригодится в доп. столбцах (бомбардир и т. п.).
function p.club(frame)
local args = frame.args
if args[1] == nil then
local parent = frame:getParent()
args = parent and parent.args or {}
end
local name = trim(args[1])
local label = trim(args[2])
if label == '' then
label = name
end
local listName = trim(args['клубы'])
local target = loadClubs(listName ~= '' and listName or nil, {})[name]
if type(target) == 'string' and trim(target) ~= '' then
return makeLink(normTitle(target), label)
end
return label
end
p['таблица'] = p.main
p['сезоны'] = p.seasons
p['зачёт'] = p.standings
p['зачет'] = p.standings
p['клуб'] = p.club
return p