- Fixed a container bug

- Added onDone to Program
This commit is contained in:
Robert Jelic
2025-06-20 14:19:05 +02:00
parent ea0f31fe37
commit 8067f23d42
16 changed files with 671 additions and 5 deletions

View File

@@ -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 <repository-url>
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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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