Module:Recipe table
Script error: The function "docPage" does not exist.
This module creates the table for {{crafting}}
, {{brewing}}
, {{smelting}}
and {{looming}}
. It can only be invoked within other modules.
Usage
The entry point is the table
function. The first argument should be whatever arguments you want to pass through, the second argument should be a table of settings.
Setting | Use |
---|---|
type | What type of recipe, e.g.: 'Crafting'
|
ingredientArgs | A table of the args which contain the ingredients, e.g.: { 'Input' }
|
outputArgs | A table of the args which contain the outputs, e.g.: { 'Output' }
|
uiFunc | The function to call from Module:UI, e.g.: 'craftingTable'
|
The module returns the wikitext table as the first parameter, and a table of unique ingredients as the second output.
Dependencies
es:Módulo:Recipe table
fr:Module:Tableau de recette
ja:モジュール:Recipe table
pt:Módulo:Recipe table
ru:Модуль:Таблица рецептов
uk:Модуль:Таблиця рецептів
local m = {}
-- Internationalization
local i18n = {
headingDescription = 'Description',
headingIngredients = 'Ingredients',
headingName = 'Name',
headingRecipe = '[[$1]] recipe',
headingRecipeFallback = 'Recipe', -- if one table features multiple recipes
moduleSlot = [[Module:Inventory slot]],
moduleUi = [[Module:UI]],
separator = ' +',
setSeparator = ' or',
tableDescription = '$1 recipes',
tableDescriptionFallback = 'Recipes',
}
-- Global dependencies and constants
local slot = require( i18n.moduleSlot )
local prefixes = slot.i18n.prefixes
local curTitle = mw.title.getCurrentTitle()
-- Loops through the input and output args and parses them into a single table,
-- with alias reference data.
-- Identical slots reuse the same table, to allow them to be compared
-- like strings.
-- The first parameter is module arguments table, the second and third are names
-- of ingredient and output arguments respectively.
local function parseRecipeArgs( args, ingredientArgVals, outputArgs )
-- Raw arguments
local recipeArgs = {}
local isOutputArg = {}
for _, arg in pairs( ingredientArgVals ) do
recipeArgs[arg] = args[arg]
end
for _, arg in pairs( outputArgs ) do
recipeArgs[arg] = args[arg]
isOutputArg[arg] = 1
end
-- Argument parsing
local parsedFrameText = {}
local parsedRecipeArgs = {}
for arg, frameText in pairs( recipeArgs ) do
if frameText then -- is there any frame content?
local randomise
if isOutputArg[arg] then
-- Do not randomize output parameters
randomise = 'never'
end
-- Remember the frame sequence
-- TODO: Normalize (frame sequences with minute whitespace changes
-- but the same content are currently treated as different)
local frames = not randomise and parsedFrameText[frameText]
if not frames then
frames = slot.parseFrameText( frameText, randomise, true )
parsedFrameText[frameText] = frames
end
parsedRecipeArgs[arg] = frames
end
end
return parsedRecipeArgs
end
-- Creates a link (with mod name if specified) with any prefix moved outside
function m.prefixedLink( name, mod )
-- Search for and remove prefixes
local prefix = ''
for _, thisPrefix in pairs( prefixes ) do
if name:find( '^' .. thisPrefix .. ' ' ) then
-- Prefix found, strip it away
prefix = thisPrefix .. ' '
name = name:sub( #prefix + 1 )
-- Also remove unwaxed prefixes
-- TODO: Make it less hard-coded
if name:find( '^' .. prefixes.unwaxed .. ' ' ) then
prefix = prefix .. prefixes.unwaxed .. ' '
name = name:sub( #prefixes.unwaxed + 1 )
end
break
end
end
-- Legacy: Add the mod prefix, if specified
-- NB: When disable mod support, please uncomment the code instead of
-- deleting it. While EnMCW doesn’t cover mods, other wikis who adapt its
-- templates and modules may do.
local page = name
if mod then
page = (slot.i18n.modLink
:gsub( '%$1', mod )
:gsub( '%$2', name:gsub('^%l', string.upper) )
)
end
return table.concat{ prefix, '[[', page, '|', name, ']]' }
end
-- Creates sets of unique items from a set of slots, using the original alias
-- name if available. Each set of items are the frames of that slot. These sets
-- are used for automatically generating links in ingredient and output cells.
-- The first parameter is a list of argument names, the second is a table of
-- argument values processed with parseRecipeArgs().
-- If the third parameter is true, also returns unique items (including aliases
-- and their contents) to save in SMW, so that they can be queried for in
-- templates like Crafting usage.
function m.makeItemSets( argVals, parsedArgs, listUnique )
-- Add an item (arg 2) to the specific set (arg 1), as long as it was not
-- added before (whether to the current set or to a previous one). If we’re
-- dealing with an alias (arg 3 provides reference data), its original name
-- is added instead of the current frame (which will be the first frame in
-- that alias’ expansion).
local usedItems = {}
local function addItemToSet( items, frame, alias )
-- This function accepts the current item set, the current frame, and
-- the alias reference data if present.
if alias then
frame = alias.frame
end
-- Add the item, if not added already
local uniqName = ( frame.mod or '' ) .. ':' .. frame.name
if not usedItems[uniqName] then
usedItems[uniqName] = true
items[#items + 1] = frame
end
-- Return the number of frames for the processing loop to advance by.
-- For a group alias, return the number of frames it expands to.
-- This way, the loop can skip over the remaining frames.
return alias and alias.length or 1
end
-- Add item to the unique lists if asked
local uniqueItems, usedUniqueItems, addUniqueItems
if listUnique then
uniqueItems = {}
usedUniqueItems = {}
addUniqueItems = function(frames, frameStart, frameCount, alias)
-- This function takes advantage of group aliases already being
-- expanded in the parsed args list by this point. The first para-
-- meter is the frame table (or subframe container), the second is
-- the starting (sub)frame index, the third is the number of frames
-- to process (larger than one if dealing with a group alias), the
-- fourth is alias reference data.
if alias then
-- Add the original alias’ name to unique items. This way we can
-- query recipes by alias names.
local uniqAliasName = (alias.frame.mod or '') .. ":" .. alias.frame.name
if not usedUniqueItems[uniqAliasName] then
usedUniqueItems[uniqAliasName] = true
uniqueItems[#uniqueItems+1] = uniqAliasName
else
-- The alias’ expanded frames are assumed to be already added
return
end
end
-- Determine index for the last frame to add; the same as start if
-- dealing with a single frame
local frameEnd = frameStart
if frameCount > 1 then
frameEnd = frameStart + frameCount - 1
if frameEnd > #frames then
frameEnd = #frames
end
end
-- Add each frame to the unique items
for i = frameStart, frameEnd do
local curFrame = frames[i]
if curFrame.name then
local uniqName = (curFrame.mod or '') .. ':' .. curFrame.name
if not usedUniqueItems[uniqName] then
usedUniqueItems[uniqName] = true
uniqueItems[#uniqueItems+1] = uniqName
end
end
end
end
else
-- No-op if not asked for unique items
addUniqueItems = function() end
end
-- Process the parsed argument values
local itemSets = {}
local i = 1
for _, arg in ipairs( argVals ) do
local frames = parsedArgs[arg]
if frames then -- not empty
local items = {}
local frameNum = 1
while frameNum <= #frames do
-- A while loop is used instead of for so that we can skip alias
-- expansions
local frame = frames[frameNum]
if frame[1] then
-- The frame is a subframe container (curly braces), process
-- it like the entire frame sequence
local subframeNum = 1
while subframeNum <= #frame do
local subframe = frame[subframeNum]
if subframe.name ~= '' then
-- Add the current subframe to the item set (and
-- unique items, if asked). If it’s part of an alias
-- expansion, add the alias to the set instead and
-- skip over the other subframes from that alias;
-- addUniqueItems will add both the alias and its
-- expansion’s frames to the unique items list.
local alias = frame.aliasReference and frame.aliasReference[subframeNum]
local aliasLength = addItemToSet( items, subframe, alias )
addUniqueItems(frame, subframeNum, aliasLength, alias)
subframeNum = subframeNum + aliasLength
else
-- Empty frame
subframeNum = subframeNum + 1
end
end
frameNum = frameNum + 1
elseif frame.name ~= '' then
-- Add the current frame (or its parent alias)
local alias = frames.aliasReference and frames.aliasReference[frameNum]
local aliasLength = addItemToSet( items, frame, alias )
addUniqueItems(frames, frameNum, aliasLength, alias)
frameNum = frameNum + aliasLength
else
-- Empty frame
frameNum = frameNum + 1
end
end
if #items > 0 then
-- Add the current item set to the list
itemSets[i] = items
i = i + 1
end
end
end
return itemSets, uniqueItems
end
-- Creates links for the name/ingredients columns out of item sets, with the
-- appropriate separators, and optionally "Any" and "Matching" prefixes removed.
function m.makeItemLinks( itemSets, removePrefixes )
-- Iterate over item sets
local links = {}
for i, itemSet in ipairs( itemSets ) do
local linkSet = {}
for i2, item in ipairs( itemSet ) do
local name = item.name
if removePrefixes then
-- Remove prefixes and uppercase first letter
name = name
:gsub( '^' .. prefixes.any .. ' ', '' )
:gsub( '^' .. prefixes.matching .. ' ', '' )
:gsub( '^%l', string.upper )
end
-- Create the link(s)
-- “A or B” names are treated like two separate items in a set.
local disjunctionA, disjunctionB = name:match("(.-) or (.+)")
if disjunctionA then
linkSet[i2] = m.prefixedLink( disjunctionA, item.mod )
.. i18n.setSeparator
.. m.prefixedLink( disjunctionB, item.mod )
else
linkSet[i2] = m.prefixedLink( name, item.mod )
end
end
links[i] = table.concat( linkSet, i18n.setSeparator .. '<br>' )
end
return table.concat( links, i18n.separator .. '<br>' )
end
-- Creates the table header. Accepts the recipe’s settings, the recipe type
-- name’s override, the table’s CSS class, flags for controlling whether to show
-- name and description columns, and the multirow flag (controls whether the
-- table will be sortable).
function m.makeHeader(recipeSettings, recipeTypeOverride, class, showName, showDescription, multirow)
-- CSS class
class = class or ''
-- Recipe result’s name
local nameCell = ''
if showName then
nameCell = i18n.headingName .. '!!'
end
-- Recipe’s description
local descriptionCell = ''
if showDescription then
descriptionCell = '!!class="unsortable"|' .. i18n.headingDescription
end
-- If the table has multiple rows, make the table sortable except the
-- recipe cell. If present, the description cell is made unsortable above
local recipeAttribs = ''
if multirow then
class = 'sortable ' .. class
recipeAttribs = 'class="unsortable"|'
end
-- Recipe heading and table description
local headingRecipe = i18n.recipeHeadingFallback
local tableDescription = i18n.tableDescriptionFallback
if recipeTypeOverride ~= 'none' and recipeSettings.type then
headingRecipe = i18n.headingRecipe:gsub( '%$1', recipeTypeOverride or recipeSettings.type )
tableDescription = i18n.tableDescription:gsub( '%$1', recipeTypeOverride or recipeSettings.type )
-- If using an inflected language like Russian, you may want to use a
-- grammatically appropriate form of recipeSettings.type supplied by a
-- separate setting, like this:
--headingRecipe = i18n.headingRecipe:gsub( '%$1', recipeSettings.type .. '|' .. recipeSettings.ofType )
end
-- Header
local header = table.concat( {
' {| class="wikitable collapsible ' .. class .. '" data-description="' .. tableDescription .. '"',
'!' .. nameCell ..
i18n.headingIngredients .. '!!' ..
recipeAttribs .. headingRecipe ..
descriptionCell
}, '\n' )
return header
end
-- Create the contents for the name cell
function m.makeNameCell( name, itemSets )
return name or m.makeItemLinks( itemSets, true )
end
-- Create the contents for the ingredients cell
function m.makeIngredientsCell( ingredients, itemSets )
return ingredients or m.makeItemLinks( itemSets )
end
-- Create a database entry with recipe data for querying
-- Powered by Semantic MediaWiki
function m.saveToDatabase(args, ingredientItems, outputItems, settings)
if not settings.type then
error("Missing type in recipe settings (required for saving recipe data)")
end
-- Main data
local recipeData = {
-- Standard parameters
["description"] = mw.text.unstrip(args.description or ''), --Remove ref tags
["name"] = args.name,
["ingredients"] = args.ingredients,
["nocat"] = args.nocat,
}
-- Also save ingredient and output arguments, as well as a few others that
-- might be important for the specific recipe (like Crafting’s shapeless)
for _, arg in ipairs(settings.ingredientArgs) do
recipeData[arg] = args[arg]
end
for _, arg in ipairs(settings.outputArgs) do
recipeData[arg] = args[arg]
end
if settings.otherArgsToSave then
for _, arg in ipairs(settings.otherArgsToSave) do
recipeData[arg] = args[arg]
end
end
local recipeJson = mw.text.jsonEncode(recipeData)
-- The subobject
local smwSubobject = {
[settings.type .. " JSON"] = recipeJson,
[settings.type .. " ingredient"] = ingredientItems,
[settings.type .. " output"] = outputItems
}
-- Additional recipe-specific properties to save
if settings.extraProperties then
for k, v in pairs(settings.extraProperties) do
smwSubobject[settings.type .. " " .. k] = v
end
end
-- A unique name is needed for our subobject
-- It will be like CRAFTING_(first_output)_boomdeyadaboomdeyadaxd
local smwSubobjectName = table.concat({settings.type:upper(), outputItems[1], mw.hash.hashValue("md4",recipeJson)}, "_")
-- Save
mw.smw.subobject(smwSubobject, smwSubobjectName)
end
-- Main entry point: Creates the table with the relevant DPL vars to allow
-- multiple table rows from separate template calls. Also returns ingredient and
-- output item sets.
-- If set in the recipe settings, also saves the recipe data to database so it
-- can be queried using templates like Crafting usage.
-- Call this function from a different module — recipe settings can’t be passed
-- from a template via #invoke.
function m.table( args, settings )
-- Current MediaWiki frame
local f = mw.getCurrentFrame()
-- are we continuing a previous table?
local multirow = f:callParserFunction( '#dplvar', 'recipetable-multirow' )
if multirow == '' then
multirow = nil
end
-- Table head and foot parameters
local showHead = args.head
local showFoot = args.foot
if multirow then
-- ignore head if continuing a table
showHead = nil
elseif showHead and not showFoot then
-- table head
multirow = true
f:callParserFunction( '#dplvar:set', 'recipetable-multirow', '1' )
else
-- only one row
showHead = true
showFoot = true
end
-- Do we show product names and recipe descriptions for this table?
local showName = args.showname
local showDescription = args.showdescription
if multirow then
if showHead then
showName = args.showname or '1'
f:callParserFunction( '#dplvar:set', 'recipetable-name', showName, 'recipetable-description', showDescription )
else
showName = f:callParserFunction( '#dplvar', 'recipetable-name' )
showDescription = f:callParserFunction( '#dplvar', 'recipetable-description' )
end
end
if showName ~= '1' then
showName = nil
end
if showDescription == '' then
showDescription = nil
end
-- Components of the recipe table or its part
local out = {}
-- Table header
if showHead then
out[1] = m.makeHeader( settings, args.recipeType, args.class, showName, showDescription, multirow )
end
-- Argument processing
local ingredientArgVals = settings.ingredientArgs
local outputArgs = settings.outputArgs
-- Parse the arguments
local parsedRecipeArgs = args
if not args.parsed then
parsedRecipeArgs = parseRecipeArgs( args, ingredientArgVals, outputArgs )
end
-- Are we saving recipe data to SMW?
-- Must be explicitly enabled in the recipe settings, can be disabled by the
-- ignoreusage parameter, and is never done on non-article namespaces or
-- subpages (hopefully the latter condition will be temporary).
local saveToDatabase = settings.saveToDatabase and not (args["ignoreusage"] or curTitle.namespace ~= 0 or curTitle.isSubpage)
-- Row cells
local cells = {}
-- Cell with names for the recipe’s product(s) (optional)
local outputItemSets, uniqueOutputItems = m.makeItemSets( outputArgs, parsedRecipeArgs, saveToDatabase )
if showName then
cells[1] = '!' .. m.makeNameCell( args.name, outputItemSets )
end
-- Cell with ingredients for the recipe
local ingredientsItemSets, uniqueIngredientItems = m.makeItemSets( ingredientArgVals, parsedRecipeArgs, saveToDatabase )
cells[#cells + 1] = '|' .. m.makeIngredientsCell( args.ingredients, ingredientsItemSets )
-- Cell with the recipe itself
local funcUi = require( i18n.moduleUi )[settings.uiFunc]
cells[#cells + 1] = '|style="padding:1px;text-align:center"|' .. funcUi(args)
-- Cell with the recipe’s description (optional)
if showDescription then
cells[#cells + 1] = '|' .. ( args.description or '' )
end
-- Put all cells together into a row
out[#out + 1] = table.concat( cells, '\n' )
-- Table footer
out[#out + 1] = showFoot and '|}' or ''
if showFoot then
f:callParserFunction( '#dplvar:set',
'recipetable-multirow', '',
'recipetable-name', '',
'recipetable-description', ''
)
end
-- Save recipe data if asked to
if saveToDatabase then
m.saveToDatabase(args, uniqueIngredientItems, uniqueOutputItems, settings)
end
return table.concat( out, '\n|-\n' ), ingredientsItemSets, outputItemSets
end
return m