Modul:UserGroups
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-12local 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
Content Disclaimer
Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.
- The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
- There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
- It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
- Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.