1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
|
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 <html> 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
|