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>:&#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