Reworked XML #2

This commit is contained in:
Robert Jelic
2025-03-05 01:25:49 +01:00
parent 5e63533650
commit 78a42954c0
5 changed files with 237 additions and 494 deletions

View File

@@ -62,7 +62,7 @@ Container.defineProperty(Container, "offsetY", {default = 0, type = "number", ca
return value
end})
---@combinedProperty offset {offsetX offsetY} Combined property for offsetX and offsetY
---@combinedProperty offset {offsetX number, offsetY number} Combined property for offsetX and offsetY
Container.combineProperties(Container, "offset", "offsetX", "offsetY")
for k, _ in pairs(elementManager:getElementList()) do

View File

@@ -3,13 +3,12 @@ local errorManager = require("errorManager")
local propertySystem = require("propertySystem")
local expect = require("libraries/expect")
--- This is the UI Manager and the starting point for your project. The following functions allow you to influence the default behavior of Basalt.
---
--- Before you can access Basalt, you need to add the following code on top of your file:
--- @usage local basalt = require("basalt")
--- What this code does is it loads basalt into the project, and you can access it by using the variable defined as "basalt".
--- @class Basalt
--- @class basalt
--- @field traceback boolean Whether to show a traceback on errors
--- @field _events table A table of events and their callbacks
--- @field _schedule function[] A table of scheduled functions
@@ -92,7 +91,7 @@ end
--- Creates and returns a new BaseFrame
--- @shortDescription Creates a new BaseFrame
--- @return table BaseFrame The created frame instance
--- @return BaseFrame BaseFrame The created frame instance
--- @usage local mainFrame = basalt.createFrame()
function basalt.createFrame()
local frame = basalt.create("BaseFrame")
@@ -111,7 +110,7 @@ end
--- Gets or creates the main frame
--- @shortDescription Gets or creates the main frame
--- @return BaseFrame table The main frame instance
--- @return BaseFrame BaseFrame The main frame instance
--- @usage local frame = basalt.getMainFrame()
function basalt.getMainFrame()
if(mainFrame == nil)then

View File

@@ -196,8 +196,7 @@ end
---@splitClass
--- Container benchmarking methods
---@class Container
---@class Container : VisualElement
local Container = {}
--- Enables benchmarking for a container and all its children

View File

@@ -1,527 +1,235 @@
local errorManager = require("errorManager")
local log = require("log")
local XMLNode = {
new = function(tag)
return {
tag = tag,
value = nil,
attributes = {},
children = {},
local XMLParser = {}
local TokenType = {
TAG_OPEN = "TAG_OPEN",
TAG_CLOSE = "TAG_CLOSE",
TAG_SELF_CLOSE = "TAG_SELF_CLOSE",
ATTRIBUTE = "ATTRIBUTE",
TEXT = "TEXT",
CDATA = "CDATA",
COMMENT = "COMMENT"
addChild = function(self, child)
table.insert(self.children, child)
end,
addAttribute = function(self, tag, value)
self.attributes[tag] = value
end
}
end
}
local function tokenize(xml)
local tokens = {}
local position = 1
local lineNumber = 1
while position <= #xml do
local char = xml:sub(position, position)
if char:match("%s") then
position = position + 1
elseif xml:sub(position, position + 8) == "<![CDATA[" then
local endPos = xml:find("]]>", position + 9)
if not endPos then errorManager.error("Unclosed CDATA section") end
table.insert(tokens, {
type = TokenType.CDATA,
value = xml:sub(position + 9, endPos - 1)
})
position = endPos + 3
elseif xml:sub(position, position + 3) == "<!--" then
local endPos = xml:find("-->", position + 4)
if not endPos then errorManager.error("Unclosed comment") end
table.insert(tokens, {
type = TokenType.COMMENT,
value = xml:sub(position + 4, endPos - 1)
})
position = endPos + 3
elseif char == "<" then
if xml:sub(position + 1, position + 1) == "/" then
local endPos = xml:find(">", position)
if not endPos then errorManager.error("Unclosed tag") end
table.insert(tokens, {
type = TokenType.TAG_CLOSE,
value = xml:sub(position + 2, endPos - 1):match("^%s*(.-)%s*$")
})
position = endPos + 1
else
local tagContent = ""
position = position + 1
local selfClosing = false
while position <= #xml do
char = xml:sub(position, position)
if char == ">" then
table.insert(tokens, {
type = selfClosing and TokenType.TAG_SELF_CLOSE or TokenType.TAG_OPEN,
value = tagContent:match("^%s*(.-)%s*$")
})
position = position + 1
break
elseif char == "/" and xml:sub(position + 1, position + 1) == ">" then
table.insert(tokens, {
type = TokenType.TAG_SELF_CLOSE,
value = tagContent:match("^%s*(.-)%s*$")
})
position = position + 2
break
elseif char == "/" and xml:sub(position - 1, position - 1):match("%s") then
selfClosing = true
else
tagContent = tagContent .. char
end
position = position + 1
end
end
else
local text = ""
while position <= #xml and xml:sub(position, position) ~= "<" do
text = text .. xml:sub(position, position)
position = position + 1
end
if text:match("%S") then
table.insert(tokens, {
type = TokenType.TEXT,
value = text:match("^%s*(.-)%s*$")
})
end
end
if char == "\n" then
lineNumber = lineNumber + 1
end
end
return tokens
end
local function parse(tokens)
local root = {
name = "root",
children = {},
attributes = {}
}
local stack = {root}
local current = root
local i = 1
while i <= #tokens do
local token = tokens[i]
if token.type == TokenType.TAG_OPEN then
local tagName, attributes = token.value:match("(%S+)(.*)")
local node = {
name = tagName,
attributes = {},
children = {},
parent = current,
line = token.line
}
for key, value in attributes:gmatch('%s(%w+)="([^"]-)"') do
node.attributes[key] = value
end
table.insert(current.children, node)
table.insert(stack, node)
current = node
elseif token.type == TokenType.TAG_SELF_CLOSE then
local tagName, attributes = token.value:match("(%S+)(.*)")
local node = {
name = tagName,
attributes = {},
children = {},
parent = current,
line = token.line
}
for key, value in attributes:gmatch('%s(%w+)="([^"]-)"') do
node.attributes[key] = value
end
table.insert(current.children, node)
elseif token.type == TokenType.TAG_CLOSE then
if current.name ~= token.value then
errorManager.error(string.format("Mismatched closing tag: expected </%s>, got </%s>",
current.name, token.value))
end
table.remove(stack)
current = stack[#stack]
elseif token.type == TokenType.TEXT then
table.insert(current.children, {
name = "#text",
value = token.value,
line = token.line
})
elseif token.type == TokenType.CDATA then
table.insert(current.children, {
name = "#cdata",
value = token.value,
line = token.line
})
elseif token.type == TokenType.COMMENT then
table.insert(current.children, {
name = "#comment",
value = token.value,
line = token.line
})
end
i = i + 1
end
return root
end
function XMLParser.parse(xmlString)
local tokens = tokenize(xmlString)
return parse(tokens)
end
local function evaluateExpression(expr, scope)
if not expr then return expr end
if expr:match("^%${.*}$") then
local inner = expr:match("^%${(.*)}$")
if inner:match("^[%w_]+$") then
if scope and scope[inner] then
return scope[inner]
else
errorManager.error(string.format('Variable "%s" not found in scope', inner))
return expr
end
end
local env = setmetatable({}, {
__index = function(_, k)
if scope and scope[k] then
return scope[k]
elseif _ENV[k] then
return _ENV[k]
else
error(string.format('Variable "%s" not found in scope', k))
end
end
})
local func, err = load("return " .. inner, "expression", "t", env)
if not func then
errorManager.error("Failed to parse expression: " .. err)
return expr
end
local ok, result = pcall(func)
if not ok then
errorManager.error("Failed to evaluate expression: " .. result)
return expr
end
return result
end
return expr:gsub("%${([^}]+)}", function(e)
if e:match("^[%w_]+$") then
if scope and scope[e] then
return tostring(scope[e])
else
errorManager.error(string.format('Variable "%s" not found in scope', e))
return e
end
end
local env = setmetatable({}, {__index = function(_, k)
return scope and scope[k] or _ENV[k]
end})
local func, err = load("return " .. e, "expression", "t", env)
if not func then
errorManager.error("Failed to parse expression: " .. err)
return e
end
return tostring(func())
local parseAttributes = function(node, s)
local _, _ = string.gsub(s, "(%w+)=([\"'])(.-)%2", function(attribute, _, value)
node:addAttribute(attribute, "\"" .. value .. "\"")
end)
local _, _ = string.gsub(s, "(%w+)={(.-)}", function(attribute, expression)
node:addAttribute(attribute, expression)
end)
end
local function convertValue(value, propertyType, scope)
if propertyType == "string" and type(value) == "string" then
if value:find("${") then
return evaluateExpression(value, scope)
end
end
if type(value) == "string" and value:match("^%${.*}$") then
return evaluateExpression(value, scope)
end
if propertyType == "number" then
if(tonumber(value) == nil) then
return value
end
return tonumber(value)
elseif propertyType == "boolean" then
return value == "true"
elseif propertyType == "color" then
return colors[value]
elseif propertyType == "table" then
local env = setmetatable({}, { __index = _ENV })
local func = load("return "..value, nil, "t", env)
if func then
return func()
end
end
return value
end
local actionHandlers = {
setProperty = function(node, element, scope)
return function(...)
local target = node.attributes.target or "self"
local targetElement
if target == "self" then
targetElement = element
elseif target == "parent" then
targetElement = element.parent
local XMLParser = {
parseText = function(xmlText)
local stack = {}
local top = XMLNode.new()
table.insert(stack, top)
local ni, c, label, xarg, empty
local i, j = 1, 1
while true do
ni, j, c, label, xarg, empty = string.find(xmlText, "<(%/?)([%w_:]+)(.-)(%/?)>", i)
if not ni then break end
local text = string.sub(xmlText, i, ni - 1);
if not string.find(text, "^%s*$") then
local lVal = (top.value or "") .. text
stack[#stack].value = lVal
end
if empty == "/" then
local lNode = XMLNode.new(label)
parseAttributes(lNode, xarg)
top:addChild(lNode)
elseif c == "" then
local lNode = XMLNode.new(label)
parseAttributes(lNode, xarg)
table.insert(stack, lNode)
top = lNode
else
targetElement = element:getBaseFrame():getChild(target)
end
local toclose = table.remove(stack)
if not targetElement then
errorManager.error(string.format('Target "%s" not found', target))
return
top = stack[#stack]
if #stack < 1 then
errorManager.error("XMLParser: nothing to close with " .. label)
end
if toclose.tag ~= label then
errorManager.error("XMLParser: trying to close " .. toclose.tag .. " with " .. label)
end
top:addChild(toclose)
end
local property = node.attributes.property
local propertyConfig = targetElement:getPropertyConfig(property)
if not propertyConfig then
errorManager.error(string.format('Unknown property "%s"', property))
return
end
local value = convertValue(node.attributes.value, propertyConfig.type, scope)
targetElement.set(property, value)
i = j + 1
end
end,
execute = function(node, element, scope)
return function(...)
local funcName = node.attributes["function"]
if not scope[funcName] then
errorManager.error(string.format('Function "%s" not found in scope', funcName))
return
end
scope[funcName](element, ...)
end
end,
setValue = function(node, element, scope)
return function(...)
local name = node.attributes.name
local value = convertValue(node.attributes.value, "string", scope)
scope[name] = value
local text = string.sub(xmlText, i);
if #stack > 1 then
error("XMLParser: unclosed " .. stack[#stack].tag)
end
return top.children
end
}
local eventParameters = {
onClick = {"self", "button", "x", "y"},
onScroll = {"self", "direction", "x", "y"},
onDrag = {"self", "button", "x", "y"},
onKey = {"self", "key"},
onChar = {"self", "char"},
onKeyUp = {"self", "key"},
local log = require("log").debug
}
local function handleEvent(node, element, scope)
local isEventNode = node.name:match("^on%u")
if not isEventNode then return end
local eventName = node.name:sub(3,3):lower() .. node.name:sub(4)
local handlers = {}
for _, child in ipairs(node.children or {}) do
if child.name == "#cdata" then
if not isEventNode then
errorManager.error("CDATA blocks can only be used inside event tags")
return
end
local eventName = node.name:sub(3)
local params = eventParameters["on"..eventName] or {"self"}
local paramString = table.concat(params, ", ")
local codeTemplate = [[
return function(%s)
%s
end
]]
local env = {}
if scope then
for k,v in pairs(scope) do
env[k] = v
end
end
env.colors = colors
env.term = term
env.math = math
local code = child.value:gsub("^%s+", ""):gsub("%s+$", "")
local finalCode = string.format(codeTemplate, paramString, code)
local func, err = load(finalCode, "event", "t", env)
if err then
errorManager.error("Failed to parse event: " .. err)
elseif func then
local eventName = node.name:sub(3,3):lower() .. node.name:sub(4)
element["on"..eventName:sub(1,1):upper()..eventName:sub(2)](element, func())
end
elseif child.name ~= "#text" then
local handler = actionHandlers[child.name]
if not handler then
errorManager.error(string.format('Unknown action tag "%s"', child.name))
return
end
table.insert(handlers, handler(child, element, scope))
end
local function convertValue(value, scope)
if value:sub(1,1) == "\"" and value:sub(-1) == "\"" then
value = value:sub(2, -2)
end
if #handlers > 0 then
element["on"..eventName:sub(1,1):upper()..eventName:sub(2)](element, function(...)
for _, handler in ipairs(handlers) do
handler(...)
end
end)
end
end
local function parsePropertyTag(node, element, scope)
local propertyConfig = element:getPropertyConfig(node.name)
if propertyConfig then
if propertyConfig.type == "table" then
local tableData = {}
for _, child in ipairs(node.children) do
if child.name == "item" or child.name == "entry" then
local entry = {}
for attr, value in pairs(child.attributes) do
if(colors[value])then
entry[attr] = colors[value]
else
entry[attr] = convertValue(value, "string", scope)
end
end
for _, prop in ipairs(child.children) do
if prop.name ~= "#text" and prop.name ~= "#cdata" then
if prop.children and #prop.children > 0 then
local firstChild = prop.children[1]
if firstChild.name == "#text" then
entry[prop.name] = convertValue(firstChild.value, "string", scope)
end
else
local subEntry = {}
for subAttr, subValue in pairs(prop.attributes) do
subEntry[subAttr] = convertValue(subValue, "string", scope)
end
entry[prop.name] = next(subEntry) and subEntry or ""
end
end
end
table.insert(tableData, entry)
end
end
element.set(node.name, tableData)
return true
if value:sub(1,2) == "${" and value:sub(-1) == "}" then
value = value:sub(3, -2)
if(scope[value])then
return scope[value]
else
local textNode = node.children[1]
if textNode and textNode.name == "#text" then
element.set(node.name, convertValue(textNode.value, propertyConfig.type, scope))
return true
errorManager.error("XMLParser: variable '" .. value .. "' not found in scope")
end
end
if value:match("^%s*<!%[CDATA%[.*%]%]>%s*$") then
local cdata = value:match("<!%[CDATA%[(.*)%]%]>")
local env = _ENV
for k,v in pairs(scope) do
env[k] = v
end
return load("return " .. cdata, nil, "bt", env)()
end
if value == "true" then
return true
elseif value == "false" then
return false
elseif colors[value] then
return colors[value]
elseif tonumber(value) then
return tonumber(value)
else
return value
end
end
local function createTableFromNode(node, scope)
local list = {}
for _, child in pairs(node.children) do
if child.tag == "item" or child.tag == "entry" then
local item = {}
for attrName, attrValue in pairs(child.attributes) do
item[attrName] = convertValue(attrValue, scope)
end
for _, subChild in pairs(child.children) do
if subChild.value then
item[subChild.tag] = convertValue(subChild.value, scope)
elseif #subChild.children > 0 then
item[subChild.tag] = createTableFromNode(subChild)
end
end
table.insert(list, item)
else
if child.value then
list[child.tag] = convertValue(child.value, scope)
elseif #child.children > 0 then
list[child.tag] = createTableFromNode(child)
end
end
end
return false
return list
end
---@class Container
local Container = {}
--- Loads and creates UI elements from XML content
--- @shortDescription Loads UI from XML string
--- @param self Container The container to load into
--- @param content string The XML content to parse
--- @param scope? table Optional scope for variable resolution
--- @return Container self The container instance
--- @usage
--- local xml = [[
--- <Frame>
--- <Button name="myButton" x="5" y="5"/>
--- </Frame>
--- ]]
--- container:loadXML(xml)
function Container:loadXML(content, scope)
scope = scope or {}
local tree = XMLParser.parse(content)
local nodes = XMLParser.parseText(content)
self:fromXML(nodes, scope)
end
local function createElements(nodes, parent, scope)
for _, node in ipairs(nodes.children) do
if node.name:sub(1,1) ~= "#" then
if node.name:match("^on") then
handleEvent(node, parent, scope)
else
local handled = parsePropertyTag(node, parent, scope)
local baseFromXml
function Container.setup()
baseFromXml = require("elementManager").getElement("BaseElement").fromXML
end
if not handled then
local elementType = node.name:sub(1,1):upper() .. node.name:sub(2)
local addMethod = "add"..elementType
function Container:fromXML(content, scope)
baseFromXml(self, content, scope)
for _, node in ipairs(content) do
local capitalizedName = node.tag:sub(1,1):upper() .. node.tag:sub(2)
if self["add"..capitalizedName] then
local element = self["add"..capitalizedName](self)
element:fromXML(node, scope)
end
end
end
if not parent[addMethod] then
local parentType = parent.get and parent.get("type") or "Unknown"
errorManager.error(string.format(
'Tag <%s> is not valid inside <%s>',
node.name, parentType:lower()
))
return
end
local element = parent[addMethod](parent, node.attributes.name)
for attr, value in pairs(node.attributes) do
local config = element:getPropertyConfig(attr)
if config then
local convertedValue = convertValue(value, config.type, scope)
element.set(attr, convertedValue)
end
end
if #node.children > 0 then
createElements(node, element, scope)
end
local BaseElement = {}
function BaseElement:fromXML(node, scope)
if(node.attributes)then
for k, v in pairs(node.attributes) do
if(self._properties[k])then
self.set(k, convertValue(v, scope))
elseif self[k] then
if(k:sub(1,2)=="on")then
local val = v:gsub("\"", "")
if(scope[val])then
self[k](self, scope[val])
else
errorManager.error("XMLParser: variable '" .. v .. "' not found in scope")
end
else
errorManager.error("XMLParser: property '" .. k .. "' not found in element '" .. self:getType() .. "'")
end
else
errorManager.error("XMLParser: property '" .. k .. "' not found in element '" .. self:getType() .. "'")
end
end
end
createElements(tree, self, scope)
return self
if(node.children)then
for _, child in pairs(node.children) do
if(self._properties[child.tag])then
if(self._properties[child.tag].type == "table")then
self.set(child.tag, createTableFromNode(child, scope))
else
self.set(child.tag, convertValue(child.value, scope))
end
else
local args = {}
if(child.children)then
for _, child in pairs(child.children) do
if(child.tag == "param")then
table.insert(args, convertValue(child.value, scope))
elseif (child.tag == "table")then
table.insert(args, createTableFromNode(child, scope))
else
errorManager.error("XMLParser: unknown child '" .. child.tag .. "' in element '" .. self:getType() .. "'")
end
end
end
if(self[child.tag])then
if(#args > 0)then
self[child.tag](self, table.unpack(args))
elseif(child.value)then
self[child.tag](self, convertValue(child.value, scope))
else
self[child.tag](self)
end
else
errorManager.error("XMLParser: method '" .. child.tag .. "' not found in element '" .. self:getType() .. "'")
end
end
end
end
end
return {
Container = Container
API = XMLParser,
Container = Container,
BaseElement = BaseElement
}

View File

@@ -2,6 +2,10 @@ local function findClassName(content)
return content:match("%-%-%-@class%s+(%w+)")
end
local function findParentClass(content)
return content:match("%-%-%-@class%s+%w+%s*:%s*(%w+)")
end
local function parseProperties(content)
local properties = {}
for line in content:gmatch("[^\r\n]+") do
@@ -80,7 +84,40 @@ local function collectAllClassNames(folder)
return classes
end
local function getParentProperties(parentClass, allClasses)
-- Rekursiv alle Properties der Elternklasse(n) holen
local properties = {}
if parentClass then
for _, classContent in pairs(allClasses) do
if classContent.name == parentClass then
-- Properties der Elternklasse kopieren
for _, prop in ipairs(classContent.properties) do
table.insert(properties, prop)
end
-- Auch von der Elternklasse der Elternklasse holen
if classContent.parent then
local parentProps = getParentProperties(classContent.parent, allClasses)
for _, prop in ipairs(parentProps) do
table.insert(properties, prop)
end
end
break
end
end
end
return properties
end
local function generateClassContent(className, properties, combinedProperties, events, allClasses)
-- Parent-Klasse finden
local parentClass = findParentClass(content)
-- Properties der Elternklasse(n) holen
local inheritedProps = getParentProperties(parentClass, allClasses)
-- Mit eigenen Properties kombinieren
for _, prop in ipairs(inheritedProps) do
table.insert(properties, prop)
end
if #properties == 0 and #events == 0 and className ~= "Container" then
return nil
end