"""Base renderer interface for doc-forge output generation. The renderer system provides a pluggable architecture for generating different output formats from the same documentation model. All renderers implement the DocRenderer protocol, ensuring consistent behavior across different output formats. This module defines the base interface and configuration that all renderers must follow. """ from __future__ import annotations from abc import ABC, abstractmethod from pathlib import Path from typing import Any, Dict, Protocol, runtime_checkable from docforge.model import Project @runtime_checkable class DocRenderer(Protocol): """Protocol for documentation renderers. DocRenderer defines the interface that all documentation renderers must implement. This protocol ensures that renderers can be used interchangeably while providing consistent behavior. All renderers must: 1. Have a unique name identifier 2. Generate source files from the documentation model 3. Support building final artifacts 4. Optionally support serving documentation locally Attributes: name: Unique identifier for the renderer """ name: str def generate_sources(self, project: Project, out_dir: Path) -> None: """Generate renderer-specific source files. This method converts the documentation model into renderer-specific source files (e.g., Markdown for MkDocs, reStructuredText for Sphinx). The generated files are written to the specified output directory. Args: project: The documentation project to render out_dir: Directory where source files should be written """ ... def build(self, config: 'RendererConfig') -> None: """Build final documentation artifacts. This method takes the generated source files and builds the final documentation artifacts (e.g., HTML site, PDF, etc.). The specific output depends on the renderer type. Args: config: Configuration for the build process """ ... def serve(self, config: 'RendererConfig') -> None: """Serve documentation locally (optional). This method starts a local development server to serve the documentation. This is optional and not all renderers support serving functionality. Args: config: Configuration for the serve process Raises: NotImplementedError: If serving is not supported """ ... class RendererConfig: """Base configuration for renderers. RendererConfig provides common configuration options that are applicable to all renderers. Each renderer can extend this class to add renderer-specific configuration options. Attributes: out_dir: Output directory for generated files project: The documentation project being rendered extra: Additional renderer-specific configuration """ def __init__( self, out_dir: Path, project: Project, extra: Optional[Dict[str, Any]] = None, ) -> None: """Initialize renderer configuration. Args: out_dir: Output directory for generated files project: The documentation project being rendered extra: Additional renderer-specific configuration options """ self.out_dir: Path = out_dir self.project: Project = project self.extra: Dict[str, Any] = extra or {} def get_extra(self, key: str, default: Any = None) -> Any: """Get an extra configuration value. Args: key: The configuration key to retrieve default: Default value if key is not found Returns: The configuration value or default """ return self.extra.get(key, default) def set_extra(self, key: str, value: Any) -> None: """Set an extra configuration value. Args: key: The configuration key to set value: The value to set """ self.extra[key] = value class BaseRenderer(ABC): """Abstract base class for renderers. BaseRenderer provides a common foundation for all renderer implementations. It implements shared functionality and defines the abstract methods that concrete renderers must implement. This class helps ensure consistent behavior across different renderer implementations while reducing code duplication. Attributes: name: Unique identifier for the renderer """ name: str def __init__(self, name: str) -> None: """Initialize the base renderer. Args: name: Unique identifier for the renderer """ self.name = name @abstractmethod def generate_sources(self, project: Project, out_dir: Path) -> None: """Generate renderer-specific source files. Args: project: The documentation project to render out_dir: Directory where source files should be written """ pass @abstractmethod def build(self, config: RendererConfig) -> None: """Build final documentation artifacts. Args: config: Configuration for the build process """ pass def serve(self, config: RendererConfig) -> None: """Serve documentation locally. Default implementation raises NotImplementedError. Renderers that support serving should override this method. Args: config: Configuration for the serve process Raises: NotImplementedError: If serving is not supported """ raise NotImplementedError(f"Serving is not supported by {self.name} renderer") def ensure_output_dir(self, out_dir: Path) -> None: """Ensure the output directory exists. Args: out_dir: Directory to ensure exists """ out_dir.mkdir(parents=True, exist_ok=True) def validate_project(self, project: Project) -> None: """Validate that the project is suitable for rendering. Args: project: The project to validate Raises: ValueError: If the project is not valid for rendering """ if project.is_empty(): raise ValueError("Project contains no modules to render") def __repr__(self) -> str: """Return a string representation of the renderer. Returns: String representation showing the renderer name """ return f"{self.__class__.__name__}(name='{self.name}')"