Small Docs test

This commit is contained in:
Robert Jelic
2025-09-13 09:54:09 +02:00
parent 7c5d735a51
commit 72f9ac2a1e
25 changed files with 894 additions and 799 deletions

View File

@@ -1,49 +0,0 @@
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
--- Initializes the Button instance
--- @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

270
tools/BasaltDoc/init.lua Normal file
View File

@@ -0,0 +1,270 @@
local BasaltDoc = {}
local args = {...}
local docsPath = fs.getDir(args[2])
local defaultPath = package.path
local format = "path;/path/?.lua;/path/?/init.lua;"
local main = format:gsub("path", docsPath)
package.path = main.."rom/?;"..defaultPath
local ok1, classParser = pcall(require, "parsers.classParser")
local ok2, functionParser = pcall(require, "parsers.functionParser")
local ok3, propertyParser = pcall(require, "parsers.propertyParser")
local ok4, eventParser = pcall(require, "parsers.eventParser")
local ok6, globalParser = pcall(require, "parsers.globalParser")
local ok5, markdownGenerator = pcall(require, "utils.markdownGenerator")
BasaltDoc.annotationHandlers = {}
function BasaltDoc.registerAnnotation(tag, handler)
BasaltDoc.annotationHandlers[tag] = handler
end
----------------------------------------------------------------
-- Standard Annotation Handlers
----------------------------------------------------------------
BasaltDoc.registerAnnotation("@param", function(target, args)
if target.type == "function" then
local paramName, optional, paramType, paramDesc = args:match("([%w_]+)(%??)[%s]*([%w_|]+)[%s]*(.*)")
if paramName then
table.insert(target.params, {
name = paramName,
type = paramType or "any",
description = paramDesc or "",
optional = optional == "?"
})
end
end
end)
BasaltDoc.registerAnnotation("@return", function(target, args)
if target.type == "function" then
local returnType, returnName, returnDesc = args:match("([%w_|]+)[%s]+([%w_]+)[%s]+(.*)")
if returnType then
table.insert(target.returns, {
type = returnType,
name = returnName or "",
description = returnDesc or ""
})
else
table.insert(target.returns, {
type = args:match("([%w_|]+)") or "any",
name = "",
description = args:match("[%w_|]+[%s]+(.*)" ) or ""
})
end
end
end)
BasaltDoc.registerAnnotation("@returns", function(target, args)
BasaltDoc.annotationHandlers["@return"](target, args)
end)
BasaltDoc.registerAnnotation("@usage", function(target, args)
if not target.usage then target.usage = {} end
table.insert(target.usage, args)
end)
BasaltDoc.registerAnnotation("@example", function(target, args)
if not target.examples then target.examples = {} end
table.insert(target.examples, args)
end)
BasaltDoc.registerAnnotation("@see", function(target, args)
if not target.see then target.see = {} end
table.insert(target.see, args)
end)
BasaltDoc.registerAnnotation("@since", function(target, args)
target.since = args
end)
BasaltDoc.registerAnnotation("@deprecated", function(target, args)
target.deprecated = args ~= "" and args or true
end)
BasaltDoc.registerAnnotation("@private", function(target, args)
target.visibility = "private"
end)
BasaltDoc.registerAnnotation("@protected", function(target, args)
target.visibility = "protected"
end)
BasaltDoc.registerAnnotation("@configDescription", function(target, args)
if target.type == "class" then
target.configDescription = args
end
end)
BasaltDoc.registerAnnotation("@shortDescription", function(target, args)
if target.type == "function" then
target.shortDescription = args
end
end)
BasaltDoc.registerAnnotation("@run", function(target, args)
if not target.run then target.run = {} end
table.insert(target.run, args)
end)
BasaltDoc.registerAnnotation("@title", function(target, args)
target.title = args
end)
BasaltDoc.registerAnnotation("@skipFunctionList", function(target, args)
target.skipFunctionList = true
end)
BasaltDoc.registerAnnotation("@skipPropertyList", function(target, args)
target.skipPropertyList = true
end)
BasaltDoc.registerAnnotation("@skipDetailedFunctionList", function(target, args)
target.skipDetailedFunctionList = true
end)
BasaltDoc.registerAnnotation("@skip", function(target, args)
target.skip = true
end)
BasaltDoc.registerAnnotation("@globalDescription", function(target, args)
if args and args ~= "" then
target.description = args
end
end)
if ok1 then classParser.setHandlers(BasaltDoc.annotationHandlers) end
if ok2 then functionParser.setHandlers(BasaltDoc.annotationHandlers) end
if ok3 then propertyParser.setHandlers(BasaltDoc.annotationHandlers) end
if ok4 then eventParser.setHandlers(BasaltDoc.annotationHandlers) end
if ok6 then globalParser.setHandlers(BasaltDoc.annotationHandlers) end
----------------------------------------------------------------
-- Main Parser
----------------------------------------------------------------
function BasaltDoc.parse(content)
local ast = { classes = {}, global = nil }
local rawLines = {}
for line in content:gmatch("([^\r\n]*)\r?\n?") do
table.insert(rawLines, line)
end
local lines = {}
local i = 1
while i <= #rawLines do
local line = rawLines[i]
if line:match("@globalDescription") then
local globalAnnotations = {line}
i = i + 1
if i <= #rawLines and rawLines[i]:match("%-%-?%[%[") then
i = i + 1
while i <= #rawLines and not rawLines[i]:match("%]%]") do
table.insert(globalAnnotations, "--- " .. rawLines[i])
i = i + 1
end
if i <= #rawLines and rawLines[i]:match("%]%]") then
i = i + 1
end
if #globalAnnotations > 0 and ok6 then
local global = globalParser.parse(globalAnnotations, table.concat(globalAnnotations, "\n"))
ast.global = global
end
end
else
table.insert(lines, line)
i = i + 1
end
end
local annotationBuffer = {}
local currentClass = nil
local firstTag = nil
local blockStartTags = {
["@class"] = true,
["@property"] = true,
["@event"] = true,
["@skip"] = true
}
local i = 1
while i <= #lines do
local line = lines[i]
if line:match("^%-%-%-?") then
table.insert(annotationBuffer, line)
if not firstTag then
local tag = line:match("@%S+")
if tag and blockStartTags[tag] then
firstTag = tag
end
end
i = i + 1
elseif #annotationBuffer > 0 then
local nextLine = lines[i]
local skip = false
if nextLine and nextLine:match("^function") and currentClass and ok2 then
local fn = functionParser.parse(annotationBuffer, nextLine)
if fn then
table.insert(currentClass.functions, fn)
end
skip = true
elseif firstTag then
if firstTag == "@class" and ok1 then
local class = classParser.parse(annotationBuffer, table.concat(annotationBuffer, "\n"))
if class and not class.skip then
table.insert(ast.classes, class)
currentClass = class
end
elseif firstTag == "@property" and currentClass and ok3 then
local prop = propertyParser.parse(annotationBuffer, table.concat(annotationBuffer, "\n"))
if prop then
table.insert(currentClass.properties, prop)
end
elseif firstTag == "@event" and currentClass and ok4 then
local evt = eventParser.parse(annotationBuffer, table.concat(annotationBuffer, "\n"))
if evt then
table.insert(currentClass.events, evt)
end
end
end
if skip then i = i + 1 end
annotationBuffer = {}
firstTag = nil
i = i + 1
else
i = i + 1
end
end
if #annotationBuffer > 0 and firstTag then
if firstTag == "@class" and ok1 then
local class = classParser.parse(annotationBuffer, table.concat(annotationBuffer, "\n"))
if class and not class.skip then
table.insert(ast.classes, class)
currentClass = class
end
end
end
return ast
end
function BasaltDoc.generateMarkdown(ast)
if ok5 then
local result = markdownGenerator.generate(ast)
return result
else
return {}
end
end
package.path = defaultPath
return BasaltDoc

View File

@@ -1,36 +0,0 @@
# 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

@@ -1,64 +0,0 @@
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

@@ -1,81 +0,0 @@
local CommentExtractor = {}
--- Extracts comment blocks that belong together
function CommentExtractor.extractBlocks(lines)
local blocks = {}
local currentBlock = {
comments = {},
codeContext = nil
}
local i = 1
while i <= #lines do
local line = lines[i]
local trimmed = line:match("^%s*(.-)%s*$")
-- Check if this is a comment line
if trimmed:match("^%-%-%-") or trimmed:match("^%-%-") then
table.insert(currentBlock.comments, trimmed)
elseif #currentBlock.comments > 0 then
-- We have comments, now look for the code that follows
local codeLineIndex = CommentExtractor.findNextCodeLine(lines, i)
if codeLineIndex then
local codeLine = lines[codeLineIndex]:match("^%s*(.-)%s*$")
currentBlock.codeContext = CommentExtractor.analyzeCode(codeLine, codeLineIndex)
end
-- Save this block and start a new one
table.insert(blocks, currentBlock)
currentBlock = {comments = {}, codeContext = nil}
end
i = i + 1
end
-- Handle remaining comments
if #currentBlock.comments > 0 then
table.insert(blocks, currentBlock)
end
return blocks
end
--- Find the next non-empty code line
function CommentExtractor.findNextCodeLine(lines, startIndex)
for i = startIndex, #lines do
local trimmed = lines[i]:match("^%s*(.-)%s*$")
if trimmed ~= "" and not trimmed:match("^%-%-") then
return i
end
end
return nil
end
--- Analyze what kind of code this is
function CommentExtractor.analyzeCode(codeLine, lineNumber)
-- Function patterns
if codeLine:match("^function%s+([%w%.%:]+)") then
local name = codeLine:match("^function%s+([%w%.%:]+)")
return {type = "function", name = name, line = codeLine, lineNumber = lineNumber}
end
if codeLine:match("^local%s+function%s+([%w%.%:]+)") then
local name = codeLine:match("^local%s+function%s+([%w%.%:]+)")
return {type = "function", name = name, line = codeLine, lineNumber = lineNumber}
end
-- Class/variable patterns
if codeLine:match("^local%s+([%w_]+)%s*=%s*setmetatable") then
local name = codeLine:match("^local%s+([%w_]+)%s*=")
return {type = "class", name = name, line = codeLine, lineNumber = lineNumber}
end
if codeLine:match("^local%s+([%w_]+)%s*=") then
local name = codeLine:match("^local%s+([%w_]+)%s*=")
return {type = "variable", name = name, line = codeLine, lineNumber = lineNumber}
end
return {type = "unknown", name = "unknown", line = codeLine, lineNumber = lineNumber}
end
return CommentExtractor

View File

@@ -1,32 +0,0 @@
local CommentExtractor = require("parser.comment_extractor")
local TagParser = require("parser.tag_parser")
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.extractBlocks(lines)
end
--- Generate markdown from comment blocks
function Parser.generateMarkdown(commentBlocks)
local markdown = {}
-- Parse each block and generate markdown
for _, block in ipairs(commentBlocks) do
local tags = TagParser.parseAllTags(block.comments)
local blockMarkdown = MarkdownGenerator.generateBlock(block, tags)
if blockMarkdown and blockMarkdown ~= "" then
table.insert(markdown, blockMarkdown)
end
end
return table.concat(markdown, "\n\n")
end
return Parser

View File

@@ -1,143 +0,0 @@
local MarkdownGenerator = {}
--- Generate markdown for a block
function MarkdownGenerator.generateBlock(block, tags)
if not block.codeContext then
return MarkdownGenerator.generateStandaloneComment(tags)
end
if block.codeContext.type == "function" then
return MarkdownGenerator.generateFunction(block.codeContext, tags)
elseif block.codeContext.type == "class" then
return MarkdownGenerator.generateClass(block.codeContext, tags)
elseif block.codeContext.type == "variable" then
return MarkdownGenerator.generateVariable(block.codeContext, tags)
else
return MarkdownGenerator.generateGeneric(block.codeContext, tags)
end
end
--- Generate markdown for function
function MarkdownGenerator.generateFunction(context, tags)
local md = {}
table.insert(md, string.format("### %s", context.name))
table.insert(md, "")
if tags.shortDescription then
table.insert(md, tags.shortDescription)
table.insert(md, "")
end
if #tags.description > 0 then
table.insert(md, table.concat(tags.description, " "))
table.insert(md, "")
end
if #tags.params > 0 then
table.insert(md, "**Parameters:**")
for _, param in ipairs(tags.params) do
table.insert(md, string.format("- `%s` (%s): %s", param.name, param.dataType, param.description))
end
table.insert(md, "")
end
if #tags.returns > 0 then
table.insert(md, "**Returns:**")
for _, ret in ipairs(tags.returns) do
table.insert(md, string.format("- `%s` (%s): %s", ret.name, ret.dataType, ret.description))
end
table.insert(md, "")
end
if tags.visibility ~= "public" then
table.insert(md, string.format("**Visibility:** %s", tags.visibility))
table.insert(md, "")
end
return table.concat(md, "\n")
end
--- Generate markdown for class
function MarkdownGenerator.generateClass(context, tags)
local md = {}
table.insert(md, string.format("## Class: %s", context.name))
if tags.class and tags.class.parent then
table.insert(md, string.format("*Extends: %s*", tags.class.parent))
end
table.insert(md, "")
if tags.shortDescription then
table.insert(md, tags.shortDescription)
table.insert(md, "")
end
if #tags.description > 0 then
table.insert(md, table.concat(tags.description, " "))
table.insert(md, "")
end
return table.concat(md, "\n")
end
--- Generate markdown for standalone comments or properties
function MarkdownGenerator.generateStandaloneComment(tags)
local md = {}
if #tags.properties > 0 then
for _, prop in ipairs(tags.properties) do
table.insert(md, string.format("**Property:** `%s` (%s) - %s", prop.name, prop.dataType, prop.description))
end
table.insert(md, "")
end
if #tags.description > 0 then
table.insert(md, table.concat(tags.description, " "))
table.insert(md, "")
end
return table.concat(md, "\n")
end
--- Generate markdown for variables
function MarkdownGenerator.generateVariable(context, tags)
local md = {}
table.insert(md, string.format("### %s", context.name))
table.insert(md, "")
if tags.shortDescription then
table.insert(md, tags.shortDescription)
table.insert(md, "")
end
if #tags.description > 0 then
table.insert(md, table.concat(tags.description, " "))
table.insert(md, "")
end
return table.concat(md, "\n")
end
--- Generate markdown for generic code
function MarkdownGenerator.generateGeneric(context, tags)
local md = {}
table.insert(md, string.format("### %s", context.name))
table.insert(md, "")
if tags.shortDescription then
table.insert(md, tags.shortDescription)
table.insert(md, "")
end
if #tags.description > 0 then
table.insert(md, table.concat(tags.description, " "))
table.insert(md, "")
end
return table.concat(md, "\n")
end
return MarkdownGenerator

View File

@@ -1,117 +0,0 @@
local TagParser = {}
--- Parse all tags from a list of comment lines
function TagParser.parseAllTags(commentLines)
local tags = {
shortDescription = nil,
description = {},
params = {},
returns = {},
visibility = "public",
properties = {},
class = nil,
other = {}
}
for _, line in ipairs(commentLines) do
local parsed = TagParser.parseSingleLine(line)
if parsed then
TagParser.addToTags(tags, parsed)
else
-- Regular description line
local desc = line:match("^%-*%s*(.+)$")
if desc and not desc:match("^@") then
table.insert(tags.description, desc)
end
end
end
return tags
end
--- Parse a single comment line for tags
function TagParser.parseSingleLine(line)
-- @shortDescription
local shortDesc = line:match("^%-*%s*@shortDescription%s+(.+)$")
if shortDesc then
return {type = "shortDescription", value = shortDesc}
end
-- @param name type description
local paramName, paramType, paramDesc = line:match("^%-*%s*@param%s+(%S+)%s+(%S+)%s*(.*)$")
if paramName then
return {
type = "param",
name = paramName,
dataType = paramType,
description = paramDesc or ""
}
end
-- @return type name description
local returnType, returnName, returnDesc = line:match("^%-*%s*@return%s+(%S+)%s+(%S+)%s*(.*)$")
if returnType then
return {
type = "return",
dataType = returnType,
name = returnName or "",
description = returnDesc or ""
}
end
-- @property name type description
local propName, propType, propDesc = line:match("^%-*%s*@property%s+(%S+)%s+(%S+)%s*(.*)$")
if propName then
return {
type = "property",
name = propName,
dataType = propType,
description = propDesc or ""
}
end
-- @class name : parent
local className, parentClass = line:match("^%-*%s*@class%s+(%S+)%s*:%s*(%S+)")
if not className then
className = line:match("^%-*%s*@class%s+(%S+)")
end
if className then
return {
type = "class",
name = className,
parent = parentClass or nil
}
end
-- Visibility tags
if line:match("^%-*%s*@private%s*$") then
return {type = "visibility", value = "private"}
end
if line:match("^%-*%s*@protected%s*$") then
return {type = "visibility", value = "protected"}
end
return nil
end
--- Add parsed tag to the tags collection
function TagParser.addToTags(tags, parsed)
if parsed.type == "shortDescription" then
tags.shortDescription = parsed.value
elseif parsed.type == "param" then
table.insert(tags.params, parsed)
elseif parsed.type == "return" then
table.insert(tags.returns, parsed)
elseif parsed.type == "property" then
table.insert(tags.properties, parsed)
elseif parsed.type == "class" then
tags.class = parsed
elseif parsed.type == "visibility" then
tags.visibility = parsed.value
else
table.insert(tags.other, parsed)
end
end
return TagParser

View File

@@ -1,59 +0,0 @@
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

@@ -1,21 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,35 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,27 +0,0 @@
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

View File

@@ -0,0 +1,34 @@
local helper = require("utils.helper")
local logger = require("utils.logger")
local classParser = {}
function classParser.parse(annotations, line)
local classLine = helper.findAnnotationLine(annotations, "class")
if not classLine then
return nil
end
local name, extends = classLine:match("^%-%-%-?%s*@class%s*([%w_%.]+)%s*:?%s*([%w_%.]*)")
local class = {
type = "class",
name = name,
extends = extends ~= "" and extends or nil,
description = nil,
properties = {},
events = {},
functions = {},
skip = false
}
if classParser.handlers then
helper.applyAnnotations(annotations, class, classParser.handlers)
end
return class
end
function classParser.setHandlers(handlers)
classParser.handlers = handlers
end
return classParser

View File

@@ -0,0 +1,37 @@
local helper = require("utils.helper")
local eventParser = {}
function eventParser.parse(annotations, line)
local eventLine = helper.findAnnotationLine(annotations, "event")
if not eventLine then return nil end
local content = table.concat(annotations, " ")
local name, params, desc = eventLine:match("^%-%-%-?%s*@event%s*([%w_]+)%s*({[^}]*})%s*(.*)")
if not name then
name, desc = eventLine:match("^%-%-%-?%s*@event%s*([%w_]+)%s+(.*)")
params = "{}"
end
if not name then
print("Warning: Could not parse @event annotation: " .. eventLine)
return nil
end
local evt = {
type = "event",
name = name,
params = params or "{}",
description = desc or ""
}
helper.applyAnnotations(annotations, evt, eventParser.handlers)
return evt
end
function eventParser.setHandlers(handlers)
eventParser.handlers = handlers
end
return eventParser

View File

@@ -0,0 +1,48 @@
local helper = require("utils.helper")
local functionParser = {}
function functionParser.parse(annotations, line)
local name = line:match("^function%s+([%w_%.]+[:.]?[%w_]+)") or line:match("^function%s+([%w_]+)")
if not name then
print("Warning: Could not extract function name from line: " .. line)
return nil
end
local f = {
type = "function",
name = name,
description = nil,
shortDescription = nil,
params = {},
returns = {},
visibility = "public"
}
if functionParser.handlers then
helper.applyAnnotations(annotations, f, functionParser.handlers)
end
local funcName = line:match("function ([%w_%.]+)")
if funcName then
if funcName:find(":") then
f.name = funcName:match(":([%w_]+)")
elseif funcName:find("%.") then
f.name = funcName:match("%.([%w_]+)")
else
f.name = funcName
end
end
if line:match("function [%w_%.]+:") then
f.static = false
else
f.static = true
end
return f
end
function functionParser.setHandlers(handlers)
functionParser.handlers = handlers
end
return functionParser

View File

@@ -0,0 +1,21 @@
local helper = require("utils.helper")
local logger = require("utils.logger")
local globalParser = {}
function globalParser.parse(annotations, line)
local global = {
description = nil
}
if globalParser.handlers then
helper.applyAnnotations(annotations, global, globalParser.handlers)
end
return global
end
function globalParser.setHandlers(handlers)
globalParser.handlers = handlers
end
return globalParser

View File

@@ -0,0 +1,30 @@
local helper = require("utils.helper")
local propertyParser = {}
function propertyParser.parse(annotations, line)
local propLine = helper.findAnnotationLine(annotations, "property")
if not propLine then return nil end
local content = table.concat(annotations, " ")
local name, type, default, desc = propLine:match("^%-%-%-?%s*@property%s*([%w_]+)%s+([%w_|%[%]]+)%s+([^%s]+)%s*(.*)")
if not name then
print("Warning: Could not parse @property annotation: " .. propLine)
return nil
end
local prop = {
type = "property",
name = name,
propType = type or "any",
default = default or "nil",
description = desc or ""
}
helper.applyAnnotations(annotations, prop, propertyParser.handlers)
return prop
end
function propertyParser.setHandlers(handlers)
propertyParser.handlers = handlers
end
return propertyParser

View File

@@ -0,0 +1,59 @@
local helper = {}
function helper.applyAnnotations(annotations, target, handlers)
local i = 1
while i <= #annotations do
local ann = annotations[i]
local tag, args = ann:match("^%-%-%-?%s*(@%S+)%s*(.*)")
if tag then
if args == ">" then
local multiArgs = ""
i = i + 1
while i <= #annotations do
local nextAnn = annotations[i]
local nextTag = nextAnn:match("^%-%-%-?%s*(@%S+)")
if nextTag then
i = i - 1
break
else
local content = nextAnn:match("^%-%-%-?%s*(.*)") or nextAnn
if multiArgs ~= "" then
multiArgs = multiArgs .. "\n" .. content
else
multiArgs = content
end
end
i = i + 1
end
args = multiArgs
end
if handlers and handlers[tag] then
handlers[tag](target, args)
end
else
local comment = ann:match("^%-%-%-?%s*(.*)") or ann
if comment and not comment:match("^@%S+") then
if target.description then
target.description = target.description .. "\n" .. comment
else
target.description = comment
end
end
end
i = i + 1
end
end
function helper.findAnnotationLine(annotations, tag)
for _, l in ipairs(annotations) do
if l:match("@" .. tag) then
return l
end
end
return nil
end
return helper

View File

@@ -0,0 +1,26 @@
local logger = {}
local logFile
function logger.init()
if not logFile then
logFile = io.open("BasaltDoc/debug_log.txt", "w")
end
end
function logger.log(message)
if not logFile then logger.init() end
if logFile then
logFile:write(os.date("%Y-%m-%d %H:%M:%S") .. ": " .. message .. "\n")
logFile:flush()
end
end
function logger.close()
if logFile then
logFile:close()
logFile = nil
end
end
return logger

View File

@@ -0,0 +1,222 @@
local markdownGenerator = {}
local function generateFunctionMarkdown(class, functions)
local md = {}
for _, f in ipairs(functions) do
local sig = "## "
if class.name then
sig = sig .. class.name .. (f.static and "." or ":")
end
sig = sig .. (f.name or "unknown") .. "("
for i, p in ipairs(f.params) do
sig = sig .. p.name
if p.optional then sig = sig .. "?" end
if i < #f.params then sig = sig .. ", " end
end
sig = sig .. ")"
table.insert(md, sig)
if f.description and f.description ~= "" then
table.insert(md, "")
table.insert(md, f.description)
table.insert(md, "")
end
if #f.params > 0 then
table.insert(md, "### Parameters")
for _, p in ipairs(f.params) do
local paramLine = "* `" .. p.name .. "`"
if p.optional then paramLine = paramLine .. " *(optional)*" end
paramLine = paramLine .. " `" .. p.type .. "`"
if p.description and p.description ~= "" then
paramLine = paramLine .. " " .. p.description
end
table.insert(md, paramLine)
end
table.insert(md, "")
end
if #f.returns > 0 then
table.insert(md, "### Returns")
for _, r in ipairs(f.returns) do
local returnLine = "* `" .. r.type .. "`"
if r.name and r.name ~= "" then
returnLine = returnLine .. " `" .. r.name .. "`"
end
if r.description and r.description ~= "" then
returnLine = returnLine .. " " .. r.description
end
table.insert(md, returnLine)
end
table.insert(md, "")
end
if f.usage then
table.insert(md, "### Usage")
table.insert(md, "```lua")
for _, usage in ipairs(f.usage) do
if usage == "" then
table.insert(md, "")
else
table.insert(md, usage)
end
end
table.insert(md, "```")
table.insert(md, "")
end
if f.run then
table.insert(md, "### Usage (Executable)")
table.insert(md, "```lua run")
for _, run in ipairs(f.run) do
if run == "" then
table.insert(md, "")
else
table.insert(md, run)
end
end
table.insert(md, "```")
table.insert(md, "")
end
if f.examples then
table.insert(md, "### Examples")
for _, example in ipairs(f.examples) do
table.insert(md, "```lua")
table.insert(md, example)
table.insert(md, "```")
end
table.insert(md, "")
end
if f.see then
table.insert(md, "### See Also")
for _, seeRef in ipairs(f.see) do
table.insert(md, "* " .. seeRef)
end
table.insert(md, "")
end
if f.deprecated then
table.insert(md, "> **⚠️ Deprecated** " .. (type(f.deprecated) == "string" and f.deprecated or "This function is deprecated"))
table.insert(md, "")
end
if f.since then
table.insert(md, "*Since: " .. f.since .. "*")
table.insert(md, "")
end
end
return md
end
function markdownGenerator.generate(ast)
local md = {}
if ast.global then
if ast.global.description then
table.insert(md, ast.global.description)
end
end
local seen = {}
for _, class in ipairs(ast.classes) do
if not class.skip and not seen[class.name] then
seen[class.name] = true
local title = class.title or class.name
table.insert(md, "# " .. title)
if class.description then
table.insert(md, "_" .. class.description .. "_")
end
if class.extends then
table.insert(md, "")
table.insert(md, "Extends: `" .. class.extends .. "`")
end
table.insert(md, "")
if not class.skipPropertyList and #class.properties > 0 then
table.insert(md, "## Properties")
table.insert(md, "")
table.insert(md, "|Property|Type|Default|Description|")
table.insert(md, "|---|---|---|---|")
for _, p in ipairs(class.properties) do
table.insert(md, string.format("|%s|%s|%s|%s|",
p.name or "",
p.propType or "any",
p.default or "nil",
p.description or ""))
end
table.insert(md, "")
end
if #class.events > 0 then
table.insert(md, "## Events")
table.insert(md, "")
table.insert(md, "|Event|Parameters|Description|")
table.insert(md, "|---|---|---|")
for _, e in ipairs(class.events) do
local params = e.params or ""
if params:match("^{.*}$") then
params = params:sub(2, -2)
end
if params == "" then params = "-" else params = "`" .. params .. "`" end
table.insert(md, string.format("|%s|%s|%s|",
e.name or "", params, e.description or ""))
end
table.insert(md, "")
end
if not class.skipFunctionList and #class.functions > 0 then
table.insert(md, "## Functions")
table.insert(md, "")
table.insert(md, "|Method|Returns|Description|")
table.insert(md, "|---|---|---|")
for _, f in ipairs(class.functions) do
local methodName = (class.name or "") .. (f.static and "." or ":") .. (f.name or "")
local anchor = methodName:lower()
if #f.params > 0 then
for _, p in ipairs(f.params) do
anchor = anchor .. "-" .. p.name:lower()
end
end
anchor = anchor:gsub("[^%w%-]", "-")
local returnType = "-"
if #f.returns > 0 then
local types = {}
for _, r in ipairs(f.returns) do
table.insert(types, r.type)
end
returnType = table.concat(types, ", ")
end
local shortDesc = f.shortDescription or f.description or ""
table.insert(md, string.format("|[%s](#%s)|%s|%s|",
methodName,
anchor,
returnType,
shortDesc))
end
table.insert(md, "")
if not class.skipDetailedFunctionList then
local functionMd = generateFunctionMarkdown(class, class.functions)
for _, line in ipairs(functionMd) do
table.insert(md, line)
end
end
end
end
end
return md
end
return markdownGenerator

View File

@@ -1,50 +1,99 @@
local arg = arg or {...}
local oldPath = package.path
local SRC_DIR = arg[1] or 'src'
local OUT_DIR = arg[2] or 'docs'
local scriptSource = debug.getinfo(1, "S").source
local scriptDir = nil
if scriptSource and scriptSource:sub(1,1) == "@" then
local scriptPath = scriptSource:sub(2)
scriptDir = scriptPath:match("^(.*)[/\\]") or "."
local BasaltDoc = require('BasaltDoc')
local fileSystem
if fs then
fileSystem = {
list = fs.list,
combine = fs.combine,
isDir = fs.isDir,
exists = fs.exists,
makeDir = fs.makeDir,
open = function(path, mode) return fs.open(path, mode) end,
getDir = fs.getDir,
readAll = function(file) return file.readAll() end,
write = function(file, data) file.write(data) end,
close = function(file) file.close() end
}
else
scriptDir = "."
local lfs = require("lfs")
fileSystem = {
list = function(dir)
local items = {}
for item in lfs.dir(dir) do
if item ~= "." and item ~= ".." then
table.insert(items, item)
end
end
return items
end,
combine = function(a, b) return a .. "/" .. b end,
isDir = function(path) return lfs.attributes(path).mode == "directory" end,
exists = function(path) return lfs.attributes(path) ~= nil end,
makeDir = lfs.mkdir,
open = io.open,
getDir = function(path) return path:match("(.+)/") end,
readAll = function(file) return file:read("*all") end,
write = function(file, data) file:write(data) end,
close = function(file) file:close() end
}
end
local parserSrc = scriptDir .. "/BasaltDoc/ldoc-markdown-parser/src/"
package.path = package.path .. ";" .. parserSrc .. "?.lua;" .. parserSrc .. "?/init.lua"
local ok, parser = pcall(require, "parser.init")
local ioAdaptor = require("tools.io")
package.path = oldPath
if not fileSystem.exists(OUT_DIR) then
fileSystem.makeDir(OUT_DIR)
end
if not ok or not parser then
-- try dofile fallback
local initPath = parserSrc .. "/parser/init.lua"
local ok2, module = pcall(dofile, initPath)
if ok2 and module then
parser = module
local function getLuaFiles(dir)
local files = {}
local list = fileSystem.list(dir)
for _, item in ipairs(list) do
local path = fileSystem.combine(dir, item)
if fileSystem.isDir(path) then
local subFiles = getLuaFiles(path)
for _, subFile in ipairs(subFiles) do
table.insert(files, subFile)
end
elseif item:match("%.lua$") then
table.insert(files, path)
end
end
return files
end
local luaFiles = getLuaFiles(SRC_DIR)
for _, filePath in ipairs(luaFiles) do
local file = fileSystem.open(filePath, "r")
if file then
local content = fileSystem.readAll(file)
fileSystem.close(file)
local ast = BasaltDoc.parse(content)
local markdown = BasaltDoc.generateMarkdown(ast)
local relativePath = filePath:gsub("^" .. SRC_DIR .. "/", ""):gsub("%.lua$", ".md")
local outPath = fileSystem.combine(OUT_DIR, relativePath)
local outDir = fileSystem.getDir(outPath)
if outDir and not fileSystem.exists(outDir) then
fileSystem.makeDir(outDir)
end
local outFile = fileSystem.open(outPath, "w")
if outFile then
fileSystem.write(outFile, table.concat(markdown, "\n"))
fileSystem.close(outFile)
print("Generated: " .. outPath)
else
print("Error writing: " .. outPath)
end
else
error("Failed to load parser.init via require and dofile (tried: "..tostring(initPath)..")")
print("Error reading: " .. filePath)
end
end
local function processFile(inputFile)
local content = ioAdaptor.readFile(inputFile)
if not content then
io.stderr:write("Failed to read: " .. tostring(inputFile) .. "\n")
return
end
local commentBlocks = parser.extractComments(content)
local md = parser.generateMarkdown(commentBlocks)
local outputFile = "build_docs/docs/references/" .. inputFile:match("^src/(.+)"):gsub("%.lua$", "")
ioAdaptor.ensureDirectory(outputFile)
ioAdaptor.writeFile(outputFile .. ".md", md)
end
local files = ioAdaptor.listFiles("src", "*.lua")
for _, file in ipairs(files) do
if not file:match("LuaLS.lua$") then
processFile(file)
end
end
print("Documentation generation complete.")