From d5ab9a91c4de06b43234ce39deb1f179148473bf Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Thu, 31 Mar 2022 16:15:58 +0800 Subject: [PATCH] Reorganize files --- manimlib/__init__.py | 1 + .../animation/transform_matching_parts.py | 20 +- manimlib/mobject/svg/labelled_string.py | 417 ++---------------- manimlib/mobject/svg/mtex_mobject.py | 338 ++++++++++++++ manimlib/mobject/svg/text_mobject.py | 40 +- 5 files changed, 409 insertions(+), 407 deletions(-) create mode 100644 manimlib/mobject/svg/mtex_mobject.py diff --git a/manimlib/__init__.py b/manimlib/__init__.py index 954a56d7..a0147cf7 100644 --- a/manimlib/__init__.py +++ b/manimlib/__init__.py @@ -38,6 +38,7 @@ from manimlib.mobject.shape_matchers import * from manimlib.mobject.svg.brace import * from manimlib.mobject.svg.drawings import * from manimlib.mobject.svg.labelled_string import * +from manimlib.mobject.svg.mtex_mobject import * from manimlib.mobject.svg.svg_mobject import * from manimlib.mobject.svg.tex_mobject import * from manimlib.mobject.svg.text_mobject import * diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index 2b452d2d..f824663d 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -199,7 +199,7 @@ class TransformMatchingStrings(AnimationGroup): filtered_source_indices_lists, filtered_target_indices_lists ]): - return + continue anims.append(anim_class(source_parts, target_parts, **kwargs)) for index in it.chain(*filtered_source_indices_lists): rest_source_indices.remove(index) @@ -207,12 +207,10 @@ class TransformMatchingStrings(AnimationGroup): rest_target_indices.remove(index) def get_common_substrs(func): - result = sorted(list( - set(func(source_mobject)).intersection(func(target_mobject)) - ), key=len, reverse=True) - if "" in result: - result.remove("") - return result + return sorted([ + substr for substr in func(source_mobject) + if substr and substr in func(target_mobject) + ], key=len, reverse=True) def get_parts_from_keys(mobject, keys): if not isinstance(keys, tuple): @@ -241,16 +239,12 @@ class TransformMatchingStrings(AnimationGroup): add_anims_from( FadeTransformPieces, LabelledString.get_parts_by_string, - get_common_substrs( - lambda mobject: mobject.specified_substrings - ) + get_common_substrs(LabelledString.get_specified_substrs) ) add_anims_from( FadeTransformPieces, LabelledString.get_parts_by_group_substr, - get_common_substrs( - lambda mobject: mobject.group_substrs - ) + get_common_substrs(LabelledString.get_group_substrs) ) fade_source = VGroup(*[ diff --git a/manimlib/mobject/svg/labelled_string.py b/manimlib/mobject/svg/labelled_string.py index 41fe4225..8f89ab2b 100644 --- a/manimlib/mobject/svg/labelled_string.py +++ b/manimlib/mobject/svg/labelled_string.py @@ -13,10 +13,6 @@ from manimlib.utils.color import color_to_int_rgb from manimlib.utils.config_ops import digest_config from manimlib.utils.iterables import adjacent_pairs from manimlib.utils.iterables import remove_list_redundancies -from manimlib.utils.tex_file_writing import tex_to_svg_file -from manimlib.utils.tex_file_writing import get_tex_config -from manimlib.utils.tex_file_writing import display_during_execution -from manimlib.logger import log from typing import TYPE_CHECKING @@ -27,9 +23,6 @@ if TYPE_CHECKING: Span = tuple[int, int] -SCALE_FACTOR_PER_FONT_POINT = 0.001 - - class _StringSVG(SVGMobject): CONFIG = { "height": None, @@ -82,8 +75,6 @@ class LabelledString(_StringSVG): self.set_fill(self.base_color) for submob, label in zip(self.submobjects, submob_labels): submob.label = label - self.submob_labels = submob_labels - self.post_parse() def pre_parse(self) -> None: self.full_span = self.get_full_span() @@ -98,12 +89,8 @@ class LabelledString(_StringSVG): self.external_specified_spans = self.get_external_specified_spans() self.specified_spans = self.get_specified_spans() self.label_span_list = self.get_label_span_list() - self.has_predefined_colors = self.get_has_predefined_colors() - - def post_parse(self) -> None: self.containing_labels_dict = self.get_containing_labels_dict() - self.specified_substrings = self.get_specified_substrings() - self.group_substrs = self.get_group_substrs() + self.has_predefined_colors = self.get_has_predefined_colors() # Toolkits @@ -113,6 +100,12 @@ class LabelledString(_StringSVG): for match_obj in re.finditer(pattern, self.string) ] + def find_substr(self, *substrs: str) -> list[Span]: + return list(it.chain(*[ + self.find_spans(re.escape(substr)) if substr else [] + for substr in remove_list_redundancies(substrs) + ])) + @staticmethod def get_neighbouring_pairs(iterable: Iterable) -> list: return list(adjacent_pairs(iterable))[:-1] @@ -290,22 +283,16 @@ class LabelledString(_StringSVG): def get_internal_specified_spans(self) -> list[Span]: return [] + @abstractmethod def get_external_specified_spans(self) -> list[Span]: - if "" in self.isolate: - return self.get_neighbouring_pairs( - list(range(len(self.string) + 1)) - ) - - return remove_list_redundancies(list(it.chain(*[ - self.find_spans(re.escape(substr)) - for substr in self.isolate - ]))) + return [] def get_specified_spans(self) -> list[Span]: spans = [ self.full_span, *self.internal_specified_spans, - *self.external_specified_spans + *self.external_specified_spans, + *self.find_substr(*self.isolate) ] shrinked_spans = list(filter( lambda span: span[0] < span[1], @@ -320,6 +307,29 @@ class LabelledString(_StringSVG): def get_label_span_list(self) -> list[Span]: return [] + def get_containing_labels_dict(self) -> dict[Span, list[int]]: + label_span_list = self.label_span_list + result = { + span: [] + for span in label_span_list + } + for span_0 in label_span_list: + for span_index, span_1 in enumerate(label_span_list): + if self.span_contains(span_0, span_1): + result[span_0].append(span_index) + elif span_0[0] < span_1[0] < span_0[1] < span_1[1]: + string_0, string_1 = [ + self.string[slice(*span)] + for span in [span_0, span_1] + ] + raise ValueError( + "Partially overlapping substrings detected: " + f"'{string_0}' and '{string_1}'" + ) + if self.full_span not in result: + result[self.full_span] = list(range(len(label_span_list))) + return result + @abstractmethod def get_inserted_string_pairs( self, use_plain_file: bool @@ -355,29 +365,6 @@ class LabelledString(_StringSVG): # Post-parsing - def get_containing_labels_dict(self) -> dict[Span, list[int]]: - label_span_list = self.label_span_list - result = { - span: [] - for span in label_span_list - } - for span_0 in label_span_list: - for span_index, span_1 in enumerate(label_span_list): - if self.span_contains(span_0, span_1): - result[span_0].append(span_index) - elif span_0[0] < span_1[0] < span_0[1] < span_1[1]: - string_0, string_1 = [ - self.string[slice(*span)] - for span in [span_0, span_1] - ] - raise ValueError( - "Partially overlapping substrings detected: " - f"'{string_0}' and '{string_1}'" - ) - if self.full_span not in result: - result[self.full_span] = list(range(len(label_span_list))) - return result - def get_cleaned_substr(self, span: Span) -> str: span_repl_dict = { tuple([index - span[0] for index in cmd_span]): "" @@ -388,18 +375,17 @@ class LabelledString(_StringSVG): self.string[slice(*span)], span_repl_dict ) - def get_specified_substrings(self) -> list[str]: + def get_specified_substrs(self) -> list[str]: return remove_list_redundancies([ self.get_cleaned_substr(span) for span in self.specified_spans ]) def get_group_span_items(self) -> tuple[list[int], list[Span]]: - if not self.submob_labels: + submob_labels = [submob.label for submob in self.submobjects] + if not submob_labels: return [], [] - return tuple(zip( - *self.compress_neighbours(self.submob_labels) - )) + return tuple(zip(*self.compress_neighbours(submob_labels))) def get_group_substrs(self) -> list[str]: group_labels, _ = self.get_group_span_items() @@ -494,14 +480,14 @@ class LabelledString(_StringSVG): return VGroup() return VGroup(*[ self.get_parts_by_custom_span(span) - for span in self.find_spans(re.escape(substr)) + for span in self.find_substr(substr) ]) def get_parts_by_group_substr(self, substr: str) -> VGroup: return VGroup(*[ group for group, group_substr in zip( - self.get_submob_groups(), self.group_substrs + self.get_submob_groups(), self.get_group_substrs() ) if group_substr == substr ]) @@ -530,326 +516,3 @@ class LabelledString(_StringSVG): def get_string(self) -> str: return self.string - - -class MTex(LabelledString): - CONFIG = { - "font_size": 48, - "alignment": "\\centering", - "tex_environment": "align*", - "tex_to_color_map": {}, - } - - def __init__(self, tex_string: str, **kwargs): - digest_config(self, kwargs) - # Prevent from passing an empty string. - if not tex_string: - tex_string = "\\quad" - self.tex_string = tex_string - self.isolate.extend(self.tex_to_color_map.keys()) - super().__init__(tex_string, **kwargs) - - self.set_color_by_tex_to_color_map(self.tex_to_color_map) - self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size) - - @property - def hash_seed(self) -> tuple: - return ( - self.__class__.__name__, - self.svg_default, - self.path_string_config, - self.base_color, - self.use_plain_file, - self.isolate, - self.tex_string, - self.alignment, - self.tex_environment, - self.tex_to_color_map - ) - - def get_file_path_by_content(self, content: str) -> str: - full_tex = self.get_tex_file_body(content) - with display_during_execution(f"Writing \"{self.string}\""): - file_path = self.tex_to_svg_file_path(full_tex) - return file_path - - def get_tex_file_body(self, content: str) -> str: - if self.tex_environment: - content = "\n".join([ - f"\\begin{{{self.tex_environment}}}", - content, - f"\\end{{{self.tex_environment}}}" - ]) - if self.alignment: - content = "\n".join([self.alignment, content]) - - tex_config = get_tex_config() - return tex_config["tex_body"].replace( - tex_config["text_to_replace"], - content - ) - - @staticmethod - def tex_to_svg_file_path(tex_file_content: str) -> str: - return tex_to_svg_file(tex_file_content) - - def pre_parse(self) -> None: - super().pre_parse() - self.backslash_indices = self.get_backslash_indices() - self.brace_index_pairs = self.get_brace_index_pairs() - self.script_char_spans = self.get_script_char_spans() - self.script_content_spans = self.get_script_content_spans() - self.script_spans = self.get_script_spans() - - # Toolkits - - @staticmethod - def get_begin_color_command_str(rgb_int: int) -> str: - rgb_tuple = MTex.int_to_rgb(rgb_int) - return "".join([ - "{{", - "\\color[RGB]", - "{", - ",".join(map(str, rgb_tuple)), - "}" - ]) - - @staticmethod - def get_end_color_command_str() -> str: - return "}}" - - # Pre-parsing - - def get_backslash_indices(self) -> list[int]: - # Newlines (`\\`) don't count. - return [ - span[1] - 1 - for span in self.find_spans(r"\\+") - if (span[1] - span[0]) % 2 == 1 - ] - - def get_unescaped_char_indices(self, *chars: str): - return sorted(filter( - lambda index: index - 1 not in self.backslash_indices, - [ - span[0] - for char in chars - for span in self.find_spans(re.escape(char)) - ] - )) - - def get_brace_index_pairs(self) -> list[Span]: - string = self.string - indices = self.get_unescaped_char_indices("{", "}") - left_brace_indices = [] - right_brace_indices = [] - left_brace_indices_stack = [] - for index in indices: - if string[index] == "{": - left_brace_indices_stack.append(index) - else: - if not left_brace_indices_stack: - raise ValueError("Missing '{' inserted") - left_brace_index = left_brace_indices_stack.pop() - left_brace_indices.append(left_brace_index) - right_brace_indices.append(index) - if left_brace_indices_stack: - raise ValueError("Missing '}' inserted") - return list(zip(left_brace_indices, right_brace_indices)) - - def get_script_char_spans(self) -> list[int]: - return [ - (index, index + 1) - for index in self.get_unescaped_char_indices("_", "^") - ] - - def get_script_content_spans(self) -> list[Span]: - result = [] - brace_indices_dict = dict(self.brace_index_pairs) - for script_char_span in self.script_char_spans: - span_begin = self.rslide(script_char_span[1], self.space_spans) - if span_begin in brace_indices_dict.keys(): - span_end = brace_indices_dict[span_begin] + 1 - else: - pattern = re.compile(r"[a-zA-Z0-9]|\\[a-zA-Z]+") - match_obj = pattern.match(self.string, pos=span_begin) - if not match_obj: - script_name = { - "_": "subscript", - "^": "superscript" - }[script_char] - raise ValueError( - f"Unclear {script_name} detected while parsing. " - "Please use braces to clarify" - ) - span_end = match_obj.end() - result.append((span_begin, span_end)) - return result - - def get_script_spans(self) -> list[Span]: - return [ - ( - self.lslide(script_char_span[0], self.space_spans), - script_content_span[1] - ) - for script_char_span, script_content_span in zip( - self.script_char_spans, self.script_content_spans - ) - ] - - # Parsing - - def get_command_repl_items(self) -> list[tuple[Span, str]]: - color_related_command_dict = { - "color": (1, False), - "textcolor": (1, False), - "pagecolor": (1, True), - "colorbox": (1, True), - "fcolorbox": (2, True), - } - result = [] - backslash_indices = self.backslash_indices - right_brace_indices = [ - right_index - for left_index, right_index in self.brace_index_pairs - ] - pattern = "".join([ - r"\\", - "(", - "|".join(color_related_command_dict.keys()), - ")", - r"(?![a-zA-Z])" - ]) - for match_obj in re.finditer(pattern, self.string): - span_begin, cmd_end = match_obj.span() - if span_begin not in backslash_indices: - continue - cmd_name = match_obj.group(1) - n_braces, substitute_cmd = color_related_command_dict[cmd_name] - span_end = self.take_nearest_value( - right_brace_indices, cmd_end, n_braces - ) + 1 - if substitute_cmd: - repl_str = "\\" + cmd_name + n_braces * "{white}" - else: - repl_str = "" - result.append(((span_begin, span_end), repl_str)) - return result - - def get_ignored_spans(self) -> list[int]: - return self.script_char_spans.copy() - - def get_internal_specified_spans(self) -> list[Span]: - # Match paired double braces (`{{...}}`). - result = [] - reversed_brace_indices_dict = dict([ - pair[::-1] for pair in self.brace_index_pairs - ]) - skip = False - for prev_right_index, right_index in self.get_neighbouring_pairs( - list(reversed_brace_indices_dict.keys()) - ): - if skip: - skip = False - continue - if right_index != prev_right_index + 1: - continue - left_index = reversed_brace_indices_dict[right_index] - prev_left_index = reversed_brace_indices_dict[prev_right_index] - if left_index != prev_left_index - 1: - continue - result.append((left_index, right_index + 1)) - skip = True - return result - - def get_label_span_list(self) -> list[Span]: - result = self.script_content_spans.copy() - for span_begin, span_end in self.specified_spans: - shrinked_end = self.lslide(span_end, self.script_spans) - if span_begin >= shrinked_end: - continue - shrinked_span = (span_begin, shrinked_end) - if shrinked_span in result: - continue - result.append(shrinked_span) - return result - - def get_inserted_string_pairs( - self, use_plain_file: bool - ) -> list[tuple[Span, tuple[str, str]]]: - if use_plain_file: - return [] - - extended_label_span_list = [ - span - if span in self.script_content_spans - else (span[0], self.rslide(span[1], self.script_spans)) - for span in self.label_span_list - ] - return [ - (span, ( - self.get_begin_color_command_str(label), - self.get_end_color_command_str() - )) - for label, span in enumerate(extended_label_span_list) - ] - - def get_other_repl_items( - self, use_plain_file: bool - ) -> list[tuple[Span, str]]: - if use_plain_file: - return [] - return self.command_repl_items.copy() - - def get_has_predefined_colors(self) -> bool: - return bool(self.command_repl_items) - - # Post-parsing - - def get_cleaned_substr(self, span: Span) -> str: - substr = super().get_cleaned_substr(span) - if not self.brace_index_pairs: - return substr - - # Balance braces. - left_brace_indices, right_brace_indices = zip(*self.brace_index_pairs) - unclosed_left_braces = 0 - unclosed_right_braces = 0 - for index in range(*span): - if index in left_brace_indices: - unclosed_left_braces += 1 - elif index in right_brace_indices: - if unclosed_left_braces == 0: - unclosed_right_braces += 1 - else: - unclosed_left_braces -= 1 - return "".join([ - unclosed_right_braces * "{", - substr, - unclosed_left_braces * "}" - ]) - - # Method alias - - def get_parts_by_tex(self, tex: str) -> VGroup: - return self.get_parts_by_string(tex) - - def get_part_by_tex(self, tex: str) -> VMobject: - return self.get_part_by_string(tex) - - def set_color_by_tex(self, tex: str, color: ManimColor): - return self.set_color_by_string(tex, color) - - def set_color_by_tex_to_color_map( - self, tex_to_color_map: dict[str, ManimColor] - ): - return self.set_color_by_string_to_color_map(tex_to_color_map) - - def get_tex(self) -> str: - return self.get_string() - - -class MTexText(MTex): - CONFIG = { - "tex_environment": None, - } diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py new file mode 100644 index 00000000..a79c8f7c --- /dev/null +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -0,0 +1,338 @@ +from __future__ import annotations + +import re +import colour +from typing import Union, Sequence + +from manimlib.mobject.svg.labelled_string import LabelledString +from manimlib.utils.tex_file_writing import tex_to_svg_file +from manimlib.utils.tex_file_writing import get_tex_config +from manimlib.utils.tex_file_writing import display_during_execution + + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from manimlib.mobject.types.vectorized_mobject import VMobject + from manimlib.mobject.types.vectorized_mobject import VGroup + ManimColor = Union[str, colour.Color, Sequence[float]] + Span = tuple[int, int] + + +SCALE_FACTOR_PER_FONT_POINT = 0.001 + + +class MTex(LabelledString): + CONFIG = { + "font_size": 48, + "alignment": "\\centering", + "tex_environment": "align*", + "tex_to_color_map": {}, + } + + def __init__(self, tex_string: str, **kwargs): + # Prevent from passing an empty string. + if not tex_string: + tex_string = "\\quad" + self.tex_string = tex_string + super().__init__(tex_string, **kwargs) + + self.set_color_by_tex_to_color_map(self.tex_to_color_map) + self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size) + + @property + def hash_seed(self) -> tuple: + return ( + self.__class__.__name__, + self.svg_default, + self.path_string_config, + self.base_color, + self.use_plain_file, + self.isolate, + self.tex_string, + self.alignment, + self.tex_environment, + self.tex_to_color_map + ) + + def get_file_path_by_content(self, content: str) -> str: + full_tex = self.get_tex_file_body(content) + with display_during_execution(f"Writing \"{self.string}\""): + file_path = self.tex_to_svg_file_path(full_tex) + return file_path + + def get_tex_file_body(self, content: str) -> str: + if self.tex_environment: + content = "\n".join([ + f"\\begin{{{self.tex_environment}}}", + content, + f"\\end{{{self.tex_environment}}}" + ]) + if self.alignment: + content = "\n".join([self.alignment, content]) + + tex_config = get_tex_config() + return tex_config["tex_body"].replace( + tex_config["text_to_replace"], + content + ) + + @staticmethod + def tex_to_svg_file_path(tex_file_content: str) -> str: + return tex_to_svg_file(tex_file_content) + + def pre_parse(self) -> None: + super().pre_parse() + self.backslash_indices = self.get_backslash_indices() + self.brace_index_pairs = self.get_brace_index_pairs() + self.script_char_spans = self.get_script_char_spans() + self.script_content_spans = self.get_script_content_spans() + self.script_spans = self.get_script_spans() + + # Toolkits + + @staticmethod + def get_begin_color_command_str(rgb_int: int) -> str: + rgb_tuple = MTex.int_to_rgb(rgb_int) + return "".join([ + "{{", + "\\color[RGB]", + "{", + ",".join(map(str, rgb_tuple)), + "}" + ]) + + @staticmethod + def get_end_color_command_str() -> str: + return "}}" + + # Pre-parsing + + def get_backslash_indices(self) -> list[int]: + # Newlines (`\\`) don't count. + return [ + span[1] - 1 + for span in self.find_spans(r"\\+") + if (span[1] - span[0]) % 2 == 1 + ] + + def get_unescaped_char_spans(self, *chars: str): + return sorted(filter( + lambda span: span[0] - 1 not in self.backslash_indices, + self.find_substr(*chars) + )) + + def get_brace_index_pairs(self) -> list[Span]: + string = self.string + left_brace_indices = [] + right_brace_indices = [] + left_brace_indices_stack = [] + for index, _ in self.get_unescaped_char_spans("{", "}"): + if string[index] == "{": + left_brace_indices_stack.append(index) + else: + if not left_brace_indices_stack: + raise ValueError("Missing '{' inserted") + left_brace_index = left_brace_indices_stack.pop() + left_brace_indices.append(left_brace_index) + right_brace_indices.append(index) + if left_brace_indices_stack: + raise ValueError("Missing '}' inserted") + return list(zip(left_brace_indices, right_brace_indices)) + + def get_script_char_spans(self) -> list[int]: + return self.get_unescaped_char_spans("_", "^") + + def get_script_content_spans(self) -> list[Span]: + result = [] + brace_indices_dict = dict(self.brace_index_pairs) + for script_char_span in self.script_char_spans: + span_begin = self.rslide(script_char_span[1], self.space_spans) + if span_begin in brace_indices_dict.keys(): + span_end = brace_indices_dict[span_begin] + 1 + else: + pattern = re.compile(r"[a-zA-Z0-9]|\\[a-zA-Z]+") + match_obj = pattern.match(self.string, pos=span_begin) + if not match_obj: + script_name = { + "_": "subscript", + "^": "superscript" + }[script_char] + raise ValueError( + f"Unclear {script_name} detected while parsing. " + "Please use braces to clarify" + ) + span_end = match_obj.end() + result.append((span_begin, span_end)) + return result + + def get_script_spans(self) -> list[Span]: + return [ + ( + self.lslide(script_char_span[0], self.space_spans), + script_content_span[1] + ) + for script_char_span, script_content_span in zip( + self.script_char_spans, self.script_content_spans + ) + ] + + # Parsing + + def get_command_repl_items(self) -> list[tuple[Span, str]]: + color_related_command_dict = { + "color": (1, False), + "textcolor": (1, False), + "pagecolor": (1, True), + "colorbox": (1, True), + "fcolorbox": (2, True), + } + result = [] + backslash_indices = self.backslash_indices + right_brace_indices = [ + right_index + for left_index, right_index in self.brace_index_pairs + ] + pattern = "".join([ + r"\\", + "(", + "|".join(color_related_command_dict.keys()), + ")", + r"(?![a-zA-Z])" + ]) + for match_obj in re.finditer(pattern, self.string): + span_begin, cmd_end = match_obj.span() + if span_begin not in backslash_indices: + continue + cmd_name = match_obj.group(1) + n_braces, substitute_cmd = color_related_command_dict[cmd_name] + span_end = self.take_nearest_value( + right_brace_indices, cmd_end, n_braces + ) + 1 + if substitute_cmd: + repl_str = "\\" + cmd_name + n_braces * "{white}" + else: + repl_str = "" + result.append(((span_begin, span_end), repl_str)) + return result + + def get_ignored_spans(self) -> list[int]: + return self.script_char_spans.copy() + + def get_internal_specified_spans(self) -> list[Span]: + # Match paired double braces (`{{...}}`). + result = [] + reversed_brace_indices_dict = dict([ + pair[::-1] for pair in self.brace_index_pairs + ]) + skip = False + for prev_right_index, right_index in self.get_neighbouring_pairs( + list(reversed_brace_indices_dict.keys()) + ): + if skip: + skip = False + continue + if right_index != prev_right_index + 1: + continue + left_index = reversed_brace_indices_dict[right_index] + prev_left_index = reversed_brace_indices_dict[prev_right_index] + if left_index != prev_left_index - 1: + continue + result.append((left_index, right_index + 1)) + skip = True + return result + + def get_external_specified_spans(self) -> list[Span]: + return self.find_substr(*self.tex_to_color_map.keys()) + + def get_label_span_list(self) -> list[Span]: + result = self.script_content_spans.copy() + for span_begin, span_end in self.specified_spans: + shrinked_end = self.lslide(span_end, self.script_spans) + if span_begin >= shrinked_end: + continue + shrinked_span = (span_begin, shrinked_end) + if shrinked_span in result: + continue + result.append(shrinked_span) + return result + + def get_inserted_string_pairs( + self, use_plain_file: bool + ) -> list[tuple[Span, tuple[str, str]]]: + if use_plain_file: + return [] + + extended_label_span_list = [ + span + if span in self.script_content_spans + else (span[0], self.rslide(span[1], self.script_spans)) + for span in self.label_span_list + ] + return [ + (span, ( + self.get_begin_color_command_str(label), + self.get_end_color_command_str() + )) + for label, span in enumerate(extended_label_span_list) + ] + + def get_other_repl_items( + self, use_plain_file: bool + ) -> list[tuple[Span, str]]: + if use_plain_file: + return [] + return self.command_repl_items.copy() + + def get_has_predefined_colors(self) -> bool: + return bool(self.command_repl_items) + + # Post-parsing + + def get_cleaned_substr(self, span: Span) -> str: + substr = super().get_cleaned_substr(span) + if not self.brace_index_pairs: + return substr + + # Balance braces. + left_brace_indices, right_brace_indices = zip(*self.brace_index_pairs) + unclosed_left_braces = 0 + unclosed_right_braces = 0 + for index in range(*span): + if index in left_brace_indices: + unclosed_left_braces += 1 + elif index in right_brace_indices: + if unclosed_left_braces == 0: + unclosed_right_braces += 1 + else: + unclosed_left_braces -= 1 + return "".join([ + unclosed_right_braces * "{", + substr, + unclosed_left_braces * "}" + ]) + + # Method alias + + def get_parts_by_tex(self, tex: str) -> VGroup: + return self.get_parts_by_string(tex) + + def get_part_by_tex(self, tex: str) -> VMobject: + return self.get_part_by_string(tex) + + def set_color_by_tex(self, tex: str, color: ManimColor): + return self.set_color_by_string(tex, color) + + def set_color_by_tex_to_color_map( + self, tex_to_color_map: dict[str, ManimColor] + ): + return self.set_color_by_string_to_color_map(tex_to_color_map) + + def get_tex(self) -> str: + return self.get_string() + + +class MTexText(MTex): + CONFIG = { + "tex_environment": None, + } diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index 2b6211d8..56944209 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -304,19 +304,14 @@ class MarkupText(LabelledString): region_indices[flag] += 1 if flag == 0: region_indices[1] += 1 + if not key: + continue for attr_dict in attr_dict_list[slice(*region_indices)]: attr_dict[key] = value return list(zip( MarkupText.get_neighbouring_pairs(index_seq), attr_dict_list[:-1] )) - def find_spans_by_word_or_span( - self, word_or_span: str | Span - ) -> list[Span]: - if isinstance(word_or_span, tuple): - return [word_or_span] - return self.find_spans(re.escape(word_or_span)) - # Pre-parsing def get_global_items_from_config(self) -> list[str, str]: @@ -408,28 +403,28 @@ class MarkupText(LabelledString): def get_local_items_from_config(self) -> list[tuple[Span, str, str]]: result = [ - (text_span, key, val) + (span, key, val) for t2x_dict, key in ( (self.t2c, "foreground"), (self.t2f, "font_family"), (self.t2s, "font_style"), (self.t2w, "font_weight") ) - for word_or_span, val in t2x_dict.items() - for text_span in self.find_spans_by_word_or_span(word_or_span) + for substr, val in t2x_dict.items() + for span in self.find_substr(substr) ] + [ - (text_span, key, val) - for word_or_span, local_config in self.local_configs.items() - for text_span in self.find_spans_by_word_or_span(word_or_span) + (span, key, val) + for substr, local_config in self.local_configs.items() + for span in self.find_substr(substr) for key, val in local_config.items() ] return [ ( - text_span, + span, self.convert_attr_key(key), self.convert_attr_val(val) ) - for text_span, key, val in result + for span, key, val in result ] def get_predefined_items(self) -> list[Span, str, str]: @@ -458,7 +453,7 @@ class MarkupText(LabelledString): (">", ">"), ("<", "<") ) - for span in self.find_spans(re.escape(char)) + for span in self.find_substr(char) ] return result @@ -468,6 +463,12 @@ class MarkupText(LabelledString): for markup_span, _, _ in self.local_items_from_markup ] + def get_external_specified_spans(self) -> list[Span]: + return [ + markup_span + for markup_span, _, _ in self.local_items_from_config + ] + def get_label_span_list(self) -> list[Span]: breakup_indices = remove_list_redundancies(list(it.chain(*it.chain( self.find_spans(r"\s+"), @@ -492,7 +493,7 @@ class MarkupText(LabelledString): def get_inserted_string_pairs( self, use_plain_file: bool ) -> list[tuple[Span, tuple[str, str]]]: - attr_items = self.predefined_items + attr_items = self.predefined_items.copy() if not use_plain_file: attr_items = [ (span, key, WHITE if key in COLOR_RELATED_KEYS else val) @@ -501,6 +502,11 @@ class MarkupText(LabelledString): (span, "foreground", self.rgb_int_to_hex(label)) for label, span in enumerate(self.label_span_list) ] + else: + attr_items += [ + (span, "", "") + for span in self.label_span_list + ] return [ (span, ( self.get_begin_tag_str(attr_dict),