Module:JSONutil/sandbox

local JSONutil = {
  item = 63869449,
  serial = "2020-11-08",
  suite = "JSONutil"
}

--[=[
      Preprocess or generate JSON data.
]=]

local Failsafe = JSONutil

JSONutil.Encoder = {
  scream = "@error@JSONencoder@",
  sep = string.char(10),
  stab = string.char(9)
}

JSONutil.more = 50  -- Length of trailing context.

local Fallback = function()
  -- Retrieve current default language code.
  -- Returns string.
  return mw.language.getContentLanguage():getCode():lower()
end -- Fallback()

local flat = function(adjust)
  -- Clean template argument string.
  -- Parameter:
  --     adjust  -- string, or not
  -- Returns:
  --     string
  local r
  if adjust then
    r = mw.text.trim(mw.text.unstripNoWiki(adjust))
  else
    r = ""
  end
  return r
end -- flat()

local flip = function(frame)
  -- Retrieve template argument indent.
  -- Parameter:
  --     frame  -- object
  -- Returns:
  --     Number of indentation level, or not.
  local r
  if frame.args.indent and frame.args.indent:match("^%d+$") then
    r = tonumber(frame.args.indent)
  end
  return r
end -- flip()

JSONutil.Encoder.Array = function(apply, adapt, alert)
  -- Convert table to JSON array.
  -- Parameter:
  --     apply  -- table, with sequence of raw elements, or
  --               string, with formatted Array, or empty
  --     adapt  -- string, with requested type, or not
  --     alert  -- true, if non-numeric elements shall trigger errors
  -- Returns:
  --     String, with JSON array.
  local r = type(apply)
  if r == "string" then
    r = mw.text.trim(apply)
    if r == "" then
      r = "[]"
    elseif r:byte(1, 1) ~= 0x5B or r:byte(-1, -1) ~= 0x5D then
      r = false
    end
  elseif r == "table" then
    local n = 0
    local strange
    for k, v in pairs(apply) do
      if type(k) == "number" then
        if k > n then
          n = k
        end
      elseif alert then
        if strange then
          strange = strange .. " "
        else
          strange = ""
        end
        strange = strange .. tostring(k)
      end
    end -- for k, v
    if strange then
      r = string.format('{ "%s": "%s" }', JSONutil.Encoder.scream, JSONutil.Encoder.string(strange))
    elseif n > 0 then
      local sep = ""
      local scope = adapt or "string"
      local s
      if type(JSONutil.Encoder[scope]) ~= "function" then
        scope = "string"
      end
      r = " ]"
      for i = n, 1, -1 do
        s = JSONutil.Encoder[scope](apply[i])
        r = string.format("%s%s%s", s, sep, r)
        sep = ",\n  "
      end -- for i = n, 1, -1
      r = "[ " .. r
    else
      r = "[]"
    end
  else
    r = false
  end
  if not r then
    r = string.format('[ "%s * %s" ]', JSONutil.Encoder.scream, "Bad Array")
  end
  return r
end -- JSONutil.Encoder.Array()

JSONutil.Encoder.boolean = function(apply)
  -- Convert string to JSON boolean.
  -- Parameter:
  --     apply  -- string, with value
  -- Returns:
  --     Boolean, as string.
  local r = mw.text.trim(apply)
  if r == "" or r == "null" or r == "false" or r == "0" or r == "-" then
    r = "false"
  else
    r = "true"
  end
  return r
end -- JSONutil.Encoder.boolean()

JSONutil.Encoder.Component = function(access, apply, adapt, align, alert)
  -- Create single entry for mapping object.
  -- Parameter:
  --     access  -- string, with component name
  --     apply   -- component value
  --     adapt   -- string, with value type, or not
  --     align   -- number, of indentation level, or not
  --     alert   --
  -- Returns:
  --     String, with JSON fragment and comma.
  local v = apply
  local types = adapt
  local indent, liner, scope, sep, sign
  if type(access) == "string" then
    sign = mw.text.trim(access)
    if sign == "" then
      sign = false
    end
  end
  if type(types) == "string" then
    types = mw.text.split(mw.text.trim(types), "%s+")
  end
  if type(types) ~= "table" then
    types = {}
    table.insert(types, "string")
  end
  if #types == 1 then
    scope = types[1]
  else
    for i = 1, #types do
      if types[i] == "boolean" then
        if v == "1" or v == 1 or v == true then
          v = "true"
          scope = "boolean"
        elseif v == "0" or v == 0 or v == false then
          v = "false"
          scope = "boolean"
        end
        if scope then
          types = {}
          break -- for i
        else
          table.remove(types, i)
        end
      end
    end -- for i
    for i = 1, #types do
      if types[i] == "number" then
        if tonumber(v) then
          v = tostring(v)
          scope = "number"
          types = {}
          break -- for i
        else
          table.remove(types, i)
        end
      end
    end -- for i
  end
  scope = scope or "string"
  if type(JSONutil.Encoder[scope]) ~= "function" then
    scope = "string"
  elseif scope == "I18N" then
    scope = "Polyglott"
  end
  if scope == "string" then
    v = v or ""
  end
  if type(align) == "number" and align > 0 then
    indent = math.floor(align)
    if indent == 0 then
      indent = false
    end
  end
  if scope == "object" or not sign then
    liner = true
  elseif scope == "string" then
    local k = mw.ustring.len(sign) + mw.ustring.len(v)
    if k > 60 then
      liner = true
    end
  end
  if liner then
    if indent then
      sep = "\n" .. string.rep("  ", indent)
    else
      sep = "\n"
    end
  else
    sep = " "
  end
  if indent then
    indent = indent + 1
  end
  return string.format(' "%s":%s%s,\n', sign or "???", sep, JSONutil.Encoder[scope](v, indent))
end -- JSONutil.Encoder.Component()

JSONutil.Encoder.Hash = function(apply, adapt, alert)
  -- Create entries for mapping object.
  -- Parameter:
  --     apply  -- table, with element value assignments
  --     adapt  -- table, with value types assignment, or not
  -- Returns:
  --     String, with JSON fragment and comma.
  local r = ""
  local s
  for k, v in pairs(apply) do
    if type(adapt) == "table" then
      s = adapt[k]
    end
    r = r .. JSONutil.Encoder.Component(tostring(k), v, s)
  end -- for k, v
  return
end -- JSONutil.Encoder.Hash()

JSONutil.Encoder.I18N = function(apply, align)
  -- Convert multilingual string table to JSON.
  -- Parameter:
  --     apply  -- table, with mapping object
  --     align  -- number, of indentation level, or not
  -- Returns:
  --     String, with JSON object.
  local r = type(apply)
  if r == "table" then
    local strange
    local fault = function(a)
      if strange then
        strange = strange .. " *\n "
      else
        strange = ""
      end
      strange = strange .. a
    end
    local got, sep, indent
    for k, v in pairs(apply) do
      if type(k) == "string" then
        k = mw.text.trim(k)
        if type(v) == "string" then
          v = mw.text.trim(v)
          if v == "" then
            fault(string.format("%s %s=", "Empty text", k))
          end
          if not (k:match("%l%l%l?") or k:match("%l%l%l?-%u%u") or k:match("%l%l%l?-%u%l%l%l+")) then
            fault(string.format("%s %s=", "Strange language code", k))
          end
        else
          v = tostring(v)
          fault(string.format("%s %s=%s", "Bad type for text", k, type(v)))
        end
        got = got or {}
        got[k] = v
      else
        fault(string.format("%s %s: %s", "Bad language code type", type(k), tostring(k)))
      end
    end -- for k, v
    if not got then
      fault("No language codes")
      got = {}
    end
    if strange then
      got[JSONutil.Encoder.scream] = strange
    end
    r = false
    if type(align) == "number" and align > 0 then
      indent = math.floor(align)
    else
      indent = 0
    end
    sep = string.rep("  ", indent + 1)
    for k, v in pairs(got) do
      if r then
        r = r .. ",\n"
      else
        r = ""
      end
      r = string.format("%s  %s%s: %s", r, sep, JSONutil.Encoder.string(k), JSONutil.Encoder.string(v))
    end -- for k, v
    r = string.format("{\n%s\n%s}", r, sep)
  elseif r == "string" then
    r = JSONutil.Encoder.string(apply)
  else
    r = string.format('{ "%s": "%s: %s" }', JSONutil.Encoder.scream, "Bad Lua type", r)
  end
  return r
end -- JSONutil.Encoder.I18N()

JSONutil.Encoder.number = function(apply)
  -- Convert string to JSON number.
  -- Parameter:
  --     apply  -- string, with presumable number
  -- Returns:
  --     Number or "NaN".
  local s = mw.text.trim(apply)
  JSONutil.Encoder.minus = JSONutil.Encoder.minus or mw.ustring.char(0x2212)
  s = s:gsub(JSONutil.Encoder.minus, "-")
  return tonumber(s:lower()) or "NaN"
end -- JSONutil.Encoder.number()

JSONutil.Encoder.object = function(apply, align)
  -- Create mapping object.
  -- Parameter:
  --     apply  -- string, with components, may end with comma
  --     align  -- number, of indentation level, or not
  -- Returns:
  --     String, with JSON fragment.
  local story = mw.text.trim(apply)
  local start = ""
  if story:sub(-1) == "," then
    story = story:sub(1, -2)
  end
  if type(align) == "number" and align > 0 then
    local indent = math.floor(align)
    if indent > 0 then
      start = string.rep("  ", indent)
    end
  end
  return string.format("%s{ %s\n%s}", start, story, start)
end -- JSONutil.Encoder.object()

JSONutil.Encoder.Polyglott = function(apply, align)
  -- Convert string or multilingual string table to JSON.
  -- Parameter:
  --     apply  -- string, with string or object
  --     align  -- number, of indentation level, or not
  -- Returns:
  --     string
  local r = type(apply)
  if r == "string" then
    r = mw.text.trim(apply)
    if not r:match('^{%s*"') or not r:match('"%s*}$') then
      r = JSONutil.Encoder.string(r)
    end
  else
    r = string.format('{ "%s": "%s: %s" }', JSONutil.Encoder.scream, "Bad Lua type", r)
  end
  return r
end -- JSONutil.Encoder.Polyglott()

JSONutil.Encoder.string = function(apply)
  -- Convert plain string to strict JSON string.
  -- Parameter:
  --     apply  -- string, with plain string
  -- Returns:
  --     string, with quoted trimmed JSON string
  return string.format(
    '"%s"',
    mw.text
      .trim(apply)
      :gsub("\\", "\\\\")
      :gsub('"', '\\"')
      :gsub(JSONutil.Encoder.sep, "\\n")
      :gsub(JSONutil.Encoder.stab, "\\t")
  )
end -- JSONutil.Encoder.string()

JSONutil.fair = function(apply)
  -- Reduce enhanced JSON data to strict JSON.
  -- Parameter:
  --     apply  -- string, with enhanced JSON
  -- Returns:
  --     1    -- string|nil|false, with error keyword
  --     2    -- string, with JSON or context
  local m = 0
  local n = 0
  local s = mw.text.trim(apply)
  local i, j, last, r, scan, sep0, sep1, start, stub, suffix
  local framework = function(a)
    -- Syntax analysis outside strings.
    local k = 1
    local c
    while k do
      k = a:find("[{%[%]}]", k)
      if k then
        c = a:byte(k, k)
        if c == 0x7B then -- {
          m = m + 1
        elseif c == 0x7D then -- }
          m = m - 1
        elseif c == 0x5B then -- [
          n = n + 1
        else -- ]
          n = n - 1
        end
        k = k + 1
      end
    end -- while k
  end -- framework()
  local free = function(a, at, f)
    -- Throws: error if /* is not matched by */
    local s = a
    local i = s:find("//", at, true)
    local k = s:find("/*", at, true)
    if i or k then
      local m = s:find(sep0, at)
      if i and (not m or i < m) then
        k = s:find("\n", i + 2, true)
        if k then
          if i == 1 then
            s = s:sub(k + 1)
          else
            s = s:sub(1, i - 1) .. s:sub(k + 1)
          end
        elseif i > 1 then
          s = s:sub(1, i - 1)
        else
          s = ""
        end
      elseif k and (not m or k < m) then
        i = s:find("*/", k + 2, true)
        if i then
          if k == 1 then
            s = s:sub(i + 2)
          else
            s = s:sub(1, k - 1) .. s:sub(i + 2)
          end
        else
          error(s:sub(k + 2), 0)
        end
        i = k
      else
        i = false
      end
      if i then
        s = mw.text.trim(s)
        if s:find("/", 1, true) then
          s = f(s, i, f)
        end
      end
    end
    return s
  end -- free()
  if s:sub(1, 1) == "{" then
    s = s:gsub(string.char(13, 10), JSONutil.Encoder.sep):gsub(string.char(13), JSONutil.Encoder.sep)
    stub = s:gsub(JSONutil.Encoder.sep, ""):gsub(JSONutil.Encoder.stab, "")
    scan = string.char(0x5B, 0x01, 0x2D, 0x1F, 0x5D) -- [ \-\ ]
    j = stub:find(scan)
    if j then
      r = "ControlChar"
      s = mw.text.trim(s:sub(j + 1))
      s = mw.ustring.sub(s, 1, JSONutil.more)
    else
      i = true
      j = 1
      last = (stub:sub(-1) == "}")
      sep0 = string.char(0x5B, 0x22, 0x27, 0x5D) -- [ " ' ]
      sep1 = string.char(0x5B, 0x5C, 0x22, 0x5D) -- [ \ " ]
    end
  else
    r = "Bracket0"
    s = mw.ustring.sub(s, 1, JSONutil.more)
  end
  while i do
    i, s = pcall(free, s, j, free)
    if i then
      i = s:find(sep0, j)
    else
      r = "CommentEnd"
      s = mw.text.trim(s)
      s = mw.ustring.sub(s, 1, JSONutil.more)
    end
    if i then
      if j == 1 then
        framework(s:sub(1, i - 1))
      end
      if s:sub(i, i) == '"' then
        stub = s:sub(j, i - 1)
        if stub:find('[^"]*,%s*[%]}]') then
          r = "CommaEnd"
          s = mw.text.trim(stub)
          s = mw.ustring.sub(s, 1, JSONutil.more)
          i = false
          j = false
        else
          if j > 1 then
            framework(stub)
          end
          i = i + 1
          j = i
        end
        while j do
          j = s:find(sep1, j)
          if j then
            if s:sub(j, j) == '"' then
              start = s:sub(1, i - 1)
              suffix = s:sub(j)
              if j > i then
                stub =
                  s:sub(i, j - 1):gsub(JSONutil.Encoder.sep, "\\n"):gsub(JSONutil.Encoder.stab, "\\t")
                j = i + stub:len()
                s = string.format("%s%s%s", start, stub, suffix)
              else
                s = start .. suffix
              end
              j = j + 1
              break -- while j
            else
              j = j + 2
            end
          else
            r = "QouteEnd"
            s = mw.text.trim(s:sub(i))
            s = mw.ustring.sub(s, 1, JSONutil.more)
            i = false
          end
        end -- while j
      else
        r = "Qoute"
        s = mw.text.trim(s:sub(i))
        s = mw.ustring.sub(s, 1, JSONutil.more)
        i = false
      end
    elseif not r then
      stub = s:sub(j)
      if stub:find('[^"]*,%s*[%]}]') then
        r = "CommaEnd"
        s = mw.text.trim(stub)
        s = mw.ustring.sub(s, 1, JSONutil.more)
      else
        framework(stub)
      end
    end
  end -- while i
  if not r and (m ~= 0 or n ~= 0) then
    if m ~= 0 then
      s = "}"
      if m > 0 then
        r = "BracketCloseLack"
        j = m
      elseif m < 0 then
        r = "BracketClosePlus"
        j = -m
      end
    else
      s = "]"
      if n > 0 then
        r = "BracketCloseLack"
        j = n
      else
        r = "BracketClosePlus"
        j = -n
      end
    end
    if j > 1 then
      s = string.format("%d %s", j, s)
    end
  elseif not (r or last) then
    stub = suffix or apply or ""
    j = stub:find("/", 1, true)
    if j then
      i, stub = pcall(free, stub, j, free)
    else
      i = true
    end
    stub = mw.text.trim(stub)
    if i then
      if stub:sub(-1) ~= "}" then
        r = "Trailing"
        s = stub:match("%}%s*(%S[^%}]*)$")
        if s then
          s = mw.ustring.sub(s, 1, JSONutil.more)
        else
          s = mw.ustring.sub(stub, -JSONutil.more)
        end
      end
    else
      r = "CommentEnd"
      s = mw.ustring.sub(stub, 1, JSONutil.more)
    end
  end
  if r and s then
    s = s:gsub(JSONutil.Encoder.sep, " ")
    s = mw.text.encode(s):gsub("|", "&#124;")
  end
  return r, s
end -- JSONutil.fair()

JSONutil.fault = function(alert, add, adapt)
  -- Retrieve formatted message.
  -- Parameter:
  --     alert  -- string, with error keyword, or other text
  --     add    -- string|nil|false, with context
  --     adapt  -- function|string|table|nil|false, for I18N
  -- Returns string, with HTML span.
  local e = mw.html.create("span"):addClass("error")
  local s = alert
  if type(s) == "string" then
    s = mw.text.trim(s)
    if s == "" then
      s = "EMPTY JSONutil.fault key"
    end
    if not s:find(" ", 1, true) then
      local storage = string.format("I18n/Module:%s.tab", JSONutil.suite)
      local lucky, t = pcall(mw.ext.data.get, storage, "_")
      if type(t) == "table" then
        t = t.data
        if type(t) == "table" then
          local e
          s = "err_" .. s
          for i = 1, #t do
            e = t[i]
            if type(e) == "table" then
              if e[1] == s then
                e = e[2]
                if type(e) == "table" then
                  local q = type(adapt)
                  if q == "function" then
                    s = adapt(e, s)
                    t = false
                  elseif q == "string" then
                    t = mw.text.split(adapt, "%s+")
                  elseif q == "table" then
                    t = adapt
                  else
                    t = {}
                  end
                  if t then
                    table.insert(t, Fallback())
                    table.insert(t, "en")
                    for k = 1, #t do
                      q = e[t[k]]
                      if type(q) == "string" then
                        s = q
                        break -- for k
                      end
                    end -- for k
                  end
                else
                  s = "JSONutil.fault I18N bad #" .. tostring(i)
                end
                break -- for i
              end
            else
              break -- for i
            end
          end -- for i
        else
          s = "INVALID JSONutil.fault I18N corrupted"
        end
      else
        s = "INVALID JSONutil.fault commons:Data: " .. type(t)
      end
    end
  else
    s = "INVALID JSONutil.fault key: " .. tostring(s)
  end
  if type(add) == "string" then
    s = string.format("%s &#8211; %s", s, add)
  end
  e:wikitext(s)
  return tostring(e)
end -- JSONutil.fault()

JSONutil.fetch = function(apply, always, adapt)
  -- Retrieve JSON data or error message.
  -- Parameter:
  --     apply   -- string, with presumable JSON text
  --     always  -- true, if apply is expected to need preprocessing
  --     adapt   -- function|string|table|nil|false, for I18N
  -- Returns table with data or string, with error as HTML span.
  local lucky, r
  if not always then
    lucky, r = pcall(mw.text.jsonDecode, apply)
  end
  if not lucky then
    lucky, r = JSONutil.fair(apply)
    if lucky then
      r = JSONutil.fault(lucky, r, adapt)
    else
      lucky, r = pcall(mw.text.jsonDecode, r)
      if not lucky then
        r = JSONutil.fault(r, false, adapt)
      end
    end
  end
  return r
end -- JSONutil.fetch()

Failsafe.failsafe = function(atleast)
  -- Retrieve versioning and check for compliance.
  -- Precondition:
  --     atleast  -- string, with required version
  --                         or "wikidata" or "~" or "@" or false
  -- Postcondition:
  --     Returns  string  -- with queried version/item, also if problem
  --              false   -- if appropriate
  -- 2020-08-17
  local since = atleast
  local last = (since == "~")
  local linked = (since == "@")
  local link = (since == "item")
  local r
  if last or link or linked or since == "wikidata" then
    local item = Failsafe.item
    since = false
    if type(item) == "number" and item > 0 then
      local suited = string.format("Q%d", item)
      if link then
        r = suited
      else
        local entity = mw.wikibase.getEntity(suited)
        if type(entity) == "table" then
          local seek = Failsafe.serialProperty or "P348"
          local vsn = entity:formatPropertyValues(seek)
          if type(vsn) == "table" and type(vsn.value) == "string" and vsn.value ~= "" then
            if last and vsn.value == Failsafe.serial then
              r = false
            elseif linked then
              if mw.title.getCurrentTitle().prefixedText == mw.wikibase.getSitelink(suited) then
                r = false
              else
                r = suited
              end
            else
              r = vsn.value
            end
          end
        end
      end
    end
  end
  if type(r) == "nil" then
    if not since or since <= Failsafe.serial then
      r = Failsafe.serial
    else
      r = false
    end
  end
  return r
end -- Failsafe.failsafe()

-- Export.
local p = {}

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.encodeArray = function(frame)
  return JSONutil.Encoder.Array(frame:getParent().args, frame.args.type, frame.args.error == "1")
end -- p.encodeArray

p.encodeComponent = function(frame)
  return JSONutil.Encoder.Component(
    frame.args.sign,
    frame.args.value,
    frame.args.type,
    flip(frame),
    frame.args.error == "1"
  )
end -- p.encodeComponent

p.encodeHash = function(frame)
  return JSONutil.Encoder.Hash(frame:getParent().args, frame.args)
end -- p.encodeHash

p.encodeI18N = function(frame)
  return JSONutil.Encoder.I18N(frame:getParent().args, flip(frame))
end -- p.encodeI18N

p.encodeObject = function(frame)
  return JSONutil.Encoder.object(flat(frame.args[1]), flip(frame))
end -- p.encodeObject

p.encodePolyglott = function(frame)
  return JSONutil.Encoder.Polyglott(flat(frame.args[1]), flip(frame))
end -- p.encodePolyglott

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