From 26de7c9ce5b04cb7ef88eeda4c2ca910df65fcf9 Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Fri, 20 May 2022 18:49:38 +0800 Subject: [PATCH 01/27] Fix a logistic bug --- manimlib/mobject/svg/string_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/svg/string_mobject.py b/manimlib/mobject/svg/string_mobject.py index 5004960e..e4376a33 100644 --- a/manimlib/mobject/svg/string_mobject.py +++ b/manimlib/mobject/svg/string_mobject.py @@ -174,8 +174,8 @@ class StringMobject(SVGMobject, ABC): ): l = self.full_span[1] span = tuple( + default_index if index is None else min(index, l) if index >= 0 else max(index + l, 0) - if index is not None else default_index for index, default_index in zip(sel, self.full_span) ) return [span] From f0984487eaf22f6d99b40a02fd4296e7c3a50770 Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sat, 21 May 2022 15:56:03 +0800 Subject: [PATCH 02/27] Construct TexTemplate class to convert tex to svg --- manimlib/constants.py | 50 +++++ manimlib/default_config.yml | 9 +- manimlib/mobject/svg/mtex_mobject.py | 17 +- manimlib/mobject/svg/tex_mobject.py | 23 +-- manimlib/mobject/svg/text_mobject.py | 4 +- manimlib/tex_templates/ctex_template.tex | 25 --- manimlib/tex_templates/tex_template.tex | 28 --- manimlib/utils/init_config.py | 19 +- manimlib/utils/simple_functions.py | 7 + manimlib/utils/tex_file_writing.py | 223 +++++++++-------------- 10 files changed, 177 insertions(+), 228 deletions(-) delete mode 100644 manimlib/tex_templates/ctex_template.tex delete mode 100644 manimlib/tex_templates/tex_template.tex diff --git a/manimlib/constants.py b/manimlib/constants.py index f0bc3269..2be127a0 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -64,6 +64,56 @@ JOINT_TYPE_MAP = { "miter": 3, } +# Related to Tex +PRESET_PREAMBLE = { + "default": ( + "\\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": ( + "\\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}", + ), + "minimized": ( + "\\usepackage{amsmath}", + "\\usepackage{amssymb}", + "\\usepackage{xcolor}", + ), +} + # Related to Text NORMAL = "NORMAL" ITALIC = "ITALIC" diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index c948a8e5..d5e89605 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -18,13 +18,12 @@ directories: temporary_storage: "" tex: executable: "latex" - template_file: "tex_template.tex" - intermediate_filetype: "dvi" - text_to_replace: "[tex_expression]" + intermediate_filetype: ".dvi" + preamble: "default" # For ctex, use the following configuration # executable: "xelatex -no-pdf" - # template_file: "ctex_template.tex" - # intermediate_filetype: "xdv" + # intermediate_filetype: ".xdv" + # preamble: "ctex" universal_import_line: "from manimlib import *" style: font: "Consolas" diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 149f313f..52947ec9 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -1,9 +1,7 @@ from __future__ import annotations 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 TexTemplate from typing import TYPE_CHECKING @@ -37,6 +35,7 @@ class MTex(StringMobject): "alignment": "\\centering", "tex_environment": "align*", "tex_to_color_map": {}, + "tex_template": None, } def __init__(self, tex_string: str, **kwargs): @@ -60,17 +59,13 @@ class MTex(StringMobject): self.tex_string, self.alignment, self.tex_environment, - self.tex_to_color_map + self.tex_to_color_map, + self.tex_template ) 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) + tex_template = self.tex_template or TexTemplate() + file_path = tex_template.get_svg_file_path(content) return file_path # Parsing diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index fb444608..2ed272c6 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -12,9 +12,7 @@ from manimlib.mobject.geometry import Line 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 TexTemplate from typing import TYPE_CHECKING @@ -44,6 +42,7 @@ class SingleStringTex(SVGMobject): "alignment": "\\centering", "math_mode": True, "organize_left_to_right": False, + "tex_template": None, } def __init__(self, tex_string: str, **kwargs): @@ -64,27 +63,21 @@ class SingleStringTex(SVGMobject): self.path_string_config, self.tex_string, self.alignment, - self.math_mode + self.math_mode, + self.tex_template ) def get_file_path(self) -> str: - full_tex = 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) + content = self.get_tex_file_body(self.tex_string) + tex_template = self.tex_template or TexTemplate() + file_path = tex_template.get_svg_file_path(content) 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()) diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index 93623c31..3e45753c 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -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 @@ -180,7 +180,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) diff --git a/manimlib/tex_templates/ctex_template.tex b/manimlib/tex_templates/ctex_template.tex deleted file mode 100644 index 65ff5df1..00000000 --- a/manimlib/tex_templates/ctex_template.tex +++ /dev/null @@ -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} diff --git a/manimlib/tex_templates/tex_template.tex b/manimlib/tex_templates/tex_template.tex deleted file mode 100644 index 5f665b61..00000000 --- a/manimlib/tex_templates/tex_template.tex +++ /dev/null @@ -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} diff --git a/manimlib/utils/init_config.py b/manimlib/utils/init_config.py index 2e5d4b32..04381ae5 100644 --- a/manimlib/utils/init_config.py +++ b/manimlib/utils/init_config.py @@ -9,7 +9,7 @@ from rich import box from rich.console import Console from rich.prompt import Confirm from rich.prompt import Prompt -from rich.rule import Rule +from rich.rule import Rule from rich.table import Table from typing import TYPE_CHECKING @@ -44,9 +44,8 @@ def init_customization() -> None: }, "tex": { "executable": "", - "template_file": "", "intermediate_filetype": "", - "text_to_replace": "[tex_expression]", + "preamble": "", }, "universal_import_line": "from manimlib import *", "style": { @@ -83,7 +82,7 @@ def init_customization() -> None: # print("Initialize configuration") try: scope = Prompt.ask( - " Select the scope of the configuration", + " Select the scope of the configuration", choices=["global", "local"], default="local" ) @@ -129,13 +128,13 @@ def init_customization() -> None: ) if tex == "latex": tex_config["executable"] = "latex" - tex_config["template_file"] = "tex_template.tex" - tex_config["intermediate_filetype"] = "dvi" + tex_config["intermediate_filetype"] = ".dvi" + tex_config["preamble"] = "default" else: tex_config["executable"] = "xelatex -no-pdf" - tex_config["template_file"] = "ctex_template.tex" - tex_config["intermediate_filetype"] = "xdv" - + tex_config["intermediate_filetype"] = ".xdv" + tex_config["preamble"] = "ctex" + console.print("[bold]Styles:[/bold]") configuration["style"]["background_color"] = Prompt.ask( " Which [bold]background color[/bold] do you want [italic](hex code)", @@ -172,7 +171,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]") diff --git a/manimlib/utils/simple_functions.py b/manimlib/utils/simple_functions.py index 1371a744..143bf350 100644 --- a/manimlib/utils/simple_functions.py +++ b/manimlib/utils/simple_functions.py @@ -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] diff --git a/manimlib/utils/tex_file_writing.py b/manimlib/utils/tex_file_writing.py index 557d08be..d3e1792a 100644 --- a/manimlib/utils/tex_file_writing.py +++ b/manimlib/utils/tex_file_writing.py @@ -1,143 +1,102 @@ from __future__ import annotations -from contextlib import contextmanager -import hashlib import os -import sys +import re from manimlib.config import get_custom_config -from manimlib.config import get_manim_dir +from manimlib.constants import PRESET_PREAMBLE from manimlib.logger import log from manimlib.utils.directories import get_tex_dir - - -SAVED_TEX_CONFIG = {} - - -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": "..." - } - """ - # 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() - 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_to_svg_file(tex_file_content: str) -> str: - svg_file = os.path.join( - get_tex_dir(), tex_hash(tex_file_content) + ".svg" - ) - if not os.path.exists(svg_file): - # If svg doesn't exist, create it - tex_to_svg(tex_file_content, svg_file) - 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)) - - # 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.") - with open(log_file, "r", encoding="utf-8") as file: - for line in file.readlines(): - if line.startswith("!"): - log.debug(f"The error could be: `{line[2:-1]}`") - raise LatexError() - return result - - -def dvi_to_svg(dvi_file: str) -> str: - """ - Converts a dvi, which potentially has multiple slides, into a - directory full of enumerated pngs corresponding with these slides. - Returns a list of PIL Image objects for these images sorted as they - where in the dvi - """ - file_type = get_tex_config()["intermediate_filetype"] - result = dvi_file.replace("." + file_type, ".svg") - if not os.path.exists(result): - commands = [ - "dvisvgm", - "\"{}\"".format(dvi_file), - "-n", - "-v", - "0", - "-o", - "\"{}\"".format(result), - ">", - os.devnull - ] - os.system(" ".join(commands)) - return result - - -# TODO, perhaps this should live elsewhere -@contextmanager -def display_during_execution(message: str) -> None: - # Only show top line - to_print = message.split("\n")[0] - max_characters = os.get_terminal_size().columns - 1 - if len(to_print) > max_characters: - to_print = to_print[:max_characters - 3] + "..." - try: - print(to_print, end="\r") - yield - finally: - print(" " * len(to_print), end="\r") - +from manimlib.utils.simple_functions import hash_string class LatexError(Exception): pass + + +class TexTemplate: + def __init__(self, preamble_type: str | None = None): + tex_config = get_custom_config()["tex"] + self.executable = tex_config["executable"] + self.dvi_ext = tex_config["intermediate_filetype"] + if preamble_type is None: + preamble_type = tex_config["preamble"] + self.preamble = list(PRESET_PREAMBLE.get( + preamble_type, PRESET_PREAMBLE["default"] + )) + + def __hash__(self) -> int: + return hash(self.get_tex_file_content("")) + + def get_tex_file_content(self, content: str) -> str: + return "\n\n".join(( + "\\documentclass[preview]{standalone}", + "\n".join(self.preamble), + "\\begin{document}", + content, + "\\end{document}" + )) + "\n" + + def get_svg_file_path(self, content: str) -> str: + full_tex = self.get_tex_file_content(content) + hash_code = hash_string(full_tex) + tex_dir = get_tex_dir() + root = os.path.join(tex_dir, hash_code) + svg_file_path = root + ".svg" + if os.path.exists(svg_file_path): + return svg_file_path + + # If svg doesn't exist, create it + replaced_content = content.replace("\n", " ") + displayed_msg = f"Writing \"{replaced_content}\"" + max_characters = os.get_terminal_size().columns - 1 + if len(displayed_msg) > max_characters: + displayed_msg = displayed_msg[:max_characters - 3] + "..." + print(displayed_msg, end="\r") + + with open(root + ".tex", "w", encoding="utf-8") as tex_file: + tex_file.write(full_tex) + + # tex to dvi + if os.system(" ".join(( + self.executable, + "-interaction=batchmode", + "-halt-on-error", + f"-output-directory=\"{tex_dir}\"", + 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}{self.dvi_ext}\"", + "-n", + "-v", + "0", + "-o", + f"\"{svg_file_path}\"", + ">", + os.devnull + ))) + + # Cleanup superfluous documents + for ext in (".tex", self.dvi_ext, ".log", ".aux"): + try: + os.remove(root + ext) + except FileNotFoundError: + pass + + print(" " * len(displayed_msg), end="\r") + return svg_file_path + + def add_preamble(self, *preamble_strs: str): + self.preamble.extend(preamble_strs) + return self From 1596356385f5b4541b6ab43c6ad263bbf1a6aa2c Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sun, 22 May 2022 10:29:20 +0800 Subject: [PATCH 03/27] Reuse hash_string function --- manimlib/mobject/svg/svg_mobject.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index 5a918e66..a1d07967 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -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() - if k in style_keys - } + style_attrs = {} + rest_attrs = {} + for k, v in root.attrib.items(): + if k in style_keys: + style_attrs[k] = v + else: + rest_attrs[k] = v - 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) + new_root = ET.Element("svg", rest_attrs) + config_style_node = ET.SubElement(new_root, "g", config_style_attrs) + root_style_node = ET.SubElement(config_style_node, "g", style_attrs) root_style_node.extend(root) return ET.ElementTree(new_root) @@ -302,8 +305,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") From 9386461d273361d15472f80dff64adddc3980ef4 Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sun, 22 May 2022 23:47:45 +0800 Subject: [PATCH 04/27] Add font attribute for Tex --- manimlib/constants.py | 50 --- manimlib/default_config.yml | 9 +- manimlib/mobject/svg/mtex_mobject.py | 15 +- manimlib/mobject/svg/tex_mobject.py | 15 +- manimlib/tex_fonts.yml | 552 +++++++++++++++++++++++++++ manimlib/utils/init_config.py | 35 +- manimlib/utils/tex_file_writing.py | 233 ++++++----- 7 files changed, 729 insertions(+), 180 deletions(-) create mode 100644 manimlib/tex_fonts.yml diff --git a/manimlib/constants.py b/manimlib/constants.py index 2be127a0..f0bc3269 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -64,56 +64,6 @@ JOINT_TYPE_MAP = { "miter": 3, } -# Related to Tex -PRESET_PREAMBLE = { - "default": ( - "\\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": ( - "\\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}", - ), - "minimized": ( - "\\usepackage{amsmath}", - "\\usepackage{amssymb}", - "\\usepackage{xcolor}", - ), -} - # Related to Text NORMAL = "NORMAL" ITALIC = "ITALIC" diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index d5e89605..6ddd042e 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -16,16 +16,9 @@ directories: # return whatever is at to the TMPDIR environment variable. If you want to # specify them elsewhere, temporary_storage: "" -tex: - executable: "latex" - intermediate_filetype: ".dvi" - preamble: "default" - # For ctex, use the following configuration - # executable: "xelatex -no-pdf" - # intermediate_filetype: ".xdv" - # preamble: "ctex" universal_import_line: "from manimlib import *" style: + tex_font: "default" font: "Consolas" background_color: "#333333" # Set the position of preview window, you can use directions, e.g. UL/DR/OL/OO/... diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 52947ec9..452344ab 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -1,7 +1,8 @@ from __future__ import annotations from manimlib.mobject.svg.string_mobject import StringMobject -from manimlib.utils.tex_file_writing import TexTemplate +from manimlib.utils.tex_file_writing import display_during_execution +from manimlib.utils.tex_file_writing import tex_content_to_svg_file from typing import TYPE_CHECKING @@ -35,7 +36,8 @@ class MTex(StringMobject): "alignment": "\\centering", "tex_environment": "align*", "tex_to_color_map": {}, - "tex_template": None, + "font": "", + "additional_preamble": "", } def __init__(self, tex_string: str, **kwargs): @@ -60,12 +62,15 @@ class MTex(StringMobject): self.alignment, self.tex_environment, self.tex_to_color_map, - self.tex_template + self.font, + self.additional_preamble ) def get_file_path_by_content(self, content: str) -> str: - tex_template = self.tex_template or TexTemplate() - file_path = tex_template.get_svg_file_path(content) + with display_during_execution(f"Writing \"{self.tex_string}\""): + file_path = tex_content_to_svg_file( + content, self.font, self.additional_preamble + ) return file_path # Parsing diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 2ed272c6..f1171e9c 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -12,7 +12,8 @@ from manimlib.mobject.geometry import Line 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 TexTemplate +from manimlib.utils.tex_file_writing import display_during_execution +from manimlib.utils.tex_file_writing import tex_content_to_svg_file from typing import TYPE_CHECKING @@ -42,7 +43,8 @@ class SingleStringTex(SVGMobject): "alignment": "\\centering", "math_mode": True, "organize_left_to_right": False, - "tex_template": None, + "font": "", + "additional_preamble": "", } def __init__(self, tex_string: str, **kwargs): @@ -64,13 +66,16 @@ class SingleStringTex(SVGMobject): self.tex_string, self.alignment, self.math_mode, - self.tex_template + self.font, + self.additional_preamble ) def get_file_path(self) -> str: content = self.get_tex_file_body(self.tex_string) - tex_template = self.tex_template or TexTemplate() - file_path = tex_template.get_svg_file_path(content) + with display_during_execution(f"Writing \"{self.tex_string}\""): + file_path = tex_content_to_svg_file( + content, self.font, self.additional_preamble + ) return file_path def get_tex_file_body(self, tex_string: str) -> str: diff --git a/manimlib/tex_fonts.yml b/manimlib/tex_fonts.yml new file mode 100644 index 00000000..b380428b --- /dev/null +++ b/manimlib/tex_fonts.yml @@ -0,0 +1,552 @@ +default: + 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: + 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} + +blank: + compiler: latex + preamble: |- + \usepackage{xcolor} + +basic: + compiler: latex + preamble: |- + \usepackage[english]{babel} + \usepackage{amsmath} + \usepackage{amssymb} + \usepackage{xcolor} + +# American Typewriter +american_typewriter: + compiler: xelatex + preamble: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{American Typewriter} + \usepackage[defaultmathsizes]{mathastext} + +# Antykwa Poltawskiego (TX Fonts for Greek and math symbols) +antykwa: + compiler: latex + preamble: |- + \usepackage[OT4,OT1]{fontenc} + \usepackage{txfonts} + \usepackage[upright]{txgreeks} + \usepackage{antpolt} + \usepackage[defaultmathsizes,nolessnomore]{mathastext} + +# Apple Chancery +apple_chancery: + compiler: xelatex + preamble: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Apple Chancery} + \usepackage[defaultmathsizes]{mathastext} + +# Auriocus Kalligraphicus (Symbol Greek) +auriocus_kalligraphicus: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage{aurical} + \renewcommand{\rmdefault}{AuriocusKalligraphicus} + \usepackage[symbolgreek]{mathastext} + +# Baskervald ADF with Fourier +baskervald_adf_fourier: + compiler: latex + preamble: |- + \usepackage[upright]{fourier} + \usepackage{baskervald} + \usepackage[defaultmathsizes,noasterisk]{mathastext} + +# Baskerville (Italic) +baskerville_it: + compiler: xelatex + preamble: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Baskerville} + \usepackage[defaultmathsizes,italic]{mathastext} + +# Biolinum +biolinum: + compiler: latex + preamble: |- + \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-Italic (PX math and Greek) +brushscriptx: + compiler: xelatex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage{pxfonts} + \renewcommand{\rmdefault}{pbsi} + \renewcommand{\mddefault}{xl} + \renewcommand{\bfdefault}{xl} + \usepackage[defaultmathsizes,noasterisk]{mathastext} + \boldmath + +# Chalkboard SE +chalkboard_se: + compiler: xelatex + preamble: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Chalkboard SE} + \usepackage[defaultmathsizes]{mathastext} + +# Chalkduster +chalkduster: + compiler: xelatex + preamble: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Chalkduster} + \usepackage[defaultmathsizes]{mathastext} + +# Comfortaa +comfortaa: + compiler: latex + preamble: |- + \usepackage[default]{comfortaa} + \usepackage[LGRgreek,defaultmathsizes,noasterisk]{mathastext} + \let\varphi\phi + \linespread{1.06} + +# Comic Sans MS +comic_sans: + compiler: xelatex + preamble: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Comic Sans MS} + \usepackage[defaultmathsizes]{mathastext} + +# Droid Sans +droid_sans: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage[default]{droidsans} + \usepackage[LGRgreek]{mathastext} + \let\varepsilon\epsilon + +# Droid Sans (Italic) +droid_sans_it: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage[default]{droidsans} + \usepackage[LGRgreek,defaultmathsizes,italic]{mathastext} + \let\varphi\phi + +# Droid Serif +droid_serif: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage[default]{droidserif} + \usepackage[LGRgreek]{mathastext} + \let\varepsilon\epsilon + +# Droid Serif (PX math symbols) (Italic) +droid_serif_px_it: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage{pxfonts} + \usepackage[default]{droidserif} + \usepackage[LGRgreek,defaultmathsizes,italic,basic]{mathastext} + \let\varphi\phi + +# ECF Augie (Euler Greek) +ecf_augie: + compiler: latex + preamble: |- + \renewcommand\familydefault{fau} + \usepackage[defaultmathsizes,eulergreek]{mathastext} + +# ECF JD (with TX fonts) +ecf_jd: + compiler: latex + preamble: |- + \usepackage{txfonts} + \usepackage[upright]{txgreeks} + \renewcommand\familydefault{fjd} + \usepackage{mathastext} + \mathversion{bold} + +# ECF Skeetch (CM Greek) +ecf_skeetch: + compiler: latex + preamble: |- + \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 (with Symbol font) +ecf_tall_paul: + compiler: latex + preamble: |- + \DeclareFontFamily{T1}{ftp}{} + \DeclareFontShape{T1}{ftp}{m}{n}{<->s*[1.4] ftpmw8t}{} + \renewcommand\familydefault{ftp} + \usepackage[symbol]{mathastext} + \let\infty\inftypsy + +# ECF Webster (with TX fonts) +ecf_webster: + compiler: xelatex + preamble: |- + \usepackage{txfonts} + \usepackage[upright]{txgreeks} + \renewcommand\familydefault{fwb} + \usepackage{mathastext} + \renewcommand{\int}{\intop\limits} + \linespread{1.5} + \mathversion{bold} + +# Electrum ADF (CM Greek) +electrum_adf: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage[LGRgreek,basic,defaultmathsizes]{mathastext} + \usepackage[lf]{electrum} + \Mathastext + \let\varphi\phi + +# Epigrafica +epigrafica: + compiler: latex + preamble: |- + \usepackage[LGR,OT1]{fontenc} + \usepackage{epigrafica} + \usepackage[basic,LGRgreek,defaultmathsizes]{mathastext} + \let\varphi\phi + \linespread{1.2} + +# Fourier Utopia (Fourier upright Greek) +fourier_utopia: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage[upright]{fourier} + \usepackage{mathastext} + +# French Cursive (Euler Greek) +french_cursive: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage[default]{frcursive} + \usepackage[eulergreek,noplusnominus,noequal,nohbar,nolessnomore,noasterisk]{mathastext} + +# GFS Bodoni +gfs_bodoni: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \renewcommand{\rmdefault}{bodoni} + \usepackage[LGRgreek]{mathastext} + \let\varphi\phi + \linespread{1.06} + +# GFS Didot (Italic) +gfs_didot: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \renewcommand\rmdefault{udidot} + \usepackage[LGRgreek,defaultmathsizes,italic]{mathastext} + \let\varphi\phi + +# GFS NeoHellenic +gfs_neoHellenic: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \renewcommand{\rmdefault}{neohellenic} + \usepackage[LGRgreek]{mathastext} + \let\varphi\phi + \linespread{1.06} + +# GNU FreeSerif (and TX fonts symbols) +gnu_freesans_tx: + compiler: xelatex + preamble: |- + \usepackage[no-math]{fontspec} + \usepackage{txfonts} + \setmainfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSerifBold,ItalicFont=FreeSerifItalic,BoldItalicFont=FreeSerifBoldItalic]{FreeSerif} + \usepackage[defaultmathsizes]{mathastext} + +# GNU FreeSerif and FreeSans +gnu_freeserif_freesans: + compiler: xelatex + preamble: |- + \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 with Fourier (Italic) +helvetica_fourier_it: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage[scaled]{helvet} + \usepackage{fourier} + \renewcommand{\rmdefault}{phv} + \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} + +# Latin Modern Typewriter Proportional +latin_modern_tw: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage[variablett]{lmodern} + \renewcommand{\rmdefault}{\ttdefault} + \usepackage[LGRgreek]{mathastext} + \MTgreekfont{lmtt} + \Mathastext + \let\varepsilon\epsilon + +# Latin Modern Typewriter Proportional (CM Greek) (Italic) +latin_modern_tw_it: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage[variablett,nomath]{lmodern} + \renewcommand{\familydefault}{\ttdefault} + \usepackage[frenchmath]{mathastext} + \linespread{1.08} + +# Libertine +libertine: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage{libertine} + \usepackage[greek=n]{libgreek} + \usepackage[noasterisk,defaultmathsizes]{mathastext} + +# Libris ADF with Fourier +libris_adf_fourier: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage[upright]{fourier} + \usepackage{libris} + \renewcommand{\familydefault}{\sfdefault} + \usepackage[noasterisk]{mathastext} + +# Minion Pro and Myriad Pro (and TX fonts symbols) +minion_pro_myriad_pro: + compiler: xelatex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage[default]{droidserif} + \usepackage[LGRgreek]{mathastext} + \let\varepsilon\epsilon + +# Minion Pro (and TX fonts symbols) +minion_pro_tx: + compiler: xelatex + preamble: |- + \usepackage{txfonts} + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Minion Pro} + \usepackage[defaultmathsizes]{mathastext} + +# New Century Schoolbook (Symbol Greek) +new_century_schoolbook: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage{newcent} + \usepackage[symbolgreek]{mathastext} + \linespread{1.1} + +# New Century Schoolbook (Symbol Greek, PX math symbols) +new_century_schoolbook_px: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage{pxfonts} + \usepackage{newcent} + \usepackage[symbolgreek,defaultmathsizes]{mathastext} + \linespread{1.06} + +# Noteworthy Light +noteworthy_light: + compiler: latex + preamble: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Noteworthy Light} + \usepackage[defaultmathsizes]{mathastext} + +# Palatino (Symbol Greek) +palatino: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage{palatino} + \usepackage[symbolmax,defaultmathsizes]{mathastext} + +# Papyrus +papyrus: + compiler: xelatex + preamble: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Papyrus} + \usepackage[defaultmathsizes]{mathastext} + +# Romande ADF with Fourier (Italic) +romande_adf_fourier_it: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage{fourier} + \usepackage{romande} + \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} + \renewcommand{\itshape}{\swashstyle} + +# SliTeX (Euler Greek) +slitex: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage{tpslifonts} + \usepackage[eulergreek,defaultmathsizes]{mathastext} + \MTEulerScale{1.06} + \linespread{1.2} + +# Times with Fourier (Italic) +times_fourier_it: + compiler: latex + preamble: |- + \usepackage{fourier} + \renewcommand{\rmdefault}{ptm} + \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} + +# URW Avant Garde (Symbol Greek) +urw_avant_garde: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage{avant} + \renewcommand{\familydefault}{\sfdefault} + \usepackage[symbolgreek,defaultmathsizes]{mathastext} + +# URW Zapf Chancery (CM Greek) +urw_zapf_chancery: + compiler: latex + preamble: |- + \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 with Fourier (Italic) +venturis_adf_fourier_it: + compiler: latex + preamble: |- + \usepackage{fourier} + \usepackage[lf]{venturis} + \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} + +# Verdana (Italic) +verdana_it: + compiler: xelatex + preamble: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Verdana} + \usepackage[defaultmathsizes,italic]{mathastext} + +# Vollkorn (TX fonts for Greek and math symbols) +vollkorn: + compiler: latex + preamble: |- + \usepackage[T1]{fontenc} + \usepackage{txfonts} + \usepackage[upright]{txgreeks} + \usepackage{vollkorn} + \usepackage[defaultmathsizes]{mathastext} + +# Vollkorn with Fourier (Italic) +vollkorn_fourier_it: + compiler: latex + preamble: |- + \usepackage{fourier} + \usepackage{vollkorn} + \usepackage[italic,nohbar]{mathastext} + +# Zapf Chancery +zapf_chancery: + compiler: latex + preamble: |- + \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} diff --git a/manimlib/utils/init_config.py b/manimlib/utils/init_config.py index 04381ae5..323bda37 100644 --- a/manimlib/utils/init_config.py +++ b/manimlib/utils/init_config.py @@ -42,14 +42,10 @@ def init_customization() -> None: "sounds": "", "temporary_storage": "", }, - "tex": { - "executable": "", - "intermediate_filetype": "", - "preamble": "", - }, "universal_import_line": "from manimlib import *", "style": { - "font": "Consolas", + "tex_font": "", + "font": "", "background_color": "", }, "window_position": "UR", @@ -119,24 +115,17 @@ 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["intermediate_filetype"] = ".dvi" - tex_config["preamble"] = "default" - else: - tex_config["executable"] = "xelatex -no-pdf" - tex_config["intermediate_filetype"] = ".xdv" - tex_config["preamble"] = "ctex" - console.print("[bold]Styles:[/bold]") - configuration["style"]["background_color"] = Prompt.ask( + style_config = configuration["style"] + style_config["tex_font"] = Prompt.ask( + " Which [bold]font[/bold] for LaTeX do you want", + default="default" + ) + style_config["font"] = Prompt.ask( + " Which [bold]font[/bold] for non-LaTeX text do you want", + default="Consolas" + ) + style_config["background_color"] = Prompt.ask( " Which [bold]background color[/bold] do you want [italic](hex code)", default="#333333" ) diff --git a/manimlib/utils/tex_file_writing.py b/manimlib/utils/tex_file_writing.py index d3e1792a..2da51caa 100644 --- a/manimlib/utils/tex_file_writing.py +++ b/manimlib/utils/tex_file_writing.py @@ -1,102 +1,157 @@ from __future__ import annotations +from contextlib import contextmanager import os import re +import yaml from manimlib.config import get_custom_config -from manimlib.constants import PRESET_PREAMBLE +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_font_config(tex_font: str) -> str: + """ + Returns a dict which should look something like this: + { + "compiler": "latex", + "preamble": "..." + } + """ + name = re.sub(r"[^a-zA-Z]", "_", tex_font).lower() + with open(os.path.join( + get_manim_dir(), "manimlib", "tex_fonts.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 font '%s', falling back to 'default'.", + name + ) + name = "default" + result = templates_dict[name] + if name not in ("default", "ctex", "blank", "basic"): + result["preamble"] = "\n".join(( + templates_dict["basic"]["preamble"], result["preamble"] + )) + return result + + +def get_tex_config() -> dict[str, str]: + # Only load once, then save thereafter + if not SAVED_TEX_CONFIG: + tex_font = get_custom_config()["style"]["tex_font"] + SAVED_TEX_CONFIG.update(get_tex_font_config(tex_font)) + return SAVED_TEX_CONFIG + + +def tex_content_to_svg_file( + content: str, tex_font: str, additional_preamble: str +) -> str: + if not tex_font: + tex_config = get_tex_config() + else: + tex_config = get_tex_font_config(tex_font) + + preamble = tex_config["preamble"] + if additional_preamble: + preamble += "\n" + additional_preamble + full_tex = "\n\n".join(( + "\\documentclass[preview]{standalone}", + preamble, + "\\begin{document}", + content, + "\\end{document}" + )) + "\n" + + svg_file = os.path.join( + get_tex_dir(), hash_string(full_tex) + ".svg" + ) + if not os.path.exists(svg_file): + # If svg doesn't exist, create it + create_tex_svg(full_tex, svg_file, tex_config["compiler"]) + return svg_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 + 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: + # 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] + "..." + try: + print(to_print, end="\r") + yield + finally: + print(" " * len(to_print), end="\r") + + class LatexError(Exception): pass - - -class TexTemplate: - def __init__(self, preamble_type: str | None = None): - tex_config = get_custom_config()["tex"] - self.executable = tex_config["executable"] - self.dvi_ext = tex_config["intermediate_filetype"] - if preamble_type is None: - preamble_type = tex_config["preamble"] - self.preamble = list(PRESET_PREAMBLE.get( - preamble_type, PRESET_PREAMBLE["default"] - )) - - def __hash__(self) -> int: - return hash(self.get_tex_file_content("")) - - def get_tex_file_content(self, content: str) -> str: - return "\n\n".join(( - "\\documentclass[preview]{standalone}", - "\n".join(self.preamble), - "\\begin{document}", - content, - "\\end{document}" - )) + "\n" - - def get_svg_file_path(self, content: str) -> str: - full_tex = self.get_tex_file_content(content) - hash_code = hash_string(full_tex) - tex_dir = get_tex_dir() - root = os.path.join(tex_dir, hash_code) - svg_file_path = root + ".svg" - if os.path.exists(svg_file_path): - return svg_file_path - - # If svg doesn't exist, create it - replaced_content = content.replace("\n", " ") - displayed_msg = f"Writing \"{replaced_content}\"" - max_characters = os.get_terminal_size().columns - 1 - if len(displayed_msg) > max_characters: - displayed_msg = displayed_msg[:max_characters - 3] + "..." - print(displayed_msg, end="\r") - - with open(root + ".tex", "w", encoding="utf-8") as tex_file: - tex_file.write(full_tex) - - # tex to dvi - if os.system(" ".join(( - self.executable, - "-interaction=batchmode", - "-halt-on-error", - f"-output-directory=\"{tex_dir}\"", - 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}{self.dvi_ext}\"", - "-n", - "-v", - "0", - "-o", - f"\"{svg_file_path}\"", - ">", - os.devnull - ))) - - # Cleanup superfluous documents - for ext in (".tex", self.dvi_ext, ".log", ".aux"): - try: - os.remove(root + ext) - except FileNotFoundError: - pass - - print(" " * len(displayed_msg), end="\r") - return svg_file_path - - def add_preamble(self, *preamble_strs: str): - self.preamble.extend(preamble_strs) - return self From 22a3bef6700510541d6ed29a2b036103d1712625 Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sat, 28 May 2022 12:40:29 +0800 Subject: [PATCH 05/27] Resolve conflicts --- manimlib/animation/transform_matching_parts.py | 2 ++ manimlib/mobject/svg/svg_mobject.py | 14 ++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index e82bafaf..ef62ff7d 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -192,6 +192,8 @@ class TransformMatchingStrings(AnimationGroup): 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), diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index a1d07967..01623aa3 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -116,15 +116,13 @@ class SVGMobject(VMobject): "style" ) root = element_tree.getroot() - style_attrs = {} - rest_attrs = {} - for k, v in root.attrib.items(): - if k in style_keys: - style_attrs[k] = v - else: - rest_attrs[k] = v + style_attrs = { + k: v + for k, v in root.attrib.items() + if k in style_keys + } - new_root = ET.Element("svg", rest_attrs) + new_root = ET.Element("svg", {}) # TODO: width, height config_style_node = ET.SubElement(new_root, "g", config_style_attrs) root_style_node = ET.SubElement(config_style_node, "g", style_attrs) root_style_node.extend(root) From f0447d7739560577d8125df3f075fe3d561f239c Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sat, 28 May 2022 21:43:37 +0800 Subject: [PATCH 06/27] Small refactors on StringMobject and relevant classes --- .../animation/transform_matching_parts.py | 142 ++-- manimlib/default_config.yml | 4 +- manimlib/mobject/svg/mtex_mobject.py | 64 +- manimlib/mobject/svg/string_mobject.py | 165 ++-- manimlib/mobject/svg/svg_mobject.py | 3 +- manimlib/mobject/svg/text_mobject.py | 188 ++--- manimlib/tex_fonts.yml | 778 ++++++++---------- manimlib/utils/init_config.py | 23 +- manimlib/utils/tex_file_writing.py | 39 +- 9 files changed, 669 insertions(+), 737 deletions(-) diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index ef62ff7d..452333fd 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -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) diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index 573ba2b2..59a88945 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -18,6 +18,8 @@ directories: temporary_storage: "" universal_import_line: "from manimlib import *" style: + # "latex" | "xelatex" + tex_compiler: "latex" tex_font: "default" font: "Consolas" text_alignment: "LEFT" @@ -41,4 +43,4 @@ camera_resolutions: high: "1920x1080" 4k: "3840x2160" default_resolution: "high" -fps: 30 \ No newline at end of file +fps: 30 diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 452344ab..055fc1b6 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -40,6 +40,16 @@ class MTex(StringMobject): "additional_preamble": "", } + CMD_PATTERN = r"\\(?:[a-zA-Z]+|.)|[_^{}]" + FLAG_DICT = { + r"{": 1, + r"}": -1 + } + CONTENT_REPL = {} + MATCH_REPL = { + r"[_^{}]": "" + } + def __init__(self, tex_string: str, **kwargs): # Prevent from passing an empty string. if not tex_string.strip(): @@ -75,44 +85,32 @@ class MTex(StringMobject): # 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( + def get_internal_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 [ + (cmd_content_spans[range_begin], {}) + for _, (range_begin, range_end) in self.group_neighbours([ + (span_begin + index, span_end - index) + for index, (span_begin, span_end) in enumerate( + cmd_content_spans + ) + ]) + if range_end - range_begin >= 2 + ] + + def get_external_specified_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) ] - return [(span, {}) for span in specified_spans] @staticmethod def get_color_cmd_str(rgb_hex: str) -> str: @@ -156,8 +154,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) diff --git a/manimlib/mobject/svg/string_mobject.py b/manimlib/mobject/svg/string_mobject.py index e4376a33..ca324216 100644 --- a/manimlib/mobject/svg/string_mobject.py +++ b/manimlib/mobject/svg/string_mobject.py @@ -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, TypeVar, Union ManimColor = Union[str, Color] Span = tuple[int, int] @@ -66,6 +66,11 @@ class StringMobject(SVGMobject, ABC): "isolate": (), } + CMD_PATTERN: str | None = None + FLAG_DICT: dict[str, int] = {} + CONTENT_REPL: dict[str, str | Callable[[re.Match], str]] = {} + MATCH_REPL: dict[str, str | Callable[[re.Match], str]] = {} + def __init__(self, string: str, **kwargs): self.string = string digest_config(self, kwargs) @@ -153,21 +158,18 @@ 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 @@ -191,24 +193,59 @@ class StringMobject(SVGMobject, ABC): result.extend(spans) return result - @staticmethod - def get_neighbouring_pairs(vals: Sequence[T]) -> list[tuple[T, T]]: - return list(zip(vals[:-1], vals[1:])) + def get_substr(self, span: Span) -> str: + return self.string[slice(*span)] @staticmethod - def compress_neighbours(vals: Sequence[T]) -> list[tuple[T, Span]]: + def get_substr_matched_obj( + substr: str, match_dict: dict[str, T] + ) -> tuple[re.Match, T] | None: + for pattern, val in match_dict.items(): + match_obj = re.fullmatch(pattern, substr, re.S) + if match_obj is None: + continue + return match_obj, val + return None + + @staticmethod + def get_substr_matched_val( + substr: str, match_dict: dict[str, T], default: T + ) -> T: + obj = StringMobject.get_substr_matched_obj(substr, match_dict) + if obj is None: + return default + _, val = obj + return val + + @staticmethod + def get_substr_matched_str( + substr: str, match_dict: dict[str, str | Callable[[re.Match], str]] + ) -> str: + obj = StringMobject.get_substr_matched_obj(substr, match_dict) + if obj is None: + return substr + match_obj, val = obj + if isinstance(val, str): + return val + return val(match_obj) + + @staticmethod + def get_neighbouring_pairs(vals: Iterable[T]) -> list[tuple[T, T]]: + val_list = list(vals) + return list(zip(val_list[:-1], val_list[1:])) + + @staticmethod + def group_neighbours(vals: Iterable[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) + unique_vals, range_lens = zip(*( + (val, len(list(grouper))) + for val, grouper in it.groupby(vals) + )) + val_ranges = StringMobject.get_neighbouring_pairs( + [0, *it.accumulate(range_lens)] + ) return list(zip(unique_vals, val_ranges)) @staticmethod @@ -228,18 +265,6 @@ class StringMobject(SVGMobject, ABC): (*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,12 +280,26 @@ class StringMobject(SVGMobject, ABC): # Parsing def parse(self) -> None: - cmd_spans = self.get_cmd_spans() + pattern = self.CMD_PATTERN + cmd_spans = [] if pattern is None else [ + match_obj.span() + for match_obj in re.finditer(pattern, self.string, re.S) + ] 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) - ) + flags = [ + self.get_substr_matched_val(substr, self.FLAG_DICT, 0) + for substr in cmd_substrs + ] + specified_items = [ + *self.get_internal_specified_items( + self.get_cmd_span_pairs(cmd_spans, flags) + ), + *self.get_external_specified_items(), + *[ + (span, {}) + for span in self.find_spans_by_selector(self.isolate) + ] + ] split_items = [ (span, attr_dict) for specified_span, attr_dict in specified_items @@ -273,31 +312,15 @@ class StringMobject(SVGMobject, ABC): 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)) + (span, self.get_substr_matched_str(substr, self.CONTENT_REPL)) for span, substr in zip(cmd_spans, cmd_substrs) ] self.cmd_repl_items_for_matching = [ - (span, self.get_repl_substr_for_matching(substr)) + (span, self.get_substr_matched_str(substr, self.MATCH_REPL)) for span, substr in zip(cmd_spans, cmd_substrs) ] self.check_overlapping() - @abstractmethod - def get_cmd_spans(self) -> list[Span]: - return [] - - @abstractmethod - def get_substr_flag(self, substr: str) -> int: - return 0 - - @abstractmethod - def get_repl_substr_for_content(self, substr: str) -> str: - return "" - - @abstractmethod - def get_repl_substr_for_matching(self, substr: str) -> str: - return "" - @staticmethod def get_cmd_span_pairs( cmd_spans: list[Span], flags: list[int] @@ -317,11 +340,17 @@ class StringMobject(SVGMobject, ABC): return result @abstractmethod - def get_specified_items( + def get_internal_specified_items( self, cmd_span_pairs: list[tuple[Span, Span]] ) -> list[tuple[Span, dict[str, str]]]: return [] + @abstractmethod + def get_external_specified_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]: @@ -387,6 +416,18 @@ class StringMobject(SVGMobject, ABC): ) -> tuple[str, str]: return "", "" + 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))) + def get_content(self, is_labelled: bool) -> str: inserted_str_pairs = [ (span, self.get_cmd_str_pair( @@ -446,7 +487,7 @@ class StringMobject(SVGMobject, ABC): return [] group_labels, labelled_submob_ranges = zip( - *self.compress_neighbours(self.labels) + *self.group_neighbours(self.labels) ) ordered_spans = [ self.labelled_spans[label] if label != -1 else self.full_span diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index 01623aa3..adfe05c0 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -122,7 +122,8 @@ class SVGMobject(VMobject): if k in style_keys } - new_root = ET.Element("svg", {}) # TODO: width, height + # Ignore other attributes in case that svgelements cannot parse them + new_root = ET.Element("svg", {}) config_style_node = ET.SubElement(new_root, "g", config_style_attrs) root_style_node = ET.SubElement(config_style_node, "g", style_attrs) root_style_node.extend(root) diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index 474ac7e3..a79d5375 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -61,9 +61,8 @@ class _Alignment: self.value = _Alignment.VAL_DICT[s.upper()] -class MarkupText(StringMobject): +class Text(StringMobject): CONFIG = { - "is_markup": True, "font_size": 48, "lsh": None, "justify": False, @@ -85,28 +84,16 @@ class MarkupText(StringMobject): "isolate": (re.compile(r"[a-zA-Z]+"), re.compile(r"\S+")), } - # 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"}, - "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"}, + CMD_PATTERN = r"""[<>&"']""" + FLAG_DICT = {} + CONTENT_REPL = { + r"<": "<", + r">": ">", + r"&": "&", + r"\"": """, + r"'": "'" } + MATCH_REPL = {} def __init__(self, text: str, **kwargs): self.full2short(kwargs) @@ -116,8 +103,6 @@ class MarkupText(StringMobject): 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) @@ -141,7 +126,6 @@ class MarkupText(StringMobject): self.base_color, self.isolate, self.text, - self.is_markup, self.font_size, self.lsh, self.justify, @@ -231,73 +215,15 @@ class MarkupText(StringMobject): # Parsing - def get_cmd_spans(self) -> list[Span]: - if not self.is_markup: - return self.find_spans(r"""[<>&"']""") - - # Unsupported passthroughs: - # "", "", "", "" - # See https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/gmarkup.c - return self.find_spans( - r"""&[\s\S]*?;|[>"']|""" - ) - - def get_substr_flag(self, substr: str) -> int: - if re.fullmatch(r"<\w[\s\S]*[^/]>", substr): - return 1 - if substr.startswith(" str: - if substr.startswith("<") and substr.endswith(">"): - return "" - return { - "<": "<", - ">": ">", - "&": "&", - "\"": """, - "'": "'" - }.get(substr, substr) - - def get_repl_substr_for_matching(self, substr: str) -> str: - if substr.startswith("<") and substr.endswith(">"): - 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 { - "<": "<", - ">": ">", - "&": "&", - """: "\"", - "'": "'" - }.get(substr, substr) - - def get_specified_items( + def get_internal_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) - ) + return [] + def get_external_specified_items( + self + ) -> list[tuple[Span, dict[str, str]]]: return [ - *internal_items, *[ (span, {key: val}) for t2x_dict, key in ( @@ -313,10 +239,6 @@ class MarkupText(StringMobject): (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) ] ] @@ -327,11 +249,13 @@ class MarkupText(StringMobject): 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([ @@ -376,8 +300,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) @@ -391,10 +315,70 @@ class MarkupText(StringMobject): return self.get_string() -class Text(MarkupText): - CONFIG = { - "is_markup": False, +class MarkupText(Text): + # Unsupported passthroughs: + # "", "", "", "" + # See https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/gmarkup.c + CMD_PATTERN = r"""|&.*?;|[>"']""" + FLAG_DICT = { + r"": -1, + r"<.*/>": 0, + r"<.*>": 1 } + CONTENT_REPL = { + r">": ">", + r"\"": """, + r"'": "'" + } + MATCH_REPL = { + r"<.*>": "", + r"&#x(.*);": lambda m: chr(int(m.group(1), 16)), + r"&#(.*);": lambda m: chr(int(m.group(1), 10)), + r"<": "<", + r">": ">", + r"&": "&", + r""": "\"", + r"'": "'" + } + + # See https://docs.gtk.org/Pango/pango_markup.html + MARKUP_TAGS = { + "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, **kwargs): + self.validate_markup_string(text) + super().__init__(text, **kwargs) + + def get_internal_specified_items( + self, cmd_span_pairs: list[tuple[Span, Span]] + ) -> list[tuple[Span, dict[str, str]]]: + attr_pattern = r"""(\w+)\s*\=\s*(["'])(.*?)\2""" + result = [] + for begin_cmd_span, end_cmd_span in cmd_span_pairs: + begin_tag = self.get_substr(begin_cmd_span) + tag_name = re.search(r"\w+", begin_tag).group() + 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, re.S + ) + } + else: + attr_dict = self.MARKUP_TAGS.get(tag_name, {}) + result.append( + ((begin_cmd_span[1], end_cmd_span[0]), attr_dict) + ) + return result class Code(MarkupText): diff --git a/manimlib/tex_fonts.yml b/manimlib/tex_fonts.yml index b380428b..51a2d60e 100644 --- a/manimlib/tex_fonts.yml +++ b/manimlib/tex_fonts.yml @@ -1,552 +1,440 @@ -default: - 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} +default: |- + \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: - 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} +ctex: |- + \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} -blank: - compiler: latex - preamble: |- - \usepackage{xcolor} +basic: |- + \usepackage[english]{babel} + \usepackage{amsmath} + \usepackage{amssymb} + \usepackage{xcolor} -basic: - compiler: latex - preamble: |- - \usepackage[english]{babel} - \usepackage{amsmath} - \usepackage{amssymb} - \usepackage{xcolor} +# A collection of TeX templates for the fonts described at +# http://jf.burnol.free.fr/showcase.html # American Typewriter -american_typewriter: - compiler: xelatex - preamble: |- - \usepackage[no-math]{fontspec} - \setmainfont[Mapping=tex-text]{American Typewriter} - \usepackage[defaultmathsizes]{mathastext} +american_typewriter: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{American Typewriter} + \usepackage[defaultmathsizes]{mathastext} # Antykwa Poltawskiego (TX Fonts for Greek and math symbols) -antykwa: - compiler: latex - preamble: |- - \usepackage[OT4,OT1]{fontenc} - \usepackage{txfonts} - \usepackage[upright]{txgreeks} - \usepackage{antpolt} - \usepackage[defaultmathsizes,nolessnomore]{mathastext} +antykwa: |- + \usepackage[OT4,OT1]{fontenc} + \usepackage{txfonts} + \usepackage[upright]{txgreeks} + \usepackage{antpolt} + \usepackage[defaultmathsizes,nolessnomore]{mathastext} # Apple Chancery -apple_chancery: - compiler: xelatex - preamble: |- - \usepackage[no-math]{fontspec} - \setmainfont[Mapping=tex-text]{Apple Chancery} - \usepackage[defaultmathsizes]{mathastext} +apple_chancery: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Apple Chancery} + \usepackage[defaultmathsizes]{mathastext} # Auriocus Kalligraphicus (Symbol Greek) -auriocus_kalligraphicus: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage{aurical} - \renewcommand{\rmdefault}{AuriocusKalligraphicus} - \usepackage[symbolgreek]{mathastext} +auriocus_kalligraphicus: |- + \usepackage[T1]{fontenc} + \usepackage{aurical} + \renewcommand{\rmdefault}{AuriocusKalligraphicus} + \usepackage[symbolgreek]{mathastext} # Baskervald ADF with Fourier -baskervald_adf_fourier: - compiler: latex - preamble: |- - \usepackage[upright]{fourier} - \usepackage{baskervald} - \usepackage[defaultmathsizes,noasterisk]{mathastext} +baskervald_adf_fourier: |- + \usepackage[upright]{fourier} + \usepackage{baskervald} + \usepackage[defaultmathsizes,noasterisk]{mathastext} # Baskerville (Italic) -baskerville_it: - compiler: xelatex - preamble: |- - \usepackage[no-math]{fontspec} - \setmainfont[Mapping=tex-text]{Baskerville} - \usepackage[defaultmathsizes,italic]{mathastext} +baskerville_it: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Baskerville} + \usepackage[defaultmathsizes,italic]{mathastext} # Biolinum -biolinum: - compiler: latex - preamble: |- - \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 +biolinum: |- + \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-Italic (PX math and Greek) -brushscriptx: - compiler: xelatex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage{pxfonts} - \renewcommand{\rmdefault}{pbsi} - \renewcommand{\mddefault}{xl} - \renewcommand{\bfdefault}{xl} - \usepackage[defaultmathsizes,noasterisk]{mathastext} - \boldmath +brushscriptx: |- + \usepackage[T1]{fontenc} + \usepackage{pxfonts} + \renewcommand{\rmdefault}{pbsi} + \renewcommand{\mddefault}{xl} + \renewcommand{\bfdefault}{xl} + \usepackage[defaultmathsizes,noasterisk]{mathastext} + \boldmath # Chalkboard SE -chalkboard_se: - compiler: xelatex - preamble: |- - \usepackage[no-math]{fontspec} - \setmainfont[Mapping=tex-text]{Chalkboard SE} - \usepackage[defaultmathsizes]{mathastext} +chalkboard_se: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Chalkboard SE} + \usepackage[defaultmathsizes]{mathastext} # Chalkduster -chalkduster: - compiler: xelatex - preamble: |- - \usepackage[no-math]{fontspec} - \setmainfont[Mapping=tex-text]{Chalkduster} - \usepackage[defaultmathsizes]{mathastext} +chalkduster: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Chalkduster} + \usepackage[defaultmathsizes]{mathastext} # Comfortaa -comfortaa: - compiler: latex - preamble: |- - \usepackage[default]{comfortaa} - \usepackage[LGRgreek,defaultmathsizes,noasterisk]{mathastext} - \let\varphi\phi - \linespread{1.06} +comfortaa: |- + \usepackage[default]{comfortaa} + \usepackage[LGRgreek,defaultmathsizes,noasterisk]{mathastext} + \let\varphi\phi + \linespread{1.06} # Comic Sans MS -comic_sans: - compiler: xelatex - preamble: |- - \usepackage[no-math]{fontspec} - \setmainfont[Mapping=tex-text]{Comic Sans MS} - \usepackage[defaultmathsizes]{mathastext} +comic_sans: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Comic Sans MS} + \usepackage[defaultmathsizes]{mathastext} # Droid Sans -droid_sans: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage[default]{droidsans} - \usepackage[LGRgreek]{mathastext} - \let\varepsilon\epsilon +droid_sans: |- + \usepackage[T1]{fontenc} + \usepackage[default]{droidsans} + \usepackage[LGRgreek]{mathastext} + \let\varepsilon\epsilon # Droid Sans (Italic) -droid_sans_it: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage[default]{droidsans} - \usepackage[LGRgreek,defaultmathsizes,italic]{mathastext} - \let\varphi\phi +droid_sans_it: |- + \usepackage[T1]{fontenc} + \usepackage[default]{droidsans} + \usepackage[LGRgreek,defaultmathsizes,italic]{mathastext} + \let\varphi\phi # Droid Serif -droid_serif: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage[default]{droidserif} - \usepackage[LGRgreek]{mathastext} - \let\varepsilon\epsilon +droid_serif: |- + \usepackage[T1]{fontenc} + \usepackage[default]{droidserif} + \usepackage[LGRgreek]{mathastext} + \let\varepsilon\epsilon # Droid Serif (PX math symbols) (Italic) -droid_serif_px_it: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage{pxfonts} - \usepackage[default]{droidserif} - \usepackage[LGRgreek,defaultmathsizes,italic,basic]{mathastext} - \let\varphi\phi +droid_serif_px_it: |- + \usepackage[T1]{fontenc} + \usepackage{pxfonts} + \usepackage[default]{droidserif} + \usepackage[LGRgreek,defaultmathsizes,italic,basic]{mathastext} + \let\varphi\phi # ECF Augie (Euler Greek) -ecf_augie: - compiler: latex - preamble: |- - \renewcommand\familydefault{fau} - \usepackage[defaultmathsizes,eulergreek]{mathastext} +ecf_augie: |- + \renewcommand\familydefault{fau} + \usepackage[defaultmathsizes,eulergreek]{mathastext} # ECF JD (with TX fonts) -ecf_jd: - compiler: latex - preamble: |- - \usepackage{txfonts} - \usepackage[upright]{txgreeks} - \renewcommand\familydefault{fjd} - \usepackage{mathastext} - \mathversion{bold} +ecf_jd: |- + \usepackage{txfonts} + \usepackage[upright]{txgreeks} + \renewcommand\familydefault{fjd} + \usepackage{mathastext} + \mathversion{bold} # ECF Skeetch (CM Greek) -ecf_skeetch: - compiler: latex - preamble: |- - \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_skeetch: |- + \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 (with Symbol font) -ecf_tall_paul: - compiler: latex - preamble: |- - \DeclareFontFamily{T1}{ftp}{} - \DeclareFontShape{T1}{ftp}{m}{n}{<->s*[1.4] ftpmw8t}{} - \renewcommand\familydefault{ftp} - \usepackage[symbol]{mathastext} - \let\infty\inftypsy +ecf_tall_paul: |- + \DeclareFontFamily{T1}{ftp}{} + \DeclareFontShape{T1}{ftp}{m}{n}{<->s*[1.4] ftpmw8t}{} + \renewcommand\familydefault{ftp} + \usepackage[symbol]{mathastext} + \let\infty\inftypsy # ECF Webster (with TX fonts) -ecf_webster: - compiler: xelatex - preamble: |- - \usepackage{txfonts} - \usepackage[upright]{txgreeks} - \renewcommand\familydefault{fwb} - \usepackage{mathastext} - \renewcommand{\int}{\intop\limits} - \linespread{1.5} - \mathversion{bold} +ecf_webster: |- + \usepackage{txfonts} + \usepackage[upright]{txgreeks} + \renewcommand\familydefault{fwb} + \usepackage{mathastext} + \renewcommand{\int}{\intop\limits} + \linespread{1.5} + \mathversion{bold} # Electrum ADF (CM Greek) -electrum_adf: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage[LGRgreek,basic,defaultmathsizes]{mathastext} - \usepackage[lf]{electrum} - \Mathastext - \let\varphi\phi +electrum_adf: |- + \usepackage[T1]{fontenc} + \usepackage[LGRgreek,basic,defaultmathsizes]{mathastext} + \usepackage[lf]{electrum} + \Mathastext + \let\varphi\phi # Epigrafica -epigrafica: - compiler: latex - preamble: |- - \usepackage[LGR,OT1]{fontenc} - \usepackage{epigrafica} - \usepackage[basic,LGRgreek,defaultmathsizes]{mathastext} - \let\varphi\phi - \linespread{1.2} +epigrafica: |- + \usepackage[LGR,OT1]{fontenc} + \usepackage{epigrafica} + \usepackage[basic,LGRgreek,defaultmathsizes]{mathastext} + \let\varphi\phi + \linespread{1.2} # Fourier Utopia (Fourier upright Greek) -fourier_utopia: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage[upright]{fourier} - \usepackage{mathastext} +fourier_utopia: |- + \usepackage[T1]{fontenc} + \usepackage[upright]{fourier} + \usepackage{mathastext} # French Cursive (Euler Greek) -french_cursive: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage[default]{frcursive} - \usepackage[eulergreek,noplusnominus,noequal,nohbar,nolessnomore,noasterisk]{mathastext} +french_cursive: |- + \usepackage[T1]{fontenc} + \usepackage[default]{frcursive} + \usepackage[eulergreek,noplusnominus,noequal,nohbar,nolessnomore,noasterisk]{mathastext} # GFS Bodoni -gfs_bodoni: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \renewcommand{\rmdefault}{bodoni} - \usepackage[LGRgreek]{mathastext} - \let\varphi\phi - \linespread{1.06} +gfs_bodoni: |- + \usepackage[T1]{fontenc} + \renewcommand{\rmdefault}{bodoni} + \usepackage[LGRgreek]{mathastext} + \let\varphi\phi + \linespread{1.06} # GFS Didot (Italic) -gfs_didot: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \renewcommand\rmdefault{udidot} - \usepackage[LGRgreek,defaultmathsizes,italic]{mathastext} - \let\varphi\phi +gfs_didot: |- + \usepackage[T1]{fontenc} + \renewcommand\rmdefault{udidot} + \usepackage[LGRgreek,defaultmathsizes,italic]{mathastext} + \let\varphi\phi # GFS NeoHellenic -gfs_neoHellenic: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \renewcommand{\rmdefault}{neohellenic} - \usepackage[LGRgreek]{mathastext} - \let\varphi\phi - \linespread{1.06} +gfs_neoHellenic: |- + \usepackage[T1]{fontenc} + \renewcommand{\rmdefault}{neohellenic} + \usepackage[LGRgreek]{mathastext} + \let\varphi\phi + \linespread{1.06} # GNU FreeSerif (and TX fonts symbols) -gnu_freesans_tx: - compiler: xelatex - preamble: |- - \usepackage[no-math]{fontspec} - \usepackage{txfonts} - \setmainfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSerifBold,ItalicFont=FreeSerifItalic,BoldItalicFont=FreeSerifBoldItalic]{FreeSerif} - \usepackage[defaultmathsizes]{mathastext} +gnu_freesans_tx: |- + \usepackage[no-math]{fontspec} + \usepackage{txfonts} + \setmainfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSerifBold,ItalicFont=FreeSerifItalic,BoldItalicFont=FreeSerifBoldItalic]{FreeSerif} + \usepackage[defaultmathsizes]{mathastext} # GNU FreeSerif and FreeSans -gnu_freeserif_freesans: - compiler: xelatex - preamble: |- - \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} +gnu_freeserif_freesans: |- + \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 with Fourier (Italic) -helvetica_fourier_it: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage[scaled]{helvet} - \usepackage{fourier} - \renewcommand{\rmdefault}{phv} - \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} +helvetica_fourier_it: |- + \usepackage[T1]{fontenc} + \usepackage[scaled]{helvet} + \usepackage{fourier} + \renewcommand{\rmdefault}{phv} + \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} # Latin Modern Typewriter Proportional -latin_modern_tw: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage[variablett]{lmodern} - \renewcommand{\rmdefault}{\ttdefault} - \usepackage[LGRgreek]{mathastext} - \MTgreekfont{lmtt} - \Mathastext - \let\varepsilon\epsilon +latin_modern_tw: |- + \usepackage[T1]{fontenc} + \usepackage[variablett]{lmodern} + \renewcommand{\rmdefault}{\ttdefault} + \usepackage[LGRgreek]{mathastext} + \MTgreekfont{lmtt} + \Mathastext + \let\varepsilon\epsilon # Latin Modern Typewriter Proportional (CM Greek) (Italic) -latin_modern_tw_it: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage[variablett,nomath]{lmodern} - \renewcommand{\familydefault}{\ttdefault} - \usepackage[frenchmath]{mathastext} - \linespread{1.08} +latin_modern_tw_it: |- + \usepackage[T1]{fontenc} + \usepackage[variablett,nomath]{lmodern} + \renewcommand{\familydefault}{\ttdefault} + \usepackage[frenchmath]{mathastext} + \linespread{1.08} # Libertine -libertine: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage{libertine} - \usepackage[greek=n]{libgreek} - \usepackage[noasterisk,defaultmathsizes]{mathastext} +libertine: |- + \usepackage[T1]{fontenc} + \usepackage{libertine} + \usepackage[greek=n]{libgreek} + \usepackage[noasterisk,defaultmathsizes]{mathastext} # Libris ADF with Fourier -libris_adf_fourier: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage[upright]{fourier} - \usepackage{libris} - \renewcommand{\familydefault}{\sfdefault} - \usepackage[noasterisk]{mathastext} +libris_adf_fourier: |- + \usepackage[T1]{fontenc} + \usepackage[upright]{fourier} + \usepackage{libris} + \renewcommand{\familydefault}{\sfdefault} + \usepackage[noasterisk]{mathastext} # Minion Pro and Myriad Pro (and TX fonts symbols) -minion_pro_myriad_pro: - compiler: xelatex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage[default]{droidserif} - \usepackage[LGRgreek]{mathastext} - \let\varepsilon\epsilon +minion_pro_myriad_pro: |- + \usepackage[T1]{fontenc} + \usepackage[default]{droidserif} + \usepackage[LGRgreek]{mathastext} + \let\varepsilon\epsilon # Minion Pro (and TX fonts symbols) -minion_pro_tx: - compiler: xelatex - preamble: |- - \usepackage{txfonts} - \usepackage[no-math]{fontspec} - \setmainfont[Mapping=tex-text]{Minion Pro} - \usepackage[defaultmathsizes]{mathastext} +minion_pro_tx: |- + \usepackage{txfonts} + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Minion Pro} + \usepackage[defaultmathsizes]{mathastext} # New Century Schoolbook (Symbol Greek) -new_century_schoolbook: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage{newcent} - \usepackage[symbolgreek]{mathastext} - \linespread{1.1} +new_century_schoolbook: |- + \usepackage[T1]{fontenc} + \usepackage{newcent} + \usepackage[symbolgreek]{mathastext} + \linespread{1.1} # New Century Schoolbook (Symbol Greek, PX math symbols) -new_century_schoolbook_px: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage{pxfonts} - \usepackage{newcent} - \usepackage[symbolgreek,defaultmathsizes]{mathastext} - \linespread{1.06} +new_century_schoolbook_px: |- + \usepackage[T1]{fontenc} + \usepackage{pxfonts} + \usepackage{newcent} + \usepackage[symbolgreek,defaultmathsizes]{mathastext} + \linespread{1.06} # Noteworthy Light -noteworthy_light: - compiler: latex - preamble: |- - \usepackage[no-math]{fontspec} - \setmainfont[Mapping=tex-text]{Noteworthy Light} - \usepackage[defaultmathsizes]{mathastext} +noteworthy_light: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Noteworthy Light} + \usepackage[defaultmathsizes]{mathastext} # Palatino (Symbol Greek) -palatino: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage{palatino} - \usepackage[symbolmax,defaultmathsizes]{mathastext} +palatino: |- + \usepackage[T1]{fontenc} + \usepackage{palatino} + \usepackage[symbolmax,defaultmathsizes]{mathastext} # Papyrus -papyrus: - compiler: xelatex - preamble: |- - \usepackage[no-math]{fontspec} - \setmainfont[Mapping=tex-text]{Papyrus} - \usepackage[defaultmathsizes]{mathastext} +papyrus: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Papyrus} + \usepackage[defaultmathsizes]{mathastext} # Romande ADF with Fourier (Italic) -romande_adf_fourier_it: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage{fourier} - \usepackage{romande} - \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} - \renewcommand{\itshape}{\swashstyle} +romande_adf_fourier_it: |- + \usepackage[T1]{fontenc} + \usepackage{fourier} + \usepackage{romande} + \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} + \renewcommand{\itshape}{\swashstyle} # SliTeX (Euler Greek) -slitex: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage{tpslifonts} - \usepackage[eulergreek,defaultmathsizes]{mathastext} - \MTEulerScale{1.06} - \linespread{1.2} +slitex: |- + \usepackage[T1]{fontenc} + \usepackage{tpslifonts} + \usepackage[eulergreek,defaultmathsizes]{mathastext} + \MTEulerScale{1.06} + \linespread{1.2} # Times with Fourier (Italic) -times_fourier_it: - compiler: latex - preamble: |- - \usepackage{fourier} - \renewcommand{\rmdefault}{ptm} - \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} +times_fourier_it: |- + \usepackage{fourier} + \renewcommand{\rmdefault}{ptm} + \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} # URW Avant Garde (Symbol Greek) -urw_avant_garde: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage{avant} - \renewcommand{\familydefault}{\sfdefault} - \usepackage[symbolgreek,defaultmathsizes]{mathastext} +urw_avant_garde: |- + \usepackage[T1]{fontenc} + \usepackage{avant} + \renewcommand{\familydefault}{\sfdefault} + \usepackage[symbolgreek,defaultmathsizes]{mathastext} # URW Zapf Chancery (CM Greek) -urw_zapf_chancery: - compiler: latex - preamble: |- - \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 +urw_zapf_chancery: |- + \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 with Fourier (Italic) -venturis_adf_fourier_it: - compiler: latex - preamble: |- - \usepackage{fourier} - \usepackage[lf]{venturis} - \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} +venturis_adf_fourier_it: |- + \usepackage{fourier} + \usepackage[lf]{venturis} + \usepackage[italic,defaultmathsizes,noasterisk]{mathastext} # Verdana (Italic) -verdana_it: - compiler: xelatex - preamble: |- - \usepackage[no-math]{fontspec} - \setmainfont[Mapping=tex-text]{Verdana} - \usepackage[defaultmathsizes,italic]{mathastext} +verdana_it: |- + \usepackage[no-math]{fontspec} + \setmainfont[Mapping=tex-text]{Verdana} + \usepackage[defaultmathsizes,italic]{mathastext} # Vollkorn (TX fonts for Greek and math symbols) -vollkorn: - compiler: latex - preamble: |- - \usepackage[T1]{fontenc} - \usepackage{txfonts} - \usepackage[upright]{txgreeks} - \usepackage{vollkorn} - \usepackage[defaultmathsizes]{mathastext} +vollkorn: |- + \usepackage[T1]{fontenc} + \usepackage{txfonts} + \usepackage[upright]{txgreeks} + \usepackage{vollkorn} + \usepackage[defaultmathsizes]{mathastext} # Vollkorn with Fourier (Italic) -vollkorn_fourier_it: - compiler: latex - preamble: |- - \usepackage{fourier} - \usepackage{vollkorn} - \usepackage[italic,nohbar]{mathastext} +vollkorn_fourier_it: |- + \usepackage{fourier} + \usepackage{vollkorn} + \usepackage[italic,nohbar]{mathastext} # Zapf Chancery -zapf_chancery: - compiler: latex - preamble: |- - \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} +zapf_chancery: |- + \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} diff --git a/manimlib/utils/init_config.py b/manimlib/utils/init_config.py index f71d4039..41d9faf5 100644 --- a/manimlib/utils/init_config.py +++ b/manimlib/utils/init_config.py @@ -44,8 +44,9 @@ def init_customization() -> None: }, "universal_import_line": "from manimlib import *", "style": { + "tex_compiler": "", "tex_font": "", - "font": "", + "font": "Consolas", "background_color": "", }, "window_position": "UR", @@ -57,7 +58,7 @@ def init_customization() -> None: "medium": "1280x720", "high": "1920x1080", "4k": "3840x2160", - "default_resolution": "high", + "default_resolution": "", }, "fps": 30, } @@ -106,14 +107,16 @@ def init_customization() -> None: console.print("[bold]Styles:[/bold]") style_config = configuration["style"] - style_config["tex_font"] = Prompt.ask( - " Which [bold]font[/bold] for LaTeX do you want", - default="default" - ) - style_config["font"] = Prompt.ask( - " Which [bold]font[/bold] for non-LaTeX text do you want", - default="Consolas" + compiler = Prompt.ask( + " Select an executable program to use to compile a LaTeX source file", + choices=["latex", "xelatex"], + default="latex" ) + style_config["tex_compiler"] = compiler + if compiler == "latex": + style_config["tex_font"] = "default" + else: + style_config["tex_font"] = "ctex" style_config["background_color"] = Prompt.ask( " Which [bold]background color[/bold] do you want [italic](hex code)", default="#333333" @@ -127,7 +130,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" diff --git a/manimlib/utils/tex_file_writing.py b/manimlib/utils/tex_file_writing.py index 2da51caa..00a95a52 100644 --- a/manimlib/utils/tex_file_writing.py +++ b/manimlib/utils/tex_file_writing.py @@ -15,14 +15,7 @@ from manimlib.utils.simple_functions import hash_string SAVED_TEX_CONFIG = {} -def get_tex_font_config(tex_font: str) -> str: - """ - Returns a dict which should look something like this: - { - "compiler": "latex", - "preamble": "..." - } - """ +def get_tex_font_preamble(tex_font: str) -> str: name = re.sub(r"[^a-zA-Z]", "_", tex_font).lower() with open(os.path.join( get_manim_dir(), "manimlib", "tex_fonts.yml" @@ -35,30 +28,40 @@ def get_tex_font_config(tex_font: str) -> str: ) name = "default" result = templates_dict[name] - if name not in ("default", "ctex", "blank", "basic"): - result["preamble"] = "\n".join(( - templates_dict["basic"]["preamble"], result["preamble"] - )) + if name not in ("default", "ctex", "basic"): + result = templates_dict["basic"] + "\n" + result return result def get_tex_config() -> dict[str, str]: + """ + Returns a dict which should look something like this: + { + "compiler": "latex", + "font": "default", + "preamble": "..." + } + """ # Only load once, then save thereafter if not SAVED_TEX_CONFIG: - tex_font = get_custom_config()["style"]["tex_font"] - SAVED_TEX_CONFIG.update(get_tex_font_config(tex_font)) + style_config = get_custom_config()["style"] + SAVED_TEX_CONFIG.update({ + "compiler": style_config["tex_compiler"], + "font": style_config["tex_font"], + "preamble": get_tex_font_preamble(style_config["tex_font"]) + }) return SAVED_TEX_CONFIG def tex_content_to_svg_file( content: str, tex_font: str, additional_preamble: str ) -> str: - if not tex_font: - tex_config = get_tex_config() + tex_config = get_tex_config() + if not tex_font or tex_font == tex_config["font"]: + preamble = tex_config["preamble"] else: - tex_config = get_tex_font_config(tex_font) + preamble = get_tex_font_preamble(tex_font) - preamble = tex_config["preamble"] if additional_preamble: preamble += "\n" + additional_preamble full_tex = "\n\n".join(( From bf530db2ed66113a7925cc138cb879132febce22 Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sat, 28 May 2022 22:01:36 +0800 Subject: [PATCH 07/27] Add ctex_basic template --- manimlib/tex_fonts.yml | 6 ++++++ manimlib/utils/tex_file_writing.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/manimlib/tex_fonts.yml b/manimlib/tex_fonts.yml index 51a2d60e..b20caef6 100644 --- a/manimlib/tex_fonts.yml +++ b/manimlib/tex_fonts.yml @@ -45,6 +45,12 @@ basic: |- \usepackage{amssymb} \usepackage{xcolor} +ctex_basic: |- + \usepackage[UTF8]{ctex} + \usepackage{amsmath} + \usepackage{amssymb} + \usepackage{xcolor} + # A collection of TeX templates for the fonts described at # http://jf.burnol.free.fr/showcase.html diff --git a/manimlib/utils/tex_file_writing.py b/manimlib/utils/tex_file_writing.py index 00a95a52..4a30c03a 100644 --- a/manimlib/utils/tex_file_writing.py +++ b/manimlib/utils/tex_file_writing.py @@ -28,7 +28,7 @@ def get_tex_font_preamble(tex_font: str) -> str: ) name = "default" result = templates_dict[name] - if name not in ("default", "ctex", "basic"): + if name not in ("default", "ctex", "basic", "ctex_basic"): result = templates_dict["basic"] + "\n" + result return result From 3faa21caddb08e795b6612fe36bbc313ef7f5994 Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sat, 28 May 2022 22:44:30 +0800 Subject: [PATCH 08/27] Add blank template --- manimlib/tex_fonts.yml | 2 ++ manimlib/utils/tex_file_writing.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/manimlib/tex_fonts.yml b/manimlib/tex_fonts.yml index b20caef6..895692c3 100644 --- a/manimlib/tex_fonts.yml +++ b/manimlib/tex_fonts.yml @@ -51,6 +51,8 @@ ctex_basic: |- \usepackage{amssymb} \usepackage{xcolor} +blank: "" + # A collection of TeX templates for the fonts described at # http://jf.burnol.free.fr/showcase.html diff --git a/manimlib/utils/tex_file_writing.py b/manimlib/utils/tex_file_writing.py index 4a30c03a..077d15cc 100644 --- a/manimlib/utils/tex_file_writing.py +++ b/manimlib/utils/tex_file_writing.py @@ -28,7 +28,7 @@ def get_tex_font_preamble(tex_font: str) -> str: ) name = "default" result = templates_dict[name] - if name not in ("default", "ctex", "basic", "ctex_basic"): + if name not in ("default", "ctex", "basic", "ctex_basic", "blank"): result = templates_dict["basic"] + "\n" + result return result From bc939fdd5b7e53a45fcba651b6bf231faf16804e Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sat, 28 May 2022 23:18:56 +0800 Subject: [PATCH 09/27] Rename font to template --- manimlib/default_config.yml | 2 +- manimlib/mobject/svg/mtex_mobject.py | 6 +++--- manimlib/mobject/svg/tex_mobject.py | 6 +++--- manimlib/{tex_fonts.yml => tex_templates.yml} | 2 +- manimlib/utils/init_config.py | 6 +++--- manimlib/utils/tex_file_writing.py | 20 +++++++++---------- 6 files changed, 21 insertions(+), 21 deletions(-) rename manimlib/{tex_fonts.yml => tex_templates.yml} (99%) diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index 59a88945..0b429b04 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -20,7 +20,7 @@ universal_import_line: "from manimlib import *" style: # "latex" | "xelatex" tex_compiler: "latex" - tex_font: "default" + tex_template: "default" font: "Consolas" text_alignment: "LEFT" background_color: "#333333" diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 055fc1b6..0ae81092 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -36,7 +36,7 @@ class MTex(StringMobject): "alignment": "\\centering", "tex_environment": "align*", "tex_to_color_map": {}, - "font": "", + "template": "", "additional_preamble": "", } @@ -72,14 +72,14 @@ class MTex(StringMobject): self.alignment, self.tex_environment, self.tex_to_color_map, - self.font, + self.template, self.additional_preamble ) def get_file_path_by_content(self, content: str) -> str: with display_during_execution(f"Writing \"{self.tex_string}\""): file_path = tex_content_to_svg_file( - content, self.font, self.additional_preamble + content, self.template, self.additional_preamble ) return file_path diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 0906fc03..3c5e2ea1 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -43,7 +43,7 @@ class SingleStringTex(SVGMobject): "alignment": "\\centering", "math_mode": True, "organize_left_to_right": False, - "font": "", + "template": "", "additional_preamble": "", } @@ -66,7 +66,7 @@ class SingleStringTex(SVGMobject): self.tex_string, self.alignment, self.math_mode, - self.font, + self.template, self.additional_preamble ) @@ -74,7 +74,7 @@ class SingleStringTex(SVGMobject): content = self.get_tex_file_body(self.tex_string) with display_during_execution(f"Writing \"{self.tex_string}\""): file_path = tex_content_to_svg_file( - content, self.font, self.additional_preamble + content, self.template, self.additional_preamble ) return file_path diff --git a/manimlib/tex_fonts.yml b/manimlib/tex_templates.yml similarity index 99% rename from manimlib/tex_fonts.yml rename to manimlib/tex_templates.yml index 895692c3..6c6b5ef4 100644 --- a/manimlib/tex_fonts.yml +++ b/manimlib/tex_templates.yml @@ -51,7 +51,7 @@ ctex_basic: |- \usepackage{amssymb} \usepackage{xcolor} -blank: "" +none: "" # A collection of TeX templates for the fonts described at # http://jf.burnol.free.fr/showcase.html diff --git a/manimlib/utils/init_config.py b/manimlib/utils/init_config.py index 41d9faf5..975eaae7 100644 --- a/manimlib/utils/init_config.py +++ b/manimlib/utils/init_config.py @@ -45,7 +45,7 @@ def init_customization() -> None: "universal_import_line": "from manimlib import *", "style": { "tex_compiler": "", - "tex_font": "", + "tex_template": "", "font": "Consolas", "background_color": "", }, @@ -114,9 +114,9 @@ def init_customization() -> None: ) style_config["tex_compiler"] = compiler if compiler == "latex": - style_config["tex_font"] = "default" + style_config["tex_template"] = "default" else: - style_config["tex_font"] = "ctex" + style_config["tex_template"] = "ctex" style_config["background_color"] = Prompt.ask( " Which [bold]background color[/bold] do you want [italic](hex code)", default="#333333" diff --git a/manimlib/utils/tex_file_writing.py b/manimlib/utils/tex_file_writing.py index 077d15cc..1dc4b734 100644 --- a/manimlib/utils/tex_file_writing.py +++ b/manimlib/utils/tex_file_writing.py @@ -15,15 +15,15 @@ from manimlib.utils.simple_functions import hash_string SAVED_TEX_CONFIG = {} -def get_tex_font_preamble(tex_font: str) -> str: - name = re.sub(r"[^a-zA-Z]", "_", tex_font).lower() +def get_tex_preamble(template_name: str) -> str: + name = re.sub(r"[^a-zA-Z]", "_", template_name).lower() with open(os.path.join( - get_manim_dir(), "manimlib", "tex_fonts.yml" + 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 font '%s', falling back to 'default'.", + "Cannot recognize template '%s', falling back to 'default'.", name ) name = "default" @@ -38,7 +38,7 @@ def get_tex_config() -> dict[str, str]: Returns a dict which should look something like this: { "compiler": "latex", - "font": "default", + "template": "default", "preamble": "..." } """ @@ -47,20 +47,20 @@ def get_tex_config() -> dict[str, str]: style_config = get_custom_config()["style"] SAVED_TEX_CONFIG.update({ "compiler": style_config["tex_compiler"], - "font": style_config["tex_font"], - "preamble": get_tex_font_preamble(style_config["tex_font"]) + "template": style_config["tex_template"], + "preamble": get_tex_preamble(style_config["tex_template"]) }) return SAVED_TEX_CONFIG def tex_content_to_svg_file( - content: str, tex_font: str, additional_preamble: str + content: str, template: str, additional_preamble: str ) -> str: tex_config = get_tex_config() - if not tex_font or tex_font == tex_config["font"]: + if not template or template == tex_config["template"]: preamble = tex_config["preamble"] else: - preamble = get_tex_font_preamble(tex_font) + preamble = get_tex_preamble(template) if additional_preamble: preamble += "\n" + additional_preamble From 97ac8c9953a3ec4a1c15ef619658272f9b0fa199 Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sun, 29 May 2022 16:26:40 +0800 Subject: [PATCH 10/27] Expand use elements --- manimlib/mobject/svg/svg_mobject.py | 40 ++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index adfe05c0..99811788 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -22,6 +22,8 @@ from manimlib.utils.simple_functions import hash_string SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {} +SVG_XMLNS = "{http://www.w3.org/2000/svg}" +SVG_XLINK = "{http://www.w3.org/1999/xlink}" def _convert_point_to_3d(x: float, y: float) -> np.ndarray: @@ -106,6 +108,8 @@ class SVGMobject(VMobject): return get_full_vector_image_path(self.file_name) def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree: + element_tree = self.expand_use_elements(element_tree) + config_style_attrs = self.generate_config_style_dict() style_keys = ( "fill", @@ -123,12 +127,42 @@ class SVGMobject(VMobject): } # Ignore other attributes in case that svgelements cannot parse them - new_root = ET.Element("svg", {}) - config_style_node = ET.SubElement(new_root, "g", config_style_attrs) - root_style_node = ET.SubElement(config_style_node, "g", style_attrs) + 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) + @staticmethod + def expand_use_elements(element_tree: ET.ElementTree) -> ET.ElementTree: + # Replace `use` elements with copies of elements they refer to + xpath = f".//{SVG_XMLNS}use[@{SVG_XLINK}href]" + element = element_tree.find(xpath) + while element is not None: + element.tag = f"{SVG_XMLNS}g" + attrs = element.attrib + href_str = attrs.pop(f"{SVG_XLINK}href")[1:] + href_element = element_tree.find(f".//{SVG_XMLNS}*[@id='{href_str}']") + if href_element is None: + continue + attrs.pop("width", None) + attrs.pop("height", None) + x = attrs.pop("x", "0") + y = attrs.pop("y", "0") + if not x == y == "0": + translate_str = f"translate({x}, {y})" + if "transform" in attrs: + attrs["transform"] = translate_str + " " + attrs["transform"] + else: + attrs["transform"] = translate_str + shadow_node = ET.SubElement(element, href_element.tag, href_element.attrib) + shadow_node.extend(href_element) + element = element_tree.find(xpath) + + for defs_element in element_tree.iterfind(f".//{SVG_XMLNS}defs"): + defs_element.clear() + return element_tree + def generate_config_style_dict(self) -> dict[str, str]: keys_converting_dict = { "fill": ("color", "fill_color"), From a73bd5d4feb10c4498b26acb831285e47bd0e5ef Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sun, 29 May 2022 16:48:30 +0800 Subject: [PATCH 11/27] Fix popping bug --- manimlib/mobject/svg/svg_mobject.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index 99811788..5754ac1b 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -136,13 +136,15 @@ class SVGMobject(VMobject): @staticmethod def expand_use_elements(element_tree: ET.ElementTree) -> ET.ElementTree: # Replace `use` elements with copies of elements they refer to - xpath = f".//{SVG_XMLNS}use[@{SVG_XLINK}href]" - element = element_tree.find(xpath) - while element is not None: + while True: + element = element_tree.find(f".//{SVG_XMLNS}use[@{SVG_XLINK}href]") + if element is None: + break + element.tag = f"{SVG_XMLNS}g" attrs = element.attrib - href_str = attrs.pop(f"{SVG_XLINK}href")[1:] - href_element = element_tree.find(f".//{SVG_XMLNS}*[@id='{href_str}']") + href_id = attrs.pop(f"{SVG_XLINK}href")[1:] + href_element = element_tree.find(f".//{SVG_XMLNS}*[@id='{href_id}']") if href_element is None: continue attrs.pop("width", None) @@ -157,7 +159,6 @@ class SVGMobject(VMobject): attrs["transform"] = translate_str shadow_node = ET.SubElement(element, href_element.tag, href_element.attrib) shadow_node.extend(href_element) - element = element_tree.find(xpath) for defs_element in element_tree.iterfind(f".//{SVG_XMLNS}defs"): defs_element.clear() From 93265c7341ae7b8bb03d2aef53411352a9ba0637 Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Mon, 30 May 2022 10:19:52 +0800 Subject: [PATCH 12/27] Remove a twice-defined method --- manimlib/mobject/svg/svg_mobject.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index 5754ac1b..334c194d 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -205,7 +205,7 @@ 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 @@ -240,17 +240,6 @@ 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) From 093af347aa81cd745a8398145671d893fcc1b5ee Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sat, 11 Jun 2022 15:15:39 +0800 Subject: [PATCH 13/27] Refactor LabelledString and relevant classes --- manimlib/mobject/svg/mtex_mobject.py | 72 ++++++--- manimlib/mobject/svg/string_mobject.py | 203 ++++++++++--------------- manimlib/mobject/svg/text_mobject.py | 195 +++++++++++++++--------- 3 files changed, 257 insertions(+), 213 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 0ae81092..4548614f 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -40,15 +40,15 @@ class MTex(StringMobject): "additional_preamble": "", } - CMD_PATTERN = r"\\(?:[a-zA-Z]+|.)|[_^{}]" - FLAG_DICT = { - r"{": 1, - r"}": -1 - } - CONTENT_REPL = {} - MATCH_REPL = { - r"[_^{}]": "" - } + #CMD_PATTERN = r"\\(?:[a-zA-Z]+|.)|[_^{}]" + #FLAG_DICT = { + # r"{": 1, + # r"}": -1 + #} + #CONTENT_REPL = {} + #MATCH_REPL = { + # r"[_^{}]": "" + #} def __init__(self, tex_string: str, **kwargs): # Prevent from passing an empty string. @@ -85,23 +85,55 @@ class MTex(StringMobject): # Parsing + @staticmethod + def get_cmd_pattern() -> str | None: + return r"(\\(?:[a-zA-Z]+|.))|([_^])|([{}])" + + @staticmethod + def get_matched_flag(match_obj: re.Match) -> int: + substr = match_obj.group() + if match_obj.group(3): + if substr == "{": + return 1 + if substr == "}": + 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 not match_obj.group(1): + return "" + return match_obj.group() + + @staticmethod def get_internal_specified_items( - self, cmd_span_pairs: list[tuple[Span, Span]] + cmd_match_pairs: list[tuple[re.Match, re.Match]] ) -> list[tuple[Span, dict[str, str]]]: cmd_content_spans = [ - (span_begin, span_end) - for (_, span_begin), (span_end, _) in cmd_span_pairs + (begin_match.end(), end_match.start()) + for begin_match, end_match in cmd_match_pairs ] + #print(MTex.get_neighbouring_pairs(cmd_content_spans)) return [ - (cmd_content_spans[range_begin], {}) - for _, (range_begin, range_end) in self.group_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 span, next_span + in MTex.get_neighbouring_pairs(cmd_content_spans) + if span[0] == next_span[0] + 1 and span[1] == next_span[1] - 1 ] + #return [ + # (cmd_content_spans[range_begin], {}) + # for _, (range_begin, range_end) in self.group_neighbours([ + # (span_begin + index, span_end - index) + # for index, (span_begin, span_end) in enumerate( + # cmd_content_spans + # ) + # ]) + # if range_end - range_begin >= 2 + #] def get_external_specified_items( self diff --git a/manimlib/mobject/svg/string_mobject.py b/manimlib/mobject/svg/string_mobject.py index ca324216..f1d31eab 100644 --- a/manimlib/mobject/svg/string_mobject.py +++ b/manimlib/mobject/svg/string_mobject.py @@ -66,11 +66,6 @@ class StringMobject(SVGMobject, ABC): "isolate": (), } - CMD_PATTERN: str | None = None - FLAG_DICT: dict[str, int] = {} - CONTENT_REPL: dict[str, str | Callable[[re.Match], str]] = {} - MATCH_REPL: dict[str, str | Callable[[re.Match], str]] = {} - def __init__(self, string: str, **kwargs): self.string = string digest_config(self, kwargs) @@ -78,7 +73,7 @@ class StringMobject(SVGMobject, ABC): self.base_color = WHITE self.base_color_hex = self.color_to_hex(self.base_color) - self.full_span = (0, len(self.string)) + self.full_len = len(self.string) self.parse() super().__init__(**kwargs) self.labels = [submob.label for submob in self.submobjects] @@ -174,11 +169,11 @@ class StringMobject(SVGMobject, ABC): isinstance(index, int) or index is None for index in sel ): - l = self.full_span[1] + l = self.full_len 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 @@ -196,58 +191,11 @@ class StringMobject(SVGMobject, ABC): def get_substr(self, span: Span) -> str: return self.string[slice(*span)] - @staticmethod - def get_substr_matched_obj( - substr: str, match_dict: dict[str, T] - ) -> tuple[re.Match, T] | None: - for pattern, val in match_dict.items(): - match_obj = re.fullmatch(pattern, substr, re.S) - if match_obj is None: - continue - return match_obj, val - return None - - @staticmethod - def get_substr_matched_val( - substr: str, match_dict: dict[str, T], default: T - ) -> T: - obj = StringMobject.get_substr_matched_obj(substr, match_dict) - if obj is None: - return default - _, val = obj - return val - - @staticmethod - def get_substr_matched_str( - substr: str, match_dict: dict[str, str | Callable[[re.Match], str]] - ) -> str: - obj = StringMobject.get_substr_matched_obj(substr, match_dict) - if obj is None: - return substr - match_obj, val = obj - if isinstance(val, str): - return val - return val(match_obj) - @staticmethod def get_neighbouring_pairs(vals: Iterable[T]) -> list[tuple[T, T]]: val_list = list(vals) return list(zip(val_list[:-1], val_list[1:])) - @staticmethod - def group_neighbours(vals: Iterable[T]) -> list[tuple[T, Span]]: - if not vals: - return [] - - unique_vals, range_lens = zip(*( - (val, len(list(grouper))) - for val, grouper in it.groupby(vals) - )) - val_ranges = StringMobject.get_neighbouring_pairs( - [0, *it.accumulate(range_lens)] - ) - return list(zip(unique_vals, val_ranges)) - @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] @@ -280,19 +228,17 @@ class StringMobject(SVGMobject, ABC): # Parsing def parse(self) -> None: - pattern = self.CMD_PATTERN - cmd_spans = [] if pattern is None else [ - match_obj.span() - for match_obj in re.finditer(pattern, self.string, re.S) - ] - cmd_substrs = [self.get_substr(span) for span in cmd_spans] + cmd_matches = list(re.finditer( + self.get_cmd_pattern(), self.string, flags=re.S + )) + cmd_spans = [match_obj.span() for match_obj in cmd_matches] flags = [ - self.get_substr_matched_val(substr, self.FLAG_DICT, 0) - for substr in cmd_substrs + self.get_matched_flag(match_obj) + for match_obj in cmd_matches ] specified_items = [ *self.get_internal_specified_items( - self.get_cmd_span_pairs(cmd_spans, flags) + self.get_cmd_match_pairs(cmd_matches, flags) ), *self.get_external_specified_items(), *[ @@ -311,37 +257,50 @@ class StringMobject(SVGMobject, ABC): 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_substr_matched_str(substr, self.CONTENT_REPL)) - for span, substr in zip(cmd_spans, cmd_substrs) - ] - self.cmd_repl_items_for_matching = [ - (span, self.get_substr_matched_str(substr, self.MATCH_REPL)) - for span, substr in zip(cmd_spans, cmd_substrs) - ] self.check_overlapping() @staticmethod - def get_cmd_span_pairs( - cmd_spans: list[Span], flags: list[int] - ) -> list[tuple[Span, Span]]: + @abstractmethod + def get_cmd_pattern() -> str: + return "" + + @staticmethod + @abstractmethod + def get_matched_flag(match_obj: re.Match) -> int: + return 0 + + @staticmethod + @abstractmethod + def replace_for_content(match_obj: re.Match) -> str: + return "" + + @staticmethod + @abstractmethod + def replace_for_matching(match_obj: re.Match) -> str: + return "" + + @staticmethod + def get_cmd_match_pairs( + cmd_matches: list[re.Match], flags: list[int] + ) -> list[tuple[re.Match, re.Match]]: result = [] - begin_cmd_spans_stack = [] - for cmd_span, flag in zip(cmd_spans, flags): + begin_cmd_matches_stack = [] + for cmd_match, flag in zip(cmd_matches, flags): if flag == 1: - begin_cmd_spans_stack.append(cmd_span) + begin_cmd_matches_stack.append(cmd_match) elif flag == -1: - if not begin_cmd_spans_stack: + if not begin_cmd_matches_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: + begin_cmd_match = begin_cmd_matches_stack.pop() + result.append((begin_cmd_match, cmd_match)) + if begin_cmd_matches_stack: raise ValueError("Missing close command") return result + @staticmethod @abstractmethod def get_internal_specified_items( - self, cmd_span_pairs: list[tuple[Span, Span]] + cmd_match_pairs: list[tuple[re.Match, re.Match]] ) -> list[tuple[Span, dict[str, str]]]: return [] @@ -365,7 +324,7 @@ class StringMobject(SVGMobject, ABC): ]) ) complement_spans = self.get_complement_spans( - self.full_span, cmd_spans + (0, self.full_len), cmd_spans ) adjusted_span = ( max(arbitrary_span[0], complement_spans[cmd_range[0]][0]), @@ -416,17 +375,13 @@ class StringMobject(SVGMobject, ABC): ) -> tuple[str, str]: return "", "" - 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))) + def replace_substr( + self, span: Span, replace_func: Callable[[re.Match], str] + ) -> str: + return re.sub( + self.get_cmd_pattern(), replace_func, self.get_substr(span), + flags=re.S + ) def get_content(self, is_labelled: bool) -> str: inserted_str_pairs = [ @@ -436,27 +391,35 @@ class StringMobject(SVGMobject, ABC): )) 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 + if inserted_str_pairs: + indices, inserted_strs = zip(*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])) + else: + indices = () + inserted_strs = () + replaced_pieces = [ + self.replace_substr(span, self.replace_for_content) + for span in zip((0, *indices), (*indices, self.full_len)) ] + #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), + *it.chain(*zip(replaced_pieces, (*inserted_strs, ""))), suffix ]) @@ -486,11 +449,15 @@ class StringMobject(SVGMobject, ABC): if not self.labels: return [] - group_labels, labelled_submob_ranges = zip( - *self.group_neighbours(self.labels) + group_labels, range_lens = zip(*( + (val, len(list(grouper))) + for val, grouper in it.groupby(self.labels) + )) + labelled_submob_ranges = self.get_neighbouring_pairs( + [0, *it.accumulate(range_lens)] ) ordered_spans = [ - self.labelled_spans[label] if label != -1 else self.full_span + self.labelled_spans[label] if label != -1 else (0, self.full_len) for label in group_labels ] interval_spans = [ @@ -508,11 +475,7 @@ class StringMobject(SVGMobject, ABC): ] 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) - ] + span, self.replace_for_matching )) for span in self.get_complement_spans( (ordered_spans[0][0], ordered_spans[-1][1]), interval_spans diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index a79d5375..fa06b313 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -61,7 +61,7 @@ class _Alignment: self.value = _Alignment.VAL_DICT[s.upper()] -class Text(StringMobject): +class MarkupText(StringMobject): CONFIG = { "font_size": 48, "lsh": None, @@ -84,21 +84,50 @@ class Text(StringMobject): "isolate": (re.compile(r"[a-zA-Z]+"), re.compile(r"\S+")), } - CMD_PATTERN = r"""[<>&"']""" - FLAG_DICT = {} - CONTENT_REPL = { - r"<": "<", - r">": ">", - r"&": "&", - r"\"": """, - r"'": "'" + # See https://docs.gtk.org/Pango/pango_markup.html + MARKUP_TAGS = { + "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"}, } - MATCH_REPL = {} + MARKUP_ENTITY_DICT = { + "<": "<", + ">": ">", + "&": "&", + "\"": """, + "'": "'" + } + #ENTITIES, ENTITY_CHARS = zip( + # ("<", "<"), + # (">", ">"), + # ("&", "&"), + # (""", "\""), + # ("'", "'") + #) + + #CMD_PATTERN = r"""[<>&"']""" + #FLAG_DICT = {} + #CONTENT_REPL = { + # r"<": "<", + # r">": ">", + # r"&": "&", + # r"\"": """, + # r"'": "'" + #} + #MATCH_REPL = {} 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: @@ -213,12 +242,80 @@ class Text(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 + @staticmethod + def get_cmd_pattern() -> str | None: + # Unsupported passthroughs: + # "", "", "", "" + # See https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/gmarkup.c + return r"""(<(/)?\w+(?:\s*\w+\s*\=\s*(["']).*?\3)*(/)?>)|(&(#(x)?)?(.*?);)|([>"'])""" + + @staticmethod + def get_matched_flag(match_obj: re.Match) -> int: + if match_obj.group(1): + if match_obj.group(2): + return -1 + if not match_obj.group(4): + return 1 + return 0 + + @staticmethod + def replace_for_content(match_obj: re.Match) -> str: + substr = match_obj.group() + if match_obj.group(9): + return MarkupText.escape_markup_char(substr) + return substr + + @staticmethod + def replace_for_matching(match_obj: re.Match) -> str: + substr = match_obj.group() + if match_obj.group(1): + return "" + if match_obj.group(5): + if match_obj.group(6): + base = 10 + if match_obj.group(7): + base = 16 + return chr(int(match_obj.group(8), base)) + return MarkupText.unescape_markup_char(substr) + return substr + + @staticmethod def get_internal_specified_items( - self, cmd_span_pairs: list[tuple[Span, Span]] + cmd_match_pairs: list[tuple[re.Match, re.Match]] ) -> list[tuple[Span, dict[str, str]]]: - return [] + attr_pattern = r"""(\w+)\s*\=\s*(["'])(.*?)\2""" + result = [] + for begin_match, end_match in cmd_match_pairs: + begin_tag = begin_match.group() + tag_name = re.search(r"\w+", begin_tag).group() + 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, re.S + ) + } + else: + attr_dict = MarkupText.MARKUP_TAGS.get(tag_name, {}) + result.append( + ((begin_match.end(), end_match.start()), attr_dict) + ) + return result def get_external_specified_items( self @@ -315,70 +412,22 @@ class Text(StringMobject): return self.get_string() -class MarkupText(Text): - # Unsupported passthroughs: - # "", "", "", "" - # See https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/gmarkup.c - CMD_PATTERN = r"""|&.*?;|[>"']""" - FLAG_DICT = { - r"": -1, - r"<.*/>": 0, - r"<.*>": 1 - } - CONTENT_REPL = { - r">": ">", - r"\"": """, - r"'": "'" - } - MATCH_REPL = { - r"<.*>": "", - r"&#x(.*);": lambda m: chr(int(m.group(1), 16)), - r"&#(.*);": lambda m: chr(int(m.group(1), 10)), - r"<": "<", - r">": ">", - r"&": "&", - r""": "\"", - r"'": "'" - } +class Text(MarkupText): + @staticmethod + def get_cmd_pattern() -> str | None: + return r"""[<>&"']""" - # See https://docs.gtk.org/Pango/pango_markup.html - MARKUP_TAGS = { - "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"}, - } + @staticmethod + def get_matched_flag(match_obj: re.Match) -> int: + return 0 - def __init__(self, text: str, **kwargs): - self.validate_markup_string(text) - super().__init__(text, **kwargs) + @staticmethod + def replace_for_content(match_obj: re.Match) -> str: + return Text.escape_markup_char(match_obj.group()) - def get_internal_specified_items( - self, cmd_span_pairs: list[tuple[Span, Span]] - ) -> list[tuple[Span, dict[str, str]]]: - attr_pattern = r"""(\w+)\s*\=\s*(["'])(.*?)\2""" - result = [] - for begin_cmd_span, end_cmd_span in cmd_span_pairs: - begin_tag = self.get_substr(begin_cmd_span) - tag_name = re.search(r"\w+", begin_tag).group() - 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, re.S - ) - } - else: - attr_dict = self.MARKUP_TAGS.get(tag_name, {}) - result.append( - ((begin_cmd_span[1], end_cmd_span[0]), attr_dict) - ) - return result + @staticmethod + def replace_for_matching(match_obj: re.Match) -> str: + return match_obj.group() class Code(MarkupText): From f434eb93e284b9b026520b181d9aa077d3f2ccbd Mon Sep 17 00:00:00 2001 From: YishiMichael Date: Sun, 7 Aug 2022 00:50:29 +0800 Subject: [PATCH 14/27] Refactor StringMobject and relevant classes --- manimlib/mobject/svg/mtex_mobject.py | 62 ++--- manimlib/mobject/svg/string_mobject.py | 365 +++++++++++++------------ manimlib/mobject/svg/text_mobject.py | 125 +++++---- 3 files changed, 278 insertions(+), 274 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 4548614f..a57c10a8 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -1,5 +1,7 @@ from __future__ import annotations +import itertools as it + 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 tex_content_to_svg_file @@ -40,16 +42,6 @@ class MTex(StringMobject): "additional_preamble": "", } - #CMD_PATTERN = r"\\(?:[a-zA-Z]+|.)|[_^{}]" - #FLAG_DICT = { - # r"{": 1, - # r"}": -1 - #} - #CONTENT_REPL = {} - #MATCH_REPL = { - # r"[_^{}]": "" - #} - def __init__(self, tex_string: str, **kwargs): # Prevent from passing an empty string. if not tex_string.strip(): @@ -86,17 +78,20 @@ class MTex(StringMobject): # Parsing @staticmethod - def get_cmd_pattern() -> str | None: - return r"(\\(?:[a-zA-Z]+|.))|([_^])|([{}])" + def get_command_pattern() -> str: + return r""" + (?P\\(?:[a-zA-Z]+|.)) + |(?P