1
Jianw
9 天以前 70f29da38121b9a467841253e3268feb5df02902
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
-------------------------------------------------------------------------------
-- 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 = { }
local unpack = table.unpack or unpack
 
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