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 , got ", - 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 = [[ ---- ----