1
0
pretty/pretty.lua

829 lines
30 KiB
Lua

--- Pretty
--
-- `pretty` is an advanced pretty printer for [Lua](lua.org). It's primarily a
-- debugging tool, aiming for human readability, by detecting pattern in the input
-- data, and creating an output string utilizing and highlighting those patterns.
--
-- ## Code Example
--
-- Setup is simple, use `pretty = require 'pretty'`, and you're good to go.
--
-- ```lua
-- > print(pretty( { 1, 2, 3 } ))
-- { 1, 2, 3 }
--
-- > print(pretty( { hello = 'world', num = 42 } ))
-- {
-- num = 42
-- hello = 'world'
-- }
--
-- > print(pretty( { abs = math.abs, max = math.max, some = function() end } ))
-- {
-- abs = builtin function (x) ... end
-- max = builtin function (x, ...) ... end
-- some = function () ... end
-- }
--
-- > print(pretty( math.abs ))
-- builtin function (x)
-- -- math.abs
-- -- Returns the absolute value of x
--
-- ...
-- end
-- ```
--
-- ## Motivation
--
-- This project is the outcome of my frustration with existing pretty printers, and
-- a desire to expand upon the pretty printer I developed for
-- [Xenoterm](https://gitfub.space/takunomi/Xenoterm). The original Xenoterm pretty
-- printer was much simpler than `pretty` - and the current is even simpler - but
-- the enhancements I make, when compared to other pretty printers, inspired me to
-- create `pretty`.
--
-- `pretty` sorts it's priorities like so:
--
-- 1. Human readability.
-- 2. Lua-compatible output.
-- 3. Customization.
--
-- I'd rather have good defaults than provide a ton of customization options. If an
-- structure avoids easy representation in Lua, I'd rather extend the syntax, than
-- lose the info.
--
-- Another aspect where `pretty` shines is in exploratory programming, when
-- attempting to avoid reliance on outside documentation. The amount of information
-- `pretty` exposes varies by the data you are inspecting. If you're inspecting
-- a list of functions, their function signatures are visible, but if you're
-- inspecting a single function, documentation and source location may appear if
-- available.
--
-- ## Features
--
-- - Written in good-old pureblood Lua, with support for PUC Lua 5.0+ and
-- LuaJIT 2.0+.
-- - Redefining what it means to be "human readable":
-- * Is multi-line centric, to aid readablitiy.
-- * Indention and alignment of keys-value pairs.
-- * Keys-value pairs are [properly](http://www.davekoelle.com/alphanum.html)
-- sorted by key type and thereafter alphabetically.
-- * The format and structure of output changes depending upon the input.
-- Maps appear differently to deeply nested tables to long sequences
-- with short strings to short lists.
-- * Uses the standard `debug` library to gain information about functions
-- and other advanced structures.
--
-- ## Installation
--
-- `pretty` is loadable directly with `require`. Either clone or download this
-- repository. Where you place it, depends upon what you want to do:
--
-- 1. **You want `pretty` in a specific project**: Place the `pretty` folder
-- somewhere in your project, and `require` it from one of your project files.
-- 2. **You want `pretty` on your system**: Place the `pretty` folder such that
-- it's visible from your Lua-path. On my system this might be
-- `/usr/local/share/lua/5.1/`. Now you can `require` it from anywhere.
--
-- ## API Documentation
--
-- `pretty` exposes a single function, the `pretty` function itself. It's function
-- signature is `pretty(value, options)`. `value` can be any Lua value. `options`
-- must be a table.
--
-- ### List of options
--
-- `pretty` is sure to complain if you give it an unknown option, or if you give an
-- option a bad value.
--
-- - `indent: string`: The string to indent with. Four spaces by default.
--
-- ## TODO
--
-- Tasks to be done before `pretty` can be called version 1.0.0, in order of
-- priority:
--
-- - Add a dedicated unicode submodule, to handle some minor alignment and
-- character escaping issues. `pretty` should escape all malformed unicode
-- sequences.
-- - Align numbers towards right for tabular views.
-- - Add support for `setmetatable`, and exploring values in metatables.
-- - Provide nice formatting for `cdata` datatype in LuaJIT.
-- - Find a better name than `pretty`.
-- - Enhance internal structure some amount. See `TODO` markers in files.
--
-- It would be nice to have the following, but these are secondary:
--
-- - Add option for colored output. Primarily syntax highlighting, but also
-- [BlueJ-style](www.bluej.org/about.html) scope highlighting, with some faint
-- background colors.
-- - Expand on the comment output in output, for `__tostring` methods, and global
-- namespaces like `io` or `math`.
-- - Fit output within a predefined width limit. Default to 80.
-- - Look into tool for understanding complex structures with recursive
-- definitions. Whatever modes are thought up, they should be automatic modes,
-- not an options. Should at least include modes for self-referential tables
-- and Directed-Acyclic-Graphs.
--
-- ## Alternative pretty printers
--
-- `pretty` is large, slow, and requires the debug library to work. It's not
-- designed for serialization purposes, nor is it concerned with offering the same
-- level of customization as other libraries do.
--
-- If you want a sleek, fast, customizable or embeddable library, there are
-- thankfully other options.
--
-- - [inspect.lua](github.com/kikito/inspect.lua): One of the classic debugging
-- pretty printers.
-- - [pprint.lua](github.com/jagt/pprint.lua): Reimplementation of `inspect.lua`
-- - [serpent](github.com/pkulchenko/serpent): Advanced and fast pretty printer.
-- - [pluto](lua-users.org/wiki/PlutoLibrary): Can serialize arbitrary parts of
-- Lua, including functions, upvalues, and proper lexical scoping. Not written
-- in native Lua.
-- - [binser](github.com/bakpakin/binser): Library for special purpose
-- serialization.
--
-- Even more are available at [the lua-users wiki](lua-users.org/wiki/TableSerialization).
--
-- ## 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 TERMINAL_WIDTH = 80
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
TERMINAL_WIDTH = term_width
--MAX_WIDTH_FOR_SINGLE_LINE_TABLE = term_width * (2 / 3)
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,
}
local COLUMN_SEPERATION = 1
--------------------------------------------------------------------------------
-- Key-value-pair Util
local function padnum(minus, dec, zeroes, n)
return dec == '.' and ("%.12f"):format(dec..zeroes..n) or ("%s%03d%s"):format(minus == '' and ':' or minus, #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("(%-?)(%.?)(0*)(%d+)", padnum)..("\0%3d"):format(#b)
local b_padded = b:gsub("(%-?)(%.?)(0*)(%d+)", padnum)..("\0%3d"):format(#a)
-- Correction for sorting of negative numbers:
if a_padded:sub(1,1) == '-' and b_padded:sub(1,1) == '-' then
a_padded, b_padded = b_padded, a_padded
end
--
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 length_of_utf8_string = import 'common' . utf8_string_length
local width_of_strings_in_l = import 'common' . width_of_strings_in_l
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(stop_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 insert_alignment_estimations (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 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].est_width = max[ l[i][2] ] - l[i][3]
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(stop_i) == 'number')
-- Find whitespace to insert
insert_alignment_estimations(l, start_i, stop_i)
-- Insert whitespace
for i = start_i, stop_i do
if type(l[i]) == 'table' and l[i][1] == 'align' then
l[i] = string.rep(' ', l[i].est_width)
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')
insert_alignment_estimations(l, start_i, stop_i)
-- 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 <= l.options.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(COLUMN_SEPERATION+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 separators, 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', length_of_utf8_string(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 <= l.options.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.
-- Only if long or sequence.
-- 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)]
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 = ' ' },
max_output_width = { type = 'number', default = TERMINAL_WIDTH }
}
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
-- Calculate derived settings
output_options.max_width_for_single_line_table = MAX_WIDTH_FOR_SINGLE_LINE_TABLE -- TODO: Make dynamic
-- Returns input_options
return output_options
end
local function length_of_longest_line_in_text (text)
assert(type(text) == 'string')
local longest_line_len = text:match '([^\n]*)$' : len()
for line in text:gmatch '(.-)\n' do
longest_line_len = math.max(longest_line_len, line:len())
end
return longest_line_len
end
local function internal_warning (fmt, ...)
io.stderr:write('[pretty/internal]: '..string.format(fmt, ...))
end
local function assert_pretty_result (repr, options)
assert(type(repr) == 'string')
assert(type(options) == 'table')
-- Determine length of longest line in output
local max_width = length_of_longest_line_in_text(repr)
if max_width > options.max_output_width then
internal_warning('Internal assertion failed. Width of output was %i, but should be less than %i.', max_width, options.max_output_width)
end
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)
-- Concat and perform last assertions.
local repr = table.concat(l, '')
assert_pretty_result(repr, options)
-- Return
return repr
end
return pretty_format