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.
Files changed (91) hide show
  1. simplex/README.md +32 -0
  2. simplex/cli/README.md +13 -0
  3. simplex/cli/__init__.py +5 -0
  4. simplex/cli/commands.py +384 -0
  5. simplex/deck/README.md +19 -0
  6. simplex/deck/__init__.py +7 -0
  7. simplex/deck/_template/assets/.gitkeep +0 -0
  8. simplex/deck/_template/assets/code/.gitkeep +0 -0
  9. simplex/deck/_template/assets/figures/.gitkeep +0 -0
  10. simplex/deck/_template/deck.toml +11 -0
  11. simplex/deck/_template/manim.cfg +3 -0
  12. simplex/deck/_template/notes.md +27 -0
  13. simplex/deck/_template/refs.bib +12 -0
  14. simplex/deck/_template/slides/__init__.py +7 -0
  15. simplex/deck/_template/slides/intro.py +21 -0
  16. simplex/deck/config.py +207 -0
  17. simplex/deck/registry.py +110 -0
  18. simplex/deck/scaffold.py +86 -0
  19. simplex/deck/section.py +40 -0
  20. simplex/engine/README.md +9 -0
  21. simplex/render/README.md +46 -0
  22. simplex/render/__init__.py +1 -0
  23. simplex/render/html.py +132 -0
  24. simplex/render/pdf.py +32 -0
  25. simplex/render/pptx.py +32 -0
  26. simplex/render/reconcile.py +350 -0
  27. simplex/render/runner.py +116 -0
  28. simplex/render/thumbnail.py +374 -0
  29. simplex/slides/README.md +9 -0
  30. simplex/slides/components/README.md +9 -0
  31. simplex/theme/README.md +9 -0
  32. simplex/web/README.md +33 -0
  33. simplex/web/__init__.py +1 -0
  34. simplex/web/bibliography.py +248 -0
  35. simplex/web/bibtex.py +129 -0
  36. simplex/web/builder.py +321 -0
  37. simplex/web/callouts.py +134 -0
  38. simplex/web/citations.py +118 -0
  39. simplex/web/equations.py +79 -0
  40. simplex/web/notes.py +135 -0
  41. simplex/web/refs.py +60 -0
  42. simplex/web/sidenotes.py +76 -0
  43. simplex/web/site_config.py +71 -0
  44. simplex/web/slide_ref.py +54 -0
  45. simplex/web/static/.gitkeep +0 -0
  46. simplex/web/static/README.md +23 -0
  47. simplex/web/static/fonts/lato/lato-latin-400-italic.woff2 +0 -0
  48. simplex/web/static/fonts/lato/lato-latin-400-normal.woff2 +0 -0
  49. simplex/web/static/fonts/lato/lato-latin-700-italic.woff2 +0 -0
  50. simplex/web/static/fonts/lato/lato-latin-700-normal.woff2 +0 -0
  51. simplex/web/static/fonts/lato/lato-latin-900-normal.woff2 +0 -0
  52. simplex/web/static/fonts/merriweather/merriweather-latin-400-italic.woff2 +0 -0
  53. simplex/web/static/fonts/merriweather/merriweather-latin-400-normal.woff2 +0 -0
  54. simplex/web/static/fonts/merriweather/merriweather-latin-700-italic.woff2 +0 -0
  55. simplex/web/static/fonts/merriweather/merriweather-latin-700-normal.woff2 +0 -0
  56. simplex/web/static/fonts/merriweather/merriweather-latin-900-normal.woff2 +0 -0
  57. simplex/web/static/htmx.min.js +1 -0
  58. simplex/web/static/katex/auto-render.min.js +1 -0
  59. simplex/web/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  60. simplex/web/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  61. simplex/web/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  62. simplex/web/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  63. simplex/web/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  64. simplex/web/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  65. simplex/web/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  66. simplex/web/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  67. simplex/web/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  68. simplex/web/static/katex/katex.min.css +1 -0
  69. simplex/web/static/katex/katex.min.js +1 -0
  70. simplex/web/static/lucide/README.md +7 -0
  71. simplex/web/static/lucide/lucide.min.js +12 -0
  72. simplex/web/static/notes.js +68 -0
  73. simplex/web/static/reveal.js/reset.css +30 -0
  74. simplex/web/static/reveal.js/reveal.css +8 -0
  75. simplex/web/static/reveal.js/reveal.js +9 -0
  76. simplex/web/static/simplex.css +1870 -0
  77. simplex/web/static/tailwind.js +64 -0
  78. simplex/web/static/viewer.js +428 -0
  79. simplex/web/templates/README.md +19 -0
  80. simplex/web/templates/_carousel.html +117 -0
  81. simplex/web/templates/base.html +110 -0
  82. simplex/web/templates/deck.html +149 -0
  83. simplex/web/templates/index.html +20 -0
  84. simplex/web/templates/revealjs.html.j2 +374 -0
  85. simplex/web/templates/section.html +74 -0
  86. simplex/web/vendor.py +148 -0
  87. simplex_web-0.2.0.dist-info/METADATA +166 -0
  88. simplex_web-0.2.0.dist-info/RECORD +91 -0
  89. simplex_web-0.2.0.dist-info/WHEEL +4 -0
  90. simplex_web-0.2.0.dist-info/entry_points.txt +2 -0
  91. simplex_web-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -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 = {"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;"}
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 = {"&": "&amp;", "<": "&lt;", '"': "&quot;"}
52
+ _TEXT = {"&": "&amp;", "<": "&lt;", ">": "&gt;"}
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)
@@ -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)
@@ -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`.