browser.py

Working through the exercises at browser.engineering
git clone https://git.sr.ht/~jbauer/browser.py
Log | Files | Refs | README | LICENSE

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:
Mbrowser.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__":