Merge pull request #1818 from YishiMichael/refactor

Add `template` and `additional_preamble` parameters to `Tex`
This commit is contained in:
Grant Sanderson 2022-09-13 12:42:15 -07:00 committed by GitHub
commit d48957c312
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1483 additions and 718 deletions

View file

@ -12,6 +12,7 @@ from manimlib.animation.fading import FadeOut
from manimlib.animation.fading import FadeIn from manimlib.animation.fading import FadeIn
from manimlib.animation.movement import Homotopy from manimlib.animation.movement import Homotopy
from manimlib.animation.transform import Transform from manimlib.animation.transform import Transform
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
from manimlib.constants import ORIGIN, RIGHT, UP from manimlib.constants import ORIGIN, RIGHT, UP
from manimlib.constants import SMALL_BUFF from manimlib.constants import SMALL_BUFF
from manimlib.constants import TAU from manimlib.constants import TAU

View file

@ -167,83 +167,95 @@ class TransformMatchingStrings(AnimationGroup):
digest_config(self, kwargs) digest_config(self, kwargs)
assert isinstance(source, StringMobject) assert isinstance(source, StringMobject)
assert isinstance(target, StringMobject) assert isinstance(target, StringMobject)
anims = []
source_indices = list(range(len(source.labels)))
target_indices = list(range(len(target.labels)))
def get_filtered_indices_lists(indices_lists, rest_indices): def get_matched_indices_lists(*part_items_list):
part_items_list_len = len(part_items_list)
indexed_part_items = sorted(it.chain(*[
[
(substr, items_index, indices_list)
for substr, indices_list in part_items
]
for items_index, part_items in enumerate(part_items_list)
]))
grouped_part_items = [
(substr, [
[indices_lists for _, _, indices_lists in grouper_2]
for _, grouper_2 in it.groupby(
grouper_1, key=lambda t: t[1]
)
])
for substr, grouper_1 in it.groupby(
indexed_part_items, key=lambda t: t[0]
)
]
return [
tuple(indices_lists_list)
for _, indices_lists_list in sorted(filter(
lambda t: t[0] and len(t[1]) == part_items_list_len,
grouped_part_items
), key=lambda t: len(t[0]), reverse=True)
]
def get_filtered_indices_lists(indices_lists, used_indices):
result = [] result = []
used = []
for indices_list in indices_lists: for indices_list in indices_lists:
if not indices_list: if not all(
continue index not in used_indices and index not in used
if not all(index in rest_indices for index in indices_list): for index in indices_list
):
continue continue
result.append(indices_list) result.append(indices_list)
for index in indices_list: used.extend(indices_list)
rest_indices.remove(index) return result, used
return result
def add_anims(anim_class, indices_lists_pairs): anim_class_items = [
for source_indices_lists, target_indices_lists in indices_lists_pairs: (ReplacementTransform, [
source_indices_lists = get_filtered_indices_lists(
source_indices_lists, source_indices
)
target_indices_lists = get_filtered_indices_lists(
target_indices_lists, target_indices
)
if not source_indices_lists or not target_indices_lists:
source_indices.extend(it.chain(*source_indices_lists))
target_indices.extend(it.chain(*target_indices_lists))
continue
anims.append(anim_class(
source.build_parts_from_indices_lists(source_indices_lists),
target.build_parts_from_indices_lists(target_indices_lists),
**kwargs
))
def get_substr_to_indices_lists_map(part_items):
result = {}
for substr, indices_list in part_items:
if substr not in result:
result[substr] = []
result[substr].append(indices_list)
return result
def add_anims_from(anim_class, func):
source_substr_map = get_substr_to_indices_lists_map(func(source))
target_substr_map = get_substr_to_indices_lists_map(func(target))
common_substrings = sorted([
s for s in source_substr_map if s and s in target_substr_map
], key=len, reverse=True)
add_anims(
anim_class,
[
(source_substr_map[substr], target_substr_map[substr])
for substr in common_substrings
]
)
add_anims(
ReplacementTransform,
[
( (
source.get_submob_indices_lists_by_selector(k), source.get_submob_indices_lists_by_selector(k),
target.get_submob_indices_lists_by_selector(v) target.get_submob_indices_lists_by_selector(v)
) )
for k, v in self.key_map.items() for k, v in self.key_map.items()
] ]),
) (FadeTransformPieces, get_matched_indices_lists(
add_anims_from( source.get_specified_part_items(),
FadeTransformPieces, target.get_specified_part_items()
StringMobject.get_specified_part_items )),
) (FadeTransformPieces, get_matched_indices_lists(
add_anims_from( source.get_group_part_items(),
FadeTransformPieces, target.get_group_part_items()
StringMobject.get_group_part_items ))
) ]
rest_source = VGroup(*[source[index] for index in source_indices]) anims = []
rest_target = VGroup(*[target[index] for index in target_indices]) source_used_indices = []
target_used_indices = []
for anim_class, pairs in anim_class_items:
for source_indices_lists, target_indices_lists in pairs:
source_filtered, source_used = get_filtered_indices_lists(
source_indices_lists, source_used_indices
)
target_filtered, target_used = get_filtered_indices_lists(
target_indices_lists, target_used_indices
)
if not source_filtered or not target_filtered:
continue
anims.append(anim_class(
source.build_parts_from_indices_lists(source_filtered),
target.build_parts_from_indices_lists(target_filtered),
**kwargs
))
source_used_indices.extend(source_used)
target_used_indices.extend(target_used)
rest_source = VGroup(*[
submob for index, submob in enumerate(source.submobjects)
if index not in source_used_indices
])
rest_target = VGroup(*[
submob for index, submob in enumerate(target.submobjects)
if index not in target_used_indices
])
if self.transform_mismatches: if self.transform_mismatches:
anims.append( anims.append(
ReplacementTransform(rest_source, rest_target, **kwargs) ReplacementTransform(rest_source, rest_target, **kwargs)

View file

@ -16,17 +16,9 @@ directories:
# return whatever is at to the TMPDIR environment variable. If you want to # return whatever is at to the TMPDIR environment variable. If you want to
# specify them elsewhere, # specify them elsewhere,
temporary_storage: "" temporary_storage: ""
tex:
executable: "latex"
template_file: "tex_template.tex"
intermediate_filetype: "dvi"
text_to_replace: "[tex_expression]"
# For ctex, use the following configuration
# executable: "xelatex -no-pdf"
# template_file: "ctex_template.tex"
# intermediate_filetype: "xdv"
universal_import_line: "from manimlib import *" universal_import_line: "from manimlib import *"
style: style:
tex_template: "default"
font: "Consolas" font: "Consolas"
text_alignment: "LEFT" text_alignment: "LEFT"
background_color: "#333333" background_color: "#333333"

View file

@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
import re
from manimlib.mobject.svg.string_mobject import StringMobject from manimlib.mobject.svg.string_mobject import StringMobject
from manimlib.utils.tex_file_writing import display_during_execution from manimlib.utils.tex_file_writing import display_during_execution
from manimlib.utils.tex_file_writing import get_tex_config from manimlib.utils.tex_file_writing import tex_content_to_svg_file
from manimlib.utils.tex_file_writing import tex_to_svg_file
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -37,6 +38,8 @@ class MTex(StringMobject):
"alignment": "\\centering", "alignment": "\\centering",
"tex_environment": "align*", "tex_environment": "align*",
"tex_to_color_map": {}, "tex_to_color_map": {},
"template": "",
"additional_preamble": "",
} }
def __init__(self, tex_string: str, **kwargs): def __init__(self, tex_string: str, **kwargs):
@ -57,77 +60,112 @@ class MTex(StringMobject):
self.path_string_config, self.path_string_config,
self.base_color, self.base_color,
self.isolate, self.isolate,
self.protect,
self.tex_string, self.tex_string,
self.alignment, self.alignment,
self.tex_environment, self.tex_environment,
self.tex_to_color_map self.tex_to_color_map,
self.template,
self.additional_preamble
) )
def get_file_path_by_content(self, content: str) -> str: def get_file_path_by_content(self, content: str) -> str:
tex_config = get_tex_config() with display_during_execution(f"Writing \"{self.tex_string}\""):
full_tex = tex_config["tex_body"].replace( file_path = tex_content_to_svg_file(
tex_config["text_to_replace"], content, self.template, self.additional_preamble
content )
)
with display_during_execution(f"Writing \"{self.string}\""):
file_path = tex_to_svg_file(full_tex)
return file_path return file_path
# Parsing # Parsing
def get_cmd_spans(self) -> list[Span]: @staticmethod
return self.find_spans(r"\\(?:[a-zA-Z]+|\s|\S)|[_^{}]") def get_command_matches(string: str) -> list[re.Match]:
# Lump together adjacent brace pairs
def get_substr_flag(self, substr: str) -> int: pattern = re.compile(r"""
return {"{": 1, "}": -1}.get(substr, 0) (?P<command>\\(?:[a-zA-Z]+|.))
|(?P<open>{+)
def get_repl_substr_for_content(self, substr: str) -> str: |(?P<close>}+)
return substr """, flags=re.X | re.S)
result = []
def get_repl_substr_for_matching(self, substr: str) -> str: open_stack = []
return substr if substr.startswith("\\") else "" for match_obj in pattern.finditer(string):
if match_obj.group("open"):
def get_specified_items( open_stack.append((match_obj.span(), len(result)))
self, cmd_span_pairs: list[tuple[Span, Span]] elif match_obj.group("close"):
) -> list[tuple[Span, dict[str, str]]]: close_start, close_end = match_obj.span()
cmd_content_spans = [ while True:
(span_begin, span_end) if not open_stack:
for (_, span_begin), (span_end, _) in cmd_span_pairs raise ValueError("Missing '{' inserted")
] (open_start, open_end), index = open_stack.pop()
specified_spans = [ n = min(open_end - open_start, close_end - close_start)
*[ result.insert(index, pattern.fullmatch(
cmd_content_spans[range_begin] string, pos=open_end - n, endpos=open_end
for _, (range_begin, range_end) in self.compress_neighbours([ ))
(span_begin + index, span_end - index) result.append(pattern.fullmatch(
for index, (span_begin, span_end) in enumerate( string, pos=close_start, endpos=close_start + n
cmd_content_spans ))
) close_start += n
]) if close_start < close_end:
if range_end - range_begin >= 2 continue
], open_end -= n
*[ if open_start < open_end:
span open_stack.append(((open_start, open_end), index))
for selector in self.tex_to_color_map break
for span in self.find_spans_by_selector(selector) else:
], result.append(match_obj)
*self.find_spans_by_selector(self.isolate) if open_stack:
] raise ValueError("Missing '}' inserted")
return [(span, {}) for span in specified_spans] return result
@staticmethod @staticmethod
def get_color_cmd_str(rgb_hex: str) -> str: def get_command_flag(match_obj: re.Match) -> int:
if match_obj.group("open"):
return 1
if match_obj.group("close"):
return -1
return 0
@staticmethod
def replace_for_content(match_obj: re.Match) -> str:
return match_obj.group()
@staticmethod
def replace_for_matching(match_obj: re.Match) -> str:
if match_obj.group("command"):
return match_obj.group()
return ""
@staticmethod
def get_attr_dict_from_command_pair(
open_command: re.Match, close_command: re.Match
) -> dict[str, str] | None:
if len(open_command.group()) >= 2:
return {}
return None
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
return [
(span, {})
for selector in self.tex_to_color_map
for span in self.find_spans_by_selector(selector)
]
@staticmethod
def get_color_command(rgb_hex: str) -> str:
rgb = MTex.hex_to_int(rgb_hex) rgb = MTex.hex_to_int(rgb_hex)
rg, b = divmod(rgb, 256) rg, b = divmod(rgb, 256)
r, g = divmod(rg, 256) r, g = divmod(rg, 256)
return f"\\color[RGB]{{{r}, {g}, {b}}}" return f"\\color[RGB]{{{r}, {g}, {b}}}"
@staticmethod @staticmethod
def get_cmd_str_pair( def get_command_string(
attr_dict: dict[str, str], label_hex: str | None attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> tuple[str, str]: ) -> str:
if label_hex is None: if label_hex is None:
return "", "" return ""
return "{{" + MTex.get_color_cmd_str(label_hex), "}}" if is_end:
return "}}"
return "{{" + MTex.get_color_command(label_hex)
def get_content_prefix_and_suffix( def get_content_prefix_and_suffix(
self, is_labelled: bool self, is_labelled: bool
@ -135,17 +173,14 @@ class MTex(StringMobject):
prefix_lines = [] prefix_lines = []
suffix_lines = [] suffix_lines = []
if not is_labelled: if not is_labelled:
prefix_lines.append(self.get_color_cmd_str(self.base_color_hex)) prefix_lines.append(self.get_color_command(
self.color_to_hex(self.base_color)
))
if self.alignment: if self.alignment:
prefix_lines.append(self.alignment) prefix_lines.append(self.alignment)
if self.tex_environment: if self.tex_environment:
if isinstance(self.tex_environment, str): prefix_lines.append(f"\\begin{{{self.tex_environment}}}")
env_prefix = f"\\begin{{{self.tex_environment}}}" suffix_lines.append(f"\\end{{{self.tex_environment}}}")
env_suffix = f"\\end{{{self.tex_environment}}}"
else:
env_prefix, env_suffix = self.tex_environment
prefix_lines.append(env_prefix)
suffix_lines.append(env_suffix)
return ( return (
"".join([line + "\n" for line in prefix_lines]), "".join([line + "\n" for line in prefix_lines]),
"".join(["\n" + line for line in suffix_lines]) "".join(["\n" + line for line in suffix_lines])
@ -156,8 +191,8 @@ class MTex(StringMobject):
def get_parts_by_tex(self, selector: Selector) -> VGroup: def get_parts_by_tex(self, selector: Selector) -> VGroup:
return self.select_parts(selector) return self.select_parts(selector)
def get_part_by_tex(self, selector: Selector) -> VGroup: def get_part_by_tex(self, selector: Selector, **kwargs) -> VGroup:
return self.select_part(selector) return self.select_part(selector, **kwargs)
def set_color_by_tex(self, selector: Selector, color: ManimColor): def set_color_by_tex(self, selector: Selector, color: ManimColor):
return self.set_parts_color(selector, color) return self.set_parts_color(selector, color)

View file

@ -18,7 +18,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from colour import Color from colour import Color
from typing import Iterable, Sequence, TypeVar, Union from typing import Callable, Iterable, Union
ManimColor = Union[str, Color] ManimColor = Union[str, Color]
Span = tuple[int, int] Span = tuple[int, int]
@ -32,7 +32,6 @@ if TYPE_CHECKING:
tuple[Union[int, None], Union[int, None]] tuple[Union[int, None], Union[int, None]]
]] ]]
] ]
T = TypeVar("T")
class StringMobject(SVGMobject, ABC): class StringMobject(SVGMobject, ABC):
@ -47,7 +46,7 @@ class StringMobject(SVGMobject, ABC):
if they want to do anything with their corresponding submobjects. if they want to do anything with their corresponding submobjects.
`isolate` parameter can be either a string, a `re.Pattern` object, `isolate` parameter can be either a string, a `re.Pattern` object,
or a 2-tuple containing integers or None, or a collection of the above. or a 2-tuple containing integers or None, or a collection of the above.
Note, substrings specified cannot *partially* overlap with each other. Note, substrings specified cannot *partly* overlap with each other.
Each instance of `StringMobject` generates 2 svg files. Each instance of `StringMobject` generates 2 svg files.
The additional one is generated with some color commands inserted, The additional one is generated with some color commands inserted,
@ -64,6 +63,7 @@ class StringMobject(SVGMobject, ABC):
}, },
"base_color": WHITE, "base_color": WHITE,
"isolate": (), "isolate": (),
"protect": (),
} }
def __init__(self, string: str, **kwargs): def __init__(self, string: str, **kwargs):
@ -71,9 +71,7 @@ class StringMobject(SVGMobject, ABC):
digest_config(self, kwargs) digest_config(self, kwargs)
if self.base_color is None: if self.base_color is None:
self.base_color = WHITE self.base_color = WHITE
self.base_color_hex = self.color_to_hex(self.base_color)
self.full_span = (0, len(self.string))
self.parse() self.parse()
super().__init__(**kwargs) super().__init__(**kwargs)
self.labels = [submob.label for submob in self.submobjects] self.labels = [submob.label for submob in self.submobjects]
@ -90,9 +88,9 @@ class StringMobject(SVGMobject, ABC):
super().generate_mobject() super().generate_mobject()
labels_count = len(self.labelled_spans) labels_count = len(self.labelled_spans)
if not labels_count: if labels_count == 1:
for submob in self.submobjects: for submob in self.submobjects:
submob.label = -1 submob.label = 0
return return
labelled_content = self.get_content(is_labelled=True) labelled_content = self.get_content(is_labelled=True)
@ -104,7 +102,7 @@ class StringMobject(SVGMobject, ABC):
"to the original svg. Skip the labelling process." "to the original svg. Skip the labelling process."
) )
for submob in self.submobjects: for submob in self.submobjects:
submob.label = -1 submob.label = 0
return return
self.rearrange_submobjects_by_positions(labelled_svg) self.rearrange_submobjects_by_positions(labelled_svg)
@ -112,18 +110,21 @@ class StringMobject(SVGMobject, ABC):
for submob, labelled_svg_submob in zip( for submob, labelled_svg_submob in zip(
self.submobjects, labelled_svg.submobjects self.submobjects, labelled_svg.submobjects
): ):
color_int = self.hex_to_int(self.color_to_hex( label = self.hex_to_int(self.color_to_hex(
labelled_svg_submob.get_fill_color() labelled_svg_submob.get_fill_color()
)) ))
if color_int > labels_count: if label >= labels_count:
unrecognizable_colors.append(color_int) unrecognizable_colors.append(label)
color_int = 0 label = 0
submob.label = color_int - 1 submob.label = label
if unrecognizable_colors: if unrecognizable_colors:
log.warning( log.warning(
"Unrecognizable color labels detected (%s, etc). " "Unrecognizable color labels detected (%s). "
"The result could be unexpected.", "The result could be unexpected.",
self.int_to_hex(unrecognizable_colors[0]) ", ".join(
self.int_to_hex(color)
for color in unrecognizable_colors
)
) )
def rearrange_submobjects_by_positions( def rearrange_submobjects_by_positions(
@ -153,30 +154,27 @@ class StringMobject(SVGMobject, ABC):
# Toolkits # Toolkits
def get_substr(self, span: Span) -> str:
return self.string[slice(*span)]
def find_spans(self, pattern: str | re.Pattern) -> list[Span]:
return [
match_obj.span()
for match_obj in re.finditer(pattern, self.string)
]
def find_spans_by_selector(self, selector: Selector) -> list[Span]: def find_spans_by_selector(self, selector: Selector) -> list[Span]:
def find_spans_by_single_selector(sel): def find_spans_by_single_selector(sel):
if isinstance(sel, str): if isinstance(sel, str):
return self.find_spans(re.escape(sel)) return [
match_obj.span()
for match_obj in re.finditer(re.escape(sel), self.string)
]
if isinstance(sel, re.Pattern): if isinstance(sel, re.Pattern):
return self.find_spans(sel) return [
match_obj.span()
for match_obj in sel.finditer(self.string)
]
if isinstance(sel, tuple) and len(sel) == 2 and all( if isinstance(sel, tuple) and len(sel) == 2 and all(
isinstance(index, int) or index is None isinstance(index, int) or index is None
for index in sel for index in sel
): ):
l = self.full_span[1] l = len(self.string)
span = tuple( span = tuple(
default_index if index is None else default_index if index is None else
min(index, l) if index >= 0 else max(index + l, 0) min(index, l) if index >= 0 else max(index + l, 0)
for index, default_index in zip(sel, self.full_span) for index, default_index in zip(sel, (0, l))
) )
return [span] return [span]
return None return None
@ -189,57 +187,12 @@ class StringMobject(SVGMobject, ABC):
if spans is None: if spans is None:
raise TypeError(f"Invalid selector: '{sel}'") raise TypeError(f"Invalid selector: '{sel}'")
result.extend(spans) result.extend(spans)
return result return list(filter(lambda span: span[0] <= span[1], result))
@staticmethod
def get_neighbouring_pairs(vals: Sequence[T]) -> list[tuple[T, T]]:
return list(zip(vals[:-1], vals[1:]))
@staticmethod
def compress_neighbours(vals: Sequence[T]) -> list[tuple[T, Span]]:
if not vals:
return []
unique_vals = [vals[0]]
indices = [0]
for index, val in enumerate(vals):
if val == unique_vals[-1]:
continue
unique_vals.append(val)
indices.append(index)
indices.append(len(vals))
val_ranges = StringMobject.get_neighbouring_pairs(indices)
return list(zip(unique_vals, val_ranges))
@staticmethod @staticmethod
def span_contains(span_0: Span, span_1: Span) -> bool: def span_contains(span_0: Span, span_1: Span) -> bool:
return span_0[0] <= span_1[0] and span_0[1] >= span_1[1] return span_0[0] <= span_1[0] and span_0[1] >= span_1[1]
@staticmethod
def get_complement_spans(
universal_span: Span, interval_spans: list[Span]
) -> list[Span]:
if not interval_spans:
return [universal_span]
span_ends, span_begins = zip(*interval_spans)
return list(zip(
(universal_span[0], *span_begins),
(*span_ends, universal_span[1])
))
def replace_substr(self, span: Span, repl_items: list[Span, str]):
if not repl_items:
return self.get_substr(span)
repl_spans, repl_strs = zip(*sorted(repl_items, key=lambda t: t[0]))
pieces = [
self.get_substr(piece_span)
for piece_span in self.get_complement_spans(span, repl_spans)
]
repl_strs = [*repl_strs, ""]
return "".join(it.chain(*zip(pieces, repl_strs)))
@staticmethod @staticmethod
def color_to_hex(color: ManimColor) -> str: def color_to_hex(color: ManimColor) -> str:
return rgb_to_hex(color_to_rgb(color)) return rgb_to_hex(color_to_rgb(color))
@ -255,131 +208,220 @@ class StringMobject(SVGMobject, ABC):
# Parsing # Parsing
def parse(self) -> None: def parse(self) -> None:
cmd_spans = self.get_cmd_spans() def get_substr(span: Span) -> str:
cmd_substrs = [self.get_substr(span) for span in cmd_spans] return self.string[slice(*span)]
flags = [self.get_substr_flag(substr) for substr in cmd_substrs]
specified_items = self.get_specified_items( configured_items = self.get_configured_items()
self.get_cmd_span_pairs(cmd_spans, flags) isolated_spans = self.find_spans_by_selector(self.isolate)
) protected_spans = self.find_spans_by_selector(self.protect)
split_items = [ command_matches = self.get_command_matches(self.string)
(span, attr_dict)
for specified_span, attr_dict in specified_items def get_key(category, i, flag):
for span in self.split_span_by_levels( def get_span_by_category(category, i):
specified_span, cmd_spans, flags if category == 0:
return configured_items[i][0]
if category == 1:
return isolated_spans[i]
if category == 2:
return protected_spans[i]
return command_matches[i].span()
index, paired_index = get_span_by_category(category, i)[::flag]
return (
index,
flag * (2 if index != paired_index else -1),
-paired_index,
flag * category,
flag * i
) )
]
self.specified_spans = [span for span, _ in specified_items] index_items = sorted([
self.split_items = split_items (category, i, flag)
self.labelled_spans = [span for span, _ in split_items] for category, item_length in enumerate((
self.cmd_repl_items_for_content = [ len(configured_items),
(span, self.get_repl_substr_for_content(substr)) len(isolated_spans),
for span, substr in zip(cmd_spans, cmd_substrs) len(protected_spans),
] len(command_matches)
self.cmd_repl_items_for_matching = [ ))
(span, self.get_repl_substr_for_matching(substr)) for i in range(item_length)
for span, substr in zip(cmd_spans, cmd_substrs) for flag in (1, -1)
] ], key=lambda t: get_key(*t))
self.check_overlapping()
inserted_items = []
labelled_items = []
overlapping_spans = []
level_mismatched_spans = []
label = 1
protect_level = 0
bracket_stack = [0]
bracket_count = 0
open_command_stack = []
open_stack = []
for category, i, flag in index_items:
if category >= 2:
protect_level += flag
if flag == 1 or category == 2:
continue
inserted_items.append((i, 0))
command_match = command_matches[i]
command_flag = self.get_command_flag(command_match)
if command_flag == 1:
bracket_count += 1
bracket_stack.append(bracket_count)
open_command_stack.append((len(inserted_items), i))
continue
if command_flag == 0:
continue
pos, i_ = open_command_stack.pop()
bracket_stack.pop()
open_command_match = command_matches[i_]
attr_dict = self.get_attr_dict_from_command_pair(
open_command_match, command_match
)
if attr_dict is None:
continue
span = (open_command_match.end(), command_match.start())
labelled_items.append((span, attr_dict))
inserted_items.insert(pos, (label, 1))
inserted_items.insert(-1, (label, -1))
label += 1
continue
if flag == 1:
open_stack.append((
len(inserted_items), category, i,
protect_level, bracket_stack.copy()
))
continue
span, attr_dict = configured_items[i] \
if category == 0 else (isolated_spans[i], {})
pos, category_, i_, protect_level_, bracket_stack_ \
= open_stack.pop()
if category_ != category or i_ != i:
overlapping_spans.append(span)
continue
if protect_level_ or protect_level:
continue
if bracket_stack_ != bracket_stack:
level_mismatched_spans.append(span)
continue
labelled_items.append((span, attr_dict))
inserted_items.insert(pos, (label, 1))
inserted_items.append((label, -1))
label += 1
labelled_items.insert(0, ((0, len(self.string)), {}))
inserted_items.insert(0, (0, 1))
inserted_items.append((0, -1))
if overlapping_spans:
log.warning(
"Partly overlapping substrings detected: %s",
", ".join(
f"'{get_substr(span)}'"
for span in overlapping_spans
)
)
if level_mismatched_spans:
log.warning(
"Cannot handle substrings: %s",
", ".join(
f"'{get_substr(span)}'"
for span in level_mismatched_spans
)
)
def reconstruct_string(
start_item: tuple[int, int],
end_item: tuple[int, int],
command_replace_func: Callable[[re.Match], str],
command_insert_func: Callable[[int, int, dict[str, str]], str]
) -> str:
def get_edge_item(i: int, flag: int) -> tuple[Span, str]:
if flag == 0:
match_obj = command_matches[i]
return (
match_obj.span(),
command_replace_func(match_obj)
)
span, attr_dict = labelled_items[i]
index = span[flag < 0]
return (
(index, index),
command_insert_func(i, flag, attr_dict)
)
items = [
get_edge_item(i, flag)
for i, flag in inserted_items[slice(
inserted_items.index(start_item),
inserted_items.index(end_item) + 1
)]
]
pieces = [
get_substr((start, end))
for start, end in zip(
[interval_end for (_, interval_end), _ in items[:-1]],
[interval_start for (interval_start, _), _ in items[1:]]
)
]
interval_pieces = [piece for _, piece in items[1:-1]]
return "".join(it.chain(*zip(pieces, (*interval_pieces, ""))))
self.labelled_spans = [span for span, _ in labelled_items]
self.reconstruct_string = reconstruct_string
def get_content(self, is_labelled: bool) -> str:
content = self.reconstruct_string(
(0, 1), (0, -1),
self.replace_for_content,
lambda label, flag, attr_dict: self.get_command_string(
attr_dict,
is_end=flag < 0,
label_hex=self.int_to_hex(label) if is_labelled else None
)
)
prefix, suffix = self.get_content_prefix_and_suffix(
is_labelled=is_labelled
)
return "".join((prefix, content, suffix))
@staticmethod
@abstractmethod @abstractmethod
def get_cmd_spans(self) -> list[Span]: def get_command_matches(string: str) -> list[re.Match]:
return [] return []
@staticmethod
@abstractmethod @abstractmethod
def get_substr_flag(self, substr: str) -> int: def get_command_flag(match_obj: re.Match) -> int:
return 0 return 0
@staticmethod
@abstractmethod @abstractmethod
def get_repl_substr_for_content(self, substr: str) -> str: def replace_for_content(match_obj: re.Match) -> str:
return ""
@abstractmethod
def get_repl_substr_for_matching(self, substr: str) -> str:
return "" return ""
@staticmethod @staticmethod
def get_cmd_span_pairs( @abstractmethod
cmd_spans: list[Span], flags: list[int] def replace_for_matching(match_obj: re.Match) -> str:
) -> list[tuple[Span, Span]]: return ""
result = []
begin_cmd_spans_stack = [] @staticmethod
for cmd_span, flag in zip(cmd_spans, flags): @abstractmethod
if flag == 1: def get_attr_dict_from_command_pair(
begin_cmd_spans_stack.append(cmd_span) open_command: re.Match, close_command: re.Match,
elif flag == -1: ) -> dict[str, str] | None:
if not begin_cmd_spans_stack: return None
raise ValueError("Missing open command")
begin_cmd_span = begin_cmd_spans_stack.pop()
result.append((begin_cmd_span, cmd_span))
if begin_cmd_spans_stack:
raise ValueError("Missing close command")
return result
@abstractmethod @abstractmethod
def get_specified_items( def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
self, cmd_span_pairs: list[tuple[Span, Span]]
) -> list[tuple[Span, dict[str, str]]]:
return [] return []
def split_span_by_levels(
self, arbitrary_span: Span, cmd_spans: list[Span], flags: list[int]
) -> list[Span]:
cmd_range = (
sum([
arbitrary_span[0] > interval_begin
for interval_begin, _ in cmd_spans
]),
sum([
arbitrary_span[1] >= interval_end
for _, interval_end in cmd_spans
])
)
complement_spans = self.get_complement_spans(
self.full_span, cmd_spans
)
adjusted_span = (
max(arbitrary_span[0], complement_spans[cmd_range[0]][0]),
min(arbitrary_span[1], complement_spans[cmd_range[1]][1])
)
if adjusted_span[0] > adjusted_span[1]:
return []
upward_cmd_spans = []
downward_cmd_spans = []
for cmd_span, flag in list(zip(cmd_spans, flags))[slice(*cmd_range)]:
if flag == 1:
upward_cmd_spans.append(cmd_span)
elif flag == -1:
if upward_cmd_spans:
upward_cmd_spans.pop()
else:
downward_cmd_spans.append(cmd_span)
return list(filter(
lambda span: self.get_substr(span).strip(),
self.get_complement_spans(
adjusted_span, downward_cmd_spans + upward_cmd_spans
)
))
def check_overlapping(self) -> None:
labelled_spans = self.labelled_spans
if len(labelled_spans) >= 16777216:
raise ValueError("Cannot handle that many substrings")
for span_0, span_1 in it.product(labelled_spans, repeat=2):
if not span_0[0] < span_1[0] < span_0[1] < span_1[1]:
continue
raise ValueError(
"Partially overlapping substrings detected: "
f"'{self.get_substr(span_0)}' and '{self.get_substr(span_1)}'"
)
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def get_cmd_str_pair( def get_command_string(
attr_dict: dict[str, str], label_hex: str | None attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> tuple[str, str]: ) -> str:
return "", "" return ""
@abstractmethod @abstractmethod
def get_content_prefix_and_suffix( def get_content_prefix_and_suffix(
@ -387,38 +429,6 @@ class StringMobject(SVGMobject, ABC):
) -> tuple[str, str]: ) -> tuple[str, str]:
return "", "" return "", ""
def get_content(self, is_labelled: bool) -> str:
inserted_str_pairs = [
(span, self.get_cmd_str_pair(
attr_dict,
label_hex=self.int_to_hex(label + 1) if is_labelled else None
))
for label, (span, attr_dict) in enumerate(self.split_items)
]
inserted_str_items = sorted([
(index, s)
for (index, _), s in [
*sorted([
(span[::-1], end_str)
for span, (_, end_str) in reversed(inserted_str_pairs)
], key=lambda t: (t[0][0], -t[0][1])),
*sorted([
(span, begin_str)
for span, (begin_str, _) in inserted_str_pairs
], key=lambda t: (t[0][0], -t[0][1]))
]
], key=lambda t: t[0])
repl_items = self.cmd_repl_items_for_content + [
((index, index), inserted_str)
for index, inserted_str in inserted_str_items
]
prefix, suffix = self.get_content_prefix_and_suffix(is_labelled)
return "".join([
prefix,
self.replace_substr(self.full_span, repl_items),
suffix
])
# Selector # Selector
def get_submob_indices_list_by_span( def get_submob_indices_list_by_span(
@ -427,59 +437,69 @@ class StringMobject(SVGMobject, ABC):
return [ return [
submob_index submob_index
for submob_index, label in enumerate(self.labels) for submob_index, label in enumerate(self.labels)
if label != -1 and self.span_contains( if self.span_contains(arbitrary_span, self.labelled_spans[label])
arbitrary_span, self.labelled_spans[label]
)
] ]
def get_specified_part_items(self) -> list[tuple[str, list[int]]]: def get_specified_part_items(self) -> list[tuple[str, list[int]]]:
return [ return [
( (
self.get_substr(span), self.string[slice(*span)],
self.get_submob_indices_list_by_span(span) self.get_submob_indices_list_by_span(span)
) )
for span in self.specified_spans for span in self.labelled_spans[1:]
] ]
def get_group_part_items(self) -> list[tuple[str, list[int]]]: def get_group_part_items(self) -> list[tuple[str, list[int]]]:
if not self.labels: if not self.labels:
return [] return []
group_labels, labelled_submob_ranges = zip( def get_neighbouring_pairs(vals):
*self.compress_neighbours(self.labels) return list(zip(vals[:-1], vals[1:]))
)
ordered_spans = [ range_lens, group_labels = zip(*(
self.labelled_spans[label] if label != -1 else self.full_span (len(list(grouper)), val)
for label in group_labels for val, grouper in it.groupby(self.labels)
] ))
interval_spans = [
(
next_span[0]
if self.span_contains(prev_span, next_span)
else prev_span[1],
prev_span[1]
if self.span_contains(next_span, prev_span)
else next_span[0]
)
for prev_span, next_span in self.get_neighbouring_pairs(
ordered_spans
)
]
group_substrs = [
re.sub(r"\s+", "", self.replace_substr(
span, [
(cmd_span, repl_str)
for cmd_span, repl_str in self.cmd_repl_items_for_matching
if self.span_contains(span, cmd_span)
]
))
for span in self.get_complement_spans(
(ordered_spans[0][0], ordered_spans[-1][1]), interval_spans
)
]
submob_indices_lists = [ submob_indices_lists = [
list(range(*submob_range)) list(range(*submob_range))
for submob_range in labelled_submob_ranges for submob_range in get_neighbouring_pairs(
[0, *it.accumulate(range_lens)]
)
]
labelled_spans = self.labelled_spans
start_items = [
(group_labels[0], 1),
*(
(curr_label, 1)
if self.span_contains(
labelled_spans[prev_label], labelled_spans[curr_label]
)
else (prev_label, -1)
for prev_label, curr_label in get_neighbouring_pairs(
group_labels
)
)
]
end_items = [
*(
(curr_label, -1)
if self.span_contains(
labelled_spans[next_label], labelled_spans[curr_label]
)
else (next_label, 1)
for curr_label, next_label in get_neighbouring_pairs(
group_labels
)
),
(group_labels[-1], -1)
]
group_substrs = [
re.sub(r"\s+", "", self.reconstruct_string(
start_item, end_item,
self.replace_for_matching,
lambda label, flag, attr_dict: ""
))
for start_item, end_item in zip(start_items, end_items)
] ]
return list(zip(group_substrs, submob_indices_lists)) return list(zip(group_substrs, submob_indices_lists))
@ -497,13 +517,13 @@ class StringMobject(SVGMobject, ABC):
def build_parts_from_indices_lists( def build_parts_from_indices_lists(
self, indices_lists: list[list[int]] self, indices_lists: list[list[int]]
) -> VGroup: ) -> VGroup:
return VGroup(*[ return VGroup(*(
VGroup(*[ VGroup(*(
self.submobjects[submob_index] self.submobjects[submob_index]
for submob_index in indices_list for submob_index in indices_list
]) ))
for indices_list in indices_lists for indices_list in indices_lists
]) ))
def build_groups(self) -> VGroup: def build_groups(self) -> VGroup:
return self.build_parts_from_indices_lists([ return self.build_parts_from_indices_lists([

View file

@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import hashlib
import os import os
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
@ -19,6 +18,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.directories import get_mobject_data_dir from manimlib.utils.directories import get_mobject_data_dir
from manimlib.utils.images import get_full_vector_image_path from manimlib.utils.images import get_full_vector_image_path
from manimlib.utils.iterables import hash_obj from manimlib.utils.iterables import hash_obj
from manimlib.utils.simple_functions import hash_string
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {} SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
@ -106,7 +106,7 @@ class SVGMobject(VMobject):
return get_full_vector_image_path(self.file_name) return get_full_vector_image_path(self.file_name)
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree: def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
config_style_dict = self.generate_config_style_dict() config_style_attrs = self.generate_config_style_dict()
style_keys = ( style_keys = (
"fill", "fill",
"fill-opacity", "fill-opacity",
@ -116,14 +116,17 @@ class SVGMobject(VMobject):
"style" "style"
) )
root = element_tree.getroot() root = element_tree.getroot()
root_style_dict = { style_attrs = {
k: v for k, v in root.attrib.items() k: v
for k, v in root.attrib.items()
if k in style_keys if k in style_keys
} }
new_root = ET.Element("svg", {}) # Ignore other attributes in case that svgelements cannot parse them
config_style_node = ET.SubElement(new_root, "g", config_style_dict) SVG_XMLNS = "{http://www.w3.org/2000/svg}"
root_style_node = ET.SubElement(config_style_node, "g", root_style_dict) new_root = ET.Element("svg")
config_style_node = ET.SubElement(new_root, f"{SVG_XMLNS}g", config_style_attrs)
root_style_node = ET.SubElement(config_style_node, f"{SVG_XMLNS}g", style_attrs)
root_style_node.extend(root) root_style_node.extend(root)
return ET.ElementTree(new_root) return ET.ElementTree(new_root)
@ -147,7 +150,7 @@ class SVGMobject(VMobject):
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]: def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
result = [] result = []
for shape in svg.elements(): for shape in svg.elements():
if isinstance(shape, se.Group): if isinstance(shape, (se.Group, se.Use)):
continue continue
elif isinstance(shape, se.Path): elif isinstance(shape, se.Path):
mob = self.path_to_mobject(shape) mob = self.path_to_mobject(shape)
@ -155,9 +158,7 @@ class SVGMobject(VMobject):
mob = self.line_to_mobject(shape) mob = self.line_to_mobject(shape)
elif isinstance(shape, se.Rect): elif isinstance(shape, se.Rect):
mob = self.rect_to_mobject(shape) mob = self.rect_to_mobject(shape)
elif isinstance(shape, se.Circle): elif isinstance(shape, (se.Circle, se.Ellipse)):
mob = self.circle_to_mobject(shape)
elif isinstance(shape, se.Ellipse):
mob = self.ellipse_to_mobject(shape) mob = self.ellipse_to_mobject(shape)
elif isinstance(shape, se.Polygon): elif isinstance(shape, se.Polygon):
mob = self.polygon_to_mobject(shape) mob = self.polygon_to_mobject(shape)
@ -168,11 +169,12 @@ class SVGMobject(VMobject):
elif type(shape) == se.SVGElement: elif type(shape) == se.SVGElement:
continue continue
else: else:
log.warning(f"Unsupported element type: {type(shape)}") log.warning("Unsupported element type: %s", type(shape))
continue continue
if not mob.has_points(): if not mob.has_points():
continue continue
self.apply_style_to_mobject(mob, shape) if isinstance(shape, se.GraphicObject):
self.apply_style_to_mobject(mob, shape)
if isinstance(shape, se.Transformable) and shape.apply: if isinstance(shape, se.Transformable) and shape.apply:
self.handle_transform(mob, shape.transform) self.handle_transform(mob, shape.transform)
result.append(mob) result.append(mob)
@ -203,21 +205,10 @@ class SVGMobject(VMobject):
) )
return mob return mob
@staticmethod
def handle_transform(mob, matrix):
mat = np.array([
[matrix.a, matrix.c],
[matrix.b, matrix.d]
])
vec = np.array([matrix.e, matrix.f, 0.0])
mob.apply_matrix(mat)
mob.shift(vec)
return mob
def path_to_mobject(self, path: se.Path) -> VMobjectFromSVGPath: def path_to_mobject(self, path: se.Path) -> VMobjectFromSVGPath:
return VMobjectFromSVGPath(path, **self.path_string_config) return VMobjectFromSVGPath(path, **self.path_string_config)
def line_to_mobject(self, line: se.Line) -> Line: def line_to_mobject(self, line: se.SimpleLine) -> Line:
return Line( return Line(
start=_convert_point_to_3d(line.x1, line.y1), start=_convert_point_to_3d(line.x1, line.y1),
end=_convert_point_to_3d(line.x2, line.y2) end=_convert_point_to_3d(line.x2, line.y2)
@ -242,15 +233,7 @@ class SVGMobject(VMobject):
)) ))
return mob return mob
def circle_to_mobject(self, circle: se.Circle) -> Circle: def ellipse_to_mobject(self, ellipse: se.Circle | se.Ellipse) -> Circle:
# svgelements supports `rx` & `ry` but `r`
mob = Circle(radius=circle.rx)
mob.shift(_convert_point_to_3d(
circle.cx, circle.cy
))
return mob
def ellipse_to_mobject(self, ellipse: se.Ellipse) -> Circle:
mob = Circle(radius=ellipse.rx) mob = Circle(radius=ellipse.rx)
mob.stretch_to_fit_height(2 * ellipse.ry) mob.stretch_to_fit_height(2 * ellipse.ry)
mob.shift(_convert_point_to_3d( mob.shift(_convert_point_to_3d(
@ -302,8 +285,7 @@ class VMobjectFromSVGPath(VMobject):
# will be saved to a file so that future calls for the same path # will be saved to a file so that future calls for the same path
# don't need to retrace the same computation. # don't need to retrace the same computation.
path_string = self.path_obj.d() path_string = self.path_obj.d()
hasher = hashlib.sha256(path_string.encode()) path_hash = hash_string(path_string)
path_hash = hasher.hexdigest()[:16]
points_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_points.npy") points_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_points.npy")
tris_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_tris.npy") tris_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_tris.npy")

View file

@ -13,8 +13,7 @@ from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import digest_config
from manimlib.utils.tex_file_writing import display_during_execution from manimlib.utils.tex_file_writing import display_during_execution
from manimlib.utils.tex_file_writing import get_tex_config from manimlib.utils.tex_file_writing import tex_content_to_svg_file
from manimlib.utils.tex_file_writing import tex_to_svg_file
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -44,6 +43,8 @@ class SingleStringTex(SVGMobject):
"alignment": "\\centering", "alignment": "\\centering",
"math_mode": True, "math_mode": True,
"organize_left_to_right": False, "organize_left_to_right": False,
"template": "",
"additional_preamble": "",
} }
def __init__(self, tex_string: str, **kwargs): def __init__(self, tex_string: str, **kwargs):
@ -64,27 +65,24 @@ class SingleStringTex(SVGMobject):
self.path_string_config, self.path_string_config,
self.tex_string, self.tex_string,
self.alignment, self.alignment,
self.math_mode self.math_mode,
self.template,
self.additional_preamble
) )
def get_file_path(self) -> str: def get_file_path(self) -> str:
full_tex = self.get_tex_file_body(self.tex_string) content = self.get_tex_file_body(self.tex_string)
with display_during_execution(f"Writing \"{self.tex_string}\""): with display_during_execution(f"Writing \"{self.tex_string}\""):
file_path = tex_to_svg_file(full_tex) file_path = tex_content_to_svg_file(
content, self.template, self.additional_preamble
)
return file_path return file_path
def get_tex_file_body(self, tex_string: str) -> str: def get_tex_file_body(self, tex_string: str) -> str:
new_tex = self.get_modified_expression(tex_string) new_tex = self.get_modified_expression(tex_string)
if self.math_mode: if self.math_mode:
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}" new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
return self.alignment + "\n" + new_tex
new_tex = self.alignment + "\n" + new_tex
tex_config = get_tex_config()
return tex_config["tex_body"].replace(
tex_config["text_to_replace"],
new_tex
)
def get_modified_expression(self, tex_string: str) -> str: def get_modified_expression(self, tex_string: str) -> str:
return self.modify_special_strings(tex_string.strip()) return self.modify_special_strings(tex_string.strip())

View file

@ -18,7 +18,7 @@ from manimlib.utils.config_ops import digest_config
from manimlib.utils.customization import get_customization from manimlib.utils.customization import get_customization
from manimlib.utils.directories import get_downloads_dir from manimlib.utils.directories import get_downloads_dir
from manimlib.utils.directories import get_text_dir from manimlib.utils.directories import get_text_dir
from manimlib.utils.tex_file_writing import tex_hash from manimlib.utils.simple_functions import hash_string
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -63,7 +63,6 @@ class _Alignment:
class MarkupText(StringMobject): class MarkupText(StringMobject):
CONFIG = { CONFIG = {
"is_markup": True,
"font_size": 48, "font_size": 48,
"lsh": None, "lsh": None,
"justify": False, "justify": False,
@ -81,21 +80,11 @@ class MarkupText(StringMobject):
"t2w": {}, "t2w": {},
"global_config": {}, "global_config": {},
"local_configs": {}, "local_configs": {},
# For backward compatibility "disable_ligatures": True,
"isolate": (re.compile(r"[a-zA-Z]+"), re.compile(r"\S+")), "isolate": re.compile(r"\w+", re.U),
} }
# See https://docs.gtk.org/Pango/pango_markup.html # See https://docs.gtk.org/Pango/pango_markup.html
MARKUP_COLOR_KEYS = {
"foreground": False,
"fgcolor": False,
"color": False,
"background": True,
"bgcolor": True,
"underline_color": True,
"overline_color": True,
"strikethrough_color": True,
}
MARKUP_TAGS = { MARKUP_TAGS = {
"b": {"font_weight": "bold"}, "b": {"font_weight": "bold"},
"big": {"font_size": "larger"}, "big": {"font_size": "larger"},
@ -107,17 +96,24 @@ class MarkupText(StringMobject):
"tt": {"font_family": "monospace"}, "tt": {"font_family": "monospace"},
"u": {"underline": "single"}, "u": {"underline": "single"},
} }
MARKUP_ENTITY_DICT = {
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
"\"": "&quot;",
"'": "&apos;"
}
def __init__(self, text: str, **kwargs): def __init__(self, text: str, **kwargs):
self.full2short(kwargs) self.full2short(kwargs)
digest_config(self, kwargs) digest_config(self, kwargs)
if not isinstance(self, Text):
self.validate_markup_string(text)
if not self.font: if not self.font:
self.font = get_customization()["style"]["font"] self.font = get_customization()["style"]["font"]
if not self.alignment: if not self.alignment:
self.alignment = get_customization()["style"]["text_alignment"] self.alignment = get_customization()["style"]["text_alignment"]
if self.is_markup:
self.validate_markup_string(text)
self.text = text self.text = text
super().__init__(text, **kwargs) super().__init__(text, **kwargs)
@ -140,8 +136,8 @@ class MarkupText(StringMobject):
self.path_string_config, self.path_string_config,
self.base_color, self.base_color,
self.isolate, self.isolate,
self.protect,
self.text, self.text,
self.is_markup,
self.font_size, self.font_size,
self.lsh, self.lsh,
self.justify, self.justify,
@ -156,7 +152,8 @@ class MarkupText(StringMobject):
self.t2s, self.t2s,
self.t2w, self.t2w,
self.global_config, self.global_config,
self.local_configs self.local_configs,
self.disable_ligatures
) )
def full2short(self, config: dict) -> None: def full2short(self, config: dict) -> None:
@ -182,7 +179,7 @@ class MarkupText(StringMobject):
self.line_width self.line_width
)) ))
svg_file = os.path.join( svg_file = os.path.join(
get_text_dir(), tex_hash(hash_content) + ".svg" get_text_dir(), hash_string(hash_content) + ".svg"
) )
if not os.path.exists(svg_file): if not os.path.exists(svg_file):
self.markup_to_svg(content, svg_file) self.markup_to_svg(content, svg_file)
@ -229,76 +226,92 @@ class MarkupText(StringMobject):
f"{validate_error}" f"{validate_error}"
) )
# Toolkits
@staticmethod
def escape_markup_char(substr: str) -> str:
return MarkupText.MARKUP_ENTITY_DICT.get(substr, substr)
@staticmethod
def unescape_markup_char(substr: str) -> str:
return {
v: k
for k, v in MarkupText.MARKUP_ENTITY_DICT.items()
}.get(substr, substr)
# Parsing # Parsing
def get_cmd_spans(self) -> list[Span]: @staticmethod
if not self.is_markup: def get_command_matches(string: str) -> list[re.Match]:
return self.find_spans(r"""[<>&"']""") pattern = re.compile(r"""
(?P<tag>
<
(?P<close_slash>/)?
(?P<tag_name>\w+)\s*
(?P<attr_list>(?:\w+\s*\=\s*(?P<quot>["']).*?(?P=quot)\s*)*)
(?P<elision_slash>/)?
>
)
|(?P<passthrough>
<\?.*?\?>|<!--.*?-->|<!\[CDATA\[.*?\]\]>|<!DOCTYPE.*?>
)
|(?P<entity>&(?P<unicode>\#(?P<hex>x)?)?(?P<content>.*?);)
|(?P<char>[>"'])
""", flags=re.X | re.S)
return list(pattern.finditer(string))
# Unsupported passthroughs: @staticmethod
# "<?...?>", "<!--...-->", "<![CDATA[...]]>", "<!DOCTYPE...>" def get_command_flag(match_obj: re.Match) -> int:
# See https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/gmarkup.c if match_obj.group("tag"):
return self.find_spans( if match_obj.group("close_slash"):
r"""&[\s\S]*?;|[>"']|</?\w+(?:\s*\w+\s*\=\s*(["'])[\s\S]*?\1)*/?>""" return -1
) if not match_obj.group("elision_slash"):
return 1
def get_substr_flag(self, substr: str) -> int:
if re.fullmatch(r"<\w[\s\S]*[^/]>", substr):
return 1
if substr.startswith("</"):
return -1
return 0 return 0
def get_repl_substr_for_content(self, substr: str) -> str: @staticmethod
if substr.startswith("<") and substr.endswith(">"): def replace_for_content(match_obj: re.Match) -> str:
if match_obj.group("tag"):
return "" return ""
return { if match_obj.group("char"):
"<": "&lt;", return MarkupText.escape_markup_char(match_obj.group("char"))
">": "&gt;", return match_obj.group()
"&": "&amp;",
"\"": "&quot;",
"'": "&apos;"
}.get(substr, substr)
def get_repl_substr_for_matching(self, substr: str) -> str: @staticmethod
if substr.startswith("<") and substr.endswith(">"): def replace_for_matching(match_obj: re.Match) -> str:
if match_obj.group("tag") or match_obj.group("passthrough"):
return "" return ""
if substr.startswith("&#") and substr.endswith(";"): if match_obj.group("entity"):
if substr.startswith("&#x"): if match_obj.group("unicode"):
char_reference = int(substr[3:-1], 16) base = 10
else: if match_obj.group("hex"):
char_reference = int(substr[2:-1], 10) base = 16
return chr(char_reference) return chr(int(match_obj.group("content"), base))
return { return MarkupText.unescape_markup_char(match_obj.group("entity"))
"&lt;": "<", return match_obj.group()
"&gt;": ">",
"&amp;": "&",
"&quot;": "\"",
"&apos;": "'"
}.get(substr, substr)
def get_specified_items( @staticmethod
self, cmd_span_pairs: list[tuple[Span, Span]] def get_attr_dict_from_command_pair(
) -> list[tuple[Span, dict[str, str]]]: open_command: re.Match, close_command: re.Match
attr_pattern = r"""(\w+)\s*\=\s*(["'])([\s\S]*?)\2""" ) -> dict[str, str] | None:
internal_items = [] pattern = r"""
for begin_cmd_span, end_cmd_span in cmd_span_pairs: (?P<attr_name>\w+)
begin_tag = self.get_substr(begin_cmd_span) \s*\=\s*
tag_name = re.match(r"<(\w+)", begin_tag).group(1) (?P<quot>["'])(?P<attr_val>.*?)(?P=quot)
if tag_name == "span": """
attr_dict = { tag_name = open_command.group("tag_name")
attr_match_obj.group(1): attr_match_obj.group(3) if tag_name == "span":
for attr_match_obj in re.finditer(attr_pattern, begin_tag) return {
} match_obj.group("attr_name"): match_obj.group("attr_val")
else: for match_obj in re.finditer(
attr_dict = MarkupText.MARKUP_TAGS.get(tag_name, {}) pattern, open_command.group("attr_list"), re.S | re.X
internal_items.append( )
((begin_cmd_span[1], end_cmd_span[0]), attr_dict) }
) return MarkupText.MARKUP_TAGS.get(tag_name, {})
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
return [ return [
*internal_items, *(
*[
(span, {key: val}) (span, {key: val})
for t2x_dict, key in ( for t2x_dict, key in (
(self.t2c, "foreground"), (self.t2c, "foreground"),
@ -308,49 +321,49 @@ class MarkupText(StringMobject):
) )
for selector, val in t2x_dict.items() for selector, val in t2x_dict.items()
for span in self.find_spans_by_selector(selector) for span in self.find_spans_by_selector(selector)
], ),
*[ *(
(span, local_config) (span, local_config)
for selector, local_config in self.local_configs.items() for selector, local_config in self.local_configs.items()
for span in self.find_spans_by_selector(selector) for span in self.find_spans_by_selector(selector)
], )
*[
(span, {})
for span in self.find_spans_by_selector(self.isolate)
]
] ]
@staticmethod @staticmethod
def get_cmd_str_pair( def get_command_string(
attr_dict: dict[str, str], label_hex: str | None attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> tuple[str, str]: ) -> str:
if is_end:
return "</span>"
if label_hex is not None: if label_hex is not None:
converted_attr_dict = {"foreground": label_hex} converted_attr_dict = {"foreground": label_hex}
for key, val in attr_dict.items(): for key, val in attr_dict.items():
substitute_key = MarkupText.MARKUP_COLOR_KEYS.get(key, None) if key in (
if substitute_key is None: "background", "bgcolor",
converted_attr_dict[key] = val "underline_color", "overline_color", "strikethrough_color"
elif substitute_key: ):
converted_attr_dict[key] = "black" converted_attr_dict[key] = "black"
elif key not in ("foreground", "fgcolor", "color"):
converted_attr_dict[key] = val
else: else:
converted_attr_dict = attr_dict.copy() converted_attr_dict = attr_dict.copy()
attrs_str = " ".join([ attrs_str = " ".join([
f"{key}='{val}'" f"{key}='{val}'"
for key, val in converted_attr_dict.items() for key, val in converted_attr_dict.items()
]) ])
return f"<span {attrs_str}>", "</span>" return f"<span {attrs_str}>"
def get_content_prefix_and_suffix( def get_content_prefix_and_suffix(
self, is_labelled: bool self, is_labelled: bool
) -> tuple[str, str]: ) -> tuple[str, str]:
global_attr_dict = { global_attr_dict = {
"foreground": self.base_color_hex, "foreground": self.color_to_hex(self.base_color),
"font_family": self.font, "font_family": self.font,
"font_style": self.slant, "font_style": self.slant,
"font_weight": self.weight, "font_weight": self.weight,
"font_size": str(self.font_size * 1024), "font_size": str(round(self.font_size * 1024)),
} }
global_attr_dict.update(self.global_config)
# `line_height` attribute is supported since Pango 1.50. # `line_height` attribute is supported since Pango 1.50.
pango_version = manimpango.pango_version() pango_version = manimpango.pango_version()
if tuple(map(int, pango_version.split("."))) < (1, 50): if tuple(map(int, pango_version.split("."))) < (1, 50):
@ -365,10 +378,17 @@ class MarkupText(StringMobject):
global_attr_dict["line_height"] = str( global_attr_dict["line_height"] = str(
((line_spacing_scale) + 1) * 0.6 ((line_spacing_scale) + 1) * 0.6
) )
if self.disable_ligatures:
global_attr_dict["font_features"] = "liga=0,dlig=0,clig=0,hlig=0"
return self.get_cmd_str_pair( global_attr_dict.update(self.global_config)
global_attr_dict, return tuple(
label_hex=self.int_to_hex(0) if is_labelled else None self.get_command_string(
global_attr_dict,
is_end=is_end,
label_hex=self.int_to_hex(0) if is_labelled else None
)
for is_end in (False, True)
) )
# Method alias # Method alias
@ -376,8 +396,8 @@ class MarkupText(StringMobject):
def get_parts_by_text(self, selector: Selector) -> VGroup: def get_parts_by_text(self, selector: Selector) -> VGroup:
return self.select_parts(selector) return self.select_parts(selector)
def get_part_by_text(self, selector: Selector) -> VGroup: def get_part_by_text(self, selector: Selector, **kwargs) -> VGroup:
return self.select_part(selector) return self.select_part(selector, **kwargs)
def set_color_by_text(self, selector: Selector, color: ManimColor): def set_color_by_text(self, selector: Selector, color: ManimColor):
return self.set_parts_color(selector, color) return self.set_parts_color(selector, color)
@ -393,9 +413,27 @@ class MarkupText(StringMobject):
class Text(MarkupText): class Text(MarkupText):
CONFIG = { CONFIG = {
"is_markup": False, # For backward compatibility
"isolate": (re.compile(r"\w+", re.U), re.compile(r"\S+", re.U)),
} }
@staticmethod
def get_command_matches(string: str) -> list[re.Match]:
pattern = re.compile(r"""[<>&"']""")
return list(pattern.finditer(string))
@staticmethod
def get_command_flag(match_obj: re.Match) -> int:
return 0
@staticmethod
def replace_for_content(match_obj: re.Match) -> str:
return Text.escape_markup_char(match_obj.group())
@staticmethod
def replace_for_matching(match_obj: re.Match) -> str:
return match_obj.group()
class Code(MarkupText): class Code(MarkupText):
CONFIG = { CONFIG = {

732
manimlib/tex_templates.yml Normal file
View file

@ -0,0 +1,732 @@
# Classical TeX templates
default:
description: ""
compiler: latex
preamble: |-
\usepackage[english]{babel}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{dsfont}
\usepackage{setspace}
\usepackage{tipa}
\usepackage{relsize}
\usepackage{textcomp}
\usepackage{mathrsfs}
\usepackage{calligra}
\usepackage{wasysym}
\usepackage{ragged2e}
\usepackage{physics}
\usepackage{xcolor}
\usepackage{microtype}
\usepackage{pifont}
\DisableLigatures{encoding = *, family = * }
\linespread{1}
ctex:
description: ""
compiler: xelatex
preamble: |-
\usepackage[UTF8]{ctex}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{dsfont}
\usepackage{setspace}
\usepackage{tipa}
\usepackage{relsize}
\usepackage{textcomp}
\usepackage{mathrsfs}
\usepackage{calligra}
\usepackage{wasysym}
\usepackage{ragged2e}
\usepackage{physics}
\usepackage{xcolor}
\usepackage{microtype}
\linespread{1}
# Simplified TeX templates
basic:
description: ""
compiler: latex
preamble: |-
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
basic_ctex:
description: ""
compiler: xelatex
preamble: |-
\usepackage[UTF8]{ctex}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
empty:
description: ""
compiler: latex
preamble: ""
empty_ctex:
description: ""
compiler: xelatex
preamble: ""
# A collection of TeX templates for the fonts described at
# http://jf.burnol.free.fr/showcase.html
american_typewriter:
description: American Typewriter
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{American Typewriter}
\usepackage[defaultmathsizes]{mathastext}
antykwa:
description: Antykwa Poltawskiego (TX Fonts for Greek and math symbols)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[OT4,OT1]{fontenc}
\usepackage{txfonts}
\usepackage[upright]{txgreeks}
\usepackage{antpolt}
\usepackage[defaultmathsizes,nolessnomore]{mathastext}
apple_chancery:
description: Apple Chancery
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Apple Chancery}
\usepackage[defaultmathsizes]{mathastext}
auriocus_kalligraphicus:
description: Auriocus Kalligraphicus (Symbol Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{aurical}
\renewcommand{\rmdefault}{AuriocusKalligraphicus}
\usepackage[symbolgreek]{mathastext}
baskervald_adf_fourier:
description: Baskervald ADF with Fourier
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[upright]{fourier}
\usepackage{baskervald}
\usepackage[defaultmathsizes,noasterisk]{mathastext}
baskerville_it:
description: Baskerville (Italic)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Baskerville}
\usepackage[defaultmathsizes,italic]{mathastext}
biolinum:
description: Biolinum
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{txfonts}
\usepackage[upright]{txgreeks}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Minion Pro}
\setsansfont[Mapping=tex-text,Scale=MatchUppercase]{Myriad Pro}
\renewcommand\familydefault\sfdefault
\usepackage[defaultmathsizes]{mathastext}
\renewcommand\familydefault\rmdefault
brushscriptx:
description: BrushScriptX-Italic (PX math and Greek)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{pxfonts}
\renewcommand{\rmdefault}{pbsi}
\renewcommand{\mddefault}{xl}
\renewcommand{\bfdefault}{xl}
\usepackage[defaultmathsizes,noasterisk]{mathastext}
\boldmath
chalkboard_se:
description: Chalkboard SE
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Chalkboard SE}
\usepackage[defaultmathsizes]{mathastext}
chalkduster:
description: Chalkduster
compiler: lualatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Chalkduster}
\usepackage[defaultmathsizes]{mathastext}
comfortaa:
description: Comfortaa
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[default]{comfortaa}
\usepackage[LGRgreek,defaultmathsizes,noasterisk]{mathastext}
\let\varphi\phi
\linespread{1.06}
comic_sans:
description: Comic Sans MS
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Comic Sans MS}
\usepackage[defaultmathsizes]{mathastext}
droid_sans:
description: Droid Sans
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[default]{droidsans}
\usepackage[LGRgreek]{mathastext}
\let\varepsilon\epsilon
droid_sans_it:
description: Droid Sans (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[default]{droidsans}
\usepackage[LGRgreek,defaultmathsizes,italic]{mathastext}
\let\varphi\phi
droid_serif:
description: Droid Serif
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[default]{droidserif}
\usepackage[LGRgreek]{mathastext}
\let\varepsilon\epsilon
droid_serif_px_it:
description: Droid Serif (PX math symbols) (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{pxfonts}
\usepackage[default]{droidserif}
\usepackage[LGRgreek,defaultmathsizes,italic,basic]{mathastext}
\let\varphi\phi
ecf_augie:
description: ECF Augie (Euler Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\renewcommand\familydefault{fau}
\usepackage[defaultmathsizes,eulergreek]{mathastext}
ecf_jd:
description: ECF JD (with TX fonts)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{txfonts}
\usepackage[upright]{txgreeks}
\renewcommand\familydefault{fjd}
\usepackage{mathastext}
\mathversion{bold}
ecf_skeetch:
description: ECF Skeetch (CM Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\DeclareFontFamily{T1}{fsk}{}
\DeclareFontShape{T1}{fsk}{m}{n}{<->s*[1.315] fskmw8t}{}
\renewcommand\rmdefault{fsk}
\usepackage[noendash,defaultmathsizes,nohbar,defaultimath]{mathastext}
ecf_tall_paul:
description: ECF Tall Paul (with Symbol font)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\DeclareFontFamily{T1}{ftp}{}
\DeclareFontShape{T1}{ftp}{m}{n}{<->s*[1.4] ftpmw8t}{}
\renewcommand\familydefault{ftp}
\usepackage[symbol]{mathastext}
\let\infty\inftypsy
ecf_webster:
description: ECF Webster (with TX fonts)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{txfonts}
\usepackage[upright]{txgreeks}
\renewcommand\familydefault{fwb}
\usepackage{mathastext}
\renewcommand{\int}{\intop\limits}
\linespread{1.5}
\mathversion{bold}
electrum_adf:
description: Electrum ADF (CM Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[LGRgreek,basic,defaultmathsizes]{mathastext}
\usepackage[lf]{electrum}
\Mathastext
\let\varphi\phi
epigrafica:
description: Epigrafica
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[LGR,OT1]{fontenc}
\usepackage{epigrafica}
\usepackage[basic,LGRgreek,defaultmathsizes]{mathastext}
\let\varphi\phi
\linespread{1.2}
fourier_utopia:
description: Fourier Utopia (Fourier upright Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[upright]{fourier}
\usepackage{mathastext}
french_cursive:
description: French Cursive (Euler Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[default]{frcursive}
\usepackage[eulergreek,noplusnominus,noequal,nohbar,nolessnomore,noasterisk]{mathastext}
gfs_bodoni:
description: GFS Bodoni
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\renewcommand{\rmdefault}{bodoni}
\usepackage[LGRgreek]{mathastext}
\let\varphi\phi
\linespread{1.06}
gfs_didot:
description: GFS Didot (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\renewcommand\rmdefault{udidot}
\usepackage[LGRgreek,defaultmathsizes,italic]{mathastext}
\let\varphi\phi
gfs_neohellenic:
description: GFS NeoHellenic
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\renewcommand{\rmdefault}{neohellenic}
\usepackage[LGRgreek]{mathastext}
\let\varphi\phi
\linespread{1.06}
gnu_freesans_tx:
description: GNU FreeSerif (and TX fonts symbols)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\usepackage{txfonts}
\setmainfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSerifBold,ItalicFont=FreeSerifItalic,BoldItalicFont=FreeSerifBoldItalic]{FreeSerif}
\usepackage[defaultmathsizes]{mathastext}
gnu_freeserif_freesans:
description: GNU FreeSerif and FreeSans
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSerifBold,ItalicFont=FreeSerifItalic,BoldItalicFont=FreeSerifBoldItalic]{FreeSerif}
\setsansfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSansBold,ItalicFont=FreeSansOblique,BoldItalicFont=FreeSansBoldOblique,Scale=MatchLowercase]{FreeSans}
\renewcommand{\familydefault}{lmss}
\usepackage[LGRgreek,defaultmathsizes,noasterisk]{mathastext}
\renewcommand{\familydefault}{\sfdefault}
\Mathastext
\let\varphi\phi
\renewcommand{\familydefault}{\rmdefault}
helvetica_fourier_it:
description: Helvetica with Fourier (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[scaled]{helvet}
\usepackage{fourier}
\renewcommand{\rmdefault}{phv}
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
latin_modern_tw:
description: Latin Modern Typewriter Proportional
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[variablett]{lmodern}
\renewcommand{\rmdefault}{\ttdefault}
\usepackage[LGRgreek]{mathastext}
\MTgreekfont{lmtt}
\Mathastext
\let\varepsilon\epsilon
latin_modern_tw_it:
description: Latin Modern Typewriter Proportional (CM Greek) (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[variablett,nomath]{lmodern}
\renewcommand{\familydefault}{\ttdefault}
\usepackage[frenchmath]{mathastext}
\linespread{1.08}
libertine:
description: Libertine
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{libertine}
\usepackage[greek=n]{libgreek}
\usepackage[noasterisk,defaultmathsizes]{mathastext}
libris_adf_fourier:
description: Libris ADF with Fourier
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[upright]{fourier}
\usepackage{libris}
\renewcommand{\familydefault}{\sfdefault}
\usepackage[noasterisk]{mathastext}
minion_pro_myriad_pro:
description: Minion Pro and Myriad Pro (and TX fonts symbols)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[default]{droidserif}
\usepackage[LGRgreek]{mathastext}
\let\varepsilon\epsilon
minion_pro_tx:
description: Minion Pro (and TX fonts symbols)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{txfonts}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Minion Pro}
\usepackage[defaultmathsizes]{mathastext}
new_century_schoolbook:
description: New Century Schoolbook (Symbol Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{newcent}
\usepackage[symbolgreek]{mathastext}
\linespread{1.1}
new_century_schoolbook_px:
description: New Century Schoolbook (Symbol Greek, PX math symbols)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{pxfonts}
\usepackage{newcent}
\usepackage[symbolgreek,defaultmathsizes]{mathastext}
\linespread{1.06}
noteworthy_light:
description: Noteworthy Light
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Noteworthy Light}
\usepackage[defaultmathsizes]{mathastext}
palatino:
description: Palatino (Symbol Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{palatino}
\usepackage[symbolmax,defaultmathsizes]{mathastext}
papyrus:
description: Papyrus
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Papyrus}
\usepackage[defaultmathsizes]{mathastext}
romande_adf_fourier_it:
description: Romande ADF with Fourier (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{fourier}
\usepackage{romande}
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
\renewcommand{\itshape}{\swashstyle}
slitex:
description: SliTeX (Euler Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{tpslifonts}
\usepackage[eulergreek,defaultmathsizes]{mathastext}
\MTEulerScale{1.06}
\linespread{1.2}
times_fourier_it:
description: Times with Fourier (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{fourier}
\renewcommand{\rmdefault}{ptm}
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
urw_avant_garde:
description: URW Avant Garde (Symbol Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{avant}
\renewcommand{\familydefault}{\sfdefault}
\usepackage[symbolgreek,defaultmathsizes]{mathastext}
urw_zapf_chancery:
description: URW Zapf Chancery (CM Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\DeclareFontFamily{T1}{pzc}{}
\DeclareFontShape{T1}{pzc}{mb}{it}{<->s*[1.2] pzcmi8t}{}
\DeclareFontShape{T1}{pzc}{m}{it}{<->ssub * pzc/mb/it}{}
\DeclareFontShape{T1}{pzc}{mb}{sl}{<->ssub * pzc/mb/it}{}
\DeclareFontShape{T1}{pzc}{m}{sl}{<->ssub * pzc/mb/sl}{}
\DeclareFontShape{T1}{pzc}{m}{n}{<->ssub * pzc/mb/it}{}
\usepackage{chancery}
\usepackage{mathastext}
\linespread{1.05}
\boldmath
venturis_adf_fourier_it:
description: Venturis ADF with Fourier (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{fourier}
\usepackage[lf]{venturis}
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
verdana_it:
description: Verdana (Italic)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Verdana}
\usepackage[defaultmathsizes,italic]{mathastext}
vollkorn:
description: Vollkorn (TX fonts for Greek and math symbols)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{txfonts}
\usepackage[upright]{txgreeks}
\usepackage{vollkorn}
\usepackage[defaultmathsizes]{mathastext}
vollkorn_fourier_it:
description: Vollkorn with Fourier (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{fourier}
\usepackage{vollkorn}
\usepackage[italic,nohbar]{mathastext}
zapf_chancery:
description: Zapf Chancery
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\DeclareFontFamily{T1}{pzc}{}
\DeclareFontShape{T1}{pzc}{mb}{it}{<->s*[1.2] pzcmi8t}{}
\DeclareFontShape{T1}{pzc}{m}{it}{<->ssub * pzc/mb/it}{}
\usepackage{chancery}
\renewcommand\shapedefault\itdefault
\renewcommand\bfdefault\mddefault
\usepackage[defaultmathsizes]{mathastext}
\linespread{1.05}

View file

@ -1,25 +0,0 @@
\documentclass[preview]{standalone}
\usepackage[UTF8]{ctex}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{dsfont}
\usepackage{setspace}
\usepackage{tipa}
\usepackage{relsize}
\usepackage{textcomp}
\usepackage{mathrsfs}
\usepackage{calligra}
\usepackage{wasysym}
\usepackage{ragged2e}
\usepackage{physics}
\usepackage{xcolor}
\usepackage{microtype}
\linespread{1}
\begin{document}
[tex_expression]
\end{document}

View file

@ -1,28 +0,0 @@
\documentclass[preview]{standalone}
\usepackage[english]{babel}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{dsfont}
\usepackage{setspace}
\usepackage{tipa}
\usepackage{relsize}
\usepackage{textcomp}
\usepackage{mathrsfs}
\usepackage{calligra}
\usepackage{wasysym}
\usepackage{ragged2e}
\usepackage{physics}
\usepackage{xcolor}
\usepackage{microtype}
\usepackage{pifont}
\DisableLigatures{encoding = *, family = * }
\linespread{1}
\begin{document}
[tex_expression]
\end{document}

View file

@ -42,14 +42,9 @@ def init_customization() -> None:
"sounds": "", "sounds": "",
"temporary_storage": "", "temporary_storage": "",
}, },
"tex": {
"executable": "",
"template_file": "",
"intermediate_filetype": "",
"text_to_replace": "[tex_expression]",
},
"universal_import_line": "from manimlib import *", "universal_import_line": "from manimlib import *",
"style": { "style": {
"tex_template": "",
"font": "Consolas", "font": "Consolas",
"background_color": "", "background_color": "",
}, },
@ -62,7 +57,7 @@ def init_customization() -> None:
"medium": "1280x720", "medium": "1280x720",
"high": "1920x1080", "high": "1920x1080",
"4k": "3840x2160", "4k": "3840x2160",
"default_resolution": "high", "default_resolution": "",
}, },
"fps": 30, "fps": 30,
} }
@ -109,24 +104,14 @@ def init_customization() -> None:
show_default=False show_default=False
) )
console.print("[bold]LaTeX:[/bold]")
tex_config = configuration["tex"]
tex = Prompt.ask(
" Select an executable program to use to compile a LaTeX source file",
choices=["latex", "xelatex"],
default="latex"
)
if tex == "latex":
tex_config["executable"] = "latex"
tex_config["template_file"] = "tex_template.tex"
tex_config["intermediate_filetype"] = "dvi"
else:
tex_config["executable"] = "xelatex -no-pdf"
tex_config["template_file"] = "ctex_template.tex"
tex_config["intermediate_filetype"] = "xdv"
console.print("[bold]Styles:[/bold]") console.print("[bold]Styles:[/bold]")
configuration["style"]["background_color"] = Prompt.ask( style_config = configuration["style"]
tex_template = Prompt.ask(
" Select a TeX template to compile a LaTeX source file",
default="default"
)
style_config["tex_template"] = tex_template
style_config["background_color"] = Prompt.ask(
" Which [bold]background color[/bold] do you want [italic](hex code)", " Which [bold]background color[/bold] do you want [italic](hex code)",
default="#333333" default="#333333"
) )
@ -139,7 +124,7 @@ def init_customization() -> None:
) )
table.add_row("480p15", "720p30", "1080p60", "2160p60") table.add_row("480p15", "720p30", "1080p60", "2160p60")
console.print(table) console.print(table)
configuration["camera_qualities"]["default_quality"] = Prompt.ask( configuration["camera_resolutions"]["default_resolution"] = Prompt.ask(
" Which one to choose as the default rendering quality", " Which one to choose as the default rendering quality",
choices=["low", "medium", "high", "ultra_high"], choices=["low", "medium", "high", "ultra_high"],
default="high" default="high"

View file

@ -1,4 +1,5 @@
from functools import lru_cache from functools import lru_cache
import hashlib
import inspect import inspect
import math import math
@ -76,3 +77,9 @@ def binary_search(function,
else: else:
return None return None
return mh return mh
def hash_string(string):
# Truncating at 16 bytes for cleanliness
hasher = hashlib.sha256(string.encode())
return hasher.hexdigest()[:16]

View file

@ -1,135 +1,152 @@
from __future__ import annotations from __future__ import annotations
from contextlib import contextmanager from contextlib import contextmanager
import hashlib
import os import os
import sys import re
import yaml
from manimlib.config import get_custom_config from manimlib.config import get_custom_config
from manimlib.config import get_manim_dir from manimlib.config import get_manim_dir
from manimlib.logger import log from manimlib.logger import log
from manimlib.utils.directories import get_tex_dir from manimlib.utils.directories import get_tex_dir
from manimlib.utils.simple_functions import hash_string
SAVED_TEX_CONFIG = {} SAVED_TEX_CONFIG = {}
def get_tex_template_config(template_name: str) -> dict[str, str]:
name = template_name.replace(" ", "_").lower()
with open(os.path.join(
get_manim_dir(), "manimlib", "tex_templates.yml"
), encoding="utf-8") as tex_templates_file:
templates_dict = yaml.safe_load(tex_templates_file)
if name not in templates_dict:
log.warning(
"Cannot recognize template '%s', falling back to 'default'.",
name
)
name = "default"
return templates_dict[name]
def get_tex_config() -> dict[str, str]: def get_tex_config() -> dict[str, str]:
""" """
Returns a dict which should look something like this: Returns a dict which should look something like this:
{ {
"executable": "latex", "template": "default",
"template_file": "tex_template.tex", "compiler": "latex",
"intermediate_filetype": "dvi", "preamble": "..."
"text_to_replace": "YourTextHere",
"tex_body": "..."
} }
""" """
# Only load once, then save thereafter # Only load once, then save thereafter
if not SAVED_TEX_CONFIG: if not SAVED_TEX_CONFIG:
custom_config = get_custom_config() template_name = get_custom_config()["style"]["tex_template"]
SAVED_TEX_CONFIG.update(custom_config["tex"]) template_config = get_tex_template_config(template_name)
# Read in template file SAVED_TEX_CONFIG.update({
template_filename = os.path.join( "template": template_name,
get_manim_dir(), "manimlib", "tex_templates", "compiler": template_config["compiler"],
SAVED_TEX_CONFIG["template_file"], "preamble": template_config["preamble"]
) })
with open(template_filename, "r", encoding="utf-8") as file:
SAVED_TEX_CONFIG["tex_body"] = file.read()
return SAVED_TEX_CONFIG return SAVED_TEX_CONFIG
def tex_hash(tex_file_content: str) -> int: def tex_content_to_svg_file(
# Truncating at 16 bytes for cleanliness content: str, template: str, additional_preamble: str
hasher = hashlib.sha256(tex_file_content.encode()) ) -> str:
return hasher.hexdigest()[:16] tex_config = get_tex_config()
if not template or template == tex_config["template"]:
compiler = tex_config["compiler"]
preamble = tex_config["preamble"]
else:
config = get_tex_template_config(template)
compiler = config["compiler"]
preamble = config["preamble"]
if additional_preamble:
preamble += "\n" + additional_preamble
full_tex = "\n\n".join((
"\\documentclass[preview]{standalone}",
preamble,
"\\begin{document}",
content,
"\\end{document}"
)) + "\n"
def tex_to_svg_file(tex_file_content: str) -> str:
svg_file = os.path.join( svg_file = os.path.join(
get_tex_dir(), tex_hash(tex_file_content) + ".svg" get_tex_dir(), hash_string(full_tex) + ".svg"
) )
if not os.path.exists(svg_file): if not os.path.exists(svg_file):
# If svg doesn't exist, create it # If svg doesn't exist, create it
tex_to_svg(tex_file_content, svg_file) create_tex_svg(full_tex, svg_file, compiler)
return svg_file return svg_file
def tex_to_svg(tex_file_content: str, svg_file: str) -> str: def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
tex_file = svg_file.replace(".svg", ".tex") if compiler == "latex":
with open(tex_file, "w", encoding="utf-8") as outfile: program = "latex"
outfile.write(tex_file_content) dvi_ext = ".dvi"
svg_file = dvi_to_svg(tex_to_dvi(tex_file)) elif compiler == "xelatex":
program = "xelatex -no-pdf"
dvi_ext = ".xdv"
else:
raise NotImplementedError(
f"Compiler '{compiler}' is not implemented"
)
# Write tex file
root, _ = os.path.splitext(svg_file)
with open(root + ".tex", "w", encoding="utf-8") as tex_file:
tex_file.write(full_tex)
# tex to dvi
if os.system(" ".join((
program,
"-interaction=batchmode",
"-halt-on-error",
f"-output-directory=\"{os.path.dirname(svg_file)}\"",
f"\"{root}.tex\"",
">",
os.devnull
))):
log.error(
"LaTeX Error! Not a worry, it happens to the best of us."
)
with open(root + ".log", "r", encoding="utf-8") as log_file:
error_match_obj = re.search(r"(?<=\n! ).*", log_file.read())
if error_match_obj:
log.debug(
"The error could be: `%s`",
error_match_obj.group()
)
raise LatexError()
# dvi to svg
os.system(" ".join((
"dvisvgm",
f"\"{root}{dvi_ext}\"",
"-n",
"-v",
"0",
"-o",
f"\"{svg_file}\"",
">",
os.devnull
)))
# Cleanup superfluous documents # Cleanup superfluous documents
tex_dir, name = os.path.split(svg_file) for ext in (".tex", dvi_ext, ".log", ".aux"):
stem, end = name.split(".") try:
for file in filter(lambda s: s.startswith(stem), os.listdir(tex_dir)): os.remove(root + ext)
if not file.endswith(end): except FileNotFoundError:
os.remove(os.path.join(tex_dir, file)) pass
return svg_file
def tex_to_dvi(tex_file: str) -> str:
tex_config = get_tex_config()
program = tex_config["executable"]
file_type = tex_config["intermediate_filetype"]
result = tex_file.replace(".tex", "." + file_type)
if not os.path.exists(result):
commands = [
program,
"-interaction=batchmode",
"-halt-on-error",
f"-output-directory=\"{os.path.dirname(tex_file)}\"",
f"\"{tex_file}\"",
">",
os.devnull
]
exit_code = os.system(" ".join(commands))
if exit_code != 0:
log_file = tex_file.replace(".tex", ".log")
log.error("LaTeX Error! Not a worry, it happens to the best of us.")
error_str = ""
with open(log_file, "r", encoding="utf-8") as file:
for line in file.readlines():
if line.startswith("!"):
error_str = line[2:-1]
log.debug(f"The error could be: `{error_str}`")
raise LatexError(error_str)
return result
def dvi_to_svg(dvi_file: str) -> str:
"""
Converts a dvi, which potentially has multiple slides, into a
directory full of enumerated pngs corresponding with these slides.
Returns a list of PIL Image objects for these images sorted as they
where in the dvi
"""
file_type = get_tex_config()["intermediate_filetype"]
result = dvi_file.replace("." + file_type, ".svg")
if not os.path.exists(result):
commands = [
"dvisvgm",
"\"{}\"".format(dvi_file),
"-n",
"-v",
"0",
"-o",
"\"{}\"".format(result),
">",
os.devnull
]
os.system(" ".join(commands))
return result
# TODO, perhaps this should live elsewhere # TODO, perhaps this should live elsewhere
@contextmanager @contextmanager
def display_during_execution(message: str) -> None: def display_during_execution(message: str):
# Only show top line # Merge into a single line
to_print = message.split("\n")[0] to_print = message.replace("\n", " ")
max_characters = os.get_terminal_size().columns - 1 max_characters = os.get_terminal_size().columns - 1
if len(to_print) > max_characters: if len(to_print) > max_characters:
to_print = to_print[:max_characters - 3] + "..." to_print = to_print[:max_characters - 3] + "..."
@ -140,6 +157,5 @@ def display_during_execution(message: str) -> None:
print(" " * len(to_print), end="\r") print(" " * len(to_print), end="\r")
class LatexError(Exception): class LatexError(Exception):
pass pass

View file

@ -17,7 +17,7 @@ rich
scipy scipy
screeninfo screeninfo
skia-pathops skia-pathops
svgelements svgelements>=1.8.1
sympy sympy
tqdm tqdm
validators validators

View file

@ -48,7 +48,7 @@ install_requires =
scipy scipy
screeninfo screeninfo
skia-pathops skia-pathops
svgelements svgelements>=1.8.1
sympy sympy
tqdm tqdm
validators validators