Skip to content
New issue

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

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

Already on GitHub? # 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.

# for free to join this conversation on GitHub. Already have an account? # 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