Construct LabelledString

This commit is contained in:
YishiMichael 2022-03-28 17:55:50 +08:00
parent 305c6e6ee9
commit 473aaea399
No known key found for this signature in database
GPG key ID: EC615C0C5A86BC80
3 changed files with 921 additions and 726 deletions

View file

@ -12,7 +12,7 @@ from manimlib.animation.transform import ReplacementTransform
from manimlib.animation.transform import Transform
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.mobject import Group
from manimlib.mobject.svg.mtex_mobject import MTex
from manimlib.mobject.svg.mtex_mobject import LabelledString
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.config_ops import digest_config
@ -153,15 +153,16 @@ class TransformMatchingTex(TransformMatchingParts):
return mobject.get_tex()
class TransformMatchingMTex(AnimationGroup):
class TransformMatchingString(AnimationGroup):
CONFIG = {
"key_map": dict(),
"transform_mismatches_class": None,
}
def __init__(self, source_mobject: MTex, target_mobject: MTex, **kwargs):
def __init__(self, source_mobject: LabelledString, target_mobject: LabelledString, **kwargs):
digest_config(self, kwargs)
assert isinstance(source_mobject, MTex)
assert isinstance(target_mobject, MTex)
assert isinstance(source_mobject, LabelledString)
assert isinstance(target_mobject, LabelledString)
anims = []
rest_source_submobs = source_mobject.submobjects.copy()
rest_target_submobs = target_mobject.submobjects.copy()
@ -207,7 +208,7 @@ class TransformMatchingMTex(AnimationGroup):
elif isinstance(key, range):
indices.extend(key)
elif isinstance(key, str):
all_parts = mobject.get_parts_by_tex(key)
all_parts = mobject.get_parts_by_string(key)
indices.extend(it.chain(*[
mobject.indices_of_part(part) for part in all_parts
]))
@ -228,31 +229,34 @@ class TransformMatchingMTex(AnimationGroup):
target_mobject.get_specified_substrings()
)
), key=len, reverse=True)
for part_tex_string in common_specified_substrings:
for part_string in common_specified_substrings:
add_anim_from(
FadeTransformPieces, MTex.get_parts_by_tex, part_tex_string
FadeTransformPieces, LabelledString.get_parts_by_string, part_string
)
common_submob_tex_strings = {
source_submob.get_tex() for source_submob in source_mobject
common_submob_strings = {
source_submob.get_string() for source_submob in source_mobject
}.intersection({
target_submob.get_tex() for target_submob in target_mobject
target_submob.get_string() for target_submob in target_mobject
})
for tex_string in common_submob_tex_strings:
for substr in common_submob_strings:
add_anim_from(
FadeTransformPieces,
lambda mobject, attr: VGroup(*[
VGroup(mob) for mob in mobject
if mob.get_tex() == attr
if mob.get_string() == attr
]),
tex_string
substr
)
anims.append(FadeOutToPoint(
VGroup(*rest_source_submobs), target_mobject.get_center(), **kwargs
))
anims.append(FadeInFromPoint(
VGroup(*rest_target_submobs), source_mobject.get_center(), **kwargs
))
if self.transform_mismatches_class is not None:
anims.append(self.transform_mismatches_class(fade_source, fade_target, **kwargs))
else:
anims.append(FadeOutToPoint(
VGroup(*rest_source_submobs), target_mobject.get_center(), **kwargs
))
anims.append(FadeInFromPoint(
VGroup(*rest_target_submobs), source_mobject.get_center(), **kwargs
))
super().__init__(*anims)

File diff suppressed because it is too large Load diff

View file

@ -2,11 +2,11 @@ from __future__ import annotations
import os
import re
import typing
from pathlib import Path
import itertools as it
import xml.sax.saxutils as saxutils
from pathlib import Path
from contextlib import contextmanager
import typing
from typing import Iterable, Sequence, Union
import pygments
@ -17,198 +17,87 @@ from manimpango import MarkupUtils
from manimlib.logger import log
from manimlib.constants import *
from manimlib.mobject.geometry import Dot
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.svg.mtex_mobject import LabelledString
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.customization import get_customization
from manimlib.utils.tex_file_writing import tex_hash
from manimlib.utils.config_ops import digest_config
from manimlib.utils.directories import get_downloads_dir
from manimlib.utils.directories import get_text_dir
from manimlib.utils.iterables import remove_list_redundancies
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.mobject.types.vectorized_mobject import VMobject
ManimColor = Union[str, colour.Color, Sequence[float]]
Span = tuple[int, int]
TEXT_MOB_SCALE_FACTOR = 0.0076
DEFAULT_LINE_SPACING_SCALE = 0.6
class _TextParser(object):
# See https://docs.gtk.org/Pango/pango_markup.html
# A tag containing two aliases will cause warning,
# so only use the first key of each group of aliases.
SPAN_ATTR_KEY_ALIAS_LIST = (
("font", "font_desc"),
("font_family", "face"),
("font_size", "size"),
("font_style", "style"),
("font_weight", "weight"),
("font_variant", "variant"),
("font_stretch", "stretch"),
("font_features",),
("foreground", "fgcolor", "color"),
("background", "bgcolor"),
("alpha", "fgalpha"),
("background_alpha", "bgalpha"),
("underline",),
("underline_color",),
("overline",),
("overline_color",),
("rise",),
("baseline_shift",),
("font_scale",),
("strikethrough",),
("strikethrough_color",),
("fallback",),
("lang",),
("letter_spacing",),
("gravity",),
("gravity_hint",),
("show",),
("insert_hyphens",),
("allow_breaks",),
("line_height",),
("text_transform",),
("segment",),
)
SPAN_ATTR_KEY_CONVERSION = {
key: key_alias_list[0]
for key_alias_list in SPAN_ATTR_KEY_ALIAS_LIST
for key in key_alias_list
}
TAG_TO_ATTR_DICT = {
"b": {"font_weight": "bold"},
"big": {"font_size": "larger"},
"i": {"font_style": "italic"},
"s": {"strikethrough": "true"},
"sub": {"baseline_shift": "subscript", "font_scale": "subscript"},
"sup": {"baseline_shift": "superscript", "font_scale": "superscript"},
"small": {"font_size": "smaller"},
"tt": {"font_family": "monospace"},
"u": {"underline": "single"},
}
def __init__(self, text: str = "", is_markup: bool = True):
self.text = text
self.is_markup = is_markup
self.global_attrs = {}
self.local_attrs = {(0, len(self.text)): {}}
self.tag_strings = set()
if is_markup:
self.parse_markup()
def parse_markup(self) -> None:
tag_pattern = r"""<(/?)(\w+)\s*((\w+\s*\=\s*('[^']*'|"[^"]*")\s*)*)>"""
attr_pattern = r"""(\w+)\s*\=\s*(?:(?:'([^']*)')|(?:"([^"]*)"))"""
start_match_obj_stack = []
match_obj_pairs = []
for match_obj in re.finditer(tag_pattern, self.text):
if not match_obj.group(1):
start_match_obj_stack.append(match_obj)
else:
match_obj_pairs.append((start_match_obj_stack.pop(), match_obj))
self.tag_strings.add(match_obj.group())
assert not start_match_obj_stack, "Unclosed tag(s) detected"
for start_match_obj, end_match_obj in match_obj_pairs:
tag_name = start_match_obj.group(2)
assert tag_name == end_match_obj.group(2), "Unmatched tag names"
assert not end_match_obj.group(3), "Attributes shan't exist in ending tags"
if tag_name == "span":
attr_dict = {
match.group(1): match.group(2) or match.group(3)
for match in re.finditer(attr_pattern, start_match_obj.group(3))
}
elif tag_name in _TextParser.TAG_TO_ATTR_DICT.keys():
assert not start_match_obj.group(3), f"Attributes shan't exist in tag '{tag_name}'"
attr_dict = _TextParser.TAG_TO_ATTR_DICT[tag_name]
else:
raise AssertionError(f"Unknown tag: '{tag_name}'")
text_span = (start_match_obj.end(), end_match_obj.start())
self.update_local_attrs(text_span, attr_dict)
@staticmethod
def convert_key_alias(key: str) -> str:
return _TextParser.SPAN_ATTR_KEY_CONVERSION[key]
@staticmethod
def update_attr_dict(attr_dict: dict[str, str], key: str, value: typing.Any) -> None:
converted_key = _TextParser.convert_key_alias(key)
attr_dict[converted_key] = str(value)
def update_global_attr(self, key: str, value: typing.Any) -> None:
_TextParser.update_attr_dict(self.global_attrs, key, value)
def update_global_attrs(self, attr_dict: dict[str, typing.Any]) -> None:
for key, value in attr_dict.items():
self.update_global_attr(key, value)
def update_local_attr(self, span: tuple[int, int], key: str, value: typing.Any) -> None:
if span[0] >= span[1]:
log.warning(f"Span {span} doesn't match any part of the string")
return
if span in self.local_attrs.keys():
_TextParser.update_attr_dict(self.local_attrs[span], key, value)
return
span_triplets = []
for sp, attr_dict in self.local_attrs.items():
if sp[1] <= span[0] or span[1] <= sp[0]:
continue
span_to_become = (max(sp[0], span[0]), min(sp[1], span[1]))
spans_to_add = []
if sp[0] < span[0]:
spans_to_add.append((sp[0], span[0]))
if span[1] < sp[1]:
spans_to_add.append((span[1], sp[1]))
span_triplets.append((sp, span_to_become, spans_to_add))
for span_to_remove, span_to_become, spans_to_add in span_triplets:
attr_dict = self.local_attrs.pop(span_to_remove)
for span_to_add in spans_to_add:
self.local_attrs[span_to_add] = attr_dict.copy()
self.local_attrs[span_to_become] = attr_dict
_TextParser.update_attr_dict(self.local_attrs[span_to_become], key, value)
def update_local_attrs(self, text_span: tuple[int, int], attr_dict: dict[str, typing.Any]) -> None:
for key, value in attr_dict.items():
self.update_local_attr(text_span, key, value)
def remove_tags(self, string: str) -> str:
for tag_string in self.tag_strings:
string = string.replace(tag_string, "")
return string
def get_text_pieces(self) -> list[tuple[str, dict[str, str]]]:
result = []
for span in sorted(self.local_attrs.keys()):
text_piece = self.remove_tags(self.text[slice(*span)])
if not text_piece:
continue
if not self.is_markup:
text_piece = saxutils.escape(text_piece)
attr_dict = self.global_attrs.copy()
attr_dict.update(self.local_attrs[span])
result.append((text_piece, attr_dict))
return result
def get_markup_str_with_attrs(self) -> str:
return "".join([
f"<span {_TextParser.get_attr_dict_str(attr_dict)}>{text_piece}</span>"
for text_piece, attr_dict in self.get_text_pieces()
])
@staticmethod
def get_attr_dict_str(attr_dict: dict[str, str]) -> str:
return " ".join([
f"{key}='{value}'"
for key, value in attr_dict.items()
])
# See https://docs.gtk.org/Pango/pango_markup.html
# A tag containing two aliases will cause warning,
# so only use the first key of each group of aliases.
SPAN_ATTR_KEY_ALIAS_LIST = (
("font", "font_desc"),
("font_family", "face"),
("font_size", "size"),
("font_style", "style"),
("font_weight", "weight"),
("font_variant", "variant"),
("font_stretch", "stretch"),
("font_features",),
("foreground", "fgcolor", "color"),
("background", "bgcolor"),
("alpha", "fgalpha"),
("background_alpha", "bgalpha"),
("underline",),
("underline_color",),
("overline",),
("overline_color",),
("rise",),
("baseline_shift",),
("font_scale",),
("strikethrough",),
("strikethrough_color",),
("fallback",),
("lang",),
("letter_spacing",),
("gravity",),
("gravity_hint",),
("show",),
("insert_hyphens",),
("allow_breaks",),
("line_height",),
("text_transform",),
("segment",),
)
COLOR_RELATED_KEYS = (
"foreground",
"background",
"underline_color",
"overline_color",
"strikethrough_color"
)
SPAN_ATTR_KEY_CONVERSION = {
key: key_alias_list[0]
for key_alias_list in SPAN_ATTR_KEY_ALIAS_LIST
for key in key_alias_list
}
TAG_TO_ATTR_DICT = {
"b": {"font_weight": "bold"},
"big": {"font_size": "larger"},
"i": {"font_style": "italic"},
"s": {"strikethrough": "true"},
"sub": {"baseline_shift": "subscript", "font_scale": "subscript"},
"sup": {"baseline_shift": "superscript", "font_scale": "superscript"},
"small": {"font_size": "smaller"},
"tt": {"font_family": "monospace"},
"u": {"underline": "single"},
}
# Temporary handler
@ -223,16 +112,9 @@ class _Alignment:
self.value = _Alignment.VAL_DICT[s.upper()]
class Text(SVGMobject):
class MarkupText(LabelledString):
CONFIG = {
# Mobject
"stroke_width": 0,
"svg_default": {
"color": WHITE,
},
"height": None,
# Text
"is_markup": False,
"is_markup": True,
"font_size": 48,
"lsh": None,
"justify": False,
@ -240,8 +122,6 @@ class Text(SVGMobject):
"alignment": "LEFT",
"line_width_factor": None,
"font": "",
"disable_ligatures": True,
"apply_space_chars": True,
"slant": NORMAL,
"weight": NORMAL,
"gradient": None,
@ -252,6 +132,7 @@ class Text(SVGMobject):
"t2w": {},
"global_config": {},
"local_configs": {},
"isolate": [],
}
def __init__(self, text: str, **kwargs):
@ -260,10 +141,15 @@ class Text(SVGMobject):
validate_error = MarkupUtils.validate(text)
if validate_error:
raise ValueError(validate_error)
self.text = text
self.parser = _TextParser(text, is_markup=self.is_markup)
super().__init__(**kwargs)
self.text = text
super().__init__(text, **kwargs)
if self.t2g:
log.warning(
"Manim currently cannot parse gradient from svg. "
"Please set gradient via `set_color_by_gradient`.",
)
if self.gradient:
self.set_color_by_gradient(*self.gradient)
if self.height is None:
@ -284,8 +170,6 @@ class Text(SVGMobject):
self.alignment,
self.line_width_factor,
self.font,
self.disable_ligatures,
self.apply_space_chars,
self.slant,
self.weight,
self.t2c,
@ -293,71 +177,32 @@ class Text(SVGMobject):
self.t2s,
self.t2w,
self.global_config,
self.local_configs
self.local_configs,
self.isolate
)
def get_file_path(self) -> str:
full_markup = self.get_full_markup_str()
def full2short(self, config: dict) -> None:
conversion_dict = {
"line_spacing_height": "lsh",
"text2color": "t2c",
"text2font": "t2f",
"text2gradient": "t2g",
"text2slant": "t2s",
"text2weight": "t2w"
}
for kwargs in [config, self.CONFIG]:
for long_name, short_name in conversion_dict.items():
if long_name in kwargs:
kwargs[short_name] = kwargs.pop(long_name)
def get_file_path_by_content(self, content: str) -> str:
svg_file = os.path.join(
get_text_dir(), tex_hash(full_markup) + ".svg"
get_text_dir(), tex_hash(content) + ".svg"
)
if not os.path.exists(svg_file):
self.markup_to_svg(full_markup, svg_file)
self.markup_to_svg(content, svg_file)
return svg_file
def get_full_markup_str(self) -> str:
if self.t2g:
log.warning(
"Manim currently cannot parse gradient from svg. "
"Please set gradient via `set_color_by_gradient`.",
)
config_style_dict = self.generate_config_style_dict()
global_attr_dict = {
"line_height": ((self.lsh or DEFAULT_LINE_SPACING_SCALE) + 1) * 0.6,
"font_family": self.font or get_customization()["style"]["font"],
"font_size": self.font_size * 1024,
"font_style": self.slant,
"font_weight": self.weight,
# TODO, it seems this doesn't work
"font_features": "liga=0,dlig=0,clig=0,hlig=0" if self.disable_ligatures else None,
"foreground": config_style_dict.get("fill", None),
"alpha": config_style_dict.get("fill-opacity", None)
}
global_attr_dict = {
k: v
for k, v in global_attr_dict.items()
if v is not None
}
global_attr_dict.update(self.global_config)
self.parser.update_global_attrs(global_attr_dict)
local_attr_items = [
(word_or_text_span, {key: value})
for t2x_dict, key in (
(self.t2c, "foreground"),
(self.t2f, "font_family"),
(self.t2s, "font_style"),
(self.t2w, "font_weight")
)
for word_or_text_span, value in t2x_dict.items()
]
local_attr_items.extend(self.local_configs.items())
for word_or_text_span, local_config in local_attr_items:
for text_span in self.find_indexes(word_or_text_span):
self.parser.update_local_attrs(text_span, local_config)
return self.parser.get_markup_str_with_attrs()
def find_indexes(self, word_or_text_span: str | tuple[int, int]) -> list[tuple[int, int]]:
if isinstance(word_or_text_span, tuple):
return [word_or_text_span]
return [
match_obj.span()
for match_obj in re.finditer(re.escape(word_or_text_span), self.text)
]
def markup_to_svg(self, markup_str: str, file_name: str) -> str:
# `manimpango` is under construction,
# so the following code is intended to suit its interface
@ -374,7 +219,7 @@ class Text(SVGMobject):
weight="NORMAL", # Already handled
size=1, # Already handled
_=0, # Empty parameter
disable_liga=False, # Already handled
disable_liga=False, # Need not to handle
file_name=file_name,
START_X=0,
START_Y=0,
@ -387,63 +232,318 @@ class Text(SVGMobject):
pango_width=pango_width
)
def generate_mobject(self) -> None:
super().generate_mobject()
# Toolkits
# Remove empty paths
submobjects = list(filter(lambda submob: submob.has_points(), self))
@staticmethod
def get_attr_dict_str(attr_dict: dict[str, str]) -> str:
return " ".join([
f"{key}='{value}'"
for key, value in attr_dict.items()
])
# Apply space characters
if self.apply_space_chars:
content_str = self.parser.remove_tags(self.text)
if self.is_markup:
content_str = saxutils.unescape(content_str)
for match_obj in re.finditer(r"\s", content_str):
char_index = match_obj.start()
space = Dot(radius=0, fill_opacity=0, stroke_opacity=0)
space.move_to(submobjects[max(char_index - 1, 0)].get_center())
submobjects.insert(char_index, space)
self.set_submobjects(submobjects)
@staticmethod
def get_begin_tag_str(attr_dict: dict[str, str]) -> str:
return f"<span {MarkupText.get_attr_dict_str(attr_dict)}>"
def full2short(self, config: dict) -> None:
conversion_dict = {
"line_spacing_height": "lsh",
"text2color": "t2c",
"text2font": "t2f",
"text2gradient": "t2g",
"text2slant": "t2s",
"text2weight": "t2w"
}
for kwargs in [config, self.CONFIG]:
for long_name, short_name in conversion_dict.items():
if long_name in kwargs:
kwargs[short_name] = kwargs.pop(long_name)
@staticmethod
def get_end_tag_str() -> str:
return "</span>"
def get_parts_by_text(self, word: str) -> VGroup:
if self.is_markup:
log.warning(
"Slicing MarkupText via `get_parts_by_text`, "
"the result could be unexpected."
)
elif not self.apply_space_chars:
log.warning(
"Slicing Text via `get_parts_by_text` without applying spaces, "
"the result could be unexpected."
)
return VGroup(*(
self[i:j]
for i, j in self.find_indexes(word)
@staticmethod
def convert_attr_key(key: str) -> str:
return SPAN_ATTR_KEY_CONVERSION[key.lower()]
@staticmethod
def convert_attr_val(val: typing.Any) -> str:
return str(val).lower()
@staticmethod
def merge_attr_items(
attr_items: list[Span, str, str]
) -> list[tuple[Span, dict[str, str]]]:
index_seq = [0]
attr_dict_list = [{}]
for span, key, value in attr_items:
if span[0] >= span[1]:
continue
region_indices = [
MarkupText.find_region_index(index, index_seq)
for index in span
]
for flag in (1, 0):
if index_seq[region_indices[flag]] == span[flag]:
continue
region_index = region_indices[flag]
index_seq.insert(region_index + 1, span[flag])
attr_dict_list.insert(
region_index + 1, attr_dict_list[region_index].copy()
)
region_indices[flag] += 1
if flag == 0:
region_indices[1] += 1
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 get_part_by_text(self, word: str) -> VMobject | None:
parts = self.get_parts_by_text(word)
return parts[0] if parts else None
# Parser
@property
def tag_items_from_markup(
self
) -> list[tuple[Span, Span, dict[str, str]]]:
if not self.is_markup:
return []
tag_pattern = r"""<(/?)(\w+)\s*((\w+\s*\=\s*('.*?'|".*?")\s*)*)>"""
attr_pattern = r"""(\w+)\s*\=\s*(?:(?:'(.*?)')|(?:"(.*?)"))"""
begin_match_obj_stack = []
match_obj_pairs = []
for match_obj in re.finditer(tag_pattern, self.string):
if not match_obj.group(1):
begin_match_obj_stack.append(match_obj)
else:
match_obj_pairs.append(
(begin_match_obj_stack.pop(), match_obj)
)
if begin_match_obj_stack:
raise ValueError("Unclosed tag(s) detected")
result = []
for begin_match_obj, end_match_obj in match_obj_pairs:
tag_name = begin_match_obj.group(2)
if tag_name != end_match_obj.group(2):
raise ValueError("Unmatched tag names")
if end_match_obj.group(3):
raise ValueError("Attributes shan't exist in ending tags")
if tag_name == "span":
attr_dict = dict([
(
MarkupText.convert_attr_key(match.group(1)),
MarkupText.convert_attr_val(
match.group(2) or match.group(3)
)
)
for match in re.finditer(
attr_pattern, begin_match_obj.group(3)
)
])
elif tag_name in TAG_TO_ATTR_DICT.keys():
if begin_match_obj.group(3):
raise ValueError(
f"Attributes shan't exist in tag '{tag_name}'"
)
attr_dict = TAG_TO_ATTR_DICT[tag_name].copy()
else:
raise ValueError(f"Unknown tag: '{tag_name}'")
result.append(
(begin_match_obj.span(), end_match_obj.span(), attr_dict)
)
return result
@property
def global_attr_items_from_config(self) -> list[str, str]:
global_attr_dict = {
"line_height": (
(self.lsh or DEFAULT_LINE_SPACING_SCALE) + 1
) * 0.6,
"font_family": self.font or get_customization()["style"]["font"],
"font_size": self.font_size * 1024,
"font_style": self.slant,
"font_weight": self.weight
}
global_attr_dict = {
k: v
for k, v in global_attr_dict.items()
if v is not None
}
result = list(it.chain(
global_attr_dict.items(),
self.global_config.items()
))
return [
(
self.convert_attr_key(key),
self.convert_attr_val(val)
)
for key, val in result
]
@property
def local_attr_items_from_config(self) -> list[tuple[Span, str, str]]:
result = [
(text_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(word_or_span)
] + [
(text_span, key, val)
for word_or_span, local_config in self.local_configs.items()
for text_span in self.find_spans(word_or_span)
for key, val in local_config.items()
]
return [
(
text_span,
self.convert_attr_key(key),
self.convert_attr_val(val)
)
for text_span, key, val in result
]
def find_spans(self, word_or_span: str | Span) -> list[Span]:
if isinstance(word_or_span, tuple):
return [word_or_span]
return [
match_obj.span()
for match_obj in re.finditer(re.escape(word_or_span), self.string)
]
@property
def skipped_spans(self) -> list[Span]:
return [
match_obj.span()
for match_obj in re.finditer(r"\s+", self.string)
]
@property
def label_span_list(self) -> list[Span]:
breakup_indices = [
index
for pattern in [
r"\s+",
r"\b",
*[
re.escape(substr)
for substr in self.get_substrs_to_isolate(self.isolate)
]
]
for match_obj in re.finditer(pattern, self.string)
for index in match_obj.span()
]
breakup_indices = sorted(filter(
lambda index: not any([
span[0] < index < span[1]
for span, _ in self.command_repl_items
]),
remove_list_redundancies([
*self.full_span, *breakup_indices
])
))
return list(filter(
lambda span: self.string[slice(*span)].strip(),
self.get_neighbouring_pairs(breakup_indices)
))
@property
def predefined_items(self) -> list[Span, str, str]:
return list(it.chain(
[
(self.full_span, key, val)
for key, val in self.global_attr_items_from_config
],
sorted([
((begin_tag_span[0], end_tag_span[1]), key, val)
for begin_tag_span, end_tag_span, attr_dict
in self.tag_items_from_markup
for key, val in attr_dict.items()
]),
self.local_attr_items_from_config
))
def get_inserted_string_pairs(
self, use_label: bool
) -> list[tuple[Span, tuple[str, str]]]:
attr_items = self.predefined_items
if use_label:
attr_items = [
(span, key, WHITE if key in COLOR_RELATED_KEYS else val)
for span, key, val in attr_items
] + [
(span, "foreground", "#{:06x}".format(label))
for label, span in enumerate(self.label_span_list)
]
return [
(span, (
self.get_begin_tag_str(attr_dict),
self.get_end_tag_str()
))
for span, attr_dict in self.merge_attr_items(attr_items)
]
@property
def inserted_string_pairs(self) -> list[tuple[Span, tuple[str, str]]]:
return self.get_inserted_string_pairs(use_label=True)
@property
def command_repl_items(self) -> list[tuple[Span, str]]:
return [
(tag_span, "")
for begin_tag, end_tag, _ in self.tag_items_from_markup
for tag_span in (begin_tag, end_tag)
]
@property
def has_predefined_colors(self) -> bool:
return any([
key in COLOR_RELATED_KEYS
for _, key, _ in self.predefined_items
])
@property
def plain_string(self) -> str:
return "".join([
self.get_begin_tag_str({"foreground": self.base_color}),
self.replace_str_by_spans(
self.string, self.get_span_replacement_dict(
self.get_inserted_string_pairs(use_label=False),
self.command_repl_items
)
),
self.get_end_tag_str()
])
def handle_submob_string(self, substr: str, string_span: Span) -> str:
if self.is_markup:
substr = saxutils.unescape(substr)
return substr
# Method alias
def get_parts_by_text(self, substr: str) -> VGroup:
return self.get_parts_by_string(substr)
def get_part_by_text(self, substr: str, index: int = 0) -> VMobject:
return self.get_part_by_string(substr, index)
def set_color_by_text(self, substr: str, color: ManimColor):
return self.set_color_by_string(substr, color)
def set_color_by_text_to_color_map(
self, text_to_color_map: dict[str, ManimColor]
):
return self.set_color_by_string_to_color_map(text_to_color_map)
def indices_of_part_by_text(
self, substr: str, index: int = 0
) -> list[int]:
return self.indices_of_part_by_string(substr, index)
def get_text(self) -> str:
return self.get_string()
class MarkupText(Text):
class Text(MarkupText):
CONFIG = {
"is_markup": True,
"apply_space_chars": False,
"is_markup": False,
}
@ -461,7 +561,9 @@ class Code(MarkupText):
digest_config(self, kwargs)
self.code = code
lexer = pygments.lexers.get_lexer_by_name(self.language)
formatter = pygments.formatters.PangoMarkupFormatter(style=self.code_style)
formatter = pygments.formatters.PangoMarkupFormatter(
style=self.code_style
)
markup = pygments.highlight(code, lexer, formatter)
markup = re.sub(r"</?tt>", "", markup)
super().__init__(markup, **kwargs)