Skip to content

Commit

Permalink
Improve colorize
Browse files Browse the repository at this point in the history
  • Loading branch information
ihabunek committed Dec 2, 2022
1 parent 0e13914 commit 59b98a7
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 21 deletions.
26 changes: 26 additions & 0 deletions tests/test_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from toot.output import colorize, strip_tags, STYLES

reset = STYLES["reset"]
red = STYLES["red"]
green = STYLES["green"]
bold = STYLES["bold"]


def test_colorize():
assert colorize("foo") == "foo"
assert colorize("<red>foo</red>") == f"{red}foo{reset}{reset}"
assert colorize("foo <red>bar</red> baz") == f"foo {red}bar{reset} baz{reset}"
assert colorize("foo <red bold>bar</red bold> baz") == f"foo {red}{bold}bar{reset} baz{reset}"
assert colorize("foo <red bold>bar</red> baz") == f"foo {red}{bold}bar{reset}{bold} baz{reset}"
assert colorize("foo <red bold>bar</> baz") == f"foo {red}{bold}bar{reset} baz{reset}"
assert colorize("<red>foo<bold>bar</bold>baz</red>") == f"{red}foo{bold}bar{reset}{red}baz{reset}{reset}"


def test_strip_tags():
assert strip_tags("foo") == "foo"
assert strip_tags("<red>foo</red>") == "foo"
assert strip_tags("foo <red>bar</red> baz") == "foo bar baz"
assert strip_tags("foo <red bold>bar</red bold> baz") == "foo bar baz"
assert strip_tags("foo <red bold>bar</red> baz") == "foo bar baz"
assert strip_tags("foo <red bold>bar</> baz") == "foo bar baz"
assert strip_tags("<red>foo<bold>bar</bold>baz</red>") == "foobarbaz"
96 changes: 75 additions & 21 deletions toot/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,93 @@
from toot.wcstring import wc_wrap


START_CODES = {
'red': '\033[31m',
'green': '\033[32m',
'yellow': '\033[33m',
'blue': '\033[34m',
'magenta': '\033[35m',
'cyan': '\033[36m',
STYLES = {
'reset': '\033[0m',
'bold': '\033[1m',
'dim': '\033[2m',
'italic': '\033[3m',
'underline': '\033[4m',
'red': '\033[91m',
'green': '\033[92m',
'yellow': '\033[93m',
'blue': '\033[94m',
'magenta': '\033[95m',
'cyan': '\033[96m',
}

END_CODE = '\033[0m'
STYLE_TAG_PATTERN = re.compile(r"""
(?<!\\) # not preceeded by a backslash - allows escaping
< # literal
(/)? # optional closing - first group
(.*?) # style names - ungreedy - second group
> # literal
""", re.X)

START_PATTERN = "<(" + "|".join(START_CODES.keys()) + ")>"

END_PATTERN = "</(" + "|".join(START_CODES.keys()) + ")>"
def colorize(message):
"""
Replaces style tags in `message` with ANSI escape codes.
Markup is inspired by HTML, but you can use multiple words pre tag, e.g.:
def start_code(match):
name = match.group(1)
return START_CODES[name]
<red bold>alert!</red bold> a thing happened
Empty closing tag will reset all styes:
def colorize(text):
text = re.sub(START_PATTERN, start_code, text)
text = re.sub(END_PATTERN, END_CODE, text)
<red bold>alert!</> a thing happened
return text
Styles can be nested:
<red>red <underline>red and underline</underline> red</red>
"""

def strip_tags(text):
text = re.sub(START_PATTERN, '', text)
text = re.sub(END_PATTERN, '', text)
def _codes(styles):
for style in styles:
yield STYLES.get(style, "")

return text
def _generator(message):
# A list is used instead of a set because we want to keep style order
# This allows nesting colors, e.g. "<blue>foo<red>bar</red>baz</blue>"
position = 0
active_styles = []

for match in re.finditer(STYLE_TAG_PATTERN, message):
is_closing = bool(match.group(1))
styles = match.group(2).strip().split()

start, end = match.span()
# Replace backslash for escaped <
yield message[position:start].replace("\\<", "<")

if is_closing:
yield STYLES["reset"]

# Empty closing tag resets all styles
if styles == []:
active_styles = []
else:
active_styles = [s for s in active_styles if s not in styles]
yield from _codes(active_styles)
else:
active_styles = active_styles + styles
yield from _codes(styles)

position = end

if position == 0:
# Nothing matched, yield the original string
yield message
else:
# Yield the remaining fragment
yield message[position:]
# Reset styles at the end to prevent leaking
yield STYLES["reset"]

return "".join(_generator(message))


def strip_tags(message):
return re.sub(STYLE_TAG_PATTERN, "", message)


def use_ansi_color():
Expand Down

0 comments on commit 59b98a7

Please sign in to comment.