Hi, idk if this fits here because it is my first time posting a resource,
But I think it fits everything that was mentioned in the About community resources post.
Anyway, this is a module that can parse xml (if written correctly, so if you type syntax errors then it won’t load lol) and instantiate the given types declared in your source.
It exposes the bloxmlTokenizer, bloxmlParser, bloxmlLoader & bloxmlStarter modules that help in loading the xml.
the bloxmlTokenizer creates tokens from source, examples of tokens are: <, >, / , = , “something in quotes” and identifiers.
then the bloxmlParser takes the tokens and returns an array of xml_containers that represent the relationships between each element.
this is then used to create the UI using the bloxmlLoader,
the bloxmlStarter exposes one function that takes a string and returns an array of UI elements, so instead of calling each class’s fn manually you call starter’s load function and under the hood it calls all class’s corresponding fn, disposes of resources and returns the result.
there are also helper modules, one for type conversion, so if you have a property of an unsupported type you can add a convert method in the module, the name needs to be ‘ToType’ where Type is your typename.
There is also string helper methods for the bloxmlTokenizer to use during lexical analysis, methods like isdigit() (unused, was used before but deleted because I made all values a string), isalphanum(), isalpha() (used in alphanum), and isspace().
you might want to run this loading operation on a separate thread because it creates a token instance for each valid character sequence (which in worst case scenario could be for every character), which means a lot of instances.
you might notice that the load fn from bloxmlStarter also accepts a dictionary called ‘listeners’, well this is were you define event handlers and then reference them in the xml by name. The corresponding fn will be called when your event is fired.
so, different types have different formatting, here are the formatting of the currently supported types:
-
Vector3 - (used before, not anymore) “x, y, z” no parens no special character, each component separated by a comma. like in the example.
-
UDim2 - same as vector3 but with four number values instead: “xscale, xoffset, yscale, yoffset”
-
UDim - same as UDim2 but with two numbers instead: “scale, offset”
-
color3 - same as vector3 but now the values represent rgb instead of xyz: “r, g b” each component is a number between 0 and 255.
-
boolean - either True/true or False/false.
-
Enum values - the name of the Enum item, the enum type will be aquired from the context, so say if I want to modify ApplyStrokeMode from UIStroke I’d do:
<UIStroke ApplyStrokeMode = "Border"/>
as you can see, only border is allowed, no dots no enum type name. (although that would make parsing easier ig) -
number - any number 0 - 9 and a dot are allowed.
-
event handlers - to connect a handler to an event you type the handler’s name, that corresponds to the name assigned to it in the given listeners dictionary.
so to create a UI element or modifier, all you need to do is follow this format:
<!--do this if your element does not have children-->
<typename Property = "value" ... />
<!--and this if it has children-->
<typename Property= "value">
<childtype Property = "value" />
</typename>
children are allowed to have children themselves.
so if you write /> then the element will have no children, that is a shorthand for writing an open tag and then immediately afterwards a closing tag.
if you have elements in between an element’s open & closing tag they will be parented to that element.
commenting is supported, so you can type <!-- -->
and anything in between will be ignored
one more warning, since in roblox, you can’t really check the type of a property itself, only values, when we do Instance.new() on the given type and a property you edited in xml 's default value is nil, it will error so it is recommended to edit those manually after the element has been loaded, until roblox adds reflection support like .GetType() and .Properties like in .NET we’re stuck with this problem.
If roblox adds that support though I’ll update the converter to use the new api instead, which would make my life easier.
here is an example of a toggleswitch UI component created using Luau & XML (I call it bloxml, xml for roblox)
local bloxmlStarter = require(game.ReplicatedStorage:WaitForChild("BloxMl"):WaitForChild("BloXMLStarter"))
local ts = game:GetService("TweenService")
local info = TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
local linfo = TweenInfo.new(0.5, Enum.EasingStyle.Linear)
local toggled = false
local backend = {
["Toggle_Activated"] = function(sender: TextButton, obj : InputObject)
local goal = toggled and {Position = UDim2.fromScale(0, 0)} or {Position = UDim2.fromScale(0.583, 0)}
local bgg = toggled and {BackgroundColor3 = Color3.new(1, 1, 1)} or {BackgroundColor3 = Color3.fromRGB(0, 170, 255)}
local st = ts:Create(sender, info, goal)
local bgt = ts:Create(sender.Parent, linfo, bgg)
st:Play()
bgt:Play()
st.Completed:Wait()
toggled = not toggled
end,
}
local source = [[
<!--this is a comment, and should be skipped!-->
<Frame Name = "ToggleSwitch" Size = "0, 96, 0, 73" Position = "0.5, 0, 0.5, 0" BackgroundColor3 = "255, 255, 255">
<UIAspectRatioConstraint AspectRatio = "2.3"/>
<UICorner CornerRadius = "0.5, 0"/>
<UIStroke ApplyStrokeMode = "Border" Color = "173, 173, 173"/>
<TextButton Name = "Toggle" Activated = "Toggle_Activated" BackgroundColor3 = "255, 255, 255" Size = "0.5, 0, 0.95, 0" Text = " ">
<UIAspectRatioConstraint />
<UICorner CornerRadius = "1, 0"/>
<UIStroke ApplyStrokeMode = "Border" Color = "230, 230, 230"/>
</TextButton>
</Frame>
]]
local objects = bloxmlStarter.load(source, backend)
objects[1].Parent = script.Parent
If there are any bugs / feature requests please tell me, so I can update the XML parser/UI loader depending on if it is a syntatical feature request or a UI different one.
Idk why you’d use this, but It makes creating UI on roblox easier, I got inspired by Microsoft’s XAML where the frontend is in xaml and the backend is in C# unlike winforms/ roblox where everything is one language and there is no separation of concerns.
Hopefully you people find this useful!
Thanks for reading!
modules:
BloxmlTokenizer:
local stringExtensions = require (script.Parent:WaitForChild("StringExtensions"))
local bloxTokenizer = {}
bloxTokenizer.__index = bloxTokenizer
--[[
TYPES:
ATTRIBUTE_VALUE: 0
IDENTIFIER: 1
OPEN_TAG: 3
CLOSE_TAG: 4
FORWARD_SLASH: 5
EQUAL: 6
]]
export type BloxmlToken = {
CharacterSequence: string,
TokenType: number,
}
export type BloxmlTokenizer = typeof(setmetatable({} :: {
sourceCode: {string},
source: string,
position: number,
result: {BloxmlToken}
}, bloxTokenizer))
local function make_tkn(char: string, t: number): BloxmlToken
return {
CharacterSequence = char,
TokenType = t,
}
end
local function tokenize_attr(tokenizer_state: BloxmlTokenizer)
tokenizer_state.position += 1--skip the "
local start = tokenizer_state.position
while tokenizer_state.position < #tokenizer_state.sourceCode and tokenizer_state.sourceCode[tokenizer_state.position] ~= '"' do
tokenizer_state.position += 1
end
local attr = string.sub(tokenizer_state.source, start, tokenizer_state.position - 1)--exclude the end "
tokenizer_state.position += 1--discard end "
table.insert(tokenizer_state.result, make_tkn(attr, 0))--insert new token
end
local function tokenize_identifier(tokenize_state: BloxmlTokenizer)
local start = tokenize_state.position
while tokenize_state.position < #tokenize_state.sourceCode and (stringExtensions.isalphanum(tokenize_state.sourceCode[tokenize_state.position]) or tokenize_state.sourceCode[tokenize_state.position] == '_') do
tokenize_state.position += 1
end
local identifier = string.sub(tokenize_state.source, start, tokenize_state.position - 1)--skip whatever character ended the loop.
table.insert(tokenize_state.result, make_tkn(identifier, 1))
end
local function discard_comment(tokenize_state: BloxmlTokenizer)
local start = tokenize_state.position
while tokenize_state.position < #tokenize_state.sourceCode and tokenize_state.sourceCode[tokenize_state.position] ~= '>' do
tokenize_state.position += 1
end
--commenting is <! > now technically, but you can stil do <!-- -->, I am not going to enforce it.
tokenize_state.position += 1 --skip the > sign.
end
function bloxTokenizer.new(source: string): BloxmlTokenizer
assert(source and source ~= "", "Failed to create tokenizer, source was nil or empty.")
return setmetatable({
sourceCode = string.split(source, ""),
source = source,
result = {},
position = 0,
}, bloxTokenizer)
end
function bloxTokenizer:Tokenize()
--reset the values
self.result = {}
self.position = 1
while self.position < #self.sourceCode do
--discard spaces
--print(self.position, self.sourceCode[self.position])
if stringExtensions.isspace(self.sourceCode[self.position]) then
self.position += 1
continue
end
--multi character tokens (have own tokenize method)
if self.sourceCode[self.position] == '"' then
tokenize_attr(self)
elseif stringExtensions.isalphanum(self.sourceCode[self.position]) or self.sourceCode[self.position] == '_' then
tokenize_identifier(self)
--single character tokens (all have same logic)
elseif self.sourceCode[self.position] == '<' then
if self.sourceCode[self.position + 1] == '!' then
discard_comment(self)
continue
end
table.insert(self.result, make_tkn(nil, 3))
self.position += 1
elseif self.sourceCode[self.position] == '>' then
table.insert(self.result, make_tkn(nil, 4))
self.position += 1
elseif self.sourceCode[self.position] == '/' then
table.insert(self.result, make_tkn(nil, 5))
self.position += 1
elseif self.sourceCode[self.position] == '=' then
table.insert(self.result, make_tkn(nil, 6))
self.position += 1
else
warn("Unknown token detected, token is discarded and tokenization continues: ", self.sourceCode[self.position])
self.position += 1
end
end
print("Tokenization Completed! Tokens available in BloxmlTokenizer.result!")
end
function bloxTokenizer:Dispose()
table.clear(self.result)
table.clear(self.sourceCode)
table.clear(self)
end
return table.freeze(bloxTokenizer)
bloxmlParser:
local BloxmlTokenizer = require (script.Parent:WaitForChild("BloXMLTokenizer"))
local bloxmlparser = {}
bloxmlparser.__index = bloxmlparser
export type xml_Container = {
typename: string,
properties: {[string]: string},
children: {xml_Container},
}
export type BloxmlParser = typeof(setmetatable({} :: {
sourceTokens: {BloxmlToken},
result: {xml_Container},
position: number,
}, bloxmlparser))
local function parse_tag(bloxmlparser: BloxmlParser): xml_Container
assert(bloxmlparser.sourceTokens[bloxmlparser.position].TokenType == 3, "A tag must start with a '<'!")
bloxmlparser.position += 1 --skip the '<'
assert(bloxmlparser.sourceTokens[bloxmlparser.position].TokenType == 1, "Expected identifier for type name!")
local typename = bloxmlparser.sourceTokens[bloxmlparser.position].CharacterSequence
bloxmlparser.position += 1 --type name already handled, skip to next token
local kvps = {}
while bloxmlparser.sourceTokens[bloxmlparser.position].TokenType == 1 do
local identifier = bloxmlparser.sourceTokens[bloxmlparser.position].CharacterSequence
bloxmlparser.position += 1--skip identifier, already handled
assert(bloxmlparser.sourceTokens[bloxmlparser.position].TokenType == 6, "Expected '=' after attribute name!")
bloxmlparser.position += 1--skip equal sign
assert(bloxmlparser.sourceTokens[bloxmlparser.position].TokenType == 0, "Expected attribute value after '='!")
local attr_val = bloxmlparser.sourceTokens[bloxmlparser.position].CharacterSequence
bloxmlparser.position += 1--go to next identifier
kvps[identifier] = attr_val--save value as kvp
end
--broken out of loop, current sign is either '/' or '>'.
if bloxmlparser.sourceTokens[bloxmlparser.position].TokenType == 5 then
bloxmlparser.position += 2 --skip / and >
return {typename = typename, properties = kvps, children = {}} --forwardslash -> no children, cuz empty tag.
end
--container tag, there are children, skip closing brace and recursively parse children.
bloxmlparser.position += 1
local children = {}
while bloxmlparser.sourceTokens[bloxmlparser.position].TokenType == 3 and bloxmlparser.sourceTokens[bloxmlparser.position + 1].TokenType ~= 5 do
table.insert(children, parse_tag(bloxmlparser))
end
bloxmlparser.position += 4 --one for the <, one for the /, one for the typename, & one for the >.
return {typename = typename, properties = kvps, children = children}
end
function bloxmlparser.new(tokens: {BloxmlToken}): BloxmlParser
return setmetatable({
result = {},
sourceTokens = tokens,
position = 1,
}, bloxmlparser)
end
function bloxmlparser:Parse()
self.position = 1
self.result = {}
while self.position < #self.sourceTokens do
table.insert(self.result, parse_tag(self))
end
print("Parsing complete! result is available in BloXMLParser.result!")
end
function bloxmlparser:Dispose()
table.clear(self.result)
table.clear(self.sourceTokens)
table.clear(self)
end
return table.freeze(bloxmlparser)
bloxmlloader:
local bloxmlParser = require (script.Parent:WaitForChild("BloXMLParser"))
local StringConverter = require (script.Parent:WaitForChild("StringConverter"))
local bloxmlloader = {}
bloxmlloader.__index = bloxmlloader
export type BloxmlLoader = typeof(setmetatable({} :: {
containers: {xml_Container},
backend: {[string]: (...any) -> ()}?
}, bloxmlloader))
local function load_container(container: xml_Container, listeners: {[string]: (...any) -> ()}): GuiObject
local success, err:Instance = pcall(Instance.new, container.typename)
assert(success, "Failed to load bloXML, type not found: "..tostring(err))
assert(err:IsA("GuiObject") or err:IsA("UIBase"), "Failed to load bloXML, cannot create instances which are not Gui Elements or Modifiers.")
for key, value in pairs(container.properties) do
local typen = typeof(err[key])
if typen == "RBXScriptSignal" then
assert(listeners[value], "Event listener not found!")
local targetfn = listeners[value]
err[key]:Connect(function(...) targetfn(err, ...) end)
continue
elseif typen == "string" then
err[key] = value
continue
elseif typen == "EnumItem" then
err[key] = StringConverter.ToEnum(tostring(err[key]), value)
continue
end
local converterFn = StringConverter["To"..typen]
assert(converterFn, "Converter not found for type: "..typen)
local iresult = converterFn(value)
assert(iresult, "Failed to convert string rep, result was nil")
err[key] = iresult
end
for _, child in ipairs(container.children) do
local ui = load_container(child, listeners)
ui.Parent = err
end
return err
end
function bloxmlloader.new(parsed_containers: {xml_Container}, listeners: {[string]: (...any) -> ()}?): BloxmlLoader
return setmetatable({
containers = parsed_containers,
backend = listeners or {},
}, bloxmlloader)
end
function bloxmlloader:GetResult(): {GuiObject}
local result = {}
for _, container in ipairs(self.containers) do
table.insert(result, load_container(container, self.backend))
end
print("Component loaded successfully!")
return result
end
function bloxmlloader:Dispose()
table.clear(self.containers)
table.clear(self.backend)
table.clear(self)
end
return table.freeze(bloxmlloader)
stringextensions:
local module = {}
function module.isdigit(char: string): boolean
local byte = char:byte()
return byte >= 48 and byte <= 57
end
function module.isspace(char: string): boolean
local byte = char:byte()
return byte == 32 or (byte >= 9 and byte <= 13)
end
function module.isalpha(char: string):boolean
local byte = char:byte()
return (byte >= 65 and byte <= 90) or (byte >= 97 and byte <= 122)
end
function module.isalphanum(char: string)
return module.isalpha(char) or module.isdigit(char)
end
return table.freeze(module)
stringconverter:
local converter = {}
function converter.ToVector3(t: string): Vector3?
local nums = string.split(t, ",")
if #nums ~= 3 then return nil end
local x, y , z = tonumber(nums[1]), tonumber(nums[2]), tonumber(nums[3])
return Vector3.new(x, y, z)
end
function converter.ToEnum(typename: string, value: string): Enum?
local enumName = string.split(typename, ".")[2] -- eg: Enum.PartType.Block where [2] = PartType.
if not Enum[enumName] then return nil end
return Enum[enumName][value]
end
function converter.ToColor3(t: string): Color3?
local nums = string.split(t, ",")
if #nums ~= 3 then return nil end
local r, g , b = tonumber(nums[1]), tonumber(nums[2]), tonumber(nums[3])
return Color3.fromRGB(r, g, b)
end
function converter.Toboolean(t: string): boolean
return string.lower(t) == "true"
end
function converter.ToUDim2(t: string): UDim2?
local nums = string.split(t, ",")
if #nums ~= 4 then return nil end
local xs, xo, ys, yo = tonumber(nums[1]), tonumber(nums[2]), tonumber(nums[3]), tonumber(nums[4])
return UDim2.new(xs, xo, ys, yo)
end
function converter.ToUDim(t: string): UDim?
local nums = string.split(t, ",")
if #nums ~= 2 then return nil end
local s, o = tonumber(nums[1]), tonumber(nums[2])
return UDim.new(s, o)
end
--add to number for number functionality, required by loading algo
converter.Tonumber = tonumber
return table.freeze(converter)
bloxml starter:
local bloxmlTokenizer = require (script.Parent:WaitForChild("BloXMLTokenizer"))
local bloxmlParser = require (script.Parent:WaitForChild("BloXMLParser"))
local bloxmlLoader = require (script.Parent:WaitForChild("BloXMLLoader"))
local bloxmlstarter = {}
function bloxmlstarter.load(source: string, backend: {[string]: (...any) -> ()}?): {GuiObject}
local tkn = bloxmlTokenizer.new(source)
tkn:Tokenize()
local prsr = bloxmlParser.new(tkn.result)
prsr:Parse()
tkn:Dispose()
local objectLoader = bloxmlLoader.new(prsr.result, backend)
local result = objectLoader:GetResult()
prsr:Dispose()
objectLoader:Dispose()
print("Element with given BloXML description loaded successfully!")
return result
end
return bloxmlstarter
Lastly, here is a link to a github repository of mine were I’ll post new versions and we can collaborate there!
Brushymilbil/XML-to-Roblox-UI-converter (github.com)
Thanks for reading!