gradientgrove 0.1.5.dev1__tar.gz → 0.1.7__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.
Files changed (18) hide show
  1. {gradientgrove-0.1.5.dev1/src/gradientgrove.egg-info → gradientgrove-0.1.7}/PKG-INFO +3 -2
  2. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/README.md +2 -1
  3. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/pyproject.toml +1 -1
  4. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/gradientgrove.css +74 -37
  5. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/latex_fast_renderer.py +129 -6
  6. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/latex_fence.py +234 -1
  7. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/markdown_ast.py +389 -145
  8. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/markdown_publish.py +125 -10
  9. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7/src/gradientgrove.egg-info}/PKG-INFO +3 -2
  10. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/LICENSE +0 -0
  11. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/setup.cfg +0 -0
  12. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/__init__.py +0 -0
  13. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/publish.py +0 -0
  14. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove.egg-info/SOURCES.txt +0 -0
  15. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove.egg-info/dependency_links.txt +0 -0
  16. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove.egg-info/entry_points.txt +0 -0
  17. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove.egg-info/requires.txt +0 -0
  18. {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gradientgrove
3
- Version: 0.1.5.dev1
3
+ Version: 0.1.7
4
4
  Summary: Python scripts for teaching and scholarship, under development. Currently contains scripts that allow the creation of both documents and slideshow from a single Markdown file, or a Mkdocs project. Create a markdown file with mkdocs-style admonitions, compile to html, revealjs, latex, and beamer outputs.
5
5
  Author-email: Stephen Flood <stephen.flood@bridgew.edu>
6
6
  License-Expression: GPL-3.0-only
@@ -55,7 +55,8 @@ PYTHONPATH=src python -m gradientgrove.markdown_ast --help
55
55
  ```
56
56
  ---
57
57
  title : "My Document"
58
- author: "Firstname Lastname"
58
+ author:
59
+ - Firstname Lastname
59
60
  ---
60
61
 
61
62
  # Background
@@ -37,7 +37,8 @@ PYTHONPATH=src python -m gradientgrove.markdown_ast --help
37
37
  ```
38
38
  ---
39
39
  title : "My Document"
40
- author: "Firstname Lastname"
40
+ author:
41
+ - Firstname Lastname
41
42
  ---
42
43
 
43
44
  # Background
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gradientgrove"
7
- version = "0.1.5.dev1" # dev versions with "0.1.5.dev1"
7
+ version = "0.1.7" # dev versions with "0.1.5.dev1"
8
8
  authors = [
9
9
  {name="Stephen Flood", email="stephen.flood@bridgew.edu"}
10
10
  ]
@@ -28,6 +28,8 @@
28
28
  --gg-math-border: #9fb8c8;
29
29
  --gg-math-accent: #50758d;
30
30
  --gg-math-title: #294b61;
31
+ --gg-exercise-bg: #f3f6f1;
32
+ --gg-exercise-border: #819276;
31
33
  }
32
34
 
33
35
 
@@ -43,7 +45,7 @@ html {
43
45
  line-height: 1.5;
44
46
  }
45
47
 
46
- body.markdown-body {
48
+ body {
47
49
  min-width: 200px;
48
50
  max-width: 980px;
49
51
  margin: 0 auto;
@@ -70,6 +72,10 @@ h4 { font-size: 1em; }
70
72
  h5 { font-size: 0.875em; }
71
73
  h6 { font-size: 0.85em; color: var(--gg-muted); }
72
74
 
75
+ dt {
76
+ font-weight: bold;
77
+ }
78
+
73
79
  p, blockquote, ul, ol, dl, table, pre, details, .admonition {
74
80
  margin-top: 0;
75
81
  margin-bottom: 1rem;
@@ -183,6 +189,19 @@ summary {
183
189
  cursor: pointer;
184
190
  }
185
191
 
192
+ .admonition.frame,
193
+ details.frame {
194
+ background: #fff;
195
+ border: 1px solid var(--gg-border);
196
+ }
197
+
198
+ .admonition.exercise,
199
+ details.exercise {
200
+ background: var(--gg-exercise-bg);
201
+ border: 1px solid var(--gg-exercise-border);
202
+ border-left: 0.35rem solid var(--gg-exercise-border);
203
+ }
204
+
186
205
  .admonition.theorem,
187
206
  .admonition.define,
188
207
  .admonition.definition,
@@ -241,6 +260,21 @@ details > :last-child {
241
260
  margin-bottom: 0;
242
261
  }
243
262
 
263
+ .gg-maketitle {
264
+ font-size: 1.25em;
265
+ text-align: center;
266
+ margin: 2rem 0 3rem;
267
+ }
268
+
269
+ .gg-maketitle h1 {
270
+ margin-bottom: 0.5rem;
271
+ }
272
+
273
+ .gg-maketitle-author,
274
+ .gg-maketitle-date {
275
+ margin: 0.25rem 0;
276
+ }
277
+
244
278
  .gg-transparent-env {
245
279
  display: contents;
246
280
  }
@@ -249,16 +283,17 @@ details > :last-child {
249
283
  box-sizing: border-box;
250
284
  width: 8.5in;
251
285
  min-height: 11in;
252
- margin: 1rem auto;
286
+ /*margin: 1rem auto;*/
287
+ margin: 0in;
253
288
  padding: 0.75in;
254
289
  background: var(--gg-bg);
255
290
  box-shadow: 0 0 0.2in rgb(0 0 0 / 15%);
256
- break-before: page;
291
+ /* break-before: page; */
257
292
  break-after: page;
258
293
  }
259
294
 
260
295
  .gg-page-content {
261
- min-height: 9.5in;
296
+ min-height: 9in;
262
297
  display: flex;
263
298
  flex-direction: column;
264
299
  }
@@ -266,7 +301,9 @@ details > :last-child {
266
301
  .gg-vfill {
267
302
  flex: 1 1 auto;
268
303
  }
269
-
304
+ .gg-vfillbox {
305
+ border: 1px solid currentColor;
306
+ }
270
307
  .gg-vspace {
271
308
  flex: 0 0 auto;
272
309
  }
@@ -275,56 +312,56 @@ details > :last-child {
275
312
  break-before: page;
276
313
  }
277
314
 
278
- @media (max-width: 52rem) {
279
- body.markdown-body {
280
- padding: 1rem;
281
- }
282
-
283
- .gg-page {
284
- width: 100%;
285
- min-height: 0;
286
- margin: 0;
287
- padding: 1rem;
288
- box-shadow: none;
289
- }
315
+ .gg-question {
316
+ margin: 1em 0;
317
+ display: flex;
318
+ flex-direction: column;
319
+ }
320
+ .gg-question:has(.gg-vfill) {
321
+ flex: 1 1 auto;
322
+ }
323
+ .gg-question-2 {
324
+ margin-left: 2em;
325
+ }
326
+ .gg-question-3 {
327
+ margin-left: 4em;
290
328
  }
291
329
 
292
330
  @media print {
293
331
  @page {
294
332
  size: letter;
295
- margin: 1in;
333
+ /* margin: 1in; */
334
+ margin-bottom:0in;
335
+ padding: 0;
296
336
  }
297
337
 
298
338
  html,
299
339
  body {
300
- width: 8.5in;
301
- min-height: 11in;
340
+ margin: 0;
341
+ padding: 0;
302
342
  background: white;
303
343
  color: black;
304
-
305
- font-size: 10pt;
344
+ font-size: 9pt;
306
345
  font-family: serif;
307
346
  }
308
-
309
- body.markdown-body {
310
- max-width: none;
311
- margin: 0;
312
- padding: 0.75in;
313
- }
314
-
315
- body.markdown-body:has(.gg-page) {
316
- padding: 0;
317
- }
318
-
319
347
  .gg-page {
348
+ height: 9in;
349
+ min-height: 0;
350
+ width: 100%;
320
351
  margin: 0;
352
+ padding: 0;
321
353
  box-shadow: none;
322
- background: white;
323
- color: black;
324
354
  }
325
-
355
+ .gg-page-content {
356
+ height: 100%;
357
+ min-height: 0;
358
+ }
359
+ .gg-vfill {
360
+ flex: 1 1 0;
361
+ }
362
+
326
363
  a {
327
364
  color: inherit;
328
365
  text-decoration: underline;
329
366
  }
330
- }
367
+ }
@@ -31,8 +31,9 @@ LATEX_RULES = {
31
31
  "em": (Template("\\emph{$body}"), "tex"),
32
32
  "i": (Template("\\emph{$body}"), "tex"),
33
33
  "code": (Template("\\lstinline`$raw`"), "raw"),
34
- "a": (Template("\\href{$href}{$body}"), "tex"),
35
-
34
+ # "a": (Template("\\href{$href}{$body}"), "tex"),
35
+ "a": (Template("\\href{$href}{$body}\\footnote{\\url{$href}}"),"tex"),
36
+
36
37
  # Start list environments with a newline so nested lists do not attach to prior item text.
37
38
  "ul": (Template("\n\\begin{itemize}\n$body\\end{itemize}\n\n"), "tex"),
38
39
  "ol": (Template("\n\\begin{enumerate}\n$body\\end{enumerate}\n\n"), "tex"),
@@ -123,14 +124,38 @@ def render_latex_fast(root, omit_envs=None, transparent_envs=None):
123
124
  if node.kind == "document":
124
125
  return "".join(render(child) for child in node.children)
125
126
 
127
+ ### CONTROLLING SPACING and PAGEBREAKS
128
+
126
129
  if node.name == "page":
127
130
  return "\n\\newpage\n" + body(node) + "\n\\newpage\n"
128
131
 
129
132
  if node.name == "newpage":
130
133
  return "\n\\newpage\n"
131
-
134
+
135
+ # if node.name == "vfill":
136
+ # return "\n\\vspace{\\stretch{1}}\n"
132
137
  if node.name == "vfill":
133
- return "\n\\vspace{\\stretch{1}}\n"
138
+ stretch = (node.title or "").strip()
139
+ if stretch.lower() in {"", "vfill"}:
140
+ stretch = "1"
141
+ return f"\n\\vspace{{\\stretch{{{stretch}}}}}\n"
142
+
143
+ # if node.name == "vfillbox":
144
+ # # return "\n\\begin{tcolorbox}[height fill]\n\\end{tcolorbox}\n"
145
+ # # return "\n\\begin{tcolorbox}\\vspace{\\stretch{1}}\\end{tcolorbox}\n"
146
+ # return "\n\\noindent\\rule[-1em]{.4pt}{1em}\\hrulefill\\rule[-1em]{.4pt}{1em}\\par\\vspace{\\stretch{1}}\\par\\noindent\\rule{.4pt}{1em}\\hrulefill\\rule{.4pt}{1em}\\par"
147
+
148
+ if node.name == "vfillbox":
149
+ stretch = (node.title or "").strip()
150
+ if stretch.lower() in {"", "vfillbox"}:
151
+ stretch = "1"
152
+ return (
153
+ "\n\\noindent\\rule[-1em]{.4pt}{1em}"
154
+ "\\hrulefill\\rule[-1em]{.4pt}{1em}\\par"
155
+ f"\\vspace{{\\stretch{{{stretch}}}}}\\par"
156
+ "\\noindent\\rule{.4pt}{1em}"
157
+ "\\hrulefill\\rule{.4pt}{1em}\\par"
158
+ )
134
159
 
135
160
  if node.name == "vspace":
136
161
  amount = (node.title or "").strip()
@@ -138,6 +163,92 @@ def render_latex_fast(root, omit_envs=None, transparent_envs=None):
138
163
  amount = "1in"
139
164
  return f"\n\\vspace{{{amount}}}\n"
140
165
 
166
+ if node.name == "vspacebox":
167
+ amount = (node.title or "").strip()
168
+ if amount.lower() in {"", "vspacebox"}:
169
+ amount = "1in"
170
+ #return f"\n\\noindent\\framebox[\\linewidth]{{\\rule{{0pt}}{{{amount}}}}}\\par\n"
171
+
172
+ return (
173
+ f"\n\\begin{{tcolorbox}}["
174
+ f"height={amount},"
175
+ "colback=white,"
176
+ "colframe=black,"
177
+ "arc=0pt,"
178
+ "boxrule=0.4pt"
179
+ "]\n"
180
+ "\\end{tcolorbox}\n"
181
+ )
182
+
183
+ ### FORMATTING DOCUMENTS
184
+ if node.name == "maketitle":
185
+ return "\n\\maketitle"
186
+
187
+ if node.tag == "div" and "toc" in node.attrs.get("class", "").split():
188
+ return "\n\\tableofcontents\n"
189
+
190
+
191
+ ### EXAM-LIKE ENVIRONMENT
192
+ if node.name == "exam":
193
+ # return "\n\\begin{enumerate}\n" + body(node) + "\\end{enumerate}\n"
194
+ out = ""
195
+ questions_started = False
196
+ for c in node.children:
197
+ if c.name == "question" and not questions_started:
198
+ out += "\n\\begin{enumerate}\n"
199
+ questions_started = True
200
+ out += render(c)
201
+ if questions_started:
202
+ out += "\n\\end{enumerate}\\n"
203
+ return out
204
+
205
+ if node.name == "question":
206
+
207
+ points = (node.title or "").strip()
208
+ points = f"({points} points)" if points.isdigit() else ""
209
+ out = f"\n\\item {points}\n"
210
+
211
+ has_subparts = False
212
+
213
+ for c in node.children:
214
+ if c.name =="question" and not has_subparts:
215
+ out += "\n\\begin{enumerate}\n"
216
+ has_subparts = True
217
+ out += render(c)
218
+
219
+ if has_subparts:
220
+ out += "\\end{enumerate}\n"
221
+
222
+ return out
223
+
224
+
225
+ if node.name == "gradetable":
226
+ exam = node.parent
227
+ questions = [
228
+ child for child in exam.children
229
+ if child.name == "question"
230
+ ]
231
+ cols = "|c|" + "|".join(["c" for qn in questions]) + "|c|"
232
+ title_row = "Question & " + " & ".join( [ qn.attrs["number"] for qn in questions ] )
233
+ points_row = "Points & " + " & ".join([ qn.attrs["points"] for qn in questions])
234
+ score_row = "Score & " + " & ".join([ " " for qn in questions ])
235
+
236
+ total = sum([ int(qn.attrs["points"]) for qn in questions])
237
+
238
+ return (
239
+ "\n\\begin{center}\n"
240
+ + "\\setlength{\\tabcolsep}{1em} \n {\\renewcommand{\\arraystretch}{1.75} \n"
241
+ + "\\begin{tabular}{"+cols+"}\n\\hline\n"
242
+ + title_row + " & Total \\\\ \\hline \n"
243
+ + points_row + " & " + str(total) + "\\\\ \\hline \n"
244
+ + score_row + " & \\\\ \\hline"
245
+ + "\\end{tabular}\n"
246
+ + "\\end{center}\n"
247
+ )
248
+
249
+
250
+
251
+ ### OTHER THINGS:
141
252
  key = node.name if node.kind in {"admonition", "details"} else (node.tag or node.name)
142
253
 
143
254
  # Math produced by pymdownx.arithmatex is already LaTeX-like content.
@@ -146,9 +257,21 @@ def render_latex_fast(root, omit_envs=None, transparent_envs=None):
146
257
  math = raw(node).strip("\n")
147
258
  return f"\n\n{math}\n\n" if key == "div" else math
148
259
 
149
- if key == "img": # Render image
260
+ # if key == "img": # Render image
261
+ # src = node.attrs.get("src", "")
262
+ # return rf"\includegraphics[width=\linewidth]{{{src}}}"
263
+
264
+ if key == "img":
150
265
  src = node.attrs.get("src", "")
151
- return rf"\includegraphics[width=\linewidth]{{{src}}}"
266
+ width = (node.attrs.get("width") or "").strip()
267
+
268
+ if width.endswith("%"):
269
+ fraction = float(width[:-1]) / 100
270
+ width = f"{fraction:g}\\linewidth"
271
+ elif not width:
272
+ width = "\\linewidth"
273
+
274
+ return rf"\includegraphics[width={width}]{{{src}}}"
152
275
 
153
276
  if key not in LATEX_RULES:
154
277
  if node.kind in {"admonition", "details"}:
@@ -51,6 +51,7 @@ WRAPPER = r"""
51
51
 
52
52
  \usetikzlibrary{graphs, graphdrawing}
53
53
  \usegdlibrary{trees}
54
+ \usepackage{forest}
54
55
 
55
56
  %%
56
57
  \tikzstyle{decision} = [diamond, draw, fill=gray!10, text width=6em, text badly centered, inner sep=0pt]
@@ -85,12 +86,50 @@ WRAPPER = r"""
85
86
  %%%% End: TikZ SPLINE
86
87
 
87
88
  \usepackage{beamerarticle}
89
+ \usetikzlibrary{overlay-beamer-styles}
88
90
  \begin{document}
89
91
  \noindent
90
92
  %s
91
93
  \end{document}
92
94
  """.strip()
93
95
 
96
+ BEAMER_WRAPPER = (
97
+ WRAPPER
98
+ .replace(
99
+ r"\documentclass[border=1pt]{standalone}",
100
+ (
101
+ r"\def\pgfsysdriver{pgfsys-dvisvgm.def}"
102
+ "\n"
103
+ r"\documentclass[beamer]{standalone}"
104
+ ),
105
+ 1,
106
+ )
107
+ .replace(
108
+ r"\usepackage{beamerarticle}",
109
+ "",
110
+ 1,
111
+ )
112
+ .replace(
113
+ r"\begin{document}",
114
+ (
115
+ r"\setbeamercolor{background canvas}{bg=}"
116
+ "\n"
117
+ r"\begin{document}"
118
+ "\n"
119
+ r"\begin{standaloneframe}[plain]"
120
+ ),
121
+ 1,
122
+ )
123
+ .replace(
124
+ r"\end{document}",
125
+ (
126
+ r"\end{standaloneframe}"
127
+ "\n"
128
+ r"\end{document}"
129
+ ),
130
+ 1,
131
+ )
132
+ )
94
133
 
95
134
  def _compile_to_svg(latex_body: str) -> bytes:
96
135
  latex_src = WRAPPER % latex_body
@@ -124,6 +163,76 @@ def _compile_to_svg(latex_body: str) -> bytes:
124
163
  return (td / "x.svg").read_bytes()
125
164
 
126
165
 
166
+ def _compile_to_overlay_svgs(latex_body: str) -> list[bytes]:
167
+ """Special code to handle beamer-style multi-page output"""
168
+ latex_src = BEAMER_WRAPPER % latex_body
169
+
170
+ with tempfile.TemporaryDirectory() as td:
171
+ td = Path(td)
172
+ (td / "x.tex").write_text(latex_src, encoding="utf-8")
173
+
174
+ # p = subprocess.run(
175
+ # ["lualatex", "-halt-on-error", "-interaction=nonstopmode", "x.tex"],
176
+ # cwd=td,
177
+ # stdout=subprocess.PIPE,
178
+ # stderr=subprocess.STDOUT,
179
+ # text=True,
180
+ # )
181
+ p = subprocess.run(
182
+ [
183
+ "lualatex",
184
+ "--output-format=dvi",
185
+ "-halt-on-error",
186
+ "-interaction=nonstopmode",
187
+ "x.tex",
188
+ ],
189
+ cwd=td,
190
+ stdout=subprocess.PIPE,
191
+ stderr=subprocess.STDOUT,
192
+ text=True,
193
+ )
194
+ if p.returncode != 0:
195
+ raise RuntimeError(p.stdout)
196
+
197
+ # p = subprocess.run(
198
+ # [
199
+ # "dvisvgm",
200
+ # "--no-fonts",
201
+ # "--pdf",
202
+ # "--page=1-",
203
+ # str(td / "x.pdf"),
204
+ # "-o",
205
+ # str(td / "x-%p.svg"),
206
+ # ],
207
+ # cwd=td,
208
+ # stdout=subprocess.PIPE,
209
+ # stderr=subprocess.STDOUT,
210
+ # text=True,
211
+ # )
212
+ p = subprocess.run(
213
+ [
214
+ "dvisvgm",
215
+ "--no-fonts",
216
+ "--page=1-",
217
+ str(td / "x.dvi"),
218
+ "-o",
219
+ str(td / "x-%p.svg"),
220
+ ],
221
+ cwd=td,
222
+ stdout=subprocess.PIPE,
223
+ stderr=subprocess.STDOUT,
224
+ text=True,
225
+ )
226
+ if p.returncode != 0:
227
+ raise RuntimeError(p.stdout)
228
+
229
+ svg_paths = sorted(
230
+ td.glob("x-*.svg"),
231
+ key=lambda p: int(p.stem.rsplit("-", 1)[1]),
232
+ )
233
+
234
+ return [p.read_bytes() for p in svg_paths]
235
+
127
236
  def latex_svg_fence(source, language, css_class, options, md, **kwargs):
128
237
  """Render a LaTeX/TikZ fenced block as an embedded SVG image.
129
238
 
@@ -232,4 +341,128 @@ def latex_cache_fence(source, language, css_class, options, md, **kwargs):
232
341
  )
233
342
 
234
343
  data_uri = "data:image/svg+xml;base64," + base64.b64encode(svg).decode("ascii")
235
- return f'<img class="{html.escape(css_class)}" src="{data_uri}" alt="{html.escape(alt)}" />'
344
+
345
+ ## ORIGINAL: Embed a single svg image
346
+ # return f'<img class="{html.escape(css_class)}" src="{data_uri}" alt="{html.escape(alt)}" />'
347
+
348
+ ## NEW: handle latex that compiles to multiple pages (e.g. tikz from beamer)
349
+
350
+ # This is the ordinary image.
351
+ plain_img = (
352
+ f'<img class="{html.escape(css_class)}" '
353
+ f'src="{data_uri}" '
354
+ f'alt="{html.escape(alt)}" />'
355
+ )
356
+
357
+ # Try compiling a Beamer version.
358
+ overlay_hash = hashlib.sha256(
359
+ (BEAMER_WRAPPER + "\0" + source).encode("utf-8")
360
+ ).hexdigest()
361
+
362
+ def overlay_number(path):
363
+ return int(path.stem.rsplit("-", 1)[1])
364
+
365
+ overlay_paths = sorted(
366
+ cache_dir.glob(f"{overlay_hash}-*.svg"),
367
+ key=overlay_number,
368
+ )
369
+
370
+ try:
371
+ if overlay_paths:
372
+ overlay_svgs = [
373
+ path.read_bytes()
374
+ for path in overlay_paths
375
+ ]
376
+ else:
377
+ overlay_svgs = _compile_to_overlay_svgs(source)
378
+
379
+ for i, overlay_svg in enumerate(
380
+ overlay_svgs,
381
+ start=1,
382
+ ):
383
+ path = cache_dir / f"{overlay_hash}-{i}.svg"
384
+ path.write_bytes(overlay_svg)
385
+
386
+ except Exception as e:
387
+ print("BEAMER OVERLAY ERROR:")
388
+ print(e)
389
+
390
+ # Beamer failed. Just use the normal image.
391
+ return plain_img
392
+
393
+ # One Beamer page means there are no overlays.
394
+ if len(overlay_svgs) <= 1:
395
+ return plain_img
396
+
397
+ # Only now do we create a static image that Reveal should hide.
398
+ static_img = (
399
+ f'<img class="{html.escape(css_class)} latex-static" '
400
+ f'src="{data_uri}" '
401
+ f'alt="{html.escape(alt)}" />'
402
+ )
403
+
404
+ # Build one image for each Beamer page.
405
+ overlay_images = []
406
+
407
+ for i, overlay_svg in enumerate(overlay_svgs, start=1):
408
+ overlay_uri = (
409
+ "data:image/svg+xml;base64,"
410
+ + base64.b64encode(overlay_svg).decode("ascii")
411
+ )
412
+
413
+ classes = html.escape(css_class)
414
+
415
+ if i > 1:
416
+ classes += " fragment"
417
+
418
+ page_alt = html.escape(
419
+ f"{alt}, step {i} of {len(overlay_svgs)}"
420
+ )
421
+
422
+ overlay_images.append(
423
+ f'<img class="{classes}" '
424
+ f'src="{overlay_uri}" '
425
+ f'alt="{page_alt}" />'
426
+ )
427
+
428
+ #
429
+ # Scope all of the CSS to THIS type of wrapper.
430
+ #
431
+ # Normal HTML:
432
+ # static image visible
433
+ # overlay pages hidden
434
+ #
435
+ # Reveal:
436
+ # static image hidden
437
+ # overlay pages visible
438
+ return (
439
+ "<style>"
440
+ ".latex-overlay-wrapper .latex-overlays{"
441
+ "display:none;"
442
+ "}"
443
+
444
+ ".reveal .latex-overlay-wrapper .latex-static{"
445
+ "display:none !important;"
446
+ "}"
447
+
448
+ ".reveal .latex-overlay-wrapper .latex-overlays{"
449
+ "display:grid;"
450
+ "}"
451
+
452
+ ".reveal .latex-overlay-wrapper .latex-overlays img{"
453
+ "grid-area:1/1;"
454
+ "margin:auto;"
455
+ "}"
456
+
457
+ ".reveal .latex-overlay-wrapper .latex-overlays img.fragment{"
458
+ "background:var(--r-background-color,white);"
459
+ "}"
460
+ "</style>"
461
+
462
+ '<div class="latex-overlay-wrapper">'
463
+ + static_img
464
+ + '<div class="latex-overlays">'
465
+ + "".join(overlay_images)
466
+ + "</div>"
467
+ + "</div>"
468
+ )