import std/[options, sequtils, sugar] import formats/html import print # Notes on std/options: # std/options provides an Option[T] type, that can be some(T) or none(T). # The get(value: Option[T], otherwise: Option[T]) provides sugar for unwrapping with a default value. # Note that Nim's UFCS means that typically calls to get() look like value.get(T()). # (T() is an object constructor, and [] are both openarray accesses and generic brackets) const window_width = 500 window_height = 500 hstep = 13 vstep = 18 # const inline_elements = [] const 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" ] # Will expand to cover flexbox and grid in the future. # The Document layout is included because pages can # have multiple Documents through the use of iframes. type LayoutKind = enum Document, Block, Inline # Improve error messages: suggest a ref object when failing from recursion type Layout = ref object node: Node children: seq[Layout] parent: Option[Layout] previous: Option[Layout] x, y, width, height: float cursorX, cursorY: float case kind: LayoutKind: of Document: discard of Block: discard of Inline: cx, cy: float # cursor weight: int # The layout tree is constructed in a two-part process: # 1. We iterate through the node tree to create a simple layout tree. # 2. We iterate through the new layout tree to create references to parent and previous nodes. # Other functions will use these references to compute positioning coordinates. # This greatly cuts down code complexity. It likely has a performance impact on large trees. func construct_tree(node: Node): Layout = var children: seq[Layout] = @[] var kind: LayoutKind = Inline case node.kind: of Element: for child in node.children: let current = child.construct_tree() children.add(current) if kind == Inline and child.kind == Element and child.tag in block_elements: kind = Block if node.children.len == 0: kind = Block of Text: discard return Layout(node: node, children: children, kind: kind) # This function does another pass through the layout tree to add references to parents and siblings. func populate_tree(self: Layout, parent: Option[Layout] = none(Layout), previous: Option[Layout] = none(Layout)) = self.parent = parent self.previous = previous var prevchild: Option[Layout] = none(Layout) for child in self.children: child.populate_tree(parent=some(self), previous=prevchild) prevchild = some(child) # Overload the `value.get(otherwise)` function for brevity. func get(self: Option[Layout]): auto = self.get(Layout()) # This function does a third pass through the layout tree to calculate positions. func calculate_position(self: Layout) = case self.kind: of Document: self.width = window_width - 2*hstep self.x = hstep self.y = vstep of Block: self.x = self.parent.get.x # every object starts at its parent's left edge... self.y = # if there is no previous sibiling, they start at their parent's top edge... if self.previous.isSome(): self.previous.get.y + self.previous.get.height else: self.parent.get.y self.width = self.parent.get.width # by default, objects are greedy and take up all the space they can get... of Inline: self.x = self.parent.get.x self.y = if self.previous.isSome(): self.previous.get.y + self.previous.get.height else: self.parent.get.y self.width = self.parent.get.width self.cx = self.x self.cy = self.y for child in self.children: calculate_position(child) case self.kind: of Document: self.height = if self.children.len != 0: self.children.map(x => x.height).foldl(a + b) + 2*vstep else: 2*vstep of Block: self.height = # height calculation must come after the recursive call if self.children.len != 0: self.children.map(x => x.height).foldl(a + b) else: 0 of Inline: self.height = self.cy - self.y # These implicitly-mutating parameters are a bit gross, but # really seem like the best way to build this layout tree. # (they're implicitly mutable because Layouts are ref objects) # Assuming the first node of an HTML object is the tag. # Right now, HTML generation is Bad so this will change in the future func layout(html: Html): Layout = var self = Layout(kind: Document, node: html[0], children: @[]) for child in self.node.children: self.children.add(child.construct_tree()) self.populate_tree() self.calculate_position() return self when isMainModule: import formats/uri, protocols/http, std/strutils proc print_layout(layout: Layout, indentation=0) = if layout.node.kind == Element: stdout.write(layout.x, " ", layout.y, " ", layout.width, " ", layout.height, " ") stdout.write(" ".repeat(indentation)) stdout.write(layout.node.tag) stdout.write(":") stdout.write(layout.kind) let parent: Layout = layout.parent.get(Layout(node: Node(kind: Text))) if parent.node.kind == Element: stdout.write(" ") stdout.write("parent:") stdout.write(parent.node.tag) let previous: Layout = layout.previous.get(Layout(node: Node(kind: Text))) if previous.node.kind == Element: stdout.write(" ") stdout.write("previous:") stdout.write(previous.node.tag) stdout.write("\n") for child in layout.children: child.printLayout(indentation + 2) let text = parseHTML(httpRequest(parseURL("https://example.org:443/index.html")).body) print_layout layout(text) for node in layout(text).children: print node.node.tag