Module:TemplatePar

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

local function fetch( light, options ) -- Return regular table with all parameters -- Precondition: --    light    -- true: template transclusion;  false: #invoke --    options  -- table; optional details --                options.low -- Postcondition: --    Return table; whitespace-only values as false -- Uses: --    TemplatePar.downcase --    TemplatePar.framing --    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( "\\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( "&gt;&gt;&gt;" ) sign = string.format( "%s",                                         sign,  tostring( btn ) ) top:wikitext( sign, "&#160;", submit ) mw.addWarning( tostring( top ) ) end show = tostring( e ) end i = show:find( "@@@", 1, true ) if i then -- No gsub since r might contain "%3" (e.g. URL) r = string.format( "%s%s%s",                                  show:sub( 1,  i - 1 ),                                   r,                                   show:sub( i + 3 ) ) else r = show end end if learn and r then -- r = fatal( r ) end s = opt.cat if type( s ) == "string" then local link if opt.errNS then local ns = mw.title.getCurrentTitle.namespace local st = type( opt.errNS ) if st == "string" then local space = string.format( ".*%%s%d%%s.*", ns ) local spaces = string.format( " %s ", opt.errNS ) if spaces:match( space ) then link = true end elseif st == "table" then for i = 1, #opt.errNS do                       if opt.errNS[ i ] == ns then link = true break   -- for i                        end end -- for i               end else link = true end if link then local cats, i               if not r then r = "" end if s:find( "@@@" ) then if type( opt.template ) == "string" then s = s:gsub( "@@@", opt.template ) end end cats = mw.text.split( s, "%s*#%s*" ) for i = 1, #cats do                   s = mw.text.trim( cats[ i ] ) if #s > 0 then r = string.format( "%s", r, s ) end end -- for i           end end end return r end -- finalize

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

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

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

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

local function form( light, options, 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 &quot;%s&quot;", show, options.say ) end r = failure( "tooShort", show, options ) end else r = failure( "invalidPar", "min", options ) end end if options.max and  not r then if type( options.max ) == "number" then if #analyze > options.max then show = " >" .. options.max if options.say then show = string.format( "%s &quot;%s&quot;", show, options.say ) end r = failure( "tooLong", show, options ) end else r = failure( "invalidPar", "max", options ) end end return r end -- format

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

local function furnish( 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( "&#35;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 = "&#35;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, &quot;%s&quot;" errMiss = string.format( s, errMiss, k ) else errMiss = string.format( "&quot;%s&quot;",                                                    k ) end elseif not errMiss then errValues = fault( errValues, r ) end break -- for r, rule end end -- for s, rule end -- for k, v       r = ( errMiss or errValues ) if r then if errMiss then r = failure( "undefined", errMiss, options ) else r = failure( "invalid", errValues, options ) end r = finalize( r, options ) end end return r or "" end -- p.match

function p.valid( 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