Modul:TemplatePar: Unterschied zwischen den Versionen

Aus FreeWiki
Zur Navigation springen Zur Suche springen
te>PerfektesChaos
(update)
K (25 Versionen importiert)
 
(15 dazwischenliegende Versionen von 5 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
--[=[ TemplatePar 2013-05-20
+
local TemplatePar = { serial = "2018-08-10",
 +
                      suite  = "TemplatePar",
 +
                      item  = 15393417,
 +
                      extern = { },
 +
                      frame  = false }
 +
--[=[
 
Template parameter utility
 
Template parameter utility
 +
* assert
 
* check
 
* check
 
* count
 
* count
* downcase
+
* countNotEmpty
 +
* downcase()
 +
* duplicates
 +
* match
 
* valid
 
* valid
* verify
+
* verify()
 
* TemplatePar()
 
* TemplatePar()
 +
* failsafe()
 
]=]
 
]=]
  
Zeile 12: Zeile 22:
  
 
-- Module globals
 
-- Module globals
local TemplatePar = { }
+
local MessagePrefix = "lua-module-TemplatePar-"
local messagePrefix = "lua-module-TemplatePar-"
+
local L10nDef = {}
local l10nDef = {}
+
L10nDef.en = {
l10nDef[ "en" ] = {
+
     badPattern  = "#invoke:TemplatePar pattern syntax error",
     badPattern  = "#invoke:TemplatePar * pattern syntax error",
+
     dupOpt      = "#invoke:TemplatePar repeated optional parameter",
     dupOpt      = "#invoke:TemplatePar * repeated optional parameter",
+
     dupRule    = "#invoke:TemplatePar conflict key/pattern",
     dupRule    = "#invoke:TemplatePar * parameter conflict key/pattern",
 
 
     empty      = "Error in template * undefined value for mandatory",
 
     empty      = "Error in template * undefined value for mandatory",
 
     invalid    = "Error in template * invalid parameter",
 
     invalid    = "Error in template * invalid parameter",
     invalidPar  = "#invoke:TemplatePar * invalid parameter",
+
     invalidPar  = "#invoke:TemplatePar invalid parameter",
     minmax      = "#invoke:TemplatePar * min > max",
+
     minmax      = "#invoke:TemplatePar min > max",
 +
    missing    = "#invoke:TemplatePar missing library",
 
     multiSpell  = "Error in template * multiple spelling of parameter",
 
     multiSpell  = "Error in template * multiple spelling of parameter",
     noErrorCat = "#invoke:TemplatePar * noError and missing category",
+
     noMSGnoCAT = "#invoke:TemplatePar neither message nor category",
     noname      = "#invoke:TemplatePar * missing parameter name",
+
     noname      = "#invoke:TemplatePar missing parameter name",
 +
    notFound    = "Error in template * missing page",
 
     tooLong    = "Error in template * parameter too long",
 
     tooLong    = "Error in template * parameter too long",
 
     tooShort    = "Error in template * parameter too short",
 
     tooShort    = "Error in template * parameter too short",
 
     undefined  = "Error in template * mandatory parameter missing",
 
     undefined  = "Error in template * mandatory parameter missing",
 
     unknown    = "Error in template * unknown parameter name",
 
     unknown    = "Error in template * unknown parameter name",
     unknownRule = "#invoke:TemplatePar * unknown rule"
+
     unknownRule = "#invoke:TemplatePar unknown rule"
}
 
l10nDef[ "de" ]  = {
 
    badPattern  = "#invoke:TemplatePar * Syntaxfehler des pattern",
 
    dupOpt      = "#invoke:TemplatePar * Optionsparameter wiederholt",
 
    dupRule    = "#invoke:TemplatePar * Parameterkonflikt key/pattern",
 
    empty      = "Fehler bei Vorlage * Pflichtparameter ohne Wert",
 
    invalid    = "Fehler bei Vorlage * Parameter ungültig",
 
    invalidPar  = "#invoke:TemplatePar * Ungültiger Parameter",
 
    minmax      = "#invoke:TemplatePar * min > max",
 
    multiSpell  = "Fehler bei Vorlage * Mehrere Parameter-Schreibweisen",
 
    noErrorCat  = "#invoke:TemplatePar * noError und keine Kategorie",
 
    noname      = "#invoke:TemplatePar * Parametername nicht angegeben",
 
    tooLong    = "Fehler bei Vorlage * Parameter zu lang",
 
    tooShort    = "Fehler bei Vorlage * Parameter zu kurz",
 
    undefined  = "Fehler bei Vorlage * Pflichtparameter fehlt",
 
    unknown    = "Fehler bei Vorlage * Parametername unbekannt",
 
    unknownRule = "#invoke:TemplatePar * Unbekannte Regel"
 
 
}
 
}
 
local Patterns = {
 
local Patterns = {
     [ "ASCII" ]   = "^[ -~]*$",
+
     [ "ASCII" ]   = "^[ -~]*$",
     [ "ASCII+" ] = "^[ -~]+$",
+
     [ "ASCII+" ]   = "^[ -~]+$",
     [ "ASCII+1" ] = "^[!-~]+$",
+
     [ "ASCII+1" ] = "^[!-~]+$",
     [ "n" ]       = "^%-?[0-9]*$",
+
     [ "n" ]       = "^[%-]?[0-9]*$",
     [ "n>0" ]     = "^[0-9]*[1-9][0-9]*$",
+
     [ "n>0" ]     = "^[0-9]*[1-9][0-9]*$",
     [ "N+" ]     = "^%-?[1-9][0-9]*$",
+
     [ "N+" ]       = "^[%-]?[1-9][0-9]*$",
     [ "N>0" ]     = "^[1-9][0-9]*$",
+
     [ "N>0" ]     = "^[1-9][0-9]*$",
     [ "x" ]       = "^[0-9A-Fa-f]*$",
+
     [ "x" ]       = "^[0-9A-Fa-f]*$",
     [ "x+" ]     = "^[0-9A-Fa-f]+$",
+
     [ "x+" ]       = "^[0-9A-Fa-f]+$",
     [ "X" ]       = "^[0-9A-F]*$",
+
     [ "X" ]       = "^[0-9A-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" ]     = "^%-?[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]+$",
+
     [ ".0+" ]     = "^[%-]?[0-9]*[%.]?[0-9]+$",
     [ "ID" ]     = "^[A-Za-z]?[A-Za-z_0-9]*$",
+
     [ "ID" ]       = "^[A-Za-z]?[A-Za-z_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]+$",
+
     [ "ABC+" ]     = "^[A-Z]+$",
     [ "Abc" ]     = "^[A-Z]*[a-z]*$",
+
     [ "Abc" ]     = "^[A-Z]*[a-z]*$",
     [ "Abc+" ]   = "^[A-Z][a-z]+$",
+
     [ "Abc+" ]     = "^[A-Z][a-z]+$",
     [ "abc" ]     = "^[a-z]*$",
+
     [ "abc" ]     = "^[a-z]*$",
     [ "abc+" ]   = "^[a-z]+$",
+
     [ "abc+" ]     = "^[a-z]+$",
     [ "aBc+" ]   = "^[a-z]+[A-Z][A-Za-z]*$",
+
     [ "aBc+" ]     = "^[a-z]+[A-Z][A-Za-z]*$",
     [ "base64" ] = "^[A-Za-z0-9%+/]*$",
+
    [ "w" ]        = "^%S*$",
     [ "base64+" ] = "^[A-Za-z0-9%+/]+$",
+
    [ "w+" ]      = "^%S+$",
     [ "+" ]       = "%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 patternCJK = false
 +
 +
 +
 +
local function containsCJK( s )
 +
    -- Is any CJK character present?
 +
    -- Precondition:
 +
    --    s  -- string
 +
    -- Postcondition:
 +
    --    Return false iff no CJK present
 +
    -- Uses:
 +
    --    >< patternCJK
 +
    --    mw.ustring.char()
 +
    --    mw.ustring.match()
 +
    local r = false
 +
    if not patternCJK then
 +
        patternCJK = mw.ustring.char( 91,
 +
                                      13312, 45,  40959,
 +
                                      131072, 45, 178207,
 +
                                      93 )
 +
    end
 +
    if mw.ustring.match( s, patternCJK ) then
 +
        r = true
 +
    end
 +
    return r
 +
end -- containsCJK()
  
  
  
function trim( s )
+
local function facility( accept, attempt )
     -- Trim string
+
     -- Check string as possible file name or other source page
 
     -- Precondition:
 
     -- Precondition:
     --    s -- string or nil; to be trimmed
+
     --    accept  -- string; requirement
 +
    --                        file
 +
    --                        file+
 +
    --                        file:
 +
    --                        file:+
 +
    --                        image
 +
    --                        image+
 +
    --                        image:
 +
    --                        image:+
 +
    --    attempt -- string; to be tested
 
     -- Postcondition:
 
     -- Postcondition:
     --    Return trimmed string or nil
+
     --    Return error keyword, or false
     if type( s ) == "string" then
+
     -- Uses:
         if s:match( "^%s*$" ) then
+
    --    >< TemplatePar.extern.FileMedia
             s = ""
+
    --    Module:FileMedia
 +
    --    FileMedia.isType()
 +
    local r
 +
    if attempt and attempt ~= "" then
 +
        local FileMedia
 +
         if TemplatePar.extern.FileMedia then
 +
             FileMedia = TemplatePar.extern.FileMedia
 
         else
 
         else
             s = s:match( "^%s*(%S.*)$" ):match( "^(.*%S)%s*$" )
+
             local lucky
 +
            lucky, FileMedia = pcall( require, "Module:FileMedia" )
 +
            if type( FileMedia ) == "table" then
 +
                FileMedia                    = FileMedia.FileMedia()
 +
                TemplatePar.extern.FileMedia = FileMedia
 +
            end
 
         end
 
         end
 +
        if type( FileMedia ) == "table" 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
 
     end
     return s
+
     return r
end -- trim()
+
end -- facility()
  
  
Zeile 109: Zeile 181:
 
     --    Return some message string
 
     --    Return some message string
 
     -- Uses:
 
     -- Uses:
     --    >  messagePrefix
+
     --    >  MessagePrefix
     --    >  l10nDef
+
     --    >  L10nDef
 
     --    mw.language.getContentLanguage()
 
     --    mw.language.getContentLanguage()
 
     --    mw.message.new()
 
     --    mw.message.new()
 
     local c = mw.language.getContentLanguage():getCode()
 
     local c = mw.language.getContentLanguage():getCode()
     local m = mw.message.new( messagePrefix .. say )
+
     local m = mw.message.new( MessagePrefix .. say )
 
     local r = false
 
     local r = false
 
     if m:isBlank() then
 
     if m:isBlank() then
         local l10n = l10nDef[ c ]
+
         local l10n = L10nDef[ c ]
 
         if not l10n then
 
         if not l10n then
             l10n = l10nDef[ "en" ]
+
             local lucky
 +
            lucky, l10n = pcall( mw.loadData,
 +
                                string.format( "Module:%s/%s",
 +
                                                TemplatePar.suite, c ) )
 +
            if type( l10n ) == "table" then
 +
                L10nDef[ c ] = l10n
 +
            end
 +
        end
 +
        if type( l10n ) ~= "table" then
 +
            l10n = L10nDef.en
 
         end
 
         end
 
         r = l10n[ say ]
 
         r = l10n[ say ]
 +
        if not r then
 +
            r = L10nDef.en[ say ]
 +
        end
 
     else
 
     else
 
         m:inLanguage( c )
 
         m:inLanguage( c )
Zeile 127: Zeile 211:
 
     end
 
     end
 
     if not r then
 
     if not r then
         r = "(((".. say .. ")))"
+
         r = string.format( "(((%s)))", say )
 
     end
 
     end
 
     return r
 
     return r
 
end -- factory()
 
end -- factory()
 
 
 
local function failsafe( story, scan )
 
    -- Test for match (possibly user-defined with synrax 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 -- failsafe()
 
  
  
Zeile 163: Zeile 233:
 
         if type( options.template ) == "string" then
 
         if type( options.template ) == "string" then
 
             if #options.template > 0 then
 
             if #options.template > 0 then
                 r = r .. " (" .. options.template .. ")"
+
                 r = string.format( "%s (%s)", r, options.template )
 
             end
 
             end
 
         end
 
         end
 
     end
 
     end
 
     if suspect then
 
     if suspect then
         r = r .. ": " .. suspect
+
         r = string.format( "%s: %s", r, suspect )
 
     end
 
     end
 
     return r
 
     return r
 
end -- failure()
 
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:
 +
    --    >< TemplatePar.extern.Multilingual
 +
    --    Module:Multilingual
 +
    --    Multilingual.isType()
 +
    local r
 +
    if attempt and attempt ~= "" then
 +
        local Multilingual
 +
        if TemplatePar.extern.Multilingual then
 +
            Multilingual = TemplatePar.extern.Multilingual
 +
        else
 +
            local lucky
 +
            lucky, Multilingual = pcall( require, "Module:Multilingual" )
 +
            if type( Multilingual ) == "table" then
 +
                Multilingual = Multilingual.Multilingual()
 +
                TemplatePar.extern.Multilingual = Multilingual
 +
            end
 +
        end
 +
        if type( Multilingual ) == "table" 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 Multilingual.isLang( group[ i ], lazy ) then
 +
                        r = "invalid"
 +
                        break -- for i
 +
                    end
 +
                end -- for i
 +
            elseif Multilingual.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:
 +
    --    >< TemplatePar.extern.Multilingual
 +
    --    Module:Multilingual
 +
    --    Multilingual.isType()
 +
    local r
 +
    if attempt and attempt ~= "" then
 +
        local URLutil
 +
        if TemplatePar.extern.URLutil then
 +
            URLutil = TemplatePar.extern.URLutil
 +
        else
 +
            local lucky
 +
            lucky, URLutil = pcall( require, "Module:URLutil" )
 +
            if type( URLutil ) == "table" then
 +
                URLutil                    = URLutil.URLutil()
 +
                TemplatePar.extern.URLutil = URLutil
 +
            end
 +
        end
 +
        if type( URLutil ) == "table" 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()
  
  
Zeile 190: Zeile 380:
 
     end
 
     end
 
     if store then
 
     if store then
         r = store .. "; " .. s
+
         r = string.format( "%s; %s", store, s )
 
     else
 
     else
 
         r = s
 
         r = s
Zeile 196: Zeile 386:
 
     return r
 
     return r
 
end -- fault()
 
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:
 +
    --    >  Patterns
 +
    --    failure()
 +
    --    mw.text.trim()
 +
    --    facility()
 +
    --    fair()
 +
    --    containsCJK()
 +
    local r    = false
 +
    local s    = false
 +
    local show = nil
 +
    local scan = false
 +
    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 = Patterns[ s ]
 +
        end
 +
        if type( scan ) == "string" then
 +
            if s == "n" or s == "0,0" or s == "0.0" then
 +
                if not analyze:match( "[0-9]" )  and
 +
                  not analyze:match( "^%s*$" ) then
 +
                    scan = false
 +
                    if options.say then
 +
                        show = string.format( "'%s'", options.say )
 +
                    end
 +
                    if abbr then
 +
                        r = show
 +
                    else
 +
                        r = failure( "invalid", show, options )
 +
                    end
 +
                end
 +
            end
 +
        elseif s ~= "*" then
 +
            local op, n, plus = s:match( "([<!=>]=?)([-0-9][%S]*)(+?)" )
 +
            if op then
 +
                n = tonumber( n )
 +
                if n then
 +
                    local i = tonumber( analyze )
 +
                    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( "^image%+?:?$" )  or
 +
                  s:match( "^file%+?:?$" ) then
 +
                r = facility( s, analyze )
 +
                n = true
 +
            elseif s:match( "langs?W?%+?" ) then
 +
                r = familiar( s, analyze )
 +
                n = true
 +
            elseif s:match( "url%+?" ) then
 +
                r = far( s, analyze )
 +
                n = true
 +
            end
 +
-- datetime+
 +
-- iso8631+
 +
-- line+
 +
            if not n and not r then
 +
                r = "unknownRule"
 +
            end
 +
            if r then
 +
                if options.say then
 +
                    show = string.format( "'%s' %s", options.say, s )
 +
                else
 +
                    show = s
 +
                end
 +
                if abbr then
 +
                    r = show
 +
                else
 +
                    r = failure( r, show, options )
 +
                end
 +
            end
 +
        end
 +
    end
 +
    if scan then
 +
        local legal, got = pcall( fair, analyze, scan )
 +
        if legal then
 +
            if not got then
 +
                if s == "aa" then
 +
                    got = containsCJK( analyze )
 +
                end
 +
                if not got then
 +
                    if options.say then
 +
                        show = string.format( "'%s'", options.say )
 +
                    end
 +
                    if abbr then
 +
                        r = show
 +
                    else
 +
                        r = failure( "invalid", show, options )
 +
                    end
 +
                end
 +
            end
 +
        else
 +
            r = failure( "badPattern",
 +
                        string.format( "%s *** %s", scan, got ),
 +
                        options )
 +
        end
 +
    end
 +
    return r
 +
end -- feasible()
  
  
Zeile 206: Zeile 538:
 
     -- Postcondition:
 
     -- Postcondition:
 
     --    Return true iff found
 
     --    Return true iff found
     local k, v
+
     local k, v, r
 
     for k, v in pairs( haystack ) do
 
     for k, v in pairs( haystack ) do
 
         if k == needle then
 
         if k == needle then
             return true
+
             r = true
 
         end
 
         end
 
     end -- for k, v
 
     end -- for k, v
     return false
+
     return r or false
 
end -- fed()
 
end -- fed()
  
Zeile 234: Zeile 566:
 
         g = TemplatePar.downcase( options )
 
         g = TemplatePar.downcase( options )
 
     else
 
     else
         g = mw.getCurrentFrame()
+
         TemplatePar.frame = mw.getCurrentFrame()
 +
        g                = TemplatePar.frame
 
         if light then
 
         if light then
 
             g = g:getParent()
 
             g = g:getParent()
Zeile 260: Zeile 593:
 
     return r
 
     return r
 
end -- fetch()
 
end -- fetch()
 +
 +
 +
 +
local function figure( append, options )
 +
    -- Extend options by rule from #invoke strings
 +
    -- Precondition:
 +
    --    append  -- string or nil; requested rule
 +
    --    options  --  table; details
 +
    --                  ++ .key
 +
    --                  ++ .pattern
 +
    -- Postcondition:
 +
    --    Return sequence table
 +
    local r = options
 +
    if type( append ) == "string" then
 +
        local story = mw.text.trim( append )
 +
        local sub  = story:match( "^/(.*%S)/$" )
 +
        if type( sub ) == "string" then
 +
            sub            = sub:gsub( "%%!", "|" )
 +
                                :gsub( "%%%(%(", "{{" )
 +
                                :gsub( "%%%)%)", "}}" )
 +
                                :gsub( "\\n", string.char( 10 ) )
 +
            options.pattern = sub
 +
            options.key    = nil
 +
        else
 +
            options.key    = story
 +
            options.pattern = nil
 +
        end
 +
    end
 +
    return r
 +
end -- figure()
  
  
Zeile 294: Zeile 657:
 
     --    submit  -- string or false or nil; non-empty error message
 
     --    submit  -- string or false or nil; non-empty error message
 
     --    options  -- table or nil; optional details
 
     --    options  -- table or nil; optional details
     --                options.noError
+
     --                options.format
 +
    --                options.preview
 
     --                options.cat
 
     --                options.cat
 
     --                options.template
 
     --                options.template
Zeile 303: Zeile 667:
 
     local r = false
 
     local r = false
 
     if submit then
 
     if submit then
 +
        local lazy  = false
 +
        local learn = false
 +
        local show  = false
 
         local opt, s
 
         local opt, s
 
         if type( options ) == "table" then
 
         if type( options ) == "table" then
             opt = options
+
             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 = "{{REVISIONID}}"
 +
                if lazy then
 +
                    show = ""
 +
                    lazy = false
 +
                end
 +
                if not TemplatePar.frame then
 +
                    TemplatePar.frame = mw.getCurrentFrame()
 +
                end
 +
                if TemplatePar.frame:preprocess( sniffer ) == "" then
 +
                    if s == "1" then
 +
                        show = "*"
 +
                    else
 +
                        show = s
 +
                    end
 +
                    learn = true
 +
                end
 +
            end
 
         else
 
         else
 
             opt = { }
 
             opt = { }
 
         end
 
         end
         if opt.noError then
+
         if lazy then
 
             if not opt.cat then
 
             if not opt.cat then
                 r = submit .. " " .. factory( "noErrorCat" )
+
                 r = string.format( "%s %s",
 +
                                  submit,  factory( "noMSGnoCAT" ) )
 
             end
 
             end
 
         else
 
         else
 
             r = submit
 
             r = submit
 
         end
 
         end
         if r then
+
         if r and  not lazy then
             r = "<span class='error'>" .. r .. "</span>"
+
             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|%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
 
         end
 
         s = opt.cat
 
         s = opt.cat
 
         if type( s ) == "string" then
 
         if type( s ) == "string" then
             if not r then
+
             local link
              r = ""
+
            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
 
             end
             if s:find( "@@@" ) then
+
             if link then
                 if type( opt.template ) == "string" then
+
                local cats, i
                    s = s:gsub( "@@@", opt.template )
+
                 if not r then
 +
                  r = ""
 
                 end
 
                 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[[Category:%s]]", r, s )
 +
                    end
 +
                end -- for i
 
             end
 
             end
            r = r .. "[[Category:" .. s .. "]]"
 
 
         end
 
         end
 
     end
 
     end
Zeile 377: Zeile 834:
 
     end -- for k, v
 
     end -- for k, v
 
     if r then
 
     if r then
         r = failure( "unknown", r, options )
+
         r = failure( "unknown",
 +
                    string.format( "'%s'", r ),
 +
                    options )
 
     else -- all names valid
 
     else -- all names valid
 
         local i, s
 
         local i, s
Zeile 491: Zeile 950:
  
  
local function form( light, options )
+
local function form( light, options, frame )
 
     -- Run parameter analysis on current environment
 
     -- Run parameter analysis on current environment
 
     -- Precondition:
 
     -- Precondition:
Zeile 498: Zeile 957:
 
     --                options.mandatory
 
     --                options.mandatory
 
     --                options.optional
 
     --                options.optional
 +
    --    frame    -- object; #invoke environment, or false
 
     -- Postcondition:
 
     -- Postcondition:
 
     --    Return string with error message as configured;
 
     --    Return string with error message as configured;
 
     --            false if valid
 
     --            false if valid
 
     -- Uses:
 
     -- Uses:
 +
    --      < TemplatePar.frame
 
     --    fold()
 
     --    fold()
    --    failure()
 
 
     --    fetch()
 
     --    fetch()
 
     --    fix()
 
     --    fix()
 
     --    finalize()
 
     --    finalize()
 
     local duty, r
 
     local duty, r
 +
    if frame then
 +
        TemplatePar.frame = frame
 +
    else
 +
        TemplatePar.frame = mw.getCurrentFrame()
 +
    end
 
     if type( options ) == "table" then
 
     if type( options ) == "table" then
 
         if type( options.mandatory ) ~= "table" then
 
         if type( options.mandatory ) ~= "table" then
Zeile 535: Zeile 1.000:
  
  
local function format( seek, options )
+
local function format( analyze, options )
     -- Check validity of one particular template parameter
+
     -- Check validity of a value
 
     -- Precondition:
 
     -- Precondition:
     --    seek    -- string non-empty; name of template parameter
+
     --    analyze  -- string to be analyzed
 
     --    options  -- table or nil; optional details
 
     --    options  -- table or nil; optional details
     --                options.pattern
+
     --                options.say
    --                options.key
 
    --                options.low
 
 
     --                options.min
 
     --                options.min
 
     --                options.max
 
     --                options.max
Zeile 549: Zeile 1.012:
 
     --            false if valid or no answer permitted
 
     --            false if valid or no answer permitted
 
     -- Uses:
 
     -- Uses:
     --    >  Patterns
+
     --    feasible()
    --    TemplatePar.downcase()
 
    --    mw.getCurrentFrame()
 
    --    frame:getParent()
 
 
     --    failure()
 
     --    failure()
    --    failsafe()
+
     local r = feasible( analyze, options, false )
    local r = false
+
     local show
    local s
 
     local scan  = false
 
    local story
 
    if type( options ) ~= "table" then
 
        options = { }
 
    end
 
    if options.low then
 
        story = TemplatePar.downcase( options )
 
    else
 
        story = mw.getCurrentFrame():getParent()
 
    end
 
    if type( story ) == "table" then
 
        story = ( story.args[ seek ] or "" )
 
        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 = trim( options.key )
 
            else
 
                s = "+"
 
            end
 
            scan = Patterns[ s ]
 
            if type( scan ) == "string" then
 
                if s == "n" or s == "0,0" or s == "0.0" then
 
                  if not story:match( "[0-9]" ) then
 
                        scan = false
 
                        r    = failure( "invalid",
 
                                        "'" .. seek .. "'",
 
                                        options )
 
                    end
 
                end
 
            else
 
                r = failure( "unknownRule", s, options )
 
            end
 
        end
 
     else
 
        r = story
 
    end
 
    if scan then
 
        local legal, got = pcall( failsafe, story, scan )
 
        if legal then
 
            if not got then
 
                r = failure( "invalid",  "'" .. seek .. "'",  options )
 
            end
 
        else
 
            r = failure( "badPattern",
 
                        scan .. " *** " .. got,
 
                        options )
 
        end
 
    end
 
 
     if options.min  and  not r then
 
     if options.min  and  not r then
 
         if type( options.min ) == "number" then
 
         if type( options.min ) == "number" then
Zeile 615: Zeile 1.021:
 
                 if options.max < options.min then
 
                 if options.max < options.min then
 
                     r = failure( "minmax",
 
                     r = failure( "minmax",
                                 tostring( options.min )
+
                                 string.format( "%d > %d",
                                .. " > " ..
+
                                                options.min,
                                tostring( options.max ),
+
                                                options.max ),
 
                                 options )
 
                                 options )
 
                 end
 
                 end
 
             end
 
             end
             if #story < options.min  and  not r then
+
             if #analyze < options.min  and  not r then
                 r = failure( "tooShort",
+
                 show = " <" .. options.min
                            " <" .. options.min .. " '" .. seek .. "'",
+
                if options.say then
                            options )
+
                    show = string.format( "%s '%s'", show, options.say )
 +
                end
 +
                r = failure( "tooShort", show, options )
 
             end
 
             end
 
         else
 
         else
Zeile 632: Zeile 1.040:
 
     if options.max  and  not r then
 
     if options.max  and  not r then
 
         if type( options.max ) == "number" then
 
         if type( options.max ) == "number" then
             if #story > options.max then
+
             if #analyze > options.max then
                 r = failure( "tooLong",
+
                 show = " >" .. options.max
                            " >" .. options.max .. " '" .. seek .. "'",
+
                if options.say then
                            options )
+
                    show = string.format( "%s '%s'", show, options.say )
 +
                end
 +
                r = failure( "tooLong", show, options )
 
             end
 
             end
 
         else
 
         else
Zeile 643: Zeile 1.053:
 
     return r
 
     return r
 
end -- format()
 
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 <br />
 +
    --                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<br />%s", append, r )
 +
            end
 +
        else
 +
            error( r, 0 )
 +
        end
 +
    end
 +
    return r
 +
end -- TemplatePar.assert()
  
  
Zeile 657: Zeile 1.202:
 
     -- Uses:
 
     -- Uses:
 
     --    form()
 
     --    form()
     return form( true, options )
+
     return form( true, options, false )
 
end -- TemplatePar.check()
 
end -- TemplatePar.check()
  
Zeile 678: Zeile 1.223:
 
     return r
 
     return r
 
end -- TemplatePar.count()
 
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()
  
  
Zeile 690: Zeile 1.256:
 
     --    mw.getCurrentFrame()
 
     --    mw.getCurrentFrame()
 
     --    frame:getParent()
 
     --    frame:getParent()
     --    mw.ustring.lower()
+
     --    flat()
    --    fault()
 
    --    failure()
 
 
     local t = mw.getCurrentFrame():getParent()
 
     local t = mw.getCurrentFrame():getParent()
 
     return flat( t.args, options )
 
     return flat( t.args, options )
Zeile 699: Zeile 1.263:
  
  
TemplatePar.valid = function ( seek, options )
+
TemplatePar.failsafe = function ( assert )
 +
    -- Retrieve versioning and check for compliance
 +
    -- Precondition:
 +
    --    assert  -- string, with required version or "wikidata",
 +
    --                or false
 +
    -- Postcondition:
 +
    --    Returns  string with appropriate version, or false
 +
    local r
 +
    if since == "wikidata" then
 +
        local item = TemplatePar.item
 +
        since = false
 +
        if type( item ) == "number"  and  item > 0 then
 +
            local ent = mw.wikibase.getEntity( string.format( "Q%d",
 +
                                                              item ) )
 +
            if type( ent ) == "table" then
 +
                local vsn = ent:formatPropertyValues( "P348" )
 +
                if type( vsn ) == "table"  and
 +
                  type( vsn.value) == "string"  and
 +
                  vsn.value ~= "" then
 +
                    r = vsn.value
 +
                end
 +
            end
 +
        end
 +
    end
 +
    if not r then
 +
        if not since  or  since <= TemplatePar.serial then
 +
            r = TemplatePar.serial
 +
        else
 +
            r = false
 +
        end
 +
    end
 +
    return r
 +
end -- TemplatePar.failsafe()
 +
 
 +
 
 +
 
 +
TemplatePar.valid = function ( access, options )
 
     -- Check validity of one particular template parameter
 
     -- Check validity of one particular template parameter
 
     -- Precondition:
 
     -- Precondition:
     --    seek     -- string; name of template parameter
+
     --    access  -- id of parameter in template transclusion
 +
     --                 string or number
 
     --    options  -- table or nil; optional details
 
     --    options  -- table or nil; optional details
 
     -- Postcondition:
 
     -- Postcondition:
Zeile 708: Zeile 1.309:
 
     --            false if valid or no answer permitted
 
     --            false if valid or no answer permitted
 
     -- Uses:
 
     -- Uses:
     --    trim()
+
     --    >< TemplatePar.frame
     --    format()
+
    --    mw.text.trim()
 +
     --    TemplatePar.downcase()
 +
    --    frame:getParent()
 +
    --    formatted()
 
     --    failure()
 
     --    failure()
 
     --    finalize()
 
     --    finalize()
     local r
+
     local r = type( access )
     if type( seek ) == "string" then
+
     if r == "string" then
         r = trim( seek )
+
         r = mw.text.trim( access )
 
         if #r == 0 then
 
         if #r == 0 then
 
             r = false
 
             r = false
 
         end
 
         end
 +
    elseif r == "number" then
 +
        r = access
 +
    else
 +
        r = false
 
     end
 
     end
 
     if r then
 
     if r then
         r = format( seek, options )
+
        local params
 +
        if type( options ) ~= "table" then
 +
            options = { }
 +
        end
 +
        if options.low then
 +
            params = TemplatePar.downcase( options )
 +
        else
 +
            if not TemplatePar.frame then
 +
                TemplatePar.frame = mw.getCurrentFrame()
 +
            end
 +
            params = TemplatePar.frame:getParent()
 +
        end
 +
         r = formatted( params, access, options )
 
     else
 
     else
 
         r = failure( "noname", false, options )
 
         r = failure( "noname", false, options )
 
     end
 
     end
     return finalize( r, options )
+
     return finalize( r, options, frame )
 
end -- TemplatePar.valid()
 
end -- TemplatePar.valid()
  
Zeile 738: Zeile 1.358:
 
     -- Uses:
 
     -- Uses:
 
     --    form()
 
     --    form()
     return form( false, options )
+
     return form( false, options, false )
 
end -- TemplatePar.verify()
 
end -- TemplatePar.verify()
  
Zeile 745: Zeile 1.365:
 
-- Provide external access
 
-- Provide external access
 
local p = {}
 
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()
  
  
Zeile 757: Zeile 1.390:
 
     --    form()
 
     --    form()
 
     --    fill()
 
     --    fill()
     local options = { optional  = { "1",
+
     local options = { optional  = { "all",
                                     "2",
+
                                     "opt",
 
                                     "cat",
 
                                     "cat",
 +
                                    "errNS",
 
                                     "low",
 
                                     "low",
                                     "noError",
+
                                     "format",
 +
                                    "preview",
 
                                     "template" },
 
                                     "template" },
                       template  = "#invoke:TemplatePar|check|"
+
                       template  = "&#35;invoke:TemplatePar|check|"
 
                     }
 
                     }
     local r = form( false, options )
+
     local r = form( false, options, frame )
 
     if not r then
 
     if not r then
         options = { mandatory = fill( frame.args[ 1 ] ),
+
         options = { mandatory = fill( frame.args.all ),
                     optional  = fill( frame.args[ 2 ] ),
+
                     optional  = fill( frame.args.opt ),
 
                     cat      = frame.args.cat,
 
                     cat      = frame.args.cat,
 +
                    errNS    = frame.args.errNS,
 
                     low      = frame.args.low,
 
                     low      = frame.args.low,
                     noError   = frame.args.noError,
+
                     format    = frame.args.format,
 +
                    preview   = frame.args.preview,
 
                     template  = frame.args.template
 
                     template  = frame.args.template
 
                   }
 
                   }
         r      = form( true, options )
+
         r      = form( true, options, frame )
 
     end
 
     end
 
     return r or ""
 
     return r or ""
end -- .check()
+
end -- p.check()
  
  
Zeile 788: Zeile 1.425:
 
     --    TemplatePar.count()
 
     --    TemplatePar.count()
 
     return tostring( TemplatePar.count() )
 
     return tostring( TemplatePar.count() )
end -- .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.valid( frame )
+
function p.match( frame )
     -- Check validity of one particular template parameter
+
     -- Combined analysis of parameters and their values
 
     -- Precondition:
 
     -- Precondition:
 
     --    frame  -- object; #invoke environment
 
     --    frame  -- object; #invoke environment
Zeile 799: Zeile 1.447:
 
     --    Return string with error message or ""
 
     --    Return string with error message or ""
 
     -- Uses:
 
     -- Uses:
 +
    --      < TemplatePar.frame
 +
    --    mw.text.trim()
 +
    --    mw.ustring.lower()
 +
    --    failure()
 
     --    form()
 
     --    form()
     --    trim()
+
     --    TemplatePar.downcase()
     --    TemplatePar.valid()
+
     --    figure()
     local options = { mandatory = { "1" },
+
    --    feasible()
                       optional  = { "2",
+
    --    fault()
                                    "cat",
+
    --    finalize()
                                    "low",
+
    local r = false
                                    "max",
+
     local options = { cat      = frame.args.cat,
                                    "min",
+
                       errNS    = frame.args.errNS,
                                    "noError",
+
                      low      = frame.args.low,
                                    "template" },
+
                      format  = frame.args.format,
                       template = "#invoke:TemplatePar|valid|"
+
                      preview  = frame.args.preview,
 +
                       template = frame.args.template
 
                     }
 
                     }
     local r      = form( false, options )
+
     local k, v, s
     if not r then
+
     local params = { }
        local s = trim( frame.args[ 2 ] )
+
    TemplatePar.frame = frame
        options = { cat      = frame.args.cat,
+
    for k, v in pairs( frame.args ) do
                    low      = frame.args.low,
+
         if type( k ) == "number" then
                    noError  = frame.args.noError,
+
             s, v = v:match( "^ *([^=]+) *= *(%S.*%S*) *$" )
                    template = frame.args.template
+
             if s then
                  }
+
                 s = mw.text.trim( s )
         if type( s ) == "string" then
+
                 if s == "" then
             local sub = s:match( "^/(.*%S)/$" )
+
                    s = false
             if type( sub ) == "string" then
+
                 end
                 sub = sub:gsub( "%%!", "|" )
 
                 sub = sub:gsub( "%%%(%(", "{{" )
 
                sub = sub:gsub( "%%%)%)", "}}" )
 
                options.pattern = sub
 
            else
 
                 options.key = s
 
 
             end
 
             end
        end
 
        if type( frame.args.min ) == "string" then
 
            s = frame.args.min:match( "^%s*([0-9]+)%s*$" )
 
 
             if s then
 
             if s then
                 options.min = tonumber( s )
+
                 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
 
             else
                 r = failure( "invalidPar",
+
                 r = failure( "invalidPar", tostring( k ), options )
                            "min=" .. frame.args.min,
+
                break -- for k, v
                            options )
 
 
             end
 
             end
 
         end
 
         end
         if type( frame.args.max ) == "string" then
+
    end -- for k, v
             s = frame.args.max:match( "^%s*([1-9][0-9]*)%s*$" )
+
    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
 +
            errValue    = false
 +
            s = targs[ k ]
 
             if s then
 
             if s then
                 options.max = tonumber( s )
+
                 if s == "" then
 +
                    lack = true
 +
                else
 +
                    lack = false
 +
                end
 
             else
 
             else
                 r = failure( "invalidPar",
+
                 s    = ""
                            "max=" .. frame.args.max,
+
                lack = true
                            options )
 
 
             end
 
             end
         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
 +
                            errMiss = string.format( "%s, '%s'",
 +
                                                    errMiss, k )
 +
                        else
 +
                            errMiss = string.format( "'%s'", k )
 +
                        end
 +
                    elseif not errMiss then
 +
                        errValues = fault( errValues, r )
 +
                    end
 +
                    break -- for r, rule
 +
                end
 +
            end -- for s, rule
 +
         end -- for k, v
 +
        r = ( errMiss or errValues )
 
         if r then
 
         if r then
 +
            if errMiss then
 +
                r = failure( "undefined", errMiss, options )
 +
            else
 +
                r = failure( "invalid", errValues, options )
 +
            end
 
             r = finalize( r, options )
 
             r = finalize( r, options )
        else
 
            s = frame.args[ 1 ] or ""
 
            r = TemplatePar.valid( s, options )
 
 
         end
 
         end
 
     end
 
     end
 
     return r or ""
 
     return r or ""
end -- .valid()
+
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 )
 +
    -- Check or retrieve version information
 +
    -- Precondition:
 +
    --    frame  -- object; #invoke environment
 +
    -- Postcondition:
 +
    --    Return string with error message or ""
 +
    -- Uses:
 +
    --    TemplatePar.failsafe()
 +
    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 TemplatePar.failsafe( since )  or  ""
 +
end -- p.failsafe()
  
  
Zeile 868: Zeile 1.603:
 
     --    Return table with functions
 
     --    Return table with functions
 
     return TemplatePar
 
     return TemplatePar
end -- .TemplatePar()
+
end -- p.TemplatePar()
  
  
  
 
return p
 
return p

Aktuelle Version vom 6. September 2019, 12:27 Uhr

local TemplatePar = { serial = "2018-08-10",

                     suite  = "TemplatePar",
                     item   = 15393417,
                     extern = { },
                     frame  = false }

--[=[ Template parameter utility

  • assert
  • check
  • count
  • countNotEmpty
  • downcase()
  • duplicates
  • match
  • valid
  • verify()
  • TemplatePar()
  • failsafe()

]=]


-- Module globals local MessagePrefix = "lua-module-TemplatePar-" local L10nDef = {} L10nDef.en = {

   badPattern  = "#invoke:TemplatePar pattern syntax error",
   dupOpt      = "#invoke:TemplatePar repeated optional parameter",
   dupRule     = "#invoke:TemplatePar conflict key/pattern",
   empty       = "Error in template * undefined value for mandatory",
   invalid     = "Error in template * invalid parameter",
   invalidPar  = "#invoke:TemplatePar invalid parameter",
   minmax      = "#invoke:TemplatePar min > max",
   missing     = "#invoke:TemplatePar missing library",
   multiSpell  = "Error in template * multiple spelling of parameter",
   noMSGnoCAT  = "#invoke:TemplatePar neither message nor category",
   noname      = "#invoke:TemplatePar missing parameter name",
   notFound    = "Error in template * missing page",
   tooLong     = "Error in template * parameter too long",
   tooShort    = "Error in template * parameter too short",
   undefined   = "Error in template * mandatory parameter missing",
   unknown     = "Error in template * unknown parameter name",
   unknownRule = "#invoke:TemplatePar unknown rule"

} local Patterns = {

   [ "ASCII" ]    = "^[ -~]*$",
   [ "ASCII+" ]   = "^[ -~]+$",
   [ "ASCII+1" ]  = "^[!-~]+$",
   [ "n" ]        = "^[%-]?[0-9]*$",
   [ "n>0" ]      = "^[0-9]*[1-9][0-9]*$",
   [ "N+" ]       = "^[%-]?[1-9][0-9]*$",
   [ "N>0" ]      = "^[1-9][0-9]*$",
   [ "x" ]        = "^[0-9A-Fa-f]*$",
   [ "x+" ]       = "^[0-9A-Fa-f]+$",
   [ "X" ]        = "^[0-9A-F]*$",
   [ "X+" ]       = "^[0-9A-F]+$",
   [ "0,0" ]      = "^[%-]?[0-9]*,?[0-9]*$",
   [ "0,0+" ]     = "^[%-]?[0-9]+,[0-9]+$",
   [ "0,0+?" ]    = "^[%-]?[0-9]+,?[0-9]*$",
   [ "0.0" ]      = "^[%-]?[0-9]*[%.]?[0-9]*$",
   [ "0.0+" ]     = "^[%-]?[0-9]+%.[0-9]+$",
   [ "0.0+?" ]    = "^[%-]?[0-9]+[%.]?[0-9]*$",
   [ ".0+" ]      = "^[%-]?[0-9]*[%.]?[0-9]+$",
   [ "ID" ]       = "^[A-Za-z]?[A-Za-z_0-9]*$",
   [ "ID+" ]      = "^[A-Za-z][A-Za-z_0-9]*$",
   [ "ABC" ]      = "^[A-Z]*$",
   [ "ABC+" ]     = "^[A-Z]+$",
   [ "Abc" ]      = "^[A-Z]*[a-z]*$",
   [ "Abc+" ]     = "^[A-Z][a-z]+$",
   [ "abc" ]      = "^[a-z]*$",
   [ "abc+" ]     = "^[a-z]+$",
   [ "aBc+" ]     = "^[a-z]+[A-Z][A-Za-z]*$",
   [ "w" ]        = "^%S*$",
   [ "w+" ]       = "^%S+$",
   [ "base64" ]   = "^[A-Za-z0-9%+/]*$",
   [ "base64+" ]  = "^[A-Za-z0-9%+/]+$",
   [ "aa" ]       = "[%a%a].*[%a%a]",
   [ "pagename" ] = string.format( "^[^#<>%%[%%]|{}%c-%c%c]+$",
                                   1, 31, 127 ),
   [ "ref" ]      = string.format( "%c'%c`UNIQ%s%sref%s%s%sQINU`%c'%c",
                                   127, 34, "%-", "%-", "%-", "%x+",
                                   "%-", 34, 127 ),
   [ "+" ]        = "%S"

} local patternCJK = false


local function containsCJK( s )

   -- Is any CJK character present?
   -- Precondition:
   --     s  -- string
   -- Postcondition:
   --     Return false iff no CJK present
   -- Uses:
   --     >< patternCJK
   --     mw.ustring.char()
   --     mw.ustring.match()
   local r = false
   if not patternCJK then
       patternCJK = mw.ustring.char( 91,
                                      13312, 45,  40959,
                                     131072, 45, 178207,
                                     93 )
   end
   if mw.ustring.match( s, 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:
   --     >< TemplatePar.extern.FileMedia
   --     Module:FileMedia
   --     FileMedia.isType()
   local r
   if attempt and attempt ~= "" then
       local FileMedia
       if TemplatePar.extern.FileMedia then
           FileMedia = TemplatePar.extern.FileMedia
       else
           local lucky
           lucky, FileMedia = pcall( require, "Module:FileMedia" )
           if type( FileMedia ) == "table" then
               FileMedia                    = FileMedia.FileMedia()
               TemplatePar.extern.FileMedia = FileMedia
           end
       end
       if type( FileMedia ) == "table" 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:
   --     >  MessagePrefix
   --     >  L10nDef
   --     mw.language.getContentLanguage()
   --     mw.message.new()
   local c = mw.language.getContentLanguage():getCode()
   local m = mw.message.new( MessagePrefix .. say )
   local r = false
   if m:isBlank() then
       local l10n = L10nDef[ c ]
       if not l10n then
           local lucky
           lucky, l10n = pcall( mw.loadData,
                                string.format( "Module:%s/%s",
                                               TemplatePar.suite, c ) )
           if type( l10n ) == "table" then
               L10nDef[ c ] = l10n
           end
       end
       if type( l10n ) ~= "table" then
           l10n = L10nDef.en
       end
       r = l10n[ say ]
       if not r then
           r = 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 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:
   --     >< TemplatePar.extern.Multilingual
   --     Module:Multilingual
   --     Multilingual.isType()
   local r
   if attempt and attempt ~= "" then
       local Multilingual
       if TemplatePar.extern.Multilingual then
           Multilingual = TemplatePar.extern.Multilingual
       else
           local lucky
           lucky, Multilingual = pcall( require, "Module:Multilingual" )
           if type( Multilingual ) == "table" then
               Multilingual = Multilingual.Multilingual()
               TemplatePar.extern.Multilingual = Multilingual
           end
       end
       if type( Multilingual ) == "table" 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 Multilingual.isLang( group[ i ], lazy ) then
                       r = "invalid"
                       break -- for i
                   end
               end -- for i
           elseif Multilingual.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:
   --     >< TemplatePar.extern.Multilingual
   --     Module:Multilingual
   --     Multilingual.isType()
   local r
   if attempt and attempt ~= "" then
       local URLutil
       if TemplatePar.extern.URLutil then
           URLutil = TemplatePar.extern.URLutil
       else
           local lucky
           lucky, URLutil = pcall( require, "Module:URLutil" )
           if type( URLutil ) == "table" then
               URLutil                    = URLutil.URLutil()
               TemplatePar.extern.URLutil = URLutil
           end
       end
       if type( URLutil ) == "table" 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 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:
   --     >  Patterns
   --     failure()
   --     mw.text.trim()
   --     facility()
   --     fair()
   --     containsCJK()
   local r    = false
   local s    = false
   local show = nil
   local scan = false
   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 = Patterns[ s ]
       end
       if type( scan ) == "string" then
           if s == "n" or s == "0,0" or s == "0.0" then
               if not analyze:match( "[0-9]" )  and
                  not analyze:match( "^%s*$" ) then
                   scan = false
                   if options.say then
                       show = string.format( "'%s'", options.say )
                   end
                   if abbr then
                       r = show
                   else
                       r = failure( "invalid", show, options )
                   end
               end
           end
       elseif s ~= "*" then
           local op, n, plus = s:match( "([<!=>]=?)([-0-9][%S]*)(+?)" )
           if op then
               n = tonumber( n )
               if n then
                   local i = tonumber( analyze )
                   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( "^image%+?:?$" )  or
                  s:match( "^file%+?:?$" ) then
               r = facility( s, analyze )
               n = true
           elseif s:match( "langs?W?%+?" ) then
               r = familiar( s, analyze )
               n = true
           elseif s:match( "url%+?" ) then
               r = far( s, analyze )
               n = true
           end

-- datetime+ -- iso8631+ -- line+

           if not n and not r then
               r = "unknownRule"
           end
           if r then
               if options.say then
                   show = string.format( "'%s' %s", options.say, s )
               else
                   show = s
               end
               if abbr then
                   r = show
               else
                   r = failure( r, show, options )
               end
           end
       end
   end
   if scan then
       local legal, got = pcall( fair, analyze, scan )
       if legal then
           if not got then
               if s == "aa" then
                   got = containsCJK( analyze )
               end
               if not got then
                   if options.say then
                       show = string.format( "'%s'", options.say )
                   end
                   if abbr then
                       r = show
                   else
                       r = failure( "invalid", show, options )
                   end
               end
           end
       else
           r = failure( "badPattern",
                        string.format( "%s *** %s", scan, got ),
                        options )
       end
   end
   return r

end -- feasible()


local function fed( haystack, needle )

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

end -- fed()


local function fetch( light, options )

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

end -- fetch()


local function figure( append, options )

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

end -- figure()


local function fill( specified )

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

end -- fill()


local function finalize( submit, options )

   -- Finalize message
   -- Precondition:
   --     submit   -- string or false or nil; non-empty error message
   --     options  -- table or nil; optional details
   --                 options.format
   --                 options.preview
   --                 options.cat
   --                 options.template
   -- Postcondition:
   --     Return string or false
   -- Uses:
   --     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 = "3926"
               if lazy then
                   show = ""
                   lazy = false
               end
               if not TemplatePar.frame then
                   TemplatePar.frame = mw.getCurrentFrame()
               end
               if TemplatePar.frame:preprocess( sniffer ) == "" then
                   if s == "1" then
                       show = "*"
                   else
                       show = s
                   end
                   learn = true
               end
           end
       else
           opt = { }
       end
       if lazy then
           if not opt.cat then
               r = string.format( "%s %s",
                                  submit,  factory( "noMSGnoCAT" ) )
           end
       else
           r = submit
       end
       if r  and  not lazy then
           local i
           if not show  or  show == "*" then
               local e = mw.html.create( "span" )
                                :attr( "class", "error" )
                                :wikitext( "@@@" )
               if learn then
                   local max  = 1000000000
                   local id   = math.floor( os.clock() * max )
                   local sign = string.format( "error_%d", id )
                   local btn  = mw.html.create( "span" )
                   local top  = mw.html.create( "div" )
                   e:attr( "id", sign )
                   btn:css( { ["background"]      = "#FFFF00",
                              ["border"]          = "#FF0000 3px solid",
                              ["font-weight"]     = "bold",
                              ["padding"]         = "2px",
                              ["text-decoration"] = "none" } )
                      :wikitext( ">>>" )
                   sign = string.format( "%s",
                                         sign,  tostring( btn ) )
                   top:wikitext( sign, " ", submit )
                   mw.addWarning( tostring( top ) )
               end
               show = tostring( e )
           end
           i = show:find( "@@@", 1, true )
           if i then
               -- No gsub() since r might contain "%3" (e.g. URL)
               r = string.format( "%s%s%s",
                                  show:sub( 1,  i - 1 ),
                                  r,
                                  show:sub( i + 3 ) )
           else
               r = show
           end
       end
       if learn and r then
           -- r = fatal( r )
       end
       s = opt.cat
       if type( s ) == "string" then
           local link
           if opt.errNS then
               local ns = mw.title.getCurrentTitle().namespace
               local st = type( opt.errNS )
               if st == "string" then
                   local space  = string.format( ".*%%s%d%%s.*", ns )
                   local spaces = string.format( " %s ", opt.errNS )
                   if spaces:match( space ) then
                       link = true
                   end
               elseif st == "table" then
                   for i = 1, #opt.errNS do
                       if opt.errNS[ i ] == ns then
                           link = true
                           break    -- for i
                       end
                   end -- for i
               end
           else
               link = true
           end
           if link then
               local cats, i
               if not r then
                  r = ""
               end
               if s:find( "@@@" ) then
                   if type( opt.template ) == "string" then
                       s = s:gsub( "@@@", opt.template )
                   end
               end
               cats = mw.text.split( s, "%s*#%s*" )
               for i = 1, #cats do
                   s = mw.text.trim( cats[ i ] )
                   if #s > 0 then
                       r = string.format( "%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 k, v
   local r = false
   for k, v in pairs( got ) do
       if not finder( valid, k ) then
           r = fault( r, k )
       end
   end -- for k, v
   if r then
       r = failure( "unknown",
                    string.format( "'%s'", r ),
                    options )
   else -- all names valid
       local i, s
       for i = 1, #duty do
           s = duty[ i ]
           if not fed( got, s ) then
               r = fault( r, s )
           end
       end -- for i
       if r then
           r = failure( "undefined", r, options )
       else -- all mandatory present
           for i = 1, #duty do
               s = duty[ i ]
               if not got[ s ] then
                   r = fault( r, s )
               end
           end -- for i
           if r then
               r = failure( "empty", r, options )
           end
       end
   end
   return r

end -- fix()


local function flat( collection, options )

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

end -- flat()


local function fold( options )

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

end -- fold()


local function form( light, options, frame )

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

end -- form()


local function format( analyze, options )

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

end -- format()


local function formatted( assignment, access, options )

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

end -- formatted()


local function furnish( frame, action )

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

end -- furnish()


TemplatePar.assert = function ( analyze, append, options )

   -- Perform parameter analysis on a single string
   -- Precondition:
   --     analyze  -- string to be analyzed
   --     append   -- string: append error message, prepending 
-- false or nil: throw error with message -- options -- table; optional details -- Postcondition: -- Return string with error message as configured; -- false if valid -- Uses: -- format() local r = format( analyze, options ) if ( r ) then if ( type( append ) == "string" ) then if ( append ~= "" ) then r = string.format( "%s
%s", append, r ) end else error( r, 0 ) end end return r

end -- TemplatePar.assert()


TemplatePar.check = function ( options )

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

end -- TemplatePar.check()


TemplatePar.count = function ()

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

end -- TemplatePar.count()


TemplatePar.countNotEmpty = function ()

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

end -- TemplatePar.countNotEmpty()


TemplatePar.downcase = function ( options )

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

end -- TemplatePar.downcase()


TemplatePar.failsafe = function ( assert )

   -- Retrieve versioning and check for compliance
   -- Precondition:
   --     assert  -- string, with required version or "wikidata",
   --                or false
   -- Postcondition:
   --     Returns  string with appropriate version, or false
   local r
   if since == "wikidata" then
       local item = TemplatePar.item
       since = false
       if type( item ) == "number"  and  item > 0 then
           local ent = mw.wikibase.getEntity( string.format( "Q%d",
                                                             item ) )
           if type( ent ) == "table" then
               local vsn = ent:formatPropertyValues( "P348" )
               if type( vsn ) == "table"  and
                  type( vsn.value) == "string"  and
                  vsn.value ~= "" then
                   r = vsn.value
               end
           end
       end
   end
   if not r then
       if not since  or  since <= TemplatePar.serial then
           r = TemplatePar.serial
       else
           r = false
       end
   end
   return r

end -- TemplatePar.failsafe()


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:
   --     >< TemplatePar.frame
   --     mw.text.trim()
   --     TemplatePar.downcase()
   --     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
           if not TemplatePar.frame then
               TemplatePar.frame = mw.getCurrentFrame()
           end
           params = TemplatePar.frame:getParent()
       end
       r = formatted( params, access, options )
   else
       r = failure( "noname", false, options )
   end
   return finalize( r, options, frame )

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()


-- Provide external access local p = {}


function p.assert( frame )

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

end -- p.assert()


function p.check( frame )

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

end -- p.check()


function p.count( frame )

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

end -- p.count()


function p.countNotEmpty( frame )

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

end -- p.countNotEmpty()


function p.match( frame )

   -- Combined analysis of parameters and their values
   -- Precondition:
   --     frame  -- object; #invoke environment
   -- Postcondition:
   --     Return string with error message or ""
   -- Uses:
   --      < TemplatePar.frame
   --     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.frame = 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
           errValue    = false
           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
                           errMiss = string.format( "%s, '%s'",
                                                    errMiss, k )
                       else
                           errMiss = string.format( "'%s'", k )
                       end
                   elseif not errMiss then
                       errValues = fault( errValues, r )
                   end
                   break -- for r, rule
               end
           end -- for s, rule
       end -- for k, v
       r = ( errMiss or errValues )
       if r then
           if errMiss then
               r = failure( "undefined", errMiss, options )
           else
               r = failure( "invalid", errValues, options )
           end
           r = finalize( r, options )
       end
   end
   return r or ""

end -- p.match()


function p.valid( frame )

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

end -- p.valid()


p.failsafe = function ( frame )

   -- Check or retrieve version information
   -- Precondition:
   --     frame  -- object; #invoke environment
   -- Postcondition:
   --     Return string with error message or ""
   -- Uses:
   --     TemplatePar.failsafe()
   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 TemplatePar.failsafe( since )  or  ""

end -- p.failsafe()


function p.TemplatePar()

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

end -- p.TemplatePar()


return p