Module:Repr

require('strict') local libraryUtil = require("libraryUtil") local checkType = libraryUtil.checkType local checkTypeForNamedArg = libraryUtil.checkTypeForNamedArg

local defaultOptions = { pretty = false, tabs = true, semicolons = false, spaces = 4, sortKeys = true, depth = 0, }

-- Define the reprRecursive variable here so that we can call the reprRecursive -- function from renderSequence and renderKeyValueTable without getting -- "Tried to read nil global reprRecursive" errors. local reprRecursive

local luaKeywords = { ["and"]     = true, ["break"]   = true, ["do"]      = true, ["else"]    = true, ["elseif"]  = true, ["end"]     = true, ["false"]   = true, ["for"]     = true, ["function"] = true, ["if"]      = true, ["in"]      = true, ["local"]   = true, ["nil"]     = true, ["not"]     = true, ["or"]      = true, ["repeat"]  = true, ["return"]  = true, ["then"]    = true, ["true"]    = true, ["until"]   = true, ["while"]   = true, }

-- -- Whether the given value is a valid Lua identifier (i.e. whether it can be -- used as a variable name.) -- local function isLuaIdentifier(str) return type(str) == "string" -- Must start with a-z, A-Z or underscore, and can only contain -- a-z, A-Z, 0-9 and underscore and str:find("^[%a_][%a%d_]*$") ~= nil -- Cannot be a Lua keyword and not luaKeywords[str] end

-- -- Render a string representation. -- local function renderString(s) return (("%q"):format(s):gsub("\\\n", "\\n")) end

-- -- Render a number representation. -- local function renderNumber(n) if n == math.huge then return "math.huge" elseif n == -math.huge then return "-math.huge" else return tostring(n) end end

-- -- Whether a table has a __tostring metamethod. -- local function hasTostringMetamethod(t) return getmetatable(t) and type(getmetatable(t).__tostring) == "function" end

-- -- Pretty print a sequence of string representations. -- This can be made to represent different constructs depending on the values -- of prefix, suffix, and separator. The amount of whitespace is controlled by -- the depth and indent parameters. -- local function prettyPrintItemsAtDepth(items, prefix, suffix, separator, indent, depth) local whitespaceAtCurrentDepth = "\n" .. indent:rep(depth) local whitespaceAtNextDepth = whitespaceAtCurrentDepth .. indent local ret = {prefix, whitespaceAtNextDepth} local first = items[1] if first ~= nil then table.insert(ret, first) end for i = 2, #items do		table.insert(ret, separator) table.insert(ret, whitespaceAtNextDepth) table.insert(ret, items[i]) end table.insert(ret, whitespaceAtCurrentDepth) table.insert(ret, suffix) return table.concat(ret) end

-- -- Render a sequence of string representations. -- This can be made to represent different constructs depending on the values of -- prefix, suffix and separator. -- local function renderItems(items, prefix, suffix, separator) return prefix .. table.concat(items, separator .. " ") .. suffix end

-- -- Render a regular table (a non-cyclic table with no __tostring metamethod). -- This can be a sequence table, a key-value table, or a mix of the two. -- local function renderNormalTable(t, context, depth) local items = {}

-- Render the items in the sequence part local seen = {} for i, value in ipairs(t) do		table.insert(items, reprRecursive(t[i], context, depth + 1)) seen[i] = true end -- Render the items in the key-value part local keyOrder = {} local keyValueStrings = {} for k, v in pairs(t) do		if not seen[k] then local kStr = isLuaIdentifier(k) and k or ("[" .. reprRecursive(k, context, depth + 1) .. "]") local vStr = reprRecursive(v, context, depth + 1) table.insert(keyOrder, kStr) keyValueStrings[kStr] = vStr end end if context.sortKeys then table.sort(keyOrder) end for _, kStr in ipairs(keyOrder) do		table.insert(items, string.format("%s = %s", kStr, keyValueStrings[kStr])) end -- Render the table structure local prefix = "{" local suffix = "}" if context.pretty then return prettyPrintItemsAtDepth(			items,			prefix,			suffix,			context.separator,			context.indent,			depth		) else return renderItems(items, prefix, suffix, context.separator) end end

-- -- Render the given table. -- As well as rendering regular tables, this function also renders cyclic tables -- and tables with a __tostring metamethod. -- local function renderTable(t, context, depth) if hasTostringMetamethod(t) then return tostring(t) elseif context.shown[t] then return "{CYCLIC}" end context.shown[t] = true local result = renderNormalTable(t, context, depth) context.shown[t] = false return result end

-- -- Recursively render a string representation of the given value. -- function reprRecursive(value, context, depth) if value == nil then return "nil" end local valueType = type(value) if valueType == "boolean" then return tostring(value) elseif valueType == "number" then return renderNumber(value) elseif valueType == "string" then return renderString(value) elseif valueType == "table" then return renderTable(value, context, depth) else return "<" .. valueType .. ">"	end end

-- -- Normalize a table of options passed by the user. -- Any values not specified will be assigned default values. -- local function normalizeOptions(options) options = options or {} local ret = {} for option, defaultValue in pairs(defaultOptions) do		local value = options[option] if value ~= nil then if type(value) == type(defaultValue) then ret[option] = value else error(					string.format( 'Invalid type for option "%s" (expected %s, received %s)', option, type(defaultValue), type(value) ),					3				)			end else ret[option] = defaultValue end end return ret end

-- -- Get the indent from the options table. -- local function getIndent(options) if options.tabs then return "\t" else return string.rep(" ", options.spaces) end end

-- -- Render a string representation of the given value. -- local function repr(value, options) checkType("repr", 2, options, "table", true) options = normalizeOptions(options) local context = {}

context.pretty = options.pretty if context.pretty then context.indent = getIndent(options) else context.indent = "" end if options.semicolons then context.separator = ";" else context.separator = "," end context.sortKeys = options.sortKeys context.shown = {} local depth = options.depth return reprRecursive(value, context, depth) end

-- -- Render a string representation of the given function invocation. -- local function invocationRepr(keywordArgs) checkType("invocationRepr", 1, keywordArgs, "table") checkTypeForNamedArg("invocationRepr", "funcName", keywordArgs.funcName, "string") checkTypeForNamedArg("invocationRepr", "args", keywordArgs.args, "table", true) checkTypeForNamedArg("invocationRepr", "options", keywordArgs.options, "table", true) local options = normalizeOptions(keywordArgs.options) local depth = options.depth

options.depth = depth + 1 local items = {} if keywordArgs.args then for _, arg in ipairs(keywordArgs.args) do			table.insert(items, repr(arg, options)) end end

local prefix = "("	local suffix = ")" local separator = "," local renderedArgs if options.pretty then renderedArgs = prettyPrintItemsAtDepth(			items,			prefix,			suffix,			separator,			getIndent(options),			depth		) else renderedArgs = renderItems(items, prefix, suffix, separator) end return keywordArgs.funcName .. renderedArgs end

return { _isLuaIdentifier = isLuaIdentifier, repr = repr, invocationRepr = invocationRepr, }