diff --git a/src/elements/Container.lua b/src/elements/Container.lua index 92f0f5c..21deeaf 100644 --- a/src/elements/Container.lua +++ b/src/elements/Container.lua @@ -102,6 +102,14 @@ end function Container:init(props, basalt) VisualElement.init(self, props, basalt) self.set("type", "Container") + self:observe("width", function() + self.set("childrenSorted", false) + self.set("childrenEventsSorted", false) + end) + self:observe("height", function() + self.set("childrenSorted", false) + self.set("childrenEventsSorted", false) + end) end --- Returns whether a child is visible @@ -339,11 +347,13 @@ end local function convertMousePosition(self, event, ...) local args = {...} - if event:find("mouse_") then - local button, absX, absY = ... - local xOffset, yOffset = self.get("offsetX"), self.get("offsetY") - local relX, relY = self:getRelativePosition(absX + xOffset, absY + yOffset) - args = {button, relX, relY} + if event then + if event:find("mouse_") then + local button, absX, absY = ... + local xOffset, yOffset = self.get("offsetX"), self.get("offsetY") + local relX, relY = self:getRelativePosition(absX + xOffset, absY + yOffset) + args = {button, relX, relY} + end end return args end @@ -680,6 +690,9 @@ end --- @private function Container:destroy() if not self:isType("BaseFrame") then + for _, child in ipairs(self.get("children")) do + child:destroy() + end self.set("childrenSorted", false) VisualElement.destroy(self) return self diff --git a/src/elements/Flexbox.lua b/src/elements/Flexbox.lua index c953241..51143b8 100644 --- a/src/elements/Flexbox.lua +++ b/src/elements/Flexbox.lua @@ -787,6 +787,7 @@ function Flexbox:addChild(element) return self end +--- Removes a child element from the flexbox --- @shortDescription Removes a child element from the flexbox --- @param element Element The child element to remove --- @return Flexbox self The flexbox instance diff --git a/src/elements/Program.lua b/src/elements/Program.lua index 34ffff5..39557bb 100644 --- a/src/elements/Program.lua +++ b/src/elements/Program.lua @@ -17,6 +17,8 @@ Program.defineProperty(Program, "path", {default = "", type = "string"}) Program.defineProperty(Program, "running", {default = false, type = "boolean"}) --- @property errorCallback function nil The error callback function Program.defineProperty(Program, "errorCallback", {default = nil, type = "function"}) +--- @property doneCallback function nil The done callback function +Program.defineProperty(Program, "doneCallback", {default = nil, type = "function"}) Program.defineEvent(Program, "*") @@ -82,6 +84,10 @@ function BasaltProgram:run(path, width, height) local ok, result = coroutine.resume(self.coroutine) term.redirect(current) if not ok then + local doneCallback = self.program.get("doneCallback") + if doneCallback then + doneCallback(self.program, ok, result) + end local errorCallback = self.program.get("errorCallback") if errorCallback then local trace = debug.traceback(self.coroutine, result) @@ -94,6 +100,14 @@ function BasaltProgram:run(path, width, height) errorManager.header = "Basalt Program Error ".. path errorManager.error(result) end + if coroutine.status(self.coroutine)=="dead" then + self.program.set("running", false) + self.program.set("program", nil) + local doneCallback = self.program.get("doneCallback") + if doneCallback then + doneCallback(self.program, ok, result) + end + end else errorManager.header = "Basalt Program Error ".. path errorManager.error("File not found") @@ -128,10 +142,23 @@ function BasaltProgram:resume(event, ...) if ok then self.filter = result + if coroutine.status(self.coroutine)=="dead" then + self.program.set("running", false) + self.program.set("program", nil) + local doneCallback = self.program.get("doneCallback") + if doneCallback then + doneCallback(self.program, ok, result) + end + end else + local doneCallback = self.program.get("doneCallback") + if doneCallback then + doneCallback(self.program, ok, result) + end local errorCallback = self.program.get("errorCallback") if errorCallback then local trace = debug.traceback(self.coroutine, result) + trace = trace == nil and "" or trace local _result = errorCallback(self.program, result, trace:gsub(result, "")) if(_result==false)then self.filter = nil @@ -235,6 +262,15 @@ function Program:onError(fn) return self end +--- Registers a callback for the program's done event +--- @shortDescription Registers a callback for the program's done event +--- @param fn function The callback function to register +--- @return Program self The Program instance +function Program:onDone(fn) + self.set("doneCallback", fn) + return self +end + --- @shortDescription Handles all incomming events --- @param event string The event to handle --- @param ... any The event arguments diff --git a/tools/BasaltDoc/Button.lua b/tools/BasaltDoc/Button.lua new file mode 100644 index 0000000..fe13885 --- /dev/null +++ b/tools/BasaltDoc/Button.lua @@ -0,0 +1,48 @@ +local elementManager = require("elementManager") +local VisualElement = elementManager.getElement("VisualElement") +local getCenteredPosition = require("libraries/utils").getCenteredPosition +---@cofnigDescription The Button is a standard button element with click handling and state management. + +--- The Button is a standard button element with click handling and state management. +---@class Button : VisualElement +local Button = setmetatable({}, VisualElement) +Button.__index = Button + +---@property text string Button Button text +Button.defineProperty(Button, "text", {default = "Button", type = "string", canTriggerRender = true}) + +Button.defineEvent(Button, "mouse_click") +Button.defineEvent(Button, "mouse_up") + +--- @shortDescription Creates a new Button instance +--- @return table self The created instance +--- @private +function Button.new() + local self = setmetatable({}, Button):__init() + self.class = Button + self.set("width", 10) + self.set("height", 3) + self.set("z", 5) + return self +end + +--- @shortDescription Initializes the Button instance +--- @param props table The properties to initialize the element with +--- @param basalt table The basalt instance +--- @protected +function Button:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "Button") +end + +--- @shortDescription Renders the Button +--- @protected +function Button:render() + VisualElement.render(self) + local text = self.get("text") + text = text:sub(1, self.get("width")) + local xO, yO = getCenteredPosition(text, self.get("width"), self.get("height")) + self:textFg(xO, yO, text, self.get("foreground")) +end + +return Button \ No newline at end of file diff --git a/tools/BasaltDoc/ldoc-markdown-parser/README.md b/tools/BasaltDoc/ldoc-markdown-parser/README.md new file mode 100644 index 0000000..cb65ac9 --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/README.md @@ -0,0 +1,36 @@ +# ldoc-markdown-parser + +## Overview +`ldoc-markdown-parser` is a simple and extensible Lua documentation parser that converts Lua documentation comments into Markdown format. It supports both single-line and multi-line comments, making it easy to document your Lua code in a structured way. + +## Features +- Extracts single-line and multi-line comments from Lua files. +- Parses custom tags such as `@property`, `@shortDescription`, `@param`, and `@return`. +- Converts extracted comments and parsed tags into Markdown format. +- Easy to extend and customize for additional tags or formatting options. + +## Installation +To install the `ldoc-markdown-parser`, clone the repository and navigate to the project directory: + +```bash +git clone +cd ldoc-markdown-parser +``` + +## Usage +To use the parser, run the `main.lua` file with the Lua interpreter, providing the path to the Lua file you want to parse: + +```bash +lua src/main.lua path/to/your/lua_file.lua +``` + +The output will be generated in Markdown format and can be found in the specified output directory. + +## Example +An example Lua file can be found in the `examples/input/example.lua`, and the expected output Markdown file is located in `examples/output/example.md`. + +## Contributing +Contributions are welcome! Please feel free to submit a pull request or open an issue for any enhancements or bug fixes. + +## License +This project is licensed under the MIT License. See the LICENSE file for more details. \ No newline at end of file diff --git a/tools/BasaltDoc/ldoc-markdown-parser/src/parser/block_parsers/function.lua b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/block_parsers/function.lua new file mode 100644 index 0000000..b2d4249 --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/block_parsers/function.lua @@ -0,0 +1,64 @@ +local FunctionParser = {} + +--- Check if a block represents a function +function FunctionParser.canParse(block) + return block.context and block.context.type == "function" +end + +--- Parse a complete function documentation block +function FunctionParser.parse(block) + local functionDoc = { + type = "function", + name = block.context.name, + shortDescription = nil, + params = {}, + returns = {}, + visibility = "public", -- default + context = block.context, + content = {} + } + + for _, line in ipairs(block.comments) do + -- Parse shortDescription + local shortDesc = line:match("^%-*%s*@shortDescription%s+(.*)$") + if shortDesc then + functionDoc.shortDescription = shortDesc + + -- Parse param + elseif line:match("^%-*%s*@param") then + local paramName, paramType, paramDesc = line:match("^%-*%s*@param%s+(%S+)%s+(%S+)%s+(.*)$") + if paramName then + table.insert(functionDoc.params, { + name = paramName, + type = paramType, + description = paramDesc or "" + }) + end + + -- Parse return + elseif line:match("^%-*%s*@return") then + local returnType, returnName, returnDesc = line:match("^%-*%s*@return%s+(%S+)%s+(%S+)%s+(.*)$") + if returnType then + table.insert(functionDoc.returns, { + type = returnType, + name = returnName or "", + description = returnDesc or "" + }) + end + + -- Parse visibility + elseif line:match("^%-*%s*@private%s*$") then + functionDoc.visibility = "private" + elseif line:match("^%-*%s*@protected%s*$") then + functionDoc.visibility = "protected" + + -- Regular content + else + table.insert(functionDoc.content, line) + end + end + + return functionDoc +end + +return FunctionParser diff --git a/tools/BasaltDoc/ldoc-markdown-parser/src/parser/comment_extractor.lua b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/comment_extractor.lua new file mode 100644 index 0000000..eef506f --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/comment_extractor.lua @@ -0,0 +1,101 @@ +local CommentExtractor = {} + +--- Extracts comments with their associated code context +-- @param lines table The lines of the Lua file as a table of strings. +-- @return table A table containing comment blocks with context. +function CommentExtractor.extractComments(lines) + local blocks = {} + local currentCommentBlock = {} + local i = 1 + + while i <= #lines do + local line = lines[i]:match("^%s*(.*)") -- Trim leading whitespace + + -- Check if this is a comment line + if line:find("^%-%-%-") or line:find("^%-%-") then + table.insert(currentCommentBlock, line) + elseif #currentCommentBlock > 0 then + -- We have accumulated comments, check if next non-empty line is code + local codeContext = nil + local j = i + + -- Skip empty lines to find the actual code + while j <= #lines and lines[j]:match("^%s*$") do + j = j + 1 + end + + if j <= #lines then + local codeLine = lines[j]:match("^%s*(.*)") + -- Check if it's a function, class, property, etc. + if codeLine:find("^function") or + codeLine:find("^local function") or + codeLine:find("^local%s+%w+%s*=") or + codeLine:find("^%w+%.%w+") then + codeContext = { + type = CommentExtractor.getCodeType(codeLine), + name = CommentExtractor.extractName(codeLine), + line = codeLine, + lineNumber = j + } + end + end + + -- Add the comment block with its context + table.insert(blocks, { + comments = currentCommentBlock, + context = codeContext + }) + + currentCommentBlock = {} + end + + i = i + 1 + end + + -- Handle any remaining comments + if #currentCommentBlock > 0 then + table.insert(blocks, { + comments = currentCommentBlock, + context = nil + }) + end + + return blocks +end + +--- Determines the type of code (function, class, property, etc.) +function CommentExtractor.getCodeType(codeLine) + if codeLine:find("^function") or codeLine:find("^local function") then + return "function" + elseif codeLine:find("^local%s+%w+%s*=%s*setmetatable") then + return "class" + elseif codeLine:find("^local%s+%w+%s*=") then + return "variable" + elseif codeLine:find("^%w+%.defineProperty") then + return "property_definition" + else + return "unknown" + end +end + +--- Extracts the name from a code line +function CommentExtractor.extractName(codeLine) + -- Function patterns + local funcName = codeLine:match("^function%s+([%w%.%:]+)") + if funcName then return funcName end + + local localFuncName = codeLine:match("^local%s+function%s+([%w%.%:]+)") + if localFuncName then return localFuncName end + + -- Variable/class patterns + local varName = codeLine:match("^local%s+([%w_]+)%s*=") + if varName then return varName end + + -- Method patterns + local methodName = codeLine:match("^([%w%.%:]+)%s*=") + if methodName then return methodName end + + return "unknown" +end + +return CommentExtractor \ No newline at end of file diff --git a/tools/BasaltDoc/ldoc-markdown-parser/src/parser/init.lua b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/init.lua new file mode 100644 index 0000000..c95090a --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/init.lua @@ -0,0 +1,29 @@ +local CommentExtractor = require("parser.comment_extractor") +local MarkdownGenerator = require("parser.markdown_generator") + +local Parser = {} + +--- Extract comments and generate markdown +function Parser.extractComments(content) + local lines = {} + for line in content:gmatch("[^\r\n]+") do + table.insert(lines, line) + end + return CommentExtractor.extractComments(lines) +end + +--- Generate markdown from comment blocks +function Parser.generateMarkdown(commentBlocks) + local parsedBlocks = {} + + -- Parse each block using the appropriate parser + for _, block in ipairs(commentBlocks) do + local parsedBlock = MarkdownGenerator.parseBlock(block) + table.insert(parsedBlocks, parsedBlock) + end + + -- Generate markdown from parsed blocks + return MarkdownGenerator.generateMarkdown(parsedBlocks) +end + +return Parser \ No newline at end of file diff --git a/tools/BasaltDoc/ldoc-markdown-parser/src/parser/markdown_generator.lua b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/markdown_generator.lua new file mode 100644 index 0000000..a622d12 --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/markdown_generator.lua @@ -0,0 +1,142 @@ +local markdownGenerator = {} +local TagParserRegistry = require("parser.tag_parser_registry") + +--- Determines which block parser should handle a given block +--- @param block table The comment block with context +--- @return string|nil The block type that can handle this block +function markdownGenerator.detectBlockType(block) + local blockParsers = TagParserRegistry.getAllBlocks() + + for blockType, parser in pairs(blockParsers) do + if parser.canParse and parser.canParse(block) then + return blockType + end + end + + return nil -- No specific block parser found, use generic parsing +end + +--- Parses a block using the appropriate block parser +--- @param block table The comment block to parse +--- @return table The parsed block data +function markdownGenerator.parseBlock(block) + local blockType = markdownGenerator.detectBlockType(block) + + if blockType then + local parser = TagParserRegistry.getBlock(blockType) + return parser.parse(block) + else + -- Generic parsing using individual tag parsers + return markdownGenerator.parseGenericBlock(block) + end +end + +--- Generic block parsing using individual tag parsers +--- @param block table The comment block to parse +--- @return table The parsed block data +function markdownGenerator.parseGenericBlock(block) + local parsedBlock = { + type = "generic", + tags = {}, + content = {}, + context = block.context + } + + for _, line in ipairs(block.comments) do + local parsed = false + local tagParsers = TagParserRegistry.getAllTags() + + -- Try each registered tag parser + for tagName, parser in pairs(tagParsers) do + local result = parser.parse(line) + if result then + table.insert(parsedBlock.tags, result) + parsed = true + break + end + end + + -- If no tag parser matched, treat as regular content + if not parsed then + table.insert(parsedBlock.content, line) + end + end + + return parsedBlock +end + +--- Converts parsed blocks to markdown +--- @param parsedBlocks table Array of parsed blocks +--- @return string The generated markdown +function markdownGenerator.generateMarkdown(parsedBlocks) + local markdown = {} + + for _, block in ipairs(parsedBlocks) do + if block.type == "function" then + table.insert(markdown, markdownGenerator.generateFunctionMarkdown(block)) + else + table.insert(markdown, markdownGenerator.generateGenericMarkdown(block)) + end + table.insert(markdown, "") -- Empty line between blocks + end + + return table.concat(markdown, "\n") +end + +--- Generate markdown for function blocks +--- @param functionBlock table The parsed function block +--- @return string The generated markdown +function markdownGenerator.generateFunctionMarkdown(functionBlock) + local md = {} + + table.insert(md, string.format("## Function: %s", functionBlock.name)) + + if functionBlock.shortDescription then + table.insert(md, string.format("**Description:** %s", functionBlock.shortDescription)) + end + + if #functionBlock.params > 0 then + table.insert(md, "**Parameters:**") + for _, param in ipairs(functionBlock.params) do + table.insert(md, string.format("- `%s` (%s): %s", param.name, param.type, param.description)) + end + end + + if #functionBlock.returns > 0 then + table.insert(md, "**Returns:**") + for _, ret in ipairs(functionBlock.returns) do + table.insert(md, string.format("- `%s` (%s): %s", ret.name, ret.type, ret.description)) + end + end + + if functionBlock.visibility ~= "public" then + table.insert(md, string.format("**Visibility:** %s", functionBlock.visibility)) + end + + return table.concat(md, "\n") +end + +--- Generate markdown for generic blocks +--- @param block table The parsed generic block +--- @return string The generated markdown +function markdownGenerator.generateGenericMarkdown(block) + local md = {} + + -- Generate markdown for tags + if #block.tags > 0 then + table.insert(md, "### Tags") + for _, tag in ipairs(block.tags) do + table.insert(md, string.format("- **%s**: %s", tag.name, tag.description)) + end + end + + -- Generate markdown for content + if #block.content > 0 then + table.insert(md, "### Content") + table.insert(md, table.concat(block.content, "\n")) + end + + return table.concat(md, "\n\n") +end + +return markdownGenerator \ No newline at end of file diff --git a/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parser.lua b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parser.lua new file mode 100644 index 0000000..1b838e9 --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parser.lua @@ -0,0 +1,19 @@ +local tagParser = {} + +local function parseTag(tagLine) + local tag, content = tagLine:match("^%s*@(.-)%s*(.*)$") + return tag, content +end + +function tagParser.parseTags(commentLines) + local tags = {} + for _, line in ipairs(commentLines) do + local tag, content = parseTag(line) + if tag then + tags[tag] = content + end + end + return tags +end + +return tagParser \ No newline at end of file diff --git a/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parser_registry.lua b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parser_registry.lua new file mode 100644 index 0000000..972a8b3 --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parser_registry.lua @@ -0,0 +1,59 @@ +local TagParserRegistry = {} + +local tagParsers = {} +local blockParsers = {} + +--- Register a new tag parser (for individual tags like @param, @return) +function TagParserRegistry.registerTag(tagName, parser) + tagParsers[tagName] = parser +end + +--- Register a new block parser (for complete blocks like functions, classes) +function TagParserRegistry.registerBlock(blockType, parser) + blockParsers[blockType] = parser +end + +--- Get a specific tag parser +function TagParserRegistry.getTag(tagName) + return tagParsers[tagName] +end + +--- Get a specific block parser +function TagParserRegistry.getBlock(blockType) + return blockParsers[blockType] +end + +--- Get all registered tag parsers +function TagParserRegistry.getAllTags() + return tagParsers +end + +--- Get all registered block parsers +function TagParserRegistry.getAllBlocks() + return blockParsers +end + +--- Auto-load all parsers +local function loadAllParsers() + -- Load tag parsers (individual tags) + local tagParsersList = {"param", "return", "property", "private", "protected", "shortDescription"} + for _, parserName in ipairs(tagParsersList) do + local success, parser = pcall(require, "parser.tag_parsers." .. parserName) + if success then + TagParserRegistry.registerTag(parserName, parser) + end + end + + -- Load block parsers (complete documentation blocks) + local blockParsersList = {"function", "class", "property_definition"} + for _, parserName in ipairs(blockParsersList) do + local success, parser = pcall(require, "parser.block_parsers." .. parserName) + if success then + TagParserRegistry.registerBlock(parserName, parser) + end + end +end + +loadAllParsers() + +return TagParserRegistry diff --git a/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parsers/property.lua b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parsers/property.lua new file mode 100644 index 0000000..1714195 --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parsers/property.lua @@ -0,0 +1,21 @@ +local PropertyParser = {} + +--- Parse @property tag +--- Example: ---@property text string Button Button text +function PropertyParser.parse(line) + local pattern = "^%-*%s*@property%s+(%S+)%s+(%S+)%s+(.*)$" + local name, dataType, description = line:match(pattern) + + if name and dataType then + return { + type = "property", + name = name, + dataType = dataType, + description = description or "" + } + end + + return nil +end + +return PropertyParser diff --git a/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parsers/shortDescription.lua b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parsers/shortDescription.lua new file mode 100644 index 0000000..e5319fd --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/src/parser/tag_parsers/shortDescription.lua @@ -0,0 +1,19 @@ +local ShortDescriptionParser = {} + +--- Parse @shortDescription tag +--- Example: --- @shortDescription Creates a new Button instance +function ShortDescriptionParser.parse(line) + local pattern = "^%-*%s*@shortDescription%s+(.*)$" + local description = line:match(pattern) + + if description then + return { + type = "shortDescription", + description = description + } + end + + return nil +end + +return ShortDescriptionParser diff --git a/tools/BasaltDoc/ldoc-markdown-parser/src/utils/file_reader.lua b/tools/BasaltDoc/ldoc-markdown-parser/src/utils/file_reader.lua new file mode 100644 index 0000000..24dd8a0 --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/src/utils/file_reader.lua @@ -0,0 +1,35 @@ +local fileReader = {} + +--- Read file content +--- @param filePath string Path to the file +--- @return string|nil content File content or nil if error +--- @return string|nil error Error message if any +function fileReader.readFile(filePath) + local file = io.open(filePath, "r") + if not file then + return nil, "Could not open file: " .. filePath + end + + local content = file:read("*all") + file:close() + + return content, nil +end + +--- Write content to file +--- @param filePath string Path to the file +--- @param content string Content to write +--- @return string|nil error Error message if any +function fileReader.writeFile(filePath, content) + local file = io.open(filePath, "w") + if not file then + return "Could not open file for writing: " .. filePath + end + + file:write(content) + file:close() + + return nil +end + +return fileReader \ No newline at end of file diff --git a/tools/BasaltDoc/ldoc-markdown-parser/src/utils/init.lua b/tools/BasaltDoc/ldoc-markdown-parser/src/utils/init.lua new file mode 100644 index 0000000..a731777 --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/src/utils/init.lua @@ -0,0 +1,16 @@ +local string_utils = require("utils.string_utils") + +local utils = {} + +-- Re-export string utilities +utils.trim = string_utils.trim +utils.is_empty = string_utils.is_empty +utils.starts_with = string_utils.starts_with +utils.ends_with = string_utils.ends_with +utils.split = string_utils.split + +function utils.isClassMethod(name) + return name:sub(1, 1):upper() == name:sub(1, 1) +end + +return utils \ No newline at end of file diff --git a/tools/BasaltDoc/ldoc-markdown-parser/src/utils/string_utils.lua b/tools/BasaltDoc/ldoc-markdown-parser/src/utils/string_utils.lua new file mode 100644 index 0000000..d3d010a --- /dev/null +++ b/tools/BasaltDoc/ldoc-markdown-parser/src/utils/string_utils.lua @@ -0,0 +1,27 @@ +local string_utils = {} + +function string_utils.trim(s) + return s:match("^%s*(.-)%s*$") +end + +function string_utils.is_empty(s) + return s == nil or s == "" +end + +function string_utils.starts_with(s, prefix) + return s:sub(1, #prefix) == prefix +end + +function string_utils.ends_with(s, suffix) + return s:sub(-#suffix) == suffix +end + +function string_utils.split(s, delimiter) + local result = {} + for match in (s..delimiter):gmatch("(.-)"..delimiter) do + table.insert(result, match) + end + return result +end + +return string_utils \ No newline at end of file