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

Модуль:Инвентарный слот

Материал из Create Wiki

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

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

local p = {}

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

-- Данные по интернационализации и локализации
local i18n = {
    -- Стандартные наименования файлов
    filename = '$1',
    --legacyFilename = 'Grid $1',
    commonsFilename = 'Grid $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('\\\\', '\'):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)

    -- Возвращаем предмет
    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 для нормальной работы