1
0
Fork 0
pretty/pstring.lua

218 lines
6.3 KiB
Lua

-- pretty.string
-- The string formatting module for pretty.
--[=[ Thoughts on displaying strings in the useful ways.
Thoughts are TODO
--]=]
local DISPLAY = assert(require((... and select('1', ...):match('.+%.') or '')..'common'), '[pretty]: Could not load vital library: common') . DISPLAY
--------------------------------------------------------------------------------
-- Constants
local NR_CHARS_IN_LONG_STRING = 40
local MAX_HORIZONAL_CHARACTER = 80
local SHORT_STR_DELIMITER = '\''
local STRING_CONT_INDICATOR = '...'
--------
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'
CHAR_TO_STR_REPR[SHORT_STR_DELIMITER:byte()] = '\\'..SHORT_STR_DELIMITER
end
local CHARACTERS_THAT_REQUIRE_ESCAPE_SEQ = '[%z\001-\008\011-\031\127]'
--------------------------------------------------------------------------------
-- Util
local function does_string_require_escaping (str)
return not not str:find(CHARACTERS_THAT_REQUIRE_ESCAPE_SEQ)
end
local escape_string do
local ESCAPE_SINGLE_BYTE = function (char) return CHAR_TO_STR_REPR[char:byte()] end
local ESCAPE_MALFORMED_CONT_BYTE = function (a, b) return a..'\\' .. b:byte() end
local ESCAPE_MALFORMED_START_BYTE = function (a, b) return '\\'..a:byte() .. b end
function escape_string (str)
-- Attempts to escape the string, to a format that is both a valid Lua
-- constant, and ledible unicode.
-- Error checking
assert(type(str) == 'string')
-- Escape single bytes
local str, count = str:gsub('.', ESCAPE_SINGLE_BYTE), 0
-- Escape malformed continuation bytes
repeat str, count = str:gsub('([^\128-\255])([\128-\191])', ESCAPE_MALFORMED_CONT_BYTE)
until count == 0
-- Escape malformed start bytes
repeat str, count = str:gsub('([\191-\255])([^\128-\191])', ESCAPE_MALFORMED_START_BYTE)
until count == 0
-- Done, lets return
return str
end
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 format_shortform_string (str, _, l)
l[#l+1] = SHORT_STR_DELIMITER
l[#l+1] = escape_string(str)
l[#l+1] = SHORT_STR_DELIMITER
end
local function safe_cut (str, si, ei)
-- Error checking
assert(type(str) == 'string')
assert(type(si) == 'number' or si == nil)
assert(type(ei) == 'number' or ei == nil)
-- Calculate
local cut_str = str:sub(si, ei)
-- Search for the number of backslashes and digits at the end of the string.
-- If the number of backslashes is even, it's a sequence of backslashes, if
-- not it's a broken escape string.
local start_of_backslashes, start_of_digits = cut_str:match '()\\*()%d?%d?$'
local nr_backslashes_before_end = start_of_digits - start_of_backslashes
if nr_backslashes_before_end % 2 == 1 then
-- Lets see if we can't shorten the escape code, to fit within the
-- cut limit.
local space_left = #cut_str - (start_of_digits - 2)
cut_str = cut_str:sub(1, start_of_digits - 2)
ei = ei - space_left
local digits, after_digits = str:match('^\\(%d?%d?%d?)()', si - 1 + start_of_digits - 1)
if space_left >= 1 + 3 - #digits:match '0*' then
ei = after_digits - 1
cut_str = cut_str .. ('\\%0'..(space_left-1)..'i'):format(digits)
end
end
return cut_str, ei
end
local function format_cut_string (str, _, l)
-- Calculate string
local str = escape_string(str)
str = safe_cut(str, 1, NR_CHARS_IN_LONG_STRING - #STRING_CONT_INDICATOR)
-- Format
l[#l+1] = SHORT_STR_DELIMITER
l[#l+1] = str
l[#l+1] = SHORT_STR_DELIMITER
l[#l+1] = STRING_CONT_INDICATOR
end
local function format_concatted_string (str, _, l)
-- Cuts the string up into smaller individual substrings, each Concatted
-- together. Is uglier compared to longform, but is at least idempotent.
-- TODO: Attempt to cut near whitespace?
-- Error checking
assert( type(str) == 'string' )
assert( type(l) == 'table' )
-- Calculate
local width_without_overhead = MAX_HORIZONAL_CHARACTER - 2*#SHORT_STR_DELIMITER - #' ..'
local str = escape_string(str)
-- Cut strings
local sub_strings, str_i = {}, 1
repeat
local sub_str, ei = safe_cut(str, str_i, str_i + width_without_overhead - 1)
str_i = ei + 1
sub_strings[#sub_strings+1] = sub_str
until str_i >= #str
-- Format them
for _, sub_str in ipairs(sub_strings) do
l[#l+1] = SHORT_STR_DELIMITER
l[#l+1] = sub_str
l[#l+1] = SHORT_STR_DELIMITER
l[#l+1] = ' ..\n'
end
l[#l] = ''
end
local function format_longform_string (str, _, l)
-- Error checking
assert(type(str) == 'string')
assert(type(l) == 'table')
-- Calculate
local level_required = smallest_secure_longform_string_level(str)
-- Format
l[#l+1] = '['..string.rep('=', level_required)..'['
l[#l+1] = '\n'
l[#l+1] = str
l[#l+1] = ']'..string.rep('=', level_required)..']'
end
return function (str, display, l)
-- pretty.format_string
-- TODO: Prefer \ddd style escaping to shorter (\n, \t), when many of the
-- \ddd already exist in the text.
-- Error checking
assert(type(str) == 'string')
assert(type(display) == 'number' and type(l) == 'table')
-- Do work
if #str < NR_CHARS_IN_LONG_STRING then
return format_shortform_string(str, nil, l)
elseif display < DISPLAY.EXPAND then
return format_cut_string (str, nil, l)
elseif does_string_require_escaping (str) then
return format_concatted_string(str, nil, l)
else
return format_longform_string(str, nil, l)
end
end