Skip to content

Commit

Permalink
Merge pull request #259 from facelessuser/enhance/has-forgive
Browse files Browse the repository at this point in the history
:has() should not have any kind of forgiving behavior
  • Loading branch information
facelessuser authored Feb 10, 2023
2 parents 792d566 + 02ca817 commit 72af948
Show file tree
Hide file tree
Showing 6 changed files with 17 additions and 96 deletions.
8 changes: 3 additions & 5 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
# Changelog

## 2.5
## 2.4

- **NEW**: Update to support changes related to `:lang()` in the official CSS spec. `:lang("")` should match unspecified
languages, e.g. `lang=""`, but not `lang=und`.

## 2.4

- **NEW**: `:nth-child()` and `:nth-last-child()` will forgive irregular comma usage.
- **NEW**: Only `:is()` and `:where()` should allow forgiving selector lists according to latest CSS (as far as Soup
Sieve supports "forgiving" which is limited to empty selectors).
- **NEW**: Formally drop Python 3.6.

## 2.3.2.post1
Expand Down
2 changes: 1 addition & 1 deletion soupsieve/__meta__.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,5 +193,5 @@ def parse_version(ver: str) -> Version:
return Version(major, minor, micro, release, pre, post, dev)


__version_info__ = Version(2, 5, 0, "final", post=1)
__version_info__ = Version(2, 4, 0, "final")
__version__ = __version_info__._get_canonical()
31 changes: 8 additions & 23 deletions soupsieve/css_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ def parse_pseudo_nth(
if postfix == '_child':
if m.group('of'):
# Parse the rest of `of S`.
nth_sel = self.parse_selectors(iselector, m.end(0), FLG_PSEUDO | FLG_OPEN | FLG_FORGIVE)
nth_sel = self.parse_selectors(iselector, m.end(0), FLG_PSEUDO | FLG_OPEN)
else:
# Use default `*|*` for `of S`.
nth_sel = CSS_NTH_OF_S_DEFAULT
Expand Down Expand Up @@ -753,7 +753,7 @@ def parse_pseudo_open(
if name == ':not':
flags |= FLG_NOT
elif name == ':has':
flags |= FLG_RELATIVE | FLG_FORGIVE
flags |= FLG_RELATIVE
elif name in (':where', ':is'):
flags |= FLG_FORGIVE

Expand All @@ -777,11 +777,6 @@ def parse_has_combinator(
if not combinator:
combinator = WS_COMBINATOR
if combinator == COMMA_COMBINATOR:
if not has_selector:
# If we've not captured any selector parts, the comma is either at the beginning of the pattern
# or following another comma, both of which are unexpected. But shouldn't fail the pseudo-class.
sel.no_match = True

sel.rel_type = rel_type
selectors[-1].relations.append(sel)
rel_type = ":" + WS_COMBINATOR
Expand Down Expand Up @@ -1070,22 +1065,12 @@ def parse_selectors(
selectors.append(sel)

# Forgive empty slots in pseudo-classes that have lists (and are forgiving)
elif is_forgive:
if is_relative:
# Handle relative selectors pseudo-classes with empty slots like `:has()`
if selectors and selectors[-1].rel_type is None and rel_type == ': ':
sel.rel_type = rel_type
sel.no_match = True
selectors[-1].relations.append(sel)
has_selector = True
else:
# Handle normal pseudo-classes with empty slots
if not selectors or not relations:
# Others like `:is()` etc.
sel.no_match = True
del relations[:]
selectors.append(sel)
has_selector = True
elif is_forgive and (not selectors or not relations):
# Handle normal pseudo-classes with empty slots like `:is()` etc.
sel.no_match = True
del relations[:]
selectors.append(sel)
has_selector = True

if not has_selector:
# We will always need to finish a selector when `:has()` is used as it leads with combining.
Expand Down
35 changes: 5 additions & 30 deletions tests/test_level4/test_has.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,45 +129,20 @@ def test_has_nested_pseudo(self):
flags=util.HTML
)

def test_has_empty(self):
"""Test has with empty slot due to multiple commas."""
def test_has_no_match(self):
"""Test has with a non-matching selector."""

self.assert_selector(
self.MARKUP2,
'div:has()',
'div:has(:paused)',
[],
flags=util.HTML
)

def test_has_multi_commas(self):
def test_has_empty(self):
"""Test has with empty slot due to multiple commas."""

self.assert_selector(
self.MARKUP2,
'div:has(> .bbbb, .ffff, , .jjjj)',
['0', '4', '8'],
flags=util.HTML
)

def test_has_leading_commas(self):
"""Test has with empty slot due to leading commas."""

self.assert_selector(
self.MARKUP2,
'div:has(, > .bbbb, .ffff, .jjjj)',
['0', '4', '8'],
flags=util.HTML
)

def test_has_trailing_commas(self):
"""Test has with empty slot due to trailing commas."""

self.assert_selector(
self.MARKUP2,
'div:has(> .bbbb, .ffff, .jjjj, )',
['0', '4', '8'],
flags=util.HTML
)
self.assert_raises('div:has()', SelectorSyntaxError)

def test_invalid_incomplete_has(self):
"""Test `:has()` fails with just a combinator."""
Expand Down
10 changes: 0 additions & 10 deletions tests/test_level4/test_nth_child.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,3 @@ def test_nth_child_of_s_complex(self):
['2', '6', '10'],
flags=util.HTML
)

def test_nth_child_forgive(self):
"""Test that `nth` child forgives bad commas."""

self.assert_selector(
self.MARKUP,
":nth-child(2n + 1 OF p.test,, span.test,)",
['2', '6', '10'],
flags=util.HTML
)
27 changes: 0 additions & 27 deletions tests/test_level4/test_nth_last_child.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,3 @@ def test_nth_child_of_s_complex(self):
['1', '3', '5', '7', '9', '11'],
flags=util.HTML
)

def test_nth_last_child_forgive(self):
"""Test `nth` last child forgives irregular commas."""

markup = """
<body>
<p id="0"></p>
<p id="1"></p>
<span id="2"></span>
<span id="3"></span>
<span id="4"></span>
<span id="5"></span>
<span id="6"></span>
<p id="7"></p>
<p id="8"></p>
<p id="9"></p>
<p id="10"></p>
<span id="11"></span>
</body>
"""

self.assert_selector(
markup,
":nth-last-child(2n + 1 of ,p[id],, span[id],)",
['1', '3', '5', '7', '9', '11'],
flags=util.HTML
)

0 comments on commit 72af948

Please sign in to comment.