17
17
# Simple demo python web browser. Lacks all sorts of important features.
18
18
19
19
from PyQt6 .QtWidgets import QApplication , QWidget , QMainWindow , QVBoxLayout , QHBoxLayout , QPushButton , QLabel , QLineEdit , QSizePolicy
20
- from PyQt6 .QtCore import QSettings , Qt , QPoint , QSize , QEvent
20
+ from PyQt6 .QtCore import QSettings , Qt , QPoint , QSize , QSocketNotifier
21
21
from PyQt6 .QtGui import QFont , QMouseEvent , QPainter , QFontMetrics
22
22
import requests
23
23
import os
24
+ import signal
24
25
import sys
26
+ import html_table # do not look inside this file, that would be cheating on a later exercise
25
27
from html .parser import HTMLParser
26
28
from urllib .parse import urlparse
27
29
@@ -37,7 +39,7 @@ class Renderer(HTMLParser, QWidget):
37
39
the right sort of text in the right places.
38
40
"""
39
41
40
- def __init__ (self , parent = None ):
42
+ def __init__ (self , browser , parent = None ):
41
43
"""
42
44
Code which is run when we create a new Renderer.
43
45
"""
@@ -54,21 +56,14 @@ def __init__(self, parent=None):
54
56
# e.g.
55
57
# (10, 20, 50, 30, "http://foo.com")
56
58
self .html = ""
57
- self .browser = None
59
+ self .browser = browser
58
60
59
61
def minimumSizeHint (self ):
60
62
"""
61
63
Returns the smallest possible size on the screen for our renderer.
62
64
"""
63
65
return QSize (800 , 400 )
64
66
65
- def set_browser (self , browser ):
66
- """
67
- Remembers a reference to the browser object, so we can tell
68
- the browser later when a link is clicked.
69
- """
70
- self .browser = browser
71
-
72
67
def mouseReleaseEvent (self , event : QMouseEvent | None ) -> None :
73
68
"""
74
69
Handle a click somewhere in the renderer area. See if it
@@ -118,6 +113,7 @@ def paintEvent(self, event):
118
113
self .space_needed_before_next_data = False
119
114
self .current_link = None # if we're in a <a href=...> hyperlink
120
115
self .known_links = list () # Links anywhere on the page
116
+ self .table = None # whether we're in an HTML table
121
117
# The following call interprets all the HTML in page_html.
122
118
# You can't see most of the code which does this because it's
123
119
# in the library which provides the HTMLParser class. But it will
@@ -127,6 +123,9 @@ def paintEvent(self, event):
127
123
# handle_data and handle_endtag depending on what's inside self.html.
128
124
self .feed (self .html )
129
125
self .painter = None
126
+ # Ignore the following two lines, they're used for exercise 4b only
127
+ if os .environ .get ("OUTPUT_STATUS" ) is not None :
128
+ print ("Rendering completed\n " , flush = True )
130
129
131
130
def handle_starttag (self , tag , attrs ):
132
131
"""
@@ -139,6 +138,13 @@ def handle_starttag(self, tag, attrs):
139
138
# Stuff inside these tags isn't actually HTML
140
139
# to display on the screen.
141
140
self .ignore_current_text = True
141
+ if self .table is not None :
142
+ # If we're inside a table, handle table-related tags but no others
143
+ if tag == 'tr' :
144
+ self .table .handle_tr_start ()
145
+ if tag == 'td' :
146
+ self .table .handle_td_start ()
147
+ return
142
148
if tag == 'b' or tag == 'strong' :
143
149
self .is_bold = True
144
150
if tag == 's' :
@@ -174,12 +180,20 @@ def handle_starttag(self, tag, attrs):
174
180
heading_number = int (tag [1 ])
175
181
font_size_difference = FONT_SIZE_INCREASES_FOR_HEADERS_1_TO_6 [heading_number - 1 ]
176
182
self .font_size += font_size_difference
183
+ if tag == 'table' :
184
+ self .table = html_table .HTMLTable ()
177
185
self .space_needed_before_next_data = True
178
186
179
187
def handle_endtag (self , tag ):
180
188
"""
181
189
Handle an HTML end tag, for example </a> or </b>
182
190
"""
191
+ if self .table is not None :
192
+ # If we're inside a table, handle table end but no other tags
193
+ if tag == 'table' :
194
+ self .y_pos = self .table .handle_table_end (self .y_pos , lambda x , y , content : self .draw_text (x , y , content ))
195
+ self .table = None
196
+ return
183
197
if tag == 'br' or tag == 'p' : # move to a new line
184
198
self .newline ()
185
199
if tag == 'script' or tag == 'style' or tag == 'title' :
@@ -221,6 +235,21 @@ def handle_data(self, data):
221
235
if self .space_needed_before_next_data :
222
236
self .space_needed_before_next_data = False
223
237
data = ' ' + data
238
+ if self .table is not None :
239
+ # If we're inside a table, ask our table layout code to
240
+ # figure out where to draw it later
241
+ self .table .handle_data (data )
242
+ else :
243
+ (text_width , text_height ) = self .draw_text (self .x_pos , self .y_pos , data )
244
+ self .x_pos = self .x_pos + text_width
245
+ if text_height > self .tallest_text_in_previous_line :
246
+ self .tallest_text_in_previous_line = text_height
247
+
248
+ def draw_text (self , x_pos , y_pos , text ):
249
+ """
250
+ Draw some text on the screen.
251
+ Returns a tuple of (x, y) space occupied
252
+ """
224
253
# Work out what font we'll draw this in.
225
254
weight = QFont .Weight .Normal
226
255
if self .is_bold :
@@ -233,26 +262,24 @@ def handle_data(self, data):
233
262
self .painter .setPen (fill )
234
263
# Work out the size of the text we're about to draw.
235
264
text_measurer = QFontMetrics (font )
236
- text_width = int (text_measurer .horizontalAdvance (data ))
265
+ text_width = int (text_measurer .horizontalAdvance (text ))
237
266
text_height = int (text_measurer .height ())
238
267
# Tell our GUI canvas to draw some text! The important bit!
239
- self .painter .drawText (QPoint (self . x_pos , self . y_pos + text_height ), data )
268
+ self .painter .drawText (QPoint (x_pos , y_pos + text_height ), text )
240
269
# If we're in a hyperlink, underline it and record its coordinates
241
270
# in case it gets clicked later.
242
271
if self .current_link is not None :
243
- self .painter .drawLine (self . x_pos , self . y_pos + text_height , self . x_pos + text_width , self . y_pos + text_height )
244
- self .known_links .append ((self . x_pos , self . y_pos , self . x_pos + text_width , self . y_pos + text_height , self .current_link ))
272
+ self .painter .drawLine (x_pos , y_pos + text_height , x_pos + text_width , y_pos + text_height )
273
+ self .known_links .append ((x_pos , y_pos , x_pos + text_width , y_pos + text_height , self .current_link ))
245
274
# Strikethrough - draw a line over the text but only
246
275
# if we don't cover more than 50% of it, we don't want it illegible
247
276
if self .is_strikethrough :
248
277
fraction_of_text_covered = 6 / self .font_size
249
278
if fraction_of_text_covered <= 0.5 :
250
- strikethrough_line_y_pos = self .y_pos + (self .font_size / 2 ) - 80
251
- self .canvas .create_line (self .x_pos , strikethrough_line_y_pos ,
252
- self .x_pos + text_width , strikethrough_line_y_pos )
253
- self .x_pos = self .x_pos + text_width
254
- if text_height > self .tallest_text_in_previous_line :
255
- self .tallest_text_in_previous_line = text_height
279
+ strikethrough_line_y_pos = y_pos + (self .font_size / 2 ) - 80
280
+ self .canvas .create_line (x_pos , strikethrough_line_y_pos ,
281
+ x_pos + text_width , strikethrough_line_y_pos )
282
+ return (text_width , text_height )
256
283
257
284
258
285
class Browser (QMainWindow ):
@@ -283,20 +310,21 @@ def __init__(self, initial_url):
283
310
toolbar .setLayout (toolbar_layout )
284
311
overall_layout = QVBoxLayout ()
285
312
overall_layout .addWidget (toolbar )
286
- self .renderer = Renderer ()
287
- self .renderer .set_browser (self )
313
+ self .renderer = Renderer (self )
288
314
overall_layout .addWidget (self .renderer )
289
315
self .status_bar = QLabel ("Status:" )
290
316
overall_layout .addWidget (self .status_bar )
291
317
widget = QWidget ()
292
318
widget .setLayout (overall_layout )
293
319
self .setCentralWidget (widget )
320
+ # Set up somewhere to remember the last URL the user used
294
321
self .settings = QSettings ("browser-learning" , "browser" )
295
322
if initial_url is None :
296
323
initial_url = self .settings .value ("url" , "https://en.wikipedia.org" , type = str )
297
- self .set_window_url (initial_url )
298
324
else :
299
325
self .navigate (initial_url )
326
+ self .set_window_url (initial_url )
327
+ self .setup_fuzzer_handling () # ignore
300
328
301
329
def go_button_clicked (self ):
302
330
"""
@@ -326,9 +354,6 @@ def set_status(self, message):
326
354
Update the status line at the bottom of the screen
327
355
"""
328
356
self .status_bar .setText (message )
329
- # Ignore the following two lines, they're used for exercise 4b only
330
- if os .environ .get ("OUTPUT_STATUS" ) is not None :
331
- print (message + "\n " , flush = True )
332
357
333
358
def set_window_url (self , url ):
334
359
"""
@@ -377,6 +402,20 @@ def setup_encryption(self, url):
377
402
elif "REQUESTS_CA_BUNDLE" in os .environ :
378
403
del os .environ ["REQUESTS_CA_BUNDLE" ]
379
404
405
+ def setup_fuzzer_handling (self ):
406
+ """
407
+ Ignore this function - it's used to set up
408
+ fuzzing for some of the later exercises.
409
+ """
410
+ self .reader , self .writer = os .pipe ()
411
+ signal .signal (signal .SIGHUP , lambda _s , _h : os .write (self .writer , b'a' ))
412
+ notifier = QSocketNotifier (self .reader , QSocketNotifier .Type .Read , self )
413
+ notifier .setEnabled (True )
414
+ def signal_received ():
415
+ os .read (self .reader , 1 )
416
+ window .go_button_clicked ()
417
+ notifier .activated .connect (signal_received )
418
+
380
419
381
420
#########################################
382
421
# Main program here
@@ -402,4 +441,4 @@ def setup_encryption(self, url):
402
441
# we need to display something on the screen, along with
403
442
# methods above like "go_button_clicked" or "mouseReleaseEvent"
404
443
# when the user interacts with the app.
405
- app .exec ()
444
+ app .exec ()
0 commit comments