added pretty module with pretty.print and pretty.string

breaking: string.pretty still exists but takes new config argument
This commit is contained in:
Max Cahill 2021-07-05 16:12:16 +10:00
parent b44b92a32d
commit e726f19a5e
3 changed files with 180 additions and 99 deletions

View File

@ -38,6 +38,7 @@ local _batteries = {
async = require_relative("async"),
manual_gc = require_relative("manual_gc"),
colour = require_relative("colour"),
pretty = require_relative("pretty"),
}
--assign aliases

176
pretty.lua Normal file
View File

@ -0,0 +1,176 @@
--[[
pretty formatting and printing for nested data structures
also able to be parsed by lua in many cases, but not always
circular references and depth limit will cause the string to contain
things that cannot be parsed.
this isn't a full serialisation solution, it's for debugging and display to humans
all exposed functions take a config table, defaults found in pretty.default_config
per_line
how many fields to print per line
indent
indentation to use for each line, or "" for single-line packed
can be a number of spaces, boolean, or a string to use verbatim
depth
a limit on how deep to explore the table
]]
local path = (...):gsub("pretty", "")
local table = require(path.."tablex") --shadow global table module
local pretty = {}
pretty.default_config = {
per_line = 1,
depth = math.huge,
indent = true,
}
--indentation to use when `indent = true` is provided
pretty.default_indent = "\t"
--pretty-print something directly
function pretty.print(input, config)
print(pretty.string(input, config))
end
--pretty-format something into a string
function pretty.string(input, config)
return pretty._process(input, config)
end
--internal
--actual processing part
function pretty._process(input, config, processing_state)
--if the input is not a table, or it has a tostring metamethod
--then we can just use tostring directly
local mt = getmetatable(input)
if type(input) ~= "table" or mt and mt.__tostring then
local s = tostring(input)
--quote strings
if type(input) == "string" then
s = '"' .. s .. '"'
end
return s
end
--pull out config
config = table.overlay(pretty.default_config, config or {})
local per_line = config.per_line
local depth = config.depth
local indent = config.indent
if type(indent) == "number" then
indent = (" "):rep(indent)
elseif type(indent) == "boolean" then
indent = indent and pretty.default_indent or ""
end
--dependent vars
local newline = indent == "" and "" or "\n"
--init or collect processing state
processing_state = processing_state or {
circular_references = {i = 1},
depth = 0,
}
processing_state.depth = processing_state.depth + 1
if processing_state.depth > depth then
return "{...}"
end
local circular_references = processing_state.circular_references
local ref = circular_references[input]
if ref then
if not ref.string then
ref.string = string.format("%%%d", circular_references.i)
circular_references.i = circular_references.i + 1
end
return ref.string
end
ref = {}
circular_references[input] = ref
local function internal_value(v)
v = pretty._process(v, config, processing_state)
if indent ~= "" then
v = v:gsub(newline, newline..indent)
end
return v
end
--otherwise, we'll build up a table representation of our data
--collate into member chunks
local chunks = {}
--(tracking for already-seen elements from ipairs)
local seen = {}
--sequential part first
--(in practice, pairs already does this, but the order isn't guaranteed)
for i, v in ipairs(input) do
seen[i] = true
table.insert(chunks, internal_value(v))
end
--non sequential follows
for k, v in pairs(input) do
if not seen[k] then
--encapsulate anything that's not a string
--todo: also keywords and strings with spaces
if type(k) ~= "string" then
k = "[" .. tostring(k) .. "]"
end
table.insert(chunks, k .. " = " .. internal_value(v))
end
end
--resolve number to newline skip after
if per_line > 1 then
local line_chunks = {}
while #chunks > 0 do
local break_next = false
local line = {}
for i = 1, per_line do
if #chunks == 0 then
break
end
local v = chunks[1]
--tables split to own line
if v:find("{") then
--break line here
break_next = true
break
else
table.insert(line, table.remove(chunks, 1))
end
end
if #line > 0 then
table.insert(line_chunks, table.concat(line, ", "))
end
if break_next then
table.insert(line_chunks, table.remove(chunks, 1))
break_next = false
end
end
chunks = line_chunks
end
--drop depth
processing_state.depth = processing_state.depth - 1
local multiline = #chunks > 1
local separator = (indent == "" or not multiline) and ", " or ",\n"..indent
local prelude = ref.string and (string.format(" <referenced as %s> ",ref.string)) or ""
if multiline then
return "{" .. prelude .. newline ..
indent .. table.concat(chunks, separator) .. newline ..
"}"
end
return "{" .. prelude .. table.concat(chunks, separator) .. "}"
end
return pretty

View File

@ -2,8 +2,9 @@
extra string routines
]]
local path = (...):gsub(".stringx", ".")
local path = (...):gsub("stringx", "")
local assert = require(path .. "assert")
local pretty = require(path .. "pretty")
local stringx = setmetatable({}, {
__index = string
@ -66,104 +67,7 @@ function stringx.split(self, delim)
return res
end
--turn input into a vaguely easy to read string
--(which is also able to be parsed by lua in many cases)
--todo: support cyclic references without crashing :)
function stringx.pretty(input, indent, after)
--if the input is not a table, or it has a tostring metamethod
--then we can just use tostring
local mt = getmetatable(input)
if type(input) ~= "table" or mt and mt.__tostring then
local s = tostring(input)
--quote strings
if type(input) == "string" then
s = '"' .. s .. '"'
end
return s
end
--resolve indentation requirements
indent = indent or ""
if type(indent) == "number" then
indent = (" "):rep(indent)
end
local newline = indent == "" and "" or "\n"
local function internal_value(v)
v = stringx.pretty(v, indent, after)
if indent ~= "" then
v = v:gsub(newline, newline..indent)
end
return v
end
--otherwise, we've got to build up a table representation
--collate into member chunks
local chunks = {}
--(tracking for already-seen elements from ipairs)
local seen = {}
--sequential part first
--(in practice, pairs already does this, but the order isn't guaranteed)
for i, v in ipairs(input) do
seen[i] = true
table.insert(chunks, internal_value(v))
end
--non sequential follows
for k, v in pairs(input) do
if not seen[k] then
--encapsulate anything that's not a string
--todo: also keywords and strings with spaces
if type(k) ~= "string" then
k = "[" .. tostring(k) .. "]"
end
table.insert(chunks, k .. " = " .. internal_value(v))
end
end
--resolve number to newline skip after
after = after or 1
if after and after > 1 then
local line_chunks = {}
while #chunks > 0 do
local break_next = false
local line = {}
for i = 1, after do
if #chunks == 0 then
break
end
local v = chunks[1]
--tables split to own line
if v:find("{") then
--break line here
break_next = true
break
else
table.insert(line, table.remove(chunks, 1))
end
end
if #line > 0 then
table.insert(line_chunks, table.concat(line, ", "))
end
if break_next then
table.insert(line_chunks, table.remove(chunks, 1))
break_next = false
end
end
chunks = line_chunks
end
local multiline = #chunks > 1
local separator = (indent == "" or not multiline) and ", " or ",\n"..indent
if multiline then
return "{" .. newline ..
indent .. table.concat(chunks, separator) .. newline ..
"}"
end
return "{" .. table.concat(chunks, separator) .. "}"
end
stringx.pretty = pretty.string
--(generate a map of whitespace byte values)
local _whitespace_bytes = {}