Source code for manim.utils.tex

"""Utilities for processing LaTeX templates."""

from __future__ import annotations

__all__ = [
    "TexTemplate",
]

import copy
import re
import warnings
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from typing import Self

    from manim.typing import StrPath

_DEFAULT_PREAMBLE = r"""\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}"""

_BEGIN_DOCUMENT = r"\begin{document}"
_END_DOCUMENT = r"\end{document}"


[docs] @dataclass(eq=True) class TexTemplate: """TeX templates are used to create ``Tex`` and ``MathTex`` objects.""" _body: str = field(default="", init=False) """A custom body, can be set from a file.""" tex_compiler: str = "latex" """The TeX compiler to be used, e.g. ``latex``, ``pdflatex`` or ``lualatex``.""" description: str = "" """A description of the template""" output_format: str = ".dvi" """The output format resulting from compilation, e.g. ``.dvi`` or ``.pdf``.""" documentclass: str = r"\documentclass[preview]{standalone}" r"""The command defining the documentclass, e.g. ``\documentclass[preview]{standalone}``.""" preamble: str = _DEFAULT_PREAMBLE r"""The document's preamble, i.e. the part between ``\documentclass`` and ``\begin{document}``.""" placeholder_text: str = "YourTextHere" """Text in the document that will be replaced by the expression to be rendered.""" post_doc_commands: str = "" r"""Text (definitions, commands) to be inserted at right after ``\begin{document}``, e.g. ``\boldmath``.""" @property def body(self) -> str: """The entire TeX template.""" return self._body or "\n".join( filter( None, [ self.documentclass, self.preamble, _BEGIN_DOCUMENT, self.post_doc_commands, self.placeholder_text, _END_DOCUMENT, ], ) ) @body.setter def body(self, value: str) -> None: self._body = value
[docs] @classmethod def from_file(cls, file: StrPath = "tex_template.tex", **kwargs: Any) -> Self: """Create an instance by reading the content of a file. Using the ``add_to_preamble`` and ``add_to_document`` methods on this instance will have no effect, as the body is read from the file. """ instance = cls(**kwargs) instance.body = Path(file).read_text(encoding="utf-8") return instance
[docs] def add_to_preamble(self, txt: str, prepend: bool = False) -> Self: r"""Adds text to the TeX template's preamble (e.g. definitions, packages). Text can be inserted at the beginning or at the end of the preamble. Parameters ---------- txt String containing the text to be added, e.g. ``\usepackage{hyperref}``. prepend Whether the text should be added at the beginning of the preamble, i.e. right after ``\documentclass``. Default is to add it at the end of the preamble, i.e. right before ``\begin{document}``. """ if self._body: warnings.warn( "This TeX template was created with a fixed body, trying to add text the preamble will have no effect.", UserWarning, stacklevel=2, ) if prepend: self.preamble = txt + "\n" + self.preamble else: self.preamble += "\n" + txt return self
[docs] def add_to_document(self, txt: str) -> Self: r"""Adds text to the TeX template just after \begin{document}, e.g. ``\boldmath``. Parameters ---------- txt String containing the text to be added. """ if self._body: warnings.warn( "This TeX template was created with a fixed body, trying to add text the document will have no effect.", UserWarning, stacklevel=2, ) self.post_doc_commands += txt return self
[docs] def get_texcode_for_expression(self, expression: str) -> str: r"""Inserts expression verbatim into TeX template. Parameters ---------- expression The string containing the expression to be typeset, e.g. ``$\sqrt{2}$`` Returns ------- :class:`str` LaTeX code based on current template, containing the given ``expression`` and ready for typesetting """ return self.body.replace(self.placeholder_text, expression)
[docs] def get_texcode_for_expression_in_env( self, expression: str, environment: str ) -> str: r"""Inserts expression into TeX template wrapped in ``\begin{environment}`` and ``\end{environment}``. Parameters ---------- expression The string containing the expression to be typeset, e.g. ``$\sqrt{2}$``. environment The string containing the environment in which the expression should be typeset, e.g. ``align*``. Returns ------- :class:`str` LaTeX code based on template, containing the given expression inside its environment, ready for typesetting """ begin, end = _texcode_for_environment(environment) return self.body.replace( self.placeholder_text, "\n".join([begin, expression, end]) )
[docs] def copy(self) -> Self: """Create a deep copy of the TeX template instance.""" return copy.deepcopy(self)
def _texcode_for_environment(environment: str) -> tuple[str, str]: r"""Processes the tex_environment string to return the correct ``\begin{environment}[extra]{extra}`` and ``\end{environment}`` strings. Parameters ---------- environment The tex_environment as a string. Acceptable formats include: ``{align*}``, ``align*``, ``{tabular}[t]{cccl}``, ``tabular}{cccl``, ``\begin{tabular}[t]{cccl}``. Returns ------- Tuple[:class:`str`, :class:`str`] A pair of strings representing the opening and closing of the tex environment, e.g. ``\begin{tabular}{cccl}`` and ``\end{tabular}`` """ environment = environment.removeprefix(r"\begin").removeprefix("{") # The \begin command takes everything and closes with a brace begin = r"\begin{" + environment # If it doesn't end on } or ], assume missing } if not begin.endswith(("}", "]")): begin += "}" # While the \end command terminates at the first closing brace split_at_brace = re.split("}", environment, maxsplit=1) end = r"\end{" + split_at_brace[0] + "}" return begin, end