mirror of
https://github.com/1bardesign/batteries.git
synced 2024-11-10 02:31:48 +00:00
added pretty module with pretty.print and pretty.string
breaking: string.pretty still exists but takes new config argument
This commit is contained in:
parent
b44b92a32d
commit
e726f19a5e
1
init.lua
1
init.lua
@ -38,6 +38,7 @@ local _batteries = {
|
|||||||
async = require_relative("async"),
|
async = require_relative("async"),
|
||||||
manual_gc = require_relative("manual_gc"),
|
manual_gc = require_relative("manual_gc"),
|
||||||
colour = require_relative("colour"),
|
colour = require_relative("colour"),
|
||||||
|
pretty = require_relative("pretty"),
|
||||||
}
|
}
|
||||||
|
|
||||||
--assign aliases
|
--assign aliases
|
||||||
|
176
pretty.lua
Normal file
176
pretty.lua
Normal 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
|
102
stringx.lua
102
stringx.lua
@ -2,8 +2,9 @@
|
|||||||
extra string routines
|
extra string routines
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local path = (...):gsub(".stringx", ".")
|
local path = (...):gsub("stringx", "")
|
||||||
local assert = require(path .. "assert")
|
local assert = require(path .. "assert")
|
||||||
|
local pretty = require(path .. "pretty")
|
||||||
|
|
||||||
local stringx = setmetatable({}, {
|
local stringx = setmetatable({}, {
|
||||||
__index = string
|
__index = string
|
||||||
@ -66,104 +67,7 @@ function stringx.split(self, delim)
|
|||||||
return res
|
return res
|
||||||
end
|
end
|
||||||
|
|
||||||
|
stringx.pretty = pretty.string
|
||||||
--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
|
|
||||||
|
|
||||||
--(generate a map of whitespace byte values)
|
--(generate a map of whitespace byte values)
|
||||||
local _whitespace_bytes = {}
|
local _whitespace_bytes = {}
|
||||||
|
Loading…
Reference in New Issue
Block a user