Minecraft Wiki
(Guess this wasn't a number?)
m (Revert)
(22 intermediate revisions by 5 users not shown)
Line 1: Line 1:
 
local p = {}
 
local p = {}
function p.dpl( f )
 
local args = f:getParent().args
 
local slot = require( 'Module:Inventory slot' )
 
local ingredients = args[1] and mw.text.split( args[1], '%s*,%s*' ) or { mw.title.getCurrentTitle().text }
 
local matchTypes = args.match and args.match:find( ',' ) and mw.text.split( args.match, '%s*,%s*' ) or args.match
 
   
local argList = {
+
local i18n = {
  +
emptyCategory = 'Empty crafting usage',
'ignoreusage', 'upcoming', 'name', 'ingredients', 'arggroups',
 
  +
moduleCrafting = [[Module:Crafting]],
1, 2, 3, 4, 5, 6, 7, 8, 9,
 
  +
moduleSlot = [[Module:Inventory slot]],
'A1', 'B1', 'C1', 'A2', 'B2', 'C2', 'A3', 'B3', 'C3',
 
  +
moduleText = [[Module:Text]],
'Output', 'description', 'fixed', 'notfixed',
 
  +
queryCategory = 'Recipe using $1',
'A1title', 'A1link', 'B1title', 'B1link', 'C1title', 'C1link',
 
  +
templateCrafting = 'Crafting',
'A2title', 'A2link', 'B2title', 'B2link', 'C2title', 'C2link',
 
  +
}
'A3title', 'A3link', 'B3title', 'B3link', 'C3title', 'C3link',
 
  +
p.i18n = i18n
'Otitle', 'Olink',
 
}
 
local anonToShaped = { 'A1', 'B1', 'C1', 'A2', 'B2', 'C2', 'A3', 'B3', 'C3' }
 
local shapedToAnon = { A1 = 1, B1 = 2, C1 = 3, A2 = 4, B2 = 5, C2 = 6, A3 = 7, B3 = 8, C3 = 9 }
 
   
local data = ''
+
local text = require( i18n.moduleText )
  +
local slot = require( i18n.moduleSlot )
if args.category then
 
  +
local crafting = require( i18n.moduleCrafting )
data = f:callParserFunction( '#dpl:', {
 
  +
local argList = {
category = args.category,
 
  +
'ignoreusage', 'upcoming', 'name', 'ingredients', 'arggroups',
nottitleregexp = args.ignore,
 
  +
1, 2, 3, 4, 5, 6, 7, 8, 9,
include = '{Crafting}:' .. table.concat( argList, ':' ),
 
  +
'A1', 'B1', 'C1', 'A2', 'B2', 'C2', 'A3', 'B3', 'C3',
mode = 'userformat',
 
  +
'Output', 'description', 'fixed', 'notfixed',
secseparators = '====',
 
  +
'A1title', 'A1link', 'B1title', 'B1link', 'C1title', 'C1link',
multisecseparators = '====',
 
  +
'A2title', 'A2link', 'B2title', 'B2link', 'C2title', 'C2link',
} )
 
  +
'A3title', 'A3link', 'B3title', 'B3link', 'C3title', 'C3link',
else
 
  +
'Otitle', 'Olink',
-- #dpl has a limit of four, so do it in chunks of 4
 
  +
'%PAGE%',
for i = 1, #ingredients, 4 do
 
  +
}
data = data .. f:callParserFunction( '#dpl:', {
 
  +
local prefixes = slot.i18n.prefixes
category = 'Recipe using ' .. table.concat( ingredients, '|Recipe using ', i, math.min( i + 3, #ingredients ) ),
 
  +
nottitleregexp = args.ignore,
 
  +
local function map(tbl, func)
include = '{Crafting}:' .. table.concat( argList, ':' ),
 
  +
local newtbl = {}
mode = 'userformat',
 
  +
for i,v in pairs(tbl) do
secseparators = '====',
 
  +
newtbl[i] = func(v)
multisecseparators = '====',
 
  +
end
} )
 
  +
return newtbl
  +
end
  +
  +
-- Flatten a nested array, only doing the numerically-indexed parts.
  +
local function flatten(tbl)
  +
local newtbl = {}
  +
local function _flat(arr)
  +
for _, v in ipairs(arr) do
  +
if type(v) == "table" and v[1] then
  +
_flat(v)
  +
else
  +
table.insert(newtbl, v)
  +
end
  +
end
  +
end
  +
_flat(tbl)
  +
return newtbl
  +
end
  +
  +
--[[Escapes special characters in ingredient names, and returns the correct
  +
pattern depending on the match type
  +
--]]
  +
local function createIngredientPatterns( ingredients, matchTypes )
  +
local patternChars = {
  +
['^'] = '%^';
  +
['$'] = '%$';
  +
['('] = '%(';
  +
[')'] = '%)';
  +
['%'] = '%%';
  +
['.'] = '%.';
  +
['['] = '%[';
  +
[']'] = '%]';
  +
['*'] = '%*';
  +
['+'] = '%+';
  +
['-'] = '%-';
  +
['?'] = '%?';
  +
['\0'] = '%z';
  +
}
  +
local patterns = {}
  +
for i, ingredient in ipairs( ingredients ) do
  +
local escaped = ingredient:gsub( '([^%w])', patternChars )
  +
if not matchTypes then
  +
patterns[i] = '%z' .. escaped .. '%z'
  +
else
  +
local matchType = matchTypes[i] or matchTypes
  +
if matchType == 'start' then
  +
patterns[i] = '%z' .. escaped
  +
elseif matchType == 'end' then
  +
patterns[i] = escaped .. '%z'
  +
else
  +
patterns[i] = escaped
  +
end
 
end
 
end
 
end
 
end
 
 
  +
return patterns
-- Comment this next line out if you're not using aliases
 
  +
end
local aliases = mw.loadData( 'Module:Inventory slot/Aliases' )
 
  +
 
  +
--[[Extracts the anonymous pipe-delimited arguments from the
local function matchPattern( ingredient, ingredientNum )
 
  +
DPL query into a table with the corresponding keys, skipping
local matchType = matchTypes
 
  +
templates with `ignoreusage` set, and skipping duplicate templates
if type( matchType ) == 'table' then
 
  +
--]]
matchType = matchTypes[ingredientNum]
 
  +
  +
local extractArgs
  +
do
  +
local seen = {}
  +
extractArgs = function( template )
  +
-- Check for duplicate template or `ignoreusage` arg
  +
if seen[template] or not template:find( '^\n|' ) then
  +
return
 
end
 
end
  +
local pattern
 
  +
seen[template] = true
local escaped = ingredient:gsub( '([%(%)])', '%%%1' )
 
  +
if matchType == 'start' then
 
  +
local tArgs = {}
pattern = '[;:%]]%s*' .. escaped
 
  +
local i = 1
elseif matchType == 'end' then
 
  +
for arg in text.gsplit( template, '\n|' ) do
pattern = escaped .. '%s*[,;%[]'
 
elseif matchType == 'any' then
+
if arg ~= '' then
  +
tArgs[argList[i]] = arg
pattern = escaped
 
else
+
end
  +
i = i + 1
pattern = '[;:%]]%s*' .. escaped .. '%s*[,;%[]'
 
 
end
 
end
 
 
  +
tArgs.nocat = '1'
return pattern
 
  +
  +
return tArgs
 
end
 
end
  +
end
   
  +
--[[Loops through the crafting args and parses them, with alias reference data
local function compareTables( a, b )
 
  +
for k, v in pairs( a ) do
 
  +
Identical slots reuse the same table, to allow them to be compared like strings
if type( b[k] ) ~= type( v ) then
 
  +
--]]
return false
 
  +
local function parseCraftingArgs( cArgs )
  +
local parsedFrameText = {}
  +
local parsedCArgs = {}
  +
for arg, frameText in pairs( cArgs ) do
  +
if frameText then
  +
local randomise = arg == 'Output' and 'never' or nil
  +
local frames = not randomise and parsedFrameText[frameText]
  +
if not frames then
  +
frames = slot.parseFrameText( frameText, randomise, true )
  +
parsedFrameText[frameText] = frames
 
end
 
end
  +
parsedCArgs[arg] = frames
if type( v ) == 'table' then
 
  +
end
if not compareTables( v, b[k] ) then
 
  +
end
return false
 
  +
  +
return parsedCArgs
  +
end
  +
  +
-- Loops through the wanted ingredients, and checks if the name contains it
  +
-- REQUIREMENT: name starts and ends with the NUL (\0) character. Simplifies operation
  +
-- for multiple names in the variable.
  +
local function containsIngredients( name, ingredientPatterns )
  +
for _, ingredient in pairs( ingredientPatterns ) do
  +
if name:find( ingredient ) then
  +
return true
  +
end
  +
end
  +
  +
return false
  +
end
  +
  +
--[[Loops through the crafting ingredients and find which parameters and
  +
frames contain the wanted ingredients
  +
  +
Returns a table if any matches are found, the table contains tables of
  +
required frame numbers, or true if all of them are required
  +
--]]
  +
local function findRequiredFrameNums( parsedCArgs, ingredientPatterns )
  +
local requiredFrameNums = {}
  +
local hasRequiredFrames
  +
for arg, frames in pairs( parsedCArgs ) do
  +
if arg ~= 'Output' then
  +
local requiredFrames = {}
  +
local count = 0
  +
for i, frame in ipairs( frames ) do
  +
-- Guess what? If we only take the first we lose the subframes.
  +
-- And then 'Cobblestone or Blackstone' starts breaking.
  +
local tframe = frame[1] and flatten(frame) or { frame }
  +
local names = '\0' .. table.concat(map(tframe, function (fr) return type(fr) == 'table' and fr.name or '' end), '\0') .. '\0'
  +
if containsIngredients( names, ingredientPatterns ) then
  +
requiredFrames[i] = true
  +
count = count + 1
 
end
 
end
  +
end
elseif v ~= b[k] then
 
  +
if count > 0 then
return false
 
  +
if count == #frames then
  +
return true
  +
end
  +
  +
hasRequiredFrames = true
  +
requiredFrames.count = count
  +
requiredFrameNums[arg] = requiredFrames
 
end
 
end
 
end
 
end
  +
end
for k, v in pairs( b ) do
 
  +
if a[k] == nil then
 
  +
return hasRequiredFrames and requiredFrameNums
return false
 
  +
end
  +
  +
--[[Generates the argument groups, either using the template's specified
  +
groups, or automatically based on the number of frames in each slot
  +
--]]
  +
local function generateArgGroups( predefinedArgGroups, parsedCArgs )
  +
local argGroups = {}
  +
if predefinedArgGroups or '' ~= '' then
  +
local i = 1
  +
for argGroup in text.gsplit( predefinedArgGroups, '%s*;%s*' ) do
  +
local groupData = { args = {} }
  +
for arg in text.gsplit( argGroup, '%s*,%s*' ) do
  +
arg = tonumber( arg ) or arg
  +
if not groupData.count then
  +
groupData.count = #parsedCArgs[arg]
  +
end
  +
groupData.args[arg] = true
  +
end
  +
argGroups[i] = groupData
  +
i = i + 1
  +
end
  +
else
  +
for arg, frames in pairs( parsedCArgs ) do
  +
local framesLen = #frames
  +
if framesLen > 0 then
  +
local groupName = framesLen
  +
local alias = frames.aliasReference and frames.aliasReference[1]
  +
if alias and alias.length == framesLen and
  +
alias.frame.name:find( '^' .. prefixes.any .. ' ' )
  +
then
  +
groupName = alias.frame.name
  +
end
  +
  +
local groupData = argGroups[groupName]
  +
if not groupData then
  +
groupData = {
  +
args = {},
  +
count = framesLen
  +
}
  +
argGroups[groupName] = groupData
  +
end
  +
groupData.args[arg] = true
 
end
 
end
 
end
 
end
return true
 
 
end
 
end
  +
  +
return argGroups
  +
end
   
  +
--[[Adds together the required frames from each slot in this group
local out = {}
 
  +
to get the total amount of frames which are relevant
local showDesciption
 
  +
local templates = {}
 
  +
Returns a table with the relevant frame numbers, if any are relevant
for template in mw.text.gsplit( data, '====' ) do
 
  +
--]]
-- If ignoreusage is empty
 
  +
local function findRelevantFrameNums( requiredFrameNumData, group )
if template:find( '^%s*|' ) then
 
local tArgs = {}
+
local relevantFrameNums = {}
local i = 0
+
local hasRelevantFrames
  +
for arg in pairs( group ) do
-- Extract the arguments from the DPL query
 
  +
local requiredFrameNums = requiredFrameNumData[arg]
for tArg in mw.text.gsplit( template, '\n|' ) do
 
  +
if requiredFrameNums and arg ~= 'Output' then
i = i + 1
 
  +
for frameNum in pairs( requiredFrameNums ) do
if tArg ~= '' then
 
  +
-- Have to use pairs as it contains a non-sequential set of numbers
local key = argList[i]
 
  +
-- so we have to skip over the extra data in the table
tArgs[key] = tArg
 
  +
if frameNum ~= 'count' then
  +
hasRelevantFrames = true
  +
relevantFrameNums[frameNum] = true
 
end
 
end
 
end
 
end
 
 
  +
relevantFrameNums.count = math.max(
local craftingArgs = {
 
  +
requiredFrameNums.count or 0,
tArgs[1] or tArgs.A1 or '', tArgs[2] or tArgs.B1 or '', tArgs[3] or tArgs.C1 or '',
 
  +
relevantFrameNums.count or 0
tArgs[4] or tArgs.A2 or '', tArgs[5] or tArgs.B2 or '', tArgs[6] or tArgs.C2 or '',
 
  +
)
tArgs[7] or tArgs.A3 or '', tArgs[8] or tArgs.B3 or '', tArgs[9] or tArgs.C3 or '',
 
  +
end
Output = tArgs.Output
 
  +
end
}
 
  +
 
  +
return hasRelevantFrames and relevantFrameNums
local expandedFrames = {}
 
  +
end
local hasIngredient
 
  +
local argsWithIngredient = {}
 
  +
--[[Loops through the relevant frame numbers and extracts them
local argGroups = {}
 
  +
into a new table, taking care of moving any alias references
for i, v in pairs( craftingArgs ) do
 
  +
and cleaning up any unnecessary subframes
if v ~= '' then
 
  +
--]]
if aliases then
 
  +
function p.extractRelevantFrames( relevantFrameNums, frames )
expandedFrames[i] = {}
 
  +
local relevantFrames = { randomise = frames.randomise }
local expandedFrame = {}
 
  +
local newFrameNum = 1
for frame in mw.text.gsplit( v, '%s*;%s*' ) do
 
  +
for frameNum, frame in ipairs( frames ) do
local parts = slot.getParts( frame )
 
  +
local relevantFrame = relevantFrameNums == true or relevantFrameNums[frameNum]
local alias = aliases[parts.name]
 
if alias then
+
if relevantFrame then
  +
if not frame[1] then
local expandedAlias = slot.expandAlias( parts, alias ):gsub( '%s*([%[%]:,;])%s*', '%1' )
 
  +
local alias = frames.aliasReference and frames.aliasReference[frameNum]
expandedFrames[i][frame] = expandedAlias:gsub( '([%(%)])', '%%%1' )
 
  +
local moveAlias = true
table.insert( expandedFrame, expandedAlias )
 
  +
if alias and relevantFrameNums ~= true then
else
 
  +
for i = frameNum, alias.length do
table.insert( expandedFrame, frame )
 
  +
if not relevantFrameNums[i] then
end
 
  +
moveAlias = false
  +
break
 
end
 
end
 
v = table.concat( expandedFrame, ';' )
 
craftingArgs[i] = v
 
 
end
 
end
  +
end
if i ~= 'Output' then
 
  +
if alias and moveAlias then
local delimitedFrames = ';' .. v .. ';'
 
  +
if not relevantFrames.aliasReference then
for ingredientNum, ingredient in pairs( ingredients ) do
 
  +
relevantFrames.aliasReference = {}
if delimitedFrames:find( matchPattern( ingredient, ingredientNum ) ) then
 
if not v:find( ';' ) then
 
hasIngredient = 'static'
 
elseif not hasIngredient then
 
hasIngredient = 'animated'
 
end
 
 
argsWithIngredient[i] = true
 
end
 
end
 
end
 
 
if not tArgs.arggroups and hasIngredient ~= 'static' then
 
local _, frameCount = v:gsub( ';', '' )
 
if frameCount > 0 then
 
frameCount = frameCount + 1
 
local group = argGroups[frameCount]
 
if not group then
 
group = { args = {} }
 
argGroups[frameCount] = group
 
end
 
group.count = frameCount
 
group.args[i] = true
 
end
 
 
end
 
end
  +
relevantFrames.aliasReference[newFrameNum] = alias
 
end
 
end
 
end
 
end
 
 
  +
relevantFrames[newFrameNum] = frame
if hasIngredient then
 
  +
newFrameNum = newFrameNum + 1
if tArgs.description then
 
  +
end
showDescription = true
 
end
+
end
  +
 
  +
-- Move frames in subframe to main frames, if the subframe
if hasIngredient == 'animated' then
 
  +
-- is the only frame
if tArgs.arggroups then
 
  +
if not relevantFrames[2] and relevantFrames[1][1] then
for argGroup in mw.text.gsplit( tArgs.arggroups, '%s*;%s*' ) do
 
  +
relevantFrames = relevantFrames[1]
local group = {}
 
  +
end
local _, frameCount
 
  +
for arg in mw.text.gsplit( argGroup, '%s*,%s*' ) do
 
  +
return relevantFrames
if arg:match( '^%d$' ) then
 
  +
end
arg = tonumber( arg )
 
end
 
if not tArgs[1] then
 
arg = shapedToAnon[arg]
 
end
 
if not frameCount then
 
_, frameCount = craftingArgs[arg]:gsub( ';', '' )
 
end
 
group[arg] = true
 
end
 
table.insert( argGroups, { count = frameCount + 1, args = group } )
 
end
 
end
 
 
for _, groupData in pairs( argGroups ) do
 
local frameCount = groupData.count
 
local group = groupData.args
 
local requiredFrames = {}
 
local requiredFramesCount = 0
 
for arg in pairs( group ) do
 
if argsWithIngredient[arg] then
 
local frames = craftingArgs[arg]
 
local frameNum = 0
 
for frame in mw.text.gsplit( frames, '%s*;%s*' ) do
 
frameNum = frameNum + 1
 
if not requiredFrames[frameNum] then
 
local delimitedFrame = ';' .. frame .. ';'
 
for ingredientNum, ingredient in pairs( ingredients ) do
 
if delimitedFrame:find( matchPattern( ingredient, ingredientNum ) ) then
 
requiredFrames[frameNum] = true
 
requiredFramesCount = requiredFramesCount + 1
 
end
 
end
 
end
 
end
 
end
 
end
 
 
-- Not all frames will be used
 
if requiredFramesCount > 0 and requiredFramesCount < frameCount then
 
for arg in pairs( group ) do
 
local frames = craftingArgs[arg]
 
local newFrames = {}
 
local frameNum = 0
 
for frame in mw.text.gsplit( frames, '%s*;%s*' ) do
 
frameNum = frameNum + 1
 
if requiredFrames[frameNum] then
 
table.insert( newFrames, frame )
 
end
 
end
 
newFrames = table.concat( newFrames, ';' )
 
 
-- If the whole expanded alias survived, collapse it again
 
if expandedFrames[arg] then
 
for frame, expandedAlias in pairs( expandedFrames[arg] ) do
 
--newFrames = 'blah' .. expandedAlias
 
newFrames = newFrames:gsub( expandedAlias, frame )
 
end
 
end
 
 
local tArg = arg
 
if arg ~= 'Output' and not tArgs[1] then
 
tArg = anonToShaped[arg]
 
end
 
tArgs[tArg] = newFrames
 
end
 
 
-- Let Module:Crafting handle the name and ingredients columns
 
tArgs.name = nil
 
tArgs.ingredients = nil
 
end
 
end
 
end
 
 
tArgs.nocat = '1'
 
   
  +
--[[Works out what data is relevant to the requested ingredients
local found = false
 
  +
for i, v in ipairs( templates ) do
 
  +
If the template contains any of the ingredients, returns it with any
if compareTables( v, tArgs ) then
 
  +
necessary modifications, and with the crafting arguments parsed
found = true
 
  +
--]]
break
 
  +
function p.processTemplate( tArgs, ingredientPatterns )
end
 
  +
local cArgs = {}
  +
for i, v in pairs( crafting.cArgVals ) do
  +
cArgs[i] = tArgs[i] or tArgs[v]
  +
end
  +
cArgs.Output = tArgs.Output
  +
local parsedCArgs = parseCraftingArgs( cArgs )
  +
  +
local requiredFrameNumData = findRequiredFrameNums( parsedCArgs, ingredientPatterns )
  +
if not requiredFrameNumData then
  +
return
  +
end
  +
  +
local newCArgs
  +
local modified
  +
if requiredFrameNumData == true then
  +
newCArgs = parsedCArgs
  +
else
  +
local argGroups = generateArgGroups( tArgs.arggroups, parsedCArgs )
  +
newCArgs = {}
  +
for _, groupData in pairs( argGroups ) do
  +
local group = groupData.args
  +
local relevantFrameNums = findRelevantFrameNums( requiredFrameNumData, group )
  +
if not relevantFrameNums then
  +
for arg in pairs( group ) do
  +
newCArgs[arg] = parsedCArgs[arg]
 
end
 
end
  +
else
if not found then
 
  +
modified = true
table.insert( templates, tArgs )
 
  +
for arg in pairs( group ) do
  +
newCArgs[arg] = p.extractRelevantFrames( relevantFrameNums, parsedCArgs[arg] )
 
end
 
end
 
end
 
end
 
end
 
end
 
end
 
end
  +
if #templates == 0 then
 
  +
-- Convert arguments back to shapeless format if they were originally
return '[[Category:Empty crafting usage]]'
 
  +
if tArgs[1] then
  +
local i = 1
  +
for argNum = 1, 9 do
  +
tArgs[argNum] = nil
  +
local cArg = newCArgs[argNum]
  +
if cArg then
  +
tArgs[i] = cArg
  +
i = i + 1
  +
end
  +
end
  +
else
  +
for i, arg in pairs( crafting.cArgVals ) do
  +
tArgs[arg] = newCArgs[i]
  +
end
 
end
 
end
  +
tArgs.Output = newCArgs.Output
  +
tArgs.parsed = true
 
 
  +
-- Let Module:Recipe table generate these
templates[1].head = '1'
 
  +
-- with the modified crafting args
templates[1].showname = '1'
 
  +
if modified then
if showDescription and args.showdesciption ~= '0' or args.showdesciption == '1' then
 
  +
tArgs.name = nil
templates[1].showdescription = '1'
 
  +
tArgs.ingredients = nil
 
end
 
end
  +
  +
return tArgs
  +
end
  +
  +
--[[Works out which frame is the first frame, and returns its
  +
name, or alias name, for sorting purposes
  +
--]]
  +
function p.getFirstFrameName( frames, subframe )
  +
local frame = frames[1]
  +
if not subframe and frame[1] then
  +
return p.getFirstFrameName( frame[1], true )
  +
end
  +
  +
local alias = frames.aliasReference and frames.aliasReference[1]
  +
return alias and alias.frame.name or frame.name
  +
end
  +
  +
--[[Performs the DPL query which retrieves the arguments from the crafting
  +
templates on the pages within the requested categories.
  +
If more than four categories are given, break them down into batches of four.
  +
--]]
  +
local function dplQueryWrapper( category, ignore )
  +
local data = {}
  +
if type(category) == 'string' then
  +
category = mw.text.split(category, '|')
  +
else
  +
assert(type(category) == 'table')
  +
end
  +
  +
local includeStr = '{' .. i18n.templateCrafting .. '}:' .. table.concat( argList, ':' )
  +
local j = 1
  +
local count = #category
  +
local catParts = text.split( i18n.queryCategory, '%$1' )
  +
for i = 1, count, 4 do
  +
local catSlice = table.concat(category, '|', i, math.min(i + 3, count))
  +
data[j] = mw.getCurrentFrame():callParserFunction( '#dpl:', {
  +
category = catSlice,
  +
nottitleregexp = ignore,
  +
include = includeStr,
  +
mode = 'userformat',
  +
secseparators = '====',
  +
multisecseparators = '===='
  +
})
  +
j = j + 1
  +
end
  +
  +
return table.concat(data)
  +
end
  +
  +
  +
--[[The main body, which retrieves the data, and returns the relevant
  +
crafting templates, sorted alphabetically
  +
--]]
  +
function p.dpl( f )
  +
local args = f
  +
if f == mw.getCurrentFrame() then
  +
args = f:getParent().args
  +
else
  +
f = mw.getCurrentFrame()
  +
end
  +
local ingredients = args[1] and text.split( args[1], '%s*,%s*' ) or { mw.title.getCurrentTitle().text }
  +
local matchTypes = args.match and args.match:find( ',' ) and text.split( args.match, '%s*,%s*' ) or args.match
  +
local ingredientPatterns = createIngredientPatterns( ingredients, matchTypes )
  +
  +
local data
  +
if args.category then
  +
data = dplQueryWrapper( args.category, args.ignore )
  +
else
  +
-- Need to format the catparts
  +
local catParts = text.split( i18n.queryCategory, '%$1' )
  +
local cats = map(ingredients, function (s)
  +
return catParts[1] .. s .. catParts[2]
  +
end)
  +
data = dplQueryWrapper( cats, args.ignore )
  +
end
  +
  +
local showDescription
  +
local templates = {}
  +
local i = 1
  +
for templateArgs in text.gsplit( data:sub( 5 ), '====' ) do
  +
local tArgs = extractArgs( templateArgs )
  +
local newArgs = tArgs and p.processTemplate( tArgs, ingredientPatterns )
  +
if newArgs then
  +
if tArgs.description then
  +
showDescription = '1'
  +
end
  +
  +
templates[i] = {
  +
args = newArgs,
  +
sortKey = mw.ustring.lower(
  +
( newArgs.name or p.getFirstFrameName( newArgs.Output ) )
  +
:gsub( '^' .. prefixes.any .. ' ', '' )
  +
:gsub( '^' .. prefixes.matching .. ' ', '' )
  +
:gsub( '^%[%[', '' )
  +
:gsub( '^[^|]+|', '' )
  +
),
  +
}
  +
i = i + 1
  +
end
  +
end
  +
  +
local templateCount = #templates
  +
if templateCount == 0 then
  +
return f:expandTemplate{ title = 'Translation category', args = { i18n.emptyCategory, project = '0' } }
  +
end
  +
  +
table.sort( templates, function( a, b )
  +
return a.sortKey < b.sortKey
  +
end )
  +
  +
local initialArgs = templates[1].args
  +
initialArgs.head = '1'
  +
initialArgs.showname = '1'
  +
initialArgs.showdescription = showDescription
 
if not args.continue then
 
if not args.continue then
templates[#templates].foot = '1'
+
templates[templateCount].args.foot = '1'
 
end
 
end
 
 
local crafting = require( 'Module:Crafting' )
 
 
local out = {}
 
local out = {}
for i, v in ipairs( templates ) do
+
for i, template in ipairs( templates ) do
  +
out[i] = ( '<!-- [[' .. template.args['%PAGE%'] .. ']] -->\n'
table.insert( out, crafting.table( v ) )
 
  +
.. crafting.table( template.args ) )
 
end
 
end
 
 
return table.concat( out, '\n' )
 
return table.concat( out, '\n' )
 
end
 
end
  +
 
return p
 
return p

Revision as of 16:15, 6 November 2020

[view | edit | history | purge]DocumentationJump to code ↴

This module implements {{crafting usage}}.

Dependencies

[view | edit | history | purge]The above documentation is transcluded from Module:Crafting usage/doc.
local p = {}

local i18n = {
	emptyCategory = 'Empty crafting usage',
	moduleCrafting = [[Module:Crafting]],
	moduleSlot = [[Module:Inventory slot]],
	moduleText = [[Module:Text]],
	queryCategory = 'Recipe using $1',
	templateCrafting = 'Crafting',
}
p.i18n = i18n

local text = require( i18n.moduleText )
local slot = require( i18n.moduleSlot )
local crafting = require( i18n.moduleCrafting )
local argList = {
	'ignoreusage', 'upcoming', 'name', 'ingredients', 'arggroups',
	1, 2, 3, 4, 5, 6, 7, 8, 9,
	'A1', 'B1', 'C1', 'A2', 'B2', 'C2', 'A3', 'B3', 'C3',
	'Output', 'description', 'fixed', 'notfixed',
	'A1title', 'A1link', 'B1title', 'B1link', 'C1title', 'C1link',
	'A2title', 'A2link', 'B2title', 'B2link', 'C2title', 'C2link',
	'A3title', 'A3link', 'B3title', 'B3link', 'C3title', 'C3link',
	'Otitle', 'Olink',
	'%PAGE%',
}
local prefixes = slot.i18n.prefixes

local function map(tbl, func)
	local newtbl = {}
    for i,v in pairs(tbl) do
        newtbl[i] = func(v)
    end
    return newtbl
end

-- Flatten a nested array, only doing the numerically-indexed parts.
local function flatten(tbl)
	local newtbl = {}
	local function _flat(arr)
		for _, v in ipairs(arr) do
			if type(v) == "table" and v[1] then
				_flat(v)
			else
				table.insert(newtbl, v)
			end
		end
	end
	_flat(tbl)
	return newtbl
end

--[[Escapes special characters in ingredient names, and returns the correct
	pattern depending on the match type
--]]
local function createIngredientPatterns( ingredients, matchTypes )
	local patternChars = {
		['^'] = '%^';
		['$'] = '%$';
		['('] = '%(';
		[')'] = '%)';
		['%'] = '%%';
		['.'] = '%.';
		['['] = '%[';
		[']'] = '%]';
		['*'] = '%*';
		['+'] = '%+';
		['-'] = '%-';
		['?'] = '%?';
		['\0'] = '%z';
	}
	local patterns = {}
	for i, ingredient in ipairs( ingredients ) do
		local escaped = ingredient:gsub( '([^%w])', patternChars )
		if not matchTypes then
			patterns[i] = '%z' .. escaped .. '%z'
		else
			local matchType = matchTypes[i] or matchTypes
			if matchType == 'start' then
				patterns[i] = '%z' .. escaped
			elseif matchType == 'end' then
				patterns[i] = escaped .. '%z'
			else
				patterns[i] = escaped
			end
		end
	end
	
	return patterns
end

--[[Extracts the anonymous pipe-delimited arguments from the
	DPL query into a table with the corresponding keys, skipping
	templates with `ignoreusage` set, and skipping duplicate templates
--]]

local extractArgs
do
	local seen = {}
	extractArgs = function( template )
		-- Check for duplicate template or `ignoreusage` arg
		if seen[template] or not template:find( '^\n|' ) then
			return
		end
		
		seen[template] = true
		
		local tArgs = {}
		local i = 1
		for arg in text.gsplit( template, '\n|' ) do
			if arg ~= '' then
				tArgs[argList[i]] = arg
			end
			i = i + 1
		end
		
		tArgs.nocat = '1'
		
		return tArgs
	end
end

--[[Loops through the crafting args and parses them, with alias reference data
	
	Identical slots reuse the same table, to allow them to be compared like strings
--]]
local function parseCraftingArgs( cArgs )
	local parsedFrameText = {}
	local parsedCArgs = {}
	for arg, frameText in pairs( cArgs ) do
		if frameText then
			local randomise = arg == 'Output' and 'never' or nil
			local frames = not randomise and parsedFrameText[frameText]
			if not frames then
				frames = slot.parseFrameText( frameText, randomise, true )
				parsedFrameText[frameText] = frames
			end
			parsedCArgs[arg] = frames
		end
	end
	
	return parsedCArgs
end

-- Loops through the wanted ingredients, and checks if the name contains it
-- REQUIREMENT: name starts and ends with the NUL (\0) character. Simplifies operation
-- for multiple names in the variable.
local function containsIngredients( name, ingredientPatterns )
	for _, ingredient in pairs( ingredientPatterns ) do
		if name:find( ingredient ) then
			return true
		end
	end
	
	return false
end

--[[Loops through the crafting ingredients and find which parameters and
	frames contain the wanted ingredients
	
	Returns a table if any matches are found, the table contains tables of
	required frame numbers, or true if all of them are required
--]]
local function findRequiredFrameNums( parsedCArgs, ingredientPatterns )
	local requiredFrameNums = {}
	local hasRequiredFrames
	for arg, frames in pairs( parsedCArgs ) do
		if arg ~= 'Output' then
			local requiredFrames = {}
			local count = 0
			for i, frame in ipairs( frames ) do
				-- Guess what? If we only take the first we lose the subframes.
				-- And then 'Cobblestone or Blackstone' starts breaking.
				local tframe = frame[1] and flatten(frame) or { frame }
				local names = '\0' .. table.concat(map(tframe, function (fr) return type(fr) == 'table' and fr.name or '' end), '\0') .. '\0'
				if containsIngredients( names, ingredientPatterns ) then
					requiredFrames[i] = true
					count = count + 1
				end
			end
			if count > 0 then
				if count == #frames then
					return true
				end
				
				hasRequiredFrames = true
				requiredFrames.count = count
				requiredFrameNums[arg] = requiredFrames
			end
		end
	end
	
	return hasRequiredFrames and requiredFrameNums
end

--[[Generates the argument groups, either using the template's specified
	groups, or automatically based on the number of frames in each slot
--]]
local function generateArgGroups( predefinedArgGroups, parsedCArgs )
	local argGroups = {}
	if predefinedArgGroups or '' ~= '' then
		local i = 1
		for argGroup in text.gsplit( predefinedArgGroups, '%s*;%s*' ) do
			local groupData = { args = {} }
			for arg in text.gsplit( argGroup, '%s*,%s*' ) do
				arg = tonumber( arg ) or arg
				if not groupData.count then
					groupData.count = #parsedCArgs[arg]
				end
				groupData.args[arg] = true
			end
			argGroups[i] = groupData
			i = i + 1
		end
	else
		for arg, frames in pairs( parsedCArgs ) do
			local framesLen = #frames
			if framesLen > 0 then
				local groupName = framesLen
				local alias = frames.aliasReference and frames.aliasReference[1]
				if alias and alias.length == framesLen and
					alias.frame.name:find( '^' .. prefixes.any .. ' ' )
				then
					groupName = alias.frame.name
				end
				
				local groupData = argGroups[groupName]
				if not groupData then
					groupData = {
						args = {},
						count = framesLen
					}
					argGroups[groupName] = groupData
				end
				groupData.args[arg] = true
			end
		end
	end
	
	return argGroups
end

--[[Adds together the required frames from each slot in this group
	to get the total amount of frames which are relevant
	
	Returns a table with the relevant frame numbers, if any are relevant
--]]
local function findRelevantFrameNums( requiredFrameNumData, group )
	local relevantFrameNums = {}
	local hasRelevantFrames
	for arg in pairs( group ) do
		local requiredFrameNums = requiredFrameNumData[arg]
		if requiredFrameNums and arg ~= 'Output' then
			for frameNum in pairs( requiredFrameNums ) do
				-- Have to use pairs as it contains a non-sequential set of numbers
				-- so we have to skip over the extra data in the table
				if frameNum ~= 'count' then
					hasRelevantFrames = true
					relevantFrameNums[frameNum] = true
				end
			end
			
			relevantFrameNums.count = math.max(
				requiredFrameNums.count or 0,
				relevantFrameNums.count or 0
			)
		end
	end
	
	return hasRelevantFrames and relevantFrameNums
end

--[[Loops through the relevant frame numbers and extracts them
	into a new table, taking care of moving any alias references
	and cleaning up any unnecessary subframes
--]]
function p.extractRelevantFrames( relevantFrameNums, frames )
	local relevantFrames = { randomise = frames.randomise }
	local newFrameNum = 1
	for frameNum, frame in ipairs( frames ) do
		local relevantFrame = relevantFrameNums == true or relevantFrameNums[frameNum]
		if relevantFrame then
			if not frame[1] then
				local alias = frames.aliasReference and frames.aliasReference[frameNum]
				local moveAlias = true
				if alias and relevantFrameNums ~= true then
					for i = frameNum, alias.length do
						if not relevantFrameNums[i] then
							moveAlias = false
							break
						end
					end
				end
				if alias and moveAlias then
					if not relevantFrames.aliasReference then
						relevantFrames.aliasReference = {}
					end
					relevantFrames.aliasReference[newFrameNum] = alias
				end
			end
			
			relevantFrames[newFrameNum] = frame
			newFrameNum = newFrameNum + 1
		end
	end
	
	-- Move frames in subframe to main frames, if the subframe
	-- is the only frame
	if not relevantFrames[2] and relevantFrames[1][1] then
		relevantFrames = relevantFrames[1]
	end
	
	return relevantFrames
end

--[[Works out what data is relevant to the requested ingredients
	
	If the template contains any of the ingredients, returns it with any
	necessary modifications, and with the crafting arguments parsed
--]]
function p.processTemplate( tArgs, ingredientPatterns )
	local cArgs = {}
	for i, v in pairs( crafting.cArgVals ) do
		cArgs[i] = tArgs[i] or tArgs[v]
	end
	cArgs.Output = tArgs.Output
	local parsedCArgs = parseCraftingArgs( cArgs )
	
	local requiredFrameNumData = findRequiredFrameNums( parsedCArgs, ingredientPatterns )
	if not requiredFrameNumData then
		return
	end
	
	local newCArgs
	local modified
	if requiredFrameNumData == true then
		newCArgs = parsedCArgs
	else
		local argGroups = generateArgGroups( tArgs.arggroups, parsedCArgs )
		newCArgs = {}
		for _, groupData in pairs( argGroups ) do
			local group = groupData.args
			local relevantFrameNums = findRelevantFrameNums( requiredFrameNumData, group )
			if not relevantFrameNums then
				for arg in pairs( group ) do
					newCArgs[arg] = parsedCArgs[arg]
				end
			else
				modified = true
				for arg in pairs( group ) do
					newCArgs[arg] = p.extractRelevantFrames( relevantFrameNums, parsedCArgs[arg] )
				end
			end
		end
	end
	
	-- Convert arguments back to shapeless format if they were originally
	if tArgs[1] then
		local i = 1
		for argNum = 1, 9 do
			tArgs[argNum] = nil
			local cArg = newCArgs[argNum]
			if cArg then
				tArgs[i] = cArg
				i = i + 1
			end
		end
	else
		for i, arg in pairs( crafting.cArgVals ) do
			tArgs[arg] = newCArgs[i]
		end
	end
	tArgs.Output = newCArgs.Output
	tArgs.parsed = true
	
	-- Let Module:Recipe table generate these
	-- with the modified crafting args
	if modified then
		tArgs.name = nil
		tArgs.ingredients = nil
	end
	
	return tArgs
end

--[[Works out which frame is the first frame, and returns its
	name, or alias name, for sorting purposes
--]]
function p.getFirstFrameName( frames, subframe )
	local frame = frames[1]
	if not subframe and frame[1] then
		return p.getFirstFrameName( frame[1], true )
	end
	
	local alias = frames.aliasReference and frames.aliasReference[1]
	return alias and alias.frame.name or frame.name
end

--[[Performs the DPL query which retrieves the arguments from the crafting
	templates on the pages within the requested categories.
	If more than four categories are given, break them down into batches of four.
--]]
local function dplQueryWrapper( category, ignore )
	local data = {}
	if type(category) == 'string' then
		category = mw.text.split(category, '|')
	else
		assert(type(category) == 'table')
	end
	
	local includeStr = '{' .. i18n.templateCrafting .. '}:' .. table.concat( argList, ':' )
	local j = 1
	local count = #category
	local catParts = text.split( i18n.queryCategory, '%$1' )
	for i = 1, count, 4 do
		local catSlice = table.concat(category, '|', i, math.min(i + 3, count))
		data[j] = mw.getCurrentFrame():callParserFunction( '#dpl:', {
			category = catSlice,
			nottitleregexp = ignore,
			include = includeStr,
			mode = 'userformat',
			secseparators = '====',
			multisecseparators = '===='
		})
		j = j + 1
	end
	
	return table.concat(data)
end


--[[The main body, which retrieves the data, and returns the relevant
	crafting templates, sorted alphabetically
--]]
function p.dpl( f )
	local args = f
	if f == mw.getCurrentFrame() then
		args = f:getParent().args
	else
		f = mw.getCurrentFrame()
	end
	local ingredients = args[1] and text.split( args[1], '%s*,%s*' ) or { mw.title.getCurrentTitle().text }
	local matchTypes = args.match and args.match:find( ',' ) and text.split( args.match, '%s*,%s*' ) or args.match
	local ingredientPatterns = createIngredientPatterns( ingredients, matchTypes )
	
	local data
	if args.category then
		data = dplQueryWrapper( args.category, args.ignore )
	else
		-- Need to format the catparts
		local catParts = text.split( i18n.queryCategory, '%$1' )
		local cats = map(ingredients, function (s)
			return catParts[1] .. s .. catParts[2]
		end)
		data = dplQueryWrapper( cats, args.ignore )
	end
	
	local showDescription
	local templates = {}
	local i = 1
	for templateArgs in text.gsplit( data:sub( 5 ), '====' ) do
		local tArgs = extractArgs( templateArgs )
		local newArgs = tArgs and p.processTemplate( tArgs, ingredientPatterns )
		if newArgs then
			if tArgs.description then
				showDescription = '1'
			end
			
			templates[i] = {
				args = newArgs,
				sortKey = mw.ustring.lower(
					( newArgs.name or p.getFirstFrameName( newArgs.Output ) )
						:gsub( '^' .. prefixes.any .. ' ', '' )
						:gsub( '^' .. prefixes.matching .. ' ', '' )
						:gsub( '^%[%[', '' )
						:gsub( '^[^|]+|', '' )
				),
			}
			i = i + 1
		end
	end
	
	local templateCount = #templates
	if templateCount == 0 then
		return f:expandTemplate{ title = 'Translation category', args = { i18n.emptyCategory, project = '0' } }
	end
	
	table.sort( templates, function( a, b )
		return a.sortKey < b.sortKey
	end )
	
	local initialArgs = templates[1].args
	initialArgs.head = '1'
	initialArgs.showname = '1'
	initialArgs.showdescription = showDescription
	if not args.continue then
		templates[templateCount].args.foot = '1'
	end
	
	local out = {}
	for i, template in ipairs( templates ) do
		out[i] = ( '<!-- [[' .. template.args['%PAGE%'] .. ']] -->\n'
		           .. crafting.table( template.args ) )
	end
	return table.concat( out, '\n' )
end

return p