Skip to content

Commit 920b3d6

Browse files
committed
Change "<>" selector to ":scope"
1 parent 37b3c0f commit 920b3d6

File tree

3 files changed

+40
-48
lines changed

3 files changed

+40
-48
lines changed

cssselect/parser.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -400,22 +400,9 @@ def parse_simple_selector(stream, inside_negation=False):
400400
stream.skip_whitespace()
401401
selector_start = len(stream.used)
402402
peek = stream.peek()
403-
if peek.type == 'IDENT' or peek == ('DELIM', '*') or peek == ('DELIM', '<'):
403+
if peek.type == 'IDENT' or peek == ('DELIM', '*'):
404404
if peek.type == 'IDENT':
405405
namespace = stream.next().value
406-
elif peek == ('DELIM', '<'):
407-
if not (len(stream.used) == 0 or
408-
(len(stream.used) == 1 and stream.used[0].type == 'S')):
409-
raise SelectorSyntaxError(
410-
'Got immediate child pseudo-element "<>" not at the start of a selector'
411-
)
412-
namespace = stream.next().value
413-
stream.skip_whitespace()
414-
peek = stream.peek()
415-
if not peek == ('DELIM', '>'):
416-
raise SelectorSyntaxError(
417-
'Got incomplete immediate child pseudo-element "<>" (no ">")'
418-
)
419406
else:
420407
stream.next()
421408
namespace = None
@@ -465,6 +452,13 @@ def parse_simple_selector(stream, inside_negation=False):
465452
continue
466453
if stream.peek() != ('DELIM', '('):
467454
result = Pseudo(result, ident)
455+
if result.ident == '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')
468462
continue
469463
stream.next()
470464
stream.skip_whitespace()

cssselect/xpath.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -220,18 +220,7 @@ def selector_to_xpath(self, selector, prefix='descendant-or-self::',
220220
assert isinstance(xpath, self.xpathexpr_cls) # help debug a missing 'return'
221221
if translate_pseudo_elements and selector.pseudo_element:
222222
xpath = self.xpath_pseudo_element(xpath, selector.pseudo_element)
223-
224-
unicode_xpath = _unicode(xpath)
225-
# CSS immediate children (CSS "<> div" to XPath "child::div" or "./div")
226-
# Works only at the start of a selector
227-
# Needed to get immediate children of a processed selector in Scrapy
228-
# product = response.css('.product')
229-
# name = product.css('<> div')
230-
child_re = r'^[ \t\r\n\f]*\<[ \t\r\n\f]*\/'
231-
if re.match(child_re, unicode_xpath):
232-
prefix = 'child::'
233-
unicode_xpath = re.sub(child_re, '', unicode_xpath)
234-
return (prefix or '') + unicode_xpath
223+
return (prefix or '') + _unicode(xpath)
235224

236225
def xpath_pseudo_element(self, xpath, pseudo_element):
237226
"""Translate a pseudo-element.
@@ -343,8 +332,6 @@ def xpath_element(self, selector):
343332
if not element:
344333
element = '*'
345334
safe = True
346-
if element == '<':
347-
safe = True
348335
else:
349336
safe = is_safe_name(element)
350337
if self.lower_case_element_names:
@@ -554,6 +541,14 @@ def xpath_lang_function(self, xpath, function):
554541
def xpath_root_pseudo(self, xpath):
555542
return xpath.add_condition("not(parent::*)")
556543

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+
557552
def xpath_first_child_pseudo(self, xpath):
558553
return xpath.add_condition('count(preceding-sibling::*) = 0')
559554

tests/test_cssselect.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -147,18 +147,19 @@ def parse_many(first, *others):
147147
'Negation[Element[div]:not(Class[Element[div].foo])]']
148148
assert parse_many('td ~ th') == [
149149
'CombinedSelector[Element[td] ~ Element[th]]']
150-
assert parse_many('<> foo') == [
151-
'CombinedSelector[Element[<] > Element[foo]]'
150+
assert parse_many(':scope > foo') == [
151+
'CombinedSelector[Pseudo[Element[*]:scope] > Element[foo]]'
152152
]
153-
assert parse_many('<> foo bar > div') == [
154-
'CombinedSelector[CombinedSelector[CombinedSelector[Element[<] > Element[foo]] '
155-
'<followed> Element[bar]] > Element[div]]'
153+
assert parse_many(':scope > foo bar > div') == [
154+
'CombinedSelector[CombinedSelector[CombinedSelector[Pseudo[Element[*]:scope] > '
155+
'Element[foo]] <followed> Element[bar]] > Element[div]]'
156156
]
157-
assert parse_many('<> #foo #bar') == [
158-
'CombinedSelector[CombinedSelector[Element[<] > Hash[Element[*]#foo]] '
159-
'<followed> Hash[Element[*]#bar]]'
157+
assert parse_many(':scope > #foo #bar') == [
158+
'CombinedSelector[CombinedSelector[Pseudo[Element[*]:scope] > '
159+
'Hash[Element[*]#foo]] <followed> Hash[Element[*]#bar]]'
160160
]
161161

162+
# TODO ADD TESTS
162163
def test_pseudo_elements(self):
163164
def parse_pseudo(css):
164165
result = []
@@ -179,6 +180,7 @@ def parse_one(css):
179180
assert parse_one('foo') == ('Element[foo]', None)
180181
assert parse_one('*') == ('Element[*]', None)
181182
assert parse_one(':empty') == ('Pseudo[Element[*]:empty]', None)
183+
assert parse_one(':scope') == ('Pseudo[Element[*]:scope]', None)
182184

183185
# Special cases for CSS 2.1 pseudo-elements
184186
assert parse_one(':BEfore') == ('Element[*]', 'before')
@@ -322,11 +324,9 @@ def get_error(css):
322324
"Got pseudo-element ::before inside :not() at 12")
323325
assert get_error(':not(:not(a))') == (
324326
"Got nested :not()")
325-
assert get_error('<> div <> header') == (
326-
'Got immediate child pseudo-element "<>" not at the start of a selector'
327+
assert get_error(':scope > div :scope header') == (
328+
'Got immediate child pseudo-element ":scope" not at the start of a selector'
327329
)
328-
assert get_error('< div p') == (
329-
'Got incomplete immediate child pseudo-element "<>" (no ">")')
330330
assert get_error('> div p') == ("Expected selector, got <DELIM '>' at 0>")
331331

332332
def test_translation(self):
@@ -501,8 +501,8 @@ def test_quoting(self):
501501
'''descendant-or-self::*[@aval = '"']''')
502502
assert css_to_xpath('*[aval=\'"""\']') == (
503503
'''descendant-or-self::*[@aval = '"""']''')
504-
assert css_to_xpath('<> div[dataimg="<testmessage>"]') == (
505-
"child::div[@dataimg = '<testmessage>']")
504+
assert css_to_xpath(':scope > div[dataimg="<testmessage>"]') == (
505+
"descendant-or-self::*[1]/div[@dataimg = '<testmessage>']")
506506

507507
def test_unicode_escapes(self):
508508
# \22 == '"' \20 == ' '
@@ -580,6 +580,7 @@ def xpath(css):
580580
assert xpath('::attr-href') == "descendant-or-self::*/@href"
581581
assert xpath('p img::attr(src)') == (
582582
"descendant-or-self::p/descendant-or-self::*/img/@src")
583+
assert xpath(':scope') == "descendant-or-self::*[1]"
583584

584585
def test_series(self):
585586
def series(css):
@@ -692,11 +693,11 @@ def pcss(main, *selectors, **kwargs):
692693
assert pcss(':lang("EN")', '*:lang(en-US)', html_only=True) == [
693694
'second-li', 'li-div']
694695
assert pcss(':lang("e")', html_only=True) == []
695-
assert pcss('<> div') == []
696-
assert pcss('<> body') == ['nil']
697-
assert pcss('<> body > div') == ['outer-div', 'foobar-div']
698-
assert pcss('<> head') == ['nil']
699-
assert pcss('<> html') == []
696+
assert pcss(':scope > div') == []
697+
assert pcss(':scope body') == ['nil']
698+
assert pcss(':scope body > div') == ['outer-div', 'foobar-div']
699+
assert pcss(':scope head') == ['nil']
700+
assert pcss(':scope html') == []
700701

701702
# --- nth-* and nth-last-* -------------------------------------
702703

@@ -878,7 +879,9 @@ def count(selector):
878879
assert count('div[class|=dialog]') == 50 # ? Seems right
879880
assert count('div[class!=madeup]') == 243 # ? Seems right
880881
assert count('div[class~=dialog]') == 51 # ? Seems right
881-
assert count('<> div') == 1
882+
assert count(':scope > div') == 1
883+
assert count(':scope > div > div[class=dialog]') == 1
884+
assert count(':scope > div div') == 242
882885

883886
XMLLANG_IDS = '''
884887
<test>

0 commit comments

Comments
 (0)