w3c / csswg-drafts

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

[css-env] Consider value-level parsing like calc() rather than token-level like var() #3285

Open SimonSapin opened 5 years ago

SimonSapin commented 5 years ago

Background

The syntax of the calc() function is defined "at the value level":

https://drafts.csswg.org/css-values/#calc-notation

A math function can be used wherever <length>, <frequency>, <angle>, <time>, <flex>, <resolution>, <percentage>, <number>, or <integer> values are allowed.

This allows both authors and implementers to only consider those functions in contexts where these value can be used. For example, display: calc(…) is never valid for any .

The var() function however works "at the token stream level", before even considering the value syntax of a given property:

https://drafts.csswg.org/css-variables/#using-variables

The var() function can be used in place of any part of a value in any property on an element.

Being so general is useful for var() because many authors will be defining custom properties with arbitrary bits of syntax. We don’t want the spec to have to anticipate and enumerate each value type that could be used in custom property values.

However, this causes var() to "infect" more of the language. The presence of a var() function somewhere in the middle of a declaration affects the behavior of the entire declaration in ways that both authors and implementers need to be aware of and deal with:

https://drafts.csswg.org/css-variables/#using-variables

If a property contains one or more var() functions, and those functions are syntactically valid, the entire property’s grammar must be assumed to be valid at parse time. It is only syntax-checked at computed-value time, after var() functions have been substituted.

In this sense, var() is a more fundamental part of CSS syntax than just another feature that can be used in values.

env()

Currently, the syntax of the env() function is defined in a way very similar to var():

https://drafts.csswg.org/css-env-1/#env-function

The env() function can be used in place of any part of a value in any property on any element, or any part of a value in any descriptor on any at-rule, and in several other places where CSS values are allowed.

I think it doesn’t need to be, and it should be redefined in a way similar to the calc() function: it is an additional set of valid values for a given set of types like <length>.

The spec defines a closed set of environment variables, and all of them are currently defined as <length>. In the future if/when new variables of other types are added (possibly <color>?), we can extend parsing support to those types.

Compat

I understand that this feature is already shipping in some browsers (despite not having a working draft yet). Hopefully it was only shipped recently enough that relatively few sites use it, and it is simple enough that they mostly use it ways (only using length variables in length contexts) that are similar to each other and not affected by this proposed change.

tabatkins commented 5 years ago

I don't think this is possible. calc() can be value-level because it resolves to a single leaf value in the grammars. The built-in env()s already have things that resolve to multiple values, however, and we don't have productions to recognize those. Author-defined env() will be even wider, with no control over what's put in there.

However, the "infection" behavior of var() isn't quite present here. You don't have to wait for any other values to be set up before you resolve it; you can just sub it in during parsing, and then invalidate the thing it's used in like normal. My current plan for author-defined env() is to just provide a JS API too (not a CSS rule), so that invariant will be maintained anyway. Should I make that clearer in the spec, to help guide implementation?

jonjohnjohnson commented 5 years ago

@tabatkins Separately, when you say...

My current plan for author-defined env() is to just provide a JS API too (not a CSS rule), so that invariant will be maintained anyway.

Does this mean that for use of author-defined env(), we will have to separate out scripts that block the document to prevent FOUC?

tabatkins commented 5 years ago

Presumably, yeah. Setting a new env() value would trigger a reparse in this idea.

emilio commented 5 years ago

I don't think this is possible. calc() can be value-level because it resolves to a single leaf value in the grammars. The built-in env()s already have things that resolve to multiple values, however, and we don't have productions to recognize those.

Can you put an example? All env vars that I know of are either lengths or times. And all the ones that are in the spec are only lengths.

However, the "infection" behavior of var() isn't quite present here. You don't have to wait for any other values to be set up before you resolve it; you can just sub it in during parsing, and then invalidate the thing it's used in like normal. My current plan for author-defined env() is to just provide a JS API too (not a CSS rule), so that invariant will be maintained anyway. Should I make that clearer in the spec, to help guide implementation?

This is not quite true, that's now how it works in any browser today. All browsers reuse the custom property machinery because well, these values do change, semi-frequently actually, due to the rotation of the device for example. And nobody wants to reparse CSS rules when something in the page changes, and less so when a author-accessible API is called.

tabatkins commented 5 years ago

Can you put an example? All env vars that I know of are either lengths or times. And all the ones that are in the spec are only lengths.

Ah, I thought there was an env() that sets all four margins at once, suitable for dropping directly into margin. If they're all value-level (and we feel like restricting things to that would be acceptable), then maybe.

And nobody wants to reparse CSS rules when something in the page changes, and less so when a author-accessible API is called.

That's a necessity if env() is used in selectors, which is one of the things that author-defined env() is intended to be useful for...

upsuper commented 5 years ago

That's a necessity if env() is used in selectors, which is one of the things that author-defined env() is intended to be useful for...

Do we need to reparse the CSS rule as a whole or just the selector? Specifically, can authors specify for example an environment --div to be { display: inline-block } and then div env(--div) to be expanded to div { display: inline-block }? Or would an env()-in-selector be restricted to be just selectors?

(This comment makes me a lot more concerned about the implementation complexity of custom env() now...)

tabatkins commented 5 years ago

Ideally it should be restricted to just the selector; I don't particularly want arbitrary substitution of stylesheet contents.

That said, imposing such a restriction would require more work than we probably want to mix into this particular feature. I guess I should back off of env() doing arbitrary substitution thruout the stylesheet, and stick with it being able to substitute in values. (Maybe still want to sub in MQs, tho perhaps that's also best done by Custom MQs...)

So yeah, let's go ahead and make these assumptions:

  1. env() is "value-level" - it produces a single terminal value, a la calc(), not a token stream like var()
  2. env() will be typed to one of the "simple" terminal types length, number, etc.

Actually, I think we can just lean on the list of possible types for the attr() function; attr() is almost exactly the same feature as env(), just drawing from the element rather than from the document. That also suggests the API shape to handle custom env() - have a map of names to string/type pairs, and parse the string as the type (same as attr()) or else it's invalid and throws.

I'm not sure why I didn't see that parallel before. Hmm tho, that still doesn't let you type-check during parsing, like you can with built-in env() or with attr(); you'd have to wait until you got the custom value set. Hm. I guess we will still have to do something at the call site for this.


Okay, so we'll set aside custom env() for a moment; I'm confident we can engineer around any problems later. Let's just talk about built-in env(). A built-in env() is typed to one of the attr() types, according to the name given to it, so you can type-check at parse time. (Currently, all the names are lengths.) I'll rewrite the spec accordingly.

chris-morgan commented 5 years ago

I submit for your consideration a practical case where the current parsing definition of env() is hostile to reasonable expectations, from https://github.com/w3c/csswg-drafts/issues/3792:

margin-bottom: 15px;
margin-bottom: env(safe-area-inset-bottom, 15px);
margin-bottom: max(env(safe-area-inset-bottom, 15px), 15px);

First rule, fine. Second rule, fine: discarded if env() is not supported, and handled properly with a suitable default if it is. Third rule, the defined behaviour of env() means that it’s valid on env()-supporting browsers, but treated as unset if max() isn’t defined, and so it overrides the second rule.

The currently necessary workaround is to shift the third rule into a @supports (margin-bottom: max(0, 15px)) block, but this is nasty.

cdoublev commented 5 months ago

A built-in env() is typed to one of the attr() types, according to the name given to it, so you can type-check at parse time.

Should it still be assumed to be valid at parse time? If yes, I do not get the reason for type checking. It is required for attr() mainly for back compat with untyped attr(), if I am not mistaken. And a valid type does not always mean a valid value in the context.