diff --git a/nbconvert/filters/ansi.py b/nbconvert/filters/ansi.py
index f2e66b4e9..4dfd0bc5b 100644
--- a/nbconvert/filters/ansi.py
+++ b/nbconvert/filters/ansi.py
@@ -1,155 +1,282 @@
-"""Filters for processing ANSI colors within Jinja templates.
-"""
+"""Filters for processing ANSI colors within Jinja templates."""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import re
-from nbconvert.utils import coloransi
-from ipython_genutils.text import strip_ansi
-
+import jinja2
__all__ = [
'strip_ansi',
'ansi2html',
- 'single_ansi2latex',
'ansi2latex'
]
-ansi_colormap = {
- '30': 'ansiblack',
- '31': 'ansired',
- '32': 'ansigreen',
- '33': 'ansiyellow',
- '34': 'ansiblue',
- '35': 'ansipurple',
- '36': 'ansicyan',
- '37': 'ansigrey',
- '01': 'ansibold',
-}
-
-html_escapes = {
- '<': '<',
- '>': '>',
- "'": ''',
- '"': '"',
- '`': '`',
-}
-ansi_re = re.compile('\x1b' + r'\[([\dA-Fa-f;]*?)m')
+_ANSI_RE = re.compile('\x1b\\[(.*?)([@-~])')
+
+_FG_HTML = (
+ 'ansiblack',
+ 'ansired',
+ 'ansigreen',
+ 'ansiyellow',
+ 'ansiblue',
+ 'ansipurple',
+ 'ansicyan',
+ 'ansigray',
+)
+
+_BG_HTML = (
+ 'ansibgblack',
+ 'ansibgred',
+ 'ansibggreen',
+ 'ansibgyellow',
+ 'ansibgblue',
+ 'ansibgpurple',
+ 'ansibgcyan',
+ 'ansibggray',
+)
+
+_FG_LATEX = (
+ 'black',
+ 'red',
+ 'green',
+ 'brown',
+ 'blue',
+ 'purple',
+ 'cyan',
+ 'lightgray',
+)
+
+_BG_LATEX = (
+ 'darkgray',
+ 'lightred',
+ 'lightgreen',
+ 'yellow',
+ 'lightblue',
+ 'lightpurple',
+ 'lightcyan',
+ 'white',
+)
+
+
+def strip_ansi(source):
+ """
+ Remove ANSI escape codes from text.
+
+ Parameters
+ ----------
+ source : str
+ Source to remove the ANSI from
+
+ """
+ return _ANSI_RE.sub('', source)
+
def ansi2html(text):
"""
- Convert ansi colors to html colors.
-
+ Convert ANSI colors to HTML colors.
+
Parameters
----------
text : str
- Text containing ansi colors to convert to html
+ Text containing ANSI colors to convert to HTML
+
"""
+ text = str(jinja2.utils.escape(text))
+ return _ansi2anything(text, _htmlconverter)
- # do ampersand first
- text = text.replace('&', '&')
-
- for c, escape in html_escapes.items():
- text = text.replace(c, escape)
-
- m = ansi_re.search(text)
- opened = False
- cmds = []
- opener = ''
- closer = ''
- while m:
- cmds = m.groups()[0].split(';')
- closer = '' if opened else ''
-
- # True if there is there more than one element in cmds, *or*
- # if there is only one but it is not equal to a string of zeroes.
- opened = len(cmds) > 1 or cmds[0] != '0' * len(cmds[0])
- classes = []
- for cmd in cmds:
- if cmd in ansi_colormap:
- classes.append(ansi_colormap[cmd])
-
- if classes:
- opener = '' % (' '.join(classes))
- else:
- opener = ''
- text = re.sub(ansi_re, closer + opener, text, 1)
- m = ansi_re.search(text)
+def ansi2latex(text):
+ """
+ Convert ANSI colors to LaTeX colors.
+
+ Parameters
+ ----------
+ text : str
+ Text containing ANSI colors to convert to LaTeX
+
+ """
+ return _ansi2anything(text, _latexconverter)
+
- if opened:
- text += ''
- return text
+def _htmlconverter(fg, bg, bold):
+ """
+ Return start and end tags for given foreground/background/bold.
+
+ """
+ if (fg, bg, bold) == (None, None, False):
+ return '', ''
+
+ classes = []
+ styles = []
+ if isinstance(fg, int):
+ classes.append(_FG_HTML[fg])
+ elif fg:
+ styles.append('color: rgb({},{},{})'.format(*fg))
-def single_ansi2latex(code):
- """Converts single ansi markup to latex format.
+ if isinstance(bg, int):
+ classes.append(_BG_HTML[bg])
+ elif bg:
+ styles.append('background-color: rgb({},{},{})'.format(*bg))
- Return latex code and number of open brackets.
+ if bold:
+ classes.append('ansibold')
- Accepts codes like '\x1b[1;32m' (bold, red) and the short form '\x1b[32m' (red)
+ starttag = ''
+ return starttag, ''
- Colors are matched to those defined in coloransi, which defines colors
- using the 0, 1 (bold) and 5 (blinking) styles. Styles 1 and 5 are
- interpreted as bold. All other styles are mapped to 0. Note that in
- coloransi, a style of 1 does not just mean bold; for example, Brown is
- "0;33", but Yellow is "1;33". An empty string is returned for unrecognised
- codes and the "reset" code '\x1b[m'.
+
+def _latexconverter(fg, bg, bold):
"""
- components = code.split(';')
- if len(components) > 1:
- # Style is digits after '['
- style = int(components[0].split('[')[-1])
- color = components[1][:-1]
- else:
- style = 0
- color = components[0][-3:-1]
-
- # If the style is not normal (0), bold (1) or blinking (5) then treat it as normal
- if style not in [0, 1, 5]:
- style = 0
-
- for name, tcode in coloransi.color_templates:
- tstyle, tcolor = tcode.split(';')
- tstyle = int(tstyle)
- if tstyle == style and tcolor == color:
- break
- else:
- return '', 0
+ Return start and end markup given foreground/background/bold.
- if style == 5:
- name = name[5:] # BlinkRed -> Red, etc
- name = name.lower()
+ """
+ if (fg, bg, bold) == (None, None, False):
+ return '', ''
- if style in [1, 5]:
- return r'\textbf{\color{'+name+'}', 1
- else:
- return r'{\color{'+name+'}', 1
+ starttag, endtag = '', ''
+
+ if isinstance(fg, int):
+ starttag += r'\textcolor{' + _FG_LATEX[fg] + '}{'
+ endtag = '}' + endtag
+ elif fg:
+ # See http://tex.stackexchange.com/a/291102/13684
+ starttag += r'\def\tcRGB{\textcolor[RGB]}\expandafter'
+ starttag += r'\tcRGB\expandafter{\detokenize{%s,%s,%s}}{' % fg
+ endtag = '}' + endtag
+
+ if isinstance(bg, int):
+ starttag += r'\setlength{\fboxsep}{0pt}\colorbox{'
+ starttag += _BG_LATEX[bg] + '}{'
+ endtag = r'\strut}' + endtag
+ elif bg:
+ starttag += r'\setlength{\fboxsep}{0pt}'
+ # See http://tex.stackexchange.com/a/291102/13684
+ starttag += r'\def\cbRGB{\colorbox[RGB]}\expandafter'
+ starttag += r'\cbRGB\expandafter{\detokenize{%s,%s,%s}}{' % bg
+ endtag = r'\strut}' + endtag
+
+ if bold:
+ starttag += r'\textbf{'
+ endtag = '}' + endtag
+ return starttag, endtag
-def ansi2latex(text):
- """Converts ansi formated text to latex version
- based on https://bitbucket.org/birkenfeld/sphinx-contrib/ansi.py
+def _ansi2anything(text, converter):
+ r"""
+ Convert ANSI colors to HTML or LaTeX.
+
+ See https://en.wikipedia.org/wiki/ANSI_escape_code
+
+ Accepts codes like '\x1b[32m' (red) and '\x1b[1;32m' (bold, red).
+ The codes 1 (bold) and 5 (blinking) are selecting a bold font, code
+ 0 and an empty code ('\x1b[m') reset colors and bold-ness.
+ Unlike in most terminals, "bold" doesn't change the color.
+ The codes 21 and 22 deselect "bold", the codes 39 and 49 deselect
+ the foreground and background color, respectively.
+ The codes 38 and 48 select the "extended" set of foreground and
+ background colors, respectively.
+
+ Non-color escape sequences (not ending with 'm') are filtered out.
+
+ Ideally, this should have the same behavior as the function
+ fixConsole() in notebook/notebook/static/base/js/utils.js.
+
"""
- color_pattern = re.compile('\x1b\\[([^m]*)m')
- last_end = 0
- openbrack = 0
- outstring = ''
- for match in color_pattern.finditer(text):
- head = text[last_end:match.start()]
- outstring += head
- if openbrack:
- outstring += '}'*openbrack
- openbrack = 0
- code = match.group()
- if not (code == coloransi.TermColors.Normal or openbrack):
- texform, openbrack = single_ansi2latex(code)
- outstring += texform
- last_end = match.end()
-
- # Add the remainer of the string and THEN close any remaining color brackets.
- outstring += text[last_end:]
- if openbrack:
- outstring += '}'*openbrack
- return outstring.strip()
+ fg, bg = None, None
+ bold = False
+ numbers = []
+ out = []
+
+ while text:
+ m = _ANSI_RE.search(text)
+ if m:
+ if m.group(2) == 'm':
+ try:
+ numbers = [int(n) if n else 0
+ for n in m.group(1).split(';')]
+ except ValueError:
+ pass # Invalid color specification
+ else:
+ pass # Not a color code
+ chunk, text = text[:m.start()], text[m.end():]
+ else:
+ chunk, text = text, ''
+
+ if chunk:
+ starttag, endtag = converter(fg, bg, bold)
+ out.append(starttag)
+ out.append(chunk)
+ out.append(endtag)
+
+ while numbers:
+ n = numbers.pop(0)
+ if n == 0:
+ fg = bg = None
+ bold = False
+ elif n in (1, 5):
+ bold = True
+ elif n in (21, 22):
+ bold = False
+ elif 30 <= n <= 37:
+ fg = n - 30
+ elif n == 38:
+ try:
+ fg = _get_extended_color(numbers)
+ except ValueError:
+ numbers.clear()
+ elif n == 39:
+ fg = None
+ elif 40 <= n <= 47:
+ bg = n - 40
+ elif n == 48:
+ try:
+ bg = _get_extended_color(numbers)
+ except ValueError:
+ numbers.clear()
+ elif n == 49:
+ bg = None
+ else:
+ pass # Unknown codes are ignored
+ return ''.join(out)
+
+
+def _get_extended_color(numbers):
+ n = numbers.pop(0)
+ if n == 2 and len(numbers) >= 3:
+ # 24-bit RGB
+ r = numbers.pop(0)
+ g = numbers.pop(0)
+ b = numbers.pop(0)
+ if not all(0 <= c <= 255 for c in (r, g, b)):
+ raise ValueError()
+ elif n == 5 and len(numbers) >= 1:
+ # 256 colors
+ idx = numbers.pop(0)
+ if idx < 0:
+ raise ValueError()
+ elif idx < 16:
+ # 8 default terminal colors
+ return idx % 8 # ignore bright/non-bright distinction
+ elif idx < 232:
+ # 6x6x6 color cube, see http://stackoverflow.com/a/27165165/500098
+ r = (idx - 16) // 36
+ r = 55 + r * 40 if r > 0 else 0
+ g = ((idx - 16) % 36) // 6
+ g = 55 + g * 40 if g > 0 else 0
+ b = (idx - 16) % 6
+ b = 55 + b * 40 if b > 0 else 0
+ elif idx < 256:
+ # grayscale, see http://stackoverflow.com/a/27165165/500098
+ r = g = b = (idx - 232) * 10 + 8
+ else:
+ raise ValueError()
+ else:
+ raise ValueError()
+ return r, g, b
diff --git a/nbconvert/filters/tests/test_ansi.py b/nbconvert/filters/tests/test_ansi.py
index b4c6f611c..701f50bd7 100644
--- a/nbconvert/filters/tests/test_ansi.py
+++ b/nbconvert/filters/tests/test_ansi.py
@@ -5,8 +5,6 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
-from nbconvert.utils.coloransi import TermColors
-
from ...tests.base import TestsBase
from ..ansi import strip_ansi, ansi2html, ansi2latex
@@ -17,60 +15,50 @@ class TestAnsi(TestsBase):
def test_strip_ansi(self):
"""strip_ansi test"""
correct_outputs = {
- '%s%s%s' % (TermColors.Green, TermColors.White, TermColors.Red) : '',
- 'hello%s' % TermColors.Blue: 'hello',
- 'he%s%sllo' % (TermColors.Yellow, TermColors.Cyan) : 'hello',
- '%shello' % TermColors.Blue : 'hello',
- '{0}h{0}e{0}l{0}l{0}o{0}'.format(TermColors.Red) : 'hello',
- 'hel%slo' % TermColors.Green : 'hello',
- 'hello' : 'hello'}
+ '\x1b[32m\x1b[1m\x1b[0;44m\x1b[38;2;255;0;255m\x1b[;m\x1b[m': '',
+ 'hello\x1b[000;34m': 'hello',
+ 'he\x1b[1;33m\x1b[;36mllo': 'hello',
+ '\x1b[;34mhello': 'hello',
+ '\x1b[31mh\x1b[31me\x1b[31ml\x1b[31ml\x1b[31mo\x1b[31m': 'hello',
+ 'hel\x1b[;00;;032;;;32mlo': 'hello',
+ 'hello': 'hello',
+ }
for inval, outval in correct_outputs.items():
- self._try_strip_ansi(inval, outval)
-
-
- def _try_strip_ansi(self, inval, outval):
- self.assertEqual(outval, strip_ansi(inval))
-
+ self.assertEqual(outval, strip_ansi(inval))
def test_ansi2html(self):
"""ansi2html test"""
correct_outputs = {
- '%s' % (TermColors.Red) : '',
- 'hello%s' % TermColors.Blue: 'hello',
- 'he%s%sllo' % (TermColors.Green, TermColors.Cyan) : 'hello',
- '%shello' % TermColors.Yellow : 'hello',
- '{0}h{0}e{0}l{0}l{0}o{0}'.format(TermColors.White) : 'hello',
- 'hel%slo' % TermColors.Green : 'hello',
- 'hello' : 'hello'}
+ '\x1b[31m': '',
+ 'hello\x1b[34m': 'hello',
+ 'he\x1b[32m\x1b[36mllo': 'hello',
+ '\x1b[1;33mhello': 'hello',
+ '\x1b[37mh\x1b[0;037me\x1b[;0037ml\x1b[00;37ml\x1b[;;37mo': 'hello',
+ 'hel\x1b[0;32mlo': 'hello',
+ 'hello': 'hello',
+ '\x1b[1mhello\x1b[33mworld\x1b[0m': 'helloworld',
+ }
for inval, outval in correct_outputs.items():
- self._try_ansi2html(inval, outval)
-
-
- def _try_ansi2html(self, inval, outval):
- self.fuzzy_compare(outval, ansi2html(inval))
-
+ self.fuzzy_compare(outval, ansi2html(inval))
def test_ansi2latex(self):
"""ansi2latex test"""
correct_outputs = {
- '%s' % (TermColors.Red) : r'{\color{red}}',
- 'hello%s' % TermColors.Blue: r'hello{\color{blue}}',
- 'he%s%sllo' % (TermColors.Green, TermColors.Cyan) : r'he{\color{green}}{\color{cyan}llo}',
- '%shello' % TermColors.Yellow : r'\textbf{\color{yellow}hello}',
- '{0}h{0}e{0}l{0}l{0}o{0}'.format(TermColors.White) : r'\textbf{\color{white}h}\textbf{\color{white}e}\textbf{\color{white}l}\textbf{\color{white}l}\textbf{\color{white}o}\textbf{\color{white}}',
- 'hel%slo' % TermColors.Green : r'hel{\color{green}lo}',
- 'hello' : 'hello',
- u'hello\x1b[34mthere\x1b[mworld' : u'hello{\\color{blue}there}world',
- u'hello\x1b[mthere': u'hellothere',
- u'hello\x1b[01;34mthere' : u"hello\\textbf{\\color{lightblue}there}",
- u'hello\x1b[001;34mthere' : u"hello\\textbf{\\color{lightblue}there}"
- }
+ '\x1b[31m': '',
+ 'hello\x1b[34m': 'hello',
+ 'he\x1b[32m\x1b[36mllo': r'he\textcolor{cyan}{llo}',
+ '\x1b[1;33mhello': r'\textcolor{brown}{\textbf{hello}}',
+ '\x1b[37mh\x1b[0;037me\x1b[;0037ml\x1b[00;37ml\x1b[;;37mo': r'\textcolor{lightgray}{h}\textcolor{lightgray}{e}\textcolor{lightgray}{l}\textcolor{lightgray}{l}\textcolor{lightgray}{o}',
+ 'hel\x1b[0;32mlo': r'hel\textcolor{green}{lo}',
+ 'hello': 'hello',
+ 'hello\x1b[34mthere\x1b[mworld': r'hello\textcolor{blue}{there}world',
+ 'hello\x1b[mthere': 'hellothere',
+ 'hello\x1b[01;34mthere': r'hello\textcolor{blue}{\textbf{there}}',
+ 'hello\x1b[001;34mthere': r'hello\textcolor{blue}{\textbf{there}}',
+ '\x1b[1mhello\x1b[33mworld\x1b[0m': r'\textbf{hello}\textcolor{brown}{\textbf{world}}',
+ }
for inval, outval in correct_outputs.items():
- self._try_ansi2latex(inval, outval)
-
-
- def _try_ansi2latex(self, inval, outval):
- self.fuzzy_compare(outval, ansi2latex(inval), case_sensitive=True)
+ self.fuzzy_compare(outval, ansi2latex(inval), case_sensitive=True)
diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx
index b46fc9c1a..18a4ae48c 100644
--- a/nbconvert/templates/latex/base.tplx
+++ b/nbconvert/templates/latex/base.tplx
@@ -64,7 +64,7 @@ This template does not define a docclass, the inheriting class must define this.
\definecolor{lightblue}{rgb}{0.53,0.81,0.92}
\definecolor{lightpurple}{rgb}{0.87,0.63,0.87}
\definecolor{lightcyan}{rgb}{0.5,1.0,0.83}
-
+
% commands and environments needed by pandoc snippets
% extracted from the output of `pandoc -s`
\providecommand{\tightlist}{%
diff --git a/nbconvert/utils/coloransi.py b/nbconvert/utils/coloransi.py
deleted file mode 100644
index 06ede5ead..000000000
--- a/nbconvert/utils/coloransi.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Tools for coloring text in ANSI terminals.
-"""
-
-# subset of IPython.utils.ansicolors, which is:
-#*****************************************************************************
-# Copyright (C) 2002-2006 Fernando Perez.
-#
-# Distributed under the terms of the BSD License. The full license is in
-# the file COPYING, distributed as part of this software.
-#*****************************************************************************
-
-__all__ = ['TermColors']
-
-
-color_templates = (
- # Dark colors
- ("Black" , "0;30"),
- ("Red" , "0;31"),
- ("Green" , "0;32"),
- ("Brown" , "0;33"),
- ("Blue" , "0;34"),
- ("Purple" , "0;35"),
- ("Cyan" , "0;36"),
- ("LightGray" , "0;37"),
- # Light colors
- ("DarkGray" , "1;30"),
- ("LightRed" , "1;31"),
- ("LightGreen" , "1;32"),
- ("Yellow" , "1;33"),
- ("LightBlue" , "1;34"),
- ("LightPurple" , "1;35"),
- ("LightCyan" , "1;36"),
- ("White" , "1;37"),
- # Blinking colors. Probably should not be used in anything serious.
- ("BlinkBlack" , "5;30"),
- ("BlinkRed" , "5;31"),
- ("BlinkGreen" , "5;32"),
- ("BlinkYellow" , "5;33"),
- ("BlinkBlue" , "5;34"),
- ("BlinkPurple" , "5;35"),
- ("BlinkCyan" , "5;36"),
- ("BlinkLightGray", "5;37"),
- )
-
-def make_color_table(in_class):
- """Build a set of color attributes in a class.
-
- Helper function for building the :class:`TermColors` and
- :class`InputTermColors`.
- """
- for name,value in color_templates:
- setattr(in_class,name,in_class._base % value)
-
-class TermColors:
- """Color escape sequences.
-
- This class defines the escape sequences for all the standard (ANSI?)
- colors in terminals. Also defines a NoColor escape which is just the null
- string, suitable for defining 'dummy' color schemes in terminals which get
- confused by color escapes.
-
- This class should be used as a mixin for building color schemes."""
-
- NoColor = '' # for color schemes in color-less terminals.
- Normal = '\033[0m' # Reset normal coloring
- _base = '\033[%sm' # Template for all other colors
-
-# Build the actual color table as a set of class attributes:
-make_color_table(TermColors)