Modul:TransText

aus Wikipedia, der freien Enzyklopädie
local TransText = { suite   = "TransText",
                    serial  = "2019-10-30",
                    item    = 0,
                    globals = { ISO15924 = 71584769 } }
local Config = {
    errBaseInvalid   = { en = "Base code invalid",
                         de = "Ausgangscode ungültig" },
    errBaseMissing   = { en = "Base code missing",
                         de = "Ausgangscode fehlt" },
    errBaseUnknown   = { en = "Base code unknown:",
                         de = "Ausgangscode unbekannt:" },
    errCompInvalid   = { en = "Definition invalid",
                         de = "Definition ungültig" },
    errCompMissing   = { en = "Definition missing",
                         de = "Definition fehlt" },
    errDefMissing    = { en = "Definition module missing",
                         de = "Definitionsmodul fehlt" },
    errTargetMissing = { en = "Target code missing",
                         de = "Zielcode fehlt" },
    errTextCoding    = { en = "Wrong encoding of base text",
                         de = "Schriftzeichen im Ausgangstext falsch" },
    errTextMissing   = { en = "Text missing",
                         de = "Ausgangstext fehlt" },
    errTransCoding   = { en = "Wrong encoding in result text",
                         de = "Schriftzeichen im Ergebnistext falsch" },
    errMissing       = { en = "Missing parameter",
                         de = "Parameter fehlt" },
    errUnkown        = { en = "Unkown parameter:",
                         de = "Parameter unbekannt:" }
               }
local Failsafe  = TransText
local GlobalMod = TransText
local Query = { slang    = "und",
                script   = false,
                template = { ["@"] = "lang",
                             ["#"] = "1",
                             ["*"] = "2" }
              }
local Data



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
    -- 2019-10-29
    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, 0 )
        end
    end
    return r
end -- foreignModule()



local function Feed( all )
    local r = { }
    local val
    for k, v in pairs( all ) do
        if type( k ) == "number"  or  k:match( "^%d" ) then
            for ik, iv in pairs( all ) do
                if type( iv ) == "table" then
                    val = Feed( iv )
                else
                    val = iv
                end
                table.insert( r, val )
            end -- for ik, iv
            break -- for k, v
        else
            if type( v ) == "table" then
                r[ k ] = Feed( v )
            else
                r[ k ] = v
            end
        end
    end -- for k, v
    return r
end -- Feed()



local function Fellow()
    -- Attach ISO15924 library
    -- Throws error, if library not existing
    local r
    if not TransText.ISO15924 then
        local got = foreignModule( "ISO15924",
                                   true,
                                   false,
                                   TransText.globals.ISO15924,
                                   true )
        if type( got ) == "table"  and
           type( got.ISO15924 ) == "function" then
            TransText.ISO15924 = got.ISO15924()
        end
        if type( TransText.ISO15924 ) == "table" then
            r = TransText.ISO15924
        else
            error( "Library ISO15924 unavailable" )
        end
    end
    return r
end -- Fellow()



local function Fetch( at )
    -- Attempt to load Data
    -- Precondition:
    --    at   -- string, page name
    -- Returns table, or not if invalid
    -- Throws error, if not existing
    local d = mw.loadData( at )
    local r
    if type( d ) == "table"  and
       type( d.data ) == "table" then
        r = Feed( d.data )
    else
        r = { }
    end
    return r
end -- Fetch()



local function Frame()
    -- Fetch current frame
    -- Returns frame
    if not Query.frame then
        Query.frame = mw.getCurrentFrame()
    end
    return Query.frame
end -- Frame()



local function facility()
    -- Fetch current site language
    -- Returns language code
    local r
    if Data then
        r = Data.stdLang
    end
    if not r then
        r = mw.language.getContentLanguage():getCode()
        if Data then
            Data.stdLang = r
        end
    end
    return r
end -- facility()



local function factory( apply )
    -- Localization of messages
    --     apply  -- string, with message key
    -- Returns message text; at least english
    local entry = Config[ apply ]
    local r
    if entry then
        r = entry[ facility() ]
        if not r then
            r = entry.en
        end
    else
        r = string.format( "????.%s.????", apply )
    end
    return r
end -- factory()



local function failure( alert, about )
    -- Format message with class="error"
    --     alert   -- string, with message key
    --     about   -- string, with explanation
    -- Returns message with markup
    local story = factory( alert )
    local err   = mw.html.create( "span" )
                         :addClass( "error" )
    local env = Frame():getParent()
    if env then
        story = string.format( "[[%s]] – %s",
                               env:getTitle(), story )
    end
    if about then
        story = string.format( "%s %s", story, about )
    end
    err:wikitext( story )
    return  tostring( err )
end -- failure()



local function fetch( assigned )
    -- Load data page
    -- Precondition:
    --     assigned   -- string or nil, for sub module
    -- Returns table, if assigned, or not
    -- Throws error, if not existing
    local sub = string.format( "%s/data", Frame():getTitle() )
    local r
    if not Data then
        Data = Fetch( sub )
    end
    if assigned then
        sub = string.format( "%s/%s", sub, assigned )
        r = Fetch( sub )
    end
    return r
end -- fetch()



local function fidelity( attempt, accept, after )
    -- Check characters for compatibility with script
    -- Precondition:
    --     attempt  -- string, source text
    --     accept   -- string, script specifier
    --     after    -- true: for result text
    -- Returns false, if okay, else string with coding sequence
    local bib = Fellow()
    local r
    if bib then
        local e, cp = bib.isScript( accept, attempt )
        if e then
            r = false
        else
            local scream
            if after then
                scream = "errTransCoding"
            else
                scream = "errTextCoding"
            end
            r = failure( scream,  bib.showScripts( cp ) )
        end
    end
    return r
end -- fidelity()



local function finish( adjust, about )
    -- Finalize template parameter text
    -- Precondition:
    --     adjust  -- string, with source text
    --     about   -- string or nil, with script or lang code
    -- Returns string, with source text
    local r = adjust:gsub( "{{", "{{" )
                    :gsub( "|",  "|" )
                    :gsub( "}}", "}}" )
    if about  and  TransText.ISO15924.isRTL( about ) then
        r = r .. "‎"
    end
    if not mw.isSubsting() then
        r = r:gsub( "&(#?x?[lrm%x]+;)", "&%1" )
    end
    return r
end -- finish()



local function first( a1, a2 )
    -- Compare a1 with a2
    --     a1  -- string, with name
    --     a2  -- string, with name
    -- Returns true if a1 < a2
    local f = function( a )
                  local d = { n = 0, s = "" }
                  local k = a
                  local s = type( k )
                  if s == "string" then
                      if k:match( "^%d+$" ) then
                          d.n = tonumber( k ) - 100
                      else
                          d.s = k
                          if k:match( "^%l+$" ) then
                              d.n = 0.1
                          elseif k:sub( 1, 3 ) == "ISO" then
                              local n, m = k:match( "^ISO(%d+)%-(%d+)$" )
                              if n then
                                  d.n = tonumber( n )
                                        + tonumber( m ) * 0.0001
                              else
                                  n   = k:match( "^ISO(%d+)$" )
                                  d.n = tonumber( n )
                              end
                          else
                              d.n = 1000000
                          end
                      end
                  elseif s == "number" then
                      d.n = k - 100
                  end
                  return d
              end
    local d1 = f( a1 )
    local d2 = f( a2 )
    local r
    if d1.n == d2.n then
        r = ( d1.s < d2.s )
    else
        r = ( d1.n < d2.n )
    end
    return r
end -- first()



local function flat( adjust )
    -- Make string of specification
    -- Precondition:
    --     adjust  -- string, number, table, with specification
    -- Returns string, with specification
    local r = adjust
    local s = type( r )
    if s == "number" then
        r = mw.ustring.char( r )
    elseif s == "string" then
    elseif s == "table" then
        local collection = {}
        for k, v in pairs( r ) do
            s = type( v )
            if s == "number" then
                table.insert( collection, mw.ustring.char( v ) )
            elseif s == "string" then
                table.insert( collection, v )
            else
                table.insert( collection, "/????/" )
            end
        end -- for k, v
        r = table.concat( collection )
    end
    return r
end -- flat()



local function flip( adjust, apply )
    -- Replace by set of string pattern rules
    -- Precondition:
    --     adjust  -- string, with text
    --     apply   -- sequence table, with tupels { seek, set }
    -- Returns string, with text
    local r = adjust
    local seek, set, v
    for i = 1, #apply do
        v          = apply[ i ]
        seek       = flat( v[ 1 ] )
        set        = flat( v[ 2 ] )
        apply[ i ] = { seek, set }
        r          = mw.ustring.gsub( r, seek, set )
    end -- for i
    return r
end -- flip()



local function flipper( apply, append )
    -- Extend table by set of replacement rules
    -- Precondition:
    --     apply   -- sequence table or nil, to be extended
    --     append  -- table, to be appended
    -- Returns sequence table with replacement rules
    local r = apply
    local v
    if type( r ) ~= "table" then
        r = { }
    end
    for i = 1, #append do
        v = append[ i ]
        table.insert( r,
                      { flat( v[ 1 ] ),
                        flat( v[ 2 ] ) } )
    end -- for i
    return r
end -- flipper()



local function flush( adjust )
    -- Cleanup for whitespace and invisible characters
    -- Precondition:
    --     adjust  -- string, with source text
    -- Returns string, with source text
    local r = adjust
    local p
    if r:find( "&", 1, true ) then
        r = mw.text.decode( r, true )
    end
    p = mw.ustring.char( 91, 0x200E, 45, 0x200F,
                             0x202A, 45, 0x202E,
                             0x2066, 45, 0x2069, 93 )
    r = mw.ustring.gsub( r, p, "" )
    p = mw.ustring.char( 91, 0x0001, 45, 0x001F,
                             0x00A0,
                             0x2002, 45, 0x200A,
                             0x202F, 93 )
    r = mw.ustring.gsub( r, p, " " )
    return r
end -- flush()



local function focus( achieve, all )
    -- Merge target specifications
    -- Precondition:
    --     achieve  -- table, with specification
    --     all      -- true: include replacements
    -- Postcondition:
    --     specification expanded and resolved
    local use = { }
    local s
    if type( Data.use ) ~= "table" then
        Data.use = { }
    end
    if type( achieve.use ) == "string" then
        table.insert( use, achieve.use )
    elseif type( achieve.use ) == "table" then
        for k, v in pairs( achieve.use ) do
            table.insert( use, v )
        end -- for k, v
    end
    achieve.use = false
    for i = 1, #use do
        s = use[ i ]
        if Data.use[ s ] then
            error( "Recursive loop: " .. s )
            break -- for i
        else
            Data.use[ s ] = true
            part = Data.trans[ s ]
            if type( part ) == "table" then
                if part.use then
                    focus( part, all )
                end
                if type( part.script ) == "string" and
                   type( achieve.script ) ~= "string" then
                    achieve.script = part.script
                end
                if all  and  type( part.replace ) == "table" then
                    achieve.replace = flipper( achieve.replace,
                                               part.replace )
                end
            else
                error( "Bad transclusion: " .. s )
            end
        end
    end -- for i
end -- focus()



local function fold( above, all, at )
    -- Merge base specifications
    -- Precondition:
    --     above  -- table, with task
    --     all    -- true: include replacements
    --     at     -- string or nil, with top entry
    -- Returns string, with error message, if any
    local use = { }
    local r, part
    if type( above.use ) == "string" then
        table.insert( use, above.use )
    elseif type( above.use ) == "table" then
        for k, v in pairs( above.use ) do
            table.insert( use, v )
        end -- for k, v
    end
    above.use = false
    if at then
        table.insert( use, at )
    end
    for i = 1, #use do
        part = Data[ use[ i ] ]
        if type( part ) == "table" then
            if part.use then
                r = fold( part, all )
            end
            if type( part.script ) == "string" then
                Query.script = part.script
            end
            if all  and  type( part.replace ) == "table" then
                Data.replace = flipper( Data.replace, part.replace )
            end
            if type( part.targets ) == "table" then
                for k, v in pairs( part.targets ) do
                    Data.trans[ k ] = v
                end -- for k, v
            end
        else
            r = failure( "errCompInvalid", v )
            break -- for i
        end
    end -- for i
    return r
end -- fold()



local function foreign( achieve )
    -- Execute transformation
    -- Precondition:
    --     achieve  -- table, with specification
    -- Returns table with components
    --     text    -- string, with transformed text
    --     script  -- string, with transformed text
    --     error   -- string, with problem
    local r = { text = Query.source }
    focus( achieve, true )
    if type( achieve.replace ) == "table" then
        r.text = flip( r.text, achieve.replace )
    end
    if type( achieve.script ) == "string" then
        r.script = achieve.script
        r.error  = fidelity( r.text, achieve.script, true )
    end
    return r
end -- foreign()



local function forward()
    -- Create template transclusion
    -- Returns string, with wikisyntax text
    local o = { }
    local t = { }
    local e, r, s, x
    if Query.template[ "@" ]:find( "%s", 1, true ) then
        Query.template[ "@" ] = string.format( Query.template[ "@" ],
                                               Query.seek )
    end
    r = string.format( "{{%s", Query.template[ "@" ] )
    e = Query.template[ "#" ]
    if e then
        s = Query.slang
        if Query.script then
            s = string.format( "%s-%s", s, Query.script )
        end
        t[ e ] = s
        table.insert( o, e )
    end
    e = Query.template[ "*" ]
    if e then
        t[ e ] = finish( Query.source,  Query.script or Query.slang )
        table.insert( o, e )
    end
    for k, v in pairs( Query.trans ) do
        if v.text then
            if Query.template[ k ] then
                k = Query.template[ k ]
            end
            t[ k ] = finish( v.text,  v.script or v.slang )
            table.insert( o, k )
            if v.error then
                x = x or ""
                x = string.format( "%s %s", x, v.error )
            end
        end
    end -- for k, v
    table.sort( o, first )
    for i = 1, #o do
        e = o[ i ]
        s = t[ e ]
        if not e:match( "^%d+$" )  or
           s:find( "=", 1, true )  then
            s = string.format( "%s=%s", e, s )
        end
        r = string.format( "%s |%s", r, s )
    end -- for i
    r = r .. "}}"
    if x then
        r = r .. x
    end
    return r
end -- forward()



local function fresh( accept, all )
    -- Build environment
    -- Precondition:
    --     accept  -- string, language and/or script specifier
    --     all     -- true: include replacements
    -- Postcondition:
    --     Returns string, if error
    local r
    if accept ~= ""  and  type( accept ) == "string" then
        Query.seek = accept
        if Query.seek:match( "^%l%l%l?%-?" ) then
            Query.slang  = Query.seek:match( "^(%l%l%l?)$" )  or
                           Query.seek:match( "^(%l%l%l?)%-%u%u$" )  or
                           Query.seek:match( "^(%l%l%l?)%-%u%l%l%l$" )
            Query.script = Query.seek:match( "^%l+%-(%u%l%l%l)$" )
            if not Query.script then
                local bib = Fellow()
                if bib then
                    Query.script = bib.getLanguageScript( Query.slang )
                end
            end
        else
            Query.script = Query.seek:match( "^(%u%l%l%l)$" )
        end
        if not ( Query.script or Query.slang ) then
            r = failure( "errBaseInvalid", Query.seek )
        end
        if not r then
            local lucky
            lucky, r = pcall( fetch )
            if lucky then
                if Data[ Query.seek ] then
                    Data.trans = { }
                    Query.task = Data[ Query.seek ]
                    if Query.task.shift  and
                       Data[ Query.task.shift ] then
                        Query.seek = Query.task.shift
                        Query.task = Data[ Query.seek ]
                    end
                    if type( Query.task.extern ) == "string" then
                        Query.task.extern = { Query.task.extern }
                    end
                    if type( Query.task.extern ) == "table" then
                        local lucky, part
                        for k, v in pairs( Query.task.extern ) do
                            lucky, part = pcall( fetch, v )
                            if type( part ) == "table" then
                                for rk, rv in pairs( part ) do
                                    Data[ rk ] = rv
                                end -- for rk, rv
                            else
                                r = failure( "errDefMissing", v )
                                break -- for k, v
                            end
                        end -- for k, v
                    end
                    if not r then
                        r = fold( Query.task, all, Query.seek )
                    end
                else
                    r = failure( "errBaseUnknown", Query.seek )
                end
            else
                r = failure( "errDefMissing" )
            end
        end
    else
        r = failure( "errBaseMissing" )
    end
    return r
end -- fresh()



local function furnish()
    -- Execute trans series
    -- Returns string, with text
    local part, r
    if type( Query.task ) == "table" then
        Query.source = flush( Query.source )
        if Data.replace then
            Query.source = flip( Query.source, Data.replace )
        end
        r = fidelity( Query.source, Query.script )
        if not r then
            local s
            for i = 1, #Query.targets do
                s    = Query.targets[ i ]
                part = Data.trans[ s ]
                if type( part ) == "table" then
                    Query.trans = Query.trans  or  { }
                    Query.trans[ s ] = foreign( part )
                else
                    if type( s ) == "string"  and  s ~= "" then
                        s = ": " .. s
                    else
                        s = false
                    end
                    r = failure( "errTargetMissing", s )
                    break -- for i
                end
            end -- for i
        end
    else
        r = failure( "errCompMissing", Query.seek )
    end
    if not r  and  Query.trans then
        r = forward()
    end
    return r
end -- furnish()



Failsafe.failsafe = function ( atleast )
    -- Retrieve versioning and check for compliance
    -- Precondition:
    --     atleast  -- string, with required version or "wikidata" or "~"
    --                 or false
    -- Postcondition:
    --     Returns  string  -- with queried version, also if problem
    --              false   -- if appropriate
    -- 2019-10-15
    local last  = ( atleast == "~" )
    local since = atleast
    local r
    if last  or  since == "wikidata" then
        local item = Failsafe.item
        since = false
        if type( item ) == "number"  and  item > 0 then
            local entity = mw.wikibase.getEntity( string.format( "Q%d",
                                                                 item ) )
            if type( entity ) == "table" then
                local 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
                    else
                        r = vsn.value
                    end
                end
            end
        end
    end
    if type( r ) == "nil" then
        if not since  or  since <= Failsafe.serial then
            r = Failsafe.serial
        else
            r = false
        end
    end
    return r
end -- Failsafe.failsafe()



TransText.fiat = function ( accept, adjust, adapt, alter )
    -- Main entry
    -- Precondition:
    --     accept  -- string, language and/or script specifier
    --     adjust  -- string, source text
    --     adapt   -- table, with required targets
    --     alter   -- string or nil, with JSON template spec
    -- Postcondition:
    --     Returns string
    local r = fresh( accept, true )
    if not r then
        local s
        if type( adjust ) == "string" then
            s = mw.text.trim( adjust )
            if s == "" then
                r = failure( "errTextMissing" )
            else
                Query.source = s
            end
        else
            r = failure( "errTextMissing" )
        end
        if not r then
            s = type( adapt )
            if s == "table" then
                Query.targets = adapt
            elseif s == "string" then
                Query.targets = { adapt }
            end
            if Query.targets then
                if alter then
                    local lucky
                    lucky, r = pcall( mw.text.jsonDecode,
                                      alter )
                    if type( r ) == "table" then
                        Query.template = r
                    end
                end
                r = furnish()
            else
                r = failure( "errTargetMissing" )
            end
        end
    end
    return r
end -- TransText.fiat()



TransText.forwarding = function ( accept )
    -- Retrieve available targets for this source
    -- Precondition:
    --     accept  -- string, language and/or script specifier
    -- Postcondition:
    --     Returns table with appropriate keys,
    --             or false,
    --             or string with error message
    local r = fresh( accept, false )
    if not r  and  Data.trans then
        r = { }
        for k, v in pairs( Data.trans ) do
            table.insert( r, k )
        end -- for k, v
        table.sort( r, first )
        for i = #r - 1, 1, -1 do
            if r[ i + 1 ] == r[ i ] then
                table.remove( r, i + 1 )
            end
        end -- for i
    end
    return r
end -- TransText.forwarding()



-- Export
local p = { }



p.fiat = function ( frame )
    -- Main task
    --     1         -- language or script code of request text
    --     2         -- request text
    --     3         -- code of first transformation
    --     template  -- 1 for template data
    local s = mw.text.trim( frame.args[ 3 ]  or  "" )
    local r
    if s ~= "" then
        local start  = mw.text.trim( frame.args[ 1 ]  or  "" )
        local source = mw.text.trim( frame.args[ 2 ]  or  "" )
        if start ~= ""  and  source ~= "" then
            local syntax = mw.text.trim( frame.args.template  or  "" )
            local trans  = { }
            table.insert( trans, s )
            for k, v in pairs( frame.args ) do
                if type( k ) == "number"  and  k > 3 then
                    s = mw.text.trim( v )
                    if s ~= "" then
                        table.insert( trans, s )
                    end
                end
            end -- for k, v
            Query.frame = frame
            r = TransText.fiat( start, source, trans, syntax )
        end
    end
    return r or ""
end -- p.fiat



p.forwarding = function ( frame )
    -- Available targets for this source
    local s  = mw.text.trim( frame.args[ 1 ]  or  "" )
    local r = TransText.forwarding( s )
    if type( r ) == "table" then
        if frame.args.code == "1" then
            local e
            for i = 1, #r do
                s = r[ i ]
                e = mw.html.create( "code" )
                           :wikitext( s )
                if s:find( "-", 1, true ) then
                    e:css( "white-space", "nowrap" )
                end
                r[ i ] = tostring( e )
            end -- for i
        end
        r = table.concat( r, " " )
    else
        r = ""
    end
    return r
end -- p.forwarding



p.from = function ( frame )
    -- Available sources
    local r
    return r or ""
end -- p.from



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.TransText = function ()
    return TransText
end -- p.TransText

return p