Nicht angemeldeter Benutzer - Bearbeiten von Seiten ist nur als angemeldeter Benutzer möglich.

Modul:Shortcuts: Unterschied zwischen den Versionen

Aus imedwiki
Zur Navigation springen Zur Suche springen
(hotfix)
K (39 Versionen importiert: Import der Hilfe zu TemplateData aus der deutschen Wikipedia [https://de.wikipedia.org/w/index.php?title=Hilfe:TemplateData/JSON&oldid=189058032])
 
(38 dazwischenliegende Versionen von 3 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
--[=[ 2014-05-31
+
local Shortcuts = { suite  = "Shortcuts",
Module: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 globals
+
 
local Config
+
 
local Errors = false
+
local function face( adjust )
local Sort  = false
+
    -- Unlink string
 +
    --    adjust  -- string, with some shortcuts
 +
    -- Returns unlinked shortcuts
 +
    return adjust:gsub( "%[%[", "" )
 +
                :gsub( "%]%]", "" )
 +
end -- face()
  
  
Zeile 17: Zeile 30:
 
     -- Returns table with shortcut page names
 
     -- Returns table with shortcut page names
 
     local s = any:gsub( "<[^>]+>", "," )
 
     local s = any:gsub( "<[^>]+>", "," )
                :gsub( "%[%[", "" )
 
                :gsub( "%]%]", "" )
 
 
     local r = mw.text.split( s, "%s*,%s*" )
 
     local r = mw.text.split( s, "%s*,%s*" )
 
     for k, v in pairs( r ) do
 
     for k, v in pairs( r ) do
Zeile 60: Zeile 71:
 
     return r
 
     return r
 
end -- facet()
 
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()
  
  
Zeile 69: Zeile 93:
 
     --    assembly  -- table, collecting assignments
 
     --    assembly  -- table, collecting assignments
 
     -- Extending assembly
 
     -- Extending assembly
 +
    -- Returns number of target pages
 
     local space = mw.site.namespaces[ alone ]
 
     local space = mw.site.namespaces[ alone ]
 +
    local r = 0
 
     if space then
 
     if space then
 
         local got
 
         local got
Zeile 81: Zeile 107:
 
             end
 
             end
 
             for k, v in pairs( t ) do
 
             for k, v in pairs( t ) do
                 got = faces( v )
+
                 got = faces( face( v ) )
 
                 for i, s in pairs( got ) do
 
                 for i, s in pairs( got ) do
 
                     table.insert( assembly,  facet( space, k, s ) )
 
                     table.insert( assembly,  facet( space, k, s ) )
 
                 end    -- for i, s
 
                 end    -- for i, s
 +
                r = r + 1
 
             end    -- for k, v
 
             end    -- for k, v
 
         end
 
         end
 
     end
 
     end
 +
    return r
 
end -- factory()
 
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()
  
  
Zeile 98: Zeile 157:
 
     local r = " " .. above
 
     local r = " " .. above
 
     if type( Config.talks ) == "table" then
 
     if type( Config.talks ) == "table" then
         local s
+
         local seek, shift
 
         for k, v in pairs( Config.talks ) do
 
         for k, v in pairs( Config.talks ) do
             s = string.format( "%A(%s):", k )
+
             seek  = string.format( "(%%A?)(%s):", k )
             r = r:gsub( s, { v } )
+
            shift = string.format( "%%1%s:", v )
 +
             r = r:gsub( seek, shift )
 
         end    -- for k, v
 
         end    -- for k, v
 
     end
 
     end
 
     return mw.text.trim( r )
 
     return mw.text.trim( r )
 
end -- fair()
 
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()
  
  
Zeile 114: Zeile 211:
 
     --    absent  -- boolean, hide message, trigger category
 
     --    absent  -- boolean, hide message, trigger category
 
     -- Returns message with markup
 
     -- Returns message with markup
     local r = string.format( "<span class=\"error\">%s</span>", alert )
+
     local e = mw.html.create( "span" )
 +
                    :addClass( "error" )
 +
                    :wikitext( alert or "???" )
 +
    local r = tostring( e )
 
     if absent  and  type( Config ) == "table" then
 
     if absent  and  type( Config ) == "table" then
         local s = Config.suppress
+
         if type( Config.suppress ) == "string" then
        if type( s ) == "string" and  s:find( "%s", 1, true ) then
+
            facilitated( "suppress" )
             r = string.format( s, r )
+
             r = string.format( Config.suppress, r )
 
         end
 
         end
 
         if type( Config.scream ) == "string" then
 
         if type( Config.scream ) == "string" then
Zeile 126: Zeile 226:
 
     return r
 
     return r
 
end -- fault()
 
end -- fault()
 
 
 
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()
 
  
  
Zeile 157: Zeile 245:
 
local function first( a1, a2 )
 
local function first( a1, a2 )
 
     -- Compare a1 with a2 in reverse title order
 
     -- Compare a1 with a2 in reverse title order
     --    a1     -- table, with assignment
+
     --    a1 -- table, with assignment
     --    a2     -- table, with assignment
+
     --    a2 -- table, with assignment
 
     -- Returns true if a1 < a2
 
     -- Returns true if a1 < a2
 
     local r
 
     local r
Zeile 173: Zeile 261:
  
  
local function flag( arglist, alone, achieved )
+
local function flag( alone, achieved )
 
     -- Analyze one shortcut
 
     -- Analyze one shortcut
    --    arglist  -- table, with parameters
 
    --        .page  -- title object, of current page
 
    --        .nsn    -- number, of current namespace
 
    --        .nsns  -- number, of subject namespace
 
 
     --    alone    -- string, with shortcut page name
 
     --    alone    -- string, with shortcut page name
 
     --    achieved  -- table, with title objects; will be extended
 
     --    achieved  -- table, with title objects; will be extended
Zeile 185: Zeile 269:
 
     if page.exists then
 
     if page.exists then
 
         if page.isRedirect then
 
         if page.isRedirect then
             local story   = page:getContent()
+
             local story = page:getContent()
             local shift   = story:match( "^#[^%[]+%[%[(.+)%]%]" )
+
             local shift = story:match( "^#[^%[]+%[%[([^%]\n]+)%]%]" )
             local redirect = mw.title.new( shift )
+
             local redirect
             if mw.title.equals( arglist.page, redirect ) then
+
            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
 
                 for k, v in pairs( achieved ) do
 
                     if mw.title.equals( page, v ) then
 
                     if mw.title.equals( page, v ) then
                         failure( alone, "duplicated" )
+
                         failure( alone,
 +
                                fallback( "sayDuplicated",
 +
                                          "duplicated" ) )
 
                         page = false
 
                         page = false
 
                         break    -- for k, v
 
                         break    -- for k, v
Zeile 199: Zeile 294:
 
                     table.insert( achieved, page )
 
                     table.insert( achieved, page )
 
                 end
 
                 end
                 if arglist.nsn == arglist.nsns
+
                 if Current.nsns == Current.nsn
 +
                  and  not Current.leave
 
                   and  Config.signature
 
                   and  Config.signature
                   and  not story:find( Config.signature, 15, true ) then
+
                   and  not story:find( Config.signature, 15 ) then
                     local say
+
                     failure( alone,
                    if type( Config.signal ) == "string" then
+
                            fallback( "signal",
                        say = Config.signal
+
                                      ".signature (category) missing" )
                    else
+
                          )
                        say = ".signature (category) missing"
 
                    end
 
                    failure( alone, say )
 
 
                 end
 
                 end
 
             else
 
             else
                 failure( alone, "wrong target" )
+
                 failure( alone,
 +
                        fallback( "sayTarget", "wrong target" ) )
 
             end
 
             end
 
         else
 
         else
             failure( alone, "regular page" )
+
             failure( alone,  fallback( "sayRegular", "regular page" ) )
 
         end
 
         end
 
     else
 
     else
         failure( alone, "missing" )
+
         failure( alone,  fallback( "sayMissing", "missing" ) )
 
     end
 
     end
 
end -- flag()
 
end -- flag()
Zeile 227: Zeile 321:
 
     --    account  -- string, with module name
 
     --    account  -- string, with module name
 
     --    alone    -- number or false, with namespace limitation
 
     --    alone    -- number or false, with namespace limitation
     -- Returns table rows until end
+
     -- Returns table rows until end, terminate table and provide totals
 
     local r = "\n|}"
 
     local r = "\n|}"
 
     local collect = { }
 
     local collect = { }
 +
    local n, previous
 
     if type( alone ) == "number" then
 
     if type( alone ) == "number" then
         factory( account, alone, collect )
+
         n = factory( account, alone, collect )
 
     elseif type( Config.rooms ) == "table" then
 
     elseif type( Config.rooms ) == "table" then
 +
        n = 0
 
         for k, v in pairs( Config.rooms ) do
 
         for k, v in pairs( Config.rooms ) do
             factory( account, v, collect )
+
             n = n + factory( account, v, collect )
 
         end    -- for k, v
 
         end    -- for k, v
 
     else
 
     else
Zeile 252: Zeile 348:
 
             shortcut = string.format( "[[%s]]", v.shortcut )
 
             shortcut = string.format( "[[%s]]", v.shortcut )
 
             second  = string.format( "[[:%s]]", v.shift )
 
             second  = string.format( "[[:%s]]", v.shift )
             r       = fiat( shortcut, second ) .. r
+
            if v.shortcut == previous then
 +
                failure( previous,
 +
                        fallback( "sayDuplicated", "duplicated" ) )
 +
            end
 +
            previous = v.shortcut
 +
             r = fiat( shortcut, second ) .. r
 
         end    -- for k, v
 
         end    -- for k, v
 
     end
 
     end
 +
    r = string.format( "%s\n%d/%d", r, #collect, n )
 
     return r
 
     return r
 
end -- flash()
 
end -- flash()
Zeile 261: Zeile 363:
  
 
local function folder( arglist )
 
local function folder( arglist )
     -- Present table row
+
     -- Present table rows
 
     --    arglist  -- table, with parameters
 
     --    arglist  -- table, with parameters
     --        .self      -- string, target page
+
     --        .targets  -- table sequence, target pages specifications
     --        .story     -- string or nil, append to target page link
+
    --        .space    -- string or nil, namespace of all target pages
     --        .suffix     -- string or nil, append to shortcut list
+
     --        .story   -- string or nil, append to target page link
 +
     --        .suffix   -- string or nil, append to shortcut list
 
     -- Returns table row markup
 
     -- Returns table row markup
     local l, nsn, shortcuts, t
+
     local n, s, space, t, targets
     local space, subject = arglist.self:match( "^([^:]+):(.+)$" )
+
     local r = false
    if space then
+
    if type( arglist.targets ) == "table" then
         local o = mw.site.namespaces[ space ]
+
         targets = arglist.targets
        if o then
+
         n      = #targets
            nsn = o.id
 
         else
 
            nsn = 0
 
        end
 
 
     else
 
     else
         nsn = 0
+
         n = 0
 
     end
 
     end
     t = string.format( "%s/%d", arglist.suite, nsn )
+
     if n == 0 then
    l, t = pcall( mw.loadData, t )
+
        r = fallback( "sayNoPage", "No target page" )
    if type( t ) == "table" then
+
    else
         shortcuts = t[ subject ]
+
        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
 
     end
     subject = string.format( "[[:%s]]", arglist.self )
+
     if r then
     if type( arglist.story ) == "string" then
+
        r = string.format( "\n|-\n|%s", fault( r, false ) )
        subject = subject .. arglist.story
+
     else
    end
+
        local shortcuts, story, suffix
    if type( shortcuts ) == "string" then
+
        if type( arglist.story ) == "string" then
        if arglist.shortcuts:sub( 1, 2 ) ~= "[[" then
+
            story = arglist.story
            local got = faces( 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
 
         end
 
         if type( arglist.suffix ) == "string" then
 
         if type( arglist.suffix ) == "string" then
             shortcuts = shortcuts .. arglist.suffix
+
             suffix = arglist.suffix
 
         end
 
         end
    else
+
        r = ""
        shortcuts = fault( "no shortcuts", false )
+
        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
 
     end
     return fiat( subject, shortcuts )
+
     return r
 
end -- folder()
 
end -- folder()
  
Zeile 311: Zeile 478:
 
     -- Analyze list of shortcuts in single page context
 
     -- Analyze list of shortcuts in single page context
 
     --    arglist  -- table, with parameters
 
     --    arglist  -- table, with parameters
    --        .page      -- title object, of current page
 
 
     --        .shortcuts  -- string, comma separated list of shortcuts
 
     --        .shortcuts  -- string, comma separated list of shortcuts
 +
    --        .leave      -- true, if dummy entry
 
     -- Throws error with message, else returns string with text
 
     -- Throws error with message, else returns string with text
     local style = Config.style
+
    local pages    = { }
     local r
+
    local shortcuts = face( arglist.shortcuts )
     if type( style ) == "string" and  style:find( "%s", 1, true ) then
+
     local style     = Config.style
         local pages = { }
+
     local got, r
         local got   = faces( arglist.shortcuts )
+
     if Config.show then
         r = string.format( style, arglist.shortcuts )
+
        facilitated( "show" )
        for k, v in pairs( got ) do
+
        r = string.format( Config.show, shortcuts )
--           flag( arglist, v, pages )
+
    end
        end    -- for k, v
+
    if Current.style and  Config[ Current.style ] then
        if Errors then
+
         style = Config[ Current.style ]
            local s = ""
+
    elseif Current.rule then
            for k, v in pairs( Errors ) do
+
         got = type( Current.rule.styling )
                 s = string.format( "%s [[%s]]:&#160;%s", s, k, v )
+
         if got == "boolean" then
            end    -- for k, v
+
            style = false
            r = r .. fault( s, true )
+
        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
 
         end
 
     else
 
     else
         error( "/config 'style' invalid" )
+
         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>:&#160;%s",
 +
                              s,
 +
                              t:fullUrl( { redirect = "no" } ),
 +
                              k,
 +
                              v )
 +
        end    -- for k, v
 +
        r = r .. fault( s, true )
 
     end
 
     end
 
     return r
 
     return r
Zeile 343: Zeile 566:
 
     --        .self      -- string, target page
 
     --        .self      -- string, target page
 
     --        .shortcuts  -- string, comma separated list of shortcuts
 
     --        .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
 
     -- Throws error with message, else returns string with text
 
     local l, t, sub
 
     local l, t, sub
Zeile 359: Zeile 584:
 
     end
 
     end
 
     if Config then
 
     if Config then
 +
        local leave = false
 
         if args.self then
 
         if args.self then
             args.page = mw.title.new( args.self )
+
             Current.self = args.self
             if not args.page.exists then
+
            Current.page = mw.title.new( Current.self )
                 args.page = false
+
             if not Current.page.exists then
                 r         = string.format( "'%s' not found", args.self )
+
                 Current.page = false
 +
                 r           = string.format( "'%s' not found",
 +
                                              Current.self )
 
             end
 
             end
         elseif args.service == "trow" then
+
         elseif args.service == "trows" then
             r = "'1=' missing"
+
             lucky = true
 
         elseif args.service == "total" then
 
         elseif args.service == "total" then
             args.page = false
+
             Current.page = false
             lucky     = true
+
             lucky       = true
 
         else
 
         else
             args.page = mw.title.getCurrentTitle()
+
             Current.page = mw.title.getCurrentTitle()
             args.self = args.page.prefixedText
+
             Current.self = Current.page.prefixedText
 
         end
 
         end
         if args.page then
+
         if Current.page then
             args.nsn = args.page.namespace
+
             Current.nsn = Current.page.namespace
             if args.nsn % 2 == 0 then
+
             if Current.nsn < 0 then
                 args.nsns = args.nsn
+
                 -- Special:Booksources
                 args.nsnt = args.nsn + 1
+
                leave = true
 +
                 lucky = true
 
             else
 
             else
                 args.nsnt = args.nsn
+
                 if Current.nsn % 2 == 0 then
                 args.nsns = args.nsn - 1
+
                    Current.nsns = Current.nsn
            end
+
                    Current.nsnt = Current.nsn + 1
            sub = string.format( "%s/%d", args.suite, args.nsns )
+
                 else
            l, t = pcall( mw.loadData, sub )
+
                    Current.nsnt = Current.nsn
            if type( t ) == "table" then
+
                    Current.nsns = Current.nsn - 1
                sub = args.page.text
+
                end
                if type( t[ sub ] ) == "string" then
+
                t = false
                    if args.shortcuts  and  not Config.locally then
+
                if type( Config.rooms ) == "table" then
                        args.shortcuts = false
+
                    for k, v in pairs( Config.rooms ) do
                        r              = "'1=' not permitted"
+
                        if v == Current.nsns then
                    else
+
                            sub = string.format( "%s/%d",
                        args.shortcuts = t[ sub ]
+
                                                  args.suite, v )
                        if args.nsn == args.nsnt then
+
                            l, t = pcall( mw.loadData, sub )
                            args.shortcuts = fair( args.shortcuts )
+
                            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
 
                     end
 
                     end
                end
+
                    if type( t ) == "table" then
            end
+
                        sub = Current.page.text
            if args.service == "template" then
+
                        if type( t[ sub ] ) == "string" then
                if type( args.shortcuts ) == "string" then
+
                            if args.shortcuts and
                    args.shortcuts = mw.text.trim( args.shortcuts )
+
                              not Config.locally then
                    if args.shortcuts == "" then
+
                                args.shortcuts = false
                         r = "no shortcuts"
+
                                r              = fallback( "sayNoLocals",
                     else
+
                                                  "'1=' not permitted" )
                         lucky = true
+
                            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
 
                     end
                elseif Config.locally then
+
                    if type( args.shortcuts ) == "string" then
                     if args.page == Config.skip 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.shortcuts = "NS:PT"
 +
                        args.leave    = true
 
                         lucky          = true
 
                         lucky          = true
                     else
+
                     elseif args.loose then
                         r = "Shortcuts template '1=' missing"
+
                        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
 
                     end
 +
                elseif args.service ~= "trows"  and
 +
                      args.service ~= "total" then
 +
                    r = "bad .service"
 
                 end
 
                 end
            elseif args.service == "trow"  or
 
                  args.service == "total" then
 
                lucky = true
 
            else
 
                r = "bad .service"
 
 
             end
 
             end
 
         end
 
         end
 
         if lucky then
 
         if lucky then
             if args.service == "template" then
+
             if leave then
 +
                r = ""  -- NOOP
 +
            elseif args.service == "template" then
 +
                Current.style  = args.style
 
                 r = follow( args )
 
                 r = follow( args )
             elseif args.service == "trow" then
+
             elseif args.service == "trows" then
 
                 r = folder( args )
 
                 r = folder( args )
 
             elseif args.service == "total" then
 
             elseif args.service == "total" then
Zeile 432: Zeile 721:
 
         end
 
         end
 
     end
 
     end
     if not lucky then
+
     if not lucky and r then
 
         r = fault( r, true )
 
         r = fault( r, true )
         if args.service == "trow" then
+
         if args.service == "trows" then
 
             r = fiat( r, false )
 
             r = fiat( r, false )
 
         end
 
         end
Zeile 452: Zeile 741:
 
     local pars  = frame:getParent().args
 
     local pars  = frame:getParent().args
 
     if params.service == "template" then
 
     if params.service == "template" then
 +
        params.loose    = faculty( pars.loose )
 
         params.shortcuts = pars[ 1 ]
 
         params.shortcuts = pars[ 1 ]
     elseif params.service == "trow" then
+
        if faculty( pars.light )  and
         params.self  = pars[ 1 ]
+
          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.story  = pars.story
 
         params.suffix = pars.suffix
 
         params.suffix = pars.suffix
Zeile 467: Zeile 782:
 
     end
 
     end
 
     lucky, r = pcall( forward, params )
 
     lucky, r = pcall( forward, params )
 +
    if not lucky then
 +
        r = fatal( r )
 +
    end
 
     return r
 
     return r
 
end -- framed()
 
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()
  
  
Zeile 476: Zeile 837:
  
 
function p.template( frame )
 
function p.template( frame )
     return  framed( frame, "template" )
+
     return  framed( frame, "template" ) or  ""
end -- p.template
+
end -- .template
  
 
function p.total( frame )
 
function p.total( frame )
 
     return  framed( frame, "total" )
 
     return  framed( frame, "total" )
end -- p.total
+
end -- .total
  
function p.trow( frame )
+
function p.trows( frame )
     return  framed( frame, "trow" )
+
     return  framed( frame, "trows" )
end -- p.trow
+
end -- .trows
  
 
function p.twoletters( frame )
 
function p.twoletters( frame )
 
     return  framed( frame, "twoletters" )
 
     return  framed( frame, "twoletters" )
end -- p.twoletters
+
end -- .twoletters
  
 
function p.test( args )
 
function p.test( args )
Zeile 500: Zeile 861:
 
     local lucky, r = pcall( forward, args )
 
     local lucky, r = pcall( forward, args )
 
     return r
 
     return r
end -- p.test()
+
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
 
return p

Aktuelle Version vom 6. November 2019, 10:26 Uhr

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>:&#160;%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