pytermgui.markup.language

Wrappers around the TIM parsing engine, implementing caching and context management.

  1"""Wrappers around the TIM parsing engine, implementing caching and context management."""
  2
  3from __future__ import annotations
  4
  5import os
  6from dataclasses import dataclass
  7from typing import Callable, Generator, Iterator, Match
  8
  9from ..colors import ColorSyntaxError, str_to_color
 10from ..regex import RE_MARKUP
 11from ..terminal import get_terminal
 12from .aliases import apply_default_aliases
 13from .macros import apply_default_macros
 14from .parsing import (
 15    PARSERS,
 16    ContextDict,
 17    MacroType,
 18    create_context_dict,
 19    eval_alias,
 20    parse_tokens,
 21    tokenize_ansi,
 22    tokenize_markup,
 23    tokens_to_markup,
 24)
 25from .style_maps import CLEARERS
 26from .tokens import Token
 27
 28STRICT_MARKUP = bool(os.getenv("PTG_STRICT_MARKUP"))
 29
 30__all__ = [
 31    "escape",
 32    "MarkupLanguage",
 33    "StyledText",
 34    "tim",
 35]
 36
 37Tokenizer = Callable[[str], Iterator[Token]]
 38
 39
 40def escape(text: str) -> str:
 41    """Escapes any markup found within the given text."""
 42
 43    def _repl(matchobj: Match) -> str:
 44        full, *_ = matchobj.groups()
 45
 46        return f"\\{full}"
 47
 48    return RE_MARKUP.sub(_repl, text)
 49
 50
 51class MarkupLanguage:
 52    """A relatively simple object that binds context to TIM parsing functions.
 53
 54    Most of the job this class has is to pass along a `ContextDict` to various
 55    "lower level" functions, in order to maintain a sort of state. It also exposes
 56    ways to modify this state, namely the `alias` and `define` methods.
 57    """
 58
 59    def __init__(
 60        self,
 61        *,
 62        strict: bool = False,
 63        default_aliases: bool = True,
 64        default_macros: bool = True,
 65    ) -> None:
 66        self._cache: dict[tuple[str, bool, bool], tuple[list[Token], str, bool]] = {}
 67
 68        self.context = create_context_dict()
 69        self._aliases = self.context["aliases"]
 70        self._macros = self.context["macros"]
 71
 72        if default_aliases:
 73            apply_default_aliases(self)
 74
 75        if default_macros:
 76            apply_default_macros(self)
 77
 78        self.strict = strict or STRICT_MARKUP
 79
 80    @property
 81    def aliases(self) -> dict[str, str]:
 82        """Returns a copy of the aliases defined in context."""
 83
 84        return self._aliases.copy()
 85
 86    @property
 87    def macros(self) -> dict[str, MacroType]:
 88        """Returns a copy of the macros defined in context."""
 89
 90        return self._macros.copy()
 91
 92    def define(self, name: str, method: MacroType) -> None:
 93        """Defines a markup macro.
 94
 95        Macros are essentially function bindings callable within markup. They can be
 96        very useful to represent changing data and simplify TIM code.
 97
 98        Args:
 99            name: The name that will be used within TIM to call the macro. Must start with
100                a bang (`!`).
101            method: The function bound to the name given above. This function will take
102                any number of strings as arguments, and return a terminal-ready (i.e. parsed)
103                string.
104        """
105
106        if not name.startswith("!"):
107            raise ValueError("TIM macro names must be prefixed by `!`.")
108
109        self._macros[name] = method
110
111    def alias(self, name: str, value: str, *, generate_unsetter: bool = True) -> None:
112        """Creates an alias from one custom name to a set of styles.
113
114        These can be used to store and reference a set of tags using only one name.
115
116        Aliases may reference other aliases, but only do this consciously, as it can become
117        a hard to follow trail of sorrow very quickly!
118
119        Args:
120            name: The name this alias will be referenced by.
121            value: The markup value that the alias will represent.
122            generate_unsetter: Disable generating clearer aliases.
123
124                For example:
125                    ```
126                    my-tag = 141 bold italic
127                    ```
128
129                will generate:
130                    ```
131                    /my-tag = /fg /bold /italic
132                    ```
133        """
134
135        value = eval_alias(value, self.context)
136
137        def _generate_unsetter() -> str:
138            unsetter = ""
139            for tag in value.split():
140                if "(" in tag and ")" in tag:
141                    tag = tag[: tag.find("(")]
142
143                if tag in self._aliases or tag in self._macros:
144                    unsetter += f" /{tag}"
145                    continue
146
147                try:
148                    color = str_to_color(tag)
149                    unsetter += f" /{'bg' if color.background else 'fg'}"
150
151                except ColorSyntaxError:
152                    unsetter += f" /{tag}"
153
154            return unsetter.lstrip(" ")
155
156        self._aliases[name] = value
157
158        if generate_unsetter:
159            self._aliases[f"/{name}"] = _generate_unsetter()
160
161    def alias_multiple(self, *, generate_unsetter: bool = True, **items: str) -> None:
162        """Runs `MarkupLanguage.alias` repeatedly for all arguments.
163
164        The same `generate_unsetter` value will be used for all calls.
165
166        You can use this in two forms:
167
168        - Traditional keyword arguments:
169
170            ```python
171            lang.alias_multiple(my-tag1="bold", my-tag2="italic")
172            ```
173
174        - Keyword argument unpacking:
175
176            ```python
177            my_aliases = {"my-tag1": "bold", "my-tag2": "italic"}
178            lang.alias_multiple(**my_aliases)
179            ```
180        """
181
182        for name, value in items.items():
183            self.alias(name, value, generate_unsetter=generate_unsetter)
184
185    def parse(
186        self,
187        text: str,
188        optimize: bool = False,
189        append_reset: bool = True,
190    ) -> str:
191        """Parses some markup text.
192
193        This is a thin wrapper around `pytermgui.markup.parsing.parse`. The main additions
194        of this wrapper are a caching system, as well as state management.
195
196        Ignoring caching, all calls to this function would be equivalent to:
197
198            def parse(self, *args, **kwargs) -> str:
199                kwargs["context"] = self.context
200
201                return parse(*args, **kwargs)
202        """
203
204        key = (text, optimize, append_reset)
205
206        if key in self._cache:
207            tokens, output, has_macro = self._cache[key]
208
209            if has_macro:
210                output = parse_tokens(
211                    tokens,
212                    optimize=optimize,
213                    append_reset=append_reset,
214                    context=self.context,
215                    ignore_unknown_tags=not self.strict,
216                )
217
218                self._cache[key] = (tokens, output, has_macro)
219
220            return output
221
222        tokens = list(tokenize_markup(text))
223
224        output = parse_tokens(
225            tokens,
226            optimize=optimize,
227            append_reset=append_reset,
228            context=self.context,
229            ignore_unknown_tags=not self.strict,
230        )
231
232        has_macro = any(token.is_macro() for token in tokens)
233
234        self._cache[key] = (tokens, output, has_macro)
235
236        return output
237
238    # TODO: This should be deprecated.
239    @staticmethod
240    def get_markup(text: str) -> str:
241        """DEPRECATED: Convert ANSI text into markup.
242
243        This function does not use context, and thus is out of place here.
244        """
245
246        return tokens_to_markup(list(tokenize_ansi(text)))
247
248    def group_styles(
249        self, text: str, tokenizer: Tokenizer = tokenize_ansi
250    ) -> Generator[StyledText, None, None]:
251        """Generate StyledText-s from some text, using our context.
252
253        See `StyledText.group_styles` for arguments.
254        """
255
256        yield from StyledText.group_styles(
257            text, tokenizer=tokenizer, context=self.context
258        )
259
260    def print(self, *args, **kwargs) -> None:
261        """Parse all arguments and pass them through to print, along with kwargs."""
262
263        parsed = []
264        for arg in args:
265            parsed.append(self.parse(str(arg)))
266
267        get_terminal().print(*parsed, **kwargs)
268
269
270tim = MarkupLanguage()
271
272
273@dataclass(frozen=True)
274class StyledText:
275    """An ANSI style-infused string.
276
277    This is a sort of helper to handle ANSI texts in a more semantic manner. It
278    keeps track of a sequence and a plain part.
279
280    Calling `len()` will return the length of the printable, non-ANSI part, and
281    indexing will return the characters at the given slice, but also include the
282    sequences that are applied to them.
283
284    To generate StyledText-s, it is recommended to use the `StyledText.yield_from_ansi`
285    classmethod.
286    """
287
288    __slots__ = ("plain", "sequences", "tokens", "link")
289
290    sequences: str
291    plain: str
292    tokens: list[Token]
293    link: str | None
294
295    # TODO: These attributes could be added in the future, though doing so would cement
296    #       StyledText-s only ever being created by `group_styles`.
297    #
298    #       Maybe we could add a `styled_text.as_bold()`, `as_color()` type API? We would
299    #       still need to somehow default the attributes somehow, which could then be done
300    #       with a helper function?
301    #
302    # foreground: Color | None
303    # background: Color | None
304    # bold: bool
305    # dim: bool
306    # italic: bool
307    # underline: bool
308    # strikethrough: bool
309    # inverse: bool
310
311    @staticmethod
312    def group_styles(
313        text: str,
314        tokenizer: Tokenizer = tokenize_ansi,
315        context: ContextDict | None = None,
316    ) -> Generator[StyledText, None, None]:
317        """Yields StyledTexts from an ANSI coded string.
318
319        A new StyledText will be created each time a non-plain token follows a
320        plain token, thus all texts will represent a single (ANSI)PLAIN group
321        of characters.
322        """
323
324        context = context if context is not None else create_context_dict()
325
326        parsers = PARSERS
327        link = None
328
329        def _parse(token: Token) -> str:
330            nonlocal link
331
332            if token.is_macro():
333                return token.markup
334
335            if token.is_hyperlink():
336                link = token
337                return ""
338
339            if link is not None and Token.is_clear(token) and token.targets(link):
340                link = None
341
342            if token.is_clear() and token.value not in CLEARERS:
343                return token.markup
344
345            # The full text (last arg) is not relevant here, as ANSI parsing doesn't
346            # use any context-defined tags, so no errors will occur.
347            return parsers[type(token)](token, context, lambda: "")  # type: ignore
348
349        tokens: list[Token] = []
350        token: Token
351
352        for token in tokenizer(text):
353            if token.is_plain():
354                yield StyledText(
355                    "".join(_parse(tkn) for tkn in tokens),
356                    token.value,
357                    tokens + [token],
358                    link.value if link is not None else None,
359                )
360
361                tokens = [tkn for tkn in tokens if not tkn.is_cursor()]
362                continue
363
364            if Token.is_clear(token):
365                tokens = [tkn for tkn in tokens if not token.targets(tkn)]
366
367                if len(tokens) > 0 and tokens[-1] == token:
368                    continue
369
370            if len(tokens) > 0 and all(tkn.is_clear() for tkn in tokens):
371                tokens = []
372
373            tokens.append(token)
374
375        # if len(tokens) > 0:
376        #     token = PlainToken("")
377
378        #     yield StyledText(
379        #         "".join(_parse(tkn) for tkn in tokens),
380        #         token.value,
381        #         tokens + [token],
382        #         link.value if link is not None else None,
383        #     )
384
385    @classmethod
386    def first_of(cls, text: str) -> StyledText | None:
387        """Returns the first element of cls.yield_from_ansi(text)."""
388
389        for item in cls.group_styles(text):
390            return item
391
392        return None
393
394    def __len__(self) -> int:
395        return len(self.plain)
396
397    def __str__(self) -> str:
398        return self.sequences + self.plain
399
400    def __getitem__(self, sli: int | slice) -> str:
401        return self.sequences + self.plain[sli]
def escape(text: str) -> str:
41def escape(text: str) -> str:
42    """Escapes any markup found within the given text."""
43
44    def _repl(matchobj: Match) -> str:
45        full, *_ = matchobj.groups()
46
47        return f"\\{full}"
48
49    return RE_MARKUP.sub(_repl, text)

Escapes any markup found within the given text.

class MarkupLanguage:
 52class MarkupLanguage:
 53    """A relatively simple object that binds context to TIM parsing functions.
 54
 55    Most of the job this class has is to pass along a `ContextDict` to various
 56    "lower level" functions, in order to maintain a sort of state. It also exposes
 57    ways to modify this state, namely the `alias` and `define` methods.
 58    """
 59
 60    def __init__(
 61        self,
 62        *,
 63        strict: bool = False,
 64        default_aliases: bool = True,
 65        default_macros: bool = True,
 66    ) -> None:
 67        self._cache: dict[tuple[str, bool, bool], tuple[list[Token], str, bool]] = {}
 68
 69        self.context = create_context_dict()
 70        self._aliases = self.context["aliases"]
 71        self._macros = self.context["macros"]
 72
 73        if default_aliases:
 74            apply_default_aliases(self)
 75
 76        if default_macros:
 77            apply_default_macros(self)
 78
 79        self.strict = strict or STRICT_MARKUP
 80
 81    @property
 82    def aliases(self) -> dict[str, str]:
 83        """Returns a copy of the aliases defined in context."""
 84
 85        return self._aliases.copy()
 86
 87    @property
 88    def macros(self) -> dict[str, MacroType]:
 89        """Returns a copy of the macros defined in context."""
 90
 91        return self._macros.copy()
 92
 93    def define(self, name: str, method: MacroType) -> None:
 94        """Defines a markup macro.
 95
 96        Macros are essentially function bindings callable within markup. They can be
 97        very useful to represent changing data and simplify TIM code.
 98
 99        Args:
100            name: The name that will be used within TIM to call the macro. Must start with
101                a bang (`!`).
102            method: The function bound to the name given above. This function will take
103                any number of strings as arguments, and return a terminal-ready (i.e. parsed)
104                string.
105        """
106
107        if not name.startswith("!"):
108            raise ValueError("TIM macro names must be prefixed by `!`.")
109
110        self._macros[name] = method
111
112    def alias(self, name: str, value: str, *, generate_unsetter: bool = True) -> None:
113        """Creates an alias from one custom name to a set of styles.
114
115        These can be used to store and reference a set of tags using only one name.
116
117        Aliases may reference other aliases, but only do this consciously, as it can become
118        a hard to follow trail of sorrow very quickly!
119
120        Args:
121            name: The name this alias will be referenced by.
122            value: The markup value that the alias will represent.
123            generate_unsetter: Disable generating clearer aliases.
124
125                For example:
126                    ```
127                    my-tag = 141 bold italic
128                    ```
129
130                will generate:
131                    ```
132                    /my-tag = /fg /bold /italic
133                    ```
134        """
135
136        value = eval_alias(value, self.context)
137
138        def _generate_unsetter() -> str:
139            unsetter = ""
140            for tag in value.split():
141                if "(" in tag and ")" in tag:
142                    tag = tag[: tag.find("(")]
143
144                if tag in self._aliases or tag in self._macros:
145                    unsetter += f" /{tag}"
146                    continue
147
148                try:
149                    color = str_to_color(tag)
150                    unsetter += f" /{'bg' if color.background else 'fg'}"
151
152                except ColorSyntaxError:
153                    unsetter += f" /{tag}"
154
155            return unsetter.lstrip(" ")
156
157        self._aliases[name] = value
158
159        if generate_unsetter:
160            self._aliases[f"/{name}"] = _generate_unsetter()
161
162    def alias_multiple(self, *, generate_unsetter: bool = True, **items: str) -> None:
163        """Runs `MarkupLanguage.alias` repeatedly for all arguments.
164
165        The same `generate_unsetter` value will be used for all calls.
166
167        You can use this in two forms:
168
169        - Traditional keyword arguments:
170
171            ```python
172            lang.alias_multiple(my-tag1="bold", my-tag2="italic")
173            ```
174
175        - Keyword argument unpacking:
176
177            ```python
178            my_aliases = {"my-tag1": "bold", "my-tag2": "italic"}
179            lang.alias_multiple(**my_aliases)
180            ```
181        """
182
183        for name, value in items.items():
184            self.alias(name, value, generate_unsetter=generate_unsetter)
185
186    def parse(
187        self,
188        text: str,
189        optimize: bool = False,
190        append_reset: bool = True,
191    ) -> str:
192        """Parses some markup text.
193
194        This is a thin wrapper around `pytermgui.markup.parsing.parse`. The main additions
195        of this wrapper are a caching system, as well as state management.
196
197        Ignoring caching, all calls to this function would be equivalent to:
198
199            def parse(self, *args, **kwargs) -> str:
200                kwargs["context"] = self.context
201
202                return parse(*args, **kwargs)
203        """
204
205        key = (text, optimize, append_reset)
206
207        if key in self._cache:
208            tokens, output, has_macro = self._cache[key]
209
210            if has_macro:
211                output = parse_tokens(
212                    tokens,
213                    optimize=optimize,
214                    append_reset=append_reset,
215                    context=self.context,
216                    ignore_unknown_tags=not self.strict,
217                )
218
219                self._cache[key] = (tokens, output, has_macro)
220
221            return output
222
223        tokens = list(tokenize_markup(text))
224
225        output = parse_tokens(
226            tokens,
227            optimize=optimize,
228            append_reset=append_reset,
229            context=self.context,
230            ignore_unknown_tags=not self.strict,
231        )
232
233        has_macro = any(token.is_macro() for token in tokens)
234
235        self._cache[key] = (tokens, output, has_macro)
236
237        return output
238
239    # TODO: This should be deprecated.
240    @staticmethod
241    def get_markup(text: str) -> str:
242        """DEPRECATED: Convert ANSI text into markup.
243
244        This function does not use context, and thus is out of place here.
245        """
246
247        return tokens_to_markup(list(tokenize_ansi(text)))
248
249    def group_styles(
250        self, text: str, tokenizer: Tokenizer = tokenize_ansi
251    ) -> Generator[StyledText, None, None]:
252        """Generate StyledText-s from some text, using our context.
253
254        See `StyledText.group_styles` for arguments.
255        """
256
257        yield from StyledText.group_styles(
258            text, tokenizer=tokenizer, context=self.context
259        )
260
261    def print(self, *args, **kwargs) -> None:
262        """Parse all arguments and pass them through to print, along with kwargs."""
263
264        parsed = []
265        for arg in args:
266            parsed.append(self.parse(str(arg)))
267
268        get_terminal().print(*parsed, **kwargs)

A relatively simple object that binds context to TIM parsing functions.

Most of the job this class has is to pass along a ContextDict to various "lower level" functions, in order to maintain a sort of state. It also exposes ways to modify this state, namely the alias and define methods.

MarkupLanguage( *, strict: bool = False, default_aliases: bool = True, default_macros: bool = True)
60    def __init__(
61        self,
62        *,
63        strict: bool = False,
64        default_aliases: bool = True,
65        default_macros: bool = True,
66    ) -> None:
67        self._cache: dict[tuple[str, bool, bool], tuple[list[Token], str, bool]] = {}
68
69        self.context = create_context_dict()
70        self._aliases = self.context["aliases"]
71        self._macros = self.context["macros"]
72
73        if default_aliases:
74            apply_default_aliases(self)
75
76        if default_macros:
77            apply_default_macros(self)
78
79        self.strict = strict or STRICT_MARKUP
aliases: dict[str, str]

Returns a copy of the aliases defined in context.

macros: dict[str, pytermgui.markup.parsing.MacroType]

Returns a copy of the macros defined in context.

def define(self, name: str, method: pytermgui.markup.parsing.MacroType) -> None:
 93    def define(self, name: str, method: MacroType) -> None:
 94        """Defines a markup macro.
 95
 96        Macros are essentially function bindings callable within markup. They can be
 97        very useful to represent changing data and simplify TIM code.
 98
 99        Args:
100            name: The name that will be used within TIM to call the macro. Must start with
101                a bang (`!`).
102            method: The function bound to the name given above. This function will take
103                any number of strings as arguments, and return a terminal-ready (i.e. parsed)
104                string.
105        """
106
107        if not name.startswith("!"):
108            raise ValueError("TIM macro names must be prefixed by `!`.")
109
110        self._macros[name] = method

Defines a markup macro.

Macros are essentially function bindings callable within markup. They can be very useful to represent changing data and simplify TIM code.

Args
  • name: The name that will be used within TIM to call the macro. Must start with a bang (!).
  • method: The function bound to the name given above. This function will take any number of strings as arguments, and return a terminal-ready (i.e. parsed) string.
def alias(self, name: str, value: str, *, generate_unsetter: bool = True) -> None:
112    def alias(self, name: str, value: str, *, generate_unsetter: bool = True) -> None:
113        """Creates an alias from one custom name to a set of styles.
114
115        These can be used to store and reference a set of tags using only one name.
116
117        Aliases may reference other aliases, but only do this consciously, as it can become
118        a hard to follow trail of sorrow very quickly!
119
120        Args:
121            name: The name this alias will be referenced by.
122            value: The markup value that the alias will represent.
123            generate_unsetter: Disable generating clearer aliases.
124
125                For example:
126                    ```
127                    my-tag = 141 bold italic
128                    ```
129
130                will generate:
131                    ```
132                    /my-tag = /fg /bold /italic
133                    ```
134        """
135
136        value = eval_alias(value, self.context)
137
138        def _generate_unsetter() -> str:
139            unsetter = ""
140            for tag in value.split():
141                if "(" in tag and ")" in tag:
142                    tag = tag[: tag.find("(")]
143
144                if tag in self._aliases or tag in self._macros:
145                    unsetter += f" /{tag}"
146                    continue
147
148                try:
149                    color = str_to_color(tag)
150                    unsetter += f" /{'bg' if color.background else 'fg'}"
151
152                except ColorSyntaxError:
153                    unsetter += f" /{tag}"
154
155            return unsetter.lstrip(" ")
156
157        self._aliases[name] = value
158
159        if generate_unsetter:
160            self._aliases[f"/{name}"] = _generate_unsetter()

Creates an alias from one custom name to a set of styles.

These can be used to store and reference a set of tags using only one name.

Aliases may reference other aliases, but only do this consciously, as it can become a hard to follow trail of sorrow very quickly!

Args
  • name: The name this alias will be referenced by.
  • value: The markup value that the alias will represent.
  • generate_unsetter: Disable generating clearer aliases.

    For example: my-tag = 141 bold italic

    will generate: /my-tag = /fg /bold /italic

def alias_multiple(self, *, generate_unsetter: bool = True, **items: str) -> None:
162    def alias_multiple(self, *, generate_unsetter: bool = True, **items: str) -> None:
163        """Runs `MarkupLanguage.alias` repeatedly for all arguments.
164
165        The same `generate_unsetter` value will be used for all calls.
166
167        You can use this in two forms:
168
169        - Traditional keyword arguments:
170
171            ```python
172            lang.alias_multiple(my-tag1="bold", my-tag2="italic")
173            ```
174
175        - Keyword argument unpacking:
176
177            ```python
178            my_aliases = {"my-tag1": "bold", "my-tag2": "italic"}
179            lang.alias_multiple(**my_aliases)
180            ```
181        """
182
183        for name, value in items.items():
184            self.alias(name, value, generate_unsetter=generate_unsetter)

Runs MarkupLanguage.alias repeatedly for all arguments.

The same generate_unsetter value will be used for all calls.

You can use this in two forms:

  • Traditional keyword arguments:

    lang.alias_multiple(my-tag1="bold", my-tag2="italic")
    
  • Keyword argument unpacking:

    my_aliases = {"my-tag1": "bold", "my-tag2": "italic"}
    lang.alias_multiple(**my_aliases)
    
def parse( self, text: str, optimize: bool = False, append_reset: bool = True) -> str:
186    def parse(
187        self,
188        text: str,
189        optimize: bool = False,
190        append_reset: bool = True,
191    ) -> str:
192        """Parses some markup text.
193
194        This is a thin wrapper around `pytermgui.markup.parsing.parse`. The main additions
195        of this wrapper are a caching system, as well as state management.
196
197        Ignoring caching, all calls to this function would be equivalent to:
198
199            def parse(self, *args, **kwargs) -> str:
200                kwargs["context"] = self.context
201
202                return parse(*args, **kwargs)
203        """
204
205        key = (text, optimize, append_reset)
206
207        if key in self._cache:
208            tokens, output, has_macro = self._cache[key]
209
210            if has_macro:
211                output = parse_tokens(
212                    tokens,
213                    optimize=optimize,
214                    append_reset=append_reset,
215                    context=self.context,
216                    ignore_unknown_tags=not self.strict,
217                )
218
219                self._cache[key] = (tokens, output, has_macro)
220
221            return output
222
223        tokens = list(tokenize_markup(text))
224
225        output = parse_tokens(
226            tokens,
227            optimize=optimize,
228            append_reset=append_reset,
229            context=self.context,
230            ignore_unknown_tags=not self.strict,
231        )
232
233        has_macro = any(token.is_macro() for token in tokens)
234
235        self._cache[key] = (tokens, output, has_macro)
236
237        return output

Parses some markup text.

This is a thin wrapper around pytermgui.markup.parsing.parse. The main additions of this wrapper are a caching system, as well as state management.

Ignoring caching, all calls to this function would be equivalent to:

def parse(self, *args, **kwargs) -> str:
    kwargs["context"] = self.context

    return parse(*args, **kwargs)
@staticmethod
def get_markup(text: str) -> str:
240    @staticmethod
241    def get_markup(text: str) -> str:
242        """DEPRECATED: Convert ANSI text into markup.
243
244        This function does not use context, and thus is out of place here.
245        """
246
247        return tokens_to_markup(list(tokenize_ansi(text)))

DEPRECATED: Convert ANSI text into markup.

This function does not use context, and thus is out of place here.

def group_styles( self, text: str, tokenizer: Callable[[str], Iterator[pytermgui.markup.tokens.Token]] = <function tokenize_ansi>) -> Generator[pytermgui.markup.language.StyledText, NoneType, NoneType]:
249    def group_styles(
250        self, text: str, tokenizer: Tokenizer = tokenize_ansi
251    ) -> Generator[StyledText, None, None]:
252        """Generate StyledText-s from some text, using our context.
253
254        See `StyledText.group_styles` for arguments.
255        """
256
257        yield from StyledText.group_styles(
258            text, tokenizer=tokenizer, context=self.context
259        )

Generate StyledText-s from some text, using our context.

See StyledText.group_styles for arguments.

def print(self, *args, **kwargs) -> None:
261    def print(self, *args, **kwargs) -> None:
262        """Parse all arguments and pass them through to print, along with kwargs."""
263
264        parsed = []
265        for arg in args:
266            parsed.append(self.parse(str(arg)))
267
268        get_terminal().print(*parsed, **kwargs)

Parse all arguments and pass them through to print, along with kwargs.

@dataclass(frozen=True)
class StyledText:
274@dataclass(frozen=True)
275class StyledText:
276    """An ANSI style-infused string.
277
278    This is a sort of helper to handle ANSI texts in a more semantic manner. It
279    keeps track of a sequence and a plain part.
280
281    Calling `len()` will return the length of the printable, non-ANSI part, and
282    indexing will return the characters at the given slice, but also include the
283    sequences that are applied to them.
284
285    To generate StyledText-s, it is recommended to use the `StyledText.yield_from_ansi`
286    classmethod.
287    """
288
289    __slots__ = ("plain", "sequences", "tokens", "link")
290
291    sequences: str
292    plain: str
293    tokens: list[Token]
294    link: str | None
295
296    # TODO: These attributes could be added in the future, though doing so would cement
297    #       StyledText-s only ever being created by `group_styles`.
298    #
299    #       Maybe we could add a `styled_text.as_bold()`, `as_color()` type API? We would
300    #       still need to somehow default the attributes somehow, which could then be done
301    #       with a helper function?
302    #
303    # foreground: Color | None
304    # background: Color | None
305    # bold: bool
306    # dim: bool
307    # italic: bool
308    # underline: bool
309    # strikethrough: bool
310    # inverse: bool
311
312    @staticmethod
313    def group_styles(
314        text: str,
315        tokenizer: Tokenizer = tokenize_ansi,
316        context: ContextDict | None = None,
317    ) -> Generator[StyledText, None, None]:
318        """Yields StyledTexts from an ANSI coded string.
319
320        A new StyledText will be created each time a non-plain token follows a
321        plain token, thus all texts will represent a single (ANSI)PLAIN group
322        of characters.
323        """
324
325        context = context if context is not None else create_context_dict()
326
327        parsers = PARSERS
328        link = None
329
330        def _parse(token: Token) -> str:
331            nonlocal link
332
333            if token.is_macro():
334                return token.markup
335
336            if token.is_hyperlink():
337                link = token
338                return ""
339
340            if link is not None and Token.is_clear(token) and token.targets(link):
341                link = None
342
343            if token.is_clear() and token.value not in CLEARERS:
344                return token.markup
345
346            # The full text (last arg) is not relevant here, as ANSI parsing doesn't
347            # use any context-defined tags, so no errors will occur.
348            return parsers[type(token)](token, context, lambda: "")  # type: ignore
349
350        tokens: list[Token] = []
351        token: Token
352
353        for token in tokenizer(text):
354            if token.is_plain():
355                yield StyledText(
356                    "".join(_parse(tkn) for tkn in tokens),
357                    token.value,
358                    tokens + [token],
359                    link.value if link is not None else None,
360                )
361
362                tokens = [tkn for tkn in tokens if not tkn.is_cursor()]
363                continue
364
365            if Token.is_clear(token):
366                tokens = [tkn for tkn in tokens if not token.targets(tkn)]
367
368                if len(tokens) > 0 and tokens[-1] == token:
369                    continue
370
371            if len(tokens) > 0 and all(tkn.is_clear() for tkn in tokens):
372                tokens = []
373
374            tokens.append(token)
375
376        # if len(tokens) > 0:
377        #     token = PlainToken("")
378
379        #     yield StyledText(
380        #         "".join(_parse(tkn) for tkn in tokens),
381        #         token.value,
382        #         tokens + [token],
383        #         link.value if link is not None else None,
384        #     )
385
386    @classmethod
387    def first_of(cls, text: str) -> StyledText | None:
388        """Returns the first element of cls.yield_from_ansi(text)."""
389
390        for item in cls.group_styles(text):
391            return item
392
393        return None
394
395    def __len__(self) -> int:
396        return len(self.plain)
397
398    def __str__(self) -> str:
399        return self.sequences + self.plain
400
401    def __getitem__(self, sli: int | slice) -> str:
402        return self.sequences + self.plain[sli]

An ANSI style-infused string.

This is a sort of helper to handle ANSI texts in a more semantic manner. It keeps track of a sequence and a plain part.

Calling len() will return the length of the printable, non-ANSI part, and indexing will return the characters at the given slice, but also include the sequences that are applied to them.

To generate StyledText-s, it is recommended to use the StyledText.yield_from_ansi classmethod.

StyledText( sequences: str, plain: str, tokens: list[pytermgui.markup.tokens.Token], link: str | None)
sequences: str
plain: str
@staticmethod
def group_styles( text: str, tokenizer: Callable[[str], Iterator[pytermgui.markup.tokens.Token]] = <function tokenize_ansi>, context: pytermgui.markup.parsing.ContextDict | None = None) -> Generator[pytermgui.markup.language.StyledText, NoneType, NoneType]:
312    @staticmethod
313    def group_styles(
314        text: str,
315        tokenizer: Tokenizer = tokenize_ansi,
316        context: ContextDict | None = None,
317    ) -> Generator[StyledText, None, None]:
318        """Yields StyledTexts from an ANSI coded string.
319
320        A new StyledText will be created each time a non-plain token follows a
321        plain token, thus all texts will represent a single (ANSI)PLAIN group
322        of characters.
323        """
324
325        context = context if context is not None else create_context_dict()
326
327        parsers = PARSERS
328        link = None
329
330        def _parse(token: Token) -> str:
331            nonlocal link
332
333            if token.is_macro():
334                return token.markup
335
336            if token.is_hyperlink():
337                link = token
338                return ""
339
340            if link is not None and Token.is_clear(token) and token.targets(link):
341                link = None
342
343            if token.is_clear() and token.value not in CLEARERS:
344                return token.markup
345
346            # The full text (last arg) is not relevant here, as ANSI parsing doesn't
347            # use any context-defined tags, so no errors will occur.
348            return parsers[type(token)](token, context, lambda: "")  # type: ignore
349
350        tokens: list[Token] = []
351        token: Token
352
353        for token in tokenizer(text):
354            if token.is_plain():
355                yield StyledText(
356                    "".join(_parse(tkn) for tkn in tokens),
357                    token.value,
358                    tokens + [token],
359                    link.value if link is not None else None,
360                )
361
362                tokens = [tkn for tkn in tokens if not tkn.is_cursor()]
363                continue
364
365            if Token.is_clear(token):
366                tokens = [tkn for tkn in tokens if not token.targets(tkn)]
367
368                if len(tokens) > 0 and tokens[-1] == token:
369                    continue
370
371            if len(tokens) > 0 and all(tkn.is_clear() for tkn in tokens):
372                tokens = []
373
374            tokens.append(token)
375
376        # if len(tokens) > 0:
377        #     token = PlainToken("")
378
379        #     yield StyledText(
380        #         "".join(_parse(tkn) for tkn in tokens),
381        #         token.value,
382        #         tokens + [token],
383        #         link.value if link is not None else None,
384        #     )

Yields StyledTexts from an ANSI coded string.

A new StyledText will be created each time a non-plain token follows a plain token, thus all texts will represent a single (ANSI)PLAIN group of characters.

@classmethod
def first_of(cls, text: str) -> pytermgui.markup.language.StyledText | None:
386    @classmethod
387    def first_of(cls, text: str) -> StyledText | None:
388        """Returns the first element of cls.yield_from_ansi(text)."""
389
390        for item in cls.group_styles(text):
391            return item
392
393        return None

Returns the first element of cls.yield_from_ansi(text).