import re
__all__ = ["tag2ansi"]
RESET_CODE = {"reset": "\033[0m"}
STYLE_TABLE = {
"bold": "1",
"dim": "2",
"italic": "3",
"underline": "4",
"blink": "5",
"reverse": "7",
"hidden": "8",
"strike": "9",
}
COLOR_TABLE = {
"black": "30",
"red": "31",
"green": "32",
"yellow": "33",
"blue": "34",
"magenta": "35",
"cyan": "36",
"white": "37",
"brightblack": "90",
"brightred": "91",
"brightgreen": "92",
"brightyellow": "93",
"brightblue": "94",
"brightmagenta": "95",
"brightcyan": "96",
"brightwhite": "97",
# --- Extended common colors (256-color or true-color) ---
"orange": "38;5;208",
"pink": "38;5;213",
"hotpink": "38;5;205",
"purple": "38;5;129",
"violet": "38;5;177",
"brown": "38;5;94",
"olive": "38;5;100",
"lime": "38;5;118",
"teal": "38;5;30",
"navy": "38;5;19",
"gold": "38;5;220",
"silver": "38;5;250",
"gray": "38;5;245",
"darkgray": "38;5;240",
"lightgray": "38;5;252",
"indigo": "38;5;57",
"coral": "38;5;203",
"skyblue": "38;5;117",
"turquoise": "38;5;45",
}
def parse_tag(tag: str) -> str:
"""
Supports forms such as `[bold red]`, `[italic #FFAA00]`, and `[color123]`.
"""
parts = tag.split(" ")
seq = ""
for part in parts:
part = part.lower()
if part in STYLE_TABLE:
seq += f"\033[{STYLE_TABLE[part]}m"
elif part in COLOR_TABLE:
seq += f"\033[{COLOR_TABLE[part]}m"
elif part.startswith("color"):
try:
idx = int(part.removeprefix("color"))
assert 0 <= idx <= 255
seq += f"\033[38;5;{idx}m"
except ValueError:
pass
elif part.startswith("#") and len(part) == 7:
try:
r = int(part[1:3], 16)
g = int(part[3:5], 16)
b = int(part[5:7], 16)
seq += f"\033[38;2;{r};{g};{b}m"
except ValueError:
pass
else:
pass
return seq
def find_matching_paren(s: str, start_idx: int) -> int:
stack = []
pairs = {")": "(", "}": "{", "]": "["}
opens = set(pairs.values())
closes = set(pairs.keys())
for i, ch in enumerate(s[start_idx:], start=start_idx):
if ch in opens:
stack.append(ch)
elif ch in closes and stack and pairs[ch] == stack[-1]:
stack.pop()
if not stack:
return i
return -1
def auto_insert_reset(segment: str) -> str:
if (reset_pos := segment.find("[reset]")) != -1:
return segment[:reset_pos] + RESET_CODE["reset"] + segment[reset_pos + len("[reset]") :]
elif not (stripped := segment.lstrip()):
return segment + RESET_CODE["reset"]
elif (first_char := stripped[0]).isalnum():
m = re.search(r"[^a-zA-Z0-9/_-]", stripped)
if not m:
return segment + RESET_CODE["reset"]
offset = len(segment) - len(stripped)
insert_at = offset + m.start()
return segment[:insert_at] + RESET_CODE["reset"] + segment[insert_at:]
elif first_char in "({[":
offset = len(segment) - len(stripped)
match_idx = find_matching_paren(segment, offset)
if match_idx == -1:
return segment + RESET_CODE["reset"]
return segment[: match_idx + 1] + RESET_CODE["reset"] + segment[match_idx + 1 :]
else:
return segment + RESET_CODE["reset"]
[docs]
def tag2ansi(text: str) -> str:
"""
Convert `[tag1 tag2]` markup into ANSI escape sequences with automatic
reset insertion.
"""
pattern = re.compile(r"\[(?!reset)([a-zA-Z0-9+#]+(?: [a-zA-Z0-9+#]+)*)\]")
result = []
last_end = 0
active_tag = None
for match in pattern.finditer(text):
tag = match.group(1)
start, end = match.span()
# Append the preceding segment first.
if last_end < start:
seg = text[last_end:start]
if active_tag:
seg = auto_insert_reset(seg)
result.append(seg)
last_end = end
# Current tag.
if tag.lower() == "reset":
result.append(RESET_CODE)
active_tag = None
else:
seq = parse_tag(tag)
if seq:
result.append(seq)
active_tag = tag
else:
result.append(match.group(0))
# Final tail segment.
if last_end < len(text):
seg = text[last_end:]
if active_tag:
seg = auto_insert_reset(seg)
result.append(seg)
return "".join(result)