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/web/equations.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
r"""Display-equation tags + cross-references.
|
|
2
|
+
|
|
3
|
+
Authors write standard LaTeX ``\tag{X}`` inside ``$$...$$`` (or already-
|
|
4
|
+
rewritten ``\[...\]``). We pull the tag out of the math content server-side
|
|
5
|
+
and emit a layout we control:
|
|
6
|
+
|
|
7
|
+
<div class="equation" id="eq-X">
|
|
8
|
+
<div class="math block">\[ ...math without \tag... \]</div>
|
|
9
|
+
<span class="eq-tag">(X)</span>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
Why move the tag out of the math:
|
|
13
|
+
|
|
14
|
+
1. ``\ref{eq-X}`` cross-references need a stable anchor on a DOM element
|
|
15
|
+
that doesn't get re-rendered by KaTeX.
|
|
16
|
+
2. KaTeX positions its built-in ``\tag`` absolutely at ``right: 0`` of
|
|
17
|
+
the math element. When ``notes.js`` scales a wide equation to fit
|
|
18
|
+
the column, the absolute tag rides inside the scaled box and ends
|
|
19
|
+
up overlapping the formula. A grid-laid sibling stays at natural
|
|
20
|
+
size, in its own column.
|
|
21
|
+
|
|
22
|
+
The returned `labels` map (``"eq-3" -> "(3)"``) is consumed by
|
|
23
|
+
``callouts.transform`` so ``\ref{eq-3}`` resolves to the linked
|
|
24
|
+
display text.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
|
|
29
|
+
# Match a `<div class="math block">\[...\]</div>` produced by dollarmath +
|
|
30
|
+
# the renderer in `notes.py`.
|
|
31
|
+
_BLOCK_RE = re.compile(
|
|
32
|
+
r'<div class="math block">\s*\\\[(?P<math>.*?)\\\]\s*</div>',
|
|
33
|
+
re.DOTALL,
|
|
34
|
+
)
|
|
35
|
+
_TAG_RE = re.compile(r"\\tag\{(?P<label>[^}]+)\}")
|
|
36
|
+
_SLUG_RE = re.compile(r"[^a-z0-9]+")
|
|
37
|
+
|
|
38
|
+
_ESCAPE = {"&": "&", "<": "<", ">": ">", '"': """}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _escape(s: str) -> str:
|
|
42
|
+
return "".join(_ESCAPE.get(c, c) for c in s)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def transform(html: str) -> tuple[str, dict[str, str]]:
|
|
46
|
+
"""Extract ``\\tag{}`` from each display math block.
|
|
47
|
+
|
|
48
|
+
Returns ``(rewritten_html, labels)`` where ``labels`` maps each
|
|
49
|
+
``eq-<slug>`` to its display string ``"(<original tag>)"``.
|
|
50
|
+
"""
|
|
51
|
+
labels: dict[str, str] = {}
|
|
52
|
+
# Track slug collisions: two equations both tagged `\tag{3}` get
|
|
53
|
+
# `eq-3` and `eq-3-2`. Stable across a single build.
|
|
54
|
+
used_slugs: dict[str, int] = {}
|
|
55
|
+
|
|
56
|
+
def replace(match: re.Match[str]) -> str:
|
|
57
|
+
math = match.group("math")
|
|
58
|
+
tag_match = _TAG_RE.search(math)
|
|
59
|
+
if not tag_match:
|
|
60
|
+
return match.group(0)
|
|
61
|
+
label = tag_match.group("label").strip()
|
|
62
|
+
slug_base = _SLUG_RE.sub("-", label.lower()).strip("-") or "tag"
|
|
63
|
+
count = used_slugs.get(slug_base, 0) + 1
|
|
64
|
+
used_slugs[slug_base] = count
|
|
65
|
+
slug = slug_base if count == 1 else f"{slug_base}-{count}"
|
|
66
|
+
eq_id = f"eq-{slug}"
|
|
67
|
+
display = f"({label})"
|
|
68
|
+
labels[eq_id] = display
|
|
69
|
+
|
|
70
|
+
clean_math = _TAG_RE.sub("", math).rstrip()
|
|
71
|
+
return (
|
|
72
|
+
f'<div class="equation" id="{_escape(eq_id)}">'
|
|
73
|
+
f'<div class="math block">\\[{clean_math}\\]</div>'
|
|
74
|
+
f'<span class="eq-tag">{_escape(display)}</span>'
|
|
75
|
+
f"</div>"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
new_html = _BLOCK_RE.sub(replace, html)
|
|
79
|
+
return new_html, labels
|
simplex/web/notes.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
r"""Render a deck's notes.md (or a raw markdown string) to academic-style HTML.
|
|
2
|
+
|
|
3
|
+
Pipeline:
|
|
4
|
+
|
|
5
|
+
1. markdown-it (commonmark) + plugins:
|
|
6
|
+
- ``dollarmath_plugin`` -- ``$...$`` / ``$$...$$`` -> KaTeX-friendly
|
|
7
|
+
``\(...\)`` / ``\[...\]``.
|
|
8
|
+
- ``footnote_plugin`` -- ``^[note]`` inline notes (Tufte sidenotes).
|
|
9
|
+
- ``anchors_plugin`` -- heading anchors for deep linking.
|
|
10
|
+
- ``slide_ref`` -- ``[slide:N]`` -> in-page clickable jumps.
|
|
11
|
+
- ``cite`` -- ``\cite{key1,key2}`` -> alpha-tag bibliography
|
|
12
|
+
links; cited keys collected on ``state.env["citations"]``.
|
|
13
|
+
2. Pygments syntax highlight for fenced code blocks (DarculaStyle).
|
|
14
|
+
3. Post-process footnote HTML into Tufte sidenote markup
|
|
15
|
+
(`web/sidenotes.py`).
|
|
16
|
+
4. Append the rendered bibliography (``<ol class="bib-list">``) when a
|
|
17
|
+
``Bibliography`` was supplied and any keys were cited.
|
|
18
|
+
|
|
19
|
+
Math (``$...$`` / ``$$...$$``) is rewritten with KaTeX-friendly ``\\(...\\)``
|
|
20
|
+
and ``\\[...\\]`` delimiters so katex auto-render (loaded in base.html) can
|
|
21
|
+
typeset it client-side. Fenced code blocks are highlighted server-side with
|
|
22
|
+
Pygments using the same DarculaStyle the video engine uses, so notes match
|
|
23
|
+
the slides visually.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from markdown_it import MarkdownIt
|
|
30
|
+
from mdit_py_plugins.anchors import anchors_plugin
|
|
31
|
+
from mdit_py_plugins.dollarmath import dollarmath_plugin
|
|
32
|
+
from mdit_py_plugins.footnote import footnote_plugin
|
|
33
|
+
from pygments import highlight as _pyg_highlight
|
|
34
|
+
from pygments.formatters import HtmlFormatter
|
|
35
|
+
from pygments.lexers import get_lexer_by_name, guess_lexer
|
|
36
|
+
from pygments.util import ClassNotFound
|
|
37
|
+
|
|
38
|
+
from simplex.theme.pygments_style import DarculaStyle
|
|
39
|
+
from simplex.web import callouts, equations, sidenotes
|
|
40
|
+
from simplex.web.bibliography import Bibliography
|
|
41
|
+
from simplex.web.citations import ENV_KEY as _CITE_ENV_KEY
|
|
42
|
+
from simplex.web.citations import make_plugin as cite_plugin
|
|
43
|
+
from simplex.web.refs import make_plugin as ref_plugin
|
|
44
|
+
from simplex.web.slide_ref import make_plugin as slide_ref_plugin
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _math_renderer(content: str, options: dict[str, Any]) -> str:
|
|
48
|
+
content = content.strip()
|
|
49
|
+
if options.get("display_mode"):
|
|
50
|
+
return f"\\[{content}\\]"
|
|
51
|
+
return f"\\({content}\\)"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# `nowrap=True` strips the Pygments <div><pre> wrap so markdown-it's own
|
|
55
|
+
# <pre><code> is the only wrap. The Darcula background lives on
|
|
56
|
+
# `.deck-notes pre` in simplex.css.
|
|
57
|
+
_FORMATTER = HtmlFormatter(nowrap=True, noclasses=True, style=DarculaStyle)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _highlight(code: str, lang: str, _attrs: str) -> str:
|
|
61
|
+
try:
|
|
62
|
+
lexer = get_lexer_by_name(lang) if lang else guess_lexer(code)
|
|
63
|
+
except ClassNotFound:
|
|
64
|
+
return "" # markdown-it falls back to its default <pre><code>
|
|
65
|
+
return _pyg_highlight(code, lexer, _FORMATTER)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _make(
|
|
69
|
+
slide_count: int | None = None,
|
|
70
|
+
bibliography: Bibliography | None = None,
|
|
71
|
+
) -> MarkdownIt:
|
|
72
|
+
md = MarkdownIt(
|
|
73
|
+
"commonmark",
|
|
74
|
+
{
|
|
75
|
+
"html": False,
|
|
76
|
+
"linkify": True,
|
|
77
|
+
"typographer": True,
|
|
78
|
+
"highlight": _highlight,
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
md.enable("table")
|
|
82
|
+
md.use(dollarmath_plugin, allow_labels=True, renderer=_math_renderer)
|
|
83
|
+
# `inline=True` enables `^[note]` inline footnotes -- post-processed into
|
|
84
|
+
# Tufte sidenotes by `sidenotes.transform`.
|
|
85
|
+
md.use(footnote_plugin, inline=True, move_to_end=True)
|
|
86
|
+
md.use(anchors_plugin, max_level=3)
|
|
87
|
+
md.use(slide_ref_plugin(slide_count=slide_count))
|
|
88
|
+
md.use(cite_plugin(bibliography))
|
|
89
|
+
md.use(ref_plugin())
|
|
90
|
+
return md
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def render_text(
|
|
94
|
+
markdown: str,
|
|
95
|
+
*,
|
|
96
|
+
slide_count: int | None = None,
|
|
97
|
+
bibliography: Bibliography | None = None,
|
|
98
|
+
) -> str:
|
|
99
|
+
"""Render a markdown string to academic-style HTML.
|
|
100
|
+
|
|
101
|
+
Pass `bibliography` to enable `\\cite{key}` -> linked alpha tags and a
|
|
102
|
+
trailing ``<section class="bibliography">``. When omitted, ``\\cite{}``
|
|
103
|
+
markers render as the literal `[key?]` "stale" tags.
|
|
104
|
+
"""
|
|
105
|
+
md = _make(slide_count=slide_count, bibliography=bibliography)
|
|
106
|
+
env: dict[str, Any] = {}
|
|
107
|
+
body = md.render(markdown, env)
|
|
108
|
+
body = sidenotes.transform(body)
|
|
109
|
+
# Equations first so the labels they emit are visible to the callouts
|
|
110
|
+
# pass, which resolves every `\ref{...}` placeholder in one walk.
|
|
111
|
+
body, eq_labels = equations.transform(body)
|
|
112
|
+
# Callouts after sidenotes so blockquote-shaped sidenotes (unlikely but
|
|
113
|
+
# possible) don't get mis-rewritten; before bibliography so `\ref{}`
|
|
114
|
+
# placeholders that point at callouts (or equations) can be resolved
|
|
115
|
+
# while we're still walking the body.
|
|
116
|
+
body = callouts.transform(body, extra_labels=eq_labels)
|
|
117
|
+
if bibliography is not None:
|
|
118
|
+
used = tuple(env.get(_CITE_ENV_KEY, []))
|
|
119
|
+
if used:
|
|
120
|
+
body += bibliography.to_html(used)
|
|
121
|
+
return body
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def render(
|
|
125
|
+
notes_md: Path,
|
|
126
|
+
*,
|
|
127
|
+
slide_count: int | None = None,
|
|
128
|
+
bibliography: Bibliography | None = None,
|
|
129
|
+
) -> str:
|
|
130
|
+
"""Render a notes.md file to HTML."""
|
|
131
|
+
return render_text(
|
|
132
|
+
notes_md.read_text(encoding="utf-8"),
|
|
133
|
+
slide_count=slide_count,
|
|
134
|
+
bibliography=bibliography,
|
|
135
|
+
)
|
simplex/web/refs.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
r"""markdown-it-py plugin -- `\ref{id}` -> cross-reference placeholder.
|
|
2
|
+
|
|
3
|
+
Emits ``<a class="ref" data-simplex-ref="id" href="#id">id</a>``. The
|
|
4
|
+
callouts post-processor (`web/callouts.py`) resolves the placeholder to
|
|
5
|
+
the proper display label (`Theorem 3.1`) once block IDs are known.
|
|
6
|
+
|
|
7
|
+
Mirrors `\cite{...}`: registered *before* `escape` so the leading
|
|
8
|
+
backslash survives, advances `state.pos` in silent mode so caller
|
|
9
|
+
loops (`parseLinkLabel`, used by inline footnotes) terminate.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from markdown_it import MarkdownIt
|
|
16
|
+
from markdown_it.rules_inline import StateInline
|
|
17
|
+
|
|
18
|
+
NAME = "ref"
|
|
19
|
+
|
|
20
|
+
# `\ref{id}` -- id can include letters, digits, dashes, dots, colons, slashes.
|
|
21
|
+
_PATTERN = re.compile(r"\\ref\{([A-Za-z0-9_:\-./]+)\}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_plugin() -> Any:
|
|
25
|
+
"""Return a markdown-it plugin emitting cross-reference placeholders."""
|
|
26
|
+
|
|
27
|
+
def plugin(md: MarkdownIt) -> None:
|
|
28
|
+
def rule(state: StateInline, silent: bool) -> bool:
|
|
29
|
+
if state.src[state.pos] != "\\":
|
|
30
|
+
return False
|
|
31
|
+
match = _PATTERN.match(state.src, state.pos)
|
|
32
|
+
if not match:
|
|
33
|
+
return False
|
|
34
|
+
if silent:
|
|
35
|
+
state.pos = match.end()
|
|
36
|
+
return True
|
|
37
|
+
ref_id = match.group(1)
|
|
38
|
+
token = state.push("html_inline", "", 0)
|
|
39
|
+
token.content = (
|
|
40
|
+
f'<a class="ref" data-simplex-ref="{_attr(ref_id)}" '
|
|
41
|
+
f'href="#{_attr(ref_id)}">{_text(ref_id)}</a>'
|
|
42
|
+
)
|
|
43
|
+
state.pos = match.end()
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
md.inline.ruler.before("escape", NAME, rule)
|
|
47
|
+
|
|
48
|
+
return plugin
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_ATTR = {"&": "&", "<": "<", '"': """}
|
|
52
|
+
_TEXT = {"&": "&", "<": "<", ">": ">"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _attr(s: str) -> str:
|
|
56
|
+
return "".join(_ATTR.get(c, c) for c in s)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _text(s: str) -> str:
|
|
60
|
+
return "".join(_TEXT.get(c, c) for c in s)
|
simplex/web/sidenotes.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Tufte-style sidenotes via `mdit-py-plugins`'s ``footnote_plugin``.
|
|
2
|
+
|
|
3
|
+
Authors write ``^[some marginal aside]`` inline. Internally markdown-it emits
|
|
4
|
+
the standard footnote markup (`<sup class="footnote-ref">` + a
|
|
5
|
+
``<section class="footnotes">`` at the bottom). We post-process that HTML so
|
|
6
|
+
each footnote body floats next to its anchor as a Tufte-style sidenote:
|
|
7
|
+
|
|
8
|
+
text<sup class="sidenote-ref" id="snref-1">1</sup><aside
|
|
9
|
+
class="sidenote" id="sn-1" role="note">body</aside> more text.
|
|
10
|
+
|
|
11
|
+
The bottom ``<section class="footnotes">`` is removed so wide-screen readers
|
|
12
|
+
see only the marginal note; narrow screens collapse the sidenote inline (via
|
|
13
|
+
CSS in ``static/simplex.css``).
|
|
14
|
+
|
|
15
|
+
Pure HTML transformation -- no JS, no token-tree gymnastics. This keeps the
|
|
16
|
+
markdown-it plugin chain (footnotes + dollarmath + citations + slide-ref)
|
|
17
|
+
working without conflicts.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
# Matches one <li id="fnN" ...>...</li>, capturing the id and the inner HTML
|
|
23
|
+
# (greedy enough to span nested tags, lazy enough to stop at the next list
|
|
24
|
+
# item). We rely on the footnote plugin's stable shape.
|
|
25
|
+
_LI_RE = re.compile(
|
|
26
|
+
r'<li\s+id="fn(?P<n>\d+)"[^>]*>\s*(?P<body>.*?)\s*</li>',
|
|
27
|
+
re.DOTALL,
|
|
28
|
+
)
|
|
29
|
+
_REF_RE = re.compile(
|
|
30
|
+
r'<sup\s+class="footnote-ref">\s*<a[^>]*href="#fn(?P<n>\d+)"[^>]*>\[(?P<num>\d+)\]</a>\s*</sup>',
|
|
31
|
+
)
|
|
32
|
+
_BACKREF_RE = re.compile(r'\s*<a[^>]*class="footnote-backref"[^>]*>[^<]*</a>')
|
|
33
|
+
_SECTION_RE = re.compile(
|
|
34
|
+
r'<hr\s+class="footnotes-sep"\s*/?>\s*<section\s+class="footnotes">.*?</section>',
|
|
35
|
+
re.DOTALL,
|
|
36
|
+
)
|
|
37
|
+
_OUTER_P_RE = re.compile(r"^\s*<p>(.*)</p>\s*$", re.DOTALL)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def transform(html: str) -> str:
|
|
41
|
+
"""Rewrite footnote markup in ``html`` into Tufte sidenote markup."""
|
|
42
|
+
bodies = _extract_bodies(html)
|
|
43
|
+
if not bodies:
|
|
44
|
+
return html
|
|
45
|
+
html = _SECTION_RE.sub("", html)
|
|
46
|
+
|
|
47
|
+
def replace_ref(match: re.Match[str]) -> str:
|
|
48
|
+
n = match.group("n")
|
|
49
|
+
num = match.group("num")
|
|
50
|
+
body = bodies.get(n, "")
|
|
51
|
+
return _render_sidenote(n, num, body)
|
|
52
|
+
|
|
53
|
+
return _REF_RE.sub(replace_ref, html)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _extract_bodies(html: str) -> dict[str, str]:
|
|
57
|
+
bodies: dict[str, str] = {}
|
|
58
|
+
for match in _LI_RE.finditer(html):
|
|
59
|
+
n = match.group("n")
|
|
60
|
+
body = _BACKREF_RE.sub("", match.group("body"))
|
|
61
|
+
# Footnote bodies are wrapped in a <p>; for an inline sidenote we want
|
|
62
|
+
# the inner content, not a block-level <p>.
|
|
63
|
+
single_para = _OUTER_P_RE.match(body)
|
|
64
|
+
if single_para:
|
|
65
|
+
body = single_para.group(1)
|
|
66
|
+
bodies[n] = body.strip()
|
|
67
|
+
return bodies
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _render_sidenote(n: str, num: str, body: str) -> str:
|
|
71
|
+
return (
|
|
72
|
+
f'<label for="sn-toggle-{n}" class="sidenote-ref" id="snref-{n}">{num}</label>'
|
|
73
|
+
f'<input type="checkbox" id="sn-toggle-{n}" class="sidenote-toggle" '
|
|
74
|
+
'aria-hidden="true" />'
|
|
75
|
+
f'<aside class="sidenote" id="sn-{n}" role="note">{body}</aside>'
|
|
76
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Site-wide configuration: committed `site.toml` merged with env overrides.
|
|
2
|
+
|
|
3
|
+
`site.toml` (in git) carries brand/nav/section ordering.
|
|
4
|
+
Env (`SIMPLEX_GA_TAG`, `SIMPLEX_BASE_URL`, `SIMPLEX_BRAND`, `SIMPLEX_PREVIEW`)
|
|
5
|
+
carries deployment concerns and is never committed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import tomllib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Self
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NavLink(BaseModel):
|
|
17
|
+
model_config = ConfigDict(frozen=True)
|
|
18
|
+
label: str
|
|
19
|
+
href: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SiteConfig(BaseModel):
|
|
23
|
+
model_config = ConfigDict(frozen=True)
|
|
24
|
+
|
|
25
|
+
brand: str = "Simplex"
|
|
26
|
+
tagline: str | None = None
|
|
27
|
+
nav: tuple[NavLink, ...] = ()
|
|
28
|
+
default_section_order: tuple[str, ...] = ()
|
|
29
|
+
|
|
30
|
+
# Deployment-only fields (loaded from env, not committed).
|
|
31
|
+
ga_tag: str = ""
|
|
32
|
+
base_url: str = "/"
|
|
33
|
+
preview: bool = False
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def ga_enabled(self) -> bool:
|
|
37
|
+
return bool(self.ga_tag) and not self.preview
|
|
38
|
+
|
|
39
|
+
def url(self, path: str) -> str:
|
|
40
|
+
"""Resolve a site-relative path against `base_url`."""
|
|
41
|
+
base = self.base_url.rstrip("/")
|
|
42
|
+
clean = path.lstrip("/")
|
|
43
|
+
if not base:
|
|
44
|
+
return f"/{clean}"
|
|
45
|
+
return f"{base}/{clean}"
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def load(cls, repo_root: Path | None = None) -> Self:
|
|
49
|
+
repo_root = repo_root or Path.cwd()
|
|
50
|
+
committed: dict[str, Any] = {}
|
|
51
|
+
toml_path = repo_root / "site.toml"
|
|
52
|
+
if toml_path.exists():
|
|
53
|
+
committed = dict(tomllib.loads(toml_path.read_text(encoding="utf-8")))
|
|
54
|
+
nav_raw = committed.pop("nav", ()) or ()
|
|
55
|
+
if isinstance(nav_raw, list):
|
|
56
|
+
committed["nav"] = tuple(NavLink(**dict(item)) for item in nav_raw)
|
|
57
|
+
dso = committed.get("default_section_order")
|
|
58
|
+
if isinstance(dso, list):
|
|
59
|
+
committed["default_section_order"] = tuple(dso)
|
|
60
|
+
|
|
61
|
+
env_overrides: dict[str, Any] = {}
|
|
62
|
+
if (ga := os.environ.get("SIMPLEX_GA_TAG")) is not None:
|
|
63
|
+
env_overrides["ga_tag"] = ga
|
|
64
|
+
if (base := os.environ.get("SIMPLEX_BASE_URL")) is not None:
|
|
65
|
+
env_overrides["base_url"] = base
|
|
66
|
+
if (brand := os.environ.get("SIMPLEX_BRAND")) is not None:
|
|
67
|
+
env_overrides["brand"] = brand
|
|
68
|
+
if (preview := os.environ.get("SIMPLEX_PREVIEW")) is not None:
|
|
69
|
+
env_overrides["preview"] = preview.lower() in {"1", "true", "yes", "on"}
|
|
70
|
+
|
|
71
|
+
return cls.model_validate(committed | env_overrides)
|
simplex/web/slide_ref.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""markdown-it-py plugin -- ``[slide:N]`` => clickable jump anchor.
|
|
2
|
+
|
|
3
|
+
Renders as ``<a href="#" class="slide-ref" data-slide="{N}">N</a>``. The
|
|
4
|
+
parent viewer.js (see ``web/static/viewer.js``) binds clicks and forwards
|
|
5
|
+
``{type: 'simplex.goto', idx: N}`` to the iframe, which subtracts one to
|
|
6
|
+
reach Reveal's 0-based horizontal index. The 1-based convention here
|
|
7
|
+
matches the sidebar's ``data-slide-target`` (= ``MainSlide.index``) so
|
|
8
|
+
both navigation paths speak the same vocabulary.
|
|
9
|
+
|
|
10
|
+
If ``slide_count`` is provided and N is out of range, the anchor is emitted
|
|
11
|
+
with the extra class ``slide-ref-stale`` so the build flags it visually
|
|
12
|
+
without breaking the page.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from markdown_it import MarkdownIt
|
|
19
|
+
from markdown_it.rules_inline import StateInline
|
|
20
|
+
|
|
21
|
+
NAME = "slide_ref"
|
|
22
|
+
_PATTERN = re.compile(r"\[slide:(\d+)\]")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_plugin(slide_count: int | None = None) -> Any:
|
|
26
|
+
"""Return a markdown-it plugin bound to a slide count for validation."""
|
|
27
|
+
|
|
28
|
+
def plugin(md: MarkdownIt) -> None:
|
|
29
|
+
def rule(state: StateInline, silent: bool) -> bool:
|
|
30
|
+
if state.src[state.pos] != "[":
|
|
31
|
+
return False
|
|
32
|
+
match = _PATTERN.match(state.src, state.pos)
|
|
33
|
+
if not match:
|
|
34
|
+
return False
|
|
35
|
+
# Silent / validation mode: must still advance `state.pos` past
|
|
36
|
+
# the match so callers like `parseLinkLabel` don't loop forever.
|
|
37
|
+
if silent:
|
|
38
|
+
state.pos = match.end()
|
|
39
|
+
return True
|
|
40
|
+
n = int(match.group(1))
|
|
41
|
+
stale = slide_count is not None and (n < 1 or n > slide_count)
|
|
42
|
+
token = state.push("html_inline", "", 0)
|
|
43
|
+
classes = "slide-ref" + (" slide-ref-stale" if stale else "")
|
|
44
|
+
title = "Slide out of range" if stale else f"Jump to slide {n}"
|
|
45
|
+
token.content = (
|
|
46
|
+
f'<a href="#" class="{classes}" data-slide="{n}" '
|
|
47
|
+
f'role="button" aria-label="{title}" title="{title}">{n}</a>'
|
|
48
|
+
)
|
|
49
|
+
state.pos = match.end()
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
md.inline.ruler.before("link", NAME, rule)
|
|
53
|
+
|
|
54
|
+
return plugin
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# web/static/
|
|
2
|
+
|
|
3
|
+
Vendored runtime assets, copied verbatim to `site/static/` at build time.
|
|
4
|
+
|
|
5
|
+
## Committed
|
|
6
|
+
|
|
7
|
+
- `simplex.css` -- site-specific styles (carousel, deck page, academic
|
|
8
|
+
notes typography, Tufte sidenotes, citations, bibliography).
|
|
9
|
+
- `viewer.js` -- parent-page bridge for the deck iframe + carousel arrows.
|
|
10
|
+
|
|
11
|
+
## Vendored for builds (not committed)
|
|
12
|
+
|
|
13
|
+
- `tailwind.js` (Tailwind Play CDN -- JIT runtime, required for arbitrary-value classes)
|
|
14
|
+
- `katex/` (CSS + fonts + JS + auto-render)
|
|
15
|
+
- `reveal.js/` (`reveal.js`, `reveal.css`, `reset.css`)
|
|
16
|
+
- `htmx.min.js` (optional, kept for future progressive enhancement)
|
|
17
|
+
- `fonts/lato/` -- Lato 400/700/900 + italics (UI + headings)
|
|
18
|
+
- `fonts/merriweather/` -- Merriweather 400/700/900 + italics (body notes)
|
|
19
|
+
|
|
20
|
+
## Don't
|
|
21
|
+
|
|
22
|
+
- Don't load these via CDN -- vendoring keeps Pages offline-safe.
|
|
23
|
+
- Don't edit the vendored files; upgrade them with `scripts/vendor.sh`.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|