Open brandonmcconnell opened 1 year ago
Values might contain commas themselves (note random-item
uses ;
as a separator), so maybe it should be
--multiple-items: (1; 2; 3; 4; 5);
--single-item: 6;
some-prop: random-item(--rnd1; spread(--multiple-items); var(--single-item));
@Loirooriol Thanks! Yes, I mistakenly used ,
;P
I think a comma would be a reasonable default for most properties, and this spec could support other delimiters like the ;
used in random()
using either of these:
* {
--multiple-items: (1; 2; 3; 4; 5);
--single-item: 6;
some-prop: random-item(var(--ident); spread(--multiple-items, ';'); var(--multiple-items));
}
* {
--multiple-items: 1, 2, 3, 4, 5;
--single-item: 6;
some-prop: random-item(var(--ident); spread(--multiple-items, ',', ';'); var(--multiple-items));
}
This way, any set of values stored in a variable can be used regardless of what delimiter is used in it, and then converted to match the format needed for its new context.
I agree that there's a need for something in this vein, to allow storing arglists for functions like random-item()
(and other semicolon-delimited lists) in a variable. Not a fan of exactly how it's proposed here, tho - it runs into some awkward grammar issues. If you want to actually use the full power of random-item()
and store a comma-separated list as one of the arguments, along others, you can't use spread()
as proposed without using a third thing as a delimiter.
The core issue here is that a function can take <any-value>
, while properties are limited to <declaration-value>
. To store an <any-value>
in a property, you need something to wrap it so it's not top-level; then you need some way to discard the wrapper when it's actually used (but not before then - if you sub a var() into another custom prop, it needs to retain the wrapper so the value remains valid).
So I think there are two possible approaches:
Define a function, call it spread1()
, that is valid as a whole argument in random-item()
and any other functions that accept anything broader than <declaration-value>
. It has the grammar spread(<any-value>)
(and thus can contain itself). Its only effect is to discard itself at computed-value time, lifting its own arguments into the parent function.
If used anywhere else it's just invalid syntax. Custom properties using *
grammar can of course use it, since they just see a token stream rather than a function.
Define a function, call it spread2()
, that isn't valid anywhere - only the *
grammar can use it. It has special substitution behavior:
*
grammar, it subs as-isThe two are basically identical in usage:
.foo {
--multiple-items: spread(1; 2; 3; 4; 5);
--single-item: 6;
some-prop: random-item(--x; var(--multiple-items); var(--single-item));
/* equivalent to */
some-prop: random-item(--x; 1; 2; 3; 4; 5; 6);
}
/* and nesting them works */
.foo {
--some-items: spread(1; 2; 3);
--more-items: spread(var(--some-items); 4; 5; 6);
--some-prop: random-item(--x; var(--more-items));
}
There are only meaningful difference between the two approaches is whether they're allowed in places that don't require them - spread1()
isn't (it's defined to only be valid in the functions that need it) while spread2()
is (can be used anywhere, but doesn't add any ability). Having the function be invalid where it's not needed (as in spread1()
) might be good at discouraging confusing overuse, but having it be more explicitly tied to variable substitution (as in spread2()
) might also be good for understanding. Really not sure which side I prefer.
@tabatkins Thanks for all the feedback! I agree, and either of those sounds good to me also. I'm still thinking about the pros and cons of each.
With that model in mind, would CSS sets always work use a semicolon delimiter rather than another symbol?
Also, would this be valid syntax, not including the spread()
at the top-level but rather in the random-item()
function itself, where the set is wrapped in parentheses without any function:
.foo {
--some-items: (1; 2; 3);
--some-prop: random-item(--x; spread(var(--some-items)));
}
If this wouldn't be considered valid syntax, it may be helpful to include some other function like set()
as well to denote creating a set (named so as not to be confused with [css-lists]
) that doesn't need immediate spreading, randomizing, or nth-ing, thereby making the set more reusable.
That way, all of these would be valid:
.foo {
--some-items: set(1; 2; 3);
/* spreading into `nth()` */
--first-item: nth(1; spread(--some-items));
/* spreading into `random-item()` */
--random-item: random-item(--x; spread(--some-items));
}
I think it could be useful for spread()
to take in variables without needing to use var()
(e.g. spread(--a; --b; 1; 2)
vs. spread(var(--a); var(--b); 1; 2)
), though I think either is perfectly acceptable if you or the CSSWG think otherwise.
I do wonder in regard to the delimiter if it would help to be able to spread into other properties using a different delimiter. For example:
:root {
--colors: red, green, blue;
}
div {
--overlay-color: rgb(0 0 0 / 75%);
--overlay: linear-gradient(rgb(0 0 0 / 75%), rgb(0 0 0 / 75%));
background: var(--overlay), linear-gradient(var(--colors)); /* requires comma delimiter */
color: random-item(--x per-element; spread(--colors)); /* requires semicolon delimiter */
}
Similar to how random()
uses a by
keyword, I wonder if spread()
could be extended to use something similar like spread(--colors; black; using ,)
and spread(--colors; black; using ;)
respectively, where the default would be ;
but could be manually set to another delimiter when needed for values that might use a different delimiter like ,
or /
.
Maybe that's a possibility to consider later though. For starters, I think either of your proposed examples more than meets the use case I was aiming for with this proposal. Thanks!
On second thought, I think the word list
—if a function is needed there—would be too easily confused with [css-lists]
, so it's probably best to call that something else like sets, I think. Anyway, I'll make that change above ☝🏼
~list()
~ → set()
With that model in mind, would CSS sets always work use a semicolon delimiter rather than another symbol?
If we need to separate <declaration-value>
s, yes, our only options for delimiters are ;
and !
. All other values are potentially valid parts of the <declaration-value>
. (See https://w3c.github.io/csswg-drafts/css-syntax/#any-value)
Also, would this be valid syntax,
Invalid under spread2()
(only allowed in a *
grammar); valid but does the wrong thing in spread1()
(it'll be equivalent to random-item(--x; (1; 2; 3))
, which has only a single item to choose from).
I think it could be useful for spread() to take in variables
Note that in either option, you only use spread()
in the custom property. In the function you're spreading into you just use var()
normally. Using spread()
directly inside of random-item()
doesn't do anything.
(That is, we're not defining a higher-level concept of "a list of values", and then invoking it at the usage point to spread it into multiple args, like the JS ...
operator. All we're doing is putting a wrapper around some values, so we can safely use <any-value>
rather than <declaration-value>
, and then removing the wrapper once actual substitution happens.
I do wonder in regard to the delimiter if it would help to be able to spread into other properties using a different delimiter.
None of those examples need any help. All of them can be accomplished today using custom properties and var()
. The sole problem that needs solving is storing something with a ;
in it (or, theoretically, a !
) in a custom property, because those two tokens aren't allowed at the top-level of a property.
(This does make me lean towards spread2()
, fwiw, because making it unambiguously a thing only usable in custom properties probably avoids this sort of confusion with JS's ...
operator.)
@tabatkins @Loirooriol It may even be worth including the functions set()
and nth()
in this proposal as all three go hand-in-hand. What do you think? I could write up separate proposals for them, but set()
is rather fundamental to this ticket unless it's unneeded in which case I'll just omit that one.
nth()
I'm not sure about — should that get its own ticket?
Also, relatedly, @Crissov had a pretty interesting proposal in a comment on the random()
ticket last week, proposing that instead of separate nth()
and random-item()
functions, introducing one new consolidated function like select()
or choose()
, though nth()
would probably still work fine for naming.
For the sake of continuity, let's call this function nth()
. nth()
would support all the usual nth
values include formulaic values:
.foo {
--items: set(1; 2; 3);
/* spreading into dynamic `nth()` */
--random-item: nth(random --x per-element; spread(--items)); /* random becomes a keyword here rather than a dedicated `random-pick` function */
--first-item: nth(1; spread(--items));
--also-first-item: nth(first; spread(--items));
--last-item: nth(last; spread(--items));
--odd-items: nth(odd; spread(--items)); /* returns filtered list: list(1; 3) */
--even-items: nth(even; spread(--items)); /* returns filtered list: list(2) */
--range-of-items: nth(3 to 5; spread(--items)); /* returns filtered list: list(3) */
--two-and-down-items: nth(-n + 2; spread(--items)); /* returns filtered list: list(1; 2) */
--all-items: nth(n; spread(--items)); /* essentially copies the list, pointless but valid */
}
@tabatkins
(This does make me lean towards spread2(), fwiw, because making it unambiguously a thing only usable in custom properties probably avoids this sort of confusion with JS's ... operator.)
Yeah, I could lean towards option 2 as well, though I think it would help to whitelist certain usages of spread()
like using it within the random-item()
function.
If we were to introduce some kind of list/set function that actually houses lists of args and makes them reusable, then it may be worth discussing my other comment and how such a function could benefit all random-item()
, nth()
, and spread()
. However, I'm not sure if that discussion would be best had here or in another thread.
though I think it would help to whitelist certain usages of spread() like using it within the random-item() function.
What are you imagining this would help with? When used directly in random-item()
the function would do literally nothing. The point of my comment was that I think spread2()
only being valid in custom properties would reduce the chance of such confusing/redundant usage.
If we were to introduce some kind of list/set function that actually houses lists of args and makes them reusable
There's no need for such a function; as long as your list isn't semicolon-separated, custom properties already hold "lists of args" just fine, and var()
makes them reusable.
TL;DR: That makes sense. In fact, I think spread()
would essentially suffice for the same use case I'm co-proposing list()
for.
In that case, spread would only need to be used in variables as you suggested. Could we still interface other functions with it down the road like nth()
?
:root {
--colors: spread(blue; red);
}
main {
background: linear-gradient(
nth(first; var(--colors)),
nth(last; var(--colors))
);
}
Yes, spread2()
as I defined is generic and doesn't care about where you use it; it just throws itself away and leaves its arguments behind any time you sub it into a non-universal grammar. So it'll automatically work in any new semicolon-delimited argument lists we define in the future.
(It'll also work anywhere else, for anything, it's just unnecessary in any other case.)
Okay, awesome. I dig that, and it'll basically remove the need for any separate list()
function down the road.
I'd love to move forward with the model from spread2()
.
Would it possibly be worth also introducing a second function like unspread()
specifically to be able to pass an entire spread list as a single object, which would also then immediately remove itself?
Like this, for example:
.foo {
--colors-rgb: spread(red; green; blue);
--colors-cmy: spread(cyan; magenta; yellow);
--random-item: random-item(color-single; random-item(color-group; unspread(--colors-rgb); unspread(--colors-cmy));
}
In this example, the innermost nested random-item()
usage randomly selects either variable var(--colors-rgb)
or var(--colors-cmy)
, and then the outer wrapping random-item()
chooses an item from the inner-selected group of colors.
I'm wondering if this holds any value— being able to pass a spread group as a group rather than its individual pieces in certain contexts, but if you think this would complicate things too much, I can understand and appreciate that viewpoint as well. Thanks.
Making that sort of nesting work would require actually thinking about these values as a "list" structure rather than just a sequence of tokens. It's possible, but I question the need for it. ^_^
@tabatkins Fair enough — thanks!
This spread()
is actually not that different from the tokens()
function from #2749, so maybe they can be unified?
Nah, they're pretty different. tokens()
from that issue prevents any resolution of what's inside of it - tokens(var(--foo))
, in particular, doesn't sub in the --foo
property, it stays as an unresolved var(--foo)
token.
spread2()
here doesn't affect the interpretation of the function at all, just substitution behavior.
They are similar in the sense that they wrap something inside a function, and then the value is unwrapped when using var()
. spread()
doesn't need to prevent the resolution of what's inside it, but on most cases the difference might not matter. So I was just wondering if we could make do with just one of these functions, but I'm fine with adding both.
The execution ends up extremely different, is the thing - holding something for one substitution (or arbitrary substitutions until explicitly un-rawed), vs holding it for arbitrary substitutions until it hits a non-universal grammar.
It's not a given that the executions need to be different. The original spread()
proposal had explicit unrawing, and "arbitrary substitutions until it hits a non-universal grammar" might be fine for tokens()
.
and "arbitrary substitutions until it hits a non-universal grammar" might be fine for tokens().
It's not; one of the use-cases that tokens()
was solving was the ability to sub a global keyword into a custom property.
Yeah, so:
--w: tokens(inherit); width: unraw(var(--w))
--w: tokens(inherit); width: var(--w)
--w: tokens(inherit); width: var(--w)
The difference is if you have a chain of custom properties referencing each other, and you want to unraw earlier. But spread()
could also benefit from that. So the execution for both could be: hold something for arbitrary substitutions until unrawed either explicitly or implicitly by reaching a non-universal grammar.
Proposal
It would help to add support for a new
spread()
in CSS that would work identically tovar()
except that it would spread any chained values contained in the variable as if they had each been used individually.Syntax
The syntax would be something like this, expressed in JS/TS:
It could then be used in CSS like this:
Using
spread()
, the random value in the example above would have an equal chance of being any of the six values—1
2
3
4
5
6
Without
spread()
, the random value generated would only be one of two options—1, 2, 3, 4, 5
6
Alternative syntax
To keep this feature in line with others in CSS, I took a more functional approach using
spread()
, however, if we wanted to follow the trend of other languages, the traditional...
spread syntax could be employed, which would look like this: