636 lines
23 KiB
Lua
636 lines
23 KiB
Lua
|
|
-- pretty.pretty
|
|
-- Main module of the `pretty` library.
|
|
|
|
-- TODO: Maybe move table formatting into its own file?
|
|
|
|
|
|
--[=[ Thoughts on displaying tables in an intuitive way.
|
|
|
|
Lua's table data-structure is likely to be the most concise data structure ever
|
|
invented. (If not, please send me a link!) Lists, maps, objects, classes,
|
|
proxies, etc. This obviously brings about it some difficulty when attempting to
|
|
represent these tables. What do we want to highlight, and what do we choose to
|
|
avoid?
|
|
|
|
One notable issue is whether to show every key that a table answers (to lift
|
|
some Smalltalk terms) to, or to just display those it contains. That is, do we
|
|
think about `__index` in the table's metatable and what it returns, or do we
|
|
ignore `__index`? For cases where `__index` is a function, we cannot say
|
|
anything about the keys that the table answers to. If `__index` is a table, we
|
|
have a better idea, but it would be cluttered to display both types of keys side
|
|
by side.
|
|
|
|
1. Native representation: Lua's native representation includes the type and
|
|
address of the table. It allows for distinguishing between unique tables,
|
|
but won't tell us anything about the contents.
|
|
2. Omission: By representing tables as the pseudo-parsable `{...}`, it's
|
|
clear we are talking about a table. We disregard the ability to
|
|
distinguish between tables.
|
|
2A. If the table is empty, we could represent it as `{}`. But what if the table
|
|
has a metatable with `__index` defined? We could continue to represent it as
|
|
`{}`, but `{...}` would be more "honest".
|
|
3. Single-line: TODO
|
|
4. Multi-line: TODO
|
|
5. Columns: For some highly-regular structures, like lists of short strings,
|
|
giving each string it's own line would be too long, but formatting them as a
|
|
single-line list would be too cluttered. Thus we can take inspiration from
|
|
the classic `ls` unix tool, and place the output into columns, to help guide
|
|
the eyes.
|
|
6. Tabular: Other structures are formatted like actual tables of data, e.g. a
|
|
sequence of tuples, like one would see in an SQL database. For these
|
|
structures it's an obvious choice to align them based on the keys.
|
|
7. Pseudo-Tabular: Some structures are almost tabular, e.g. they are sequences
|
|
of tuples, but some of the tuples differ in their structure. For these
|
|
structures it's still useful to tabulate the keys that all tuples share. To
|
|
do this we should sort the key order descending by the number of tuples with
|
|
the key.
|
|
But what do we do about the the outlier keys? We can either justify the
|
|
entire table, and give specific spots for the outlier keys, thereby
|
|
significantly increasing the size of the table, or we can leave the table
|
|
unjustified, abandoning it's eye-guiding attributes.
|
|
8. Special cases: (Array-tree, Table-Tree, Linked-List, Predictive Sequences) TODO
|
|
|
|
--]=]
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Import files
|
|
|
|
local import
|
|
do
|
|
local thispath = ... and select('1', ...):match('.+%.') or ''
|
|
import = function (name, ignore_failure) return require(thispath..name) end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Constants
|
|
|
|
local ERROR_UNKNOWN_TYPE = [[
|
|
[pretty]: Attempting to format unsupported value of type "%s".
|
|
A native formatting of the value is: %s
|
|
|
|
We are attempting to cover all Lua features, so please report this bug, so we can improve.
|
|
]]
|
|
|
|
local MAX_WIDTH_FOR_SINGLE_LINE_TABLE = 38
|
|
if io and io.popen then
|
|
local f = io.popen "tput cols"
|
|
local term_width = f:read '*n'
|
|
f:close()
|
|
-- if term_width then MAX_WIDTH_FOR_SINGLE_LINE_TABLE = term_width * 3 / 2 end
|
|
end
|
|
|
|
local KEY_TYPE_SORT_ORDER = {
|
|
['number'] = 0,
|
|
['string'] = 1,
|
|
['boolean'] = 2,
|
|
['table'] = 3,
|
|
['userdata'] = 4,
|
|
['thread'] = 5,
|
|
['function'] = 6,
|
|
}
|
|
|
|
local VALUE_TYPE_SORT_ORDER = {
|
|
['nil'] = 0,
|
|
['boolean'] = 1,
|
|
['number'] = 2,
|
|
['string'] = 3,
|
|
['table'] = 4,
|
|
['userdata'] = 5,
|
|
['thread'] = 6,
|
|
['function'] = 7,
|
|
}
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Key-value-pair Util
|
|
|
|
local function padnum(d)
|
|
local dec, n = string.match(d, "(%.?)0*(.+)")
|
|
return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n)
|
|
end
|
|
|
|
local function alphanum_compare_strings (a, b)
|
|
-- Compares two strings alphanumerically.
|
|
|
|
assert(type(a) == 'string')
|
|
assert(type(b) == 'string')
|
|
|
|
local a_padded = a:gsub("%.?%d+", padnum)..("\0%3d"):format(#b)
|
|
local b_padded = b:gsub("%.?%d+", padnum)..("\0%3d"):format(#a)
|
|
local A_padded, B_padded = a_padded:upper(), b_padded:upper()
|
|
if A_padded == B_padded then return a_padded < b_padded end
|
|
return A_padded < B_padded
|
|
end
|
|
|
|
local function compare_key_value_pair (a, b)
|
|
-- Function for comparing two key-value pairs, given as `{ key, value }`.
|
|
-- Pretty complex due to our high standards for sorting.
|
|
|
|
assert(type(a) == 'table')
|
|
assert(type(b) == 'table')
|
|
|
|
-- Get types
|
|
local type_key_a, type_key_b = type(a[1]), type(b[1])
|
|
local type_value_a, type_value_b = type(a[2]), type(b[2])
|
|
|
|
-- Tons of compare
|
|
if (1 == (type_key_a == 'number' and 1 or 0) + (type_key_b == 'number' and 1 or 0)) then
|
|
return type_key_a == 'number'
|
|
elseif (type_key_a == 'number' and type_key_b == 'number') then
|
|
return a[1] < b[1]
|
|
elseif (type_value_a ~= type_value_b) then
|
|
return VALUE_TYPE_SORT_ORDER[type_value_a] < VALUE_TYPE_SORT_ORDER[type_value_b]
|
|
elseif (type_key_a == 'string' and type_key_b == 'string') then
|
|
return alphanum_compare_strings(a[1], b[1])
|
|
elseif (type_key_a ~= type_key_b) then
|
|
return KEY_TYPE_SORT_ORDER[type_value_a] < KEY_TYPE_SORT_ORDER[type_value_b]
|
|
end
|
|
end
|
|
|
|
local function get_key_value_pairs_in_proper_order (t)
|
|
-- Generates a sequence of key value pairs, in proper order.
|
|
-- Proper order is:
|
|
-- 1. All integer keys are first, in order
|
|
-- 2. Then by value type, as defined in VALUE_TYPE_SORT_ORDER in the top.
|
|
-- 3. Then by key type.
|
|
-- 3.1. String in alphanumeric order
|
|
-- 3.2. Other wierdness.
|
|
|
|
-- Error checking
|
|
assert(type(t) == 'table')
|
|
|
|
-- Find Key-Value Pairs
|
|
local kv_pairs = {}
|
|
for key, value in pairs(t) do kv_pairs[#kv_pairs+1] = { key, value } end
|
|
|
|
-- Sort them into the correct order
|
|
table.sort(kv_pairs, compare_key_value_pair)
|
|
|
|
-- Return them
|
|
return kv_pairs
|
|
end
|
|
|
|
local function fill_holes_in_key_value_pairs (kv_pairs)
|
|
-- Fills holes in key-value pairs for a sequences with `nil` values. All
|
|
-- keys must be numbers, and key-value pairs must be sorted already.
|
|
|
|
-- Holes can sometimes appear in otherwise nicely structured sequences. We
|
|
-- want to avoid displaying a sequence as `{[1] = 1, [3] = 3}` when
|
|
-- `{1, nil, 3}` would work nicely.
|
|
|
|
-- Error checking
|
|
assert(type(kv_pairs) == 'table')
|
|
|
|
-- Add hole filling value
|
|
assert(type(kv_pairs[1][1]) == 'number')
|
|
for i = 2, #kv_pairs do
|
|
assert(type(kv_pairs[i][1]) == 'number')
|
|
for j = kv_pairs[i-1][1] + 1, kv_pairs[i][1] - 1 do
|
|
kv_pairs[#kv_pairs+1] = { j, nil }
|
|
end
|
|
end
|
|
|
|
-- Sort key-value pairs, to place above pairs into their correct locations.
|
|
table.sort(kv_pairs, compare_key_value_pair)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Formatting Util
|
|
|
|
local function width_of_strings_in_l (l, start_i, stop_i)
|
|
|
|
-- Argument fixing and Error Checking
|
|
assert(type(l) == 'table')
|
|
|
|
local start_i, stop_i = start_i or 1, stop_i or #l
|
|
|
|
assert(type(start_i) == 'number')
|
|
assert(type(start_i) == 'number')
|
|
|
|
-- Do stuff
|
|
local width = 0
|
|
for i = start_i, stop_i do
|
|
width = width + ((type(l[i]) ~= 'string') and 1 or #l[i])
|
|
end
|
|
return width
|
|
end
|
|
|
|
local function ignore_alignment_info (l, start_i, stop_i)
|
|
|
|
-- Argument fixing and Error Checking
|
|
assert(type(l) == 'table')
|
|
|
|
local start_i, stop_i = start_i or 1, stop_i or #l
|
|
|
|
assert(type(start_i) == 'number')
|
|
assert(type(start_i) == 'number')
|
|
|
|
-- Do stuff
|
|
for i = start_i, stop_i do
|
|
if type(l[i]) == 'table' and l[i][1] == 'align' then
|
|
l[i] = ''
|
|
end
|
|
end
|
|
end
|
|
|
|
local function fix_alignment (l, start_i, stop_i)
|
|
|
|
-- Argument fixing and Error Checking
|
|
assert(type(l) == 'table')
|
|
|
|
local start_i, stop_i = start_i or 1, stop_i or #l
|
|
|
|
assert(type(start_i) == 'number')
|
|
assert(type(start_i) == 'number')
|
|
|
|
-- Find maximums
|
|
local max = {}
|
|
for i = start_i, stop_i do
|
|
if type(l[i]) == 'table' and l[i][1] == 'align' then
|
|
max[ l[i][2] ] = math.max( l[i][3], max[ l[i][2] ] or 0 )
|
|
end
|
|
end
|
|
|
|
-- Insert the proper whitespace
|
|
for i = start_i, stop_i do
|
|
if type(l[i]) == 'table' and l[i][1] == 'align' then
|
|
l[i] = string.rep(' ', max[ l[i][2] ] - l[i][3])
|
|
end
|
|
end
|
|
end
|
|
|
|
local function attempt_to_align_into_columns (l, start_i, stop_i, nr_items_pr_row)
|
|
assert(type(l) == 'table')
|
|
assert(type(start_i) == 'number')
|
|
assert(type(stop_i) == 'number')
|
|
assert(type(nr_items_pr_row) == 'number')
|
|
|
|
local column = {}
|
|
---
|
|
local start_of_item_i, item_nr = nil, 0
|
|
for i = start_i, stop_i do
|
|
if type(l[i]) == 'table' and (l[i][1] == 'indent' or l[i][1] == 'seperator' or l[i][1] == 'unindent') then
|
|
if start_of_item_i then
|
|
local width_of_item = width_of_strings_in_l(l, start_of_item_i, i-1)
|
|
local column_i = (item_nr-1)%nr_items_pr_row+1
|
|
column[column_i] = math.max(column[column_i] or 0, width_of_item)
|
|
end
|
|
start_of_item_i, item_nr = i + 1, item_nr + 1
|
|
end
|
|
end
|
|
---
|
|
local width = nr_items_pr_row * 2 - 1 -- FIXME: Magic numbers: 2 = #', ', 1 = #' '
|
|
for i = 1, #column do width = width + column[i] end
|
|
--
|
|
return width, column
|
|
end
|
|
|
|
local function align_into_columns (l, start_i, stop_i)
|
|
|
|
-- Argument fixing and Error Checking
|
|
assert(type(l) == 'table')
|
|
|
|
local start_i, stop_i = start_i or 1, stop_i or #l
|
|
|
|
assert(type(start_i) == 'number')
|
|
assert(type(stop_i) == 'number')
|
|
|
|
-- Find columns
|
|
local columns = nil
|
|
for nr_items_pr_row = 10, 1, -1 do -- TODO: Do this more intelligently.
|
|
local column_width
|
|
column_width, columns = attempt_to_align_into_columns(l, start_i, stop_i, nr_items_pr_row)
|
|
if column_width <= MAX_WIDTH_FOR_SINGLE_LINE_TABLE then break end
|
|
end
|
|
|
|
-- Change alignment of columns
|
|
local start_of_item_i, item_nr = nil, 0
|
|
for i = start_i, stop_i do
|
|
if type(l[i]) == 'table' and l[i][1] == 'align' then
|
|
local column_i = (item_nr-1)%#columns+1
|
|
l[i][2] = l[i][2] .. '_column_'..column_i
|
|
elseif (l[i][1] == 'indent' or l[i][1] == 'seperator' or l[i][1] == 'unindent') then
|
|
start_of_item_i, item_nr = i + 1, item_nr + 1
|
|
end
|
|
end
|
|
|
|
-- Fix newly changed alignment
|
|
fix_alignment(l, start_i, stop_i)
|
|
|
|
-- Quick-exit on only a single column
|
|
if #columns == 1 then return end
|
|
|
|
-- Fit into columns.
|
|
local start_of_item_i, item_nr = nil, 0
|
|
for i = start_i, stop_i do
|
|
if type(l[i]) ~= 'table' then
|
|
-- Do nothing
|
|
elseif (l[i][1] == 'indent' or l[i][1] == 'seperator' or l[i][1] == 'unindent') then
|
|
if start_of_item_i and l[i][1] == 'seperator' then
|
|
local column_i = (item_nr-1)%#columns+1
|
|
if column_i ~= #columns then
|
|
local width_of_item = width_of_strings_in_l(l, start_of_item_i, i-1)
|
|
l[i] = l[i][2] .. ' ' .. (' '):rep(columns[column_i]-width_of_item)
|
|
end
|
|
end
|
|
start_of_item_i, item_nr = i + 1, item_nr + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
local function align_into_tabular_style (l, start_i, stop_i)
|
|
-- Adds alignment after seperators, to create nicely aligned tabular-format.
|
|
|
|
-- Argument fixing and Error Checking
|
|
local start_i, stop_i = start_i or 1, stop_i or #l
|
|
|
|
assert(type(l) == 'table')
|
|
assert(type(start_i) == 'number')
|
|
assert(type(stop_i) == 'number')
|
|
assert(type(l[start_i]) == 'table' and l[start_i][1] == 'indent')
|
|
assert(type(l[stop_i]) == 'table' and l[stop_i][1] == 'unindent')
|
|
|
|
-- Calculate where to insert new alignment.
|
|
local indent, key_nr, index_of_last_meta, insert_later = 0, 0, 1, {}
|
|
for i = start_i + 1, stop_i - 1 do
|
|
if type(l[i]) ~= 'table' then
|
|
-- Do nothing
|
|
elseif l[i][1] == 'indent' then
|
|
indent = indent + 1
|
|
if indent == 1 then key_nr = 1 end
|
|
index_of_last_meta = i
|
|
elseif l[i][1] == 'unindent' then
|
|
insert_later[#insert_later+1] = {'align', 'end_subtable_'..key_nr, width_of_strings_in_l(l, index_of_last_meta+1, i), i}
|
|
index_of_last_meta, key_nr = i, key_nr + 1
|
|
indent = indent - 1
|
|
elseif l[i][1] == 'seperator' and indent ~= 0 then
|
|
insert_later[#insert_later+1] = {'align', 'key_'..key_nr, width_of_strings_in_l(l, index_of_last_meta+1, i), i+1}
|
|
index_of_last_meta, key_nr = i, key_nr + 1
|
|
end
|
|
end
|
|
-- Insert new alignment.
|
|
for i = #insert_later, 1, -1 do
|
|
local dat = insert_later[i]
|
|
table.insert(l, dat[#dat], dat)
|
|
dat[#dat] = nil
|
|
end
|
|
-- Fix that alignemnt
|
|
return fix_alignment(l, start_i)
|
|
end
|
|
|
|
local function fix_seperator_info (l, indent_char)
|
|
|
|
-- Error Checking
|
|
assert(type(l) == 'table')
|
|
assert(type(indent_char) == 'string')
|
|
-- Do stuff
|
|
|
|
local display, inline_depth = 0, nil
|
|
for i = 1, #l do
|
|
if type(l[i]) ~= 'table' then
|
|
-- Do nothing
|
|
elseif l[i][1] == 'seperator' then
|
|
assert(l[i][2] == nil or type(l[i][2]) == 'string')
|
|
l[i] = (l[i][2] or '') .. (inline_depth and ' ' or ('\n' .. indent_char:rep(display)))
|
|
elseif l[i][1] == 'indent' then
|
|
display, inline_depth = display + 1, inline_depth or l[i][3] == 'inline' and display + 1 or nil
|
|
l[i] = l[i][2] .. (inline_depth and ' ' or ('\n' .. indent_char:rep(display)))
|
|
elseif l[i][1] == 'unindent' then
|
|
l[i] = (inline_depth and ' ' or ('\n' .. indent_char:rep(display-1))) .. l[i][2]
|
|
display, inline_depth = display - 1, (display ~= inline_depth) and inline_depth or nil
|
|
end
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
local analyze_structure = import 'analyze_structure'
|
|
local TABLE_TYPE = import 'common' . TABLE_TYPE
|
|
local DISPLAY = import 'common' . DISPLAY
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Key-value pair formatting.
|
|
|
|
local function format_key_and_value_string_map (key, value, display, l, format_value)
|
|
l[#l+1] = key
|
|
l[#l+1] = { 'align', 'key', #key }
|
|
l[#l+1] = ' = '
|
|
return format_value(value, display, l)
|
|
end
|
|
|
|
local function format_key_and_value_arbitr_map (key, value, display, l, format_value)
|
|
local index_before_key = #l+1
|
|
l[#l+1] = '['
|
|
format_value(key, DISPLAY.HIDE, l)
|
|
l[#l+1] = ']'
|
|
l[#l+1] = { 'align', 'key', width_of_strings_in_l(l, index_before_key) }
|
|
l[#l+1] = ' = '
|
|
return format_value(value, display, l)
|
|
end
|
|
|
|
local function format_key_and_value_sequence (key, value, display, l, format_value)
|
|
return format_value(value, display, l)
|
|
end
|
|
|
|
local TABLE_TYPE_TO_PAIR_FORMAT = {
|
|
[TABLE_TYPE.EMPTY] = format_key_and_value_sequence,
|
|
[TABLE_TYPE.SEQUENCE] = format_key_and_value_sequence,
|
|
[TABLE_TYPE.SET] = format_key_and_value_arbitr_map,
|
|
[TABLE_TYPE.MIXED] = format_key_and_value_arbitr_map,
|
|
[TABLE_TYPE.STRING_MAP] = format_key_and_value_string_map,
|
|
[TABLE_TYPE.PURE_MAP] = format_key_and_value_arbitr_map,
|
|
}
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Table formatting
|
|
|
|
local function format_table (t, display, l, format_value)
|
|
-- Error Checking
|
|
assert(type(t) == 'table')
|
|
assert(type(display) == 'number' and type(l) == 'table')
|
|
|
|
-- Find table info
|
|
if not l.info[t] then analyze_structure(t, display, l.info) end
|
|
local table_info = l.info[t]
|
|
assert(table_info)
|
|
|
|
-- If empty or not a lot of space, give a small represetation: `{...}`
|
|
if table_info.type == TABLE_TYPE.EMPTY or display <= DISPLAY.SMALL then
|
|
l '{'
|
|
if l.options._table_addr_comment then l[#l+1] = ' --[[' .. table_info.address .. ']] ' end
|
|
if table_info.type ~= TABLE_TYPE.EMPTY then l[#l+1] = '...' end
|
|
return l '}'
|
|
end
|
|
|
|
-- Get key-value pairs, and possibly fill holes.
|
|
local key_value_pairs = get_key_value_pairs_in_proper_order(t)
|
|
if table_info.type == TABLE_TYPE.SEQUENCE and l.info[t].has_holes then
|
|
fill_holes_in_key_value_pairs(key_value_pairs)
|
|
end
|
|
|
|
-- Find pair formatting function
|
|
local pair_format_func = TABLE_TYPE_TO_PAIR_FORMAT[table_info.type]
|
|
local start_of_table_i = #l + 1
|
|
assert(pair_format_func)
|
|
|
|
-- Begin formatting table.
|
|
local next_display = display - 1
|
|
if (l.info[t].value_types.nr_types >= 2) then next_display = DISPLAY.SMALL end
|
|
|
|
l[#l+1] = {'indent', '{'}
|
|
if l.options._table_addr_comment then l[#l+1], l[#l+2] = '--[['..table_info.address..']]', {'seperator'} end
|
|
for _, pair in ipairs(key_value_pairs) do
|
|
pair_format_func(pair[1], pair[2], next_display, l, format_value)
|
|
l[#l+1] = {'seperator', ','}
|
|
end
|
|
if l[#l][1] == 'seperator' then l[#l] = nil end
|
|
l[#l+1] = {'unindent', '}'}
|
|
|
|
-- Decide for short or long table formatting.
|
|
local table_width = width_of_strings_in_l(l, start_of_table_i)
|
|
if table_width <= MAX_WIDTH_FOR_SINGLE_LINE_TABLE then
|
|
-- Is short table: Ignore "width of key".
|
|
l[start_of_table_i][3] = 'inline'
|
|
ignore_alignment_info(l, start_of_table_i)
|
|
elseif table_info.is_leaf_node then
|
|
-- Is leaf node: Can format into columns.
|
|
-- NOTE: Currently we only allow leaf-nodes to format into columns, due
|
|
-- to issues with table alignment.
|
|
align_into_columns(l, start_of_table_i)
|
|
elseif table_info.is_tabular then
|
|
align_into_tabular_style(l, start_of_table_i, #l)
|
|
else
|
|
-- Is long table: Fix whitespace alignment
|
|
fix_alignment(l, start_of_table_i)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Coroutine formatting
|
|
|
|
local function format_coroutine (value, _, l)
|
|
-- Formats a coroutine. Unfortunantly we cannot gather a lot of information
|
|
-- about coroutines.
|
|
|
|
-- Error check
|
|
assert(type(value) == 'thread')
|
|
assert(type(l) == 'table')
|
|
|
|
-- Do stuff
|
|
l[#l+1] = coroutine.status(value)
|
|
l[#l+1] = ' coroutine: '
|
|
l[#l+1] = tostring(value):sub(9)
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Primitive formatting
|
|
|
|
local function format_primitive (value, _, l)
|
|
-- Error check
|
|
assert(type(l) == 'table')
|
|
-- Do stuff
|
|
l[#l+1] = tostring(value)
|
|
end
|
|
|
|
local TYPE_TO_FORMAT_FUNC = {
|
|
['nil'] = format_primitive,
|
|
['boolean'] = format_primitive,
|
|
['number'] = import 'number',
|
|
['string'] = import 'pstring',
|
|
['thread'] = format_coroutine,
|
|
['table'] = format_table,
|
|
['function'] = import 'function',
|
|
|
|
-- TODO
|
|
['userdata'] = format_primitive,
|
|
['cdata'] = import 'cdata', -- Luajit exclusive ?
|
|
}
|
|
|
|
local function format_value (value, display, l)
|
|
assert(type(display) == 'number' and type(l) == 'table')
|
|
local formatting = TYPE_TO_FORMAT_FUNC[type(value)]
|
|
--print(value, formatting)
|
|
if formatting then
|
|
formatting(value, display, l, format_value)
|
|
else
|
|
error(ERROR_UNKNOWN_TYPE:format(type(value), tostring(value)), 2)
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- StringBuilder
|
|
|
|
local StringBuilder = {}
|
|
StringBuilder.__index = StringBuilder
|
|
StringBuilder.__call = function (l, s) l[#l+1] = s end
|
|
setmetatable(StringBuilder, {
|
|
__call = function() return setmetatable({}, StringBuilder) end
|
|
})
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
local DEBUG_OPTION_USED = { }
|
|
|
|
local KNOWN_OPTIONS = {
|
|
_table_addr_comment = { type = 'boolean', default = false, debug = 'debug' }, -- TODO: Maybe automatically display table address when display = 0?
|
|
|
|
indent = { type = 'string', default = ' ' },
|
|
}
|
|
|
|
local function ensure_that_all_options_are_known (input_options)
|
|
-- Goes through all the given options, throws error if one is unknown, gives
|
|
-- warning if debug or experimental. Creates a clone of the given table, to
|
|
-- avoid defaults leaking.
|
|
|
|
-- Error check that options were table
|
|
assert(type(input_options) == 'table')
|
|
|
|
-- Error check options
|
|
for option_name, option_value in pairs(input_options) do
|
|
if not KNOWN_OPTIONS[option_name] then
|
|
error(('[pretty]: Unknown option: %s. Was given value %s (%s)'):format(option_name, option_value, type(option_value)), 2)
|
|
elseif type(option_value) ~= KNOWN_OPTIONS[option_name].type then
|
|
error(('[pretty]: Bad value given to option %s: %s (%s). Expected value of type %s'):format(option_name, option_value, type(option_value), KNOWN_OPTIONS[option_name].type), 2)
|
|
elseif KNOWN_OPTIONS[option_name].accepted and not KNOWN_OPTIONS[option_name].accepted[option_value] then
|
|
error(('[pretty]: Bad value given to option %s: %s (%s).'):format(option_name, option_value, type(option_value)), 2)
|
|
elseif KNOWN_OPTIONS[option_name].debug and not DEBUG_OPTION_USED[option_name] then
|
|
DEBUG_OPTION_USED[option_name] = true
|
|
print(('[pretty]: Using %s option "%s".\n Please note that this option may change at any time. It is not stable,\n not tested, and may indeed break or be removed without warning.'):format(KNOWN_OPTIONS[option_name].debug, option_name))
|
|
end
|
|
end
|
|
|
|
-- Create output options
|
|
local output_options = {}
|
|
-- Assign default values
|
|
for option_name, option_info in pairs(KNOWN_OPTIONS) do
|
|
if input_options[option_name] ~= nil then
|
|
output_options[option_name] = input_options[option_name]
|
|
else
|
|
output_options[option_name] = option_info.default
|
|
end
|
|
end
|
|
-- Returns input_options
|
|
return output_options
|
|
end
|
|
|
|
local function pretty_format (value, options)
|
|
-- Error checking
|
|
local options = ensure_that_all_options_are_known(options or {})
|
|
|
|
-- Setup StringBuilder
|
|
local l = StringBuilder()
|
|
l.options = options
|
|
l.info = analyze_structure(value, 3)
|
|
|
|
-- Format value.
|
|
format_value(value, DISPLAY.EXPAND, l)
|
|
|
|
-- If any alignment info still exists, ignore it
|
|
fix_seperator_info(l, l.options.indent)
|
|
ignore_alignment_info(l)
|
|
|
|
return table.concat(l, '')
|
|
end
|
|
|
|
return pretty_format
|