simplex-web 0.2.0__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.
- simplex/README.md +32 -0
- simplex/cli/README.md +13 -0
- simplex/cli/__init__.py +5 -0
- simplex/cli/commands.py +384 -0
- simplex/deck/README.md +19 -0
- simplex/deck/__init__.py +7 -0
- simplex/deck/_template/assets/.gitkeep +0 -0
- simplex/deck/_template/assets/code/.gitkeep +0 -0
- simplex/deck/_template/assets/figures/.gitkeep +0 -0
- simplex/deck/_template/deck.toml +11 -0
- simplex/deck/_template/manim.cfg +3 -0
- simplex/deck/_template/notes.md +27 -0
- simplex/deck/_template/refs.bib +12 -0
- simplex/deck/_template/slides/__init__.py +7 -0
- simplex/deck/_template/slides/intro.py +21 -0
- simplex/deck/config.py +207 -0
- simplex/deck/registry.py +110 -0
- simplex/deck/scaffold.py +86 -0
- simplex/deck/section.py +40 -0
- simplex/engine/README.md +9 -0
- simplex/render/README.md +46 -0
- simplex/render/__init__.py +1 -0
- simplex/render/html.py +132 -0
- simplex/render/pdf.py +32 -0
- simplex/render/pptx.py +32 -0
- simplex/render/reconcile.py +350 -0
- simplex/render/runner.py +116 -0
- simplex/render/thumbnail.py +374 -0
- simplex/slides/README.md +9 -0
- simplex/slides/components/README.md +9 -0
- simplex/theme/README.md +9 -0
- simplex/web/README.md +33 -0
- simplex/web/__init__.py +1 -0
- simplex/web/bibliography.py +248 -0
- simplex/web/bibtex.py +129 -0
- simplex/web/builder.py +321 -0
- simplex/web/callouts.py +134 -0
- simplex/web/citations.py +118 -0
- simplex/web/equations.py +79 -0
- simplex/web/notes.py +135 -0
- simplex/web/refs.py +60 -0
- simplex/web/sidenotes.py +76 -0
- simplex/web/site_config.py +71 -0
- simplex/web/slide_ref.py +54 -0
- simplex/web/static/.gitkeep +0 -0
- simplex/web/static/README.md +23 -0
- simplex/web/static/fonts/lato/lato-latin-400-italic.woff2 +0 -0
- simplex/web/static/fonts/lato/lato-latin-400-normal.woff2 +0 -0
- simplex/web/static/fonts/lato/lato-latin-700-italic.woff2 +0 -0
- simplex/web/static/fonts/lato/lato-latin-700-normal.woff2 +0 -0
- simplex/web/static/fonts/lato/lato-latin-900-normal.woff2 +0 -0
- simplex/web/static/fonts/merriweather/merriweather-latin-400-italic.woff2 +0 -0
- simplex/web/static/fonts/merriweather/merriweather-latin-400-normal.woff2 +0 -0
- simplex/web/static/fonts/merriweather/merriweather-latin-700-italic.woff2 +0 -0
- simplex/web/static/fonts/merriweather/merriweather-latin-700-normal.woff2 +0 -0
- simplex/web/static/fonts/merriweather/merriweather-latin-900-normal.woff2 +0 -0
- simplex/web/static/htmx.min.js +1 -0
- simplex/web/static/katex/auto-render.min.js +1 -0
- simplex/web/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- simplex/web/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- simplex/web/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- simplex/web/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- simplex/web/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- simplex/web/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- simplex/web/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- simplex/web/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- simplex/web/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- simplex/web/static/katex/katex.min.css +1 -0
- simplex/web/static/katex/katex.min.js +1 -0
- simplex/web/static/lucide/README.md +7 -0
- simplex/web/static/lucide/lucide.min.js +12 -0
- simplex/web/static/notes.js +68 -0
- simplex/web/static/reveal.js/reset.css +30 -0
- simplex/web/static/reveal.js/reveal.css +8 -0
- simplex/web/static/reveal.js/reveal.js +9 -0
- simplex/web/static/simplex.css +1870 -0
- simplex/web/static/tailwind.js +64 -0
- simplex/web/static/viewer.js +428 -0
- simplex/web/templates/README.md +19 -0
- simplex/web/templates/_carousel.html +117 -0
- simplex/web/templates/base.html +110 -0
- simplex/web/templates/deck.html +149 -0
- simplex/web/templates/index.html +20 -0
- simplex/web/templates/revealjs.html.j2 +374 -0
- simplex/web/templates/section.html +74 -0
- simplex/web/vendor.py +148 -0
- simplex_web-0.2.0.dist-info/METADATA +166 -0
- simplex_web-0.2.0.dist-info/RECORD +91 -0
- simplex_web-0.2.0.dist-info/WHEEL +4 -0
- simplex_web-0.2.0.dist-info/entry_points.txt +2 -0
- simplex_web-0.2.0.dist-info/licenses/LICENSE +21 -0
simplex/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# src/simplex
|
|
2
|
+
|
|
3
|
+
The `simplex` distribution's half of the namespace: deck discovery,
|
|
4
|
+
render pipeline, web portal, and CLI. The plugin half (`simplex.plugin`,
|
|
5
|
+
`simplex.engine`, `simplex.theme`, `simplex.slides`) is shipped by the
|
|
6
|
+
`manim-simplex` distribution and merged into this same namespace at
|
|
7
|
+
import time via PEP 420.
|
|
8
|
+
|
|
9
|
+
This directory ships **no** `__init__.py`. Don't add one -- it would
|
|
10
|
+
shadow the implicit namespace and break the `manim-simplex` half.
|
|
11
|
+
|
|
12
|
+
## Public surface (this distribution)
|
|
13
|
+
|
|
14
|
+
- `simplex.deck` -- `DeckConfig`, `discover`, `scaffold`
|
|
15
|
+
- `simplex.render` -- `runner`, `reconcile`, `html`, `pdf`, `pptx`, `thumbnail`
|
|
16
|
+
- `simplex.web` -- `builder`, `notes`, bibliography stack, templates, static, SSE reload
|
|
17
|
+
- `simplex.cli.commands:app` -- the Typer app behind `uv run simplex`
|
|
18
|
+
|
|
19
|
+
## Public surface (re-exported from `manim-simplex`)
|
|
20
|
+
|
|
21
|
+
- `simplex.plugin:activate` -- the `manim.plugins` entry-point (set `plugins = simplex` in your `manim.cfg`)
|
|
22
|
+
- `simplex.slides` -- `BaseSlide`, `make_chrome`
|
|
23
|
+
- `simplex.theme` -- `Theme`, `WebPalette`, `active_theme`, `get_active_theme`, `presets`, `render_web_css`
|
|
24
|
+
- `simplex.engine` -- `apply_theme_defaults`, `Region`, `Remove`, `register_exit`, `set_exit_animation`, `clear_scene`, `SimplexSectionType`
|
|
25
|
+
|
|
26
|
+
## Don't
|
|
27
|
+
|
|
28
|
+
- Don't add `src/simplex/__init__.py`. The namespace must stay implicit.
|
|
29
|
+
- Don't import `manim_editor`. The legacy package is deprecated and not a dependency.
|
|
30
|
+
- Don't wrap Manim constructors. Authors write vanilla Manim; the framework configures defaults underneath via `Mobject.set_default(...)`.
|
|
31
|
+
- Don't add a custom quality enum. Use `manim.constants.QUALITIES` keys directly.
|
|
32
|
+
- Don't bypass the plugin entry-point by setting `manim.config` from your scene. The plugin runs once at import time; that's the correct seam.
|
simplex/cli/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# cli/
|
|
2
|
+
|
|
3
|
+
Typer-based command surface exposed as `simplex` via `[project.scripts]`.
|
|
4
|
+
|
|
5
|
+
## Public surface
|
|
6
|
+
|
|
7
|
+
- `app` -- Typer application (entry point: `simplex.cli.commands:app`)
|
|
8
|
+
- Commands: `new`, `render`, `build`, `serve`, `clean`, `doctor`
|
|
9
|
+
|
|
10
|
+
## Don't
|
|
11
|
+
|
|
12
|
+
- Don't add business logic here. Commands are thin shells over `deck`, `render`, and `web`.
|
|
13
|
+
- Don't `os.chdir` outside of `serve`; that command intentionally enters `site/` to use `http.server`.
|
simplex/cli/__init__.py
ADDED
simplex/cli/commands.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Simplex CLI -- new | init | render | build | serve | test | clean | doctor."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import http.server
|
|
6
|
+
import shutil
|
|
7
|
+
import socketserver
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Annotated
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from simplex.deck.registry import discover
|
|
19
|
+
from simplex.deck.scaffold import scaffold as deck_scaffold
|
|
20
|
+
from simplex.render import pdf, runner
|
|
21
|
+
from simplex.web.builder import build as build_site
|
|
22
|
+
from simplex.web.site_config import SiteConfig
|
|
23
|
+
|
|
24
|
+
app = typer.Typer(help="Simplex -- Manim-slides framework with a generated portal.")
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
_DECKS = Path("decks")
|
|
28
|
+
_SITE = Path("site")
|
|
29
|
+
|
|
30
|
+
# Reload signaling for `simplex serve --watch`.
|
|
31
|
+
_RELOAD_EVENT = threading.Event()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command()
|
|
35
|
+
def new(target: str) -> None:
|
|
36
|
+
"""Scaffold a new deck.
|
|
37
|
+
|
|
38
|
+
``simplex new <slug>`` creates ``decks/<slug>/`` (featured section).
|
|
39
|
+
``simplex new <section>/<slug>`` creates ``decks/<section>/<slug>/``.
|
|
40
|
+
"""
|
|
41
|
+
dest = deck_scaffold(target, _DECKS)
|
|
42
|
+
console.print(f"[green]Created[/green] {dest}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.command()
|
|
46
|
+
def init(
|
|
47
|
+
target_dir: Annotated[
|
|
48
|
+
Path | None,
|
|
49
|
+
typer.Argument(
|
|
50
|
+
help="Directory to create. Default: prompt + git clone the template.",
|
|
51
|
+
),
|
|
52
|
+
] = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Scaffold a new lectures repo from ``shlomi-perles/simplex-lectures-template``.
|
|
55
|
+
|
|
56
|
+
Requires the ``gh`` CLI for full template integration; falls back to
|
|
57
|
+
``git clone`` of the public template otherwise.
|
|
58
|
+
"""
|
|
59
|
+
template_repo = "shlomi-perles/simplex-lectures-template"
|
|
60
|
+
if target_dir is None:
|
|
61
|
+
target_dir = Path(typer.prompt("New repo directory name"))
|
|
62
|
+
if target_dir.exists():
|
|
63
|
+
raise typer.BadParameter(f"{target_dir} already exists")
|
|
64
|
+
|
|
65
|
+
gh_path = shutil.which("gh")
|
|
66
|
+
if gh_path is not None:
|
|
67
|
+
repo_name = typer.prompt(
|
|
68
|
+
f"GitHub repo to create (default: {target_dir.name})",
|
|
69
|
+
default=target_dir.name,
|
|
70
|
+
)
|
|
71
|
+
subprocess.run(
|
|
72
|
+
[
|
|
73
|
+
gh_path,
|
|
74
|
+
"repo",
|
|
75
|
+
"create",
|
|
76
|
+
repo_name,
|
|
77
|
+
"--template",
|
|
78
|
+
template_repo,
|
|
79
|
+
"--clone",
|
|
80
|
+
"--private",
|
|
81
|
+
],
|
|
82
|
+
check=True,
|
|
83
|
+
)
|
|
84
|
+
console.print(f"[green]Created[/green] {repo_name} from {template_repo}")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
git_path = shutil.which("git")
|
|
88
|
+
if git_path is None:
|
|
89
|
+
raise typer.BadParameter("neither `gh` nor `git` is available on PATH")
|
|
90
|
+
subprocess.run(
|
|
91
|
+
[git_path, "clone", f"https://github.com/{template_repo}.git", str(target_dir)],
|
|
92
|
+
check=True,
|
|
93
|
+
)
|
|
94
|
+
shutil.rmtree(target_dir / ".git", ignore_errors=True)
|
|
95
|
+
console.print(
|
|
96
|
+
f"[green]Cloned[/green] template into {target_dir}. "
|
|
97
|
+
"Run `git init && git add . && git commit -m initial` inside it next."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command()
|
|
102
|
+
def render(
|
|
103
|
+
target: str,
|
|
104
|
+
scene: Annotated[
|
|
105
|
+
list[str] | None,
|
|
106
|
+
typer.Option(
|
|
107
|
+
"--scene",
|
|
108
|
+
help="Re-render only this scene class. Repeatable.",
|
|
109
|
+
),
|
|
110
|
+
] = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Render a single deck.
|
|
113
|
+
|
|
114
|
+
Triple-syntax targets accepted:
|
|
115
|
+
|
|
116
|
+
- ``slug`` full deck
|
|
117
|
+
- ``slug::SceneClass`` one scene (alias for ``--scene SceneClass``)
|
|
118
|
+
- ``slug::SceneClass::MainName`` reserved; for now renders the whole scene
|
|
119
|
+
"""
|
|
120
|
+
slug, _, scene_spec = target.partition("::")
|
|
121
|
+
site_cfg = SiteConfig.load()
|
|
122
|
+
registry = discover(_DECKS, default_section_order=site_cfg.default_section_order)
|
|
123
|
+
deck = registry.find_deck(slug)
|
|
124
|
+
if deck is None:
|
|
125
|
+
raise typer.BadParameter(f"unknown deck: {slug}")
|
|
126
|
+
|
|
127
|
+
scene_filter: tuple[str, ...] = tuple(scene or ())
|
|
128
|
+
if scene_spec:
|
|
129
|
+
scene_name, _, _main = scene_spec.partition("::")
|
|
130
|
+
scene_filter = (*scene_filter, scene_name)
|
|
131
|
+
|
|
132
|
+
out = _SITE / "decks" / deck.slug
|
|
133
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
runner.render(deck, output_dir=out, scenes=scene_filter)
|
|
137
|
+
except ValueError as exc:
|
|
138
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
139
|
+
|
|
140
|
+
with contextlib.suppress(subprocess.SubprocessError, FileNotFoundError, ImportError):
|
|
141
|
+
pdf.export(deck, output_dir=out)
|
|
142
|
+
|
|
143
|
+
console.print(f"[green]Rendered[/green] {deck.slug} -> {out}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.command()
|
|
147
|
+
def build(
|
|
148
|
+
only: Annotated[
|
|
149
|
+
list[str] | None,
|
|
150
|
+
typer.Option("--only", help="Only build this deck slug. Repeatable."),
|
|
151
|
+
] = None,
|
|
152
|
+
scene: Annotated[
|
|
153
|
+
list[str] | None,
|
|
154
|
+
typer.Option(
|
|
155
|
+
"--scene",
|
|
156
|
+
help="Re-render only this scene class on every selected deck.",
|
|
157
|
+
),
|
|
158
|
+
] = None,
|
|
159
|
+
no_render: Annotated[
|
|
160
|
+
bool,
|
|
161
|
+
typer.Option("--no-render", help="Skip rendering; only rebuild HTML/portal."),
|
|
162
|
+
] = False,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Build the full static portal under ``site/``."""
|
|
165
|
+
build_site(
|
|
166
|
+
_DECKS,
|
|
167
|
+
_SITE,
|
|
168
|
+
render=not no_render,
|
|
169
|
+
only=tuple(only or ()),
|
|
170
|
+
scenes=tuple(scene or ()),
|
|
171
|
+
)
|
|
172
|
+
console.print(f"[green]Built[/green] {_SITE}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.command()
|
|
176
|
+
def test(
|
|
177
|
+
only: Annotated[
|
|
178
|
+
list[str] | None,
|
|
179
|
+
typer.Option("--only", help="Only test this deck slug. Repeatable."),
|
|
180
|
+
] = None,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Smoke-render every deck by rendering only the first animation.
|
|
183
|
+
|
|
184
|
+
Used in CI: catches scene-construction errors without paying for full
|
|
185
|
+
video encoding. Exits non-zero on the first deck that fails to render.
|
|
186
|
+
"""
|
|
187
|
+
site_cfg = SiteConfig.load()
|
|
188
|
+
registry = discover(_DECKS, default_section_order=site_cfg.default_section_order)
|
|
189
|
+
only_set = set(only or ())
|
|
190
|
+
|
|
191
|
+
failures: list[tuple[str, str]] = []
|
|
192
|
+
for section in registry.sections:
|
|
193
|
+
for deck in section.decks:
|
|
194
|
+
if only_set and deck.slug not in only_set:
|
|
195
|
+
continue
|
|
196
|
+
out = _SITE / "decks" / deck.slug
|
|
197
|
+
try:
|
|
198
|
+
runner.render(deck, output_dir=out, write_last_frame=True)
|
|
199
|
+
console.print(f"[green]ok[/green] {deck.slug}")
|
|
200
|
+
except (subprocess.CalledProcessError, ValueError) as exc:
|
|
201
|
+
failures.append((deck.slug, str(exc)))
|
|
202
|
+
console.print(f"[red]FAIL[/red] {deck.slug}: {exc}")
|
|
203
|
+
|
|
204
|
+
if failures:
|
|
205
|
+
raise typer.Exit(code=1)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.command()
|
|
209
|
+
def serve(
|
|
210
|
+
port: Annotated[int, typer.Option(help="Port to serve on.")] = 8000,
|
|
211
|
+
watch: Annotated[
|
|
212
|
+
bool,
|
|
213
|
+
typer.Option(
|
|
214
|
+
"--watch/--no-watch",
|
|
215
|
+
help="Watch decks/ + src/ and reload the browser on save.",
|
|
216
|
+
),
|
|
217
|
+
] = False,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Serve ``site/`` via the stdlib HTTP server.
|
|
220
|
+
|
|
221
|
+
With ``--watch``, also runs a watchfiles loop that re-runs the build on
|
|
222
|
+
every save and pushes an SSE event so open browser tabs reload.
|
|
223
|
+
"""
|
|
224
|
+
if not _SITE.exists():
|
|
225
|
+
raise typer.BadParameter("site/ does not exist -- run `simplex build` first")
|
|
226
|
+
|
|
227
|
+
handler_cls = _make_handler(_SITE)
|
|
228
|
+
server = _SimplexTCPServer(("", port), handler_cls)
|
|
229
|
+
console.print(f"Serving http://localhost:{port}")
|
|
230
|
+
|
|
231
|
+
server_thread: threading.Thread | None = None
|
|
232
|
+
try:
|
|
233
|
+
if watch:
|
|
234
|
+
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
235
|
+
server_thread.start()
|
|
236
|
+
asyncio.run(_watch_loop())
|
|
237
|
+
else:
|
|
238
|
+
server.serve_forever(poll_interval=0.2)
|
|
239
|
+
except KeyboardInterrupt:
|
|
240
|
+
console.print("\n[yellow]stopping[/yellow]")
|
|
241
|
+
finally:
|
|
242
|
+
if server_thread is not None:
|
|
243
|
+
server.shutdown()
|
|
244
|
+
server_thread.join(timeout=2)
|
|
245
|
+
server.server_close()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class _SimplexTCPServer(socketserver.ThreadingTCPServer):
|
|
249
|
+
allow_reuse_address = True
|
|
250
|
+
daemon_threads = True
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _make_handler(site_dir: Path) -> type[http.server.BaseHTTPRequestHandler]:
|
|
254
|
+
"""Return a request handler that serves files + an SSE endpoint."""
|
|
255
|
+
|
|
256
|
+
class _Handler(http.server.SimpleHTTPRequestHandler):
|
|
257
|
+
def __init__(self, *args: object, **kwargs: object) -> None:
|
|
258
|
+
super().__init__(*args, directory=str(site_dir), **kwargs) # type: ignore[arg-type]
|
|
259
|
+
|
|
260
|
+
def do_GET(self) -> None:
|
|
261
|
+
if self.path == "/_simplex/events":
|
|
262
|
+
self.send_response(200)
|
|
263
|
+
self.send_header("Content-Type", "text/event-stream")
|
|
264
|
+
self.send_header("Cache-Control", "no-cache")
|
|
265
|
+
self.send_header("Connection", "keep-alive")
|
|
266
|
+
self.end_headers()
|
|
267
|
+
self._stream_events()
|
|
268
|
+
return
|
|
269
|
+
super().do_GET()
|
|
270
|
+
|
|
271
|
+
def _stream_events(self) -> None:
|
|
272
|
+
try:
|
|
273
|
+
while True:
|
|
274
|
+
if _RELOAD_EVENT.wait(timeout=30):
|
|
275
|
+
_RELOAD_EVENT.clear()
|
|
276
|
+
self.wfile.write(b"event: reload\ndata: 1\n\n")
|
|
277
|
+
else:
|
|
278
|
+
self.wfile.write(b": keepalive\n\n")
|
|
279
|
+
self.wfile.flush()
|
|
280
|
+
except BrokenPipeError:
|
|
281
|
+
pass
|
|
282
|
+
except ConnectionResetError:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
286
|
+
pass # quiet by default
|
|
287
|
+
|
|
288
|
+
return _Handler
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
async def _watch_loop() -> None:
|
|
292
|
+
"""Watchfiles + rebuild + broadcast SSE reload on every change."""
|
|
293
|
+
try:
|
|
294
|
+
from watchfiles import awatch
|
|
295
|
+
except ImportError as exc:
|
|
296
|
+
raise typer.BadParameter(
|
|
297
|
+
"watchfiles is required for --watch; install with `uv sync`"
|
|
298
|
+
) from exc
|
|
299
|
+
|
|
300
|
+
targets = [p for p in (Path("decks"), Path("src") / "simplex") if p.exists()]
|
|
301
|
+
console.print(f"[yellow]watching[/yellow] {', '.join(str(t) for t in targets)}")
|
|
302
|
+
|
|
303
|
+
async for changes in awatch(*targets, debounce=200):
|
|
304
|
+
affected = sorted(_affected_deck_slugs(changes))
|
|
305
|
+
if not affected:
|
|
306
|
+
continue
|
|
307
|
+
console.print(f"[yellow]reload[/yellow] {', '.join(affected)}")
|
|
308
|
+
try:
|
|
309
|
+
build_site(_DECKS, _SITE, only=tuple(affected), watch=True)
|
|
310
|
+
_RELOAD_EVENT.set()
|
|
311
|
+
except Exception as exc:
|
|
312
|
+
console.print(f"[red]build failed[/red]: {exc}")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _affected_deck_slugs(changes: Iterable[tuple[object, str]]) -> set[str]:
|
|
316
|
+
"""Map watchfiles change paths to the deck slugs they affect.
|
|
317
|
+
|
|
318
|
+
A change under ``decks/<slug>/...`` affects that slug. A change under
|
|
319
|
+
``src/simplex/...`` affects every deck (return empty -> caller rebuilds all).
|
|
320
|
+
"""
|
|
321
|
+
slugs: set[str] = set()
|
|
322
|
+
src_changed = False
|
|
323
|
+
for _, path in changes:
|
|
324
|
+
parts = Path(path).resolve().relative_to(Path.cwd().resolve(), walk_up=True).parts
|
|
325
|
+
if len(parts) >= 2 and parts[0] == "decks":
|
|
326
|
+
slugs.add(parts[1])
|
|
327
|
+
elif len(parts) >= 2 and parts[0] == "src":
|
|
328
|
+
src_changed = True
|
|
329
|
+
if src_changed:
|
|
330
|
+
slugs = set() # signals "rebuild everything"
|
|
331
|
+
return slugs
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@app.command()
|
|
335
|
+
def clean(
|
|
336
|
+
deck: Annotated[
|
|
337
|
+
list[str] | None,
|
|
338
|
+
typer.Option(
|
|
339
|
+
"--deck",
|
|
340
|
+
help="Only clean these deck slugs (site/decks/<slug>/).",
|
|
341
|
+
),
|
|
342
|
+
] = None,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Remove ``site/`` and ``media/``.
|
|
345
|
+
|
|
346
|
+
With ``--deck <slug>`` (repeatable), only that deck's output is removed.
|
|
347
|
+
"""
|
|
348
|
+
if deck:
|
|
349
|
+
site_cfg = SiteConfig.load()
|
|
350
|
+
registry = discover(_DECKS, default_section_order=site_cfg.default_section_order)
|
|
351
|
+
for slug in deck:
|
|
352
|
+
d = registry.find_deck(slug)
|
|
353
|
+
if d is None:
|
|
354
|
+
raise typer.BadParameter(f"unknown deck: {slug}")
|
|
355
|
+
site_deck = _SITE / "decks" / d.slug
|
|
356
|
+
if site_deck.exists():
|
|
357
|
+
shutil.rmtree(site_deck)
|
|
358
|
+
console.print(f"removed {site_deck}")
|
|
359
|
+
console.print(f"[green]Cleaned[/green] {d.slug}")
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
for target in (_SITE, Path("media")):
|
|
363
|
+
if target.exists():
|
|
364
|
+
shutil.rmtree(target)
|
|
365
|
+
console.print(f"removed {target}")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@app.command()
|
|
369
|
+
def doctor() -> None:
|
|
370
|
+
"""Verify required system binaries are reachable on PATH."""
|
|
371
|
+
required = ("latex", "ffmpeg", "manim", "manim-slides")
|
|
372
|
+
ok = True
|
|
373
|
+
for tool in required:
|
|
374
|
+
found = shutil.which(tool)
|
|
375
|
+
if found:
|
|
376
|
+
console.print(f"[green]ok[/green] {tool} -> {found}")
|
|
377
|
+
else:
|
|
378
|
+
console.print(f"[red]MISSING[/red] {tool}")
|
|
379
|
+
ok = False
|
|
380
|
+
sys.exit(0 if ok else 1)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
if __name__ == "__main__":
|
|
384
|
+
app()
|
simplex/deck/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# deck/
|
|
2
|
+
|
|
3
|
+
Per-deck configuration, sectioned discovery, scaffolding.
|
|
4
|
+
|
|
5
|
+
## Public surface
|
|
6
|
+
|
|
7
|
+
- `DeckConfig` -- frozen pydantic model loaded from `deck.toml`.
|
|
8
|
+
- `SectionConfig` -- frozen model loaded from `decks/<dir>/_section.toml`.
|
|
9
|
+
- `discover(decks_dir)` -- returns a `SectionedRegistry` (sections -> decks).
|
|
10
|
+
- `scaffold(target, decks_dir)` -- copies `_template/` into either
|
|
11
|
+
`decks/<slug>/` (featured) or `decks/<section>/<slug>/`.
|
|
12
|
+
|
|
13
|
+
## Don't
|
|
14
|
+
|
|
15
|
+
- Don't load `deck.toml` outside of `DeckConfig.load`.
|
|
16
|
+
- Don't recurse deeper than one level -- decks live at `decks/<slug>/` or
|
|
17
|
+
`decks/<section>/<slug>/`, never below.
|
|
18
|
+
- Don't add fields without a default. Older decks must keep validating
|
|
19
|
+
after a config bump.
|
simplex/deck/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
slug = "__SLUG__"
|
|
2
|
+
title = "__TITLE__"
|
|
3
|
+
summary = "Replace with a one-line description."
|
|
4
|
+
tags = []
|
|
5
|
+
theme = "dastimator_dark"
|
|
6
|
+
quality = "high_quality"
|
|
7
|
+
voiceover = false
|
|
8
|
+
category = ""
|
|
9
|
+
created_at = "__CREATED_AT__"
|
|
10
|
+
order = 1000
|
|
11
|
+
entrypoints = ["slides.intro:Intro"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Notes
|
|
2
|
+
|
|
3
|
+
This deck is a starting point. Replace the contents of `slides/` and fill these notes.
|
|
4
|
+
|
|
5
|
+
Refer to a slide inline with `[slide:1]` to add a clickable jump link.
|
|
6
|
+
|
|
7
|
+
Add a sidenote with the `^[...]` syntax — it floats into the right margin on wide screens.^[Sidenotes are Tufte-style: a numbered reference inline, the body in the gutter; narrow viewports collapse to an inline reveal on click.]
|
|
8
|
+
|
|
9
|
+
Inline math: $a^2 + b^2 = c^2$. Display math:
|
|
10
|
+
|
|
11
|
+
$$
|
|
12
|
+
\int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}
|
|
13
|
+
$$
|
|
14
|
+
|
|
15
|
+
Theorem / definition / lemma / remark / proof blockquotes turn into colour-coded callouts and get anchored ids automatically:
|
|
16
|
+
|
|
17
|
+
> **Theorem 1.1.** Every blockquote whose first paragraph begins with `**Theorem N.N.**` (or `Lemma`, `Definition`, `Remark`, `Proof`, …) becomes a referenceable callout.
|
|
18
|
+
|
|
19
|
+
Reference one by id with `\ref{theorem-1-1}` — the renderer resolves it to **Theorem 1.1** automatically.
|
|
20
|
+
|
|
21
|
+
Cite the bundled `refs.bib` with `\cite{key}` — the build appends a References section automatically (`\cite{KB15}` here).
|
|
22
|
+
|
|
23
|
+
Code blocks render with Pygments:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
print("hello, simplex")
|
|
27
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
% Bibliography for the deck. Cite in notes.md with `\cite{key}`.
|
|
2
|
+
% The web build (`simplex build`) parses this file, generates biblatex-style
|
|
3
|
+
% alpha labels (e.g. `[KB15]`), and appends a References section to the
|
|
4
|
+
% rendered notes page.
|
|
5
|
+
|
|
6
|
+
@inproceedings{KB15,
|
|
7
|
+
author = {Kingma, Diederik P. and Ba, Jimmy},
|
|
8
|
+
title = {Adam: A Method for Stochastic Optimization},
|
|
9
|
+
booktitle = {International Conference on Learning Representations (ICLR)},
|
|
10
|
+
year = {2015},
|
|
11
|
+
url = {http://arxiv.org/abs/1412.6980},
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Scene modules live alongside this file (e.g. `intro.py`).
|
|
2
|
+
|
|
3
|
+
Reference them from `deck.toml` as `entrypoints = ["slides.intro:Intro", ...]`;
|
|
4
|
+
the runner loads each entrypoint module directly. Don't re-export scene
|
|
5
|
+
classes here -- manim's discovery filters by `__module__`, so re-exports
|
|
6
|
+
are silently dropped.
|
|
7
|
+
"""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Template intro scene -- one slide with a title + subtitle."""
|
|
2
|
+
|
|
3
|
+
from manim import DOWN, ORIGIN, Tex, Write
|
|
4
|
+
|
|
5
|
+
from simplex.slides import BaseSlide
|
|
6
|
+
from simplex.theme.context import get_active_theme
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Intro(BaseSlide):
|
|
10
|
+
title: str = "Hello, Simplex"
|
|
11
|
+
subtitle: str = r"$f(x) = e^{i\pi} + 1 = 0$"
|
|
12
|
+
|
|
13
|
+
def construct(self) -> None:
|
|
14
|
+
theme = get_active_theme()
|
|
15
|
+
title_mob = Tex(self.title, font_size=theme.typography.h1)
|
|
16
|
+
self.region.place(title_mob, ORIGIN)
|
|
17
|
+
|
|
18
|
+
sub = Tex(self.subtitle, font_size=theme.typography.h2)
|
|
19
|
+
sub.next_to(title_mob, DOWN, buff=0.4)
|
|
20
|
+
self.play(Write(title_mob), Write(sub))
|
|
21
|
+
self.next_slide()
|