Module:Recipe table
Jump to navigation
Jump to search
Documentation for this module may be created at Module:Recipe table/doc
-- Used to render Factory recipe tables
-- Inspired by Minecraft Wiki's [[Module:Recipe table]]
require("Module:No globals");
local checkType = require("libraryUtil").checkType;
local slot = require("Module:Factory slot");
local UI = require("Module:UI");
--- @type fun(value: any, default?: boolean): boolean|nil
local yn = require("Module:Yesno");
local p = {};
--- @generic I: string
--- @generic O: string
--- @param args table
--- @param inArgNames I[]
--- @param outArgNames O[]
--- @return { [I|O]: ItemStack[]|nil }
local function parseRecipeArgs(args, inArgNames, outArgNames)
-- Raw recipe arguments
--- @type table<string, { value: string, isOutput: boolean }>
local recipeArgs = {};
for _, name in ipairs(inArgNames) do
recipeArgs[name] = { value = args[name], isOutput = false };
end
for _, name in ipairs(outArgNames) do
recipeArgs[name] = { value = args[name], isOutput = true };
end
--- @type table<string, ItemStack[]>
local parsedStacks = {};
--- @type table<string, ItemStack[]>
local parsedArgs = {};
for name, arg in pairs(recipeArgs) do
local value = arg.value;
if (value) then
local stacks = parsedStacks[value];
if (not stacks) then
stacks = { slot.parseStack(value) };
-- Deduplicates stacks with identical serialization,
-- used to ensure identity when comparing item stacks by value
local serialized = slot.stringifyStack(stacks[1]);
if (serialized ~= value) then
if (parsedStacks[serialized]) then
stacks = parsedStacks[serialized];
else
parsedStacks[serialized] = stacks;
end
end
parsedStacks[value] = stacks;
end
parsedArgs[name] = stacks;
end
end
return parsedArgs;
end
--- Collects the parsed item stacks into sets of unique items;
--- e.g.: The recipe for Producer (Town) [T1]
--- `{ A1 = Screw [T1], B1 = Plate [T1], A2 = Plate [T1], B2 = Screw [T1] }`
--- would return `{ { Screw [T1] }, { Plate [T1] } }`
---
--- @generic K:string
--- @param argNames K[]
--- @param parsedArgs table<K, ItemStack[]|nil>
--- @return ItemStack[][]
local function collectItemSets(argNames, parsedArgs)
--- @type table<string, boolean>
local encountered = {};
--- @param items ItemStack[]
--- @param stack ItemStack
local function addNewItem(items, stack)
local name = stack.name;
if (not encountered[name]) then
encountered[name] = true;
table.insert(items, stack);
end
end
local itemSets = {}
for _, arg in ipairs(argNames) do
local stacks = parsedArgs[arg];
if (stacks) then
local items = {};
for _, stack in ipairs(stacks) do
addNewItem(items, stack);
end
if #items > 0 then
table.insert(itemSets, items);
end
end
end
return itemSets;
end
local function oxfordComma(list, conjunction)
if (#list < 3) then
return mw.text.listToText(list, nil, conjunction);
end
return mw.text.listToText(list, ", ", conjunction and ("," .. conjunction) or ", and ");
end
--- Lists item names
---
--- @param itemSets ItemStack[][]
--- @return string
local function listItemNames(itemSets)
local nameSets = {};
for _, itemSet in ipairs(itemSets) do
local nameSet = {};
for _, item in ipairs(itemSet) do
local name = item.name;
table.insert(nameSet, name);
end
table.insert(nameSets, oxfordComma(nameSet, " or "));
end
return table.concat(nameSets, ";<br/>");
end
--- @param root MwHtml
--- @param class? string
--- @param showName? boolean
--- @param multiRow? boolean
local function createTableHead(root, class, showName, multiRow)
local headRow = mw.html.create("tr");
if (showName) then
headRow:tag("th"):attr("scope", "col"):wikitext("Name");
end
local recipeCol = headRow:tag("th"):attr("scope", "col"):wikitext("Recipe");
if (multiRow) then
recipeCol:addClass("unsortable");
-- Create an unclosed table element
root:wikitext('<table class="wikitable collapsible sortable');
if (class) then
root:wikitext(' ', mw.text.encode(class));
end
root:wikitext('">');
else
root:addClass("wikitable collapsible")
:addClass(class);
end
root:node(headRow);
end
--- @class TableOptions
--- @field uiMethod string the method in [[Module:UI]] to call to build the recipe UI
--- @field inputArgs string[] the recipe input argument names
--- @field outputArgs string[] the recipe output argument names
--- @param args table
--- @param opts TableOptions
function p.table(args, opts)
checkType('"Module:Recipe table".table', 1, args, "table");
checkType('"Module:Recipe table".table', 2, opts, "table");
local multiRow = yn(args.continue);
local showName;
local showHead = yn(args.head);
local showFoot = yn(args.foot);
if (multiRow) then
-- continuing a large table
showHead = false;
showName = true;
elseif (showHead and not showFoot) then
-- multi-row table start
multiRow = true;
showName = true;
else
showHead = true;
showFoot = true;
showName = yn(args.showName);
end
local root = mw.html.create(not multiRow and "table" or nil);
if (showHead) then
createTableHead(root, args.class, showName, multiRow);
end
local inArgNames = opts.inputArgs;
local outArgNames = opts.outputArgs;
local parsedRecipeArgs = parseRecipeArgs(args, inArgNames, outArgNames);
local outItems = collectItemSets(outArgNames, parsedRecipeArgs);
local row = root:tag("tr");
if (showName) then
row
:tag("th"):attr("scope", "row")
:wikitext(args.name or listItemNames(outItems));
end
row:tag("td"):node(UI[opts.uiMethod](args));
if (multiRow and showFoot) then
root:wikitext("</table>");
end
return root;
end
return p;