inkflow 0.1.0.dev0__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.
Files changed (48) hide show
  1. inkflow/__init__.py +31 -0
  2. inkflow/cli.py +266 -0
  3. inkflow/content.py +227 -0
  4. inkflow/export.py +123 -0
  5. inkflow/layout.py +279 -0
  6. inkflow/manifest.py +145 -0
  7. inkflow/markdown.py +158 -0
  8. inkflow/ns.py +9 -0
  9. inkflow/pdf.html +19 -0
  10. inkflow/pipeline.py +244 -0
  11. inkflow/presenter.css +196 -0
  12. inkflow/presenter.html +50 -0
  13. inkflow/presenter.js +400 -0
  14. inkflow/server.py +380 -0
  15. inkflow/theme/layouts/center.svg +3 -0
  16. inkflow/theme/layouts/cover.svg +5 -0
  17. inkflow/theme/layouts/default.svg +4 -0
  18. inkflow/theme/layouts/end.svg +4 -0
  19. inkflow/theme/layouts/fact.svg +4 -0
  20. inkflow/theme/layouts/media-left.svg +5 -0
  21. inkflow/theme/layouts/media-right.svg +5 -0
  22. inkflow/theme/layouts/quote.svg +4 -0
  23. inkflow/theme/layouts/section.svg +4 -0
  24. inkflow/theme/layouts/statement.svg +3 -0
  25. inkflow/theme/layouts/two-cols-header.svg +5 -0
  26. inkflow/theme/layouts/two-cols.svg +5 -0
  27. inkflow/theme/main.svg +3 -0
  28. inkflow/theme/numbered-main.svg +6 -0
  29. inkflow/theme/showcase/deck.py +18 -0
  30. inkflow/theme/showcase/slides/01-cover.md +7 -0
  31. inkflow/theme/showcase/slides/02-section.md +7 -0
  32. inkflow/theme/showcase/slides/03-default.md +11 -0
  33. inkflow/theme/showcase/slides/04-center.md +8 -0
  34. inkflow/theme/showcase/slides/05-two-cols.md +17 -0
  35. inkflow/theme/showcase/slides/06-two-cols-header.md +13 -0
  36. inkflow/theme/showcase/slides/07-fact.md +7 -0
  37. inkflow/theme/showcase/slides/08-quote.md +7 -0
  38. inkflow/theme/showcase/slides/09-statement.md +3 -0
  39. inkflow/theme/showcase/slides/10-media-left.md +15 -0
  40. inkflow/theme/showcase/slides/11-media-right.md +16 -0
  41. inkflow/theme/showcase/slides/12-end.md +3 -0
  42. inkflow/theme/styles.css +55 -0
  43. inkflow/tui.py +128 -0
  44. inkflow-0.1.0.dev0.dist-info/METADATA +12 -0
  45. inkflow-0.1.0.dev0.dist-info/RECORD +48 -0
  46. inkflow-0.1.0.dev0.dist-info/WHEEL +4 -0
  47. inkflow-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  48. inkflow-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
inkflow/pipeline.py ADDED
@@ -0,0 +1,244 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from lxml import etree
6
+
7
+ from inkflow import ns
8
+ from inkflow.content import (
9
+ inject_style,
10
+ remove_unreferenced_zones,
11
+ substitute_content,
12
+ substitute_zone_numbers,
13
+ )
14
+ from inkflow.layout import resolve_chain, resolve_parent_path, strip_layout_layers
15
+ from inkflow.manifest import (
16
+ Animation,
17
+ Bounce,
18
+ Deck,
19
+ FadeIn,
20
+ FadeOut,
21
+ MarkdownSlide,
22
+ Slide,
23
+ Transition,
24
+ )
25
+
26
+ # ── Path conventions ─────────────────────────────────────────────────────────
27
+
28
+
29
+ def resolve_slide_src(src: str, project_dir: Path) -> Path:
30
+ """Resolve a Slide.src string to an absolute Path.
31
+
32
+ Bare single-part names (no separator) are looked up in slides/.
33
+ Explicit paths (with / or absolute) are resolved relative to project_dir.
34
+ """
35
+ p = Path(src)
36
+ if p.is_absolute():
37
+ return p if p.suffix else p.with_suffix(".svg")
38
+ if len(p.parts) == 1:
39
+ name = p.stem if p.suffix else src
40
+ return project_dir / "slides" / (name + ".svg")
41
+ if not p.suffix:
42
+ p = p.with_suffix(".svg")
43
+ return (project_dir / p).resolve()
44
+
45
+
46
+ def _resolve_content_src(src: str, project_dir: Path) -> Path:
47
+ """Resolve a MarkdownSlide content path to an absolute Path.
48
+
49
+ Bare single-part names are looked up in slides/ with a .md suffix.
50
+ """
51
+ p = Path(src)
52
+ if p.is_absolute():
53
+ return p if p.suffix else p.with_suffix(".md")
54
+ if len(p.parts) == 1:
55
+ name = p.stem if p.suffix else src
56
+ return project_dir / "slides" / (name + ".md")
57
+ if not p.suffix:
58
+ p = p.with_suffix(".md")
59
+ return (project_dir / p).resolve()
60
+
61
+
62
+ # ── Animation classes ─────────────────────────────────────────────────────────
63
+
64
+ _ANIM_CLASS: dict[type, str] = {
65
+ FadeIn: "anim-fade-in",
66
+ FadeOut: "anim-fade-out",
67
+ Bounce: "anim-bounce",
68
+ }
69
+
70
+ _INKSCAPE_NAMESPACES: frozenset[str] = frozenset({ns.INKSCAPE, ns.SODIPODI})
71
+
72
+
73
+ def clean_inkscape_svg(src: Path) -> str:
74
+ tree = etree.parse(src)
75
+ root = tree.getroot()
76
+
77
+ for ns_uri in _INKSCAPE_NAMESPACES:
78
+ for el in root.findall(f".//{{{ns_uri}}}*"):
79
+ parent = el.getparent()
80
+ if parent is not None:
81
+ parent.remove(el)
82
+
83
+ for el in root.iter():
84
+ to_del = [
85
+ k
86
+ for k in el.attrib
87
+ if isinstance(k, str)
88
+ and any(k.startswith(f"{{{ns_uri}}}") for ns_uri in _INKSCAPE_NAMESPACES)
89
+ ]
90
+ for k in to_del:
91
+ del el.attrib[k]
92
+
93
+ etree.cleanup_namespaces(root)
94
+ return etree.tostring(root, encoding="unicode", pretty_print=True)
95
+
96
+
97
+ def annotate_svg(svg_str: str, animations: list[Animation]) -> str:
98
+ root = etree.fromstring(svg_str.encode())
99
+
100
+ for anim in animations:
101
+ css_class = _ANIM_CLASS.get(type(anim))
102
+ if css_class is None:
103
+ continue
104
+
105
+ eid = anim.element.lstrip("#")
106
+ el = root.find(f'.//*[@id="{eid}"]')
107
+ if el is None:
108
+ print(f"[inkflow] warning: element #{eid} not found in SVG")
109
+ continue
110
+
111
+ existing = el.get("class", "")
112
+ el.set("class", f"{existing} {css_class}".strip())
113
+ el.set("data-step", str(anim.step))
114
+
115
+ return etree.tostring(root, encoding="unicode")
116
+
117
+
118
+ def _serialize_transition(t: Transition | None) -> dict[str, object]:
119
+ if t is None:
120
+ return {"type": "cut", "duration": 0.0}
121
+ return {"type": type(t).__name__.lower(), **vars(t)}
122
+
123
+
124
+ def resolve_transitions(deck: Deck) -> list[dict[str, object]]:
125
+ return [
126
+ _serialize_transition(
127
+ slide.transition if slide.transition is not None else deck.transition
128
+ )
129
+ for slide in deck.slides
130
+ ]
131
+
132
+
133
+ def compose_with_ancestors(svg_str: str, chain: list[Path]) -> str:
134
+ """Prepend ancestor SVG content below the slide's own content."""
135
+ slide_root = etree.fromstring(svg_str.encode())
136
+ strip_layout_layers(slide_root)
137
+
138
+ ancestor_groups: list[etree._Element] = [] # pyright: ignore[reportPrivateUsage]
139
+ merged_defs: list[etree._Element] = [] # pyright: ignore[reportPrivateUsage]
140
+
141
+ for ancestor_path in chain:
142
+ anc_str = clean_inkscape_svg(ancestor_path)
143
+ anc_root = etree.fromstring(anc_str.encode())
144
+ strip_layout_layers(anc_root)
145
+
146
+ for defs_el in anc_root.findall(f"{{{ns.SVG}}}defs"):
147
+ merged_defs.extend(list(defs_el))
148
+
149
+ children = [el for el in anc_root if el.tag != f"{{{ns.SVG}}}defs"]
150
+ if children:
151
+ g = etree.Element(f"{{{ns.SVG}}}g")
152
+ for child in children:
153
+ g.append(child)
154
+ ancestor_groups.append(g)
155
+
156
+ if merged_defs:
157
+ slide_defs = slide_root.find(f"{{{ns.SVG}}}defs")
158
+ if slide_defs is None:
159
+ slide_defs = etree.Element(f"{{{ns.SVG}}}defs")
160
+ slide_root.insert(0, slide_defs)
161
+ for i, def_el in enumerate(merged_defs):
162
+ slide_defs.insert(i, def_el)
163
+
164
+ insert_pos = next(
165
+ (i + 1 for i, el in enumerate(slide_root) if el.tag == f"{{{ns.SVG}}}defs"),
166
+ 0,
167
+ )
168
+ for i, group in enumerate(ancestor_groups):
169
+ slide_root.insert(insert_pos + i, group)
170
+
171
+ return etree.tostring(slide_root, encoding="unicode")
172
+
173
+
174
+ def _resolve_markdown_slide(
175
+ ms: MarkdownSlide, project_dir: Path, theme: str | None
176
+ ) -> Slide:
177
+ from inkflow.markdown import build_slide_content
178
+
179
+ # Use project_dir as the synthetic svg_path parent so multi-part relative
180
+ # paths in the template string resolve relative to the project root.
181
+ template_path = resolve_parent_path(
182
+ ms.template, project_dir / "_", project_dir, theme
183
+ )
184
+ content_path = (
185
+ _resolve_content_src(ms.content, project_dir)
186
+ if ms.content is not None
187
+ else None
188
+ )
189
+ content = build_slide_content(content_path, ms.steps, ms._extra) # pyright: ignore[reportPrivateUsage]
190
+ return Slide(
191
+ src=str(template_path),
192
+ animations=ms.animations,
193
+ content=content,
194
+ transition=ms.transition,
195
+ style=ms.style,
196
+ )
197
+
198
+
199
+ def process_slide(
200
+ slide: Slide,
201
+ project_dir: Path,
202
+ theme: str | None,
203
+ slide_number: int,
204
+ total_slides: int,
205
+ deck_style: str = "",
206
+ font_size: int = 36,
207
+ ) -> str:
208
+ src = resolve_slide_src(slide.src, project_dir)
209
+ svg_str = clean_inkscape_svg(src)
210
+ chain = resolve_chain(src, project_dir, theme)
211
+ if chain:
212
+ svg_str = compose_with_ancestors(svg_str, chain)
213
+ svg_str = substitute_zone_numbers(svg_str, slide_number, total_slides)
214
+ if slide.content:
215
+ svg_str = substitute_content(svg_str, slide.content, project_dir, font_size)
216
+ if slide.animations:
217
+ svg_str = annotate_svg(svg_str, slide.animations)
218
+ combined_css = "\n".join(filter(None, [deck_style, slide.style]))
219
+ if combined_css:
220
+ svg_str = inject_style(svg_str, combined_css)
221
+ svg_str = remove_unreferenced_zones(svg_str)
222
+ return svg_str
223
+
224
+
225
+ def process_deck(deck: Deck, project_dir: Path) -> list[str]:
226
+ total = len(deck.slides)
227
+ results: list[str] = []
228
+ for i, entry in enumerate(deck.slides):
229
+ if isinstance(entry, MarkdownSlide):
230
+ slide = _resolve_markdown_slide(entry, project_dir, deck.theme)
231
+ else:
232
+ slide = entry
233
+ results.append(
234
+ process_slide(
235
+ slide,
236
+ project_dir,
237
+ deck.theme,
238
+ i + 1,
239
+ total,
240
+ deck.style,
241
+ deck.font_size,
242
+ )
243
+ )
244
+ return results
inkflow/presenter.css ADDED
@@ -0,0 +1,196 @@
1
+ /* Catppuccin Mocha */
2
+ :root {
3
+ --bg: #1e1e2e;
4
+ --surface: #313244;
5
+ --overlay: #45475a;
6
+ --text: #cdd6f4;
7
+ --subtext: #a6adc8;
8
+ --accent: #cba6f7;
9
+ --green: #a6e3a1;
10
+ --red: #f38ba8;
11
+ --blue: #89b4fa;
12
+ }
13
+
14
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
15
+
16
+ body {
17
+ background: var(--bg);
18
+ color: var(--text);
19
+ font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
20
+ height: 100vh;
21
+ display: flex;
22
+ flex-direction: column;
23
+ overflow: hidden;
24
+ user-select: none;
25
+ }
26
+
27
+ #stage {
28
+ flex: 1;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ padding: 0.5rem;
33
+ min-height: 0;
34
+ }
35
+
36
+ #stage > svg,
37
+ #stage > :first-child {
38
+ max-width: 100%;
39
+ max-height: 100%;
40
+ width: auto;
41
+ height: auto;
42
+ }
43
+
44
+ #statusbar {
45
+ flex-shrink: 0;
46
+ background: var(--surface);
47
+ color: var(--subtext);
48
+ font-size: 0.7rem;
49
+ padding: 0.25rem 0.75rem;
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 1rem;
53
+ border-top: 1px solid var(--accent);
54
+ }
55
+
56
+ #statusbar .sep { color: var(--overlay); }
57
+
58
+ #ws-dot {
59
+ display: inline-block;
60
+ width: 6px;
61
+ height: 6px;
62
+ border-radius: 50%;
63
+ background: var(--red);
64
+ margin-right: 4px;
65
+ transition: background 0.3s;
66
+ }
67
+ #ws-dot.connected { background: var(--green); }
68
+
69
+ #hint {
70
+ margin-left: auto;
71
+ color: var(--overlay);
72
+ }
73
+
74
+ /* ── Curtain (blackout / whiteout) ── */
75
+ #curtain {
76
+ display: none;
77
+ position: fixed;
78
+ inset: 0;
79
+ z-index: 20;
80
+ cursor: pointer;
81
+ }
82
+ #curtain.visible { display: block; }
83
+
84
+ /* ── Help overlay ── */
85
+ #help {
86
+ display: none;
87
+ position: fixed;
88
+ inset: 0;
89
+ z-index: 30;
90
+ background: rgba(0, 0, 0, 0.6);
91
+ align-items: center;
92
+ justify-content: center;
93
+ }
94
+ #help.visible { display: flex; }
95
+
96
+ #help-box {
97
+ background: var(--surface);
98
+ border: 1px solid var(--accent);
99
+ border-radius: 8px;
100
+ padding: 1.5rem 2rem;
101
+ min-width: 26rem;
102
+ }
103
+
104
+ #help-box h2 {
105
+ color: var(--accent);
106
+ font-size: 0.85rem;
107
+ margin-bottom: 1rem;
108
+ letter-spacing: 0.05em;
109
+ }
110
+
111
+ #help-box table { border-collapse: collapse; width: 100%; }
112
+ #help-box td { padding: 0.2rem 0.5rem; font-size: 0.75rem; }
113
+ #help-box td:first-child { color: var(--accent); white-space: nowrap; text-align: right; }
114
+ #help-box td:last-child { color: var(--subtext); padding-left: 1rem; }
115
+
116
+ #help-box p {
117
+ margin-top: 1rem;
118
+ font-size: 0.7rem;
119
+ color: var(--overlay);
120
+ text-align: center;
121
+ }
122
+
123
+ kbd {
124
+ background: var(--overlay);
125
+ color: var(--text);
126
+ border-radius: 3px;
127
+ padding: 0.1em 0.35em;
128
+ font-size: 0.85em;
129
+ }
130
+
131
+ /* ── Error overlay ── */
132
+ #error-overlay {
133
+ display: none;
134
+ position: fixed;
135
+ inset: 0;
136
+ z-index: 50;
137
+ background: rgba(30, 30, 46, 0.95);
138
+ align-items: flex-start;
139
+ justify-content: center;
140
+ padding: 3rem 2rem;
141
+ overflow-y: auto;
142
+ }
143
+ #error-overlay.visible { display: flex; }
144
+
145
+ #error-box {
146
+ background: var(--surface);
147
+ border: 1px solid var(--red);
148
+ border-radius: 8px;
149
+ padding: 1.5rem 2rem;
150
+ max-width: 72rem;
151
+ width: 100%;
152
+ }
153
+
154
+ #error-box h2 {
155
+ color: var(--red);
156
+ font-size: 0.85rem;
157
+ margin-bottom: 1rem;
158
+ letter-spacing: 0.05em;
159
+ }
160
+
161
+ #error-box pre {
162
+ color: var(--text);
163
+ font-size: 0.75rem;
164
+ white-space: pre-wrap;
165
+ word-break: break-all;
166
+ line-height: 1.6;
167
+ }
168
+
169
+ /* ── Animation classes ── */
170
+
171
+ .anim-fade-in {
172
+ opacity: 0;
173
+ transition: opacity 0.4s ease;
174
+ }
175
+ .anim-fade-in.active {
176
+ opacity: 1;
177
+ }
178
+
179
+ .anim-fade-out {
180
+ opacity: 1;
181
+ transition: opacity 0.4s ease;
182
+ }
183
+ .anim-fade-out.active {
184
+ opacity: 0;
185
+ }
186
+
187
+ /* Bounce-in from below */
188
+ .anim-bounce {
189
+ opacity: 0;
190
+ transform: translateY(14px);
191
+ transition: opacity 0.35s ease, transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
192
+ }
193
+ .anim-bounce.active {
194
+ opacity: 1;
195
+ transform: translateY(0);
196
+ }
inkflow/presenter.html ADDED
@@ -0,0 +1,50 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="__DATA_THEME__">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Inkflow</title>
7
+ <style>__STYLES__</style>
8
+ <style>__CSS__</style>
9
+ </head>
10
+ <body>
11
+ <div id="error-overlay">
12
+ <div id="error-box">
13
+ <h2>BUILD ERROR</h2>
14
+ <pre id="error-msg"></pre>
15
+ </div>
16
+ </div>
17
+ <div id="curtain"></div>
18
+ <div id="help" role="dialog" aria-modal="true">
19
+ <div id="help-box">
20
+ <h2>KEYBINDINGS</h2>
21
+ <table>
22
+ <tr><td>→ Space l</td><td>Advance step / next slide</td></tr>
23
+ <tr><td>← Backspace h</td><td>Back one step / previous slide</td></tr>
24
+ <tr><td>↓ j</td><td>Next slide (skip steps)</td></tr>
25
+ <tr><td>↑ k</td><td>Previous slide (skip steps)</td></tr>
26
+ <tr><td>^ Home</td><td>First slide</td></tr>
27
+ <tr><td>$ End</td><td>Last slide</td></tr>
28
+ <tr><td>g 12 ↵</td><td>Go to slide 12</td></tr>
29
+ <tr><td>b .</td><td>Blackout</td></tr>
30
+ <tr><td>w</td><td>Whiteout</td></tr>
31
+ <tr><td>f</td><td>Toggle fullscreen</td></tr>
32
+ <tr><td>t</td><td>Toggle dark / light mode</td></tr>
33
+ <tr><td>?</td><td>This help</td></tr>
34
+ </table>
35
+ <p>Press <kbd>?</kbd> or <kbd>Esc</kbd> to close</p>
36
+ </div>
37
+ </div>
38
+ <div id="stage"></div>
39
+ <div id="statusbar">
40
+ <span id="slide-info">– / –</span>
41
+ <span class="sep">|</span>
42
+ <span id="step-info">step –</span>
43
+ <span class="sep">|</span>
44
+ <span><span id="ws-dot"></span><span id="ws-label">disconnected</span></span>
45
+ <span id="hint">← → Space · navigate · g goto · ? help</span>
46
+ </div>
47
+
48
+ <script>__JS__</script>
49
+ </body>
50
+ </html>