# -*- coding: utf-8 -*- """ sphinx.highlighting ~~~~~~~~~~~~~~~~~~~ Highlight code blocks using Pygments. :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import sys import cgi import re import parser from sphinx.util.texescape import tex_hl_escape_map try: import pygments from pygments import highlight from pygments.lexers import PythonLexer, PythonConsoleLexer, CLexer, \ TextLexer, RstLexer from pygments.lexers import get_lexer_by_name, guess_lexer from pygments.formatters import HtmlFormatter, LatexFormatter from pygments.filters import ErrorToken from pygments.style import Style from pygments.styles import get_style_by_name from pygments.styles.friendly import FriendlyStyle from pygments.token import Generic, Comment, Number except ImportError: pygments = None else: class SphinxStyle(Style): """ Like friendly, but a bit darker to enhance contrast on the green background. """ background_color = '#eeffcc' default_style = '' styles = FriendlyStyle.styles styles.update({ Generic.Output: '#333', Comment: 'italic #408090', Number: '#208050', }) lexers = dict( none = TextLexer(), python = PythonLexer(), pycon = PythonConsoleLexer(), # the python3 option exists as of Pygments 0.12, but it doesn't # do any harm in previous versions pycon3 = PythonConsoleLexer(python3=True), rest = RstLexer(), c = CLexer(), ) for _lexer in lexers.values(): _lexer.add_filter('raiseonerror') escape_hl_chars = {ord(u'@'): u'@PYGZat[]', ord(u'['): u'@PYGZlb[]', ord(u']'): u'@PYGZrb[]'} # used if Pygments is not available _LATEX_STYLES = r''' \newcommand\PYGZat{@} \newcommand\PYGZlb{[} \newcommand\PYGZrb{]} ''' parsing_exceptions = (SyntaxError, UnicodeEncodeError) if sys.version_info < (2, 5): # Python <= 2.4 raises MemoryError when parsing an # invalid encoding cookie parsing_exceptions += MemoryError, class PygmentsBridge(object): def __init__(self, dest='html', stylename='sphinx'): self.dest = dest if not pygments: return if stylename == 'sphinx': style = SphinxStyle elif '.' in stylename: module, stylename = stylename.rsplit('.', 1) style = getattr(__import__(module, None, None, ['']), stylename) else: style = get_style_by_name(stylename) self.hfmter = {False: HtmlFormatter(style=style), True: HtmlFormatter(style=style, linenos=True)} self.lfmter = {False: LatexFormatter(style=style, commandprefix='PYG'), True: LatexFormatter(style=style, linenos=True, commandprefix='PYG')} def unhighlighted(self, source): if self.dest == 'html': return '
' + cgi.escape(source) + '
\n' else: # first, escape highlighting characters like Pygments does source = source.translate(escape_hl_chars) # then, escape all characters nonrepresentable in LaTeX source = source.translate(tex_hl_escape_map) return '\\begin{Verbatim}[commandchars=@\\[\\]]\n' + \ source + '\\end{Verbatim}\n' def try_parse(self, src): # Make sure it ends in a newline src += '\n' # Replace "..." by a mark which is also a valid python expression # (Note, the highlighter gets the original source, this is only done # to allow "..." in code and still highlight it as Python code.) mark = "__highlighting__ellipsis__" src = src.replace("...", mark) # lines beginning with "..." are probably placeholders for suite src = re.sub(r"(?m)^(\s*)" + mark + "(.)", r"\1"+ mark + r"# \2", src) # if we're using 2.5, use the with statement if sys.version_info >= (2, 5): src = 'from __future__ import with_statement\n' + src if isinstance(src, unicode): # Non-ASCII chars will only occur in string literals # and comments. If we wanted to give them to the parser # correctly, we'd have to find out the correct source # encoding. Since it may not even be given in a snippet, # just replace all non-ASCII characters. src = src.encode('ascii', 'replace') try: parser.suite(src) except parsing_exceptions: return False else: return True def highlight_block(self, source, lang, linenos=False): if not pygments: return self.unhighlighted(source) if lang in ('py', 'python'): if source.startswith('>>>'): # interactive session lexer = lexers['pycon'] else: # maybe Python -- try parsing it if self.try_parse(source): lexer = lexers['python'] else: return self.unhighlighted(source) elif lang in ('python3', 'py3') and source.startswith('>>>'): # for py3, recognize interactive sessions, but do not try parsing... lexer = lexers['pycon3'] elif lang == 'guess': try: lexer = guess_lexer(source) except Exception: return self.unhighlighted(source) else: if lang in lexers: lexer = lexers[lang] else: lexer = lexers[lang] = get_lexer_by_name(lang) lexer.add_filter('raiseonerror') try: if self.dest == 'html': return highlight(source, lexer, self.hfmter[bool(linenos)]) else: hlsource = highlight(source, lexer, self.lfmter[bool(linenos)]) return hlsource.translate(tex_hl_escape_map) except ErrorToken: # this is most probably not the selected language, # so let it pass unhighlighted return self.unhighlighted(source) def get_stylesheet(self): if not pygments: if self.dest == 'latex': return _LATEX_STYLES # no HTML styles needed return '' if self.dest == 'html': return self.hfmter[0].get_style_defs() else: styledefs = self.lfmter[0].get_style_defs() # workaround for Pygments < 0.12 if styledefs.startswith('\\newcommand\\at{@}'): styledefs += _LATEX_STYLES return styledefs