Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interesting psuedo class to keep an eye on :in() #224

Open
facelessuser opened this issue Sep 8, 2021 · 8 comments · May be fixed by #228
Open

Interesting psuedo class to keep an eye on :in() #224

facelessuser opened this issue Sep 8, 2021 · 8 comments · May be fixed by #228
Labels
C: css-custom CSS custom selectors. P: maybe Pending approval of low priority request. skip-triage Tells bot to not tag a new issue with 'triage'. T: feature Feature.

Comments

@facelessuser
Copy link
Owner

https://drafts.csswg.org/css-cascade-6/#in-scope-selector

It would be way too early to expect that this gets implemented officially or that the spec wouldn't change right under us, but something to keep an eye on. It may be fun to play with to see how the code would actually look and how useful it is.

If I'm feeling adventurous, maybe implement it under something like :--soup-in() for experimental purposes.

@facelessuser facelessuser added T: feature Feature. skip-triage Tells bot to not tag a new issue with 'triage'. labels Sep 8, 2021
@facelessuser
Copy link
Owner Author

It seems to match the current element and/or any element under it which matches the complex selector with the pseudo-class. I'm not quite sure exactly all the ways the "end" boundary affects things.

I assume that the parent element either matches the scope (and also doesn't match the end scope) or a child of the current element is matched if it matches the scope but doesn't match the end scope relative to the parent.

I think it sounds more complicated than it will be to implement...I think. At first blush, it seems that we could convert:

el:in(.whatever .this, .is / .not, .this)

to something like this:

el:is(:is(.whatever .this, .is), :is(.whatever .this, .is) *):not(:is(.not, .this), :is(.not, .this) *)

I'm not at all sure though 🙃. It may be interesting to prototype.

@facelessuser
Copy link
Owner Author

Threw together a prototype, and it seems to work pretty well. From reading, it appears that :in() should not affect the :scope. Based on this, I'm under the assumption that I do not have to restrict the reach of the selectors in :in(). I was curious about this specifically for the <scope-end> portion. It would add a lot of complexity. We'd have to somehow track and pass around the <scope-start> which we don't currently have a way to do.

So, let's consider an example. Here we will target .that with a start of .this or .what, but set an end boundary of .nada and .bad.

from bs4 import _soup

html = """
<div class="this">
    <div class="nada">
        <span class="that good">one</span>
        <span class="that bad">two</span>
        <span>three</span>
    </div>
</div>
<div class="what">
    <div class="nope">
        <span class="that bad">one</span>
        <span class="that good">two</span>
        <span>three</span>
    </div>
</div>
<div class="this that"></div>
"""

soup = _soup(html, 'html.parser')
print(soup.select('.that:in(.this, .what / .nada, .bad)'))

This gives us:

[<span class="that good">two</span>, <div class="this that"></div>]

Assuming I interpreted the spec correctly, this should also be correct and match nothing:

from bs4 import _soup

html = """
<div class=".bad">
    <div class="this">
        <div class="nada">
            <span class="that good">one</span>
            <span class="that bad">two</span>
            <span>three</span>
        </div>
    </div>
</bad>
"""

soup = _soup(html, 'html.parser')
print(soup.select('.that:in(.this, .what / .nada, .bad)'))

Output:

[]

Yes, the wrapper <div class=".bad"> negates everything, even though it is outside of <div class="this">. As :scope shouldn't be changed by the pseudo-class, it seems this should be correct:

Note: This does not effect the :scope elements for the selector.

There are absolutely no examples out is this is very new, but I can see this potentially being a useful selector. This is assuming this actually makes it to a browser and doesn't get dropped in the spec. It also assumes they don't make it a lot more complicated to implement.

@facelessuser
Copy link
Owner Author

NOTES:

I imagine, if worse came to worse, and we had to ensure the end boundary was under the start boundary, we could tag the inclusive selector list with a push_scope flag and the exclusive selector list with a pop_scope flag. The matcher would be able to check the scope stack and verify that the end scope proximity is not further than the start scope. CSS selectors will favor closest in proximity anyways, so if a given selector is out of range, there probably wasn't a closer one.

While the CSS engine will favor the closest element based on a given selector, selectors in a selector list are not sorted by proximity. We may have to track the last index tried in the inclusive list so we can retry at the next index if the scope is negated by the exclusive list.

Basically, we may have to wrap are inclusive/exclusive list in some special :in logic, which means we probably can't just reuse :is() and :not() if :not() must be under :is().

@facelessuser
Copy link
Owner Author

My interpretation is not exactly right. After reading A New Donut Selector, I think I better understand.

If we want to implement this experimental selector, I imagine we will target the main element, walk itself and its parents seeing if we find the upper boundary, then walk its parents again to see if the lower boundary gets in the way. We would stop as soon as we hit the upper boundary. The lower boundary must come under the upper boundary (they can't be the same). The target element can also be the lower boundary. In some ways, this makes things easier on us and in other ways a little more complicated.

I think it is doable though.

Again, the name may change, the spec may change, a lot of things could change, so if we wanted to make use before the spec is rock solid, we'd use a -soup prefix as the final could be very different from what we do. But if it proves useful, it may be worth getting an early prototype in.

@facelessuser
Copy link
Owner Author

Refactor went nice, and while there needs to be some cleanup and checks for corner cases, it seems that we are able to find a tag between two clear boundaries, unlike the initial implementation. I'm optimistic this could be pretty useful. I may get an experimental branch up sometime soon-ish.

@facelessuser
Copy link
Owner Author

The experimental branch is up via PR #228. No tests are currently present, but it does appear to work as described in the CSS document. I've seen the selector described as :in-scope() and :in(). Obviously, some bikeshedding will need to be done, but we may call it something like :-soup-in-scope() or :-soup-in(). Again, there are two different approaches they are considering, one using @scope (from) to (to) {} and this :in(). One, both, or none may make it into the final spec. Obviously, the one we care most about is :in() as the other will not really work in our environment. The real question are:

  1. Do we find it useful enough to release into the wild as a new experimental selector.
  2. Do we wait to see how the proposal evolves (syntax wise) or release it sooner rather than later and risk changing it in the future.
  3. Do we only wait until browsers start implementing it (assuming it makes it all the way through the process).

@facelessuser facelessuser added C: css-custom CSS custom selectors. P: maybe Pending approval of low priority request. labels Sep 25, 2021
@facelessuser
Copy link
Owner Author

Sounds like CSS is backing away from using a selector for things like querySelectorAll and just sticking to @scope rules.

So, what this means is that there will be no official pseudo-class for us to implement. We could implement our own pseudo-class despite CSS not implementing one. Or, we could allow select and select_one to receive an upper and lower bound and then just search within those bounds; this would basically be a rough equivalent of @scope and require no new pseudo-classes.

Or, we could just pass on doing anything as there is no official selector and we aren't obligated to implement some @scope equivalent.

I still think though that there something like :-soup-in() could be useful, so something to think about.

@facelessuser
Copy link
Owner Author

If we do this, it is likely to be pretty basic in approach. An easy way of looking at it is simply that an element is matched, and then we look at the closest parent that satisfies the start limit, if that matches, we check if it has an even closer parent that matches the lower limit. If the lower limit is found, we are out of bounds and cannot match. The element that is the lower limit, in this case, could still match itself though.

I guess that is a question though, do the limits themselves qualify as a match? I guess this is an inclusive/exclusive question, and I know the official @scope spec is discussing such issues as well. We'd likely pick something that corresponds well to whatever they kind of decide.

Another thing I was thinking about...why couldn't this be its own function, like closest?

sv.in_scope(pattern, start, end)

Anyways, @scope is probably still a long way from being fully fleshed out, not to mention being implemented in CSS, and any sort of analogous implementation here is probably a long way off as well. Mainly wanted to get my thoughts down for the future though as I've been thinking about it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C: css-custom CSS custom selectors. P: maybe Pending approval of low priority request. skip-triage Tells bot to not tag a new issue with 'triage'. T: feature Feature.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant