Modul:PageTree
Die Dokumentation für dieses Modul kann unter Modul:PageTree/Doku erstellt werden
local PageTree = { suite = "PageTree",
serial = "2024-04-02",
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
]=]
if mw.site.server:find( ".beta.wmflabs.org", 4, true ) then
require( "Module:No Globals" )
end
local Failsafe = PageTree
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>→[[%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
local lucky
lucky, 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 .. " > "
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()
Failsafe.failsafe = function ( atleast )
-- Retrieve versioning and check for compliance
-- Precondition:
-- atleast -- string, with required version
-- or wikidata|item|~|@ or false
-- Postcondition:
-- Returns string -- with queried version/item, also if problem
-- false -- if appropriate
-- 2024-03-01
local since = atleast
local last = ( since == "~" )
local linked = ( since == "@" )
local link = ( since == "item" )
local r
if last or link or linked or since == "wikidata" then
local item = Failsafe.item
since = false
if type( item ) == "number" and item > 0 then
local suited = string.format( "Q%d", item )
if link then
r = suited
else
local entity = mw.wikibase.getEntity( suited )
if type( entity ) == "table" then
local seek = Failsafe.serialProperty or "P348"
local vsn = entity:formatPropertyValues( seek )
if type( vsn ) == "table" and
type( vsn.value ) == "string" and
vsn.value ~= "" then
if last and vsn.value == Failsafe.serial then
r = false
elseif linked then
if mw.title.getCurrentTitle().prefixedText
== mw.wikibase.getSitelink( suited ) then
r = false
else
r = suited
end
else
r = vsn.value
end
end
end
end
elseif link then
r = false
end
end
if type( r ) == "nil" then
if not since or since <= Failsafe.serial then
r = Failsafe.serial
else
r = false
end
end
return r
end -- Failsafe.failsafe()
-- 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 Failsafe.failsafe( since ) or ""
end -- p.failsafe()
setmetatable( p, { __call = function ( func, ... )
setmetatable( p, nil )
return Failsafe
end } )
return p