import std/options
import std/strutils
import css/cssparser
import html/catom
import utils/twtstr
type
SelectorType* = enum
stType, stId, stAttr, stClass, stUniversal, stPseudoClass, stPseudoElement
PseudoElem* = enum
peNone, peBefore, peAfter,
# internal
peInputText, peTextareaText, peImage, peNewline, peVideo, peAudio, peCanvas
PseudoClass* = enum
pcFirstChild, pcLastChild, pcOnlyChild, pcHover, pcRoot, pcNthChild,
pcNthLastChild, pcChecked, pcFocus, pcIs, pcNot, pcWhere, pcLang, pcLink,
pcVisited
CombinatorType* = enum
ctNone, ctDescendant, ctChild, ctNextSibling, ctSubsequentSibling
SelectorParser = object
selectors: seq[ComplexSelector]
cvals: seq[CSSComponentValue]
at: int
failed: bool
factory: CAtomFactory
RelationType* {.size: sizeof(int) div 2.} = enum
rtExists, rtEquals, rtToken, rtBeginDash,
rtStartsWith, rtEndsWith, rtContains
RelationFlag* {.size: sizeof(int) div 2.} = enum
rfNone, rfI, rfS
SelectorRelation* = object
t*: RelationType
flag*: RelationFlag
Selector* = ref object # Simple selector
case t*: SelectorType
of stType:
tag*: CAtom
when defined(debug):
tags: string
of stId:
id*: CAtom
when defined(debug):
ids: string
of stClass:
class*: CAtom
when defined(debug):
classs: string
of stAttr:
attr*: CAtom
when defined(debug):
attrs: string
value*: string
rel*: SelectorRelation
of stUniversal: #TODO namespaces?
discard
of stPseudoClass:
pseudo*: PseudoData
of stPseudoElement:
elem*: PseudoElem
PseudoData* = object
case t*: PseudoClass
of pcNthChild, pcNthLastChild:
anb*: CSSAnB
ofsels*: SelectorList
of pcIs, pcWhere, pcNot:
fsels*: SelectorList
of pcLang:
s*: string
else: discard
CompoundSelector* = object
ct*: CombinatorType # relation to the next entry in a ComplexSelector.
sels*: seq[Selector]
ComplexSelector* = seq[CompoundSelector]
SelectorList* = seq[ComplexSelector]
iterator items*(sels: CompoundSelector): Selector {.inline.} =
for it in sels.sels:
yield it
func `[]`*(sels: CompoundSelector; i: int): Selector {.inline.} =
return sels.sels[i]
func `[]`*(sels: CompoundSelector; i: BackwardsIndex): Selector {.inline.} =
return sels.sels[i]
func len*(sels: CompoundSelector): int {.inline.} =
return sels.sels.len
proc add*(sels: var CompoundSelector; sel: Selector) {.inline.} =
sels.sels.add(sel)
# For debugging
func tostr(ftype: enum): string =
return ($ftype).split('_')[1..^1].join('-').toLowerAscii()
func `$`*(cxsel: ComplexSelector): string
func `$`*(sel: Selector): string =
case sel.t
of stType:
when defined(debug):
return sel.tags
else:
return "ATOM" & $int(sel.tag)
of stId:
when defined(debug):
return "#" & sel.ids
else:
return "#ATOM" & $int(sel.id)
of stAttr:
let rel = case sel.rel.t
of rtExists: ""
of rtEquals: "="
of rtToken: "~="
of rtBeginDash: "|="
of rtStartsWith: "^="
of rtEndsWith: "$="
of rtContains: "*="
let flag = case sel.rel.flag
of rfNone: ""
of rfI: " i"
of rfS: " s"
let attrs = when defined(debug):
sel.attrs
else:
"ATOM" & $int(sel.attr)
return '[' & attrs & rel & sel.value & flag & ']'
of stClass:
when defined(debug):
return "." & sel.classs
else:
return ".ATOM" & $int(sel.id)
of stUniversal:
return "*"
of stPseudoClass:
result = ':' & sel.pseudo.t.tostr()
case sel.pseudo.t
of pcIs, pcNot, pcWhere:
result &= '('
for fsel in sel.pseudo.fsels:
result &= $fsel
if fsel != sel.pseudo.fsels[^1]:
result &= ", "
result &= ')'
of pcNthChild, pcNthLastChild:
result &= '(' & $sel.pseudo.anb.A & 'n' & $sel.pseudo.anb.B
if sel.pseudo.ofsels.len != 0:
result &= " of "
for fsel in sel.pseudo.ofsels:
result &= $fsel
if fsel != sel.pseudo.ofsels[^1]:
result &= ','
result &= ')'
else: discard
of stPseudoElement:
return "::" & sel.elem.tostr()
func `$`*(sels: CompoundSelector): string =
for sel in sels:
result &= $sel
func `$`*(cxsel: ComplexSelector): string =
for sels in cxsel:
result &= $sels
case sels.ct
of ctDescendant: result &= ' '
of ctChild: result &= " > "
of ctNextSibling: result &= " + "
of ctSubsequentSibling: result &= " ~ "
of ctNone: discard
func `$`*(slist: SelectorList): string =
var s = false
for cxsel in slist:
if s:
result &= ", "
result &= $cxsel
s = true
func getSpecificity*(cxsel: ComplexSelector): int
func getSpecificity(sel: Selector): int =
case sel.t
of stId:
result += 1000000
of stClass, stAttr:
result += 1000
of stPseudoClass:
case sel.pseudo.t
of pcIs, pcNot:
var best = 0
for child in sel.pseudo.fsels:
let s = getSpecificity(child)
if s > best:
best = s
result += best
of pcNthChild, pcNthLastChild:
if sel.pseudo.ofsels.len != 0:
var best = 0
for child in sel.pseudo.ofsels:
let s = getSpecificity(child)
if s > best:
best = s
result += best
result += 1000
of pcWhere: discard
else: result += 1000
of stType, stPseudoElement:
result += 1
of stUniversal:
discard
func getSpecificity*(sels: CompoundSelector): int =
for sel in sels:
result += getSpecificity(sel)
func getSpecificity*(cxsel: ComplexSelector): int =
for sels in cxsel:
result += getSpecificity(sels)
func pseudo*(cxsel: ComplexSelector): PseudoElem =
if cxsel[^1][^1].t == stPseudoElement:
return cxsel[^1][^1].elem
return peNone
proc consume(state: var SelectorParser): CSSComponentValue =
result = state.cvals[state.at]
inc state.at
proc has(state: var SelectorParser; i = 0): bool =
return not state.failed and state.at + i < state.cvals.len
proc peek(state: var SelectorParser; i = 0): CSSComponentValue =
return state.cvals[state.at + i]
template fail() =
state.failed = true
return
template get_tok(cval: CSSComponentValue): CSSToken =
let c = cval
if not (c of CSSToken): fail
CSSToken(c)
proc parseSelectorList(cvals: seq[CSSComponentValue]; factory: CAtomFactory):
SelectorList
# Functions that may contain other selectors, functions, etc.
proc parseRecursiveSelectorFunction(state: var SelectorParser;
class: PseudoClass; body: seq[CSSComponentValue]): Selector =
var fun = Selector(
t: stPseudoClass,
pseudo: PseudoData(t: class),
)
fun.pseudo.fsels = parseSelectorList(body, state.factory)
if fun.pseudo.fsels.len == 0: fail
return fun
proc parseNthChild(state: var SelectorParser; cssfunction: CSSFunction;
data: PseudoData): Selector =
var data = data
var (anb, i) = parseAnB(cssfunction.value)
if anb.isNone: fail
data.anb = anb.get
var nthchild = Selector(t: stPseudoClass, pseudo: data)
while i < cssfunction.value.len and cssfunction.value[i] == cttWhitespace:
inc i
if i >= cssfunction.value.len:
return nthchild
let lasttok = get_tok cssfunction.value[i]
if lasttok.tokenType != cttIdent or not lasttok.value.equalsIgnoreCase("of"):
fail
if i == cssfunction.value.len: fail
nthchild.pseudo.ofsels = cssfunction.value[i..^1]
.parseSelectorList(state.factory)
if nthchild.pseudo.ofsels.len == 0: fail
return nthchild
proc skipWhitespace(state: var SelectorParser) =
while state.has() and state.peek() of CSSToken and
CSSToken(state.peek()).tokenType == cttWhitespace:
inc state.at
proc parseLang(cvals: seq[CSSComponentValue]): Selector =
var state = SelectorParser(cvals: cvals)
state.skipWhitespace()
if not state.has(): fail
let tok = get_tok state.consume()
if tok.tokenType != cttIdent: fail
return Selector(t: stPseudoClass, pseudo: PseudoData(t: pcLang, s: tok.value))
proc parseSelectorFunction(state: var SelectorParser; cssfunction: CSSFunction):
Selector =
return case cssfunction.name.toLowerAscii()
of "not":
state.parseRecursiveSelectorFunction(pcNot, cssfunction.value)
of "is":
state.parseRecursiveSelectorFunction(pcIs, cssfunction.value)
of "where":
state.parseRecursiveSelectorFunction(pcWhere, cssfunction.value)
of "nth-child":
state.parseNthChild(cssfunction, PseudoData(t: pcNthChild))
of "nth-last-child":
state.parseNthChild(cssfunction, PseudoData(t: pcNthLastChild))
of "lang":
parseLang(cssfunction.value)
else: fail
proc parsePseudoSelector(state: var SelectorParser): Selector =
if not state.has(): fail
let cval = state.consume()
if cval of CSSToken:
template add_pseudo_element(element: PseudoElem) =
return Selector(t: stPseudoElement, elem: element)
let tok = CSSToken(cval)
case tok.tokenType
of cttIdent:
template add_pseudo_class(class: PseudoClass) =
return Selector(t: stPseudoClass, pseudo: PseudoData(t: class))
case tok.value.toLowerAscii()
of "before": add_pseudo_element peBefore
of "after": add_pseudo_element peAfter
of "first-child": add_pseudo_class pcFirstChild
of "last-child": add_pseudo_class pcLastChild
of "only-child": add_pseudo_class pcOnlyChild
of "hover": add_pseudo_class pcHover
of "root": add_pseudo_class pcRoot
of "checked": add_pseudo_class pcChecked
of "focus": add_pseudo_class pcFocus
of "link": add_pseudo_class pcLink
of "visited": add_pseudo_class pcVisited
else: fail
of cttColon:
if not state.has(): fail
let tok = get_tok state.consume()
if tok.tokenType != cttIdent: fail
case tok.value.toLowerAscii()
of "before": add_pseudo_element peBefore
of "after": add_pseudo_element peAfter
else: fail
else: fail
elif cval of CSSFunction:
return state.parseSelectorFunction(CSSFunction(cval))
else: fail
proc parseComplexSelector(state: var SelectorParser): ComplexSelector
proc parseAttributeSelector(state: var SelectorParser;
cssblock: CSSSimpleBlock): Selector =
if cssblock.token.tokenType != cttLbracket: fail
var state2 = SelectorParser(cvals: cssblock.value)
state2.skipWhitespace()
if not state2.has(): fail
let attr = get_tok state2.consume()
if attr.tokenType != cttIdent: fail
state2.skipWhitespace()
if not state2.has():
return Selector(
t: stAttr,
attr: state.factory.toAtom(attr.value),
rel: SelectorRelation(t: rtExists)
)
let delim = get_tok state2.consume()
if delim.tokenType != cttDelim: fail
let rel = case delim.cvalue
of '~': rtToken
of '|': rtBeginDash
of '^': rtStartsWith
of '$': rtEndsWith
of '*': rtContains
of '=': rtEquals
else: fail
if rel != rtEquals:
let delim = get_tok state2.consume()
if delim.tokenType != cttDelim or delim.cvalue != '=': fail
state2.skipWhitespace()
if not state2.has(): fail
let value = get_tok state2.consume()
if value.tokenType notin {cttIdent, cttString}: fail
state2.skipWhitespace()
var flag = rfNone
if state2.has():
let delim = get_tok state2.consume()
if delim.tokenType != cttIdent: fail
if delim.value.equalsIgnoreCase("i"):
flag = rfI
elif delim.value.equalsIgnoreCase("s"):
flag = rfS
return Selector(
t: stAttr,
attr: state.factory.toAtom(attr.value),
value: value.value,
rel: SelectorRelation(
t: rel,
flag: flag
)
)
proc parseClassSelector(state: var SelectorParser): Selector =
if not state.has(): fail
let tok = get_tok state.consume()
if tok.tokenType != cttIdent: fail
let class = state.factory.toAtom(tok.value)
result = Selector(t: stClass, class: class)
when defined(debug):
result.classs = tok.value
proc parseCompoundSelector(state: var SelectorParser): CompoundSelector =
result = CompoundSelector()
while state.has():
let cval = state.peek()
if cval of CSSToken:
let tok = CSSToken(cval)
case tok.tokenType
of cttIdent:
inc state.at
let s = tok.value.toLowerAscii()
let tag = state.factory.toAtom(s)
let sel = Selector(t: stType, tag: tag)
when defined(debug):
sel.tags = s
result.add(sel)
of cttColon:
inc state.at
result.add(state.parsePseudoSelector())
of cttHash:
inc state.at
let id = state.factory.toAtom(tok.value)
result.add(Selector(t: stId, id: id))
when defined(debug):
result[^1].ids = tok.value
of cttComma: break
of cttDelim:
case tok.cvalue
of '.':
inc state.at
result.add(state.parseClassSelector())
of '*':
inc state.at
result.add(Selector(t: stUniversal))
of '>', '+', '~': break
else: fail
of cttWhitespace:
# skip trailing whitespace
if not state.has(1) or state.peek(1) == cttComma:
inc state.at
elif state.peek(1) == cttDelim:
let tok = CSSToken(state.peek(1))
if tok.cvalue in {'>', '+', '~'}:
inc state.at
break
else: fail
elif cval of CSSSimpleBlock:
inc state.at
result.add(state.parseAttributeSelector(CSSSimpleBlock(cval)))
else:
fail
proc parseComplexSelector(state: var SelectorParser): ComplexSelector =
while true:
state.skipWhitespace()
let sels = state.parseCompoundSelector()
result.add(sels)
if sels.len == 0: fail
if not state.has():
break # finish
let tok = get_tok state.consume()
case tok.tokenType
of cttDelim:
case tok.cvalue
of '>': result[^1].ct = ctChild
of '+': result[^1].ct = ctNextSibling
of '~': result[^1].ct = ctSubsequentSibling
else: fail
of cttWhitespace:
result[^1].ct = ctDescendant
of cttComma:
break # finish
else: fail
if result.len == 0 or result[^1].ct != ctNone:
fail
proc parseSelectorList(cvals: seq[CSSComponentValue]; factory: CAtomFactory):
SelectorList =
var state = SelectorParser(cvals: cvals, factory: factory)
var res: SelectorList = @[]
while state.has():
res.add(state.parseComplexSelector())
if not state.failed:
return res
proc parseSelectors*(cvals: seq[CSSComponentValue]; factory: CAtomFactory):
seq[ComplexSelector] =
return parseSelectorList(cvals, factory)
proc parseSelectors*(ibuf: string; factory: CAtomFactory):
seq[ComplexSelector] =
return parseSelectors(parseComponentValues(ibuf), factory)