# Module:TemplatePar

local TemplatePar = { serial = "2023-03-20",  
suite = "TemplatePar",  
item = 15393417,  
globals = { DateTime = 20652535,  
FileMedia = 24765326,  
Multilingual = 47541920,  
TemplUtl = 52364930,  
URLutil = 10859193 } }  
--[=[  
Template parameter utility  
- assert
- check
- count
- countNotEmpty
- downcase
- duplicates
- match
- valid
- verify
- TemplatePar
- failsafe
]=]  

local Local = { = false }  
local Failsafe = TemplatePar  
local GlobalMod = Local  

-- Module globals  
Local.messagePrefix = "lua-module-TemplatePar-"  
Local.L10nDef = {}  
Local.L10nDef.en = {  
badPattern = "&#35;invoke:TemplatePar pattern syntax error",  
dupOpt = "&#35;invoke:TemplatePar repeated optional parameter",  
dupRule = "&#35;invoke:TemplatePar conflict key/pattern",  
empty = "Error in template * undefined value for mandatory",  
invalid = "Error in template * invalid parameter",  
invalidPar = "&#35;invoke:TemplatePar invalid parameter",  
minmax = "&#35;invoke:TemplatePar min > max",  
missing = "&#35;invoke:TemplatePar missing library",  
multiSpell = "Error in template * multiple spelling of parameter",  
noMSGnoCAT = "&#35;invoke:TemplatePar neither message nor category",  
noname = "&#35;invoke:TemplatePar missing parameter name",  
notFound = "Error in template * missing page",  
tooLong = "Error in template * parameter too long",  
tooShort = "Error in template * parameter too short",  
unavailable = "Error in template * parameter name missing",  
undefined = "Error in template * mandatory parameter missing",  
unknown = "Error in template * unknown parameter name",  
unknownRule = "&#35;invoke:TemplatePar unknown rule"  
}  
Local.patterns = {  
[ "ASCII" ] = "^[ -~]*$",  
[ "ASCII+" ] = "^[ -~]+$",  
[ "ASCII+1" ] = "^[!-~]+$",  
[ "n" ] = "^[%-]?[0-9]*$",  
[ "n>0" ] = "^[0-9]*[1-9][0-9]*$",  
[ "N+" ] = "^[%-]?[1-9][0-9]*$",  
[ "N>0" ] = "^[1-9][0-9]*$",  
[ "x" ] = "^[0-9A-Fa-f]*$",  
[ "x+" ] = "^[0-9A-Fa-f]+$",  
[ "X" ] = "^[0-9A-F]*$",  
[ "X+" ] = "^[0-9A-F]+$",  
[ "0,0" ] = "^[%-]?[0-9]*,?[0-9]*$",  
[ "0,0+" ] = "^[%-]?[0-9]+,[0-9]+$",  
[ "0,0+?" ] = "^[%-]?[0-9]+,?[0-9]*$",  
[ "0.0" ] = "^[%-]?[0-9]*[%.]?[0-9]*$",  
[ "0.0+" ] = "^[%-]?[0-9]+%.[0-9]+$",  
[ "0.0+?" ] = "^[%-]?[0-9]+[%.]?[0-9]*$",  
[ ".0+" ] = "^[%-]?[0-9]*[%.]?[0-9]+$",  
[ "ID" ] = "^[A-Za-z]?[A-Za-z_0-9]*$",  
[ "ID+" ] = "^[A-Za-z][A-Za-z_0-9]*$",  
[ "ABC" ] = "^[A-Z]*$",  
[ "ABC+" ] = "^[A-Z]+$",  
[ "Abc" ] = "^[A-Z]*[a-z]*$",  
[ "Abc+" ] = "^[A-Z][a-z]+$",  
[ "abc" ] = "^[a-z]*$",  
[ "abc+" ] = "^[a-z]+$",  
[ "aBc+" ] = "^[a-z]+[A-Z][A-Za-z]*$",  
[ "w" ] = "^%S*$",  
[ "w+" ] = "^%S+$",  
[ "base64" ] = "^[A-Za-z0-9%+/]*$",  
[ "base64+" ] = "^[A-Za-z0-9%+/]+$",  
[ "aa" ] = "[%a%a].*[%a%a]",  
[ "pagename" ] = string.format( "^[^#<>%%[%%]|{}%c-%c%c]+$",  
1, 31, 127 ),  
[ "ref" ] = string.format( "%c'%c`UNIQ%s%sref%s%s%sQINU`%c'%c",  
127, 34, "%-", "%-", "%-", "%x+",  
"%-", 34, 127 ),  
[ "+" ] = "%S"  
}  
Local.boolean = { ["1"] = true,  
["true"] = true,  
y = true,  
yes = true,  
on = true,  
["0"] = true,  
["false"] = true,  
["-"] = true,  
n = true,  
no = true,  
off = true }  
Local.patternCJK = false  

local foreignModule = function ( access, advanced, append, alt, alert )  
-- Fetch global module  
-- Precondition:  
-- access -- string, with name of base module  
-- advanced -- true, for require; else mw.loadData  
-- append -- string, with subpage part, if any; or false  
-- alt -- number, of wikidata item of root; or false  
-- alert -- true, for throwing error on data problem  
-- Postcondition:  
-- Returns whatever, probably table  
-- 2020-01-01  
local storage = access  
local finer = function  
if append then  
storage = string.format( "%s/%s",  
storage,  
append )  
end  
end  
local fun, lucky, r, suited  
if advanced then  
fun = require  
else  
fun = mw.loadData  
end  
GlobalMod.globalModules = GlobalMod.globalModules or { }  
suited = GlobalMod.globalModules[ access ]  
if not suited then  
finer  
lucky, r = pcall( fun, "Module:" .. storage )  
end  
if not lucky then  
if not suited and  
type( alt ) == "number" and  
alt > 0 then  
suited = string.format( "Q%d", alt )  
suited = mw.wikibase.getSitelink( suited )  
GlobalMod.globalModules[ access ] = suited or true  
end  
if type( suited ) == "string" then  
storage = suited  
finer  
lucky, r = pcall( fun, storage )  
end  
if not lucky and alert then  
error( "Missing or invalid page: " .. storage )  
end  
end  
return r  
end -- foreignModule  

local function Foreign( access )  
-- Access standardized library  
-- Precondition:  
-- access -- string, with name of base module  
-- Postcondition:  
-- Return library table, or not  
-- Uses:  
local r  
if Local[ access ] then  
r = Local[ access ]  
else  
local bib = foreignModule( access,  
true,  
false,  
TemplatePar.globals[ access ],  
false )  
if type( bib ) == "table" and  
type( bib[ access ] ) == "function" then  
bib = bib[ access ]  
if type( bib ) == "table" then  
r = bib  
Local[ access ] = bib  
end  
end  
end  
return r  
end -- Foreign  

local function containsCJK( analyse )  
-- Is any CJK character present?  
-- Precondition:  
-- analyse -- string  
-- Postcondition:  
-- Return false iff no CJK present  
-- Uses:  
-- >< Local.patternCJK  
-- mw.ustring.char  
-- mw.ustring.match  
local r = false  
if not Local.patternCJK then  
Local.patternCJK = mw.ustring.char( 91,  
13312, 45, 40959,  
131072, 45, 178207,  
93 )  
end  
if mw.ustring.match( analyse, Local.patternCJK ) then  
r = true  
end  
return r  
end -- containsCJK  

local function facility( accept, attempt )  
-- Check string as possible file name or other source page  
-- Precondition:  
-- accept -- string; requirement  
-- file  
-- file+  
-- file:  
-- file:+  
-- image  
-- image+  
-- image:  
-- image:+  
-- attempt -- string; to be tested  
-- Postcondition:  
-- Return error keyword, or false  
-- Uses:  
-- Module:FileMedia  
-- Foreign  
-- FileMedia.isFile  
-- FileMedia.isType  
local r  
if attempt and attempt ~= "" then  
local FileMedia = Foreign( "FileMedia" )  
if FileMedia and type( FileMedia.isFile ) == "function"  
and type( FileMedia.isType ) == "function" then  
local s, live = accept:match( "^([a-z]+)(:?)%+?$" )  
if live then  
if FileMedia.isType( attempt, s ) then  
if FileMedia.isFile( attempt ) then  
r = false  
else  
r = "notFound"  
end  
else  
r = "invalid"  
end  
elseif FileMedia.isType( attempt, s ) then  
r = false  
else  
r = "invalid"  
end  
else  
r = "missing"  
end  
elseif accept:match( "%+$" ) then  
r = "empty"  
else  
r = false  
end  
return r  
end -- facility  

local function factory( say )  
-- Retrieve localized message string in content language  
-- Precondition:  
-- say -- string; message ID  
-- Postcondition:  
-- Return some message string  
-- Uses:  
-- > Local.messagePrefix  
-- > Local.L10nDef  
-- mw.message.new  
-- mw.language.getContentLanguage  
-- Module:Multilingual  
-- Foreign  
-- TemplatePar.framing  
-- Multilingual.tabData  
local m = mw.message.new( Local.messagePrefix .. say )  
local r = false  
if m:isBlank then  
local c = mw.language.getContentLanguage:getCode  
local l10n = Local.L10nDef[ c ]  
if l10n then  
r = l10n[ say ]  
else  
local MultiL = Foreign( "Multilingual" )  
if MultiL and type( MultiL.tabData ) == "function" then  
local lang  
r, lang = MultiL.tabData( "I18n/Module:TemplatePar",  
say,  
false,  
TemplatePar.framing )  
end  
end  
if not r then  
r = Local.L10nDef.en[ say ]  
end  
else  
m:inLanguage( c )  
r = m:plain  
end  
if not r then  
r = string.format( "(((%s)))", say )  
end  
return r  
end -- factory  

local function faculty( accept, attempt )  
-- Check string as possible boolean  
-- Precondition:  
-- accept -- string; requirement  
-- boolean  
-- boolean+  
-- attempt -- string; to be tested  
-- Postcondition:  
-- Return error keyword, or false  
-- Uses:  
-- Module:TemplUtl  
-- Foreign  
-- TemplUtl.faculty  
local r  
r = mw.text.trim( attempt ):lower  
if r == "" then  
if accept == "boolean+" then  
r = "empty"  
else  
r = false  
end  
elseif Local.boolean[ r ] or r:match( "^[01%-]+$" ) then  
r = false  
else  
local TemplUtl = Foreign( "TemplUtl" )  
if TemplUtl and type( TemplUtl.faculty ) == "function" then  
r = TemplUtl.faculty( r, "-" )  
if r == "-" then  
r = "invalid"  
else  
r = false  
end  
else  
r = "invalid"  
end  
end  
return r  
end -- faculty  

local function failure( spec, suspect, options )  
-- Submit localized error message  
-- Precondition:  
-- spec -- string; message ID  
-- suspect -- string or nil; additional information  
-- options -- table or nil; optional details  
-- options.template  
-- Postcondition:  
-- Return string  
-- Uses:  
-- factory  
local r = factory( spec )  
if type( options ) == "table" then  
if type( options.template ) == "string" then  
if #options.template > 0 then  
r = string.format( "%s (%s)", r, options.template )  
end  
end  
end  
if suspect then  
r = string.format( "%s: %s", r, suspect )  
end  
return r  
end -- failure  

local function fair( story, scan )  
-- Test for match (possibly user-defined with syntax error)  
-- Precondition:  
-- story -- string; parameter value  
-- scan -- string; pattern  
-- Postcondition:  
-- Return nil, if not matching, else non-nil  
-- Uses:  
-- mw.ustring.match  
return mw.ustring.match( story, scan )  
end -- fair  

local function familiar( accept, attempt )  
-- Check string as possible language name or list  
-- Precondition:  
-- accept -- string; requirement  
-- lang  
-- langs  
-- langW  
-- langsW  
-- lang+  
-- langs+  
-- langW+  
-- langsW+  
-- attempt -- string; to be tested  
-- Postcondition:  
-- Return error keyword, or false  
-- Uses:  
-- Module:Multilingual  
-- Foreign  
-- Multilingual.isLang  
local r  
if attempt and attempt ~= "" then  
local MultiL = Foreign( "Multilingual" )  
if MultiL and type( MultiL.isLang ) == "function" then  
local lazy = accept:find( "W", 1, true )  
if accept:find( "s", 1, true ) then  
local group = mw.text.split( attempt, "%s+" )  
r = false  
for i = 1, #group do  
if not MultiL.isLang( group[ i ], lazy ) then  
r = "invalid"  
break -- for i  
end  
end -- for i  
elseif MultiL.isLang( attempt, lazy ) then  
r = false  
else  
r = "invalid"  
end  
else  
r = "missing"  
end  
elseif accept:find( "+", 1, true ) then  
r = "empty"  
else  
r = false  
end  
return r  
end -- familiar  

local function far( accept, attempt )  
-- Check string as possible URL  
-- Precondition:  
-- accept -- string; requirement  
-- url  
-- url+  
-- attempt -- string; to be tested  
-- Postcondition:  
-- Return error keyword, or false  
-- Uses:  
-- Module:URLutil  
-- Foreign  
-- URLutil.isWebURL  
local r  
if attempt and attempt ~= "" then  
local URLutil = Foreign( "URLutil" )  
if URLutil and type( URLutil.isWebURL ) == "function" then  
if URLutil.isWebURL( attempt ) then  
r = false  
else  
r = "invalid"  
end  
else  
r = "missing"  
end  
elseif accept:find( "+", 1, true ) then  
r = "empty"  
else  
r = false  
end  
return r  
end -- far  

local function fast( accept, attempt )  
-- Check string as possible date or time  
-- Precondition:  
-- accept -- string; requirement  
-- datetime  
-- datetime+  
-- datetime/y  
-- datetime/y+  
-- datetime/ym  
-- datetime/ym+  
-- datetime/ymd  
-- datetime/ymd+  
-- attempt -- string; to be tested  
-- Postcondition:  
-- Return error keyword, or false  
-- Uses:  
-- Module:DateTime  
-- Foreign  
-- DateTime.DateTime  
local r  
r = mw.text.trim( attempt )  
if r == "" then  
if accept:find( "+", 1, true ) then  
r = "empty"  
else  
r = false  
end  
else  
local DateTime = Foreign( "DateTime" )  
if type( DateTime ) == "table" then  
local d = DateTime( attempt )  
if type( d ) == "table" then  
if accept:find( "/", 1, true ) then  
r = "invalid"  
if accept:sub( 1, 10 ) == "datetime/y" then  
if d.year then  
r = false  
if accept:sub( 1, 11 ) == "datetime/ym" then  
if d.month then  
if accept:sub( 1, 12 )  
== "datetime/ymd" then  
if not d.dom then  
r = "invalid"  
end  
end  
else  
r = "invalid"  
end  
end  
end  
end  
else  
r = false  
end  
else  
r = "invalid"  
end  
else  
r = "invalid"  
end  
end  
return r  
end -- fast  

local function fault( store, key )  
-- Add key to collection string and insert separator  
-- Precondition:  
-- store -- string or nil or false; collection string  
-- key -- string or number; to be appended  
-- Postcondition:  
-- Return string; extended  
local r  
local s  
if type( key ) == "number" then  
s = tostring( key )  
else  
s = key  
end  
if store then  
r = string.format( "%s; %s", store, s )  
else  
r = s  
end  
return r  
end -- fault  

local function feasible( analyze, options, abbr )  
-- Check content of a value  
-- Precondition:  
-- analyze -- string to be analyzed  
-- options -- table or nil; optional details  
-- options.pattern  
-- options.key  
-- options.say  
-- abbr -- true: abbreviated error message  
-- Postcondition:  
-- Return string with error message as configured;  
-- false if valid or no answer permitted  
-- Uses:  
-- > Local.patterns  
-- failure  
-- mw.text.trim  
-- faculty  
-- fast  
-- facility  
-- familiar  
-- far  
-- fair  
-- containsCJK  
local r = false  
local s = false  
local show = nil  
local scan = false  
local stuff = mw.text.trim( analyze )  
if type( options.pattern ) == "string" then  
if options.key then  
r = failure( "dupRule", false, options )  
else  
scan = options.pattern  
end  
else  
if type( options.key ) == "string" then  
s = mw.text.trim( options.key )  
else  
s = "+"  
end  
if s ~= "*" then  
scan = Local.patterns[ s ]  
end  
if type( scan ) == "string" then  
if s == "n" or s == "0,0" or s == "0.0" then  
if not stuff:match( "[0-9]" ) and  
not stuff:match( "^%s*$" ) then  
scan = false  
if options.say then  
show = string.format( "&quot;%s&quot;", options.say )  
end  
if abbr then  
r = show  
else  
r = failure( "invalid", show, options )  
end  
end  
end  
elseif s ~= "*" then  
local op, n, plus = s:match( "([<!=>]=?)([-0-9][%S]*)(+?)" )  
if op then  
n = tonumber( n )  
if n then  
local i = tonumber( stuff )  
if i then  
if op == "<" then  
i = ( i < n )  
elseif op == "<=" then  
i = ( i <= n )  
elseif op == ">" then  
i = ( i > n )  
elseif op == ">=" then  
i = ( i >= n )  
elseif op == "==" then  
i = ( i == n )  
elseif op == "!=" then  
i = ( i ~= n )  
else  
n = false  
end  
end  
if not i then  
r = "invalid"  
end  
elseif plus then  
r = "undefined"  
end  
elseif s:match( "^boolean%+?$" ) then  
r = faculty( s, stuff )  
n = true  
elseif s:match( "^datetime/?y?m?d?%+?$" ) then  
r = fast( s, stuff )  
n = true  
elseif s:match( "^image%+?:?$" ) or  
s:match( "^file%+?:?$" ) then  
r = facility( s, stuff )  
n = true  
elseif s:match( "langs?W?%+?" ) then  
r = familiar( s, stuff )  
n = true  
elseif s:match( "url%+?" ) then  
r = far( s, stuff )  
n = true  
end  
-- datetime+  
-- iso8631+  
-- line+  
if not n and not r then  
r = "unknownRule"  
end  
if r then  
if options.say then  
show = string.format( "&quot;%s&quot; %s", options.say, s )  
else  
show = s  
end  
if abbr then  
r = show  
else  
r = failure( r, show, options )  
end  
end  
end  
end  
if scan then  
local legal, got = pcall( fair, stuff, scan )  
if legal then  
if not got then  
if s == "aa" then  
got = containsCJK( stuff )  
end  
if not got then  
if options.say then  
show = string.format( "&quot;%s&quot;", options.say )  
end  
if abbr then  
r = show  
else  
r = failure( "invalid", show, options )  
end  
end  
end  
else  
r = failure( "badPattern",  
string.format( "%s *** %s", scan, got ),  
options )  
end  
end  
return r  
end -- feasible  

local function fed( haystack, needle )  
-- Find needle in haystack map  
-- Precondition:  
-- haystack -- table; map of key values  
-- needle -- any; identifier  
-- Postcondition:  
-- Return true iff found  
local k, v, r  
for k, v in pairs( haystack ) do  
if k == needle then  
r = true  
end  
end -- for k, v  
return r or false  
end -- fed  

local function fetch( light, options )  
-- Return regular table with all parameters  
-- Precondition:  
-- light -- true: template transclusion; false: #invoke  
-- options -- table; optional details  
-- options.low  
-- Postcondition:  
-- Return table; whitespace-only values as false  
-- Uses:  
-- TemplatePar.downcase  
-- TemplatePar.framing  
-- :getParent  
local g, k, v  
local r = { }  
if options.low then  
g = TemplatePar.downcase( options )  
else  
g = TemplatePar.framing  
if light then  
g = g:getParent  
end  
g = g.args  
end  
if type( g ) == "table" then  
r = { }  
for k, v in pairs( g ) do  
if type( v ) == "string" then  
if v:match( "^%s*$" ) then  
v = false  
end  
else  
v = false  
end  
if type( k ) == "number" then  
k = tostring( k )  
end  
r[ k ] = v  
end -- for k, v  
else  
r = g  
end  
return r  
end -- fetch  

local function figure( append, options )  
-- Extend options by rule from #invoke strings  
-- Precondition:  
-- append -- string or nil; requested rule  
-- options -- table; details  
-- ++ .key  
-- ++ .pattern  
-- Postcondition:  
-- Return sequence table  
local r = options  
if type( append ) == "string" then  
local story = mw.text.trim( append )  
local sub = story:match( "^/(.*%S)/$" )  
if type( sub ) == "string" then  
sub = sub:gsub( "%%!", "|" )  
:gsub( "%%%(%(", "" )  
:gsub( "\\n", string.char( 10 ) )  
options.pattern = sub  
options.key = nil  
else  
options.key = story  
options.pattern = nil  
end  
end  
return r  
end -- figure  

local function fill( specified )  
-- Split requirement string separated by '='  
-- Precondition:  
-- specified -- string or nil; requested parameter set  
-- Postcondition:  
-- Return sequence table  
-- Uses:  
-- mw.text.split  
local r  
if specified then  
local i, s  
r = mw.text.split( specified, "%s*=%s*" )  
for i = #r, 1, -1 do  
s = r[ i ]  
if #s == 0 then  
table.remove( r, i )  
end  
end -- for i, -1  
else  
r = { }  
end  
return r  
end -- fill  

local function finalize( submit, options )  
-- Finalize message  
-- Precondition:  
-- submit -- string or false or nil; non-empty error message  
-- options -- table or nil; optional details  
-- options.format  
-- options.preview  
-- options.cat  
-- options.template  
-- Postcondition:  
-- Return string or false  
-- Uses:  
-- TemplatePar.framing  
-- factory  
local r = false  
if submit then  
local lazy = false  
local learn = false  
local show = false  
local opt, s  
if type( options ) == "table" then  
opt = options  
show = opt.format  
lazy = ( show == "" or show == "0" or show == "-" )  
s = opt.preview  
if type( s ) == "string" and  
s ~= "" and s ~= "0" and s ~= "-" then  
local sniffer = ""  
if lazy then  
show = ""  
lazy = false  
end  
if TemplatePar.framing:preprocess( sniffer ) == "" then  
if s == "1" then  
show = "*"  
else  
show = s  
end  
learn = true  
end  
end  
else  
opt = { }  
end  
if lazy then  
if not opt.cat then  
r = string.format( "%s %s",  
submit, factory( "noMSGnoCAT" ) )  
end  
else  
r = submit  
end  
if r and not lazy then  
local i  
if not show or show == "*" then  
local e = mw.html.create( "span" )  
:attr( "class", "error" )  
:wikitext( "@@@" )  
if learn then  
local max = 1000000000  
local id = math.floor( os.clock * max )  
local sign = string.format( "error_%d", id )  
local btn = mw.html.create( "span" )  
local top = mw.html.create( "div" )  
e:attr( "id", sign )  
btn:css( { ["background"] = "#FFFF00",  
[""] = "#FF0000 solid",  
["font-weight"] = "bold",  
["padding"] = "",  
["text-decoration"] = "" } )  
:wikitext( "&gt;&gt;&gt;" )  
sign = string.format( "%s",  
sign, tostring( btn ) )  
top:wikitext( sign, "&#160;", submit )  
mw.addWarning( tostring( top ) )  
end  
show = tostring( e )  
end  
i = show:find( "@@@", 1, true )  
if i then  
-- No gsub since r might contain "%3" (e.g. URL)  
r = string.format( "%s%s%s",  
show:sub( 1, i - 1 ),  
r,  
show:sub( i + 3 ) )  
else  
r = show  
end  
end  
if learn and r then  
-- r = fatal( r )  
end  
s = opt.cat  
if type( s ) == "string" then  
local link  
if opt.errNS then  
local ns = mw.title.getCurrentTitle.namespace  
local st = type( opt.errNS )  
if st == "string" then  
local space = string.format( ".*%%s%d%%s.*", ns )  
local spaces = string.format( " %s ", opt.errNS )  
if spaces:match( space ) then  
link = true  
end  
elseif st == "table" then  
for i = 1, #opt.errNS do  
if opt.errNS[ i ] == ns then  
link = true  
break -- for i  
end  
end -- for i  
end  
else  
link = true  
end  
if link then  
local cats, i  
if not r then  
r = ""  
end  
if s:find( "@@@" ) then  
if type( opt.template ) == "string" then  
s = s:gsub( "@@@", opt.template )  
end  
end  
cats = mw.text.split( s, "%s*#%s*" )  
for i = 1, #cats do  
s = mw.text.trim( cats[ i ] )  
if #s > 0 then  
r = string.format( "%s", r, s )  
end  
end -- for i  
end  
end  
end  
return r  
end -- finalize  

local function finder( haystack, needle )  
-- Find needle in haystack sequence  
-- Precondition:  
-- haystack -- table; sequence of key names, downcased if low  
-- needle -- any; key name  
-- Postcondition:  
-- Return true iff found  
local i  
for i = 1, #haystack do  
if haystack[ i ] == needle then  
return true  
end  
end -- for i  
return false  
end -- finder  

local function fix( valid, duty, got, options )  
-- Perform parameter analysis  
-- Precondition:  
-- valid -- table; unique sequence of known parameters  
-- duty -- table; sequence of mandatory parameters  
-- got -- table; sequence of current parameters  
-- options -- table or nil; optional details  
-- Postcondition:  
-- Return string as configured; empty if valid  
-- Uses:  
-- finder  
-- fault  
-- failure  
-- fed  
local r = false  
local lack  
for k, v in pairs( got ) do  
if k == "" then  
lack = true  
break -- for k, v  
elseif not finder( valid, k ) then  
r = fault( r, k )  
end  
end -- for k, v  
if lack then  
r = failure( "unavailable", false, options )  
elseif r then  
r = failure( "unknown",  
string.format( "&quot;%s&quot;", r ),  
options )  
else -- all names valid  
local i, s  
for i = 1, #duty do  
s = duty[ i ]  
if not fed( got, s ) then  
r = fault( r, s )  
end  
end -- for i  
if r then  
r = failure( "undefined", r, options )  
else -- all mandatory present  
for i = 1, #duty do  
s = duty[ i ]  
if not got[ s ] then  
r = fault( r, s )  
end  
end -- for i  
if r then  
r = failure( "empty", r, options )  
end  
end  
end  
return r  
end -- fix  

local function flat( collection, options )  
-- Return all table elements with downcased string  
-- Precondition:  
-- collection -- table; k=v pairs  
-- options -- table or nil; optional messaging details  
-- Postcondition:  
-- Return table, may be empty; or string with error message.  
-- Uses:  
-- mw.ustring.lower  
-- fault  
-- failure  
local k, v  
local r = { }  
local e = false  
for k, v in pairs( collection ) do  
if type ( k ) == "string" then  
k = mw.ustring.lower( k )  
if r[ k ] then  
e = fault( e, k )  
end  
end  
r[ k ] = v  
end -- for k, v  
if e then  
r = failure( "multiSpell", e, options )  
end  
return r  
end -- flat  

local function fold( options )  
-- Merge two tables, create new sequence if both not empty  
-- Precondition:  
-- options -- table; details  
-- options.mandatory sequence to keep unchanged  
-- options.optional sequence to be appended  
-- options.low downcased expected  
-- Postcondition:  
-- Return merged table, or message string if error  
-- Uses:  
-- finder  
-- fault  
-- failure  
-- flat  
local i, e, r, s  
local base = options.mandatory  
local extend = options.optional  
if #base == 0 then  
if #extend == 0 then  
r = { }  
else  
r = extend  
end  
else  
if #extend == 0 then  
r = base  
else  
e = false  
for i = 1, #extend do  
s = extend[ i ]  
if finder( base, s ) then  
e = fault( e, s )  
end  
end -- for i  
if e then  
r = failure( "dupOpt", e, options )  
else  
r = { }  
for i = 1, #base do  
table.insert( r, base[ i ] )  
end -- for i  
for i = 1, #extend do  
table.insert( r, extend[ i ] )  
end -- for i  
end  
end  
end  
if options.low and type( r ) == "table" then  
r = flat( r, options )  
end  
return r  
end -- fold  

local function form( light, options, )  
-- Run parameter analysis on current environment  
-- Precondition:  
-- light -- true: template transclusion; false: #invoke  
-- options -- table or nil; optional details  
-- options.mandatory  
-- options.optional  
-- -- object; #invoke environment, or false  
-- Postcondition:  
-- Return string with error message as configured;  
-- false if valid  
-- Uses:  
-- TemplatePar.framing  
-- fold  
-- fetch  
-- fix  
-- finalize  
local duty, r  
if then  
TemplatePar.framing  
end  
if type( options ) == "table" then  
if type( options.mandatory ) ~= "table" then  
options.mandatory = { }  
end  
duty = options.mandatory  
if type( options.optional ) ~= "table" then  
options.optional = { }  
end  
r = fold( options )  
else  
options = { }  
duty = { }  
r = { }  
end  
if type( r ) == "table" then  
local got = fetch( light, options )  
if type( got ) == "table" then  
r = fix( r, duty, got, options )  
else  
r = got  
end  
end  
return finalize( r, options )  
end -- form  

local function format( analyze, options )  
-- Check validity of a value  
-- Precondition:  
-- analyze -- string to be analyzed  
-- options -- table or nil; optional details  
-- options.say  
-- options.min  
-- options.max  
-- Postcondition:  
-- Return string with error message as configured;  
-- false if valid or no answer permitted  
-- Uses:  
-- feasible  
-- failure  
local r = feasible( analyze, options, false )  
local show  
if options.min and not r then  
if type( options.min ) == "number" then  
if type( options.max ) == "number" then  
if options.max < options.min then  
r = failure( "minmax",  
string.format( "%d > %d",  
options.min,  
options.max ),  
options )  
end  
end  
if #analyze < options.min and not r then  
show = " <" .. options.min  
if options.say then  
show = string.format( "%s &quot;%s&quot;", show, options.say )  
end  
r = failure( "tooShort", show, options )  
end  
else  
r = failure( "invalidPar", "min", options )  
end  
end  
if options.max and not r then  
if type( options.max ) == "number" then  
if #analyze > options.max then  
show = " >" .. options.max  
if options.say then  
show = string.format( "%s &quot;%s&quot;", show, options.say )  
end  
r = failure( "tooLong", show, options )  
end  
else  
r = failure( "invalidPar", "max", options )  
end  
end  
return r  
end -- format  

local function formatted( assignment, access, options )  
-- Check validity of one particular parameter in a collection  
-- Precondition:  
-- assignment -- collection  
-- access -- id of parameter in collection  
-- options -- table or nil; optional details  
-- Postcondition:  
-- Return string with error message as configured;  
-- false if valid or no answer permitted  
-- Uses:  
-- mw.text.trim  
-- format  
-- failure  
local r = false  
if type( assignment ) == "table" then  
local story = assignment.args[ access ] or ""  
if type( access ) == "number" then  
story = mw.text.trim( story )  
end  
if type( options ) ~= "table" then  
options = { }  
end  
options.say = access  
r = format( story, options )  
end  
return r  
end -- formatted  

local function furnish( , action )  
-- Prepare #invoke evaluation of .assert or .valid  
-- Precondition:  
-- -- object; #invoke environment  
-- action -- "assert" or "valid"  
-- Postcondition:  
-- Return string with error message or ""  
-- Uses:  
-- form  
-- failure  
-- finalize  
-- TemplatePar.valid  
-- TemplatePar.assert  
local options = { mandatory = { "1" },  
optional = { "2",  
"cat",  
"errNS",  
"low",  
"max",  
"min",  
"format",  
"preview",  
"template" },  
template = string.format( "&#35;invoke:%s|%s|",  
"TemplatePar",  
action )  
}  
local r = form( false, options, )  
if not r then  
local s  
options = { cat = .args.cat,  
errNS = .args.errNS,  
low = .args.low,  
format = .args.format,  
preview = .args.preview,  
template = .args.template  
}  
options = figure( .args[ 2 ], options )  
if type( .args.min ) == "string" then  
s = .args.min:match( "^%s*([0-9]+)%s*$" )  
if s then  
options.min = tonumber( s )  
else  
r = failure( "invalidPar",  
"min=" .. .args.min,  
options )  
end  
end  
if type( .args.max ) == "string" then  
s = .args.max:match( "^%s*([1-9][0-9]*)%s*$" )  
if s then  
options.max = tonumber( s )  
else  
r = failure( "invalidPar",  
"max=" .. .args.max,  
options )  
end  
end  
if r then  
r = finalize( r, options )  
else  
s = .args[ 1 ] or ""  
r = tonumber( s )  
if ( r ) then  
s = r  
end  
if action == "valid" then  
r = TemplatePar.valid( s, options )  
elseif action == "assert" then  
r = TemplatePar.assert( s, "", options )  
end  
end  
end  
return r or ""  
end -- furnish  

TemplatePar.assert = function ( analyze, append, options )  
-- Perform parameter analysis on a single string  
-- Precondition:  
-- analyze -- string to be analyzed  
-- append -- string: append error message, prepending  

-- false or nil: throw error with message  
-- options -- table; optional details  
-- Postcondition:  
-- Return string with error message as configured;  
-- false if valid  
-- Uses:  
-- format  
local r = format( analyze, options )  
if ( r ) then  
if ( type( append ) == "string" ) then  
if ( append ~= "" ) then  
r = string.format( "%s  
%s", append, r )  
end  
else  
error( r, 0 )  
end  
end  
return r  
end -- TemplatePar.assert  

TemplatePar.check = function ( options )  
-- Run parameter analysis on current template environment  
-- Precondition:  
-- options -- table or nil; optional details  
-- options.mandatory  
-- options.optional  
-- Postcondition:  
-- Return string with error message as configured;  
-- false if valid  
-- Uses:  
-- form  
return form( true, options, false )  
end -- TemplatePar.check  

TemplatePar.count = function  
-- Return number of template parameters  
-- Postcondition:  
-- Return number, starting at 0  
-- Uses:  
-- mw.getCurrentFrame  
-- :getParent  
local k, v  
local r = 0  
local t = mw.getCurrentFrame:getParent  
local o = t.args  
for k, v in pairs( o ) do  
r = r + 1  
end -- for k, v  
return r  
end -- TemplatePar.count  

TemplatePar.countNotEmpty = function  
-- Return number of template parameters with more than whitespace  
-- Postcondition:  
-- Return number, starting at 0  
-- Uses:  
-- mw.getCurrentFrame  
-- :getParent  
local k, v  
local r = 0  
local t = mw.getCurrentFrame:getParent  
local o = t.args  
for k, v in pairs( o ) do  
if not v:match( "^%s*$" ) then  
r = r + 1  
end  
end -- for k, v  
return r  
end -- TemplatePar.countNotEmpty  

TemplatePar.downcase = function ( options )  
-- Return all template parameters with downcased name  
-- Precondition:  
-- options -- table or nil; optional messaging details  
-- Postcondition:  
-- Return table, may be empty; or string with error message.  
-- Uses:  
-- mw.getCurrentFrame  
-- :getParent  
-- flat  
local t = mw.getCurrentFrame:getParent  
return flat( t.args, options )  
end -- TemplatePar.downcase  

TemplatePar.valid = function ( access, options )  
-- Check validity of one particular template parameter  
-- Precondition:  
-- access -- id of parameter in template transclusion  
-- string or number  
-- options -- table or nil; optional details  
-- Postcondition:  
-- Return string with error message as configured;  
-- false if valid or no answer permitted  
-- Uses:  
-- mw.text.trim  
-- TemplatePar.downcase  
-- TemplatePar.framing  
-- :getParent  
-- formatted  
-- failure  
-- finalize  
local r = type( access )  
if r == "string" then  
r = mw.text.trim( access )  
if #r == 0 then  
r = false  
end  
elseif r == "number" then  
r = access  
else  
r = false  
end  
if r then  
local params  
if type( options ) ~= "table" then  
options = { }  
end  
if options.low then  
params = TemplatePar.downcase( options )  
else  
params = TemplatePar.framing:getParent  
end  
r = formatted( params, access, options )  
else  
r = failure( "noname", false, options )  
end  
return finalize( r, options )  
end -- TemplatePar.valid  

TemplatePar.verify = function ( options )  
-- Perform #invoke parameter analysis  
-- Precondition:  
-- options -- table or nil; optional details  
-- Postcondition:  
-- Return string with error message as configured;  
-- false if valid  
-- Uses:  
-- form  
return form( false, options, false )  
end -- TemplatePar.verify  

TemplatePar.framing = function  
-- Ensure availability of object  
-- Precondition:  
-- -- object; #invoke environment, or false  
-- Postcondition:  
-- Return object  
-- Uses:  
-- >< Local.  
if not Local. then  
if type == "table" and  
type( .args ) == "table" and  
type( .getParent ) == "function" and  
type( :getParent ) == "table" and  
type( :getParent.getParent ) == "function" and  
type( :getParent:getParent ) == "nil" then  
Local. =  
else  
Local. = mw.getCurrentFrame  
end  
end  
return Local.  
end -- TemplatePar.framing  

Failsafe.failsafe = function ( atleast )  
-- Retrieve versioning and check for compliance  
-- Precondition:  
-- atleast -- string, with required version  
-- or wikidata|item|~|@ or false  
-- Postcondition:  
-- Returns string -- with queried version/item, also if problem  
-- false -- if appropriate  
-- 2020-08-17  
local since = atleast  
local last = ( since == "~" )  
local linked = ( since == "@" )  
local link = ( since == "item" )  
local r  
if last or link or linked or since == "wikidata" then  
local item = Failsafe.item  
since = false  
if type( item ) == "number" and item > 0 then  
local suited = string.format( "Q%d", item )  
if link then  
r = suited  
else  
local entity = mw.wikibase.getEntity( suited )  
if type( entity ) == "table" then  
local seek = Failsafe.serialProperty or "P348"  
local vsn = entity:formatPropertyValues( seek )  
if type( vsn ) == "table" and  
type( vsn.value ) == "string" and  
vsn.value ~= "" then  
if last and vsn.value == Failsafe.serial then  
r = false  
elseif linked then  
if mw.title.getCurrentTitle.prefixedText  
== mw.wikibase.getSitelink( suited ) then  
r = false  
else  
r = suited  
end  
else  
r = vsn.value  
end  
end  
end  
end  
end  
end  
if type( r ) == "nil" then  
if not since or since <= Failsafe.serial then  
r = Failsafe.serial  
else  
r = false  
end  
end  
return r  
end -- Failsafe.failsafe  

-- Provide external access  
local p = {}  

function p.assert  
-- Perform parameter analysis on some single string  
-- Precondition:  
-- -- object; #invoke environment  
-- Postcondition:  
-- Return string with error message or ""  
-- Uses:  
-- furnish  
return furnish( , "assert" )  
end -- p.assert  

function p.check  
-- Check validity of template parameters  
-- Precondition:  
-- -- object; #invoke environment  
-- Postcondition:  
-- Return string with error message or ""  
-- Uses:  
-- form  
-- fill  
local options = { optional = { "all",  
"opt",  
"cat",  
"errNS",  
"low",  
"format",  
"preview",  
"template" },  
template = "&#35;invoke:TemplatePar|check|"  
}  
local r = form( false, options, )  
if not r then  
options = { mandatory = fill( .args.all ),  
optional = fill( .args.opt ),  
cat = .args.cat,  
errNS = .args.errNS,  
low = .args.low,  
format = .args.format,  
preview = .args.preview,  
template = .args.template  
}  
r = form( true, options, )  
end  
return r or ""  
end -- p.check  

function p.count  
-- Count number of template parameters  
-- Postcondition:  
-- Return string with digits including "0"  
-- Uses:  
-- TemplatePar.count  
return tostring( TemplatePar.count )  
end -- p.count  

function p.countNotEmpty  
-- Count number of template parameters which are not empty  
-- Postcondition:  
-- Return string with digits including "0"  
-- Uses:  
-- TemplatePar.countNotEmpty  
return tostring( TemplatePar.countNotEmpty )  
end -- p.countNotEmpty  

function p.match  
-- Combined analysis of parameters and their values  
-- Precondition:  
-- -- object; #invoke environment  
-- Postcondition:  
-- Return string with error message or ""  
-- Uses:  
-- TemplatePar.framing  
-- mw.text.trim  
-- mw.ustring.lower  
-- failure  
-- form  
-- TemplatePar.downcase  
-- figure  
-- feasible  
-- fault  
-- finalize  
local r = false  
local options = { cat = .args.cat,  
errNS = .args.errNS,  
low = .args.low,  
format = .args.format,  
preview = .args.preview,  
template = .args.template  
}  
local k, v, s  
local params = { }  
TemplatePar.framing  
for k, v in pairs( .args ) do  
if type( k ) == "number" then  
s, v = v:match( "^ *([^=]+) *= *(%S.*%S*) *$" )  
if s then  
s = mw.text.trim( s )  
if s == "" then  
s = false  
end  
end  
if s then  
if options.low then  
s = mw.ustring.lower( s )  
end  
if params[ s ] then  
s = params[ s ]  
s[ #s + 1 ] = v  
else  
params[ s ] = { v }  
end  
else  
r = failure( "invalidPar", tostring( k ), options )  
break -- for k, v  
end  
end  
end -- for k, v  
if not r then  
s = { }  
for k, v in pairs( params ) do  
s[ #s + 1 ] = k  
end -- for k, v  
options.optional = s  
r = form( true, options, )  
end  
if not r then  
local errMiss, errValues, lack, rule  
local targs = :getParent.args  
options.optional = nil  
if options.low then  
targs = TemplatePar.downcase  
else  
targs = :getParent.args  
end  
errMiss = false  
errValues = false  
for k, v in pairs( params ) do  
options.say = k  
s = targs[ k ]  
if s then  
if s == "" then  
lack = true  
else  
lack = false  
end  
else  
s = ""  
lack = true  
end  
for r, rule in pairs( v ) do  
options = figure( rule, options )  
r = feasible( s, options, true )  
if r then  
if lack then  
if errMiss then  
s = "%s, &quot;%s&quot;"  
errMiss = string.format( s, errMiss, k )  
else  
errMiss = string.format( "&quot;%s&quot;",  
k )  
end  
elseif not errMiss then  
errValues = fault( errValues, r )  
end  
break -- for r, rule  
end  
end -- for s, rule  
end -- for k, v  
r = ( errMiss or errValues )  
if r then  
if errMiss then  
r = failure( "undefined", errMiss, options )  
else  
r = failure( "invalid", errValues, options )  
end  
r = finalize( r, options )  
end  
end  
return r or ""  
end -- p.match  

function p.valid  
-- Check validity of one particular template parameter  
-- Precondition:  
-- -- object; #invoke environment  
-- Postcondition:  
-- Return string with error message or ""  
-- Uses:  
-- furnish  
return furnish( , "valid" )  
end -- p.valid  

p.failsafe = function  
-- Versioning interface  
local s = type  
local since  
if s == "table" then  
since = .args[ 1 ]  
elseif s == "string" then  
since =  
end  
if since then  
since = mw.text.trim( since )  
if since == "" then  
since = false  
end  
end  
return Failsafe.failsafe( since ) or ""  
end -- p.failsafe  

function p.TemplatePar  
-- Retrieve function access for modules  
-- Postcondition:  
-- Return table with functions  
return TemplatePar  
end -- p.TemplatePar  

setmetatable( p, { __call = function ( func, ... )  
setmetatable( p, nil )  
return Failsafe  
end } )  

return p