w3c / csswg-drafts

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

[css-syntax][css-nesting] Design of `@nest` rule #10234

Closed LeaVerou closed 4 months ago

LeaVerou commented 6 months ago

Opening this as requested by @astearns

In #8738 we resolved to stop hoisting interleaved declarations and introduce an @nest rule that means "exactly the same thing as the parent" instead of wrapping in :is(), which is how interleaved declarations will be represented in the CSS OM. Since we were not able to get consensus on the specifics, but we had consensus that any solution along these lines is better than the status quo, we agreed that Tab would spec whatever (commit here), and we'd discuss the details later, since fixing the specifics is more web compatible than changing the current behavior after even longer.

[!NOTE] An interleaved declaration is a declaration that comes after one or more nested rules.

The issues around which we could not reach consensus were:

  1. If authors have no reason to write @nest and it’s only introduced to represent interleaved declarations and rules, should they even be able to?
  2. If @nest rules are magically added around interleaved declarations should they also be removed during serialization?
    1. If they are removed during serialization, does this happen always, or only when part of a larger rule (e.g. as part of .cssText)?
  3. Do we even need a new @nest rule? What if we simply use the existing CSSStyleDeclaration object to represent interleaved rules? (proposed by @mdubet)
  4. How does setProperty() work if we go with one of the designs that involve more magic?
  5. What happens when a rule is removed and thus two sets of interleaved declarations become adjacent?
    1. Similar issue not brought up in the call: what about the case when these rules are first? Should they be merged with rule.style?

These are not orthogonal decisions: it seems clear that if @nest serializes to include an actual @nest {} rule, that @nest rule needs to also be valid author code. So essentially there are three possible designs:

  1. Magic-minimizing @nest (proposed by @tabatkins, supported by @emilio @andruud @Loirooriol): The rule is automatically added around interleaved declarations, but there is no more magic besides that.
  2. Author-exposure minimizing @nest (proposed by @LeaVerou, supported by @fantasai @astearns): The rule becomes a CSS OM detail, with no corresponding CSS syntax, and is removed on serialization (regardless of how serialization happens).
  3. No @nest, just CSSStyleDeclaration in the CSSOM (proposed by @mdubet, supported by @LeaVerou @fantasai).
    • Criticism: That means .cssRules will also return non-rules? Would .insertRule() also accept CSSStyleDeclaration?

For 2 and 3, there are also design variations based on the answer to 4 and 5 above.

My position:

So I would propose a design that would minimize author exposure to all of this, and would just try to do what's reasonable when reading and modifying the CSS OM:

Should rule.style be magic?

One thing I'm ambivalent about is whether rule.style should be magic too. This would mean:

Pros & Cons:

If we decide to avoid magic here, we can make the API more palatable by:

andruud commented 5 months ago

@fantasai

@andruud

That means it won't round-trip "structurally", like we talked about before. But maybe that's not so bad.

It would, why wouldn't it?

If you do e.g.:

.a {
  --x: 1;
  .b { ... }
  --y: 2;
}

If you now deleteRule the .b, then we presumably have two CSSNestingDeclarations that will collapse into one when round-tripped.

I think whether the first declaration block is represented as a rule is up for discussion too. Perhaps there's less to settle if we don't do that.

I think this makes many of the problems go away, yes, but probably not all ...

And the reason we made this trade-off is because we are willing to accept a slightly less nice CSSOM (which is already quite terrible with all its string-based APIs and reportedly only used by 1% of web developers) to the benefit of all other web developers.

Is there really no rule-like form @nest can take which makes it OK for the other 99% to occasionally encounter it [1]? @group? Or even a prelude-less {}? Additionally, we can reduce the exposure with Tab's serialization proposal, making the flattened representation the canonical one.

[1] Which can only happen if they write it themselves, or if they're reading the CSS of someone else who did.

annevk commented 5 months ago

I think if we can agree that insertRule() can modify an existing CSSNestingDeclarations, deleteRule() should maybe be able to do the same thing?

To us it's the mere idea of adding syntax to CSS purely for CSSOM that seems backwards so I don't think alternative ways of writing that would remove the objection.

andruud commented 5 months ago

I think if we can agree that insertRule() can modify an existing CSSNestingDeclarations, deleteRule() should maybe be able to do the same thing?

I would rather just accept that CSSNestingDeclarations can exist next to each other and that they'll collapse into one if round-tripped. Then we can keep insertRule etc. about as dumb as they are today.

annevk commented 5 months ago

That's fair. One thing that makes me wonder about is if that would end up invalidating some implementation assumptions. Such as that

background:red;
background:lime;

would only give a single background declaration within what is conceptually a single declaration block. Seems manageable though.

tabatkins commented 5 months ago

And the reason we made this trade-off is because we are willing to accept a slightly less nice CSSOM (which is already quite terrible with all its string-based APIs and reportedly only used by 1% of web developers) to the benefit of all other web developers.

I don't quite understand this reasoning, tho - the effects of this are, as far as I can tell, only really observed by people doing the same sort of CSSOM manipulation. You'd only see it if you're stringifying rules and looking at the result. Do you have an example use-case where the stringification helps other people outside of CSSOM-manipulators?

This is the reason I keep harping on (a) the audience and (b) the benefit - I still don't think this has been adequately argued for.

annevk commented 5 months ago

If you introduce a new at-rule, it will show up in lists of at-rules, and people will wonder what it's for. People will encounter it here and there and have to look up what it means, etc. I.e., you add to the complexity of CSS syntax.

tabatkins commented 5 months ago

Right, so we're judging the balance of one authoring cost (having to bear the psychic weight of knowing this do-nothing rule exists, if you're the sort of person who reads lists of at-rules) vs another authoring cost (having to deal with more complicated/magical behavior for several OM methods), plus the impl/spec cost of that magic. (And the future spec cost of having to engineer around this magic; as I said above, I think this would block us changing to an ObservableArray for .cssRules, or at least make such a switch way more complicated and fragile.)

I think "knowing a do-nothing rule exists" is a lot less pain than "dealing with weird behavior when doing rule-tree manipulation".

(I speak from experience here; I reimplemented most of the DOM for myself in Python, even with all its warts, because the XML library I'm using in Bikeshed has a terrible data model where text is different from other nodes, and many of its methods have odd magic behavior in the same vein as what's being suggested here. I kept accidentally writing broken code as a direct result of the magic (text would get deleted, or duplicated, or moved in unpredictable ways), so using the DOM and its shitty but simple and consistent "everything is a node" behavior was way better.)

I imagine tutorials would just look something like:

@nest rules: sometimes implicitly produced by the CSS parser when you're mixing declarations after rules. You can write it yourself, but it doesn't actually do anything. Most of the time CSS doesn't serialize them, either, so you'll only see them if you're using them manually, or manipulating the CSSOM directly.

We can also make the name much less attractive and more obviously internal, if that would help, like @implicit-nested-properties-group {...} or something. Slightly on the ridiculous side for length, so it's not something an author would ever reach for when writing code manually, but also a very descriptive name that suggests what it's doing immediately, and is much easier to google for.

Loirooriol commented 5 months ago

If WebKit keeps objecting to @nest, instead of making CSSOM nonsensical, I would prefer revisiting https://github.com/w3c/csswg-drafts/issues/10234#issuecomment-2079994176:

LeaVerou commented 5 months ago

If WebKit keeps objecting to @nest

Both Blink and WebKit are objecting here (to different things). If only one was objecting, we would be able to move forwards. Blink is also being inflexible here.

At this point, I'd be fine with anything, because we're fast approaching the point where we'd be stuck with the hoisting behavior, which is worse than any proposed solution.

If we go the route of a new @-rule, I did propose naming it @group which is something not tied to nesting, that we can layer behavior on top of later.

romainmenke commented 5 months ago

I still like the @group proposal but I also wonder if we would design this exact thing if we didn't need it to patch the wart for nesting.

Would we actually spec @group tobikeshed { } with the IACVT behavior? Or would we pick something more like @fallback { }, @catch { } or even extend @try { }?


If @group <ident> {} is something we want anyway then it would be good if WebKit could give feedback on this proposal. They only gave feedback on @nest {} and specifically on it having no purpose. Exactly this feedback was already addressed by the @group {} proposal.

emilio commented 5 months ago

My understanding is that Blink and Gecko are both on the same page, in terms of how to best move forward...

LeaVerou commented 5 months ago

@romainmenke This is about the name, any <ident> behavior would be added later. L1 would be just @group {}.

@emilio My understanding from the conversations so far though is that it's Blink that is being inflexible though; Gecko agrees with Blink, but is willing to go a different route if we have consensus for it.

romainmenke commented 5 months ago

The objection from WebKit is that @nest {} doesn't have a purpose other than fixing this wart. @group {} ultimately has the same issue unless we already commit to giving it a purpose later. I am not sure we should be designing new syntax in this order.

In this way I also agree with @tabatkins when they said:

We can also make the name much less attractive and more obviously internal, if that would help, like @implicit-nested-properties-group {...} or something. Slightly on the ridiculous side for length, so it's not something an author would ever reach for when writing code manually, but also a very descriptive name that suggests what it's doing immediately, and is much easier to google for.

mdubet commented 5 months ago

Precisely, the objection is that @nest {} doesn't have any purpose other than fixing this wart and will not have one in the future neither because the name is very specific (and confusing with the nesting selector).

EDIT: This doesn't imply at all that we would accept @group or @very-weird-name (because as Romain said, they have the same issue of extending the CSS syntax for no reason except this wart). My comment just reiterates the multiple reasons we have rejected @nest.

Loirooriol commented 5 months ago

@LeaVerou To clarify, I wasn't trying to blame WebKit for not being able to move forward. With "keeps objecting" I meant if possible mitigations like https://github.com/w3c/csswg-drafts/issues/10234#issuecomment-2123407797 don't convince them to drop the objection to @nest.

astearns commented 5 months ago

I think I prefer @some-weird-name to @group, because if authors somehow do find a use for it that is different or more specific than grouping or nesting, we can replace/alias the weird name with something more closely matching the discovered use.

jensimmons commented 5 months ago

Could someone who believes @nest is a good solution, please answer this question… pretend a community college professor is emailing you, and asks: "I want to teach my students to use CSS Nesting. Could you explain when / how / where they should use the new @nest rule? What does that nested CSS look like?"

emilio commented 5 months ago

I want to teach my students to use CSS Nesting. Could you explain when / how / where they should use the new @nest rule? What does that nested CSS look like?

I don't see what the issue would be with an answer like "You should never need to write @nest manually, it gets added where needed by the CSS parser."

fantasai commented 5 months ago

@emilio It's necessary to write manually if you're using .insertRule() though, isn't it?

Also people still write </p> even though it has always been added where needed by the HTML parser. I think we can expect the same for @nest and similar once it exists.

emilio commented 5 months ago

Sure, it would if you're calling insertRule or so.

I don't see how this compares to </p>. Omitting closing tags in HTML is the exception, while the intuitive syntax for CSS nesting would be the rule.

tabatkins commented 5 months ago

It's necessary to write manually if you're using .insertRule() though, isn't it?

As I keep arguing, that's a very advanced usage. You don't teach people how to use .insertRule() (or really any of the OM except .style, probably) in a beginner CSS course. It's something needed extremely rarely, for advanced CSS tooling use-cases only. (Personally, I have never, not a single time, used it in my own webdev.)

And, as I've argued, the cost of "when I, a CSS tooling author, use .insertRule(), I have to insert a rule of some sort (and @nest, or whatever we call it, is the easiest) to insert a block of declarations between two other rules" is more than outweighed by the benefit of "when I, a CSS tooling author, do arbitrary manipulations to the OM, everything works in a simple and expected way, and the structures appear in close correspondence with the method calls I make".

(Plus, we could certainly still add .insertDeclarations(), which creates the rule for you automatically. If that's a use-case we want to make easier, I think that's a very reasonable thing to do. It's roughly neutral on typing (longer method name, but you don't need to add the @nest{ and } prefix/suffix to your string), but it does communicate intent well.)

Could someone who believes @nest is a good solution, please answer this question… pretend a community college professor is emailing you, and asks: "I want to teach my students to use CSS Nesting. Could you explain when / how / where they should use the new @nest rule? What does that nested CSS look like?"

I wrote something to this effect in my previous comment.

LeaVerou commented 5 months ago

I think I prefer @some-weird-name to @group, because if authors somehow do find a use for it that is different or more specific than grouping or nesting, we can replace/alias the weird name with something more closely matching the discovered use.

Not sure about getting stuck with @some-weird-name for time immemorial though. I think @group is sufficiently generic that it can work decently with any use.

css-meeting-bot commented 5 months ago

The CSS Working Group just discussed [css-syntax][css-nesting] Design of `@nest` rule, and agreed to the following:

The full IRC log of that discussion <keithamus> emilio: Leah added this, I can introduce it a bit
<keithamus> ... there still seems to be some disagreement on best path forward
<astearns> could we resolve on @some-long-weird-name?
<Rossen4> s/Leah/Lea/
<keithamus> lea: the gist is another issue we resolved to stop hosting decls coming after nested rules, with decls inside a rule coming after get hoisted to the top which results in strange conflict resolution
<keithamus> lea: Tab proposed the @ rule which avoid this. There was push back as it only exists in the OM and has no purpose for authors, only exists to make spec editors lives easier
<fantasai> s/in the OM/for the purpose of the OM/
<Rossen4> q
<keithamus> ... there are some challenges to not introducing. One proposal to have the @nest rule but not serialize, so you only get plain decls.
<keithamus> ... Tabs opposition is that CSSOM isn't used that much so whats the point
<keithamus> ... another proposal to make a new object to represent the interleaved decls.
<keithamus> ... Blink is strongly opposing not having the rule
<keithamus> ... On the grounds that CSSOM is not used frequently
<keithamus> ... also that syntax is pointless from author perspective
<keithamus> ... what if we call it @group then extend it later with functionality? What if we can give this rule a purpose?
<TabAtkins> q+
<kizu> +1 to "<@astearns> could we resolve on @some-long-weird-name?"
<keithamus> ... my position is that I tend to agree with webkit. Against priority of constituencies. People will find some weird ways to use it
<keithamus> ... I disagree with Tab's assertion that CSSOM is infrequently used.
<keithamus> ... We do plan to eventually add nesting & author styles - extremely author facing.
<emilio> q+
<keithamus> ... many devs modify css properties on the fly, indirectly or not. It's extremely author facing
<keithamus> ... I worry if we can't reach consensus we're stuck with status quo - the hoisting behavior, which is worse than any solutions proposed
<keithamus> ... I'd be happier with @nest vs hoisting
<keithamus> ... even though I'm opposed
<miriam> +1 current behavior is worse than any of the proposals
<keithamus> Rossen4: we can bikeshed later on naming. Prefer path forward suggested by Alan with @some-long-weird-name and bikeshed later
<Rossen4> ack fantasai
<keithamus> fantasai: webkit is opposed to new @ rule for the purpose of making it easier to specify CSSOM. Only reason this rule is being proposed. We don't think that's good for authors
<keithamus> ... flexible to what form the OM does take.
<keithamus> ... we posted one that we think gives some useful interfaces for authors. If people don't like it we're flexible
<keithamus> ... but one thing we're opposed to is this thing that gets parsed out
<Rossen4> ack TabAtkins
<keithamus> TabAtkins: lea's characterization of my position is incomplete. Creating things in the OM is vary rarely used. Crawling via .style or other - there's no meaningful difference. Creating an OM from scratch is only where you'd see the difference. That's incredibly rare to do. Only CSS tooling does it
<keithamus> ... the set of authors we're effecting positively or negatively is minuscule, and these authors are advances. We don't want to give them bad stuff just because, but we trust them to navigate this
<keithamus> ... webkit proposal give us genuinely worse tradeoffs. When you serialize you get something useful but insert rule or manip gets magical in a bad way, where insertrule might merge or insert before or after. Deleting rules has odd behavior as well
<keithamus> ... two decls next to eachother need to be resolved.
<keithamus> ... unexpected tree structure munging is difficult to work with as a user of the API
<keithamus> ... bikeshed represents HTML structure of the doc with a well used XML library but it sucks for HTML. APIs are hard to predict and has inconsistent behavior with text nodes as you're moving around element nodes
<keithamus> ... I used to have bugs in bikeshed due to this. I reimplemented the DOM and used the DOM wrapper just because DOM treats text and other nodes the same, a predictable behavior.
<keithamus> ... similar using an @ rule of some kind means we don't do any weird magic with OM manip.
<keithamus> ... We have consistent behavior, nothing weird needs to happen with add/remove. No cleanup
<keithamus> ... simplifies impl and specs. Importantly it makes manipulation predictable for the author which is far more important to maintain
<keithamus> ... if necessary we're fine with having a serialization rule which most of the time omits @ nest. Anything you parse in can always serialize back out without showing the rule. It just relies on adding one serialization quirk, which you'll never notice unless you're creating rules which couldn't be produced by the parser.
<keithamus> ... in all other cases it'll be invisible. This is acceptable to us
<keithamus> ... I think it's a bad idea to complicate the data model with magic.
<Rossen4> ack emilio
<keithamus> emilio: I wanted to say similar. What lea said implied the model consistent only helps browser/spec editors, I don't think that's true. Let's not overcomplicate the solution.
<lea> q?
<keithamus> ... having extra @ rule vs having to invent weird stuff... the trade-off is clear for me.
<Rossen4> ack fantasai
<Zakim> fantasai, you wanted to clarify that a bunch of what Tab objects to is not something we're requiring and to clarify that most of what Tab objects to is not something we're
<Zakim> ... requiring
<fantasai> https://github.com/w3c/csswg-drafts/issues/10234
<keithamus> fantasai: Tab is referring to this
<fantasai> https://github.com/w3c/csswg-drafts/issues/10234#issuecomment-2116380146
<keithamus> fantasai: which is one proposal which we suggested as a possibility. We clearly said we're flexible how it's represented in the CSSOM
<TabAtkins> q+
<emilio> q+
<keithamus> ... so the long spiel about merging, that's objectionable, we can drop it
<keithamus> ... what we were proposing is that we introducing a CSS Nested Declaration object inherit from CSSStyleRule, it has accessors... when you use insertrule, and there happens to be an adjacent rule, you'd merge the declarations into that.
<keithamus> ... if that's a problem we don't have to do that.
<keithamus> ... the only thing we're stating is that it serializes without an at-rule. Consequence is not merging in CSSOM when you serailize and parse it back in 2 objects get merged together.
<keithamus> ... we think this is acceptable vs a new at-rule to avoid it.
<fantasai> "We would also be OK with alternative solutions that don't introduce an at-rule"
<fantasai> idk how to get more explicit than that
<keithamus> TabAtkins: if that's the constraint it would be good to express it. It seemed like it was requiring more magic. Serialization & reparsing.. I don't care too much about that, as long as tree manip doesn't do unpredictable magic
<Rossen4> ack TabAtkins
<keithamus> ... changing behavior of insert rule to allow declaration lists... currently it throws but the OM API is old. It's always unpredictable if things like this are compat issues.
<keithamus> ... if it's purely a matter of producing some sort of object, call is CSSDeclarationList, and it serializes declarations, and you're ok with it changing structure when you roundtrip, I'm okay with that
<Rossen4> ack emilio
<keithamus> ... but all the magic proposed in thread was what I was objecting to
<keithamus> emilio: I still prefer avoiding rounttripping. Apples proposal without the extra magic is basically @nest without saying @nest.
<TabAtkins> Note again that "just doing an at-rule" also always serializes as bare declarations *as long as you haven't used OM manipulation to do something funky*.
<keithamus> ... that's not amazing but that seems way better than the original tree-monkeying stuff
<Rossen4> ack dbaron
<TabAtkins> Or network packet delays can cause separate text nodes, I think, directly from the parser.
<keithamus> dbaron: Wanted to point out the idea you get a different OM when you serialize and reparse is something we already have with HTML: emtpy text nodes or adjacent text nodes. DOM has an API to normalize these.
<Rossen4> q?
<dbaron> https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize
<keithamus> Rossen4: are we getting close? The original proposal in IRC seems to be landing okay?
<Rossen4> ack fantasai
<fantasai> PROPOSAL:
<fantasai> 1. Introduce a CSSNestedDeclarations object inheriting from CSSRule and having a .style accessor, and use that to represent all the declaration lists in a CSSStyleRule. It serializes as a raw declaration list.
<fantasai> 2. Extend .insertRule() to parse declarations (or, if Web-compat requires it, add .insertDeclarations())
<fantasai> 3. Open question about the first declaration block.
<keithamus> fantasai: we should handle 3 as a follow up
<emilio> q+
<keithamus> emilio: to be honest I still prefer a regular rule than bare decls, but this is a fine compromise if people are unwilling
<Rossen4> ack emilio
<fantasai> s/())/()) into a CSSNestedDeclarations object/
<keithamus> TabAtkins: I don't think first decl block is an open question. We still have weird magic behavior. The first block of stuff is definitely put in a stylerules.style not reflected in child rules
<keithamus> fantasai: 100%. I think theres a question about if its also represented in CSS rules.
<keithamus> emilio: I don't think putting it in 2 places is great
<keithamus> TabAtkins: I don't think we can. If you delete the first block you'll be invoking magic behaviour
<keithamus> ... we'll have to re-create at some point. Exactly the magical behavior I want to avoid
<keithamus> emilio: that's true.. calling delete rule would be weird
<keithamus> fantasai: if we do this authors can have a single consistent API for all of the contents of the style rule
<keithamus> TabAtkins: If we were designing these from scratch I'd agree
<keithamus> ... but with history, the only way to maintain it safely would be additional magic with delete rule. I want to avoid the tree magic as much as possible
<keithamus> fantasai: can we open that conversation separately?
<keithamus> TabAtkins: I can guarantee my position but the others, mild objections, but this is acceptable
<keithamus> Rossen4: Can we summarise the compromise?
<keithamus> TabAtkins: In the proposal
<keithamus> Rossen4: all 3?
<keithamus> fantasai: 3rd isn't really a thing
<keithamus> Rossen4: any additional points or objections>?
<keithamus> s/>?/?
<fantasai> 1. Introduce a CSSNestedDeclarations object inheriting from CSSRule and having a .style accessor, and use that to represent all the declaration lists in a CSSStyleRule. It serializes as a raw declaration list.
<keithamus> lea: can someone restate the proposal?
<fantasai> 2. Extend .insertRule() to parse declarations (or, if Web-compat requires it, add .insertDeclarations())
<fantasai> s/())/()) into a CSSNestedDeclarations object/
<keithamus> lea: that seems great
<keithamus> Rossen4: I'll call this resolved
<fantasai> RESOLVED: 1. Introduce a CSSNestedDeclarations object inheriting from CSSRule and having a .style accessor, and use that to represent the declaration lists in a CSSStyleRule. It serializes as a raw declaration list. 2. Extend .insertRule() to parse declarations (or, if Web-compat requires, add .insertDeclarations()) into a CSSNestedDeclarations Object. 3. Open a new issue wrt the first declarations block.
LeaVerou commented 4 months ago

Possibly too late to change the design, but I just had an idea: What if we do introduce the rule, call it @group, and instead of introducing identifiers in the preamble, we have a revert-group value that would work similarly to revert-layer, i.e. would revert the declaration to what it would have been if the group was not applied. Then groups have a purpose that is not turned on by default (making them suitable for representing nested declarations) but is still super useful: they allow authors to override the IACVT behavior with an actual fallback! This is especially useful in combination with something like if():

@group {
    border-radius: if(style(--button-shape: pill), infinity, revert-group);
}