Modul:PageTree

aus Wikipedia, der freien Enzyklopädie
local PageTree = { suite   = "PageTree",
                   serial  = "2018-09-13",
                   item    = 56033297,
                   maxSub  = 10,
                   strings = { "segment",
                               "self",
                               "stamped",
                               "subpager",
                               "suppress" },
                   toggles = { "lazy",
                               "level",
                               "lineup",
                               "light",
                               "linked",
                               "limit",
                               "list" } }
--[=[
Module:PageTree
Rendering and administration of hierarchical wiki page structures
]=]



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 PageTree.suite then
            s = PageTree.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 factory( 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 -- factory()



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( PageTree.hide ) do
        if ask:match( v ) then
            r = true
            break -- for k, v
        end
    end -- for k, v
    return r
end -- fade()



local function failures()
    -- Check all pages
    local redirect = {}
    local unknown  = {}
    local r, s, title
    local n = 0
    for k, v in pairs( PageTree.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 PageTree.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 = PageTree.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 PageTree.series
    local r = ancestor:match( "^(.+)/[^/]+$" )
    if not r then
        r = ancestor:match( "^([^:]+:).+$" )
        if not r then
            r = PageTree.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 PageTree.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 = factory( v )
                end
            end
        elseif s == "string" then
            if type( v ) == "table" then
                s = k
                e = factory( v )
            end
        elseif k == true then    -- root
            if PageTree.pages[ true ] then
                bad[ "true" ] = "duplicated"
            elseif type( v ) == "table" then
                if type( v.seed ) == "string" then
                    PageTree.pages[ true ]          = factory( v )
                    PageTree.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 PageTree.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 = { }
                    PageTree.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 PageTree 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
            PageTree.maxSub = r.maxSub
        end
        if type( r.stamp ) == "string" then
            if PageTree.stamp then
                if PageTree.stamp < r.stamp then
                    PageTree.stamp = r.stamp
                end
            else
                PageTree.stamp = r.stamp
            end
        end
        if type( r.start ) == "string" then
            s = mw.text.trim( r.start )
            if s ~= "" then
                PageTree.start = s
            end
        end
        if not PageTree.pages then
            PageTree.pages = { }
        end
        if type( r.pages ) == "table" then
            if not PageTree.pages then
                PageTree.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 PageTree.linked and type( about.shift ) == "string" then
        r = string.format( "%s <small>&#8594;[[%s]]</small>",
                           r, fair( about.shift ) )
    end
    if PageTree.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 PageTree.Sort then
        r, PageTree.Sort = pcall( require, "Module:Sort" )
        if type( PageTree.Sort ) == "table" then
            PageTree.Sort = PageTree.Sort.Sort()
        else
            error( "Module:Sort not ready" )
        end
    end
    return string.upper( PageTree.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 = PageTree.pages[ a1 ]
    local e2 = PageTree.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 PageTree.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 = PageTree.pages[ above.children[ i ] ]
            if e.list == false then
                let = PageTree.list
            elseif PageTree.hide then
                let = not fade( e.seed )
            else
                let = true
            end
            if let then
                local s
                if not e.less then
                    PageTree.item = PageTree.item + 1
                    serial        = string.format( "%s_%d",
                                                   PageTree.serial,
                                                   PageTree.item )
                    if table.maxn( e.children ) > 0 then
                        s = "mw-collapsible"
                        if amount >= already then
                            s = s .. " mw-collapsed"
                        end
                        r = string.format( "%s\n<div class='%s' %s %s>",
                                              r,
                                              s,
                                              "data-expandtext='[+]'",
                                              "data-collapsetext='[-]'" )
                        s = "</div>"
                    else
                        s = ""
                    end
                end
                r = string.format( "%s\n%s%s",
                                   r,
                                   string.rep( ahead, amount ),
                                   field( e, false ) )
                if not e.less then
                    local style
                    if amount >= already then
                        style  = " style='display:none'"
                    else
                        style = ""
                    end
                    r = string.format( "%s\n<div %s%s>\n%s\n</div>%s",
                                       r,
--                                     span,
                                       "class='mw-collapsible-content'",
                                       style,
                                       flip( already,
                                             ahead,
                                             amount + 1,
                                             e ),
                                       s )
                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 = PageTree.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 }
                    PageTree.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( PageTree.pages ) do
        if v.super == nil then
            flow( k )
        elseif not PageTree.pages[ v.super ] then
            flow( v.super )
        end
    end -- for k, v
    for k, v in pairs( PageTree.pages ) do
        if PageTree.level then
            let = ( not v.seed:find( "/" ) )
        end
        if let and v.super then
            e = PageTree.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    = PageTree.pages[ above.children[ i ] ]
            lift = ( all or above.long )
            if e.list == false then
                let = PageTree.list
            elseif PageTree.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( PageTree.pages ) do
        let = true
        if v.list == false  and
           ( not PageTree.list or v.loose or k == true ) then
            let = false
        elseif PageTree.level and v.seed:find( "/" ) then
            let = false
        elseif PageTree.hide then
            let = not fade( v.seed )
        end
        if let then
            if v.show then
                v.show = nil
            end
            if PageTree.light then
                local j, k = v.seed:find( PageTree.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 PageTree.light )
        if PageTree.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 = PageTree.pages[ r ]
    if r then
        if type( PageTree.init ) == "number" then
            init = PageTree.init
            if PageTree.init < 1 then
                init = 1
            end
        else
            init = 1
        end
        if type( PageTree.serial ) ~= "string"
           or PageTree.serial == "" then
            PageTree.serial = "pageTree"
        end
        PageTree.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 = PageTree.self
    local higher, i, r
    if ancestor then
        higher = PageTree.pages[ ancestor ]
        if type( higher ) == "table" then
            higher.super = false
        end
    else
        local point = PageTree.pages[ sup ]
        if not point then
            sup = true
        elseif point.list == false then
            higher = PageTree.pages[ sup ]
            if type( higher ) == "table" then
                if not higher.loose then
                    sup = true
                end
            else
                sup = true
            end
        end
    end
    for i = PageTree.maxSub, 0, -1 do
        higher = PageTree.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     = PageTree.self
    local r
    if type( sup ) == "string" and not sup:find( "/", 1, true ) then
        flow( sup )
        repeat
            higher = PageTree.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 > PageTree.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 = PageTree.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
        PageTree.series  = args.series
        PageTree.service = args.service
        PageTree.suite   = args.suite
        if type( args.hide ) == "table" then
            PageTree.hide = args.hide
        elseif type( args.suppress ) == "string" then
            PageTree.hide = { }
            table.insert( PageTree.hide, args.suppress )
        end
        if PageTree.series:match( "[:/]$" ) then
            PageTree.start = args.series
        else
            PageTree.start = args.series .. "/"
        end
        r = feed( "/" .. PageTree.series )
        if r then
            r = fault( r )
        else
            local life = true
            if PageTree.service == "path"  or
               PageTree.service == "subpages" then
                if args.self then
                    PageTree.self = args.self
                else
                    PageTree.page = mw.title.getCurrentTitle()
                    PageTree.self = PageTree.page.prefixedText
                end
                if not PageTree.pages[ PageTree.self ] then
                     if type( PageTree.pages[ true ] ) == "table" then
                        PageTree.self = true
                    else
                        life = false
                    end
                end
            end
            if life then
                if PageTree.service == "subpages" then
                    r = formatSub( args.subpager, args.frame )
                elseif PageTree.service == "check" then
                    PageTree.linked = args.linked
                    r = failures()
                else
                    for k, v in pairs( PageTree.toggles ) do
                        PageTree[ v ] = args[ v ]
                    end -- for k, v
                    if PageTree.service == "all" then
                        r = formatAll()
                    else
                        local segment
                        if type( args.segment ) == "string" then
                            segment = fair( args.segment )
                            if not PageTree.pages[ segment ] then
                                PageTree.pages[ segment ] =
                                                    { seed     = segment,
                                                      children = { },
                                                      super    = true,
                                                      list     = false }
                            end
                        end
                        fluent()
                        if PageTree.service == "path" then
                            r = formatPath( segment )
                        elseif PageTree.service == "expand" then
                            r = formatExpand( segment, args )
                        else
                            if args.limit == "1"  or
                               args.limit == true then
                                PageTree.limit = true
                            end
                            r = formatTree( segment )
                        end
                    end
                    if r and args.stamped and PageTree.stamp then
                        local babel = mw.language.getContentLanguage()
                        local stamp = babel:formatDate( args.stamped,
                                                        PageTree.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( PageTree.strings ) do
            if pars[ v ]  and  pars[ v ] ~= "" then
                params[ v ] = pars[ v ]
            end
        end -- for k, v
        for k, v in pairs( PageTree.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()



PageTree.failsafe = function ( assert )
    -- Retrieve versioning and check for compliance
    -- Precondition:
    --     assert  -- string, with required version or "wikidata",
    --                or false
    -- Postcondition:
    --     Returns  string with appropriate version, or false
    local r
    if since == "wikidata" then
        local item = PageTree.item
        since = false
        if type( item ) == "number"  and  item > 0 then
            local ent = mw.wikibase.getEntity( string.format( "Q%d",
                                                              item ) )
            if type( ent ) == "table" then
                local vsn = ent:formatPropertyValues( "P348" )
                if type( vsn ) == "table"  and
                   type( vsn.value ) == "string"  and
                   vsn.value ~= "" then
                    r = vsn.value
                end
            end
        end
    end
    if not r then
        if not since  or  since <= PageTree.serial then
            r = PageTree.serial
        else
            r = false
        end
    end
    return r
end -- PageTree.failsafe()



-- 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 PageTree
end -- p.test()

p.failsafe = function ( frame )
    -- Check or retrieve version information
    -- Precondition:
    --     frame  -- object; #invoke environment
    -- Postcondition:
    --     Return string with error message or ""
    -- Uses:
    --     PageTree.failsafe()
    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 PageTree.failsafe( since )  or  ""
end -- p.failsafe()

return p