Modul:TemplatePar

    Aus mxlinuxusers.de

    Die Dokumentation für dieses Modul kann unter Modul:TemplatePar/Doku erstellt werden

    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 = "{{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
                opt = { }
            end
            if lazy then
                if not opt.cat then
                    r = string.format( "%s %s",
                                       submit,  factory( "noMSGnoCAT" ) )
                end
            else
                r = submit
            end
            if r  and  not lazy then
                local i
                if not show  or  show == "*" then
                    local e = mw.html.create( "span" )
                                     :attr( "class", "error" )
                                     :wikitext( "@@@" )
                    if learn then
                        local max  = 1000000000
                        local id   = math.floor( os.clock() * max )
                        local sign = string.format( "error_%d", id )
                        local btn  = mw.html.create( "span" )
                        local top  = mw.html.create( "div" )
                        e:attr( "id", sign )
                        btn:css( { ["background"]      = "#FFFF00",
                                   ["border"]          = "#FF0000 3px solid",
                                   ["font-weight"]     = "bold",
                                   ["padding"]         = "2px",
                                   ["text-decoration"] = "none" } )
                           :wikitext( "&gt;&gt;&gt;" )
                        sign = string.format( "[[#%s|%s]]",
                                              sign,  tostring( btn ) )
                        top:wikitext( sign, "&#160;", submit )
                        mw.addWarning( tostring( top ) )
                    end
                    show = tostring( e )
                end
                i = show:find( "@@@", 1, true )
                if i then
                    -- No gsub() since r might contain "%3" (e.g. URL)
                    r = string.format( "%s%s%s",
                                       show:sub( 1,  i - 1 ),
                                       r,
                                       show:sub( i + 3 ) )
                else
                    r = show
                end
            end
            if learn and r then
                -- r = fatal( r )
            end
            s = opt.cat
            if type( s ) == "string" then
                local link
                if opt.errNS then
                    local ns = mw.title.getCurrentTitle().namespace
                    local st = type( opt.errNS )
                    if st == "string" then
                        local space  = string.format( ".*%%s%d%%s.*", ns )
                        local spaces = string.format( " %s ", opt.errNS )
                        if spaces:match( space ) then
                            link = true
                        end
                    elseif st == "table" then
                        for i = 1, #opt.errNS do
                            if opt.errNS[ i ] == ns then
                                link = true
                                break    -- for i
                            end
                        end -- for i
                    end
                else
                    link = true
                end
                if link then
                    local cats, i
                    if not r then
                       r = ""
                    end
                    if s:find( "@@@" ) then
                        if type( opt.template ) == "string" then
                            s = s:gsub( "@@@", opt.template )
                        end
                    end
                    cats = mw.text.split( s, "%s*#%s*" )
                    for i = 1, #cats do
                        s = mw.text.trim( cats[ i ] )
                        if #s > 0 then
                            r = string.format( "%s[[Category:%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( "&#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()
    
    
    
    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  = "&#35;invoke:TemplatePar|check|"
                        }
        local r = form( false, options, frame )
        if not r then
            options = { mandatory = fill( frame.args.all ),
                        optional  = fill( frame.args.opt ),
                        cat       = frame.args.cat,
                        errNS     = frame.args.errNS,
                        low       = frame.args.low,
                        format    = frame.args.format,
                        preview   = frame.args.preview,
                        template  = frame.args.template
                      }
            r       = form( true, options, frame )
        end
        return r or ""
    end -- p.check()
    
    
    
    function p.count( frame )
        -- Count number of template parameters
        -- Postcondition:
        --     Return string with digits including "0"
        -- Uses:
        --     TemplatePar.count()
        return tostring( TemplatePar.count() )
    end -- p.count()
    
    
    
    function p.countNotEmpty( frame )
        -- Count number of template parameters which are not empty
        -- Postcondition:
        --     Return string with digits including "0"
        -- Uses:
        --     TemplatePar.countNotEmpty()
        return tostring( TemplatePar.countNotEmpty() )
    end -- p.countNotEmpty()
    
    
    
    function p.match( frame )
        -- Combined analysis of parameters and their values
        -- Precondition:
        --     frame  -- object; #invoke environment
        -- Postcondition:
        --     Return string with error message or ""
        -- Uses:
        --      < TemplatePar.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