Modul:WLink

Aus GurkiPedia
Version vom 21. Juni 2024, 22:06 Uhr von Tin (Diskussion | Beiträge) (1 Version importiert)

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

local WLink = { suite   = "WLink",
                serial  = "2022-05-09",
                item    = 19363224,
                globals = { URLutil = 10859193 } };

--[=[
ansiPercent()
formatURL()
getArticleBase()
getBaseTitle()
getEscapedTitle()
getExtension()
getFile()
getFragment()
getLanguage()
getLinktextProblem()
getNamespace()
getNamespaced()
getPlain()
getProject()
getTarget()
getTalkPage()
getTargetPage()
getTitle()
getWeblink()
getWikilink()
isBracketedLink()
isBracketedURL()
isCategorization()
isExternalLink()
isInterlanguage()
isInterwiki()
isMedia()
isTalkPage()
isTitledLink()
isValidLink()
isValidLinktext()
isWikilink()
pageLink()
pageTarget()
wikilink()
failsafe()
]=]



-- local globals
local URLutil   = false;
local Failsafe  = WLink;
local GlobalMod = WLink;



local htmlInline = { b      = true,
                     bdi    = true,
                     bdo    = true,
                     big    = true,
                     code   = true,
                     em     = true,
                     i      = true,
                     kbd    = true,
                     s      = true,
                     samp   = true,
                     small  = true,
                     span   = true,
                     strong = true,
                     style  = true,
                     sub    = true,
                     sup    = true,
                     tt     = true,
                     var    = true };



local foreignModule = function ( access, advanced, append, alt, alert )
    -- Fetch global module
    -- Precondition:
    --     access    -- string, with name of base module
    --     advanced  -- true, for require(); else mw.loadData()
    --     append    -- string, with subpage part, if any; or false
    --     alt       -- number, of wikidata item of root; or false
    --     alert     -- true, for throwing error on data problem
    -- Postcondition:
    --     Returns whatever, probably table
    -- 2020-01-01
    local storage = access;
    local finer = function ()
                      if append then
                          storage = string.format( "%s/%s",
                                                   storage,
                                                   append );
                      end
                  end
    local fun, lucky, r, suited;
    if advanced then
        fun = require;
    else
        fun = mw.loadData;
    end
    GlobalMod.globalModules = GlobalMod.globalModules or { };
    suited = GlobalMod.globalModules[ access ];
    if not suited then
        finer();
        lucky, r = pcall( fun,  "Module:" .. storage );
    end
    if not lucky then
        if not suited  and
           type( alt ) == "number"  and
           alt > 0 then
            suited = string.format( "Q%d", alt );
            suited = mw.wikibase.getSitelink( suited );
            GlobalMod.globalModules[ access ] = suited or true;
        end
        if type( suited ) == "string" then
            storage = suited;
            finer();
            lucky, r = pcall( fun, storage );
        end
        if not lucky and alert then
            error( "Missing or invalid page: " .. storage );
        end
    end
    return r;
end -- foreignModule()



local utilURL = function ()
    -- Attach URLutil library module
    -- Postcondition:
    --     Returns  table, with URLutil library
    --     Throws error, if not available
    if not URLutil then
        local util = foreignModule( "URLutil",
                                    true,
                                    false,
                                    WLink.globals.URLutil );
        if type( util ) == "table" then
            URLutil = util.URLutil();
        else
            util = "library URLutil invalid";
        end
        if type( URLutil ) ~= "table" then
            error( util, 0 );
        end
    end
    return URLutil;
end -- utilURL()



local cleanWikilink = function ( access )
    -- Refine wikilink spacing and decode
    -- Precondition:
    --     access  -- string, with presumable link
    -- Postcondition:
    --     Returns  string, with pretty target
    local r;
    if not WLink.lrm then
        WLink.lrm = mw.ustring.char( 0x200E );
        WLink.rlm = mw.ustring.char( 0x200F );
    end
    r = access:gsub( "_",        " " )
              :gsub( " ",   " " )
              :gsub( " ", " " )
              :gsub( " ",   " " )
              :gsub( " ",  " " )
              :gsub( "‎",    "" )
              :gsub( "‏",    "" )
              :gsub( WLink.lrm,  "" )
              :gsub( WLink.rlm,  "" )
              :gsub( "%s+",      " " );
    r = mw.text.decode( r );
    return r;
end -- cleanWikilink()



local contentExtlink = function ( attempt )
    -- Retrieve span of external link between brackets
    -- Precondition:
    --     attempt  -- string, with presumable link
    --                         the first char is expected to be "["
    -- Postcondition:
    --     Returns  string, number, number
    --                  string including whitespace
    --                  number with index of relevant "["
    --                  number with index after relevant "]"
    --              false if nothing found
    local r1 = false;
    local r2 = false;
    local r3 = attempt:find( "]", 2, true );
    if r3 then
        local s = attempt:sub( 2,  r3 - 1 );
        local i = s:find( "[", 1, true );
        if i then
            r1 = s:sub( i + 1 );
            r2 = i;
        else
            r1 = s;
            r2 = 1;
        end
    else
        r3 = false;
    end
    return r1, r2, r3;
end -- contentExtlink()



local contentWikilink = function ( attempt )
    -- Retrieve span of wikilink between brackets
    -- Precondition:
    --     attempt  -- string, with presumable link
    --                        the first two chars are expected to be "[["
    -- Postcondition:
    --     Returns  string, number, number
    --                  string including whitespace
    --                  number with index of relevant "[["
    --                  number with index after relevant "]]"
    --              false if nothing found
    local r1 = false;
    local r2 = false;
    local r3 = attempt:find( "]]", 3, true );
    if r3 then
        local s = attempt:sub( 3,  r3 - 1 );
        local i = s:find( "[[", 1, true );
        if i then
            r1 = s:sub( i + 2 );
            r2 = i;
        else
            r1 = s;
            r2 = 1;
        end
    end
    return r1, r2, r3;
end -- contentWikilink()



local extractExtlink = function ( attempt )
    -- Retrieve external link
    -- Precondition:
    --     attempt  -- string, with presumable link
    --                        the first char is expected to be "["
    -- Postcondition:
    --     Returns  string, string
    --                  first with target and title
    --                  second result false if not titled
    --              false if nothing found
    local r1 = false;
    local r2 = false;
    local s = contentExtlink( attempt );
    if s then
        local i = s:find( "%s", 1 );
        if i then
            r1 = s:sub( 1,  i - 1 );
            r2 = mw.text.trim( s:sub( i + 1 ) );
            if r2 == "" then
                r2 = false;
            end
        else
            r1 = s;
        end
        if r1 then
            r1 = mw.text.trim( r1 );
            if r1 == ""  or
               not utilURL().isResourceURL( r1 ) then
                r1 = false;
            end
        end
        if not r1 then
            r2 = false;
        end
    end
    return r1, r2;
end -- extractExtlink()



local extractWikilink = function ( attempt )
    -- Retrieve wikilink
    -- Precondition:
    --     attempt  -- string, with presumable link
    --                        the first two chars are expected to be "[["
    -- Postcondition:
    --     Returns  string, string
    --                  first with target
    --                  second result title, or false if not piped
    --              false if nothing found
    local r1 = false;
    local r2 = false;
    local s = contentWikilink( attempt );
    if s then
        local i = s:find( "|", 1, true );
        if i then
            r1 = s:sub( 1,  i - 1 );
            r2 = s:sub( i + 1 );
        else
            r1 = s;
        end
        r1 = mw.text.trim( r1 );
        if r1 == "" then
            r1 = false;
        else
            r1 = cleanWikilink( r1 );
        end
    end
    return r1, r2;
end -- extractWikilink()



local farming = function ( already )
    -- Retrieve wikifarm project information
    -- Precondition:
    --     already  -- table, with wikilink components
    -- Postcondition:
    --     Returns  table, with wikilink components extended
    local r = already;
    if not r.project then
        local codes  = { mediawiki   = "mw",
                         wikibooks   = "b",
                         wikidata    = "d",
                         wikinews    = "n",
                         wikipedia   = "w",
                         wikiquote   = "q",
                         wikisource  = "s",
                         wikiversity = "v",
                         wikivoyage  = "voy",
                         wiktionary  = "wikt" };
        local server = mw.site.server:gsub( "([/.])m%.",
                                             "%1" )
                                      :gsub( "%.beta%.wmflabs%.org$",
                                             ".org" );
        local site   = server:match( "[/.](%l+)%.org$" );
        r.project = codes[ site ];
        if r.project then
            if not r.lang  and
               r.project ~= "mw"  and  r.project ~= "d" then
                r.lang = server:match( "//(%l+)%." );
            end
        else
            site = server:match( "//(%l+)%.wikimedia%.org$" );
            if site == "commons"  or  site == "meta" then
                r.project = site;
            end
        end
    end
    if r.project  and  r.ns  and
       ( r.project == "commons"  or
         r.project == "d"        or
         r.project == "meta"     or
         r.project == "mw" ) then
        r.language = true;
    end
    return r;
end -- farming()



local prefix = function ( ask )
    -- Interprete prefix of language or project type
    -- Precondition:
    --     ask    -- string, with presumable prefix
    -- Postcondition:
    --     Returns  string,string or nil
    --                     first  string one of "lead", "lang", "project"
    --                     second string is formatted value
    --                       type is one of "lead", "lang", "project"
    --              nil if nothing found
    local r1, r2;
    local prefixes = { b           = true,
                       c           = "commons",
                       d           = true,
                       commons     = true,
                       m           = "meta",
                       mediawiki   = "mw",
                       mw          = true,
                       meta        = true,
                       n           = true,
                       q           = true,
                       s           = true,
                       simple      = false,
                       v           = true,
                       voy         = true,
                       w           = true,
                       wikibooks   = "b",
                       wikidata    = "d",
                       wikinews    = "n",
                       wikipedia   = "w",
                       wikiquote   = "q",
                       wikisource  = "s",
                       wikiversity = "v",
                       wikivoyage  = "voy",
                       wikt        = true,
                       wiktionary  = "wikt"
                     };
    local s = mw.text.trim( ask );
    if s ~= "" then
        local p;
        s = s:lower();
        p = prefixes[ s ];
        if p == true then
            r1 = "project";
            r2 = s;
        elseif p then
            r1 = "project";
            r2 = p;
        elseif p == false then
            r1 = "lang";
            r2 = s;
        elseif s:match( "^%l%l%l?$" )
               and  mw.language.isSupportedLanguage( s ) then
            r1 = "lang";
            r2 = s;
        end
    end
    return r1, r2;
end -- prefix()



local target = function ( attempt, lonely )
    -- Retrieve first target (wikilink or URL), or entire string
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    --     lonely   -- remove fragment, if true
    -- Postcondition:
    --     Returns  string, number
    --                  string, with detected link target, or entire
    --                  number, with number of brackets, if found, or 2
    local r1, r2 = WLink.getTarget( attempt );
    if not r1 then
        r1 = mw.text.trim( attempt );
        r2 = 2;
    end
    if lonely then
        local i = r1:find( "#", 1, true );
        if i == 1 then
            r1 = "";
        elseif i then
            r1 = r1:sub( 1, i - 1 );
        end
    end
    return r1, r2;
end -- target()



function WLink.ansiPercent( attempt, alter )
    -- Convert string by ANSI encoding rather than UTF-8 encoding
    -- Precondition:
    --     attempt  -- string, with presumable ANSI characters
    --     alter    -- string or nil, to use for spaces instead of %20
    -- Postcondition:
    --     Returns  string, encoded
    local k, s;
    local r = attempt;
    if alter then
        r = r:gsub( " ", alter );
    end
    for i = mw.ustring.len( r ), 1, -1 do
        k = mw.ustring.codepoint( r, i, i );
        if k <= 32  or  k > 126 then
            if k > 255 then
                s = mw.ustring.sub( r, i, i );
                if k > 2047 then
                    s = string.format( "%%%2X%%%2X%%%2X",
                                       s:byte( 1, 1 ),
                                       s:byte( 2, 2 ),
                                       s:byte( 3, 3 ) );
                else
                    s = string.format( "%%%2X%%%2X",
                                       s:byte( 1, 1 ),
                                       s:byte( 2, 2 ) );
                end
            else
                s = string.format( "%%%02X", k );
            end
            r = string.format( "%s%s%s",
                               mw.ustring.sub( r,  1,  i - 1 ),
                               s,
                               mw.ustring.sub( r,  i + 1 ) );
        end
    end -- for --i
    return r;
end -- WLink.ansiPercent()



function WLink.formatURL( adjust, assure )
    -- Create bracketed link, if not yet
    -- Precondition:
    --     adjust  -- string, with URL or domain/path or bracketed link
    --     assure  -- boolean, true for secure HTTP
    -- Postcondition:
    --     Returns  string, with bracketed link
    --              false on invalid format
    local r;
    if type( adjust ) == "string" then
        if WLink.isBracketedLink( adjust ) then
            r = adjust;
        else
            local url = mw.text.trim( adjust );
            local host;
            utilURL();
            host = URLutil.getHost( adjust );
            if not host then
                url = "://" .. adjust;
                if assure then
                    url = "s" .. url;
                end
                url  = "http" .. url;
                host = URLutil.getHost( url );
            end
            if host then
                local path = URLutil.getRelativePath( url );
                local show;
                if path == "/" then
                    if not url:match( "/$" ) then
                        url = url .. "/";
                    end
                    show = host;
                else
                    local i = path:find( "#" );
                    if i then
                        path = path:sub( 1,  i - 1 );
                    end
                    show = host .. path;
                end
                r = string.format( "[%s %s]", url, show );
            else
                r = adjust;
            end
        end
    else
        r = false;
    end
    return r;
end -- WLink.formatURL()



function WLink.getArticleBase( attempt )
    -- Retrieve generic article title, no fragment nor brackets
    -- Precondition:
    --     attempt  -- string, with wikilink or page title
    --                         current page title, if missing
    -- Postcondition:
    --     Returns  string, with identified lemma, or all
    --              false on invalid format
    local r;
    if attempt then
        local m;
        r, m = target( attempt, true );
        if m ~= 2 then
            r = false;
        end
    else
        r = mw.title.getCurrentTitle().text;
    end
    if r then
        local sub = r:match( "^(.*%S) *%(.+%)$" );
        if sub then
            r = sub;
        end
    end
    return r;
end -- WLink.getArticleBase()



function WLink.getBaseTitle( attempt )
    -- Retrieve last segment in subpage, no fragment
    -- Precondition:
    --     attempt  -- string, with wikilink or page title
    -- Postcondition:
    --     Returns  string, with identified segment, or all
    local r;
    local s, m = target( attempt, true );
    if m == 2 then
        local sub = s:match( "/([^/]+)$" );
        if sub then
            r = sub;
        else
            r = s;
        end
    else
        r = false;
    end
    return r;
end -- WLink.getBaseTitle()



function WLink.getEscapedTitle( attempt )
    -- Retrieve escaped link title
    -- Precondition:
    --     attempt  -- string, with presumable link title
    -- Postcondition:
    --     Returns  string, with suitable link title
    local s = mw.text.trim( attempt );
    return s:gsub( "\n", " " )
            :gsub( "%[", "&#91;" )
            :gsub( "%]", "&#93;" )
            :gsub( "|",  "&#124;" );
end -- WLink.getEscapedTitle()



function WLink.getExtension( attempt )
    -- Retrieve media extension
    -- Precondition:
    --     attempt  -- string, with wikilink (media link) or page title
    --                         if URL, PDF may be detected
    -- Postcondition:
    --     Returns  string, with detected downcased media type
    --              false if no extension found
    local r = false;
    local s, m = target( attempt );
    if m == 2 then
        s = s:match( "%.(%a+)$" );
        if s then
            r = s:lower();
        end
    elseif s:upper():match( "[%./](PDF)%W?" ) then
        r = "pdf";
    end
    return r;
end -- WLink.getExtension()



function WLink.getFile( attempt )
    -- Retrieve media page identifier
    -- Precondition:
    --     attempt  -- string, with wikilink (media link) or page title
    -- Postcondition:
    --     Returns  string, with detected file title
    --                      no namespace nor project
    --              false if no file found
    local r = false;
    local s, m = target( attempt );
    if m == 2 then
        local slow    = ":" .. s:lower();
        local find = function ( a )
                         local seek = string.format( ":%s:().+%%.%%a+$",
                                                     a:lower() );
                         local join = slow:find( seek );
                         local ret;
                         if join then
                             ret = s:sub( join + #a + 1 );
                         end
                         return ret;
                     end;
        r = find( "file" );
        if not r then
            local trsl = mw.site.namespaces[ 6 ];
            r = find( trsl.name );
            if not r then
                trsl = trsl.aliases;
                for k, v in pairs( trsl ) do
                    r = find( v );
                    if r then
                        break; -- for k, v
                    end
                end -- for k, v
            end
        end
    end
    return r;
end -- WLink.getFile()



function WLink.getFragment( attempt )
    -- Retrieve fragment
    -- Precondition:
    --     attempt  -- string, with presumable fragment
    -- Postcondition:
    --     Returns  string, with detected fragment
    --              false if no address found
    local r = false;
    local s, m = target( attempt );
    if s then
        local i = s:find( "#", 1, true );
        if i then
            if i > 1 then
                s = s:sub( i - 1 );
                i = 2;
            end
            if s:find( "&#", 1, true ) then
                s = mw.text.decode( s );
                i = s:find( "#", 1, true );
                if not i then
                   s = "";
                   i = 0;
                end
            end
            s = s:sub( i + 1 );
            r = mw.text.trim( s );
            if r == "" then
                r = false;
            elseif m == 2 then
                r = r:gsub( "%.(%x%x)", "%%%1" )
                     :gsub( "_", " " );
                r = mw.uri.decode( r, "PATH" );
            end
        end
    end
    return r;
end -- WLink.getFragment()



function WLink.getLanguage( attempt )
    -- Retrieve language project identifier
    -- Precondition:
    --     attempt  -- string, with wikilink or page title
    -- Postcondition:
    --     Returns  string, with detected downcased language identifier
    --              false if no project language found
    local r = false;
    local s, m = WLink.getTarget( attempt );
    if m == 2 then
        local w = WLink.wikilink( s );
        if w  and  w.lang then
            r = w.lang;
        end
    end
    return r;
end -- WLink.getLanguage()



function WLink.getLinktextProblem( attempt )
    -- Which problem has this presumable link text?
    -- Precondition:
    --     attempt  -- string, with presumable linktext
    -- Postcondition:
    --     Returns  string, with error message, or false
    local r;
    if attempt:find( "]", 1, true ) then
        r = "&#93;";
    elseif mw.text.unstripNoWiki( attempt ) ~= attempt then
        r = "&lt;nowiki&gt;";
    elseif attempt:find( "\n", 1, true ) then
        r = "&#92;n";
    elseif mw.text.unstrip( attempt ) ~= attempt then
        if not WLink.stripREF then
            WLink.stripREF = string.format( "%c%c%c%c%s%c%c%c%c",
                                            127, 39, 34, 96,
                                            "UNIQ%-+ref%-%x+%-QINU",
                                            96, 34, 39, 127 );
        end
        if mw.ustring.find( attempt, WLink.stripREF ) then
            r = "&lt;ref&gt;";
        end
    end
    if not r then
        local i = attempt:find( "<", 1, true );
        if i then
            local s = mw.ustring.lower( attempt:sub( i ) );
            local sign = true;
            local skip;
            while sign  and  not r do
                skip, sign = s:match( "^([^<]*< *)(%l[%l%d]*)[ /]*.*>" );
                if sign then
                    if htmlInline[ sign ] then
                        i = skip:len() + sign:len() + 1;
                        s = s:sub( i );
                    else
                        r = string.format( "&lt;%s&gt;", sign );
                    end
                end
            end    -- while sign and not r
        end
        if not r then
            local s = attempt .. " ";
            if s:find( "ISBN ", 1, true ) then
                r = s:match( "(ISBN %d[%-%d]+[%dxX])%W" );
            end
            if not r then
                if s:find( "PMID ", 1, true ) then
                    r = s:match( "(PMID [1-9]%d*)%W" );
                end
                if not r then
                    if s:find( "RFC ", 1, true ) then
                        r = s:match( "(RFC [1-9]%d?%d?%d?)%W" );
                    end
                end
            end
        end
    end
    return r or false;
end -- WLink.getLinktextProblem()



function WLink.getNamespace( attempt )
    -- Retrieve namespace number
    -- Precondition:
    --     attempt  -- string, with wikilink or page title
    -- Postcondition:
    --     Returns  number, of detected namespace
    --              false if no namespace found
    local r = false;
    local s, m = WLink.getTarget( attempt );
    if m == 2 then
        local w = WLink.wikilink( s );
        if w  and  not w.lang  and  not w.project  and  w.ns then
            r = w.ns;
        end
    end
    return r;
end -- WLink.getNamespace()



function WLink.getNamespaced( area, attempt )
    -- Retrieve page in namespace
    -- Precondition:
    --     area     -- string or number, with some namespace spec
    --     attempt  -- string, with wikilink or page title or page name
    -- Postcondition:
    --     Returns  page prefixed by namespace,
    --              false if invalid
    local r = false;
    local s = type( area );
    local room;
    if s == "string" then
        room = mw.site.namespaces[ tonumber( area )  or  area ];
    elseif s == "number" then
        room = mw.site.namespaces[ area ];
    end
    if room then
        local m;
        s, m = WLink.getTarget( attempt );
        if not s then
            s = attempt;
        elseif m ~= 2 then
            s = false;
        end
        if s then
            local w = WLink.wikilink( s );
            if w  and  not w.lang  and  not w.project  and
               ( not w.ns  or  w.ns == room.id ) then
                r = string.format( "%s:%s",
                                   room.name, w.title );
            end
        end
    end
    return r;
end -- WLink.getNamespaced()



function WLink.getPlain( attempt )
    -- Retrieve text with all links replaced by link titles
    -- Precondition:
    --     attempt  -- string, with wikitext
    -- Postcondition:
    --     Returns  string, with modified wikitext without links
    local r = attempt;
    local i = 1;
    local j, k, n, lean, s, shift, span, space, suffix;
    while ( true ) do
        j = r:find( "[", i, true );
        if j then
            suffix = r:sub( j );
            i      = j + 1;
            lean   = ( r:byte( i, i ) == 91 );
            if lean then
                s, k, n = contentWikilink( suffix );
            else
                s, k, n = contentExtlink( suffix );
            end
            if s then
                if k > 1 then
                    n      = n - k;
                    i      = j + k + 1;
                    j      = i - 1;
                    suffix = r:sub( j );
                end
                if lean then
                    s, shift = extractWikilink( suffix );
                    if s then
                        space = s:match( "^([^:]+):" );
                        if space then
                            space = mw.site.namespaces[ space ];
                            if space then
                                space = space.id;
                            end
                        end
                        if space == 6  or  space == 14 then
                            shift = "";
                        elseif not shift then
                            shift = s;
                        end
                    else
                        s     = "";
                        shift = "";
                    end
                else
                    span, shift = extractExtlink( suffix );
                    if span then
                        if not shift then
                            shift = "";
                        end
                    else
                        shift = string.format( "[%s]", s );
                    end
                    i = i - 1;
                end
                if j > 1 then
                    s = r:sub( 1, j - 1 );
                else
                    s = "";
                end
                r = string.format( "%s%s%s",
                                   s,  shift,  r:sub( n + i ) );
                i = i + #shift;
            else
                break; -- while true
            end
        else
            break; -- while true
        end
    end    -- while true
    return r;
end -- WLink.getPlain()



function WLink.getProject( attempt )
    -- Retrieve wikifarm project identifier
    -- Precondition:
    --     attempt  -- string, with wikilink or page title
    -- Postcondition:
    --     Returns  string, with detected downcased project identifier
    --              false if no project identifier found
    local r = false;
    local s, m = WLink.getTarget( attempt );
    if m == 2 then
        local w = WLink.wikilink( s );
        if w  and  w.project then
            r = w.project;
        end
    end
    return r;
end -- WLink.getProject()



function WLink.getTalkPage( attempt )
    -- Retrieve talk page name for attempt, or that page name itself
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  string  or  false
    local r = false;
    local s, m = WLink.getTarget( attempt );
    if m ~= 2  and  attempt then
        s = mw.text.trim( attempt );
    end
    if s  and  s ~= "" then
        local w = mw.title.new( s );
        if w then
            w = w.talkPageTitle;
            if w then
                r = w.prefixedText;
            end
        end
    end
    return r;
end -- WLink.getTalkPage()



function WLink.getTarget( attempt )
    -- Retrieve first target (wikilink or URL)
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  string, number
    --                  string, with first detected link target
    --                  number, with number of brackets, if found
    --              false if nothing found
    local r1 = false;
    local r2 = false;
    local i  = attempt:find( "[", 1, true );
    if i then
        local m;
        r1 = attempt:sub( i );
        if r1:byte( 2, 2 ) == 91 then
            m  = 2;
            r1 = extractWikilink( r1 );
        else
            m  = 1;
            r1 = extractExtlink( r1 );
        end
        if r1 then
            r2 = m;
        end
    else
        r1 = attempt:match( "%A?([hf]t?tps?://%S+)%s?" );
        if r1 then
            if utilURL().isResourceURL( r1 ) then
                r2 = 0;
            else
                r1 = false;
            end
        else
            r1 = false;
        end
    end
    return r1, r2;
end -- WLink.getTarget()



function WLink.getTargetPage( attempt )
    -- Retrieve first target page (page name or URL of page)
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  string, with first detected linked page
    --              false if nothing found
    local r1, r2 = WLink.getTarget( attempt );
    if r1 then
        local i = r1:find( "#", 1, true );
        if i then
            if i == 1 then
                r1 = false;
            else
                r1 = mw.text.trim( r1:sub( 1,  i - 1 ) );
            end
        end
    end
    return r1, r2;
end -- WLink.getTargetPage()



function WLink.getTitle( attempt )
    -- Retrieve first link title (wikilink or URL), or wikilink target
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  string, with first detected link target
    --              false if nothing found
    local r = false;
    local i = attempt:find( "[", 1, true );
    if i then
        local s1, s2;
        r = attempt:sub( i );
        if r:byte( 2, 2 ) == 91 then
            s1, s2 = extractWikilink( r );
            if s2 then
                r = s2;
            else
                r = s1;
            end
        else
            s1, r = extractExtlink( r );
        end
    end
    return r;
end -- WLink.getTitle()



function WLink.getWeblink( attempt, anURLutil )
    -- Retrieve bracketed link from resource URL
    -- Precondition:
    --     attempt    -- string, with URL, or something different
    --     anURLutil  -- library module object, or nil
    -- Postcondition:
    --     Returns  string, with first detected link target
    --              false if nothing found
    local second = ".ac.co.go.gv.or.";
    local r;
    if type( anURLutil ) == "table" then
        URLutil = anURLutil;
    else
        utilURL();
    end
    if URLutil.isResourceURL( attempt ) then
        local site    = URLutil.getAuthority( attempt );
        local service = attempt;
        local show;
        if #attempt == #site then
           site = site .. "/";
        end
        show = URLutil.getTop3domain( "//" .. site );
        if show then
            local scan   = "[%./](%a[%a%%%-]*%a)(%.%l%l%.)(%a+)$";
            local search = "." .. show;
            local s1, s2, s3 = search:match( scan );
            if s2 then
                if not second:find( s2, 1, true ) then
                    show = string.format( "%s%s",  s2:sub( 2 ),  s3 );
                end
            else
                show = false;
            end
        end
        if not show then
            show = URLutil.getTop2domain( "//" .. site );
            if not show then
                show = URLutil.getHost( "//" .. site );
            end
        end
        if not service:match( "^[a-z:]*//.+/" ) then
            service = service .. "/";
        end
        r = string.format( "[%s %s]", service, show );
    else
        r = attempt;
    end
    return r;
end -- WLink.getWeblink()



function WLink.getWikilink( attempt, appear )
    -- Retrieve bracketed link from text
    -- Precondition:
    --     attempt  -- string, with current target, or plain
    --     appear   -- string, with link title, or nil
    -- Postcondition:
    --     Returns  string, with first detected link target
    --              false if nothing found
    local r = WLink.pageTarget( attempt );
    if r then
        if appear then
            local show = WLink.getEscapedTitle( appear );
            if show ~= r  and  show ~= "" then
                r = string.format( "%s|%s", r, show );
            end
        end
        r = string.format( "[[%s]]", r );
    end
    return r;
end -- WLink.getWikilink()



function WLink.isBracketedLink( attempt )
    -- Does attempt match a bracketed link?
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  boolean
    local r = false;
    local i = attempt:find( "[", 1, true );
    if i then
        local s = attempt:sub( i );
        if s:byte( 2, 2 ) == 91 then
            s = extractWikilink( s );
        else
            s = extractExtlink( s );
        end
        if s then
            r = true;
        end
    end
    return r;
end -- WLink.isBracketedLink()



function WLink.isBracketedURL( attempt )
    -- Does attempt match a bracketed URL?
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  boolean
    local s, r = WLink.getTarget( attempt );
    return ( r == 1 );
end -- WLink.isBracketedURL()



function WLink.isCategorization( attempt )
    -- Does attempt match a categorization?
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  boolean
    local r = false;
    local s, m = WLink.getTarget( attempt );
    if m == 2 then
        local w = WLink.wikilink( s );
        if w  and  w.ns == 14
              and  not ( w.lead or w.lang or w.project )
              and  w.title ~= "" then
            r = true;
        end
    end
    return r;
end -- WLink.isCategorization()



function WLink.isExternalLink( attempt )
    -- Does attempt match an external link?
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  boolean
    local s, r = WLink.getTarget( attempt );
    if r then
        r = ( r < 2 );
    end
    return r;
end -- WLink.isExternalLink()



function WLink.isInterlanguage( attempt )
    -- Does attempt match an interlanguage link?
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  boolean
    local r = false;
    local s, m = WLink.getTarget( attempt );
    if m == 2 then
        local w = WLink.wikilink( s );
        if w and w.lang and not w.project and not w.lead
             and  w.title ~= "" then
            r = true;
        end
    end
    return r;
end -- WLink.isInterlanguage()



function WLink.isInterwiki( attempt )
    -- Does attempt match an interwiki link within wikifarm?
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  boolean
    local r = false;
    local s, m = WLink.getTarget( attempt );
    if m == 2 then
        local w = WLink.wikilink( s );
        if w  and  ( w.lang or w.project )  and  w.title ~= "" then
            r = true;
        end
    end
    return r;
end -- WLink.isInterwiki()



function WLink.isMedia( attempt )
    -- Does attempt match a media translusion?
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  boolean
    local r = false;
    local s, m = WLink.getTarget( attempt );
    if m == 2 then
        local w = WLink.wikilink( s );
        if w  and  w.ns == 6
           and  not ( w.lead or w.lang or w.project )
           and  w.title ~= ""
           and  WLink.getExtension( w.title ) then
            r = true;
        end
    end
    return r;
end -- WLink.isMedia()



function WLink.isTalkPage( attempt )
    -- Does attempt describe a talk page?
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  boolean
    local r = false;
    local s, m = WLink.getTarget( attempt );
    if m ~= 2  and  attempt then
        s = mw.text.trim( attempt );
    end
    if s  and  s ~= "" then
        local w = mw.title.new( s );
        if w then
            r = w.isTalkPage;
        end
    end
    return r;
end -- WLink.isTalkPage()



function WLink.isTitledLink( attempt )
    -- Does attempt match a titled link?
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  boolean
    local r = false;
    local i = attempt:find( "[", 1, true );
    if i then
        local c, n;
        local s = attempt:sub( i );
        if s:byte( 2, 2 ) == 91 then
            n = s:find( "%]%]", 5 );
            c = "|";
        else
            n = s:find( "%]", 8 );
            c = "%s%S";
        end
        if n then
            local m = s:find( c, 2 );
            if m  and  m + 1 < n  and  WLink.getTarget( attempt ) then
                r = true;
            end
        end
    end
    return r;
end -- WLink.isTitledLink()



function WLink.isValidLink( attempt )
    -- Does attempt match a link?
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  boolean
    local u, r = WLink.getTarget( attempt );
    if r then
        if r < 2 then
            if u:find( "''", 1, true ) then
                r = false;
            else
                r = true;
            end
        else
            r = true;
        end
    end
    return r;
end -- WLink.isValidLink()



function WLink.isValidLinktext( attempt, allow )
    -- Is attempt a plain inline text?
    -- Precondition:
    --     attempt  -- string, with presumable linktext
    --     allow    -- boolean or nil, if multiline permitted
    -- Postcondition:
    --     Returns  boolean
    local s;
    if allow  and  s:find( "\n", 1, true ) then
        s = attempt:gsub( "\n", " " );
    else
        s = attempt;
    end
    return  not WLink.getLinktextProblem( s );
end -- WLink.isValidLinktext()



function WLink.isWikilink( attempt )
    -- Does attempt match a wikilink?
    -- Precondition:
    --     attempt  -- string, with presumable link somewhere
    -- Postcondition:
    --     Returns  boolean
    local s, m = WLink.getTarget( attempt );
    return ( m == 2 );
end -- WLink.isWikilink()



function WLink.pageLink( attempt, appear, assure )
    -- Create safely standardized wikilink target of a page
    --     attempt  -- string, with presumable link
    --     appear   -- string or true or nil, with link title
    --     assure   -- string or nil, shield against wiki template syntax
    --                 "URL" or "WIKI"
    -- Postcondition:
    --     Returns  string with link target
    local r = WLink.pageTarget( attempt, assure );
    if appear then
        local show;
        if type( appear ) == "string" then
            show = appear;
        else
            show = attempt;
        end
        r = string.format( "%s|%s", r, show );
    end
    return r;
end -- WLink.pageLink()



function WLink.pageTarget( attempt, assure )
    -- Create standardized wikilink target of a page
    -- Precondition:
    --     attempt  -- string, with presumable link
    --                         expected to be enclosed in "[[" "]]"
    --                         else wikilink
    --                 table, of assignments with { type, value }
    --                        type is one of "lead",
    --                             "project", "lang",
    --                             "ns", "space", "title"
    --     assure   -- string or nil, shield against wiki template syntax
    --                 "URL" or "WIKI"
    -- Postcondition:
    --     Returns  string with link target
    local p = type( attempt );
    local s = assure;
    local r;
    if p == "string" then
        p = WLink.wikilink( attempt );
    elseif p == "table" then
        p = attempt;
    else
        p = false;
    end
    if p then
        local site  = p.project;
        local slang = p.lang;
        local lead;
        if p.title:sub( 1, 1 ) == "#" then
            p.title = mw.title.getCurrentTitle().text .. p.title;
        end
        if p.ns then
            if not slang then
                p = farming( p );
            end
            if p.lang  and
               p.lang ~= mw.language.getContentLanguage():getCode() then
                p.language = true;
            end
            if p.language then
                p.space = mw.site.namespaces[ p.ns ].canonicalName;
            end
            lead = ( p.ns == 6  or  p.ns == 14 );
        end
        if slang then
            lead = true;
        end
        if s == "WIKI" then
            if not site   and
               ( lead  or
                 ( not p.space  and
                   p.title and p.title:match( "^[*;]" ) ) ) then
                p     = farming( p );
                site  = p.project;
                slang = p.lang;
            end
            s = false;
        end
        if site then
            r = site .. ":";
        elseif lead then
            r = ":";
        else
            r = "";
        end
        if slang then
            r = string.format( "%s%s:", r, slang );
        end
        if p.space then
            r = string.format( "%s%s:", r, p.space );
        end
        if p.title then
            r = r .. p.title;
        end
        if r == "" then
            r = false;
        end
    end
    if not r then
        p = { lang = mw.language.getContentLanguage():getCode() };
        if s == "WIKI" then
            r = WLink.pageTarget( p, "WIKI" );
        else
            r = string.format( ":%s:", p.lang );
        end
    end
    if s == "URL"  and  r:match( "^[*#;:]" ) then
        r = mw.uri.encode( r:sub( 1, 1 ) )  ..  r:sub( 2 );
    end
    return r;
end -- WLink.pageTarget()



function WLink.wikilink( attempt )
    -- Retrieve wikilink components
    -- Precondition:
    --     attempt  -- string, with presumable link
    --                         expected to be enclosed in "[[" "]]"
    --                         else wikilink
    -- Postcondition:
    --     Returns  table or false
    --              table of assignments with { type, value }
    --                       type is one of "lead",
    --                          "project", "lang",
    --                          "ns", "space", "title"
    --              false if nothing found
    local s = contentWikilink( attempt or "" );
    local got, i, n, r;
    if not s then
        s = attempt;
    end
    if s:find( "%", 1, true ) then
        s = mw.uri.decode( s, "PATH" );
    end
    i = s:find( "|", 1, true );
    if i then
        s = s:sub( 1, i - 1 );
    end
    got = mw.text.split( s, ":" );
    n   = table.maxn( got );
    if n == 1 then
        r = { title = mw.text.trim( s ) };
    else
        local j, k, o, v;
        r = { title = "" };
        while ( got[ 1 ] == "" ) do
            r.lead = true;
            table.remove( got, 1 );
            n = n - 1;
        end    -- while  got[ 1 ] == ""
        if n > 4 then
            k = 4;
        elseif n > 1 then
            k = n - 1;
        else
            k = 1;
        end
        j = k;
        for i = 1, j do
            s = mw.text.trim( got[ i ] );
            if s ~= "" then
                o = mw.site.namespaces[ s ];
                if o then
                    r.ns    = o.id;
                    r.space = o.name;
                    k = i + 1;
                    j = i - 1;
                    break; -- for i
                end
            end
        end -- for i
        for i = 1, j do
            o, v = prefix( got[ i ] );
            if o then
                if r[ o ] then
                    k = i;
                    break; -- for i
                else
                    if i >= k then
                        k = i + 1;
                    end
                    r[ o ] = v;
                end
            else
                if i == 1  and  r.ns then
                    r.ns    = false;
                    r.space = false;
                end
                k = i;
                break; -- for i
            end
        end -- for i
        if k > 0 then
            for i = k, n do
                r.title = r.title .. got[ i ];
                if i < n then
                    r.title = r.title .. ":";
                end
            end -- for i
        end
    end
    r.title = cleanWikilink( r.title );
    if r.lead and
       ( r.project  or
         ( not r.lang  and  r.ns ~= 6  and  r.ns ~= 14 ) ) then
        r.lead = false;
    end
    return r;
end -- WLink.wikilink()



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()



local function Template( frame, action, leave, lone )
    -- Run actual code from template transclusion
    -- Precondition:
    --     frame   -- object
    --     action  -- string, with function name
    --     leave   -- true: keep whitespace around
    --     lone    -- true: permit call without parameters
    -- Postcondition:
    --     Return string; might be error message
    local lucky = true;
    local s = false;
    local r = false;
    local safe, space;
    for k, v in pairs( frame.args ) do
        if k == 1 then
            if leave then
                s = v;
            else
                s = mw.text.trim( v );
            end
        elseif ( k == 2   and
                 ( action == "getNamespaced"  or
                   action == "getWikilink"  or
                   action == "pageLink" ) )    or
               ( k == "space"  and  action == "ansiPercent" ) then
            v = mw.text.trim( v );
            if v ~= "" then
                space = v;
            end
        elseif k == "safe"  and  action == "pageLink" then
            v = mw.text.trim( v );
            if v ~= "" then
                safe = v;
            end
        elseif k == "lines"  and  action == "isValidLinktext" then
            space = ( k == "1" );
        elseif k ~= "template" then
            lucky = false;
            if r then
                r = r .. "|";
            else
                r = "Unknown parameter: ";
            end
            r = string.format( "%s%s=", r, k );
        end
    end -- for k, v
    if lucky then
        if s or lone then
            lucky, r = pcall( WLink[ action ],  s,  space,  safe );
        else
            r = "Parameter missing";
            lucky = false;
        end
    end
    if lucky then
        if type( r ) == "boolean" then
            if r then
                r = "1";
            else
                r = "";
            end
        end
    else
        local e = mw.html.create( "span" );
        r = tostring( e:addClass( "error" )
                       :wikitext( r ) );
    end
    return r;
end -- Template()



-- Export
local p = { };

p.ansiPercent = function ( frame )
    return Template( frame, "ansiPercent" );
end
p.formatURL = function ( frame )
    return Template( frame, "formatURL" );
end
p.getArticleBase = function ( frame )
    return Template( frame, "getArticleBase", false, true );
end
p.getBaseTitle = function ( frame )
    return Template( frame, "getBaseTitle" );
end
p.getEscapedTitle = function ( frame )
    return Template( frame, "getEscapedTitle" );
end
p.getExtension = function ( frame )
    return Template( frame, "getExtension" );
end
p.getFile = function ( frame )
    return Template( frame, "getFile" );
end
p.getFragment = function ( frame )
    return Template( frame, "getFragment" );
end
p.getInterwiki = function ( frame )
    return Template( frame, "getInterwiki" );
end
p.getLanguage = function ( frame )
    return Template( frame, "getLanguage" );
end
p.getLinktextProblem = function ( frame )
    return Template( frame, "getLinktextProblem" );
end
p.getNamespace = function ( frame )
    return tostring( Template( frame, "getNamespace" ) );
end
p.getNamespaced = function ( frame )
    return tostring( Template( frame, "getNamespaced" ) );
end
p.getPlain = function ( frame )
    return Template( frame, "getPlain" );
end
p.getProject = function ( frame )
    return Template( frame, "getProject" );
end
p.getTalkPage = function ( frame )
    return Template( frame, "getTalkPage" );
end
p.getTarget = function ( frame )
    return Template( frame, "getTarget" );
end
p.getTargetPage = function ( frame )
    return Template( frame, "getTargetPage" );
end
p.getTitle = function ( frame )
    return Template( frame, "getTitle" );
end
p.getWeblink = function ( frame )
    return Template( frame, "getWeblink" );
end
p.getWikilink = function ( frame )
    return Template( frame, "getWikilink" );
end
p.isBracketedLink = function ( frame )
    return Template( frame, "isBracketedLink" );
end
p.isBracketedURL = function ( frame )
    return Template( frame, "isBracketedURL" );
end
p.isCategorization = function ( frame )
    return Template( frame, "isCategorization" );
end
p.isExternalLink = function ( frame )
    return Template( frame, "isExternalLink" );
end
p.isInterlanguage = function ( frame )
    return Template( frame, "isInterlanguage" );
end
p.isInterwiki = function ( frame )
    return Template( frame, "isInterwiki" );
end
p.isMedia = function ( frame )
    return Template( frame, "isMedia" );
end
p.isTalkPage = function ( frame )
    return Template( frame, "isTalkPage" );
end
p.isTitledLink = function ( frame )
    return Template( frame, "isTitledLink" );
end
p.isValidLink = function ( frame )
    return Template( frame, "isValidLink" );
end
p.isValidLinktext = function ( frame )
    return Template( frame, "isValidLinktext" );
end
p.isWeblink = function ( frame )
    return Template( frame, "isWeblink" );
end
p.isWikilink = function ( frame )
    return Template( frame, "isWikilink" );
end
p.pageLink = function ( frame )
    return Template( frame, "pageLink" );
end
p.failsafe = function ( frame )
    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
p.WLink = function ()
    return WLink;
end

return p;