import os import webbrowser from enum import Enum from typing import Any, Callable, Dict, Generator, List, Type import click from click import Context, Parameter from pydantic import BaseModel from .commons import folder_path_option, verbosity_option from .config import PresentationConfig from .defaults import REVEALJS_TEMPLATE from .present import get_scenes_presentation_config def validate_config_option( ctx: Context, param: Parameter, value: Any ) -> Dict[str, str]: config = {} for c_option in value: try: key, value = c_option.split("=") config[key] = value except ValueError: raise click.BadParameter( f"Configuration options `{c_option}` could not be parsed into a proper (key, value) pair. Please use an `=` sign to separate key from value." ) return config class Converter(BaseModel): presentation_configs: List[PresentationConfig] = [] assets_dir: str = "{basename}_assets" def convert_to(self, dest: str): """Converts self, i.e., a list of presentations, into a given format.""" raise NotImplementedError def open(self, file: str) -> bool: """Opens a file, generated with converter, using appropriate application.""" return webbrowser.open(file) @classmethod def from_string(cls, s: str) -> Type["Converter"]: """Returns the appropriate converter from a string name.""" return { "html": RevealJS, }[s] class JSBool(str, Enum): true = "true" false = "false" class RevealTheme(str, Enum): black = "black" white = "white" league = "league" beige = "beige" sky = "sky" night = "night" serif = "serif" simple = "simple" soralized = "solarized" blood = "blood" moon = "moon" class RevealJS(Converter): background_color: str = "black" controls: JSBool = JSBool.false embedded: JSBool = JSBool.false fragments: JSBool = JSBool.false height: str = "100%" loop: JSBool = JSBool.false progress: JSBool = JSBool.false reveal_version: str = "3.7.0" reveal_theme: RevealTheme = RevealTheme.black shuffle: JSBool = JSBool.false title: str = "Manim Slides" width: str = "100%" class Config: use_enum_values = True def get_sections_iter(self) -> Generator[str, None, None]: """Generates a sequence of sections, one per slide, that will be included into the html template.""" for presentation_config in self.presentation_configs: for slide_config in presentation_config.slides: file = presentation_config.files[slide_config.start_animation] file = os.path.join(self.assets_dir, os.path.basename(file)) if slide_config.is_loop(): yield f'
' else: yield f'
' def convert_to(self, dest: str): dirname = os.path.dirname(dest) basename, ext = os.path.splitext(os.path.basename(dest)) self.assets_dir = self.assets_dir.format( dirname=dirname, basename=basename, ext=ext ) full_assets_dir = os.path.join(dirname, self.assets_dir) os.makedirs(full_assets_dir, exist_ok=True) for presentation_config in self.presentation_configs: presentation_config.concat_animations().move_to(full_assets_dir) with open(dest, "w") as f: sections = "".join(self.get_sections_iter()) content = REVEALJS_TEMPLATE.format(sections=sections, **self.dict()) f.write(content) def show_config_options(function: Callable) -> Callable: """Wraps a function to add a `--show-config` option.""" def callback(ctx: Context, param: Parameter, value: bool) -> None: if not value or ctx.resilient_parsing: return to = ctx.params.get("to") if to: converter = Converter.from_string(to)(scenes=[]) for key, value in converter.dict().items(): click.echo(f"{key}: {repr(value)}") ctx.exit() else: raise click.UsageError( "Using --show-config option requires to first specify --to option." ) return click.option( "--show-config", is_flag=True, help="Show supported options for given format and exit.", default=None, expose_value=False, show_envvar=True, callback=callback, )(function) @click.command() @click.argument("scenes", nargs=-1) @folder_path_option @click.argument("dest") @click.option( "--to", type=click.Choice(["html"], case_sensitive=False), default="html", show_default=True, help="Set the conversion format to use.", ) @click.option( "--open", "open_result", is_flag=True, help="Open the newly created file using the approriate application.", ) @click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.") @click.option( "-c", "--config", "config_options", multiple=True, callback=validate_config_option, help="Configuration options passed to the converter. E.g., pass `-cbackground_color=red` to set the background color to red (if supported).", ) @show_config_options @verbosity_option def convert(scenes, folder, dest, to, open_result, force, config_options): """ Convert SCENE(s) into a given format and writes the result in DEST. """ presentation_configs = get_scenes_presentation_config(scenes, folder) converter = Converter.from_string(to)( presentation_configs=presentation_configs, **config_options ) converter.convert_to(dest) if open_result: converter.open(dest)