pygments_highlighter.py
232 lines
| 8.5 KiB
| text/x-python
|
PythonLexer
epatters
|
r2603 | # System library imports. | ||
Evan Patterson
|
r3304 | from IPython.external.qt import QtGui | ||
epatters
|
r2728 | from pygments.formatters.html import HtmlFormatter | ||
epatters
|
r2602 | from pygments.lexer import RegexLexer, _TokenType, Text, Error | ||
epatters
|
r2715 | from pygments.lexers import PythonLexer | ||
epatters
|
r2728 | from pygments.styles import get_style_by_name | ||
epatters
|
r2602 | |||
Thomas Kluyver
|
r13353 | # Local imports | ||
from IPython.utils.py3compat import string_types | ||||
epatters
|
r2602 | |||
def get_tokens_unprocessed(self, text, stack=('root',)): | ||||
""" Split ``text`` into (tokentype, text) pairs. | ||||
Monkeypatched to store the final stack on the object itself. | ||||
Thomas Kluyver
|
r18720 | |||
The `text` parameter this gets passed is only the current line, so to | ||||
highlight things like multiline strings correctly, we need to retrieve | ||||
the state from the previous line (this is done in PygmentsHighlighter, | ||||
below), and use it to continue processing the current line. | ||||
epatters
|
r2602 | """ | ||
pos = 0 | ||||
tokendefs = self._tokens | ||||
epatters
|
r2603 | if hasattr(self, '_saved_state_stack'): | ||
statestack = list(self._saved_state_stack) | ||||
epatters
|
r2602 | else: | ||
statestack = list(stack) | ||||
statetokens = tokendefs[statestack[-1]] | ||||
while 1: | ||||
for rexmatch, action, new_state in statetokens: | ||||
m = rexmatch(text, pos) | ||||
if m: | ||||
Carlos Cordoba
|
r18719 | if action is not None: | ||
if type(action) is _TokenType: | ||||
yield pos, action, m.group() | ||||
else: | ||||
for item in action(self, m): | ||||
yield item | ||||
epatters
|
r2602 | pos = m.end() | ||
if new_state is not None: | ||||
# state transition | ||||
if isinstance(new_state, tuple): | ||||
for state in new_state: | ||||
if state == '#pop': | ||||
statestack.pop() | ||||
elif state == '#push': | ||||
statestack.append(statestack[-1]) | ||||
else: | ||||
statestack.append(state) | ||||
elif isinstance(new_state, int): | ||||
# pop | ||||
del statestack[new_state:] | ||||
elif new_state == '#push': | ||||
statestack.append(statestack[-1]) | ||||
else: | ||||
assert False, "wrong state def: %r" % new_state | ||||
statetokens = tokendefs[statestack[-1]] | ||||
break | ||||
else: | ||||
try: | ||||
if text[pos] == '\n': | ||||
# at EOL, reset state to "root" | ||||
pos += 1 | ||||
statestack = ['root'] | ||||
statetokens = tokendefs['root'] | ||||
yield pos, Text, u'\n' | ||||
continue | ||||
yield pos, Error, text[pos] | ||||
pos += 1 | ||||
except IndexError: | ||||
break | ||||
epatters
|
r2603 | self._saved_state_stack = list(statestack) | ||
epatters
|
r2602 | |||
# Monkeypatch! | ||||
RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed | ||||
epatters
|
r2725 | class PygmentsBlockUserData(QtGui.QTextBlockUserData): | ||
epatters
|
r2602 | """ Storage for the user data associated with each line. | ||
""" | ||||
syntax_stack = ('root',) | ||||
def __init__(self, **kwds): | ||||
Thomas Kluyver
|
r13361 | for key, value in kwds.items(): | ||
epatters
|
r2602 | setattr(self, key, value) | ||
QtGui.QTextBlockUserData.__init__(self) | ||||
def __repr__(self): | ||||
attrs = ['syntax_stack'] | ||||
Bernardo B. Marques
|
r4872 | kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr)) | ||
epatters
|
r2602 | for attr in attrs ]) | ||
epatters
|
r2725 | return 'PygmentsBlockUserData(%s)' % kwds | ||
epatters
|
r2602 | |||
class PygmentsHighlighter(QtGui.QSyntaxHighlighter): | ||||
""" Syntax highlighter that uses Pygments for parsing. """ | ||||
epatters
|
r2728 | #--------------------------------------------------------------------------- | ||
# 'QSyntaxHighlighter' interface | ||||
#--------------------------------------------------------------------------- | ||||
epatters
|
r2602 | def __init__(self, parent, lexer=None): | ||
super(PygmentsHighlighter, self).__init__(parent) | ||||
MinRK
|
r10082 | self._document = self.document() | ||
epatters
|
r2728 | self._formatter = HtmlFormatter(nowrap=True) | ||
epatters
|
r2602 | self._lexer = lexer if lexer else PythonLexer() | ||
epatters
|
r2728 | self.set_style('default') | ||
epatters
|
r2602 | |||
Evan Patterson
|
r3304 | def highlightBlock(self, string): | ||
epatters
|
r2602 | """ Highlight a block of text. | ||
""" | ||||
epatters
|
r2728 | prev_data = self.currentBlock().previous().userData() | ||
epatters
|
r2602 | if prev_data is not None: | ||
epatters
|
r2603 | self._lexer._saved_state_stack = prev_data.syntax_stack | ||
elif hasattr(self._lexer, '_saved_state_stack'): | ||||
del self._lexer._saved_state_stack | ||||
epatters
|
r2602 | |||
# Lex the text using Pygments | ||||
epatters
|
r2894 | index = 0 | ||
Evan Patterson
|
r3304 | for token, text in self._lexer.get_tokens(string): | ||
epatters
|
r2894 | length = len(text) | ||
self.setFormat(index, length, self._get_format(token)) | ||||
index += length | ||||
epatters
|
r2602 | |||
epatters
|
r2603 | if hasattr(self._lexer, '_saved_state_stack'): | ||
epatters
|
r2725 | data = PygmentsBlockUserData( | ||
syntax_stack=self._lexer._saved_state_stack) | ||||
epatters
|
r2602 | self.currentBlock().setUserData(data) | ||
# Clean up for the next go-round. | ||||
epatters
|
r2603 | del self._lexer._saved_state_stack | ||
epatters
|
r2602 | |||
epatters
|
r2728 | #--------------------------------------------------------------------------- | ||
# 'PygmentsHighlighter' interface | ||||
#--------------------------------------------------------------------------- | ||||
def set_style(self, style): | ||||
""" Sets the style to the specified Pygments style. | ||||
""" | ||||
Thomas Kluyver
|
r13353 | if isinstance(style, string_types): | ||
epatters
|
r2728 | style = get_style_by_name(style) | ||
self._style = style | ||||
self._clear_caches() | ||||
def set_style_sheet(self, stylesheet): | ||||
""" Sets a CSS stylesheet. The classes in the stylesheet should | ||||
correspond to those generated by: | ||||
pygmentize -S <style> -f html | ||||
Note that 'set_style' and 'set_style_sheet' completely override each | ||||
other, i.e. they cannot be used in conjunction. | ||||
""" | ||||
self._document.setDefaultStyleSheet(stylesheet) | ||||
self._style = None | ||||
self._clear_caches() | ||||
#--------------------------------------------------------------------------- | ||||
# Protected interface | ||||
#--------------------------------------------------------------------------- | ||||
def _clear_caches(self): | ||||
""" Clear caches for brushes and formats. | ||||
epatters
|
r2602 | """ | ||
epatters
|
r2728 | self._brushes = {} | ||
self._formats = {} | ||||
epatters
|
r2602 | |||
def _get_format(self, token): | ||||
""" Returns a QTextCharFormat for token or None. | ||||
""" | ||||
if token in self._formats: | ||||
return self._formats[token] | ||||
epatters
|
r2728 | |||
if self._style is None: | ||||
result = self._get_format_from_document(token, self._document) | ||||
else: | ||||
result = self._get_format_from_style(token, self._style) | ||||
self._formats[token] = result | ||||
return result | ||||
def _get_format_from_document(self, token, document): | ||||
Bernardo B. Marques
|
r4872 | """ Returns a QTextCharFormat for token by | ||
epatters
|
r2728 | """ | ||
Bradley M. Froehle
|
r7847 | code, html = next(self._formatter._format_lines([(token, u'dummy')])) | ||
epatters
|
r2728 | self._document.setHtml(html) | ||
return QtGui.QTextCursor(self._document).charFormat() | ||||
def _get_format_from_style(self, token, style): | ||||
""" Returns a QTextCharFormat for token by reading a Pygments style. | ||||
""" | ||||
epatters
|
r2894 | result = QtGui.QTextCharFormat() | ||
epatters
|
r2728 | for key, value in style.style_for_token(token).items(): | ||
epatters
|
r2602 | if value: | ||
if key == 'color': | ||||
result.setForeground(self._get_brush(value)) | ||||
elif key == 'bgcolor': | ||||
result.setBackground(self._get_brush(value)) | ||||
elif key == 'bold': | ||||
result.setFontWeight(QtGui.QFont.Bold) | ||||
elif key == 'italic': | ||||
result.setFontItalic(True) | ||||
elif key == 'underline': | ||||
result.setUnderlineStyle( | ||||
QtGui.QTextCharFormat.SingleUnderline) | ||||
elif key == 'sans': | ||||
result.setFontStyleHint(QtGui.QFont.SansSerif) | ||||
elif key == 'roman': | ||||
result.setFontStyleHint(QtGui.QFont.Times) | ||||
elif key == 'mono': | ||||
result.setFontStyleHint(QtGui.QFont.TypeWriter) | ||||
return result | ||||
def _get_brush(self, color): | ||||
""" Returns a brush for the color. | ||||
""" | ||||
result = self._brushes.get(color) | ||||
if result is None: | ||||
qcolor = self._get_color(color) | ||||
result = QtGui.QBrush(qcolor) | ||||
self._brushes[color] = result | ||||
return result | ||||
def _get_color(self, color): | ||||
epatters
|
r2725 | """ Returns a QColor built from a Pygments color string. | ||
""" | ||||
epatters
|
r2602 | qcolor = QtGui.QColor() | ||
epatters
|
r2715 | qcolor.setRgb(int(color[:2], base=16), | ||
epatters
|
r2602 | int(color[2:4], base=16), | ||
int(color[4:6], base=16)) | ||||
return qcolor | ||||