about summary refs log tree commit diff stats
path: root/src/css/stylednode.nim
blob: 1b99773a9e6d1d075830ba35b0d2fd1ae338978c (plain) (blame)
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import css/selectorparser
import css/values
import html/dom

import chame/tags

# Container to hold a style and a node.
# Pseudo-elements are implemented using StyledNode objects without nodes. Input
# elements are implemented as internal "pseudo-elements."
#
# To avoid having to invalidate the entire tree on pseudo-class changes, each
# node holds a list of nodes their CSS values depend on. (This list may include
# the node itself.) In addition, nodes also store each value valid for
# dependency d. These are then used for checking the validity of StyledNodes.
#
# In other words - say we have to apply the author stylesheets of the following
# document:
#
# <style>
# div:hover { color: red; }
# :not(input:checked) + p { display: none; }
# </style>
# <div>This div turns red on hover.</div>
# <input type=checkbox>
# <p>This paragraph is only shown when the checkbox above is checked.
#
# That produces the following dependency graph (simplified):
# div -> div (hover)
# p -> input (checked)
#
# Then, to check if a node has been invalidated, we just iterate over all
# recorded dependencies of each StyledNode, and check if their registered value
# of the pseudo-class still matches that of its associated element.
#
# So in our example, for div we check if div's :hover pseudo-class has changed,
# for p we check whether input's :checked pseudo-class has changed.

type
  StyledType* = enum
    STYLED_ELEMENT, STYLED_TEXT, STYLED_REPLACEMENT

  DependencyType* = enum
    DEPEND_HOVER, DEPEND_CHECKED, DEPEND_FOCUS

  DependencyInfo* = object
    # All nodes we depend on, for each dependency type d.
    nodes*: array[DependencyType, seq[StyledNode]]
    # Previous value. The owner StyledNode is marked as invalid when one of
    # these no longer matches the DOM value.
    prev: array[DependencyType, bool]

  StyledNode* = ref object
    parent*: StyledNode
    node*: Node
    pseudo*: PseudoElem
    case t*: StyledType
    of STYLED_TEXT:
      text*: string
    of STYLED_ELEMENT:
      computed*: CSSComputedValues
      children*: seq[StyledNode]
      depends*: DependencyInfo
    of STYLED_REPLACEMENT: # replaced elements: quotes, or (TODO) markers, images
      content*: CSSContent

# For debugging
func `$`*(node: StyledNode): string =
  if node == nil:
    return "nil"
  case node.t
  of STYLED_TEXT:
    return "#text " & node.text
  of STYLED_ELEMENT:
    if node.node != nil:
      return $node.node
    return $node.pseudo
  of STYLED_REPLACEMENT:
    return "#replacement"

iterator branch*(node: StyledNode): StyledNode {.inline.} =
  var node = node
  while node != nil:
    yield node
    node = node.parent

iterator elementList*(node: StyledNode): StyledNode {.inline.} =
  for child in node.children:
    yield child

iterator elementList_rev*(node: StyledNode): StyledNode {.inline.} =
  for i in countdown(node.children.high, 0):
    yield node.children[i]

func findElement*(root: StyledNode, elem: Element): StyledNode =
  var stack: seq[StyledNode]
  for child in root.elementList_rev:
    if child.t == STYLED_ELEMENT and child.pseudo == PSEUDO_NONE:
      stack.add(child)
  let en = Node(elem)
  while stack.len > 0:
    let node = stack.pop()
    if node.node == en:
      return node
    for child in node.elementList_rev:
      if child.t == STYLED_ELEMENT and child.pseudo == PSEUDO_NONE:
        stack.add(child)

func isDomElement*(styledNode: StyledNode): bool {.inline.} =
  styledNode.t == STYLED_ELEMENT and styledNode.pseudo == PSEUDO_NONE

# DOM-style getters, for Element interoperability...
func parentElement*(node: StyledNode): StyledNode {.inline.} =
  node.parent

func checked(element: Element): bool =
  if element.tagType == TAG_INPUT:
    let input = HTMLInputElement(element)
    result = input.checked

func isValid*(styledNode: StyledNode): bool =
  if styledNode.t == STYLED_TEXT:
    return true
  if styledNode.t == STYLED_REPLACEMENT:
    return true
  if styledNode.node != nil and Element(styledNode.node).invalid:
    return false
  for d in DependencyType:
    for child in styledNode.depends.nodes[d]:
      assert child.node != nil
      let elem = Element(child.node)
      case d
      of DEPEND_HOVER:
        if child.depends.prev[d] != elem.hover:
          return false
      of DEPEND_CHECKED:
        if child.depends.prev[d] != elem.checked:
          return false
      of DEPEND_FOCUS:
        let focus = elem.document.focus == elem
        if child.depends.prev[d] != focus:
          return false
  return true

proc applyDependValues*(styledNode: StyledNode) =
  let elem = Element(styledNode.node)
  styledNode.depends.prev[DEPEND_HOVER] = elem.hover
  styledNode.depends.prev[DEPEND_CHECKED] = elem.checked
  let focus = elem.document.focus == elem
  styledNode.depends.prev[DEPEND_FOCUS] = focus
  elem.invalid = false

proc addDependency*(styledNode, dep: StyledNode, t: DependencyType) =
  if dep notin styledNode.depends.nodes[t]:
    styledNode.depends.nodes[t].add(dep)

func newStyledElement*(parent: StyledNode, element: Element, computed: CSSComputedValues, reg: DependencyInfo): StyledNode =
  result = StyledNode(t: STYLED_ELEMENT, computed: computed, node: element, parent: parent)
  result.depends = reg
  result.parent = parent

func newStyledElement*(parent: StyledNode, element: Element): StyledNode =
  result = StyledNode(t: STYLED_ELEMENT, node: element, parent: parent)
  result.parent = parent

# Root
func newStyledElement*(element: Element): StyledNode =
  result = StyledNode(t: STYLED_ELEMENT, node: element)

func newStyledElement*(parent: StyledNode, pseudo: PseudoElem, computed: CSSComputedValues, reg: sink DependencyInfo): StyledNode =
  result = StyledNode(t: STYLED_ELEMENT, computed: computed, pseudo: pseudo, parent: parent)
  result.depends = reg
  result.parent = parent

func newStyledElement*(parent: StyledNode, pseudo: PseudoElem, computed: CSSComputedValues): StyledNode =
  result = StyledNode(t: STYLED_ELEMENT, computed: computed, pseudo: pseudo, parent: parent)
  result.parent = parent

func newStyledText*(parent: StyledNode, text: string): StyledNode =
  result = StyledNode(t: STYLED_TEXT, text: text, parent: parent)
  result.parent = parent

func newStyledText*(parent: StyledNode, text: Text): StyledNode =
  result = StyledNode(t: STYLED_TEXT, text: text.data, node: text, parent: parent)
  result.parent = parent

func newStyledReplacement*(parent: StyledNode, content: CSSContent): StyledNode =
  return StyledNode(
    t: STYLED_REPLACEMENT,
    parent: parent,
    content: content
  )