├── README.md ├── export.lua └── json.lua /README.md: -------------------------------------------------------------------------------- 1 | # export-aseprite-file 2 | 3 | > by David Capello 4 | 5 | This is a script that can be used to export the data from a 6 | `.aseprite` file into a JSON + a collection of PNG files. This script 7 | will work with Aseprite v1.2.26 and support the future v1.3 to export 8 | tilemap/tileset data. 9 | 10 | ## Example 11 | 12 | Usage: 13 | 14 | aseprite -b map.aseprite -script export.lua 15 | 16 | In this example `export.lua` will create a folder named `map` with 17 | some files inside: 18 | 19 | ``` 20 | map/ 21 | sprite.json 22 | image1.png 23 | image2.png 24 | tileset1.png 25 | ``` 26 | 27 | An example of `map/sprite.json` content: 28 | 29 | ```json 30 | { 31 | "filename": "map.aseprite", 32 | "width": 32, 33 | "height": 32, 34 | "frames": [ 35 | { "duration": 0.1 }, 36 | { "duration": 0.15 } 37 | ], 38 | "layers": [ 39 | { 40 | "name": "Group Layer", 41 | "layers": [ 42 | { 43 | "name": "Common Layer", 44 | "cels": [ 45 | { 46 | "bounds": { "x": 10, "y": 13, "width": 12, "height": 13 }, 47 | "frame": 0, 48 | "image": "map/image1.png" 49 | }, 50 | { 51 | "bounds": { "x": 6, "y": 15, "width": 12, "height": 12 }, 52 | "frame": 1, 53 | "image": "map/image2.png" 54 | } 55 | ] 56 | } 57 | ] 58 | }, 59 | { 60 | "name": "Tilemap Layer", 61 | "tileset": 0, 62 | "cels": [ 63 | { 64 | "bounds": { "x": 0, "y": 0, "width": 32, "height": 32 }, 65 | "data": "text1", 66 | "frame": 0, 67 | "tilemap": { 68 | "width": 4, 69 | "height": 4, 70 | "tiles": [ 71 | 0, 1, 2, 3, 72 | 4, 5, 5, 6, 73 | 7, 5, 5, 8, 74 | 9, 10, 11, 12 75 | ] 76 | } 77 | }, 78 | { 79 | "bounds": { "x": 1, "y": 1, "width": 32, "height": 32 }, 80 | "frame": 1, 81 | "color": "#f7a547", 82 | "data": "text2", 83 | "tilemap": { 84 | "width": 4, 85 | "height": 4, 86 | "tiles": [ 87 | 0, 1, 1, 3, 88 | 4, 4, 4, 8, 89 | 4, 4, 4, 8, 90 | 9, 10, 10, 12 91 | ] 92 | } 93 | } 94 | ] 95 | } 96 | ], 97 | "tilesets": [ 98 | { 99 | "grid": { 100 | "tileSize": { "width": 8, "height": 8 } 101 | }, 102 | "image": "map/tileset1.png" 103 | } 104 | ], 105 | "tags": [ 106 | { 107 | "name": "Tag A", 108 | "aniDir": "pingpong", 109 | "color": "#000000", 110 | "from": 0, 111 | "to": 2 112 | }, 113 | { 114 | "name": "Tag B", 115 | "aniDir": "forward", 116 | "color": "#000000", 117 | "from": 0, 118 | "to": 1 119 | }, 120 | { 121 | "name": "Tag C", 122 | "aniDir": "reverse", 123 | "color": "#000000", 124 | "from": 1, 125 | "to": 2 126 | } 127 | ], 128 | "slices": [ 129 | { 130 | "name": "Slice 1", 131 | "color": "#0000ff", 132 | "bounds": { "x": 4, "y": 19, "width": 8, "height": 6 } 133 | }, 134 | { 135 | "name": "Slice 2", 136 | "color": "#0000ff", 137 | "bounds": { "x": 14, "y": 9, "width": 9, "height": 11 }, 138 | "center": { "x": 1, "y": 1, "width": 7, "height": 9 } 139 | }, 140 | { 141 | "name": "Slice 3", 142 | "color": "#0000ff", 143 | "data": "text3", 144 | "bounds": { "x": 17, "y": 23, "width": 8, "height": 7 }, 145 | "pivot": { "x": 4, "y": 2 } 146 | } 147 | ] 148 | } 149 | ``` 150 | 151 | ## Acknowledges 152 | 153 | This project uses [json.lua](https://github.com/rxi/json.lua) by 154 | [rxi](https://github.com/rxi) to export Lua tables to JSON files. 155 | 156 | ## License 157 | 158 | This code is distributed under the terms of the MIT license. You can 159 | use this code for your own purpose to export the specific data that 160 | you need. 161 | -------------------------------------------------------------------------------- /export.lua: -------------------------------------------------------------------------------- 1 | -- export.lua 2 | -- Copyright (C) 2020 David Capello 3 | -- 4 | -- This file is released under the terms of the MIT license. 5 | 6 | local spr = app.sprite 7 | if not spr then spr = app.activeSprite end -- just to support older versions of Aseprite 8 | if not spr then return print "No active sprite" end 9 | 10 | if ColorMode.TILEMAP == nil then ColorMode.TILEMAP = 4 end 11 | assert(ColorMode.TILEMAP == 4) 12 | 13 | local fs = app.fs 14 | local pc = app.pixelColor 15 | local output_folder = fs.joinPath(app.fs.filePath(spr.filename), fs.fileTitle(spr.filename)) 16 | local image_n = 0 17 | local tileset_n = 0 18 | 19 | local function write_json_data(filename, data) 20 | local json = dofile('./json.lua') 21 | local file = io.open(filename, "w") 22 | file:write(json.encode(data)) 23 | file:close() 24 | end 25 | 26 | local function fill_user_data(t, obj) 27 | if obj.color.alpha > 0 then 28 | if obj.color.alpha == 255 then 29 | t.color = string.format("#%02x%02x%02x", 30 | obj.color.red, 31 | obj.color.green, 32 | obj.color.blue) 33 | else 34 | t.color = string.format("#%02x%02x%02x%02x", 35 | obj.color.red, 36 | obj.color.green, 37 | obj.color.blue, 38 | obj.color.alpha) 39 | end 40 | end 41 | if pcall(function() return obj.data end) then -- a tag doesn't have the data field pre-v1.3 42 | if obj.data and obj.data ~= "" then 43 | t.data = obj.data 44 | end 45 | end 46 | end 47 | 48 | local function export_tileset(tileset) 49 | local t = {} 50 | local grid = tileset.grid 51 | local size = grid.tileSize 52 | t.grid = { tileSize={ width=grid.tileSize.width, height=grid.tileSize.height } } 53 | if #tileset > 0 then 54 | local spec = spr.spec 55 | spec.width = size.width 56 | spec.height = size.height * #tileset 57 | local image = Image(spec) 58 | image:clear() 59 | for i = 0,#tileset-1 do 60 | local tile = tileset:getTile(i) 61 | image:drawImage(tile, 0, i*size.height) 62 | end 63 | 64 | tileset_n = tileset_n + 1 65 | local imageFn = fs.joinPath(output_folder, "tileset" .. tileset_n .. ".png") 66 | image:saveAs(imageFn) 67 | t.image = imageFn 68 | end 69 | return t 70 | end 71 | 72 | local function export_tilesets(tilesets) 73 | local t = {} 74 | for _,tileset in ipairs(tilesets) do 75 | table.insert(t, export_tileset(tileset)) 76 | end 77 | return t 78 | end 79 | 80 | local function export_frames(frames) 81 | local t = {} 82 | for _,frame in ipairs(frames) do 83 | table.insert(t, { duration=frame.duration }) 84 | end 85 | return t 86 | end 87 | 88 | local function export_cel(cel) 89 | local t = { 90 | frame=cel.frameNumber-1, 91 | bounds={ x=cel.bounds.x, 92 | y=cel.bounds.y, 93 | width=cel.bounds.width, 94 | height=cel.bounds.height } 95 | } 96 | 97 | if cel.image.colorMode == ColorMode.TILEMAP then 98 | local tilemap = cel.image 99 | -- save tilemap 100 | t.tilemap = { width=tilemap.width, 101 | height=tilemap.height, 102 | tiles={} } 103 | for it in tilemap:pixels() do 104 | table.insert(t.tilemap.tiles, pc.tileI(it())) 105 | end 106 | else 107 | -- save regular cel 108 | image_n = image_n + 1 109 | local imageFn = fs.joinPath(output_folder, "image" .. image_n .. ".png") 110 | cel.image:saveAs(imageFn) 111 | t.image = imageFn 112 | end 113 | 114 | fill_user_data(t, cel) 115 | return t 116 | end 117 | 118 | local function export_cels(cels) 119 | local t = {} 120 | for _,cel in ipairs(cels) do 121 | table.insert(t, export_cel(cel)) 122 | end 123 | return t 124 | end 125 | 126 | local function get_tileset_index(layer) 127 | for i,tileset in ipairs(layer.sprite.tilesets) do 128 | if layer.tileset == tileset then 129 | return i-1 130 | end 131 | end 132 | return -1 133 | end 134 | 135 | local function export_layer(layer, export_layers) 136 | local t = { name=layer.name } 137 | if layer.isImage then 138 | if layer.opacity < 255 then 139 | t.opacity = layer.opacity 140 | end 141 | if layer.blendMode ~= BlendMode.NORMAL then 142 | t.blendMode = layer.blendMode 143 | end 144 | if #layer.cels >= 1 then 145 | t.cels = export_cels(layer.cels) 146 | end 147 | if pcall(function() return layer.isTilemap end) then 148 | if layer.isTilemap then 149 | t.tileset = get_tileset_index(layer) 150 | end 151 | end 152 | elseif layer.isGroup then 153 | t.layers = export_layers(layer.layers) 154 | end 155 | fill_user_data(t, layer) 156 | return t 157 | end 158 | 159 | local function export_layers(layers) 160 | local t = {} 161 | for _,layer in ipairs(layers) do 162 | table.insert(t, export_layer(layer, export_layers)) 163 | end 164 | return t 165 | end 166 | 167 | local function ani_dir(d) 168 | local values = { "forward", "reverse", "pingpong" } 169 | return values[d+1] 170 | end 171 | 172 | local function export_tag(tag) 173 | local t = { 174 | name=tag.name, 175 | from=tag.fromFrame.frameNumber-1, 176 | to=tag.toFrame.frameNumber-1, 177 | aniDir=ani_dir(tag.aniDir) 178 | } 179 | fill_user_data(t, tag) 180 | return t 181 | end 182 | 183 | local function export_tags(tags) 184 | local t = {} 185 | for _,tag in ipairs(tags) do 186 | table.insert(t, export_tag(tag, export_tags)) 187 | end 188 | return t 189 | end 190 | 191 | local function export_slice(slice) 192 | local t = { 193 | name=slice.name, 194 | bounds={ x=slice.bounds.x, 195 | y=slice.bounds.y, 196 | width=slice.bounds.width, 197 | height=slice.bounds.height } 198 | } 199 | if slice.center then 200 | t.center={ x=slice.center.x, 201 | y=slice.center.y, 202 | width=slice.center.width, 203 | height=slice.center.height } 204 | end 205 | if slice.pivot then 206 | t.pivot={ x=slice.pivot.x, 207 | y=slice.pivot.y } 208 | end 209 | fill_user_data(t, slice) 210 | return t 211 | end 212 | 213 | local function export_slices(slices) 214 | local t = {} 215 | for _,slice in ipairs(slices) do 216 | table.insert(t, export_slice(slice, export_slices)) 217 | end 218 | return t 219 | end 220 | 221 | ---------------------------------------------------------------------- 222 | -- Creates output folder 223 | 224 | fs.makeDirectory(output_folder) 225 | 226 | ---------------------------------------------------------------------- 227 | -- Write /sprite.json file in the output folder 228 | 229 | local jsonFn = fs.joinPath(output_folder, "sprite.json") 230 | local data = { 231 | filename=spr.filename, 232 | width=spr.width, 233 | height=spr.height, 234 | frames=export_frames(spr.frames), 235 | layers=export_layers(spr.layers) 236 | } 237 | if #spr.tags > 0 then 238 | data.tags = export_tags(spr.tags) 239 | end 240 | if #spr.slices > 0 then 241 | data.slices = export_slices(spr.slices) 242 | end 243 | if pcall(function() return spr.tilesets end) then 244 | data.tilesets = export_tilesets(spr.tilesets) 245 | end 246 | write_json_data(jsonFn, data) 247 | -------------------------------------------------------------------------------- /json.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- json.lua 3 | -- 4 | -- Copyright (c) 2020 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(1, 4), 16 ) 208 | local n2 = tonumber( s:sub(7, 10), 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 res = "" 220 | local j = i + 1 221 | local k = j 222 | 223 | while j <= #str do 224 | local x = str:byte(j) 225 | 226 | if x < 32 then 227 | decode_error(str, j, "control character in string") 228 | 229 | elseif x == 92 then -- `\`: Escape 230 | res = res .. str:sub(k, j - 1) 231 | j = j + 1 232 | local c = str:sub(j, j) 233 | if c == "u" then 234 | local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) 235 | or str:match("^%x%x%x%x", j + 1) 236 | or decode_error(str, j - 1, "invalid unicode escape in string") 237 | res = res .. parse_unicode_escape(hex) 238 | j = j + #hex 239 | else 240 | if not escape_chars[c] then 241 | decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") 242 | end 243 | res = res .. escape_char_map_inv[c] 244 | end 245 | k = j + 1 246 | 247 | elseif x == 34 then -- `"`: End of string 248 | res = res .. str:sub(k, j - 1) 249 | return res, j + 1 250 | end 251 | 252 | j = j + 1 253 | end 254 | 255 | decode_error(str, i, "expected closing quote for string") 256 | end 257 | 258 | 259 | local function parse_number(str, i) 260 | local x = next_char(str, i, delim_chars) 261 | local s = str:sub(i, x - 1) 262 | local n = tonumber(s) 263 | if not n then 264 | decode_error(str, i, "invalid number '" .. s .. "'") 265 | end 266 | return n, x 267 | end 268 | 269 | 270 | local function parse_literal(str, i) 271 | local x = next_char(str, i, delim_chars) 272 | local word = str:sub(i, x - 1) 273 | if not literals[word] then 274 | decode_error(str, i, "invalid literal '" .. word .. "'") 275 | end 276 | return literal_map[word], x 277 | end 278 | 279 | 280 | local function parse_array(str, i) 281 | local res = {} 282 | local n = 1 283 | i = i + 1 284 | while 1 do 285 | local x 286 | i = next_char(str, i, space_chars, true) 287 | -- Empty / end of array? 288 | if str:sub(i, i) == "]" then 289 | i = i + 1 290 | break 291 | end 292 | -- Read token 293 | x, i = parse(str, i) 294 | res[n] = x 295 | n = n + 1 296 | -- Next token 297 | i = next_char(str, i, space_chars, true) 298 | local chr = str:sub(i, i) 299 | i = i + 1 300 | if chr == "]" then break end 301 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 302 | end 303 | return res, i 304 | end 305 | 306 | 307 | local function parse_object(str, i) 308 | local res = {} 309 | i = i + 1 310 | while 1 do 311 | local key, val 312 | i = next_char(str, i, space_chars, true) 313 | -- Empty / end of object? 314 | if str:sub(i, i) == "}" then 315 | i = i + 1 316 | break 317 | end 318 | -- Read key 319 | if str:sub(i, i) ~= '"' then 320 | decode_error(str, i, "expected string for key") 321 | end 322 | key, i = parse(str, i) 323 | -- Read ':' delimiter 324 | i = next_char(str, i, space_chars, true) 325 | if str:sub(i, i) ~= ":" then 326 | decode_error(str, i, "expected ':' after key") 327 | end 328 | i = next_char(str, i + 1, space_chars, true) 329 | -- Read value 330 | val, i = parse(str, i) 331 | -- Set 332 | res[key] = val 333 | -- Next token 334 | i = next_char(str, i, space_chars, true) 335 | local chr = str:sub(i, i) 336 | i = i + 1 337 | if chr == "}" then break end 338 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 339 | end 340 | return res, i 341 | end 342 | 343 | 344 | local char_func_map = { 345 | [ '"' ] = parse_string, 346 | [ "0" ] = parse_number, 347 | [ "1" ] = parse_number, 348 | [ "2" ] = parse_number, 349 | [ "3" ] = parse_number, 350 | [ "4" ] = parse_number, 351 | [ "5" ] = parse_number, 352 | [ "6" ] = parse_number, 353 | [ "7" ] = parse_number, 354 | [ "8" ] = parse_number, 355 | [ "9" ] = parse_number, 356 | [ "-" ] = parse_number, 357 | [ "t" ] = parse_literal, 358 | [ "f" ] = parse_literal, 359 | [ "n" ] = parse_literal, 360 | [ "[" ] = parse_array, 361 | [ "{" ] = parse_object, 362 | } 363 | 364 | 365 | parse = function(str, idx) 366 | local chr = str:sub(idx, idx) 367 | local f = char_func_map[chr] 368 | if f then 369 | return f(str, idx) 370 | end 371 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 372 | end 373 | 374 | 375 | function json.decode(str) 376 | if type(str) ~= "string" then 377 | error("expected argument of type string, got " .. type(str)) 378 | end 379 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 380 | idx = next_char(str, idx, space_chars, true) 381 | if idx <= #str then 382 | decode_error(str, idx, "trailing garbage") 383 | end 384 | return res 385 | end 386 | 387 | 388 | return json 389 | --------------------------------------------------------------------------------