Module:Recipe: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
No edit summary |
||
(One intermediate revision by the same user not shown) | |||
Line 465: | Line 465: | ||
local membersTemplate = edit | local membersTemplate = edit | ||
if members == 'Yes' then | if members == 'Yes' then | ||
membersTemplate = " | membersTemplate = "-" | ||
elseif members == 'No' then | elseif members == 'No' then | ||
membersTemplate = "126" | membersTemplate = "126" | ||
Line 631: | Line 631: | ||
end | end | ||
return table.concat(cats,'') | return table.concat(cats,'') |
Latest revision as of 09:45, 27 August 2024
Documentation for this module may be created at Module:Recipe/doc
--<nowiki>
local p = {}
-- convert some used globals to locals to improve performance
local math = math
local string = string
local table = table
local mw = mw
local expr = mw.ext.ParserFunctions.expr
local coins = require('Module:Coins')._amount
local yesno = require('Module:Yesno')
local params = require('Module:Paramtest')
local commas = require('Module:Addcommas')
local geprice = require('Module:Exchange')._price
local skillpic = require('Module:SCP')._main
local editbutton = require('Module:Edit button')
local onmain = require('Module:Mainonly').on_main
local currencies = require('Module:Currencies')._amount
local edit = editbutton('? (edit)')
-- Tools that need special handling
local toolsList = {
['Axe'] = '[[File:Bronze axe.png|link=Axe]]',
['Watering can'] = '[[File:Watering can(8).png|link=Watering can]]',
['No'] = 'No',
}
local facilitiesIcons = {
['Skull'] = 'No',
['Anvil'] = '[[File:Anvil icon.png|link=Anvil]]',
['Apothecary'] = '[[File:Apothecary icon.png|link=Apothecary]]',
['Banner easel'] = '[[File:Banner easel icon.png|link=Banner easel]]',
['Barbarian anvil'] = '[[File:Anvil icon.png|link=Anvil]]',
['Bench with vice'] = '[[File:Bench with vice icon.png|link=Bench with vice]]',
['Bench with lathe'] = '[[File:Bench with lathe icon.png|link=Bench with lathe]]',
['Blast Furnace'] = '[[File:Furnace icon.png|link=Blast Furnace]]',
['Blast furnace'] = '[[File:Furnace icon.png|link=Blast Furnace]]',
['Big Compost Bin'] = '[[File:Farming patch icon.png|link=Big Compost Bin]]',
['Brewery'] = '[[File:Brewery icon.png|link=Brewery]]',
['Clay oven'] = '[[File:Cooking range icon.png|link=Clay oven]]',
['Compost Bin'] = '[[File:Farming patch icon.png|link=Compost Bin]]',
['Cooking range'] = '[[File:Cooking range icon.png|link=Cooking range]]',
['Cooking range (2018 Easter event)'] = '[[File:Cooking range icon.png|link=Cooking range (2018 Easter event)]]',
['Crafting table 1'] = '[[File:Crafting table 1 icon.png|link=Crafting table 1]]',
['Crafting table 2'] = '[[File:Crafting table 2 icon.png|link=Crafting table 2]]',
['Crafting table 3'] = '[[File:Crafting table 3 icon.png|link=Crafting table 3]]',
['Crafting table 4'] = '[[File:Crafting table 4 icon.png|link=Crafting table 4]]',
['Dairy churn'] = '[[File:Dairy churn icon.png|link=Dairy churn]]',
['Dairy cow'] = '[[File:Dairy cow icon.png|link=Dairy cow]]',
['Demon lectern'] = '[[File:Demon lectern icon.png|link=Demon lectern]]',
['Eagle lectern'] = '[[File:Eagle lectern icon.png|link=Eagle lectern]]',
['Eodan'] = '[[File:Tannery icon.png|link=Tannery]]',
['Fancy Clothes Store'] = '[[File:Clothes shop icon.png|link=Fancy Clothes Store]]',
['Farming patch'] = '[[File:Farming patch icon.png|link=Farming/Patch_locations]]',
['Furnace'] = '[[File:Furnace icon.png|link=Furnace]]',
['Furnace (Elemental Workshop)'] = '[[File:Furnace icon.png|link=Furnace (Elemental Workshop)]]',
['Loom'] = '[[File:Loom icon.png|link=Loom]]',
['Lovakite furnace'] = '[[File:Furnace icon.png|link=Lovakite furnace]]',
['Mahogany demon lectern'] = '[[File:Mahogany demon lectern icon.png|link=Mahogany demon lectern]]',
['Mahogany eagle lectern'] = '[[File:Mahogany eagle lectern icon.png|link=Mahogany eagle lectern]]',
['Metal Press'] = '[[File:Furnace icon.png|link=Metal Press]]',
['Oak lectern'] = '[[File:Oak lectern icon.png|link=Oak lectern]]',
['Oak workbench'] = '[[File:Oak workbench icon.png|link=Oak workbench]]',
['Pluming stand'] = '[[File:Pluming stand icon.png|link=Pluming stand]]',
['Potter\'s Wheel'] = '[[File:Pottery wheel icon.png|link=Potter\'s Wheel]]',
['Pottery Oven'] = '[[File:Pottery wheel icon.png|link=Pottery Oven]]',
['Sawmill'] = '[[File:Sawmill icon.png|link=Sawmill]]',
['Sand pit'] = '[[File:Sandpit icon.png|link=Sand pit]]',
['Sbott'] = '[[File:Tannery icon.png|link=Tannery]]',
['Shield easel'] = '[[File:Shield easel icon.png|link=Shield easel]]',
['Singing bowl'] = '[[File:Singing bowl icon.png|link=Singing bowl]]',
['Spinning wheel'] = '[[File:Spinning wheel icon.png|link=Spinning wheel]]',
['Steel framed workbench'] = '[[File:Steel framed workbench icon.png|link=Steel framed workbench]]',
['Tannery'] = '[[File:Tannery icon.png|link=Tannery]]',
['Taxidermist'] = '[[File:Taxidermist icon.png|link=Taxidermist]]',
['Teak demon lectern'] = '[[File:Teak demon lectern icon.png|link=Teak demon lectern]]',
['Teak eagle lectern'] = '[[File:Teak eagle lectern icon.png|link=Teak eagle lectern]]',
['Thakkrad Sigmundson'] = '[[File:Tannery icon.png|link=Thakkrad Sigmundson]]',
['Water'] = '[[File:Water source icon.png|link=Water]]',
['Whetstone'] = '[[File:Whetstone icon.png|link=Whetstone]]',
['Windmill'] = '[[File:Windmill icon.png|link=Windmill]]',
['Woodcutting stump'] = '[[File:Woodcutting stump icon.png|link=Woodcutting stump]]',
['Wooden workbench'] = '[[File:Wooden workbench icon.png|link=Wooden workbench]]',
['Workbench (Guardians of the Rift)'] = '[[File:Workbench (Guardians of the Rift).png|30px|link=Workbench (Guardians of the Rift)]]',
}
function p.main(frame)
local args = frame:getParent().args
local function cost_to_number(cost_v, name, currencyName)
if currencyName ~= nil then
if cost_v == nil then
return 1
elseif tonumber(commas._strip(cost_v),10) then
return tonumber(commas._strip(cost_v),10)
elseif tonumber(expr(cost_v),10) then
return expr(cost_v)
end
elseif cost_v == nil then
if pcall(function () geprice(name) end) then
return geprice(name)
else
return 0
end
elseif string.lower(cost_v) == 'no' then
return 0
elseif tonumber(commas._strip(cost_v),10) then
return tonumber(commas._strip(cost_v),10)
elseif tonumber(expr(cost_v),10) then
return expr(cost_v)
end
return 0
end
local function mat_list(objType)
local ret_list = {}
for i=1,11,1 do
local mat = args[objType..i]
if mat and params.has_content(mat) then
local name = mat
local txt = params.default_to(args[objType..i..'txt'], nil)
local qty = params.default_to(args[objType..i..'quantity'],'1')
local img = params.default_to(args[objType..i..'pic'], name)..'.png'
local cost_v = args[objType..i..'cost']
local currencyName = params.default_to(args[objType..i..'currency'], nil)
local itemnote = args[objType..i..'itemnote'] or nil
local qtynote = args[objType..i..'quantitynote'] or nil
local subtxt = args[objType..i..'subtxt'] or nil
table.insert(ret_list, {
name = name,
txt = txt,
cost = cost_to_number(cost_v, name, currencyName),
quantity = qty,
image = string.format('[[File:%s|link=]]', img, mat),
currency_name = currencyName,
outputnote = itemnote,
quantitynote = qtynote,
subtxt = subtxt
} )
end
end
return ret_list
end
local function skill_list()
local ret_list = {}
for i=1,10,1 do
local skill = args['skill'..i]
if skill and params.has_content(skill) then
local name = skill
local lvl = params.default_to(args['skill'..i..'lvl'],'?')
local boost = params.default_to(args['skill'..i..'boostable'],'')
local exp = commas._strip(params.default_to(args['skill'..i..'exp'],'?'))
table.insert(ret_list, {
name = name,
level = lvl,
boostable = boost,
experience = exp,
} )
end
end
return ret_list
end
local output = mat_list('output')
local materials = mat_list('mat')
local skills = skill_list()
local members = ''
if params.has_content(args.members) then
members = yesno(args.members, true)
if members then
members = 'Yes'
else
members = 'No'
end
end
local useSmw = true
if params.has_content(args.smw) then
useSmw = args.smw:lower() ~= 'no'
end
return p._main(frame, args, args.tools, skills, members, args.notes, materials, output, args.facilities, args.ticks, args.ticksnote, useSmw)
end
--
-- Generates an array of table rows based on skills required for the recipe.
--
-- @param skills {array} List of skill requirements generated by skill_list function.
-- @return {array} List of html tr elements for each skill requirement. Empty table if skills is an empty table or nil.
-- @return {bool} True if any skill requirement, other than level 1, is missing a boostable value in skills. False otherwise.
--
local function generate_skills_rows(skills)
local requirements = {}
local unknown_boostable_flag = false
-- Should this use yesno instead of lower comparison?
for i, v in ipairs(skills) do
local levelText = v.level == '?' and edit or v.level
-- Determine which boostable flag to add
if string.lower(v.boostable) == 'yes' then
levelText = levelText .. ' <sup title="This requirement is boostable" style="cursor:help; text-decoration: underline dotted;">(b)</sup>'
elseif string.lower(v.boostable) == 'no' then
levelText = levelText .. ' <sup title="This requirement is not boostable" style="cursor:help; text-decoration: underline dotted;">(nb)</sup>'
elseif (tonumber(v.level) or 0) > 1 then
levelText = levelText .. ' <sup title="Unknown whether this requirement is boostable" style="cursor:help; text-decoration: underline dotted;">?</sup>'
unknown_boostable_flag = true
end
local xp = v.experience == '?' and edit or v.experience
requirement = mw.html.create('tr')
requirement
:tag('td'):attr('colspan', 2):wikitext(skillpic(v.name, nil, true)):done()
:tag('td'):wikitext(levelText):done()
:tag('td'):wikitext(tonumber(xp) ~= nil and commas._add(xp) or xp):done()
table.insert(requirements, requirement)
end
return requirements, unknown_boostable_flag
end
--
-- Generates a td element for the ticks required.
--
-- @param frame {table} Frame passed in to main.
-- @param ticks {string|number} One of {nil, '', 'NA', 'Varies', [0-9]+} (case agnostic). Other strings will result in an error.
-- @param ticks_note {string} Custom note to be inserted into tick cell if ticks are varied or have a number value.
-- @return {html td element} A td element holding the number of ticks required, along with a note if applicable.
-- @return {bool} True if ticks_note was added to the td element. False otherwise.
--
local function generate_ticks_cell(frame, ticks, ticks_note)
local has_ref_tag = false
local ticks_cell = mw.html.create('td')
local note = ''
-- Prepare a note if ticks_note is given
if ticks_note ~= nil then
note = frame:extensionTag{ name='ref', content = ticks_note, args = { group='r' } }
end
-- Add the ticks value to the cell
ticks_cell:wikitext(ticks .. note):done()
return ticks_cell, has_ref_tag
end
--
-- Generates a tr element for the item described by item_data.
--
-- @param frame {table} Frame passed in to main.
-- @param item_data {table} A table representing a single item required or output by the recipe. Single member of a table produced by mat_list.
-- @return {html tr element} A tr element holding all information contained in item_data.
-- @return {int} Total cost of the given amount of this item.
-- @return {bool} True if either an item note or a quantity note was added to the tr element. False otherwise.
--
local function make_row(frame, item_data)
local classOverride
local textAlign = 'right'
local has_ref_tag = false
if item_data.currency_name ~= nil then
mat_ttl = currencies(item_data.quantity * item_data.cost, item_data.currency_name)
elseif item_data.cost == 0 then
mat_ttl = 'N/A'
classOverride = 'table-na'
textAlign = 'center'
else
mat_ttl = coins(item_data.quantity * item_data.cost)
end
local name = item_data.txt and string.format('%s|%s', item_data.name, item_data.txt) or string.format('%s', item_data.name)
local itemnote = item_data.outputnote and frame:extensionTag{ name = 'ref', content = item_data.outputnote, args = { group = 'r' } } or ''
if (itemnote ~= '') then has_ref_tag = true end
local quantitynote = item_data.quantitynote and frame:extensionTag{ name = 'ref', content = item_data.quantitynote, args = { group = 'r' } } or ''
if (quantitynote ~= '') then has_ref_tag = true end
return mw.html.create('tr')
:tag('td'):wikitext(item_data.image):done()
:tag('td'):wikitext(name .. itemnote):done()
:tag('td'):wikitext(commas._add(item_data.quantity) .. quantitynote):done()
:tag('td'):addClass(classOverride):css({ ['text-align'] = textAlign }):wikitext(mat_ttl):done(),
item_data.quantity * item_data.cost,
has_ref_tag
end
--
-- Generates a list of tr elements describing all the items given.
--
-- @param frame {table} Frame passed in to main.
-- @param items {array} A list containing a all items required or output by the recipe. Produced by mat_list.
-- @return {array} A list of tr elements, holding one row for each item in items.
-- @return {table} A table of total prices for each currency used by members of items.
-- @return {bool} True if either an item note or a quantity note was added to any tr element. False otherwise.
--
local function generate_rows(frame, items)
local currency_costs = {
['Coins'] = 0
}
local has_ref_tag = false
local rows = {}
for i, v in ipairs(items) do
local row, row_cost, has_row_note = make_row(frame, v)
if row_cost ~= 0 then
if v.currency_name ~= nil then
currency_costs[v.currency_name] = (currency_costs[v.currency_name] and currency_costs[v.currency_name] or 0) + v.quantity * v.cost
else
currency_costs['Coins'] = currency_costs['Coins'] + v.quantity * v.cost
end
end
has_ref_tag = has_ref_tag or has_row_note
table.insert(rows, row)
end
return rows, currency_costs, has_ref_tag
end
--
-- Generates a tr element containing all of the total costs in the currencies for items in item_costs.
--
-- @param items {array} A list containing a all items required or output by the recipe. Produced by mat_list.
-- @param item_costs {table} A table of total prices for each currency used by members of items. Produced by generate_rows.
-- @return {html tr element} A tr element holding all costs found in item_costs.
--
function generate_total_cost_row(items, item_costs)
local total_cost_row = mw.html.create('tr')
if #items == 0 then
total_cost_row:tag('td'):attr('colspan','5')
:css({ ['font-style'] = 'italic', ['text-align'] = 'center' }):wikitext('Materials unlisted '..editbutton()):done()
else
local total_cost_breakdown = ''
for i, v in next, item_costs, nil do
total_cost_breakdown = (string.len(total_cost_breakdown) == 0 and total_cost_breakdown or total_cost_breakdown .. '<br />') .. (i == 'Coins' and coins(v) or currencies(v, i))
end
total_cost_row:tag('th'):attr('colspan', 3):css({['text-align'] = 'right'}):wikitext('Total cost'):done()
:tag('td'):css({['text-align'] = 'right'}):wikitext(total_cost_breakdown)
end
return total_cost_row
end
--
-- Generates a tr element containing the difference between output and material costs (in coins).
--
-- @param frame {table} Frame passed in to main.
-- @param ticks {string|number} The number of ticks required to create one output. String or nil values will result in an assumption of 5.
-- @param materials_coins_cost {number} The total cost in coins of the materials.
-- @param outputs_coins_cost {number} The total cost in coins of the outputs.
-- @return {html tr element} A tr element holding the profit from one conversion of materials into outputs.
-- @return {bool} True if a note was added to the profit indicating questionable profits. False otherwise.
--
function generate_profit_row(frame, ticks, materials_coins_cost, outputs_coins_cost)
local profit = outputs_coins_cost - materials_coins_cost
local note = ''
local has_ref_tag = false
-- Find ticks per action. Assume 5 if nothing else is given.
-- If it takes 0 ticks, set to 1/8 since 8 actions can be performed per tick.
local ticks_per_action = tonumber(ticks) or 5
if ticks_per_action == 0 then
ticks_per_action = 1/8
end
-- Create and populate the table row element
local profit_row = mw.html.create('tr')
profit_row
:tag('th'):attr('colspan', 3):css({['text-align'] = 'right'}):wikitext('Profit'):done()
:tag('td'):css({['text-align'] = 'right'}):wikitext(coins(profit) .. note):done()
return profit_row, has_ref_tag
end
--
-- Main
--
function p._main(frame, args, tools, skills, members, notes, materials, output, facilities, ticks, ticks_note, useSmw)
local function toolImages(t)
local images = {}
if params.is_empty(t) then
return 'None'
end
local spl = mw.text.split(t, ",")
for _, image_i in ipairs(spl) do
image_i = mw.text.trim(image_i)
if toolsList[image_i] then
table.insert(images, toolsList[image_i])
else
table.insert(images, string.format("[[File:%s.png|link=]]", image_i, image_i))
end
end
return table.concat(images)
end
local function facilityLinks(f)
local links = {}
if params.is_empty(f) then
return 'None'
end
local spl = mw.text.split(f, ",")
for _, link_i in ipairs(spl) do
if facilitiesIcons[link_i] ~= nil then
table.insert(links, string.format("%s [[%s]]", facilitiesIcons[link_i], link_i))
else
table.insert(links, string.format("[[%s]]", link_i))
end
end
return table.concat(links, "<br />")
end
--------------------------------------------------------------------------------
-- START OF REQUIREMENTS TABLE
-- This table contains skill reqs and xp, quest reqs, members req, and ticks
local requirementsTable = mw.html.create('table')
:addClass('wikitable align-center-2 align-right-3')
:css({ width = '100%',
['margin-bottom'] = '0' })
requirementsTable:tag('caption'):wikitext("Requirements"):done()
-- Skills
local unknown_boostable_flag = false
if #skills ~= 0 then
local skill_requirements = mw.html.create('tr')
skill_requirements
:tag('th'):attr('colspan', 2):wikitext('Skill'):done()
:tag('th'):wikitext('Level'):done()
:tag('th'):wikitext('XP'):done()
skill_requirement_rows, unknown_boostable_flag = generate_skills_rows(skills)
for _, row in ipairs(skill_requirement_rows) do
skill_requirements:node(row)
end
requirementsTable:node(skill_requirements)
end
-- Notes
if notes ~= nil then
requirementsTable:tag('tr')
:tag('td'):attr('colspan', 4):wikitext(notes):done()
end
-- Members and Ticks row
local members_and_ticks_row = mw.html.create('tr')
-- Should this use yesno?
local membersTemplate = edit
if members == 'Yes' then
membersTemplate = "-"
elseif members == 'No' then
membersTemplate = "126"
end
members_and_ticks_row
:tag('th'):wikitext('Combat level'):done()
:tag('td'):wikitext(membersTemplate):done()
:tag('th'):attr('title', 'Ticks per action'):wikitext('Location'):done()
local ticks_cell, has_ticks_ref_tag = generate_ticks_cell(frame, ticks, ticks_note)
members_and_ticks_row:node(ticks_cell)
requirementsTable:node(members_and_ticks_row)
--Tools and Facilities row
if params.has_content(tools) or params.has_content(facilities) then
local toolImgs = toolImages(tools)
local facilityLnks = facilityLinks(facilities)
requirementsTable:tag('tr')
:tag('th'):wikitext('Skull'):done()
:tag('td'):css({ ['text-align'] = 'center' }):wikitext(toolImgs):done()
:tag('th'):wikitext('Facilities'):done()
:tag('td'):css({ ['text-align'] = 'center' }):wikitext(facilityLnks):done()
end
-- END OF REQUIREMENTS TABLE
----------------------------------------------------------------------------
----------------------------------------------------------------------------
-- START OF MATERIALS AND PRODUCTS TABLE
-- Contains materials (item, qty, cost), total cost, products, and profit
-- All rows to be in the materials and products table should be appended to materialsTable
local materialsTable = mw.html.create('table')
:addClass('wikitable align-center-1 align-right-3 align-right-4')
:css({ width = '100%',
['margin-top'] = '0' })
materialsTable:tag('caption'):wikitext("Materials"):done()
-- Table header
materialsTable:tag('tr')
:tag('th'):attr('colspan', 2):wikitext('Item'):done()
:tag('th'):wikitext('Quantity'):done()
:tag('th'):wikitext('Cost'):done()
-- Materials
material_rows, material_costs, has_material_ref_tag = generate_rows(frame, materials)
for _, row in ipairs(material_rows) do
materialsTable:node(row)
end
-- Total cost
local total_cost_row = generate_total_cost_row(materials, material_costs)
materialsTable:node(total_cost_row)
-- Products
output_rows, output_cost, has_output_ref_tag = generate_rows(frame, output)
for _, row in ipairs(output_rows) do
materialsTable:node(row)
end
-- Profit
local profit_row, has_profit_ref_tag
if output_cost['Coins'] > 0 then
profit_row, has_profit_ref_tag = generate_profit_row(frame, ticks, material_costs['Coins'], output_cost['Coins'])
materialsTable:node(profit_row)
end
-- END OF MATERIALS AND PRODUCTS TABLE
----------------------------------------------------------------------------
-- Append the two tables a parent div
local parent = mw.html.create('div')
:addClass('recipe-table')
:cssText('width:-moz-fit-content;width:fit-content;')
parent:node(requirementsTable)
parent:node(materialsTable)
-- Set smw stuff
if useSmw then
local jsonObject = {skills = skills, members = members, materials = {}, output = output[1], ticks = ticks, facilities = facilities, tools = tools}
local materialNames = {}
for _, v in ipairs(materials) do
table.insert(jsonObject.materials, {name = v.name, quantity = v.quantity})
table.insert(materialNames, v.name)
end
local smwmap = {
['Uses material'] = materialNames,
['Uses tool'] = mw.text.split(tools or '', '%s*,%s*'),
['Uses facility'] = mw.text.split(facilities or '', '%s*,%s*'),
['Is members only'] = members,
['Production JSON'] = mw.text.jsonEncode(jsonObject),
['Is boostable'] = {},
['Uses skill'] = {},
}
for i, v in pairs(smwmap) do
-- trim off any {{!}}foo
if type(v) == 'table' then
for j, w in ipairs(v) do
v[j] = mw.text.split(w, '|')[1]
end
end
end
for _, s in ipairs(skills) do
smwmap[s.name..' level'] = tonumber(s.level)
smwmap[s.name..' experience'] = tonumber(s.experience)
table.insert(smwmap['Uses skill'], s.name)
if yesno(s.boostable, false) then
table.insert(smwmap['Is boostable'], s.name)
end
end
end
-- If there are any ref tags, add a Reflist section
local outro = ''
local has_ref_tag = has_ticks_ref_tag or has_material_ref_tag or has_output_ref_tag or has_profit_ref_tag
if has_ref_tag then
outro = '<div class="reflist">\n' .. frame:extensionTag{ name='references', args = { group='r' } } .. '</div>'
end
-- Return div with tables + categories + reflist
return tostring(parent) .. categories(args, skills, unknown_boostable_flag) .. outro
end
function categories(args, skills, unknown_boostable_flag)
if not onmain() then
return ''
end
local cats = {}
if unknown_boostable_flag then
table.insert(cats, '[[Category:Recipes missing boostable]]')
end
local missinglvl = false
local missingexp = false
local nonnumexp = false
for i, s in ipairs(skills) do
if s.name then
table.insert(cats, string.format('[[Category:%s]]', s.name))
end
if string.find(s.level, '?') then
missinglvl = true
end
if string.find(s.experience, '?') then
missingexp = true
elseif tonumber(s.experience) == nil then
nonnumexp = true
end
end
if missinglvl then
table.insert(cats, '[[Category:Missing skill info values]]')
end
if missingexp then
table.insert(cats, '[[Category:Needs experience info]]')
end
if nonnumexp then
table.insert(cats, '[[Category:Pages with non-numeric experience quantity]]')
end
if (args.ticks or '') == '' then
table.insert(cats, '[[Category:Recipes missing ticks]]')
end
return table.concat(cats,'')
end
return p