2016-12-28 23:51:07 +00:00
2017-07-17 19:33:11 +00:00
-- pretty.pretty
-- Main module of the `pretty` library.
-- TODO: Maybe move table formatting into its own file?
2017-07-20 08:38:24 +00:00
--[=[ Thoughts on displaying tables in an intuitive way.
Lua ' s table datastructure is likely to be the most consise 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 , or to
just display those it contains . That is , do we think about ` __index ` 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 then the output would be cluttered .
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 about the contents.
2. It continues : By representing tables as the pseudo - parsable ` { ... } ` , it ' s
clear we are talking about a table . We disregard the ability to
distinguish between tables .
3. Special case for 2. 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 " .
4. Single - line : TODO
5. Multi - line : TODO
2017-07-20 10:01:26 +00:00
6. Format into columns : ( like ls ) TODO
2017-07-20 08:38:24 +00:00
7. Tabular : TODO
8. Special cases : ( Array - tree , Table - Tree , Linked - List ) TODO
--]=]
2017-07-17 19:33:11 +00:00
--------------------------------------------------------------------------------
2017-01-05 12:37:44 +00:00
-- Ensure loading library, if it exists, no matter where pretty.lua was loaded from.
-- Load the library component
2017-06-24 16:36:01 +00:00
local format_number , format_function , analyze_structure , TABLE_TYPE
2017-01-05 12:37:44 +00:00
do
local thispath = ... and select ( ' 1 ' , ... ) : match ( ' .+%. ' ) or ' '
2017-05-25 20:53:02 +00:00
local function import ( name , ignore_failure )
local was_loaded , lib_or_error = pcall ( require , thispath .. name )
if not was_loaded then
if ignore_failure then return nil end
error ( ' [pretty]: Could not load vital library: ' .. name .. ' .lua: \n \t ' .. lib_or_error )
end
return lib_or_error
end
2017-01-16 15:10:10 +00:00
2017-04-03 09:55:49 +00:00
-- Load number and function formatting
2017-05-25 20:53:02 +00:00
-- Will use a very simple number formatting, if number.lua is not available.
-- Will use a very simple function formatting, if function.lua is not available.
2017-06-05 21:24:24 +00:00
format_number = import ( ' number ' , true ) or function ( value , _ , l ) l [ # l + 1 ] = tostring ( value ) end
format_function = import ( ' function ' , true ) or function ( value , _ , l ) l [ # l + 1 ] = ' function (...) --[[ ' .. tostring ( value ) : sub ( 11 ) .. ' ]] end ' end
2017-01-16 15:10:10 +00:00
2017-04-03 09:55:49 +00:00
-- Load other stuff
2017-05-25 20:53:02 +00:00
analyze_structure = import ' analyze_structure '
TABLE_TYPE = import ' table_type '
2017-01-05 12:37:44 +00:00
end
--
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-04-14 10:19:23 +00:00
local NR_CHARS_IN_LONG_STRING = 40
local MAX_WIDTH_FOR_SINGLE_LINE_TABLE = 38
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 ,
}
local CHAR_TO_STR_REPR = { }
do
for i = 00 , 031 do CHAR_TO_STR_REPR [ i ] = ' \\ 0 ' .. ( i < 10 and ' 0 ' or ' ' ) .. i end
for i = 32 , 255 do CHAR_TO_STR_REPR [ i ] = string.char ( i ) end
CHAR_TO_STR_REPR [ 7 ] = ' \\ a '
CHAR_TO_STR_REPR [ 8 ] = ' \\ b '
CHAR_TO_STR_REPR [ 9 ] = ' \t '
CHAR_TO_STR_REPR [ 10 ] = ' \n '
CHAR_TO_STR_REPR [ 11 ] = ' \\ v '
CHAR_TO_STR_REPR [ 12 ] = ' \\ f '
CHAR_TO_STR_REPR [ 13 ] = ' \\ r '
CHAR_TO_STR_REPR [ 92 ] = ' \\ \\ '
CHAR_TO_STR_REPR [ 127 ] = ' \\ 127 '
end
--------------------------------------------------------------------------------
-- 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 )
2017-07-18 11:38:05 +00:00
-- 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 )
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 ' )
-- Do stuff
2016-12-28 23:51:07 +00:00
local key_value_pairs = { }
for key , value in pairs ( t ) do
key_value_pairs [ # key_value_pairs + 1 ] = { key , value }
end
2017-07-18 11:38:05 +00:00
table.sort ( key_value_pairs , compare_key_value_pair )
2016-12-28 23:51:07 +00:00
return key_value_pairs
end
2017-04-14 08:56:38 +00:00
local function fill_holes_in_key_value_pairs ( key_value_pairs )
-- NOTE: Assumes that all keys are numbers
2017-06-05 20:34:23 +00:00
-- Error checking
assert ( type ( key_value_pairs ) == ' table ' )
-- Do stuff
2017-04-14 08:56:38 +00:00
for i = 2 , # key_value_pairs do
for j = key_value_pairs [ i - 1 ] [ 1 ] + 1 , key_value_pairs [ i ] [ 1 ] - 1 do
key_value_pairs [ # key_value_pairs + 1 ] = { j , nil }
end
end
2017-07-18 11:38:05 +00:00
table.sort ( key_value_pairs , compare_key_value_pair )
2017-04-14 08:56:38 +00:00
end
2017-06-09 14:24:26 +00:00
local function smallest_secure_longform_string_level ( str )
2017-07-18 11:38:05 +00:00
-- Determines the level a longform string needs to use, to avoid code
2017-06-09 14:24:26 +00:00
-- injection. For example, if we want to use longform on the string
-- 'Hello ]] World', we cannot use level-0 as this would result in
-- '[[Hello ]] World]]', which could be an issue in certain applications.
-- Error checking
assert ( type ( str ) == ' string ' )
-- Do stuff
local levels = { [ 1 ] = 1 }
str : gsub ( ' %]=*%] ' , function ( m ) levels [ m : len ( ) ] = true end )
return # levels - 1
end
2016-12-28 23:51:07 +00:00
local function escape_string ( str )
2017-06-05 20:34:23 +00:00
-- Error checking
assert ( type ( str ) == ' string ' )
-- Do stuff
2016-12-28 23:51:07 +00:00
local l = { }
2017-07-18 11:38:05 +00:00
for i = 1 , # str do l [ # l + 1 ] = CHAR_TO_STR_REPR [ str : byte ( i ) ] end
2016-12-28 23:51:07 +00:00
return table.concat ( l , ' ' )
end
2017-06-05 20:34:23 +00:00
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
2017-01-05 12:44:47 +00:00
local width = 0
2017-06-05 20:34:23 +00:00
for i = start_i , stop_i do
2017-04-14 10:19:23 +00:00
width = width + ( ( type ( l [ i ] ) ~= ' string ' ) and 1 or # l [ i ] )
2017-01-05 12:44:47 +00:00
end
return width
end
2017-01-05 13:28:31 +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 ' )
assert ( type ( start_i ) == ' number ' )
-- Do stuff
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-01-05 13:28:31 +00:00
l [ i ] = ' '
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 ' )
assert ( type ( start_i ) == ' number ' )
2017-01-05 13:28:31 +00:00
-- Find maximums
local max = { }
2017-04-14 10:19: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
max [ l [ i ] [ 2 ] ] = math.max ( l [ i ] [ 3 ] , max [ l [ i ] [ 2 ] ] or 0 )
2017-01-05 13:28:31 +00:00
end
end
-- Insert the proper whitespace
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
l [ i ] = string.rep ( ' ' , max [ l [ i ] [ 2 ] ] - l [ i ] [ 3 ] )
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 ' )
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 ( start_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
2017-04-14 12:01:01 +00:00
local function fix_seperator_info ( l , indent_char , max_depth )
2017-06-05 20:34:23 +00:00
-- Error Checking
assert ( type ( l ) == ' table ' )
assert ( type ( indent_char ) == ' string ' )
assert ( type ( max_depth ) == ' number ' )
-- Do stuff
2017-04-14 12:01:01 +00:00
local depth , inline_depth = 0 , nil
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 ' )
l [ i ] = ( l [ i ] [ 2 ] or ' ' ) .. ( inline_depth and ' ' or ( ' \n ' .. indent_char : rep ( depth ) ) )
2017-04-14 10:19:23 +00:00
elseif l [ i ] [ 1 ] == ' indent ' then
2017-04-14 12:01:01 +00:00
depth , inline_depth = depth + 1 , inline_depth or l [ i ] [ 3 ] == ' inline ' and depth + 1 or nil
l [ i ] = l [ i ] [ 2 ] .. ( inline_depth and ' ' or ( ' \n ' .. indent_char : rep ( depth ) ) )
2017-04-14 10:19:23 +00:00
elseif l [ i ] [ 1 ] == ' unindent ' then
2017-04-14 12:01:01 +00:00
l [ i ] = ( inline_depth and ' ' or ( ' \n ' .. indent_char : rep ( depth - 1 ) ) ) .. l [ i ] [ 2 ]
depth , inline_depth = depth - 1 , ( depth ~= inline_depth ) and inline_depth or nil
2017-04-14 10:19:23 +00:00
end
end
end
2016-12-28 23:51:07 +00:00
--------------------------------------------------------------------------------
-- Formatting stuff
2017-06-05 20:47:07 +00:00
local format_value
2016-12-28 23:51:07 +00:00
-- Ways to format keys
2017-06-05 21:24:24 +00:00
local function format_key_and_value_string_map ( l , key , value , depth )
2016-12-29 04:34:44 +00:00
l [ # l + 1 ] = key
2017-04-14 12:01:01 +00:00
l [ # l + 1 ] = { ' align ' , ' key ' , # key }
2016-12-29 04:34:44 +00:00
l [ # l + 1 ] = ' = '
2017-06-05 21:24:24 +00:00
return format_value ( value , depth , l )
2016-12-28 23:51:07 +00:00
end
2017-06-05 21:24:24 +00:00
local function format_key_and_value_arbitr_map ( l , key , value , depth )
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-06-05 21:24:24 +00:00
format_value ( key , math.huge , 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-06-05 21:24:24 +00:00
return format_value ( value , depth , l )
2016-12-28 23:51:07 +00:00
end
2017-06-05 21:24:24 +00:00
local function format_key_and_value_sequence ( l , key , value , depth )
return format_value ( value , depth , 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
2016-12-29 10:37:31 +00:00
-- Formatting tables
2017-06-16 15:04:14 +00:00
local function format_table ( t , depth , l )
2017-06-05 20:47:07 +00:00
-- TODO: Add more nuanced formatting.
2017-06-05 21:24:24 +00:00
-- Error Checking
2017-06-05 20:34:23 +00:00
assert ( type ( t ) == ' table ' )
2017-06-05 21:13:50 +00:00
assert ( type ( depth ) == ' number ' and type ( l ) == ' table ' )
2017-06-05 20:34:23 +00:00
-- Do stuff
2017-06-24 16:36:01 +00:00
if not l.info [ t ] then analyze_structure ( t , l.options . max_depth - depth , l.info ) end
local table_info = l.info [ t ]
assert ( table_info )
2017-06-05 20:47:07 +00:00
2017-06-05 21:13:50 +00:00
if l.options . recursion == ' marked ' and table_info.marker then
2017-06-05 20:47:07 +00:00
l [ # l + 1 ] , l [ # l + 2 ] , l [ # l + 3 ] = ' < ' , table_info.marker , ' > '
end
local already_visited = l.visited [ t ]
l.visited [ t ] = true
2017-07-20 08:38:24 +00:00
-- If empty, visited or above max-depth, give a small represetation: `{...}`
if table_info.type == TABLE_TYPE.EMPTY or depth >= l.options . max_depth or already_visited 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 ' } '
2017-06-05 20:47:07 +00:00
end
-- 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.
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-06-05 21:24:24 +00:00
pair_format_func ( l , pair [ 1 ] , pair [ 2 ] , depth + 1 )
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 )
if table_width <= MAX_WIDTH_FOR_SINGLE_LINE_TABLE then
-- Is short table: Ignore the "width of key"-shit
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.
-- 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-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-06-05 20:47:07 +00:00
-- Formatting Strings
2016-12-28 23:51:07 +00:00
2017-06-05 21:24:24 +00:00
local function format_string ( str , depth , l )
2016-12-28 23:51:07 +00:00
-- TODO: Add option for escaping unicode characters.
2017-06-09 14:20:33 +00:00
-- TODO: Improve cutstring argument.
2017-07-18 11:38:05 +00:00
-- TODO: Move this to it's own file.
2016-12-28 23:51:07 +00:00
2017-06-05 20:34:23 +00:00
-- Error checking
assert ( type ( str ) == ' string ' )
2017-06-05 21:13:50 +00:00
assert ( type ( depth ) == ' number ' and type ( l ) == ' table ' )
2017-06-05 20:34:23 +00:00
-- Do work
2016-12-28 23:51:07 +00:00
local is_long_string = ( str : len ( ) >= NR_CHARS_IN_LONG_STRING )
local newline_or_tab_index = str : find ( ' [ \n \t ] ' )
local single_quote_index = str : find ( ' \' ' )
local double_quote_index = str : find ( ' \" ' )
2017-01-16 15:10:10 +00:00
-- ...
local chance_of_longform = is_long_string and ( ( newline_or_tab_index or math.huge ) <= NR_CHARS_IN_LONG_STRING ) or double_quote_index and single_quote_index
2017-06-05 21:13:50 +00:00
local cut_string_index = l.options . cut_strings and ( is_long_string or chance_of_longform )
and math.min ( NR_CHARS_IN_LONG_STRING - 3 , newline_or_tab_index or 1 / 0 , double_quote_index or 1 / 0 , single_quote_index or 1 / 0 )
2016-12-28 23:51:07 +00:00
local longform = chance_of_longform and ( ( not cut_string_index ) or cut_string_index < math.min ( newline_or_tab_index or 1 / 0 , double_quote_index or 1 / 0 , single_quote_index or 1 / 0 ) )
local escape_newline_and_tab = not longform and newline_or_tab_index
-- Determine string delimiters
local left , right
if longform then
local level = smallest_secure_longform_string_level ( str )
left , right = ' [ ' .. string.rep ( ' = ' , level ) .. ' [ ' , ' ] ' .. string.rep ( ' = ' , level ) .. ' ] '
if newline_or_tab_index then str = ' \n ' .. str end
elseif not single_quote_index then
left , right = ' \' ' , ' \' '
else
left , right = ' \" ' , ' \" '
end
-- Cut string
if cut_string_index then str = str : sub ( 1 , cut_string_index ) end
str = escape_string ( str )
-- Escape newline and tab
if escape_newline_and_tab then str = str : gsub ( ' \n ' , ' \\ n ' ) : gsub ( ' \t ' , ' \\ t ' ) end
2017-01-05 12:37:44 +00:00
l [ # l + 1 ] = left
l [ # l + 1 ] = str
l [ # l + 1 ] = right
2016-12-28 23:51:07 +00:00
end
2017-06-05 21:24:24 +00:00
local function format_coroutine ( value , depth , 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 ' )
assert ( type ( depth ) == ' number ' and 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-06-05 21:24:24 +00:00
local function format_primitive ( value , depth , l )
-- Error check
assert ( type ( depth ) == ' number ' and type ( l ) == ' table ' )
-- 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 ,
[ ' number ' ] = format_number ,
[ ' string ' ] = format_string ,
[ ' thread ' ] = format_coroutine ,
[ ' table ' ] = format_table ,
2017-04-14 11:24:29 +00:00
[ ' function ' ] = format_function , -- TODO: Improve a little
[ ' userdata ' ] = format_primitive , -- TODO
[ ' cdata ' ] = format_primitive , -- TODO & Luajit only
2016-12-29 14:33:43 +00:00
}
2017-06-05 21:24:24 +00:00
function format_value ( value , depth , l )
2017-06-05 21:13:50 +00:00
assert ( type ( depth ) == ' 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-06-05 21:24:24 +00:00
formatting ( value , depth , 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-06-24 16:53:59 +00:00
_table_addr_comment = { type = ' boolean ' , default = false , debug = ' debug ' } ,
2017-06-09 14:20:33 +00:00
cut_strings = { type = ' boolean ' , default = false } ,
2017-06-09 15:24:05 +00:00
indent = { type = ' string ' , default = ' ' } ,
2017-06-09 14:20:33 +00:00
max_depth = { type = ' number ' , default = math.huge } ,
2017-06-24 16:53:59 +00:00
short_builtins = { type = ' boolean ' , default = false } , -- TODO: Outphase this. Rather automatically use the short versions in places where it would be strange to find the function, like keys, etc.
2017-06-24 18:06:36 +00:00
recursion = { type = ' string ' , default = ' ignore ' , accepted = { [ ' ignore ' ] = true , [ ' marked ' ] = true } } ,
2017-04-03 14:39:19 +00:00
}
local function ensure_that_all_options_are_known ( options )
2017-06-05 20:58:09 +00:00
assert ( type ( options ) == ' table ' )
2017-06-09 14:20:33 +00:00
-- Error check options
2017-04-03 14:39:19 +00:00
for option_name , option_value in pairs ( 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 )
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
error ( ( ' [pretty]: Bad value given to option %s: %s (%s). Expected one of: %s ' ) : format ( option_name , option_value , type ( option_value ) , table.concat ( KNOWN_OPTIONS [ option_name ] . accepted , ' , ' ) ) , 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-06-09 14:20:33 +00:00
-- Assign default values
for option_name , option_info in pairs ( KNOWN_OPTIONS ) do
2017-06-09 15:06:10 +00:00
if options [ option_name ] == nil then
2017-06-09 14:20:33 +00:00
options [ option_name ] = option_info.default
end
end
-- Returns options
return options
2017-04-03 14:39:19 +00:00
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.visited = { next_mark = 1 }
l.options = options
2017-06-15 17:01:41 +00:00
l.info = analyze_structure ( value , options.max_depth )
2017-06-05 20:58:09 +00:00
-- Format value.
2017-06-05 21:24:24 +00:00
format_value ( value , 0 , l )
2017-01-05 13:28:31 +00:00
-- If any alignment info still exists, ignore it
2017-04-14 12:01:01 +00:00
fix_seperator_info ( l , l.options . indent , l.options . max_depth )
2017-01-05 13:28:31 +00:00
ignore_alignment_info ( l )
2017-01-05 12:37:44 +00:00
return table.concat ( l , ' ' )
2016-12-28 23:51:07 +00:00
end
return pretty_format