mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-01-05 15:00:29 +00:00
Use a Wadler style pretty printer in the Lua REPL (#334)
- Add a cc.pretty module, which provides a Wadler style pretty printer [1]. - The cc.pretty.pretty function converts an arbitrary object into a pretty-printed document. This can then be printed to the screen with cc.pretty.{write, print} or converted to a string with cc.pretty.render. - Convert the Lua REPL to use the pretty printer rather than textutils.serialise. [1]: http://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
This commit is contained in:
parent
c79f643ba7
commit
798868427e
@ -0,0 +1,416 @@
|
|||||||
|
--- Provides a "pretty printer", for rendering data structures in an
|
||||||
|
-- aesthetically pleasing manner.
|
||||||
|
--
|
||||||
|
-- In order to display something using @{cc.pretty}, you build up a series of
|
||||||
|
-- @{documents|Doc}. These behave a little bit like strings; you can concatenate
|
||||||
|
-- them together and then print them to the screen.
|
||||||
|
--
|
||||||
|
-- However, documents also allow you to control how they should be printed. There
|
||||||
|
-- are several functions (such as @{nest} and @{group}) which allow you to control
|
||||||
|
-- the "layout" of the document. When you come to display the document, the 'best'
|
||||||
|
-- (most compact) layout is used.
|
||||||
|
--
|
||||||
|
-- @module cc.pretty
|
||||||
|
-- @usage Print a table to the terminal
|
||||||
|
-- local pretty = require "cc.pretty"
|
||||||
|
-- pretty.write(pretty.dump({ 1, 2, 3 }))
|
||||||
|
--
|
||||||
|
-- @usage Build a custom document and display it
|
||||||
|
-- local pretty = require "cc.pretty"
|
||||||
|
-- pretty.write(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world")))
|
||||||
|
|
||||||
|
local expect = require "cc.expect".expect
|
||||||
|
local type, getmetatable, setmetatable, colours, str_write = type, getmetatable, setmetatable, colours, write
|
||||||
|
|
||||||
|
--- @{table.insert} alternative, but with the length stored inline.
|
||||||
|
local function append(out, value)
|
||||||
|
local n = out.n + 1
|
||||||
|
out[n], out.n = value, n
|
||||||
|
end
|
||||||
|
|
||||||
|
--- A document, which
|
||||||
|
--
|
||||||
|
-- Documents effectively represent a sequence of strings in alternative layouts,
|
||||||
|
-- which we will try to print in the most compact form necessary.
|
||||||
|
--
|
||||||
|
-- @type Doc
|
||||||
|
local Doc = { }
|
||||||
|
|
||||||
|
--- An empty document.
|
||||||
|
local empty = setmetatable({ tag = "nil" }, Doc)
|
||||||
|
|
||||||
|
--- A document with a single space in it.
|
||||||
|
local space = setmetatable({ tag = "text", text = " " }, Doc)
|
||||||
|
|
||||||
|
--- A line break. When collapsed with @{group}, this will be replaced with @{empty}.
|
||||||
|
local line = setmetatable({ tag = "line", flat = empty }, Doc)
|
||||||
|
|
||||||
|
--- A line break. When collapsed with @{group}, this will be replaced with @{space}.
|
||||||
|
local space_line = setmetatable({ tag = "line", flat = space }, Doc)
|
||||||
|
|
||||||
|
local text_cache = { [""] = empty, [" "] = space, ["\n"] = space_line }
|
||||||
|
|
||||||
|
local function mk_text(text, colour)
|
||||||
|
return text_cache[text] or setmetatable({ tag = "text", text = text, colour = colour }, Doc)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Create a new document from a string.
|
||||||
|
--
|
||||||
|
-- If your string contains multiple lines, @{group} will flatten the string
|
||||||
|
-- into a single line, with spaces between each line.
|
||||||
|
--
|
||||||
|
-- @tparam string text The string to construct a new document with.
|
||||||
|
-- @tparam[opt] number colour The colour this text should be printed with. If not given, we default to the current
|
||||||
|
-- colour.
|
||||||
|
-- @treturn Doc The document with the provided text.
|
||||||
|
local function text(text, colour)
|
||||||
|
expect(1, text, "string")
|
||||||
|
expect(2, colour, "number", "nil")
|
||||||
|
|
||||||
|
local cached = text_cache[text]
|
||||||
|
if cached then return cached end
|
||||||
|
|
||||||
|
local new_line = text:find("\n", 1)
|
||||||
|
if not new_line then return mk_text(text, colour) end
|
||||||
|
|
||||||
|
-- Split the string by "\n". With a micro-optimisation to skip empty strings.
|
||||||
|
local doc = setmetatable({ tag = "concat", n = 0 }, Doc)
|
||||||
|
if new_line ~= 1 then append(doc, mk_text(text:sub(1, new_line - 1), colour)) end
|
||||||
|
|
||||||
|
new_line = new_line + 1
|
||||||
|
while true do
|
||||||
|
local next_line = text:find("\n", new_line)
|
||||||
|
append(doc, space_line)
|
||||||
|
if not next_line then
|
||||||
|
if new_line <= #text then append(doc, mk_text(text:sub(new_line), colour)) end
|
||||||
|
return doc
|
||||||
|
else
|
||||||
|
if new_line <= next_line - 1 then
|
||||||
|
append(doc, mk_text(text:sub(new_line, next_line - 1), colour))
|
||||||
|
end
|
||||||
|
new_line = next_line + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Concatenate several documents together. This behaves very similar to string concatenation.
|
||||||
|
|
||||||
|
-- @tparam Doc|string ... The documents to concatenate.
|
||||||
|
-- @treturn Doc The concatenated documents.
|
||||||
|
-- @usage pretty.concat(doc1, " - ", doc2)
|
||||||
|
-- @usage doc1 .. " - " .. doc2
|
||||||
|
local function concat(...)
|
||||||
|
local args = table.pack(...)
|
||||||
|
for i = 1, args.n do
|
||||||
|
if type(args[i]) == "string" then args[i] = text(args[i]) end
|
||||||
|
if getmetatable(args[i]) ~= Doc then expect(i, args[i], "document") end
|
||||||
|
end
|
||||||
|
|
||||||
|
if args.n == 0 then return empty end
|
||||||
|
if args.n == 1 then return args[1] end
|
||||||
|
|
||||||
|
args.tag = "concat"
|
||||||
|
return setmetatable(args, Doc)
|
||||||
|
end
|
||||||
|
|
||||||
|
Doc.__concat = concat
|
||||||
|
|
||||||
|
--- Indent later lines of the given document with the given number of spaces.
|
||||||
|
--
|
||||||
|
-- For instance, nesting the document
|
||||||
|
-- ```txt
|
||||||
|
-- foo
|
||||||
|
-- bar
|
||||||
|
-- ``
|
||||||
|
-- by two spaces will produce
|
||||||
|
-- ```txt
|
||||||
|
-- foo
|
||||||
|
-- bar
|
||||||
|
-- ```
|
||||||
|
--
|
||||||
|
-- @tparam number depth The number of spaces with which the document should be indented.
|
||||||
|
-- @tparam Doc doc The document to indent.
|
||||||
|
-- @treturn Doc The nested document.
|
||||||
|
-- @usage pretty.nest(2, pretty.text("foo\nbar"))
|
||||||
|
local function nest(depth, doc)
|
||||||
|
expect(1, depth, "number")
|
||||||
|
if getmetatable(doc) ~= Doc then expect(2, doc, "document") end
|
||||||
|
if depth <= 0 then error("depth must be a positive number", 2) end
|
||||||
|
|
||||||
|
return setmetatable({ tag = "nest", depth = depth, doc }, Doc)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function flatten(doc)
|
||||||
|
if doc.flat then return doc.flat end
|
||||||
|
|
||||||
|
local kind = doc.tag
|
||||||
|
if kind == "nil" or kind == "text" then
|
||||||
|
return doc
|
||||||
|
elseif kind == "concat" then
|
||||||
|
local out = setmetatable({ tag = "concat", n = doc.n }, Doc)
|
||||||
|
for i = 1, doc.n do out[i] = flatten(doc[i]) end
|
||||||
|
doc.flat, out.flat = out, out -- cache the flattened node
|
||||||
|
return out
|
||||||
|
elseif kind == "nest" then
|
||||||
|
return flatten(doc[1])
|
||||||
|
elseif kind == "group" then
|
||||||
|
return doc[1]
|
||||||
|
else
|
||||||
|
error("Unknown doc " .. kind)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Builds a document which is displayed on a single line if there is enough
|
||||||
|
-- room, or as normal if not.
|
||||||
|
--
|
||||||
|
-- @tparam Doc doc The document to group.
|
||||||
|
-- @treturn Doc The grouped document.
|
||||||
|
local function group(doc)
|
||||||
|
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
|
||||||
|
|
||||||
|
if doc.tag == "group" then return doc end -- Skip if already grouped.
|
||||||
|
|
||||||
|
local flattened = flatten(doc)
|
||||||
|
if flattened == doc then return doc end -- Also skip if flattening does nothing.
|
||||||
|
return setmetatable({ tag = "group", flattened, doc }, Doc)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_remaining(doc, width)
|
||||||
|
local kind = doc.tag
|
||||||
|
if kind == "nil" or kind == "line" then
|
||||||
|
return width
|
||||||
|
elseif kind == "text" then
|
||||||
|
return width - #doc.text
|
||||||
|
elseif kind == "concat" then
|
||||||
|
for i = 1, doc.n do
|
||||||
|
width = get_remaining(doc[i], width)
|
||||||
|
if width < 0 then break end
|
||||||
|
end
|
||||||
|
return width
|
||||||
|
elseif kind == "group" or kind == "nest" then
|
||||||
|
return get_remaining(kind[1])
|
||||||
|
else
|
||||||
|
error("Unknown doc " .. kind)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Display a document on the terminal.
|
||||||
|
--
|
||||||
|
-- @tparam Doc doc The document to render
|
||||||
|
-- @tparam[opt] number ribbon_frac The maximum fraction of the width that we should write in.
|
||||||
|
local function write(doc, ribbon_frac)
|
||||||
|
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
|
||||||
|
expect(2, ribbon_frac, "number", "nil")
|
||||||
|
|
||||||
|
local term = term
|
||||||
|
local width, height = term.getSize()
|
||||||
|
local ribbon_width = (ribbon_frac or 0.6) * width
|
||||||
|
if ribbon_width < 0 then ribbon_width = 0 end
|
||||||
|
if ribbon_width > width then ribbon_width = width end
|
||||||
|
|
||||||
|
local def_colour = term.getTextColour()
|
||||||
|
local current_colour = def_colour
|
||||||
|
|
||||||
|
local function go(doc, indent, col)
|
||||||
|
local kind = doc.tag
|
||||||
|
if kind == "nil" then
|
||||||
|
return col
|
||||||
|
elseif kind == "text" then
|
||||||
|
local doc_colour = doc.colour or def_colour
|
||||||
|
if doc_colour ~= current_colour then
|
||||||
|
term.setTextColour(doc_colour)
|
||||||
|
current_colour = doc_colour
|
||||||
|
end
|
||||||
|
|
||||||
|
str_write(doc.text)
|
||||||
|
|
||||||
|
return col + #doc.text
|
||||||
|
elseif kind == "line" then
|
||||||
|
local _, y = term.getCursorPos()
|
||||||
|
if y < height then
|
||||||
|
term.setCursorPos(indent + 1, y + 1)
|
||||||
|
else
|
||||||
|
term.scroll(1)
|
||||||
|
term.setCursorPos(indent + 1, height)
|
||||||
|
end
|
||||||
|
|
||||||
|
return indent
|
||||||
|
elseif kind == "concat" then
|
||||||
|
for i = 1, doc.n do col = go(doc[i], indent, col) end
|
||||||
|
return col
|
||||||
|
elseif kind == "nest" then
|
||||||
|
return go(doc[1], indent + doc.depth, col)
|
||||||
|
elseif kind == "group" then
|
||||||
|
if get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then
|
||||||
|
return go(doc[1], indent, col)
|
||||||
|
else
|
||||||
|
return go(doc[2], indent, col)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
error("Unknown doc " .. kind)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local col = math.max(term.getCursorPos() - 1, 0)
|
||||||
|
go(doc, 0, col)
|
||||||
|
if current_colour ~= def_colour then term.setTextColour(def_colour) end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Display a document on the terminal with a trailing new line.
|
||||||
|
--
|
||||||
|
-- @tparam Doc doc The document to render.
|
||||||
|
-- @tparam[opt] number ribbon_frac The maximum fraction of the width that we should write in.
|
||||||
|
local function print(doc, ribbon_frac)
|
||||||
|
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
|
||||||
|
expect(2, ribbon_frac, "number", "nil")
|
||||||
|
write(doc, ribbon_frac)
|
||||||
|
str_write("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Render a document, converting it into a string.
|
||||||
|
--
|
||||||
|
-- @tparam Doc doc The document to render.
|
||||||
|
-- @tparam[opt] number width The maximum width of this document. Note that long strings will not be wrapped to
|
||||||
|
-- fit this width - it is only used for finding the best layout.
|
||||||
|
-- @tparam[opt] number ribbon_frac The maximum fraction of the width that we should write in.
|
||||||
|
local function render(doc, width, ribbon_frac)
|
||||||
|
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
|
||||||
|
expect(2, width, "number", "nil")
|
||||||
|
expect(3, ribbon_frac, "number", "nil")
|
||||||
|
|
||||||
|
local ribbon_width
|
||||||
|
if width then
|
||||||
|
ribbon_width = (ribbon_frac or 0.6) * width
|
||||||
|
if ribbon_width < 0 then ribbon_width = 0 end
|
||||||
|
if ribbon_width > width then ribbon_width = width end
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = { n = 0 }
|
||||||
|
local function go(doc, indent, col)
|
||||||
|
local kind = doc.tag
|
||||||
|
if kind == "nil" then
|
||||||
|
return col
|
||||||
|
elseif kind == "text" then
|
||||||
|
append(out, doc.text)
|
||||||
|
return col + #doc.text
|
||||||
|
elseif kind == "line" then
|
||||||
|
append(out, "\n" .. (" "):rep(indent))
|
||||||
|
return indent
|
||||||
|
elseif kind == "concat" then
|
||||||
|
for i = 1, doc.n do col = go(doc[i], indent, col) end
|
||||||
|
return col
|
||||||
|
elseif kind == "nest" then
|
||||||
|
return go(doc[1], indent + doc.depth, col)
|
||||||
|
elseif kind == "group" then
|
||||||
|
if not width or get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then
|
||||||
|
return go(doc[1], indent, col)
|
||||||
|
else
|
||||||
|
return go(doc[2], indent, col)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
error("Unknown doc " .. kind)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
go(doc, 0, 0)
|
||||||
|
return table.concat(out, "", 1, out.n)
|
||||||
|
end
|
||||||
|
|
||||||
|
local keywords = {
|
||||||
|
[ "and" ] = true, [ "break" ] = true, [ "do" ] = true, [ "else" ] = true,
|
||||||
|
[ "elseif" ] = true, [ "end" ] = true, [ "false" ] = true, [ "for" ] = true,
|
||||||
|
[ "function" ] = 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
local comma = text(",")
|
||||||
|
local braces = text("{}")
|
||||||
|
local obrace, cbrace = text("{"), text("}")
|
||||||
|
local obracket, cbracket = text("["), text("] = ")
|
||||||
|
|
||||||
|
local function key_compare(a, b)
|
||||||
|
local ta, tb = type(a), type(b)
|
||||||
|
|
||||||
|
if ta == "string" then return tb ~= "string" or a < b
|
||||||
|
elseif tb == "string" then return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if ta == "number" then return tb ~= "number" or a < b end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function pretty_impl(obj, tracking)
|
||||||
|
local obj_type = type(obj)
|
||||||
|
if obj_type == "string" then
|
||||||
|
local formatted = ("%q"):format(obj):gsub("\\\n", "\\n")
|
||||||
|
return text(formatted, colours.red)
|
||||||
|
elseif obj_type == "number" then
|
||||||
|
return text(tostring(obj), colours.magenta)
|
||||||
|
elseif obj_type ~= "table" or tracking[obj] then
|
||||||
|
return text(tostring(obj), colours.lightGrey)
|
||||||
|
elseif getmetatable(obj) ~= nil and getmetatable(obj).__tostring then
|
||||||
|
return text(tostring(obj))
|
||||||
|
elseif next(obj) == nil then
|
||||||
|
return braces
|
||||||
|
else
|
||||||
|
tracking[obj] = true
|
||||||
|
local doc = setmetatable({ tag = "concat", n = 1, space_line }, Doc)
|
||||||
|
|
||||||
|
local length, keys, keysn = #obj, {}, 1
|
||||||
|
for k in pairs(obj) do keys[keysn], keysn = k, keysn + 1 end
|
||||||
|
table.sort(keys, key_compare)
|
||||||
|
|
||||||
|
for i = 1, keysn - 1 do
|
||||||
|
if i > 1 then append(doc, comma) append(doc, space_line) end
|
||||||
|
|
||||||
|
local k = keys[i]
|
||||||
|
local v = obj[k]
|
||||||
|
local ty = type(k)
|
||||||
|
if ty == "number" and k % 1 == 0 and k >= 1 and k <= length then
|
||||||
|
append(doc, pretty_impl(v, tracking))
|
||||||
|
elseif ty == "string" and not keywords[k] and k:match("^[%a_][%a%d_]*$") then
|
||||||
|
append(doc, text(k .. " = "))
|
||||||
|
append(doc, pretty_impl(v, tracking))
|
||||||
|
else
|
||||||
|
append(doc, obracket)
|
||||||
|
append(doc, pretty_impl(k, tracking))
|
||||||
|
append(doc, cbracket)
|
||||||
|
append(doc, pretty_impl(v, tracking))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tracking[obj] = nil
|
||||||
|
return group(concat(obrace, nest(2, concat(table.unpack(doc, 1, n))), space_line, cbrace))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Pretty-print an arbitrary object, converting it into a document.
|
||||||
|
--
|
||||||
|
-- This can then be rendered with @{write} or @{print}.
|
||||||
|
--
|
||||||
|
-- @param obj The object to pretty-print.
|
||||||
|
-- @treturn Doc The object formatted as a document.
|
||||||
|
-- @usage Display a table on the screen
|
||||||
|
-- local pretty = require "cc.pretty"
|
||||||
|
-- pretty.print(pretty.pretty({ 1, 2, 3 })
|
||||||
|
local function pretty(obj)
|
||||||
|
return pretty_impl(obj, {})
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
empty = empty,
|
||||||
|
space = space,
|
||||||
|
line = line,
|
||||||
|
space_line = space_line,
|
||||||
|
text = text,
|
||||||
|
concat = concat,
|
||||||
|
nest = nest,
|
||||||
|
group = group,
|
||||||
|
|
||||||
|
write = write,
|
||||||
|
print = print,
|
||||||
|
render = render,
|
||||||
|
|
||||||
|
pretty = pretty,
|
||||||
|
}
|
@ -6,6 +6,8 @@ if #tArgs > 0 then
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local pretty = require "cc.pretty"
|
||||||
|
|
||||||
local bRunning = true
|
local bRunning = true
|
||||||
local tCommandHistory = {}
|
local tCommandHistory = {}
|
||||||
local tEnv = {
|
local tEnv = {
|
||||||
@ -87,20 +89,11 @@ while bRunning do
|
|||||||
local n = 1
|
local n = 1
|
||||||
while n < tResults.n or n <= nForcePrint do
|
while n < tResults.n or n <= nForcePrint do
|
||||||
local value = tResults[ n + 1 ]
|
local value = tResults[ n + 1 ]
|
||||||
if type( value ) == "table" then
|
local ok, serialised = pcall(pretty.pretty, value)
|
||||||
local metatable = getmetatable( value )
|
if ok then
|
||||||
if type(metatable) == "table" and type(metatable.__tostring) == "function" then
|
pretty.print(serialised)
|
||||||
print( tostring( value ) )
|
|
||||||
else
|
|
||||||
local ok, serialised = pcall( textutils.serialise, value )
|
|
||||||
if ok then
|
|
||||||
print( serialised )
|
|
||||||
else
|
|
||||||
print( tostring( value ) )
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
print( tostring( value ) )
|
print(tostring(value))
|
||||||
end
|
end
|
||||||
n = n + 1
|
n = n + 1
|
||||||
end
|
end
|
||||||
|
208
src/test/resources/test-rom/spec/modules/cc/pretty_spec.lua
Normal file
208
src/test/resources/test-rom/spec/modules/cc/pretty_spec.lua
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
local with_window = require "test_helpers".with_window
|
||||||
|
|
||||||
|
describe("cc.pretty", function()
|
||||||
|
local pp = require("cc.pretty")
|
||||||
|
|
||||||
|
describe("text", function()
|
||||||
|
it("is constant for the empty string", function()
|
||||||
|
expect(pp.text("")):eq(pp.empty)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("is constant for a space", function()
|
||||||
|
expect(pp.text(" ")):eq(pp.space)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("is constant for a newline", function()
|
||||||
|
expect(pp.text("\n")):eq(pp.space_line)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("validates arguments", function()
|
||||||
|
expect.error(pp.text, 123):eq("bad argument #1 (expected string, got number)")
|
||||||
|
expect.error(pp.text, "", ""):eq("bad argument #2 (expected number, got string)")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("produces text documents", function()
|
||||||
|
expect(pp.text("a")):same({ tag = "text", text = "a" })
|
||||||
|
expect(pp.text("a", colours.grey)):same({ tag = "text", text = "a", colour = colours.grey })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("splits lines", function()
|
||||||
|
expect(pp.text("a\nb"))
|
||||||
|
:same(pp.concat(pp.text("a"), pp.space_line, pp.text("b")))
|
||||||
|
expect(pp.text("ab\ncd\nef"))
|
||||||
|
:same(pp.concat(pp.text("ab"), pp.space_line, pp.text("cd"), pp.space_line, pp.text("ef")))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("preserves empty lines", function()
|
||||||
|
expect(pp.text("a\n\nb"))
|
||||||
|
:same(pp.concat(pp.text("a"), pp.space_line, pp.space_line, pp.text("b")))
|
||||||
|
expect(pp.text("\n\nb"))
|
||||||
|
:same(pp.concat(pp.space_line, pp.space_line, pp.text("b")))
|
||||||
|
expect(pp.text("a\n\n"))
|
||||||
|
:same(pp.concat(pp.text("a"), pp.space_line, pp.space_line))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("concat", function()
|
||||||
|
it("returns empty with 0 arguments", function()
|
||||||
|
expect(pp.concat()):eq(pp.empty)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("acts as the identity with 1 argument", function()
|
||||||
|
local x = pp.text("test")
|
||||||
|
expect(pp.concat(x)):eq(x)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("coerces strings", function()
|
||||||
|
expect(pp.concat("a", "b")):same(pp.concat(pp.text("a"), pp.text("b")))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("validates arguments", function()
|
||||||
|
expect.error(pp.concat, 123):eq("bad argument #1 (expected document, got number)")
|
||||||
|
expect.error(pp.concat, "", {}):eq("bad argument #2 (expected document, got table)")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("can be used as an operator", function()
|
||||||
|
local a, b = pp.text("a"), pp.text("b")
|
||||||
|
expect(pp.concat(a, b)):same(a .. b)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("group", function()
|
||||||
|
it("is idempotent", function()
|
||||||
|
local x = pp.group(pp.text("a\nb"))
|
||||||
|
expect(pp.group(x)):eq(x)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("does nothing for flat strings", function()
|
||||||
|
local x = pp.text("a")
|
||||||
|
expect(pp.group(x)):eq(x)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Allows us to test
|
||||||
|
local function test_output(display)
|
||||||
|
it("displays the empty document", function()
|
||||||
|
expect(display(pp.empty)):same { "" }
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("displays a multiline string", function()
|
||||||
|
expect(display(pp.text("hello\nworld"))):same {
|
||||||
|
"hello",
|
||||||
|
"world",
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("displays a nested string", function()
|
||||||
|
expect(display(pp.nest(2, pp.concat("hello", pp.line, "world")))):same {
|
||||||
|
"hello",
|
||||||
|
" world",
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("displays a flattened group", function()
|
||||||
|
expect(display(pp.group(pp.concat("hello", pp.space_line, "world")))):same {
|
||||||
|
"hello world",
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(display(pp.group(pp.concat("hello", pp.line, "world")))):same {
|
||||||
|
"helloworld",
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("displays an expanded group", function()
|
||||||
|
expect(display(pp.group(pp.concat("hello darkness", pp.space_line, "my old friend")))):same {
|
||||||
|
"hello darkness",
|
||||||
|
"my old friend",
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("group removes nest", function()
|
||||||
|
expect(display(pp.group(pp.nest(2, pp.concat("hello", pp.space_line, "world"))))):same {
|
||||||
|
"hello world",
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe("write", function()
|
||||||
|
local function display(doc)
|
||||||
|
local w = with_window(20, 10, function() pp.write(doc) end)
|
||||||
|
local _, y = w.getCursorPos()
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
for i = 1, y do out[i] = w.getLine(i):gsub("%s+$", "") end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
test_output(display)
|
||||||
|
|
||||||
|
it("wraps a long string", function()
|
||||||
|
expect(display(pp.text("hello world this is a long string which will wrap"))):same {
|
||||||
|
"hello world this is",
|
||||||
|
"a long string which",
|
||||||
|
"will wrap",
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("render", function()
|
||||||
|
local function display(doc)
|
||||||
|
local rendered = pp.render(doc, 20)
|
||||||
|
local n, lines = 1, {}
|
||||||
|
for line in (rendered .. "\n"):gmatch("([^\n]*)\n") do lines[n], n = line, n + 1 end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
test_output(display)
|
||||||
|
|
||||||
|
it("does not wrap a long string", function()
|
||||||
|
expect(display(pp.text("hello world this is a long string which will wrap"))):same {
|
||||||
|
"hello world this is a long string which will wrap",
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("pretty", function()
|
||||||
|
--- We make use of "render" here, as it's considerably easier than checking against the actual structure.
|
||||||
|
-- However, it does also mean our tests are less unit-like.
|
||||||
|
local function pretty(x, width) return pp.render(pp.pretty(x), width) end
|
||||||
|
|
||||||
|
describe("tables", function()
|
||||||
|
it("displays empty tables", function()
|
||||||
|
expect(pp.pretty({})):same(pp.text("{}"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("displays list-like tables", function()
|
||||||
|
expect(pretty({ 1, 2, 3 })):eq("{ 1, 2, 3 }")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("displays mixed tables", function()
|
||||||
|
expect(pretty({ n = 3, 1, 2, 3 })):eq("{ n = 3, 1, 2, 3 }")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("escapes keys", function()
|
||||||
|
expect(pretty({ ["and"] = 1, ["not that"] = 2 })):eq('{ ["and"] = 1, ["not that"] = 2 }')
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("sorts keys", function()
|
||||||
|
expect(pretty({ c = 1, b = 2, a = 3 })):eq('{ a = 3, b = 2, c = 1 }')
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("groups tables", function()
|
||||||
|
expect(pretty({ 1, 2, 3 }, 4)):eq("{\n 1,\n 2,\n 3\n}")
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("shows numbers", function()
|
||||||
|
expect(pretty(123)):eq("123")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("shows strings", function()
|
||||||
|
expect(pretty("hello\nworld")):eq('"hello\\nworld"')
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("shows functions", function()
|
||||||
|
expect(pretty(pretty)):eq(tostring(pretty))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
Loading…
Reference in New Issue
Block a user