| ------------------------------------------------------------------------------- |
| -- 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 |