diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3796c97f..463075ef 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 60bf4bdc..c4276972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/docs/source/reference/magic_example.ipynb b/docs/source/reference/magic_example.ipynb index 62c1e978..11eff423 100644 --- a/docs/source/reference/magic_example.ipynb +++ b/docs/source/reference/magic_example.ipynb @@ -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, diff --git a/manim_slides/convert.py b/manim_slides/convert.py index f36dbd86..b693bc3c 100644 --- a/manim_slides/convert.py +++ b/manim_slides/convert.py @@ -15,6 +15,8 @@ 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 @@ def load_template(self) -> str: 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 @@ def prefix(i: int) -> str: 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 @@ def prefix(i: int) -> str: **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 @@ def xpath(el: etree.Element, query: str) -> etree.XPath: 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 callback(ctx: Context, param: Parameter, value: bool) -> None: 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 callback(ctx: Context, param: Parameter, value: bool) -> None: 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 callback(ctx: Context, param: Parameter, value: bool) -> None: 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 callback(ctx: Context, param: Parameter, value: bool) -> None: 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, diff --git a/manim_slides/templates/revealjs.html b/manim_slides/templates/revealjs.html index d59b56db..33db2c35 100644 --- a/manim_slides/templates/revealjs.html +++ b/manim_slides/templates/revealjs.html @@ -19,7 +19,7 @@