Module:Format TemplateData

local TemplateData = {
  item = 46997995,
  serial = "2025-02-07",
  suite = "TemplateData"
}

local Failsafe = TemplateData

local Config = {
  -- Multiple option names mapped into unique internal fields.
  basicCnf = {
    catProblem = "strange",
    classMultiColumns = "selMultClm",
    classNoNumTOC = "suppressTOCnum",
    classTable = "classTable",
    cssParWrap = "cssTabWrap",
    cssParams = "cssTable",
    docpageCreate = "suffix",
    docpageDetect = "subpage",
    helpAliases = "supportAliases",
    helpBoolean = "support4boolean",
    helpContent = "support4content",
    helpDate = "support4date",
    helpDefault = "support4default",
    helpFile = "support4wiki-file-name",
    helpFormat = "supportFormat",
    helpLine = "support4line",
    helpNumber = "support4number",
    helpPage = "support4wiki-page-name",
    helpString = "support4string",
    helpTemplate = "support4wiki-template-name",
    helpURL = "support4url",
    helpUser = "support4wiki-user-name",
    msgDescMiss = "solo",
    tStylesMultiColumns = "stylesMultClm",
    tStylesTOCnum = "stylesTOCnum"
  },
  classTable = { "wikitable" },  -- Classes for params table
  cssTable = false,              -- Styles for params table
  cssTabWrap = false,            -- Styles for params table wrapper
  debug = false,
  debugmultilang = "#c0c0c0",
  jsonDebug = "json-code-lint",  -- Class for jsonDebug tool
  loudly = false,                -- Show exported element, etc.
  solo = false,                  -- Complaint on missing description
  strange = false,               -- Title of maintenance category
  subpage = false,               -- Pattern to identify subpage
  suffix = false,                -- Subpage creation scheme
  suppressTOCnum = false         -- Class for TOC number suppression
}
local Data = {
  div = false,      -- <div class="mw-templatedata-doc-wrap">
  got = false,      -- table, initial templatedata object
  heirs = false,    -- table, params that are inherited
  jump = false,     -- source position at end of "params"
  less = false,     -- main description missing
  lasting = false,  -- old syntax encountered
  lazy = false,     -- doc mode; do not generate effective <templatedata>
  leading = false,  -- show TOC
  -- low = false,   -- 1= mode
  order = false,    -- parameter sequence
  params = false,   -- table, exported parameters
  scream = false,   -- error messages
  sibling = false,  -- TOC juxtaposed
  slang = nil,      -- project/user language code
  slim = false,     -- JSON reduced to plain
  source = false,   -- JSON input
  strip = false,    -- <templatedata> evaluation
  tag = false,      -- table, exported root element
  title = false,    -- page
  tree = false      -- table, rewritten templatedata object
}
local Permit = {
  builder = {
    after = "block",
    align = "block",
    block = "block",
    compressed = "block",
    dense = "block",
    grouped = "inline",
    half = "inline",
    indent = "block",
    inline = "inline",
    last = "block",
    lead = "block",
    newlines = "*",
    spaced = "inline"
  },
  colors = {
    bg = "var(--background-color-base, #fff)",
    deprecated = "#ffcbcb",
    fg = "var(--color-base, #000)",
    optional = "#eaecf0",
    required = "#eaf3ff",
    suggested = "#fff",
    tableheadbg = "var(--background-color-progressive-subtle, #b3b7ff)"
  },
  params = {
    aliases = "table",
    autovalue = "string",
    default = "string table I18N nowiki",
    deprecated = "boolean string I18N",
    description = "string table I18N",
    example = "string table I18N nowiki",
    inherits = "string",
    label = "string table I18N",
    required = "boolean",
    style = "string table",
    suggested = "boolean",
    suggestedvalues = "string table number boolean",
    type = "string"
  },
  root = {
    description = "string table I18N",
    format = "string",
    maps = "table",
    paramOrder = "table",
    params = "table",
    sets = "table"
  },
  search = "[{,]%%s*(['\"])%s%%1%%s*:%%s*%%{",
  types = {
    boolean = true,
    content = true,
    date = true,
    line = true,
    number = true,
    string = true,
    unknown = true,
    url = true,
    ["string/line"] = "line",
    ["string/wiki-page-name"] = "wiki-page-name",
    ["string/wiki-user-name"] = "wiki-user-name",
    ["unbalanced-wikitext"] = true,
    ["wiki-file-name"] = true,
    ["wiki-page-name"] = true,
    ["wiki-template-name"] = true,
    ["wiki-user-name"] = true
  }
}

local function Fault(alert)
  -- Memorize error message.
  -- Parameter:
  --     alert  -- string, error message
  if Data.scream then
    Data.scream = string.format("%s *** %s", Data.scream, alert)
  else
    Data.scream = alert
  end
end -- Fault()

local function Fetch(ask, allow)
  -- Fetch module.
  -- Parameter:
  --     ask    -- string, with name
  --                       "/global"
  --                       "JSONutil"
  --                       "Multilingual"
  --                       "Text"
  --                       "WLink"
  --     allow  -- true: no error if unavailable
  -- Returns table of module
  -- Error: Module not available
  local sign = ask
  local r, stem
  if sign:sub(1, 1) == "/" then
    sign = TemplateData.frame:getTitle() .. sign
  else
    stem = sign
    sign = "Module:" .. stem
  end
  if TemplateData.extern then
    r = TemplateData.extern[sign]
  else
    TemplateData.extern = {}
  end
  if not r then
    local lucky, g = pcall(require, sign)
    if type(g) == "table" then
      if stem and type(g[stem]) == "function" then
        r = g[stem]()
      else
        r = g
      end
      TemplateData.extern[sign] = r
    elseif not allow then
      error(string.format("Fetch(%s) %s", sign, g), 0)
    end
  end
  return r
end -- Fetch()

local function Foreign()
  -- Guess human language, returns slang or not.
  if type(Data.slang) == "nil" then
    local Multilingual = Fetch("Multilingual", true)
    if Multilingual and type(Multilingual.userLangCode) == "function" then
      Data.slang = Multilingual.userLangCode()
    else
      Data.slang = mw.language.getContentLanguage():getCode():lower()
    end
  end
  if Data.slang and mw.ustring.codepoint(Data.slang, 1, 1) > 122 then
    Data.slang = false
  end
  return Data.slang
end -- Foreign()

local function facet(ask, at)
  -- Find physical position of parameter definition in JSON.
  -- Parameter:
  --     ask  -- string, parameter name
  --     at   -- number, physical position within definition
  -- Returns number or nil.
  local seek = string.format(Permit.search, ask:gsub("%%", "%%%%"):gsub("([%-.()+*?^$%[%]])", "%%%1"))
  local i, k, r, slice, source
  if not Data.jump then
    Data.jump = Data.source:find("params", 2)
    if Data.jump then
      Data.jump = Data.jump + 7
    else
      Data.jump = 1
    end
  end
  i, k = Data.source:find(seek, at + Data.jump)
  while i and not r do
    source = Data.source:sub(k + 1)
    slice = source:match('^%s*"([^"]+)"s*:')
    if not slice then
      slice = source:match("^%s*'([^']+)'%s*:")
    end
    if (slice and Permit.params[slice]) or source:match("^%s*%}") then
      r = k
    else
      i, k = Data.source:find(seek, k)
    end
  end -- while i
  return r
end -- facet()

local function facilities(apply)
  -- Retrieve details of suggestedvalues.
  -- Parameter:
  --     apply  -- table, with plain or enhanced values
  --               .suggestedvalues  -- table|string|number, or more
  -- Returns
  --     1  -- table, with suggestedvalues
  --     2  -- table, with CSS map, or not
  --     3  -- string, with class, or not
  --     4  -- string, with templatestyles, or not
  local elements = apply.suggestedvalues
  local s = type(elements)
  local r1, r2, r3, r4
  if s == "table" then
    local values = elements.values
    if type(values) == "table" then
      r1 = values
      if type(elements.scroll) == "string" then
        r2 = r2 or {}
        r2.height = apply.scroll
        r2.overflow = "auto"
      end
      if type(elements.minwidth) == "string" then
        local s = type(elements.maxcolumns)
        r2 = r2 or {}
        r2["column-width"] = elements.minwidth
        if s == "string" or s == "number" then
          s = tostring(elements.maxcolumns)
          r2["column-count"] = s
        end
        if type(Config.selMultClm) == "string" then
          r3 = Config.selMultClm
        end
        if type(Config.stylesMultClm) == "string" then
          local src = Config.stylesMultClm .. "/styles.css"
          r4 = TemplateData.frame:extensionTag("templatestyles", nil, { src = src })
        end
      end
    elseif elements and elements ~= "" then
      r1 = elements
    end
  elseif s == "string" then
    s = mw.text.trim(about)
    if s ~= "" then
      r1 = {}
      table.insert(r1, { code = s })
    end
  elseif s == "number" then
    r1 = {}
    table.insert(r1, { code = tostring(elements) })
  end
  return r1, r2, r3, r4
end -- facilities()

local function factory(adapt)
  -- Retrieve localized text from system message.
  -- Parameter:
  --     adapt  -- string, message ID after "templatedata-"
  -- Returns string, with localized text
  local o = mw.message.new("templatedata-" .. adapt)
  if Foreign() then
    o:inLanguage(Data.slang)
  end
  return o:plain()
end -- factory()

local function faculty(adjust)
  -- Test template argument for Boolean.
  --     adjust  -- string or nil
  -- Returns boolean.
  local s = type(adjust)
  local r
  if s == "string" then
    r = mw.text.trim(adjust)
    r = (r ~= "" and r ~= "0")
  elseif s == "boolean" then
    r = adjust
  else
    r = false
  end
  return r
end -- faculty()

local function failures()
  -- Retrieve error collection and category, returns string.
  local r
  if Data.scream then
    local e = mw.html.create("span"):addClass("error"):wikitext(Data.scream)
    r = tostring(e)
    mw.addWarning("'''TemplateData'''<br />" .. Data.scream)
    if Config.strange then
      r = string.format("%s[[category:%s]]", r, Config.strange)
    end
  else
    r = ""
  end
  return r
end -- failures()

local function fair(adjust)
  -- Reduce text to one line of plain text, or noexport wikitext blocks.
  --     adjust  -- string
  -- Returns string with adjusted text.
  local f = function(a)
    return a:gsub("%s*\n%s*", " "):gsub("%s%s+", " ")
  end
  local tags = {
    {
      start = "<noexport>",
      stop = "</noexport>",
    },
    {
      start = "<exportonly>",
      stop = "</exportonly>",
      l = false,
    },
  }
  local r = adjust
  local i, j, k, s, tag
  for m = 1, 2 do
    tag = tags[m]
    if r:find(tag.start, 1, true) then
      s = r
      r = ""
      i = 1
      tag.l = true
      j, k = s:find(tag.start, i, true)
      while j do
        if j > 1 then
          r = r .. f(s:sub(i, j - 1))
        end
        i = k + 1
        j, k = s:find(tag.stop, i, true)
        if j then
          if m == 1 then
            r = r .. s:sub(i, j - 1)
          end
          i = k + 1
          j, k = s:find(tag.start, i, true)
        else
          Fault("missing " .. tag.stop)
        end
      end -- while j
      r = r .. s:sub(i)
    elseif m == 1 then
      r = f(r)
    end
  end -- for m
  if tags[2].l then
    r = r:gsub("<exportonly>.*</exportonly>", "")
  end
  return r
end -- fair()

local function fancy(advance, alert)
  -- Present JSON source.
  -- Parameter:
  --     advance  -- true, for nice
  --     alert    -- true, for visible
  -- Returns string.
  local r
  if Data.source then
    local support = Config.jsonDebug
    local css
    if advance then
      css = {
        height = "6em",
        resize = "vertical",
      }
      r = {
        [1] = "syntaxhighlight",
        [2] = Data.source,
        lang = "json",
        style = table.concat(css, ";"),
      }
      if alert then
        r.class(support)
      end
      r = TemplateData.frame:callParserFunction("#tag", r)
    else
      css = {
        ["font-size"] = "0.77em",
        ["line-height"] = "1.35",
      }
      if alert then
        css.resize = "vertical"
      else
        css.display = "none"
      end
      r = mw.html.create("pre"):addClass(support):css(css):wikitext(mw.text.encode(Data.source))
      r = tostring(r)
    end
    r = "\n" .. r
  else
    r = ""
  end
  return r
end -- fancy()

local function faraway(alternatives)
  -- Retrieve best language version from multilingual text.
  -- Parameter:
  --     alternatives  -- table, to be evaluated
  -- Returns
  --     1  -- string, with best match
  --     2  -- table of other versions, if any
  local n = 0
  local variants = {}
  local r1, r2
  for k, v in pairs(alternatives) do
    if type(v) == "string" then
      v = mw.text.trim(v)
      if v ~= "" and type(k) == "string" then
        k = k:lower()
        variants[k] = v
        n = n + 1
      end
    end
  end -- for k, v
  if n > 0 then
    local Multilingual = Fetch("Multilingual", true)
    if Multilingual and type(Multilingual.i18n) == "function" then
      local show, slang = Multilingual.i18n(variants)
      if show then
        r1 = show
        variants[slang] = nil
        r2 = variants
      end
    end
    if not r1 then
      Foreign()
      for k, v in pairs(variants) do
        if n == 1 then
          r1 = v
        elseif Data.slang == k then
          variants[k] = nil
          r1 = v
          r2 = variants
        end
      end -- for k, v
    end
    if r2 and Multilingual then
      for k, v in pairs(r2) do
        if v and not Multilingual.isLang(k, true) then
          Fault(string.format("%s <code>lang=%s</code>", "Invalid", k))
        end
      end -- for k, v
    end
  end
  return r1, r2
end -- faraway()

local function fashioned(about, asked, assign)
  -- Create description head.
  -- Parameter:
  --     about   -- table, supposed to contain description
  --     asked   -- true, if mandatory description
  --     assign  -- <block>, if to be equipped
  -- Returns <block> with head, or nil.
  local para = assign or mw.html.create("div")
  local plus, r
  if about and about.description then
    if type(about.description) == "string" then
      para:wikitext(about.description)
    else
      para:wikitext(about.description[1])
      plus = mw.html.create("ul")
      plus:css("text-align", "left")
      for k, v in pairs(about.description[2]) do
        plus:node(
          mw.html
            .create("li")
            :node(mw.html.create("code"):wikitext(k))
            :node(mw.html.create("br"))
            :wikitext(fair(v))
        )
      end -- for k, v
      if Config.loudly then
        plus = mw.html
          .create("div")
          :css("background-color", Config.debugmultilang)
          :css("color", "inherit")
          :node(plus)
      else
        plus:addClass("templatedata-maintain"):css("display", "none")
      end
    end
  elseif Config.solo and asked then
    para:addClass("error"):wikitext(Config.solo)
    Data.less = true
  else
    para = false
  end
  if para then
    if plus then
      r = mw.html.create("div"):node(para):node(plus)
    else
      r = para
    end
  end
  return r
end -- fashioned()

local function fatten(access)
  -- Create table row for subheadline.
  -- Parameter:
  --     access  -- string, with name
  -- Returns <tr>.
  local param = Data.tree.params[access]
  local sub, sort = access:match("(=+)%s*(%S.*)$")
  local headline = mw.html.create(string.format("h%d", #sub))
  local r = mw.html.create("tr")
  local td = mw.html.create("td"):attr("colspan", "5"):attr("data-sort-value", "!" .. sort)
  local s
  if param.style then
    s = type(param.style)
    if s == "table" then
      td:css(param.style)
    elseif s == "string" then
      td:cssText(param.style)
    end
  end
  s = fashioned(param, false, headline)
  if s then
    headline = s
  else
    headline:wikitext(sort)
  end
  td:node(headline)
  r:node(td)
  return r
end -- fatten()

local function fathers()
  -- Merge parameters with inherited values.
  local n = 0
  local p = Data.params
  local t = Data.tree.params
  local p2, t2
  for k, v in pairs(Data.heirs) do
    n = n + 1
  end -- for k, v
  for i = 1, n do
    if Data.heirs then
      for k, v in pairs(Data.heirs) do
        if v and not Data.heirs[v] then
          n = n - 1
          t[k].inherits = nil
          Data.heirs[k] = nil
          p2 = {}
          t2 = {}
          if p[v] then
            for k2, v2 in pairs(p[v]) do
              p2[k2] = v2
            end -- for k2, v2
            if p[k] then
              for k2, v2 in pairs(p[k]) do
                if type(v2) ~= "nil" then
                  p2[k2] = v2
                end
              end -- for k2, v2
            end
            p[k] = p2
            for k2, v2 in pairs(t[v]) do
              t2[k2] = v2
            end -- for k2, v2
            for k2, v2 in pairs(t[k]) do
              if type(v2) ~= "nil" then
                t2[k2] = v2
              end
            end -- for k2, v2
            t[k] = t2
          else
            Fault("No params[] inherits " .. v)
          end
        end
      end -- for k, v
    end
  end -- i = 1, n
  if n > 0 then
    local s
    for k, v in pairs(Data.heirs) do
      if v then
        if s then
          s = string.format("%s &#124; %s", s, k)
        else
          s = "Circular inherits: " .. k
        end
      end
    end -- for k, v
    Fault(s)
  end
end -- fathers()

local function favorize()
  -- Local customization issues.
  local boole = { ["font-size"] = "1.25em" }
  local l, cx = pcall(mw.loadData, TemplateData.frame:getTitle() .. "/config")
  local scripting, style
  TemplateData.ltr = not mw.language.getContentLanguage():isRTL()
  if TemplateData.ltr then
    scripting = "left"
  else
    scripting = "right"
  end
  boole["margin-" .. scripting] = "3em"
  Permit.boole = {
    [false] = {
      css = boole,
      lead = true,
      show = "&#x2610;",
    },
    [true] = {
      css = boole,
      lead = true,
      show = "&#x2611;",
    },
  }
  Permit.css = {}
  for k, v in pairs(Permit.colors) do
    if k == "tableheadbg" then
      k = "tablehead"
    end
    if k == "fg" then
      style = "color"
    else
      style = "background-color"
    end
    Permit.css[k] = {}
    Permit.css[k][style] = v
  end -- for k, v
  if type(cx) == "table" then
    local c, s
    if type(cx.permit) == "table" then
      if type(cx.permit.boole) == "table" then
        if type(cx.permit.boole[true]) == "table" then
          Permit.boole[false] = cx.permit.boole[false]
        end
        if type(cx.permit.boole[true]) == "table" then
          Permit.boole[true] = cx.permit.boole[true]
        end
      end
      if type(cx.permit.css) == "table" then
        for k, v in pairs(cx.permit.css) do
          if type(v) == "table" then
            Permit.css[k] = v
          end
        end -- for k, v
      end
    end
    for k, v in pairs(Config.basicCnf) do
      s = type(cx[k])
      if s == "string" or s == "table" then
        Config[v] = cx[k]
      end
    end -- for k, v
  end
  if type(Config.subpage) ~= "string" or type(Config.suffix) ~= "string" then
    local got = mw.message.new("templatedata-doc-subpage")
    local suffix
    if got:isDisabled() then
      suffix = "doc"
    else
      suffix = got:plain()
    end
    if type(Config.subpage) ~= "string" then
      Config.subpage = string.format("/%s$", suffix)
    end
    if type(Config.suffix) ~= "string" then
      Config.suffix = string.format("%%s/%s", suffix)
    end
  end
end -- favorize()

local function feasible(all, at, about)
  -- Deal with suggestedvalues within parameter.
  -- Parameter:
  --     all    -- parameter details
  --               .default
  --               .type
  --     at     -- string, with parameter name
  --     about  -- suggestedvalues  -- table,
  --                                   value and possibly description
  --                                   table may have elements:
  --                                   .code    -- mandatory
  --                                   .label   -- table|string
  --                                   .support -- table|string
  --                                   .icon    -- string
  --                                   .class   -- table|string
  --                                   .css     -- table
  --                                   .style   -- string
  --                                   .less    -- true: suppress code
  -- Returns
  --     1: mw.html object <ul>
  --     2: sequence table with values, or nil
  local h = {}
  local e, r1, r2, s, v
  if #about > 0 then
    for i = 1, #about do
      e = about[i]
      s = type(e)
      if s == "table" then
        if type(e.code) == "string" then
          s = mw.text.trim(e.code)
          if s == "" then
            e = nil
          else
            e.code = s
          end
        else
          e = nil
          s = string.format("params.%s.%s[%d] %s", at, "suggestedvalues", i, "MISSING 'code:'")
        end
      elseif s == "string" then
        s = mw.text.trim(e)
        if s == "" then
          e = nil
          s = string.format("params.%s.%s[%d] EMPTY", at, "suggestedvalues", i)
          Fault(s)
        else
          e = { code = s }
        end
      elseif s == "number" then
        e = { code = tostring(e) }
      else
        s = string.format("params.%s.%s[%d] INVALID", at, "suggestedvalues", i)
        Fault(s)
        e = false
      end
      if e then
        v = v or {}
        table.insert(v, e)
        if h[e.code] then
          s = string.format("params.%s.%s REPEATED %s", at, "suggestedvalues", e.code)
          Fault(s)
        else
          h[e.code] = true
        end
      end
    end -- for i
  else
    Fault(string.format("params.%s.suggestedvalues %s", at, "NOT AN ARRAY"))
  end
  if v then
    local code, d, k, less, story, swift, t, u
    r1 = mw.html.create("ul")
    r2 = {}
    for i = 1, #v do
      u = mw.html.create("li")
      e = v[i]
      table.insert(r2, e.code)
      story = false
      less = (e.less == true)
      if not less then
        swift = e.code
        if e.support then
          local scream, support
          s = type(e.support)
          if s == "string" then
            support = e.support
          elseif s == "table" then
            support = faraway(e.support)
          else
            scream = "INVALID"
          end
          if support then
            s = mw.text.trim(support)
            if s == "" then
              scream = "EMPTY"
            elseif s:find("[%[%]|%<%>]") then
              scream = "BAD PAGE"
            else
              support = s
            end
          end
          if scream then
            s = string.format("params.%s.%s[%d].support %s", at, "suggestedvalues", i, scream)
            Fault(s)
          else
            swift = string.format("[[:%s|%s]]", support, swift)
          end
        end
        if all.type:sub(1, 5) == "wiki-" and swift == e.code then
          local rooms = {
            file = 6,
            temp = 10,
            user = 2,
          }
          local ns = rooms[all.type:sub(6, 9)] or 0
          t = mw.title.makeTitle(ns, swift)
          if t and t.exists then
            swift = string.format("[[:%s|%s]]", t.prefixedText, swift)
          end
        end
        if e.code == all.default then
          k = 800
        else
          k = 300
        end
        code =
          mw.html.create("code"):css("font-weight", tostring(k)):css("white-space", "nowrap"):wikitext(swift)
        u:node(code)
      end
      if e.class then
        s = type(e.class)
        if s == "string" then
          u:addClass(e.class)
        elseif s == "table" then
          for k, s in pairs(e.class) do
            u:addClass(s)
          end -- for k, s
        else
          s = string.format("params.%s.%s[%d].class INVALID", at, "suggestedvalues", i)
          Fault(s)
        end
      end
      if e.css then
        if type(e.css) == "table" then
          u:css(e.css)
        else
          s = string.format("params.%s.%s[%d].css INVALID", at, "suggestedvalues", i)
          Fault(s)
        end
      end
      if e.style then
        if type(e.style) == "string" then
          u:cssText(e.style)
        else
          s = string.format("params.%s.%s[%d].style INVALID", at, "suggestedvalues", i)
          Fault(s)
        end
      end
      if all.type == "wiki-file-name" and not e.icon then
        e.icon = e.code
      end
      if e.label then
        s = type(e.label)
        if s == "string" then
          s = mw.text.trim(e.label)
          if s == "" then
            s = string.format("params.%s.%s[%d].label %s", at, "suggestedvalues", i, "EMPTY")
            Fault(s)
          else
            story = s
          end
        elseif s == "table" then
          story = faraway(e.label)
        else
          s = string.format("params.%s.%s[%d].label INVALID", at, "suggestedvalues", i)
          Fault(s)
        end
      end
      s = false
      if type(e.icon) == "string" then
        t = mw.title.makeTitle(6, e.icon)
        if t and t.file.exists then
          local g = mw.html.create("span")
          s = string.format("[[%s|16px]]", t.prefixedText)
          g:attr("role", "presentation"):wikitext(s)
          s = tostring(g)
        end
      end
      if not s and not less and e.label then
        s = mw.ustring.char(0x2013)
      end
      if s then
        d = mw.html.create("span"):wikitext(s)
        if TemplateData.ltr then
          if not less then
            d:css("margin-left", "0.5em")
          end
          if story then
            d:css("margin-right", "0.5em")
          end
        else
          if not less then
            d:css("margin-right", "0.5em")
          end
          if story then
            d:css("margin-left", "0.5em")
          end
        end
        u:node(d)
      end
      if story then
        u:wikitext(story)
      end
      r1:newline():node(u)
    end -- for i
  end
  if not r1 and v ~= false then
    Fault(string.format("params.%s.suggestedvalues INVALID", at))
    r1 = mw.html.create("code"):addClass("error"):wikitext("INVALID")
  end
  return r1, r2
end -- feasible()

local function feat()
  -- Check and store parameter sequence.
  if Data.source then
    local i = 0
    local s
    for k, v in pairs(Data.tree.params) do
      if i == 0 then
        Data.order = {}
        i = 1
        s = k
      else
        i = 2
        break -- for k, v
      end
    end -- for k, v
    if i > 1 then
      local pointers = {}
      local points = {}
      local given = {}
      for k, v in pairs(Data.tree.params) do
        i = facet(k, 1)
        if type(v) == "table" then
          if type(v.label) == "string" then
            s = mw.text.trim(v.label)
            if s == "" then
              s = k
            end
          else
            s = k
          end
          if given[s] then
            if given[s] == 1 then
              local scream = "Parameter label '%s' detected multiple times"
              Fault(string.format(scream, s))
              given[s] = 2
            end
          else
            given[s] = 1
          end
        end
        if i then
          table.insert(points, i)
          pointers[i] = k
          i = facet(k, i)
          if i then
            s = "Parameter '%s' detected twice"
            Fault(string.format(s, k))
          end
        else
          s = "Parameter '%s' not detected"
          Fault(string.format(s, k))
        end
      end -- for k, v
      table.sort(points)
      for i = 1, #points do
        table.insert(Data.order, pointers[points[i]])
      end -- i = 1, #points
    elseif s then
      table.insert(Data.order, s)
    end
  end
end -- feat()

local function feature(access)
  -- Create table row for parameter, check and display violations.
  -- Parameter:
  --     access  -- string, with name
  -- Returns <tr>.
  local mode, s, status
  local fine = function(a)
    s = mw.text.trim(a)
    return a == s and a ~= "" and not a:find("%|=\n") and not a:find("%s%s")
  end
  local begin = mw.html.create("td")
  local code = mw.html.create("code")
  local desc = mw.html.create("td")
  local eager = mw.html.create("td")
  local legal = true
  local param = Data.tree.params[access]
  local ranking = { "required", "suggested", "optional", "deprecated" }
  local r = mw.html.create("tr")
  local styles = "mw-templatedata-doc-param-"
  local sort, typed

  for k, v in pairs(param) do
    if v == "" then
      param[k] = false
    end
  end -- for k, v

  -- label
  sort = param.label or access
  if sort:match("^%d+$") then
    begin:attr("data-sort-value", string.format("%05d", tonumber(sort)))
  end
  begin:css("font-weight", "700"):wikitext(sort)

  -- name and aliases
  code:css("font-size", "0.92em"):css("white-space", "nowrap"):wikitext(access)
  if not fine(access) then
    code:addClass("error")
    Fault(string.format("Bad ID params.<code>%s</code>", access))
    legal = false
    begin:attr("data-sort-value", " " .. sort)
  end
  code = mw.html.create("td"):addClass(styles .. "name"):node(code)
  if access:match("^%d+$") then
    code:attr("data-sort-value", string.format("%05d", tonumber(access)))
  end
  if type(param.aliases) == "table" then
    local lapsus, syn
    for k, v in pairs(param.aliases) do
      code:tag("br")
      if type(v) == "string" then
        if not fine(v) then
          lapsus = true
          code:node(mw.html.create("span"):addClass("error"):css("font-style", "italic"):wikitext("string"))
            :wikitext(s)
        else
          if Config.supportAliases then
            s = string.format("[[%s|%s]]", Config.supportAliases, mw.text.nowiki(s))
          end
          syn = mw.html.create("span"):addClass(styles .. "alias"):css("white-space", "nowrap"):wikitext(s)
          code:node(syn)
        end
      else
        lapsus = true
        code:node(mw.html.create("code"):addClass("error"):wikitext(type(v)))
      end
    end -- for k, v
    if lapsus then
      s = string.format("params.<code>%s</code>.aliases", access)
      Fault(factory("invalid-value"):gsub("$1", s))
      legal = false
    end
  end

  -- description etc.
  s = fashioned(param)
  if s then
    desc:node(s)
  end
  if param.style then
    s = type(param.style)
    if s == "table" then
      desc:css(param.style)
    elseif s == "string" then
      desc:cssText(param.style)
    end
  end
  if param.suggestedvalues or param.default or param.example or param.autovalue then
    local details = {
      "suggestedvalues",
      "default",
      "example",
      "autovalue",
    }
    local dl = mw.html.create("dl")
    local dd, section, show
    for i = 1, #details do
      s = details[i]
      show = param[s]
      if show then
        dd = mw.html.create("dd")
        section = factory("doc-param-" .. s)
        if s == "default" and Config.support4default then
          section = string.format("[[%s|%s]]", Config.support4default, mw.text.nowiki(section))
        end
        if param.type == "boolean" and (show == "0" or show == "1") then
          local boole = Permit.boole[(show == "1")]
          if boole.lead == true then
            dd:node(mw.html.create("code"):wikitext(show)):wikitext(" ")
          end
          if type(boole.show) == "string" then
            local v = mw.html.create("span"):attr("aria-hidden", "true"):wikitext(boole.show)
            if boole.css then
              v:css(boole.css)
            end
            dd:node(v)
          end
          if type(boole.suffix) == "string" then
            dd:wikitext(boole.suffix)
          end
          if boole.lead == false then
            dd:wikitext(" "):node(mw.html.create("code"):wikitext(show))
          end
        elseif s == "suggestedvalues" then
          local v, css, class, ts = facilities(param)
          if v then
            local ul
            ul, v = feasible(param, access, v)
            if v then
              dd:newline():node(ul)
              if css then
                dd:css(css)
                if class then
                  dd:addClass(class)
                end
                if ts then
                  dd:newline()
                  dd:node(ts)
                end
              end
              Data.params[access].suggestedvalues = v
            end
          end
        else
          dd:wikitext(show)
        end
        dl:node(mw.html.create("dt"):wikitext(section)):node(dd)
      end
    end -- i = 1, #details
    desc:node(dl)
  end

  -- type
  if type(param.type) == "string" then
    param.type = mw.text.trim(param.type)
    if param.type == "" then
      param.type = false
    end
  end
  if param.type then
    s = Permit.types[param.type]
    typed = mw.html.create("td"):addClass(styles .. "type")
    if s then
      if s == "string" then
        Data.params[access].type = s
        typed:wikitext(factory("doc-param-type-" .. s)):tag("br")
        typed:node(mw.html.create("span"):addClass("error"):wikitext(param.type))
        Data.lasting = true
      else
        local support = Config["support4" .. param.type]
        s = factory("doc-param-type-" .. param.type)
        if support then
          s = string.format("[[%s|%s]]", support, s)
        end
        typed:wikitext(s)
      end
    else
      Data.params[access].type = "unknown"
      typed:addClass("error"):wikitext("INVALID")
      s = string.format("params.<code>%s</code>.type", access)
      Fault(factory("invalid-value"):gsub("$1", s))
      legal = false
    end
  else
    typed = mw.html.create("td"):wikitext(factory("doc-param-type-unknown"))
    Data.params[access].type = "unknown"
    if param.default then
      Data.params[access].default = nil
      Fault("Default value requires <code>type</code>")
      legal = false
    end
  end
  typed:addClass("navigation-not-searchable")
  -- status
  if param.required then
    mode = 1
    if param.autovalue then
      Fault(string.format("autovalued <code>%s</code> required", access))
      legal = false
    end
    if param.default then
      Fault(string.format("Defaulted <code>%s</code> required", access))
      legal = false
    end
    if param.deprecated then
      Fault(string.format("Required deprecated <code>%s</code>", access))
      legal = false
    end
  elseif param.deprecated then
    mode = 4
  elseif param.suggested then
    mode = 2
  else
    mode = 3
  end
  status = ranking[mode]
  ranking = factory("doc-param-status-" .. status)
  if mode == 1 or mode == 4 then
    ranking = mw.html.create("span"):css("font-weight", "700"):wikitext(ranking)
    if type(param.deprecated) == "string" then
      ranking:tag("br")
      ranking:wikitext(param.deprecated)
    end
    if param.suggested and mode == 4 then
      s = string.format("Suggesting deprecated <code>%s</code>", access)
      Fault(s)
      legal = false
    end
  end
  eager
    :attr("data-sort-value", tostring(mode))
    :node(ranking)
    :addClass(string.format("%sstatus-%s %s", styles, status, "navigation-not-searchable"))

  -- <tr>
  r:attr("id", "templatedata:" .. mw.uri.anchorEncode(access))
    :css(Permit.css[status])
    :addClass(styles .. status)
    :node(begin)
    :node(code)
    :node(desc)
    :node(typed)
    :node(eager)
    :newline()
  if not legal then
    r:css("border", "3px solid #f00")
  end
  return r
end -- feature()

local function features()
  -- Create <table> for parameters.
  -- Returns <table>, or nil.
  local r
  if Data.tree and Data.tree.params then
    local tbl = mw.html.create("table")
    local tr = mw.html.create("tr")
    feat()
    if Data.order and #Data.order > 1 then
      tbl:addClass("sortable")
    end
    if type(Config.classTable) == "table" then
      for k, v in pairs(Config.classTable) do
        tbl:addClass(v)
      end -- for k, v
    end
    if type(Config.cssTable) == "table" then
      tbl:css(Config.cssTable)
    end
    tr:addClass("navigation-not-searchable")
      :node(
        mw.html.create("th"):attr("colspan", "2"):css(Permit.css.tablehead):wikitext(factory("doc-param-name"))
      )
      :node(mw.html.create("th"):css(Permit.css.tablehead):wikitext(factory("doc-param-desc")))
      :node(mw.html.create("th"):css(Permit.css.tablehead):wikitext(factory("doc-param-type")))
      :node(mw.html.create("th"):css(Permit.css.tablehead):wikitext(factory("doc-param-status")))
    tbl
      :newline()
      --         :node( mw.html.create( "thead" )
      :node(tr)
      --              )
      :newline()
    if Data.order then
      local leave, s
      for i = 1, #Data.order do
        s = Data.order[i]
        if s:sub(1, 1) == "=" then
          leave = true
          tbl:node(fatten(s))
          Data.order[i] = false
        elseif s:match("[=|]") then
          Fault(string.format("Bad param <code>%s</code>", s))
        else
          tbl:node(feature(s))
        end
      end -- for i = 1, #Data.order
      if leave then
        for i = #Data.order, 1, -1 do
          if not Data.order[i] then
            table.remove(Data.order, i)
          end
        end -- for i = #Data.order, 1, -1
      end
      Data.tag.paramOrder = Data.order
    end
    if Config.cssTabWrap or Data.scroll then
      r = mw.html.create("div")
      if type(Config.cssTabWrap) == "table" then
        r:css(Config.cssTabWrap)
      elseif type(Config.cssTabWrap) == "string" then
        -- deprecated
        r:cssText(Config.cssTabWrap)
      end
      if Data.scroll then
        r:css("height", Data.scroll):css("overflow", "auto")
      end
      r:node(tbl)
    else
      r = tbl
    end
  end
  return r
end -- features()

local function fellow(any, assigned, at)
  -- Check sets[] parameter and issue error message, if necessary.
  -- Parameter:
  --     any       -- should be number
  --     assigned  -- parameter name
  --     at        -- number, of set
  local s
  if type(any) ~= "number" then
    s = "<code>sets[%d].params[%s]</code>??"
    Fault(string.format(s, at, mw.text.nowiki(tostring(any))))
  elseif type(assigned) == "string" then
    if not Data.got.params[assigned] then
      s = "<code>sets[%d].params %s</code> is undefined"
      Fault(string.format(s, at, assigned))
    end
  else
    s = "<code>sets[%d].params[%d] = %s</code>??"
    Fault(string.format(s, k, type(assigned)))
  end
end -- fellow()

local function fellows()
  -- Check sets[] and issue error message, if necessary.
  local s
  if type(Data.got.sets) == "table" then
    if type(Data.got.params) == "table" then
      for k, v in pairs(Data.got.sets) do
        if type(k) == "number" then
          if type(v) == "table" then
            for ek, ev in pairs(v) do
              if ek == "label" then
                s = type(ev)
                if s ~= "string" and s ~= "table" then
                  s = "<code>sets[%d].label</code>??"
                  Fault(string.format(s, k))
                end
              elseif ek == "params" and type(ev) == "table" then
                for pk, pv in pairs(ev) do
                  fellow(pk, pv, k)
                end -- for pk, pv
              else
                ek = mw.text.nowiki(tostring(ek))
                s = "<code>sets[%d][%s]</code>??"
                Fault(string.format(s, k, ek))
              end
            end -- for ek, ev
          else
            k = mw.text.nowiki(tostring(k))
            v = mw.text.nowiki(tostring(v))
            s = string.format("<code>sets[%s][%s]</code>??", k, v)
            Fault(s)
          end
        else
          k = mw.text.nowiki(tostring(k))
          s = string.format("<code>sets[%s]</code> ?????", k)
          Fault(s)
        end
      end -- for k, v
    else
      s = "<code>params</code> required for <code>sets</code>"
      Fault(s)
    end
  else
    s = "<code>sets</code> needs to be of <code>object</code> type"
    Fault(s)
  end
end -- fellows()

local function finalize(advance)
  -- Wrap presentation into frame.
  -- Parameter:
  --     advance  -- true, for nice
  -- Returns string.
  local r, lapsus
  if Data.div then
    r = tostring(Data.div)
  elseif Data.strip then
    r = Data.strip
  else
    lapsus = true
    r = ""
  end
  r = r .. failures()
  if Data.source then
    local live = (advance or lapsus)
    if not live then
      live = TemplateData.frame:preprocess("{{REVISIONID}}")
      live = (live == "")
    end
    if live then
      r = r .. fancy(advance, lapsus)
    end
  end
  return r
end -- finalize()

local function find()
  -- Find JSON data within page source (title).
  -- Returns string, or nil.
  local s = Data.title:getContent()
  local i, j = s:find("<templatedata>", 1, true)
  local r
  if i then
    local k = s:find("</templatedata>", j, true)
    if k then
      r = mw.text.trim(s:sub(j + 1, k - 1))
    end
  end
  return r
end -- find()

local function flat(adjust)
  -- Remove formatting from text string for VisualEditor.
  -- Parameter:
  --     arglist  -- string, to be stripped, or nil
  -- Returns string, or nil.
  local r
  if adjust then
    r = adjust:gsub("\n", " ")
    if r:find("<noexport>", 1, true) then
      r = r:gsub("<noexport>.*</noexport>", "")
    end
    if r:find("<exportonly>", 1, true) then
      r = r:gsub("</?exportonly>", "")
    end
    if r:find("''", 1, true) then
      r = r:gsub("'''", ""):gsub("''", "")
    end
    if r:find("<", 1, true) then
      local Text = Fetch("Text")
      r = r:gsub("<br */?>", "\r\n"):gsub("<sup>2</sup>", "&sup2;"):gsub("<sup>3</sup>", "&sup3;")
      r = Text.getPlain(r)
    end
    if r:find("[", 1, true) then
      local WLink = Fetch("WLink")
      if WLink.isBracketedURL(r) then
        r = r:gsub("%[([hf]tt?ps?://%S+) [^%]]+%]", "%1")
      end
      r = WLink.getPlain(r)
    end
    if r:find("&", 1, true) then
      r = mw.text.decode(r)
      if r:find("&shy;", 1, true) then
        r = r:gsub("&shy;", "")
      end
    end
  end
  return r
end -- flat()

local function flush()
  -- JSON encode narrowed input; obey unnamed (numerical) parameters.
  -- Returns <templatedata> JSON string.
  local r
  if Data.tag then
    r = mw.text.jsonEncode(Data.tag):gsub("%}$", ",")
  else
    r = "{"
  end
  r = r .. '\n"params":{'
  if Data.order then
    local sep = ""
    local s
    for i = 1, #Data.order do
      s = Data.order[i]
      r = string.format("%s%s\n%s:%s", r, sep, mw.text.jsonEncode(s), mw.text.jsonEncode(Data.params[s]))
      sep = ",\n"
    end -- for i = 1, #Data.order
  end
  r = r .. "\n}\n}"
  return r
end -- flush()

local function focus(access)
  -- Check components; focus multilingual description, build trees.
  -- Parameter:
  --     access  -- string, name of parameter, nil for root
  local f = function(a, at)
    local r
    if at then
      r = string.format("<code>params.%s</code>", at)
    else
      r = "''root''"
    end
    if a then
      r = string.format("%s<code>.%s</code>", r, a)
    end
    return r
  end
  local parent
  if access then
    parent = Data.got.params[access]
  else
    parent = Data.got
  end
  if type(parent) == "table" then
    local elem, permit, s, scope, slot, tag, target
    if access then
      permit = Permit.params
      if type(access) == "number" then
        slot = tostring(access)
      else
        slot = access
      end
    else
      permit = Permit.root
    end
    for k, v in pairs(parent) do
      scope = permit[k]
      if scope then
        s = type(v)
        if s == "string" and k ~= "format" then
          v = mw.text.trim(v)
        end
        if scope:find(s, 1, true) then
          if scope:find("I18N", 1, true) then
            if s == "string" then
              elem = fair(v)
            elseif s == "table" then
              local translated
              v, translated = faraway(v)
              if v then
                if translated and k == "description" then
                  elem = {
                    [1] = fair(v),
                    [2] = translated,
                  }
                else
                  elem = fair(v)
                end
              else
                elem = false
              end
            end
            if type(v) == "string" then
              if k == "deprecated" then
                if v == "1" then
                  v = true
                elseif v == "0" then
                  v = false
                end
                elem = v
              elseif scope:find("nowiki", 1, true) then
                elem = mw.text.nowiki(v)
                elem = elem:gsub("&#13;" .. string.char(10), "<br>")
                v = v:gsub(string.char(13), "")
              else
                v = flat(v)
              end
            elseif s == "boolean" then
              if scope:find("boolean", 1, true) then
                elem = v
              else
                s = "Type <code>boolean</code> bad for " .. f(k, slot)
                Fault(s)
              end
            end
          else
            if k == "params" and not access then
              v = nil
              elem = nil
            elseif k == "format" and not access then
              elem = mw.text.decode(v)
              v = nil
            elseif k == "inherits" then
              elem = v
              if not Data.heirs then
                Data.heirs = {}
              end
              Data.heirs[slot] = v
              v = nil
            elseif k == "style" then
              elem = v
              v = nil
            elseif s == "string" then
              v = mw.text.nowiki(v)
              elem = v
            else
              elem = v
            end
          end
          if type(elem) ~= "nil" then
            if not target then
              if access then
                if not Data.tree.params then
                  Data.tree.params = {}
                end
                Data.tree.params[slot] = {}
                target = Data.tree.params[slot]
              else
                Data.tree = {}
                target = Data.tree
              end
            end
            target[k] = elem
            elem = false
          end
          if type(v) ~= "nil" then
            if not tag then
              if access then
                if type(v) == "string" and v.sub(1, 1) == "=" then
                  v = nil
                else
                  if not Data.params then
                    Data.params = {}
                  end
                  Data.params[slot] = {}
                  tag = Data.params[slot]
                end
              else
                Data.tag = {}
                tag = Data.tag
              end
            end
            if type(v) ~= "nil" and k ~= "suggestedvalues" then
              tag[k] = v
            end
          end
        else
          s = string.format("Type <code>%s</code> bad for %s", scope, f(k, slot))
          Fault(s)
        end
      else
        Fault("Unknown component " .. f(k, slot))
      end
    end -- for k, v
    if not access and Data.got.sets then
      fellows()
    end
  else
    Fault(f() .. " needs to be of <code>object</code> type")
  end
end -- focus()

local function format()
  -- Build formatted element.
  -- Returns <inline>.
  local source = Data.tree.format:lower()
  local r, s
  if source == "inline" or source == "block" then
    r = mw.html.create("i"):wikitext(source)
  else
    local code
    if source:find("|", 1, true) then
      local scan = "^[\n ]*%{%{[\n _]*|[\n _]*=[\n _]*%}%}[\n ]*$"
      if source:match(scan) then
        code = source:gsub("\n", "N")
      else
        s = mw.text.nowiki(source):gsub("\n", "&#92;n")
        s = tostring(mw.html.create("code"):wikitext(s))
        Fault("Invalid format " .. s)
        source = false
      end
    else
      local words = mw.text.split(source, "%s+")
      local show, start, support, unknown
      for i = 1, #words do
        s = words[i]
        if i == 1 then
          start = s
        end
        support = Permit.builder[s]
        if support == start or support == "*" then
          Permit.builder[s] = true
        elseif s:match("^[1-9]%d?") and Permit.builder.align then
          Permit.builder.align = tonumber(s)
        else
          if unknown then
            unknown = string.format("%s %s", unknown, s)
          else
            unknown = s
          end
        end
      end -- i = 1, #words
      if unknown then
        s = tostring(mw.html.create("code"):css("white-space", "nowrap"):wikitext(s))
        Fault("Unknown/misplaced format keyword " .. s)
        source = false
        start = false
      end
      if start == "inline" then
        if Permit.builder.half == true then
          show = "inline half"
          code = "{{_ |_=_}}"
        elseif Permit.builder.grouped == true then
          show = "inline grouped"
          code = "{{_ | _=_}}"
        elseif Permit.builder.spaced == true then
          show = "inline spaced"
          code = "{{_ | _ = _ }}"
        end
        if Permit.builder.newlines == true then
          show = show or "inline"
          code = code or "{{_|_=_}}"
          show = show .. " newlines"
          code = string.format("N%sN", code)
        end
      elseif start == "block" then
        local space = ""   -- amid "|" and name
        local spaced = " " -- preceding "="
        local spacer = " " -- following "="
        local suffix = "N" -- closing "}}" on new line
        show = "block"
        if Permit.builder.indent == true then
          start = " "
          show = "block indent"
        else
          start = ""
        end
        if Permit.builder.compressed == true then
          spaced = ""
          spacer = ""
          show = show .. " compressed"
          if Permit.builder.last == true then
            show = show .. " last"
          else
            suffix = ""
          end
        else
          if Permit.builder.lead == true then
            show = show .. " lead"
            space = " "
          end
          if type(Permit.builder.align) ~= "string" then
            local n
            s = " align"
            if Permit.builder.align == true then
              n = 0
              if type(Data.got) == "table" and type(Data.got.params) == "table" then
                for k, v in pairs(Data.got.params) do
                  if type(v) == "table" and not v.deprecated and type(k) == "string" then
                    k = mw.ustring.len(k)
                    if k > n then
                      n = k
                    end
                  end
                end -- for k, v
              end
            else
              n = Permit.builder.align
              if type(n) == "number" and n > 1 then
                s = string.format("%s %d", s, n)
              else
                n = 0 -- How come?
              end
            end
            if n > 1 then
              spaced = string.rep("_", n - 1) .. " "
            end
            show = show .. s
          elseif Permit.builder.after == true then
            spaced = ""
            show = show .. " after"
          elseif Permit.builder.dense == true then
            spaced = ""
            spacer = ""
            show = show .. " dense"
          end
          if Permit.builder.last == true then
            suffix = spacer
            show = show .. " last"
          end
        end
        code = string.format("N{{_N%s|%s_%s=%s_%s}}N", start, space, spaced, spacer, suffix)
        if show == "block" then
          show = "block newlines"
        end
      end
      if show then
        r = mw.html.create("span"):wikitext(show)
      end
    end
    if code then
      source = code:gsub("N", "\n")
      code = mw.text.nowiki(code):gsub("N", "&#92;n")
      code = mw.html.create("code"):css("margin-left", "1em"):css("margin-right", "1em"):wikitext(code)
      if r then
        r = mw.html.create("span"):node(r):node(code)
      else
        r = code
      end
    end
  end
  if source and Data.tag then
    Data.tag.format = source
  end
  return r
end -- format()

local function formatter()
  -- Build presented documentation.
  -- Returns <div>.
  local r = mw.html.create("div")
  local x = fashioned(Data.tree, true, r)
  local s
  if x then
    r = x
  end
  if Data.leading then
    local toc = mw.html.create("div")
    local shift
    if Config.suppressTOCnum then
      toc:addClass(Config.suppressTOCnum)
      if type(Config.stylesTOCnum) == "string" then
        local src = Config.stylesTOCnum .. "/styles.css"
        s = TemplateData.frame:extensionTag("templatestyles", nil, { src = src })
        r:newline():node(s)
      end
    end
    toc:addClass("navigation-not-searchable"):css("margin-top", "0.5em"):wikitext("__TOC__")
    if Data.sibling then
      local block = mw.html.create("div")
      if TemplateData.ltr then
        shift = "right"
      else
        shift = "left"
      end
      block:css("float", shift):wikitext(Data.sibling)
      r:newline():node(block):newline()
    end
    r:newline():node(toc):newline()
    if shift then
      r:node(mw.html.create("div"):css("clear", shift)):newline()
    end
  end
  s = features()
  if s then
    if Data.leading then
      r:node(mw.html.create("h" .. Config.nested):wikitext(factory("doc-params"))):newline()
    end
    r:node(s)
  end
  if Data.shared then
    local global = mw.html.create("div"):attr("id", "templatedata-global")
    local shift
    if TemplateData.ltr then
      shift = "right"
    else
      shift = "left"
    end
    global:css("float", shift):wikitext(string.format("[[%s|%s]]", Data.shared, "Global"))
    r:newline():node(global)
  end
  if Data.tree and Data.tree.format then
    local e = format()
    if e then
      local show = "Format"
      if Config.supportFormat then
        show = string.format("[[%s|%s]]", Config.supportFormat, show)
      end
      r:node(mw.html.create("p"):addClass("navigation-not-searchable"):wikitext(show .. ": "):node(e))
    end
  end
  return r
end -- formatter()

local function free()
  -- Remove JSON comment lines.
  if Data.source:find("//", 1, true) then
    Data.source:gsub("([{,\"'])(%s*\n%s*//.*\n%s*)([{},\"'])", "%1%3")
  end
end -- free()

local function full()
  -- Build survey table from JSON data, append invisible <templatedata>.
  Data.div = mw.html.create("div"):addClass("mw-templatedata-doc-wrap")
  if Permit.css.bg then
    Data.div:css(Permit.css.bg)
  end
  if Permit.css.fg then
    Data.div:css(Permit.css.fg)
  end
  focus()
  if Data.tag then
    if type(Data.got.params) == "table" then
      for k, v in pairs(Data.got.params) do
        focus(k)
      end -- for k, v
      if Data.heirs then
        fathers()
      end
    end
  end
  Data.div:node(formatter())
  if not Data.lazy then
    Data.slim = flush()
    if TemplateData.frame then
      local div = mw.html.create("div")
      local tdata = {
        [1] = "templatedata",
        [2] = Data.slim,
      }
      Data.strip = TemplateData.frame:callParserFunction("#tag", tdata)
      div:wikitext(Data.strip)
      if Config.loudly then
        Data.div:node(mw.html.create("hr"):css({ height = "7ex" }))
      else
        div:css("display", "none")
      end
      Data.div:node(div)
    end
  end
  if Data.lasting then
    Fault("deprecated type syntax")
  end
  if Data.less then
    Fault(Config.solo)
  end
end -- full()

local function furnish(adapt, arglist)
  -- Analyze transclusion.
  -- Parameter:
  --     adapt    -- table, #invoke parameters
  --     arglist  -- table, template parameters
  -- Returns string.
  local source
  favorize()
  -- deprecated:
  for k, v in pairs(Config.basicCnf) do
    if adapt[k] and adapt[k] ~= "" then
      Config[v] = adapt[k]
    end
  end -- for k, v
  if arglist.heading and arglist.heading:match("^[3-6]$") then
    Config.nested = arglist.heading
  else
    Config.nested = "2"
  end
  Config.loudly = faculty(arglist.debug or adapt.debug)
  Data.lazy = faculty(arglist.lazy) and not Config.loudly
  Data.leading = faculty(arglist.TOC)
  if Data.leading and arglist.TOCsibling then
    Data.sibling = mw.text.trim(arglist.TOCsibling)
  end
  if arglist.lang then
    Data.slang = arglist.lang:lower()
  elseif adapt.lang then
    Data.slang = adapt.lang:lower()
  end
  if arglist.JSON then
    source = arglist.JSON
  elseif arglist.Global then
    source = TemplateData.getGlobalJSON(arglist.Global, arglist.Local)
  elseif arglist[1] then
    local s = mw.text.trim(arglist[1])
    local start = s:sub(1, 1)
    if start == "<" then
      Data.strip = s
    elseif start == "{" then
      source = s
    elseif mw.ustring.sub(s, 1, 8) == mw.ustring.char(127, 39, 34, 96, 85, 78, 73, 81) then
      Data.strip = s
    end
  end
  if type(arglist.vertical) == "string" and arglist.vertical:match("^%d*%.?%d+[emprx]+$") then
    Data.scroll = arglist.vertical
  end
  if not source then
    Data.title = mw.title.getCurrentTitle()
    source = find()
    if not source and not Data.title.text:match(Config.subpage) then
      local s = string.format(Config.suffix, Data.title.prefixedText)
      Data.title = mw.title.new(s)
      if Data.title.exists then
        source = find()
      end
    end
  end
  if not Data.lazy then
    if not Data.title then
      Data.title = mw.title.getCurrentTitle()
    end
    Data.lazy = Data.title.text:match(Config.subpage)
  end
  if type(source) == "string" then
    TemplateData.getPlainJSON(source)
  end
  return finalize(faculty(arglist.source))
end -- furnish()

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 == "~")
  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
    elseif link then
      r = false
    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()

TemplateData.getGlobalJSON = function(access, adapt)
  -- Retrieve TemplateData from a global repository (JSON).
  -- Parameter:
  --     access  -- string, with page specifier (on WikiMedia Commons)
  --     adapt   -- JSON string or table with local overrides
  -- Returns true, if succeeded.
  local plugin = Fetch("/global")
  local r
  if type(plugin) == "table" and type(plugin.fetch) == "function" then
    local s, got = plugin.fetch(access, adapt)
    if got then
      Data.got = got
      Data.order = got.paramOrder
      Data.shared = s
      r = true
      full()
    else
      Fault(s)
    end
  end
  return r
end -- TemplateData.getGlobalJSON()

TemplateData.getPlainJSON = function(adapt)
  -- Reduce enhanced JSON data to plain text localized JSON.
  -- Parameter:
  --     adapt  -- string, with enhanced JSON
  -- Returns string, or not.
  if type(adapt) == "string" then
    local JSONutil = Fetch("JSONutil", true)
    Data.source = adapt
    free()
    if JSONutil then
      local Multilingual = Fetch("Multilingual", true)
      local f
      if Multilingual then
        f = Multilingual.i18n
      end
      Data.got = JSONutil.fetch(Data.source, true, f)
    else
      local lucky
      lucky, Data.got = pcall(mw.text.jsonDecode, Data.source)
    end
    if type(Data.got) == "table" then
      full()
    elseif not Data.strip then
      local scream = type(Data.got)
      if scream == "string" then
        scream = Data.got
      else
        scream = "Data.got: " .. scream
      end
      Fault("fatal JSON error: " .. scream)
    end
  end
  return Data.slim
end -- TemplateData.getPlainJSON()

TemplateData.test = function(adapt, arglist)
  TemplateData.frame = mw.getCurrentFrame()
  return furnish(adapt, arglist)
end -- TemplateData.test()

-- Export.
local p = {}

p.f = function(frame)
  -- Template call.
  local lucky, r
  TemplateData.frame = frame
  lucky, r = pcall(furnish, frame.args, frame:getParent().args)
  if not lucky then
    Fault("INTERNAL: " .. r)
    r = failures()
  end
  return r
end -- p.f

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.TemplateData = function()
  -- Module interface.
  return TemplateData
end

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

return p

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.