Implements HTML rendering system for Information Spaces: - SpaceRenderer: Abstract base class for renderers - RenderConfig: Configuration for format, theme, TOC, etc. - RenderResult: Immutable result with content hash and metadata - ThemeConfig: Layered theme system with customization - CompositeRenderer: Multi-format renderer delegation - MarkdownToHTMLRenderer: Full markdown-to-HTML conversion - Theme support (github, dark, minimal, academic) - Code block handling - Link target="_blank" for external links - Table of contents generation - Heading ID generation for navigation - HTMLRendererFactory: Factory for common renderer configurations - SpaceRenderingService: Orchestration layer - Transclusion variable substitution - Render caching with automatic invalidation - Event emission (RENDER_STARTED, RENDER_COMPLETED, RENDER_FAILED) - Batch rendering support - Statistics tracking - SpaceRenderingServiceBuilder: Fluent builder pattern 60 unit tests covering all components. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
240 lines
6.7 KiB
Python
240 lines
6.7 KiB
Python
"""
|
|
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())
|