This commit is contained in:
Kai Vogelgesang
2025-09-18 18:33:58 +02:00
commit 489ddd75b9
76 changed files with 46624555 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
import json
from pathlib import Path
from sphinx.util import logging
from docutils import nodes
from .parser import PatchedNotebookParser
from .directive import CellMetaDirective, CellMetaNode
__version__ = "0.1"
# Used to render an element node as HTML
# see https://github.com/executablebooks/sphinx-thebe/blob/v0.2.1/sphinx_thebe/__init__.py#L219
def visit_element_html(self, node):
self.body.append(node.html())
raise nodes.SkipNode
# Used for nodes that do not need to be rendered
def skip(self, node):
raise nodes.SkipNode
def add_static_sources(app):
static_path = Path(__file__).parent / "static"
app.config.html_static_path.append(static_path.as_posix())
def enable_presentation_mode(app, pagename, templatename, context, doctree):
if not doctree or not (meta_nodes := doctree.traverse(CellMetaNode)):
# page has no cell metadata -> no slideshow
return
if not any(
"slideshow" in json.loads(node["metadata"])
for node in meta_nodes
):
# notebook was not configured to be a slideshow
return
context["header_buttons"].append(
{
"type": "javascript",
"javascript": "startPresentation()",
"tooltip": "Start presenting",
"icon": "fas fa-chart-bar",
}
)
app.add_css_file("vendor/reveal.css")
app.add_css_file("vendor/simple.css") # theme
app.add_css_file("fix-theme.css")
app.add_js_file("vendor/reveal.js")
app.add_js_file("present.js")
def setup(app):
# override the MyST-NB NotebookParser with our patched version
app.add_source_parser(PatchedNotebookParser, override=True)
# register directive and node required to process the added metadata
app.add_directive("cell_meta", CellMetaDirective)
app.add_node(
CellMetaNode,
html=(visit_element_html, None),
latex=(skip, None),
textinfo=(skip, None),
text=(skip, None),
man=(skip, None),
override=True,
)
# include sources for static js / css content
app.connect("builder-inited", add_static_sources)
# add the button and js / css files
# set priority so this runs before the download button is added
app.connect("html-page-context", enable_presentation_mode, priority=500)
return {
"version": __version__,
"parallel_read_safe": True,
"parallel_write_safe": True,
}

View File

@@ -0,0 +1,16 @@
from sphinx.util.docutils import SphinxDirective
from docutils import nodes
class CellMetaNode(nodes.Element):
def html(self):
metadata = self["metadata"]
return f'<script type="application/json" data-cell-meta="">{metadata}</script>'
class CellMetaDirective(SphinxDirective):
has_content = True
def run(self):
joined = "".join(self.content)
return [CellMetaNode(metadata=joined)]

View File

@@ -0,0 +1,218 @@
import json
from myst_nb.parser import *
SPHINX_LOGGER = logging.getLogger(__name__)
# Mostly taken and slightly adapted from https://github.com/executablebooks/MyST-NB/blob/v0.13.2/myst_nb/parser.py
class PatchedNotebookParser(MystParser):
"""Docutils parser for Markedly Structured Text (MyST) and Jupyter Notebooks."""
supported = ("myst-nb",)
translate_section_name = None
config_section = "myst-nb parser"
config_section_dependencies = ("parsers",)
def parse(
self, inputstring: str, document: nodes.document, renderer: str = "sphinx"
) -> None:
self.reporter = document.reporter
self.env = document.settings.env # type: BuildEnvironment
converter = get_nb_converter(
self.env.doc2path(self.env.docname, True),
self.env,
inputstring.splitlines(keepends=True),
)
if converter is None:
# Read the notebook as a text-document
super().parse(inputstring, document=document)
return
try:
ntbk = converter.func(inputstring)
except Exception as error:
SPHINX_LOGGER.error(
"MyST-NB: Conversion to notebook failed: %s",
error,
# exc_info=True,
location=(self.env.docname, 1),
)
return
# add outputs to notebook from the cache
if self.env.config["jupyter_execute_notebooks"] != "off":
ntbk = generate_notebook_outputs(
self.env, ntbk, show_traceback=self.env.config["execution_show_tb"]
)
# Parse the notebook content to a list of syntax tokens and an env
# containing global data like reference definitions
md_parser, env, tokens = patched_nb_to_tokens( # <-- patched here
ntbk,
(
self.env.myst_config # type: ignore[attr-defined]
if converter is None
else converter.config
),
self.env.config["nb_render_plugin"],
)
# Write the notebook's output to disk
path_doc = nb_output_to_disc(ntbk, document)
# Update our glue key list with new ones defined in this page
glue_domain = NbGlueDomain.from_env(self.env)
glue_domain.add_notebook(ntbk, path_doc)
# Render the Markdown tokens to docutils AST.
tokens_to_docutils(md_parser, env, tokens, document)
def patched_nb_to_tokens(
ntbk: nbf.NotebookNode, config: MdParserConfig, renderer_plugin: str
) -> Tuple[MarkdownIt, Dict[str, Any], List[Token]]:
"""Parse the notebook content to a list of syntax tokens and an env,
containing global data like reference definitions.
"""
md = default_parser(config)
# setup the markdown parser
# Note we disable front matter parsing,
# because this is taken from the actual notebook metadata
md.disable("front_matter", ignoreInvalid=True)
md.renderer = SphinxNBRenderer(md)
# make a sandbox where all the parsing global data,
# like reference definitions will be stored
env: Dict[str, Any] = {}
rules = md.core.ruler.get_active_rules()
# First only run pre-inline chains
# so we can collect all reference definitions, etc, before assessing references
def parse_block(src, start_line):
with md.reset_rules():
# enable only rules up to block
md.core.ruler.enableOnly(rules[: rules.index("inline")])
tokens = md.parse(src, env)
for token in tokens:
if token.map:
token.map = [start_line + token.map[0], start_line + token.map[1]]
for dup_ref in env.get("duplicate_refs", []):
if "fixed" not in dup_ref:
dup_ref["map"] = [
start_line + dup_ref["map"][0],
start_line + dup_ref["map"][1],
]
dup_ref["fixed"] = True
return tokens
block_tokens = []
source_map = ntbk.metadata.get("source_map", None)
# get language lexer name
langinfo = ntbk.metadata.get("language_info", {})
lexer = langinfo.get("pygments_lexer", langinfo.get("name", None))
if lexer is None:
ntbk.metadata.get("kernelspec", {}).get("language", None)
# TODO log warning if lexer is still None
for cell_index, nb_cell in enumerate(ntbk.cells):
# if the the source_map has been stored (for text-based notebooks),
# we use that do define the starting line for each cell
# otherwise, we set a pseudo base that represents the cell index
start_line = source_map[cell_index] if source_map else (cell_index + 1) * 10000
start_line += 1 # use base 1 rather than 0
# Skip empty cells
if len(nb_cell["source"].strip()) == 0:
continue
# skip cells tagged for removal
# TODO this logic should be deferred to a transform
tags = nb_cell.metadata.get("tags", [])
if ("remove_cell" in tags) or ("remove-cell" in tags):
continue
### Patched here
# Add a Token with a cell_meta directive, i.e.:
#
# ```cell_meta
# {"slideshow": {"slide_type": "slide"}}
# ```
block_tokens.append(
Token(
type="fence",
tag="code",
nesting=0,
attrs={},
map=[start_line, start_line],
level=0,
children=None,
content=json.dumps(nb_cell.metadata),
markup="```",
info="{cell_meta}",
meta={},
block=True,
hidden=False,
)
)
### / Patched here
if nb_cell["cell_type"] == "markdown":
# we add the cell index to tokens,
# so they can be included in the error logging,
block_tokens.extend(parse_block(nb_cell["source"], start_line))
elif nb_cell["cell_type"] == "code":
# here we do nothing but store the cell as a custom token
block_tokens.append(
Token(
"nb_code_cell",
"",
0,
meta={"cell": nb_cell, "lexer": lexer, "renderer": renderer_plugin},
map=[start_line, start_line],
)
)
# Now all definitions have been gathered,
# we run inline and post-inline chains, to expand the text.
# Note we assume here that these rules never require the actual source text,
# only acting on the existing tokens
state = StateCore("", md, env, block_tokens)
with md.reset_rules():
md.core.ruler.enableOnly(rules[rules.index("inline") :])
md.core.process(state)
# Add the front matter.
# Note that myst_parser serialises dict/list like keys, when rendering to
# docutils docinfo. These could be read back with `json.loads`.
state.tokens = [
Token(
"front_matter",
"",
0,
map=[0, 0],
content=({k: v for k, v in ntbk.metadata.items()}), # type: ignore[arg-type]
)
] + state.tokens
# If there are widgets, this will embed the state of all widgets in a script
if contains_widgets(ntbk):
state.tokens.append(
Token(
"jupyter_widget_state",
"",
0,
map=[0, 0],
meta={"state": get_widgets(ntbk)},
)
)
return md, env, state.tokens

View File

@@ -0,0 +1,14 @@
.reveal .headerlink,
.reveal .copybtn {
display: none;
}
.reveal .highlight > pre {
width: 100%;
}
.reveal .past[hidden],
.reveal .present[hidden],
.reveal .future[hidden] {
display: block !important;
}

View File

@@ -0,0 +1,145 @@
const getContent = (el) => {
let result = [];
for (const child of el.children) {
if (child.tagName.toLowerCase() === "section") {
result.push(...getContent(child));
} else {
result.push(child);
}
}
return result;
};
const contentToCells = (elements) => {
/*
type Cell = {
slideType: "slide" | "subslide" | "fragment" | "notes" | "skip" | "",
children: HTMLElement[],
}
*/
let cells = [];
let currentCell;
for (const child of elements) {
if (child.tagName.toLowerCase() === "script" && child.dataset.hasOwnProperty("cellMeta")) {
const meta = JSON.parse(child.innerText);
let slideType = (meta.slideshow || {}).slide_type;
currentCell = {
slideType: ((slideType === undefined) || (slideType == '-')) ? '' : slideType,
children: [],
};
cells.push(currentCell);
} else {
if (currentCell === undefined) continue;
currentCell.children.push(child);
}
}
return cells;
};
const startPresentation = () => {
console.log("starting presentation...");
// TODO this feels hacky
const realroot = document.querySelector(".container-xl");
let fakeroot = document.createElement("div");
fakeroot.style.width = "100vw";
fakeroot.style.height = "100vh";
fakeroot.style.position = "absolute";
fakeroot.style.left = 0;
fakeroot.style.top = 0;
// create .reveal > .slides > section structure
let revealDiv = document.createElement("div");
revealDiv.classList.add("reveal");
fakeroot.appendChild(revealDiv);
let revealSlides = document.createElement("div");
revealSlides.classList.add("slides");
revealDiv.appendChild(revealSlides);
// get content
const main = document.getElementById("main-content");
const mainDiv = main.children[0];
let content = getContent(mainDiv);
// see https://github.com/damianavila/RISE/blob/5.7.1/classic/rise/static/main.js#L219
let slide_counter = -1, subslide_counter = -1;
let slide_section, subslide_section;
function new_slide() {
slide_counter += 1;
subslide_counter = -1;
let element = document.createElement("section");
revealSlides.appendChild(element);
return element;
}
function new_subslide() {
subslide_counter += 1;
let element = document.createElement("section");
element.id = `slide-${slide_counter}-${subslide_counter}`;
slide_section.appendChild(element);
return element;
}
let content_on_slide1 = false;
slide_section = new_slide();
subslide_section = new_subslide();
let current_fragment = subslide_section;
let cells = contentToCells(content);
for (let cell of cells) {
// update slide structure
if (content_on_slide1) {
if (cell.slideType === 'slide') {
slide_section = new_slide();
current_fragment = subslide_section = new_subslide();
} else if (cell.slideType === 'subslide') {
current_fragment = subslide_section = new_subslide();
} else if (cell.slideType === 'fragment') {
cell.fragment_div = current_fragment = document.createElement("div");
cell.fragment_div.classList.add("fragment");
subslide_section.appendChild(cell.fragment_div);
}
} else if (cell.slideType !== "notes" && cell.slideType !== "skip") {
content_on_slide1 = true;
}
// add cell content to slides
if (cell.slideType === "notes") {
let aside = document.createElement("aside");
aside.classList.add("notes");
for (let child of cell.children) {
aside.appendChild(child);
}
subslide_section.appendChild(aside);
} else if (cell.slideType !== "skip") {
for (let child of cell.children) {
current_fragment.appendChild(child);
}
}
}
document.body.appendChild(fakeroot);
realroot.style.display = "none";
console.log("fakeroot ready, initializing reveal");
let deck = new Reveal(revealDiv, {
overview: false,
});
deck.initialize();
}

View File

@@ -0,0 +1,30 @@
/* http://meyerweb.com/eric/tools/css/reset/
v4.0 | 20180602
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
main, menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, main, menu, nav, section {
display: block;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,360 @@
/**
* A simple theme for reveal.js presentations, similar
* to the default theme. The accent color is darkblue.
*
* This theme is Copyright (C) 2012 Owen Versteeg, https://github.com/StereotypicalApps. It is MIT licensed.
* reveal.js is Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
*/
@import url(https://fonts.googleapis.com/css?family=News+Cycle:400,700);
@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);
section.has-dark-background, section.has-dark-background h1, section.has-dark-background h2, section.has-dark-background h3, section.has-dark-background h4, section.has-dark-background h5, section.has-dark-background h6 {
color: #fff;
}
/*********************************************
* GLOBAL STYLES
*********************************************/
:root {
--r-background-color: #fff;
--r-main-font: Lato, sans-serif;
--r-main-font-size: 40px;
--r-main-color: #000;
--r-block-margin: 20px;
--r-heading-margin: 0 0 20px 0;
--r-heading-font: News Cycle, Impact, sans-serif;
--r-heading-color: #000;
--r-heading-line-height: 1.2;
--r-heading-letter-spacing: normal;
--r-heading-text-transform: none;
--r-heading-text-shadow: none;
--r-heading-font-weight: normal;
--r-heading1-text-shadow: none;
--r-heading1-size: 3.77em;
--r-heading2-size: 2.11em;
--r-heading3-size: 1.55em;
--r-heading4-size: 1em;
--r-code-font: monospace;
--r-link-color: #00008B;
--r-link-color-dark: #00003f;
--r-link-color-hover: #0000f1;
--r-selection-background-color: rgba(0, 0, 0, 0.99);
--r-selection-color: #fff;
}
.reveal-viewport {
background: #fff;
background-color: var(--r-background-color);
}
.reveal {
font-family: var(--r-main-font);
font-size: var(--r-main-font-size);
font-weight: normal;
color: var(--r-main-color);
}
.reveal ::selection {
color: var(--r-selection-color);
background: var(--r-selection-background-color);
text-shadow: none;
}
.reveal ::-moz-selection {
color: var(--r-selection-color);
background: var(--r-selection-background-color);
text-shadow: none;
}
.reveal .slides section,
.reveal .slides section > section {
line-height: 1.3;
font-weight: inherit;
}
/*********************************************
* HEADERS
*********************************************/
.reveal h1,
.reveal h2,
.reveal h3,
.reveal h4,
.reveal h5,
.reveal h6 {
margin: var(--r-heading-margin);
color: var(--r-heading-color);
font-family: var(--r-heading-font);
font-weight: var(--r-heading-font-weight);
line-height: var(--r-heading-line-height);
letter-spacing: var(--r-heading-letter-spacing);
text-transform: var(--r-heading-text-transform);
text-shadow: var(--r-heading-text-shadow);
word-wrap: break-word;
}
.reveal h1 {
font-size: var(--r-heading1-size);
}
.reveal h2 {
font-size: var(--r-heading2-size);
}
.reveal h3 {
font-size: var(--r-heading3-size);
}
.reveal h4 {
font-size: var(--r-heading4-size);
}
.reveal h1 {
text-shadow: var(--r-heading1-text-shadow);
}
/*********************************************
* OTHER
*********************************************/
.reveal p {
margin: var(--r-block-margin) 0;
line-height: 1.3;
}
/* Remove trailing margins after titles */
.reveal h1:last-child,
.reveal h2:last-child,
.reveal h3:last-child,
.reveal h4:last-child,
.reveal h5:last-child,
.reveal h6:last-child {
margin-bottom: 0;
}
/* Ensure certain elements are never larger than the slide itself */
.reveal img,
.reveal video,
.reveal iframe {
max-width: 95%;
max-height: 95%;
}
.reveal strong,
.reveal b {
font-weight: bold;
}
.reveal em {
font-style: italic;
}
.reveal ol,
.reveal dl,
.reveal ul {
display: inline-block;
text-align: left;
margin: 0 0 0 1em;
}
.reveal ol {
list-style-type: decimal;
}
.reveal ul {
list-style-type: disc;
}
.reveal ul ul {
list-style-type: square;
}
.reveal ul ul ul {
list-style-type: circle;
}
.reveal ul ul,
.reveal ul ol,
.reveal ol ol,
.reveal ol ul {
display: block;
margin-left: 40px;
}
.reveal dt {
font-weight: bold;
}
.reveal dd {
margin-left: 40px;
}
.reveal blockquote {
display: block;
position: relative;
width: 70%;
margin: var(--r-block-margin) auto;
padding: 5px;
font-style: italic;
background: rgba(255, 255, 255, 0.05);
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2);
}
.reveal blockquote p:first-child,
.reveal blockquote p:last-child {
display: inline-block;
}
.reveal q {
font-style: italic;
}
.reveal pre {
display: block;
position: relative;
width: 90%;
margin: var(--r-block-margin) auto;
text-align: left;
font-size: 0.55em;
font-family: var(--r-code-font);
line-height: 1.2em;
word-wrap: break-word;
box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.15);
}
.reveal code {
font-family: var(--r-code-font);
text-transform: none;
tab-size: 2;
}
.reveal pre code {
display: block;
padding: 5px;
overflow: auto;
max-height: 400px;
word-wrap: normal;
}
.reveal .code-wrapper {
white-space: normal;
}
.reveal .code-wrapper code {
white-space: pre;
}
.reveal table {
margin: auto;
border-collapse: collapse;
border-spacing: 0;
}
.reveal table th {
font-weight: bold;
}
.reveal table th,
.reveal table td {
text-align: left;
padding: 0.2em 0.5em 0.2em 0.5em;
border-bottom: 1px solid;
}
.reveal table th[align=center],
.reveal table td[align=center] {
text-align: center;
}
.reveal table th[align=right],
.reveal table td[align=right] {
text-align: right;
}
.reveal table tbody tr:last-child th,
.reveal table tbody tr:last-child td {
border-bottom: none;
}
.reveal sup {
vertical-align: super;
font-size: smaller;
}
.reveal sub {
vertical-align: sub;
font-size: smaller;
}
.reveal small {
display: inline-block;
font-size: 0.6em;
line-height: 1.2em;
vertical-align: top;
}
.reveal small * {
vertical-align: top;
}
.reveal img {
margin: var(--r-block-margin) 0;
}
/*********************************************
* LINKS
*********************************************/
.reveal a {
color: var(--r-link-color);
text-decoration: none;
transition: color 0.15s ease;
}
.reveal a:hover {
color: var(--r-link-color-hover);
text-shadow: none;
border: none;
}
.reveal .roll span:after {
color: #fff;
background: var(--r-link-color-dark);
}
/*********************************************
* Frame helper
*********************************************/
.reveal .r-frame {
border: 4px solid var(--r-main-color);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
}
.reveal a .r-frame {
transition: all 0.15s linear;
}
.reveal a:hover .r-frame {
border-color: var(--r-link-color);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.55);
}
/*********************************************
* NAVIGATION CONTROLS
*********************************************/
.reveal .controls {
color: var(--r-link-color);
}
/*********************************************
* PROGRESS BAR
*********************************************/
.reveal .progress {
background: rgba(0, 0, 0, 0.2);
color: var(--r-link-color);
}
/*********************************************
* PRINT BACKGROUND
*********************************************/
@media print {
.backgrounds {
background-color: var(--r-background-color);
}
}