Module:JSON: Difference between revisions
Created page with "local export = {} local m_table = require("Module:table") local codepoint = require("Module:string utilities").codepoint local concat = table.concat local converter -- forward declaration local format = string.format local getmetatable = getmetatable local index_ipairs = m_table.indexIpairs local insert = table.insert local is_array = m_table.isArray local is_finite_real_number = require("Module:math").is_finite_real_number local is_utf8 = mw.ustring.isutf8 local pairs..." |
m 1 revision imported |
||
| (One intermediate revision by one other user not shown) | |||
| Line 1: | Line 1: | ||
local export = {} | local export = {} | ||
local m_math = require("Module:math") | |||
local m_str_utils = require("Module:string utilities") | |||
local m_table = require("Module:table") | local m_table = require("Module:table") | ||
local codepoint = | local codepoint = m_str_utils.codepoint | ||
local concat = table.concat | local concat = table.concat | ||
local converter -- forward declaration | local converter -- forward declaration | ||
local find = string.find | |||
local format = string.format | local format = string.format | ||
local | local gsub = string.gsub | ||
local insert = table.insert | local insert = table.insert | ||
local ipairs = ipairs | |||
local is_array = m_table.isArray | local is_array = m_table.isArray | ||
local is_finite_real_number = | local is_finite_real_number = m_math.is_finite_real_number | ||
local is_integer = m_math.is_integer | |||
local is_utf8 = mw.ustring.isutf8 | local is_utf8 = mw.ustring.isutf8 | ||
local match = string.match | |||
local toNFC = mw.ustring.toNFC | |||
local tonumber = tonumber | |||
local pairs = pairs | local pairs = pairs | ||
local sorted_pairs = m_table.sortedPairs | local sorted_pairs = m_table.sortedPairs | ||
local trycall = require("Module:fun/trycall") | |||
local type = type | local type = type | ||
local | |||
local function json_fromBoolean(b) | |||
return b and "true" or "false" | |||
end | |||
-- Given a finite real number x, returns a string containing its JSON | -- Given a finite real number x, returns a string containing its JSON | ||
-- representation, with enough precision that it | -- representation, with enough precision that it should round-trip correctly | ||
-- (depending on the well-behavedness of the system on the other end). | -- (depending on the well-behavedness of the system on the other end). | ||
local function json_fromNumber(x, level) | local function json_fromNumber(x, level) | ||
if is_finite_real_number(x) then | if not is_finite_real_number(x) then | ||
return format("%. | error(format("Cannot encode non-finite real number %g", x), level) | ||
end | |||
-- Give integers within the range RFC 7159 considers interoperable. | |||
if is_integer(x) and x < 0x1p53 and x > -0x1p53 then | |||
return format("%d", x) | |||
end | |||
-- Otherwise, give a (double) float with the %g specifier, which handles any | |||
-- leading/trailing 0s etc. Double floats have precision ranging from 15 to | |||
-- 17 digits, meaning rounding artefacts can appear when precision is set to | |||
-- 16 or 17 (e.g. 1.1 is converted to 1.1000000000000001). Avoid this by | |||
-- trying each in turn, returning the first one which converts back into the | |||
-- original number, which avoids implying that it has higher precision than | |||
-- it really does. | |||
for prec = 15, 17 do | |||
local poss = format(format("%%.%dg", prec), x) | |||
if prec == 17 or tonumber(poss) == x then | |||
x = poss | |||
break | |||
end | |||
end | end | ||
-- If there's an exponent, remove any + sign and leading 0s from it. | |||
if find(x, "e", nil, true) then | |||
return (gsub(x, "(e%-?)%+?0*", "%1")) | |||
end | |||
-- If it resembles an integer, convert it to scientific notation to avoid | |||
-- the other end interpreting it as one. | |||
local d, f = match(x, "^(%d)(%d-)0*$") | |||
return d and format("%s%s%se%d", d, f == "" and "" or ".", f, #x - 1) or x | |||
end | end | ||
local | local function escape_codepoint(cp) | ||
if cp < 0x10000 then | |||
[ | return format("\\u%04X", cp) | ||
end | |||
cp = cp - 0x10000 | |||
return format("\\u%04X\\u%04X", 0xD800 + (cp / 1024), 0xDC00 + (cp % 1024)) | |||
end | |||
[ | |||
local escapes | |||
local function get_escapes() | |||
escapes, get_escapes = { | |||
[0x8] = [[\b]], [0x9] = [[\t]], [0xA] = [[\n]], [0xC] = [[\f]], | |||
[0xD] = [[\r]], [0x22] = [[\"]], [0x2F] = [[\/]], [0x5C] = [[\\]], | |||
}, nil | |||
local function _add(cp) | |||
if escapes[cp] == nil then | |||
escapes[cp] = escape_codepoint(cp) | |||
end | |||
end | |||
local function add(cp1, cp2) | |||
if cp2 == nil then | |||
return _add(cp1) | |||
end | |||
for cp = cp1, cp2 do | |||
_add(cp) | |||
end | |||
end | |||
add(0x0000, 0x001F) | |||
add(0x007F, 0x00A0) | |||
add(0x00AD) | |||
add(0x034F) | |||
add(0x0600, 0x0605) | |||
add(0x061C) | |||
add(0x06DD) | |||
add(0x070F) | |||
add(0x0890, 0x0891) | |||
add(0x08E2) | |||
add(0x115F, 0x1160) | |||
add(0x1680) | |||
add(0x17B4, 0x17B5) | |||
add(0x180B, 0x180F) | |||
add(0x2000, 0x200F) | |||
add(0x2028, 0x202F) | |||
add(0x205F, 0x206F) | |||
add(0x3000) | |||
add(0x3164) | |||
add(0xFDD0, 0xFDEF) | |||
add(0xFE00, 0xFE0F) | |||
add(0xFEFF) | |||
add(0xFFA0) | |||
add(0xFFF0, 0xFFFF) | |||
add(0x110BD) | |||
add(0x110CD) | |||
add(0x1107F) | |||
add(0x13430, 0x1343F) | |||
add(0x16FE4) | |||
add(0x1BC9D) | |||
add(0x1BCA0, 0x1BCA3) | |||
add(0x1D173, 0x1D17A) | |||
for i = 0x2, 0x11 do | |||
i = i * 0x10000 | |||
add(i - 2, i - 1) | |||
end | end | ||
return | |||
return escapes | |||
end | end | ||
local function escape_char( | local function escape_char(ch) | ||
return | local cp = codepoint(ch) | ||
return (escapes or get_escapes())[cp] or escape_codepoint(cp) | |||
end | |||
local function maybe_escape_char(ch) | |||
local cp = codepoint(ch) | |||
if cp >= 0xE0000 and cp <= 0xE0FFF then | |||
return escape_char(ch) | |||
end | |||
return (escapes or get_escapes())[cp] or ch | |||
end | end | ||
| Line 56: | Line 154: | ||
if not is_utf8(s) then | if not is_utf8(s) then | ||
error(format("Cannot encode non-UTF-8 string '%s'", s), level) | error(format("Cannot encode non-UTF-8 string '%s'", s), level) | ||
end | end | ||
return '"' .. s .. '"' | local pattern = '[%c"/\\\128-\255][\128-\191]*' | ||
if not ascii then | |||
local escaped = gsub(s, pattern, maybe_escape_char) | |||
if escaped == toNFC(escaped) then | |||
return '"' .. escaped .. '"' | |||
end | |||
end | |||
return '"' .. gsub(s, pattern, escape_char) .. '"' | |||
end | end | ||
| Line 71: | Line 168: | ||
local ret, open, close = {} | local ret, open, close = {} | ||
if is_array(t) then | if is_array(t) then | ||
for key, value in | for key, value in ipairs(t) do | ||
ret[key] = converter(value, opts, current, level + 1) or "null" | ret[key] = converter(value, opts, current, level + 1) or "null" | ||
end | end | ||
| Line 81: | Line 178: | ||
for key, value in (opts.sort_keys and sorted_pairs or pairs)(t) do | for key, value in (opts.sort_keys and sorted_pairs or pairs)(t) do | ||
local key_type = type(key) | local key_type = type(key) | ||
if key_type == "number" then | if key_type == "boolean" then | ||
key = json_fromBoolean(key) | |||
elseif key_type == "number" then | |||
key = json_fromNumber(key, level + 1) | key = json_fromNumber(key, level + 1) | ||
elseif key_type ~= "string" then | elseif key_type ~= "string" then | ||
| Line 110: | Line 209: | ||
return "null" | return "null" | ||
elseif val_type == "boolean" then | elseif val_type == "boolean" then | ||
return this | return json_fromBoolean(this) | ||
elseif val_type == "number" then | elseif val_type == "number" then | ||
return json_fromNumber(this, level + 1) | return json_fromNumber(this, level + 1) | ||
| Line 131: | Line 230: | ||
return json_fromTable(this, opts, current, level + 1) | return json_fromTable(this, opts, current, level + 1) | ||
end | end | ||
local | -- Try to call it. | ||
-- If | local success, new = trycall(to_json, this, {lua_table = true}) | ||
if success then | |||
local ret = converter( | -- If successful, use the returned value. | ||
local ret = converter(new, opts, current, level + 1) | |||
current[this] = nil | current[this] = nil | ||
return ret | return ret | ||
end | end | ||
-- | -- Otherwise, treat as a conventional value. | ||
return json_fromTable(this, opts, current, level + 1) | return json_fromTable(this, opts, current, level + 1) | ||
end | end | ||
-- This function makes an effort to convert an arbitrary Lua value to a string | -- This function makes an effort to convert an arbitrary Lua value to a string | ||
-- containing a JSON representation of it | -- containing a JSON representation of it. | ||
function export.toJSON(this, opts) | function export.toJSON(this, opts) | ||
return converter(this, opts == nil and {} or opts, {}, 3) | return converter(this, opts == nil and {} or opts, {}, 3) | ||