Skip to content

Commit 99bc54c

Browse files
authored
Merge pull request #87 from sortafreel/css-immediate-children
CSS immediate children (:scope selector)
2 parents cb7a7e2 + 4b96685 commit 99bc54c

File tree

4 files changed

+69
-5
lines changed

4 files changed

+69
-5
lines changed

cssselect/parser.py

+7
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,13 @@ def parse_simple_selector(stream, inside_negation=False):
452452
continue
453453
if stream.peek() != ('DELIM', '('):
454454
result = Pseudo(result, ident)
455+
if result.__repr__() == 'Pseudo[Element[*]:scope]':
456+
if not (len(stream.used) == 2 or
457+
(len(stream.used) == 3
458+
and stream.used[0].type == 'S')):
459+
raise SelectorSyntaxError(
460+
'Got immediate child pseudo-element ":scope" '
461+
'not at the start of a selector')
455462
continue
456463
stream.next()
457464
stream.skip_whitespace()

cssselect/xpath.py

+8
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,14 @@ def xpath_lang_function(self, xpath, function):
541541
def xpath_root_pseudo(self, xpath):
542542
return xpath.add_condition("not(parent::*)")
543543

544+
# CSS immediate children (CSS ":scope > div" to XPath "child::div" or "./div")
545+
# Works only at the start of a selector
546+
# Needed to get immediate children of a processed selector in Scrapy
547+
# for product in response.css('.product'):
548+
# description = product.css(':scope > div::text').get()
549+
def xpath_scope_pseudo(self, xpath):
550+
return xpath.add_condition("1")
551+
544552
def xpath_first_child_pseudo(self, xpath):
545553
return xpath.add_condition('count(preceding-sibling::*) = 0')
546554

docs/index.rst

+2
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,10 @@ in the Level 3 specification:
108108
* ``:not()`` accepts a *sequence of simple selectors*, not just single
109109
*simple selector*. For example, ``:not(a.important[rel])`` is allowed,
110110
even though the negation contains 3 *simple selectors*.
111+
* ``:scope`` allows to access immediate children of a selector: ``product.css(':scope > div::text')``, simillar to XPath ``child::div``. Must be used at the start of a selector. Simplified version of `level 4 reference`_.
111112

112113
.. _an early draft: http://www.w3.org/TR/2001/CR-css3-selectors-20011113/#content-selectors
114+
.. _level 4 reference: https://developer.mozilla.org/en-US/docs/Web/CSS/:scope
113115

114116
..
115117
The following claim was copied from lxml:

tests/test_cssselect.py

+52-5
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,20 @@ def parse_many(first, *others):
146146
'Negation[Element[div]:not(Class[Element[div].foo])]']
147147
assert parse_many('td ~ th') == [
148148
'CombinedSelector[Element[td] ~ Element[th]]']
149+
assert parse_many(':scope > foo') == [
150+
'CombinedSelector[Pseudo[Element[*]:scope] > Element[foo]]'
151+
]
152+
assert parse_many(' :scope > foo') == [
153+
'CombinedSelector[Pseudo[Element[*]:scope] > Element[foo]]'
154+
]
155+
assert parse_many(':scope > foo bar > div') == [
156+
'CombinedSelector[CombinedSelector[CombinedSelector[Pseudo[Element[*]:scope] > '
157+
'Element[foo]] <followed> Element[bar]] > Element[div]]'
158+
]
159+
assert parse_many(':scope > #foo #bar') == [
160+
'CombinedSelector[CombinedSelector[Pseudo[Element[*]:scope] > '
161+
'Hash[Element[*]#foo]] <followed> Hash[Element[*]#bar]]'
162+
]
149163

150164
def test_pseudo_elements(self):
151165
def parse_pseudo(css):
@@ -164,9 +178,16 @@ def parse_one(css):
164178
assert len(result) == 1
165179
return result[0]
166180

181+
def test_pseudo_repr(css):
182+
result = parse(css)
183+
assert len(result) == 1
184+
selector = result[0]
185+
return selector.parsed_tree.__repr__()
186+
167187
assert parse_one('foo') == ('Element[foo]', None)
168188
assert parse_one('*') == ('Element[*]', None)
169189
assert parse_one(':empty') == ('Pseudo[Element[*]:empty]', None)
190+
assert parse_one(':scope') == ('Pseudo[Element[*]:scope]', None)
170191

171192
# Special cases for CSS 2.1 pseudo-elements
172193
assert parse_one(':BEfore') == ('Element[*]', 'before')
@@ -190,11 +211,14 @@ def parse_one(css):
190211
'CombinedSelector[Hash[Element[lorem]#ipsum] ~ '
191212
'Pseudo[Attrib[Class[Hash[Element[a]#b].c][href]]:empty]]',
192213
'selection')
193-
194-
parse_pseudo('foo:before, bar, baz:after') == [
195-
('Element[foo]', 'before'),
196-
('Element[bar]', None),
197-
('Element[baz]', 'after')]
214+
assert parse_pseudo(':scope > div, foo bar') == [
215+
('CombinedSelector[Pseudo[Element[*]:scope] > Element[div]]', None),
216+
('CombinedSelector[Element[foo] <followed> Element[bar]]', None)
217+
]
218+
assert parse_pseudo('foo:before, bar, baz:after') == [
219+
('Element[foo]', 'before'), ('Element[bar]', None),
220+
('Element[baz]', 'after')
221+
]
198222

199223
# Special cases for CSS 2.1 pseudo-elements are ignored by default
200224
for pseudo in ('after', 'before', 'first-line', 'first-letter'):
@@ -211,6 +235,11 @@ def parse_one(css):
211235
self.assertRaises(ExpressionError, tr.selector_to_xpath, selector,
212236
translate_pseudo_elements=True)
213237

238+
# Special test for the unicode symbols and ':scope' element if check
239+
# Errors if use repr() instead of __repr__()
240+
assert test_pseudo_repr(u':fİrst-child') == u'Pseudo[Element[*]:fİrst-child]'
241+
assert test_pseudo_repr(':scope') == 'Pseudo[Element[*]:scope]'
242+
214243
def test_specificity(self):
215244
def specificity(css):
216245
selectors = parse(css)
@@ -310,6 +339,13 @@ def get_error(css):
310339
"Got pseudo-element ::before inside :not() at 12")
311340
assert get_error(':not(:not(a))') == (
312341
"Got nested :not()")
342+
assert get_error(':scope > div :scope header') == (
343+
'Got immediate child pseudo-element ":scope" not at the start of a selector'
344+
)
345+
assert get_error('div :scope header') == (
346+
'Got immediate child pseudo-element ":scope" not at the start of a selector'
347+
)
348+
assert get_error('> div p') == ("Expected selector, got <DELIM '>' at 0>")
313349

314350
def test_translation(self):
315351
def xpath(css):
@@ -483,6 +519,8 @@ def test_quoting(self):
483519
'''descendant-or-self::*[@aval = '"']''')
484520
assert css_to_xpath('*[aval=\'"""\']') == (
485521
'''descendant-or-self::*[@aval = '"""']''')
522+
assert css_to_xpath(':scope > div[dataimg="<testmessage>"]') == (
523+
"descendant-or-self::*[1]/div[@dataimg = '<testmessage>']")
486524

487525
def test_unicode_escapes(self):
488526
# \22 == '"' \20 == ' '
@@ -560,6 +598,7 @@ def xpath(css):
560598
assert xpath('::attr-href') == "descendant-or-self::*/@href"
561599
assert xpath('p img::attr(src)') == (
562600
"descendant-or-self::p/descendant-or-self::*/img/@src")
601+
assert xpath(':scope') == "descendant-or-self::*[1]"
563602

564603
def test_series(self):
565604
def series(css):
@@ -672,6 +711,11 @@ def pcss(main, *selectors, **kwargs):
672711
assert pcss(':lang("EN")', '*:lang(en-US)', html_only=True) == [
673712
'second-li', 'li-div']
674713
assert pcss(':lang("e")', html_only=True) == []
714+
assert pcss(':scope > div') == []
715+
assert pcss(':scope body') == ['nil']
716+
assert pcss(':scope body > div') == ['outer-div', 'foobar-div']
717+
assert pcss(':scope head') == ['nil']
718+
assert pcss(':scope html') == []
675719

676720
# --- nth-* and nth-last-* -------------------------------------
677721

@@ -853,6 +897,9 @@ def count(selector):
853897
assert count('div[class|=dialog]') == 50 # ? Seems right
854898
assert count('div[class!=madeup]') == 243 # ? Seems right
855899
assert count('div[class~=dialog]') == 51 # ? Seems right
900+
assert count(':scope > div') == 1
901+
assert count(':scope > div > div[class=dialog]') == 1
902+
assert count(':scope > div div') == 242
856903

857904
XMLLANG_IDS = '''
858905
<test>

0 commit comments

Comments
 (0)