Source code for manim.mobject.text.code_mobject

"""Mobject representing highlighted source code listings."""

from __future__ import annotations

__all__ = [
    "Code",
]

import re
from pathlib import Path
from typing import Any, Literal

from bs4 import BeautifulSoup, Tag
from pygments import highlight
from pygments.formatters.html import HtmlFormatter
from pygments.lexers import get_lexer_by_name, guess_lexer, guess_lexer_for_filename
from pygments.styles import get_all_styles

from manim.constants import *
from manim.mobject.geometry.arc import Dot
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.typing import StrPath
from manim.utils.color import WHITE, ManimColor


[docs] class Code(VMobject, metaclass=ConvertToOpenGL): """A highlighted source code listing. Examples -------- Normal usage:: listing = Code( "helloworldcpp.cpp", tab_width=4, formatter_style="emacs", background="window", language="cpp", background_config={"stroke_color": WHITE}, paragraph_config={"font": "Noto Sans Mono"}, ) We can also render code passed as a string. As the automatic language detection can be a bit flaky, it is recommended to specify the language explicitly: .. manim:: CodeFromString :save_last_frame: class CodeFromString(Scene): def construct(self): code = '''from manim import Scene, Square class FadeInSquare(Scene): def construct(self): s = Square() self.play(FadeIn(s)) self.play(s.animate.scale(2)) self.wait()''' rendered_code = Code( code_string=code, language="python", background="window", background_config={"stroke_color": "maroon"}, ) self.add(rendered_code) Parameters ---------- code_file The path to the code file to display. code_string Alternatively, the code string to display. language The programming language of the code. If not specified, it will be guessed from the file extension or the code itself. formatter_style The style to use for the code highlighting. Defaults to ``"vim"``. A list of all available styles can be obtained by calling :meth:`.Code.get_styles_list`. tab_width The width of a tab character in spaces. Defaults to 4. add_line_numbers Whether to display line numbers. Defaults to ``True``. line_numbers_from The first line number to display. Defaults to 1. background The type of background to use. Can be either ``"rectangle"`` (the default) or ``"window"``. background_config Keyword arguments passed to the background constructor. Default settings are stored in the class attribute :attr:`.default_background_config` (which can also be modified directly). paragraph_config Keyword arguments passed to the constructor of the :class:`.Paragraph` objects holding the code, and the line numbers. Default settings are stored in the class attribute :attr:`.default_paragraph_config` (which can also be modified directly). """ _styles_list_cache: list[str] | None = None default_background_config: dict[str, Any] = { "buff": 0.3, "fill_color": ManimColor("#222"), "stroke_color": WHITE, "corner_radius": 0.2, "stroke_width": 1, "fill_opacity": 1, } default_paragraph_config: dict[str, Any] = { "font": "Monospace", "font_size": 24, "line_spacing": 0.5, "disable_ligatures": True, } code: VMobject def __init__( self, code_file: StrPath | None = None, code_string: str | None = None, language: str | None = None, formatter_style: str = "vim", tab_width: int = 4, add_line_numbers: bool = True, line_numbers_from: int = 1, background: Literal["rectangle", "window"] = "rectangle", background_config: dict[str, Any] | None = None, paragraph_config: dict[str, Any] | None = None, ): super().__init__() if code_file is not None: code_file = Path(code_file) code_string = code_file.read_text(encoding="utf-8") lexer = guess_lexer_for_filename(code_file.name, code_string) elif code_string is not None: if language is not None: lexer = get_lexer_by_name(language) else: lexer = guess_lexer(code_string) else: raise ValueError("Either a code file or a code string must be specified.") code_string = code_string.expandtabs(tabsize=tab_width) formatter = HtmlFormatter( style=formatter_style, noclasses=True, cssclasses="", ) soup = BeautifulSoup( highlight(code_string, lexer, formatter), features="html.parser" ) self._code_html = soup.find("pre") assert isinstance(self._code_html, Tag) # as we are using Paragraph to render the text, we need to find the character indices # of the segments of changed color in the HTML code color_ranges = [] current_line_color_ranges = [] current_line_char_index = 0 for child in self._code_html.children: if child.name == "span": try: child_style = child["style"] match_ = re.match( r"color: (#[A-Fa-f0-9]{6}|#[A-Fa-f0-9]{3})", child_style ) color = None if match_ is None else match_.group(1) except KeyError: color = None current_line_color_ranges.append( ( current_line_char_index, current_line_char_index + len(child.text), color, ) ) current_line_char_index += len(child.text) else: for char in child.text: if char == "\n": color_ranges.append(current_line_color_ranges) current_line_color_ranges = [] current_line_char_index = 0 else: current_line_char_index += 1 color_ranges.append(current_line_color_ranges) code_lines = self._code_html.get_text().removesuffix("\n").split("\n") if paragraph_config is None: paragraph_config = {} base_paragraph_config = self.default_paragraph_config.copy() base_paragraph_config.update(paragraph_config) from manim.mobject.text.text_mobject import Paragraph self.code_lines = Paragraph( *code_lines, **base_paragraph_config, ) for line, color_range in zip(self.code_lines, color_ranges, strict=False): for start, end, color in color_range: line[start:end].set_color(color) if add_line_numbers: base_paragraph_config.update({"alignment": "right"}) self.line_numbers = Paragraph( *[ str(i) for i in range( line_numbers_from, line_numbers_from + len(self.code_lines) ) ], **base_paragraph_config, ) self.line_numbers.next_to(self.code_lines, direction=LEFT).align_to( self.code_lines, UP ) self.add(self.line_numbers) for line in self.code_lines: line.submobjects = [c for c in line if not isinstance(c, Dot)] self.add(self.code_lines) if background_config is None: background_config = {} background_config_base = self.default_background_config.copy() background_config_base.update(background_config) if background == "rectangle": self.background = SurroundingRectangle( self, **background_config_base, ) elif background == "window": buttons = VGroup( Dot(radius=0.1, stroke_width=0, color=button_color) for button_color in ["#ff5f56", "#ffbd2e", "#27c93f"] ).arrange(RIGHT, buff=0.1) buttons.next_to(self, UP, buff=0.1).align_to(self, LEFT).shift(LEFT * 0.1) self.background = SurroundingRectangle( VGroup(self, buttons), **background_config_base, ) buttons.shift(UP * 0.1 + LEFT * 0.1) self.background.add(buttons) else: raise ValueError(f"Unknown background type: {background}") self.add_to_back(self.background)
[docs] @classmethod def get_styles_list(cls) -> list[str]: """Get the list of all available formatter styles.""" if cls._styles_list_cache is None: cls._styles_list_cache = list(get_all_styles()) return cls._styles_list_cache