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.
- {gradientgrove-0.1.5.dev1/src/gradientgrove.egg-info → gradientgrove-0.1.7}/PKG-INFO +3 -2
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/README.md +2 -1
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/pyproject.toml +1 -1
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/gradientgrove.css +74 -37
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/latex_fast_renderer.py +129 -6
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/latex_fence.py +234 -1
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/markdown_ast.py +389 -145
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/markdown_publish.py +125 -10
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7/src/gradientgrove.egg-info}/PKG-INFO +3 -2
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/LICENSE +0 -0
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/setup.cfg +0 -0
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/__init__.py +0 -0
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove/publish.py +0 -0
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove.egg-info/SOURCES.txt +0 -0
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove.egg-info/dependency_links.txt +0 -0
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove.egg-info/entry_points.txt +0 -0
- {gradientgrove-0.1.5.dev1 → gradientgrove-0.1.7}/src/gradientgrove.egg-info/requires.txt +0 -0
- {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.
|
|
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:
|
|
58
|
+
author:
|
|
59
|
+
- Firstname Lastname
|
|
59
60
|
---
|
|
60
61
|
|
|
61
62
|
# Background
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gradientgrove"
|
|
7
|
-
version = "0.1.
|
|
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
|
|
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:
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
)
|