Modul:PageTree

Die Dokumentation für dieses Modul kann unter Modul:PageTree/Doku erstellt werden

--[=[ 2015-05-18
Module:pageTree
]=]



-- local globals
local Current = { maxSub = 10 }
local Sort    = false
local Strings = { "segment", "self", "stamped", "subpager", "suppress" }
local Toggles = { "lazy", "level", "lineup", "light", "linked", "limit",
                  "list" }



local function face( about )
    -- Ensure presence of entry title
    --     about  -- table, with entry
    --               .show   -- link title
    --               .seed   -- page name
    if not about.show then
       about.show = about.seed:match( "/([^/]+)$" )
       if not about.show then
           about.show = about.seed:match( "^[^:]+:(.+)$" )
           if not about.show then
               about.show = about.seed
           end
       end
    end
end -- face()



local function facility( access )
    -- Load data table
    --     access  -- string, with path of module
    --                        maybe relative, if starting with "/"
    local s = access
    local lucky, r
    if s:byte( 1, 1 ) == 47 then    -- "/"
        if Current.suite then
            s = Current.suite .. s
        end
    end
    lucky, r = pcall( mw.loadData, s )
    if type( r ) ~= "table" then
        r = string.format( "'%s' invalid",  s )
    end
    return r
end -- facility()



local function fade( ask )
    -- Check whether page is to be hidden
    --     ask  -- string, with page name
    -- Returns true if to be hidden
    local r = false
    for k, v in pairs( Current.hide ) do
        if ask:match( v ) then
            r = true
            break -- for k, v
        end
    end -- for k, v
    return r
end -- fade()



local function failsafe( apply )
    -- Clone read-only table
    --     apply  -- table, with basic data elements, read-only
    -- Returns message with markup
    local r = { }
    for k, v in pairs( apply ) do
        r[ k ] = v
    end -- for k, v
    return r
end -- failsafe()



local function failures()
    -- Check all pages
    local redirect = {}
    local unknown  = {}
    local r, s, title
    local n = 0
    for k, v in pairs( Current.pages ) do
        n = n + 1
        s = v.seed
        if type( s ) == "string" then
            title = mw.title.new( s )
            if not title then
                table.insert( unknown, s )
            elseif title.exists then
                if v.shift then
                    if not title.isRedirect then
                        table.insert( redirect,
                                      "(-)" .. s )
                    end
                elseif Current.linked and
                       title.isRedirect then
                    table.insert( redirect,
                                  "(+)" .. s )
                end
            else
                table.insert( unknown, s )
            end
        end
    end -- for k, v
    r = string.format( "n=%d", n )
    n = table.maxn( unknown )
    if n > 0 then
        s = "*** unknown:"
        for i = 1, n do
            r = string.format( "%s %s %s", r, s, unknown[ i ] )
            s = "|"
        end -- for i
    else
        n = table.maxn( redirect )
        if n > 0 then
            s = "*** unexpected redirect:"
            for i = 1, n do
                r = string.format( "%s %s %s", r, s, redirect[ i ] )
                s = "|"
            end -- for i
        end
    end
    return r
end -- failures()



local function fair( adopt )
    -- Expand relative page name, if necessary
    --     adopt  -- string, with page name
    -- Returns absolute page name, or false
    local r
    if adopt:byte( 1, 1 ) == 47 then    -- "/"
        r = Current.start .. adopt:sub( 2 )
    else
        r = adopt
    end
    r = mw.text.trim( r )
    if r == "" then
        r = false
    end
    return r
end -- fair()



local function fasten( adopt )
    -- Format restrictions
    --     adopt  -- string, with restriction entry
    -- Returns absolute page name, or false
    local designs = {
     autoconfirmed       = "background:#FFFF80",
     editeditorprotected = "background:#FFFF00;border:#FF0000 1px solid",
     superprotect        = "background:#FF0000;border:#FFFF00 9px solid",
     sysop               = "background:#FFFF00;border:#FF0000 3px solid",
     ["?????????"]       = "border:#FF0000 5px solid;color:#FF0000" }
    local restrictions = mw.text.split( adopt, ":" )
    local r = ""
    local start = "margin-left:2em;"
    local staff, strict, style
    for i = 1, #restrictions do
        strict, staff = restrictions[ i ]:match( "^(.*)=(.+)$" )
        strict = mw.text.trim( strict )
        if strict == "" then
            strict = "?????????"
        end
        style = designs[ staff ]
        if not style then
            style  = designs[ "?????????" ]
            strict = strict .. "?????????"
        end
        if start then
            style = start .. style
            start = false
        end
        style = style .. ";padding-left:3px;padding-right:3px;"
        r     = string.format( "%s<span style='%s'>%s</span>",
                           r, style, strict )
    end -- for i
    return r
end -- fasten()



local function fatal( alert )
    -- Format disaster message with class="error" and put into category
    --     alert   -- string, with message, or other data
    -- Returns message string with markup
    local ecat = mw.message.new( "Scribunto-common-error-category" )
    local r = type( alert )
    if r == "string" then
        r = alert
    else
        r = "???? " .. r
    end
    if ecat:isBlank() then
        ecat = ""
    else
        ecat = string.format( "[[Category:%s]]", ecat:plain() )
    end
    r = string.format( "<span class=\"error\">FATAL LUA ERROR %s</span>",
                       r )
        .. ecat
    return r
end -- fatal()



local function father( ancestor )
    -- Find parent page
    --     ancestor  -- string, with page name
    -- Returns page name of parent, or Current.series
    local r = ancestor:match( "^(.+)/[^/]+$" )
    if not r then
        r = ancestor:match( "^([^:]+:).+$" )
        if not r then
            r = Current.series
        end
    end
    return r
end -- father()



local function fault( alert )
    -- Format message with class="error"
    --     alert  -- string, with message
    -- Returns message with markup
    return string.format( "<span class=\"error\">%s</span>", alert )
end -- fault()



local function features( apply, access )
    -- Fill Current.pages with elements
    --     apply   -- table, with definitions, read-only
    --     access  -- string, with relative path of module
    -- Returns error message, if failed, or false, if fine
    local r, e, s
    local bad = { }
    local tmp = { }
    for k, v in pairs( apply ) do
        s = type( k )
        e = false
        if s == "number" then
            s = type( v )
            if s == "string" then
                s = v
                e = { }
            elseif s == "table" then
                if type( v.seed ) == "string" then
                    s = v.seed
                    e = failsafe( v )
                end
            end
        elseif s == "string" then
            if type( v ) == "table" then
                s = k
                e = failsafe( v )
            end
        elseif k == true then    -- root
            if Current.pages[ true ] then
                bad[ "true" ] = "duplicated"
            elseif type( v ) == "table" then
                if type( v.seed ) == "string" then
                    Current.pages[ true ]          = failsafe( v )
                    Current.pages[ true ].children = { }
                else
                    bad[ "true" ] = "seed missing"
                end
            else
                bad[ "true" ] = "invalid"
            end
        end
        if e then
            s = fair( s )
            if tmp[ s ] then
                bad[ s ] = "duplicated"
            else
                tmp[ s ] = true
            end
            if s then
                if not Current.pages[ s ] then
                    e.seed = s
                    if e.super then
                        if type( e.super ) == "string" then
                            e.super = fair( e.super )
                        end
                    elseif e.super == nil then
                        e.super = father( s )
                    end
                    e.children = { }
                    Current.pages[ s ] = e
                end
            end
        end
    end -- for k, v
    e = 0
    r = string.format( " in '%s'", access )
    for k, v in pairs( bad ) do
        e = e + 1
        r = string.format( "%s * [%s]: %s ", r, k, v )
    end -- for k, v
    if e == 0 then
        r = false
    elseif e == 1 then
        r = "Error" .. r
    else
        r = "Errors" .. r
    end
    return r
end -- features()



local function feed( access )
    -- Fill Current with data, if not yet set
    --     access  -- string, with relative path of module
    -- Returns error message, if failed, or false, if fine
    local r = facility( access )
    if type( r ) == "table" then
        local s
        if type( r.maxSub ) == "number" then
            Current.maxSub = r.maxSub
        end
        if type( r.stamp ) == "string" then
            if Current.stamp then
                if Current.stamp < r.stamp then
                    Current.stamp = r.stamp
                end
            else
                Current.stamp = r.stamp
            end
        end
        if type( r.start ) == "string" then
            s = mw.text.trim( r.start )
            if s ~= "" then
                Current.start = s
            end
        end
        if not Current.pages then
            Current.pages = { }
        end
        if type( r.pages ) == "table" then
            if not Current.pages then
                Current.pages = { }
            end
            s = features( r.pages, access )
            if s then
                r = s
            end
        end
        if type( r ) == "table" then
            if type( r.sub ) == "string" then
                r = feed( string.format( "%s/%s", access, r.sub ) )
            else
                r = false
            end
        end
    end
    return r
end -- feed()



local function field( about, absolute )
    -- Format entry as link
    --     about     -- table, with entry
    --                  .show        -- link title
    --                  .seed        -- page name
    --                  .shift       -- redirect target
    --                  .protection  -- restrictions
    --     absolute  -- true, if real page name to be shown
    -- Returns string
    local r
    if absolute then
        r = string.format( "[[%s]]", about.seed )
    else
        face( about )
        if about.show == about.seed then
            r = string.format( "[[%s]]", about.seed )
        else
            r = string.format( "[[%s|%s]]", about.seed, about.show )
        end
    end
    if type( about.suffix ) == "string" then
        r = string.format( "%s %s", r, about.suffix )
    end
    if Current.linked and type( about.shift ) == "string" then
        r = string.format( "%s <small>&#8594;[[%s]]</small>",
                           r, fair( about.shift ) )
    end
    if Current.limit and type( about.protection ) == "string" then
        r = string.format( "%s %s",
                           r, fasten( about.protection ) )
    end
    return r
end -- field()



local function filter( adjust )
    -- Create sort key (Latin ASCII upcased)
    --     adjust  -- string, to be standardized
    -- Returns string with key
    if not Sort then
        r, Sort = pcall( require, "Module:Sort" )
        if type( Sort ) == "table" then
            Sort = Sort.Sort()
        else
            error( "Module:Sort not ready" )
        end
    end
    return string.upper( Sort.lex( adjust, "latin", false ) )
end -- filter()



local function first( a1, a2, abs )
    -- Compare a1 with a2 in lexicographical order
    --     a1   -- table, with page entry
    --     a2   -- table, with page entry
    --     abs  -- true, if .show to be used rather than .seed
    -- Returns true if a1 < a2
    if not a1.sort then
        if abs then
            face( a1 )
            a1.sort = filter( a1.show )
        else
            a1.sort = filter( a1.seed )
        end
    end
    if not a2.sort then
        if abs then
            face( a2 )
            a2.sort = filter( a2.show )
        else
            a2.sort = filter( a2.seed )
        end
    end
    return ( a1.sort < a2.sort )
end -- first()



local function firsthand( a1, a2 )
    -- Compare a1 with a2, considering .show
    --     a1  -- string, with page name
    --     a2  -- string, with page name
    -- Returns true if a1 < a2
    return first( a1, a2, true )
end -- first()



local function firstly( a1, a2 )
    -- Compare a1 with a2, considering .index
    --     a1  -- string, with page name
    --     a2  -- string, with page name
    -- Returns true if a1 < a2
    local e1 = Current.pages[ a1 ]
    local e2 = Current.pages[ a2 ]
    local r
    if e1.index then
        if e2.index then
            r = ( e1.index < e2.index )
        else
            r = true
        end
    elseif e2.index then
        r = false
    else
        r = first( e1, e2, true )
    end
    return r
end -- firstly()



local function flag( ahead )
    -- Returns string with leading list syntax, either "#" or "*" or ":"
    --     ahead  -- string, with syntax in case of .lazy
    local r
    if Current.lazy then
        r = ":"
    else
        r = ahead
    end
    return r
end -- flag()



local function flip( already, ahead, amount, above )
    -- Render subtree as expandable/collapsible list of entries
    --     already  -- number, of initially visible levels
    --     ahead    -- string, leading list syntax, either "#" or "*"
    --     amount   -- number, of leading elements
    --     above    -- table, with top element (not shown)
    --                 .children -- will be shown
    -- Returns string with story
    local n = table.maxn( above.children )
    local r = ""
    if n > 0 then
        local live = ( already <= amount )
--      local span = "<span ></span>"
        local e, let, serial
        table.sort( above.children, firstly )
        for i = 1, n do
            e = Current.pages[ above.children[ i ] ]
            if e.list == false then
                let = Current.list
            elseif Current.hide then
                let = not fade( e.seed )
            else
                let = true
            end
            if let then
                if not e.less then
                    Current.item = Current.item + 1
                    serial       = string.format( "%s_%d",
                                                  Current.serial,
                                                  Current.item )
                    r = string.format( "%s\n<div %s %s %s>",
                                       r,
                                       "class='mw-collapsible'",
                                       "data-expandtext='[+]'",
                                       "data-collapsetext='[-]'" )
                end
                r = string.format( "%s\n%s%s",
                                   r,  ahead,  field( e, false ) )
                if not e.less then
                    r = string.format( "%s\n<div %s>\n%s\n%s",
                                       r,
--                                     span,
                                       "class='mw-collapsible-content'",
                                       flip( ahead,
                                             amount + 1,
                                             e,
                                             already ),
                                       "</div></div>" )
                end
            end
        end -- for i
    end
    return r
end -- flip()



local function flow( acquire )
    -- Collect the .super in path
    --     acquire  -- string, with page name
    if type( acquire ) == "string" then
        local e = Current.pages[ acquire ]
        local s = false
        if e then
            s = e.super
        end
        if not s then
            s = acquire:match( "^(.+)/[^/]+$" )
            if not s then
                s = acquire:match( "^([^:]+:)" )
            end
            if s then
                if not e then
                    e                        = { children = { },
                                                 seed     = acquire }
                    Current.pages[ acquire ] = e
                end
                e.super = s
            elseif e then
                e.super = true
            end
        end
        if type( s ) == "string"  and  s~= acquire then
            flow( s )
        end
    end
end -- flow()



local function fluent()
    -- Collect all .children; add .super where missing
    local let = true
    local e
    for k, v in pairs( Current.pages ) do
        if v.super == nil then
            flow( k )
        elseif not Current.pages[ v.super ] then
            flow( v.super )
        end
    end -- for k, v
    for k, v in pairs( Current.pages ) do
        if Current.level then
            let = ( not v.seed:find( "/" ) )
        end
        if let and v.super then
            e = Current.pages[ v.super ]
            if e then
                table.insert( e.children, k )
            end
        end
    end -- for k, v
end -- fluent()



local function follow( ahead, amount, above, all )
    -- Render subtree as list of entries
    --     ahead   -- string, with leading list syntax, either "#" or "*"
    --     amount  -- number, of leading elements
    --     above   -- table, with top element (not shown)
    --                .children -- will be shown
    --     all     -- true if all grandchildren shall be shown
    -- Returns string with story
    local n = table.maxn( above.children )
    local r = ""
    if n > 0 then
        local e, let, lift
        local start = "\n" .. string.rep( ahead, amount )
        table.sort( above.children, firstly )
        for i = 1, n do
            e    = Current.pages[ above.children[ i ] ]
            lift = ( all or above.long )
            if e.list == false then
                let = Current.list
            elseif Current.hide then
                let = not fade( e.seed )
            else
                let = lift
            end
            if let then
                r = string.format( "%s%s%s",
                                   r,  start,  field( e, false ) )
                if lift and ( all or not e.less ) then
                    r = r .. follow( ahead,  amount + 1,  e,  all )
                end
            end
        end -- for i
    end
    return r
end -- follow()



local function formatAll()
    -- Render as single list of entries
    local collect = { }
    local n       = 0
    local r, let
    for k, v in pairs( Current.pages ) do
        let = true
        if v.list == false  and
           ( not Current.list or v.loose or k == true ) then
            let = false
        elseif Current.level and v.seed:find( "/" ) then
            let = false
        elseif Current.hide then
            let = not fade( v.seed )
        end
        if let then
            if v.show then
                v.show = nil
            end
            if Current.light then
                local j, k = v.seed:find( Current.start )
                if j == 1 then
                    v.show = v.seed:sub( k + 1 )
                end
            end
            n            = n + 1
            collect[ n ] = v
        end
    end -- for k, v
    if n > 0 then
        local start
        local long = ( not Current.light )
        if Current.lineup then
            start = " * "
        else
            start = "\n" .. flag( "#" )
        end
        table.sort( collect, firsthand )
        r = ""
        for k, v in pairs( collect ) do
            r = string.format( "%s%s%s",
                               r,
                               start,
                               field( v, long ) )
        end -- for k, v
    else
        r = false
    end
    return r
end -- formatAll()



local function formatExpand( ancestor, args )
    -- Render entire tree as collapsible list text
    --     ancestor  -- string, with name of root element, or false
    --     args      -- table, with control information
    -- Returns string with story, or false
    local init, r
    if type( ancestor ) == "string" then
        r = ancestor
    else
        r = true
    end
    r = Current.pages[ r ]
    if r then
        if type( Current.init ) == "number" then
            init = Current.init
            if Current.init < 1 then
                init = 1
            end
        else
            init = 1
        end
        if type( Current.serial ) ~= "string"
           or Current.serial == "" then
            Current.serial = "pageTree"
        end
        Current.item = 0
        r = flip( init,  flag( "#" ),  1,  r )
    else
        r = false
    end
    return r
end -- formatExpand()



local function formatPath( ancestor )
    -- Render tree as partially opened list
    --     ancestor  -- string, with name of root element, or false
    -- Returns string with story
    local sup = Current.self
    local higher, i, r
    if ancestor then
        higher = Current.pages[ ancestor ]
        if type( higher ) == "table" then
            higher.super = false
        end
    else
        local point = Current.pages[ sup ]
        if not point then
            sup = true
        elseif point.list == false then
            higher = Current.pages[ sup ]
            if type( higher ) == "table" then
                if not higher.loose then
                    sup = true
                end
            else
                sup = true
            end
        end
    end
    for i = Current.maxSub, 0, -1 do
        higher = Current.pages[ sup ]
        if type( higher ) == "table" then
            higher.long = true
            sup         = higher.super
            if not sup then
                break    -- for
            end
        else
            higher = false
            break    -- for
        end
    end    -- for --i
    if higher then
        r = follow( flag( "*" ),  1,  higher,  false )
    else
        r = false
    end
    return r
end -- formatPath()



local function formatSub( amend, around )
    -- Render tree as subpage hierarchy sequence
    --     amend   -- string, with name of template, or false
    --     around  -- object, with frame, or false
    -- Returns string with story, or false
    local higher
    local n       = 1
    local reverse = { }
    local sup     = Current.self
    local r
    if type( sup ) == "string" and not sup:find( "/", 1, true ) then
        flow( sup )
        repeat
            higher = Current.pages[ sup ]
            if type( higher ) == "table" then
                sup          = higher.super
                reverse[ n ] = higher
                if higher.loose then
                    n = -1
                    break    -- repeat
                elseif sup then
                    n = n + 1
                    if n > Current.maxSub then
                        reverse[ n ] = { seed = "???????" }
                        break    -- repeat
                    end
                else
                    break    -- repeat
                end
            else
                break    -- repeat
            end
        until not higher
    end
    if n > 1 then
        for i = n, 2, -1 do
            reverse[ i ] = field( reverse[ i ], false )
        end -- for i
        if amend then
            local frame
            local ordered = { }
            if around then
                frame = around
            else
                frame = mw.getCurrentFrame()
            end
            for i = n, 2, -1 do
                ordered[ n - i + 1 ] = reverse[ i ]
            end -- for i
            r = frame:expandTemplate{ title=amend, args=ordered }
        else
            r = ""
            for i = n, 2, -1 do
                if i < n then
                    r = r .. "&#160;&#62; "
                end
                r = r .. reverse[ i ]
            end -- for i
        end
    else
        r = false
    end
    return r
end -- formatSub()



local function formatTree( ancestor )
    -- Render entire tree as list text
    --     ancestor  -- string, with name of root element, or false
    -- Returns string with story, or false
    local r
    if type( ancestor ) == "string" then
        r = ancestor
    else
        r = true
    end
    r = Current.pages[ r ]
    if r then
        r = follow( flag( "#" ),  1,  r,  true )
    else
        r = false
    end
    return r
end -- formatTree()



local function forward( args )
    -- Execute main task
    --     args  -- table, with arguments
    -- Returns string with story, or false
    local r
    if type( args.series ) == "string"  and
       type( args.service ) == "string"  and
       type( args.suite ) == "string" then
        Current.series  = args.series
        Current.service = args.service
        Current.suite   = args.suite
        if type( args.hide ) == "table" then
            Current.hide = args.hide
        elseif type( args.suppress ) == "string" then
            Current.hide = { }
            table.insert( Current.hide, args.suppress )
        end
        if Current.series:match( "[:/]$" ) then
            Current.start = args.series
        else
            Current.start = args.series .. "/"
        end
        r = feed( "/" .. Current.series )
        if r then
            r = fault( r )
        else
            local life = true
            if Current.service == "path"  or
               Current.service == "subpages" then
                if args.self then
                    Current.self = args.self
                else
                    Current.page = mw.title.getCurrentTitle()
                    Current.self = Current.page.prefixedText
                end
                if not Current.pages[ Current.self ] then
                     if type( Current.pages[ true ] ) == "table" then
                        Current.self = true
                    else
                        life = false
                    end
                end
            end
            if life then
                if Current.service == "subpages" then
                    r = formatSub( args.subpager, args.frame )
                elseif Current.service == "check" then
                    Current.linked = args.linked
                    r = failures()
                else
                    for k, v in pairs( Toggles ) do
                        Current[ v ] = args[ v ]
                    end -- for k, v
                    if Current.service == "all" then
                        r = formatAll()
                    else
                        local segment
                        if type( args.segment ) == "string" then
                            segment = fair( args.segment )
                            if not Current.pages[ segment ] then
                                Current.pages[ segment ] =
                                                    { seed     = segment,
                                                      children = { },
                                                      super    = true,
                                                      list     = false }
                            end
                        end
                        fluent()
                        if Current.service == "path" then
                            r = formatPath( segment )
                        elseif Current.service == "expand" then
                            r = formatExpand( segment, args )
                        else
                            if args.limit == "1"  or
                               args.limit == true then
                                Current.limit = true
                            end
                            r = formatTree( segment )
                        end
                    end
                    if r and args.stamped and Current.stamp then
                        local babel = mw.language.getContentLanguage()
                        local stamp = babel:formatDate( args.stamped,
                                                        Current.stamp )
                        r = stamp .. r
                    end
                end
            else
                r = false
            end
        end
    end
    return r
end -- forward()



local function framed( frame, action )
    -- #invoke call
    --     action  -- string, with keyword
    local params = { service = action,
                     suite   = frame:getTitle() }
    local pars   = frame.args
    local r      = pars[ 1 ]
    if r then
        params.series = mw.text.trim( r )
        if params.series == "" then
            r = false
        end
    end
    if r then
        local lucky
        params.frame = frame
        for k, v in pairs( Strings ) do
            if pars[ v ]  and  pars[ v ] ~= "" then
                params[ v ] = pars[ v ]
            end
        end -- for k, v
        for k, v in pairs( Toggles ) do
            if pars[ v ] then
                params[ v ] = ( pars[ v ] == "1" )
            end
        end -- for k, v
        lucky, r = pcall( forward, params )
        if not lucky then
            r = fatal( r )
        end
    else
        r = fault( "'1=' missing" )
    end
    if not r then
        r = ""
    end
    return r
end -- framed()



-- Export
local p = { }

-- lazy   = do not number but use bullets or nothing
-- level  = top level entries only
-- light  = strip prefix
-- linked = show redirects
-- list   = show suppressed entries

function p.all( frame )
    return  framed( frame, "all" )
end -- p.all

function p.check( frame )
    return  framed( frame, "check" )
end -- p.check

function p.expand( frame )
    return  framed( frame, "expand" )
end -- p.expand

function p.path( frame )
    return  framed( frame, "path" )
end -- p.path

function p.subpages( frame )
    return  framed( frame, "subpages" )
end -- p.subpages

function p.tree( frame )
    return  framed( frame, "tree" )
end -- p.tree

function p.test( args )
    -- Debugging
    --     args  -- table, with arguments; mandatory:
    --              .series   -- tree
    --              .service  -- action mode
    --              .suite    -- Module path
    --              .self     -- page name, in service="path"
    --              .limit    -- show restrictions
    local lucky, r = pcall( forward, args )
    return r or Current
end -- p.test()

return p