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
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.movement import Homotopy
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 SMALL_BUFF
from manimlib.constants import TAU

View File

@ -167,83 +167,95 @@ class TransformMatchingStrings(AnimationGroup):
digest_config(self, kwargs)
assert isinstance(source, 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 = []
used = []
for indices_list in indices_lists:
if not indices_list:
continue
if not all(index in rest_indices for index in indices_list):
if not all(
index not in used_indices and index not in used
for index in indices_list
):
continue
result.append(indices_list)
for index in indices_list:
rest_indices.remove(index)
return result
used.extend(indices_list)
return result, used
def add_anims(anim_class, indices_lists_pairs):
for source_indices_lists, target_indices_lists in indices_lists_pairs:
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,
[
anim_class_items = [
(ReplacementTransform, [
(
source.get_submob_indices_lists_by_selector(k),
target.get_submob_indices_lists_by_selector(v)
)
for k, v in self.key_map.items()
]
)
add_anims_from(
FadeTransformPieces,
StringMobject.get_specified_part_items
)
add_anims_from(
FadeTransformPieces,
StringMobject.get_group_part_items
)
]),
(FadeTransformPieces, get_matched_indices_lists(
source.get_specified_part_items(),
target.get_specified_part_items()
)),
(FadeTransformPieces, get_matched_indices_lists(
source.get_group_part_items(),
target.get_group_part_items()
))
]
rest_source = VGroup(*[source[index] for index in source_indices])
rest_target = VGroup(*[target[index] for index in target_indices])
anims = []
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:
anims.append(
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
# specify them elsewhere,
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 *"
style:
tex_template: "default"
font: "Consolas"
text_alignment: "LEFT"
background_color: "#333333"
@ -49,4 +41,4 @@ camera_resolutions:
high: "1920x1080"
4k: "3840x2160"
default_resolution: "high"
fps: 30
fps: 30

View File

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

View File

@ -18,7 +18,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from colour import Color
from typing import Iterable, Sequence, TypeVar, Union
from typing import Callable, Iterable, Union
ManimColor = Union[str, Color]
Span = tuple[int, int]
@ -32,7 +32,6 @@ if TYPE_CHECKING:
tuple[Union[int, None], Union[int, None]]
]]
]
T = TypeVar("T")
class StringMobject(SVGMobject, ABC):
@ -47,7 +46,7 @@ class StringMobject(SVGMobject, ABC):
if they want to do anything with their corresponding submobjects.
`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.
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.
The additional one is generated with some color commands inserted,
@ -64,6 +63,7 @@ class StringMobject(SVGMobject, ABC):
},
"base_color": WHITE,
"isolate": (),
"protect": (),
}
def __init__(self, string: str, **kwargs):
@ -71,9 +71,7 @@ class StringMobject(SVGMobject, ABC):
digest_config(self, kwargs)
if self.base_color is None:
self.base_color = WHITE
self.base_color_hex = self.color_to_hex(self.base_color)
self.full_span = (0, len(self.string))
self.parse()
super().__init__(**kwargs)
self.labels = [submob.label for submob in self.submobjects]
@ -90,9 +88,9 @@ class StringMobject(SVGMobject, ABC):
super().generate_mobject()
labels_count = len(self.labelled_spans)
if not labels_count:
if labels_count == 1:
for submob in self.submobjects:
submob.label = -1
submob.label = 0
return
labelled_content = self.get_content(is_labelled=True)
@ -104,7 +102,7 @@ class StringMobject(SVGMobject, ABC):
"to the original svg. Skip the labelling process."
)
for submob in self.submobjects:
submob.label = -1
submob.label = 0
return
self.rearrange_submobjects_by_positions(labelled_svg)
@ -112,18 +110,21 @@ class StringMobject(SVGMobject, ABC):
for submob, labelled_svg_submob in zip(
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()
))
if color_int > labels_count:
unrecognizable_colors.append(color_int)
color_int = 0
submob.label = color_int - 1
if label >= labels_count:
unrecognizable_colors.append(label)
label = 0
submob.label = label
if unrecognizable_colors:
log.warning(
"Unrecognizable color labels detected (%s, etc). "
"Unrecognizable color labels detected (%s). "
"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(
@ -153,30 +154,27 @@ class StringMobject(SVGMobject, ABC):
# 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_single_selector(sel):
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):
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(
isinstance(index, int) or index is None
for index in sel
):
l = self.full_span[1]
l = len(self.string)
span = tuple(
default_index if index is None else
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 None
@ -189,57 +187,12 @@ class StringMobject(SVGMobject, ABC):
if spans is None:
raise TypeError(f"Invalid selector: '{sel}'")
result.extend(spans)
return 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))
return list(filter(lambda span: span[0] <= span[1], result))
@staticmethod
def span_contains(span_0: Span, span_1: Span) -> bool:
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
def color_to_hex(color: ManimColor) -> str:
return rgb_to_hex(color_to_rgb(color))
@ -255,131 +208,220 @@ class StringMobject(SVGMobject, ABC):
# Parsing
def parse(self) -> None:
cmd_spans = self.get_cmd_spans()
cmd_substrs = [self.get_substr(span) for span in cmd_spans]
flags = [self.get_substr_flag(substr) for substr in cmd_substrs]
specified_items = self.get_specified_items(
self.get_cmd_span_pairs(cmd_spans, flags)
)
split_items = [
(span, attr_dict)
for specified_span, attr_dict in specified_items
for span in self.split_span_by_levels(
specified_span, cmd_spans, flags
def get_substr(span: Span) -> str:
return self.string[slice(*span)]
configured_items = self.get_configured_items()
isolated_spans = self.find_spans_by_selector(self.isolate)
protected_spans = self.find_spans_by_selector(self.protect)
command_matches = self.get_command_matches(self.string)
def get_key(category, i, flag):
def get_span_by_category(category, i):
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]
self.split_items = split_items
self.labelled_spans = [span for span, _ in split_items]
self.cmd_repl_items_for_content = [
(span, self.get_repl_substr_for_content(substr))
for span, substr in zip(cmd_spans, cmd_substrs)
]
self.cmd_repl_items_for_matching = [
(span, self.get_repl_substr_for_matching(substr))
for span, substr in zip(cmd_spans, cmd_substrs)
]
self.check_overlapping()
index_items = sorted([
(category, i, flag)
for category, item_length in enumerate((
len(configured_items),
len(isolated_spans),
len(protected_spans),
len(command_matches)
))
for i in range(item_length)
for flag in (1, -1)
], key=lambda t: get_key(*t))
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
def get_cmd_spans(self) -> list[Span]:
def get_command_matches(string: str) -> list[re.Match]:
return []
@staticmethod
@abstractmethod
def get_substr_flag(self, substr: str) -> int:
def get_command_flag(match_obj: re.Match) -> int:
return 0
@staticmethod
@abstractmethod
def get_repl_substr_for_content(self, substr: str) -> str:
return ""
@abstractmethod
def get_repl_substr_for_matching(self, substr: str) -> str:
def replace_for_content(match_obj: re.Match) -> str:
return ""
@staticmethod
def get_cmd_span_pairs(
cmd_spans: list[Span], flags: list[int]
) -> list[tuple[Span, Span]]:
result = []
begin_cmd_spans_stack = []
for cmd_span, flag in zip(cmd_spans, flags):
if flag == 1:
begin_cmd_spans_stack.append(cmd_span)
elif flag == -1:
if not begin_cmd_spans_stack:
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
def replace_for_matching(match_obj: re.Match) -> str:
return ""
@staticmethod
@abstractmethod
def get_attr_dict_from_command_pair(
open_command: re.Match, close_command: re.Match,
) -> dict[str, str] | None:
return None
@abstractmethod
def get_specified_items(
self, cmd_span_pairs: list[tuple[Span, Span]]
) -> list[tuple[Span, dict[str, str]]]:
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
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
@abstractmethod
def get_cmd_str_pair(
attr_dict: dict[str, str], label_hex: str | None
) -> tuple[str, str]:
return "", ""
def get_command_string(
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> str:
return ""
@abstractmethod
def get_content_prefix_and_suffix(
@ -387,38 +429,6 @@ class StringMobject(SVGMobject, ABC):
) -> tuple[str, str]:
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
def get_submob_indices_list_by_span(
@ -427,59 +437,69 @@ class StringMobject(SVGMobject, ABC):
return [
submob_index
for submob_index, label in enumerate(self.labels)
if label != -1 and self.span_contains(
arbitrary_span, self.labelled_spans[label]
)
if self.span_contains(arbitrary_span, self.labelled_spans[label])
]
def get_specified_part_items(self) -> list[tuple[str, list[int]]]:
return [
(
self.get_substr(span),
self.string[slice(*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]]]:
if not self.labels:
return []
group_labels, labelled_submob_ranges = zip(
*self.compress_neighbours(self.labels)
)
ordered_spans = [
self.labelled_spans[label] if label != -1 else self.full_span
for label in group_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
)
]
def get_neighbouring_pairs(vals):
return list(zip(vals[:-1], vals[1:]))
range_lens, group_labels = zip(*(
(len(list(grouper)), val)
for val, grouper in it.groupby(self.labels)
))
submob_indices_lists = [
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))
@ -497,13 +517,13 @@ class StringMobject(SVGMobject, ABC):
def build_parts_from_indices_lists(
self, indices_lists: list[list[int]]
) -> VGroup:
return VGroup(*[
VGroup(*[
return VGroup(*(
VGroup(*(
self.submobjects[submob_index]
for submob_index in indices_list
])
))
for indices_list in indices_lists
])
))
def build_groups(self) -> VGroup:
return self.build_parts_from_indices_lists([

View File

@ -1,6 +1,5 @@
from __future__ import annotations
import hashlib
import os
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.images import get_full_vector_image_path
from manimlib.utils.iterables import hash_obj
from manimlib.utils.simple_functions import hash_string
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
@ -106,7 +106,7 @@ class SVGMobject(VMobject):
return get_full_vector_image_path(self.file_name)
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 = (
"fill",
"fill-opacity",
@ -116,14 +116,17 @@ class SVGMobject(VMobject):
"style"
)
root = element_tree.getroot()
root_style_dict = {
k: v for k, v in root.attrib.items()
style_attrs = {
k: v
for k, v in root.attrib.items()
if k in style_keys
}
new_root = ET.Element("svg", {})
config_style_node = ET.SubElement(new_root, "g", config_style_dict)
root_style_node = ET.SubElement(config_style_node, "g", root_style_dict)
# Ignore other attributes in case that svgelements cannot parse them
SVG_XMLNS = "{http://www.w3.org/2000/svg}"
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)
return ET.ElementTree(new_root)
@ -147,7 +150,7 @@ class SVGMobject(VMobject):
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
result = []
for shape in svg.elements():
if isinstance(shape, se.Group):
if isinstance(shape, (se.Group, se.Use)):
continue
elif isinstance(shape, se.Path):
mob = self.path_to_mobject(shape)
@ -155,9 +158,7 @@ class SVGMobject(VMobject):
mob = self.line_to_mobject(shape)
elif isinstance(shape, se.Rect):
mob = self.rect_to_mobject(shape)
elif isinstance(shape, se.Circle):
mob = self.circle_to_mobject(shape)
elif isinstance(shape, se.Ellipse):
elif isinstance(shape, (se.Circle, se.Ellipse)):
mob = self.ellipse_to_mobject(shape)
elif isinstance(shape, se.Polygon):
mob = self.polygon_to_mobject(shape)
@ -168,11 +169,12 @@ class SVGMobject(VMobject):
elif type(shape) == se.SVGElement:
continue
else:
log.warning(f"Unsupported element type: {type(shape)}")
log.warning("Unsupported element type: %s", type(shape))
continue
if not mob.has_points():
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:
self.handle_transform(mob, shape.transform)
result.append(mob)
@ -203,21 +205,10 @@ class SVGMobject(VMobject):
)
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:
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(
start=_convert_point_to_3d(line.x1, line.y1),
end=_convert_point_to_3d(line.x2, line.y2)
@ -242,15 +233,7 @@ class SVGMobject(VMobject):
))
return mob
def circle_to_mobject(self, circle: se.Circle) -> 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:
def ellipse_to_mobject(self, ellipse: se.Circle | se.Ellipse) -> Circle:
mob = Circle(radius=ellipse.rx)
mob.stretch_to_fit_height(2 * ellipse.ry)
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
# don't need to retrace the same computation.
path_string = self.path_obj.d()
hasher = hashlib.sha256(path_string.encode())
path_hash = hasher.hexdigest()[:16]
path_hash = hash_string(path_string)
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")

View File

@ -13,8 +13,7 @@ from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
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 get_tex_config
from manimlib.utils.tex_file_writing import tex_to_svg_file
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
from typing import TYPE_CHECKING
@ -44,6 +43,8 @@ class SingleStringTex(SVGMobject):
"alignment": "\\centering",
"math_mode": True,
"organize_left_to_right": False,
"template": "",
"additional_preamble": "",
}
def __init__(self, tex_string: str, **kwargs):
@ -64,27 +65,24 @@ class SingleStringTex(SVGMobject):
self.path_string_config,
self.tex_string,
self.alignment,
self.math_mode
self.math_mode,
self.template,
self.additional_preamble
)
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}\""):
file_path = tex_to_svg_file(full_tex)
file_path = tex_content_to_svg_file(
content, self.template, self.additional_preamble
)
return file_path
def get_tex_file_body(self, tex_string: str) -> str:
new_tex = self.get_modified_expression(tex_string)
if self.math_mode:
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
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
)
return self.alignment + "\n" + new_tex
def get_modified_expression(self, tex_string: str) -> str:
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.directories import get_downloads_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
@ -63,7 +63,6 @@ class _Alignment:
class MarkupText(StringMobject):
CONFIG = {
"is_markup": True,
"font_size": 48,
"lsh": None,
"justify": False,
@ -81,21 +80,11 @@ class MarkupText(StringMobject):
"t2w": {},
"global_config": {},
"local_configs": {},
# For backward compatibility
"isolate": (re.compile(r"[a-zA-Z]+"), re.compile(r"\S+")),
"disable_ligatures": True,
"isolate": re.compile(r"\w+", re.U),
}
# 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 = {
"b": {"font_weight": "bold"},
"big": {"font_size": "larger"},
@ -107,17 +96,24 @@ class MarkupText(StringMobject):
"tt": {"font_family": "monospace"},
"u": {"underline": "single"},
}
MARKUP_ENTITY_DICT = {
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
"\"": "&quot;",
"'": "&apos;"
}
def __init__(self, text: str, **kwargs):
self.full2short(kwargs)
digest_config(self, kwargs)
if not isinstance(self, Text):
self.validate_markup_string(text)
if not self.font:
self.font = get_customization()["style"]["font"]
if not self.alignment:
self.alignment = get_customization()["style"]["text_alignment"]
if self.is_markup:
self.validate_markup_string(text)
self.text = text
super().__init__(text, **kwargs)
@ -140,8 +136,8 @@ class MarkupText(StringMobject):
self.path_string_config,
self.base_color,
self.isolate,
self.protect,
self.text,
self.is_markup,
self.font_size,
self.lsh,
self.justify,
@ -156,7 +152,8 @@ class MarkupText(StringMobject):
self.t2s,
self.t2w,
self.global_config,
self.local_configs
self.local_configs,
self.disable_ligatures
)
def full2short(self, config: dict) -> None:
@ -182,7 +179,7 @@ class MarkupText(StringMobject):
self.line_width
))
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):
self.markup_to_svg(content, svg_file)
@ -229,76 +226,92 @@ class MarkupText(StringMobject):
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
def get_cmd_spans(self) -> list[Span]:
if not self.is_markup:
return self.find_spans(r"""[<>&"']""")
@staticmethod
def get_command_matches(string: str) -> list[re.Match]:
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:
# "<?...?>", "<!--...-->", "<![CDATA[...]]>", "<!DOCTYPE...>"
# See https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/gmarkup.c
return self.find_spans(
r"""&[\s\S]*?;|[>"']|</?\w+(?:\s*\w+\s*\=\s*(["'])[\s\S]*?\1)*/?>"""
)
def get_substr_flag(self, substr: str) -> int:
if re.fullmatch(r"<\w[\s\S]*[^/]>", substr):
return 1
if substr.startswith("</"):
return -1
@staticmethod
def get_command_flag(match_obj: re.Match) -> int:
if match_obj.group("tag"):
if match_obj.group("close_slash"):
return -1
if not match_obj.group("elision_slash"):
return 1
return 0
def get_repl_substr_for_content(self, substr: str) -> str:
if substr.startswith("<") and substr.endswith(">"):
@staticmethod
def replace_for_content(match_obj: re.Match) -> str:
if match_obj.group("tag"):
return ""
return {
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
"\"": "&quot;",
"'": "&apos;"
}.get(substr, substr)
if match_obj.group("char"):
return MarkupText.escape_markup_char(match_obj.group("char"))
return match_obj.group()
def get_repl_substr_for_matching(self, substr: str) -> str:
if substr.startswith("<") and substr.endswith(">"):
@staticmethod
def replace_for_matching(match_obj: re.Match) -> str:
if match_obj.group("tag") or match_obj.group("passthrough"):
return ""
if substr.startswith("&#") and substr.endswith(";"):
if substr.startswith("&#x"):
char_reference = int(substr[3:-1], 16)
else:
char_reference = int(substr[2:-1], 10)
return chr(char_reference)
return {
"&lt;": "<",
"&gt;": ">",
"&amp;": "&",
"&quot;": "\"",
"&apos;": "'"
}.get(substr, substr)
if match_obj.group("entity"):
if match_obj.group("unicode"):
base = 10
if match_obj.group("hex"):
base = 16
return chr(int(match_obj.group("content"), base))
return MarkupText.unescape_markup_char(match_obj.group("entity"))
return match_obj.group()
def get_specified_items(
self, cmd_span_pairs: list[tuple[Span, Span]]
) -> list[tuple[Span, dict[str, str]]]:
attr_pattern = r"""(\w+)\s*\=\s*(["'])([\s\S]*?)\2"""
internal_items = []
for begin_cmd_span, end_cmd_span in cmd_span_pairs:
begin_tag = self.get_substr(begin_cmd_span)
tag_name = re.match(r"<(\w+)", begin_tag).group(1)
if tag_name == "span":
attr_dict = {
attr_match_obj.group(1): attr_match_obj.group(3)
for attr_match_obj in re.finditer(attr_pattern, begin_tag)
}
else:
attr_dict = MarkupText.MARKUP_TAGS.get(tag_name, {})
internal_items.append(
((begin_cmd_span[1], end_cmd_span[0]), attr_dict)
)
@staticmethod
def get_attr_dict_from_command_pair(
open_command: re.Match, close_command: re.Match
) -> dict[str, str] | None:
pattern = r"""
(?P<attr_name>\w+)
\s*\=\s*
(?P<quot>["'])(?P<attr_val>.*?)(?P=quot)
"""
tag_name = open_command.group("tag_name")
if tag_name == "span":
return {
match_obj.group("attr_name"): match_obj.group("attr_val")
for match_obj in re.finditer(
pattern, open_command.group("attr_list"), re.S | re.X
)
}
return MarkupText.MARKUP_TAGS.get(tag_name, {})
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
return [
*internal_items,
*[
*(
(span, {key: val})
for t2x_dict, key in (
(self.t2c, "foreground"),
@ -308,49 +321,49 @@ class MarkupText(StringMobject):
)
for selector, val in t2x_dict.items()
for span in self.find_spans_by_selector(selector)
],
*[
),
*(
(span, local_config)
for selector, local_config in self.local_configs.items()
for span in self.find_spans_by_selector(selector)
],
*[
(span, {})
for span in self.find_spans_by_selector(self.isolate)
]
)
]
@staticmethod
def get_cmd_str_pair(
attr_dict: dict[str, str], label_hex: str | None
) -> tuple[str, str]:
def get_command_string(
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> str:
if is_end:
return "</span>"
if label_hex is not None:
converted_attr_dict = {"foreground": label_hex}
for key, val in attr_dict.items():
substitute_key = MarkupText.MARKUP_COLOR_KEYS.get(key, None)
if substitute_key is None:
converted_attr_dict[key] = val
elif substitute_key:
if key in (
"background", "bgcolor",
"underline_color", "overline_color", "strikethrough_color"
):
converted_attr_dict[key] = "black"
elif key not in ("foreground", "fgcolor", "color"):
converted_attr_dict[key] = val
else:
converted_attr_dict = attr_dict.copy()
attrs_str = " ".join([
f"{key}='{val}'"
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(
self, is_labelled: bool
) -> tuple[str, str]:
global_attr_dict = {
"foreground": self.base_color_hex,
"foreground": self.color_to_hex(self.base_color),
"font_family": self.font,
"font_style": self.slant,
"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.
pango_version = manimpango.pango_version()
if tuple(map(int, pango_version.split("."))) < (1, 50):
@ -365,10 +378,17 @@ class MarkupText(StringMobject):
global_attr_dict["line_height"] = str(
((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,
label_hex=self.int_to_hex(0) if is_labelled else None
global_attr_dict.update(self.global_config)
return tuple(
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
@ -376,8 +396,8 @@ class MarkupText(StringMobject):
def get_parts_by_text(self, selector: Selector) -> VGroup:
return self.select_parts(selector)
def get_part_by_text(self, selector: Selector) -> VGroup:
return self.select_part(selector)
def get_part_by_text(self, selector: Selector, **kwargs) -> VGroup:
return self.select_part(selector, **kwargs)
def set_color_by_text(self, selector: Selector, color: ManimColor):
return self.set_parts_color(selector, color)
@ -393,9 +413,27 @@ class MarkupText(StringMobject):
class Text(MarkupText):
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):
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": "",
"temporary_storage": "",
},
"tex": {
"executable": "",
"template_file": "",
"intermediate_filetype": "",
"text_to_replace": "[tex_expression]",
},
"universal_import_line": "from manimlib import *",
"style": {
"tex_template": "",
"font": "Consolas",
"background_color": "",
},
@ -62,7 +57,7 @@ def init_customization() -> None:
"medium": "1280x720",
"high": "1920x1080",
"4k": "3840x2160",
"default_resolution": "high",
"default_resolution": "",
},
"fps": 30,
}
@ -109,24 +104,14 @@ def init_customization() -> None:
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]")
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)",
default="#333333"
)
@ -139,7 +124,7 @@ def init_customization() -> None:
)
table.add_row("480p15", "720p30", "1080p60", "2160p60")
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",
choices=["low", "medium", "high", "ultra_high"],
default="high"
@ -161,7 +146,7 @@ def init_customization() -> None:
file_name = os.path.join(os.getcwd(), "custom_config.yml")
with open(file_name, "w", encoding="utf-8") as f:
yaml.dump(configuration, f)
console.print(f"\n:rocket: You have successfully set up a {scope} configuration file!")
console.print(f"You can manually modify it in: [cyan]`{file_name}`[/cyan]")

View File

@ -1,4 +1,5 @@
from functools import lru_cache
import hashlib
import inspect
import math
@ -76,3 +77,9 @@ def binary_search(function,
else:
return None
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 contextlib import contextmanager
import hashlib
import os
import sys
import re
import yaml
from manimlib.config import get_custom_config
from manimlib.config import get_manim_dir
from manimlib.logger import log
from manimlib.utils.directories import get_tex_dir
from manimlib.utils.simple_functions import hash_string
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]:
"""
Returns a dict which should look something like this:
{
"executable": "latex",
"template_file": "tex_template.tex",
"intermediate_filetype": "dvi",
"text_to_replace": "YourTextHere",
"tex_body": "..."
"template": "default",
"compiler": "latex",
"preamble": "..."
}
"""
# Only load once, then save thereafter
if not SAVED_TEX_CONFIG:
custom_config = get_custom_config()
SAVED_TEX_CONFIG.update(custom_config["tex"])
# Read in template file
template_filename = os.path.join(
get_manim_dir(), "manimlib", "tex_templates",
SAVED_TEX_CONFIG["template_file"],
)
with open(template_filename, "r", encoding="utf-8") as file:
SAVED_TEX_CONFIG["tex_body"] = file.read()
template_name = get_custom_config()["style"]["tex_template"]
template_config = get_tex_template_config(template_name)
SAVED_TEX_CONFIG.update({
"template": template_name,
"compiler": template_config["compiler"],
"preamble": template_config["preamble"]
})
return SAVED_TEX_CONFIG
def tex_hash(tex_file_content: str) -> int:
# Truncating at 16 bytes for cleanliness
hasher = hashlib.sha256(tex_file_content.encode())
return hasher.hexdigest()[:16]
def tex_content_to_svg_file(
content: str, template: str, additional_preamble: str
) -> str:
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(
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 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
def tex_to_svg(tex_file_content: str, svg_file: str) -> str:
tex_file = svg_file.replace(".svg", ".tex")
with open(tex_file, "w", encoding="utf-8") as outfile:
outfile.write(tex_file_content)
svg_file = dvi_to_svg(tex_to_dvi(tex_file))
def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
if compiler == "latex":
program = "latex"
dvi_ext = ".dvi"
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
tex_dir, name = os.path.split(svg_file)
stem, end = name.split(".")
for file in filter(lambda s: s.startswith(stem), os.listdir(tex_dir)):
if not file.endswith(end):
os.remove(os.path.join(tex_dir, file))
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
for ext in (".tex", dvi_ext, ".log", ".aux"):
try:
os.remove(root + ext)
except FileNotFoundError:
pass
# TODO, perhaps this should live elsewhere
@contextmanager
def display_during_execution(message: str) -> None:
# Only show top line
to_print = message.split("\n")[0]
def display_during_execution(message: str):
# Merge into a single line
to_print = message.replace("\n", " ")
max_characters = os.get_terminal_size().columns - 1
if len(to_print) > max_characters:
to_print = to_print[:max_characters - 3] + "..."
@ -140,6 +157,5 @@ def display_during_execution(message: str) -> None:
print(" " * len(to_print), end="\r")
class LatexError(Exception):
pass

View File

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

View File

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