teddecor.TED.markup

TED includes a parser to get literal strings from TED markup, along with a pprint function that outputs the literal string from a TED markup.

Raises: MacroMissingError: If there is an incorrect macro or color specifier MacroError: If there is a general formatting error with a macro

  1"""TED includes a parser to get literal strings from TED markup, along with a pprint
  2function that outputs the literal string from a TED markup.
  3
  4Raises:
  5    MacroMissingError: If there is an incorrect macro or color specifier
  6    MacroError: If there is a general formatting error with a macro
  7"""
  8from __future__ import annotations
  9
 10from typing import Iterator, Callable
 11from .tokens import Token, Color, Text, Bold, Underline, Formatter, HLink, Reset, Func
 12from .formatting import BOLD, UNDERLINE, RESET, LINK, FUNC
 13
 14__all__ = [
 15    "TED",
 16]
 17
 18
 19class TEDParser:
 20    """Main class exposed by the library to give access the markup utility functions."""
 21
 22    def __init__(self) -> None:
 23        self._funcs = FUNC
 24
 25    def __split_macros(self, text: str) -> Iterator[str]:
 26        """Takes a macro, surrounded by brackets `[]` and splits the nested/chained macros.
 27
 28        Args:
 29            text (str): The contents of the macro inside of brackets `[]`
 30
 31        Yields:
 32            Iterator[str]: Iterates from each token to the next until entire macro is consumed
 33        """
 34        schars = ["@", "~", "!"]
 35        last, index = 0, 0
 36        while index < len(text):
 37            if index != 0:
 38                if text[index] in schars:
 39                    yield text[last:index]
 40                    last = index
 41
 42            index += 1
 43
 44        if last != index:
 45            yield text[last:]
 46
 47    def __parse_macro(self, text: str) -> list[Token]:
 48        """Takes the chained, nested, or single macros and generates a token based on it's type.
 49
 50        Args:
 51            text (str): The macro content inside of brackets `[]`
 52
 53        Returns:
 54            list[Token]: The list of tokens created from the macro content inside of brackets `[]`
 55        """
 56        tokens = []
 57
 58        if len(text) == 0:
 59            tokens.append(Reset())
 60            return tokens
 61
 62        for sub_macro in self.__split_macros(text):
 63            sub_macro = sub_macro.strip()
 64            if sub_macro.startswith("@"):
 65                tokens.append(Color(sub_macro))
 66            elif sub_macro.startswith("~"):
 67                tokens.append(HLink(sub_macro))
 68            elif sub_macro.startswith("^"):
 69                tokens.append(Func(sub_macro, self._funcs))
 70        return tokens
 71
 72    def __optimize(self, tokens: list) -> list:
 73        """Takes the generated tokens from the markup string and removes and combines tokens where possible.
 74
 75        Example:
 76            Since there can be combinations such as fg, bg, bold, and underline they can be represented in two ways.
 77            * Unoptimized - `\\x1b[1m\\x1b[4m\\x1b[31m\\x1b[41m`
 78            * Optimized - `\\x1b[1;4;31;41m`
 79
 80
 81            Also, if many fg, bg, bold, and underline tokens are repeated they will be optimized.
 82            * `*Bold* *Still bold` translates to `\\x1b[1mBold still bold\\x1b[0m`
 83                * You can see that it removes unnecessary tokens as the affect is null.
 84            * `[@> red @> green]Green text` translates to `\\x1b[32mGreen text\\x1b[0m`
 85                * Here is an instance of overriding the colors. Order matters here, but since you are applying the foreground repeatedly only the last one will show up. So all previous declerations are removed.
 86
 87        Args:
 88            tokens (list): The list of tokens generated from parsing the TED markup
 89
 90        Returns:
 91            list: The optimized list of tokens. Bold, underline, fg, and bg tokens are combined into Formatter tokens
 92        """
 93        open_link = False
 94        func = None
 95        formatter = Formatter()
 96        output = []
 97        for token in tokens:
 98            if isinstance(token, Color):
 99                formatter.color = token
100            elif isinstance(token, Bold):
101                formatter.bold = token
102            elif isinstance(token, Underline):
103                formatter.underline = token
104            elif isinstance(token, HLink):
105                if token.closing and open_link:
106                    open_link = False
107                    output.append(token)
108                elif not token.closing and open_link:
109                    token.value = LINK.CLOSE + token.value
110                    output.append(token)
111                else:
112                    open_link = True
113                    output.append(token)
114            elif isinstance(token, Func):
115                func = token
116            else:
117                if not formatter.is_empty():
118                    output.append(formatter)
119                    formatter = Formatter()
120                if func is not None:
121                    new_value = func.exec(token.value)
122                    if isinstance(new_value, str):
123                        token.value = new_value
124                    func = None
125                output.append(token)
126
127        if not formatter.is_empty():
128            output.append(formatter)
129        if open_link:
130            output.append(HLink("~"))
131
132        return output
133
134    def __parse_tokens(self, string: str):
135        """Splits the TED markup string into tokens. If `*` or `_` are found then a Bold or Underline token will be generated respectively.
136        If `[` is found then it marches to the end of the macro, `]`, and then parses it. All special characters can be escaped with `\\`
137
138        Args:
139            text (str): The TED markup string that will be parsed
140
141        Raises:
142            MacroError: If a macro is not closed
143
144        Returns:
145            str: The translated ansi representation of the given sting
146        """
147
148        bold_state = BOLD.POP
149        """BOLD: The current state/value of being bold. Either is bold, or is not bold."""
150
151        underline_state = UNDERLINE.POP
152        """UNDERLINE: The current state/value of being underlined. Either is underlined, or is not underlined."""
153
154        text: list = []
155        """The chunks of text between special tokens."""
156
157        output: list = []
158        """Final output of the parse."""
159
160        escaped: bool = False
161        """Indicates whether to escape the next character or not."""
162
163        index: int = 0
164        """Current index of walking through the markup string."""
165
166        def consume_macro(index: int):
167            """Starts from start of the macro and grabs characters until at the end of the macro.
168
169            Args:
170                index (int): The current index in the string
171
172            Raises:
173                MacroError: If at the end of the markup string and the macro isn't closed
174
175            Returns:
176                int: Index after moving to the end of the macro
177            """
178            start = index
179            index += 1
180            char = string[index]
181            macro = []
182            while char != "]":
183                macro.append(char)
184                index += 1
185                if index == len(string):
186                    raise ValueError(f"Macro's must be closed \n {string[start-1:]}")
187                char = string[index]
188            output.extend(self.__parse_macro("".join(macro)))
189
190            return index
191
192        while index < len(string):
193            char = string[index]
194            if char == "*" and not escaped:
195                if len(text) > 0:
196                    output.append(Text("".join(text)))
197                    text = []
198                bold_state = BOLD.inverse(bold_state)
199                output.append(Bold(bold_state))
200            elif char == "_" and not escaped:
201                if len(text) > 0:
202                    output.append(Text("".join(text)))
203                    text = []
204                underline_state = UNDERLINE.inverse(underline_state)
205                output.append(Underline(underline_state))
206            elif char == "[" and not escaped:
207                if len(text) > 0:
208                    output.append(Text("".join(text)))
209                    text = []
210                index = consume_macro(index)
211            elif char == "\\" and not escaped:
212                escaped = True
213            else:
214                text.append(char)
215                escaped = False
216
217            index += 1
218
219        if len(text) > 0:
220            output.append(Text("".join(text)))
221            text = []
222
223        return "".join(str(token) for token in self.__optimize(output)) + RESET
224
225    def define(self, name: str, callback: Callable) -> None:
226        """Adds a callable function to the functions macro. This allows it to be called from withing a macro.
227        Functions must return a string, if it doesn't it will ignore the the return. It will automaticaly grab the next text block and use it for the input of the function.
228        The function should manipulate the text and return the result.
229
230        Args:
231            name (str): The name associated with the function. Used in the macro
232            callback (Callable): The function to call when the macro is executed
233        """
234        self._funcs.update({name: callback})
235
236    def parse(self, text: str) -> str:
237        """Parses a TED markup string and returns the translated ansi equivilent.
238
239        Args:
240            text (str): The TED markup string
241
242        Returns:
243            str: The ansi translated string
244        """
245        return self.__parse_tokens(text)
246
247    def print(self, *args) -> None:
248        """Works similare to the buildin print function.
249        Takes all arguments and passes them through the parser.
250        When finished it will print the results to the screen with a space inbetween the args.
251
252        Args:
253            *args (Any): Any argument that is a string or has a __str__ implementation
254        """
255        parsed = []
256        for arg in args:
257            parsed.append(self.parse(str(arg)))
258
259        print(*parsed)
260
261    @staticmethod
262    def encode(text: str) -> str:
263        """Utility to automatically escape/encode special markup characters.
264
265        Args:
266            text (str): The string to encode/escape
267
268        Returns:
269            str: The escaped/encoded version of the given string
270        """
271        schars = ["*", "_", "["]
272        for char in schars:
273            text = f"\{char}".join(text.split(char))
274        return text
275
276    @staticmethod
277    def strip(text: str) -> str:
278        """Removes TED specific markup.
279
280        Args:
281            text (str): String to strip markup from.
282
283        Returns:
284            str: Version of text free from markup.S
285        """
286        from re import sub
287
288        return sub(
289            r"\x1b\[(\d{0,2};?)*m|(?<!\\)\*|(?<!\\)_|(?<!\\)\[[^\[\]]+\]|\\",
290            "",
291            text,
292        )
293
294
295TED = TEDParser()
TED = <teddecor.TED.markup.TEDParser object>