Jump to content

Module:Inventory slot: Difference between revisions

From Joyful's Civilization
m 1 revision imported
No edit summary
 
Line 1: Line 1:
local p = {}
local p = {}


-- Internationalization data
local i18n = {
local i18n = {
-- Name formats for pages and files
filename = 'Invicon $1',
filename = 'Invicon $1',
legacyFilename = 'Grid $1.png',
legacyFilename = 'Grid $1',
modLink = 'Mods/$1/$2',
modLink = 'Mods/$1/$2',
-- Dependencies
moduleAliases = [[Module:Inventory slot/Aliases]],
moduleAliases = [[Module:Inventory slot/Aliases]],
moduleInvData = [[Module:InvSprite]],
moduleModData = 'Module:InvSprite/Mods/$1',
moduleRandom = [[Module:Random]],
moduleRandom = [[Module:Random]],
moduleSprite = [[Module:Sprite]],
-- List of special prefixes which should be handled by
-- List of special prefixes which should be handled by
-- other modules (such as being moved outside links)
-- other modules (such as being moved outside links)
-- When localizing, you might want to use a separate list of patterns
-- matching the prefixes’ grammatical forms depending on the language
prefixes = {
prefixes = {
any = 'Any',
any = 'Any',
matching = 'Matching',
matching = 'Matching',
damaged = 'Damaged',
damaged = 'Damaged',
unwaxed = 'Unwaxed',
},
},
-- List of suffixes that are usually stripped from links and tooltips
suffixes = {
suffixes = {
rev = 'Revision %d+',
-- berev = 'BE%d+',
-- jerev= 'JE%d+',
be = 'BE',
be = 'BE',
lce = 'LCE',
lce = 'LCE',
sm = 'SM',
},
},
templateFileUrl = 'FileUrl',
}
}
p.i18n = i18n
p.i18n = i18n


-- Global dependencies and constants
local random = require( i18n.moduleRandom ).random
local random = require( i18n.moduleRandom ).random
local sprite = require( i18n.moduleSprite ).sprite
local aliases = mw.loadData( i18n.moduleAliases )
local aliases = mw.loadData( i18n.moduleAliases )
local ids = mw.loadData( i18n.moduleInvData ).ids
local modIds = {}
local pageName = mw.title.getCurrentTitle().text
local pageName = mw.title.getCurrentTitle().text
local vanilla = { v = 1, vanilla = 1, mc = 1, minecraft = 1 }
-- Auxilliary functions --


-- Performs a simple recursive clone of a table's values
-- Splits a given text into fragments separated by semicolons that are not
-- inside square brackets. Originally written by AttemptToCallNil for the
-- Russian wiki.
-- It processes the text byte-by-byte due to being written under a much stricter
-- Lua runtime budget, with no LuaSandbox and mw.text.split being unperformant.
-- See also https://help.fandom.com/wiki/Extension:Scribunto#Known_issues_and_solutions
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, "Excessive square brackets found")
nesting = true
elseif byte == rbrace then
assert(nesting, "Unbalanced square brackets found")
nesting = false
end
end
assert(not nesting, "Unbalanced square brackets found")
frames[frameIndex] = text:sub(splitStart, text:len())
for index = 1, #frames do
frames[index] = (frames[index]:gsub("^%s+", ""):gsub("%s+$", "")) -- faster than mw.text.trim
end
return frames
end
 
-- Performs a simple recursive clone of a table’s values.
-- Probably exists due to mw.clone() being unusable on tables from mw.loadData()
-- at the time (see the link to help.fandom.com above)
local function cloneTable( origTable )
local function cloneTable( origTable )
local newTable = {}
local newTable = {}
Line 44: Line 94:
end
end


--[[Merges a list, or inserts a string
-- Merges a list, or inserts a string or table into a table,
or table into a table
-- depending on what the second argument happens to be
--]]
local function mergeList( parentTable, content )
local function mergeList( parentTable, content )
local i = #parentTable + 1
local i = #parentTable + 1
Line 61: Line 110:
end
end


-- Creates the HTML for an item
-- Creates the HTML node for a given item.
local function makeItem( frame, i, args )
-- The actual icon file is found and added here
local item = mw.html.create( 'span' ):addClass( 'invslot-item' )
local function makeItem( frame, args )
if args.imgclass then
local item = ( mw.html.create('span')
item:addClass( args.imgclass )
:addClass('invslot-item')
end
:addClass(args.imgclass)
if frame.name == '' then
:cssText(args.imgstyle)
)
if (frame.name or '') == '' then
-- Empty frame, no icon to add
return item
return item
end
end
local category
-- Frame parameters
local title = frame.title or mw.text.trim( args.title or '' )
local title = frame.title or mw.text.trim( args.title or '' )
local mod = frame.mod
local mod = frame.mod
local name = frame.name or ''
local name = frame.name
local num = frame.num
local num = frame.num
local description = frame.text
local description = frame.text
local img, idData
-- Split the extension out of the frame’s name
local extension
if name:match('%.gif') or name:match('%.png') then
extension = name:sub(-4)
name = name:sub(0, -5)
elseif name:match('%.webp') then
extension = '.webp'
name = name:sub(0, -6)
else
extension = '.png'
end
-- Determine the file name
local img
if mod then
if mod then
local modData = modIds[mod]
-- Legacy mod support
if not modData and mw.title.new( i18n.moduleModData:gsub( '%$1', mod ) ).exists then
-- Comment out instead of deleting, as other wikis may find it useful
modData = mw.loadData( i18n.moduleModData:gsub( '%$1', mod ) )
img = i18n.legacyFilename:gsub( '%$1', name .. ' (' .. mod .. ')' )
modIds[mod] = modData
else
end
-- Fall back to an individual image if the sprite is lacking
if modData and modData[name] then
img = i18n.filename:gsub( '%$1', name)
idData = modData[name]
end
else
img = img .. extension
img = i18n.legacyFilename:gsub( '%$1', name .. ' (' .. mod .. ')' )
 
end
-- Strip suffixes out
elseif ids[name] then
for _, suffix in pairs( i18n.suffixes ) do
idData = ids[name]
name = name:gsub( ' ' .. suffix .. '$', '' )
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 )
end
end
-- Determine the link’s target
local link = args.link or ''
local link = args.link or ''
if link == '' then
if link == '' then
Line 102: Line 166:
link = i18n.modLink:gsub( '%$1', mod ):gsub( '%$2', name )
link = i18n.modLink:gsub( '%$1', mod ):gsub( '%$2', name )
else
else
-- Strip the “Damaged” prefix out
link = name:gsub( '^' .. i18n.prefixes.damaged .. ' ', '' )
link = name:gsub( '^' .. i18n.prefixes.damaged .. ' ', '' )
for _, suffix in pairs( i18n.suffixes ) do
link = link:gsub( ' ' .. suffix .. '$', '' )
end
end
end
elseif link:lower() == 'none' then
elseif link:lower() == 'none' then
-- Disable the link
link = nil
link = nil
end
end
if link == pageName then
if link and link:gsub('^%l', string.upper) == pageName then
link = nil
link = nil
end
end
-- Tooltip titles. If JavaScript is not enabled, the slot will gracefully
-- degrade to a simplified title without minetip formatting
local formattedTitle
local formattedTitle
local plainTitle
local plainTitle
if title == '' then
if title == '' then
-- If the title is not set, default to the slot’s name
plainTitle = name
plainTitle = name
elseif title:lower() ~= 'none' then
elseif title:lower() ~= 'none' then
-- Special character escapes
plainTitle = title:gsub( '\\\\', '\' ):gsub( '\\&', '&' )
plainTitle = title:gsub( '\\\\', '\' ):gsub( '\\&', '&' )
local formatPattern = '&[0-9a-fk-or]'
-- The default title will have special formatting code stripped out
if plainTitle:match( formatPattern ) then
local formatPatterns = {'&[0-9a-jl-qs-vyzr]', '&#%x%x%x%x%x%x', '&$%x%x%x'}
formattedTitle = title
for _, formatPattern in ipairs( formatPatterns ) do
plainTitle = plainTitle:gsub( formatPattern, '' )
if plainTitle:match( formatPattern ) then
formattedTitle = title
plainTitle = plainTitle:gsub( formatPattern, '' )
end
end
end
if plainTitle == '' then
if plainTitle == '' then
-- If the title field only has formatting code, the frame’s name
-- is automatically used. For minetips it’s done by JavaScript
-- by appending the plain title.
plainTitle = name
plainTitle = name
else
else
-- Re-encode the
plainTitle = plainTitle:gsub( '\', '\\' ):gsub( '&', '&' )
plainTitle = plainTitle:gsub( '\', '\\' ):gsub( '&', '&' )
end
end
elseif link then
elseif link then
if img then
-- Disable the tooltip that will otherwise appear with a link
formattedTitle = ''
formattedTitle = ''
else
plainTitle = ''
end
end
end
-- Minetips are controlled by custom HTML attributes.
-- See [[MediaWiki:Common.js]] for implementation in JavaScript
item:attr{
item:attr{
['data-minetip-title'] = formattedTitle,
['data-minetip-title'] = formattedTitle,
Line 145: Line 218:
}
}
if img then
-- & is re-escaped because mw.html treats attributes as plain text,
-- & is re-escaped because mw.html treats attributes
-- but MediaWiki doesn’t.
-- as plain text, but MediaWiki doesn't
local escapedTitle = ( plainTitle or '' ):gsub( '&', '&' )
local escapedTitle = ( plainTitle or '' ):gsub( '&', '&' )
item:addClass( 'invslot-item-image' )
-- Alt text
:wikitext( '[[File:', img, '|32x32px|link=', link or '', '|', escapedTitle, ']]' )
local altText = img .. ': Inventory sprite for ' .. name .. ' in Minecraft as shown in-game'
else
if link then
local image
altText = altText .. ' linking to ' .. link
if mod then
end
image = args.spritesheet or mod .. 'Sprite.png'
if formattedTitle or plainTitle or link then
altText = altText .. ' with description: ' .. ( formattedTitle or plainTitle or link )
if description then
altText = altText .. ' ' .. description:gsub( '/', ' ' )
end
end
if link then
altText = altText:gsub( '&[0-9a-jl-qs-wr]', '' )
item:wikitext( '[[', link, '|' )
end
local image, spriteCat = sprite{
iddata = idData, title = plainTitle,
image = image, data = 'InvSprite',
nourl = args.nourl,
}
item:node( image )
category = spriteCat
end
end
-- Add the image
item:addClass( 'invslot-item-image' )
:wikitext( '[[File:', img, '|32x32px|link=', link or '', '|alt=', altText, '|', escapedTitle, ']]' )
-- Add the stack number, if present and in 2-999 range
if num and num > 1 and num < 1000 then
if num and num > 1 and num < 1000 then
if img and link then
if link then
item:wikitext( '[[', link, '|' )
item:wikitext( '[[', link, '|' )
end
end
Line 177: Line 249:
:attr{ title = plainTitle }
:attr{ title = plainTitle }
:wikitext( num )
:wikitext( num )
if numStyle then
if args.numstyle then
number:cssText( numStyle )
number:cssText( args.numstyle )
end
end
if img and link then
if link then
item:wikitext( ']]' )
item:wikitext( ']]' )
end
end
end
end
if not img and link then
-- The HTML node is now ready
item:wikitext( ']]' )
end
item:wikitext( category )
return item
return item
end
end


-- Main entry point
-- Publicly available functions --
 
-- Main entry point: Creates the whole slot
function p.slot( f )
function p.slot( f )
-- Incoming arguments
local args = f.args or f
local args = f.args or f
if f == mw.getCurrentFrame() and args[1] == nil then
if f == mw.getCurrentFrame() and args[1] == nil then
Line 201: Line 271:
end
end
-- TODO: Add support for unexpanded frame sequences in table format
if not args.parsed then
if not args.parsed then
-- Assumed to be a string, trim it
args[1] = mw.text.trim( args[1] or '' )
args[1] = mw.text.trim( args[1] or '' )
end
end
-- Legacy mod support. Comment out instead of deleting; might be useful
-- for other wikis
-- TODO: Support multiple mod alias tables at once (like on RuMCW)
local modData = {
local modData = {
aliases = args.modaliases or '',
aliases = args.modaliases or '',
Line 218: Line 293:
end
end
-- Get the frame sequence in table format
local frames
local frames
if args.parsed then
if args.parsed then
-- Already parsed in some other module, such as Recipe table
frames = args[1]
frames = args[1]
elseif args[1] ~= '' then
elseif args[1] ~= '' then
-- Parse the frame string
-- TODO: Make the “randomise” flag not hard-coded to invslot-large CSS class
-- (ostensibly for output slots) as not all output slots are large
local randomise = args.class == 'invslot-large' and 'never' or nil
local randomise = args.class == 'invslot-large' and 'never' or nil
frames = p.parseFrameText( args[1], randomise, false, modData )
frames = p.parseFrameText( args[1], randomise, false, modData )
end
end
-- Create the slot node and add applicable styles
local body = mw.html.create( 'span' ):addClass( 'invslot' ):css{ ['vertical-align'] = args.align }
-- Is the slot animated?
local animated = frames and #frames > 1
local animated = frames and #frames > 1
local imgClass = args.imgclass
local numStyle = args.numstyle
local body = mw.html.create( 'span' ):addClass( 'invslot' ):css{ ['vertical-align'] = args.align }
if animated then
if animated then
body:addClass( 'animated' )
body:addClass( 'animated' )
end
end
if args.class then
body:addClass( args.class )
-- Default background
end
if ( args.default or '' ) ~= '' then -- default background
if args.style then
body:addClass( 'invslot-default-' .. string.lower( args.default ):gsub( ' ', '-' ) )
body:cssText( args.style )
end
if ( args.default or '' ) ~= '' then
body:css( 'background-image', f:expandTemplate{ title = i18n.templateFileUrl, args = { args.default .. '.png' } } )
end
end
-- Custom styles
body:addClass( args.class )
body:cssText( args.style )
--mw.logObject( frames )
--mw.logObject( frames )
if not frames then
if not frames or #frames == 0 then
-- Empty slot
return tostring( body )
return tostring( body )
end
end
-- We have frames, add them
local activeFrame = frames.randomise == true and random( #frames ) or 1
local activeFrame = frames.randomise == true and random( #frames ) or 1
for i, frame in ipairs( frames ) do
for i, frame in ipairs( frames ) do
local item
local item
-- Table is a list, must contain subframes
if frame[1] then
if frame[1] then
-- This is a subframe container. Each animation cycle of the slot
-- will show a subframe, one at a time.
-- Create a container node for subframes
item = body:tag( 'span' ):addClass( 'animated-subframe' )
item = body:tag( 'span' ):addClass( 'animated-subframe' )
local subActiveFrame = frame.randomise and random( #frame ) or 1
local subActiveFrame = frame.randomise == true and random( #frame ) or 1
-- Add subframes to the note
for sI, sFrame in ipairs( frame ) do
for sI, sFrame in ipairs( frame ) do
local sItem = makeItem( sFrame, sI, args )
local sItem = makeItem( sFrame, args )
item:node( sItem )
item:node( sItem )
-- Set this subframe as active
if sI == subActiveFrame then
if sI == subActiveFrame then
sItem:addClass( 'animated-active' )
sItem:addClass( 'animated-active' )
Line 263: Line 352:
end
end
else
else
item = makeItem( frame, i, args )
-- A simple frame
item = makeItem( frame, args )
body:node( item )
body:node( item )
end
end
if i == activeFrame and animated then
if i == activeFrame and animated then
-- Set this frame as active, if we have multiple of them
item:addClass( 'animated-active' )
item:addClass( 'animated-active' )
end
end
end
end
-- The slot is ready
return tostring( body )
return tostring( body )
end
end


--[[Parses the frame text into a table of frames and subframes,
-- Parses the frame text into a table of frames and subframes,
expanding aliases (and optionally retaining a reference), and
-- expanding aliases (and optionally retaining a reference), and
deciding if the slot can be randomised
-- deciding if the slot can be randomised.
--]]
-- Alias references are used in [[Module:Recipe table]] to create links and
-- lists of unique items.
function p.parseFrameText( framesText, randomise, aliasReference, modData )
function p.parseFrameText( framesText, randomise, aliasReference, modData )
-- Frame sequences
local frames = { randomise = randomise }
local frames = { randomise = randomise }
local subframes = {}
local subframes = {}
-- Is the current frame a subframe?
local subframe
local subframe
-- The list of expanded aliases, will be added to the frame sequence
-- if aliasReference is set to true AND if there are any aliases to expand.
local expandedAliases
local expandedAliases
local splitFrames = mw.text.split( mw.text.trim( framesText ), '%s*;%s*' )
for _, frameText in ipairs( splitFrames ) do
-- Split the frame string by semicolons (respecting square brackets)
local splitFrames = splitOnUnenclosedSemicolons( framesText )
-- Iterate on frame fragments
for i, frameText in ipairs( splitFrames ) do
-- Subframes are grouped by curly braces
frameText = frameText:gsub( '^%s*{%s*', function()
frameText = frameText:gsub( '^%s*{%s*', function()
subframe = true
subframe = true
return ''
return ''
end )
end )
if subframe then
if subframe then
-- Closing brace found
frameText = frameText:gsub( '%s*}%s*$', function()
frameText = frameText:gsub( '%s*}%s*$', function()
subframe = 'last'
subframe = 'last'
Line 295: Line 401:
end )
end )
end
end
-- Convert the frame text into table format, applying the default mod
-- if needed.
local frame = p.makeFrame( frameText, modData and modData.default )
local frame = p.makeFrame( frameText, modData and modData.default )
-- Alias processing
-- TODO: Rework mod support to automatically load relevant alias tables,
-- for use on other wikis that may want it. This will allow supporting
-- multiple mod alias tables at once. Comment out instead of deleting!
local newFrame = frame
local newFrame = frame
if aliases or modData.aliases then
if aliases or modData.aliases then
local id = frame.name
local id = frame.name
if frame.mod then
if frame.mod then
-- is this really needed? RuMCW doesn’t add mod prefixes in mod aliases
id = frame.mod .. ':' .. id
id = frame.mod .. ':' .. id
end
end
Line 306: Line 421:
aliases and aliases[id]
aliases and aliases[id]
if alias then
if alias then
-- Alias found, expand it
newFrame = p.getAlias( alias, frame )
newFrame = p.getAlias( alias, frame )
-- Save the alias references, if asked
if aliasReference then
if aliasReference then
-- The alias data includes the original unexpanded frame
-- and the number of frames it has expanded to.
-- The alias reference table is not sequential — indices for
-- each alias data object correspond to that alias’ first
-- (or only) expanded frame. Which is not added to the frame
-- sequence yet
local curFrame = #frames + 1
local curFrame = #frames + 1
local aliasData = { frame = frame, length = #newFrame }
local aliasData = { frame = frame, length = #newFrame }
if subframe then
if subframe then
-- Subframe containers will have their own
-- alias reference tables
if not subframes.aliasReference then
if not subframes.aliasReference then
subframes.aliasReference = {}
subframes.aliasReference = {}
Line 324: Line 450:
end
end
end
end
-- Alias processing ends here
-- Add frames and control randomization
if subframe then
if subframe then
-- Add the frame to the current subframe container
mergeList( subframes, newFrame )
mergeList( subframes, newFrame )
-- Randomise starting frame for "Any *" aliases, as long as the alias is the only subframe
-- Randomise starting frame for "Any *" aliases, as long as the
-- alias is the only subframe (and randomization is not disabled)
if frames.randomise ~= 'never' and subframes.randomise == nil and
if frames.randomise ~= 'never' and subframes.randomise == nil and
frame.name:match( '^' .. i18n.prefixes.any .. ' ' ) then
frame.name:match( '^' .. i18n.prefixes.any .. ' ' )
then
subframes.randomise = true
subframes.randomise = true
else
else
subframes.randomise = false
subframes.randomise = false
end
end
-- Disable randomization
if frames.randomise ~= 'never' then
if frames.randomise ~= 'never' then
frames.randomise = false
frames.randomise = false
end
end
if subframe == 'last' then
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
if #subframes == 1 or #splitFrames == i and #frames == 0 then
-- If the subframe container only has one expanded frame or
-- is the only frame in the whole sequence, its contents are
-- extracted into the main frame sequence
local lastFrame = #frames
mergeList( frames, subframes )
mergeList( frames, subframes )
-- Inherit the randomise flag if it’s the only frame
if #splitFrames == 1 then
frames.randomise = subframes.randomise
end
-- Append alias reference data, if present
if aliasReference and subframes.aliasReference then
if not expandedAliases then
expandedAliases = {}
end
for i, aliasRefData in pairs(subframes.aliasReference) do
expandedAliases[lastFrame + i] = aliasRefData
end
end
else
else
-- Add the subframe container to the frame sequence
table.insert( frames, subframes )
table.insert( frames, subframes )
end
end
-- Finished processing this subframe container
subframes = {}
subframes = {}
subframe = nil
subframe = nil
end
end
else
else
-- Randomise starting frame for "Any *" aliases, as long as the alias is the only frame
-- Randomize starting frame for "Any *" aliases, as long as the alias is the only frame
if frames.randomise == nil and frame.name:match( '^' .. i18n.prefixes.any .. ' ' ) then
if frames.randomise ~= 'never' and frame.name:match( '^' .. i18n.prefixes.any .. ' ' ) then
frames.randomise = true
frames.randomise = true
elseif frames.randomise ~= 'never' then
else
frames.randomise = false
frames.randomise = false
end
end
-- Add the expanded frame(s) to the frame sequence
mergeList( frames, newFrame )
mergeList( frames, newFrame )
end
end
end
end
-- Add the alias reference, if we’re compiling one
frames.aliasReference = expandedAliases
frames.aliasReference = expandedAliases
-- The frame sequence is ready
return frames
return frames
end
end


--[[Returns a new table with the parts of the parent frame
-- Applies parameters from the parent frame (such as title or text)
added to the alias
-- to the alias’ expansion
--]]
function p.getAlias( aliasFrames, parentFrame )
function p.getAlias( aliasFrames, parentFrame )
-- If alias is just a name, return the parent frame with the new name
-- If alias is just a name, return the parent frame with the new name
Line 380: Line 537:
end
end
-- Common case: group alias
local expandedFrames = {}
local expandedFrames = {}
for i, aliasFrame in ipairs( aliasFrames ) do
for i, aliasFrame in ipairs( aliasFrames ) do
local expandedFrame
local expandedFrame
if type( aliasFrame ) == 'string' then
if type( aliasFrame ) == 'string' then
-- Simple expansion frame in string format
expandedFrame = { name = aliasFrame }
expandedFrame = { name = aliasFrame }
else
else
-- Expansion frame in table format
-- As it’s loaded with mw.loadData, it must be cloned
-- before changing
expandedFrame = cloneTable( aliasFrame )
expandedFrame = cloneTable( aliasFrame )
end
end
-- Apply the parent frame’s settings
expandedFrame.title = parentFrame.title or expandedFrame.title
expandedFrame.title = parentFrame.title or expandedFrame.title
expandedFrame.mod = parentFrame.mod or expandedFrame.mod
expandedFrame.num = parentFrame.num or expandedFrame.num
expandedFrame.num = parentFrame.num or expandedFrame.num
expandedFrame.text = parentFrame.text or expandedFrame.text
expandedFrame.text = parentFrame.text or expandedFrame.text
-- Legacy mod support. Comment out instead of deleting
-- TODO: invert the priority for mod parameter, to allow
-- group mod aliases with vanilla items?
expandedFrame.mod = parentFrame.mod or expandedFrame.mod
expandedFrames[i] = expandedFrame
expandedFrames[i] = expandedFrame
Line 399: Line 567:
end
end


function p.expandAlias( parentFrame, alias )
-- Convert the frame object back into string format
return p.getAlias( alias, parentFrame )
end
 
function p.stringifyFrame( frame )
function p.stringifyFrame( frame )
if not frame.name then
if not frame.name then
Line 417: Line 582:
end
end


-- Convert the frame sequence into string format
function p.stringifyFrames( frames )
function p.stringifyFrames( frames )
for i, frame in ipairs( frames ) do
for i, frame in ipairs( frames ) do
frames[i] = p.stringifyFrame( frame )
if frame[1] then
-- Subframe container
-- As the format and the syntax are the same, process it recursively
frames[i] = '{' .. p.stringifyFrames( frame ) .. '}'
else
frames[i] = p.stringifyFrame( frame )
end
end
end
return table.concat( frames, ';' )
return table.concat( frames, ';' )
end
end


-- Splits up the frame text into its parts
-- Converts the frame text into a frame object
function p.makeFrame( frameText, mod )
-- Full syntax: [Title]Mod:Name,Number[Text]
function p.makeFrame( frameText, defaultMod )
-- Simple frame with no parts
-- Simple frame with no parts
if not frameText:match( '[%[:,]' ) then
if not frameText:match( '[%[:,]' ) then
return {
return {
mod = mod,
mod = defaultMod,
name = mw.text.trim( frameText ),
name = mw.text.trim( frameText ),
}
}
end
end
frameText = frameText:gsub( '%s*([%[%]:,;])%s*', '%1' )
-- Complex frame
local frame = {}
local frame = {}
frame.title = frameText:match( '^%[([^%]]+)%]' )
frame.mod = frameText:match( '([^:%]]+):' ) or mod
-- Title
local vanilla = { v = 1, vanilla = 1, mc = 1, minecraft = 1 }
local title, rest = frameText:match( '^%s*%[([^%]]*)%]%s*(.*)' )
if frame.mod and vanilla[mw.ustring.lower( frame.mod )] or frame.mod == '' then
if title then
frame.mod = nil
frame.title = title
frameText = rest
end
end
local nameStart = ( frameText:find( ':' ) or frameText:find( '%]' ) or 0 ) + 1
-- Additional tooltip text
if nameStart - 1 == #frameText then
local rest, text = frameText:match( '([^%]]*)%s*%[([^%]]*)%]%s*$' )
nameStart = 1
if text then
frame.text = text
frameText = rest
end
end
frame.name = frameText:sub( nameStart, ( frameText:find( '[,%[]', nameStart ) or 0 ) - 1 )
frame.num = math.floor( frameText:match( ',(%d+)' ) or 0 )
-- Legacy mod support
if frame.num == 0 then
-- Comment out instead of deleting
frame.num = nil
local mod, rest = frameText:match('^([^:]+):%s*(.*)')
if mod then
if not vanilla[mod:lower()] then
frame.mod = mod
end
frameText = rest
else
frame.mod = defaultMod
frameText = frameText:gsub('^:', '')
end
end
frame.text = frameText:match( '%[([^%]]+)%]$' )
-- Name and stack size
-- The pattern will match the last comma, so you can use names with commas
-- like so: “Potatiesh, Greatstaff of the Peasant,1”
local name, num = frameText:match('(.*),%s*(%d+)')
if num then
-- Number is set
frame.name = mw.text.trim(name)
frame.num = math.floor(num)
if frame.num < 2 then
frame.num = nil
end
else
-- No number
frame.name = mw.text.trim(frameText)
end
-- The frame object is ready
return frame
return frame
end
end
function p.getParts( frameText, mod )
 
return p.makeFrame( frameText, mod )
-- This line should be the last one:
end
return p
return p

Latest revision as of 20:17, 16 October 2025

This is the documentation page. It will be transcluded into the main module page. See Template:Documentation for more information

This module implements {{inventory slot}}.

Dependencies

de:Modul:Slot es:Módulo:Inventory slot fr:Module:Case inventaire ja:モジュール:Inventory slot ko:모듈:Inventory slot pl:Moduł:Inventory slot ru:Модуль:Инвентарный слот pt:Módulo:Inventory slot uk:Модуль:Інвентарний слот zh:模块:Inventory slot



local p = {}

-- Internationalization data
local i18n = {
	-- Name formats for pages and files
	filename = 'Invicon $1',
	legacyFilename = 'Grid $1',
	modLink = 'Mods/$1/$2',
	
	-- Dependencies
	moduleAliases = [[Module:Inventory slot/Aliases]],
	moduleRandom = [[Module:Random]],
	
	-- List of special prefixes which should be handled by
	-- other modules (such as being moved outside links)
	-- When localizing, you might want to use a separate list of patterns
	-- matching the prefixes’ grammatical forms depending on the language
	prefixes = {
		any = 'Any',
		matching = 'Matching',
		damaged = 'Damaged',
		unwaxed = 'Unwaxed',
	},
	
	-- List of suffixes that are usually stripped from links and tooltips
	suffixes = {
		rev = 'Revision %d+',
		-- berev = 'BE%d+',
		-- jerev= 'JE%d+',
		be = 'BE',
		lce = 'LCE',
		sm = 'SM',
	},
}
p.i18n = i18n

-- Global dependencies and constants
local random = require( i18n.moduleRandom ).random
local aliases = mw.loadData( i18n.moduleAliases )
local pageName = mw.title.getCurrentTitle().text
local vanilla = { v = 1, vanilla = 1, mc = 1, minecraft = 1 }

-- Auxilliary functions --

-- Splits a given text into fragments separated by semicolons that are not
-- inside square brackets. Originally written by AttemptToCallNil for the
-- Russian wiki.
-- It processes the text byte-by-byte due to being written under a much stricter
-- Lua runtime budget, with no LuaSandbox and mw.text.split being unperformant.
-- See also https://help.fandom.com/wiki/Extension:Scribunto#Known_issues_and_solutions
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, "Excessive square brackets found")
			nesting = true
		elseif byte == rbrace then
			assert(nesting, "Unbalanced square brackets found")
			nesting = false
		end
	end
	assert(not nesting, "Unbalanced square brackets found")
	frames[frameIndex] = text:sub(splitStart, text:len())
	
	for index = 1, #frames do
		frames[index] = (frames[index]:gsub("^%s+", ""):gsub("%s+$", "")) -- faster than mw.text.trim
	end
	
	return frames
end

-- Performs a simple recursive clone of a table’s values.
-- Probably exists due to mw.clone() being unusable on tables from mw.loadData()
-- at the time (see the link to help.fandom.com above)
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

-- Merges a list, or inserts a string or table into a table,
-- depending on what the second argument happens to be
local function mergeList( parentTable, content )
	local i = #parentTable + 1
	if content[1] then
		-- Merge list into table
		for _, v in ipairs( content ) do
			parentTable[i] = v
			i = i + 1
		end
	else
		-- Add strings or tables to table
		parentTable[i] = content
	end
end

-- Creates the HTML node for a given item.
-- The actual icon file is found and added here
local function makeItem( frame, args )
	local item = ( mw.html.create('span')
		:addClass('invslot-item')
		:addClass(args.imgclass)
		:cssText(args.imgstyle)
	)
	
	if (frame.name or '') == '' then
		-- Empty frame, no icon to add
		return item
	end
	
	-- Frame parameters
	local title = frame.title or mw.text.trim( args.title or '' )
	local mod = frame.mod
	local name = frame.name
	local num = frame.num
	local description = frame.text
	
	-- Split the extension out of the frame’s name
	local extension
	if name:match('%.gif') or name:match('%.png') then
		extension = name:sub(-4)
		name = name:sub(0, -5)
	elseif name:match('%.webp') then
		extension = '.webp'
		name = name:sub(0, -6)
	else
		extension = '.png'
	end
	
	-- Determine the file name
	local img
	if mod then
		-- Legacy mod support
		-- Comment out instead of deleting, as other wikis may find it useful
		img = i18n.legacyFilename:gsub( '%$1', name .. ' (' .. mod .. ')' )
	else
		-- Fall back to an individual image if the sprite is lacking
		img = i18n.filename:gsub( '%$1', name)
	end
	img = img .. extension

	-- Strip suffixes out
	for _, suffix in pairs( i18n.suffixes ) do
		name = name:gsub( ' ' .. suffix .. '$', '' )
	end
	
	-- Determine the link’s target
	local link = args.link or ''
	if link == '' then
		if mod then
			link = i18n.modLink:gsub( '%$1', mod ):gsub( '%$2', name )
		else
			-- Strip the “Damaged” prefix out
			link = name:gsub( '^' .. i18n.prefixes.damaged .. ' ', '' )
		end
	elseif link:lower() == 'none' then
		-- Disable the link
		link = nil
	end
	if link and link:gsub('^%l', string.upper) == pageName then
		link = nil
	end
	
	-- Tooltip titles. If JavaScript is not enabled, the slot will gracefully
	-- degrade to a simplified title without minetip formatting
	local formattedTitle
	local plainTitle
	if title == '' then
		-- If the title is not set, default to the slot’s name
		plainTitle = name
	elseif title:lower() ~= 'none' then
		-- Special character escapes
		plainTitle = title:gsub( '\\\\', '&#92;' ):gsub( '\\&', '&#38;' )
		
		-- The default title will have special formatting code stripped out
		local formatPatterns = {'&[0-9a-jl-qs-vyzr]', '&#%x%x%x%x%x%x', '&$%x%x%x'}
		for _, formatPattern in ipairs( formatPatterns ) do
			if plainTitle:match( formatPattern ) then
				formattedTitle = title
				plainTitle = plainTitle:gsub( formatPattern, '' )
			end
		end
		
		if plainTitle == '' then
			-- If the title field only has formatting code, the frame’s name
			-- is automatically used. For minetips it’s done by JavaScript
			-- by appending the plain title.
			plainTitle = name
		else
			-- Re-encode the 
			plainTitle = plainTitle:gsub( '&#92;', '\\' ):gsub( '&#38;', '&' )
		end
	elseif link then
		-- Disable the tooltip that will otherwise appear with a link
		formattedTitle = ''
	end
	
	-- Minetips are controlled by custom HTML attributes.
	-- See [[MediaWiki:Common.js]] for implementation in JavaScript
	item:attr{
		['data-minetip-title'] = formattedTitle,
		['data-minetip-text'] = description
	}
	
	-- & is re-escaped because mw.html treats attributes as plain text,
	-- but MediaWiki doesn’t.
	local escapedTitle = ( plainTitle or '' ):gsub( '&', '&#38;' )
	
	-- Alt text
	local altText = img .. ': Inventory sprite for ' .. name .. ' in Minecraft as shown in-game'
	if link then
		altText = altText .. ' linking to ' .. link
	end
	if formattedTitle or plainTitle or link then
		altText = altText .. ' with description: ' .. ( formattedTitle or plainTitle or link )
		if description then
			altText = altText .. ' ' .. description:gsub( '/', ' ' )
		end
		altText = altText:gsub( '&[0-9a-jl-qs-wr]', '' )
	end
	
	-- Add the image
	item:addClass( 'invslot-item-image' )
		:wikitext( '[[File:', img, '|32x32px|link=', link or '', '|alt=', altText, '|', escapedTitle, ']]' )
	
	-- Add the stack number, if present and in 2-999 range
	if num and num > 1 and num < 1000 then
		if link then
			item:wikitext( '[[', link, '|' )
		end
		local number = item
			:tag( 'span' )
				:addClass( 'invslot-stacksize' )
				:attr{ title = plainTitle }
				:wikitext( num )
		if args.numstyle then
			number:cssText( args.numstyle )
		end
		if link then
			item:wikitext( ']]' )
		end
	end
	
	-- The HTML node is now ready
	return item
end

-- Publicly available functions --

-- Main entry point: Creates the whole slot
function p.slot( f )
	-- Incoming arguments
	local args = f.args or f
	if f == mw.getCurrentFrame() and args[1] == nil then
		args = f:getParent().args
	end
	
	-- TODO: Add support for unexpanded frame sequences in table format
	if not args.parsed then
		-- Assumed to be a string, trim it
		args[1] = mw.text.trim( args[1] or '' )
	end
	
	-- Legacy mod support. Comment out instead of deleting; might be useful
	-- for other wikis
	-- TODO: Support multiple mod alias tables at once (like on RuMCW)
	local modData = {
		aliases = args.modaliases or '',
		default = args.mod
	}
	if modData.aliases ~= '' then
		modData.aliases = mw.loadData( 'Module:' .. modData.aliases )
	else
		modData.aliases = nil
	end
	if args.mod == '' then
		modData.default = nil
	end
	
	-- Get the frame sequence in table format
	local frames
	if args.parsed then
		-- Already parsed in some other module, such as Recipe table
		frames = args[1]
	elseif args[1] ~= '' then
		-- Parse the frame string
		-- TODO: Make the “randomise” flag not hard-coded to invslot-large CSS class
		-- (ostensibly for output slots) as not all output slots are large
		local randomise = args.class == 'invslot-large' and 'never' or nil
		frames = p.parseFrameText( args[1], randomise, false, modData )
	end
	
	-- Create the slot node and add applicable styles
	local body = mw.html.create( 'span' ):addClass( 'invslot' ):css{ ['vertical-align'] = args.align }
	
	-- Is the slot animated?
	local animated = frames and #frames > 1
	if animated then
		body:addClass( 'animated' )
	end
	
	-- Default background
	if ( args.default or '' ) ~= '' then -- default background
		body:addClass( 'invslot-default-' .. string.lower( args.default ):gsub( ' ', '-' ) )
	end
	
	-- Custom styles
	body:addClass( args.class )
	body:cssText( args.style )
	
	--mw.logObject( frames )
	if not frames or #frames == 0 then
		-- Empty slot
		return tostring( body )
	end
	
	-- We have frames, add them
	local activeFrame = frames.randomise == true and random( #frames ) or 1
	for i, frame in ipairs( frames ) do
		local item
		if frame[1] then
			-- This is a subframe container. Each animation cycle of the slot
			-- will show a subframe, one at a time.
			-- Create a container node for subframes
			item = body:tag( 'span' ):addClass( 'animated-subframe' )
			local subActiveFrame = frame.randomise == true and random( #frame ) or 1
			
			-- Add subframes to the note
			for sI, sFrame in ipairs( frame ) do
				local sItem = makeItem( sFrame, args )
				item:node( sItem )
				
				-- Set this subframe as active
				if sI == subActiveFrame then
					sItem:addClass( 'animated-active' )
				end
			end
		else
			-- A simple frame
			item = makeItem( frame, args )
			body:node( item )
		end
		if i == activeFrame and animated then
			-- Set this frame as active, if we have multiple of them
			item:addClass( 'animated-active' )
		end
	end
	
	-- The slot is ready
	return tostring( body )
end

-- Parses the frame text into a table of frames and subframes,
-- expanding aliases (and optionally retaining a reference), and
-- deciding if the slot can be randomised.
-- Alias references are used in [[Module:Recipe table]] to create links and
-- lists of unique items.
function p.parseFrameText( framesText, randomise, aliasReference, modData )
	-- Frame sequences
	local frames = { randomise = randomise }
	local subframes = {}
	
	-- Is the current frame a subframe?
	local subframe
	
	-- The list of expanded aliases, will be added to the frame sequence
	-- if aliasReference is set to true AND if there are any aliases to expand.
	local expandedAliases
	
	-- Split the frame string by semicolons (respecting square brackets)
	local splitFrames = splitOnUnenclosedSemicolons( framesText )
	
	-- Iterate on frame fragments
	for i, frameText in ipairs( splitFrames ) do
		-- Subframes are grouped by curly braces
		frameText = frameText:gsub( '^%s*{%s*', function()
			subframe = true
			return ''
		end )
		
		if subframe then
			-- Closing brace found
			frameText = frameText:gsub( '%s*}%s*$', function()
				subframe = 'last'
				return ''
			end )
		end
		
		-- Convert the frame text into table format, applying the default mod
		-- if needed.
		local frame = p.makeFrame( frameText, modData and modData.default )
		
		-- Alias processing
		-- TODO: Rework mod support to automatically load relevant alias tables,
		-- for use on other wikis that may want it. This will allow supporting
		-- multiple mod alias tables at once. Comment out instead of deleting!
		local newFrame = frame
		if aliases or modData.aliases then
			local id = frame.name
			if frame.mod then
				-- is this really needed? RuMCW doesn’t add mod prefixes in mod aliases
				id = frame.mod .. ':' .. id
			end
			
			local alias = modData and modData.aliases and modData.aliases[id] or
				aliases and aliases[id]
			if alias then
				-- Alias found, expand it
				newFrame = p.getAlias( alias, frame )
				
				-- Save the alias references, if asked
				if aliasReference then
					-- The alias data includes the original unexpanded frame
					-- and the number of frames it has expanded to.
					-- The alias reference table is not sequential — indices for
					-- each alias data object correspond to that alias’ first
					-- (or only) expanded frame. Which is not added to the frame
					-- sequence yet
					local curFrame = #frames + 1
					local aliasData = { frame = frame, length = #newFrame }
					if subframe then
						-- Subframe containers will have their own
						-- alias reference tables
						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
		-- Alias processing ends here
		
		-- Add frames and control randomization
		if subframe then
			-- Add the frame to the current subframe container
			mergeList( subframes, newFrame )
			
			-- Randomise starting frame for "Any *" aliases, as long as the
			-- alias is the only subframe (and randomization is not disabled)
			if frames.randomise ~= 'never' and subframes.randomise == nil and
				frame.name:match( '^' .. i18n.prefixes.any .. ' ' )
			then
				subframes.randomise = true
			else
				subframes.randomise = false
			end
			
			-- Disable randomization
			if frames.randomise ~= 'never' then
				frames.randomise = false
			end
			if subframe == 'last' then
				if #subframes == 1 or #splitFrames == i and #frames == 0 then
					-- If the subframe container only has one expanded frame or
					-- is the only frame in the whole sequence, its contents are
					-- extracted into the main frame sequence
					local lastFrame = #frames
					mergeList( frames, subframes )
					
					-- Inherit the randomise flag if it’s the only frame
					if #splitFrames == 1 then
						frames.randomise = subframes.randomise
					end
					
					-- Append alias reference data, if present
					if aliasReference and subframes.aliasReference then
						if not expandedAliases then
							expandedAliases = {}
						end
						for i, aliasRefData in pairs(subframes.aliasReference) do
							expandedAliases[lastFrame + i] = aliasRefData
						end
					end
				else
					-- Add the subframe container to the frame sequence
					table.insert( frames, subframes )
				end
				
				-- Finished processing this subframe container
				subframes = {}
				subframe = nil
			end
		else
			-- Randomize starting frame for "Any *" aliases, as long as the alias is the only frame
			if frames.randomise ~= 'never' and frame.name:match( '^' .. i18n.prefixes.any .. ' ' ) then
				frames.randomise = true
			else
				frames.randomise = false
			end
			
			-- Add the expanded frame(s) to the frame sequence
			mergeList( frames, newFrame )
		end
	end
	
	-- Add the alias reference, if we’re compiling one
	frames.aliasReference = expandedAliases
	
	-- The frame sequence is ready
	return frames
end

-- Applies parameters from the parent frame (such as title or text)
-- to the alias’ expansion
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
	
	-- Common case: group alias
	local expandedFrames = {}
	for i, aliasFrame in ipairs( aliasFrames ) do
		local expandedFrame
		if type( aliasFrame ) == 'string' then
			-- Simple expansion frame in string format
			expandedFrame = { name = aliasFrame }
		else
			-- Expansion frame in table format
			-- As it’s loaded with mw.loadData, it must be cloned
			-- before changing
			expandedFrame = cloneTable( aliasFrame )
		end
		
		-- Apply the parent frame’s settings
		expandedFrame.title = parentFrame.title or expandedFrame.title
		expandedFrame.num = parentFrame.num or expandedFrame.num
		expandedFrame.text = parentFrame.text or expandedFrame.text
		
		-- Legacy mod support. Comment out instead of deleting
		-- TODO: invert the priority for mod parameter, to allow
		-- group mod aliases with vanilla items?
		expandedFrame.mod = parentFrame.mod or expandedFrame.mod
		
		expandedFrames[i] = expandedFrame
	end
	
	return expandedFrames
end

-- Convert the frame object back into string format
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

-- Convert the frame sequence into string format
function p.stringifyFrames( frames )
	for i, frame in ipairs( frames ) do
		if frame[1] then
			-- Subframe container
			-- As the format and the syntax are the same, process it recursively
			frames[i] = '{' .. p.stringifyFrames( frame ) .. '}'
		else
			frames[i] = p.stringifyFrame( frame )
		end
	end
	return table.concat( frames, ';' )
end

-- Converts the frame text into a frame object
-- Full syntax: [Title]Mod:Name,Number[Text]
function p.makeFrame( frameText, defaultMod )
	-- Simple frame with no parts
	if not frameText:match( '[%[:,]' ) then
		return {
			mod = defaultMod,
			name = mw.text.trim( frameText ),
		}
	end
	
	-- Complex frame
	local frame = {}
	
	-- Title
	local title, rest = frameText:match( '^%s*%[([^%]]*)%]%s*(.*)' )
	if title then
		frame.title = title
		frameText = rest
	end
	
	-- Additional tooltip text
	local rest, text = frameText:match( '([^%]]*)%s*%[([^%]]*)%]%s*$' )
	if text then
		frame.text = text
		frameText = rest
	end
	
	-- Legacy mod support
	-- Comment out instead of deleting
	local mod, rest = frameText:match('^([^:]+):%s*(.*)')
	if mod then
		if not vanilla[mod:lower()] then
			frame.mod = mod
		end
		frameText = rest
	else
		frame.mod = defaultMod
		frameText = frameText:gsub('^:', '')
	end
	
	-- Name and stack size
	-- The pattern will match the last comma, so you can use names with commas
	-- like so: “Potatiesh, Greatstaff of the Peasant,1”
	local name, num = frameText:match('(.*),%s*(%d+)')
	if num then
		-- Number is set
		frame.name = mw.text.trim(name)
		frame.num = math.floor(num)
		if frame.num < 2 then
			frame.num = nil
		end
	else
		-- No number
		frame.name = mw.text.trim(frameText)
	end
	
	-- The frame object is ready
	return frame
end

-- This line should be the last one:
return p