import html from manimlib.constants import * from manimlib.container.container import Container from manimlib.mobject.geometry import Rectangle, Dot, RoundedRectangle from manimlib.mobject.shape_matchers import SurroundingRectangle from manimlib.mobject.svg.text_mobject import Paragraph from manimlib.mobject.types.vectorized_mobject import VGroup import re from pygments import highlight from pygments.lexers import get_lexer_by_name from pygments.formatters.html import HtmlFormatter ''' 1) Code is VGroup() with three things 1.1) Code[0] is Code.background_mobject which can be a 1.1.1) Rectangle() if background == "rectangle" 1.1.2) VGroup() of Rectangle() and Dot() for three buttons if background == "window" 1.2) Code[1] is Code.line_numbers Which is a Paragraph() object, this mean you can use Code.line_numbers[0] or Code[1][0] to access first line number 1.3) Code[2] is Code.code 1.3.1) Which is a Paragraph() with color highlighted, this mean you can use Code.code[1] or Code[2][1] line number 1 Code.code[1][0] or Code.code[1][0] first character of line number 1 Code.code[1][0:5] or Code.code[1][0:5] first five characters of line number 1 ''' class Code(VGroup): CONFIG = { "tab_width": 3, "line_spacing": 0.1, "scale_factor": 0.5, "run_time": 1, "font": 'Monospac821 BT', 'stroke_width': 0, 'margin': 0.3, 'indentation_char': " ", "background": "rectangle", # or window "corner_radius": 0.2, 'insert_line_no': True, 'line_no_from': 1, "line_no_buff": 0.4, 'style': 'vim', 'language': 'cpp', 'generate_html_file': False } def __init__(self, file_name=None, **kwargs): Container.__init__(self, **kwargs) self.file_name = file_name or self.file_name self.ensure_valid_file() self.style = self.style.lower() self.gen_html_string() strati = self.html_string.find("background:") self.background_color = self.html_string[strati + 12:strati + 19] self.gen_code_json() self.code = self.gen_colored_lines() if self.insert_line_no: self.line_numbers = self.gen_line_numbers() self.line_numbers.next_to(self.code, direction=LEFT, buff=self.line_no_buff) if self.background == "rectangle": if self.insert_line_no: forground = VGroup(self.code, self.line_numbers) else: forground = self.code self.background_mobject = SurroundingRectangle(forground, buff=self.margin, color=self.background_color, fill_color=self.background_color, stroke_width=0, fill_opacity=1, ) self.background_mobject.round_corners(self.corner_radius) else: if self.insert_line_no: forground = VGroup(self.code, self.line_numbers) else: forground = self.code height = forground.get_height() + 0.1 * 3 + 2 * self.margin width = forground.get_width() + 0.1 * 3 + 2 * self.margin rrect = RoundedRectangle(corner_radius=self.corner_radius, height=height, width=width, stroke_width=0, color=self.background_color, fill_opacity=1) red_button = Dot(radius=0.1, stroke_width=0, color='#ff5f56') red_button.shift(LEFT * 0.1 * 3) yellow_button = Dot(radius=0.1, stroke_width=0, color='#ffbd2e') green_button = Dot(radius=0.1, stroke_width=0, color='#27c93f') green_button.shift(RIGHT * 0.1 * 3) buttons = VGroup(red_button, yellow_button, green_button) buttons.shift( UP * (height / 2 - 0.1 * 2 - 0.05) + LEFT * (width / 2 - 0.1 * 5 - self.corner_radius / 2 - 0.05)) self.background_mobject = VGroup(rrect, buttons) x = (height - forground.get_height()) / 2 - 0.1 * 3 self.background_mobject.shift(forground.get_center()) self.background_mobject.shift(UP * x) if self.insert_line_no: VGroup.__init__(self, self.background_mobject, self.line_numbers, *self.code, **kwargs) else: VGroup.__init__(self, self.background_mobject, Dot(fill_opacity=0, stroke_opacity=0), *self.code, **kwargs) self.move_to(np.array([0, 0, 0])) def apply_points_function_about_point(self, func, about_point=None, about_edge=None): if about_point is None: if about_edge is None: about_edge = self.get_corner(UP + LEFT) about_point = self.get_critical_point(about_edge) for mob in self.family_members_with_points(): mob.points -= about_point mob.points = func(mob.points) mob.points += about_point return self def ensure_valid_file(self): if self.file_name is None: raise Exception("Must specify file for Code") possible_paths = [ os.path.join(os.path.join("assets", "codes"), self.file_name), self.file_name, ] for path in possible_paths: if os.path.exists(path): self.file_path = path return raise IOError("No file matching %s in codes directory" % self.file_name) def gen_line_numbers(self): line_numbers_array = [] for line_no in range(0, self.code_json.__len__()): number = str(self.line_no_from + line_no) line_numbers_array.append(number) line_numbers = Paragraph(*[i for i in line_numbers_array], line_spacing=self.line_spacing, alignment="right", font=self.font, stroke_width=self.stroke_width).scale(self.scale_factor) return line_numbers def gen_colored_lines(self): lines_text = [] for line_no in range(0, self.code_json.__len__()): line_str = "" for word_index in range(self.code_json[line_no].__len__()): line_str = line_str + self.code_json[line_no][word_index][0] lines_text.append(self.tab_spaces[line_no] * "\t" + line_str) code = Paragraph(*[i for i in lines_text], line_spacing=self.line_spacing, tab_width=self.tab_width, alignment="left", font=self.font, stroke_width=self.stroke_width).scale(self.scale_factor) for line_no in range(code.__len__()): line = code[line_no] line_char_index = self.tab_spaces[line_no] for word_index in range(self.code_json[line_no].__len__()): line[line_char_index:line_char_index + self.code_json[line_no][word_index][0].__len__()].set_color( self.code_json[line_no][word_index][1]) line_char_index += self.code_json[line_no][word_index][0].__len__() return code def gen_html_string(self): file = open(self.file_path, "r") code_str = file.read() file.close() self.html_string = hilite_me(code_str, self.language, {}, self.style, self.insert_line_no, "border:solid gray;border-width:.1em .1em .1em .8em;padding:.2em .6em;") if self.generate_html_file: os.makedirs(os.path.join("assets", "codes", "generated_html_files"), exist_ok=True) file = open(os.path.join("assets", "codes", "generated_html_files", self.file_name + ".html"), "w") file.write(self.html_string) file.close() def gen_code_json(self): if self.background_color == "#111111" or \ self.background_color == "#272822" or \ self.background_color == "#202020" or \ self.background_color == "#000000": self.default_color = "#ffffff" else: self.default_color = "#000000" for i in range(3, -1, -1): self.html_string = self.html_string.replace("" + " " * i, "") for i in range(10, -1, -1): self.html_string = self.html_string.replace("" + " " * i, " " * i + "") self.html_string = self.html_string.replace("background-color:", "background:") if self.insert_line_no: start_point = self.html_string.find("
") lines[0] = lines[0][start_point + 1:] # print(lines) self.code_json = [] self.tab_spaces = [] code_json_line_index = -1 for line_index in range(0, lines.__len__()): if lines[line_index].__len__() == 0: continue # print(lines[line_index]) self.code_json.append([]) code_json_line_index = code_json_line_index + 1 if lines[line_index].startswith(self.indentation_char): start_point = lines[line_index].find("<") starting_string = lines[line_index][:start_point] indentation_char_count = lines[line_index][:start_point].count(self.indentation_char) if starting_string.__len__() != indentation_char_count * self.indentation_char.__len__(): lines[line_index] = "\t" * indentation_char_count + starting_string[starting_string.rfind( self.indentation_char) + self.indentation_char.__len__():] + \ lines[line_index][start_point:] else: lines[line_index] = "\t" * indentation_char_count + lines[line_index][start_point:] indentation_char_count = 0 while lines[line_index][indentation_char_count] == '\t': indentation_char_count = indentation_char_count + 1 self.tab_spaces.append(indentation_char_count) # print(lines[line_index]) lines[line_index] = self.correct_non_span(lines[line_index]) # print(lines[line_index]) words = lines[line_index].split("") end_point = words[word_index].find("") text = words[word_index][start_point + 1:end_point] text = html.unescape(text) if text != "": # print(text, "'" + color + "'") self.code_json[code_json_line_index].append([text, color]) # print(self.code_json) def correct_non_span(self, line_str): words = line_str.split("") line_str = "" for i in range(0, words.__len__()): if i != words.__len__() - 1: j = words[i].find("' + words[i][starti:j] + "" else: temp = '' + words[i][starti:j] temp = temp + words[i][j:] words[i] = temp if words[i] != "": line_str = line_str + words[i] + "" return line_str def hilite_me(code, lexer, options, style, linenos, divstyles): lexer = lexer or 'python' style = style or 'colorful' defstyles = 'overflow:auto;width:auto;' formatter = HtmlFormatter(style=style, linenos=False, noclasses=True, cssclass='', cssstyles=defstyles + divstyles, prestyles='margin: 0') html = highlight(code, get_lexer_by_name(lexer, **options), formatter) if linenos: html = insert_line_numbers(html) html = "" + html return html def get_default_style(): return 'border:solid gray;border-width:.1em .1em .1em .8em;padding:.2em .6em;' def insert_line_numbers(html): match = re.search('(]*>)(.*)()', html, re.DOTALL) if not match: return html pre_open = match.group(1) pre = match.group(2) pre_close = match.group(3) html = html.replace(pre_close, '
' + pre_open + lines + ' | ' + pre_open) return html |