Перейти к содержанию

Модуль:Football: различия между версиями

Создан модуль
 
Нет описания правки
 
(не показаны 52 промежуточные версии этого же участника)
Строка 2: Строка 2:


-- =========================================================
-- =========================================================
-- Модуль:Football
-- НАСТРОЙКИ
--
-- Формат:
--
-- {{#invoke:Football|main
-- | teams = НАР;АПР;ВЕР;ЗВЕ
--
-- | name_НАР = [[Наро-Фоминск]]
-- | name_АПР = [[Апрелевка]]
-- | name_ВЕР = [[Верея]]
-- | name_ЗВЕ = [[Звезда]]
--
-- | match1 = НАР;АПР;2:1
-- | match2 = ВЕР;ЗВЕ;0:0
-- | match3 = НАР;ВЕР;1:1
-- }}
--
-- match = хозяева;гости;счёт
--
-- Поддерживаются:
--  2:1
--  0:0
--  -
--  —
--  перенесён
--  отменён
-- =========================================================
-- =========================================================


-- Виды карточек в порядке вывода столбцов.
-- key — часть имени параметров: matchN_<key> и <key>_КОД.
local CARD_TYPES = {
{
key = "yellow",
title = "Жёлтые карточки",
background = "#ffd200"
},
{
key = "yellowred",
title = "Удаления за вторую жёлтую карточку",
background = "linear-gradient(135deg, #ffd200 50%, #e30613 50%)"
},
{
key = "red",
title = "Прямые красные карточки",
background = "#e30613"
}
}


-- ---------------------------------------------------------
-- Сколько снятий очков можно привязать к одному матчу:
-- Вспомогательные функции
-- matchN_deduction, matchN_deduction2 … matchN_deduction5
-- ---------------------------------------------------------
local MAX_DEDUCTIONS = 5


local function trim(s)
-- Части параметра start_КОД (стартовые показатели команды)
    if not s then
-- в порядке записи. label — обозначение части
        return ''
-- в описании формата и в сообщениях об ошибках.
    end
local START_PARTS = {
{ key = "played", label = "И" },
{ key = "wins", label = "В" },
{ key = "draws", label = "Н" },
{ key = "losses", label = "П" },
{ key = "goalsFor", label = "МЗ" },
{ key = "goalsAgainst", label = "МП" },
{ key = "points", label = "О" },
{ key = "yellow", label = "ЖК", card = true },
{ key = "yellowred", label = "2ЖК", card = true },
{ key = "red", label = "КК", card = true }
}


    return mw.text.trim(s)
-- Формат параметра start_КОД: «И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК»
local START_FORMAT
 
do
local labels = {}
 
for _, part in ipairs(START_PARTS) do
table.insert(labels, part.label)
end
 
START_FORMAT = table.concat(labels, ";")
end
 
 
-- =========================================================
-- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
-- =========================================================
 
local function trim(value)
if value == nil then
return ""
end
 
return mw.text.trim(tostring(value))
end
 
 
local function split(value, separator)
local result = {}
 
for part in mw.text.gsplit(value, separator, true) do
table.insert(result, trim(part))
end
 
return result
end
end




local function split(s, separator)
local function number(value, default)
    if not s or s == '' then
local n = tonumber(value)
        return {}
 
    end
if n == nil then
return default
end


    return mw.text.split(s, separator, true)
return n
end
end




local function htmlEscape(s)
-- Число для текста: 3 → «3», 1.5 → «1,5»
    return mw.text.nowiki(s)
local function formatNumber(n)
if n == math.floor(n) then
return string.format("%d", n)
end
 
return (string.gsub(tostring(n), "%.", ","))
end
end




-- ---------------------------------------------------------
-- Форма слова после числа: 1 очко, 2 очка, 5 очков
-- Разбор счёта
local function plural(n, one, few, many)
-- ---------------------------------------------------------
n = math.abs(n)
 
if n ~= math.floor(n) then
return few
end
 
local lastTwo = n % 100
local last = n % 10
 
if lastTwo >= 11 and lastTwo <= 14 then
return many
elseif last == 1 then
return one
elseif last >= 2 and last <= 4 then
return few
end
 
return many
end
 
 
-- Пара значений «хозяева:гости», например «2:1»
local function parsePair(value)
local home, away =
trim(value):match("^(%d+)%s*:%s*(%d+)$")
 
if home and away then
return tonumber(home), tonumber(away)
end
 
return nil
end
 
 
-- Метка сноски: 1 → a, 2 → b … 26 → z, 27 → aa
local function noteLabel(index)
local label = ""
 
while index > 0 do
local rest = (index - 1) % 26
label = string.char(97 + rest) .. label
index = math.floor((index - 1) / 26)
end
 
return label
end
 
 
-- =========================================================
-- РАЗБОР СЧЁТА
-- =========================================================


local function parseScore(score)
local function parseScore(score)
    score = trim(score)
score = trim(score)
 
-- Матч не сыгран
if score == "" or score == "-" or score == "—" then
return {
played = false,
status = ""
}
end
 
-- Перенесён
if score == "перенесён" or score == "перенесен" then
return {
played = false,
status = "перенесён"
}
end
 
-- Отменён
if score == "отменён" or score == "отменен" then
return {
played = false,
status = "отменён"
}
end
 
-- Обычный счёт
local home, away = score:match("^(%d+)%s*:%s*(%d+)$")
 
if home and away then
return {
played = true,
homeGoals = tonumber(home),
awayGoals = tonumber(away),
score = home .. ":" .. away
}
end
 
-- Неизвестный формат
return {
played = false,
status = "ошибка: " .. score
}
end
 


    if score == '' then
-- =========================================================
        return nil
-- РАЗБОР ЗАПИСИ О МАТЧЕ
    end
-- =========================================================


    -- Матч ещё не сыгран
-- Значение matchN или h2h_matchN в одном из форматов:
    if score == '-' or score == '—' then
-- тур;круг;хозяева;гости;счёт
        return {
-- круг;хозяева;гости;счёт
            played = false,
-- хозяева;гости;счёт
            status = ''
-- Возвращает nil, если частей меньше трёх.
        }
local function parseMatchValue(value)
    end


    -- Перенесённый матч
local parts = split(value, ";")
    if score == 'перенесён' or score == 'перенесен' then
        return {
            played = false,
            status = 'перенесён'
        }
    end


    -- Отменённый матч
-- Формат с номером тура
    if score == 'отменён' or score == 'отменен' then
if #parts >= 5 then
        return {
return {
            played = false,
tour = parts[1],
            status = 'отменён'
round = parts[2],
        }
home = parts[3],
    end
away = parts[4],
score = parts[5]
}
end


    -- Обычный счёт
-- Формат с номером круга
    local home, away = score:match('^(%d+)%s*:%s*(%d+)$')
if #parts == 4 then
return {
tour = "",
round = parts[1],
home = parts[2],
away = parts[3],
score = parts[4]
}
end


    if home and away then
-- Формат без номера круга
        return {
if #parts == 3 then
            played = true,
return {
            home = tonumber(home),
tour = "",
            away = tonumber(away),
round = "",
            score = home .. ':' .. away
home = parts[1],
        }
away = parts[2],
    end
score = parts[3]
}
end


    -- Неизвестное значение
return nil
    return {
        played = false,
        status = score
    }
end
end




-- ---------------------------------------------------------
-- =========================================================
-- Создание пустой статистики команды
-- СОЗДАНИЕ КОМАНДЫ
-- ---------------------------------------------------------
-- =========================================================


local function newTeam(code, name)
local function newTeam(code, name)
    return {
return {
        code = code,
code = code,
        name = name,
name = name,
 
played = 0,
wins = 0,
draws = 0,
losses = 0,
 
goalsFor = 0,
goalsAgainst = 0,


        played = 0,
points = 0,
        wins = 0,
        draws = 0,
        losses = 0,


        goalsFor = 0,
-- Сколько очков снято всего
        goalsAgainst = 0,
deducted = 0,


        points = 0
-- Сноски о снятии очков
    }
notes = {},
 
-- Карточки из стартовых показателей и матчей
matchCards = {
yellow = 0,
yellowred = 0,
red = 0
},
 
-- Итоговые карточки (из матчей или из параметров команды)
yellow = 0,
yellowred = 0,
red = 0
}
end
end




-- ---------------------------------------------------------
-- Добавляет к показателям команды стартовые
-- Основная функция
-- (параметр start_КОД), если они указаны
-- ---------------------------------------------------------
local function addStart(team, start)
 
if not start then
return
end
 
team.played = team.played + start.played
team.wins = team.wins + start.wins
team.draws = team.draws + start.draws
team.losses = team.losses + start.losses
 
team.goalsFor =
team.goalsFor + start.goalsFor
 
team.goalsAgainst =
team.goalsAgainst + start.goalsAgainst
 
team.points =
team.points + start.points
 
for _, cardType in ipairs(CARD_TYPES) do
 
team.matchCards[cardType.key] =
team.matchCards[cardType.key] + start[cardType.key]
 
end
 
end
 
 
-- =========================================================
-- СОЗДАНИЕ HTML
-- =========================================================
 
local function cell(tag, content, class)
local element = mw.html.create(tag)
 
if class and class ~= "" then
element
:addClass(class)
end
 
if content ~= nil then
element
:wikitext(tostring(content))
end
 
return element
end
 
 
-- Значок карточки для заголовка столбца.
-- Рисуется средствами CSS, поэтому не нужны файлы изображений.
local function cardIcon(cardType)
return mw.html.create("span")
:addClass("football-card")
:addClass("football-card-" .. cardType.key)
:attr("title", cardType.title)
:css("display", "inline-block")
:css("width", "0.7em")
:css("height", "0.95em")
:css("vertical-align", "middle")
:css("border-radius", "0.12em")
:css("border", "1px solid rgba(0, 0, 0, 0.35)")
:css("background", cardType.background)
:wikitext("&#8203;")
end
 
 
-- =========================================================
-- ОСНОВНАЯ ФУНКЦИЯ
-- =========================================================


function p.main(frame)
function p.main(frame)


    local args = frame.args
local args = frame.args
 
-- -----------------------------------------------------
-- НАСТРОЙКИ ОЧКОВ
-- -----------------------------------------------------
 
local pointsWin =
number(args.points_win, 3)
 
local pointsDraw =
number(args.points_draw, 1)
 
local pointsLoss =
number(args.points_loss, 0)
 
 
-- -----------------------------------------------------
-- ЗАГОЛОВКИ РАЗДЕЛОВ
-- -----------------------------------------------------
 
-- 3 → «=== Заголовок ===» (по умолчанию),
-- 2 → «== Заголовок ==», 4 → «==== Заголовок ====»
local headingLevel =
math.floor(number(args.heading_level, 3))
 
headingLevel =
math.max(2, math.min(6, headingLevel))
 
local function heading(text)
local marks = string.rep("=", headingLevel)
 
-- Заголовок вики-разметки должен стоять
-- на отдельной строке
return "\n" .. marks .. " " .. text .. " " .. marks .. "\n"
end
 
 
-- -----------------------------------------------------
-- КОМАНДЫ
-- -----------------------------------------------------
 
local teamsParameter = trim(args.teams)
 
if teamsParameter == "" then
return "'''Ошибка:''' не указан параметр ''teams''."
end
 
local teamCodes = split(teamsParameter, ";")
 
local teams = {}
local teamOrder = {}
 
for _, code in ipairs(teamCodes) do
 
if code ~= "" then
 
if teams[code] then
return "'''Ошибка:''' команда с кодом '''" ..
code .. "''' указана более одного раза."
end
 
local name = trim(args["name_" .. code])
 
if name == "" then
name = code
end
 
teams[code] = newTeam(code, name)
 
table.insert(teamOrder, code)
end
end
 
 
-- -----------------------------------------------------
-- СТАРТОВЫЕ ПОКАЗАТЕЛИ
-- -----------------------------------------------------
 
-- start_КОД = И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК — показатели,
-- с которыми команда начинает турнир. Нужны, например,
-- когда после первого этапа таблица делится на группы:
-- таблица каждой группы начинается с итогов первого этапа.
-- Пустая часть считается нулём; пустые И и О вычисляются
-- по победам, ничьим и поражениям. Параметры для кодов,
-- которых нет в teams, не читаются.
 
-- Возвращает показатели (nil, если параметр не указан)
-- или nil и текст ошибки
local function parseStart(code)
 
local paramName = "start_" .. code
local raw = trim(args[paramName])
 
if raw == "" then
return nil
end
 
local function fail(text)
return nil,
"'''Ошибка:''' в параметре ''" ..
paramName .. "'' " .. text
end
 
local parts = split(raw, ";")
 
if #parts > #START_PARTS then
 
return fail(
"больше " .. #START_PARTS .. " значений. " ..
"Формат: ''" .. START_FORMAT .. "''."
)
 
end
 
local start = {
-- Указаны ли карточки
hasCards = false
}
 
for index, part in ipairs(START_PARTS) do
 
local value = parts[index] or ""
 
if part.key == "points" then
 
-- Очки могут быть дробными и отрицательными:
-- «12,5», «-3» (пустые вычисляются ниже)
if value ~= "" then
 
local normalized =
string.gsub(value, ",", ".")
 
normalized =
string.gsub(normalized, "−", "-")
 
local amount = tonumber(normalized)
 
if amount == nil or
  amount ~= amount or
  math.abs(amount) == math.huge then
 
return fail(
"очки (О) должны быть числом, " ..
"а указано «" .. value .. "»."
)
 
end
 
start.points = amount
 
end
 
elseif value == "" then
 
-- Пустое число игр вычисляется ниже
if part.key ~= "played" then
start[part.key] = 0
end
 
elseif value:match("^%d+$") then
 
start[part.key] = tonumber(value)
 
if part.card then
start.hasCards = true
end
 
else
 
return fail(
"значение " .. part.label ..
" должно быть целым неотрицательным числом, " ..
"а указано «" .. value .. "»."
)
 
end
 
end
 
 
local results =
start.wins + start.draws + start.losses
 
if start.played == nil then
 
start.played = results
 
elseif start.played ~= results then
 
return fail(
"число игр (" .. start.played .. ") не равно " ..
"сумме побед, ничьих и поражений (" ..
results .. ")."
)
 
end
 
 
if start.points == nil then
 
start.points =
start.wins * pointsWin +
start.draws * pointsDraw +
start.losses * pointsLoss
 
end
 
return start
end
 
 
for _, code in ipairs(teamOrder) do
 
local start, errorText = parseStart(code)
 
if errorText then
return errorText
end
 
if start then
 
teams[code].start = start
 
addStart(teams[code], start)
 
end
 
end
 
 
-- Префикс якорей для сносок. Он должен быть уникальным
-- на странице, поэтому по умолчанию зависит от списка команд.
local idPrefix = trim(args.id)
 
if idPrefix == "" then
 
idPrefix = "football"
 
if mw.hash and mw.hash.hashValue then
 
local ok, hash = pcall(
mw.hash.hashValue,
"crc32b",
teamsParameter
)
 
if ok and hash then
idPrefix = idPrefix .. "-" .. hash
end
 
end
 
else
-- Символы, которые ломают якорь или ссылку [[#…]]
idPrefix = string.gsub(idPrefix, "[%s%[%]{}|#<>\"'&]+", "_")
end
 
 
-- -----------------------------------------------------
-- УЧЁТ РЕЗУЛЬТАТА И СНЯТИЯ ОЧКОВ
-- -----------------------------------------------------
 
-- Добавляет результат сыгранного матча в статистику.
-- teamsMap — итоговые команды или команды «по турам».
local function addResult(teamsMap, match)
 
local homeTeam = teamsMap[match.home]
local awayTeam = teamsMap[match.away]
 
homeTeam.played = homeTeam.played + 1
awayTeam.played = awayTeam.played + 1
 
homeTeam.goalsFor =
homeTeam.goalsFor + match.homeGoals
 
homeTeam.goalsAgainst =
homeTeam.goalsAgainst + match.awayGoals
 
awayTeam.goalsFor =
awayTeam.goalsFor + match.awayGoals
 
awayTeam.goalsAgainst =
awayTeam.goalsAgainst + match.homeGoals
 
 
if match.homeGoals > match.awayGoals then
 
homeTeam.wins = homeTeam.wins + 1
awayTeam.losses = awayTeam.losses + 1
 
homeTeam.points =
homeTeam.points + pointsWin
 
awayTeam.points =
awayTeam.points + pointsLoss
 
 
elseif match.homeGoals < match.awayGoals then
 
awayTeam.wins = awayTeam.wins + 1
homeTeam.losses = homeTeam.losses + 1
 
awayTeam.points =
awayTeam.points + pointsWin
 
homeTeam.points =
homeTeam.points + pointsLoss
 
 
else
 
homeTeam.draws = homeTeam.draws + 1
awayTeam.draws = awayTeam.draws + 1
 
homeTeam.points =
homeTeam.points + pointsDraw
 
awayTeam.points =
awayTeam.points + pointsDraw
 
end
 
end
 
 
-- Снимает очки, привязанные к матчу
-- (независимо от того, сыгран ли он)
local function applyDeductions(teamsMap, match)
 
for _, deduction in ipairs(match.deductions) do
 
local team = teamsMap[deduction.team]
 
team.points =
team.points - deduction.points
 
team.deducted =
team.deducted + deduction.points
 
end
 
end
 
 
-- -----------------------------------------------------
-- МАТЧИ
-- -----------------------------------------------------
 
local matches = {}
 
-- Все снятия очков в порядке матчей
local deductions = {}
 
-- Указаны ли где-нибудь карточки
local hasCards = false
 
local matchNumber = 1
 
while true do
 
local value = args["match" .. matchNumber]
 
if value == nil or trim(value) == "" then
break
end
 
local fields = parseMatchValue(value)
 
if not fields then
 
return "'''Ошибка:''' матч " ..
matchNumber ..
" имеет неправильный формат."
 
end
 
local tour = fields.tour
local round = fields.round
local home = fields.home
local away = fields.away
local score = fields.score
 
 
-- Проверяем команды
 
if not teams[home] then
 
return "'''Ошибка:''' в матче " ..
matchNumber ..
" неизвестная команда '''" ..
home .. "'''."
 
end
 
if not teams[away] then
 
return "'''Ошибка:''' в матче " ..
matchNumber ..
" неизвестная команда '''" ..
away .. "'''."
 
end
 
 
local parsed = parseScore(score)
 
local match = {
number = matchNumber,
tour = tour,
round = round,
 
home = home,
away = away,
 
played = parsed.played,
status = parsed.status,
 
homeGoals = parsed.homeGoals,
awayGoals = parsed.awayGoals,
score = parsed.score,
 
deductions = {}
}
 
 
-- -------------------------------------------------
-- КАРТОЧКИ В МАТЧЕ
-- -------------------------------------------------
 
-- matchN_yellow, matchN_yellowred, matchN_red
-- в формате «хозяева:гости»
 
for _, cardType in ipairs(CARD_TYPES) do
 
local paramName =
"match" .. matchNumber .. "_" .. cardType.key
 
local raw = trim(args[paramName])
 
if raw ~= "" then
 
local homeCards, awayCards = parsePair(raw)
 
if homeCards == nil then
 
return "'''Ошибка:''' параметр ''" ..
paramName ..
"'' имеет неправильный формат «" ..
raw ..
"». Укажите карточки хозяев и гостей " ..
"через двоеточие, например ''2:1''."
 
end
 
local homeCardsTable = teams[home].matchCards
local awayCardsTable = teams[away].matchCards
 
homeCardsTable[cardType.key] =
homeCardsTable[cardType.key] + homeCards
 
awayCardsTable[cardType.key] =
awayCardsTable[cardType.key] + awayCards
 
hasCards = true
 
end
 
end
 
 
-- -------------------------------------------------
-- СНЯТИЕ ОЧКОВ ЗА МАТЧ
-- -------------------------------------------------
 
-- matchN_deduction = код;очки;причина
 
for i = 1, MAX_DEDUCTIONS do
 
local paramName =
"match" .. matchNumber .. "_deduction"
 
if i > 1 then
paramName = paramName .. i
end
 
local raw = trim(args[paramName])
 
if raw ~= "" then
 
local deductionParts = split(raw, ";")
 
local code = deductionParts[1]
 
-- Допускается и «1,5», и «1.5»
local amount = tonumber(
(string.gsub(deductionParts[2] or "", ",", "."))
)
 
if code ~= home and code ~= away then
 
return "'''Ошибка:''' в параметре ''" ..
paramName ..
"'' должна быть указана команда матча " ..
matchNumber .. " ('''" .. home ..
"''' или '''" .. away ..
"'''), а указано «" .. code .. "»."
 
end
 
if amount == nil or
  amount == 0 or
  amount ~= amount or
  math.abs(amount) == math.huge then
 
return "'''Ошибка:''' в параметре ''" ..
paramName ..
"'' не указано, сколько очков снято."
 
end
 
-- Причина — всё, что после второй «;»
local reason =
table.concat(deductionParts, "; ", 3)
 
reason = string.gsub(reason, "[%s%.]+$", "")
 
local deduction = {
team = code,
points = math.abs(amount),
reason = reason,
match = match
}
 
table.insert(match.deductions, deduction)
table.insert(deductions, deduction)
 
end
 
end
 
 
table.insert(matches, match)
 
 
-- -------------------------------------------------
-- СТАТИСТИКА
-- -------------------------------------------------
 
if parsed.played then
addResult(teams, match)
end
 
applyDeductions(teams, match)
 
 
matchNumber = matchNumber + 1
end
 
 
-- =====================================================
-- МАТЧИ ДЛЯ ЛИЧНЫХ ВСТРЕЧ
-- =====================================================
 
-- h2h_matchN — матчи, сыгранные до этого турнира (например,
-- в первом этапе), в тех же форматах, что и matchN.
-- Они учитываются только в критерии «личные» и не влияют
-- на показатели команд, шахматку и таблицу по турам.
-- Матчи с командами, которых нет в teams, пропускаются,
-- поэтому можно вставить все матчи первого этапа.
 
local h2hMatches = {}
 
local h2hNumber = 1
 
while true do
 
local paramName = "h2h_match" .. h2hNumber
local value = trim(args[paramName])
 
if value == "" then
break
end
 
local fields = parseMatchValue(value)
 
if not fields then
 
return "'''Ошибка:''' параметр ''" ..
paramName ..
"'' имеет неправильный формат."
 
end
 
local parsed = parseScore(fields.score)
 
if parsed.played and
  teams[fields.home] and
  teams[fields.away] then
 
table.insert(h2hMatches, {
home = fields.home,
away = fields.away,
homeGoals = parsed.homeGoals,
awayGoals = parsed.awayGoals
})
 
end
 
h2hNumber = h2hNumber + 1
end
 
 
-- =====================================================
-- КАРТОЧКИ КОМАНД
-- =====================================================
 
-- Параметры yellow_КОД, yellowred_КОД, red_КОД задают
-- итог команды. Если указан хотя бы один из них, карточки
-- команды берутся только из этих параметров (неуказанные
-- считаются нулём), а карточки из матчей и стартовых
-- показателей не учитываются.
 
for _, code in ipairs(teamOrder) do
 
local team = teams[code]
 
local override = {}
local hasOverride = false
 
for _, cardType in ipairs(CARD_TYPES) do
 
local paramName = cardType.key .. "_" .. code
local raw = trim(args[paramName])
 
if raw ~= "" then
 
if not raw:match("^%d+$") then
 
return "'''Ошибка:''' параметр ''" ..
paramName ..
"'' должен быть целым неотрицательным " ..
"числом, а указано «" .. raw .. "»."
 
end
 
override[cardType.key] = tonumber(raw)
hasOverride = true
 
end
 
end
 
for _, cardType in ipairs(CARD_TYPES) do
 
if hasOverride then
team[cardType.key] =
override[cardType.key] or 0
else
team[cardType.key] =
team.matchCards[cardType.key]
end
 
end
 
if hasOverride or
  (team.start and team.start.hasCards) then
hasCards = true
end
 
end
 
 
-- =====================================================
-- РАЗНИЦА МЯЧЕЙ
-- =====================================================
 
for _, code in ipairs(teamOrder) do
 
local team = teams[code]
 
team.goalDifference =
team.goalsFor - team.goalsAgainst
 
end
 
 
-- =====================================================
-- ЛИЧНЫЕ ВСТРЕЧИ
-- =====================================================
 
-- Добавляет сыгранный матч к статистике личных встреч
-- команды code, если обе команды матча входят в группу.
local function addHeadToHeadMatch(result, code, group, match)
 
if not (group[match.home] and group[match.away]) then
return
end
 
local scored
local conceded
 
if match.home == code then
scored = match.homeGoals
conceded = match.awayGoals
elseif match.away == code then
scored = match.awayGoals
conceded = match.homeGoals
else
return
end
 
result.goalsFor =
result.goalsFor + scored
 
result.goalsAgainst =
result.goalsAgainst + conceded
 
if scored > conceded then
 
result.points =
result.points + pointsWin
 
result.wins =
result.wins + 1
 
elseif scored == conceded then
 
result.points =
result.points + pointsDraw
 
else
 
result.points =
result.points + pointsLoss
 
end
 
end
 
 
-- Возвращает статистику команды только в матчах
-- против команд из указанной группы.
-- Матчи h2h_matchN учитываются всегда, а матчи турнира —
-- если задан lastTour, только с номером тура не больше lastTour.
-- Снятие очков на личные встречи не влияет.
 
local function headToHead(code, group, lastTour)
 
local result = {
points = 0,
goalsFor = 0,
goalsAgainst = 0,
goalDifference = 0,
wins = 0
}
 
-- Матчи, сыгранные до этого турнира
for _, match in ipairs(h2hMatches) do
addHeadToHeadMatch(result, code, group, match)
end
 
for _, match in ipairs(matches) do
 
local inRange = true
 
if lastTour then
local tourNumber = tonumber(match.tour)
 
inRange =
tourNumber ~= nil and tourNumber <= lastTour
end
 
if match.played and inRange then
addHeadToHeadMatch(result, code, group, match)
end
 
end
 
result.goalDifference =
result.goalsFor - result.goalsAgainst
 
return result
end
 
 
-- =====================================================
-- КРИТЕРИИ ОПРЕДЕЛЕНИЯ МЕСТ
-- =====================================================
 
-- По умолчанию:
-- 1. очки
-- 2. разница мячей
-- 3. забитые мячи
-- 4. победы
-- 5. название
 
local criteria = {}
 
for i = 1, 10 do
 
local criterion = trim(args["tie" .. i])
 
if criterion ~= "" then
table.insert(criteria, criterion)
end
 
end
 
-- Если критерии не указаны
 
if #criteria == 0 then
 
criteria = {
"очки",
"разница",
"забитые",
"победы",
"название"
}
 
end
 
 
-- -----------------------------------------------------
-- Сравнение команд
-- -----------------------------------------------------
 
-- Возвращает функцию сравнения для table.sort.
-- teamsMap — команды, среди которых ищутся равные по очкам
-- (для критерия «личные»); lastTour — последний
-- учитываемый тур (nil — все матчи).
 
local function makeComparator(teamsMap, lastTour)
 
return function(a, b)
 
for _, criterion in ipairs(criteria) do
 
-- -----------------------------------------
-- ОЧКИ
-- -----------------------------------------
 
if criterion == "очки" then
 
if a.points ~= b.points then
return a.points > b.points
end
 
 
-- -----------------------------------------
-- РАЗНИЦА МЯЧЕЙ
-- -----------------------------------------
 
elseif criterion == "разница" then
 
if a.goalDifference ~= b.goalDifference then
return a.goalDifference > b.goalDifference
end
 
 
-- -----------------------------------------
-- ЗАБИТЫЕ МЯЧИ
-- -----------------------------------------
 
elseif criterion == "забитые" then
 
if a.goalsFor ~= b.goalsFor then
return a.goalsFor > b.goalsFor
end
 
 
-- -----------------------------------------
-- ПОБЕДЫ
-- -----------------------------------------
 
elseif criterion == "победы" then
 
if a.wins ~= b.wins then
return a.wins > b.wins
end
 
 
-- -----------------------------------------
-- ИГРЫ
-- -----------------------------------------
 
elseif criterion == "игры" then
 
if a.played ~= b.played then
return a.played > b.played
end
 
 
-- -----------------------------------------
-- ЛИЧНЫЕ
-- -----------------------------------------
 
elseif criterion == "личные" then
 
-- Все команды с таким же
-- количеством очков, как у a и b.
 
local group = {}
 
for _, code in ipairs(teamOrder) do
 
if teamsMap[code].points == a.points then
group[code] = true
end
 
end
 
local ha = headToHead(a.code, group, lastTour)
local hb = headToHead(b.code, group, lastTour)
 
if ha.points ~= hb.points then
return ha.points > hb.points
end
 
if ha.goalDifference ~= hb.goalDifference then
return ha.goalDifference > hb.goalDifference
end
 
if ha.goalsFor ~= hb.goalsFor then
return ha.goalsFor > hb.goalsFor
end
 
if ha.wins ~= hb.wins then
return ha.wins > hb.wins
end
 
 
-- -----------------------------------------
-- НАЗВАНИЕ
-- -----------------------------------------
 
elseif criterion == "название" then
 
local aname =
mw.text.unstrip(a.name)
 
local bname =
mw.text.unstrip(b.name)
 
if aname ~= bname then
return aname < bname
end
 
end
 
end
 
 
-- Последний резервный критерий —
-- код команды.
 
return a.code < b.code
end
 
end
 
 
-- =====================================================
-- НУМЕРАЦИЯ МЕСТ
-- =====================================================
 
-- first_place — номер первого места таблицы. Например,
-- в таблице группы за 7–14 места первая строка — 7-е место.
-- Места в highlightN и progress_highlightN указываются
-- в этой же нумерации.
 
local placeOffset = 0
 
local firstPlace = trim(args.first_place)
 
if firstPlace ~= "" then
 
if not firstPlace:match("^%d+$") or
  tonumber(firstPlace) < 1 then
 
return "'''Ошибка:''' параметр ''first_place'' должен " ..
"быть целым положительным числом, а указано «" ..
firstPlace .. "»."
 
end
 
placeOffset = tonumber(firstPlace) - 1
 
end
 
 
-- =====================================================
-- Выделение победителей и проигравших
-- =====================================================
 
-- highlightN = место;количество;класс;текст
 
local highlights = {}
 
-- Есть ли текст хотя бы у одного выделения
local hasHighlightText = false
 
for i = 1, 15 do
 
local hl = trim(args["highlight" .. i])
 
if hl == "" then
break
end
 
local parts = split(hl, ";")
 
local place = tonumber(parts[1])
 
if place then
 
-- Номер строки таблицы с учётом first_place
local row = math.floor(place) - placeOffset
 
local count =
math.max(1, math.floor(tonumber(parts[2]) or 1))
 
-- Зона, которая начинается выше таблицы,
-- обрезается по первой строке
if row < 1 then
count = count + row - 1
row = 1
end
 
-- Зоны целиком за пределами таблицы пропускаются
if count >= 1 and row <= #teamOrder then
 
local highlight = {
place = row,
count = count,
color = parts[3] or "",
-- Текст может сам содержать «;»
text = table.concat(parts, "; ", 4)
}
 
if highlight.text ~= "" then
hasHighlightText = true
end
 
table.insert(highlights, highlight)
 
end
 
end
 
end
 
table.sort(highlights, function(a, b)
return a.place < b.place
end)
 
 
-- -----------------------------------------------------
-- Режим отображения шахматки
-- -----------------------------------------------------
-- По умолчанию первая строчка - домашние игры, вторая строчка - гостевые
-- Если resultsMode = round, то первая строчка - 1 круг, вторая строчка - 2 круг
local resultsMode = 0
if args["resultsMode"] ~= nil and args["resultsMode"] ~= "" then
if args["resultsMode"] == "round" then
resultsMode = 1
end
end
 
-- rounds = 1 — однокруговой турнир: у каждой команды в шахматке
-- одна строка, в ячейке — единственный матч пары (resultsMode
-- при этом не действует). rounds = 2 или пусто — двухкруговой.
local roundsParameter = trim(args.rounds)
 
if roundsParameter ~= "" and
  roundsParameter ~= "1" and
  roundsParameter ~= "2" then
 
return "'''Ошибка:''' параметр ''rounds'' может быть равен " ..
"1 (однокруговой турнир) или 2 (двухкруговой), " ..
"а указано «" .. roundsParameter .. "»."
 
end
 
local singleRound = roundsParameter == "1"
 
 
-- =====================================================
-- ПРОГРЕСС ПО ТУРАМ
-- =====================================================
 
local function buildProgress()
 
local progress = {}
 
-- Собираем список туров
local tours = {}
 
for _, match in ipairs(matches) do
 
local tourNumber = tonumber(match.tour)
 
if tourNumber then
tours[tourNumber] = true
end
 
end
 
local tourNumbers = {}
 
for tourNumber in pairs(tours) do
table.insert(tourNumbers, tourNumber)
end
 
table.sort(tourNumbers)
 
 
-- Таблица, которая последовательно
-- пересчитывается после каждого тура
local currentTeams = {}
 
for _, code in ipairs(teamOrder) do
currentTeams[code] = newTeam(
code,
teams[code].name
)
 
-- Стартовые показатели действуют с первого тура
addStart(currentTeams[code], teams[code].start)
end
 
 
for _, tourNumber in ipairs(tourNumbers) do
 
-- Добавляем матчи данного тура
for _, match in ipairs(matches) do
 
if tonumber(match.tour) == tourNumber then
 
if match.played then
addResult(currentTeams, match)
end
 
-- Снятие очков действует с этого тура
applyDeductions(currentTeams, match)
 
end
 
end
 
 
-- Разница мячей
for _, code in ipairs(teamOrder) do
 
local team = currentTeams[code]
 
team.goalDifference =
team.goalsFor - team.goalsAgainst
 
end
 
 
-- Таблица после этого тура
local tourStandings = {}
 
for _, code in ipairs(teamOrder) do
table.insert(
tourStandings,
currentTeams[code]
)
end
 
table.sort(
tourStandings,
makeComparator(currentTeams, tourNumber)
)
 
 
progress[tourNumber] = {}
 
for position, team in ipairs(tourStandings) do
 
progress[tourNumber][team.code] =
position
 
end
 
end
 
 
return progress, tourNumbers
 
end
 
 
-- =====================================================
-- СОРТИРОВКА
-- =====================================================
 
local standings = {}
 
for _, code in ipairs(teamOrder) do
table.insert(standings, teams[code])
end
 
table.sort(standings, makeComparator(teams, nil))
 
local teamCount = #standings
 
 
-- =====================================================
-- Подсветка
-- =====================================================
 
-- Цвет строк
for _, hl in ipairs(highlights) do
 
local first = math.max(hl.place, 1)
local last = math.min(hl.place + hl.count - 1, teamCount)
 
for position = first, last do
standings[position].highlightColor = hl.color
end
 
end
 
-- Ячейки столбца «Повышение или выбывание».
-- Каждая зона — одна объединённая ячейка с текстом,
-- места между зонами — объединённые пустые ячейки.
if hasHighlightText then
 
local position = 1
local index = 1
 
while position <= teamCount do
 
-- Пропускаем зоны, которые начинаются выше
-- текущей строки (пересекаются с предыдущей)
while highlights[index] and
      highlights[index].place < position do
index = index + 1
end
 
local hl = highlights[index]
local span
local text
 
if hl and hl.place == position then
 
span = math.min(hl.count, teamCount - position + 1)
text = hl.text
index = index + 1
 
else
 
local nextPlace = teamCount + 1
 
if hl then
nextPlace = math.min(hl.place, teamCount + 1)
end
 
span = nextPlace - position
text = ""
 
end
 
standings[position].zoneCell = {
text = text,
span = span
}
 
position = position + span
end
 
end
 
 
-- =====================================================
-- ПРИМЕЧАНИЯ О СНЯТИИ ОЧКОВ
-- =====================================================
 
-- Текст примечания, например:
-- «С команды «Спартак» снято 3 очка в 5-м туре
-- (матч «Спартак» — «Локомотив») за неявку на матч.»
local function deductionText(deduction)
 
local match = deduction.match
 
local text =
"С команды «" .. teams[deduction.team].name ..
"» снято " .. formatNumber(deduction.points) .. " " ..
plural(deduction.points, "очко", "очка", "очков")
 
local tourNumber = tonumber(match.tour)
 
if tourNumber then
text = text .. " в " ..
formatNumber(tourNumber) .. "-м туре"
end
 
text = text ..
" (матч «" .. teams[match.home].name ..
"» — «" .. teams[match.away].name .. "»)"
 
if deduction.reason ~= "" then
text = text .. " " .. deduction.reason
end
 
return text .. "."
end
 
 
-- Сноски нумеруются сверху вниз по итоговой таблице
local notes = {}
 
for _, team in ipairs(standings) do
 
for _, deduction in ipairs(deductions) do
 
if deduction.team == team.code then
 
local label = noteLabel(#notes + 1)
 
local note = {
label = label,
id = idPrefix .. "-note-" .. label,
refId = idPrefix .. "-ref-" .. label,
text = deductionText(deduction)
}
 
deduction.note = note
 
table.insert(notes, note)
table.insert(team.notes, note)
 
end
 
end
 
end
 
 
-- Сноска вида [a]. Сноска в итоговой таблице получает id,
-- чтобы из примечания можно было вернуться к ней (↑).
local function noteMarker(note, withAnchor)
 
local marker =
mw.html.create("sup")
:addClass("reference")
:addClass("football-note-ref")
:wikitext(
"[[#" .. note.id .. "|&#91;" ..
note.label .. "&#93;]]"
)
 
if withAnchor then
marker:attr("id", note.refId)
end
 
return marker
end
 
 
-- =====================================================
-- ТАБЛИЦА ЧЕМПИОНАТА
-- =====================================================
 
local root =
mw.html.create("div")
:addClass("football-tables")
 
 
local standingsTable =
mw.html.create("table")
:addClass("wikitable")
:addClass("football-standings")
:css("text-align", "center")
 
 
-- Заголовок
 
local standingsHeader =
mw.html.create("tr")
 
local headings = {
"Место",
"Команда",
"И",
"В",
"Н",
"П",
"МЗ",
"МП",
"РМ",
"О"
}
 
for _, title in ipairs(headings) do
 
standingsHeader:node(
cell("th", title)
)
 
end
 
-- Столбцы карточек выводятся, только если
-- карточки где-нибудь указаны
if hasCards then
 
for _, cardType in ipairs(CARD_TYPES) do
 
standingsHeader:node(
mw.html.create("th")
:addClass("football-cards")
:attr("title", cardType.title)
:node(cardIcon(cardType))
)
 
end
 
end
 
if hasHighlightText then
 
standingsHeader:node(
cell("th", "Повышение или выбывание")
)
 
end
 
standingsTable:node(standingsHeader)
 
 
-- Строки
 
for position, team in ipairs(standings) do
 
local row =
mw.html.create("tr")
 
if team.highlightColor and team.highlightColor ~= "" then
row:addClass("highlight"):addClass(team.highlightColor)
end
 
row:node(
cell("td", position + placeOffset)
)
 
 
-- Название и сноски о снятии очков
 
local teamName =
cell("td", team.name)
:css("text-align", "left")
 
for _, note in ipairs(team.notes) do
teamName:node(
noteMarker(note, true)
)
end
 
row:node(
teamName
)
 
 
row:node(
cell("td", team.played)
)
 
row:node(
cell("td", team.wins)
)
 
row:node(
cell("td", team.draws)
)
 
row:node(
cell("td", team.losses)
)
 
row:node(
cell("td", team.goalsFor)
)
 
row:node(
cell("td", team.goalsAgainst)
)
 
 
local gd
 
if team.goalDifference > 0 then
gd = "+" .. team.goalDifference
else
gd = tostring(team.goalDifference)
end
 
row:node(
cell("td", gd)
)
 
 
local pointsCell =
cell("td", formatNumber(team.points))
 
if team.deducted > 0 then
 
pointsCell:attr(
"title",
"Снято " .. formatNumber(team.deducted) .. " " ..
plural(team.deducted, "очко", "очка", "очков")
)
 
end
 
row:node(
pointsCell
)
 
 
if hasCards then
 
for _, cardType in ipairs(CARD_TYPES) do
 
row:node(
cell("td", team[cardType.key], "football-cards")
)
 
end
 
end
 
 
if hasHighlightText and team.zoneCell then
 
local zone =
cell("td", team.zoneCell.text)
 
if team.zoneCell.span > 1 then
zone:attr("rowspan", team.zoneCell.span)
end
 
row:node(
zone
)
 
end
 
 
standingsTable:node(row)
 
end
 
 
root:node(standingsTable)
 
 
-- =====================================================
-- ИНФОРМАЦИЯ О КРИТЕРИЯХ
-- =====================================================
 
local criteriaText = table.concat(criteria, " → ")
 
local info =
mw.html.create("div")
:addClass("football-info")
:wikitext(
"Критерии определения мест: " ..
criteriaText
)
 
root:node(info)
 
 
-- =====================================================
-- ПРИМЕЧАНИЯ ПОД ТАБЛИЦЕЙ
-- =====================================================


    -- -----------------------------------------------------
if #notes > 0 then
    -- Настройки начисления очков
    -- -----------------------------------------------------


    local pointsWin = tonumber(args.points_win) or 3
local notesBlock =
    local pointsDraw = tonumber(args.points_draw) or 1
mw.html.create("div")
    local pointsLoss = tonumber(args.points_loss) or 0
:addClass("football-info")
:addClass("football-notes")
:wikitext("Примечания:")


local list =
notesBlock:tag("ol")
:css("list-style-type", "lower-alpha")


    -- -----------------------------------------------------
for _, note in ipairs(notes) do
    -- Список команд
    -- -----------------------------------------------------


    local teamCodes = {}
local item =
list:tag("li")
:attr("id", note.id)


    if args.teams then
item:tag("span")
        local list = split(args.teams, ';')
:addClass("mw-cite-backlink")
:wikitext("[[#" .. note.refId .. "|↑]]")


        for _, code in ipairs(list) do
item:wikitext(" ")
            code = trim(code)


            if code ~= '' then
item:tag("span")
                table.insert(teamCodes, code)
:addClass("reference-text")
            end
:wikitext(note.text)
        end
    end


end


    -- -----------------------------------------------------
root:node(notesBlock)
    -- Создаём данные команд
    -- -----------------------------------------------------


    local teams = {}
end


    for _, code in ipairs(teamCodes) do


        local name = args['name_' .. code]
-- =====================================================
-- РЕЗУЛЬТАТЫ
-- =====================================================


        if not name or trim(name) == '' then
root:wikitext(heading("Результаты"))
            name = code
        end


        teams[code] = newTeam(code, name)
    end


local resultsTable =
mw.html.create("table")
:addClass("wikitable")
:addClass("football-results")
:css("text-align", "center")


    -- -----------------------------------------------------
    -- Список матчей
    -- -----------------------------------------------------


    local matches = {}
-- Заголовок


    local matchNumber = 1
local header =
mw.html.create("tr")


    while true do
header:node(
cell("th", "", "football-corner")
)


        local value = args['match' .. matchNumber]
for _, code in ipairs(teamOrder) do


        if not value then
header:node(
            break
cell(
        end
"th",
teams[code].name,
"football-team-header"
)
)


        value = trim(value)
end


        if value ~= '' then
resultsTable:node(header)


            local parts = split(value, ';')


            if #parts >= 3 then
-- Матрица результатов


                local home = trim(parts[1])
-- Матч для ячейки шахматки.
                local away = trim(parts[2])
-- eitherVenue = false — только матч на поле homeCode,
                local score = trim(parts[3])
-- true — матч этой пары на любом поле (однокруговой турнир).
-- Если подходящих матчей несколько, берётся первый сыгранный,
-- если сыгранных нет — первый с пометкой («перенесён» и т. п.),
-- иначе просто первый.
local function findMatch(homeCode, awayCode, eitherVenue)


                local parsedScore = parseScore(score)
local found
local foundRank = 0


                table.insert(matches, {
for _, match in ipairs(matches) do
                    number = matchNumber,
                    home = home,
                    away = away,
                    score = parsedScore
                })


                -- -------------------------------------------------
local suits =
                -- Рассчитываем статистику
(match.home == homeCode and match.away == awayCode) or
                -- -------------------------------------------------
(eitherVenue and
match.home == awayCode and match.away == homeCode)


                if parsedScore and parsedScore.played then
if suits then


                    -- Если обе команды существуют
local rank = 1
                    if teams[home] and teams[away] then


                        local h = teams[home]
if match.played then
                        local a = teams[away]
rank = 3
elseif match.status ~= "" then
rank = 2
end


                        h.played = h.played + 1
if rank > foundRank then
                        a.played = a.played + 1
found = match
foundRank = rank
end


                        h.goalsFor = h.goalsFor + parsedScore.home
end
                        h.goalsAgainst = h.goalsAgainst + parsedScore.away


                        a.goalsFor = a.goalsFor + parsedScore.away
end
                        a.goalsAgainst = a.goalsAgainst + parsedScore.home


return found
end


                        -- Победа хозяев
                        if parsedScore.home > parsedScore.away then


                            h.wins = h.wins + 1
-- Ячейка шахматки для команды строки rowCode.
                            a.losses = a.losses + 1
-- Счёт записан как «хозяева:гости»,
-- а цвет показывает результат команды строки.
local function makeResultCell(match, rowCode)


                            h.points = h.points + pointsWin
local resultCell =
                            a.points = a.points + pointsLoss
mw.html.create("td")


if not match then
return resultCell:wikitext("—")
end


                        -- Победа гостей
if match.played then
                        elseif parsedScore.home < parsedScore.away then


                            a.wins = a.wins + 1
local scored = match.homeGoals
                            h.losses = h.losses + 1
local conceded = match.awayGoals


                            a.points = a.points + pointsWin
if match.away == rowCode then
                            h.points = h.points + pointsLoss
scored = match.awayGoals
conceded = match.homeGoals
end


if scored > conceded then
resultCell
:addClass("ui")
:addClass("green")
elseif scored < conceded then
resultCell
:addClass("ui")
:addClass("red")
else
resultCell
:addClass("ui")
:addClass("yellow")
end


                        -- Ничья
resultCell:wikitext(match.score)
                        else


                            h.draws = h.draws + 1
elseif match.status ~= "" then
                            a.draws = a.draws + 1
resultCell:wikitext(match.status)
else
resultCell:wikitext("—")
end


                            h.points = h.points + pointsDraw
return resultCell
                            a.points = a.points + pointsDraw
end


                        end
                    end
                end
            end
        end


        matchNumber = matchNumber + 1
-- Строк у команды: в двухкруговом турнире две
    end
-- (дома и в гостях или 1-й и 2-й круг), в однокруговом — одна
local rowsPerTeam = 2


if singleRound then
rowsPerTeam = 1
end


    -- -----------------------------------------------------
    -- Разница мячей
    -- -----------------------------------------------------


    for _, code in ipairs(teamCodes) do
for _, homeCode in ipairs(teamOrder) do


        local t = teams[code]
local homeRow =
mw.html.create("tr")


        t.goalDifference =
local awayRow =
            t.goalsFor - t.goalsAgainst
mw.html.create("tr")


    end
local teamHeader =
cell(
"th",
teams[homeCode].name,
"football-team-header"
)


if rowsPerTeam > 1 then
teamHeader:attr("rowspan", rowsPerTeam)
end


    -- -----------------------------------------------------
teamHeader:css("text-align", "right")
    -- Сортировка турнирной таблицы
    --
    -- 1. Очки
    -- 2. Разница мячей
    -- 3. Забитые мячи
    -- 4. Название команды
    -- -----------------------------------------------------


    local standings = {}
homeRow:node(teamHeader)


    for _, code in ipairs(teamCodes) do
        table.insert(standings, teams[code])
    end


for _, awayCode in ipairs(teamOrder) do


    table.sort(standings, function(a, b)
if homeCode == awayCode then


        if a.points ~= b.points then
local diagonal = cell("td", "—", "football-diagonal")
            return a.points > b.points
        end


        if a.goalDifference ~= b.goalDifference then
if rowsPerTeam > 1 then
            return a.goalDifference > b.goalDifference
diagonal:attr("rowspan", rowsPerTeam)
        end
end


        if a.goalsFor ~= b.goalsFor then
homeRow:node(
            return a.goalsFor > b.goalsFor
diagonal
        end
)


        return a.name < b.name
elseif singleRound then


    end)
-- Единственный матч пары, на чьём бы поле он ни был
homeRow:node(
makeResultCell(
findMatch(homeCode, awayCode, true),
homeCode
)
)


else


    -- -----------------------------------------------------
local homeMatch = findMatch(homeCode, awayCode, false)
    -- HTML
local awayMatch = findMatch(awayCode, homeCode, false)
    -- -----------------------------------------------------


    local root = mw.html.create('div')
local homeResultCell = makeResultCell(homeMatch, homeCode)
        :addClass('football-tables')
local awayResultCell = makeResultCell(awayMatch, homeCode)


-- Нужно ли поменять местами гостевую и домашнюю игры:
-- в режиме по кругам первая строка — матч 1-го круга,
-- вторая — матч 2-го круга
local changeOrder =
resultsMode == 1 and (
(homeMatch ~= nil and homeMatch.round == "2") or
(awayMatch ~= nil and awayMatch.round == "1")
)


    -- =====================================================
if changeOrder then
    -- ТАБЛИЦА РЕЗУЛЬТАТОВ
homeRow:node(awayResultCell)
    -- =====================================================
awayRow:node(homeResultCell)
else
homeRow:node(homeResultCell)
awayRow:node(awayResultCell)
end


    root
end
        :tag('h3')
        :wikitext('Результаты')
        :done()


end


    local resultsTable = root
resultsTable:node(homeRow)
        :tag('table')
        :addClass('wikitable')
        :addClass('football-results')


if rowsPerTeam > 1 then
resultsTable:node(awayRow)
end


    -- Заголовок
end


    local header = resultsTable:tag('tr')


    header
root:node(resultsTable)
        :tag('th')
        :wikitext('Команда')
        :done()




    for _, code in ipairs(teamCodes) do
-- =====================================================
-- ПРОГРЕСС ПО ТУРАМ
-- =====================================================


        header
local showProgress = trim(args.show_progress) == "yes"
            :tag('th')
            :wikitext(teams[code].name)
            :done()


    end
local progress
local tourNumbers


if showProgress then


    -- Строки команд
progress, tourNumbers =
buildProgress()


    for _, homeCode in ipairs(teamCodes) do


        local row = resultsTable:tag('tr')
root:wikitext(heading("Положение команд по турам"))


        row
            :tag('th')
            :wikitext(teams[homeCode].name)
            :done()


-- Без номеров туров строить нечего
if #tourNumbers == 0 then


        for _, awayCode in ipairs(teamCodes) do
root:node(
mw.html.create("div")
:addClass("football-info")
:wikitext(
"''Нет матчей с номером тура. " ..
"Для этой таблицы матчи нужно указывать " ..
"в формате «тур;круг;хозяева;гости;счёт».''"
)
)


            local cell = row:tag('td')
showProgress = false


            -- Диагональ
end


            if homeCode == awayCode then
end


                cell
                    :css('background-color', '#eaecf0')
                    :css('text-align', 'center')
                    :wikitext('—')


            else
if showProgress then


                -- Ищем матч
local progressTable =
                local found = nil
mw.html.create("table")
:addClass("wikitable")
:addClass("football-progress")
:css("text-align", "center")


                for _, match in ipairs(matches) do


                    if match.home == homeCode
-- -------------------------------------------------
                        and match.away == awayCode then
-- Цвета мест
-- -------------------------------------------------


                        found = match
local progressHighlights = {}
                        break


                    elseif match.home == awayCode
for i = 1, 15 do
                        and match.away == homeCode then


                        found = match
local value =
                        break
trim(args["progress_highlight" .. i])


                    end
if value ~= "" then
                end


local parts =
split(value, ";")


                if found and found.score then
local place =
tonumber(parts[1])


                    local score = found.score
local color =
parts[2] or ""


                    if score.played then
if place and color ~= "" then


                        local displayedScore
progressHighlights[place] =
color


                        if found.home == homeCode then
end


                            displayedScore =
end
                                score.home .. ':' .. score.away


                        else
end


                            displayedScore =
                                score.away .. ':' .. score.home


                        end
-- -------------------------------------------------
-- Сноски о снятии очков: тур → команда → сноски
-- -------------------------------------------------


                        cell
local progressNotes = {}
                            :css('text-align', 'center')
                            :wikitext(displayedScore)


                    elseif score.status ~= '' then
for _, deduction in ipairs(deductions) do


                        cell
local tourNumber = tonumber(deduction.match.tour)
                            :css('text-align', 'center')
                            :css('color', '#54595d')
                            :wikitext(score.status)


                    else
if tourNumber and deduction.note then


                        cell
progressNotes[tourNumber] =
                            :css('text-align', 'center')
progressNotes[tourNumber] or {}
                            :wikitext('—')


                    end
local teamNotes =
progressNotes[tourNumber][deduction.team] or {}


                else
table.insert(teamNotes, deduction.note)


                    cell
progressNotes[tourNumber][deduction.team] =
                        :css('text-align', 'center')
teamNotes
                        :wikitext('—')


                end
end
            end
        end
    end


end


    -- =====================================================
    -- ТУРНИРНАЯ ТАБЛИЦА
    -- =====================================================


    root
-- -------------------------------------------------
        :tag('h3')
-- Заголовок
        :wikitext('Турнирная таблица')
-- -------------------------------------------------
        :done()


local progressHeader =
mw.html.create("tr")


    local standingsTable = root
progressHeader:node(
        :tag('table')
cell("th", "Команда")
        :addClass('wikitable')
:attr("rowspan", 2)
        :addClass('football-standings')
)


progressHeader:node(
cell("th", "Тур")
:attr("colspan", #tourNumbers)
)


    -- Заголовок
progressTable:node(progressHeader)


    local sHeader = standingsTable:tag('tr')
local tourHeader =
mw.html.create("tr")


    local headers = {
for _, tourNumber in ipairs(tourNumbers) do
        'Место',
        'Команда',
        'И',
        'В',
        'Н',
        'П',
        'Мячи',
        '±',
        'О'
    }


tourHeader:node(
cell(
"th",
tourNumber
)
)


    for _, text in ipairs(headers) do
end


        sHeader
progressTable:node(tourHeader)
            :tag('th')
            :wikitext(text)
            :done()


    end


-- -------------------------------------------------
-- Строки команд
-- -------------------------------------------------


    -- Строки
for _, code in ipairs(teamOrder) do


    for place, team in ipairs(standings) do
local row =
mw.html.create("tr")


        local row = standingsTable:tag('tr')


row:node(
cell(
"th",
teams[code].name
)
)


        row
            :tag('td')
            :css('text-align', 'center')
            :wikitext(place)
            :done()


for _, tourNumber in ipairs(tourNumbers) do


        row
-- Место с учётом first_place
            :tag('th')
local position =
            :wikitext(team.name)
progress[tourNumber][code] + placeOffset
            :done()


local resultCell =
cell("td", position)


        row
            :tag('td')
            :css('text-align', 'center')
            :wikitext(team.played)
            :done()


local color =
progressHighlights[position]


        row
if color and color ~= "" then
            :tag('td')
            :css('text-align', 'center')
            :wikitext(team.wins)
            :done()


resultCell
:addClass("highlight")
:addClass(color)


        row
end
            :tag('td')
            :css('text-align', 'center')
            :wikitext(team.draws)
            :done()




        row
-- В туре, где сняты очки, — та же сноска,
            :tag('td')
-- что и в итоговой таблице
            :css('text-align', 'center')
local cellNotes =
            :wikitext(team.losses)
progressNotes[tourNumber] and
            :done()
progressNotes[tourNumber][code]


if cellNotes then


        row
resultCell:addClass("football-deduction")
            :tag('td')
            :css('text-align', 'center')
            :wikitext(
                team.goalsFor .. ':' .. team.goalsAgainst
            )
            :done()


for _, note in ipairs(cellNotes) do
resultCell:node(
noteMarker(note, false)
)
end


        local gd = team.goalDifference
end


        local gdText


        if gd > 0 then
row:node(resultCell)
            gdText = '+' .. gd
        else
            gdText = tostring(gd)
        end


end


        row
            :tag('td')
            :css('text-align', 'center')
            :wikitext(gdText)
            :done()


progressTable:node(row)


        row
end
            :tag('td')
            :css('text-align', 'center')
            :wikitext(team.points)
            :done()


    end


root:node(progressTable)


    return tostring(root)
end


return tostring(root)
end
end




return p
return p