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]
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.
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.
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
Returns a copy of the macros defined in context.
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.
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
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)
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)
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.
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.
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.
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.
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.