295 lines
10 KiB
Lua
295 lines
10 KiB
Lua
-------------------------------------------------------------------------------
|
|
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
|
--
|
|
-- All rights reserved.
|
|
--
|
|
-- This program and the accompanying materials are made available
|
|
-- under the terms of the Eclipse Public License v1.0 which
|
|
-- accompanies this distribution, and is available at
|
|
-- http://www.eclipse.org/legal/epl-v10.html
|
|
--
|
|
-- This program and the accompanying materials are also made available
|
|
-- under the terms of the MIT public license which accompanies this
|
|
-- distribution, and is available at http://www.lua.org/license.html
|
|
--
|
|
-- Contributors:
|
|
-- Fabien Fleutot - API and implementation
|
|
--
|
|
----------------------------------------------------------------------
|
|
|
|
----------------------------------------------------------------------
|
|
----------------------------------------------------------------------
|
|
--
|
|
-- Lua objects pretty-printer
|
|
--
|
|
----------------------------------------------------------------------
|
|
----------------------------------------------------------------------
|
|
|
|
local M = { }
|
|
|
|
M.DEFAULT_CFG = {
|
|
hide_hash = false; -- Print the non-array part of tables?
|
|
metalua_tag = true; -- Use Metalua's backtick syntax sugar?
|
|
fix_indent = nil; -- If a number, number of indentation spaces;
|
|
-- If false, indent to the previous brace.
|
|
line_max = nil; -- If a number, tries to avoid making lines with
|
|
-- more than this number of chars.
|
|
initial_indent = 0; -- If a number, starts at this level of indentation
|
|
keywords = { }; -- Set of keywords which must not use Lua's field
|
|
-- shortcuts {["foo"]=...} -> {foo=...}
|
|
}
|
|
|
|
local function valid_id(cfg, x)
|
|
if type(x) ~= "string" then return false end
|
|
if not x:match "^[a-zA-Z_][a-zA-Z0-9_]*$" then return false end
|
|
if cfg.keywords and cfg.keywords[x] then return false end
|
|
return true
|
|
end
|
|
|
|
local __tostring_cache = setmetatable({ }, {__mode='k'})
|
|
|
|
-- Retrieve the string produced by `__tostring` metamethod if present,
|
|
-- return `false` otherwise. Cached in `__tostring_cache`.
|
|
local function __tostring(x)
|
|
local the_string = __tostring_cache[x]
|
|
if the_string~=nil then return the_string end
|
|
local mt = getmetatable(x)
|
|
if mt then
|
|
local __tostring = mt.__tostring
|
|
if __tostring then
|
|
the_string = __tostring(x)
|
|
__tostring_cache[x] = the_string
|
|
return the_string
|
|
end
|
|
end
|
|
if x~=nil then __tostring_cache[x] = false end -- nil is an illegal key
|
|
return false
|
|
end
|
|
|
|
local xlen -- mutually recursive with `xlen_type`
|
|
|
|
local xlen_cache = setmetatable({ }, {__mode='k'})
|
|
|
|
-- Helpers for the `xlen` function
|
|
local xlen_type = {
|
|
["nil"] = function ( ) return 3 end;
|
|
number = function (x) return #tostring(x) end;
|
|
boolean = function (x) return x and 4 or 5 end;
|
|
string = function (x) return #string.format("%q",x) end;
|
|
}
|
|
|
|
function xlen_type.table (adt, cfg, nested)
|
|
local custom_string = __tostring(adt)
|
|
if custom_string then return #custom_string end
|
|
|
|
-- Circular referenced objects are printed with the plain
|
|
-- `tostring` function in nested positions.
|
|
if nested [adt] then return #tostring(adt) end
|
|
nested [adt] = true
|
|
|
|
local has_tag = cfg.metalua_tag and valid_id(cfg, adt.tag)
|
|
local alen = #adt
|
|
local has_arr = alen>0
|
|
local has_hash = false
|
|
local x = 0
|
|
|
|
if not cfg.hide_hash then
|
|
-- first pass: count hash-part
|
|
for k, v in pairs(adt) do
|
|
if k=="tag" and has_tag then
|
|
-- this is the tag -> do nothing!
|
|
elseif type(k)=="number" and k<=alen and math.fmod(k,1)==0 and k>0 then
|
|
-- array-part pair -> do nothing!
|
|
else
|
|
has_hash = true
|
|
if valid_id(cfg, k) then x=x+#k
|
|
else x = x + xlen (k, cfg, nested) + 2 end -- count surrounding brackets
|
|
x = x + xlen (v, cfg, nested) + 5 -- count " = " and ", "
|
|
end
|
|
end
|
|
end
|
|
|
|
for i = 1, alen do x = x + xlen (adt[i], nested) + 2 end -- count ", "
|
|
|
|
nested[adt] = false -- No more nested calls
|
|
|
|
if not (has_tag or has_arr or has_hash) then return 3 end
|
|
if has_tag then x=x+#adt.tag+1 end
|
|
if not (has_arr or has_hash) then return x end
|
|
if not has_hash and alen==1 and type(adt[1])~="table" then
|
|
return x-2 -- substract extraneous ", "
|
|
end
|
|
return x+2 -- count "{ " and " }", substract extraneous ", "
|
|
end
|
|
|
|
|
|
-- Compute the number of chars it would require to display the table
|
|
-- on a single line. Helps to decide whether some carriage returns are
|
|
-- required. Since the size of each sub-table is required many times,
|
|
-- it's cached in [xlen_cache].
|
|
xlen = function (x, cfg, nested)
|
|
-- no need to compute length for 1-line prints
|
|
if not cfg.line_max then return 0 end
|
|
nested = nested or { }
|
|
if x==nil then return #"nil" end
|
|
local len = xlen_cache[x]
|
|
if len then return len end
|
|
local f = xlen_type[type(x)]
|
|
if not f then return #tostring(x) end
|
|
len = f (x, cfg, nested)
|
|
xlen_cache[x] = len
|
|
return len
|
|
end
|
|
|
|
local function consider_newline(p, len)
|
|
if not p.cfg.line_max then return end
|
|
if p.current_offset + len <= p.cfg.line_max then return end
|
|
if p.indent < p.current_offset then
|
|
p:acc "\n"; p:acc ((" "):rep(p.indent))
|
|
p.current_offset = p.indent
|
|
end
|
|
end
|
|
|
|
local acc_value
|
|
|
|
local acc_type = {
|
|
["nil"] = function(p) p:acc("nil") end;
|
|
number = function(p, adt) p:acc (tostring (adt)) end;
|
|
string = function(p, adt) p:acc ((string.format ("%q", adt):gsub("\\\n", "\\n"))) end;
|
|
boolean = function(p, adt) p:acc (adt and "true" or "false") end }
|
|
|
|
-- Indentation:
|
|
-- * if `cfg.fix_indent` is set to a number:
|
|
-- * add this number of space for each level of depth
|
|
-- * return to the line as soon as it flushes things further left
|
|
-- * if not, tabulate to one space after the opening brace.
|
|
-- * as a result, it never saves right-space to return before first element
|
|
|
|
function acc_type.table(p, adt)
|
|
if p.nested[adt] then p:acc(tostring(adt)); return end
|
|
p.nested[adt] = true
|
|
|
|
local has_tag = p.cfg.metalua_tag and valid_id(p.cfg, adt.tag)
|
|
local alen = #adt
|
|
local has_arr = alen>0
|
|
local has_hash = false
|
|
|
|
local previous_indent = p.indent
|
|
|
|
if has_tag then p:acc("`"); p:acc(adt.tag) end
|
|
|
|
local function indent(p)
|
|
if not p.cfg.fix_indent then p.indent = p.current_offset
|
|
else p.indent = p.indent + p.cfg.fix_indent end
|
|
end
|
|
|
|
-- First pass: handle hash-part
|
|
if not p.cfg.hide_hash then
|
|
for k, v in pairs(adt) do
|
|
|
|
if has_tag and k=='tag' then -- pass the 'tag' field
|
|
elseif type(k)=="number" and k<=alen and k>0 and math.fmod(k,1)==0 then
|
|
-- pass array-part keys (consecutive ints less than `#adt`)
|
|
else -- hash-part keys
|
|
if has_hash then p:acc ", " else -- 1st hash-part pair ever found
|
|
p:acc "{ "; indent(p)
|
|
end
|
|
|
|
-- Determine whether a newline is required
|
|
local is_id, expected_len=valid_id(p.cfg, k)
|
|
if is_id then expected_len=#k+xlen(v, p.cfg, p.nested)+#" = , "
|
|
else expected_len = xlen(k, p.cfg, p.nested)+xlen(v, p.cfg, p.nested)+#"[] = , " end
|
|
consider_newline(p, expected_len)
|
|
|
|
-- Print the key
|
|
if is_id then p:acc(k); p:acc " = " else
|
|
p:acc "["; acc_value (p, k); p:acc "] = "
|
|
end
|
|
|
|
acc_value (p, v) -- Print the value
|
|
has_hash = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Now we know whether there's a hash-part, an array-part, and a tag.
|
|
-- Tag and hash-part are already printed if they're present.
|
|
if not has_tag and not has_hash and not has_arr then p:acc "{ }";
|
|
elseif has_tag and not has_hash and not has_arr then -- nothing, tag already in acc
|
|
else
|
|
assert (has_hash or has_arr) -- special case { } already handled
|
|
local no_brace = false
|
|
if has_hash and has_arr then p:acc ", "
|
|
elseif has_tag and not has_hash and alen==1 and type(adt[1])~="table" then
|
|
-- No brace required; don't print "{", remember not to print "}"
|
|
p:acc (" "); acc_value (p, adt[1]) -- indent= indent+(cfg.fix_indent or 0))
|
|
no_brace = true
|
|
elseif not has_hash then
|
|
-- Braces required, but not opened by hash-part handler yet
|
|
p:acc "{ "; indent(p)
|
|
end
|
|
|
|
-- 2nd pass: array-part
|
|
if not no_brace and has_arr then
|
|
local expected_len = xlen(adt[1], p.cfg, p.nested)
|
|
consider_newline(p, expected_len)
|
|
acc_value(p, adt[1]) -- indent+(cfg.fix_indent or 0)
|
|
for i=2, alen do
|
|
p:acc ", ";
|
|
consider_newline(p, xlen(adt[i], p.cfg, p.nested))
|
|
acc_value (p, adt[i]) --indent+(cfg.fix_indent or 0)
|
|
end
|
|
end
|
|
if not no_brace then p:acc " }" end
|
|
end
|
|
p.nested[adt] = false -- No more nested calls
|
|
p.indent = previous_indent
|
|
end
|
|
|
|
|
|
function acc_value(p, v)
|
|
local custom_string = __tostring(v)
|
|
if custom_string then p:acc(custom_string) else
|
|
local f = acc_type[type(v)]
|
|
if f then f(p, v) else p:acc(tostring(v)) end
|
|
end
|
|
end
|
|
|
|
|
|
-- FIXME: new_indent seems to be always nil?!s detection
|
|
-- FIXME: accumulator function should be configurable,
|
|
-- so that print() doesn't need to bufferize the whole string
|
|
-- before starting to print.
|
|
function M.tostring(t, cfg)
|
|
|
|
cfg = cfg or M.DEFAULT_CFG or { }
|
|
|
|
local p = {
|
|
cfg = cfg;
|
|
indent = 0;
|
|
current_offset = cfg.initial_indent or 0;
|
|
buffer = { };
|
|
nested = { };
|
|
acc = function(self, str)
|
|
table.insert(self.buffer, str)
|
|
self.current_offset = self.current_offset + #str
|
|
end;
|
|
}
|
|
acc_value(p, t)
|
|
return table.concat(p.buffer)
|
|
end
|
|
|
|
function M.print(...) return print(M.tostring(...)) end
|
|
function M.sprintf(fmt, ...)
|
|
local args={...}
|
|
for i, v in pairs(args) do
|
|
local t=type(v)
|
|
if t=='table' then args[i]=M.tostring(v)
|
|
elseif t=='nil' then args[i]='nil' end
|
|
end
|
|
return string.format(fmt, unpack(args))
|
|
end
|
|
|
|
function M.printf(...) print(M.sprintf(...)) end
|
|
|
|
return M |