Zum Inhalt springen

Modul:Vorlage:Phab

Us der alemannische Wikipedia, der freie Dialäkt-Enzyklopedy

Die Dokumentation für dieses Modul kann unter Modul:Vorlage:Phab/Doku erstellt werden

local Phab = { suite  = "Phab",
               serial = "2021-01-01",
               sick   = "Category:Wikipedia:Vorlagenfehler/Vorlage:Phab",
               site   = "https://phabricator.wikimedia.org/",
               item   = 97313076 }
--[=[
{{Template:Phab}}
]=]
local Failsafe = Phab



local function concatKeys( args )
    -- Concatenate keys
    -- Precondition:
    --     args   -- table; assignments
    local r
    for k in pairs( args ) do
        if r then
            r = r .. " "
        else
            r = ""
        end
        r = r .. tostring( k )
    end -- for k
    return r
end -- concatKeys()



local function file( args )
    -- Link to a File
    -- Precondition:
    --     args   -- table; assignments
    --                      .File
    --                      .Show
    --                      .style
    -- Postcondition:
    --     Returns string with bracketed external link, if fine
    --     Throws error on failure
    local r = args.File
    local j = r:find( ",", 1, true )
    local n, params, show
    if j then
        params = r:sub( j + 1 )
        r      = r:sub( 1,  j - 1 )
        -- params = mw.text.split( split, "%s*,%s*" )
        -- , width=, height=, size=
        -- , layout, float, alt
    end
    n = r:match( "^[Ff]?([0-9]+)%s*$" )
    if n then
        n = tonumber( n )   -- discard leading zeros
    else
        r = string.format( "Invalid format: 'File=%s'", r )
        error( r, 0 )
    end
    if args.style then
        if args.Show then
            show = args.Show
        else
            show = string.format( "F%d", n )
        end
        show = string.format( "<span style='%s'>%s</span>",
                              args.style, show )
    end
    if show then
        r = string.format( "[[phab:F%d|%s]]", n, show )
    else
        r = string.format( "[[phab:F%d]]", n )
    end
    return r
end -- file()



local function git( action, assembly, adopt, attach, anchor, against )
    -- Create URL for mediawiki GIT since 2013-06-06
    -- Precondition:
    --     action    -- string; kind of request
    --     assembly  -- string; project
    --     adopt     -- string; branch
    --     attach    -- string; file or directory (or empty)
    --     anchor    -- string; line number (or empty)
    --     against   -- string; diff ./. previous ID (or empty)
    -- Postcondition:
    --     Return URL
    -- Uses:
    --     mw.uri.encode()
    local r = "https://git.wikimedia.org/"
    if action == "file" then
        r = r .. "blob"
        if against then
            r = r .. "diff"
        end
    elseif action == "dir" then
        r = r .. "tree"
    elseif action == "plain" then
        r = r .. "raw"
    elseif action == "commit" or
           action == "commitdiff" or
           action == "history" then
        r = r .. action
    end
    r = r .. "/" .. mw.uri.encode( assembly ) .. "/" .. adopt
    if attach then
        r = r .. "/" .. mw.uri.encode( attach, "PATH" )
    end
    if anchor then
        r = r .. "#L" .. anchor
    end
    return r
end -- git()



local function id40( ask )
    -- Check for 40 hex lowercase id
    -- Precondition:
    --     ask  -- string; project
    -- Postcondition:
    --     Throws error, if failed
    if #ask ~= 40 or ask:match( "^[0-9a-f]+$" ) ~= ask then
        local e = "Invalid identifier: " .. ask
        error( e,  0 )
    end
end -- id40()



local function intersect( args, alone )
    -- Find elements occurring in both args and alone
    -- Precondition:
    --     args   -- table; assignments
    --     alone  -- table (sequence)
    -- Postcondition:
    --     Return table with sequence of matching elements, or false
    local r = { }
    local i
    for k, v in pairs( args ) do
        for i = 1, #alone do
            if k == alone[ i ] then
                table.insert( r, k )
            end
        end -- for i
    end -- for k, v
    if #r == 0 then
        r = false
    end
    return r
end -- intersect()



local function lonely( args, alone )
    -- Find elements occurring in both args and alone
    -- Precondition:
    --     args   -- table; assignments
    --     alone  -- table (sequence)
    -- Postcondition:
    --     Throws error, if occurring in both
    --     Return single matching element, or false
    -- Uses:
    --     intersect()
    --     mw.text.listToText()
    local r = intersect( args, alone )
    if r then
        if #r > 1 then
            r = "Must not be used together: ''"
                .. mw.text.listToText( r,  ", ",  " " ) .. "''"
            error( r, 0 )
        end
        r = r[ 1 ]
    end
    return r
end -- lonely()



local function mock( args )
    -- Link to a Mock
    -- Precondition:
    --     args   -- table; assignments
    --                      .Mock
    --                      .Show
    --                      .style
    -- Postcondition:
    --     Returns string with bracketed external link, if fine
    --     Throws error on failure
    local r = args.Mock
    local n = r:match( "^[Mm]?([0-9]+)$" )
    local show
    if n then
        n = tonumber( n )   -- discard leading zeros
    else
        r = string.format( "Invalid format: 'Mock=%s'", r )
        error( r, 0 )
    end
    if args.style then
        if args.Show then
            show = args.Show
        else
            show = string.format( "M%d", n )
        end
        show = string.format( "<span style='%s'>%s</span>",
                              args.style, show )
    end
    if show then
        r = string.format( "[[phab:M%d|%s]]", n, show )
    else
        r = string.format( "[[phab:M%d]]", n )
    end
    return r
end -- mock()



local function phabAssigned( frame )
    -- Retrieve table with callsign assignments
    -- Precondition:
    --     frame   -- object or nil
    -- Postcondition:
    --     Return table or nil
    --     Throws error on failure
    -- Uses:
    --     mw.getCurrentFrame()
    --     mw.loadData()
    local got, lucky, r, s
    if not frame then
        frame = mw.getCurrentFrame()
    end
    s = frame:getTitle() .. "/callsigns"
    lucky, got = pcall( mw.loadData, s )
    if type( got ) == "table" then
        r = got
    else
        error( "Invalid: " .. s,  0 )
    end
    return r
end -- phabAssigned()



local function phabBoards( args )
    -- Link to a board, project, team: tag
    -- Precondition:
    --     args   -- table; assignments
    --                      .Engage
    --                      .Show
    --                      .style
    --               string; single ID
    -- Postcondition:
    --     Returns string URL, if fine
    local s = type( args )
    local n, r, show, tags
    if s == "table" then
        if type( args.Engage ) == "string" then
            tags = args.Engage
        end
        if args.Show and ( args.Show ~= "" ) then
            show = args.Show
        end
    elseif s == "string" then
        tags = args
    end
    if not tags  or  tags == "" then
        tags = "MediaWiki-General-or-Unknown"
    end
    tags = mw.text.split( tags, "%s+" )
    n    = #tags
    if show  and  n > 1 then
        show = false
    end
    for i = 1, n do
        s = tags[ i ]
        if r then
            r = r .. " | "
        else
            r = ""
        end
        if not show then
            show = "#" .. s
        end
        if args.style then
            show = string.format( "<span style='%s'>%s</span>",
                                  args.style, show )
        end
        r = string.format( "%s[[phab:tag/%s|%s]]",
                           r, s, show )
    end -- for i
    return r
end -- phabBoards()



local function phabCallsigns( args, frame )
    -- Create wikitable of all callsigns
    -- Precondition:
    --     args   -- table; options
    --     frame  -- object or nil
    -- Uses:
    --     phabAssigned()
    local connect = phabAssigned( frame )
    local r
    if connect then
        local limit     = ( args.callsigns == "0" )
        local callsigns = { }
        local order     = { }
        local sub       = "phab:diffusion/"
        local details, pages, s, sign, support
        r = "{| class='wikitable sortable'\n" ..
            "|- class='hintergrundfarbe6'\n" ..
            "! Callsign !! ''repository'' !!class='unsortable'| Infos\n"
        for k, v in pairs( connect ) do
            if k:find( "/", 3, true ) then
                -- GIT legacy
                v = false
            elseif limit  and  type( v ) == "string" then
                details = connect[ v ]
                if type( details ) == "table"   and
                   type( details.list ) == "boolean"   and
                   not details.list then
                    v = false
                end
            end
            if type( v ) == "string" then
                s = callsigns[ v ]
                if s then
                    if #s < #k then
                        callsigns[ v ] = k
                    end
                else
                    callsigns[ v ] = k
                    table.insert( order, v )
                end
            end
        end -- for k, v
        table.sort( order )
        for i = 1, #order do
            sign  = order[ i ]
            show  = callsigns[ sign ]
            r     = string.format( "%s|- id='%s'\n|[[%s%s|%s]]||%s\n",
                                   r, sign, sub, sign, sign, show )
            pages = { }
            details = connect[ sign ]
            if type( details ) ~= "table" then
                details = { }
            end
            if details.say then
                table.insert( pages, details.say )
            end
            if details.support then
                table.insert( pages,
                              string.format( "[[%s]]",
                                             details.support ) )
            end
            if type( details.tags ) == "table" then
                for k, v in pairs( details.tags ) do
                    table.insert( pages,  phabBoards( v ) )
                end -- for k, v
            end
            support = details.mwPage
            if not support then
                s = show:match( "^extension%-([%w_]+)$" )
                if s then
                    support = "Extension:" .. s
                end
            end
            if support  and  support ~= "" then
                table.insert( pages,
                              string.format( "[[mw:%s]]", support ) )
            end
            if #pages > 0 then
                r = string.format( "%s|%s\n",
                                   r,  table.concat( pages, "<br>" ) )
            end
        end -- for i
        r = string.format( "%s|}\n%d ",
                           r, #order )
    end
    return r
end -- phabCallsigns()



local function phabDiffusion( action, assembly, adopt, attach,
                              anchor, against, as, frame )
    -- Create path for Phabricator Diffusion since 2015-03
    -- Precondition:
    --     action    -- string; kind of request
    --     assembly  -- string; project
    --     adopt     -- string; branch
    --     attach    -- string; file or directory (or empty)
    --     anchor    -- string; line number (or empty)
    --     against   -- string; diff ./. previous ID (or empty)
    --     as        -- string; s  = query
    --     frame     -- object or nil
    -- Postcondition:
    --     Return        1  -- URL, if identified, or nil
    --                   2  -- callsign, or nil
    -- Uses:
    --     phabAssigned()
    local translate = phabAssigned( frame )
    local r, shortcut, sub
    if translate then
        if assembly:match( "^%u%u%u?%u?$" ) then
            shortcut = assembly
        elseif assembly:match( "^R%d+$" ) then
            shortcut = assembly
            sub      = "source/" .. shortcut
        else
            shortcut = translate[ assembly ]
            if not shortcut  and  assembly:find( "/", 3, true ) then
                -- GIT legacy
                local s = assembly:gsub( "mediawiki/extensions/",
                                         "extension-" )
                shortcut = translate[ s ]
            end
        end
        if shortcut then
            local state, swift
            if adopt ~= "master"  and  adopt ~= "HEAD" then
                state = adopt
            end
            if attach and  not state then
                state = "master"
            end
            if state then
                if action then
                    if action == "commit"   or
                       action == "commitdiff" then
                        if attach then
                            if action == "commit" then
                                swift = "browse"
                            else
                                swift = "change"
                            end
                            state = "master"
                        else
                            sub = string.format( "r%s%s",
                                                 shortcut, state )
                        end
                    elseif action == "history" then
                        swift = action
                    else
                        swift = "browse"
                    end
                else
                    swift = "browse"
                end
            end
            if not sub then
                sub = "diffusion/" .. shortcut
                if swift then
                    sub = string.format( "%s/%s", sub, swift )
                end
                if state then
                    local sep
                    if state == "master" then
                        sep = "/"
                    else
                        sep = ""
                    end
                    sub = string.format( "%s%s%s", sub, sep, state )
                end
                if attach then
                    sub = string.format( "%s/%s", sub, attach )
                    if adopt
                       and  adopt ~= "HEAD"
                       and  state ~= adopt then
                        sub = string.format( "%s;%s", sub, adopt )
                    end
                    if anchor then
                        sub = string.format( "%s$%s", sub, anchor )
                    end
                end
                if as then
                    sub = string.format( "%s?%s", sub, as )
                end
            end
            r = Phab.site .. sub:gsub( " ", "%%20" )
        end
    end
    return r, shortcut
end -- phabDiffusion()



local function phabUser( args, frame )
    -- Link to a user
    -- Precondition:
    --     args   -- table; assignments
    --                      .User
    --                      .Show
    --                      .style
    --     frame  -- object or nil
    -- Postcondition:
    --     Returns string URL, if fine
    -- Uses:
    --     mw.text.trim()
    local r
    local single = args.User
    if type( single ) == "string" then
        single = mw.text.trim( single )
    end
    if type( single ) == "string"  and  single ~= "" then
        local show = args.Show
        if not show  or  show == "" then
            show = "@" .. single
        end
        if not frame then
            frame = mw.getCurrentFrame()
        end
        if args.style then
            show = string.format( "<span style='%s'>%s</span>",
                                  args.style, show )
        end
        r = string.format( "[[phab:p/%s|%s]]",
                           frame:callParserFunction( "urlencode",
                                                     single ),
                           show )
    else
        r = string.format( "Missing User :-(" )
        error( r, 0 )
    end
    return r
end -- phabUser()



local function sourcing( args, frame )
    -- Link to a code source, directory, branch or related
    -- Precondition:
    --     args   -- table; assignments
    --     frame  -- object or nil
    -- Postcondition:
    --     Returns string, if fine
    --     Throws error on failure
    -- Uses:
    --     lonely()
    --     id40()
    --     phabDiffusion()
    --     git()
    local jump   = false
    local last   = false
    local query  = false
    local scope  = "mediawiki/core"
    local show
    local since  = false
    local slot   = "HEAD"
    local source = false
    local swift  = "dir"
    local lapsus, load, r
    r = lonely( args,
                { "branch", "commit", "commitdiff", "diff" } )
    if r then
        slot = args[ r ]
    end
    r = lonely( args,  { "dir", "file", "plain" } )
    if r then
        swift  = r
        source = args[ swift ]
        if args.file then
            local suffix = args.file:match( "%.(%l+)$" )
            if suffix then
                if suffix == "svg" then
                    query = "as=source&highlight=xml&"
                elseif suffix == "json" or
                       suffix == "txt" or
                       suffix == "xml" then
                    query = "?"
                end
                if query then
                    query = query .. "blame=off"
                end
                if suffix == "md" then
                    query = "as=remarkup"
                end
            end
        end
    end
    r = lonely( args,  { "commit", "commitdiff" } )
    if r then
        swift = r
        load  = true
    end
    if args.history then
        last = true
    end
    if args.project then
        scope = args.project
    end
    if args.line or args.diff then
        if not args.file then
            r = " valid for ''file'' only."
            if args.line then
                r = "Number ''line''" .. r
                if args.diff then
                    r = " / " .. r
                end
            end
            if args.diff then
                r = "Version ''diff''" .. r
            end
            error( r, 0 )
        elseif args.line then
            if args.diff then
                r = "No ''line'' on ''diff'' page available"
                error( r, 0 )
            end
            jump = args.line
        end
    end
    if load then
        if scope == "SVN" then
            r = "Invalid SVN identifier: "
            if slot:match( "^[0-9]+$" ) then
                slot = tonumber( slot )
                if slot >= 1  and  slot <= 115794 then
                    r = false
                end
                slot = string.format( "%d", slot )
            end
            if r then
                local e = r .. slot
                error( e,  0 )
            end
        else
            id40( slot )
        end
    elseif args.diff then
        since = args.diff
        id40( since )
    elseif last then
        swift = "history"
    end
    r, show = phabDiffusion( swift, scope, slot, source, jump, since,
                             query, frame )
    if args.title and ( args.title ~= "" ) then
        show = args.title
    elseif last and source then
        show = "history: " .. source
    elseif source then
        show = source
        if args.diff then
            show = string.format( "%s ./.%s", show, since:sub( 1, 7 ) )
        elseif load then
            show = string.format( "%s;%s", show, slot:sub( 1, 7 ) )
        end
    elseif load then
        if show then
            show   = "r" .. show
        else
            show = "GIT:"
        end
        show = show .. slot:sub( 1, 7 )
    else
        show = scope .. "/*"
    end
    if r then
        r = string.format( "[%s %s]", r, show )
    else
        if not scope:find( "/", 3, true ) then
            error( "Unknown project: " .. scope,  0 )
        end
        lonely( args,
                { "commit", "commitdiff", "dir", "file", "plain" } )
        r = git( swift, scope, slot, source, jump, since )
        r = string.format( "[%s %s]", r, show )
        lapsus = true
    end
    if lapsus then
        r = string.format( "%s[[%s/Diffusion]]",
                           r, Phab.sick )
    end
    return r
end -- sourcing()



local function task( args )
    -- Link to a Task
    -- Precondition:
    --     args   -- table; assignments
    --                      .Task
    --                      .Anchor
    --                      .Show
    --                      .style
    -- Postcondition:
    --     Returns string with bracketed external link, if fine
    --     Throws error on failure
    local r   = args.Task
    local j   = r:find( "#", 1, true )
    local sub = args.Anchor
    local n, show
    if j then
        local scroll = r:sub( j + 1 )
        r      = r:sub( 1,  j - 1 )
        scroll = scroll:match( "^([0-9]+)$" )
        if scroll  and  not sub then
            sub = scroll
        end
    end
    n = r:match( "^[Tt]?([0-9]+)%s*$" )
    if n then
        n = tonumber( n )   -- discard leading zeros
    else
        r = string.format( "Invalid format: 'Task=%s'", r )
        error( r, 0 )
    end
    if sub then
        sub = string.format( "#%s", sub )
    else
        sub = ""
    end
    if args.Show then
        show = args.Show
    else
        show = string.format( "phab:T%d", n )
        if args.Anchor then
            show = string.format( "%s&#160;#%s", show, args.Anchor )
        end
    end
    if args.style then
        show = string.format( "<span style='%s'>%s</span>",
                              args.style, show )
    end
    r = string.format( "[[phab:T%d%s|%s]]", n, sub, show )
    return r
end -- task()



local function taskBugzilla( args )
    -- Link to an old Bugzilla ticket
    -- Precondition:
    --     args   -- table; assignments
    -- Postcondition:
    --     Returns string, if fine
    --     Throws error on failure
    -- Uses:
    --     task()
    local r = args.Bugzilla
    local j = r:find( "#", 1, true )
    local item, n, show
    if j then
        local scroll = r:sub( j + 1 )
        r      = r:sub( 1,  j - 1 )
        scroll = scroll:match( "^[cC]?([0-9]+)$" )
        if scroll then
            item = tonumber( scroll )
        end
    end
    n = r:match( "^([0-9]+)%s*$" )
    if n then
        n = tonumber( n )   -- discard leading zeros
        if n <= 73681 then
            args.Task = tostring( n + 2000 )
        else
            n = false
        end
    end
    if not n then
        r = string.format( "Invalid number: 'Bugzilla=%s'", r )
        error( r, 0 )
    end
    r = string.format( "https://old-bugzilla.wikimedia.org/%s%d",
                       "show_bug.cgi?id=", n )
    show = string.format( "Bugzilla:%d", n )
    if item then
        r    = string.format( "%s#c%d", r, item )
        show = string.format( "%s&nbsp;#c%d", show, item )
    end
    r = string.format( "[%s %s]", r, show )
    r = string.format( "%s <small style=\"font-weight:normal\">(%s)</small>",
                       task( args ),  r )
    return r
end -- taskBugzilla()



local function unified( args, frame )
    -- Link to a management issue
    -- Precondition:
    --     args   -- table; assignments
    -- Postcondition:
    --     Returns string, if fine
    --     Throws error on failure
    -- Uses:
    --     lonely()
    --     taskBugzilla()
    --     task()
    --     phabBoards()
    --     phabUser()
    local r
    lonely( args,
            { "Bugzilla", "Countdown", "Differential", "Engage",
              "File", "Gerrit", "Join", "Mock", "Paste", "Review",
              "Task", "User" } )
    if args.Bugzilla then
        r = taskBugzilla( args )
    elseif args.Engage then
        r = phabBoards( args )
    elseif args.File then
        r = file( args )
    elseif args.Mock then
        r = mock( args )
    elseif args.Task then
        r = task( args )
    elseif args.User then
        r = phabUser( args, frame )
    else
        r = "NOT YET READY"
        error( r, 0 )
    end
    --    Countdown
    --    Differential
    --    Gerrit
    --    Join
    --    Paste
    --    Review
    --    Leerzeichen-getrennte Liste
    return r
end -- unified()



local function main( args, frame )
    -- Do the job
    -- Precondition:
    --     args   -- table; assignments
    --     frame  -- object or nil
    -- Postcondition:
    --     Returns string with link, if fine
    --     Throws error on failure
    -- Uses:
    --     unified()
    --     sourcing()
    local defaults = { Bugzilla = "",
                       Engage   = true,
                       User     = true }
    local got    = { }
    local mode   = 0
    local params = { style = 0,
                     Anchor       = 1,
                     Anchor       = 1,
                     Bugzilla     = 1,
                     Countdown    = 1,
                     Differential = 1,
                     Engage       = 1,
                     File         = 1,
          --         Gerrit       = 1,
                     Join         = 1,
                     Mock         = 1,
                     Paste        = 1,
                     Review       = 1,
                     Show         = 1,
                     Task         = 1,
                     User         = 1,
                     branch     = 2,
                     commit     = 2,
                     commitdiff = 2,
                     diff       = 2,
                     dir        = 2,
                     file       = 2,
                     history    = 2,
                     line       = 2,
                     plain      = 2,
                     project    = 2,
                     title      = 2,
                     callsigns = 3 }
    local gr, s
    for k, v in pairs( args ) do
        mode = params[ k ]
        if not mode then
            mode = -1
        end
        if not got[ mode ] then
            got[ mode ] = { }
        end
        got[ mode ][ k ] = v
    end -- for k, v
    r = got[ -1 ]
    if r then
        if #r == 1 then
            s = ""
        else
            s = "s"
        end
        r = string.format( "Unknown parameter%s: '%s'",
                           s,  concatKeys( r ) )
        error( r, 0 )
    elseif got[ 1 ] and got[ 2 ] then
        r = string.format( "'%s' conflicting with '%s'",
                           concatKeys( got[ 1 ] ),
                           concatKeys( got[ 2 ] ) )
        error( r, 0 )
    else
        local procs  = { unified, sourcing, phabCallsigns }
        if mode == 0 then
            mode = 2
            got[ 2 ] = { }
        end
        if got[ 0 ] then
            for k, v in pairs( got[ 0 ] ) do
                got[ mode ][ k ] = v
            end -- for k, v
        end
        got = got[ mode ]
        for k, v in pairs( got ) do
            s = type( v )
            if s == "number" then
                v = tostring( v )
            elseif s == "string" then
                if #v == 0 then
                    v = defaults[ k ]
                end
            elseif v == true then
                v = defaults[ k ]
            else
                v = nil
            end
            if v then
                got[ k ] = v
            else
                got[ k ] = nil
            end
        end -- for k, v
        r = procs[ mode ]( got, frame )
    end
    return r
end -- main()



Failsafe.failsafe = function ( atleast )
    -- Retrieve versioning and check for compliance
    -- Precondition:
    --     atleast  -- string, with required version
    --                         or wikidata|item|~|@ or false
    -- Postcondition:
    --     Returns  string  -- with queried version/item, also if problem
    --              false   -- if appropriate
    -- 2020-08-17
    local since = atleast
    local last    = ( since == "~" )
    local linked  = ( since == "@" )
    local link    = ( since == "item" )
    local r
    if last  or  link  or  linked  or  since == "wikidata" then
        local item = Failsafe.item
        since = false
        if type( item ) == "number"  and  item > 0 then
            local suited = string.format( "Q%d", item )
            if link then
                r = suited
            else
                local entity = mw.wikibase.getEntity( suited )
                if type( entity ) == "table" then
                    local seek = Failsafe.serialProperty or "P348"
                    local vsn  = entity:formatPropertyValues( seek )
                    if type( vsn ) == "table"  and
                       type( vsn.value ) == "string"  and
                       vsn.value ~= "" then
                        if last  and  vsn.value == Failsafe.serial then
                            r = false
                        elseif linked then
                            if mw.title.getCurrentTitle().prefixedText
                               ==  mw.wikibase.getSitelink( suited ) then
                                r = false
                            else
                                r = suited
                            end
                        else
                            r = vsn.value
                        end
                    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.test( a )
    local lucky, r = pcall( main, a )
    return r
end

function p.f( frame )
    local lucky, r = pcall( main, frame:getParent().args, frame )
    if not lucky then
        r = string.format( "<span class=\"error\">%s</span>[[%s]]",
                           r, Phab.sick )
    end
    return r
end

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 -- p.failsafe

return p