Minecraft Wiki
Advertisement
Данная страница входит в проект Обновление модулей.

Черновик разрабатываемой новой версии модуля Инвентарный слот.

-------------------------------------------------------------------
--- Модуль для отображения инвентарных слотов в Minecraft Wiki.
--- Форк [[:en:Special:Permalink/1326065]]
-------------------------------------------------------------------
-- <nowiki>

local p = {}
local slot_utils = require("Модуль:Инвентарный слот2/Utils")

local i18n = {
	filename = 'Invicon $1',
	legacyFilename = 'Grid $1.png',
	modLink = '$1/$2',
	moduleAliases = [[Модуль:Инвентарный слот2/Псевдонимы]],
	moduleInvData = [[Модуль:ИнвСпрайт]],
	moduleRandom = [[Модуль:Случайные числа]],
	moduleSprite = [[Модуль:Спрайт]],
	moduleProcessArgs = [[Модуль:ProcessArgs]],
	-- Список особых префиксов, которые должны быть обработаны другими модулями
	-- (в том числе убраны из ссылок)
	prefixes = {
		any = {'Любой', 'Любая', 'Любое', 'Любые'},
		matching = {'Соответствующий', 'Соответствующая', 'Соответствующее', 'Соответствующие'},
		damaged = {'Повреждённый', 'Повреждённая', 'Повреждённое', 'Повреждённые'},
	},
    -- Проверяет строку name на соответствие какой-либо из строк, заданных выше
    -- в prefixes с ключом prefixName. Возвращает true/false.
	matchesPrefix = function(name, prefixName)
		local prefixData = p.i18n.prefixes[prefixName]
		assert(prefixData, ("недопустимый ID префикса: %s"):format(prefixName))
		for _, prefix in ipairs(prefixData) do
			if name:find(prefix) == 1 then
				return true
			end
		end
		return false
	end,
	stripPrefix = function(name, prefixName)
		local prefixData = p.i18n.prefixes[prefixName]
		assert(prefixData, ("недопустимый ID префикса: %s"):format(prefixName))
		for _, prefix in ipairs(prefixData) do
			local name, matched = name:gsub("^" .. prefix .. " ", "")
			if matched == 1 then
				return name
			end
		end
		return name
	end,
	suffixes = {
		be = 'BE',
		lce = 'LCE',
	},
	templateFileUrl = 'FileUrl',
	moduleModData = "Модуль:ИнвСпрайт/$1"
}
p.i18n = i18n

local random = require( i18n.moduleRandom ).random
local sprite = require( i18n.moduleSprite ).sprite
local processArgs = require( i18n.moduleProcessArgs )
local modNameAliases = mw.loadData("Модуль:Модификации")

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

local pageName = mw.title.getCurrentTitle().text
local vanillaPrefixes = { v = 1, vanilla = 1, mc = 1, minecraft = 1 }

function p.loadModData(mod)
	return slot_utils.loadMaybeData(("Модуль:ИнвСпрайт/%s"):format(mod))
end

-- Создаёт HTML-код предмета.
--
-- frame: структура фрейма, содержащая данные фрейма.
-- i: индекс фрейма или подфрейма
-- args: обработанные аргументы модуля
local function makeItem( frame, i, args )
	local item = mw.html.create( 'span' )
	    :addClass( 'invslot-item' )
	    :addClass( args.imageClass ) -- no-op when given nil
	    :cssText( args.imageStyle ) -- no-op when given nil
	
	if frame.name == '' then
		return item
	end
	
	local category
	local title = frame.title or ((args.title or ''):gsub("^%s+", ""):gsub("%s+$", ""))
	local mod = frame.mod
	local name = frame.name or ''
	local num = frame.num
	local description = frame.text
	
	local img, idData, modData, enName, imageOverride
	if mod then
		local modResults = p.resolveModData(mod, name)
		modData = modResults.modData
		idData = modResults.idData
		img = modResults.img
		enName = modResults.enName
		imageOverride = modResults.imageOverride
	elseif ids[name] then
		idData = ids[name]
	elseif name:match( '\.gif$' ) or name:match( '\.png$' ) then
		img = i18n.filename:gsub( '%$1', name )
		-- Remove file extension from name
		name = name:sub( 0, -5 )
	else
		img = i18n.legacyFilename:gsub( '%$1', name )
	end
	
	local link = args.link or ''
	if link == '' then
		if mod then
			link = i18n.modLink:gsub( '%$1', mod ):gsub( '%$2', name )
		else
			link = i18n.stripPrefix(name, "damaged")
			for _, suffix in pairs( i18n.suffixes ) do
				link = link:gsub( ' ' .. suffix .. '$', '' )
			end
		end
	elseif link:lower() == 'нет' then
		link = nil
	end
	if link == pageName then
		link = nil
	end
	
	local formattedTitle
	local plainTitle
	if title == '' then
		plainTitle = name
	elseif title:lower() ~= 'нет' 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
	
	description = p.resolveMultipage(mod, idData, description)
	
	item:attr{
		['data-minetip-title'] = formattedTitle,
		['data-minetip-text'] = description,
		['data-modinfo-text'] = mod,
		['data-minetip-lowtitle'] = enName
	}
	
	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( '[[File:', img, '|32x32px|link=', link or '', '|', escapedTitle, ']]' )
	else
		local image
		local data = 'ИнвСпрайт'
		if mod then
			image = args.stylesheet or imageOverride
			data = data .. '/' .. mod
		end
		
		if link then
			item:wikitext( '[[', link, '|' )
		end
		
		local image, spriteCat = sprite{
			["масштаб"] = args.scale, ["данныеID"] = idData, ["назв"] = plainTitle,
			["изобр"] = image, ["данные"] = data,
			nourl = args.nourl,
		}
		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 )
				:cssText( args.numberStyle ) -- no-op when given nil
		if img and link then
			item:wikitext( ']]' )
		end
	end
	
	if idData and link then
		item:wikitext( ']]' )
	end
	
	item:wikitext( category )
	
	return item
end

---- Функции для операций над псевдонимами. ------------------------------------

--[[Returns a new table with the parts of the parent frame
	added to the alias
--]]
function p.getAlias( aliasFrames, parentFrame )
	-- If alias is just a name, return the parent frame with the new name
	if type( aliasFrames ) == 'string' then
		local expandedFrame = mw.clone( parentFrame )
		expandedFrame.name = aliasFrames
		return { expandedFrame }
	end
	
	-- Single frame alias, put in list
	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
			expandedFrame = slot_utils.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
		
		expandedFrames[i] = expandedFrame
	end
	
	return expandedFrames
end

function p.expandAlias( parentFrame, alias )
	error("Вызвана устаревшая функция p.expandAlias из модуля Инвентарный слот!")
	-- return p.getAlias( alias, parentFrame )
end
---- Функции для операций, связанных с модификациями. --------------------------

--[[
Возвращает окончательное название модификации из приведённого.

Задействует псевдонимы для обозначения модификаций и оригинальной игры.

Параметры:
• mod (строка) — название модификации, введённое пользователем.

Возвращает каноническое название модификации (строку) или nil, если название
указывает на оригинальную игру.
]]
function p.resolveMod(mod)
	if (not mod)
	    or (vanillaPrefixes[mw.ustring.lower(mod)])
	    or (mod == '') then
	    	-- mw.log("mod = " .. (tostring(mod) or "?") .. ", type = " .. type(mod) .. "; rejecting" )
		    return nil
	else
		local mod = modNameAliases[mod] or mod
		return (mod:gsub('_', ' '))
	end
end

function p.resolveModData(mod, name)
	local results = {}
	results.modData = p.loadModData(mod)
	local idListOverride = results.modData['настройки']['списокID']
	results.imageOverride = results.modData['настройки']['изобр']
	if idListOverride then
		results.modData = slot_utils.loadMaybeData('Модуль:' .. idListOverride)
	end
	
	
	-- River said: "wtb .?"; I agree
	results.modData = results.modData and results.modData["IDы"]
	results.idData = results.modData and results.modData[name]
	results.enName = results.idData and results.idData.en
	
	if not (results.modData and results.idData) then
		results.img = i18n.legacyFilename:gsub( '%$1', name .. ' (' .. mod .. ')' )
	end
		
	return results
end

function p.loadModAliases(mod)
	return slot_utils.loadMaybeData(("Модуль:ИнвСпрайт/%s/Псевдонимы2"):format(mod))
end

function p.resolveMultipage(mod, idData, description)
	if mod == 'GregTech' and idData and idData["страница"] then
		local mat_info = slot_utils.loadMaybeData('Модуль:Обработка/Материалы/' .. idData["страница"])
		if mat_info then
			local formula = mat_info["Формула"]
			local melt = mat_info["Плавление"]
			local boil = mat_info["Испарение"]
			return (description or '') .. '/&7Формула: &e' .. formula .. '/&6t° плавления: ' .. melt .. 'K/&ct° испарения: ' .. boil .. 'K'
		end
	end
	
	return description
end

---- Функции для операций над фреймами. ----------------------------------------
-- Фрейм — структура, описывающая один кадр анимации инвентарного слота.
--
-- Поля фрейма:
-- • title (строка): заглавие всплывающей подсказки. Если не задано, подсказки нет.
-- • mod (строка): название модификации. Если не задано, предмет берётся из основной игры.
-- • name (строка): название предмета. Должно быть задано.
-- • num (число): количество предметов в ячейке. Равно nil, если не задано.
-- • text (строка): текст всплывающей подсказки.
----

--[[
Возвращает строковое представление структуры фрейма frame.
]]
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

--[[
Берёт последовательность структур фреймов frames и заменяет в этой
последовательности каждую структуру её строковым представлением. Затем
возвращает одну строку, объединяющую все фреймы.
]]
function p.stringifyFrames( frames )
	for i, frame in ipairs( frames ) do
		frames[i] = p.stringifyFrame( frame )
	end
	return table.concat( frames, ';' )
end

--[[
Берёт текстовое представление фрейма (frameText) и переопределение модификации
(mod). Возвращает структуру фрейма, соответствующую тексту frameText. Если во
frameText не задана модификация, используется значение mod.
]]
function p.makeFrame( frameText, mod )
	-- Simple frame with no parts
	if not frameText:match( '[%[:,]' ) then
		return {
			mod = p.resolveMod(mod),
			name = (frameText:gsub("^%s+", ""):gsub("%s+$", "")),
		}
	end
	
	local frame = {}
	
	-- Эти две функции должны быть вызваны первыми, чтобы gsub ниже не портил подсказки.
	frameText, frame.title = slot_utils.removePrefix(frameText, '%s*%[%s*(.-)%s*%]%s*')
	frameText, frame.text = slot_utils.removeSuffix(frameText, '%s*%[%s*(.-)%s*%]%s*')
	
	-- Убирает пробелы, окружающие особые символы (квадратные скобки, двоеточия,
	-- запятые и точки с запятой).
	frameText = frameText:gsub( '%s*([%[%]:,;])%s*', '%1' )
	
	frameText, frame.mod = slot_utils.removePrefix(frameText, '([^:,;%[%]]+):', mod)
	frame.mod = p.resolveMod(frame.mod)
	
	frameText, frame.name = slot_utils.removePrefix(frameText, '[^,;%[]+')
	frameText, frame.num = slot_utils.removePrefix(frameText, ',(%d+)')
	frame.num = math.floor( frame.num or 0 )
	if frame.num == 0 then
		frame.num = nil
	end
	
	return frame
end
function p.getParts( frameText, mod )
	error("Вызвана устаревшая функция p.getParts из модуля Инвентарный слот!")
	-- return p.makeFrame( frameText, mod )
end

---- Парсер списка фреймов. ----------------------------------------------------
--[[
Разбивает список фреймов на строки, описывающие конкретные фреймы.

Учитывает, что разделитель фреймов (точка с запятой) может находиться ещё и в
подсказке.

Выдаёт ошибку при некорректном использовании квадратных скобок.

Возвращает последовательность строковых представлений фреймов. Начальные и
конечные пробельные символы в каждой строке уже убраны.
]]
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+$", "")) -- faster mw.text.trim
	end
	
	return frames
end

--[[
Удаляет синтаксис подфреймов, выдавая ошибку при его некорректном использовании.

Принимает текст фрейма (frameText) и состояние наличия подфрейма (subframe).

Возвращает новые значения frameText и subframe
]]
local function setSubframe(frameText, subframe)
	local matchedStart, matchedEnd
	
	frameText, matchedStart = frameText:gsub('^{%s*', '')
	if matchedStart == 1 then
		subframe = true
	end
	
	frameText, matchedEnd = frameText:gsub('}%s*$', '')
	assert(subframe or matchedEnd == 0, "Неожиданная закрывающая фигурная скобка при отсутствии подфреймов.")
	if matchedEnd == 1 then
		subframe = "last"
	end
	
	return frameText, subframe
end

--[[
Таблица parserArgs должна содержать следующие значения:
 • frames: строка со списком фреймов;
 • randomize: булево значение; если ложно, воспроизведение в случайном порядке
    не допускается;
 • defaultMod: название модификации по умолчанию.
]]
function p.parseFrameText(parserArgs)
	local frames = { randomise = parserArgs.randomise }
	local subframes = {}
	local subframe
	local expandedAliases
	local defaultMod = parserArgs.defaultMod
	local splitFrames = splitOnUnenclosedSemicolons(parserArgs.frames)
	
	for _, frameText in ipairs( splitFrames ) do
		frameText, subframe = setSubframe(frameText, subframe)
		local frame = p.makeFrame( frameText, defaultMod )
		local modAliases = frame.mod and p.loadModAliases(frame.mod)
		local newFrame = frame
		
		if aliases or modAliases then
			local id = frame.name
			if frame.mod then
				id = frame.mod .. ':' .. id
			end
			
			local alias = modAliases and modAliases[id] or
				aliases and aliases[id]
			if alias then
				newFrame = p.getAlias( alias, frame )
				if aliasReference then
					local curFrame = #frames + 1
					local aliasData = { frame = frame, length = #newFrame }
					if subframe then
						if not subframes.aliasReference then
							subframes.aliasReference = {}
						end
						subframes.aliasReference[#subframes + 1] = aliasData
					else
						if not expandedAliases then
							expandedAliases = {}
						end
						expandedAliases[curFrame] = aliasData
					end
				end
			end
		end
		
		if subframe then
			slot_utils.mergeList( subframes, newFrame )
			-- Randomise starting frame for "Any *" aliases, as long as the alias is the only subframe
			if frames.randomise ~= 'never' and subframes.randomise == nil and
				i18n.matchesPrefix(frame.name, "any") then
				subframes.randomise = true
			else
				subframes.randomise = false
			end
			if frames.randomise ~= 'never' then
				frames.randomise = false
			end
			if subframe == 'last' then
				-- No point having a subframe containing a single frame,
				-- or the subframe being the only frame
				if #subframes == 1 or #splitFrames == i and #frames == 0 then
					slot_utils.mergeList( frames, subframes )
				else
					table.insert( frames, subframes )
				end
				subframes = {}
				subframe = nil
			end
		else
			-- Randomise starting frame for "Any *" aliases, as long as the alias is the only frame
			if frames.randomise == nil and i18n.matchesPrefix(frame.name, "any") then
				frames.randomise = true
			elseif frames.randomise ~= 'never' then
				frames.randomise = false
			end
			slot_utils.mergeList( frames, newFrame )
		end
	end
	
	frames.aliasReference = expandedAliases
	
	return frames
end

---- Основная функция. ---------------------------------------------------------
function p.slot_main(pargs)
	local frames = pargs.frames
	local animated = frames and #frames > 1
	local imgClass = pargs.imageClass
	local body = mw.html.create( 'span' )
	    :addClass( 'invslot' )
		:addClass( pargs.class ) -- no-op when given nil
	    :css{ ['vertical-align'] = pargs.align }
	    :cssText( pargs.style ) -- no-op when given nil
	    
	if animated then
		body:addClass( 'animated2' )
	end
	
	if pargs.default then
		body:css( 'background-image', mw.getCurrentFrame():expandTemplate{ title = i18n.templateFileUrl, args = { pargs.default .. '.png' } } )
	end
	
	local scale = pargs.scale
	if scale ~= 1 then
		if scale == 0.5 then
			body:css('top', '-1px')
		end
		
		local size = scale * 32
		body:css{
			width = size .. 'px',
			height = size .. 'px'
		}
	end
	
	--mw.logObject( frames )
	if not frames then
		return tostring( body )
	end
	
	local activeFrame = frames.randomise == true and random( #frames ) or 1
	for i, frame in ipairs( frames ) do
		local item
		-- Table is a list, must contain subframes
		if frame[1] then
			item = body:tag( 'span' ):addClass( 'animated2-subframe' )
			local subActiveFrame = frame.randomise and random( #frame ) or 1
			for sI, sFrame in ipairs( frame ) do
				local sItem = makeItem( sFrame, sI, pargs )
				item:node( sItem )
				
				if sI == subActiveFrame then
					sItem:addClass( 'animated2-active' )
				end
			end
		else
			item = makeItem( frame, i, pargs )
			body:node( item )
		end
		if i == activeFrame and animated then
			item:addClass( 'animated2-active' )
		end
	end
	
	return tostring( body )
end

---- Основная точка входа для Шаблон:Инвентарный слот. -------------------------
-- Другие модули должны вызывать p.slot_main.
----

--[[
Параметры фрейма.
Формат: Локализованное название (оригинальное название): описание

Совместимые с оригинальными:
 • первый параметр (содержащий данные слота);
 • мод (mod): модификация по умолчанию;
 • классизобр (imgclass): CSS-класс изображения;
 • стильцифр (numstyle):
 • ссылка (link)
 • выравн (align)
 • класс (class)
 • стиль (style)
 • умолчание (default)
 • назв (title)
 • таблспрайтов (spritesheet)
 • nourl

НЕ СОВМЕСТИМЫЕ с оригинальными:
 • parsed: используйте p.slot_main() вместо parsed.
 • модпсевдонимы (modaliases): псевдонимы не глобальны для всех модификаций, а локальны для каждой модификации.
 
Введённые помимо оригинальных:
 • стильизобр
 • масштаб
 • умолчаниеCSS
 • Версия
]]
function p.slot( f )
	local args = require(i18n.moduleProcessArgs).merge(true)
	
	local pargs = {}
	
	assert(not args.parsed, "Флаг parsed несовместим с переводной версией. Используйте p.slot_main вместо него.")
	
	local defaultMod = args["мод"]
	
	pargs.stylesheet = args["таблспрайтов"]
	pargs.class = args["класс"]
	pargs.imageClass = args["классизобр"]
	
	pargs.style = args["стиль"]
	pargs.imageStyle = args["стильизобр"]
	pargs.numberStyle = args["стильцифр"]
	
	pargs.align = args["выравн"]
	pargs.scale = tonumber(args["масштаб"] or 1)
	
	pargs.default = args["умолчание"]
	pargs.cssDefault = args["умолчаниеCSS"]
	pargs.defaultVersion = args["Версия"]
	
	pargs.link = args["ссылка"]
	pargs.title = args["назв"]
	
	pargs.frames = p.parseFrameText({
		frames     = args[1],
		randomize  = pargs.class == 'invslot-large' and 'never' or nil,
		defaultMod = defaultMod
	})
	
	return p.slot_main(pargs)
end

return p

-- </nowiki>
Advertisement