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

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

Материал из LemonCraft Wiki
Новая страница: «---------------------------------------------------------------------------- --- Модуль для отображения инвентарных слотов в Minecraft Wiki. --- ВНИМАНИЕ: Любые изменения в этом модуле отразятся на тысячах статей! ---------------------------------------------------------------------------- local p = {} ------------------------------------- -- Глобально...»
Нет описания правки
 
(не показана 1 промежуточная версия этого же участника)
Строка 25: Строка 25:
-- Модули, отвечающие за англоязычные названия (вместе с псевдонимами)
-- Модули, отвечающие за англоязычные названия (вместе с псевдонимами)
moduleEnglish = [[Модуль:Инвентарный слот/Англоязычные названия]],
moduleEnglish = [[Модуль:Инвентарный_слот/Англоязычные_названия]],
moduleModEnglish = [[Модуль:Инвентарный слот/Англоязычные названия/$1]],
moduleModEnglish = [[Модуль:Инвентарный слот/Англоязычные названия/$1]],



Текущая версия от 14:39, 30 апреля 2025

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

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

local p = {}

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

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

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

	-- Модули, отвечающие за псевдонимы
	moduleAliases = [[Модуль:Инвентарный слот/Псевдонимы]],
	moduleModAliases = [[Модуль:Инвентарный слот/Псевдонимы/$1]],
	
	-- Модули, отвечающие за англоязычные названия (вместе с псевдонимами)
	moduleEnglish = [[Модуль:Инвентарный_слот/Англоязычные_названия]],
	moduleModEnglish = [[Модуль:Инвентарный слот/Англоязычные названия/$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',
	},
	
	-- Список цветов
	colors = {
		"белый", "оранжевый", "сиреневый", "голубой", "жёлтый", "лаймовый",
		"розовый", "серый", "светло-серый", "бирюзовый", "фиолетовый", "синий",
		"коричневый", "зелёный", "красный", "чёрный"
	}
}
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 vanilla = {v = 1, vanilla = 1, mc = 1, minecraft = 1}
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
	
	local scale = tonumber(args["масштаб"]) or 1
	local imgSize = 32 * scale
	if scale ~= 1 then
		item:css{
			width = imgSize .. "px",
			height = imgSize .. "px"
		}
	end

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

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

	-- Параметры фрейма
	local title = mw.text.trim(args["назв"] or '')
	if mw.ustring.lower(title) ~= "нет" then
		title = frame.title or title
	end
	local mod = frame.mod
	local name = frame.name
	local num = frame.num
	local description = frame.text
	local english = 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 not english then
			local enNames = tryLoadData(i18n.moduleModEnglish:gsub("%$1", mod))
			english = enNames and enNames[name]
		end
		
		if modData and modData[name] then -- из ИнвСпрайта
			idData = modData[name]
			english = english or idData["en"] -- для совместимости
		else -- из Grid-файла
			name, extension = splitExtension(name)
			img = i18n.filename:gsub("%$1", name .. ' (' .. mod .. ')') .. '.' .. extension
		end
		-- Конец обработки иконок из модификаций
	else
		-- Ванильная иконка
		
		-- Импорт англоязычного названия из отдельного модуля (при необходимости)
		if not english then
			local enNames = mw.loadData(i18n.moduleEnglish)
			english = enNames and enNames[name]
		end
		
		if type(frame.commons) == 'string' then
			-- Ванильный Invicon-файл из общего хранилища
			img, extension = splitExtension(frame.commons)
			img = i18n.commonsFilename:gsub("%$1", img) .. '.' .. extension
		elseif english and frame.commons ~= false then
			-- Автоопределение Invicon-файла по англоязычному названию.
			-- ВНИМАНИЕ: если вручную переопределяете англоназвание, укажите
			-- явным образом Invicon-файл!
			img, extension = splitExtension(english)
			img = i18n.commonsFilename:gsub("%$1", img) .. '.' .. extension
		elseif ids[name] then
			-- Ванильный ИнвСпрайт
			idData = ids[name]
		else
			-- Ванильный Grid-файл, загруженный локально
			name, extension = splitExtension(name)
			img = i18n.filename:gsub("%$1", name) .. '.' .. extension
		end
	end

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

	-- Формирование цели ссылки
	local link = mw.text.trim(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(link, ' ' .. 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-jl-qs-wr]'
		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'] = english
	}

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

		-- & экранируется повторно, так как mw.html считает атрибуты
		-- простым текстом, а MediaWiki — нет.
		local escapedTitle = (plainTitle or ''):gsub('&', '&')
		item:addClass('invslot-item-image')
			:wikitext('[[Файл:', img, '|', imgSize, 'x', imgSize, 'px|link=', link or '', '|', escapedTitle, ']]')
	else
		-- ИнвСпрайт
		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 type(args[1]) == "string" 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] and args[1] ~= '' and args[1] ~= {} then
		local randomise = args["класс"] == 'invslot-large' and "никогда" or nil
		if args[1][1] then
			frames = p.expandAliases(args[1], randomise, false)
		else
			frames = p.parseFrameText(args[1], randomise, false, defaultMod)
		end
	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 or #frames == 0 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["рандомизация"] == true 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.convertFrameText(framesText, defaultMod)
	-- Списки фреймов
	local frames = {}
	local subframes = {}
	
	-- Является ли текущий фрейм подфреймом?
	local subframe
	
	-- Обрабатываем фреймы
	for i, frameText in ipairs(splitOnUnenclosedSemicolons(framesText)) 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
		
		-- Добавление фреймов
		if subframe then
			mergeList(subframes, newFrame)
			
			if subframe == "последний" then
				table.insert(frames, subframes)
				subframes = {}
				subframe = nil
			end
		else
			mergeList(frames, newFrame)
		end
	end
	
	-- Возвращаем последовательность фреймов
	return frames
end

-- Раскрывает все псевдонимы в последовательности фреймов (в т. ч. подфреймов).
-- Позволяет сохранить ссылку на исходный псевдоним. Также определяет, нужно ли
-- рандомизировать слот.
function p.expandAliases(frames, randomize, aliasReference)
	local newFrames = { ["рандомизация"] = randomize }
	
	-- Раскрытые псевдонимы
	local expandedAliases
	
	-- Обрабатываем фреймы
	for i, frame in ipairs(frames) do
		local alias
		
		if frame[1] then
			-- Это контейнер подфреймов, осуществляем раскрытие рекурсивно
			local subframes = p.expandAliases(frame, randomize, aliasReference)
			
			if #subframes == 1 then
				table.insert(newFrames, subframes[1])
			else
				table.insert(newFrames, subframes)
			end
		else
			-- Выбираем таблицу псевдонимов в зависимости от указания модификации
			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]
				end
			else
				-- Используем ванильные псевдонимы
				alias = aliases[frame.name]
			end
			
			-- Псевдоним найден
			if alias then
				-- Раскрываем его
				newFrame = p.getAlias(alias, frame)
	
				-- Сохраняем ссылку на исходный псевдоним
				if aliasReference then
					local curFrame = #newFrames + 1
					local aliasData = { ["фрейм"] = frame, ["длина"] = #newFrame }
					
					if not expandedAliases then
						expandedAliases = {}
					end
					expandedAliases[curFrame] = aliasData
				end
				
				-- Добавляем фрейм(ы)
				mergeList(newFrames, newFrame)
				
				-- Включаем рандомизацию первого фрейма для псевдонимов вида
				-- «любой предмет», если этот псевдоним является единственным
				-- фреймом.
				if newFrames["рандомизация"] ~= "никогда" and mw.ustring.match(frame.name, '^' .. i18n.prefixesMatch.any .. ' ') then
					newFrames["рандомизация"] = true
				else
					newFrames["рандомизация"] = false
				end
			else
				-- Не псевдоним; добавляем как есть
				table.insert(newFrames, frame)
			end
		end
	end
	
	newFrames["ссылканапсевдонимы"] = expandedAliases
	
	-- Возвращаем раскрытую последовательность фреймов
	return newFrames
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

		-- Раскрываем псевдонимы
		local alias
		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]
			end
		else
			alias = aliases[frame.name]
		end

		-- Псевдоним найден
		if alias then
			-- Раскрываем его
			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["рандомизация"] ~= "никогда" and mw.ustring.match(frame.name, '^' .. i18n.prefixesMatch.any .. ' ') then
				frames["рандомизация"] = true
			else
				frames["рандомизация"] = false
			end

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

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

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

-- Раскрывает заданный псевдоним в таблицу, дополнив её данными из материнского фрейма
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.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
		
		-- Если у фрейма в псевдонимах задана модификация, то она имеет приоритет
		-- Так можно создавать для модов псевдонимы с ванильными вещами
		if expandedFrame.mod then
			if not vanilla[mw.ustring.lower(expandedFrame.mod)] then
				expandedFrame.mod = modNames[expandedFrame.mod] or expandedFrame.mod
			else
				expandedFrame.mod = nil
			end
		else
			expandedFrame.mod = parentFrame.mod
		end

		-- Добавляем фрейм в список
		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
	
	local title = frame.title and ('[' .. frame.title .. ']') or ''
	local english = frame.english and ('[' .. frame.english .. ']') or ''
	local commons = frame.commons and ('[' .. frame.commons .. ']') or ''
	local mod = frame.mod or 'minecraft'
	local num = frame.num or '1'
	local text = frame.text and ('[' .. frame.text .. ']') or ''
	
	return title .. english .. commons .. mod .. ':' .. frame.name .. ',' .. num .. text
end

-- Преобразует последовательность фреймов в текстовый формат
function p.stringifyFrames(frames)
	local strFrames = {}
	for i, frame in ipairs(frames) do
		if frame[1] then
			-- контейнер подфреймов
			strFrames[i] = '{' .. p.stringifyFrames(frame) .. '}'
		else
			strFrames[i] = p.stringifyFrame(frame)
		end
	end
	return table.concat(strFrames, ';')
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*(.*)')
	if title then
		frame.title = title
		frameText = rest
	end
	
	-- Англоязычный заголовок
	local english, rest = mw.ustring.match(frameText, '^%[([^%]]*)%]%s*(.*)')
	if english then
		frame.english = english
		frameText = rest
	end
	
	-- Название файла в общем хранилище
	local commons, rest = mw.ustring.match(frameText, '^%[([^%]]*)%]%s*(.*)')
	if commons then
		frame.commons = commons
		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*(.*)')
	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
		frameText = frameText:gsub('^:', '')
	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