csswg-drafts icon indicating copy to clipboard operation
csswg-drafts copied to clipboard

[css-nesting-1] Syntax Invites Errors

Open fantasai opened this issue 3 years ago • 141 comments
trafficstars

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 avatar Oct 05 '22 19:10 fantasai

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

  • relative selectors, like in @scope and :has() (which essentially imply a prefixed & )
  • &-anchored selectors, as in the current proposal, to enable patterns like &.foo or .foo &
    • Note: see https://github.com/w3c/csswg-drafts/issues/5745 for making these valid everywhere

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

fantasai avatar Oct 05 '22 20:10 fantasai

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.

tabatkins avatar Oct 05 '22 21:10 tabatkins

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;
      }

romainmenke avatar Oct 05 '22 21:10 romainmenke

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 avatar Oct 05 '22 22:10 mirisuzanne

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)

mirisuzanne avatar Oct 05 '22 22:10 mirisuzanne

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.

astearns avatar Oct 05 '22 22:10 astearns

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 avatar Oct 05 '22 22:10 LeaVerou

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.

LeaVerou avatar Oct 05 '22 22:10 LeaVerou

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

dbaron avatar Oct 06 '22 00:10 dbaron

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

romainmenke avatar Oct 06 '22 01:10 romainmenke

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.

bradkemper avatar Oct 06 '22 03:10 bradkemper

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

argyleink avatar Oct 06 '22 04:10 argyleink

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.

bradkemper avatar Oct 06 '22 05:10 bradkemper

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

romainmenke avatar Oct 06 '22 10:10 romainmenke

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

LeaVerou avatar Oct 06 '22 14:10 LeaVerou

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.

devongovett avatar Oct 06 '22 15:10 devongovett

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 avatar Oct 06 '22 16:10 mirisuzanne

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.

mirisuzanne avatar Oct 06 '22 16:10 mirisuzanne

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.

LeaVerou avatar Oct 06 '22 16:10 LeaVerou

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.

jimmyfrasche avatar Oct 06 '22 16:10 jimmyfrasche

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.

LeaVerou avatar Oct 06 '22 17:10 LeaVerou

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.

devongovett avatar Oct 06 '22 17:10 devongovett

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.

romainmenke avatar Oct 06 '22 17:10 romainmenke

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 avatar Oct 06 '22 18:10 LeaVerou

@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?

LeaVerou avatar Oct 06 '22 18:10 LeaVerou

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 avatar Oct 06 '22 18:10 romainmenke

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

romainmenke avatar Oct 06 '22 18:10 romainmenke

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.

tabatkins avatar Oct 06 '22 20:10 tabatkins

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

tabatkins avatar Oct 06 '22 20:10 tabatkins

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.

Loirooriol avatar Oct 06 '22 22:10 Loirooriol