Difference between revisions of "Module:Recipe table"

From The Perfect Tower II
Jump to navigation Jump to search
(Create module for formatting Factory recipe tables, inspired by and partially based on Minecraft Wiki's “Module:Recipe table”)
 
(Fix missing table head)
 
Line 152: Line 152:
 
:addClass(class);
 
:addClass(class);
 
end
 
end
 +
 +
root:node(headRow);
 
end
 
end
  

Latest revision as of 17:35, 20 April 2026

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;