""" Abstract base class for Information Space renderers. This module provides the foundation for rendering resolved markdown content to various output formats, with support for theming and caching. """ from abc import ABC, abstractmethod from dataclasses import dataclass, field from enum import Enum from pathlib import Path from typing import Dict, Any, Optional, List, Set import hashlib class RenderFormat(Enum): """Supported output formats for rendering.""" HTML = "html" PDF = "pdf" # Future DOCX = "docx" # Future LATEX = "latex" # Future @dataclass class ThemeConfig: """ Configuration for rendering themes. Attributes: name: Theme name (e.g., 'github', 'academic', 'minimal') layers: List of theme layers to combine (highest priority first) custom_css: Optional custom CSS to append custom_properties: Theme property overrides """ name: str = "default" layers: List[str] = field(default_factory=lambda: ["basic"]) custom_css: Optional[str] = None custom_properties: Dict[str, Any] = field(default_factory=dict) @dataclass class RenderConfig: """ Configuration for rendering operations. Attributes: format: Output format theme: Theme configuration include_toc: Whether to include table of contents highlight_code: Whether to apply syntax highlighting image_max_width: Maximum image width image_max_height: Maximum image height embed_assets: Whether to embed assets inline base_url: Base URL for relative links """ format: RenderFormat = RenderFormat.HTML theme: ThemeConfig = field(default_factory=ThemeConfig) include_toc: bool = False highlight_code: bool = True image_max_width: str = "100%" image_max_height: str = "auto" embed_assets: bool = False base_url: Optional[str] = None # Advanced options sanitize_html: bool = True link_target_blank: bool = True generate_heading_ids: bool = True @dataclass class RenderResult: """ Result of a rendering operation. Attributes: content: The rendered content format: The output format content_hash: Hash of the rendered content source_hash: Hash of the source content document_id: ID of the rendered document space_id: ID of the containing space dependencies: Document IDs this render depends on metadata: Additional rendering metadata """ content: str format: RenderFormat content_hash: str source_hash: str document_id: str space_id: str dependencies: Set[str] = field(default_factory=set) metadata: Dict[str, Any] = field(default_factory=dict) @staticmethod def compute_hash(content: str) -> str: """Compute a hash of content.""" return hashlib.sha256(content.encode('utf-8')).hexdigest()[:16] class SpaceRenderer(ABC): """ Abstract base class for space content renderers. Renderers transform resolved markdown content into output formats like HTML, PDF, or other document formats. """ def __init__(self, config: Optional[RenderConfig] = None): """ Initialize the renderer. Args: config: Rendering configuration """ self.config = config or RenderConfig() @property @abstractmethod def supported_formats(self) -> List[RenderFormat]: """Return list of formats this renderer supports.""" pass @abstractmethod def render( self, content: str, document_id: str, space_id: str, dependencies: Optional[Set[str]] = None, metadata: Optional[Dict[str, Any]] = None, ) -> RenderResult: """ Render markdown content. Args: content: Resolved markdown content to render document_id: ID of the document being rendered space_id: ID of the containing space dependencies: Document IDs this content depends on metadata: Additional metadata for rendering Returns: RenderResult with rendered content """ pass def supports_format(self, format: RenderFormat) -> bool: """Check if this renderer supports a format.""" return format in self.supported_formats def validate_config(self) -> List[str]: """ Validate the current configuration. Returns: List of validation errors (empty if valid) """ errors = [] if self.config.format not in self.supported_formats: errors.append( f"Format {self.config.format.value} not supported. " f"Supported: {[f.value for f in self.supported_formats]}" ) return errors class CompositeRenderer: """ Manages multiple renderers and delegates to the appropriate one. Allows rendering to different formats using a unified interface. """ def __init__(self): """Initialize the composite renderer.""" self._renderers: Dict[RenderFormat, SpaceRenderer] = {} def register(self, renderer: SpaceRenderer) -> None: """ Register a renderer for its supported formats. Args: renderer: Renderer to register """ for format in renderer.supported_formats: self._renderers[format] = renderer def get_renderer(self, format: RenderFormat) -> Optional[SpaceRenderer]: """Get the renderer for a format.""" return self._renderers.get(format) def render( self, content: str, document_id: str, space_id: str, format: RenderFormat = RenderFormat.HTML, dependencies: Optional[Set[str]] = None, metadata: Optional[Dict[str, Any]] = None, ) -> RenderResult: """ Render content using the appropriate renderer. Args: content: Markdown content to render document_id: Document ID space_id: Space ID format: Target output format dependencies: Document dependencies metadata: Additional metadata Returns: RenderResult Raises: ValueError: If no renderer registered for format """ renderer = self._renderers.get(format) if not renderer: raise ValueError(f"No renderer registered for format: {format.value}") return renderer.render( content=content, document_id=document_id, space_id=space_id, dependencies=dependencies, metadata=metadata, ) def supported_formats(self) -> List[RenderFormat]: """Get all supported formats.""" return list(self._renderers.keys())