Module:GetArgs

Documentation for this module may be created at Module:GetArgs/doc

--[[
	Handles most of the boilerplate necessary to fetch arguments, such as trimming
	leading/trailing whitespace, making blank arguments evaluate to false, correctly
	choosing between current-frame and parent-frame, etc.

	Originally written on the English Wikipedia by Mr. Stradivarius, Jackmcbarn, and Anomie.

	Code released under the GPL v2+ as per:
	https://en.wikipedia.org/w/index.php?diff=624020648&oldid=624019645
	https://en.wikipedia.org/w/index.php?diff=624073793&oldid=624020648

	@license GNU GPL v2+
]]

local libraryUtil = require( 'libraryUtil' )

local sandboxSuffixPattern = string.format( '/%s$',
	mw.message.new( 'scribunto-template-sandbox-subpage-name' ):plain():gsub("%p", "%%%0")
)

local function tidyValDefault( k, v )
	if type( v ) == 'string' then
		v = v:match( '^%s*(.-)%s*$' )
		if v == '' then
			return nil
		end
	end
	return v
end

local function tidyValTrimOnly( k, v )
	if type( v ) == 'string' then
		return v:match( '^%s*(.-)%s*$' )
	else
		return v
	end
end

local function tidyValRemoveBlanksOnly( k, v )
	if type( v ) == 'string' and not v:find( '%S' ) then
		return nil
	else
		return v
	end
end

local function tidyValNoChange( k, v )
	return v
end

local function matchesTitle( given, title )
	local tp = type( given )
	return ( tp == 'string' or tp == 'number' ) and mw.title.new( given ).prefixedText == title
end

-- Checks whether title matches any wrapper templates that were specified by
-- the user.
local function titleIsWrapper( title, wrappers )
	title = title:gsub( sandboxSuffixPattern, '' )
	if matchesTitle( wrappers, title ) then
		return true
	elseif type( wrappers ) == 'table' then
		-- If either a key or a value matches, it's a match
		if wrappers[title] ~= nil then
			return true
		else
			for _,v in ipairs( wrappers ) do
				if matchesTitle( v, title ) then
					return true
				end
			end
		end
	end
	return false
end

return function( options )
	libraryUtil.checkType( 'getArgs', 1, options, 'table', true )
	local frame = options.frame
	if frame == nil then
		return {}
	end
	libraryUtil.checkTypeForNamedArg( 'getArgs', 'frame', frame, 'table' )

	--[[
	-- Get the tables to look up arguments from. These could be any of:
	-- * the frame arguments
	-- * the parent frame arguments
	-- * a table of arguments passed from a Lua module or from the debug console
	-- We try to select the correct one(s) based on whether we were passed a
	-- valid frame object and whether we are being called from a wrapper
	-- template. Usually we only use one of these tables, but if
	-- options.wrappersUseFrame is set we may use both the frame arguments and
	-- the parent frame arguments.
	--]]
	local argTables = {}
	if type( frame.args ) == 'table' and type( frame.getParent ) == 'function' then
		-- We were passed a valid frame. Find out if the title of its parent
		-- frame is contained in options.wrappers.
		if options.wrappers then
			local parent = frame:getParent()
			if parent and titleIsWrapper( parent:getTitle(), options.wrappers ) then
				argTables[1] = parent.args
			end
		end
		if not argTables[1] or options.wrappersUseFrame then
			table.insert( argTables, frame.args )
		end
	else
		-- Assume frame is a table of arguments passed from a Lua module or
		-- from the debug console.
		argTables[1] = frame
	end

	-- Get tidyVal, the function that we use to trim whitespace and remove
	-- blank arguments. This can be set by the user with options.valueFunc.
	-- Otherwise it is generated from options.trim and options.removeBlanks.
	-- We define four separate default tidyVal functions so that we can avoid
	-- checking options.trim and options.removeBlanks every time we look up an
	-- argument.
	local tidyVal = options.valueFunc
	if tidyVal then
		libraryUtil.checkTypeForNamedArg( 'getArgs', 'valueFunc', tidyVal, 'function' )
	elseif options.trim ~= false then
		if options.removeBlanks ~= false then
			tidyVal = tidyValDefault
		else
			tidyVal = tidyValTrimOnly
		end
	else
		if options.removeBlanks ~= false then
			tidyVal = tidyValRemoveBlanksOnly
		else
			tidyVal = tidyValNoChange
		end
	end

	-- Set up a metatable to allow transparent fetching of arguments from argTables
	-- * mt - the metatable
	-- * fetchedArgs - a table to store memoized arguments fetched from argTables
	-- * nilArgs - a table to memoize nil arguments
	-- * donePairs - whether pairs has been run
	-- * doneIpairs - whether ipairs has been run
	local mt, fetchedArgs, nilArgs = {}, {}, {}
	local donePairs, doneIpairs = false, false

	-- This function merges arguments from argument tables into fetchedArgs,
	-- Earlier argument tables take precedence over later ones; once a value
	-- is written it is not overwritten.
	local function mergeArgs( tables )
		for _, t in ipairs( tables ) do
			for key, val in pairs( t ) do
				if fetchedArgs[key] == nil and not nilArgs[key] then
					local tidiedVal = tidyVal( key, val )
					if tidiedVal == nil then
						nilArgs[key] = true
					else
						fetchedArgs[key] = tidiedVal
					end
				end
			end
		end
	end

	function mt.__index( t, key )
		local val = fetchedArgs[key]
		if val ~= nil then
			return val
		elseif donePairs or nilArgs[key] then
			-- If pairs has been run we already have all the arguments in
			-- fetchedArgs.
			return nil
		end
		for _, argTable in ipairs( argTables ) do
			local argTableVal = tidyVal( key, argTable[key] )
			if argTableVal ~= nil then
				fetchedArgs[key] = argTableVal
				return argTableVal
			end
		end
		nilArgs[key] = true
		return nil
	end

	function mt.__newindex( t, key, val )
		if val == nil then
			-- We need to memoize the nil so that we don't look up the key in
			-- the argument tables if it is accessed again.
			nilArgs[key] = true
		end
		fetchedArgs[key] = val
	end

	function mt.__pairs()
		if not donePairs then
			donePairs = true
			mergeArgs( argTables )
		end
		return pairs( fetchedArgs )
	end

	local function inext(t, i)
		-- This uses our __index metamethod
		local v = t[i + 1]
		if v ~= nil then
			return i + 1, v
		end
	end

	function mt.__ipairs(t)
		return inext, t, 0
	end

	return setmetatable( {}, mt )
end