You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
164 lines
5.7 KiB
164 lines
5.7 KiB
"""
|
|
sphinx.highlighting
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
Highlight code blocks using Pygments.
|
|
|
|
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
|
|
:license: BSD, see LICENSE for details.
|
|
"""
|
|
|
|
from functools import partial
|
|
from importlib import import_module
|
|
from typing import Any, Dict
|
|
|
|
from pygments import highlight
|
|
from pygments.filters import ErrorToken
|
|
from pygments.formatter import Formatter
|
|
from pygments.formatters import HtmlFormatter, LatexFormatter
|
|
from pygments.lexer import Lexer
|
|
from pygments.lexers import (CLexer, Python3Lexer, PythonConsoleLexer, PythonLexer, RstLexer,
|
|
TextLexer, get_lexer_by_name, guess_lexer)
|
|
from pygments.style import Style
|
|
from pygments.styles import get_style_by_name
|
|
from pygments.util import ClassNotFound
|
|
|
|
from sphinx.locale import __
|
|
from sphinx.pygments_styles import NoneStyle, SphinxStyle
|
|
from sphinx.util import logging, texescape
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
lexers = {} # type: Dict[str, Lexer]
|
|
lexer_classes = {
|
|
'none': partial(TextLexer, stripnl=False),
|
|
'python': partial(PythonLexer, stripnl=False),
|
|
'python3': partial(Python3Lexer, stripnl=False),
|
|
'pycon': partial(PythonConsoleLexer, stripnl=False),
|
|
'pycon3': partial(PythonConsoleLexer, python3=True, stripnl=False),
|
|
'rest': partial(RstLexer, stripnl=False),
|
|
'c': partial(CLexer, stripnl=False),
|
|
} # type: Dict[str, Lexer]
|
|
|
|
|
|
escape_hl_chars = {ord('\\'): '\\PYGZbs{}',
|
|
ord('{'): '\\PYGZob{}',
|
|
ord('}'): '\\PYGZcb{}'}
|
|
|
|
# used if Pygments is available
|
|
# use textcomp quote to get a true single quote
|
|
_LATEX_ADD_STYLES = r'''
|
|
\renewcommand\PYGZsq{\textquotesingle}
|
|
'''
|
|
|
|
|
|
class PygmentsBridge:
|
|
# Set these attributes if you want to have different Pygments formatters
|
|
# than the default ones.
|
|
html_formatter = HtmlFormatter
|
|
latex_formatter = LatexFormatter
|
|
|
|
def __init__(self, dest: str = 'html', stylename: str = 'sphinx',
|
|
latex_engine: str = None) -> None:
|
|
self.dest = dest
|
|
self.latex_engine = latex_engine
|
|
|
|
style = self.get_style(stylename)
|
|
self.formatter_args = {'style': style} # type: Dict[str, Any]
|
|
if dest == 'html':
|
|
self.formatter = self.html_formatter
|
|
else:
|
|
self.formatter = self.latex_formatter
|
|
self.formatter_args['commandprefix'] = 'PYG'
|
|
|
|
def get_style(self, stylename: str) -> Style:
|
|
if stylename is None or stylename == 'sphinx':
|
|
return SphinxStyle
|
|
elif stylename == 'none':
|
|
return NoneStyle
|
|
elif '.' in stylename:
|
|
module, stylename = stylename.rsplit('.', 1)
|
|
return getattr(import_module(module), stylename)
|
|
else:
|
|
return get_style_by_name(stylename)
|
|
|
|
def get_formatter(self, **kwargs: Any) -> Formatter:
|
|
kwargs.update(self.formatter_args)
|
|
return self.formatter(**kwargs)
|
|
|
|
def get_lexer(self, source: str, lang: str, opts: Dict = None,
|
|
force: bool = False, location: Any = None) -> Lexer:
|
|
if not opts:
|
|
opts = {}
|
|
|
|
# find out which lexer to use
|
|
if lang in ('py', 'python'):
|
|
if source.startswith('>>>'):
|
|
# interactive session
|
|
lang = 'pycon'
|
|
else:
|
|
lang = 'python'
|
|
elif lang in ('py3', 'python3', 'default'):
|
|
if source.startswith('>>>'):
|
|
lang = 'pycon3'
|
|
else:
|
|
lang = 'python3'
|
|
|
|
if lang in lexers:
|
|
# just return custom lexers here (without installing raiseonerror filter)
|
|
return lexers[lang]
|
|
elif lang in lexer_classes:
|
|
lexer = lexer_classes[lang](**opts)
|
|
else:
|
|
try:
|
|
if lang == 'guess':
|
|
lexer = guess_lexer(source, **opts)
|
|
else:
|
|
lexer = get_lexer_by_name(lang, **opts)
|
|
except ClassNotFound:
|
|
logger.warning(__('Pygments lexer name %r is not known'), lang,
|
|
location=location)
|
|
lexer = lexer_classes['none'](**opts)
|
|
|
|
if not force:
|
|
lexer.add_filter('raiseonerror')
|
|
|
|
return lexer
|
|
|
|
def highlight_block(self, source: str, lang: str, opts: Dict = None,
|
|
force: bool = False, location: Any = None, **kwargs: Any) -> str:
|
|
if not isinstance(source, str):
|
|
source = source.decode()
|
|
|
|
lexer = self.get_lexer(source, lang, opts, force, location)
|
|
|
|
# highlight via Pygments
|
|
formatter = self.get_formatter(**kwargs)
|
|
try:
|
|
hlsource = highlight(source, lexer, formatter)
|
|
except ErrorToken:
|
|
# this is most probably not the selected language,
|
|
# so let it pass unhighlighted
|
|
if lang == 'default':
|
|
pass # automatic highlighting failed.
|
|
else:
|
|
logger.warning(__('Could not lex literal_block as "%s". '
|
|
'Highlighting skipped.'), lang,
|
|
type='misc', subtype='highlighting_failure',
|
|
location=location)
|
|
lexer = self.get_lexer(source, 'none', opts, force, location)
|
|
hlsource = highlight(source, lexer, formatter)
|
|
|
|
if self.dest == 'html':
|
|
return hlsource
|
|
else:
|
|
# MEMO: this is done to escape Unicode chars with non-Unicode engines
|
|
return texescape.hlescape(hlsource, self.latex_engine)
|
|
|
|
def get_stylesheet(self) -> str:
|
|
formatter = self.get_formatter()
|
|
if self.dest == 'html':
|
|
return formatter.get_style_defs('.highlight')
|
|
else:
|
|
return formatter.get_style_defs() + _LATEX_ADD_STYLES
|