|
1 # -*- coding: utf-8 -*- |
|
2 """ |
|
3 sphinx.highlighting |
|
4 ~~~~~~~~~~~~~~~~~~~ |
|
5 |
|
6 Highlight code blocks using Pygments. |
|
7 |
|
8 :copyright: 2007-2008 by Georg Brandl. |
|
9 :license: BSD. |
|
10 """ |
|
11 |
|
12 import sys |
|
13 import cgi |
|
14 import re |
|
15 import parser |
|
16 |
|
17 from sphinx.util.texescape import tex_hl_escape_map |
|
18 |
|
19 try: |
|
20 import pygments |
|
21 from pygments import highlight |
|
22 from pygments.lexers import PythonLexer, PythonConsoleLexer, CLexer, \ |
|
23 TextLexer, RstLexer |
|
24 from pygments.lexers import get_lexer_by_name, guess_lexer |
|
25 from pygments.formatters import HtmlFormatter, LatexFormatter |
|
26 from pygments.filters import ErrorToken |
|
27 from pygments.style import Style |
|
28 from pygments.styles import get_style_by_name |
|
29 from pygments.styles.friendly import FriendlyStyle |
|
30 from pygments.token import Generic, Comment, Number |
|
31 except ImportError: |
|
32 pygments = None |
|
33 else: |
|
34 class SphinxStyle(Style): |
|
35 """ |
|
36 Like friendly, but a bit darker to enhance contrast on the green |
|
37 background. |
|
38 """ |
|
39 |
|
40 background_color = '#eeffcc' |
|
41 default_style = '' |
|
42 |
|
43 styles = FriendlyStyle.styles |
|
44 styles.update({ |
|
45 Generic.Output: '#333', |
|
46 Comment: 'italic #408090', |
|
47 Number: '#208050', |
|
48 }) |
|
49 |
|
50 lexers = dict( |
|
51 none = TextLexer(), |
|
52 python = PythonLexer(), |
|
53 pycon = PythonConsoleLexer(), |
|
54 # the python3 option exists as of Pygments 0.12, but it doesn't |
|
55 # do any harm in previous versions |
|
56 pycon3 = PythonConsoleLexer(python3=True), |
|
57 rest = RstLexer(), |
|
58 c = CLexer(), |
|
59 ) |
|
60 for _lexer in lexers.values(): |
|
61 _lexer.add_filter('raiseonerror') |
|
62 |
|
63 |
|
64 escape_hl_chars = {ord(u'@'): u'@PYGZat[]', |
|
65 ord(u'['): u'@PYGZlb[]', |
|
66 ord(u']'): u'@PYGZrb[]'} |
|
67 |
|
68 # used if Pygments is not available |
|
69 _LATEX_STYLES = r''' |
|
70 \newcommand\PYGZat{@} |
|
71 \newcommand\PYGZlb{[} |
|
72 \newcommand\PYGZrb{]} |
|
73 ''' |
|
74 |
|
75 |
|
76 parsing_exceptions = (SyntaxError, UnicodeEncodeError) |
|
77 if sys.version_info < (2, 5): |
|
78 # Python <= 2.4 raises MemoryError when parsing an |
|
79 # invalid encoding cookie |
|
80 parsing_exceptions += MemoryError, |
|
81 |
|
82 |
|
83 class PygmentsBridge(object): |
|
84 def __init__(self, dest='html', stylename='sphinx'): |
|
85 self.dest = dest |
|
86 if not pygments: |
|
87 return |
|
88 if stylename == 'sphinx': |
|
89 style = SphinxStyle |
|
90 elif '.' in stylename: |
|
91 module, stylename = stylename.rsplit('.', 1) |
|
92 style = getattr(__import__(module, None, None, ['']), stylename) |
|
93 else: |
|
94 style = get_style_by_name(stylename) |
|
95 self.hfmter = {False: HtmlFormatter(style=style), |
|
96 True: HtmlFormatter(style=style, linenos=True)} |
|
97 self.lfmter = {False: LatexFormatter(style=style, commandprefix='PYG'), |
|
98 True: LatexFormatter(style=style, linenos=True, |
|
99 commandprefix='PYG')} |
|
100 |
|
101 def unhighlighted(self, source): |
|
102 if self.dest == 'html': |
|
103 return '<pre>' + cgi.escape(source) + '</pre>\n' |
|
104 else: |
|
105 # first, escape highlighting characters like Pygments does |
|
106 source = source.translate(escape_hl_chars) |
|
107 # then, escape all characters nonrepresentable in LaTeX |
|
108 source = source.translate(tex_hl_escape_map) |
|
109 return '\\begin{Verbatim}[commandchars=@\\[\\]]\n' + \ |
|
110 source + '\\end{Verbatim}\n' |
|
111 |
|
112 def try_parse(self, src): |
|
113 # Make sure it ends in a newline |
|
114 src += '\n' |
|
115 |
|
116 # Replace "..." by a mark which is also a valid python expression |
|
117 # (Note, the highlighter gets the original source, this is only done |
|
118 # to allow "..." in code and still highlight it as Python code.) |
|
119 mark = "__highlighting__ellipsis__" |
|
120 src = src.replace("...", mark) |
|
121 |
|
122 # lines beginning with "..." are probably placeholders for suite |
|
123 src = re.sub(r"(?m)^(\s*)" + mark + "(.)", r"\1"+ mark + r"# \2", src) |
|
124 |
|
125 # if we're using 2.5, use the with statement |
|
126 if sys.version_info >= (2, 5): |
|
127 src = 'from __future__ import with_statement\n' + src |
|
128 |
|
129 if isinstance(src, unicode): |
|
130 # Non-ASCII chars will only occur in string literals |
|
131 # and comments. If we wanted to give them to the parser |
|
132 # correctly, we'd have to find out the correct source |
|
133 # encoding. Since it may not even be given in a snippet, |
|
134 # just replace all non-ASCII characters. |
|
135 src = src.encode('ascii', 'replace') |
|
136 |
|
137 try: |
|
138 parser.suite(src) |
|
139 except parsing_exceptions: |
|
140 return False |
|
141 else: |
|
142 return True |
|
143 |
|
144 def highlight_block(self, source, lang, linenos=False): |
|
145 if not pygments: |
|
146 return self.unhighlighted(source) |
|
147 if lang in ('py', 'python'): |
|
148 if source.startswith('>>>'): |
|
149 # interactive session |
|
150 lexer = lexers['pycon'] |
|
151 else: |
|
152 # maybe Python -- try parsing it |
|
153 if self.try_parse(source): |
|
154 lexer = lexers['python'] |
|
155 else: |
|
156 return self.unhighlighted(source) |
|
157 elif lang in ('python3', 'py3') and source.startswith('>>>'): |
|
158 # for py3, recognize interactive sessions, but do not try parsing... |
|
159 lexer = lexers['pycon3'] |
|
160 elif lang == 'guess': |
|
161 try: |
|
162 lexer = guess_lexer(source) |
|
163 except Exception: |
|
164 return self.unhighlighted(source) |
|
165 else: |
|
166 if lang in lexers: |
|
167 lexer = lexers[lang] |
|
168 else: |
|
169 lexer = lexers[lang] = get_lexer_by_name(lang) |
|
170 lexer.add_filter('raiseonerror') |
|
171 try: |
|
172 if self.dest == 'html': |
|
173 return highlight(source, lexer, self.hfmter[bool(linenos)]) |
|
174 else: |
|
175 hlsource = highlight(source, lexer, self.lfmter[bool(linenos)]) |
|
176 return hlsource.translate(tex_hl_escape_map) |
|
177 except ErrorToken: |
|
178 # this is most probably not the selected language, |
|
179 # so let it pass unhighlighted |
|
180 return self.unhighlighted(source) |
|
181 |
|
182 def get_stylesheet(self): |
|
183 if not pygments: |
|
184 if self.dest == 'latex': |
|
185 return _LATEX_STYLES |
|
186 # no HTML styles needed |
|
187 return '' |
|
188 if self.dest == 'html': |
|
189 return self.hfmter[0].get_style_defs() |
|
190 else: |
|
191 styledefs = self.lfmter[0].get_style_defs() |
|
192 # workaround for Pygments < 0.12 |
|
193 if styledefs.startswith('\\newcommand\\at{@}'): |
|
194 styledefs += _LATEX_STYLES |
|
195 return styledefs |