Modul:UserGroups
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:
2022-12-12
local UserGroups = { suite = "userGroups",
serial = "2022-12-12",
setup = ".json",
item = 115663308 }
--[=[
Administration of users with special properties like sysop or bureaucrat
]=]
local Failsafe = UserGroups
UserGroups.I18N = {
aliasFormer = { en = "previously" },
aliasLater = { en = "later" },
dataSource = { en = "Data source" },
entryCount = { en = "$1 entries" },
errAccountBad = { en = "Invalid account: $1" },
errAccountDuplicate = { en = "Duplicated account: $1" },
errAccountEmpty = { en = "Empty account name detected" },
errAccountUntrimmed = { en = "Untrimmed account: $1" },
errDataBad = { en = "Invalid data: $1" },
errDataGroup = { en = "Invalid groups: $1" },
errDuplicatingRedir = { en = "Duplicated renaming: $1" },
errLinkTarget = { en = "Bad link target: $1" },
errNoData = { en = "No data to process" },
errPingingModule = { en = "No Pinging module" },
errUnknownType = { en = "Unknown result type: $1" },
THaccount = { en = "Account" },
THcode = { en = "Group" },
THcodes = { en = "Groups" },
THdetails = { en = "Details" },
THfrom = { en = "From" },
THgender = { en = mw.ustring.char( 0x2640, 0x2642 ) },
THinfo = { en = "Info" },
THsince = { en = "Since" },
THtill = { en = "Till" }
}
UserGroups.r =
{ gender = { },
graph = { },
Lua = { },
number = { },
ol = { },
ping = { },
plain = { },
raw = { },
table = { died = "†" },
target = { },
timeline = { seek = "<pre id=\"usergroups%-timeline\"[^>]*>",
stamp = "dd/mm/yyyy" },
ul = { }
}
UserGroups.DateTime = { }
UserGroups.Remark = { max = 1000000 }
local function factory( apply, about )
-- Localization of messages
-- apply -- string, with message key
-- about -- string, with explanation, or not
-- Returns message text; at least english
local entry = UserGroups.I18N[ apply ]
local r
if entry then
r = entry[ mw.language.getContentLanguage():getCode() ]
if not r then
r = entry.en
end
if about and r:find( "$1", 1, true ) then
r = r:gsub( "%$1", mw.text.nowiki( about ) )
end
else
r = string.format( "????NoMessage@%s %s????",
UserGroups.suite, apply )
end
return r
end -- factory()
local function fair( account )
-- Normalize a user nick
-- account -- string, with nick
-- Returns string
local r = mw.text.decode( account )
r = r:gsub( "_", " " )
:gsub( "‎", "" )
:gsub( "‏", "" )
:gsub( mw.ustring.char( 0xA0 ), " " )
:gsub( mw.ustring.char( 0x200E ), "" )
:gsub( mw.ustring.char( 0x200F ), "" )
:gsub( "%s%s+", " " )
r = mw.text.trim( r )
return r
end -- fair()
local function familiar( adapt )
-- Poor man’s sort normalization for names
-- adapt -- string
-- Returns string
local r = mw.ustring.gsub( adapt, "ß" , "ss" )
r = mw.ustring.gsub( r, "ä" , "ae" )
r = mw.ustring.gsub( r, "ö" , "oe" )
r = mw.ustring.gsub( r, "ü" , "ue" )
r = mw.ustring.gsub( r, "Ä" , "Ae" )
r = mw.ustring.gsub( r, "Ö" , "Oe" )
r = mw.ustring.gsub( r, "Ü" , "Ue" )
r = mw.ustring.gsub( r, "é" , "e" )
r = mw.ustring.gsub( r, "ó" , "o" )
r = mw.ustring.gsub( r, "ú" , "u" )
return r
end -- familiar()
local function fault( alert )
-- Format message with class="error"
-- alert -- string, with message
-- Returns mw.html
local r = mw.html.create( "span" )
:addClass( "error" )
if type( alert ) == "string" and alert ~= "" then
r:wikitext( alert )
else
r:wikitext( string.format( "????fault@%s????",
UserGroups.suite ) )
end
return r
end -- fault()
local function feed( apply )
-- Clone table without metatable
-- apply -- table
-- Returns table
local r
if type( apply ) == "table" then
r = { }
for k, v in pairs( apply ) do
if type( v ) == "table" then
v = feed( v )
end
r[ k ] = v
end -- for k, v
else
r = apply
end
return r
end -- feed()
local function female( alike )
-- Retrieve gender mark
-- alike -- string, with word, or not
-- Returns string, or not
local r
if type( alike ) == "string" then
r = mw.text.trim( alike )
r = r:sub( 1, 1 )
:lower()
if r == "f" then
r = 0x2640
elseif r == "m" then
r = 0x2642
else
r = false
end
if r then
r = mw.ustring.char( r )
end
end
return r
end -- female()
local function fence( at )
-- Check whether this is a live date
-- at -- table, with entry
-- Returns
-- 1 -- table, with improved entry, or false
-- 2 -- boolean
local s = type( at )
local r1, r2
if s == "string" then
r1 = { date = at }
elseif s == "table" then
r1 = at
else
r1 = false
r2 = true
end
if r1 then
if type( r1.date ) == "string" then
s = mw.text.trim( r1.date )
if s == "" then
r1.date = false
r2 = true
elseif s == "-" or s == true then
r2 = false
else
local DateTime = UserGroups.DateTime.fetch()
if DateTime then
local date
if type( r1.sort ) == "string" then
date = r1.sort
else
date = r1.date
end
date = DateTime( date )
if date then
if not UserGroups.today then
UserGroups.today = DateTime()
end
r2 = ( date >= UserGroups.today )
else
r1.date = false
r2 = true
end
else
r2 = false
end
end
elseif r1.date == true then
r2 = false
else
r1.date = false
r2 = true
end
elseif at == true then
r2 = false
else
r2 = true
end
return r1, r2
end -- fence()
local function fields( accept )
-- Count elements without metatable
-- accept -- table
-- Returns number
local r
if type( accept ) == "table" then
r = 0
for k, v in pairs( accept ) do
if type( k ) == "number" and k > r then
r = k
end
end -- for k, v
end
return r
end -- fields()
local function fill( ask, account, assign )
-- Analyze and normalize one user entry
-- ask -- table, with current request
-- .sole -- single account
-- .subset -- account pattern
-- .codes -- groups
-- .live -- active
-- .state -- member|ping
-- account -- string, with validated user nick
-- assign -- table, with entry
-- Returns
-- 1 -- table, with processed entry, or not
-- 2 -- string, with localized error, or not
-- 3 -- string, with renamed account, or not
local e, r1, r2, r3
r1 = { sign = account }
if type( assign ) == "string" then
e = { code = assign }
else
e = assign
end
if type( e ) == "table" then
if fields( e ) > 0 then
r1.groups = feed( e )
e = false
elseif type( e.code ) == "string" then
r1.groups = { }
table.insert( r1.groups, feed( e ) )
e = false
elseif type( e.groups ) == "table" then
if fields( e.groups ) == 0 then
if type( e.groups.code ) == "string" then
r1.groups = { }
table.insert( r1.groups, feed( e.groups ) )
else
r2 = factory( "errDataGroup", account )
r1 = false
end
else
r1.groups = feed( e.groups )
end
elseif type( e.groups ) == "string" then
r1.groups = { }
table.insert( r1.groups, e.groups )
elseif type( e.renamed ) == "string" then
r3 = e.renamed
r1 = false
else
r2 = factory( "errDataBad", account )
r1 = false
end
if r1 then
if e then
for k, v in pairs( e ) do
if k ~= "groups" then
r1[ k ] = feed( v )
end
end -- for k, v
end
if r1.groups then
local g, s
for k, v in pairs( r1.groups ) do
s = type( v )
if s == "string" then
r1.groups[ k ] = { code = v }
elseif s ~= "table" then
r2 = factory( "errDataGroup", account )
r1 = false
end
end -- for k, v
if r1 and ask.limit then
for i = #r1.groups, 1, -1 do
g = r1.groups[ i ]
if not ask.codes.bunch[ g.code ] then
table.remove( r1.groups, i )
end
end -- for i
if #r1.groups == 0 then
r1 = false
end
end
if r1 and type( ask.live ) == "boolean" then
local live
for i = #r1.groups, 1, -1 do
g = r1.groups[ i ]
s = type( g.till )
if s == "string" then
g.till = mw.text.trim( g.till )
if g.till == "" then
g.till = false
else
g.till = { date = g.till }
end
elseif s == "table" then
if g.till.date == "" then
g.till = false
end
elseif g.till ~= true then
g.till = false
end
if g.till then
g.till, live = fence( g.till )
else
live = true
end
r1.groups[ i ].till = g.till
if live == ask.live then
r1.groups[ i ].live = live
else
table.remove( r1.groups, i )
end
end -- for i
if #r1.groups == 0 then
r1 = false
end
end
if r1 and ask.codes and ask.codes.exclude then
for k, v in pairs( ask.codes.exclude ) do
for i = 1, #r1.groups do
g = r1.groups[ i ]
if g.code == k then
r1 = false
break -- for i
end
end -- for i
if not r1 then
break -- for k, v
end
end -- for k, v
end
if r1 and ask.codes then
e = true
for i = #r1.groups, 1, -1 do
g = r1.groups[ i ]
for k, v in pairs( ask.codes.bunch ) do
if g.code == k then
if ask.live == false and
g.live then
r1 = false
end
e = false
break -- for k, v
end
end -- for k, v
if not r1 then
break -- for i
end
end -- for i
if r1 and
#r1.groups == 0 or e then
r1 = false
end
end
if r1 and ask.codes and ask.codes.law then
local bunch = { }
for k, v in pairs( ask.codes.bunch ) do
bunch[ k ] = true
end -- for k, v
for k, v in pairs( ask.codes.bunch ) do
for i = 1, #r1.groups do
if r1.groups[ i ].code == k then
bunch[ k ] = false
end
end -- for i
end -- for k, v
for k, v in pairs( bunch ) do
if v then
r1 = false
break -- for v
end
end -- for k, v
end
if r1 then
for i = 1, #r1.groups do
g = r1.groups[ i ]
if g.state == "" or
g.state == "member" then
g.state = false
end
if ask.state == "member" and g.state then
table.remove( r1.groups, 1 )
end
end -- for i
if #r1.groups == 0 then
r1 = false
end
end
elseif not r3 then
r2 = factory( "errNoGroup", account )
r1 = false
end
end
else
r2 = factory( "errDataBad", account )
r1 = false
end
return r1, r2, r3
end -- fill()
local function filling( ask, account, assign )
-- Analyze and normalize one user nick and entry
-- ask -- table, with current request
-- account -- string, with user nick
-- assign -- table, with entry
-- Returns
-- 1 -- table, with processed entry, or not
-- 2 -- string, with localized error, or not
local s = type( account )
local r1, r2, r3
if s == "string" then
s = fair( account )
if s == "" then
r2 = factory( "errAccountEmpty" )
elseif s == account then
if s:find( "[@/|:{}%[%]<>]" ) then
r2 = factory( "errAccountBad", s )
else
r1, r2, r3 = fill( ask, s, assign )
end
else
r2 = factory( "errAccountUntrimmed", s )
end
else
s = string.format( "%s: %s", s, tostring( account ) )
r2 = factory( "errAccountBad", s )
end
return r1, r2, r3
end -- filling()
local function filter( accounts, arglist )
-- Narrow data to current request
-- accounts -- table, with data
-- arglist -- table, with current request
-- Returns
-- 1 -- table, with processed data map, or not
-- 2 -- table, with errors, or not
-- 3 -- sequence table, with sort order, or not
local query = { }
local entry, err, p, r1, r2, r3, renamed, s
if type( arglist.sole ) == "string" then
query.sole = arglist.sole
elseif type( arglist.subset ) == "string" then
query.subset = arglist.subset
end
if arglist.scheme == "ping" or arglist.scheme == "target" then
arglist.live = true
end
if type( arglist.live ) == "boolean" then
query.live = arglist.live
if not query.live and
type( arglist.codes ) == "table" and
#arglist.codes ~= 1 then
arglist.codes = false
end
end
if type( arglist.codes ) == "table" then
local n = 0
local q = { }
for i = 1, #arglist.codes do
s = arglist.codes[ i ]
if type( s ) == "string" then
s = mw.text.trim( s )
if s == "&" then
q.law = query.live
elseif s ~= "" and s ~= "-" then
if s:sub( 1, 1 ) == "-" then
if query.live then
s = s:sub( 2 )
q.exclude = q.exclude or { }
q.exclude[ s ] = true
end
else
n = n + 1
end
q.bunch = q.bunch or { }
q.bunch[ s ] = true
end
end
end -- for i
if q.bunch then
if q.law and n < 2 then
q.law = false
end
query.codes = q
end
else
arglist.codes = false
end
if arglist.codes and
query.codes and
arglist.scheme ~= "table" then
query.limit = true
end
if arglist.scheme == "ping" then
query.state = "ping"
else
query.state = "member"
end
for k, v in pairs( accounts ) do
if query.sole then
if k ~= query.sole then
k = false
end
elseif query.subset then
if not mw.ustring.match( k, query.subset ) then
k = false
end
end
if k then
if type( k ) == "number" then
k = tostring( k )
end
entry, err, p = filling( query, k, v )
if err then
r2 = r2 or { }
table.insert( r2, err )
r1 = false
elseif p then
renamed = renamed or { }
table.insert( renamed,
{ [1] = k,
[2] = p } )
end
if entry and not r2 then
r1 = r1 or { }
s = mw.ustring.sub( entry.sign, 1, 1 )
s = mw.ustring.upper( s ) ..
mw.ustring.sub( entry.sign, 2 )
if r1[ entry.sign ] or r1[ s ] then
r2 = { }
table.insert( r2,
factory( "errAccountDuplicate",
entry.sign ) )
r1 = false
else
r1[ entry.sign ] = entry
end
end
end
end -- for k, v
if r1 then
local f = function ( a1, a2 )
return a1.s < a2.s
end -- f()
local o = { }
if renamed then
for k, v in pairs( renamed ) do
if r1[ v[ 2 ] ] then
if r1[ v[ 1 ] ] then
r2 = { }
table.insert( r2,
factory( "errDuplicatingRedir",
v[ 1 ] ) )
elseif arglist.scheme == "table" and
not arglist.live then
r1[ v[ 1 ] ] = { shift = v[ 2 ],
sign = v[ 1 ] }
end
end
end -- for k, v
end
for k, v in pairs( r1 ) do
s = familiar( k )
s = string.format( "%s |%s", mw.ustring.upper( k ), k )
table.insert( o,
{ a = k,
s = s } )
end -- for k, v
table.sort( o, f )
r3 = { }
for i = 1, #o do
table.insert( r3, o[ i ].a )
end -- for i
end
return r1, r2, r3
end -- filter()
local function first()
-- Initialize configuration
if not UserGroups.config then
local s = string.format( "Module:%s%s",
UserGroups.suite, UserGroups.setup )
local lucky, json = pcall( mw.loadJsonData, s )
if type( json ) ~= "table" and
type( UserGroups.item ) == "number" and
UserGroups.item > 0 then
s = string.format( "Q%d", UserGroups.item )
s = mw.wikibase.getSitelink( s )
if type( s ) == "string" then
s = string.format( "%s%s", s, UserGroups.setup )
lucky, json = pcall( mw.loadJsonData, s )
end
end
if type( json ) == "table" then
UserGroups.config = feed( json )
if type( UserGroups.config.i18n ) == "table" then
local trsl
for k, v in pairs( UserGroups.config.i18n ) do
trsl = UserGroups.I18N[ k ]
if type( trsl ) == "table" and
type( v ) == "table" then
for slang, s in pairs( v ) do
if type( s ) == "string" and
mw.text.trim( s ) ~= "" then
UserGroups.I18N[ k ][ slang ] = s
end
end -- for slang, s
end
end -- for k, v
end
else
UserGroups.config = { }
end
end
end -- first()
local function furnish( adjust, add, assign )
-- Assign attributes to element
-- adjust -- mw.html, or not
-- add -- table, string, with class, or not
-- assign -- table, string, with , or not
if type( adjust ) == "table" and
type( adjust.create ) == "function" then
local s
if add then
s = type( add )
if s == "string" then
adjust:addClass( add )
elseif s == "table" then
for k, v in pairs( add ) do
if type( v ) == "string"
and v ~= "" then
adjust:addClass( v )
end
end -- for k, v
end
end
if assign then
s = type( assign )
if s == "string" then
adjust:cssText( assign )
elseif s == "table" then
adjust:css( assign )
end
end
end
end -- furnish()
UserGroups.f = function ( arglist )
-- Major productive function
-- arglist -- table, with current request
-- Returns mw.html
local data, lucky, params, r
first()
if type( arglist ) == "table" then
params = arglist
else
params = { }
end
if params.json then
lucky, data = pcall( mw.text.jsonDecode, params.json )
elseif type( params.data ) == "table" then
data = params.data
lucky = true
else
local source
if type( params.source ) == "string" then
source = params.source
elseif type( UserGroups.config.source ) == "string" then
source = UserGroups.config.source
params.source = source
end
if source then
lucky, data = pcall( mw.loadJsonData, source )
end
end
if lucky then
if type( data.accounts ) == "table" then
local explain = data.codes
local err, order
data, err, order = filter( data.accounts, params )
if data and not err then
params.scheme = params.scheme or "table"
r = UserGroups.r[ params.scheme ]
if type( r ) == "table" and
type( r.format ) == "function" then
r, err = r.format( data, order, params, explain )
else
err = { }
table.insert( err,
factory( "errUnknownType",
params.scheme ) )
end
if not err and
type( r ) == "table" and
params.scheme ~= "Lua" then
furnish( r,
UserGroups.config.class,
UserGroups.config.style )
furnish( r, params.class, params.style )
if type( params.id ) == "string" and
params.id ~= "" then
r:attr( "id", params.id )
end
end
end
if err then
local li
r = mw.html.create( "ul" )
for i = 1, #err do
s = err[ i ]
li = mw.html.create( "li" )
li:wikitext( err[ i ] )
r:newline()
:node( li )
end -- for i
end
else
r = fault( factory( "errNoData" ) )
end
else
if not data then
data = factory( "errNoData" )
end
r = fault( data )
end
return r or false
end -- UserGroups.f()
UserGroups.r.gender.format = function ( accounts, along, arglist )
-- Retrieve gender of account
-- accounts -- table, with data
-- along -- table, with order
-- arglist -- table, with current request
-- .account
-- Returns
-- 1 -- "f", "m", "-", false
local r
if type( arglist ) == "table" and
type( arglist.account ) == "string" then
local e = accounts[ arglist.account ]
if type( e ) == "table" then
if type( e.gender ) == "string" then
local s = mw.text.trim( e.gender ):sub( 1, 1 ):lower()
if s == "f" or s == "m" then
r = s
end
end
r = r or "-"
end
end
return r or false
end -- UserGroups.r.gender.format()
UserGroups.r.Lua.format = function ( accounts, along, arglist )
-- Format data as Lua sequence table of strings
-- accounts -- table, with data
-- along -- table, with order
-- arglist -- table, with current request
-- Returns
-- 1 -- sequence table
return along
end -- UserGroups.r.Lua.format()
UserGroups.r.number.format = function ( accounts, along, arglist )
-- Retrieve number of accounts
-- accounts -- table, with data
-- along -- table, with order
-- arglist -- table, with current request
-- Returns
-- 1 -- number
return #along
end -- UserGroups.r.number.format()
UserGroups.r.ol.format = function ( accounts, along, arglist )
-- Format data as <ol>
-- accounts -- table, with data
-- along -- table, with order
-- arglist -- table, with current request
-- Returns
-- 1 -- mw.html.ol
return UserGroups.r.ul.format( accounts, along, arglist, nil, true )
end -- UserGroups.r.ol.format()
UserGroups.r.ping.format = function ( accounts, along, arglist )
-- Format data as ping list
-- accounts -- table, with data
-- along -- table, with order
-- arglist -- table, with current request
-- Returns
-- 1 -- mw.html.span
-- 2 -- table, with errors, or not
local r1, r2
if not UserGroups.Pinging then
local lucky, Pinging = pcall( require, "Module:Pinging" )
if type( Pinging ) == "table" then
UserGroups.Pinging = Pinging()
else
UserGroups.Pinging = true
end
end
if type( UserGroups.Pinging ) == "table" and
type( UserGroups.Pinging.f ) == "function" then
local cnf, light, show
if type( arglist ) == "table" and
type( arglist.ping ) == "table" then
cnf = arglist.ping
light = cnf.light
show = cnf.show
end
r1 = mw.html.create( "span" )
r1:wikitext( UserGroups.Pinging.f( along,
false,
light,
cnf,
false,
show ) )
else
r2 = { }
table.insert( r2, factory( "errPingingModule" ) )
end
return r1, r2
end -- UserGroups.r.ping.format()
UserGroups.r.plain.format = function ( accounts, along, arglist )
-- Format data as plain text
-- accounts -- table, with data
-- along -- table, with order
-- arglist -- table, with current request
-- Returns
-- 1 -- mw.html.div
local r1 = mw.html.create( "div" )
local s = UserGroups.r.raw.format( accounts, along, arglist )
if s then
local pre = { style = "white-space: nowrap" }
UserGroups.frame = UserGroups.frame or mw.getCurrentFrame()
pre[ 1 ] = s
r1:newline()
:wikitext( UserGroups.frame:extensionTag( "pre", pre ) )
:newline()
end
return r1
end -- UserGroups.r.plain.format()
UserGroups.r.raw.format = function ( accounts, along, arglist )
-- Format data as raw text, \n separated
-- accounts -- table, with data
-- along -- table, with order
-- arglist -- table, with current request
-- Returns
-- 1 -- string, or nil
local r1, s
for i = 1, #along do
s = along[ i ]
if r1 then
r1 = string.format( "%s\n%s", r1, s )
else
r1 = s
end
end -- for i
return r1
end -- UserGroups.r.raw.format()
UserGroups.r.table.factory = function ( account, apply )
-- Process account through external
-- account -- table, with user data
-- apply -- string, with processing scheme
-- Returns string, with wikitext
local r
if type( UserGroups.config.process ) == "table" then
local def = UserGroups.config.process[ apply ]
if type( def ) == "table" and
type( def.transclude ) == "string" and
def.nick then
local p = { }
UserGroups.frame = UserGroups.frame or mw.getCurrentFrame()
p[ def.nick ] = account.sign
if def.groups then
for i = 1, #account.groups do
p[ account.groups[ i ].code ] = "1"
end -- for i
end
r = UserGroups.frame:expandTemplate{ title = def.transclude,
args = p }
end
end
return r
end -- UserGroups.r.table.factory()
UserGroups.r.table.feature = function ( above )
-- Label table head cell
-- above -- string, with code
-- Returns string, with wikitext
local s = above
local l = ( s:sub( 1, 1 ) == "#" )
local h, r
if l then
s = s:sub( 2 )
end
h = mw.text.split( s, "#" )
s = h[ 1 ]
if l then
if s == "groups" then
s = h[ 2 ]
end
r = factory( "TH" .. s )
else
if mw.ustring.find( s, "%A" ) then
local e = mw.html.create( "span" )
e:css( "white-space", "nowrap" )
:wikitext( s )
s = tostring( e )
end
r = s
end
return r
end -- UserGroups.r.table.feature()
UserGroups.r.table.fiat = function ( at, assign )
-- Assign content in cell to row
-- at -- mw.html.tr
-- assign -- table or string, with something
local td = mw.html.create( "td" )
local s = type( assign )
if s == "string" and assign ~= "" then
s = assign
elseif s == "table" then
s = assign.content
if s and
type( assign.attr ) == "table" and
#assign.attr > 0 then
local e
for i = 1, #assign.attr do
e = assign.attr[ i ]
if type( e.key ) == "string" and
type( e.value ) == "string" then
td:attr( e.key, e.value )
end
end -- for i
end
else
s = false
end
if s and s ~= true then
td:wikitext( s )
end
at:node( td )
:newline()
end -- UserGroups.r.table.fiat()
UserGroups.r.table.fill = function ( at, account, accounts, arglist )
-- Content for entire table data column
-- at -- number, of column
-- account -- string, with nick
-- accounts -- table, with data
-- arglist -- table, with current request
-- Returns table, with
-- [1] -- wikitext or table of first row
-- [2] -- wikitext or table of second row, or not
-- [*] -- wikitext or table of third etc. row, or not
local stack = arglist.table.columns[ at ]
local entry = accounts[ account ]
local r = { }
local k, s, sub
if stack:sub( 1, 1 ) == "#" then
s = stack:sub( 2 )
k = s:find( "#", 2, true )
if k then
sub = s:sub( k + 1 )
s = s:sub( 1, k - 1 )
end
if s == "account" then
local opts = { }
if sub then
local o = mw.text.split( sub, "#" )
for i = 1, #o do
s = o[ i ]
k = s:find( ":", 2, true )
if k == 8 and s:sub( 1, 7 ) == "process" then
opts.process = s:sub( 9 )
else
opts[ s ] = true
end
end -- for i
end
s = false
if opts.process then
s = UserGroups.r.table.factory( entry, opts.process )
end
if not s then
s = string.format( "[[user:%s|%s]]", account, account )
end
if opts.gender then
k = female( entry.gender )
if k then
s = string.format( "%s %s", s, k )
end
end
if opts.died and entry.died then
local e = mw.html.create( "span" )
e:css( "vertical-align", "super" )
:css( "margin-left", "0.2em" )
:css( "margin-right", "0.2em" )
:wikitext( UserGroups.r.table.died )
s = s .. tostring( e )
end
if opts.alias and entry.alias then
local x = { }
local e
if type( entry.alias ) == "string" then
table.insert( x, { account = entry.alias } )
elseif type( entry.alias ) == "table" then
if #entry.alias > 0 then
for i = 1, #entry.alias do
e = entry.alias[ i ]
if type( e ) == "string" then
table.insert( x,
{ account = e } )
elseif type( e ) == "table" then
table.insert( x, e )
end
end -- for i
else
table.insert( x, entry.alias )
end
end
if #x > 0 then
local history = mw.html.create( "div" )
local div, h, same, sign
for i = 1, #x do
h = x[ i ]
div = mw.html.create( "div" )
if h.later then
same = "aliasLater"
else
same = "aliasFormer"
end
e = mw.html.create( "span" )
e:css( "font-style", "italic" )
:wikitext( factory( same ) .. " " )
div:newline()
:node( e )
:wikitext( " " )
if type( h.account ) == "string" then
sign = mw.text.trim( h.account )
if sign == "" then
sign = false
end
else
sign = false
end
if sign then
if h.link == true then
sign = string.format( "[[user:%s|%s]]",
sign, sign )
end
else
sign = "?????"
end
div:wikitext( sign )
if h.suffix then
local post = UserGroups.Remark.feed(
h.suffix )
if post then
div:node( post )
end
end
history:newline()
:node( div )
:newline()
end -- for i
s = s .. tostring( history )
end
end
if type( entry.groups ) == "table" then
if arglist.table.large then
k = #entry.groups
else
k = 1
end
if k == 1 then
table.insert( r, s )
else
local o = { content = tostring( s ),
attr = { } }
table.insert( o.attr,
{ key = "rowspan",
value= tostring( k ) } )
table.insert( r, o )
s = false
for i = 1, k - 1 do
table.insert( r, false )
end -- for i
end
end
elseif s == "codes" then
if entry.shift then
s = UserGroups.r.table.forward( entry.shift,
accounts,
arglist )
else
local o = UserGroups.r.table.focus( entry.groups )
local sign
s = false
for i = 1, #o do
sign = o[ i ]
if mw.ustring.find( sign, "%A" ) then
local e = mw.html.create( "span" )
e:css( "white-space", "nowrap" )
:wikitext( sign )
sign = tostring( e )
end
if s then
s = string.format( "%s %s", s, sign )
else
s = sign
end
end -- for i
end
elseif s == "details" then
s = entry.details
elseif s == "gender" then
s = female( entry.gender ) or true
if sub == "preference" then
UserGroups.cLang = UserGroups.cLang or
mw.language.getContentLanguage()
sub = UserGroups.cLang:gender( account, "m", "f", "-" )
if sub ~= "-" then
if s == true then
s = sub
else
s = string.format( "%s %s",
tostring( s ),
sub )
end
end
end
elseif s == "groups" then
if entry.shift then
if at == 2 then
s = UserGroups.r.table.forward( entry.shift,
accounts,
arglist )
else
s = false
end
else
local codes, e
if sub == "code#see" then
sub = "code"
codes = UserGroups.config.codes
else
sub = sub or "code"
end
for i = 1, #entry.groups do
e = entry.groups[ i ]
s = e[ sub ]
if sub == "code" then
if mw.ustring.find( s, "%A" ) then
local html = mw.html.create( "span" )
html:css( "white-space", "nowrap" )
:wikitext( s )
s = tostring( html )
end
if codes then
local group = codes[ e.code ]
if type( group ) == "table" and
type( group.see ) == "string" then
s = string.format( "[[%s|%s]]",
group.see, s )
end
end
elseif sub == "from" or
sub == "since" or
sub == "till" then
s = UserGroups.r.table.fromto( s )
elseif sub == "info" then
end
table.insert( r, s or true )
end -- for i
if sub == "code" and #r > 1 then
local n = 1
for i = #r, 1, -1 do
if i == 1 then
s = true
else
s = r[ i - 1 ]
end
if r[ i ] == s then
r[ i ] = false
n = n + 1
elseif n > 1 then
e = { content = r[ i ],
attr = { } }
table.insert( e.attr,
{ key = "rowspan",
value= tostring( n ) } )
r[ i ] = e
r[ i + 1 ] = false
n = 1
end
end -- for i
end
s = true
end
else
s = true
end
if s then
table.insert( r, s )
end
elseif entry.groups then
local g = entry.groups
local e, v
s = stack
k = s:find( "#", 2, true )
if k then
sub = s:sub( k + 1 )
s = s:sub( 1, k - 1 )
end
sub = sub or "1"
for i = 1, #g do
e = g[ i ]
if e.code == s then
if sub == "1" then
v = { content = "X",
attr = { } }
table.insert( v.attr,
{ key = "style",
value = "font-weight:bold;"
.. "text-align:center;" } )
table.insert( v.attr,
{ key = "title",
value = e.code } )
elseif sub == "from" or
sub == "since" or
sub == "till" then
v = UserGroups.r.table.fromto( e[ sub ] )
elseif sub == "info" then
v = e.info
if e.suffix then
local post = UserGroups.Remark.feed( e.suffix )
if post then
v = { content = v .. tostring( post ),
attr = { } }
table.insert( v.attr,
{ key = "data-sort-value",
value = e.info } )
end
end
end
break -- for i
end
end -- for i
table.insert( r, v or true )
end
return r
end -- UserGroups.r.table.fill()
UserGroups.r.table.focus = function ( assigned )
-- Collect group identifiers of an account
-- assigned -- table, with user group data
-- Returns table, with sorted unique group identifiers
local r = { }
if #assigned == 1 then
table.insert( r, assigned[ 1 ].code )
else
r = { }
for i = 1, #assigned do
table.insert( r, assigned[ i ].code )
end -- for i
table.sort( r, UserGroups.r.table.follows )
for i = #r, 2, -1 do
if r[ i ] == r[ i - 1 ] then
table.remove( r, i )
end
end -- for i
end
return r
end -- UserGroups.r.table.focus()
UserGroups.r.table.follows = function ( a1, a2 )
-- Group ranking sort order
-- a1 -- string, with group code
-- a2 -- string, with group code
-- Returns true, if a1 < a2
local r
if UserGroups.r.table.order then
local k1 = UserGroups.r.table.order[ a1 ]
local k2 = UserGroups.r.table.order[ a2 ]
if k1 then
if k2 then
r = ( k1 < k2 )
else
r = true
end
elseif k2 then
r = false
else
r = ( a1 < a2 )
end
else
r = ( a1 < a2 )
end
return r
end -- UserGroups.r.table.follows()
UserGroups.r.table.format = function ( accounts,
along,
arglist,
about )
-- Format data as table
-- accounts -- table, with data
-- along -- table, with order
-- arglist -- table, with current request
-- about -- table, with codes for legend and order, or not
-- Returns
-- 1 -- mw.html.table / mw.html.div
-- 2 -- table, with errors, or not
local r1 = mw.html.create( "table" )
:addClass( "wikitable" )
local config = UserGroups.config
local multi = 0
local c, caption, columns, e, lead, n, order, r2, s, td, th, tr
UserGroups.Remark.first()
arglist.table = arglist.table or { }
if arglist.table.caption then
e = mw.html.create( "caption" )
s = type( arglist.table.caption )
if s == "string" then
if s ~= "" then
caption = e:wikitext( arglist.table.caption )
end
elseif s == "table" then
caption = e:newline()
:node( arglist.table.caption )
end
end
if type( config.table ) == "table" then
furnish( r1, config.table.class, config.table.style )
end
if type( config.died ) == "string" and
mw.text.trim( config.died ) ~= "" then
UserGroups.r.table.died = config.died
end
if type( arglist.table.columns ) == "table" then
for i = 1, #arglist.table.columns do
s = arglist.table.columns[ i ]
if type( s ) == "string" then
s = mw.text.trim( s )
if s ~= "" then
if s:sub( 1, 8 ) == "#account" then
if i == 1 then
lead = true
else
s = false
end
end
if s then
columns = columns or { }
table.insert( columns, s )
end
end
end
end -- for i
end
arglist.table.columns = columns or { }
if not lead and #along > 1 then
table.insert( arglist.table.columns, 1, "#account" )
end
if #arglist.table.columns > 1 then
tr = mw.html.create( "tr" )
furnish( tr, arglist.table.THclass, arglist.table.THstyle )
tr:newline()
for i = 1, #arglist.table.columns do
s = arglist.table.columns[ i ]
th = mw.html.create( "th" )
th:wikitext( UserGroups.r.table.feature( s ) )
tr:node( th )
:newline()
if s:sub( 1, 1 ) == "#" then
if s:sub( 2, 7 ) == "groups" then
arglist.table.large = true
end
else
arglist.table.light = true
end
end -- for i
end
if arglist.table.large and arglist.table.light then
for i = #arglist.table.columns, 1, -1 do
s = arglist.table.columns[ i ]
if s:sub( 1, 7 ) == "#groups" then
table.remove( arglist.columns, i )
end
end -- for i
arglist.table.large = false
end
-- :node( mw.html.create( "thead" )
if caption then
r1:node( caption )
:newline()
end
if tr then
r1:newline()
:node( tr )
:newline()
end
-- )
if type( config.codes ) ~= "table" then
config.codes = { }
end
if arglist.codes and type( arglist.codes ) ~= "table" then
arglist.codes = false
end
if type( about ) == "table" then
if type( about.groups ) == "table" then
for k, v in pairs( about.groups ) do
if type( v ) == "table" and
type( v.see ) == "string" then
e = config.codes[ k ]
if type( e ) ~= "table" then
config.codes[ k ] = { }
e = config.codes[ k ]
end
if type( e.see ) ~= "string" then
config.codes[ k ].see = v.see
end
end
end -- for k, v
end
if type( about.order ) == "table" then
UserGroups.r.table.order = { }
for k, v in pairs( about.order ) do
if type( v ) == "string" and v ~= "" then
UserGroups.r.table.order[ v ] = k
end
end -- for k, v
end
end
if arglist.table.large and arglist.codes then
order = { }
for k = #along, 1, -1 do
s = along[ k ]
c = accounts[ s ]
if type( c.groups ) == "table" then
for i = #c.groups, 1, -1 do
e = c.groups[ i ]
for j = 1, #arglist.codes do
if arglist.codes[ j ] == e.code then
e = false
break -- for j
end
end -- for j
if e then
table.remove( accounts[ s ].groups, i )
end
end
if #accounts[ s ].groups > 0 then
table.insert( order, 1, s )
end
end
end
else
order = along
end
for k = 1, #order do
columns = { }
tr = mw.html.create( "tr" )
multi = multi + 1
s = order[ k ]
n = 1
tr:attr( "id", UserGroups.r.table.fragment( s ) )
for i = 1, #arglist.table.columns do
c = UserGroups.r.table.fill( i, s, accounts, arglist )
table.insert( columns, c )
if c[ 1 ] then
UserGroups.r.table.fiat( tr, c[ 1 ] )
end
if #c > n then
n = #c
end
end -- for i
tr:newline()
r1:node( tr )
:newline()
for m = 2, n - 1 do
tr = mw.html.create( "tr" )
multi = multi + 1
for i = 1, #columns do
c = columns[ i ]
if c[ m ] then
UserGroups.r.table.fiat( tr, c[ m ] )
end
end -- for i
r1:node( tr )
:newline()
end -- for m
end -- for k
if #order > 1 and arglist.table.number then
tr = mw.html.create( "tr" )
td = mw.html.create( "td" )
td:addClass( "sortbottom" )
:attr( "colspan", tostring( #arglist.table.columns ) )
:attr( "data-sort-value", "{entryCount}" )
:wikitext( factory( "entryCount", tostring( #along ) ) )
tr:newline()
:node( td )
:newline()
r1:node( tr )
:newline()
end
if type( arglist.source ) == "string" then
tr = mw.html.create( "tr" )
td = mw.html.create( "td" )
td:addClass( "sortbottom" )
:attr( "colspan", tostring( #arglist.table.columns ) )
:attr( "data-sort-value", "{source}" )
:css( "font-size", "86%" )
:wikitext( factory( "dataSource" ) )
:wikitext( string.format( " [[%s]]", arglist.source ) )
tr:newline()
:node( td )
:newline()
r1:node( tr )
:newline()
end
if multi > 1 and #arglist.table.columns > 1 then
r1:addClass( "sortable" )
end
r1 = UserGroups.Remark.finish( r1 )
return r1, r2
end -- UserGroups.r.table.format()
UserGroups.r.table.forward = function ( across, accounts, arglist )
-- Shift renamed account
-- across -- string, with renamed account
-- accounts -- table, with data
-- arglist -- table, with current request
-- Returns string or element, or not
local r
if accounts[ across ] then
local n = #arglist.table.columns - 1
r = UserGroups.r.table.fragment( across )
r = string.format( "→ [[#%s|%s]]", r, across )
if n > 1 then
r = { content = r,
attr = { } }
table.insert( r.attr,
{ key = "colspan",
value = tostring( n ) } )
end
end
return r
end -- UserGroups.r.table.forward()
UserGroups.r.table.fragment = function ( account )
-- Fragment identifier
-- account -- string, with validated user nick
-- Returns identifier
return string.format( "@%s@", account )
end -- UserGroups.r.table.fragment()
UserGroups.r.table.fromto = function ( at )
-- Assign date to structure
-- at -- string, with date, or table, or nothing
-- Returns string or table
-- .content = formatted date
-- .attr { { key = "data-sort-value"
-- value = ISO date } )
local s = type( at )
local o, r, sort
if s == "string" then
s = at
elseif s == "table" then
o = at
s = o.date
if type( o.sort ) == "string" and
o.sort ~= "" then
sort = o.sort
end
else
s = false
end
if type( s ) == "string" then
s = mw.text.trim( s )
if s == "" then
s = false
end
else
s = false
end
if s then
if s == "-" or s == true then
r = false
elseif s:match( "^%?+$" ) then
r = "???"
elseif s:find( "?", 2, true ) then
r = s
else
local DateTime = UserGroups.DateTime.fetch()
r = { attr = { } }
if DateTime then
local date = DateTime( sort or s )
if date.year then
local scheme
if type( UserGroups.config.date ) == "string" then
scheme = UserGroups.config.date
else
if date.month then
if date.dom then
scheme = "Y-m-d"
else
scheme = "Y-m"
end
else
scheme = "Y"
end
end
if sort then
r.content = s
else
r.content = date:format( scheme )
end
sort = "#" .. date:format( "Ymd" )
end
else
r.content = s
sort = s
end
table.insert( r.attr, { key = "data-sort-value",
value = sort } )
if r.content then
if o then
local snap
s = type( o.vsn )
if s == "string" then
snap = mw.text.trim( o.vsn )
if snap == "" then
snap = false
end
elseif s == "number" then
if o.vsn > 0 and
math.floor( o.vsn ) == o.vsn then
snap = tostring( o.vsn )
end
end
if snap then
s = "Special:PermaLink/" .. snap
elseif type( o.log ) == "number" then
if o.log > 0 and
math.floor( o.log ) == o.log then
s = string.format( "%s%s",
"Special:Redirect/logid/",
tostring( o.log ) )
else
s = false
end
elseif type( o.page ) == "string" then
s = mw.text.trim( o.page )
if s == "" then
s = false
elseif not s:find( ":", 1, true ) then
local e = mw.html.create( "span" )
s = factory( "errLinkTarget", s )
e:addClass( "error" )
:wikitext( s )
r.content = tostring( e )
s = false
end
else
s = false
end
if s and
type( o.prefix ) == "string" then
snap = mw.text.trim( o.prefix )
if snap ~= "" then
if not snap:match( ":$" ) then
snap = snap .. ":"
end
s = snap .. s
end
end
if s then
if s:find( "[", 1, true ) or
s:find( "|", 1, true ) or
s:find( "]", 1, true ) then
local e = mw.html.create( "span" )
s = factory( "errLinkTarget", s )
e:addClass( "error" )
:wikitext( s )
r.content = tostring( e )
else
r.content = string.format( "[[%s|%s]]",
s, r.content )
end
end
end
else
local e = mw.html.create( "span" )
:addClass( "error" )
:wikitext( s )
r.content = tostring( e )
r.attr[ 1 ].value = s
end
end
end
return r or true
end -- UserGroups.r.table.fromto()
UserGroups.r.target.format = function ( accounts, along, arglist )
-- Format data as #target list
-- accounts -- table, with data
-- along -- table, with order
-- arglist -- table, with current request
-- Returns
-- 1 -- mw.html.ul
local r = mw.html.create( "ul" )
local target = { }
local li
UserGroups.frame = UserGroups.frame or mw.getCurrentFrame()
for i = 1, #along do
li = mw.html.create( "li" )
target[ 1 ] = mw.title.makeTitle( 2, along[ i ] ).prefixedText
li:wikitext( UserGroups.frame:callParserFunction( "#target",
target ) )
r:newline()
:node( li )
end -- for i
r:newline()
return r
end -- UserGroups.r.target.format()
UserGroups.r.timeline.factory = function ( area, ahead, after, arglist )
-- Combine timeline template with current content and parameters
-- area -- string, with major source code
-- ahead -- number, with lowest date (8digit)
-- after -- number, with highest date (8digit)
-- arglist -- table, with current request
-- arglist.timeline {}
-- Returns mw.html.div, or not
local t = mw.title.new( arglist.timeline.template )
local r
if t.exists then
local scheme = t:getContent()
if scheme then
local Timeline = UserGroups.r.timeline
r = scheme:match( Timeline.seek .. "(.+)</pre>" )
if r then
local i = ahead - ahead % 10000
local k = after - after % 10000
local s
Timeline = arglist.timeline
r = r:gsub( "§MAINBAR§", area )
:gsub( "§FROM§", tostring( i * 0.0001 ) )
:gsub( "§TILL§", tostring( k * 0.0001 ) )
if type( Timeline.caption ) == "string" then
s = Timeline.caption:gsub( "%s+", "_" )
r = r:gsub( "§CAPTION§", s )
end
if type( Timeline.previous ) == "string" then
s = Timeline.previous:gsub( "%s+", "_" )
r = r:gsub( "§PREVIOUS§", s )
end
if type( Timeline.elected ) == "string" then
s = Timeline.elected:gsub( "%s+", "_" )
r = r:gsub( "§ELECTED§", s )
end
end
end
end
if r then
UserGroups.frame = UserGroups.frame or mw.getCurrentFrame()
r = UserGroups.frame:extensionTag( "timeline", r )
if r then
local div = mw.html.create( "div" )
div:newline()
:wikitext( r )
:newline()
r = div
end
end
return r
end -- UserGroups.r.timeline.factory()
UserGroups.r.timeline.fetch = function ( accounts, alone )
-- Retrieve ordered lists of periods per account
-- accounts -- table, with data
-- alone -- string, with group
-- Returns table, or not
-- account: sequence table of periods (chronolcal)
-- period: table of { f= t= b= e= } periods
local r
if type( accounts ) == "table" then
local f = function ( a1, a2 )
return a1.f < a2.f
end -- f()
local d, g, from, till
for k, v in pairs( accounts ) do
if type( v ) == "table" and
type( v.groups ) == "table" and
#v.groups > 0 then
for i = 1, #v.groups do
g = v.groups[ i ]
if g.code == alone then
from = UserGroups.r.timeline.fit( g.from )
till = UserGroups.r.timeline.fit( g.till )
if from and till and from <= till then
d = { f = from,
t = till,
b = UserGroups.r.timeline.fit( g.on ),
e = UserGroups.r.timeline.fit( g.off )
}
r = r or { }
r[ k ] = r[ k ] or { }
table.insert( r[ k ], d )
end -- for i
if r and r[ k ] then
table.sort( r[ k ], f )
end
end
end
end
end -- for k, v
end
return r
end -- UserGroups.r.timeline.fetch()
UserGroups.r.timeline.fiat = function ( at )
-- Format date for <timeline>
-- at -- number, with 8 digits date
-- Returns string
local i = at % 100
local k = ( at - i ) * 0.01
local m = k % 100
local j = ( k - m ) * 0.01
local r
if UserGroups.r.timeline.stamp == "dd/mm/yyyy" then
r = string.format( "%02d/%02d/%04d", i, m, j )
elseif UserGroups.r.timeline.stamp == "mm/dd/yyyy" then
r = string.format( "%02d/%02d/%04d", i, m, j )
else
r = string.format( "%04d-%02d-%02d", j, m, i )
end
return r
end -- UserGroups.r.timeline.fiat()
UserGroups.r.timeline.fill = function ( arrive, along, amount )
-- Retrieve timeline MainBar plot area
-- arrive -- table, with events per account
-- along -- table, with ordered list of accounts by lowest from
-- amount -- number, with longest event list
-- Returns string, or not
local DateTime = UserGroups.DateTime.fetch()
local r
if DateTime then
local fiat = UserGroups.r.timeline.fiat
local s1 = "\n at:%s mark:(line, term)"
local s2 = "\n color:%s from:%s till:%s text:\"[[user:%s|%s]]\""
local event, events, n, now, s, sign, state
if not UserGroups.today then
UserGroups.today = DateTime()
end
now = tonumber( UserGroups.today:format( "Ymd" ) )
for i = 1, amount do
n = 0
if r then
r = r .. "\n barset:break"
else
r = ""
end
for j = 1, #along do
sign = along[ j ]
events = arrive[ sign ]
event = events[ i ]
s = type( event )
if s == "number" then
s = string.format( s1, fiat( event ) )
elseif s == "table" then
if event.t > now then
state = "elected"
else
state = "previous"
end
s = string.format( s2,
state,
fiat( event.f ),
fiat( event.t ),
sign,
sign )
else
s = false
n = n + 1
end
if s then
if n > 0 then
for k = 1, n do
r = r .. "\n barset:skip"
end -- for k
n = 0
end
r = r .. s
end
end -- for j
end -- for i
end
return r
end -- UserGroups.r.timeline.fiat()
UserGroups.r.timeline.first = function ( accounts )
-- Retrieve ordered list of accounts per lowest from
-- adjacent -- table, with account period sequences
-- Returns
-- 1 -- sequence table, with nicks
-- 2 -- number, with first year
local f = function ( a1, a2 )
local k1 = accounts[ a1 ][ 1 ].f
local k2 = accounts[ a2 ][ 1 ].f
local rs
if k1 == k2 then
rs = a1 < a2
else
rs = k1 < k2
end
return rs
end -- f()
local r1 = { }
for k, v in pairs( accounts ) do
table.insert( r1, k )
end -- for k, v
table.sort( r1, f )
return r1, accounts[ r1[ 1 ] ][ 1 ].f
end -- UserGroups.r.timeline.first()
UserGroups.r.timeline.fit = function ( at )
-- Retrieve YYYY-MM-DD
-- at -- string, or table
-- Returns number, or not
local s = type( at )
local r
if s == "string" then
s = at
elseif s == "table" then
s = at.date
if type( at.sort ) == "string" and
at.sort ~= "" then
s = at.sort
elseif type( at.date ) == "string" and
at.date ~= "" then
s = at.date
end
else
s = false
end
if type( s ) == "string" then
local j, m, i = s:match( "^%s*(20%d%d)-([01]%d)-([0-3]%d)%s*$" )
if j then
j = tonumber( j )
m = tonumber( m )
i = tonumber( i )
if m > 0 and m <= 12 and i > 0 and i <= 31 then
r = 10000 * j + 100 * m + i
end
end
end
return r
end -- UserGroups.r.timeline.fit()
UserGroups.r.timeline.follows = function ( along, adjacent, accounts )
-- Retrieve ordered lists of periods per account
-- along -- table, with chronological order
-- adjacent -- table, with period sequences
-- accounts -- table, with user data
-- Returns
-- 1 -- table, with chronological events per account
-- 2 -- number, maximum event count
-- 3 -- number, last date at all
local r1 = { }
local r2 = 0
local r3 = 0
local breaks, events, m, p, period, periods, sign, term
for i = 1, #along do
sign = along[ i ]
periods = adjacent[ sign ]
events = { }
breaks = { }
m = 0
term = false
for j = 1, #periods do
p = periods[ j ]
if p.b then
table.insert( breaks, p.b )
end
if m == p.f then
if term then
term.t = p.t
table.insert( breaks, m )
else
term = p
end
elseif term then
table.insert( events, mw.clone( term ) )
term = false
else
term = p
end
if p.e then
table.insert( breaks, p.e )
end
if p.f then
end
m = p.t
if m > r3 then
r3 = m
end
end -- for j
if term then
table.insert( events, term )
end
for j = 1, #breaks do
table.insert( events, breaks[ j ] )
end -- for j
r1[ sign ] = events
if #events > r2 then
r2 = #events
end
end -- for i
return r1, r2, r3
end -- UserGroups.r.timeline.follows()
UserGroups.r.timeline.format = function ( accounts, along, arglist )
-- Format data as <timeline>
-- accounts -- table, with data
-- along -- table, with alphabetical order
-- arglist -- table, with current request
-- Returns
-- 1 -- mw.html.div
-- 2 -- table, with errors, or not
local r1, r2
if type( arglist ) == "table" and
type( arglist.codes ) == "table" and
#arglist.codes == 1 and
type( arglist.codes[ 1 ] ) == "string" and
type( arglist.timeline ) == "table" and
type( arglist.timeline.template ) == "string" then
local single = mw.text.trim( arglist.codes[ 1 ] )
if single and single ~= "" then
local Timeline = UserGroups.r.timeline
local timed = Timeline.fetch( accounts, single )
if timed then
local rows, min = Timeline.first( timed )
local terms, n, max = Timeline.follows( rows,
timed,
accounts )
if n > 0 then
local story = Timeline.fill( terms, rows, n )
if story then
r1 = Timeline.factory( story,
min,
max,
arglist )
end
end
end
end
end
return r1, r2
end -- UserGroups.r.timeline.format()
UserGroups.r.ul.format = function ( accounts,
along,
arglist,
about,
amount )
-- Format data as <ul>, or <ol>
-- accounts -- table, with data
-- along -- table, with order
-- arglist -- table, with current request
-- about -- ignored
-- amount -- boolean, for <ol>
-- Returns
-- 1 -- mw.html.ul, or mw.html.ol
local li, r, s
if amount then
s = "ol"
else
s = "ul"
end
r = mw.html.create( s )
for i = 1, #along do
s = along[ i ]
li = mw.html.create( "li" )
li:wikitext( string.format( "[[user:%s|%s]]", s, s ) )
r:newline()
:node( li )
end -- for i
r:newline()
return r
end -- UserGroups.r.ul.format()
UserGroups.DateTime.fetch = function ()
-- Returns object, or false if unavailable
local r
if UserGroups.DateTime.obj then
r = UserGroups.DateTime.obj
else
local lucky, DateTime = pcall( require, "Module:DateTime" )
if type( DateTime ) == "table" then
UserGroups.DateTime.obj = DateTime.DateTime()
r = UserGroups.DateTime.obj
else
UserGroups.DateTime.obj = true
end
end
return r
end -- UserGroups.DateTime.fetch()
UserGroups.Remark.feed = function ( apply )
-- Register one remark
-- apply -- string, with wikitext
-- Returns mw.html.span, with link to remark, or false if empty
local r, s
if type( apply ) == "string" then
s = mw.text.trim( apply )
if s == "" then
s = false
end
end
if s then
local n, sign
table.insert( UserGroups.Remark.collection, s )
n = #UserGroups.Remark.collection
sign = UserGroups.Remark.fragment( n )
r = mw.html.create( "span" )
r:attr( "id", sign .. "*" )
:css( "vertical-align", "super" )
:wikitext( string.format( "[[#%s|(%d)]]", sign, n ) )
end
return r
end -- UserGroups.Remark.feed()
UserGroups.Remark.finish = function ( already )
-- Append remarks to already, if any
-- already -- mw.html, with main content
-- Returns mw.html.div or already
local n = #UserGroups.Remark.collection
local div, r, sign, story
if n > 0 then
r = mw.html.create( "div" )
r:newline()
:node( already )
for i = 1, n do
story = UserGroups.Remark.collection[ i ]
sign = UserGroups.Remark.fragment( i )
div = mw.html.create( "div" )
div:attr( "id", sign )
:wikitext( string.format( "[[#%s*|(%d)]] %s",
sign, i, story ) )
r:newline()
:node( div )
end -- for i
r:newline()
else
r = already
end
return r
end -- UserGroups.Remark.finish()
UserGroups.Remark.first = function ()
-- Initialize or reset Remarks
UserGroups.Remark.collection = { }
UserGroups.Remark.sign = false
end -- UserGroups.Remark.first()
UserGroups.Remark.fragment = function ( at )
-- Retrieve remark identifier
-- at -- number, in sequence
if not UserGroups.Remark.sign then
local id = math.floor( os.clock() * UserGroups.Remark.max )
UserGroups.Remark.sign = string.format( "%s-%d-",
UserGroups.suite, id )
end
return string.format( "%s%d", UserGroups.Remark.sign, at )
end -- UserGroups.Remark.fragment()
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()
local p = { }
p.f = function ( frame )
local params = { source = frame.args.source,
json = frame.args.JSON,
sole = frame.args.account,
subset = frame.args.pattern,
codes = frame.args.codes,
scheme = frame.args.type,
class = frame.args.class,
style = frame.args.style,
id = frame.args.id
}
local lucky, r
UserGroups.frame = frame
for k, v in pairs( params ) do
if v == "" then
params[ k ] = false
end
end -- for k, v
if params.codes then
params.codes = mw.text.split( params.codes, "%s+" )
end
if frame.args["active.state"] == "1" then
params.live = true
elseif frame.args["active.state"] == "0" then
params.live = false
end
if frame.args.caption and
frame.args.caption ~= "" and
( params.scheme == "table" or
params.scheme == "timeline" ) then
params[ params.scheme ] = { caption = frame.args.caption }
end
if frame.args["table.columns"] or
frame.args["table.number"] or
frame.args["table.THclass"] or
frame.args["table.THstyle"] then
params.table = params.table or { }
params.table.columns = frame.args["table.columns"]
params.table.number = ( frame.args["table.number"] == "1" )
params.table.THclass = frame.args["table.THclass"]
params.table.THstyle = frame.args["table.THstyle"]
for k, v in pairs( params.table ) do
if v == "" then
params.table[ k ] = false
end
end -- for k, v
if params.table.columns then
params.table.columns =
mw.text.split( params.table.columns, "%s+" )
end
end
if frame.args["timeline.elected"] or
frame.args["timeline.previous"] or
frame.args["timeline.template"] then
params.timeline = params.timeline or { }
params.timeline.elected = frame.args["timeline.elected"]
params.timeline.previous = frame.args["timeline.previous"]
params.timeline.template = frame.args["timeline.template"]
for k, v in pairs( params.timeline ) do
if v == "" then
params.timeline[ k ] = false
end
end -- for k, v
end
lucky, r = pcall( UserGroups.f, params )
if not lucky then
r = fault( r )
end
return tostring( r or "" )
end -- p.f()
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 -- userGroups