diff --git a/Lua/pt.lua b/Lua/pt.lua new file mode 100644 index 0000000..5f16f1b --- /dev/null +++ b/Lua/pt.lua @@ -0,0 +1,143 @@ +require 'extra' + +local pt = {} +pt.__index = pt +setmetatable(pt, pt) + +function rawstr(v) + if v == nil then return 'nil' end + local mt = getmetatable(v) + local ts = mt and rawget(mt, '__tostring') + if not ts then return tostring(v) end + mt.__tostring = nil + local s = tostring(v) + mt.__tostring = ts + return s +end + +function getaddr(t) + return rawstr(t):sub(#type(t) + 3) +end + +function copy(t) + -- shallow copy + if type(t) ~= 'table' then return end + local new = {} + for key, value in pairs(t) do + new[key] = value + end + return new +end + +function pt.__call(pt, args) + -- print a table as semi-valid YAML + -- with references to prevent recursion/duplication + local t = args.table or args[1] + local self = {} + setmetatable(self, pt) + self.seen = copy(args.seen) or {} + self.skipped = copy(args.skipped) or {} + self.seen_elsewhere = args.seen or {} + self.depth = args.depth or 16 + self.writer = args.writer or io.write + self.skeleton = args.skeleton or false + self.queued = {} + self:inner('__root__', t, '') + return self.seen +end + +function pt:write(...) + self.writer(...) +end + +function pt.safekey(k) + if type(k) == 'table' then + return 't'..getaddr(k) + end + local s = tostring(k) + s = s:gsub('[\r\n]', '') + return s:find('[^%w_]') and ('%q'):format(s) or s +end + +function pt.safeval(v, indent) + if type(v) == 'function' then + return 'f'..getaddr(v) + end + local s = tostring(v) + if type(v) == 'number' then + return s + end + s = s:find('[\r\n]') and ('\n'..s):gsub('[\r\n]', '\n'..indent..' ') or s + --local safe = ('%q'):format(s) + --return s == safe:sub(2, -2) and s or safe + -- TODO: finish matching valid characters + return s:find('[^%w_()[]{}.]') and ('%q'):format(s) or s +end + +function pt:inner(k, v, indent) + if type(v) ~= 'table' then + if self.skeleton then return end + self:write(indent, pt.safekey(k), ': ') + self:write(pt.safeval(v, indent), '\n') + return + end + + local addr = getaddr(v) + self:write(indent, pt.safekey(k)) + + if #indent > self.depth or self.skipped[addr] then + --self.skipped[addr] = true -- TODO: extra logics + self:write(': #t', addr, '\n') + return + end + + if self.seen[addr] or self.queued[addr] then + self:write(': *t', addr, self.seen_elsewhere[addr] and ' #\n' or '\n') + return + end + + self.seen[addr] = true + + self:write(': &t', addr, '\n') + self:outer(v, indent..' ') +end + +function pt:outer(t, indent) + if type(t) ~= "table" then + self:write(indent, pt.safeval(t, indent), '\n') + return + end + + local ours = {} + local not_ours = {} + + for k,v in opairs(t) do + if type(v) == 'table' then + local addr = getaddr(v) + if not self.queued[addr] and not self.seen[addr] and not self.skipped[addr] then + self.queued[addr] = true + ours[k] = v + else + not_ours[k] = v + end + else + self:inner(k, v, indent) + end + end + + for k,v in opairs(not_ours) do + self:inner(k, v, indent) + end + + for k,v in opairs(ours) do + self.queued[getaddr(v)] = nil + self:inner(k, v, indent) + end + + local mt = getmetatable(t) + if mt ~= nil then + self:inner('__metatable', mt, indent) + end +end + +return pt