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     = { frame = false } local Failsafe  = TemplatePar local GlobalMod = Local  -- Module globals Local.messagePrefix = "lua-module-TemplatePar-" Local.L10nDef = {} Local.L10nDef.en = {     badPattern  = "#invoke:TemplatePar pattern syntax error",     dupOpt      = "#invoke:TemplatePar repeated optional parameter",     dupRule     = "#invoke:TemplatePar conflict key/pattern",     empty       = "Error in template * undefined value for mandatory",     invalid     = "Error in template * invalid parameter",     invalidPar  = "#invoke:TemplatePar invalid parameter",     minmax      = "#invoke:TemplatePar min > max",     missing     = "#invoke:TemplatePar missing library",     multiSpell  = "Error in template * multiple spelling of parameter",     noMSGnoCAT  = "#invoke:TemplatePar neither message nor category",     noname      = "#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 = "#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( ""%s"", 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( ""%s" %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( ""%s"", 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()     --     frame: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( "%%%)%)", "}}" )                                  :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",                                ["border"]          = "#FF0000 3px solid",                                ["font-weight"]     = "bold",                                ["padding"]         = "2px",                                ["text-decoration"] = "none" } )                        :wikitext( ">>>" )                     sign = string.format( "%s",                                           sign,  tostring( btn ) )                     top:wikitext( sign, " ", 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( "%sCategory:%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( ""%s"", 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, frame )     -- Run parameter analysis on current environment     -- Precondition:     --     light    -- true: template transclusion;  false: #invoke     --     options  -- table or nil; optional details     --                 options.mandatory     --                 options.optional     --     frame    -- 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 frame then         TemplatePar.framing( frame )     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 "%s"", 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 "%s"", 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( frame, action )     -- Prepare #invoke evaluation of .assert() or .valid()     -- Precondition:     --     frame    -- 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( "#invoke:%s|%s|",                                                  "TemplatePar",                                                  action )                     }     local r       = form( false, options, frame )     if not r then         local s         options = { cat      = frame.args.cat,                     errNS    = frame.args.errNS,                     low      = frame.args.low,                     format   = frame.args.format,                     preview  = frame.args.preview,                     template = frame.args.template                   }         options = figure( frame.args[ 2 ], options )         if type( frame.args.min ) == "string" then             s = frame.args.min:match( "^%s*([0-9]+)%s*$" )             if s then                 options.min = tonumber( s )             else                 r = failure( "invalidPar",                              "min=" .. frame.args.min,                              options )             end         end         if type( frame.args.max ) == "string" then             s = frame.args.max:match( "^%s*([1-9][0-9]*)%s*$" )             if s then                 options.max = tonumber( s )             else                 r = failure( "invalidPar",                              "max=" .. frame.args.max,                              options )             end         end         if r then             r = finalize( r, options )         else             s = frame.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()     --     frame: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()     --     frame: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()     --     frame: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()     --     frame: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( frame )     -- Ensure availability of frame object     -- Precondition:     --     frame  -- object; #invoke environment, or false     -- Postcondition:     --     Return frame object     -- Uses:     --     >< Local.frame     if not Local.frame then         if type( frame ) == "table"  and            type( frame.args ) == "table"  and            type( frame.getParent ) == "function"  and            type( frame:getParent() ) == "table"  and            type( frame:getParent().getParent ) == "function"  and            type( frame:getParent():getParent() ) == "nil" then             Local.frame = frame         else             Local.frame = mw.getCurrentFrame()         end     end     return Local.frame 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( frame )     -- Perform parameter analysis on some single string     -- Precondition:     --     frame  -- object; #invoke environment     -- Postcondition:     --     Return string with error message or ""     -- Uses:     --     furnish()     return furnish( frame, "assert" ) end -- p.assert()  function p.check( frame )     -- Check validity of template parameters     -- Precondition:     --     frame  -- 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  = "#invoke:TemplatePar|check|"                     }     local r = form( false, options, frame )     if not r then         options = { mandatory = fill( frame.args.all ),                     optional  = fill( frame.args.opt ),                     cat       = frame.args.cat,                     errNS     = frame.args.errNS,                     low       = frame.args.low,                     format    = frame.args.format,                     preview   = frame.args.preview,                     template  = frame.args.template                   }         r       = form( true, options, frame )     end     return r or "" end -- p.check()  function p.count( frame )     -- 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( frame )     -- 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( frame )     -- Combined analysis of parameters and their values     -- Precondition:     --     frame  -- 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      = frame.args.cat,                       errNS    = frame.args.errNS,                       low      = frame.args.low,                       format   = frame.args.format,                       preview  = frame.args.preview,                       template = frame.args.template                     }     local k, v, s     local params = { }     TemplatePar.framing( frame )     for k, v in pairs( frame.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, frame )     end     if not r then         local errMiss, errValues, lack, rule         local targs = frame:getParent().args         options.optional = nil         if options.low then             targs = TemplatePar.downcase()         else             targs = frame: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, "%s""                             errMiss = string.format( s, errMiss, k )                         else                             errMiss = string.format( ""%s"",                                                      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( frame )     -- Check validity of one particular template parameter     -- Precondition:     --     frame  -- object; #invoke environment     -- Postcondition:     --     Return string with error message or ""     -- Uses:     --     furnish()     return furnish( frame, "valid" ) end -- p.valid()  p.failsafe = function ( frame )     -- Versioning interface     local s = type( frame )     local since     if s == "table" then         since = frame.args[ 1 ]     elseif s == "string" then         since = frame     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