Module:Inventory slot: Difference between revisions

From Modded Wiki
Jump to navigation Jump to search
mw.html annoying treats attribute values as plain text, rather than HTML text which can have HTML entities in it, unlike the rest of MediaWiki and the web in general. Also fixed formatting only titles not working.
m 58 revisions imported
 
(33 intermediate revisions by 6 users not shown)
Line 1: Line 1:
local p = {}
local p = {}


function p.slot( 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:Inventory slot/Aliases' )
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 modAliases = args.modaliases or ''
local img
if modAliases ~= '' then
if mod then
modAliases = mw.loadData( 'Module:' .. modAliases )
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
else
modAliases = nil
-- Fall back to an individual image if the sprite is lacking
img = i18n.filename:gsub( '%$1', name .. '.png' )
end
end
if aliases or modAliases then
local link = args.link or ''
local frames = {}
if link == '' then
for frame in mw.text.gsplit( args[1], '%s*;%s*' ) do
if mod then
local frameParts = p.getParts( frame, args.mod )
link = i18n.modLink:gsub( '%$1', mod ):gsub( '%$2', name )
else
local id = frameParts.name
link = name:gsub( '^' .. i18n.prefixes.damaged .. ' ', '' )
if frameParts.mod then
for _, suffix in pairs( i18n.suffixes ) do
id = frameParts.mod .. ':' .. id
link = link:gsub( ' ' .. suffix .. '$', '' )
end
local alias
if modAliases and modAliases[id] then
alias = modAliases[id]
elseif aliases and aliases[id] then
alias = aliases[id]
end
if alias then
table.insert( frames, p.expandAlias( frameParts, alias ) )
else
table.insert( frames, frame )
end
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( '\\\\', '\' ):gsub( '\\&', '&' )
local formatPattern = '&[0-9a-fk-or]'
if plainTitle:match( formatPattern ) then
formattedTitle = title
plainTitle = plainTitle:gsub( formatPattern, '' )
end
end
args[1] = table.concat( frames, ';' )
if plainTitle == '' then
plainTitle = name
else
plainTitle = plainTitle:gsub( '\', '\\' ):gsub( '&', '&' )
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( '&', '&' )
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
end
local sprite
local frames
local ids = mw.loadData( [[Module:InvSprite/IDs]] ).ids
if args.parsed then
local modIds = {}
frames = args[1]
local animated = args[1]:find( ';' )
elseif args[1] ~= '' then
local pageName = mw.title.getCurrentTitle().text
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 imgClass = args.imgclass
local numStyle = args.numstyle
local numStyle = args.numstyle
Line 62: Line 243:
if args.style then
if args.style then
body:cssText( args.style )
body:cssText( args.style )
end
if ( args.default or '' ) ~= '' then
body:addClass( 'invslot-default-' .. string.lower( args.default ):gsub( ' ', '-' ) )
end
end
if ( args.default or '' ) ~= '' then
--mw.logObject( frames )
body:css( 'background-image', '{{FileUrl|' .. args.default .. '.png}}' )
if not frames then
return tostring( body )
end
end
local first = true
local activeFrame = frames.randomise == true and random( #frames ) or 1
for frame in mw.text.gsplit( args[1], '%s*;%s*' ) do
for i, frame in ipairs( frames ) do
local item
local item
if frame ~= '' or frame == '' and animated then
-- Table is a list, must contain subframes
item = body:tag( 'span' ):addClass( 'invslot-item' )
if frame[1] then
if imgClass then
item = body:tag( 'span' ):addClass( 'animated-subframe' )
item:addClass( imgClass )
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
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
end
local frame = p.makeFrame( frameText, modData and modData.default )
if frame == '' then
local newFrame = frame
( item or body ):tag( 'br' )
if aliases or modData.aliases then
else
local id = frame.name
local category
if frame.mod then
local parts = p.getParts( frame, args.mod )
id = frame.mod .. ':' .. id
local title = parts.title or mw.text.trim( args.title or '' )
end
local mod = parts.mod
local name = parts.name
local num = parts.num
local description = parts.text
local img, idData
local alias = modData and modData.aliases and modData.aliases[id] or
if mod then
aliases and aliases[id]
local modData = modIds[mod]
if alias then
if not modData and mw.title.new( 'Module:InvSprite/Mods/' .. mod .. '/IDs' ).exists then
newFrame = p.getAlias( alias, frame )
modData = mw.loadData( 'Module:InvSprite/Mods/' .. mod .. '/IDs' )
if aliasReference then
modIds[mod] = modData
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
if modData and modData[name] then
end
idData = modData[name]
end
else
img = name .. ' (' .. mod .. ')'
if subframe then
end
mergeList( subframes, newFrame )
elseif ids[name] then
-- Randomise starting frame for "Any *" aliases, as long as the alias is the only subframe
idData = ids[name]
if frames.randomise ~= 'never' and subframes.randomise == nil and
frame.name:match( '^' .. i18n.prefixes.any .. ' ' ) then
subframes.randomise = true
else
else
img = name
subframes.randomise = false
end
local link = args.link or ''
if link == '' then
if mod then
link = 'Mods/' .. mod .. '/' .. name
else
link = name
end
elseif link:lower() == 'none' then
link = nil
end
end
if link == pageName then
if frames.randomise ~= 'never' then
link = nil
frames.randomise = false
end
end
if subframe == 'last' then
local formattedTitle
-- No point having a subframe containing a single frame,
local plainTitle
-- or the subframe being the only frame
if title == '' then
if #subframes == 1 or #splitFrames == i and #frames == 0 then
plainTitle = name
mergeList( frames, subframes )
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
if img then
formattedTitle = ''
else
else
plainTitle = ''
table.insert( frames, subframes )
end
end
subframes = {}
subframe = nil
end
end
else
item:attr{
-- Randomise starting frame for "Any *" aliases, as long as the alias is the only frame
['data-minetip-title'] = formattedTitle,
if frames.randomise == nil and frame.name:match( '^' .. i18n.prefixes.any .. ' ' ) then
['data-minetip-text'] = description
frames.randomise = true
}
elseif frames.randomise ~= 'never' then
frames.randomise = false
if img then
-- & is re-escaped because mw.html treats attributes
-- as plain text, but MediaWiki doesn't
local escapedTitle = ( plainTitle or '' ):gsub( '&', '&#38;' )
item:addClass( 'invslot-item-image' )
:wikitext( '[[File:Grid ', img, '.png|32x32px|link=', link or '', '|', escapedTitle, ']]' )
else
if not sprite then
sprite = require( [[Module:Sprite2]] ).sprite
end
local image
if mod then
image = args.spritesheet or mod .. 'Sprite.png'
end
if link then
item:wikitext( '[[', link, '|' )
end
local image, spriteCat = sprite{
iddata = idData, title = plainTitle,
image = image, settings = 'InvSprite'
}
item:node( image )
category = spriteCat
end
if num and num > 1 and num < 1000 then
if img and link then
item:wikitext( '[[', link, '|' )
end
local number = item
:tag( 'span' )
:addClass( 'invslot-stacksize' )
:attr{ title = plainTitle }
:wikitext( num )
if numStyle then
number:cssText( numStyle )
end
if img and link then
item:wikitext( ']]' )
end
end
if idData and link then
item:wikitext( ']]' )
end
item:wikitext( category )
end
if first then
if animated and item then
item:addClass( 'active' )
end
end
first = false
mergeList( frames, newFrame )
end
end
end
end
return body
frames.aliasReference = expandedAliases
return frames
end
end


function p.expandAlias( frameParts, alias )
--[[Returns a new table with the parts of the parent frame
-- If the frame has no parts, we can just return the alias as-is
added to the alias
if not frameParts.title and not frameParts.mod and not frameParts.num and not frameParts.text then
--]]
return 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
end
local expandedFrames = {}
local expandedFrames = {}
for aliasFrame in mw.text.gsplit( alias, '%s*;%s*' ) do
for i, aliasFrame in ipairs( aliasFrames ) do
local aliasParts = p.getParts( aliasFrame )
local expandedFrame
aliasParts.title = frameParts.title or aliasParts.title or ''
if type( aliasFrame ) == 'string' then
aliasParts.mod = frameParts.mod or aliasParts.mod or 'Minecraft'
expandedFrame = { name = aliasFrame }
aliasParts.num = frameParts.num or aliasParts.num or ''
else
aliasParts.text = frameParts.text or aliasParts.text or ''
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
table.insert( expandedFrames, string.format(
expandedFrames[i] = expandedFrame
'[%s]%s:%s,%s[%s]',
aliasParts.title, aliasParts.mod, aliasParts.name, aliasParts.num, aliasParts.text
) )
end
end
return table.concat( expandedFrames, ';' )
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
end


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