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

1"""Quarto export service. 

2 

3This module provides the QuartoExporter class that wraps the existing 

4QuartoExporter implementation from text_optimization.citation_formatter. 

5""" 

6 

7import io 

8import re 

9import zipfile 

10from typing import Optional 

11 

12from loguru import logger 

13 

14from .base import BaseExporter, ExportOptions, ExportResult 

15from .registry import ExporterRegistry 

16 

17 

18@ExporterRegistry.register 

19class QuartoExporter(BaseExporter): 

20 """Quarto exporter using the existing QuartoExporter implementation. 

21 

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. 

25 

26 The export produces a ZIP file containing: 

27 - The .qmd document 

28 - A references.bib bibliography file 

29 """ 

30 

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 ) 

38 

39 self._legacy = LegacyQuartoExporter() 

40 self._is_line_breaking_char = is_line_breaking_char 

41 

42 @property 

43 def format_name(self) -> str: 

44 return "quarto" 

45 

46 @property 

47 def file_extension(self) -> str: 

48 return ".zip" 

49 

50 @property 

51 def mimetype(self) -> str: 

52 return "application/zip" 

53 

54 def export( 

55 self, 

56 markdown_content: str, 

57 options: Optional[ExportOptions] = None, 

58 ) -> ExportResult: 

59 """Convert markdown content to Quarto format. 

60 

61 Args: 

62 markdown_content: The markdown text to convert 

63 options: Optional export options (title, metadata) 

64 

65 Returns: 

66 ExportResult with ZIP content as bytes containing .qmd and .bib files 

67 

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) 

74 

75 options = options or ExportOptions() 

76 

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 ) 

86 

87 # Generate Quarto document 

88 exported_content = self._legacy.export_to_quarto( 

89 markdown_content, title 

90 ) 

91 

92 # Generate bibliography 

93 bib_content = self._legacy._generate_bibliography(markdown_content) 

94 

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 ) 

109 

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) 

117 

118 zip_bytes = zip_buffer.getvalue() 

119 zip_buffer.close() 

120 

121 filename = f"{safe_title}_quarto.zip" 

122 

123 logger.info( 

124 f"Generated Quarto with bibliography in memory (zip), " 

125 f"size: {len(zip_bytes)} bytes" 

126 ) 

127 

128 return ExportResult( 

129 content=zip_bytes, 

130 filename=filename, 

131 mimetype=self.mimetype, 

132 ) 

133 

134 except Exception: 

135 logger.exception("Error generating Quarto") 

136 raise