Открыть меню
Открыть персональное меню
Вы не представились системе
Your IP address will be publicly visible if you make any edits.

Модуль:Инвентарный слот: различия между версиями

Материал из Create Wiki
м (1 версия импортирована)
(перевод ряда функций gsub и match на mw.ustring)
Строка 1: Строка 1:
-------------------------------------------------------------------
----------------------------------------------------------------------------
--- Модуль для отображения инвентарных слотов в Minecraft Wiki.
--- Модуль для отображения инвентарных слотов в Minecraft Wiki.
-------------------------------------------------------------------
--- ВНИМАНИЕ: Любые изменения в этом модуле отразятся на тысячах статей!
----------------------------------------------------------------------------


local p = {}
local p = {}


-- Список приставок к названиям, обрабатываемых другими модулями.
-------------------------------------
-- Так будет легче, например, убирать их из целей ссылок.
-- Глобально экспортируемые данные
-- ВНИМАНИЕ: указывайте все варианты склонения по родам и числам.
-------------------------------------
 
-- Данные по интернационализации и локализации
local i18n = {
    -- Стандартные наименования файлов
    filename = 'Grid $1',
    --legacyFilename = 'Grid $1',
    commonsFilename = 'Invicon $1', -- для файлов из общего хранилища
 
    -- Ссылка на статью, касающуюся модификации
    modLink = '$1/$2',
 
    -- Модули, отвечающие за псевдонимы
    moduleAliases = [[Модуль:Инвентарный слот/Псевдонимы]],
    moduleModAliases = [[Модуль:Инвентарный слот/Псевдонимы/$1]],
 
    -- Спрайтовые модули
    moduleSprite = [[Модуль:Спрайт]],
    moduleInvData = [[Модуль:ИнвСпрайт]],
    moduleModData = [[Модуль:ИнвСпрайт/$1]],
 
    -- Служебные модули
    moduleRandom = [[Модуль:Случайные числа]],
    moduleUtils = [[Модуль:Специальные утилиты]],
    moduleMods = [[Модуль:Модификации]],
 
    -- Начальные формы приставок к названиям некоторых псевдонимов.
    -- Для использования с модулем «Склонение прилагательных»
    prefixes = {
        any = "любой",
        matching = "соответствующий",
        damaged = "повреждённый",
        unwaxed = "невощёный"
    },
 
    -- Выражения для поиска вышеуказанных приставок.
    -- Так будет легче, например, убирать их из целей ссылок.
    -- Именно их, а не prefixes, следует использовать при переводе
    -- модулей с англовики в функциях gsub и match (причём с mw.ustring).
    prefixesMatch = {
        any = 'Люб[оаы][йяе]',
        matching = 'Соответствующ[иае][йяе]',
        damaged = 'Повреждённ[ыао][йяе]',
        unwaxed = '[Нн]евощён[ыао][йяе]',
    },
 
    -- Список суффиксов
    suffixes = {
        be = 'BE',
        lce = 'LCE',
    }
}
p.i18n = i18n
 
-- Для совместимости
p.prefixes = {
p.prefixes = {
'Любой', 'Любая', 'Любое', 'Любые',
    'Любой', 'Любая', 'Любое', 'Любые',
'Повреждённый', 'Повреждённая', 'Повреждённое', 'Повреждённые', -- использование Ё обязательно
    'Повреждённый', 'Повреждённая', 'Повреждённое', 'Повреждённые',
'Соответствующий', 'Соответствующая', 'Соответствующее', 'Соответствующие'
    'Соответствующий', 'Соответствующая', 'Соответствующее', 'Соответствующие',
    'Невощёный', 'Невощёная', 'Невощёное', 'Невощёные'
}
}
p.modAliases = mw.loadData("Модуль:Модификации")


p.modAliases = mw.loadData("Модуль:Модификации")
-----------------------------------------
-- Внутренние глобальные данные модуля
-----------------------------------------
 
local modIds = {}
local modAliases = {}
 
local random = require(i18n.moduleRandom).random
local tryLoadData = require(i18n.moduleUtils).tryLoadData
local mergeList = require(i18n.moduleUtils).mergeList
local sprite = require(i18n.moduleSprite).sprite
 
local aliases = mw.loadData(i18n.moduleAliases)
local modNames = mw.loadData(i18n.moduleMods)
local ids = mw.loadData(i18n.moduleInvData)["IDы"]
 
local pageName = mw.title.getCurrentTitle().text
 
-----------------------
-- Служебные функции
-----------------------
 
-- Разбор строки, разделённой точками с запятой.
-- Учитывает, что точка с запятой может быть внутри квадратных скобок.
local function splitOnUnenclosedSemicolons(text)
    local semicolon, lbrace, rbrace = (";[]"):byte(1, 3)
    local nesting = false
    local splitStart = 1
    local frameIndex = 1
    local frames = {}
 
    for index = 1, text:len() do
    local byte = text:byte(index)
        if byte == semicolon and not nesting then
            frames[frameIndex] = text:sub(splitStart, index - 1)
            frameIndex = frameIndex + 1
            splitStart = index + 1
        elseif byte == lbrace then
            assert(not nesting, "Ошибка синтаксиса: чрезмерные квадратные скобки")
            nesting = true
        elseif byte == rbrace then
            assert(nesting, "Ошибка синтаксиса: несбалансированные квадратные скобки")
            nesting = false
        end
    end
    assert(not nesting, "Ошибка синтаксиса: несбалансированные квадратные скобки")
    frames[frameIndex] = text:sub(splitStart, text:len())
 
    for index = 1, #frames do
        frames[index] = (frames[index]:gsub("^%s+", ""):gsub("%s+$", "")) -- быстрее mw.text.trim
    end
 
    return frames
end
p.splitOnUnenclosedSemicolons = splitOnUnenclosedSemicolons -- для совместимости
 
-- Простая рекурсивная копия значений таблицы
local function cloneTable(origTable)
    local newTable = {}
    for k, v in pairs(origTable) do
        if type(v) == "table" then
            v = cloneTable(v)
        end
        newTable[k] = v
    end
    return newTable
end
 
-- Отделяет расширение от названия фрейма, если оно есть.
-- Возвращяет название без расширения и либо само расширение,
-- либо png в случае его отсутствия.
local function splitExtension(name)
    if name:match('%.gif$') or name:match('%.png$') then
        -- расширения англоязычные, представляют собой ASCII-символы,
        -- поэтому обычные match и sub безопасны
        return name:sub(0, -5), name:sub(-3)
    elseif name:match('%.webp$') then
        return name:sub(0, -6), 'webp'
    else
        return name, 'png'
    end
end
 
-- Создаёт HTML-код для предмета.
-- args является таблицей аргументов, принятой самим модулем
local function makeItem(frame, args)
    -- Создание HTML-элемента
    local item = mw.html.create('span'):addClass('invslot-item')
    if args["классизобр"] then
        item:addClass(args["классизобр"])
    end
    if args["стильизобр"] then
        item:cssText(args["стильизобр"])
    end
 
    if frame.name == '' then
        -- пустой фрейм
        return item
    end
 
    local category -- категории
 
    -- Параметры фрейма
    local title = frame.title or mw.text.trim(args["назв"] or '')
    local mod = frame.mod
    local name = frame.name or ''
    local num = frame.num
    local description = frame.text
    local en_name = frame.english
 
    -- Построение изображения
    local img, idData, extension
 
    -- Модификация?
    if mod then
        -- Пытаемся загрузить список ИнвСпрайтов для модификации
        modData = modIds[mod]
        if not modData then
            modData = tryLoadData(i18n.moduleModData:gsub("%$1", mod))
            if modData then
                local idsOverride = modData["настройки"]["списокID"]
                if idsOverride then
                    modData = tryLoadData("Модуль:" .. idsOverride)["IDы"]
                else
                    modData = modData["IDы"]
                end
            end
            modIds[mod] = modData
        end
 
        if modData and modData[name] then -- из ИнвСпрайта
            idData = modData[name]
            en_name = en_name or idData["en"] -- для совместимости
        else -- из Grid-файла
            name, extension = splitExtension(name)
            img = i18n.filename:gsub("%$1", name .. ' (' .. mod .. ')') .. '.' .. extension
        end
        -- Конец обработки иконок из модификаций
    elseif type(frame.commons) == 'string' then
        -- Ванильный Invicon-файл из общего хранилища
        img, extension = splitExtension(frame.commons)
        img = i18n.commonsFilename:gsub("%$1", img) .. '.' .. extension
    elseif en_name and frame.commons ~= false then
        -- Автоопределение Invicon-файла по англоязычному названию
        en_name, extension = splitExtension(en_name)
        img = i18n.commonsFilename:gsub("%$1", en_name) .. '.' .. extension
    elseif ids[name] then
        -- Ванильный ИнвСпрайт
        idData = ids[name]
    else
        -- Ванильный Grid-файл, загруженный локально
        name, extension = splitExtension(name)
        img = i18n.filename:gsub("%$1", name) .. '.' .. extension
    end
 
    -- К данному моменту задана переменная:
    -- 1) idData, если иконка взята из таблицы спрайтов, либо
    -- 2) img, если иконка берётся из Grid-файла.
 
    -- Формирование цели ссылки
    local link = args["ссылка"] or ''
    if link == '' then -- поведение по умолчанию
        if mod then
            link = i18n.modLink:gsub('%$1', mod):gsub('%$2', name)
        else
            -- Убираем префикс повреждённых предметов
            link = mw.ustring.gsub(name, '^'.. i18n.prefixesMatch.damaged .. ' ', '')
 
            -- Убираем суффиксы изданий
            for _, suffix in pairs(i18n.suffixes) do
                link = mw.ustring.gsub(name, ' ' .. suffix .. '$', '')
            end
        end
    elseif mw.ustring.lower(link) == "нет" then
        -- Отключение ссылки
        link = nil
    end
    if link and mw.ustring.gsub(link, "^%l", mw.ustring.upper) == pageName then
        -- отключаем ссылку на текущую страницу
        link = nil
    end
 
    -- Форматирование заголовка
    local formattedTitle, plainTitle
 
    if title == '' then
        plainTitle = name
    elseif mw.ustring.lower(title) ~= "нет" then
        -- Временное преобразование экранированных служебных символов
        plainTitle = title:gsub('\\\\', '\'):gsub('\\&', '&')
 
        -- Очищаем «простой» заголовок от форматирования
        local formatPattern = '&[0-9a-fk-or]'
        if plainTitle:match(formatPattern) then
            formattedTitle = title
            plainTitle = plainTitle:gsub(formatPattern, '')
        end
 
        if plainTitle == '' then
            plainTitle = name
        else
            -- Превращаем экранированные символы в финальную форму
            plainTitle = plainTitle:gsub('\', '\\'):gsub('&', '&')
        end
    elseif link then
        if img then
            formattedTitle = ''
        else
            plainTitle = ''
        end
    end
 
    -- Добавляем атрибуты для minetip
    item:attr{
        ['data-minetip-title'] = formattedTitle,
        ['data-minetip-text'] = description,
        ['data-modinfo-text'] = mod,
        ['data-minetip-lowtitle'] = en_name
    }
 
    -- Иконка
    if img then
        -- Grid-файл
 
        -- & экранируется повторно, так как mw.html считает атрибуты
        -- простым текстом, а MediaWiki — нет.
        local escapedTitle = (plainTitle or ''):gsub('&', '&')
        item:addClass('invslot-item-image')
            :wikitext('[[Файл:', img, '|32x32px|link=', link or '', '|', escapedTitle, ']]')
    else
        -- ИнвСпрайт
        local scale = args["масштаб"] or 1
 
        if link then -- начало ссылки
            item:wikitext('[[', link, '|')
        end
 
        local image, spriteCat
        local dataPage = mod and ("ИнвСпрайт/" .. mod) or "ИнвСпрайт"
 
        image, spriteCat = sprite{
            ["масштаб"] = scale, ["данныеID"] = idData, ["назв"] = plainTitle,
            ["данные"] = dataPage
        }
        item:node(image)
        category = spriteCat
    end
 
    -- Размер стопки
    if num and num > 1 and num < 1000 then
        if img and link then
            -- Открываем ссылку, если используется Grid-файл.
            -- Для ИнвСпрайта ссылка уже была открыта ранее.
            item:wikitext('[[', link, '|')
        end
 
        local number = item:tag('span')
          :addClass('invslot-stacksize')
          :attr{ title = plainTitle }
          :wikitext(num)
        if args["стильцифр"] then
            number:cssText(args["стильцифр"])
        end
 
        if img and link then
            -- Закрываем ссылку, если используется Grid-файл
            item:wikitext(']]')
        end
    end
 
    if idData and link then
        -- Закрываем ссылку, если используется ИнвСпрайт
        item:wikitext(']]')
    end
 
    -- Добавляем категории
    item:wikitext(category)


-- Служебная функция: Разбор строки, разделённой точками с запятой
    -- Возвращаем предмет
function p.splitOnUnenclosedSemicolons(text)
    return item
local semicolon, lbrace, rbrace = (";[]"):byte(1, 3)
local nesting = false
local splitStart = 1
local frameIndex = 1
local frames = {}
for index = 1, text:len() do
local byte = text:byte(index)
if byte == semicolon and not nesting then
frames[frameIndex] = text:sub(splitStart, index - 1)
frameIndex = frameIndex + 1
splitStart = index + 1
elseif byte == lbrace then
assert(not nesting, "Ошибка синтаксиса: чрезмерные квадратные скобки")
nesting = true
elseif byte == rbrace then
assert(nesting, "Ошибка синтаксиса: несбалансированные квадратные скобки")
nesting = false
end
end
assert(not nesting, "Ошибка синтаксиса: несбалансированные квадратные скобки")
frames[frameIndex] = text:sub(splitStart, text:len())
for index = 1, #frames do
frames[index] = (frames[index]:gsub("^%s+", ""):gsub("%s+$", "")) -- faster mw.text.trim
end
return frames
end
end


--- Создание слота
----------------------------------
-- Общедоступные функции модуля
----------------------------------
 
-- Создаёт слот. Служит основной точкой входа
function p.slot(f)
function p.slot(f)
    -- Получение аргументов
    local args = f.args or f
    if f == mw.getCurrentFrame() and args[1] == nil then
        args = f:getParent().args
    end


--- Получение аргументов
    if not args["обработанный"] then
local args = f.args or f
    -- Нормализация первого аргумента
if f == mw.getCurrentFrame() and args[1] == nil then
        args[1] = mw.text.trim(args[1] or '')
args = f:getParent().args
    end
end
    -- Если задан аргумент «обработан», то подразумевается, что
    -- args[1] — это список фреймов в табличном формате.
-- Первый аргумент
args[1] = mw.text.trim(args[1] or '')
--- Псевдонимы оригинальной игры
local aliases = mw.loadData('Модуль:Инвентарный слот/Псевдонимы')


     --- Проверка и замена всех фреймов на псевдонимы
     -- Модификация по умолчанию
    local defaultMod = mw.text.trim(args["мод"] or '')
    if defaultMod == '' then
        defaultMod = nil
    elseif modNames[defaultMod] then
        defaultMod = modNames[defaultMod]
    end
   
    if defaultMod then
        defaultMod = defaultMod:gsub('_', ' ')
        defaultMod = mw.ustring.gsub(defaultMod, "^%l", mw.ustring.upper)
    end
 
    -- Сохраняем список фреймов в табличном формате
    local frames
    if args["обработанный"] then
        frames = args[1]
    elseif args[1] ~= '' then
        local randomise = args.class == 'invslot-large' and "никогда" or nil
        frames = p.parseFrameText(args[1], randomise, false, defaultMod)
    end
 
    -- Слот анимированный?
    local animated = frames and #frames > 1
 
    -- Построение HTML-элемента
    local body = mw.html.create('span'):addClass('invslot'):css{
        ['vertical-align'] = args["выравн"]
    }
 
    if animated then
        -- включаем анимацию
        body:addClass('animated')
    end
 
    -- Масштабирование слота
    local scale = tonumber(args["масштаб"]) or 1
    if scale ~= 1 then
        local imgSize = 32 * scale
        body:css{ width = imgSize .. "px", height = imgSize .. "px" }
        if scale == 0.5 then
            -- компенсация высоты для уменьшенного слота?
            body:css{ top = '-1px' }
        end
    end
 
    -- Добавляем дополнительные классы и стили
    if args["класс"] then
        body:addClass(args["класс"])
    end
    if args["стиль"] then
        body:cssText(args["стиль"])
    end
 
    -- Добавляем фон умолчания
    if (args["умолчание"] or "") ~= "" then
        body:addClass(args["умолчание"] .. '-slot')
    end
 
    -- Фоновый спрайт умолчания для GregTech (временная реализация)
    local backID = args["Фон ИД"]
    if defaultMod and defaultMod:match("GregTech") and backID then
        local pos = backID - 1
        local left = (pos % 10) * 32
        local top = math.floor(pos / 10) * 32
 
        body:addClass('gt-invslot'):css{
            ['background-size'] = '320px auto',
            ['background-position'] = '-' .. left .. 'px -' .. top .. 'px'
        }
    end
 
    if not frames then
        -- Вырожденный случай (нет фреймов)
        return tostring(body)
    end
 
    -- Активный фрейм для анимации
    local activeFrame = frames["рандомизация"] == true and random(#frames) or 1
 
    -- Добавление фреймов
    for i, frame in ipairs(frames) do
        local item
 
        -- Проверка на наличие подфреймов
        if frame[1] then
            -- Контейнер подфреймов
            item = body:tag('span'):addClass('animated-subframe')
            local subActiveFrame = frame["рандомизация"] and random(#frame) or 1
 
            for sI, sFrame in ipairs(frame) do
                -- Добавляем подфрейм
                local sItem = makeItem(sFrame, args)
                item:node(sItem)
 
                if sI == subActiveFrame then
                    -- задаём активным
                    sItem:addClass('animated-active')
                end
            end
        else
            -- Обычный фрейм
            item = makeItem(frame, args)
            body:node(item)
        end
 
        if i == activeFrame and animated then
            -- задаём активным
            item:addClass('animated-active')
        end
    end
 
    -- Возвращаем готовый слот
    return tostring(body)
end
 
-- Преобразует текстовый список фреймов в таблицу фреймов и подфреймов.
-- Все псевдонимы раскрываются (с возможным сохранением ссылок).
-- Также функция определяет, нужно ли рандомизировать слот.
function p.parseFrameText(framesText, randomize, aliasReference, defaultMod)
    -- Списки фреймов
    local frames = { ["рандомизация"] = randomize }
    local subframes = {}
 
    -- Является ли текущий фрейм подфреймом?
    local subframe
 
    -- Раскрытые псевдонимы
    local expandedAliases
 
    -- Фреймы в текстовом виде
    local splitFrames = splitOnUnenclosedSemicolons(framesText)


     local frames = {}
     for i, frameText in ipairs(splitFrames) do
for _, frame in ipairs(p.splitOnUnenclosedSemicolons(args[1])) do
        -- Подфреймы группируются в фигурные скобки
        frameText = frameText:gsub('^%s*{%s*', function()
            subframe = true
            return ''
        end )
 
        if subframe then
            -- Находим закрывающую фигурную скобку
            frameText = frameText:gsub('%s*}%s*$', function()
                subframe = "последний"
                return ''
            end )
        end
 
        -- Преобразуем фрейм в табличный формат с применением
        -- модификации по умолчанию
        local frame = p.makeFrame(frameText, defaultMod)
        local newFrame = frame
 
        -- Раскрываем псевдонимы.
        -- По умолчанию псевдонимы старого формата преобразуются в новый.
        -- Если на странице псевдонимов старый формат не используется, то для
        -- повышения производительности рекомендуется задать флаг в виде
        -- поля «__отключить_старые_псевдонимы», равного true.
        -- Данное преобразование должно быть убрано после обновления всех
        -- старых страниц псевдонимов.
        local alias, convertLegacyAliases
        if frame.mod then
            if not modAliases[frame.mod] then
                modAliases[frame.mod] = tryLoadData(i18n.moduleModAliases:gsub('%$1', frame.mod))
            end


local frameParts = p.getParts( frame, args["мод"] )
            if modAliases[frame.mod] then
local id = frameParts.name
                alias = modAliases[frame.mod][frame.name]
        -- if frameParts.mod then
                convertLegacyAliases = modAliases[frame.mod]["__отключить_старые_псевдонимы"] ~= true
-- id = frameParts.mod .. ':' .. id
            end
-- end
         elseif aliases then
          
             alias = aliases[frame.name]
        --- Загрузка списка псевдонимов к модам
            convertLegacyAliases = aliases["__отключить_старые_псевдонимы"] ~= true
local modAliases
if frameParts.mod then
             if mw.title.new('Модуль:Инвентарный слот/Псевдонимы/' .. frameParts.mod).exists then
                modAliases = mw.loadData('Модуль:Инвентарный слот/Псевдонимы/' .. frameParts.mod)
end
         end
         end


         local alias = nil
         -- Псевдоним найден
if frameParts.mod then
        if alias then
             if modAliases and modAliases[id] then
            -- Если надо, преобразуем его из старого формата
             if convertLegacyAliases and type(alias) == "string" then
                alias = p.parseAliasText(alias)
            end
 
            -- Раскрываем его
            newFrame = p.getAlias(alias, frame)


             --- псевдонимы следует прописывать в модуле в формате ['имя'] = '[титл]:имя[доп. текст]',  титл и доп.текст необязательно указывать
             if aliasReference then
                -- Сохраняем ссылку на псевдоним
                local curFrame = #frames + 1
                local aliasData = { ["фрейм"] = frame, ["длина"] = #newFrame }


                 local title = mw.ustring.match ( modAliases[id], '^%[([^%]]+)%]' )
                 if subframe then
                if title then
                    if not subframes["ссылканапсевдонимы"] then
                     local aaa = mw.ustring.match ( modAliases[id], '^%[[^%]]+%](.+)$' )
                        subframes["ссылканапсевдонимы"] = {}
                     if aaa then
                    end
                         alias = '[' .. title .. ']' .. frameParts.mod .. ':' .. aaa
                     subframes["ссылканапсевдонимы"][#subframes+1] = aliasData
                    else
                else
                        alias = '[' .. title .. ']' .. frameParts.mod .. ':' .. frameParts.name
                     if not expandedAliases then
                         expandedAliases = {}
                     end
                     end
                 else
                    expandedAliases[curFrame] = aliasData
                    alias = frameParts.mod .. ':' .. modAliases[id]
                 end
            end
        end
        -- Конец обработки псевдонимов
 
        -- Добавление фреймов и управление рандомизацией
        if subframe then
            mergeList(subframes, newFrame)
            -- Включаем рандомизацию первого фрейма для псевдонима вида
            -- «любой предмет», если этот псевдоним является единственным
            -- подфреймом.
            if frames["рандомизация"] ~= "никогда" and subframes["рандомизация"] == nil and mw.ustring.match(frame.name, '^' .. i18n.prefixesMatch.any .. ' ') then
                subframes["рандомизация"] = true
            else
                subframes["рандомизация"] = false
            end
 
            if frames["рандомизация"] ~= "никогда" then
                frames["рандомизация"] = false
            end
 
            if subframe == "последний" then
                if #subframes == 1 or #splitFrames == i and #frames == 0 then
                    -- Если подфрейм единственный в своём контейнере, а тем более
                    -- во всей последовательности фреймов, то он «извлекается»
                    -- из контейнера
                    mergeList(frames, subframes)
                else
                    table.insert(frames, subframes)
                 end
                 end
                subframes = {}
                subframe = nil
            end
        else
            -- Включаем рандомизацию первого фрейма для псевдонимов вида
            -- «любой предмет», если этот псевдоним является единственным
            -- фреймом.
            if frames["рандомизация"] == nil and mw.ustring.match(frame.name, '^' .. i18n.prefixesMatch.any .. ' ') then
                frames["рандомизация"] = true
            elseif frames["рандомизация"] ~= "никогда" then
                frames["рандомизация"] = false
             end
             end
elseif aliases and aliases[id] then
alias = aliases[id]
end


if alias then
            mergeList(frames, newFrame)
table.insert( frames, p.expandAlias( frameParts, alias ) )
        end -- конец добавления фреймов в последовательность
else
    end
table.insert( frames, frame )
 
end
    -- Сохраняем ссылку на псевдонимы, если сохранена
end
    frames["ссылканапсевдонимы"] = expandedAliases
     args[1] = table.concat( frames, ';' )
 
    -- Возвращяем последовательность фреймов
    return frames
end
 
-- Функция совместимости: Преобразует текстовый псевдоним в табличный
-- По сути, является сильно упрощённым аналогом parseFrameText.
function p.parseAliasText(aliasText)
    local aliasFrames = {}
    local splitFrames = splitOnUnenclosedSemicolons(aliasText)
 
    for i, frameText in ipairs(splitFrames) do
        table.insert(aliasFrames, p.makeFrame(frameText))
    end
 
    return aliasFrames
end
 
-- Раскрывает заданный псевдоним в таблицу, дополнив её данными из материнского фрейма
-- Псевдоним должен быть либо строкой с названием, либо в табличном формате.
-- Псевдонимы старого формата должны быть преобразованы заранее при работе функции
-- parseFrameText.
function p.getAlias(aliasFrames, parentFrame)
    -- Если псевдоним состоит лишь из названия, то используем его для переопределения
     -- названия материнского фрейма.
    -- Действует только при отключенном преобразовании псевдонимов старого формата.
    if type(aliasFrames) == 'string' then
        local expandedFrame = mw.clone(parentFrame)
        expandedFrame.name = aliasFrames
        return { expandedFrame }
    end


     --- Проверка и замена всех фреймов на псевдонимы (конец)
     -- Если псевдоним является единичным фреймом, то помещаем его в список.
    -- В случае псевдонимов старого формата это уже должно быть сделано при их
--- Построение спрайта
    -- преобразовании.
    if aliasFrames.name then
-- Параметры
        aliasFrames = { aliasFrames }
local sprite
    end
local ids = mw.loadData([[Модуль:ИнвСпрайт]])["IDы"]
local modIds = {}
local back_modData = {}
local param
local animated = mw.ustring.find(args[1], ';')
local pageName = mw.title.getCurrentTitle().text
local imgClass = args["классизобр"]
local imgStyle = args["стильизобр"]
local numStyle = args["стильцифр"]
local scale = args["масштаб"] or '1'
local imgSize = 32 * scale
local valign
local body = mw.html.create('span')
-- Для повышения эффективности сборки CSS-стилей здесь используется ровно тот же трюк, что и в [[Модуль:Спрайт]].
-- А именно, собирается таблица (причём без table.insert!) и в конкатенированном виде передаётся через cssText.
local css = {}
if args["выравн"] then
css[#css+1] = "vertical-align:" .. args["выравн"]
end
if scale == '0.5' then
css[#css+1] = 'top:-1px'
end


if animated then
    -- Общий случай: псевдоним для группы фреймов
body:addClass('animated')
    local expandedFrames = {}
end
    for i, aliasFrame in ipairs(aliasFrames) do
if args["класс"] then
        local expandedFrame
body:addClass(args["класс"])
        if type(aliasFrame) == 'string' then
end
            -- Простой фрейм
            expandedFrame = { name = aliasFrame }
body:addClass('invslot')
        else
css[#css+1] = "width:" .. imgSize .. "px; height:" .. imgSize .. "px"
            -- Сложный фрейм.
            -- Поскольку он импортирован через mw.loadData, то для изменения
-- Если args["стиль"] равен nil, то ничего по сути в таблицу не добавляется.
            -- содержимого его необходимо клонировать.
css[#css+1] = args["стиль"]
            expandedFrame = cloneTable(aliasFrame)
        end
if (args["умолчание"] or '') ~= '' then
        expandedFrame.title = parentFrame.title or expandedFrame.title
-- css[#css+1] = 'background-image: {{FileUrl|' .. args["умолчание"] .. '.png}}'
        expandedFrame.mod = parentFrame.mod or expandedFrame.mod
body:addClass(args["умолчание"] .. '-slot')
        expandedFrame.num = parentFrame.num or expandedFrame.num
end
        expandedFrame.text = parentFrame.text or expandedFrame.text
        expandedFrame.english = parentFrame.english or expandedFrame.english
        expandedFrame.commons = parentFrame.commons or expandedFrame.commons


    ---спрайты для фона
        -- Добавляем фрейм в список
local n = args["умолчаниеCSS"]
        expandedFrames[i] = expandedFrame
local Back_ID = args["Фон ИД"]
    end
local mod = args["мод"]
    return expandedFrames
if (n or Back_ID) and mod then
end
if not Back_ID then
if mw.title.new('Модуль:ФоновыйСпрайт/' .. mod).exists then
back_modData = mw.loadData('Модуль:ФоновыйСпрайт/' .. mod .. '/ID')
param = mw.loadData('Модуль:ФоновыйСпрайт/' .. mod)
size = param["разм"]
sheet_size = param["формат"]
Back_ID = back_modData[n]
css[#css+1] = 'background-image: {{FileUrl|' .. mod .. 'bkgrdCSS.png}}'
end
else
body:addClass('gt-invslot')
size = 16
sheet_size = 160
end
if Back_ID then
local pos = Back_ID - 1
local back_scale = 32 / size
local tiles = sheet_size / size
local left = pos % tiles * 32
local top = math.floor( pos / tiles ) * 32
css[#css+1] = 'background-size:' .. sheet_size * back_scale ..'px; background-position: -' .. left .. 'px -' .. top ..'px'
end
end
-- Все CSS-стили применяются к body-элементу
body:cssText(table.concat(css, ";"))
--- Обработка фреймов
local first = true
for _, frame in ipairs(p.splitOnUnenclosedSemicolons(args[1])) do
local item
if frame ~= '' or frame == '' and animated then
item = body:tag('span'):addClass('invslot-item')
if imgClass then
item:addClass(imgClass)
end
if imgStyle then
item:cssText(imgStyle)
end
end
if frame == '' then
(item or body):tag('br')
else
local category
local parts = p.getParts(frame, args["мод"])
local title = parts.title or mw.text.trim(args["назв"] or '')
local mod = parts.mod
            local name = parts.name
local num = parts.num
local description = parts.text
           
            --- Добавляем к доп тексту название мода
local img, idData, en_name
if mod then
local modData = modIds[mod]
if not modData and mw.title.new('Модуль:ИнвСпрайт/' .. mod).exists then
local modDataModule = mw.loadData('Модуль:ИнвСпрайт/' .. mod)
local idListOverride = modDataModule['настройки']['списокID']
if idListOverride then
modData = mw.loadData('Модуль:' .. idListOverride)['IDы']
else
modData = modDataModule['IDы']
end
modIds[mod] = modData
end
if modData and modData[name] then
idData = modData[name]
                    en_name = idData["en"]
else
img = name .. ' (' .. mod .. ')'
end
elseif ids[name] then
idData = ids[name]
else
img = name
end
local link = args["ссылка"] or ''
if link == '' then
if mod then
link = mod .. '/' .. name
else
link = mw.ustring.gsub(name, '^Повреждённ[ыао][йяе] ', '')
end
elseif mw.ustring.lower(link) == 'нет' then
link = nil
end
if link == pageName then
link = nil
end
local formattedTitle
local plainTitle
if title == '' then
plainTitle = name
elseif mw.ustring.lower(title) ~= 'нет' then
plainTitle = mw.ustring.gsub(mw.ustring.gsub(title, '\\\\', '&#92;'), '\\&', '&#38;')
local formatPattern = '&[0-9a-fk-or]'
if mw.ustring.match(plainTitle, formatPattern) then
formattedTitle = title
plainTitle = mw.ustring.gsub(plainTitle, formatPattern, '')
end
if plainTitle == '' then
plainTitle = name
else
plainTitle =  mw.ustring.gsub(mw.ustring.gsub(plainTitle, '&#92;', '\\'), '&#38;', '&')
end
elseif link then
if img then
formattedTitle = ''
else
plainTitle = ''
end
end
if mod == 'GregTech' and idData and idData["страница"] and mw.title.new('Модуль:GregTechProc/Материалы/' .. idData["страница"]).exists then
local mat_info = mw.loadData('Модуль:GregTechProc/Материалы/' .. idData["страница"])
if mat_info then
local formula = mat_info["Формула"]
local melt = mat_info["Плавление"]
local boil = mat_info["Испарение"]
if not description then
description = ''
end
description = description .. '/&7Формула: &e' .. formula .. '/&6t° плавления: ' .. melt .. 'K/&ct° испарения: ' .. boil .. 'K'
end
end
item:attr{
['data-minetip-title'] = formattedTitle,
['data-minetip-text'] = description,
['data-modinfo-text'] = mod,
                ['data-minetip-lowtitle'] = en_name
}
if img then
-- & is re-escaped because mw.html treats attributes
-- as plain text, but MediaWiki doesn't
local escapedTitle = ( plainTitle or '' ):gsub( '&', '&#38;' )
item:addClass('invslot-item-image')
:wikitext('[[Файл:Grid ', img, '.png|' .. imgSize .. 'x' .. imgSize .. 'px|link=', link or '', '|', escapedTitle, ']]')
:cssText('width:' .. imgSize .. 'px; height:' .. imgSize .. 'px')
else
if not sprite then
sprite = require([[Модуль:Спрайт]]).sprite
end
local image
if mod then
image = (args["таблспрайтов"] or mod or "Inv") .. 'CSS.png'
end
if link then
item:wikitext('[[', link, '|')
end


local image, spriteCat
-- Обёртка для обеспечения совместимости
local dataPage = "ИнвСпрайт"
function p.expandAlias(parentFrame, alias)
if mod then
    return p.getAlias(alias, parentFrame)
dataPage = "ИнвСпрайт/" .. mod
end
image, spriteCat = sprite{
["масштаб"] = scale, ["данныеID"] = idData, ["назв"] = plainTitle,
["изобр"] = image, ["данные"] = dataPage
}
item:node(image)
category = spriteCat
end
if num and num > 1 and num < 1000 then
if img and link then
item:wikitext('[[', link, '|')
end
local number = item
:tag('span')
:addClass('invslot-stacksize')
:attr{title = plainTitle}
:wikitext(num)
if numStyle then
number:cssText(numStyle)
end
if img and link then
item:wikitext(']]')
end
end
if idData and link then
item:wikitext(']]')
end
item:wikitext(category)
end
if first then
if animated and item then
item:addClass('active')
end
first = false
end
end
return tostring( body )
end
end


function p.expandAlias( frameParts, alias )
-- Преобразует фрейм в текстовый формат
-- If the frame has no parts, we can just return the alias as-is
function p.stringifyFrame(frame)
--[[if not frameParts.title and not frameParts.mod and not frameParts.num and not frameParts.text then
    if not frame.name then
return alias
        -- вырожденный случай
end--]]
        return ''
    end
local expandedFrames = {}
 
for _, aliasFrame in ipairs(p.splitOnUnenclosedSemicolons(alias)) do
    return string.format(
local aliasParts = p.getParts(aliasFrame)
        '[%s]%s:%s,%s[%s]',
aliasParts.title = frameParts.title or aliasParts.title or ''
        frame.title or '',
aliasParts.mod = frameParts.mod or aliasParts.mod or 'Minecraft'
        frame.mod or 'Minecraft',
aliasParts.num = frameParts.num or aliasParts.num or ''
        frame.name,
aliasParts.text = frameParts.text or aliasParts.text or ''
        frame.num or '',
        frame.text or ''
table.insert(expandedFrames, mw.ustring.format(
    )
'[%s]%s:%s,%s[%s]',
end
aliasParts.title, aliasParts.mod, aliasParts.name, aliasParts.num, aliasParts.text
 
))
-- Преобразует последовательность фреймов в текстовый формат
end
function p.stringifyFrames(frames)
    local frames = {}
return table.concat(expandedFrames, ';')
    for i, frame in ipairs(frames) do
        frames[i] = p.stringifyFrame(frame)
    end
    return table.concat(frames, ';')
end
end


-- Преобразует текстовое обозначение фрейма в табличный формат
function p.makeFrame(frameText, defaultMod)
    -- Простейший случай: одно только название
    if not frameText:match('[%[:,]') then
        return { mod = defaultMod, name = mw.text.trim(frameText) }
    end
    -- Сложный фрейм
    local frame = {}
    -- Заголовок
    local title, rest = mw.ustring.match(frameText, '^%s*%[%s*([^%]]*%s*)%]%s*(.*)')
    if title then
        frame.title = title
        frameText = rest
    end


function p.getParts(frame, mod)
    -- Дополнительный текст
----Функция получает название предмета в формате "[титл]мод:имя[доп.текст],число"
    local rest, text = mw.ustring.match(frameText, '([^%]]*)%s*%[([^%]]*)%]%s*$')
----parts.title = титл, название предмета при наведении
    if text then
----parts.mod = мод
        frame.text = text
----parts.name = имя
        frameText = rest
----parts.text = текст, дополнительный текст при наведении на предмет
    end
----parts.num = число


local parts = {}
    -- Модификация
parts.title = mw.ustring.match(frame, '^%[%s*([^%]]+)%s*%]')
    local mod, rest = mw.ustring.match(frameText, '^([^:]+):%s*(.*)')
     local vanilla = {v = 1, vanilla = 1, mc = 1, minecraft = 1}
     local modPattern
     if mod then
     if mw.ustring.match(frame, '^%[.*%]([a-zA-Zа-яА-Я0-9ёЁé _%-\']+):') then
        if not vanilla[mw.ustring.lower(mod)] then
    modPattern = '^%[.*%]([a-zA-Zа-яА-Я0-9ёЁé _%-\']+):'
            frame.mod = modNames[mod] or mod
            frame.mod = frame.mod:gsub('_', ' ')
            frame.mod = mw.ustring.gsub(frame.mod, "^%l", mw.ustring.upper)
        end
        frameText = rest
     else
     else
    modPattern = '^([a-zA-Zа-яА-Я0-9ёЁé _%-\']+):'
        frame.mod = defaultMod
     end
     end


parts.mod = mw.text.trim(mw.ustring.match(frame, modPattern) or mod or '') ---- Получаем название мода
    -- Название и размер стопки
 
    local name, num = mw.ustring.match(frameText, '(.*),%s*(%d+)')
local vanilla = {v = 1, vanilla = 1, mc = 1, minecraft = 1}
    if num then
if parts.mod == '' or vanilla[mw.ustring.lower(parts.mod)] then
        -- Есть размер
parts.mod = nil
        frame.name = mw.text.trim(name)
else
        frame.num = math.floor(num)
    if p.modAliases[parts.mod] then
        if frame.num < 2 then
    parts.mod = p.modAliases[parts.mod]
            frame.num = nil
         end
         end
         parts.mod = mw.ustring.gsub(parts.mod,'_',' ')
    else
         -- Размера нет
        frame.name = mw.text.trim(frameText)
     end
     end


local _, nameStartV = mw.ustring.find( frame, '^%[[^%]]*%]' )
     return frame
local nameStart = ( ({mw.ustring.find( frame, modPattern )})[2] or nameStartV or 0 ) + 1
if nameStart - 1 == #frame then
nameStart = 1
end
parts.name = mw.text.trim( mw.ustring.sub( frame, nameStart, ( mw.ustring.find( frame, '[,%[]', nameStart ) or 0 ) - 1 ) )
parts.num = math.floor(mw.ustring.match(frame, ',%s*(%d+)') or 0)
if parts.num == 0 then
parts.num = nil
end
parts.text = mw.ustring.match(frame, '%[%s*([^%]]+)%s*%]$')
      
return parts
end
end
 
-- Псевдоним функции для совместимости
p.getParts = p.makeFrame
 
return p
return p

Версия от 18:07, 4 февраля 2024

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

----------------------------------------------------------------------------
--- Модуль для отображения инвентарных слотов в Minecraft Wiki.
--- ВНИМАНИЕ: Любые изменения в этом модуле отразятся на тысячах статей!
----------------------------------------------------------------------------

local p = {}

-------------------------------------
-- Глобально экспортируемые данные
-------------------------------------

-- Данные по интернационализации и локализации
local i18n = {
    -- Стандартные наименования файлов
    filename = 'Grid $1',
    --legacyFilename = 'Grid $1',
    commonsFilename = 'Invicon $1', -- для файлов из общего хранилища

    -- Ссылка на статью, касающуюся модификации
    modLink = '$1/$2',

    -- Модули, отвечающие за псевдонимы
    moduleAliases = [[Модуль:Инвентарный слот/Псевдонимы]],
    moduleModAliases = [[Модуль:Инвентарный слот/Псевдонимы/$1]],

    -- Спрайтовые модули
    moduleSprite = [[Модуль:Спрайт]],
    moduleInvData = [[Модуль:ИнвСпрайт]],
    moduleModData = [[Модуль:ИнвСпрайт/$1]],

    -- Служебные модули
    moduleRandom = [[Модуль:Случайные числа]],
    moduleUtils = [[Модуль:Специальные утилиты]],
    moduleMods = [[Модуль:Модификации]],

    -- Начальные формы приставок к названиям некоторых псевдонимов.
    -- Для использования с модулем «Склонение прилагательных»
    prefixes = {
        any = "любой",
        matching = "соответствующий",
        damaged = "повреждённый",
        unwaxed = "невощёный"
    },

    -- Выражения для поиска вышеуказанных приставок.
    -- Так будет легче, например, убирать их из целей ссылок.
    -- Именно их, а не prefixes, следует использовать при переводе
    -- модулей с англовики в функциях gsub и match (причём с mw.ustring).
    prefixesMatch = {
        any = 'Люб[оаы][йяе]',
        matching = 'Соответствующ[иае][йяе]',
        damaged = 'Повреждённ[ыао][йяе]',
        unwaxed = '[Нн]евощён[ыао][йяе]',
    },

    -- Список суффиксов
    suffixes = {
        be = 'BE',
        lce = 'LCE',
    }
}
p.i18n = i18n

-- Для совместимости
p.prefixes = {
    'Любой', 'Любая', 'Любое', 'Любые',
    'Повреждённый', 'Повреждённая', 'Повреждённое', 'Повреждённые',
    'Соответствующий', 'Соответствующая', 'Соответствующее', 'Соответствующие',
    'Невощёный', 'Невощёная', 'Невощёное', 'Невощёные'
}
p.modAliases = mw.loadData("Модуль:Модификации")

-----------------------------------------
-- Внутренние глобальные данные модуля
-----------------------------------------

local modIds = {}
local modAliases = {}

local random = require(i18n.moduleRandom).random
local tryLoadData = require(i18n.moduleUtils).tryLoadData
local mergeList = require(i18n.moduleUtils).mergeList
local sprite = require(i18n.moduleSprite).sprite

local aliases = mw.loadData(i18n.moduleAliases)
local modNames = mw.loadData(i18n.moduleMods)
local ids = mw.loadData(i18n.moduleInvData)["IDы"]

local pageName = mw.title.getCurrentTitle().text

-----------------------
-- Служебные функции
-----------------------

-- Разбор строки, разделённой точками с запятой.
-- Учитывает, что точка с запятой может быть внутри квадратных скобок.
local function splitOnUnenclosedSemicolons(text)
    local semicolon, lbrace, rbrace = (";[]"):byte(1, 3)
    local nesting = false
    local splitStart = 1
    local frameIndex = 1
    local frames = {}

    for index = 1, text:len() do
    local byte = text:byte(index)
        if byte == semicolon and not nesting then
            frames[frameIndex] = text:sub(splitStart, index - 1)
            frameIndex = frameIndex + 1
            splitStart = index + 1
        elseif byte == lbrace then
            assert(not nesting, "Ошибка синтаксиса: чрезмерные квадратные скобки")
            nesting = true
        elseif byte == rbrace then
            assert(nesting, "Ошибка синтаксиса: несбалансированные квадратные скобки")
            nesting = false
        end
    end
    assert(not nesting, "Ошибка синтаксиса: несбалансированные квадратные скобки")
    frames[frameIndex] = text:sub(splitStart, text:len())

    for index = 1, #frames do
        frames[index] = (frames[index]:gsub("^%s+", ""):gsub("%s+$", "")) -- быстрее mw.text.trim
    end

    return frames
end
p.splitOnUnenclosedSemicolons = splitOnUnenclosedSemicolons -- для совместимости

-- Простая рекурсивная копия значений таблицы
local function cloneTable(origTable)
    local newTable = {}
    for k, v in pairs(origTable) do
        if type(v) == "table" then
            v = cloneTable(v)
        end
        newTable[k] = v
    end
    return newTable
end

-- Отделяет расширение от названия фрейма, если оно есть.
-- Возвращяет название без расширения и либо само расширение,
-- либо png в случае его отсутствия.
local function splitExtension(name)
    if name:match('%.gif$') or name:match('%.png$') then
        -- расширения англоязычные, представляют собой ASCII-символы,
        -- поэтому обычные match и sub безопасны
        return name:sub(0, -5), name:sub(-3)
    elseif name:match('%.webp$') then
        return name:sub(0, -6), 'webp'
    else
        return name, 'png'
    end
end

-- Создаёт HTML-код для предмета.
-- args является таблицей аргументов, принятой самим модулем
local function makeItem(frame, args)
    -- Создание HTML-элемента
    local item = mw.html.create('span'):addClass('invslot-item')
    if args["классизобр"] then
        item:addClass(args["классизобр"])
    end
    if args["стильизобр"] then
        item:cssText(args["стильизобр"])
    end

    if frame.name == '' then
        -- пустой фрейм
        return item
    end

    local category -- категории

    -- Параметры фрейма
    local title = frame.title or mw.text.trim(args["назв"] or '')
    local mod = frame.mod
    local name = frame.name or ''
    local num = frame.num
    local description = frame.text
    local en_name = frame.english

    -- Построение изображения
    local img, idData, extension

    -- Модификация?
    if mod then
        -- Пытаемся загрузить список ИнвСпрайтов для модификации
        modData = modIds[mod]
        if not modData then
            modData = tryLoadData(i18n.moduleModData:gsub("%$1", mod))
            if modData then
                local idsOverride = modData["настройки"]["списокID"]
                if idsOverride then
                    modData = tryLoadData("Модуль:" .. idsOverride)["IDы"]
                else
                    modData = modData["IDы"]
                end
            end
            modIds[mod] = modData
        end

        if modData and modData[name] then -- из ИнвСпрайта
            idData = modData[name]
            en_name = en_name or idData["en"] -- для совместимости
        else -- из Grid-файла
            name, extension = splitExtension(name)
            img = i18n.filename:gsub("%$1", name .. ' (' .. mod .. ')') .. '.' .. extension
        end
        -- Конец обработки иконок из модификаций
    elseif type(frame.commons) == 'string' then
        -- Ванильный Invicon-файл из общего хранилища
        img, extension = splitExtension(frame.commons)
        img = i18n.commonsFilename:gsub("%$1", img) .. '.' .. extension
    elseif en_name and frame.commons ~= false then
        -- Автоопределение Invicon-файла по англоязычному названию
        en_name, extension = splitExtension(en_name)
        img = i18n.commonsFilename:gsub("%$1", en_name) .. '.' .. extension
    elseif ids[name] then
        -- Ванильный ИнвСпрайт
        idData = ids[name]
    else
        -- Ванильный Grid-файл, загруженный локально
        name, extension = splitExtension(name)
        img = i18n.filename:gsub("%$1", name) .. '.' .. extension
    end

    -- К данному моменту задана переменная:
    -- 1) idData, если иконка взята из таблицы спрайтов, либо
    -- 2) img, если иконка берётся из Grid-файла.

    -- Формирование цели ссылки
    local link = args["ссылка"] or ''
    if link == '' then -- поведение по умолчанию
        if mod then
            link = i18n.modLink:gsub('%$1', mod):gsub('%$2', name)
        else
            -- Убираем префикс повреждённых предметов
            link = mw.ustring.gsub(name, '^'.. i18n.prefixesMatch.damaged .. ' ', '')

            -- Убираем суффиксы изданий
            for _, suffix in pairs(i18n.suffixes) do
                link = mw.ustring.gsub(name, ' ' .. suffix .. '$', '')
            end
        end
    elseif mw.ustring.lower(link) == "нет" then
        -- Отключение ссылки
        link = nil
    end
    if link and mw.ustring.gsub(link, "^%l", mw.ustring.upper) == pageName then
        -- отключаем ссылку на текущую страницу
        link = nil
    end

    -- Форматирование заголовка
    local formattedTitle, plainTitle

    if title == '' then
        plainTitle = name
    elseif mw.ustring.lower(title) ~= "нет" then
        -- Временное преобразование экранированных служебных символов
        plainTitle = title:gsub('\\\\', '&#92;'):gsub('\\&', '&#38;')

        -- Очищаем «простой» заголовок от форматирования
        local formatPattern = '&[0-9a-fk-or]'
        if plainTitle:match(formatPattern) then
            formattedTitle = title
            plainTitle = plainTitle:gsub(formatPattern, '')
        end

        if plainTitle == '' then
            plainTitle = name
        else
            -- Превращаем экранированные символы в финальную форму
            plainTitle = plainTitle:gsub('&#92;', '\\'):gsub('&#38;', '&')
        end
    elseif link then
        if img then
            formattedTitle = ''
        else
            plainTitle = ''
        end
    end

    -- Добавляем атрибуты для minetip
    item:attr{
        ['data-minetip-title'] = formattedTitle,
        ['data-minetip-text'] = description,
        ['data-modinfo-text'] = mod,
        ['data-minetip-lowtitle'] = en_name
    }

    -- Иконка
    if img then
        -- Grid-файл

        -- & экранируется повторно, так как mw.html считает атрибуты
        -- простым текстом, а MediaWiki — нет.
        local escapedTitle = (plainTitle or ''):gsub('&', '&#38;')
        item:addClass('invslot-item-image')
            :wikitext('[[Файл:', img, '|32x32px|link=', link or '', '|', escapedTitle, ']]')
    else
        -- ИнвСпрайт
        local scale = args["масштаб"] or 1

        if link then -- начало ссылки
            item:wikitext('[[', link, '|')
        end

        local image, spriteCat
        local dataPage = mod and ("ИнвСпрайт/" .. mod) or "ИнвСпрайт"

        image, spriteCat = sprite{
            ["масштаб"] = scale, ["данныеID"] = idData, ["назв"] = plainTitle,
            ["данные"] = dataPage
        }
        item:node(image)
        category = spriteCat
    end

    -- Размер стопки
    if num and num > 1 and num < 1000 then
        if img and link then
            -- Открываем ссылку, если используется Grid-файл.
            -- Для ИнвСпрайта ссылка уже была открыта ранее.
            item:wikitext('[[', link, '|')
        end

        local number = item:tag('span')
           :addClass('invslot-stacksize')
           :attr{ title = plainTitle }
           :wikitext(num)
        if args["стильцифр"] then
            number:cssText(args["стильцифр"])
        end

        if img and link then
            -- Закрываем ссылку, если используется Grid-файл
            item:wikitext(']]')
        end
    end

    if idData and link then
        -- Закрываем ссылку, если используется ИнвСпрайт
        item:wikitext(']]')
    end

    -- Добавляем категории
    item:wikitext(category)

    -- Возвращаем предмет
    return item
end

----------------------------------
-- Общедоступные функции модуля
----------------------------------

-- Создаёт слот. Служит основной точкой входа
function p.slot(f)
    -- Получение аргументов
    local args = f.args or f
    if f == mw.getCurrentFrame() and args[1] == nil then
        args = f:getParent().args
    end

    if not args["обработанный"] then
    -- Нормализация первого аргумента
        args[1] = mw.text.trim(args[1] or '')
    end
    -- Если задан аргумент «обработан», то подразумевается, что
    -- args[1] — это список фреймов в табличном формате.

    -- Модификация по умолчанию
    local defaultMod = mw.text.trim(args["мод"] or '')
    if defaultMod == '' then
        defaultMod = nil
    elseif modNames[defaultMod] then
        defaultMod = modNames[defaultMod]
    end
    
    if defaultMod then
        defaultMod = defaultMod:gsub('_', ' ')
        defaultMod = mw.ustring.gsub(defaultMod, "^%l", mw.ustring.upper)
    end

    -- Сохраняем список фреймов в табличном формате
    local frames
    if args["обработанный"] then
        frames = args[1]
    elseif args[1] ~= '' then
        local randomise = args.class == 'invslot-large' and "никогда" or nil
        frames = p.parseFrameText(args[1], randomise, false, defaultMod)
    end

    -- Слот анимированный?
    local animated = frames and #frames > 1

    -- Построение HTML-элемента
    local body = mw.html.create('span'):addClass('invslot'):css{
        ['vertical-align'] = args["выравн"]
    }

    if animated then
        -- включаем анимацию
        body:addClass('animated')
    end

    -- Масштабирование слота
    local scale = tonumber(args["масштаб"]) or 1
    if scale ~= 1 then
        local imgSize = 32 * scale
        body:css{ width = imgSize .. "px", height = imgSize .. "px" }
        if scale == 0.5 then
            -- компенсация высоты для уменьшенного слота?
            body:css{ top = '-1px' }
        end
    end

    -- Добавляем дополнительные классы и стили
    if args["класс"] then
        body:addClass(args["класс"])
    end
    if args["стиль"] then
        body:cssText(args["стиль"])
    end

    -- Добавляем фон умолчания
    if (args["умолчание"] or "") ~= "" then
        body:addClass(args["умолчание"] .. '-slot')
    end

    -- Фоновый спрайт умолчания для GregTech (временная реализация)
    local backID = args["Фон ИД"]
    if defaultMod and defaultMod:match("GregTech") and backID then
        local pos = backID - 1
        local left = (pos % 10) * 32
        local top = math.floor(pos / 10) * 32

        body:addClass('gt-invslot'):css{
            ['background-size'] = '320px auto',
            ['background-position'] = '-' .. left .. 'px -' .. top .. 'px'
        }
    end

    if not frames then
        -- Вырожденный случай (нет фреймов)
        return tostring(body)
    end

    -- Активный фрейм для анимации
    local activeFrame = frames["рандомизация"] == true and random(#frames) or 1

    -- Добавление фреймов
    for i, frame in ipairs(frames) do
        local item

        -- Проверка на наличие подфреймов
        if frame[1] then
            -- Контейнер подфреймов
            item = body:tag('span'):addClass('animated-subframe')
            local subActiveFrame = frame["рандомизация"] and random(#frame) or 1

            for sI, sFrame in ipairs(frame) do
                -- Добавляем подфрейм
                local sItem = makeItem(sFrame, args)
                item:node(sItem)

                if sI == subActiveFrame then
                    -- задаём активным
                    sItem:addClass('animated-active')
                end
            end
        else
            -- Обычный фрейм
            item = makeItem(frame, args)
            body:node(item)
        end

        if i == activeFrame and animated then
            -- задаём активным
            item:addClass('animated-active')
        end
    end

    -- Возвращаем готовый слот
    return tostring(body)
end

-- Преобразует текстовый список фреймов в таблицу фреймов и подфреймов.
-- Все псевдонимы раскрываются (с возможным сохранением ссылок).
-- Также функция определяет, нужно ли рандомизировать слот.
function p.parseFrameText(framesText, randomize, aliasReference, defaultMod)
    -- Списки фреймов
    local frames = { ["рандомизация"] = randomize }
    local subframes = {}

    -- Является ли текущий фрейм подфреймом?
    local subframe

    -- Раскрытые псевдонимы
    local expandedAliases

    -- Фреймы в текстовом виде
    local splitFrames = splitOnUnenclosedSemicolons(framesText)

    for i, frameText in ipairs(splitFrames) do
        -- Подфреймы группируются в фигурные скобки
        frameText = frameText:gsub('^%s*{%s*', function()
            subframe = true
            return ''
        end )

        if subframe then
            -- Находим закрывающую фигурную скобку
            frameText = frameText:gsub('%s*}%s*$', function()
                subframe = "последний"
                return ''
            end )
        end

        -- Преобразуем фрейм в табличный формат с применением
        -- модификации по умолчанию
        local frame = p.makeFrame(frameText, defaultMod)
        local newFrame = frame

        -- Раскрываем псевдонимы.
        -- По умолчанию псевдонимы старого формата преобразуются в новый.
        -- Если на странице псевдонимов старый формат не используется, то для
        -- повышения производительности рекомендуется задать флаг в виде
        -- поля «__отключить_старые_псевдонимы», равного true.
        -- Данное преобразование должно быть убрано после обновления всех
        -- старых страниц псевдонимов.
        local alias, convertLegacyAliases
        if frame.mod then
            if not modAliases[frame.mod] then
                modAliases[frame.mod] = tryLoadData(i18n.moduleModAliases:gsub('%$1', frame.mod))
            end

            if modAliases[frame.mod] then
                alias = modAliases[frame.mod][frame.name]
                convertLegacyAliases = modAliases[frame.mod]["__отключить_старые_псевдонимы"] ~= true
            end
        elseif aliases then
            alias = aliases[frame.name]
            convertLegacyAliases = aliases["__отключить_старые_псевдонимы"] ~= true
        end

        -- Псевдоним найден
        if alias then
            -- Если надо, преобразуем его из старого формата
            if convertLegacyAliases and type(alias) == "string" then
                alias = p.parseAliasText(alias)
            end

            -- Раскрываем его
            newFrame = p.getAlias(alias, frame)

            if aliasReference then
                -- Сохраняем ссылку на псевдоним
                local curFrame = #frames + 1
                local aliasData = { ["фрейм"] = frame, ["длина"] = #newFrame }

                if subframe then
                    if not subframes["ссылканапсевдонимы"] then
                        subframes["ссылканапсевдонимы"] = {}
                    end
                    subframes["ссылканапсевдонимы"][#subframes+1] = aliasData
                else
                    if not expandedAliases then
                        expandedAliases = {}
                    end
                    expandedAliases[curFrame] = aliasData
                end
            end
        end
        -- Конец обработки псевдонимов

        -- Добавление фреймов и управление рандомизацией
        if subframe then
            mergeList(subframes, newFrame)
            -- Включаем рандомизацию первого фрейма для псевдонима вида
            -- «любой предмет», если этот псевдоним является единственным
            -- подфреймом.
            if frames["рандомизация"] ~= "никогда" and subframes["рандомизация"] == nil and mw.ustring.match(frame.name, '^' .. i18n.prefixesMatch.any .. ' ') then
                subframes["рандомизация"] = true
            else
                subframes["рандомизация"] = false
            end

            if frames["рандомизация"] ~= "никогда" then
                frames["рандомизация"] = false
            end

            if subframe == "последний" then
                if #subframes == 1 or #splitFrames == i and #frames == 0 then
                    -- Если подфрейм единственный в своём контейнере, а тем более
                    -- во всей последовательности фреймов, то он «извлекается»
                    -- из контейнера
                    mergeList(frames, subframes)
                else
                    table.insert(frames, subframes)
                end
                subframes = {}
                subframe = nil
            end
        else
            -- Включаем рандомизацию первого фрейма для псевдонимов вида
            -- «любой предмет», если этот псевдоним является единственным
            -- фреймом.
            if frames["рандомизация"] == nil and mw.ustring.match(frame.name, '^' .. i18n.prefixesMatch.any .. ' ') then
                frames["рандомизация"] = true
            elseif frames["рандомизация"] ~= "никогда" then
                frames["рандомизация"] = false
            end

            mergeList(frames, newFrame)
        end -- конец добавления фреймов в последовательность
    end

    -- Сохраняем ссылку на псевдонимы, если сохранена
    frames["ссылканапсевдонимы"] = expandedAliases

    -- Возвращяем последовательность фреймов
    return frames
end

-- Функция совместимости: Преобразует текстовый псевдоним в табличный
-- По сути, является сильно упрощённым аналогом parseFrameText.
function p.parseAliasText(aliasText)
    local aliasFrames = {}
    local splitFrames = splitOnUnenclosedSemicolons(aliasText)

    for i, frameText in ipairs(splitFrames) do
        table.insert(aliasFrames, p.makeFrame(frameText))
    end

    return aliasFrames
end

-- Раскрывает заданный псевдоним в таблицу, дополнив её данными из материнского фрейма
-- Псевдоним должен быть либо строкой с названием, либо в табличном формате.
-- Псевдонимы старого формата должны быть преобразованы заранее при работе функции
-- parseFrameText.
function p.getAlias(aliasFrames, parentFrame)
    -- Если псевдоним состоит лишь из названия, то используем его для переопределения
    -- названия материнского фрейма.
    -- Действует только при отключенном преобразовании псевдонимов старого формата.
    if type(aliasFrames) == 'string' then
        local expandedFrame = mw.clone(parentFrame)
        expandedFrame.name = aliasFrames
        return { expandedFrame }
    end

    -- Если псевдоним является единичным фреймом, то помещаем его в список.
    -- В случае псевдонимов старого формата это уже должно быть сделано при их
    -- преобразовании.
    if aliasFrames.name then
        aliasFrames = { aliasFrames }
    end

    -- Общий случай: псевдоним для группы фреймов
    local expandedFrames = {}
    for i, aliasFrame in ipairs(aliasFrames) do
        local expandedFrame
        if type(aliasFrame) == 'string' then
            -- Простой фрейм
            expandedFrame = { name = aliasFrame }
        else
            -- Сложный фрейм.
            -- Поскольку он импортирован через mw.loadData, то для изменения
            -- содержимого его необходимо клонировать.
            expandedFrame = cloneTable(aliasFrame)
        end
        expandedFrame.title = parentFrame.title or expandedFrame.title
        expandedFrame.mod = parentFrame.mod or expandedFrame.mod
        expandedFrame.num = parentFrame.num or expandedFrame.num
        expandedFrame.text = parentFrame.text or expandedFrame.text
        expandedFrame.english = parentFrame.english or expandedFrame.english
        expandedFrame.commons = parentFrame.commons or expandedFrame.commons

        -- Добавляем фрейм в список
        expandedFrames[i] = expandedFrame
    end
    return expandedFrames
end

-- Обёртка для обеспечения совместимости
function p.expandAlias(parentFrame, alias)
    return p.getAlias(alias, parentFrame)
end

-- Преобразует фрейм в текстовый формат
function p.stringifyFrame(frame)
    if not frame.name then
        -- вырожденный случай
        return ''
    end

    return string.format(
        '[%s]%s:%s,%s[%s]',
        frame.title or '',
        frame.mod or 'Minecraft',
        frame.name,
        frame.num or '',
        frame.text or ''
    )
end

-- Преобразует последовательность фреймов в текстовый формат
function p.stringifyFrames(frames)
    local frames = {}
    for i, frame in ipairs(frames) do
        frames[i] = p.stringifyFrame(frame)
    end
    return table.concat(frames, ';')
end

-- Преобразует текстовое обозначение фрейма в табличный формат
function p.makeFrame(frameText, defaultMod)
    -- Простейший случай: одно только название
    if not frameText:match('[%[:,]') then
        return { mod = defaultMod, name = mw.text.trim(frameText) }
    end

    -- Сложный фрейм
    local frame = {}

    -- Заголовок
    local title, rest = mw.ustring.match(frameText, '^%s*%[%s*([^%]]*%s*)%]%s*(.*)')
    if title then
        frame.title = title
        frameText = rest
    end

    -- Дополнительный текст
    local rest, text = mw.ustring.match(frameText, '([^%]]*)%s*%[([^%]]*)%]%s*$')
    if text then
        frame.text = text
        frameText = rest
    end

    -- Модификация
    local mod, rest = mw.ustring.match(frameText, '^([^:]+):%s*(.*)')
    local vanilla = {v = 1, vanilla = 1, mc = 1, minecraft = 1}
    if mod then
        if not vanilla[mw.ustring.lower(mod)] then
            frame.mod = modNames[mod] or mod
            frame.mod = frame.mod:gsub('_', ' ')
            frame.mod = mw.ustring.gsub(frame.mod, "^%l", mw.ustring.upper)
        end
        frameText = rest
    else
        frame.mod = defaultMod
    end

    -- Название и размер стопки
    local name, num = mw.ustring.match(frameText, '(.*),%s*(%d+)')
    if num then
        -- Есть размер
        frame.name = mw.text.trim(name)
        frame.num = math.floor(num)
        if frame.num < 2 then
            frame.num = nil
        end
    else
        -- Размера нет
        frame.name = mw.text.trim(frameText)
    end

    return frame
end

-- Псевдоним функции для совместимости
p.getParts = p.makeFrame

return p
Сайт использует Cookie для нормальной работы