缺氧 wiki 编辑团队提示:注册账号并登录后体验更佳,且可通过参数设置定制优化您的浏览体验!

该站点为镜像站点,如果你想帮助这个由玩家志愿编辑的 wiki 站点,请前往原站点参与编辑,
同时欢迎加入编辑讨论群 851803695 与其他编辑者一起参与建设!

全站通知:

模块:Dev/Json

来自缺氧WIKI_BWIKI_哔哩哔哩
跳到导航 跳到搜索

此模块的文档可以在模块:Dev/Json/doc创建

--  <nowiki>
--- JSON high-performance bidirectional conversion framework.
--  This module is a fork of the [[github:LuaDist/dkjson|dkjson]] library by
--  David Kolf, removing LPeg support and registration of the `_G.json` global.
--  
--  On Wikia, it serves as a polyfill for the `mw.text.jsonEncode` and
--  `mw.text.jsonDecode` PHP interfaces available in Scribunto core. Json is
--  able to perform within one order of magnitude (20%) to its PHP counterpart,
--  while also supporting multiline comments.
--  
--  [[wikipedia:JSON|JSON]] is a data serialisation format by the IETF and JS
--  developer Douglas Crockford that maps data in objects consisting of arrays
--  (key-value pairs) and lists (ordered element collections).
--  
--  The @{json.encode} function will use two spaces for indentation when
--  it is enabled, matching the behaviour of `mw.text.jsonDecode`. If you need
--  to output JSON with four spaces per indent, use @{string.gsub} and
--  parentheses - `(json.encode({json.null}):gsub('\n( +)','\n%1%1'))`.
--  
--  The @{json.decode} function accepts optional arguments to enable the
--  bidirectional processing of any JSON data. The RFC 8259 JSON specification
--  does not support JavaScript's inline and multiline comments, but these are
--  stripped gracefully in the parser anyway along with any whitespace.
--  
--  @script         json
--  @release        stable
--  @author         [[github::dhkolf|David Kolf]] ([[github:LuaDist/dkjson|Github]])
--  @author         [[User:8nml|8nml]]
--  @version        2.5.0+wikia:dev

local json, utils = {}, {}

--  Global dependencies.
local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset =
    pairs,
    type,
    tostring,
    tonumber,
    getmetatable,
    setmetatable,
    rawset
local error, require, pcall, select = error, require, pcall, select
local floor, huge = math.floor, math.huge
local strrep, strgsub, strsub, strbyte, strchar, strfind, strlen, strformat =
    string.rep,
    string.gsub,
    string.sub,
    string.byte,
    string.char,
    string.find,
    string.len,
    string.format
local strmatch = string.match
local concat = table.concat

--  Module variables.
local escapecodes = {
    ['"'] = '\\"',
    ['\\'] = '\\\\',
    ['\b'] = '\\b',
    ['\f'] = '\\f',
    ['\n'] = '\\n',
    ['\r'] = '\\r',
    ['\t'] = '\\t'
}
local escapechars = {
    ['"'] = '"',
    ['\\'] = '\\',
    ['/'] = '/',
    ['b'] = '\b',
    ['f'] = '\f',
    ['n'] = '\n',
    ['r'] = '\r',
    ['t'] = '\t'
}
local decpoint, numfilter

--- Checks if a Lua table is an array.
--  @param              {table} tbl Table to test array rules on.
--  @return             {boolean} Whether the table is an array.
--  @local
function utils.isarray(tbl)
    local max, n, arraylen = 0, 0, 0

    for k, v in pairs(tbl) do
        if k == 'n' and type(v) == 'number' then
            arraylen = v
            if v > max then
                max = v
            end
        else
            if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
                return false
            end
            if k > max then
                max = k
            end
            n = n + 1
        end
    end

    -- An array implicitly does not have too many holes.
    if max > 10 and max > arraylen and max > n * 2 then
        return false
    end

    return true, max
end

--- Generator for escaped UTF-8 strings from Unicode characters.
--  @param          {number} uchar Unicode character to escape.
--  @return         {string} UTF-8 escaped string with backslashes.
--  @local
function utils.escapeutf8(uchar)
    local value = escapecodes[uchar]
    if value then
        return value
    end
    local a, b, c, d = strbyte(uchar, 1, 4)
    a, b, c, d = a or 0, b or 0, c or 0, d or 0
    if a <= 0x7f then
        value = a
    elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
        value = (a - 0xc0) * 0x40 + b - 0x80
    elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
        value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
    elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
        value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
    else
        return ''
    end
    if value <= 0xffff then
        return strformat('\\u%.4x', value)
    elseif value <= 0x10ffff then
        -- encode as UTF-16 surrogate pair
        value = value - 0x10000
        local highsur, lowsur = 0xD800 + floor(value / 0x400), 0xDC00 + (value % 0x400)
        return strformat('\\u%.4x\\u%.4x', highsur, lowsur)
    else
        return ''
    end
end

--- String substitution predicated on pattern lookup.
--  This function offers significant optimisation over @{string.gsub},
--  which always builds a new string in a buffer even when there is no
--  match. The original string is returned when the string doesn't
--  contain the pattern (which is often the case).
--  @function           utils.fsub
--  @param              {string} str Target string for replacement.
--  @param              {string} pattern Pattern to replace.
--  @param              {function|table|string} Replacement value or
--                      generator.
--  @local
function utils.fsub(str, pattern, repl)
    if strfind(str, pattern) then
        return strgsub(str, pattern, repl)
    else
        return str
    end
end

--- Computes the document coordinates of a character position.
--  @param              {string} str String to index character position
--                      in.
--  @param              {number} where Character position including
--                      newlines.
--  @return             {string} Line and column coordinates for
--                      `where`.
--  @local
function utils.location(str, where)
    local line, pos, linepos = 1, 1, 0
    while true do
        pos = strfind(str, '\n', pos, true)
        if pos and pos < where then
            line = line + 1
            linepos = pos
            pos = pos + 1
        else
            break
        end
    end
    return 'line ' .. line .. ', column ' .. (where - linepos)
end

--- Generates an exception for unterminated JSON values.
--  @param              {string} str Invalid JSON string.
--  @param              {string} what Type of unterminated value.
--  @param              {string} where Character 1-index position.
--  @local
function utils.unterminated(str, what, where)
    return nil, strlen (str) + 1, 'unterminated ' .. what .. ' at ' .. utils.location(str, where)
end

--- Whitespace scanner for JSON string at a specified position.
--  @param              {string} str JSON string input.
--  @param              {string} where Start character 1-index position.
--  @local
function utils.scanwhite(str, pos)
    while true do
        pos = strfind(str, '%S', pos)
        if not pos then
            return nil
        end
        local sub2 = strsub(str, pos, pos + 1)
        if sub2 == '\239\187' and strsub(str, pos + 2, pos + 2) == '\191' then
            -- UTF-8 Byte Order Mark
            pos = pos + 3
        elseif sub2 == '//' then
            pos = strfind(str, '[\n\r]', pos + 2)
            if not pos then
                return nil
            end
        elseif sub2 == '/*' then
            pos = strfind(str, '*/', pos + 2)
            if not pos then
                return nil
            end
            pos = pos + 2
        else
            return pos
        end
    end
end

--- Unicode character conversion to UTF-8 string.
--  @param              {number} value Unicode character.
--  @return             {string|nil} UTF-8 string representation.
--  @local
function utils.unichar(value)
    if value < 0 then
        return nil
    elseif value <= 0x007f then
        return strchar(value)
    elseif value <= 0x07ff then
        return strchar(0xc0 + floor(value / 0x40), 0x80 + (floor(value) % 0x40))
    elseif value <= 0xffff then
        return strchar(0xe0 + floor(value / 0x1000), 0x80 + (floor(value / 0x40) % 0x40), 0x80 + (floor(value) % 0x40))
    elseif value <= 0x10ffff then
        return strchar(
            0xf0 + floor(value / 0x40000),
            0x80 + (floor(value / 0x1000) % 0x40),
            0x80 + (floor(value / 0x40) % 0x40),
            0x80 + (floor(value) % 0x40)
        )
    else
        return nil
    end
end

--- Quoted string scanner for JSON string at a specified position.
--  @param              {string} str JSON string input.
--  @param              {string} where Start character 1-index position.
--  @local
function utils.scanstring(str, pos)
    local lastpos = pos + 1
    local buffer, n = {}, 0
    while true do
        local nextpos = strfind(str, '["\\]', lastpos)
        if not nextpos then
            utils.unterminated(str, 'string', pos)
        end
        if nextpos > lastpos then
            n = n + 1
            buffer[n] = strsub(str, lastpos, nextpos - 1)
        end
        if strsub(str, nextpos, nextpos) == '"' then
            lastpos = nextpos + 1
            break
        else
            local escchar = strsub(str, nextpos + 1, nextpos + 1)
            local value
            if escchar == 'u' then
                value = tonumber(strsub(str, nextpos + 2, nextpos + 5), 16)
                if value then
                    local value2
                    if 0xD800 <= value and value <= 0xDBff then
                        -- we have the high surrogate of UTF-16. Check if there is a
                        -- low surrogate escaped nearby to combine them.
                        if strsub(str, nextpos + 6, nextpos + 7) == '\\u' then
                            value2 = tonumber(strsub(str, nextpos + 8, nextpos + 11), 16)
                            if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then
                                value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000
                            else
                                value2 = nil -- in case it was out of range for a low surrogate
                            end
                        end
                    end
                    value = value and utils.unichar(value)
                    if value then
                        if value2 then
                            lastpos = nextpos + 12
                        else
                            lastpos = nextpos + 6
                        end
                    end
                end
            end
            if not value then
                value = escapechars[escchar] or escchar
                lastpos = nextpos + 2
            end
            n = n + 1
            buffer[n] = value
        end
    end
    if n == 1 then
        return buffer[1], lastpos
    elseif n > 1 then
        return concat(buffer), lastpos
    else
        return '', lastpos
    end
end

--- Object scanner for JSON string at a specified position.
--  @local
function utils.scantable(what, closechar, str, startpos, nullval, objectmeta, arraymeta)
    local len = strlen(str)
    local tbl, n = {}, 0
    local pos = startpos + 1
    if what == 'object' then
        setmetatable(tbl, objectmeta)
    else
        setmetatable(tbl, arraymeta)
    end
    while true do
        pos = utils.scanwhite(str, pos)
        if not pos then
            return utils.unterminated(str, what, startpos)
        end
        local char = strsub(str, pos, pos)
        if char == closechar then
            return tbl, pos + 1
        end
        local val1, err
        val1, pos, err = utils.scanvalue(str, pos, nullval, objectmeta, arraymeta)
        if err then
            return nil, pos, err
        end
        pos = utils.scanwhite(str, pos)
        if not pos then
            return utils.unterminated(str, what, startpos)
        end
        char = strsub(str, pos, pos)
        if char == ':' then
            if val1 == nil then
                return nil, pos, 'cannot use nil as table index (at ' .. utils.location(str, pos) .. ')'
            end
            pos = utils.scanwhite(str, pos + 1)
            if not pos then
                return utils.unterminated(str, what, startpos)
            end
            local val2
            val2, pos, err = utils.scanvalue(str, pos, nullval, objectmeta, arraymeta)
            if err then
                return nil, pos, err
            end
            tbl[val1] = val2
            pos = utils.scanwhite(str, pos)
            if not pos then
                return utils.unterminated(str, what, startpos)
            end
            char = strsub(str, pos, pos)
        else
            n = n + 1
            tbl[n] = val1
        end
        if char == ',' then
            pos = pos + 1
        end
    end
end

--- JSON value scanner in JSON string at a specified position.
--  @local
function utils.scanvalue(str, pos, nullval, objectmeta, arraymeta)
    pos = pos or 1
    pos = utils.scanwhite(str, pos)
    if not pos then
        return nil, strlen (str) + 1, 'no valid JSON value (reached the end)'
    end
    local char = strsub(str, pos, pos)
    if char == '{' then
        return utils.scantable('object', '}', str, pos, nullval, objectmeta, arraymeta)
    elseif char == '[' then
        return utils.scantable('array', ']', str, pos, nullval, objectmeta, arraymeta)
    elseif char == '"' then
        return utils.scanstring(str, pos)
    else
        local pstart, pend = strfind(str, '^%-?[%d%.]+[eE]?[%+%-]?%d*', pos)
        if pstart then
            local number = utils.str2num(strsub(str, pstart, pend))
            if number then
                return number, pend + 1
            end
        end
        pstart, pend = strfind(str, '^%a%w*', pos)
        if pstart then
            local name = strsub(str, pstart, pend)
            if name == 'true' then
                return true, pend + 1
            elseif name == 'false' then
                return false, pend + 1
            elseif name == 'null' then
                return nullval, pend + 1
            end
        end
        return nil, pos, 'no valid JSON value at ' .. utils.location(str, pos)
    end
end

--- Character replacement utility.
--  @param              {string} str Character sequence.
--  @param              {string} o Character to replace.
--  @param              {string} n Replacement character.
--  @return             {string} Sequence with first instance of `o` character
--                      replaced by `n` character.
--  @local
function utils.replace(str, o, n)
    local i, j = strfind(str, o, 1, true)
    if i then
        return strsub(str, 1, i - 1) .. n .. strsub(str, j + 1, -1)
    else
        return str
    end
end

-- locale independent num2str and str2num functions

--- Updates decimal point and number filter pattern.
--  @todo               Refactor out (as `os.setlocale` is not defined).
--  @local
function utils.updatedecpoint()
    decpoint = strmatch(tostring(0.5), '([^05+])')
    -- build a filter that can be used to remove group separators
    numfilter = '[^0-9%-%+eE' .. strgsub(decpoint, '[%^%$%(%)%%%.%[%]%*%+%-%?]', '%%%0') .. ']+'
end

utils.updatedecpoint()

--- Localised string conversion.
--  @local
function utils.num2str(num)
    return utils.replace(utils.fsub(tostring(num), numfilter, ''), decpoint, '.')
end

--- Localised numerical conversion.
--  @local
function utils.str2num(str)
    local num = tonumber(utils.replace(str, '.', decpoint))
    if not num then
        utils.updatedecpoint()
        num = tonumber(utils.replace(str, '.', decpoint))
    end
    return num
end

--- Inserts newlines into the encoding buffer of the encoder state.
--  @param              {number} level Indentation level.
--  @param              {table} buffer Serialisation stack buffer as an array.
--  @param              {number} buflen Length of encoding buffer.
--  @return             {number} New buffer length after buffer insertion.
--  @local
function utils._addnewline(level, buffer, buflen)
    buffer[buflen + 1] = '\n'
    buffer[buflen + 2] = strrep('  ', level)
    buflen = buflen + 2
    return buflen
end

--- Encoding buffer insertion in iterator form.
--  @local
function utils.addpair(key, value, prev, indent, level, buffer, buflen, tables, globalorder, state)
    local kt = type(key)
    if kt ~= 'string' and kt ~= 'number' then
        return nil, 'type "' .. kt .. '" is not supported as a key by JSON'
    end
    if prev then
        buflen = buflen + 1
        buffer[buflen] = ','
    end
    if indent then
        buflen = utils._addnewline(level, buffer, buflen)
    end
    buffer[buflen + 1] = json.quote(key)
    buffer[buflen + 2] = ':'
    return utils._encode(value, indent, level, buffer, buflen + 2, tables, globalorder, state)
end

--- Custom append function for encoding buffer.
--  @param              {string} res Item to append to buffer.
--  @param              {table} buffer Serialisation stack buffer as an array.
--  @param[opt]         {table} state Serialiser state/option configuration.
--  @local
function utils.appendcustom(res, buffer, state)
    local buflen = state.bufferlen
    if type(res) == 'string' then
        buflen = buflen + 1
        buffer[buflen] = res
    end
    return buflen
end

--- Generates exceptions with custom exception handler support.
--  @local
function utils.exception(reason, value, state, buffer, buflen, defaultmessage)
    defaultmessage = defaultmessage or reason
    local handler = state.exception
    if not handler then
        return nil, defaultmessage
    else
        state.bufferlen = buflen
        local ret, msg = handler(reason, value, state, defaultmessage)
        if not ret then
            return nil, msg or defaultmessage
        end
        return utils.appendcustom(ret, buffer, state)
    end
end

--- Private JSON encoding utility.
--  @local
function utils._encode(value, indent, level, buffer, buflen, tables, globalorder, state)
    local valtype = type(value)
    local valmeta = getmetatable(value)
    valmeta = type(valmeta) == 'table' and valmeta -- only tables
    local valtojson = valmeta and valmeta.__tojson

    if valtojson then
        if tables[value] then
            return utils.exception('reference cycle', value, state, buffer, buflen)
        end
        tables[value] = true
        state.bufferlen = buflen
        local ret, msg = valtojson(value, state)

        if not ret then
            return utils.exception('custom encoder failed', value, state, buffer, buflen, msg)
        end

        tables[value] = nil
        buflen = utils.appendcustom(ret, buffer, state)

    elseif value == nil then
        buflen = buflen + 1
        buffer[buflen] = 'null'

    elseif valtype == 'number' then
        local s

        -- The value is NaN (n ~= n) or Inf (+/-math.huge), so return null.
        -- This is the behaviour of the original JSON implementation.
        if value ~= value or value >= huge or -value >= huge then
            
            s = 'null'

        else
            s = utils.num2str(value)
        end

        buflen = buflen + 1
        buffer[buflen] = s

    elseif valtype == 'boolean' then
        buflen = buflen + 1
        buffer[buflen] = value and 'true' or 'false'

    elseif valtype == 'string' then
        buflen = buflen + 1
        buffer[buflen] = json.quote(value)

    elseif valtype == 'table' then
        if tables[value] then
            return utils.exception('reference cycle', value, state, buffer, buflen)
        end

        tables[value] = true
        level = level + 1
        local isa, n = utils.isarray(value)

        if n == 0 and valmeta and valmeta.__jsontype == 'object' then
            isa = false
        end
        local msg

        -- The value is a JSON array.
        if isa then
            buflen = buflen + 1
            buffer[buflen] = '['
            for i = 1, n do
                buflen, msg = utils._encode(value[i], indent, level, buffer, buflen, tables, globalorder, state)

                if not buflen then
                    return nil, msg
                end

                if i < n then
                    buflen = buflen + 1
                    buffer[buflen] = ','
                end
            end
            buflen = buflen + 1
            buffer[buflen] = ']'

        -- The value is a JSON object.
        else
            local prev = false
            buflen = buflen + 1
            buffer[buflen] = '{'
            local order = valmeta and valmeta.__jsonorder or globalorder

            -- The JSON object is ordered in the encoder call.
            if order then
                local used = {}
                n = #order

                for i = 1, n do
                    local k = order[i]
                    local v = value[k]

                    if v ~= nil then
                        used[k] = true
                        buflen, msg = utils.addpair(k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
                        prev = true -- add a seperator before the next element
                    end
                end

                for k, v in pairs(value) do
                    if not used[k] then
                        buflen, msg =
                            utils.addpair(k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)

                        if not buflen then
                            return nil, msg
                        end
                        prev = true -- add a seperator before the next element
                    end
                end

            -- Otherwise, the JSON object is unordered in the encoder call.
            else
                for k, v in pairs(value) do
                    buflen, msg = utils.addpair(k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
                    if not buflen then
                        return nil, msg
                    end
                    prev = true -- add a seperator before the next element
                end
            end

            if indent then
                buflen = utils._addnewline(level - 1, buffer, buflen)
            end

            buflen = buflen + 1
            buffer[buflen] = '}'
        end

        tables[value] = nil

    else
        return utils.exception('unsupported type', value, state, buffer, buflen, 'type "' .. valtype .. '" is not supported by JSON')
    end

    return buflen
end

--- Optional metatable setter in the decoder.
--  @param              {boolean} Whether to attach metatables for `'object'` and `'array'`.
--  @local
function utils.optionalmetatables(mt)
    if mt then
        return {__jsontype = 'object'}, {__jsontype = 'array'}
    end
end

--- Version of the JSON library.
json.version = 'dkjson 2.5.0+wikia:dev'

--- JSON serialiser in pure Lua for data objects.
--  @param              {table|string|number|boolean|nil} value
--                      Lua upvalue to serialise into JSON. A table can only use
--                      strings and numbers as keys and its values have to be
--                      valid objects as well.
--  @param[opt]         {table} state Serialiser state/option configuration.
--  @param[opt]         {boolean} state.indent
--                      Whether the serialised string will be formatted with
--                      newlines and indentations. If not, the encoded JSON
--                      will be compressed to one line.
--  @param[opt]         {table} state.keyorder
--                      Array that specifies the ordering of keys in the encoded
--                      output. If an object has keys which are not in this
--                      array they are written after the sorted keys.
--  @param[opt]         {number} state.level
--                      Initial level of indentation used when `state.indent` is
--                      enabled. For each level, four spaces are added. Default:
--                      `0`.
--  @param[opt]         {number} state.bufferlen
--                      The target length of the buffer array, to validate
--                      against the true buffer length.
--  @param[opt]         {number} state.tables
--                      Internal set for reference cycles. It is written to by
--                      the table scanning utilities and has every table that is
--                      currently processed attached as a key. The set key
--                      becomes temporary when the table value is absent.
--  @param[opt]         {function} state.exception
--                      Custom exception handler, called when the encoder cannot
--                      encode a given value. See @{json.encode_exception} for a
--                      example function and the handler parameter description.
--  @error[729]         {string} Exceptions for invalid data types, reference
--                      cycles in tables or custom encoding failures thrown by
--                      any @{__tojson} metafields.
--  @return             {string|number} JSON string representation of the data
--                      object, or boolean for `state.bufferlen` validity. The
--                      Lua values for ECMAScript's `Inf` and `NaN` are encoded
--                      as `null` per the JSON specification. The tables within
--                      the object are only serialised as arrays if the
--                      following rules apply:
--                       * All table keys are numerical non-zero integers.
--                       * More than half of the array elements are non-nil
--                      values **IF** the array size is greater than 10.
function json.encode(value, state)
    state = state or {}
    local oldbuffer = state.buffer
    local buffer = oldbuffer or {}
    state.buffer = buffer

    utils.updatedecpoint()
    local ret, msg = utils._encode(
        value,
        state.indent,
        state.level or 0,
        buffer,
        state.bufferlen or 0,
        state.tables or {},
        state.keyorder,
        state
    )

    if not ret then
        error(msg, 2)

    elseif oldbuffer == buffer then
        state.bufferlen = ret
        return true

    else
        state.bufferlen = nil
        state.buffer = nil
        return concat(buffer)
    end
end

--- JSON parser in pure Lua for valid JSON strings.
--  Converts a JSON string representation of Lua data into the corresponding Lua
--  table or primitive.
--  @param              {string} str JSON string representation of value.
--  @param[opt]         {string} position Internal argument for start character
--                      position. Default: `1` - start of string.
--  @param[opt]         {string|table} null Value to use for JSON's `null` value.
--                      Accepts @{json.null} for internal bidirectionality.
--                      Default: `nil`.
--  @param[opt]         {boolean} mt Assign metatables to objects for internal
--                      bidirectionality.
--  @error[opt,760]     {string} Exception with character position upon parsing
--                      failure.
--  @return             Deserialised Lua data from JSON string.
function json.decode(str, position, null, mt)
    local objectmeta, arraymeta = utils.optionalmetatables(mt)
    local val, pos, msg = utils.scanvalue(str, position, null, objectmeta, arraymeta)

    if msg then
        error(msg)

    else
        return val
    end
end

--- JSON double quote escape generator from UTF-8 strings.
--  @param              {string} value Unquoted string representation of key.
--  @return             Double quote with Unicode backslash escapes.
--  @see                [[github:douglascrockford/JSON-js/blob/2a76286/json2.js#L168]]
function json.quote(value)
    -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
    value = utils.fsub(value, '[%z\1-\31"\\\127]', utils.escapeutf8)
    if strfind(value, '[\194\216\220\225\226\239]') then
        value = utils.fsub(value, '\194[\128-\159\173]', utils.escapeutf8)
        value = utils.fsub(value, '\216[\128-\132]', utils.escapeutf8)
        value = utils.fsub(value, '\220\143', utils.escapeutf8)
        value = utils.fsub(value, '\225\158[\180\181]', utils.escapeutf8)
        value = utils.fsub(value, '\226\128[\140-\143\168-\175]', utils.escapeutf8)
        value = utils.fsub(value, '\226\129[\160-\175]', utils.escapeutf8)
        value = utils.fsub(value, '\239\187\191', utils.escapeutf8)
        value = utils.fsub(value, '\239\191[\176-\191]', utils.escapeutf8)
    end
    return '"' .. value .. '"'
end

--- Newline insertion utility for @{__tojson} metafields.
--  When `state.indent` is set, this function add a newline to `state.buffer`
--  and adds spaces according to `state.level`.
--  @param              {table} state State tracking from @{json.encode}.
function json.add_newline(state)
    if state.indent then
        state.bufferlen = utils._addnewline(state.level or 0, state.buffer, state.bufferlen or #(state.buffer))
    end
end

--- Exception encoder for debugging malformed input data.
--  This function is passed to `state.exception` in @{json.encode}. Instead of
--  raising an error, this function encodes the error message as a string.
--  @param              {string} reason Error message normally raised.
--  @param              {string} value Original value that caused exception.
--  @param              {string} state Serialiser state/option configuration.
--  @param              {string} defaultmessage Error message normally raised.
function json.encode_exception(reason, value, state, defaultmessage)
    return json.quote('<' .. defaultmessage .. '>')
end

--- Lua representation of JSON null object.
--  This function is useful for bidirectionality in @{json.decode}.
--  @field              {function} __tojson Returns `'null'` when encoding.
json.null = setmetatable({}, {
    __tojson = function()
        return 'null'
    end
})

--- JSON serialisation metafields.
--  The @{json.encode} method supports a series of metafields used in the
--  configuration of serialisation behaviour when encoding Lua.
--  @section            value

--- Serialisation order configuration.
--  Overwrites the @{json.encode} `keyorder` option for any specific table or
--  subtable it is attached to. If a key is not present in the order array, it
--  is serialised after the listed keys.
--  @member             {string} value.__jsonorder

--- Serialisation object class name.
--  Defines which  a Lua table should be rendered as a JSON object or a JSON
--  array. Accepts a value of `"object"` or `"array"`. This value is only tested
--  for empty tables. Default: `"array"`.
--  @member             {string} value.__jsontype

--- Serialisation handler function.
--  This function can either add directly to the buffer and return true, or you
--  can return a string. On errors nil and a message should be returned.
--  @function           value.__tojson
--  @param              {table} state State tracking for JSON serialisation.
--  @param              {table} state.buffer Buffer array containing the string
--                      nodes generated by @{json.encode}.
--  @param              {table} state.bufferlen Buffer length, used for tracking.
--  @return             {string|boolean|nil} JSON representation of the Lua item
--                      as a string, or `true` when the handler inserts data
--                      directly into the buffer. If the handler is throwing an
--                      exception message, this return value is set to `nil`.
--  @return[opt]        {string} Error message describing serialisation
--                      failure, to be thrown in @{json.encode}.

return json