Module:Repr

From OODA WIKI

This module contains functions for generating string representations of Lua objects. It is inspired by Python's repr function.

Usage

To use the module, first you have to import it.

local mRepr = require("Module:Repr")

Then you can use the functions it contains. The documentation for each function is below.

repr

This function generates a string representation of any given Lua object. The idea is that if you copy the string this function produces it, and paste it back into a Lua program, then you should be able to reproduce the original object. This doesn't work for all values, but it should hold for simple cases.

For example, mRepr.repr({bool = true, number = 6, str = "hello world"}) will output the string {bool = true, number = 6, str = "hello world"}.

Basic syntax:

mRepr.repr(value)

Full syntax:

mRepr.repr(value, options)

Parameters:

  • value: The value to convert to a string. This can be any Lua value. This parameter is optional, and defaults to nil.
  • options: A table of options. This parameter is optional.

The following options can be specified in the options table:

  • pretty: If true, output the string in "pretty" format (as in pretty-printing). This will add new lines and indentation between table items. If false, format everything on one line. The default is false.
  • tabs: If true, indent with tabs; otherwise, indent with spaces. The default is true. This only has an effect if pretty is true.
  • spaces: The number of spaces to indent with, if tabs is false. The default is 4. This only has an effect if pretty is true.
  • semicolons: If true, table items are separated with semicolons. If false, they are separated with spaces. The default is false.
  • sortKeys: If true, sort table keys in lexical order, after other table key formatting has been applied (such as adding square brackets). If false, table keys are output in arbitrary order (the order they are processed by the pairs function). The default is true.
  • depth: The indentation depth to output the top-level object at. The default is 0. This only has an effect if pretty is true.

Features:

  • The function handles cyclic tables gracefully; when it detects a cycle, the inner table is rendered as {CYCLIC}.
  • __tostring metamethods are automatically called if they are available.
  • The sequence part of a table is always rendered as a sequence. If there are also key-value pairs, they will be rendered after the sequence part.

Here is an example that shows off all the bells and whistles:

local myTable = {
	hello = "repr",
	usefulness = 100,
	isEasyToUse = true,
	sequence = {"a", "sequence", "table"},
	mixed = {"a", "sequence", with = "key-value pairs"},
	subTables = {
		moreInfo = "Calls itself recursively on sub-tables"
	},
	usesToString = setmetatable({}, {__tostring = function () return "__tostring functions are called automatically" end}),
	["$YMBOL$"] = "Keys that aren't Lua identifiers are quoted";
	[{also = "Tables as keys work too";}] = "in case you need that",
	cyclic = {note = "cyclical tables are printed as just {CYCLIC}"}
}
myTable.cyclic.cyclic = myTable.cyclic  -- Create a cycle
 
local options = {
	pretty = true,      -- print with \n and indentation?
	semicolons = false, -- when printing tables, use semicolons (;) instead of commas (,)?
	sortKeys = true,    -- when printing dictionary tables, sort keys alphabetically?
	spaces = 3,         -- when pretty printing, use how many spaces to indent?
	tabs = false,       -- when pretty printing, use tabs instead of spaces?
	depth = 0,          -- when pretty pretty printing, what level to start indenting at?
}
mw.log(mRepr.repr(myTable, options))

This logs the following:

{
   ["$YMBOL$"] = "Keys that aren't Lua identifiers are quoted",
   [{
      also = "Tables as keys work too"
   }] = "in case you need that",
   cyclic = {
      cyclic = {CYCLIC},
      note = "cyclical tables are printed as just {CYCLIC}"
   },
   hello = "repr",
   isEasyToUse = true,
   mixed = {
      "a",
      "sequence",
      with = "key-value pairs"
   },
   sequence = {
      "a",
      "sequence",
      "table"
   },
   subTables = {
      moreInfo = "Calls itself recursively on sub-tables"
   },
   usefulness = 100,
   usesToString = __tostring functions are called automatically
}

invocationRepr

This function generates a string representation of a function invocation.

Basic syntax:

mRepr.invocationRepr{funcName = functionName, args = functionArgs}

Full syntax:

mRepr.invocationRepr{funcName = functionName, args = functionArgs, options = options}

Parameters:

  • funcName: The function name. This parameter is required, and must be a string.
  • args: The function arguments. This should be sequence table. The sequence items can be any Lua value, and will each be rendered using the [[#repr|]] function. This argument is optional.
  • options: A table of options. The options are the same as for the repr function. This argument is optional.

Examples:

mRepr.invocationRepr{funcName = "myFunc", args = {"test", 4, true, {"a", "b", "c"}}}

Result: myFunc("test", 4, true, {"a", "b", "c"})


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