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

[css-nesting-1] Invalid nested selectors #7503

Closed
cdoublev opened this issue Jul 15, 2022 · 15 comments
Closed

[css-nesting-1] Invalid nested selectors #7503

cdoublev opened this issue Jul 15, 2022 · 15 comments
Labels
Closed as Duplicate Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. css-nesting-1 Current Work

Comments

@cdoublev
Copy link
Collaborator

I think CSS Nesting or CSS Selector 4 (probably the former) should clarify something like "An invalid complex or compound selector remains invalid when some of its parts are nested":

::before {
  &:hover {} /* valid */
  & div {} /* invalid */
}

Currently, in CSS Selector 4:

A pseudo-element may be immediately followed by any combination of the user action pseudo-classes [...].

Note that, unless otherwise specified in a future specification, pseudo-classes other than the user action pseudo-classes are not valid when compounded to a pseudo-element; so, for example, ::before:first-child is an invalid selector.

Some pseudo-elements are defined to have internal structure. These pseudo-elements may be followed by child/descendant combinators to express those relationships. Selectors containing combinators after the pseudo-element are otherwise invalid.

@romainmenke
Copy link
Member

see : #7433

this makes me think we could add a warning to the spec that any nesting inside a ::before (or of same type) will produce an invalid selector per https://www.w3.org/TR/selectors-4/#matches-pseudo and have no matches.

@cdoublev
Copy link
Collaborator Author

Changing the title to broaden the topic with the following cases:

type { &type {} }
.subclass { &type {} }

The first can only be invalid, even when expanded using :is().

The latter would be valid only if it is expanded using :is().

The nesting selector can be desugared by replacing it with the parent style rule’s selector, wrapped in an :is() selector.

This currently appear in a note. Maybe it should be normative?

@cdoublev cdoublev changed the title [css-nesting-1] Invalid nested selectors for a pseudo-element selector [css-nesting-1] Invalid nested selectors Nov 29, 2022
@romainmenke
Copy link
Member

romainmenke commented Nov 29, 2022

Those are only invalid when treating the selectors as strings but that is something only preprocessors do.

I would assume type { &type {} } to be valid because these work fine :

p:is(p) {
	color: green;
}

is(p):is(p) {
	color: green;
}

This currently appear in a note. Maybe it should be normative?

I agree, because in practice these are tightly coupled :

  • Blink is implementing nesting based on :is() internals.
  • pseudo elements have the same restrictions

@cdoublev
Copy link
Collaborator Author

cdoublev commented Nov 29, 2022

Yeah, Romain, you are right: p { &p {} } could be valid, and p { &div {} } could just match nothing.


The nesting selector is allowed anywhere in a compound selector, even before a type selector, violating the normal restrictions on ordering within a compound selector.

svg { &|rect {} }: is it valid or does the above definition means anywhere at the "top-level" of <compound-selector>?

EDIT: nope, it seems not, according the note related to the BEM pattern, which should also be normative, imo, therefore I think the above definition can be clarified.

@romainmenke
Copy link
Member

svg { &|rect {} }: is it valid or does the above definition means anywhere at the "top-level" of <compound-selector>?

Also somewhat related to : #7972

.foo { :is(& .bar, .bar) { …}}

@tabatkins
Copy link
Member

For the question in the OP, this is a dupe of #2880.

type1 { &type2 {...}} is indeed perfectly valid, equivalent to type2:is(type1) {...}. It won't match anything, but that's fine.

svg { &|rect {} } is also valid, equivalent to |rect:is(svg) - it only matches an element which is an <svg> and is a <rect xmlns="">, which again won't match anything, but that's still fine. ^_^

@tabatkins tabatkins added Closed as Duplicate Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. labels Jan 10, 2023
@cdoublev
Copy link
Collaborator Author

cdoublev commented Jan 11, 2023

I did not asked how to handle invalid nested style rules but if a selector of a nested style rule that would be invalid when desugared, should be invalid.

I do not know how selectors in ::before { &:root {} }, :root { &::before {} }, type { &::before {} }, type { undeclared|& {} }, .class { html|& {} }, etc, are supposed to be desugared and validated using :is().

Example 10 seems to be defining that selectors that would be invalid (like & representing pseudo-element, ie. the first example) are just ignored, which is surprising because parsing the top-level selector is not forgiving.

For example, html|:hover, .class:hover {} is entirely invalid but :hover { html|&, .class& {} } would be valid?

Anyway, I will wait for the next edits of the spec.

@cdoublev
Copy link
Collaborator Author

Following latest spec edits and after carefully re-reading all related issues, I have a better understanding of how a nest-containing selector is desugared. Basically, & can be considered as being replaced by :is() containing the selector of the parent rule.

First, I think an example showing that there is no difference between div { &:hover {} } and div { :hover& {} } would be usefull (whereas only div:hover is valid).

Second, it is still not clear to me if this resolution with :is() implies that for:

:root { ::before&, .valid {} }   /* `::before:is(:root)` is invalid */
span { ::before + &, .valid {} } /* `::before + :is(span)` is invalid */
  1. nested rules are valid but ::before + & and ::before + & do not match any element
  2. nested rules are invalid

I intentionally used ::before in the selector of the nested style rules, because I do not know if desugaring with :is() is slightly more complex than I defined it in the introduction of this comment, which would mean the answer is 1.

But more importantly and put more explicitly, I would like to know if desugaring & with :is() implies forgiving parsing for <relative-selector-list>.

@tabatkins
Copy link
Member

tabatkins commented Feb 16, 2023

But more importantly and put more explicitly, I would like to know if desugaring & with :is() implies forgiving parsing for .

No, the desugaring with :is() is only ever mentioned as an approximation; the spec does not actually use :is() for anything internally, and so nothing specific about :is() is carried thru.

it is still not clear to me if this resolution with :is() implies that for:

As currently written, ::before& is invalid as the selectors grammar only allows <pseudo-class-selector> after it. However, I suspect we should make that valid, as :hover { .foo&, ::before& {...}} makes sense and would be valid if expanded out manually.

::before + & is valid, as you can put a combinator after a pseudo-element. (It won't match anything in your example, but that's fine.)

I'm not sure why your comment says "::before + :is(span) is invalid"; it's definitely valid per Selectors grammar.

@cdoublev
Copy link
Collaborator Author

As currently written, ::before& is invalid as the selectors grammar only allows <pseudo-class-selector> after it.

Right, sorry. Thanks for considering allowing it.

I'm not sure why your comment says "::before + :is(span) is invalid"; it's definitely valid per Selectors grammar.

I may be misunderstanding 3.6.5. Internal Structure:

Some pseudo-elements are defined to have internal structure. These pseudo-elements may be followed by child/descendant combinators to express those relationships. Selectors containing combinators after the pseudo-element are otherwise invalid.

So the explicit answer to my question is: if the prelude of a nested style rule is invalid (after desugaring &), the whole nested style rule is invalid. Ok, thanks!

@tabatkins
Copy link
Member

Ah yes, you're right, ::before specifically disallows combinators after itself, but in the general case that selector could be valid. ^_^

But yeah, & does not invoke any forgivingness; this differs from how it would act if it were desugared literally using :is().

@cdoublev
Copy link
Collaborator Author

As currently written, ::before& is invalid as the selectors grammar only allows <pseudo-class-selector> after it.

Please note that with &::before, the problem remains, depending on how you expect it to desugar, which seems to be by "moving" & to the end of the selector:

type1 { &type2 {...}} is indeed perfectly valid, equivalent to type2:is(type1) {...}. [...]

svg { &|rect {} } is also valid, equivalent to |rect:is(svg) [...]

&::before is a valid <relative-selector-list> but ::before:is(:root) (desugared from :root { &::before {} }) is invalid, because ::before:root is invalid, according to Selectors:

Certain pseudo-elements may be immediately followed by any combination of certain pseudo-classe [...]. This specification allows any pseudo-element to be followed by any combination of the logical combination pseudo-classes and the user action pseudo-classes. [...] Combinations that are not explicitly allowed are invalid selectors.

NOTE: The logical combination pseudo-classes pass any restrictions on validity of selectors at their position to their arguments.

However the examples in the spec seems to remember its position. This is another reason why I commented:

First, I think an example showing that there is no difference between div { &:hover {} } and div { :hover& {} } would be usefull (whereas only div:hover is valid).

@cdoublev
Copy link
Collaborator Author

If &::before and ::before& are supposed to be equivalent, it may be safer to define the selector of a nested style rule with <relative-real-selector-list> for now.

@tabatkins
Copy link
Member

Note that "desugaring" is purely informative. Nothing shown in the examples actually occurs in impls; we're just showing equivalent selectors so the behavior can be understood in terms of existing features.

which seems to be by "moving" & to the end of the selector:

These examples are all showing cases where the compound selector contains a type selector. When writing a selector without an &, as we're showing here, type selectors are required to go first, so the reordering is required. :is(type1)type2 is just an invalid selector; it wouldn't make sense for us to write that as a desugared version.

More generally, rearranging components of a compound selector is a no-op. .foo.bar and .bar.foo are exactly equivalent.

On the other hand, &::before and ::before& are absolutely not equivalent; you're moving the & to a different compound selector entirely.

@cdoublev
Copy link
Collaborator Author

Ok, I got it, thanks. &type and type& are equivalent, even if & represents * or type. &.class and .class& are equivalent, even if & represents type. But &::pseudo and ::pseudo& are never equivalent, whatever & represents.

Fortunately, I think authors will always choose the position of & as if it were a macro, even if it is not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Closed as Duplicate Closed as Question Answered Used when the issue is more of a question than a problem, and it's been answered. css-nesting-1 Current Work
Projects
None yet
Development

No branches or pull requests

4 participants