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>→[[%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 .. " > "
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