inkflow 0.1.0.dev0__tar.gz
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.
- inkflow-0.1.0.dev0/.claude/hooks/ruff-format.sh +8 -0
- inkflow-0.1.0.dev0/.claude/settings.json +15 -0
- inkflow-0.1.0.dev0/.gitattributes +1 -0
- inkflow-0.1.0.dev0/.githooks/pre-commit +13 -0
- inkflow-0.1.0.dev0/.github/workflows/ci.yml +22 -0
- inkflow-0.1.0.dev0/.github/workflows/release.yml +33 -0
- inkflow-0.1.0.dev0/.gitignore +7 -0
- inkflow-0.1.0.dev0/CLAUDE.md +82 -0
- inkflow-0.1.0.dev0/LICENSE +21 -0
- inkflow-0.1.0.dev0/PKG-INFO +12 -0
- inkflow-0.1.0.dev0/README.md +116 -0
- inkflow-0.1.0.dev0/ROADMAP.md +245 -0
- inkflow-0.1.0.dev0/docs/assets/logo-dark-icon.svg +17 -0
- inkflow-0.1.0.dev0/docs/assets/logo-dark-landscape.svg +18 -0
- inkflow-0.1.0.dev0/docs/assets/logo-dark-portrait.svg +17 -0
- inkflow-0.1.0.dev0/docs/assets/logo-light-icon.svg +17 -0
- inkflow-0.1.0.dev0/docs/assets/logo-light-landscape.svg +17 -0
- inkflow-0.1.0.dev0/docs/assets/logo-light-portrait.svg +17 -0
- inkflow-0.1.0.dev0/example/assets/demo.jpg +0 -0
- inkflow-0.1.0.dev0/example/assets/demo.mp4 +0 -0
- inkflow-0.1.0.dev0/example/deck.py +57 -0
- inkflow-0.1.0.dev0/example/layouts/content.svg +14 -0
- inkflow-0.1.0.dev0/example/layouts/main.svg +13 -0
- inkflow-0.1.0.dev0/example/layouts/media-right.svg +18 -0
- inkflow-0.1.0.dev0/example/slides/01-title.svg +15 -0
- inkflow-0.1.0.dev0/example/slides/02-diagram.svg +40 -0
- inkflow-0.1.0.dev0/example/slides/03-crossfade.svg +17 -0
- inkflow-0.1.0.dev0/example/slides/04-morph.svg +17 -0
- inkflow-0.1.0.dev0/example/slides/05-markdown.md +13 -0
- inkflow-0.1.0.dev0/example/slides/06-image.md +12 -0
- inkflow-0.1.0.dev0/example/slides/07-video.md +11 -0
- inkflow-0.1.0.dev0/pyproject.toml +49 -0
- inkflow-0.1.0.dev0/src/inkflow/__init__.py +31 -0
- inkflow-0.1.0.dev0/src/inkflow/cli.py +266 -0
- inkflow-0.1.0.dev0/src/inkflow/content.py +227 -0
- inkflow-0.1.0.dev0/src/inkflow/export.py +123 -0
- inkflow-0.1.0.dev0/src/inkflow/layout.py +279 -0
- inkflow-0.1.0.dev0/src/inkflow/manifest.py +145 -0
- inkflow-0.1.0.dev0/src/inkflow/markdown.py +158 -0
- inkflow-0.1.0.dev0/src/inkflow/ns.py +9 -0
- inkflow-0.1.0.dev0/src/inkflow/pdf.html +19 -0
- inkflow-0.1.0.dev0/src/inkflow/pipeline.py +244 -0
- inkflow-0.1.0.dev0/src/inkflow/presenter.css +196 -0
- inkflow-0.1.0.dev0/src/inkflow/presenter.html +50 -0
- inkflow-0.1.0.dev0/src/inkflow/presenter.js +400 -0
- inkflow-0.1.0.dev0/src/inkflow/server.py +380 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/center.svg +3 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/cover.svg +5 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/default.svg +4 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/end.svg +4 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/fact.svg +4 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/media-left.svg +5 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/media-right.svg +5 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/quote.svg +4 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/section.svg +4 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/statement.svg +3 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/two-cols-header.svg +5 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/layouts/two-cols.svg +5 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/main.svg +3 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/numbered-main.svg +6 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/deck.py +18 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/01-cover.md +7 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/02-section.md +7 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/03-default.md +11 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/04-center.md +8 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/05-two-cols.md +17 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/06-two-cols-header.md +13 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/07-fact.md +7 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/08-quote.md +7 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/09-statement.md +3 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/10-media-left.md +15 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/11-media-right.md +16 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/showcase/slides/12-end.md +3 -0
- inkflow-0.1.0.dev0/src/inkflow/theme/styles.css +55 -0
- inkflow-0.1.0.dev0/src/inkflow/tui.py +128 -0
- inkflow-0.1.0.dev0/tests/__init__.py +0 -0
- inkflow-0.1.0.dev0/tests/test_content.py +211 -0
- inkflow-0.1.0.dev0/tests/test_layout.py +238 -0
- inkflow-0.1.0.dev0/tests/test_manifest.py +166 -0
- inkflow-0.1.0.dev0/tests/test_markdown.py +207 -0
- inkflow-0.1.0.dev0/tests/test_pipeline.py +300 -0
- inkflow-0.1.0.dev0/tests/test_server.py +147 -0
- inkflow-0.1.0.dev0/uv.lock +569 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.svg diff=inkscape-svg
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Strip Inkscape editor metadata from staged SVG files before committing.
|
|
3
|
+
# Install: git config core.hooksPath .githooks
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
mapfile -t staged < <(git diff --cached --name-only --diff-filter=ACM | grep -E '\.svg$' || true)
|
|
8
|
+
[ ${#staged[@]} -eq 0 ] && exit 0
|
|
9
|
+
|
|
10
|
+
echo "[inkflow] cleaning staged SVGs..."
|
|
11
|
+
uv run inkflow clean "${staged[@]}"
|
|
12
|
+
|
|
13
|
+
git add "${staged[@]}"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
check:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v6
|
|
11
|
+
|
|
12
|
+
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
|
|
13
|
+
|
|
14
|
+
- run: uv sync
|
|
15
|
+
|
|
16
|
+
- run: uv run ruff format --check src/ tests/
|
|
17
|
+
|
|
18
|
+
- run: uv run ruff check src/ tests/
|
|
19
|
+
|
|
20
|
+
- run: uv run basedpyright
|
|
21
|
+
|
|
22
|
+
- run: uv run pytest
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
release:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v6
|
|
16
|
+
|
|
17
|
+
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
|
|
18
|
+
|
|
19
|
+
- name: Check version matches tag
|
|
20
|
+
run: |
|
|
21
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
22
|
+
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
23
|
+
if [ "$TAG" != "$VERSION" ]; then
|
|
24
|
+
echo "Tag $GITHUB_REF_NAME does not match pyproject.toml version $VERSION"
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
- run: uv build
|
|
29
|
+
|
|
30
|
+
- name: Smoke test
|
|
31
|
+
run: uv run --with dist/*.whl --no-project -- inkflow build example/deck.py -o /tmp/inkflow-smoke
|
|
32
|
+
|
|
33
|
+
- run: uv publish
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Inkflow — codebase guide
|
|
2
|
+
|
|
3
|
+
## Running
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uv sync # install deps into .venv
|
|
7
|
+
uv run inkflow serve example/deck.py # start server at localhost:7777
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Git setup (one-time, per clone)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
uv run inkflow setup-git
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Configures two things:
|
|
17
|
+
- **Pre-commit hook** (`.githooks/pre-commit`) — strips Inkscape editor metadata from staged SVGs before every commit, so viewport pan/zoom/window state never lands in history
|
|
18
|
+
- **SVG diff driver** — `git diff`, `git log -p`, and GitHub's diff view show only visual changes even for SVGs that haven't been cleaned in-place
|
|
19
|
+
|
|
20
|
+
Git won't run this automatically on clone — that's an intentional git security boundary — so it needs to be run once. After that it's invisible.
|
|
21
|
+
|
|
22
|
+
SVG source files should be kept clean (no Inkscape metadata) in the repository. Run `uv run inkflow clean example/slides/*.svg` to clean any files committed before the hook was in place.
|
|
23
|
+
|
|
24
|
+
## Project layout
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
src/inkflow/
|
|
28
|
+
__init__.py exports: Deck, Slide, FadeIn, FadeOut, Bounce, Cut, Crossfade, Morph
|
|
29
|
+
manifest.py dataclasses for the deck DSL
|
|
30
|
+
pipeline.py SVG cleaning (lxml) + animation annotation
|
|
31
|
+
server.py HTTP server, WebSocket server, file watcher, build pipeline
|
|
32
|
+
cli.py CLI entry point
|
|
33
|
+
presenter.html shell template — inlined with CSS/JS at serve time
|
|
34
|
+
presenter.css presenter styles
|
|
35
|
+
presenter.js presenter logic (navigation, transitions, WS)
|
|
36
|
+
example/
|
|
37
|
+
deck.py 4-slide example deck
|
|
38
|
+
slides/ source SVGs (Inkscape-authored)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Key architecture decisions
|
|
42
|
+
|
|
43
|
+
**No Inkscape subprocess at serve time.** Inkscape is only used by the human author in the GUI. The pipeline reads SVG files directly with lxml, strips Inkscape/Sodipodi editor namespaces, and annotates elements with animation classes. This avoids GUI window flashes and is instant.
|
|
44
|
+
|
|
45
|
+
**Live reload pushes slides over WebSocket, not `location.reload()`.** When files change the server sends `{"type":"update","slides":[...],"transitions":[...]}` and the presenter swaps content in place, preserving the current slide index. Errors are sent as `{"type":"error","message":"..."}` and displayed as an overlay. The HTTP response includes `Cache-Control: no-store` so hard refreshes always get fresh content.
|
|
46
|
+
|
|
47
|
+
**`loadSlide()` vs `applyStep()` in the presenter JS.** Step advances within a slide must NOT re-render `stage.innerHTML` — that would make CSS transitions invisible because the browser only paints once per JS task. `loadSlide()` sets innerHTML (elements start at opacity 0). Subsequent `applyStep()` calls only toggle `.active` on existing DOM elements, triggering CSS transitions.
|
|
48
|
+
|
|
49
|
+
**`deck.py` is a Python module, not YAML/TOML.** Loaded via `importlib.util.spec_from_file_location`. Must define a module-level `deck` variable of type `Deck`.
|
|
50
|
+
|
|
51
|
+
**Morph transition uses a rAF loop over SVG attributes, not CSS transforms.** CSS `transform: translate(Xpx)` on SVG elements is interpreted in SVG user units, not CSS viewport pixels — FLIP-based approaches produce a coordinate gap proportional to the viewBox scale. Instead, `morphSlide()` snapshots raw geometry attributes (`x`, `y`, `width`, `height`, `rx` for rects; `cx`, `cy`, `r` for circles) before the innerHTML swap, then drives a `requestAnimationFrame` loop that calls `setAttribute` each frame in SVG user units. Colors are lerped channel-by-channel. Exit-only elements are reconstructed as ghost nodes in the new SVG and faded out. Backward navigation passes the outgoing slide's transition to `loadSlide()` so the morph plays in reverse.
|
|
52
|
+
|
|
53
|
+
**`presenter.html`/`css`/`js` are inlined at serve time.** `_build_html()` in `server.py` reads all three files and substitutes `__CSS__`, `__JS__`, `__SLIDES_JSON__`, `__TRANSITIONS_JSON__`, `__WS_PORT__`, `__ERROR_JSON__` tokens. Edit the source files and reload the browser to see changes.
|
|
54
|
+
|
|
55
|
+
## Server
|
|
56
|
+
|
|
57
|
+
- HTTP on port 7777 (asyncio streams, custom handler)
|
|
58
|
+
- WebSocket on port 7778 (websockets 16.0 — uses `websockets.asyncio.server.serve`, not the legacy `websockets.serve`)
|
|
59
|
+
- File watcher: `watchfiles.awatch` (async generator)
|
|
60
|
+
- Both run inside an `asyncio.TaskGroup`
|
|
61
|
+
|
|
62
|
+
## Animation pipeline
|
|
63
|
+
|
|
64
|
+
`pipeline.py`:
|
|
65
|
+
1. `clean_inkscape_svg(src)` — parse with lxml, remove elements/attrs in `http://www.inkscape.org/namespaces/inkscape` and `http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd`, call `etree.cleanup_namespaces()`, serialize to string
|
|
66
|
+
2. `annotate_svg(svg_str, animations)` — find elements by id (stripping leading `#`), set `class` and `data-step` attributes
|
|
67
|
+
|
|
68
|
+
CSS class map: `Fade → anim-fade-in`, `FadeOut → anim-fade-out`, `Bounce → anim-bounce`
|
|
69
|
+
|
|
70
|
+
## Dependencies
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
lxml>=5.0 SVG processing
|
|
74
|
+
websockets>=12.0 WebSocket server (uses 16.x asyncio API)
|
|
75
|
+
watchfiles>=0.21 inotify-based file watcher
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Not yet implemented
|
|
79
|
+
|
|
80
|
+
- Layout chain inlining (`inkflow:parent` resolution, `inject-layout`, `inkflow new`) — see ROADMAP.md
|
|
81
|
+
- Font embedding (fonttools)
|
|
82
|
+
- Morph for non-primitive shapes: `<path>` and `<polygon>` fall back to instant cut; `<g>` groups are cloned and faded as a unit — put the ID on the `<g>` (not on individual children) when you want a shape+label pair to enter/exit together
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nick Le Large
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inkflow
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
|
+
Summary: Beautiful slides from SVG. Your editor, your style.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: click>=8.0
|
|
8
|
+
Requires-Dist: lxml>=5.0
|
|
9
|
+
Requires-Dist: markdown-it-py>=3.0
|
|
10
|
+
Requires-Dist: rich>=15.0.0
|
|
11
|
+
Requires-Dist: watchfiles>=0.21
|
|
12
|
+
Requires-Dist: websockets>=12.0
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-light-landscape.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/assets/logo-dark-landscape.svg">
|
|
5
|
+
<img src="docs/assets/logo-dark-landscape.svg" width="80%">
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center"><strong>Beautiful slides from SVG. Your editor, your style.</strong></p>
|
|
10
|
+
|
|
11
|
+
> **Early-stage software.**
|
|
12
|
+
> The core pipeline works end-to-end —
|
|
13
|
+
> animations, transitions, live reload, markdown slides, static export.
|
|
14
|
+
> The API is not stable and key features are still missing.
|
|
15
|
+
> Use at your own risk.
|
|
16
|
+
|
|
17
|
+
## The idea
|
|
18
|
+
|
|
19
|
+
Every presentation tool makes the same tradeoff:
|
|
20
|
+
either you get a nice visual authoring environment (PowerPoint, Keynote, Google Slides)
|
|
21
|
+
or you get something that plays well with version control and plaintext workflows (Beamer, Slidev, reveal.js).
|
|
22
|
+
You rarely get both,
|
|
23
|
+
and you almost never get freeform visual design with git-friendly source files and animated, sequenced output.
|
|
24
|
+
Worse, the tools with the best visual editors tend to lock your content into proprietary formats
|
|
25
|
+
that are tied to a specific platform or subscription.
|
|
26
|
+
|
|
27
|
+
Inkflow tries to bridge that gap.
|
|
28
|
+
The source files are SVGs, Markdown, and a Python config file.
|
|
29
|
+
Any SVG editor works.
|
|
30
|
+
Inkscape is the first-class supported environment,
|
|
31
|
+
but you can use Inkscape, Affinity Designer, a text editor, or anything else that produces valid SVG.
|
|
32
|
+
The presentation layer is a **Python pipeline** that turns those files into an animated, browser-based presenter.
|
|
33
|
+
Because everything is open formats, plaintext, and diffable,
|
|
34
|
+
you are not tied to any particular software or cloud service.
|
|
35
|
+
Switch editors, move repos, or swap tools at any time without losing your work.
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
A deck is a plain Python file:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from inkflow import Bounce, Crossfade, Cut, Deck, FadeIn, MarkdownSlide, Media, Morph, Slide
|
|
43
|
+
|
|
44
|
+
deck = Deck()
|
|
45
|
+
|
|
46
|
+
deck.slides = [
|
|
47
|
+
# SVG slide: draw freely in Inkscape, animate elements by id
|
|
48
|
+
Slide(
|
|
49
|
+
"slides/01-title.svg",
|
|
50
|
+
animations=[
|
|
51
|
+
FadeIn("#headline", step=1),
|
|
52
|
+
FadeIn("#subtitle", step=2),
|
|
53
|
+
],
|
|
54
|
+
),
|
|
55
|
+
Slide("slides/02-diagram.svg", transition=Crossfade(), animations=[
|
|
56
|
+
Bounce("#box-a", step=1),
|
|
57
|
+
Bounce("#box-b", step=2),
|
|
58
|
+
]),
|
|
59
|
+
Slide("slides/03-chart.svg", transition=Morph(duration=0.7)),
|
|
60
|
+
|
|
61
|
+
# Markdown slide: write content in .md, render into a layout SVG
|
|
62
|
+
MarkdownSlide("layouts/content.svg", content="slides/04-notes.md"),
|
|
63
|
+
MarkdownSlide(
|
|
64
|
+
"layouts/media-right.svg",
|
|
65
|
+
content="slides/05-image.md",
|
|
66
|
+
media=Media("assets/photo.jpg", fit="cover"),
|
|
67
|
+
),
|
|
68
|
+
]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Both slide types support injecting content into named SVG elements:
|
|
72
|
+
`TextBox` to fill a text placeholder, `Media` to embed an image or video.
|
|
73
|
+
`Slide` is SVG-first: the SVG carries the design, with optional content slots for dynamic parts.
|
|
74
|
+
`MarkdownSlide` is layout-first: a template SVG defines the structure,
|
|
75
|
+
a Markdown file provides the text, and named kwargs fill any additional slots.
|
|
76
|
+
It is shorthand for the common case, built on the same injection mechanism.
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv add inkflow # or: pip install inkflow
|
|
82
|
+
inkflow serve deck.py
|
|
83
|
+
# press "o" in the tui to open http://localhost:7777 in your browser,
|
|
84
|
+
# press ? in the presenter for keyboard shortcuts
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
To try the bundled example:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://github.com/ll-nick/inkflow
|
|
91
|
+
cd inkflow
|
|
92
|
+
uv run inkflow serve example/deck.py
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
No SVG editor is invoked at serve time — Inkscape or any other tool writes the files, Inkflow reads them.
|
|
96
|
+
Saving a slide reloads the presenter automatically.
|
|
97
|
+
|
|
98
|
+
## Commands
|
|
99
|
+
|
|
100
|
+
| Command | Description |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `inkflow serve deck.py` | Start the live-reload presenter |
|
|
103
|
+
| `inkflow build deck.py` | Export a self-contained HTML directory for offline use |
|
|
104
|
+
| `inkflow export deck.py` | Export a PDF via headless Chromium |
|
|
105
|
+
|
|
106
|
+
## Architecture
|
|
107
|
+
|
|
108
|
+
- **`deck.py`:** Python manifest; gives you autocomplete and programmatic slide generation for free
|
|
109
|
+
- **SVG pipeline:** lxml strips Inkscape editor metadata,
|
|
110
|
+
then annotates elements with CSS animation classes and `data-step` attributes based on the manifest
|
|
111
|
+
- **Layout system:** `MarkdownSlide` injects Markdown content into layout SVGs;
|
|
112
|
+
built-in theme layouts cover common slide types
|
|
113
|
+
- **Local server:** asyncio HTTP server serves the presenter HTML with slides embedded as JSON;
|
|
114
|
+
a WebSocket server pushes live-reload signals when files change
|
|
115
|
+
- **Browser presenter:** — vanilla HTML/JS/CSS, no framework
|
|
116
|
+
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
Things that would need to exist before this is a tool someone would actually give a talk with.
|
|
4
|
+
|
|
5
|
+
Items are roughly ordered by how much they matter. The first two sections are load-bearing — without them the tool is a toy. Everything after is about closing the gap to a real presenter experience. The final section is honest about what is out of scope or architecturally not possible.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Decided: design questions
|
|
10
|
+
|
|
11
|
+
**SVG editor agnosticism**
|
|
12
|
+
The tool has no hard dependency on Inkscape at runtime. The pipeline only reads and writes standard SVG; it never spawns Inkscape as a subprocess. Any vector editor that exports well-formed SVG — Inkscape, Figma, Affinity Designer, Sketch, or hand-coded files — works as an authoring tool. `clean_svg()` strips Inkscape/Sodipodi editor metadata if present, and is a no-op on files from other tools.
|
|
13
|
+
|
|
14
|
+
The one Inkscape-specific feature is `inject-layout`: it writes `inkscape:groupmode="layer"` and `sodipodi:insensitive` attributes so Inkscape's GUI renders the injected preview layers correctly. These attributes are harmless in any other tool. A future refinement is to make the Inkscape layer attributes opt-in and write only `data-inkflow-*` attributes by default.
|
|
15
|
+
|
|
16
|
+
**One file per slide vs multi-page SVG**
|
|
17
|
+
Staying with one file per slide. Inkscape's multi-page SVG format uses `inkscape:page` elements that are not standard SVG; parsing them would tightly couple the tool to Inkscape's internal implementation. One-file-per-slide produces cleaner git diffs and maps to the standard SVG model. Worth revisiting only if Inkscape formalises the format.
|
|
18
|
+
|
|
19
|
+
**Markdown slide types: foreignObject**
|
|
20
|
+
An existing Markdown library (`markdown-it-py`) converts content to HTML in one call — no custom Markdown parsing. The HTML is injected into a `<foreignObject>` at the placeholder zone's geometry. Typography and color come from the CSS cascade (theme `styles.css` + project `styles.css`) injected into the HTML `<head>`, which cascades into all `<foreignObject>` content. The SVG text generation approach is not pursued: it cannot support tables, images, or nested lists and hits a ceiling quickly.
|
|
21
|
+
|
|
22
|
+
**Zone naming convention**
|
|
23
|
+
Content placeholder zones in layout SVGs are `<rect>` elements with `id="zone-{name}"`. The `zone-` prefix is the single naming rule authors need to learn. Zone rects must carry explicit `x`, `y`, `width`, `height` attributes.
|
|
24
|
+
|
|
25
|
+
Standard zone names with reserved pipeline behaviour:
|
|
26
|
+
- `zone-title` — receives the leading `# H1` auto-extracted from a markdown file
|
|
27
|
+
- `zone-subtitle` — receives the `## H2` immediately following the title, before any body content
|
|
28
|
+
- `zone-content` — default content zone; receives everything not routed elsewhere
|
|
29
|
+
- `zone-slide-number` — a `<text>` element; text content replaced with the current slide number at pipeline time
|
|
30
|
+
- `zone-slide-total` — a `<text>` element; text content replaced with the total slide count
|
|
31
|
+
|
|
32
|
+
Unreferenced zone rects — zones present in a layout but not filled by the slide — are silently removed from the output.
|
|
33
|
+
|
|
34
|
+
**Markdown file format**
|
|
35
|
+
`MarkdownSlide` reads a single `.md` file per slide. Custom syntax is limited to two block markers using the same `::name::` form:
|
|
36
|
+
|
|
37
|
+
- `::zone-name::` — routes all content from this point to the named zone until the next marker
|
|
38
|
+
- `::step::` — inserts an animation step boundary within the current zone; the step counter continues from wherever the slide's SVG animations left off
|
|
39
|
+
|
|
40
|
+
Auto-extraction applies when the file contains no explicit `::` markers:
|
|
41
|
+
1. A leading `# H1` is extracted into `zone-title`, if the layout has that zone
|
|
42
|
+
2. A `## H2` immediately following (before any body content) is extracted into `zone-subtitle`
|
|
43
|
+
3. Everything else goes to `zone-content`
|
|
44
|
+
|
|
45
|
+
Explicit markers always override auto-extraction. Single-zone layouts need no markers at all.
|
|
46
|
+
|
|
47
|
+
**Layout path resolution**
|
|
48
|
+
Prefix syntaxes bypass the search entirely:
|
|
49
|
+
- `local:foo` — `{project_root}/layouts/foo.svg`, error if not found
|
|
50
|
+
- `theme:foo` — `{theme_dir}/layouts/foo.svg`, error if not found or no theme set
|
|
51
|
+
- `builtin:foo` — inkflow built-in layouts
|
|
52
|
+
- `./foo`, `../foo` — relative to the current SVG file's location (used by theme-internal chains)
|
|
53
|
+
- `/absolute/path` — literal filesystem path
|
|
54
|
+
|
|
55
|
+
Bare single-part names (no prefix, no `/`) are resolved by a three-level search in order: project `layouts/` → active theme `layouts/` → inkflow built-in `layouts/`. First match wins.
|
|
56
|
+
|
|
57
|
+
This resolution applies uniformly wherever a layout name appears: the first argument of `MarkdownSlide`, the `inkflow:parent` attribute on any SVG, and `inkflow new`.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Open design questions
|
|
62
|
+
|
|
63
|
+
**Layout overrides**
|
|
64
|
+
Some layouts visually override the base layout frame — a section divider or full-bleed title slide typically drops the footer and page number. The mechanism for opting out (per-layout or per-slide) is not yet decided. Candidates: a flag attribute on the layout SVG root (`inkflow:no-layout="true"`), or a `layout_zones` exclusion list in `deck.py`. This affects both inject-layout (which layers to inject) and the pipeline (which elements to include).
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Layout inheritance
|
|
69
|
+
|
|
70
|
+
The layout system uses a general parent/child model. Each SVG file can declare its parent via an `inkflow:parent` attribute on the root `<svg>` element. This creates an arbitrary-depth chain. A typical deck has three levels:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
slides/05-bullets.svg
|
|
74
|
+
↑ inkflow:parent
|
|
75
|
+
layouts/bullets.svg ← layout: content zone positions
|
|
76
|
+
↑ inkflow:parent
|
|
77
|
+
theme/numbered-main.svg ← adds zone-slide-number / zone-slide-total
|
|
78
|
+
↑ inkflow:parent
|
|
79
|
+
theme/main.svg ← base: background, brand elements (no parent — chain terminates)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The `inkflow:parent` attribute is set by tooling (`inkflow new`, `inject-layout`), not by hand.
|
|
83
|
+
|
|
84
|
+
**`inject-layout` command**
|
|
85
|
+
`inkflow inject-layout` resolves the parent chain for each file and injects each ancestor as a separate locked Inkscape layer below the slide's own content, providing a spatial reference during authoring. The command is idempotent: it compares `inkflow:layout-hash` on existing layers against current file content and only rewrites stale entries. The pipeline strips all layout layers before serving; they never appear in the browser.
|
|
86
|
+
|
|
87
|
+
`inkflow inject-layout --check` reports stale files without rewriting. `inkflow new <layout> <path>` calls `inject-layout` automatically after creating the file.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Pipeline completeness
|
|
92
|
+
|
|
93
|
+
**Font embedding**
|
|
94
|
+
SVGs reference fonts by name. On the authoring machine this works; on any other machine it may not. Use fonttools to resolve fonts via fontconfig, base64-encode them, and inline `@font-face` declarations inside a `<defs>` block. Makes each output SVG self-contained without converting text to paths.
|
|
95
|
+
|
|
96
|
+
**PDF export**
|
|
97
|
+
Distinct from static HTML export. PDF is how you share slides with conference organizers, submit to proceedings, and post a permanent copy online. The cleanest path: build the static HTML, then print it to PDF via headless Chromium (`--print-to-pdf`). Each slide needs to map to exactly one PDF page, which requires a print stylesheet that shows one slide at a time.
|
|
98
|
+
|
|
99
|
+
**Static HTML export**
|
|
100
|
+
`inkflow build deck.py` produces a single self-contained HTML file — all slides inlined, fonts embedded, no server required. How you present from an unfamiliar machine and the intermediate step before PDF export.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Presenter experience
|
|
105
|
+
|
|
106
|
+
**Slide picker**
|
|
107
|
+
`g` currently enters a numeric goto buffer (`g: 12_`, Enter jumps, Escape cancels). The next step is replacing this with a full picker overlay: a modal where typing digits narrows by slide number and Enter or click jumps. Requires an optional `title` field on `Slide` in `deck.py`; the manifest field is cleaner than auto-extracting from SVG.
|
|
108
|
+
|
|
109
|
+
**Hidden / draft slides**
|
|
110
|
+
`Slide("...", visible=False)` keeps a slide in `deck.py` and its SVG on disk but excludes it from the presentation. Essential for working decks where you trim slides depending on audience or time without deleting anything.
|
|
111
|
+
|
|
112
|
+
**Presenter view**
|
|
113
|
+
A second window (or `/presenter` URL) showing the current slide, the next slide preview, step counter, and a running clock. The two windows stay in sync via the same WebSocket connection. Essential for any talk longer than ten minutes.
|
|
114
|
+
|
|
115
|
+
**Remote control**
|
|
116
|
+
A `/remote` URL serving a minimal forward/back interface designed for a phone browser. Sends the same advance/retreat messages the keyboard sends. The WebSocket architecture makes this nearly free to implement.
|
|
117
|
+
|
|
118
|
+
**Speaker notes**
|
|
119
|
+
A `notes` field on `Slide` that appears in the presenter view but not the main display. Plain string in `deck.py`; rendered as markdown in the presenter window.
|
|
120
|
+
|
|
121
|
+
**Drawing and annotation mode**
|
|
122
|
+
During Q&A, freehand drawing on the current slide is more useful than a laser pointer. A toggle (e.g. D key) that overlays a canvas element and lets you draw with the mouse or stylus. Drawings are ephemeral — they don't persist between slides.
|
|
123
|
+
|
|
124
|
+
**Laser pointer**
|
|
125
|
+
A coloured dot that follows the mouse while a modifier key is held.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Content features
|
|
130
|
+
|
|
131
|
+
**Per-step code line highlighting**
|
|
132
|
+
In technical presentations, stepping through a code block with specific lines highlighted per keypress is one of the most-used Slidev features. A `CodeSlide` template (or a `Highlight` animation that targets line ranges) that dims non-highlighted lines and brightens the relevant ones on each step.
|
|
133
|
+
|
|
134
|
+
**Math / LaTeX**
|
|
135
|
+
Enable the `dollarmath` plugin in `markdown-it-py` (emits `<span class="math-inline">` / `<div class="math-block">`), and load KaTeX in the presenter to render those spans client-side. Since `<foreignObject>` content is real HTML in the browser, KaTeX's auto-render works there without modification.
|
|
136
|
+
|
|
137
|
+
**Section dividers and table of contents**
|
|
138
|
+
A `SectionSlide("title")` type that marks a section boundary. The pipeline can auto-generate a TOC slide from all section boundaries, and section titles can appear in the presenter status bar.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Authoring experience
|
|
143
|
+
|
|
144
|
+
**`inkflow set-parent <file> <parent>` command**
|
|
145
|
+
Updates `inkflow:parent` on an existing slide SVG and re-runs `inject-layout` on that file. Use when changing a slide's layout after initial creation.
|
|
146
|
+
|
|
147
|
+
**Element ID validation**
|
|
148
|
+
When `deck.py` references `#headline` but the SVG has no matching element, this should be a hard build error: name the slide file and the missing ID, stop the build. Currently a silent skip.
|
|
149
|
+
|
|
150
|
+
**Watch-only mode**
|
|
151
|
+
`inkflow watch deck.py` rebuilds on change but doesn't open a browser or run a server. Useful for catching `deck.py` errors immediately during authoring.
|
|
152
|
+
|
|
153
|
+
**Custom slide dimensions**
|
|
154
|
+
Currently hardcoded 1920×1080. Should be a per-deck setting on `Deck(width=..., height=...)`, used by the pipeline to set the SVG viewport and by the presenter to size the stage correctly.
|
|
155
|
+
|
|
156
|
+
**Inkscape layer conventions for Morph**
|
|
157
|
+
For within-slide morphing, the two element states need to live somewhere in the SVG. A layer naming convention needs to be documented with a worked example.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Theme
|
|
162
|
+
|
|
163
|
+
**Implement the default theme**
|
|
164
|
+
The current built-in theme (`src/inkflow/theme/`) is a visual placeholder — bare zone rects on a flat background with no typography, brand, or decoration. It needs to become a real, polished default that demonstrates what inkflow can look like out of the box:
|
|
165
|
+
- A proper background with a coherent color palette using the `--inkflow-*` CSS variables
|
|
166
|
+
- Styled layout SVGs using the semantic CSS classes (`theme-accent`, `theme-surface`, etc.)
|
|
167
|
+
- Readable typography for headings, body text, and code blocks defined in `styles.css`
|
|
168
|
+
- Light and dark variants that both look intentional, not just inverted
|
|
169
|
+
- The showcase deck should serve as an example of a well-designed presentation, not just a test harness for every layout type
|
|
170
|
+
|
|
171
|
+
**Named theme support**
|
|
172
|
+
`_resolve_theme_dir` currently raises an error for bare theme names like `Deck(theme="catppuccin-mocha")`. Explicit path themes (`Deck(theme="./my-theme")`) work today. Named resolution requires a discovery mechanism — most naturally, a pip package naming convention (`inkflow-theme-{name}`).
|
|
173
|
+
|
|
174
|
+
**Pip-installable themes**
|
|
175
|
+
Auto-discovery via a naming convention would allow `Deck(theme="catppuccin-mocha")` to just work after `pip install inkflow-theme-catppuccin-mocha`, with no `deck.py` path configuration.
|
|
176
|
+
|
|
177
|
+
**Theme eject**
|
|
178
|
+
`inkflow eject-theme` copies a theme into the project directory and updates `deck.py` to point at the local copy. Useful for heavy customisation without forking the theme package.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Documentation
|
|
183
|
+
|
|
184
|
+
The minimum bar is a small set of markdown files in the repository — a `README.md` that covers installation and a five-minute example, and a `docs/` folder with focused pages on the layout system, zone substitutions, `MarkdownSlide`, animations, and transitions.
|
|
185
|
+
|
|
186
|
+
The preferred target is a static site on GitHub Pages, auto-built on every push to `main`. The right tool is **MkDocs with the Material theme** (zero Node.js dependency, fits naturally in a Python project) or **VitePress** (what Slidev itself uses, requires Node.js but produces a better look with interactive demos).
|
|
187
|
+
|
|
188
|
+
Minimum page set:
|
|
189
|
+
- **Getting started** — installation, `inkflow serve example/deck.py`, first custom slide
|
|
190
|
+
- **Layout system** — `inkflow:parent`, zone rects, `inject-layout`, layout chains, path resolution
|
|
191
|
+
- **MarkdownSlide** — `::zone::` / `::step::` syntax, auto-extraction, `image=` / `video=` kwargs
|
|
192
|
+
- **Animations** — `FadeIn`, `FadeOut`, `Bounce`, step model, `steps=True`
|
|
193
|
+
- **Transitions** — Cut, Crossfade, Morph; per-slide and deck-level
|
|
194
|
+
- **`deck.py` reference** — all `Deck`, `Slide`, `MarkdownSlide` fields in one place
|
|
195
|
+
- **Themes** — directory structure, `styles.css` cascade, semantic CSS classes, `Deck(theme=...)`
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Reliability and packaging
|
|
200
|
+
|
|
201
|
+
**Port conflict handling**
|
|
202
|
+
If 7777 or 7778 are already in use the server crashes with an OS error. Should auto-detect a free port or give a useful message.
|
|
203
|
+
|
|
204
|
+
**CLI polish**
|
|
205
|
+
`--no-browser` flag to suppress auto-opening a tab. `--host` for presenting from a remote machine over SSH forwarding. `--version`. Better `--help` output.
|
|
206
|
+
|
|
207
|
+
**Packaging**
|
|
208
|
+
A PyPI release so `uvx inkflow serve deck.py` works without a checkout. Possibly a Nix flake. Right now you need to clone the repo and `uv sync`.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Nice to have
|
|
213
|
+
|
|
214
|
+
- **More animation types** — `Scale`, `Rotate`, `Draw` (stroke-dashoffset path drawing), `Highlight` (brief colour pulse)
|
|
215
|
+
- **Morph for paths and groups** — the current morph interpolates geometry attributes for `<rect>`, `<circle>`, and `<ellipse>`; `<path>`, `<polygon>`, `<g>`, and `<text>` fall back to an instant cut
|
|
216
|
+
- **Auto-advance** — timed slides for kiosk or lightning-talk use
|
|
217
|
+
- **Slide overview** — press Escape for a thumbnail grid, click to jump
|
|
218
|
+
- **Hyperlinks** — SVG `<a>` elements open in a new tab during presentation
|
|
219
|
+
- **Within-slide Morph** — an element changes shape as part of a step sequence on a single slide, distinct from the cross-slide morph
|
|
220
|
+
- **Configurable keybindings** — a `keybindings` dict on `Deck` that overrides the defaults, injected into the presenter as JSON
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Out of scope and hard limits
|
|
225
|
+
|
|
226
|
+
**Auto-reflow and bullet lists for Inkscape-authored text**
|
|
227
|
+
Inkscape is a drawing tool that supports text, not a text layout tool. It has no native bullet list feature — you place bullet characters manually and indent by hand. Inkscape 1.2 added SVG 2.0 `shape-inside` text wrapping, but browser support is absent in Firefox and experimental in Chrome, so the pipeline cannot rely on it. In practice this is not a problem: if you need bullet lists or reflowing text, use a `TextBox` placeholder in Inkscape and write the content as Markdown in `deck.py`.
|
|
228
|
+
|
|
229
|
+
**PPTX export**
|
|
230
|
+
SVG is arbitrary vector geometry; PPTX has its own shape/text model. Conversion fidelity for anything non-trivial would be poor. If you need a PPTX, use PowerPoint.
|
|
231
|
+
|
|
232
|
+
**WYSIWYG layout preview in Inkscape**
|
|
233
|
+
A structural consequence of the pipeline-inlining approach: if the layout chain is resolved at build time, Inkscape cannot show it during authoring without `inject-layout`. The tool commits to this model — the injected layers are a spatial reference, not a live preview.
|
|
234
|
+
|
|
235
|
+
**Real-time collaboration**
|
|
236
|
+
SVG files on disk and a local Python server are the wrong substrate. Git handles version history already.
|
|
237
|
+
|
|
238
|
+
**Interaction-triggered animations**
|
|
239
|
+
Hover effects or click-a-specific-element-to-reveal. The step model advances globally on keypress; adding per-element interactivity would require a significant rethink of the animation model.
|
|
240
|
+
|
|
241
|
+
**Accessibility**
|
|
242
|
+
SVG element order does not correspond to visual reading order, there is no semantic structure, and screen reader support for inline SVG is inconsistent. Out of scope until the core tool is stable.
|
|
243
|
+
|
|
244
|
+
**Export to video / screen recording**
|
|
245
|
+
Automated recording as MP4 requires driving the presenter JS from outside the browser. The complexity is disproportionate; system screen recorders already exist.
|