Rewrite MathTex to make it more robust regarding splitting (#4515)

* Extracted the method get_mob_from_shape_element

* Moved more functionality to get_mob_from_shape_element

* More cleanup

* Parse the svg file while maintaining the group structure.

* Make the svg groups available

* Handle PERF401 issue

* [pre-commit.ci] pre-commit autoupdate (#4506)

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.7 → v0.14.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.7...v0.14.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Added an example of the issue

* Experimenting with coloring elements from the latex equation

* ...

* Regular expression can now match more than one object

* Process the string by applying the substrings in the order they match

* Code refactoring and added type annotations

* ...

* Added a lot of test cases

* More examples

* More examples

* Use matched_strings_and_ids to simplify existing methods

* Remove unused code

* Update get_part_by_tex to use matched_strings_and_ids

* This is required for test_MathTable to pass

* Ensure that self.texstring is set.

* Added more examples from exising issues in the github repo

* Ensure that latex groups are maintained by adding an additional pair of curly braces around the extracted part

* ExampleScene -> Scene

* Added comment

* _break_up_by_substrings

* Refactor code

* Added comment to example

* Handle integer inputs well.

* Expose the original tex_string

* Do not treat the content of substrings_to_isolate as regular expressions.

* Updated examples

* Update examples

* Fix SVMobject caching issue.

* Remove traces from brace_notation_split_occurred

* Simplify MathTex::_break_up_by_substrings

* Fix small issue in tex that in some cases moved elements a tiny bit around

* No use of regular expressions for locate substrings.

* Updated notes to the set of test cases

* Handle issues with the center environment.

* Add example

* Fix issue with rectangles (e.g. from sqrt)

* ConvertToOpenGL

* Reduce the number of nesting levels.

* Use the specified arg_seperator

* Deal with the double curly brace markup

* Code cleanup

* Code cleanup

* Rollback a few changes

* Code cleanup

* Adjust paths the generated artefacts in tests that rely on MathTex

* Added a remark to the using text guide on enclosing snippets in curly braces for substrings_to_isolate to work

* Added space around the numerator argument to frac to avoid having double curly braces in the example.

This would otherwise trigger MathTex to split the string at that location.

* Log errors properly and display some information about the errors and their context.

* Code refactoring as suggested by Benjamin

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
This commit is contained in:
Henrik Skov Midtiby 2026-02-16 14:22:56 +01:00 committed by GitHub
commit 357bb3fbba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 248 additions and 138 deletions

View file

@ -424,7 +424,7 @@ may be expected. To color only ``x`` yellow, we have to do the following:
class CorrectLaTeXSubstringColoring(Scene):
def construct(self):
equation = MathTex(
r"e^x = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
r"e^{x} = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
substrings_to_isolate="x"
)
equation.set_color_by_tex("x", YELLOW)
@ -434,6 +434,8 @@ By setting ``substrings_to_isolate`` to ``x``, we split up the
:class:`~.MathTex` into substrings automatically and isolate the ``x`` components
into individual substrings. Only then can :meth:`~.set_color_by_tex` be used
to achieve the desired result.
If one of the ``substrings_to_isolate`` is in a sub or superscript, it needs
to be enclosed by curly brackets.
Note that Manim also supports a custom syntax that allows splitting
a TeX string into substrings easily: simply enclose parts of your formula

View file

@ -21,12 +21,12 @@ from ..geometry.arc import Circle
from ..geometry.line import Line
from ..geometry.polygram import Polygon, Rectangle, RoundedRectangle
from ..opengl.opengl_compatibility import ConvertToOpenGL
from ..types.vectorized_mobject import VMobject
from ..types.vectorized_mobject import VGroup, VMobject
__all__ = ["SVGMobject", "VMobjectFromSVGPath"]
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
SVG_HASH_TO_MOB_MAP: dict[int, SVGMobject] = {}
def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
@ -127,6 +127,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
self.stroke_color = stroke_color
self.stroke_opacity = stroke_opacity # type: ignore[assignment]
self.stroke_width = stroke_width # type: ignore[assignment]
self.id_to_vgroup_dict: dict[str, VGroup] = {}
if self.stroke_width is None:
self.stroke_width = 0
@ -170,6 +171,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
if hash_val in SVG_HASH_TO_MOB_MAP:
mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
self.add(*mob)
self.id_to_vgroup_dict = mob.id_to_vgroup_dict
return
self.generate_mobject()
@ -203,8 +205,9 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
svg = se.SVG.parse(modified_file_path)
modified_file_path.unlink()
mobjects = self.get_mobjects_from(svg)
mobjects, mobject_dict = self.get_mobjects_from(svg)
self.add(*mobjects)
self.id_to_vgroup_dict = mobject_dict
self.flip(RIGHT) # Flip y
def get_file_path(self) -> Path:
@ -258,7 +261,9 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
result[svg_key] = str(svg_default_dict[style_key])
return result
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
def get_mobjects_from(
self, svg: se.SVG
) -> tuple[list[VMobject], dict[str, VGroup]]:
"""Convert the elements of the SVG to a list of mobjects.
Parameters
@ -267,36 +272,77 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
The parsed SVG file.
"""
result: list[VMobject] = []
for shape in svg.elements():
# can we combine the two continue cases into one?
if isinstance(shape, se.Group): # noqa: SIM114
continue
elif isinstance(shape, se.Path):
mob: VMobject = self.path_to_mobject(shape)
elif isinstance(shape, se.SimpleLine):
mob = self.line_to_mobject(shape)
elif isinstance(shape, se.Rect):
mob = self.rect_to_mobject(shape)
elif isinstance(shape, (se.Circle, se.Ellipse)):
mob = self.ellipse_to_mobject(shape)
elif isinstance(shape, se.Polygon):
mob = self.polygon_to_mobject(shape)
elif isinstance(shape, se.Polyline):
mob = self.polyline_to_mobject(shape)
elif isinstance(shape, se.Text):
mob = self.text_to_mobject(shape)
elif isinstance(shape, se.Use) or type(shape) is se.SVGElement:
continue
else:
logger.warning(f"Unsupported element type: {type(shape)}")
continue
if mob is None or not mob.has_points():
continue
self.apply_style_to_mobject(mob, shape)
if isinstance(shape, se.Transformable) and shape.apply:
self.handle_transform(mob, shape.transform)
result.append(mob)
return result
stack: list[tuple[se.SVGElement, int]] = []
stack.append((svg, 1))
group_id_number = 0
vgroup_stack: list[str] = ["root"]
vgroup_names: list[str] = ["root"]
vgroups: dict[str, VGroup] = {"root": VGroup()}
while len(stack) > 0:
element, depth = stack.pop()
# Reduce stack heights
vgroup_stack = vgroup_stack[0:(depth)]
try:
group_name = str(element.values["id"])
except Exception:
group_name = f"numbered_group_{group_id_number}"
group_id_number += 1
vg = VGroup()
vgroup_names.append(group_name)
vgroup_stack.append(group_name)
vgroups[group_name] = vg
if isinstance(element, (se.Group, se.Use)):
stack.extend((subelement, depth + 1) for subelement in element[::-1])
# Add element to the parent vgroup
try:
if isinstance(
element,
(
se.Path,
se.SimpleLine,
se.Rect,
se.Circle,
se.Ellipse,
se.Polygon,
se.Polyline,
se.Text,
),
):
mob = self.get_mob_from_shape_element(element)
if mob is not None:
result.append(mob)
for parent_name in vgroup_stack[:-1]:
vgroups[parent_name].add(mob)
except Exception as e:
logger.error(f"Exception occurred in 'get_mobjects_from'. Details: {e}")
return result, vgroups
def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None:
if isinstance(shape, se.Path):
mob: VMobject | None = self.path_to_mobject(shape)
elif isinstance(shape, se.SimpleLine):
mob = self.line_to_mobject(shape)
elif isinstance(shape, se.Rect):
mob = self.rect_to_mobject(shape)
elif isinstance(shape, (se.Circle, se.Ellipse)):
mob = self.ellipse_to_mobject(shape)
elif isinstance(shape, se.Polygon):
mob = self.polygon_to_mobject(shape)
elif isinstance(shape, se.Polyline):
mob = self.polyline_to_mobject(shape)
elif isinstance(shape, se.Text):
mob = self.text_to_mobject(shape)
else:
logger.warning(f"Unsupported element type: {type(shape)}")
mob = None
if mob is None or not mob.has_points():
return mob
self.apply_style_to_mobject(mob, shape)
if isinstance(shape, se.Transformable) and shape.apply:
self.handle_transform(mob, shape.transform)
return mob
@staticmethod
def handle_transform(mob: VMobject, matrix: se.Matrix) -> VMobject:

View file

@ -1078,11 +1078,11 @@ class IntegerTable(Table):
[[0,30,45,60,90],
[90,60,45,30,0]],
col_labels=[
MathTex(r"\frac{\sqrt{0}}{2}"),
MathTex(r"\frac{\sqrt{1}}{2}"),
MathTex(r"\frac{\sqrt{2}}{2}"),
MathTex(r"\frac{\sqrt{3}}{2}"),
MathTex(r"\frac{\sqrt{4}}{2}")],
MathTex(r"\frac{ \sqrt{0} }{2}"),
MathTex(r"\frac{ \sqrt{1} }{2}"),
MathTex(r"\frac{ \sqrt{2} }{2}"),
MathTex(r"\frac{ \sqrt{3} }{2}"),
MathTex(r"\frac{ \sqrt{4} }{2}")],
row_labels=[MathTex(r"\sin"), MathTex(r"\cos")],
h_buff=1,
element_to_mobject_config={"unit": r"^{\circ}"})

View file

@ -12,7 +12,7 @@ r"""Mobjects representing text rendered using LaTeX.
from __future__ import annotations
from manim.utils.color import BLACK, ManimColor, ParsableManimColor
from manim.utils.color import BLACK, ParsableManimColor
__all__ = [
"SingleStringMathTex",
@ -23,10 +23,9 @@ __all__ = [
]
import itertools as it
import operator as op
import re
from collections.abc import Iterable, Sequence
from collections.abc import Iterable
from functools import reduce
from textwrap import dedent
from typing import Any, Self
@ -39,6 +38,10 @@ from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.utils.tex import TexTemplate
from manim.utils.tex_file_writing import tex_to_svg_file
from ..opengl.opengl_compatibility import ConvertToOpenGL
MATHTEX_SUBSTRING = "substring"
class SingleStringMathTex(SVGMobject):
"""Elementary building block for rendering text with LaTeX.
@ -264,22 +267,30 @@ class MathTex(SingleStringMathTex):
self.tex_template = kwargs.pop("tex_template", config["tex_template"])
self.arg_separator = arg_separator
self.substrings_to_isolate = (
[] if substrings_to_isolate is None else substrings_to_isolate
[] if substrings_to_isolate is None else list(substrings_to_isolate)
)
if tex_to_color_map is None:
self.tex_to_color_map: dict[str, ParsableManimColor] = {}
else:
self.tex_to_color_map = tex_to_color_map
self.substrings_to_isolate.extend(self.tex_to_color_map.keys())
self.tex_environment = tex_environment
self.brace_notation_split_occurred = False
self.tex_strings = self._break_up_tex_strings(tex_strings)
self.tex_strings = self._prepare_tex_strings(tex_strings)
self.matched_strings_and_ids: list[tuple[str, str]] = []
try:
joined_string = self._join_tex_strings_with_unique_deliminters(
self.tex_strings, self.substrings_to_isolate
)
super().__init__(
self.arg_separator.join(self.tex_strings),
joined_string,
tex_environment=self.tex_environment,
tex_template=self.tex_template,
**kwargs,
)
# Save the original tex_string
self.tex_string = self.arg_separator.join(self.tex_strings)
self._break_up_by_substrings()
except ValueError as compilation_error:
if self.brace_notation_split_occurred:
@ -301,36 +312,109 @@ class MathTex(SingleStringMathTex):
if self.organize_left_to_right:
self._organize_submobjects_left_to_right()
def _break_up_tex_strings(self, tex_strings: Sequence[str]) -> list[str]:
# Separate out anything surrounded in double braces
pre_split_length = len(tex_strings)
tex_strings_brace_splitted = [
re.split("{{(.*?)}}", str(t)) for t in tex_strings
def _prepare_tex_strings(self, tex_strings: Iterable[str]) -> list[str]:
# Deal with the case where tex_strings contains integers instead
# of strings.
tex_strings_validated = [
string if isinstance(string, str) else str(string) for string in tex_strings
]
tex_strings_combined = sum(tex_strings_brace_splitted, [])
if len(tex_strings_combined) > pre_split_length:
# Locate double curly bracers
tex_strings_validated_two = []
for tex_string in tex_strings_validated:
split = re.split(r"{{|}}", tex_string)
tex_strings_validated_two.extend(split)
if len(tex_strings_validated_two) > len(tex_strings_validated):
self.brace_notation_split_occurred = True
return [string for string in tex_strings_validated_two if len(string) > 0]
# Separate out any strings specified in the isolate
# or tex_to_color_map lists.
patterns = []
patterns.extend(
[
f"({re.escape(ss)})"
for ss in it.chain(
self.substrings_to_isolate,
self.tex_to_color_map.keys(),
def _join_tex_strings_with_unique_deliminters(
self, tex_strings: list[str], substrings_to_isolate: Iterable[str]
) -> str:
joined_string = ""
ssIdx = 0
for idx, tex_string in enumerate(tex_strings):
string_part = rf"\special{{dvisvgm:raw <g id='unique{idx:03d}'>}}"
self.matched_strings_and_ids.append((tex_string, f"unique{idx:03d}"))
# Try to match with all substrings_to_isolate and apply the first match
# then match again (on the rest of the string) and continue until no
# characters are left in the string
unprocessed_string = str(tex_string)
processed_string = ""
while len(unprocessed_string) > 0:
first_match = self._locate_first_match(
substrings_to_isolate, unprocessed_string
)
],
if first_match:
processed, unprocessed_string = self._handle_match(
ssIdx, first_match
)
processed_string = processed_string + processed
ssIdx += 1
else:
processed_string = processed_string + unprocessed_string
unprocessed_string = ""
string_part += processed_string
if idx < len(tex_strings) - 1:
string_part += self.arg_separator
string_part += r"\special{dvisvgm:raw </g>}"
joined_string = joined_string + string_part
return joined_string
def _locate_first_match(
self, substrings_to_isolate: Iterable[str], unprocessed_string: str
) -> re.Match | None:
first_match_start = len(unprocessed_string)
first_match_length = 0
first_match = None
for substring in substrings_to_isolate:
match = re.match(f"(.*?)({re.escape(substring)})(.*)", unprocessed_string)
if match and len(match.group(1)) < first_match_start:
first_match = match
first_match_start = len(match.group(1))
first_match_length = len(match.group(2))
elif match and len(match.group(1)) == first_match_start:
# Break ties by looking at length of matches.
if first_match_length < len(match.group(2)):
first_match = match
first_match_start = len(match.group(1))
first_match_length = len(match.group(2))
return first_match
def _handle_match(self, ssIdx: int, first_match: re.Match) -> tuple[str, str]:
pre_match = first_match.group(1)
matched_string = first_match.group(2)
post_match = first_match.group(3)
pre_string = (
rf"\special{{dvisvgm:raw <g id='unique{ssIdx:03d}{MATHTEX_SUBSTRING}'>}}"
)
pattern = "|".join(patterns)
if pattern:
pieces = []
for s in tex_strings_combined:
pieces.extend(re.split(pattern, s))
else:
pieces = tex_strings_combined
return [p for p in pieces if p]
post_string = r"\special{dvisvgm:raw </g>}"
self.matched_strings_and_ids.append(
(matched_string, f"unique{ssIdx:03d}{MATHTEX_SUBSTRING}")
)
processed_string = pre_match + pre_string + matched_string + post_string
unprocessed_string = post_match
return processed_string, unprocessed_string
@property
def _substring_matches(self) -> list[tuple[str, str]]:
"""Return only the 'ss' (substring_to_isolate) matches."""
return [
(tex, id_)
for tex, id_ in self.matched_strings_and_ids
if id_.endswith(MATHTEX_SUBSTRING)
]
@property
def _main_matches(self) -> list[tuple[str, str]]:
"""Return only the main tex_string matches."""
return [
(tex, id_)
for tex, id_ in self.matched_strings_and_ids
if not id_.endswith(MATHTEX_SUBSTRING)
]
def _break_up_by_substrings(self) -> Self:
"""
@ -339,51 +423,32 @@ class MathTex(SingleStringMathTex):
of tex_strings)
"""
new_submobjects: list[VMobject] = []
curr_index = 0
for tex_string in self.tex_strings:
sub_tex_mob = SingleStringMathTex(
tex_string,
tex_environment=self.tex_environment,
tex_template=self.tex_template,
try:
for tex_string, tex_string_id in self._main_matches:
mtp = MathTexPart()
mtp.tex_string = tex_string
mtp.add(*self.id_to_vgroup_dict[tex_string_id].submobjects)
new_submobjects.append(mtp)
except KeyError:
logger.error(
f"MathTex: Could not find SVG group for tex part '{tex_string}' (id: {tex_string_id}). Using fallback to root group."
)
num_submobs = len(sub_tex_mob.submobjects)
new_index = (
curr_index + num_submobs + len("".join(self.arg_separator.split()))
)
if num_submobs == 0:
last_submob_index = min(curr_index, len(self.submobjects) - 1)
sub_tex_mob.move_to(self.submobjects[last_submob_index], RIGHT)
else:
sub_tex_mob.submobjects = self.submobjects[curr_index:new_index]
new_submobjects.append(sub_tex_mob)
curr_index = new_index
new_submobjects.append(self.id_to_vgroup_dict["root"])
self.submobjects = new_submobjects
return self
def get_parts_by_tex(
self, tex: str, substring: bool = True, case_sensitive: bool = True
) -> VGroup:
def test(tex1: str, tex2: str) -> bool:
if not case_sensitive:
tex1 = tex1.lower()
tex2 = tex2.lower()
if substring:
return tex1 in tex2
else:
return tex1 == tex2
return VGroup(*(m for m in self.submobjects if test(tex, m.get_tex_string())))
def get_part_by_tex(self, tex: str, **kwargs: Any) -> MathTex | None:
all_parts = self.get_parts_by_tex(tex, **kwargs)
return all_parts[0] if all_parts else None
def get_part_by_tex(self, tex: str, **kwargs: Any) -> VGroup | None:
for tex_str, match_id in self.matched_strings_and_ids:
if tex_str == tex:
return self.id_to_vgroup_dict[match_id]
return None
def set_color_by_tex(
self, tex: str, color: ParsableManimColor, **kwargs: Any
) -> Self:
parts_to_color = self.get_parts_by_tex(tex, **kwargs)
for part in parts_to_color:
part.set_color(color)
for tex_str, match_id in self.matched_strings_and_ids:
if tex_str == tex:
self.id_to_vgroup_dict[match_id].set_color(color)
return self
def set_opacity_by_tex(
@ -409,22 +474,18 @@ class MathTex(SingleStringMathTex):
"""
if remaining_opacity is not None:
self.set_opacity(opacity=remaining_opacity)
for part in self.get_parts_by_tex(tex):
part.set_opacity(opacity)
for tex_str, match_id in self.matched_strings_and_ids:
if tex_str == tex:
self.id_to_vgroup_dict[match_id].set_opacity(opacity)
return self
def set_color_by_tex_to_color_map(
self, texs_to_color_map: dict[str, ParsableManimColor], **kwargs: Any
) -> Self:
for texs, color in list(texs_to_color_map.items()):
try:
# If the given key behaves like tex_strings
texs + ""
self.set_color_by_tex(texs, ManimColor(color), **kwargs)
except TypeError:
# If the given key is a tuple
for tex in texs:
self.set_color_by_tex(tex, ManimColor(color), **kwargs)
for match in self.matched_strings_and_ids:
if match[0] == texs:
self.id_to_vgroup_dict[match[1]].set_color(color)
return self
def index_of_part(self, part: MathTex) -> int:
@ -433,16 +494,17 @@ class MathTex(SingleStringMathTex):
raise ValueError("Trying to get index of part not in MathTex")
return split_self.index(part)
def index_of_part_by_tex(self, tex: str, **kwargs: Any) -> int:
part = self.get_part_by_tex(tex, **kwargs)
if part is None:
return -1
return self.index_of_part(part)
def sort_alphabetically(self) -> None:
self.submobjects.sort(key=lambda m: m.get_tex_string())
class MathTexPart(VMobject, metaclass=ConvertToOpenGL):
tex_string: str
def __repr__(self) -> str:
return f"{type(self).__name__}({repr(self.tex_string)})"
class Tex(MathTex):
r"""A string compiled with LaTeX in normal mode.

View file

@ -1,8 +1,8 @@
{"levelname": "INFO", "module": "logger_utils", "message": "Log file will be saved in <>"}
{"levelname": "INFO", "module": "tex_file_writing", "message": "Writing <> to <>"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: LaTeX Error: File `notapackage.sty' not found.\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\special{dvisvgm:raw <g id='unique000'>}\\frac{1}{0}\\special{dvisvgm:raw </g>}\n"}
{"levelname": "INFO", "module": "tex_file_writing", "message": "You do not have package notapackage.sty installed."}
{"levelname": "INFO", "module": "tex_file_writing", "message": "Install notapackage.sty it using your LaTeX package manager, or check for typos."}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: Emergency stop.\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\special{dvisvgm:raw <g id='unique000'>}\\frac{1}{0}\\special{dvisvgm:raw </g>}\n"}

View file

@ -10,7 +10,7 @@ from manim import MathTex, SingleStringMathTex, Tex, TexTemplate, tempconfig
def test_MathTex(config):
MathTex("a^2 + b^2 = c^2")
assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists()
assert Path(config.media_dir, "Tex", "05bb0a41ed575f00.svg").exists()
def test_SingleStringMathTex(config):
@ -29,7 +29,7 @@ def test_double_braces_testing(text_input, length_sub):
def test_tex(config):
Tex("The horse does not eat cucumber salad.")
assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists()
assert Path(config.media_dir, "Tex", "5384b41741a246bd.svg").exists()
def test_tex_temp_directory(tmpdir, monkeypatch):
@ -42,12 +42,12 @@ def test_tex_temp_directory(tmpdir, monkeypatch):
with tempconfig({"media_dir": "media"}):
Tex("The horse does not eat cucumber salad.")
assert Path("media", "Tex").exists()
assert Path("media", "Tex", "c3945e23e546c95a.svg").exists()
assert Path("media", "Tex", "5384b41741a246bd.svg").exists()
def test_percent_char_rendering(config):
Tex(r"\%")
assert Path(config.media_dir, "Tex", "4a583af4d19a3adf.tex").exists()
assert Path(config.media_dir, "Tex", "32509dd0ea993961.tex").exists()
def test_tex_whitespace_arg():
@ -218,11 +218,11 @@ def test_tex_garbage_collection(tmpdir, monkeypatch, config):
Path(tmpdir, "media").mkdir(exist_ok=True)
config.media_dir = "media"
tex_without_log = Tex("Hello World!") # d771330b76d29ffb.tex
assert Path("media", "Tex", "d771330b76d29ffb.tex").exists()
assert not Path("media", "Tex", "d771330b76d29ffb.log").exists()
tex_without_log = Tex("Hello World!") # 058a4e242c57db6d.tex
assert Path("media", "Tex", "058a4e242c57db6d.tex").exists()
assert not Path("media", "Tex", "058a4e242c57db6d.log").exists()
config.no_latex_cleanup = True
tex_with_log = Tex("Hello World, again!") # da27670a37b08799.tex
assert Path("media", "Tex", "da27670a37b08799.log").exists()
tex_with_log = Tex("Hello World, again!") # 45b4e7819cc20cb1.tex
assert Path("media", "Tex", "45b4e7819cc20cb1.log").exists()

View file

@ -9,7 +9,7 @@ from manim import MathTex, SingleStringMathTex, Tex
def test_MathTex(config, using_opengl_renderer):
MathTex("a^2 + b^2 = c^2")
assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists()
assert Path(config.media_dir, "Tex", "05bb0a41ed575f00.svg").exists()
def test_SingleStringMathTex(config, using_opengl_renderer):
@ -28,7 +28,7 @@ def test_double_braces_testing(using_opengl_renderer, text_input, length_sub):
def test_tex(config, using_opengl_renderer):
Tex("The horse does not eat cucumber salad.")
assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists()
assert Path(config.media_dir, "Tex", "5384b41741a246bd.svg").exists()
def test_tex_whitespace_arg(using_opengl_renderer):