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 return value
end}) 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") Container.combineProperties(Container, "offset", "offsetX", "offsetY")
for k, _ in pairs(elementManager:getElementList()) do for k, _ in pairs(elementManager:getElementList()) do

View File

@@ -3,13 +3,12 @@ local errorManager = require("errorManager")
local propertySystem = require("propertySystem") local propertySystem = require("propertySystem")
local expect = require("libraries/expect") 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. --- 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: --- Before you can access Basalt, you need to add the following code on top of your file:
--- @usage local basalt = require("basalt") --- @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". --- 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 traceback boolean Whether to show a traceback on errors
--- @field _events table A table of events and their callbacks --- @field _events table A table of events and their callbacks
--- @field _schedule function[] A table of scheduled functions --- @field _schedule function[] A table of scheduled functions
@@ -92,7 +91,7 @@ end
--- Creates and returns a new BaseFrame --- Creates and returns a new BaseFrame
--- @shortDescription Creates 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() --- @usage local mainFrame = basalt.createFrame()
function basalt.createFrame() function basalt.createFrame()
local frame = basalt.create("BaseFrame") local frame = basalt.create("BaseFrame")
@@ -111,7 +110,7 @@ end
--- Gets or creates the main frame --- Gets or creates the main frame
--- @shortDescription 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() --- @usage local frame = basalt.getMainFrame()
function basalt.getMainFrame() function basalt.getMainFrame()
if(mainFrame == nil)then if(mainFrame == nil)then

View File

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

View File

@@ -1,527 +1,235 @@
local errorManager = require("errorManager") local errorManager = require("errorManager")
local log = require("log")
local XMLParser = {} local XMLNode = {
local TokenType = { new = function(tag)
TAG_OPEN = "TAG_OPEN", return {
TAG_CLOSE = "TAG_CLOSE", tag = tag,
TAG_SELF_CLOSE = "TAG_SELF_CLOSE", value = nil,
ATTRIBUTE = "ATTRIBUTE",
TEXT = "TEXT",
CDATA = "CDATA",
COMMENT = "COMMENT"
}
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 = {}, attributes = {},
children = {}, children = {},
parent = current,
line = token.line addChild = function(self, child)
table.insert(self.children, child)
end,
addAttribute = function(self, tag, value)
self.attributes[tag] = value
end
}
end
} }
for key, value in attributes:gmatch('%s(%w+)="([^"]-)"') do local parseAttributes = function(node, s)
node.attributes[key] = value local _, _ = string.gsub(s, "(%w+)=([\"'])(.-)%2", function(attribute, _, value)
end node:addAttribute(attribute, "\"" .. value .. "\"")
end)
table.insert(current.children, node) local _, _ = string.gsub(s, "(%w+)={(.-)}", function(attribute, expression)
table.insert(stack, node) node:addAttribute(attribute, expression)
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())
end) end)
end end
local function convertValue(value, propertyType, scope) local XMLParser = {
if propertyType == "string" and type(value) == "string" then parseText = function(xmlText)
if value:find("${") then local stack = {}
return evaluateExpression(value, scope) 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 end
end if empty == "/" then
local lNode = XMLNode.new(label)
if type(value) == "string" and value:match("^%${.*}$") then parseAttributes(lNode, xarg)
return evaluateExpression(value, scope) top:addChild(lNode)
end elseif c == "" then
local lNode = XMLNode.new(label)
if propertyType == "number" then parseAttributes(lNode, xarg)
if(tonumber(value) == nil) then table.insert(stack, lNode)
return value top = lNode
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
else else
targetElement = element:getBaseFrame():getChild(target) local toclose = table.remove(stack)
end
if not targetElement then top = stack[#stack]
errorManager.error(string.format('Target "%s" not found', target)) if #stack < 1 then
return errorManager.error("XMLParser: nothing to close with " .. label)
end end
if toclose.tag ~= label then
local property = node.attributes.property errorManager.error("XMLParser: trying to close " .. toclose.tag .. " with " .. label)
local propertyConfig = targetElement:getPropertyConfig(property)
if not propertyConfig then
errorManager.error(string.format('Unknown property "%s"', property))
return
end end
top:addChild(toclose)
local value = convertValue(node.attributes.value, propertyConfig.type, scope)
targetElement.set(property, value)
end end
end, i = j + 1
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 end
scope[funcName](element, ...) local text = string.sub(xmlText, i);
end if #stack > 1 then
end, error("XMLParser: unclosed " .. stack[#stack].tag)
setValue = function(node, element, scope)
return function(...)
local name = node.attributes.name
local value = convertValue(node.attributes.value, "string", scope)
scope[name] = value
end end
return top.children
end end
} }
local eventParameters = { local log = require("log").debug
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 function convertValue(value, scope)
if value:sub(1,1) == "\"" and value:sub(-1) == "\"" then
local function handleEvent(node, element, scope) value = value:sub(2, -2)
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 end
local eventName = node.name:sub(3) if value:sub(1,2) == "${" and value:sub(-1) == "}" then
local params = eventParameters["on"..eventName] or {"self"} value = value:sub(3, -2)
if(scope[value])then
local paramString = table.concat(params, ", ") return scope[value]
else
local codeTemplate = [[ errorManager.error("XMLParser: variable '" .. value .. "' not found in scope")
return function(%s) end
%s
end end
]]
local env = {} if value:match("^%s*<!%[CDATA%[.*%]%]>%s*$") then
if scope then local cdata = value:match("<!%[CDATA%[(.*)%]%]>")
local env = _ENV
for k,v in pairs(scope) do for k,v in pairs(scope) do
env[k] = v env[k] = v
end end
return load("return " .. cdata, nil, "bt", env)()
end end
env.colors = colors if value == "true" then
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
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 return true
else elseif value == "false" then
local textNode = node.children[1]
if textNode and textNode.name == "#text" then
element.set(node.name, convertValue(textNode.value, propertyConfig.type, scope))
return true
end
end
end
return false return false
elseif colors[value] then
return colors[value]
elseif tonumber(value) then
return tonumber(value)
else
return value
end
end end
---@class Container local function createTableFromNode(node, scope)
local Container = {} local list = {}
--- Loads and creates UI elements from XML content for _, child in pairs(node.children) do
--- @shortDescription Loads UI from XML string if child.tag == "item" or child.tag == "entry" then
--- @param self Container The container to load into local item = {}
--- @param content string The XML content to parse
--- @param scope? table Optional scope for variable resolution for attrName, attrValue in pairs(child.attributes) do
--- @return Container self The container instance item[attrName] = convertValue(attrValue, scope)
--- @usage end
--- local xml = [[
--- <Frame> for _, subChild in pairs(child.children) do
--- <Button name="myButton" x="5" y="5"/> if subChild.value then
--- </Frame> item[subChild.tag] = convertValue(subChild.value, scope)
--- ]] elseif #subChild.children > 0 then
--- container:loadXML(xml) 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 list
end
local Container = {}
function Container:loadXML(content, scope) function Container:loadXML(content, scope)
scope = scope or {} scope = scope or {}
local tree = XMLParser.parse(content) local nodes = XMLParser.parseText(content)
self:fromXML(nodes, scope)
end
local function createElements(nodes, parent, scope) local baseFromXml
for _, node in ipairs(nodes.children) do function Container.setup()
if node.name:sub(1,1) ~= "#" then baseFromXml = require("elementManager").getElement("BaseElement").fromXML
if node.name:match("^on") then end
handleEvent(node, parent, scope)
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
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 else
local handled = parsePropertyTag(node, parent, scope) errorManager.error("XMLParser: variable '" .. v .. "' not found in scope")
if not handled then
local elementType = node.name:sub(1,1):upper() .. node.name:sub(2)
local addMethod = "add"..elementType
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
end end
else
errorManager.error("XMLParser: property '" .. k .. "' not found in element '" .. self:getType() .. "'")
end end
else
errorManager.error("XMLParser: property '" .. k .. "' not found in element '" .. self:getType() .. "'")
end end
end end
end end
createElements(tree, self, scope) if(node.children)then
return self 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 end
return { 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+)") return content:match("%-%-%-@class%s+(%w+)")
end end
local function findParentClass(content)
return content:match("%-%-%-@class%s+%w+%s*:%s*(%w+)")
end
local function parseProperties(content) local function parseProperties(content)
local properties = {} local properties = {}
for line in content:gmatch("[^\r\n]+") do for line in content:gmatch("[^\r\n]+") do
@@ -80,7 +84,40 @@ local function collectAllClassNames(folder)
return classes return classes
end 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) 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 if #properties == 0 and #events == 0 and className ~= "Container" then
return nil return nil
end end