├── .github └── workflows │ ├── issue.yaml │ └── test.yaml ├── .gitignore ├── LICENSE ├── README.md ├── hjson-lua-0.3.0-1.rockspec ├── hjson.lua ├── hjson ├── decoder.lua ├── encoder.lua └── encoderH.lua └── test ├── assets ├── charset2_result.hjson ├── charset2_result.json ├── charset2_test.hjson ├── charset_result.hjson ├── charset_result.json ├── charset_test.hjson ├── comments_result.hjson ├── comments_result.json ├── comments_test.hjson ├── empty_result.hjson ├── empty_result.json ├── empty_test.hjson ├── extra │ ├── notabs_result.hjson │ ├── notabs_result.json │ ├── notabs_test.json │ ├── notabs_testmeta.hjson │ ├── root_result.hjson │ ├── root_result.json │ ├── root_test.hjson │ ├── root_testmeta.hjson │ ├── separator_result.hjson │ ├── separator_result.json │ ├── separator_test.json │ └── separator_testmeta.hjson ├── failCharset1_test.hjson ├── failJSON02_test.json ├── failJSON05_test.json ├── failJSON06_test.json ├── failJSON07_test.json ├── failJSON08_test.json ├── failJSON10_test.json ├── failJSON11_test.json ├── failJSON12_test.json ├── failJSON13_test.json ├── failJSON14_test.json ├── failJSON15_test.json ├── failJSON16_test.json ├── failJSON17_test.json ├── failJSON19_test.json ├── failJSON20_test.json ├── failJSON21_test.json ├── failJSON22_test.json ├── failJSON23_test.json ├── failJSON26_test.json ├── failJSON28_test.json ├── failJSON29_test.json ├── failJSON30_test.json ├── failJSON31_test.json ├── failJSON32_test.json ├── failJSON33_test.json ├── failJSON34_test.json ├── failKey1_test.hjson ├── failKey2_test.hjson ├── failKey3_test.hjson ├── failKey4_test.hjson ├── failKey5_test.hjson ├── failMLStr1_test.hjson ├── failObj1_test.hjson ├── failObj2_test.hjson ├── failObj3_test.hjson ├── failStr1a_test.hjson ├── failStr1b_test.hjson ├── failStr1c_test.hjson ├── failStr1d_test.hjson ├── failStr2a_test.hjson ├── failStr2b_test.hjson ├── failStr2c_test.hjson ├── failStr2d_test.hjson ├── failStr3a_test.hjson ├── failStr3b_test.hjson ├── failStr3c_test.hjson ├── failStr3d_test.hjson ├── failStr4a_test.hjson ├── failStr4b_test.hjson ├── failStr4c_test.hjson ├── failStr4d_test.hjson ├── failStr5a_test.hjson ├── failStr5b_test.hjson ├── failStr5c_test.hjson ├── failStr5d_test.hjson ├── failStr6a_test.hjson ├── failStr6b_test.hjson ├── failStr6c_test.hjson ├── failStr6d_test.hjson ├── failStr7a_test.hjson ├── failStr8a_test.hjson ├── kan_result.hjson ├── kan_result.json ├── kan_test.hjson ├── keys_result.hjson ├── keys_result.json ├── keys_test.hjson ├── mltabs_result.hjson ├── mltabs_result.json ├── mltabs_test.json ├── oa_result.hjson ├── oa_result.json ├── oa_test.hjson ├── pass1_result.hjson ├── pass1_result.json ├── pass1_test.json ├── pass2_result.hjson ├── pass2_result.json ├── pass2_test.json ├── pass3_result.hjson ├── pass3_result.json ├── pass3_test.json ├── pass4_result.hjson ├── pass4_result.json ├── pass4_test.json ├── passSingle_result.hjson ├── passSingle_result.json ├── passSingle_test.hjson ├── stringify │ ├── quotes_all_result.hjson │ ├── quotes_all_result.json │ ├── quotes_all_test.hjson │ ├── quotes_all_testmeta.hjson │ ├── quotes_always_result.hjson │ ├── quotes_always_result.json │ ├── quotes_always_test.hjson │ ├── quotes_always_testmeta.hjson │ ├── quotes_keys_result.hjson │ ├── quotes_keys_result.json │ ├── quotes_keys_test.hjson │ ├── quotes_keys_testmeta.hjson │ ├── quotes_strings_ml_result.hjson │ ├── quotes_strings_ml_result.json │ ├── quotes_strings_ml_test.json │ ├── quotes_strings_ml_testmeta.hjson │ ├── quotes_strings_result.hjson │ ├── quotes_strings_result.json │ ├── quotes_strings_test.hjson │ └── quotes_strings_testmeta.hjson ├── stringify1_result.hjson ├── stringify1_result.json ├── stringify1_test.hjson ├── strings2_result.hjson ├── strings2_result.json ├── strings2_test.hjson ├── strings_result.hjson ├── strings_result.json ├── strings_test.hjson ├── trail_result.hjson ├── trail_result.json └── trail_test.hjson ├── json.lua └── test.lua /.github/workflows/issue.yaml: -------------------------------------------------------------------------------- 1 | name: Issue assignment 2 | 3 | on: 4 | issues: 5 | types: [opened, edited, labeled, unlabeled] 6 | 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | steps: 13 | - uses: pozil/auto-assign-issue@v1 14 | with: 15 | assignees: cryi 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | types: [opened, reopened, synchronize] 12 | 13 | jobs: 14 | test-lua-5_4: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | submodules: recursive 20 | 21 | - name: setup eli 22 | uses: alis-is/setup-eli@v1 23 | 24 | - name: test hjson 25 | run: | 26 | eli ./test/test.lua -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 V (alis.is) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hjson-lua 2 | A lightweight H/JSON library for Lua 3 | 4 | Ported from [hjson-py](https://github.com/hjson/hjson-py). Inspired by rxi - [json.lua](https://github.com/rxi/json.lua). 5 | 6 | * Implemented in pure Lua: tested with Lua 5.3 and 5.4 7 | 8 | ## Setup 9 | 10 | ### Lua Rocks 11 | 12 | ```sh 13 | luarocks install hjson-lua 14 | ``` 15 | ### Manual 16 | 17 | 1. drop [hjson.lua](tree/master/hjson.lua) and [hjson folder](tree/master/hjson) folder into your project 18 | 2. require hjson.lua 19 | * `hjson = require "hjson"` 20 | 21 | ## Usage 22 | 23 | Library exports json.lua like and JS like api. 24 | 25 | - Lua object to HJSON - returns HJSON string 26 | - `encode(obj, options)` 27 | - `stringify(obj, options)` 28 | - Parameters: 29 | - `obj` - Lua object - `table`, `string`, `number`, `nil`, `boolean` 30 | - `options` table with following values: 31 | - `indent` - default `" "`. Accepts string of whitespace characters or a number representing number of spaces (non indented HJSON is JSON, automatically forwards to `_to_json` version) 32 | - `skip_keys` - default `true` Skips invalid keys. If false throws error on invalid key. 33 | - Valid key types: `boolean`, `nil`, `string` 34 | - `sort_keys` - whether to sort keys in objects 35 | - `item_sort_key` - sort function which is passed to `table.sort` sorting object keys 36 | - `invalid_objects_as_type` if true functions and others objects are replaced with their type name in format `__lua_` e.g. `__lua_function` 37 | - Lua object to JSON - returns JSON string 38 | - `encode(obj, options)` 39 | - `stringify(obj, options)` 40 | - Parameters: 41 | - `obj` - Lua object - `table`, `string`, `number`, `nil`, `boolean` 42 | - `options` table with following values: 43 | - `indent` - default `" "`. Accepts string of whitespace characters or a number representing number of spaces (non indented HJSON is JSON, automatically forwards to `_to_json` version) 44 | - `skip_keys` - default `true` Skips invalid keys. If false throws error on invalid key. 45 | - Valid key types: `boolean`, `nil`, `string` 46 | - `sort_keys` - whether to sort keys in objects 47 | - `item_sort_key` - sort function which is passed to `table.sort` sorting object keys 48 | - `invalid_objects_as_type` if true functions and others objects are replaced with their type name in format `__lua_` e.g. `__lua_function` 49 | - H/JSON to Lua object - returns Lua object 50 | - `decode(str, strict, object_hook, object_pairs_hook)` 51 | - `parse(str, strict, object_hook, object_pairs_hook)` 52 | - Parameters: 53 | - `str` has to be valid HJSON string 54 | - `strict` default `true` . If true parse/decode fails on invalid control characters. 55 | - `object_hook` - `function(obj)` hook which allows to adjust tables generated from JSON on per JSON object basis (including nested objects). `obj` is lua `table`. 56 | - `object_pairs_hook` - `function(pairs)` hook which allows to adjust table before generation. `pairs` is table (in array form) composited from `key/value` pairs. It is called before the table for `object_hook` is generated. 57 | 58 | *`null` values contained within an array or object are converted to `nil` and are therefore lost upon decoding.* 59 | 60 | ## License 61 | This library is free software; you can redistribute it and/or modify it under 62 | the terms of the MIT license. See [LICENSE](LICENSE) for details. -------------------------------------------------------------------------------- /hjson-lua-0.3.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "hjson-lua" 2 | version = "0.3.0-1" 3 | source = { 4 | url = "git://github.com/hjson/hjson-lua.git", 5 | } 6 | description = { 7 | summary = "A lightweight H/JSON library for Lua.", 8 | homepage = "https://github.com/hjson/hjson-lua", 9 | license = "MIT" 10 | } 11 | dependencies = { 12 | "lua >= 5.3" 13 | } 14 | build = { 15 | type = "builtin", 16 | modules = { 17 | ["hjson"] = "hjson.lua", 18 | ["hjson.decoder"] = "hjson/decoder.lua", 19 | ["hjson.encoder"] = "hjson/encoder.lua", 20 | ["hjson.encoderH"] = "hjson/encoderH.lua" 21 | } 22 | } -------------------------------------------------------------------------------- /hjson.lua: -------------------------------------------------------------------------------- 1 | -- MIT License - Copyright (c) 2025 V (alis.is) 2 | local decoder = require "hjson.decoder" 3 | local encoder = require "hjson.encoder" 4 | local encoderH = require "hjson.encoderH" 5 | 6 | ---@class HJsonKeyValuePair 7 | ---@field key any 8 | ---@field value any 9 | 10 | ---#DES 'hjson.decode' 11 | --- 12 | ---decodes h/json 13 | ---@param str string 14 | ---@param options HjsonDecoderOptions 15 | ---@return any result 16 | ---@return string? error 17 | ---@return boolean success 18 | local function decode(str, options) 19 | return decoder:new(options):decode(str) 20 | end 21 | 22 | ---@class HJsonEncodeOptions 23 | ---@field indent string|boolean|integer|nil 24 | ---@field skip_keys boolean? skip invalid keys 25 | ---@field sort_keys boolean? 26 | ---@field item_sort_key (fun(k1:any, k2:any): boolean)? 27 | ---@field invalid_objects_as_type boolean? 28 | 29 | ---@param options HJsonEncodeOptions? 30 | ---@return HJsonEncodeOptions 31 | local function preprocess_encode_options(options) 32 | if type(options) ~= "table" then 33 | local result = {} --[[@as HJsonEncodeOptions]] 34 | return result 35 | end 36 | 37 | return options 38 | end 39 | 40 | ---#DES 'hjson.encode_to_json' 41 | --- 42 | ---encodes json 43 | ---@param obj any 44 | ---@param options HJsonEncodeOptions? 45 | ---@return string? result 46 | ---@return string? error 47 | local function encode_json(obj, options) 48 | options = preprocess_encode_options(options) 49 | 50 | return encoder:new(options):encode(obj) 51 | end 52 | 53 | ---#DES 'hjson.encode' 54 | --- 55 | ---encodes hjson 56 | ---@param obj any 57 | ---@param options HJsonEncodeOptions? 58 | ---@return string? result 59 | ---@return string? error 60 | local function encode(obj, options) 61 | options = preprocess_encode_options(options) --[[@as HJsonEncodeOptions]] 62 | 63 | if options.indent == "" or options.indent == false or options.indent == 0 then 64 | return encode_json(obj, options) 65 | end 66 | 67 | return encoderH:new(options):encode(obj) 68 | end 69 | 70 | local hjson = { 71 | encode = encode, 72 | ---#DES 'hjson.stringify' 73 | --- 74 | ---encodes hjson 75 | ---@param obj any 76 | ---@param options HJsonEncodeOptions? 77 | ---@return string? result 78 | ---@return string? error 79 | stringify = encode, 80 | encode_to_json = encode_json, 81 | ---#DES 'hjson.stringify_to_json' 82 | --- 83 | ---encodes json 84 | ---@param obj any 85 | ---@param options HJsonEncodeOptions? 86 | ---@return string? result 87 | ---@return string? error 88 | stringify_to_json = encode_json, 89 | decode = decode, 90 | ---#DES 'hjson.parse' 91 | --- 92 | ---decodes h/json 93 | ---@param str string 94 | ---@param options HjsonDecoderOptions 95 | ---@return any result 96 | ---@return string? error 97 | ---@return boolean success 98 | parse = decode, 99 | } 100 | 101 | return hjson 102 | -------------------------------------------------------------------------------- /hjson/decoder.lua: -------------------------------------------------------------------------------- 1 | -- MIT License - Copyright (c) 2025 V (alis.is) 2 | local DEFAULT_MAX_DEPTH = 1000 3 | 4 | local WHITESPACE = " \t\n\r" 5 | local PUNCTUATOR = "{}[],:" 6 | local BACKSLASH = { 7 | ['"'] = string.char(34), 8 | ["'"] = string.char(39), 9 | ["\\"] = string.char(92), 10 | ["/"] = string.char(47), 11 | ["b"] = string.char(8), 12 | ["f"] = string.char(12), 13 | ["n"] = string.char(10), 14 | ["r"] = string.char(13), 15 | ["t"] = string.char(9) 16 | } 17 | 18 | local function trim(s) 19 | local n = s:find "%S" 20 | return n and s:match(".*%S", n) or "" 21 | end 22 | 23 | local function charAt(s, pos) 24 | assert(type(s) == "string", "Invalid argument") 25 | return s:sub(pos, pos) 26 | end 27 | 28 | local function decode_error(str, idx, msg) 29 | local line_count = 1 30 | local col_count = 1 31 | for i = 1, idx - 1 do 32 | col_count = col_count + 1 33 | if charAt(str, i) == "\n" then 34 | line_count = line_count + 1 35 | col_count = 1 36 | end 37 | end 38 | error(string.format("%s at line %d col %d", msg, line_count, col_count)) 39 | end 40 | 41 | ---@class HjsonDecoderOptions 42 | ---@field strict boolean? 43 | ---@field object_hook (fun(obj: table): table)? 44 | ---@field object_pairs_hook (fun(pairs: HJsonKeyValuePair[]): HJsonKeyValuePair[])? 45 | ---@field max_depth number? 46 | 47 | ---@class HjsonDecoder 48 | ---@field decode fun(self: HjsonDecoder, s: string): any, string?, boolean 49 | 50 | local HjsonDecoder = {} 51 | --- Hjson decoder 52 | --- Performs the following translations in decoding by default: 53 | --- ** NOTE: nil is used in lua for reference removal and so arrays with null wont contain nil in lua representation. 54 | --- Same objects wont contain keys with nil value. 55 | --- +---------------+-------------------+ 56 | --- | JSON | Lua | 57 | --- +===============+===================+ 58 | --- | object | table | 59 | --- +---------------+-------------------+ 60 | --- | array | table | 61 | --- +---------------+-------------------+ 62 | --- | string | string | 63 | --- +---------------+-------------------+ 64 | --- | number | number | 65 | --- +---------------+-------------------+ 66 | --- | true | true | 67 | --- +---------------+-------------------+ 68 | --- | false | false | 69 | --- +---------------+-------------------+ 70 | --- | null | nil | 71 | --- +---------------+-------------------+ 72 | 73 | --- Creates a new HjsonDecoder instance 74 | ---@param options HjsonDecoderOptions 75 | ---@return HjsonDecoder 76 | function HjsonDecoder:new(options) 77 | if type(options) ~= "table" then 78 | options = { 79 | strict = type(options) == "boolean" and options or true, -- for backward compatibility 80 | } 81 | end 82 | 83 | if type(options.strict) ~= "boolean" then 84 | options.strict = true 85 | end 86 | if type(options.max_depth) ~= "number" or options.max_depth < 1 then 87 | options.max_depth = DEFAULT_MAX_DEPTH 88 | end 89 | 90 | local memo = {} 91 | 92 | local function getEol(s, _end) 93 | -- skip until eol 94 | while true do 95 | local ch = charAt(s, _end) 96 | if ch == "\r" or ch == "\n" or ch == "" then 97 | return _end 98 | end 99 | _end = _end + 1 100 | end 101 | end 102 | 103 | local function getNext(s, _end) 104 | local ch 105 | while true do 106 | -- Use a slice to prevent IndexError from being raised 107 | ch = charAt(s, _end) 108 | -- Skip whitespace 109 | while WHITESPACE:find(ch, 1, true) do 110 | if ch == "" then 111 | return ch, _end 112 | end 113 | _end = _end + 1 114 | ch = charAt(s, _end) 115 | end 116 | 117 | -- Hjson allows comments 118 | local ch2 = charAt(s, _end + 1) 119 | if ch == "#" or ch == "/" and ch2 == "/" then 120 | _end = getEol(s, _end) 121 | elseif ch == "/" and ch2 == "*" then 122 | _end = _end + 2 123 | ch = charAt(s, _end) 124 | 125 | while ch ~= "" and not (ch == "*" and charAt(s, _end + 1) == "/") do 126 | _end = _end + 1 127 | ch = charAt(s, _end) 128 | end 129 | 130 | if ch ~= "" then 131 | _end = _end + 2 132 | end 133 | else 134 | break 135 | end 136 | end 137 | return ch, _end 138 | end 139 | 140 | local function skipIndent(s, _end, n) 141 | local ch = charAt(s, _end) 142 | local IDENTCHARS = " \t\r" 143 | while ch ~= "" and IDENTCHARS:find(ch, 1, true) and (n > 0 or n < 0) do 144 | _end = _end + 1 145 | n = n - 1 146 | ch = charAt(s, _end) 147 | end 148 | return _end 149 | end 150 | 151 | local function codepointToUtf8(n, s, _end) 152 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 153 | local f = math.floor 154 | if n <= 0x7f then 155 | return string.char(n) 156 | elseif n <= 0x7ff then 157 | return string.char(f(n / 64) + 192, n % 64 + 128) 158 | elseif n <= 0xffff then 159 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 160 | elseif n <= 0x10ffff then 161 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, f(n % 4096 / 64) + 128, n % 64 + 128) 162 | end 163 | decode_error(s, _end, string.format("invalid unicode codepoint '%x'", n)) 164 | end 165 | 166 | local function parseString(s, _end) 167 | --[[ 168 | Scan the string s for a JSON string. End is the index of the 169 | character in s after the quote that started the JSON string. 170 | ]] 171 | local chunks = "" 172 | local begin = _end - 1 173 | 174 | -- callers make sure that string starts with " or ' 175 | local exitCh = charAt(s, begin) 176 | local function scan_string() 177 | local content, terminator = s:match('(.-)([\'"\\%z\001-\031])', _end) 178 | if not content then 179 | decode_error(s, begin, "Unterminated string") 180 | end 181 | 182 | _end = _end + #content + #terminator 183 | chunks = chunks .. content 184 | 185 | if terminator == exitCh then 186 | return true -- break 187 | elseif terminator == '"' or terminator == "'" then 188 | chunks = chunks .. terminator 189 | return -- continue 190 | elseif terminator ~= "\\" then 191 | if options.strict then 192 | decode_error(s, begin, "Invalid control character " .. terminator) 193 | else 194 | chunks = chunks .. terminator 195 | return -- continue 196 | end 197 | end 198 | 199 | if #s < _end then 200 | decode_error(s, _end, "Unterminated string") 201 | end 202 | 203 | local chars 204 | local esc = charAt(s, _end) 205 | if esc ~= "u" then 206 | if not esc or not BACKSLASH[esc] then 207 | decode_error(s, _end, "Invalid \\X escape sequence") 208 | end 209 | chars = BACKSLASH[esc] 210 | _end = _end + 1 211 | else 212 | -- Unicode escape sequence 213 | local msg = "Invalid \\uXXXX escape sequence" 214 | esc = s:sub(_end + 1, _end + 4) 215 | if #esc ~= 4 or not esc:find("%x%x%x%x") then 216 | decode_error(s, _end - 1, msg) 217 | end 218 | _end = _end + 5 219 | if esc:find("^[dD][89aAbB]") and s:sub(_end, _end + 1) == "\\u" then 220 | local esc2 = s:sub(_end + 2, _end + 6) 221 | local n1 = tonumber(esc, 16) 222 | local n2 = tonumber(esc2, 16) 223 | chars = codepointToUtf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000, s, _end) 224 | _end = _end + 7 225 | else 226 | local n = tonumber(esc, 16) 227 | chars = codepointToUtf8(n, s, _end - 5) 228 | end 229 | end 230 | chunks = chunks .. chars 231 | end 232 | while not scan_string() do end 233 | return chunks, _end 234 | end 235 | 236 | local function parseMultilineString(s, _end) 237 | -- Scan multiline string 238 | local string = "" 239 | local triple = 0 240 | 241 | -- we are at ''' - get indent 242 | local indent = 0 243 | while true do 244 | local ch = charAt(s, _end - indent - 1) 245 | if ch == "\n" then 246 | break 247 | end 248 | indent = indent + 1 249 | end 250 | 251 | -- skip white/to (newline) 252 | _end = skipIndent(s, _end + 3, -1) 253 | local ch = charAt(s, _end) 254 | if ch == "\n" then 255 | _end = skipIndent(s, _end + 1, indent) 256 | end 257 | 258 | -- When parsing multiline string values, we must look for ' characters 259 | local function scan_mlstring() 260 | ch = charAt(s, _end) 261 | if ch == "" then 262 | decode_error(s, _end, "Bad multiline string") 263 | end 264 | if ch == "'" then 265 | triple = triple + 1 266 | _end = _end + 1 267 | if triple == 3 then 268 | if charAt(string, -1) == "\n" then 269 | string = string:sub(1, -2) 270 | end 271 | return string, _end 272 | else 273 | return false 274 | end 275 | else 276 | while triple > 0 do 277 | string = string .. "'" 278 | triple = triple - 1 279 | end 280 | end 281 | 282 | if ch == "\n" then 283 | string = string .. ch 284 | _end = skipIndent(s, _end + 1, indent) 285 | else 286 | if ch ~= "\r" then 287 | string = string .. ch 288 | end 289 | _end = _end + 1 290 | end 291 | end 292 | while true do 293 | local s, _end = scan_mlstring() 294 | if s then 295 | return s, _end 296 | end 297 | end 298 | end 299 | 300 | local function parsePrimitive(s, _end) 301 | -- Scan s until eol. return string, True, False or nil 302 | local chf, begin = getNext(s, _end) 303 | _end = begin 304 | if PUNCTUATOR:find(chf, 1, true) then 305 | decode_error(s, _end, "Found a punctuator character when expecting a quoteless string (check your syntax)") 306 | end 307 | 308 | while true do 309 | local ch = charAt(s, _end) 310 | local isEol = ch == "\r" or ch == "\n" or ch == "" 311 | 312 | if 313 | isEol or ch == "," or ch == "}" or ch == "]" or ch == "#" or 314 | ch == "/" and (charAt(s, _end + 1) == "/" or charAt(s, _end + 1) == "*") 315 | then 316 | local m = nil 317 | local integer = nil 318 | local frac = nil 319 | local exp = nil 320 | 321 | local trimmed_range = trim(s:sub(begin, _end - 1)) 322 | if chf == "n" and trimmed_range == "null" then 323 | return nil, _end 324 | elseif chf == "t" and trimmed_range == "true" then 325 | return true, _end 326 | elseif chf == "f" and trimmed_range == "false" then 327 | return false, _end 328 | elseif chf == "-" or chf >= "0" and chf <= "9" then 329 | -- NUMBER_RE = re.compile(r'[\t ]*(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?[\t ]*') 330 | integer = s:match("^[\t ]*(-?[1-9]%d*)", begin) or s:match("^[\t ]*(-?0)", begin) 331 | if integer then 332 | frac = s:match("^(%.%d+)", begin + #integer) or "" 333 | exp = s:match("^([eE][-+]?%d+)", begin + #integer + #frac) or "" 334 | local ending = s:match("^([\t ]*)", begin + #integer + #frac + #exp) or "" 335 | m = integer .. frac .. exp .. ending 336 | end 337 | end 338 | if m and begin + #m == _end then 339 | local res = tonumber(integer .. frac .. exp) 340 | return res, _end 341 | end 342 | 343 | if isEol then 344 | return trimmed_range, _end 345 | end 346 | end 347 | _end = _end + 1 348 | end 349 | end 350 | 351 | local function scanKeyName(s, _end) 352 | local ch, _end = getNext(s, _end) 353 | if ch == '"' or ch == "'" then 354 | return parseString(s, _end + 1) 355 | end 356 | 357 | local begin = _end 358 | local space = -1 359 | while true do 360 | ch = charAt(s, _end) 361 | if ch == "" then 362 | decode_error(s, _end, "Bad key name (eof)") 363 | end 364 | if ch == ":" then 365 | if begin == _end then 366 | decode_error(s, begin, "Found ':' but no key name (for an empty key name use quotes)") 367 | elseif space >= 0 then 368 | if space ~= _end - 1 then 369 | decode_error(s, _end, "Found whitespace in your key name (use quotes to include)") 370 | end 371 | return trim(s:sub(begin, _end - 1)), _end 372 | else 373 | return s:sub(begin, _end - 1), _end 374 | end 375 | elseif WHITESPACE:find(ch, 1, true) then 376 | if space < 0 or space == _end - 1 then 377 | space = _end 378 | end 379 | elseif ch == "{" or ch == "}" or ch == "[" or ch == "]" or ch == "," then 380 | decode_error( 381 | s, 382 | begin, 383 | "Found '" .. 384 | ch .. 385 | "' where a key name was expected (check your syntax or use quotes if the key name includes {}[],: or whitespace)" 386 | ) 387 | end 388 | _end = _end + 1 389 | end 390 | end 391 | 392 | local function parse_object(state, scan_once, is_object_without_braces, depth) 393 | if is_object_without_braces == nil then 394 | is_object_without_braces = false 395 | end 396 | local s = state.s 397 | local _end = state._end 398 | 399 | local function memo_get(memo, key, default) 400 | if memo[key] == nil then 401 | memo[key] = default 402 | end 403 | 404 | return memo[key] 405 | end 406 | 407 | local function dict(t) 408 | local result = {} 409 | 410 | for _, value in pairs(t) do 411 | assert(type(value) == "table", "Cannot convert non array object to table...") 412 | for k, v in pairs(value) do 413 | result[k] = v 414 | end 415 | end 416 | return result 417 | end 418 | 419 | local pairs = {} 420 | local ch 421 | ch, _end = getNext(s, _end) 422 | 423 | -- Trivial empty object 424 | if not is_object_without_braces and ch == "}" then 425 | if type(options.object_pairs_hook) == "function" then 426 | local result = options.object_pairs_hook(pairs) 427 | return result, _end + 1 428 | end 429 | pairs = {} 430 | if type(options.object_hook) == "function" then 431 | pairs = options.object_hook(pairs) 432 | end 433 | return pairs, _end + 1 434 | end 435 | 436 | while true do 437 | local key 438 | local value 439 | key, _end = scanKeyName(s, _end) 440 | key = memo_get(memo, key, key) 441 | 442 | ch, _end = getNext(s, _end) 443 | if ch ~= ":" then 444 | decode_error(s, _end, "Expecting ':' delimiter") 445 | end 446 | 447 | ch, _end = getNext(s, _end + 1) 448 | value, _end = scan_once(s, _end, depth) 449 | table.insert(pairs, {[key] = value}) 450 | 451 | ch, _end = getNext(s, _end) 452 | if ch == "," then 453 | ch, _end = getNext(s, _end + 1) 454 | end 455 | 456 | if is_object_without_braces then 457 | if ch == "" then 458 | break 459 | end 460 | else 461 | if ch == "}" then 462 | _end = _end + 1 463 | break 464 | end 465 | end 466 | ch, _end = getNext(s, _end) 467 | end 468 | if type(options.object_pairs_hook) == "function" then 469 | local result = options.object_pairs_hook(pairs) 470 | return result, _end 471 | end 472 | 473 | local obj = dict(pairs) 474 | if type(options.object_hook) == "function" then 475 | obj = options.object_hook(obj) 476 | end 477 | return obj, _end 478 | end 479 | 480 | local function parse_array(state, scan_once, depth) 481 | local ch 482 | local s = state.s 483 | local _end = state._end 484 | local values = {} 485 | ch, _end = getNext(s, _end) 486 | 487 | if ch == "" then 488 | decode_error(s, _end, "End of input while parsing an array (did you forget a closing ']'?)") 489 | end 490 | -- Look-ahead for trivial empty array 491 | if ch == "]" then 492 | return values, _end + 1 493 | end 494 | 495 | while true do 496 | local value 497 | value, _end = scan_once(s, _end, depth) 498 | table.insert(values, value) 499 | ch, _end = getNext(s, _end) 500 | if ch == "," then 501 | ch, _end = getNext(s, _end + 1) 502 | end 503 | 504 | if ch == "]" then 505 | _end = _end + 1 506 | break 507 | end 508 | 509 | ch, _end = getNext(s, _end) 510 | end 511 | return values, _end 512 | end 513 | 514 | local function _scan_once(string, idx, depth) 515 | if type(depth) ~= "number" then 516 | depth = 0 517 | end 518 | depth = depth + 1 519 | if depth > options.max_depth then 520 | decode_error(string, idx, "Exceeded max depth") 521 | end 522 | 523 | local ch = charAt(string, idx) 524 | if not ch then 525 | decode_error(string, idx, "Expecting value") 526 | end 527 | 528 | if ch == '"' or ch == "'" then 529 | if string:sub(idx, idx + 2) == "'''" then 530 | return parseMultilineString(string, idx) 531 | else 532 | return parseString(string, idx + 1) 533 | end 534 | elseif ch == "{" then 535 | return parse_object({s = string, _end = idx + 1}, _scan_once, false, depth) 536 | elseif ch == "[" then 537 | return parse_array({s = string, _end = idx + 1}, _scan_once, depth) 538 | end 539 | return parsePrimitive(string, idx) 540 | end 541 | 542 | local function scan_once(string, idx) 543 | if idx <= 0 then 544 | decode_error(string, idx, "expecting value") 545 | end 546 | local status, result, _end = pcall(_scan_once, string, idx) 547 | memo = {} 548 | if not status then 549 | error(result) 550 | end 551 | 552 | return result, _end 553 | end 554 | 555 | local function scan_object_once(string, idx) 556 | if idx <= 0 then 557 | decode_error(string, idx, "expecting value") 558 | end 559 | local status, result, _end = pcall(parse_object, {s = string, _end = idx}, _scan_once, true) 560 | memo = {} 561 | if not status then 562 | error(result) 563 | end 564 | 565 | return result, _end 566 | end 567 | 568 | -- Finally create and return instance of decoder 569 | local hd = { 570 | get_next = getNext, 571 | scan_once = scan_once, 572 | scan_object_once = scan_object_once 573 | } 574 | setmetatable(hd, self) 575 | self.__index = self 576 | 577 | return hd 578 | end 579 | 580 | ---@param s any 581 | ---@return any, string?, boolean 582 | function HjsonDecoder:decode(s) 583 | --[[ 584 | Returns the Lua representation of ``s`` (a ``ascii`` or ``utf-8`` string 585 | instance containing a JSON document) 586 | ]] 587 | assert(type(s) == "string", "HjsonDecoder:decode expects a string as input") 588 | 589 | local status, objOrError = pcall(function() 590 | local obj, _end = self:__raw_decode(s) 591 | _, _end = self.get_next(s, _end) 592 | if _end ~= #s + 1 then 593 | decode_error(s, _end, "extra data") 594 | end 595 | return obj 596 | end) 597 | 598 | if not status then 599 | return nil, objOrError, false 600 | end 601 | 602 | return objOrError, nil, true 603 | end 604 | 605 | function HjsonDecoder:__raw_decode(s, idx) 606 | local ch 607 | if idx == nil then 608 | idx = 1 609 | end 610 | 611 | if idx <= 0 then 612 | decode_error(s, idx, "expecting value") 613 | end 614 | 615 | -- Strip UTF-8 bom 616 | if (#s > idx) then 617 | local b1, b2, b3 = s:byte(1, 3) 618 | if b1 == 0xfe and b2 == 0xff then 619 | s = s:sub(2) 620 | elseif b1 == 0xef and b2 == 0xbb and b3 == 0xbf then 621 | s = s:sub(4) 622 | end 623 | end 624 | 625 | ch, idx = self.get_next(s, idx) 626 | if idx == 1 and ch == "" then 627 | return {}, 1 628 | end 629 | 630 | if ch == "{" or ch == "[" then 631 | return self.scan_once(s, idx) 632 | else 633 | -- assume we have a root object without braces 634 | local status, result, _end = pcall(self.scan_object_once, s, idx) 635 | if status then 636 | return result, _end 637 | else 638 | local status2, result2, _end = pcall(self.scan_once, s, idx) 639 | if not status2 then 640 | error(result) 641 | end 642 | return result2, _end 643 | end 644 | end 645 | end 646 | return HjsonDecoder 647 | -------------------------------------------------------------------------------- /hjson/encoder.lua: -------------------------------------------------------------------------------- 1 | -- MIT License - Copyright (c) 2025 V (alis.is) 2 | local escape_char_map = { 3 | ["\\"] = "\\\\", 4 | ['"'] = '\\"', 5 | ["\b"] = "\\b", 6 | ["\f"] = "\\f", 7 | ["\n"] = "\\n", 8 | ["\r"] = "\\r", 9 | ["\t"] = "\\t" 10 | } 11 | local default_indent = false 12 | 13 | local function isArray(t) 14 | local i = 0 15 | for _ in pairs(t) do 16 | i = i + 1 17 | if t[i] == nil then return false end 18 | end 19 | return true 20 | end 21 | 22 | local function escapeChar(c) 23 | return escape_char_map[c] or string.format("\\u%04x", c:byte()) 24 | end 25 | 26 | local function encodeString(s) 27 | return '"' .. s:gsub('[%z\1-\31\\"]', escapeChar) .. '"' 28 | end 29 | 30 | local function encodeNil(val) return "null" end 31 | 32 | local function encodeNumber(val) 33 | -- Check for -inf and inf 34 | if val <= -math.huge or val >= math.huge then 35 | return nil, "unexpected number value '" .. tostring(val) .. "'" 36 | end 37 | return string.format("%.14g", val) 38 | end 39 | 40 | local JsonEncoder = {} 41 | 42 | function JsonEncoder:new(options) 43 | if type(options) ~= "table" then options = {} end 44 | local indent, skip_invalid_keys, sort_keys, item_sort_key, invalid_objects_as_type = 45 | options.indent, options.skip_keys, options.sort_keys, 46 | options.item_sort_key, options.invalid_objects_as_type 47 | 48 | if skip_invalid_keys == nil then skip_invalid_keys = true end 49 | local indent_type = type(indent) 50 | assert(indent_type == "string" or indent_type == "number" or indent_type == "boolean" or indent == nil, 51 | "indent has to be of type string, number or boolean, got " .. indent_type) 52 | assert(indent_type ~= "string" or indent:match("^%s*$"), 53 | "indent has to be a string consisting of whitespace characters only") 54 | 55 | if type(indent) == "number" then 56 | indent = math.floor(indent) 57 | indent = string.rep(" ", indent) 58 | end 59 | if indent == true then indent = "\t" end 60 | if not indent or indent == "" then indent = default_indent end 61 | 62 | local stack = {} 63 | local currentIndentLevel = 0 64 | 65 | ---@param key string|number|boolean|nil 66 | ---@return string?, string? 67 | local function stringifyKey(key) 68 | local _type = type(key) 69 | if _type == "boolean" or _type == "number" then 70 | return tostring(key) 71 | elseif _type == "nil" then 72 | return "null" 73 | elseif _type == "string" then 74 | return encodeString(key) 75 | end 76 | if skip_invalid_keys then return nil end 77 | return nil, "invalid key type - " .. _type .. " (" .. tostring(key) .. ")" 78 | end 79 | 80 | ---@param arr any[] 81 | ---@param encode fun(v: any): string?, string? 82 | ---@return string?, string? 83 | local function encodeArray(arr, encode) 84 | if not arr or #arr == 0 then return "[]" end 85 | if stack[arr] then 86 | return nil, "circular reference" 87 | end 88 | stack[arr] = true 89 | local separator = "," 90 | local newlineIndent = "" 91 | if indent then 92 | currentIndentLevel = currentIndentLevel + 1 93 | newlineIndent = "\n" .. string.rep(indent, currentIndentLevel) 94 | separator = separator .. newlineIndent 95 | end 96 | local buf = "[" .. newlineIndent 97 | for i, v in ipairs(arr) do 98 | local encoded, err = encode(v) 99 | if not encoded then 100 | return nil, err 101 | end 102 | buf = buf .. encoded 103 | if i ~= #arr then buf = buf .. separator end 104 | end 105 | if indent then 106 | currentIndentLevel = currentIndentLevel - 1 107 | buf = buf .. "\n" .. string.rep(indent, currentIndentLevel) 108 | end 109 | buf = buf .. "]" 110 | stack[arr] = nil 111 | return buf 112 | end 113 | 114 | local function encodeTable(tab, encode) 115 | if not tab then return "{}" end 116 | if stack[tab] then 117 | return nil, "circular reference" 118 | end 119 | stack[tab] = true 120 | local newlineIndent = "" 121 | local separator = "," 122 | local keySeparator = ":" 123 | if indent then 124 | currentIndentLevel = currentIndentLevel + 1 125 | newlineIndent = "\n" .. string.rep(indent, currentIndentLevel) 126 | separator = separator .. newlineIndent 127 | keySeparator = ": " 128 | end 129 | 130 | local keysetMap = {} -- stringified key (sk) is key for real key 131 | local keyset = {} 132 | 133 | for k in pairs(tab) do 134 | local key, err = stringifyKey(k) 135 | if key ~= nil then 136 | table.insert(keyset, key) 137 | keysetMap[key] = k 138 | elseif err ~= nil then 139 | return nil, err 140 | end 141 | end 142 | if sort_keys then 143 | if type(item_sort_key) == "function" then 144 | table.sort(keyset, item_sort_key) 145 | else 146 | table.sort(keyset, 147 | function(a, b) 148 | return a:upper() < b:upper() 149 | end) 150 | end 151 | end 152 | local buf = "{" .. newlineIndent 153 | for i, sk in ipairs(keyset) do 154 | local k = keysetMap[sk] 155 | local v = tab[k] 156 | local encoded, err = encode(v) 157 | if not encoded then 158 | return nil, err 159 | end 160 | buf = buf .. sk .. keySeparator .. encoded 161 | if i ~= #keyset then buf = buf .. separator end 162 | end 163 | if indent then 164 | currentIndentLevel = currentIndentLevel - 1 165 | buf = buf .. "\n" .. string.rep(indent, currentIndentLevel) 166 | end 167 | buf = buf .. "}" 168 | stack[tab] = nil 169 | return buf 170 | end 171 | 172 | local encodeFunctionMap = { 173 | ["nil"] = encodeNil, 174 | ["table"] = encodeTable, 175 | ["array"] = encodeArray, 176 | ["string"] = encodeString, 177 | ["number"] = encodeNumber, 178 | ["boolean"] = tostring 179 | } 180 | 181 | local function encode(o) 182 | local _type = type(o) 183 | if _type == "table" then 184 | if isArray(o) then 185 | _type = "array" 186 | else 187 | _type = "table" 188 | end 189 | end 190 | local func = encodeFunctionMap[_type] 191 | if type(func) == "function" then return func(o, encode) end 192 | if invalid_objects_as_type then 193 | return encodeFunctionMap["string"]('__lua_' .. type(o)) 194 | end 195 | return nil, "unexpected type '" .. _type .. "'" 196 | end 197 | 198 | local je = {_encode = encode} 199 | setmetatable(je, self) 200 | self.__index = self 201 | 202 | return je 203 | end 204 | 205 | function JsonEncoder:encode(o, allowNonJsonTypes) 206 | return self._encode(o, allowNonJsonTypes) 207 | end 208 | 209 | return JsonEncoder 210 | -------------------------------------------------------------------------------- /hjson/encoderH.lua: -------------------------------------------------------------------------------- 1 | -- MIT License - Copyright (c) 2025 V (alis.is) 2 | local escape_char_map = { 3 | ["\\"] = "\\\\", 4 | ['"'] = '\\"', 5 | ["\b"] = "\\b", 6 | ["\f"] = "\\f", 7 | ["\n"] = "\\n", 8 | ["\r"] = "\\r", 9 | ["\t"] = "\\t" 10 | } 11 | 12 | local default_indent = "\t" 13 | 14 | local COMMONRANGE = "\127-\159" -- // TODO: add unicode escape sequences 15 | 16 | local function containsSequences(s, sequences) 17 | for _, v in ipairs(sequences) do if s:find(v) then return true end end 18 | return false 19 | end 20 | 21 | local function needsEscape(s) 22 | return containsSequences(s, {"%z", '[\\"\001-\031' .. COMMONRANGE .. "]"}) 23 | end 24 | 25 | local function needsQuotes(s) 26 | local sequences = { 27 | "^%s", '^"', "^'", "^#", "^/%*", "^//", "^{", "^}", "^%[", "^%]", "^:", 28 | "^,", "%s$", "%z", "[\001-\031" .. COMMONRANGE .. "]" 29 | } 30 | return containsSequences(s, sequences) 31 | end 32 | 33 | local function needsEscapeML(s) 34 | local sequences = { 35 | "'''", "^[\\s]+$", "%z", "[\01-\08\011\012\014-\031" .. COMMONRANGE .. "]" 36 | } 37 | return containsSequences(s, sequences) 38 | end 39 | 40 | local function needsEscapeName(s) 41 | local sequences = {'[,{%[}%]%s:#"\']', "//", "/%*", "'''"} 42 | return containsSequences(s, sequences) or needsQuotes(s) 43 | end 44 | 45 | local function startsWithNumber(s) 46 | local integer = s:match("^[\t ]*(-?[1-9]%d*)") or 47 | s:match("^[\t ]*(-?0)") 48 | if integer then 49 | local frac = s:match("^(%.%d+)", #integer + 1) or "" 50 | local exp = s:match("^([eE][-+]?%d+)", #integer + #frac + 1) or "" 51 | local ending = s:match("^%s*$", #integer + #frac + #exp + 1) or 52 | s:match("^%s*[%[,%]}#].*$", 53 | #integer + #frac + #exp + 1) or 54 | s:match("^%s*//.*$", #integer + #frac + #exp + 1) or 55 | s:match("^%s*/%*.*$", #integer + #frac + #exp + 1) or 56 | "" 57 | local m = integer .. frac .. exp .. ending 58 | 59 | if #m == #s then return true end 60 | end 61 | return false 62 | end 63 | 64 | local function startsWithKeyword(s) 65 | local sequences = {"^true%s*$", "^false%s*$", "^null%s*$"} 66 | local startSequences = { 67 | "^true%s*[,%]}#].*$", "^false%s*[,%]}#].*$", "^null%s*[,%]}#].*$" 68 | } 69 | 70 | return containsSequences(s, sequences) or 71 | (containsSequences(s, startSequences)) 72 | end 73 | 74 | local function is_array(t) 75 | local i = 0 76 | for _ in pairs(t) do 77 | i = i + 1 78 | if t[i] == nil then return false end 79 | end 80 | return true 81 | end 82 | 83 | local function escapeChar(c) 84 | return escape_char_map[c] or string.format("\\u%04x", c:byte()) 85 | end 86 | 87 | local function encodeNil(val) return "null" end 88 | 89 | ---@return string?, string? 90 | local function encodeNumber(val) 91 | -- -- Check for -inf and inf 92 | if val <= -math.huge or val >= math.huge then 93 | return nil, "unexpected number value '" .. tostring(val) .. "'" 94 | end 95 | return string.format("%.14g", val) 96 | end 97 | 98 | local HjsonEncoder = {} 99 | 100 | function HjsonEncoder:new(options) 101 | if type(options) ~= "table" then options = {} end 102 | local indent, skip_invalid_keys, sort_keys, item_sort_key, invalid_objects_as_type = 103 | options.indent, options.skip_keys, options.sort_keys, 104 | options.item_sort_key, options.invalid_objects_as_type 105 | 106 | if skip_invalid_keys == nil then skip_invalid_keys = true end 107 | local indent_type = type(indent) 108 | assert(indent_type == "string" or indent_type == "number" or indent_type == "boolean" or indent == nil, 109 | "indent has to be of type string, number or boolean, got " .. indent_type) 110 | assert(indent_type ~= "string" or indent:match("^%s*$"), 111 | "indent has to be a string consisting of whitespace characters only") 112 | assert(type(indent) ~= "boolean" or indent == true, 113 | "if indent is a boolean, it has to be true, got " .. tostring(indent)) 114 | 115 | if type(indent) == "number" then 116 | indent = math.floor(indent) 117 | indent = string.rep(" ", indent) 118 | end 119 | if not indent or indent == true or indent == "" then indent = default_indent end 120 | 121 | local stack = {} 122 | local currentIndentLevel = 0 123 | 124 | local function encodeMultiLineString(str) 125 | if not str or #str == 0 then return "''''''" end 126 | 127 | currentIndentLevel = currentIndentLevel + 1 128 | local newlineIndent = "\n" .. string.rep(indent, currentIndentLevel) 129 | currentIndentLevel = currentIndentLevel - 1 130 | 131 | return newlineIndent .. "'''" .. newlineIndent .. 132 | str:gsub("\n", newlineIndent) .. newlineIndent .. "'''" 133 | end 134 | 135 | local function encodeString(s) 136 | local isNumber = false 137 | local first = s:sub(1, 1) 138 | if first == "-" or first >= "0" and first <= "9" then 139 | isNumber = startsWithNumber(s) 140 | end 141 | if needsQuotes(s) or isNumber or startsWithKeyword(s) then 142 | if not needsEscape(s) then 143 | return '"' .. s .. '"' 144 | elseif not needsEscapeML(s) and s:find("\n") and s:find("[^%s\\]") then 145 | return encodeMultiLineString(s) 146 | else 147 | return '"' .. s:gsub('[%z\1-\31\\"]', escapeChar) .. '"' 148 | end 149 | else 150 | return s 151 | end 152 | end 153 | 154 | ---@param key string|number|boolean|nil 155 | ---@return string?, string? 156 | local function stringifyKey(key) 157 | local _type = type(key) 158 | if _type == "boolean" or _type == "number" then 159 | return tostring(key) 160 | elseif _type == "nil" then 161 | return "null" 162 | elseif _type == "string" then 163 | if not key or #key == 0 then return '""' end 164 | -- Check if we can insert this name without quotes 165 | if needsEscapeName(key) then 166 | return '"' .. key:gsub('[%z\1-\31\\"]', escapeChar) .. '"' 167 | else 168 | -- return without quotes 169 | return key 170 | end 171 | end 172 | if skip_invalid_keys then return nil end 173 | return nil, "invalid key type - " .. _type .. " (" .. tostring(key) .. ")" 174 | end 175 | 176 | ---@param arr any[] 177 | ---@param encode fun(v: any): string?, string? 178 | ---@return string?, string? 179 | local function encodeArray(arr, encode) 180 | if not arr or #arr == 0 then return "[]" end 181 | if stack[arr] then 182 | return nil, "circular reference" 183 | end 184 | stack[arr] = true 185 | 186 | currentIndentLevel = currentIndentLevel + 1 187 | local newlineIndent = "\n" .. string.rep(indent, currentIndentLevel) 188 | local separator = newlineIndent 189 | 190 | local buf = "[" .. newlineIndent 191 | for i, v in ipairs(arr) do 192 | local encoded, err = encode(v) 193 | if not encoded then 194 | return nil, err 195 | end 196 | buf = buf .. encoded 197 | if i ~= #arr then buf = buf .. separator end 198 | end 199 | currentIndentLevel = currentIndentLevel - 1 200 | buf = buf .. "\n" .. string.rep(indent, currentIndentLevel) .. "]" 201 | stack[arr] = nil 202 | return buf 203 | end 204 | 205 | ---@param tab table 206 | ---@param encode fun(v: any): string?, string? 207 | ---@return string?, string? 208 | local function encodeTable(tab, encode) 209 | if not tab then return "{}" end 210 | if stack[tab] then 211 | return nil, "circular reference" 212 | end 213 | stack[tab] = true 214 | 215 | currentIndentLevel = currentIndentLevel + 1 216 | local newlineIndent = "\n" .. string.rep(indent, currentIndentLevel) 217 | local separator = newlineIndent 218 | local keySeparator = ": " 219 | 220 | -- stringified key (sk) is key in keysetMap pointing to original non stringified key key 221 | local keysetMap = {} 222 | local keyset = {} 223 | 224 | for k in pairs(tab) do 225 | local key, err = stringifyKey(k) 226 | if key ~= nil then 227 | table.insert(keyset, key) 228 | keysetMap[key] = k 229 | elseif err ~= nil then 230 | return nil, err 231 | end 232 | end 233 | if sort_keys then 234 | if type(item_sort_key) == "function" then 235 | table.sort(keyset, item_sort_key) 236 | else 237 | table.sort(keyset, 238 | function(a, b) 239 | return a:upper() < b:upper() 240 | end) 241 | end 242 | end 243 | local buf = "{" .. newlineIndent 244 | for i, sk in ipairs(keyset) do 245 | local k = keysetMap[sk] 246 | local v = tab[k] 247 | 248 | local key = sk 249 | local encoded, err = encode(v) 250 | if not encoded then 251 | return nil, err 252 | end 253 | buf = buf .. key .. keySeparator .. encoded 254 | if i ~= #keyset then buf = buf .. separator end 255 | end 256 | currentIndentLevel = currentIndentLevel - 1 257 | buf = buf .. "\n" .. string.rep(indent, currentIndentLevel) .. "}" 258 | 259 | stack[tab] = nil 260 | return buf 261 | end 262 | 263 | ---@type table 264 | local encodeFunctionMap = { 265 | ["nil"] = encodeNil, 266 | ["table"] = encodeTable, 267 | ["array"] = encodeArray, 268 | ["string"] = encodeString, 269 | ["number"] = encodeNumber, 270 | ["boolean"] = tostring 271 | } 272 | 273 | ---@param o any 274 | ---@return string?, string? 275 | local function _encode(o) 276 | local _type = type(o) 277 | if _type == "table" then 278 | if is_array(o) then 279 | _type = "array" 280 | else 281 | _type = "table" 282 | end 283 | end 284 | local func = encodeFunctionMap[_type] 285 | if type(func) == "function" then return func(o, _encode) end 286 | if invalid_objects_as_type then 287 | return encodeFunctionMap["string"]('__lua_' .. type(o)) 288 | end 289 | return nil, "unexpected type '" .. _type .. "'" 290 | end 291 | 292 | local je = {_encode = _encode} 293 | setmetatable(je, self) 294 | self.__index = self 295 | 296 | return je 297 | end 298 | 299 | function HjsonEncoder:encode(o) return self._encode(o) end 300 | 301 | return HjsonEncoder 302 | -------------------------------------------------------------------------------- /test/assets/charset2_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | uescape: "\u0000,\u0001,\uffff" 3 | French: ° î â ê Î Â Ê é É è à ù È À Ù ë Ë ç Ç œ « » 4 | German: ä Ä ö Ö ü Ü ß 5 | Italian: ° é ç à è ì ò ù À È Ì Ò Ù 6 | Spanish: ñ Ñ ü Ü á é í ó ú Á É Í Ó Ú º ¿ ¡ 7 | hex: ģ䕧覫췯ꯍ 8 | } -------------------------------------------------------------------------------- /test/assets/charset2_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "uescape": "\u0000,\u0001,￿", 3 | "French": "° î â ê Î Â Ê é É è à ù È À Ù ë Ë ç Ç œ « »", 4 | "German": "ä Ä ö Ö ü Ü ß", 5 | "Italian": "° é ç à è ì ò ù À È Ì Ò Ù", 6 | "Spanish": "ñ Ñ ü Ü á é í ó ú Á É Í Ó Ú º ¿ ¡", 7 | "hex": "ģ䕧覫췯ꯍ" 8 | } -------------------------------------------------------------------------------- /test/assets/charset2_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | uescape: "\u0000,\u0001,\uffff" 3 | French: ° î â ê Î Â Ê é É è à ù È À Ù ë Ë ç Ç œ « » 4 | German: ä Ä ö Ö ü Ü ß 5 | Italian: ° é ç à è ì ò ù À È Ì Ò Ù 6 | Spanish: ñ Ñ ü Ü á é í ó ú Á É Í Ó Ú º ¿ ¡ 7 | hex: "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A" 8 | } 9 | -------------------------------------------------------------------------------- /test/assets/charset_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | ql-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ 3 | js-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ 4 | ml-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ 5 | } -------------------------------------------------------------------------------- /test/assets/charset_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "ql-ascii": "! \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", 3 | "js-ascii": "! \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", 4 | "ml-ascii": "! \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" 5 | } -------------------------------------------------------------------------------- /test/assets/charset_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | ql-ascii: ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ 3 | js-ascii: "! \"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" 4 | ml-ascii: 5 | ''' 6 | ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ 7 | ''' 8 | } 9 | -------------------------------------------------------------------------------- /test/assets/comments_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | foo1: This is a string value. # part of the string 3 | foo2: This is a string value. 4 | bar1: This is a string value. // part of the string 5 | bar2: This is a string value. 6 | foobar1: This is a string value./* part of the string */ 7 | foobar2: This is a string value. 8 | rem1: "# test" 9 | rem2: "// test" 10 | rem3: "/* test */" 11 | num1: 0 12 | num2: 0 13 | num3: 2 14 | true1: true 15 | true2: true 16 | true3: true 17 | false1: false 18 | false2: false 19 | false3: false 20 | null1: null 21 | null2: null 22 | null3: null 23 | str1: 00 # part of the string 24 | str2: 00.0 // part of the string 25 | str3: 02 /* part of the string */ 26 | URL1: http://example.com 27 | URL2: http://example.com 28 | URL3: http://example.com 29 | URL4: http://example.com 30 | } -------------------------------------------------------------------------------- /test/assets/comments_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo1": "This is a string value. # part of the string", 3 | "foo2": "This is a string value.", 4 | "bar1": "This is a string value. // part of the string", 5 | "bar2": "This is a string value.", 6 | "foobar1": "This is a string value./* part of the string */", 7 | "foobar2": "This is a string value.", 8 | "rem1": "# test", 9 | "rem2": "// test", 10 | "rem3": "/* test */", 11 | "num1": 0, 12 | "num2": 0, 13 | "num3": 2, 14 | "true1": true, 15 | "true2": true, 16 | "true3": true, 17 | "false1": false, 18 | "false2": false, 19 | "false3": false, 20 | "null1": null, 21 | "null2": null, 22 | "null3": null, 23 | "str1": "00 # part of the string", 24 | "str2": "00.0 // part of the string", 25 | "str3": "02 /* part of the string */", 26 | "URL1": "http://example.com", 27 | "URL2": "http://example.com", 28 | "URL3": "http://example.com", 29 | "URL4": "http://example.com" 30 | } -------------------------------------------------------------------------------- /test/assets/comments_test.hjson: -------------------------------------------------------------------------------- 1 | // test 2 | # all 3 | // comment 4 | /* 5 | styles 6 | */ 7 | # with lf 8 | 9 | 10 | 11 | # ! 12 | 13 | { 14 | # hjson style comment 15 | foo1: This is a string value. # part of the string 16 | foo2: "This is a string value." # a comment 17 | 18 | // js style comment 19 | bar1: This is a string value. // part of the string 20 | bar2: "This is a string value." // a comment 21 | 22 | /* js block style comments */foobar1:/* more */This is a string value./* part of the string */ 23 | /* js block style comments */foobar2:/* more */"This is a string value."/* a comment */ 24 | 25 | rem1: "# test" 26 | rem2: "// test" 27 | rem3: "/* test */" 28 | 29 | num1: 0 # comment 30 | num2: 0.0 // comment 31 | num3: 2 /* comment */ 32 | 33 | true1: true # comment 34 | true2: true // comment 35 | true3: true /* comment */ 36 | 37 | false1: false # comment 38 | false2: false // comment 39 | false3: false /* comment */ 40 | 41 | null1: null # comment 42 | null2: null // comment 43 | null3: null /* comment */ 44 | 45 | str1: 00 # part of the string 46 | str2: 00.0 // part of the string 47 | str3: 02 /* part of the string */ 48 | 49 | # this causes problems with the syntax definitions for some editors: 50 | URL1: "http://example.com", 51 | "URL2": "http://example.com", 52 | URL3: http://example.com 53 | "URL4": http://example.com 54 | } 55 | -------------------------------------------------------------------------------- /test/assets/empty_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | "": empty 3 | } -------------------------------------------------------------------------------- /test/assets/empty_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "empty" 3 | } -------------------------------------------------------------------------------- /test/assets/empty_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | "": empty 3 | } 4 | -------------------------------------------------------------------------------- /test/assets/extra/notabs_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | foo: "bar\tjoe\noki\tdoki\n\t\ttwo tabs" 3 | } -------------------------------------------------------------------------------- /test/assets/extra/notabs_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar\tjoe\noki\tdoki\n\t\ttwo tabs" 3 | } -------------------------------------------------------------------------------- /test/assets/extra/notabs_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar\tjoe\noki\tdoki\n\t\ttwo tabs" 3 | } -------------------------------------------------------------------------------- /test/assets/extra/notabs_testmeta.hjson: -------------------------------------------------------------------------------- 1 | { 2 | options: { 3 | multiline: no-tabs 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/assets/extra/root_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | database: 3 | { 4 | host: 127.0.0.1 5 | port: 555 6 | } 7 | } -------------------------------------------------------------------------------- /test/assets/extra/root_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "host": "127.0.0.1", 4 | "port": 555 5 | } 6 | } -------------------------------------------------------------------------------- /test/assets/extra/root_test.hjson: -------------------------------------------------------------------------------- 1 | // a object with the root braces omitted 2 | database: 3 | { 4 | host: 127.0.0.1 5 | port: 555 6 | } 7 | -------------------------------------------------------------------------------- /test/assets/extra/root_testmeta.hjson: -------------------------------------------------------------------------------- 1 | { 2 | options: { 3 | legacyRoot: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/assets/extra/separator_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | foo: "bar", 3 | unicorn: "rainbow", 4 | cat: 1, 5 | hello: 6 | [ 7 | "world", 8 | "!" 9 | ] 10 | } -------------------------------------------------------------------------------- /test/assets/extra/separator_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "unicorn": "rainbow", 4 | "cat": 1, 5 | "hello": [ 6 | "world", 7 | "!" 8 | ] 9 | } -------------------------------------------------------------------------------- /test/assets/extra/separator_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "unicorn": "rainbow", 4 | "cat": 1, 5 | "hello": [ 6 | "world", 7 | "!" 8 | ] 9 | } -------------------------------------------------------------------------------- /test/assets/extra/separator_testmeta.hjson: -------------------------------------------------------------------------------- 1 | { 2 | options: { 3 | separator: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/assets/failCharset1_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid \u char 3 | char: "\uxxxx" 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failJSON02_test.json: -------------------------------------------------------------------------------- 1 | ["Unclosed array" -------------------------------------------------------------------------------- /test/assets/failJSON05_test.json: -------------------------------------------------------------------------------- 1 | ["double extra comma",,] -------------------------------------------------------------------------------- /test/assets/failJSON06_test.json: -------------------------------------------------------------------------------- 1 | [ , "<-- missing value"] -------------------------------------------------------------------------------- /test/assets/failJSON07_test.json: -------------------------------------------------------------------------------- 1 | ["Comma after the close"], -------------------------------------------------------------------------------- /test/assets/failJSON08_test.json: -------------------------------------------------------------------------------- 1 | ["Extra close"]] -------------------------------------------------------------------------------- /test/assets/failJSON10_test.json: -------------------------------------------------------------------------------- 1 | {"Extra value after close": true} "misplaced quoted value" -------------------------------------------------------------------------------- /test/assets/failJSON11_test.json: -------------------------------------------------------------------------------- 1 | {"Illegal expression": 1 + 2} -------------------------------------------------------------------------------- /test/assets/failJSON12_test.json: -------------------------------------------------------------------------------- 1 | {"Illegal invocation": alert()} -------------------------------------------------------------------------------- /test/assets/failJSON13_test.json: -------------------------------------------------------------------------------- 1 | {"Numbers cannot have leading zeroes": 013} -------------------------------------------------------------------------------- /test/assets/failJSON14_test.json: -------------------------------------------------------------------------------- 1 | {"Numbers cannot be hex": 0x14} -------------------------------------------------------------------------------- /test/assets/failJSON15_test.json: -------------------------------------------------------------------------------- 1 | ["Illegal backslash escape: \x15"] -------------------------------------------------------------------------------- /test/assets/failJSON16_test.json: -------------------------------------------------------------------------------- 1 | [\naked] -------------------------------------------------------------------------------- /test/assets/failJSON17_test.json: -------------------------------------------------------------------------------- 1 | ["Illegal backslash escape: \017"] -------------------------------------------------------------------------------- /test/assets/failJSON19_test.json: -------------------------------------------------------------------------------- 1 | {"Missing colon" null} -------------------------------------------------------------------------------- /test/assets/failJSON20_test.json: -------------------------------------------------------------------------------- 1 | {"Double colon":: null} -------------------------------------------------------------------------------- /test/assets/failJSON21_test.json: -------------------------------------------------------------------------------- 1 | {"Comma instead of colon", null} -------------------------------------------------------------------------------- /test/assets/failJSON22_test.json: -------------------------------------------------------------------------------- 1 | ["Colon instead of comma": false] -------------------------------------------------------------------------------- /test/assets/failJSON23_test.json: -------------------------------------------------------------------------------- 1 | ["Bad value", truth] -------------------------------------------------------------------------------- /test/assets/failJSON26_test.json: -------------------------------------------------------------------------------- 1 | ["tab\ character\ in\ string\ "] -------------------------------------------------------------------------------- /test/assets/failJSON28_test.json: -------------------------------------------------------------------------------- 1 | ["line\ 2 | break"] -------------------------------------------------------------------------------- /test/assets/failJSON29_test.json: -------------------------------------------------------------------------------- 1 | [0e] -------------------------------------------------------------------------------- /test/assets/failJSON30_test.json: -------------------------------------------------------------------------------- 1 | [0e+] -------------------------------------------------------------------------------- /test/assets/failJSON31_test.json: -------------------------------------------------------------------------------- 1 | [0e+-1] -------------------------------------------------------------------------------- /test/assets/failJSON32_test.json: -------------------------------------------------------------------------------- 1 | {"Comma instead if closing brace": true, -------------------------------------------------------------------------------- /test/assets/failJSON33_test.json: -------------------------------------------------------------------------------- 1 | ["mismatch"} -------------------------------------------------------------------------------- /test/assets/failJSON34_test.json: -------------------------------------------------------------------------------- 1 | A quoteless string is OK, 2 | but two must be contained in an array. 3 | -------------------------------------------------------------------------------- /test/assets/failKey1_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid name 3 | wrong name: 0 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failKey2_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid name 3 | {name: 0 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failKey3_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid name 3 | key,name: 0 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failKey4_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid name 3 | : 0 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failKey5_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid name 3 | '''foo''': 0 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failMLStr1_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid multiline string 3 | ml: ''' 4 | -------------------------------------------------------------------------------- /test/assets/failObj1_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid obj 3 | noDelimiter 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/assets/failObj2_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid obj 3 | noEnd 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /test/assets/failObj3_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # missing key 3 | 4 | [ 5 | test 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /test/assets/failStr1a_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: ] 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr1b_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: ]x 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr1c_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | foo 3 | # invalid quoteless string 4 | ] 5 | ] 6 | -------------------------------------------------------------------------------- /test/assets/failStr1d_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | foo 3 | # invalid quoteless string 4 | ]x 5 | ] 6 | -------------------------------------------------------------------------------- /test/assets/failStr2a_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: } 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr2b_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: }x 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr2c_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | foo 3 | # invalid quoteless string 4 | } 5 | ] 6 | -------------------------------------------------------------------------------- /test/assets/failStr2d_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | foo 3 | # invalid quoteless string 4 | }x 5 | ] 6 | -------------------------------------------------------------------------------- /test/assets/failStr3a_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: { 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr3b_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: {x 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr3c_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | foo 3 | # invalid quoteless string 4 | { 5 | ] 6 | -------------------------------------------------------------------------------- /test/assets/failStr3d_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | foo 3 | # invalid quoteless string 4 | {x 5 | ] 6 | -------------------------------------------------------------------------------- /test/assets/failStr4a_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: [ 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr4b_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: [x 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr4c_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | foo 3 | # invalid quoteless string 4 | [ 5 | ] 6 | -------------------------------------------------------------------------------- /test/assets/failStr4d_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | foo 3 | # invalid quoteless string 4 | [x 5 | ] 6 | -------------------------------------------------------------------------------- /test/assets/failStr5a_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: : 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr5b_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: :x 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr5c_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | foo 3 | # invalid quoteless string 4 | : 5 | ] 6 | -------------------------------------------------------------------------------- /test/assets/failStr5d_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | foo 3 | # invalid quoteless string 4 | :x 5 | ] 6 | -------------------------------------------------------------------------------- /test/assets/failStr6a_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: , 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr6b_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid quoteless string 3 | ql: ,x 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/failStr6c_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | # invalid quoteless string 3 | # note that if there were a preceding value the comma would 4 | # be allowed/ignored as a separator/trailing comma 5 | , 6 | ] 7 | -------------------------------------------------------------------------------- /test/assets/failStr6d_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | # invalid quoteless string 3 | # note that if there were a preceding value the comma would 4 | # be allowed/ignored as a separator/trailing comma 5 | ,x 6 | ] 7 | -------------------------------------------------------------------------------- /test/assets/failStr7a_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid string containing a newline 3 | foo : " 4 | " 5 | } 6 | -------------------------------------------------------------------------------- /test/assets/failStr8a_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # invalid ml-string 3 | foo : ""'text''' 4 | } 5 | -------------------------------------------------------------------------------- /test/assets/kan_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | numbers: 3 | [ 4 | 0 5 | 0 6 | 0 7 | 42 8 | 42.1 9 | -5 10 | -5.1 11 | 1701 12 | -1701 13 | 12.345 14 | -12.345 15 | ] 16 | native: 17 | [ 18 | true 19 | true 20 | false 21 | false 22 | null 23 | null 24 | ] 25 | strings: 26 | [ 27 | x 0 28 | .0 29 | 00 30 | 01 31 | 0 0 0 32 | 42 x 33 | 42.1 asdf 34 | 1.2.3 35 | -5 0 - 36 | -5.1 -- 37 | 17.01e2 + 38 | -17.01e2 : 39 | 12345e-3 @ 40 | -12345e-3 $ 41 | true true 42 | x true 43 | false false 44 | x false 45 | null null 46 | x null 47 | ] 48 | } -------------------------------------------------------------------------------- /test/assets/kan_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "numbers": [ 3 | 0, 4 | 0, 5 | 0, 6 | 42, 7 | 42.1, 8 | -5, 9 | -5.1, 10 | 1701, 11 | -1701, 12 | 12.345, 13 | -12.345 14 | ], 15 | "native": [ 16 | true, 17 | true, 18 | false, 19 | false, 20 | null, 21 | null 22 | ], 23 | "strings": [ 24 | "x 0", 25 | ".0", 26 | "00", 27 | "01", 28 | "0 0 0", 29 | "42 x", 30 | "42.1 asdf", 31 | "1.2.3", 32 | "-5 0 -", 33 | "-5.1 --", 34 | "17.01e2 +", 35 | "-17.01e2 :", 36 | "12345e-3 @", 37 | "-12345e-3 $", 38 | "true true", 39 | "x true", 40 | "false false", 41 | "x false", 42 | "null null", 43 | "x null" 44 | ] 45 | } -------------------------------------------------------------------------------- /test/assets/kan_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # the comma forces a whitespace check 3 | numbers: 4 | [ 5 | 0 6 | 0 , 7 | -0 8 | 42 , 9 | 42.1 , 10 | -5 11 | -5.1 12 | 17.01e2 13 | -17.01e2 14 | 12345e-3 , 15 | -12345e-3 , 16 | ] 17 | native: 18 | [ 19 | true , 20 | true 21 | false , 22 | false 23 | null , 24 | null 25 | ] 26 | strings: 27 | [ 28 | x 0 29 | .0 30 | 00 31 | 01 32 | 0 0 0 33 | 42 x 34 | 42.1 asdf 35 | 1.2.3 36 | -5 0 - 37 | -5.1 -- 38 | 17.01e2 + 39 | -17.01e2 : 40 | 12345e-3 @ 41 | -12345e-3 $ 42 | true true 43 | x true 44 | false false 45 | x false 46 | null null 47 | x null 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /test/assets/keys_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | unquoted_key: test 3 | _unquoted: test 4 | test-key: test 5 | -test: test 6 | .key: test 7 | trailing: test 8 | trailing2: test 9 | "#c1": test 10 | "foo#bar": test 11 | "//bar": test 12 | "foo//bar": test 13 | "/*foo*/": test 14 | "foo/*foo*/bar": test 15 | "/*": test 16 | "foo/*bar": test 17 | "\"": test 18 | "foo\"bar": test 19 | "'''": test 20 | "foo'''bar": test 21 | "'": test 22 | "'foo": test 23 | "foo'bar": test 24 | ":": test 25 | "foo:bar": test 26 | "{": test 27 | "foo{bar": test 28 | "}": test 29 | "foo}bar": test 30 | "[": test 31 | "foo[bar": test 32 | "]": test 33 | "foo]bar": test 34 | nl1: test 35 | nl2: test 36 | nl3: test 37 | } -------------------------------------------------------------------------------- /test/assets/keys_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "unquoted_key": "test", 3 | "_unquoted": "test", 4 | "test-key": "test", 5 | "-test": "test", 6 | ".key": "test", 7 | "trailing": "test", 8 | "trailing2": "test", 9 | "#c1": "test", 10 | "foo#bar": "test", 11 | "//bar": "test", 12 | "foo//bar": "test", 13 | "/*foo*/": "test", 14 | "foo/*foo*/bar": "test", 15 | "/*": "test", 16 | "foo/*bar": "test", 17 | "\"": "test", 18 | "foo\"bar": "test", 19 | "'''": "test", 20 | "foo'''bar": "test", 21 | "'": "test", 22 | "'foo": "test", 23 | "foo'bar": "test", 24 | ":": "test", 25 | "foo:bar": "test", 26 | "{": "test", 27 | "foo{bar": "test", 28 | "}": "test", 29 | "foo}bar": "test", 30 | "[": "test", 31 | "foo[bar": "test", 32 | "]": "test", 33 | "foo]bar": "test", 34 | "nl1": "test", 35 | "nl2": "test", 36 | "nl3": "test" 37 | } -------------------------------------------------------------------------------- /test/assets/keys_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # unquoted keys 3 | unquoted_key: test 4 | _unquoted: test 5 | test-key: test 6 | -test: test 7 | .key: test 8 | # trailing spaces in key names are ignored 9 | trailing : test 10 | trailing2 : test 11 | # comment char in key name 12 | "#c1": test 13 | "foo#bar": test 14 | "//bar": test 15 | "foo//bar": test 16 | "/*foo*/": test 17 | "foo/*foo*/bar": test 18 | "/*": test 19 | "foo/*bar": test 20 | # quotes in key name 21 | "\"": test 22 | "foo\"bar": test 23 | "'''": test 24 | "foo'''bar": test 25 | "'": test 26 | "'foo": test 27 | "foo'bar": test 28 | # control char in key name 29 | ":": test 30 | "foo:bar": test 31 | "{": test 32 | "foo{bar": test 33 | "}": test 34 | "foo}bar": test 35 | "[": test 36 | "foo[bar": test 37 | "]": test 38 | "foo]bar": test 39 | # newline 40 | nl1: 41 | test 42 | nl2 43 | : 44 | test 45 | 46 | nl3 47 | 48 | : 49 | 50 | test 51 | } 52 | -------------------------------------------------------------------------------- /test/assets/mltabs_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | foo: 3 | ''' 4 | bar joe 5 | oki doki 6 | two tabs 7 | ''' 8 | } -------------------------------------------------------------------------------- /test/assets/mltabs_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar\tjoe\noki\tdoki\n\t\ttwo tabs" 3 | } -------------------------------------------------------------------------------- /test/assets/mltabs_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar\tjoe\noki\tdoki\n\t\ttwo tabs" 3 | } 4 | -------------------------------------------------------------------------------- /test/assets/oa_result.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | a 3 | {} 4 | {} 5 | [] 6 | [] 7 | { 8 | b: 1 9 | c: [] 10 | d: {} 11 | } 12 | [] 13 | ] -------------------------------------------------------------------------------- /test/assets/oa_result.json: -------------------------------------------------------------------------------- 1 | [ 2 | "a", 3 | {}, 4 | {}, 5 | [], 6 | [], 7 | { 8 | "b": 1, 9 | "c": [], 10 | "d": {} 11 | }, 12 | [] 13 | ] -------------------------------------------------------------------------------- /test/assets/oa_test.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | a 3 | {} 4 | {} 5 | [] 6 | [] 7 | { 8 | b: 1 9 | c: [] 10 | d: {} 11 | } 12 | [] 13 | ] 14 | -------------------------------------------------------------------------------- /test/assets/pass1_result.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | JSON Test Pattern pass1 3 | { 4 | "object with 1 member": 5 | [ 6 | array with 1 element 7 | ] 8 | } 9 | {} 10 | [] 11 | -42 12 | true 13 | false 14 | null 15 | { 16 | integer: 1234567890 17 | real: -9876.54321 18 | e: 1.23456789e-13 19 | E: 1.23456789e+34 20 | -: 2.3456789012e+76 21 | zero: 0 22 | one: 1 23 | space: " " 24 | quote: '''"''' 25 | backslash: \ 26 | controls: "\b\f\n\r\t" 27 | slash: / & / 28 | alpha: abcdefghijklmnopqrstuvwyz 29 | ALPHA: ABCDEFGHIJKLMNOPQRSTUVWYZ 30 | digit: 0123456789 31 | 0123456789: digit 32 | special: `1~!@#$%^&*()_+-={':[,]}|;.? 33 | hex: ģ䕧覫췯ꯍ 34 | true: true 35 | false: false 36 | null: null 37 | array: [] 38 | object: {} 39 | address: 50 St. James Street 40 | url: http://www.JSON.org/ 41 | comment: "// /* */": " " 43 | " s p a c e d ": 44 | [ 45 | 1 46 | 2 47 | 3 48 | 4 49 | 5 50 | 6 51 | 7 52 | ] 53 | compact: 54 | [ 55 | 1 56 | 2 57 | 3 58 | 4 59 | 5 60 | 6 61 | 7 62 | ] 63 | jsontext: '''{"object with 1 member":["array with 1 element"]}''' 64 | quotes: " " %22 0x22 034 " 65 | "/\\\"쫾몾ꮘﳞ볚\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": A key can be any string 66 | } 67 | 0.5 68 | 98.6 69 | 99.44 70 | 1066 71 | 10 72 | 1 73 | 0.1 74 | 1 75 | 2 76 | 2 77 | rosebud 78 | ] -------------------------------------------------------------------------------- /test/assets/pass1_result.json: -------------------------------------------------------------------------------- 1 | [ 2 | "JSON Test Pattern pass1", 3 | { 4 | "object with 1 member": [ 5 | "array with 1 element" 6 | ] 7 | }, 8 | {}, 9 | [], 10 | -42, 11 | true, 12 | false, 13 | null, 14 | { 15 | "integer": 1234567890, 16 | "real": -9876.54321, 17 | "e": 1.23456789e-13, 18 | "E": 1.23456789e+34, 19 | "-": 2.3456789012e+76, 20 | "zero": 0, 21 | "one": 1, 22 | "space": " ", 23 | "quote": "\"", 24 | "backslash": "\\", 25 | "controls": "\b\f\n\r\t", 26 | "slash": "/ & /", 27 | "alpha": "abcdefghijklmnopqrstuvwyz", 28 | "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ", 29 | "digit": "0123456789", 30 | "0123456789": "digit", 31 | "special": "`1~!@#$%^&*()_+-={':[,]}|;.?", 32 | "hex": "ģ䕧覫췯ꯍ", 33 | "true": true, 34 | "false": false, 35 | "null": null, 36 | "array": [], 37 | "object": {}, 38 | "address": "50 St. James Street", 39 | "url": "http://www.JSON.org/", 40 | "comment": "// /* */": " ", 42 | " s p a c e d ": [ 43 | 1, 44 | 2, 45 | 3, 46 | 4, 47 | 5, 48 | 6, 49 | 7 50 | ], 51 | "compact": [ 52 | 1, 53 | 2, 54 | 3, 55 | 4, 56 | 5, 57 | 6, 58 | 7 59 | ], 60 | "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}", 61 | "quotes": "" \" %22 0x22 034 "", 62 | "/\\\"쫾몾ꮘﳞ볚\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string" 63 | }, 64 | 0.5, 65 | 98.6, 66 | 99.44, 67 | 1066, 68 | 10, 69 | 1, 70 | 0.1, 71 | 1, 72 | 2, 73 | 2, 74 | "rosebud" 75 | ] -------------------------------------------------------------------------------- /test/assets/pass1_test.json: -------------------------------------------------------------------------------- 1 | [ 2 | "JSON Test Pattern pass1", 3 | {"object with 1 member":["array with 1 element"]}, 4 | {}, 5 | [], 6 | -42, 7 | true, 8 | false, 9 | null, 10 | { 11 | "integer": 1234567890, 12 | "real": -9876.543210, 13 | "e": 0.123456789e-12, 14 | "E": 1.234567890E+34, 15 | "-": 23456789012E66, 16 | "zero": 0, 17 | "one": 1, 18 | "space": " ", 19 | "quote": "\"", 20 | "backslash": "\\", 21 | "controls": "\b\f\n\r\t", 22 | "slash": "/ & \/", 23 | "alpha": "abcdefghijklmnopqrstuvwyz", 24 | "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ", 25 | "digit": "0123456789", 26 | "0123456789": "digit", 27 | "special": "`1~!@#$%^&*()_+-={':[,]}|;.?", 28 | "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A", 29 | "true": true, 30 | "false": false, 31 | "null": null, 32 | "array":[ ], 33 | "object":{ }, 34 | "address": "50 St. James Street", 35 | "url": "http://www.JSON.org/", 36 | "comment": "// /* */": " ", 38 | " s p a c e d " :[1,2 , 3 39 | 40 | , 41 | 42 | 4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7], 43 | "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}", 44 | "quotes": "" \u0022 %22 0x22 034 "", 45 | "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?" 46 | : "A key can be any string" 47 | }, 48 | 0.5 ,98.6 49 | , 50 | 99.44 51 | , 52 | 53 | 1066, 54 | 1e1, 55 | 0.1e1, 56 | 1e-1, 57 | 1e00,2e+00,2e-00 58 | ,"rosebud"] -------------------------------------------------------------------------------- /test/assets/pass2_result.hjson: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [ 4 | [ 5 | [ 6 | [ 7 | [ 8 | [ 9 | [ 10 | [ 11 | [ 12 | [ 13 | [ 14 | [ 15 | [ 16 | [ 17 | [ 18 | [ 19 | [ 20 | Not too deep 21 | ] 22 | ] 23 | ] 24 | ] 25 | ] 26 | ] 27 | ] 28 | ] 29 | ] 30 | ] 31 | ] 32 | ] 33 | ] 34 | ] 35 | ] 36 | ] 37 | ] 38 | ] 39 | ] -------------------------------------------------------------------------------- /test/assets/pass2_result.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [ 4 | [ 5 | [ 6 | [ 7 | [ 8 | [ 9 | [ 10 | [ 11 | [ 12 | [ 13 | [ 14 | [ 15 | [ 16 | [ 17 | [ 18 | [ 19 | [ 20 | "Not too deep" 21 | ] 22 | ] 23 | ] 24 | ] 25 | ] 26 | ] 27 | ] 28 | ] 29 | ] 30 | ] 31 | ] 32 | ] 33 | ] 34 | ] 35 | ] 36 | ] 37 | ] 38 | ] 39 | ] -------------------------------------------------------------------------------- /test/assets/pass2_test.json: -------------------------------------------------------------------------------- 1 | [[[[[[[[[[[[[[[[[[["Not too deep"]]]]]]]]]]]]]]]]]]] -------------------------------------------------------------------------------- /test/assets/pass3_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | "JSON Test Pattern pass3": 3 | { 4 | "The outermost value": must be an object or array. 5 | "In this test": It is an object. 6 | } 7 | } -------------------------------------------------------------------------------- /test/assets/pass3_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "JSON Test Pattern pass3": { 3 | "The outermost value": "must be an object or array.", 4 | "In this test": "It is an object." 5 | } 6 | } -------------------------------------------------------------------------------- /test/assets/pass3_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "JSON Test Pattern pass3": { 3 | "The outermost value": "must be an object or array.", 4 | "In this test": "It is an object." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/assets/pass4_result.hjson: -------------------------------------------------------------------------------- 1 | 10 -------------------------------------------------------------------------------- /test/assets/pass4_result.json: -------------------------------------------------------------------------------- 1 | 10 -------------------------------------------------------------------------------- /test/assets/pass4_test.json: -------------------------------------------------------------------------------- 1 | 2 | 10 3 | -------------------------------------------------------------------------------- /test/assets/passSingle_result.hjson: -------------------------------------------------------------------------------- 1 | allow quoteless strings -------------------------------------------------------------------------------- /test/assets/passSingle_result.json: -------------------------------------------------------------------------------- 1 | "allow quoteless strings" -------------------------------------------------------------------------------- /test/assets/passSingle_test.hjson: -------------------------------------------------------------------------------- 1 | allow quoteless strings -------------------------------------------------------------------------------- /test/assets/stringify/quotes_all_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | "unicorn": "rainbow" 4 | "cat": 1 5 | "hello": 6 | [ 7 | "world" 8 | "!" 9 | ] 10 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_all_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "unicorn": "rainbow", 4 | "cat": 1, 5 | "hello": [ 6 | "world", 7 | "!" 8 | ] 9 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_all_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "unicorn": "rainbow", 4 | "cat": 1, 5 | "hello": [ 6 | "world", 7 | "!" 8 | ] 9 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_all_testmeta.hjson: -------------------------------------------------------------------------------- 1 | { 2 | options: { 3 | quotes: "all" 4 | } 5 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_always_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | foo: "bar" 3 | unicorn: "rainbow" 4 | cat: 1 5 | hello: 6 | [ 7 | "world" 8 | "!" 9 | ] 10 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_always_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "unicorn": "rainbow", 4 | "cat": 1, 5 | "hello": [ 6 | "world", 7 | "!" 8 | ] 9 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_always_test.hjson: -------------------------------------------------------------------------------- 1 | // Test if `always` keeps working as before 2 | { 3 | "foo": "bar", 4 | "unicorn": "rainbow", 5 | "cat": 1, 6 | "hello": [ 7 | "world", 8 | "!" 9 | ] 10 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_always_testmeta.hjson: -------------------------------------------------------------------------------- 1 | { 2 | options: { 3 | quotes: "always" 4 | } 5 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_keys_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | "foo": bar 3 | "unicorn": rainbow 4 | "cat": 1 5 | "hello": 6 | [ 7 | world 8 | ! 9 | ] 10 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_keys_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "unicorn": "rainbow", 4 | "cat": 1, 5 | "hello": [ 6 | "world", 7 | "!" 8 | ] 9 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_keys_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "unicorn": "rainbow", 4 | "cat": 1, 5 | "hello": [ 6 | "world", 7 | "!" 8 | ] 9 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_keys_testmeta.hjson: -------------------------------------------------------------------------------- 1 | { 2 | options: { 3 | quotes: "keys" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/assets/stringify/quotes_strings_ml_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | unicorn: "foo\nbar\nrainbow" 3 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_strings_ml_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "unicorn": "foo\nbar\nrainbow" 3 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_strings_ml_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "unicorn": "foo\nbar\nrainbow" 3 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_strings_ml_testmeta.hjson: -------------------------------------------------------------------------------- 1 | { 2 | options: { 3 | quotes: strings 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/assets/stringify/quotes_strings_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | foo: "bar" 3 | unicorn: "rainbow" 4 | cat: 1 5 | hello: 6 | [ 7 | "world" 8 | "!" 9 | ] 10 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_strings_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "unicorn": "rainbow", 4 | "cat": 1, 5 | "hello": [ 6 | "world", 7 | "!" 8 | ] 9 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_strings_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "unicorn": "rainbow", 4 | "cat": 1, 5 | "hello": [ 6 | "world", 7 | "!" 8 | ] 9 | } -------------------------------------------------------------------------------- /test/assets/stringify/quotes_strings_testmeta.hjson: -------------------------------------------------------------------------------- 1 | { 2 | options: { 3 | quotes: "strings" 4 | } 5 | } -------------------------------------------------------------------------------- /test/assets/stringify1_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | quotes: 3 | { 4 | num1: "1,2" 5 | num2: "-1.1 ," 6 | num3: "1e10 ,2" 7 | num4: "-1e-10," 8 | kw1: "true," 9 | kw2: "false ," 10 | kw3: "null,123" 11 | close1: "1}" 12 | close1b: "1 }" 13 | close2: "1]" 14 | close2b: "1 ]" 15 | close3: "1," 16 | close3b: "1 ," 17 | comment1: "1#str" 18 | comment2: "1//str" 19 | comment3: "1/*str*/" 20 | punc1: "{" 21 | punc1b: "{foo" 22 | punc2: "}" 23 | punc2b: "}foo" 24 | punc3: "[" 25 | punc3b: "[foo" 26 | punc4: "]" 27 | punc4b: "]foo" 28 | punc5: "," 29 | punc5b: ",foo" 30 | punc6: ":" 31 | punc6b: ":foo" 32 | } 33 | noquotes: 34 | { 35 | num0: .1,2 36 | num1: 1.1.1,2 37 | num2: -.1, 38 | num3: 1e10e,2 39 | num4: -1e--10, 40 | kw1: true1, 41 | kw2: false0, 42 | kw3: null0, 43 | close1: a} 44 | close2: a] 45 | comment1: a#str 46 | comment2: a//str 47 | comment3: a/*str*/ 48 | } 49 | } -------------------------------------------------------------------------------- /test/assets/stringify1_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "quotes": { 3 | "num1": "1,2", 4 | "num2": "-1.1 ,", 5 | "num3": "1e10 ,2", 6 | "num4": "-1e-10,", 7 | "kw1": "true,", 8 | "kw2": "false ,", 9 | "kw3": "null,123", 10 | "close1": "1}", 11 | "close1b": "1 }", 12 | "close2": "1]", 13 | "close2b": "1 ]", 14 | "close3": "1,", 15 | "close3b": "1 ,", 16 | "comment1": "1#str", 17 | "comment2": "1//str", 18 | "comment3": "1/*str*/", 19 | "punc1": "{", 20 | "punc1b": "{foo", 21 | "punc2": "}", 22 | "punc2b": "}foo", 23 | "punc3": "[", 24 | "punc3b": "[foo", 25 | "punc4": "]", 26 | "punc4b": "]foo", 27 | "punc5": ",", 28 | "punc5b": ",foo", 29 | "punc6": ":", 30 | "punc6b": ":foo" 31 | }, 32 | "noquotes": { 33 | "num0": ".1,2", 34 | "num1": "1.1.1,2", 35 | "num2": "-.1,", 36 | "num3": "1e10e,2", 37 | "num4": "-1e--10,", 38 | "kw1": "true1,", 39 | "kw2": "false0,", 40 | "kw3": "null0,", 41 | "close1": "a}", 42 | "close2": "a]", 43 | "comment1": "a#str", 44 | "comment2": "a//str", 45 | "comment3": "a/*str*/" 46 | } 47 | } -------------------------------------------------------------------------------- /test/assets/stringify1_test.hjson: -------------------------------------------------------------------------------- 1 | // test if stringify produces correct output 2 | { 3 | quotes: 4 | { 5 | num1: "1,2" 6 | num2: "-1.1 ," 7 | num3: "1e10 ,2" 8 | num4: "-1e-10," 9 | kw1: "true," 10 | kw2: "false ," 11 | kw3: "null,123" 12 | close1: "1}" 13 | close1b: "1 }" 14 | close2: "1]" 15 | close2b: "1 ]" 16 | close3: "1," 17 | close3b: "1 ," 18 | comment1: "1#str" 19 | comment2: "1//str" 20 | comment3: "1/*str*/" 21 | punc1: "{" 22 | punc1b: "{foo" 23 | punc2: "}" 24 | punc2b: "}foo" 25 | punc3: "[" 26 | punc3b: "[foo" 27 | punc4: "]" 28 | punc4b: "]foo" 29 | punc5: "," 30 | punc5b: ",foo" 31 | punc6: ":" 32 | punc6b: ":foo" 33 | } 34 | noquotes: 35 | { 36 | num0: ".1,2" 37 | num1: "1.1.1,2" 38 | num2: "-.1," 39 | num3: "1e10e,2" 40 | num4: "-1e--10," 41 | kw1: "true1," 42 | kw2: "false0," 43 | kw3: "null0," 44 | close1: "a}" 45 | close2: "a]" 46 | comment1: "a#str" 47 | comment2: "a//str" 48 | comment3: "a/*str*/" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/assets/strings2_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | key1: a key in single quotes 3 | "key 2": a key in single quotes 4 | "key \"": a key in single quotes 5 | text: 6 | [ 7 | single quoted string 8 | '''You need quotes for escapes''' 9 | " untrimmed " 10 | "untrimmed " 11 | containing " double quotes 12 | containing " double quotes 13 | containing " double quotes 14 | '''"containing more " double quotes"''' 15 | containing ' single quotes 16 | containing ' single quotes 17 | containing ' single quotes 18 | "'containing more ' single quotes'" 19 | "'containing more ' single quotes'" 20 | "\n" 21 | " \n" 22 | "\n \n \n \n" 23 | "\t\n" 24 | ] 25 | foo3a: asdf''' 26 | foo3b: "'''asdf" 27 | foo4a: "asdf'''\nasdf" 28 | foo4b: "asdf\n'''asdf" 29 | } -------------------------------------------------------------------------------- /test/assets/strings2_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1": "a key in single quotes", 3 | "key 2": "a key in single quotes", 4 | "key \"": "a key in single quotes", 5 | "text": [ 6 | "single quoted string", 7 | "You need quotes\tfor escapes", 8 | " untrimmed ", 9 | "untrimmed ", 10 | "containing \" double quotes", 11 | "containing \" double quotes", 12 | "containing \" double quotes", 13 | "\"containing more \" double quotes\"", 14 | "containing ' single quotes", 15 | "containing ' single quotes", 16 | "containing ' single quotes", 17 | "'containing more ' single quotes'", 18 | "'containing more ' single quotes'", 19 | "\n", 20 | " \n", 21 | "\n \n \n \n", 22 | "\t\n" 23 | ], 24 | "foo3a": "asdf'''", 25 | "foo3b": "'''asdf", 26 | "foo4a": "asdf'''\nasdf", 27 | "foo4b": "asdf\n'''asdf" 28 | } -------------------------------------------------------------------------------- /test/assets/strings2_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # Hjson 3 allows the use of single quotes 3 | 4 | 'key1': a key in single quotes 5 | 'key 2': a key in single quotes 6 | 'key "': a key in single quotes 7 | 8 | text: [ 9 | 'single quoted string' 10 | 'You need quotes\tfor escapes' 11 | ' untrimmed ' 12 | 'untrimmed ' 13 | 'containing " double quotes' 14 | 'containing \" double quotes' 15 | "containing \" double quotes" 16 | '"containing more " double quotes"' 17 | 'containing \' single quotes' 18 | "containing ' single quotes" 19 | "containing \' single quotes" 20 | "'containing more ' single quotes'" 21 | "\'containing more \' single quotes\'" 22 | 23 | '\n' 24 | ' \n' 25 | '\n \n \n \n' 26 | '\t\n' 27 | ] 28 | 29 | # escapes/no escape 30 | 31 | foo3a: 'asdf\'\'\'' 32 | foo3b: '\'\'\'asdf' 33 | 34 | foo4a: 'asdf\'\'\'\nasdf' 35 | foo4b: 'asdf\n\'\'\'asdf' 36 | } 37 | -------------------------------------------------------------------------------- /test/assets/strings_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | text1: This is a valid string value. 3 | text2: a \ is just a \ 4 | text3: '''You need quotes for escapes''' 5 | text4a: " untrimmed " 6 | text4b: " untrimmed" 7 | text4c: "untrimmed " 8 | notml1: "\n" 9 | notml2: " \n" 10 | notml3: "\n \n \n \n" 11 | notml4: "\t\n" 12 | multiline1: 13 | ''' 14 | first line 15 | indented line 16 | last line 17 | ''' 18 | multiline2: 19 | ''' 20 | first line 21 | indented line 22 | last line 23 | ''' 24 | multiline3: 25 | ''' 26 | first line 27 | indented line 28 | last line 29 | 30 | 31 | ''' 32 | foo1a: asdf\"'a\s\w 33 | foo1b: asdf\"'a\s\w 34 | foo1c: asdf\"'a\s\w 35 | foo2a: '''"asdf"''' 36 | foo2b: '''"asdf"''' 37 | foo3a: asdf''' 38 | foo3b: "'''asdf" 39 | foo4a: "asdf'''\nasdf" 40 | foo4b: "asdf\n'''asdf" 41 | arr: 42 | [ 43 | one 44 | two 45 | three 46 | four 47 | ] 48 | not: 49 | { 50 | number: 5 51 | negative: -4.2 52 | yes: true 53 | no: false 54 | null: null 55 | array: 56 | [ 57 | 1 58 | 2 59 | 3 60 | 4 61 | 5 62 | 6 63 | 7 64 | 8 65 | 9 66 | 0 67 | -1 68 | 0.5 69 | ] 70 | } 71 | special: 72 | { 73 | true: "true" 74 | false: "false" 75 | null: "null" 76 | one: "1" 77 | two: "2" 78 | minus: "-3" 79 | } 80 | } -------------------------------------------------------------------------------- /test/assets/strings_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "text1": "This is a valid string value.", 3 | "text2": "a \\ is just a \\", 4 | "text3": "You need quotes\tfor escapes", 5 | "text4a": " untrimmed ", 6 | "text4b": " untrimmed", 7 | "text4c": "untrimmed ", 8 | "notml1": "\n", 9 | "notml2": " \n", 10 | "notml3": "\n \n \n \n", 11 | "notml4": "\t\n", 12 | "multiline1": "first line\n indented line\nlast line", 13 | "multiline2": "first line\n indented line\nlast line", 14 | "multiline3": "first line\n indented line\nlast line\n\n", 15 | "foo1a": "asdf\\\"'a\\s\\w", 16 | "foo1b": "asdf\\\"'a\\s\\w", 17 | "foo1c": "asdf\\\"'a\\s\\w", 18 | "foo2a": "\"asdf\"", 19 | "foo2b": "\"asdf\"", 20 | "foo3a": "asdf'''", 21 | "foo3b": "'''asdf", 22 | "foo4a": "asdf'''\nasdf", 23 | "foo4b": "asdf\n'''asdf", 24 | "arr": [ 25 | "one", 26 | "two", 27 | "three", 28 | "four" 29 | ], 30 | "not": { 31 | "number": 5, 32 | "negative": -4.2, 33 | "yes": true, 34 | "no": false, 35 | "null": null, 36 | "array": [ 37 | 1, 38 | 2, 39 | 3, 40 | 4, 41 | 5, 42 | 6, 43 | 7, 44 | 8, 45 | 9, 46 | 0, 47 | -1, 48 | 0.5 49 | ] 50 | }, 51 | "special": { 52 | "true": "true", 53 | "false": "false", 54 | "null": "null", 55 | "one": "1", 56 | "two": "2", 57 | "minus": "-3" 58 | } 59 | } -------------------------------------------------------------------------------- /test/assets/strings_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # simple 3 | 4 | text1: This is a valid string value. 5 | text2:a \ is just a \ 6 | 7 | text3: "You need quotes\tfor escapes" 8 | 9 | text4a: " untrimmed " 10 | text4b: " untrimmed" 11 | text4c: "untrimmed " 12 | 13 | notml1: "\n" 14 | notml2: " \n" 15 | notml3: "\n \n \n \n" 16 | notml4: "\t\n" 17 | 18 | # multiline string 19 | 20 | multiline1: 21 | ''' 22 | first line 23 | indented line 24 | last line 25 | ''' 26 | 27 | multiline2: 28 | '''first line 29 | indented line 30 | last line''' 31 | 32 | multiline3: 33 | ''' 34 | first line 35 | indented line 36 | last line 37 | 38 | 39 | ''' # trailing lf 40 | 41 | # escapes/no escape 42 | 43 | foo1a: asdf\"'a\s\w 44 | foo1b: '''asdf\"'a\s\w''' 45 | foo1c: "asdf\\\"'a\\s\\w" 46 | 47 | foo2a: "\"asdf\"" 48 | foo2b: '''"asdf"''' 49 | 50 | foo3a: "asdf'''" 51 | foo3b: "'''asdf" 52 | 53 | foo4a: "asdf'''\nasdf" 54 | foo4b: "asdf\n'''asdf" 55 | 56 | # in arrays 57 | arr: 58 | [ 59 | one 60 | two 61 | "three" 62 | '''four''' 63 | ] 64 | 65 | # not strings 66 | not: 67 | { 68 | number: 5 69 | negative: -4.2 70 | yes: true 71 | no: false 72 | null: null 73 | array: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, -1, 0.5 ] 74 | } 75 | 76 | # special quoted 77 | special: 78 | { 79 | true: "true" 80 | false: "false" 81 | null: "null" 82 | one: "1" 83 | two: "2" 84 | minus: "-3" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/assets/trail_result.hjson: -------------------------------------------------------------------------------- 1 | { 2 | foo: 0 -- this string starts at 0 and ends at 1, preceding and trailing whitespace is ignored -- 1 3 | } -------------------------------------------------------------------------------- /test/assets/trail_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "0 -- this string starts at 0 and ends at 1, preceding and trailing whitespace is ignored -- 1" 3 | } -------------------------------------------------------------------------------- /test/assets/trail_test.hjson: -------------------------------------------------------------------------------- 1 | { 2 | // the following line contains trailing whitespace: 3 | foo: 0 -- this string starts at 0 and ends at 1, preceding and trailing whitespace is ignored -- 1 4 | } 5 | -------------------------------------------------------------------------------- /test/json.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- json.lua 3 | -- 4 | -- Copyright (c) 2019 rxi 5 | -- 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | -- this software and associated documentation files (the "Software"), to deal in 8 | -- the Software without restriction, including without limitation the rights to 9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | -- of the Software, and to permit persons to whom the Software is furnished to do 11 | -- so, subject to the following conditions: 12 | -- 13 | -- The above copyright notice and this permission notice shall be included in all 14 | -- copies or substantial portions of the Software. 15 | -- 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | -- SOFTWARE. 23 | -- 24 | 25 | local json = { _version = "0.1.2" } 26 | 27 | ------------------------------------------------------------------------------- 28 | -- Encode 29 | ------------------------------------------------------------------------------- 30 | 31 | local encode 32 | 33 | local escape_char_map = { 34 | [ "\\" ] = "\\\\", 35 | [ "\"" ] = "\\\"", 36 | [ "\b" ] = "\\b", 37 | [ "\f" ] = "\\f", 38 | [ "\n" ] = "\\n", 39 | [ "\r" ] = "\\r", 40 | [ "\t" ] = "\\t", 41 | } 42 | 43 | local escape_char_map_inv = { [ "\\/" ] = "/" } 44 | for k, v in pairs(escape_char_map) do 45 | escape_char_map_inv[v] = k 46 | end 47 | 48 | 49 | local function escape_char(c) 50 | return escape_char_map[c] or string.format("\\u%04x", c:byte()) 51 | end 52 | 53 | 54 | local function encode_nil(val) 55 | return "null" 56 | end 57 | 58 | 59 | local function encode_table(val, stack) 60 | local res = {} 61 | stack = stack or {} 62 | 63 | -- Circular reference? 64 | if stack[val] then error("circular reference") end 65 | 66 | stack[val] = true 67 | 68 | if rawget(val, 1) ~= nil or next(val) == nil then 69 | -- Treat as array -- check keys are valid and it is not sparse 70 | local n = 0 71 | for k in pairs(val) do 72 | if type(k) ~= "number" then 73 | error("invalid table: mixed or invalid key types") 74 | end 75 | n = n + 1 76 | end 77 | if n ~= #val then 78 | error("invalid table: sparse array") 79 | end 80 | -- Encode 81 | for i, v in ipairs(val) do 82 | table.insert(res, encode(v, stack)) 83 | end 84 | stack[val] = nil 85 | return "[" .. table.concat(res, ",") .. "]" 86 | 87 | else 88 | -- Treat as an object 89 | for k, v in pairs(val) do 90 | if type(k) ~= "string" then 91 | error("invalid table: mixed or invalid key types") 92 | end 93 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 94 | end 95 | stack[val] = nil 96 | return "{" .. table.concat(res, ",") .. "}" 97 | end 98 | end 99 | 100 | 101 | local function encode_string(val) 102 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 103 | end 104 | 105 | 106 | local function encode_number(val) 107 | -- Check for NaN, -inf and inf 108 | if val ~= val or val <= -math.huge or val >= math.huge then 109 | error("unexpected number value '" .. tostring(val) .. "'") 110 | end 111 | return string.format("%.14g", val) 112 | end 113 | 114 | 115 | local type_func_map = { 116 | [ "nil" ] = encode_nil, 117 | [ "table" ] = encode_table, 118 | [ "string" ] = encode_string, 119 | [ "number" ] = encode_number, 120 | [ "boolean" ] = tostring, 121 | } 122 | 123 | 124 | encode = function(val, stack) 125 | local t = type(val) 126 | local f = type_func_map[t] 127 | if f then 128 | return f(val, stack) 129 | end 130 | error("unexpected type '" .. t .. "'") 131 | end 132 | 133 | 134 | function json.encode(val) 135 | return ( encode(val) ) 136 | end 137 | 138 | 139 | ------------------------------------------------------------------------------- 140 | -- Decode 141 | ------------------------------------------------------------------------------- 142 | 143 | local parse 144 | 145 | local function create_set(...) 146 | local res = {} 147 | for i = 1, select("#", ...) do 148 | res[ select(i, ...) ] = true 149 | end 150 | return res 151 | end 152 | 153 | local space_chars = create_set(" ", "\t", "\r", "\n") 154 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 155 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 156 | local literals = create_set("true", "false", "null") 157 | 158 | local literal_map = { 159 | [ "true" ] = true, 160 | [ "false" ] = false, 161 | [ "null" ] = nil, 162 | } 163 | 164 | 165 | local function next_char(str, idx, set, negate) 166 | for i = idx, #str do 167 | if set[str:sub(i, i)] ~= negate then 168 | return i 169 | end 170 | end 171 | return #str + 1 172 | end 173 | 174 | 175 | local function decode_error(str, idx, msg) 176 | local line_count = 1 177 | local col_count = 1 178 | for i = 1, idx - 1 do 179 | col_count = col_count + 1 180 | if str:sub(i, i) == "\n" then 181 | line_count = line_count + 1 182 | col_count = 1 183 | end 184 | end 185 | error( string.format("%s at line %d col %d", msg, line_count, col_count) ) 186 | end 187 | 188 | 189 | local function codepoint_to_utf8(n) 190 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 191 | local f = math.floor 192 | if n <= 0x7f then 193 | return string.char(n) 194 | elseif n <= 0x7ff then 195 | return string.char(f(n / 64) + 192, n % 64 + 128) 196 | elseif n <= 0xffff then 197 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 198 | elseif n <= 0x10ffff then 199 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 200 | f(n % 4096 / 64) + 128, n % 64 + 128) 201 | end 202 | error( string.format("invalid unicode codepoint '%x'", n) ) 203 | end 204 | 205 | 206 | local function parse_unicode_escape(s) 207 | local n1 = tonumber( s:sub(3, 6), 16 ) 208 | local n2 = tonumber( s:sub(9, 12), 16 ) 209 | -- Surrogate pair? 210 | if n2 then 211 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 212 | else 213 | return codepoint_to_utf8(n1) 214 | end 215 | end 216 | 217 | 218 | local function parse_string(str, i) 219 | local has_unicode_escape = false 220 | local has_surrogate_escape = false 221 | local has_escape = false 222 | local last 223 | for j = i + 1, #str do 224 | local x = str:byte(j) 225 | 226 | if x < 32 then 227 | decode_error(str, j, "control character in string") 228 | end 229 | 230 | if last == 92 then -- "\\" (escape char) 231 | if x == 117 then -- "u" (unicode escape sequence) 232 | local hex = str:sub(j + 1, j + 5) 233 | if not hex:find("%x%x%x%x") then 234 | decode_error(str, j, "invalid unicode escape in string") 235 | end 236 | if hex:find("^[dD][89aAbB]") then 237 | has_surrogate_escape = true 238 | else 239 | has_unicode_escape = true 240 | end 241 | else 242 | local c = string.char(x) 243 | if not escape_chars[c] then 244 | decode_error(str, j, "invalid escape char '" .. c .. "' in string") 245 | end 246 | has_escape = true 247 | end 248 | last = nil 249 | 250 | elseif x == 34 then -- '"' (end of string) 251 | local s = str:sub(i + 1, j - 1) 252 | if has_surrogate_escape then 253 | s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) 254 | end 255 | if has_unicode_escape then 256 | s = s:gsub("\\u....", parse_unicode_escape) 257 | end 258 | if has_escape then 259 | s = s:gsub("\\.", escape_char_map_inv) 260 | end 261 | return s, j + 1 262 | 263 | else 264 | last = x 265 | end 266 | end 267 | decode_error(str, i, "expected closing quote for string") 268 | end 269 | 270 | 271 | local function parse_number(str, i) 272 | local x = next_char(str, i, delim_chars) 273 | local s = str:sub(i, x - 1) 274 | local n = tonumber(s) 275 | if not n then 276 | decode_error(str, i, "invalid number '" .. s .. "'") 277 | end 278 | return n, x 279 | end 280 | 281 | 282 | local function parse_literal(str, i) 283 | local x = next_char(str, i, delim_chars) 284 | local word = str:sub(i, x - 1) 285 | if not literals[word] then 286 | decode_error(str, i, "invalid literal '" .. word .. "'") 287 | end 288 | return literal_map[word], x 289 | end 290 | 291 | 292 | local function parse_array(str, i) 293 | local res = {} 294 | i = i + 1 295 | while 1 do 296 | local x 297 | i = next_char(str, i, space_chars, true) 298 | -- Empty / end of array? 299 | if str:sub(i, i) == "]" then 300 | i = i + 1 301 | break 302 | end 303 | -- Read token 304 | x, i = parse(str, i) 305 | table.insert(res, x) 306 | -- Next token 307 | i = next_char(str, i, space_chars, true) 308 | local chr = str:sub(i, i) 309 | i = i + 1 310 | if chr == "]" then break end 311 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 312 | end 313 | return res, i 314 | end 315 | 316 | 317 | local function parse_object(str, i) 318 | local res = {} 319 | i = i + 1 320 | while 1 do 321 | local key, val 322 | i = next_char(str, i, space_chars, true) 323 | -- Empty / end of object? 324 | if str:sub(i, i) == "}" then 325 | i = i + 1 326 | break 327 | end 328 | -- Read key 329 | if str:sub(i, i) ~= '"' then 330 | decode_error(str, i, "expected string for key") 331 | end 332 | key, i = parse(str, i) 333 | -- Read ':' delimiter 334 | i = next_char(str, i, space_chars, true) 335 | if str:sub(i, i) ~= ":" then 336 | decode_error(str, i, "expected ':' after key") 337 | end 338 | i = next_char(str, i + 1, space_chars, true) 339 | -- Read value 340 | val, i = parse(str, i) 341 | -- Set 342 | res[key] = val 343 | -- Next token 344 | i = next_char(str, i, space_chars, true) 345 | local chr = str:sub(i, i) 346 | i = i + 1 347 | if chr == "}" then break end 348 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 349 | end 350 | return res, i 351 | end 352 | 353 | 354 | local char_func_map = { 355 | [ '"' ] = parse_string, 356 | [ "0" ] = parse_number, 357 | [ "1" ] = parse_number, 358 | [ "2" ] = parse_number, 359 | [ "3" ] = parse_number, 360 | [ "4" ] = parse_number, 361 | [ "5" ] = parse_number, 362 | [ "6" ] = parse_number, 363 | [ "7" ] = parse_number, 364 | [ "8" ] = parse_number, 365 | [ "9" ] = parse_number, 366 | [ "-" ] = parse_number, 367 | [ "t" ] = parse_literal, 368 | [ "f" ] = parse_literal, 369 | [ "n" ] = parse_literal, 370 | [ "[" ] = parse_array, 371 | [ "{" ] = parse_object, 372 | } 373 | 374 | 375 | parse = function(str, idx) 376 | local chr = str:sub(idx, idx) 377 | local f = char_func_map[chr] 378 | if f then 379 | return f(str, idx) 380 | end 381 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 382 | end 383 | 384 | 385 | function json.decode(str) 386 | if type(str) ~= "string" then 387 | error("expected argument of type string, got " .. type(str)) 388 | end 389 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 390 | idx = next_char(str, idx, space_chars, true) 391 | if idx <= #str then 392 | decode_error(str, idx, "trailing garbage") 393 | end 394 | return res 395 | end 396 | 397 | 398 | return json 399 | -------------------------------------------------------------------------------- /test/test.lua: -------------------------------------------------------------------------------- 1 | -- MIT License - Copyright (c) 2025 V (alis.is) 2 | -- remove preloaded modules 3 | package.preload["hjson"] = nil 4 | package.loaded["hjson"] = nil 5 | package.preload["hjson.decoder"] = nil 6 | package.loaded["hjson.decoder"] = nil 7 | package.preload["hjson.encoder"] = nil 8 | package.loaded["hjson.encoder"] = nil 9 | package.preload["hjson.encoderH"] = nil 10 | package.loaded["hjson.encoderH"] = nil 11 | 12 | 13 | package.path = package.path .. ";./hjson/?.lua" 14 | 15 | json = require("test.json") 16 | hjson = require("hjson") 17 | 18 | decode = hjson.decode 19 | encode = hjson.encode_to_json 20 | encodeH = hjson.encode 21 | 22 | test_decoder = true 23 | test_encoder = true 24 | test_encoderH = true 25 | 26 | tests_override = {} 27 | 28 | tests = { 29 | "test/assets/charset2_test.hjson", 30 | "test/assets/charset_test.hjson", 31 | "test/assets/comments_test.hjson", 32 | "test/assets/empty_test.hjson", 33 | "test/assets/extra/notabs_test.json", 34 | "test/assets/extra/root_test.hjson", 35 | "test/assets/extra/separator_test.json", 36 | "test/assets/failCharset1_test.hjson", 37 | "test/assets/failJSON02_test.json", 38 | "test/assets/failJSON05_test.json", 39 | "test/assets/failJSON06_test.json", 40 | "test/assets/failJSON07_test.json", 41 | "test/assets/failJSON08_test.json", 42 | "test/assets/failJSON10_test.json", 43 | "test/assets/failJSON11_test.json", 44 | "test/assets/failJSON12_test.json", 45 | "test/assets/failJSON13_test.json", 46 | "test/assets/failJSON14_test.json", 47 | "test/assets/failJSON15_test.json", 48 | "test/assets/failJSON16_test.json", 49 | "test/assets/failJSON17_test.json", 50 | "test/assets/failJSON19_test.json", 51 | "test/assets/failJSON20_test.json", 52 | "test/assets/failJSON21_test.json", 53 | "test/assets/failJSON22_test.json", 54 | "test/assets/failJSON23_test.json", 55 | "test/assets/failJSON26_test.json", 56 | "test/assets/failJSON28_test.json", 57 | "test/assets/failJSON29_test.json", 58 | "test/assets/failJSON30_test.json", 59 | "test/assets/failJSON31_test.json", 60 | "test/assets/failJSON32_test.json", 61 | "test/assets/failJSON33_test.json", 62 | "test/assets/failJSON34_test.json", 63 | "test/assets/failKey1_test.hjson", 64 | "test/assets/failKey2_test.hjson", 65 | "test/assets/failKey3_test.hjson", 66 | "test/assets/failKey4_test.hjson", 67 | "test/assets/failKey5_test.hjson", 68 | "test/assets/failMLStr1_test.hjson", 69 | "test/assets/failObj1_test.hjson", 70 | "test/assets/failObj2_test.hjson", 71 | "test/assets/failObj3_test.hjson", 72 | "test/assets/failStr1a_test.hjson", 73 | "test/assets/failStr1b_test.hjson", 74 | "test/assets/failStr1c_test.hjson", 75 | "test/assets/failStr1d_test.hjson", 76 | "test/assets/failStr2a_test.hjson", 77 | "test/assets/failStr2b_test.hjson", 78 | "test/assets/failStr2c_test.hjson", 79 | "test/assets/failStr2d_test.hjson", 80 | "test/assets/failStr3a_test.hjson", 81 | "test/assets/failStr3b_test.hjson", 82 | "test/assets/failStr3c_test.hjson", 83 | "test/assets/failStr3d_test.hjson", 84 | "test/assets/failStr4a_test.hjson", 85 | "test/assets/failStr4b_test.hjson", 86 | "test/assets/failStr4c_test.hjson", 87 | "test/assets/failStr4d_test.hjson", 88 | "test/assets/failStr5a_test.hjson", 89 | "test/assets/failStr5b_test.hjson", 90 | "test/assets/failStr5c_test.hjson", 91 | "test/assets/failStr5d_test.hjson", 92 | "test/assets/failStr6a_test.hjson", 93 | "test/assets/failStr6b_test.hjson", 94 | "test/assets/failStr6c_test.hjson", 95 | "test/assets/failStr6d_test.hjson", 96 | "test/assets/failStr7a_test.hjson", 97 | "test/assets/failStr8a_test.hjson", 98 | "test/assets/kan_test.hjson", 99 | "test/assets/keys_test.hjson", 100 | "test/assets/mltabs_test.json", 101 | "test/assets/oa_test.hjson", 102 | "test/assets/pass1_test.json", 103 | "test/assets/pass2_test.json", 104 | "test/assets/pass3_test.json", 105 | "test/assets/pass4_test.json", 106 | "test/assets/passSingle_test.hjson", 107 | "test/assets/stringify/quotes_all_test.hjson", 108 | "test/assets/stringify/quotes_always_test.hjson", 109 | "test/assets/stringify/quotes_keys_test.hjson", 110 | "test/assets/stringify/quotes_strings_ml_test.json", 111 | "test/assets/stringify/quotes_strings_test.hjson", 112 | "test/assets/stringify1_test.hjson", 113 | "test/assets/strings2_test.hjson", 114 | "test/assets/strings_test.hjson", 115 | "test/assets/trail_test.hjson" 116 | } 117 | results = { 118 | "test/assets/charset2_result.hjson", 119 | "test/assets/charset2_result.json", 120 | "test/assets/charset_result.hjson", 121 | "test/assets/charset_result.json", 122 | "test/assets/comments_result.hjson", 123 | "test/assets/comments_result.json", 124 | "test/assets/empty_result.hjson", 125 | "test/assets/empty_result.json", 126 | "test/assets/extra/notabs_result.hjson", 127 | "test/assets/extra/notabs_result.json", 128 | "test/assets/extra/root_result.hjson", 129 | "test/assets/extra/root_result.json", 130 | "test/assets/extra/separator_result.hjson", 131 | "test/assets/extra/separator_result.json", 132 | "test/assets/kan_result.hjson", 133 | "test/assets/kan_result.json", 134 | "test/assets/keys_result.hjson", 135 | "test/assets/keys_result.json", 136 | "test/assets/mltabs_result.hjson", 137 | "test/assets/mltabs_result.json", 138 | "test/assets/oa_result.hjson", 139 | "test/assets/oa_result.json", 140 | "test/assets/pass1_result.hjson", 141 | "test/assets/pass1_result.json", 142 | "test/assets/pass2_result.hjson", 143 | "test/assets/pass2_result.json", 144 | "test/assets/pass3_result.hjson", 145 | "test/assets/pass3_result.json", 146 | "test/assets/pass4_result.hjson", 147 | "test/assets/pass4_result.json", 148 | "test/assets/passSingle_result.hjson", 149 | "test/assets/passSingle_result.json", 150 | "test/assets/stringify/quotes_all_result.hjson", 151 | "test/assets/stringify/quotes_all_result.json", 152 | "test/assets/stringify/quotes_always_result.hjson", 153 | "test/assets/stringify/quotes_always_result.json", 154 | "test/assets/stringify/quotes_keys_result.hjson", 155 | "test/assets/stringify/quotes_keys_result.json", 156 | "test/assets/stringify/quotes_strings_ml_result.hjson", 157 | "test/assets/stringify/quotes_strings_ml_result.json", 158 | "test/assets/stringify/quotes_strings_result.hjson", 159 | "test/assets/stringify/quotes_strings_result.json", 160 | "test/assets/stringify1_result.hjson", 161 | "test/assets/stringify1_result.json", 162 | "test/assets/strings2_result.hjson", 163 | "test/assets/strings2_result.json", 164 | "test/assets/strings_result.hjson", 165 | "test/assets/strings_result.json", 166 | "test/assets/trail_result.hjson", 167 | "test/assets/trail_result.json" 168 | } 169 | 170 | function readFile(file) 171 | local f = assert(io.open(file, "rb")) 172 | local content = f:read("*all") 173 | f:close() 174 | return content 175 | end 176 | 177 | function get_result_file(testFile) 178 | r = testFile:gsub("_test%..?json", "_result.json") 179 | for i, v in ipairs(results) do 180 | if v == r then 181 | return v 182 | end 183 | end 184 | return nil 185 | end 186 | 187 | function print_object(t) 188 | if type(t) ~= "table" then 189 | print(t) 190 | else 191 | for k, v in pairs(t) do 192 | _type = type(v) 193 | if _type == "table" then 194 | print_object(v) 195 | else 196 | print("k:", k, "v:", v) 197 | end 198 | end 199 | end 200 | end 201 | 202 | function is_array(t) 203 | local i = 0 204 | for _ in pairs(t) do 205 | i = i + 1 206 | if t[i] == nil then 207 | return false 208 | end 209 | end 210 | return true 211 | end 212 | 213 | function equal(t1, t2) 214 | result = true 215 | if (type(t1) ~= "table") then 216 | if t1 ~= t2 then 217 | print(t1, t2) 218 | end 219 | return t1 == t2 220 | end 221 | for k, v in pairs(t1) do 222 | _type = type(v) 223 | if _type ~= type(t2[k]) then 224 | result = false 225 | break 226 | elseif _type == "function" then 227 | -- ignoring functions 228 | elseif _type == "table" then 229 | if not equal(v, t2[k]) then 230 | result = false 231 | break 232 | end 233 | elseif v ~= t2[k] then 234 | print('result1: "' .. v .. '", key: ' .. k) 235 | print('result2: "' .. t2[k] .. '", key: ' .. k) 236 | result = false 237 | break 238 | end 239 | end 240 | return result 241 | end 242 | 243 | local decoder_failed = 0 244 | local decoder_skipped = 0 245 | local decoder_success = 0 246 | 247 | if #tests_override > 0 then 248 | tests = tests_override 249 | end 250 | if test_decoder then 251 | print("Running decoder tests: ") 252 | for i, v in ipairs(tests) do 253 | local shouldfail = v:find("fail") 254 | local file = readFile(v) 255 | local decoded_test, err = decode(file) 256 | if shouldfail and not decoded_test then 257 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "OK") 258 | decoder_success = decoder_success + 1 259 | elseif shouldfail then 260 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "FAILED - should fail") 261 | decoder_failed = decoder_failed + 1 262 | elseif not decoded_test then 263 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "FAILED - decoder failed - " .. tostring(err)) 264 | decoder_failed = decoder_failed + 1 265 | else 266 | local result_file = get_result_file(v) 267 | if not result_file then 268 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "SKIPPED - no result file") 269 | decoder_skipped = decoder_skipped + 1 270 | else 271 | file = readFile(result_file) 272 | decoded_result, err = json.decode(file) 273 | if not decoded_result then 274 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "SKIPPED - failed to parse result file") 275 | decoder_skipped = decoder_skipped + 1 276 | elseif equal(decoded_test, decoded_result) then 277 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "OK") 278 | decoder_success = decoder_success + 1 279 | else 280 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "FAILED - results do not match") 281 | decoder_failed = decoder_failed + 1 282 | end 283 | end 284 | end 285 | end 286 | print("========================") 287 | print("Decoder testing finished") 288 | 289 | print("Total", #tests, "Success", decoder_success, "Skipped", decoder_skipped, "Failed", decoder_failed) 290 | end 291 | 292 | local encoder_failed = 0 293 | local encoder_skipped = 0 294 | local encoder_success = 0 295 | 296 | if test_encoder then 297 | print("Running decoder tests: ") 298 | for i, v in ipairs(tests) do 299 | local should_fail = v:find("fail") 300 | local file = readFile(v) 301 | local decoded_test, err = decode(file) 302 | 303 | if should_fail and not decoded_test then 304 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "OK") 305 | encoder_success = encoder_success + 1 306 | elseif should_fail then 307 | print( 308 | tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, 309 | "SKIPPED - decode of the test file should fail and succeeded" 310 | ) 311 | encoder_skipped = encoder_skipped + 1 312 | elseif not decoded_test then 313 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "SKIPPED - decode of the test file failed") 314 | encoder_skipped = encoder_skipped + 1 315 | else 316 | local encoded_test, err = encode(decoded_test) 317 | if not encoded_test then 318 | print( 319 | tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, 320 | "FAILED - encoder failed - " .. tostring(err) 321 | ) 322 | encoder_failed = encoder_failed + 1 323 | else 324 | local result_file = get_result_file(v) 325 | if not result_file then 326 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "SKIPPED - no result file") 327 | encoder_skipped = encoder_skipped + 1 328 | else 329 | decoded_test, err = json.decode(encoded_test) 330 | if not decoded_test then 331 | else 332 | file = readFile(result_file) 333 | local decoded_result, _ = json.decode(file) 334 | if not decoded_result then 335 | print( 336 | tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, 337 | "SKIPPED - failed to parse result file" 338 | ) 339 | encoder_skipped = encoder_skipped + 1 340 | elseif equal(decoded_test, decoded_result) then 341 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "OK") 342 | encoder_success = encoder_success + 1 343 | else 344 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "FAILED - results do not match") 345 | encoder_failed = encoder_failed + 1 346 | end 347 | end 348 | end 349 | end 350 | end 351 | end 352 | print("========================") 353 | print("Encoder testing finished") 354 | 355 | print("Total", #tests, "Success", encoder_success, "Skipped", encoder_skipped, "Failed", encoder_failed) 356 | end 357 | 358 | local encoderH_failed = 0 359 | local encoderH_skipped = 0 360 | local encoderH_success = 0 361 | 362 | if test_encoderH then 363 | print("Running decoder tests: ") 364 | for i, v in ipairs(tests) do 365 | local shouldfail = v:find("fail") 366 | local file = readFile(v) 367 | local decoded_test, err = decode(file) 368 | 369 | if shouldfail and not decoded_test then 370 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "OK") 371 | encoderH_success = encoderH_success + 1 372 | elseif shouldfail then 373 | print( 374 | tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, 375 | "SKIPPED - decode of the test file should fail and succeeded" 376 | ) 377 | encoderH_skipped = encoderH_skipped + 1 378 | elseif not decoded_test then 379 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "SKIPPED - decode of the test file failed") 380 | encoderH_skipped = encoderH_skipped + 1 381 | else 382 | local encoded_test, _ = encodeH(decoded_test) 383 | if not encoded_test then 384 | print( 385 | tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, 386 | "FAILED - encoder failed - " .. encoded_test 387 | ) 388 | encoderH_failed = encoderH_failed + 1 389 | else 390 | local result_file = get_result_file(v) 391 | if not result_file then 392 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "SKIPPED - no result file") 393 | encoderH_skipped = encoderH_skipped + 1 394 | else 395 | local decoded_test, _ = decode(encoded_test) 396 | if not decoded_test then 397 | else 398 | file = readFile(result_file) 399 | local decoded_result, _ = json.decode(file) 400 | if not decoded_result then 401 | print( 402 | tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, 403 | "SKIPPED - failed to parse result file" 404 | ) 405 | encoderH_skipped = encoderH_skipped + 1 406 | elseif equal(decoded_test, decoded_result) then 407 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "OK") 408 | encoderH_success = encoderH_success + 1 409 | else 410 | print(tostring(i) .. "/" .. tostring(#tests) .. " - " .. v, "FAILED - results do not match") 411 | encoderH_failed = encoderH_failed + 1 412 | end 413 | end 414 | end 415 | end 416 | end 417 | end 418 | print("========================") 419 | print("EncoderH testing finished") 420 | 421 | print("Total", #tests, "Success", encoderH_success, "Skipped", encoderH_skipped, "Failed", encoderH_failed) 422 | end 423 | 424 | print("\n\n\n================ HJSON.lua TEST RESULTS =================") 425 | total_tests = 0 426 | total_success = 0 427 | total_skipped = 0 428 | total_failed = 0 429 | if test_decoder then 430 | print("Decoder Tests", #tests, "Success", decoder_success, "Skipped", decoder_skipped, "Failed", decoder_failed) 431 | total_tests = total_tests + #tests 432 | total_success = total_success + decoder_success 433 | total_skipped = total_skipped + decoder_skipped 434 | total_failed = total_failed + decoder_failed 435 | end 436 | if test_encoder then 437 | print("Encoder Tests", #tests, "Success", encoder_success, "Skipped", encoder_skipped, "Failed", encoder_failed) 438 | total_tests = total_tests + #tests 439 | total_success = total_success + encoder_success 440 | total_skipped = total_skipped + encoder_skipped 441 | total_failed = total_failed + encoder_failed 442 | end 443 | if test_encoderH then 444 | print("EncoderH Tests", #tests, "Success", encoderH_success, "Skipped", encoderH_skipped, "Failed", encoderH_failed) 445 | total_tests = total_tests + #tests 446 | total_success = total_success + encoderH_success 447 | total_skipped = total_skipped + encoderH_skipped 448 | total_failed = total_failed + encoderH_failed 449 | end 450 | print("--------------------------------------------------------------------") 451 | print("Total\t", total_tests, "Success", total_success, "Skipped", total_skipped, "Failed", total_failed) 452 | print( 453 | "Total (%)", 454 | "100%", 455 | "Success", 456 | tostring((total_success * 100) / total_tests) .. "%", 457 | "Skipped", 458 | tostring((total_skipped * 100) / total_tests) .. "%", 459 | "Failed", 460 | tostring((total_failed * 100) / total_tests) .. "%" 461 | ) 462 | --------------------------------------------------------------------------------