about summary refs log tree commit diff stats
path: root/src/css/sheet.nim
blob: d1b47f1cdf1dea5212877fbbfea48bf43c0821ed (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
import streams
import tables

import css/mediaquery
import css/cssparser
import css/selectorparser
import html/tags

type
  CSSRuleBase* = ref object of RootObj

  CSSRuleDef* = ref object of CSSRuleBase
    sels*: SelectorList
    decls*: seq[CSSDeclaration]

  CSSConditionalDef* = ref object of CSSRuleBase
    children*: CSSStylesheet

  CSSMediaQueryDef* = ref object of CSSConditionalDef
    query*: MediaQueryList

  CSSStylesheet* = ref object
    mq_list*: seq[CSSMediaQueryDef]
    tag_table*: array[TagType, seq[CSSRuleDef]]
    id_table*: TableRef[string, seq[CSSRuleDef]]
    class_table*: TableRef[string, seq[CSSRuleDef]]
    general_list*: seq[CSSRuleDef]
    len*: int

type SelectorHashes = object
  tag: TagType
  id: string
  class: string

func newStylesheet*(cap: int): CSSStylesheet =
  new(result)
  let bucketsize = cap div 2
  result.id_table = newTable[string, seq[CSSRuleDef]](bucketsize)
  result.class_table = newTable[string, seq[CSSRuleDef]](bucketsize)
  result.general_list = newSeqOfCap[CSSRuleDef](bucketsize)

proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool

proc getSelectorIds(hashes: var SelectorHashes, sels: CompoundSelector) =
  for sel in sels:
    if hashes.getSelectorIds(sel):
      break

# For now, we match elements against the *last* selector.
#TODO this is inefficient, so we should eventually get rid of this
# function
proc getSelectorIds(hashes: var SelectorHashes, cxsel: ComplexSelector) =
  hashes.getSelectorIds(cxsel[^1])

proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool =
  case sel.t
  of TYPE_SELECTOR:
    hashes.tag = sel.tag
    return true
  of CLASS_SELECTOR:
    hashes.class = sel.class
    return true
  of ID_SELECTOR:
    hashes.id = sel.id
    return true
  of ATTR_SELECTOR, PSELEM_SELECTOR, UNIVERSAL_SELECTOR:
    return false
  of PSEUDO_SELECTOR:
    if sel.pseudo.t in {PSEUDO_IS, PSEUDO_WHERE}:
      # Basically just hash whatever the selectors have in common:
      #1. get the hashable values of selector 1
      #2. for every other selector x:
      #3.   get hashable values of selector x
      #4.   store hashable values of selector x that aren't stored yet
      #5.   for every hashable value of selector 1 that doesn't match selector x
      #6.     cancel hashable value
      var cancel_tag = false
      var cancel_id = false
      var cancel_class = false
      var i = 0
      if i < sel.pseudo.fsels.len:
        hashes.getSelectorIds(sel.pseudo.fsels[i])
        inc i

      while i < sel.pseudo.fsels.len:
        var nhashes: SelectorHashes
        nhashes.getSelectorIds(sel.pseudo.fsels[i])
        if hashes.tag == TAG_UNKNOWN:
          hashes.tag = nhashes.tag
        elif not cancel_tag and nhashes.tag != TAG_UNKNOWN and nhashes.tag != hashes.tag:
          cancel_tag = true

        if hashes.id == "":
          hashes.id = nhashes.id
        elif not cancel_id and nhashes.id != "" and nhashes.id != hashes.id:
          cancel_id = true

        if hashes.class == "":
          hashes.class = nhashes.class
        elif not cancel_class and nhashes.class != "" and nhashes.class != hashes.class:
          cancel_class = true

        inc i

      if cancel_tag:
        hashes.tag = TAG_UNKNOWN
      if cancel_id:
        hashes.id = ""
      if cancel_class:
        hashes.class = ""

      if hashes.tag != TAG_UNKNOWN or hashes.id != "" or hashes.class != "":
        return true

iterator gen_rules*(sheet: CSSStylesheet, tag: TagType, id: string, classes: seq[string]): CSSRuleDef =
  for rule in sheet.tag_table[tag]:
    yield rule
  if id != "":
    if sheet.id_table.hasKey(id):
      for rule in sheet.id_table[id]:
        yield rule
  if classes.len > 0:
    for class in classes:
      if sheet.class_table.hasKey(class):
        for rule in sheet.class_table[class]:
          yield rule
  for rule in sheet.general_list:
    yield rule

proc add(sheet: var CSSStylesheet, rule: CSSRuleDef) =
  var hashes: SelectorHashes
  for cxsel in rule.sels:
    hashes.getSelectorIds(cxsel)
    if hashes.tag != TAG_UNKNOWN:
      sheet.tag_table[hashes.tag].add(rule)
    elif hashes.id != "":
      if hashes.id notin sheet.id_table:
        sheet.id_table[hashes.id] = newSeq[CSSRuleDef]()
      sheet.id_table[hashes.id].add(rule)
    elif hashes.class != "":
      if hashes.class notin sheet.class_table:
        sheet.class_table[hashes.class] = newSeq[CSSRuleDef]()
      sheet.class_table[hashes.class].add(rule)
    else:
      sheet.general_list.add(rule)

proc add*(sheet: var CSSStylesheet, rule: CSSRuleBase) {.inline.} =
  if rule of CSSRuleDef:
    sheet.add(CSSRuleDef(rule))
  else:
    sheet.mq_list.add(CSSMediaQueryDef(rule))
  inc sheet.len

proc add*(sheet: var CSSStylesheet, sheet2: CSSStylesheet) {.inline.} =
  sheet.general_list.add(sheet2.general_list)
  for tag in TagType:
    sheet.tag_table[tag].add(sheet2.tag_table[tag])
  for key, value in sheet2.id_table.pairs:
    if key notin sheet.id_table:
      sheet.id_table[key] = newSeq[CSSRuleDef]()
    sheet.id_table[key].add(value)
  for key, value in sheet2.class_table.pairs:
    if key notin sheet.class_table:
      sheet.class_table[key] = newSeq[CSSRuleDef]()
    sheet.class_table[key].add(value)
  sheet.len += sheet2.len

proc getDeclarations(rule: CSSQualifiedRule): seq[CSSDeclaration] {.inline.} =
  rule.oblock.value.parseListOfDeclarations2()

proc addRule(stylesheet: var CSSStylesheet, rule: CSSQualifiedRule) =
  let sels = parseSelectors(rule.prelude)
  if sels.len > 0:
    let r = CSSRuleDef(sels: sels, decls: rule.getDeclarations())
    stylesheet.add(r)

proc addAtRule(stylesheet: var CSSStylesheet, atrule: CSSAtRule) =
  case atrule.name
  of "media":
    let query = parseMediaQueryList(atrule.prelude)
    let rules = atrule.oblock.value.parseListOfRules()
    if rules.len > 0:
      var media = CSSMediaQueryDef()
      media.children = newStylesheet(rules.len)
      media.query = query
      for rule in rules:
        if rule of CSSAtRule:
          media.children.addAtRule(CSSAtRule(rule))
        else:
          media.children.addRule(CSSQualifiedRule(rule))
      stylesheet.add(media)
  else: discard #TODO

proc parseStylesheet*(s: Stream): CSSStylesheet =
  let css = parseCSS(s)
  result = newStylesheet(css.value.len)
  for v in css.value:
    if v of CSSAtRule: result.addAtRule(CSSAtRule(v))
    else: result.addRule(CSSQualifiedRule(v))
  s.close()

proc parseStylesheet*(s: string): CSSStylesheet =
  return newStringStream(s).parseStylesheet()
class="o"><- next list # inside list list2 <- insert 6, list2 # check structure like before list2 <- copy list 10:num/raw <- first list2 list2 <- next list2 11:num/raw <- first list2 list2 <- next list2 12:num/raw <- first list2 list2 <- next list2 13:num/raw <- first list2 list2 <- prev list2 20:num/raw <- first list2 list2 <- prev list2 21:num/raw <- first list2 list2 <- prev list2 22:num/raw <- first list2 30:bool/raw <- equal list, list2 ] memory-should-contain [ 10 <- 5 # scanning next 11 <- 4 12 <- 6 # inserted element 13 <- 3 20 <- 6 # then prev 21 <- 4 22 <- 5 30 <- 1 # list back at start ] ] scenario inserting-at-end-of-duplex-list [ local-scope list:&:duplex-list:num <- push 3, 0 list <- push 4, list list <- push 5, list run [ list2:&:duplex-list:num <- next list # inside list list2 <- next list2 # now at end of list list2 <- insert 6, list2 # check structure like before list2 <- copy list 10:num/raw <- first list2 list2 <- next list2 11:num/raw <- first list2 list2 <- next list2 12:num/raw <- first list2 list2 <- next list2 13:num/raw <- first list2 list2 <- prev list2 20:num/raw <- first list2 list2 <- prev list2 21:num/raw <- first list2 list2 <- prev list2 22:num/raw <- first list2 30:bool/raw <- equal list, list2 ] memory-should-contain [ 10 <- 5 # scanning next 11 <- 4 12 <- 3 13 <- 6 # inserted element 20 <- 3 # then prev 21 <- 4 22 <- 5 30 <- 1 # list back at start ] ] scenario inserting-after-start-of-duplex-list [ local-scope list:&:duplex-list:num <- push 3, 0 list <- push 4, list list <- push 5, list run [ list <- insert 6, list # check structure like before list2:&:duplex-list:num <- copy list 10:num/raw <- first list2 list2 <- next list2 11:num/raw <- first list2 list2 <- next list2 12:num/raw <- first list2 list2 <- next list2 13:num/raw <- first list2 list2 <- prev list2 20:num/raw <- first list2 list2 <- prev list2 21:num/raw <- first list2 list2 <- prev list2 22:num/raw <- first list2 30:bool/raw <- equal list, list2 ] memory-should-contain [ 10 <- 5 # scanning next 11 <- 6 # inserted element 12 <- 4 13 <- 3 20 <- 4 # then prev 21 <- 6 22 <- 5 30 <- 1 # list back at start ] ] # remove 'x' from its surrounding list 'in' # # Returns null if and only if list is empty. Beware: in that case any other # pointers to the head are now invalid. def remove x:&:duplex-list:_elem/contained-in:in, in:&:duplex-list:_elem -> in:&:duplex-list:_elem [ local-scope load-ingredients # if 'x' is null, return return-unless x next-node:&:duplex-list:_elem <- get *x, next:offset prev-node:&:duplex-list:_elem <- get *x, prev:offset # null x's pointers *x <- put *x, next:offset, 0 *x <- put *x, prev:offset, 0 # if next-node is not null, set its prev pointer { break-unless next-node *next-node <- put *next-node, prev:offset, prev-node } # if prev-node is not null, set its next pointer and return { break-unless prev-node *prev-node <- put *prev-node, next:offset, next-node return } # if prev-node is null, then we removed the head node at 'in' # return the new head rather than the old 'in' return next-node ] scenario removing-from-duplex-list [ local-scope list:&:duplex-list:num <- push 3, 0 list <- push 4, list list <- push 5, list run [ list2:&:duplex-list:num <- next list # second element list <- remove list2, list 10:bool/raw <- equal list2, 0 # check structure like before list2 <- copy list 11:num/raw <- first list2 list2 <- next list2 12:num/raw <- first list2 20:&:duplex-list:num/raw <- next list2 list2 <- prev list2 30:num/raw <- first list2 40:bool/raw <- equal list, list2 ] memory-should-contain [ 10 <- 0 # remove returned non-null 11 <- 5 # scanning next, skipping deleted element 12 <- 3 20 <- 0 # no more elements 30 <- 5 # prev of final element 40 <- 1 # list back at start ] ] scenario removing-from-start-of-duplex-list [ local-scope list:&:duplex-list:num <- push 3, 0 list <- push 4, list list <- push 5, list run [ list <- remove list, list # check structure like before list2:&:duplex-list:num <- copy list 10:num/raw <- first list2 list2 <- next list2 11:num/raw <- first list2 20:&:duplex-list:num/raw <- next list2 list2 <- prev list2 30:num/raw <- first list2 40:bool/raw <- equal list, list2 ] memory-should-contain [ 10 <- 4 # scanning next, skipping deleted element 11 <- 3 20 <- 0 # no more elements 30 <- 4 # prev of final element 40 <- 1 # list back at start ] ] scenario removing-from-end-of-duplex-list [ local-scope list:&:duplex-list:num <- push 3, 0 list <- push 4, list list <- push 5, list run [ # delete last element list2:&:duplex-list:num <- next list list2 <- next list2 list <- remove list2, list 10:bool/raw <- equal list2, 0 # check structure like before list2 <- copy list 11:num/raw <- first list2 list2 <- next list2 12:num/raw <- first list2 20:&:duplex-list:num/raw <- next list2 list2 <- prev list2 30:num/raw <- first list2 40:bool/raw <- equal list, list2 ] memory-should-contain [ 10 <- 0 # remove returned non-null 11 <- 5 # scanning next, skipping deleted element 12 <- 4 20 <- 0 # no more elements 30 <- 5 # prev of final element 40 <- 1 # list back at start ] ] scenario removing-from-singleton-duplex-list [ local-scope list:&:duplex-list:num <- push 3, 0 run [ list <- remove list, list 1:num/raw <- copy list ] memory-should-contain [ 1 <- 0 # back to an empty list ] ] def remove x:&:duplex-list:_elem/contained-in:in, n:num, in:&:duplex-list:_elem -> in:&:duplex-list:_elem [ local-scope load-ingredients i:num <- copy 0 curr:&:duplex-list:_elem <- copy x { done?:bool <- greater-or-equal i, n break-if done? break-unless curr next:&:duplex-list:_elem <- next curr in <- remove curr, in curr <- copy next i <- add i, 1 loop } ] scenario removing-multiple-from-duplex-list [ local-scope list:&:duplex-list:num <- push 3, 0 list <- push 4, list list <- push 5, list run [ list2:&:duplex-list:num <- next list # second element list <- remove list2, 2, list stash list ] trace-should-contain [ app: 5 ] ] # remove values between 'start' and 'end' (both exclusive). # also clear pointers back out from start/end for hygiene. # set end to 0 to delete everything past start. # can't set start to 0 to delete everything before end, because there's no # clean way to return the new head pointer. def remove-between start:&:duplex-list:_elem, end:&:duplex-list:_elem/contained-in:start -> start:&:duplex-list:_elem [ local-scope load-ingredients next:&:duplex-list:_elem <- get *start, next:offset nothing-to-delete?:bool <- equal next, end return-if nothing-to-delete? assert next, [malformed duplex list] # start->next->prev = 0 # start->next = end *next <- put *next, prev:offset, 0 *start <- put *start, next:offset, end return-unless end # end->prev->next = 0 # end->prev = start prev:&:duplex-list:_elem <- get *end, prev:offset assert prev, [malformed duplex list - 2] *prev <- put *prev, next:offset, 0 *end <- put *end, prev:offset, start ] scenario remove-range [ # construct a duplex list with six elements [13, 14, 15, 16, 17, 18] local-scope list:&:duplex-list:num <- push 18, 0 list <- push 17, list list <- push 16, list list <- push 15, list list <- push 14, list list <- push 13, list run [ # delete 16 onwards # first pointer: to the third element list2:&:duplex-list:num <- next list list2 <- next list2 list2 <- remove-between list2, 0 # now check the list 10:num/raw <- get *list, value:offset list <- next list 11:num/raw <- get *list, value:offset list <- next list 12:num/raw <- get *list, value:offset 20:&:duplex-list:num/raw <- next list ] memory-should-contain [ 10 <- 13 11 <- 14 12 <- 15 20 <- 0 ] ] scenario remove-range-to-final [ local-scope # construct a duplex list with six elements [13, 14, 15, 16, 17, 18] list:&:duplex-list:num <- push 18, 0 list <- push 17, list list <- push 16, list list <- push 15, list list <- push 14, list list <- push 13, list run [ # delete 15, 16 and 17 # start pointer: to the second element list2:&:duplex-list:num <- next list # end pointer: to the last (sixth) element end:&:duplex-list:num <- next list2 end <- next end end <- next end end <- next end remove-between list2, end # now check the list 10:num/raw <- get *list, value:offset list <- next list 11:num/raw <- get *list, value:offset list <- next list 12:num/raw <- get *list, value:offset 20:&:duplex-list:num/raw <- next list ] memory-should-contain [ 10 <- 13 11 <- 14 12 <- 18 20 <- 0 # no more elements ] ] scenario remove-range-empty [ local-scope # construct a duplex list with three elements [13, 14, 15] list:&:duplex-list:num <- push 15, 0 list <- push 14, list list <- push 13, list run [ # delete between first and second element (i.e. nothing) list2:&:duplex-list:num <- next list remove-between list, list2 # now check the list 10:num/raw <- get *list, value:offset list <- next list 11:num/raw <- get *list, value:offset list <- next list 12:num/raw <- get *list, value:offset 20:&:duplex-list:num/raw <- next list ] # no change memory-should-contain [ 10 <- 13 11 <- 14 12 <- 15 20 <- 0 ] ] scenario remove-range-to-end [ local-scope # construct a duplex list with six elements [13, 14, 15, 16, 17, 18] list:&:duplex-list:num <- push 18, 0 list <- push 17, list list <- push 16, list list <- push 15, list list <- push 14, list list <- push 13, list run [ # remove the third element and beyond list2:&:duplex-list:num <- next list remove-between list2, 0 # now check the list 10:num/raw <- get *list, value:offset list <- next list 11:num/raw <- get *list, value:offset 20:&:duplex-list:num/raw <- next list ] memory-should-contain [ 10 <- 13 11 <- 14 20 <- 0 ] ] # insert list beginning at 'start' after 'in' def splice in:&:duplex-list:_elem, start:&:duplex-list:_elem/contained-in:in -> in:&:duplex-list:_elem [ local-scope load-ingredients return-unless in return-unless start end:&:duplex-list:_elem <- last start next:&:duplex-list:_elem <- next in { break-unless next *end <- put *end, next:offset, next *next <- put *next, prev:offset, end } *in <- put *in, next:offset, start *start <- put *start, prev:offset, in ] # insert contents of 'new' after 'in' def insert in:&:duplex-list:_elem, new:&:@:_elem -> in:&:duplex-list:_elem [ local-scope load-ingredients return-unless in return-unless new len:num <- length *new return-unless len curr:&:duplex-list:_elem <- copy in idx:num <- copy 0 { done?:bool <- greater-or-equal idx, len break-if done? c:_elem <- index *new, idx insert c, curr # next iter curr <- next curr idx <- add idx, 1 loop } ] def append in:&:duplex-list:_elem, new:&:duplex-list:_elem/contained-in:in -> in:&:duplex-list:_elem [ local-scope load-ingredients last:&:duplex-list:_elem <- last in *last <- put *last, next:offset, new return-unless new *new <- put *new, prev:offset, last ] def last in:&:duplex-list:_elem -> result:&:duplex-list:_elem [ local-scope load-ingredients result <- copy in { next:&:duplex-list:_elem <- next result break-unless next result <- copy next loop } ] # does a duplex list start with a certain sequence of elements? def match x:&:duplex-list:_elem, y:&:@:_elem -> result:bool [ local-scope load-ingredients i:num <- copy 0 max:num <- length *y { done?:bool <- greater-or-equal i, max break-if done? expected:_elem <- index *y, i return-unless x, 0/no-match curr:_elem <- first x curr-matches?:bool <- equal curr, expected return-unless curr-matches?, 0/no-match x <- next x i <- add i, 1 loop } return 1/successful-match ] scenario duplex-list-match [ local-scope list:&:duplex-list:char <- push 97/a, 0 list <- push 98/b, list list <- push 99/c, list list <- push 100/d, list run [ 10:bool/raw <- match list, [] 11:bool/raw <- match list, [d] 12:bool/raw <- match list, [dc] 13:bool/raw <- match list, [dcba] 14:bool/raw <- match list, [dd] 15:bool/raw <- match list, [dcbax] ] memory-should-contain [ 10 <- 1 # matches [] 11 <- 1 # matches [d] 12 <- 1 # matches [dc] 13 <- 1 # matches [dcba] 14 <- 0 # does not match [dd] 15 <- 0 # does not match [dcbax] ] ] # helper for debugging def dump-from x:&:duplex-list:_elem [ local-scope load-ingredients $print x, [: ] { break-unless x c:_elem <- get *x, value:offset $print c, [ ] x <- next x { is-newline?:bool <- equal c, 10/newline break-unless is-newline? $print 10/newline $print x, [: ] } loop } $print 10/newline, [---], 10/newline ] scenario stash-duplex-list [ local-scope list:&:duplex-list:num <- push 1, 0 list <- push 2, list list <- push 3, list run [ stash [list:], list ] trace-should-contain [ app: list: 3 <-> 2 <-> 1 ] ] def to-text in:&:duplex-list:_elem -> result:text [ local-scope load-ingredients buf:&:buffer:char <- new-buffer 80 buf <- to-buffer in, buf result <- buffer-to-array buf ] # variant of 'to-text' which stops printing after a few elements (and so is robust to cycles) def to-text-line in:&:duplex-list:_elem -> result:text [ local-scope load-ingredients buf:&:buffer:char <- new-buffer 80 buf <- to-buffer in, buf, 6 # max elements to display result <- buffer-to-array buf ] def to-buffer in:&:duplex-list:_elem, buf:&:buffer:char -> buf:&:buffer:char [ local-scope load-ingredients { break-if in buf <- append buf, [[]] return } # append in.value to buf val:_elem <- get *in, value:offset buf <- append buf, val # now prepare next next:&:duplex-list:_elem <- next in nextn:num <- copy next return-unless next buf <- append buf, [ <-> ] # and recurse remaining:num, optional-ingredient-found?:bool <- next-ingredient { break-if optional-ingredient-found? # unlimited recursion buf <- to-buffer next, buf return } { break-unless remaining # limited recursion remaining <- subtract remaining, 1 buf <- to-buffer next, buf, remaining return } # past recursion depth; insert ellipses and stop append buf, [...] ] scenario stash-empty-duplex-list [ local-scope x:&:duplex-list:num <- copy 0 run [ stash x ] trace-should-contain [ app: [] ] ]