--[[ This library describes a formatting mechanism akin to Rust's std::fmt. It has a couple building blocks: * A new syntax for formatting strings, taken verbatim from Rust. It'd also be possible to use printf-style formatting specifiers to integrate with the existing string.format utility. * An equivalent to Rust's `Display` trait. We're mapping the semantics of tostring and the __tostring metamethod onto this trait. A lot of types should already have __tostring implementations, too! * An equivalent to Rust's `Debug` trait. This library Lua-ifies that idea by inventing a new metamethod, `__fmtDebug`. We pass along the "extended form" attribute which is the equivalent of the "alternate mode" in Rust's Debug trait since it's the author's opinion that treating it as a verbosity flag is semantically accurate. ]] --[[ The default implementation of __fmtDebug for tables when the extended option is not set. ]] local function defaultTableDebug(buffer, input) buffer:writeRaw("{") for key, value in pairs(input) do buffer:write("[{:?}] = {:?}", key, value) if next(input, key) ~= nil then buffer:writeRaw(", ") end end buffer:writeRaw("}") end --[[ The default implementation of __fmtDebug for tables with the extended option set. ]] local function defaultTableDebugExtended(buffer, input) -- Special case for empty tables. if next(input) == nil then buffer:writeRaw("{}") return end buffer:writeLineRaw("{") buffer:indent() for key, value in pairs(input) do buffer:writeLine("[{:?}] = {:#?},", key, value) end buffer:unindent() buffer:writeRaw("}") end --[[ The default debug representation for all types. ]] local function debugImpl(buffer, value, extendedForm) local valueType = typeof(value) if valueType == "string" then local formatted = string.format("%q", value) buffer:writeRaw(formatted) elseif valueType == "table" then local valueMeta = getmetatable(value) if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then -- This type implement's the metamethod we made up to line up with -- Rust's 'Debug' trait. valueMeta.__fmtDebug(value, buffer, extendedForm) else if extendedForm then defaultTableDebugExtended(buffer, value) else defaultTableDebug(buffer, value) end end elseif valueType == "Instance" then buffer:writeRaw(value:GetFullName()) else buffer:writeRaw(tostring(value)) end end --[[ Defines and implements the library's template syntax. ]] local function writeFmt(buffer, template, ...) local currentArg = 0 local i = 1 local len = #template while i <= len do local openBrace = template:find("{", i) if openBrace == nil then -- There are no remaining open braces in this string, so we can -- write the rest of the string to the buffer. buffer:writeRaw(template:sub(i)) break else -- We found an open brace! This could be: -- - A literal '{', written as '{{' -- - The beginning of an interpolation, like '{}' -- - An error, if there's no matching '}' local charAfterBrace = template:sub(openBrace + 1, openBrace + 1) if charAfterBrace == "{" then -- This is a literal brace, so we'll write everything up to this -- point (including the first brace), and then skip over the -- second brace. buffer:writeRaw(template:sub(i, openBrace)) i = openBrace + 2 else -- This SHOULD be an interpolation. We'll find our matching -- brace and treat the contents as the formatting specifier. -- If there were any unwritten characters before this -- interpolation, write them to the buffer. if openBrace - i > 0 then buffer:writeRaw(template:sub(i, openBrace - 1)) end local closeBrace = template:find("}", openBrace + 1) assert(closeBrace ~= nil, "Unclosed formatting specifier. Use '{{' to write an open brace.") local formatSpecifier = template:sub(openBrace + 1, closeBrace - 1) currentArg = currentArg + 1 local arg = select(currentArg, ...) if formatSpecifier == "" then -- This should use the equivalent of Rust's 'Display', ie -- tostring and the __tostring metamethod. buffer:writeRaw(tostring(arg)) elseif formatSpecifier == ":?" then -- This should use the equivalent of Rust's 'Debug', -- invented for this library as __fmtDebug. debugImpl(buffer, arg, false) elseif formatSpecifier == ":#?" then -- This should use the equivlant of Rust's 'Debug' with the -- 'alternate' (ie expanded) flag set. debugImpl(buffer, arg, true) else error("unsupported format specifier " .. formatSpecifier, 2) end i = closeBrace + 1 end end end end local function debugOutputBuffer() local buffer = {} local startOfLine = true local indentLevel = 0 local indentation = "" function buffer:writeLine(template, ...) writeFmt(self, template, ...) self:nextLine() end function buffer:writeLineRaw(value) self:writeRaw(value) self:nextLine() end function buffer:write(template, ...) return writeFmt(self, template, ...) end function buffer:writeRaw(value) if #value > 0 then if startOfLine and #indentation > 0 then startOfLine = false table.insert(self, indentation) end table.insert(self, value) startOfLine = false end end function buffer:nextLine() table.insert(self, "\n") startOfLine = true end function buffer:indent() indentLevel = indentLevel + 1 indentation = string.rep(" ", indentLevel) end function buffer:unindent() indentLevel = math.max(0, indentLevel - 1) indentation = string.rep(" ", indentLevel) end function buffer:finish() return table.concat(self, "") end return buffer end local function fmt(template, ...) local buffer = debugOutputBuffer() writeFmt(buffer, template, ...) return buffer:finish() end --[[ Wrap the given object in a type that implements the given function as its Debug implementation, and forwards __tostring to the type's underlying tostring implementation. ]] local function debugify(object, fmtFunc) return setmetatable({}, { __fmtDebug = function(_, ...) return fmtFunc(object, ...) end, __tostring = function() return tostring(object) end, }) end return { debugOutputBuffer = debugOutputBuffer, fmt = fmt, debugify = debugify, }