Module:Repr
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 tonil
.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 ifpretty
is true.spaces
: The number of spaces to indent with, iftabs
is false. The default is 4. This only has an effect ifpretty
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 ifpretty
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('Module:No globals')
local defaultOptions = {
pretty = false;
tabs = true;
semicolons = false;
spaces = 4;
sortKeys = true;
}
-- 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 be nonempty
and str:len() > 0
-- can only contain a-z, A-Z, 0-9 and underscore
and not str:find("[^%d%a_]")
-- cannot begin with digit
and not tonumber(str:sub(1, 1))
-- cannot be 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
--[[
-- Whether the given value is a positive integer.
--]]
local function isPositiveInteger(value)
return type(value) == "number"
and value >= 1
and value < math.huge
and value == math.floor(value)
end
--[[
-- Whether the given table is a sequence table.
--]]
local function isSequence(t)
for k in pairs(t) do
if not isPositiveInteger(k) then
return false
end
end
return true
end
--[[
-- Render a table with pre-rendered item strings.
-- Other functions take care of properly formatting the table items, and this
-- function takes care of formatting the opening and closing braces, and the
-- whitespace between the items.
--]]
local function renderTableItemStrings(itemStrings, context, depth)
local valueSeparator = context.separator .. context.makeTableItemWhitespace(depth)
local ret = {"{", context.makeTableStartWhitespace(depth)}
local first = itemStrings[1]
if first ~= nil then
table.insert(ret, first)
end
for i = 2, #itemStrings do
table.insert(ret, valueSeparator)
table.insert(ret, itemStrings[i])
end
table.insert(ret, context.makeTableEndWhitespace(depth))
table.insert(ret, "}")
return table.concat(ret)
end
--[[
-- Render a sequence table.
--]]
local function renderSequence(t, context, depth)
local itemStrings = {}
for i = 1, #t do
table.insert(itemStrings, reprRecursive(t[i], context, depth + 1))
end
return renderTableItemStrings(itemStrings, context, depth)
end
--[[
-- Render a table of key-value pairs.
--]]
local function renderKeyValueTable(t, context, depth)
local keyOrder = {}
local keyValueStrings = {}
for k, v in pairs(t) do
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
if context.sortKeys then
table.sort(keyOrder)
end
local itemStrings = {}
for _, kStr in ipairs(keyOrder) do
table.insert(itemStrings, string.format("%s = %s", kStr, keyValueStrings[kStr]))
end
return renderTableItemStrings(itemStrings, context, depth)
end
--[[
-- Render the given table.
-- The table can be a sequence table or a table of key-value pairs. If the table
-- has a __tostring metamethod that is used to render the table, and there is
-- detection for cyclic tables.
--]]
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
if isSequence(t) then
result = renderSequence(t, context, depth)
else
result = renderKeyValueTable(t, context, depth)
end
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
local function returnEmptyString()
return ""
end
local function returnSpace()
return " "
end
--[[
-- Render a string representation of the given value.
--]]
local function repr(value, options)
options = options or {}
local function getOption(option)
local value = options[option]
if value ~= nil then
return value
else
return defaultOptions[option]
end
end
local context = {}
local indent
if getOption("tabs") then
indent = "\t"
else
indent = (" "):rep(getOption("spaces"))
end
local function makeWhitespaceForCurrentDepth(depth)
return "\n" .. indent:rep(depth)
end
local function makeWhitespaceForNextDepth(depth)
return "\n" .. indent:rep(depth + 1)
end
if getOption("pretty") then
context.makeTableStartWhitespace = makeWhitespaceForNextDepth
context.makeTableItemWhitespace = makeWhitespaceForNextDepth
context.makeTableEndWhitespace = makeWhitespaceForCurrentDepth
else
context.makeTableStartWhitespace = returnEmptyString
context.makeTableItemWhitespace = returnSpace
context.makeTableEndWhitespace = returnEmptyString
end
if getOption("semicolons") then
context.separator = ";"
else
context.separator = ","
end
context.sortKeys = getOption("sortKeys")
context.shown = {}
local depth = 0
return reprRecursive(value, context, depth)
end
--[[
-- Render a string representation of the given function invocation.
--]]
local function invocationRepr(keywordArgs)
local renderedArgs = {}
for _, arg in ipairs(keywordArgs.args) do
table.insert(renderedArgs, repr(arg, keywordArgs.options))
end
return string.format("%s(%s)", keywordArgs.funcName, table.concat(renderedArgs, ", "))
end
return {
isLuaIdentifier = isLuaIdentifier,
repr = repr,
invocationRepr = invocationRepr,
}