More actions
Created page with "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 -- matchin..." Β |
mNo edit summary Β |
||
(6 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 = { | ||
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]], | ||
prefixes = { | prefixes = { | ||
any = 'Any', | any = 'Any', | ||
Line 23: | Line 24: | ||
}, | }, | ||
suffixes = { | suffixes = { | ||
rev = 'Revision %d+', | rev = 'Revision %d+', | ||
be = 'BE', | be = 'BE', | ||
lce = 'LCE', | lce = 'LCE', | ||
Line 35: | Line 33: | ||
p.i18n = i18n | p.i18n = i18n | ||
-- | -- Lazy-loaded dependencies | ||
local random | local random, aliases, VHAliases | ||
local vanilla = { v = 1, vanilla = 1, mc = 1, minecraft = 1 } | 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 function splitOnUnenclosedSemicolons(text) | ||
local semicolon, lbrace, rbrace = | local semicolon, lbrace, rbrace = 59, 91, 93 -- ASCII values for ;[] | ||
local | local bracketDepth = 0 | ||
local splitStart = 1 | local splitStart = 1 | ||
local frames = {} | |||
local frameIndex = 1 | local frameIndex = 1 | ||
for index = 1, text | for index = 1, #text do | ||
local byte = text:byte(index) | local byte = text:byte(index) | ||
if byte == semicolon and | 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 | ||
bracketDepth = bracketDepth + 1 | |||
elseif byte == rbrace then | elseif byte == rbrace then | ||
bracketDepth = bracketDepth - 1 | |||
end | end | ||
end | end | ||
frames[frameIndex] = text:sub(splitStart):match("^%s*(.-)%s*$") | |||
frames[frameIndex] = text:sub(splitStart | |||
return frames | return frames | ||
end | end | ||
-- | -- Optimized table merging | ||
local function mergeList(parentTable, content) | |||
local parentLen = #parentTable | |||
Β | |||
local function mergeList( parentTable, content ) | |||
local | |||
if content[1] then | if content[1] then | ||
-- Merge list into table | -- Merge list into table | ||
for | for i, v in ipairs(content) do | ||
parentTable[i] = v | parentTable[parentLen + i] = v | ||
end | end | ||
else | else | ||
-- Add | -- Add single item | ||
parentTable[ | parentTable[parentLen + 1] = content | ||
end | end | ||
end | end | ||
-- | -- Optimized item creation with reduced expensive calls | ||
local function makeItem(frame, args) | |||
local function makeItem( frame, args ) | local item = mw.html.create('span') | ||
local item = | |||
:addClass('invslot-item') | :addClass('invslot-item') | ||
:addClass(args.imgclass) | :addClass(args.imgclass) | ||
:cssText(args.imgstyle) | :cssText(args.imgstyle) | ||
if | if not frame.name or frame.name == '' then | ||
return item | return item | ||
end | end | ||
-- Frame parameters | -- Frame parameters | ||
local title = frame.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 | ||
-- | -- Optimized file extension detection | ||
local | local img, extension | ||
if mod then | if mod then | ||
img = i18n.legacyFilename:gsub('%$1', name .. ' (' .. mod .. ')') | |||
extension = '.png' -- Default for legacy | |||
img = i18n.legacyFilename:gsub( '%$1', name .. ' (' .. mod .. ')' ) | |||
else | 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 | end | ||
img = img .. extension | img = img .. extension | ||
Β | |||
-- Strip suffixes | -- Strip suffixes (optimized) | ||
for _, suffix in pairs( i18n.suffixes ) do | local cleanName = name | ||
for _, suffix in pairs(i18n.suffixes) do | |||
cleanName = cleanName:gsub(' ' .. suffix .. '$', '') | |||
end | end | ||
-- Determine | -- 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', | link = i18n.modLink:gsub('%$1', mod):gsub('%$2', cleanName) | ||
else | else | ||
link = cleanName:gsub('^' .. i18n.prefixes.damaged .. ' ', '') | |||
link = | |||
end | end | ||
elseif link:lower() == 'none' then | elseif link:lower() == 'none' then | ||
link = nil | link = nil | ||
end | end | ||
if link | |||
-- 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 | ||
-- | -- Process title and tooltips | ||
local formattedTitle, plainTitle | |||
local formattedTitle | |||
if title == '' then | if title == '' then | ||
plainTitle = cleanName | |||
plainTitle = | |||
elseif title:lower() ~= 'none' then | elseif title:lower() ~= 'none' then | ||
plainTitle = title:gsub('\\\\', '\'):gsub('\\&', '&') | |||
plainTitle = title:gsub( '\\\\', '\' ):gsub( '\\&', '&' ) | |||
-- | -- 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 | end | ||
if plainTitle == '' then | if plainTitle == '' then | ||
plainTitle = cleanName | |||
plainTitle = | |||
else | else | ||
plainTitle = plainTitle:gsub('\', '\\'):gsub('&', '&') | |||
plainTitle = plainTitle:gsub( '\', '\\' ):gsub( '&', '&' ) | |||
end | end | ||
elseif link then | elseif link then | ||
formattedTitle = '' | formattedTitle = '' | ||
end | 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('&', '&') | |||
local escapedTitle = ( plainTitle or '' ):gsub( '&', '&' ) | |||
-- Alt text | -- Alt text | ||
local altText = | local altText = 'Inventory sprite for ' .. cleanName | ||
if link then | if link then | ||
altText = altText .. ' linking to ' .. link | altText = altText .. ' linking to ' .. link | ||
end | end | ||
-- Add the image | -- Add the image | ||
item:addClass( 'invslot-item-image' ) | item:addClass('invslot-item-image') | ||
:wikitext( '[[File:', img, ' | :wikitext('[[File:', img, '|link=', link or '', '|alt=', altText, '|', escapedTitle, ']]') | ||
-- Add | -- Add stack number | ||
if num and num > 1 and num < 1000 then | if num and num > 1 and num < 1000 then | ||
local numberSpan = item:tag('span') | |||
:addClass('invslot-stacksize') | |||
:attr('title', plainTitle) | |||
local | :wikitext(num) | ||
if args.numstyle then | if args.numstyle then | ||
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 | ||
return item | return item | ||
end | 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 | -- Main slot function with caching | ||
function p.slot( f ) | function p.slot(f) | ||
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 | ||
-- | -- Prepare args and create cache key | ||
local frameText = '' | |||
if not args.parsed then | if not args.parsed then | ||
-- | frameText = (args[1] or ''):match("^%s*(.-)%s*$") -- trim | ||
args[1] = | 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 | ||
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 = { | local modData = { | ||
default = args.mod ~= '' and args.mod or nil | |||
} | } | ||
-- | -- Parse frames | ||
local frames | local frames | ||
if args.parsed then | if args.parsed then | ||
frames = args[1] | frames = args[1] | ||
elseif args[1] ~= '' then | elseif args[1] and args[1] ~= '' then | ||
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 | -- Create slot | ||
local body = mw.html.create( 'span' ):addClass( 'invslot' ):css | local body = mw.html.create('span') | ||
:addClass('invslot') | |||
:css('vertical-align', args.align) | |||
-- | -- Handle animation | ||
if frames and #frames > 1 then | |||
body:addClass('animated') | |||
body:addClass( 'animated' ) | |||
end | end | ||
-- Default background | -- Default background | ||
if | if args.default and args.default ~= '' then | ||
body:addClass( 'invslot-default-' .. | 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 | ||
if not frames or #frames == 0 then | 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 | end | ||
for i, frame in ipairs(frames) do | |||
for i, frame in ipairs( frames ) do | |||
local item | local item | ||
if frame[1] then | if frame[1] then | ||
-- | -- Subframe container | ||
item = body:tag('span'):addClass('animated-subframe') | |||
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 | |||
for sI, sFrame in ipairs(frame) do | |||
for sI, sFrame in ipairs( frame ) do | local sItem = makeItem(sFrame, args) | ||
local sItem = makeItem( sFrame, args ) | |||
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 | ||
-- | -- Simple frame | ||
item = makeItem( frame, args | item = makeItem(frame, args) | ||
end | end | ||
if i == activeFrame and | |||
if i == activeFrame and #frames > 1 then | |||
item:addClass( 'animated-active' ) | item:addClass('animated-active') | ||
end | end | ||
body:node(item) | |||
end | end | ||
local result = tostring(body) | |||
frameCache[cacheKey] = result | |||
return result | |||
end | end | ||
-- | -- Optimized frame parsing | ||
function p.parseFrameText(framesText, randomise, aliasReference, modData) | |||
function p.parseFrameText( framesText, randomise, aliasReference, modData ) | |||
local frames = { randomise = randomise } | local frames = { randomise = randomise } | ||
local subframes = {} | local subframes = {} | ||
local subframe = false | |||
local expandedAliases = aliasReference and {} or nil | |||
local splitFrames = splitOnUnenclosedSemicolons(framesText) | |||
local splitFrames = splitOnUnenclosedSemicolons( framesText ) | |||
for i, frameText in ipairs(splitFrames) do | |||
for i, frameText in ipairs( splitFrames ) do | -- Handle subframes | ||
-- | if frameText:find('^%s*{') then | ||
frameText = frameText:gsub( '^%s*{%s*', | frameText = frameText:gsub('^%s*{%s*', '') | ||
subframe = true | subframe = true | ||
end | |||
end | |||
if subframe then | if subframe and frameText:find('}%s*$') then | ||
frameText = frameText:gsub('%s*}%s*$', '') | |||
frameText = frameText:gsub( '%s*}%s*$', | subframe = 'last' | ||
end | end | ||
-- | -- Create frame | ||
local frame = p.makeFrame(frameText, modData and modData.default) | |||
local frame = p.makeFrame( frameText, modData and modData.default ) | |||
-- Alias processing | -- 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 newFrame = frame | local alias = getAliasFromCache(frame.name, isModItem) | ||
if | |||
local | |||
if alias then | if alias then | ||
newFrame = p.getAlias(alias, frame) | |||
newFrame = p.getAlias( alias, frame ) | |||
if aliasReference then | if aliasReference then | ||
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 | ||
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 | ||
expandedAliases[curFrame] = aliasData | expandedAliases[curFrame] = aliasData | ||
end | end | ||
Line 450: | Line 409: | ||
end | end | ||
end | end | ||
-- Add frames | -- Add frames | ||
if subframe then | if subframe then | ||
mergeList(subframes, newFrame) | |||
mergeList( subframes, newFrame ) | |||
-- | -- Handle randomization | ||
if frames.randomise ~= 'never' and subframes.randomise == nil and | if frames.randomise ~= 'never' and subframes.randomise == nil and | ||
frame.name: | frame.name and frame.name:find('^' .. i18n.prefixes.any .. ' ') then | ||
subframes.randomise = true | subframes.randomise = true | ||
else | else | ||
Line 467: | Line 422: | ||
end | end | ||
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 | ||
local lastFrame = #frames | local lastFrame = #frames | ||
mergeList( frames, subframes ) | mergeList(frames, subframes) | ||
if #splitFrames == 1 then | if #splitFrames == 1 then | ||
frames.randomise = subframes.randomise | frames.randomise = subframes.randomise | ||
end | end | ||
if aliasReference and subframes.aliasReference then | if aliasReference and subframes.aliasReference then | ||
for j, aliasRefData in pairs(subframes.aliasReference) do | |||
expandedAliases[lastFrame + j] = aliasRefData | |||
for | |||
expandedAliases[lastFrame + | |||
end | end | ||
end | end | ||
else | else | ||
frames[#frames + 1] = subframes | |||
end | end | ||
subframes = {} | subframes = {} | ||
subframe = | subframe = false | ||
end | end | ||
else | else | ||
if frames.randomise ~= 'never' and frame.name and frame.name:find('^' .. i18n.prefixes.any .. ' ') then | |||
if frames.randomise ~= 'never' and frame.name: | |||
frames.randomise = true | frames.randomise = true | ||
else | else | ||
Line 510: | Line 454: | ||
end | end | ||
mergeList(frames, newFrame) | |||
mergeList( frames, newFrame ) | |||
end | end | ||
end | end | ||
frames.aliasReference = expandedAliases | frames.aliasReference = expandedAliases | ||
return frames | return frames | ||
end | end | ||
-- | -- Optimized alias expansion | ||
function p.getAlias(aliasFrames, parentFrame) | |||
function p.getAlias( aliasFrames, parentFrame ) | if type(aliasFrames) == 'string' then | ||
local expandedFrame = { | |||
if type( aliasFrames ) == 'string' then | name = aliasFrames, | ||
local expandedFrame = | title = parentFrame.title, | ||
num = parentFrame.num, | |||
text = parentFrame.text, | |||
mod = parentFrame.mod | |||
} | |||
return { expandedFrame } | return { expandedFrame } | ||
end | end | ||
if aliasFrames.name then | if aliasFrames.name then | ||
aliasFrames = { aliasFrames } | aliasFrames = { aliasFrames } | ||
end | end | ||
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 | ||
expandedFrame = { name = aliasFrame } | expandedFrame = { name = aliasFrame } | ||
else | else | ||
-- | -- Shallow clone for performance | ||
expandedFrame = {} | |||
for k, v in pairs(aliasFrame) do | |||
expandedFrame[k] = v | |||
end | |||
end | end | ||
-- Apply | -- 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 | ||
expandedFrame.mod = parentFrame.mod or expandedFrame.mod | expandedFrame.mod = parentFrame.mod or expandedFrame.mod | ||
Line 567: | Line 504: | ||
end | end | ||
-- | -- Optimized frame creation | ||
function p.makeFrame(frameText, defaultMod) | |||
-- Fast path for simple frames | |||
if not frameText:find('[%[:,]') then | |||
Β | |||
Β | |||
function p.makeFrame( frameText, defaultMod ) | |||
-- | |||
if not frameText: | |||
return { | return { | ||
mod = defaultMod, | mod = defaultMod, | ||
name = | 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 | ||
-- | -- Text | ||
rest, frame.text = frameText:match('([^%]]*)%s*%[([^%]]*)%]%s*$') | |||
if | if frame.text then | ||
frameText = rest | frameText = rest | ||
end | end | ||
-- | -- Mod | ||
local mod, rest = frameText:match('^([^:]+):%s*(.*)') | local mod, rest = frameText:match('^([^:]+):%s*(.*)') | ||
if mod | if mod and not vanilla[mod:lower()] then | ||
frame.mod = mod | |||
frameText = rest | |||
elseif mod then | |||
frameText = rest | frameText = rest | ||
else | else | ||
frameText = frameText:gsub('^:', '') | frameText = frameText:gsub('^:', '') | ||
end | end | ||
-- Name and | -- Name and number | ||
local name, num = frameText:match('(.*),%s*(%d+)') | local name, num = frameText:match('(.*),%s*(%d+)') | ||
if num then | if num then | ||
frame.name = name:match("^%s*(.-)%s*$") | |||
frame.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 | ||
frame.name = frameText:match("^%s*(.-)%s*$") | |||
frame.name = | |||
end | end | ||
return frame | return frame | ||
end | 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 | 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('\\\\', '\'):gsub('\\&', '&')
-- 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('\', '\\'):gsub('&', '&')
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('&', '&')
-- 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