Modul:TemplatePar

Aus AnthroWiki

TemplatePar – Modul mit Hilfsfunktionen für die Vorlagenprogrammierung; namentlich die Analyse der Parameter der umgebenden Vorlageneinbindung.

  • check – Gesamt-Einbindung der Vorlage prüfen
  • count – Anzahl der Parameter der Vorlage
  • countNotEmpty – Anzahl der nicht-leeren Parameter der Vorlage
  • match – Vorlageneinbindung mit gefordertem Profil abgleichen
  • valid – Einzelnen Parameterwert prüfen

Insbesondere sollen den Anwendern für Standardfälle der Parameteranalyse auf einfache Weise Standard-Fehlermeldungen dargestellt werden.

Funktionen für Vorlagen

Einzelfunktionen

Alle Funktionen wirken hinsichtlich der analysierten Parameter auf die umgebende Vorlageneinbindung; die Parameter von #invoke spezifizieren die Regeln für die Analyse.

assert

Überprüfung einer beliebigen Zeichenkette nach den Regeln für das Parameterformat.
1
Zu untersuchende Zeichenkette (Pflichtparameter).
2
Format („Nur Ziffern“, „ASCII“, begrenzter Zeichensatz, Lua-pattern); siehe unten. Optional, aber sinnvollerweise in der Regel anzugeben.

check

Überprüfung auf vorgesehene und unerwartete Parameter der Vorlageneinbindung; Vollständigkeit von Pflichtangaben. Details siehe unten.
Eine Fehlermeldung wird mit class=error zurückgegeben.
Wenn nichts zurückgegeben wird, scheint alles in Ordnung zu sein.
Parameter (alle optional):
all
Namen der Pflichtparameter; müssen auch mit Wert belegt sein.
Untereinander durch Gleichheitszeichen = getrennt.
Fehlende Angaben lösen eine Standard-Fehlermeldung aus; wenn individuelle Fehlermeldungen gewünscht werden, sind sie unter opt aufzuführen und mittels Vorlagenprogrammierung zu analysieren.
opt
Namen der optionalen Parameter.
Untereinander durch Gleichheitszeichen = getrennt.
low
Ignoriere Groß- und Kleinschreibung.
Beispiele und Test.

count

Anzahl der Parameter der umgebenden Vorlageneinbindung.
Parameter: Keine (beim #invoke)
Das Ergebnis ist eine Zahl ab 0.
Beispiele und Test.

countNotEmpty

Anzahl der Parameter der umgebenden Vorlageneinbindung, die nicht leer sind (höchstens Leerzeichen oder Zeilenumbruch enthalten).
Parameter: Keine (beim #invoke)
Das Ergebnis ist eine Zahl ab 0.
Beispiele und Test.

match

Umgebende Vorlageneinbindung mit gefordertem Profil abgleichen.
1
Regel im Format   1=Parametername=Spezifikation
Die Spezifikation entspricht der von valid.
nnn
Wie 1 – beliebig viele unbenannte Parameter als Regeln; in beliebiger Reihenfolge und nicht lückenlos.
Alle zulässigen Parameternamen müssen aufgelistet werden; optionale sind mit der Bedingung * anzugeben oder einer geeigneten Spezifikation zu unterwerfen.
Es wird eine Fehlermeldung (class=error) zurückgegeben, wenn etwas nicht stimmt; sonst nichts.
Fatale Fehler führen in der folgenden Reihenfolge zum Abbruch: Unbekannter Parametername – fehlender Parameter – ungültiger Parameterwert.
Beispiele und Test.
Mittels der Funktion lässt sich durch einen einzigen Aufruf gleichzeitig für alle Parameter eine Prüfung ausführen auf
  • Pflichtparameter vorhanden?
  • keine unbekannten Parameternamen (Falschschreibung)?
  • gültige Werte für jeden Parameter?
Für jeden Parameternamen bei der Vorlageneinbindung muss mindestens eine Regel vorhanden sein. Gibt es für einen Vorlagenparameter keine Regel, ist er unbekannt und damit unzulässig.
  • Für denselben Parameternamen kann es mehrere Regeln geben. Die Einhaltung der Regeln wird in der Reihenfolge geprüft, in der sie vereinbart werden.
  • Die Reihenfolge der Parameternamen ist ohne Bedeutung.

valid

Überprüfung eines einzelnen Parameterwerts.
Eine Fehlermeldung wird mit class=error zurückgegeben.
Wenn nichts zurückgegeben wird, scheint alles in Ordnung zu sein.
Parameter (bis auf 1 alle optional):
1
Name des einzelnen Parameters.
2
Format („Nur Ziffern“, „ASCII“, begrenzter Zeichensatz, Lua-pattern); siehe unten.
min
Mindestlänge ≥0.
max
Maximale Länge >0.
low
Ignoriere Groß- und Kleinschreibung.
Aus der Gruppe 2, min, max sollte sinnvollerweise wenigstens eine angegeben werden.
Beispiele und Test.

Handhabung von Fehlern

Alle Funktionen, die auf einen Fehlerfall führen können (check, valid), unterstützen auch die nachstehenden Parameter:

template
Titel der für Autoren sichtbaren Vorlage (für Fehlermeldungen).
Kann auch ein anderes Schlüsselwort sein; vor allem für Untervorlagen vorgesehen.
Ein Wikilink ist darin ebenfalls möglich; etwa auf eine bestimmte Doku-, Hilfe- oder Projektseite. (Hier wäre – anders als beim Titel – natürlich der Namensraum anzugeben.)
cat
Titel einer Wartungskategorie.
Im Fehlerfall wird diese aktiviert.
Mit errNS lässt sich die Kategorisierung auf bestimmte Namensräume beschränken.
Falls der Titel die Zeichenkette @@@ enthält und template gesetzt ist, wird dies durch template ersetzt.
errNS
Leerzeichen-getrennte Auflistung von Namensraum-Nummern, auf die cat beschränkt bleibt.
format
Fehlermeldung formatieren oder unterdrücken.
  • Standardfall, Standardmeldung mit class="error" formatiert:
    • Parameter format nicht angegeben.
    • |format=*|
  • Unterdrücken (dann sollte cat= gesetzt sein):
    • |format=|
    • |format=0|
    • |format=-|
  • Fester Text, eigene Formatierung:
    • |format=<anfang>Fester Text</ende>|
  • Freie Formatierung der Standardmeldung:
    • Enthält @@@ als Platzhalter für die unformatierte Standardmeldung.
    • |format=<anfang>Eigener Text; @@@</ende>|
preview
Unterdrückung der Fehlermeldung im Vorschaumodus unterdrücken, also trotz |format=0| usw. immer Standardmeldung anzeigen:
  • |preview=1|
Fester Text, eigene Formatierung im Vorschaumodus:
  • |preview=<anfang>Fester Text</ende>|
Standardmeldung in eigener Formatierung im Vorschaumodus:
  • |preview=<anfang>Standardmeldung: @@@</ende>|


Parameterprüfung (check)

Anwendungsfall für check am Beispiel der {{Information}}:

{{#invoke:TemplatePar
         |check
         |all= Beschreibung= Quelle= Urheber=
         |opt= Datum= Genehmigung= Andere Versionen= Anmerkungen=
         |template=[[Vorlage:Information]]}}
  • Weil der Name eines Vorlagenparameters kein Gleichheitszeichen = enthalten kann, werden diese zur Abtrennung der Namen benutzt. Leerzeichen vor und nach dem Namen werden ignoriert. Zusätzliche Gleichheitszeichen sind unproblematisch.
  • Die ersten drei Vorlagenparameter des Beispiels sind Pflichtparameter und müssen auch mit Wert angegeben werden.
  • Alle bei der Vorlageneinbindung benutzten Parameter, die weder unter all= noch opt= aufgeführt sind, lösen eine Fehlermeldung aus.
  • Die unbenannten Vorlagenparameter tragen ihre laufende Nummer als Namen; im Übrigen werden die Namen in der Fehlermeldung bezeichnet.
  • Es gibt vier Fehlermeldungen:
    1. #invoke:TemplatePar * Optionsparameter wiederholt
      • Beim Aufruf des Moduls wurde ein Name sowohl unter all= als auch opt= angegeben.
    2. Fehler bei Vorlage * Parametername unbekannt
      • Bei der Vorlageneinbindung wurde ein Parameter benutzt, der weder unter all= noch opt= aufgeführt ist.
    3. Fehler bei Vorlage * Pflichtparameter fehlt
      • Bei der Vorlageneinbindung fehlt ein unter all= aufgeführter Parameter.
    4. Fehler bei Vorlage * Pflichtparameter ohne Wert
      • Bei der Vorlageneinbindung ist ein unter all= aufgeführter Parameter nur mit Gleichheitszeichen, aber ohne sichtbaren Wert angegeben; oder ein unbenannter Parameter ist leer.
  • Die Fehlermeldungen sind hierarchisch gestaffelt. Bei einem Schreibfehler, der zu einem Problem höherer Ordnung führt, könnten die weiteren lediglich Folgefehler sein, die sich mit Korrektur des Grundproblems von selbst erledigen. Es wird daher nur die wichtigste gefundene Fehlerkategorie angezeigt.

Parameterformat (valid)

Für die optionale Bedingung 2 in valid gibt es zwei Grundtypen:

  1. Schlüsselwort
    • Erleichterung für Vorlagenprogrammierer; einige Ausdrücke mit Lua-Patterns nicht möglich.
    • Spezielle Datentypen wie URL, DOI, ISBN, Datum sind nicht erwünscht. Hier müssten bei jedem neuen Typ alle Artikel (mehrere Hunderttausend) neu zusammengestellt werden. Hingegen gibt es darauf spezialisierte Funktionen wie in URLutil.
  2. Lua-Pattern (Ustring)
    • Eingeschlossen in Schrägstriche, um sie von einem Schlüsselwort unterscheiden zu können und ggf. führende und schließende Leerzeichen bei Werten unbenannter Vorlagenparameter zu identifizieren.
    • Die Zeichenketten | sowie {{ und }} sind in der Vorlagenprogrammierung nicht möglich. Dafür sind zu ersetzen:
      • |%!
      • {{%((
      • }}%))

Zur Spalte Implementierung siehe Wikipedia:Hilfe:Lua/Zeichenketten #Pattern

Schlüssel für vereinfachten Zugriff
Schlüssel Bedeutung Implementierung
ASCII ASCII-Zeichen (Codes 32–126), nur innerhalb einer Zeile, oder leer ^[ -~]*$
ASCII+ wie vor; nicht leer ^[ -~]+$
ASCII+1 in einem Wort; sonst wie vor und nicht leer ^[!-~]+$
n Nur ASCII-Ziffern 0–9, sowie vorangestelltes Vorzeichen ASCII-Minus, oder leer ^[%-]?[0-9]*$
einzelnes Minus ausschließen
n>0 Nur ASCII-Ziffern 0–9, ohne Vorzeichen, nicht leer und mindestens eine Ziffer nicht Null ^[0-9]*[1-9][0-9]*$
N+ Wie n, aber führende Null nicht erlaubt und nicht leer ^[%-]?[1-9][0-9]*$
N>0 Wie n>0, aber führende Null nicht erlaubt ^[1-9][0-9]*$
x Hexadezimalzahl; ASCII-Ziffern 0–9 Buchstaben a–f A–F, oder leer ^[0-9A-Fa-f]*$
x+ wie vor; nicht leer ^[0-9A-Fa-f]+$
X Hexadezimalzahl; ASCII-Ziffern 0–9 Buchstaben A–F, oder leer ^[0-9A-F]*$
X+ wie vor; nicht leer ^[0-9A-F]+$
0,0 Beliebige Zahl; auch kleiner Null; kann Komma enthalten; oder leer ^[%-]?[0-9]*,?[0-9]*$
nicht nur Minus oder Komma
0,0+ Zahl mit Komma und Nachkommastelle; auch kleiner Null ^[%-]?[0-9]+,[0-9]+$
0,0+? Beliebige Zahl; auch kleiner Null; kann Komma und Nachkommastelle enthalten; nicht leer ^[%-]?[0-9]+,?[0-9]*$
0.0 Beliebige Zahl; auch kleiner Null; kann Dezimalpunkt enthalten; oder leer ^[%-]?[0-9]*[%.]?[0-9]*$
nicht nur Minus oder Punkt
0.0+ Zahl mit Dezimalpunkt und Dezimalstelle; auch kleiner Null ^[%-]?[0-9]+%.[0-9]+$
0.0+? Beliebige Zahl; auch kleiner Null; kann Dezimalpunkt und Dezimalstelle enthalten; nicht leer ^[%-]?[0-9]+[%.]?[0-9]*$
.0+ Beliebige Zahl; auch kleiner Null; kann Dezimalpunkt und Dezimalstelle enthalten, aber nicht notwendigerweise mit Ziffer davor; nicht leer ^[%-]?[0-9]*[%.]?[0-9]+$
aa Mindestens zwei Buchstaben (nicht nur nebeneinander: N.N.) oder ein CJK %a.*%a
oder CJK
ID Identifizierer unter Einschränkungen, wie sie für viele Sprachen üblich sind: ASCII, beginnend mit Buchstaben; weiter Ziffern und Unterstreichungsstrich; oder leer.
Die nachfolgenden ASCII-Wörter sind ebenfalls unter dem Aspekt solcher Bezeichner zu sehen; etwa als Komponente einer URL.
^[A-Za-z]?[A-Za-z_0-9]*$
ID+ wie vor; aber nicht leer. ^[A-Za-z][A-Za-z_0-9]*$
ABC Wort in ASCII-Großbuchstaben; oder leer. ^[A-Z]*$
ABC+ wie vor; aber nicht leer. ^[A-Z]+$
Abc Wort in ASCII-Buchstaben mit nur erstem Buchstaben groß; oder leer. ^[A-Z]*[a-z]*$
Abc+ wie vor; aber nicht leer. ^[A-Z][a-z]+$
abc Wort in ASCII-Kleinbuchstaben; oder leer. ^[a-z]*$
abc+ wie vor; aber nicht leer. ^[a-z]+$
aBc+ Wort in ASCII-Buchstaben mit lower camel casing; nicht leer. ^[a-z]+[A-Z][A-Za-z]*$
base64 Base64; oder leer. ^[A-Za-z0-9%+/]*$
base64+ wie vor; aber nicht leer. ^[A-Za-z0-9%+/]+$
pagename Zulässiger Seitenname; nicht leer. nicht #<>[]|{} oder 12710 oder 1–3110

file
file+
file:
file:+
image
image+
image:
image:+

Zulässiger Titel oder Name einer Mediendatei bei file (der Namensraum darf vorangestellt sein).

  • Mit + nicht leer.
  • Mit : muss die Mediendatei existieren.

Als image muss das Dateiformat für ein Einzelbild geeignet sein.

<nnnn
<=nnnn
>nnnn
>=nnnn
==nnnn
!=nnnn

Bedingung: Arithmetischer Vergleich oder (auch) numerische Ungleichheit
* leer oder beliebig
+
nichts
nicht leer %S
In Planung
date Irgendein bekanntes Format für Datum und auch Zeit.
 
Hier eher nicht vorgesehen
  • Vermutlich eher in einem Modul:WLink:
    • Name einer existierenden Seite
  • Basis-Datentypen
    • Datum, Zeit, URL, ISBN

Beispiele (Testseite)

Eine Testseite illustriert praktische Beispiele.


--[=[ TemplatePar 2015-02-14
Template parameter utility
* assert
* check
* count
* countNotEmpty
* downcase()
* match
* valid
* verify()
* TemplatePar()
]=]



-- Module globals
local TemplatePar = { }
local MessagePrefix = "lua-module-TemplatePar-"
local L10nDef = {}
L10nDef.en = {
    badPattern  = "&#35;invoke:TemplatePar pattern syntax error",
    dupOpt      = "&#35;invoke:TemplatePar repeated optional parameter",
    dupRule     = "&#35;invoke:TemplatePar conflict key/pattern",
    empty       = "Error in template * undefined value for mandatory",
    invalid     = "Error in template * invalid parameter",
    invalidPar  = "&#35;invoke:TemplatePar invalid parameter",
    minmax      = "&#35;invoke:TemplatePar min > max",
    missing     = "&#35;invoke:TemplatePar missing library",
    multiSpell  = "Error in template * multiple spelling of parameter",
    noMSGnoCAT  = "&#35;invoke:TemplatePar neither message nor category",
    noname      = "&#35;invoke:TemplatePar missing parameter name",
    notFound    = "Error in template * missing page",
    tooLong     = "Error in template * parameter too long",
    tooShort    = "Error in template * parameter too short",
    undefined   = "Error in template * mandatory parameter missing",
    unknown     = "Error in template * unknown parameter name",
    unknownRule = "&#35;invoke:TemplatePar unknown rule"
}
L10nDef.de  = {
    badPattern  = "&#35;invoke:TemplatePar Syntaxfehler des pattern",
    dupOpt      = "&#35;invoke:TemplatePar Optionsparameter wiederholt",
    dupRule     = "&#35;invoke:TemplatePar Konflikt key/pattern",
    empty       = "Fehler bei Vorlage * Pflichtparameter ohne Wert",
    invalid     = "Fehler bei Vorlage * Parameter ungültig",
    invalidPar  = "&#35;invoke:TemplatePar Ungültiger Parameter",
    minmax      = "&#35;invoke:TemplatePar min > max",
    multiSpell  = "Fehler bei Vorlage * Mehrere Parameter-Schreibweisen",
    noMSGnoCAT  = "&#35;invoke:TemplatePar weder Meldung noch Kategorie",
    noname      = "&#35;invoke:TemplatePar Parameter nicht angegeben",
    notFound    = "Fehler bei Vorlage * Seite fehlt",
    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 = "&#35;invoke:TemplatePar Unbekannte Regel"
}
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 ),
    [ "+" ]        = "%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:
    --     Module:FileMedia
    --     FileMedia.isType()
    local r
    if attempt and attempt ~= "" then
        local lucky, FileMedia = pcall( require, "Module:FileMedia" )
        if type( FileMedia ) == "table" then
            FileMedia = FileMedia.FileMedia()
            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
            l10n = L10nDef[ "en" ]
        end
        r = l10n[ say ]
    else
        m:inLanguage( c )
        r = m:plain()
    end
    if not r then
        r = string.format( "(((%s)))", say )
    end
    return r
end -- factory()



local function failsafe( 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 -- failsafe()



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 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()
    --     failsafe()
    --     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( "langW?%+?" ) then
                n = "lang"
-- lang lang+
-- langW langW+
            end
            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( failsafe, 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
    for k, v in pairs( haystack ) do
        if k == needle then
            return true
        end
    end -- for k, v
    return 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
        g = mw.getCurrentFrame()
        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( "%%!", "|" )
            sub             = sub:gsub( "%%%(%(", "{{" )
            sub             = sub:gsub( "%%%)%)", "}}" )
            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, frame )
    -- 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
    --     frame    -- object, or false
    -- Postcondition:
    --     Return string or false
    -- Uses:
    --     factory()
    local r = false
    if submit then
        local opt, s
        local lazy = false
        local show = false
        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
                if lazy then
                    show = ""
                    lazy = false
                end
                if not frame then
                    frame = mw.getCurrentFrame()
                end
                if frame:preprocess( "{{REVISIONID}}" ) == "" then
                    if s == "1" then
                        show = "*"
                    else
                        show = s
                    end
                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
                show = "<span class=\"error\">@@@</span>"
            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
        s = opt.cat
        if type( s ) == "string" then
            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
                        opt.errNS = false
                    end
                elseif st == "table" then
                    for i = 1, #opt.errNS do
                        if opt.errNS[ i ] == ns then
                            opt.errNS = false
                            break    -- for i
                        end
                    end -- for i
                end
            end
            if opt.errNS then
                r = ""
            else
                if not r then
                   r = ""
                end
                if s:find( "@@@" ) then
                    if type( opt.template ) == "string" then
                        s = s:gsub( "@@@", opt.template )
                    end
                end
                local i
                local 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, or false
    -- Postcondition:
    --     Return string with error message as configured;
    --            false if valid
    -- Uses:
    --     fold()
    --     fetch()
    --     fix()
    --     finalize()
    local duty, r
    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, frame )
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, frame )
        else
            s = frame.args[ 1 ] or ""
            r = tonumber( s )
            if ( r ) then
                s = r
            end
            if action == "valid" then
                r = TemplatePar.valid( s, options, frame )
            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.valid = function ( access, options, frame )
    -- Check validity of one particular template parameter
    -- Precondition:
    --     access   -- id of parameter in template transclusion
    --                 string or number
    --     options  -- table or nil; optional details
    --     frame    -- object; #invoke environment
    -- Postcondition:
    --     Return string with error message as configured;
    --            false if valid or no answer permitted
    -- Uses:
    --     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
            params = 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 -- .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 -- .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 -- .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 -- .countNotEmpty()



function p.match( frame )
    -- Combined analysis of parameters and their values
    -- Postcondition:
    --     Return string with error message or ""
    -- Uses:
    --     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 = { }
    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, frame )
        end
    end
    return r or ""
end -- .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 -- .valid()



function p.TemplatePar()
    -- Retrieve function access for modules
    -- Postcondition:
    --     Return table with functions
    return TemplatePar
end -- .TemplatePar()



return p