commit d5f4469c0d253755fb6b589d20259b886fea2c91
parent 8dd89c49c41c4c7d90aacbaac65eb18aea00892f
Author: Jake Bauer <jbauer@paritybit.ca>
Date: Tue, 21 Feb 2023 17:16:12 -0500
Finish end of Chapter 4 and Chapter 5
Diffstat:
M | browser.py | | | 227 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------- |
1 file changed, 188 insertions(+), 39 deletions(-)
diff --git a/browser.py b/browser.py
@@ -12,6 +12,16 @@ SCROLLSTEP = 100
EXEC_TIME = 0
+BLOCK_ELEMENTS = [
+ "html", "body", "article", "section", "nav", "aside",
+ "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "header",
+ "footer", "address", "p", "hr", "pre", "blockquote",
+ "ol", "ul", "menu", "li", "dl", "dt", "dd", "figure",
+ "figcaption", "main", "div", "table", "form", "fieldset",
+ "legend", "details", "summary"
+]
+
+
class Browser:
def __init__(self):
self.scroll = 0
@@ -24,19 +34,17 @@ class Browser:
self.canvas.pack()
self.window.bind("<Down>", self.scrolldown)
self.window.bind("<Up>", self.scrollup)
- self.display_list = []
- self.nodes = None
+ self.document = None
def scrolldown(self, e):
- self.canvas.delete("all")
- self.scroll += SCROLLSTEP
+ max_y = self.document.height - HEIGHT
+ self.scroll = min(self.scroll + SCROLLSTEP, max_y)
self.draw()
def scrollup(self, e):
- self.canvas.delete("all")
- self.scroll -= SCROLLSTEP
+ self.scroll = max(self.scroll - SCROLLSTEP, 0)
self.draw()
@@ -53,11 +61,14 @@ class Browser:
print('{0:.5f}'.format(end - start), "- Lexed response body")
start = timer()
- self.display_list = Layout(self.nodes).display_list
+ self.document = DocumentLayout(self.nodes)
+ self.document.layout()
end = timer()
print('{0:.5f}'.format(end - start), "- Computed page layout")
start = timer()
+ self.display_list = []
+ self.document.paint(self.display_list)
self.draw()
end = timer()
print('{0:.5f}'.format(end - start), "- Canvas drawn")
@@ -66,25 +77,96 @@ class Browser:
def draw(self):
- for x, y, word, font in self.display_list:
- if y > self.scroll + HEIGHT: continue
- if y + VSTEP < self.scroll: continue
- self.canvas.create_text(x, y - self.scroll, text=word,
- font=font, anchor="nw")
+ self.canvas.delete("all")
+ for cmd in self.display_list:
+ if cmd.top > self.scroll + HEIGHT: continue
+ if cmd.bottom < self.scroll: continue
+ cmd.execute(self.scroll, self.canvas)
+
+
+class DocumentLayout:
+ def __init__(self, node):
+ self.node = node
+ self.parent = None
+ self.children = []
+
+
+ def layout(self):
+ child = BlockLayout(self.node, self, None)
+ self.children.append(child)
+ self.width = WIDTH - 2*HSTEP
+ self.x = HSTEP
+ self.y = VSTEP
-class Layout:
- def __init__(self, nodes):
+ child.layout()
+ self.height = child.height + 2*VSTEP
+
+
+ def paint(self, display_list):
+ self.children[0].paint(display_list)
+
+
+class BlockLayout:
+ def __init__(self, node, parent, previous):
+ self.node = node
+ self.parent = parent
+ self.previous = previous
+ self.children = []
+ self.x = None
+ self.y = None
+ self.width = None
+ self.height = None
self.display_list = []
- self.line = []
- self.cursor_x = HSTEP
- self.cursor_y = VSTEP
- self.weight = "normal"
- self.slant = "roman"
- self.size = 10
- self.display = True
- self.recurse(nodes)
- self.flush()
+
+
+ def layout(self):
+ self.width = self.parent.width
+ self.x = self.parent.x
+
+ if self.previous:
+ self.y = self.previous.y + self.previous.height
+ else:
+ self.y = self.parent.y
+
+ mode = layout_mode(self.node)
+ if mode == "block":
+ previous = None
+ for child in self.node.children:
+ next = BlockLayout(child, self, previous)
+ self.children.append(next)
+ previous = next
+ else:
+ self.cursor_x = 0
+ self.cursor_y = 0
+ self.weight = "normal"
+ self.slant = "roman"
+ self.size = 10
+ self.display = True
+
+ self.line = []
+ self.recurse(self.node)
+ self.flush()
+
+ for child in self.children:
+ child.layout()
+
+ if mode == "block":
+ self.height = sum([
+ child.height for child in self.children])
+ else:
+ self.height = self.cursor_y
+
+
+ def paint(self, display_list):
+ if isinstance(self.node, Element) and self.node.tag == "pre":
+ x2, y2 = self.x + self.width, self.y + self.height
+ rect = DrawRect(self.x, self.y, x2, y2, "gray")
+ display_list.append(rect)
+ for x, y, word, font in self.display_list:
+ display_list.append(DrawText(x, y, word, font))
+ for child in self.children:
+ child.paint(display_list)
def flush(self):
@@ -92,21 +174,22 @@ class Layout:
metrics = [font.metrics() for x, word, font in self.line]
max_ascent = max([metric["ascent"] for metric in metrics])
baseline = self.cursor_y + 1.25 * max_ascent
- for x, word, font in self.line:
- y = baseline - font.metrics("ascent")
+ for rel_x, word, font in self.line:
+ x = self.x + rel_x
+ y = self.y + baseline - font.metrics("ascent")
self.display_list.append((x, y, word, font))
- self.cursor_x = HSTEP
+ self.cursor_x = 0
self.line = []
max_descent = max([metric["descent"] for metric in metrics])
self.cursor_y = baseline + 1.25 * max_descent
- def text(self, tok):
+ def text(self, node):
if not self.display: return
font = get_font(self.size, self.weight, self.slant)
- for word in tok.text.split():
+ for word in node.text.split():
w = font.measure(word)
- if self.cursor_x + w >= WIDTH - HSTEP:
+ if self.cursor_x + w >= self.width:
self.flush()
self.line.append((self.cursor_x, word, font))
self.cursor_x += w + font.measure(" ")
@@ -143,16 +226,51 @@ class Layout:
self.cursor_y += VSTEP
- def recurse(self, tree):
- if isinstance(tree, Text):
- self.text(tree)
+ def recurse(self, node):
+ if isinstance(node, Text):
+ self.text(node)
else:
- self.open_tag(tree.tag)
- for child in tree.children:
+ self.open_tag(node.tag)
+ for child in node.children:
self.recurse(child)
- self.close_tag(tree.tag)
+ self.close_tag(node.tag)
+class DrawText:
+ def __init__(self, x1, y1, text, font):
+ self.top = y1
+ self.left = x1
+ self.text = text
+ self.font = font
+ self.bottom = y1+font.metrics("linespace")
+
+
+ def execute(self, scroll, canvas):
+ canvas.create_text(
+ self.left, self.top - scroll,
+ text=self.text,
+ font=self.font,
+ anchor='nw',
+ )
+
+
+class DrawRect:
+ def __init__(self, x1, y1, x2, y2, color):
+ self.top = y1
+ self.left = x1
+ self.bottom = y2
+ self.right = x2
+ self.color = color
+
+
+ def execute(self, scroll, canvas):
+ canvas.create_rectangle(
+ self.left, self.top - scroll,
+ self.right, self.bottom - scroll,
+ width=0,
+ fill=self.color,
+ )
+
class Text:
def __init__(self, text, parent):
@@ -185,10 +303,33 @@ class HTMLParser:
"area", "base", "br", "col", "embed", "hr", "img", "input",
"link", "meta", "param", "source", "track", "wbr",
]
+ self.HEAD_TAGS = [
+ "base", "basefont", "bgsound", "noscript",
+ "link", "meta", "title", "style", "script",
+ ]
+
+
+ def implicit_tags(self, tag):
+ while True:
+ open_tags = [node.tag for node in self.unfinished]
+ if open_tags == [] and tag != "html":
+ self.add_tag("html")
+ elif open_tags == ["html"] \
+ and tag not in ["head", "body", "/html"]:
+ if tag in self.HEAD_TAGS:
+ self.add_tag("head")
+ else:
+ self.add_tag("body")
+ elif open_tags == ["html", "head"] \
+ and tag not in ["/head"] + self.HEAD_TAGS:
+ self.add_tag("/head")
+ else:
+ break
def add_text(self, text):
if text.isspace(): return
+ self.implicit_tags(None)
parent = self.unfinished[-1]
node = Text(text, parent)
parent.children.append(node)
@@ -197,6 +338,7 @@ class HTMLParser:
def add_tag(self, tag):
tag, attributes = self.get_attributes(tag)
if tag.startswith("!"): return
+ self.implicit_tags(tag)
if tag.startswith("/"):
if len(self.unfinished) == 1: return
node = self.unfinished.pop()
@@ -314,10 +456,17 @@ def get_font(size, weight, slant):
return FONTS[key]
-def print_tree(node, indent=0):
- print(" " * indent, node)
- for child in node.children:
- print_tree(child, indent + 2)
+def layout_mode(node):
+ if isinstance(node, Text):
+ return "inline"
+ elif node.children:
+ if any([isinstance(child, Element) and \
+ child.tag in BLOCK_ELEMENTS for child in node.children]):
+ return "block"
+ else:
+ return "inline"
+ else:
+ return "block"
if __name__ == "__main__":