"""Filters for processing ANSI colors within Jinja templates.""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. import re import jinja2 __all__ = [ 'strip_ansi', 'ansi2html', 'ansi2latex' ] _ANSI_RE = re.compile('\x1b\\[(.*?)([@-~])') _ANSI_COLORS = ( 'ansi-black', 'ansi-red', 'ansi-green', 'ansi-yellow', 'ansi-blue', 'ansi-magenta', 'ansi-cyan', 'ansi-white', 'ansi-black-intense', 'ansi-red-intense', 'ansi-green-intense', 'ansi-yellow-intense', 'ansi-blue-intense', 'ansi-magenta-intense', 'ansi-cyan-intense', 'ansi-white-intense', ) 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. Parameters ---------- text : unicode Text containing ANSI colors to convert to HTML """ text = jinja2.utils.escape(text) return _ansi2anything(text, _htmlconverter) def ansi2latex(text): """ Convert ANSI colors to LaTeX colors. Parameters ---------- text : unicode Text containing ANSI colors to convert to LaTeX """ return _ansi2anything(text, _latexconverter) def _htmlconverter(fg, bg, bold, underline, inverse): """ Return start and end tags for given foreground/background/bold/underline. """ if (fg, bg, bold, underline, inverse) == (None, None, False, False, False): return '', '' classes = [] styles = [] if inverse: fg, bg = bg, fg if isinstance(fg, int): classes.append(_ANSI_COLORS[fg] + '-fg') elif fg: styles.append('color: rgb({},{},{})'.format(*fg)) elif inverse: classes.append('ansi-default-inverse-fg') if isinstance(bg, int): classes.append(_ANSI_COLORS[bg] + '-bg') elif bg: styles.append('background-color: rgb({},{},{})'.format(*bg)) elif inverse: classes.append('ansi-default-inverse-bg') if bold: classes.append('ansi-bold') if underline: classes.append('ansi-underline') starttag = '' def _latexconverter(fg, bg, bold, underline, inverse): """ Return start and end markup given foreground/background/bold/underline. """ if (fg, bg, bold, underline, inverse) == (None, None, False, False, False): return '', '' starttag, endtag = '', '' if inverse: fg, bg = bg, fg if isinstance(fg, int): starttag += r'\textcolor{' + _ANSI_COLORS[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 elif inverse: starttag += r'\textcolor{ansi-default-inverse-fg}{' endtag = '}' + endtag if isinstance(bg, int): starttag += r'\setlength{\fboxsep}{0pt}' starttag += r'\colorbox{' + _ANSI_COLORS[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 elif inverse: starttag += r'\setlength{\fboxsep}{0pt}' starttag += r'\colorbox{ansi-default-inverse-bg}{' endtag = r'\strut}' + endtag if bold: starttag += r'\textbf{' endtag = '}' + endtag if underline: starttag += r'\underline{' endtag = '}' + endtag return starttag, endtag 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). 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. """ fg, bg = None, None bold = False underline = False inverse = False numbers = [] out = [] while text: m = _ANSI_RE.search(text) if m: if m.group(2) == 'm': try: # Empty code is same as code 0 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 + 8 if bold and fg in range(8) else fg, bg, bold, underline, inverse) out.append(starttag) out.append(chunk) out.append(endtag) while numbers: n = numbers.pop(0) if n == 0: # Code 0 (same as empty code): reset everything fg = bg = None bold = underline = inverse = False elif n == 1: bold = True elif n == 4: underline = True elif n == 5: # Code 5: blinking bold = True elif n == 7: inverse = True elif n in (21, 22): bold = False elif n == 24: underline = False elif n == 27: inverse = 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 elif 90 <= n <= 97: fg = n - 90 + 8 elif 100 <= n <= 107: bg = n - 100 + 8 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: # 16 default terminal colors return idx 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