require('strict')

local p = {}

local char = string.char
local floor = math.floor
local insert = table.insert
local ustring = require('mw.ustring')
local sub = ustring.sub
local upper = ustring.upper
local lower = ustring.lower

local language = mw.language
local isValidCode = language.isValidCode
local isValidBuiltInCode = language.isValidBuiltInCode
local isKnownLanguageTag = language.isKnownLanguageTag
local fetchLanguageName = language.fetchLanguageName
local languageNames = language.fetchLanguageNames()

local langs = require('Module:Multilingual description/sort/sandbox')
local dir = require('Module:Dir/sandbox')

local function toUTF8(code)
   -- Note: this function is stricter than the builtin function of the Lua library, but it
   -- detects without error invalid codes (which are "escaped" insterad of returning an error)
   if code >= 0 and code == floor(code) then
       if code <= 0x7F then -- use 1-byte UTF-8 for ASCII (including C0 controls)
           return char(code)
       elseif code <= 0x7FF then -- use 2-byte UTF-8 for characters in the lowest alphabetic part of the BMP (C1 controls, generic diacritics, and most frequently used characters for Latin, Greek, Cyrillic, Armenian, Hebrew, Arabic, Syriac, Thaana or N'Ko scripts)
           local b0, b1 = code % 64, floor(code / 64)
           return char(0xC0 + b1) .. char(0x80 + b0)
       elseif code <= 0xD7FF
           or code >= 0xE000 and code <= 0xFFCF
           or code >= 0xFFF0 and code <= 0xFFFD then -- use 3-byte UTF-8 for the rest of the BMP (excluding surrogate code points and non-characters)
           local b0, b1, b2 = code % 64, floor(code / 64)
           b1, b2 = b1 % 64, floor(b1 / 64)
           return char(0xE0 + b2) .. char(0x80 + b1) .. char(0x80 + b0)
       elseif code <= 0x10FFFF and code % 0x10000 <= 0xFFFD then -- use 4-byte UTF-8 for supplementary characters (excluding non-characters)
           local b0, b1, b2, b3 = code % 64, floor(code / 64)
           b1, b2 = b1 % 64, floor(b1 / 64)
           b2, b3 = b2 % 64, floor(b2 / 64)
           return char(0xF0 + b3) .. char(0x80 + b2) .. char(0x80 + b1) .. char(0x80 + b0)
       end
   end
   return '\\{' .. tostring(code) .. '}' -- not a valid Unicode code point, or non-character
end
local nce = { -- used in the first pass of htmlize(wikitext) for converting some known named character entities to UTF-8
    quot = '"',
    amp = '&',
    apos = "'",
    lt = '<',
    gt = '>',
    nbsp = toUTF8(0xA0),
    shy = toUTF8(0xAD),
    enquad = toUTF8(0x2000),
    emquad = toUTF8(0x2001),
    ensp = toUTF8(0x2002),
    emsp = toUTF8(0x2003),
    emsp13 = toUTF8(0x2004),
    emsp14 = toUTF8(0x2005),
    emsp16 = toUTF8(0x2006),
    numsp = toUTF8(0x2007),
    puncsp = toUTF8(0x2008),
    thinsp = toUTF8(0x2009),
    hairsp = toUTF8(0x200A),
    zwsp = toUTF8(0x200B),
    zwnj = toUTF8(0x200C),
    zwj  = toUTF8(0x200D),
    lrm  = toUTF8(0x200E),
    rlm  = toUTF8(0x200F),
    lsep = toUTF8(0x2028),
    psep = toUTF8(0x2029),
    lre = toUTF8(0x202A),
    rle = toUTF8(0x202B),
    pdf = toUTF8(0x202C),
    lro = toUTF8(0x202D),
    rlo = toUTF8(0x202E),
    nnbsp = toUTF8(0x202F),
    mathsp = toUTF8(0x205F),
    wj = toUTF8(0x2060), NoBreak = toUTF8(0x2060),
    lri = toUTF8(0x2066),
    rli = toUTF8(0x2067),
    fsi = toUTF8(0x2068),
    pdi = toUTF8(0x2069),
    iss = toUTF8(0x206A),
    ass = toUTF8(0x206B),
    iafs = toUTF8(0x206C),
    aafs = toUTF8(0x206D),
    natds = toUTF8(0x206E),
    nomds = toUTF8(0x206F),
    idsp = toUTF8(0x3000),
    zwnbsp  = toUTF8(0xFEFF),
}
local codemap = { -- will generate a stringmap used in the second pass htmlize(wikitext) for visual sanity check of the UTF-8 encoding of isolated characters
    -- Indexed by Unicode codepoint:
    -- Non-ASCII codepoints will be added to a stringmap (from their multibyte UTF-8 encoding), in an unspecified order of code point values, to replace them first by visual strings.
    -- ASCII codepoints will be converted using a single character pattern, after mapping all other non-ASCII codepoints from this table.
    [0x00] = '\\000',  -- <control> <NUL>      invalid in HTML (%z class in patterns)
    [0x01] = '\\001',  -- <control> <SOH>      invalid in HTML
    [0x02] = '\\002',  -- <control> <STX>      invalid in HTML
    [0x03] = '\\003',  -- <control> <ETX>      invalid in HTML
    [0x04] = '\\004',  -- <control> <EOT>      invalid in HTML
    [0x05] = '\\005',  -- <control> <ENQ>      invalid in HTML
    [0x06] = '\\006',  -- <control> <ACK>      invalid in HTML
    [0x07] = '\\a',    -- <control> <BEL> '\a' invalid in HTML
    [0x08] = '\\b',    -- <control> <BS>  '\b' invalid in HTML
    [0x09] = '&#9;',   -- <control> <HT>  '\t' valid in HTML (use source form, can be interpreted)
    [0x0A] = '&#10;',  -- <control> <LF>  '\n' valid in HTML (use source form, can be interpreted)
    [0x0B] = '\\v',    -- <control> <VT>  '\v' invalid in HTML
    [0x0C] = '\\f',    -- <control> <FF>  '\f' invalid in HTML
    [0x0D] = '&#13;',  -- <control> <CR>  '\r' valid in HTML (use source form, can be interpreted)
    [0x0E] = '\\014',  -- <control> <SO>       invalid in HTML
    [0x0F] = '\\015',  -- <control> <SI>       invalid in HTML
    [0x10] = '\\016',  -- <control> <DLE>      invalid in HTML
    [0x11] = '\\017',  -- <control> <DC1>      invalid in HTML
    [0x12] = '\\018',  -- <control> <DC2>      invalid in HTML
    [0x13] = '\\019',  -- <control> <DC3>      invalid in HTML
    [0x14] = '\\020',  -- <control> <DC4>      invalid in HTML
    [0x15] = '\\021',  -- <control> <NAK>      invalid in HTML
    [0x16] = '\\022',  -- <control> <SYN>      invalid in HTML
    [0x17] = '\\023',  -- <control> <ETB>      invalid in HTML
    [0x18] = '\\024',  -- <control> <CAN>      invalid in HTML
    [0x19] = '\\025',  -- <control> <EM>       invalid in HTML
    [0x1A] = '\\026',  -- <control> <SUB>      invalid in HTML
    [0x1B] = '\\027',  -- <control> <ESC> '\E' invalid in HTML
    [0x1C] = '\\028',  -- <control> <FS>       invalid in HTML
    [0x1D] = '\\029',  -- <control> <GS>       invalid in HTML
    [0x1E] = '\\030',  -- <control> <RS>       invalid in HTML
    [0x1F] = '\\031',  -- <control> <US>       invalid in HTML
    [0x20] = '&#32;',  -- make sure it is visible and not stripped (use source form, can be interpreted)
    [0x22] = '&quot;', -- '"' HTML escape
    [0x26] = '&amp;',  -- '&' HTML escape
    [0x3C] = '&lt;',   -- '<' HTML escape
    [0x3E] = '&gt;',   -- '>' HTML escape
    [0x5B] = '&#91;',  -- '[' Wiki escape
    [0x5C] = '\\\\',   -- '\\' make sure it is distinguished visually from an escape for invalid controls in HTML
    [0x5D] = '&#93;',  -- ']' Wiki escape
    [0x7B] = '&#123;', -- '{' Wiki escape
    [0x7C] = '&#124;', -- '|' Wiki escape
    [0x7D] = '&#125;', -- '}' Wiki escape
    [0x7F] = '\\127',  -- <control> <DEL>      invalid in HTML
    [0x80] = '\\128',  -- <control> <PAD>      invalid in HTML
    [0x81] = '\\129',  -- <control> <HOP>      invalid in HTML
    [0x82] = '\\130',  -- <control> <BPH>      invalid in HTML
    [0x83] = '\\131',  -- <control> <NBH>      invalid in HTML
    [0x84] = '\\132',  -- <control> <IND>      invalid in HTML
    [0x85] = '&#133;', -- <control> <NEL>      valid in HTML
    [0x86] = '\\134',  -- <control> <SSA>      invalid in HTML
    [0x87] = '\\135',  -- <control> <ESA>      invalid in HTML
    [0x88] = '\\136',  -- <control> <HTS>      invalid in HTML
    [0x89] = '\\137',  -- <control> <HTJ>      invalid in HTML
    [0x8A] = '\\138',  -- <control> <LTS>      invalid in HTML
    [0x8B] = '\\139',  -- <control> <PLD>      invalid in HTML
    [0x8C] = '\\140',  -- <control> <PLU>      invalid in HTML
    [0x8D] = '\\141',  -- <control> <RI>       invalid in HTML
    [0x8E] = '\\142',  -- <control> <SS2>      invalid in HTML
    [0x8F] = '\\143',  -- <control> <SS3>      invalid in HTML
    [0x90] = '\\144',  -- <control> <DCS>      invalid in HTML
    [0x91] = '\\145',  -- <control> <PU1>      invalid in HTML
    [0x92] = '\\146',  -- <control> <PU2>      invalid in HTML
    [0x93] = '\\147',  -- <control> <STS>      invalid in HTML
    [0x94] = '\\148',  -- <control> <CCH>      invalid in HTML
    [0x95] = '\\149',  -- <control> <MW>       invalid in HTML
    [0x96] = '\\150',  -- <control> <SPA>      invalid in HTML
    [0x97] = '\\151',  -- <control> <EPA>      invalid in HTML
    [0x98] = '\\152',  -- <control> <SOS>      invalid in HTML
    [0x99] = '\\153',  -- <control> <SGCI>     invalid in HTML
    [0x9A] = '\\154',  -- <control> <SCI>      invalid in HTML
    [0x9B] = '\\155',  -- <control> <CSI>      invalid in HTML
    [0x9C] = '\\156',  -- <control> <ST>       invalid in HTML
    [0x9D] = '\\157',  -- <control> <OSC>      invalid in HTML
    [0x9E] = '\\158',  -- <control> <PM>       invalid in HTML
    [0x9F] = '\\159',  -- <control> <APC>      invalid in HTML
    [0xA0] = '&nbsp;',     -- <NON-BREAKING SPACE> HTML escape
    [0xAD] = '&shy;',      -- <SOFT HYPHEN> HTML escape
    [0x2000] = '&enquad;', -- <EN QUAD> HTML escape
    [0x2001] = '&emquad;', -- <EN QUAD> HTML escape
    [0x2002] = '&ensp;',   -- <EN SPACE> HTML escape
    [0x2003] = '&emsp;',   -- <EM SPACE> HTML escape
    [0x2004] = '&emsp13;', -- <THREE-PER-EM SPACE> HTML escape
    [0x2005] = '&emsp14;', -- <FOUR-PER-EM SPACE> HTML escape
    [0x2006] = '&emsp16;', -- <SIX-PER-EM SPACE> HTML escape
    [0x2007] = '&numsp;',  -- <FIGURE SPACE> HTML escape
    [0x2008] = '&puncsp;', -- <PUNCTUATION SPACE> HTML escape
    [0x2009] = '&thinsp;', -- <THIN SPACE> HTML escape
    [0x200A] = '&hairsp;', -- <HAIRY SPACE> HTML escape
    [0x200B] = '&zwsp;',   -- <ZERO-WIDTH SPACE> HTML escape
    [0x200C] = '&zwnj;',   -- <ZERO-WIDTH NON-JOINER> HTML escape
    [0x200D] = '&zwj;',    -- <ZERO-WIDTH JOINER> HTML escape
    [0x200E] = '&lrm;',    -- <LEFT TO RIGHT MARK> HTML escape (deprecated)
    [0x200F] = '&rlm;',    -- <RIGHT TO LEFT MARK> HTML escape (deprecated)
    [0x2028] = '&lsep;',   -- <LINE SEPARATOR> HTML escape
    [0x2029] = '&psep;',   -- <PARAGRAPH SEPARATOR> HTML escape
    [0x202A] = '&lre;',    -- <LEFT-TO-RIGHT EMBEDDING> HTML escape, like <bde dir="ltr"> (deprecated)
    [0x202B] = '&rle;',    -- <RIGHT-TO-LEFT EMBEDDING> HTML escape, like <bde dir="rtl"> (deprecated)
    [0x202C] = '&pdf;',    -- <POP DIRECTIONAL FORMATTING> HTML escape, like </bde> or </bdo> (deprecated)
    [0x202D] = '&lro;',    -- <LEFT-TO-RIGHT OVERRIDE> HTML escape, like <bdo dir="ltr"> (deprecated)
    [0x202E] = '&rlo;',    -- <RIGHT-TO-LEFT OVERRIDE> HTML escape, like <bdo dir="rtl"> (deprecated)
    [0x202F] = '&nnbsp;',  -- <NARROW NON-BREAKING SPACE> HTML escape
    [0x205F] = '&mathsp;', -- <MATHEMATICAL MEAN SPACE> HTML escape
    [0x2060] = '&wj;',     -- <WORD JOINER> HTML escape
    [0x2066] = '&lri;',    -- <LEFT-TO-RIGHT ISOLATE> HTML escape, like <bdi dir="ltr"> (recommended)
    [0x2067] = '&rli;',    -- <RIGHT-TO-LEFT ISOLATE> HTML escape, like <bdi dir="rtl"> (recommended)
    [0x2068] = '&fsi;',    -- <FIRST STRONG ISOLATE> HTML escape, like <bdi dir="auto"> or <bdi> (recommended)
    [0x2069] = '&pdi;',    -- <POP DIRECTIONAL ISOLATE> HTML escape, like </bdi> (recommended)
    [0x206A] = '&iss;',    -- <INHIBIT SYMMETRIC SWAPPING> HTML escape
    [0x206B] = '&ass;',    -- <ACTIVATE SYMMETRIC SWAPPING> HTML escape
    [0x206C] = '&iafs;',   -- <INHIBIT ARABIC FORM SHAPING> HTML escape
    [0x206D] = '&aafs;',   -- <ACTIVATE ARABIC FORM SHAPING> HTML escape
    [0x206E] = '&natds;',  -- <NATIONAL DIGIT SHAPES> HTML escape (deprecated)
    [0x206F] = '&nomds;',  -- <NOMINAL DIGIT SHAPES> HTML escape (deprecated)
    [0x3000] = '&idsp;',   -- <IDEOGRAPHIC SPACE> HTML escape
    [0xFEFF] = '&zwnbsp;', -- <ZERO-WIDTH NON-BREAKING SPACE> HTML escape (deprecated)
    [0xFFFC] = '&objr;',   -- <OBJECT REPLACEMENT CHARACTER> HTML escape (deprecated)
    [0xFFFD] = '&repl;',   -- <REPLACEMENT CHARACTER> HTML escape (deprecated)
}
-- There's no way to handle multibyte strings in a single gsub(), because Lua patterns do not support alternation with '|'.
-- Instead we build a character class for fast 1-to-1 substitutions. This only works for single-byte ASCII, not for
-- substituting multibyte UTF-8. For others, we generate separate gsub() patterns.
local charsubst, stringmap = {}, {}
for code, subst in pairs(codemap) do
   local pattern = toUTF8(code)
   if #pattern == 1 then
       charsubst[pattern] = subst
   else
       pattern = pattern:gsub('^^', '%%^'):gsub('$$', '%%$'):gsub('[%%()*+%-.?%[]','%%%1') -- magic characters in search patterns
       subst = subst:gsub('%%', '%%%%') -- magic characters in substitutions
       insert(stringmap, { pattern, subst })
   end
end
insert(stringmap, { '&' , '&#38;' }) -- HTML escape (standard)
insert(stringmap, { '<' , '&#60;' }) -- HTML escape (standard)
insert(stringmap, { '>' , '&#62;' }) -- HTML escape (standard)
insert(stringmap, { '^ ' , '&#32;' }) -- Wiki escape (reserved for monospaced line)
insert(stringmap, { "''" , "'&apos;" }) -- Wiki escape (reserved for italic or bold styles)
insert(stringmap, { '^!' , '&#33;' }) -- Wiki escape (reserved for table header start)
insert(stringmap, { '!!' , '!&#33;' }) -- Wiki escape (reserved for table header middle)
insert(stringmap, { '^#' , '&#35;' }) -- Wiki escape (reserved for item of numbered lists)
insert(stringmap, { '^%*' , '&#42;' }) -- Wiki escape (reserved for item of bulleted lists)
insert(stringmap, { ':' , '&#58;' }) -- Wiki escape (reserved for definition item of definition list)
insert(stringmap, { '^;' , '&#59;' }) -- Wiki escape (reserved for term item of  of definition list)
insert(stringmap, { '^=' , '&#61;' }) -- Wiki escape (reserved for section headings)
insert(stringmap, { "%[" , "&#91" }) -- Wiki escape (reserved for external links or wikilinks)
insert(stringmap, { "%]" , "&#92;" }) -- Wiki escape (reserved for external links or wikilinks)
insert(stringmap, { "{{" , "{&#123;" }) -- Wiki escape (reserved for transcluding templates, calling parser functions, or replacing a template parameter value)
insert(stringmap, { "^{|" , "{&#124;" }) -- Wiki escape (reserved for tables)
insert(stringmap, { "|" , "&#124;" }) -- Wiki escape (reserved between table cells, or parameters of template transclusion or parser functions)
insert(stringmap, { "}}" , "}&#125;" }) -- Wiki escape (reserved for transcluding templates, calling parser functions, or replacing a template parameter value)
insert(stringmap, { '~~~' , '~~&#126;' }) -- Wiki escape (reserved for user signatures)
local function htmlize(wikitext)
    wikitext = wikitext
       :gsub('(&[#%w]+;)', function(s)
               if s:sub(2, 2) == '#' then
                   if s:sub(3, 3) == 'x' then s = tonumber(s:sub(4,-2), 16) else s = tonumber(s(3,-2), 10) end
                   if s ~= nil then return toUTF8(s) end
                   return nil
               end
               return nce[s:sub(2,-2)]
           end)
       :gsub('.', charsubst)
    for _, map in ipairs(stringmap) do
        wikitext = wikitext:gsub(map[1], map[2])
    end
    return wikitext
end

function p.test_order()
    local seen, result = {}, {}
    insert(result, '<div lang="en" dir="ltr" class="plainlinks" style="font-size:smaller;float:right;margin-left:.5em">&#91;&nbsp;[' .. mw.getCurrentFrame():preprocess('{{FULLURL:{{FULLPAGENAME}}}}') .. '?action=purge Refresh this page]&nbsp;&#93;</div>')
    insert(result, '<table lang="en" dir="ltr" class="wikitable sortable" style="font-family:\'Noto Sans\',\'Segoe UI\',\'Noto Sans Historic\',\'Segoe UI Historic\',Robo,sans-serif">')
    insert(result, '<caption>' ..
        'Check display order (native names grouped by direction, script, then sorted in locale-neutral UCA order)<br />' ..
        '<small>If you see orange lines at bottom of this table, these languages added to MediaWiki are missing in the [[Module:Multilingual description/sort/sandbox|sort order]] (check also their assigned [[Module:Dir/RTL overrides/sandbox|direction]])</small>' ..
        '</caption>')
    -- <thead>
    insert(result, '<tr>')
    insert(result, '<th scope="col">#</th>')
    insert(result, '<th scope="col">Code</th>')
    insert(result, '<th scope="col">Valid<br />Code,<br /> Valid<br />BuiltIn<br />Code?')
    insert(result, '<th scope="col">Known<br /> Language<br/> Tag <small>(by</br> <code>&#123;{#language:|en}&#125;</code>)</small></th>')
    insert(result, '<th scope="col">Native<br /> language<br />name?</th>')
    insert(result, '<th scope="col">Direction, <br /> Sorting<br /> initial<br/> <small>(uppercased)</small></th>')
    insert(result, '<th scope="col">Native name<br /> <small>(lowercase initial is often preferred)</small></th>')
    insert(result, '<th scope="col">Full<br /> localisation<br /> supported?</th>')
    insert(result, '<th scope="col">Known fallbacks<br/> <small>(on this wiki)</small></th>')
    insert(result, '<th scope="col">English name (HTML-encoded)<br /> <ul style="text-align:left;font-size:smaller"><li>Yellow: same as native?<br /> <small>(not necessarily wrong)</small></li><li>Red: non-Basic Latin, incorrect encoding or missing data<br /> <small>(report bug in MediaWiki #language)</small></li></ul></th>')
    insert(result, '</tr>')
    -- </thead>
    -- <tbody>
    local bgOrange, bgRed, bgYellow, bgWhite =
        'background:#8F0;color:#000',
        'background:#FCC;color:#000',
        'background:#FFC;color:#000',
        'background:#F7F8FF;color:#202122'
    local rank = 0
    for _,lang in pairs(langs) do
        if type(lang) == 'string' then
            rank = rank + 1
            local nameStatus = 'OK'
			local validCode = isValidCode(lang)
			local validBuiltInCode = isValidBuiltInCode(lang)
            local knownLanguageTag = isKnownLanguageTag(lang)
            local englishName = fetchLanguageName(lang, 'en')
			local nativeName = languageNames[lang]
            if seen[lang] then
                nameStatus = '<b>Duplicate code</b>'
            else
                seen[lang] = true
                if nativeName == nil then
                    nameStatus = '<b>No</b>'
                    nativeName = fetchLanguageName(lang)
                end
            end
            local nativeDir = dir.select(lang, 'rtl', 'ltr')
            local initialLetter = sub(upper(lower(nativeName)), 1, 1)
            local supportedLanguage = language.isSupportedLanguage(lang)
            --
            insert(result, '<tr style="' .. (supportedLanguage and bgWhite or knownLanguageTag and bgYellow or bgRed) .. '">')
            insert(result, '<th scope="row" style="text-align:right">' .. rank .. '</th>')
            insert(result, '<td><code>' .. lang .. '</code></td>')
            insert(result, '<td style="text-align:center">' .. (validCode and 'Yes' or '<b>No</b>') ..
                ', ' .. (validBuiltInCode and 'Yes' or '<b>No</b>') .. '</td>')
            insert(result, '<td style="text-align:center">' .. (knownLanguageTag and 'Yes' or '<b>No</b>') .. '</td>')
            insert(result, '<td style="text-align:center">' .. nameStatus .. '</td>')
            insert(result, '<td style="text-align:center">' .. nativeDir .. ', ' .. initialLetter .. '</td>')
            insert(result, '<td dir="' .. nativeDir .. '">' .. nativeName .. '</td>')
            insert(result, '<td style="text-align:center">' .. (supportedLanguage and 'Yes' or '<b>No</b>') .. '</td>')
            insert(result, '<td><code>' .. table.concat(language.getFallbacksFor(lang), ', ') .. '</code></td>')
            insert(result, '<td style="' ..
                ((nativeName == englishName and (englishName:find("^[A-Z][ '()%-/0-9A-Za-z]*['()%-/0-9A-Za-z]$") and bgYellow or bgRed)
                ) or bgWhite) .. '">' .. htmlize(englishName) .. '</td>')
            insert(result, '</tr>')
        end
    end
    -- Other languages not seen
    local otherlangs = {}
    for lang, _ in pairs(languageNames) do
        if not seen[lang] then
            insert(otherlangs, lang)
        end
    end
    table.sort(otherlangs, function(lang1, lang2)
        return upper(lower(languageNames[lang1])) < upper(lower(languageNames[lang2]))
    end)
    for _, lang in pairs(otherlangs) do
        if type(lang) == 'string' then
            rank = rank + 1
            local nameStatus = '<b>Still not sorted</b>'
			local validCode = isValidCode(lang)
			local validBuiltInCode = isValidBuiltInCode(lang)
            local knownLanguageTag = isKnownLanguageTag(lang)
            local englishName = fetchLanguageName(lang, 'en')
            local nativeName = languageNames[lang]
            local nativeDir = dir.select(lang, 'rtl', 'ltr')
            local initialLetter = sub(upper(lower(nativeName)), 1, 1)
            local supportedLanguage = language.isSupportedLanguage(lang)
            --
            insert(result, '<tr style="' .. (supportedLanguage and bgOrange or knownLanguageTag and bgYellow or bgRed) .. '">')
            insert(result, '<th scope="row" style="text-align:right">' .. rank .. '</th>')
            insert(result, '<td><code>' .. lang .. '</code></td>')
            insert(result, '<td style="text-align:center">' .. (validCode and 'Yes' or '<b>No</b>') ..
                ', ' .. (validBuiltInCode and 'Yes' or '<b>No</b>') .. '</td>')
            insert(result, '<td style="text-align:center">' .. (knownLanguageTag and 'Yes' or '<b>No</b>') .. '</td>')
            insert(result, '<td style="text-align:center">' .. nameStatus .. '</td>')
            insert(result, '<td style="text-align:center">' .. nativeDir .. ', ' .. initialLetter .. '</td>')
            insert(result, '<td dir="' .. nativeDir .. '">' .. nativeName .. '</td>')
            insert(result, '<td style="text-align:center">' .. (supportedLanguage and 'Yes' or '<b>No</b>') .. '</td>')
            insert(result, '<td><code>' .. table.concat(language.getFallbacksFor(lang), ', ') .. '</code></td>')
            insert(result, '<td style="' ..
                ((nativeName == englishName and
                 (englishName:find("^[A-Z][ '()%-/0-9A-Za-z]*['()%-/0-9A-Za-z]$") and bgYellow or bgRed)
                ) or bgWhite) .. '">' .. htmlize(englishName) .. '</td>')
            insert(result, '</tr>')
        end
    end
    -- </tbody>
    insert(result, '</table>')
    return table.concat(result)
end

-- Tricky test functions, needed during search of solutions for the severly limited methods of MediaWiki "frame" objects
-- that now no longer not support any kind of expansion for MediaWiki special tags (except "nowiki" tags).
-- May be we'll have some Lua extension library for supporting some special tags (such libraries exist now for Wikidata, and for "#expr").

local function show(title, text)
    return '<b>' .. title .. ':</b> "<tt>' .. htmlize(text) .. '</tt>"'
end

function p.test_prefix(...)
    local args = { ... }
    local frame
    if type(args[1]) == 'table' and type(args[1].args) == 'table' and type(args[1].getParent) == 'function' then
        frame = args[1]
        local pframe = frame:getParent() or frame
        local args = frame.args or pframe.args or {}
    else
       frame = mw.getCurrentFrame()
    end

    local text = args.text or ''

    local preprocessed = frame:preprocess(text)
    local expanded = mw.text.unstrip(preprocessed)

    return show('text', text) .. ',<br /> '
        .. show('preprocessed', preprocessed) .. ',<br /> '
        .. show('expanded', expanded) .. '.'
end

--p.test_tag(frame) -- {{#invoke:...|test_tag|1=tag|2=content|option1=value1|option2=value2...}}
--p.test_tag('tag','content')
--p.test_tag{'tag','content'}
--p.test_tag('tag', nil,      { option1 = "value1", option2 = "value2"... })
--p.test_tag('tag','content', { option1 = "value1", option2 = "value2"... })
--p.test_tag{'tag',             option1 = "value1", option2 = "value2"... }
--p.test_tag{'tag','content',   option1 = "value1", option2 = "value2"... }
function p.test_tag(...)
    local args = { ... }
    local frame
    if type(args[1]) == 'table' and type(args[1].args) == 'table' and type(args[1].getParent) == 'function' then
        frame = args[1]
        local pframe = frame:getParent() or frame
        local args = frame.args or pframe.args or {}
    else
       frame = mw.getCurrentFrame()
    end

    local tag, content = args[1] or '', args[2] or ''
    if type(tag) == 'table' then
        args = tag
        tag, content = args[1] or '', args[2] or ''
        table.remove(args, 2)
        table.remove(args, 1)
    elseif type(args[3]) == 'table' then
        args = args[3]
    else
        table.remove(args, 2)
        table.remove(args, 1)
    end
    local text = '{{#tag:' .. tag .. '|' .. content
    for k, v in pairs(args) do
	    if type(v) == 'string' or type(v) == 'number' then
            text = text .. '|' .. k .. '=' .. v
        end
    end
    text = text .. '}}'

    local preprocessed = frame:extensionTag{name=tag, content=content, args=args}
    local expanded = mw.text.unstrip(preprocessed)

    return show('text', text) .. ',<br /> '
        .. show('preprocessed', preprocessed) .. ',<br /> '
        .. show('expanded', expanded) .. '.'
end
return p
"https://si.wikibooks.org/w/index.php?title=Module:Multilingual_description/sort/testcases/sandbox&oldid=23910" වෙතින් සම්ප්‍රවේශනය කෙරිණි