Jump to content

Module:Recipe: Difference between revisions

From The Petit Planet Wiki
Replacing with my custom version from ZZZ, because it's better here.
No edit summary
Line 1: Line 1:
local p = {}
local p = {}
local Card = require('Module:Card')
local Card = require('Module:Card')
local category = mw.html.create()


--Arguments for template
-- Arguments for template
local param = {
local param = {
   sCharArg = 'character',
   sCharArg = 'character',
   sTypeArg = 'type',
   sTypeArg = 'type',
   sSortArg = 'sort',
   sSortArg = 'sort',
   sSortDelim = ';', --Needs to be pattern
   sSortDelim = ';', -- Needs to be pattern-safe for current usage
   sSortDelim2 = '¤',
   sSortDelim2 = '¤',
   sTimeArg = 'time',
   sTimeArg = 'time',
Line 14: Line 14:
   sPage = 'page'
   sPage = 'page'
}
}
p.param = param
p.param = param


function padLeft(str, amt)
local function padLeft(str, amt)
return string.rep('0', amt - str:len()) .. str
  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
end


function addRecipe(sAmt, sName)
local function addRecipe(category, sAmt, sName)
   local cardNode = Card._main({
   local cardNode = Card._main({
     name = sName,
     name = sName,
Line 27: Line 44:
     mobile = 'bulleted'
     mobile = 'bulleted'
   })
   })
   category:wikitext('[[Category:Uses ', sName, '|', padLeft(sAmt:gsub('%D', ''), 10), ']]')
 
   category:wikitext(
    '[[Category:Uses ',
    sName,
    '|',
    padLeft(tostring(sAmt or ''):gsub('%D', ''), 10),
    ']]'
  )
 
   return cardNode
   return cardNode
end
end


function pluralize(sOut,plural)
local function addSimpleCard(sName, sText)
   if plural then
   return Card._main({
     sOut = sOut .. 's'
     name = sName,
  end
    text = sText,
   return sOut
    show_caption = '1',
    mobile = 'bulleted'
   })
end
end


function p.main(frame)
local function renderHeader(args, output, category)
   local args = require('Module:Arguments').getArgs(frame, {
   local header = output:tag('div'):addClass('recipe_header')
     parentFirst = true,
 
     wrapper = { 'Template:Recipe' }
  local sType = args[param.sTypeArg]
   })
 
   return p._main(args,frame)
  header:tag('span'):addClass('recipe_header_main')
    -- Mobile Icon/Text, preserved from original
     :tag('span'):addClass('mobileHide'):wikitext('<big>' .. sType .. '</big>'):done()
    -- Desktop Icon/Text, preserved from original
    :tag('span'):addClass('mobileHide'):wikitext('<big>' .. sType .. '</big>'):done()
    -- Text
     :tag('span'):addClass('recipe_header_text'):wikitext('[[' .. sType .. ']]'):done()
 
   category:wikitext('[[Category:' .. sType .. ']]')
 
   return header
end
end


function p._main(args,frame)
local function renderTime(args, header)
  local output = mw.html.create('div')
   if type(args[param.sTimeArg]) ~= 'string' then
    :addClass('recipe_container')
     return
   if type(args[param.sTypeArg]) ~= 'string' then
  end
     output:tag('span'):addClass('error'):wikitext('Recipe type error. Must include recipe type.')
 
  else
  local tTime = {}
     local tArgsProcessed = {}
  local key = false
     local header = output:tag('div'):addClass('recipe_header')
  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


     --Type Parse
     if n2 ~= '' then
    local sType = args[param.sTypeArg]
       table.insert(tTime, n2)
    header:tag('span'):addClass('recipe_header_main')
       -- Mobile Icon
      :tag('span'):addClass('mobileHide'):wikitext('<big>' .. sType .. '</big>'):done()
      -- Desktop Icon
      :tag('span'):addClass('mobileHide'):wikitext('<big>' .. sType .. '</big>'):done()
      -- Text
      :tag('span'):addClass('recipe_header_text'):wikitext('[[' ..  sType .. ']]'):done()
    category:wikitext('[[Category:' .. sType .. ']]')


    --Time Parse
       if n2 ~= '1' then
    if type(args[param.sTimeArg]) == 'string' then
        plural = true
       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) --insert first value definitely
          if n1 ~= '1' then
              plural = true --generate plural
          end
          if d == 'd' then
              table.insert(tTime, pluralize('day', plural))
              plural = false --consume plural
              key = true
          end
          if n2 ~= '' then
              table.insert(tTime, n2)
              if n2 ~= '1' then
                  plural = true --generate plural
              end
          end
          if h == 'h' then
              table.insert(tTime,pluralize('hour', plural))
              plural = false --consume plural
              key = true
          end
          if n3 ~= '' then
              table.insert(tTime, n3)
              if n3 ~= '1' then
                  plural = true --generate plural
              end
          end
          if m == 'm' then
              table.insert(tTime, pluralize('minute', plural))
              plural = false --consume plural
              key = true
          end
          if n4 ~= '' then
              table.insert(tTime, n4)
              if n4 ~= '1' then
                  plural = true --generate plural
              end
          end
          if s == 's' then
              table.insert(tTime, pluralize('second', plural))
              key = true
          end
       end
       end
      if not(key) then
    end
          table.insert(tTime,pluralize('minute', plural))
 
    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
       local sTime = ''
    end
       for _, each in ipairs(tTime) do
 
          sTime = sTime .. each .. ' '
    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
      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
     end
    --Character Parse
 
     if type(args[param.sCharArg]) == 'string' then
     if s == 's' then
       local sCharName = args[param.sCharArg]
       table.insert(tTime, pluralize('second', plural))
      header:tag('span'):addClass('recipe_header_sub')
      key = true
        :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
     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
local function renderYield(args, body)
  if args[param.sYieldArg] == '0' then
    return
  end


local body = output:tag('div'):addClass('recipe_body')
  local sYield = args[param.sYieldArg] or '1'
  local pageTitle = args.page or mw.title.getCurrentTitle().text


--Check if sSortDelim2 exists in sSortArg
  local yieldCard = Card._main({
if ((args[param.sSortArg] or ''):find(param.sSortDelim2) ~= nil) then
    name = pageTitle,
param.sSortDelim = param.sSortDelim2
    text = sYield,
end
    show_caption = '1',
  })
    --Process Sort Parameter (Split by Delimiter)
 
    for sKey in string.gmatch(args[param.sSortArg] or '', '([^' .. param.sSortDelim .. ']+)') do
  body
      if type(args[sKey]) ~= 'nil' then
    :tag('div'):addClass('recipe_body_icon')
        body:node(addRecipe(args[sKey], sKey))
      :wikitext('➜'):done()
        tArgsProcessed[sKey] = true
    :tag('div'):addClass('recipe_body_yield')
      :node(yieldCard)
 
  -- Preserved from original, though unused.
  local itemText = require('Module:Item').main{
    [1] = pageTitle,
    [2] = '15',
    x = sYield
  }
end
 
--------------------------------------------------------------------------------
-- Standard, non-cooking recipe rendering
--------------------------------------------------------------------------------
 
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
 
  -- Check if sSortDelim2 exists in sSortArg.
  if ((args[param.sSortArg] or ''):find(param.sSortDelim2) ~= nil) then
    sSortDelim = param.sSortDelim2
  end
 
  -- Process Sort Parameter, split by delimiter.
  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
 
  -- Process rest of unrestricted parameters.
  for sKey, sVal in pairs(args) do
    local restricted = false
 
    for _, keyParam in pairs(param) do
      if keyParam == sKey then
        restricted = true
       end
       end
     end
     end


     --Process Rest of Unrestricted Parameters
     if tArgsProcessed[sKey] ~= true and not restricted then
     for sKey,sVal in pairs(args) do
      body:node(addRecipe(category, sVal, sKey))
      --Check if restricted parameter
    end
      local restricted = false
  end
       for _, keyParam in pairs(param) do
 
        if keyParam == sKey then
  renderYield(args, body)
          restricted = true
 
        end
  body:done()
      end
end
      --If not processed, and not a restricted parameter
 
       if tArgsProcessed[sKey] ~= true and not(restricted) then
--------------------------------------------------------------------------------
        body:node(addRecipe(sVal, sKey))
-- Cooking recipe rendering
       end
--------------------------------------------------------------------------------
 
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 = {}
  }
 
  -- Single type:
  -- |item_1_type = Fish
  if argExists(args, prefix .. '_type') then
    table.insert(ingredient.types, args[prefix .. '_type'])
  end
 
  -- Multiple types:
  -- |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
 
  -- Multiple accepted specific items:
  -- |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 = {}
 
  -- Single kitchenware:
  -- |kitchenware = Cooking Pot
  if argExists(args, 'kitchenware') then
    table.insert(kitchenware, args.kitchenware)
  end
 
  -- Multiple kitchenware:
  -- |kitchenware_1 = Cooking Pot
  -- |kitchenware_2 = Frying Pan
  local index = 1
 
  while argExists(args, 'kitchenware_' .. index) do
    table.insert(kitchenware, args['kitchenware_' .. index])
    index = index + 1
  end
 
  return kitchenware
end
 
local function renderCookingTypeRequirement(category, typeBox, amountText, types)
  if #types == 1 then
    local typeName = types[1]
 
    typeBox:tag('div')
      :addClass('recipe_body_cooking_requirement')
       :wikitext(amountText .. ' of [[:Category:' .. typeName .. '|' .. typeName .. ']]')
 
    category:wikitext('[[Category:Uses ', typeName, ']]')
 
    return
  end
 
  typeBox:tag('div')
    :addClass('recipe_body_cooking_requirement')
    :wikitext(amountText .. ' of any of:')
 
  for _, typeName in ipairs(types) do
    typeBox:tag('div')
      :addClass('recipe_body_cooking_type')
      :wikitext('[[:Category:' .. typeName .. '|' .. typeName .. ']]')
 
    category:wikitext('[[Category:Uses ', typeName, ']]')
  end
end
 
local function renderCookingIngredient(category, body, ingredient)
  local wrapper = body:tag('div')
    :addClass('recipe_body_cooking_ingredient')
 
  local amountText = tostring(ingredient.amount or '1')
 
  if ingredient.quality then
    amountText = amountText .. ' ' .. ingredient.quality
  end
 
  -- Specific item:
  -- |item_1 = Rice
  if ingredient.item then
    wrapper:node(addRecipe(category, amountText, ingredient.item))
    return
  end
 
  -- Multiple specific item options:
  -- |item_1_option_1 = Rice
  -- |item_1_option_2 = Wheat
  if #ingredient.options > 0 then
    local optionBox = wrapper:tag('div')
      :addClass('recipe_body_cooking_options')
 
    optionBox:tag('div')
      :addClass('recipe_body_cooking_requirement')
      :wikitext(amountText .. ' of any of:')
 
    for _, itemName in ipairs(ingredient.options) do
       optionBox:node(addRecipe(category, '', itemName))
    end
 
    return
  end
 
  -- Type-based ingredient:
  -- |item_1_type = Fish
  -- or:
  -- |item_1_type_1 = Freshwater Fish
  -- |item_1_type_2 = Saltwater Fish
  if #ingredient.types > 0 then
    local typeBox = wrapper:tag('div')
      :addClass('recipe_body_cooking_types')
 
    renderCookingTypeRequirement(category, typeBox, amountText, ingredient.types)
 
    return
  end
end
 
local function renderKitchenware(category, body, args)
  local kitchenware = getKitchenware(args)
 
  if #kitchenware == 0 then
    return
  end
 
  local wrapper = body:tag('div')
    :addClass('recipe_body_kitchenware')
 
  wrapper:tag('div')
    :addClass('recipe_body_kitchenware_label')
    :wikitext('Kitchenware')
 
  for _, itemName in ipairs(kitchenware) do
    wrapper:node(addSimpleCard(itemName, ''))
    category:wikitext('[[Category:Uses ', itemName, ']]')
  end
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
     end


     --Yield Parse
     foundIngredient = true
     if args[param.sYieldArg] ~= '0' then
    renderCookingIngredient(category, body, ingredient)
    local sYield = args[param.sYieldArg] or '1'
 
    local pageTitle = args.page or mw.title.getCurrentTitle().text
     ingredientIndex = ingredientIndex + 1
    local yieldCard = Card._main{
  end
      name = pageTitle,
 
      text = sYield,
  if not foundIngredient then
      show_caption = '1',
    body:tag('span')
    }
      :addClass('error')
    body
      :wikitext('Cooking recipe error. No cooking ingredients found.')
      :tag('div'):addClass('recipe_body_icon')
  end
        :wikitext('➜'):done()
 
      :tag('div'):addClass('recipe_body_yield')
  renderKitchenware(category, body, args)
        :node(yieldCard)
 
       
  renderYield(args, body)
    local itemText = require('Module:Item').main{
 
    [1] = pageTitle,
  body:done()
    [2] = '15',
end
    x = sYield
 
    }
--------------------------------------------------------------------------------
   
-- 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')
 
  -- Important: category must be local to this render.
  -- Do not keep it as a module-level variable.
  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
    body:done()
   end
   end
   output:node(require('Module:Namespace detect').main{["main"]=category})
 
   output:node(require('Module:Namespace detect').main({
    ["main"] = category
  }))
 
   return output
   return output
end
end


return p
return p

Revision as of 07:29, 7 May 2026

Module documentation

Lua module for {{Recipe}}.

local p = {}

local Card = require('Module:Card')

-- Arguments for template
local param = {
  sCharArg = 'character',
  sTypeArg = 'type',
  sSortArg = 'sort',
  sSortDelim = ';', -- Needs to be pattern-safe for current usage
  sSortDelim2 = '¤',
  sTimeArg = 'time',
  sYieldArg = 'yield',
  sPage = 'page'
}

p.param = param

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 addRecipe(category, sAmt, sName)
  local cardNode = Card._main({
    name = sName,
    text = sAmt,
    show_caption = '1',
    mobile = 'bulleted'
  })

  category:wikitext(
    '[[Category:Uses ',
    sName,
    '|',
    padLeft(tostring(sAmt or ''):gsub('%D', ''), 10),
    ']]'
  )

  return cardNode
end

local function addSimpleCard(sName, sText)
  return Card._main({
    name = sName,
    text = sText,
    show_caption = '1',
    mobile = 'bulleted'
  })
end

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')
    -- Mobile Icon/Text, preserved from original
    :tag('span'):addClass('mobileHide'):wikitext('<big>' .. sType .. '</big>'):done()
    -- Desktop Icon/Text, preserved from original
    :tag('span'):addClass('mobileHide'):wikitext('<big>' .. sType .. '</big>'):done()
    -- Text
    :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

local function renderYield(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)

  -- Preserved from original, though unused.
  local itemText = require('Module:Item').main{
    [1] = pageTitle,
    [2] = '15',
    x = sYield
  }
end

--------------------------------------------------------------------------------
-- Standard, non-cooking recipe rendering
--------------------------------------------------------------------------------

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

  -- Check if sSortDelim2 exists in sSortArg.
  if ((args[param.sSortArg] or ''):find(param.sSortDelim2) ~= nil) then
    sSortDelim = param.sSortDelim2
  end

  -- Process Sort Parameter, split by delimiter.
  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

  -- Process rest of unrestricted parameters.
  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

  renderYield(args, body)

  body:done()
end

--------------------------------------------------------------------------------
-- Cooking recipe rendering
--------------------------------------------------------------------------------

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 = {}
  }

  -- Single type:
  -- |item_1_type = Fish
  if argExists(args, prefix .. '_type') then
    table.insert(ingredient.types, args[prefix .. '_type'])
  end

  -- Multiple types:
  -- |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

  -- Multiple accepted specific items:
  -- |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 = {}

  -- Single kitchenware:
  -- |kitchenware = Cooking Pot
  if argExists(args, 'kitchenware') then
    table.insert(kitchenware, args.kitchenware)
  end

  -- Multiple kitchenware:
  -- |kitchenware_1 = Cooking Pot
  -- |kitchenware_2 = Frying Pan
  local index = 1

  while argExists(args, 'kitchenware_' .. index) do
    table.insert(kitchenware, args['kitchenware_' .. index])
    index = index + 1
  end

  return kitchenware
end

local function renderCookingTypeRequirement(category, typeBox, amountText, types)
  if #types == 1 then
    local typeName = types[1]

    typeBox:tag('div')
      :addClass('recipe_body_cooking_requirement')
      :wikitext(amountText .. ' of [[:Category:' .. typeName .. '|' .. typeName .. ']]')

    category:wikitext('[[Category:Uses ', typeName, ']]')

    return
  end

  typeBox:tag('div')
    :addClass('recipe_body_cooking_requirement')
    :wikitext(amountText .. ' of any of:')

  for _, typeName in ipairs(types) do
    typeBox:tag('div')
      :addClass('recipe_body_cooking_type')
      :wikitext('[[:Category:' .. typeName .. '|' .. typeName .. ']]')

    category:wikitext('[[Category:Uses ', typeName, ']]')
  end
end

local function renderCookingIngredient(category, body, ingredient)
  local wrapper = body:tag('div')
    :addClass('recipe_body_cooking_ingredient')

  local amountText = tostring(ingredient.amount or '1')

  if ingredient.quality then
    amountText = amountText .. ' ' .. ingredient.quality
  end

  -- Specific item:
  -- |item_1 = Rice
  if ingredient.item then
    wrapper:node(addRecipe(category, amountText, ingredient.item))
    return
  end

  -- Multiple specific item options:
  -- |item_1_option_1 = Rice
  -- |item_1_option_2 = Wheat
  if #ingredient.options > 0 then
    local optionBox = wrapper:tag('div')
      :addClass('recipe_body_cooking_options')

    optionBox:tag('div')
      :addClass('recipe_body_cooking_requirement')
      :wikitext(amountText .. ' of any of:')

    for _, itemName in ipairs(ingredient.options) do
      optionBox:node(addRecipe(category, '', itemName))
    end

    return
  end

  -- Type-based ingredient:
  -- |item_1_type = Fish
  -- or:
  -- |item_1_type_1 = Freshwater Fish
  -- |item_1_type_2 = Saltwater Fish
  if #ingredient.types > 0 then
    local typeBox = wrapper:tag('div')
      :addClass('recipe_body_cooking_types')

    renderCookingTypeRequirement(category, typeBox, amountText, ingredient.types)

    return
  end
end

local function renderKitchenware(category, body, args)
  local kitchenware = getKitchenware(args)

  if #kitchenware == 0 then
    return
  end

  local wrapper = body:tag('div')
    :addClass('recipe_body_kitchenware')

  wrapper:tag('div')
    :addClass('recipe_body_kitchenware_label')
    :wikitext('Kitchenware')

  for _, itemName in ipairs(kitchenware) do
    wrapper:node(addSimpleCard(itemName, ''))
    category:wikitext('[[Category:Uses ', itemName, ']]')
  end
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

  renderKitchenware(category, body, args)

  renderYield(args, 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')

  -- Important: category must be local to this render.
  -- Do not keep it as a module-level variable.
  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