Skip to content

Commit 1189f37

Browse files
Rapsssitopre-commit-ci[bot]jeertmans
authored
feat(convert/html): inline CSS and JS with convert --one_file --offline (#505)
* feat: Inline CSS and JS by default with --offline * chore(fmt): auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore: Add test * Add one_file parameter * chore(fmt): auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix lint * Fix typo * Fix typo * Fix IPython magic doc * Update manim_slides/convert.py Co-authored-by: Jérome Eertmans <[email protected]> * Add test for one_file=true * chore(fmt): auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update manim_slides/convert.py Co-authored-by: Jérome Eertmans <[email protected]> * Update manim_slides/convert.py Co-authored-by: Jérome Eertmans <[email protected]> * Update docs/source/reference/sharing.md Co-authored-by: Jérome Eertmans <[email protected]> * Update manim_slides/convert.py Co-authored-by: Jérome Eertmans <[email protected]> * chore(fmt): auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add changelog and tests * Fix IPython magic * Update docs/source/faq.md * Update CHANGELOG.md --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jérome Eertmans <[email protected]>
1 parent e50271b commit 1189f37

File tree

9 files changed

+240
-36
lines changed

9 files changed

+240
-36
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
(unreleased)=
1111
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.2.0...HEAD)
1212

13+
(unreleased-added)=
14+
### Added
15+
16+
- Added CSS and JS inline for `manim-slides convert` if `--offline`
17+
and `--one-file` (`-cone_file`) are used for HTML output.
18+
[@Rapsssito](https://github.com/Rapsssito) [#505](https://github.com/jeertmans/manim-slides/pull/505)
19+
20+
(unreleased-changed)=
21+
### Changed
22+
23+
- Deprecate `-cdata_uri` in favor of `-cone_file` for `manim-slides convert`.
24+
[@Rapsssito](https://github.com/Rapsssito) [#505](https://github.com/jeertmans/manim-slides/pull/505)
25+
1326
(v5.2.0)=
1427
## [v5.2.0](https://github.com/jeertmans/manim-slides/compare/v5.1.10...v5.2.0)
1528

docs/source/_static/template.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
{%- for presentation_config in presentation_configs -%}
2222
{% set outer_loop = loop %}
2323
{%- for slide_config in presentation_config.slides -%}
24-
{%- if data_uri -%}
24+
{%- if one_file -%}
2525
{% set file = file_to_data_uri(slide_config.file) %}
2626
{%- else -%}
2727
{% set file = assets_dir / slide_config.file.name %}
@@ -315,7 +315,7 @@
315315
hideCursorTime: {{ hide_cursor_time }}
316316
});
317317

318-
{% if data_uri %}
318+
{% if one_file %}
319319
// Fix found by @t-fritsch on GitHub
320320
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
321321
function fixBase64VideoBackground(event) {

docs/source/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Questions related to `manim-slides convert [SCENES]... output.html`.
102102

103103
### I moved my `.html` file and it stopped working
104104

105-
If you did not specify `-cdata_uri=true` when converting,
105+
If you did not specify `--one-file` (or `-cone_file=true`) when converting,
106106
then Manim Slides generated a folder containing all
107107
the video files, in the same folder as the HTML
108108
output. As the path to video files is a relative path,

docs/source/reference/sharing.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,10 @@ and it there to preserve the original aspect ratio (16:9).
137137

138138
### Sharing ONE HTML file
139139

140-
If you set the `data_uri` option to `true` (with `-cdata_uri=true`),
141-
all animations will be data URI encoded, making the HTML a self-contained
142-
presentation file that can be shared on its own.
140+
If you set the `--one-file` flag, all animations will be data URI encoded,
141+
making the HTML a self-contained presentation file that can be shared
142+
on its own. If you also set the `--offline` flag, the JS and CSS files will
143+
be included in the HTML file as well.
143144

144145
### Over the internet
145146

manim_slides/convert.py

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import subprocess
66
import tempfile
77
import textwrap
8+
import warnings
89
import webbrowser
910
from base64 import b64encode
1011
from collections import deque
@@ -298,9 +299,9 @@ class RevealJS(Converter):
298299
Please check out https://revealjs.com/config/ for more details.
299300
"""
300301

301-
# Export option: use data-uri
302-
data_uri: bool = Field(
303-
False, description="Store all animations inside the HTML as data URI."
302+
# Export option:
303+
one_file: bool = Field(
304+
False, description="Embed all assets (e.g., animations) inside the HTML."
304305
)
305306
offline: bool = Field(
306307
False, description="Download remote assets for offline presentation."
@@ -562,11 +563,10 @@ def convert_to(self, dest: Path) -> None: # noqa: C901
562563
)
563564
full_assets_dir = dirname / assets_dir
564565

565-
if not self.data_uri or self.offline:
566+
if not self.one_file or self.offline:
566567
logger.debug(f"Assets will be saved to: {full_assets_dir}")
567-
full_assets_dir.mkdir(parents=True, exist_ok=True)
568568

569-
if not self.data_uri:
569+
if not self.one_file:
570570
num_presentation_configs = len(self.presentation_configs)
571571

572572
if num_presentation_configs > 1:
@@ -585,6 +585,7 @@ def prefix(i: int) -> str:
585585
def prefix(i: int) -> str:
586586
return ""
587587

588+
full_assets_dir.mkdir(parents=True, exist_ok=True)
588589
for i, presentation_config in enumerate(self.presentation_configs):
589590
presentation_config.copy_to(
590591
full_assets_dir, include_reversed=False, prefix=prefix(i)
@@ -611,28 +612,50 @@ def prefix(i: int) -> str:
611612
get_duration_ms=get_duration_ms,
612613
has_notes=has_notes,
613614
env=os.environ,
614-
prefix=prefix if not self.data_uri else None,
615+
prefix=prefix if not self.one_file else None,
615616
**options,
616617
)
617-
618-
if self.offline:
619-
soup = BeautifulSoup(content, "html.parser")
620-
session = requests.Session()
621-
622-
for tag, inner in [("link", "href"), ("script", "src")]:
623-
for item in soup.find_all(tag):
624-
if item.has_attr(inner) and (link := item[inner]).startswith(
625-
"http"
626-
):
627-
asset_name = link.rsplit("/", 1)[1]
628-
asset = session.get(link)
618+
# If not offline, write the content to the file
619+
if not self.offline:
620+
f.write(content)
621+
return
622+
623+
# If offline, download remote assets and store them in the assets folder
624+
soup = BeautifulSoup(content, "html.parser")
625+
session = requests.Session()
626+
627+
for tag, inner in [("link", "href"), ("script", "src")]:
628+
for item in soup.find_all(tag):
629+
if item.has_attr(inner) and (link := item[inner]).startswith(
630+
"http"
631+
):
632+
asset_name = link.rsplit("/", 1)[1]
633+
asset = session.get(link)
634+
if self.one_file:
635+
# If it is a CSS file, inline it
636+
if tag == "link" and "stylesheet" in item["rel"]:
637+
item.decompose()
638+
style = soup.new_tag("style")
639+
style.string = asset.text
640+
soup.head.append(style)
641+
# If it is a JS file, inline it
642+
elif tag == "script":
643+
item.decompose()
644+
script = soup.new_tag("script")
645+
script.string = asset.text
646+
soup.head.append(script)
647+
else:
648+
raise ValueError(
649+
f"Unable to inline {tag} asset: {link}"
650+
)
651+
else:
652+
full_assets_dir.mkdir(parents=True, exist_ok=True)
629653
with open(full_assets_dir / asset_name, "wb") as asset_file:
630654
asset_file.write(asset.content)
631655

632656
item[inner] = str(assets_dir / asset_name)
633657

634-
content = str(soup)
635-
658+
content = str(soup)
636659
f.write(content)
637660

638661

@@ -919,6 +942,12 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None:
919942
help="Use the template given by FILE instead of default one. "
920943
"To echo the default template, use '--show-template'.",
921944
)
945+
@click.option(
946+
"--one-file",
947+
is_flag=True,
948+
help="Embed all local assets (e.g., video files) in the output file. "
949+
"The is a convenient alias to '-cone_file=true'.",
950+
)
922951
@click.option(
923952
"--offline",
924953
is_flag=True,
@@ -937,6 +966,7 @@ def convert(
937966
config_options: dict[str, str],
938967
template: Optional[Path],
939968
offline: bool,
969+
one_file: bool,
940970
) -> None:
941971
"""Convert SCENE(s) into a given format and writes the result in DEST."""
942972
presentation_configs = get_scenes_presentation_config(scenes, folder)
@@ -954,6 +984,28 @@ def convert(
954984
else:
955985
cls = Converter.from_string(to)
956986

987+
if (
988+
one_file
989+
and issubclass(cls, (RevealJS, HtmlZip))
990+
and "one_file" not in config_options
991+
):
992+
config_options["one_file"] = "true"
993+
994+
# Change data_uri to one_file and print a warning if present
995+
if "data_uri" in config_options:
996+
warnings.warn(
997+
"The 'data_uri' configuration option is deprecated and will be "
998+
"removed in the next major version. "
999+
"Use 'one_file' instead.",
1000+
DeprecationWarning,
1001+
stacklevel=2,
1002+
)
1003+
config_options["one_file"] = (
1004+
config_options["one_file"]
1005+
if "one_file" in config_options
1006+
else config_options.pop("data_uri")
1007+
)
1008+
9571009
if (
9581010
offline
9591011
and issubclass(cls, (RevealJS, HtmlZip))

manim_slides/ipython/ipython_magic.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def construct(self):
125125
in a cell and evaluate it. Then, a typical Jupyter notebook cell for Manim Slides
126126
could look as follows::
127127
128-
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true data_uri=true
128+
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true one_file=true
129129
130130
class MySlide(Slide):
131131
def construct(self):
@@ -222,17 +222,29 @@ def construct(self):
222222

223223
kwargs = dict(arg.split("=", 1) for arg in manim_slides_args)
224224

225-
if embed: # Embedding implies data-uri
226-
kwargs["data_uri"] = "true"
225+
# If data_uri is set, raise a warning
226+
if "data_uri" in kwargs:
227+
logger.warning(
228+
"'data_uri' configuration option is deprecated and will be removed in a future release. "
229+
"Please use 'one_file' instead."
230+
)
231+
kwargs["one_file"] = (
232+
kwargs["one_file"]
233+
if "one_file" in kwargs
234+
else kwargs.pop("data_uri")
235+
)
236+
237+
if embed: # Embedding implies one_file
238+
kwargs["one_file"] = "true"
227239

228240
# TODO: FIXME
229-
# Seems like files are blocked so date-uri is the only working option...
230-
if kwargs.get("data_uri", "false").lower().strip() == "false":
241+
# Seems like files are blocked so one_file is the only working option...
242+
if kwargs.get("one_file", "false").lower().strip() == "false":
231243
logger.warning(
232-
"data_uri option is currently automatically enabled, "
244+
"one_file option is currently automatically enabled, "
233245
"because using local video files does not seem to work properly."
234246
)
235-
kwargs["data_uri"] = "true"
247+
kwargs["one_file"] = "true"
236248

237249
presentation_configs = get_scenes_presentation_config(
238250
[clsname], Path("./slides")

manim_slides/templates/revealjs.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
{% for presentation_config in presentation_configs -%}
2323
{% set outer_loop = loop %}
2424
{%- for slide_config in presentation_config.slides -%}
25-
{%- if data_uri -%}
25+
{%- if one_file -%}
2626
{% set file = file_to_data_uri(slide_config.file) %}
2727
{%- else -%}
2828
{% set file = assets_dir / (prefix(outer_loop.index0) + slide_config.file.name) %}
@@ -320,7 +320,7 @@
320320
hideCursorTime: {{ hide_cursor_time }}
321321
});
322322

323-
{% if data_uri -%}
323+
{% if one_file -%}
324324
// Fix found by @t-fritsch on GitHub
325325
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
326326
function fixBase64VideoBackground(event) {

tests/test_convert.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from pathlib import Path
44

55
import pytest
6+
import requests
7+
from bs4 import BeautifulSoup
68

79
from manim_slides.config import PresentationConfig
810
from manim_slides.convert import (
@@ -173,6 +175,101 @@ def test_revealjs_offline_converter(
173175
]:
174176
assert (assets_dir / file).exists()
175177

178+
def test_revealjs_data_encode(
179+
self,
180+
tmp_path: Path,
181+
presentation_config: PresentationConfig,
182+
monkeypatch: pytest.MonkeyPatch,
183+
) -> None:
184+
# Mock requests.Session.get to return a fake response (should not be called)
185+
class MockResponse:
186+
def __init__(self, content: bytes, text: str, status_code: int) -> None:
187+
self.content = content
188+
self.text = text
189+
self.status_code = status_code
190+
191+
# Apply the monkeypatch
192+
monkeypatch.setattr(
193+
requests.Session,
194+
"get",
195+
lambda self, url: MockResponse(
196+
b"body { background-color: #9a3241; }",
197+
"body { background-color: #9a3241; }",
198+
200,
199+
),
200+
)
201+
out_file = tmp_path / "slides.html"
202+
RevealJS(
203+
presentation_configs=[presentation_config], offline="false", one_file="true"
204+
).convert_to(out_file)
205+
assert out_file.exists()
206+
# Check that assets are not stored
207+
assert not (tmp_path / "slides_assets").exists()
208+
209+
with open(out_file, encoding="utf-8") as file:
210+
content = file.read()
211+
212+
soup = BeautifulSoup(content, "html.parser")
213+
214+
# Check if video is encoded in base64
215+
videos = soup.find_all("section")
216+
assert all(
217+
"data:video/mp4;base64," in video["data-background-video"]
218+
for video in videos
219+
)
220+
221+
# Check if CSS is not inlined
222+
styles = soup.find_all("style")
223+
assert not any("background-color: #9a3241;" in style.string for style in styles)
224+
# Check if JS is not inlined
225+
scripts = soup.find_all("script")
226+
assert not any(
227+
"background-color: #9a3241;" in (script.string or "") for script in scripts
228+
)
229+
230+
def test_revealjs_offline_inlining(
231+
self,
232+
tmp_path: Path,
233+
presentation_config: PresentationConfig,
234+
monkeypatch: pytest.MonkeyPatch,
235+
) -> None:
236+
# Mock requests.Session.get to return a fake response
237+
class MockResponse:
238+
def __init__(self, content: bytes, text: str, status_code: int) -> None:
239+
self.content = content
240+
self.text = text
241+
self.status_code = status_code
242+
243+
# Apply the monkeypatch
244+
monkeypatch.setattr(
245+
requests.Session,
246+
"get",
247+
lambda self, url: MockResponse(
248+
b"body { background-color: #9a3241; }",
249+
"body { background-color: #9a3241; }",
250+
200,
251+
),
252+
)
253+
254+
out_file = tmp_path / "slides.html"
255+
RevealJS(
256+
presentation_configs=[presentation_config], offline="true", one_file="true"
257+
).convert_to(out_file)
258+
assert out_file.exists()
259+
260+
with open(out_file, encoding="utf-8") as file:
261+
content = file.read()
262+
263+
soup = BeautifulSoup(content, "html.parser")
264+
265+
# Check if CSS is inlined
266+
styles = soup.find_all("style")
267+
assert any("background-color: #9a3241;" in style.string for style in styles)
268+
269+
# Check if JS is inlined
270+
scripts = soup.find_all("script")
271+
assert any("background-color: #9a3241;" in script.string for script in scripts)
272+
176273
def test_htmlzip_converter(
177274
self, tmp_path: Path, presentation_config: PresentationConfig
178275
) -> None:

0 commit comments

Comments
 (0)