Module:TemplatePar

local TemplatePar = { serial = "2023-03-20",

suite = "TemplatePar",

item = 15393417,

globals = { DateTime = 20652535,

FileMedia = 24765326,

Multilingual = 47541920,

TemplUtl = 52364930,

URLutil = 10859193 } }

--[=[

Template parameter utility

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

]=]

local Local = { frame = false }

local Failsafe = TemplatePar

local GlobalMod = Local

-- Module globals

Local.messagePrefix = "lua-module-TemplatePar-"

Local.L10nDef = {}

Local.L10nDef.en = {

badPattern = "#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-9][0-9]*$",

[ "N+" ] = "^[%-]?[1-9][0-9]*$",

[ "N>0" ] = "^[1-9][0-9]*$",

[ "x" ] = "^[0-9A-Fa-f]*$",

[ "x+" ] = "^[0-9A-Fa-f]+$",

[ "X" ] = "^[0-9A-F]*$",

[ "X+" ] = "^[0-9A-F]+$",

[ "0,0" ] = "^[%-]?[0-9]*,?[0-9]*$",

[ "0,0+" ] = "^[%-]?[0-9]+,[0-9]+$",

[ "0,0+?" ] = "^[%-]?[0-9]+,?[0-9]*$",

[ "0.0" ] = "^[%-]?[0-9]*[%.]?[0-9]*$",

[ "0.0+" ] = "^[%-]?[0-9]+%.[0-9]+$",

[ "0.0+?" ] = "^[%-]?[0-9]+[%.]?[0-9]*$",

[ ".0+" ] = "^[%-]?[0-9]*[%.]?[0-9]+$",

[ "ID" ] = "^[A-Za-z]?[A-Za-z_0-9]*$",

[ "ID+" ] = "^[A-Za-z][A-Za-z_0-9]*$",

[ "ABC" ] = "^[A-Z]*$",

[ "ABC+" ] = "^[A-Z]+$",

[ "Abc" ] = "^[A-Z]*[a-z]*$",

[ "Abc+" ] = "^[A-Z][a-z]+$",

[ "abc" ] = "^[a-z]*$",

[ "abc+" ] = "^[a-z]+$",

[ "aBc+" ] = "^[a-z]+[A-Z][A-Za-z]*$",

[ "w" ] = "^%S*$",

[ "w+" ] = "^%S+$",

[ "base64" ] = "^[A-Za-z0-9%+/]*$",

[ "base64+" ] = "^[A-Za-z0-9%+/]+$",

[ "aa" ] = "[%a%a].*[%a%a]",

[ "pagename" ] = string.format( "^[^#<>%%[%%]|{}%c-%c%c]+$",

1, 31, 127 ),

[ "ref" ] = string.format( "%c'%c`UNIQ%s%sref%s%s%sQINU`%c'%c",

127, 34, "%-", "%-", "%-", "%x+",

"%-", 34, 127 ),

[ "+" ] = "%S"

}

Local.boolean = { ["1"] = true,

["true"] = true,

y = true,

yes = true,

on = true,

["0"] = true,

["false"] = true,

["-"] = true,

n = true,

no = true,

off = true }

Local.patternCJK = false

local foreignModule = function ( access, advanced, append, alt, alert )

-- Fetch global module

-- Precondition:

-- access -- string, with name of base module

-- advanced -- true, for require(); else mw.loadData()

-- append -- string, with subpage part, if any; or false

-- alt -- number, of wikidata item of root; or false

-- alert -- true, for throwing error on data problem

-- Postcondition:

-- Returns whatever, probably table

-- 2020-01-01

local storage = access

local finer = function ()

if append then

storage = string.format( "%s/%s",

storage,

append )

end

end

local fun, lucky, r, suited

if advanced then

fun = require

else

fun = mw.loadData

end

GlobalMod.globalModules = GlobalMod.globalModules or { }

suited = GlobalMod.globalModules[ access ]

if not suited then

finer()

lucky, r = pcall( fun, "Module:" .. storage )

end

if not lucky then

if not suited and

type( alt ) == "number" and

alt > 0 then

suited = string.format( "Q%d", alt )

suited = mw.wikibase.getSitelink( suited )

GlobalMod.globalModules[ access ] = suited or true

end

if type( suited ) == "string" then

storage = suited

finer()

lucky, r = pcall( fun, storage )

end

if not lucky and alert then

error( "Missing or invalid page: " .. storage )

end

end

return r

end -- foreignModule()

local function Foreign( access )

-- Access standardized library

-- Precondition:

-- access -- string, with name of base module

-- Postcondition:

-- Return library table, or not

-- Uses:

local r

if Local[ access ] then

r = Local[ access ]

else

local bib = foreignModule( access,

true,

false,

TemplatePar.globals[ access ],

false )

if type( bib ) == "table" and

type( bib[ access ] ) == "function" then

bib = bib[ access ]()

if type( bib ) == "table" then

r = bib

Local[ access ] = bib

end

end

end

return r

end -- Foreign()

local function containsCJK( analyse )

-- Is any CJK character present?

-- Precondition:

-- analyse -- string

-- Postcondition:

-- Return false iff no CJK present

-- Uses:

-- >< Local.patternCJK

-- mw.ustring.char()

-- mw.ustring.match()

local r = false

if not Local.patternCJK then

Local.patternCJK = mw.ustring.char( 91,

13312, 45, 40959,

131072, 45, 178207,

93 )

end

if mw.ustring.match( analyse, Local.patternCJK ) then

r = true

end

return r

end -- containsCJK()

local function facility( accept, attempt )

-- Check string as possible file name or other source page

-- Precondition:

-- accept -- string; requirement

-- file

-- file+

-- file:

-- file:+

-- image

-- image+

-- image:

-- image:+

-- attempt -- string; to be tested

-- Postcondition:

-- Return error keyword, or false

-- Uses:

-- Module:FileMedia

-- Foreign()

-- FileMedia.isFile()

-- FileMedia.isType()

local r

if attempt and attempt ~= "" then

local FileMedia = Foreign( "FileMedia" )

if FileMedia and type( FileMedia.isFile ) == "function"

and type( FileMedia.isType ) == "function" then

local s, live = accept:match( "^([a-z]+)(:?)%+?$" )

if live then

if FileMedia.isType( attempt, s ) then

if FileMedia.isFile( attempt ) then

r = false

else

r = "notFound"

end

else

r = "invalid"

end

elseif FileMedia.isType( attempt, s ) then

r = false

else

r = "invalid"

end

else

r = "missing"

end

elseif accept:match( "%+$" ) then

r = "empty"

else

r = false

end

return r

end -- facility()

local function factory( say )

-- Retrieve localized message string in content language

-- Precondition:

-- say -- string; message ID

-- Postcondition:

-- Return some message string

-- Uses:

-- > Local.messagePrefix

-- > Local.L10nDef

-- mw.message.new()

-- mw.language.getContentLanguage()

-- Module:Multilingual

-- Foreign()

-- TemplatePar.framing()

-- Multilingual.tabData()

local m = mw.message.new( Local.messagePrefix .. say )

local r = false

if m:isBlank() then

local c = mw.language.getContentLanguage():getCode()

local l10n = Local.L10nDef[ c ]

if l10n then

r = l10n[ say ]

else

local MultiL = Foreign( "Multilingual" )

if MultiL and type( MultiL.tabData ) == "function" then

local lang

r, lang = MultiL.tabData( "I18n/Module:TemplatePar",

say,

false,

TemplatePar.framing() )

end

end

if not r then

r = Local.L10nDef.en[ say ]

end

else

m:inLanguage( c )

r = m:plain()

end

if not r then

r = string.format( "(((%s)))", say )

end

return r

end -- factory()

local function faculty( accept, attempt )

-- Check string as possible boolean

-- Precondition:

-- accept -- string; requirement

-- boolean

-- boolean+

-- attempt -- string; to be tested

-- Postcondition:

-- Return error keyword, or false

-- Uses:

-- Module:TemplUtl

-- Foreign()

-- TemplUtl.faculty()

local r

r = mw.text.trim( attempt ):lower()

if r == "" then

if accept == "boolean+" then

r = "empty"

else

r = false

end

elseif Local.boolean[ r ] or r:match( "^[01%-]+$" ) then

r = false

else

local TemplUtl = Foreign( "TemplUtl" )

if TemplUtl and type( TemplUtl.faculty ) == "function" then

r = TemplUtl.faculty( r, "-" )

if r == "-" then

r = "invalid"

else

r = false

end

else

r = "invalid"

end

end

return r

end -- faculty()

local function failure( spec, suspect, options )

-- Submit localized error message

-- Precondition:

-- spec -- string; message ID

-- suspect -- string or nil; additional information

-- options -- table or nil; optional details

-- options.template

-- Postcondition:

-- Return string

-- Uses:

-- factory()

local r = factory( spec )

if type( options ) == "table" then

if type( options.template ) == "string" then

if #options.template > 0 then

r = string.format( "%s (%s)", r, options.template )

end

end

end

if suspect then

r = string.format( "%s: %s", r, suspect )

end

return r

end -- failure()

local function fair( story, scan )

-- Test for match (possibly user-defined with syntax error)

-- Precondition:

-- story -- string; parameter value

-- scan -- string; pattern

-- Postcondition:

-- Return nil, if not matching, else non-nil

-- Uses:

-- mw.ustring.match()

return mw.ustring.match( story, scan )

end -- fair()

local function familiar( accept, attempt )

-- Check string as possible language name or list

-- Precondition:

-- accept -- string; requirement

-- lang

-- langs

-- langW

-- langsW

-- lang+

-- langs+

-- langW+

-- langsW+

-- attempt -- string; to be tested

-- Postcondition:

-- Return error keyword, or false

-- Uses:

-- Module:Multilingual

-- Foreign()

-- Multilingual.isLang()

local r

if attempt and attempt ~= "" then

local MultiL = Foreign( "Multilingual" )

if MultiL and type( MultiL.isLang ) == "function" then

local lazy = accept:find( "W", 1, true )

if accept:find( "s", 1, true ) then

local group = mw.text.split( attempt, "%s+" )

r = false

for i = 1, #group do

if not MultiL.isLang( group[ i ], lazy ) then

r = "invalid"

break -- for i

end

end -- for i

elseif MultiL.isLang( attempt, lazy ) then

r = false

else

r = "invalid"

end

else

r = "missing"

end

elseif accept:find( "+", 1, true ) then

r = "empty"

else

r = false

end

return r

end -- familiar()

local function far( accept, attempt )

-- Check string as possible URL

-- Precondition:

-- accept -- string; requirement

-- url

-- url+

-- attempt -- string; to be tested

-- Postcondition:

-- Return error keyword, or false

-- Uses:

-- Module:URLutil

-- Foreign()

-- URLutil.isWebURL()

local r

if attempt and attempt ~= "" then

local URLutil = Foreign( "URLutil" )

if URLutil and type( URLutil.isWebURL ) == "function" then

if URLutil.isWebURL( attempt ) then

r = false

else

r = "invalid"

end

else

r = "missing"

end

elseif accept:find( "+", 1, true ) then

r = "empty"

else

r = false

end

return r

end -- far()

local function fast( accept, attempt )

-- Check string as possible date or time

-- Precondition:

-- accept -- string; requirement

-- datetime

-- datetime+

-- datetime/y

-- datetime/y+

-- datetime/ym

-- datetime/ym+

-- datetime/ymd

-- datetime/ymd+

-- attempt -- string; to be tested

-- Postcondition:

-- Return error keyword, or false

-- Uses:

-- Module:DateTime

-- Foreign()

-- DateTime.DateTime()

local r

r = mw.text.trim( attempt )

if r == "" then

if accept:find( "+", 1, true ) then

r = "empty"

else

r = false

end

else

local DateTime = Foreign( "DateTime" )

if type( DateTime ) == "table" then

local d = DateTime( attempt )

if type( d ) == "table" then

if accept:find( "/", 1, true ) then

r = "invalid"

if accept:sub( 1, 10 ) == "datetime/y" then

if d.year then

r = false

if accept:sub( 1, 11 ) == "datetime/ym" then

if d.month then

if accept:sub( 1, 12 )

== "datetime/ymd" then

if not d.dom then

r = "invalid"

end

end

else

r = "invalid"

end

end

end

end

else

r = false

end

else

r = "invalid"

end

else

r = "invalid"

end

end

return r

end -- fast()

local function fault( store, key )

-- Add key to collection string and insert separator

-- Precondition:

-- store -- string or nil or false; collection string

-- key -- string or number; to be appended

-- Postcondition:

-- Return string; extended

local r

local s

if type( key ) == "number" then

s = tostring( key )

else

s = key

end

if store then

r = string.format( "%s; %s", store, s )

else

r = s

end

return r

end -- fault()

local function feasible( analyze, options, abbr )

-- Check content of a value

-- Precondition:

-- analyze -- string to be analyzed

-- options -- table or nil; optional details

-- options.pattern

-- options.key

-- options.say

-- abbr -- true: abbreviated error message

-- Postcondition:

-- Return string with error message as configured;

-- false if valid or no answer permitted

-- Uses:

-- > Local.patterns

-- failure()

-- mw.text.trim()

-- faculty()

-- fast()

-- facility()

-- familiar()

-- far()

-- fair()

-- containsCJK()

local r = false

local s = false

local show = nil

local scan = false

local stuff = mw.text.trim( analyze )

if type( options.pattern ) == "string" then

if options.key then

r = failure( "dupRule", false, options )

else

scan = options.pattern

end

else

if type( options.key ) == "string" then

s = mw.text.trim( options.key )

else

s = "+"

end

if s ~= "*" then

scan = Local.patterns[ s ]

end

if type( scan ) == "string" then

if s == "n" or s == "0,0" or s == "0.0" then

if not stuff:match( "[0-9]" ) and

not stuff:match( "^%s*$" ) then

scan = false

if options.say then

show = string.format( ""%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 )

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( "%%%)%)", "}}" )

: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 = "{{REVISIONID}}"

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( "%sCategory:%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 < options.min then

r = failure( "minmax",

string.format( "%d > %d",

options.min,

options.max ),

options )

end

end

if #analyze < options.min and not r then

show = " <" .. options.min

if options.say then

show = string.format( "%s "%s"", show, options.say )

end

r = failure( "tooShort", show, options )

end

else

r = failure( "invalidPar", "min", options )

end

end

if options.max and not r then

if type( options.max ) == "number" then

if #analyze > options.max then

show = " >" .. options.max

if options.say then

show = string.format( "%s "%s"", show, options.say )

end

r = failure( "tooLong", show, options )

end

else

r = failure( "invalidPar", "max", options )

end

end

return r

end -- format()

local function formatted( assignment, access, options )

-- Check validity of one particular parameter in a collection

-- Precondition:

-- assignment -- collection

-- access -- id of parameter in collection

-- options -- table or nil; optional details

-- Postcondition:

-- Return string with error message as configured;

-- false if valid or no answer permitted

-- Uses:

-- mw.text.trim()

-- format()

-- failure()

local r = false

if type( assignment ) == "table" then

local story = assignment.args[ access ] or ""

if type( access ) == "number" then

story = mw.text.trim( story )

end

if type( options ) ~= "table" then

options = { }

end

options.say = access

r = format( story, options )

end

return r

end -- formatted()

local function furnish( frame, action )

-- Prepare #invoke evaluation of .assert() or .valid()

-- Precondition:

-- frame -- object; #invoke environment

-- action -- "assert" or "valid"

-- Postcondition:

-- Return string with error message or ""

-- Uses:

-- form()

-- failure()

-- finalize()

-- TemplatePar.valid()

-- TemplatePar.assert()

local options = { mandatory = { "1" },

optional = { "2",

"cat",

"errNS",

"low",

"max",

"min",

"format",

"preview",

"template" },

template = string.format( "#invoke:%s|%s|",

"TemplatePar",

action )

}

local r = form( false, options, frame )

if not r then

local s

options = { cat = frame.args.cat,

errNS = frame.args.errNS,

low = frame.args.low,

format = frame.args.format,

preview = frame.args.preview,

template = frame.args.template

}

options = figure( frame.args[ 2 ], options )

if type( frame.args.min ) == "string" then

s = frame.args.min:match( "^%s*([0-9]+)%s*$" )

if s then

options.min = tonumber( s )

else

r = failure( "invalidPar",

"min=" .. frame.args.min,

options )

end

end

if type( frame.args.max ) == "string" then

s = frame.args.max:match( "^%s*([1-9][0-9]*)%s*$" )

if s then

options.max = tonumber( s )

else

r = failure( "invalidPar",

"max=" .. frame.args.max,

options )

end

end

if r then

r = finalize( r, options )

else

s = frame.args[ 1 ] or ""

r = tonumber( s )

if ( r ) then

s = r

end

if action == "valid" then

r = TemplatePar.valid( s, options )

elseif action == "assert" then

r = TemplatePar.assert( s, "", options )

end

end

end

return r or ""

end -- furnish()

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

-- Perform parameter analysis on a single string

-- Precondition:

-- analyze -- string to be analyzed

-- append -- string: append error message, prepending

-- false or nil: throw error with message

-- options -- table; optional details

-- Postcondition:

-- Return string with error message as configured;

-- false if valid

-- Uses:

-- format()

local r = format( analyze, options )

if ( r ) then

if ( type( append ) == "string" ) then

if ( append ~= "" ) then

r = string.format( "%s
%s", append, r )

end

else

error( r, 0 )

end

end

return r

end -- TemplatePar.assert()

TemplatePar.check = function ( options )

-- Run parameter analysis on current template environment

-- Precondition:

-- options -- table or nil; optional details

-- options.mandatory

-- options.optional

-- Postcondition:

-- Return string with error message as configured;

-- false if valid

-- Uses:

-- form()

return form( true, options, false )

end -- TemplatePar.check()

TemplatePar.count = function ()

-- Return number of template parameters

-- Postcondition:

-- Return number, starting at 0

-- Uses:

-- mw.getCurrentFrame()

-- frame:getParent()

local k, v

local r = 0

local t = mw.getCurrentFrame():getParent()

local o = t.args

for k, v in pairs( o ) do

r = r + 1

end -- for k, v

return r

end -- TemplatePar.count()

TemplatePar.countNotEmpty = function ()

-- Return number of template parameters with more than whitespace

-- Postcondition:

-- Return number, starting at 0

-- Uses:

-- mw.getCurrentFrame()

-- frame:getParent()

local k, v

local r = 0

local t = mw.getCurrentFrame():getParent()

local o = t.args

for k, v in pairs( o ) do

if not v:match( "^%s*$" ) then

r = r + 1

end

end -- for k, v

return r

end -- TemplatePar.countNotEmpty()

TemplatePar.downcase = function ( options )

-- Return all template parameters with downcased name

-- Precondition:

-- options -- table or nil; optional messaging details

-- Postcondition:

-- Return table, may be empty; or string with error message.

-- Uses:

-- mw.getCurrentFrame()

-- frame:getParent()

-- flat()

local t = mw.getCurrentFrame():getParent()

return flat( t.args, options )

end -- TemplatePar.downcase()

TemplatePar.valid = function ( access, options )

-- Check validity of one particular template parameter

-- Precondition:

-- access -- id of parameter in template transclusion

-- string or number

-- options -- table or nil; optional details

-- Postcondition:

-- Return string with error message as configured;

-- false if valid or no answer permitted

-- Uses:

-- mw.text.trim()

-- TemplatePar.downcase()

-- TemplatePar.framing()

-- frame:getParent()

-- formatted()

-- failure()

-- finalize()

local r = type( access )

if r == "string" then

r = mw.text.trim( access )

if #r == 0 then

r = false

end

elseif r == "number" then

r = access

else

r = false

end

if r then

local params

if type( options ) ~= "table" then

options = { }

end

if options.low then

params = TemplatePar.downcase( options )

else

params = TemplatePar.framing():getParent()

end

r = formatted( params, access, options )

else

r = failure( "noname", false, options )

end

return finalize( r, options )

end -- TemplatePar.valid()

TemplatePar.verify = function ( options )

-- Perform #invoke parameter analysis

-- Precondition:

-- options -- table or nil; optional details

-- Postcondition:

-- Return string with error message as configured;

-- false if valid

-- Uses:

-- form()

return form( false, options, false )

end -- TemplatePar.verify()

TemplatePar.framing = function( frame )

-- Ensure availability of frame object

-- Precondition:

-- frame -- object; #invoke environment, or false

-- Postcondition:

-- Return frame object

-- Uses:

-- >< Local.frame

if not Local.frame then

if type( frame ) == "table" and

type( frame.args ) == "table" and

type( frame.getParent ) == "function" and

type( frame:getParent() ) == "table" and

type( frame:getParent().getParent ) == "function" and

type( frame:getParent():getParent() ) == "nil" then

Local.frame = frame

else

Local.frame = mw.getCurrentFrame()

end

end

return Local.frame

end -- TemplatePar.framing()

Failsafe.failsafe = function ( atleast )

-- Retrieve versioning and check for compliance

-- Precondition:

-- atleast -- string, with required version

-- or wikidata|item|~|@ or false

-- Postcondition:

-- Returns string -- with queried version/item, also if problem

-- false -- if appropriate

-- 2020-08-17

local since = atleast

local last = ( since == "~" )

local linked = ( since == "@" )

local link = ( since == "item" )

local r

if last or link or linked or since == "wikidata" then

local item = Failsafe.item

since = false

if type( item ) == "number" and item > 0 then

local suited = string.format( "Q%d", item )

if link then

r = suited

else

local entity = mw.wikibase.getEntity( suited )

if type( entity ) == "table" then

local seek = Failsafe.serialProperty or "P348"

local vsn = entity:formatPropertyValues( seek )

if type( vsn ) == "table" and

type( vsn.value ) == "string" and

vsn.value ~= "" then

if last and vsn.value == Failsafe.serial then

r = false

elseif linked then

if mw.title.getCurrentTitle().prefixedText

== mw.wikibase.getSitelink( suited ) then

r = false

else

r = suited

end

else

r = vsn.value

end

end

end

end

end

end

if type( r ) == "nil" then

if not since or since <= Failsafe.serial then

r = Failsafe.serial

else

r = false

end

end

return r

end -- Failsafe.failsafe()

-- Provide external access

local p = {}

function p.assert( frame )

-- Perform parameter analysis on some single string

-- Precondition:

-- frame -- object; #invoke environment

-- Postcondition:

-- Return string with error message or ""

-- Uses:

-- furnish()

return furnish( frame, "assert" )

end -- p.assert()

function p.check( frame )

-- Check validity of template parameters

-- Precondition:

-- frame -- object; #invoke environment

-- Postcondition:

-- Return string with error message or ""

-- Uses:

-- form()

-- fill()

local options = { optional = { "all",

"opt",

"cat",

"errNS",

"low",

"format",

"preview",

"template" },

template = "#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