#!/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 = { # Base Drawable methods (inherited by all UI elements) 'Drawable': { 'get_bounds': { 'signature': 'get_bounds()', 'description': 'Get the bounding rectangle of this drawable element.', 'returns': 'tuple: (x, y, width, height) representing the element\'s bounds', 'note': 'The bounds are in screen coordinates and account for current position and size.' }, 'move': { 'signature': 'move(dx, dy)', 'description': 'Move the element by a relative offset.', 'args': [ ('dx', 'float', 'Horizontal offset in pixels'), ('dy', 'float', 'Vertical offset in pixels') ], 'note': 'This modifies the x and y position properties by the given amounts.' }, 'resize': { 'signature': 'resize(width, height)', 'description': 'Resize the element to new dimensions.', 'args': [ ('width', 'float', 'New width in pixels'), ('height', 'float', 'New height in pixels') ], 'note': 'Behavior varies by element type. Some elements may ignore or constrain dimensions.' } }, # Caption-specific methods 'Caption': { 'get_bounds': { 'signature': 'get_bounds()', 'description': 'Get the bounding rectangle of the text.', 'returns': 'tuple: (x, y, width, height) based on text content and font size', 'note': 'Bounds are automatically calculated from the rendered text dimensions.' }, 'move': { 'signature': 'move(dx, dy)', 'description': 'Move the caption by a relative offset.', 'args': [ ('dx', 'float', 'Horizontal offset in pixels'), ('dy', 'float', 'Vertical offset in pixels') ] }, 'resize': { 'signature': 'resize(width, height)', 'description': 'Set text wrapping bounds (limited support).', 'args': [ ('width', 'float', 'Maximum width for text wrapping'), ('height', 'float', 'Currently unused') ], 'note': 'Full text wrapping is not yet implemented. This prepares for future multiline support.' } }, # Entity-specific methods 'Entity': { 'at': { 'signature': 'at(x, y)', 'description': 'Get the GridPointState at the specified grid coordinates relative to this entity.', 'args': [ ('x', 'int', 'Grid x offset from entity position'), ('y', 'int', 'Grid y offset from entity position') ], 'returns': 'GridPointState: State of the grid point at the specified position', 'note': 'Requires entity to be associated with a grid. Raises ValueError if not.' }, 'die': { 'signature': 'die()', 'description': 'Remove this entity from its parent grid.', 'returns': 'None', 'note': 'The entity object remains valid but is no longer rendered or updated.' }, 'index': { 'signature': 'index()', 'description': 'Get the index of this entity in its grid\'s entity collection.', 'returns': 'int: Zero-based index in the parent grid\'s entity list', 'note': 'Raises RuntimeError if not associated with a grid, ValueError if not found.' }, 'get_bounds': { 'signature': 'get_bounds()', 'description': 'Get the bounding rectangle of the entity\'s sprite.', 'returns': 'tuple: (x, y, width, height) of the sprite bounds', 'note': 'Delegates to the internal sprite\'s get_bounds method.' }, 'move': { 'signature': 'move(dx, dy)', 'description': 'Move the entity by a relative offset in pixels.', 'args': [ ('dx', 'float', 'Horizontal offset in pixels'), ('dy', 'float', 'Vertical offset in pixels') ], 'note': 'Updates both sprite position and entity grid position.' }, 'resize': { 'signature': 'resize(width, height)', 'description': 'Entities do not support direct resizing.', 'args': [ ('width', 'float', 'Ignored'), ('height', 'float', 'Ignored') ], 'note': 'This method exists for interface compatibility but has no effect.' } }, # Frame-specific methods 'Frame': { 'get_bounds': { 'signature': 'get_bounds()', 'description': 'Get the bounding rectangle of the frame.', 'returns': 'tuple: (x, y, width, height) representing the frame bounds' }, 'move': { 'signature': 'move(dx, dy)', 'description': 'Move the frame and all its children by a relative offset.', 'args': [ ('dx', 'float', 'Horizontal offset in pixels'), ('dy', 'float', 'Vertical offset in pixels') ], 'note': 'Child elements maintain their relative positions within the frame.' }, 'resize': { 'signature': 'resize(width, height)', 'description': 'Resize the frame to new dimensions.', 'args': [ ('width', 'float', 'New width in pixels'), ('height', 'float', 'New height in pixels') ], 'note': 'Does not automatically resize children. Set clip_children=True to clip overflow.' } }, # Grid-specific methods 'Grid': { 'at': { 'signature': 'at(x, y) or at((x, y))', 'description': 'Get the GridPoint at the specified grid coordinates.', 'args': [ ('x', 'int', 'Grid x coordinate (0-based)'), ('y', 'int', 'Grid y coordinate (0-based)') ], 'returns': 'GridPoint: The grid point at (x, y)', 'note': 'Raises IndexError if coordinates are out of range. Accepts either two arguments or a tuple.', 'example': 'point = grid.at(5, 3) # or grid.at((5, 3))' }, 'get_bounds': { 'signature': 'get_bounds()', 'description': 'Get the bounding rectangle of the entire grid.', 'returns': 'tuple: (x, y, width, height) of the grid\'s display area' }, 'move': { 'signature': 'move(dx, dy)', 'description': 'Move the grid display by a relative offset.', 'args': [ ('dx', 'float', 'Horizontal offset in pixels'), ('dy', 'float', 'Vertical offset in pixels') ], 'note': 'Moves the entire grid viewport. Use center property to pan within the grid.' }, 'resize': { 'signature': 'resize(width, height)', 'description': 'Resize the grid\'s display viewport.', 'args': [ ('width', 'float', 'New viewport width in pixels'), ('height', 'float', 'New viewport height in pixels') ], 'note': 'Changes the visible area, not the grid dimensions. Use zoom to scale content.' } }, # Sprite-specific methods 'Sprite': { 'get_bounds': { 'signature': 'get_bounds()', 'description': 'Get the bounding rectangle of the sprite.', 'returns': 'tuple: (x, y, width, height) based on texture size and scale', 'note': 'Bounds account for current scale. Returns (x, y, 0, 0) if no texture.' }, 'move': { 'signature': 'move(dx, dy)', 'description': 'Move the sprite by a relative offset.', 'args': [ ('dx', 'float', 'Horizontal offset in pixels'), ('dy', 'float', 'Vertical offset in pixels') ] }, 'resize': { 'signature': 'resize(width, height)', 'description': 'Resize the sprite by adjusting its scale.', 'args': [ ('width', 'float', 'Target width in pixels'), ('height', 'float', 'Target height in pixels') ], 'note': 'Calculates and applies uniform scale to best fit the target dimensions.' } }, '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')] } }, # Collection methods (shared by EntityCollection and UICollection) 'EntityCollection': { 'append': { 'signature': 'append(entity)', 'description': 'Add an entity to the end of the collection.', 'args': [ ('entity', 'Entity', 'The entity to add') ] }, 'remove': { 'signature': 'remove(entity)', 'description': 'Remove the first occurrence of an entity from the collection.', 'args': [ ('entity', 'Entity', 'The entity to remove') ], 'note': 'Raises ValueError if entity is not found.' }, 'extend': { 'signature': 'extend(iterable)', 'description': 'Add multiple entities from an iterable.', 'args': [ ('iterable', 'iterable', 'An iterable of Entity objects') ] }, 'count': { 'signature': 'count(entity)', 'description': 'Count occurrences of an entity in the collection.', 'args': [ ('entity', 'Entity', 'The entity to count') ], 'returns': 'int: Number of times the entity appears' }, 'index': { 'signature': 'index(entity)', 'description': 'Find the index of the first occurrence of an entity.', 'args': [ ('entity', 'Entity', 'The entity to find') ], 'returns': 'int: Zero-based index of the entity', 'note': 'Raises ValueError if entity is not found.' } }, 'UICollection': { 'append': { 'signature': 'append(drawable)', 'description': 'Add a drawable element to the end of the collection.', 'args': [ ('drawable', 'Drawable', 'Any UI element (Frame, Caption, Sprite, Grid)') ] }, 'remove': { 'signature': 'remove(drawable)', 'description': 'Remove the first occurrence of a drawable from the collection.', 'args': [ ('drawable', 'Drawable', 'The drawable to remove') ], 'note': 'Raises ValueError if drawable is not found.' }, 'extend': { 'signature': 'extend(iterable)', 'description': 'Add multiple drawables from an iterable.', 'args': [ ('iterable', 'iterable', 'An iterable of Drawable objects') ] }, 'count': { 'signature': 'count(drawable)', 'description': 'Count occurrences of a drawable in the collection.', 'args': [ ('drawable', 'Drawable', 'The drawable to count') ], 'returns': 'int: Number of times the drawable appears' }, 'index': { 'signature': 'index(drawable)', 'description': 'Find the index of the first occurrence of a drawable.', 'args': [ ('drawable', 'Drawable', 'The drawable to find') ], 'returns': 'int: Zero-based index of the drawable', 'note': 'Raises ValueError if drawable is not found.' } } } return method_docs.get(class_name, {}).get(method_name, {}) def generate_function_docs(): """Generate documentation for all mcrfpy module functions.""" function_docs = { # Scene Management 'createScene': { 'signature': 'createScene(name: str) -> None', 'description': 'Create a new empty scene.', 'args': [ ('name', 'str', 'Unique name for the new scene') ], 'returns': 'None', 'exceptions': [ ('ValueError', 'If a scene with this name already exists') ], 'note': 'The scene is created but not made active. Use setScene() to switch to it.', 'example': '''mcrfpy.createScene("game") mcrfpy.createScene("menu") mcrfpy.setScene("game")''' }, 'setScene': { 'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None', 'description': 'Switch to a different scene with optional transition effect.', 'args': [ ('scene', 'str', 'Name of the scene to switch to'), ('transition', 'str', 'Transition type ("fade", "slide_left", "slide_right", "slide_up", "slide_down"). Default: None'), ('duration', 'float', 'Transition duration in seconds. Default: 0.0 for instant') ], 'returns': 'None', 'exceptions': [ ('KeyError', 'If the scene doesn\'t exist'), ('ValueError', 'If the transition type is invalid') ], 'example': '''mcrfpy.setScene("menu") mcrfpy.setScene("game", "fade", 0.5) mcrfpy.setScene("credits", "slide_left", 1.0)''' }, 'currentScene': { 'signature': 'currentScene() -> str', 'description': 'Get the name of the currently active scene.', 'args': [], 'returns': 'str: Name of the current scene', 'example': '''scene = mcrfpy.currentScene() print(f"Currently in scene: {scene}")''' }, 'sceneUI': { 'signature': 'sceneUI(scene: str = None) -> list', 'description': 'Get all UI elements for a scene.', 'args': [ ('scene', 'str', 'Scene name. If None, uses current scene. Default: None') ], 'returns': 'list: All UI elements (Frame, Caption, Sprite, Grid) in the scene', 'exceptions': [ ('KeyError', 'If the specified scene doesn\'t exist') ], 'example': '''# Get UI for current scene ui_elements = mcrfpy.sceneUI() # Get UI for specific scene menu_ui = mcrfpy.sceneUI("menu") for element in menu_ui: print(f"{element.name}: {type(element).__name__}")''' }, 'keypressScene': { 'signature': 'keypressScene(handler: callable) -> None', 'description': 'Set the keyboard event handler for the current scene.', 'args': [ ('handler', 'callable', 'Function that receives (key_name: str, is_pressed: bool)') ], 'returns': 'None', 'note': 'The handler is called for every key press and release event. Key names are single characters (e.g., "A", "1") or special keys (e.g., "Space", "Enter", "Escape").', 'example': '''def on_key(key, pressed): if pressed: if key == "Space": player.jump() elif key == "Escape": mcrfpy.setScene("pause_menu") else: # Handle key release if key in ["A", "D"]: player.stop_moving() mcrfpy.keypressScene(on_key)''' }, # Audio Functions 'createSoundBuffer': { 'signature': 'createSoundBuffer(filename: str) -> int', 'description': 'Load a sound effect from a file and return its buffer ID.', 'args': [ ('filename', 'str', 'Path to the sound file (WAV, OGG, FLAC)') ], 'returns': 'int: Buffer ID for use with playSound()', 'exceptions': [ ('RuntimeError', 'If the file cannot be loaded') ], 'note': 'Sound buffers are stored in memory for fast playback. Load sound effects once and reuse the buffer ID.', 'example': '''# Load sound effects jump_sound = mcrfpy.createSoundBuffer("assets/sounds/jump.wav") coin_sound = mcrfpy.createSoundBuffer("assets/sounds/coin.ogg") # Play later mcrfpy.playSound(jump_sound)''' }, 'loadMusic': { 'signature': 'loadMusic(filename: str, loop: bool = True) -> None', 'description': 'Load and immediately play background music from a file.', 'args': [ ('filename', 'str', 'Path to the music file (WAV, OGG, FLAC)'), ('loop', 'bool', 'Whether to loop the music. Default: True') ], 'returns': 'None', 'note': 'Only one music track can play at a time. Loading new music stops the current track.', 'example': '''# Play looping background music mcrfpy.loadMusic("assets/music/theme.ogg") # Play music once without looping mcrfpy.loadMusic("assets/music/victory.ogg", loop=False)''' }, 'playSound': { 'signature': 'playSound(buffer_id: int) -> None', 'description': 'Play a sound effect using a previously loaded buffer.', 'args': [ ('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()') ], 'returns': 'None', 'exceptions': [ ('RuntimeError', 'If the buffer ID is invalid') ], 'note': 'Multiple sounds can play simultaneously. Each call creates a new sound instance.', 'example': '''# Load once explosion_sound = mcrfpy.createSoundBuffer("explosion.wav") # Play multiple times for enemy in destroyed_enemies: mcrfpy.playSound(explosion_sound)''' }, 'getMusicVolume': { 'signature': 'getMusicVolume() -> int', 'description': 'Get the current music volume level.', 'args': [], 'returns': 'int: Current volume (0-100)', 'example': '''volume = mcrfpy.getMusicVolume() print(f"Music volume: {volume}%")''' }, 'getSoundVolume': { 'signature': 'getSoundVolume() -> int', 'description': 'Get the current sound effects volume level.', 'args': [], 'returns': 'int: Current volume (0-100)', 'example': '''volume = mcrfpy.getSoundVolume() print(f"Sound effects volume: {volume}%")''' }, 'setMusicVolume': { 'signature': 'setMusicVolume(volume: int) -> None', 'description': 'Set the global music volume.', 'args': [ ('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)') ], 'returns': 'None', 'example': '''# Mute music mcrfpy.setMusicVolume(0) # Half volume mcrfpy.setMusicVolume(50) # Full volume mcrfpy.setMusicVolume(100)''' }, 'setSoundVolume': { 'signature': 'setSoundVolume(volume: int) -> None', 'description': 'Set the global sound effects volume.', 'args': [ ('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)') ], 'returns': 'None', 'example': '''# Audio settings from options menu mcrfpy.setSoundVolume(sound_slider.value) mcrfpy.setMusicVolume(music_slider.value)''' }, # UI Utilities 'find': { 'signature': 'find(name: str, scene: str = None) -> UIDrawable | None', 'description': 'Find the first UI element with the specified name.', 'args': [ ('name', 'str', 'Exact name to search for'), ('scene', 'str', 'Scene to search in. Default: current scene') ], 'returns': 'Frame, Caption, Sprite, Grid, or Entity if found; None otherwise', 'note': 'Searches scene UI elements and entities within grids. Returns the first match found.', 'example': '''# Find in current scene player = mcrfpy.find("player") if player: player.x = 100 # Find in specific scene menu_button = mcrfpy.find("start_button", "main_menu")''' }, 'findAll': { 'signature': 'findAll(pattern: str, scene: str = None) -> list', 'description': 'Find all UI elements matching a name pattern.', 'args': [ ('pattern', 'str', 'Name pattern with optional wildcards (* matches any characters)'), ('scene', 'str', 'Scene to search in. Default: current scene') ], 'returns': 'list: All matching UI elements and entities', 'note': 'Supports wildcard patterns for flexible searching.', 'example': '''# Find all enemies enemies = mcrfpy.findAll("enemy*") for enemy in enemies: enemy.sprite_id = 0 # Reset sprite # Find all buttons buttons = mcrfpy.findAll("*_button") for btn in buttons: btn.visible = True # Find exact matches health_bars = mcrfpy.findAll("health_bar") # No wildcards = exact match''' }, # System Functions 'exit': { 'signature': 'exit() -> None', 'description': 'Cleanly shut down the game engine and exit the application.', 'args': [], 'returns': 'None', 'note': 'This immediately closes the window and terminates the program. Ensure any necessary cleanup is done before calling.', 'example': '''def quit_game(): # Save game state save_progress() # Exit mcrfpy.exit()''' }, 'getMetrics': { 'signature': 'getMetrics() -> dict', 'description': 'Get current performance metrics.', 'args': [], 'returns': '''dict: Performance data with keys: - frame_time: Last frame duration in seconds - avg_frame_time: Average frame time - fps: Frames per second - draw_calls: Number of draw calls - ui_elements: Total UI element count - visible_elements: Visible element count - current_frame: Frame counter - runtime: Total runtime in seconds''', 'example': '''metrics = mcrfpy.getMetrics() print(f"FPS: {metrics['fps']}") print(f"Frame time: {metrics['frame_time']*1000:.1f}ms") print(f"Draw calls: {metrics['draw_calls']}") print(f"Runtime: {metrics['runtime']:.1f}s") # Performance monitoring if metrics['fps'] < 30: print("Performance warning: FPS below 30")''' }, 'setTimer': { 'signature': 'setTimer(name: str, handler: callable, interval: int) -> None', 'description': 'Create or update a recurring timer.', 'args': [ ('name', 'str', 'Unique identifier for the timer'), ('handler', 'callable', 'Function called with (runtime: float) parameter'), ('interval', 'int', 'Time between calls in milliseconds') ], 'returns': 'None', 'note': 'If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument.', 'example': '''# Simple repeating timer def spawn_enemy(runtime): enemy = mcrfpy.Entity() enemy.x = random.randint(0, 800) grid.entities.append(enemy) mcrfpy.setTimer("enemy_spawner", spawn_enemy, 2000) # Every 2 seconds # Timer with runtime check def update_timer(runtime): time_left = 60 - runtime timer_text.text = f"Time: {int(time_left)}" if time_left <= 0: mcrfpy.delTimer("game_timer") game_over() mcrfpy.setTimer("game_timer", update_timer, 100) # Update every 100ms''' }, 'delTimer': { 'signature': 'delTimer(name: str) -> None', 'description': 'Stop and remove a timer.', 'args': [ ('name', 'str', 'Timer identifier to remove') ], 'returns': 'None', 'note': 'No error is raised if the timer doesn\'t exist.', 'example': '''# Stop spawning enemies mcrfpy.delTimer("enemy_spawner") # Clean up all game timers for timer_name in ["enemy_spawner", "powerup_timer", "score_updater"]: mcrfpy.delTimer(timer_name)''' }, 'setScale': { 'signature': 'setScale(multiplier: float) -> None', 'description': 'Scale the game window size.', 'args': [ ('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)') ], 'returns': 'None', 'exceptions': [ ('ValueError', 'If multiplier is not between 0.2 and 4.0') ], 'note': 'The internal resolution remains 1024x768, but the window is scaled. This is deprecated - use Window.resolution instead.', 'example': '''# Double the window size mcrfpy.setScale(2.0) # Half size window mcrfpy.setScale(0.5) # Better approach (not deprecated): mcrfpy.Window.resolution = (1920, 1080)''' } } return function_docs 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 isinstance(obj, type): # Include built-in functions and other callables (but not classes) 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 using enhanced documentation.""" html_parts = [] html_parts.append('
') # Get enhanced documentation func_docs = generate_function_docs() if func_name in func_docs: doc_info = func_docs[func_name] # Signature signature = doc_info.get('signature', f'{func_name}(...)') html_parts.append(f'

{escape_html(signature)}

') # Description if 'description' in doc_info: html_parts.append(f'

{escape_html(doc_info["description"])}

') # Arguments if 'args' in doc_info and doc_info['args']: html_parts.append('
') html_parts.append('
Arguments:
') html_parts.append('
') for arg_name, arg_type, arg_desc in doc_info['args']: html_parts.append(f'
{escape_html(arg_name)} : {escape_html(arg_type)}
') html_parts.append(f'
{escape_html(arg_desc)}
') html_parts.append('
') html_parts.append('
') # Returns if 'returns' in doc_info and doc_info['returns']: html_parts.append('
') html_parts.append('
Returns:
') html_parts.append(f'

{escape_html(doc_info["returns"])}

') html_parts.append('
') # Exceptions if 'exceptions' in doc_info and doc_info['exceptions']: html_parts.append('
') html_parts.append('
Raises:
') html_parts.append('
') for exc_type, exc_desc in doc_info['exceptions']: html_parts.append(f'
{escape_html(exc_type)}
') html_parts.append(f'
{escape_html(exc_desc)}
') html_parts.append('
') html_parts.append('
') # Note if 'note' in doc_info: html_parts.append('
') html_parts.append(f'

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

') html_parts.append('
') # Example if 'example' in doc_info: html_parts.append('
') html_parts.append('
Example:
') html_parts.append('
')
            html_parts.append(escape_html(doc_info['example']))
            html_parts.append('
') html_parts.append('
') else: # Fallback to parsing docstring if not in enhanced docs 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()