Skip to content

Commit 267c361

Browse files
committed
update 1.8.5
1 parent afc8014 commit 267c361

4 files changed

Lines changed: 122 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
## ... `v1.8.5`
2020
* made the help command `xulbux-help` new primarily use console default colors so it fits the user's console theme
2121
* changed the default `box_bg_color` in `Console.log_box_filled()` from `green` to `br:green`
22+
* removed the `*c` and `*color` formatting codes, since the user should just use `default` to achieve the exact same instead
23+
* fixed a bug in all methods of `FormatCodes`, where as soon as you used more than a single modifier format code (*e.g.* `[ll]` *or* `[++]`), it was treated as invalid and ignored
24+
* renamed the method `FormatCodes.remove_formatting()` to `FormatCodes.remove()`
25+
* added a new method `FormatCodes.escape()` which will escape all valid formatting codes in a string
2226

2327
## 11.11.2025 `v1.8.4` 𝓢𝓲𝓷𝓰𝓵𝓮𝓼 𝓓𝓪𝔂 🥇😉
2428
* adjusted `Regex.hsla_str()` to not include optional degree (`°`) and percent (`%`) symbols in the captured groups

src/xulbux/console.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ def log(
455455
px, mx = (" " * title_px) if has_title_bg else "", " " * title_mx
456456
tab = " " * (tab_size - 1 - ((len(mx) + (title_len := len(title) + 2 * len(px))) % tab_size))
457457
if format_linebreaks:
458-
clean_prompt, removals = FormatCodes.remove_formatting(str(prompt), get_removals=True, _ignore_linebreaks=True)
458+
clean_prompt, removals = FormatCodes.remove(str(prompt), get_removals=True, _ignore_linebreaks=True)
459459
prompt_lst = (
460460
String.split_count(l, Console.w - (title_len + len(tab) + 2 * len(mx))) for l in str(clean_prompt).splitlines()
461461
)
@@ -773,7 +773,7 @@ def __prepare_log_box(
773773
else:
774774
lines = [line for val in values for line in str(val).splitlines()]
775775

776-
unfmt_lines = [FormatCodes.remove_formatting(line, default_color) for line in lines]
776+
unfmt_lines = [FormatCodes.remove(line, default_color) for line in lines]
777777
max_line_len = max(len(line) for line in unfmt_lines)
778778
return lines, cast(list[tuple[str, tuple[tuple[int, str], ...]]], unfmt_lines), max_line_len
779779

src/xulbux/format_codes.py

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,9 @@
136136
137137
1. `[*]` 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
146144
Unlike the standard console colors, the default color can be changed by using the following modifiers:
@@ -162,7 +160,7 @@
162160
from .regex import Regex, Match, Pattern
163161
from .color import Color, rgba, Rgba, Hexa
164162

165-
from typing import Optional, cast
163+
from typing import Optional, Literal, cast
166164
import ctypes as _ctypes
167165
import regex as _rx
168166
import sys as _sys
@@ -187,7 +185,7 @@
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)
@@ -200,7 +198,7 @@
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

tests/test_format_codes.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,47 @@ def test_escape_ansi():
3636
assert FormatCodes.escape_ansi(ansi_string) == escaped_string
3737

3838

39+
def test_escape():
40+
# TEST BASIC FORMATTING CODES
41+
assert FormatCodes.escape("[b]Hello[_]") == "[/b]Hello[/_]"
42+
assert FormatCodes.escape("[bold|italic]Text[_]") == "[/bold|italic]Text[/_]"
43+
44+
# TEST WITH COLORS
45+
assert FormatCodes.escape("[#F87]Hello[_]") == "[/#F87]Hello[/_]"
46+
assert FormatCodes.escape("[rgb(255, 136, 119)]Hello[_]") == "[/rgb(255, 136, 119)]Hello[/_]"
47+
48+
# TEST WITH DEFAULT COLOR
49+
assert FormatCodes.escape("[default]Hello", default_color="#FFF") == "[/default]Hello"
50+
assert FormatCodes.escape("[bg:default]Hello", default_color="#FFF") == "[/bg:default]Hello"
51+
52+
# TEST WITH * FORMATTING CODE
53+
assert FormatCodes.escape("[*]Hello", default_color="#FFF") == "[/*]Hello"
54+
assert FormatCodes.escape("[b|*]Hello", default_color="#FFF") == "[/b|*]Hello"
55+
56+
# TEST WITH AUTO-RESET
57+
assert FormatCodes.escape("[b](Hello)") == "[/b](Hello)"
58+
assert FormatCodes.escape("[*](Hello)", default_color="#FFF") == "[/*](Hello)"
59+
60+
# TEST INVALID FORMATTING CODES (SHOULD REMAIN UNCHANGED)
61+
assert FormatCodes.escape("[invalid]Hello") == "[invalid]Hello"
62+
assert FormatCodes.escape("[default]Hello") == "[default]Hello" # NO 'default_color'
63+
assert FormatCodes.escape("[*]Hello") == "[/*]Hello" # NO 'default_color'
64+
65+
# TEST ALREADY ESCAPED CODES
66+
assert FormatCodes.escape("[/b]Hello") == "[/b]Hello"
67+
assert FormatCodes.escape("[/*]Hello", default_color="#FFF") == "[/*]Hello"
68+
69+
# TEST WITH BRIGHTNESS MODIFIERS
70+
assert FormatCodes.escape("[l]Hello", default_color="#FFF") == "[/l]Hello"
71+
assert FormatCodes.escape("[ll]Hello", default_color="#FFF") == "[/ll]Hello"
72+
assert FormatCodes.escape("[+]Hello", default_color="#FFF") == "[/+]Hello"
73+
assert FormatCodes.escape("[++]Hello", default_color="#FFF") == "[/++]Hello"
74+
assert FormatCodes.escape("[d]Hello", default_color="#FFF") == "[/d]Hello"
75+
assert FormatCodes.escape("[dd]Hello", default_color="#FFF") == "[/dd]Hello"
76+
assert FormatCodes.escape("[-]Hello", default_color="#FFF") == "[/-]Hello"
77+
assert FormatCodes.escape("[--]Hello", default_color="#FFF") == "[/--]Hello"
78+
79+
3980
def test_remove_ansi():
4081
ansi_string = f"{bold}Hello {orange}World!{reset}"
4182
clean_string = "Hello World!"
@@ -52,11 +93,11 @@ def test_remove_ansi_with_removals():
5293
def test_remove_formatting():
5394
format_string = "[b](Hello [#F87](World!))"
5495
clean_string = "Hello World!"
55-
assert FormatCodes.remove_formatting(format_string) == clean_string
96+
assert FormatCodes.remove(format_string) == clean_string
5697

5798

5899
def test_remove_formatting_with_removals():
59100
format_string = "[b](Hello [#F87](World!))"
60101
clean_string = "Hello World!"
61102
removals = ((0, default), (0, bold), (6, orange), (12, default), (12, reset_bold))
62-
assert FormatCodes.remove_formatting(format_string, default_color="#FFF", get_removals=True) == (clean_string, removals)
103+
assert FormatCodes.remove(format_string, default_color="#FFF", get_removals=True) == (clean_string, removals)

0 commit comments

Comments
 (0)