Module:Text
local Text = { serial = &quot;2024-06-05&quot;,
                suite  = &quot;Text&quot;,
                item   = 29387871 }
 --[=[
 Text utilities
 ]=]
 local Failsafe    = Text
 local GlobalMod   = Text
 local Patterns    = { }
 local RangesLatin = false
 local SeekQuote   = false
 
 
 
 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( &quot;%s/%s&quot;,
                                                    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,  &quot;Module:&quot; .. storage )
     end
     if not lucky then
         if not suited  and
            type( alt ) == &quot;number&quot;  and
            alt &gt; 0 then
             suited = string.format( &quot;Q%d&quot;, alt )
             suited = mw.wikibase.getSitelink( suited )
             GlobalMod.globalModules[ access ] = suited or true
         end
         if type( suited ) == &quot;string&quot; then
             storage = suited
             finer()
             lucky, r = pcall( fun, storage )
         end
         if not lucky and alert then
             error( &quot;Missing or invalid page: &quot; .. storage, 0 )
         end
     end
     return r
 end -- foreignModule()
 
 
 
 local function factoryQuote()
     -- Create quote definitions
     if not Text.quoteLang then
         local quoting = foreignModule( &quot;Text&quot;,
                                        false,
                                        &quot;quoting&quot;,
                                        Text.item )
         if type( quoting ) == &quot;table&quot; then
             Text.quoteLang = quoting.langs
             Text.quoteType = quoting.types
         end
         if type( Text.quoteLang ) ~= &quot;table&quot; then
             Text.quoteLang = { }
         end
         if type( Text.quoteType ) ~= &quot;table&quot; then
             Text.quoteType = { }
         end
         if type( Text.quoteLang.en ) ~= &quot;string&quot; then
             Text.quoteLang.en = &quot;ld&quot;
         end
         if type( Text.quoteType[ Text.quoteLang.en ] ) ~= &quot;table&quot; then
             Text.quoteType[ Text.quoteLang.en ] = { { 8220, 8221 },
                                                     { 8216, 8217 } }
         end
     end
 end -- factoryQuote()
 
 
 
 local function fiatQuote( apply, alien, advance )
     -- Quote text
     -- Parameter:
     --     apply    -- string, with text
     --     alien    -- string, with language code
     --     advance  -- number, with level 1 or 2
     local r = apply
     local quotes, suite
     factoryQuote()
     if alien then
         suite = mw.text.trim( alien )
         if suite == &quot;&quot; then
             suite = false
         else
             local s = Text.quoteLang[ suite ]
             if s then
                 suite = s
             else
                 local slang = suite:match( &quot;^(%l+)-&quot; )
                 if slang then
                     suite = Text.quoteLang[ slang ]
                 end
             end
         end
     end
     if not suite then
         suite = Text.quoteLang.en
     end
     quotes = Text.quoteType[ suite ]
     if quotes then
         local space
         if quotes[ 3 ] then
             space = &quot;&amp;#160;&quot;
         else
             space = &quot;&quot;
         end
         quotes = quotes[ advance ]
         if quotes then
             r = mw.ustring.format( &quot;%s%s%s%s%s&quot;,
                                    mw.ustring.char( quotes[ 1 ] ),
                                    space,
                                    apply,
                                    space,
                                    mw.ustring.char( quotes[ 2 ] ) )
         end
     else
         mw.log( &quot;fiatQuote() &quot; .. suite )
     end
     return r
 end -- fiatQuote()
 
 
 
 Text.char = function ( apply, again, accept )
     -- Create string from codepoints
     -- Parameter:
     --     apply   -- table (sequence) with numerical codepoints, or nil
     --     again   -- number of repetitions, or nil
     --     accept  -- true, if no error messages to be appended
     -- Returns: string
     local r
     if type( apply ) == &quot;table&quot; then
         local bad   = { }
         local codes = { }
         local s
         for k, v in pairs( apply ) do
             s = type( v )
             if s == &quot;number&quot; then
                 if v &lt; 32  and  v ~= 9  and  v ~= 10 then
                     v = tostring( v )
                 else
                     v = math.floor( v )
                     s = false
                 end
             elseif s ~= &quot;string&quot; then
                 v = tostring( v )
             end
             if s then
                 table.insert( bad, v )
             else
                 table.insert( codes, v )
             end
         end -- for k, v
         if #bad == 0 then
             if #codes &gt; 0 then
                 r = mw.ustring.char( unpack( codes ) )
                 if again then
                     if type( again ) == &quot;number&quot; then
                         local n = math.floor( again )
                         if n &gt; 1 then
                             r = r:rep( n )
                         elseif n &lt; 1 then
                             r = &quot;&quot;
                         end
                     else
                         s = &quot;bad repetitions: &quot; .. tostring( again )
                     end
                 end
             end
         else
             s = &quot;bad codepoints: &quot; .. table.concat( bad, &quot; &quot; )
         end
         if s  and  not accept then
             r = tostring(  mw.html.create( &quot;span&quot; )
                                   :addClass( &quot;error&quot; )
                                   :wikitext( s ) )
         end
     end
     return r or &quot;&quot;
 end -- Text.char()
 
 
 
 Text.concatParams = function ( args, apply, adapt )
     -- Concat list items into one string
     -- Parameter:
     --     args   -- table (sequence) with numKey=string
     --     apply  -- string (optional); separator (default: &quot;|&quot;)
     --     adapt  -- string (optional); format including &quot;%s&quot;
     -- Returns: string
     local collect = { }
     for k, v in pairs( args ) do
         if type( k ) == &quot;number&quot; then
             v = mw.text.trim( v )
             if v ~= &quot;&quot; then
                 if adapt then
                     v = mw.ustring.format( adapt, v )
                 end
                 table.insert( collect, v )
             end
         end
     end -- for k, v
     return table.concat( collect,  apply or &quot;|&quot; )
 end -- Text.concatParams()
 
 
 
 Text.containsCJK = function ( analyse )
     -- Is any CJK code within?
     -- Parameter:
     --     analyse  -- string
     -- Returns: true, if CJK detected
     local r
     if not Patterns.CJK then
         Patterns.CJK = mw.ustring.char( 91,
                                          0x3400, 45,  0x9FFF,
                                         0x20000, 45, 0x2B81F,
                                         93 )
     end
     if mw.ustring.find( analyse, Patterns.CJK ) then
         r = true
     else
         r = false
     end
     return r
 end -- Text.containsCJK()
 
 
 
 Text.getPlain = function ( adjust )
     -- Remove wikisyntax from string, except templates
     -- Parameter:
     --     adjust  -- string
     -- Returns: string
     local i = adjust:find( &quot;&lt;!--&quot;, 1, true )
     local r = adjust
     local j
     while i do
         j = r:find( &quot;--&gt;&quot;,  i + 3,  true )
         if j then
             r = r:sub( 1, i ) .. r:sub( j + 3 )
         else
             r = r:sub( 1, i )
         end
         i = r:find( &quot;&lt;!--&quot;, i, true )
     end    -- &quot;&lt;!--&quot;
     r = r:gsub( &quot;(&lt;/?%l[^&gt;]*&gt;)&quot;, &quot;&quot; )
          :gsub( &quot;'''(.+)'''&quot;, &quot;%1&quot; )
          :gsub( &quot;''(.+)''&quot;, &quot;%1&quot; )
          :gsub( &quot;&amp;nbsp;&quot;, &quot; &quot; )
     return mw.text.unstrip( r )
 end -- Text.getPlain()
 
 
 
 Text.isLatinRange = function ( adjust )
     -- Are characters expected to be latin or symbols within latin texts?
     -- Precondition:
     --     adjust  -- string, or nil for initialization
     -- Returns: true, if valid for latin only
     local r
     if not RangesLatin then
         RangesLatin = { {   0x07, 0x02AF },
                         { 0x1D6B, 0x1D9A },
                         { 0x1E00, 0x1EFF },
                         { 0x2002, 0x203A },
                         { 0x2190, 0x23BD } }
     end
     if not Patterns.Latin then
         local range
         Patterns.Latin = &quot;^[&quot;
         for i = 1, #RangesLatin do
             range = RangesLatin[ i ]
             Patterns.Latin = Patterns.Latin ..
                            mw.ustring.char( range[ 1 ], 45, range[ 2 ] )
         end    -- for i
         Patterns.Latin = Patterns.Latin .. &quot;]*$&quot;
     end
     if adjust then
         if mw.ustring.match( adjust, Patterns.Latin ) then
             r = true
         else
             r = false
         end
     end
     return r
 end -- Text.isLatinRange()
 
 
 
 Text.isQuote = function ( ask )
     -- Is this character any quotation mark?
     -- Parameter:
     --     ask  -- string, with single character
     -- Returns: true, if ask is quotation mark
     local r
     if not SeekQuote then
         SeekQuote = mw.ustring.char(   34,       -- &quot;
                                        39,       -- '
                                       171,       -- laquo
                                       187,       -- raquo
                                      8216,       -- lsquo
                                      8217,       -- rsquo
                                      8218,       -- sbquo
                                      8220,       -- ldquo
                                      8221,       -- rdquo
                                      8222,       -- bdquo
                                      8249,       -- lsaquo
                                      8250,       -- rsaquo
                                      0x300C,     -- CJK
                                      0x300D,     -- CJK
                                      0x300E,     -- CJK
                                      0x300F )    -- CJK
     end
     if ask == &quot;&quot; then
         r = false
     elseif mw.ustring.find( SeekQuote, ask, 1, true ) then
         r = true
     else
         r = false
     end
     return r
 end -- Text.isQuote()
 
 
 
 Text.listToText = function ( args, adapt )
     -- Format list items similar to mw.text.listToText()
     -- Parameter:
     --     args   -- table (sequence) with numKey=string
     --     adapt  -- string (optional); format including &quot;%s&quot;
     -- Returns: string
     local collect = { }
     for k, v in pairs( args ) do
         if type( k ) == &quot;number&quot; then
             v = mw.text.trim( v )
             if v ~= &quot;&quot; then
                 if adapt then
                     v = mw.ustring.format( adapt, v )
                 end
                 table.insert( collect, v )
             end
         end
     end -- for k, v
     return mw.text.listToText( collect )
 end -- Text.listToText()
 
 
 
 Text.quote = function ( apply, alien, advance )
     -- Quote text
     -- Parameter:
     --     apply    -- string, with text
     --     alien    -- string, with language code, or nil
     --     advance  -- number, with level 1 or 2, or nil
     -- Returns: quoted string
     local mode, slang
     if type( alien ) == &quot;string&quot; then
         slang = mw.text.trim( alien ):lower()
     else
         local pageLang = mw.title.getCurrentTitle().pageLanguage
         if pageLang then
             slang = pageLang.code
         else
             slang = mw.language.getContentLanguage():getCode()
         end
     end
     if advance == 2 then
         mode = 2
     else
         mode = 1
     end
     return fiatQuote( mw.text.trim( apply ), slang, mode )
 end -- Text.quote()
 
 
 
 Text.quoteUnquoted = function ( apply, alien, advance )
     -- Quote text, if not yet quoted and not empty
     -- Parameter:
     --     apply    -- string, with text
     --     alien    -- string, with language code, or nil
     --     advance  -- number, with level 1 or 2, or nil
     -- Returns: string; possibly quoted
     local r = mw.text.trim( apply )
     local s = mw.ustring.sub( r, 1, 1 )
     if s ~= &quot;&quot;  and  not Text.isQuote( s, advance ) then
         s = mw.ustring.sub( r, -1, 1 )
         if not Text.isQuote( s ) then
             r = Text.quote( r, alien, advance )
         end
     end
     return r
 end -- Text.quoteUnquoted()
 
 
 
 Text.removeDiacritics = function ( adjust )
     -- Remove all diacritics
     -- Parameter:
     --     adjust  -- string
     -- Returns: string; all latin letters should be ASCII
     --                  or basic greek or cyrillic or symbols etc.
     local cleanup, decomposed
     if not Patterns.Combined then
         Patterns.Combined = mw.ustring.char( 91,
                                              0x0300, 45, 0x036F,
                                              0x1AB0, 45, 0x1AFF,
                                              0x1DC0, 45, 0x1DFF,
                                              0xFE20, 45, 0xFE2F,
                                              93 )
     end
     decomposed = mw.ustring.toNFD( adjust )
     cleanup    = mw.ustring.gsub( decomposed, Patterns.Combined, &quot;&quot; )
     return mw.ustring.toNFC( cleanup )
 end -- Text.removeDiacritics()
 
 
 
 Text.removeWhitespace = function ( adjust )
     -- Remove all whitespace, or replace with ASCII space
     -- Parameter:
     --     adjust  -- string
     -- Returns: string; modified
     local r = mw.text.decode( adjust )
     if r:find( &quot;&amp;&quot;, 1, true ) then
         r = r:gsub( &quot;&amp;lrm;&quot;, &quot;&quot; )
              :gsub( &quot;&amp;rlm;&quot;, &quot;&quot; )
              :gsub( &quot;&amp;zwj;&quot;, &quot;&quot; )
              :gsub( &quot;&amp;zwnj;&quot;, &quot;&quot; )
              :gsub( &quot;&amp;thinsp;&quot;, &quot; &quot; )
              :gsub( &quot;&amp;ensp;&quot;, &quot; &quot; )
              :gsub( &quot;&amp;emsp;&quot;, &quot; &quot; )
     end
     if not Patterns.Whitespace then
        Patterns.Whitespace = mw.ustring.char( 0x00AD,
                                               91, 0x200C, 45, 0x200F, 93,
                                               91, 0x2028, 45, 0x202E, 93,
                                               0x205F,
                                               0x2060 )
        Patterns.Space      = mw.ustring.char( 0x00A0,
                                               0x1680,
                                               91, 0x2000, 45, 0x200A, 93,
                                               0x202F,
                                               0x205F,
                                               0x3000,
                                               0x303F )
     end
     r = mw.ustring.gsub( r, Patterns.Whitespace, &quot;&quot; )
     r = mw.ustring.gsub( r, Patterns.Space, &quot; &quot; )
     return mw.text.trim( r )
 end -- Text.removeWhitespace()
 
 
 
 Text.sentenceTerminated = function ( analyse )
     -- Is string terminated by dot, question or exclamation mark?
     --     Quotation, link termination and so on granted
     -- Parameter:
     --     analyse  -- string
     -- Returns: true, if sentence terminated
     local r  = mw.text.trim( analyse )
     local lt = r:find( &quot;&lt;&quot;, 1, true )
     if not Patterns.Terminated then
         Patterns.Terminated = mw.ustring.char( 91,
                                                0x3002,
                                                0xFF01,
                                                0xFF0E,
                                                0xFF1F )
                                            .. &quot;!%.%?…][\&quot;'%]‹›«»‘’“”]*$&quot;
     end
     if lt then
         r = r:gsub( &quot;&lt;/span&gt;&quot;, &quot;&quot; )
     end
     if mw.ustring.find( r, Patterns.Terminated ) then
         r = true
     elseif lt then
         local s = &quot;&lt;bdi[^&gt;]* dir=\&quot;([lr]t[rl])\&quot;.+&lt;/bdi&gt;&lt;/bdo&gt;&quot;
         s = r:match( s )
         if s then
             if mw.language.getContentLanguage():isRTL() then
                 r = ( s == &quot;ltr&quot; )
             else
                 r = ( s == &quot;rtl&quot; )
             end
         else
             r = false
         end
     else
         r = false
     end
     return r
 end -- Text.sentenceTerminated()
 
 
 
 Text.tokenWords = function ( adjust )
     -- Split text in words of digits or letters
     -- Precondition:
     --     adjust  -- string
     -- Returns: string with
     local r = mw.uri.decode( adjust, &quot;WIKI&quot; )
     if r:find( &quot;&amp;&quot;, 1, true ) then
         r = mw.text.decode( r )
     end
     r = Text.removeWhitespace( r )
     r = mw.ustring.gsub( r, &quot;[%p%s]+&quot;, &quot; &quot; )
     return r
 end -- Text.tokenWords()
 
 
 
 Text.ucfirstAll = function ( adjust )
     -- Capitalize all words
     -- Precondition:
     --     adjust  -- string
     -- Returns: string with all first letters in upper case
     local r = &quot; &quot; .. adjust
     local i = 1
     local c, j, m
     if adjust:find( &quot;&amp;&quot; ) then
         r = r:gsub( &quot;&amp;amp;&quot;,      &quot;&amp;#38;&quot; )
              :gsub( &quot;&amp;lt;&quot;,       &quot;&amp;#60;&quot; )
              :gsub( &quot;&amp;gt;&quot;,       &quot;&amp;#62;&quot; )
              :gsub( &quot;&amp;nbsp;&quot;,    &quot;&amp;#160;&quot; )
              :gsub( &quot;&amp;thinsp;&quot;, &quot;&amp;#8201;&quot; )
              :gsub( &quot;&amp;zwnj;&quot;,   &quot;&amp;#8204;&quot; )
              :gsub( &quot;&amp;zwj;&quot;,    &quot;&amp;#8205;&quot; )
              :gsub( &quot;&amp;lrm;&quot;,    &quot;&amp;#8206;&quot; )
              :gsub( &quot;&amp;rlm;&quot;,    &quot;&amp;#8207;&quot; )
         m = true
     end
     while i do
         i = mw.ustring.find( r, &quot;%W%l&quot;, i )
         if i then
             j = i + 1
             c = mw.ustring.upper( mw.ustring.sub( r, j, j ) )
             r = string.format( &quot;%s%s%s&quot;,
                                mw.ustring.sub( r, 1, i ),
                                c,
                                mw.ustring.sub( r, i + 2 ) )
             i = j
         end
     end -- while i
     r = r:sub( 2 )
     if m then
         r = r:gsub(     &quot;&amp;#38;&quot;, &quot;&amp;amp;&quot; )
              :gsub(     &quot;&amp;#60;&quot;, &quot;&amp;lt;&quot; )
              :gsub(     &quot;&amp;#62;&quot;, &quot;&amp;gt;&quot; )
              :gsub(    &quot;&amp;#160;&quot;, &quot;&amp;nbsp;&quot; )
              :gsub(   &quot;&amp;#8201;&quot;, &quot;&amp;thinsp;&quot; )
              :gsub(   &quot;&amp;#8204;&quot;, &quot;&amp;zwnj;&quot; )
              :gsub(   &quot;&amp;#8205;&quot;, &quot;&amp;zwj;&quot; )
              :gsub(   &quot;&amp;#8206;&quot;, &quot;&amp;lrm;&quot; )
              :gsub(   &quot;&amp;#8207;&quot;, &quot;&amp;rlm;&quot; )
              :gsub( &quot;&amp;#X(%x+);&quot;, &quot;&amp;#x%1;&quot; )
     end
     return r
 end -- Text.ucfirstAll()
 
 
 
 Text.uprightNonlatin = function ( adjust )
     -- Ensure non-italics for non-latin text parts
     --     One single greek letter might be granted
     -- Precondition:
     --     adjust  -- string
     -- Returns: string with non-latin parts enclosed in &lt;span&gt;
     local r
     Text.isLatinRange()
     if mw.ustring.match( adjust, Patterns.Latin ) then
         -- latin only, horizontal dashes, quotes
         r = adjust
     else
         local c
         local e = mw.html.create( &quot;span&quot; )
         local j = false
         local k = 1
         local m = false
         local n = mw.ustring.len( adjust )
         local p
         local flat = function ( a )
                   -- isLatin
                   local range
                   for i = 1, #RangesLatin do
                       range = RangesLatin[ i ]
                       if a &gt;= range[ 1 ]  and  a &lt;= range[ 2 ] then
                           return true
                       end
                   end    -- for i
               end -- flat()
         local focus = function ( a )
                   -- char is not ambivalent
                   local r = ( a &gt; 64 )
                   if r then
                       r = ( a &lt; 8192  or  a &gt; 8212 )
                   else
                       r = ( a == 38  or  a == 60 )    -- '&amp;' '&lt;'
                   end
                   return r
               end -- focus()
         local form = function ( a )
                 return string.format( p,
                                       r,
                                       mw.ustring.sub( adjust, k, j - 1 ),
                                       mw.ustring.sub( adjust, j, a ) )
               end -- form()
         e:attr( &quot;dir&quot;, &quot;auto&quot; )
          :css( &quot;font-style&quot;, &quot;normal&quot; )
          :wikitext( &quot;%s&quot; )
         p = &quot;%s%s&quot; .. tostring( e )
         r = &quot;&quot;
         for i = 1, n do
             c = mw.ustring.codepoint( adjust, i, i )
             if focus( c ) then
                 if flat( c ) then
                     if j then
                         if m then
                             if i == m then
                                 -- single greek letter.
                                 j = false
                             end
                             m = false
                         end
                         if j then
                             local nx = i - 1
                             local s  = &quot;&quot;
                             for ix = nx, 1, -1 do
                                 c = mw.ustring.sub( adjust, ix, ix )
                                 if c == &quot; &quot;  or  c == &quot;(&quot; then
                                     nx = nx - 1
                                     s  = c .. s
                                 else
                                     break -- for ix
                                 end
                             end -- for ix
                             r = form( nx ) .. s
                             j = false
                             k = i
                         end
                     end
                 elseif not j then
                     j = i
                     if c &gt;= 880  and  c &lt;= 1023 then
                         -- single greek letter?
                         m = i + 1
                     else
                         m = false
                     end
                 end
             elseif m then
                 m = m + 1
             end
         end    -- for i
         if j  and  ( not m  or  m &lt; n ) then
             r = form( n )
         else
             r = r .. mw.ustring.sub( adjust, k )
         end
     end
     return r
 end -- Text.uprightNonlatin()
 
 
 
 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
     -- 2024-03-01
     local since  = atleast
     local last   = ( since == &quot;~&quot; )
     local linked = ( since == &quot;@&quot; )
     local link   = ( since == &quot;item&quot; )
     local r
     if last  or  link  or  linked  or  since == &quot;wikidata&quot; then
         local item = Failsafe.item
         since = false
         if type( item ) == &quot;number&quot;  and  item &gt; 0 then
             local suited = string.format( &quot;Q%d&quot;, item )
             if link then
                 r = suited
             else
                 local entity = mw.wikibase.getEntity( suited )
                 if type( entity ) == &quot;table&quot; then
                     local seek = Failsafe.serialProperty or &quot;P348&quot;
                     local vsn  = entity:formatPropertyValues( seek )
                     if type( vsn ) == &quot;table&quot;  and
                        type( vsn.value ) == &quot;string&quot;  and
                        vsn.value ~= &quot;&quot; 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
         elseif link then
             r = false
         end
     end
     if type( r ) == &quot;nil&quot; then
         if not since  or  since &lt;= Failsafe.serial then
             r = Failsafe.serial
         else
             r = false
         end
     end
     return r
 end -- Failsafe.failsafe()
 
 
 
 Text.test = function ( about )
     local r
     if about == &quot;quote&quot; then
         factoryQuote()
         r = { QuoteLang = Text.quoteLang,
               QuoteType = Text.quoteType }
     end
     return r
 end -- Text.test()
 
 
 
 -- Export
 local p = { }
 
 function p.char( frame )
     local params = frame:getParent().args
     local story = params[ 1 ]
     local codes, lenient, multiple
     if not story then
         params = frame.args
         story  = params[ 1 ]
     end
     if story then
         local items = mw.text.split( story, &quot;%s+&quot; )
         if #items &gt; 0 then
             local j
             lenient  = ( params.errors == &quot;0&quot; )
             codes    = { }
             multiple = tonumber( params[ &quot;*&quot; ] )
             for k, v in pairs( items ) do
                 if v:sub( 1, 1 ) == &quot;x&quot; then
                     j = tonumber( &quot;0&quot; .. v )
                 elseif v == &quot;&quot; then
                     v = false
                 else
                     j = tonumber( v )
                 end
                 if v then
                     table.insert( codes,  j or v )
                 end
             end -- for k, v
         end
     end
     return Text.char( codes, multiple, lenient )
 end
 
 function p.concatParams( frame )
     local args
     local template = frame.args.template
     if type( template ) == &quot;string&quot; then
         template = mw.text.trim( template )
         template = ( template == &quot;1&quot; )
     end
     if template then
         args = frame:getParent().args
     else
         args = frame.args
     end
     return Text.concatParams( args,
                               frame.args.separator,
                               frame.args.format )
 end
 
 function p.containsCJK( frame )
     return Text.containsCJK( frame.args[ 1 ] or &quot;&quot; ) and &quot;1&quot; or &quot;&quot;
 end
 
 function p.getPlain( frame )
     return Text.getPlain( frame.args[ 1 ] or &quot;&quot; )
 end
 
 function p.isLatinRange( frame )
     return Text.isLatinRange( frame.args[ 1 ] or &quot;&quot; ) and &quot;1&quot; or &quot;&quot;
 end
 
 function p.isQuote( frame )
     return Text.isQuote( frame.args[ 1 ] or &quot;&quot; ) and &quot;1&quot; or &quot;&quot;
 end
 
 
 
 function p.listToFormat(frame)
     local lists = {}
     local pformat = frame.args[&quot;format&quot;]
     local sep = frame.args[&quot;sep&quot;] or &quot;;&quot;
 
     -- Parameter parsen: Listen
     for k, v in pairs(frame.args) do
         local knum = tonumber(k)
         if knum then lists[knum] = v end
     end
 
     -- Listen splitten
     local maxListLen = 0
     for i = 1, #lists do
         lists[i] = mw.text.split(lists[i], sep)
         if #lists[i] &gt; maxListLen then maxListLen = #lists[i] end
     end
 
     -- Ergebnisstring generieren
     local result = &quot;&quot;
     local result_line = &quot;&quot;
     for i = 1, maxListLen do
         result_line = pformat
         for j = 1, #lists do
             result_line = mw.ustring.gsub( result_line,
                                            &quot;%%s&quot;,
                                            lists[ j ][ i ],
                                            1 )
         end
         result = result .. result_line
     end
 
     return result
 end
 
 
 
 function p.listToText( frame )
     local args
     local template = frame.args.template
     if type( template ) == &quot;string&quot; then
         template = mw.text.trim( template )
         template = ( template == &quot;1&quot; )
     end
     if template then
         args = frame:getParent().args
     else
         args = frame.args
     end
     return Text.listToText( args, frame.args.format )
 end
 
 
 
 function p.quote( frame )
     local slang = frame.args[2]
     if type( slang ) == &quot;string&quot; then
         slang = mw.text.trim( slang )
         if slang == &quot;&quot; then
             slang = false
         end
     end
     return Text.quote( frame.args[ 1 ] or &quot;&quot;,
                        slang,
                        tonumber( frame.args[3] ) )
 end
 
 
 
 function p.quoteUnquoted( frame )
     local slang = frame.args[2]
     if type( slang ) == &quot;string&quot; then
         slang = mw.text.trim( slang )
         if slang == &quot;&quot; then
             slang = false
         end
     end
     return Text.quoteUnquoted( frame.args[ 1 ] or &quot;&quot;,
                                slang,
                                tonumber( frame.args[3] ) )
 end
 
 
 
 function p.removeDiacritics( frame )
     return Text.removeDiacritics( frame.args[ 1 ] or &quot;&quot; )
 end
 
 function p.sentenceTerminated( frame )
     return Text.sentenceTerminated( frame.args[ 1 ] or &quot;&quot; ) and &quot;1&quot; or &quot;&quot;
 end
 
 function p.tokenWords( frame )
     return Text.tokenWords( frame.args[ 1 ] or &quot;&quot; )
 end
 
 function p.ucfirstAll( frame )
     return Text.ucfirstAll( frame.args[ 1 ] or &quot;&quot; )
 end
 
 function p.unstrip( frame )
     return mw.text.trim( mw.text.unstrip( frame.args[ 1 ] or &quot;&quot; ) )
 end
 
 function p.uprightNonlatin( frame )
     return Text.uprightNonlatin( frame.args[ 1 ] or &quot;&quot; )
 end
 
 
 
 function p.zip(frame)
     local lists = {}
     local seps = {}
     local defaultsep = frame.args[&quot;sep&quot;] or &quot;&quot;
     local innersep = frame.args[&quot;isep&quot;] or &quot;&quot;
     local outersep = frame.args[&quot;osep&quot;] or &quot;&quot;
 
     -- Parameter parsen
     for k, v in pairs(frame.args) do
         local knum = tonumber(k)
         if knum then lists[knum] = v else
             if string.sub(k, 1, 3) == &quot;sep&quot; then
                 local sepnum = tonumber(string.sub(k, 4))
                 if sepnum then seps[sepnum] = v end
             end
         end
     end
     -- sofern keine expliziten Separatoren angegeben sind, den Standardseparator verwenden
     for i = 1, math.max(#seps, #lists) do
         if not seps[i] then seps[i] = defaultsep end
     end
 
     -- Listen splitten
     local maxListLen = 0
     for i = 1, #lists do
         lists[i] = mw.text.split(lists[i], seps[i])
         if #lists[i] &gt; maxListLen then maxListLen = #lists[i] end
     end
 
     local result = &quot;&quot;
     for i = 1, maxListLen do
         if i ~= 1 then result = result .. outersep end
         for j = 1, #lists do
             if j ~= 1 then result = result .. innersep end
             result = result .. (lists[j][i] or &quot;&quot;)
         end
     end
     return result
 end
 
 
 
 p.failsafe = function ( frame )
     -- Versioning interface
     local s = type( frame )
     local since
     if s == &quot;table&quot; then
         since = frame.args[ 1 ]
     elseif s == &quot;string&quot; then
         since = frame
     end
     if since then
         since = mw.text.trim( since )
         if since == &quot;&quot; then
             since = false
         end
     end
     return Failsafe.failsafe( since )  or  &quot;&quot;
 end -- p.failsafe()
 
 
 
 p.Text = function ()
     return Text
 end -- p.Text
 
 setmetatable( p,  { __call = function ( func, ... )
                                  setmetatable( p, nil )
                                  return Failsafe
                              end } )