w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.44k stars 656 forks source link

[css-nesting-1] Syntax Invites Errors #7834

Closed fantasai closed 1 year ago

fantasai commented 1 year ago

Edit: 👉🏼 UPDATED SUMMARY TABLE OF SYNTAX PROPOSALS 👈🏼 (from https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1282776786 )


As mentioned in #7796, there are some problems with the currently-proposed nesting syntax:

fantasai commented 1 year ago

[ @LeaVerou, @jensimmons, @bradkemper, @tabatkins, @mirisuzanne and I discussed this problem today; this comment is a summary of the discussions.]

The following options outline the available syntax space for nested style rules:

  1. Prefix each individual rule
  2. Nest rules into a block
  3. Switch into rule-parsing mode after a syntactic trigger

The first option has the problems outlined above. The second option increases the indentation level, which CSSWG had concluded was objectionable. So that leaves us the third option.

Regardless, once we're in rule-parsing mode, in order to make nested style rules compatible with style rules elsewhere, the desire is to allow both:


In terms of syntactic triggers for a rule-parsing mode, the following options were considered:

After the trigger, parsing would continue in rule-parsing mode and not in declaration-parsing mode, so &-prefixing would no longer be required for subsequent nested style rules.

Forwards compatibility considerations:

Backwards compatibility considerations:

The recommended option from the discussions today was to allow as a trigger both:

Pros/cons of triggering on &-prefixed rules:

Pros/cons of triggering on @nest:

Pros/cons of triggering on new delimiter:

Agenda+ to discuss

tabatkins commented 1 year ago

Pulling out for clarity, the suggested change is:

Taking the following example Sass from the original Nesting issue:

.main-nav {
   display: flex;
   ul {
      list-style-type: none;
      li {
         margin: 0;
         padding: 0;
      }
      a {
         display: flex;
         padding: 0.5em 1em;
      }
   }

   nav& {
      display:  block;
   }
}

You'd write this in the new proposal as one of the following:

.main-nav {
   display: flex;
   & ul {
      list-style-type: none;
      & li {
         margin: 0;
         padding: 0;
      }
      a {
         display: flex;
         padding: 0.5em 1em;
      }
   }

   nav& {
      display:  block;
   }
}

or

.main-nav {
   display: flex;
   @nest;
   ul {
      list-style-type: none;
      @nest;
      li {
         margin: 0;
         padding: 0;
      }
      a {
         display: flex;
         padding: 0.5em 1em;
      }
   }

   nav& {
      display:  block;
   }
}

Which you use is up to preference.

romainmenke commented 1 year ago

I think this proposal introduces several issues. A parsing switch mechanic also effects humans, code editors, ...

A person needs to have seen the parsing switch to be able to understand the code. In a reasonably large file this might be dozens of line above, way out of view. From their perspective the code looks indented as if from a conditional rule.

A syntax highlighter might not be parser based and will have difficulty with this.

Overal I think this solves some writing issues but negatively affects readability.


Diffing code is a good example I think :

         margin: 0;
         padding: 0;
      }
-       & a {
+       & b {
         display: flex;
         padding: 0.5em 1em;
      }

vs.

         margin: 0;
         padding: 0;
      }
-       a {
+       b {
         display: flex;
         padding: 0.5em 1em;
      }
mirisuzanne commented 1 year ago

A person needs to have seen the parsing switch to be able to understand the code. In a reasonably large file this might be dozens of line above, way out of view. From their perspective the code looks indented as if from a conditional rule.

I don't think that's actually true in practice. The code means the same thing either way. The switch just changes if that code is valid or not. And that switch is only needed where you change from declarations to nesting. In a reasonably large file, you can tell what's valid from context. If you're looking at nested selectors, then you're past the point of the switch, and you can continue using nested selectors. If you're looking at declarations, then you should see a switch as you scroll down to nested stuff.

If you are adding nested stuff right after declarations, you need to add a switch. If you're on either side of that switch already, and it's off-screen - it's pretty clear which side you are on from context.

mirisuzanne commented 1 year ago

Part of the implication of this proposal is that you never mix back-and-forth between nested rules and declarations:

astearns commented 1 year ago

Part of the implication of this proposal is that you never mix back-and-forth between nested rules and declarations:

* Declarations always come first

* Once you start nesting, everything has to be nested (within that block/nesting-level)

Right, that is my main hesitation about this proposal. We get some good copy-and-paste behavior at the expense of a different copy-and-paste problem, where people who regularly paste new declarations at the end of an existing block may not get what they expect.

LeaVerou commented 1 year ago

One thing I'd like to point out is that in the code examples @tabatkins posted, using @nest as the parsing switch appears to be preferable, as it looks more consistent because everything is a descendant selector.

However, in real nesting use cases, the first few rules are often (though of course not always) specifying variations of the base rule, e.g. &:disabled, &:nth-child(odd), &.foo etc, so you have the ampersand there anyway. E.g.:

section {
    declarations;

    &.main {
        declarations;
    }

    h1 {
        declarations;
    }

    p {
        declarations;
    }

    ...
}

so the parsing switch often comes naturally, whereas with @nest it needs to be explicit in all cases and adds a fair bit of noise. I'm fine with this noise being opt-in, for authors that want it, but it should not be mandatory.

Note that this is exactly how the above code would have been written in Sass (which was designed without our parsing constraints and thus is IMO the most natural syntax for this, so the closer we can get, the better).

LeaVerou commented 1 year ago

Part of the implication of this proposal is that you never mix back-and-forth between nested rules and declarations:

* Declarations always come first

* Once you start nesting, everything has to be nested (within that block/nesting-level)

Right, that is my main hesitation about this proposal. We get some good copy-and-paste behavior at the expense of a different copy-and-paste problem, where people who regularly paste new declarations at the end of an existing block may not get what they expect.

a) Interleaving declarations and rules makes code harder to read, you now have to hunt down the entire rule with all its descendants to understand how the base element is styled, so I'm fine disallowing that. b) For that reason, I think it's a fairly uncommon practice even in contexts that allow it, such as our @page or Sass' nesting. We can probably ask the Almanac folks for some stats on that if it would be useful.

dbaron commented 1 year ago
  • Con: first selector in a set of nested style rules has this special requirement, which is an odd positional requirement

I'm a bit concerned about the implications of this for editing of style sheets. In particular, it introduces cases where deleting a rule (which has the initial & to trigger the switch) will invalidate the rules after it, if they don't have that & trigger. For example, in the middle code block in @tabatkins's comment deleting the & li { margin: 0; padding: 0; } rule will invalidate the rule following it.

Maybe it's something we can live with given all the constraints here, but I just wanted to point out explicitly what this implies about removing rules from style sheets in the process of editing them.

romainmenke commented 1 year ago

maybe we should split out concerns/feedback into separate issues?


@romainmenke said :

A person needs to have seen the parsing switch to be able to understand the code. In a reasonably large file this might be dozens of line above, way out of view. From their perspective the code looks indented as if from a conditional rule.

@mirisuzanne said :

I don't think that's actually true in practice. The code means the same thing either way. The switch just changes if that code is valid or not. And that switch is only needed where you change from declarations to nesting. In a reasonably large file, you can tell what's valid from context. If you're looking at nested selectors, then you're past the point of the switch, and you can continue using nested selectors. If you're looking at declarations, then you should see a switch as you scroll down to nested stuff.

If you are adding nested stuff right after declarations, you need to add a switch. If you're on either side of that switch already, and it's off-screen - it's pretty clear which side you are on from context.

The case I had in mind was this :

@media (min-width: 300px) {
  /* a lot of css */
    margin: 0;
    padding: 0;
  }

  a { /* "a" is just "a", no nesting */
    display: flex;
    padding: 0.5em 1em;
  }
  /* a lot more css */
}

vs.

.my-component {
  /* a lot of css */
    margin: 0;
    padding: 0;
  }

  a { /* "a" is ".my-component a" */
    display: flex;
    padding: 0.5em 1em;
  }
  /* a lot more css */
}

I've always seen the required & as a useful reading aid while also solving a parser implementation detail. It makes it clear that & a is part of something else and knowing what & stands for is important context.

Maybe I am alone in thinking that the more verbose syntax of the current draft is actually a good feature :)

bradkemper commented 1 year ago

I'm a bit concerned about the implications of this for editing of style sheets. In particular, it introduces cases where deleting a rule (which has the initial & to trigger the switch) will invalidate the rules after it, if they don't have that & trigger. For example, in the middle code block in @tabatkins's comment deleting the & li { margin: 0; padding: 0; } rule will invalidate the rule following it.

I initially had this concern too. I think in practice, I would probably add the & to the beginning of each rule as if it was required. And maybe some Linters might even require it as a best practice. Maybe a shortcut in editors to add it to the beginning of each line in a selection. If there were enough rules to make this cumbersome, then I'd switch to using an @nest (or maybe @brad) switch instead.

The point is, you could always continue to prefix each line with a & (except when you need the & to be later in the selector), and that might even make it more obvious that you were in a nested context. And would make it easier to move rules around, delete them, etc.

argyleink commented 1 year ago

Wish I was invited to this call. @romainmenke you're not alone.

bradkemper commented 1 year ago

Maybe I am alone in thinking that the more verbose syntax of the current draft is actually a good feature :)

But you could still be that verbose if you want to. You could still proceed each rule with an &. You just wouldn't need @nest at the beginning of the ones with an & somewhere else in the selector, unless it was the first rule after any declarations.

And actually, you could still write @nest multiple times and in multiple places between rules if you wanted to (switching to a mode you are already in). So I don't see why you couldn't write it in the previous sustains if you wanted, and it should still parse the same, I think.

This also means you could write the rules in a SASS compatible way.

romainmenke commented 1 year ago

This also means you could write the rules in a SASS compatible way.

Also reading a lot of statements on Twitter that this change would allow you to copy/paste from Sass. But that would not be true as I understand it.

Sass and the nesting specification would still be different for complex selectors.

.a .b {
  /* 
    might need a preceding `@nest;`
    or a might need to be `@nest .c &` depending on final syntax
  */
  .c & {
    color: green
  }
}

sass :

.c .a .b { /* "a" is a descendant of "c" */
  color: green;
}

current draft :

.c :is(.a .b) { /* "a" and "c" might be the same element, or one might be an ancestor of the other */
  color: green;
}

I think it is misleading to present this change to the specification as "compatible with Sass".

LeaVerou commented 1 year ago
  • Con: first selector in a set of nested style rules has this special requirement, which is an odd positional requirement

I'm a bit concerned about the implications of this for editing of style sheets. In particular, it introduces cases where deleting a rule (which has the initial & to trigger the switch) will invalidate the rules after it, if they don't have that & trigger. For example, in the middle code block in @tabatkins's comment deleting the & li { margin: 0; padding: 0; } rule will invalidate the rule following it.

Maybe it's something we can live with given all the constraints here, but I just wanted to point out explicitly what this implies about removing rules from style sheets in the process of editing them.

Do note that it's far easier to debug all your nested rules suddenly not being applied, than the current situation where it's per-rule and thus leaving out a necessary & causes much smaller regressions. I've been using the current syntax through PostCSS for years, and there have been so many times where it took me a fair bit of debugging to realize a CSS bug was caused by me forgetting the the & out in one rule (and even longer to even spot said bug).

Also reading a lot of statements on Twitter that this change would allow you to copy/paste from Sass. But that would not be true as I understand it.

This is orthogonal to nesting syntax, and is true regardless of how the nested rules are specified.

I think what people on Twitter are rejoicing about is that this makes it easier to migrate from Sass. The edits required are now O(N) on the number of rules with children, not O(Nk) where k is the number of child rules per rule, and in practice even fewer as often the first nested rule starts with & anyway (see my earlier comment).

devongovett commented 1 year ago

I think it's pretty strange and confusing to have different requirements for the first nested rule vs the others just to avoid a single character in some cases. Also agree with the above points about making editing (eg inserting or reordering rules) more difficult. Better to just be consistent about it IMO.

I think, just like variables, people coming from SASS etc just aren't used to the syntax yet. Once it becomes standard and everyone starts using it, it'll become second nature.

mirisuzanne commented 1 year ago

There are, for sure, tradeoffs and issues with every option we've considered here. All of that is well documented in the proposal above. They all introduce potential footguns for authors, and they all come with caveats at the edges - issues that will impact some code styles more than others.

From my perspective, the priority of this proposal was a flexible and forgiving syntax. In the most common cases, it just works - and authors can migrate from existing code (both Sass and the PostCSS polyfill) easily. It's also possible to copy/paste that code to the root of the document, or into a scope rule, etc. The implications change slightly in those different cases, but in each one the code makes sense - and has the expected behavior.

From there, authors can choose to make various improvements, based on their preferred code styles. This was also very clearly true of the existing syntax - where some authors may choose to require @nest, and others would choose not to. I agree, I likely wouldn't want to rely on 'the first & is special' behavior - so I would discuss with my team if we want to always use &, or always use @nest. Either way, we would set up a linter to enforce the style we want on our projects.

Even though I wouldn't rely on it for large projects, I still think it makes sense to allow that flexibility as far as possible, so that most things will just work. It's unfortunate that as far as possible stops with the first &, but there are similar issues with the other proposals.

We are not going to achieve a perfect syntax here that everyone loves. But we can achieve one that has the flexibility to handle most of what authors throw at it.

mirisuzanne commented 1 year ago

I understand that for professional developers flexible and forgiving is not always the priority we choose. Most of us use linters to make our code less flexible and less forgiving. We can still do that! But I believe flexible and forgiving is actually a pretty good guiding ideal for CSS itself, even if many of us will use tooling to enforce more consistent code styles.

LeaVerou commented 1 year ago

To add to Mia's excellent responses above, it is actually fairly common for the last thing in a sequence to have more forgiving syntax, for example:

I'd argue the last n-1 things having more forgiving syntax than the first one is a fairly similar pattern to the first n-1 things having the stricter syntax and the last one having the more forgiving syntax.

Also, there are two ways to frame this:

  1. Either @nest OR a &-prefixed rule can switch parsing mode and both are fine
  2. @nest; is mandatory; but CSS will error correct and insert it if it encounters an &-prefixed rule.

I wonder if some of the people against this proposal would be more amenable to the second framing? Though these are not necessarily entirely equivalent, there could be OM differences, depending on how we represent @nest in the OM.

jimmyfrasche commented 1 year ago

If viewing it as an error-correction why couldn't it be inserted whenever it encounters a non-literal token? That way it would only require an extraneous & or @nest when the first selector starts with a tag name.

LeaVerou commented 1 year ago

If viewing it as an error-correction why couldn't it be inserted whenever it encounters a non-literal token? That way it would only require an extraneous & or @nest when the first selector starts with a tag name.

I'd actually be fine with that.

devongovett commented 1 year ago

I think the important thing is to be consistent. Requiring & in some cases but not others is confusing for beginners and advanced authors alike. Unlike semicolons and braces, & has a significant impact on the meaning of the rule. With &, it's immediately obvious where the "context" (i.e. parent) is inserted into the selector. When it isn't required, authors must either know the rules about how it's automatically inserted or guess about it. This creates more to learn, forget, misunderstand, etc. leading to frustration and bugs.

Sure, lint rules can be invented, but why create this confusion in the first place? IMO, if we're even talking about a lint rule being needed to disable a new language feature, it's probably not a good idea. Especially since the only benefit is saving a single character per rule. Perhaps users of SASS will need to re-learn things, but the majority of CSS developers have never used SASS so I don't think this is a good argument. Plus, the nesting syntax already works differently in other ways as @romainmenke mentioned, so re-learning will be necessary either way. Clarity and consistency is far more important than SASS compatibility in my opinion.

romainmenke commented 1 year ago

I think it is also important to challenge the opinions that sparked this.

selectors within nested context are incompatible with those outside of nested context, and with syntax within @scope, which invites a lot of copy-paste errors when transferring code among these contexts

This is not solved by this syntax change. It can only be resolved by giving & meaning everywhere. (that is why there is : https://github.com/w3c/csswg-drafts/issues/5745)

the requirement to include & within all the selectors in a list even beyond syntactic disambiguation (instead of allowing relative selectors) is annoying to type, easy to forget.

The only selectors that can omit & are those that use a descendant combinator.

& .foo -> .foo

But not all these :

- &.foo - & + .foo - & > .foo - & ~ .foo

~~And also not when the selector is the first non-declaration. The gains are very minimal here.~~

the requirement to include & within all the selectors in a list even beyond syntactic disambiguation (instead of allowing relative selectors) makes automated (and manual) conversion of existing code much more difficult (requires selector parsing rather than regex).

This is untrue. Regex will never be sufficient for migrations from Sass to nested CSS because complex selectors work fundamentally different.

This is also a one time event and should not be used to motivate a lasting language design.

&.foo and & .foo are easily confusable (both for reading and for typing), and so long as the latter is required for nested relative selectors this will be a commonly encountered problem

The same is true for .foo& vs. .foo &. How does this proposal help in that case?


I am really trying to see the upsides of this proposal and have begon working on some sample code that compares certain aspects. Doing this so that I can get a feel for this version of nesting. https://github.com/romainmenke/nesting-proposal-changes-7834

At the moment however I am really struggling with this. I don't agree with the stated issues and I don't see them solved by the changes.


Correction

I initially overlooked the addition of relative selectors which enables > .foo, + .foo, ~ .foo.

This comment has been edited to strike through the incorrect statements.

LeaVerou commented 1 year ago

I think it is also important to challenge the opinions that sparked this.

selectors within nested context are incompatible with those outside of nested context, and with syntax within @scope, which invites a lot of copy-paste errors when transferring code among these contexts

This is not solved by this syntax change. It can only be resolved by giving & meaning everywhere. (that is why there is : #5745)

That only solves it one way: you can then paste from a nested context to @scope or non-scoped contexts, but you cannot paste from @scope into a nested context.

The only selectors that can omit & are those that use a descendant combinator.

& .foo -> .foo

But not all these :

  • &.foo
  • & + .foo
  • & > .foo
  • & ~ .foo

Nope, under this proposal <relative-selector>, i.e. + .foo, > .foo or ~ .foo will be perfectly valid.

&.foo and & .foo are easily confusable (both for reading and for typing), and so long as the latter is required for nested relative selectors this will be a commonly encountered problem

The same is true for .foo& vs. .foo &. How does this proposal help in that case?

Both .foo& and .foo & are far rarer than & .foo and &.foo which are some of the most common nested selectors. Also, I'm having trouble following this logic. If something is confusing it justifies something else being confusing?

I am really trying to see the upsides of this proposal and have begon working on some sample code that compares certain aspects. Doing this so that I can get a feel for this version of nesting. romainmenke/nesting-proposal-changes-7834

This sample code demonstrates my earlier point that for a lot of code, no non-optional ampersands will even need to be included.

LeaVerou commented 1 year ago

@romainmenke I took a stab at converting your sample code following this proposal to sample code with a mandatory @nest; (since you are arguing that ampersands should not kick the parser into rule mode, that's the alternative — requiring @nest in all rules that have children).

This is the result: ```css .block { color: green; box-sizing: border-box; height: auto; padding-left: 1.25rem; padding-right: 1.25rem; position: relative; width: 100%; z-index: 1; @nest; @media (min-width: 48rem) { padding-left: 2rem; padding-right: 2rem; } @media (min-width: 80rem) { padding-left: 3rem; padding-right: 3rem; } @media (prefers-color-scheme: dark) { color: lime; } &:hover { outline: 2px solid currentColor; } &.block--orange { color: orange; @nest; @media (prefers-color-scheme: dark) { color: yellow; } } .block__element { align-items: center; display: flex; flex-direction: column; justify-content: center; right: 2rem; text-align: center; top: 50%; transform: translate(-5px, calc(-50% - 1.625rem)); z-index: 2; @nest; @media (min-width: 48rem) { right: 3rem; } @media (min-width: 80rem) { right: 4rem; } .block--orange & { text-decoration-color: black; @nest; @media (prefers-color-scheme: dark) { text-decoration-color: white; } } } } ```

Is this preferable? Is it more understandable?

romainmenke commented 1 year ago

Is this preferable? Is it more understandable?

No this is, in my personal opinion, much worse. :) Which is also why I doing my best to avoid @nest; until I can find a good example of CSS code where it feels natural and can be presented as a good thing.

But in these examples @nest; is not required because of the coding styles used:

This coding style makes it highly unlikely that you will ever need to use @nest;. That however doesn't make @nest; a good thing.


since you are arguing that ampersands should not kick the parser into rule mode

Not saying that.

I am concerned about a parser switch that appears once. With a required & or @nest .foo & in each selector you have more context as a reader.

romainmenke commented 1 year ago

Nope, under this proposal , i.e. + .foo, > .foo or ~ .foo will be perfectly valid.

I overlooked that, thank you for pointing that out.

I wonder if we can have relative selector support in nesting without breaking the current proposal and if this would be implementable in browsers?

Both .foo& and .foo & are far rarer than & .foo and &.foo which are some of the most common nested selectors. Also, I'm having trouble following this logic. If something is confusing it justifies something else being confusing?

If it isn't a truly good solution that covers the entire problem I sometimes (not always) find it better to do nothing. This gives a result that is worse but is consistently worse instead of confusing in its own way.

If &<space> or <space>& is a problem that must be solved then I think there must be a better way than having the ability to omit the leading &.

tabatkins commented 1 year ago

If viewing it as an error-correction why couldn't it be inserted whenever it encounters a non-literal token? That way it would only require an extraneous & or @nest when the first selector starts with a tag name.

Yeah, I've been thinking thru the parsing implications, and this should be doable. I need to be a little careful, because people today rely on "put some random symbol at the start of your property to 'comment it out'" and that needs to be preserved, but I believe I can handle this very reasonably in the spec.

Currently the parsing rules for style blocks are (ignoring the ending conditions and the one bit that's for the current Nesting spec):

To accommodate this new bit, I'd instead have:

Then "consume an ambiguous rule" is identical to the existing "consume a qualified rule", except it'll fail early if it encounters a top-level semicolon, raising a parse error and returning nothing.

I believe it's okay in practice to defer a "parse this as X or fail" decision until later in the stream, while "parse this as X or as Y" needs to be know which with small, finite lookahead (which is why "just do it like Sass" is problematic for our impls).

Note, tho, that this will slightly tie our hands in the future - we'll never be able to change the property syntax to start with a non-ident (like doing additive CSS by writing +transform: ... or something). This probably isn't a huge deal, but it is definitely a forwards-compat/language evolution issue to worry about.

tabatkins commented 1 year ago

The benefit of this, btw, is that the "parsing switch" becomes almost automatic as soon as you start writing rules. The sole exception is if your first rule's selector starts with a tagname; that will require the @nest; explicit switch. In all other cases you can simply pop rules in and not worry about it.

(And to help fix the potential author confusion for this one case, devtools can rely on arbitrary lookahead to tell whether something that's parsing as a random unknown property actually looks like a rule, and flag that as a potential error.)

Loirooriol commented 1 year ago

this will slightly tie our hands in the future

I don't like compromising the future of the language just to avoid typing & or @nest when nesting. Especially when in one case you will still need to use @nest anyways. I think the "flexible and forgiving syntax" will just cause confusion among occasional/newbie authors who don't want to learn all the intricacies of the language.

bradkemper commented 1 year ago

if you see anything else: if you're in "declarations mode", consume an ambiguous rule. If this succeeds, switch to "rules" mode. if you're in "rules" mode, consume a rule.

I guess recognizing comments would be in there somewhere too, like if you see a slash, look ahead another character to see if it is a *. Comments could be in either mode.

It would be nice if we also explicitly recognized double slashes as the beginning of a comment that extends to the end of the line. Like SCSS commenting.

Alohci commented 1 year ago

Does & have to be a prefix to a selector, or is it sufficient on its own?

i.e. is &{} exactly equivalent to @nest; ?

sesse commented 1 year ago

&{} would create an empty rule (which would be visible to e.g. CSSOM), so I don't think it's exactly equivalent to @nest.

romainmenke commented 1 year ago

Parser starts in "declarations" mode. If you see an at-keyword, consume an at-rule. Switch to "rules" mode if not already in it. If you see an ident: if you're in "declarations" mode, consume a declaration. If you're in "rules" mode, consume a rule. if you see anything else: if you're in "declarations mode", consume an ambiguous rule. If this succeeds, switch to >"rules" mode. if you're in "rules" mode, consume a rule.

How does this round trip with serialisation when the first at-keyword encountered forms something that browser doesn't understand?

Hypothetically @when shipped in Safari but not in Chrome. I am guessing it parse fine initially in both browser versions. But won't it become invalid?

CSS source :

.foo {
  color: green;

  @when media(min-width: 300px) {
    color: red;
  }

  bar:hover {
    color: purple;
  }
}

first time : rule.cssText :

.foo {
  color: green;

  bar:hover {
    color: purple;
  }
}

second time : rule.cssText :

.foo {
  color: green;
}

Just reading and writing CSS with this syntax can mutate the stylesheet. Is this ok?

tabatkins commented 1 year ago

I guess recognizing comments would be in there somewhere too, like if you see a slash, look ahead another character to see if it is a *. Comments could be in either mode.

Comments are handled at an earlier level in the parser; they get consumed and thrown away whenever you ask for the next token, so none of the parser algorithms see them at all.

It would be nice if we also explicitly recognized double slashes as the beginning of a comment that extends to the end of the line. Like SCSS commenting.

It would be, but likely impossible due to legacy content, unfortunately.

tabatkins commented 1 year ago

Does & have to be a prefix to a selector, or is it sufficient on its own?

It's a selector on its own, so yeah, & {...} is fine.

tabatkins commented 1 year ago

How does this round trip with serialisation when the first at-keyword encountered forms something that browser doesn't understand?

This issue is triggered by more cases than just this - using the OM to insert rules can trigger it too.

The serialization algo for CSSStyleRule will have to handle it - if the first childRule is a CSSStyleRule with a selector that wouldn't trigger the switch, it'll first serialize an @nest;. This'll introduce a no-op rule that didn't exist in the source, but after that it roundtrips perfectly.

argyleink commented 1 year ago

I think the goal of the proposed change is to have syntax closer to SCSS and to have hopefully better developer ergonomics to avoid common programming errors. This would be via the "parser switch" model vs an & before each rule. Let me know if I missed any other goal. :)

Pros

Cons

Because of all of these cons, I feel that we should at least not jump to changing the spec right now, and perhaps consider these changes for a level 2 spec, since the proposed changes are more-or-less backwards compatible.

Below is an enumeration of the possible ways to nest. The current spec has two ways (start with & or start with @nest), and the proposed changes make it 4 (the two original styles, @nest; … and first-selector-starts-with-&). I thought it would be useful to write them out so that everyone can see the actual syntax options in practice.

Current spec options for nesting

.foo {
  & .bar {}
  &.bar {}
  & > .bar {}
  &:hover {}

  @nest .qux & {}
  @nest .qux& {}
  @nest .qux > & {}
  @nest .qux &:hover {}
}

.foo {
  @nest & .bar {}
  @nest &.bar {}
  @nest & > .bar {}
  @nest &:hover {}

  @nest .qux & {}
  @nest .qux& {}
  @nest .qux > & {}
  @nest .qux &:hover {}
}

/* specialty case, nesting @media, no `&` required unless creating new nested selectors */
.foo {
  --brand: hotpink;

  @media (prefers-color-scheme: light) {
    --brand: rebeccapurple;
  }
}

Proposed options for nesting

.foo {
  @nest;

  .bar {}
  &.bar {}
  > .bar {}
  &:hover {}

  .qux & {}
  .qux& {}
  .qux > & {}
  .qux &:hover {}
}

.foo {
  /* moved &.bar to first selector to trigger parser switch, if it's removed the rest break..? */
  &.bar {}
  .bar {}
  > .bar {}
  &:hover {}

  .qux & {}
  .qux& {}
  .qux > & {}
  .qux &:hover {}
}

/* previous syntax options still work */
.foo {
  & .bar {}
  &.bar {}
  & > .bar {}
  &:hover {}

  @nest .qux & {}
  @nest .qux& {}
  @nest .qux > & {}
  @nest .qux &:hover {}
}

.foo {
  @nest & .bar {}
  @nest &.bar {}
  @nest & > .bar {}
  @nest &:hover {}

  @nest .qux & {}
  @nest .qux& {}
  @nest .qux > & {}
  @nest .qux &:hover {}
}

/* unchanged */
.foo {
  --brand: hotpink;

  @media (prefers-color-scheme: light) {
    --brand: rebeccapurple;
  }
}
chrishtr commented 1 year ago

If we end up going with the proposed syntax, I also think we should consider removing the first-selector-starts-with-& mode and instead require @nest; if you want to go into the alternate parsing mode. This would avoid programming errors for authors. Example: suppose you have this selector:

.foo {
  &.bar {}
  .baz & {}
}

And then you decide actually the .bar rule was not needed for some reason. Then you go ahead and edit it to delete that line, resulting in:

.foo {
  .baz & {}
}

This selector is now an error, because there was no rule starting with an & and @nest was not present in the rule or at the start of the selector block. The result could be a silently broken site where all the baz widgets are broken mysteriously.

Removing first-selector-starts-with-& would also make use of @nest; purely opt-in, and have no side-effects on authors who prefer not to use it.

tabatkins commented 1 year ago

@chrishtr Look up at https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1270665794 - I believe we can make the detection more aggressive so those examples work just fine.

chrishtr commented 1 year ago

@chrishtr Look up at #7834 (comment) - I believe we can make the detection more aggressive so those examples work just fine.

Oh ok. Would all examples work though?

Loirooriol commented 1 year ago

I think some arguments from Tab in #5738 are still very relevant against the new proposal:

(The current spec has 2 cases, I have lost track of how many different variants the new proposal has. So less learnable)

Loirooriol commented 1 year ago

And regarding to defaulting to descentant combinator, it's not clear to me at all why

.foo {
  color: blue;
  @nest; /* BTW, I'm not even sure if this is needed :( Would the colon suffice to switch the parser? */
  :hover { color: cyan }
}

would be like

.foo { color: blue; }
.foo :hover { color: cyan; }

and not e.g.

.foo { color: blue; }
.foo:hover { color: cyan; }

I think requiring & makes the result much easier to understand. I would only allow to omit a leading & when the selector starts with a combinator symbol. And since the descendant combinator doesn't have any symbol of its own, then require explicit & (or reconsider adding >> as the proper descendant combinator).

I know that .foo:has(:hover) defaults to the descendant combinator, but at least there this behavior is kinda implied by the :has name, and it would be pointless if it resolved to .foo:hover. That's not the case with nesting.

tabatkins commented 1 year ago

And regarding to defaulting to descentant combinator, it's not clear to me at all why

This is how Sass and every Sass-like preprocessor works. It's not new, it's precisely what authors would expect if we allowed naked nesting like this.

(The current spec has 2 cases, I have lost track of how many different variants the new proposal has. So less learnable)

Both proposals have exactly two cases, so I'm not sure what you're referring to.

If going with the proposal as originally presented, the cases are:

If going with my more aggressive parsing suggestion, the first case changes from "starts its selector with &" to "starts its selector with anything but a tagname selector". The second case is identical.

romainmenke commented 1 year ago

One of the things that keeps nagging at me is that this proposal is a 180 on so many aspects that have been discussed before. Things that were advocated against are now suddenly advocated for. I don't understand or see what is so different about the whole of this proposal.

Nesting is a complex topic and so much has been said about this. I also do not want to bring up specific quotes (unless asked for) because I believe anyone can change their mind or point of view.

Re-reading previous threads and the great insights shared about the reasoning behind the current syntax, I just don't understand how this proposal is disregarding all that.

It feels as if a giant leap was taken by some and the reasoning behind it isn't being properly explained.


romainmenke commented 1 year ago

@argyleink said in https://github.com/w3c/csswg-drafts/issues/7834#issuecomment-1272155603 :

I think the goal of the proposed change is to have syntax closer to SCSS and to have hopefully better developer ergonomics to avoid common programming errors. This would be via the "parser switch" model vs an & before each rule. Let me know if I missed any other goal. :)

There is another goal, to have support for relative selectors in nested selector contexts. Relative selectors sometimes (not always!) copy-paste nicely between contexts.

The logic appears to be :

  1. we want relative selectors in nesting because relative selectors copy-paste nicely to other contexts
  2. relative selectors imply .foo { bar { /* decl */ } } and the parser algorithms don't allow that
  3. we need the @nest; noop to fix the parser issue.
  4. the resulting syntax happens to also be more similar to Sass-style nesting

But there might be different ways to have relative selectors in nesting. I've opened https://github.com/w3c/csswg-drafts/issues/7854 to explore one possible alternative.

Pros

Folks who aren’t thrilled to add an & before their nested selectors will appreciate the familiarity and convenience of the new proposal. They’d also appreciate the flexibility to use more than one way of expressing the syntax.

This could be solved in preprocessors where the ambiguity is less of an issue. The parsers in preprocessors are of a different design.

Not saying that it can not be solved in the syntax, but syntactic sugar is what tooling is really great at.


The learning curve increases for anyone not coming from SCSS, because the previous syntax's simplicity and rigidity was a feature. Folks who do know SCSS still have things to learn, it's not a free switch. For example, BEM style nesting won't port over and most copy/paste scenarios will require throwing the @nest; switch in "just in case" or an "I don’t know why, I just always put it there".

Increasing the flexibility also increases the potential for accidental programming errors. More options also means more cases to be aware of, or the style sheet ends up with errors. It gives some but takes some. Increased flexibility also means ramping up to more nesting styles when reading other authors’ nested styles, and increases friction to understanding style sheets between teams (or even within the same team between selectors that do or don’t have @nest; or when importing third-party sheets.

I think this proposal has less flexibility. It is only more flexibly when writing nested CSS from scratch. At that point you can chose between multiple options.

And copy-paste from Sass (which is a one time action) happens to be correct more often than with the current syntax.

But it places more burden on readers because they have to figure out again and again which of the modes is currently in play.

It also restricts the available options when refactoring code. Needing to alter unrelated CSS to make it valid again after removing a rule should be avoidable in the syntax design.

How does it interact with git merges? What if your colleague removes the first rule and you changed the selector of the second?

In the current specification each rule is (in)valid on its own. There is no spooky action at a distance. This makes reading easier and enables more mutations.


Below is an enumeration of the possible ways to nest. The current spec has two ways (start with & or start with @nest), and the proposed changes make it 4 (the two original styles, @nest; … and first-selector-starts-with-&). I thought it would be useful to write them out so that everyone can see the actual syntax options in practice. Current spec options for nesting

I am unsure if @nest .foo { /* ... */ } still exists in this proposal. I don't see it mentioned anywhere.

LeaVerou commented 1 year ago

It seems that all of the pushback centers around requiring the first selector to start with & (unless preceded by an @-rule) while subsequent ones don't need to. We are converging towards a variation of the proposal where the only cases that actually require the & are selectors that start with a tag name.

Here's a crazy idea: What if we just don't allow nested selectors to start with a tag name at all, anywhere? Then there is no different handling of the first rule which some people seem to react strongly against, and any nested rule kicks the parser into rule mode, and authors can always use :is(tag) for the cases where they need this (or just & tagname). With this proposal, we can probably even entirely drop @nest (but we don't have to).

There is somewhat similar precedent in CSS: regular selectors cannot have tags anywhere in the middle, they can only start with a tag, unless :is()/:where() is used. Not the same thing, but there is precedent for having special parsing rules around tag selectors.

Also, if at some point in the future CSS grammars have to become LR(2) for other reasons, we can allow nested selectors to start with a tag then.

Note that this will be valid with this proposal:

.foo {
    .bar strong {
    }

    & strong {

    }
}

And regarding to defaulting to descentant combinator, it's not clear to me at all why

Not just preprocessors, but also our own :has(), and @scope.

devongovett commented 1 year ago

I think & should be required in all nested selectors because it makes it clear where the parent selector is inserted and how the selectors are combined. Not requiring it leaves the reader guessing.

I believe this was a mistake in SASS and other pre-processors that we now have the opportunity to fix. Preprocessors could still support their existing syntax and output native nesting with the & inserted if they want. I think CSS should have the least ambiguous syntax possible, and leave the sugar to the preprocessors.

romainmenke commented 1 year ago

It seems that all of the pushback centers around requiring the first selector to start with & (unless preceded by an @-rule) while subsequent ones don't need to.

My pushback is because you can omit it in subsequent selectors, not because it is required in the first selectors. (just to clarify :) )

I agree with @devongovett that & should be required in nested selectors.


But this is again a simplification of the arguments against this proposal. @nest; is also highly problematic in my opinion.

Too much is sacrificed for the features of this proposal (relative selectors, easy copy-paste)

jimmyfrasche commented 1 year ago

Star hack. Tag names and *?