Jump to content

Module:Item Usage: Difference between revisions

From The Petit Planet Wiki
Created page with "local p = {} local lib = require('Module:Feature') local Parse = require('Module:Parser').getTemplateArgs local Rlib = require('Module:Recipe').param local Item = require('Module:Item')._main function p.main(frame) local args = require('Module:Arguments').getArgs(frame, { parentFirst = true, wrappers = {'Template:Item Usage'} }) local item = args[1] or mw.title.getCurrentTitle().rootText local count = 0 local craftType = args['type'] local out = mw.html.create..."
 
No edit summary
 
(3 intermediate revisions by 2 users not shown)
Line 1: Line 1:
local p = {}
local p = {}
local lib = require('Module:Feature')
local lib = require('Module:Feature')
local Parse = require('Module:Parser').getTemplateArgs
local Parse = require('Module:Parser').getTemplateArgs
local Rlib = require('Module:Recipe').param
local Rlib = require('Module:Recipe').param
local Item = require('Module:Item')._main
local Item = require('Module:Item')._main
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function isNotEmpty(v)
return lib.isNotEmpty(v)
end
local function normalizeText(s)
return mw.ustring.lower(mw.text.trim(tostring(s or '')))
end
local function getKitchenwareArg()
return Rlib.sKitchArg or 'kitchenware'
end
local function getSortDelimiter(sort)
return lib.ternary(
sort:find(Rlib.sSortDelim2),
Rlib.sSortDelim2,
Rlib.sSortDelim
)
end
local function isCookingRecipe(recipe)
return normalizeText(recipe[Rlib.sTypeArg]) == 'cooking'
end
local function getMaxCookingItemIndex(recipe)
local maxIndex = 0
for key, _ in pairs(recipe) do
local index = tostring(key):match('^item_(%d+)')
if index then
index = tonumber(index)
if index and index > maxIndex then
maxIndex = index
end
end
end
return maxIndex
end
--------------------------------------------------------------------------------
-- Usage detection
--------------------------------------------------------------------------------
local function standardRecipeUsesItem(recipe, item)
if not isNotEmpty(recipe['sort']) then
return false
end
if not isNotEmpty(recipe[item]) then
return false
end
for name in lib.gsplit(recipe['sort'], getSortDelimiter(recipe['sort'])) do
if isNotEmpty(name) and name == item then
return true
end
end
return false
end
local function cookingRecipeUsesItem(recipe, item)
local maxIndex = getMaxCookingItemIndex(recipe)
for index = 1, maxIndex do
local prefix = 'item_' .. index
-- Specific item:
-- |item_1 = Rice
if isNotEmpty(recipe[prefix]) and recipe[prefix] == item then
return true
end
-- Single type:
-- |item_1_type = Fish
if isNotEmpty(recipe[prefix .. '_type']) and recipe[prefix .. '_type'] == item then
return true
end
-- Multiple types:
-- |item_1_type_1 = Freshwater Fish
-- |item_1_type_2 = Saltwater Fish
local typeIndex = 1
while isNotEmpty(recipe[prefix .. '_type_' .. typeIndex]) do
if recipe[prefix .. '_type_' .. typeIndex] == item then
return true
end
typeIndex = typeIndex + 1
end
-- Multiple accepted specific items:
-- |item_1_option_1 = Rice
-- |item_1_option_2 = Wheat
local optionIndex = 1
while isNotEmpty(recipe[prefix .. '_option_' .. optionIndex]) do
if recipe[prefix .. '_option_' .. optionIndex] == item then
return true
end
optionIndex = optionIndex + 1
end
end
-- Kitchenware:
-- |kitchenware = Cooking Pot
local kitchenwareArg = getKitchenwareArg()
if isNotEmpty(recipe[kitchenwareArg]) and recipe[kitchenwareArg] == item then
return true
end
-- Multiple kitchenware:
-- |kitchenware_1 =
-- |kitchenware_2 =
local kitchenwareIndex = 1
while isNotEmpty(recipe[kitchenwareArg .. '_' .. kitchenwareIndex]) do
if recipe[kitchenwareArg .. '_' .. kitchenwareIndex] == item then
return true
end
kitchenwareIndex = kitchenwareIndex + 1
end
return false
end
local function recipeUsesItem(recipe, item)
if isCookingRecipe(recipe) then
return cookingRecipeUsesItem(recipe, item)
end
return standardRecipeUsesItem(recipe, item)
end
--------------------------------------------------------------------------------
-- Recipe display helpers
--------------------------------------------------------------------------------
local function renderItem(name, amount, quality)
local out = tostring(Item({
name,
size = 20,
x = amount
}))
if isNotEmpty(quality) then
out = out .. ' <small>(' .. quality .. ')</small>'
end
return out
end
local function renderIconLink(name, amount, quality)
local out = ''
if isNotEmpty(amount) then
out = tostring(amount) .. '× '
end
out = out .. '[[File:' .. name .. ' Icon.png|20x20px|link=' .. name .. ']] [[' .. name .. ']]'
if isNotEmpty(quality) then
out = out .. ' <small>(' .. quality .. ')</small>'
end
return out
end
local function renderStandardIngredients(recipe)
local ingredients = {}
if not isNotEmpty(recipe['sort']) then
return ingredients
end
for name in lib.gsplit(recipe['sort'], getSortDelimiter(recipe['sort'])) do
if isNotEmpty(name) and isNotEmpty(recipe[name]) then
table.insert(ingredients, Item({
name,
size = 20,
x = recipe[name]
}))
end
end
return ingredients
end
local function renderCookingIngredient(recipe, index)
local prefix = 'item_' .. index
local amount = recipe[prefix .. '_amount'] or '1'
local quality = recipe[prefix .. '_quality']
-- Specific item:
-- |item_1 = Rice
if isNotEmpty(recipe[prefix]) then
return renderItem(recipe[prefix], amount, quality)
end
-- Specific item options:
-- |item_1_option_1 = Rice
-- |item_1_option_2 = Wheat
local options = {}
local optionIndex = 1
while isNotEmpty(recipe[prefix .. '_option_' .. optionIndex]) do
table.insert(options, renderItem(
recipe[prefix .. '_option_' .. optionIndex],
amount,
quality
))
optionIndex = optionIndex + 1
end
if #options > 0 then
return table.concat(options, ' / ')
end
-- Type requirements:
-- |item_1_type = Fish
-- |item_1_type_1 = Freshwater Fish
-- |item_1_type_2 = Saltwater Fish
local types = {}
if isNotEmpty(recipe[prefix .. '_type']) then
table.insert(types, renderIconLink(
recipe[prefix .. '_type'],
amount,
quality
))
end
local typeIndex = 1
while isNotEmpty(recipe[prefix .. '_type_' .. typeIndex]) do
table.insert(types, renderIconLink(
recipe[prefix .. '_type_' .. typeIndex],
amount,
quality
))
typeIndex = typeIndex + 1
end
if #types > 0 then
return table.concat(types, ' / ')
end
return nil
end
local function renderCookingIngredients(recipe)
local ingredients = {}
local maxIndex = getMaxCookingItemIndex(recipe)
for index = 1, maxIndex do
local rendered = renderCookingIngredient(recipe, index)
if isNotEmpty(rendered) then
table.insert(ingredients, rendered)
end
end
return ingredients
end
local function renderCookingKitchenware(recipe)
local kitchenwareArg = getKitchenwareArg()
local kitchenware = {}
if isNotEmpty(recipe[kitchenwareArg]) then
table.insert(kitchenware, renderIconLink(recipe[kitchenwareArg]))
end
local index = 1
while isNotEmpty(recipe[kitchenwareArg .. '_' .. index]) do
table.insert(kitchenware, renderIconLink(recipe[kitchenwareArg .. '_' .. index]))
index = index + 1
end
return kitchenware
end
local function renderRecipeIngredients(recipe)
if isCookingRecipe(recipe) then
return renderCookingIngredients(recipe)
end
return renderStandardIngredients(recipe)
end
local function hasCookingRecipes(valids)
for _, recipes in pairs(valids) do
for _, recipe in pairs(recipes) do
if isCookingRecipe(recipe) then
return true
end
end
end
return false
end
--------------------------------------------------------------------------------
-- Main
--------------------------------------------------------------------------------


function p.main(frame)
function p.main(frame)
Line 10: Line 331:
wrappers = {'Template:Item Usage'}
wrappers = {'Template:Item Usage'}
})
})
local item = args[1] or mw.title.getCurrentTitle().rootText
local item = args[1] or mw.title.getCurrentTitle().rootText
local count = 0
local count = 0
local craftType = args['type']
local craftType = args['type']
local out = mw.html.create()
local out = mw.html.create()
if lib.isNotEmpty(frame.args.pages) and frame.args.pages:find('%$%$%$') then
 
if isNotEmpty(frame.args.pages) and frame.args.pages:find('%$%$%$') then
local valids = {}
local valids = {}
for page in lib.gsplit(frame.args.pages, '$$$') do
for page in lib.gsplit(frame.args.pages, '$$$') do
local data = Parse(page, { only='Recipe' } )
local data = Parse(page, { only = 'Recipe' })
if type(data[1]) ~= 'table' then data = {data} end
 
if type(data[1]) ~= 'table' then
data = { data }
end
 
for _, call in ipairs(data) do
for _, call in ipairs(data) do
if lib.isNotEmpty(call['sort']) and lib.isNotEmpty(call[item]) then
if recipeUsesItem(call, item) then
for name in lib.gsplit(call['sort'], lib.ternary(call['sort']:find(Rlib.sSortDelim2), Rlib.sSortDelim2, Rlib.sSortDelim)) do
valids[page] = valids[page] or {}
if (lib.isNotEmpty(name) and name == item) then
table.insert(valids[page], call)
valids[page] = valids[page] or {}
count = count + 1
table.insert(valids[page], call)
count = count + 1
end
end
end
end
end
end
end
end
 
if craftType == 'Furnishing Set' then
if craftType == 'Furnishing Set' then
out:wikitext('There ', lib.ternary(count > 1,'are', 'is'), ' <b>', tostring(count), '</b> [[Furnishing Set]]', lib.ternary(count > 1, 's', ''), ' that use', lib.ternary(count > 1, '', 's'), ' [[', item , ']] as a component:\n')
out:wikitext(
'There ',
lib.ternary(count > 1, 'are', 'is'),
' <b>',
tostring(count),
'</b> [[Furnishing Set]]',
lib.ternary(count > 1, 's', ''),
' that use',
lib.ternary(count > 1, '', 's'),
' [[',
item,
']] as a component:\n'
)
else
else
out:wikitext('There ', lib.ternary(count > 1,'are', 'is'), ' <b>', tostring(count), '</b> use', lib.ternary(count > 1, 's', ''), ' for [[', item , ']]:\n')
out:wikitext(
'There ',
lib.ternary(count > 1, 'are', 'is'),
' <b>',
tostring(count),
'</b> use',
lib.ternary(count > 1, 's', ''),
' for [[',
item,
']]:\n'
)
end
end
 
out:node(p._main(valids, craftType))
out:node(p._main(valids, craftType))
return out
return out
else
else
Line 46: Line 393:
out:wikitext('No recipes use [[', item, ']] as an ingredient.')
out:wikitext('No recipes use [[', item, ']] as an ingredient.')
end
end
return out
return out
end
end
Line 51: Line 399:


function p._main(valids, craftType)
function p._main(valids, craftType)
local _table = mw.html.create('table'):addClass('fandom-table article-table sortable')
local _table = mw.html.create('table'):addClass('wikitable article-table sortable')
local showKitchenware = hasCookingRecipes(valids)
-- header row
 
-- Header row
local tr = _table:tag('tr')
local tr = _table:tag('tr')
tr:tag('th'):wikitext('Item')
tr:tag('th'):wikitext('Item')
tr:tag('th'):wikitext('Craft Type')
tr:tag('th'):wikitext('Craft Type')
if showKitchenware then
tr:tag('th'):wikitext('Kitchenware')
end
tr:tag('th'):wikitext(craftType == 'Furnishing Set' and 'Components' or 'Recipe')
tr:tag('th'):wikitext(craftType == 'Furnishing Set' and 'Components' or 'Recipe')
-- mw.logObject(valids)
 
-- rest of table
for page, recipes in lib.spairs(valids) do
for page, recipes in lib.spairs(valids) do
local tr = _table:tag('tr')
local tr = _table:tag('tr')
 
for i, recipe in pairs(recipes) do
for i, recipe in pairs(recipes) do
if i == 1 then
if i == 1 then
local cell = tr:tag('td'):attr('rowspan', #valids[page])
local cell = tr:tag('td'):attr('rowspan', #valids[page])
if recipe[Rlib.sTypeArg]:find('Set') then
if recipe[Rlib.sTypeArg]:find('Set') then
cell
cell
:tag('div')
:tag('div')
:css{['text-align'] = 'center', ['width'] = '165px'}
:css{
:wikitext('[[File:Furnishing Set ', page, ' Display.png|160x80px|alt=', page, '|link=', page, ']]<br>[[', page, ']]')
['text-align'] = 'center',
['width'] = '165px'
}
:wikitext(
'[[File:Furnishing Set ',
page,
' Display.png|160x80px|alt=',
page,
'|link=',
page,
']]<br>[[',
page,
']]'
)
else
else
cell:wikitext(Item({ page, x = recipe[Rlib.sYieldArg] }))
cell:wikitext(Item({
page,
x = recipe[Rlib.sYieldArg]
}))
end
end
else
else
tr = _table:tag('tr')
tr = _table:tag('tr')
end
end
tr:tag('td'):css{['text-align'] = 'center'}:wikitext('[[', recipe[Rlib.sTypeArg], ']]')
 
local ingredients = {}
tr:tag('td')
for name in lib.gsplit(recipe['sort'], lib.ternary(recipe['sort']:find(Rlib.sSortDelim2), Rlib.sSortDelim2, Rlib.sSortDelim)) do
:css{['text-align'] = 'center'}
if (lib.isNotEmpty(name) and lib.isNotEmpty(recipe[name])) then
:wikitext('[[', recipe[Rlib.sTypeArg], ']]')
table.insert(ingredients, Item({ name, size = 20, x = recipe[name] }))
 
if showKitchenware then
if isCookingRecipe(recipe) then
local kitchenware = renderCookingKitchenware(recipe)
tr:tag('td'):wikitext(table.concat(kitchenware, '<br>'))
else
tr:tag('td'):wikitext('—')
end
end
end
end
local ingredients = renderRecipeIngredients(recipe)
tr:tag('td'):wikitext(table.concat(ingredients, '<br>'))
tr:tag('td'):wikitext(table.concat(ingredients, '<br>'))
end
end
end
end
-- mw.logObject(_table) --debug
 
return _table
return _table
end
end


return p
return p

Latest revision as of 19:29, 13 May 2026

Module documentation

This module implements {{Item Usage}}.

local p = {}

local lib = require('Module:Feature')
local Parse = require('Module:Parser').getTemplateArgs
local Rlib = require('Module:Recipe').param
local Item = require('Module:Item')._main

--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------

local function isNotEmpty(v)
	return lib.isNotEmpty(v)
end

local function normalizeText(s)
	return mw.ustring.lower(mw.text.trim(tostring(s or '')))
end

local function getKitchenwareArg()
	return Rlib.sKitchArg or 'kitchenware'
end

local function getSortDelimiter(sort)
	return lib.ternary(
		sort:find(Rlib.sSortDelim2),
		Rlib.sSortDelim2,
		Rlib.sSortDelim
	)
end

local function isCookingRecipe(recipe)
	return normalizeText(recipe[Rlib.sTypeArg]) == 'cooking'
end

local function getMaxCookingItemIndex(recipe)
	local maxIndex = 0

	for key, _ in pairs(recipe) do
		local index = tostring(key):match('^item_(%d+)')

		if index then
			index = tonumber(index)

			if index and index > maxIndex then
				maxIndex = index
			end
		end
	end

	return maxIndex
end

--------------------------------------------------------------------------------
-- Usage detection
--------------------------------------------------------------------------------

local function standardRecipeUsesItem(recipe, item)
	if not isNotEmpty(recipe['sort']) then
		return false
	end

	if not isNotEmpty(recipe[item]) then
		return false
	end

	for name in lib.gsplit(recipe['sort'], getSortDelimiter(recipe['sort'])) do
		if isNotEmpty(name) and name == item then
			return true
		end
	end

	return false
end

local function cookingRecipeUsesItem(recipe, item)
	local maxIndex = getMaxCookingItemIndex(recipe)

	for index = 1, maxIndex do
		local prefix = 'item_' .. index

		-- Specific item:
		-- |item_1 = Rice
		if isNotEmpty(recipe[prefix]) and recipe[prefix] == item then
			return true
		end

		-- Single type:
		-- |item_1_type = Fish
		if isNotEmpty(recipe[prefix .. '_type']) and recipe[prefix .. '_type'] == item then
			return true
		end

		-- Multiple types:
		-- |item_1_type_1 = Freshwater Fish
		-- |item_1_type_2 = Saltwater Fish
		local typeIndex = 1

		while isNotEmpty(recipe[prefix .. '_type_' .. typeIndex]) do
			if recipe[prefix .. '_type_' .. typeIndex] == item then
				return true
			end

			typeIndex = typeIndex + 1
		end

		-- Multiple accepted specific items:
		-- |item_1_option_1 = Rice
		-- |item_1_option_2 = Wheat
		local optionIndex = 1

		while isNotEmpty(recipe[prefix .. '_option_' .. optionIndex]) do
			if recipe[prefix .. '_option_' .. optionIndex] == item then
				return true
			end

			optionIndex = optionIndex + 1
		end
	end

	-- Kitchenware:
	-- |kitchenware = Cooking Pot
	local kitchenwareArg = getKitchenwareArg()

	if isNotEmpty(recipe[kitchenwareArg]) and recipe[kitchenwareArg] == item then
		return true
	end

	-- Multiple kitchenware:
	-- |kitchenware_1 =
	-- |kitchenware_2 =
	local kitchenwareIndex = 1

	while isNotEmpty(recipe[kitchenwareArg .. '_' .. kitchenwareIndex]) do
		if recipe[kitchenwareArg .. '_' .. kitchenwareIndex] == item then
			return true
		end

		kitchenwareIndex = kitchenwareIndex + 1
	end

	return false
end

local function recipeUsesItem(recipe, item)
	if isCookingRecipe(recipe) then
		return cookingRecipeUsesItem(recipe, item)
	end

	return standardRecipeUsesItem(recipe, item)
end

--------------------------------------------------------------------------------
-- Recipe display helpers
--------------------------------------------------------------------------------

local function renderItem(name, amount, quality)
	local out = tostring(Item({
		name,
		size = 20,
		x = amount
	}))

	if isNotEmpty(quality) then
		out = out .. ' <small>(' .. quality .. ')</small>'
	end

	return out
end

local function renderIconLink(name, amount, quality)
	local out = ''

	if isNotEmpty(amount) then
		out = tostring(amount) .. '× '
	end

	out = out .. '[[File:' .. name .. ' Icon.png|20x20px|link=' .. name .. ']] [[' .. name .. ']]'

	if isNotEmpty(quality) then
		out = out .. ' <small>(' .. quality .. ')</small>'
	end

	return out
end

local function renderStandardIngredients(recipe)
	local ingredients = {}

	if not isNotEmpty(recipe['sort']) then
		return ingredients
	end

	for name in lib.gsplit(recipe['sort'], getSortDelimiter(recipe['sort'])) do
		if isNotEmpty(name) and isNotEmpty(recipe[name]) then
			table.insert(ingredients, Item({
				name,
				size = 20,
				x = recipe[name]
			}))
		end
	end

	return ingredients
end

local function renderCookingIngredient(recipe, index)
	local prefix = 'item_' .. index
	local amount = recipe[prefix .. '_amount'] or '1'
	local quality = recipe[prefix .. '_quality']

	-- Specific item:
	-- |item_1 = Rice
	if isNotEmpty(recipe[prefix]) then
		return renderItem(recipe[prefix], amount, quality)
	end

	-- Specific item options:
	-- |item_1_option_1 = Rice
	-- |item_1_option_2 = Wheat
	local options = {}
	local optionIndex = 1

	while isNotEmpty(recipe[prefix .. '_option_' .. optionIndex]) do
		table.insert(options, renderItem(
			recipe[prefix .. '_option_' .. optionIndex],
			amount,
			quality
		))

		optionIndex = optionIndex + 1
	end

	if #options > 0 then
		return table.concat(options, ' / ')
	end

	-- Type requirements:
	-- |item_1_type = Fish
	-- |item_1_type_1 = Freshwater Fish
	-- |item_1_type_2 = Saltwater Fish
	local types = {}

	if isNotEmpty(recipe[prefix .. '_type']) then
		table.insert(types, renderIconLink(
			recipe[prefix .. '_type'],
			amount,
			quality
		))
	end

	local typeIndex = 1

	while isNotEmpty(recipe[prefix .. '_type_' .. typeIndex]) do
		table.insert(types, renderIconLink(
			recipe[prefix .. '_type_' .. typeIndex],
			amount,
			quality
		))

		typeIndex = typeIndex + 1
	end

	if #types > 0 then
		return table.concat(types, ' / ')
	end

	return nil
end

local function renderCookingIngredients(recipe)
	local ingredients = {}
	local maxIndex = getMaxCookingItemIndex(recipe)

	for index = 1, maxIndex do
		local rendered = renderCookingIngredient(recipe, index)

		if isNotEmpty(rendered) then
			table.insert(ingredients, rendered)
		end
	end

	return ingredients
end

local function renderCookingKitchenware(recipe)
	local kitchenwareArg = getKitchenwareArg()
	local kitchenware = {}

	if isNotEmpty(recipe[kitchenwareArg]) then
		table.insert(kitchenware, renderIconLink(recipe[kitchenwareArg]))
	end

	local index = 1

	while isNotEmpty(recipe[kitchenwareArg .. '_' .. index]) do
		table.insert(kitchenware, renderIconLink(recipe[kitchenwareArg .. '_' .. index]))
		index = index + 1
	end

	return kitchenware
end

local function renderRecipeIngredients(recipe)
	if isCookingRecipe(recipe) then
		return renderCookingIngredients(recipe)
	end

	return renderStandardIngredients(recipe)
end

local function hasCookingRecipes(valids)
	for _, recipes in pairs(valids) do
		for _, recipe in pairs(recipes) do
			if isCookingRecipe(recipe) then
				return true
			end
		end
	end

	return false
end

--------------------------------------------------------------------------------
-- Main
--------------------------------------------------------------------------------

function p.main(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		parentFirst = true,
		wrappers = {'Template:Item Usage'}
	})

	local item = args[1] or mw.title.getCurrentTitle().rootText
	local count = 0
	local craftType = args['type']
	local out = mw.html.create()

	if isNotEmpty(frame.args.pages) and frame.args.pages:find('%$%$%$') then
		local valids = {}

		for page in lib.gsplit(frame.args.pages, '$$$') do
			local data = Parse(page, { only = 'Recipe' })

			if type(data[1]) ~= 'table' then
				data = { data }
			end

			for _, call in ipairs(data) do
				if recipeUsesItem(call, item) then
					valids[page] = valids[page] or {}
					table.insert(valids[page], call)
					count = count + 1
				end
			end
		end

		if craftType == 'Furnishing Set' then
			out:wikitext(
				'There ',
				lib.ternary(count > 1, 'are', 'is'),
				' <b>',
				tostring(count),
				'</b> [[Furnishing Set]]',
				lib.ternary(count > 1, 's', ''),
				' that use',
				lib.ternary(count > 1, '', 's'),
				' [[',
				item,
				']] as a component:\n'
			)
		else
			out:wikitext(
				'There ',
				lib.ternary(count > 1, 'are', 'is'),
				' <b>',
				tostring(count),
				'</b> use',
				lib.ternary(count > 1, 's', ''),
				' for [[',
				item,
				']]:\n'
			)
		end

		out:node(p._main(valids, craftType))

		return out
	else
		if craftType == 'Furnishing Set' then
			out:wikitext('No [[Furnishing Set]]s use [[', item, ']] as a component.')
		else
			out:wikitext('No recipes use [[', item, ']] as an ingredient.')
		end

		return out
	end
end

function p._main(valids, craftType)
	local _table = mw.html.create('table'):addClass('wikitable article-table sortable')
	local showKitchenware = hasCookingRecipes(valids)

	-- Header row
	local tr = _table:tag('tr')
	tr:tag('th'):wikitext('Item')
	tr:tag('th'):wikitext('Craft Type')

	if showKitchenware then
		tr:tag('th'):wikitext('Kitchenware')
	end

	tr:tag('th'):wikitext(craftType == 'Furnishing Set' and 'Components' or 'Recipe')

	for page, recipes in lib.spairs(valids) do
		local tr = _table:tag('tr')

		for i, recipe in pairs(recipes) do
			if i == 1 then
				local cell = tr:tag('td'):attr('rowspan', #valids[page])

				if recipe[Rlib.sTypeArg]:find('Set') then
					cell
						:tag('div')
						:css{
							['text-align'] = 'center',
							['width'] = '165px'
						}
						:wikitext(
							'[[File:Furnishing Set ',
							page,
							' Display.png|160x80px|alt=',
							page,
							'|link=',
							page,
							']]<br>[[',
							page,
							']]'
						)
				else
					cell:wikitext(Item({
						page,
						x = recipe[Rlib.sYieldArg]
					}))
				end
			else
				tr = _table:tag('tr')
			end

			tr:tag('td')
				:css{['text-align'] = 'center'}
				:wikitext('[[', recipe[Rlib.sTypeArg], ']]')

			if showKitchenware then
				if isCookingRecipe(recipe) then
					local kitchenware = renderCookingKitchenware(recipe)
					tr:tag('td'):wikitext(table.concat(kitchenware, '<br>'))
				else
					tr:tag('td'):wikitext('—')
				end
			end

			local ingredients = renderRecipeIngredients(recipe)

			tr:tag('td'):wikitext(table.concat(ingredients, '<br>'))
		end
	end

	return _table
end

return p