diff options
author | Georg Brandl <georg@python.org> | 2016-02-14 19:07:16 +0100 |
---|---|---|
committer | Georg Brandl <georg@python.org> | 2016-02-14 19:07:16 +0100 |
commit | f8e9795479bdace4e822641567d806538a345f4f (patch) | |
tree | 1f7b1d99f35d28a1ef2a27a49046797e48179186 | |
parent | e71840a35ffb2aa453542ecd6f770ffbaa7db439 (diff) | |
parent | 064edec39bc9075dad066450c9a5bab254f4a581 (diff) | |
download | pygments-f8e9795479bdace4e822641567d806538a345f4f.tar.gz |
Merged in Carreau/pygments-main/256ansi (pull request #531)
Allow ansi escape sequence as color format
-rw-r--r-- | doc/docs/styles.rst | 59 | ||||
-rw-r--r-- | pygments/formatters/terminal256.py | 38 | ||||
-rw-r--r-- | pygments/style.py | 43 | ||||
-rw-r--r-- | tests/test_lexers_other.py | 23 | ||||
-rw-r--r-- | tests/test_terminal_formatter.py | 78 |
5 files changed, 232 insertions, 9 deletions
diff --git a/doc/docs/styles.rst b/doc/docs/styles.rst index d56db0db..0076d062 100644 --- a/doc/docs/styles.rst +++ b/doc/docs/styles.rst @@ -143,3 +143,62 @@ a way to iterate over all styles: >>> from pygments.styles import get_all_styles >>> styles = list(get_all_styles()) + + +.. _AnsiTerminalStyle: + +Terminal Styles +=============== + +.. versionadded:: 2.2 + +Custom styles used with `Terminal256` formatter can also defines colors using +ansi-color. To do so use the `#ansigreen`, `#ansired` or any other colors +defined in ``pygments.style.ansilist``. Foreground ANSI colors will be mapped +to the corresponding `escape codes 30 to 37 +<https://en.wikipedia.org/wiki/ANSI_escape_code#Colors>`_ thus respecting any +custom color mapping and themes provided by many terminal emulators. Light +variant are treated for foreground color with and extra bold flag. +`bg:#ansi<color>` will also be respected, except the light variant will be the +same shade as their light variant. + +See following example where the color of the string `"hello world"` is governed +by the escape sequence `\x1b34;01m` (Ansi Blue, Bold, `41` beeing red background) +instead of an extended foreground & background color. + +.. sourcecode:: pycon + + >>> from pygments import highlight + >>> from pygments.style import Style + >>> from pygments.token import Token + >>> from pygments.lexers import Python3Lexer + >>> from pygments.formatters import Terminal256Formatter + + >>> class MyStyle(Style): + styles = { + Token.String: '#ansiblue bg:#ansired', + } + + >>> code = 'print("Hello World")' + >>> result = highlight(code, Python3Lexer(), Terminal256Formatter(style=MyStyle)) + >>> print(result.encode()) + b'print(\x1b[34;41;01m"\x1b[39;49;00m\x1b[34;41;01mHello World\x1b[39;49;00m\x1b[34;41;01m"\x1b[39;49;00m)\n' + +Style that use `#ansi*` colors might not correctly work with +formatters others than ``Terminal256``. `HtmlFormatter` is capable of handling +some `#ansi*` code and will map to a fixed HTML/CSS color. For example, +`#ansiblue` will be converted to `color:#0000ff` , `#ansired` to `color:#ff0000`. + +By definition of Ansi color the following color are considered "light" colors, +and will be rendered by most terminal as bold: + + - "darkgray", "red", "green", "yellow", "blue", "fuchsia", "turquoise", + "white" + + +The following are considered "dark" color and will be rendered as non-bold: + + - "black", "darkred", "darkgreen", "brown", "darkblue", "purple", "teal", + "lightgray" + +Exact behavior might depends on the terminal emulator you are using, and its settings. diff --git a/pygments/formatters/terminal256.py b/pygments/formatters/terminal256.py index af311955..1aa19f25 100644 --- a/pygments/formatters/terminal256.py +++ b/pygments/formatters/terminal256.py @@ -27,6 +27,8 @@ import sys from pygments.formatter import Formatter +from pygments.console import codes +from pygments.style import ansilist __all__ = ['Terminal256Formatter', 'TerminalTrueColorFormatter'] @@ -47,9 +49,21 @@ class EscapeSequence: def color_string(self): attrs = [] if self.fg is not None: - attrs.extend(("38", "5", "%i" % self.fg)) + if self.fg in ansilist: + esc = codes[self.fg[5:]] + if ';01m' in esc: + self.bold = True + # extract fg color code. + attrs.append(esc[2:4]) + else : + attrs.extend(("38", "5", "%i" % self.fg)) if self.bg is not None: - attrs.extend(("48", "5", "%i" % self.bg)) + if self.bg in ansilist: + esc = codes[self.bg[5:]] + # extract fg color code, add 10 for bg. + attrs.append(str(int(esc[2:4])+10)) + else : + attrs.extend(("48", "5", "%i" % self.bg)) if self.bold: attrs.append("01") if self.underline: @@ -89,6 +103,13 @@ class Terminal256Formatter(Formatter): and converts them to nearest ANSI 256-color escape sequences. Bold and underline attributes from the style are preserved (and displayed). + .. versionadded:: 2.2 + + If the used style defined foreground colors in the form `#ansi*`, then + `Terminal256Formatter` will map these to non extended foreground color. + + See AnsiTerminalStyle_ for more informations. + .. versionadded:: 0.9 Options accepted: @@ -169,6 +190,10 @@ class Terminal256Formatter(Formatter): def _color_index(self, color): index = self.best_match.get(color, None) + if color in ansilist: + # strip the `#ansi` part an look up code + index = color + self.best_match[color] = index if index is None: try: rgb = int(str(color), 16) @@ -185,9 +210,14 @@ class Terminal256Formatter(Formatter): def _setup_styles(self): for ttype, ndef in self.style: escape = EscapeSequence() - if ndef['color']: + # get foreground from ansicolor if set + if ndef['ansicolor']: + escape.fg = self._color_index(ndef['ansicolor']) + elif ndef['color']: escape.fg = self._color_index(ndef['color']) - if ndef['bgcolor']: + if ndef['bgansicolor']: + escape.bg = self._color_index(ndef['bgansicolor']) + elif ndef['bgcolor']: escape.bg = self._color_index(ndef['bgcolor']) if self.usebold and ndef['bold']: escape.bold = True diff --git a/pygments/style.py b/pygments/style.py index b2b990ea..8c7de528 100644 --- a/pygments/style.py +++ b/pygments/style.py @@ -13,6 +13,29 @@ from pygments.token import Token, STANDARD_TYPES from pygments.util import add_metaclass + +_ansimap = { + ## + '#ansiblack': '000000', + '#ansidarkred': '7f0000', + '#ansidarkgreen': '007f00', + '#ansibrown': '7f7fe0', + '#ansidarkblue': '00007f', + '#ansipurple': '7f007f', + '#ansiteal': '007f7f', + '#ansilightgray': 'e5e5e5', + ### normal + '#ansidarkgray': '555555', + '#ansired': 'ff0000', + '#ansigreen': '00ff00', + '#ansiyellow': 'ffff00', + '#ansiblue': '0000ff', + '#ansifuchsia': 'ff00ff', + '#ansiturquoise': '00ffff', + '#ansiwhite': 'ffffff', + } +ansilist = set(_ansimap.keys()) + class StyleMeta(type): def __new__(mcs, name, bases, dct): @@ -22,6 +45,8 @@ class StyleMeta(type): obj.styles[token] = '' def colorformat(text): + if text in ansilist: + return text if text[0:1] == '#': col = text[1:] if len(col) == 6: @@ -79,16 +104,30 @@ class StyleMeta(type): def style_for_token(cls, token): t = cls._styles[token] + ansicolor = None + color = t[0] + if color.startswith('#ansi'): + ansicolor = color + color = _ansimap[color] + bgansicolor = None + bgcolor = t[4] + if bgcolor.startswith('#ansi'): + bgansicolor = bgcolor + bgcolor = _ansimap[bgcolor] + return { - 'color': t[0] or None, + 'color': color or None, 'bold': bool(t[1]), 'italic': bool(t[2]), 'underline': bool(t[3]), - 'bgcolor': t[4] or None, + 'bgcolor': bgcolor or None, 'border': t[5] or None, 'roman': bool(t[6]) or None, 'sans': bool(t[7]) or None, 'mono': bool(t[8]) or None, + 'ansicolor': ansicolor, + 'bgansicolor': bgansicolor, + } def list_styles(cls): diff --git a/tests/test_lexers_other.py b/tests/test_lexers_other.py index bb667c05..4c5132ad 100644 --- a/tests/test_lexers_other.py +++ b/tests/test_lexers_other.py @@ -16,6 +16,26 @@ from pygments.lexers.scripting import EasytrieveLexer, JclLexer, RexxLexer def _exampleFilePath(filename): return os.path.join(os.path.dirname(__file__), 'examplefiles', filename) +_MAX_LENGTH = 80 + +def safe_repr(obj, short=False): + try: + result = repr(obj) + except Exception: + result = object.__repr__(obj) + if not short or len(result) < _MAX_LENGTH: + return result + return result[:_MAX_LENGTH] + ' [truncated]...' + + +class MyTestCase(unittest.TestCase): + ### Assert less is 2.7+ only. + def assertLess(self, a, b, msg=None): + """Just like self.assertTrue(a < b), but with a nicer default message.""" + if not a < b: + standardMsg = '%s not less than %s' % (safe_repr(a), safe_repr(b)) + self.fail(self._formatMessage(msg, standardMsg)) + class AnalyseTextTest(unittest.TestCase): def _testCanRecognizeAndGuessExampleFiles(self, lexer): @@ -42,8 +62,7 @@ class AnalyseTextTest(unittest.TestCase): for lexerToTest in LEXERS_TO_TEST: self._testCanRecognizeAndGuessExampleFiles(lexerToTest) - -class EasyTrieveLexerTest(unittest.TestCase): +class EasyTrieveLexerTest(MyTestCase): def testCanGuessFromText(self): self.assertLess(0, EasytrieveLexer.analyse_text('MACRO')) self.assertLess(0, EasytrieveLexer.analyse_text('\nMACRO')) diff --git a/tests/test_terminal_formatter.py b/tests/test_terminal_formatter.py index 07337cd5..84373790 100644 --- a/tests/test_terminal_formatter.py +++ b/tests/test_terminal_formatter.py @@ -14,7 +14,12 @@ import re from pygments.util import StringIO from pygments.lexers.sql import PlPgsqlLexer -from pygments.formatters import TerminalFormatter +from pygments.formatters import TerminalFormatter,Terminal256Formatter, HtmlFormatter, LatexFormatter + +from pygments.style import Style +from pygments.token import Token +from pygments.lexers import Python3Lexer +from pygments import highlight DEMO_TEXT = '''\ -- comment @@ -49,3 +54,74 @@ class TerminalFormatterTest(unittest.TestCase): for a, b in zip(DEMO_TEXT.splitlines(), plain.splitlines()): self.assertTrue(a in b) + + + + + + +class MyStyle(Style): + + styles = { + Token.Comment: '#ansidarkgray', + Token.String: '#ansiblue bg:#ansidarkred', + Token.Number : '#ansigreen bg:#ansidarkgreen', + Token.Number.Hex: '#ansidarkgreen bg:#ansired', + } + + + +code = ''' +# this should be a comment +print("Hello World") +async def function(a,b,c, *d, **kwarg:Bool)->Bool: + pass + return 123, 0xb3e3 + +''' +_MAX_LENGTH = 80 + +def safe_repr(obj, short=False): + try: + result = repr(obj) + except Exception: + result = object.__repr__(obj) + if not short or len(result) < _MAX_LENGTH: + return result + return result[:_MAX_LENGTH] + ' [truncated]...' + +class MyTest(unittest.TestCase): + + def assertIn(self, member, container, msg=None): + """Just like self.assertTrue(a in b), but with a nicer default message.""" + if member not in container: + standardMsg = '%s not found in %s' % (safe_repr(member), + safe_repr(container)) + self.fail(self._formatMessage(msg, standardMsg)) + + +termtest = lambda x: highlight(x, Python3Lexer(), Terminal256Formatter(style=MyStyle)) +class Terminal256FormatterTest(MyTest): + + + def test_style_html(self): + style = HtmlFormatter(style=MyStyle).get_style_defs() + self.assertIn('#555555',style, "ansigray for comment not html css style") + + def test_tex_works(self): + """check tex Formatter don't crash""" + highlight(code, Python3Lexer(), LatexFormatter(style=MyStyle)) + + def test_html_works(self): + highlight(code, Python3Lexer(), HtmlFormatter(style=MyStyle)) + + def test_256esc_seq(self): + """ + test that a few escape sequences are actualy used when using #ansi<> color codes + """ + self.assertIn('32;41',termtest('0x123')) + self.assertIn('32;42',termtest('123')) + self.assertIn('30;01',termtest('#comment')) + self.assertIn('34;41',termtest('"String"')) + + |