about summary refs log tree commit diff stats
path: root/src/css/match.nim
blob: 065d34cfb902586261365c939c5f8a3c896aadc3 (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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
import std/strutils

import chame/tags
import css/cssparser
import css/selectorparser
import html/catom
import html/dom
import utils/twtstr

# We use three match types.
# "mtTrue" and "mtFalse" are self-explanatory.
# "mtContinue" is like "mtFalse", but also modifies "depends".  This
# "depends" change is only propagated at the end if no selector before
# the pseudo element matches the element, and the last match was
# "mtContinue".
#
# Since style is only recomputed (e.g. when the hovered element changes)
# for elements that are included in "depends", this has the effect of
# minimizing such recomputations to cases where it's really necessary.
type MatchType = enum
  mtFalse, mtTrue, mtContinue

converter toMatchType(b: bool): MatchType =
  return MatchType(b)

#TODO rfNone should match insensitively for certain properties
func matchesAttr(element: Element; sel: Selector): bool =
  case sel.rel.t
  of rtExists: return element.attrb(sel.attr)
  of rtEquals:
    case sel.rel.flag
    of rfNone: return element.attr(sel.attr) == sel.value
    of rfI: return element.attr(sel.attr).equalsIgnoreCase(sel.value)
    of rfS: return element.attr(sel.attr) == sel.value
  of rtToken:
    let val = element.attr(sel.attr)
    case sel.rel.flag
    of rfNone: return sel.value in val.split(AsciiWhitespace)
    of rfI:
      let val = val.toLowerAscii()
      let selval = sel.value.toLowerAscii()
      return selval in val.split(AsciiWhitespace)
    of rfS: return sel.value in val.split(AsciiWhitespace)
  of rtBeginDash:
    let val = element.attr(sel.attr)
    case sel.rel.flag
    of rfNone:
      return val == sel.value or sel.value.startsWith(val & '-')
    of rfI:
      return val.equalsIgnoreCase(sel.value) or
        sel.value.startsWithIgnoreCase(val & '-')
    of rfS:
      return val == sel.value or sel.value.startsWith(val & '-')
  of rtStartsWith:
    let val = element.attr(sel.attr)
    case sel.rel.flag
    of rfNone: return val.startsWith(sel.value)
    of rfI: return val.startsWithIgnoreCase(sel.value)
    of rfS: return val.startsWith(sel.value)
  of rtEndsWith:
    let val = element.attr(sel.attr)
    case sel.rel.flag
    of rfNone: return val.endsWith(sel.value)
    of rfI: return val.endsWithIgnoreCase(sel.value)
    of rfS: return val.endsWith(sel.value)
  of rtContains:
    let val = element.attr(sel.attr)
    case sel.rel.flag
    of rfNone: return val.contains(sel.value)
    of rfI:
      let val = val.toLowerAscii()
      let selval = sel.value.toLowerAscii()
      return val.contains(selval)
    of rfS: return val.contains(sel.value)

func matches*(element: Element; cxsel: ComplexSelector;
  depends: var DependencyInfo): bool

func matches(element: Element; slist: SelectorList;
    depends: var DependencyInfo): bool =
  for cxsel in slist:
    if element.matches(cxsel, depends):
      return true
  return false

func matches(element: Element; pseudo: PseudoData;
    depends: var DependencyInfo): MatchType =
  case pseudo.t
  of pcFirstChild: return element.parentNode.firstElementChild == element
  of pcLastChild: return element.parentNode.lastElementChild == element
  of pcFirstNode: return element.isFirstVisualNode()
  of pcLastNode: return element.isLastVisualNode()
  of pcOnlyChild:
    return element.parentNode.firstElementChild == element and
      element.parentNode.lastElementChild == element
  of pcHover:
    depends.add(element, dtHover)
    if element.hover:
      return mtTrue
    return mtContinue
  of pcRoot: return element == element.document.documentElement
  of pcNthChild:
    let A = pseudo.anb.A # step
    let B = pseudo.anb.B # start
    if pseudo.ofsels.len == 0:
      let i = element.elIndex + 1
      if A == 0:
        return i == B
      let j = (i - B)
      if A < 0:
        return j <= 0 and j mod A == 0
      return j >= 0 and j mod A == 0
    if element.matches(pseudo.ofsels, depends):
      var i = 1
      for child in element.parentNode.elementList:
        if child == element:
          if A == 0:
            return i == B
          let j = (i - B)
          if A < 0:
            return j <= 0 and j mod A == 0
          return j >= 0 and j mod A == 0
        if child.matches(pseudo.ofsels, depends):
          inc i
    return mtFalse
  of pcNthLastChild:
    let A = pseudo.anb.A # step
    let B = pseudo.anb.B # start
    if pseudo.ofsels.len == 0:
      let last = element.parentNode.lastElementChild
      let i = last.elIndex + 1 - element.elIndex
      if A == 0:
        return i == B
      let j = (i - B)
      if A < 0:
        return j <= 0 and j mod A == 0
      return j >= 0 and j mod A == 0
    if element.matches(pseudo.ofsels, depends):
      var i = 1
      for child in element.parentNode.elementList_rev:
        if child == element:
          if A == 0:
            return i == B
          let j = (i - B)
          if A < 0:
            return j <= 0 and j mod A == 0
          return j >= 0 and j mod A == 0
        if child.matches(pseudo.ofsels, depends):
          inc i
    return mtFalse
  of pcChecked:
    if element.tagType == TAG_INPUT:
      depends.add(element, dtChecked)
      if HTMLInputElement(element).checked:
        return mtTrue
    elif element.tagType == TAG_OPTION:
      depends.add(element, dtChecked)
      if HTMLOptionElement(element).selected:
        return mtTrue
    return mtContinue
  of pcFocus:
    depends.add(element, dtFocus)
    if element.document.focus == element:
      return mtTrue
    return mtContinue
  of pcTarget:
    depends.add(element, dtTarget)
    if element.document.target == element:
      return mtTrue
    return mtContinue
  of pcNot:
    return not element.matches(pseudo.fsels, depends)
  of pcIs, pcWhere:
    return element.matches(pseudo.fsels, depends)
  of pcLang:
    return pseudo.s == "en" #TODO languages?
  of pcLink:
    return element.tagType in {TAG_A, TAG_AREA} and element.attrb(satHref)
  of pcVisited:
    return mtFalse

func matches(element: Element; sel: Selector; depends: var DependencyInfo):
    MatchType =
  case sel.t
  of stType:
    return element.localName == sel.tag
  of stClass:
    for it in element.classList:
      if sel.class == it.toLowerAscii():
        return mtTrue
    return mtFalse
  of stId:
    return sel.id == element.id.toLowerAscii()
  of stAttr:
    return element.matchesAttr(sel)
  of stPseudoClass:
    return element.matches(sel.pseudo, depends)
  of stPseudoElement:
    return mtTrue
  of stUniversal:
    return mtTrue

func matches(element: Element; sels: CompoundSelector;
    depends: var DependencyInfo): MatchType =
  var res = mtTrue
  for sel in sels:
    case element.matches(sel, depends)
    of mtFalse: return mtFalse
    of mtTrue: discard
    of mtContinue: res = mtContinue
  return res

# Note: this modifies "depends".
func matches*(element: Element; cxsel: ComplexSelector;
    depends: var DependencyInfo): bool =
  var e = element
  var pmatch = mtTrue
  var mdepends = DependencyInfo.default
  for i in countdown(cxsel.high, 0):
    var match = mtFalse
    let csel = cxsel[i]
    case csel.ct
    of ctNone:
      match = e.matches(csel, mdepends)
    of ctDescendant:
      e = e.parentElement
      while e != nil:
        case e.matches(csel, mdepends)
        of mtFalse: discard
        of mtTrue:
          match = mtTrue
          break
        of mtContinue: match = mtContinue # keep looking
        e = e.parentElement
    of ctChild:
      e = e.parentElement
      if e != nil:
        match = e.matches(csel, mdepends)
    of ctNextSibling:
      let prev = e.previousElementSibling
      if prev != nil:
        e = prev
        match = e.matches(csel, mdepends)
    of ctSubsequentSibling:
      let parent = e.parentNode
      for j in countdown(e.index - 1, 0):
        let child = parent.childList[j]
        if child of Element:
          let child = Element(child)
          case child.matches(csel, mdepends)
          of mtTrue:
            e = child
            match = mtTrue
            break
          of mtFalse: discard
          of mtContinue: match = mtContinue # keep looking
    if match == mtFalse:
      return false # we can discard depends.
    if pmatch == mtContinue and match == mtTrue or e == nil:
      pmatch = mtContinue
      break # we must update depends.
    pmatch = match
  depends.merge(mdepends)
  if pmatch == mtContinue:
    return false
  return true

# Forward declaration hack
matchesImpl = func(element: Element; cxsels: seq[ComplexSelector]): bool
    {.nimcall.} =
  var dummy = DependencyInfo.default
  return element.matches(cxsels, dummy)