มอดูล:TemplatePar

local TemplatePar = { serial  = "2023-03-20",
suite   = "TemplatePar",
item    = 15393417,
globals = { DateTime     = 20652535,
FileMedia    = 24765326,
Multilingual = 47541920,
TemplUtl     = 52364930,
URLutil      = 10859193 } }
--=
Template parameter utility- assert- check- count- countNotEmpty- downcase()- duplicates- match- valid- verify()- TemplatePar()- failsafe()
**
local Local     = { frame = false }
local Failsafe  = TemplatePar
local GlobalMod = Local
-- Module globals
Local.messagePrefix = "lua-module-TemplatePar-"
Local.L10nDef = {}
Local.L10nDef.en = {
badPattern  = "#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",
unavailable = "Error in template * parameter name missing",
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-90-9*$",
"N+"        = "^%-?1-90-9*$",
"N>0"       = "^1-90-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-zA-Za-z_0-9*$",
"ABC"       = "^A-Z*$",
"ABC+"      = "^A-Z+$",
"Abc"       = "^A-Z*a-z*$",
"Abc+"      = "^A-Za-z+$",
"abc"       = "^a-z*$",
"abc+"      = "^a-z+$",
"aBc+"      = "^a-z+A-ZA-Za-z*$",
"w"         = "^%S*$",
"w+"        = "^%S+$",
"base64"    = "^A-Za-z0-9%+/*$",
"base64+"   = "^A-Za-z0-9%+/+$",
"aa"        = "%a%a.*%a%a",
"pagename"  = string.format( "^^#%%%%|{}%c-%c%c+$",
1, 31, 127 ),
"ref"       = string.format( "%c'%c`UNIQ%s%sref%s%s%sQINU`%c'%c",
127, 34, "%-", "%-", "%-", "%x+",
"%-", 34, 127 ),
"+"         = "%S"
}
Local.boolean = { "1"     = true,
"true"  = true,
y         = true,
yes       = true,
on        = true,
"0"     = true,
"false" = true,
"-"     = true,
n         = true,
no        = true,
off       = true }
Local.patternCJK = false
local foreignModule = function ( access, advanced, append, alt, alert )
-- Fetch global module
-- Precondition:
--     access    -- string, with name of base module
--     advanced  -- true, for require(); else mw.loadData()
--     append    -- string, with subpage part, if any; or false
--     alt       -- number, of wikidata item of root; or false
--     alert     -- true, for throwing error on data problem
-- Postcondition:
--     Returns whatever, probably table
-- 2020-01-01
local storage = access
local finer = function ()
if append then
storage = string.format( "%s/%s",
storage,
append )
end
end
local fun, lucky, r, suited
if advanced then
fun = require
else
fun = mw.loadData
end
GlobalMod.globalModules = GlobalMod.globalModules or { }
suited = GlobalMod.globalModules access 
if not suited then
finer()
lucky, r = pcall( fun,  "Module:" .. storage )
end
if not lucky then
if not suited  and
type( alt ) == "number"  and
alt > 0 then
suited = string.format( "Q%d", alt )
suited = mw.wikibase.getSitelink( suited )
GlobalMod.globalModules access  = suited or true
end
if type( suited ) == "string" then
storage = suited
finer()
lucky, r = pcall( fun, storage )
end
if not lucky and alert then
error( "Missing or invalid page: " .. storage )
end
end
return r
end -- foreignModule()
local function Foreign( access  )
-- Access standardized library
-- Precondition:
--     access  -- string, with name of base module
-- Postcondition:
--     Return library table, or not
-- Uses:
local r
if Local access  then
r = Local access 
else
local bib = foreignModule( access,
true,
false,
TemplatePar.globals access ,
false )
if type( bib ) == "table"   and
type( bib access  ) == "function" then
bib = bib access ()
if type( bib ) == "table" then
r               = bib
Local access  = bib
end
end
end
return r
end -- Foreign()
local function containsCJK( analyse )
-- Is any CJK character present?
-- Precondition:
--     analyse  -- string
-- Postcondition:
--     Return false iff no CJK present
-- Uses:
--     >  Local.messagePrefix
--     >  Local.L10nDef
--     mw.message.new()
--     mw.language.getContentLanguage()
--     Module:Multilingual
--     Foreign()
--     TemplatePar.framing()
--     Multilingual.tabData()
local m = mw.message.new( Local.messagePrefix .. say )
local r = false
if m:isBlank() then
local c = mw.language.getContentLanguage():getCode()
local l10n = Local.L10nDef c 
if l10n then
r = l10n say 
else
local MultiL = Foreign( "Multilingual" )
if MultiL  and  type( MultiL.tabData ) == "function" then
local lang
r, lang = MultiL.tabData( "I18n/Module:TemplatePar",
say,
false,
TemplatePar.framing() )
end
end
if not r then
r = Local.L10nDef.en say 
end
else
m:inLanguage( c )
r = m:plain()
end
if not r then
r = string.format( "(((%s)))", say )
end
return r
end -- factory()
local function faculty( accept, attempt )
-- Check string as possible boolean
-- Precondition:
--     accept   -- string; requirement
--                         boolean
--                         boolean+
--     attempt  -- string; to be tested
-- Postcondition:
--     Return error keyword, or false
-- Uses:
--     Module:TemplUtl
--     Foreign()
--     TemplUtl.faculty()
local r
r = mw.text.trim( attempt ):lower()
if r == "" then
if accept == "boolean+" then
r = "empty"
else
r = false
end
elseif Local.boolean r   or   r:match( "^01%-+$" ) then
r = false
else
local TemplUtl = Foreign( "TemplUtl" )
if TemplUtl  and  type( TemplUtl.faculty ) == "function" then
r = TemplUtl.faculty( r, "-" )
if r == "-" then
r = "invalid"
else
r = false
end
else
r = "invalid"
end
end
return r
end -- faculty()
local function failure( spec, suspect, options )
-- Submit localized error message
-- Precondition:
--     spec     -- string; message ID
--     suspect  -- string or nil; additional information
--     options  -- table or nil; optional details
--                 options.template
-- Postcondition:
--     Return string
-- Uses:
--     factory()
local r = factory( spec )
if type( options ) == "table" then
if type( options.template ) == "string" then
if #options.template > 0 then
r = string.format( "%s (%s)", r, options.template )
end
end
end
if suspect then
r = string.format( "%s: %s", r, suspect )
end
return r
end -- failure()
local function fair( story, scan )
-- Test for match (possibly user-defined with syntax error)
-- Precondition:
--     story  -- string; parameter value
--     scan   -- string; pattern
-- Postcondition:
--     Return nil, if not matching, else non-nil
-- Uses:
--     mw.ustring.match()
return  mw.ustring.match( story, scan )
end -- fair()
local function familiar( accept, attempt )
-- Check string as possible language name or list
-- Precondition:
--     accept   -- string; requirement
--                         lang
--                         langs
--                         langW
--                         langsW
--                         lang+
--                         langs+
--                         langW+
--                         langsW+
--     attempt  -- string; to be tested
-- Postcondition:
--     Return error keyword, or false
-- Uses:
--     Module:Multilingual
--     Foreign()
--     Multilingual.isLang()
local r
if attempt and attempt ~= "" then
local MultiL = Foreign( "Multilingual" )
if MultiL  and  type( MultiL.isLang ) == "function" then
local lazy = accept:find( "W", 1, true )
if accept:find( "s", 1, true ) then
local group = mw.text.split( attempt, "%s+" )
r = false
for i = 1, #group do
if not MultiL.isLang( group i , lazy ) then
r = "invalid"
break -- for i
end
end -- for i
elseif MultiL.isLang( attempt, lazy ) then
r = false
else
r = "invalid"
end
else
r = "missing"
end
elseif accept:find( "+", 1, true ) then
r = "empty"
else
r = false
end
return r
end -- familiar()
local function far( accept, attempt )
-- Check string as possible URL
-- Precondition:
--     accept   -- string; requirement
--                         url
--                         url+
--     attempt  -- string; to be tested
-- Postcondition:
--     Return error keyword, or false
-- Uses:
--     Module:URLutil
--     Foreign()
--     URLutil.isWebURL()
local r
if attempt and attempt ~= "" then
local URLutil = Foreign( "URLutil" )
if URLutil  and  type( URLutil.isWebURL ) == "function" then
if URLutil.isWebURL( attempt ) then
r = false
else
r = "invalid"
end
else
r = "missing"
end
elseif accept:find( "+", 1, true ) then
r = "empty"
else
r = false
end
return r
end -- far()
local function fast( accept, attempt )
-- Check string as possible date or time
-- Precondition:
--     accept   -- string; requirement
--                         datetime
--                         datetime+
--                         datetime/y
--                         datetime/y+
--                         datetime/ym
--                         datetime/ym+
--                         datetime/ymd
--                         datetime/ymd+
--     attempt  -- string; to be tested
-- Postcondition:
--     Return error keyword, or false
-- Uses:
--     Module:DateTime
--     Foreign()
--     DateTime.DateTime()
local r
r = mw.text.trim( attempt )
if r == "" then
if accept:find( "+", 1, true ) then
r = "empty"
else
r = false
end
else
local DateTime = Foreign( "DateTime" )
if type( DateTime ) == "table" then
local d = DateTime( attempt )
if type( d ) == "table" then
if accept:find( "/", 1, true ) then
r = "invalid"
if accept:sub( 1, 10 ) == "datetime/y" then
if d.year then
r = false
if accept:sub( 1, 11 ) == "datetime/ym" then
if d.month then
if accept:sub( 1, 12 )

**"datetime/ymd" then
if not d.dom then
r = "invalid"
end
end
else
r = "invalid"
end
end
end
end
else
r = false
end
else
r = "invalid"
end
else
r = "invalid"
end
end
return r
end -- fast()
local function fault( store, key )
-- Add key to collection string and insert separator
-- Precondition:
--     store  -- string or nil or false; collection string
--     key    -- string or number; to be appended
-- Postcondition:
--     Return string; extended
local r
local s
if type( key ) == "number" then
s = tostring( key )
else
s = key
end
if store then
r = string.format( "%s; %s", store, s )
else
r = s
end
return r
end -- fault()
local function feasible( analyze, options, abbr )
-- Check content of a value
-- Precondition:
--     analyze  -- string to be analyzed
--     options  -- table or nil; optional details
--                 options.pattern
--                 options.key
--                 options.say
--     abbr     -- true: abbreviated error message
-- Postcondition:
--     Return string with error message as configured;
--            false if valid or no answer permitted
-- Uses:
--     >  Local.patterns
--     failure()
--     mw.text.trim()
--     faculty()
--     fast()
--     facility()
--     familiar()
--     far()
--     fair()
--     containsCJK()
local r     = false
local s     = false
local show  = nil
local scan  = false
local stuff = mw.text.trim( analyze )
if type( options.pattern ) == "string" then
if options.key then
r = failure( "dupRule", false, options )
else
scan = options.pattern
end
else
if type( options.key ) == "string" then
s = mw.text.trim( options.key )
else
s = "+"
end
if s ~= "*" then
scan = Local.patterns s 
end
if type( scan ) == "string" then
if s == "n" or s == "0,0" or s == "0.0" then
if not stuff:match( "0-9" )  and
not stuff:match( "^%s*$" ) then
scan = false
if options.say then
show = string.format( ""%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( stuff )
if i then
if op == "" then
i = ( i > n )
elseif op == ">=" then
i = ( i >= n )
elseif op == "==" then
i = ( i == n )
elseif op == "!=" then
i = ( i ~= n )
else
n = false
end
end
if not i then
r = "invalid"
end
elseif plus then
r = "undefined"
end
elseif s:match( "^boolean%+?$" ) then
r = faculty( s, stuff )
n = true
elseif s:match( "^datetime/?y?m?d?%+?$" ) then
r = fast( s, stuff )
n = true
elseif s:match( "^image%+?:?$" )  or
s:match( "^file%+?:?$" ) then
r = facility( s, stuff )
n = true
elseif s:match( "langs?W?%+?" ) then
r = familiar( s, stuff )
n = true
elseif s:match( "url%+?" ) then
r = far( s, stuff )
n = true
end
-- datetime+
-- iso8631+
-- line+
if not n and not r then
r = "unknownRule"
end
if r then
if options.say then
show = string.format( ""%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, stuff, scan )
if legal then
if not got then
if s == "aa" then
got = containsCJK( stuff )
end
if not got then
if options.say then
show = string.format( ""%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()
--     TemplatePar.framing()
--     frame:getParent()
local g, k, v
local r = { }
if options.low then
g = TemplatePar.downcase( options )
else
g = TemplatePar.framing()
if light then
g = g:getParent()
end
g = g.args
end
if type( g ) == "table"  then
r = { }
for k, v in pairs( g ) do
if type( v ) == "string" then
if v:match( "^%s*$" ) then
v = false
end
else
v = false
end
if type( k ) == "number" then
k = tostring( k )
end
r k  = v
end -- for k, v
else
r = g
end
return r
end -- fetch()
local function figure( append, options )
-- Extend options by rule from #invoke strings
-- Precondition:
--     append   -- string or nil; requested rule
--     options  --  table; details
--                  ++ .key
--                  ++ .pattern
-- Postcondition:
--     Return sequence table
local r = options
if type( append ) == "string" then
local story = mw.text.trim( append )
local sub   = story:match( "^/(.*%S)/$" )
if type( sub ) == "string" then
sub             = sub:gsub( "%%!", "|" )
:gsub( "%%%(%(", "" )
:gsub( "\\n", string.char( 10 ) )
options.pattern = sub
options.key     = nil
else
options.key     = story
options.pattern = nil
end
end
return r
end -- figure()
local function fill( specified )
-- Split requirement string separated by '='
-- Precondition:
--     specified  -- string or nil; requested parameter set
-- Postcondition:
--     Return sequence table
-- Uses:
--     mw.text.split()
local r
if specified then
local i, s
r = mw.text.split( specified, "%s*=%s*" )
for i = #r, 1, -1 do
s = r i 
if #s == 0 then
table.remove( r, i )
end
end -- for i, -1
else
r = { }
end
return r
end -- fill()
local function finalize( submit, options )
-- Finalize message
-- Precondition:
--     submit   -- string or false or nil; non-empty error message
--     options  -- table or nil; optional details
--                 options.format
--                 options.preview
--                 options.cat
--                 options.template
-- Postcondition:
--     Return string or false
-- Uses:
--     TemplatePar.framing()
--     factory()
local r = false
if submit then
local lazy  = false
local learn = false
local show  = false
local opt, s
if type( options ) == "table" then
opt  = options
show = opt.format
lazy = ( show == ""  or  show == "0"  or  show == "-" )
s    = opt.preview
if type( s ) == "string"  and
s ~= ""  and  s ~= "0"  and  s ~= "-" then
local sniffer = ""
if lazy then
show = ""
lazy = false
end
if TemplatePar.framing():preprocess( sniffer ) == "" then
if s == "1" then
show = "*"
else
show = s
end
learn = true
end
end
else
opt = { }
end
if lazy then
if not opt.cat then
r = string.format( "%s %s",
submit,  factory( "noMSGnoCAT" ) )
end
else
r = submit
end
if r  and  not lazy then
local i
if not show  or  show == "*" then
local e = mw.html.create( "span" )
:attr( "class", "error" )
:wikitext( "@@@" )
if learn then
local max  = 1000000000
local id   = math.floor( os.clock() * max )
local sign = string.format( "error_%d", id )
local btn  = mw.html.create( "span" )
local top  = mw.html.create( "div" )
e:attr( "id", sign )
btn:css( { "background"      = "#FFFF00",
"border"          = "#FF0000 3px solid",
"font-weight"     = "bold",
"padding"         = "2px",
"text-decoration" = "none" } )
:wikitext( ">>>" )
sign = string.format( "%s",
sign,  tostring( btn ) )
top:wikitext( sign, " ", submit )
mw.addWarning( tostring( top ) )
end
show = tostring( e )
end
i = show:find( "@@@", 1, true )
if i then
-- No gsub() since r might contain "%3" (e.g. URL)
r = string.format( "%s%s%s",
show:sub( 1,  i - 1 ),
r,
show:sub( i + 3 ) )
else
r = show
end
end
if learn and r then
-- r = fatal( r )
end
s = opt.cat
if type( s ) == "string" then
local link
if opt.errNS then
local ns = mw.title.getCurrentTitle().namespace
local st = type( opt.errNS )
if st == "string" then
local space  = string.format( ".*%%s%d%%s.*", ns )
local spaces = string.format( " %s ", opt.errNS )
if spaces:match( space ) then
link = true
end
elseif st == "table" then
for i = 1, #opt.errNS do
if opt.errNS i  == ns then
link = true
break    -- for i
end
end -- for i
end
else
link = true
end
if link then
local cats, i
if not r then
r = ""
end
if s:find( "@@@" ) then
if type( opt.template ) == "string" then
s = s:gsub( "@@@", opt.template )
end
end
cats = mw.text.split( s, "%s*#%s*" )
for i = 1, #cats do
s = mw.text.trim( cats i  )
if #s > 0 then
r = string.format( "%s", r, s )
end
end -- for i
end
end
end
return r
end -- finalize()
local function finder( haystack, needle )
-- Find needle in haystack sequence
-- Precondition:
--     haystack  -- table; sequence of key names, downcased if low
--     needle    -- any; key name
-- Postcondition:
--     Return true iff found
local i
for i = 1, #haystack do
if haystack i  == needle then
return true
end
end -- for i
return false
end -- finder()
local function fix( valid, duty, got, options )
-- Perform parameter analysis
-- Precondition:
--     valid    -- table; unique sequence of known parameters
--     duty     -- table; sequence of mandatory parameters
--     got      -- table; sequence of current parameters
--     options  -- table or nil; optional details
-- Postcondition:
--     Return string as configured; empty if valid
-- Uses:
--     finder()
--     fault()
--     failure()
--     fed()
local r = false
local lack
for k, v in pairs( got ) do
if k == "" then
lack = true
break    -- for k, v
elseif not finder( valid, k ) then
r = fault( r, k )
end
end -- for k, v
if lack then
r = failure( "unavailable", false, options )
elseif r then
r = failure( "unknown",
string.format( ""%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.framing()
--     fold()
--     fetch()
--     fix()
--     finalize()
local duty, r
if frame then
TemplatePar.framing( frame )
end
if type( options ) == "table" then
if type( options.mandatory ) ~= "table" then
options.mandatory = { }
end
duty = options.mandatory
if type( options.optional ) ~= "table" then
options.optional = { }
end
r = fold( options )
else
options = { }
duty    = { }
r       = { }
end
if type( r ) == "table" then
local got = fetch( light, options )
if type( got ) == "table" then
r = fix( r, duty, got, options )
else
r = got
end
end
return finalize( r, options )
end -- form()
local function format( analyze, options )
-- Check validity of a value
-- Precondition:
--     analyze  -- string to be analyzed
--     options  -- table or nil; optional details
--                 options.say
--                 options.min
--                 options.max
-- Postcondition:
--     Return string with error message as configured;
--            false if valid or no answer permitted
-- Uses:
--     feasible()
--     failure()
local r = feasible( analyze, options, false )
local show
if options.min  and  not r then
if type( options.min ) == "number" then
if type( options.max ) == "number" then
if options.max  %d",
options.min,
options.max ),
options )
end
end
if #analyze  options.max then
show = " >" .. options.max
if options.say then
show = string.format( "%s "%s"", show, options.say )
end
r = failure( "tooLong", show, options )
end
else
r = failure( "invalidPar", "max", options )
end
end
return r
end -- format()
local function formatted( assignment, access, options )
-- Check validity of one particular parameter in a collection
-- Precondition:
--     assignment  -- collection
--     access      -- id of parameter in collection
--     options     -- table or nil; optional details
-- Postcondition:
--     Return string with error message as configured;
--            false if valid or no answer permitted
-- Uses:
--     mw.text.trim()
--     format()
--     failure()
local r = false
if type( assignment ) == "table" then
local story = assignment.args access  or ""
if type( access ) == "number" then
story = mw.text.trim( story )
end
if type( options ) ~= "table" then
options = { }
end
options.say = access
r = format( story, options )
end
return r
end -- formatted()
local function furnish( frame, action )
-- Prepare #invoke evaluation of .assert() or .valid()
-- Precondition:
--     frame    -- object; #invoke environment
--     action   -- "assert" or "valid"
-- Postcondition:
--     Return string with error message or ""
-- Uses:
--     form()
--     failure()
--     finalize()
--     TemplatePar.valid()
--     TemplatePar.assert()
local options = { mandatory = { "1" },
optional  = { "2",
"cat",
"errNS",
"low",
"max",
"min",
"format",
"preview",
"template" },
template  = string.format( "#invoke:%s|%s|",
"TemplatePar",
action )
}
local r       = form( false, options, frame )
if not r then
local s
options = { cat      = frame.args.cat,
errNS    = frame.args.errNS,
low      = frame.args.low,
format   = frame.args.format,
preview  = frame.args.preview,
template = frame.args.template
}
options = figure( frame.args 2 , options )
if type( frame.args.min ) == "string" then
s = frame.args.min:match( "^%s*(0-9+)%s*$" )
if s then
options.min = tonumber( s )
else
r = failure( "invalidPar",
"min=" .. frame.args.min,
options )
end
end
if type( frame.args.max ) == "string" then
s = frame.args.max:match( "^%s*(1-90-9*)%s*$" )
if s then
options.max = tonumber( s )
else
r = failure( "invalidPar",
"max=" .. frame.args.max,
options )
end
end
if r then
r = finalize( r, options )
else
s = frame.args 1  or ""
r = tonumber( s )
if ( r ) then
s = r
end
if action == "valid" then
r = TemplatePar.valid( s, options )
elseif action == "assert" then
r = TemplatePar.assert( s, "", options )
end
end
end
return r or ""
end -- furnish()
TemplatePar.assert = function ( analyze, append, options )
-- Perform parameter analysis on a single string
-- Precondition:
--     analyze  -- string to be analyzed
--     append   -- string: append error message, prepending 
--                 false or nil: throw error with message
--     options  -- table; optional details
-- Postcondition:
--     Return string with error message as configured;
--            false if valid
-- Uses:
--     format()
local r = format( analyze, options )
if ( r ) then
if ( type( append ) == "string" ) then
if ( append ~= "" ) then
r = string.format( "%s %s", append, r )
end
else
error( r, 0 )
end
end
return r
end -- TemplatePar.assert()
TemplatePar.check = function ( options )
-- Run parameter analysis on current template environment
-- Precondition:
--     options  -- table or nil; optional details
--                 options.mandatory
--                 options.optional
-- Postcondition:
--     Return string with error message as configured;
--            false if valid
-- Uses:
--     form()
return form( true, options, false )
end -- TemplatePar.check()
TemplatePar.count = function ()
-- Return number of template parameters
-- Postcondition:
--     Return number, starting at 0
-- Uses:
--     mw.getCurrentFrame()
--     frame:getParent()
local k, v
local r = 0
local t = mw.getCurrentFrame():getParent()
local o = t.args
for k, v in pairs( o ) do
r = r + 1
end -- for k, v
return r
end -- TemplatePar.count()
TemplatePar.countNotEmpty = function ()
-- Return number of template parameters with more than whitespace
-- Postcondition:
--     Return number, starting at 0
-- Uses:
--     mw.getCurrentFrame()
--     frame:getParent()
local k, v
local r = 0
local t = mw.getCurrentFrame():getParent()
local o = t.args
for k, v in pairs( o ) do
if not v:match( "^%s*$" ) then
r = r + 1
end
end -- for k, v
return r
end -- TemplatePar.countNotEmpty()
TemplatePar.downcase = function ( options )
-- Return all template parameters with downcased name
-- Precondition:
--     options  -- table or nil; optional messaging details
-- Postcondition:
--     Return table, may be empty; or string with error message.
-- Uses:
--     mw.getCurrentFrame()
--     frame:getParent()
--     flat()
local t = mw.getCurrentFrame():getParent()
return flat( t.args, options )
end -- TemplatePar.downcase()
TemplatePar.valid = function ( access, options )
-- Check validity of one particular template parameter
-- Precondition:
--     access   -- id of parameter in template transclusion
--                 string or number
--     options  -- table or nil; optional details
-- Postcondition:
--     Return string with error message as configured;
--            false if valid or no answer permitted
-- Uses:
--     mw.text.trim()
--     TemplatePar.downcase()
--     TemplatePar.framing()
--     frame:getParent()
--     formatted()
--     failure()
--     finalize()
local r = type( access )
if r == "string" then
r = mw.text.trim( access )
if #r == 0 then
r = false
end
elseif r == "number" then
r = access
else
r = false
end
if r then
local params
if type( options ) ~= "table" then
options = { }
end
if options.low then
params = TemplatePar.downcase( options )
else
params = TemplatePar.framing():getParent()
end
r = formatted( params, access, options )
else
r = failure( "noname", false, options )
end
return finalize( r, options )
end -- TemplatePar.valid()
TemplatePar.verify = function ( options )
-- Perform #invoke parameter analysis
-- Precondition:
--     options  -- table or nil; optional details
-- Postcondition:
--     Return string with error message as configured;
--            false if valid
-- Uses:
--     form()
return form( false, options, false )
end -- TemplatePar.verify()
TemplatePar.framing = function( frame )
-- Ensure availability of frame object
-- Precondition:
--     frame  -- object; #invoke environment, or false
-- Postcondition:
--     Return frame object
-- Uses:
--     > 0 then
local suited = string.format( "Q%d", item )
if link then
r = suited
else
local entity = mw.wikibase.getEntity( suited )
if type( entity ) == "table" then
local seek = Failsafe.serialProperty or "P348"
local vsn  = entity:formatPropertyValues( seek )
if type( vsn ) == "table"  and
type( vsn.value ) == "string"  and
vsn.value ~= "" then
if last  and  vsn.value == Failsafe.serial then
r = false
elseif linked then
if mw.title.getCurrentTitle().prefixedText

** mw.wikibase.getSitelink( suited ) then
r = false
else
r = suited
end
else
r = vsn.value
end
end
end
end
end
end
if type( r ) == "nil" then
if not since  or  since <= Failsafe.serial then
r = Failsafe.serial
else
r = false
end
end
return r
end -- Failsafe.failsafe()
-- Provide external access
local p = {}
function p.assert( frame )
-- Perform parameter analysis on some single string
-- Precondition:
--     frame  -- object; #invoke environment
-- Postcondition:
--     Return string with error message or ""
-- Uses:
--     furnish()
return furnish( frame, "assert" )
end -- p.assert()
function p.check( frame )
-- Check validity of template parameters
-- Precondition:
--     frame  -- object; #invoke environment
-- Postcondition:
--     Return string with error message or ""
-- Uses:
--     form()
--     fill()
local options = { optional  = { "all",
"opt",
"cat",
"errNS",
"low",
"format",
"preview",
"template" },
template  = "#invoke:TemplatePar|check|"
}
local r = form( false, options, frame )
if not r then
options = { mandatory = fill( frame.args.all ),
optional  = fill( frame.args.opt ),
cat       = frame.args.cat,
errNS     = frame.args.errNS,
low       = frame.args.low,
format    = frame.args.format,
preview   = frame.args.preview,
template  = frame.args.template
}
r       = form( true, options, frame )
end
return r or ""
end -- p.check()
function p.count( frame )
-- Count number of template parameters
-- Postcondition:
--     Return string with digits including "0"
-- Uses:
--     TemplatePar.count()
return tostring( TemplatePar.count() )
end -- p.count()
function p.countNotEmpty( frame )
-- Count number of template parameters which are not empty
-- Postcondition:
--     Return string with digits including "0"
-- Uses:
--     TemplatePar.countNotEmpty()
return tostring( TemplatePar.countNotEmpty() )
end -- p.countNotEmpty()
function p.match( frame )
-- Combined analysis of parameters and their values
-- Precondition:
--     frame  -- object; #invoke environment
-- Postcondition:
--     Return string with error message or ""
-- Uses:
--     TemplatePar.framing()
--     mw.text.trim()
--     mw.ustring.lower()
--     failure()
--     form()
--     TemplatePar.downcase()
--     figure()
--     feasible()
--     fault()
--     finalize()
local r = false
local options = { cat      = frame.args.cat,
errNS    = frame.args.errNS,
low      = frame.args.low,
format   = frame.args.format,
preview  = frame.args.preview,
template = frame.args.template
}
local k, v, s
local params = { }
TemplatePar.framing( frame )
for k, v in pairs( frame.args ) do
if type( k ) == "number" then
s, v = v:match( "^ *(^=+) *= *(%S.*%S*) *$" )
if s then
s = mw.text.trim( s )
if s == "" then
s = false
end
end
if s then
if options.low then
s = mw.ustring.lower( s )
end
if params s  then
s = params s 
s #s + 1  = v
else
params s  = { v }
end
else
r = failure( "invalidPar",  tostring( k ),  options )
break -- for k, v
end
end
end -- for k, v
if not r then
s = { }
for k, v in pairs( params ) do
s #s + 1  = k
end -- for k, v
options.optional = s
r = form( true, options, frame )
end
if not r then
local errMiss, errValues, lack, rule
local targs = frame:getParent().args
options.optional = nil
if options.low then
targs = TemplatePar.downcase()
else
targs = frame:getParent().args
end
errMiss   = false
errValues = false
for k, v in pairs( params ) do
options.say = k
s           = targs k 
if s then
if s == "" then
lack = true
else
lack = false
end
else
s    = ""
lack = true
end
for r, rule in pairs( v ) do
options = figure( rule, options )
r       = feasible( s, options, true )
if r then
if lack then
if errMiss then
s       = "%s, "%s""
errMiss = string.format( 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 )
-- Versioning interface
local s = type( frame )
local since
if s == "table" then
since = frame.args 1 
elseif s == "string" then
since = frame
end
if since then
since = mw.text.trim( since )
if since == "" then
since = false
end
end
return Failsafe.failsafe( since )  or  ""
end -- p.failsafe
function p.TemplatePar()
-- Retrieve function access for modules
-- Postcondition:
--     Return table with functions
return TemplatePar
end -- p.TemplatePar()
setmetatable( p,  { __call = function ( func, ... )
setmetatable( p, nil )
return Failsafe
end } )
return p