quantalogic 0.35.0__py3-none-any.whl → 0.50.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantalogic/__init__.py +0 -4
- quantalogic/agent.py +603 -363
- quantalogic/agent_config.py +233 -46
- quantalogic/agent_factory.py +34 -22
- quantalogic/coding_agent.py +16 -14
- quantalogic/config.py +2 -1
- quantalogic/console_print_events.py +4 -8
- quantalogic/console_print_token.py +2 -2
- quantalogic/docs_cli.py +15 -10
- quantalogic/event_emitter.py +258 -83
- quantalogic/flow/__init__.py +23 -0
- quantalogic/flow/flow.py +595 -0
- quantalogic/flow/flow_extractor.py +672 -0
- quantalogic/flow/flow_generator.py +89 -0
- quantalogic/flow/flow_manager.py +407 -0
- quantalogic/flow/flow_manager_schema.py +169 -0
- quantalogic/flow/flow_yaml.md +419 -0
- quantalogic/generative_model.py +109 -77
- quantalogic/get_model_info.py +5 -5
- quantalogic/interactive_text_editor.py +100 -73
- quantalogic/main.py +17 -21
- quantalogic/model_info_list.py +3 -3
- quantalogic/model_info_litellm.py +14 -14
- quantalogic/prompts.py +2 -1
- quantalogic/{llm.py → quantlitellm.py} +29 -39
- quantalogic/search_agent.py +4 -4
- quantalogic/server/models.py +4 -1
- quantalogic/task_file_reader.py +5 -5
- quantalogic/task_runner.py +20 -20
- quantalogic/tool_manager.py +10 -21
- quantalogic/tools/__init__.py +98 -68
- quantalogic/tools/composio/composio.py +416 -0
- quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
- quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
- quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
- quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
- quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
- quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
- quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
- quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
- quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
- quantalogic/tools/duckduckgo_search_tool.py +2 -4
- quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
- quantalogic/tools/finance/ccxt_tool.py +373 -0
- quantalogic/tools/finance/finance_llm_tool.py +387 -0
- quantalogic/tools/finance/google_finance.py +192 -0
- quantalogic/tools/finance/market_intelligence_tool.py +520 -0
- quantalogic/tools/finance/technical_analysis_tool.py +491 -0
- quantalogic/tools/finance/tradingview_tool.py +336 -0
- quantalogic/tools/finance/yahoo_finance.py +236 -0
- quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
- quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
- quantalogic/tools/git/clone_repo_tool.py +189 -0
- quantalogic/tools/git/git_operations_tool.py +532 -0
- quantalogic/tools/google_packages/google_news_tool.py +480 -0
- quantalogic/tools/grep_app_tool.py +123 -186
- quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
- quantalogic/tools/jinja_tool.py +6 -10
- quantalogic/tools/language_handlers/__init__.py +22 -9
- quantalogic/tools/list_directory_tool.py +131 -42
- quantalogic/tools/llm_tool.py +45 -15
- quantalogic/tools/llm_vision_tool.py +59 -7
- quantalogic/tools/markitdown_tool.py +17 -5
- quantalogic/tools/nasa_packages/models.py +47 -0
- quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
- quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
- quantalogic/tools/nasa_packages/services.py +82 -0
- quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
- quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
- quantalogic/tools/product_hunt/services.py +63 -0
- quantalogic/tools/rag_tool/__init__.py +48 -0
- quantalogic/tools/rag_tool/document_metadata.py +15 -0
- quantalogic/tools/rag_tool/query_response.py +20 -0
- quantalogic/tools/rag_tool/rag_tool.py +566 -0
- quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
- quantalogic/tools/read_html_tool.py +24 -38
- quantalogic/tools/replace_in_file_tool.py +10 -10
- quantalogic/tools/safe_python_interpreter_tool.py +10 -24
- quantalogic/tools/search_definition_names.py +2 -2
- quantalogic/tools/sequence_tool.py +14 -23
- quantalogic/tools/sql_query_tool.py +17 -19
- quantalogic/tools/tool.py +39 -15
- quantalogic/tools/unified_diff_tool.py +1 -1
- quantalogic/tools/utilities/csv_processor_tool.py +234 -0
- quantalogic/tools/utilities/download_file_tool.py +179 -0
- quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
- quantalogic/tools/utils/__init__.py +1 -4
- quantalogic/tools/utils/create_sample_database.py +24 -38
- quantalogic/tools/utils/generate_database_report.py +74 -82
- quantalogic/tools/wikipedia_search_tool.py +17 -21
- quantalogic/utils/ask_user_validation.py +1 -1
- quantalogic/utils/async_utils.py +35 -0
- quantalogic/utils/check_version.py +3 -5
- quantalogic/utils/get_all_models.py +2 -1
- quantalogic/utils/git_ls.py +21 -7
- quantalogic/utils/lm_studio_model_info.py +9 -7
- quantalogic/utils/python_interpreter.py +113 -43
- quantalogic/utils/xml_utility.py +178 -0
- quantalogic/version_check.py +1 -1
- quantalogic/welcome_message.py +7 -7
- quantalogic/xml_parser.py +0 -1
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/METADATA +40 -1
- quantalogic-0.50.0.dist-info/RECORD +148 -0
- quantalogic-0.35.0.dist-info/RECORD +0 -102
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,623 @@
|
|
1
|
+
"""Tool for converting markdown content to well-structured PDF documents.
|
2
|
+
|
3
|
+
Why this tool:
|
4
|
+
- Provides a standardized way to convert markdown to professional PDF documents
|
5
|
+
- Maintains consistent styling and formatting across documents
|
6
|
+
- Handles complex elements like diagrams, code blocks, and tables
|
7
|
+
- Supports customization through style configurations and templates
|
8
|
+
"""
|
9
|
+
|
10
|
+
import json
|
11
|
+
import os
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import ClassVar, Dict, List, Optional, Union
|
14
|
+
|
15
|
+
import markdown
|
16
|
+
import mermaid
|
17
|
+
from bs4 import BeautifulSoup, Tag
|
18
|
+
from loguru import logger
|
19
|
+
from pygments import highlight
|
20
|
+
from pygments.formatters import HtmlFormatter
|
21
|
+
from pygments.lexers import TextLexer, get_lexer_by_name
|
22
|
+
from weasyprint import CSS, HTML
|
23
|
+
from weasyprint.text.fonts import FontConfiguration
|
24
|
+
|
25
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
26
|
+
|
27
|
+
|
28
|
+
class MarkdownToPdfTool(Tool):
|
29
|
+
"""Converts markdown to professional PDF documents with advanced formatting."""
|
30
|
+
|
31
|
+
model_config = {
|
32
|
+
"arbitrary_types_allowed": True
|
33
|
+
}
|
34
|
+
|
35
|
+
name: str = "markdown_to_pdf_tool"
|
36
|
+
description: str = (
|
37
|
+
"Converts markdown to PDF with support for images, Mermaid diagrams, "
|
38
|
+
"code blocks, tables, and advanced formatting."
|
39
|
+
)
|
40
|
+
need_validation: bool = False
|
41
|
+
|
42
|
+
arguments: List[ToolArgument] = [
|
43
|
+
ToolArgument(
|
44
|
+
name="markdown_content",
|
45
|
+
arg_type="string",
|
46
|
+
description="Markdown content with support for Mermaid, images, code blocks, and tables",
|
47
|
+
required=True,
|
48
|
+
example="# Title\n\nContent with **bold** text\n\n```mermaid\ngraph TD\nA-->B\n```",
|
49
|
+
),
|
50
|
+
ToolArgument(
|
51
|
+
name="output_path",
|
52
|
+
arg_type="string",
|
53
|
+
description="Path for saving the PDF file",
|
54
|
+
required=True,
|
55
|
+
example="/path/to/output.pdf",
|
56
|
+
),
|
57
|
+
ToolArgument(
|
58
|
+
name="style_config",
|
59
|
+
arg_type="string",
|
60
|
+
description="JSON string with style settings (fonts, colors, sizes)",
|
61
|
+
required=False,
|
62
|
+
example='{"font_family": "Calibri", "font_size": "11pt"}',
|
63
|
+
),
|
64
|
+
ToolArgument(
|
65
|
+
name="css_template",
|
66
|
+
arg_type="string",
|
67
|
+
description="Optional CSS template for custom styling",
|
68
|
+
required=False,
|
69
|
+
example="path/to/custom.css",
|
70
|
+
),
|
71
|
+
]
|
72
|
+
|
73
|
+
DEFAULT_STYLES: ClassVar[Dict[str, Union[str, int, Dict[str, str]]]] = {
|
74
|
+
"font_family": "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
|
75
|
+
"heading_font": "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
|
76
|
+
"code_font": "'Source Code Pro', 'Consolas', monospace",
|
77
|
+
"font_size": "12pt",
|
78
|
+
"line_height": "1.6",
|
79
|
+
"text_color": "#333333",
|
80
|
+
"heading_color": "#2c3e50",
|
81
|
+
"link_color": "#0366d6",
|
82
|
+
"code_bg": "#f6f8fa",
|
83
|
+
"code_color": "#24292e",
|
84
|
+
"code_border": "#eaecef",
|
85
|
+
"page_size": "A4",
|
86
|
+
"margins": {
|
87
|
+
"top": "2.5cm",
|
88
|
+
"right": "2.5cm",
|
89
|
+
"bottom": "2.5cm",
|
90
|
+
"left": "2.5cm"
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
DEFAULT_CSS: ClassVar[str] = """
|
95
|
+
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap');
|
96
|
+
|
97
|
+
@page {
|
98
|
+
size: %(page_size)s;
|
99
|
+
margin: %(margins_top)s %(margins_right)s %(margins_bottom)s %(margins_left)s;
|
100
|
+
}
|
101
|
+
|
102
|
+
body {
|
103
|
+
font-family: %(font_family)s;
|
104
|
+
font-size: %(font_size)s;
|
105
|
+
line-height: %(line_height)s;
|
106
|
+
color: %(text_color)s;
|
107
|
+
}
|
108
|
+
|
109
|
+
h1, h2, h3, h4, h5, h6 {
|
110
|
+
font-family: %(heading_font)s;
|
111
|
+
color: %(heading_color)s;
|
112
|
+
margin-top: 1.5em;
|
113
|
+
margin-bottom: 0.5em;
|
114
|
+
}
|
115
|
+
|
116
|
+
a {
|
117
|
+
color: %(link_color)s;
|
118
|
+
text-decoration: none;
|
119
|
+
}
|
120
|
+
|
121
|
+
pre {
|
122
|
+
background-color: %(code_bg)s;
|
123
|
+
border: 1px solid %(code_border)s;
|
124
|
+
border-radius: 6px;
|
125
|
+
padding: 16px;
|
126
|
+
overflow-x: auto;
|
127
|
+
font-size: 85%%;
|
128
|
+
line-height: 1.45;
|
129
|
+
margin: 1em 0;
|
130
|
+
}
|
131
|
+
|
132
|
+
pre code {
|
133
|
+
font-family: %(code_font)s;
|
134
|
+
color: %(code_color)s;
|
135
|
+
background: none;
|
136
|
+
padding: 0;
|
137
|
+
font-size: inherit;
|
138
|
+
white-space: pre;
|
139
|
+
word-break: normal;
|
140
|
+
word-wrap: normal;
|
141
|
+
}
|
142
|
+
|
143
|
+
code {
|
144
|
+
font-family: %(code_font)s;
|
145
|
+
background-color: %(code_bg)s;
|
146
|
+
border-radius: 3px;
|
147
|
+
font-size: 85%%;
|
148
|
+
margin: 0;
|
149
|
+
padding: 0.2em 0.4em;
|
150
|
+
}
|
151
|
+
|
152
|
+
.mermaid-container {
|
153
|
+
margin: 2em 0;
|
154
|
+
padding: 1em;
|
155
|
+
border: 1px solid %(code_border)s;
|
156
|
+
border-radius: 8px;
|
157
|
+
background-color: white;
|
158
|
+
}
|
159
|
+
|
160
|
+
.mermaid-code {
|
161
|
+
margin-bottom: 1.5em;
|
162
|
+
}
|
163
|
+
|
164
|
+
.mermaid-code-header {
|
165
|
+
font-weight: bold;
|
166
|
+
color: #666;
|
167
|
+
margin-bottom: 0.5em;
|
168
|
+
font-size: 90%%;
|
169
|
+
border-bottom: 1px solid #eee;
|
170
|
+
padding-bottom: 0.3em;
|
171
|
+
}
|
172
|
+
|
173
|
+
.mermaid-code-content {
|
174
|
+
background-color: %(code_bg)s;
|
175
|
+
padding: 1em;
|
176
|
+
border-radius: 6px;
|
177
|
+
border: 1px solid %(code_border)s;
|
178
|
+
font-family: %(code_font)s;
|
179
|
+
font-size: 85%%;
|
180
|
+
line-height: 1.45;
|
181
|
+
}
|
182
|
+
|
183
|
+
.mermaid-visualization {
|
184
|
+
margin-top: 1.5em;
|
185
|
+
}
|
186
|
+
|
187
|
+
.mermaid-visualization-header {
|
188
|
+
font-weight: bold;
|
189
|
+
color: #666;
|
190
|
+
margin-bottom: 0.5em;
|
191
|
+
font-size: 90%%;
|
192
|
+
border-bottom: 1px solid #eee;
|
193
|
+
padding-bottom: 0.3em;
|
194
|
+
}
|
195
|
+
|
196
|
+
.mermaid-diagram {
|
197
|
+
text-align: center;
|
198
|
+
padding: 1em;
|
199
|
+
background-color: white;
|
200
|
+
}
|
201
|
+
|
202
|
+
.mermaid-diagram img {
|
203
|
+
max-width: 100%%;
|
204
|
+
height: auto;
|
205
|
+
margin: 0 auto;
|
206
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
207
|
+
border-radius: 4px;
|
208
|
+
}
|
209
|
+
|
210
|
+
.diagram-caption {
|
211
|
+
text-align: center;
|
212
|
+
color: #666;
|
213
|
+
font-style: italic;
|
214
|
+
font-size: 90%%;
|
215
|
+
margin-top: 0.5em;
|
216
|
+
}
|
217
|
+
|
218
|
+
/* Syntax Highlighting Styles */
|
219
|
+
.highlight .hll { background-color: #ffc; }
|
220
|
+
.highlight .c { color: #998; font-style: italic; }
|
221
|
+
.highlight .err { color: #a61717; background-color: #e3d2d2; }
|
222
|
+
.highlight .k { color: #000; font-weight: bold; }
|
223
|
+
.highlight .o { color: #000; font-weight: bold; }
|
224
|
+
.highlight .cm { color: #998; font-style: italic; }
|
225
|
+
.highlight .cp { color: #999; font-weight: bold; font-style: italic; }
|
226
|
+
.highlight .c1 { color: #998; font-style: italic; }
|
227
|
+
.highlight .cs { color: #999; font-weight: bold; font-style: italic; }
|
228
|
+
.highlight .gd { color: #000; background-color: #fdd; }
|
229
|
+
.highlight .ge { color: #000; font-style: italic; }
|
230
|
+
.highlight .gr { color: #a00; }
|
231
|
+
.highlight .gh { color: #999; }
|
232
|
+
.highlight .gi { color: #000; background-color: #dfd; }
|
233
|
+
.highlight .go { color: #888; }
|
234
|
+
.highlight .gp { color: #555; }
|
235
|
+
.highlight .gs { font-weight: bold; }
|
236
|
+
.highlight .gu { color: #aaa; }
|
237
|
+
.highlight .gt { color: #a00; }
|
238
|
+
.highlight .kc { color: #000; font-weight: bold; }
|
239
|
+
.highlight .kd { color: #000; font-weight: bold; }
|
240
|
+
.highlight .kn { color: #000; font-weight: bold; }
|
241
|
+
.highlight .kp { color: #000; font-weight: bold; }
|
242
|
+
.highlight .kr { color: #000; font-weight: bold; }
|
243
|
+
.highlight .kt { color: #458; font-weight: bold; }
|
244
|
+
.highlight .m { color: #099; }
|
245
|
+
.highlight .s { color: #d01040; }
|
246
|
+
.highlight .na { color: #008080; }
|
247
|
+
.highlight .nb { color: #0086B3; }
|
248
|
+
.highlight .nc { color: #458; font-weight: bold; }
|
249
|
+
.highlight .no { color: #008080; }
|
250
|
+
.highlight .nd { color: #3c5d5d; font-weight: bold; }
|
251
|
+
.highlight .ni { color: #800080; }
|
252
|
+
.highlight .ne { color: #900; font-weight: bold; }
|
253
|
+
.highlight .nf { color: #900; font-weight: bold; }
|
254
|
+
.highlight .nl { color: #900; font-weight: bold; }
|
255
|
+
.highlight .nn { color: #555; }
|
256
|
+
.highlight .nt { color: #000080; }
|
257
|
+
.highlight .nv { color: #008080; }
|
258
|
+
.highlight .ow { color: #000; font-weight: bold; }
|
259
|
+
.highlight .w { color: #bbb; }
|
260
|
+
.highlight .mf { color: #099; }
|
261
|
+
.highlight .mh { color: #099; }
|
262
|
+
.highlight .mi { color: #099; }
|
263
|
+
.highlight .mo { color: #099; }
|
264
|
+
.highlight .sb { color: #d01040; }
|
265
|
+
.highlight .sc { color: #d01040; }
|
266
|
+
.highlight .sd { color: #d01040; }
|
267
|
+
.highlight .s2 { color: #d01040; }
|
268
|
+
.highlight .se { color: #d01040; }
|
269
|
+
.highlight .sh { color: #d01040; }
|
270
|
+
.highlight .si { color: #d01040; }
|
271
|
+
.highlight .sx { color: #d01040; }
|
272
|
+
.highlight .sr { color: #009926; }
|
273
|
+
.highlight .s1 { color: #d01040; }
|
274
|
+
.highlight .ss { color: #990073; }
|
275
|
+
.highlight .bp { color: #999; }
|
276
|
+
.highlight .vc { color: #008080; }
|
277
|
+
.highlight .vg { color: #008080; }
|
278
|
+
.highlight .vi { color: #008080; }
|
279
|
+
.highlight .il { color: #099; }
|
280
|
+
|
281
|
+
img {
|
282
|
+
max-width: 100%%;
|
283
|
+
height: auto;
|
284
|
+
display: block;
|
285
|
+
margin: 1em auto;
|
286
|
+
}
|
287
|
+
|
288
|
+
blockquote {
|
289
|
+
margin: 1em 0;
|
290
|
+
padding-left: 1em;
|
291
|
+
border-left: 4px solid #ddd;
|
292
|
+
color: #666666;
|
293
|
+
}
|
294
|
+
|
295
|
+
table {
|
296
|
+
width: 100%%;
|
297
|
+
border-collapse: collapse;
|
298
|
+
margin: 1em 0;
|
299
|
+
}
|
300
|
+
|
301
|
+
th, td {
|
302
|
+
border: 1px solid #ddd;
|
303
|
+
padding: 8px;
|
304
|
+
text-align: left;
|
305
|
+
}
|
306
|
+
|
307
|
+
th {
|
308
|
+
background-color: #f6f8fa;
|
309
|
+
}
|
310
|
+
|
311
|
+
@media print {
|
312
|
+
body {
|
313
|
+
background-color: white;
|
314
|
+
}
|
315
|
+
|
316
|
+
pre, code {
|
317
|
+
white-space: pre-wrap;
|
318
|
+
word-wrap: break-word;
|
319
|
+
}
|
320
|
+
}
|
321
|
+
"""
|
322
|
+
|
323
|
+
def _normalize_path(self, path: str) -> Path:
|
324
|
+
"""Convert path string to normalized Path object.
|
325
|
+
|
326
|
+
Args:
|
327
|
+
path: Input path string
|
328
|
+
|
329
|
+
Returns:
|
330
|
+
Normalized Path object
|
331
|
+
"""
|
332
|
+
if path.startswith("~"):
|
333
|
+
path = os.path.expanduser(path)
|
334
|
+
return Path(path).resolve()
|
335
|
+
|
336
|
+
def _parse_style_config(self, style_config: Optional[str]) -> Dict[str, str]:
|
337
|
+
"""Parse and validate style configuration.
|
338
|
+
|
339
|
+
Args:
|
340
|
+
style_config: JSON style configuration string
|
341
|
+
|
342
|
+
Returns:
|
343
|
+
Merged style configuration dictionary
|
344
|
+
"""
|
345
|
+
try:
|
346
|
+
if not style_config:
|
347
|
+
return self.DEFAULT_STYLES.copy()
|
348
|
+
|
349
|
+
custom_styles = json.loads(style_config)
|
350
|
+
styles = self.DEFAULT_STYLES.copy()
|
351
|
+
styles.update(custom_styles)
|
352
|
+
return styles
|
353
|
+
except json.JSONDecodeError as e:
|
354
|
+
logger.error(f"Invalid style configuration JSON: {e}")
|
355
|
+
return self.DEFAULT_STYLES.copy()
|
356
|
+
|
357
|
+
def _generate_css(self, styles: Dict[str, str], css_template: Optional[str] = None) -> CSS:
|
358
|
+
"""Generate CSS for PDF styling.
|
359
|
+
|
360
|
+
Args:
|
361
|
+
styles: Style configuration dictionary
|
362
|
+
css_template: Optional path to CSS template file
|
363
|
+
|
364
|
+
Returns:
|
365
|
+
WeasyPrint CSS object
|
366
|
+
"""
|
367
|
+
# Unpack margins for CSS template
|
368
|
+
if isinstance(styles.get('margins'), dict):
|
369
|
+
margins = styles['margins']
|
370
|
+
styles.update({
|
371
|
+
'margins_top': margins.get('top', '2.5cm'),
|
372
|
+
'margins_right': margins.get('right', '2.5cm'),
|
373
|
+
'margins_bottom': margins.get('bottom', '2.5cm'),
|
374
|
+
'margins_left': margins.get('left', '2.5cm')
|
375
|
+
})
|
376
|
+
|
377
|
+
base_css = self.DEFAULT_CSS % styles
|
378
|
+
|
379
|
+
font_config = FontConfiguration()
|
380
|
+
css_list = [CSS(string=base_css, font_config=font_config)]
|
381
|
+
|
382
|
+
if css_template:
|
383
|
+
try:
|
384
|
+
template_path = self._normalize_path(css_template)
|
385
|
+
if template_path.exists():
|
386
|
+
css_list.append(CSS(filename=str(template_path), font_config=font_config))
|
387
|
+
except Exception as e:
|
388
|
+
logger.error(f"Error loading CSS template: {e}")
|
389
|
+
|
390
|
+
return css_list
|
391
|
+
|
392
|
+
def _create_mermaid_code_section(self, soup: BeautifulSoup, code: str) -> Tag:
|
393
|
+
"""Create the code section of a Mermaid diagram.
|
394
|
+
|
395
|
+
Args:
|
396
|
+
soup: BeautifulSoup instance for HTML manipulation
|
397
|
+
code: The Mermaid diagram code
|
398
|
+
|
399
|
+
Returns:
|
400
|
+
BeautifulSoup Tag containing the code section
|
401
|
+
"""
|
402
|
+
code_section = soup.new_tag('div')
|
403
|
+
code_section['class'] = 'mermaid-code'
|
404
|
+
|
405
|
+
# Add header
|
406
|
+
header = soup.new_tag('div')
|
407
|
+
header['class'] = 'mermaid-code-header'
|
408
|
+
header.string = 'Mermaid Diagram Code'
|
409
|
+
code_section.append(header)
|
410
|
+
|
411
|
+
# Add code content
|
412
|
+
content = soup.new_tag('div')
|
413
|
+
content['class'] = 'mermaid-code-content'
|
414
|
+
code_tag = soup.new_tag('code')
|
415
|
+
code_tag['class'] = 'language-mermaid'
|
416
|
+
code_tag.string = code
|
417
|
+
content.append(code_tag)
|
418
|
+
code_section.append(content)
|
419
|
+
|
420
|
+
return code_section
|
421
|
+
|
422
|
+
def _create_mermaid_visualization(self, soup: BeautifulSoup, code: str) -> Tag:
|
423
|
+
"""Create the visualization section of a Mermaid diagram.
|
424
|
+
|
425
|
+
Args:
|
426
|
+
soup: BeautifulSoup instance for HTML manipulation
|
427
|
+
code: The Mermaid diagram code to render
|
428
|
+
|
429
|
+
Returns:
|
430
|
+
BeautifulSoup Tag containing the visualization section
|
431
|
+
"""
|
432
|
+
viz_section = soup.new_tag('div')
|
433
|
+
viz_section['class'] = 'mermaid-visualization'
|
434
|
+
|
435
|
+
# Add header
|
436
|
+
header = soup.new_tag('div')
|
437
|
+
header['class'] = 'mermaid-visualization-header'
|
438
|
+
header.string = 'Diagram Visualization'
|
439
|
+
viz_section.append(header)
|
440
|
+
|
441
|
+
# Create diagram container
|
442
|
+
diagram = soup.new_tag('div')
|
443
|
+
diagram['class'] = 'mermaid-diagram'
|
444
|
+
|
445
|
+
try:
|
446
|
+
# Generate diagram
|
447
|
+
svg_data = mermaid.generate_diagram(code)
|
448
|
+
|
449
|
+
# Create and add image
|
450
|
+
img = soup.new_tag('img')
|
451
|
+
img['src'] = f"data:image/svg+xml;base64,{svg_data}"
|
452
|
+
img['alt'] = 'Mermaid Diagram'
|
453
|
+
diagram.append(img)
|
454
|
+
|
455
|
+
# Add caption
|
456
|
+
caption = soup.new_tag('div')
|
457
|
+
caption['class'] = 'diagram-caption'
|
458
|
+
caption.string = 'Generated diagram visualization'
|
459
|
+
diagram.append(caption)
|
460
|
+
|
461
|
+
except Exception as e:
|
462
|
+
logger.error(f"Failed to generate Mermaid diagram: {e}")
|
463
|
+
error = soup.new_tag('div')
|
464
|
+
error['style'] = 'color: red; padding: 1em;'
|
465
|
+
error.string = f'Error generating diagram: {str(e)}'
|
466
|
+
diagram.append(error)
|
467
|
+
|
468
|
+
viz_section.append(diagram)
|
469
|
+
return viz_section
|
470
|
+
|
471
|
+
def _process_mermaid_diagrams(self, html_content: str) -> str:
|
472
|
+
"""Convert Mermaid diagram code blocks to images in HTML while preserving the original code.
|
473
|
+
|
474
|
+
Args:
|
475
|
+
html_content: HTML content with Mermaid code blocks
|
476
|
+
|
477
|
+
Returns:
|
478
|
+
HTML content with Mermaid diagrams converted to images while keeping the code
|
479
|
+
"""
|
480
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
481
|
+
mermaid_blocks = soup.find_all('code', class_='language-mermaid')
|
482
|
+
|
483
|
+
for block in mermaid_blocks:
|
484
|
+
try:
|
485
|
+
# Create main container
|
486
|
+
container = soup.new_tag('div')
|
487
|
+
container['class'] = 'mermaid-container'
|
488
|
+
|
489
|
+
# Add code section
|
490
|
+
container.append(self._create_mermaid_code_section(soup, block.text))
|
491
|
+
|
492
|
+
# Add visualization section
|
493
|
+
container.append(self._create_mermaid_visualization(soup, block.text))
|
494
|
+
|
495
|
+
# Replace original block
|
496
|
+
parent = block.parent
|
497
|
+
if parent.name == 'pre':
|
498
|
+
parent.replace_with(container)
|
499
|
+
else:
|
500
|
+
block.replace_with(container)
|
501
|
+
|
502
|
+
except Exception as e:
|
503
|
+
logger.error(f"Error processing Mermaid diagram block: {e}")
|
504
|
+
continue
|
505
|
+
|
506
|
+
return str(soup)
|
507
|
+
|
508
|
+
def _process_code_blocks(self, html_content: str) -> str:
|
509
|
+
"""Process and syntax highlight code blocks.
|
510
|
+
|
511
|
+
Args:
|
512
|
+
html_content: HTML content with code blocks
|
513
|
+
|
514
|
+
Returns:
|
515
|
+
HTML content with syntax highlighted code blocks
|
516
|
+
"""
|
517
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
518
|
+
code_blocks = soup.find_all('code', class_=lambda x: x and x.startswith('language-'))
|
519
|
+
|
520
|
+
for block in code_blocks:
|
521
|
+
try:
|
522
|
+
# Get the language from the class
|
523
|
+
lang = block['class'][0].replace('language-', '')
|
524
|
+
if lang == 'mermaid':
|
525
|
+
continue # Skip Mermaid blocks as they're handled separately
|
526
|
+
|
527
|
+
# Get the appropriate lexer
|
528
|
+
try:
|
529
|
+
lexer = get_lexer_by_name(lang)
|
530
|
+
except ValueError:
|
531
|
+
lexer = TextLexer()
|
532
|
+
|
533
|
+
# Highlight the code
|
534
|
+
formatter = HtmlFormatter(style='github', cssclass='highlight')
|
535
|
+
highlighted = highlight(block.text, lexer, formatter)
|
536
|
+
|
537
|
+
# Create a new tag with the highlighted code
|
538
|
+
new_tag = soup.new_tag('div')
|
539
|
+
new_tag['class'] = 'highlight-wrapper'
|
540
|
+
new_tag.append(BeautifulSoup(highlighted, 'html.parser'))
|
541
|
+
|
542
|
+
# Replace the original code block
|
543
|
+
if block.parent.name == 'pre':
|
544
|
+
block.parent.replace_with(new_tag)
|
545
|
+
else:
|
546
|
+
block.replace_with(new_tag)
|
547
|
+
|
548
|
+
except Exception as e:
|
549
|
+
logger.error(f"Error processing code block: {e}")
|
550
|
+
continue
|
551
|
+
|
552
|
+
return str(soup)
|
553
|
+
|
554
|
+
def execute(self, **kwargs) -> str:
|
555
|
+
"""Execute the markdown to PDF conversion.
|
556
|
+
|
557
|
+
Args:
|
558
|
+
**kwargs: Tool arguments including markdown_content, output_path,
|
559
|
+
style_config, and css_template
|
560
|
+
|
561
|
+
Returns:
|
562
|
+
Success message with output path
|
563
|
+
"""
|
564
|
+
try:
|
565
|
+
markdown_content = kwargs['markdown_content']
|
566
|
+
output_path = self._normalize_path(kwargs['output_path'])
|
567
|
+
style_config = kwargs.get('style_config')
|
568
|
+
css_template = kwargs.get('css_template')
|
569
|
+
|
570
|
+
# Create output directory if it doesn't exist
|
571
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
572
|
+
|
573
|
+
# Convert markdown to HTML with extensions
|
574
|
+
html_content = markdown.markdown(
|
575
|
+
markdown_content,
|
576
|
+
extensions=[
|
577
|
+
'extra',
|
578
|
+
'codehilite',
|
579
|
+
'tables',
|
580
|
+
'fenced_code',
|
581
|
+
'sane_lists',
|
582
|
+
'nl2br',
|
583
|
+
'attr_list'
|
584
|
+
]
|
585
|
+
)
|
586
|
+
|
587
|
+
# Process code blocks with syntax highlighting
|
588
|
+
html_content = self._process_code_blocks(html_content)
|
589
|
+
|
590
|
+
# Process Mermaid diagrams
|
591
|
+
html_content = self._process_mermaid_diagrams(html_content)
|
592
|
+
|
593
|
+
# Generate CSS
|
594
|
+
styles = self._parse_style_config(style_config)
|
595
|
+
css_list = self._generate_css(styles, css_template)
|
596
|
+
|
597
|
+
# Convert to PDF
|
598
|
+
html = HTML(string=html_content)
|
599
|
+
html.write_pdf(
|
600
|
+
str(output_path),
|
601
|
+
stylesheets=css_list,
|
602
|
+
optimize_size=('fonts', 'images')
|
603
|
+
)
|
604
|
+
|
605
|
+
return f"Successfully created PDF at: {output_path}"
|
606
|
+
|
607
|
+
except Exception as e:
|
608
|
+
error_msg = f"Error converting markdown to PDF: {str(e)}"
|
609
|
+
logger.error(error_msg)
|
610
|
+
raise RuntimeError(error_msg)
|
611
|
+
|
612
|
+
|
613
|
+
if __name__ == "__main__":
|
614
|
+
# Example usage with error handling
|
615
|
+
try:
|
616
|
+
tool = MarkdownToPdfTool()
|
617
|
+
result = tool.execute(
|
618
|
+
markdown_content="# Test Document\n\nThis is a test.",
|
619
|
+
output_path="test_output.pdf"
|
620
|
+
)
|
621
|
+
print(result)
|
622
|
+
except Exception as e:
|
623
|
+
print(f"Error: {e}")
|