about summary refs log tree commit diff stats
path: root/src/css/selectorparser.nim
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-05-10 22:57:39 +0200
committerbptato <nincsnevem662@gmail.com>2022-05-10 22:57:39 +0200
commitf0241b2fec3e41744aa0c900fc4e6d3c46fe4757 (patch)
tree51dc8763cab5973590d16ac4460731fd4b5035a3 /src/css/selectorparser.nim
parent4026a4e92957634ede3f6723e582b30064e750cd (diff)
downloadchawan-f0241b2fec3e41744aa0c900fc4e6d3c46fe4757.tar.gz
Rename conflicting source files
Nim can't really differentiate between them, unfortunately.
Diffstat (limited to 'src/css/selectorparser.nim')
-rw-r--r--src/css/selectorparser.nim340
1 files changed, 340 insertions, 0 deletions
diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim
new file mode 100644
index 00000000..d44d7168
--- /dev/null
+++ b/src/css/selectorparser.nim
@@ -0,0 +1,340 @@
+import unicode
+
+import css/cssparser
+import html/tags
+
+type
+  SelectorType* = enum
+    TYPE_SELECTOR, ID_SELECTOR, ATTR_SELECTOR, CLASS_SELECTOR,
+    UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR, FUNC_SELECTOR,
+    COMBINATOR_SELECTOR
+
+  QueryMode = enum
+    QUERY_TYPE, QUERY_CLASS, QUERY_ATTR, QUERY_DELIM, QUERY_VALUE,
+    QUERY_PSEUDO, QUERY_PSELEM, QUERY_DESC_COMBINATOR, QUERY_CHILD_COMBINATOR,
+    QUERY_NEXT_SIBLING_COMBINATOR, QUERY_SUBSEQ_SIBLING_COMBINATOR
+
+  PseudoElem* = enum
+    PSEUDO_NONE, PSEUDO_BEFORE, PSEUDO_AFTER
+
+  PseudoClass* = enum
+    PSEUDO_FIRST_CHILD, PSEUDO_LAST_CHILD, PSEUDO_ONLY_CHILD, PSEUDO_HOVER,
+    PSEUDO_ROOT, PSEUDO_NTH_CHILD
+
+  CombinatorType* = enum
+    DESCENDANT_COMBINATOR, CHILD_COMBINATOR, NEXT_SIBLING_COMBINATOR,
+    SUBSEQ_SIBLING_COMBINATOR
+
+  SelectorParser = object
+    selectors: seq[SelectorList]
+    query: QueryMode
+    combinator: Selector
+
+  #TODO combinators
+  Selector* = ref object of RootObj
+    case t*: SelectorType
+    of TYPE_SELECTOR:
+      tag*: TagType
+    of ID_SELECTOR:
+      id*: string
+    of ATTR_SELECTOR:
+      attr*: string
+      value*: string
+      rel*: char
+    of CLASS_SELECTOR:
+      class*: string
+    of UNIVERSAL_SELECTOR: #TODO namespaces?
+      discard
+    of PSEUDO_SELECTOR:
+      pseudo*: PseudoClass
+      pseudonum*: float64
+    of PSELEM_SELECTOR:
+      elem*: PseudoElem
+    of FUNC_SELECTOR:
+      name*: string
+      fsels*: seq[SelectorList]
+    of COMBINATOR_SELECTOR:
+      ct*: CombinatorType
+      csels*: seq[SelectorList]
+
+  SelectorList* = ref object
+    sels*: seq[Selector]
+    pseudo*: PseudoElem
+
+proc add(sellist: SelectorList, sel: Selector) = sellist.sels.add(sel)
+proc add(sellist: SelectorList, sels: SelectorList) = sellist.sels.add(sels.sels)
+proc `[]`*(sellist: SelectorList, i: int): Selector = sellist.sels[i]
+proc len*(sellist: SelectorList): int = sellist.sels.len
+
+func getSpecificity*(sels: SelectorList): int
+
+func getSpecificity(sel: Selector): int =
+  case sel.t
+  of ID_SELECTOR:
+    result += 1000000
+  of CLASS_SELECTOR, ATTR_SELECTOR, PSEUDO_SELECTOR:
+    result += 1000
+  of TYPE_SELECTOR, PSELEM_SELECTOR:
+    result += 1
+  of FUNC_SELECTOR:
+    case sel.name
+    of "is":
+      var best = 0
+      for child in sel.fsels:
+        let s = getSpecificity(child)
+        if s > best:
+          best = s
+      result += best
+    of "not":
+      for child in sel.fsels:
+        result += getSpecificity(child)
+    else: discard
+  of UNIVERSAL_SELECTOR:
+    discard
+  of COMBINATOR_SELECTOR:
+    for child in sel.csels:
+      result += getSpecificity(child)
+
+func getSpecificity*(sels: SelectorList): int =
+  for sel in sels.sels:
+    result += getSpecificity(sel)
+
+func optimizeSelectorList*(selectors: SelectorList): SelectorList =
+  new(result)
+  #pass 1: check for invalid sequences
+  var i = 1
+  while i < selectors.len:
+    let sel = selectors[i]
+    if sel.t == TYPE_SELECTOR or sel.t == UNIVERSAL_SELECTOR:
+      return SelectorList()
+    inc i
+
+  #pass 2: move selectors in combination
+  if selectors.len > 1:
+    var i = 0
+    var slow = SelectorList()
+    if selectors[0].t == UNIVERSAL_SELECTOR:
+      inc i
+
+    while i < selectors.len:
+      if selectors[i].t in {ATTR_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR}:
+        slow.add(selectors[i])
+      else:
+        result.add(selectors[i])
+      inc i
+
+    result.add(slow)
+  else:
+    result.add(selectors[0])
+
+proc addSelector(state: var SelectorParser, sel: Selector) =
+  if state.combinator != nil:
+    if sel.t == PSELEM_SELECTOR:
+      state.combinator.csels[^1].pseudo = sel.elem
+    state.combinator.csels[^1].add(sel)
+  else:
+    if sel.t == PSELEM_SELECTOR:
+      state.selectors[^1].pseudo = sel.elem
+    state.selectors[^1].add(sel)
+
+proc getLastSel(state: SelectorParser): Selector =
+  if state.combinator != nil:
+    return state.combinator.csels[^1].sels[^1]
+  else:
+    return state.selectors[^1].sels[^1]
+
+proc addSelectorList(state: var SelectorParser) =
+  if state.combinator != nil:
+    state.selectors[^1].add(state.combinator)
+    state.combinator = nil
+  state.selectors.add(SelectorList())
+
+proc parseSelectorCombinator(state: var SelectorParser, ct: CombinatorType, csstoken: CSSToken) =
+  if csstoken.tokenType in {CSS_IDENT_TOKEN, CSS_HASH_TOKEN,
+                            CSS_COLON_TOKEN}:
+    if state.combinator != nil and state.combinator.ct != ct:
+      let nc = Selector(t: COMBINATOR_SELECTOR, ct: ct)
+      nc.csels.add(SelectorList())
+      nc.csels[^1].add(state.combinator)
+      state.combinator = nc
+
+    if state.combinator == nil:
+      state.combinator = Selector(t: COMBINATOR_SELECTOR, ct: ct)
+
+    state.combinator.csels.add(state.selectors[^1])
+    if state.combinator.csels[^1].len > 0:
+      state.combinator.csels.add(SelectorList())
+    state.selectors[^1] = SelectorList()
+    state.query = QUERY_TYPE
+
+proc parseSelectorToken(state: var SelectorParser, csstoken: CSSToken) =
+  case state.query
+  of QUERY_DESC_COMBINATOR:
+    state.parseSelectorCombinator(DESCENDANT_COMBINATOR, csstoken)
+  of QUERY_CHILD_COMBINATOR:
+    if csstoken.tokenType == CSS_WHITESPACE_TOKEN:
+      return
+    state.parseSelectorCombinator(CHILD_COMBINATOR, csstoken)
+  of QUERY_NEXT_SIBLING_COMBINATOR:
+    if csstoken.tokenType == CSS_WHITESPACE_TOKEN:
+      return
+    state.parseSelectorCombinator(NEXT_SIBLING_COMBINATOR, csstoken)
+  of QUERY_SUBSEQ_SIBLING_COMBINATOR:
+    if csstoken.tokenType == CSS_WHITESPACE_TOKEN:
+      return
+    state.parseSelectorCombinator(SUBSEQ_SIBLING_COMBINATOR, csstoken)
+  else: discard
+
+  case csstoken.tokenType
+  of CSS_IDENT_TOKEN:
+    case state.query
+    of QUERY_CLASS:
+      state.addSelector(Selector(t: CLASS_SELECTOR, class: $csstoken.value))
+    of QUERY_TYPE:
+      state.addSelector(Selector(t: TYPE_SELECTOR, tag: tagType($csstoken.value)))
+    of QUERY_PSEUDO:
+      case $csstoken.value
+      of "before":
+        state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_BEFORE))
+      of "after":
+        state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_AFTER))
+      of "first-child":
+        state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_FIRST_CHILD))
+      of "last-child":
+        state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_LAST_CHILD))
+      of "only-child":
+        state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_ONLY_CHILD))
+      of "hover":
+        state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_HOVER))
+      of "root":
+        state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_ROOT))
+    of QUERY_PSELEM:
+      case $csstoken.value
+      of "before":
+        state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_BEFORE))
+      of "after":
+        state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_AFTER))
+      else: discard
+    else: discard
+    state.query = QUERY_TYPE
+  of CSS_DELIM_TOKEN:
+    case csstoken.rvalue
+    of Rune('.'):
+      state.query = QUERY_CLASS
+    of Rune('>'):
+      if state.selectors[^1].len > 0 or state.combinator != nil:
+        state.query = QUERY_CHILD_COMBINATOR
+    of Rune('+'):
+      if state.selectors[^1].len > 0 or state.combinator != nil:
+        state.query = QUERY_NEXT_SIBLING_COMBINATOR
+    of Rune('~'):
+      if state.selectors[^1].len > 0 or state.combinator != nil:
+        state.query = QUERY_SUBSEQ_SIBLING_COMBINATOR
+    of Rune('*'):
+      state.addSelector(Selector(t: UNIVERSAL_SELECTOR))
+    else: discard
+  of CSS_HASH_TOKEN:
+    state.addSelector(Selector(t: ID_SELECTOR, id: $csstoken.value))
+  of CSS_COMMA_TOKEN:
+    if state.selectors[^1].len > 0:
+      state.addSelectorList()
+  of CSS_WHITESPACE_TOKEN:
+    if state.selectors[^1].len > 0 or state.combinator != nil:
+      state.query = QUERY_DESC_COMBINATOR
+  of CSS_COLON_TOKEN:
+    if state.query == QUERY_PSEUDO:
+      state.query = QUERY_PSELEM
+    else:
+      state.query = QUERY_PSEUDO
+  else: discard
+
+proc parseSelectorSimpleBlock(state: var SelectorParser, cssblock: CSSSimpleBlock) =
+  case cssblock.token.tokenType
+  of CSS_LBRACKET_TOKEN:
+    state.query = QUERY_ATTR
+    for cval in cssblock.value:
+      if cval of CSSToken:
+        let csstoken = (CSSToken)cval
+        case csstoken.tokenType
+        of CSS_IDENT_TOKEN:
+          case state.query
+          of QUERY_ATTR:
+            state.query = QUERY_DELIM
+            state.addSelector(Selector(t: ATTR_SELECTOR, attr: $csstoken.value, rel: ' '))
+          of QUERY_VALUE:
+            state.getLastSel().value = $csstoken.value
+            break
+          else: discard
+        of CSS_STRING_TOKEN:
+          case state.query
+          of QUERY_VALUE:
+            state.getLastSel().value = $csstoken.value
+            break
+          else: discard
+        of CSS_DELIM_TOKEN:
+          case csstoken.rvalue
+          of Rune('~'), Rune('|'), Rune('^'), Rune('$'), Rune('*'):
+            if state.query == QUERY_DELIM:
+              state.getLastSel().rel = char(csstoken.rvalue)
+          of Rune('='):
+            if state.query == QUERY_DELIM:
+              if state.getLastSel().rel == ' ':
+                state.getLastSel().rel = '='
+              state.query = QUERY_VALUE
+          else: discard
+        else: discard
+    state.query = QUERY_TYPE
+  else: discard
+
+proc parseSelectorFunction(state: var SelectorParser, cssfunction: CSSFunction) =
+  case $cssfunction.name
+  of "not", "is":
+    if state.query != QUERY_PSEUDO:
+      return
+    state.query = QUERY_TYPE
+  of "nth-child":
+    if state.query != QUERY_PSEUDO:
+      return
+    if cssfunction.value.len != 1 or not (cssfunction.value[0] of CSSToken):
+      return
+    if CSSToken(cssfunction.value[0]).tokenType != CSS_NUMBER_TOKEN:
+      return
+    let num = CSSToken(cssfunction.value[0]).nvalue
+    if num == float64(int64(num)):
+      state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_NTH_CHILD, pseudonum: num))
+    state.query = QUERY_TYPE
+    return
+  else: return
+  var fun = Selector(t: FUNC_SELECTOR, name: $cssfunction.name)
+  state.addSelector(fun)
+
+  let osels = state.selectors
+  let ocomb = state.combinator
+  state.combinator = nil
+  state.selectors = newSeq[SelectorList]()
+  state.addSelectorList()
+  for cval in cssfunction.value:
+    if cval of CSSToken:
+      state.parseSelectorToken(CSSToken(cval))
+    elif cval of CSSSimpleBlock:
+      state.parseSelectorSimpleBlock(CSSSimpleBlock(cval))
+    elif cval of CSSFunction:
+      state.parseSelectorFunction(CSSFunction(cval))
+  fun.fsels = state.selectors
+  state.selectors = osels
+  state.combinator = ocomb
+
+func parseSelectors*(cvals: seq[CSSComponentValue]): seq[SelectorList] =
+  var state = SelectorParser()
+  state.addSelectorList()
+  for cval in cvals:
+    if cval of CSSToken:
+      state.parseSelectorToken(CSSToken(cval))
+    elif cval of CSSSimpleBlock:
+      state.parseSelectorSimpleBlock(CSSSimpleBlock(cval))
+    elif cval of CSSFunction:
+      state.parseSelectorFunction(CSSFunction(cval))
+  if state.combinator != nil:
+    state.selectors[^1].add(state.combinator)
+    state.combinator = nil
+  return state.selectors