Modul:PageTree
Die Dokumentation für dieses Modul kann unter Modul:PageTree/doc erstellt werden
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>→[[%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 .. " > " 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