diff --git a/IceHUD.lua b/IceHUD.lua index ecd5ff0..3498619 100644 --- a/IceHUD.lua +++ b/IceHUD.lua @@ -7,8 +7,6 @@ local SML = LibStub("LibSharedMedia-3.0") local ACR = LibStub("AceConfigRegistry-3.0") local ConfigDialog = LibStub("AceConfigDialog-3.0") local icon = LibStub("LibDBIcon-1.0", true) -local AceGUI = LibStub("AceGUI-3.0") -local AceSerializer = LibStub("AceSerializer-3.0", 1) local pendingModuleLoads = {} local bReadyToRegisterModules = false @@ -92,6 +90,43 @@ end IceHUD.deepcopy = deepcopy +function IceHUD:removeDefaults(db, defaults, blocker) + -- remove all metatables from the db, so we don't accidentally create new sub-tables through them + setmetatable(db, nil) + -- loop through the defaults and remove their content + for k,v in pairs(defaults) do + if type(v) == "table" and type(db[k]) == "table" then + -- if a blocker was set, dive into it, to allow multi-level defaults + self:removeDefaults(db[k], v, blocker and blocker[k]) + if next(db[k]) == nil then + db[k] = nil + end + else + -- check if the current value matches the default, and that its not blocked by another defaults table + if db[k] == defaults[k] and (not blocker or blocker[k] == nil) then + db[k] = nil + end + end + end +end + +function IceHUD:populateDefaults(db, defaults, blocker) + -- remove all metatables from the db, so we don't accidentally create new sub-tables through them + setmetatable(db, nil) + -- loop through the defaults and add their content + for k,v in pairs(defaults) do + if type(v) == "table" and type(db[k]) == "table" then + -- if a blocker was set, dive into it, to allow multi-level defaults + self:populateDefaults(db[k], v, blocker and blocker[k]) + else + -- check if the current value matches the default, and that its not blocked by another defaults table + if db[k] == nil then + db[k] = defaults[k] + end + end + end +end + IceHUD.Location = "Interface\\AddOns\\IceHUD" StaticPopupDialogs["ICEHUD_CUSTOM_BAR_CREATED"] = diff --git a/IceHUD.toc b/IceHUD.toc index 3331599..c4223eb 100644 --- a/IceHUD.toc +++ b/IceHUD.toc @@ -100,5 +100,8 @@ modules\ArcaneCharges.lua modules\RollTheBones.lua #@do-not-package@ +IceHUD_Options\Json.lua +IceHUD_Options\JsonDecode.lua +IceHUD_Options\JsonEncode.lua IceHUD_Options\Options.lua #@end-do-not-package@ diff --git a/IceHUD_Options/IceHUD_Options.toc b/IceHUD_Options/IceHUD_Options.toc index 0831299..93a5a49 100644 --- a/IceHUD_Options/IceHUD_Options.toc +++ b/IceHUD_Options/IceHUD_Options.toc @@ -11,4 +11,7 @@ ## Dependencies: IceHUD ## LoadOnDemand: 1 +Json.lua +JsonDecode.lua +JsonEncode.lua Options.lua diff --git a/IceHUD_Options/Json.lua b/IceHUD_Options/Json.lua new file mode 100644 index 0000000..26e59e3 --- /dev/null +++ b/IceHUD_Options/Json.lua @@ -0,0 +1 @@ +IceHUD.json = {} diff --git a/IceHUD_Options/JsonDecode.lua b/IceHUD_Options/JsonDecode.lua new file mode 100644 index 0000000..8be6a16 --- /dev/null +++ b/IceHUD_Options/JsonDecode.lua @@ -0,0 +1,241 @@ +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[select(i, ...)] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + ["true"] = true, + ["false"] = false, + ["null"] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + + return string.format("%s at line %d col %d", msg, line_count, col_count) +end + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + return "", string.format("invalid unicode codepoint '%x'", n) +end + +local function parse_unicode_escape(s) + local n1 = tonumber(s:sub(1, 4), 16) + local n2 = tonumber(s:sub(7, 10), 16) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + return str, j, decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or false + if hex == false then + return str, j-1, decode_error(str, j - 1, "invalid unicode escape in string") + end + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + return str, j-1, decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + return str, i, decode_error(str, i, "expected closing quote for string") +end + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + return -1, -1, decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + return false, -1, decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then return nil, -1, decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + return nil, -1, decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + return nil, -1, decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then return nil, -1, decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + +local char_func_map = { + ['"'] = parse_string, + ["0"] = parse_number, + ["1"] = parse_number, + ["2"] = parse_number, + ["3"] = parse_number, + ["4"] = parse_number, + ["5"] = parse_number, + ["6"] = parse_number, + ["7"] = parse_number, + ["8"] = parse_number, + ["9"] = parse_number, + ["-"] = parse_number, + ["t"] = parse_literal, + ["f"] = parse_literal, + ["n"] = parse_literal, + ["["] = parse_array, + ["{"] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + return false, -1, decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function IceHUD.json.decode(str) + if type(str) ~= "string" then + return nil, "expected argument of type string, got " .. type(str) + end + local res, idx, err = parse(str, next_char(str, 1, space_chars, true)) + if err ~= nil then + return nil, err + end + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + return nil, decode_error(str, idx, "trailing garbage") + end + return res, nil +end diff --git a/IceHUD_Options/JsonEncode.lua b/IceHUD_Options/JsonEncode.lua new file mode 100644 index 0000000..0ebe412 --- /dev/null +++ b/IceHUD_Options/JsonEncode.lua @@ -0,0 +1,100 @@ +local encode + +local escape_char_map = { + ["\\"] = "\\", + ["\""] = "\"", + ["\b"] = "b", + ["\f"] = "f", + ["\n"] = "n", + ["\r"] = "r", + ["\t"] = "t", +} + +local escape_char_map_inv = { ["/"] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + +local function encode_nil(val) + return "null" +end + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + +local type_func_map = { + ["nil"] = encode_nil, + ["table"] = encode_table, + ["string"] = encode_string, + ["number"] = encode_number, + ["boolean"] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + +function IceHUD.json.encode(val) + return (encode(val)) +end diff --git a/IceHUD_Options/Options.lua b/IceHUD_Options/Options.lua index 5b089fe..823be6c 100644 --- a/IceHUD_Options/Options.lua +++ b/IceHUD_Options/Options.lua @@ -1,6 +1,8 @@ local LibDualSpec = LibStub('LibDualSpec-1.0', true) local L = LibStub("AceLocale-3.0"):GetLocale("IceHUD", false) local icon = LibStub("LibDBIcon-1.0", true) +local AceGUI = LibStub("AceGUI-3.0") +local AceSerializer = LibStub("AceSerializer-3.0", 1) local lastCustomModule = "Bar" IceHUD_Options = {} @@ -757,6 +759,9 @@ function IceHUD_Options:OnLoad() self:GenerateModuleOptions(true) self.options.args.colors.args = IceHUD.IceCore:GetColorOptions() self.options.args.profiles = LibStub("AceDBOptions-3.0"):GetOptionsTable(IceHUD.db) + --@debug@ + IceHUD_Options:SetupProfileImportButtons() + --@end-debug@ -- Add dual-spec support if IceHUD.db ~= nil and LibDualSpec then @@ -787,14 +792,14 @@ function IceHUD_Options:SetupProfileImportButtons() editbox:SetLabel("Profile") editbox:SetFullWidth(true) editbox:SetFullHeight(true) - local profileTable = deepcopy(IceHUD.db.profile) + local profileTable = IceHUD.deepcopy(IceHUD.db.profile) IceHUD:removeDefaults(profileTable, IceHUD.IceCore.defaults.profile) - editbox:SetText(IceHUD:Serialize(profileTable)) + editbox:SetText(IceHUD.json.encode(profileTable)) editbox:DisableButton(true) frame:AddChild(editbox) end, hidden = - -- hello, snooper! this feature doesn't actually work yet, so enabling it won't help you much :) + -- hello, snooper! exporting works well enough, but importing is very rough, so enable this at your own peril --[===[@non-debug@ true --@end-non-debug@]===] @@ -803,7 +808,7 @@ function IceHUD_Options:SetupProfileImportButtons() --@end-debug@ , disabled = - -- hello, snooper! this feature doesn't actually work yet, so enabling it won't help you much :) + -- hello, snooper! exporting works well enough, but importing is very rough, so enable this at your own peril --[===[@non-debug@ true --@end-non-debug@]===] @@ -824,11 +829,14 @@ function IceHUD_Options:SetupProfileImportButtons() frame:SetStatusText("Exported profile details") frame:SetLayout("Flow") frame:SetCallback("OnClose", function(widget) - local success, newTable = IceHUD:Deserialize(widget.children[1]:GetText()) - if success then + local newTable, err = IceHUD.json.decode(widget.children[1]:GetText()) + if err ~= nil then + print("failed to import profile: "..err) + else + print("importing profile") IceHUD:PreProfileChanged() IceHUD:populateDefaults(newTable, IceHUD.IceCore.defaults.profile) - IceHUD.db.profile = deepcopy(newTable) + IceHUD.db.profile = IceHUD.deepcopy(newTable) IceHUD:PostProfileChanged() end AceGUI:Release(widget) @@ -841,7 +849,7 @@ function IceHUD_Options:SetupProfileImportButtons() frame:AddChild(editbox) end, hidden = - -- hello, snooper! this feature doesn't actually work yet, so enabling it won't help you much :) + -- hello, snooper! this feature is really rough, so enable it at your own peril --[===[@non-debug@ true --@end-non-debug@]===] @@ -850,7 +858,7 @@ function IceHUD_Options:SetupProfileImportButtons() --@end-debug@ , disabled = - -- hello, snooper! this feature doesn't actually work yet, so enabling it won't help you much :) + -- hello, snooper! this feature is really rough, so enable it at your own peril --[===[@non-debug@ true --@end-non-debug@]===] @@ -862,7 +870,3 @@ function IceHUD_Options:SetupProfileImportButtons() } end end - ---@debug@ -IceHUD_Options:SetupProfileImportButtons() ---@end-debug@ diff --git a/IceHUD_Options/TablePrint.lua b/IceHUD_Options/TablePrint.lua new file mode 100644 index 0000000..f79eadc --- /dev/null +++ b/IceHUD_Options/TablePrint.lua @@ -0,0 +1,37 @@ +local function table_print(tt, indent, done) + done = done or {} + indent = indent or 0 + if type(tt) == "table" then + local sb = {} + for key, value in pairs(tt) do + table.insert(sb, string.rep(" ", indent)) -- indent it + if type(value) == "table" and not done[value] then + done[value] = true + table.insert(sb, key .. " = {\n"); + table.insert(sb, table_print(value, indent + 2, done)) + table.insert(sb, string.rep(" ", indent)) -- indent it + table.insert(sb, "}\n"); + elseif "number" == type(key) then + table.insert(sb, string.format("\"%s\"\n", tostring(value))) + else + table.insert(sb, string.format( + "%s = \"%s\"\n", tostring(key), tostring(value))) + end + end + return table.concat(sb) + else + return tt .. "\n" + end +end + +local function to_string(tbl) + if "nil" == type(tbl) then + return tostring(nil) + elseif "table" == type(tbl) then + return table_print(tbl) + elseif "string" == type(tbl) then + return tbl + else + return tostring(tbl) + end +end