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:
John McCardle 2025-10-30 21:20:50 -04:00
commit 4e94291cfb
10 changed files with 2159 additions and 225 deletions

View file

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