From 78a42954c042a0b4511c159016846c5f4a3c411c Mon Sep 17 00:00:00 2001
From: Robert Jelic <36573031+NoryiE@users.noreply.github.com>
Date: Wed, 5 Mar 2025 01:25:49 +0100
Subject: [PATCH] Reworked XML #2
---
src/elements/Container.lua | 2 +-
src/main.lua | 7 +-
src/plugins/benchmark.lua | 3 +-
src/plugins/xml.lua | 682 +++++++++++--------------------------
tools/annotationParser.lua | 37 ++
5 files changed, 237 insertions(+), 494 deletions(-)
diff --git a/src/elements/Container.lua b/src/elements/Container.lua
index 548a954..548d990 100644
--- a/src/elements/Container.lua
+++ b/src/elements/Container.lua
@@ -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
diff --git a/src/main.lua b/src/main.lua
index d31809a..e289703 100644
--- a/src/main.lua
+++ b/src/main.lua
@@ -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
diff --git a/src/plugins/benchmark.lua b/src/plugins/benchmark.lua
index a4dad25..1be3ea3 100644
--- a/src/plugins/benchmark.lua
+++ b/src/plugins/benchmark.lua
@@ -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
diff --git a/src/plugins/xml.lua b/src/plugins/xml.lua
index 7d1351b..02f78d7 100644
--- a/src/plugins/xml.lua
+++ b/src/plugins/xml.lua
@@ -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) == "", 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) == "", 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*%s*$") then
+ local cdata = value:match("")
+ 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 = [[
----
----
----
---- ]]
---- 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
}
\ No newline at end of file
diff --git a/tools/annotationParser.lua b/tools/annotationParser.lua
index 524362a..de4097d 100644
--- a/tools/annotationParser.lua
+++ b/tools/annotationParser.lua
@@ -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