Module:Inventory slot: Difference between revisions

From Modded Wiki
Jump to navigation Jump to search
Whose stupid idea was it to use capital names? Oh... right
Reverted my changes on this for now
Tag: Manual revert
 
(99 intermediate revisions by 10 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
modLink = '$1:$2',
if f == mw.getCurrentFrame() and args[1] == nil then
moduleAliases = [[Module:Inventory slot/Aliases]],
args = f:getParent().args
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 num = frame.num
local description = frame.text
local animated = args[1]:find( ';' )
local mod = frame.mod or 'Minecraft'
local pageName = mw.title.getCurrentTitle().subpageText
local name = frame.name
local class = args.class or ''
local imgClass = args.imgclass or ''
local align = args.align or ''
local cell = {}
for frame in mw.text.gsplit( args[1], '%s*;%s*' ) do
 
if frame == '' then
if frame.name:match( '%.gif$' ) and frame.name:match( '%.png$' ) then
if animated then
name = frame.name
table.insert( cell, '<span class="item"><br></span>' )
mod = ''
else
end
table.insert( cell, '<br>' )
 
end
local minecraft = mod == "Minecraft" or mod == "minecraft"
 
local img = frame.img
if not img then
if name:match( '%.gif$' ) or name:match( '%.png$' ) then
img = name
-- Remove file extension from name
name = name:sub( 0, -5 )
elseif minecraft then
img = 'Invicon ' .. name .. '.png'
elseif mod then
img = mod .. ' ' .. name .. '.png'
else
else
local parts = p.getParts( frame, args.mod )
img = name
local tooltipTitle = parts.title
local mod = parts.mod
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
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
end
if name:match( ' Revision %d+' ) then
name = name:gsub( ' Revision %d+', '' )
end
end
if animated then
local link = args.link or ''
cell[1] = cell[1]:gsub( 'class="item', 'class="item active' )
if link == '' then
class = 'animated ' .. class
link = i18n.modLink:gsub( '%$1', mod ):gsub( '%$2', name )
elseif link:lower() == 'none' then
link = nil
end
if link == pageName then
link = nil
end
end
if align ~= '' then
local formattedTitle
align = ' style="vertical-align:' .. align .. '"'
local plainTitle
if title == '' then
plainTitle = name
elseif title:lower() ~= 'none' then
plainTitle = title:gsub( '\\\\', '&#92;' ):gsub( '\\&', '&#38;' )
local formatPatterns = {'&[0-9a-jl-qs-vyzr]', '&#%x%x%x%x%x%x', '&$%x%x%x'}
for _, formatPattern in ipairs( formatPatterns ) do
if plainTitle:match( formatPattern ) then
formattedTitle = title
plainTitle = plainTitle:gsub( formatPattern, '' )
end
end
if plainTitle == '' then
plainTitle = name
else
plainTitle = plainTitle:gsub( '&#92;', '\\' ):gsub( '&#38;', '&' )
end
elseif link then
formattedTitle = ''
end
end
local html = {
'<span class="grid2 ' .. class .. '"' .. align .. '>',
item:attr{
table.concat( cell, '' ),
['data-minetip-title'] = formattedTitle,
'</span>'
['data-minetip-text'] = description
}
}
if ( args.default or '' ) ~= '' then
-- & is re-escaped because mw.html treats attributes
local defaultClass = ''
-- as plain text, but MediaWiki doesn't
if animated then
local escapedTitle = ( plainTitle or '' ):gsub( '&', '&#38;' )
defaultClass = ' skip'
local altText = img .. ': Inventory sprite for ' .. name .. ' in Minecraft as shown in-game'
if link then
altText = altText .. ' linking to ' .. link
end
if formattedTitle or plainTitle or link then
altText = altText .. ' with description: ' .. ( formattedTitle or plainTitle or link )
if description then
altText = altText .. ' ' .. description:gsub( '/', ' ' )
end
altText = altText:gsub( '&[0-9a-jl-qs-wr]', '' )
end
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
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 == true 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
local lastFrame = #frames
mergeList( frames, subframes )
-- Inherit the randomise flag if it's the only frame
if #splitFrames == 1 then
frames.randomise = subframes.randomise
end
-- Append alias reference data, if present
if aliasReference and subframes.aliasReference then
if not expandedAliases then
expandedAliases = {}
end
for i, aliasRefData in pairs(subframes.aliasReference) do
expandedAliases[lastFrame + i] = aliasRefData
end
end
else
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 ~= 'never' and frame.name:match( '^' .. i18n.prefixes.any .. ' ' ) then
frames.randomise = true
else
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
end


-- Furnace
function p.expandAlias( parentFrame, alias )
function p.furnace( f )
return p.getAlias( alias, parentFrame )
local args = f
end
if f == mw.getCurrentFrame() then
 
args = f:getParent().args
function p.stringifyFrame( frame )
if not frame.name then
return ''
end
end
args = require( 'Module:ProcessArgs' ).norm( args )
return string.format(
'[%s]%s,%s[%s]',
local progress = 'Furnace Progress'
frame.title or '',
local burning = ' (in-active)'
frame.name,
local smelting = burning
frame.num or '',
local fuelUsage = 'Fire'
frame.text or ''
)
if args.Progress then
end
progress = args.Progress .. ' Progress'
 
if args.Mod then
function p.stringifyFrames( frames )
progress = progress .. ' (' .. args.Mod .. ')'
for i, frame in ipairs( frames ) do
end
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 15:39, 30 April 2025

This module implements {{inventory slot}}.

Dependencies

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



local p = {}

local i18n = {
	modLink = '$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 num = frame.num
	local description = frame.text
	
	local mod = frame.mod or 'Minecraft'
	local name = frame.name
	

	if frame.name:match( '%.gif$' ) and frame.name:match( '%.png$' ) then 
		name = frame.name
		mod = ''
	end

	local minecraft = mod == "Minecraft" or mod == "minecraft"

	local img = frame.img
	if not img then
		if name:match( '%.gif$' ) or name:match( '%.png$' ) then
			img = name
			-- Remove file extension from name
			name = name:sub( 0, -5 )
		elseif minecraft then
			img = 'Invicon ' .. name .. '.png'
		elseif mod then
			img = mod .. ' ' .. name .. '.png'
		else
			img = name
		end
	end

	if name:match( ' Revision %d+' ) then
		name = name:gsub( ' Revision %d+', '' )
	end
	
	local link = args.link or ''
	if link == '' then
		link = i18n.modLink:gsub( '%$1', mod ):gsub( '%$2', name )
	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 formatPatterns = {'&[0-9a-jl-qs-vyzr]', '&#%x%x%x%x%x%x', '&$%x%x%x'}
		for _, formatPattern in ipairs( formatPatterns ) do
			if plainTitle:match( formatPattern ) then
				formattedTitle = title
				plainTitle = plainTitle:gsub( formatPattern, '' )
			end
		end
		
		if plainTitle == '' then
			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-jl-qs-wr]', '' )
	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 == true 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
					local lastFrame = #frames
					mergeList( frames, subframes )
					
					-- Inherit the randomise flag if it's the only frame
					if #splitFrames == 1 then
						frames.randomise = subframes.randomise
					end
					
					-- Append alias reference data, if present
					if aliasReference and subframes.aliasReference then
						if not expandedAliases then
							expandedAliases = {}
						end
						for i, aliasRefData in pairs(subframes.aliasReference) do
							expandedAliases[lastFrame + i] = aliasRefData
						end
					end
				else
					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 ~= 'never' and frame.name:match( '^' .. i18n.prefixes.any .. ' ' ) then
				frames.randomise = true
			else
				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]',
		frame.title or '',
		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