Modul:TemplUtl

Modul:TemplUtl

Version vom 17. Mai 2022, 14:07 Uhr von imported>PerfektesChaos (2022-05-16)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)

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

local TemplUtl = { suite  = "TemplUtl",
                   serial = "2022-05-16",
                   item   = 52364930 };
--[=[
Utilities to support template programming.
]=]
local Failsafe = TemplUtl;



local fallible = function ( adjust, ahead )
    -- Check for leading character disturbing syntax
    -- Precondition:
    --    adjust  -- string; trimmed wikitext
    --    ahead   -- true, if leading syntax shall start on new line
    -- Postcondition:
    --    Returns string, modified if necessary
    local r = adjust;
    local c = r:byte( 1, 1 );
    local lead;
    if c <= 59  and
       ( c==35 or c==42 or c==58 or c==59 ) then
        lead = true;
    elseif c == 123  or  c == 124 then
        local c2 = r:byte( 2, 1 );
        if c == 123  and  c2 == 124 then
            lead = true;
        elseif ahead  and  c == 124  and
               ( c2 == 43  or  c2 == 45  or  c2 == 125 ) then
            lead = true;
        end
    end
    if lead then
        if ahead then
            r = "\n" .. r;
        else
            r = mw.text.nowiki( r:sub( 1, 1 ) )  ..  r:sub( 2 );
        end
    end
    return r;
end -- fallible()



local fiatTitleRegExp = function ( accept )
    -- Create pattern to detect page name
    -- Precondition:
    --     accept  -- string; trimmed title
    -- Postcondition:
    --    Returns string with pattern
    local start = mw.ustring.sub( accept, 1, 1 );
    local r;
    if mw.ustring.match( start, "%a" ) then
        r = string.format( "[%s%s]%s",
                           mw.ustring.lower( start ),
                           mw.ustring.upper( start ),
                           mw.ustring.sub( accept, 2 ) );
    else
        r = accept;
    end
    if r:match( " " ) then
        r = r:gsub( "%", "%%" )
             :gsub( "[%-^.?+*()$]", "%$1" )
             :gsub( "_", " " )
             :gsub( "%s+", "[%s_]+" );
    end
    return r;
end -- fiatTitleRegExp()



local framing = function ( frame )
    -- Ensure availability of frame object
    -- Precondition:
    --     frame  -- object; #invoke environment, or false
    -- Postcondition:
    --     Return frame object
    if not TemplUtl.frame then
        if type( frame ) == "table" then
            TemplUtl.frame = frame;
        else
            TemplUtl.frame = mw.getCurrentFrame();
        end
    end
    return TemplUtl.frame;
end -- framing()



TemplUtl.facets = function ( ask, adjust )
    local r = ask;
    if adjust == "%"  and  r:find( "%%%x%x" ) then
        r = mw.uri.decode( r, "PATH" );
    elseif r:find( "&", 1, true ) then
        r = mw.text.decode( r );
    end
    r = mw.ustring.gsub( r, "[%s%p%c]+", " " );
    r = mw.text.trim( r );
    return r;
end -- TemplUtl.facets()



TemplUtl.faculty = function ( analyze, another )
    -- Test template arg for boolean
    --     analyze  -- string, boolean, number or nil
    --     another  -- fallback: string, boolean, or nil
    --                 "-" to test for explicit vocabulary choice
    -- Returns boolean, or "-"
    local s = type( analyze );
    local r;
    if s == "string" then
        r = mw.text.trim( analyze );
        if r == ""  then
            r = TemplUtl.faculty( another, nil );
        elseif r:find( "1", 1, true )  and
               r:match( "^[0%-]*1[01%-]*$" ) then
            r = true;
        elseif r:match( "^[0%-]+$" ) then
            r = false;
        else
            r = r:lower();
            if r == "y"  or
               r == "yes"  or
               r == "true"  or
               r == "on" then
                r = true;
            elseif r == "n"  or
                   r == "no"  or
                   r == "false"  or
                   r == "off" then
                r = false;
            else
                if not TemplUtl.boolang then
                    -- TODO: page language
                    local l, d = pcall( mw.ext.data.get, "i18n/01.tab" );
                    if type( d ) == "table"  and
                       type( d.data ) == "table" then
                        local f = function ( at )
                                  local e = d.data[ at ];
                                  l = e[ 1 ];
                                  s = e[ 2 ];
                                  if type( l ) == "boolean"  and
                                     type( s ) == "string" then
                                      s = mw.text.split( s, "|" );
                                      for i = 1, #s do
                                          TemplUtl.boolang[ s[ i ] ] = l;
                                      end -- for i
                                  end
                              end
                        TemplUtl.boolang = { };
                        f( 1 );
                        f( 2 );
                    else
                        TemplUtl.boolang = true;
                    end
                end
                if type( TemplUtl.boolang ) == "table" then
                    s = TemplUtl.boolang[ r ];
                    if type( s ) == "boolean" then
                        r = s;
                    end
                end
                if type( r ) ~= "boolean" then
                    s = type( another );
                    if s == "nil" then
                        r = true;
                    elseif s == "boolean" then
                        r = another;
                    elseif s == "string" then
                        s = mw.text.trim( another );
                        if s == "-" then
                            r = "-";
                        elseif s == "" then
                            r = true;
                        else
                            r = TemplUtl.faculty( s );
                        end
                    end
                end
            end
        end
    elseif s == "boolean" then
        r = analyze;
    elseif s == "number" then
        r = ( analyze ~= 0 );
    else
        r = false;
    end
    return r;
end -- TemplUtl.faculty()



TemplUtl.failure = function ( alert, always, addClass, frame )
    -- Format error message, mostly hidden
    --     alert     -- string: message
    --     always    -- boolean, or nil: do not hide
    --     addClass  -- string, or nil: add classes to element
    --     frame     -- object, or nil
    -- Returns string
    local err  = mw.html.create( "span" )
                        :addClass( "error" )
                        :wikitext( alert );
    local live = ( framing( frame ):preprocess( "{{REVISIONID}}" )
                   == "" );
    if type( addClass ) == "string" then
        err:addClass( addClass )
    end
    if live then
        local max  = 1000000000;
        local id   = math.floor( os.clock() * max );
        local sign = string.format( "error_%d", id );
        local btn  = mw.html.create( "span" );
        local top  = mw.html.create( "div" );
        err:attr( "id", sign );
        -- TODO: LTR
        btn:css( { ["background"]      = "#FFFF00",
                   ["border"]          = "#FF0000 3px solid",
                   ["font-weight"]     = "bold",
                   ["padding"]         = "2px",
                   ["text-decoration"] = "none" } )
           :wikitext( "&gt;&gt;&gt;" );
        sign = string.format( "[[#%s|%s]]",  sign,  tostring( btn ) );
        top:wikitext( sign, "&#160;", alert );
        mw.addWarning( tostring( top:attr( "role", "alert" ) ) );
    elseif not always then
        err:css( { ["display"]     = "none" } );
--      err:css( { ["display"]     = "inline-block",
--                 ["line-height"] = "0",
--                 ["max-height"]  = "0",
--                 ["max-width"]   = "0",
--                 ["visibility"]  = "hidden" } );
    end
    return tostring( err );
end -- TemplUtl.failure()



TemplUtl.fake = function ( access )
    -- Simulation of template transclusion
    -- Precondition:
    --    access  -- string; page name (template)
    if type( access ) == "string" then
        local s = mw.text.trim( access );
        if s ~= "" then
            local t = mw.title.new( s, 10 );
            if not mw.title.equals( mw.title.getCurrentTitle(), t )  and
               t.exists then
                t:getContent();
            end
        end
    end
end -- TemplUtl.fake()



TemplUtl.fakes = function ( array, frame, ahead, answer )
    -- Simulation of template transclusions
    -- Precondition:
    --    array   -- table, with template title strings
    --    frame   -- object, or nil
    --    ahead   -- string, or nil, with common prefix
    --    answer  -- true, or nil, for list creation
    -- Postcondition:
    --    Returns string, if answer requested
    local e = framing( frame );
    local f = function ( a )
                  e:expandTemplate{ title = a };
              end
    local s = ahead or "";
    local r;
    for k, v in pairs( array ) do
        if type( k ) == "number" and
           type( v ) == "string" then
            v = s .. mw.text.trim( v );
            pcall( f, v );
            if answer then
                if r then
                    r = r .. "\n";
                else
                    r = "";
                end
                r = string.format( "%s* [[Template:%s|%s]]", r, v, v );
            end
        end
    end -- for k, v
    return r;
end -- TemplUtl.fakes()



TemplUtl.feasible = function ( address )
    -- Does this describe an URL beginning?
    -- Precondition:
    --    address  -- string; what to inspect, URL presumed
    -- Postcondition:
    --    Returns true, if URL beginning
    local start, r = address:match( "^%s*((%a*:?)//)" );
    if start then
        if r == "" then
            r = true;
        elseif r:sub( -1, -1 ) == ":" then
            local schemes = ":ftp:ftps:http:https:";
            r = ":" .. r:lower();
            if schemes:find( r, 1, true ) then
                r = true;
            else
                r = false;
            end
        else
            r = false;
        end
    end
    return r;
end -- TemplUtl.feasible()



TemplUtl.feed = function ( area, ahead, at, after )
    -- Detect next free "|" or "}}"
    -- Precondition:
    --     area   -- string; template transclusion
    --     ahead  -- string; opening element, or false
    --     at     -- number; byte position in area where to start
    --     after  -- true, if only to search for "}}"
    -- Postcondition:
    --    Returns
    --          -- number; byte position in area
    --             -- before "|" or "}}", may be at end
    --             -- to continue search; ahead has been closed
    --          -- true, if to be continued at number
    local j    = at;
    local loop = true;
    local c, k, r, s, seek;
    if after then
        seek = "[{}<]";
    else
        seek = "[%[%]|{}<:]";
    end
    while loop do
        j = area:find( seek, j );
        if j then
            c = area:byte( j, j );
            if c == 123 then    -- {
                k = j + 1;
                if area:byte( k, k ) == 123 then
                    k = k + 1;
                    if area:byte( k, k ) == 123 then
                        j, loop = TemplUtl.feed( area, "{{{", k, after );
                    else
                        k = k - 1;
                        j, loop = TemplUtl.feed( area, "{{", k, after );
                    end
                    if not loop then
                        r = j;
                    end
                end
            elseif c == 125 then    -- }
                k = j + 1;
                if area:byte( k, k ) == 125 then
                    if ahead == "{{" then
                        r = k;
                        break;    -- while loop;
                    elseif ahead == "{{{" then
                        k = k + 1;
                        if area:byte( k, k ) == 125 then
                            r = k;
                            break;    -- while loop;
                        end
                    elseif not ahead then
                        r    = j - 1;
                        loop = false;
                    end
                end
            elseif c == 60 then    -- <
                k = j + 3;
                if area:sub( j, k ) == "<!--" then
                    k = area:find( "-->", k );
                    if k then
                        j = k + 2;
                    end
                else
                    local skip;
                    s    = area:sub( j + 1 ):lower();
                    skip = s:match( "^%s*nowiki%s*>" );
                    if skip then
                        local n = skip:len();
                        n, k = s:find( "<%s*/%s*nowiki%s*>", n );
                        if k then
                            j = j + k;
                        else
                            loop = false;
                        end
                    end
                end
            elseif c == 124 then    -- |
                if not r then
                    r = j - 1;
                end
                if not ahead then
                    loop = false;
                end
            elseif c == 91 then    -- [
                k = j + 1;
                if area:byte( k, k ) == 91 then
                    k = k + 1;
                    j, loop = TemplUtl.feed( area, "[[", k, after );
                elseif TemplUtl.feasible( area:sub( k ) ) then
                    k = k + 3;
                    j, loop = TemplUtl.feed( area, "[", k, after );
                end
                if not loop then
                    r = j;
                end
            elseif c == 93 then    -- ]
                if ahead == "[" then
                    r = j;
                    break;    -- while loop
                elseif ahead == "[[" then
                    k = j + 1;
                    if area:byte( k, k ) == 93 then
                        r = k;
                        break;    -- while loop
                    end
                end
            elseif c == 58 then    -- :
                s = area:sub( j + 1,  j + 2 );
                if s == "//" then
                    s = " " .. area:sub( 1,  j + 2 );
                    s = s:match( "%s(%a+://)$" );
                    if s  and  TemplUtl.feasible( s ) then
                        s = area .. " ";
                        s = s:match( "([^%s|]+)%s", j );
                        if s then
                            k = s:find( "}}" );
                            if k then
                                j = j + k + 1;
                            else
                                j = j + s:len();
                            end
                        end
                    end
                end
            end
            j = j + 1;
        else
            loop = false;
        end
    end -- while loop
    if not r then
        r = area:len();
    end
    return r, loop;
end -- TemplUtl.feed()



TemplUtl.feeder = function ( area, at )
    -- Retrieve all parameters
    -- Precondition:
    --     area   -- string; template transclusion
    --     at     -- optional number; byte position in area of "{{"
    -- Postcondition:
    --    Returns
    --          -- table
    --              [0]       -- template, page, parser function name
    --              [1]       -- unnamed parameter
    --              ["name"]  -- named parameter
    --          -- string; error message, if any, else nil
    local n = 0;
    local j, k, p, r, r2, s, v;
    if type( at ) == "number" then
        j = at + 2;
    else
        j = 3;
    end
    while true do
        k = TemplUtl.feed( area, false, j );
        s = area:sub( j, k );
        s = s:gsub( "<!--.*-->", "" );
        if n == 0 then
            r = { [ 0 ] = s };
            n = 1;
        else
            p, v = s:match( "^([^=]*)=(.*)$" );
            if p then
                if p:match( "^%s*%d+%s*$" )  then
                    p = tonumber( p );
                else
                    p = mw.text.trim( p );
                end
                v = mw.text.trim( v );
            else
                p = n;
                v = s;
                n = n + 1;
            end
            if r[ p ] then
                if r2 then
                    r2 = r2 .. " * ";
                else
                    r2 = "";
                end
                r2 = string.format( "%s%s '%s'",
                                    r2,
                                    "duplicated parameter",
                                    tostring( p ) );
            end
            r[ p ] = v;
        end
        s = area:sub( k + 1,  k + 2 );
        if s == "}}" then
            break;    -- while true
        elseif s == "" then
            r2 = "template not closed";
            break;    -- while true
        end
        j = k + 2;
    end -- while true
    return r, r2;
end -- TemplUtl.feeder()



TemplUtl.fetch = function ( area, ask )
    -- Find assignment of a named template parameter
    -- Precondition:
    --     area  -- string; template transclusion
    --     ask   -- string; parameter name
    -- Postcondition:
    --    Returns string with trimmed parameter value, or nil
    --     Does not return value if template inside
    local r;
    local scan = string.format( "%s%s%s",
                                "|%s*", ask, "%s*=(.+)$" );
    r = mw.ustring.match( area, scan );
    if r then
        local j = TemplUtl.feed( r, false, 1 );
        r = r:sub( 1, j );
        if r then
            r = mw.text.trim( r );
            if r == "" then
                r = nil;
            end
        end
    end
    return r;
end -- TemplUtl.fetch()



TemplUtl.find = function ( area, access, at, alter )
    -- Find next occurrence of a template
    -- Precondition:
    --     area    -- string; where to search
    --     access  -- string; trimmed (template) title
    --     at      -- optional number; ustring position in area, if not 1
    --     alter   -- optional string; lowercase namespace pattern
    --                                 "" for article
    --                                 no colon (:)
    -- Postcondition:
    --    Returns ustring position of "{{" in area, or false
    -- Requires:
    --     fiatTitleRegExp()
    local scan = string.format( "{{%s%s%s",
                                "([%w_%s:]*)%s*",
                                fiatTitleRegExp( access ),
                                "%s*([|}<]!?)" );
    local r, space, start, suffix;
    if type( at ) == "number" then
        r = at;
    else
        r = 1;
    end
    while true do
        r = mw.ustring.find( area, scan, r );
        if r then
            start, suffix = mw.ustring.match( area, scan, r );
            if start then
                start = mw.text.trim( start );
                if start == "" then
                    break; -- while true
                elseif alter then
                    if not space then
                        space = string.format( "^:?%s:$", alter );
                    end
                    start = mw.ustring.lower( start );
                    if mw.ustring.match( start, space ) then
                        break; -- while true
                    end
                else
                    start = start:match( "^:?(.+):$" );
                    if start then
                        start = mw.ustring.lower( start );
                        if start == "template" then
                            break; -- while true
                        else
                            if not space then
                                space = mw.site.namespaces[ 10 ].name;
                                space = mw.ustring.lower( space );
                            end
                            start = start:gsub( "_", " " )
                                         :gsub( "%s+", " " );
                            if start == space then
                                break; -- while true
                            end
                        end
                    end
                end
            else
                break; -- while true
            end
            r = r + 2;
        else
            r = false;
            break; -- while true
        end
    end -- while true
    return r;
end -- TemplUtl.find()


-- finder()
--      1 page name
--      2 template title / page name
--      3 4 5 6
--        more like 2



TemplUtl.firstbreak = function ( adjust )
    -- Precede leading character with newline if specific syntax
    -- Precondition:
    --    adjust  -- string; trimmed wikitext
    -- Postcondition:
    --    Returns string, modified if necessary
    return fallible( adjust, true );
end -- TemplUtl.firstbreak()



TemplUtl.flat = function ( area )
    -- Remove syntax elements that hide effective syntax only
    -- Precondition:
    --     area  -- string; unparsed wikitext to be reduced
    -- Postcondition:
    --    Returns cleared wikitext
    local delimiters = { { "<%s*NOWIKI%s*>", "<%s*/%s*NOWIKI%s*>" },
                         { "<!--", "-->", true },
                         { "<%s*PRE%s*>", "<%s*/%s*PRE%s*>" },
                         { "<%s*SYNTAXHIGHLIGHT[^<>]*>",
                           "<%s*/%s*SYNTAXHIGHLIGHT%s*>" } };
    local i          = 1;
    local r          = area;
    local k, m, n;
    if not TemplUtl.Delimiters then
        local c, sD, sP;
        TemplUtl.Delimiters = { };
        for j = 1, #delimiters do
            table.insert( TemplUtl.Delimiters, { } );
            for ji = 1, 2 do
                sD = delimiters[ j ][ ji ];
                sP = "";
                for js = 1, #sD, 1 do
                    c = sD:byte( js, js );
                    if c >= 65  and  c <= 90 then
                        sP = string.format( "%s[%c%c]",
                                            sP,  c,  c + 32 );
                    else
                        sP = sP .. string.char( c );
                    end
                end -- for js
                table.insert( TemplUtl.Delimiters[ j ], sP );
            end -- for ji
        end -- for j
    end
    while ( true ) do
        k = false;
        for j = 1, #delimiters do
            m = r:find( TemplUtl.Delimiters[ j ][ 1 ],
                        i,
                        TemplUtl.Delimiters[ j ][ 3 ] );
            if m  and  ( not k  or  m < k ) then
                k = m;
                n = j;
            end
        end -- for j
        if k then
            local s
            if k > 1 then
                i = k - 1;
                s = r:sub( 1, i );
            else
                s = "";
            end
            j, m  =  r:find( TemplUtl.Delimiters[ n ][ 2 ],
                             k + 1,
                             TemplUtl.Delimiters[ n ][ 3 ] );
            if m then
                r = s  ..  r:sub( m + 1 );
            else
                r = s;
                break; -- while true
            end
        else
            break; -- while true
        end
    end -- while true
    return r;
end -- TemplUtl.flat()



TemplUtl.nowiki1 = function ( adjust )
    -- HTML-escape leading character if disturbing syntax
    -- Precondition:
    --    adjust  -- string; trimmed wikitext
    -- Postcondition:
    --    Returns string, modified if necessary
    return fallible( adjust, false );
end -- TemplUtl.nowiki1()



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.facets( frame )
    return TemplUtl.facets( frame.args[ 1 ]  or  "",
                            frame.args.decode );
end -- p.facets

function p.faculty( frame )
    local r = TemplUtl.faculty( frame.args[ 1 ],
                                frame.args[ 2 ] );
    if r ~= "-" then
        r = r and "1";
    end
    return r or "";
end -- p.faculty

function p.failure( frame )
    local scream = mw.text.trim( frame.args[ 1 ]  or  "" );
    local loud   = frame.args[ 2 ];
    local select = frame.args.class;
    if scream == "" then
        scream = "?????????";
    end
    if loud then
        loud = TemplUtl.faculty( loud, nil );
    end
    return TemplUtl.failure( scream, loud, select, frame );
end -- p.failure

function p.fake( frame )
    TemplUtl.fake( frame.args[ 1 ]  or  "",  frame );
    return "";
end -- p.fake

function p.fakes( frame )
    local list = ( frame.args.list == "1" );
    local r    = TemplUtl.fakes( frame.args,
                                 frame,
                                 frame.args.prefix,
                                 list );
    return r or "";
end -- p.fakes

function p.firstbreak( frame )
    local r = ( frame.args[ 1 ] );
    if r then
        r = mw.text.trim( r );
        if r ~= "" then
            r = TemplUtl.firstbreak( r );
        end
    end
    return r or "";
end -- p.firstbreak

function p.from( frame )
    local r = frame:getParent():getTitle();
    if r then
        r = string.format( "&#123;&#123;%s&#125;&#125;", r );
    end
    return r or "";
end -- p.from

function p.isRedirect()
    return mw.title.getCurrentTitle().isRedirect and "1"  or  "";
end -- p.isRedirect

function p.nowiki1( frame )
    local r = ( frame.args[ 1 ] );
    if r then
        r = mw.text.trim( r );
        if r ~= "" then
            r = TemplUtl.nowiki1( r );
        end
    end
    return r or "";
end -- p.nowiki1

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

p.TemplUtl = function ()
    return TemplUtl;
end -- p.TemplUtl()

setmetatable( p,  { __call = function ( func, ... )
                                 setmetatable( p, nil );
                                 return Failsafe;
                             end } );

return p;