Module:Inventory slot: Difference between revisions

From Modded Wiki
Jump to navigation Jump to search
No edit summary
m 58 revisions imported
 
(51 intermediate revisions by 7 users not shown)
Line 1: Line 1:
local p = {}
local p = {}
-- Individual cell
 
function p.cell( f )
local i18n = {
local args = f.args or f
filename = 'Invicon $1',
if f == mw.getCurrentFrame() and args[1] == nil then
legacyFilename = 'Grid $1.png',
args = f:getParent().args
modLink = 'Mods/$1/$2',
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)
prefixes = {
any = 'Any',
matching = 'Matching',
damaged = 'Damaged',
unwaxed = 'Unwaxed',
},
suffixes = {
be = 'BE',
lce = 'LCE',
},
}
p.i18n = i18n
 
local random = require( i18n.moduleRandom ).random
local aliases = mw.loadData( i18n.moduleAliases )
local pageName = mw.title.getCurrentTitle().text
 
--[[Splits a given text into fragments separated by semicolons that are not
    inside square brackets. Written by AttemptToCallNil for the Russian wiki
--]]
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
end
assert(not nesting, "Unbalanced square brackets found")
frames[frameIndex] = text:sub(splitStart, text:len())
args[1] = mw.text.trim( args[1] or '' )
for index = 1, #frames do
frames[index] = (frames[index]:gsub("^%s+", ""):gsub("%s+$", "")) -- faster mw.text.trim
end
-- Comment this next line out if you're not using aliases
return frames
local aliases = mw.loadData( 'Module:Grid/Aliases' )
end
if aliases then
 
local frames = {}
-- Performs a simple recursive clone of a table's values
for frame in mw.text.gsplit( args[1], '%s*;%s*' ) do
local function cloneTable( origTable )
local frameParts = p.getParts( frame )
local newTable = {}
if aliases[frameParts.name] then
for k, v in pairs( origTable ) do
local aliasFrames = {}
if type( v ) == 'table' then
for aliasFrame in mw.text.gsplit( aliases[frameParts.name], '%s*;%s*' ) do
v = cloneTable( v )
local aliasParts = p.getParts( aliasFrame )
end
aliasParts.title = frameParts.title or aliasParts.title or ''
newTable[k] = v
aliasParts.mod = frameParts.mod or aliasParts.mod or ''
end
aliasParts.num = frameParts.num or aliasParts.num or ''
return newTable
aliasParts.text = frameParts.text or aliasParts.text or ''
end
 
aliasFrames[#aliasFrames + 1] = string.format( '[%s]%s:%s,%s[%s]', aliasParts.title, aliasParts.mod, aliasParts.name, aliasParts.num, aliasParts.text )
--[[Merges a list, or inserts a string
end
or table into a table
--]]
frames[#frames + 1] = table.concat( aliasFrames, ';' )
local function mergeList( parentTable, content )
else
local i = #parentTable + 1
frames[#frames + 1] = frame
if content[1] then
end
-- Merge list into table
for _, v in ipairs( content ) do
parentTable[i] = v
i = i + 1
end
end
else
args[1] = table.concat( frames, ';' )
-- Add strings or tables to table
parentTable[i] = content
end
end
 
-- Creates the HTML for an item
local function makeItem( frame, i, args )
local item = mw.html.create( 'span' ):addClass( 'invslot-item' )
if args.imgclass then
item:addClass( args.imgclass )
end
if frame.name == '' then
return item
end
end
local category
local title = frame.title or mw.text.trim( args.title or '' )
local mod = frame.mod
local name = frame.name or ''
local num = frame.num
local description = frame.text
local animated = args[1]:find( ';' )
local img
local pageName = mw.title.getCurrentTitle().subpageText
if mod then
local class = args.class or ''
img = i18n.legacyFilename:gsub( '%$1', name .. ' (' .. mod .. ')' )
local imgClass = args.imgclass or ''
elseif name:match( '%.gif$' ) or name:match( '%.png$' ) then
local align = args.align or ''
img = i18n.filename:gsub( '%$1', name )
local cell = {}
-- Remove file extension from name
name = name:sub( 0, -5 )
else
-- Fall back to an individual image if the sprite is lacking
img = i18n.filename:gsub( '%$1', name .. '.png' )
end
for frame in mw.text.gsplit( args[1], '%s*;%s*' ) do
local link = args.link or ''
if frame == '' then
if link == '' then
if animated then
if mod then
table.insert( cell, '<span class="item"><br></span>' )
link = i18n.modLink:gsub( '%$1', mod ):gsub( '%$2', name )
else
table.insert( cell, '<br>' )
end
else
else
local parts = p.getParts( frame, args.mod )
link = name:gsub( '^' .. i18n.prefixes.damaged .. ' ', '' )
local tooltipTitle = parts.title
for _, suffix in pairs( i18n.suffixes ) do
local mod = parts.mod
link = link:gsub( ' ' .. suffix .. '$', '' )
local name = parts.name
local num = parts.num
local tooltipDesc = parts.text
local img
if mod then
img = name .. ' (' .. mod .. ')'
else
img = name
end
local link = args.link or ''
if link == '' then
if name == pageName then
link = 'none'
elseif mod then
link = 'Mods/' .. mod .. '/' .. name
else
link = name
end
end
end
local title = args.title or ''
if title == '' then
if tooltipTitle then
title = tooltipTitle:gsub( '&[0-9a-fk-or]', '' )
elseif link:lower() == 'none' or link ~= name:gsub( '%s%(.*', '' ) then
title = name:gsub( '%s%(.*', '' )
end
end
if not tooltipTitle and title:lower() == 'none' then
tooltipTitle = 0
end
local alt = img
if title:lower() ~= 'none' then
alt = title
end
if link:lower() == 'none' then
link = ''
end
if title:lower() == 'none' then
title = ''
end
local tooltip = ''
if tooltipTitle then
tooltip = ' data-minetip-title="' .. tooltipTitle .. '"'
end
if tooltipDesc then
tooltip = tooltip .. ' data-minetip-text="' .. tooltipDesc .. '"'
end
local image = {
'<span class="item ' .. imgClass .. '" title="' .. title .. '"' .. tooltip .. '>',
'[[File:Grid ' .. img .. '.png|32x32px|link=' .. link .. '|alt=' .. alt .. ']]',
'</span>',
}
image[1] = image[1]:gsub( ' title=""', '' )
image[2] = image[2]:gsub( '||', '|' )
if num and num > 1 and num < 1000 then
if link ~= '' then
num = '[[' .. link .. '|' .. num .. ']]'
end
table.insert( image, 3, '<span class="number">' .. num .. '</span>' )
end
table.insert( cell, table.concat( image, '' ) )
end
end
elseif link:lower() == 'none' then
link = nil
end
if link == pageName then
link = nil
end
end
if animated then
local formattedTitle
cell[1] = cell[1]:gsub( 'class="item', 'class="item active' )
local plainTitle
class = 'animated ' .. class
if title == '' then
plainTitle = name
elseif title:lower() ~= 'none' then
plainTitle = title:gsub( '\\\\', '&#92;' ):gsub( '\\&', '&#38;' )
local formatPattern = '&[0-9a-fk-or]'
if plainTitle:match( formatPattern ) then
formattedTitle = title
plainTitle = plainTitle:gsub( formatPattern, '' )
end
if plainTitle == '' then
plainTitle = name
else
plainTitle = plainTitle:gsub( '&#92;', '\\' ):gsub( '&#38;', '&' )
end
elseif link then
formattedTitle = ''
end
end
if align ~= '' then
item:attr{
align = ' style="vertical-align:' .. align .. '"'
['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;' )
local altText = img .. ': Inventory sprite for ' .. name .. ' in Minecraft as shown in-game'
if link then
altText = altText .. ' linking to ' .. link
end
end
local html = {
if formattedTitle or plainTitle or link then
'<span class="grid grid2 ' .. class .. '"' .. align .. '>',
altText = altText .. ' with description: ' .. ( formattedTitle or plainTitle or link )
table.concat( cell, '' ),
if description then
'</span>'
altText = altText .. ' ' .. description:gsub( '/', ' ' )
}
end
altText = altText:gsub( '&[0-9a-fk-or]', '' )
end
item:addClass( 'invslot-item-image' )
:wikitext( '[[File:', img, '|32x32px|link=', link or '', '|alt=', altText, '|', escapedTitle, ']]' )
if ( args.default or '' ) ~= '' then
if num and num > 1 and num < 1000 then
local defaultClass = ''
if link then
if animated then
item:wikitext( '[[', link, '|' )
defaultClass = ' skip'
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
table.insert( html, 2, '<span class="default-item' .. defaultClass .. '">[[File:Grid ' .. args.default .. '.png|32px|alt=|link=]]</span>' )
end
end
html = table.concat( html, '' ):gsub( ' "', '"' )
item:wikitext( category )
return html
return item
end
end


function p.getParts( frame, mod )
-- Main entry point
local parts = {}
function p.slot( f )
parts.title = frame:match( '^%[%s*([^%]]+)%s*%]' )
local args = f.args or f
if f == mw.getCurrentFrame() and args[1] == nil then
args = f:getParent().args
end
parts.mod = mw.text.trim( frame:match( '([^:%]]+):' ) or mod or '' )
if not args.parsed then
args[1] = mw.text.trim( args[1] or '' )
end
local vanilla = { v = 1, vanilla = 1, mc = 1, minecraft = 1 }
local modData = {
if parts.mod == '' or vanilla[parts.mod:lower()] then
aliases = args.modaliases or '',
parts.mod = 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
end
local nameStart = ( frame:find( ':' ) or frame:find( '%]' ) or 0 ) + 1
local frames
if nameStart - 1 == #frame then
if args.parsed then
nameStart = 1
frames = args[1]
elseif args[1] ~= '' then
local randomise = args.class == 'invslot-large' and 'never' or nil
frames = p.parseFrameText( args[1], randomise, false, modData )
end
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
body:addClass( 'animated' )
end
if args.class then
body:addClass( args.class )
end
if args.style then
body:cssText( args.style )
end
if ( args.default or '' ) ~= '' then
body:addClass( 'invslot-default-' .. string.lower( args.default ):gsub( ' ', '-' ) )
end
end
parts.name = mw.text.trim( frame:sub( nameStart, ( frame:find( '[,%[]', nameStart ) or 0 ) - 1 ) )
parts.num = math.floor( frame:match( ',%s*(%d+)' ) or 0 )
--mw.logObject( frames )
if parts.num == 0 then
if not frames then
parts.num = nil
return tostring( body )
end
end
parts.text = frame:match( '%[%s*([^%]]+)%s*%]$' )
local activeFrame = frames.randomise == true and random( #frames ) or 1
for i, frame in ipairs( frames ) do
local item
-- Table is a list, must contain subframes
if frame[1] then
item = body:tag( 'span' ):addClass( 'animated-subframe' )
local subActiveFrame = frame.randomise and random( #frame ) or 1
for sI, sFrame in ipairs( frame ) do
local sItem = makeItem( sFrame, sI, args )
item:node( sItem )
if sI == subActiveFrame then
sItem:addClass( 'animated-active' )
end
end
else
item = makeItem( frame, i, args )
body:node( item )
end
if i == activeFrame and animated then
item:addClass( 'animated-active' )
end
end
return parts
return tostring( body )
end
end


--- GUI variants; called directly to avoid the overhead of a bunch of #invoke calls per GUI
--[[Parses the frame text into a table of frames and subframes,
-- Crafting table
expanding aliases (and optionally retaining a reference), and
function p.craftingTable( f )
deciding if the slot can be randomised
local args = f
--]]
if f == mw.getCurrentFrame() then
function p.parseFrameText( framesText, randomise, aliasReference, modData )
args = f:getParent().args
local frames = { randomise = randomise }
local subframes = {}
local subframe
local expandedAliases
local splitFrames = splitOnUnenclosedSemicolons( framesText )
for i, frameText in ipairs( splitFrames ) do
frameText = frameText:gsub( '^%s*{%s*', function()
subframe = true
return ''
end )
if subframe then
frameText = frameText:gsub( '%s*}%s*$', function()
subframe = 'last'
return ''
end )
end
local frame = p.makeFrame( frameText, modData and modData.default )
local newFrame = frame
if aliases or modData.aliases then
local id = frame.name
if frame.mod then
id = frame.mod .. ':' .. id
end
local alias = modData and modData.aliases and modData.aliases[id] or
aliases and aliases[id]
if alias then
newFrame = p.getAlias( alias, frame )
if aliasReference then
local curFrame = #frames + 1
local aliasData = { frame = frame, length = #newFrame }
if subframe then
if not subframes.aliasReference then
subframes.aliasReference = {}
end
subframes.aliasReference[#subframes + 1] = aliasData
else
if not expandedAliases then
expandedAliases = {}
end
expandedAliases[curFrame] = aliasData
end
end
end
end
if subframe then
mergeList( subframes, newFrame )
-- Randomise starting frame for "Any *" aliases, as long as the alias is the only subframe
if frames.randomise ~= 'never' and subframes.randomise == nil and
frame.name:match( '^' .. 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
-- 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
mergeList( frames, subframes )
else
table.insert( frames, subframes )
end
subframes = {}
subframe = nil
end
else
-- Randomise starting frame for "Any *" aliases, as long as the alias is the only frame
if frames.randomise == nil and frame.name:match( '^' .. i18n.prefixes.any .. ' ' ) then
frames.randomise = true
elseif frames.randomise ~= 'never' then
frames.randomise = false
end
mergeList( frames, newFrame )
end
end
end
local arrow = 'Arrow (small)'
frames.aliasReference = expandedAliases
local shapeless = ''
if args.arrow or '' ~= '' then
return frames
arrow = args.arrow .. ' (' .. args.Mod .. ')'
end
 
--[[Returns a new table with the parts of the parent frame
added to the alias
--]]
function p.getAlias( aliasFrames, parentFrame )
-- If alias is just a name, return the parent frame with the new name
if type( aliasFrames ) == 'string' then
local expandedFrame = mw.clone( parentFrame )
expandedFrame.name = aliasFrames
return { expandedFrame }
end
end
if args.shapeless or '' ~= '' then
shapeless = '<span title="This recipe is shapeless; the inputs may be placed in any arrangement in the crafting grid.">[[File:Grid layout Shapeless.png|link=]]</span>'
-- Single frame alias, put in list
if aliasFrames.name then
aliasFrames = { aliasFrames }
end
end
local html = {
local expandedFrames = {}
'{| class="grid-Crafting_Table" cellpadding="0" cellspacing="0"',
for i, aliasFrame in ipairs( aliasFrames ) do
'| ' .. p.cell{ args.A1, mod = args.Mod, link = args.A1link, title = args.A1title },
local expandedFrame
'| ' .. p.cell{ args.B1, mod = args.Mod, link = args.B1link, title = args.B1title },
if type( aliasFrame ) == 'string' then
'| ' .. p.cell{ args.C1, mod = args.Mod, link = args.C1link, title = args.C1title },
expandedFrame = { name = aliasFrame }
'| rowspan="2" class="arrow" | [[File:Grid layout ' .. arrow .. '.png|link=]]',
else
'| rowspan="3" | ' .. p.cell{ args.Output, mod = args.Mod, link = args.Olink, title = args.Otitle, class = 'output' },
expandedFrame = cloneTable( aliasFrame )
'|-',
end
'| ' .. p.cell{ args.A2, mod = args.Mod, link = args.A2link, title = args.A2title },
expandedFrame.title = parentFrame.title or expandedFrame.title
'| ' .. p.cell{ args.B2, mod = args.Mod, link = args.B2link, title = args.B2title },
expandedFrame.mod = parentFrame.mod or expandedFrame.mod
'| ' .. p.cell{ args.C2, mod = args.Mod, link = args.C2link, title = args.C2title },
expandedFrame.num = parentFrame.num or expandedFrame.num
'|-',
expandedFrame.text = parentFrame.text or expandedFrame.text
'| ' .. p.cell{ args.A3, mod = args.Mod, link = args.A3link, title = args.A3title },
'| ' .. p.cell{ args.B3, mod = args.Mod, link = args.B3link, title = args.B3title },
expandedFrames[i] = expandedFrame
'| ' .. p.cell{ args.C3, mod = args.Mod, link = args.C3link, title = args.C3title },
end
'| class="shapeless" | ' .. shapeless,
'|}'
}
return table.concat( html, '\n' );
return expandedFrames
end
 
function p.expandAlias( parentFrame, alias )
return p.getAlias( alias, parentFrame )
end
end


-- Furnace
function p.stringifyFrame( frame )
function p.furnace( f )
if not frame.name then
local args = f
return ''
if f == mw.getCurrentFrame() then
args = f:getParent().args
end
end
args = require( 'Module:ProcessArgs' ).norm( args )
return string.format(
'[%s]%s:%s,%s[%s]',
local progress = 'Furnace Progress'
frame.title or '',
local burning = ' (in-active)'
frame.mod or 'Minecraft',
local smelting = burning
frame.name,
local fuelUsage = 'Fire'
frame.num or '',
frame.text or ''
if args.Progress then
)
progress = args.Progress .. ' Progress'
end
if args.Mod then
 
progress = progress .. ' (' .. args.Mod .. ')'
function p.stringifyFrames( frames )
end
for i, frame in ipairs( frames ) do
frames[i] = p.stringifyFrame( frame )
end
end
return table.concat( frames, ';' )
end


if args.Input and args.Fuel then
-- Splits up the frame text into its parts
burning = ''
function p.makeFrame( frameText, mod )
if args.Output then
-- Simple frame with no parts
smelting = ''
if not frameText:match( '[%[:,]' ) then
end
return {
mod = mod,
name = mw.text.trim( frameText ),
}
end
end
if args.FuelUsage then
frameText = frameText:gsub( '%s*([%[%]:,;])%s*', '%1' )
fuelUsage = args.FuelUsage
if args.Mod then
local frame = {}
fuelUsage = fuelUsage .. ' (' .. args.Mod .. ')'
frame.title = frameText:match( '^%[([^%]]+)%]' )
end
frame.mod = frameText:match( '([^:%]]+):' ) or mod
local vanilla = { v = 1, vanilla = 1, mc = 1, minecraft = 1 }
if frame.mod and vanilla[mw.ustring.lower( frame.mod )] or frame.mod == '' then
frame.mod = nil
end
end
local html = {
local nameStart = ( frameText:find( ':' ) or frameText:find( '%]' ) or 0 ) + 1
'{| class="grid-Furnace" cellpadding="0" cellspacing="0"',
if nameStart - 1 == #frameText then
'| ' .. p.cell{ args.Input, mod = args.Mod, link = args.Ilink, title = args.Ititle },
nameStart = 1
'| rowspan="3" class="arrow" | [[File:Grid layout ' .. progress .. smelting .. '.png|link=]]',
'| rowspan="3" class="output" | ' .. p.cell{ args.Output, mod = args.Mod, link = args.Olink, title = args.Otitle, class = 'output' },
'|-',
'| [[File:Grid layout ' .. fuelUsage .. burning .. '.png|link=]]',
'|-',
'| ' .. p.cell{ args.Fuel, mod = args.Mod, link = args.Flink, title = args.Ftitle },
'|}'
}
return table.concat( html, '\n' );
end
 
-- Brewing Stand
function p.brewingStand( f )
local args = f
if f == mw.getCurrentFrame() then
args = f:getParent().args
end
end
args = require( 'Module:ProcessArgs' ).norm( args )
frame.name = frameText:sub( nameStart, ( frameText:find( '[,%[]', nameStart ) or 0 ) - 1 )
local inactive = ' (In-active)'
frame.num = math.floor( frameText:match( ',(%d+)' ) or 0 )
if args.Input and ( args.Output1 or args.Output2 or args.Output3 ) then
if frame.num == 0 then
inactive = ''
frame.num = nil
end
end
local html = {
frame.text = frameText:match( '%[([^%]]+)%]$' )
'<div class="grid-Brewing_Stand">',
'{| cellpadding="0" cellspacing="0"',
'| class="bubbles" | [[File:Grid layout Brewing Bubbles.gif|link=]]',
'| class="input" | ' .. p.cell{ args.Input, mod = args.Mod, link = args.Ilink, title = args.Ititle },
'| [[File:Grid layout Brewing Arrow' .. inactive .. '.png|link=]]',
'|-',
'| class="output1" | ' .. p.cell{ args.Output1, mod = args.Mod, link = args.O1link, title = args.O1title, default = 'layout Brewing Empty' },
'| class="output2" | ' .. p.cell{ args.Output2, mod = args.Mod, link = args.O2link, title = args.O2title, default = 'layout Brewing Empty' },
'| class="output3" | ' .. p.cell{ args.Output3, mod = args.Mod, link = args.O3link, title = args.O3title, default = 'layout Brewing Empty' },
'|-',
'| class="paths" colspan="3" | [[File:Grid layout Brewing Paths.png|link=]]',
'|}',
'</div>'
}
return table.concat( html, '\n' );
return frame
end
function p.getParts( frameText, mod )
return p.makeFrame( frameText, mod )
end
end
   
   
return p
return p

Latest revision as of 08:10, 4 July 2024

This module implements {{inventory slot}}.

Dependencies[edit source]

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 = {}

local i18n = {
	filename = 'Invicon $1',
	legacyFilename = 'Grid $1.png',
	modLink = 'Mods/$1/$2',
	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)
	prefixes = {
		any = 'Any',
		matching = 'Matching',
		damaged = 'Damaged',
		unwaxed = 'Unwaxed',
	},
	suffixes = {
		be = 'BE',
		lce = 'LCE',
	},
}
p.i18n = i18n

local random = require( i18n.moduleRandom ).random
local aliases = mw.loadData( i18n.moduleAliases )
local pageName = mw.title.getCurrentTitle().text

--[[Splits a given text into fragments separated by semicolons that are not
    inside square brackets. Written by AttemptToCallNil for the Russian wiki
--]]
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 mw.text.trim
	end
	
	return frames
end

-- Performs a simple recursive clone of a table's values
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
--]]
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 for an item
local function makeItem( frame, i, args )
	local item = mw.html.create( 'span' ):addClass( 'invslot-item' )
	if args.imgclass then
		item:addClass( args.imgclass )
	end
	if frame.name == '' then
		return item
	end
	local category
	local title = frame.title or mw.text.trim( args.title or '' )
	local mod = frame.mod
	local name = frame.name or ''
	local num = frame.num
	local description = frame.text
	
	local img
	if mod then
		img = i18n.legacyFilename:gsub( '%$1', name .. ' (' .. mod .. ')' )
	elseif name:match( '%.gif$' ) or name:match( '%.png$' ) then
		img = i18n.filename:gsub( '%$1', name )
		-- Remove file extension from name
		name = name:sub( 0, -5 )
	else
		-- Fall back to an individual image if the sprite is lacking
		img = i18n.filename:gsub( '%$1', name .. '.png' )
	end
	
	local link = args.link or ''
	if link == '' then
		if mod then
			link = i18n.modLink:gsub( '%$1', mod ):gsub( '%$2', name )
		else
			link = name:gsub( '^' .. i18n.prefixes.damaged .. ' ', '' )
			for _, suffix in pairs( i18n.suffixes ) do
				link = link:gsub( ' ' .. suffix .. '$', '' )
			end
		end
	elseif link:lower() == 'none' then
		link = nil
	end
	if link == pageName then
		link = nil
	end
	
	local formattedTitle
	local plainTitle
	if title == '' then
		plainTitle = name
	elseif title:lower() ~= 'none' then
		plainTitle = title:gsub( '\\\\', '&#92;' ):gsub( '\\&', '&#38;' )
		
		local formatPattern = '&[0-9a-fk-or]'
		if plainTitle:match( formatPattern ) then
			formattedTitle = title
			plainTitle = plainTitle:gsub( formatPattern, '' )
		end
		
		if plainTitle == '' then
			plainTitle = name
		else
			plainTitle = plainTitle:gsub( '&#92;', '\\' ):gsub( '&#38;', '&' )
		end
	elseif link then
		formattedTitle = ''
	end
	
	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;' )
	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-fk-or]', '' )
	end
	item:addClass( 'invslot-item-image' )
		:wikitext( '[[File:', img, '|32x32px|link=', link or '', '|alt=', altText, '|', escapedTitle, ']]' )
	
	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
	
	item:wikitext( category )
	
	return item
end

-- Main entry point
function p.slot( f )
	local args = f.args or f
	if f == mw.getCurrentFrame() and args[1] == nil then
		args = f:getParent().args
	end
	
	if not args.parsed then
		args[1] = mw.text.trim( args[1] or '' )
	end
	
	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
	
	local frames
	if args.parsed then
		frames = args[1]
	elseif args[1] ~= '' then
		local randomise = args.class == 'invslot-large' and 'never' or nil
		frames = p.parseFrameText( args[1], randomise, false, modData )
	end
	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
		body:addClass( 'animated' )
	end
	if args.class then
		body:addClass( args.class )
	end
	if args.style then
		body:cssText( args.style )
	end
	if ( args.default or '' ) ~= '' then
		body:addClass( 'invslot-default-' .. string.lower( args.default ):gsub( ' ', '-' ) )
	end
	
	--mw.logObject( frames )
	if not frames then
		return tostring( body )
	end
	
	local activeFrame = frames.randomise == true and random( #frames ) or 1
	for i, frame in ipairs( frames ) do
		local item
		-- Table is a list, must contain subframes
		if frame[1] then
			item = body:tag( 'span' ):addClass( 'animated-subframe' )
			local subActiveFrame = frame.randomise and random( #frame ) or 1
			for sI, sFrame in ipairs( frame ) do
				local sItem = makeItem( sFrame, sI, args )
				item:node( sItem )
				
				if sI == subActiveFrame then
					sItem:addClass( 'animated-active' )
				end
			end
		else
			item = makeItem( frame, i, args )
			body:node( item )
		end
		if i == activeFrame and animated then
			item:addClass( 'animated-active' )
		end
	end
	
	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
--]]
function p.parseFrameText( framesText, randomise, aliasReference, modData )
	local frames = { randomise = randomise }
	local subframes = {}
	local subframe
	local expandedAliases
	local splitFrames = splitOnUnenclosedSemicolons( framesText )
	for i, frameText in ipairs( splitFrames ) do
		frameText = frameText:gsub( '^%s*{%s*', function()
			subframe = true
			return ''
		end )
		if subframe then
			frameText = frameText:gsub( '%s*}%s*$', function()
				subframe = 'last'
				return ''
			end )
		end
		local frame = p.makeFrame( frameText, modData and modData.default )
		local newFrame = frame
		if aliases or modData.aliases then
			local id = frame.name
			if frame.mod then
				id = frame.mod .. ':' .. id
			end
			
			local alias = modData and modData.aliases and modData.aliases[id] or
				aliases and aliases[id]
			if alias then
				newFrame = p.getAlias( alias, frame )
				if aliasReference then
					local curFrame = #frames + 1
					local aliasData = { frame = frame, length = #newFrame }
					if subframe then
						if not subframes.aliasReference then
							subframes.aliasReference = {}
						end
						subframes.aliasReference[#subframes + 1] = aliasData
					else
						if not expandedAliases then
							expandedAliases = {}
						end
						expandedAliases[curFrame] = aliasData
					end
				end
			end
		end
		
		if subframe then
			mergeList( subframes, newFrame )
			-- Randomise starting frame for "Any *" aliases, as long as the alias is the only subframe
			if frames.randomise ~= 'never' and subframes.randomise == nil and
				frame.name:match( '^' .. 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
				-- 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
					mergeList( frames, subframes )
				else
					table.insert( frames, subframes )
				end
				subframes = {}
				subframe = nil
			end
		else
			-- Randomise starting frame for "Any *" aliases, as long as the alias is the only frame
			if frames.randomise == nil and frame.name:match( '^' .. i18n.prefixes.any .. ' ' ) then
				frames.randomise = true
			elseif frames.randomise ~= 'never' then
				frames.randomise = false
			end
			mergeList( frames, newFrame )
		end
	end
	
	frames.aliasReference = expandedAliases
	
	return frames
end

--[[Returns a new table with the parts of the parent frame
	added to the alias
--]]
function p.getAlias( aliasFrames, parentFrame )
	-- If alias is just a name, return the parent frame with the new name
	if type( aliasFrames ) == 'string' then
		local expandedFrame = mw.clone( parentFrame )
		expandedFrame.name = aliasFrames
		return { expandedFrame }
	end
	
	-- Single frame alias, put in list
	if aliasFrames.name then
		aliasFrames = { aliasFrames }
	end
	
	local expandedFrames = {}
	for i, aliasFrame in ipairs( aliasFrames ) do
		local expandedFrame
		if type( aliasFrame ) == 'string' then
			expandedFrame = { name = aliasFrame }
		else
			expandedFrame = cloneTable( aliasFrame )
		end
		expandedFrame.title = parentFrame.title or expandedFrame.title
		expandedFrame.mod = parentFrame.mod or expandedFrame.mod
		expandedFrame.num = parentFrame.num or expandedFrame.num
		expandedFrame.text = parentFrame.text or expandedFrame.text
		
		expandedFrames[i] = expandedFrame
	end
	
	return expandedFrames
end

function p.expandAlias( parentFrame, alias )
	return p.getAlias( alias, parentFrame )
end

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 )
	for i, frame in ipairs( frames ) do
		frames[i] = p.stringifyFrame( frame )
	end
	return table.concat( frames, ';' )
end

-- Splits up the frame text into its parts
function p.makeFrame( frameText, mod )
	-- Simple frame with no parts
	if not frameText:match( '[%[:,]' ) then
		return {
			mod = mod,
			name = mw.text.trim( frameText ),
		}
	end
	
	frameText = frameText:gsub( '%s*([%[%]:,;])%s*', '%1' )
	
	local frame = {}
	frame.title = frameText:match( '^%[([^%]]+)%]' )
	
	frame.mod = frameText:match( '([^:%]]+):' ) or mod
	local vanilla = { v = 1, vanilla = 1, mc = 1, minecraft = 1 }
	if frame.mod and vanilla[mw.ustring.lower( frame.mod )] or frame.mod == '' then
		frame.mod = nil
	end
	
	local nameStart = ( frameText:find( ':' ) or frameText:find( '%]' ) or 0 ) + 1
	if nameStart - 1 == #frameText then
		nameStart = 1
	end
	frame.name = frameText:sub( nameStart, ( frameText:find( '[,%[]', nameStart ) or 0 ) - 1 )
	
	frame.num = math.floor( frameText:match( ',(%d+)' ) or 0 )
	if frame.num == 0 then
		frame.num = nil
	end
	
	frame.text = frameText:match( '%[([^%]]+)%]$' )
	
	return frame
end
function p.getParts( frameText, mod )
	return p.makeFrame( frameText, mod )
end
 
return p