"""HTML Export and Browser PDF for Opus Orchestrator. Uses browser for PDF generation - no LaTeX required! """ import json from pathlib import Path from typing import Optional from dataclasses import dataclass from datetime import datetime @dataclass class HTMLOptions: """Options for HTML export.""" template: str = "memoir" # memoir, academic, minimal theme: str = "light" # light, dark, sepia font: str = "serif" # serif, sans include_toc: bool = True author: str = "" dedication: str = "" date: str = "" def __post_init__(self): if not self.date: self.date = datetime.now().strftime("%Y") # HTML Templates TEMPLATES = { "memoir": { "name": "Memoir", "description": "Novel, memoir, personal narrative", "fonts": ["Merriweather", "Lora"], "background": "#fdfbf7", "text": "#2c2c2c", }, "academic": { "name": "Academic", "description": "Technical, textbook, educational", "fonts": ["Roboto", "Open Sans"], "background": "#ffffff", "text": "#1a1a1a", }, "minimal": { "name": "Minimal", "description": "Clean, simple design", "fonts": ["Inter", "System UI"], "background": "#ffffff", "text": "#000000", }, } class HTMLExporter: """Export manuscript to HTML and PDF via browser.""" def __init__(self, template_dir: Optional[str] = None): if template_dir: self.template_dir = Path(template_dir) else: self.template_dir = Path(__file__).parent / "templates" / "html" def export( self, manuscript, book_title: str, options: Optional[HTMLOptions] = None, ) -> str: """Export manuscript to HTML. Args: manuscript: The Manuscript to export book_title: Title for the book options: HTMLOptions Returns: HTML string """ opts = options or HTMLOptions() template_info = TEMPLATES.get(opts.template, TEMPLATES["memoir"]) # Build HTML html_parts = [ self._build_head(book_title, template_info, opts), self._build_body(manuscript, book_title, opts), ] return "\n".join(html_parts) def export_to_file( self, manuscript, book_title: str, output_path: str, options: Optional[HTMLOptions] = None, ) -> dict: """Export to HTML file.""" html = self.export(manuscript, book_title, options) output_file = Path(output_path) output_file.parent.mkdir(parents=True, exist_ok=True) output_file.write_text(html) return { "output_file": str(output_file), "template": options.template if options else "memoir", "size": len(html), } def _build_head( self, book_title: str, template_info: dict, options: HTMLOptions, ) -> str: """Build HTML head with styles.""" font_import = self._get_font_import(template_info["fonts"]) return f""" {book_title} {font_import} """ def _build_body( self, manuscript, book_title: str, options: HTMLOptions, ) -> str: """Build HTML body from chapters.""" parts = [""] # Title page parts.append('
') parts.append(f"

{book_title}

") if options.author: parts.append(f'
by {options.author}
') if options.date: parts.append(f'
{options.date}
') parts.append("
") # Dedication if options.dedication: parts.append(f'
{options.dedication}
') # Table of contents if options.include_toc: parts.append('

Contents

") # Chapters for i, chapter in enumerate(manuscript.chapters, 1): parts.append(f'
') parts.append(f'Chapter {i}') parts.append(f"

{chapter.title}

") # Content content = self._markdown_to_html(chapter.content or "") parts.append(content) parts.append("
") parts.append("") return "\n".join(parts) def _markdown_to_html(self, text: str) -> str: """Convert basic markdown to HTML.""" import re # Headers text = re.sub(r'^#### (.+)$', r'

\1

', text, flags=re.MULTILINE) text = re.sub(r'^### (.+)$', r'

\1

', text, flags=re.MULTILINE) text = re.sub(r'^## (.+)$', r'

\1

', text, flags=re.MULTILINE) text = re.sub(r'^# (.+)$', r'

\1

', text, flags=re.MULTILINE) # Bold/italic text = re.sub(r'\*\*\*(.+?)\*\*\*', r'\1', text) text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) text = re.sub(r'\*(.+?)\*', r'\1', text) # Code text = re.sub(r'```(.+?)```', r'
\1
', text, flags=re.DOTALL) text = re.sub(r'`(.+?)`', r'\1', text) # Lists text = re.sub(r'^- (.+)$', r'
  • \1
  • ', text, flags=re.MULTILINE) text = re.sub(r'^\d+\. (.+)$', r'
  • \1
  • ', text, flags=re.MULTILINE) # Wrap consecutive
  • in ') text = '\n'.join(new_lines) # Paragraphs paragraphs = [] for para in text.split('\n\n'): para = para.strip() if para and not para.startswith('<'): para = f'

    {para}

    ' paragraphs.append(para) text = '\n'.join(paragraphs) return text def _get_font_import(self, fonts: list) -> str: """Get Google Fonts import URL.""" font_query = "|".join(fonts).replace(" ", "+") return f'' def export_to_html( manuscript, book_title: str, output_path: str = "", template: str = "memoir", **options, ) -> dict: """Convenience function to export to HTML. Args: manuscript: The Manuscript book_title: Book title output_path: Output .html path (optional) template: Template name **options: Additional HTMLOptions Returns: Export result with HTML string or file path """ opts = HTMLOptions(template=template, **options) exporter = HTMLExporter() if output_path: return exporter.export_to_file(manuscript, book_title, output_path, opts) else: html = exporter.export(manuscript, book_title, opts) return {"html": html, "template": template} def export_to_pdf( manuscript, book_title: str, output_path: str, template: str = "memoir", **options, ) -> dict: """Export to PDF via browser. Args: manuscript: The Manuscript book_title: Book title output_path: Output .pdf path template: Template name **options: Additional HTMLOptions Returns: Export result """ # First export to HTML opts = HTMLOptions(template=template, **options) exporter = HTMLExporter() html = exporter.export(manuscript, book_title, opts) # Save HTML temporarily html_path = output_path.replace(".pdf", ".html") Path(html_path).write_text(html) return { "html_file": html_path, "pdf_file": output_path, "template": template, "html": html, }