Skip to content

Commit d6c05b9

Browse files
committed
replace sphinx image directives with jupyter ones on download
1 parent e5a9248 commit d6c05b9

1 file changed

Lines changed: 48 additions & 2 deletions

File tree

docs/source/_ext/inline_downloads.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
55
1. Convert MyST admonitions to HTML ``<div class="alert ...">`` blocks.
66
2. Convert MyST dropdowns to ``<details>`` elements (with literalinclude inlined).
7-
3. Strip MyST-only inline roles to plain text.
8-
4. Remove target labels and self-referential download links.
7+
3. Inline ``{image}``/``{figure}`` directives as self-contained base64 ``<img>`` tags.
8+
4. Strip MyST-only inline roles to plain text.
9+
5. Remove target labels and self-referential download links.
910
1011
The setup cell's ``%run -i setup_tutorial.py`` is left untouched: the bootstrap at
1112
the top of that cell fetches the helpers at runtime, so the notebook is
@@ -15,7 +16,9 @@
1516

1617
from __future__ import annotations
1718

19+
import base64
1820
import json
21+
import mimetypes
1922
import re
2023
from pathlib import Path
2124
from typing import Any
@@ -68,6 +71,33 @@ def _read_include(rel_path: str, source_dir: Path) -> str | None:
6871
return None
6972

7073

74+
def _render_image(rel_path: str, options: dict[str, str], source_dir: Path) -> str:
75+
"""Render an ``{image}``/``{figure}`` target as a self-contained ``<img>`` tag.
76+
77+
The image bytes are embedded as a base64 data URI so the downloaded notebook
78+
displays them without depending on the working directory or the runtime fetch
79+
of ``include/``. Falls back to the relative path if the file is missing.
80+
"""
81+
path = source_dir / rel_path
82+
attrs = ''
83+
if (width := options.get('width')) is not None:
84+
attrs += f' width="{width}"'
85+
if (alt := options.get('alt')) is not None:
86+
attrs += f' alt="{alt}"'
87+
88+
if path.is_file():
89+
mime = mimetypes.guess_type(path.name)[0] or 'application/octet-stream'
90+
data = base64.b64encode(path.read_bytes()).decode('ascii')
91+
src = f'data:{mime};base64,{data}'
92+
else:
93+
src = rel_path
94+
95+
img = f'<img src="{src}"{attrs}>'
96+
if options.get('align') in ('center', 'left', 'right'):
97+
return f'<div align="{options["align"]}">\n{img}\n</div>'
98+
return img
99+
100+
71101
def _convert_myst_block(lines: list[str], source_dir: Path) -> list[str]:
72102
"""Process a markdown cell's lines, converting MyST block directives."""
73103
output: list[str] = []
@@ -77,6 +107,22 @@ def _convert_myst_block(lines: list[str], source_dir: Path) -> list[str]:
77107
line = lines[i]
78108
m = _FENCE_OPEN.match(line)
79109
if m is None:
110+
bm = _BACKTICK_DIRECTIVE.match(line)
111+
if bm is not None and bm.group(1) in ('image', 'figure'):
112+
rel_path = bm.group(2).strip()
113+
i += 1
114+
options = {}
115+
while i < len(lines) and (om := _DIRECTIVE_OPT.match(lines[i])) is not None:
116+
options[om.group(1)] = om.group(2)
117+
i += 1
118+
# Drop any remaining content (e.g. a figure caption) up to the
119+
# closing backtick fence.
120+
while i < len(lines) and not lines[i].strip().startswith('```'):
121+
i += 1
122+
if i < len(lines):
123+
i += 1 # consume the closing fence
124+
output.append(_render_image(rel_path, options, source_dir))
125+
continue
80126
output.append(line)
81127
i += 1
82128
continue

0 commit comments

Comments
 (0)