Modul:Shortcuts

Vorlagenprogrammierung Diskussionen Lua Unterseiten
Modul Deutsch English

Modul: Dokumentation

Diese Seite enthält Code in der Programmiersprache Lua. Einbindungszahl Cirrus


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

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.