--[[-- # Pretty `pretty` is an advanced pretty printer for [Lua](https://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 pure-blood 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 readability. * Indention and alignment of keys-value pairs. * Keys-value pairs are [alpha-numerically](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 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. 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". 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 this_path = ... and select('1', ...):match('.+%.') or '' import = function (name, ignore_failure) return require(this_path..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