Closed tabatkins closed 7 years ago
The CSS Working Group just discussed CSSNumericValue, and agreed to the following resolutions:
RESOLVED: Switch to CSSMathValue-based expression tree with toSum() method for returning dictionary of unit types (as described aboce)
That is, throw away CSSCalcValue, in favor of:
interface CSSMathValue {
attribute CSSMathOperator operator;
attribute Array arguments;
readonly attribute DOMString type;
};
enum CSSMathOperator { "+", "*", "/", "min", "max" };
partial interface CSSNumericValue {
record<DOMString, double> toSum();
};
toSum()
is analogous to to
- if possible, it collapses it down to a sum of simple units, and represents that as a JS object.
So, for example, calc(5 * (1px + 2em))
becomes CSSMath("*", [5, CSSMath("+", [1px, 2em])])
. If you call .toSum()
on that, you get {em: 10, px: 5}
.
Question! What's the representation of calc(1 * 2 / 3 * 4 / 5)
? Can we re-order arbitrarily to simplify things, like Math("/", [Math("*", [1, 2, 4]), 3, 5])
? Or do we need to preserve ordering, like Math("*", [1, Math("/", [2, 3]), Math("*", [4, 5])])
?
And I assume that the "/" operator can take >2 arguments, with the first argument serving as the numerator and all additional arguments being multiplied together to form the denominator? So that Math("/", [1, 2, 3])
implies calc(1 / 2 / 3)
. (This is the interpretation that Lisp gives to the /
function.)
And I assume that 0-arguments are an error? We don't have a privileged identity value - we can't tell if Math("+", [])
should resolve to a 0px, 0deg, or what. And the identify values for min/max are positive/negative infinity, which aren't CSS values.
Question! What's the representation of
calc(1 * 2 / 3 * 4 / 5)
?
Discussion with @shans and @plinss led us to two possibilities:
Proper ordering, with parens, is ((((1 2) / 3) 4) / 5). So that would be the tree produced:
Math("/", [Math("*", [Math("/", [Math("*", [1, 2]), 3]), 4]), 5])
For negatives, we don't have a "-" op, we just push the negative into the value and use "+". We can do the same here; instead of a "/" op, we just have an "inverse" op, and use "*" otherwise. So the tree produced would be:
Math("*", [1, 2, Math("inverse", [3]), 4, Math("inverse", 5)])
We prefer (2), so I'm gonna use that in the spec unless somebody objects.
Did some bikeshedding w/ @tabatkins OOB: “Reciprocal” is an alternative to “inverse”. Personally, I prefer that.
"reciprocal" is above my comfort-level for spelling.
As an alternative: add a "pow" operation, and use it (with a -1 arg) to represent division.
Eh, after some thinking, I’m fine with “inverse”.
add a "pow" operation
I think introducing operations that aren’t available in OG calc()
is misleading.
By analogy with "*"
, I think I'll just call it "1/"
, actually.
And tho I don't think we'll ever build it that way, it needs to be able to accept multiple arguments (since the other operators do), which I think I'll interpret as being multiplied together before the whole thing is inverted. That is, CSSMathValue("+", 1, CSSMathValue("1/", 2, 3))
will equal calc(1 + (1 / 2 / 3))
.
I like “1/”, but looking at this ambiguity, doesn’t option 1 from your earlier comment seem more attractive in terms of developer experience?
Not particularly? I don't see why one or the other is better in terms of DevExp. Unless I go all-out and make them both take exactly two arguments, it seems like you have to handle pretty much the same sort of thing.
Starting to spec this, tho, and I'm feeling the lack of a negation operator. It's actually kinda weird to distribute the negation inward to the leaves; it makes .sub()
dramatically different in effect from everything else. I think I'd prefer to add a "0-" operator too.
Maybe drop the numeric suffixes and just use "/" and "-" for consistency, if it seems clear enough. Just want to make it clear that ("-", 2, 3)
== calc(-(2 + 3))
rather than calc(2 - 3)
. It's clearer when there's 1 arg or 3+ args, but the 2-arg case feels ambiguous with "-" and "/", but clear with "0-" and "1/".
And I think, rather than the engine always only outputting 1-arg sub/div, I'd go ahead and collect consecutive ones, so calc(1 / 2 / 3)
becomes ("*", 1, ("/", 2, 3))
(rather than ("*", 1, ("/", 2), ("/", 3))
).
Ugh I don't like any of this.
Okay, so refresh. There's not really any use-case for allowing authors to change the operator; I don't think there's a reasonable case for swapping from calc(1 + 2 + 3)
to calc(1 * 2 * 3)
, for example.
So, instead, I can have six classes, one for each operation: CSSMathAdd
, CSSMathMul
, CSSMathNeg
, CSSMathInv
, CSSMathMin
, and CSSMathMax
. Add/mul/min/max take 1+ arguments, neg/inv take exactly one argument. Structure is like:
interface CSSMathAdd {
attribute CSSNumericList arguments;
readonly attribute CSSMathOperator operator;
readonly attribute DOMString type;
}
// or
interface CSSMathNeg {
attribute CSSNumericValue argument;
readonly attribute CSSMathOperator operator;
readonly attribute DOMString type;
}
operator
returns "+", "-", "*", "/", "min", or "max". It's redundant with the interface itself, but makes it easier to handle in general. (For example, you can drop it into a switch statement, which you can't do as easily with a type test.)
I'm definitely going for that last one (separate interfaces per operator). It works better in general, and it's more future-proof; right now all our calc() operators take either 1 argument, or N identically-handled arguments, but it's possible we may add operators in the future that take, say, 2 arguments that are interpreted differently. (For example, a round() function.) In that case, it's better to have an interface that reflects the two arguments properly, as separate attributes.
Still a good chunk of dangling references to fix, but this is overall done now.
I haven't added it to the spec yet, but the CSSWG resolved to add min()/max() to the V&U spec at the Seattle 2017 f2f. This'll need a new class to represent it properly.