Modul:UserGroups

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.
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( "&lrm;", "" )
         :gsub( "&rlm;", "" )
         :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&nbsp;%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.

  1. 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:
  2. 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.
  3. 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.
  4. 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.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.