709 lines
16 KiB
Lua
709 lines
16 KiB
Lua
-- Terminal color (and formatting) codes.
|
|
local C = {
|
|
e = '\27[0m', -- reset
|
|
|
|
-- Text attributes.
|
|
br = '\27[1m', -- bright
|
|
di = '\27[2m', -- dim
|
|
it = '\27[3m', -- italics
|
|
un = '\27[4m', -- underscore
|
|
bl = '\27[5m', -- blink
|
|
re = '\27[7m', -- reverse
|
|
hi = '\27[8m', -- hidden
|
|
|
|
-- Text colors.
|
|
k = '\27[30m', -- black
|
|
r = '\27[31m', -- red
|
|
g = '\27[32m', -- green
|
|
y = '\27[33m', -- yellow
|
|
b = '\27[34m', -- blue
|
|
m = '\27[35m', -- magenta
|
|
c = '\27[36m', -- cyan
|
|
w = '\27[37m', -- white
|
|
|
|
-- Background colors.
|
|
_k = '\27[40m', -- black
|
|
_r = '\27[41m', -- red
|
|
_g = '\27[42m', -- green
|
|
_y = '\27[43m', -- yellow
|
|
_b = '\27[44m', -- blue
|
|
_m = '\27[45m', -- magenta
|
|
_c = '\27[46m', -- cyan
|
|
_w = '\27[47m' -- white
|
|
}
|
|
|
|
local METATABLE = { "<metatable>", colors = C.it .. C.y }
|
|
local INDENT = " "
|
|
|
|
-- The default sequence separator.
|
|
local SEP = " "
|
|
|
|
-- The open and close brackets can be any piece (notably, a sequence with
|
|
-- colors). The separator must be a plain string.
|
|
local BOPEN, BSEP, BCLOSE = 1, 2, 3
|
|
|
|
-- The default frame brackets and separator.
|
|
local BRACKETS = {
|
|
{ "{", colors = C.br },
|
|
",",
|
|
{ "}", colors = C.br }
|
|
}
|
|
|
|
local STR_HALF = 30
|
|
local MAX_STR_LEN = STR_HALF * 2
|
|
|
|
-- Names to use for named references. The order is important; these are aligned
|
|
-- with the colors in `NAME_COLORS`.
|
|
local NAMES = {
|
|
"Cherry",
|
|
"Apple",
|
|
"Lemon",
|
|
"Blueberry",
|
|
"Jam",
|
|
"Cream",
|
|
"Rhubarb",
|
|
"Lime",
|
|
"Butter",
|
|
"Grape",
|
|
"Pomegranate",
|
|
"Sugar",
|
|
"Cinnamon",
|
|
"Avocado",
|
|
"Honey",
|
|
}
|
|
|
|
-- Colors to use for named references. Don't use black nor white.
|
|
local NAME_COLORS = { C.r, C.g, C.y, C.b, C.m, C.c }
|
|
|
|
-- Reserved Lua keywords as a convenient look-up table.
|
|
local RESERVED = {
|
|
['and'] = true,
|
|
['break'] = true,
|
|
['do'] = true,
|
|
['else'] = true,
|
|
['elseif'] = true,
|
|
['end'] = true,
|
|
['false'] = true,
|
|
['for'] = true,
|
|
['function'] = true,
|
|
['goto'] = true,
|
|
['if'] = true,
|
|
['in'] = true,
|
|
['local'] = true,
|
|
['nil'] = true,
|
|
['not'] = true,
|
|
['or'] = true,
|
|
['repeat'] = true,
|
|
['return'] = true,
|
|
['then'] = true,
|
|
['true'] = true,
|
|
['until'] = true,
|
|
['while'] = true
|
|
}
|
|
|
|
|
|
--
|
|
-- Namers
|
|
--
|
|
|
|
local function new_namer ()
|
|
local index = 1
|
|
local suffix = 1
|
|
local color_index = 1
|
|
|
|
return function ()
|
|
-- Pick the name.
|
|
local result = NAMES [index]
|
|
if suffix > 1 then
|
|
result = result .. " " .. tostring (suffix)
|
|
end
|
|
|
|
index = index + 1
|
|
if index > #NAMES then
|
|
index = 1
|
|
suffix = suffix + 1
|
|
end
|
|
|
|
-- Pick the color.
|
|
local color = NAME_COLORS [color_index]
|
|
|
|
color_index = color_index + 1
|
|
if color_index > #NAME_COLORS then
|
|
color_index = 1
|
|
end
|
|
|
|
return { result, colors = C.un .. color }
|
|
end
|
|
end
|
|
|
|
|
|
--
|
|
-- Context
|
|
--
|
|
|
|
|
|
local function new_context ()
|
|
return {
|
|
occur = {},
|
|
named = {},
|
|
next_name = new_namer (),
|
|
|
|
prev_indent = '',
|
|
next_indent = INDENT,
|
|
line_len = 0,
|
|
max_width = 78,
|
|
|
|
result = ''
|
|
}
|
|
end
|
|
|
|
|
|
--
|
|
-- Translating into pieces
|
|
--
|
|
|
|
-- Translaters take any Lua value and create pieces to represent them.
|
|
--
|
|
-- Some values should only be serialized once, both to prevent cycles and to
|
|
-- prevent redundancy. Or in other cases, these values cannot be serialized
|
|
-- (such as functions) but if they appear multiple times we want to express
|
|
-- that they are the same.
|
|
--
|
|
-- When a translater encounters such a value for the first time, it is
|
|
-- registered in the context in `occur`. The value is wrapped in a plain table
|
|
-- with the `id` field pointing to the original value. If the value is
|
|
-- serializable, such as a table, then the the `def` field contains the piece
|
|
-- to display. If it is unserializable or it is not the first time this value
|
|
-- has occurred, the `def` field is nil.
|
|
--
|
|
-- In the cleaning stage, these `id` fields are replaced with their names. If a
|
|
-- `def` field is present, then a sequence is generated to define the name with
|
|
-- the piece.
|
|
|
|
local translaters = {}
|
|
local translate, ident_friendly
|
|
|
|
|
|
function translate (val, ctx)
|
|
-- Try to find a type-specific translater.
|
|
local by_type = translaters [type (val)]
|
|
|
|
if by_type then
|
|
-- If there is a type-specific translater, call it.
|
|
return by_type (val, ctx)
|
|
end
|
|
|
|
-- Otherwise perform the default translation.
|
|
|
|
-- Check whether we've already encountered this value.
|
|
if ctx.occur [val] then
|
|
-- We have; give it a name if we haven't already.
|
|
if not ctx.named [val] then
|
|
ctx.named [val] = ctx.next_name ()
|
|
end
|
|
|
|
-- Return the value as a reference.
|
|
return { id = val }
|
|
else
|
|
-- We haven't; mark it as encountered.
|
|
ctx.occur [val] = true
|
|
|
|
-- Return the value as a definition.
|
|
return { id = val, def = tostring (val) }
|
|
end
|
|
end
|
|
|
|
|
|
translaters ['function'] = function (val, ctx)
|
|
-- Check whether we've already encountered this function.
|
|
if ctx.occur [val] then
|
|
-- We have; give it a name if we haven't already.
|
|
if not ctx.named [val] then
|
|
ctx.named [val] = ctx.next_name ()
|
|
end
|
|
else
|
|
-- We haven't; mark it as encountered.
|
|
ctx.occur [val] = true
|
|
end
|
|
|
|
-- Return the unserialized function.
|
|
return { id = val }
|
|
end
|
|
|
|
|
|
function translaters.table (val, ctx)
|
|
-- Check whether we've already encountered this table.
|
|
if ctx.occur [val] then
|
|
-- We have; give it a name if we haven't already.
|
|
if not ctx.named [val] then
|
|
ctx.named [val] = ctx.next_name ()
|
|
end
|
|
|
|
-- Return the unserialized table.
|
|
return { id = val }
|
|
else
|
|
-- We haven't; mark it as encountered.
|
|
ctx.occur [val] = true
|
|
|
|
-- Construct the frame for this table.
|
|
local result = {
|
|
bracket = BRACKETS
|
|
}
|
|
|
|
-- The equals-sign between key and value.
|
|
local eq = { "=", colors = C.di }
|
|
|
|
-- Represent the metatable, if present.
|
|
local mt = getmetatable (val)
|
|
if mt then
|
|
-- Translate the metatable.
|
|
mt = translate (mt, ctx)
|
|
table.insert (result, { METATABLE, eq, mt })
|
|
end
|
|
|
|
-- Represent the contents.
|
|
for k, v in pairs (val) do
|
|
-- If it is a string key which can be represented without quotes, leave
|
|
-- it plain.
|
|
if ident_friendly (k) then
|
|
-- Leave the key as it is.
|
|
k = { k, colors = C.m }
|
|
else
|
|
-- Otherwise translate the key.
|
|
k = translate (k, ctx)
|
|
end
|
|
|
|
-- Translate the value.
|
|
v = translate (v, ctx)
|
|
|
|
table.insert (result, { k, eq, v })
|
|
end
|
|
|
|
-- Wrap the result with its id.
|
|
return { id = val, def = result }
|
|
end
|
|
end
|
|
|
|
|
|
function translaters.string (val)
|
|
if #val <= MAX_STR_LEN then
|
|
-- The string is short enough; display it all.
|
|
local a = string.format ('%q', val)
|
|
a = string.gsub (a, '\n', 'n')
|
|
|
|
return { a, colors = C.g }
|
|
else
|
|
-- The string is too long. Only show the start and end.
|
|
local a = string.format ('%q', string.sub (val, 1, STR_HALF))
|
|
a = string.gsub (a, '\n', 'n')
|
|
local b = string.format ('%q', string.sub (val, -STR_HALF))
|
|
b = string.gsub (b, '\n', 'n')
|
|
|
|
return { a, { "...", colors = C.di }, b, colors = C.g, sep = '', tight = true }
|
|
end
|
|
end
|
|
|
|
|
|
function translaters.number (val)
|
|
return { tostring (val), colors = C.m .. C.br }
|
|
end
|
|
|
|
|
|
-- Check whether a value can be represented as a Lua identifier, without the
|
|
-- need for quotes or translation.
|
|
--
|
|
-- If the value is not a string, this immediately returns false. Otherwise, the
|
|
-- string must be a valid Lua name: a sequence of letters, digits, and
|
|
-- underscores that doesn't start with a digit and isn't a reserved keyword.
|
|
--
|
|
-- See http://www.lua.org/manual/5.3/manual.html#3.1
|
|
function ident_friendly (val)
|
|
-- The value must be a string.
|
|
if type (val) ~= 'string' then
|
|
return false
|
|
end
|
|
|
|
if string.find (val, '^[_%a][_%a%d]*$') then
|
|
-- The value is a Lua name; check if it is reserved.
|
|
if RESERVED [val] then
|
|
-- The value is a resreved keyword.
|
|
return false
|
|
else
|
|
-- The value is a valid name.
|
|
return true
|
|
end
|
|
else
|
|
-- The value is not a Lua name.
|
|
return false
|
|
end
|
|
end
|
|
|
|
|
|
--
|
|
-- Cleaning pieces
|
|
--
|
|
|
|
|
|
local function clean (piece, ctx)
|
|
if type (piece) == 'table' then
|
|
-- Check if it's an id reference.
|
|
if piece.id then
|
|
local name = ctx.named [piece.id]
|
|
local def = piece.def
|
|
|
|
-- Check whether it has been given a name.
|
|
if name then
|
|
local header = {
|
|
"<", type (piece.id), " ", name, ">",
|
|
colors = C.it,
|
|
sep = '',
|
|
tight = true
|
|
}
|
|
-- Named. Check whether the reference has a definition.
|
|
if def then
|
|
-- Create a sequence defining the name to the definition.
|
|
return { header, { "is", colors = C.di }, clean (piece.def, ctx) }
|
|
else
|
|
-- Show just the name.
|
|
return header
|
|
end
|
|
else
|
|
-- No name. Check whether the reference has a definition.
|
|
if def then
|
|
-- Display the definition without any header.
|
|
return clean (piece.def, ctx)
|
|
else
|
|
-- Display just the type.
|
|
return {
|
|
"<", type (piece.id), ">",
|
|
colors = C.it,
|
|
sep = '',
|
|
tight = true
|
|
}
|
|
end
|
|
end
|
|
|
|
-- Check if it's a frame.
|
|
elseif piece.bracket then
|
|
-- Clean each child.
|
|
for i, child in ipairs (piece) do
|
|
piece [i] = clean (child, ctx)
|
|
end
|
|
return piece
|
|
|
|
-- Otherwise it's a sequence.
|
|
else
|
|
-- Clean each child.
|
|
for i, child in ipairs (piece) do
|
|
piece [i] = clean (child, ctx)
|
|
end
|
|
return piece
|
|
end
|
|
else
|
|
-- It's a plain value, not a table; no cleaning is needed.
|
|
return piece
|
|
end
|
|
end
|
|
|
|
|
|
--
|
|
-- Displaying pieces
|
|
--
|
|
|
|
|
|
-- Pieces are either frames (with brackets), sequences (no brackets), or
|
|
-- strings.
|
|
|
|
-- Frames are displayed either short-form as { a = 1 } or long-form as
|
|
-- {
|
|
-- a = 1
|
|
-- }.
|
|
|
|
|
|
-- Declare all the local functions first, so they can refer to each other.
|
|
local min_len, display, display_frame, display_sequence, display_string,
|
|
display_frame_short, display_frame_long, newline, newline_no_indent,
|
|
write, write_nolength, space_here, space_newline
|
|
|
|
|
|
-- Dispatch based on the piece's type.
|
|
function display (piece, ctx)
|
|
if type (piece) == 'string' then
|
|
-- String.
|
|
return display_string (piece, ctx)
|
|
elseif piece.bracket then
|
|
-- Frame.
|
|
return display_frame (piece, ctx)
|
|
else
|
|
-- Sequence.
|
|
return display_sequence (piece, ctx)
|
|
end
|
|
end
|
|
|
|
|
|
-- Display a frame.
|
|
function display_frame (frame, ctx)
|
|
if #frame == 0 then
|
|
-- If the frame is empty, just display the brackets.
|
|
local str = {
|
|
frame.bracket [BOPEN], frame.bracket [BCLOSE],
|
|
sep = '',
|
|
tight = true
|
|
}
|
|
return display (str, ctx)
|
|
end
|
|
|
|
local ml = min_len (frame)
|
|
|
|
-- Try to fit the frame short-form on this line.
|
|
if ml <= space_here (ctx) then
|
|
return display_frame_short (frame, ctx)
|
|
|
|
-- Otherwise try to fit it short-form on the next line.
|
|
elseif ml <= space_newline (ctx) then
|
|
newline (ctx)
|
|
return display_frame_short (frame, ctx)
|
|
|
|
-- Otherwise display it long-form.
|
|
else
|
|
return display_frame_long (frame, ctx)
|
|
end
|
|
end
|
|
|
|
|
|
function display_frame_short (frame, ctx)
|
|
-- Short-form frames never wrap onto new lines, so we don't need to do any
|
|
-- length checking (it's already been done for us).
|
|
|
|
-- Write the open bracket.
|
|
display (frame.bracket [BOPEN], ctx)
|
|
write (" ", ctx)
|
|
|
|
-- Display the first child.
|
|
display (frame [1], ctx)
|
|
|
|
-- Display the remaining children.
|
|
for i = 2, #frame do
|
|
local child = frame [i]
|
|
|
|
-- Write the separator.
|
|
write (frame.bracket [BSEP], ctx)
|
|
write (" ", ctx)
|
|
|
|
-- Display the child.
|
|
display (child, ctx)
|
|
end
|
|
|
|
-- Write the close bracket.
|
|
write (" ", ctx)
|
|
display (frame.bracket [BCLOSE], ctx)
|
|
end
|
|
|
|
|
|
function display_frame_long (frame, ctx)
|
|
-- Remember the original value of next_indent.
|
|
local old_old_indent = ctx.prev_indent
|
|
local old_indent = ctx.next_indent
|
|
|
|
-- Display the open bracket.
|
|
display (frame.bracket [BOPEN], ctx)
|
|
|
|
-- Increase the indentation.
|
|
ctx.prev_indent = old_indent
|
|
ctx.next_indent = old_indent .. INDENT
|
|
|
|
-- For all but the last child...
|
|
for i = 1, #frame - 1 do
|
|
local child = frame [i]
|
|
|
|
-- Start a new line with old indentation.
|
|
newline_no_indent (ctx)
|
|
write (old_indent, ctx)
|
|
|
|
-- Display the child.
|
|
display (child, ctx)
|
|
|
|
-- Write the separator.
|
|
write (frame.bracket [BSEP], ctx)
|
|
end
|
|
|
|
-- For the last child...
|
|
do
|
|
local child = frame [#frame]
|
|
|
|
-- Start a new line with old indentation.
|
|
newline_no_indent (ctx)
|
|
write (old_indent, ctx)
|
|
|
|
-- Display the child.
|
|
display (child, ctx)
|
|
-- No separator.
|
|
end
|
|
|
|
-- Write the close bracket.
|
|
newline_no_indent (ctx)
|
|
write (old_old_indent, ctx)
|
|
display (frame.bracket [BCLOSE], ctx)
|
|
|
|
-- Return to the old indentation.
|
|
ctx.prev_indent = old_old_indent
|
|
ctx.next_indent = old_indent
|
|
end
|
|
|
|
|
|
function display_sequence (piece, ctx)
|
|
if #piece > 0 then
|
|
-- Check if this is a tight sequence.
|
|
if piece.tight then
|
|
-- Try to fit the entire sequence on one line.
|
|
local ml = min_len (piece, ctx)
|
|
|
|
-- If it won't fit here, but it would fit on the next line, then write it
|
|
-- on the next line; otherwise, write it here.
|
|
if ml > space_here (ctx) and ml <= space_newline (ctx) then
|
|
newline (ctx)
|
|
end
|
|
end
|
|
|
|
-- Apply the colors, if given.
|
|
if piece.colors then
|
|
write_nolength (piece.colors, ctx)
|
|
end
|
|
|
|
-- Display the first child.
|
|
display (piece [1], ctx)
|
|
|
|
-- For each following children:
|
|
for i = 2, #piece do
|
|
local child = piece [i]
|
|
|
|
-- Apply the colors, if given.
|
|
if piece.colors then
|
|
write_nolength (piece.colors, ctx)
|
|
end
|
|
|
|
-- Write a separator.
|
|
write (piece.sep or SEP, ctx)
|
|
|
|
-- Then display the child.
|
|
display (child, ctx)
|
|
end
|
|
|
|
-- Reset the colors.
|
|
if piece.colors then
|
|
write_nolength (C.e, ctx)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function display_string (piece, ctx)
|
|
local ml = min_len (piece)
|
|
|
|
-- If it won't fit here, but it would fit on the next line, then write it on
|
|
-- the next line; otherwise, write it here.
|
|
if ml > space_here (ctx) and ml <= space_newline (ctx) then
|
|
newline (ctx)
|
|
end
|
|
|
|
write (piece, ctx)
|
|
end
|
|
|
|
|
|
-- The minimum length to display this piece, if it is placed all on one line.
|
|
function min_len (piece, ctx)
|
|
-- For strings, simply return their length.
|
|
if type (piece) == 'string' then
|
|
return #piece
|
|
end
|
|
|
|
-- Otherwise, we have some calculations to do.
|
|
local result = 0
|
|
|
|
if piece.bracket then
|
|
-- This is a frame.
|
|
|
|
-- If it's an empty frame, just the open and close brackets.
|
|
if #piece == 0 then
|
|
return min_len (piece.bracket [BOPEN]) + min_len (piece.bracket [BCLOSE])
|
|
end
|
|
|
|
-- Open and close brackets, plus a space for each.
|
|
result = result + min_len (piece.bracket [BOPEN]) +
|
|
min_len (piece.bracket [BCLOSE]) + 2
|
|
|
|
-- A separator between each item, plus a space for each.
|
|
result = result + (#piece - 1) * (#piece.bracket[BSEP] + 1)
|
|
else
|
|
-- This is a sequence.
|
|
|
|
-- If it's an empty sequence, then nothing.
|
|
if #piece == 0 then
|
|
return 0
|
|
end
|
|
|
|
-- A single separator between each item.
|
|
result = result + (#piece - 1) * #(piece.sep or SEP)
|
|
end
|
|
|
|
-- For both frames and sequences:
|
|
-- Find the minimum length of each child.
|
|
for _, child in ipairs (piece) do
|
|
result = result + min_len (child, ctx)
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
function newline (ctx)
|
|
ctx.result = ctx.result .. "\n"
|
|
ctx.line_len = 0
|
|
write (ctx.next_indent, ctx)
|
|
end
|
|
|
|
|
|
function newline_no_indent (ctx)
|
|
ctx.result = ctx.result .. "\n"
|
|
ctx.line_len = 0
|
|
end
|
|
|
|
|
|
function write (str, ctx)
|
|
ctx.result = ctx.result .. str
|
|
ctx.line_len = ctx.line_len + #str
|
|
end
|
|
|
|
|
|
function write_nolength (str, ctx)
|
|
ctx.result = ctx.result .. str
|
|
end
|
|
|
|
|
|
function space_here (ctx)
|
|
return math.max (0, ctx.max_width - ctx.line_len)
|
|
end
|
|
|
|
|
|
function space_newline (ctx)
|
|
return math.max (0, ctx.max_width - #ctx.next_indent)
|
|
end
|
|
|
|
|
|
--
|
|
-- Main function
|
|
--
|
|
|
|
|
|
return function (val)
|
|
if val == nil then
|
|
print (nil)
|
|
else
|
|
local ctx = new_context ()
|
|
local piece = translate (val, ctx)
|
|
piece = clean (piece, ctx)
|
|
display (piece, ctx)
|
|
print (C.e .. ctx.result .. C.e)
|
|
end
|
|
end
|