Module:Recipe
Appearance
| Main | Documentation | Usage |
Lua module for {{Recipe}}.
local p = {}
local Card = require('Module:Card')
-- Arguments for template
local param = {
sCharArg = 'character',
sTypeArg = 'type',
sSortArg = 'sort',
sSortDelim = ';',
sSortDelim2 = '¤',
sTimeArg = 'time',
sYieldArg = 'yield',
sPage = 'page'
}
p.param = param
--------------------------------------------------------------------------------
-- Utility
--------------------------------------------------------------------------------
local function padLeft(str, amt)
str = tostring(str or '')
return string.rep('0', math.max(amt - str:len(), 0)) .. str
end
local function pluralize(sOut, plural)
if plural then
sOut = sOut .. 's'
end
return sOut
end
local function normalizeType(s)
return mw.ustring.lower(mw.text.trim(tostring(s or '')))
end
local function argExists(args, key)
return args[key] ~= nil and tostring(args[key]) ~= ''
end
local function joinDisplay(list, separator)
local out = {}
for _, value in ipairs(list) do
table.insert(out, '[[' .. value .. ']]')
end
return table.concat(out, separator or ' / ')
end
local function joinFileNameParts(list)
return table.concat(list, ' & ')
end
--------------------------------------------------------------------------------
-- Card helpers
--------------------------------------------------------------------------------
local function addRecipe(category, sAmt, sName, quality)
local cardArgs = {
name = sName,
text = sAmt,
show_caption = '1',
mobile = 'bulleted'
}
if quality then
cardArgs.quality = quality
end
local cardNode = Card._main(cardArgs)
category:wikitext(
'[[Category:Uses ',
sName,
'|',
padLeft(tostring(sAmt or ''):gsub('%D', ''), 10),
']]'
)
return cardNode
end
local function addMiniCard(category, sName, sText, quality)
local cardArgs = {
name = sName,
text = sText or '',
show_caption = '1',
mobile = 'bulleted',
mini = '1'
}
if quality then
cardArgs.quality = quality
end
local cardNode = Card._main(cardArgs)
if category and sName then
category:wikitext('[[Category:Uses ', sName, ']]')
end
return cardNode
end
--------------------------------------------------------------------------------
-- Header rendering
--------------------------------------------------------------------------------
local function renderHeader(args, output, category)
local header = output:tag('div'):addClass('recipe_header')
local sType = args[param.sTypeArg]
header:tag('span'):addClass('recipe_header_main')
:tag('span'):addClass('mobileHide'):wikitext('<big>' .. sType .. '</big>'):done()
:tag('span'):addClass('mobileHide'):wikitext('<big>' .. sType .. '</big>'):done()
:tag('span'):addClass('recipe_header_text'):wikitext('[[' .. sType .. ']]'):done()
category:wikitext('[[Category:' .. sType .. ']]')
return header
end
local function renderTime(args, header)
if type(args[param.sTimeArg]) ~= 'string' then
return
end
local tTime = {}
local key = false
local plural = false
for n1, d, n2, h, n3, m, n4, s in mw.ustring.gmatch(
args[param.sTimeArg],
'(%d+)(d?)(%d*)(h?)(%d*)(m?)(%d*)(s?)'
) do
table.insert(tTime, n1)
if n1 ~= '1' then
plural = true
end
if d == 'd' then
table.insert(tTime, pluralize('day', plural))
plural = false
key = true
end
if n2 ~= '' then
table.insert(tTime, n2)
if n2 ~= '1' then
plural = true
end
end
if h == 'h' then
table.insert(tTime, pluralize('hour', plural))
plural = false
key = true
end
if n3 ~= '' then
table.insert(tTime, n3)
if n3 ~= '1' then
plural = true
end
end
if m == 'm' then
table.insert(tTime, pluralize('minute', plural))
plural = false
key = true
end
if n4 ~= '' then
table.insert(tTime, n4)
if n4 ~= '1' then
plural = true
end
end
if s == 's' then
table.insert(tTime, pluralize('second', plural))
key = true
end
end
if not key then
table.insert(tTime, pluralize('minute', plural))
end
local sTime = ''
for _, each in ipairs(tTime) do
sTime = sTime .. each .. ' '
end
header:tag('span'):addClass('recipe_header_sub')
:tag('span'):addClass('mobileHide'):wikitext(' for '):done()
:tag('span'):addClass('hidden'):wikitext(' for '):done()
:tag('span'):addClass('recipe_header_text'):wikitext(sTime):done()
end
local function renderCharacter(args, header)
if type(args[param.sCharArg]) ~= 'string' then
return
end
local sCharName = args[param.sCharArg]
header:tag('span'):addClass('recipe_header_sub')
:tag('span'):addClass('mobileHide'):wikitext(' with '):done()
:tag('span'):addClass('hidden'):wikitext('[[File:' .. sCharName .. ' Icon.png|24x24px|link=' .. sCharName .. ']]'):done()
:tag('span'):addClass('recipe_header_text'):wikitext('[[' .. sCharName .. ']]'):done()
end
--------------------------------------------------------------------------------
-- Standard recipe rendering
--------------------------------------------------------------------------------
local function renderStandardYield(args, body)
if args[param.sYieldArg] == '0' then
return
end
local sYield = args[param.sYieldArg] or '1'
local pageTitle = args.page or mw.title.getCurrentTitle().text
local yieldCard = Card._main({
name = pageTitle,
text = sYield,
show_caption = '1',
})
body
:tag('div'):addClass('recipe_body_icon')
:wikitext('➜'):done()
:tag('div'):addClass('recipe_body_yield')
:node(yieldCard)
local itemText = require('Module:Item').main{
[1] = pageTitle,
[2] = '15',
x = sYield
}
end
local function renderStandardRecipe(args, frame, output, category)
local tArgsProcessed = {}
local header = renderHeader(args, output, category)
renderTime(args, header)
renderCharacter(args, header)
local body = output:tag('div'):addClass('recipe_body')
local sSortDelim = param.sSortDelim
if ((args[param.sSortArg] or ''):find(param.sSortDelim2) ~= nil) then
sSortDelim = param.sSortDelim2
end
for sKey in string.gmatch(args[param.sSortArg] or '', '([^' .. sSortDelim .. ']+)') do
if type(args[sKey]) ~= 'nil' then
body:node(addRecipe(category, args[sKey], sKey))
tArgsProcessed[sKey] = true
end
end
for sKey, sVal in pairs(args) do
local restricted = false
for _, keyParam in pairs(param) do
if keyParam == sKey then
restricted = true
end
end
if tArgsProcessed[sKey] ~= true and not restricted then
body:node(addRecipe(category, sVal, sKey))
end
end
renderStandardYield(args, body)
body:done()
end
--------------------------------------------------------------------------------
-- Cooking recipe parsing
--------------------------------------------------------------------------------
local function getCookingIngredient(args, index)
local prefix = 'item_' .. index
local ingredient = {
index = index,
amount = args[prefix .. '_amount'] or '1',
quality = args[prefix .. '_quality'],
item = args[prefix],
types = {},
options = {}
}
-- |item_1_type = Fish
if argExists(args, prefix .. '_type') then
table.insert(ingredient.types, args[prefix .. '_type'])
end
-- |item_1_type_1 = Freshwater Fish
-- |item_1_type_2 = Saltwater Fish
local typeIndex = 1
while argExists(args, prefix .. '_type_' .. typeIndex) do
table.insert(ingredient.types, args[prefix .. '_type_' .. typeIndex])
typeIndex = typeIndex + 1
end
-- |item_1_option_1 = Rice
-- |item_1_option_2 = Wheat
local optionIndex = 1
while argExists(args, prefix .. '_option_' .. optionIndex) do
table.insert(ingredient.options, args[prefix .. '_option_' .. optionIndex])
optionIndex = optionIndex + 1
end
local hasIngredient =
ingredient.item ~= nil
or #ingredient.types > 0
or #ingredient.options > 0
if not hasIngredient then
return nil
end
return ingredient
end
local function getKitchenware(args)
local kitchenware = {}
if argExists(args, 'kitchenware') then
table.insert(kitchenware, args.kitchenware)
end
local index = 1
while argExists(args, 'kitchenware_' .. index) do
table.insert(kitchenware, args['kitchenware_' .. index])
index = index + 1
end
return kitchenware
end
--------------------------------------------------------------------------------
-- Cooking recipe display
--------------------------------------------------------------------------------
local function renderCookingTypeCard(category, body, ingredient)
local wrapper = body:tag('div')
:addClass('recipe_body_cooking_ingredient')
:addClass('recipe_body_cooking_type_card')
local fileBase = joinFileNameParts(ingredient.types)
local fileName = 'Recipe ' .. fileBase .. ' Icon.png'
local caption = joinDisplay(ingredient.types, ' / ')
for _, typeName in ipairs(ingredient.types) do
category:wikitext('[[Category:Uses ', typeName, ']]')
end
local amountText = tostring(ingredient.amount or '1')
local card = wrapper:tag('div')
:addClass('card')
:addClass('recipe-type-card')
if ingredient.quality then
card:addClass('card-quality-' .. normalizeType(ingredient.quality))
end
card:tag('div')
:addClass('card-image')
:wikitext('[[File:' .. fileName .. '|64x64px|link=]]')
card:tag('div')
:addClass('card-caption')
:wikitext(caption)
if amountText ~= '' then
card:tag('div')
:addClass('card-text')
:wikitext(amountText)
end
end
local function renderCookingOptionCards(category, body, ingredient)
local wrapper = body:tag('div')
:addClass('recipe_body_cooking_ingredient')
:addClass('recipe_body_cooking_options')
for _, itemName in ipairs(ingredient.options) do
wrapper:node(addRecipe(category, ingredient.amount, itemName, ingredient.quality))
end
end
local function renderCookingIngredient(category, body, ingredient)
-- Specific item:
-- |item_1 = Rice
if ingredient.item then
body:node(addRecipe(category, ingredient.amount, ingredient.item, ingredient.quality))
return
end
-- Multiple accepted specific items:
-- |item_1_option_1 = Rice
-- |item_1_option_2 = Wheat
if #ingredient.options > 0 then
renderCookingOptionCards(category, body, ingredient)
return
end
-- Type-based ingredient:
-- |item_1_type = Fish
-- |item_1_type_1 = Freshwater Fish
-- |item_1_type_2 = Saltwater Fish
if #ingredient.types > 0 then
renderCookingTypeCard(category, body, ingredient)
return
end
end
local function renderKitchenwareIcon(body, kitchenware)
body:tag('div')
:addClass('recipe_body_icon')
:addClass('recipe_body_kitchenware_icon')
:wikitext('[[File:Icon_' .. kitchenware .. '.png|35x35px|link=' .. kitchenware .. ']]')
end
local function renderKitchenwareMiniCard(category, body, kitchenware)
body:tag('div')
:addClass('recipe_body_icon')
:addClass('recipe_body_kitchenware_card')
:node(addMiniCard(category, kitchenware, ''))
end
local function renderCookingYield(args, category, body)
if args[param.sYieldArg] == '0' then
return
end
local sYield = args[param.sYieldArg] or '1'
local pageTitle = args.page or mw.title.getCurrentTitle().text
local kitchenware = getKitchenware(args)
local yieldCard = Card._main({
name = pageTitle,
text = sYield,
show_caption = '1',
})
if #kitchenware == 0 then
body:tag('div')
:addClass('recipe_body_icon')
:wikitext('➜')
elseif #kitchenware == 1 then
-- Default: icon mode.
-- Change this to renderKitchenwareMiniCard(category, body, kitchenware[1])
-- if you prefer mini-card kitchenware display.
renderKitchenwareIcon(body, kitchenware[1])
category:wikitext('[[Category:Uses ', kitchenware[1], ']]')
else
local wrapper = body:tag('div')
:addClass('recipe_body_icon')
:addClass('recipe_body_kitchenware_multi')
for _, itemName in ipairs(kitchenware) do
wrapper:node(addMiniCard(category, itemName, ''))
end
end
body:tag('div')
:addClass('recipe_body_yield')
:node(yieldCard)
local itemText = require('Module:Item').main{
[1] = pageTitle,
[2] = '15',
x = sYield
}
end
local function renderCookingRecipe(args, frame, output, category)
local header = renderHeader(args, output, category)
renderTime(args, header)
renderCharacter(args, header)
local body = output:tag('div')
:addClass('recipe_body')
:addClass('recipe_body_cooking')
local ingredientIndex = 1
local foundIngredient = false
while true do
local ingredient = getCookingIngredient(args, ingredientIndex)
if not ingredient then
break
end
foundIngredient = true
renderCookingIngredient(category, body, ingredient)
ingredientIndex = ingredientIndex + 1
end
if not foundIngredient then
body:tag('span')
:addClass('error')
:wikitext('Cooking recipe error. No cooking ingredients found.')
end
renderCookingYield(args, category, body)
body:done()
end
--------------------------------------------------------------------------------
-- Entry points
--------------------------------------------------------------------------------
function p.main(frame)
local args = require('Module:Arguments').getArgs(frame, {
parentFirst = true,
wrapper = { 'Template:Recipe' }
})
return p._main(args, frame)
end
function p._main(args, frame)
local output = mw.html.create('div')
:addClass('recipe_container')
local category = mw.html.create()
if type(args[param.sTypeArg]) ~= 'string' then
output:tag('span')
:addClass('error')
:wikitext('Recipe type error. Must include recipe type.')
else
local sType = args[param.sTypeArg]
if normalizeType(sType) == 'cooking' then
renderCookingRecipe(args, frame, output, category)
else
renderStandardRecipe(args, frame, output, category)
end
end
output:node(require('Module:Namespace detect').main({
["main"] = category
}))
return output
end
return p