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/presenter.js ADDED
@@ -0,0 +1,400 @@
1
+ // ── Injected by server ──
2
+ const INITIAL_SLIDES = __SLIDES_JSON__;
3
+ const INITIAL_TRANSITIONS = __TRANSITIONS_JSON__;
4
+ const WS_PORT = __WS_PORT__;
5
+ const INITIAL_ERROR = __ERROR_JSON__;
6
+
7
+ // ── State ──
8
+ let slides = INITIAL_SLIDES;
9
+ let transitions = INITIAL_TRANSITIONS;
10
+ let slideIndex = 0;
11
+ let step = 0;
12
+ let _maxStepCache = null;
13
+ let gotoMode = false;
14
+ let gotoBuffer = '';
15
+
16
+ // ── DOM refs ──
17
+ const stage = document.getElementById('stage');
18
+ const slideInfo = document.getElementById('slide-info');
19
+ const stepInfo = document.getElementById('step-info');
20
+ const wsDot = document.getElementById('ws-dot');
21
+ const wsLabel = document.getElementById('ws-label');
22
+ const curtain = document.getElementById('curtain');
23
+ const help = document.getElementById('help');
24
+ const errorOverlay = document.getElementById('error-overlay');
25
+ const errorMsg = document.getElementById('error-msg');
26
+
27
+ // ── Helpers ──
28
+ function maxStep() {
29
+ if (_maxStepCache !== null) return _maxStepCache;
30
+ let m = 0;
31
+ stage.querySelectorAll('[data-step]').forEach(el => {
32
+ const s = +el.getAttribute('data-step');
33
+ if (s > m) m = s;
34
+ });
35
+ return (_maxStepCache = m);
36
+ }
37
+
38
+ function syncURL() {
39
+ const search = step > 0 ? `?clicks=${step}` : '';
40
+ try {
41
+ history.replaceState(null, '', `/${slideIndex + 1}${search}`);
42
+ } catch (_) {}
43
+ }
44
+
45
+ function readURL() {
46
+ const seg = window.location.pathname.replace(/^\//, '');
47
+ const n = parseInt(seg, 10);
48
+ if (!isNaN(n) && n >= 1 && n <= slides.length) slideIndex = n - 1;
49
+ const clicks = parseInt(new URLSearchParams(window.location.search).get('clicks') ?? '0', 10);
50
+ if (!isNaN(clicks) && clicks >= 0) step = clicks;
51
+ }
52
+
53
+ function updateStatus() {
54
+ slideInfo.textContent = gotoMode
55
+ ? `g: ${gotoBuffer}_`
56
+ : `${slideIndex + 1} / ${slides.length}`;
57
+ stepInfo.textContent = `step ${step}`;
58
+ syncURL();
59
+ }
60
+
61
+ // Toggle .active on already-loaded SVG elements — triggers CSS transitions.
62
+ // Never touches innerHTML, so transitions fire correctly.
63
+ function applyStep() {
64
+ stage.querySelectorAll('[data-step]').forEach(el =>
65
+ el.classList.toggle('active', +el.getAttribute('data-step') <= step)
66
+ );
67
+ updateStatus();
68
+ }
69
+
70
+ // ── Morph transition ──
71
+ // Drives matched IDs via a rAF loop that sets SVG geometry attributes directly in
72
+ // SVG user units — no CSS px ↔ SVG unit conversion, no coordinate space ambiguity.
73
+ // Unmatched new elements fade in; unmatched old elements disappear immediately.
74
+
75
+ function _geomAttrs(el) {
76
+ const g = k => parseFloat(el.getAttribute(k) ?? '0');
77
+ switch (el.tagName.toLowerCase()) {
78
+ case 'rect': return { x: g('x'), y: g('y'), width: g('width'), height: g('height'), rx: g('rx') };
79
+ case 'circle': return { cx: g('cx'), cy: g('cy'), r: g('r') };
80
+ case 'ellipse': return { cx: g('cx'), cy: g('cy'), rx: g('rx'), ry: g('ry') };
81
+ default: return null;
82
+ }
83
+ }
84
+
85
+ function _parseHexColor(s) {
86
+ const h = (s ?? '').replace('#', '');
87
+ if (h.length === 3) return h.split('').map(c => parseInt(c + c, 16));
88
+ if (h.length === 6) return [0, 2, 4].map(i => parseInt(h.slice(i, i + 2), 16));
89
+ return null;
90
+ }
91
+
92
+ function _lerpColor(a, b, t) {
93
+ const ca = _parseHexColor(a), cb = _parseHexColor(b);
94
+ if (!ca || !cb) return t < 0.5 ? a : b;
95
+ return '#' + ca.map((c, i) => Math.round(c + (cb[i] - c) * t).toString(16).padStart(2, '0')).join('');
96
+ }
97
+
98
+ function _ease(t) { return t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t + 2, 2) / 2; }
99
+
100
+ function morphSlide(duration, then) {
101
+ const ms = duration * 1000;
102
+
103
+ // 1. Snapshot old elements in SVG user units before swap
104
+ const fromMap = new Map();
105
+ stage.querySelectorAll('[id]').forEach(el =>
106
+ fromMap.set(el.id, {
107
+ tag: el.tagName.toLowerCase(),
108
+ geom: _geomAttrs(el),
109
+ fill: el.getAttribute('fill'),
110
+ stroke: el.getAttribute('stroke'),
111
+ })
112
+ );
113
+
114
+ // 2. Swap to new slide
115
+ stage.innerHTML = slides[slideIndex];
116
+ _maxStepCache = null;
117
+ const newSvg = stage.querySelector('svg');
118
+ if (!newSvg) { updateStatus(); if (then) then(); return; }
119
+ updateStatus();
120
+
121
+ // 3. Build task list; snap morph elements to old positions before first paint
122
+ const tasks = [];
123
+ const seenIds = new Set();
124
+ newSvg.querySelectorAll('[id]').forEach(el => {
125
+ seenIds.add(el.id);
126
+ const from = fromMap.get(el.id);
127
+ const toGeom = _geomAttrs(el);
128
+ if (from && from.geom && toGeom && from.tag === el.tagName.toLowerCase()) {
129
+ // Capture target color before overwriting with old values
130
+ const toFill = el.getAttribute('fill');
131
+ const toStroke = el.getAttribute('stroke');
132
+ // Snap to old geometry so first paint shows old position
133
+ for (const [k, v] of Object.entries(from.geom)) el.setAttribute(k, v);
134
+ if (from.fill) el.setAttribute('fill', from.fill);
135
+ if (from.stroke) el.setAttribute('stroke', from.stroke);
136
+ tasks.push({ type: 'morph', el, from, toGeom, toFill, toStroke });
137
+ } else if (!from) {
138
+ // New element — fade in
139
+ el.style.opacity = '0';
140
+ tasks.push({ type: 'fade', el, toOpacity: parseFloat(el.getAttribute('opacity') ?? '1') });
141
+ }
142
+ // matched but unmorphable (text, group, path) → instant cut, leave as-is
143
+ });
144
+
145
+ // Exit elements: had geometry on old slide, absent from new — ghost them in and fade out
146
+ for (const [id, from] of fromMap) {
147
+ if (seenIds.has(id) || !from.geom) continue;
148
+ const ghost = document.createElementNS('http://www.w3.org/2000/svg', from.tag);
149
+ for (const [k, v] of Object.entries(from.geom)) ghost.setAttribute(k, v);
150
+ if (from.fill) ghost.setAttribute('fill', from.fill);
151
+ if (from.stroke) ghost.setAttribute('stroke', from.stroke);
152
+ newSvg.appendChild(ghost);
153
+ tasks.push({ type: 'exit', el: ghost });
154
+ }
155
+
156
+ // 4. Drive animation via requestAnimationFrame
157
+ const t0 = performance.now();
158
+ function frame(now) {
159
+ const raw = Math.min((now - t0) / ms, 1);
160
+ const e = _ease(raw);
161
+ for (const task of tasks) {
162
+ if (task.type === 'morph') {
163
+ for (const k of Object.keys(task.toGeom))
164
+ task.el.setAttribute(k, task.from.geom[k] + (task.toGeom[k] - task.from.geom[k]) * e);
165
+ if (task.from.fill && task.toFill) task.el.setAttribute('fill', _lerpColor(task.from.fill, task.toFill, e));
166
+ if (task.from.stroke && task.toStroke) task.el.setAttribute('stroke', _lerpColor(task.from.stroke, task.toStroke, e));
167
+ } else if (task.type === 'exit') {
168
+ task.el.style.opacity = String(1 - _ease(Math.min(raw / 0.7, 1)));
169
+ } else {
170
+ task.el.style.opacity = String(_ease(Math.max(0, Math.min((raw - 0.3) / 0.5, 1))) * task.toOpacity);
171
+ }
172
+ }
173
+ if (raw < 1) { requestAnimationFrame(frame); return; }
174
+ // Restore final attribute state cleanly
175
+ for (const task of tasks) {
176
+ if (task.type === 'morph') {
177
+ for (const [k, v] of Object.entries(task.toGeom)) task.el.setAttribute(k, v);
178
+ if (task.toFill) task.el.setAttribute('fill', task.toFill);
179
+ if (task.toStroke) task.el.setAttribute('stroke', task.toStroke);
180
+ } else if (task.type === 'exit') {
181
+ task.el.remove();
182
+ } else {
183
+ task.el.style.opacity = '';
184
+ }
185
+ }
186
+ if (then) then();
187
+ }
188
+ requestAnimationFrame(frame);
189
+ }
190
+
191
+ const TRANSITIONS = {
192
+ morph(swap, t, then) {
193
+ if (t.duration > 0 && slides.length) { morphSlide(t.duration, then); return; }
194
+ swap(); if (then) then();
195
+ },
196
+ crossfade(swap, t, then) {
197
+ if (t.duration <= 0) { swap(); if (then) then(); return; }
198
+ stage.style.transition = `opacity ${t.duration}s ease`;
199
+ stage.style.opacity = '0';
200
+ setTimeout(() => {
201
+ swap();
202
+ requestAnimationFrame(() => { stage.style.opacity = '1'; if (then) then(); });
203
+ }, t.duration * 1000);
204
+ },
205
+ };
206
+
207
+ // Replace innerHTML with new slide content. Does NOT call applyStep() —
208
+ // elements start in their pre-transition state so the next advance() triggers
209
+ // a real animated transition. Optional `then` runs after content is swapped.
210
+ // Pass `transition` to override the destination slide's declared transition (used
211
+ // when navigating backward so the outgoing slide's transition plays in reverse).
212
+ function loadSlide(then = null, transition = null) {
213
+ const swap = () => {
214
+ stage.innerHTML = slides.length ? slides[slideIndex] : '<p style="color:var(--accent);padding:2rem">No slides.</p>';
215
+ _maxStepCache = null;
216
+ updateStatus();
217
+ };
218
+
219
+ const t = transition ?? transitions[slideIndex] ?? { type: 'cut', duration: 0 };
220
+ const handler = TRANSITIONS[t.type];
221
+ if (handler) { handler(swap, t, then); return; }
222
+
223
+ stage.style.transition = 'none';
224
+ stage.style.opacity = '1';
225
+ swap();
226
+ if (then) then();
227
+ }
228
+
229
+ // ── Navigation ──
230
+ function advance() {
231
+ if (step < maxStep()) {
232
+ step++;
233
+ applyStep();
234
+ } else if (slideIndex < slides.length - 1) {
235
+ slideIndex++;
236
+ step = 0;
237
+ loadSlide();
238
+ }
239
+ }
240
+
241
+ function retreat() {
242
+ if (step > 0) {
243
+ step--;
244
+ applyStep();
245
+ } else if (slideIndex > 0) {
246
+ const t = transitions[slideIndex];
247
+ slideIndex--;
248
+ step = 0;
249
+ loadSlide(() => { step = maxStep(); applyStep(); }, t);
250
+ }
251
+ }
252
+
253
+ function nextSlide() {
254
+ if (slideIndex < slides.length - 1) { slideIndex++; step = 0; loadSlide(); }
255
+ }
256
+
257
+ function prevSlide() {
258
+ if (slideIndex > 0) {
259
+ const t = transitions[slideIndex];
260
+ slideIndex--;
261
+ step = 0;
262
+ loadSlide(null, t);
263
+ }
264
+ }
265
+
266
+ function toggleFullscreen() {
267
+ if (!document.fullscreenElement) document.documentElement.requestFullscreen();
268
+ else document.exitFullscreen();
269
+ }
270
+
271
+ function showCurtain(color) { curtain.style.background = color; curtain.classList.add('visible'); }
272
+ function hideCurtain() { curtain.classList.remove('visible'); }
273
+ function toggleCurtain(color) {
274
+ curtain.classList.contains('visible') ? hideCurtain() : showCurtain(color);
275
+ }
276
+
277
+ function toggleHelp() { help.classList.toggle('visible'); }
278
+
279
+ function gotoFirst() { slideIndex = 0; step = 0; loadSlide(); }
280
+ function gotoLast() { slideIndex = slides.length - 1; step = 0; loadSlide(); }
281
+
282
+ // ── Error display ──
283
+ function showError(msg) {
284
+ errorMsg.textContent = msg;
285
+ errorOverlay.classList.add('visible');
286
+ }
287
+ function hideError() { errorOverlay.classList.remove('visible'); }
288
+
289
+ // ── Go-to-slide ──
290
+ function enterGoto() { gotoMode = true; gotoBuffer = ''; updateStatus(); }
291
+ function exitGoto() { gotoMode = false; gotoBuffer = ''; updateStatus(); }
292
+ function commitGoto() {
293
+ const n = parseInt(gotoBuffer, 10);
294
+ exitGoto();
295
+ if (!isNaN(n) && n >= 1 && n <= slides.length) {
296
+ slideIndex = n - 1;
297
+ step = 0;
298
+ loadSlide();
299
+ }
300
+ }
301
+
302
+ function toggleTheme() {
303
+ const html = document.documentElement;
304
+ html.dataset.theme = html.dataset.theme === 'light' ? '' : 'light';
305
+ }
306
+
307
+ curtain.addEventListener('click', hideCurtain);
308
+ help.addEventListener('click', e => { if (e.target === help) toggleHelp(); });
309
+ stage.addEventListener('click', advance);
310
+
311
+ // ── Keybindings ──
312
+ // To make keys configurable via deck.py in the future, merge a server-injected
313
+ // KEYBINDINGS_OVERRIDES object into this map before the listener runs.
314
+ const KEYBINDINGS = {
315
+ 'ArrowRight': { action: advance, preventDefault: true },
316
+ ' ': { action: advance, preventDefault: true },
317
+ 'l': { action: advance, preventDefault: true },
318
+ 'ArrowLeft': { action: retreat, preventDefault: true },
319
+ 'Backspace': { action: retreat, preventDefault: true },
320
+ 'h': { action: retreat, preventDefault: true },
321
+ 'ArrowDown': { action: nextSlide, preventDefault: true },
322
+ 'j': { action: nextSlide, preventDefault: true },
323
+ 'ArrowUp': { action: prevSlide, preventDefault: true },
324
+ 'k': { action: prevSlide, preventDefault: true },
325
+ 'Home': { action: gotoFirst },
326
+ '^': { action: gotoFirst },
327
+ 'End': { action: gotoLast },
328
+ '$': { action: gotoLast },
329
+ 'g': { action: enterGoto },
330
+ 'f': { action: toggleFullscreen },
331
+ 'b': { action: () => toggleCurtain('black') },
332
+ '.': { action: () => toggleCurtain('black') },
333
+ 'w': { action: () => toggleCurtain('white') },
334
+ '?': { action: toggleHelp },
335
+ 't': { action: toggleTheme },
336
+ };
337
+
338
+ document.addEventListener('keydown', e => {
339
+ if (help.classList.contains('visible')) {
340
+ if (e.key === '?' || e.key === 'Escape') toggleHelp();
341
+ return;
342
+ }
343
+ if (curtain.classList.contains('visible')) { hideCurtain(); return; }
344
+
345
+ if (gotoMode) {
346
+ if (e.key >= '0' && e.key <= '9') { gotoBuffer += e.key; updateStatus(); }
347
+ else if (e.key === 'Enter') { e.preventDefault(); commitGoto(); }
348
+ else if (e.key === 'Backspace') { e.preventDefault(); gotoBuffer = gotoBuffer.slice(0, -1); updateStatus(); }
349
+ else if (e.key === 'Escape') { exitGoto(); }
350
+ return;
351
+ }
352
+
353
+ const binding = KEYBINDINGS[e.key];
354
+ if (binding) {
355
+ if (binding.preventDefault) e.preventDefault();
356
+ binding.action();
357
+ }
358
+ });
359
+
360
+ // ── WebSocket live reload ──
361
+ function connectWS() {
362
+ if (!WS_PORT) {
363
+ wsLabel.textContent = 'offline';
364
+ return;
365
+ }
366
+ const ws = new WebSocket(`ws://localhost:${WS_PORT}`);
367
+
368
+ ws.onopen = () => {
369
+ wsDot.className = 'connected';
370
+ wsLabel.textContent = 'live';
371
+ };
372
+
373
+ ws.onmessage = (ev) => {
374
+ const msg = JSON.parse(ev.data);
375
+ if (msg.type === 'update') {
376
+ slides = msg.slides;
377
+ transitions = msg.transitions ?? [];
378
+ hideError();
379
+ slideIndex = Math.min(slideIndex, Math.max(0, slides.length - 1));
380
+ step = 0;
381
+ loadSlide();
382
+ } else if (msg.type === 'error') {
383
+ showError(msg.message);
384
+ }
385
+ };
386
+
387
+ ws.onclose = () => {
388
+ wsDot.className = '';
389
+ wsLabel.textContent = 'disconnected';
390
+ setTimeout(connectWS, 2000);
391
+ };
392
+
393
+ ws.onerror = () => ws.close();
394
+ }
395
+
396
+ // ── Boot ──
397
+ readURL();
398
+ loadSlide(() => { if (step > 0) applyStep(); });
399
+ if (INITIAL_ERROR) showError(INITIAL_ERROR);
400
+ connectWS();