From 244bbcbff230c41eaee490cf9e3d2d8878aa391f Mon Sep 17 00:00:00 2001 From: Ivanzzzc <46572469+IvanNazaruk@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:39:49 +0200 Subject: [PATCH] v1.0.0 first release --- .gitignore | 7 + DearPyGui_Markdown/__init__.py | 423 ++++++++++++++++++++++++ DearPyGui_Markdown/attribute_types.py | 179 ++++++++++ DearPyGui_Markdown/font_attributes.py | 89 +++++ DearPyGui_Markdown/line_atributes.py | 211 ++++++++++++ DearPyGui_Markdown/parser.py | 387 ++++++++++++++++++++++ DearPyGui_Markdown/text_attributes.py | 157 +++++++++ DearPyGui_Markdown/text_entities.py | 422 +++++++++++++++++++++++ README.md | 2 +- example/font.py | 39 +++ example/fonts/InterTight-Bold.ttf | Bin 0 -> 309904 bytes example/fonts/InterTight-BoldItalic.ttf | Bin 0 -> 316464 bytes example/fonts/InterTight-Italic.ttf | Bin 0 -> 312040 bytes example/fonts/InterTight-Regular.ttf | Bin 0 -> 305456 bytes example/main.py | 81 +++++ example/test_text.py | 91 +++++ requirements.txt | 3 + 17 files changed, 2090 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 DearPyGui_Markdown/__init__.py create mode 100644 DearPyGui_Markdown/attribute_types.py create mode 100644 DearPyGui_Markdown/font_attributes.py create mode 100644 DearPyGui_Markdown/line_atributes.py create mode 100644 DearPyGui_Markdown/parser.py create mode 100644 DearPyGui_Markdown/text_attributes.py create mode 100644 DearPyGui_Markdown/text_entities.py create mode 100644 example/font.py create mode 100644 example/fonts/InterTight-Bold.ttf create mode 100644 example/fonts/InterTight-BoldItalic.ttf create mode 100644 example/fonts/InterTight-Italic.ttf create mode 100644 example/fonts/InterTight-Regular.ttf create mode 100644 example/main.py create mode 100644 example/test_text.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ec1984 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ +venv/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file diff --git a/DearPyGui_Markdown/__init__.py b/DearPyGui_Markdown/__init__.py new file mode 100644 index 0000000..85fd984 --- /dev/null +++ b/DearPyGui_Markdown/__init__.py @@ -0,0 +1,423 @@ +import threading +import time +import traceback +from typing import List, Any, Callable, Union, Tuple + +import dearpygui.dearpygui as dpg + + +def get_text_size(text: str, *, wrap_width: float = -1.0, font: int | str = 0, **kwargs) -> list[float | int] | tuple[float | int, ...]: + strip_text = text.strip() + while 1: + size: list[float, float] = dpg.get_text_size(text, wrap_width=wrap_width, font=font, **kwargs) + + if size is None: + continue + if size[1] == 0: + continue + if size[0] == 0 and len(strip_text) != 0: + continue + break + return size + + +class CallInNextFrame: + __started = False + now_frame_queue = [] + + def __new__(cls, func): + def decorator(*args, **kwargs): + cls.append(func, *args, **kwargs) + + return decorator + + @classmethod + def append(cls, func, *args, **kwargs): + if cls.__started is False: + cls.__started = True + threading.Thread(target=cls._worker, daemon=True).start() + cls.now_frame_queue.append( + [func, args, kwargs] + ) + + @classmethod + def _worker(cls): + while True: + if len(cls.now_frame_queue) == 0: + time.sleep(0.015) + continue + next_frame_queue = cls.now_frame_queue.copy() + cls.now_frame_queue.clear() + dpg.split_frame() + for func, args, kwargs in next_frame_queue: + try: + func(*args, **kwargs) + except Exception: + traceback.print_exc() + + +class CallWhenDPGStarted: + __thread = None + STARTUP_DONE = False + functions_queue = [] + + @classmethod + def append(cls, func, *args, **kwargs): + if cls.__thread is None: + cls.__thread = True + threading.Thread(target=cls._worker, daemon=True).start() + if not cls.STARTUP_DONE: + cls.functions_queue.append( + [func, args, kwargs] + ) + return + try: + func(*args, **kwargs) + except Exception: + traceback.print_exc() + + @classmethod + def _worker(cls): + while dpg.get_frame_count() <= 1: + time.sleep(0.01) + dpg.split_frame() + cls.STARTUP_DONE = True + for func, args, kwargs in cls.functions_queue: + try: + func(*args, **kwargs) + except Exception: + traceback.print_exc() + del cls.functions_queue + + +from . import font_attributes +from . import line_atributes +from . import parser +from . import text_attributes +from . import text_entities +from .font_attributes import set_font_registry, set_add_font_function, set_font + + +def wrap_text_entity(text: text_entities.StrEntity | text_entities.TextEntity, width: int | float = -1) -> text_entities.LineEntity: + def get_width(str_entity: text_entities.StrEntity | text_entities.TextEntity) -> int | float: + return text_entities.LineEntity.get_width(str_entity) + + def to_words(str_entity: text_entities.StrEntity | text_entities.TextEntity) -> list[text_entities.StrEntity | text_entities.TextEntity]: + words_list = [] + word = None + chars = str_entity.chars() + for char in chars: + if str(char) == " ": + if word is not None: + words_list.append(word) + words_list.append(char) + word = None + else: + if word is None: + word = char + else: + word = word + char + if word is not None: + words_list.append(word) + return words_list + + print_text = text_entities.LineEntity() + paragraphs_list = text.split('\n') + if width < 0: + for paragraph in paragraphs_list: + print_text.append(paragraph) + return print_text + + for paragraph in paragraphs_list: + sentence = text_entities.StrEntity('') + if len(paragraph) == 0: + print_text.append(paragraph) + continue + + words_list = to_words(paragraph) + for word in words_list: + sentence_with_next_word = sentence + word + if get_width(sentence_with_next_word) <= width: + sentence = sentence_with_next_word + continue + if len(sentence) != 0: + print_text.append(sentence) + if get_width(word) <= width: + sentence = word + continue + + sentence = text_entities.StrEntity('') + for i, char in enumerate(word.chars()): + sentence_with_next_char = sentence + char + if get_width(sentence_with_next_char) <= width: + sentence = sentence_with_next_char + else: + if len(sentence) > 0: + print_text.append(sentence) + sentence = char + print_text.append(sentence) + + return print_text + + +class _ConvertedMessageEntity: + def __init__(self, entity: parser.MessageEntity): + self.entity = entity + self.offset = entity.offset + match type(self.entity): + case parser.MessageEntitySeparator: + self.end = self.offset + case _: + self.end = self.offset + entity.length + + @property + def object(self): + match type(self.entity): + case parser.MessageEntityBold: + return font_attributes.Bold + case parser.MessageEntityItalic: + return font_attributes.Italic + case parser.MessageEntityUnderline: + return text_attributes.Underline + case parser.MessageEntityStrike: + return text_attributes.Strike + case parser.MessageEntityCode: + return text_attributes.Code + case parser.MessageEntityPre: + return text_attributes.Pre(attribute_connector=self.entity.attribute_connector) + case parser.MessageEntityTextUrl: + return text_attributes.Url(self.entity.url, + attribute_connector=self.entity.attribute_connector) + case parser.MessageEntityFont: + return font_attributes.Font(self.entity.color, + self.entity.size) + case parser.MessageEntityBlockquote: + return line_atributes.Blockquote(self.entity.depth, + attribute_connector=self.entity.attribute_connector) + case parser.MessageEntityUnorderedList: + return line_atributes.List(self.entity.depth, + attribute_connector=self.entity.attribute_connector, + task=self.entity.task, + task_done=self.entity.task_done) + case parser.MessageEntityOrderedList: + return line_atributes.List(self.entity.depth, + attribute_connector=self.entity.attribute_connector, + ordered=True, + index=self.entity.index, + task=self.entity.task, + task_done=self.entity.task_done) + case parser.MessageEntitySeparator: + return line_atributes.Separator + case parser.MessageEntityH1: + return font_attributes.H1 + case parser.MessageEntityH2: + return font_attributes.H2 + case parser.MessageEntityH3: + return font_attributes.H3 + case parser.MessageEntityH4: + return font_attributes.H4 + case parser.MessageEntityH5: + return font_attributes.H5 + case parser.MessageEntityH6: + return font_attributes.H6 + case _: + raise ValueError(f'Unidentified MessageEntity: {type(self.entity)}') + + def __repr__(self): + return f'<{self.offset}, {self.end} {self.entity}>' + + def __eq__(self, index: int): + if isinstance(self.entity, parser.MessageEntitySeparator): + return self.offset == index + return self.offset < index <= self.end + + +class MarkdownText: + text_entity: text_entities.TextEntity | text_entities.StrEntity + + def __init__(self, markdown_text: str): + clear_text, attributes = parser.parse(markdown_text) + for i in range(len(attributes)): + attributes[i] = _ConvertedMessageEntity(attributes[i]) + + attribute_points = [] + for entity in attributes: + attribute_points.append(entity.offset) + attribute_points.append(entity.end) + + attribute_points.append(len(clear_text)) + attribute_points = list(set(attribute_points)) + attribute_points.sort() + + if len(attribute_points) == 0: + self.text_entity = text_entities.StrEntity(clear_text) + else: + self.text_entity = text_entities.TextEntity() + + for i, point in enumerate(attribute_points): + str_attributes = [] + for entity in attributes: + if point == entity: + str_attributes.append(entity.object) + + past_point = attribute_points[i - 1] if i != 0 else 0 + + str_entity = text_entities.StrEntity(clear_text[past_point:point:]) + if line_atributes.Separator in str_attributes: + del str_attributes[str_attributes.index(line_atributes.Separator)] + if len(str(str_entity)) > 0: + str_entity = text_entities.StrEntity(str(str_entity).removesuffix('\n')) + str_entity.set_attributes(str_attributes) + self.text_entity.append(str_entity) + + if point != 0: + self.text_entity.append( + text_entities.StrEntity('\n') + ) + str_entity = text_entities.StrEntity(' ') + str_entity.set_attributes([line_atributes.Separator]) # noqa + self.text_entity.append(str_entity) + else: + str_entity.set_attributes(str_attributes) + self.text_entity.append(str_entity) + + def add(self, wrap: int | float = -1, parent=0): + ''' + :param wrap: Number of pixels from the start of the item until wrapping starts. + :param parent: Parent to add this item to. (runtime adding) + :return: group with rendered text + ''' + print_text: text_entities.LineEntity = wrap_text_entity(self.text_entity, width=wrap) + + with dpg.group(parent=parent, horizontal=True) as group: + text_group = dpg.add_group(parent=group) + attributes_group = dpg.add_group(parent=group) + + if not CallWhenDPGStarted.STARTUP_DONE: + CallWhenDPGStarted.append(dpg.bind_item_theme, group, text_entities.AttributeController.dpg_group_theme) + CallWhenDPGStarted.append(print_text.render, parent=text_group, attributes_group=attributes_group) + else: + dpg.bind_item_theme(group, text_entities.AttributeController.dpg_group_theme) + print_text.render(parent=text_group, attributes_group=attributes_group) + return group + + +def add_text(markdown_text: str, + wrap: float | int = -1, + parent: int | str = 0, + pos: list[int | float, int | float] | tuple[int | float, int | float] = None, ): + ''' + :param wrap: Number of pixels from the start of the item until wrapping starts. + :param parent: Parent to add this item to. (runtime adding) + :pos: Places the item relative to window coordinates, [0,0] is top left. + :return: group with rendered text + ''' + rendered_group = MarkdownText(markdown_text=markdown_text).add(wrap=wrap, parent=parent) + if pos is not None: + dpg.set_item_pos(rendered_group, pos) + return rendered_group + + +def add_text_italic(default_value: str = '', *, label: str = None, user_data: Any = None, use_internal_label: bool = True, tag: Union[int, str] = 0, indent: int = -1, parent: Union[int, str] = 0, before: Union[int, str] = 0, source: Union[int, str] = 0, payload_type: str = '$$DPG_PAYLOAD', drag_callback: Callable = None, drop_callback: Callable = None, show: bool = True, pos: Union[List[int], Tuple[int, ...]] = [], filter_key: str = '', tracked: bool = False, track_offset: float = 0.5, wrap: int = -1, bullet: bool = False, color: Union[List[int], Tuple[int, ...]] = (-255, 0, 0, 255), show_label: bool = False, **kwargs) -> Union[int, str]: + """ Adds italic text. Text can have an optional label that will display to the right of the text. + + Args: + default_value (str, optional): + label (str, optional): Overrides 'name' as label. + user_data (Any, optional): User data for callbacks + use_internal_label (bool, optional): Use generated internal label instead of user specified (appends ### uuid). + tag (Union[int, str], optional): Unique id used to programmatically refer to the item.If label is unused this will be the label. + indent (int, optional): Offsets the widget to the right the specified number multiplied by the indent style. + parent (Union[int, str], optional): Parent to add this item to. (runtime adding) + before (Union[int, str], optional): This item will be displayed before the specified item in the parent. + source (Union[int, str], optional): Overrides 'id' as value storage key. + payload_type (str, optional): Sender string type must be the same as the target for the target to run the payload_callback. + drag_callback (Callable, optional): Registers a drag callback for drag and drop. + drop_callback (Callable, optional): Registers a drop callback for drag and drop. + show (bool, optional): Attempt to render widget. + pos (Union[List[int], Tuple[int, ...]], optional): Places the item relative to window coordinates, [0,0] is top left. + filter_key (str, optional): Used by filter widget. + tracked (bool, optional): Scroll tracking + track_offset (float, optional): 0.0f:top, 0.5f:center, 1.0f:bottom + wrap (int, optional): Number of pixels from the start of the item until wrapping starts. + bullet (bool, optional): Places a bullet to the left of the text. + color (Union[List[int], Tuple[int, ...]], optional): Color of the text (rgba). + show_label (bool, optional): Displays the label to the right of the text. + id (Union[int, str], optional): (deprecated) + Returns: + Union[int, str] + """ + dpg_text = dpg.add_text(default_value, label=label, user_data=user_data, use_internal_label=use_internal_label, tag=tag, indent=indent, parent=parent, before=before, source=source, payload_type=payload_type, drag_callback=drag_callback, drop_callback=drop_callback, show=show, pos=pos, filter_key=filter_key, tracked=tracked, track_offset=track_offset, wrap=wrap, bullet=bullet, color=color, show_label=show_label, **kwargs) + dpg.bind_item_font(dpg_text, + font=font_attributes.Italic.get_font()) + return dpg_text + + +def add_text_bold(default_value: str = '', *, label: str = None, user_data: Any = None, use_internal_label: bool = True, tag: Union[int, str] = 0, indent: int = -1, parent: Union[int, str] = 0, before: Union[int, str] = 0, source: Union[int, str] = 0, payload_type: str = '$$DPG_PAYLOAD', drag_callback: Callable = None, drop_callback: Callable = None, show: bool = True, pos: Union[List[int], Tuple[int, ...]] = [], filter_key: str = '', tracked: bool = False, track_offset: float = 0.5, wrap: int = -1, bullet: bool = False, color: Union[List[int], Tuple[int, ...]] = (-255, 0, 0, 255), show_label: bool = False, **kwargs) -> Union[int, str]: + """ Adds bold text. Text can have an optional label that will display to the right of the text. + + Args: + default_value (str, optional): + label (str, optional): Overrides 'name' as label. + user_data (Any, optional): User data for callbacks + use_internal_label (bool, optional): Use generated internal label instead of user specified (appends ### uuid). + tag (Union[int, str], optional): Unique id used to programmatically refer to the item.If label is unused this will be the label. + indent (int, optional): Offsets the widget to the right the specified number multiplied by the indent style. + parent (Union[int, str], optional): Parent to add this item to. (runtime adding) + before (Union[int, str], optional): This item will be displayed before the specified item in the parent. + source (Union[int, str], optional): Overrides 'id' as value storage key. + payload_type (str, optional): Sender string type must be the same as the target for the target to run the payload_callback. + drag_callback (Callable, optional): Registers a drag callback for drag and drop. + drop_callback (Callable, optional): Registers a drop callback for drag and drop. + show (bool, optional): Attempt to render widget. + pos (Union[List[int], Tuple[int, ...]], optional): Places the item relative to window coordinates, [0,0] is top left. + filter_key (str, optional): Used by filter widget. + tracked (bool, optional): Scroll tracking + track_offset (float, optional): 0.0f:top, 0.5f:center, 1.0f:bottom + wrap (int, optional): Number of pixels from the start of the item until wrapping starts. + bullet (bool, optional): Places a bullet to the left of the text. + color (Union[List[int], Tuple[int, ...]], optional): Color of the text (rgba). + show_label (bool, optional): Displays the label to the right of the text. + id (Union[int, str], optional): (deprecated) + Returns: + Union[int, str] + """ + dpg_text = dpg.add_text(default_value, label=label, user_data=user_data, use_internal_label=use_internal_label, tag=tag, indent=indent, parent=parent, before=before, source=source, payload_type=payload_type, drag_callback=drag_callback, drop_callback=drop_callback, show=show, pos=pos, filter_key=filter_key, tracked=tracked, track_offset=track_offset, wrap=wrap, bullet=bullet, color=color, show_label=show_label, **kwargs) + dpg.bind_item_font(dpg_text, + font=font_attributes.Bold.get_font()) + return dpg_text + + +def add_text_bold_italic(default_value: str = '', *, label: str = None, user_data: Any = None, use_internal_label: bool = True, tag: Union[int, str] = 0, indent: int = -1, parent: Union[int, str] = 0, before: Union[int, str] = 0, source: Union[int, str] = 0, payload_type: str = '$$DPG_PAYLOAD', drag_callback: Callable = None, drop_callback: Callable = None, show: bool = True, pos: Union[List[int], Tuple[int, ...]] = [], filter_key: str = '', tracked: bool = False, track_offset: float = 0.5, wrap: int = -1, bullet: bool = False, color: Union[List[int], Tuple[int, ...]] = (-255, 0, 0, 255), show_label: bool = False, **kwargs) -> Union[int, str]: + """ Adds bold italic text. Text can have an optional label that will display to the right of the text. + + Args: + default_value (str, optional): + label (str, optional): Overrides 'name' as label. + user_data (Any, optional): User data for callbacks + use_internal_label (bool, optional): Use generated internal label instead of user specified (appends ### uuid). + tag (Union[int, str], optional): Unique id used to programmatically refer to the item.If label is unused this will be the label. + indent (int, optional): Offsets the widget to the right the specified number multiplied by the indent style. + parent (Union[int, str], optional): Parent to add this item to. (runtime adding) + before (Union[int, str], optional): This item will be displayed before the specified item in the parent. + source (Union[int, str], optional): Overrides 'id' as value storage key. + payload_type (str, optional): Sender string type must be the same as the target for the target to run the payload_callback. + drag_callback (Callable, optional): Registers a drag callback for drag and drop. + drop_callback (Callable, optional): Registers a drop callback for drag and drop. + show (bool, optional): Attempt to render widget. + pos (Union[List[int], Tuple[int, ...]], optional): Places the item relative to window coordinates, [0,0] is top left. + filter_key (str, optional): Used by filter widget. + tracked (bool, optional): Scroll tracking + track_offset (float, optional): 0.0f:top, 0.5f:center, 1.0f:bottom + wrap (int, optional): Number of pixels from the start of the item until wrapping starts. + bullet (bool, optional): Places a bullet to the left of the text. + color (Union[List[int], Tuple[int, ...]], optional): Color of the text (rgba). + show_label (bool, optional): Displays the label to the right of the text. + id (Union[int, str], optional): (deprecated) + Returns: + Union[int, str] + """ + dpg_text = dpg.add_text(default_value, label=label, user_data=user_data, use_internal_label=use_internal_label, tag=tag, indent=indent, parent=parent, before=before, source=source, payload_type=payload_type, drag_callback=drag_callback, drop_callback=drop_callback, show=show, pos=pos, filter_key=filter_key, tracked=tracked, track_offset=track_offset, wrap=wrap, bullet=bullet, color=color, show_label=show_label, **kwargs) + dpg.bind_item_font(dpg_text, + font=font_attributes.BoldItalic.get_font()) + return dpg_text diff --git a/DearPyGui_Markdown/attribute_types.py b/DearPyGui_Markdown/attribute_types.py new file mode 100644 index 0000000..fd8fda3 --- /dev/null +++ b/DearPyGui_Markdown/attribute_types.py @@ -0,0 +1,179 @@ +import traceback + +import dearpygui.dearpygui as dpg + +from . import CallInNextFrame + +font_registry = 0 +add_font = dpg.add_font + + +def set_font_registry(font_registry_tag: int | str): + global font_registry + font_registry = font_registry_tag + + +def set_add_font_function(function): + global add_font + add_font = function + + +def math_round(number: float | int) -> int: + return int(number + (0.5 if number > 0 else -0.5)) + + +class AttributeConnector(list): + handler: int | None = None + + def __hash__(self): + return id(self) + + +class Attribute: + attribute_connector: AttributeConnector | None = None + + @property + def object(self): + return type(self) + + def __eq__(self, other): + if type(other) is type: + return type(self) == other + return super().__eq__(other) + + +class LineAttribute(Attribute): + def get_width(self) -> int | float: + ... + + def render(self, text_height: int | float, parent=0, attributes_group=0): + ... + + @CallInNextFrame + def post_render(self, attributes_group=0): + ... + + +class FontAttribute(Attribute): + _fonts: dict = None + + font_path: str + font = None + now_font_size = None + + @classmethod + def set_font(cls, path, size: float | int): + cls.font_path = path + cls._fonts = {} + size = math_round(size) + cls.font = cls.get_font(size) + cls.now_font_size = size + + @classmethod + def get_font(cls, size: float | int = None): + if size is None: + return cls.font + size = math_round(size) + if cls._fonts is None: + return None + font = cls._fonts.get(size, None) + if font is not None: + return font + font = add_font(file=cls.font_path, size=size, parent=font_registry) + cls._fonts[size] = font + return font + + @classmethod + def get_now_font_size(cls): + return cls.now_font_size + + +class HoverAttribute(Attribute): + _mouse_move_handler = None + _hovered_items = {} + _handler: int = None + + now_hover_item = None + + def __new__(cls, *args, **kwargs): + if cls._mouse_move_handler is None: + with dpg.handler_registry() as cls._mouse_move_handler: + dpg.add_mouse_move_handler(callback=lambda: cls._check_hovered_items()) + return super().__new__(cls) + + def __init__(self, attribute_connector: AttributeConnector | None): + if attribute_connector is None: + attribute_connector = AttributeConnector() + self.attribute_connector = attribute_connector + + @classmethod + def _check_hovered_items(cls): + for item in list(cls._hovered_items): + if not dpg.is_item_hovered(item): + callback = cls._hovered_items[item] + try: + callback() + except Exception: + traceback.print_exc() + del cls._hovered_items[item] + + @classmethod + def add_to_check_hover_items(cls, item: int | str, callback=None): + cls._hovered_items[item] = callback + + def _create_handler(self): + if self.attribute_connector is not None: + if self.attribute_connector.handler is not None: + self._handler = self.attribute_connector.handler + return + + with dpg.item_handler_registry() as handler: + dpg.add_item_clicked_handler(callback=lambda s, info, u: self.click(info[0])) + dpg.add_item_hover_handler(callback=lambda s, item, u: self._hover(item)) + self._handler = handler + + if self.attribute_connector is not None: + self.attribute_connector.handler = self._handler + + def add_item_to_handler(self, item): + if self._handler is None: + self._create_handler() + dpg.bind_item_handler_registry(item, self._handler) + + def render(self, *args, **kwargs): + self.attribute_connector.append(self) + + def hover(self): + ... + + def _hover(self, hovere_item): + if self.now_hover_item is not None: + if dpg.is_item_hovered(hovere_item): + self.now_hover_item = hovere_item + return + self.now_hover_item = hovere_item + self.add_to_check_hover_items(hovere_item, self._unhover) + + if self.attribute_connector is None: + self.hover() + else: + for attribute in self.attribute_connector: + attribute.hover() + + def unhover(self): + ... + + def _unhover(self): + if dpg.is_item_hovered(self.now_hover_item): + self.add_to_check_hover_items(self.now_hover_item, self._unhover) + return + self.now_hover_item = None + + if self.attribute_connector is None: + self.unhover() + else: + for attribute in self.attribute_connector: + attribute.unhover() + + def click(self, mouse_button): + ... diff --git a/DearPyGui_Markdown/font_attributes.py b/DearPyGui_Markdown/font_attributes.py new file mode 100644 index 0000000..2a5b30f --- /dev/null +++ b/DearPyGui_Markdown/font_attributes.py @@ -0,0 +1,89 @@ +import ast +import os.path + +from .attribute_types import * + + +class Font(Attribute): + color: list[int, int, int, int] + size: int | None + + def __init__(self, color: str | list, size: str | float | int): + if isinstance(size, str): + try: + size = ast.literal_eval(size) + except Exception: + traceback.print_exc() + size = None + self.size = size + + if not isinstance(color, list) and not isinstance(color, tuple): + try: + color = ast.literal_eval(color) + except Exception: + color = color.removeprefix('#') + color = tuple(int(color[i:i + 2], 16) for i in [*range(0, len(color), 2)]) # HEX to RGB + color = list(color)[:4:] + for i in range(4 - len(color)): + color.append(255) + self.color = color + + +class Default(FontAttribute): ... + + +class Bold(FontAttribute): ... + + +class Italic(FontAttribute): ... + + +class BoldItalic(FontAttribute): ... + + +class H1(FontAttribute): + font_multiply = 2 + + +class H2(FontAttribute): + font_multiply = 1.5 + + +class H3(FontAttribute): + font_multiply = 1.17 + + +class H4(FontAttribute): + font_multiply = 1 + + +class H5(FontAttribute): + font_multiply = 0.83 + + +class H6(FontAttribute): + font_multiply = 0.67 + + +def set_font(font_size: int | float = 13, *, + default: str | os.PathLike[str] = None, + bold: str | os.PathLike[str] = None, + italic: str | os.PathLike[str] = None, + italic_bold: str | os.PathLike[str] = None) -> int: + """ + :return: default font + """ + fonts = { + Default: default, + Bold: bold, + Italic: italic, + BoldItalic: italic_bold, + } + for Font in fonts: + font_path = fonts[Font] + if font_path: + Font.set_font(font_path, font_size) + else: + Font.set_font(Font.font_path, font_size) + + return Default.get_font() diff --git a/DearPyGui_Markdown/line_atributes.py b/DearPyGui_Markdown/line_atributes.py new file mode 100644 index 0000000..a17652a --- /dev/null +++ b/DearPyGui_Markdown/line_atributes.py @@ -0,0 +1,211 @@ +import dearpygui.dearpygui as dpg + +from . import get_text_size +from .attribute_types import CallInNextFrame +from .attribute_types import LineAttribute, AttributeConnector +from .font_attributes import Default + + +class Separator(LineAttribute): + @staticmethod + def render(parent=0, attributes_group=0): # noqa + height = get_text_size('|', font=Default.get_font())[1] + with dpg.group(before=parent) as group: + dpg.add_spacer(parent=group, height=int(height * 0.5)) + dpg.add_separator(parent=group) + dpg.add_spacer(parent=group, height=int(height * 0.5)) + + +class Blockquote(LineAttribute): + width = 20 + line_width = 6 + + depth: int + color = [50, 55, 65, 255] + + drawlist_group: int + + def __init__(self, depth: int, attribute_connector: AttributeConnector): + self.depth = depth + self.attribute_connector = attribute_connector + + def __repr__(self): + return f"
tag is
+ # probably intended for syntax highlighting.
+ #
+ # Syntax highlighting is set with
+ # codeblock
+ # inside tags
+ pre = self._building_entities['pre']
+ try:
+ pre.language = attrs['class'][len('language-'):]
+ except KeyError:
+ pass
+ except KeyError:
+ EntityType = MessageEntityCode
+ case "pre":
+ EntityType = MessageEntityPre
+ args['language'] = ''
+ case "a":
+ url = attrs.get("href", None)
+ if not url:
+ return
+ if url.startswith('mailto:'):
+ url = url.removeprefix('mailto:')
+ EntityType = MessageEntityEmail
+ else:
+ if self.get_starttag_text() == url:
+ EntityType = MessageEntityUrl
+ else:
+ EntityType = MessageEntityTextUrl
+ args['url'] = url
+ url = None
+ self._open_tags_meta.popleft()
+ self._open_tags_meta.appendleft(url)
+ case "font" | "span":
+ EntityType = MessageEntityFont
+ color = attrs.get("color", None)
+ size = attrs.get("size", None)
+ style_string = attrs.get("style", None)
+ try:
+ if style_string:
+ style_dict = {}
+ for style in style_string.split(";"):
+ style = style.split(":", 1)
+ style_dict[style[0]] = style[1].strip()
+ color = style_dict.get("color", None).removeprefix("rgb").removeprefix("a")
+ except Exception:
+ traceback.print_exc()
+
+ if color:
+ args["color"] = color
+ if size:
+ args["size"] = size
+ case "ol":
+ self.opened_list_depth.append(MessageEntityOrderedList)
+ ordered_list_index = attrs.get("start", 1)
+ try:
+ ordered_list_index = int(ordered_list_index)
+ except Exception:
+ ordered_list_index = 1
+ traceback.print_exc()
+ finally:
+ self.ordered_list_index_by_depth[len(self.opened_list_depth)] = ordered_list_index
+ case "ul":
+ self.opened_list_depth.append(MessageEntityUnorderedList)
+ case "li":
+ self.li_open_count += 1
+ tag = f"{tag}_{self.li_open_count}"
+ if 'task' in attrs:
+ args['task'] = True
+ elif 'task-done' in attrs:
+ args['task'] = True
+ args['task_done'] = True
+
+ EntityType = self.opened_list_depth[-1]
+ args["depth"] = len(self.opened_list_depth)
+ if EntityType is MessageEntityOrderedList:
+ args["index"] = self.ordered_list_index_by_depth[len(self.opened_list_depth)]
+ self.ordered_list_index_by_depth[len(self.opened_list_depth)] += 1
+ case "hr":
+ EntityType = MessageEntitySeparator
+ case "h1":
+ EntityType = MessageEntityH1
+ case "h2":
+ EntityType = MessageEntityH2
+ case "h3":
+ EntityType = MessageEntityH3
+ case "h4":
+ EntityType = MessageEntityH4
+ case "h5":
+ EntityType = MessageEntityH5
+ case "h6":
+ EntityType = MessageEntityH6
+ if EntityType is not None and tag not in self._building_entities:
+ self._building_entities[tag] = EntityType(
+ offset=len(self.text),
+ # The length will be determined when closing the tag.
+ length=0,
+ **args)
+
+ def handle_data(self, text):
+ previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''
+ if previous_tag == 'a':
+ url = self._open_tags_meta[0]
+ if url:
+ text = url
+
+ text = html.unescape(text)
+ for tag, entity in self._building_entities.items():
+ entity.length += len(text)
+
+ self.text += text
+
+ def handle_endtag(self, tag):
+ try:
+ self._open_tags.popleft()
+ self._open_tags_meta.popleft()
+ except IndexError:
+ pass
+ match tag:
+ case "blockquote":
+ tag = f"{tag}_{self.blockquote_depth}"
+ self.blockquote_depth -= 1
+ case "ol":
+ self.ordered_list_index_by_depth[len(self.opened_list_depth)] = 1
+ del self.opened_list_depth[-1]
+ case "ul":
+ del self.opened_list_depth[-1]
+ case "li":
+ tag = f"{tag}_{self.li_open_count}"
+ self.li_open_count += -1
+ entity = self._building_entities.pop(tag, None)
+ if not entity:
+ return
+
+ self.entities.append(entity)
+
+
+class _PygmentsRenderer(mistletoe.HTMLRenderer):
+ formatter = HtmlFormatter(style='monokai')
+ formatter.noclasses = True
+
+ def __init__(self, *extras):
+ super().__init__(*extras)
+
+ def render_block_code(self, token):
+ code = token.children[0].content
+ lexer = get_lexer(token.language) if token.language else guess_lexer(code)
+ return highlight(code, lexer, self.formatter)
+
+
+def parse(html_text: str) -> [str, list[MessageEntity]]:
+ """
+ Parses the given HTML message and returns its stripped representation
+ plus a list of the MessageEntity's that were found.
+
+ :param html: the message with HTML to be parsed.
+ :return: a tuple consisting of (clean message, [message entities]).
+ """
+ if not html_text:
+ return html_text, []
+ # html_text = html.unescape(_MarkdownIt.render(html_text))
+ html_text = mistletoe.markdown(html_text, renderer=_PygmentsRenderer)
+
+ html_text = html_text.replace('\n', '').replace('\n
', '
')
+ html_text = html_text.replace('
\n', ' ').replace('\n ', '')
+ html_text = html_text.replace('\n', '').replace('\n
', '
')
+ html_text = html_text.replace('\n', '').replace('\n
', '
')
+ html_text = html_text.replace('\n', '')
+
+ html_text = html_text.replace('\n', '\n')
+ html_text = html_text.replace('', '\n')
+ # task list support
+ html_text = html_text.replace("- [x] ", "
- [X] ")
+ html_text = html_text.replace("
- [X] ", "
- ")
+ html_text = html_text.replace("
- [ ] ", "
- ")
+
+ # html_text = html_text.replace("
", "
\n")
+
+ parser = _HTMLToParser()
+ parser.feed(_add_surrogate(html_text))
+ text = _strip_text(parser.text, parser.entities)
+ return _del_surrogate(text), parser.entities
diff --git a/DearPyGui_Markdown/text_attributes.py b/DearPyGui_Markdown/text_attributes.py
new file mode 100644
index 0000000..bf53762
--- /dev/null
+++ b/DearPyGui_Markdown/text_attributes.py
@@ -0,0 +1,157 @@
+from . import get_text_size
+from .attribute_types import *
+
+
+class Underline(Attribute):
+ @staticmethod
+ def render(dpg_text: int, dpg_text_group: int, font=None, parent=0, color=(255, 255, 255, 255)):
+ '''
+ :return: [drawlist, draw_line]
+ '''
+ pos = dpg.get_item_pos(dpg_text_group)
+ x, y = pos
+ group_width, group_height = dpg.get_item_rect_size(dpg_text_group)
+ text_width, text_height = get_text_size(dpg.get_value(dpg_text), font=font)
+ y = y + (group_height - text_height) / 2
+ with dpg.group(pos=[x, y], parent=parent) as drawlist_group:
+ with dpg.drawlist(parent=drawlist_group, width=group_width, height=text_height) as drawlist:
+ thickness = text_height / 15
+ line_y = text_height - thickness + thickness / 5
+ line = dpg.draw_line([0, line_y], [group_width, line_y], parent=drawlist, color=color, thickness=thickness)
+ return drawlist, line
+
+
+class Strike(Attribute):
+ @staticmethod
+ def render(dpg_text: int, dpg_text_group: int, font=None, parent=0, color=(255, 255, 255)):
+ '''
+ :return: [drawlist, draw_line]
+ '''
+ pos = dpg.get_item_pos(dpg_text_group)
+ x, y = pos
+ group_width, group_height = dpg.get_item_rect_size(dpg_text_group)
+ text_width, text_height = get_text_size(dpg.get_value(dpg_text), font=font)
+ y = y + (group_height - text_height) / 2
+ with dpg.group(pos=[x, y], parent=parent) as drawlist_group:
+ with dpg.drawlist(parent=drawlist_group, width=group_width, height=text_height) as drawlist:
+ thickness = text_height / 15
+ line_y = text_height / 2 + thickness / 2 + text_height / 20
+ line = dpg.draw_line([0, line_y], [group_width, line_y], parent=drawlist, color=color, thickness=thickness)
+ return drawlist, line
+
+
+class Code(Attribute):
+ color = (55, 55, 65, 255)
+ border_color = color
+
+ @classmethod
+ def render(cls, dpg_text_group: int):
+ width, height = dpg.get_item_rect_size(dpg_text_group)
+ pos = dpg.get_item_pos(dpg_text_group)
+ child = dpg.get_item_children(dpg_text_group, 1)[0]
+ group = dpg.add_group(pos=pos, before=child)
+ with dpg.drawlist(parent=group, width=width, height=height) as drawlist:
+ dpg.draw_quad([0, 0], [width, 0],
+ [width, height], [0, height],
+ fill=cls.color,
+ color=cls.border_color,
+ parent=drawlist)
+
+
+class Pre(Attribute):
+ color = (55, 55, 65, 255)
+ border_color = (110, 110, 130, 200)
+
+ def __init__(self, attribute_connector: AttributeConnector):
+ self.attribute_connector = attribute_connector
+ self.attribute_connector.max_width = 0
+ self.attribute_connector.x0, self.attribute_connector.y0 = (None, None)
+ self.attribute_connector.x1, self.attribute_connector.y1 = (None, None)
+ self.attribute_connector.used_y = []
+
+ def render(self, dpg_text_group: int):
+ self.dpg_text_group = dpg_text_group
+ self.width, self.height = dpg.get_item_rect_size(dpg_text_group)
+ pos = dpg.get_item_pos(dpg_text_group)
+ pos_end = (self.width + pos[0], pos[1] + self.height)
+
+ a_c = self.attribute_connector
+ if a_c.x0 is None:
+ a_c.x0, a_c.y0 = pos
+ a_c.x1, a_c.y1 = pos_end
+
+ if a_c.x0 > pos[0]:
+ a_c.x0 = pos[0]
+ if a_c.y0 > pos[1]:
+ a_c.y0 = pos[1]
+ if a_c.x1 < pos_end[0]:
+ a_c.x1 = pos_end[0]
+ if a_c.y1 < pos_end[1]:
+ a_c.y1 = pos_end[1]
+
+ @CallInNextFrame
+ def post_render(self, attributes_group=0):
+ width, height = dpg.get_item_rect_size(self.dpg_text_group)
+ pos = dpg.get_item_pos(self.dpg_text_group)
+ child = dpg.get_item_children(self.dpg_text_group, 1)[0]
+ group = dpg.add_group(pos=pos, before=child)
+ children = dpg.get_item_children(dpg.get_item_parent(self.dpg_text_group), 1)
+ a_c = self.attribute_connector
+ if children[-1] == self.dpg_text_group:
+ width = a_c.x1 - pos[0]
+ with dpg.group(parent=attributes_group, pos=(a_c.x0, a_c.y0)) as border_group:
+ border_width = a_c.x1 - a_c.x0
+ border_height = a_c.y1 - a_c.y0
+ with dpg.drawlist(parent=border_group, width=border_width, height=border_height) as border_drawlist:
+ dpg.draw_quad([0, 0], [border_width, 0],
+ [border_width, border_height], [0, border_height],
+ color=self.border_color,
+ parent=border_drawlist)
+
+ with dpg.drawlist(parent=group, width=width, height=height) as drawlist:
+ dpg.draw_quad([0, 0], [width, 0],
+ [width, height], [0, height],
+ fill=self.color,
+ color=self.color,
+ parent=drawlist)
+
+
+class Url(HoverAttribute):
+ color: list[int, int, int, int] = (85, 135, 205, 255)
+ line_color: list[int, int, int, int] = (255, 255, 255, 0)
+ hover_color: list[int, int, int, int] = (153, 187, 255, 255)
+
+ url: str
+
+ dpg_text_objects: list[int]
+ underline_objects: list[int]
+
+ def __init__(self, url: str, attribute_connector: AttributeConnector | None):
+ super().__init__(attribute_connector)
+ self.url = url
+ self.dpg_text_objects = []
+ self.underline_objects = []
+ self.now_hover_item = None
+
+ def render(self, dpg_text, font=None, parent=0):
+ super().render()
+ self.add_item_to_handler(dpg_text)
+ self.dpg_text_objects.append(dpg_text)
+ dpg.configure_item(dpg_text, color=self.color)
+
+ def hover(self):
+ for item in self.dpg_text_objects:
+ dpg.configure_item(item, color=self.hover_color)
+ for item in self.underline_objects:
+ dpg.configure_item(item, color=self.hover_color)
+
+ def unhover(self):
+ for item in self.dpg_text_objects:
+ dpg.configure_item(item, color=self.color)
+ for item in self.underline_objects:
+ dpg.configure_item(item, color=self.line_color)
+
+ def click(self, mouse_button):
+ if mouse_button in [2, 0]:
+ import webbrowser
+ webbrowser.open_new_tab(self.url)
diff --git a/DearPyGui_Markdown/text_entities.py b/DearPyGui_Markdown/text_entities.py
new file mode 100644
index 0000000..068960d
--- /dev/null
+++ b/DearPyGui_Markdown/text_entities.py
@@ -0,0 +1,422 @@
+import copy
+from typing import TypeVar
+
+import dearpygui.dearpygui as dpg # noqa
+
+from .font_attributes import *
+from .line_atributes import *
+from .text_attributes import *
+
+
+class AttributeController(list[Attribute]):
+ dpg_group_theme: int = None
+ text_color: list[int, int, int, int]
+ font: None | int
+
+ def __new__(cls, *args, **kwargs):
+ if cls.dpg_group_theme is None:
+ with dpg.theme() as cls.dpg_group_theme:
+ with dpg.theme_component(dpg.mvAll):
+ dpg.add_theme_style(dpg.mvStyleVar_ItemSpacing, 0, 0, category=dpg.mvThemeCat_Core)
+ return list.__new__(cls)
+
+ def __init__(self, attributes: list[Attribute]):
+ super().__init__()
+ self.clear()
+
+ for attribute in reversed(attributes):
+ if attribute in self:
+ continue
+
+ if attribute in [Bold, Italic]:
+ if BoldItalic in self:
+ continue
+ opposite_attribute = Italic if (attribute is Bold) else Bold
+ if opposite_attribute in self:
+ self[self.index(opposite_attribute)] = BoldItalic # noqa
+ continue
+ self.append(attribute)
+
+ def get_font(self) -> None | int:
+ font_size = Default.get_now_font_size()
+
+ used_heading_attribute = False
+ for heading_attribute in [H1, H2, H3, H4, H5, H6]:
+ if heading_attribute in self:
+ font_size *= heading_attribute.font_multiply
+ used_heading_attribute = True
+ break
+
+ if Font in self:
+ _Font: Font = self[self.index(Font)] # noqa
+ font_size = _Font.size
+
+ if Bold in self:
+ self.font = Bold.get_font(font_size)
+ elif Italic in self:
+ self.font = Italic.get_font(font_size)
+ elif BoldItalic in self:
+ self.font = BoldItalic.get_font(font_size)
+ else:
+ if used_heading_attribute:
+ self.font = Bold.get_font(font_size)
+ else:
+ self.font = Default.get_font(font_size)
+ return self.font
+
+ def get_color(self) -> list[int, int, int, int]:
+ self.text_color = [255, 255, 255, 255]
+ if Font in self:
+ _Font: Font = self[self.index(Font)] # noqa
+ self.text_color = _Font.color
+
+ if Url in self:
+ _Url: Url = self[self.index(Url)] # noqa
+ self.text_color = _Url.color
+ return self.text_color
+
+ def get_height(self) -> float | int:
+ return get_text_size('Tg,y', font=self.get_font())[1]
+
+ def render(self, text: str, parent=0, attributes_group=0, max_text_height: int | float = -1):
+ if Separator in self:
+ return
+
+ self.get_font()
+ self.get_color()
+
+ parent_text_group = parent
+ if max_text_height > 0:
+ spacer_height = max_text_height - get_text_size(text, font=self.font)[1]
+ if spacer_height > 1:
+ with dpg.group(parent=parent) as parent_text_group:
+ dpg.add_spacer(height=spacer_height, parent=parent_text_group)
+ dpg.bind_item_theme(parent_text_group, self.dpg_group_theme)
+
+ with dpg.group(parent=parent_text_group) as dpg_text_group:
+ dpg_text = dpg.add_text(text, parent=dpg_text_group, color=self.text_color)
+ dpg.bind_item_font(dpg_text, self.font)
+ dpg.bind_item_theme(dpg_text_group, self.dpg_group_theme)
+
+ self.render_attributes(dpg_text, dpg_text_group, self.font, attributes_group)
+
+ @CallInNextFrame
+ def render_attributes(self, dpg_text, dpg_text_group, font=None, attributes_group=0):
+ strike_drawlist, strike_line = None, None
+ if Strike in self:
+ strike_drawlist, strike_line = Strike.render(dpg_text, dpg_text_group,
+ font=font,
+ parent=attributes_group,
+ color=self.text_color)
+
+ underline_drawlist, underline_line = None, None
+ if Underline in self:
+ underline_drawlist, underline_line = Underline.render(dpg_text, dpg_text_group,
+ font=font,
+ parent=attributes_group,
+ color=self.text_color)
+
+ if Url in self:
+ url_attribute: Url = self[self.index(Url)] # noqa
+ if strike_drawlist: # Strike in self
+ url_attribute.add_item_to_handler(strike_drawlist)
+ url_attribute._objects.append(strike_line)
+ dpg.configure_item(strike_line, color=url_attribute.color)
+
+ if underline_drawlist: # Underline in self
+ url_attribute.line_color = url_attribute.color
+ else:
+ underline_drawlist, underline_line = Underline.render(dpg_text, dpg_text_group, font=font, parent=attributes_group)
+
+ url_attribute.underline_objects.append(underline_line)
+ dpg.configure_item(underline_line, color=url_attribute.line_color)
+
+ url_attribute.render(dpg_text, font=font, parent=attributes_group)
+
+ if Code in self:
+ Code.render(dpg_text_group)
+ if Pre in self:
+ pre_attribute: Pre = self[self.index(Pre)] # noqa
+ pre_attribute.render(dpg_text_group)
+
+
+SelfStrEntity = TypeVar("SelfStrEntity", bound="StrEntity")
+SelfTextEntity = TypeVar("SelfTextEntity", bound="TextEntity")
+
+
+class StrEntity(str):
+ attributes: AttributeController
+
+ def __init__(self, text: str = ''):
+ self.attributes = AttributeController([])
+
+ super(StrEntity, self).__init__()
+
+ def set_attributes(self, attributes: list[Attribute] | Attribute):
+ self.attributes = AttributeController(attributes)
+
+ def __repr__(self):
+ return f'<{str(self)} ({self.attributes})>'
+
+ def __str__(self):
+ return super(StrEntity, self).__str__()
+
+ def __add__(self, __object: SelfStrEntity | SelfTextEntity) -> SelfStrEntity | SelfTextEntity:
+ if isinstance(__object, StrEntity):
+ if self.attributes == __object.attributes:
+ str_entity = StrEntity(''.join([self, __object]))
+ str_entity.attributes = self.attributes
+ return str_entity
+ elif isinstance(__object, StrEntity) and len(__object) == 0:
+ return self
+ elif len(self) == 0:
+ return __object
+ else:
+ return TextEntity([self, __object])
+ elif isinstance(__object, TextEntity) or issubclass(__object, TextEntity):
+ add_object = self + __object[0]
+ if isinstance(add_object, StrEntity):
+ return TextEntity([add_object, *__object[1::]])
+ elif isinstance(add_object, TextEntity):
+ return TextEntity([self, *__object])
+ elif isinstance(add_object, LineEntity):
+ return LineEntity([self, *__object])
+ else:
+ return TextEntity([self]) + __object
+
+ def get_width(self) -> float | int:
+ font = self.attributes.get_font()
+ text = str(self)
+ return get_text_size(text, font=font)[0]
+
+ def get_height(self) -> float | int:
+ return self.attributes.get_height()
+
+ def get_all_attributes(self):
+ return [*self.attributes]
+
+ def recreate_attributes(self):
+ list_of_attributes = self.attributes.copy()
+ del self.attributes
+ self.attributes = AttributeController([])
+ for i, attribute in enumerate(list_of_attributes):
+ if not isinstance(attribute, type):
+ attribute_connector = attribute.attribute_connector
+ attribute = copy.deepcopy(attribute)
+ attribute.attribute_connector = attribute_connector
+ self.attributes.append(attribute)
+
+ def items(self) -> list[SelfStrEntity]:
+ items = [*self]
+ for i in range(len(items)):
+ items[i] = StrEntity(items[i])
+ items[i].attributes = self.attributes
+ return items
+
+ def chars(self) -> list[SelfStrEntity]:
+ return self.items()
+
+ def split(self, sep: str | None = None, **kwargs) -> list[SelfStrEntity]:
+ _list: list[StrEntity | str] = super(StrEntity, self).split(sep, **kwargs)
+ for i in range(len(_list)):
+ _list[i] = StrEntity(_list[i])
+ _list[i].attributes = self.attributes
+ return _list
+
+ def render(self, parent=0, attributes_group=0, max_text_height: int | float = -1):
+ self.attributes.render(text=str(self),
+ parent=parent,
+ attributes_group=attributes_group,
+ max_text_height=max_text_height)
+
+
+class TextEntity(list[StrEntity | SelfTextEntity]):
+ def split(self, sep: str | None = None) -> list[StrEntity | list[SelfStrEntity]]:
+ _list = []
+ for StrEntity in self:
+ parts = StrEntity.split(sep)
+ if len(parts) == 0:
+ continue
+
+ if len(_list) == 0:
+ _list.extend(parts)
+ else:
+ _list[-1] = _list[-1] + parts[0]
+ _list.extend(parts[1::])
+
+ return _list
+
+ def __add__(_self, __object: StrEntity | SelfTextEntity) -> SelfTextEntity:
+ self = TextEntity(_self.copy())
+ if isinstance(__object, StrEntity):
+ add_object = self[-1] + __object
+ if isinstance(add_object, StrEntity):
+ self[-1] = add_object
+ elif isinstance(add_object, TextEntity) or issubclass(add_object, TextEntity):
+ del self[-1]
+ self.extend(add_object)
+ else:
+ raise ValueError(f'Undefined type: {type(add_object)}')
+ return self
+ else:
+ add_object = self[-1] + __object[0]
+ if isinstance(add_object, StrEntity):
+ self[-1] = add_object
+ self.extend(__object[1::])
+ elif isinstance(add_object, TextEntity) or issubclass(add_object, TextEntity):
+ self.extend(__object)
+ else:
+ raise ValueError(f'Undefined type: {type(add_object)}')
+ return self
+
+ def __repr__(self):
+ return f''
+
+ def __str__(self):
+ return ''.join(str(item) for item in self)
+
+ def recreate_attributes(self):
+ for item in self:
+ item.recreate_attributes()
+
+ def get_width(self) -> float | int:
+ width = 0
+ for item in self:
+ width += item.get_width()
+
+ return width
+
+ def get_height(self) -> float | int:
+ max_height = 0
+ for item in self:
+ height = item.get_height()
+ if height > max_height:
+ max_height = height
+ return max_height
+
+ @property
+ def attributes(self) -> AttributeController:
+ if len(self) == 0:
+ return AttributeController([])
+ return self[0].attributes
+
+ def get_all_attributes(self) -> list[Attribute]:
+ attributes = []
+ for item in self:
+ attributes.extend(item.get_all_attributes())
+ return attributes
+
+ def items(self) -> list[StrEntity | SelfTextEntity]:
+ return [*self]
+
+ def chars(self) -> list[StrEntity]:
+ all_chars = []
+ for item in self:
+ all_chars.extend(item.chars())
+ return all_chars
+
+ def render(self, parent=0, attributes_group=0, max_text_height: int | float = -1):
+ with dpg.group(horizontal=True, parent=parent) as group:
+ for item in self:
+ item.render(parent=group,
+ attributes_group=attributes_group,
+ max_text_height=max_text_height)
+ dpg.bind_item_theme(group, AttributeController.dpg_group_theme)
+
+
+class LineEntity(TextEntity):
+ post_render_queue: list
+
+ def __repr__(self):
+ return f''
+
+ def __str__(self):
+ return '\n'.join(str(item) for item in self)
+
+ def recreate_attributes(self):
+ for item in self:
+ item.recreate_attributes()
+
+ def append(self, __object: TextEntity | StrEntity) -> None:
+ __object.recreate_attributes()
+ super().append(__object)
+ attributes = __object.get_all_attributes()
+ list_attributes: list[List] = self.get_attributes_by_type(attributes, List) # noqa
+ if len(list_attributes) > 0:
+ sorted_by_attribute_connector = {}
+ for attribute in list_attributes:
+ attribute_connector = attribute.attribute_connector
+ if attribute_connector.first_line_objects is not None:
+ continue
+ if attribute_connector not in sorted_by_attribute_connector:
+ sorted_by_attribute_connector[attribute_connector] = list()
+ sorted_by_attribute_connector[attribute_connector].append(attribute)
+
+ for attribute_connector in sorted_by_attribute_connector:
+ attribute_connector.first_line_objects = []
+ for attribute in sorted_by_attribute_connector[attribute_connector]:
+ attribute_connector.first_line_objects.append(attribute)
+
+ @staticmethod
+ def get_attributes_by_type(attributes: list[Attribute], type: type) -> list[Attribute]:
+ return list(filter(lambda item: isinstance(item, type), attributes))
+
+ @staticmethod
+ def remove_duplicates_by_depth(attributes: list[Attribute]) -> list[Attribute]:
+ return list({i.depth: i for i in attributes}.values())
+
+ @staticmethod
+ def get_width(text_entity: TextEntity | StrEntity) -> float | int: # noqa
+ width = text_entity.get_width()
+ attributes = text_entity.get_all_attributes()
+
+ blockquote_attributes: list[Blockquote] = LineEntity.get_attributes_by_type(attributes, Blockquote) # noqa
+ blockquote_attributes: list[Blockquote] = LineEntity.remove_duplicates_by_depth(blockquote_attributes) # noqa
+ for attribute in blockquote_attributes:
+ width += attribute.get_width()
+
+ list_attributes: list[List] = LineEntity.get_attributes_by_type(attributes, List) # noqa
+ list_attributes: list[List] = LineEntity.remove_duplicates_by_depth(list_attributes) # noqa
+ for attribute in list_attributes:
+ width += attribute.get_width()
+ separator_attributes: list[Separator] = LineEntity.get_attributes_by_type(attributes, Separator) # noqa
+ if len(separator_attributes) > 0:
+ width = -1
+
+ return width
+
+ def render(self, parent=0, attributes_group=0): # noqa
+ self.post_render_queue = list()
+ for item in self:
+ with dpg.group(horizontal=True, parent=parent) as group:
+ self.render_attributes(item, parent=group, attributes_group=attributes_group)
+ item.render(parent=group,
+ attributes_group=attributes_group,
+ max_text_height=item.get_height())
+
+ dpg.bind_item_theme(group, AttributeController.dpg_group_theme)
+
+ for attribute in self.post_render_queue:
+ attribute.post_render(attributes_group=attributes_group)
+ del self.post_render_queue
+
+ def render_attributes(self, text_entity: TextEntity, parent=0, attributes_group=0):
+ attributes = text_entity.get_all_attributes()
+ text_height = text_entity.get_height()
+ blockquote_attributes: list[Blockquote] = self.get_attributes_by_type(attributes, Blockquote) # noqa
+ blockquote_attributes: list[Blockquote] = self.remove_duplicates_by_depth(blockquote_attributes) # noqa
+ for attribute in blockquote_attributes:
+ attribute.render(text_height, parent=parent, attributes_group=attributes_group)
+
+ list_attributes: list[List] = self.get_attributes_by_type(attributes, List) # noqa
+ list_attributes: list[List] = self.remove_duplicates_by_depth(list_attributes) # noqa
+ for attribute in list_attributes:
+ attribute.render(text_height, parent=parent, attributes_group=attributes_group)
+ self.post_render_queue.append(attribute)
+
+ pre_attributes: list[Pre] = self.get_attributes_by_type(attributes, Pre) # noqa
+ for attribute in pre_attributes:
+ self.post_render_queue.append(attribute)
+
+ if Separator in attributes:
+ Separator.render(parent=parent, attributes_group=attributes_group)
diff --git a/README.md b/README.md
index 4045c34..8046131 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
# DearPyGui-Markdown
-Almost all of the basic Markdown implementation, as well as additional support in the form of text customization (color, size) as a custom HTML tag.
+Almost all basic Markdown implementation, as well as additional support in the form of text customization (color, size) as a custom HTML tag.
diff --git a/example/font.py b/example/font.py
new file mode 100644
index 0000000..0b6a027
--- /dev/null
+++ b/example/font.py
@@ -0,0 +1,39 @@
+import ctypes
+
+import dearpygui.dearpygui as dpg
+
+import DearPyGui_Markdown as dpg_markdown
+
+font_size = 25
+default_path = './example/fonts/InterTight-Regular.ttf'
+bold_path = './example/fonts/InterTight-Bold.ttf'
+italic_path = './example/fonts/InterTight-Italic.ttf'
+italic_bold_path = './example/fonts/InterTight-BoldItalic.ttf'
+
+
+def add_font(file, size: int | float, parent=0, **kwargs) -> int:
+ if not isinstance(size, (int, float)):
+ raise ValueError(f'font size must be an integer or float. Not {type(size)}')
+
+ with dpg.font(file, size, parent=parent, **kwargs) as font:
+ dpg.add_font_range_hint(dpg.mvFontRangeHint_Default, parent=font)
+ dpg.add_font_range_hint(dpg.mvFontRangeHint_Cyrillic, parent=font)
+ return font
+
+
+def load() -> int:
+ '''
+ :return: default font
+ '''
+ ctypes.windll.shcore.SetProcessDpiAwareness(1)
+
+ dpg_markdown.set_font_registry(dpg.add_font_registry())
+ dpg_markdown.set_add_font_function(add_font)
+
+ return dpg_markdown.set_font(
+ font_size=font_size,
+ default=default_path,
+ bold=bold_path,
+ italic=italic_path,
+ italic_bold=italic_bold_path
+ )
diff --git a/example/fonts/InterTight-Bold.ttf b/example/fonts/InterTight-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..4a97e1f9ccc12c1c8a23016b1b8098999a67c1f5
GIT binary patch
literal 309904
zcmc${2V7Lg_5gh6-n#`0N;irqb*-qtvWiGino>l%(ou?tO0z3qLq(8ciCtrhEt;rF
z)YuYBj8R`=OkRrV>Pw7iYGQ$%{mz-Y4NElt@Av(_mzRi#*_qSk%xN>r5su?J!hb}L
z3l9kmvn_0YpA)=h10XIuc4Ykd|I{aQg6~Hh@w^!xpEz{>8LNq$n0uJx5>||i@8>n<
z*t@PAUzh^3WW^^0C)^Y-hH>K2DENEG*ooQ2m*0=y2fv5$V;u9d%ZhO|n7Yrx&-eAR_#|0LFKplsz$*x3xXP@n^1aoWiGga#{I+
zU+rGv_=_DmE-$;dG`F}wUb_%}e-*|nIG)R+*1}rFDNcaDaq(8pR_@ML&Uu25){8ID
zuBO%&`~Hs0Q~m%WJ8+zAr|b%hYs+!j&IJE)CC+xvB#Z9nd9sQp^mSU(j!qT2w)`l&
z^2k%H&|dsor;|RZSSI)2koOAZTvAQSjqnO1yoOkt!EYPj7Ar|xG7rbo4B1YFVZ2Jo
z#>0G-DV){Bo7gc{=$Ro*Y-*(UiRypEqve;U@I{_<;6PgvOi#9bxcf;ST{Em{
z@eLVGGC@Zrnhk4qf5xiB#Y64m>+NN4XDfdm2BaY@Cnqd4C+DfCXxpR(4NXlQ8kA;a
z#OFq&8o4$k!vI&>8R2V)B}q2I9gOhJ+zygpfGhSI;0m3rhnb+83~*%y_k{_rJgSGQ
zBsd7VjyuZ#g!7>y$gUVa*Gd75pHo<@m(_Bdaeb2lR&mG7))5W!Slp29FjIh916)aD
z3ryn`VMaLMBf)8=>^(Dl>V^NAt^18}d;_SF5g+AG2Dn87IY7=ax{A%|k}oDRP2&}_
z4C9rxoR-XywxnVQY{?Fs4ab!c*ty1kT#1d^*{%a$A+y(x!#{VI^lC!rsc|n3z2Qg*Sq^a*rAS5p#Ak+7%XX4=4vA`s@u3|0V$s5*%R#hA<%fW^CD)1Z76j|?Y^VaiK15yxPaw&D+a~cX0L;7(Z<7?Y+GMc#X5A
zz&D?x=iYjo3_dgOn>|Gx=zA9G#EHSBuVrTKEeR}27}SOYw9DRc-S!Ykx^s^t9~{2)
zgSkU;Lfp%~Sik9uNiIR@{`n_XqCH@1GL-@$FvGutiAsW_ftBFZ+_w^(st`vbd>L7#
zga2`%2sh57QZl$v`zw`j2o%uqz3+K1b
zJD5%guXrR7*HK~3JN}shNe5*Spo4bYOL*eLPGOrE_A~q_THD$3A_2YRi|K4KweDng
z_Q_g0pVWU(LiX*aM`<`cM=ZyFvSa&|vBdHqJ^0hl^uR$p^JG_~ZQLeKc185y`k`)+
zfwE<2>vWB9Si_>gHU_t-(0OvoXTx!$$RgNb&BnuF-BoW6TwBd^olJps1TKrFFrt)T
z&Rop3<=OOUO?oonnWWMb!@*8=KAY@kk*6mEwM8PMk_<3dHFGk+*BJ{|O<-2-9c4jf9&wTS;B2?3}2z0#~)(Wa*K{Gr!CStVhl4i0q}YBRRu{7KvfL$
zJtcSzS8oPKQzpTyIe)#}dYX*~lg0hT=3}4J+xHmeOSbYP%~0fTH8s%(#N#LMBqy2B
zHM2qBK*C9Gj=HRy0$oUkip4kFXW&A@Q9@HhWP!4^(`vrdb)$y2l$(|^S821Q*e1#V
z>d-5-g;Ukc6E>UF+$VhoEmRgdc5LVquBeeHn!%jW;9zh@bxlkH=!{C_HR`IFGnxhP
zCgzMv@HJcwb4C#!qeN#^f^X(rnKPQf=0Rta!4*1jyO|`L3~+Edlg!|_Gue39fw6iC
z0}q&?GQdZ$D1@zRra@KQK+`-*bT-*|a5i0-vnh>7XH%Mo$T-t@=66Z(I!)zUMheB$&e{c>a9{PD*(
zlP38R5->Ru_jAB19k=;4s%;>|jz3S*cTKK7o;&{NtjPH6(VC?GBxW1&Obv-hQ4x|A
zS$$$$)~SYL=V2R81tD#eT@>YFU^9Bm5|c^|M1u!BQ!j+6t%Oj^l|QV3ZJ!9AA0}=4
zU`Z#{xb4MZEjIiCaE9k-qwypnBB&6V2oW^l^1wkV2Y$e5jSi9uKfrhp06+UOmdF)C
z01s}MwH=;){5S8?8|#SZu6=2EZO*i{k%WwVXZz5)$^q)`tKQAHNdk9lqo3JsCqAF<
zqB^HB^TPIG`YFpq7beZpkgbb
zk}F4vqjFgLYK)kmN+53#Nev9$PJe!V=})`U=HFfqAx5dzj%?aouqAE8(yWB)cyYq!
z2ep~|Zr5!i+`9Ib~x{M$zt^h^ly45(VrBOIn!Q=`7((Z5E%YeT7+{
ze4X}!UKRY9|DVB-t|T$&SLxoQ>0$xQV8wNTIY4y4DY=JA&9GFfjh1N{@e3|#8f818
zyQNF}fDE=ohIUIxQB7o5Lgc^kq|Z=L9WX+;AfSfcWsONOp6)q=XXM}v5f|u%03Sa6
z#u4Iww&Kg(6FSg$J7{8ahm^jSo%Pz(2``TdqI3DlJLuQ8M@aH7cS!PE!xz0hSsg#9
zTgjK})_qwPU7OkF*JEes&6j~zwluRP*^Jh?WQ)OvlwAgt1gH-vVAEbj>n<|ujaloB3{D|j{fGy#*1JtEHNHZwETsr
zFgq$&y$CJT1LbVM+2T6Ua-vZPcmPPWRFdBfw8)#}d7L%p2edeA0t9b#v8cI;n#;vof2N4
zA@oe`{mrl5Uo`C=ttA$LbKjlNMDNjM-)$w_fpplUfwwhz6RP6MC-;aLF#DZ??9lAI@Y`b6{yE#|!_!sWn!9H%cx~dubjcpb5@0u*{5H0MvxpyGMf~VFZI$ebb`78Z
z$P@FFKm(XP!2x$G4kI#m#MBs|N1V`~Z%jQZOAs|js0r3C-YL;|kF=0p!f!k~it
zui?apwNL>icqRE*fB0pXVTO6ok0CZ-wv?=8tl;qQBWvk7Q`iM6aFhAbkcxs_?qc%D
z;63o(%+fHpa;|O{uAy56u0lTZdnI^{PR-!s0KS?X9TL2n{(!2a)5F0tY-hH?6^KTI
z(VwOCoQVw!q<~Nd`FP%gB?;M#3Y`$iR$2t`as_flTB(AzFgW=4e4cW*0dBF5-^(aT
z2Dg6)OnmvqPYesn${f3PPY4B~CzsAN}+z`&AzozF1
z`fJiWL|{n`-Fg_G%{G?=ucKeHc{%%@Tv`7GIy9-eyF7Z2a^-!*8skDf&p1k9
z40|^MJVSwwk2Gtot~;ZCJ)O#@nZ_$?bxzFjvDDobegWCrp%>%fp~fRgI;ZgjgiTCz
zi+)&4{5<0{exrI3&GfHrv))@}Wi>dVuUB@s*s=MK`m1H2-!PB(6Vq=eGdY4!|CA|(
z?~IzRb$D!clbj!}ddfmfS!H2>FsKU#Xx@-ahyN?AV5i&ioyZLB=X^i9h!(7a%`%y9
z)aJH)z>hmlN9*@X7?WHL>N3P?!wr&?g)!Zf;MLqb2~OQ`Dc~i8gMr_!-{G0&aM(x7
zAC~HE&<@#cmRx11`GS2{ctDcIh#~nodhg+k+##dA=@veZR@2D00nZ9v9vfz>`PR9+
z4_)5!4L|HOtwMb1rdI3JakCyymDUko0v5;&z7JZtO4*fWFDWF|-Bx5nG9}FCFWwiBzH2x|Hl)UPl^w5c_oDGkB4&nWus_YY7nc|
zF`)rN>e_0>>m-a}mga;O(LVoE~S0(hhVg#x;l!FDqJ*}!kqW^5sY
z`AXrv*N*OKc_eXM5{Uc_7~cU-LuaV)VCdiq#V%JX=Pa_2thFNRNHx9Kj?VdrR<)y-
zWmj4pgr6UI3Kv?22xnV-B;Az1!Gr@l7;g+8otZp1<7g32ZYI?o8(pD;4h`TO#S44DyLgmp8V;u8!OP5RaQI#?n$FHMQKI
zdX|PUmUiSEF{#Q5CB`tN71p9B={V`k!p=ipvLECaok2)PbFBUAc194T@|H|KEJAK=
zjx=m(biWUSL)U@pW}fbsa<+mYHwgY69uBkWSj!G#5iitIj8Hv1c(vLc
z3PG4Pup(A6D}4ZM=Z=vi%QLuxF3YUkW$k*sa&6(p`+LWbU=qGzUd${v;zcj3DnhF_
zh7x)*b>E{+^S|6k?$M9wNm3(}Yr}jp2WP!S{0rhi8DgIc+=fkYevi7=)x<=@XFIG5~Z`lV+U__q)R8^>WxBaN#ezNT?(
zlSuGt?njU)FaVj&j>=A44|5A@jB1|FccHMOc9tLp@y9O)1{$D{v1mJG9;mn
zJcrehK}u#wg1IzRub+dajd^YP1yL@AoL(s8_Ff!yZJJr#c@X0$rurlz8o9nK)yFu^
zQhgG94J648b*95C)hEF>a~D{uPm&C#`WPJQ&QWHPY%;(h)yG^)RAt;YMtCK;&s;=l
z9;_d;d6ZR#dKKcyTtsQz8?q0`EJ-8eUUF=yaKN}E*xOdFP9Lc*sjglzKZJ$vo5
zhie=DNgIU!e4$uf|ET0eR6*{=^!-$8eTNMT*a0;X+>FXro~~)XX9C-Q{rxk_>pOU
zn!!uOJPA&-T1l^-+-2@vD8iXnf%B-8431s}t^=oi5J+)07%-@e2J6dOnDJ6MI|=$i
zE2Kj$d4+R;z@K}c_`UfyJ$H8Qw@?`+0T#Xq6NAh4W@hav4V;iTuq}PBefIY2vYUtK
zp1b$x?n6&6jWQKEEITZBfS+tI;}@zT4$o5$(yvbJU8mjt56TgdEu%S)G%uhUusTzX
z%fJ%!oXt!jYc!BVfg+t_5FLxPBvEV=C4WK^QJz6`7OQ3Z$O=4vVA9QSG}~c~KHFem
zz;(ft0^~NqokB-OUyKoypV9aB?g}ddIvVA*BzY0|WM(StA1U9T@9iu2xxmI50Jp;OL%6@Jcd{9q5>Nfc!cHFm9kg
zQdfaghyy=K-xARp6B5K439t}%mXF;1n-Qt$ety%_uIejr6uJnSe|`c(;BFdVxDi$q
zur3=9T(mJ?CjfG_aM(JhksTLvw|ES#KzC7mclxFI^Dj;P
zlMKojG&tLzsbn>C8Vpv;*}=iaS*21K#HZgIPq+_Fq}`$g59ybClc5s*%u|e)zqXdX
z|H+(N|E!#-JTzU7oeFNOS;NW}v&eGhW311}rQ-*;)qJ{Y@r|8n89Q%OzkF5Gp4L}x85y@_R@t_c(c5KL9^M+-N92DM
z6r}seMFpgL!4fj%W8$!N3$g#Gd~x2F&HZOwTi*25EJCJTTe#zGoya3V#R}soA9$;lz
zsRMOZrGo5&KHumd6``aml!KFijW7J-L-eneBIz%Ws)~=D7Cg9Q)YfBPF4t|{aepaK
zKUOX*iJCAlXhP7!>}@lKE}_(J(wDD{$o2A!*La2Wx9QRM?Dj<;mXuytket&yYJg98
z-ww|9!wc)vmwYf0_{H#Ie7~XRyVRp5!LI^+3~sSXW=l{`5*#z!()emFoAHCel??{C
z#R{1>If3J8Po&!bhaQ&hunupS2STjDt(~%e#Tuc5$(JnocU&^_(N_K|hS5&u1cBn!
z>adSYVhq|zi7}OAio_TiZ$2IrY39Q`#u$Jzg~aiwU;qaNW1qud9FFJVpfG)KuNkz1
z=v&NOJX9K~z~ke|yF#f*Vi+Cw}bv3L%a5Lm1PlwwX%<*d(9;>iUaBJMV@9+Xq0dNgJ&$J68d
zly!gB3B*fWlsTd}cxYMVuo*7&X1GwVo#L)kZ5&(u$&6aa;ZREWN1aQ~S0zmtIwdA%
z`Y>NlFTt8Zvxz36;#&R7KSTSo?g0>bfnm`Z7>`&$N0Yt69*UhvDf9dA5asilTc91{nV`#TyyFfB?!OpB`Y_EVQKmmTF
zWbic3tg?#TG0=6Vfg-a+J1L_d^ED54K-FY9`QrGNQ6+wa1Wk*LtBMxM0C6S#`^`70
zwr1$`ZP^Qhi-O3&!kCf;qQ`;@r6nIO+J45fhfV(wFHO9sSMG>czL-4e{F3zg!r}JL
zmVG0921NDFK_N@R=h|bEI2$qm-caMmvsFzDil@r)4{RJ>GdY-QB)tn6+VgJLs3k=q
zCUs|9^A{OpCUKr%OfE~k0WgUn(2|u`MOdgRLn}
zW?M?}bo9m;H(245h?)%Jf+sOXH-IKG>RYcQ{2x}LiH6=W#hfRXGBO7bb4Dx#vi4@o
zX)?eekd=CN855Yl&c<7;6Q@Z|0Ud)V>&Jtwp4bQR94UedZkOWR2Qct=2=E{;p{qGhtV8tlA_g0Sh%Hyl=>sLqH9z24U;u
ze}MUgR9`TJ(i(5M9BwK+Mdifdqn6JR?+Q1eAoZl~`LC*8{qc{hp2=h#B1Iq%Zmxkq
z1T1cZn-gI^-kb;>5+Th4@iGvhXQB?{W&QlM+%INA#CVwzt%PC)iB$})CeN$I|Eghb37$;Z%Jb61i#>3jcKOX;!k{H}LM
zySR36KppK~eRyK!h4S(Xl@kwFcPC-ArClt&^UfuD=1~4I!s@GRBY$Hgr-9^1y2{|d
zEP+&;fu!j}#%!59d1soX
zTVQ4g*-!Gdlbw2Za_ZXEi8%Cf0)yvwgx-03VC<~WVRMqZ1ZUM>Ss|QxbZW@NF>Wr<
z`J{4n8&fm{)*b<8Ber!&Nm~!B*g+2#6yvR+bsmmIH9HXPQ0@8JU9@=x@h9`<~
zW#ie&J-o-!Pgl|w+i5@V625M6oxg;TCG+{Dq#!|jWMOgjeeLd@KeKBuhT9XwiRpQ(
z+Y?MbB{<$PK#yGpHEV@pvjnF(NV_@w;nb&)QUyI$$Sd$BWKfocvsJHb
zj7^|%WHIT}j`Stt=pb^1uAm=5*@&)&AHm(CB5BduYdet^D)K+^w_tuYj{@=R#aLCr
zz0L5E;E1OLuOx4TUl33nGHu1v`b92ws&)7B5Wl#(8&IlZu>C@~)AvBsgpfV4*pj1;CGET*Yl;7~4YL$69tB7zmedg4N!54U9lDKPwOe#{Bng+72Wc
z1m8Ju&&jmV;bVH!26|rbbaCOgX^Y8!3$g^)Ume^+wI{Tv_#gwu?BHOS!NIYN9UKw{
zrAEwZxf@LAF%XoTk%V5R#v&!4U=_>%q_r2zwVn7|M@3sAzSI0Gt0J?(jPV2YjF;L;
z^^?Hl3=Sd7C)jV#N#nu4lHkUWMGt>WP{g>t(A6lYmfj{@Oj3Hd`zdI3&CbAxaE*
zsX&|=ypo9{2I2~HcpaAyafQHD>2Axv2WE(1L?BcQ1_?OZXz&^m+HUxp-Y0c`3QV>i
zDo(d$*;&VHi(gG6{ZtX2qsMz{3v#ngHq6`}2Y_$@^dp>zJ);l!mw#K5bb6cEv-!W_
zDLsZ)#_(%?Ta_qcGl0^%e1Sd4xtv;ig|?0tKcKGZ|?YcCOAYjpsYS{QV6(rkyaZk
z*0rU^Fc_1pR(MokqpBR*wa8g|cHe#?+n3ZixFjW`Da+RDz?`>;!$z`%KBU#MD-UWF
z9_p&H!}yuH5Lbj&x#={)gi0smtXko1yl7ds6-YR8T0CK756&ZIVwm&nT2|g
z30NVW$S@vyq~HWEVk%e79cMeIkY>awFNE$PEIUBTEW>F3>;y7%;+7mZe_TC
z-~e2Kb6ewW@2m0fFeLU>YL-V370~{yg4_b88Cgkmf&FW=u0hA`wBJQ(V;$j7+m|k0
zTxwskkdTG7j?t5c3=UXyZc+o)?RMO)t0P74FA5wqc#^Q8@Y0I4UzC-8zGmg+3E$5u
zEuIc}!}3!2Z>R8(z);J3B=xm9i}#F#R^i=CDqb5y?pcNeg@rkQ#>7ya_!Vdj*gr@y
zz*hFMS3nyZG6$}i#=w@gCX>!yB~(i~1qEm=`QI@XgYA9kKy`HgI4{^$AS34Ouxtup
zJcq-b$tVjl1m@l9>^=gFmooTAK}ML_$E%i53S%DpH_U^Vem_S4dxga&$nftle2%Sy
zpF_|7K3k{te0T-(b*2lF4FjiDZ*}XqEwBzW>*mW9yoXj3C5>lkpwDaqc6@yX7Xy+#d9O(XP5#g~is+@3Y_*S&e9Gd;I%EJ4>kr>`b2{DHb@lOw!hrpU((ZEE5o03fg
zZwnsIcX1%y=JPYJ(B{x>bQ7=mS*#^tP@t%7E{Cxg`wbOi7&i_2A#u|VIVyaoy}5&T
z)Vi5&t;mgPVZ28I^kP5T+jyLCizRAF1qa;=y59k(PK??v=ypT>###7>j6U(F2TAl%
ze#GwIE2{49q0>lW>U1b2rCsGYx(ZyaQr_*?7BGfSN$vx0|A5t`&dl}_Z^4g7e;O!}
zt$mc#sg+|RuG_MOXj=I+Yr*GGORZpjATzF!hb9uR9r-|AqwF-^1Pg{9UG(9kj1Ck@
z+%yiBf*rPEK#mEl3BO`V{?1%N=Kj24)&~tEi!YLwc6ZO;H#dTK=Y$4ls0kU6%>VIK
zuN7yftof=eZtZ^;1iVQ+zFK7W&^o^EljY;b%nc#LD{I(@vLukCBLtD)S~8iw#MTFL
z1c4}EAG4szl*1Dskn!;V_lQ~8Z^;Uhpv{kdKsNgB)ZQkl(i6qS$IGT3n_P0dEFdW<
zASh84!p|0x9^EHHgo`cP8iijUb)Iyzxa6JUlA|TXMGV-9kMFEF}bZyh#P^Q2;&$wMRYFD+m
z*=5H2YqmbBC#2@yhV|Df+tbfF6pW3S0ExW8WwD`&YU>+(Bu(iN4}ck8EZes1zi>Vz
z?w%Jt%)c;nd&q=wcBZ7T*EG=nEvdLT!Xy6=gY6I~xfTHC9XjY(xd_
zrbx+DhRgQ5`_{V->2B>|J9N0hb;dcsYuS?4SoxRgak^Z;^DROwlppZ2Au;r~=VK^v
zwNS#iBl>}{V9!FJxnPD4{^ajy*OCkLGP$f>EBf8L*L+U&YkB9dzkn`(xQLmq{}h)C
zW1&)NC8TS&PNtXc-n}b?<5=K(Dwx9zF~hw1%^RBE#0>f!O>9p;)sb`U#f;`He1dkb
zn4zuX%OJ)~0Y@+wge9TW0gH%+B?wwG{1-LTz&CHWdW%{KSHks-1BfLr&($uNNH2p2
z`!zpXyMWH${oCx>w|52>ROO6aRG=A^-Me>Af;w-}*vJX-F-D|m=DmC5l{Ua7
zV87vEBOz_u+_7i_@~w@tPppZvu(&H%%7F*4mBq!32WV#5k*6;f!>bZdwgxZ76=E;m
z5xS3j=?B{3f5(JyOz;kXT>$;Yu?KBHO=KYeoG
zFzy28O&H3>LL9w7)I>u$YEAl|NKPaq9^!Z6-E_6A{4@R~-kAyQNV2DA{KsQ9j4Rxc
z2`2Mzz&sf+M>CFQ49{w5!^0W`t}Bw^G;zy$($jq_jpxt6edYi1W3?a;?aMF2e_o~C
z$oGJw2NVJ=#Ueb;KIWawy
z7S`^rve2mE10C{Kj_Q-zFji~1UOlq!XPR`sX%-#ZS%^y4!F>XU!g0ZnlXnJprGU9N
zbGe_H8zaHn=;3l@%@i!NNYLp9sKv@jmI#Fi#!_w=>w#mLqtuJVF^_-dW$81>6h#N)
zOs`D9T8cClGmwal++uB^!U**=K$X=lc)wX13wNeDPc9IoqcS{F!mRNNbD00g*sZU9
z_unvE{K1gRZnpS8@m(g97wBnHz#Z0Ox?e!kBwiY*iu}LRW!}y9f2MH>w6=vpi`)MT
zTD2QY{cgJHD0TTbBh`(%A{}OdP?po>aHUf(_cg^o8s)y#2;FS)oe?_FFbCB8mzrrT
zbbU*6C@cJp8jDqZ2K7KFbNZ#)J>#}mWS9$_do!I?1R3BK>%~84wQ+63ZjmdioQ!M8
zp%Tv4P(V6E$(i)FlL4+I;!P8#EaGQcuB_|IW`XOgDl=T_`6$z9`F!In8_7j(lM$D<
zjPRx0e@){RS7$dwBFZb2?fHu6JVw&^(7g|GB|*gle+1ypk&hmHGt7H3o=9^)#vu=ZX2M%S{>X!Ul{UI=h
z9Rn(G6PewIbJ|&-%rG3xh^%|7g8Z4D(1vJ
zbie?1&@7P-8os+;T>;KEg9NW53GjOq=Wilhh2iYc8O2jm<;nsfkR50tzR4kn@|g7O
z4$2Z-8ve5}6(j8}=|@CuVHZ=8KI_u#>6dvW@l2&5qJZNWA`LaD5xB&jJa|#=
z3dxy54U^lWZ>7P!3fztKvfsPHyCFYf=9IZH9wB|m8x4*&g!GOJ2uuue={i`- _Le4lyMm~>5d({eLiPK!2Fu
h!?p6~3?L7bMUke9rzEj
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