11from __future__ import absolute_import
22
33import json
4+ import keyword
45import os
56import re
67
1011
1112
1213class CodeHighlighter (QSyntaxHighlighter ):
13- def __init__ (self , widget ):
14+
15+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
16+ # # # INITIALIZATION # # #
17+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
18+
19+ def __init__ (self , widget , language ):
1420 super (CodeHighlighter , self ).__init__ (widget )
21+ self ._consoleMode = False
22+
23+ self .initHighlightVariables ()
24+ self .setLanguage (language )
25+
26+ self .defineHighlightVariables ()
27+
28+ def initHighlightVariables (self ):
29+ """Initialize the variables which will be used in code highlighting"""
30+
31+ # For each call of highlightBlock, keep track of the spans of each highlight, so
32+ # we can prevent overlapping spans (ie if a string is found, but it's within a
33+ # comment, do not highlight it).
34+ self .spans = []
1535
16- # setup the search rules
36+ # Language specific lists
37+ self ._comments = []
1738 self ._keywords = []
1839 self ._strings = []
19- self ._comments = []
20- self ._consoleMode = False
21- # color storage
40+
41+ # Patterns
42+ self ._commentPattern = None
43+ self ._keywordPattern = None
44+ self ._resultPattern = None
45+ self ._stringsPattern = None
46+
47+ # Formats
48+ self ._commentFormat = None
49+ self ._keywordFormat = None
50+ self ._resultFormat = None
51+ self ._stringFormat = None
52+
53+ # Colors. These may be overriden by parent colors, which themselves my be
54+ # overridden by stylesheets (ie Bright.css)
2255 self ._commentColor = QColor (0 , 206 , 52 )
23- self ._keywordColor = QColor (17 , 154 , 255 )
24- self ._stringColor = QColor (255 , 128 , 0 )
56+ self ._keywordColor = QColor (255 , 0 , 255 )
2557 self ._resultColor = QColor (125 , 128 , 128 )
58+ self ._stringColor = QColor (255 , 128 , 0 )
2659
27- # setup the font
28- font = widget .font ()
29- font .setFamily ('Courier New' )
30- widget .setFont (font )
31-
32- def commentColor (self ):
33- # pull the color from the parent if possible because this doesn't support
34- # stylesheets
35- parent = self .parent ()
36- if parent and hasattr (parent , 'commentColor' ):
37- return parent .commentColor
38- return self ._commentColor
60+ def setLanguage (self , lang ):
61+ """Sets the language of the highlighter by loading the json definition"""
62+ filename = resourcePath ('lang/%s.json' % lang .lower ())
63+ if os .path .exists (filename ):
64+ data = json .load (open (filename ))
65+ self .setObjectName (data .get ('name' , '' ))
3966
40- def setCommentColor (self , color ):
41- # set the color for the parent if possible because this doesn't support
42- # stylesheets
43- parent = self .parent ()
44- if parent and hasattr (parent , 'commentColor' ):
45- parent .commentColor = color
46- self ._commentColor = color
67+ self ._comments = data .get ('comments' , [])
68+ self ._strings = data .get ('strings' , [])
4769
48- def commentFormat (self ):
49- """returns the comments QTextCharFormat for this highlighter"""
50- format = QTextCharFormat ()
51- format .setForeground (self .commentColor ())
52- format .setFontItalic (True )
70+ # If using python, we can get keywords dynamically, otherwise get them from
71+ # the language json data.
72+ if lang .lower () == "python" :
73+ self ._keywords = keyword .kwlist
74+ else :
75+ self ._keywords = data .get ('keywords' , [])
5376
54- return format
77+ return True
78+ return False
5579
56- def isConsoleMode (self ):
57- """checks to see if this highlighter is in console mode"""
58- return self ._consoleMode
80+ def defineHighlightVariables (self ):
81+ """Define the formats and regex patterns which will be used to highlight
82+ code."""
83+ # Define highlight formats
84+ self .defineCommentFormat ()
85+ self .defineKeywordFormat ()
86+ self .defineResultFormat ()
87+ self .defineStringFormat ()
88+
89+ # Define highlight regex patterns
90+ self .defineCommentPattern ()
91+ self .defineKeywordPattern ()
92+ self .defineResultPattern ()
93+ self .defineStringPattern ()
94+
95+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
96+ # # # PROCESSING # # #
97+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
5998
6099 def highlightBlock (self , text ):
61- """highlights the inputed text block based on the rules of this code
100+ """Highlights the inputed text block based on the rules of this code
62101 highlighter"""
63- if not self .isConsoleMode () or str (text ).startswith ('>>>' ):
64- # format the result lines
65- format = self .resultFormat ()
66- parent = self .parent ()
67- if parent and hasattr (parent , 'outputPrompt' ):
68- self .highlightText (
69- text ,
70- re .compile ('%s[^\\ n]*' % re .escape (parent .outputPrompt ())),
71- format ,
72- )
73102
74- # format the keywords
75- format = self .keywordFormat ()
76- for kwd in self ._keywords :
77- self .highlightText (text , re .compile (r'\b%s\b' % kwd ), format )
103+ # Reset the highlight spans for this text block
104+ self .spans = []
78105
79- # format the strings
80- format = self .stringFormat ()
106+ if not self .isConsoleMode () or str (text ).startswith ('>>>' ):
81107
82- for string in self ._strings :
108+ # We only have a result pattern if the parent has an attr "outputPrompt", so
109+ # only proceed if we have been able to define self._resultPattern.
110+ if self ._resultPattern :
83111 self .highlightText (
84112 text ,
85- re . compile ( '{s}[^{s}]*{s}' . format ( s = string )) ,
86- format ,
113+ self . _resultPattern ,
114+ self . _resultFormat ,
87115 )
88116
89- # format the comments
90- format = self .commentFormat ()
91- for comment in self ._comments :
92- self .highlightText (text , re .compile (comment ), format )
117+ # Format the strings
118+ self .highlightText (
119+ text ,
120+ self ._stringPattern ,
121+ self ._stringFormat ,
122+ )
93123
94- def highlightText (self , text , expr , format , offset = 0 , includeLast = False ):
124+ # format the comments
125+ self .highlightText (
126+ text ,
127+ self ._commentPattern ,
128+ self ._commentFormat ,
129+ )
130+
131+ # Format the keywords
132+ self .highlightText (
133+ text ,
134+ self ._keywordPattern ,
135+ self ._keywordFormat ,
136+ )
137+
138+ def highlightText (self , text , expr , format ):
95139 """Highlights a text group with an expression and format
96140
97141 Args:
98142 text (str): text to highlight
99143 expr (QRegularExpression): search parameter
100144 format (QTextCharFormat): formatting rule
101- includeLast (bool): whether or not the last character should be highlighted
102145 """
146+ if expr is None or not text :
147+ return
148+
103149 # highlight all the given matches to the expression in the text
104150 for match in expr .finditer (text ):
105- start , end = match .span ()
151+ match_span = match .span ()
152+ start , end = match_span
106153 length = end - start
107- if includeLast :
108- length += 1
109- self .setFormat (start , length , format )
154+
155+ # Determine if the current highlight is within an already determined
156+ # highlight, if so, let's block it.
157+ blocked = False
158+ for span in self .spans :
159+ if start > span [0 ] and start < span [- 1 ]:
160+ blocked = True
161+ break
162+
163+ if not blocked :
164+ self .setFormat (start , length , format )
165+
166+ # Append the current span to self.spans, so we can later block
167+ # new highlights which should be blocked
168+ self .spans .append (match_span )
169+
170+ def isConsoleMode (self ):
171+ """checks to see if this highlighter is in console mode"""
172+ return self ._consoleMode
173+
174+ def setConsoleMode (self , state = False ):
175+ """sets the highlighter to only apply to console strings
176+ (lines starting with >>>)
177+ """
178+ self ._consoleMode = state
179+
180+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
181+ # # # COLORS # # #
182+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
183+
184+ def commentColor (self ):
185+ # Pull the color from the parent if possible because this doesn't support
186+ # stylesheets
187+ parent = self .parent ()
188+ if parent and hasattr (parent , 'commentColor' ):
189+ return parent .commentColor
190+ return self ._commentColor
191+
192+ def setCommentColor (self , color ):
193+ # set the color for the parent if possible because this doesn't support
194+ # stylesheets
195+ parent = self .parent ()
196+ if parent and hasattr (parent , 'commentColor' ):
197+ parent .commentColor = color
198+ self ._commentColor = color
110199
111200 def keywordColor (self ):
112201 # pull the color from the parent if possible because this doesn't support
@@ -124,13 +213,6 @@ def setKeywordColor(self, color):
124213 parent .keywordColor = color
125214 self ._keywordColor = color
126215
127- def keywordFormat (self ):
128- """returns the keywords QTextCharFormat for this highlighter"""
129- format = QTextCharFormat ()
130- format .setForeground (self .keywordColor ())
131-
132- return format
133-
134216 def resultColor (self ):
135217 # pull the color from the parent if possible because this doesn't support
136218 # stylesheets
@@ -147,31 +229,6 @@ def setResultColor(self, color):
147229 parent .resultColor = color
148230 self ._resultColor = color
149231
150- def resultFormat (self ):
151- """returns the result QTextCharFormat for this highlighter"""
152- fmt = QTextCharFormat ()
153- fmt .setForeground (self .resultColor ())
154- return fmt
155-
156- def setConsoleMode (self , state = False ):
157- """sets the highlighter to only apply to console strings
158- (lines starting with >>>)
159- """
160- self ._consoleMode = state
161-
162- def setLanguage (self , lang ):
163- """sets the language of the highlighter by loading the json definition"""
164- filename = resourcePath ('lang/%s.json' % lang .lower ())
165- if os .path .exists (filename ):
166- data = json .load (open (filename ))
167- self .setObjectName (data .get ('name' , '' ))
168- self ._keywords = data .get ('keywords' , [])
169- self ._comments = data .get ('comments' , [])
170- self ._strings = data .get ('strings' , [])
171-
172- return True
173- return False
174-
175232 def stringColor (self ):
176233 # pull the color from the parent if possible because this doesn't support
177234 # stylesheets
@@ -188,8 +245,56 @@ def setStringColor(self, color):
188245 parent .stringColor = color
189246 self ._stringColor = color
190247
191- def stringFormat (self ):
192- """returns the keywords QTextCharFormat for this highligter"""
193- format = QTextCharFormat ()
194- format .setForeground (self .stringColor ())
195- return format
248+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
249+ # # # Formats # # #
250+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
251+
252+ def defineCommentFormat (self ):
253+ """Define the comment format based on the comment color"""
254+ self ._commentFormat = QTextCharFormat ()
255+ self ._commentFormat .setForeground (self .commentColor ())
256+ self ._commentFormat .setFontItalic (True )
257+
258+ def defineKeywordFormat (self ):
259+ """Define the keyword format based on the keyword color"""
260+ self ._keywordFormat = QTextCharFormat ()
261+ self ._keywordFormat .setForeground (self .keywordColor ())
262+
263+ def defineResultFormat (self ):
264+ """Define the result format based on the result color"""
265+ self ._resultFormat = QTextCharFormat ()
266+ self ._resultFormat .setForeground (self .resultColor ())
267+
268+ def defineStringFormat (self ):
269+ """Define the string format based on the string color"""
270+ self ._stringFormat = QTextCharFormat ()
271+ self ._stringFormat .setForeground (self .stringColor ())
272+
273+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
274+ # # # PATTERNS # # #
275+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
276+
277+ def defineCommentPattern (self ):
278+ """Define the regex pattern to use for comment"""
279+ pattern = "|" .join (self ._comments )
280+ self ._commentPattern = re .compile (pattern )
281+
282+ def defineKeywordPattern (self ):
283+ """Define the regex pattern to use for keyword"""
284+ keywords = [r"\b{}\b" .format (word ) for word in self ._keywords ]
285+ pattern = "|" .join (keywords )
286+ self ._keywordPattern = re .compile (pattern )
287+
288+ def defineResultPattern (self ):
289+ """Define the regex pattern to use for results"""
290+ parent = self .parent ()
291+ if parent and hasattr (parent , 'outputPrompt' ):
292+ prompt = parent .outputPrompt ()
293+ pattern = '{}[^\n ]*' .format (prompt )
294+ self ._resultPattern = re .compile (pattern )
295+
296+ def defineStringPattern (self ):
297+ """Define the regex pattern to use for strings."""
298+ lst = ["""{0}[^{0}\n ]+{0}""" .format (st ) for st in self ._strings ]
299+ pattern = "|" .join (lst )
300+ self ._stringPattern = re .compile (pattern )
0 commit comments