pygments_highlighter.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. # Copyright (c) Jupyter Development Team.
  2. # Distributed under the terms of the Modified BSD License.
  3. from qtpy import QtGui
  4. from qtconsole.qstringhelpers import qstring_length
  5. from ipython_genutils.py3compat import PY3, string_types
  6. from pygments.formatters.html import HtmlFormatter
  7. from pygments.lexer import RegexLexer, _TokenType, Text, Error
  8. from pygments.lexers import PythonLexer, Python3Lexer
  9. from pygments.styles import get_style_by_name
  10. def get_tokens_unprocessed(self, text, stack=('root',)):
  11. """ Split ``text`` into (tokentype, text) pairs.
  12. Monkeypatched to store the final stack on the object itself.
  13. The `text` parameter this gets passed is only the current line, so to
  14. highlight things like multiline strings correctly, we need to retrieve
  15. the state from the previous line (this is done in PygmentsHighlighter,
  16. below), and use it to continue processing the current line.
  17. """
  18. pos = 0
  19. tokendefs = self._tokens
  20. if hasattr(self, '_saved_state_stack'):
  21. statestack = list(self._saved_state_stack)
  22. else:
  23. statestack = list(stack)
  24. statetokens = tokendefs[statestack[-1]]
  25. while 1:
  26. for rexmatch, action, new_state in statetokens:
  27. m = rexmatch(text, pos)
  28. if m:
  29. if action is not None:
  30. if type(action) is _TokenType:
  31. yield pos, action, m.group()
  32. else:
  33. for item in action(self, m):
  34. yield item
  35. pos = m.end()
  36. if new_state is not None:
  37. # state transition
  38. if isinstance(new_state, tuple):
  39. for state in new_state:
  40. if state == '#pop':
  41. statestack.pop()
  42. elif state == '#push':
  43. statestack.append(statestack[-1])
  44. else:
  45. statestack.append(state)
  46. elif isinstance(new_state, int):
  47. # pop
  48. del statestack[new_state:]
  49. elif new_state == '#push':
  50. statestack.append(statestack[-1])
  51. else:
  52. assert False, "wrong state def: %r" % new_state
  53. statetokens = tokendefs[statestack[-1]]
  54. break
  55. else:
  56. try:
  57. if text[pos] == '\n':
  58. # at EOL, reset state to "root"
  59. pos += 1
  60. statestack = ['root']
  61. statetokens = tokendefs['root']
  62. yield pos, Text, u'\n'
  63. continue
  64. yield pos, Error, text[pos]
  65. pos += 1
  66. except IndexError:
  67. break
  68. self._saved_state_stack = list(statestack)
  69. # Monkeypatch!
  70. RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
  71. class PygmentsBlockUserData(QtGui.QTextBlockUserData):
  72. """ Storage for the user data associated with each line.
  73. """
  74. syntax_stack = ('root',)
  75. def __init__(self, **kwds):
  76. for key, value in kwds.items():
  77. setattr(self, key, value)
  78. QtGui.QTextBlockUserData.__init__(self)
  79. def __repr__(self):
  80. attrs = ['syntax_stack']
  81. kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
  82. for attr in attrs ])
  83. return 'PygmentsBlockUserData(%s)' % kwds
  84. class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
  85. """ Syntax highlighter that uses Pygments for parsing. """
  86. #---------------------------------------------------------------------------
  87. # 'QSyntaxHighlighter' interface
  88. #---------------------------------------------------------------------------
  89. def __init__(self, parent, lexer=None):
  90. super(PygmentsHighlighter, self).__init__(parent)
  91. self._document = self.document()
  92. self._formatter = HtmlFormatter(nowrap=True)
  93. self.set_style('default')
  94. if lexer is not None:
  95. self._lexer = lexer
  96. else:
  97. if PY3:
  98. self._lexer = Python3Lexer()
  99. else:
  100. self._lexer = PythonLexer()
  101. def highlightBlock(self, string):
  102. """ Highlight a block of text.
  103. """
  104. prev_data = self.currentBlock().previous().userData()
  105. if prev_data is not None:
  106. self._lexer._saved_state_stack = prev_data.syntax_stack
  107. elif hasattr(self._lexer, '_saved_state_stack'):
  108. del self._lexer._saved_state_stack
  109. # Lex the text using Pygments
  110. index = 0
  111. for token, text in self._lexer.get_tokens(string):
  112. length = qstring_length(text)
  113. self.setFormat(index, length, self._get_format(token))
  114. index += length
  115. if hasattr(self._lexer, '_saved_state_stack'):
  116. data = PygmentsBlockUserData(
  117. syntax_stack=self._lexer._saved_state_stack)
  118. self.currentBlock().setUserData(data)
  119. # Clean up for the next go-round.
  120. del self._lexer._saved_state_stack
  121. #---------------------------------------------------------------------------
  122. # 'PygmentsHighlighter' interface
  123. #---------------------------------------------------------------------------
  124. def set_style(self, style):
  125. """ Sets the style to the specified Pygments style.
  126. """
  127. if isinstance(style, string_types):
  128. style = get_style_by_name(style)
  129. self._style = style
  130. self._clear_caches()
  131. def set_style_sheet(self, stylesheet):
  132. """ Sets a CSS stylesheet. The classes in the stylesheet should
  133. correspond to those generated by:
  134. pygmentize -S <style> -f html
  135. Note that 'set_style' and 'set_style_sheet' completely override each
  136. other, i.e. they cannot be used in conjunction.
  137. """
  138. self._document.setDefaultStyleSheet(stylesheet)
  139. self._style = None
  140. self._clear_caches()
  141. #---------------------------------------------------------------------------
  142. # Protected interface
  143. #---------------------------------------------------------------------------
  144. def _clear_caches(self):
  145. """ Clear caches for brushes and formats.
  146. """
  147. self._brushes = {}
  148. self._formats = {}
  149. def _get_format(self, token):
  150. """ Returns a QTextCharFormat for token or None.
  151. """
  152. if token in self._formats:
  153. return self._formats[token]
  154. if self._style is None:
  155. result = self._get_format_from_document(token, self._document)
  156. else:
  157. result = self._get_format_from_style(token, self._style)
  158. self._formats[token] = result
  159. return result
  160. def _get_format_from_document(self, token, document):
  161. """ Returns a QTextCharFormat for token by
  162. """
  163. code, html = next(self._formatter._format_lines([(token, u'dummy')]))
  164. self._document.setHtml(html)
  165. return QtGui.QTextCursor(self._document).charFormat()
  166. def _get_format_from_style(self, token, style):
  167. """ Returns a QTextCharFormat for token by reading a Pygments style.
  168. """
  169. result = QtGui.QTextCharFormat()
  170. for key, value in style.style_for_token(token).items():
  171. if value:
  172. if key == 'color':
  173. result.setForeground(self._get_brush(value))
  174. elif key == 'bgcolor':
  175. result.setBackground(self._get_brush(value))
  176. elif key == 'bold':
  177. result.setFontWeight(QtGui.QFont.Bold)
  178. elif key == 'italic':
  179. result.setFontItalic(True)
  180. elif key == 'underline':
  181. result.setUnderlineStyle(
  182. QtGui.QTextCharFormat.SingleUnderline)
  183. elif key == 'sans':
  184. result.setFontStyleHint(QtGui.QFont.SansSerif)
  185. elif key == 'roman':
  186. result.setFontStyleHint(QtGui.QFont.Times)
  187. elif key == 'mono':
  188. result.setFontStyleHint(QtGui.QFont.TypeWriter)
  189. return result
  190. def _get_brush(self, color):
  191. """ Returns a brush for the color.
  192. """
  193. result = self._brushes.get(color)
  194. if result is None:
  195. qcolor = self._get_color(color)
  196. result = QtGui.QBrush(qcolor)
  197. self._brushes[color] = result
  198. return result
  199. def _get_color(self, color):
  200. """ Returns a QColor built from a Pygments color string.
  201. """
  202. qcolor = QtGui.QColor()
  203. qcolor.setRgb(int(color[:2], base=16),
  204. int(color[2:4], base=16),
  205. int(color[4:6], base=16))
  206. return qcolor