Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Inventory slot: Difference between revisions

From Vault Hunters Official Wiki
m Protected "Module:Inventory slot": Module page ([Edit=Allow only administrators] (indefinite) [Move=Allow only administrators] (indefinite))
mNo edit summary
Β 
(5 intermediate revisions by 2 users not shown)
Line 1: Line 1:
local p = {}
local p = {}
-- Cache for this page load
local frameCache = {}
local aliasCache = {}
local fileExistsCache = {}


-- Internationalization data
-- Internationalization data
local i18n = {
local i18n = {
-- Name formats for pages and files
filename = 'Invicon $1',
filename = 'Invicon $1',
legacyFilename = 'Grid $1',
legacyFilename = 'Grid $1',
Line 10: Line 14:
-- Dependencies
-- Dependencies
moduleAliases = [[Module:Inventory slot/Aliases]],
moduleAliases = [[Module:Inventory slot/Aliases]],
moduleModAliases = [[Module:Inventory slot/VHAliases]], -- New mod aliases module
moduleRandom = [[Module:Random]],
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 = {
prefixes = {
any = 'Any',
any = 'Any',
Line 23: Line 24:
},
},
-- List of suffixes that are usually stripped from links and tooltips
suffixes = {
suffixes = {
rev = 'Revision %d+',
rev = 'Revision %d+',
-- berev = 'BE%d+',
-- jerev= 'JE%d+',
be = 'BE',
be = 'BE',
lce = 'LCE',
lce = 'LCE',
Line 35: Line 33:
p.i18n = i18n
p.i18n = i18n


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


-- Auxilliary functions --
-- Initialize dependencies only when needed
local function initDependencies()
if not random then
random = require(i18n.moduleRandom).random
end
if not aliases then
aliases = mw.loadData(i18n.moduleAliases)
end
end


-- Splits a given text into fragments separated by semicolons that are not
-- Load mod aliases only when needed
-- inside square brackets. Originally written by AttemptToCallNil for the
local function getVHAliases()
-- Russian wiki.
if not modAliases then
-- It processes the text byte-by-byte due to being written under a much stricter
local success, result = pcall(mw.loadData, i18n.moduleVHAliases)
-- Lua runtime budget, with no LuaSandbox and mw.text.split being unperformant.
if success then
-- See also https://help.fandom.com/wiki/Extension:Scribunto#Known_issues_and_solutions
VHAliases = result
else
VHAliases = {} -- Empty table if module doesn't exist
end
end
return VHAliases
end
Β 
-- Optimized file existence check with caching
local function fileExists(filename)
if fileExistsCache[filename] ~= nil then
return fileExistsCache[filename]
end
local title = mw.title.new(filename, 'File')
local exists = title and title.fileExists
fileExistsCache[filename] = exists
return exists
end
Β 
-- Fast semicolon splitting (optimized version)
local function splitOnUnenclosedSemicolons(text)
local function splitOnUnenclosedSemicolons(text)
local semicolon, lbrace, rbrace = (";[]"):byte(1, 3)
local semicolon, lbrace, rbrace = 59, 91, 93 -- ASCII values for ;[]
local nesting = false
local bracketDepth = 0
local splitStart = 1
local splitStart = 1
local frames = {}
local frameIndex = 1
local frameIndex = 1
local frames = {}
for index = 1, text:len() do
for index = 1, #text do
local byte = text:byte(index)
local byte = text:byte(index)
if byte == semicolon and not nesting then
if byte == semicolon and bracketDepth == 0 then
frames[frameIndex] = text:sub(splitStart, index - 1)
frames[frameIndex] = text:sub(splitStart, index - 1):match("^%s*(.-)%s*$")
frameIndex = frameIndex + 1
frameIndex = frameIndex + 1
splitStart = index + 1
splitStart = index + 1
elseif byte == lbrace then
elseif byte == lbrace then
assert(not nesting, "Excessive square brackets found")
bracketDepth = bracketDepth + 1
nesting = true
elseif byte == rbrace then
elseif byte == rbrace then
assert(nesting, "Unbalanced square brackets found")
bracketDepth = bracketDepth - 1
nesting = false
end
end
end
end
assert(not nesting, "Unbalanced square brackets found")
frames[frameIndex] = text:sub(splitStart):match("^%s*(.-)%s*$")
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
return frames
end
end


-- Performs a simple recursive clone of a table’s values.
-- Optimized table merging
-- Probably exists due to mw.clone() being unusable on tables from mw.loadData()
local function mergeList(parentTable, content)
-- at the time (see the link to help.fandom.com above)
local parentLen = #parentTable
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
if content[1] then
-- Merge list into table
-- Merge list into table
for _, v in ipairs( content ) do
for i, v in ipairs(content) do
parentTable[i] = v
parentTable[parentLen + i] = v
i = i + 1
end
end
else
else
-- Add strings or tables to table
-- Add single item
parentTable[i] = content
parentTable[parentLen + 1] = content
end
end
end
end


-- Creates the HTML node for a given item.
-- Optimized item creation with reduced expensive calls
-- The actual icon file is found and added here
local function makeItem(frame, args)
local function makeItem( frame, args )
local item = mw.html.create('span')
local item = ( mw.html.create('span')
:addClass('invslot-item')
:addClass('invslot-item')
:addClass(args.imgclass)
:addClass(args.imgclass)
:cssText(args.imgstyle)
:cssText(args.imgstyle)
)
if (frame.name or '') == '' then
if not frame.name or frame.name == '' then
-- Empty frame, no icon to add
return item
return item
end
end
-- Frame parameters
-- Frame parameters
local title = frame.title or mw.text.trim( args.title or '' )
local title = frame.title or args.title or ''
title = title:match("^%s*(.-)%s*$") -- trim
local mod = frame.mod
local mod = frame.mod
local name = frame.name
local name = frame.name
Line 131: Line 130:
local description = frame.text
local description = frame.text
-- Split the extension out of the frame’s name
-- Optimized file extension detection
local extension
local img, 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
-- Legacy mod support
img = i18n.legacyFilename:gsub('%$1', name .. ' (' .. mod .. ')')
-- Comment out instead of deleting, as other wikis may find it useful
extension = '.png' -- Default for legacy
img = i18n.legacyFilename:gsub( '%$1', name .. ' (' .. mod .. ')' )
else
else
-- Fall back to an individual image if the sprite is lacking
local baseName = name
img = i18n.filename:gsub( '%$1', name)
local baseImg = i18n.filename:gsub('%$1', baseName)
-- Check extensions in order of preference, with caching
local extensions = {'.png', '.gif', '.webp'}
extension = '.png' -- default
for _, ext in ipairs(extensions) do
if fileExists(baseImg .. ext) then
extension = ext
break
end
end
img = baseImg
end
end
img = img .. extension
img = img .. extension
Β 
-- Strip suffixes out
-- Strip suffixes (optimized)
for _, suffix in pairs( i18n.suffixes ) do
local cleanName = name
name = name:gsub( ' ' .. suffix .. '$', '' )
for _, suffix in pairs(i18n.suffixes) do
cleanName = cleanName:gsub(' ' .. suffix .. '$', '')
end
end
-- Determine the link’s target
-- Determine link target
local link = args.link or ''
local link = args.link or ''
if link == '' then
if link == '' then
if mod then
if mod then
link = i18n.modLink:gsub( '%$1', mod ):gsub( '%$2', name )
link = i18n.modLink:gsub('%$1', mod):gsub('%$2', cleanName)
else
else
-- Strip the β€œDamaged” prefix out
link = cleanName:gsub('^' .. i18n.prefixes.damaged .. ' ', '')
link = name:gsub( '^' .. i18n.prefixes.damaged .. ' ', '' )
end
end
elseif link:lower() == 'none' then
elseif link:lower() == 'none' then
-- Disable the link
link = nil
link = nil
end
end
if link and link:gsub('^%l', string.upper) == pageName then
link = nil
-- Avoid self-links
if link then
local pageName = mw.title.getCurrentTitle().text
if link:gsub('^%l', string.upper) == pageName then
link = nil
end
end
end
-- Tooltip titles. If JavaScript is not enabled, the slot will gracefully
-- Process title and tooltips
-- degrade to a simplified title without minetip formatting
local formattedTitle, plainTitle
local formattedTitle
local plainTitle
if title == '' then
if title == '' then
-- If the title is not set, default to the slot’s name
plainTitle = cleanName
plainTitle = name
elseif title:lower() ~= 'none' then
elseif title:lower() ~= 'none' then
-- Special character escapes
plainTitle = title:gsub('\\\\', '\'):gsub('\\&', '&')
plainTitle = title:gsub( '\\\\', '\' ):gsub( '\\&', '&' )
-- The default title will have special formatting code stripped out
-- Check for formatting codes
local formatPatterns = {'&[0-9a-jl-qs-vyzr]', '&#%x%x%x%x%x%x', '&$%x%x%x'}
if plainTitle:find('&[0-9a-jl-qs-vyzr]') or plainTitle:find('&#%x%x%x%x%x%x') or plainTitle:find('&$%x%x%x') then
for _, formatPattern in ipairs( formatPatterns ) do
formattedTitle = title
if plainTitle:match( formatPattern ) then
plainTitle = plainTitle:gsub('&[0-9a-jl-qs-vyzr]', ''):gsub('&#%x%x%x%x%x%x', ''):gsub('&$%x%x%x', '')
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
plainTitle = cleanName
-- is automatically used. For minetips it’s done by JavaScript
-- by appending the plain title.
plainTitle = name
else
else
-- Re-encode the
plainTitle = plainTitle:gsub('\', '\\'):gsub('&', '&')
plainTitle = plainTitle:gsub( '\', '\\' ):gsub( '&', '&' )
end
end
elseif link then
elseif link then
-- Disable the tooltip that will otherwise appear with a link
formattedTitle = ''
formattedTitle = ''
end
end
-- Minetips are controlled by custom HTML attributes.
-- Set minetip attributes
-- See [[MediaWiki:Common.js]] for implementation in JavaScript
if formattedTitle or description then
item:attr{
item:attr('data-minetip-title', formattedTitle)
['data-minetip-title'] = formattedTitle,
item:attr('data-minetip-text', description)
['data-minetip-text'] = description
end
}
-- & is re-escaped because mw.html treats attributes as plain text,
-- Escape title for HTML
-- but MediaWiki doesn’t.
local escapedTitle = (plainTitle or ''):gsub('&', '&')
local escapedTitle = ( plainTitle or '' ):gsub( '&', '&' )
-- Alt text
-- Alt text
local altText = img .. ': Inventory sprite for ' .. name .. ' in Minecraft as shown in-game'
local altText = 'Inventory sprite for ' .. cleanName
if link then
if link then
altText = altText .. ' linking to ' .. link
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
end
-- Add the image
-- Add the image
item:addClass( 'invslot-item-image' )
item:addClass('invslot-item-image')
:wikitext( '[[File:', img, '|32x32px|link=', link or '', '|alt=', altText, '|', escapedTitle, ']]' )
:wikitext('[[File:', img, '|link=', link or '', '|alt=', altText, '|', escapedTitle, ']]')
-- Add the stack number, if present and in 2-999 range
-- Add stack number
if num and num > 1 and num < 1000 then
if num and num > 1 and num < 1000 then
if link then
local numberSpan = item:tag('span')
item:wikitext( '[[', link, '|' )
:addClass('invslot-stacksize')
end
:attr('title', plainTitle)
local number = item
:wikitext(num)
:tag( 'span' )
:addClass( 'invslot-stacksize' )
:attr{ title = plainTitle }
:wikitext( num )
if args.numstyle then
if args.numstyle then
number:cssText( args.numstyle )
numberSpan:cssText(args.numstyle)
end
end
if link then
if link then
item:wikitext( ']]' )
-- Wrap the number in a link
item:wikitext('[[', link, '|'):node(numberSpan):wikitext(']]')
end
end
end
end
-- The HTML node is now ready
return item
return item
end
end


-- Publicly available functions --
-- Optimized alias lookup with caching
local function getAliasFromCache(id, isModItem)
local cacheKey = (isModItem and 'mod:' or 'vanilla:') .. id
if aliasCache[cacheKey] then
return aliasCache[cacheKey]
end
initDependencies()
local alias
if isModItem then
alias = getVHAliases()[id]
else
alias = aliases[id]
end
aliasCache[cacheKey] = alias or false -- Cache negative results too
return alias
end


-- Main entry point: Creates the whole slot
-- Main slot function with caching
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 271: Line 266:
end
end
-- TODO: Add support for unexpanded frame sequences in table format
-- Prepare args and create cache key
local frameText = ''
if not args.parsed then
if not args.parsed then
-- Assumed to be a string, trim it
frameText = (args[1] or ''):match("^%s*(.-)%s*$") -- trim
args[1] = mw.text.trim( args[1] or '' )
args[1] = frameText
else
-- For parsed args, create a simple cache key from table contents
if args[1] and type(args[1]) == 'table' then
frameText = 'parsed_' .. tostring(#args[1])
end
end
end
-- Legacy mod support. Comment out instead of deleting; might be useful
local cacheKey = frameText .. '|' .. (args.class or '') .. '|' .. (args.style or '') .. '|' .. (args.mod or '')
-- for other wikis
if frameCache[cacheKey] then
-- TODO: Support multiple mod alias tables at once (like on RuMCW)
return frameCache[cacheKey]
end
-- Legacy mod support
local modData = {
local modData = {
aliases = args.modaliases or '',
default = args.mod ~= '' and args.mod or nil
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
-- Parse frames
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] and 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
-- Create slot
local body = mw.html.create( 'span' ):addClass( 'invslot' ):css{ ['vertical-align'] = args.align }
local body = mw.html.create('span')
:addClass('invslot')
:css('vertical-align', args.align)
-- Is the slot animated?
-- Handle animation
local animated = frames and #frames > 1
if frames and #frames > 1 then
if animated then
body:addClass('animated')
body:addClass( 'animated' )
end
end
-- Default background
-- Default background
if ( args.default or '' ) ~= '' then -- default background
if args.default and args.default ~= '' then
body:addClass( 'invslot-default-' .. string.lower( args.default ):gsub( ' ', '-' ) )
body:addClass('invslot-default-' .. args.default:lower():gsub(' ', '-'))
end
end
-- Custom styles
-- Custom styles
body:addClass( args.class )
if args.class then body:addClass(args.class) end
body:cssText( args.style )
if args.style then body:cssText(args.style) end
--mw.logObject( frames )
if not frames or #frames == 0 then
if not frames or #frames == 0 then
-- Empty slot
local result = tostring(body)
return tostring( body )
frameCache[cacheKey] = result
return result
end
-- Add frames
local activeFrame = 1
if frames.randomise == true then
if not random then
initDependencies()
end
activeFrame = random(#frames)
end
end
-- We have frames, add them
for i, frame in ipairs(frames) do
local activeFrame = frames.randomise == true and random( #frames ) or 1
for i, frame in ipairs( frames ) do
local item
local item
if frame[1] then
if frame[1] then
-- This is a subframe container. Each animation cycle of the slot
-- Subframe container
-- will show a subframe, one at a time.
item = body:tag('span'):addClass('animated-subframe')
-- Create a container node for subframes
local subActiveFrame = frame.randomise == true and random(#frame) or 1
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
for sI, sFrame in ipairs( frame ) do
local sItem = makeItem(sFrame, args)
local sItem = makeItem( sFrame, args )
item:node( sItem )
-- Set this subframe as active
if sI == subActiveFrame then
if sI == subActiveFrame then
sItem:addClass( 'animated-active' )
sItem:addClass('animated-active')
end
end
item:node(sItem)
end
end
else
else
-- A simple frame
-- Simple frame
item = makeItem( frame, args )
item = makeItem(frame, args)
body:node( item )
end
end
if i == activeFrame and animated then
-- Set this frame as active, if we have multiple of them
if i == activeFrame and #frames > 1 then
item:addClass( 'animated-active' )
item:addClass('animated-active')
end
end
body:node(item)
end
end
-- The slot is ready
local result = tostring(body)
return tostring( body )
frameCache[cacheKey] = result
return result
end
end


-- Parses the frame text into a table of frames and subframes,
-- Optimized frame parsing
-- expanding aliases (and optionally retaining a reference), and
function p.parseFrameText(framesText, randomise, aliasReference, modData)
-- 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 frames = { randomise = randomise }
local subframes = {}
local subframes = {}
local subframe = false
local expandedAliases = aliasReference and {} or nil
-- Is the current frame a subframe?
local splitFrames = splitOnUnenclosedSemicolons(framesText)
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
for i, frameText in ipairs( splitFrames ) do
-- Handle subframes
-- Subframes are grouped by curly braces
if frameText:find('^%s*{') then
frameText = frameText:gsub( '^%s*{%s*', function()
frameText = frameText:gsub('^%s*{%s*', '')
subframe = true
subframe = true
return ''
end
end )
if subframe then
if subframe and frameText:find('}%s*$') then
-- Closing brace found
frameText = frameText:gsub('%s*}%s*$', '')
frameText = frameText:gsub( '%s*}%s*$', function()
subframe = 'last'
subframe = 'last'
return ''
end )
end
end
-- Convert the frame text into table format, applying the default mod
-- Create frame
-- if needed.
local frame = p.makeFrame(frameText, modData and modData.default)
local frame = p.makeFrame( frameText, modData and modData.default )
-- Alias processing
-- Alias processing (optimized)
-- TODO: Rework mod support to automatically load relevant alias tables,
local newFrame = { frame }
-- for use on other wikis that may want it. This will allow supporting
if frame.name and frame.name ~= '' then
-- multiple mod alias tables at once. Comment out instead of deleting!
local isModItem = frame.mod and frame.mod ~= '' and not vanilla[frame.mod:lower()]
local newFrame = frame
local alias = getAliasFromCache(frame.name, isModItem)
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
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 442: Line 404:
subframes.aliasReference[#subframes + 1] = aliasData
subframes.aliasReference[#subframes + 1] = aliasData
else
else
if not expandedAliases then
expandedAliases = {}
end
expandedAliases[curFrame] = aliasData
expandedAliases[curFrame] = aliasData
end
end
Line 450: Line 409:
end
end
end
end
-- Alias processing ends here
-- Add frames and control randomization
-- Add frames
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
-- Handle randomization
-- 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 .. ' ' )
frame.name and frame.name:find('^' .. i18n.prefixes.any .. ' ') then
then
subframes.randomise = true
subframes.randomise = true
else
else
Line 467: Line 422:
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
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
local lastFrame = #frames
mergeList( frames, subframes )
mergeList(frames, subframes)
-- Inherit the randomise flag if it’s the only frame
if #splitFrames == 1 then
if #splitFrames == 1 then
frames.randomise = subframes.randomise
frames.randomise = subframes.randomise
end
end
-- Append alias reference data, if present
if aliasReference and subframes.aliasReference then
if aliasReference and subframes.aliasReference then
if not expandedAliases then
for j, aliasRefData in pairs(subframes.aliasReference) do
expandedAliases = {}
expandedAliases[lastFrame + j] = aliasRefData
end
for i, aliasRefData in pairs(subframes.aliasReference) do
expandedAliases[lastFrame + i] = aliasRefData
end
end
end
end
else
else
-- Add the subframe container to the frame sequence
frames[#frames + 1] = subframes
table.insert( frames, subframes )
end
end
-- Finished processing this subframe container
subframes = {}
subframes = {}
subframe = nil
subframe = false
end
end
else
else
-- Randomize starting frame for "Any *" aliases, as long as the alias is the only frame
if frames.randomise ~= 'never' and frame.name and frame.name:find('^' .. i18n.prefixes.any .. ' ') then
if frames.randomise ~= 'never' and frame.name:match( '^' .. i18n.prefixes.any .. ' ' ) then
frames.randomise = true
frames.randomise = true
else
else
Line 510: Line 454:
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


-- Applies parameters from the parent frame (such as title or text)
-- Optimized alias expansion
-- to the alias’ expansion
function p.getAlias(aliasFrames, parentFrame)
function p.getAlias( aliasFrames, parentFrame )
if type(aliasFrames) == 'string' then
-- If alias is just a name, return the parent frame with the new name
local expandedFrame = {
if type( aliasFrames ) == 'string' then
name = aliasFrames,
local expandedFrame = mw.clone( parentFrame )
title = parentFrame.title,
expandedFrame.name = aliasFrames
num = parentFrame.num,
text = parentFrame.text,
mod = parentFrame.mod
}
return { expandedFrame }
return { expandedFrame }
end
end
-- Single frame alias, put in list
if aliasFrames.name then
if aliasFrames.name then
aliasFrames = { aliasFrames }
aliasFrames = { aliasFrames }
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
-- Shallow clone for performance
-- As it’s loaded with mw.loadData, it must be cloned
expandedFrame = {}
-- before changing
for k, v in pairs(aliasFrame) do
expandedFrame = cloneTable( aliasFrame )
expandedFrame[k] = v
end
end
end
-- Apply the parent frame’s settings
-- Apply parent settings (only if not already set)
expandedFrame.title = parentFrame.title or expandedFrame.title
expandedFrame.title = parentFrame.title or expandedFrame.title
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
expandedFrame.mod = parentFrame.mod or expandedFrame.mod
Line 567: Line 504:
end
end


-- Convert the frame object back into string format
-- Optimized frame creation
function p.stringifyFrame( frame )
function p.makeFrame(frameText, defaultMod)
if not frame.name then
-- Fast path for simple frames
return ''
if not frameText:find('[%[:,]') then
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 {
return {
mod = defaultMod,
mod = defaultMod,
name = mw.text.trim( frameText ),
name = frameText:match("^%s*(.-)%s*$"),
}
}
end
end
-- Complex frame
-- Complex frame parsing
local frame = {}
local frame = { mod = defaultMod }
-- Title
-- Title
local title, rest = frameText:match( '^%s*%[([^%]]*)%]%s*(.*)' )
local title, rest = frameText:match('^%s*%[([^%]]*)%]%s*(.*)')
if title then
if title then
frame.title = title
frame.title = title
Line 617: Line 524:
end
end
-- Additional tooltip text
-- Text
local rest, text = frameText:match( '([^%]]*)%s*%[([^%]]*)%]%s*$' )
rest, frame.text = frameText:match('([^%]]*)%s*%[([^%]]*)%]%s*$')
if text then
if frame.text then
frame.text = text
frameText = rest
frameText = rest
end
end
-- Legacy mod support
-- Mod
-- Comment out instead of deleting
local mod, rest = frameText:match('^([^:]+):%s*(.*)')
local mod, rest = frameText:match('^([^:]+):%s*(.*)')
if mod then
if mod and not vanilla[mod:lower()] then
if not vanilla[mod:lower()] then
frame.mod = mod
frame.mod = mod
frameText = rest
end
elseif mod then
frameText = rest
frameText = rest
else
else
frame.mod = defaultMod
frameText = frameText:gsub('^:', '')
frameText = frameText:gsub('^:', '')
end
end
-- Name and stack size
-- Name and number
-- 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+)')
local name, num = frameText:match('(.*),%s*(%d+)')
if num then
if num then
-- Number is set
frame.name = name:match("^%s*(.-)%s*$")
frame.name = mw.text.trim(name)
frame.num = math.floor(tonumber(num))
frame.num = math.floor(num)
if frame.num < 2 then
if frame.num < 2 then
frame.num = nil
frame.num = nil
end
end
else
else
-- No number
frame.name = frameText:match("^%s*(.-)%s*$")
frame.name = mw.text.trim(frameText)
end
end
-- The frame object is ready
return frame
return frame
end
end


-- This line should be the last one:
-- Utility functions (unchanged)
function p.stringifyFrame(frame)
if not frame.name then
return ''
end
return string.format(
'[%s]%s:%s,%s[%s]',
frame.title or '',
frame.mod or 'Minecraft',
frame.name,
frame.num or '',
frame.text or ''
)
end
Β 
function p.stringifyFrames(frames)
local result = {}
for i, frame in ipairs(frames) do
if frame[1] then
result[i] = '{' .. p.stringifyFrames(frame) .. '}'
else
result[i] = p.stringifyFrame(frame)
end
end
return table.concat(result, ';')
end
Β 
return p
return p

Latest revision as of 16:15, 13 August 2025

This module implements {{Inventory slot}}.

Dependencies


local p = {}

-- Cache for this page load
local frameCache = {}
local aliasCache = {}
local fileExistsCache = {}

-- Internationalization data
local i18n = {
	filename = 'Invicon $1',
	legacyFilename = 'Grid $1',
	modLink = 'Mods/$1/$2',
	
	-- Dependencies
	moduleAliases = [[Module:Inventory slot/Aliases]],
	moduleModAliases = [[Module:Inventory slot/VHAliases]], -- New mod aliases module
	moduleRandom = [[Module:Random]],
	
	prefixes = {
		any = 'Any',
		matching = 'Matching',
		damaged = 'Damaged',
		unwaxed = 'Unwaxed',
	},
	
	suffixes = {
		rev = 'Revision %d+',
		be = 'BE',
		lce = 'LCE',
		sm = 'SM',
	},
}
p.i18n = i18n

-- Lazy-loaded dependencies
local random, aliases, VHAliases
local vanilla = { v = 1, vanilla = 1, mc = 1, minecraft = 1 }

-- Initialize dependencies only when needed
local function initDependencies()
	if not random then
		random = require(i18n.moduleRandom).random
	end
	if not aliases then
		aliases = mw.loadData(i18n.moduleAliases)
	end
end

-- Load mod aliases only when needed
local function getVHAliases()
	if not modAliases then
		local success, result = pcall(mw.loadData, i18n.moduleVHAliases)
		if success then
			VHAliases = result
		else
			VHAliases = {} -- Empty table if module doesn't exist
		end
	end
	return VHAliases
end

-- Optimized file existence check with caching
local function fileExists(filename)
	if fileExistsCache[filename] ~= nil then
		return fileExistsCache[filename]
	end
	
	local title = mw.title.new(filename, 'File')
	local exists = title and title.fileExists
	fileExistsCache[filename] = exists
	return exists
end

-- Fast semicolon splitting (optimized version)
local function splitOnUnenclosedSemicolons(text)
	local semicolon, lbrace, rbrace = 59, 91, 93 -- ASCII values for ;[]
	local bracketDepth = 0
	local splitStart = 1
	local frames = {}
	local frameIndex = 1
	
	for index = 1, #text do
		local byte = text:byte(index)
		if byte == semicolon and bracketDepth == 0 then
			frames[frameIndex] = text:sub(splitStart, index - 1):match("^%s*(.-)%s*$")
			frameIndex = frameIndex + 1
			splitStart = index + 1
		elseif byte == lbrace then
			bracketDepth = bracketDepth + 1
		elseif byte == rbrace then
			bracketDepth = bracketDepth - 1
		end
	end
	frames[frameIndex] = text:sub(splitStart):match("^%s*(.-)%s*$")
	
	return frames
end

-- Optimized table merging
local function mergeList(parentTable, content)
	local parentLen = #parentTable
	if content[1] then
		-- Merge list into table
		for i, v in ipairs(content) do
			parentTable[parentLen + i] = v
		end
	else
		-- Add single item
		parentTable[parentLen + 1] = content
	end
end

-- Optimized item creation with reduced expensive calls
local function makeItem(frame, args)
	local item = mw.html.create('span')
		:addClass('invslot-item')
		:addClass(args.imgclass)
		:cssText(args.imgstyle)
	
	if not frame.name or frame.name == '' then
		return item
	end
	
	-- Frame parameters
	local title = frame.title or args.title or ''
	title = title:match("^%s*(.-)%s*$") -- trim
	local mod = frame.mod
	local name = frame.name
	local num = frame.num
	local description = frame.text
	
	-- Optimized file extension detection
	local img, extension
	if mod then
		img = i18n.legacyFilename:gsub('%$1', name .. ' (' .. mod .. ')')
		extension = '.png' -- Default for legacy
	else
		local baseName = name
		local baseImg = i18n.filename:gsub('%$1', baseName)
		
		-- Check extensions in order of preference, with caching
		local extensions = {'.png', '.gif', '.webp'}
		extension = '.png' -- default
		for _, ext in ipairs(extensions) do
			if fileExists(baseImg .. ext) then
				extension = ext
				break
			end
		end
		img = baseImg
	end
	img = img .. extension
	
	-- Strip suffixes (optimized)
	local cleanName = name
	for _, suffix in pairs(i18n.suffixes) do
		cleanName = cleanName:gsub(' ' .. suffix .. '$', '')
	end
	
	-- Determine link target
	local link = args.link or ''
	if link == '' then
		if mod then
			link = i18n.modLink:gsub('%$1', mod):gsub('%$2', cleanName)
		else
			link = cleanName:gsub('^' .. i18n.prefixes.damaged .. ' ', '')
		end
	elseif link:lower() == 'none' then
		link = nil
	end
	
	-- Avoid self-links
	if link then
		local pageName = mw.title.getCurrentTitle().text
		if link:gsub('^%l', string.upper) == pageName then
			link = nil
		end
	end
	
	-- Process title and tooltips
	local formattedTitle, plainTitle
	if title == '' then
		plainTitle = cleanName
	elseif title:lower() ~= 'none' then
		plainTitle = title:gsub('\\\\', '&#92;'):gsub('\\&', '&#38;')
		
		-- Check for formatting codes
		if plainTitle:find('&[0-9a-jl-qs-vyzr]') or plainTitle:find('&#%x%x%x%x%x%x') or plainTitle:find('&$%x%x%x') then
			formattedTitle = title
			plainTitle = plainTitle:gsub('&[0-9a-jl-qs-vyzr]', ''):gsub('&#%x%x%x%x%x%x', ''):gsub('&$%x%x%x', '')
		end
		
		if plainTitle == '' then
			plainTitle = cleanName
		else
			plainTitle = plainTitle:gsub('&#92;', '\\'):gsub('&#38;', '&')
		end
	elseif link then
		formattedTitle = ''
	end
	
	-- Set minetip attributes
	if formattedTitle or description then
		item:attr('data-minetip-title', formattedTitle)
		item:attr('data-minetip-text', description)
	end
	
	-- Escape title for HTML
	local escapedTitle = (plainTitle or ''):gsub('&', '&#38;')
	
	-- Alt text
	local altText = 'Inventory sprite for ' .. cleanName
	if link then
		altText = altText .. ' linking to ' .. link
	end
	
	-- Add the image
	item:addClass('invslot-item-image')
		:wikitext('[[File:', img, '|link=', link or '', '|alt=', altText, '|', escapedTitle, ']]')
	
	-- Add stack number
	if num and num > 1 and num < 1000 then
		local numberSpan = item:tag('span')
			:addClass('invslot-stacksize')
			:attr('title', plainTitle)
			:wikitext(num)
		
		if args.numstyle then
			numberSpan:cssText(args.numstyle)
		end
		
		if link then
			-- Wrap the number in a link
			item:wikitext('[[', link, '|'):node(numberSpan):wikitext(']]')
		end
	end
	
	return item
end

-- Optimized alias lookup with caching
local function getAliasFromCache(id, isModItem)
	local cacheKey = (isModItem and 'mod:' or 'vanilla:') .. id
	if aliasCache[cacheKey] then
		return aliasCache[cacheKey]
	end
	
	initDependencies()
	local alias
	
	if isModItem then
		alias = getVHAliases()[id]
	else
		alias = aliases[id]
	end
	
	aliasCache[cacheKey] = alias or false -- Cache negative results too
	return alias
end

-- Main slot function with caching
function p.slot(f)
	local args = f.args or f
	if f == mw.getCurrentFrame() and args[1] == nil then
		args = f:getParent().args
	end
	
	-- Prepare args and create cache key
	local frameText = ''
	if not args.parsed then
		frameText = (args[1] or ''):match("^%s*(.-)%s*$") -- trim
		args[1] = frameText
	else
		-- For parsed args, create a simple cache key from table contents
		if args[1] and type(args[1]) == 'table' then
			frameText = 'parsed_' .. tostring(#args[1])
		end
	end
	
	local cacheKey = frameText .. '|' .. (args.class or '') .. '|' .. (args.style or '') .. '|' .. (args.mod or '')
	if frameCache[cacheKey] then
		return frameCache[cacheKey]
	end
	
	-- Legacy mod support
	local modData = {
		default = args.mod ~= '' and args.mod or nil
	}
	
	-- Parse frames
	local frames
	if args.parsed then
		frames = args[1]
	elseif args[1] and args[1] ~= '' then
		local randomise = args.class == 'invslot-large' and 'never' or nil
		frames = p.parseFrameText(args[1], randomise, false, modData)
	end
	
	-- Create slot
	local body = mw.html.create('span')
		:addClass('invslot')
		:css('vertical-align', args.align)
	
	-- Handle animation
	if frames and #frames > 1 then
		body:addClass('animated')
	end
	
	-- Default background
	if args.default and args.default ~= '' then
		body:addClass('invslot-default-' .. args.default:lower():gsub(' ', '-'))
	end
	
	-- Custom styles
	if args.class then body:addClass(args.class) end
	if args.style then body:cssText(args.style) end
	
	if not frames or #frames == 0 then
		local result = tostring(body)
		frameCache[cacheKey] = result
		return result
	end
	
	-- Add frames
	local activeFrame = 1
	if frames.randomise == true then
		if not random then
			initDependencies()
		end
		activeFrame = random(#frames)
	end
	
	for i, frame in ipairs(frames) do
		local item
		if frame[1] then
			-- Subframe container
			item = body:tag('span'):addClass('animated-subframe')
			local subActiveFrame = frame.randomise == true and random(#frame) or 1
			
			for sI, sFrame in ipairs(frame) do
				local sItem = makeItem(sFrame, args)
				if sI == subActiveFrame then
					sItem:addClass('animated-active')
				end
				item:node(sItem)
			end
		else
			-- Simple frame
			item = makeItem(frame, args)
		end
		
		if i == activeFrame and #frames > 1 then
			item:addClass('animated-active')
		end
		
		body:node(item)
	end
	
	local result = tostring(body)
	frameCache[cacheKey] = result
	return result
end

-- Optimized frame parsing
function p.parseFrameText(framesText, randomise, aliasReference, modData)
	local frames = { randomise = randomise }
	local subframes = {}
	local subframe = false
	local expandedAliases = aliasReference and {} or nil
	
	local splitFrames = splitOnUnenclosedSemicolons(framesText)
	
	for i, frameText in ipairs(splitFrames) do
		-- Handle subframes
		if frameText:find('^%s*{') then
			frameText = frameText:gsub('^%s*{%s*', '')
			subframe = true
		end
		
		if subframe and frameText:find('}%s*$') then
			frameText = frameText:gsub('%s*}%s*$', '')
			subframe = 'last'
		end
		
		-- Create frame
		local frame = p.makeFrame(frameText, modData and modData.default)
		
		-- Alias processing (optimized)
		local newFrame = { frame }
		if frame.name and frame.name ~= '' then
			local isModItem = frame.mod and frame.mod ~= '' and not vanilla[frame.mod:lower()]
			local alias = getAliasFromCache(frame.name, isModItem)
			
			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
						expandedAliases[curFrame] = aliasData
					end
				end
			end
		end
		
		-- Add frames
		if subframe then
			mergeList(subframes, newFrame)
			
			-- Handle randomization
			if frames.randomise ~= 'never' and subframes.randomise == nil and
				frame.name and frame.name:find('^' .. i18n.prefixes.any .. ' ') then
				subframes.randomise = true
			else
				subframes.randomise = false
			end
			
			if frames.randomise ~= 'never' then
				frames.randomise = false
			end
			
			if subframe == 'last' then
				if #subframes == 1 or (#splitFrames == i and #frames == 0) then
					local lastFrame = #frames
					mergeList(frames, subframes)
					
					if #splitFrames == 1 then
						frames.randomise = subframes.randomise
					end
					
					if aliasReference and subframes.aliasReference then
						for j, aliasRefData in pairs(subframes.aliasReference) do
							expandedAliases[lastFrame + j] = aliasRefData
						end
					end
				else
					frames[#frames + 1] = subframes
				end
				
				subframes = {}
				subframe = false
			end
		else
			if frames.randomise ~= 'never' and frame.name and frame.name:find('^' .. i18n.prefixes.any .. ' ') then
				frames.randomise = true
			else
				frames.randomise = false
			end
			
			mergeList(frames, newFrame)
		end
	end
	
	frames.aliasReference = expandedAliases
	return frames
end

-- Optimized alias expansion
function p.getAlias(aliasFrames, parentFrame)
	if type(aliasFrames) == 'string' then
		local expandedFrame = {
			name = aliasFrames,
			title = parentFrame.title,
			num = parentFrame.num,
			text = parentFrame.text,
			mod = parentFrame.mod
		}
		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
			-- Shallow clone for performance
			expandedFrame = {}
			for k, v in pairs(aliasFrame) do
				expandedFrame[k] = v
			end
		end
		
		-- Apply parent settings (only if not already set)
		expandedFrame.title = parentFrame.title or expandedFrame.title
		expandedFrame.num = parentFrame.num or expandedFrame.num  
		expandedFrame.text = parentFrame.text or expandedFrame.text
		expandedFrame.mod = parentFrame.mod or expandedFrame.mod
		
		expandedFrames[i] = expandedFrame
	end
	
	return expandedFrames
end

-- Optimized frame creation
function p.makeFrame(frameText, defaultMod)
	-- Fast path for simple frames
	if not frameText:find('[%[:,]') then
		return {
			mod = defaultMod,
			name = frameText:match("^%s*(.-)%s*$"),
		}
	end
	
	-- Complex frame parsing
	local frame = { mod = defaultMod }
	
	-- Title
	local title, rest = frameText:match('^%s*%[([^%]]*)%]%s*(.*)')
	if title then
		frame.title = title
		frameText = rest
	end
	
	-- Text
	rest, frame.text = frameText:match('([^%]]*)%s*%[([^%]]*)%]%s*$')
	if frame.text then
		frameText = rest
	end
	
	-- Mod
	local mod, rest = frameText:match('^([^:]+):%s*(.*)')
	if mod and not vanilla[mod:lower()] then
		frame.mod = mod
		frameText = rest
	elseif mod then
		frameText = rest
	else
		frameText = frameText:gsub('^:', '')
	end
	
	-- Name and number
	local name, num = frameText:match('(.*),%s*(%d+)')
	if num then
		frame.name = name:match("^%s*(.-)%s*$")
		frame.num = math.floor(tonumber(num))
		if frame.num < 2 then
			frame.num = nil
		end
	else
		frame.name = frameText:match("^%s*(.-)%s*$")
	end
	
	return frame
end

-- Utility functions (unchanged)
function p.stringifyFrame(frame)
	if not frame.name then
		return ''
	end
	return string.format(
		'[%s]%s:%s,%s[%s]',
		frame.title or '',
		frame.mod or 'Minecraft',
		frame.name,
		frame.num or '',
		frame.text or ''
	)
end

function p.stringifyFrames(frames)
	local result = {}
	for i, frame in ipairs(frames) do
		if frame[1] then
			result[i] = '{' .. p.stringifyFrames(frame) .. '}'
		else
			result[i] = p.stringifyFrame(frame)
		end
	end
	return table.concat(result, ';')
end

return p