diff options
Diffstat (limited to '')
-rw-r--r-- | sphinx/util/console.py | 122 |
1 files changed, 101 insertions, 21 deletions
diff --git a/sphinx/util/console.py b/sphinx/util/console.py index 0fc9450..4257056 100644 --- a/sphinx/util/console.py +++ b/sphinx/util/console.py @@ -6,6 +6,37 @@ import os import re import shutil import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Final + + # fmt: off + def reset(text: str) -> str: ... # NoQA: E704 + def bold(text: str) -> str: ... # NoQA: E704 + def faint(text: str) -> str: ... # NoQA: E704 + def standout(text: str) -> str: ... # NoQA: E704 + def underline(text: str) -> str: ... # NoQA: E704 + def blink(text: str) -> str: ... # NoQA: E704 + + def black(text: str) -> str: ... # NoQA: E704 + def white(text: str) -> str: ... # NoQA: E704 + def red(text: str) -> str: ... # NoQA: E704 + def green(text: str) -> str: ... # NoQA: E704 + def yellow(text: str) -> str: ... # NoQA: E704 + def blue(text: str) -> str: ... # NoQA: E704 + def fuchsia(text: str) -> str: ... # NoQA: E704 + def teal(text: str) -> str: ... # NoQA: E704 + + def darkgray(text: str) -> str: ... # NoQA: E704 + def lightgray(text: str) -> str: ... # NoQA: E704 + def darkred(text: str) -> str: ... # NoQA: E704 + def darkgreen(text: str) -> str: ... # NoQA: E704 + def brown(text: str) -> str: ... # NoQA: E704 + def darkblue(text: str) -> str: ... # NoQA: E704 + def purple(text: str) -> str: ... # NoQA: E704 + def turquoise(text: str) -> str: ... # NoQA: E704 + # fmt: on try: # check if colorama is installed to support color on Windows @@ -13,8 +44,26 @@ try: except ImportError: colorama = None +_CSI: Final[str] = re.escape('\x1b[') # 'ESC [': Control Sequence Introducer + +# Pattern matching ANSI control sequences containing colors. +_ansi_color_re: Final[re.Pattern[str]] = re.compile(r'\x1b\[(?:\d+;){0,2}\d*m') + +_ansi_re: Final[re.Pattern[str]] = re.compile( + _CSI + + r""" + (?: + (?:\d+;){0,2}\d*m # ANSI color code ('m' is equivalent to '0m') + | + [012]?K # ANSI Erase in Line ('K' is equivalent to '0K') + )""", + re.VERBOSE | re.ASCII, +) +"""Pattern matching ANSI CSI colors (SGR) and erase line (EL) sequences. + +See :func:`strip_escape_sequences` for details. +""" -_ansi_re: re.Pattern[str] = re.compile('\x1b\\[(\\d\\d;){0,2}\\d\\dm') codes: dict[str, str] = {} @@ -37,7 +86,7 @@ def term_width_line(text: str) -> str: return text + '\n' else: # codes are not displayed, this must be taken into account - return text.ljust(_tw + len(text) - len(_ansi_re.sub('', text))) + '\r' + return text.ljust(_tw + len(text) - len(strip_escape_sequences(text))) + '\r' def color_terminal() -> bool: @@ -55,9 +104,7 @@ def color_terminal() -> bool: if 'COLORTERM' in os.environ: return True term = os.environ.get('TERM', 'dumb').lower() - if term in ('xterm', 'linux') or 'color' in term: - return True - return False + return term in ('xterm', 'linux') or 'color' in term def nocolor() -> None: @@ -87,41 +134,74 @@ def colorize(name: str, text: str, input_mode: bool = False) -> str: def strip_colors(s: str) -> str: - return re.compile('\x1b.*?m').sub('', s) + """Remove the ANSI color codes in a string *s*. + + .. caution:: + + This function is not meant to be used in production and should only + be used for testing Sphinx's output messages. + + .. seealso:: :func:`strip_escape_sequences` + """ + return _ansi_color_re.sub('', s) + + +def strip_escape_sequences(text: str, /) -> str: + r"""Remove the ANSI CSI colors and "erase in line" sequences. + + Other `escape sequences `__ (e.g., VT100-specific functions) are not + supported and only control sequences *natively* known to Sphinx (i.e., + colors declared in this module and "erase entire line" (``'\x1b[2K'``)) + are eliminated by this function. + + .. caution:: + + This function is not meant to be used in production and should only + be used for testing Sphinx's output messages that were not tempered + with by third-party extensions. + + .. versionadded:: 7.3 + + This function is added as an *experimental* feature. + + __ https://en.wikipedia.org/wiki/ANSI_escape_code + """ + return _ansi_re.sub('', text) def create_color_func(name: str) -> None: def inner(text: str) -> str: return colorize(name, text) + globals()[name] = inner _attrs = { - 'reset': '39;49;00m', - 'bold': '01m', - 'faint': '02m', - 'standout': '03m', + 'reset': '39;49;00m', + 'bold': '01m', + 'faint': '02m', + 'standout': '03m', 'underline': '04m', - 'blink': '05m', + 'blink': '05m', } -for _name, _value in _attrs.items(): - codes[_name] = '\x1b[' + _value +for __name, __value in _attrs.items(): + codes[__name] = '\x1b[' + __value _colors = [ - ('black', 'darkgray'), - ('darkred', 'red'), + ('black', 'darkgray'), + ('darkred', 'red'), ('darkgreen', 'green'), - ('brown', 'yellow'), - ('darkblue', 'blue'), - ('purple', 'fuchsia'), + ('brown', 'yellow'), + ('darkblue', 'blue'), + ('purple', 'fuchsia'), ('turquoise', 'teal'), ('lightgray', 'white'), ] -for i, (dark, light) in enumerate(_colors, 30): - codes[dark] = '\x1b[%im' % i - codes[light] = '\x1b[%im' % (i + 60) +for __i, (__dark, __light) in enumerate(_colors, 30): + codes[__dark] = '\x1b[%im' % __i + codes[__light] = '\x1b[%im' % (__i + 60) _orig_codes = codes.copy() |