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