##// END OF EJS Templates
Deal with ansi escape codes when nbconverting to latex...
Richard Everson -
Show More
@@ -1,158 +1,177 b''
1 """Filters for processing ANSI colors within Jinja templates.
1 """Filters for processing ANSI colors within Jinja templates.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Copyright (c) 2013, the IPython Development Team.
4 # Copyright (c) 2013, the IPython Development Team.
5 #
5 #
6 # Distributed under the terms of the Modified BSD License.
6 # Distributed under the terms of the Modified BSD License.
7 #
7 #
8 # The full license is in the file COPYING.txt, distributed with this software.
8 # The full license is in the file COPYING.txt, distributed with this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Imports
12 # Imports
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 import re
15 import re
16 from IPython.utils import coloransi
16 from IPython.utils import coloransi
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Classes and functions
19 # Classes and functions
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 __all__ = [
22 __all__ = [
23 'strip_ansi',
23 'strip_ansi',
24 'ansi2html',
24 'ansi2html',
25 'single_ansi2latex',
25 'single_ansi2latex',
26 'ansi2latex'
26 'ansi2latex'
27 ]
27 ]
28
28
29 def strip_ansi(source):
29 def strip_ansi(source):
30 """
30 """
31 Remove ansi from text
31 Remove ansi from text
32
32
33 Parameters
33 Parameters
34 ----------
34 ----------
35 source : str
35 source : str
36 Source to remove the ansi from
36 Source to remove the ansi from
37 """
37 """
38
38
39 return re.sub(r'\033\[(\d|;)+?m', '', source)
39 return re.sub(r'\033\[(\d|;)+?m', '', source)
40
40
41
41
42 def ansi2html(text):
42 def ansi2html(text):
43 """
43 """
44 Conver ansi colors to html colors.
44 Conver ansi colors to html colors.
45
45
46 Parameters
46 Parameters
47 ----------
47 ----------
48 text : str
48 text : str
49 Text containing ansi colors to convert to html
49 Text containing ansi colors to convert to html
50 """
50 """
51
51
52 ansi_colormap = {
52 ansi_colormap = {
53 '30': 'ansiblack',
53 '30': 'ansiblack',
54 '31': 'ansired',
54 '31': 'ansired',
55 '32': 'ansigreen',
55 '32': 'ansigreen',
56 '33': 'ansiyellow',
56 '33': 'ansiyellow',
57 '34': 'ansiblue',
57 '34': 'ansiblue',
58 '35': 'ansipurple',
58 '35': 'ansipurple',
59 '36': 'ansicyan',
59 '36': 'ansicyan',
60 '37': 'ansigrey',
60 '37': 'ansigrey',
61 '01': 'ansibold',
61 '01': 'ansibold',
62 }
62 }
63
63
64 # do ampersand first
64 # do ampersand first
65 text = text.replace('&', '&')
65 text = text.replace('&', '&')
66 html_escapes = {
66 html_escapes = {
67 '<': '&lt;',
67 '<': '&lt;',
68 '>': '&gt;',
68 '>': '&gt;',
69 "'": '&apos;',
69 "'": '&apos;',
70 '"': '&quot;',
70 '"': '&quot;',
71 '`': '&#96;',
71 '`': '&#96;',
72 }
72 }
73
73
74 for c, escape in html_escapes.items():
74 for c, escape in html_escapes.items():
75 text = text.replace(c, escape)
75 text = text.replace(c, escape)
76
76
77 ansi_re = re.compile('\x1b' + r'\[([\dA-Fa-f;]*?)m')
77 ansi_re = re.compile('\x1b' + r'\[([\dA-Fa-f;]*?)m')
78 m = ansi_re.search(text)
78 m = ansi_re.search(text)
79 opened = False
79 opened = False
80 cmds = []
80 cmds = []
81 opener = ''
81 opener = ''
82 closer = ''
82 closer = ''
83 while m:
83 while m:
84 cmds = m.groups()[0].split(';')
84 cmds = m.groups()[0].split(';')
85 closer = '</span>' if opened else ''
85 closer = '</span>' if opened else ''
86
86
87 # True if there is there more than one element in cmds, *or*
87 # True if there is there more than one element in cmds, *or*
88 # if there is only one but it is not equal to a string of zeroes.
88 # if there is only one but it is not equal to a string of zeroes.
89 opened = len(cmds) > 1 or cmds[0] != '0' * len(cmds[0])
89 opened = len(cmds) > 1 or cmds[0] != '0' * len(cmds[0])
90 classes = []
90 classes = []
91 for cmd in cmds:
91 for cmd in cmds:
92 if cmd in ansi_colormap:
92 if cmd in ansi_colormap:
93 classes.append(ansi_colormap.get(cmd))
93 classes.append(ansi_colormap.get(cmd))
94
94
95 if classes:
95 if classes:
96 opener = '<span class="%s">' % (' '.join(classes))
96 opener = '<span class="%s">' % (' '.join(classes))
97 else:
97 else:
98 opener = ''
98 opener = ''
99 text = re.sub(ansi_re, closer + opener, text, 1)
99 text = re.sub(ansi_re, closer + opener, text, 1)
100
100
101 m = ansi_re.search(text)
101 m = ansi_re.search(text)
102
102
103 if opened:
103 if opened:
104 text += '</span>'
104 text += '</span>'
105 return text
105 return text
106
106
107
107
108 def single_ansi2latex(code):
108 def single_ansi2latex(code):
109 """Converts single ansi markup to latex format
109 """Converts single ansi markup to latex format.
110
110
111 Return latex code and number of open brackets.
111 Return latex code and number of open brackets.
112
113 Accepts codes like '\x1b[1;32m' (bold, red) and the short form '\x1b[32m' (red)
114
115 Colors are matched to those defined in coloransi, which defines colors
116 using the 0, 1 (bold) and 5 (blinking) styles. Styles 1 and 5 are
117 interpreted as bold. All other styles are mapped to 0. Note that in
118 coloransi, a style of 1 does not just mean bold; for example, Brown is
119 "0;33", but Yellow is "1;33". An empty string is returned for unrecognised
120 codes and the "reset" code '\x1b[m'.
112 """
121 """
113 for color in coloransi.color_templates:
122 components = code.split(';')
114
123 if len(components) > 1:
115 #Make sure to get the color code (which is a part of the overall style)
124 style = components[0][-1]
116 # i.e. 0;31 is valid
125 color = components[1][:-1]
117 # 31 is also valid, and means the same thing
126 else:
118 #coloransi.color_templates stores the longer of the two formats %d;%d
127 style = '0'
119 #Get the short format so we can parse that too. Short format only exist
128 color = components[0][-3:-1]
120 #if no other formating is applied (the other number must be a 0)!
129
121 style_code = getattr(coloransi.TermColors, color[0])
130 # If the style is not normal (0), bold (1) or blinking (5) then treat it as normal
122 color_code = style_code.split(';')[1]
131 if style not in '015':
123 is_normal = style_code.split(';')[0] == '0'
132 style = '0'
124
133
125 # regular weight
134 for name, tcode in coloransi.color_templates:
126 if (code == style_code) or (is_normal and code == color_code):
135 tstyle, tcolor = tcode.split(';')
127
136 if tstyle == style and tcolor == color:
128 return r'{\color{'+color[0].lower()+'}', 1
137 break
129 # bold
138 else:
130 if code == style_code[:3]+str(1)+style_code[3:]:
139 return '', 0
131 return r'\textbf{\color{'+color[0].lower()+'}', 1
140
132 return '', 0
141 if style == '5':
142 name = name[5:] # BlinkRed -> Red, etc
143 name = name.lower()
144
145 if style in '15':
146 return r'\textbf{\color{'+name+'}', 1
147 else:
148 return r'{\color{'+name+'}', 1
133
149
134 def ansi2latex(text):
150 def ansi2latex(text):
135 """Converts ansi formated text to latex version
151 """Converts ansi formated text to latex version
136
152
137 based on https://bitbucket.org/birkenfeld/sphinx-contrib/ansi.py
153 based on https://bitbucket.org/birkenfeld/sphinx-contrib/ansi.py
138 """
154 """
139 color_pattern = re.compile('\x1b\\[([^m]+)m')
155 color_pattern = re.compile('\x1b\\[([^m]*)m')
140 last_end = 0
156 last_end = 0
141 openbrack = 0
157 openbrack = 0
142 outstring = ''
158 outstring = ''
143 for match in color_pattern.finditer(text):
159 for match in color_pattern.finditer(text):
144 head = text[last_end:match.start()]
160 head = text[last_end:match.start()]
145 outstring += head
161 outstring += head
146 if openbrack:
162 if openbrack:
147 outstring += '}'*openbrack
163 outstring += '}'*openbrack
148 openbrack = 0
164 openbrack = 0
149 if not (match.group() == coloransi.TermColors.Normal or openbrack):
165 code = match.group()
150 texform, openbrack = single_ansi2latex(match.group())
166 if not (code == coloransi.TermColors.Normal or openbrack):
167 texform, openbrack = single_ansi2latex(code)
151 outstring += texform
168 outstring += texform
152 last_end = match.end()
169 last_end = match.end()
153
170
154 # Add the remainer of the string and THEN close any remaining color brackets.
171 # Add the remainer of the string and THEN close any remaining color brackets.
155 outstring += text[last_end:]
172 outstring += text[last_end:]
156 if openbrack:
173 if openbrack:
157 outstring += '}'*openbrack
174 outstring += '}'*openbrack
158 return outstring.strip()
175 return outstring.strip()
176
177
@@ -1,84 +1,87 b''
1 """
1 """
2 Module with tests for ansi filters
2 Module with tests for ansi filters
3 """
3 """
4
4
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Copyright (c) 2013, the IPython Development Team.
6 # Copyright (c) 2013, the IPython Development Team.
7 #
7 #
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9 #
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 from IPython.utils.coloransi import TermColors
17 from IPython.utils.coloransi import TermColors
18
18
19 from ...tests.base import TestsBase
19 from ...tests.base import TestsBase
20 from ..ansi import strip_ansi, ansi2html, ansi2latex
20 from ..ansi import strip_ansi, ansi2html, ansi2latex
21
21
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Class
24 # Class
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27 class TestAnsi(TestsBase):
27 class TestAnsi(TestsBase):
28 """Contains test functions for ansi.py"""
28 """Contains test functions for ansi.py"""
29
29
30 def test_strip_ansi(self):
30 def test_strip_ansi(self):
31 """strip_ansi test"""
31 """strip_ansi test"""
32 correct_outputs = {
32 correct_outputs = {
33 '%s%s%s' % (TermColors.Green, TermColors.White, TermColors.Red) : '',
33 '%s%s%s' % (TermColors.Green, TermColors.White, TermColors.Red) : '',
34 'hello%s' % TermColors.Blue: 'hello',
34 'hello%s' % TermColors.Blue: 'hello',
35 'he%s%sllo' % (TermColors.Yellow, TermColors.Cyan) : 'hello',
35 'he%s%sllo' % (TermColors.Yellow, TermColors.Cyan) : 'hello',
36 '%shello' % TermColors.Blue : 'hello',
36 '%shello' % TermColors.Blue : 'hello',
37 '{0}h{0}e{0}l{0}l{0}o{0}'.format(TermColors.Red) : 'hello',
37 '{0}h{0}e{0}l{0}l{0}o{0}'.format(TermColors.Red) : 'hello',
38 'hel%slo' % TermColors.Green : 'hello',
38 'hel%slo' % TermColors.Green : 'hello',
39 'hello' : 'hello'}
39 'hello' : 'hello'}
40
40
41 for inval, outval in correct_outputs.items():
41 for inval, outval in correct_outputs.items():
42 self._try_strip_ansi(inval, outval)
42 self._try_strip_ansi(inval, outval)
43
43
44
44
45 def _try_strip_ansi(self, inval, outval):
45 def _try_strip_ansi(self, inval, outval):
46 self.assertEqual(outval, strip_ansi(inval))
46 self.assertEqual(outval, strip_ansi(inval))
47
47
48
48
49 def test_ansi2html(self):
49 def test_ansi2html(self):
50 """ansi2html test"""
50 """ansi2html test"""
51 correct_outputs = {
51 correct_outputs = {
52 '%s' % (TermColors.Red) : '<span class="ansired"></span>',
52 '%s' % (TermColors.Red) : '<span class="ansired"></span>',
53 'hello%s' % TermColors.Blue: 'hello<span class="ansiblue"></span>',
53 'hello%s' % TermColors.Blue: 'hello<span class="ansiblue"></span>',
54 'he%s%sllo' % (TermColors.Green, TermColors.Cyan) : 'he<span class="ansigreen"></span><span class="ansicyan">llo</span>',
54 'he%s%sllo' % (TermColors.Green, TermColors.Cyan) : 'he<span class="ansigreen"></span><span class="ansicyan">llo</span>',
55 '%shello' % TermColors.Yellow : '<span class="ansiyellow">hello</span>',
55 '%shello' % TermColors.Yellow : '<span class="ansiyellow">hello</span>',
56 '{0}h{0}e{0}l{0}l{0}o{0}'.format(TermColors.White) : '<span class="ansigrey">h</span><span class="ansigrey">e</span><span class="ansigrey">l</span><span class="ansigrey">l</span><span class="ansigrey">o</span><span class="ansigrey"></span>',
56 '{0}h{0}e{0}l{0}l{0}o{0}'.format(TermColors.White) : '<span class="ansigrey">h</span><span class="ansigrey">e</span><span class="ansigrey">l</span><span class="ansigrey">l</span><span class="ansigrey">o</span><span class="ansigrey"></span>',
57 'hel%slo' % TermColors.Green : 'hel<span class="ansigreen">lo</span>',
57 'hel%slo' % TermColors.Green : 'hel<span class="ansigreen">lo</span>',
58 'hello' : 'hello'}
58 'hello' : 'hello'}
59
59
60 for inval, outval in correct_outputs.items():
60 for inval, outval in correct_outputs.items():
61 self._try_ansi2html(inval, outval)
61 self._try_ansi2html(inval, outval)
62
62
63
63
64 def _try_ansi2html(self, inval, outval):
64 def _try_ansi2html(self, inval, outval):
65 self.fuzzy_compare(outval, ansi2html(inval))
65 self.fuzzy_compare(outval, ansi2html(inval))
66
66
67
67
68 def test_ansi2latex(self):
68 def test_ansi2latex(self):
69 """ansi2latex test"""
69 """ansi2latex test"""
70 correct_outputs = {
70 correct_outputs = {
71 '%s' % (TermColors.Red) : r'{\color{red}}',
71 '%s' % (TermColors.Red) : r'{\color{red}}',
72 'hello%s' % TermColors.Blue: r'hello{\color{blue}}',
72 'hello%s' % TermColors.Blue: r'hello{\color{blue}}',
73 'he%s%sllo' % (TermColors.Green, TermColors.Cyan) : r'he{\color{green}}{\color{cyan}llo}',
73 'he%s%sllo' % (TermColors.Green, TermColors.Cyan) : r'he{\color{green}}{\color{cyan}llo}',
74 '%shello' % TermColors.Yellow : r'{\color{yellow}hello}',
74 '%shello' % TermColors.Yellow : r'\textbf{\color{yellow}hello}',
75 '{0}h{0}e{0}l{0}l{0}o{0}'.format(TermColors.White) : r'{\color{white}h}{\color{white}e}{\color{white}l}{\color{white}l}{\color{white}o}{\color{white}}',
75 '{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}}',
76 'hel%slo' % TermColors.Green : r'hel{\color{green}lo}',
76 'hel%slo' % TermColors.Green : r'hel{\color{green}lo}',
77 'hello' : 'hello'}
77 'hello' : 'hello',
78 u'hello\x1b[34mthere\x1b[mworld' : u'hello{\\color{blue}there}world',
79 u'hello\x1b[mthere': u'hellothere'
80 }
78
81
79 for inval, outval in correct_outputs.items():
82 for inval, outval in correct_outputs.items():
80 self._try_ansi2latex(inval, outval)
83 self._try_ansi2latex(inval, outval)
81
84
82
85
83 def _try_ansi2latex(self, inval, outval):
86 def _try_ansi2latex(self, inval, outval):
84 self.fuzzy_compare(outval, ansi2latex(inval), case_sensitive=True)
87 self.fuzzy_compare(outval, ansi2latex(inval), case_sensitive=True)
General Comments 0
You need to be logged in to leave comments. Login now