diff options
author | bptato <nincsnevem662@gmail.com> | 2023-01-23 01:02:43 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2023-01-27 17:41:57 +0100 |
commit | 167dd67270d5a432c584b61f5a22281ca47017d9 (patch) | |
tree | 4603f59617718cf255ce0ef562e85c530ed0137f /src/css | |
parent | 11271f01439f2593a79501e83b13688e032fe7ed (diff) | |
download | chawan-167dd67270d5a432c584b61f5a22281ca47017d9.tar.gz |
WIP selector rewrite
pretty slow for some reason
Diffstat (limited to 'src/css')
-rw-r--r-- | src/css/match.nim | 155 | ||||
-rw-r--r-- | src/css/selectorparser.nim | 620 | ||||
-rw-r--r-- | src/css/sheet.nim | 37 | ||||
-rw-r--r-- | src/css/stylednode.nim | 4 |
4 files changed, 365 insertions, 451 deletions
diff --git a/src/css/match.nim b/src/css/match.nim index b17c690d..b05fd392 100644 --- a/src/css/match.nim +++ b/src/css/match.nim @@ -22,11 +22,11 @@ func attrSelectorMatches(elem: Element, sel: Selector): bool = of '*': return elem.attr(sel.attr).contains(sel.value) else: return false -func selectorsMatch*[T: Element|StyledNode](elem: T, selectors: ComplexSelector, felem: T = nil): bool +func selectorsMatch*[T: Element|StyledNode](elem: T, cxsel: ComplexSelector, felem: T = nil): bool -func selectorsMatch*[T: Element|StyledNode](elem: T, selectors: SelectorList, felem: T = nil): bool = - for slist in selectors: - if elem.selectorsMatch(slist, felem): +func selectorsMatch*[T: Element|StyledNode](elem: T, slist: SelectorList, felem: T = nil): bool = + for cxsel in slist: + if elem.selectorsMatch(cxsel, felem): return true return false @@ -43,7 +43,7 @@ func pseudoSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: return elem.hover of PSEUDO_ROOT: return elem == elem.document.html of PSEUDO_NTH_CHILD: - if sel.pseudo.ofsels.issome and not selem.selectorsMatch(sel.pseudo.ofsels.get, felem): + if sel.pseudo.ofsels.len != 0 and not selem.selectorsMatch(sel.pseudo.ofsels, felem): return false let A = sel.pseudo.anb.A # step let B = sel.pseudo.anb.B # start @@ -60,11 +60,11 @@ func pseudoSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: if A < 0: return (i - B) <= 0 and (i - B) mod A == 0 return (i - B) >= 0 and (i - B) mod A == 0 - if sel.pseudo.ofsels.isnone or child.selectorsMatch(sel.pseudo.ofsels.get, felem): + if sel.pseudo.ofsels.len == 0 or child.selectorsMatch(sel.pseudo.ofsels, felem): inc i return false of PSEUDO_NTH_LAST_CHILD: - if sel.pseudo.ofsels.issome and not selem.selectorsMatch(sel.pseudo.ofsels.get, felem): + if sel.pseudo.ofsels.len == 0 and not selem.selectorsMatch(sel.pseudo.ofsels, felem): return false let A = sel.pseudo.anb.A # step let B = sel.pseudo.anb.B # start @@ -81,7 +81,7 @@ func pseudoSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: if A < 0: return (i - B) <= 0 and (i - B) mod A == 0 return (i - B) >= 0 and (i - B) mod A == 0 - if sel.pseudo.ofsels.isnone or child.selectorsMatch(sel.pseudo.ofsels.get, felem): + if sel.pseudo.ofsels.len != 0 or child.selectorsMatch(sel.pseudo.ofsels, felem): inc i return false of PSEUDO_CHECKED: @@ -105,75 +105,6 @@ func pseudoSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: of PSEUDO_VISITED: return false -func combinatorSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T): bool = - let selem = elem - # combinator without at least two members makes no sense - # actually, combinators with more than two elements are a pretty bad idea - # too. TODO getting rid of them would simplify this function greatly - assert sel.csels.len > 1 - if selem.selectorsMatch(sel.csels[^1], felem): - var i = sel.csels.len - 2 - case sel.ct - of DESCENDANT_COMBINATOR: - when selem is StyledNode: - var e = elem.parent - else: - var e = elem.parentElement - while e != nil and i >= 0: - if e.selectorsMatch(sel.csels[i], felem): - dec i - when elem is StyledNode: - e = e.parent - else: - e = e.parentElement - of CHILD_COMBINATOR: - when elem is StyledNode: - var e = elem.parent - else: - var e = elem.parentElement - while e != nil and i >= 0: - if not e.selectorsMatch(sel.csels[i], felem): - return false - dec i - when elem is StyledNode: - e = e.parent - else: - e = e.parentElement - of NEXT_SIBLING_COMBINATOR: - var found = false - let parent = when elem is StyledNode: elem.parent - else: elem.parentElement - if parent == nil: return false - for child in parent.elementList_rev: - when elem is StyledNode: - if not child.isDomElement: continue - if found: - if not child.selectorsMatch(sel.csels[i], felem): - return false - dec i - if i < 0: - return true - if child == elem: - found = true - of SUBSEQ_SIBLING_COMBINATOR: - var found = false - let parent = when selem is StyledNode: selem.parent - else: elem.parentElement - if parent == nil: return false - for child in parent.elementList_rev: - when selem is StyledNode: - if not child.isDomElement: continue - if child == selem: - found = true - continue - if not found: continue - if child.selectorsMatch(sel.csels[i], felem): - dec i - if i < 0: - return true - return i == -1 - return false - func selectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T = nil): bool = let selem = elem when elem is StyledNode: @@ -193,21 +124,73 @@ func selectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T = n return true of UNIVERSAL_SELECTOR: return true - of COMBINATOR_SELECTOR: - return combinatorSelectorMatches(selem, sel, felem) + +func selectorsMatch[T: Element|StyledNode](elem: T, sels: CompoundSelector, felem: T): bool = + for sel in sels: + if not selectorMatches(elem, sel, felem): + return false + return true + +func complexSelectorMatches[T: Element|StyledNode](elem: T, cxsel: ComplexSelector, felem: T = nil): bool = + var e = elem + for i in countdown(cxsel.high, 0): + let sels = cxsel[i] + if e == nil: + return false + var match = false + case sels.ct + of NO_COMBINATOR: + match = e.selectorsMatch(sels, felem): + of DESCENDANT_COMBINATOR: + e = e.parentElement + while e != nil: + if e.selectorsMatch(sels, felem): + match = true + break + e = e.parentElement + of CHILD_COMBINATOR: + e = e.parentElement + if e != nil: + match = e.selectorsMatch(sels, felem) + of NEXT_SIBLING_COMBINATOR: + if e.parentElement == nil: return false + var found = false + for child in e.parentElement.elementList_rev: + when elem is StyledNode: + if not child.isDomElement: continue + if e == child: + found = true + continue + if found: + e = child + if not e.selectorsMatch(sels, felem): + return false + of SUBSEQ_SIBLING_COMBINATOR: + var found = false + if e.parentElement == nil: return false + for child in e.parentElement.elementList_rev: + when elem is StyledNode: + if not child.isDomElement: continue + if child == elem: + found = true + continue + if not found: continue + if child.selectorsMatch(sels, felem): + e = child + match = true + break + if not match: + return false + return true # WARNING for StyledNode, this has the side effect of modifying depends. #TODO make that an explicit flag or something, also get rid of the Element case -func selectorsMatch*[T: Element|StyledNode](elem: T, selectors: ComplexSelector, felem: T = nil): bool = - let felem = if felem != nil: +func selectorsMatch*[T: Element|StyledNode](elem: T, cxsel: ComplexSelector, felem: T = nil): bool = + var felem = if felem != nil: felem else: elem - - for sel in selectors: - if not selectorMatches(elem, sel, felem): - return false - return true + return elem.complexSelectorMatches(cxsel, felem) proc querySelectorAll(node: Node, q: string): seq[Element] = let selectors = parseSelectors(newStringStream(q)) diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim index b10c8f45..dad58671 100644 --- a/src/css/selectorparser.nim +++ b/src/css/selectorparser.nim @@ -9,13 +9,7 @@ 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, - QUERY_FAILED + UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR PseudoElem* = enum PSEUDO_NONE, PSEUDO_BEFORE, PSEUDO_AFTER, @@ -29,15 +23,16 @@ type PSEUDO_LINK, PSEUDO_VISITED CombinatorType* = enum - DESCENDANT_COMBINATOR, CHILD_COMBINATOR, NEXT_SIBLING_COMBINATOR, - SUBSEQ_SIBLING_COMBINATOR + NO_COMBINATOR, DESCENDANT_COMBINATOR, CHILD_COMBINATOR, + NEXT_SIBLING_COMBINATOR, SUBSEQ_SIBLING_COMBINATOR SelectorParser = object selectors: seq[ComplexSelector] - query: QueryMode - combinator: Selector + cvals: seq[CSSComponentValue] + at: int + failed: bool - Selector* = ref object # compound selector + Selector* = ref object # Simple selector case t*: SelectorType of TYPE_SELECTOR: tag*: TagType @@ -55,32 +50,47 @@ type 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] + ofsels*: 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] + 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("-").tolower() -func `$`*(sellist: ComplexSelector): string +func `$`*(cxsel: ComplexSelector): string func `$`*(sel: Selector): string = case sel.t @@ -109,37 +119,44 @@ func `$`*(sel: Selector): string = for fsel in sel.pseudo.fsels: result &= $fsel if fsel != sel.pseudo.fsels[^1]: - result &= ',' + 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: + if sel.pseudo.ofsels.len != 0: result &= " of " - for fsel in sel.pseudo.ofsels.get: + for fsel in sel.pseudo.ofsels: result &= $fsel - if fsel != sel.pseudo.ofsels.get[^1]: + if fsel != sel.pseudo.ofsels[^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 i in 0 ..< sel.csels.len: - result &= $sel.csels[i] - if i == 0 or i != sel.csels.high: - result &= delim - -func `$`*(sellist: ComplexSelector): string = - for sel in sellist: + +func `$`*(sels: CompoundSelector): string = + for sel in sels: result &= $sel -func getSpecificity*(sels: ComplexSelector): int +func `$`*(cxsel: ComplexSelector): string = + for sels in cxsel: + result &= $sels + case sels.ct + of DESCENDANT_COMBINATOR: result &= ' ' + of CHILD_COMBINATOR: result &= " > " + of NEXT_SIBLING_COMBINATOR: result &= " + " + of SUBSEQ_SIBLING_COMBINATOR: result &= " ~ " + of NO_COMBINATOR: 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 @@ -157,9 +174,9 @@ func getSpecificity(sel: Selector): int = best = s result += best of PSEUDO_NTH_CHILD, PSEUDO_NTH_LAST_CHILD: - if sel.pseudo.ofsels.isSome: + if sel.pseudo.ofsels.len != 0: var best = 0 - for child in sel.pseudo.ofsels.get: + for child in sel.pseudo.ofsels: let s = getSpecificity(child) if s > best: best = s @@ -171,325 +188,236 @@ func getSpecificity(sel: Selector): int = result += 1 of UNIVERSAL_SELECTOR: discard - of COMBINATOR_SELECTOR: - for child in sel.csels: - result += getSpecificity(child) -func getSpecificity*(sels: ComplexSelector): int = +func getSpecificity*(sels: CompoundSelector): 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 +func getSpecificity*(cxsel: ComplexSelector): int = + for sels in cxsel: + result += getSpecificity(sels) + +func pseudo*(cxsel: ComplexSelector): PseudoElem = + if cxsel[^1][^1].t == PSELEM_SELECTOR: + return cxsel[^1][^1].elem return PSEUDO_NONE -proc flushCombinator(state: var SelectorParser) = - 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.query = QUERY_FAILED - elif state.combinator.csels[^1].len != 0: - state.selectors[^1].add(state.combinator) - else: - state.query = QUERY_FAILED - state.combinator = nil - -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) = - state.flushCombinator() - state.selectors.add(newSeq[Selector]()) - -func getComplexSelectors(state: var SelectorParser): seq[ComplexSelector] = - state.flushCombinator() - return state.selectors - -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('.') and 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) = - if csstoken.tokenType == CSS_WHITESPACE_TOKEN: - # do not interpret whitespace before/after a combinator selector token - if state.query in {QUERY_CHILD_COMBINATOR, QUERY_NEXT_SIBLING_COMBINATOR, - QUERY_SUBSEQ_SIBLING_COMBINATOR}: - return - elif state.combinator != nil and state.combinator.csels[^1].len <= 1: - return - case state.query - of QUERY_DESC_COMBINATOR: - state.parseSelectorCombinator(DESCENDANT_COMBINATOR, csstoken) - of QUERY_CHILD_COMBINATOR: - state.parseSelectorCombinator(CHILD_COMBINATOR, csstoken) - of QUERY_NEXT_SIBLING_COMBINATOR: - state.parseSelectorCombinator(NEXT_SIBLING_COMBINATOR, csstoken) - of QUERY_SUBSEQ_SIBLING_COMBINATOR: - 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: +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]): SelectorList + +# Functions that may contain other selectors, functions, etc. +proc parseRecursiveSelectorFunction(state: var SelectorParser, class: PseudoClass, body: seq[CSSComponentValue]): Selector = + var fun = Selector( + t: PSEUDO_SELECTOR, + pseudo: PseudoData(t: class), + ) + fun.pseudo.fsels = parseSelectorList(body) + 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: PSEUDO_SELECTOR, pseudo: data) + while i < cssfunction.value.len and cssfunction.value[i] == CSS_WHITESPACE_TOKEN: + inc i + if i >= cssfunction.value.len: + return nthchild + if (get_tok cssfunction.value[i]).value != "of": fail + if i == cssfunction.value.len: fail + nthchild.pseudo.ofsels = parseSelectorList(cssfunction.value[i..^1]) + 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 == CSS_WHITESPACE_TOKEN: + 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 != CSS_IDENT_TOKEN: fail + return Selector(t: PSEUDO_SELECTOR, pseudo: PseudoData(t: PSEUDO_LANG, s: tok.value)) + +proc parseSelectorFunction(state: var SelectorParser, cssfunction: CSSFunction): Selector = + case cssfunction.name + of "not": return state.parseRecursiveSelectorFunction(PSEUDO_NOT, cssfunction.value) + of "is": return state.parseRecursiveSelectorFunction(PSEUDO_IS, cssfunction.value) + of "where": return state.parseRecursiveSelectorFunction(PSEUDO_WHERE, cssfunction.value) + of "nth-child": return state.parseNthChild(cssfunction, PseudoData(t: PSEUDO_NTH_CHILD)) + of "nth-last-child": return state.parseNthChild(cssfunction, PseudoData(t: PSEUDO_NTH_LAST_CHILD)) + of "lang": return 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: PSELEM_SELECTOR, elem: element) + let tok = CSSToken(cval) + case tok.tokenType + of CSS_IDENT_TOKEN: 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 - else: - state.query = QUERY_FAILED - return - of QUERY_PSELEM: - case csstoken.value - of "before": - add_pseudo_element PSEUDO_BEFORE - of "after": - add_pseudo_element PSEUDO_AFTER - else: - state.query = QUERY_FAILED - return - else: - state.query = QUERY_FAILED - return - 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: - state.query = QUERY_FAILED - return - of CSS_HASH_TOKEN: - state.addSelector(Selector(t: ID_SELECTOR, id: csstoken.value)) - of CSS_COMMA_TOKEN: - state.flushCombinator() - if state.selectors[^1].len > 0: - state.addComplexSelector() - of CSS_WHITESPACE_TOKEN: - if state.selectors[^1].len > 0 or state.combinator != nil and state.combinator.csels[^1].len > 0: - state.query = QUERY_DESC_COMBINATOR - of CSS_COLON_TOKEN: - if state.query == QUERY_PSEUDO: - state.query = QUERY_PSELEM - else: - state.query = QUERY_PSEUDO - else: - state.query = QUERY_FAILED - return - -proc parseSelectorSimpleBlock(state: var SelectorParser, cssblock: CSSSimpleBlock) = - if cssblock.token.tokenType != CSS_LBRACKET_TOKEN: - state.query = QUERY_FAILED - return - state.query = QUERY_ATTR - for cval in cssblock.value: + return Selector(t: PSEUDO_SELECTOR, pseudo: PseudoData(t: class)) + case tok.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 + else: fail + of CSS_COLON_TOKEN: + if not state.has(): fail + let tok = get_tok state.consume() + if tok.tokenType != CSS_IDENT_TOKEN: fail + case tok.value + of "before": add_pseudo_element PSEUDO_BEFORE + of "after": add_pseudo_element PSEUDO_AFTER + 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 != CSS_LBRACKET_TOKEN: fail + var state2 = SelectorParser(cvals: cssblock.value) + state2.skipWhitespace() + if not state2.has(): fail + let attr = get_tok state2.consume() + if attr.tokenType != CSS_IDENT_TOKEN: fail + state2.skipWhitespace() + if not state2.has(): return Selector(t: ATTR_SELECTOR, attr: attr.value, rel: ' ') + let delim0 = get_tok state2.consume() + if delim0.tokenType != CSS_DELIM_TOKEN: fail + case delim0.rvalue + of Rune('~'), Rune('|'), Rune('^'), Rune('$'), Rune('*'): + let delim1 = get_tok state2.consume() + if delim1.tokenType != CSS_DELIM_TOKEN: fail + of Rune('='): + discard + else: fail + state2.skipWhitespace() + if not state2.has(): fail + let value = get_tok state2.consume() + if value.tokenType notin {CSS_IDENT_TOKEN, CSS_STRING_TOKEN}: fail + return Selector(t: ATTR_SELECTOR, attr: attr.value, value: value.value, rel: cast[char](delim0.rvalue)) + +proc parseClassSelector(state: var SelectorParser): Selector = + if not state.has(): fail + let tok = get_tok state.consume() + if tok.tokenType != CSS_IDENT_TOKEN: fail + return Selector(t: CLASS_SELECTOR, class: tok.value) + +proc parseCompoundSelector(state: var SelectorParser): CompoundSelector = + while state.has(): + let cval = state.peek() if cval of CSSToken: - let csstoken = (CSSToken)cval - case csstoken.tokenType + let tok = CSSToken(cval) + case tok.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 + inc state.at + result.add(Selector(t: TYPE_SELECTOR, tag: tagType(tok.value))) + of CSS_COLON_TOKEN: + inc state.at + result.add(state.parsePseudoSelector()) + of CSS_HASH_TOKEN: + inc state.at + result.add(Selector(t: ID_SELECTOR, id: tok.value)) + of CSS_COMMA_TOKEN: break 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 - -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 state.query == QUERY_FAILED: - break - if cval of CSSToken: - state.parseSelectorToken(CSSToken(cval)) + case tok.rvalue + of Rune('.'): + inc state.at + result.add(state.parseClassSelector()) + of Rune('*'): + inc state.at + result.add(Selector(t: UNIVERSAL_SELECTOR)) + of Rune('>'), Rune('+'), Rune('~'): break + else: fail + of CSS_WHITESPACE_TOKEN: + # skip trailing whitespace + if not state.has(1) or state.peek(1) == CSS_COMMA_TOKEN: + inc state.at + elif state.peek(1) == CSS_DELIM_TOKEN: + let tok = CSSToken(state.peek(1)) + if tok.rvalue == Rune('>') or tok.rvalue == Rune('+') or tok.rvalue == Rune('~'): + inc state.at + break + else: fail elif cval of CSSSimpleBlock: - state.parseSelectorSimpleBlock(CSSSimpleBlock(cval)) - elif cval of CSSFunction: - state.parseSelectorFunction(CSSFunction(cval)) - if state.query == QUERY_FAILED: - state.selectors = @[] - state.combinator = nil - else: - 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) + inc state.at + result.add(state.parseAttributeSelector(CSSSimpleBlock(cval))) 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 parseLang(state: var SelectorParser, body: seq[CSSComponentValue]) = - if body.len == 0: - state.query = QUERY_FAILED - return - var i = 0 - template tok: CSSComponentValue = body[i] - while i < body.len: - if tok != CSS_WHITESPACE_TOKEN: break - inc i - if i >= body.len: - state.query = QUERY_FAILED - return - if tok != CSS_IDENT_TOKEN: return - state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PseudoData(t: PSEUDO_LANG, s: CSSToken(tok).value))) - -# Functions that may contain other selectors, functions, etc. -proc parseRecursiveSelectorFunction(state: var SelectorParser, class: PseudoClass, body: seq[CSSComponentValue]) = - state.query = QUERY_TYPE - var data = PseudoData(t: class) - var fun = Selector(t: PSEUDO_SELECTOR, pseudo: data) - state.addSelector(fun) - fun.pseudo.fsels = state.parseSelectorFunctionBody(body) - -proc parseSelectorFunction(state: var SelectorParser, cssfunction: CSSFunction) = - if state.query != QUERY_PSEUDO: - state.query = QUERY_FAILED - return - case cssfunction.name - of "not": - state.parseRecursiveSelectorFunction(PSEUDO_NOT, cssfunction.value) - of "is": - state.parseRecursiveSelectorFunction(PSEUDO_IS, cssfunction.value) - of "where": - state.parseRecursiveSelectorFunction(PSEUDO_WHERE, cssfunction.value) - of "nth-child": - state.parseNthChild(cssfunction, PseudoData(t: PSEUDO_NTH_CHILD)) - of "nth-last-child": - state.parseNthChild(cssfunction, PseudoData(t: PSEUDO_NTH_LAST_CHILD)) - of "lang": - state.parseLang(cssfunction.value) - else: - state.query = QUERY_FAILED + 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() + var cxsel: ComplexSelector + case tok.tokenType + of CSS_DELIM_TOKEN: + case tok.rvalue + of Rune('>'): result[^1].ct = CHILD_COMBINATOR + of Rune('+'): result[^1].ct = NEXT_SIBLING_COMBINATOR + of Rune('~'): result[^1].ct = SUBSEQ_SIBLING_COMBINATOR + else: fail + of CSS_WHITESPACE_TOKEN: + result[^1].ct = DESCENDANT_COMBINATOR + of CSS_COMMA_TOKEN: + break # finish + else: fail + if result.len == 0 or result[^1].ct != NO_COMBINATOR: + fail + +proc parseSelectorList(cvals: seq[CSSComponentValue]): SelectorList = + var state = SelectorParser(cvals: cvals) + var res: SelectorList + while state.has(): + res.add(state.parseComplexSelector()) + if not state.failed: + return res func parseSelectors*(cvals: seq[CSSComponentValue]): seq[ComplexSelector] = {.cast(noSideEffect).}: - var state = SelectorParser() - state.addComplexSelector() - for cval in cvals: - if state.query == QUERY_FAILED: - break - if cval of CSSToken: - state.parseSelectorToken(CSSToken(cval)) - elif cval of CSSSimpleBlock: - state.parseSelectorSimpleBlock(CSSSimpleBlock(cval)) - elif cval of CSSFunction: - state.parseSelectorFunction(CSSFunction(cval)) - state.flushCombinator() - if state.selectors.len > 0 and state.selectors[^1].len == 0: - state.query = QUERY_FAILED - if state.query == QUERY_FAILED: - return @[] - return state.selectors + return parseSelectorList(cvals) proc parseSelectors*(stream: Stream): seq[ComplexSelector] = return parseSelectors(parseListOfComponentValues(stream)) diff --git a/src/css/sheet.nim b/src/css/sheet.nim index 846f6b31..d1b47f1c 100644 --- a/src/css/sheet.nim +++ b/src/css/sheet.nim @@ -39,6 +39,19 @@ func newStylesheet*(cap: int): CSSStylesheet = 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: @@ -52,11 +65,6 @@ proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool = return true of ATTR_SELECTOR, PSELEM_SELECTOR, UNIVERSAL_SELECTOR: return false - of COMBINATOR_SELECTOR: - for sel in sel.csels[^1]: - if hashes.getSelectorIds(sel): - return true - return false of PSEUDO_SELECTOR: if sel.pseudo.t in {PSEUDO_IS, PSEUDO_WHERE}: # Basically just hash whatever the selectors have in common: @@ -71,18 +79,12 @@ proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool = var cancel_class = false var i = 0 if i < sel.pseudo.fsels.len: - let list = sel.pseudo.fsels[i] - for sel in list: - if hashes.getSelectorIds(sel): - break + hashes.getSelectorIds(sel.pseudo.fsels[i]) inc i while i < sel.pseudo.fsels.len: - let list = sel.pseudo.fsels[i] var nhashes: SelectorHashes - for sel in list: - if nhashes.getSelectorIds(sel): - break + 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: @@ -127,11 +129,8 @@ iterator gen_rules*(sheet: CSSStylesheet, tag: TagType, id: string, classes: seq proc add(sheet: var CSSStylesheet, rule: CSSRuleDef) = var hashes: SelectorHashes - for list in rule.sels: - for sel in list: - if hashes.getSelectorIds(sel): - break - + for cxsel in rule.sels: + hashes.getSelectorIds(cxsel) if hashes.tag != TAG_UNKNOWN: sheet.tag_table[hashes.tag].add(rule) elif hashes.id != "": @@ -171,7 +170,7 @@ proc getDeclarations(rule: CSSQualifiedRule): seq[CSSDeclaration] {.inline.} = proc addRule(stylesheet: var CSSStylesheet, rule: CSSQualifiedRule) = let sels = parseSelectors(rule.prelude) - if sels.len > 0 and sels[^1].len > 0: + if sels.len > 0: let r = CSSRuleDef(sels: sels, decls: rule.getDeclarations()) stylesheet.add(r) diff --git a/src/css/stylednode.nim b/src/css/stylednode.nim index c18c8779..0eac3c66 100644 --- a/src/css/stylednode.nim +++ b/src/css/stylednode.nim @@ -93,6 +93,10 @@ func findElement*(root: StyledNode, elem: Element): StyledNode = func isDomElement*(styledNode: StyledNode): bool {.inline.} = styledNode.t == STYLED_ELEMENT and styledNode.pseudo == PSEUDO_NONE +# DOM-style getters, for Element interoperability... +func parentElement*(node: StyledNode): StyledNode {.inline.} = + node.parent + func checked(element: Element): bool = if element.tagType == TAG_INPUT: let input = HTMLInputElement(element) |