w3c / csswg-drafts

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

[css-values] Type conversion functions #6408

Open SebastianZ opened 3 years ago

SebastianZ commented 3 years ago

In #542 there was already some discussion about whether type conversion for strings should be im- or explicit. And it seems the conclusion was to be explicit about that.

I'd like to go a step further and start the discussion whether other type conversion functions should be introduced, too.

One use case I had today was that I wanted to use a counter within a calculation, i.e. something like calc(counter(x) * 3). Though that currently doesn't work because counter(x) doesn't return a numeric value.

This could be approached in two ways:

  1. One conversion function that is able to convert any type into any other type. This would require several parameters for the value to convert, the type to convert to and an optional unit.

    Examples:

    grid-row-start: calc(convert(counter(x), "<numeric>") * 3);
    height: convert(counter(x), "<length>", "em");
    content: convert(5, "<string>");
    transform: rotate(convert("45", "<angle>", "deg"));
  2. One conversion function for each type.

    Examples:

    grid-row-start: calc(number(counter(x)) * 3);
    height: length(counter(x), "em");
    content: string(5);
    transform: rotate(angle("45", "deg"));

Sebastian

Loirooriol commented 3 years ago

See #1026. Rather than converting the internal counter value into a string, and then parsing it as a number, something like counter-value() would make more sense to me. Anyways, upsuper raises some concerns regarding parallelization.

SebastianZ commented 3 years ago

Thank you for pointing me at #1026, @Loirooriol!

For my current use case, a counter-value() function would suffice, too.

Though I still believe general type conversion functions would have some value and would make the conversion explicit. @upsuper's point about the phase at which conversion should happen is important, of course. If they are meant to work everywhere, I guess they need to do the conversion at used value time.

Note that general conversion may also be used for functions returning a number like the discussed random() function or all the mathematical expressions. And other type conversions like from angles to numbers or the other way round also may have their use cases.

Sebastian

LeaVerou commented 2 years ago

The problem with a general conversion function is that often these conversions are impossible, so the function needs to either give up and return NaN or do weird stuff (like JS' parseInt() and parseFloat()).

Since most use cases of conversion to number revolve around counters, I think we should add counter-value() or a similar counter-based syntax (what about an extra argument in counter()?) and see if any other use cases emerge that are not addressed by this. These syntaxes would also allow us to get a number even when the counter has a non-numerical style, e.g. upper-roman.

Note that the conversion to strings is an entirely different issue that is discussed in #542.

fantasai commented 2 years ago

@SebastianZ Seems to me we should close this out in favor of #1026 and #542, does that work for you or is there something left here that we should keep it open for?

SebastianZ commented 2 years ago

1026 and #542 cover the current use cases of converting counter values to numbers and any kind of value to strings.

Note that the main point of this issue is to discuss whether it would be better to provide a unified and explicit way for type conversion instead of having different mechanisms. And the idea of making all type conversions explicit actually rose from the discussion in #542, as I initially mentioned.

The problem with a general conversion function is that often these conversions are impossible, so the function needs to either give up and return NaN or do weird stuff (like JS' parseInt() and parseFloat()). ... Note that the conversion to strings is an entirely different issue that is discussed in #542.

You're right. Though I don't see this as a problem but an advantage of an explicit function. And I include string conversion in this, as that is probably the most common use case for type conversions.

Of course a general mechanism as I mentioned it in my initial post has the downside that it's much longer to write than implicit conversion. Also in relation to @LeaVerou's point, it might not be obvious to authors if and how specific conversions actually work. Though again, I believe a general explicit way of converting different values into each other is better than doing it implicitly or having different solutions for different cases.

Sebastian

LeaVerou commented 2 years ago

I think there's one case where implicit conversion makes sense, and I just opened an issue on it: #6786

Though again, I believe a general explicit way of converting different values into each other is better than [...] having different solutions for different cases.

We generally don't add things without enough use cases, just for completeness' sake. If going from and to any type has enough use cases, sure. But so far it seems that use cases concentrate in a couple of conversions.

SebastianZ commented 2 years ago

We generally don't add things without enough use cases, just for completeness' sake. If going from and to any type has enough use cases, sure. But so far it seems that use cases concentrate in a couple of conversions.

A general function could also be defined to just cover the current use cases for now and be extended in the future.

Sebastian

Crissov commented 6 months ago

In a comment on #10001, I suggested that it would be useful for authors to be able to parse an attribute value, class in particular, as a space-separated list and randomly access its entries. Would this be in scope of this issue?

With explicit type conversion functions, the current specification of attr() could be simplified a lot because its second parameter <attributes-type> would not be necessary anymore. (Possibly #4482 is the relevant issue, or #3702.)

SebastianZ commented 3 weeks ago

In a comment on #10001, I suggested that it would be useful for authors to be able to parse an attribute value, class in particular, as a space-separated list and randomly access its entries. Would this be in scope of this issue?

Well, a space-separated list is is a more complex type than e.g. <string> or <number>. It might be added at some point, though for now I'd like to focus on the simple types.

Sebastian

SebastianZ commented 3 weeks ago

To make progress on this, here's a concrete proposal:

Let's introduce a convert() function that converts one value into another value. It's syntax should look like this:

convert() = convert( <declaration-value>, <conversion-type>, <conversion-unit>? )

where <conversion-type> must be a syntax string and <conversion-unit> a unit string.

Which syntax strings are allowed as <conversion-type> needs to be discussed. Though I suggest to be able to convert any supported syntax component name. The current use cases would then look like this:

Definition for converting to <string>:

Definition for converting to <number>:

Definition for converting to <integer>:

Sebastian

Loirooriol commented 3 weeks ago

Is this addressing any usecase not covered by #1026 and #542? Because I would prefer to address these with a specific solution for them.

Also it's not clear if converting string to number will accept scientific notation, leading/surrounding whitespace, strings like "calc(1 + 3)", etc.

Also not a fan of just dropping units. Converting 2em into 2 is better achieved with calc(2em / 1em).

SebastianZ commented 2 weeks ago

Is this addressing any usecase not covered by #1026 and #542? Because I would prefer to address these with a specific solution for them.

Yes, explicit type conversion functions may also cover type conversion in attr() and possibly even complex types, as noted earlier by @Crissov.

Also it's not clear if converting string to number will accept scientific notation, leading/surrounding whitespace, strings like "calc(1 + 3)", etc.

Any string that can be interpreted as a valid <number> value. That at least includes scientific notation. For math functions like calc(), I tend to say yes, to surrounding whitespace I'd say no. Though that needs to be discussed.

Also not a fan of just dropping units. Converting 2em into 2 is better achieved with calc(2em / 1em).

So how would you convert a string "2em" into a number? Should it just fail?

Sebastian

Loirooriol commented 2 weeks ago

type conversion functions may also cover type conversion in attr()

If we had multiple functions like that, it could make sense to do it in a generic way. But if attr() is the only case, it's probably simpler to just keep the 2nd parameter.

Any string that can be interpreted as a valid <number> value [...] to surrounding whitespace I'd say no

Well, if it's parsing as <number>, then I would expect to use https://www.w3.org/TR/css-syntax-3/#parse-a-component-value and ignore whitespace tokens.

So how would you convert a string "2em" into a number? Should it just fail?

Yes, it should fail like in https://drafts.csswg.org/css-values-5/#valdef-attr-number If you want to get a number, you should convert it into a length, and then use calc() to divide by 1em or whatever.

Crissov commented 2 weeks ago

Isn’t there also a proposal somewhere for getting query parameters or fragment identifiers from the stylesheet URL into property value space? That, env(), var(), string(), content() and counter() seem like likely candidates besides attr(). (Some of those may be bad ideas.)

SebastianZ commented 2 weeks ago

@LeaVerou mentioned in a CSS Day talk from 2022 some other use cases for explicit type conversion. She used counters to display a percentage on a bar chart. Here's a screenshot of that talk (@LeaVerou Hope that's fine to post it here!):

Using counter for displaying percentages in `content`

With explicit type conversion, you could shorten this example to

<div style="--p: 49%">F</div>
.bar-chart > div {
  height: var(--p);
}

.bar-chart > div::before {
  content: convert(var(--p), <string>);
}

And it would look much less hacky.

type conversion functions may also cover type conversion in attr()

If we had multiple functions like that, it could make sense to do it in a generic way. But if attr() is the only case, it's probably simpler to just keep the 2nd parameter.

Explicit type conversion is non-exclusive to implicit conversions. So, an attr() with a second parameter could coexist. The use cases for explicit conversions are much broader, though.

Any string that can be interpreted as a valid <number> value [...] to surrounding whitespace I'd say no

Well, if it's parsing as <number>, then I would expect to use https://www.w3.org/TR/css-syntax-3/#parse-a-component-value and ignore whitespace tokens.

Fine for me.

So how would you convert a string "2em" into a number? Should it just fail?

Yes, it should fail like in https://drafts.csswg.org/css-values-5/#valdef-attr-number If you want to get a number, you should convert it into a length, and then use calc() to divide by 1em or whatever.

Fair enough. Instead of using calc() you could also convert to a length first and then to a number, i.e. convert(convert("2em", <length>), <number>).


Isn’t there also a proposal somewhere for getting query parameters or fragment identifiers from the stylesheet URL into property value space?

I'm not sure there is one for extracting information from a URL. Maybe you are referring to linked parameters to pass parameters to a resource?

If there was some way to extract information from a URL, explicit type conversion could be used to interpret those parameters, though.

Sebastian

LeaVerou commented 2 weeks ago

@SebastianZ In general, the need to print out a numeric value as text comes up all the time. E.g. most recently in printing out page numbers that were set via a Paged.js-like script (and not via counters). But also for debugging — until there is a better way.

@Crissov this is a completely orthogonal issue. Please search and open a new issue if it doesn't exist.

SebastianZ commented 2 weeks ago

I've updated my suggestion to include the use case for converting to <integer>, incorporated some feedback and added a few more details.

Also not a fan of just dropping units. Converting 2em into 2 is better achieved with calc(2em / 1em).

This is debatable. As an author, I'd expect 2em to be convertible to a number or integer, making the conversion resilient. Though I don't have a strong opinion about this.

Sebastian

Loirooriol commented 2 weeks ago

You can't directly convert lengths to numbers. If 1em = 16px, it doesn't make sense for 1em and 16px to be converted to different things, and for 1em and 1px to be converted to the same number since one length is larger than the other. And what number does calc(1px + 1em) get converted into? It just doesn't make sense.

See Why does hypot() allow dimensions (values with units), but pow() and sqrt() only work on numbers? in the spec.