--- 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