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"})
--- repr - Version 1.2
-- Ozzypig - ozzypig.com - http://twitter.com/Ozzypig
-- Check out this thread for more info:
-- https://devforum.roblox.com/t/repr-function-for-printing-tables/276575
--[[
local repr = require(3148021300)
local myTable = {
hello = "world",
score = 5,
isCool = true
}
print(repr(myTable)) --> {hello = "world", isCool = true, score = 5}
]]
local defaultSettings = {
pretty = false;
robloxFullName = false;
robloxProperFullName = true;
robloxClassName = true;
tabs = false;
semicolons = false;
spaces = 3;
sortKeys = true;
}
-- lua keywords
local keywords = {["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}
local function isLuaIdentifier(str)
if type(str) ~= "string" then return false end
-- must be nonempty
if str:len() == 0 then return false end
-- can only contain a-z, A-Z, 0-9 and underscore
if str:find("[^%d%a_]") then return false end
-- cannot begin with digit
if tonumber(str:sub(1, 1)) then return false end
-- cannot be keyword
if keywords[str] then return false end
return true
end
-- works like Instance:GetFullName(), but invalid Lua identifiers are fixed (e.g. workspace["The Dude"].Humanoid)
local function properFullName(object, usePeriod)
if object == nil or object == game then return "" end
local s = object.Name
local usePeriod = true
if not isLuaIdentifier(s) then
s = ("[%q]"):format(s)
usePeriod = false
end
if not object.Parent or object.Parent == game then
return s
else
return properFullName(object.Parent) .. (usePeriod and "." or "") .. s
end
end
local depth = 0
local shown
local INDENT
local reprSettings
local function repr(value, reprSettings)
reprSettings = reprSettings or defaultSettings
INDENT = (" "):rep(reprSettings.spaces or defaultSettings.spaces)
if reprSettings.tabs then
INDENT = "\t"
end
local v = value --args[1]
local tabs = INDENT:rep(depth)
if depth == 0 then
shown = {}
end
if type(v) == "string" then
return ("%q"):format(v)
elseif type(v) == "number" then
if v == math.huge then return "math.huge" end
if v == -math.huge then return "-math.huge" end
return tonumber(v)
elseif type(v) == "boolean" then
return tostring(v)
elseif type(v) == "nil" then
return "nil"
elseif type(v) == "table" and type(v.__tostring) == "function" then
return tostring(v.__tostring(v))
elseif type(v) == "table" and getmetatable(v) and type(getmetatable(v).__tostring) == "function" then
return tostring(getmetatable(v).__tostring(v))
elseif type(v) == "table" then
if shown[v] then return "{CYCLIC}" end
shown[v] = true
local str = "{" .. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or "")
local isArray = true
for k, v in pairs(v) do
if type(k) ~= "number" then
isArray = false
break
end
end
if isArray then
for i = 1, #v do
if i ~= 1 then
str = str .. (reprSettings.semicolons and ";" or ",") .. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or " ")
end
depth = depth + 1
str = str .. repr(v[i], reprSettings)
depth = depth - 1
end
else
local keyOrder = {}
local keyValueStrings = {}
for k, v in pairs(v) do
depth = depth + 1
local kStr = isLuaIdentifier(k) and k or ("[" .. repr(k, reprSettings) .. "]")
local vStr = repr(v, reprSettings)
--[[str = str .. ("%s = %s"):format(
isLuaIdentifier(k) and k or ("[" .. repr(k, reprSettings) .. "]"),
repr(v, reprSettings)
)]]
table.insert(keyOrder, kStr)
keyValueStrings[kStr] = vStr
depth = depth - 1
end
if reprSettings.sortKeys then table.sort(keyOrder) end
local first = true
for _, kStr in pairs(keyOrder) do
if not first then
str = str .. (reprSettings.semicolons and ";" or ",") .. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or " ")
end
str = str .. ("%s = %s"):format(kStr, keyValueStrings[kStr])
first = false
end
end
shown[v] = false
if reprSettings.pretty then
str = str .. "\n" .. tabs
end
str = str .. "}"
return str
elseif typeof then
-- Check Roblox types
if typeof(v) == "Instance" then
return (reprSettings.robloxFullName
and (reprSettings.robloxProperFullName and properFullName(v) or v:GetFullName())
or v.Name) .. (reprSettings.robloxClassName and ((" (%s)"):format(v.ClassName)) or "")
elseif typeof(v) == "Axes" then
local s = {}
if v.X then table.insert(s, repr(Enum.Axis.X, reprSettings)) end
if v.Y then table.insert(s, repr(Enum.Axis.Y, reprSettings)) end
if v.Z then table.insert(s, repr(Enum.Axis.Z, reprSettings)) end
return ("Axes.new(%s)"):format(table.concat(s, ", "))
elseif typeof(v) == "BrickColor" then
return ("BrickColor.new(%q)"):format(v.Name)
elseif typeof(v) == "CFrame" then
return ("CFrame.new(%s)"):format(table.concat({v:GetComponents()}, ", "))
elseif typeof(v) == "Color3" then
return ("Color3.new(%d, %d, %d)"):format(v.r, v.g, v.b)
elseif typeof(v) == "ColorSequence" then
if #v.Keypoints > 2 then
return ("ColorSequence.new(%s)"):format(repr(v.Keypoints, reprSettings))
else
if v.Keypoints[1].Value == v.Keypoints[2].Value then
return ("ColorSequence.new(%s)"):format(repr(v.Keypoints[1].Value, reprSettings))
else
return ("ColorSequence.new(%s, %s)"):format(
repr(v.Keypoints[1].Value, reprSettings),
repr(v.Keypoints[2].Value, reprSettings)
)
end
end
elseif typeof(v) == "ColorSequenceKeypoint" then
return ("ColorSequenceKeypoint.new(%d, %s)"):format(v.Time, repr(v.Value, reprSettings))
elseif typeof(v) == "DockWidgetPluginGuiInfo" then
return ("DockWidgetPluginGuiInfo.new(%s, %s, %s, %s, %s, %s, %s)"):format(
repr(v.InitialDockState, reprSettings),
repr(v.InitialEnabled, reprSettings),
repr(v.InitialEnabledShouldOverrideRestore, reprSettings),
repr(v.FloatingXSize, reprSettings),
repr(v.FloatingYSize, reprSettings),
repr(v.MinWidth, reprSettings),
repr(v.MinHeight, reprSettings)
)
elseif typeof(v) == "Enums" then
return "Enums"
elseif typeof(v) == "Enum" then
return ("Enum.%s"):format(tostring(v))
elseif typeof(v) == "EnumItem" then
return ("Enum.%s.%s"):format(tostring(v.EnumType), v.Name)
elseif typeof(v) == "Faces" then
local s = {}
for _, enumItem in pairs(Enum.NormalId:GetEnumItems()) do
if v[enumItem.Name] then
table.insert(s, repr(enumItem, reprSettings))
end
end
return ("Faces.new(%s)"):format(table.concat(s, ", "))
elseif typeof(v) == "NumberRange" then
if v.Min == v.Max then
return ("NumberRange.new(%d)"):format(v.Min)
else
return ("NumberRange.new(%d, %d)"):format(v.Min, v.Max)
end
elseif typeof(v) == "NumberSequence" then
if #v.Keypoints > 2 then
return ("NumberSequence.new(%s)"):format(repr(v.Keypoints, reprSettings))
else
if v.Keypoints[1].Value == v.Keypoints[2].Value then
return ("NumberSequence.new(%d)"):format(v.Keypoints[1].Value)
else
return ("NumberSequence.new(%d, %d)"):format(v.Keypoints[1].Value, v.Keypoints[2].Value)
end
end
elseif typeof(v) == "NumberSequenceKeypoint" then
if v.Envelope ~= 0 then
return ("NumberSequenceKeypoint.new(%d, %d, %d)"):format(v.Time, v.Value, v.Envelope)
else
return ("NumberSequenceKeypoint.new(%d, %d)"):format(v.Time, v.Value)
end
elseif typeof(v) == "PathWaypoint" then
return ("PathWaypoint.new(%s, %s)"):format(
repr(v.Position, reprSettings),
repr(v.Action, reprSettings)
)
elseif typeof(v) == "PhysicalProperties" then
return ("PhysicalProperties.new(%d, %d, %d, %d, %d)"):format(
v.Density, v.Friction, v.Elasticity, v.FrictionWeight, v.ElasticityWeight
)
elseif typeof(v) == "Random" then
return "<Random>"
elseif typeof(v) == "Ray" then
return ("Ray.new(%s, %s)"):format(
repr(v.Origin, reprSettings),
repr(v.Direction, reprSettings)
)
elseif typeof(v) == "RBXScriptConnection" then
return "<RBXScriptConnection>"
elseif typeof(v) == "RBXScriptSignal" then
return "<RBXScriptSignal>"
elseif typeof(v) == "Rect" then
return ("Rect.new(%d, %d, %d, %d)"):format(
v.Min.X, v.Min.Y, v.Max.X, v.Max.Y
)
elseif typeof(v) == "Region3" then
local min = v.CFrame.p + v.Size * -.5
local max = v.CFrame.p + v.Size * .5
return ("Region3.new(%s, %s)"):format(
repr(min, reprSettings),
repr(max, reprSettings)
)
elseif typeof(v) == "Region3int16" then
return ("Region3int16.new(%s, %s)"):format(
repr(v.Min, reprSettings),
repr(v.Max, reprSettings)
)
elseif typeof(v) == "TweenInfo" then
return ("TweenInfo.new(%d, %s, %s, %d, %s, %d)"):format(
v.Time, repr(v.EasingStyle, reprSettings), repr(v.EasingDirection, reprSettings),
v.RepeatCount, repr(v.Reverses, reprSettings), v.DelayTime
)
elseif typeof(v) == "UDim" then
return ("UDim.new(%d, %d)"):format(
v.Scale, v.Offset
)
elseif typeof(v) == "UDim2" then
return ("UDim2.new(%d, %d, %d, %d)"):format(
v.X.Scale, v.X.Offset, v.Y.Scale, v.Y.Offset
)
elseif typeof(v) == "Vector2" then
return ("Vector2.new(%d, %d)"):format(v.X, v.Y)
elseif typeof(v) == "Vector2int16" then
return ("Vector2int16.new(%d, %d)"):format(v.X, v.Y)
elseif typeof(v) == "Vector3" then
return ("Vector3.new(%d, %d, %d)"):format(v.X, v.Y, v.Z)
elseif typeof(v) == "Vector3int16" then
return ("Vector3int16.new(%d, %d, %d)"):format(v.X, v.Y, v.Z)
elseif typeof(v) == "DateTime" then
return ("DateTime.fromIsoDate(%q)"):format(v:ToIsoDate())
else
return "<Roblox:" .. typeof(v) .. ">"
end
else
return "<" .. type(v) .. ">"
end
end
return repr