Module:Item

From Terraria Wiki
Jump to navigation Jump to search
Important.svg
CAUTION: Terraria Wiki code is complex!!!
If you want to use this code on another wiki, wiki.gg staff are not able to assist you.
Please consider picking a different wiki to adapt code from, or making your own templates!
Remember that content on a wiki is more important than fancy formatting.
Lua.svg Documentation The documentation below is transcluded from Module:Item/doc. (edit | history)

This module provides the functionality of the {{item}} template.

Usage

Wikitext

The module can be called from wikitext with the functions listed below.

go

{{#invoke:Item| go | <string of parameters> }}

Displays the entity. This function expects its parameters as one long string in the format @param1:value^@param2:value^. It is about 20% faster overall to do this compared to using individual parameters, and performance is an important consideration for this widely used module.

This function is generally only intended to be called from the template mentioned above. See the template documentation for details about the available parameters.

purge

{{#invoke:Item| purge | <string of parameters> }}

When the go function is invoked, its result is cached to improve the performance of subsequent invocations with identical parameters. This function purges the cache, which may be necessary in case there are issues.

Take the invocation {{#invoke:Item|go|@name:Stone Block^@text:Stone^}} (via {{item|Stone Block|Stone}}) as an example. The go function computes the output for this set of parameters and stores that output to the cache. When this very same invocation occurs again later on the same page, the go function does not have to compute the output fully anew again, because it already did so once before. It can simply retrieve the output from the cache.

The cache is cleared automatically after 24 hours but it can be cleared earlier by invoking the purge function. This may be necessary if a cached output is erroneous.

This function is generally only intended to be called from the {{item/purge}} template. Simply take the normal {{item}} transclusion that should be purged (e.g. {{item|Stone Block|Stone}}), change item to item/purge (e.g. {{item/purge|Stone Block|Stone}}), and save it on a sandbox page (just previewing is not sufficient). Make sure to include any relevant transclusions of {{options}} that affect the {{item}}.

storeImageInfo

{{#invoke:Item| storeImageInfo | <file name> }}

Stores image info to the Imageinfo cargo table.

Other modules

The module can be called from another module with the functions listed below.

parse

require('Module:Item').parse('<string of parameters>')

Transforms the combined parameter string into a table of parameters.

Examples
Code Variable result
local item = require('Module:Item')
local result = item.parse('@name:Stone Block^@text:Stone^@image:Ebonstone Block.png^@note:Corrupt^')
{
    [1] = "Stone Block",
    [2] = "Stone",
    ["image"] = "Ebonstone Block.png",
    ["name"] = "Stone Block",
    ["note"] = "Corrupt",
    ["text"] = "Stone",
}

-- Import functions
local vardefine = mw.ext.VariablesLua.vardefine
local trim = mw.text.trim
local cargo = mw.ext.cargo
local cache = mw.ext.LuaCache
local eicons = require('Module:Exclusive').simpleEicons
local tr = require('Module:Tr')

---Holds the tables with the l10n information for the different languages, taken from the l10n submodule.
local l10nInfo = mw.loadData('Module:Item/l10n')

---Holds the l10n information for the current language, as key-value pairs.
local l10nTable

---The current language. Determines which l10n table to use.
local lang

---Holds the arguments from the template call.
local args


local shouldCache = true

local options, optionsSnap = (function()
	local options, snap = {}, ''
	local op = require('Module:Options')
	for _,key in ipairs(op.getAllKeys('item')) do
		local v = op.get('item', key)
		snap = snap..v..'∥'
		if v ~= '' then
			options[key] = v
		end
	end
	return options, snap
end)()

-- Note: 
-- for getArg('x', f()), f() will always be parsed; 
-- for getArg('x') or f(), f() will only be parsed when used.
local function getArg(key, defaultForEmpty, defaultForBlank)
	local value = args[key]
	if not value then -- there is no |key=xxx 
		return defaultForEmpty 
	elseif value == '' then -- explicit |key=|
		if defaultForBlank == nil then
			return defaultForEmpty
		else
			return defaultForBlank
		end
	else
		return value
	end
end


local md5 = function(str)
	return mw.hash.hashValue('md5', str)
end

local function pairsByKeys(t)
	local a = {}
	for n in pairs(t) do table.insert(a, n) end
	table.sort(a, function(x, y) return tostring(x) < tostring(y) end)
	local i = 0      -- iterator variable
	local iter = function ()   -- iterator function
		i = i + 1
		if a[i] == nil then return nil
		else return a[i], t[a[i]]
		end
	end
	return iter
end

---@param value string
---@param default bool
---@return bool
local function bool(value, default)
	if value then
		value = string.lower(tostring(value))
		if value == 'y' or value == 'yes' or value == '1' or value == 'true' or value == 'on' then
			return true
		elseif value == 'n' or value == 'no' or value == '0' or value == 'false' or value == 'off' then
			return false
		end
		return default
	end
end

---Return the l10n string associated with the `key`.
---@param key string
---@return string
local function l10n(key)
	return l10nTable[key] or l10nInfo['en'][key]
end

local function getCacheKey(arg, lang)
	local arr = { lang, optionsSnap }
	for k,v in pairsByKeys(arg) do
		arr[#arr+1] = k..':'..v
	end
	return table.concat(arr, '※');
end

---Split the `str` on each `div` in it and return the result as a table. It can be over 60x faster then mw.text.split
---Original version credit: http://richard.warburton.it. This version trims each substring.
---@param div string
---@param str string
---@return table|boolean
local function explode(div,str)
	if (div=='') then return false end
	local pos,arr = 0,{}
	-- for each divider found
	for st,sp in function() return string.find(str,div,pos,true) end do
		arr[#arr + 1] = trim(string.sub(str,pos,st-1)) -- Attach chars left of current divider
		pos = sp + 1 -- Jump past current divider
	end
	arr[#arr + 1] = trim(string.sub(str,pos)) -- Attach chars right of last divider
	return arr
end

---Encode a string for use in a MediaWiki URI fragment.
---@param anchorid string
---@return string
local function anchorencode(anchorid)
	-- mw.uri.anchorEncode turns spaces into the HTML entity for underscores but
	-- we don't want that
	-- (e.g. `mw.uri.anchorEncode('hello world')` => 'hello&#95;world')
	-- (the parentheses are there to drop the second return value of `string.gsub`)
	return (string.gsub(mw.uri.anchorEncode(anchorid), '&#95;', '_'))
end

---Extract scale, width, and height from an input string. Up to two of the three can be empty in the input.
---Example: `5x7px*0.75` → `0.75`, `5`, `7`
---@param size string
---@return string basescale
---@return number width
---@return number height
local function parseSize(size)
	if not size then return end
	local basescale, width, height
	size, basescale = unpack(explode('*', size))
	if size ~= '' then
		width, height = unpack(explode('x', string.gsub(size, 'px', '')))
		width, height = tonumber(width), tonumber(height)
		if width == 0 then width = nil end
		if height == 0 then height = nil end
	end
	return basescale, width, height
end

---Return width, height, and caching date for the specified `imagename` from the Imageinfo cargo table.
---@param imagename string
---@return number width
---@return number height
---@return string cached
local function getInfoFromCargo(imagename)
	-- try to get from cargo cache
	local result = mw.ext.cargo.query('Imageinfo', 'width, height, cached', {
		-- md5name for case-sensitive query.
		where = 'md5name='.. "'"..md5(imagename).."'",
		orderBy = "cached DESC",
		limit = 1,
	})
	for _, row in ipairs(result) do
		return tonumber(row['width']), tonumber(row['height']), row['cached']
	end
end

---Store width and height of the specified `imagename` to the Imageinfo cargo table and return them.
---Width and height are computed via the `#imgw:` and `#imgh:` parser functions, respectively.
---@param imagename string
---@return number width
---@return number height
local function storeInfoToCargo(imagename)
	-- don't cache {{item}}'s result when parsing imagesize fails.
	shouldCache = false
	local imageTitle = mw.title.new("File:" .. imagename)
	local width, height = imageTitle.file.width, imageTitle.file.height
	if width and width ~= 0 and height and height ~= 0 then
		shouldCache = true -- ok, cache it.
		mw.getCurrentFrame():callParserFunction('#cargo_store:_table=Imageinfo',{
			image = imagename,
			md5name = md5(imagename),
			width = width,
			height = height,
			cached = os.time(),
		})
	end
	return width, height
end

---Retrieve the dimensions of the specified `image` from the Imageinfo cargo table.
---If it doesn't have any data for the image yet, store it.
---@param imagename string
---@return number width
---@return number height
local function getSizeInfo(imagename)
	local width, height, cached = getInfoFromCargo(imagename)
	-- cache missed, init cache
	if not cached then
		width, height = storeInfoToCargo(imagename)
	end
	if width == 0 then width = nil end
	if height == 0 then height = nil end
	return width, height
end

---Compute the final width and height of the image.
---If necessary, retrieve data from or store data to the Imageinfo cargo table.
---@param imagename string
---@param width number
---@param height number
---@param scale number
---@param maxwidth number
---@param maxheight number
---@return number width
---@return number height
local function getImageSize(imagename, width, height, scale, maxwidth, maxheight)
	-- get size info from image file itself (may be expensive)
	local w, h = getSizeInfo(imagename) -- store data to cache

	-- if width and height are not given as input, but scale/maxwidth/maxheight are, then
	-- set width and height to the original dimensions of the image
	if not width and not height and (scale or maxwidth or maxheight) then
		width, height = w, h
	end

	-- apply scale to width/height if needed
	if scale then
		if width then width = width * scale end
		if height then height = height * scale end
	end

	-- apply maxwidth/maxheight
	if maxwidth then
		if width then
			if width > maxwidth then width = maxwidth end
		else
			if height then width = maxwidth end
		end
	end
	if maxheight then
		if height then
			if height > maxheight then height = maxheight end
		else
			if width then height = maxheight end
		end
	end

	-- round to natural numbers
	if width then width = math.ceil(width) end
	if height then height = math.ceil(height) end

	return width, height
end

---Extract width and height from an input string.
---Example: `6x9px` → `6`, `9`
---@param maxsize string
---@return number maxwidth
---@return number maxheight
local function parseMaxSize(maxsize)
	if not maxsize then return end
	local maxwidth, maxheight = unpack(explode('x', string.gsub(maxsize, 'px', '')))
	maxwidth, maxheight = tonumber(maxwidth), tonumber(maxheight)
	if maxwidth == 0 then maxwidth = nil end
	if maxheight == 0 then maxheight = nil end
	return maxwidth, maxheight
end

---Assemble the final wikicode for an image.
---@param imagename string
---@param link string
---@param text string
---@param size string As accepted by the `[[File:` syntax, e.g. `5x7px*0.75`.
---@param scale number This will be multiplied by the scale in `size`, if necessary.
---@param maxsize string
---@return string
local function imagecode(imagename, link, text, size, scale, maxsize)
	local imageOutput = '[[File:' .. imagename .. '|link='.. link .. '|' .. text
	if size or scale or maxsize then
		local basescale, width, height = parseSize(size) -- width, height: number or nil (basescale is string!)
		scale = (tonumber(scale) or 1) * (tonumber(basescale) or 1) -- combine the scale parameter and scale from the size parameter
		if scale == 0 or scale == 1 then
			scale = nil
		end
		local maxwidth, maxheight = parseMaxSize(maxsize)
		width, height = getImageSize(imagename, width, height, scale, maxwidth, maxheight) -- can be 0
		if width or height then
			imageOutput = imageOutput .. '|' .. (width or '') .. 'x' .. (height or '') .. 'px'
		end
	end
	return imageOutput .. ']]'
end

---Return the full `[[File:` wikicode for each image in the input (multiple are separated with `/`).
---@param image string
---@param link string
---@param text string
---@param size string
---@param scale string
---@param maxsize string
---@return string
local function images(image, link, text, size, scale, maxsize)

	if not image:find('/') then
		-- there is only one image in the input
		return imagecode(image, link, text, size, scale, maxsize)
	end

	-- there are multiple images in the input, separated with a slash
	image = explode('/', image)
	local result = ''
	if size and size:find('/') then
		-- there are multiple sizes in the size parameter
		size = explode('/', size) -- so turn it into a table
		for i, v in ipairs(image) do -- iterate over the images
			result = result .. imagecode(v, link, text, size[i], scale, maxsize) -- create the wikicode (using the respective size)
		end
	else
		for i, v in ipairs(image) do -- iterate over the images
			result = result .. imagecode(v, link, text, size, scale, maxsize) -- create the wikicode
		end
	end
	return result
end

---Return a string like `Internal Item ID: `, depending on the `_type`.
---@param _type '"item"'|'"tile"'|'"wall"'|'"npc"'|'"mount"'|'"buff"'|'"projectile"'|'"armor"'
---@return string
local function getIdText(_type)
	local id_text
	if _type == 'item' then -- a shortcut for faster
		id_text = l10n('id_text_item')
	elseif _type == 'tile' then
		id_text = l10n('id_text_tile')
	elseif _type == 'wall' then
		id_text = l10n('id_text_wall')
	elseif _type == 'npc' then
		id_text = l10n('id_text_npc')
	elseif _type == 'mount' then
		id_text = l10n('id_text_mount')
	elseif _type == 'buff' or _type == 'debuff' then
		id_text = l10n('id_text_buff')
	elseif _type == 'projectile' then
		id_text = l10n('id_text_projectile')
	elseif _type == 'armor' then
		id_text = l10n('id_text_armor')
	else
		id_text = l10n('id_text_item')
	end
	return id_text
end

-----------------------------------------------------------------
-- main return object
return {

go = function(frame, inputArgs)
	args = inputArgs or frame:getParent().args
	lang = getArg('lang') or require('Module:Lang').get() or 'en'

	-- cache?
	local cache_key = getCacheKey(args, lang)
	local cached = cache.get(cache_key)
	if cached then
		return cached
	end
	
	l10nTable = l10nInfo[lang] or l10nInfo['en']

	local name = trim(getArg(1, ''))
	if name == '' then
		name = frame:expandTemplate{ title = (getArg('type') or options.type or 'item')..'NameFromId', args = {getArg('id'), lang='en'} }
	end
	local trName = (lang == 'en') and name or tr.translate(name, lang)
	
	local text = getArg('t')
	if not text then
		local t = trim(getArg(2, ''))
		if t == '' then
			text = trName
		else
			text = frame:expandTemplate{ title = 'displaytext', args = {name, t, lang=lang} }
		end
	end
	
	local nolink = bool(getArg('nolink') or options.nolink)
	local link = nolink and '' or getArg('link', nil, '') or tr.translateLink(name, lang) -- now: link == '' means nolink

	-- set output flags
	local outputImage, outputText, outputTable = true, true, false
	local mode = getArg('mode', nil, '') or options.mode
	if mode then
		if mode == 'image' or mode == 'imageonly' or mode =='onlyimage' then
			outputText = false
		elseif mode == 'text' or mode == 'noimage' then
			outputImage = false
		elseif mode == 'table' or mode == '2-cell' then
			outputTable = true
		end
	end

	local hovertext
	if outputImage and not outputText then
		-- with image only, the hovertext will only be displayed on the image, so it should be text or {{tr|name}} or link (in that order)
		if text ~= '' then
			hovertext = text
		elseif name ~= '' then
			hovertext = trName
		else
			hovertext = link
		end
	else
		-- with image and/or text, the hovertext will be displayed on the image and on the text, so it should be {{tr|name}} or text or link (in that order)
		if name ~= '' then
			hovertext = trName
		elseif text ~= '' then
			hovertext = text
		else
			hovertext = link
		end
	end

	local class = 'i'

	local imageOutput, textOutput
	-- wikicode for the image(s)
	if outputImage then
		local imageArg = getArg('image') or string.gsub(name, "[:/]%s*", " ")..'.'..getArg('ext', 'png')
		if string.find(imageArg, '%[%[[fF]ile:') then
			imageOutput = '<span class="img">' .. imageArg .. '</span>'
		else
			imageOutput = images(imageArg, link, hovertext, getArg('size'), getArg('scale') or options.scale, getArg('maxsize') or options.maxsize)
		end
	else
		imageOutput = ''
	end
	-- wikicode for the text
	if outputText then
		local note, note2, bignote = getArg('note'), getArg('note2'), getArg('bignote')

		local id = getArg('id')
		local showid = bool(getArg('showid') or options.showid or (id and 'y'))

		-- prepare: wrap?
		local wrap 
		if showid or note2 then
			wrap = false
		else
			wrap = bool(getArg('wrap') or options.wrap)
		end

		-- prepare: eicons
		local iconstr = ''
		if text ~= '' then -- no display text no eicons.
			local icons = getArg('icons') or options.icons or 'y'
			icons = bool(icons, icons)
			if icons then
				local small = bool((showid or note2 or wrap) and 'y' or getArg('small') or options.small)
				if icons == true then
					iconstr = eicons(name, lang, small)
				else
					-- make the size of {{eicons}} from {{{icon}}} input match {{{small}}} setting.
					if small then
						iconstr = string.gsub(icons, ' class="eico ', ' class="eico s ')
					else
						iconstr = string.gsub(icons, ' class="eico s ', ' class="eico ')
					end
				end
			end
		end
		
		-- prepare: link and display text
		if link == '' or text == '' or string.find(text, '%[%[.-%]%]') then
			text = '<span title="'..hovertext..'">'..text..'</span>'
		else
			if text == link then
				text = '<span>[['..text..']]</span>'
			else
				text = '<span>[['..link..'|'..text..']]</span>'
			end
		end

		-- assemble HTML code
		local content = text -- item name link text first.
		-- '-w' class means 'wrapmode', optimized for multiple lines of text. But it should be disabled for single line text.
		local wrapclass = false
		if wrap then
			-- eicons in the same line
			if iconstr ~= '' then
				wrapclass = true
				content = content .. iconstr
			end
			-- note in a new line
			if note then
				wrapclass = true
				content = content .. '<span class="note">' .. note .. '</span>'
			end
		else
			-- note in the same line
			if note then
				content = content .. '<span class="note">' .. note .. '</span>'
			end
			-- eicons in the same line
			if iconstr ~= '' then
				content = content .. iconstr
			end
			-- note2 in a new line
			if note2 then
				wrapclass = true
				content = content .. '<span class="note2">' .. note2 .. '</span>'
			end
			-- id in a new line
			if showid then
				wrapclass = true
				local idType = (getArg('type') or options.type or 'item'):lower()
				if not id then
					-- get ID automatically via {{itemIdFromName}} or the like
					id = frame:expandTemplate{ title = idType .. 'IdFromName', args = {name} }
				end
				local idText = getIdText(idType)
				content = content .. '<span class="id">' .. idText .. id .. '</span>'
			end
		end
		if wrapclass then
			class = class .. ' -w'
		end
		if bignote then
			textOutput = '<span>' .. content .. '</span><span>' .. bignote .. '</span>'
		else
			textOutput = '<span>' .. content .. '</span>'
		end
	else
		textOutput = ''
	end

	-- handle custom CSS
	local inputClass, inputCss = getArg('class', nil, '') or options.class, getArg('css', nil, '') or options.css
	if inputClass then
		class = class .. ' ' .. inputClass -- add to existing classes
	end
	local attr = {class = class}
	if inputCss then
		attr.style = inputCss -- set the style attribute to parameter value
	end

	-- anchor:
	local anchor = bool(getArg('anchor') or options.anchor)
	if anchor then
		anchor = mw.text.tag('s', {class = 'anchor', id = anchorencode(name)}, '')
		if lang ~= 'en' then
			anchor = mw.text.tag('s', {class = 'anchor', id = anchorencode(trName)}, '')
		end
	end

	-- output
	local return_string
	if outputTable then
		-- table output
		attr.class = class
		local rowspan = getArg('rowspan')
		local rowspan_text = (rowspan and (' rowspan=' .. rowspan) or '')
		-- prepare the two cells
		local first_cell_pre = rowspan_text .. ' class="il1c"'
		local first_cell_content = mw.text.tag('span', attr, imageOutput)
		if anchor then
			first_cell_content = anchor .. first_cell_content
		end
		local second_cell_pre = rowspan_text .. ' class="il2c"'
		local second_cell_content = mw.text.tag('span', attr, textOutput)
		-- combine
		return_string = first_cell_pre .. " | " .. first_cell_content .. " || " .. second_cell_pre .. " | " .. second_cell_content
	else
		-- non-table output (text/image)
		return_string = mw.text.tag('span', attr, anchor and (imageOutput .. textOutput .. anchor) or (imageOutput .. textOutput) )
	end

	-- cache output for reuse
	if shouldCache then
		cache.set(cache_key, return_string, 3600*24) -- cache for 24 hours
	end

	-- output
	return return_string

end,

purge = function(frame, inputArgs)
	args = inputArgs or frame:getParent().args
	lang = getArg('lang') or require('Module:Lang').get() or 'en'

	local cache_key = getCacheKey(args, lang)
	cache.delete(cache_key)
end,

storeImageInfo = function(frame)
	local width, height = storeInfoToCargo(frame.args[1])
	if not width or width == 0 or not height or height == 0 then
		return
	else
		vardefine('__imageinfo:exists', '1')
		vardefine('__imageinfo:width', width)
		vardefine('__imageinfo:height', height)
		return
	end
end,

}