feat(convert): allow fully offline HTML presentation (#440)

* feat(cli): allow offline HTML presentations

* feat(convert): allow fully offline HTML presentation

TODO: check if this is really the case, especially for nested dependencies?

Closes #438

* fix(cli): typo

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(tests): add tests

* fix(cli): use full path

* fix(tests): typo

* chore(ci): avoid specific kernel name

* fix ?

* chore(lib): simplify logic

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Jérome Eertmans
2024-11-01 10:00:48 +01:00
committed by GitHub
parent 75af26e601
commit d8acbae165
8 changed files with 106 additions and 45 deletions

View File

@ -6,9 +6,6 @@ build:
apt_packages:
- libpango1.0-dev
- ffmpeg
jobs:
post_install:
- ipython kernel install --name "manim-slides" --user
sphinx:
builder: html
configuration: docs/source/conf.py

View File

@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.1.9...HEAD)
(unreleased-added)=
### Added
- Added `--offline` option to `manim-slides convert` for offline
HTML presentations.
[#440](https://github.com/jeertmans/manim-slides/pull/440)
(v5.1.9)=
## [v5.1.9](https://github.com/jeertmans/manim-slides/compare/v5.1.8...v5.1.9)

View File

@ -78,9 +78,9 @@
],
"metadata": {
"kernelspec": {
"display_name": "manim-slides",
"display_name": ".venv",
"language": "python",
"name": "manim-slides"
"name": "python3"
},
"language_info": {
"codemirror_mode": {
@ -92,7 +92,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
"version": "3.11.8"
}
},
"nbformat": 4,

View File

@ -15,6 +15,8 @@ from typing import Any, Callable, Optional, Union
import av
import click
import pptx
import requests
from bs4 import BeautifulSoup
from click import Context, Parameter
from jinja2 import Template
from lxml import etree
@ -287,8 +289,11 @@ class RevealTheme(str, StrEnum):
class RevealJS(Converter):
# Export option: use data-uri
# Export option:
data_uri: bool = False
offline: bool = Field(
False, description="Download remote assets for offline presentation."
)
# Presentation size options from RevealJS
width: Union[Str, int] = Str("100%")
height: Union[Str, int] = Str("100%")
@ -385,14 +390,11 @@ class RevealJS(Converter):
def open(self, file: Path) -> None:
webbrowser.open(file.absolute().as_uri())
def convert_to(self, dest: Path) -> None:
def convert_to(self, dest: Path) -> None: # noqa: C901
"""
Convert this configuration into a RevealJS HTML presentation, saved to
DEST.
"""
if self.data_uri:
assets_dir = Path("") # Actually we won't care.
else:
dirname = dest.parent
basename = dest.stem
ext = dest.suffix
@ -402,10 +404,11 @@ class RevealJS(Converter):
)
full_assets_dir = dirname / assets_dir
if not self.data_uri or self.offline:
logger.debug(f"Assets will be saved to: {full_assets_dir}")
full_assets_dir.mkdir(parents=True, exist_ok=True)
if not self.data_uri:
num_presentation_configs = len(self.presentation_configs)
if num_presentation_configs > 1:
@ -435,6 +438,8 @@ class RevealJS(Converter):
revealjs_template = Template(self.load_template())
options = self.model_dump()
if assets_dir is not None:
options["assets_dir"] = assets_dir
has_notes = any(
@ -451,6 +456,24 @@ class RevealJS(Converter):
**options,
)
if self.offline:
soup = BeautifulSoup(content, "html.parser")
session = requests.Session()
for tag, inner in [("link", "href"), ("script", "src")]:
for item in soup.find_all(tag):
if item.has_attr(inner) and (link := item[inner]).startswith(
"http"
):
asset_name = link.rsplit("/", 1)[1]
asset = session.get(link)
with open(full_assets_dir / asset_name, "wb") as asset_file:
asset_file.write(asset.content)
item[inner] = str(assets_dir / asset_name)
content = str(soup)
f.write(content)
@ -590,7 +613,7 @@ class PowerPoint(Converter):
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wrap a function to add a `--show-config` option."""
"""Wrap a function to add a '--show-config' option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
@ -621,7 +644,7 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wrap a function to add a `--show-template` option."""
"""Wrap a function to add a '--show-template' option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
@ -666,7 +689,6 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
is_flag=True,
help="Open the newly created file using the appropriate application.",
)
@click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.")
@click.option(
"-c",
"--config",
@ -674,7 +696,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
multiple=True,
callback=validate_config_option,
help="Configuration options passed to the converter. "
"E.g., pass ``-cslide_number=true`` to display slide numbers.",
"E.g., pass '-cslide_number=true' to display slide numbers.",
)
@click.option(
"--use-template",
@ -682,7 +704,13 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
metavar="FILE",
type=click.Path(exists=True, dir_okay=False, path_type=Path),
help="Use the template given by FILE instead of default one. "
"To echo the default template, use ``--show-template``.",
"To echo the default template, use '--show-template'.",
)
@click.option(
"--offline",
is_flag=True,
help="Download any remote content and store it in the assets folder. "
"The is a convenient alias to '-coffline=true'.",
)
@show_template_option
@show_config_options
@ -693,9 +721,9 @@ def convert(
dest: Path,
to: str,
open_result: bool,
force: bool,
config_options: dict[str, str],
template: Optional[Path],
offline: bool,
) -> None:
"""Convert SCENE(s) into a given format and writes the result in DEST."""
presentation_configs = get_scenes_presentation_config(scenes, folder)
@ -713,6 +741,13 @@ def convert(
else:
cls = Converter.from_string(to)
if (
offline
and issubclass(cls, (RevealJS, HtmlZip))
and "offline" not in config_options
):
config_options["offline"] = "true"
converter = cls(
presentation_configs=presentation_configs,
template=template,

View File

@ -19,7 +19,7 @@
<body>
<div class="reveal">
<div class="slides">
{%- for presentation_config in presentation_configs -%}
{% for presentation_config in presentation_configs -%}
{% set outer_loop = loop %}
{%- for slide_config in presentation_config.slides -%}
{%- if data_uri -%}
@ -33,14 +33,15 @@
data-background-video="{{ file }}"
{% if loop.index == 1 and outer_loop.index == 1 -%}
data-background-video-muted
{%- endif %}
{%- endif -%}
{% if slide_config.loop -%}
data-background-video-loop
{%- endif -%}
{% if slide_config.auto_next -%}
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
{%- endif -%}>
{% if slide_config.notes != "" -%}
{%- endif %}
>
{%- if slide_config.notes != "" -%}
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
{%- endif %}
</section>

View File

@ -18,6 +18,7 @@ classifiers = [
]
dependencies = [
"av>=9.0.0",
"beautifulsoup4>=4.12.3",
"click>=8.1.3",
"click-default-group>=1.2.2",
"jinja2>=3.1.2",

View File

@ -156,6 +156,24 @@ class TestConverter:
file_contents = out_file.read_text()
assert "manim" in file_contents.casefold()
def test_revealjs_offline_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:
out_file = tmp_path / "slides.html"
RevealJS(presentation_configs=[presentation_config], offline="true").convert_to(
out_file
)
assert out_file.exists()
assets_dir = Path(tmp_path / "slides_assets")
assert assets_dir.is_dir()
for file in [
"black.min.css",
"reveal.min.css",
"reveal.min.js",
"zenburn.min.css",
]:
assert (assets_dir / file).exists()
def test_htmlzip_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:

4
uv.lock generated
View File

@ -1237,10 +1237,11 @@ wheels = [
[[package]]
name = "manim-slides"
version = "5.1.7"
version = "5.1.9"
source = { editable = "." }
dependencies = [
{ name = "av" },
{ name = "beautifulsoup4" },
{ name = "click" },
{ name = "click-default-group" },
{ name = "jinja2" },
@ -1335,6 +1336,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "av", specifier = ">=9.0.0" },
{ name = "beautifulsoup4", specifier = ">=4.12.3" },
{ name = "click", specifier = ">=8.1.3" },
{ name = "click-default-group", specifier = ">=1.2.2" },
{ name = "docutils", marker = "extra == 'docs'", specifier = ">=0.20.1" },