Jump to: navigation, search

Module:Crafting usage

Revision as of 23:52, 14 May 2017 by CraftSpider (talk | contribs)
Documentation

This module implements {{Crafting usage}}.

Dependencies

The above documentation is transcluded from Module:Crafting usage/doc.

This module implements {{Crafting usage}}.

Dependencies


local p = {}

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

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',
}
local prefixes = slot.i18n.prefixes

--[[Escapes the parenthesis in ingredient names, and returns the correct
	pattern depending on the match type
--]]
local function createIngredientPatterns( ingredients, matchTypes )
	local patterns = {}
	for i, ingredient in ipairs( ingredients ) do
		local escaped = ingredient:gsub( '([()])', '%%%1' )
		if not matchTypes then
			patterns[i] = '^' .. escaped .. '$'
		else
			local matchType = matchTypes[i] or matchTypes
			if matchType == 'start' then
				patterns[i] = '^' .. escaped
			elseif matchType == 'end' then
				patterns[i] = escaped .. '$'
			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 mw.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
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
				if containsIngredients( frame.name or frame[1].name, 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 mw.text.gsplit( predefinedArgGroups, '%s*;%s*' ) do
			local groupData = { args = {} }
			for arg in mw.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
--]]
function dplQuery( category, ignore )
	test = mw.title.new(category, "Category")
	test = test.exists
	if test then
		return mw.getCurrentFrame():callParserFunction( '#dpl:', {
			category = category,
			nottitleregexp = ignore,
			include = '{' .. i18n.templateCrafting .. '}:' .. table.concat( argList, ':' ),
			mode = 'userformat',
			secseparators = '====',
			multisecseparators = '===='
		} )
	end
	return nil
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 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 ingredientPatterns = createIngredientPatterns( ingredients, matchTypes )
	
	local data
	if args.category then
		data = dplQuery( args.category, args.ignore )
	else
		-- DPL has a limit of four categories, so do it in chunks
		data = {}
		local dataNum = 1
		local ingredientCount = #ingredients
		local catParts = mw.text.split( i18n.queryCategory, '%$1' )
		for i = 1, ingredientCount, 4 do
			data[dataNum] = dplQuery(
				catParts[1] .. table.concat(
					ingredients,
					catParts[2] .. '|' .. catParts[1],
					i,
					math.min( i + 3, ingredientCount )
				) .. catParts[2],
				args.ignore
			)
			dataNum = dataNum + 1
		end
		data = table.concat( data )
	end
	
	if data == nil or data == "" then
		return "Recipe Category not found"
	end
	
	local showDescription
	local templates = {}
	local i = 1
	for templateArgs in mw.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 "[[Category:" .. emptyCategory .. "]]" --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] = crafting.table( template.args )
	end
	return table.concat( out, '\n' )
end

return p


Cookies help us deliver our services. By using our services, you agree to our use of cookies.