Spaces:
Sleeping
Sleeping
Added Support of Mermaid rendering.
Browse files- src/streamlit_app.py +101 -11
src/streamlit_app.py
CHANGED
|
@@ -4,6 +4,9 @@ from weasyprint import HTML, CSS
|
|
| 4 |
from datetime import datetime
|
| 5 |
import base64
|
| 6 |
import os
|
|
|
|
|
|
|
|
|
|
| 7 |
from io import BytesIO
|
| 8 |
from PIL import Image
|
| 9 |
|
|
@@ -242,6 +245,40 @@ def get_css_template(font_name, spacing="normal", font_size=11):
|
|
| 242 |
text-decoration: underline;
|
| 243 |
}}
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
/* Syntax highlighting for code blocks - Complete Pygments style */
|
| 246 |
.codehilite .hll {{ background-color: #ffffcc }}
|
| 247 |
.codehilite .c {{ color: #008000; font-style: italic }} /* Comment */
|
|
@@ -315,6 +352,9 @@ def get_css_template(font_name, spacing="normal", font_size=11):
|
|
| 315 |
|
| 316 |
def generate_html(markdown_text, title, font_name, spacing="normal", font_size=11, logo_data=None):
|
| 317 |
"""Convert Markdown to styled HTML"""
|
|
|
|
|
|
|
|
|
|
| 318 |
# Convert Markdown to HTML with extensions
|
| 319 |
md = markdown.Markdown(
|
| 320 |
extensions=[
|
|
@@ -334,7 +374,7 @@ def generate_html(markdown_text, title, font_name, spacing="normal", font_size=1
|
|
| 334 |
}
|
| 335 |
}
|
| 336 |
)
|
| 337 |
-
body_html = md.convert(
|
| 338 |
|
| 339 |
# Handle logo
|
| 340 |
logo_html = ""
|
|
@@ -396,6 +436,65 @@ def image_to_base64(image_file):
|
|
| 396 |
st.error(f"Error processing image: {str(e)}")
|
| 397 |
return None
|
| 398 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
# Streamlit UI
|
| 400 |
st.title("π Markdown to PDF Converter")
|
| 401 |
st.markdown("Convert your Markdown documents into beautifully formatted PDFs with syntax highlighting")
|
|
@@ -471,35 +570,26 @@ with col1:
|
|
| 471 |
markdown_text = st.text_area(
|
| 472 |
"Markdown Content",
|
| 473 |
value="""# Welcome to Markdown PDF
|
| 474 |
-
|
| 475 |
This is a **sample** document with **syntax highlighting**.
|
| 476 |
-
|
| 477 |
## Features
|
| 478 |
-
|
| 479 |
- Beautiful typography
|
| 480 |
- Professional layout
|
| 481 |
- Code highlighting with Pygments
|
| 482 |
- Atomic list item page breaks
|
| 483 |
-
|
| 484 |
## Code Example
|
| 485 |
-
|
| 486 |
```python
|
| 487 |
def fibonacci(n):
|
| 488 |
\"\"\"Calculate Fibonacci number\"\"\"
|
| 489 |
if n <= 1:
|
| 490 |
return n
|
| 491 |
return fibonacci(n-1) + fibonacci(n-2)
|
| 492 |
-
|
| 493 |
print(fibonacci(10))
|
| 494 |
```
|
| 495 |
-
|
| 496 |
## JavaScript Example
|
| 497 |
-
|
| 498 |
```javascript
|
| 499 |
const greeting = "Hello, World!";
|
| 500 |
console.log(greeting);
|
| 501 |
```
|
| 502 |
-
|
| 503 |
Enjoy! π""",
|
| 504 |
height=400,
|
| 505 |
help="Supports GitHub-Flavored Markdown"
|
|
@@ -590,7 +680,7 @@ with col_btn2:
|
|
| 590 |
st.divider()
|
| 591 |
col_footer1, col_footer2, col_footer3 = st.columns(3)
|
| 592 |
with col_footer1:
|
| 593 |
-
st.caption("π Built with Streamlit
|
| 594 |
with col_footer2:
|
| 595 |
st.caption("π Supports GitHub-Flavored Markdown")
|
| 596 |
with col_footer3:
|
|
|
|
| 4 |
from datetime import datetime
|
| 5 |
import base64
|
| 6 |
import os
|
| 7 |
+
import re
|
| 8 |
+
import zlib
|
| 9 |
+
import requests
|
| 10 |
from io import BytesIO
|
| 11 |
from PIL import Image
|
| 12 |
|
|
|
|
| 245 |
text-decoration: underline;
|
| 246 |
}}
|
| 247 |
|
| 248 |
+
/* Mermaid diagram container - fits within one page */
|
| 249 |
+
.mermaid-container {{
|
| 250 |
+
max-height: 600px;
|
| 251 |
+
width: 100%;
|
| 252 |
+
page-break-inside: avoid;
|
| 253 |
+
break-inside: avoid;
|
| 254 |
+
margin: {1.5 * margin_multiplier}em 0;
|
| 255 |
+
text-align: center;
|
| 256 |
+
background: #fafafa;
|
| 257 |
+
border: 1px solid #e0e0e0;
|
| 258 |
+
border-radius: 8px;
|
| 259 |
+
padding: 1em;
|
| 260 |
+
box-sizing: border-box;
|
| 261 |
+
}}
|
| 262 |
+
|
| 263 |
+
.mermaid-container img {{
|
| 264 |
+
max-width: 100%;
|
| 265 |
+
max-height: 550px;
|
| 266 |
+
width: auto;
|
| 267 |
+
height: auto;
|
| 268 |
+
object-fit: contain;
|
| 269 |
+
display: block;
|
| 270 |
+
margin: 0 auto;
|
| 271 |
+
}}
|
| 272 |
+
|
| 273 |
+
.mermaid-error {{
|
| 274 |
+
padding: 1em;
|
| 275 |
+
background: #fff3cd;
|
| 276 |
+
border: 1px solid #ffc107;
|
| 277 |
+
border-radius: 4px;
|
| 278 |
+
color: #856404;
|
| 279 |
+
margin: 1em 0;
|
| 280 |
+
}}
|
| 281 |
+
|
| 282 |
/* Syntax highlighting for code blocks - Complete Pygments style */
|
| 283 |
.codehilite .hll {{ background-color: #ffffcc }}
|
| 284 |
.codehilite .c {{ color: #008000; font-style: italic }} /* Comment */
|
|
|
|
| 352 |
|
| 353 |
def generate_html(markdown_text, title, font_name, spacing="normal", font_size=11, logo_data=None):
|
| 354 |
"""Convert Markdown to styled HTML"""
|
| 355 |
+
# Process Mermaid blocks first (renders diagrams and keeps code blocks)
|
| 356 |
+
processed_text = process_mermaid_blocks(markdown_text)
|
| 357 |
+
|
| 358 |
# Convert Markdown to HTML with extensions
|
| 359 |
md = markdown.Markdown(
|
| 360 |
extensions=[
|
|
|
|
| 374 |
}
|
| 375 |
}
|
| 376 |
)
|
| 377 |
+
body_html = md.convert(processed_text)
|
| 378 |
|
| 379 |
# Handle logo
|
| 380 |
logo_html = ""
|
|
|
|
| 436 |
st.error(f"Error processing image: {str(e)}")
|
| 437 |
return None
|
| 438 |
|
| 439 |
+
def encode_mermaid_for_ink(mermaid_code):
|
| 440 |
+
"""Encode Mermaid code for mermaid.ink API using pako deflate + base64"""
|
| 441 |
+
# Use zlib to compress (pako compatible)
|
| 442 |
+
compressed = zlib.compress(mermaid_code.encode('utf-8'), level=9)
|
| 443 |
+
# Remove zlib header (first 2 bytes) and checksum (last 4 bytes) for raw deflate
|
| 444 |
+
raw_deflate = compressed[2:-4]
|
| 445 |
+
# Base64 encode with URL-safe characters
|
| 446 |
+
encoded = base64.urlsafe_b64encode(raw_deflate).decode('utf-8')
|
| 447 |
+
return encoded
|
| 448 |
+
|
| 449 |
+
def render_mermaid_to_svg(mermaid_code):
|
| 450 |
+
"""
|
| 451 |
+
Render Mermaid code to SVG using mermaid.ink API
|
| 452 |
+
Returns: SVG content as string, or None on failure
|
| 453 |
+
"""
|
| 454 |
+
try:
|
| 455 |
+
encoded = encode_mermaid_for_ink(mermaid_code)
|
| 456 |
+
url = f"https://mermaid.ink/svg/pako:{encoded}"
|
| 457 |
+
|
| 458 |
+
response = requests.get(url, timeout=30)
|
| 459 |
+
if response.status_code == 200:
|
| 460 |
+
return response.text
|
| 461 |
+
else:
|
| 462 |
+
return None
|
| 463 |
+
except Exception as e:
|
| 464 |
+
print(f"Mermaid rendering error: {e}")
|
| 465 |
+
return None
|
| 466 |
+
|
| 467 |
+
def process_mermaid_blocks(markdown_text):
|
| 468 |
+
"""
|
| 469 |
+
Parse ::mermaid...:: blocks and replace with rendered SVG + code block
|
| 470 |
+
Returns: modified markdown/HTML hybrid with embedded diagrams
|
| 471 |
+
"""
|
| 472 |
+
# Pattern to match ::mermaid\n...\n::
|
| 473 |
+
pattern = r'::mermaid\n(.*?)\n::'
|
| 474 |
+
|
| 475 |
+
def replace_mermaid(match):
|
| 476 |
+
mermaid_code = match.group(1).strip()
|
| 477 |
+
|
| 478 |
+
# Render to SVG
|
| 479 |
+
svg_content = render_mermaid_to_svg(mermaid_code)
|
| 480 |
+
|
| 481 |
+
if svg_content:
|
| 482 |
+
# Embed SVG as base64 data URL
|
| 483 |
+
svg_base64 = base64.b64encode(svg_content.encode('utf-8')).decode('utf-8')
|
| 484 |
+
img_tag = f'<div class="mermaid-container"><img src="data:image/svg+xml;base64,{svg_base64}" alt="Mermaid Diagram" /></div>'
|
| 485 |
+
else:
|
| 486 |
+
img_tag = '<div class="mermaid-error">β οΈ Failed to render Mermaid diagram</div>'
|
| 487 |
+
|
| 488 |
+
# Create code block for raw mermaid code (will be processed by markdown later)
|
| 489 |
+
code_block = f'\n```mermaid\n{mermaid_code}\n```\n'
|
| 490 |
+
|
| 491 |
+
# Return both: image first, then code block
|
| 492 |
+
return f'\n{img_tag}\n{code_block}'
|
| 493 |
+
|
| 494 |
+
# Process all mermaid blocks
|
| 495 |
+
result = re.sub(pattern, replace_mermaid, markdown_text, flags=re.DOTALL)
|
| 496 |
+
return result
|
| 497 |
+
|
| 498 |
# Streamlit UI
|
| 499 |
st.title("π Markdown to PDF Converter")
|
| 500 |
st.markdown("Convert your Markdown documents into beautifully formatted PDFs with syntax highlighting")
|
|
|
|
| 570 |
markdown_text = st.text_area(
|
| 571 |
"Markdown Content",
|
| 572 |
value="""# Welcome to Markdown PDF
|
|
|
|
| 573 |
This is a **sample** document with **syntax highlighting**.
|
|
|
|
| 574 |
## Features
|
|
|
|
| 575 |
- Beautiful typography
|
| 576 |
- Professional layout
|
| 577 |
- Code highlighting with Pygments
|
| 578 |
- Atomic list item page breaks
|
|
|
|
| 579 |
## Code Example
|
|
|
|
| 580 |
```python
|
| 581 |
def fibonacci(n):
|
| 582 |
\"\"\"Calculate Fibonacci number\"\"\"
|
| 583 |
if n <= 1:
|
| 584 |
return n
|
| 585 |
return fibonacci(n-1) + fibonacci(n-2)
|
|
|
|
| 586 |
print(fibonacci(10))
|
| 587 |
```
|
|
|
|
| 588 |
## JavaScript Example
|
|
|
|
| 589 |
```javascript
|
| 590 |
const greeting = "Hello, World!";
|
| 591 |
console.log(greeting);
|
| 592 |
```
|
|
|
|
| 593 |
Enjoy! π""",
|
| 594 |
height=400,
|
| 595 |
help="Supports GitHub-Flavored Markdown"
|
|
|
|
| 680 |
st.divider()
|
| 681 |
col_footer1, col_footer2, col_footer3 = st.columns(3)
|
| 682 |
with col_footer1:
|
| 683 |
+
st.caption("π Built with Streamlit")
|
| 684 |
with col_footer2:
|
| 685 |
st.caption("π Supports GitHub-Flavored Markdown")
|
| 686 |
with col_footer3:
|