Module:TemplatePar/sandbox explained

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

-- Module globalslocal TemplatePar = local MessagePrefix = "lua-module-TemplatePar-"local L10nDef = L10nDef.en = L10nDef.de = local Patterns = 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 patternCJK = patternCJK or mw.ustring.char(91, 13312, 45, 40959, 131072, 45, 178207, 93) if mw.ustring.match(s, patternCJK) then r = true end return rend -- 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 rend -- 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 ] or L10nDef["en" ] r = l10n[say ] else m:inLanguage(c) r = m:plain end r = r or string.format("(((%s)))", say) return rend -- 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 rend -- 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 rend -- 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 rend -- 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 falseend -- 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 rend -- 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("%%%(%(", "") options.pattern = sub options.key = nil else options.key = story options.pattern = nil end end return rend -- 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 rend -- 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 frame = frame or mw.getCurrentFrame if frame:preprocess("")

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

@@@" 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 r = r or "" 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", r, s) end end -- for i end end end return rend -- 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 falseend -- 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 rend -- 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 rend -- 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 rend -- 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 rend -- 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 rend -- 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 = local r = form(false, options, frame) if not r then local s options = 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
-- 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 rend -- 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 rend -- 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 rend -- 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 accesslocal 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 = local r = form(false, options, frame) if not r then options = 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 = 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 ] = 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 TemplateParend -- .TemplatePar

return p