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] Syntax Invites Errors #7834

Closed
fantasai opened this issue Oct 5, 2022 · 179 comments
Closed

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

fantasai opened this issue Oct 5, 2022 · 179 comments

Comments

@fantasai
Copy link
Collaborator

fantasai commented Oct 5, 2022

Edit: 👉🏼 UPDATED SUMMARY TABLE OF SYNTAX PROPOSALS 👈🏼 (from #7834 (comment) )


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

  • 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
  • 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, and makes automated (and manual) conversion of existing code much more difficult (requires selector parsing rather than regex).
  • &.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
@fantasai fantasai added the css-nesting-1 Current Work label Oct 5, 2022
@fantasai
Copy link
Collaborator Author

fantasai commented Oct 5, 2022

[ @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:

  • A &-prefixed style rule (possibly empty)
  • A specific delimiter token such as && or @.
  • At rules, sub-options being:
    • A specific @rule such as @nest;
    • Any known @rule, including a new (essentially otherwise no-op) @nest;
    • Any conditional rule or @nest;
    • Any statement @rule (including @nest;) but not any block @rules
    • Any @rule known or unknown

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:

  • If we allow any at-rule, known or unknown, to act as a trigger, then we we will have difficulty in the future if we want to introduce an at-rule that can be interleaved with declarations.
  • If we restrict to a known set of at-rules, then we have difficulty in the future when we introduce another at-rule to the set, because new style sheets using that as the trigger won't trigger in older browsers.

Backwards compatibility considerations:

  • Allowing &-prefixed rules to trigger is means existing nested code continues to work.

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

  • An &-prefixed style rule
  • Any at-rule, known or unknown (and introduce @nest; as a convenient convention)

Pros/cons of triggering on &-prefixed rules:
- Pro: compatibility with existing nested code
- Pro: less noisy than introducing @nest everywhere
- Con: first selector in a set of nested style rules has this special requirement, which is an odd positional requirement

Pros/cons of triggering on @nest:
- Pro: nested style rules are all subject to the same requirements
- Con: Lots of visually-impactful clutter in highly structured stylesheets

Pros/cons of triggering on new delimiter:
- Pro: nested style rules are all subject to the same requirements
- Pro: less noisy than introducing @nest everywhere
- Con: such a delimiter is consequently banned from Selector grammar forever

Agenda+ to discuss

@tabatkins
Copy link
Member

tabatkins commented Oct 5, 2022

Pulling out for clarity, the suggested change is:

  • Switching https://w3c.github.io/csswg-drafts/css-syntax-3/#consume-style-block from a per-nested-rule logic to a parsing switch: the parser starts parsing in a "declarations and at-rules" mode, but as soon as it sees an at-rule or an &, it switches to an "at-rules and style rules" mode. (This at-rule does not have to be valid to trigger this, as otherwise it means your nested rules might not work due to an unrelated at-rule not being supported yet. In the Syntax spec I'll literally just trigger it upon seeing the at-keyword token.)
  • Nested style rules relax their syntax requirements and just use <relative-selector-list>. They no longer require an &, at the start or anywhere. They work with the existing Sass/etc logic - if an & is mentioned in the selector, and the selector doesn't start with a combinator, it's used as-is; otherwise, it's assumed to be a relative selector chaining off of &. That is, .foo is interpreted as & .foo, + .foo is interpreted as & + .foo, but .foo & is left as-is.
  • We still introduce @nest, but just as a no-op rule that can be used to trigger the "see an at-rule" switch, if your first nested selector doesn't or can't start with an &. CSSOM will use this if necessary when serializing, if the first rule in its .cssRules list is a CSSStyleRule.

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
Copy link
Member

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
Copy link
Contributor

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
Copy link
Contributor

mirisuzanne commented Oct 5, 2022

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)

@astearns
Copy link
Member

astearns commented Oct 5, 2022

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
Copy link
Member

LeaVerou commented Oct 5, 2022

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
Copy link
Member

LeaVerou commented Oct 5, 2022

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
Copy link
Member

dbaron commented Oct 6, 2022

  • 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
Copy link
Member

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
Copy link
Contributor

bradkemper commented Oct 6, 2022

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
Copy link
Contributor

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

@bradkemper
Copy link
Contributor

bradkemper commented Oct 6, 2022

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
Copy link
Member

romainmenke commented Oct 6, 2022

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
Copy link
Member

LeaVerou commented Oct 6, 2022

  • 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
Copy link
Contributor

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
Copy link
Contributor

mirisuzanne commented Oct 6, 2022

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
Copy link
Contributor

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
Copy link
Member

LeaVerou commented Oct 6, 2022

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:

  • In CSS, the last semicolon in a declaration list is optional
  • In CSS, the last closing brace is optional
  • In JS, commas after array elements are mandatory, except for the last one
  • In JS, commas after function arguments are mandatory, except for the last one
  • In JS, commas after key:value pairs in object literals are mandatory, except for the last one

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
Copy link

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
Copy link
Member

LeaVerou commented Oct 6, 2022

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
Copy link
Contributor

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
Copy link
Member

romainmenke commented Oct 6, 2022

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)

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
Copy link
Member

LeaVerou commented Oct 6, 2022

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
Copy link
Member

LeaVerou commented Oct 6, 2022

@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:
.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
Copy link
Member

romainmenke commented Oct 6, 2022

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:

  • at rules come first, so you already have a trigger for the parser switch
  • modifiers come second (&:focus or &.something--modifier)
  • children third (.child is equivalent to & .child).
  • complex things hidden at the end.

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
Copy link
Member

romainmenke commented Oct 6, 2022

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
Copy link
Member

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):

  • if you see an at-keyword, consume an at-rule
  • if you see an ident, consume a declaration
  • if you see anything else, this is a parse error; throw out everything until you see a semicolon

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

  • 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.

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.

@sesse
Copy link
Contributor

sesse commented Oct 27, 2022

The spec is now updated to Option 3, after a quick hack session with @argyleink; we haven't touched the CSSOM section yet, but the syntax stuff is all done (both in Nesting and in Syntax).

Blink changes are also in review, although they've been done independently, so I'll need to check whether I diverged from the spec at some point. There seems to be still some issues to file to get the interpretation of e.g. :is() 100% nailed down.

Re: serialization of selectors, we shouldn't do any magical insertion or dropping of stuff. The spec defines how to interpret selectors, but there's no need to "normalize" them to any particular form.

This feels odd. We're already normalizing selectors; in particular, for whitespace. Also, of course we (implementors) need to do magical insertion of stuff; that's the only way we can understand what these nested selectors mean and match against them. If & > .a and > .a were to serialize differently, we would probably still store the &, but have some sort of “this & was added implicitly” bit solely for the point of serialization. Which feels a bit pointless, but isn't difficult per se (we already do it for relative selectors).

I guess maybe we're coming from different sides here; I see the spec talks about everything being relative selectors now, except that sometimes they're not (and you don't know until after you're done parsing them). I don't intuitively interpret “.foo” standalone as a relative selector, and I think losing the “interpret & as :is(…parent list…)” is a bit sad (the new text just says “any relative selectors are relative to the elements represented by the nesting selector”, which sort of makes the :is() equivalence a secret and its specificity less than obvious?). But if you do define most of those selectors as relative, I understand that you wouldn't want to convert them on serialization.

@tabatkins
Copy link
Member

We're already normalizing selectors; in particular, for whitespace. Also, of course we (implementors) need to do magical insertion of stuff;

We don't "normalize" serialization for other relative selectors, like in :has(); this should be the same.

I guess maybe we're coming from different sides here;

If you're familiar with Sass or many other "nested CSS" preprocessors, the syntax as the spec now describes is pretty much exactly what they do. (Just with the "no starting with an ident" restriction.) If you're not familiar with those tools, yeah, it might not be the most obvious at first, but it seems to be a very popular and understandable syntax since it's been around for so many years and copied in so many tools.

I think losing the “interpret & as :is(…parent list…)” is a bit sad (the new text just says “any relative selectors are relative to the elements represented by the nesting selector”, which sort of makes the :is() equivalence a secret and its specificity less than obvious?).

The section that defines the nesting selector still talks about the :is() equivalence, rather explicitly.

@romainmenke
Copy link
Member

The spec is now updated to Option 3, after a quick hack session with argyleink; we haven't touched the CSSOM section yet, but the syntax stuff is all done (both in Nesting and in Syntax).

PostCSS plugin changes are mostly done : https://www.npmjs.com/package/@csstools/postcss-nesting-experimental
This should make it possible for authors to experiment more easily with the updated nesting proposal and with relative selector syntax.

@sesse
Copy link
Contributor

sesse commented Oct 29, 2022

Blink changes are also all but done, but we figured out that the syntax changes break several tests (including ACID2), so it can't go in yet.

@sesse
Copy link
Contributor

sesse commented Nov 1, 2022

Chromium as of 109.0.5394.0 is updated with the new syntax.

@mrleblanc101
Copy link

mrleblanc101 commented Nov 3, 2022

Why not adopt the SCSS syntax entirely ?

I know it could cause parsing issue of existing CSS, but we already had a similar problem with HTML5. So we simply created a new Doctype to handle that.

Why not just add a declaration at the top of the file to tell the browser to parse this file differently ? We already have something like this with @charset "UTF-8"

Something like this:

@nesting 

.my-class {
  nested-tag {}
  .nested-class {}
}

@sesse
Copy link
Contributor

sesse commented Nov 3, 2022

Why not adopt the SCSS syntax entirely ?

As has been discussed in the thread already, and also noted in the draft standard, the case of “nested-tag {}” would be ambiguous to parse without increasing the parser's lookahead (either explicitly or by implicit means, such as restarts).

I know it could cause parsing issue of existing CSS, but we already had a similar problem with HTML5. So we simply created a new Doctype to handle that.

The word “simply” is not one that comes to mind if you're suggesting maintaining two entirely separate CSS parsers indefinitely (both in the spec and in code).

@LeaVerou
Copy link
Member

LeaVerou commented Nov 3, 2022

Why not adopt the SCSS syntax entirely ?

Blink implementers refused to consider it. @emilio said it could have been feasible for Gecko. See #7961 where I spent a fair bit of my time trying to come up with a solution that involves only a minimal performance hit (the backtracking would be limited to only element:pseudo nested selectors, which are fairly rare in nested stylesheets). This was the response from @sesse :

I don't share this goal; I would like us to just get the syntax (any syntax) specified, out the door and be done with it. So no, I won't be spending resources trying to bend our minds around how we tokenize CSS to this avail; I can NAK what is a non-starter for us, and that's what you'll get.

He also mentioned having to give up some performance optimizations to go that route, without responding to my question asking what they are so it could be more widely examined if they'd really need to go.

But hey, at least the syntax we (thus far) went with allows for the syntax to be relaxed in the future (perhaps once it becomes obvious to everyone how much that would increase developer ergonomics).

@devongovett
Copy link
Contributor

There are a lot of assumptions and personal opinions being thrown around here. Your own poll showed that developers prefer to write the & in every selector: https://twitter.com/leaverou/status/1579902585540345857. Yet this was pushed through anyway. It does not seem like feedback is being taken seriously, both from implementors and from authors.

@LeaVerou
Copy link
Member

LeaVerou commented Nov 3, 2022

There are a lot of assumptions and personal opinions being thrown around here. Your own poll showed that developers prefer to write the & in every selector: twitter.com/leaverou/status/1579902585540345857. Yet this was pushed through anyway. It does not seem like feedback is being taken seriously, both from implementors and from authors.

This has already been addressed, I think by @fantasai: The poll showed that about half of developers (52%) want to include & in every selector, and half (48%) do not. Making it optional caters to both; the first group can still include & in every selector. Making it mandatory only caters to one of the two groups.

Also, if you read the comments, there was some confusion about what an optional & would be, many assumed it would be optional in every case, including things like &.foo, &:hover, .foo & etc and wanted to change their vote when they realized that was not the case. This was clarified in the subsequent poll.

I understand that you feel strongly about this, but right now none of the proposals considered by the group involves a mandatory &, so this is not a productive debate, and I'm not going to engage in it further. Perhaps we can agree that if & is optional, it's better if it's optional in every case where selectors begin with & and a combinator? (I do not think it's so much better that it's warrants moving to a postfix syntax, but if we could get it to be optional in every case and keep a nested syntax, that would be ideal).

@romainmenke
Copy link
Member

romainmenke commented Nov 3, 2022

For starters, I personally like the syntax of proposal 3 on principle.
I would like it even more if we could omit & everywhere. It is just more consistent.

I like it because it gives authors more options and from an author perspective this is always a good thing :

  • you can write & everywhere
  • you can use a linter to enforce & everywhere
  • you can omit & everywhere

I do not have strong opinions about nesting itself.
For me nesting is a minor feature and should not dominate the language.

This for context because it has been pointed out that I apparently have strong feelings about all this and that I oppose the syntax change. I do not.
I've spend the past weekend making sure there is a PostCSS plugin for it. I am fully on board with the resolution to switch to proposal 3.

I will miss @nest blocks but not that much. I thought they were an elegant "catch all" solution for writing nested CSS. Something you truly do not have to think about, it just works.

I do have strong opinions about a parser switch.
This was only part of proposal 2.

I also have strong opinions about ignoring side-effects and downsides and I prefer having this made clear and explicit.


This as a prelude to say that I don't have an issue with the substance of this thread.
I do however have an issue with how this all played out.

I don't think it is ok to have a one sided discussion were one groups arguments are always immediately discarded.

I definitely don't think it is ok to call out individuals and mark them as solely responsible for why we can't have a certain syntax.

I think we can and must do better.
These issues are not important enough for all this drama, none are.

@devongovett
Copy link
Contributor

This was clarified in the subsequent poll.

It should be noted that this poll has significantly fewer votes. The difference between them is also very subtle.

I understand that you feel strongly about this, but right now none of the proposals considered by the group involves a mandatory &

The previous syntax (option 1) was exactly this.

@LeaVerou
Copy link
Member

LeaVerou commented Nov 3, 2022

The previous syntax (option 1) was exactly this.

I said "considered by the group", not "in existence". My understanding is that it's down to 3 vs 4 right now, and all other proposals are off.

@devongovett
Copy link
Contributor

Exactly my point. This was pushed through, despite significant pushback, by a few members of the working group with very strong opinions.

I'll back out of the discussion now because it doesn't seem to be going anywhere, but I am disappointed by the process here.

@pyronaur
Copy link

pyronaur commented Nov 9, 2022

Exactly my point. This was pushed through, despite significant pushback, by a few members of the working group with very strong opinions.

I'll back out of the discussion now because it doesn't seem to be going anywhere, but I am disappointed by the process here.

This is why I don't even attempt to participate in the discussion.

If Scss syntax can't be used, I don't care what the outcome is, I'll still use a preprocessor anyway.

If efficiency is what browsers care about, maybe a new "efficient syntax" needs to be created instead and we should all use prepeocessor of choice to compile to that.

@cdoublev
Copy link
Collaborator

The spec is now updated to Option 3, after a quick hack session with @argyleink

Can you please clarify why consume a style block's content looks for an identifier or a functional notation to consume a declaration? Is it to anticipate a future declaration syntax?

  • <ident-token>
  • <function-token>
  • <function>
    Reconsume the current input token. Initialize a temporary list, initially empty. As long as the next input token is anything other than a <semicolon-token> or <EOF-token> , consume a component value and append it to the temporary list. Consume a declaration from the temporary list. If anything was returned, append it to decls .

@LeaVerou
Copy link
Member

@cdoublev See #7834 (comment)

@devongovett
Copy link
Contributor

Should this issue be closed, or is something still outstanding here? I see the spec was updated already in d30c9f6. I have implemented the changes in Lightning CSS (parcel-bundler/lightningcss#340) as well, and will ship soon behind a flag, but if more changes are expected I will hold off.

@tabatkins
Copy link
Member

Note just as a general rule that anything is subject to change until a browser ships it to their stable channel and it starts being used by a non-trivial number of pages, so that changing it would cause too much breakage.

That said, I personally do not believe it will change further from where it currently is.

@fantasai
Copy link
Collaborator Author

The CSSWG resolved on Option 3 as described in these minutes.

@LeaVerou
Copy link
Member

LeaVerou commented Feb 8, 2023

@tabatkins do you happen to know what happened to the Markdown file of proposals? I just looked it up for a TAG discussion and it appears to have been deleted (I could only find it in the history, here)

@tabatkins
Copy link
Member

Yeah, I deleted it earlier this month; it didn't seem like it was still needed. If you want to preserve it as an artifact, feel free to do so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests