Modul:Shortcuts
Aus KGS-Wiki
Die Dokumentation für dieses Modul kann unter Modul:Shortcuts/Doku erstellt werden
local Shortcuts = { suite = "Shortcuts",
serial = "2019-08-01",
item = 0 }
--[=[
Support shortcut redirects
]=]
local Failsafe = Shortcuts
-- local globals
local Current = { rule = { } }
local Errors = false
local Sort = false
local function face( adjust )
-- Unlink string
-- adjust -- string, with some shortcuts
-- Returns unlinked shortcuts
return adjust:gsub( "%[%[", "" )
:gsub( "%]%]", "" )
end -- face()
local function faces( any )
-- Retrieve single shortcut names
-- any -- string, with shortcut name list
-- Returns table with shortcut page names
local s = any:gsub( "<[^>]+>", "," )
local r = mw.text.split( s, "%s*,%s*" )
for k, v in pairs( r ) do
if mw.text.trim( v ) == "" then
r[ k ] = nil
end
end -- for k, v
return r
end -- faces()
local function facet( area, assign, assigned )
-- Create mapping shortcut->target
-- area -- string, with target namespace name and colon
-- assign -- string, with target page name
-- assigned -- string, with shortcut page name
-- Returns table with entry
-- .shift -- string, with target page name
-- .shortcut -- string, with shortcut page name
-- .nsn -- number, of shortcut namespace
-- .sort -- string, with sortable shortcut page title
local space, subject = assigned:match( "^([^:]+):(.+)$" )
local r = { }
r.shift = area .. assign
r.shortcut = assigned
if space then
local o = mw.site.namespaces[ space ]
if o then
r.nsn = o.id
end
end
if not r.nsn then
r.nsn = 0
subject = assigned
end
if Sort then
subject = Sort.lex( subject, "latin", false )
end
r.sort = string.upper( subject )
return r
end -- facet()
local function facilitated( attempt )
-- Check whether Config string is a text formatting pattern
-- attempt -- string, with format pattern
-- Throws error with message, else does nothing
local s = Config[ attempt ]
if type( s ) ~= "string" or
not s:find( "%s", 1, true ) then
error( string.format( "/config '%s' invalid", attempt ) )
end
end -- facilitated()
local function factory( account, alone, assembly )
-- Retrieve mappings shortcut->target for entire target namespace
-- account -- string, with module name
-- alone -- number, of namespace
-- assembly -- table, collecting assignments
-- Extending assembly
-- Returns number of target pages
local space = mw.site.namespaces[ alone ]
local r = 0
if space then
local got
local sub = string.format( "%s/%d", account, alone )
local l, t = pcall( mw.loadData, sub )
if type( t ) == "table" then
if space.id == 0 then
space = ""
else
space = space.name .. ":"
end
for k, v in pairs( t ) do
got = faces( face( v ) )
for i, s in pairs( got ) do
table.insert( assembly, facet( space, k, s ) )
end -- for i, s
r = r + 1
end -- for k, v
end
end
return r
end -- factory()
local function faculty( adjust )
-- Test template arg for boolean
-- adjust -- string or nil
-- Returns boolean
local s = type( adjust )
local r
if s == "string" then
r = mw.text.trim( adjust )
r = ( r ~= "" and r ~= "0" )
elseif s == "boolean" then
r = adjust
else
r = false
end
return r
end -- faculty()
local function failure( assigned, about )
-- Add one message to Errors
-- assigned -- string, with shortcut page name
-- about -- string, with error keyword
if type( Errors ) ~= "table" then
Errors = { }
end
Errors[ assigned ] = about
end -- failure()
local function fair( above )
-- Convert shortcut list namespaces into talk pages
-- alert -- string, with shortcuts
-- Returns converted shortcuts
local r = " " .. above
if type( Config.talks ) == "table" then
local seek, shift
for k, v in pairs( Config.talks ) do
seek = string.format( "(%%A?)(%s):", k )
shift = string.format( "%%1%s:", v )
r = r:gsub( seek, shift )
end -- for k, v
end
return mw.text.trim( r )
end -- fair()
local function fallback( access, alt )
-- Retrieve message text
-- access -- string, with message ID
-- alt -- string, with plain message, if no access
-- Returns string, with any message
local r = Config[ access ]
if type( r ) ~= "string" then
r = alt or "***** UNKNOWN MESSAGE " .. access .. " *****"
end
return r
end -- fallback()
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 elem = mw.html.create( "span" )
:addClass( "error" )
local ecat = mw.message.new( "Scribunto-common-error-category" )
local r = type( alert )
if r == "string" then
r = alert
else
r = "???? " .. r
end
elem:wikitext( string.format( "FATAL LUA ERROR %s", r ) )
r = tostring( elem )
if not ecat:isBlank() then
ecat = string.format( "%s[[Category:%s]]", r, ecat:plain() )
end
return r
end -- fatal()
local function fault( alert, absent )
-- Format message with class="error"; add Config (category etc.)
-- alert -- string, with message
-- absent -- boolean, hide message, trigger category
-- Returns message with markup
local e = mw.html.create( "span" )
:addClass( "error" )
:wikitext( alert or "???" )
local r = tostring( e )
if absent and type( Config ) == "table" then
if type( Config.suppress ) == "string" then
facilitated( "suppress" )
r = string.format( Config.suppress, r )
end
if type( Config.scream ) == "string" then
r = string.format( "%s[[Category:%s]]", r, Config.scream )
end
end
return r
end -- fault()
local function fiat( ahead, after )
-- Format table row
-- ahead -- string, with first cell
-- after -- string or false, with second cell
-- Returns table row markup
local r = string.format( "\n|-\n|%s", ahead )
if after then
r = string.format( "%s||%s", r, after )
end
return r
end -- fiat()
local function first( a1, a2 )
-- Compare a1 with a2 in reverse title order
-- a1 -- table, with assignment
-- a2 -- table, with assignment
-- Returns true if a1 < a2
local r
if a1.shortcut == a2.shortcut then
r = ( string.upper( a1.shift ) > string.upper( a2.shift ) )
elseif a1.sort == a2.sort then
r = ( a1.nsn > a2.nsn )
else
r = ( a1.sort > a2.sort )
end
return r
end -- first()
local function flag( alone, achieved )
-- Analyze one shortcut
-- alone -- string, with shortcut page name
-- achieved -- table, with title objects; will be extended
-- Adds error message to collection
local page = mw.title.new( alone )
if page.exists then
if page.isRedirect then
local story = page:getContent()
local shift = story:match( "^#[^%[]+%[%[([^%]\n]+)%]%]" )
local redirect
if not shift or
shift:match( "%%%x%x" ) then
redirect = false
else
redirect = mw.title.new( shift )
end
if not redirect then
failure( alone,
fallback( "sayBadLink", "bad link encoding" ) )
elseif mw.title.equals( Current.page, redirect ) then
for k, v in pairs( achieved ) do
if mw.title.equals( page, v ) then
failure( alone,
fallback( "sayDuplicated",
"duplicated" ) )
page = false
break -- for k, v
end
end -- for k, v
if page then
table.insert( achieved, page )
end
if Current.nsns == Current.nsn
and not Current.leave
and Config.signature
and not story:find( Config.signature, 15 ) then
failure( alone,
fallback( "signal",
".signature (category) missing" )
)
end
else
failure( alone,
fallback( "sayTarget", "wrong target" ) )
end
else
failure( alone, fallback( "sayRegular", "regular page" ) )
end
else
failure( alone, fallback( "sayMissing", "missing" ) )
end
end -- flag()
local function flash( account, alone )
-- Create all item table body with two columns; shortcut and target
-- account -- string, with module name
-- alone -- number or false, with namespace limitation
-- Returns table rows until end, terminate table and provide totals
local r = "\n|}"
local collect = { }
local n, previous
if type( alone ) == "number" then
n = factory( account, alone, collect )
elseif type( Config.rooms ) == "table" then
n = 0
for k, v in pairs( Config.rooms ) do
n = n + factory( account, v, collect )
end -- for k, v
else
r = r .. "'Config.rooms' not found"
collect = false
end
if collect then
local second, shortcut
if not Sort then
local l, t = pcall( require, "Module:Sort" )
if type( t ) == "table" then
Sort = t.Sort()
end
end
table.sort( collect, first )
for k, v in pairs( collect ) do
shortcut = string.format( "[[%s]]", v.shortcut )
second = string.format( "[[:%s]]", v.shift )
if v.shortcut == previous then
failure( previous,
fallback( "sayDuplicated", "duplicated" ) )
end
previous = v.shortcut
r = fiat( shortcut, second ) .. r
end -- for k, v
end
r = string.format( "%s\n%d/%d", r, #collect, n )
return r
end -- flash()
local function folder( arglist )
-- Present table rows
-- arglist -- table, with parameters
-- .targets -- table sequence, target pages specifications
-- .space -- string or nil, namespace of all target pages
-- .story -- string or nil, append to target page link
-- .suffix -- string or nil, append to shortcut list
-- Returns table row markup
local n, s, space, t, targets
local r = false
if type( arglist.targets ) == "table" then
targets = arglist.targets
n = #targets
else
n = 0
end
if n == 0 then
r = fallback( "sayNoPage", "No target page" )
else
if type( arglist.space ) == "string" then
space = mw.text.trim( arglist.space )
if space == "" then
space = false
end
end
if not space then
if n == 1 then
space, s = targets[ 1 ]:match( "^([^:]*):(.+)$" )
targets[ 1 ] = s
space = mw.text.trim( space )
if space == "" then
space = false
end
else
r = fallback( "sayNoNamespace", "No target namespace" )
end
end
if not r then
local o, nsn
if space then
o = mw.site.namespaces[ space ]
end
if o then
nsn = o.id
space = string.format( ":%s:", o.name )
else
nsn = 0
space = ":"
end
if type( Config.rooms ) == "table" then
for k, v in pairs( Config.rooms ) do
if v == nsn then
local l
s = string.format( "%s/%d", arglist.suite, nsn )
l, t = pcall( mw.loadData, s )
break -- for k, v
end
end -- for k, v
end
if type( t ) ~= "table" then
r = string.format( "%d (%s) * %s",
nsn,
space,
fallback( "sayNamespaceOff",
"Namespace not configured" )
)
end
end
end
if r then
r = string.format( "\n|-\n|%s", fault( r, false ) )
else
local shortcuts, story, suffix
if type( arglist.story ) == "string" then
story = arglist.story
end
if type( arglist.suffix ) == "string" then
suffix = arglist.suffix
end
r = ""
for i = 1, n do
s = mw.text.trim( targets[ i ] )
shortcuts = t[ s ]
s = string.format( "[[%s%s]]", space, s )
if story then
s = s .. story
end
if type( shortcuts ) == "string" then
if shortcuts:sub( 1, 2 ) ~= "[[" then
local got = faces( face( shortcuts ) )
shortcuts = ""
for k, v in pairs( got ) do
shortcuts = string.format( "%s, [[%s]]",
shortcuts, v )
end -- for k, v
shortcuts = shortcuts:sub( 3 )
end
if suffix then
shortcuts = shortcuts .. suffix
end
else
shortcuts = fault( fallback( "sayUnregistered",
"no shortcuts registered" ),
false )
end
r = r .. fiat( s, shortcuts )
end -- for i
end
return r
end -- folder()
local function follow( arglist )
-- Analyze list of shortcuts in single page context
-- arglist -- table, with parameters
-- .shortcuts -- string, comma separated list of shortcuts
-- .leave -- true, if dummy entry
-- Throws error with message, else returns string with text
local pages = { }
local shortcuts = face( arglist.shortcuts )
local style = Config.style
local got, r
if Config.show then
facilitated( "show" )
r = string.format( Config.show, shortcuts )
end
if Current.style and Config[ Current.style ] then
style = Config[ Current.style ]
elseif Current.rule then
got = type( Current.rule.styling )
if got == "boolean" then
style = false
elseif got == "string" then
style = Config[ Current.rule.styling ]
end
end
if style then
if not Config.light then
local s
facilitated( "style" )
if style:find( ".sub.", 8, true ) then
style = style:gsub( "%.sub%.", "-sub" )
end
if style:find( "-shortcut:", 16, true ) then
if Current.page.isSubpage then
s = "%1line-height: 0; top: -2.5em%2"
else
s = "%1top: -1em%2"
end
style = style:gsub( "([; '])%-shortcut:%s*top([; '])",
s )
s = "%sdata-shortcut-clear-right='1'"
if style:find( s ) then
e = mw.html.create( "div" )
e:css( { ["clear"] = "right",
["height"] = "0" } )
style = style:gsub( s, "" ) .. tostring( e )
end
end
r = string.format( style, r )
if r:find( "###", 16, true ) then
local k = shortcuts:find( ",", 3, true )
s = shortcuts
if k then
s = s:sub( 1, k - 1 )
end
r = r:gsub( "###", s, 1 )
end
end
else
r = ""
end
got = shortcuts:gsub( "<s>[^<]+</s>", "" )
:gsub( "<strike>[^<]+</strike>", "" )
got = faces( got )
for k, v in pairs( got ) do
if not arglist.leave then
flag( v, pages )
end
end -- for k, v
if Errors then
local s = "Shortcuts * "
local t
for k, v in pairs( Errors ) do
t = mw.title.new( k )
s = string.format( "%s <u>[%s %s]</u>: %s",
s,
t:fullUrl( { redirect = "no" } ),
k,
v )
end -- for k, v
r = r .. fault( s, true )
end
return r
end -- follow()
local function forward( args )
-- Perform task
-- args -- table, with parameters
-- .self -- string, target page
-- .shortcuts -- string, comma separated list of shortcuts
-- .style -- string, particular style ID
-- .loose -- boolean, ignore undefined shortcuts
-- Throws error with message, else returns string with text
local l, t, sub
local lucky = false
local r = false
if type( args.suite ) == "string" then
sub = args.suite .. "/config"
l, t = pcall( mw.loadData, sub )
if type( t ) == "table" then
Config = t
else
r = string.format( "'%s' invalid", sub )
end
else
r = "bad .suite"
end
if Config then
local leave = false
if args.self then
Current.self = args.self
Current.page = mw.title.new( Current.self )
if not Current.page.exists then
Current.page = false
r = string.format( "'%s' not found",
Current.self )
end
elseif args.service == "trows" then
lucky = true
elseif args.service == "total" then
Current.page = false
lucky = true
else
Current.page = mw.title.getCurrentTitle()
Current.self = Current.page.prefixedText
end
if Current.page then
Current.nsn = Current.page.namespace
if Current.nsn < 0 then
-- Special:Booksources
leave = true
lucky = true
else
if Current.nsn % 2 == 0 then
Current.nsns = Current.nsn
Current.nsnt = Current.nsn + 1
else
Current.nsnt = Current.nsn
Current.nsns = Current.nsn - 1
end
t = false
if type( Config.rooms ) == "table" then
for k, v in pairs( Config.rooms ) do
if v == Current.nsns then
sub = string.format( "%s/%d",
args.suite, v )
l, t = pcall( mw.loadData, sub )
break -- for k, v
end
end -- for k, v
end
if args.service == "template" then
if type( Config.rules ) == "table" then
local rules = Config.rules[ Current.nsn ]
if type( rules ) == "table" then
local seek = Current.page.text
local scope
for k, v in pairs( rules ) do
if type( v ) == "table" then
if type( v.sub ) ~= "string"
or mw.ustring.match( seek,
v.sub ) then
Current.rule.styling = v.styling
Current.rule.locally = v.locally
end
end
end -- for k, v
end
end
if type( t ) == "table" then
sub = Current.page.text
if type( t[ sub ] ) == "string" then
if args.shortcuts and
not Config.locally then
args.shortcuts = false
r = fallback( "sayNoLocals",
"'1=' not permitted" )
else
args.shortcuts = t[ sub ]
if Current.nsn == Current.nsnt then
args.shortcuts
= fair( args.shortcuts )
end
end
elseif Current.rule.locally then
args.shortcuts = false
end
elseif Current.rule.locally then
args.shortcuts = false
end
if type( args.shortcuts ) == "string" then
local suitable = Config.patternSuitable or ""
local syntactic = "[_#%{%}|]"
suitable = string.format( "^[ -~%s]+$",
suitable )
args.shortcuts = mw.text.trim( args.shortcuts )
if args.shortcuts == "" then
r = fallback( "sayNoShortcuts",
"no shortcuts" )
elseif Current.leave and
not args.shortcuts:find( "/" ) then
leave = true
elseif mw.ustring.match( args.shortcuts,
suitable ) and
not args.shortcuts:match( syntactic ) then
lucky = true
else
r = fallback( "sayInvalidChar",
"shortcut with invalid character" )
end
elseif Current.page.prefixedText == Config.skip then
args.shortcuts = "NS:PT"
args.leave = true
lucky = true
elseif args.loose then
leave = true
elseif Config.locally then
r = fallback( "sayUnknown",
"Shortcuts template:"
.. " page not registered,"
.. " '1=' missing" )
end
if not lucky and Current.rule.locally then
lucky = true
leave = true
end
elseif args.service ~= "trows" and
args.service ~= "total" then
r = "bad .service"
end
end
end
if lucky then
if leave then
r = "" -- NOOP
elseif args.service == "template" then
Current.style = args.style
r = follow( args )
elseif args.service == "trows" then
r = folder( args )
elseif args.service == "total" then
r = flash( args.suite, args.nsn )
end
end
end
if not lucky and r then
r = fault( r, true )
if args.service == "trows" then
r = fiat( r, false )
end
end
return r
end -- forward()
local function framed( frame, action )
-- #invoke call in template environment
-- action -- string, with keyword
-- Returns markup
local lucky, r
local params = { service = action,
suite = frame:getTitle() }
local pars = frame:getParent().args
if params.service == "template" then
params.loose = faculty( pars.loose )
params.shortcuts = pars[ 1 ]
if faculty( pars.light ) and
params.light and
frame.args.shortcut then
params.shortcuts = frame.args.shortcut
end
if params.shortcuts then
params.shortcuts = mw.text.trim( params.shortcuts )
if params.shortcuts == "" then
params.shortcuts = false
end
end
params.style = pars.style
elseif params.service == "trows" then
local k, v, s
local got = { }
for k, v in pairs( pars ) do
if type( k ) == "number" then
s = mw.text.trim( v )
if s ~= "" then
table.insert( got, s )
end
end
end -- for k, v
if #got > 0 then
params.targets = got
end
params.space = pars.space
params.story = pars.story
params.suffix = pars.suffix
elseif params.service == "total" then
params.nsn = pars[ 1 ]
if params.nsn and
params.nsn:match( "^(%d+)$" ) then
params.nsn = tonumber( params.nsn )
else
params.nsn = false
end
end
lucky, r = pcall( forward, params )
if not lucky then
r = fatal( r )
end
return r
end -- framed()
Failsafe.failsafe = function ( atleast )
-- Retrieve versioning and check for compliance
-- Precondition:
-- atleast -- string, with required version or "wikidata" or "~"
-- or false
-- Postcondition:
-- Returns string -- with queried version, also if problem
-- false -- if appropriate
local last = ( atleast == "~" )
local since = atleast
local r
if last or since == "wikidata" then
local item = Failsafe.item
since = false
if type( item ) == "number" and item > 0 then
local entity = mw.wikibase.getEntity( string.format( "Q%d",
item ) )
if type( entity ) == "table" then
local vsn = entity:formatPropertyValues( "P348" )
if type( vsn ) == "table" and
type( vsn.value ) == "string" and
vsn.value ~= "" then
if last and vsn.value == Failsafe.serial then
r = false
else
r = vsn.value
end
end
end
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 = { }
function p.template( frame )
return framed( frame, "template" ) or ""
end -- .template
function p.total( frame )
return framed( frame, "total" )
end -- .total
function p.trows( frame )
return framed( frame, "trows" )
end -- .trows
function p.twoletters( frame )
return framed( frame, "twoletters" )
end -- .twoletters
function p.test( args )
-- Debugging
-- args -- table, with arguments; mandatory:
-- .suite -- Module path
-- .service -- action mode, like "template"
-- .shortcuts -- list
-- .self -- (target) page name
local lucky, r = pcall( forward, args )
return r
end -- .test()
p.failsafe = function ( frame )
-- Versioning interface
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 -- .failsafe
p.Shortcuts = function ()
-- Module interface
return Shortcuts
end -- .Shortcuts
return p