Модуль:Recipes
Перейти к навигации
Перейти к поиску
Документация Документация, указанная ниже, находится на странице «Модуль:Recipes/док». (править | история)
См. также этот модуль на английском языке: Module:Recipes. В нём может содержаться более полная или подробная информация.
Этот модуль используется для работы шаблона Recipes. Для более подробной информации см. документацию шаблона.
--------------------------------------------------------------------------------
--
-- =============================================================================
--
-- Module:Recipes
--
-- Storing and querying of crafting recipe data
--
-- =============================================================================
--
-- Code annotations:
-- This module is documented according to LuaCATS (Lua Comment and Type System).
-- LuaCATS comments are prefixed with three dashes (---) and use Markdown syntax.
-- For a full list of annotations, see the following link:
-- https://luals.github.io/wiki/annotations/
--
--------------------------------------------------------------------------------
---Holds the tables with the localization (l10n) information for the different
---languages, taken from the l10n submodule.
local l10n_info = mw.loadData('Module:Recipes/l10n')
---Holds the l10n information for the current language, as key-value pairs.
---@type table<string, string>
local l10n_table
---The current language. Determines which l10n table to use.
---@type string
local lang
local trim = mw.text.trim
local tag = mw.text.tag
local item_link = require('Module:Item').go
local getItemStat = require('Module:Iteminfo').getItemStat
local itemNameData = require('Module:ItemNames').getData
local moduleTr = require('Module:Tr')
local cache = require 'mw.ext.LuaCache'
local variables = mw.ext.VariablesLua
---A cached version of the current frame, the interface to the parser.
local currentFrame
---Holds the arguments from the template call.
---@type table<string, string>
local inputArgs
---Counters for `p.query`: the function displays a table which by default consists
---of the columns 'Result', 'Ingredients', and 'Crafting station'. In `tableStart`,
---additional columns (extra columns, 'extCols') are potentially added, depending
---on template parameters. Those extCols are grouped by their position:
---1. col-A-1, col-A-2, ...
---2. 'Result' (default column, not extCol)
---3. col-B-1, col-B-2, ...
---4. 'Ingredients' (default column, not extCol)
---5. col-C-1, col-C-2, ...
---6. station-col-before-1, station-col-before-2, ...
---7. 'Crafting station' (default column, not extCol)
---8. station-col-after-1, station-col-after-2, ...
---9. col-D-1, col-D-2, ...
---How many extCols there are in each group is important information for the functions
---that produce table rows, so those counters are made available globally via this
---table. They are defined in `tableStart`.
local extColCounts = {
A = 0,
B = 0,
C = 0,
D = 0,
stationBefore = 0,
stationAfter = 0,
}
---A record from the `Recipes` cargo table. All fields are strings or `nil`, even
---though in the cargo table they might be integers, Booleans, etc.
---@class (exact) CargoTableRecord
---@field result string? Item name
---@field resultid string? Item ID
---@field resultimage string? File name
---@field resulttext string? Note
---@field amount string?
---@field version string? `<platform1> <platform2>`
---@field station string?
---@field ingredients string? `¦itemname1¦^¦itemname2¦`
---@field ings string? `¦itemname1¦amount1^¦itemname2¦amount2`
---@field args string? `itemname1#i:...#n:...¦amount1^itemname2#i:...#n:...¦amount2`
---Return the l10n string associated with the `key`.
---@param key string
---@return string
local function l10n(key)
return l10n_table[key] or l10n_info['en'][key]
end
---Return a trimmed version of the value of the template parameter with the specified `key`.
---Return `nil` if the parameter is empty or unset.
---@param key string|integer
---@return string?
local function getArg(key)
local value = inputArgs[key]
if not value then
return nil
end
value = trim(value)
if value == '' then
return nil
end
return value
end
---Conversion table for the `getBoolArg` function.
---@type table<string, boolean>
local stringToBoolean = {
['y'] = true, ['yes'] = true, ['1'] = true, ['on'] = true,
['n'] = false, ['no'] = false, ['0'] = false, ['off'] = false,
}
---Convert the value of the template parameter with the specified `key` to a Boolean.
---Return `default` or `nil` if the value is not a recognized Boolean string.
---@param key string|integer
---@param default boolean?
---@return boolean?
local function getBoolArg(key, default)
local value = getArg(key)
if stringToBoolean[value] ~= nil then
return stringToBoolean[value]
end
return default
end
---Split the `str` on each `div` in it and return the result as a table. Original
---version credit: http://richard.warburton.it. This version trims each substring.
---@param div string
---@param str string
---@return string[]? strExploded Is `nil` if `div` is an empty string
local function explode(div, str)
if (div=='') then return nil end
local pos,arr = 0,{}
-- for each divider found
for st,sp in function() return string.find(str,div,pos,true) end do
arr[#arr+1] = trim(string.sub(str,pos,st-1)) -- Attach chars left of current divider
pos = sp+1 -- Jump past current divider
end
arr[#arr+1] = trim(string.sub(str,pos)) -- Attach chars right of last divider
return arr
end
---Escape all single quotes (normal and as HTML entities) with backslashes.
---@param stringToEscape string
---@return string
local function escape(stringToEscape)
local s, _ = stringToEscape:gsub("'", "\\'"):gsub("'", "\\'")
return s -- discard the number of replacements; irrelevant
end
---Add single quotes around the string and escape any quotes inside the string.
---@param stringToEnclose string
---@return string
local function enclose(stringToEnclose)
return "'" .. escape(stringToEnclose) .. "'"
end
---Row order modes for the table output of `p.query`.
---@alias ResultOrderMode
---| 'result' # Order by English result item name
---| 'resultid' # Order by result item ID
---Modes of adding station name categories to the output of `p.query`.
---@enum ECateModes
local cateModes = {
NEVER = 1,
IF_RESULT_IS_PAGENAME = 2,
ALWAYS = 3,
}
---Options that affect the table output of `p.query`.
local options = {
---Whether the items in the result column should have an anchor.
resultanchor = false,
---Page name to transclude in place of each result cell.
---@type string?
resultTemplate = nil,
---How to order the rows.
---@type ResultOrderMode
resultOrder = 'result',
---Under what circumstances to add categorization (station name categories).
---@type ECateModes
cateMode = cateModes.IF_RESULT_IS_PAGENAME,
---Whether to add links to the items in the result column.
linkResult = true,
---Whether to display the "Internal Item ID" text on each item in the result
---column.
showResultId = false,
---Whether to group (i.e. "merge") identical, consecutive cells in the result
---column.
groupResults = true,
---Whether to include a "Crafting station" column in the table.
makeStationColumn = true,
---Whether to group (i.e. "merge") identical, consecutive cells in the station
---column.
groupStations = true,
}
---Set values in the `options` table based on the `inputArgs`.
local function setOptions()
options.resultanchor = getArg('resultanchor')
options.resultTemplate = getArg('resulttemplate')
options.groupResults = getBoolArg('grouping', options.groupResults)
options.linkResult = getBoolArg('link', options.linkResult)
options.showResultId = getBoolArg('showresultid', options.showResultId)
if getBoolArg('orderbyid') then
options.resultOrder = 'resultid'
end
options.makeStationColumn = not getBoolArg('nostation', not options.makeStationColumn)
options.groupStations = options.makeStationColumn
if options.makeStationColumn then
options.groupStations = getBoolArg('stationgrouping', options.groupStations)
end
-- categorization modes:
-- `|cate=force/all` => ALWAYS
-- `|cate=n/no/0/off` => NEVER
-- `|cate=y/yes/1/on/(anything else)` => IF_RESULT_IS_PAGENAME
local cateArg = getArg('cate')
if cateArg == 'force' or cateArg == 'all' then
options.cateMode = cateModes.ALWAYS
elseif not getBoolArg('cate', true) then
options.cateMode = cateModes.NEVER
end
end
---Cache for the `tr` function to increase speed on repeated translations.
---@type table<string, string>
local trCache = {}
---Translate an input text into the target language via `Module:Tr`.
---@param text string Text to translate
---@param lang string Target language
---@param link? boolean Whether to translate a page name
---@return string translatedText
local function tr(text, lang, link)
-- assemble the identifier for the cache table
local cacheKey = lang .. '|' .. text
if link then
cacheKey = cacheKey .. '|l'
end
-- look up in the cache table: was this input translated before?
local result = trCache[cacheKey]
if not result then
-- no result in the cache table; this is the first time
-- perform the translation
result = link and moduleTr.translateLink(text, lang) or moduleTr.translate(text, lang)
-- add the translated result to the cache table for the next time
trCache[cacheKey] = result
end
return result
end
---Cache for the `itemLink` function to increase speed on repeated item links.
---@type table<string, string>
local itemLinkCache = {}
---Make an {{item}} link via `Module:Item`.
---@param name string Name of the item, may be suffixed with `#i:<image>` and/or `#n:<note>`
---@param args? table<string, string> Parameters for `Module:Item`
---@return string linkedItem
local function itemLink(name, args)
-- assemble the identifier for the cache table
local cacheKey = name .. '|'
if args then
for k, v in pairs(args) do
cacheKey = cacheKey .. k .. '=' .. tostring(v) .. '|'
end
end
-- look up in the cache table: was an item link made for this input before?
local cachedItemLink = itemLinkCache[cacheKey]
if cachedItemLink then
-- cache exists; just return it
return cachedItemLink
end
-- there was no result in the cache table; this is the first time
---Arguments for the `item_link` call.
---@type table<string, string>
local ilArgs = {}
if args then
ilArgs = mw.clone(args) -- deep copy
end
-- look for a '#' in the item name input
local pos = name:find('#', 1, true)
if pos then
-- there is a '#', so the item name input has some extra data:
-- `#i:<image>` and/or `#n:<note>`
-- e.g.: `Wood#i:Acorn.png#n:After Moon Lord is defeated`
local itemnameStripped = name:sub(1, pos-1) -- actual item name without the extra data
-- extract all the extra data
for k, v in name:gmatch("#(%l):([^#]+)") do
-- `k` is the lowercase letter `i` or `n`
-- `v` is the value (`<image>` or `<note>`)
if k == 'i' then
-- extra data: image (`#i:<image>`)
-- any `&` in the data value is replaced by the item name
-- special shortcut: `#i:old` for `#i:& (old).png`
if v == 'old' then
ilArgs.image = itemnameStripped .. ' (old).png'
else
ilArgs.image = string.gsub(v, '&', itemnameStripped)
end
elseif k == 'n' then
-- extra data: image (`#n:<note>`)
ilArgs.note2 = v
end
end
name = itemnameStripped
end
ilArgs[1] = name
if not ilArgs[2] or ilArgs[2] == '' then
ilArgs[2] = tr(name, lang, false)
end
ilArgs.small = 'y'
ilArgs.lang = lang or 'en'
ilArgs.nolink = ilArgs.nolink and 'y' or nil
-- perform the item link expansion
local result = item_link(currentFrame, ilArgs)
-- add the result to the cache table for the next time
itemLinkCache[cacheKey] = result
return result
end
---Cache for the `versionIcons` function to increase speed on repeated icons.
---@type { [string]: string }
local versionIconsCache = {}
---Transform a string of versions into their respective icons via `{{version icons}}`.
---@param versionStr string Input, e.g. `desktop-console-mobile`
---@return string versionIconsStr
local function versionIcons(versionStr)
-- assemble the identifier for the cache table
local cacheKey = lang .. ':recipes:versionicon:' .. versionStr
-- look up in the cache table: was this version input transformed before?
local versionIconsStr = versionIconsCache[cacheKey] or cache.get(cacheKey)
if not versionIconsStr then
-- no result in the cache table; this is the first time
-- perform the transformation
versionIconsStr = currentFrame:expandTemplate{ title = 'version icons', args = {versionStr} }
-- add the version icons to the cache table for the next time
versionIconsCache[cacheKey] = versionIconsStr
cache.set(cacheKey, versionIconsStr, 3600*24) -- LuaCache: 24 hours
end
return versionIconsStr
end
---Cache for the `addCate` function so that no category is added more than once.
---@type { [string]: true }
local cateCache = {}
---Add the `stationName` to the list of stations for which a category will be
---output by `cateStr`.
---@param stationName string
local function addCate(stationName)
cateCache[stationName] = true
end
---Return the wikitext for all categories in the `cateCache`. Add the generic
---"craftable" category if the `cateCache` is not empty.
---@return string
local function cateStr()
local cateStrings = {}
for station, _ in pairs(cateCache) do
cateStrings[#cateStrings+1] = (
'[[Category:'
.. (l10n('station_cate')[station] or tr(station, lang, true))
.. ']]'
)
end
if #cateStrings > 0 then
-- there is at least one category to add, so also add the generic "craftable"
-- category; and add it before any of the others
table.insert(cateStrings, 1, '[[Category:' .. l10n('cate_craftable') .. ']]')
end
return table.concat(cateStrings)
end
---Metals that should be displayed first have even values, and their alternatives
---have their value plus 1.
local metals = {
Copper = 0, Tin = 1,
Iron = 2, Lead = 3,
Silver = 4, Tungsten = 5,
Gold = 6, Platinum = 7,
Cobalt = 8, Palladium = 9,
Mythril = 10, Orichalcum = 11,
Adamantite = 12, Titanium = 13,
}
---Explode a string of ingredients separated by `/` characters. Normalize "metal"
---ingredients like so:
---* `Iron/Lead Bar` => `{ 'Iron Bar', 'Lead Bar' }`
---* `Lead/Iron Bar` => `{ 'Iron Bar', 'Lead Bar' }`
---* `Lead Bar/Iron Bar` => `{ 'Iron Bar', 'Lead Bar' }`
---* `Titanium/Copper Helmet` => `{ 'Titanium Helmet', 'Copper Helmet' }`
---Only perform this normalization on the metals defined in `metals`. Otherwise:
---* `Acorn` => `{ 'Acorn' }`
---* `Grubby/Sluggy/Buggy` => `{ 'Grubby', 'Sluggy', 'Buggy' }`
---@param ingredient string
---@return string[]
local function splitIngredients(ingredient)
-- count the number of `/` characters in `ingredient` (up to 2, we don't care about more)
local _, count = ingredient:gsub('/', '/', 2)
if count ~= 1 then
-- there is only 1 item name or 3 or more item names in `ingredient`
return explode('/', ingredient) --[[@as string[] ]]
end
-- there are exactly 2 item names in `ingredient`
local pattern = '^%s*(%S+)%s*(.-)/%s*(%S+)%s*(.-)$'
-- * `Iron/Lead Bar` => `item1a=Iron`, `item2a=Lead`, `item2b=Bar`
-- * `Silver Watch/Tungsten Watch` => `item1a=Silver`, `item1b=Watch`, `item2a=Tungsten`, `item2b=Watch`
-- * `Ebonwood/Shadewood` => `item1a=Ebonwood`, `item2a=Shadewood`
local item1a, item1b, item2a, item2b = ingredient:match(pattern)
if metals[item1a] and metals[item2a] then
if tostring(item1b) == '' then
-- copy the common suffix to the first item, e.g.:
-- `Iron/Lead Bar` => `Iron Bar/Lead Bar`
item1b = item2b
end
-- check for incorrect order: if `item1a` has an odd value in `metals` and
-- `item2a` has the even value before it (e.g. item1a=Lead, item2a=Iron),
-- then the items are alternative metals but in the wrong order;
-- the second part of this condition ensures that only alternative metals
-- are considered and not something like item1a=Titanium, item2a=Copper
if metals[item1a] % 2 == 1 and metals[item2a] == metals[item1a] - 1 then
-- swap item1 and item2
item1a, item1b, item2a, item2b = item2a, item2b, item1a, item1b
end
end
return { trim(item1a .. ' ' .. item1b), trim(item2a .. ' ' .. item2b) }
end
---Normalize an ingredient input for storing it to the cargo table:
---* `Lead Bar` => `¦Lead Bar¦`
---* `Iron/Lead Bar` => `¦Iron Bar¦Lead Bar¦`
---* `Lead/Iron Bar` => `¦Iron Bar¦Lead Bar¦`
---* Strip extra data like `#i:` and `#n:`
---@param ingredient string Ingredient name input to normalize
---@return string
local function normalizeIngredient(ingredient)
local items = splitIngredients(ingredient)
for i, item in ipairs(items) do
-- strip the "extdata" (the first '#' character and everything after it)
items[i] = item:gsub('#.+', '')
end
return '¦' .. table.concat(items, '¦') .. '¦'
end
---Whether the `initItemGroups` function has already been called.
local itemGroupsInitialized = false
---Names of all item groups, indexed (e.g. `[1]='Any Wood'`).
---@type table<integer, string>
local itemGroupNames = {}
---Group membership of all items (e.g. `['Ebonwood']=1`).
---@type table<string, integer>
local itemGroupLookup = {}
---Fill the `itemGroupNames` and `itemGroupLookup` tables with data.
local function initItemGroups()
itemGroupsInitialized = true
---Initialize one item group.
---@param groupname string Name of the group
---@param groupcontents table List of item names in the group
local function group(groupname, groupcontents)
-- append the `groupName` to `itemGroupNames`
local newIndex = #itemGroupNames + 1
itemGroupNames[newIndex] = groupname
-- store each item of `groupcontents` with the `itemGroupNames` index of this group
for _, itemname in ipairs(groupcontents) do
itemGroupLookup[itemname] = newIndex
end
end
group('Any Wood', {
'Wood', 'Ebonwood', 'Rich Mahogany', 'Pearlwood', 'Shadewood',
'Spooky Wood', 'Boreal Wood', 'Palm Wood', 'Ash Wood',
})
group('Any Iron Bar', {
'Iron Bar', 'Lead Bar',
})
group('Any Sand', {
'Sand Block', 'Pearlsand Block', 'Crimsand Block', 'Ebonsand Block',
'Hardened Sand Block', 'Hardened Ebonsand Block', 'Hardened Crimsand Block',
'Hardened Pearlsand Block',
})
group('Any Pressure Plate', {
'Red Pressure Plate', 'Green Pressure Plate', 'Gray Pressure Plate',
'Brown Pressure Plate', 'Blue Pressure Plate', 'Yellow Pressure Plate',
'Lihzahrd Pressure Plate',
})
group('Any Bird', {
'Bird', 'Blue Jay', 'Cardinal',
})
group('Any Scorpion', {
'Black Scorpion', 'Scorpion',
})
group('Any Squirrel', {
'Squirrel', 'Red Squirrel',
})
group('Any Jungle Bug', {
'Grubby', 'Sluggy', 'Buggy',
})
group('Any Duck', {
'Mallard Duck', 'Duck',
})
group('Any Butterfly', {
'Sulphur Butterfly', 'Julia Butterfly', 'Monarch Butterfly',
'Purple Emperor Butterfly', 'Red Admiral Butterfly', 'Tree Nymph Butterfly',
'Ulysses Butterfly', 'Zebra Swallowtail Butterfly',
})
group('Any Firefly', {
'Firefly', 'Lightning Bug',
})
group('Any Snail', {
'Snail', 'Glowing Snail',
})
group('Any Fruit', {
'Apple', 'Apricot', 'Banana', 'Blackcurrant', 'Blood Orange', 'Cherry',
'Coconut', 'Dragon Fruit', 'Elderberry', 'Grapefruit', 'Lemon', 'Mango',
'Peach', 'Pineapple', 'Plum', 'Rambutan', 'Star Fruit', 'Spicy Pepper',
'Pomegranate',
})
group('Any Dragonfly', {
'Black Dragonfly', 'Blue Dragonfly', 'Green Dragonfly', 'Orange Dragonfly',
'Red Dragonfly', 'Yellow Dragonfly',
})
group('Any Turtle', {
'Turtle', 'Jungle Turtle',
})
group('Any Macaw', {
'Scarlet Macaw', 'Blue Macaw',
})
group('Any Cockatiel', {
'Yellow Cockatiel', 'Gray Cockatiel',
})
group('Any Cloud Balloon', {
'Cloud in a Balloon', 'Blue Horseshoe Balloon',
})
group('Any Blizzard Balloon', {
'Blizzard in a Balloon', 'White Horseshoe Balloon',
})
group('Any Sandstorm Balloon', {
'Sandstorm in a Balloon', 'Yellow Horseshoe Balloon',
})
group('Any Balloon', {
'Silly Green Balloon', 'Silly Pink Balloon', 'Silly Purple Balloon',
})
group('Any Guide to Critter Companionship', {
'Guide to Critter Companionship', 'Guide to Critter Companionship (Inactive)',
})
group('Any Guide to Environmental Preservation', {
'Guide to Environmental Preservation',
'Guide to Environmental Preservation (Inactive)',
})
end
---Return the name of the ingredient group that the `item` belongs to.
---@param item string Name of the ingredient (e.g. "Ebonwood")
---@return string? groupName Name of the group (e.g. "Any Wood")
local function getItemGroupName(item)
if not itemGroupsInitialized then
initItemGroups()
end
return itemGroupLookup[item] and itemGroupNames[itemGroupLookup[item]] or nil
end
---Normalize a crafting station name:
---* `Altar` => `Demon Altar`
---@param station string Crafting station name to normalize
---@return string
local function normalizeStation(station)
if station == 'Altar' then
station = 'Demon Altar'
end
return station
end
---Normalize a version string:
---* Discard invalid/unknown versions
---* Order, lower case, one space as separator
---* All versions => empty string
---@param version string Version string to normalize
---@return string
local function normalizeVersion(version)
if not version or version == '' then
return ''
end
version = trim(version):lower()
-- search for all the valid versions and put the ones that are found in a table
local normalizedVersion = {}
if version:find('desktop', 1, true) then
table.insert(normalizedVersion, 'desktop')
end
if version:find('console', 1, true) then
table.insert(normalizedVersion, 'console')
end
if version:find('old-gen', 1, true) then
table.insert(normalizedVersion, 'old-gen')
end
if version:find('japan', 1, true) then
table.insert(normalizedVersion, 'japan')
end
if version:find('mobile', 1, true) then
table.insert(normalizedVersion, 'mobile')
end
if version:find('3ds', 1, true) then
table.insert(normalizedVersion, '3ds')
end
if #normalizedVersion == 0 then
-- no valid versions were found in the `version` input
return ''
end
local normalizedVersionStr = table.concat(normalizedVersion, ' ')
if normalizedVersionStr == 'desktop console old-gen mobile 3ds' then
-- all of the versions (except "japan") were found in the input
return ''
end
return normalizedVersionStr
end
---Turn the `constraintArgs` into an SQL WHERE string.
---@param constraintArgs table<string, string>
---@return string whereStr
local function makeConstraints(constraintArgs)
---@type string[]
local constraints = {}
-- station = '...'
local stationArg = trim(constraintArgs['station'] or '')
if stationArg ~= '' then
-- convert `s1/s2/s3` to `station=s1 OR station=s2 OR station=s3`
local pieces = explode('/', stationArg)
---@cast pieces -?
for i, v in ipairs(pieces) do
pieces[i] = 'station = ' .. enclose(normalizeStation(v))
end
constraints[#constraints+1] = table.concat(pieces, ' OR ')
end
-- station != '...'
local stationnotArg = trim(constraintArgs['stationnot'] or '')
if stationnotArg ~= '' then
-- convert `s1/s2/s3` to `station<>s1 AND station<>s2 AND station<>s3`
local pieces = explode('/', stationnotArg)
---@cast pieces -?
for i, v in ipairs(pieces) do
pieces[i] = 'station <> ' .. enclose(normalizeStation(v))
end
constraints[#constraints+1] = table.concat(pieces, ' AND ')
end
-- result = '...'
local resultArg = trim(constraintArgs['result'] or '')
if resultArg ~= '' then
-- convert `r1/r2/LIKE r3` to `result=r1 OR result=r2 OR result LIKE r3`
local pieces = explode('/', resultArg)
---@cast pieces -?
for i, v in ipairs(pieces) do
if mw.ustring.sub(v, 1, 5) == 'LIKE ' then
pieces[i] = 'result LIKE ' .. enclose(trim(mw.ustring.sub(v, 6)))
else
pieces[i] = 'result = ' .. enclose(v)
end
end
constraints[#constraints+1] = table.concat(pieces, ' OR ')
end
-- result != '...'
local resultnotArg = trim(constraintArgs['resultnot'] or '')
if resultnotArg ~= '' then
-- convert `r1/r2/LIKE r3` to `result<>r1 AND result<>r2 AND result NOT LIKE r3`
local pieces = explode('/', resultnotArg)
---@cast pieces -?
for i, v in ipairs(pieces) do
if mw.ustring.sub(v, 1, 5) == 'LIKE ' then
pieces[i] = 'result NOT LIKE ' .. enclose(trim(mw.ustring.sub(v, 6)))
else
pieces[i] = 'result <> ' .. enclose(v)
end
end
constraints[#constraints+1] = table.concat(pieces, ' AND ')
end
-- ingredient = '...'
local ingredientArg = trim(constraintArgs['ingredient'] or '')
if ingredientArg ~= '' then
local pieces = explode('/', ingredientArg)
---@cast pieces -?
for i, v in ipairs(pieces) do
-- add the "Any x" group of the item name (`v`) if it is in such a group,
-- unless the prefix is "#" or "LIKE "
if mw.ustring.sub(v, 1, 1) == '#' then
pieces[i] = "ingredients HOLDS LIKE '%¦" .. escape(mw.ustring.sub(v, 2)) .. "¦%'"
elseif mw.ustring.sub(v, 1, 5) == 'LIKE ' then
pieces[i] = "ingredients HOLDS LIKE '%¦" .. escape(trim(mw.ustring.sub(v, 6))) .. "¦%'"
else
pieces[i] = "ingredients HOLDS LIKE '%¦" .. escape(v) .. "¦%'"
local group = getItemGroupName(v)
if group then
pieces[i] = pieces[i] .. " OR ingredients HOLDS LIKE '%¦" .. escape(group) .. "¦%'"
end
end
end
constraints[#constraints+1] = table.concat(pieces, ' OR ')
end
-- legacy
local legacyArg = trim(constraintArgs['legacy'] or '')
if legacyArg == '' then
-- default behavior if legacyArg is not specified or empty
local page = mw.title.getCurrentTitle() --[[@as table]]
if page:hasSubjectNamespace(0) then
-- mainspace and main talk space: no legacy recipes, but only if the
-- article has a corresponding legacy page (this variable is set in
-- {{legacy nav tab}})
if variables.var('__article_legacy_nav_tab_page') ~= '' then
legacyArg = 'n'
end
end
if page:hasSubjectNamespace(11000) then
-- "Legacy:" namespace and associated talk space: only legacy recipes
legacyArg = 'y'
end
end
if stringToBoolean[legacyArg] == nil then
-- the legacyArg is an invalid Boolean string, so ignore it and include
-- all recipes in the query, both legacy and non-legacy
elseif stringToBoolean[legacyArg] then
constraints[#constraints+1] = 'legacy = "1"'
else
constraints[#constraints+1] = 'legacy = "0" OR legacy IS NULL'
end
-- versions
local versionArg = constraintArgs['version'] or constraintArgs['versions'] or ''
versionArg = normalizeVersion(versionArg)
if versionArg ~= '' then
constraints[#constraints+1] = 'version = ' .. enclose(versionArg)
end
local whereStr = ''
if #constraints > 0 then
whereStr = '(' .. table.concat(constraints, ') AND (') .. ')'
end
return whereStr
end
---Format a table cell in the "Result" column.
---@param record CargoTableRecord Record of the current row
---@return string cellContent
local function resultCell(record)
---@type string[]
local cellContents = {}
local result = record.result --[[@as string]]
local version = record.version or ''
---Arguments for the `{{item}}` call used for formatting the result item.
local itemArgs = { nolink = not options.linkResult, class = 'multi-line' }
if options.resultanchor then
itemArgs.anchor = (
'<div class="anchor" id="'
.. currentFrame:callParserFunction('anchorencode', tr(result, lang))
.. '"></div>'
)
end
-- version text
if version ~= '' then
cellContents[#cellContents+1] = '<div class="version-note note-text small">'
cellContents[#cellContents+1] = l10n('version_note_before')
cellContents[#cellContents+1] = versionIcons(version)
cellContents[#cellContents+1] = l10n('version_note_after')
cellContents[#cellContents+1] = '</div>'
-- if there is version-exclusive info, don't show eicons on the result {{item}}
itemArgs.icons = 'no'
end
if options.showResultId then
itemArgs.id = record.resultid
end
if record.resultimage then
itemArgs.image = record.resultimage
end
cellContents[#cellContents+1] = itemLink(result, itemArgs)
-- add amount text
if record.amount ~= '1' then
cellContents[#cellContents+1] = tag('span', { class = 'am' }, record.amount)
end
-- add note text
local resultnote = record.resulttext or ''
if resultnote ~= '' then
local note_l10n = l10n('result_note') or {}
resultnote = note_l10n[resultnote] or resultnote
cellContents[#cellContents+1] = tag('div', { class = 'result-note note-text small' }, resultnote)
end
-- combine all the cell contents into one string
local cellContentStr = table.concat(cellContents)
if options.resultTemplate then
local templateOutput = currentFrame:expandTemplate{
title = options.resultTemplate, args = {
link = options.linkResult, showid = options.showResultId,
resultid = record.resultid, resultimage = record.resultimage,
resultnote = resultnote, result = result, amount = record.amount,
versions = version,
}
}
-- "@@@@" is the placeholder for the regular result item output
cellContentStr = templateOutput:gsub('@@@@', cellContentStr)
end
return cellContentStr
end
---Format a table cell in the "Ingredients" column.
---@param ingredientsArgs string The `args` field of the `CargoTableRecord` of the current row
---@param itemLinkArgs? table<string, string> Arguments for each ingredient's {{item}}
---@return string cellContent `<ul>` element
local function ingredientsCell(ingredientsArgs, itemLinkArgs)
---List of ingredient strings, each in the format `itemname#i:...#n:...¦amount`.
local ingredients = explode('^', ingredientsArgs) --[[@as string[] ]]
for i, ingredient in ipairs(ingredients) do
local item, amount = ingredient:match('^(.-)¦(.-)$')
-- some old-gen recipes that contain substitutable ingredients are not
-- stored as separate recipes and so will contain ingredients like
-- `Adamantite/Titanium Bar`
-- (see [[Alternative crafting ingredients#Substitutable ingredients]])
local items = splitIngredients(item)
-- make an {{item}} for each one
for j, subItem in ipairs(items) do
items[j] = itemLink(subItem, itemLinkArgs)
end
local ingredientStr = table.concat(items, l10n('ingredients_sep'))
-- add the amount string
if amount ~= '1' then
ingredientStr = ingredientStr .. tag('span', { class = 'am' }, amount)
end
ingredients[i] = tag('li', nil, ingredientStr)
end
return tag('ul', nil, table.concat(ingredients))
end
---Format the `station` string with `{{item}}` or a similar representation.
---* Replace occurrences of "and" with `andStr`.
---* Expand certain stations which have alternatives (e.g. Iron Anvil => "Iron
--- Anvil or Lead Anvil") and separate them with `orStr`.
---* Return the `station` unchanged if there is an unrecognized station in it
--- (see the three tables in `formatStationLinkSingle` for all recognized ones).
---@param station string
---@param itemLinkArgs table<string, string> Arguments to be passed to each `{{item}}` in this function
---@param andStr string String for "and"
---@param orStr string String for "or"
---@param orStrInner string String for "or" when inside an "and" (e.g. "Iron Anvil *orStrInner* Lead Anvil and Ecto Mist")
---@return string
local function stationLink(station, itemLinkArgs, andStr, orStr, orStrInner)
-- split the `station` input on " and " and format each part, then concatenate
-- the formatted parts back with `andStr`
local compoundStationParts = explode(' and ', station)
local hasCompounds = #compoundStationParts > 1
-- Table that contains initial link arguments
-- use `mw.clone` instead of `=`, otherwise the tables `oldLinkArgs` and `itemLinkArgs` would link to the same object
local oldLinkArgs = mw.clone(itemLinkArgs)
---Make an `itemLink` for `name` with the base `itemLinkArgs`.
---@param name string
---@param extraArgs? table<string, string> Additional arguments for `itemLink`; overrides `itemLinkArgs` if necessary
---@return string
local function il(name, extraArgs)
itemLinkArgs = mw.clone(oldLinkArgs)
if extraArgs then
for k, v in pairs(extraArgs) do itemLinkArgs[k] = v end
end
return itemLink(name, itemLinkArgs)
end
---Format the `station` with `itemLink` or a similar representation, depending on the station.
---@param singleStation string
---@return string|false
local function formatStationLinkSingle(singleStation)
---Stations with completely custom formatting.
local otherFormat = {
['By Hand'] = l10n('station_by_hand'),
['Chlorophyte Extractinator'] = il('Chlorophyte Extractinator', {[2] = l10n('station_chlorophyte_extractinator')}),
['Ecto Mist'] = il('Ecto Mist', {mode = 'text'}),
['Placed Bottle only'] = il('Placed Bottle'),
['Shimmer'] = il('Shimmer', {[2] = l10n('station_shimmer')}),
['Snow biome'] = il('Snow biome', {mode = 'text'}),
['Snow Biome'] = il('Snow biome', {mode = 'text'}),
}
---Stations with no special formatting, just a simple `itemLink`.
local simpleItemLink = {
['Ancient Manipulator'] = true,
['Autohammer'] = true,
['Blend-O-Matic'] = true,
['Bone Welder'] = true,
['Bookcase'] = true,
['Campfire'] = true,
['Chair'] = true,
['Crystal Ball'] = true,
['Decay Chamber'] = true,
['Dye Vat'] = true,
['Flesh Cloning Vat'] = true,
['Furnace'] = true,
['Glass Kiln'] = true,
['Heavy Work Bench'] = true,
['Hellforge'] = true,
['Honey Dispenser'] = true,
['Honey'] = true,
['Ice Machine'] = true,
['Imbuing Station'] = true,
['Keg'] = true,
['Lava'] = true,
['Lihzahrd Furnace'] = true,
['Living Loom'] = true,
['Living Wood'] = true,
['Loom'] = true,
['Meat Grinder'] = true,
['Sawmill'] = true,
['Sky Mill'] = true,
['Solidifier'] = true,
['Steampunk Boiler'] = true,
['Table'] = true,
['Teapot'] = true,
["Tinkerer's Workshop"] = true,
['Work Bench'] = true,
}
---Stations that have one or more alternatives, concatenated with "or".
local multipleItemLinks = {
['Adamantite Forge'] = { 'Titanium Forge', },
['Cooking Pot'] = { 'Cauldron', },
['Demon Altar'] = { 'Crimson Altar', },
['Iron Anvil'] = { 'Lead Anvil', },
['Mythril Anvil'] = { 'Orichalcum Anvil', },
['Placed Bottle'] = { 'Alchemy Table', },
['Water'] = { 'Sink', },
}
if otherFormat[singleStation] then
return otherFormat[singleStation]
elseif simpleItemLink[singleStation] then
return il(singleStation)
elseif multipleItemLinks[singleStation] then
-- e.g. "Iron Anvil" => "Iron Anvil or Lead Anvil"
---List of `itemLinks` for each alternative of this station.
local allAlternatives = { il(singleStation) }
for _, altStation in ipairs(multipleItemLinks[singleStation]) do
allAlternatives[#allAlternatives+1] = il(altStation)
end
-- if there are other stations in the full `station` string (e.g.
-- "Iron Anvil and Ecto Mist"), then wrap this in an extra <span> tag
-- and use the `orStrInner` as separator
if hasCompounds then
return tag('span', {class = 'ib'}, table.concat(allAlternatives, orStrInner))
else
return table.concat(allAlternatives, orStr)
end
end
-- the `singleStation` was not in any of the three tables, so it's invalid
return false
end
---@type string[]
local formattedStations = {}
for i, compoundStationPart in ipairs(compoundStationParts) do
local formattedStation = formatStationLinkSingle(compoundStationPart)
if formattedStation == false then
-- the formatting failed because `compoundStationPart` was not recognized
-- as a valid station, so abort all formatting now and return the
-- input unchanged
return station
end
formattedStations[i] = formattedStation
end
return table.concat(formattedStations, andStr)
end
---Format a table cell in the "Crafting station" column.
---@param station string The `station` field of the `CargoTableRecord` of the current row
---@param inline? boolean Whether not to set `wrap=y` on all `{{item}}`s, like it is done by default
---@return string cellContent
local function stationCell(station, inline)
return stationLink(
station,
inline and {} or {wrap = 'y'},
l10n('station_sep_and'),
l10n('station_sep_or'),
l10n('station_sep_or')
)
end
---Format a crafting station in a "compact" way (for the `compact` mode of `p.extract`).
---@param station string The `station` field of the `CargoTableRecord` to format
---@param formatString string "@@@@" within this string will be replaced with the station
---@return string? formattedStation `nil` if the station is "By Hand" (which will never be formatted)
local function compactStation(station, formatString)
if station == 'By Hand' then
return
end
local s, _ = formatString:gsub('@@@@', stationLink(
station,
{mode = 'image'},
l10n('compact_sep_stationAnd'),
l10n('compact_sep_stationOr'),
l10n('compact_sep_stationOrInner')
))
return s -- discard the number of replacements from `gsub`; irrelevant
end
---Construct the HTML attributes for the `<table>` element.
---@return table<'class'|'id'|'style', string>
local function tableAttributes()
local sortable = getBoolArg('sortable', true) and 'sortable ' or ''
local customClasses = getArg('class') or ''
return {
class = 'terraria cellborder recipes ' .. sortable .. customClasses,
id = getArg('id'),
style = getArg('css') or getArg('style'),
}
end
---Return the caption (if provided) and the header row of the table.
---Also fill the global `extColCounts` table.
---@param caption? string
---@return string tableStartElements `<caption>` element and `<tr>` element
local function tableStart(caption)
---List of `<th>` elements that will constitute the first `<tr>` of the table.
---@type string[]
local headerCells = {}
---Add a `<th>` for each of the template parameters that have the common
---`argPrefix` and have a value set, then return the number of those parameters.
---For example, with `argPrefix=foo-` and template parameters like so:
---`{{recipes|foo-1=X|foo-2=Y|foo-3=Z|foo-8=Q}}`, the function will
---add `<th>X</th> <th>Y</th> <th>Z</th>` to the `headerCells` list and return
---`3`. The `foo-8` is disregarded because it is not consecutive.
---@param argPrefix string Common prefix of the template parameter names
---@param htmlClass? string Class to set on each `<th>` element
---@return integer count Number of template parameters that have the `argPrefix`
local function addColumnsForEach(argPrefix, htmlClass)
-- start at parameter `argPrefix1`, then `argPrefix2`, etc., and go up
-- until the parameter is not set or its value is blank
local i = 1
while true do
local v = getArg(argPrefix .. i)
if not v then
break
end
headerCells[#headerCells+1] = tag('th', { class = htmlClass }, v)
i = i + 1
end
return i - 1
end
-- fill the `headerCells` list:
-- <th> for each `col-A-x` parameter
extColCounts.A = addColumnsForEach('col-A-')
-- <th> for the result column
headerCells[#headerCells+1] = tag('th',
{ class = 'result' },
(getArg('header-result') or l10n('header_result'))
)
-- <th> for each `col-B-x` parameter
extColCounts.B = addColumnsForEach('col-B-')
-- <th> for the ingredients column
headerCells[#headerCells+1] = tag('th',
{ class = 'ingredients' },
(getArg('header-ingredients') or l10n('header_ingredients'))
)
-- <th> for each `col-C-x` parameter
extColCounts.C = addColumnsForEach('col-C-')
if options.makeStationColumn then
-- <th> for each `station-col-before-x` parameter
extColCounts.stationBefore = addColumnsForEach('station-col-before-', 'station')
-- <th> for the station column
headerCells[#headerCells+1] = tag('th',
{ class = 'station' },
(getArg('header-station') or l10n('header_station'))
)
-- <th> for each `station-col-after-x` parameter
extColCounts.stationAfter = addColumnsForEach('station-col-after-', 'station')
end
-- <th> for each `col-D-x` parameter
extColCounts.D = addColumnsForEach('col-D-')
-- put all the header cells into a <tr> tag
local headerRow = tag('tr', nil, table.concat(headerCells))
-- put the `caption` text (if present) into a <caption> element
local captionElem = caption and tag('caption', nil, caption) or ''
return captionElem .. headerRow
end
---Format the cargo record as a table row.
---@param record CargoTableRecord
---@param recordIndex integer Index of this record among all records (the full cargo query result)
---@param status table Data to track across multiple rows
---@return string rowElement `<tr>` element
local function tableRow(record, recordIndex, status)
---Custom identifier of this record's crafting station, as defined in a template parameter.
local stationIndex = getArg('station-index-' .. record.station)
---Custom identifier of this record, as defined in a template parameter.
local resultIndex = (
getArg('result-index-#' .. recordIndex)
or getArg('result-index-' .. record.result .. '-' .. (record.version or ''))
or getArg('result-index-' .. record.result)
)
---Unique identifier for the result of this record, for comparing with
---`status.result`.
local result = table.concat(
{
record.result, (record.resultid or ''), (record.resultimage or ''),
(record.resulttext or ''), record.amount, (record.version or '')
},
'|'
)
-- update the `status` table
if status.station ~= record.station then
-- the previous record has a different crafting station from this one
status.station = record.station
status.stationCounterPrev = status.stationCounter
status.stationCounter = 1
else
-- the previous record has the same crafting station as this one
status.stationCounter = status.stationCounter + 1
end
if status.result ~= result then
-- the previous record has a different result identifier from this one
status.result = result
status.resultCounterPrev = status.resultCounter
status.resultCounter = 1
else
-- the previous record has the same result identifier as this one
status.resultCounter = status.resultCounter + 1
end
if status.resultIndex ~= resultIndex then
-- the previous record has a different custom result identifier from this one
status.resultIndex = resultIndex
status.resultIndexCounterPrev = status.resultIndexCounter
status.resultIndexCounter = 1
else
-- the previous record has the same custom result identifier as this one
status.resultIndexCounter = status.resultIndexCounter + 1
end
---List of `<td>` elements.
---@type string[]
local cells = {}
---Get the value of a `-row-` template parameter, put it in a `<td>`
---element and add that element to the `cells` list.
---@param columnName string Parameter name without index and prefix, e.g. `col-A-1`
---@param htmlClass string Class to set on each `<td>` element
---@param index? string Custom identifier of a crafting station or result item
---@param counter integer
---@param needGroup boolean
---@param placeholder string Placeholder for the `rowspan` attribute
local function extColCell(columnName, htmlClass, index, counter, needGroup, placeholder)
local content = ''
local attr = { class = htmlClass }
if index then
local cellName = index .. '-row-' .. columnName
content = getArg(cellName) or ''
attr.colspan = getArg(cellName .. '-colspan')
end
-- add `rowspan` attribute to this cell if needed
if counter == 1 and needGroup then
attr.rowspan = placeholder
end
cells[#cells+1] = tag('td', attr, content)
end
---Call `extColCell()` for each column in a group of columns, e.g. for
---`col-A-1`, `col-A-2`, and `col-A-3`, if `columnPrefix=col-A-` and `count=3`.
---@param count integer Number of cells in this group
---@param columnPrefix string Common prefix of the group of columns
---@param station? boolean
local function extColCells(count, columnPrefix, station)
if not station and (options.groupResults and status.resultIndexCounter > 1) then
return
end
for i = 1, count do
local columnName = columnPrefix .. i
if station then
extColCell(
columnName, 'station ' .. columnName, stationIndex,
status.stationCounter, options.groupStations, 'xxxrowspanxxx'
)
else
extColCell(
columnName, columnName, resultIndex,
status.resultIndexCounter, options.groupResults, 'zzzrowspanzzz'
)
end
end
end
-- cells for extCols A
extColCells(extColCounts.A, 'col-A-')
-- cell for result item
if not options.groupResults or status.resultCounter == 1 then
local attr = { class = 'result', ['data-sort-value'] = record.result }
if status.resultCounter == 1 and options.groupResults then
attr.rowspan = 'yyyrowspanyyy'
end
cells[#cells+1] = tag('td', attr, resultCell(record))
end
-- cells for extCols B
extColCells(extColCounts.B, 'col-B-')
-- cell for ingredient items
cells[#cells+1] = tag('td', { class = 'ingredients' }, ingredientsCell(record.args))
-- cell for extCols C
extColCells(extColCounts.C, 'col-C-')
if options.makeStationColumn and (not options.groupStations or status.stationCounter == 1) then
-- cells for extCols stationBefore
extColCells(extColCounts.stationBefore, 'station-col-before-', true)
-- cell for crafting station
cells[#cells+1] = tag('td',
{ class = 'station', rowspan = options.groupStations and 'xxxrowspanxxx' or nil},
stationCell(record.station)
)
-- cells for extCols stationAfter
extColCells(extColCounts.stationAfter, 'station-col-after-', true)
end
-- cells for extCols D
extColCells(extColCounts.D, 'col-D-')
-- put all of the cells into a <tr> element
return tag('tr', { ['data-rowid'] = recordIndex }, table.concat(cells))
end
---Format all cargo records as table rows.
---@param cargoQueryResult CargoTableRecord[] Records to format
---@param rootpagename string
---@return string[] rowElements `<tr>` elements
local function normalRows(cargoQueryResult, rootpagename)
---When iterating over the cargo records, track this data. It is needed for
---grouping the table rows with `rowspan`.
local status = {
---Crafting station of the current record. Multiple records can have the
---same crafting station.
---@type string?
station = nil,
---Current number of records that have the `status.station`.
stationCounter = 0,
---Total number of records that have the previous `status.station`.
stationCounterPrev = 0,
---Result identifier (item + image + amount etc.) of the current record.
---Multiple records can have the same result identifier (e.g. an item
---with different recipes).
---@type string?
result = nil,
---Current number of records that have the `status.result`.
resultCounter = 0,
---Total number of records that have the previous `status.result`.
resultCounterPrev = 0,
---Custom result identifier (as defined in a template parameter) of the
---current record.
---@type string?
resultIndex = nil,
---Current number of records that have the `status.resultIndex`.
resultIndexCounter = 0,
---Total number of records that have the previous `status.resultIndex`.
resultIndexCounterPrev = 0,
}
---List of `<tr>` elements.
---@type string[]
local rows = {}
-- iterate over the rows (records) of the cargo query result
for i, record in ipairs(cargoQueryResult) do
-- make a <tr> element and update the `status` table for this record
rows[i] = tableRow(record, i, status)
-- grouping via `rowspan` attributes:
-- only if we're past the first row, only if grouping is even enabled in
-- the `options`, and only if the current row started a new group
if i > 1 and options.groupStations and status.stationCounter == 1 then
-- this row has a new station, so group all of the previous rows
rows[i-status.stationCounterPrev] = rows[i-status.stationCounterPrev]:gsub('xxxrowspanxxx', status.stationCounterPrev)
end
if i > 1 and options.groupResults and status.resultCounter == 1 then
-- this row has a new result identifier, so group all of the previous rows
rows[i-status.resultCounterPrev] = rows[i-status.resultCounterPrev]:gsub('yyyrowspanyyy', status.resultCounterPrev)
end
if i > 1 and options.groupResults and status.resultIndexCounter == 1 then
-- this row has a new custom result identifier, so group all of the previous rows
rows[i-status.resultIndexCounterPrev] = rows[i-status.resultIndexCounterPrev]:gsub('zzzrowspanzzz', status.resultIndexCounterPrev)
end
-- categorization
local needCate = options.cateMode ~= cateModes.NEVER
if options.cateMode == cateModes.IF_RESULT_IS_PAGENAME then
-- in this cateMode: only categorize if result item name equals pagename
needCate = tr(record.result, lang) == rootpagename
end
if needCate then
addCate(record.station)
end
end
-- grouping for the last groups
if rows[1] then -- unnecessary if there aren't any rows
if options.groupStations then
rows[#rows-status.stationCounter+1] = rows[#rows-status.stationCounter+1]:gsub('xxxrowspanxxx', status.stationCounter)
end
if options.groupResults then
rows[#rows-status.resultCounter+1] = rows[#rows-status.resultCounter+1]:gsub('yyyrowspanyyy', status.resultCounter)
rows[#rows-status.resultIndexCounter+1] = rows[#rows-status.resultIndexCounter+1]:gsub('zzzrowspanzzz', status.resultIndexCounter)
end
end
return rows
end
---Return all extra rows. These have completely manual contents, which are taken
---from the template parameters starting with `extrow-`.
---@param isTop? boolean Whether to consider the parameters starting with `topextrow-` instead
---@return string rowElements Zero, one, or more `<tr>` elements
local function extRows(isTop)
-- the following three variables are row-specific and are reset for each row
-- in the loop below
---Prefix for the template parameter; includes the current row index.
---@type string
local argPrefix
---List of `<td>` elements.
---@type string[]
local cellsInThisRow
---Whether none of the cells in this row has any content.
---@type boolean
local rowIsEmpty
---Get the value of an `extrow-` template parameter, put it in a `<td>`
---element and add that element to the `cellsInThisRow` list.
---@param columnName string Parameter name without prefix, e.g. `col-A-1`
---@param htmlClass? string Class to set on each `<td>` element
local function extColCell(columnName, htmlClass)
local cellName = argPrefix .. columnName
local content = getArg(cellName)
if content then
rowIsEmpty = false
end
cellsInThisRow[#cellsInThisRow+1] = tag('td',
{
class = htmlClass or columnName,
colspan = getArg(cellName .. '-colspan'),
rowspan = getArg(cellName .. '-rowspan')
},
content or ''
)
end
---Call `extColCell()` for each column in a group of columns, e.g. for
---`col-A-1`, `col-A-2`, and `col-A-3`, if `columnPrefix=col-A-` and `count=3`.
---@param count integer Number of cells in this group
---@param columnPrefix string Common prefix of the group of columns
---@param station? boolean Whether to add the `station` class to each `<td>` element
local function extColCells(count, columnPrefix, station)
for i = 1, count do
local columnName = columnPrefix .. i
extColCell(columnName, station and ('station ' .. columnName) or nil)
end
end
---String that all the template parameters start with.
local commonArgPrefix = isTop and 'topextrow-' or 'extrow-'
---List of `<tr>` elements.
---@type string[]
local rows = {}
-- gather all of the template parameters that start with `(top)extrow-1` for
-- all of the extcols, then `(top)extrow-2`, and so on, until there are no
-- parameters for a row
local rowIndex = 0
while true do
rowIndex = rowIndex + 1
-- start new row: reset list of cells and emptiness flag
cellsInThisRow = {}
rowIsEmpty = true
-- in this iteration, only consider template parameters with the current row index
argPrefix = commonArgPrefix .. rowIndex .. '-'
extColCells(extColCounts.A, 'col-A-')
extColCell('col-result', 'result')
extColCells(extColCounts.B, 'col-B-')
extColCell('col-ingredients', 'ingredients')
extColCells(extColCounts.C, 'col-C-')
if options.makeStationColumn then
extColCells(extColCounts.stationBefore, 'station-col-before-', true)
extColCell('col-station', 'station')
extColCells(extColCounts.stationAfter, 'station-col-after-', true)
end
extColCells(extColCounts.D, 'col-D-')
if not rowIsEmpty then
-- put all of the cells in this row into a <tr> element
local attr = { ['data-' .. commonArgPrefix .. 'id'] = rowIndex }
rows[#rows+1] = tag('tr', attr, table.concat(cellsInThisRow))
else
-- none of the template parameters (for none of the columns) were set
-- or had a non-blank value, so we reached the last row and can return now
break
end
end
return table.concat(rows)
end
---Format the result of a cargo query as a recipes table.
---@param cargoQueryResult CargoTableRecord[]
---@param rootpagename string
---@param caption? string Caption of the whole table
---@param expectedrows? integer Number of expected table rows
---@return string
local function tableBody(cargoQueryResult, rootpagename, caption, expectedrows)
local tableStartStr = tableStart(caption)
local extRowsTopStr = extRows(true)
local normalTableRows = normalRows(cargoQueryResult, rootpagename)
local normalRowsStr = table.concat(normalTableRows)
local extRowsStr = extRows()
local tableBodyStr = (
tableStartStr .. -- <caption> and header <tr>
extRowsTopStr .. -- <tr>s with manual data
normalRowsStr .. -- <tr>s with cargo data
extRowsStr -- <tr>s with manual data
)
local rowsCount = #normalTableRows
local attributes = tableAttributes()
attributes['data-expectedRows'] = expectedrows
attributes['data-totalRows'] = rowsCount
local str = tag('table', attributes, tableBodyStr)
if expectedrows and rowsCount ~= expectedrows then
str = str .. '[[Category:'.. l10n('cate_unexpected_rows_count') .. ']]'
end
if not expectedrows and rowsCount == 0 then
str = str .. '[[Category:'.. l10n('cate_no_row') .. ']]'
end
-- categorization
if options.cateMode ~= cateModes.NEVER then
str = str .. cateStr()
end
return str
end
--------------------------------------------------------------------------------
---Main return object
local p = {}
---For `{{recipes/register}}`: register a recipe to the cargo table.
---@param frame table Interface to the parser (`mw.frame`)
p.register = function(frame)
inputArgs = frame:getParent().args -- global input args cache
---List of ingredients (`itemname`), used for querying.
local ingredients = {}
---List of ingredients (`fulliteminput¦amount`), used for building ingredient cells.
local ingArgs = {}
---List of ingredients (`itemnameamount`), used in `GROUP BY` to eliminate duplicate rows.
local ings = {}
-- iterate over pairs of unnamed parameters
local index = 1
while inputArgs[index*2-1] do
-- parameters 1, 3, 5, etc.: ingredient
local itemStr = trim(inputArgs[index*2-1])
local item = normalizeIngredient(itemStr) -- e.g. ¦Iron Bar¦Lead Bar¦
-- parameters 2, 4, 6, etc.: amount
local amount = trim(inputArgs[index*2])
-- append to each of the three lists
ingredients[index] = item
ingArgs[index] = itemStr .. '¦' .. amount
ings[index] = item .. amount
index = index + 1
end
table.sort(ings)
-- determine the "legacy" boolean
local legacyArg = getBoolArg('legacy')
if legacyArg == nil then
-- default behavior if legacyArg is not specified or empty/invalid
local page = mw.title.getCurrentTitle() --[[@as table]]
if page:hasSubjectNamespace(0) then
-- mainspace and main talk space
legacyArg = false
end
if page:hasSubjectNamespace(11000) then
-- "Legacy:" namespace and associated talk space
legacyArg = true
end
end
-- store to cargo
frame:callParserFunction('#cargo_store:_table=Recipes', {
result = getArg('result') or '',
resultid = getArg('resultid') or '',
resultimage = getArg('image') or '',
resulttext = getArg('note') or '', -- reuse the deprecated/unused "resulttext" field for notes
amount = getArg('amount') or '',
version = normalizeVersion(getArg('version') or ''),
station = normalizeStation(getArg('station') or ''),
ingredients = table.concat(ingredients, '^'),
ings = table.concat(ings, '^'),
args = table.concat(ingArgs, '^'),
legacy = legacyArg and 'yes' or 'no',
})
end -- p.register
---For `{{recipes}}`: query the cargo table and return a table of recipes.
---@param frame table Interface to the parser (`mw.frame`)
---@return string recipesTable Wikitext of the recipes table
p.query = function(frame)
currentFrame = frame -- global frame cache
inputArgs = frame:getParent().args -- global input args cache
lang = frame.args['lang'] or 'en'
l10n_table = l10n_info[lang] or l10n_info['en']
setOptions()
local where = getArg('where') or ''
if where == '' then
where = makeConstraints(inputArgs)
end
-- if there are no constraints, then there can never be any results
if where == '' then
return frame:expandTemplate{ title = 'error', args = {
'Recipes: No constraints',
from = 'Recipes',
inline = 'y'
}}
end
---@type integer?
local expectedrows
local expectedrowsArg = getArg('expectedrows') or ''
if expectedrowsArg ~= '' then
expectedrows = tonumber(expectedrowsArg)
end
local rootpagename = mw.title.getCurrentTitle().rootText
-- prepare the cargo query
local orderBy = options.resultOrder .. ', amount DESC, version'
if options.makeStationColumn then
-- if we should print a station column, then we must order by station first
orderBy = 'station, ' .. orderBy
end
local fields = 'result, resultid, resultimage, resulttext, amount, version, station, args'
local groupBy = 'resultid, result, amount, ings, version, station'
-- perform the cargo query
---@type CargoTableRecord[]
local result = mw.ext.cargo.query('Recipes', fields, {
where = where,
groupBy = groupBy,
orderBy = orderBy,
limit = 2000,
})
-- format the query result as a table
return tableBody(result, rootpagename, getArg('title'), expectedrows)
end -- p.query
---For `{{recipes/extract}}`: query the cargo table and return a compact display.
---@param frame table Interface to the parser (`mw.frame`)
---@return string
p.extract = function(frame)
currentFrame = frame -- global frame cache
inputArgs = frame:getParent().args -- global input args cache
lang = frame.args['lang'] or 'en'
l10n_table = l10n_info[lang] or l10n_info['en']
local where = getArg('where') or ''
if where == '' then
where = makeConstraints(inputArgs)
end
-- if there are no constraints, then there can never be any results
if where == '' then
return frame:expandTemplate{ title = 'error', args = {
'Recipes/extract: No constraints',
from = 'Recipes',
inline = 'y'
}}
end
-- prepare the cargo query
local orderBy = getBoolArg('orderbyid') and 'resultid' or 'result'
orderBy = orderBy .. ', amount DESC, version' -- do not order by station
local fields = 'result, resultid, resultimage, resulttext, amount, version, station, args, ings'
local groupBy = 'resultid, result, amount, version, ings'
-- perform the cargo query
---@type CargoTableRecord[]
local result = mw.ext.cargo.query('Recipes', fields, {
where = where,
groupBy = groupBy,
orderBy = orderBy,
limit = 20, -- this is enough
})
---Functions for processing the result of the cargo query. They return the
---output that is to be displayed, e.g. `handles.station` displays the
---crafting station of the extracted recipe.
---The template parameter `mode` determines which of these functions to use.
---@type { [string]: fun(): string }
local handles = {}
---Format the recipes in a short, "compact" way.
---This is the default output mode.
---@return string
handles.compact = function()
---Whether to display the `record.version` on each record.
local withVersion = not getBoolArg('noversion', false)
---Whether to display the `record.resulttext` on each record.
local withNote = not getBoolArg('nonote', false)
---The amount text will normally not be displayed if it is '1'; this
---parameter forces it even if it is '1'.
local forceAmountText = getBoolArg('full', false)
---Whether to display the `record.result` on each record.
local withResult = getBoolArg('withresult', false)
---Whether to display the `record.station` on each record.
local withStation = not getBoolArg('nostation', false)
---All of the recipes from the cargo query, each one formatted in a
---compact way and wrapped in `<span class="recipe"></span>`.
---@type string[]
local compactedRecipes = {}
-- iterate over the rows (records) of the cargo query result, format each
-- one, and put it in the `compactedRecipes` list
for index, record in ipairs(result) do
---Result item, formatted with `{{item}}`.
local resultStr = ''
if withResult then
---Arguments for the `{{item}}` call used for formatting the result item.
local itemArgs = { mode = 'image' }
if record.resultimage then
itemArgs.image = record.resultimage
end
resultStr = itemLink(record.result, itemArgs)
-- add amount text at the front
if record.amount ~= '1' or forceAmountText then
resultStr = record.amount .. ' ' .. resultStr
end
-- add note text
local resultnote = record.resulttext or ''
if withNote and resultnote ~= '' then
local note_l10n = l10n('result_note') or {}
resultnote = note_l10n[resultnote] or resultnote
resultStr = resultStr .. tag('span', { class = 'result-note note-text small' }, resultnote)
end
end
---All ingredient items, formatted with `{{item}}` and separated by '+'.
local ingredientsStr = ''
---List of ingredient strings, each in the format `itemname#i:...#n:...¦amount`.
---Iterate over each one and replace it with its formatted output via
---`{{item}}`.
local ingList = explode('^', record.args) --[[@as string[] ]]
---Arguments for the `{{item}}` call used for formatting each ingredient item.
local itemArgs = { mode = 'image' }
for i, ingredient in ipairs(ingList) do
local item, amount = ingredient:match('^(.-)¦(.-)$')
-- some old-gen recipes that contain substitutable ingredients are not
-- stored as separate recipes and so will contain ingredients like
-- `Adamantite/Titanium Bar`
-- (see [[Alternative crafting ingredients#Substitutable ingredients]])
local items = splitIngredients(item)
-- make an {{item}} for each one
for j, subItem in ipairs(items) do
items[j] = itemLink(subItem, itemArgs)
end
local ingredientStr = table.concat(items, ' / ')
-- add amount text at the front
if amount ~= '1' or forceAmountText then
ingredientStr = amount .. ' ' .. ingredientStr
end
ingList[i] = ingredientStr
end
ingredientsStr = table.concat(ingList, ' + ')
---Crafting station, formatted with `{{item}}`.
local stationStr = ''
if withStation then
stationStr = compactStation(record.station, getArg('formatStation') or l10n('compact_format_station')) or ''
end
---The entire compacted recipe.
local recipeStr = ingredientsStr
-- (default: only the ingredients and nothing else)
-- The three variable parts of the record (that are relevant for this
-- output) have all been formatted with `{{item}}` above:
-- * ingredient items (`ingredientStr`)
-- * result item (`resultStr`)
-- * crafting station (`stationStr`)
-- Now they need to be arranged for the output. This arrangement is
-- localized, because some languages might require tweaks. For example,
-- `<ingredients> = <result>` might not make much sense in some language,
-- and `<result> ← <ingredients>` might be better.
-- Furthermore, the arrangement can be customized entirely via a template
-- parameter.
-- The arrangement is provided by a `formatString`, in which there
-- can be placeholders that will be replaced by the `{{item}}`s mentioned
-- above.
---String with placeholders like `@station@` which are to be replaced
---with the respective `{{item}}`-formatted strings.
local formatString = getArg('format')
if not formatString then
-- no template parameter provided; use the default
if withStation and stationStr then
if withResult then
formatString = l10n('compact_format_recipeWithResultAndStation')
else
formatString = l10n('compact_format_recipeWithStation')
end
else
if withResult then
formatString = l10n('compact_format_recipeWithResult')
else
-- `withStation` is `false` (or `stationStr` is empty)
-- and `withResult` is `false`... so only the `ingredientStr`
-- remains, which needs no formatting
end
end
end
if formatString then
local replacement = {
['@station@'] = stationStr,
['@ingredient@'] = ingredientsStr,
['@result@'] = resultStr,
}
-- replace each occurrence of `@station@`, `@ingredient@`, and
-- `@result@` as defined above; leave all other strings unchanged,
-- like `@foo@`
recipeStr = formatString:gsub('@.-@', function(s) return replacement[s] or s end)
end
-- Add the version icons to the recipe. How they are arranged in
-- relation to the rest of the recipe (e.g. in front of the recipe,
-- behind the recipe, with a certain separator, etc.) is determined
-- by a localized formatting string (or an entirely custom one, via
-- a template parameter), similar to the process above.
if withVersion and (record.version or '') ~= '' then
local versionFormatString = getArg('formatVersion') or l10n('compact_format_WithVersion')
local replacement = {
['@version@'] = versionIcons(record.version),
['@recipe@'] = recipeStr,
}
-- replace each occurrence of `@version@` and `@recipe@` as defined
-- above; leave all other strings unchanged, like `@foo@`
recipeStr = versionFormatString:gsub('@.-@', function(s) return replacement[s] or s end)
end
-- wrap the compacted recipe in a CSS class
compactedRecipes[index] = tag('span', {class = 'recipe'}, recipeStr)
end
if #compactedRecipes == 0 then
return ''
end
local sep = getArg('sep') or getArg('seperator') or l10n('compact_default_sep')
if sep == 'null' or sep == 'nil' then
sep = ''
end
-- combine all the compacted recipes
return tag('span', { class = 'recipes extract compact' }, table.concat(compactedRecipes, sep))
end -- handles.compact
---Format the crafting ingredient items with `{{item}}`.
---@return string
handles.ingredients = function()
---Whether to display the `record.version` on each record.
local withVersion = not getBoolArg('noversion', false)
---Whether to display exclusivity icons on each ingredient's `{{item}}`.
local withVersionIcons = not getBoolArg('noversionicon', false)
---Arguments for the `{{item}}` call used for formatting each ingredient item.
local itemArgs = {}
if not withVersionIcons then
itemArgs.icons = 'n'
end
---All ingredient possibilities, with each one having all its ingredients
---formatted with `{{item}}` each, and each one wrapped in
---`<div class="ingredients"></div>`.
local ingredientStrs = {}
-- iterate over the rows (records) of the cargo query result, format each
-- one, and put it in the `rows` list
for i, record in ipairs(result) do
local ingredientStr = ingredientsCell(record.args, itemArgs)
-- add version icons
if withVersion and (record.version or '') ~= '' then
local formatString = getArg('formatVersion') or l10n('ingredients_format_WithVersion')
local replacement = {
['@version@'] = versionIcons(record.version),
['@ingredient@'] = ingredientStr,
}
-- replace each occurrence of `@version@` and `@ingredient@` as
-- defined above; leave all other strings unchanged, like `@foo@`
ingredientStr = formatString:gsub('@.-@', function(s) return replacement[s] or s end)
end
ingredientStrs[i] = tag('div', {class = 'ingredients'}, ingredientStr)
end
if #ingredientStrs == 0 then
return ''
end
local sep = getArg('sep') or getArg('seperator') or l10n('ingredients_default_sep')
if sep == 'null' or sep == 'nil' then
sep = ''
end
-- combine all the ingredient possibilities
return tag('div', { class = 'recipes extract ingredients' }, table.concat(ingredientStrs, sep))
end -- handles.ingredients
---Format the crafting station with `{{item}}`.
---@return string
handles.station = function()
---The first row (record) of the cargo query result. All other rows are
---ignored.
local record = result[1]
if not record then
-- the cargo query had no result
return ''
end
return tag('span', { class = 'recipes extract station' }, stationCell(record.station, true))
end -- handles.station
---Format the crafting station with `{{item}}` in a "compact" way.
---@return string
handles['station-compact'] = function()
---The first row (record) of the cargo query result. All other rows are
---ignored.
local record = result[1]
if not record then
-- the cargo query had no result
return ''
end
local stationStr = compactStation(record.station, getArg('formatStation') or l10n('compact_format_station'))
return tag('span', { class = 'recipes extract station' }, stationStr or '')
end -- handles['station-compact']
---Format the crafting result item with `{{item}}`.
---@return string
handles.result = function()
---The first row (record) of the cargo query result. All other rows are
---ignored.
local record = result[1]
if not record then
-- the cargo query had no result
return ''
end
---Whether to add the item ID to the result `{{item}}`.
local showResultId = getBoolArg('showresultid', false)
---Whether to link the result `{{item}}`.
local withResultLink = getBoolArg('link', true)
---The amount text will normally not be displayed if it is '1'; this
---parameter forces it even if it is '1'.
local forceAmountText = getBoolArg('full', false)
---Whether to display the `record.resulttext` on each record.
local withNote = not getBoolArg('nonote', false)
---Whether to display the `record.version` on each record.
local withVersion = not getBoolArg('noversion', false)
---Arguments for the `{{item}}` call used for formatting the result item.
local itemArgs = { nolink = not withResultLink, class = 'multi-line' }
if showResultId then
itemArgs.id = record.resultid
end
if record.resultimage then
itemArgs.image = record.resultimage
end
local resultStr = itemLink(record.result, itemArgs)
-- add amount text
if record.amount ~= '1' or forceAmountText then
---String with placeholders `@result@` and/or `@amount@` which are
---to be replaced with the respective data.
local formatString = getArg('format') or l10n('result_format_WithAmount')
local replacement = {
['@result@'] = resultStr,
['@amount@'] = record.amount,
}
-- replace each occurrence of `@result@` and `@amount@` as defined
-- above; leave all other strings unchanged, like `@foo@`
resultStr = formatString:gsub('@.-@', function(s) return replacement[s] or s end)
end
-- add note text
local resultnote = record.resulttext or ''
if withNote and resultnote ~= '' then
local note_l10n = l10n('result_note') or {}
resultnote = note_l10n[resultnote] or resultnote
resultStr = resultStr .. tag('span', { class = 'result-note note-text small' }, resultnote)
end
-- add version icons
if withVersion and (record.version or '') ~= '' then
---String with placeholders `@result@` and/or `@version@` which are
---to be replaced with the respective data.
local formatString = getArg('formatVersion') or l10n('result_format_WithVersion')
local replacement = {
['@result@'] = resultStr,
['@version@'] = versionIcons(record.version),
}
-- replace each occurrence of `@result@` and `@version@` as defined
-- above; leave all other strings unchanged, like `@foo@`
resultStr = formatString:gsub('@.-@', function(s) return replacement[s] or s end)
end
return tag('span', { class = 'recipes extract result' }, resultStr)
end -- handles.result
---Format the crafting result item amount.
---@return string
handles.amount = function()
---The first row (record) of the cargo query result. All other rows are
---ignored.
local record = result[1]
if not record then
-- the cargo query had no result
return ''
end
return tag('span', { class = 'recipes extract amount' }, record.amount)
end -- handles.amount
---Add the buy prices of all ingredient items (multiplied by their amounts).
---@return string
handles['ingredients-buy'] = function()
---The first row (record) of the cargo query result. All other rows are
---ignored.
local record = result[1]
if not record then
-- the cargo query had no result
return ''
end
---Combined buy price of all ingredient items.
local value = 0
for _, v in ipairs(explode('^', record.ings)) do
local item, amount = v:match('^¦(.-)¦(.-)$')
local itemId = tonumber(itemNameData('itemIdFromName', item)) or 0
local itemValue = getItemStat(itemId, 'value')
value = value + itemValue * amount
end
return tostring(value)
end -- handles['ingredients-buy']
---Add the sell values of all ingredient items (multiplied by their amounts).
---@return string
handles['ingredients-sell'] = function()
---The first row (record) of the cargo query result. All other rows are
---ignored.
local record = result[1]
if not record then
-- the cargo query had no result
return ''
end
---Combined sell value of all ingredient items.
local value = 0
for _, v in ipairs(explode('^', record.ings)) do
local item, amount = v:match('^¦(.-)¦(.-)$')
local itemId = tonumber(itemNameData('itemIdFromName', item)) or 0
local itemValue = math.floor(getItemStat(itemId, 'value') / 5)
value = value + itemValue * amount
end
return tostring(value)
end -- handles['ingredients-sell']
-- function name aliases:
handles.resultamount = handles.amount
-- display the result using the specified `handles` function ("mode")
local mode = getArg('mode') or 'compact'
if handles[mode] then
return (handles[mode])()
else
return currentFrame:expandTemplate{ title = 'error', args = {
'Recipes/extract: Invalid mode',
from = 'Recipes',
inline = 'y'
}}
end
end -- p.extract
---For `{{recipes/count}}`: query the cargo table and return the number of rows.
---@param frame table Interface to the parser (`mw.frame`)
---@return integer
p.count = function(frame)
local args = frame:getParent().args
local where = trim(args['where'] or '')
if where == '' then
where = makeConstraints(args)
end
-- if there are no constraints, then there can never be any results
if where == '' then
return 0
end
-- perform the cargo query
-- since we must use `GROUP BY` to eliminate duplicates, we can not use
-- `COUNT()` to get the number of rows directly; and `COUNT(DISTINCT ...)`
-- will also not yield the correct result due to the `HOLDS` keyword
local result = mw.ext.cargo.query('Recipes', 'resultid', {
where = where,
groupBy = 'resultid, result, amount, ings, version',
limit = 2000,
})
return #result -- number of rows
end -- p.count
---For `{{recipes/exist}}`: query the cargo table and return whether it yields any rows at all.
---@param frame table Interface to the parser (`mw.frame`)
---@return ''|'yes' exists Blank if no row
p.exist = function(frame)
local args = frame:getParent().args
local where = trim(args['where'] or '')
if where == '' then
where = makeConstraints(args)
end
-- if there are no constraints, then there can never be any results
if where == '' then
return ''
end
-- perform the cargo query
local result = mw.ext.cargo.query('Recipes', 'result', {
where = where,
limit = 1, -- this is enough because we only want to see if there are any rows at all
})
return result[1] and 'yes' or ''
end -- p.exist
return p