mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-22 04:56:24 +08:00
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:
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,27 +390,25 @@ 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
|
||||
dirname = dest.parent
|
||||
basename = dest.stem
|
||||
ext = dest.suffix
|
||||
|
||||
assets_dir = Path(
|
||||
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
||||
)
|
||||
full_assets_dir = dirname / assets_dir
|
||||
assets_dir = Path(
|
||||
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
||||
)
|
||||
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,7 +438,9 @@ class RevealJS(Converter):
|
||||
revealjs_template = Template(self.load_template())
|
||||
|
||||
options = self.model_dump()
|
||||
options["assets_dir"] = assets_dir
|
||||
|
||||
if assets_dir is not None:
|
||||
options["assets_dir"] = assets_dir
|
||||
|
||||
has_notes = any(
|
||||
slide_config.notes != ""
|
||||
@ -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,
|
||||
|
@ -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 -%}
|
||||
@ -27,23 +27,24 @@
|
||||
{%- else -%}
|
||||
{% set file = assets_dir / slide_config.file.name %}
|
||||
{%- endif -%}
|
||||
<section
|
||||
data-background-size={{ background_size }}
|
||||
data-background-color="{{ presentation_config.background_color }}"
|
||||
data-background-video="{{ file }}"
|
||||
{% if loop.index == 1 and outer_loop.index == 1 -%}
|
||||
data-background-video-muted
|
||||
{%- 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 != "" -%}
|
||||
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
||||
{%- endif %}
|
||||
</section>
|
||||
<section
|
||||
data-background-size={{ background_size }}
|
||||
data-background-color="{{ presentation_config.background_color }}"
|
||||
data-background-video="{{ file }}"
|
||||
{% if loop.index == 1 and outer_loop.index == 1 -%}
|
||||
data-background-video-muted
|
||||
{%- 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 != "" -%}
|
||||
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
||||
{%- endif %}
|
||||
</section>
|
||||
{%- endfor -%}
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
|
@ -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",
|
||||
|
@ -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
4
uv.lock
generated
@ -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" },
|
||||
|
Reference in New Issue
Block a user