docs: Complete Phase 7 documentation system with parser fixes and man pages
Fixed critical documentation generation bugs and added complete multi-format output support. All documentation now generates cleanly from MCRF_* macros. ## Parser Fixes (tools/generate_dynamic_docs.py) Fixed parse_docstring() function: - Added "Raises:" section support (was missing entirely) - Fixed function name duplication in headings - Was: `createSoundBuffercreateSoundBuffer(...)` - Now: `createSoundBuffer(filename: str) -> int` - Proper section separation between Returns and Raises - Handles MCRF_* macro format correctly Changes: - Rewrote parse_docstring() to parse by section markers - Fixed markdown generation (lines 514-539) - Fixed HTML generation (lines 385-413, 446-473) - Added "raises" field to parsed output dict ## Man Page Generation New files: - tools/generate_man_page.sh - Pandoc wrapper for man page generation - docs/mcrfpy.3 - Unix man page (section 3 for library functions) Uses pandoc with metadata: - Section 3 (library functions) - Git version tag in footer - Current date in header ## Master Orchestration Script New file: tools/generate_all_docs.sh Single command generates all documentation formats: - HTML API reference (docs/api_reference_dynamic.html) - Markdown API reference (docs/API_REFERENCE_DYNAMIC.md) - Unix man page (docs/mcrfpy.3) - Type stubs (stubs/mcrfpy.pyi via generate_stubs_v2.py) Includes error checking (set -e) and helpful output messages. ## Documentation Updates (CLAUDE.md) Updated "Regenerating Documentation" section: - Documents new ./tools/generate_all_docs.sh master script - Lists all output files with descriptions - Notes pandoc as system requirement - Clarifies generate_stubs_v2.py is preferred (has @overload support) ## Type Stub Decision Assessed generate_stubs.py vs generate_stubs_v2.py: - generate_stubs.py has critical bugs (missing commas in method signatures) - generate_stubs_v2.py produces high-quality manually-maintained stubs - Decision: Keep v2, use it in master script ## Files Modified Modified: - CLAUDE.md (25 lines changed) - tools/generate_dynamic_docs.py (121 lines changed) - docs/api_reference_dynamic.html (359 lines changed) Created: - tools/generate_all_docs.sh (28 lines) - tools/generate_man_page.sh (12 lines) - docs/mcrfpy.3 (1070 lines) - stubs/mcrfpy.pyi (532 lines) - stubs/mcrfpy/__init__.pyi (213 lines) - stubs/mcrfpy/automation.pyi (24 lines) - stubs/py.typed (0 bytes) Total: 2159 insertions, 225 deletions ## Testing Verified: - Man page viewable with `man docs/mcrfpy.3` - No function name duplication in docs/API_REFERENCE_DYNAMIC.md - Raises sections properly separated from Returns - Master script successfully generates all formats ## Related Issues Addresses requirements from Phase 7 documentation issues. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
621d719c25
commit
4e94291cfb
10 changed files with 2159 additions and 225 deletions
28
tools/generate_all_docs.sh
Executable file
28
tools/generate_all_docs.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/bash
|
||||
set -e # Exit on any error
|
||||
|
||||
echo "=== McRogueFace Documentation Generation ==="
|
||||
|
||||
# Verify build exists
|
||||
if [ ! -f "./build/mcrogueface" ]; then
|
||||
echo "ERROR: build/mcrogueface not found. Run 'make' first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate API docs (HTML + Markdown)
|
||||
echo "Generating API documentation..."
|
||||
./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py
|
||||
|
||||
# Generate type stubs (using v2 - manually maintained high-quality stubs)
|
||||
echo "Generating type stubs..."
|
||||
./build/mcrogueface --headless --exec tools/generate_stubs_v2.py
|
||||
|
||||
# Generate man page
|
||||
echo "Generating man page..."
|
||||
./tools/generate_man_page.sh
|
||||
|
||||
echo "=== Documentation generation complete ==="
|
||||
echo " HTML: docs/api_reference_dynamic.html"
|
||||
echo " Markdown: docs/API_REFERENCE_DYNAMIC.md"
|
||||
echo " Man page: docs/mcrfpy.3"
|
||||
echo " Stubs: stubs/mcrfpy.pyi"
|
||||
|
|
@ -82,67 +82,90 @@ except ImportError:
|
|||
sys.exit(1)
|
||||
|
||||
def parse_docstring(docstring):
|
||||
"""Parse a docstring to extract signature, description, args, and returns."""
|
||||
"""Parse a docstring to extract signature, description, args, returns, and raises."""
|
||||
if not docstring:
|
||||
return {"signature": "", "description": "", "args": [], "returns": "", "example": ""}
|
||||
|
||||
return {"signature": "", "description": "", "args": [], "returns": "", "raises": "", "example": ""}
|
||||
|
||||
lines = docstring.strip().split('\n')
|
||||
result = {
|
||||
"signature": "",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"returns": "",
|
||||
"raises": "",
|
||||
"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 = []
|
||||
returns_lines = []
|
||||
raises_lines = []
|
||||
example_lines = []
|
||||
in_example = False
|
||||
|
||||
|
||||
for line in lines:
|
||||
line_lower = line.strip().lower()
|
||||
|
||||
|
||||
# Detect section headers
|
||||
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()
|
||||
# Capture any text on the same line as "Returns:"
|
||||
content_after_colon = line[line.find(':')+1:].strip()
|
||||
if content_after_colon:
|
||||
returns_lines.append(content_after_colon)
|
||||
continue
|
||||
elif line_lower.startswith("raises:") or line_lower.startswith("raise:"):
|
||||
current_section = "raises"
|
||||
# Capture any text on the same line as "Raises:"
|
||||
content_after_colon = line[line.find(':')+1:].strip()
|
||||
if content_after_colon:
|
||||
raises_lines.append(content_after_colon)
|
||||
continue
|
||||
elif line_lower.startswith("example:") or line_lower.startswith("examples:"):
|
||||
in_example = True
|
||||
current_section = "example"
|
||||
continue
|
||||
elif line_lower.startswith("note:"):
|
||||
# Notes go into description
|
||||
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(" "):
|
||||
|
||||
# Skip blank lines unless we're in example section
|
||||
if not line.strip() and current_section != "example":
|
||||
continue
|
||||
|
||||
# Add content to appropriate section
|
||||
if current_section == "description":
|
||||
description_lines.append(line)
|
||||
elif current_section == "args" and line.strip():
|
||||
# Parse argument lines like " x: X coordinate"
|
||||
# Parse argument lines like " filename: Path to file"
|
||||
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()
|
||||
|
||||
elif current_section == "returns" and line.strip():
|
||||
returns_lines.append(line.strip())
|
||||
elif current_section == "raises" and line.strip():
|
||||
raises_lines.append(line.strip())
|
||||
elif current_section == "example":
|
||||
example_lines.append(line)
|
||||
|
||||
result["description"] = '\n'.join(description_lines).strip()
|
||||
result["returns"] = ' '.join(returns_lines).strip()
|
||||
result["raises"] = ' '.join(raises_lines).strip()
|
||||
result["example"] = '\n'.join(example_lines).strip()
|
||||
|
||||
|
||||
return result
|
||||
|
||||
def get_all_functions():
|
||||
|
|
@ -361,27 +384,32 @@ def generate_html_docs():
|
|||
for func_name in sorted(functions.keys()):
|
||||
func_info = functions[func_name]
|
||||
parsed = func_info["parsed"]
|
||||
|
||||
|
||||
# Use signature if available (already includes name), otherwise use just name
|
||||
heading = parsed['signature'] if parsed['signature'] else f"{func_name}(...)"
|
||||
html_content += f"""
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}</code></h3>
|
||||
<h3><code class="function-signature">{heading}</code></h3>
|
||||
"""
|
||||
if parsed['description']:
|
||||
description = transform_doc_links(parsed['description'], format='html')
|
||||
html_content += f" <p>{description}</p>\n"
|
||||
|
||||
|
||||
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['raises']:
|
||||
html_content += f" <p><span class='raises'>Raises:</span> {html.escape(parsed['raises'])}</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
|
||||
|
|
@ -419,25 +447,30 @@ def generate_html_docs():
|
|||
if method_name == '__init__':
|
||||
continue
|
||||
parsed = method_info['parsed']
|
||||
|
||||
|
||||
# Use signature if available (already includes name), otherwise use just name
|
||||
heading = parsed['signature'] if parsed['signature'] else f"{method_name}(...)"
|
||||
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>
|
||||
<h5><code class="method-name">{heading}</code></h5>
|
||||
"""
|
||||
|
||||
if parsed['description']:
|
||||
description = transform_doc_links(parsed['description'], format='html')
|
||||
html_content += f" <p>{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"
|
||||
|
||||
|
||||
if parsed['raises']:
|
||||
html_content += f" <p style='margin-left: 20px;'><span class='raises'>Raises:</span> {html.escape(parsed['raises'])}</p>\n"
|
||||
|
||||
html_content += " </div>\n"
|
||||
|
||||
html_content += " </div>\n"
|
||||
|
|
@ -491,22 +524,27 @@ def generate_markdown_docs():
|
|||
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"
|
||||
|
||||
# Use signature if available (already includes name), otherwise use just name
|
||||
heading = parsed['signature'] if parsed['signature'] else f"{func_name}(...)"
|
||||
md_content += f"### `{heading}`\n\n"
|
||||
|
||||
if parsed['description']:
|
||||
description = transform_doc_links(parsed['description'], format='markdown')
|
||||
md_content += f"{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['raises']:
|
||||
md_content += f"**Raises:** {parsed['raises']}\n\n"
|
||||
|
||||
if parsed['example']:
|
||||
md_content += f"**Example:**\n```python\n{parsed['example']}\n```\n\n"
|
||||
|
||||
|
|
@ -542,21 +580,26 @@ def generate_markdown_docs():
|
|||
if method_name == '__init__':
|
||||
continue
|
||||
parsed = method_info['parsed']
|
||||
|
||||
md_content += f"#### `{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n"
|
||||
|
||||
# Use signature if available (already includes name), otherwise use just name
|
||||
heading = parsed['signature'] if parsed['signature'] else f"{method_name}(...)"
|
||||
md_content += f"#### `{heading}`\n\n"
|
||||
|
||||
if parsed['description']:
|
||||
description = transform_doc_links(parsed['description'], format='markdown')
|
||||
md_content += f"{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['raises']:
|
||||
md_content += f"**Raises:** {parsed['raises']}\n\n"
|
||||
|
||||
# Constants
|
||||
md_content += "## Constants\n\n"
|
||||
|
|
|
|||
12
tools/generate_man_page.sh
Executable file
12
tools/generate_man_page.sh
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
# Convert markdown docs to man page format
|
||||
|
||||
pandoc docs/API_REFERENCE_DYNAMIC.md \
|
||||
-s -t man \
|
||||
--metadata title="MCRFPY" \
|
||||
--metadata section=3 \
|
||||
--metadata date="$(date +%Y-%m-%d)" \
|
||||
--metadata footer="McRogueFace $(git describe --tags 2>/dev/null || echo 'dev')" \
|
||||
-o docs/mcrfpy.3
|
||||
|
||||
echo "Generated docs/mcrfpy.3"
|
||||
Loading…
Add table
Add a link
Reference in a new issue