diff --git a/.gitmodules b/.gitmodules index f89fde895..dec6600f3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "tests/third_party/json"] - path = tests/third_party/json - url = https://github.com/aseprite/json.lua [submodule "third_party/pixman"] path = third_party/pixman url = https://github.com/aseprite/pixman.git diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index b7ab12109..b5bac13f8 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -184,6 +184,7 @@ if(ENABLE_SCRIPTING) script/image_iterator_class.cpp script/image_spec_class.cpp script/images_class.cpp + script/json_class.cpp script/keys.cpp script/layer_class.cpp script/layers_class.cpp diff --git a/src/app/script/engine.cpp b/src/app/script/engine.cpp index 1150ba175..576fb8d2c 100644 --- a/src/app/script/engine.cpp +++ b/src/app/script/engine.cpp @@ -154,6 +154,7 @@ void register_app_pixel_color_object(lua_State* L); void register_app_fs_object(lua_State* L); void register_app_command_object(lua_State* L); void register_app_preferences_object(lua_State* L); +void register_json_object(lua_State* L); void register_brush_class(lua_State* L); void register_cel_class(lua_State* L); @@ -255,12 +256,13 @@ Engine::Engine() // Generic code used by metatables run_mt_index_code(L); - // Register global app object + // Register global objects (app, json) register_app_object(L); register_app_pixel_color_object(L); register_app_fs_object(L); register_app_command_object(L); register_app_preferences_object(L); + register_json_object(L); // Register constants lua_newtable(L); diff --git a/src/app/script/json_class.cpp b/src/app/script/json_class.cpp new file mode 100644 index 000000000..f13f0fe93 --- /dev/null +++ b/src/app/script/json_class.cpp @@ -0,0 +1,314 @@ +// Aseprite +// Copyright (C) 2023 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "app/script/luacpp.h" +#include "app/script/values.h" + +#include + +#include "json11.hpp" + +namespace app { +namespace script { + +namespace { + +struct Json { }; +using JsonObj = json11::Json; +using JsonArrayIterator = JsonObj::array::const_iterator; +using JsonObjectIterator = JsonObj::object::const_iterator; + +void push_json_value(lua_State* L, const JsonObj& value) +{ + switch (value.type()) { + case json11::Json::NUL: + lua_pushnil(L); + break; + case json11::Json::NUMBER: + lua_pushnumber(L, value.number_value()); + break; + case json11::Json::BOOL: + lua_pushboolean(L, value.bool_value()); + break; + case json11::Json::STRING: + lua_pushstring(L, value.string_value().c_str()); + break; + case json11::Json::ARRAY: + case json11::Json::OBJECT: + push_obj(L, value); + break; + } +} + +JsonObj get_json_value(lua_State* L, int index) +{ + switch (lua_type(L, index)) { + + case LUA_TNONE: + case LUA_TNIL: + return JsonObj(); + + case LUA_TBOOLEAN: + return JsonObj(lua_toboolean(L, index) ? true: false); + + case LUA_TNUMBER: + return JsonObj(lua_tonumber(L, index)); + + case LUA_TSTRING: + return JsonObj(lua_tostring(L, index)); + + case LUA_TTABLE: + if (is_array_table(L, index)) { + JsonObj::array items; + if (index < 0) + --index; + lua_pushnil(L); + while (lua_next(L, index) != 0) { + items.push_back(get_json_value(L, -1)); + lua_pop(L, 1); // pop the value lua_next(), leave the key in the stack + } + return JsonObj(items); + } + else { + JsonObj::object items; + lua_pushnil(L); + if (index < 0) + --index; + while (lua_next(L, index) != 0) { + if (const char* k = lua_tostring(L, -2)) { + items[k] = get_json_value(L, -1); + } + lua_pop(L, 1); // pop the value lua_next(), leave the key in the stack + } + return JsonObj(items); + } + break; + + case LUA_TUSERDATA: + // TODO convert rectangles, point, size, uuids? + break; + + } + return JsonObj(); +} + +int JsonObj_gc(lua_State* L) +{ + get_obj(L, 1)->~JsonObj(); + return 0; +} + +int JsonObj_eq(lua_State* L) +{ + auto a = get_obj(L, 1); + auto b = get_obj(L, 2); + return (*a == *b); +} + +int JsonObj_len(lua_State* L) +{ + auto obj = get_obj(L, 1); + switch (obj->type()) { + case json11::Json::STRING: + lua_pushinteger(L, obj->string_value().size()); + break; + case json11::Json::ARRAY: + lua_pushinteger(L, obj->array_items().size()); + break; + case json11::Json::OBJECT: + lua_pushinteger(L, obj->object_items().size()); + break; + case json11::Json::NUL: + lua_pushnil(L); + break; + default: + case json11::Json::NUMBER: + case json11::Json::BOOL: + lua_pushinteger(L, 1); + break; + } + return 1; +} + +int JsonObj_index(lua_State* L) +{ + auto obj = get_obj(L, 1); + if (obj->type() == json11::Json::OBJECT) { + if (auto key = lua_tostring(L, 2)) { + push_json_value(L, (*obj)[key]); + return 1; + } + } + else if (obj->type() == json11::Json::ARRAY) { + auto i = lua_tointeger(L, 2) - 1; // Adjust to 0-based index + push_json_value(L, (*obj)[i]); + return 1; + } + return 0; +} + +int JsonObj_newindex(lua_State* L) +{ + auto obj = get_obj(L, 1); + if (obj->type() == json11::Json::OBJECT) { + if (auto key = lua_tostring(L, 2)) { + // TODO ugly hack, but it works + const_cast&> + (obj->object_items())[key] = get_json_value(L, 3); + } + } + else if (obj->type() == json11::Json::ARRAY) { + auto i = lua_tointeger(L, 2) - 1; // Adjust to 0-based index + const_cast&> + (obj->array_items())[i] = get_json_value(L, 3); + } + return 0; +} + +int JsonObj_pairs_next(lua_State* L) +{ + auto obj = get_obj(L, 1); + auto& it = *get_obj(L, lua_upvalueindex(1)); + if (it == obj->object_items().end()) + return 0; + lua_pushstring(L, (*it).first.c_str()); + push_json_value(L, (*it).second); + ++it; + return 2; +} + +int JsonObj_ipairs_next(lua_State* L) +{ + auto obj = get_obj(L, 1); + auto& it = *get_obj(L, lua_upvalueindex(1)); + if (it == obj->array_items().end()) + return 0; + lua_pushinteger(L, (it - obj->array_items().begin() + 1)); + push_json_value(L, (*it)); + ++it; + return 0; +} + +int JsonObj_pairs(lua_State* L) +{ + auto obj = get_obj(L, 1); + if (obj->type() == json11::Json::OBJECT) { + push_obj(L, obj->object_items().begin()); + lua_pushcclosure(L, JsonObj_pairs_next, 1); + lua_pushvalue(L, 1); // Copy the same obj as the second return value + return 2; + } + else if (obj->type() == json11::Json::ARRAY) { + push_obj(L, obj->array_items().begin()); + lua_pushcclosure(L, JsonObj_ipairs_next, 1); + lua_pushvalue(L, 1); // Copy the same obj as the second return value + return 2; + } + return 0; +} + +int JsonObj_tostring(lua_State* L) +{ + auto obj = get_obj(L, 1); + lua_pushstring(L, obj->dump().c_str()); + return 1; +} + +int JsonObjectIterator_gc(lua_State* L) +{ + get_obj(L, 1)->~JsonObjectIterator(); + return 0; +} + +int JsonArrayIterator_gc(lua_State* L) +{ + get_obj(L, 1)->~JsonArrayIterator(); + return 0; +} + +int Json_decode(lua_State* L) +{ + if (const char* s = lua_tostring(L, 1)) { + std::string err; + auto json = json11::Json::parse(s, std::strlen(s), err); + if (!err.empty()) + return luaL_error(L, err.c_str()); + push_obj(L, json); + return 1; + } + return 0; +} + +int Json_encode(lua_State* L) +{ + // Encode a JsonObj, we deep copy it (create a deep copy) + if (auto obj = may_get_obj(L, 1)) { + lua_pushstring(L, obj->dump().c_str()); + return 1; + } + // Encode a Lua table + else if (lua_istable(L, 1)) { + lua_pushstring(L, get_json_value(L, 1).dump().c_str()); + return 1; + } + return 0; +} + +const luaL_Reg JsonObj_methods[] = { + { "__gc", JsonObj_gc }, + { "__eq", JsonObj_eq }, + { "__len", JsonObj_len }, + { "__index", JsonObj_index }, + { "__newindex", JsonObj_newindex }, + { "__pairs", JsonObj_pairs }, + { "__tostring", JsonObj_tostring }, + { nullptr, nullptr } +}; + +const luaL_Reg JsonObjectIterator_methods[] = { + { "__gc", JsonObjectIterator_gc }, + { nullptr, nullptr } +}; + +const luaL_Reg JsonArrayIterator_methods[] = { + { "__gc", JsonArrayIterator_gc }, + { nullptr, nullptr } +}; + +const luaL_Reg Json_methods[] = { + { "decode", Json_decode }, + { "encode", Json_encode }, + { nullptr, nullptr } +}; + +} // anonymous namespace + +DEF_MTNAME(Json); +DEF_MTNAME(JsonObj); +DEF_MTNAME(JsonObjectIterator); +DEF_MTNAME(JsonArrayIterator); + +void register_json_object(lua_State* L) +{ + REG_CLASS(L, JsonObj); + REG_CLASS(L, JsonObjectIterator); + REG_CLASS(L, JsonArrayIterator); + REG_CLASS(L, Json); + + lua_newtable(L); // Create a table which will be the "json" object + lua_pushvalue(L, -1); + luaL_getmetatable(L, get_mtname()); + lua_setmetatable(L, -2); + lua_setglobal(L, "json"); + lua_pop(L, 1); // Pop json table +} + +} // namespace script +} // namespace app diff --git a/src/app/script/values.cpp b/src/app/script/values.cpp index 7f4478094..dcd2176d2 100644 --- a/src/app/script/values.cpp +++ b/src/app/script/values.cpp @@ -467,37 +467,14 @@ doc::UserData::Variant get_value_from_lua(lua_State* L, int index) v = std::string(lua_tostring(L, index)); break; - case LUA_TTABLE: { - int i = 0; - bool isArray = true; - if (index < 0) - --index; - lua_pushnil(L); - while (lua_next(L, index) != 0) { - if (lua_isinteger(L, -2)) { - if (++i != lua_tointeger(L, -2)) { - isArray = false; - lua_pop(L, 2); // Pop value and key - break; - } - } - else { - isArray = false; - lua_pop(L, 2); - break; - } - lua_pop(L, 1); // Pop the value, leave the key for lua_next() - } - if (index < 0) - ++index; - if (isArray) { + case LUA_TTABLE: + if (is_array_table(L, index)) { v = get_value_from_lua(L, index); } else { v = get_value_from_lua(L, index); } break; - } case LUA_TUSERDATA: { if (auto rect = may_get_obj(L, index)) { @@ -579,5 +556,28 @@ doc::UserData::Vector get_value_from_lua(lua_State* L, int index) return v; } +bool is_array_table(lua_State* L, int index) +{ + if (index < 0) + --index; + + int i = 0; + lua_pushnil(L); + while (lua_next(L, index) != 0) { + if (lua_isinteger(L, -2)) { + if (++i != lua_tointeger(L, -2)) { + lua_pop(L, 2); // Pop value and key + return false; + } + } + else { + lua_pop(L, 2); + return false; + } + lua_pop(L, 1); // Pop the value, leave the key for lua_next() + } + return true; +} + } // namespace script } // namespace app diff --git a/src/app/script/values.h b/src/app/script/values.h index 7b5044a18..400267d0b 100644 --- a/src/app/script/values.h +++ b/src/app/script/values.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019 Igara Studio S.A. +// Copyright (C) 2019-2023 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -21,6 +21,9 @@ T get_value_from_lua(lua_State* L, int index); template void push_value_to_lua(lua_State* L, const T& value); +// Returns true if the given table is an array +bool is_array_table(lua_State* L, int index); + } // namespace script } // namespace app diff --git a/tests/cli/sheet.sh b/tests/cli/sheet.sh index d0e2dad81..7bcd294e2 100644 --- a/tests/cli/sheet.sh +++ b/tests/cli/sheet.sh @@ -17,7 +17,6 @@ if ! $ASEPRITE -b sprites/1empty3.aseprite --sheet "$d/sheet.png" > "$d/stdout.j exit 1 fi cat >$d/compare.lua <$d/compare.lua <$d/check.lua <$d/compare.lua <$d/compare.lua <$d/check.lua <$d/check.lua <$d/check.lua <$d/compare.lua <