diff --git a/opus_orchestrator/__init__.py b/opus_orchestrator/__init__.py index 6521919..65e068c 100644 --- a/opus_orchestrator/__init__.py +++ b/opus_orchestrator/__init__.py @@ -174,3 +174,16 @@ def __getattr__(name): from opus_orchestrator.latex_compile import compile_pdf return compile_pdf raise AttributeError(f"module has no attribute {name!r}") + +# HTML Export +def __getattr__(name): + if name == "export_to_html": + from opus_orchestrator.html_export import export_to_html + return export_to_html + if name == "export_to_pdf": + from opus_orchestrator.html_export import export_to_pdf + return export_to_pdf + if name == "HTMLExporter": + from opus_orchestrator.html_export import HTMLExporter + return HTMLExporter + raise AttributeError(f"module has no attribute {name!r}") diff --git a/opus_orchestrator/html_export.py b/opus_orchestrator/html_export.py new file mode 100644 index 0000000..686a569 --- /dev/null +++ b/opus_orchestrator/html_export.py @@ -0,0 +1,418 @@ +"""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""" + +
+ + +\1', text, flags=re.DOTALL)
+ text = re.sub(r'`(.+?)`', r'\1', text)
+
+ # Lists
+ text = re.sub(r'^- (.+)$', r'{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, + }