You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

725 lines
18 KiB
Lua

-- https://github.com/nenofite/mm
-- 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
}
-- If we're on Windows, set all colors to empty strings so we don't spam the
-- output with meaningless escape codes.
local ON_WINDOWS = string.find(package.path, '\\') ~= nil
if ON_WINDOWS then
for k, v in pairs(C) do
C[k] = ''
end
end
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, ctx)
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, ctx)
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
return nil
else
local ctx = new_context()
local piece = translate(val, ctx)
piece = clean(piece, ctx)
display(piece, ctx)
return (C.e .. ctx.result .. C.e)
end
end