diff --git a/pretty.lua b/pretty.lua index 2ba1331..c3107a8 100644 --- a/pretty.lua +++ b/pretty.lua @@ -41,7 +41,7 @@ a table, we have a better idea, but then the output would be cluttered. -- Ensure loading library, if it exists, no matter where pretty.lua was loaded from. -- Load the library component -local format_number, format_function, analyze_structure, TABLE_TYPE +local format_number, format_function, format_string, analyze_structure, TABLE_TYPE do local thispath = ... and select('1', ...):match('.+%.') or '' local function import (name, ignore_failure) @@ -56,14 +56,18 @@ do -- Load number and function formatting -- 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. - 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 + -- Will use a very simple string formatting, if string.lua is not available. + 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 + format_string = import('pstring', true) or function (value, _, l) l[#l+1] = '[['..value..']]' end -- Load other stuff analyze_structure = import 'analyze_structure' TABLE_TYPE = import 'table_type' end --- + +-------------------------------------------------------------------------------- +-- Constants local ERROR_UNKNOWN_TYPE = [[ [pretty]: Attempting to format unsupported value of type "%s". @@ -72,7 +76,6 @@ local ERROR_UNKNOWN_TYPE = [[ We are attempting to cover all Lua features, so please report this bug, so we can improve. ]] -local NR_CHARS_IN_LONG_STRING = 40 local MAX_WIDTH_FOR_SINGLE_LINE_TABLE = 38 local KEY_TYPE_SORT_ORDER = { @@ -96,24 +99,8 @@ local VALUE_TYPE_SORT_ORDER = { ['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 +-- Key-value-pair Util local function padnum(d) local dec, n = string.match(d, "(%.?)0*(.+)") @@ -197,31 +184,8 @@ local function fill_holes_in_key_value_pairs (key_value_pairs) table.sort(key_value_pairs, compare_key_value_pair) end -local function smallest_secure_longform_string_level (str) - -- Determines the level a longform string needs to use, to avoid code - -- 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 - -local function escape_string (str) - - -- Error checking - assert(type(str) == 'string') - - -- Do stuff - local l = {} - for i = 1, #str do l[#l+1] = CHAR_TO_STR_REPR[str:byte(i)] end - return table.concat(l, '') -end +-------------------------------------------------------------------------------- +-- Formatting Util local function width_of_strings_in_l (l, start_i, stop_i) @@ -495,53 +459,6 @@ end -- Formatting Strings -local function format_string (str, depth, l) - -- TODO: Add option for escaping unicode characters. - -- TODO: Improve cutstring argument. - -- TODO: Move this to it's own file. - - -- Error checking - assert( type(str) == 'string' ) - assert(type(depth) == 'number' and type(l) == 'table') - - -- Do work - - 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('\"') - - -- ... - 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 - 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) - - 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 - - l[#l+1] = left - l[#l+1] = str - l[#l+1] = right -end local function format_coroutine (value, depth, l) -- Formats a coroutine. Unfortunantly we cannot gather a lot of information @@ -572,7 +489,7 @@ local TYPE_TO_FORMAT_FUNC = { ['thread'] = format_coroutine, ['table'] = format_table, - ['function'] = format_function, -- TODO: Improve a little + ['function'] = format_function, ['userdata'] = format_primitive, -- TODO ['cdata'] = format_primitive, -- TODO & Luajit only } diff --git a/pstring.lua b/pstring.lua new file mode 100644 index 0000000..c1ee54b --- /dev/null +++ b/pstring.lua @@ -0,0 +1,114 @@ + +-- pretty.string +-- The string formatting module for pretty. + +--[=[ Thoughts on displaying strings in the useful ways. + +TODO + +--]=] + +-------------------------------------------------------------------------------- +-- Constants + +local NR_CHARS_IN_LONG_STRING = 40 + +local CHAR_TO_STR_REPR = {} + +do + for i = 00, 031 do CHAR_TO_STR_REPR[i] = ('\\%03i'):format(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 escape_string (str) + -- Attempts to escape the string, to a format that is both a valid Lua + -- constant, and ledible unicode. + + -- TODO: Escape invalid unicode sequences. + + -- Error checking + assert(type(str) == 'string') + + -- Do stuff + local l = {} + for i = 1, #str do l[#l+1] = CHAR_TO_STR_REPR[str:byte(i)] end + return table.concat(l, '') +end + +local function smallest_secure_longform_string_level (str) + -- Determines the level a longform string needs to use, to avoid code + -- 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 + +-------------------------------------------------------------------------------- + +return function (str, depth, l) + -- pretty.format_string + + -- TODO: Add option for escaping unicode characters. + -- TODO: Improve cutstring argument. + + -- Error checking + assert( type(str) == 'string' ) + assert(type(depth) == 'number' and type(l) == 'table') + + -- Do work + + 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('\"') + + -- ... + 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 + 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) + + 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 + + l[#l+1] = left + l[#l+1] = str + l[#l+1] = right +end diff --git a/test/test_pretty.lua b/test/test_pretty.lua index 6753ab1..bce7f0c 100644 --- a/test/test_pretty.lua +++ b/test/test_pretty.lua @@ -34,85 +34,6 @@ local function format_test (t) end, { line = debug.getinfo(2).currentline }) end --------------------------------------------------------------------------------- --- Strings - -format_test { - input = 'Hello World', - expect = '\'Hello World\'', -} - -format_test { - input = 'Hello \'World\'', - expect = '\"Hello \'World\'\"', -} - -format_test { - input = 'Hello \"World\"', - expect = '\'Hello \"World\"\'', -} - -format_test { - input = 'Hello [[World]]', - expect = '\'Hello [[World]]\'', -} - -format_test { - input = '\'Hello\' [[World]]', - expect = '\"\'Hello\' [[World]]\"', -} - -format_test { - input = '\'Hello\' \"there\" [[World]]', - expect = '[=[\'Hello\' \"there\" [[World]]]=]', -} - -format_test { - input = '\'Hello\' \"there\" [=[World]=]', - expect = '[[\'Hello\' \"there\" [=[World]=]]]', -} - -format_test { - input = '\nHello World', - expect = '\'\\nHello World\'', -} - -format_test { - input = '\'\"\n', - expect = '[[\n\'\"\n]]', -} - -format_test { - input = '\n', - expect = '\'\\n\'', -} - -format_test { - input = '\\', - expect = '\'\\\\\'', -} - -format_test { - input = '\000', - expect = '\'\\000\'', -} -format_test { - input = '\a\b\v\r\f', - expect = '\'\\a\\b\\v\\r\\f\'', -} - -format_test { - input = 'ø', - expect = '\'ø\'', -} - -format_test { - name = 'Malformed Unicode is escaped', - input = '\000\001\003\012\169\003\000\030', - expect = '\'\\000\\000\\001\\003\\012\\169\\003\\000\\030\'', -} - - -------------------------------------------------------------------------------- -- Primitive types diff --git a/test/test_pstring.lua b/test/test_pstring.lua new file mode 100644 index 0000000..d8aab6c --- /dev/null +++ b/test/test_pstring.lua @@ -0,0 +1,102 @@ + +local SUITE = require 'TestSuite' 'string' +SUITE:setEnviroment{ + format = require('pretty') +} + +-------------------------------------------------------------------------------- + +-- Compat +if not loadstring then loadstring = load end -- Lua 5.3 compat +-- + +local function format_test (t) + SUITE:addTest(t.name or t.expect, function () + local actual_result = format(t.input, t.options) + assert_equal(t.expect, actual_result) + end, { line = debug.getinfo(2).currentline }) +end + +-------------------------------------------------------------------------------- + +format_test { + input = 'Hello World', + expect = '\'Hello World\'', +} + +format_test { + input = 'Hello \'World\'', + expect = '\"Hello \'World\'\"', +} + +format_test { + input = 'Hello \"World\"', + expect = '\'Hello \"World\"\'', +} + +format_test { + input = 'Hello [[World]]', + expect = '\'Hello [[World]]\'', +} + +format_test { + input = '\'Hello\' [[World]]', + expect = '\"\'Hello\' [[World]]\"', +} + +format_test { + input = '\'Hello\' \"there\" [[World]]', + expect = '[=[\'Hello\' \"there\" [[World]]]=]', +} + +format_test { + input = '\'Hello\' \"there\" [=[World]=]', + expect = '[[\'Hello\' \"there\" [=[World]=]]]', +} + +format_test { + input = '\nHello World', + expect = '\'\\nHello World\'', +} + +format_test { + input = '\'\"\n', + expect = '[[\n\'\"\n]]', +} + +format_test { + input = '\n', + expect = '\'\\n\'', +} + +format_test { + input = '\\', + expect = '\'\\\\\'', +} + +format_test { + input = '\000', + expect = '\'\\000\'', +} +format_test { + input = '\a\b\v\r\f', + expect = '\'\\a\\b\\v\\r\\f\'', +} + +-------------------------------------------------------------------------------- +-- Unicode + +format_test { + input = 'ø', + expect = '\'ø\'', +} + +format_test { + name = 'Malformed Unicode is escaped', + input = '\000\001\003\012\169\003\000\030', + expect = '\'\\000\\000\\001\\003\\012\\169\\003\\000\\030\'', +} + +-------------------------------------------------------------------------------- + +return SUITE