2024-07-10 17:20:58 +00:00
--- 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
2017-07-20 08:38:24 +00:00
2017-07-17 19:33:11 +00:00
--------------------------------------------------------------------------------
2017-07-24 09:49:45 +00:00
-- Import files
2017-07-17 19:33:11 +00:00
2017-07-24 09:49:45 +00:00
local import
2017-01-05 12:37:44 +00:00
do
local thispath = ... and select ( ' 1 ' , ... ) : match ( ' .+%. ' ) or ' '
2017-07-24 09:49:45 +00:00
import = function ( name , ignore_failure ) return require ( thispath .. name ) end
2017-01-05 12:37:44 +00:00
end
2017-07-20 17:20:29 +00:00
--------------------------------------------------------------------------------
-- Constants
2016-12-29 17:40:30 +00:00
2016-12-29 14:33:43 +00:00
local ERROR_UNKNOWN_TYPE = [ [
[ pretty ] : Attempting to format unsupported value of type " %s " .
A native formatting of the value is : % s
2017-04-03 09:24:51 +00:00
We are attempting to cover all Lua features , so please report this bug , so we can improve .
] ]
2016-12-29 11:11:48 +00:00
2017-12-11 10:04:11 +00:00
local TERMINAL_WIDTH = 80
2017-04-14 10:19:23 +00:00
local MAX_WIDTH_FOR_SINGLE_LINE_TABLE = 38
2017-12-11 10:04:11 +00:00
2017-10-09 11:05:28 +00:00
if io and io.popen then
local f = io.popen " tput cols "
local term_width = f : read ' *n '
f : close ( )
2017-12-11 10:04:11 +00:00
if term_width then
TERMINAL_WIDTH = term_width
--MAX_WIDTH_FOR_SINGLE_LINE_TABLE = term_width * (2 / 3)
end
2017-10-09 11:05:28 +00:00
end
2016-12-28 23:51:07 +00:00
2017-04-05 11:10:23 +00:00
local KEY_TYPE_SORT_ORDER = {
[ ' number ' ] = 0 ,
[ ' string ' ] = 1 ,
[ ' boolean ' ] = 2 ,
[ ' table ' ] = 3 ,
[ ' userdata ' ] = 4 ,
[ ' thread ' ] = 5 ,
[ ' function ' ] = 6 ,
}
local VALUE_TYPE_SORT_ORDER = {
2016-12-28 23:51:07 +00:00
[ ' nil ' ] = 0 ,
[ ' boolean ' ] = 1 ,
[ ' number ' ] = 2 ,
[ ' string ' ] = 3 ,
[ ' table ' ] = 4 ,
[ ' userdata ' ] = 5 ,
[ ' thread ' ] = 6 ,
[ ' function ' ] = 7 ,
}
2017-11-18 12:23:43 +00:00
local COLUMN_SEPERATION = 1
2016-12-28 23:51:07 +00:00
--------------------------------------------------------------------------------
2017-07-20 17:20:29 +00:00
-- Key-value-pair Util
2016-12-28 23:51:07 +00:00
2017-10-22 11:06:21 +00:00
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 )
2016-12-28 23:51:07 +00:00
end
local function alphanum_compare_strings ( a , b )
2017-07-18 11:38:05 +00:00
-- Compares two strings alphanumerically.
assert ( type ( a ) == ' string ' )
assert ( type ( b ) == ' string ' )
2017-10-22 11:06:21 +00:00
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
--
2017-06-25 11:16:34 +00:00
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
2016-12-28 23:51:07 +00:00
end
2017-07-18 11:38:05 +00:00
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 ' )
2016-12-28 23:51:07 +00:00
-- 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
2017-04-05 11:10:23 +00:00
if ( 1 == ( type_key_a == ' number ' and 1 or 0 ) + ( type_key_b == ' number ' and 1 or 0 ) ) then
return type_key_a == ' number '
2017-01-16 15:10:10 +00:00
elseif ( type_key_a == ' number ' and type_key_b == ' number ' ) then
return a [ 1 ] < b [ 1 ]
2017-04-05 11:10:23 +00:00
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 ]
2016-12-28 23:51:07 +00:00
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:
2017-04-05 11:10:23 +00:00
-- 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.
2016-12-28 23:51:07 +00:00
2017-06-05 20:34:23 +00:00
-- Error checking
assert ( type ( t ) == ' table ' )
2017-07-25 13:37:37 +00:00
-- Find Key-Value Pairs
local kv_pairs = { }
for key , value in pairs ( t ) do kv_pairs [ # kv_pairs + 1 ] = { key , value } end
2016-12-28 23:51:07 +00:00
2017-07-25 13:37:37 +00:00
-- Sort them into the correct order
table.sort ( kv_pairs , compare_key_value_pair )
2016-12-28 23:51:07 +00:00
2017-07-25 13:37:37 +00:00
-- Return them
return kv_pairs
2016-12-28 23:51:07 +00:00
end
2017-07-25 13:37:37 +00:00
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.
2017-06-05 20:34:23 +00:00
2017-07-25 13:37:37 +00:00
-- 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.
2017-06-05 20:34:23 +00:00
2017-07-25 13:37:37 +00:00
-- 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 }
2017-04-14 08:56:38 +00:00
end
end
2017-07-25 13:37:37 +00:00
-- Sort key-value pairs, to place above pairs into their correct locations.
table.sort ( kv_pairs , compare_key_value_pair )
2017-04-14 08:56:38 +00:00
end
2017-07-20 17:20:29 +00:00
--------------------------------------------------------------------------------
-- Formatting Util
2016-12-28 23:51:07 +00:00
2017-10-22 12:26:19 +00:00
local length_of_utf8_string = import ' common ' . utf8_string_length
2017-11-18 12:23:43 +00:00
local width_of_strings_in_l = import ' common ' . width_of_strings_in_l
2017-10-22 12:26:19 +00:00
2017-11-18 12:23:43 +00:00
local function ignore_alignment_info ( l , start_i , stop_i )
2017-06-05 20:34:23 +00:00
-- 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 ' )
2017-11-18 12:23:43 +00:00
assert ( type ( stop_i ) == ' number ' )
2017-06-05 20:34:23 +00:00
-- Do stuff
for i = start_i , stop_i do
2017-11-18 12:23:43 +00:00
if type ( l [ i ] ) == ' table ' and l [ i ] [ 1 ] == ' align ' then
l [ i ] = ' '
end
2017-01-05 12:44:47 +00:00
end
end
2017-11-18 12:23:43 +00:00
local function insert_alignment_estimations ( l , start_i , stop_i )
2017-06-05 20:34:23 +00:00
-- 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 ' )
2017-11-18 12:23:43 +00:00
assert ( type ( stop_i ) == ' number ' )
2017-06-05 20:34:23 +00:00
2017-11-18 12:23:43 +00:00
-- Find maximums
local max = { }
2017-06-05 20:34:23 +00:00
for i = start_i , stop_i do
2017-04-14 12:01:01 +00:00
if type ( l [ i ] ) == ' table ' and l [ i ] [ 1 ] == ' align ' then
2017-11-18 12:23:43 +00:00
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 ]
2017-01-05 13:28:31 +00:00
end
end
end
local function fix_alignment ( l , start_i , stop_i )
2017-06-05 20:34:23 +00:00
-- Argument fixing and Error Checking
assert ( type ( l ) == ' table ' )
2017-04-14 10:19:23 +00:00
local start_i , stop_i = start_i or 1 , stop_i or # l
2017-06-05 20:34:23 +00:00
assert ( type ( start_i ) == ' number ' )
2017-11-18 12:23:43 +00:00
assert ( type ( stop_i ) == ' number ' )
2017-06-05 20:34:23 +00:00
2017-11-18 12:23:43 +00:00
-- Find whitespace to insert
insert_alignment_estimations ( l , start_i , stop_i )
2017-01-05 13:28:31 +00:00
2017-11-18 12:23:43 +00:00
-- Insert whitespace
2017-01-05 13:28:31 +00:00
for i = start_i , stop_i do
2017-04-14 12:01:01 +00:00
if type ( l [ i ] ) == ' table ' and l [ i ] [ 1 ] == ' align ' then
2017-11-18 12:23:43 +00:00
l [ i ] = string.rep ( ' ' , l [ i ] . est_width )
2017-01-05 13:28:31 +00:00
end
end
end
2016-12-29 15:54:31 +00:00
2017-07-20 10:01:26 +00:00
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 ' )
2017-11-18 12:23:43 +00:00
assert ( type ( stop_i ) == ' number ' )
2017-07-20 10:01:26 +00:00
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
2017-07-22 14:36:32 +00:00
assert ( type ( start_i ) == ' number ' )
assert ( type ( stop_i ) == ' number ' )
2017-07-20 10:01:26 +00:00
2017-11-18 12:23:43 +00:00
insert_alignment_estimations ( l , start_i , stop_i )
2017-07-20 10:01:26 +00:00
-- 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 )
2017-12-11 10:04:11 +00:00
if column_width <= l.options . max_width_for_single_line_table then break end
2017-07-20 10:01:26 +00:00
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 )
2017-11-18 12:23:43 +00:00
l [ i ] = l [ i ] [ 2 ] .. ( ' ' ) : rep ( COLUMN_SEPERATION + columns [ column_i ] - width_of_item )
2017-07-20 10:01:26 +00:00
end
end
start_of_item_i , item_nr = i + 1 , item_nr + 1
end
end
end
2017-07-22 14:36:32 +00:00
local function align_into_tabular_style ( l , start_i , stop_i )
2017-11-18 12:23:43 +00:00
-- Adds alignment after separators, to create nicely aligned tabular-format.
2017-07-22 14:36:32 +00:00
-- Argument fixing and Error Checking
local start_i , stop_i = start_i or 1 , stop_i or # l
2017-11-18 12:23:43 +00:00
assert ( type ( l ) == ' table ' )
assert ( type ( start_i ) == ' number ' )
assert ( type ( stop_i ) == ' number ' )
2017-07-22 14:36:32 +00:00
assert ( type ( l [ start_i ] ) == ' table ' and l [ start_i ] [ 1 ] == ' indent ' )
2017-11-18 12:23:43 +00:00
assert ( type ( l [ stop_i ] ) == ' table ' and l [ stop_i ] [ 1 ] == ' unindent ' )
2017-07-22 14:36:32 +00:00
-- 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
2017-07-25 15:49:06 +00:00
local function fix_seperator_info ( l , indent_char )
2017-06-05 20:34:23 +00:00
-- Error Checking
assert ( type ( l ) == ' table ' )
assert ( type ( indent_char ) == ' string ' )
-- Do stuff
2017-07-25 15:49:06 +00:00
local display , inline_depth = 0 , nil
2017-04-14 12:01:01 +00:00
for i = 1 , # l do
2017-04-14 10:19:23 +00:00
if type ( l [ i ] ) ~= ' table ' then
-- Do nothing
elseif l [ i ] [ 1 ] == ' seperator ' then
2017-06-11 11:53:06 +00:00
assert ( l [ i ] [ 2 ] == nil or type ( l [ i ] [ 2 ] ) == ' string ' )
2017-07-25 15:49:06 +00:00
l [ i ] = ( l [ i ] [ 2 ] or ' ' ) .. ( inline_depth and ' ' or ( ' \n ' .. indent_char : rep ( display ) ) )
2017-04-14 10:19:23 +00:00
elseif l [ i ] [ 1 ] == ' indent ' then
2017-07-25 15:49:06 +00:00
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 ) ) )
2017-04-14 10:19:23 +00:00
elseif l [ i ] [ 1 ] == ' unindent ' then
2017-07-25 15:49:06 +00:00
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
2017-04-14 10:19:23 +00:00
end
end
end
2017-07-24 09:49:45 +00:00
--------------------------------------------------------------------------------
2017-10-09 11:54:56 +00:00
local analyze_structure = import ' analyze_structure '
local TABLE_TYPE = import ' common ' . TABLE_TYPE
local DISPLAY = import ' common ' . DISPLAY
2017-07-24 09:49:45 +00:00
2016-12-28 23:51:07 +00:00
--------------------------------------------------------------------------------
2017-07-25 13:37:37 +00:00
-- Key-value pair formatting.
2016-12-28 23:51:07 +00:00
2017-07-25 15:49:06 +00:00
local function format_key_and_value_string_map ( key , value , display , l , format_value )
2016-12-29 04:34:44 +00:00
l [ # l + 1 ] = key
2017-10-22 12:26:19 +00:00
l [ # l + 1 ] = { ' align ' , ' key ' , length_of_utf8_string ( key ) }
2016-12-29 04:34:44 +00:00
l [ # l + 1 ] = ' = '
2017-07-25 15:49:06 +00:00
return format_value ( value , display , l )
2016-12-28 23:51:07 +00:00
end
2017-07-25 15:49:06 +00:00
local function format_key_and_value_arbitr_map ( key , value , display , l , format_value )
2017-01-05 12:44:47 +00:00
local index_before_key = # l + 1
2016-12-29 04:34:44 +00:00
l [ # l + 1 ] = ' [ '
2017-07-25 15:49:06 +00:00
format_value ( key , DISPLAY.HIDE , l )
2016-12-29 04:34:44 +00:00
l [ # l + 1 ] = ' ] '
2017-04-14 12:01:01 +00:00
l [ # l + 1 ] = { ' align ' , ' key ' , width_of_strings_in_l ( l , index_before_key ) }
2016-12-29 04:34:44 +00:00
l [ # l + 1 ] = ' = '
2017-07-25 15:49:06 +00:00
return format_value ( value , display , l )
2016-12-28 23:51:07 +00:00
end
2017-07-25 15:49:06 +00:00
local function format_key_and_value_sequence ( key , value , display , l , format_value )
return format_value ( value , display , l )
2016-12-29 10:37:31 +00:00
end
2016-12-28 23:51:07 +00:00
2016-12-29 11:11:48 +00:00
local TABLE_TYPE_TO_PAIR_FORMAT = {
2017-04-14 08:56:38 +00:00
[ TABLE_TYPE.EMPTY ] = format_key_and_value_sequence ,
2017-04-03 09:24:51 +00:00
[ TABLE_TYPE.SEQUENCE ] = format_key_and_value_sequence ,
2017-04-14 08:56:38 +00:00
[ TABLE_TYPE.SET ] = format_key_and_value_arbitr_map ,
[ TABLE_TYPE.MIXED ] = format_key_and_value_arbitr_map ,
2017-04-03 09:24:51 +00:00
[ TABLE_TYPE.STRING_MAP ] = format_key_and_value_string_map ,
[ TABLE_TYPE.PURE_MAP ] = format_key_and_value_arbitr_map ,
2016-12-29 11:11:48 +00:00
}
2016-12-28 23:51:07 +00:00
2017-07-25 13:37:37 +00:00
-------------------------------------------------------------------------------
-- Table formatting
2016-12-29 10:37:31 +00:00
2017-07-25 15:49:06 +00:00
local function format_table ( t , display , l , format_value )
2017-06-05 21:24:24 +00:00
-- Error Checking
2017-06-05 20:34:23 +00:00
assert ( type ( t ) == ' table ' )
2017-07-25 15:49:06 +00:00
assert ( type ( display ) == ' number ' and type ( l ) == ' table ' )
2017-06-05 20:34:23 +00:00
2017-07-25 15:49:06 +00:00
-- Find table info
if not l.info [ t ] then analyze_structure ( t , display , l.info ) end
2017-06-24 16:36:01 +00:00
local table_info = l.info [ t ]
assert ( table_info )
2017-06-05 20:47:07 +00:00
2017-07-25 15:49:06 +00:00
-- If empty or not a lot of space, give a small represetation: `{...}`
if table_info.type == TABLE_TYPE.EMPTY or display <= DISPLAY.SMALL then
2017-07-20 08:38:24 +00:00
l ' { '
if l.options . _table_addr_comment then l [ # l + 1 ] = ' --[[ ' .. table_info.address .. ' ]] ' end
2017-07-21 11:12:02 +00:00
if table_info.type ~= TABLE_TYPE.EMPTY then l [ # l + 1 ] = ' ... ' end
2017-07-20 08:38:24 +00:00
return l ' } '
2017-06-05 20:47:07 +00:00
end
2017-07-25 15:49:06 +00:00
2017-06-05 20:47:07 +00:00
-- Get key-value pairs, and possibly fill holes.
2016-12-29 11:11:48 +00:00
local key_value_pairs = get_key_value_pairs_in_proper_order ( t )
2017-04-14 11:06:43 +00:00
if table_info.type == TABLE_TYPE.SEQUENCE and l.info [ t ] . has_holes then
2017-04-14 08:56:38 +00:00
fill_holes_in_key_value_pairs ( key_value_pairs )
end
2016-12-29 10:37:31 +00:00
2017-07-20 08:38:24 +00:00
-- Find pair formatting function
2017-06-05 20:47:07 +00:00
local pair_format_func = TABLE_TYPE_TO_PAIR_FORMAT [ table_info.type ]
2017-04-14 10:19:23 +00:00
local start_of_table_i = # l + 1
2017-06-05 20:47:07 +00:00
assert ( pair_format_func )
2016-12-29 10:37:31 +00:00
2017-06-05 20:47:07 +00:00
-- Begin formatting table.
2017-07-25 15:49:06 +00:00
local next_display = display - 1
if ( l.info [ t ] . value_types.nr_types >= 2 ) then next_display = DISPLAY.SMALL end
2017-06-05 20:47:07 +00:00
l [ # l + 1 ] = { ' indent ' , ' { ' }
2017-06-05 22:22:30 +00:00
if l.options . _table_addr_comment then l [ # l + 1 ] , l [ # l + 2 ] = ' --[[ ' .. table_info.address .. ' ]] ' , { ' seperator ' } end
2016-12-28 23:51:07 +00:00
for _ , pair in ipairs ( key_value_pairs ) do
2017-07-25 15:49:06 +00:00
pair_format_func ( pair [ 1 ] , pair [ 2 ] , next_display , l , format_value )
2017-06-11 11:53:06 +00:00
l [ # l + 1 ] = { ' seperator ' , ' , ' }
2016-12-28 23:51:07 +00:00
end
2017-06-11 11:53:06 +00:00
if l [ # l ] [ 1 ] == ' seperator ' then l [ # l ] = nil end
2017-04-14 12:01:01 +00:00
l [ # l + 1 ] = { ' unindent ' , ' } ' }
2017-01-05 12:37:44 +00:00
2017-06-05 20:47:07 +00:00
-- Decide for short or long table formatting.
2017-04-14 10:19:23 +00:00
local table_width = width_of_strings_in_l ( l , start_of_table_i )
2017-12-11 10:04:11 +00:00
if table_width <= l.options . max_width_for_single_line_table then
2017-07-22 14:36:32 +00:00
-- Is short table: Ignore "width of key".
2017-04-14 12:01:01 +00:00
l [ start_of_table_i ] [ 3 ] = ' inline '
2017-04-14 10:19:23 +00:00
ignore_alignment_info ( l , start_of_table_i )
2017-07-20 10:01:26 +00:00
elseif table_info.is_leaf_node then
-- Is leaf node: Can format into columns.
2017-12-11 10:04:11 +00:00
-- Only if long or sequence.
2017-07-20 10:01:26 +00:00
-- 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 )
2017-07-22 14:36:32 +00:00
elseif table_info.is_tabular then
align_into_tabular_style ( l , start_of_table_i , # l )
2017-04-14 10:19:23 +00:00
else
-- Is long table: Fix whitespace alignment
fix_alignment ( l , start_of_table_i )
end
2016-12-28 23:51:07 +00:00
end
2017-07-25 13:37:37 +00:00
-------------------------------------------------------------------------------
-- Coroutine formatting
2016-12-28 23:51:07 +00:00
2017-07-25 15:49:06 +00:00
local function format_coroutine ( value , _ , l )
2017-07-18 11:38:05 +00:00
-- Formats a coroutine. Unfortunantly we cannot gather a lot of information
-- about coroutines.
2017-06-05 21:24:24 +00:00
-- Error check
assert ( type ( value ) == ' thread ' )
2017-07-25 15:49:06 +00:00
assert ( type ( l ) == ' table ' )
2017-07-18 11:38:05 +00:00
2017-06-05 21:24:24 +00:00
-- Do stuff
2017-01-05 12:37:44 +00:00
l [ # l + 1 ] = coroutine.status ( value )
l [ # l + 1 ] = ' coroutine: '
l [ # l + 1 ] = tostring ( value ) : sub ( 9 )
2016-12-29 14:33:43 +00:00
end
2017-07-25 13:37:37 +00:00
-------------------------------------------------------------------------------
-- Primitive formatting
2017-07-22 14:36:32 +00:00
2017-07-25 15:49:06 +00:00
local function format_primitive ( value , _ , l )
2017-06-05 21:24:24 +00:00
-- Error check
2017-07-25 15:49:06 +00:00
assert ( type ( l ) == ' table ' )
2017-06-05 21:24:24 +00:00
-- Do stuff
2017-01-05 12:37:44 +00:00
l [ # l + 1 ] = tostring ( value )
2016-12-29 14:33:43 +00:00
end
local TYPE_TO_FORMAT_FUNC = {
[ ' nil ' ] = format_primitive ,
[ ' boolean ' ] = format_primitive ,
2017-07-24 09:49:45 +00:00
[ ' number ' ] = import ' number ' ,
[ ' string ' ] = import ' pstring ' ,
2016-12-29 14:33:43 +00:00
[ ' thread ' ] = format_coroutine ,
[ ' table ' ] = format_table ,
2017-07-24 09:49:45 +00:00
[ ' function ' ] = import ' function ' ,
2016-12-29 14:33:43 +00:00
-- TODO
[ ' userdata ' ] = format_primitive ,
2017-08-07 08:39:45 +00:00
[ ' cdata ' ] = import ' cdata ' , -- Luajit exclusive ?
2016-12-29 14:33:43 +00:00
}
2017-07-25 15:49:06 +00:00
local function format_value ( value , display , l )
assert ( type ( display ) == ' number ' and type ( l ) == ' table ' )
2017-04-14 08:56:38 +00:00
local formatting = TYPE_TO_FORMAT_FUNC [ type ( value ) ]
if formatting then
2017-07-25 15:49:06 +00:00
formatting ( value , display , l , format_value )
2016-12-29 14:33:43 +00:00
else
2017-01-05 12:44:47 +00:00
error ( ERROR_UNKNOWN_TYPE : format ( type ( value ) , tostring ( value ) ) , 2 )
2016-12-28 23:51:07 +00:00
end
end
--------------------------------------------------------------------------------
2017-06-05 20:58:09 +00:00
-- StringBuilder
local StringBuilder = { }
StringBuilder.__index = StringBuilder
StringBuilder.__call = function ( l , s ) l [ # l + 1 ] = s end
setmetatable ( StringBuilder , {
__call = function ( ) return setmetatable ( { } , StringBuilder ) end
} )
--------------------------------------------------------------------------------
2017-06-24 16:53:59 +00:00
local DEBUG_OPTION_USED = { }
2017-04-14 12:01:01 +00:00
2017-04-03 14:39:19 +00:00
local KNOWN_OPTIONS = {
2017-07-25 15:49:06 +00:00
_table_addr_comment = { type = ' boolean ' , default = false , debug = ' debug ' } , -- TODO: Maybe automatically display table address when display = 0?
2017-06-24 16:53:59 +00:00
2017-06-09 15:24:05 +00:00
indent = { type = ' string ' , default = ' ' } ,
2017-12-11 10:04:11 +00:00
max_output_width = { type = ' number ' , default = TERMINAL_WIDTH }
2017-04-03 14:39:19 +00:00
}
2017-07-20 11:43:30 +00:00
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 ' )
2017-06-09 14:20:33 +00:00
-- Error check options
2017-07-20 11:43:30 +00:00
for option_name , option_value in pairs ( input_options ) do
2017-04-03 14:39:19 +00:00
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 )
2017-06-09 14:20:33 +00:00
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
2017-07-21 11:12:02 +00:00
error ( ( ' [pretty]: Bad value given to option %s: %s (%s). ' ) : format ( option_name , option_value , type ( option_value ) ) , 2 )
2017-06-24 16:53:59 +00:00
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 ) )
2017-04-03 14:39:19 +00:00
end
end
2017-07-20 11:43:30 +00:00
-- Create output options
local output_options = { }
2017-06-09 14:20:33 +00:00
-- Assign default values
for option_name , option_info in pairs ( KNOWN_OPTIONS ) do
2017-07-20 11:43:30 +00:00
if input_options [ option_name ] ~= nil then
output_options [ option_name ] = input_options [ option_name ]
else
output_options [ option_name ] = option_info.default
2017-06-09 14:20:33 +00:00
end
end
2017-12-11 10:04:11 +00:00
-- Calculate derived settings
output_options.max_width_for_single_line_table = MAX_WIDTH_FOR_SINGLE_LINE_TABLE -- TODO: Make dynamic
2017-07-20 11:43:30 +00:00
-- Returns input_options
return output_options
2017-04-03 14:39:19 +00:00
end
2017-12-11 10:04:11 +00:00
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
2016-12-28 23:51:07 +00:00
local function pretty_format ( value , options )
2017-06-05 20:58:09 +00:00
-- Error checking
2017-06-09 14:20:33 +00:00
local options = ensure_that_all_options_are_known ( options or { } )
2017-06-05 20:58:09 +00:00
-- Setup StringBuilder
local l = StringBuilder ( )
l.options = options
2017-07-25 15:49:06 +00:00
l.info = analyze_structure ( value , 3 )
2017-06-05 20:58:09 +00:00
-- Format value.
2017-07-25 15:49:06 +00:00
format_value ( value , DISPLAY.EXPAND , l )
2017-01-05 13:28:31 +00:00
-- If any alignment info still exists, ignore it
2017-07-25 15:49:06 +00:00
fix_seperator_info ( l , l.options . indent )
2017-01-05 13:28:31 +00:00
ignore_alignment_info ( l )
2017-12-11 10:04:11 +00:00
-- Concat and perform last assertions.
local repr = table.concat ( l , ' ' )
assert_pretty_result ( repr , options )
-- Return
return repr
2016-12-28 23:51:07 +00:00
end
return pretty_format
2017-11-18 12:23:43 +00:00