Modul:MediaWikiGadgetDefinition
Zur Navigation springen
Zur Suche springen
Vorlagenprogrammierung | Diskussionen | Lua | Unterseiten | |||
Modul | Deutsch | English
|
Modul: | Dokumentation |
Diese Seite enthält Code in der Programmiersprache Lua. Einbindungszahl Cirrus
Dies ist die (produktive) Mutterversion eines global benutzten Lua-Moduls.
Wenn die serial-Information nicht übereinstimmt, müsste eine Kopie hiervon in das lokale Wiki geschrieben werden.
Wenn die serial-Information nicht übereinstimmt, müsste eine Kopie hiervon in das lokale Wiki geschrieben werden.
Versionsbezeichnung auf WikiData:
2024-11-15
local MediaWikiGadgetDefinition = { suite = "MediaWikiGadgetDefinition",
serial = "2024-11-15",
item = 111520596 }
--[==[
Documentation, validation and comparison of MediaWiki gadget definitions
]==]
local Failsafe = MediaWikiGadgetDefinition
local GlobalMod = MediaWikiGadgetDefinition
local ACTIVE = { }
local COMMONS = { }
local ERROR = { }
local GLOBAL = { }
local HTML = { bgcolorSleeping = "D0D0D0" }
local I18N = { }
local OPTIONS = { }
local ROW = { comment = { live = false,
suffix = "#" } }
local TEMPLATE = { }
local VALUES = { }
COMMONS.tabs = { config = "MediaWikiGadgetDefinition",
actions = "MediaWiki/URLactions",
contentModels = "MediaWiki/Contentmodels",
skins = "MediaWiki/SkinsWMF",
modules = "MediaWiki/ResourceLoaderModules"
}
OPTIONS.config = { { sign = "ID",
live = true,
mode = 0,
type = "string",
scan = "rootpage" },
{ sign = "ResourceLoader",
live = true,
mode = 1,
type = "boolean" },
{ sign = "default",
live = true,
mode = 1,
type = "boolean" },
{ sign = "hidden",
live = true,
mode = 1,
type = "boolean" },
{ sign = "package",
live = true,
mode = 1,
type = "boolean" },
{ sign = "targets",
live = true,
mode = 1,
type = { "string" },
scan = "array" },
{ sign = "skins",
live = true,
mode = 1,
type = { "string" },
scan = "array" },
{ sign = "rights",
live = true,
mode = 1,
type = { "string" },
scan = "letter-id" },
{ sign = "actions",
live = true,
mode = 1,
type = { "string" },
scan = "array" },
{ sign = "namespaces",
live = true,
mode = 1,
type = { "number" },
scan = "namespace" },
{ sign = "specials",
live = false,
mode = 1,
type = { "string" },
scan = "LetterID" },
{ sign = "contentModels",
live = true,
mode = 1,
type = { "string" },
scan = "array" },
{ sign = "pageprops",
live = false,
mode = 1,
type = { "string" },
scan = "letter-_ID" },
{ sign = "transcludes",
live = false,
mode = 1,
type = { "string" },
scan = "page" },
{ sign = "groups",
live = false,
mode = 1,
type = { "string" },
scan = "letter-id" },
{ sign = "userlangs",
live = false,
mode = 1,
type = { "string" },
scan = "lang" },
{ sign = "contentlangs",
live = false,
mode = 1,
type = { "string" },
scan = "lang" },
{ sign = "category",
live = false,
mode = 1,
type = "string",
scan = "page" },
{ sign = "categories",
live = true,
mode = 1,
type = { "string" },
scan = "page" },
{ sign = "type",
live = true,
mode = 1,
type = "string",
scan = "radio" },
{ sign = "top",
live = true,
mode = 1,
type = "boolean" },
{ sign = "supportsUrlLoad",
live = true,
mode = 1,
type = "boolean" },
{ sign = "dependencies",
live = true,
mode = 2,
type = { "string" },
lone = true,
scan = "letter.id" },
{ sign = "scripts",
live = true,
mode = 3,
type = { "string" },
scan = "page" },
{ sign = "styles",
live = true,
mode = 3,
type = { "string" },
scan = "page" },
{ sign = "datas",
live = true,
mode = 3,
type = { "string" },
scan = "page" },
{ sign = "peers",
live = true,
mode = 3,
type = { "string" },
lone = true },
{ sign = "messages",
live = false,
mode = 3,
type = { "string" },
lone = true,
scan = "rootpage" },
{ sign = "Comment",
live = false,
mode = 4,
type = "string" }
}
VALUES.arrays = { actions = false,
contentModels = false,
skins = false,
targets = { { "desktop", true },
{ "mobile", true } }
}
VALUES.radio = { type = { { "general", true },
{ "styles", true } }
}
VALUES.validate = { LetterID = { { "%u%a+" } },
lang = { { "%l%l%l?",
"%l%l%l?-%a+" } },
["letter-_ID"] = { { "%l[%-_%a]+%a" },
{ "[%-_][%-_]" } },
["letter-id"] = { { "%l[%-%l]+%l" },
{ "--" } },
["letter.id"] = { { "%l[%.%l]+%l" },
{ "%.%." } },
namespace = { { "-?[1-9]%d*" } },
page = { { "[^#%[%]<>{}|]+" },
{ "[ _][ _]" } },
rootpage = { { "[^/#%[%]<>{}|]+" },
{ "[ _][ _]" } }
}
I18N.texts =
{ ActiveSpec = { en = "Active specification.",
de = "Aktive Spezifikation." },
Assigned = { en = "Assigned",
de = "Zuweisung" },
AtLine = { en = "at line",
de = "in Zeile" },
BadAssigned = { en = "assignment to sole boolean",
de = "Wertzuweisung an boolean" },
BadLines = { en = "Bad definition lines" },
BadNamespace = { en = "Bad namespace",
de = "Namensraum ungültig" },
BadTemplateArg = { en = "Template argument no string",
de = "Vorlagenparameter kein string" },
BadTemplateArgs = { en = "Template arguments no table",
de = "Vorlagenparameter keine table" },
defaultTrue = { en = "default for all",
de = "Vorgabe für alle" },
defaultFalse = { en = "activation needed",
de = "benötigt Aktivierung" },
Detail = { en = "Property",
de = "Eigenschaft" },
hiddenTrue = { en = "hidden",
de = "versteckt" },
hiddenFalse = { en = "configurable",
de = "konfigurierbar" },
IDmissing = { en = "ID missing",
de = "ID erforderlich" },
InvalidHeadID = { en = "Invalid headline ID",
de = "Ungültige Überschrift-ID" },
InvalidHeadline = { en = "Invalid headline",
de = "Ungültige Überschrift" },
InvalidID = { en = "Invalid gadget ID",
de = "Ungültige Gadget ID" },
InvalidResource = { en = "Invalid resource page name",
de = "Seitenname der Ressource ungültig" },
InvalidScope = { en = "Scope invalid",
de = "Ungültiger Scope" },
MissingValue = { en = "Missing value",
de = "Wert fehlt" },
NoClosingBracket = { en = "Closing bracket missing",
de = "Schließende Klammer fehlt" },
NoCommentsYet = { en = "Comments not yet available",
de = "Kommentare noch nicht möglich" },
NoGadgetID = { en = "Gadget ID missing",
de = "Gadget ID erforderlich" },
NoID = { en = "Missing identifier",
de = "Bezeichner fehlt" },
NoOptionID = { en = "Option name missing",
de = "Optionsname fehlt" },
NoResources = { en = "No resource page name",
de = "Kein Seitenname einer Ressource" },
packageTrue = { en = "package support",
de = "package-Unterstützung" },
packageFalse = { en = "no package support",
de = "keine package-Unterstützung" },
RepeatedID = { en = "Gadget ID repeated",
de = "Gadget ID wiederholt" },
RepeatedOption = { en = "Option name repeated",
de = "Optionsname wiederholt" },
ResourceLoaderTrue = { en = "supported",
de = "unterstützt" },
ResourceLoaderFalse = { en = "not supported",
de = "nicht unterstützt" },
SpecMismatch = { en = "Specification mismatch:",
de = "Spezifikation abweichend:" },
supportsUrlLoadTrue = { en = "URL loading supported",
de = "Laden per URL unterstützt" },
supportsUrlLoadFalse = { en = "URL loading not supported",
de = "Laden per URL nicht unterstützt" },
topTrue = { en = "top loading",
de = "top-Laden" },
topFalse = { en = "regular loading sequence",
de = "normale Ladereihenfolge" },
UnknownOptionID = { en = "Unknown option name",
de = "Optionsname unbekannt" },
Whitespace = { en = "Superfluous whitespace",
de = "Überflüssiger Whitespace" }
}
local find = function ( area, at )
-- Check for existing page
-- Precondition:
-- area -- number, of namespace, or not
-- at -- string, with name of page, or mw.title
-- Postcondition:
-- Returns true if page existing
local r, t
if area then
t = mw.title.makeTitle( area, at )
elseif type( at ) == "table" then
t = at
end
if t and t.protectionLevels.edit then
r = true
end
return r
end -- find()
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
-- 2019-10-29
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, 0 )
end
end
return r
end -- foreignModule()
ACTIVE.first = function ()
-- Initialize site effective definitions
local source = "Gadgets-definition"
local ns, s, t
ACTIVE.start = "Gadget-"
if GLOBAL.scope == "site" then
s = GLOBAL.config["2302"]
if type( s ) == "string" and
s ~= "" and
s ~= "-" then
ns = 2302
source = s
ACTIVE.ns = 2300
ACTIVE.start = ""
else
ns = 8
end
elseif GLOBAL.scope == "global" then
elseif GLOBAL.scope == "text" then
else
ns, s = GLOBAL.focus( GLOBAL.scope )
if ns then
ACTIVE.ns = ns
if s:sub( -1, 1 ) ~= "/" then
s = s .. "/"
end
source = s .. source
ACTIVE.start = s .. "Gadget-"
end
end
if ns then
t = mw.title.makeTitle( ns, source )
ACTIVE.ns = ACTIVE.ns or ns
ACTIVE.source = t.prefixedText
ACTIVE.live = find( false, t )
if ACTIVE.live then
ACTIVE.story = GLOBAL.frame():expandTemplate{ title = t }
ACTIVE.title = t
end
end
end -- ACTIVE.first()
COMMONS.factory = function ( all, access )
-- Retrieve global configuration
-- Precondition:
-- all -- table, with raw tabular data
-- access -- string, with keyword
-- Postcondition:
-- Returns table, with configuration data
local r = { }
local t = { }
local e, s, v
table.insert( t, "string" )
if access == "modules" then
table.insert( t, "string" )
elseif access == "actions" then
table.insert( t, "boolean" )
elseif access == "contentModels" then
elseif access == "skins" then
table.insert( t, "boolean" )
end
for i = 1, #all do
e = all[ i ]
if type( e ) == "table" and
type( e[ 1 ] ) == "string" then
s = mw.text.trim( e[ 1 ] )
if s ~= "" and
not s:find( "[ \"\n=&|{}%[%]<>#]" ) then
v = e[ 2 ]
if t[ 2 ] then
if type( v ) ~= t[ 2 ] then
s = false
end
elseif access == "contentModels" then
v = true
end
if s then
if access == "modules" then
r[ s ] = v
else
e = { }
table.insert( e, s )
table.insert( e, v )
table.insert( r, e )
end
end
end
end
end -- for i
return r
end -- COMMONS.factory()
COMMONS.fetch = function ( access )
-- Retrieve global configuration
-- Precondition:
-- access -- string, with keyword
-- Postcondition:
-- Returns table, with configuration data, or true if unavailable
COMMONS.tabDATA = COMMONS.tabDATA or { }
if not COMMONS.tabDATA[ access ] then
local storage = COMMONS.tabs[ access ]
local v
if storage then
local lucky, data
storage = string.format( "%s.tab", storage )
lucky, data = pcall( mw.ext.data.get, storage )
if type( data ) == "table" then
data = data.data
if type( data ) == "table" then
v = data
end
end
end
if v then
v = COMMONS.factory( v, access )
else
v = true
end
COMMONS.tabDATA[ access ] = v
end
return COMMONS.tabDATA[ access ]
end -- COMMONS.fetch()
ERROR.fault = function ( about, area )
-- Register an error
-- Precondition:
-- about -- string, with message
-- area -- string, with field, or not
local e = { }
ERROR.errors = ERROR.errors or { }
table.insert( e, about )
table.insert( e, area )
table.insert( ERROR.errors, e )
end -- ERROR.fault()
ERROR.flat = function ( about, align )
-- Format code
-- Precondition:
-- about -- string, with message
-- align -- true, if string requested
-- Postcondition:
-- Returns mw.html, or string
local r = mw.html.create( "code" )
:wikitext( mw.text.nowiki( about ) )
if align then
r = tostring( r )
end
return r
end -- ERROR.flat()
ERROR.full = function ()
-- Retrieve list of all error messages
-- Postcondition:
-- Returns mw.html, or not
local r
return r
end -- ERROR.full()
GLOBAL.features = function ()
-- Retrieve local configuration
-- Postcondition:
-- Configuration updated where requested
local site = GLOBAL.frame():getTitle() .. "/local"
local seek = string.format( "^%s:(.+)$",
mw.site.namespaces[ 828 ].name )
site = site:match( seek, 1 )
if find( 828, site ) then
local lucky, config = pcall( mw.loadData, site )
if type( config ) == "table" then
if type( config.modules ) == "table" then
for k, v in pairs( config.modules ) do
GLOBAL.modules[ k ] = v
end -- for k, v
end
if type( config.actions ) == "table" then
for k, v in pairs( config.actions ) do
VALUES.arrays.actions[ k ] = v
end -- for k, v
end
if type( config.IDexceptions ) == "table" then
for k, v in pairs( config.IDexceptions ) do
GLOBAL.IDexceptions[ k ] = v
end -- for k, v
end
end
end
end -- GLOBAL.features()
GLOBAL.first = function ()
-- Initialize global and local configuration, if not yet done
-- Postcondition:
-- Configuration up to date
if not GLOBAL.config then
local config = COMMONS.fetch( "config" )
if type( config ) ~= "table" then
config = { }
end
GLOBAL.config = config
VALUES.full()
config = COMMONS.fetch( "modules" )
if type( config ) ~= "table" then
config = { }
end
GLOBAL.modules = config
GLOBAL.IDexceptions = { }
GLOBAL.features()
OPTIONS.first()
end
end -- GLOBAL.first()
GLOBAL.focus = function ( area )
-- Analyze page name in name space
-- Precondition:
-- area -- string, with page name, not in gadget spaces
-- Postcondition:
-- Returns:
-- 1. namespace number
-- 2. normalized page title
local i = area:find( ":", 1, true )
local rNS, rT
if i then
local s
if i == 1 then
rNS = 0
rT = area:sub( i + 1 )
else
local q
s = mw.text.trim( area:sub( 1, i - 1 ) )
q = mw.site.namespaces[ s ]
if q and
q.id ~= 8 and
q.id ~= 2300 and
q.id ~= 2302 then
rNS = q.id
end
end
if rNS then
s = mw.text.trim( area:sub( i + 1 ) )
rT = string.format( "%s:%s",
mw.site.namespaces[ rNS ].name,
s )
end
else
rNS = 0
rT = area
end
return rNS, rT
end -- GLOBAL.focus()
GLOBAL.frame = function ( frame )
-- Retrieve and memorize frame object
-- Precondition:
-- frame -- frame object, or not
-- Postcondition:
-- Returns frame object
if frame then
GLOBAL.Frame = frame
end
if not GLOBAL.Frame then
GLOBAL.Frame = mw.getCurrentFrame()
end
return GLOBAL.Frame
end -- GLOBAL.frame()
HTML.feature = function ( at, assigned, about, fault )
-- Format single gadget option table row
-- Precondition:
-- at -- string, with gadget option name
-- assigned -- gadget option value
-- about -- table, with gadget option configuration
-- fault -- function, as error handler
-- Postcondition:
-- Returns mw.html object <tr>
local r = mw.html.create( "tr" )
local td1 = mw.html.create( "td" )
local td2 = mw.html.create( "td" )
local e, ns, s
td1:wikitext( at )
if at:sub( 1, 1 ) == "-" then
s = at:sub( 2 )
td1:attr( "data-sort-value", s .. "-" )
else
s = at
end
if about.type == "boolean" then
if assigned then
s = s .. "True"
else
s = s .. "False"
end
td2:wikitext( I18N.fetch( s ) )
elseif type( about.type ) == "table" and
type( assigned ) == "table" then
if s == "targets" or
s == "skins" or
s == "rights" or
s == "actions" or
s == "contentModels" or
s == "pageprops" or
s == "groups" or
s == "type" then
for i = 1, #assigned do
e = mw.html.create( "code" )
:css( "margin-right", "0.4em" )
:css( "white-space", "nowrap" )
:wikitext( assigned[ i ] )
td2:node( e )
end -- for i
elseif s == "namespaces" then
local q
for i = 1, #assigned do
s = assigned[ i ]
ns = tonumber( s )
e = mw.html.create( "code" )
:css( "margin-right", "0.4em" )
:wikitext( s )
q = mw.site.namespaces[ ns ]
if q then
e:attr( "title", q.name .. ":" )
else
e:css( "background-color", "#FF0000" )
e:css( "color", "#FFFF00" )
e:css( "font-weight", "bold" )
end
td2:node( e )
end -- for i
elseif s == "specials" then
local space = mw.site.namespaces.special.name
local k = #space + 2
for i = 1, #assigned do
s = mw.text.nowiki( assigned[ i ] )
e = mw.html.create( "code" )
:css( "margin-right", "0.4em" )
:wikitext( s )
s = GLOBAL.frame():callParserFunction( "#special",
assigned[ i ] )
e:attr( "title", s:sub( k ) )
td2:node( e )
end -- for i
elseif s == "transcludes" or
s == "categories" then
local link14 = ( s == "categories" )
for i = 1, #assigned do
s = mw.text.nowiki( assigned[ i ] )
if link14 then
s = string.format( ":%s:%s",
mw.site.namespaces[14].name,
s )
end
s = string.format( "[[%s]]", s )
e = mw.html.create( "span" )
:css( "margin-right", "0.4em" )
:css( "white-space", "nowrap" )
e:wikitext( s )
td2:node( e )
end -- for i
elseif s == "userlangs" or
s == "contentlangs" then
elseif s == "dependencies" then
local hash = COMMONS.tabDATA.modules
if type( hash ) ~= "table" then
hash = false
end
for i = 1, #assigned do
s = mw.text.nowiki( assigned[ i ] )
e = mw.html.create( "span" )
:css( "margin-right", "0.4em" )
:css( "white-space", "nowrap" )
if hash and hash[ s ] then
s = string.format( "[[%s|%s]]", hash[ s ], s )
end
e:wikitext( s )
td2:node( e )
end -- for i
elseif s == "scripts" or
s == "styles" or
s == "datas" or
s == "peers" then
local t
for i = 1, #assigned do
s = mw.text.nowiki( assigned[ i ] )
t = mw.title.makeTitle( ACTIVE.ns,
ACTIVE.start .. s )
e = mw.html.create( "span" )
:css( "margin-right", "0.4em" )
:css( "white-space", "nowrap" )
:wikitext( string.format( "[[%s|%s]]",
t.prefixedText,
s ) )
td2:node( e )
end -- for i
elseif s == "messages" then
else
td2:wikitext( tostring(assigned) )
end
else
td2:wikitext( tostring(assigned) )
end
if not about.live then
r:css( "background-color", "#" .. HTML.bgcolorSleeping )
end
r:node( td1 )
:node( td2 )
return r
end -- HTML.feature()
HTML.features = function ( assigned )
-- Format gadget options table
-- Precondition:
-- assigned -- table, with gadget options
-- Postcondition:
-- Returns mw.html object <table>
local r = mw.html.create( "table" )
local n = 0
local fault = function ( add )
assigned.ERRORS = assigned.ERRORS or { }
table.insert( assigned.ERRORS, add )
end
local e, o, s, td, th1, th2
r:addClass( "wikitable" )
if assigned.ID then
local t
s = mw.text.nowiki( assigned.ID )
t = mw.title.makeTitle( 8, "Gadget-" .. s )
e = mw.html.create( "caption" )
:addClass( "mw-code" )
:attr( "id", s )
:wikitext( string.format( "[[%s|%s]]",
t.prefixedText, s ) )
r:newline()
:node( e )
if find( false, t ) then
s = GLOBAL.frame():callParserFunction( "int",
t.text )
td = mw.html.create( "td" )
:attr( "colspan", "2" )
:wikitext( s )
r:newline()
:node( mw.html.create( "tr" )
:addClass( "sortbottom" )
:node( td ) )
end
else
fault( I18N.fetch( "NoID" ) )
end
th1 = mw.html.create( "th" )
:wikitext( I18N.fetch( "Detail" ) )
th2 = mw.html.create( "th" )
:addClass( "unsortable" )
:wikitext( I18N.fetch( "Assigned" ) )
r:newline()
:node( mw.html.create( "tr" )
:node( th1 )
:node( th2 ) )
for i = 2, #OPTIONS.order - 1 do
o = OPTIONS.order[ i ]
s = o.sign
e = assigned[ s ]
if type( e ) ~= "nil" then
r:newline()
:node( HTML.feature( s, e, o, fault ) )
n = n + 1
end
end -- for i
if n > 1 then
r:addClass( "sortable" )
end
if assigned.Comment then
td = mw.html.create( "td" )
:addClass( "sortbottom" )
:attr( "colspan", "2" )
:wikitext( assigned.Comment )
r:newline()
:node( mw.html.create( "tr" )
:node( td ) )
end
if assigned.ERRORS then
e = mw.html.create( "ul" )
for i = 1, #assigned.ERRORS do
e:node( mw.html.create( "li" )
:wikitext( assigned.ERRORS[ i ] ) )
end -- for i
td = mw.html.create( "td" )
:addClass( "error" )
:addClass( "sortbottom" )
:attr( "colspan", "2" )
:node( e )
r:newline()
:node( mw.html.create( "tr" )
:node( td ) )
end
r:newline()
return r
end -- HTML.features()
HTML.flat = function ( apply )
-- Format coded value
-- Precondition:
-- apply -- string, with value
-- Postcondition:
-- Returns mw.html object <div>
local e = mw.html.create( "pre" )
local r = mw.html.create( "div" )
e:addClass( "mw-code" )
:css( "overflow", "auto" )
:css( "margin-right", "0.5em" )
:css( "margin-top", "0.5em" )
:css( "white-space", "nowrap" )
:wikitext( mw.text.nowiki( apply ) )
r:addClass( "mw-highlight mw-highlight-lang-text" )
:addClass( "mw-highlight-copy" )
:css( "margin-right", "2em" )
:css( "padding-top", "1em" )
:newline()
:node( e )
return r
end -- HTML.flat()
HTML.folder = function ( assigned )
-- Build gadget options table with resulting definition row and link
-- Precondition:
-- assigned -- table, with gadget options
-- Postcondition:
-- Returns mw.html object <div>
local e = mw.html.create( "div" )
:wikitext( string.format( "[[%s]]",
ACTIVE.source ) )
local g = ROW.fiat( assigned )
local r = mw.html.create( "div" )
:node( HTML.features( assigned ) )
:newline()
:node( HTML.flat( g ) )
:newline()
:node( e )
local d = ROW.found( assigned.ID )
if d then
d = OPTIONS.fair( assigned, d )
e = mw.html.create( "div" )
if d then
local diff = mw.html.create( "div" )
local p = mw.html.create( "span" )
p:css( "color", "#FF0000" )
:css( "font-weight", "bold" )
:wikitext( I18N.fetch( "SpecMismatch" ) )
e:node( p )
diff:css( "margin-left", "1em" )
:css( "margin-right", "1em" )
for k, v in pairs( d ) do
diff:node( mw.html.create( "code" )
:wikitext( k, " " ) )
for i = 1, #v do
p = v[ i ]
diff:wikitext( " " )
diff:node( mw.html.create( "em" )
:wikitext( mw.text.nowiki( p[ 1 ] ) ) )
diff:wikitext( " " )
diff:node( mw.html.create( "strong" )
:wikitext( mw.text.nowiki( p[ 2 ] ) ) )
end -- for i
diff:node( mw.html.create( "br" ) )
end -- for k, v
e:node( diff )
else
e:css( "color", "#00A000" )
:css( "font-weight", "bold" )
:wikitext( I18N.fetch( "ActiveSpec" ) )
end
r:newline()
:node( e )
end
return r
end -- HTML.folder()
I18N.fetch = function ( ask )
-- Retrieve localized text
-- Precondition:
-- ask -- string, with keyword
-- Postcondition:
-- Returns string, with translation
local e = I18N.texts[ ask ]
local r
if e then
if not I18N.slang then
I18N.slang = mw.language.getContentLanguage():getCode()
:lower()
end
r = e[ I18N.slang ]
if not e then
r = e.en
end
end
return r or string.format( "I18N{%s}", ask )
end -- I18N.fetch()
OPTIONS.fair = function ( a1, a2 )
-- Compare two definitions
-- Precondition:
-- a1 -- table, with definitions
-- a2 -- table, with definitions
-- Postcondition:
-- Returns hash table, of mismatching assignments
local l, o, r, s, v1, v2
for i = 1, #OPTIONS.order do
o = OPTIONS.order[ i ]
if o.live then
s = o.sign
v1 = a1[ s ]
v2 = a2[ s ]
if v1 and v2 and v1 ~= v2 then
if type( v1 ) == "table" and
type( v2 ) == "table" then
if #v1 == #v2 then
l = false
for k = 1, #v1 do
if v1[ k ] ~= v2[ k ] then
l = true
break -- for k
end
end -- for k
if not l then
v1 = false
v2 = false
end
end
end
if v1 ~= v2 then
r = r or { }
o = { }
table.insert( o, v1 )
table.insert( o, v2 )
r[ s ] = o
end
end
end
end -- for i
return r
end -- OPTIONS.fair()
OPTIONS.first = function ()
-- Initialize gadget options configuration
-- Postcondition:
-- Configuration up to date
local e, s, u
OPTIONS.hash = { }
OPTIONS.order = { }
for i = #OPTIONS.config, 1, -1 do
e = OPTIONS.config[ i ]
if not e.live then
s = GLOBAL[ e.sign ]
if s == "1" or
s == true then
e.live = true
end
end
s = e.sign
OPTIONS.hash[ s ] = e
if e.mode then
if e.mode == 1 and
not e.lone and
e.type ~= "boolean" then
s = "-" .. e.sign
if not OPTIONS.hash[ s ] then
u = { sign = s,
live = false,
mode = 1,
type = e.type,
scan = e.scan }
table.insert( OPTIONS.order, 1, u )
OPTIONS.hash[ s ] = u
table.insert( OPTIONS.config, u )
end
end
table.insert( OPTIONS.order, 1, e )
end
end -- for i
end -- OPTIONS.first()
ROW.failed = function ( assigned, at )
-- Create line number referenced error in Gadgets-definition
-- Precondition:
-- assigned -- string, with bad data
-- at -- number, with line number
-- Postcondition:
-- Returns string, with formatted message, leading space
local r
if type( assigned ) == "string" and
type( at ) == "number" then
r = mw.html.create( "span" )
:node( ERROR.flat( assigned ) )
:wikitext( string.format( " %s ",
I18N.fetch( "AtLine" ) ) )
:node( mw.html.create( "code" )
:wikitext( string.format( "%d", at ) ) )
r = " " .. tostring( r )
end
return r or ""
end -- ROW.failed()
ROW.fetch = function ( all, access )
-- Parse line in Gadgets-definition
-- Precondition:
-- all -- table, with all line specifications
-- .hash -- table, with gadget ID mapping
-- .rows -- sequence table, with line content
-- access -- number or string, to be accessed
-- Postcondition:
-- Returns table, with line properties
local r
if type( all ) == "table" then
local errors, slot
local fault = function ( add )
errors = errors or { }
local s = add
if slot then
s = s .. ROW.failed( slot,
all.hash[ slot ] )
end
table.insert( errors, s )
end
local s = type( access )
local n
if s == "number" then
n = access
elseif s == "string" then
n = all.hash[ access ]
slot = access
end
if n then
local story = all.rows[ n ]
if type( story ) == "string" and
story ~= "" then
local i, j, o, opts, ress
story = story:gsub( "^%s*%*%s*", "" )
r = { }
if type( ROW.comment ) == "table" and
type( ROW.comment.suffix ) == "string" and
ROW.comment.suffix ~= "" then
i = story:find( ROW.comment.suffix )
if i then
if i == 1 then
r.Comment = story:sub( 2 )
else
r.Comment = story:sub( i + 1 )
story = story:sub( 1, i - 1 )
story = mw.text.trim( story )
end
r.Comment = mw.text.trim( r.Comment )
end
end
if r.Comment and
not ( ROW.comment.live or ROW.lazy ) then
fault( I18N.fetch( "NoCommentsYet" ) )
end
if not ROW.lazy then
fault( I18N.fetch( "Whitespace" ) )
end
i = story:find( "[", 1, true )
if i then
if i > 1 then
slot = mw.text.trim( story:sub( 1, i - 1 ) )
j = story:find( "]", i + 1, true )
if j then
s = mw.text.trim( story:sub( i + 1,
j - 1 ) )
opts = mw.text.split( s, "%s*|%s*" )
if #opts < 1 then
opts = false
end
story = mw.text.trim( story:sub( j + 1 ) )
else
fault( I18N.fetch( "NoClosingBracket" ) )
story = ""
end
else
slot = ""
story = ""
end
else
i = story:find( "|", 1, true )
if i then
slot = mw.text.trim( story:sub( 1, i - 1 ) )
story = mw.text.trim( story:sub( i + 1 ) )
else
slot = ""
story = ""
end
end
if slot == "" then
fault( I18N.fetch( "NoGadgetID" ) )
ress = { }
else
ress = VALUES.feed( story, "%s*|%s*" ) or { }
end
if opts then
local set, v
for i = 1, #opts do
s = mw.text.trim( opts[ i ] )
j = s:find( "=", i, true )
if j then
if j == 1 then
set = false
o = false
s = string.format( "%s %s",
ERROR.flat( slot, true ),
I18N.fetch( "NoOptionID" ) )
fault( s )
else
set = s:sub( j + 1 )
set = mw.text.trim( set )
s = s:sub( 1, j - 1 )
s = mw.text.trim( s )
o = OPTIONS.hash[ s ]
end
else
o = OPTIONS.hash[ s ]
set = false
end
if o then
if r[ o.sign ] then
s = string.format( "%s|%s",
slot,
o.sign )
s = string.format( "%s %s",
ERROR.flat( s, true ),
I18N.fetch( "RepeatedOption" ) )
fault( s )
elseif type( o.type ) == "table" then
if set then
v = VALUES.feed( set,
",",
o.type[ 1 ]
== "number" )
else
v = false
end
if type( v ) == "table" then
if o.sign == "peers" then
ROW.fill( r, v, "peers", fault )
end
else
s = string.format( "%s|%s=",
slot,
o.sign )
s = string.format( "%s %s",
s,
I18N.fetch( "MissingValue" ) )
fault( s )
end
elseif o.type == "boolean" then
if set then
v = false
s = string.format( "%s|%s=",
slot,
o.sign )
s = string.format( "%s %s",
s,
I18N.fetch( "BadAssigned" ) )
else
v = true
end
elseif o.type == "string" then
if set then
s = string.format( "%s|%s=",
slot,
o.sign )
s = string.format( "%s %s",
s,
I18N.fetch( "MissingValue" ) )
fault( s )
else
v = set
end
end
if v then
r[ o.sign ] = v
end
else
s = string.format( "%s %s",
I18N.fetch( "UnknownOptionID" ),
ERROR.flat( s, true ) )
fault( s )
end
end -- for i
end
if #ress > 0 then
ROW.fill( r, ress, false, fault )
elseif o and o.sign ~= "peers" then
fault( I18N.fetch( "NoResources" ) )
end
end
end
end
return r
end -- ROW.fetch()
ROW.fiat = function ( about )
-- Create line for Gadgets-definition
-- Precondition:
-- about -- table, with gadget options
-- Postcondition:
-- Returns string, with line
local r
if about.ID then
local o, set, sources, v
r = string.format( "* %s", about.ID )
for i = 2, #OPTIONS.order do
o = OPTIONS.order[ i ]
if o.live then
v = about[ o.sign ]
if v then
if o.mode == 1 or o.mode == 2 then
if set then
set = set .. "|"
else
set = ""
end
set = set .. o.sign
if type( o.type ) == "table" and
type( v ) == "table" then
set = set .. "="
for k = 1, #v do
if k > 1 then
set = set .. ","
end
set = set .. v[ k ]
end -- for k
end
elseif o.mode == 3 then
sources = sources or ""
for k = 1, #v do
sources = string.format( "%s|%s",
sources,
v[ k ] )
end -- for k
end
end
end
end -- for i
if set then
r = string.format( "%s[%s]", r, set )
end
if sources then
r = r .. sources
end
else
r = "*"
end
return r
end -- ROW.fiat()
ROW.fill = function ( all, assign, ancestor, fault )
-- Distribute resource pages
-- Precondition:
-- all -- table, with gadget options
-- about -- sequence table, with resource pages
-- ancestor -- "peers", or not
-- fault -- function, as error handler
-- Postcondition:
-- resource page has been added to all table
if type( all ) == "table" and
type( assign ) == "table" then
local types
local src, swap, types
if ancestor == "peers" then
types = { peers = "%.css$" }
else
types = { scripts = "%.js$",
styles = "%.css$",
datas = "%.json$" }
end
for i = 1, #assign do
src = assign[ i ]
swap = false
for k, v in pairs( types ) do
if src:find( v ) then
swap = k
end
end -- for k, v
if swap then
all[ swap ] = all[ swap ] or { }
table.insert( all[ swap ], src )
else
fault( string.format( "%s %s",
I18N.fetch( "InvalidResource" ),
ERROR.flat( src, true ) ) )
end
end -- for i
end
end -- ROW.fill()
ROW.first = function ( all, apply )
-- Convert Gadgets-definition lines into tables
-- Precondition:
-- all -- string, with all Gadgets-definition lines
-- apply -- mw.title or string, with origin
-- Postcondition:
-- Returns table, with
-- .rows -- sequence table of line strings
-- .hash -- mapping table of gadget ID to lines
-- .elts -- sequence table of heading IDs and lines
-- .origin -- mw.title or string
local r
if type( all ) == "string" then
local errors
local fault = function ( add )
errors = errors or { }
table.insert( errors, add )
end
local elts = { }
local hash = { }
local rows = mw.text.split( all, "%s*\n%s*" )
local k, s, scream
r = { origin = apply }
for i = 1, #rows do
s = mw.text.trim( rows[ i ] )
if s ~= "" then
if s:sub( 1, 2 ) == "==" then
scream = false
k, s = s:match( "^(==+)%s*([^%s=].+[^%s=])%s*%1$" )
if s then
table.insert( elts, s )
if not VALUES.fine( s, "rootpage" ) then
scream = "InvalidHeadID"
end
else
scream = "InvalidHeadline"
end
if scream then
s = I18N.fetch( scream ) .. ROW.failed( "==", i )
fault( s )
end
else
s = s:gsub( "^%s*%*%s*", "" )
k = s:find( "[%[|]" )
if k then
if k == 1 then
s = ""
else
s = mw.text.trim( s:sub( 1, k - 1 ) )
end
end
if s ~= "" then
scream = false
table.insert( elts, i )
if hash[ s ] then
scream = "RepeatedID"
else
hash[ s ] = i
if not VALUES.fine( s, "rootpage" ) then
scream = "InvalidID"
end
end
if scream then
s = I18N.fetch( scream )
.. ROW.failed( s, i )
fault( s )
end
end
end
end
end -- for i
r.rows = rows
r.elems = elts
r.hash = hash
if errors then
s = I18N.fetch( "BadLines" ) .. ROW.from( apply )
table.insert( errors, s, 1 )
for i = 1, #errors do
ERROR.fault( errors[ i ], "ROW" )
end -- for i
end
end
return r
end -- ROW.first()
ROW.found = function ( ask )
-- Retrieve single entry from active
-- Precondition:
-- ask -- string, with entry ID
local r
if ACTIVE.live and not ACTIVE.def then
ACTIVE.def = ROW.first( ACTIVE.story, ACTIVE.title )
end
if ACTIVE.def then
r = ROW.fetch( ACTIVE.def, ask )
end
return r
end -- ROW.found()
ROW.from = function ( all )
-- Format origin
-- Precondition:
-- all -- table, with Gadgets-definition representation
-- Postcondition:
-- Returns string, with hint
local r
if type( all ) == "table" then
if type( all.origin ) == "table" then
local s = all.origin.prefixedText
if type( s ) == "string" then
r = string.format( "[[%s]]", s )
else
r = "??????????"
end
else
r = "TEXT"
end
end
return r or ""
end -- ROW.from()
TEMPLATE.fill = function ( all )
-- Convert template parameters into hash
-- Precondition:
-- all -- table, with template parameters
-- Postcondition:
-- Returns table, with options
local r = { }
if type( all ) == "table" then
local errors
local fault = function ( add )
r.ERRORS = r.ERRORS or { }
table.insert( r.ERRORS, add )
end
local n, o, s, seps, vals
r = { }
for k, v in pairs( all ) do
if type( v ) == "string" then
o = OPTIONS.hash[ k ]
if o then
s = type( o.type )
if s == "string" then
s = o.type
elseif s == "table" then
s = string.format( "{%s}", o.type[ 1 ] )
else
s = false
end
if s == "boolean" then
if v == "1" then
r[ k ] = true
else
r[ k ] = false
end
elseif s == "string" then
r[ k ] = v
elseif s then
v = v:gsub( "|", "|" )
:gsub( "|", "|" )
:gsub( "|", "|" )
if o.scan == "page" then
seps = "%s*|%s*"
else
seps = "[%s,|]+"
end
vals = mw.text.split( v, seps )
for i = 1, #vals do
v = mw.text.trim( vals[ i ] )
if v == "" then
v = false
elseif s == "{number}" then
n = tonumber( v )
if n then
v = n
else
s = string.format( "%s %s",
I18N.fetch( "BadNamespace" ),
ERROR.flat( v, true ) )
v = false
end
end
if v then
r[ k ] = r[ k ] or { }
table.insert( r[ k ], v )
end
end -- for i
end
else
s = string.format( "%s %s",
I18N.fetch( "UnknownOptionID" ),
ERROR.flat( k, true ) )
fault( s )
end
else
s = string.format( "%s %s",
I18N.fetch( "BadTemplateArg" ),
ERROR.flat( k, true ) )
fault( s )
end
end -- for k, v
else
fault( I18N.fetch( "BadTemplateArgs" ) )
end
return r
end -- TEMPLATE.fill()
TEMPLATE.filter = function ( all, apart )
-- Discard ignorable template parameters
-- Precondition:
-- all -- table, with template parameters
-- apart -- string, with ignored template parameters
-- Postcondition:
-- Returns table, with template parameters
local clear = { }
local r = { }
for i = 1, #apart do
clear[ apart[ i ] ] = true
end -- for i
for k, v in pairs( all ) do
if not clear[ k ] then
r[ k ] = v
end
end -- for k, v
return r
end -- TEMPLATE.filter()
VALUES.features = function ( all, adjust, after )
-- Convert list of items
-- Precondition:
-- all -- string, with list of items
-- adjust -- string, with type of item
-- after -- string, with separator
-- Postcondition:
-- Returns table, with items, or not
local scan = mw.text.trim( all )
local suitable = adjust[ 1 ]
local r, sep, v, vals
if after == "," then
sep = "%s*,%s*"
else
sep = "[%s,|]+"
end
vals = mw.text.split( scan, sep )
for i = 1, #vals do
s = mw.text.split( vals[ i ] )
if s == "" then
v = false
elseif suitable == "number" then
v = VALUES.fine( s, "namespace" )
else
v = s
end
if v then
r = r or { }
table.insert( r, v )
end
end -- for i
return r
end -- VALUES.features()
VALUES.feed = function ( analyse, at, ask )
-- Check value
-- Precondition:
-- analyse -- string, with list
-- at -- string, with separator
-- ask -- true, requesting number elements, or not
-- Postcondition:
-- Returns table, or string with error, or nothing
local r = mw.text.split( analyse, at )
local s
for i = #r, 1, -1 do
s = mw.text.trim( r[ i ] )
if s == "" then
table.remove( r, i )
elseif ask then
if s:match( "^-?%d+$" ) then
r[ i ] = tonumber( s )
else
r = { }
break -- for i
end
end
end -- for i
if #r == 0 then
r = false
end
return r
end -- VALUES.feed()
VALUES.fine = function ( analyse, as )
-- Check value
-- Precondition:
-- analyse -- string or number, with value
-- as -- string, with key of pattern
-- Postcondition:
-- Returns true, if fine
local s = type( analyse )
local n, r
if s == "string" then
local req = VALUES.validate[ as ]
s = type( req )
if s == "table" then
local oblis = req[ 1 ]
local excls = req[ 2 ]
if type( oblis ) == "table" then
for i = 1, #oblis do
s = oblis[ i ]
if type( s ) == "string" then
r = analyse:match( string.format( "^%s$", s ) )
if r then
r = true
break -- for i
end
end
end -- for i
end
if r and type( excls ) == "table" then
for i = 1, #excls do
s = excls[ i ]
if type( s ) == "string" and
analyse:match( s ) then
r = false
break -- for i
end
end -- for i
end
elseif s == "string" then
end
if type( r ) ~= "boolean" then
ERROR.fault( "VALUES.validate: " .. as, "INTERNAL" )
elseif r and as == "namespace" then
n = tonumber( analyse )
end
elseif s == "number" then
n = analyse
end
if n then
if mw.site.namespaces[ n ] then
r = n
else
r = false
end
end
return r
end -- VALUES.fine()
VALUES.full = function ()
-- Retrieve list of permitted items
-- Postcondition:
-- tables with permitted items present
for k, v in pairs( VALUES.arrays ) do
if not VALUES.arrays[ k ] then
VALUES.arrays[ k ] = COMMONS.fetch( k ) or { }
end
end -- for k, v
end -- VALUES.full()
MediaWikiGadgetDefinition.f = function ( arglist, frame )
-- Perform standard functionality
-- Precondition:
-- arglist -- table, with parameters
-- frame -- frame object, or not
-- Postcondition:
-- Returns mw.html, or not
local r, s
GLOBAL.frame( frame )
if type( arglist.id ) == "string" then
GLOBAL.id = mw.text.trim( arglist.id )
end
if type( arglist.template ) == "table" then
GLOBAL.template = arglist.template
if type( GLOBAL.template.ID ) == "string" then
GLOBAL.id = mw.text.trim( GLOBAL.template.ID )
end
if type( arglist.IGNORE ) == "table" then
GLOBAL.template = TEMPLATE.filter( GLOBAL.template,
arglist.IGNORE )
end
end
if GLOBAL.id == "" or
GLOBAL.id == "-" then
GLOBAL.id = false
end
s = arglist.use
if type( s ) == "string" then
s = mw.text.trim( s )
if s ~= "" and
s ~= "-" then
ACTIVE.story = s
GLOBAL.scope = "text"
if not GLOBAL.id then
GLOBAL.id = "*"
end
end
end
if GLOBAL.id then
local tbl
if type( arglist.strictRows ) == "boolean" then
ROWS.lazy = arglist.strictRows
end
if type( arglist.Build ) == "string" then
s = arglist.Build:upper()
if s == "JSON" or s == "ROWS" then
GLOBAL.syntax = s
end
end
GLOBAL.syntax = GLOBAL.syntax or "HTML"
if not GLOBAL.scope and
type( arglist.scope ) == "string" then
s = mw.text.trim( arglist.scope )
if s == "site" or
s == "global" then
GLOBAL.scope = s
elseif s == "" then
GLOBAL.scope = "site"
else
if s:find( ":", 1, true ) then
local ns
ns, GLOBAL.scope = GLOBAL.focus( s )
end
end
if not GLOBAL.scope then
ERROR.fault( I18N.fetch( "InvalidScope" ), "#invoke" )
end
end
GLOBAL.scope = GLOBAL.scope or "site"
if arglist.Export and
GLOBAL.scope == "site" and
GLOBAL.syntax == "HTML" then
HTML.export = true
end
GLOBAL.first()
if GLOBAL.scope ~= "global" then
ACTIVE.first()
end
if GLOBAL.template then
tbl = TEMPLATE.fill( GLOBAL.template )
r = HTML.folder( tbl )
end
else
ERROR.fault( I18N.fetch( "IDmissing" ), "#invoke" )
end
return r
end -- .f()
MediaWikiGadgetDefinition.params = function ()
-- Communicate possible template parameters
-- Postcondition:
-- Returns mw.html <ul>
local r = mw.html.create( "ul" )
local e, o
OPTIONS.first()
for i = 2, #OPTIONS.order do
o = OPTIONS.order[ i ]
e = mw.html.create( "li" )
:wikitext( o.sign )
if not o.live then
e:css( "text-decoration", "line-through" )
end
r:newline()
:node( e )
end -- for i
return r
end -- .params()
MediaWikiGadgetDefinition.suggestedvalues = function ( array )
local r
if type( VALUES.arrays[ array ] ) == "boolean" then
local vals = COMMONS.fetch( array )
if type( vals ) == "table" then
local sep = ""
local e
for i = 1, #vals do
e = vals[ i ]
if e[ 2 ] then
r = string.format( "%s%s\"%s\"",
r or "",
sep,
e[ 1 ] )
sep = ", "
end
end -- for i
end
end
r = r or "ERROR"
return string.format( "[ %s ]", r )
end -- .suggestedvalues()
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
-- 2024-03-01
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
elseif link then
r = false
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()
-- Export
local p = { }
p.f = function ( frame )
local params = { }
if frame.args.TEMPLATE == "1" then
params.template = frame:getParent().args
if frame.args.IGNORE and
frame.args.IGNORE ~="" then
params.IGNORE = mw.text.split( frame.args.IGNORE,
"%s*|%s*" )
end
else
if frame.args.Use and
frame.args.Use ~="" and
frame.args.Use ~="-" then
params.use = frame.args.Use
else
if frame.args.ExportLink == 1 then
params.export = true
end
params.scope = frame.args.Scope
end
end
if frame.args.StrictRows == "1" then
params.strictRows = true
end
if frame.args.ID and
frame.args.ID ~= "" and
frame.args.ID ~= "-" then
params.id = frame.args.ID
end
if frame.args.Build and
frame.args.Build ~="" then
params.build = frame.args.Build
end
return MediaWikiGadgetDefinition.f( params, frame ) or ""
end -- p.f
p.params = function ( frame )
return MediaWikiGadgetDefinition.params()
end -- p.params
p.suggestedvalues = function ( frame )
local s = frame.args[ 1 ]
if s then
s = mw.text.trim( s )
if s == "" then
s = false
end
end
return MediaWikiGadgetDefinition.suggestedvalues( s )
end -- p.suggestedvalues
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
setmetatable( p, { __call = function ( func, ... )
setmetatable( p, nil )
return Failsafe
end } )
return p