import options
import streams
import strutils
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, 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,
# internal
PSEUDO_INPUT_TEXT, PSEUDO_TEXTAREA_TEXT, PSEUDO_IMAGE, PSEUDO_NEWLINE
PseudoClass* = enum
PSEUDO_FIRST_CHILD, PSEUDO_LAST_CHILD, PSEUDO_ONLY_CHILD, PSEUDO_HOVER,
PSEUDO_ROOT, PSEUDO_NTH_CHILD, PSEUDO_NTH_LAST_CHILD, PSEUDO_CHECKED,
PSEUDO_FOCUS, PSEUDO_IS, PSEUDO_NOT, PSEUDO_WHERE, PSEUDO_LANG,
PSEUDO_LINK, PSEUDO_VISITED
CombinatorType* = enum
DESCENDANT_COMBINATOR, CHILD_COMBINATOR, NEXT_SIBLING_COMBINATOR,
SUBSEQ_SIBLING_COMBINATOR
SelectorParser = object
selectors: seq[ComplexSelector]
query: QueryMode
combinator: Selector
Selector* = ref object of RootObj # compound selector
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*: PseudoData
of PSELEM_SELECTOR:
elem*: PseudoElem
of COMBINATOR_SELECTOR:
ct*: CombinatorType
csels*: SelectorList
PseudoData* = object
case t*: PseudoClass
of PSEUDO_NTH_CHILD, PSEUDO_NTH_LAST_CHILD:
anb*: CSSAnB
ofsels*: Option[SelectorList]
of PSEUDO_IS, PSEUDO_WHERE, PSEUDO_NOT:
fsels*: SelectorList
of PSEUDO_LANG:
s*: string
else: discard
# Kind of an oversimplification, but the distinction between complex and
# compound selectors isn't too significant.
ComplexSelector* = seq[Selector]
SelectorList* = seq[ComplexSelector]
# For debugging
proc tostr(ftype: enum): string =
return ($ftype).split('_')[1..^1].join("-").tolower()
proc `$`*(sellist: ComplexSelector): string
proc `$`*(sel: Selector): string =
case sel.t
of TYPE_SELECTOR:
return tagName(sel.tag)
of ID_SELECTOR:
return '#' & sel.id
of ATTR_SELECTOR:
var rel = ""
if sel.rel == '=':
rel = "="
elif sel.rel == ' ':
discard
else:
rel = sel.rel & '='
return '[' & sel.attr & rel & sel.value & ']'
of CLASS_SELECTOR:
return '.' & sel.class
of UNIVERSAL_SELECTOR:
return "*"
of PSEUDO_SELECTOR:
result = ':' & sel.pseudo.t.tostr()
case sel.pseudo.t
of PSEUDO_IS, PSEUDO_NOT, PSEUDO_WHERE:
result &= '('
for fsel in sel.pseudo.fsels:
result &= $fsel
if fsel != sel.pseudo.fsels[^1]:
result &= ','
result &= ')'
of PSEUDO_NTH_CHILD, PSEUDO_NTH_LAST_CHILD:
result &= '(' & $sel.pseudo.anb.A & 'n' & $sel.pseudo.anb.B
if sel.pseudo.ofsels.issome:
result &= " of "
for fsel in sel.pseudo.ofsels.get:
result &= $fsel
if fsel != sel.pseudo.ofsels.get[^1]:
result &= ','
result &= ')'
else: discard
of PSELEM_SELECTOR:
return "::" & sel.elem.tostr()
of COMBINATOR_SELECTOR:
var delim: char
case sel.ct
of DESCENDANT_COMBINATOR: delim = ' '
of CHILD_COMBINATOR: delim = '>'
of NEXT_SIBLING_COMBINATOR: delim = '+'
of SUBSEQ_SIBLING_COMBINATOR: delim = '~'
for slist in sel.csels:
result &= $slist
if slist != sel.csels[^1]:
result &= delim
proc `$`*(sellist: ComplexSelector): string =
for sel in sellist:
result &= $sel
func getSpecificity*(sels: ComplexSelector): int
func getSpecificity(sel: Selector): int =
case sel.t
of ID_SELECTOR:
result += 1000000
of CLASS_SELECTOR, ATTR_SELECTOR:
result += 1000
of PSEUDO_SELECTOR:
case sel.pseudo.t
of PSEUDO_IS, PSEUDO_NOT:
var best = 0
for child in sel.pseudo.fsels:
let s = getSpecificity(child)
if s > best:
best = s
result += best
of PSEUDO_NTH_CHILD, PSEUDO_NTH_LAST_CHILD:
if sel.pseudo.ofsels.issome:
var best = 0
for child in sel.pseudo.ofsels.get:
let s = getSpecificity(child)
if s > best:
best = s
result += best
result += 1000
of PSEUDO_WHERE: discard
else: result += 1000
of TYPE_SELECTOR, PSELEM_SELECTOR:
result += 1
of UNIVERSAL_SELECTOR:
discard
of COMBINATOR_SELECTOR:
for child in sel.csels:
result += getSpecificity(child)
func getSpecificity*(sels: ComplexSelector): int =
for sel in sels:
result += getSpecificity(sel)
func pseudo*(sels: ComplexSelector): PseudoElem =
var sel = sels[^1]
while sel.t == COMBINATOR_SELECTOR:
sel = sel.csels[^1][^1]
if sel.t == PSELEM_SELECTOR:
return sel.elem
return PSEUDO_NONE
proc addSelector(state: var SelectorParser, sel: Selector) =
if state.combinator != nil:
state.combinator.csels[^1].add(sel)
else:
state.selectors[^1].add(sel)
proc getLastSel(state: SelectorParser): Selector =
if state.combinator != nil:
return state.combinator.csels[^1][^1]
else:
return state.selectors[^1][^1]
proc addComplexSelector(state: var SelectorParser) =
if state.combinator != nil:
state.selectors[^1].add(state.combinator)
state.combinator = nil
state.selectors.add(newSeq[Selector]())
func getComplexSelectors(state: var SelectorParser): seq[ComplexSelector] =
result = state.selectors
if state.combinator != nil:
result[^1].add(state.combinator)
proc parseSelectorCombinator(state: var SelectorParser, ct: CombinatorType, csstoken: CSSToken) =
if csstoken.tokenType notin {CSS_IDENT_TOKEN, CSS_HASH_TOKEN, CSS_COLON_TOKEN} and
(csstoken.tokenType != CSS_DELIM_TOKEN or csstoken.rvalue != Rune('.')):
return
if state.combinator != nil and state.combinator.ct != ct:
let nc = Selector(t: COMBINATOR_SELECTOR, ct: ct)
nc.csels.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(newSeq[Selector]())
state.selectors[^1].setLen(0)
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
template add_pseudo_element(element: PseudoElem) =
state.addSelector(Selector(t: PSELEM_SELECTOR, elem: element))
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:
template add_pseudo_class(class: PseudoClass) =
state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PseudoData(t: class)))
case csstoken.value
of "before":
add_pseudo_element PSEUDO_BEFORE
of "after":
add_pseudo_element PSEUDO_AFTER
of "first-child":
add_pseudo_class PSEUDO_FIRST_CHILD
of "last-child":
add_pseudo_class PSEUDO_LAST_CHILD
of "only-child":
add_pseudo_class PSEUDO_ONLY_CHILD
of "hover":
add_pseudo_class PSEUDO_HOVER
of "root":
add_pseudo_class PSEUDO_ROOT
of "checked":
add_pseudo_class PSEUDO_CHECKED
of "focus":
add_pseudo_class PSEUDO_FOCUS
of "link":
add_pseudo_class PSEUDO_LINK
of "visited":
add_pseudo_class PSEUDO_VISITED
of QUERY_PSELEM:
case csstoken.value
of "before":
add_pseudo_element PSEUDO_BEFORE
of "after":
add_pseudo_element 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.addComplexSelector()
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)
proc parseSelectorFunctionBody(state: var SelectorParser, body: seq[CSSComponentValue]): seq[ComplexSelector] =
let osels = state.selectors
let ocomb = state.combinator
state.combinator = nil
state.selectors = newSeq[ComplexSelector]()
state.addComplexSelector()
for cval in body:
if cval of CSSToken:
state.parseSelectorToken(CSSToken(cval))
elif cval of CSSSimpleBlock:
state.parseSelectorSimpleBlock(CSSSimpleBlock(cval))
elif cval of CSSFunction:
state.parseSelectorFunction(CSSFunction(cval))
result = state.getComplexSelectors()
state.selectors = osels
state.combinator = ocomb
proc parseNthChild(state: var SelectorParser, cssfunction: CSSFunction, data: PseudoData) =
var data = data
let (anb, i) = parseAnB(cssfunction.value)
if anb.issome:
data.anb = anb.get
var nthchild = Selector(t: PSEUDO_SELECTOR, pseudo: data)
var i = i
while i < cssfunction.value.len and cssfunction.value[i] == CSS_WHITESPACE_TOKEN:
inc i
if i >= cssfunction.value.len:
state.addSelector(nthchild)
else:
if cssfunction.value[i] == CSS_IDENT_TOKEN and CSSToken(cssfunction.value[i]).value == "of":
if i < cssfunction.value.len:
let body = cssfunction.value[i..^1]
let val = state.parseSelectorFunctionBody(body)
if val.len > 0:
nthchild.pseudo.ofsels = some(val)
state.addSelector(nthchild)
state.query = QUERY_TYPE
proc parseSelectorFunction(state: var SelectorParser, cssfunction: CSSFunction) =
if state.query != QUERY_PSEUDO:
return
let ftype = case cssfunction.name
of "not": PSEUDO_NOT
of "is": PSEUDO_IS
of "where": PSEUDO_WHERE
of "nth-child":
state.parseNthChild(cssfunction, PseudoData(t: PSEUDO_NTH_CHILD))
return
of "nth-last-child":
state.parseNthChild(cssfunction, PseudoData(t: PSEUDO_NTH_LAST_CHILD))
return
of "lang":
if cssfunction.value.len == 0: return
var i = 0
template tok: CSSComponentValue = cssfunction.value[i]
while i < cssfunction.value.len:
if tok != CSS_WHITESPACE_TOKEN: break
inc i
if i >= cssfunction.value.len: return
if tok != CSS_IDENT_TOKEN: return
state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PseudoData(t: PSEUDO_LANG, s: CSSToken(tok).value)))
return
else: return
state.query = QUERY_TYPE
var data = PseudoData(t: ftype)
var fun = Selector(t: PSEUDO_SELECTOR, pseudo: data)
state.addSelector(fun)
fun.pseudo.fsels = state.parseSelectorFunctionBody(cssfunction.value)
func parseSelectors*(cvals: seq[CSSComponentValue]): seq[ComplexSelector] = {.cast(noSideEffect).}:
var state = SelectorParser()
state.addComplexSelector()
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:
if state.combinator.csels.len == 1:
if state.combinator.ct == DESCENDANT_COMBINATOR:
# otherwise it's an invalid combinator
state.selectors[^1].add(state.combinator.csels[0])
else:
state.selectors.setLen(0)
elif state.combinator.csels[^1].len != 0:
state.selectors[^1].add(state.combinator)
state.combinator = nil
if state.selectors.len > 0 and state.selectors[^1].len == 0:
# invalid selector
state.selectors.setLen(0)
return state.selectors
proc parseSelectors*(stream: Stream): seq[ComplexSelector] =
return parseSelectors(parseListOfComponentValues(stream))