w3c / csswg-drafts

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

[css-values-5] we should ensure an if with no fallback can be reasoned about later #10956

Open keithamus opened 1 month ago

keithamus commented 1 month ago

/If the if conditionals (https://github.com/w3c/csswg-drafts/issues/10064) have an open question as to whether a condition without a valid fallback should be empty token stream or IACTV. I believe we should consider how these can compose and how user can reason about whether or not the condition resolved to one-or-the-other where possible. Some contrives examples:

--my-hue: if(style(max-width: 400px): 300);
background: oklch(70% 0.1 var(--my-hue, 0)) /* This should fallback to zero */
--my-lch: if(style(max-width: 400px): 300);
--my-keyword: if(style(max-width: 500px): red);

background: if(
  var(--my-keyword): var(--my-keyword); /* when 500px this should resolve to `red` */
  else: oklch(70% 0.1 var(--my-hue, 0)); /* otherwise we might have an okclh hue */
); // This should be `red` or `oklch`

I'm unsure if IACTV vs empty token stream precludes conditions such as these but I think if they do that should be a deciding factor,

LeaVerou commented 1 month ago

@keithamus I don't understand the second example, I think there may be a typo there? The first one won't work with the current behavior, but that’s a bit of a special case (empty values are valid in custom properties). For non-custom properties, if their whole value is if() they would become IACVT if nothing matches. However, in the general case, an empty token stream is more composable, as you can have multiple sequential if() that are independent and compose a value, whereas if a single if() makes the whole declaration IACVT, that limits what you can do.

I suspect your first example can be rewritten, but without a real-world use case it's hard to suggest how.

keithamus commented 1 month ago

I think they could both be rewritten to avoid if/else but I was trying to demonstrate a property of the if result is that it should be introspectable in subsequent ifs.

tabatkins commented 3 weeks ago

Agenda+ because, on review, this case isn't actually possible! Earlier, I thought you could just make one of the if() branches resolve to the guaranteed-invalid value and suggested, as an example, using an invalid var(). But that makes the whole property it's in invalid unconditionally (either at parse time, if it's grammatically invalid, or at substitution time, if it's cyclic or non-existent); it doesn't let you just get the invalidity in the one if() branch.

So, there's no way for an if() in a custom property, currently, to later be introspected for validity.

I can see two possible fixes:

  1. We could add a test to if() for empty streams, like if(empty(var(--foo)): ...), which is true if it's empty (importantly, after substitution). This would let you test for if an if() failed all its conditions.
  2. We could finally add a reserved global keyword that is guaranteed-invalid. If we do this, I propose we spell it guaranteed-invalid, which is a bit past our normal spelling-complexity limits, but makes me laugh. Then, you could write --foo: if(...; else: guaranteed-invalid), and then var(--foo, 0) will trigger fallback if the if() hit its else clause, because at evaluation time it resolved to the guaranteed-invalid value, which is what actually triggers fallback in var().

Possibly we should do both; testing if a variable is empty might be useful regardless, even if the emptiness came from something other than an if() that failed all its tests.

tabatkins commented 2 weeks ago

Deferring this until we precisely nail down the execution/substitution model for nested substitution functions. (https://github.com/w3c/csswg-drafts/issues/11144)


If #11144 goes as I expect, then this case is possible. An invalid variable in the value of a branch will just become the guaranteed-invalid value, which is allowed; only when that branch is selectede for substitution will it trigger and turn the whole thing invalid. That is, if((1 > 2): bar; else: var()) will successfully parse, then during substitution it'll fall down to the else clause and substitute itself with the guaranteed-invalid value. This can then be caught by later var(...) usage, or even first-valid(...), to replace with a different fallback value.