#!/usr/bin/env python3 """Generate high-quality HTML API reference documentation for McRogueFace.""" import os import sys import datetime import html from pathlib import Path import mcrfpy def escape_html(text: str) -> str: """Escape HTML special characters.""" return html.escape(text) if text else "" def format_docstring_as_html(docstring: str) -> str: """Convert docstring to properly formatted HTML.""" if not docstring: return "" # Split and process lines lines = docstring.strip().split('\n') result = [] in_code_block = False for line in lines: # Convert \n to actual newlines line = line.replace('\\n', '\n') # Handle code blocks if line.strip().startswith('```'): if in_code_block: result.append('') in_code_block = False else: result.append('
')
                in_code_block = True
            continue
            
        # Convert markdown-style code to HTML
        if '`' in line and not in_code_block:
            import re
            line = re.sub(r'`([^`]+)`', r'\1', line)
        
        if in_code_block:
            result.append(escape_html(line))
        else:
            result.append(escape_html(line) + '
') if in_code_block: result.append('
') return '\n'.join(result) def get_class_details(cls): """Get detailed information about a class.""" info = { 'name': cls.__name__, 'doc': cls.__doc__ or "", 'methods': {}, 'properties': {}, 'bases': [] } # Get real base classes (excluding object) for base in cls.__bases__: if base.__name__ != 'object': info['bases'].append(base.__name__) # Special handling for Entity which doesn't inherit from Drawable if cls.__name__ == 'Entity' and 'Drawable' in info['bases']: info['bases'].remove('Drawable') # Get methods and properties for attr_name in dir(cls): if attr_name.startswith('__') and attr_name != '__init__': continue try: attr = getattr(cls, attr_name) if isinstance(attr, property): info['properties'][attr_name] = { 'doc': (attr.fget.__doc__ if attr.fget else "") or "", 'readonly': attr.fset is None } elif callable(attr) and not attr_name.startswith('_'): info['methods'][attr_name] = attr.__doc__ or "" except: pass return info def generate_class_init_docs(class_name): """Generate initialization documentation for specific classes.""" init_docs = { 'Entity': { 'signature': 'Entity(x=0, y=0, sprite_id=0)', 'description': 'Game entity that can be placed in a Grid.', 'args': [ ('x', 'int', 'Grid x coordinate. Default: 0'), ('y', 'int', 'Grid y coordinate. Default: 0'), ('sprite_id', 'int', 'Sprite index for rendering. Default: 0') ], 'example': '''entity = mcrfpy.Entity(5, 10, 42) entity.move(1, 0) # Move right one tile''' }, 'Color': { 'signature': 'Color(r=255, g=255, b=255, a=255)', 'description': 'RGBA color representation.', 'args': [ ('r', 'int', 'Red component (0-255). Default: 255'), ('g', 'int', 'Green component (0-255). Default: 255'), ('b', 'int', 'Blue component (0-255). Default: 255'), ('a', 'int', 'Alpha component (0-255). Default: 255') ], 'example': 'red = mcrfpy.Color(255, 0, 0)' }, 'Font': { 'signature': 'Font(filename)', 'description': 'Load a font from file.', 'args': [ ('filename', 'str', 'Path to font file (TTF/OTF)') ] }, 'Texture': { 'signature': 'Texture(filename)', 'description': 'Load a texture from file.', 'args': [ ('filename', 'str', 'Path to image file (PNG/JPG/BMP)') ] }, 'Vector': { 'signature': 'Vector(x=0.0, y=0.0)', 'description': '2D vector for positions and directions.', 'args': [ ('x', 'float', 'X component. Default: 0.0'), ('y', 'float', 'Y component. Default: 0.0') ] }, 'Animation': { 'signature': 'Animation(property_name, start_value, end_value, duration, transition="linear", loop=False)', 'description': 'Animate UI element properties over time.', 'args': [ ('property_name', 'str', 'Property to animate (e.g., "x", "y", "scale")'), ('start_value', 'float', 'Starting value'), ('end_value', 'float', 'Ending value'), ('duration', 'float', 'Duration in seconds'), ('transition', 'str', 'Easing function. Default: "linear"'), ('loop', 'bool', 'Whether to loop. Default: False') ], 'properties': ['current_value', 'elapsed_time', 'is_running', 'is_finished'] }, 'GridPoint': { 'description': 'Represents a single tile in a Grid.', 'properties': ['x', 'y', 'texture_index', 'solid', 'transparent', 'color'] }, 'GridPointState': { 'description': 'State information for a GridPoint.', 'properties': ['visible', 'discovered', 'custom_flags'] }, 'Timer': { 'signature': 'Timer(name, callback, interval_ms)', 'description': 'Create a recurring timer.', 'args': [ ('name', 'str', 'Unique timer identifier'), ('callback', 'callable', 'Function to call'), ('interval_ms', 'int', 'Interval in milliseconds') ] } } return init_docs.get(class_name, {}) def generate_method_docs(method_name, class_name): """Generate documentation for specific methods.""" method_docs = { 'Entity': { 'at': { 'signature': 'at(x, y)', 'description': 'Check if entity is at given grid coordinates.', 'args': [('x', 'int'), ('y', 'int')], 'returns': 'bool: True if entity is at (x, y)' }, 'die': { 'signature': 'die()', 'description': 'Remove this entity from its parent grid.', 'note': 'The entity object remains valid but is no longer rendered.' } }, 'Grid': { 'at': { 'signature': 'at(x, y)', 'description': 'Get the GridPoint at the specified coordinates.', 'args': [('x', 'int'), ('y', 'int')], 'returns': 'GridPoint: The tile at (x, y), or None if out of bounds' } }, 'Animation': { 'get_current_value': { 'signature': 'get_current_value()', 'description': 'Get the current interpolated value.', 'returns': 'float: Current animation value' }, 'start': { 'signature': 'start(target)', 'description': 'Start the animation on a target UI element.', 'args': [('target', 'UIDrawable', 'The element to animate')] } } } return method_docs.get(class_name, {}).get(method_name, {}) def generate_collection_docs(class_name): """Generate documentation for collection classes.""" collection_docs = { 'EntityCollection': { 'description': 'Container for Entity objects in a Grid. Supports iteration and indexing.', 'methods': { 'append': 'Add an entity to the collection', 'remove': 'Remove an entity from the collection', 'extend': 'Add multiple entities from an iterable', 'count': 'Count occurrences of an entity', 'index': 'Find the index of an entity' } }, 'UICollection': { 'description': 'Container for UI drawable elements. Supports iteration and indexing.', 'methods': { 'append': 'Add a UI element to the collection', 'remove': 'Remove a UI element from the collection', 'extend': 'Add multiple UI elements from an iterable', 'count': 'Count occurrences of a UI element', 'index': 'Find the index of a UI element' } }, 'UICollectionIter': { 'description': 'Iterator for UICollection. Automatically created when iterating over a UICollection.' }, 'UIEntityCollectionIter': { 'description': 'Iterator for EntityCollection. Automatically created when iterating over an EntityCollection.' } } return collection_docs.get(class_name, {}) def format_class_html(cls_info, class_name): """Format a class as HTML with proper structure.""" html_parts = [] # Class header html_parts.append(f'
') html_parts.append(f'

class {class_name}

') # Inheritance if cls_info['bases']: html_parts.append(f'

Inherits from: {", ".join(cls_info["bases"])}

') # Get additional documentation init_info = generate_class_init_docs(class_name) collection_info = generate_collection_docs(class_name) # Constructor signature for classes with __init__ if init_info.get('signature'): html_parts.append('
') html_parts.append('
')
        html_parts.append(escape_html(init_info['signature']))
        html_parts.append('
') html_parts.append('
') # Description description = "" if collection_info.get('description'): description = collection_info['description'] elif init_info.get('description'): description = init_info['description'] elif cls_info['doc']: # Parse description from docstring doc_lines = cls_info['doc'].strip().split('\n') # Skip constructor line if present start_idx = 1 if doc_lines and '(' in doc_lines[0] else 0 if start_idx < len(doc_lines): description = '\n'.join(doc_lines[start_idx:]).strip() if description: html_parts.append('
') html_parts.append(f'

{format_docstring_as_html(description)}

') html_parts.append('
') # Constructor arguments if init_info.get('args'): html_parts.append('
') html_parts.append('

Arguments:

') html_parts.append('
') for arg_name, arg_type, arg_desc in init_info['args']: html_parts.append(f'
{arg_name} ({arg_type})
') html_parts.append(f'
{escape_html(arg_desc)}
') html_parts.append('
') html_parts.append('
') # Properties/Attributes props = cls_info.get('properties', {}) if props or init_info.get('properties'): html_parts.append('
') html_parts.append('

Attributes:

') html_parts.append('
') # Add documented properties from init_info if init_info.get('properties'): for prop_name in init_info['properties']: html_parts.append(f'
{prop_name}
') html_parts.append(f'
Property of {class_name}
') # Add actual properties for prop_name, prop_info in props.items(): readonly = ' (read-only)' if prop_info.get('readonly') else '' html_parts.append(f'
{prop_name}{readonly}
') if prop_info.get('doc'): html_parts.append(f'
{escape_html(prop_info["doc"])}
') html_parts.append('
') html_parts.append('
') # Methods methods = cls_info.get('methods', {}) collection_methods = collection_info.get('methods', {}) if methods or collection_methods: html_parts.append('
') html_parts.append('

Methods:

') for method_name, method_doc in {**collection_methods, **methods}.items(): if method_name == '__init__': continue html_parts.append('
') # Get specific method documentation method_info = generate_method_docs(method_name, class_name) if method_info: # Use detailed documentation html_parts.append(f'
{method_info["signature"]}
') html_parts.append(f'

{escape_html(method_info["description"])}

') if method_info.get('args'): html_parts.append('

Arguments:

') html_parts.append('
    ') for arg in method_info['args']: if len(arg) == 3: html_parts.append(f'
  • {arg[0]} ({arg[1]}): {arg[2]}
  • ') else: html_parts.append(f'
  • {arg[0]} ({arg[1]})
  • ') html_parts.append('
') if method_info.get('returns'): html_parts.append(f'

Returns: {escape_html(method_info["returns"])}

') if method_info.get('note'): html_parts.append(f'

Note: {escape_html(method_info["note"])}

') else: # Use docstring html_parts.append(f'
{method_name}(...)
') if isinstance(method_doc, str) and method_doc: html_parts.append(f'

{escape_html(method_doc)}

') html_parts.append('
') html_parts.append('
') # Example if init_info.get('example'): html_parts.append('
') html_parts.append('

Example:

') html_parts.append('
')
        html_parts.append(escape_html(init_info['example']))
        html_parts.append('
') html_parts.append('
') html_parts.append('
') html_parts.append('
') return '\n'.join(html_parts) def generate_html_documentation(): """Generate complete HTML API documentation.""" html_parts = [] # HTML header html_parts.append(''' McRogueFace API Reference
''') # Title and timestamp html_parts.append('

McRogueFace API Reference

') html_parts.append(f'

Generated on {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

') # Overview if mcrfpy.__doc__: html_parts.append('
') html_parts.append('

Overview

') # Process the docstring properly doc_lines = mcrfpy.__doc__.strip().split('\\n') for line in doc_lines: if line.strip().startswith('Example:'): html_parts.append('

Example:

') html_parts.append('
')
            elif line.strip() and not line.startswith(' '):
                html_parts.append(f'

{escape_html(line)}

') elif line.strip(): # Code line html_parts.append(escape_html(line)) html_parts.append('
') html_parts.append('
') # Table of Contents html_parts.append('
') html_parts.append('

Table of Contents

') html_parts.append('') html_parts.append('
') # Collect all components classes = {} functions = {} for name in sorted(dir(mcrfpy)): if name.startswith('_'): continue obj = getattr(mcrfpy, name) if isinstance(obj, type): classes[name] = obj elif callable(obj) and not hasattr(obj, '__self__'): functions[name] = obj # Classes section html_parts.append('

Classes

') # Group classes ui_classes = ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity'] collection_classes = ['EntityCollection', 'UICollection', 'UICollectionIter', 'UIEntityCollectionIter'] system_classes = ['Color', 'Vector', 'Texture', 'Font'] other_classes = [name for name in classes if name not in ui_classes + collection_classes + system_classes] # UI Components html_parts.append('

UI Components

') for class_name in ui_classes: if class_name in classes: cls_info = get_class_details(classes[class_name]) html_parts.append(format_class_html(cls_info, class_name)) # Collections html_parts.append('

Collections

') for class_name in collection_classes: if class_name in classes: cls_info = get_class_details(classes[class_name]) html_parts.append(format_class_html(cls_info, class_name)) # System Types html_parts.append('

System Types

') for class_name in system_classes: if class_name in classes: cls_info = get_class_details(classes[class_name]) html_parts.append(format_class_html(cls_info, class_name)) # Other Classes html_parts.append('

Other Classes

') for class_name in other_classes: if class_name in classes: cls_info = get_class_details(classes[class_name]) html_parts.append(format_class_html(cls_info, class_name)) # Functions section html_parts.append('

Functions

') # Group functions by category scene_funcs = ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'] audio_funcs = ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'] ui_funcs = ['find', 'findAll'] system_funcs = ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale'] # Scene Management html_parts.append('

Scene Management

') for func_name in scene_funcs: if func_name in functions: html_parts.append(format_function_html(func_name, functions[func_name])) # Audio html_parts.append('

Audio

') for func_name in audio_funcs: if func_name in functions: html_parts.append(format_function_html(func_name, functions[func_name])) # UI Utilities html_parts.append('

UI Utilities

') for func_name in ui_funcs: if func_name in functions: html_parts.append(format_function_html(func_name, functions[func_name])) # System html_parts.append('

System

') for func_name in system_funcs: if func_name in functions: html_parts.append(format_function_html(func_name, functions[func_name])) # Automation Module if hasattr(mcrfpy, 'automation'): html_parts.append('
') html_parts.append('

Automation Module

') html_parts.append('

The mcrfpy.automation module provides testing and automation capabilities for simulating user input and capturing screenshots.

') automation = mcrfpy.automation auto_funcs = [] for name in sorted(dir(automation)): if not name.startswith('_'): obj = getattr(automation, name) if callable(obj): auto_funcs.append((name, obj)) for name, func in auto_funcs: html_parts.append('
') html_parts.append(f'

automation.{name}

') if func.__doc__: # Extract just the description, not the repeated signature doc_lines = func.__doc__.strip().split(' - ') if len(doc_lines) > 1: description = doc_lines[1] else: description = func.__doc__.strip() html_parts.append(f'

{escape_html(description)}

') html_parts.append('
') html_parts.append('
') # Close HTML html_parts.append('''
''') return '\n'.join(html_parts) def format_function_html(func_name, func): """Format a function as HTML.""" html_parts = [] html_parts.append('
') # Parse docstring doc = func.__doc__ or "" lines = doc.strip().split('\n') if doc else [] # Extract signature signature = func_name + '(...)' if lines and '(' in lines[0]: signature = lines[0].strip() html_parts.append(f'

{escape_html(signature)}

') # Process rest of docstring if len(lines) > 1: in_section = None for line in lines[1:]: stripped = line.strip() if stripped in ['Args:', 'Returns:', 'Raises:', 'Note:', 'Example:']: in_section = stripped[:-1] html_parts.append(f'

{in_section}:

') elif in_section == 'Example': if not stripped: continue if stripped.startswith('>>>') or (len(lines) > lines.index(line) + 1 and lines[lines.index(line) + 1].strip().startswith('>>>')): html_parts.append('
')
                    html_parts.append(escape_html(stripped))
                    # Get rest of example
                    idx = lines.index(line) + 1
                    while idx < len(lines) and lines[idx].strip():
                        html_parts.append(escape_html(lines[idx]))
                        idx += 1
                    html_parts.append('
') break elif in_section and stripped: if in_section == 'Args': # Format arguments nicely if ':' in stripped: param, desc = stripped.split(':', 1) html_parts.append(f'

{escape_html(param.strip())}: {escape_html(desc.strip())}

') else: html_parts.append(f'

{escape_html(stripped)}

') else: html_parts.append(f'

{escape_html(stripped)}

') elif stripped and not in_section: html_parts.append(f'

{escape_html(stripped)}

') html_parts.append('
') html_parts.append('
') return '\n'.join(html_parts) def main(): """Generate improved HTML API documentation.""" print("Generating improved HTML API documentation...") # Generate HTML html_content = generate_html_documentation() # Write to file output_path = Path("docs/api_reference_improved.html") output_path.parent.mkdir(exist_ok=True) with open(output_path, 'w', encoding='utf-8') as f: f.write(html_content) print(f"✓ Generated {output_path}") print(f" File size: {len(html_content):,} bytes") # Also generate a test to verify the HTML test_content = '''#!/usr/bin/env python3 """Test the improved HTML API documentation.""" import os import sys from pathlib import Path def test_html_quality(): """Test that the HTML documentation meets quality standards.""" html_path = Path("docs/api_reference_improved.html") if not html_path.exists(): print("ERROR: HTML documentation not found") return False with open(html_path, 'r') as f: content = f.read() # Check for common issues issues = [] # Check that \\n is not present literally if '\\\\n' in content: issues.append("Found literal \\\\n in HTML content") # Check that markdown links are converted if '[' in content and '](#' in content: issues.append("Found unconverted markdown links") # Check for proper HTML structure if '

Args:

' in content: issues.append("Args: should not be an H4 heading") if '

Attributes:

' not in content: issues.append("Missing proper Attributes: headings") # Check for duplicate method descriptions if content.count('Get bounding box as (x, y, width, height)') > 20: issues.append("Too many duplicate method descriptions") # Check specific improvements if 'Entity' in content and 'Inherits from: Drawable' in content: issues.append("Entity incorrectly shown as inheriting from Drawable") if not issues: print("✓ HTML documentation passes all quality checks") return True else: print("Issues found:") for issue in issues: print(f" - {issue}") return False if __name__ == '__main__': if test_html_quality(): print("PASS") sys.exit(0) else: print("FAIL") sys.exit(1) ''' test_path = Path("tests/test_html_quality.py") with open(test_path, 'w') as f: f.write(test_content) print(f"✓ Generated test at {test_path}") if __name__ == '__main__': main()