prezo 2026.1.1__py3-none-any.whl → 2026.1.3__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.
- prezo/__init__.py +8 -0
- prezo/app.py +25 -31
- prezo/config.py +1 -1
- prezo/export/__init__.py +36 -0
- prezo/export/common.py +77 -0
- prezo/export/html.py +340 -0
- prezo/export/images.py +261 -0
- prezo/export/pdf.py +497 -0
- prezo/export/svg.py +170 -0
- prezo/layout.py +680 -0
- prezo/parser.py +11 -4
- prezo/widgets/__init__.py +9 -1
- prezo/widgets/slide_content.py +81 -0
- {prezo-2026.1.1.dist-info → prezo-2026.1.3.dist-info}/METADATA +25 -4
- {prezo-2026.1.1.dist-info → prezo-2026.1.3.dist-info}/RECORD +17 -10
- prezo/export.py +0 -835
- {prezo-2026.1.1.dist-info → prezo-2026.1.3.dist-info}/WHEEL +0 -0
- {prezo-2026.1.1.dist-info → prezo-2026.1.3.dist-info}/entry_points.txt +0 -0
prezo/__init__.py
CHANGED
|
@@ -128,6 +128,13 @@ def main() -> None:
|
|
|
128
128
|
action="store_true",
|
|
129
129
|
help="Export without window decorations (for printing)",
|
|
130
130
|
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--pdf-backend",
|
|
133
|
+
metavar="BACKEND",
|
|
134
|
+
choices=["auto", "chrome", "inkscape", "cairosvg"],
|
|
135
|
+
default="auto",
|
|
136
|
+
help="PDF conversion backend (auto, chrome, inkscape, cairosvg). Default: auto",
|
|
137
|
+
)
|
|
131
138
|
parser.add_argument(
|
|
132
139
|
"--scale",
|
|
133
140
|
metavar="FACTOR",
|
|
@@ -218,6 +225,7 @@ def main() -> None:
|
|
|
218
225
|
width=width,
|
|
219
226
|
height=height,
|
|
220
227
|
chrome=not args.no_chrome,
|
|
228
|
+
pdf_backend=args.pdf_backend,
|
|
221
229
|
),
|
|
222
230
|
)
|
|
223
231
|
else:
|
prezo/app.py
CHANGED
|
@@ -39,7 +39,7 @@ from .screens import (
|
|
|
39
39
|
)
|
|
40
40
|
from .terminal import ImageCapability, detect_image_capability
|
|
41
41
|
from .themes import get_next_theme, get_theme
|
|
42
|
-
from .widgets import ImageDisplay, StatusBar
|
|
42
|
+
from .widgets import ImageDisplay, SlideContent, StatusBar
|
|
43
43
|
|
|
44
44
|
WELCOME_MESSAGE = """\
|
|
45
45
|
# Welcome to Prezo
|
|
@@ -199,7 +199,7 @@ class PrezoApp(App):
|
|
|
199
199
|
|
|
200
200
|
ENABLE_COMMAND_PALETTE = True
|
|
201
201
|
COMMAND_PALETTE_BINDING = "ctrl+p"
|
|
202
|
-
COMMANDS: ClassVar[set[type[Provider]]] = {PrezoCommands}
|
|
202
|
+
COMMANDS: ClassVar[set[type[Provider]]] = {PrezoCommands}
|
|
203
203
|
|
|
204
204
|
CSS = """
|
|
205
205
|
Screen {
|
|
@@ -389,7 +389,7 @@ class PrezoApp(App):
|
|
|
389
389
|
yield ImageDisplay(id="slide-image")
|
|
390
390
|
# Text container
|
|
391
391
|
with VerticalScroll(id="slide-container"):
|
|
392
|
-
yield
|
|
392
|
+
yield SlideContent("", id="slide-content")
|
|
393
393
|
with Vertical(id="notes-panel"):
|
|
394
394
|
yield Static("Notes", id="notes-title")
|
|
395
395
|
yield Markdown("", id="notes-content")
|
|
@@ -519,7 +519,7 @@ class PrezoApp(App):
|
|
|
519
519
|
recent_section = _format_recent_files(self.state.recent_files)
|
|
520
520
|
if recent_section:
|
|
521
521
|
welcome += recent_section
|
|
522
|
-
self.query_one("#slide-content",
|
|
522
|
+
self.query_one("#slide-content", SlideContent).set_content(welcome)
|
|
523
523
|
status = self.query_one("#status-bar", StatusBar)
|
|
524
524
|
status.current = 0
|
|
525
525
|
status.total = 1
|
|
@@ -555,35 +555,27 @@ class PrezoApp(App):
|
|
|
555
555
|
image_container.add_class("visible")
|
|
556
556
|
|
|
557
557
|
# Apply layout based on MARP directive
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
image_container, before=slide_container
|
|
575
|
-
)
|
|
576
|
-
elif layout in ("background", "fit"):
|
|
577
|
-
# Background/fit images: show image full width behind/above text
|
|
578
|
-
image_container.add_class("layout-inline")
|
|
579
|
-
horizontal_container.move_child(
|
|
580
|
-
image_container, before=slide_container
|
|
581
|
-
)
|
|
558
|
+
match first_image.layout:
|
|
559
|
+
case "left":
|
|
560
|
+
image_container.add_class("layout-left")
|
|
561
|
+
horizontal_container.move_child(
|
|
562
|
+
image_container, before=slide_container
|
|
563
|
+
)
|
|
564
|
+
case "right":
|
|
565
|
+
image_container.add_class("layout-right")
|
|
566
|
+
horizontal_container.move_child(
|
|
567
|
+
image_container, after=slide_container
|
|
568
|
+
)
|
|
569
|
+
case "inline" | "background" | "fit":
|
|
570
|
+
image_container.add_class("layout-inline")
|
|
571
|
+
horizontal_container.move_child(
|
|
572
|
+
image_container, before=slide_container
|
|
573
|
+
)
|
|
582
574
|
|
|
583
575
|
# Apply dynamic width if size_percent is specified
|
|
584
576
|
default_size = 50
|
|
585
577
|
has_custom_size = first_image.size_percent != default_size
|
|
586
|
-
if has_custom_size and layout in ("left", "right"):
|
|
578
|
+
if has_custom_size and first_image.layout in ("left", "right"):
|
|
587
579
|
image_container.styles.width = f"{first_image.size_percent}%"
|
|
588
580
|
else:
|
|
589
581
|
image_container.styles.width = None # Reset to CSS default
|
|
@@ -591,7 +583,9 @@ class PrezoApp(App):
|
|
|
591
583
|
image_widget.clear()
|
|
592
584
|
|
|
593
585
|
# Use cleaned content (bg images already removed by parser)
|
|
594
|
-
self.query_one("#slide-content",
|
|
586
|
+
self.query_one("#slide-content", SlideContent).set_content(
|
|
587
|
+
slide.content.strip()
|
|
588
|
+
)
|
|
595
589
|
|
|
596
590
|
container = self.query_one("#slide-container", VerticalScroll)
|
|
597
591
|
container.scroll_home(animate=False)
|
|
@@ -738,7 +732,7 @@ class PrezoApp(App):
|
|
|
738
732
|
def watch_app_theme(self, theme_name: str) -> None:
|
|
739
733
|
"""Apply theme when it changes."""
|
|
740
734
|
# Only apply to widgets after mount (watcher fires during init)
|
|
741
|
-
if not self.is_mounted:
|
|
735
|
+
if not self.is_mounted:
|
|
742
736
|
return
|
|
743
737
|
self._apply_theme(theme_name)
|
|
744
738
|
self.notify(f"Theme: {theme_name}", timeout=1)
|
prezo/config.py
CHANGED
prezo/export/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Export functionality for prezo presentations.
|
|
2
|
+
|
|
3
|
+
Exports presentations to PDF, HTML, PNG, and SVG formats.
|
|
4
|
+
|
|
5
|
+
IMPORTANT: PDF/PNG/SVG export must be a faithful image of the TUI console.
|
|
6
|
+
This requires proper monospace font loading. If Fira Code is not available,
|
|
7
|
+
alignment may be incorrect.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .common import (
|
|
13
|
+
ExportError,
|
|
14
|
+
check_font_availability,
|
|
15
|
+
print_font_warnings,
|
|
16
|
+
)
|
|
17
|
+
from .html import export_to_html, render_slide_to_html, run_html_export
|
|
18
|
+
from .images import export_slide_to_image, export_to_images, run_image_export
|
|
19
|
+
from .pdf import combine_svgs_to_pdf, export_to_pdf, run_export
|
|
20
|
+
from .svg import render_slide_to_svg
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ExportError",
|
|
24
|
+
"check_font_availability",
|
|
25
|
+
"combine_svgs_to_pdf",
|
|
26
|
+
"export_slide_to_image",
|
|
27
|
+
"export_to_html",
|
|
28
|
+
"export_to_images",
|
|
29
|
+
"export_to_pdf",
|
|
30
|
+
"print_font_warnings",
|
|
31
|
+
"render_slide_to_html",
|
|
32
|
+
"render_slide_to_svg",
|
|
33
|
+
"run_export",
|
|
34
|
+
"run_html_export",
|
|
35
|
+
"run_image_export",
|
|
36
|
+
]
|
prezo/export/common.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Common utilities and constants for export functionality."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExportError(Exception):
|
|
11
|
+
"""Raised when an export operation fails."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Exit codes for CLI (only used in run_* wrapper functions)
|
|
15
|
+
EXIT_SUCCESS = 0
|
|
16
|
+
EXIT_FAILURE = 2
|
|
17
|
+
|
|
18
|
+
# Backwards compatibility aliases (deprecated, use exceptions instead)
|
|
19
|
+
EXPORT_SUCCESS = EXIT_SUCCESS
|
|
20
|
+
EXPORT_FAILED = EXIT_FAILURE
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check_font_availability() -> list[str]:
|
|
24
|
+
"""Check if required fonts are available on the system.
|
|
25
|
+
|
|
26
|
+
Returns a list of warning messages (empty if all fonts are available).
|
|
27
|
+
"""
|
|
28
|
+
warnings = []
|
|
29
|
+
|
|
30
|
+
# Check for fc-list (fontconfig) to query system fonts
|
|
31
|
+
fc_list_path = shutil.which("fc-list")
|
|
32
|
+
if fc_list_path:
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
[fc_list_path, ":family"],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
timeout=5,
|
|
39
|
+
check=False,
|
|
40
|
+
)
|
|
41
|
+
fonts = result.stdout.lower()
|
|
42
|
+
|
|
43
|
+
# Check for Fira Code (primary monospace font)
|
|
44
|
+
if "fira code" not in fonts and "firacode" not in fonts:
|
|
45
|
+
warnings.append(
|
|
46
|
+
"Fira Code font not found. Install it for best results:\n"
|
|
47
|
+
" macOS: brew install --cask font-fira-code\n"
|
|
48
|
+
" Ubuntu: sudo apt install fonts-firacode\n"
|
|
49
|
+
" Or download from: https://github.com/tonsky/FiraCode"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
53
|
+
# Can't check fonts, skip warning
|
|
54
|
+
pass
|
|
55
|
+
else:
|
|
56
|
+
# No fontconfig available (Windows or minimal system)
|
|
57
|
+
# We can't easily check fonts, so just note the requirement
|
|
58
|
+
warnings.append(
|
|
59
|
+
"Cannot verify font availability. For correct alignment, ensure "
|
|
60
|
+
"Fira Code font is installed."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return warnings
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def print_font_warnings(warnings: list[str]) -> None:
|
|
67
|
+
"""Print font warnings to stderr."""
|
|
68
|
+
if warnings:
|
|
69
|
+
print("\n⚠️ Font Warning:", file=sys.stderr)
|
|
70
|
+
for warning in warnings:
|
|
71
|
+
for line in warning.split("\n"):
|
|
72
|
+
print(f" {line}", file=sys.stderr)
|
|
73
|
+
print(
|
|
74
|
+
"\n Without proper fonts, column alignment may be incorrect in exports.",
|
|
75
|
+
file=sys.stderr,
|
|
76
|
+
)
|
|
77
|
+
print(file=sys.stderr)
|
prezo/export/html.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""HTML export functionality."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from prezo.parser import clean_marp_directives, extract_notes, parse_presentation
|
|
8
|
+
from prezo.themes import get_theme
|
|
9
|
+
|
|
10
|
+
from .common import EXIT_FAILURE, EXIT_SUCCESS, ExportError
|
|
11
|
+
|
|
12
|
+
# HTML export templates
|
|
13
|
+
HTML_TEMPLATE = """\
|
|
14
|
+
<!DOCTYPE html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="UTF-8">
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
<title>{title}</title>
|
|
20
|
+
<style>
|
|
21
|
+
* {{
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
}}
|
|
26
|
+
body {{
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
28
|
+
background: {background};
|
|
29
|
+
color: {text};
|
|
30
|
+
min-height: 100vh;
|
|
31
|
+
}}
|
|
32
|
+
.slides {{
|
|
33
|
+
max-width: 1200px;
|
|
34
|
+
margin: 0 auto;
|
|
35
|
+
padding: 2rem;
|
|
36
|
+
}}
|
|
37
|
+
.slide {{
|
|
38
|
+
background: {surface};
|
|
39
|
+
border: 1px solid {border};
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
padding: 3rem 4rem;
|
|
42
|
+
margin-bottom: 3rem;
|
|
43
|
+
min-height: 70vh;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
page-break-after: always;
|
|
47
|
+
}}
|
|
48
|
+
.slide-number {{
|
|
49
|
+
color: {text_muted};
|
|
50
|
+
font-size: 0.9rem;
|
|
51
|
+
margin-bottom: 1rem;
|
|
52
|
+
padding-bottom: 0.5rem;
|
|
53
|
+
border-bottom: 1px solid {border};
|
|
54
|
+
}}
|
|
55
|
+
.slide-content {{
|
|
56
|
+
flex: 1;
|
|
57
|
+
}}
|
|
58
|
+
h1 {{
|
|
59
|
+
font-size: 2.5rem;
|
|
60
|
+
margin-bottom: 1.5rem;
|
|
61
|
+
color: {primary};
|
|
62
|
+
}}
|
|
63
|
+
h2 {{
|
|
64
|
+
font-size: 2rem;
|
|
65
|
+
margin-bottom: 1.2rem;
|
|
66
|
+
color: {primary};
|
|
67
|
+
}}
|
|
68
|
+
h3 {{
|
|
69
|
+
font-size: 1.5rem;
|
|
70
|
+
margin-bottom: 1rem;
|
|
71
|
+
color: {text};
|
|
72
|
+
}}
|
|
73
|
+
p {{
|
|
74
|
+
font-size: 1.2rem;
|
|
75
|
+
line-height: 1.6;
|
|
76
|
+
margin-bottom: 1rem;
|
|
77
|
+
}}
|
|
78
|
+
ul, ol {{
|
|
79
|
+
margin: 1rem 0;
|
|
80
|
+
padding-left: 2rem;
|
|
81
|
+
}}
|
|
82
|
+
li {{
|
|
83
|
+
font-size: 1.2rem;
|
|
84
|
+
line-height: 1.6;
|
|
85
|
+
margin-bottom: 0.5rem;
|
|
86
|
+
}}
|
|
87
|
+
pre {{
|
|
88
|
+
background: {background};
|
|
89
|
+
border-radius: 4px;
|
|
90
|
+
padding: 1rem;
|
|
91
|
+
overflow-x: auto;
|
|
92
|
+
font-family: 'Fira Code', 'Consolas', monospace;
|
|
93
|
+
font-size: 1rem;
|
|
94
|
+
margin: 1rem 0;
|
|
95
|
+
}}
|
|
96
|
+
code {{
|
|
97
|
+
font-family: 'Fira Code', 'Consolas', monospace;
|
|
98
|
+
background: {background};
|
|
99
|
+
padding: 0.2rem 0.4rem;
|
|
100
|
+
border-radius: 3px;
|
|
101
|
+
font-size: 0.95em;
|
|
102
|
+
}}
|
|
103
|
+
pre code {{
|
|
104
|
+
padding: 0;
|
|
105
|
+
background: none;
|
|
106
|
+
}}
|
|
107
|
+
table {{
|
|
108
|
+
width: 100%;
|
|
109
|
+
border-collapse: collapse;
|
|
110
|
+
margin: 1rem 0;
|
|
111
|
+
}}
|
|
112
|
+
th, td {{
|
|
113
|
+
border: 1px solid {border};
|
|
114
|
+
padding: 0.75rem;
|
|
115
|
+
text-align: left;
|
|
116
|
+
}}
|
|
117
|
+
th {{
|
|
118
|
+
background: {background};
|
|
119
|
+
}}
|
|
120
|
+
blockquote {{
|
|
121
|
+
border-left: 4px solid {primary};
|
|
122
|
+
padding-left: 1rem;
|
|
123
|
+
margin: 1rem 0;
|
|
124
|
+
font-style: italic;
|
|
125
|
+
color: {text_muted};
|
|
126
|
+
}}
|
|
127
|
+
a {{
|
|
128
|
+
color: {primary};
|
|
129
|
+
}}
|
|
130
|
+
img {{
|
|
131
|
+
max-width: 100%;
|
|
132
|
+
height: auto;
|
|
133
|
+
}}
|
|
134
|
+
/* Multi-column layouts */
|
|
135
|
+
.columns {{
|
|
136
|
+
display: flex;
|
|
137
|
+
gap: 2rem;
|
|
138
|
+
align-items: flex-start;
|
|
139
|
+
}}
|
|
140
|
+
.columns > div {{
|
|
141
|
+
flex: 1;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
}}
|
|
144
|
+
.notes {{
|
|
145
|
+
margin-top: 2rem;
|
|
146
|
+
padding: 1rem;
|
|
147
|
+
background: {background};
|
|
148
|
+
border-radius: 4px;
|
|
149
|
+
font-size: 0.9rem;
|
|
150
|
+
color: {text_muted};
|
|
151
|
+
}}
|
|
152
|
+
.notes-title {{
|
|
153
|
+
font-weight: bold;
|
|
154
|
+
margin-bottom: 0.5rem;
|
|
155
|
+
}}
|
|
156
|
+
@media print {{
|
|
157
|
+
.slide {{
|
|
158
|
+
break-inside: avoid;
|
|
159
|
+
page-break-inside: avoid;
|
|
160
|
+
}}
|
|
161
|
+
body {{
|
|
162
|
+
background: white;
|
|
163
|
+
color: black;
|
|
164
|
+
}}
|
|
165
|
+
}}
|
|
166
|
+
</style>
|
|
167
|
+
</head>
|
|
168
|
+
<body>
|
|
169
|
+
<div class="slides">
|
|
170
|
+
{slides}
|
|
171
|
+
</div>
|
|
172
|
+
</body>
|
|
173
|
+
</html>
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
SLIDE_TEMPLATE = """\
|
|
177
|
+
<div class="slide" id="slide-{num}">
|
|
178
|
+
<div class="slide-number">Slide {display_num} of {total}</div>
|
|
179
|
+
<div class="slide-content">
|
|
180
|
+
{content}
|
|
181
|
+
</div>
|
|
182
|
+
{notes}
|
|
183
|
+
</div>
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
NOTES_TEMPLATE = """\
|
|
187
|
+
<div class="notes">
|
|
188
|
+
<div class="notes-title">Presenter Notes</div>
|
|
189
|
+
{notes_content}
|
|
190
|
+
</div>
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def render_slide_to_html(content: str) -> str:
|
|
195
|
+
"""Convert markdown content to basic HTML.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
content: Markdown content of the slide.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
HTML string for the slide content.
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
import markdown # noqa: PLC0415
|
|
206
|
+
|
|
207
|
+
html = markdown.markdown(
|
|
208
|
+
content,
|
|
209
|
+
extensions=["tables", "fenced_code", "codehilite"],
|
|
210
|
+
)
|
|
211
|
+
except ImportError:
|
|
212
|
+
# Fallback: basic markdown-to-html conversion
|
|
213
|
+
import html as html_mod # noqa: PLC0415
|
|
214
|
+
|
|
215
|
+
html = html_mod.escape(content)
|
|
216
|
+
# Basic transformations
|
|
217
|
+
html = html.replace("\n\n", "</p><p>")
|
|
218
|
+
html = f"<p>{html}</p>"
|
|
219
|
+
|
|
220
|
+
return html
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def export_to_html(
|
|
224
|
+
source: Path,
|
|
225
|
+
output: Path,
|
|
226
|
+
*,
|
|
227
|
+
theme: str = "dark",
|
|
228
|
+
include_notes: bool = False,
|
|
229
|
+
) -> Path:
|
|
230
|
+
"""Export presentation to HTML.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
source: Path to the markdown presentation.
|
|
234
|
+
output: Path for the output HTML file.
|
|
235
|
+
theme: Theme to use for styling.
|
|
236
|
+
include_notes: Whether to include presenter notes.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Path to the created HTML file.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
ExportError: If export fails.
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
if not source.exists():
|
|
246
|
+
msg = f"Source file not found: {source}"
|
|
247
|
+
raise ExportError(msg)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
presentation = parse_presentation(source)
|
|
251
|
+
except Exception as e:
|
|
252
|
+
msg = f"Failed to parse presentation: {e}"
|
|
253
|
+
raise ExportError(msg) from e
|
|
254
|
+
|
|
255
|
+
if presentation.total_slides == 0:
|
|
256
|
+
msg = "Presentation has no slides"
|
|
257
|
+
raise ExportError(msg)
|
|
258
|
+
|
|
259
|
+
theme_obj = get_theme(theme)
|
|
260
|
+
|
|
261
|
+
# Render each slide
|
|
262
|
+
slides_html = []
|
|
263
|
+
for i, slide in enumerate(presentation.slides):
|
|
264
|
+
# Use raw_content and clean with keep_divs=True to preserve column layouts
|
|
265
|
+
slide_content, _ = extract_notes(slide.raw_content)
|
|
266
|
+
cleaned_content = clean_marp_directives(slide_content, keep_divs=True)
|
|
267
|
+
content_html = render_slide_to_html(cleaned_content)
|
|
268
|
+
|
|
269
|
+
# Handle notes
|
|
270
|
+
notes_html = ""
|
|
271
|
+
if include_notes and slide.notes:
|
|
272
|
+
notes_content = render_slide_to_html(slide.notes)
|
|
273
|
+
notes_html = NOTES_TEMPLATE.format(notes_content=notes_content)
|
|
274
|
+
|
|
275
|
+
slide_html = SLIDE_TEMPLATE.format(
|
|
276
|
+
num=i,
|
|
277
|
+
display_num=i + 1,
|
|
278
|
+
total=presentation.total_slides,
|
|
279
|
+
content=content_html,
|
|
280
|
+
notes=notes_html,
|
|
281
|
+
)
|
|
282
|
+
slides_html.append(slide_html)
|
|
283
|
+
|
|
284
|
+
# Build final HTML
|
|
285
|
+
title = presentation.title or source.stem
|
|
286
|
+
html = HTML_TEMPLATE.format(
|
|
287
|
+
title=title,
|
|
288
|
+
background=theme_obj.background,
|
|
289
|
+
surface=theme_obj.surface,
|
|
290
|
+
text=theme_obj.text,
|
|
291
|
+
text_muted=theme_obj.text_muted,
|
|
292
|
+
primary=theme_obj.primary,
|
|
293
|
+
border=theme_obj.text_muted,
|
|
294
|
+
slides="\n".join(slides_html),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
output.write_text(html)
|
|
299
|
+
return output
|
|
300
|
+
except Exception as e:
|
|
301
|
+
msg = f"Failed to write HTML: {e}"
|
|
302
|
+
raise ExportError(msg) from e
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def run_html_export(
|
|
306
|
+
source: str,
|
|
307
|
+
output: str | None = None,
|
|
308
|
+
*,
|
|
309
|
+
theme: str = "light",
|
|
310
|
+
include_notes: bool = False,
|
|
311
|
+
) -> int:
|
|
312
|
+
"""Run HTML export from command line.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
source: Path to the markdown presentation (string).
|
|
316
|
+
output: Optional path for the output HTML (string).
|
|
317
|
+
theme: Theme to use for styling.
|
|
318
|
+
include_notes: Whether to include presenter notes.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Exit code (0 for success).
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
import sys # noqa: PLC0415
|
|
325
|
+
|
|
326
|
+
source_path = Path(source)
|
|
327
|
+
output_path = Path(output) if output else source_path.with_suffix(".html")
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
result_path = export_to_html(
|
|
331
|
+
source_path,
|
|
332
|
+
output_path,
|
|
333
|
+
theme=theme,
|
|
334
|
+
include_notes=include_notes,
|
|
335
|
+
)
|
|
336
|
+
print(f"Exported to {result_path}")
|
|
337
|
+
return EXIT_SUCCESS
|
|
338
|
+
except ExportError as e:
|
|
339
|
+
print(f"error: {e}", file=sys.stderr)
|
|
340
|
+
return EXIT_FAILURE
|