-- Copyright (c) 2013 Enrique GarcĂa Cota -- -- Permission is hereby granted, free of charge, to any person obtaining a -- copy of this software and associated documentation files (the -- "Software"), to deal in the Software without restriction, including -- without limitation the rights to use, copy, modify, merge, publish, -- distribute, sublicense, and/or sell copies of the Software, and to -- permit persons to whom the Software is furnished to do so, subject to -- the following conditions: -- -- The above copyright notice and this permission notice shall be included -- in all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -- OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- Apostrophizes the string if it has quotes, but not aphostrophes -- Otherwise, it returns a regular quoted string local function smartQuote(str) if str:match('"') and not str:match("'") then return "'" .. str .. "'" end return '"' .. str:gsub('"', '\\"') .. '"' end local controlCharsTranslation = { ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" } local function escapeChar(c) return controlCharsTranslation[c] end local function escape(str) local result = str:gsub("\\", "\\\\"):gsub("(%c)", escapeChar) return result end local function isIdentifier(str) return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) end local function isArrayKey(k, length) return type(k) == 'number' and 1 <= k and k <= length end local function isDictionaryKey(k, length) return not isArrayKey(k, length) end local defaultTypeOrders = { ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 } local function sortKeys(a, b) local ta, tb = type(a), type(b) -- strings and numbers are sorted numerically/alphabetically if ta == tb and (ta == 'string' or ta == 'number') then return a < b end local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] -- Two default types are compared according to the defaultTypeOrders table if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] elseif dta then return true -- default types before custom ones elseif dtb then return false -- custom types after default ones end -- custom types are sorted out alphabetically return ta < tb end local function getDictionaryKeys(t) local keys, length = {}, #t for k,_ in pairs(t) do if isDictionaryKey(k, length) then table.insert(keys, k) end end table.sort(keys, sortKeys) return keys end local function getToStringResultSafely(t, mt) local __tostring = type(mt) == 'table' and rawget(mt, '__tostring') local str, ok if type(__tostring) == 'function' then ok, str = pcall(__tostring, t) str = ok and str or 'error: ' .. tostring(str) end if type(str) == 'string' and #str > 0 then return str end end local maxIdsMetaTable = { __index = function(self, typeName) rawset(self, typeName, 0) return 0 end } local idsMetaTable = { __index = function (self, typeName) local col = setmetatable({}, {__mode = "kv"}) rawset(self, typeName, col) return col end } local function countTableAppearances(t, tableAppearances) tableAppearances = tableAppearances or setmetatable({}, {__mode = "k"}) if type(t) == 'table' then if not tableAppearances[t] then tableAppearances[t] = 1 for k,v in pairs(t) do countTableAppearances(k, tableAppearances) countTableAppearances(v, tableAppearances) end countTableAppearances(getmetatable(t), tableAppearances) else tableAppearances[t] = tableAppearances[t] + 1 end end return tableAppearances end local function parse_filter(filter) if type(filter) == 'function' then return filter end -- not a function, so it must be a table or table-like filter = type(filter) == 'table' and filter or {filter} local dictionary = {} for _,v in pairs(filter) do dictionary[v] = true end return function(x) return dictionary[x] end end local function makePath(path, key) local newPath, len = {}, #path for i=1, len do newPath[i] = path[i] end newPath[len+1] = key return newPath end ------------------------------------------------------------------- function inspect(rootObject, options) options = options or {} local depth = options.depth or math.huge local filter = parse_filter(options.filter or {}) local tableAppearances = countTableAppearances(rootObject) local buffer = {} local maxIds = setmetatable({}, maxIdsMetaTable) local ids = setmetatable({}, idsMetaTable) local level = 0 local blen = 0 -- buffer length local function puts(...) local args = {...} for i=1, #args do blen = blen + 1 buffer[blen] = tostring(args[i]) end end local function down(f) level = level + 1 f() level = level - 1 end local function tabify() puts("\n", string.rep(" ", level)) end local function commaControl(needsComma) if needsComma then puts(',') end return true end local function alreadyVisited(v) return ids[type(v)][v] ~= nil end local function getId(v) local tv = type(v) local id = ids[tv][v] if not id then id = maxIds[tv] + 1 maxIds[tv] = id ids[tv][v] = id end return id end local putValue -- forward declaration that needs to go before putTable & putKey local function putKey(k) if isIdentifier(k) then return puts(k) end puts( "[" ) putValue(k, {}) puts("]") end local function putTable(t, path) if alreadyVisited(t) then puts('