510 lines
18 KiB
Python
510 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Dynamic documentation generator for McRogueFace.
|
|
Extracts all documentation directly from the compiled module using introspection.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import inspect
|
|
import datetime
|
|
import html
|
|
import re
|
|
from pathlib import Path
|
|
|
|
# Must be run with McRogueFace as interpreter
|
|
try:
|
|
import mcrfpy
|
|
except ImportError:
|
|
print("Error: This script must be run with McRogueFace as the interpreter")
|
|
print("Usage: ./build/mcrogueface --exec generate_dynamic_docs.py")
|
|
sys.exit(1)
|
|
|
|
def parse_docstring(docstring):
|
|
"""Parse a docstring to extract signature, description, args, and returns."""
|
|
if not docstring:
|
|
return {"signature": "", "description": "", "args": [], "returns": "", "example": ""}
|
|
|
|
lines = docstring.strip().split('\n')
|
|
result = {
|
|
"signature": "",
|
|
"description": "",
|
|
"args": [],
|
|
"returns": "",
|
|
"example": ""
|
|
}
|
|
|
|
# First line often contains the signature
|
|
if lines and '(' in lines[0] and ')' in lines[0]:
|
|
result["signature"] = lines[0].strip()
|
|
lines = lines[1:] if len(lines) > 1 else []
|
|
|
|
# Parse the rest
|
|
current_section = "description"
|
|
description_lines = []
|
|
example_lines = []
|
|
in_example = False
|
|
|
|
for line in lines:
|
|
line_lower = line.strip().lower()
|
|
|
|
if line_lower.startswith("args:") or line_lower.startswith("arguments:"):
|
|
current_section = "args"
|
|
continue
|
|
elif line_lower.startswith("returns:") or line_lower.startswith("return:"):
|
|
current_section = "returns"
|
|
result["returns"] = line[line.find(':')+1:].strip()
|
|
continue
|
|
elif line_lower.startswith("example:") or line_lower.startswith("examples:"):
|
|
in_example = True
|
|
continue
|
|
elif line_lower.startswith("note:"):
|
|
if description_lines:
|
|
description_lines.append("")
|
|
description_lines.append(line)
|
|
continue
|
|
|
|
if in_example:
|
|
example_lines.append(line)
|
|
elif current_section == "description" and not line.startswith(" "):
|
|
description_lines.append(line)
|
|
elif current_section == "args" and line.strip():
|
|
# Parse argument lines like " x: X coordinate"
|
|
match = re.match(r'\s+(\w+):\s*(.+)', line)
|
|
if match:
|
|
result["args"].append({
|
|
"name": match.group(1),
|
|
"description": match.group(2).strip()
|
|
})
|
|
elif current_section == "returns" and line.strip() and line.startswith(" "):
|
|
result["returns"] += " " + line.strip()
|
|
|
|
result["description"] = '\n'.join(description_lines).strip()
|
|
result["example"] = '\n'.join(example_lines).strip()
|
|
|
|
return result
|
|
|
|
def get_all_functions():
|
|
"""Get all module-level functions."""
|
|
functions = {}
|
|
for name in dir(mcrfpy):
|
|
if name.startswith('_'):
|
|
continue
|
|
obj = getattr(mcrfpy, name)
|
|
if inspect.isbuiltin(obj) or inspect.isfunction(obj):
|
|
doc_info = parse_docstring(obj.__doc__)
|
|
functions[name] = {
|
|
"name": name,
|
|
"doc": obj.__doc__ or "",
|
|
"parsed": doc_info
|
|
}
|
|
return functions
|
|
|
|
def get_all_classes():
|
|
"""Get all classes and their methods/properties."""
|
|
classes = {}
|
|
for name in dir(mcrfpy):
|
|
if name.startswith('_'):
|
|
continue
|
|
obj = getattr(mcrfpy, name)
|
|
if inspect.isclass(obj):
|
|
class_info = {
|
|
"name": name,
|
|
"doc": obj.__doc__ or "",
|
|
"methods": {},
|
|
"properties": {},
|
|
"bases": [base.__name__ for base in obj.__bases__ if base.__name__ != 'object']
|
|
}
|
|
|
|
# Get methods and properties
|
|
for attr_name in dir(obj):
|
|
if attr_name.startswith('__') and attr_name != '__init__':
|
|
continue
|
|
|
|
try:
|
|
attr = getattr(obj, attr_name)
|
|
if callable(attr):
|
|
method_doc = attr.__doc__ or ""
|
|
class_info["methods"][attr_name] = {
|
|
"doc": method_doc,
|
|
"parsed": parse_docstring(method_doc)
|
|
}
|
|
elif isinstance(attr, property):
|
|
prop_doc = (attr.fget.__doc__ if attr.fget else "") or ""
|
|
class_info["properties"][attr_name] = {
|
|
"doc": prop_doc,
|
|
"readonly": attr.fset is None
|
|
}
|
|
except:
|
|
pass
|
|
|
|
classes[name] = class_info
|
|
return classes
|
|
|
|
def get_constants():
|
|
"""Get module constants."""
|
|
constants = {}
|
|
for name in dir(mcrfpy):
|
|
if name.startswith('_') or name[0].islower():
|
|
continue
|
|
obj = getattr(mcrfpy, name)
|
|
if not (inspect.isclass(obj) or callable(obj)):
|
|
constants[name] = {
|
|
"name": name,
|
|
"value": repr(obj) if not name.startswith('default_') else f"<{name}>",
|
|
"type": type(obj).__name__
|
|
}
|
|
return constants
|
|
|
|
def generate_html_docs():
|
|
"""Generate HTML documentation."""
|
|
functions = get_all_functions()
|
|
classes = get_all_classes()
|
|
constants = get_constants()
|
|
|
|
html_content = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>McRogueFace API Reference</title>
|
|
<style>
|
|
body {{
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background-color: #f5f5f5;
|
|
}}
|
|
.container {{
|
|
background-color: white;
|
|
padding: 30px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}}
|
|
h1, h2, h3, h4, h5 {{
|
|
color: #2c3e50;
|
|
}}
|
|
.toc {{
|
|
background-color: #f8f9fa;
|
|
padding: 20px;
|
|
border-radius: 4px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
.toc ul {{
|
|
list-style-type: none;
|
|
padding-left: 20px;
|
|
}}
|
|
.toc > ul {{
|
|
padding-left: 0;
|
|
}}
|
|
.toc a {{
|
|
text-decoration: none;
|
|
color: #3498db;
|
|
}}
|
|
.toc a:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
.method-section {{
|
|
margin-bottom: 30px;
|
|
padding: 20px;
|
|
background-color: #f8f9fa;
|
|
border-radius: 4px;
|
|
border-left: 4px solid #3498db;
|
|
}}
|
|
.function-signature {{
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
background-color: #e9ecef;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
margin: 10px 0;
|
|
}}
|
|
.class-name {{
|
|
color: #e74c3c;
|
|
font-weight: bold;
|
|
}}
|
|
.method-name {{
|
|
color: #3498db;
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
}}
|
|
.property-name {{
|
|
color: #27ae60;
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
}}
|
|
.arg-name {{
|
|
color: #8b4513;
|
|
font-weight: bold;
|
|
}}
|
|
.arg-type {{
|
|
color: #666;
|
|
font-style: italic;
|
|
}}
|
|
code {{
|
|
background-color: #f4f4f4;
|
|
padding: 2px 5px;
|
|
border-radius: 3px;
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
}}
|
|
pre {{
|
|
background-color: #f4f4f4;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
overflow-x: auto;
|
|
}}
|
|
.deprecated {{
|
|
text-decoration: line-through;
|
|
opacity: 0.6;
|
|
}}
|
|
.note {{
|
|
background-color: #fff3cd;
|
|
border-left: 4px solid #ffc107;
|
|
padding: 10px;
|
|
margin: 10px 0;
|
|
}}
|
|
.returns {{
|
|
color: #28a745;
|
|
font-weight: bold;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>McRogueFace API Reference</h1>
|
|
<p><em>Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</em></p>
|
|
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
|
|
|
|
<div class="toc">
|
|
<h2>Table of Contents</h2>
|
|
<ul>
|
|
<li><a href="#functions">Functions</a></li>
|
|
<li><a href="#classes">Classes</a>
|
|
<ul>
|
|
"""
|
|
|
|
# Add classes to TOC
|
|
for class_name in sorted(classes.keys()):
|
|
html_content += f' <li><a href="#{class_name}">{class_name}</a></li>\n'
|
|
|
|
html_content += """ </ul>
|
|
</li>
|
|
<li><a href="#constants">Constants</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<h2 id="functions">Functions</h2>
|
|
"""
|
|
|
|
# Generate function documentation
|
|
for func_name in sorted(functions.keys()):
|
|
func_info = functions[func_name]
|
|
parsed = func_info["parsed"]
|
|
|
|
html_content += f"""
|
|
<div class="method-section">
|
|
<h3><code class="function-signature">{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}</code></h3>
|
|
<p>{html.escape(parsed['description'])}</p>
|
|
"""
|
|
|
|
if parsed['args']:
|
|
html_content += " <h4>Arguments:</h4>\n <ul>\n"
|
|
for arg in parsed['args']:
|
|
html_content += f" <li><span class='arg-name'>{arg['name']}</span>: {html.escape(arg['description'])}</li>\n"
|
|
html_content += " </ul>\n"
|
|
|
|
if parsed['returns']:
|
|
html_content += f" <p><span class='returns'>Returns:</span> {html.escape(parsed['returns'])}</p>\n"
|
|
|
|
if parsed['example']:
|
|
html_content += f" <h4>Example:</h4>\n <pre><code>{html.escape(parsed['example'])}</code></pre>\n"
|
|
|
|
html_content += " </div>\n"
|
|
|
|
# Generate class documentation
|
|
html_content += "\n <h2 id='classes'>Classes</h2>\n"
|
|
|
|
for class_name in sorted(classes.keys()):
|
|
class_info = classes[class_name]
|
|
|
|
html_content += f"""
|
|
<div class="method-section">
|
|
<h3 id="{class_name}"><span class="class-name">{class_name}</span></h3>
|
|
"""
|
|
|
|
if class_info['bases']:
|
|
html_content += f" <p><em>Inherits from: {', '.join(class_info['bases'])}</em></p>\n"
|
|
|
|
if class_info['doc']:
|
|
html_content += f" <p>{html.escape(class_info['doc'])}</p>\n"
|
|
|
|
# Properties
|
|
if class_info['properties']:
|
|
html_content += " <h4>Properties:</h4>\n <ul>\n"
|
|
for prop_name, prop_info in sorted(class_info['properties'].items()):
|
|
readonly = " (read-only)" if prop_info['readonly'] else ""
|
|
html_content += f" <li><span class='property-name'>{prop_name}</span>{readonly}"
|
|
if prop_info['doc']:
|
|
html_content += f": {html.escape(prop_info['doc'])}"
|
|
html_content += "</li>\n"
|
|
html_content += " </ul>\n"
|
|
|
|
# Methods
|
|
if class_info['methods']:
|
|
html_content += " <h4>Methods:</h4>\n"
|
|
for method_name, method_info in sorted(class_info['methods'].items()):
|
|
if method_name == '__init__':
|
|
continue
|
|
parsed = method_info['parsed']
|
|
|
|
html_content += f"""
|
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
|
<h5><code class="method-name">{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}</code></h5>
|
|
"""
|
|
|
|
if parsed['description']:
|
|
html_content += f" <p>{html.escape(parsed['description'])}</p>\n"
|
|
|
|
if parsed['args']:
|
|
html_content += " <div style='margin-left: 20px;'>\n"
|
|
for arg in parsed['args']:
|
|
html_content += f" <div><span class='arg-name'>{arg['name']}</span>: {html.escape(arg['description'])}</div>\n"
|
|
html_content += " </div>\n"
|
|
|
|
if parsed['returns']:
|
|
html_content += f" <p style='margin-left: 20px;'><span class='returns'>Returns:</span> {html.escape(parsed['returns'])}</p>\n"
|
|
|
|
html_content += " </div>\n"
|
|
|
|
html_content += " </div>\n"
|
|
|
|
# Constants
|
|
html_content += "\n <h2 id='constants'>Constants</h2>\n <ul>\n"
|
|
for const_name, const_info in sorted(constants.items()):
|
|
html_content += f" <li><code>{const_name}</code> ({const_info['type']}): {const_info['value']}</li>\n"
|
|
html_content += " </ul>\n"
|
|
|
|
html_content += """
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# Write the file
|
|
output_path = Path("docs/api_reference_dynamic.html")
|
|
output_path.parent.mkdir(exist_ok=True)
|
|
output_path.write_text(html_content)
|
|
print(f"Generated {output_path}")
|
|
print(f"Found {len(functions)} functions, {len(classes)} classes, {len(constants)} constants")
|
|
|
|
def generate_markdown_docs():
|
|
"""Generate Markdown documentation."""
|
|
functions = get_all_functions()
|
|
classes = get_all_classes()
|
|
constants = get_constants()
|
|
|
|
md_content = f"""# McRogueFace API Reference
|
|
|
|
*Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
|
|
|
|
*This documentation was dynamically generated from the compiled module.*
|
|
|
|
## Table of Contents
|
|
|
|
- [Functions](#functions)
|
|
- [Classes](#classes)
|
|
"""
|
|
|
|
# Add classes to TOC
|
|
for class_name in sorted(classes.keys()):
|
|
md_content += f" - [{class_name}](#{class_name.lower()})\n"
|
|
|
|
md_content += "- [Constants](#constants)\n\n"
|
|
|
|
# Functions
|
|
md_content += "## Functions\n\n"
|
|
|
|
for func_name in sorted(functions.keys()):
|
|
func_info = functions[func_name]
|
|
parsed = func_info["parsed"]
|
|
|
|
md_content += f"### `{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n"
|
|
|
|
if parsed['description']:
|
|
md_content += f"{parsed['description']}\n\n"
|
|
|
|
if parsed['args']:
|
|
md_content += "**Arguments:**\n"
|
|
for arg in parsed['args']:
|
|
md_content += f"- `{arg['name']}`: {arg['description']}\n"
|
|
md_content += "\n"
|
|
|
|
if parsed['returns']:
|
|
md_content += f"**Returns:** {parsed['returns']}\n\n"
|
|
|
|
if parsed['example']:
|
|
md_content += f"**Example:**\n```python\n{parsed['example']}\n```\n\n"
|
|
|
|
# Classes
|
|
md_content += "## Classes\n\n"
|
|
|
|
for class_name in sorted(classes.keys()):
|
|
class_info = classes[class_name]
|
|
|
|
md_content += f"### {class_name}\n\n"
|
|
|
|
if class_info['bases']:
|
|
md_content += f"*Inherits from: {', '.join(class_info['bases'])}*\n\n"
|
|
|
|
if class_info['doc']:
|
|
md_content += f"{class_info['doc']}\n\n"
|
|
|
|
# Properties
|
|
if class_info['properties']:
|
|
md_content += "**Properties:**\n"
|
|
for prop_name, prop_info in sorted(class_info['properties'].items()):
|
|
readonly = " *(read-only)*" if prop_info['readonly'] else ""
|
|
md_content += f"- `{prop_name}`{readonly}"
|
|
if prop_info['doc']:
|
|
md_content += f": {prop_info['doc']}"
|
|
md_content += "\n"
|
|
md_content += "\n"
|
|
|
|
# Methods
|
|
if class_info['methods']:
|
|
md_content += "**Methods:**\n\n"
|
|
for method_name, method_info in sorted(class_info['methods'].items()):
|
|
if method_name == '__init__':
|
|
continue
|
|
parsed = method_info['parsed']
|
|
|
|
md_content += f"#### `{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n"
|
|
|
|
if parsed['description']:
|
|
md_content += f"{parsed['description']}\n\n"
|
|
|
|
if parsed['args']:
|
|
md_content += "**Arguments:**\n"
|
|
for arg in parsed['args']:
|
|
md_content += f"- `{arg['name']}`: {arg['description']}\n"
|
|
md_content += "\n"
|
|
|
|
if parsed['returns']:
|
|
md_content += f"**Returns:** {parsed['returns']}\n\n"
|
|
|
|
# Constants
|
|
md_content += "## Constants\n\n"
|
|
for const_name, const_info in sorted(constants.items()):
|
|
md_content += f"- `{const_name}` ({const_info['type']}): {const_info['value']}\n"
|
|
|
|
# Write the file
|
|
output_path = Path("docs/API_REFERENCE_DYNAMIC.md")
|
|
output_path.parent.mkdir(exist_ok=True)
|
|
output_path.write_text(md_content)
|
|
print(f"Generated {output_path}")
|
|
|
|
if __name__ == "__main__":
|
|
print("Generating dynamic documentation from mcrfpy module...")
|
|
generate_html_docs()
|
|
generate_markdown_docs()
|
|
print("Documentation generation complete!") |