136136
1371371. `[*]` resets everything, just like `[_]`, but the text color will remain in `default_color`
138138 (if no `default_color` is set, it resets everything, exactly like `[_]`)
139- 2. `[*color]` `[*c]` will reset the text color, just like `[_color]`, but then also make it `default_color`
140- (if no `default_color` is set, both are treated as invalid formatting codes)
141- 3. `[default]` will just color the text in `default_color`
139+ 2. `[default]` will just color the text in `default_color`
142140 (if no `default_color` is set, it's treated as an invalid formatting code)
143- 4 . `[background:default]` `[BG:default]` will color the background in `default_color`
141+ 3 . `[background:default]` `[BG:default]` will color the background in `default_color`
144142 (if no `default_color` is set, both are treated as invalid formatting codes)\n
145143
146144Unlike the standard console colors, the default color can be changed by using the following modifiers:
162160from .regex import Regex , Match , Pattern
163161from .color import Color , rgba , Rgba , Hexa
164162
165- from typing import Optional , cast
163+ from typing import Optional , Literal , cast
166164import ctypes as _ctypes
167165import regex as _rx
168166import sys as _sys
187185}
188186_COMPILED : dict [str , Pattern ] = { # PRECOMPILE REGULAR EXPRESSIONS
189187 "*" : _re .compile (r"\[\s*([^]_]*?)\s*\*\s*([^]_]*?)\]" ),
190- "*color " : _re .compile (r"\[\s* ([^]_] *?)\s*\*c(?:olor)? \s*([^]_]*?)\] " ),
188+ "*_inside " : _re .compile (r"([^|] *?)\s*\*\s*([^|]*) " ),
191189 "ansi_seq" : _re .compile (ANSI .CHAR + r"(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])" ),
192190 "formatting" : _rx .compile (
193191 Regex .brackets ("[" , "]" , is_group = True , ignore_in_strings = False )
200198 "bg?_default" : _re .compile (r"(?i)((?:" + _PREFIX_RX ["BG" ] + r")?)\s*default" ),
201199 "bg_default" : _re .compile (r"(?i)" + _PREFIX_RX ["BG" ] + r"\s*default" ),
202200 "modifier" : _re .compile (
203- r"(?i)((?:BG\s*:)?)\s*("
201+ r"(?i)^ ((?:BG\s*:)?)\s*("
204202 + "|" .join (
205203 [f"{ _re .escape (m )} +" for m in _DEFAULT_COLOR_MODS ["lighten" ] + _DEFAULT_COLOR_MODS ["darken" ]]
206204 )
@@ -267,20 +265,13 @@ def to_ansi(
267265 `format_codes` module documentation."""
268266 if not isinstance (string , str ):
269267 string = str (string )
270- use_default , default_specified = False , default_color is not None
271- if _validate_default and default_specified :
272- if Color .is_valid_rgba (default_color , False ):
273- use_default = True
274- elif Color .is_valid_hexa (default_color , False ):
275- use_default , default_color = True , Color .to_rgba (default_color ) # type: ignore[assignment]
268+ if _validate_default :
269+ use_default , default_color = FormatCodes .__validate_default_color (default_color )
276270 else :
277- use_default = default_specified
278- default_color = cast (Optional [rgba ], default_color )
271+ use_default = default_color is not None
272+ default_color = cast (Optional [rgba ], default_color )
279273 if use_default :
280274 string = _COMPILED ["*" ].sub (r"[\1_|default\2]" , string ) # REPLACE `[…|*|…]` WITH `[…|_|default|…]`
281- string = _COMPILED ["*color" ].sub (
282- r"[\1default\2]" , string
283- ) # REPLACE `[…|*color|…]` OR `[…|*c|…]` WITH `[…|default|…]`
284275 else :
285276 string = _COMPILED ["*" ].sub (r"[\1_\2]" , string ) # REPLACE `[…|*|…]` WITH `[…|_|…]`
286277
@@ -311,7 +302,7 @@ def replace_keys(match: Match) -> str:
311302 _default_start = False ,
312303 _validate_default = False ,
313304 )
314- format_keys = [ k . strip () for k in formats . split ( "|" ) if k . strip ()]
305+ format_keys = FormatCodes . __formats_to_keys ( formats )
315306 ansi_formats = [
316307 r if (r := FormatCodes .__get_replacement (k , default_color , brightness_steps )) != k else f"[{ k } ]"
317308 for k in format_keys
@@ -367,6 +358,52 @@ def escape_ansi(ansi_string: str) -> str:
367358 """Escapes all ANSI codes in the string, so they are visible when output to the console."""
368359 return ansi_string .replace (ANSI .CHAR , ANSI .ESCAPED_CHAR )
369360
361+ @staticmethod
362+ def escape (string : str , default_color : Optional [Rgba | Hexa ] = None , _escape_char : Literal ["/" , "\\ " ] = "/" ) -> str :
363+ """Escapes all valid formatting codes in the string, so they are visible when output
364+ to the console using `FormatCodes.print()`. Invalid formatting codes remain unchanged.\n
365+ -----------------------------------------------------------------------------------------
366+ For exact information about how to use special formatting codes, see the
367+ `format_codes` module documentation."""
368+ if not isinstance (string , str ):
369+ string = str (string )
370+
371+ use_default , default_color = FormatCodes .__validate_default_color (default_color )
372+
373+ def escape_format_code (match : Match ) -> str :
374+ """Escape formatting code if it contains valid format keys."""
375+ formats , auto_reset_txt = match .group (1 ), match .group (3 )
376+
377+ # CHECK IF ALREADY ESCAPED OR CONTAINS NO FORMATTING
378+ if not formats or _COMPILED ["escape_char_cond" ].match (match .group (0 )):
379+ return match .group (0 )
380+
381+ # TEMPORARILY REPLACE `*` FOR VALIDATION
382+ _formats = formats
383+ if use_default :
384+ _formats = _COMPILED ["*_inside" ].sub (r"\1_|default\2" , formats )
385+ else :
386+ _formats = _COMPILED ["*_inside" ].sub (r"\1_\2" , formats )
387+
388+ if all ((FormatCodes .__get_replacement (k , default_color ) != k ) for k in FormatCodes .__formats_to_keys (_formats )):
389+ # ESCAPE THE FORMATTING CODE
390+ escaped = f"[{ _escape_char } { formats } ]"
391+ if auto_reset_txt :
392+ # RECURSIVELY ESCAPE FORMATTING IN AUTO-RESET TEXT
393+ escaped_auto_reset = FormatCodes .escape (auto_reset_txt , default_color , _escape_char )
394+ escaped += f"({ escaped_auto_reset } )"
395+ return escaped
396+ else :
397+ # KEEP INVALID FORMATTING CODES AS-IS
398+ result = f"[{ formats } ]"
399+ if auto_reset_txt :
400+ # STILL RECURSIVELY PROCESS AUTO-RESET TEXT
401+ escaped_auto_reset = FormatCodes .escape (auto_reset_txt , default_color , _escape_char )
402+ result += f"({ escaped_auto_reset } )"
403+ return result
404+
405+ return "\n " .join (_COMPILED ["formatting" ].sub (escape_format_code , l ) for l in string .split ("\n " ))
406+
370407 @staticmethod
371408 def remove_ansi (
372409 ansi_string : str ,
@@ -397,7 +434,7 @@ def replacement(match: Match) -> str:
397434 return _COMPILED ["ansi_seq" ].sub ("" , ansi_string )
398435
399436 @staticmethod
400- def remove_formatting (
437+ def remove (
401438 string : str ,
402439 default_color : Optional [Rgba | Hexa ] = None ,
403440 get_removals : bool = False ,
@@ -432,6 +469,21 @@ def __config_console() -> None:
432469 pass
433470 _CONSOLE_ANSI_CONFIGURED = True
434471
472+ @staticmethod
473+ def __formats_to_keys (formats : str ) -> list [str ]:
474+ return [k .strip () for k in formats .split ("|" ) if k .strip ()]
475+
476+ @staticmethod
477+ def __validate_default_color (default_color : Optional [Rgba | Hexa ]) -> tuple [bool , Optional [rgba ]]:
478+ """Validate and convert `default_color` to rgba format."""
479+ if default_color is None :
480+ return False , None
481+ if Color .is_valid_rgba (default_color , False ):
482+ return True , cast (rgba , default_color )
483+ elif Color .is_valid_hexa (default_color , False ):
484+ return True , Color .to_rgba (default_color )
485+ return False , None
486+
435487 @staticmethod
436488 def __get_default_ansi (
437489 default_color : rgba ,
@@ -447,10 +499,7 @@ def __get_default_ansi(
447499 return (ANSI .SEQ_BG_COLOR if format_key and _COMPILED ["bg_default" ].search (format_key ) else ANSI .SEQ_COLOR ).format (
448500 * _default_color
449501 )
450- if format_key is None or not (format_key in _modifiers [0 ] or format_key in _modifiers [1 ]):
451- return None
452- match = _COMPILED ["modifier" ].match (format_key )
453- if not match :
502+ if format_key is None or not (match := _COMPILED ["modifier" ].match (format_key )):
454503 return None
455504 is_bg , modifiers = match .groups ()
456505 adjust = 0
0 commit comments