Coverage for src/local_deep_research/exporters/quarto_exporter.py: 100%
47 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
1"""Quarto export service.
3This module provides the QuartoExporter class that wraps the existing
4QuartoExporter implementation from text_optimization.citation_formatter.
5"""
7import io
8import re
9import zipfile
10from typing import Optional
12from loguru import logger
14from .base import BaseExporter, ExportOptions, ExportResult
15from .registry import ExporterRegistry
18@ExporterRegistry.register
19class QuartoExporter(BaseExporter):
20 """Quarto exporter using the existing QuartoExporter implementation.
22 This exporter converts markdown content to Quarto (.qmd) format,
23 which can be rendered to HTML, PDF, or other formats using the
24 Quarto publishing system.
26 The export produces a ZIP file containing:
27 - The .qmd document
28 - A references.bib bibliography file
29 """
31 def __init__(self):
32 """Initialize Quarto exporter."""
33 # Import here to avoid circular imports
34 from ..text_optimization.citation_formatter import (
35 QuartoExporter as LegacyQuartoExporter,
36 is_line_breaking_char,
37 )
39 self._legacy = LegacyQuartoExporter()
40 self._is_line_breaking_char = is_line_breaking_char
42 @property
43 def format_name(self) -> str:
44 return "quarto"
46 @property
47 def file_extension(self) -> str:
48 return ".zip"
50 @property
51 def mimetype(self) -> str:
52 return "application/zip"
54 def export(
55 self,
56 markdown_content: str,
57 options: Optional[ExportOptions] = None,
58 ) -> ExportResult:
59 """Convert markdown content to Quarto format.
61 Args:
62 markdown_content: The markdown text to convert
63 options: Optional export options (title, metadata)
65 Returns:
66 ExportResult with ZIP content as bytes containing .qmd and .bib files
68 Raises:
69 ValueError: If content exceeds maximum size limit
70 """
71 try:
72 # Check content size limit to prevent OOM errors
73 self._validate_content_size(markdown_content)
75 options = options or ExportOptions()
77 # Extract title from markdown if not provided
78 title = options.title
79 if not title:
80 title_match = re.search(
81 r"^#\s+(.+)$", markdown_content, re.MULTILINE
82 )
83 title = (
84 title_match.group(1) if title_match else "Research Report"
85 )
87 # Generate Quarto document
88 exported_content = self._legacy.export_to_quarto(
89 markdown_content, title
90 )
92 # Generate bibliography
93 bib_content = self._legacy._generate_bibliography(markdown_content)
95 # Use base class method for safe filename generation, then extract base name
96 base_filename = self._generate_safe_filename(options.title)
97 # Remove the .zip extension that _generate_safe_filename adds
98 safe_title = base_filename.replace(self.file_extension, "")
99 # The base sanitizer deliberately keeps '\\s' whitespace so
100 # the HTTP route can percent-encode it (pinned contract).
101 # But this stem is embedded RAW as a zip entry name, where
102 # CR/LF/TAB would corrupt the archive listing. Strip with
103 # the repo's one shared line-breaking-char class (C0, DEL,
104 # C1, U+2028/U+2029) at this sink, rather than a narrower
105 # ad-hoc range that would drift from it again.
106 safe_title = "".join(
107 ch for ch in safe_title if not self._is_line_breaking_char(ch)
108 )
110 # Create a zip file in memory containing both files
111 zip_buffer = io.BytesIO()
112 with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
113 # Add the Quarto document
114 zipf.writestr(f"{safe_title}.qmd", exported_content)
115 # Add the bibliography file
116 zipf.writestr("references.bib", bib_content)
118 zip_bytes = zip_buffer.getvalue()
119 zip_buffer.close()
121 filename = f"{safe_title}_quarto.zip"
123 logger.info(
124 f"Generated Quarto with bibliography in memory (zip), "
125 f"size: {len(zip_bytes)} bytes"
126 )
128 return ExportResult(
129 content=zip_bytes,
130 filename=filename,
131 mimetype=self.mimetype,
132 )
134 except Exception:
135 logger.exception("Error generating Quarto")
136 raise