1local inspect ={
2  _VERSION = 'inspect.lua 3.1.0',
3  _URL     = 'http://github.com/kikito/inspect.lua',
4  _DESCRIPTION = 'human-readable representations of tables',
5  _LICENSE = [[
6    MIT LICENSE
7
8    Copyright (c) 2013 Enrique García Cota
9
10    Permission is hereby granted, free of charge, to any person obtaining a
11    copy of this software and associated documentation files (the
12    "Software"), to deal in the Software without restriction, including
13    without limitation the rights to use, copy, modify, merge, publish,
14    distribute, sublicense, and/or sell copies of the Software, and to
15    permit persons to whom the Software is furnished to do so, subject to
16    the following conditions:
17
18    The above copyright notice and this permission notice shall be included
19    in all copies or substantial portions of the Software.
20
21    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22    OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24    IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
25    CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
26    TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27    SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28  ]]
29}
30
31local tostring = tostring
32
33inspect.KEY       = setmetatable({}, {__tostring = function() return 'inspect.KEY' end})
34inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end})
35
36local function rawpairs(t)
37  return next, t, nil
38end
39
40-- Apostrophizes the string if it has quotes, but not aphostrophes
41-- Otherwise, it returns a regular quoted string
42local function smartQuote(str)
43  if str:match('"') and not str:match("'") then
44    return "'" .. str .. "'"
45  end
46  return '"' .. str:gsub('"', '\\"') .. '"'
47end
48
49-- \a => '\\a', \0 => '\\0', 31 => '\31'
50local shortControlCharEscapes = {
51  ["\a"] = "\\a",  ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
52  ["\r"] = "\\r",  ["\t"] = "\\t", ["\v"] = "\\v"
53}
54local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031
55for i=0, 31 do
56  local ch = string.char(i)
57  if not shortControlCharEscapes[ch] then
58    shortControlCharEscapes[ch] = "\\"..i
59    longControlCharEscapes[ch]  = string.format("\\%03d", i)
60  end
61end
62
63local function escape(str)
64  return (str:gsub("\\", "\\\\")
65             :gsub("(%c)%f[0-9]", longControlCharEscapes)
66             :gsub("%c", shortControlCharEscapes))
67end
68
69local function isIdentifier(str)
70  return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" )
71end
72
73local function isSequenceKey(k, sequenceLength)
74  return type(k) == 'number'
75     and 1 <= k
76     and k <= sequenceLength
77     and math.floor(k) == k
78end
79
80local defaultTypeOrders = {
81  ['number']   = 1, ['boolean']  = 2, ['string'] = 3, ['table'] = 4,
82  ['function'] = 5, ['userdata'] = 6, ['thread'] = 7
83}
84
85local function sortKeys(a, b)
86  local ta, tb = type(a), type(b)
87
88  -- strings and numbers are sorted numerically/alphabetically
89  if ta == tb and (ta == 'string' or ta == 'number') then return a < b end
90
91  local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
92  -- Two default types are compared according to the defaultTypeOrders table
93  if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb]
94  elseif dta     then return true  -- default types before custom ones
95  elseif dtb     then return false -- custom types after default ones
96  end
97
98  -- custom types are sorted out alphabetically
99  return ta < tb
100end
101
102-- For implementation reasons, the behavior of rawlen & # is "undefined" when
103-- tables aren't pure sequences. So we implement our own # operator.
104local function getSequenceLength(t)
105  local len = 1
106  local v = rawget(t,len)
107  while v ~= nil do
108    len = len + 1
109    v = rawget(t,len)
110  end
111  return len - 1
112end
113
114local function getNonSequentialKeys(t)
115  local keys, keysLength = {}, 0
116  local sequenceLength = getSequenceLength(t)
117  for k,_ in rawpairs(t) do
118    if not isSequenceKey(k, sequenceLength) then
119      keysLength = keysLength + 1
120      keys[keysLength] = k
121    end
122  end
123  table.sort(keys, sortKeys)
124  return keys, keysLength, sequenceLength
125end
126
127local function countTableAppearances(t, tableAppearances)
128  tableAppearances = tableAppearances or {}
129
130  if type(t) == 'table' then
131    if not tableAppearances[t] then
132      tableAppearances[t] = 1
133      for k,v in rawpairs(t) do
134        countTableAppearances(k, tableAppearances)
135        countTableAppearances(v, tableAppearances)
136      end
137      countTableAppearances(getmetatable(t), tableAppearances)
138    else
139      tableAppearances[t] = tableAppearances[t] + 1
140    end
141  end
142
143  return tableAppearances
144end
145
146local copySequence = function(s)
147  local copy, len = {}, #s
148  for i=1, len do copy[i] = s[i] end
149  return copy, len
150end
151
152local function makePath(path, ...)
153  local keys = {...}
154  local newPath, len = copySequence(path)
155  for i=1, #keys do
156    newPath[len + i] = keys[i]
157  end
158  return newPath
159end
160
161local function processRecursive(process, item, path, visited)
162  if item == nil then return nil end
163  if visited[item] then return visited[item] end
164
165  local processed = process(item, path)
166  if type(processed) == 'table' then
167    local processedCopy = {}
168    visited[item] = processedCopy
169    local processedKey
170
171    for k,v in rawpairs(processed) do
172      processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
173      if processedKey ~= nil then
174        processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
175      end
176    end
177
178    local mt  = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
179    if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field
180    setmetatable(processedCopy, mt)
181    processed = processedCopy
182  end
183  return processed
184end
185
186
187
188-------------------------------------------------------------------
189
190local Inspector = {}
191local Inspector_mt = {__index = Inspector}
192
193function Inspector:puts(...)
194  local args   = {...}
195  local buffer = self.buffer
196  local len    = #buffer
197  for i=1, #args do
198    len = len + 1
199    buffer[len] = args[i]
200  end
201end
202
203function Inspector:down(f)
204  self.level = self.level + 1
205  f()
206  self.level = self.level - 1
207end
208
209function Inspector:tabify()
210  self:puts(self.newline, string.rep(self.indent, self.level))
211end
212
213function Inspector:alreadyVisited(v)
214  return self.ids[v] ~= nil
215end
216
217function Inspector:getId(v)
218  local id = self.ids[v]
219  if not id then
220    local tv = type(v)
221    id              = (self.maxIds[tv] or 0) + 1
222    self.maxIds[tv] = id
223    self.ids[v]     = id
224  end
225  return tostring(id)
226end
227
228function Inspector:putKey(k)
229  if isIdentifier(k) then return self:puts(k) end
230  self:puts("[")
231  self:putValue(k)
232  self:puts("]")
233end
234
235function Inspector:putTable(t)
236  if t == inspect.KEY or t == inspect.METATABLE then
237    self:puts(tostring(t))
238  elseif self:alreadyVisited(t) then
239    self:puts('<table ', self:getId(t), '>')
240  elseif self.level >= self.depth then
241    self:puts('{...}')
242  else
243    if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end
244
245    local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t)
246    local mt                = getmetatable(t)
247    if (vim and sequenceLength == 0 and nonSequentialKeysLength == 0
248        and mt == vim._empty_dict_mt) then
249      self:puts(tostring(t))
250      return
251    end
252
253    self:puts('{')
254    self:down(function()
255      local count = 0
256      for i=1, sequenceLength do
257        if count > 0 then self:puts(',') end
258        self:puts(' ')
259        self:putValue(t[i])
260        count = count + 1
261      end
262
263      for i=1, nonSequentialKeysLength do
264        local k = nonSequentialKeys[i]
265        if count > 0 then self:puts(',') end
266        self:tabify()
267        self:putKey(k)
268        self:puts(' = ')
269        self:putValue(t[k])
270        count = count + 1
271      end
272
273      if type(mt) == 'table' then
274        if count > 0 then self:puts(',') end
275        self:tabify()
276        self:puts('<metatable> = ')
277        self:putValue(mt)
278      end
279    end)
280
281    if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing }
282      self:tabify()
283    elseif sequenceLength > 0 then -- array tables have one extra space before closing }
284      self:puts(' ')
285    end
286
287    self:puts('}')
288  end
289end
290
291function Inspector:putValue(v)
292  local tv = type(v)
293
294  if tv == 'string' then
295    self:puts(smartQuote(escape(v)))
296  elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
297         tv == 'cdata' or tv == 'ctype' or (vim and v == vim.NIL) then
298    self:puts(tostring(v))
299  elseif tv == 'table' then
300    self:putTable(v)
301  else
302    self:puts('<', tv, ' ', self:getId(v), '>')
303  end
304end
305
306-------------------------------------------------------------------
307
308function inspect.inspect(root, options)
309  options       = options or {}
310
311  local depth   = options.depth   or math.huge
312  local newline = options.newline or '\n'
313  local indent  = options.indent  or '  '
314  local process = options.process
315
316  if process then
317    root = processRecursive(process, root, {}, {})
318  end
319
320  local inspector = setmetatable({
321    depth            = depth,
322    level            = 0,
323    buffer           = {},
324    ids              = {},
325    maxIds           = {},
326    newline          = newline,
327    indent           = indent,
328    tableAppearances = countTableAppearances(root)
329  }, Inspector_mt)
330
331  inspector:putValue(root)
332
333  return table.concat(inspector.buffer)
334end
335
336setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end })
337
338return inspect
339