summaryrefslogtreecommitdiffstats
path: root/sphinx/util/console.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/util/console.py')
-rw-r--r--sphinx/util/console.py122
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()