Модуль:Sports table/sub
Материал из вики «Вече»
Дополнительные действия
Для документации этого модуля может быть создана страница Модуль:Sports table/sub/doc
-- Общий подмодуль семейства спортивных модулей:
-- данные и функции, которыми пользуются сразу несколько модулей семейства.
-- Содержит палитру и разбор значений цвета; разбор логических значений, названий команд и флагов,
-- счёта и дат; идентификаторы матчей и ссылки со счёта на отчёты; сверку с реестром сокращений и
-- подсказки о статьях про дерби; сообщения предпросмотра и правила отслеживающих категорий;
-- поиск ручного оформления; подбор ширин столбцов и перенос текста в столбце результатов;
-- общие части турнирной таблицы — шапку, числа в ячейках, сноски, обозначения статуса;
-- генератор блока CSS палитры для страниц стилей.
-- Подключается из [[Модуль:Sports table]], [[Модуль:Sports results]], [[Модуль:Sports group]], из
-- подмодулей стиля турнирной таблицы, из [[Модуль:Sports table/argcheck]],
-- [[Модуль:Sports table/auto]], [[Модуль:Sports table/totalscheck]],
-- [[Модуль:Sports table/source]], из [[Модуль:Football box/base]] (через него — из карточек матча)
-- и из [[Модуль:Football event]]. Через #invoke вызывается только print_styles.
-- Документация — [[Модуль:Sports table/doc]] и [[Модуль:Sports results/doc]];
-- о палитре и таблицах стилей — раздел «Палитра и таблицы стилей» первой из них.
local ppp = {}
-- Генератор случайных чисел засевается при загрузке подмодуля. Из него вызывающие модули берут
-- значение rand_val (math.random()), которое дописывают к идентификаторам сносок: без этого сноски
-- двух таблиц на одной странице получили бы одинаковые идентификаторы и слились бы.
-- require загружает модуль один раз за вызов, поэтому засев срабатывает однажды,
-- сколько бы модулей семейства в вызове ни подключали подмодуль.
math.randomseed(os.time() + (os.clock() * 10 ^ 6))
-- Служебные функции ---------------------------------------------------------------------
-- Возвращает обёртку функции одного аргумента, которая запоминает её результаты — все возвращаемые значения —
-- на время вызова. Ею обёрнуты разбор цветов, флагов, счёта и дат и построение якорей:
-- одно и то же значение за вызов разбирается многократно — для каждой встречи команды, для ширины столбца,
-- для якорей, — а разбор идёт через mw.ustring, который заметно медленнее встроенных строковых функций.
-- Аргумент nil не запоминается: ключом таблицы он быть не может.
local function memoize(fn)
local cache = {}
return function(s)
if s == nil then
return fn(s)
end
local hit = cache[s]
if hit == nil then
hit = {fn(s)}
cache[s] = hit
end
return unpack(hit)
end
end
-- Обрезает по краям пробелы и прямые кавычки;
-- возвращает nil, если после обрезки ничего не осталось.
local function trim(s)
if s == nil or s:match('^%s*(.-)%s*$') == '' then
return nil
end
s = s:match('^[\'"%s]*(.-)[\'"%s]*$')
return (s ~= '') and s or nil
end
-- Та же обрезка для других модулей:
-- [[Модуль:Football box/base]] обрезает ею значения карточек матча.
ppp.trim = trim
-- Палитра и цвета -----------------------------------------------------------------------
-- Палитра фоновых цветов, общая для семейства: ею заливаются строки турнирной таблицы,
-- клетки шахматки и карточки матча. Цифра в имени — уровень оттенка: 1 — самый насыщенный,
-- 4 — самый светлый. white1 и пустое имя дают «inherit», то есть отсутствие собственной заливки.
-- Значения для тёмной темы — в ppp.colors_dark.
ppp.colors = {
gold1 = '#f7f6a8', silver1 = '#dce5e5', bronze1 = '#ffdab9',
green1 = '#bbf3bb', green2 = '#ccf9cc', green3 = '#ddfcdd', green4 = '#eeffee',
blue1 = '#bbf3ff', blue2 = '#ccf9ff', blue3 = '#ddfcff', blue4 = '#eeffff',
yellow1 = '#ffffbb', yellow2 = '#ffffcc', yellow3 = '#ffffdd', yellow4 = '#ffffee',
violet1 = '#bbbbff', violet2 = '#ccccff', violet3 = '#ddddff', violet4 = '#eeeeff',
red1 = '#ffbbbb', red2 = '#ffcccc', red3 = '#ffdddd', red4 = '#ffeeee',
black1 = '#bbbbbb', black2 = '#cccccc', black3 = '#dddddd', black4 = '#eeeeee',
orange1 = '#feead5', orange2 = '#fff5e8',
white1 = 'inherit',
[''] = 'inherit',
-- Два цвета вне четырёхуровневой схемы: они приняты в рувики как стандартные
accent = '#eaf3ff', -- голубой для выделения карточки
neutral = '#f8f9fa'}
-- Палитра для тёмной темы: имена те же, что в ppp.colors, тон тот же, светлота понижена.
-- Светлая заливка на тёмном фоне была бы самым светлым пятном на странице — ярче текста;
-- с пониженной светлотой смысл цвета читается тот же, а заливка не слепит.
-- Верхняя граница выбрана по ссылке: выше неё контраст ссылки с заливкой падает ниже 4,5.
-- Сам модуль эти значения не выводит: они нужны таблицам стилей, а страница стилей не проходит
-- через вики-разметку. Туда их переносит блок CSS от ppp.print_styles, а здесь они записаны,
-- чтобы у палитры был один источник.
ppp.colors_dark = {
gold1 = '#4c3d00', silver1 = '#3f3f3f', bronze1 = '#5c361f',
green1 = '#194319', green2 = '#153715', green3 = '#112c11', green4 = '#0d210d',
blue1 = '#193b43', blue2 = '#153337', blue3 = '#112a2c', blue4 = '#0d2121',
yellow1 = '#414118', yellow2 = '#373715', yellow3 = '#2c2c11', yellow4 = '#21210d',
violet1 = '#191943', violet2 = '#151537', violet3 = '#11112c', violet4 = '#0d0d21',
red1 = '#431919', red2 = '#371515', red3 = '#2c1111', red4 = '#210d0d',
black1 = '#2e2e2e', black2 = '#262626', black3 = '#1f1f1f', black4 = '#171717',
orange1 = '#432e19', orange2 = '#342614',
white1 = 'inherit',
[''] = 'inherit',
-- Цвета вне четырёхуровневой схемы — см. ppp.colors.
accent = '#192b43', -- голубой для выделения карточки
neutral = '#222425'} -- нейтральный серый оформления MediaWiki
-- Цвет текста, который вызывающие модули задают вместе с любым непрозрачным фоном из ppp.colors,
-- выведенным встроенным стилем. В тёмной теме встроенный стиль не меняется: фон остался бы светлым,
-- а унаследованный цвет текста посветлел бы — светлый текст на светлом фоне.
-- Значение — стандартный цвет основного текста Википедии (см. [[ВП:Цветовой контраст]]).
ppp.text_color = '#202122'
-- Цвет полосы между группами позиций (splitN в [[Модуль:Sports table]]) — для светлой и тёмной темы.
-- Это цвет разделительных линий оформления MediaWiki (border-color-subtle): полоса светлее рамки таблицы,
-- но видна и на заливке строк, и на фоне таблицы. Серые палитры для неё не годятся:
-- они подобраны как заливка и в тёмной теме почти сливаются с фоном. Выводит его ppp.print_styles.
ppp.split_colors = {light = '#c8ccd1', dark = '#54595d'}
-- Цвет линии между кругами в шахматке ([[Модуль:Sports results]]). Это стандартный цвет рамок таблиц MediaWiki —
-- по той же причине, по которой в палитре есть accent и neutral: линии семейства не отличаются от линий остального оформления.
ppp.border_color = '#a2a9b1'
-- Имена палитры, которым назначен смысл в оформлении таблиц и карточек матча.
-- Для них ppp.palette_color возвращает происхождение «named», для всех прочих значений —
-- «custom», и такие страницы вызывающие модули заносят в отслеживающую категорию.
ppp.assigned_colors = {
gold1 = true, silver1 = true, bronze1 = true,
green1 = true, green2 = true, green3 = true, green4 = true,
blue1 = true, blue2 = true, blue3 = true, blue4 = true,
yellow1 = true, yellow2 = true, yellow3 = true, yellow4 = true,
violet1 = true, violet2 = true, violet3 = true, violet4 = true,
red1 = true, red2 = true, red3 = true, red4 = true,
black1 = true, black2 = true, black3 = true, black4 = true,
orange1 = true, orange2 = true,
white1 = true, accent = true, neutral = true
}
-- Имена оттенков без номера уровня: «gold» читается как «gold1». Так пишут по привычке к названиям цветов CSS,
-- и разумнее понять это как первый уровень оттенка, чем выводить ярко-жёлтый #ffd700,
-- на котором текст почти не читается. Происхождение у такого значения — «custom».
local bare_hues = {
green = 'green1', blue = 'blue1', yellow = 'yellow1', red = 'red1',
black = 'black1', gold = 'gold1', violet = 'violet1', orange = 'orange1',
silver = 'silver1', bronze = 'bronze1', white = 'white1'
}
-- Разбирает значение цвета без учёта регистра и пробелов по краям.
-- Возвращает цвет и его происхождение:
-- цвет, «named» — имя из ppp.assigned_colors; так и следует писать;
-- цвет, «custom» — имя оттенка без номера (bare_hues), шестнадцатеричный код из трёх или шести
-- цифр с решёткой или без неё, слово из латинских букв (название цвета CSS);
-- nil, «custom» — значение задано, но цветом не является;
-- nil, nil — значение не задано или пустое.
-- Всё, что может быть цветом, применяется, а не отбрасывается: в англовики шестнадцатеричный код —
-- документированный способ задать фон, разметку оттуда постоянно копируют, и отказ означал бы
-- пропавшую заливку на тысячах страниц. Вместо этого вызывающие модули заносят в отслеживающую
-- категорию всё, кроме «named». Название цвета CSS пропускается как есть:
-- свойство и место подстановки задаёт модуль, а недопустимое значение браузер просто не применит.
function ppp.palette_color(name)
if name == nil then
return nil, nil
end
local key = mw.ustring.lower(mw.ustring.gsub(tostring(name), '^%s*(.-)%s*$', '%1'))
if key == '' then
return nil, nil
end
if ppp.assigned_colors[key] then
return ppp.colors[key], 'named'
end
local bare = bare_hues[key]
if bare then
return ppp.colors[bare], 'custom'
end
-- Шестнадцатеричный цвет, записанный с решёткой или без неё.
local hex = mw.ustring.match(key, '^#?(%x%x%x)$') or mw.ustring.match(key, '^#?(%x%x%x%x%x%x)$')
if hex then
return '#' .. hex, 'custom'
end
-- Название цвета CSS: только латинские буквы.
if mw.ustring.match(key, '^[a-z]+$') then
return key, 'custom'
end
-- Значение задано, но цветом не является: опечатка в имени палитры, посторонний текст.
-- Заливки не будет, но происхождение возвращается —
-- иначе такой параметр не оставлял бы следа, и найти его было бы нечем.
return nil, 'custom'
end
ppp.palette_color = memoize(ppp.palette_color)
-- Логические значения -------------------------------------------------------------------
-- Канонические написания логических параметров: yes и no, принятые в вики-разметке, и да и нет,
-- естественные для русского раздела; регистр не важен (см. ppp.is_canonical_boolean).
-- [[Модуль:Yesno]] понимает и другие варианты — y/n, true/false, д/н, +/− и 1/0, — и разметка с
-- ними работает, но вызывающие модули собирают их, как и нераспознанные значения, в отслеживающую
-- категорию: по её наполнению видно, что стоит привести к единому виду, а что действительно сломано.
-- Набор общий для таблиц и карточек матча ([[Модуль:Sports table/argcheck]],
-- [[Модуль:Football box/base]]): значение вида «aet=1» отмечается одинаково,
-- в каком бы модуле оно ни встретилось.
ppp.canonical_boolean = {
['yes'] = true, ['no'] = true,
['да'] = true, ['нет'] = true
}
-- Возвращает true, если значение записано в каноническом виде. nil каноническим не считается;
-- пустые значения вызывающие модули отсеивают сами.
function ppp.is_canonical_boolean(v)
return v ~= nil and ppp.canonical_boolean[mw.ustring.lower(tostring(v))] == true
end
-- Предпросмотр, отслеживающие категории, страница вызова --------------------------------
-- Ответ ppp.in_preview, запомненный на время вызова.
local preview_state
-- Возвращает true, если страница показывается в предпросмотре: у несохранённой правки
-- ещё нет номера версии. Ответ в пределах вызова не меняется, поэтому вычисляется один раз.
function ppp.in_preview(frame)
if preview_state == nil then
preview_state = frame:preprocess('{{REVISIONID}}') == ''
end
return preview_state
end
-- Строит сообщение для предпросмотра; показывать ли его, решает вызывающий модуль
-- (см. ppp.in_preview). Все модули семейства выводят сообщения через эту функцию,
-- чтобы их вид не расходился. Уровень level:
-- 'error' — красное «Предупреждение: …», точно установленная проблема: опечатка в имени параметра, ссылка на необъявленную команду;
-- 'info' — серое сообщение без приставки, сведения, а не неполадка;
-- иной — обычно 'warning': зелёное «Возможная проблема: …», эвристика, которая может ошибаться:
-- расхождение сумм, подозрительная корректировка очков.
-- При inline сообщение выводится элементом span, а не div — для значков внутри строки текста
-- ([[Модуль:Football event]]), где блок разорвал бы строку.
function ppp.preview_note(level, text, inline)
local tag = inline and 'span' or 'div'
if level == 'info' then
return tostring(mw.html.create(tag)
:css('color', '#54595d')
:wikitext(text))
end
local is_error = level == 'error'
return tostring(mw.html.create(tag)
:css('color', is_error and 'red' or 'green')
:wikitext((is_error and 'Предупреждение: ' or 'Возможная проблема: ') .. text))
end
-- Переводит ссылку на отслеживающую категорию в текст для предпросмотра:
-- «[[Категория:Страницы, использующие спортивные таблицы с неизвестными параметрами|foo]]»
-- становится «неизвестными параметрами = foo». Сама ссылка на категорию в тексте невидима,
-- и без перевода сообщение осталось бы пустым. Строка, которая не является ссылкой на категорию,
-- возвращается как есть.
function ppp.warning_text(link)
local name, key = mw.ustring.match(link, '^%[%[Категория:([^|%]]*)|?(.-)%]%]$')
if not name then
return link
end
name = mw.ustring.gsub(name, '^Страницы, использующие спортивные таблицы с ', '')
name = mw.ustring.gsub(name, '^Страницы с ', '')
return key ~= '' and name .. ' = ' .. key or name
end
-- Окончания имён песочниц и тестовых страниц, русские и английские.
local excluded_subpages = {'/песочница', '/тесты', '/sandbox', '/testcases'}
-- Возвращает true, если на странице title (по умолчанию — текущей) отслеживающие категории
-- не ставятся: в личном пространстве, в [[Википедия:Песочница]] с её подстраницами и на подстраницах
-- из excluded_subpages проверяют разметку, а не ведут данные, и категории там засоряли бы списки
-- для исправления. Правило одно для всего семейства — таблиц, шахматок, карточек матча и значков событий.
function ppp.tracking_excluded(title)
title = title or mw.title.getCurrentTitle()
if title.namespace == 2 then
return true
end
local name = title.fullText or ''
if name == 'Википедия:Песочница' or name:find('^Википедия:Песочница/') then
return true
end
for _, sub in ipairs(excluded_subpages) do
if name:sub(-#sub) == sub then
return true
end
end
return false
end
-- Возвращает отслеживающую категорию для статьи, где модуль вызван через #invoke напрямую, минуя шаблон, либо пустую строку.
-- В рувики так делать не рекомендуется: при изменении набора параметров править приходится каждую статью, а не один шаблон.
-- Категория собирает такие статьи для перевода на шаблоны; ключ сортировки — имя модуля. Учитываются только статьи.
function ppp.direct_invoke_category(frame)
local current = mw.title.getCurrentTitle()
if current.namespace ~= 0 then
return ''
end
local parent = frame:getParent()
if not parent or parent:getTitle() ~= current.fullText then
return ''
end
local module_name = mw.ustring.gsub(frame:getTitle() or '', '^Модуль:', '')
return '[[Категория:Статьи с прямым вызовом спортивных модулей|' .. module_name .. ']]'
end
-- Возвращает true на странице, где лежат данные таблицы: на странице шаблона, на его подстранице
-- /doc и в статье, где модуль вызван напрямую. Проверки по реестру сокращений (сверка сокращений,
-- подсказки о статьях про дерби) выполняются только там:
-- статьи, подключающие шаблон, реестр не загружают, а исправлять найденное нужно в шаблоне.
function ppp.data_page(frame)
local parent = frame:getParent()
local current = mw.title.getCurrentTitle().fullText
return parent ~= nil and (parent:getTitle() == current or parent:getTitle() .. '/doc' == current)
end
-- Возвращает true, если вызов относится к другому разделу страницы. Шаблон с несколькими таблицами
-- можно подключить частично, указав при подключении transcludesection (или section); вызов с другим
-- значением section тогда не выводится. Проверка общая для турнирных таблиц, шахматок и карточек матча.
function ppp.skip_section(frame)
local parent = frame:getParent()
local tsection = parent and (parent.args['transcludesection'] or parent.args['section']) or ''
local bsection = frame.args['section'] or ''
return tsection ~= '' and bsection ~= '' and tsection ~= bsection
end
-- Ручное оформление ---------------------------------------------------------------------
-- Приёмы ручного оформления: название для сообщения и паттерн. Значения приходят к модулю уже развёрнутыми,
-- поэтому ищутся не имена шаблонов, а то, во что они разворачиваются: {{nobr}} даёт класс nowrap или white-space,
-- {{small}} — тег small. Одним поиском покрываются все написания сразу. Разметка обычных шаблонов —
-- флаги, сноски, надстрочники — под эти паттерны не попадает.
local manual_marks = {
{'запрет переноса', 'white%-space%s*:%s*nowrap'},
{'запрет переноса', 'class%s*=%s*"[^"]*nowrap'},
{'запрет переноса', "class%s*=%s*'[^']*nowrap"},
{'размер шрифта', '<%s*[Ss]mall[%s>]'},
{'размер шрифта', '<%s*[Bb]ig[%s>]'},
{'размер шрифта', 'font%-size%s*:'},
{'ширина', '[^%-%w]width%s*:'},
{'ширина', 'width%s*=%s*"%d'},
{'отступы', 'padding%s*:'},
{'выравнивание', 'text%-align%s*:'},
{'перенос строки', '<%s*[Bb][Rr][%s/>]'}
}
-- Распознаёт ручное оформление в значении параметра: запрет переноса, размер шрифта, ширину, отступы,
-- выравнивание, принудительный перенос строки. Возвращает название первого найденного приёма
-- (см. manual_marks) или nil. При allow_breaks перенос строки не отмечается —
-- для полей-списков, где он разделяет записи.
-- Оформление семейства рассчитано на узкие экраны: ширины столбцов вычисляются, перенос разрешается там,
-- где он нужен, размер шрифта задан классами. Ручная вставка этому мешает: запрет переноса в длинной строке
-- выталкивает её за край экрана, а уменьшение шрифта поверх уже уменьшенного нарушает [[ВП:РТ]].
-- Принудительный перенос ломает строку в заданном месте при любой ширине экрана:
-- на широком оставляет пустоту справа, на узком не мешает тексту переноситься ещё и самому.
-- Перенос внутри сноски не виден: сноски приходят к модулю метками вместо текста.
-- Функция проверяет все значения вызова, поэтому сделана дешёвой: каждому приёму нужен знак «<», «:» или «=»,
-- и значение без них отсеивается сразу. Паттерны состоят из знаков ASCII, поэтому сравнение побайтовое.
function ppp.manual_format(value, allow_breaks)
if type(value) ~= 'string' or not value:find('[<:=]') then
return nil
end
for _, mark in ipairs(manual_marks) do
if not (allow_breaks and mark[1] == 'перенос строки') and value:find(mark[2]) then
return mark[1]
end
end
return nil
end
-- Приводит размер шрифта в процентах к допустимому по [[ВП:РТ]]: значение меньше 85% поднимается до 85%.
-- Возвращает значение и признак того, что его пришлось поднять. Значение не в процентах возвращается без изменений.
function ppp.clamp_font_size(value)
local size = value and mw.ustring.match(value, '^%s*(%d+%.?%d*)%s*%%%s*$')
if not size then
return value, false
end
if tonumber(size) < 85 then
return '85%', true
end
return value, false
end
-- Названия команд и флаги ---------------------------------------------------------------
-- Части паттернов для ppp.team_name. В Lua нет регулярных выражений, только паттерны, поэтому
-- варианты записи перебираются по списку: флаг стоит до названия или после, выведен шаблоном
-- {{флаг}} (span с классом flagicon) или прямой ссылкой на файл и отделён пробелом или .
-- Само название бывает ссылкой с подписью, ссылкой без подписи или простым текстом,
-- за которым может следовать уточнение в скобках.
local span_begin = '<span class="flagicon">'
local span_end = '</span>'
local any_file = '%[%[File:.+%]%]'
local any_file_ru = '%[%[Файл:.+%]%]'
local any_space = '%s*'
local nbsp = ' '
local flagicon = span_begin .. '%[%[Файл:.+%]%]' .. span_end
local clarification = '%b()'
local wikilink_text = '%[%[.+|([^%]]+)%]%]'
local wikilink_same = '%[%[([^%]]+)%]%]'
local nolink = '([^%[%]]+)'
-- Виды разделителя между флагом и названием и виды самого названия.
-- В каждом виде названия одна группа захвата — само название.
local spaces = {any_space, nbsp}
local targets = {
wikilink_text, wikilink_text .. any_space .. clarification,
wikilink_same, wikilink_same .. any_space .. clarification,
nolink, nolink .. any_space .. clarification}
-- Возвращает название по первому подошедшему паттерну, обрезанное trim, либо исходную строку,
-- если не подошёл ни один. Сравнение побайтовое: все разделители в паттернах — знаки ASCII,
-- и захват не может начаться или закончиться посреди многобайтовой буквы. mw.ustring здесь
-- многократно медленнее: паттернов почти восемьдесят, и каждый разбирался бы заново.
local function check_any_pattern(s, patterns)
for _, pattern in ipairs(patterns) do
local m = s:match(pattern)
if m then
return trim(m)
end
end
return s
end
-- Добавляет в list паттерны для всех сочетаний «вид названия × вид разделителя»;
-- fun собирает из них паттерн с флагом. Каждый паттерн привязан к концу строки.
local function pattern_maker(list, fun)
for _, target in ipairs(targets) do
for _, space in ipairs(spaces) do
table.insert(list, fun(space, target) .. '$')
end
end
end
-- Все варианты записи названия. Первыми проверяются варианты с шаблоном {{флаг}}, чтобы в название
-- не попал открывающий span; последними — названия без флага, без привязки к концу строки.
local team_name_variants = {}
pattern_maker(team_name_variants, function(space, target) return flagicon .. space .. target end)
pattern_maker(team_name_variants, function(space, target) return target .. space .. flagicon end)
pattern_maker(team_name_variants, function(space, target) return any_file .. space .. target end)
pattern_maker(team_name_variants, function(space, target) return target .. space .. any_file end)
pattern_maker(team_name_variants, function(space, target) return any_file_ru .. space .. target end)
pattern_maker(team_name_variants, function(space, target) return target .. space .. any_file_ru end)
for _, target in ipairs(targets) do
table.insert(team_name_variants, target)
end
-- Кэш ppp.team_name на время вызова; false — в значении не нашлось названия.
local team_name_cache = {}
-- Возвращает отображаемое название команды: без флага до или после него и без разметки ссылки
-- (у ссылки берётся подпись). nil и пустая строка возвращаются как есть; nil возвращается и тогда,
-- когда после обрезки от названия ничего не остаётся.
function ppp.team_name(s)
if s == nil or s == '' then
return s
end
local cached = team_name_cache[s]
if cached == nil then
cached = check_any_pattern(s, team_name_variants) or false
team_name_cache[s] = cached
end
return cached or nil
end
-- Написания пространства имён файлов для паттернов, без учёта регистра.
-- Открыты для [[Модуль:Sports results]].
ppp.file_prefixes = {'[Ff][Ii][Ll][Ee]', '[Фф][Аа][Йй][Лл]'}
-- Паттерны флага: шаблон {{флаг}} (span с классом flagicon вокруг картинки) и прямая ссылка на файл,
-- в обоих написаниях пространства имён. Собираются один раз, при загрузке.
local flag_patterns = {}
for _, prefix in ipairs(ppp.file_prefixes) do
flag_patterns[#flag_patterns + 1] = '<span[^<>]*flagicon[^<>]*>%s*%[%[' .. prefix .. ':[^%[%]]*%]%]%s*</span>'
end
for _, prefix in ipairs(ppp.file_prefixes) do
flag_patterns[#flag_patterns + 1] = '%[%[' .. prefix .. ':[^%[%]]*%]%]'
end
-- Возвращает флаг, которым начинается значение, либо nil. Нужна для ширины столбца названий
-- (ppp.measure_team_width), для проверки места флага в карточке матча ([[Модуль:Football box/base]])
-- и для переноса флага хозяев в конец названия ([[Модуль:Sports group]]).
function ppp.leading_flag(s)
if s == nil or s == '' then
return nil
end
for _, pattern in ipairs(flag_patterns) do
local flag = mw.ustring.match(s, '^%s*(' .. pattern .. ')')
if flag then
return flag
end
end
return nil
end
ppp.leading_flag = memoize(ppp.leading_flag)
-- Возвращает флаг, которым заканчивается значение, либо nil.
-- Признаки флага те же, что у ppp.leading_flag.
function ppp.trailing_flag(s)
if s == nil or s == '' then
return nil
end
for _, pattern in ipairs(flag_patterns) do
local flag = mw.ustring.match(s, '(' .. pattern .. ')%s*$')
if flag then
return flag
end
end
return nil
end
ppp.trailing_flag = memoize(ppp.trailing_flag)
-- Убирает из значения ссылки на файлы (обычно это флаг команды),
-- опустевшую обёртку шаблона флага и оставшийся в начале неразрывный пробел.
function ppp.strip_files(s)
s = s or ''
for _, prefix in ipairs(ppp.file_prefixes) do
s = mw.ustring.gsub(s, '%[%[' .. prefix .. ':[^%[%]]*%]%]', '')
end
-- Обёртка шаблона флага: после удаления самого файла от неё остаётся пустой span,
-- который в выводе не виден, но занимает место в разметке и мешает дальнейшим
-- заменам по образцу. Снимается только опустевшая — с содержимым не трогается.
s = mw.ustring.gsub(s, '<span class="flagicon">%s*</span>', '')
return (mw.ustring.gsub(s, '^%s* %s*', ''))
end
-- Снимает разметку, не относящуюся к содержанию: файлы (ppp.strip_files), надстрочники вместе
-- с содержимым, прочие теги (содержимое остаётся), служебные метки расширений и кавычки.
-- Вики-ссылки не трогает — их разбирают вызывающие функции, каждая по-своему.
local function strip_markup(s)
s = ppp.strip_files(s)
s = mw.ustring.gsub(s, '<[Ss][Uu][Pp]>[^<>]*</[Ss][Uu][Pp]>', '')
s = mw.ustring.gsub(s, '<[Aa][Bb][Bb][Rr][^>]*>([^<]*)</[Aa][Bb][Bb][Rr]>', '%1')
s = mw.ustring.gsub(s, '</?%w+[^<>]*>', '')
s = mw.ustring.gsub(s, '\127[^\127]*UNIQ%-%-[^\127]*%-%x+%-QINU[^\127]*\127', '')
s = mw.ustring.gsub(s, '[\'"]', '')
return s
end
-- Сводит и повторяющиеся пробелы к одиночным пробелам и обрезает края (trim);
-- вместо nil возвращает пустую строку.
local function collapse_spaces(s)
s = mw.ustring.gsub(s, '%s* %s*', ' ')
s = mw.ustring.gsub(s, '%s%s*', ' ')
return trim(s) or ''
end
-- Строит ключ команды для идентификатора матча и реестра сокращений. В отличие от ppp.team_name
-- и ppp.makeanchor у ссылки берётся её цель, а не подпись, и уточнение в скобках сохраняется:
function ppp.team_key(s)
s = strip_markup(s)
s = mw.ustring.gsub(s, '%[%[([^%[%]%|]*)%|[^%[%]]*%]%]', '%1')
s = mw.ustring.gsub(s, '%[%[([^%[%]]*)%]%]', '%1')
return collapse_spaces(s)
end
ppp.team_key = memoize(ppp.team_key)
-- Возвращает функцию сравнения кодов команд для сортировки по алфавиту: по названию из name_КОД
-- (без флага и разметки ссылки — ppp.team_name, без учёта регистра), при равных названиях —
-- по коду. Названия разбираются один раз на каждый код. Сортируют ею автосборка ([[Модуль:Sports table/auto]])
-- и расписание ([[Модуль:Sports results/schedule]]): команды без заданного порядка у обоих идут одинаково.
function ppp.name_sorter(Args)
local key = {}
local function keyof(c)
if key[c] == nil then
key[c] = mw.ustring.lower(ppp.team_name(Args['name_' .. c] or c) or c)
end
return key[c]
end
return function(x, y)
local kx, ky = keyof(x), keyof(y)
if kx == ky then return x < y end
return kx < ky
end
end
-- Делит окончание имени параметра матча («ААА_БББ») на коды хозяев и гостей. Подчёркивание
-- может стоять и внутри кода, поэтому из всех вариантов деления выбирается тот, где известны оба кода
-- (набор codes), затем — где известен код хозяев, затем — гостей; если не известен ни один, деление идёт
-- по последнему подчёркиванию. Возвращает nil, если подчёркивания нет. Общая для автосборки и расписания:
-- матч, который читает одна, другая должна найти по тем же кодам.
function ppp.split_pair(rest, codes)
local cands = {}
local i = rest:find('_', 1, true)
while i do
local h, a = rest:sub(1, i - 1), rest:sub(i + 1)
if h ~= '' and a ~= '' then
cands[#cands + 1] = {h, a}
end
i = rest:find('_', i + 1, true)
end
if #cands == 0 then return nil end
for _, c in ipairs(cands) do
if codes[c[1]] and codes[c[2]] then return c[1], c[2] end
end
for _, c in ipairs(cands) do
if codes[c[1]] then return c[1], c[2] end
end
for _, c in ipairs(cands) do
if codes[c[2]] then return c[1], c[2] end
end
return cands[#cands][1], cands[#cands][2]
end
-- Счёт ----------------------------------------------------------------------------------
-- Приводит разделитель счёта к двоеточию. В источниках и в разметке, скопированной из англовики,
-- встречаются дефис, короткое и длинное тире, минус и мнемоники – и —; счёт при этом
-- может быть подписью ссылки — внутренней или внешней, как у шаблона {{score link}}. Заменяется
-- только разделитель между двумя числами в начале значения:
-- примечания, уточнения и пояснения после счёта остаются нетронутыми.
function ppp.format_score(s)
s = mw.ustring.gsub(s or '', '^%s*([%d%.]+)%s*[–−—%-]%s*([%d%.]+)', '%1:%2')
s = mw.ustring.gsub(s, '^%s*([%d%.]+)%s*&[MmNn][Dd][Aa][Ss][Hh];%s*([%d%.]+)', '%1:%2')
s = mw.ustring.gsub(s, '^%s*(%[%[[^%[%]]*%|[%d%.]+)%s*[–−—%-]%s*([%d%.]+)', '%1:%2')
s = mw.ustring.gsub(s, '^%s*(%[[^%[%]%s]*%s+[%d%.]+)%s*[–−—%-]%s*([%d%.]+)', '%1:%2')
s = mw.ustring.gsub(s, '^%s*(%[%[[^%[%]]*%|[%d%.]+)%s*&[MmNn][Dd][Aa][Ss][Hh];%s*([%d%.]+)', '%1:%2')
s = mw.ustring.gsub(s, '^%s*(%[[^%[%]%s]*%s+[%d%.]+)%s*&[MmNn][Dd][Aa][Ss][Hh];%s*([%d%.]+)', '%1:%2')
return s
end
-- Разбирает счёт вида «2:1» на два числа. Перед разбором снимается оформление:
-- служебные метки сносок, надстрочники, теги, ссылки (остаются подписи), мнемоники тире
-- и кавычки полужирного и курсива. Разбирается только начало значения: приписка после счёта не мешает.
-- Возвращает nil, если в начале значения счёта нет: в этих полях пишут и даты, и прочерки.
function ppp.score_pair(s)
if s == nil or s == '' then
return nil
end
-- Обычный счёт без оформления разбирается сразу.
local plain_a, plain_b = s:match('^(%d+):(%d+)$')
if plain_a then
return tonumber(plain_a), tonumber(plain_b)
end
-- Метки сносок: к моменту, когда модуль получает аргумент, тег ref уже заменён служебной меткой.
s = mw.ustring.gsub(s, '\127[^\127]*\127', '')
-- Надстрочник удаляется вместе с содержимым: там стоит номер примечания
-- или пояснение, а не часть счёта. У остальных тегов содержимое сохраняется.
s = mw.ustring.gsub(s, '<[Ss][Uu][Pp].->.-</[Ss][Uu][Pp]>', '')
s = mw.ustring.gsub(s, '</?%w+[^<>]*>', '')
-- Ссылки заменяются своей подписью: счёт часто оборачивают ссылкой на отчёт.
s = mw.ustring.gsub(s, '%[%[[^%[%]%|]*%|([^%[%]]*)%]%]', '%1')
s = mw.ustring.gsub(s, '%[%[([^%[%]]*)%]%]', '%1')
s = mw.ustring.gsub(s, '%[%S+%s+([^%[%]]*)%]', '%1')
-- Сущности приводятся к обычным знакам: в разметке встречаются и они.
s = mw.ustring.gsub(s, '&[Nn][Dd][Aa][Ss][Hh];', '-')
s = mw.ustring.gsub(s, '&[Mm][Dd][Aa][Ss][Hh];', '-')
s = mw.ustring.gsub(s, '−', '-')
s = mw.ustring.gsub(s, ' ', ' ')
s = mw.ustring.gsub(s, "'+", '')
local a, b = mw.ustring.match(s, '^%s*(%d+)%s*[:%-–—−‒―‐‑]%s*(%d+)')
if not a then
return nil
end
return tonumber(a), tonumber(b)
end
ppp.score_pair = memoize(ppp.score_pair)
-- Возвращает true, если значение клетки — счёт: обычный, в ссылке или с оформлением
-- (см. ppp.score_pair), либо технический результат «+:−» или «−:+». Всё прочее —
-- дата предстоящего матча, «перенесён», пояснение — счётом не считается.
function ppp.is_score_value(s)
if s == nil or s == '' then
return false
end
if ppp.score_pair(s) then
return true
end
local plain = mw.ustring.gsub(s, '\127[^\127]*\127', '')
plain = mw.ustring.gsub(plain, '%[%[[^%[%]|]*|([^%[%]]*)%]%]', '%1')
plain = mw.ustring.gsub(plain, '</?%w+[^<>]*>', '')
return mw.ustring.find(plain, '^%s*[%+%-−–—]%s*:%s*[%+%-−–—]') ~= nil
end
ppp.is_score_value = memoize(ppp.is_score_value)
-- Коды исхода матча в result_ХХХ_УУУ шахматки. stage — чем матч решился:
-- 'ot' — дополнительное время или овертайм, 'pk' — серия пенальти или буллитов,
-- nil — основное время. outcome — исход для команды из левого столбца (хозяев):
-- 1 — победа, 0 — ничья, −1 — поражение. У OT, PK и SO исхода нет:
-- он виден из счёта, а код уточняет только стадию.
-- Коды с исходом те же, что у параметра result карточки матча ([[Модуль:Football box/base]]):
-- W, L, OTW, OTD, OTL, PW, PL. SO, SW и SL — английские написания, принятые наравне с PK, PW и PL;
-- PKW и PKL — написание расписаний англовики, то же, что PW и PL.
-- По этой таблице закрашивает клетки [[Модуль:Sports results]] и считает исходы автосборка
-- стилей с овертаймом ([[Модуль:Sports table/auto]]): цвет клетки и строка таблицы берут исход
-- из одного места и разойтись не могут.
ppp.result_codes = {
W = {outcome = 1},
L = {outcome = -1},
OT = {stage = 'ot'},
OTW = {stage = 'ot', outcome = 1},
OTD = {stage = 'ot', outcome = 0},
OTL = {stage = 'ot', outcome = -1},
PK = {stage = 'pk'},
SO = {stage = 'pk'},
PW = {stage = 'pk', outcome = 1},
PL = {stage = 'pk', outcome = -1},
SW = {stage = 'pk', outcome = 1},
SL = {stage = 'pk', outcome = -1},
PKW = {stage = 'pk', outcome = 1},
PKL = {stage = 'pk', outcome = -1},
}
-- Код исхода с точки зрения соперника: победа становится поражением, стадия сохраняется.
-- Коды без исхода (OT, PK, SO) и ничья OTD остаются как есть. Нужен расписанию
-- ([[Модуль:Sports results/schedule]]): матч записан один раз, а виден в строках обеих команд.
ppp.inverse_result = {
W = 'L', L = 'W', OTW = 'OTL', OTL = 'OTW',
PW = 'PL', PL = 'PW', SW = 'SL', SL = 'SW', PKW = 'PKL', PKL = 'PKW',
}
-- Даты ----------------------------------------------------------------------------------
-- Названия месяцев в родительном и именительном падеже; значение — родительный падеж.
local month_names = {
['января'] = 'января', ['январь'] = 'января', ['февраля'] = 'февраля', ['февраль'] = 'февраля',
['марта'] = 'марта', ['март'] = 'марта', ['апреля'] = 'апреля', ['апрель'] = 'апреля',
['мая'] = 'мая', ['май'] = 'мая', ['июня'] = 'июня', ['июнь'] = 'июня',
['июля'] = 'июля', ['июль'] = 'июля', ['августа'] = 'августа', ['август'] = 'августа',
['сентября'] = 'сентября', ['сентябрь'] = 'сентября', ['октября'] = 'октября', ['октябрь'] = 'октября',
['ноября'] = 'ноября', ['ноябрь'] = 'ноября', ['декабря'] = 'декабря', ['декабрь'] = 'декабря'
}
-- Приводит дату к единому виду: снимает ссылки, теги и приписки «года», «год», «г.», сводит пробелы,
-- убирает ведущий ноль у числа. Ссылки на день и год в тексте статьи не приняты, поэтому снимаются.
-- Значение, которое датой не распознаётся, возвращается как есть — в поле даты встречаются и диапазоны, и пояснения.
function ppp.format_date(value)
-- Дата начинается с числа, за которым идёт пробел, , тег или конец ссылки;
-- значение без такого места датой быть не может — это счёт или текст.
if type(value) ~= 'string' or not value:find('%d[%s&<%]|]') then
return value
end
-- Дата, целиком обёрнутая в ссылку на статью без цифр в названии — так в шахматке ставят на дату
-- предстоящего матча ссылку на статью о дерби, — сохраняет ссылку: приводится только подпись.
local target, label = value:match('^%s*%[%[([^%[%]|]+)|([^%[%]]+)%]%]%s*$')
if target and not target:find('%d') then
local inner = ppp.format_date(label)
return inner ~= label and '[[' .. target .. '|' .. inner .. ']]' or value
end
local s = value
s = mw.ustring.gsub(s, '%[%[[^%[%]|]*|([^%[%]]*)%]%]', '%1')
s = mw.ustring.gsub(s, '%[%[([^%[%]]*)%]%]', '%1')
s = mw.ustring.gsub(s, '<[^<>]*>', '')
s = mw.ustring.gsub(s, ' ', ' ')
s = mw.ustring.gsub(s, '%s+', ' ')
s = mw.text.trim(s)
-- Приписка «года» снимается отдельным шагом, а не необязательными буквами в образце:
-- знак «?» относился бы к последнему байту буквы, а не к букве целиком.
s = mw.ustring.gsub(s, '%s+года$', '')
s = mw.ustring.gsub(s, '%s+год$', '')
s = mw.ustring.gsub(s, '%s+г%.?$', '')
-- Название месяца берётся целым словом и узнаётся по словарю:
-- так разбор не зависит от того, какие знаки считать буквами.
local day, month, year = mw.ustring.match(s, '^(%d%d?)%s+(%S+)%s+(%d%d%d%d)%.?$')
if not day then
day, month = mw.ustring.match(s, '^(%d%d?)%s+(%S+)%.?$')
end
local canon = month and month_names[mw.ustring.lower(month)]
if not canon then
return value
end
return tonumber(day) .. ' ' .. canon .. (year and (' ' .. year) or '')
end
ppp.format_date = memoize(ppp.format_date)
-- Якоря и ссылки на отчёты о матчах -----------------------------------------------------
-- Приводит произвольный текст к виду, пригодному для идентификатора раздела: снимает разметку
-- (strip_markup), заменяет ссылки их подписями, сводит пробелы. У ссылки берётся подпись:
-- для заголовков и названий турниров важно то, что видит читатель. Вызывается карточками матча.
function ppp.makeanchor(s)
s = strip_markup(s)
s = mw.ustring.gsub(s, '%[%[[^%[%]%|]*%|([^%[%]]*)%]%]', '%1')
s = mw.ustring.gsub(s, '%[%[([^%[%]]*)%]%]', '%1')
return collapse_spaces(s)
end
ppp.makeanchor = memoize(ppp.makeanchor)
-- Разделитель команд в идентификаторе матча. На нём сходятся две стороны: карточка матча ставит якорь
-- ([[Модуль:Football box/base]]), шахматка строит ссылку на него ([[Модуль:Sports results]],
-- через ppp.match_anchor). Разойдись разделители — ссылки перестали бы попадать в якоря.
ppp.anchor_separator = ' — '
-- Строит основной идентификатор матча вида «Динамо (Москва) — Спартак (Москва)» из ключей команд
-- (ppp.team_key). Карточка матча ставит его якорем, шахматка — целью ссылки со счёта.
function ppp.match_anchor(team1, team2)
return ppp.team_key(team1 or 'Team 1')
.. ppp.anchor_separator .. ppp.team_key(team2 or 'Team 2')
end
-- Разделитель команд в идентификаторе матча, принятый в англовики.
ppp.anchor_separator_en = ' v '
-- Возвращает все написания идентификатора матча — для якорей карточки. Первое — основное
-- (как у ppp.match_anchor); остальные нужны, чтобы в якорь попадала и ссылка,
-- построенная по другому соглашению: подписи ссылок через тире и подписи через латинское «v»,
-- как в англовики. Англовики берёт подпись ссылки, этот модуль — её цель, поэтому «Динамо» и «Динамо (Москва)»
-- дают разные строки; разметку карточек массово копируют из англовики, и оба написания встречаются в статьях наравне.
-- Пустые и повторяющиеся варианты отбрасываются: у команды без уточнения подпись и цель ссылки совпадают.
function ppp.match_anchors(team1, team2)
local key1, key2 = ppp.team_key(team1 or 'Team 1'), ppp.team_key(team2 or 'Team 2')
local name1, name2 = ppp.team_name(team1 or 'Team 1'), ppp.team_name(team2 or 'Team 2')
local variants = {
key1 .. ppp.anchor_separator .. key2,
name1 .. ppp.anchor_separator .. name2,
name1 .. ppp.anchor_separator_en .. name2
}
local list, seen = {}, {}
for _, v in ipairs(variants) do
if v ~= '' and not seen[v] then
seen[v] = true
list[#list + 1] = v
end
end
return list
end
-- Решает, куда вести ссылку со счёта на отчёт о матче. Возвращает функцию:
-- по якорю матча она отдаёт страницу, которую надо подставить перед «#»,
-- либо пустую строку, если отчёт лежит на текущей странице.
-- group — группа, первый позиционный параметр шаблона.
--
-- Порядок решения:
-- 1. задан baselink — страница из него (пустая строка, если это текущая страница);
-- 2. модуль вызван без шаблона, через [[Модуль:Excerpt]] или на странице самого шаблона — отчёты рядом;
-- 3. тот же вызов выводит и отчёты (show_box) — отчёты рядом;
-- 4. текущая страница — статья с тем же именем, что у шаблона, — отчёты рядом;
-- 5. на странице есть вызов того же шаблона для той же группы, выводящий отчёты:
-- целиком (show_box) — отчёты рядом; матчи одной команды (её код — второй позиционный параметр) —
-- рядом отчёты о матчах этой команды;
-- 6. иначе — статья в основном пространстве с тем же именем, что у шаблона.
-- Пятый шаг читает исходный текст текущей страницы — один раз на таблицу. Во время предпросмотра
-- там лежит ещё сохранённая версия: вызов, добавленный в этой правке, будет учтён после сохранения.
function ppp.report_link_resolver(frame, Args, group)
local current = mw.title.getCurrentTitle()
local here = function() return '' end
local override = Args['baselink']
if override and override ~= '' then
if override == current.fullText then
return here
end
return function() return override end
end
local parent = frame:getParent() and frame:getParent():getTitle() or ''
if parent == '' or parent == 'Модуль:Excerpt' or parent == current.fullText then
return here
end
if Args['show_box'] and Args['show_box'] ~= '' then
return here
end
local ptitle = mw.title.new(parent)
local article = ptitle and ptitle.text or parent
if article == current.fullText then
return here
end
-- Ищет на странице вызовы того же шаблона для той же группы. Имя сравнивается
-- без пространства имён, без учёта регистра первой буквы и с пробелом вместо «_».
local function norm(s)
s = mw.text.trim((mw.ustring.gsub(s or '', '_', ' ')))
s = mw.ustring.gsub(s, '^[Шш]аблон:', '')
s = mw.ustring.gsub(s, '^[Tt]emplate:', '')
return mw.ustring.lower(mw.ustring.sub(s, 1, 1)) .. mw.ustring.sub(s, 2)
end
local want, grp = norm(article), mw.text.trim(group or '')
local all, codes = false, {}
local src = current:getContent() or ''
for call in src:gmatch('%b{}') do
local inner = call:match('^{{(.*)}}$')
if inner then
local f = mw.text.split(inner, '|', true)
if #f >= 2 and norm(f[1]) == want and mw.text.trim(f[2]) == grp then
for i = 3, #f do
local key, val = f[i]:match('^%s*([^=]-)%s*=%s*(.-)%s*$')
if key == 'show_box' and val ~= '' then
all = true
elseif not key and i == 3 and mw.text.trim(f[i]) ~= '' then
codes[#codes + 1] = mw.text.trim(f[i])
end
end
end
end
end
if all then
return here
end
-- Отчёты отдельных команд: ссылка на матч остаётся на странице, если в нём
-- играет одна из них. Сравнивается ключ команды — тот же, из которого собран якорь.
local keys = {}
for _, c in ipairs(codes) do
keys[ppp.team_key(Args['name_' .. c] or c)] = true
end
return function(anchor)
local a, b = mw.ustring.match(anchor or '', '^(.-)' .. ppp.anchor_separator .. '(.*)$')
if (a and keys[a]) or (b and keys[b]) then
return ''
end
return article
end
end
-- Ведёт ссылки со счёта вида «[[#якорь|подпись]]» туда, где лежат отчёты: дописывает перед «#»
-- страницу, которую выбирает ppp.report_link_resolver для группы из первого параметра шаблона.
-- t — массив строк разметки таблицы, изменяется на месте. Паттерн состоит из знаков ASCII,
-- поэтому замена побайтовая: строки таблицы длинные, и mw.ustring разбирал бы каждую целиком.
function ppp.resolve_report_links(frame, Args, t)
local resolve = ppp.report_link_resolver(frame, Args, frame:getParent() and frame:getParent().args[1])
for k = 1, #t do
if t[k]:find('[[#', 1, true) then
t[k] = t[k]:gsub('%[%[#([^%[%]|]*)%|', function(anchor)
return '[[' .. resolve(anchor) .. '#' .. anchor .. '|'
end)
end
end
end
-- Строка итогов и показатели команд -----------------------------------------------------
-- Код псевдокоманды, под которым [[Модуль:Sports table]] добавляет строку итогов «Всего».
-- Командой она не является: проверки, перебирающие команды по name_ХХХ, её пропускают.
ppp.total_row_code = 'SPORTS_TABLE_TOTAL'
-- Имена показателей команды во всех стилях. По ним ppp.has_stats проверяет, заполнен ли у команды
-- хоть один показатель: незаполненный показатель модуль считает нулём, и строка из одних нулей
-- неотличима от команды, данные которой просто не внесли.
ppp.stat_prefixes = {
'win', 'draw', 'loss', 'gf', 'ga', 'pf', 'pa', 'matches', 'rw',
'hwin', 'hdraw', 'hloss', 'hgf', 'hga', 'hpf', 'hpa', 'hrw',
'awin', 'adraw', 'aloss', 'agf', 'aga', 'apf', 'apa', 'arw',
'OTwin', 'OTloss', 'PKwin', 'PKloss', 'SOwin', 'SOloss', 'tiebr',
'adjust_points', 'startpoints'
}
-- Возвращает true, если у команды code заполнен хоть один показатель из ppp.stat_prefixes.
function ppp.has_stats(Args, code)
for _, prefix in ipairs(ppp.stat_prefixes) do
local v = Args[prefix .. '_' .. code]
if v ~= nil and mw.text.trim(tostring(v)) ~= '' then
return true
end
end
return false
end
-- Прибавляет показатели команды code к строке итогов (ppp.total_row_code), записывая суммы в Args.
-- fields — имена показателей стиля; показатель с синонимом записывается парой {'gf', 'pf'}:
-- значение берётся из любого написания, а сумма всегда пишется под основным именем —
-- оно читается первым, и накопленное под синонимом заслонялось бы им.
function ppp.add_to_totals(Args, code, fields)
local total = ppp.total_row_code
for _, f in ipairs(fields) do
local name, alt = f, nil
if type(f) == 'table' then
name, alt = f[1], f[2]
end
local v = Args[name .. '_' .. code] or (alt and Args[alt .. '_' .. code])
Args[name .. '_' .. total] = (tonumber(Args[name .. '_' .. total]) or 0) + (tonumber(v) or 0)
end
end
-- Реестр сокращений и статьи о дерби ----------------------------------------------------
-- Страница реестра сокращений команд. Одна команда в разных шаблонах может получить разные сокращения:
-- Сверить шаблоны друг с другом при выводе нельзя — это разные вызовы модуля, — поэтому каждый сверяется с общим реестром.
-- Реестр — JSON-страница [[Модуль:Sports table/codes.json]]: данные чисто технические,
-- в JSON их может прочесть любой модуль, а обновить — бот. Движок не даёт сохранить неверный JSON,
-- так что сломанной страница остаться не может.
ppp.registry_page = 'Модуль:Sports table/codes.json'
-- Возвращает ключ реестра для значения названия команды — статью о команде, то есть цель ссылки
-- в названии (ppp.team_key). Ключ один во всех шаблонах, в отличие от подписи ссылки и положения
-- флага. Первая буква приводится к прописной, подчёркивания — к пробелам:
-- так имена страниц сравнивает и сам движок.
function ppp.registry_key(s)
local key = mw.text.trim((mw.ustring.gsub(ppp.team_key(s or ''), '_', ' ')))
return mw.ustring.upper(mw.ustring.sub(key, 1, 1)) .. mw.ustring.sub(key, 2)
end
-- Возвращает сокращение, которое видит читатель: подпись из short_ХХХ, если она задана, иначе код —
-- он и попадает в заголовок столбца шахматки. Сверяется именно видимое: латинский код
-- с кириллическим short_ХХХ читатель видит кириллицей, и расхождением это не считается. Если
-- в short_ХХХ нет букв (например, там только флаг), возвращает nil: такую команду не с чем сверять.
function ppp.visible_code(code, short)
if short and short ~= '' then
-- Картинка флага снимается до разбора ссылок: у неё тоже есть квадратные скобки,
-- и без подписи она приняла бы вид сокращения.
short = ppp.strip_files(short)
local shown = short:match('%[%[[^%[%]|]*|([^%[%]]*)%]%]') or short
shown = mw.text.trim((mw.ustring.gsub(shown, '<[^<>]*>', '')))
if not mw.ustring.find(shown, '%a') then
return nil
end
return shown
end
return code
end
-- Возвращает реестр целиком — {teams = {статья = сокращение}, pending = {статья = {варианты}},
-- derbies = {«статья|статья» = статья о дерби}} — либо nil, если страницы реестра нет.
-- Реестр загружается через mw.loadJsonData — один раз на страницу, сколько бы таблиц на ней ни было.
function ppp.code_registry_data()
local ok, data = pcall(mw.loadJsonData, ppp.registry_page)
return ok and data or nil
end
-- Возвращает только сокращения команд (раздел teams) либо nil.
function ppp.code_registry()
local data = ppp.code_registry_data()
return data and data.teams or nil
end
-- Сверяет сокращения команд таблицы (name_ХХХ, short_ХХХ) с реестром:
-- registry — раздел teams, pending — раздел pending.
-- Возвращает три отсортированных списка:
-- расхождения {code, shown, want, team} — команда есть в реестре, но записана иначе;
-- команды, которых в реестре нет; команды из раздела pending.
-- Отсутствие в реестре — не ошибка: реестр пополняется по мере надобности, и требовать заранее
-- занести в него все команды мира незачем. Команда из раздела pending отдаётся отдельно:
-- она в реестре есть, и добавлять её не нужно.
function ppp.check_codes(Args, registry, pending)
local mismatches, missing, undecided = {}, {}, {}
for k, v in pairs(Args) do
local code = type(k) == 'string' and k:match('^name_(.+)$')
if code and code ~= ppp.total_row_code then
local key = ppp.registry_key(v)
local shown = ppp.visible_code(code, Args['short_' .. code])
if key ~= '' and shown then
local want = registry[key]
if want and want ~= shown then
mismatches[#mismatches + 1] = {code = code, shown = shown, want = want, team = key}
elseif not want and pending and pending[key] then
undecided[#undecided + 1] = key
elseif not want then
missing[#missing + 1] = key
end
end
end
end
table.sort(mismatches, function(a, b) return a.code < b.code end)
table.sort(missing)
table.sort(undecided)
return mismatches, missing, undecided
end
-- Приводит имя страницы к виду, в котором его сравнивает движок:
-- края обрезаны, подчёркивания заменены пробелами, первая буква прописная.
local function page_key(title)
local key = mw.text.trim((mw.ustring.gsub(title or '', '_', ' ')))
return mw.ustring.upper(mw.ustring.sub(key, 1, 1)) .. mw.ustring.sub(key, 2)
end
-- Возвращает статью о дерби для пары команд по значениям их названий (name_ХХХ, team1/team2)
-- либо nil. derbies — раздел derbies реестра: ключ — статьи обеих команд через «|», в любом порядке.
-- Сравниваются статьи (ppp.registry_key), а не подписи: одинаковые подписи у разных клубов пару не образуют.
function ppp.derby_article(derbies, name_a, name_b)
if not derbies then
return nil
end
local ka, kb = ppp.registry_key(name_a), ppp.registry_key(name_b)
if ka == '' or kb == '' or ka == kb then
return nil
end
return derbies[ka .. '|' .. kb] or derbies[kb .. '|' .. ka]
end
-- Возвращает true, если в значении есть вики-ссылка на статью article. Цели сравниваются так же,
-- как их сравнивает движок (page_key); якорь в ссылке не учитывается.
function ppp.links_to(value, article)
if type(value) ~= 'string' or not value:find('[[', 1, true) then
return false
end
local want = page_key(article)
for target in value:gmatch('%[%[([^%[%]|#]*)') do
if page_key(target) == want then
return true
end
end
return false
end
-- Строит подсказки о статьях про дерби для шахматки: для пар из раздела derbies реестра, у которых
-- в клетке предстоящего матча (значение — не счёт) нет ссылки на статью о дерби. Перебираются
-- команды на позициях first..last и все legs кругов (match_ХХХ_УУУ или matchN_ХХХ_УУУ); на пару —
-- одна подсказка. Ссылка сама не ставится: решает редактор. Возвращает список строк.
function ppp.derby_hints(Args, team_list, first, last, legs, derbies)
local out, seen = {}, {}
if not derbies then
return out
end
for i = first, last do
for j = first, last do
local a, b = team_list[i], team_list[j]
if i ~= j and a and b and a ~= ppp.total_row_code and b ~= ppp.total_row_code then
local name_a, name_b = Args['name_' .. a] or a, Args['name_' .. b] or b
local article = ppp.derby_article(derbies, name_a, name_b)
if article then
for l = 1, legs do
local m = (legs == 1) and 'match_' or 'match' .. l .. '_'
local v = Args[m .. a .. '_' .. b]
local pair = a < b and a .. '|' .. b or b .. '|' .. a
if v and v ~= '' and not seen[pair] and not ppp.is_score_value(v)
and not ppp.links_to(v, article) then
seen[pair] = true
out[#out + 1] = ppp.team_name(name_a) .. ' — ' .. ppp.team_name(name_b)
.. ': есть статья [[' .. article .. ']]; ссылку на неё можно поставить'
.. ' на дату матча — [[' .. article .. '|дата]]'
end
end
end
end
end
end
return out
end
-- Ширины столбцов -----------------------------------------------------------------------
-- Подбирает ширину столбца названий команд по самому длинному названию.
-- Длина считается по отображаемому тексту (ppp.team_name без шаблонов, тегов и разметки ссылок)
-- из расчёта полширины em на знак; к ней прибавляются флаг перед названием, буквы статуса и знак сноски,
-- если они есть хоть у одной команды. Перебираются все параметры name_ХХХ, а не список выведенных команд:
-- заголовок строится раньше, чем этот список собран. Разница возникает только там, где часть строк
-- скрыта параметрами show_limit или showteam: столбец тогда окажется шире нужного, но не уже — названия не обрежутся.
-- Нижняя граница, 10em, держится под общий вид: турнир из коротких названий вроде ПСВ, АЗ и ХИК
-- получал бы столбец уже числовых, и таблица выглядела бы перекошенной. Верхняя, 14em,
-- нужна, чтобы одно длинное название не съедало таблицу: за ней название переносится, и столбец перестаёт расти.
-- Возвращает ширину строкой вида «12.50em» и вторым значением признак того, что ширина упёрлась
-- в верхнюю границу. Признак нужен шахматке: там название выводится в одну строку, и столбец при
-- нехватке места не сжимается, а прокручивается; но название длиннее потолка одной строкой не
-- поместится никогда, и перенос там разрешается.
function ppp.measure_team_width(Args)
local longest = 0
local with_flag = false
local status_letters = 0
local has_note = false
-- Один проход по аргументам: названия (name_ХХХ), буквы статуса (status_ХХХ) и сноски (note_ХХХ).
-- Параметров у таблицы сотни, и отдельный проход на каждый вид заметно дороже.
for k, raw in pairs(Args) do
if type(k) == 'string' and type(raw) == 'string' then
local prefix = k:match('^(%a+)_.')
if prefix == 'name' then
if ppp.leading_flag(raw) then
with_flag = true
end
local shown = ppp.team_name(raw) or raw
shown = mw.ustring.gsub(shown, '%b{}', '')
shown = mw.ustring.gsub(shown, '<[^<>]*>', '')
shown = mw.ustring.gsub(shown, '%[%[[^%[%]|]*|([^%[%]]*)%]%]', '%1')
shown = mw.ustring.gsub(shown, '%[%[([^%[%]]*)%]%]', '%1')
local n = mw.ustring.len(mw.text.trim(shown))
if n > longest then
longest = n
end
elseif prefix == 'status' then
local n = select(2, mw.ustring.gsub(raw, '%a', ''))
if n > status_letters then
status_letters = n
end
elseif prefix == 'note' and raw ~= '' then
has_note = true
end
end
end
-- Буквы статуса выводятся после названия или перед ним; по ширине это сопоставимо:
-- Берётся самая длинная запись: ширина у столбца одна, и определяет её самая широкая строка.
local extra = status_letters > 0 and (status_letters * 2 * 0.5 + 0.6) or 0
if has_note then
extra = extra + 1.4
end
local width = longest * 0.5 + (with_flag and 1.5 or 0) + extra + 0.6
local capped = false
if width < 10 then
width = 10
elseif width > 14 then
width = 14
capped = true
end
return string.format('%.2fem', width), capped
end
-- Подбирает общую ширину числовых столбцов: 2,5em, если все значения не длиннее двух знаков,
-- и 3em, если хоть одно из них — сто и больше. Учитываются победы, ничьи, поражения,
-- забитые и пропущенные — общие, дома и в гостях (win_ХХХ, hwin_ХХХ, awin_ХХХ и т. д.).
-- Очки считает ppp.measure_points_width, соотношение мячей — ppp.measure_ratio_width;
-- разница мячей не учитывается — у неё свой знак.
-- Выравнивание достигается шириной, а не разрядностью (ppp.num_align): невидимые нули занимают
-- место, и при центрировании видимое число сдвигается влево. Нижняя граница — два знака, даже когда
-- все значения однозначные: столбцы стоят вплотную, и разница между столбцом под одну цифру
-- и под две заметнее выигранного места.
local num_width_fields = {win = true, draw = true, loss = true, gf = true, ga = true, pf = true,
pa = true, hwin = true, hdraw = true, hloss = true, hgf = true, hga = true, awin = true,
adraw = true, aloss = true, agf = true, aga = true}
function ppp.measure_num_width(Args)
local longest = 2
for k, v in pairs(Args) do
if type(k) == 'string' and num_width_fields[k:match('^(%a+)_')] and (tonumber(v) or 0) >= 100 then
longest = 3
break
end
end
-- Полширины цифры на третий знак.
return string.format('%.2fem', 2.5 + (longest > 2 and 0.5 or 0))
end
-- Подбирает ширину столбца очков: 2,5em, ещё 0,5em, если у кого-то сто очков и больше,
-- и ещё 0,9em, если задано хоть одно примечание hth_ХХХ. Очки считаются по win_ХХХ, draw_ХХХ и loss_ХХХ
-- с winpoints, drawpoints и losspoints (по умолчанию 3, 1, 0, при show_draw=no — 2, 0, 1) плюс
-- adjust_points_ХХХ и startpoints_ХХХ. Считаются здесь же, а не берутся готовыми: строки таблицы
-- строятся позже заголовка, и к моменту, когда нужна ширина, посчитанных очков ещё нет.
-- Ширина своя, отдельная от прочих числовых: знак сноски о личных встречах подмодуль стиля крепит
-- к столбцу, по которому ранжирует таблицу, — обычно к очкам. Расширять из-за него все числовые
-- столбцы значило бы отдать место семи столбцам ради одного: поля стали бы заметно шире,
-- и непонятно, откуда взялась пустота.
function ppp.measure_points_width(Args)
local show_draw = (Args['show_draw'] or 'yes') ~= 'no'
local win_points = tonumber(Args['winpoints']) or (show_draw and 3 or 2)
local draw_points = tonumber(Args['drawpoints']) or (show_draw and 1 or 0)
local loss_points = tonumber(Args['losspoints']) or (show_draw and 0 or 1)
-- Один проход: очки по win_ХХХ и наличие примечаний hth_ХХХ.
local most, has_hth = 0, false
for k, v in pairs(Args) do
if type(k) == 'string' then
local code = k:match('^win_(.+)$')
if code then
local p = win_points * (tonumber(v) or 0)
+ draw_points * (tonumber(Args['draw_' .. code]) or 0)
+ loss_points * (tonumber(Args['loss_' .. code]) or 0)
+ (tonumber(Args['adjust_points_' .. code]) or 0)
+ (tonumber(Args['startpoints_' .. code]) or 0)
if p > most then
most = p
end
elseif type(v) == 'string' and v ~= '' and k:match('^hth_.') then
has_hth = true
end
end
end
local width = 2.5
if most >= 100 then
width = width + 0.5
end
if has_hth then
width = width + 0.9
end
return string.format('%.2fem', width)
end
-- Возвращает ширину столбца соотношения или процента мячей. Там выводится дробь вида «1,250» или «100,0» —
-- пять знаков, заметно длиннее прочих чисел, — и подгонять под неё соседние столбцы значило бы
-- отдать им место, которое нужно одному.
function ppp.measure_ratio_width()
-- Пять знаков с запятой плюс отступы ячейки.
return '2.90em'
end
-- Перенос текста в столбце результатов --------------------------------------------------
-- Предел неразрывной части текста в столбце результатов, в знаках. Около пятнадцати em: столько
-- помещается на экране телефона рядом с краем прокрученной таблицы.
ppp.result_phrase_limit = 30
-- Текст не длиннее этого, в знаках, не разбивается на строки даже в ячейке на несколько строк таблицы:
-- «Стыковые матчи» и подобные короткие подписи читаются одной строкой.
ppp.result_short_text = 24
-- Предлоги, союзы и частицы, которые не отрываются от следующего слова.
local glue_words = {}
for w in ('в во на с со к ко о об обо от ото до по за из изо у без безо для при про под подо '
.. 'над надо перед пред через между из-за из-под и а но или либо ни не'):gmatch('%S+') do
glue_words[w] = true
end
-- Тире и дефис отдельным словом: не отрываются от предыдущего слова.
local result_dashes = {['—'] = true, ['–'] = true, ['-'] = true, ['−'] = true}
-- Видимые знаки для мнемоник, от которых зависят правила переноса;
-- прочие мнемоники считаются одним знаком.
local result_entities = {[' '] = '\194\160', ['—'] = '—', ['–'] = '–', ['−'] = '−'}
-- Разбирает текст на слова. Разделитель слов — пробел вне разметки: пробел внутри тега,
-- ссылки или шаблона словом не считается. Возвращает список слов, список разделителей после каждого слова
-- (' ' — пробел, иначе сам тег <br>) и теги <br> перед первым словом. Слово хранит свои куски разметки
-- (parts; у куска-ссылки в link — её цель и подпись), видимый текст (vis) и его длину в знаках (len).
local function result_words(text)
local words, gaps, lead = {}, {}, ''
local cur = {parts = {}, vis = '', len = 0}
local function add(s, vis, len, link)
cur.parts[#cur.parts + 1] = {s = s, link = link}
cur.vis = cur.vis .. vis
cur.len = cur.len + len
end
local function finish(gap)
if #cur.parts > 0 then
words[#words + 1] = cur
gaps[#words] = gap
cur = {parts = {}, vis = '', len = 0}
elseif gap and gap ~= ' ' then
if #words > 0 then
gaps[#words] = gap
else
lead = lead .. gap
end
end
end
local i, n = 1, #text
while i <= n do
local c = text:sub(i, i)
if c == ' ' or c == '\t' then
finish(' ')
i = text:find('[^ \t]', i) or n + 1
elseif c == '\127' then
-- Метка сноски: на экране это знак вида [1].
local j = text:find('\127', i + 1, true) or n
add(text:sub(i, j), '', 2)
i = j + 1
elseif c == '<' then
local j = text:find('>', i, true)
if j then
local tag = text:sub(i, j)
if tag:match('^<%s*[Bb][Rr][%s/>]') then
finish(tag)
else
add(tag, '', 0)
end
i = j + 1
else
add(c, c, 1)
i = i + 1
end
elseif text:sub(i, i + 1) == '[[' and text:find(']]', i + 2, true) then
local j = text:find(']]', i + 2, true)
local inner = text:sub(i + 2, j - 1)
local target, label = inner:match('^([^|]*)|(.*)$')
target = target or inner
if mw.ustring.find(target, '^%s*[Ff][Ii][Ll][Ee]%s*:') or mw.ustring.find(target, '^%s*[Фф][Аа][Йй][Лл]%s*:')
or mw.ustring.find(target, '^%s*[Ii][Mm][Aa][Gg][Ee]%s*:') then
-- Картинка, обычно флаг: по ширине — около двух знаков.
add(text:sub(i, j + 1), '', 2)
else
local shown = (label or target):gsub('<[^<>]*>', '')
add(text:sub(i, j + 1), shown, mw.ustring.len(shown),
{target = target, label = label or target})
end
i = j + 2
elseif c == '[' and (text:find('^%[https?://', i) or text:find('^%[//', i)) and text:find(']', i, true) then
local j = text:find(']', i, true)
local shown = text:sub(i + 1, j - 1):match('^%S+%s+(.*)$') or ''
add(text:sub(i, j), shown, mw.ustring.len(shown))
i = j + 1
elseif c == '&' and text:match('^&#?%w+;', i) then
local ent = text:match('^&#?%w+;', i)
add(ent, result_entities[ent] or '?', 1)
i = i + #ent
elseif c == '{' and text:match('^%b{}', i) then
local tpl = text:match('^%b{}', i)
local shown = tpl:match('|([^|{}]*)}}$') or ''
add(tpl, shown, mw.ustring.len(shown))
i = i + #tpl
else
local j = text:find('[ \t\127<%[&{]', i + 1) or n + 1
local chunk = text:sub(i, j - 1)
add(chunk, chunk, mw.ustring.len(chunk))
i = j
end
end
finish(nil)
return words, gaps, lead
end
-- Определяет вид разделителя после каждого слова: 'hard' — тег <br>,
-- 'phrase' — пробел после запятой или точки с запятой,
-- 'glue' — пробел, на котором строку рвать нельзя,
-- 'word' — обычный пробел. Затем делит текст на части по разделителям 'phrase' и 'hard'.
-- Возвращает виды разделителей и части {first, last, len, hard}.
local function result_structure(words, gaps)
local kinds, phrases = {}, {}
for k = 1, #words - 1 do
local w, nxt = words[k], words[k + 1]
if gaps[k] ~= ' ' then
kinds[k] = 'hard'
elseif w.vis:find('[,;]$') then
kinds[k] = 'phrase'
elseif glue_words[mw.ustring.lower(w.vis)] or w.vis:find('^%d') or result_dashes[nxt.vis]
or nxt.vis == '' then
-- Слово без видимого текста — метка сноски или одиночный тег — остаётся при
-- предыдущем слове: знак сноски в начале строки оторван от своего текста.
kinds[k] = 'glue'
else
kinds[k] = 'word'
end
end
local first = 1
for k = 1, #words do
if k == #words or kinds[k] == 'phrase' or kinds[k] == 'hard' then
local len = 0
for m = first, k do
len = len + words[m].len + (m > first and 1 or 0)
end
phrases[#phrases + 1] = {first = first, last = k, len = len, hard = kinds[k] == 'hard'}
first = k + 1
end
end
return kinds, phrases
end
-- Разбирает текст ячейки для расчёта раскладки: слова, разделители, части, куски и natural —
-- ширину текста без переносов. tail — ширина знака сноски после текста, в знаках;
-- она прибавляется к последнему слову. Для пустого текста и текста со сложной разметкой —
-- переводом строки, блоком div, вложенной таблицей, блоком fbmulticomp —
-- возвращает nil: такой текст не трогается.
local function result_measure(text, tail)
if text == '' or text:find('\n', 1, true) or text:find('<%s*[Dd][Ii][Vv]') or text:find('<%s*[Tt][Aa][Bb][Ll][Ee]')
or text:find('fbmulticomp', 1, true) then
return nil
end
local words, gaps, lead = result_words(text)
if #words == 0 then
return nil
end
words[#words].len = words[#words].len + (tail or 0)
local kinds, phrases = result_structure(words, gaps)
-- Куски — слова вместе со склеенными с ними соседями: внутри куска строку не рвут.
-- gap — вид разделителя после куска. name — кусок начинает название: первое его слово,
-- не считая предлогов, — ссылка или слово с прописной буквы. Перед названием строку
-- рвать лучше, чем между другими словами: так не разрываются сочетания вроде
-- «основной этап», а название турнира целиком уходит на следующую строку.
local chunks = {}
local cur
for k, w in ipairs(words) do
if cur then
cur.len = cur.len + 1 + w.len
else
cur = {first = k, len = w.len}
end
if cur.name == nil and not glue_words[mw.ustring.lower(w.vis)] then
cur.name = w.parts[1].link ~= nil or mw.ustring.find(w.vis, '^%u') ~= nil
end
if kinds[k] ~= 'glue' then
cur.last, cur.gap = k, kinds[k]
chunks[#chunks + 1] = cur
cur = nil
end
end
-- Ширина текста без переносов: самая длинная из строк между тегами <br>.
local natural, line = 0, 0
for _, c in ipairs(chunks) do
line = line + (line > 0 and 1 or 0) + c.len
if c.gap == 'hard' or c.gap == nil then
natural = math.max(natural, line)
line = 0
end
end
return {words = words, gaps = gaps, lead = lead, kinds = kinds, phrases = phrases,
chunks = chunks, natural = natural}
end
-- Подбирает разрывы строк для кусков chunks: не больше rows строк, каждая не шире width знаков.
-- Разрыв после запятой или тега <br> ничего не стоит, перед названием — половину, между другими словами —
-- единицу; из раскладок с наименьшей ценой берётся самая ровная (с наименьшей суммой квадратов недобора до width).
-- Возвращает цену и набор разрывов — номера кусков, после которых начинается новая строка, — либо nil, если уложиться нельзя.
local function result_breaks(chunks, width, rows)
local n = #chunks
-- best[i][l] — лучшая раскладка кусков 1..i ровно в l строк, где кусок i завершает строку.
local best = {[0] = {[0] = {cost = 0, sq = 0}}}
for i = 1, n do
best[i] = {}
local len = -1
for a = i, 1, -1 do
-- Строка из кусков a..i. Тег <br> внутри строки оказаться не может.
if a < i and chunks[a].gap == 'hard' then
break
end
len = len + 1 + chunks[a].len
if len > width then
break
end
local step = 0
if a > 1 and chunks[a - 1].gap == 'word' then
step = chunks[a].name and 0.5 or 1
end
for l, prev in pairs(best[a - 1]) do
if l < rows then
local cand = {cost = prev.cost + step, sq = prev.sq + (width - len) ^ 2, from = a, lines = l}
local have = best[i][l + 1]
if not have or cand.cost < have.cost or (cand.cost == have.cost and cand.sq < have.sq) then
best[i][l + 1] = cand
end
end
end
end
end
local pick, pick_l
for l, cand in pairs(best[n]) do
if not pick or cand.cost < pick.cost or (cand.cost == pick.cost and cand.sq < pick.sq) then
pick, pick_l = cand, l
end
end
if not pick then
return nil
end
local breaks = {}
local i, l = n, pick_l
while i > 0 do
local cand = best[i][l]
if cand.from > 1 then
breaks[cand.from - 1] = true
end
i, l = cand.from - 1, cand.lines
end
return pick.cost, breaks
end
-- Подбирает ширину, при которой разобранный текст укладывается в rows строк. Сперва находится
-- наименьшая возможная ширина; затем среди ширин не более чем на треть больших выбирается та, при
-- которой цена разрывов (см. result_breaks) меньше всего: разрыв после запятой предпочтительнее,
-- и ради него столбец может стать немного шире — но не заметно. Если уложиться не удаётся ни при
-- какой ширине, возвращает m.natural.
local function result_fit(m, rows)
local lo = 0
for _, c in ipairs(m.chunks) do
lo = math.max(lo, c.len)
end
local narrowest
local pick, pick_cost
for width = lo, m.natural do
local cost = result_breaks(m.chunks, width, rows)
if cost then
narrowest = narrowest or width
if width * 0.75 > narrowest then
break
end
if not pick_cost or cost < pick_cost then
pick, pick_cost = width, cost
end
if cost == 0 then
break
end
end
end
return pick or m.natural
end
-- Рассчитывает раскладку столбца результатов. cells — ячейки столбца {text, rows, tail}, где rows —
-- число строк таблицы под ячейкой, tail — ширина знака сноски; header — текст заголовка столбца.
-- Возвращает для каждой ячейки набор разрывов строк для ppp.wrap_result_text либо nil, если текст
-- остаётся на усмотрение браузера. Ширины считаются в знаках: сравниваются тексты одного столбца
-- между собой, и переводить их в em незачем.
-- Ширину столбца задают заголовок и ячейки в одну строку таблицы: их текст в одну строку и есть
-- естественная ширина столбца на широком экране. Ячейка, объединяющая несколько строк таблицы,
-- вмещает столько же строк текста, не увеличивая высоту таблицы. Если её текст в одну строку шире
-- столбца, он распределяется по этим строкам: сперва по запятым, а между словами — лишь когда это
-- делает столбец заметно уже. Короткий текст (ppp.result_short_text) не разбивается совсем.
-- Разрывы в такой ячейке ставит модуль, а не браузер: число строк известно заранее, и модуль
-- выбирает места разрыва по правилам, а браузер переносит по мере заполнения строки и разорвал бы,
-- например, название турнира посередине.
function ppp.result_layout(cells, header)
local base = 0
if header and header ~= '' then
-- Заголовок полужирный: его буквы примерно на десятую шире.
local shown = mw.ustring.gsub(header, '<[^<>]*>', '')
shown = mw.ustring.gsub(shown, '%[%[[^%[%]|]*|([^%[%]]*)%]%]', '%1')
shown = mw.ustring.gsub(shown, '%[%[([^%[%]]*)%]%]', '%1')
shown = mw.ustring.gsub(shown, '\127[^\127]*\127', '[1]')
base = mw.ustring.len(mw.text.trim(shown)) * 1.1
end
local measured = {}
for i, cell in ipairs(cells) do
measured[i] = result_measure(cell.text or '', cell.tail)
if measured[i] and cell.rows == 1 then
base = math.max(base, measured[i].natural)
end
end
-- Ширина столбца: наибольшая из нужных. Ячейки, которым её мало, переносятся.
local width = base
for i, cell in ipairs(cells) do
local m = measured[i]
if m and cell.rows > 1 then
local need = m.natural
if need > math.max(base, ppp.result_short_text) then
need = math.min(m.natural, math.max(result_fit(m, cell.rows), ppp.result_short_text))
end
width = math.max(width, need)
end
end
-- Текст, который шире столбца лишь немного, переносить незачем:
-- столбец расширится на пару знаков, зато текст останется одной строкой.
local wide = width
for i, cell in ipairs(cells) do
local m = measured[i]
if m and cell.rows > 1 and m.natural <= width * 1.1 then
wide = math.max(wide, m.natural)
end
end
width = wide
local out = {}
for i, cell in ipairs(cells) do
local m = measured[i]
if m and cell.rows > 1 and m.natural > width then
local _, breaks = result_breaks(m.chunks, width, cell.rows)
out[i] = breaks
end
end
return out
end
-- Готовит текст ячейки столбца результатов к переносу. Сам перенос выполняет браузер —
-- по той ширине, которая достаётся столбцу на экране читателя, — а модуль решает, где строку рвать можно,
-- и склеивает остальные места знаком . Поэтому на широком экране текст остаётся в одну строку,
-- а на узком переносится, но только в разрешённых местах. Правила:
-- * основное место переноса — пробел после запятой или точки с запятой;
-- * часть текста между запятыми не разрывается, если она не длиннее ppp.result_phrase_limit;
-- * внутри более длинной части перенос возможен между словами, но предлог, союз, частица и число
-- не отрываются от следующего слова, а тире и знак сноски — от предыдущего;
-- * подпись вики-ссылки не разрывается, если слово с ней не длиннее того же предела;
-- * слово с дефисом, тире или косой чертой внутри — «2-й», «плей-офф», «3—5», «1/8» —
-- оборачивается классом st-nobr: браузер переносит после этих знаков, как после пробела.
-- breaks — разрывы строк от ppp.result_layout: на их месте ставится <br>. Тег <br> в самом тексте
-- остаётся принудительным переносом: разметку редактора модуль не меняет, а такое значение отмечает
-- ppp.manual_format. Текст со сложной разметкой возвращается как есть.
function ppp.wrap_result_text(text, breaks)
local m = result_measure(text or '')
if not m then
return text
end
local limit = ppp.result_phrase_limit
local phrase_of = {}
for _, ph in ipairs(m.phrases) do
for k = ph.first, ph.last do
phrase_of[k] = ph
end
end
local break_after = {}
if breaks then
for c_i in pairs(breaks) do
break_after[m.chunks[c_i].last] = true
end
end
local out = {m.lead}
for k, w in ipairs(m.words) do
-- Обёртка st-nobr — только у слова без тегов:
-- вокруг незакрытого тега она нарушила бы вложенность разметки.
local short = mw.ustring.len(w.vis) <= limit
local joined = short and mw.ustring.find(w.vis, '[^%s][%-‐‑–—/][^%s]')
if joined then
for _, part in ipairs(w.parts) do
if part.s:sub(1, 1) == '<' then
joined = false
end
end
end
if joined then
out[#out + 1] = '<span class="st-nobr">'
end
for _, part in ipairs(w.parts) do
local link = part.link
if link and short and link.label:find(' ', 1, true)
and not link.label:find('<', 1, true) then
out[#out + 1] = '[[' .. link.target .. '|' .. link.label:gsub(' ', ' ') .. ']]'
else
out[#out + 1] = part.s
end
end
if joined then
out[#out + 1] = '</span>'
end
local kind = m.kinds[k]
if kind == 'hard' then
out[#out + 1] = m.gaps[k]
elseif break_after[k] then
out[#out + 1] = '<br>'
elseif kind == 'phrase' then
out[#out + 1] = ' '
elseif kind == 'glue' or (kind == 'word' and phrase_of[k].len <= limit) then
out[#out + 1] = ' '
elseif kind == 'word' then
out[#out + 1] = ' '
end
end
return table.concat(out)
end
-- Числа в ячейках -----------------------------------------------------------------------
-- Записывает число x, как принято в русском тексте: с десятичной запятой, с нулём в целой части («0,917», а не «.917»)
-- и со знаком минуса вместо дефиса. prec — число знаков после запятой: значение округляется, половина —
-- в большую по модулю сторону, и недостающие знаки дописываются нулями («1,000»). Без prec целое выводится
-- без дробной части, дробное — со всеми своими знаками («2,5»). Ноль, получившийся при округлении отрицательного значения, выводится без минуса.
-- Запись задана здесь явно: вид дробей в таблице не зависит от того, как числа форматирует язык вики.
function ppp.decimal(x, prec)
local sign = (x < 0) and '−' or ''
x = math.abs(x)
local s
if prec then
local scale = 10 ^ prec
s = string.format('%.' .. prec .. 'f', math.floor(x * scale + 0.5) / scale)
else
s = tostring(x)
end
if tonumber(s) == 0 then
sign = ''
end
return sign .. (s:gsub('%.', ','))
end
-- Параметры, числа в которых бывают дробными или отрицательными: очки за исходы
-- (winpoints, OTlosspoints, goalpoints и т. п.), поправка и начальные очки, забитое и пропущенное —
-- в том числе дома и на выезде (hgf_, aga_ и т. п.).
local DECIMAL_PARAMS = {'^%a*points$', '^adjust_points_', '^startpoints_', '^[ha]?[gp][fa]_'}
-- Приводит числа, записанные по-русски, к виду, который читает tonumber: «0,5» — к «0.5»,
-- минус знаком «−» или «−» — к дефису. Иначе такое значение не прочиталось бы как число:
-- ничья за «0,5» очка молча считалась бы по умолчанию, поправка «−3» — нулём. Меняются
-- только параметры из DECIMAL_PARAMS и только значения, целиком состоящие из числа;
-- текст («—», «н/д») остаётся как есть. Args изменяется на месте.
function ppp.read_decimals(Args)
local fixed = {}
for k, v in pairs(Args) do
if type(k) == 'string' and type(v) == 'string' and v:find('[,&\226]') then
for _, pattern in ipairs(DECIMAL_PARAMS) do
if k:find(pattern) then
local s = mw.text.trim(v):gsub('^−%s*', '-'):gsub('^−%s*', '-')
s = s:gsub('^(%-?%d*),(%d+)$', '%1.%2')
if tonumber(s) then
fixed[k] = s
end
break
end
end
end
end
for k, v in pairs(fixed) do
Args[k] = v
end
end
-- Дописывает слева невидимые нули (класс st-pad), чтобы числа разной разрядности занимали одинаковую
-- ширину и выравнивались по правому краю столбца. Итоговая разрядность на единицу больше digits:
-- при 1 — два знака, при 2 — три, при 3 — четыре. Выравнивание не применяется, если digits или num не числовые,
-- digits вне этого ряда, число отрицательное или уже нужной разрядности. Дробное число
-- (половина игры отставания, очки при ничьей за пол-очка) записывается через ppp.decimal;
-- целое и текст возвращаются как есть.
-- Отрицательное значение передаётся без знака — num_align(- points, digits), — а минус вызывающий модуль
-- приписывает к результату: с отрицательным числом проверка n >= 0 не пройдёт, и выравнивание молча не применится.
function ppp.num_align(num, digits)
local n = tonumber(num)
if type(num) == 'number' and num ~= math.floor(num) then
num = ppp.decimal(num)
end
if n and tonumber(digits) then
digits = tonumber(digits)
if digits == 1 then
if n >= 0 and n < 10 then
return '<span class="st-pad">0</span>' .. num
end
elseif digits == 2 then
if n >= 0 and n < 10 then
return '<span class="st-pad">00</span>' .. num
elseif n >= 10 and n < 100 then
return '<span class="st-pad">0</span>' .. num
end
elseif digits == 3 then
if n >= 0 and n < 10 then
return '<span class="st-pad">000</span>' .. num
elseif n >= 10 and n < 100 then
return '<span class="st-pad">00</span>' .. num
elseif n >= 100 and n < 1000 then
return '<span class="st-pad">0</span>' .. num
end
end
end
return num
end
-- Возвращает разность забитого и пропущенного со знаком — «+3», 0, «−2», «+1,5» —
-- либо тире, если одно из чисел не задано (передаётся как '?').
function ppp.goal_diff(gfor, gaig)
if gfor == '?' or gaig == '?' then
return '—'
end
local d = gfor - gaig
if d > 0 then
return '+' .. ppp.decimal(d)
elseif d < 0 then
return ppp.decimal(d)
end
return d
end
-- Подпись столбца отставания от опорной команды (show_GB).
ppp.gb_header = '<abbr title="Отставание от опорной команды">Отст.</abbr>'
-- Возвращает текст ячейки отставания: тире у самой опорной команды (gb == nil) и при нулевом
-- отставании, иначе число игр отставания или опережение со знаком плюс, выровненные ppp.num_align
function ppp.gb_text(gb, digits)
if gb == nil or gb == 0 then
return '—'
elseif gb > 0 then
return ppp.num_align(gb, digits)
end
return '+' .. ppp.num_align(- gb, digits)
end
-- Сноски --------------------------------------------------------------------------------
-- Строит сноску к примечанию команды (note_ХХХ) в турнирной таблице и в шахматке.
-- Примечание — либо собственный текст own_text (is_own) под ключом entry_key, либо ссылка
-- на примечание другой команды: ref_key, её позиция ref_position и текст ref_text.
-- Если сноска с этим ключом уже создана (note_id_list) или её создаст строка владельца
-- в видимом диапазоне ii_start..ii_end, ставится ссылка на неё по имени;
-- иначе сноску с текстом создаёт текущая строка. note_id_list — накопитель идентификаторов,
-- изменяется на месте; rand_val дополняет идентификаторы (см. засев генератора в начале модуля).
-- Примечания к матчам шахматка строит своей функцией по тому же образцу; так же устроена ppp.hth.
function ppp.build_note(frame, rand_val, note_id_list, ii_start, ii_end, entry_key, own_text, is_own, ref_key, ref_position, ref_text)
local note_id, note_string
if is_own then
note_id = '"table_note_' .. entry_key .. rand_val .. '"'
note_id_list[entry_key] = note_id
note_string = frame:expandTemplate{title = 'efn',
args = {group = 'lower-alpha', name = note_id, own_text}
}
else
-- Сноска уже создана либо её создаст строка владельца в видимом диапазоне —
--достаточно сослаться на неё по имени.
if note_id_list[ref_key] or (ref_position and ref_position >= ii_start and ref_position <= ii_end) then
note_id = '"table_note_' .. ref_key .. rand_val .. '"'
note_string = frame:extensionTag{name = 'ref',
args = {group = 'lower-alpha', name = note_id}
}
else
note_id = '"table_note_' .. ref_key .. rand_val .. '"'
note_id_list[ref_key] = note_id
note_string = frame:expandTemplate{title = 'efn',
args = {group = 'lower-alpha', name = note_id, ref_text}
}
end
end
return note_string
end
-- Строит сноски к примечанию о личных встречах команды team_code_ii (параметр hth_ХХХ).
-- Значение hth_local может быть:
-- а) собственным текстом примечания;
-- б) кодом другой команды с собственным примечанием hth_ — тогда обе команды ссылаются на одну сноску;
-- в) списком таких кодов через запятую — если у каждого кода из списка есть своё примечание hth_;
-- иначе весь список считается текстом примечания (вариант «а»).
-- Сноски строятся только в полной таблице (full_table). Сноску чужого примечания создаёт строка владельца,
-- если он в видимом диапазоне ii_start..ii_end, иначе текущая строка (как в ppp.build_note). hth_id_list —
-- накопитель идентификаторов, изменяется на месте; team_list — позиции команд по кодам; rand_val дополняет идентификаторы.
-- Возвращает таблицу: str — текст сносок, list — hth_id_list, notes_exist — признак «в таблице есть примечания».
function ppp.hth(frame, Args, full_table, hth_id_list, hth_local, notes_exist, team_list, team_code_ii, ii_start, ii_end, rand_val)
local hth_string = ''
if hth_local and full_table then
notes_exist = true
-- Пробует разобрать hth_local как список кодов (вариант «в»): признаётся списком,
-- только если элементов больше одного и у каждого есть своё hth_-примечание.
local hth_local_table = mw.text.split(hth_local, '%s*,%s*')
local multiref = 1
if #hth_local_table > 1 then
for _, hth_loc in ipairs(hth_local_table) do
multiref = multiref * (Args['hth_' .. hth_loc] and 1 or 0)
end
else
multiref = 0
end
-- Списком не оказалось: дальше обрабатывается одно значение целиком.
if multiref < 1 then
hth_local_table = {hth_local}
end
for _, hth_loc in ipairs(hth_local_table) do
local hth_ref_text = Args['hth_' .. hth_loc]
local hth_id
if not hth_ref_text then
-- Вариант «а»: hth_loc — сам текст примечания. Создаёт сноску
-- и запоминает её идентификатор за текущей командой.
hth_id = '"table_hth_' .. team_code_ii .. rand_val .. '"'
hth_id_list[team_code_ii] = hth_id
hth_string = hth_string .. frame:expandTemplate{title = 'efn',
args = {group = 'lower-alpha', name = hth_id, hth_loc}
}
else
-- Варианты «б» и «в»: hth_loc — код команды с собственным примечанием.
local hth_local_num = team_list[hth_loc] or -1
if hth_id_list[hth_loc] or (hth_local_num >= ii_start and hth_local_num <= ii_end) then
-- Сноска уже создана либо будет создана в пределах видимого диапазона —
-- достаточно сослаться на неё по тому же идентификатору.
hth_id = '"table_hth_' .. hth_loc .. rand_val .. '"'
hth_string = hth_string .. frame:extensionTag{name = 'ref',
args = {group = 'lower-alpha', name = hth_id}
}
else
-- Команда-владелец примечания вне видимого диапазона: сноску создаёт
-- текущая строка, запоминая идентификатор за кодом владельца.
hth_id = '"table_hth_' .. hth_loc .. rand_val .. '"'
hth_id_list[hth_loc] = hth_id
hth_string = hth_string .. frame:expandTemplate{title = 'efn',
args = {group = 'lower-alpha', name = hth_id, hth_ref_text}
}
end
end
end
end
return {str = hth_string, list = hth_id_list, notes_exist = notes_exist}
end
-- Обозначения статуса -------------------------------------------------------------------
-- Собирает обозначения статуса команд, которыми пользуются и подмодули стиля
-- (буквы рядом с названием команды), и нижний колонтитул таблицы (легенда). Возвращает:
-- code — словарь «буква → текст статуса»: общий словарь, дополненный status_overrides
-- и текстами status_text_<буква>; буква без описания получает «?»;
-- called — словарь «буква → использована ли»: здесь у всех букв false, отмечают их вызывающие
-- модули при построении строк, и от этого зависит состав легенды;
-- letters — буквы в порядке вывода легенды: сначала перечисленные в status_order, затем
-- остальной латинский алфавит, без повторов;
-- position — место букв относительно названия: 'after' либо, при status_pos=before, 'before'.
-- Необязательный status_overrides — словарь {буква = текст},
-- который заменяет или дополняет отдельные буквы общего словаря.
function ppp.status(Args, status_overrides)
-- Общий словарь обозначений, единый для всех стилей.
local status_code = {
A = 'Команда вышла в следующий раунд',
C = 'Чемпион',
D = 'Команда дисквалифицирована',
E = 'Команда выбыла из розыгрыша',
G = 'Гости',
H = 'Хозяева',
O = 'Победитель плей-офф',
P = 'Повышение команды (выход в дивизион уровнем выше)',
Q = 'Команда квалифицировалась в указанную стадию',
R = 'Выбывание команды (в дивизион уровнем ниже)',
S = 'Победитель регулярного сезона',
T = 'Команда квалифицировалась в турнир, но его конкретная стадия пока неизвестна'}
if type(status_overrides) == 'table' then
for letter, text in pairs(status_overrides) do
status_code[letter] = text
end
end
-- Буквы из status_order выводятся в легенде первыми, поэтому ставятся впереди алфавита;
-- повторы устраняются ниже, в общем проходе по буквам.
local status_letters = (Args['status_order'] or '') .. 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
-- Расположение букв статуса: после названия команды, если не задано иное.
local status_position = 'after'
if string.lower(Args['status_pos'] or '') == 'before' then
status_position = 'before'
end
-- Один проход по буквам заводит для каждой признак «использована», подставляет заглушку
-- для букв без описания, убирает повторное вхождение буквы, уже названной в status_order,
-- и применяет текст status_text_<буква>. Буквы обычно латинские, и тогда строка перебирается побайтово:
-- так проход обходится без mw.ustring, который на каждом шаге заново разбирал бы всю строку.
local status_called = {}
local ascii = not status_letters:find('[\128-\255]')
for l in (ascii and status_letters:gmatch('.') or mw.text.gsplit(status_letters, '')) do
status_called[l] = false
status_code[l] = status_code[l] or '?'
if ascii then
status_letters = status_letters:gsub('(' .. l .. '.-)' .. l, '%1')
else
status_letters = mw.ustring.gsub(status_letters, '(' .. l .. '.-)' .. l, '%1')
end
if Args['status_text_' .. l] then
status_code[l] = Args['status_text_' .. l]
end
end
return {code = status_code, called = status_called, letters = status_letters, position = status_position}
end
-- Стили и шапка таблицы -----------------------------------------------------------------
-- Возвращает подключение таблицы стилей семейства [[Шаблон:Sports table/styles.css]].
-- Подключение держится здесь, а не у каждого вызывающего модуля: имя страницы должно быть в одном месте.
-- Вызывают [[Модуль:Sports table]] и [[Модуль:Sports results]].
-- Если за результатом идёт вики-таблица, между ними обязателен перевод строки: разметка «{|»
-- разбирается, только когда стоит в начале строки, а тег стилей это начало занимает, —
-- таблица тогда выводится как обычный текст вместе с именами классов. Перед HTML-тегом перевод строки
-- не нужен. Повторные вызовы безвредны: MediaWiki подключает один и тот же файл однократно.
function ppp.styles()
return mw.getCurrentFrame():extensionTag{
name = 'templatestyles', args = {src = 'Sports table/styles.css'}
}
end
-- Добавляет в накопитель tt заголовок одного столбца. tt — таблица {count, tab_text}: число
-- столбцов и массив строк разметки; счётчик увеличивается, строка дописывается, возвращается тот же
-- tt. width подставляется в свойство width; при width == 'auto' ширина не задаётся. Число без
-- единиц измерения считается пикселями — так ширину пишут редакторы в параметрах вроде teamwidth —
-- и переводится в em из расчёта 14px = 1em; модули семейства передают ширину уже в em.
-- Необязательный extra — дополнительные свойства стиля, каждое с точкой с запятой в конце.
-- Передаётся отдельно от ширины: приписанный к ней, он сорвал бы перевод пикселей в em,
-- а без разделителя слился бы с шириной в одно неверное свойство.
function ppp.colhead(tt, width, text, extra)
local head_string
extra = extra or ''
if width == 'auto' then
head_string = '! scope="col"' .. (extra ~= '' and ' style="' .. extra .. '"' or '')
.. ' |' .. text .. '\n'
else
if mw.ustring.match(width, '^[0-9]+$') then
width = (tonumber(width) / 14) .. 'em'
end
head_string = '! scope="col" style="width:' .. width .. ';' .. extra .. '" |' .. text .. '\n'
end
tt.count = tt.count + 1
table.insert(tt.tab_text, head_string)
return tt
end
-- Открывает турнирную таблицу для подмодуля стиля: подбирает ширины столбцов, выводит начало разметки
-- (с классом sortable при sortable_table) и подпись из title или table_header, затем первые столбцы заголовка —
-- позицию (если задан pos_label), группу (в полной таблице при group_col) и команду с подписью team_header и ссылками VTE_text.
-- Возвращает накопитель заголовков (см. ppp.colhead) и ширины столбцов {team, num, ratio, points}.
-- Ширины вычисляются по данным каждой таблицы, поэтому задаются заголовкам встроенным стилем:
-- ширину столбца определяет его заголовок. Названия — teamwidth либо ширина по самому длинному названию
-- (ppp.measure_team_width); числовые столбцы — одна ширина на все; соотношение и процент мячей — своя, под дробь;
-- очки — pointswidth либо своя, под сноски и трёхзначные числа.
function ppp.open_table(t, Args, pos_label, group_col, VTE_text, full_table)
local yesno = require('Модуль:Yesno')
local w = {
team = Args['teamwidth'] or ppp.measure_team_width(Args),
num = ppp.measure_num_width(Args),
ratio = ppp.measure_ratio_width(),
points = Args['pointswidth'] or ppp.measure_points_width(Args)
}
local sort_text = yesno(Args['sortable_table'] or 'no') and ' sortable' or ''
table.insert(t, '{| class="wikitable st-table' .. sort_text .. '"\n')
if Args['title'] or Args['table_header'] then
table.insert(t, '|+ ' .. (Args['title'] or Args['table_header']) .. '\n')
end
local tt = {count = 0, tab_text = t}
if pos_label ~= nil then
tt = ppp.colhead(tt, w.num, pos_label)
end
if full_table and group_col then
tt = ppp.colhead(tt, w.num, Args['group_header'] or '<abbr title="Группа">Гр.</abbr>')
end
tt = ppp.colhead(tt, w.team, (Args['team_header'] or 'Команда') .. VTE_text)
return tt, w
end
-- Дописывает в заголовок готовую ячейку столбца результатов results_header_txt —
-- только в полной таблице (full_table).
function ppp.close_header(tt, full_table, results_header_txt)
if full_table then
tt.count = tt.count + 1
table.insert(tt.tab_text, results_header_txt)
end
return tt
end
-- Блок CSS палитры: точка входа для #invoke ---------------------------------------------
-- Порядок цветов в блоке CSS. Задан явно, а не обходом ppp.colors: порядок обхода pairs() в Lua
-- не гарантирован, и блок мог бы менять порядок от вызова к вызову — сравнить его с тем,
-- что лежит на странице стилей, стало бы нельзя. white1 и пустое имя сюда не входят:
-- собственной заливки у них нет.
local palette_order = {
'green1', 'green2', 'green3', 'green4',
'blue1', 'blue2', 'blue3', 'blue4',
'yellow1', 'yellow2', 'yellow3', 'yellow4',
'violet1', 'violet2', 'violet3', 'violet4',
'red1', 'red2', 'red3', 'red4',
'black1', 'black2', 'black3', 'black4',
'orange1', 'orange2',
'gold1', 'silver1', 'bronze1',
'accent', 'neutral'
}
-- Заливки служебных элементов таблиц: селекторы и цвет — имя палитры либо пара значений
-- для светлой и тёмной темы. ppp.print_styles выводит их вместе с палитрой (вид table),
-- и значения не переписываются в таблицу стилей руками.
-- tr.st-split — полоса между группами позиций (splitN в [[Модуль:Sports table]]),
-- цвет ppp.split_colors;
-- td.sr-void — служебная клетка шахматки: команда против самой себя или скрытый результат
-- ([[Модуль:Sports results]]); в самостоятельной шахматке (sr-table) и в клетках
-- результатов турнирной таблицы при show_matches=yes (st-table). Цвет — neutral,
-- тот же, которым карточка матча помечает несостоявшуюся игру.
-- Селектор с именем элемента, как у цветов палитры, а прочие свойства этих элементов таблица
-- стилей задаёт селектором без него: у двух правил одного селектора stylelint отметил бы повтор.
local service_fills = {
{{'.st-table tr.st-split'}, ppp.split_colors},
{{'.st-table td.sr-void', '.sr-table td.sr-void'}, 'neutral'},
}
-- Возвращает приставку классов заливки для вида kind: 'f-' у карточек матча (box, collapsible),
-- 'st-' у таблиц. У каждой части семейства свои имена классов, и соответствие между ними держится в одном месте.
function ppp.class_prefix(kind)
return (kind == 'box' or kind == 'collapsible') and 'f-' or 'st-'
end
-- Строит блок CSS с палитрой для страницы стилей; вызывается через #invoke на странице документации.
-- Страница стилей не проходит через вики-разметку, и значения из модуля туда не подставить —
-- там должны стоять готовые. Поэтому блок собирается здесь, а редактор копирует его целиком:
-- ручной шаг сводится к замене блока, а не к набору значений.
-- Первый параметр — вид, от которого зависит, на каком элементе стоит класс заливки:
-- table — по умолчанию: ячейки турнирной таблицы и шахматки; блок для [[Шаблон:Sports table/styles.css]];
-- box — корневой элемент карточки матча; блок для [[Шаблон:Football box/styles.css]];
-- collapsible — внутренняя таблица сворачиваемой карточки; блок для [[Шаблон:Football box collapsible/styles.css]].
-- Для каждого цвета выводится правило светлой темы и два правила тёмной (ppp.colors_dark):
-- для тёмной темы, выбранной в настройках (html.skin-theme-clientpref-night), и для темы по настройке системы
-- (html.skin-theme-clientpref-os). Для таблиц за цветами палитры идут заливки служебных элементов (service_fills).
-- Результат — блок syntaxhighlight, готовый к копированию.
function ppp.print_styles(frame)
local kind = mw.text.trim((frame and frame.args and frame.args[1]) or 'table')
local roots
if kind == 'box' then
roots = {'.footballbox'}
elseif kind == 'collapsible' then
-- Класс цвета ставится внутренней таблице, а не корневому блоку: корень несёт
-- только имя карточки, и селектор из двух классов на одном элементе не совпал бы.
roots = {'.footballbox-collapsible .fcevent'}
else
-- Класс заливки стоит на ячейке, а не на строке: при show_matches в ту же строку
-- дописываются клетки шахматки, и заливка строки просвечивала бы через них.
-- Ячейка названия команды — заголовок строки, отсюда отдельный th.
roots = {'.st-table td', '.st-table th', '.sr-table td'}
end
local function rules(prefix, theme, indent)
local colors = (theme == 'dark') and ppp.colors_dark or ppp.colors
local out = {}
for _, name in ipairs(palette_order) do
local sel = {}
for _, root in ipairs(roots) do
sel[#sel + 1] = indent .. prefix .. root .. '.' .. ppp.class_prefix(kind) .. name
end
out[#out + 1] = table.concat(sel, ',\n') .. ' {'
out[#out + 1] = indent .. '\t' .. 'background: ' .. colors[name] .. ';'
out[#out + 1] = indent .. '}'
out[#out + 1] = ''
end
if kind ~= 'box' and kind ~= 'collapsible' then
for _, sf in ipairs(service_fills) do
local sel = {}
for _, selector in ipairs(sf[1]) do
sel[#sel + 1] = indent .. prefix .. selector
end
local col = sf[2]
col = (type(col) == 'table') and col[theme] or colors[col]
out[#out + 1] = table.concat(sel, ',\n') .. ' {'
out[#out + 1] = indent .. '\t' .. 'background: ' .. col .. ';'
out[#out + 1] = indent .. '}'
out[#out + 1] = ''
end
end
return table.concat(out, '\n')
end
local css = rules('', 'light', '')
.. '\n@media screen {\n'
.. rules('html.skin-theme-clientpref-night ', 'dark', '\t')
.. '}\n\n@media screen and (prefers-color-scheme: dark) {\n'
.. rules('html.skin-theme-clientpref-os ', 'dark', '\t')
.. '}\n'
return frame:extensionTag{name = 'syntaxhighlight', content = css, args = {lang = 'css'}}
end
return ppp