Line 1: |
Line 1: |
| --[[ | | --[[ |
− | Handles most of the boilerplate necessary to fetch arguments, such as trimming
| + | Handles most of the boilerplate necessary to fetch arguments, such as trimming |
− | leading/trailing whitespace, making blank arguments evaluate to false, correctly
| + | leading/trailing whitespace, making blank arguments evaluate to false, correctly |
− | choosing between current-frame and parent-frame, etc.
| + | choosing between current-frame and parent-frame, etc. |
− |
| + | |
− | Originally written on the English Wikipedia by Mr. Stradivarius, Jackmcbarn, and Anomie.
| + | Originally written on the English Wikipedia by Mr. Stradivarius, Jackmcbarn, and Anomie. |
− |
| + | |
− | Code released under the GPL v2+ as per:
| + | 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=624020648&oldid=624019645 |
− | https://en.wikipedia.org/w/index.php?diff=624073793&oldid=624020648
| + | https://en.wikipedia.org/w/index.php?diff=624073793&oldid=624020648 |
− |
| + | |
− | @license GNU GPL v2+
| + | @license GNU GPL v2+ |
| ]] | | ]] |
− |
| + | |
| local libraryUtil = require( 'libraryUtil' ) | | local libraryUtil = require( 'libraryUtil' ) |
− |
| + | |
| local sandboxSuffixPattern = string.format( '/%s$', | | local sandboxSuffixPattern = string.format( '/%s$', |
− | mw.message.new( 'scribunto-template-sandbox-subpage-name' ):plain():gsub("%p", "%%%0")
| + | mw.message.new( 'scribunto-template-sandbox-subpage-name' ):plain():gsub("%p", "%%%0") |
| ) | | ) |
− |
| + | |
− | -- Generate four different tidyVal functions, so that we don't have to check the options every
| |
− | -- time we call it.
| |
− |
| |
| local function tidyValDefault( k, v ) | | local function tidyValDefault( k, v ) |
− | if type( v ) == 'string' then
| + | if type( v ) == 'string' then |
− | v = v:match( '^%s*(.-)%s*$' )
| + | v = v:match( '^%s*(.-)%s*$' ) |
− | if v == '' then
| + | if v == '' then |
− | return nil
| + | return nil |
− | end
| + | end |
− | end
| + | end |
− | return v
| + | return v |
| end | | end |
− |
| + | |
| local function tidyValTrimOnly( k, v ) | | local function tidyValTrimOnly( k, v ) |
− | if type( v ) == 'string' then
| + | if type( v ) == 'string' then |
− | return v:match( '^%s*(.-)%s*$' )
| + | return v:match( '^%s*(.-)%s*$' ) |
− | else
| + | else |
− | return v
| + | return v |
− | end
| + | end |
| end | | end |
− |
| + | |
| local function tidyValRemoveBlanksOnly( k, v ) | | local function tidyValRemoveBlanksOnly( k, v ) |
− | if type( v ) == 'string' and not v:find( '%S' ) then
| + | if type( v ) == 'string' and not v:find( '%S' ) then |
− | return nil
| + | return nil |
− | else
| + | else |
− | return v
| + | return v |
− | end
| + | end |
| end | | end |
− |
| + | |
| local function tidyValNoChange( k, v ) | | local function tidyValNoChange( k, v ) |
− | return v
| + | return v |
| end | | 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 ) | | return function( options ) |
− | libraryUtil.checkType( 'getArgs', 1, options, 'table', true )
| + | libraryUtil.checkType( 'getArgs', 1, options, 'table', true ) |
− | local frame = options.frame
| + | local frame = options.frame |
− | if frame == nil then
| + | if frame == nil then |
− | return {}
| + | return {} |
− | end
| + | end |
− | libraryUtil.checkTypeForNamedArg( 'getArgs', 'frame', frame, 'table' )
| + | libraryUtil.checkTypeForNamedArg( 'getArgs', 'frame', frame, 'table' ) |
− |
| + | |
− | --[[
| + | --[[ |
− | -- Get the argument tables. If we were passed a valid frame object, see if we're being
| + | -- Get the tables to look up arguments from. These could be any of: |
− | -- called via a wrapper. If so, then get pargs, the wrapper's arguments (and if
| + | -- * the frame arguments |
− | -- wrappersUseFrame is set, the frame's arguments as well). Otherwise, just get fargs,
| + | -- * the parent frame arguments |
− | -- the frame's arguments. If we weren't passed a valid frame object, we are being called
| + | -- * a table of arguments passed from a Lua module or from the debug console |
− | -- from another Lua module or from the debug console, so assume that we were passed a
| + | -- We try to select the correct one(s) based on whether we were passed a |
− | -- table of args directly, and assign it to a new variable (luaArgs).
| + | -- valid frame object and whether we are being called from a wrapper |
− | --]]
| + | -- template. Usually we only use one of these tables, but if |
− | local fargs, pargs, luaArgs
| + | -- options.wrappersUseFrame is set we may use both the frame arguments and |
− | if type( frame.args ) == 'table' and type( frame.getParent ) == 'function' then
| + | -- the parent frame arguments. |
− | if options.wrappers then
| + | --]] |
− | local parent = frame:getParent()
| + | local argTables = {} |
− | local found = false
| + | if type( frame.args ) == 'table' and type( frame.getParent ) == 'function' then |
− | if parent then
| + | -- We were passed a valid frame. Find out if the title of its parent |
− | local title = parent:getTitle():gsub( sandboxSuffixPattern, '' )
| + | -- frame is contained in options.wrappers. |
− | if options.wrappers == title then
| + | if options.wrappers then |
− | found = true
| + | local parent = frame:getParent() |
− | elseif type( options.wrappers ) == 'table' then
| + | if parent and titleIsWrapper( parent:getTitle(), options.wrappers ) then |
− | -- If either a key or a value matches, it's a match
| + | argTables[1] = parent.args |
− | if options.wrappers[title] ~= nil then
| + | end |
− | found = true
| + | end |
− | else
| + | if not argTables[1] or options.wrappersUseFrame then |
− | for _,v in ipairs( options.wrappers ) do
| + | table.insert( argTables, frame.args ) |
− | if v == title then
| + | end |
− | found = true
| + | else |
− | break
| + | -- Assume frame is a table of arguments passed from a Lua module or |
− | end
| + | -- from the debug console. |
− | end
| + | argTables[1] = frame |
− | end
| + | end |
− | end
| + | |
− |
| + | -- Get tidyVal, the function that we use to trim whitespace and remove |
− | if found then
| + | -- blank arguments. This can be set by the user with options.valueFunc. |
− | pargs = parent.args
| + | -- Otherwise it is generated from options.trim and options.removeBlanks. |
− | end
| + | -- We define four separate default tidyVal functions so that we can avoid |
− | end
| + | -- checking options.trim and options.removeBlanks every time we look up an |
− |
| + | -- argument. |
− | if not found or options.wrappersUseFrame then
| + | local tidyVal = options.valueFunc |
− | fargs = frame.args
| + | if tidyVal then |
− | end
| + | libraryUtil.checkTypeForNamedArg( 'getArgs', 'valueFunc', tidyVal, 'function' ) |
− | else
| + | elseif options.trim ~= false then |
− | fargs = frame.args
| + | if options.removeBlanks ~= false then |
− | end
| + | tidyVal = tidyValDefault |
− | else
| + | else |
− | luaArgs = frame
| + | tidyVal = tidyValTrimOnly |
− | end
| + | end |
− |
| + | else |
− | -- Set the order of precedence of the argument tables. If the variables are nil, nothing
| + | if options.removeBlanks ~= false then |
− | -- will be added to the table, which is how we avoid clashes between the frame/parent
| + | tidyVal = tidyValRemoveBlanksOnly |
− | -- args and the Lua args.
| + | else |
− | local argTables = { pargs }
| + | tidyVal = tidyValNoChange |
− | argTables[#argTables + 1] = fargs
| + | end |
− | argTables[#argTables + 1] = luaArgs
| + | end |
− |
| + | |
− | -- Generate the tidyVal function. If it has been specified by the user, we use that; if
| + | -- Set up a metatable to allow transparent fetching of arguments from argTables |
− | -- not, we choose one of four functions depending on the options chosen. This is so that
| + | -- * mt - the metatable |
− | -- we don't have to call the options table every time the function is called.
| + | -- * fetchedArgs - a table to store memoized arguments fetched from argTables |
− | local tidyVal = options.valueFunc
| + | -- * nilArgs - a table to memoize nil arguments |
− | if tidyVal then
| + | -- * donePairs - whether pairs has been run |
− | libraryUtil.checkTypeForNamedArg( 'getArgs', 'valueFunc', tidyVal, 'function' )
| + | -- * doneIpairs - whether ipairs has been run |
− | elseif options.trim ~= false then
| + | local mt, fetchedArgs, nilArgs = {}, {}, {} |
− | if options.removeBlanks ~= false then
| + | local donePairs, doneIpairs = false, false |
− | tidyVal = tidyValDefault
| + | |
− | else
| + | -- This function merges arguments from argument tables into fetchedArgs, |
− | tidyVal = tidyValTrimOnly
| + | -- Earlier argument tables take precedence over later ones; once a value |
− | end
| + | -- is written it is not overwritten. |
− | else
| + | local function mergeArgs( tables ) |
− | if options.removeBlanks ~= false then
| + | for _, t in ipairs( tables ) do |
− | tidyVal = tidyValRemoveBlanksOnly
| + | for key, val in pairs( t ) do |
− | else
| + | if fetchedArgs[key] == nil and not nilArgs[key] then |
− | tidyVal = tidyValNoChange
| + | local tidiedVal = tidyVal( key, val ) |
− | end
| + | if tidiedVal == nil then |
− | end
| + | nilArgs[key] = true |
− |
| + | else |
− | -- Set up the args, fetchedArgs and nilArgs tables. args will be the one accessed from
| + | fetchedArgs[key] = tidiedVal |
− | -- functions, and fetchedArgs will hold the actual arguments. Nil arguments are memoized
| + | end |
− | -- in nilArgs, and args_mt connects all of them together.
| + | end |
− | local args, fetchedArgs, nilArgs, args_mt = {}, {}, {}, {}
| + | end |
− | local donePairs, doneIpairs = false, false
| + | end |
− |
| + | end |
− | local function mergeArgs( iterator, tables )
| + | |
− | -- Accepts multiple tables as input and merges their keys and values into one
| + | function mt.__index( t, key ) |
− | -- table using the specified iterator. If a value is already present, it is not
| + | local val = fetchedArgs[key] |
− | -- overwritten; tables listed earlier have precedence. We are also memoizing nil
| + | if val ~= nil then |
− | -- values, but those values can be overwritten.
| + | return val |
− | for _, t in ipairs( tables ) do
| + | elseif donePairs or nilArgs[key] then |
− | for key, val in iterator( t ) do
| + | -- If pairs has been run we already have all the arguments in |
− | if fetchedArgs[key] == nil then
| + | -- fetchedArgs. |
− | local tidiedVal = tidyVal( key, val )
| + | return nil |
− | if tidiedVal == nil then
| + | end |
− | nilArgs[key] = true
| + | for _, argTable in ipairs( argTables ) do |
− | else
| + | local argTableVal = tidyVal( key, argTable[key] ) |
− | fetchedArgs[key] = tidiedVal
| + | if argTableVal ~= nil then |
− | end
| + | fetchedArgs[key] = argTableVal |
− | end
| + | return argTableVal |
− | end
| + | end |
− | end
| + | end |
− | end
| + | nilArgs[key] = true |
− |
| + | return nil |
− | -- Define metatable behaviour. Arguments are memoized in the fetchedArgs table, and are
| + | end |
− | -- only fetched from the argument tables once. Fetching arguments from the argument
| + | |
− | -- tables is the most resource-intensive step in this module, so we try and avoid it
| + | function mt.__newindex( t, key, val ) |
− | -- where possible. For this reason, nil arguments are also memoized, in the nilArgs
| + | if val == nil then |
− | -- table. Also, we keep a record in the metatable of when pairs and ipairs have been
| + | -- We need to memoize the nil so that we don't look up the key in |
− | -- called, so we do not run pairs and ipairs on the argument tables more than once. We
| + | -- the argument tables if it is accessed again. |
− | -- also do not run ipairs on fargs and pargs if pairs has already been run, as all the
| + | nilArgs[key] = true |
− | -- arguments will already have been copied over.
| + | end |
− |
| + | fetchedArgs[key] = val |
− | function args_mt.__index( t, key )
| + | end |
− | -- Fetches an argument when the args table is indexed. First we check to see if
| + | |
− | -- the value is memoized, and if not we try and fetch it from the argument
| + | function mt.__pairs() |
− | -- tables. When we check memoization, we need to check fetchedArgs before
| + | if not donePairs then |
− | -- nilArgs, as both can be non-nil at the same time. If the argument is not
| + | donePairs = true |
− | -- present in fetchedArgs, we also check whether pairs has been run yet. If
| + | mergeArgs( argTables ) |
− | -- pairs has already been run, we return nil. This is because all the arguments
| + | end |
− | -- will have already been copied into fetchedArgs by the mergeArgs function,
| + | return pairs( fetchedArgs ) |
− | -- meaning that any other arguments must be nil.
| + | end |
− | local val = fetchedArgs[key]
| + | |
− | if val ~= nil then
| + | local function inext(t, i) |
− | return val
| + | -- This uses our __index metamethod |
− | elseif donePairs or nilArgs[key] then
| + | local v = t[i + 1] |
− | return nil
| + | if v ~= nil then |
− | end
| + | return i + 1, v |
− | for _, argTable in ipairs( argTables ) do
| + | end |
− | local argTableVal = tidyVal( key, argTable[key] )
| + | end |
− | if argTableVal == nil then
| + | |
− | nilArgs[key] = true
| + | function mt.__ipairs(t) |
− | else
| + | return inext, t, 0 |
− | fetchedArgs[key] = argTableVal
| + | end |
− | return argTableVal
| + | |
− | end
| + | return setmetatable( {}, mt ) |
− | end
| |
− | return nil
| |
− | end
| |
− |
| |
− | function args_mt.__newindex( t, key, val )
| |
− | if val == nil then
| |
− | -- If the argument is to be overwritten with nil, we need to memoize the
| |
− | -- nil in nilArgs, so that the value isn't looked up in the argument
| |
− | -- tables if it is accessed again.
| |
− | nilArgs[key] = true
| |
− | end
| |
− | fetchedArgs[key] = val
| |
− | end
| |
− |
| |
− | function args_mt.__pairs()
| |
− | if not donePairs then
| |
− | donePairs = true
| |
− | mergeArgs( pairs, argTables )
| |
− | end
| |
− | return pairs( fetchedArgs )
| |
− | end
| |
− |
| |
− | function args_mt.__ipairs()
| |
− | if not doneIpairs and not donePairs then
| |
− | doneIpairs = true
| |
− | mergeArgs( ipairs, argTables )
| |
− | end
| |
− | return ipairs( fetchedArgs )
| |
− | end
| |
− |
| |
− | return setmetatable( args, args_mt )
| |
| end | | end |