Modul:PageTree

Template-info.png Dokumentation für das Modul PageTree[Ansicht] [Bearbeiten] [Versionsgeschichte] [Aktualisieren]

Funktion

Das Modul PageTree stellt eine Liste von Artikel in einer gewünschten hierarchischen Struktur dar. Die entsprechenden Artikel sind dabei in einem Untermodul zu deklarieren.

Verwendung

Vorlagen, die dieses Modul benutzen

  • {{Hilfe}} – Navigationsbox für alle Hilfeartikel auf Wikivoyage.

Vorhandene Artikellisten

Hinweise
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
    local since = assert
    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