Module:TemplatePar
local TemplatePar = { serial  = &quot;2023-03-20&quot;,
                       suite   = &quot;TemplatePar&quot;,
                       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 = &quot;lua-module-TemplatePar-&quot;
 Local.L10nDef = {}
 Local.L10nDef.en = {
     badPattern  = &quot;&amp;#35;invoke:TemplatePar pattern syntax error&quot;,
     dupOpt      = &quot;&amp;#35;invoke:TemplatePar repeated optional parameter&quot;,
     dupRule     = &quot;&amp;#35;invoke:TemplatePar conflict key/pattern&quot;,
     empty       = &quot;Error in template * undefined value for mandatory&quot;,
     invalid     = &quot;Error in template * invalid parameter&quot;,
     invalidPar  = &quot;&amp;#35;invoke:TemplatePar invalid parameter&quot;,
     minmax      = &quot;&amp;#35;invoke:TemplatePar min &gt; max&quot;,
     missing     = &quot;&amp;#35;invoke:TemplatePar missing library&quot;,
     multiSpell  = &quot;Error in template * multiple spelling of parameter&quot;,
     noMSGnoCAT  = &quot;&amp;#35;invoke:TemplatePar neither message nor category&quot;,
     noname      = &quot;&amp;#35;invoke:TemplatePar missing parameter name&quot;,
     notFound    = &quot;Error in template * missing page&quot;,
     tooLong     = &quot;Error in template * parameter too long&quot;,
     tooShort    = &quot;Error in template * parameter too short&quot;,
     unavailable = &quot;Error in template * parameter name missing&quot;,
     undefined   = &quot;Error in template * mandatory parameter missing&quot;,
     unknown     = &quot;Error in template * unknown parameter name&quot;,
     unknownRule = &quot;&amp;#35;invoke:TemplatePar unknown rule&quot;
 }
 Local.patterns = {
     [ &quot;ASCII&quot; ]    = &quot;^[ -~]*$&quot;,
     [ &quot;ASCII+&quot; ]   = &quot;^[ -~]+$&quot;,
     [ &quot;ASCII+1&quot; ]  = &quot;^[!-~]+$&quot;,
     [ &quot;n&quot; ]        = &quot;^[%-]?[0-9]*$&quot;,
     [ &quot;n&gt;0&quot; ]      = &quot;^[0-9]*[1-9][0-9]*$&quot;,
     [ &quot;N+&quot; ]       = &quot;^[%-]?[1-9][0-9]*$&quot;,
     [ &quot;N&gt;0&quot; ]      = &quot;^[1-9][0-9]*$&quot;,
     [ &quot;x&quot; ]        = &quot;^[0-9A-Fa-f]*$&quot;,
     [ &quot;x+&quot; ]       = &quot;^[0-9A-Fa-f]+$&quot;,
     [ &quot;X&quot; ]        = &quot;^[0-9A-F]*$&quot;,
     [ &quot;X+&quot; ]       = &quot;^[0-9A-F]+$&quot;,
     [ &quot;0,0&quot; ]      = &quot;^[%-]?[0-9]*,?[0-9]*$&quot;,
     [ &quot;0,0+&quot; ]     = &quot;^[%-]?[0-9]+,[0-9]+$&quot;,
     [ &quot;0,0+?&quot; ]    = &quot;^[%-]?[0-9]+,?[0-9]*$&quot;,
     [ &quot;0.0&quot; ]      = &quot;^[%-]?[0-9]*[%.]?[0-9]*$&quot;,
     [ &quot;0.0+&quot; ]     = &quot;^[%-]?[0-9]+%.[0-9]+$&quot;,
     [ &quot;0.0+?&quot; ]    = &quot;^[%-]?[0-9]+[%.]?[0-9]*$&quot;,
     [ &quot;.0+&quot; ]      = &quot;^[%-]?[0-9]*[%.]?[0-9]+$&quot;,
     [ &quot;ID&quot; ]       = &quot;^[A-Za-z]?[A-Za-z_0-9]*$&quot;,
     [ &quot;ID+&quot; ]      = &quot;^[A-Za-z][A-Za-z_0-9]*$&quot;,
     [ &quot;ABC&quot; ]      = &quot;^[A-Z]*$&quot;,
     [ &quot;ABC+&quot; ]     = &quot;^[A-Z]+$&quot;,
     [ &quot;Abc&quot; ]      = &quot;^[A-Z]*[a-z]*$&quot;,
     [ &quot;Abc+&quot; ]     = &quot;^[A-Z][a-z]+$&quot;,
     [ &quot;abc&quot; ]      = &quot;^[a-z]*$&quot;,
     [ &quot;abc+&quot; ]     = &quot;^[a-z]+$&quot;,
     [ &quot;aBc+&quot; ]     = &quot;^[a-z]+[A-Z][A-Za-z]*$&quot;,
     [ &quot;w&quot; ]        = &quot;^%S*$&quot;,
     [ &quot;w+&quot; ]       = &quot;^%S+$&quot;,
     [ &quot;base64&quot; ]   = &quot;^[A-Za-z0-9%+/]*$&quot;,
     [ &quot;base64+&quot; ]  = &quot;^[A-Za-z0-9%+/]+$&quot;,
     [ &quot;aa&quot; ]       = &quot;[%a%a].*[%a%a]&quot;,
     [ &quot;pagename&quot; ] = string.format( &quot;^[^#&lt;&gt;%%[%%]|{}%c-%c%c]+$&quot;,
                                     1, 31, 127 ),
     [ &quot;ref&quot; ]      = string.format( &quot;%c'%c`UNIQ%s%sref%s%s%sQINU`%c'%c&quot;,
                                     127, 34, &quot;%-&quot;, &quot;%-&quot;, &quot;%-&quot;, &quot;%x+&quot;,
                                     &quot;%-&quot;, 34, 127 ),
     [ &quot;+&quot; ]        = &quot;%S&quot;
 }
 Local.boolean = { [&quot;1&quot;]     = true,
                   [&quot;true&quot;]  = true,
                   y         = true,
                   yes       = true,
                   on        = true,
                   [&quot;0&quot;]     = true,
                   [&quot;false&quot;] = true,
                   [&quot;-&quot;]     = 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( &quot;%s/%s&quot;,
                                                    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,  &quot;Module:&quot; .. storage )
     end
     if not lucky then
         if not suited  and
            type( alt ) == &quot;number&quot;  and
            alt &gt; 0 then
             suited = string.format( &quot;Q%d&quot;, alt )
             suited = mw.wikibase.getSitelink( suited )
             GlobalMod.globalModules[ access ] = suited or true
         end
         if type( suited ) == &quot;string&quot; then
             storage = suited
             finer()
             lucky, r = pcall( fun, storage )
         end
         if not lucky and alert then
             error( &quot;Missing or invalid page: &quot; .. 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 ) == &quot;table&quot;   and
            type( bib[ access ] ) == &quot;function&quot; then
             bib = bib[ access ]()
             if type( bib ) == &quot;table&quot; 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:
     --     &gt;&lt; 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 ~= &quot;&quot; then
         local FileMedia = Foreign( &quot;FileMedia&quot; )
         if FileMedia  and  type( FileMedia.isFile ) == &quot;function&quot;
                       and  type( FileMedia.isType ) == &quot;function&quot; then
             local s, live = accept:match( &quot;^([a-z]+)(:?)%+?$&quot; )
             if live then
                 if FileMedia.isType( attempt, s ) then
                     if FileMedia.isFile( attempt ) then
                         r = false
                     else
                         r = &quot;notFound&quot;
                     end
                 else
                     r = &quot;invalid&quot;
                 end
             elseif FileMedia.isType( attempt, s ) then
                 r = false
             else
                 r = &quot;invalid&quot;
             end
         else
             r = &quot;missing&quot;
         end
     elseif accept:match( &quot;%+$&quot; ) then
         r = &quot;empty&quot;
     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:
     --     &gt;  Local.messagePrefix
     --     &gt;  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( &quot;Multilingual&quot; )
             if MultiL  and  type( MultiL.tabData ) == &quot;function&quot; then
                 local lang
                 r, lang = MultiL.tabData( &quot;I18n/Module:TemplatePar&quot;,
                                           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( &quot;(((%s)))&quot;, 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 == &quot;&quot; then
         if accept == &quot;boolean+&quot; then
             r = &quot;empty&quot;
         else
             r = false
         end
     elseif Local.boolean[ r ]  or   r:match( &quot;^[01%-]+$&quot; ) then
         r = false
     else
         local TemplUtl = Foreign( &quot;TemplUtl&quot; )
         if TemplUtl  and  type( TemplUtl.faculty ) == &quot;function&quot; then
             r = TemplUtl.faculty( r, &quot;-&quot; )
             if r == &quot;-&quot; then
                 r = &quot;invalid&quot;
             else
                 r = false
             end
         else
             r = &quot;invalid&quot;
         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 ) == &quot;table&quot; then
         if type( options.template ) == &quot;string&quot; then
             if #options.template &gt; 0 then
                 r = string.format( &quot;%s (%s)&quot;, r, options.template )
             end
         end
     end
     if suspect then
         r = string.format( &quot;%s: %s&quot;, 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 ~= &quot;&quot; then
         local MultiL = Foreign( &quot;Multilingual&quot; )
         if MultiL  and  type( MultiL.isLang ) == &quot;function&quot; then
             local lazy = accept:find( &quot;W&quot;, 1, true )
             if accept:find( &quot;s&quot;, 1, true ) then
                 local group = mw.text.split( attempt, &quot;%s+&quot; )
                 r = false
                 for i = 1, #group do
                     if not MultiL.isLang( group[ i ], lazy ) then
                         r = &quot;invalid&quot;
                         break -- for i
                     end
                 end -- for i
             elseif MultiL.isLang( attempt, lazy ) then
                 r = false
             else
                 r = &quot;invalid&quot;
             end
         else
             r = &quot;missing&quot;
         end
     elseif accept:find( &quot;+&quot;, 1, true ) then
         r = &quot;empty&quot;
     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 ~= &quot;&quot; then
         local URLutil = Foreign( &quot;URLutil&quot; )
         if URLutil  and  type( URLutil.isWebURL ) == &quot;function&quot; then
             if URLutil.isWebURL( attempt ) then
                 r = false
             else
                 r = &quot;invalid&quot;
             end
         else
             r = &quot;missing&quot;
         end
     elseif accept:find( &quot;+&quot;, 1, true ) then
         r = &quot;empty&quot;
     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 == &quot;&quot; then
         if accept:find( &quot;+&quot;, 1, true ) then
             r = &quot;empty&quot;
         else
             r = false
         end
     else
         local DateTime = Foreign( &quot;DateTime&quot; )
         if type( DateTime ) == &quot;table&quot; then
             local d = DateTime( attempt )
             if type( d ) == &quot;table&quot; then
                 if accept:find( &quot;/&quot;, 1, true ) then
                     r = &quot;invalid&quot;
                     if accept:sub( 1, 10 ) == &quot;datetime/y&quot; then
                         if d.year then
                             r = false
                             if accept:sub( 1, 11 ) == &quot;datetime/ym&quot; then
                                 if d.month then
                                     if accept:sub( 1, 12 )
                                                    == &quot;datetime/ymd&quot; then
                                         if not d.dom then
                                             r = &quot;invalid&quot;
                                         end
                                     end
                                 else
                                     r = &quot;invalid&quot;
                                 end
                             end
                         end
                     end
                 else
                     r = false
                 end
             else
                 r = &quot;invalid&quot;
             end
         else
             r = &quot;invalid&quot;
         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 ) == &quot;number&quot; then
         s = tostring( key )
     else
         s = key
     end
     if store then
         r = string.format( &quot;%s; %s&quot;, 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:
     --     &gt;  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 ) == &quot;string&quot; then
         if options.key then
             r = failure( &quot;dupRule&quot;, false, options )
         else
             scan = options.pattern
         end
     else
         if type( options.key ) == &quot;string&quot; then
             s = mw.text.trim( options.key )
         else
             s = &quot;+&quot;
         end
         if s ~= &quot;*&quot; then
             scan = Local.patterns[ s ]
         end
         if type( scan ) == &quot;string&quot; then
             if s == &quot;n&quot; or s == &quot;0,0&quot; or s == &quot;0.0&quot; then
                 if not stuff:match( &quot;[0-9]&quot; )  and
                    not stuff:match( &quot;^%s*$&quot; ) then
                     scan = false
                     if options.say then
                         show = string.format( &quot;&amp;quot;%s&amp;quot;&quot;, options.say )
                     end
                     if abbr then
                         r = show
                     else
                         r = failure( &quot;invalid&quot;, show, options )
                     end
                 end
             end
         elseif s ~= &quot;*&quot; then
             local op, n, plus = s:match( &quot;([&lt;!=&gt;]=?)([-0-9][%S]*)(+?)&quot; )
             if op then
                 n = tonumber( n )
                 if n then
                     local i = tonumber( stuff )
                     if i then
                         if op == &quot;&lt;&quot; then
                             i = ( i &lt; n )
                         elseif op == &quot;&lt;=&quot; then
                             i = ( i &lt;= n )
                         elseif op == &quot;&gt;&quot; then
                             i = ( i &gt; n )
                         elseif op == &quot;&gt;=&quot; then
                             i = ( i &gt;= n )
                         elseif op == &quot;==&quot; then
                             i = ( i == n )
                         elseif op == &quot;!=&quot; then
                             i = ( i ~= n )
                         else
                             n = false
                         end
                     end
                     if not i then
                         r = &quot;invalid&quot;
                     end
                 elseif plus then
                     r = &quot;undefined&quot;
                 end
             elseif s:match( &quot;^boolean%+?$&quot; ) then
                 r = faculty( s, stuff )
                 n = true
             elseif s:match( &quot;^datetime/?y?m?d?%+?$&quot; ) then
                 r = fast( s, stuff )
                 n = true
             elseif s:match( &quot;^image%+?:?$&quot; )  or
                    s:match( &quot;^file%+?:?$&quot; ) then
                 r = facility( s, stuff )
                 n = true
             elseif s:match( &quot;langs?W?%+?&quot; ) then
                 r = familiar( s, stuff )
                 n = true
             elseif s:match( &quot;url%+?&quot; ) then
                 r = far( s, stuff )
                 n = true
             end
 -- datetime+
 -- iso8631+
 -- line+
             if not n and not r then
                 r = &quot;unknownRule&quot;
             end
             if r then
                 if options.say then
                     show = string.format( &quot;&amp;quot;%s&amp;quot; %s&quot;, 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 == &quot;aa&quot; then
                     got = containsCJK( stuff )
                 end
                 if not got then
                     if options.say then
                         show = string.format( &quot;&amp;quot;%s&amp;quot;&quot;, options.say )
                     end
                     if abbr then
                         r = show
                     else
                         r = failure( &quot;invalid&quot;, show, options )
                     end
                 end
             end
         else
             r = failure( &quot;badPattern&quot;,
                          string.format( &quot;%s *** %s&quot;, 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 ) == &quot;table&quot;  then
         r = { }
         for k, v in pairs( g ) do
             if type( v ) == &quot;string&quot; then
                 if v:match( &quot;^%s*$&quot; ) then
                     v = false
                 end
             else
                 v = false
             end
             if type( k ) == &quot;number&quot; 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 ) == &quot;string&quot; then
         local story = mw.text.trim( append )
         local sub   = story:match( &quot;^/(.*%S)/$&quot; )
         if type( sub ) == &quot;string&quot; then
             sub             = sub:gsub( &quot;%%!&quot;, &quot;|&quot; )
                                  :gsub( &quot;%%%(%(&quot;, &quot;{{&quot; )
                                  :gsub( &quot;%%%)%)&quot;, &quot;}}&quot; )
                                  :gsub( &quot;\\n&quot;, 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, &quot;%s*=%s*&quot; )
         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 ) == &quot;table&quot; then
             opt  = options
             show = opt.format
             lazy = ( show == &quot;&quot;  or  show == &quot;0&quot;  or  show == &quot;-&quot; )
             s    = opt.preview
             if type( s ) == &quot;string&quot;  and
                s ~= &quot;&quot;  and  s ~= &quot;0&quot;  and  s ~= &quot;-&quot; then
                 local sniffer = &quot;{{REVISIONID}}&quot;
                 if lazy then
                     show = &quot;&quot;
                     lazy = false
                 end
                 if TemplatePar.framing():preprocess( sniffer ) == &quot;&quot; then
                     if s == &quot;1&quot; then
                         show = &quot;*&quot;
                     else
                         show = s
                     end
                     learn = true
                 end
             end
         else
             opt = { }
         end
         if lazy then
             if not opt.cat then
                 r = string.format( &quot;%s %s&quot;,
                                    submit,  factory( &quot;noMSGnoCAT&quot; ) )
             end
         else
             r = submit
         end
         if r  and  not lazy then
             local i
             if not show  or  show == &quot;*&quot; then
                 local e = mw.html.create( &quot;span&quot; )
                                  :attr( &quot;class&quot;, &quot;error&quot; )
                                  :wikitext( &quot;@@@&quot; )
                 if learn then
                     local max  = 1000000000
                     local id   = math.floor( os.clock() * max )
                     local sign = string.format( &quot;error_%d&quot;, id )
                     local btn  = mw.html.create( &quot;span&quot; )
                     local top  = mw.html.create( &quot;div&quot; )
                     e:attr( &quot;id&quot;, sign )
                     btn:css( { [&quot;background&quot;]      = &quot;#FFFF00&quot;,
                                [&quot;border&quot;]          = &quot;#FF0000 3px solid&quot;,
                                [&quot;font-weight&quot;]     = &quot;bold&quot;,
                                [&quot;padding&quot;]         = &quot;2px&quot;,
                                [&quot;text-decoration&quot;] = &quot;none&quot; } )
                        :wikitext( &quot;&amp;gt;&amp;gt;&amp;gt;&quot; )
                     sign = string.format( &quot;[[#%s|%s]]&quot;,
                                           sign,  tostring( btn ) )
                     top:wikitext( sign, &quot;&amp;#160;&quot;, submit )
                     mw.addWarning( tostring( top ) )
                 end
                 show = tostring( e )
             end
             i = show:find( &quot;@@@&quot;, 1, true )
             if i then
                 -- No gsub() since r might contain &quot;%3&quot; (e.g. URL)
                 r = string.format( &quot;%s%s%s&quot;,
                                    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 ) == &quot;string&quot; then
             local link
             if opt.errNS then
                 local ns = mw.title.getCurrentTitle().namespace
                 local st = type( opt.errNS )
                 if st == &quot;string&quot; then
                     local space  = string.format( &quot;.*%%s%d%%s.*&quot;, ns )
                     local spaces = string.format( &quot; %s &quot;, opt.errNS )
                     if spaces:match( space ) then
                         link = true
                     end
                 elseif st == &quot;table&quot; 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 = &quot;&quot;
                 end
                 if s:find( &quot;@@@&quot; ) then
                     if type( opt.template ) == &quot;string&quot; then
                         s = s:gsub( &quot;@@@&quot;, opt.template )
                     end
                 end
                 cats = mw.text.split( s, &quot;%s*#%s*&quot; )
                 for i = 1, #cats do
                     s = mw.text.trim( cats[ i ] )
                     if #s &gt; 0 then
                         r = string.format( &quot;%s[[Category:%s]]&quot;, 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 == &quot;&quot; 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( &quot;unavailable&quot;, false, options )
     elseif r then
         r = failure( &quot;unknown&quot;,
                      string.format( &quot;&amp;quot;%s&amp;quot;&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( &quot;undefined&quot;, 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( &quot;empty&quot;, 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 ) == &quot;string&quot; 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( &quot;multiSpell&quot;, 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( &quot;dupOpt&quot;, 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 ) == &quot;table&quot; 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 ) == &quot;table&quot; then
         if type( options.mandatory ) ~= &quot;table&quot; then
             options.mandatory = { }
         end
         duty = options.mandatory
         if type( options.optional ) ~= &quot;table&quot; then
             options.optional = { }
         end
         r = fold( options )
     else
         options = { }
         duty    = { }
         r       = { }
     end
     if type( r ) == &quot;table&quot; then
         local got = fetch( light, options )
         if type( got ) == &quot;table&quot; 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 ) == &quot;number&quot; then
             if type( options.max ) == &quot;number&quot; then
                 if options.max &lt; options.min then
                     r = failure( &quot;minmax&quot;,
                                  string.format( &quot;%d &gt; %d&quot;,
                                                 options.min,
                                                 options.max ),
                                  options )
                 end
             end
             if #analyze &lt; options.min  and  not r then
                 show = &quot; &lt;&quot; .. options.min
                 if options.say then
                     show = string.format( &quot;%s &amp;quot;%s&amp;quot;&quot;, show, options.say )
                 end
                 r = failure( &quot;tooShort&quot;, show, options )
             end
         else
             r = failure( &quot;invalidPar&quot;, &quot;min&quot;, options )
         end
     end
     if options.max  and  not r then
         if type( options.max ) == &quot;number&quot; then
             if #analyze &gt; options.max then
                 show = &quot; &gt;&quot; .. options.max
                 if options.say then
                     show = string.format( &quot;%s &amp;quot;%s&amp;quot;&quot;, show, options.say )
                 end
                 r = failure( &quot;tooLong&quot;, show, options )
             end
         else
             r = failure( &quot;invalidPar&quot;, &quot;max&quot;, 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 ) == &quot;table&quot; then
         local story = assignment.args[ access ] or &quot;&quot;
         if type( access ) == &quot;number&quot; then
             story = mw.text.trim( story )
         end
         if type( options ) ~= &quot;table&quot; 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   -- &quot;assert&quot; or &quot;valid&quot;
     -- Postcondition:
     --     Return string with error message or &quot;&quot;
     -- Uses:
     --     form()
     --     failure()
     --     finalize()
     --     TemplatePar.valid()
     --     TemplatePar.assert()
     local options = { mandatory = { &quot;1&quot; },
                       optional  = { &quot;2&quot;,
                                     &quot;cat&quot;,
                                     &quot;errNS&quot;,
                                     &quot;low&quot;,
                                     &quot;max&quot;,
                                     &quot;min&quot;,
                                     &quot;format&quot;,
                                     &quot;preview&quot;,
                                     &quot;template&quot; },
                       template  = string.format( &quot;&amp;#35;invoke:%s|%s|&quot;,
                                                  &quot;TemplatePar&quot;,
                                                  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 ) == &quot;string&quot; then
             s = frame.args.min:match( &quot;^%s*([0-9]+)%s*$&quot; )
             if s then
                 options.min = tonumber( s )
             else
                 r = failure( &quot;invalidPar&quot;,
                              &quot;min=&quot; .. frame.args.min,
                              options )
             end
         end
         if type( frame.args.max ) == &quot;string&quot; then
             s = frame.args.max:match( &quot;^%s*([1-9][0-9]*)%s*$&quot; )
             if s then
                 options.max = tonumber( s )
             else
                 r = failure( &quot;invalidPar&quot;,
                              &quot;max=&quot; .. frame.args.max,
                              options )
             end
         end
         if r then
             r = finalize( r, options )
         else
             s = frame.args[ 1 ] or &quot;&quot;
             r = tonumber( s )
             if ( r ) then
                 s = r
             end
             if action == &quot;valid&quot; then
                 r = TemplatePar.valid( s, options )
             elseif action == &quot;assert&quot; then
                 r = TemplatePar.assert( s, &quot;&quot;, options )
             end
         end
     end
     return r or &quot;&quot;
 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 &lt;br /&gt;
     --                 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 ) == &quot;string&quot; ) then
             if ( append ~= &quot;&quot; ) then
                 r = string.format( &quot;%s&lt;br /&gt; %s&quot;, 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( &quot;^%s*$&quot; ) 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 == &quot;string&quot; then
         r = mw.text.trim( access )
         if #r == 0 then
             r = false
         end
     elseif r == &quot;number&quot; then
         r = access
     else
         r = false
     end
     if r then
         local params
         if type( options ) ~= &quot;table&quot; 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( &quot;noname&quot;, 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:
     --     &gt;&lt; Local.frame
     if not Local.frame then
         if type( frame ) == &quot;table&quot;  and
            type( frame.args ) == &quot;table&quot;  and
            type( frame.getParent ) == &quot;function&quot;  and
            type( frame:getParent() ) == &quot;table&quot;  and
            type( frame:getParent().getParent ) == &quot;function&quot;  and
            type( frame:getParent():getParent() ) == &quot;nil&quot; 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 == &quot;~&quot; )
     local linked  = ( since == &quot;@&quot; )
     local link    = ( since == &quot;item&quot; )
     local r
     if last  or  link  or  linked  or  since == &quot;wikidata&quot; then
         local item = Failsafe.item
         since = false
         if type( item ) == &quot;number&quot;  and  item &gt; 0 then
             local suited = string.format( &quot;Q%d&quot;, item )
             if link then
                 r = suited
             else
                 local entity = mw.wikibase.getEntity( suited )
                 if type( entity ) == &quot;table&quot; then
                     local seek = Failsafe.serialProperty or &quot;P348&quot;
                     local vsn  = entity:formatPropertyValues( seek )
                     if type( vsn ) == &quot;table&quot;  and
                        type( vsn.value ) == &quot;string&quot;  and
                        vsn.value ~= &quot;&quot; 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 ) == &quot;nil&quot; then
         if not since  or  since &lt;= 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 &quot;&quot;
     -- Uses:
     --     furnish()
     return furnish( frame, &quot;assert&quot; )
 end -- p.assert()
 
 
 
 function p.check( frame )
     -- Check validity of template parameters
     -- Precondition:
     --     frame  -- object; #invoke environment
     -- Postcondition:
     --     Return string with error message or &quot;&quot;
     -- Uses:
     --     form()
     --     fill()
     local options = { optional  = { &quot;all&quot;,
                                     &quot;opt&quot;,
                                     &quot;cat&quot;,
                                     &quot;errNS&quot;,
                                     &quot;low&quot;,
                                     &quot;format&quot;,
                                     &quot;preview&quot;,
                                     &quot;template&quot; },
                       template  = &quot;&amp;#35;invoke:TemplatePar|check|&quot;
                     }
     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 &quot;&quot;
 end -- p.check()
 
 
 
 function p.count( frame )
     -- Count number of template parameters
     -- Postcondition:
     --     Return string with digits including &quot;0&quot;
     -- 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 &quot;0&quot;
     -- 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 &quot;&quot;
     -- 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 ) == &quot;number&quot; then
             s, v = v:match( &quot;^ *([^=]+) *= *(%S.*%S*) *$&quot; )
             if s then
                 s = mw.text.trim( s )
                 if s == &quot;&quot; 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( &quot;invalidPar&quot;,  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 == &quot;&quot; then
                     lack = true
                 else
                     lack = false
                 end
             else
                 s    = &quot;&quot;
                 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       = &quot;%s, &amp;quot;%s&amp;quot;&quot;
                             errMiss = string.format( s, errMiss, k )
                         else
                             errMiss = string.format( &quot;&amp;quot;%s&amp;quot;&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( &quot;undefined&quot;, errMiss, options )
             else
                 r = failure( &quot;invalid&quot;, errValues, options )
             end
             r = finalize( r, options )
         end
     end
     return r or &quot;&quot;
 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 &quot;&quot;
     -- Uses:
     --     furnish()
     return furnish( frame, &quot;valid&quot; )
 end -- p.valid()
 
 
 
 p.failsafe = function ( frame )
     -- Versioning interface
     local s = type( frame )
     local since
     if s == &quot;table&quot; then
         since = frame.args[ 1 ]
     elseif s == &quot;string&quot; then
         since = frame
     end
     if since then
         since = mw.text.trim( since )
         if since == &quot;&quot; then
             since = false
         end
     end
     return Failsafe.failsafe( since )  or  &quot;&quot;
 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 } )