Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/css-calc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

### Unreleased (patch)

- Fixed `tan()` at asymptote values beyond the first period (`270deg`, `-270deg`, ...) to match the specification
- Fixed `round(line-width, ...)` to choose the non-zero candidate multiple when `A` is negative
- Fixed `round(down/up, ...)` with a negative step to choose the correct candidate multiple
- Fixed `log(A, 0)` to return `NaN` as specified (only `B` values between 0 and 1, or greater than 1, are valid)
- Fixed `log(1, B)` to return `0⁺` as specified
- Fixed `random()` to not mutate the caller's `options` object
- Fixed `random(fixed <number>, ...)` to clamp the value to the highest representable value less than 1
- Fixed `random()` to treat `max < min` as `max = min` instead of swapping the arguments
- Fixed `random()` to not return an unreachable `max` when a `step` is given
- Fixed `random()` to return `A` (the minimum) when `A` is infinite, instead of `NaN`
- Fix infectious `NaN`
- Updated [`@csstools/css-tokenizer`](https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer) to [`4.0.1`](https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer/CHANGELOG.md#401) (patch)

Expand Down
2 changes: 1 addition & 1 deletion packages/css-calc/dist/index.mjs

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion packages/css-calc/src/functions/calc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,10 @@ function parseRandomValueSharing(fnNode: FunctionNode, nodes: Array<ComponentVal
return -1;
}

x.fixed = Math.max(0, Math.min(fixedNumber.value[4].value, 1 - 0.000_000_001));
// https://drafts.csswg.org/css-values-5/#random-caching
// The random base value is clamped to the highest representable value less than 1,
// so random base values remain in the half-open range [0, 1).
x.fixed = Math.max(0, Math.min(fixedNumber.value[4].value, 1 - Number.EPSILON / 2));

continue;
}
Expand All @@ -833,6 +836,14 @@ function parseRandomValueSharing(fnNode: FunctionNode, nodes: Array<ComponentVal
continue;
}

if (i === 0) {
// The leading ident is not a <random-key> (e.g. a <calc-keyword> like
// `NaN`, `infinity`, `-infinity`, `e` or `pi`).
// It is the start of the first <calc-sum> argument.
// https://drafts.csswg.org/css-values-5/#random
return [x, nodes];
}

return -1;
}

Expand Down
9 changes: 8 additions & 1 deletion packages/css-calc/src/functions/hypot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function solveHypot(hypotNode: FunctionNode, solvedNodes: Array<Component
return -1;
}

// https://drafts.csswg.org/css-values-4/#exponent-ranges
// https://drafts.csswg.org/css-values-4/#exponent-infinities
// NaN is infectious, forcing the function to return NaN if any argument calculation is NaN.
if (tokens.some((x) => Number.isNaN(x[4].value))) {
return resultToCalculation(hypotNode, firstSolvedToken, Number.NaN);
Expand All @@ -35,6 +35,13 @@ export function solveHypot(hypotNode: FunctionNode, solvedNodes: Array<Component
}

const values = tokens.map((x) => x[4].value);

// https://drafts.csswg.org/css-values-4/#exponent-infinities
// NaN is infectious, forcing the function to return NaN if any argument calculation is NaN.
if (values.some(Number.isNaN)) {
return resultToCalculation(hypotNode, firstSolvedToken, Number.NaN);
}

const result = Math.hypot(...values);

return resultToCalculation(hypotNode, firstSolvedToken, result);
Expand Down
12 changes: 12 additions & 0 deletions packages/css-calc/src/functions/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ export function solveLog(logNode: FunctionNode, solvedNodes: Array<ComponentValu
return -1;
}

// https://drafts.csswg.org/css-values-4/#exponent-infinities
// If B is 1 or negative, the result is NaN.
// B values between 0 and 1 (exclusive), or greater than 1, are valid.
if (bToken[4].value === 1 || bToken[4].value <= 0) {
return numberToCalculation(logNode, Number.NaN);
}

// If A is 1, the result is 0⁺.
if (aToken[4].value === 1) {
return numberToCalculation(logNode, +0);
}

const result = Math.log(aToken[4].value) / Math.log(bToken[4].value);

return numberToCalculation(logNode, result);
Expand Down
131 changes: 73 additions & 58 deletions packages/css-calc/src/functions/random.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,6 @@ export function solveRandom(randomNode: FunctionNode, randomValueSharing: Random
return -1;
}

if (!options.randomCaching) {
options.randomCaching = {
propertyName: '',
propertyN: 0,
elementID: '',
documentID: '',
};
}

if (options.randomCaching && !options.randomCaching.propertyN) {
options.randomCaching.propertyN = 0;
}

const aToken = a.value;
if (!isTokenNumeric(aToken)) {
return -1;
Expand All @@ -57,44 +44,66 @@ export function solveRandom(randomNode: FunctionNode, randomValueSharing: Random
return resultToCalculation(randomNode, aToken, Number.NaN);
}

// If A is infinite, the result is infinite.
if (!Number.isFinite(aToken[4].value)) {
return resultToCalculation(randomNode, aToken, Number.NaN);
return resultToCalculation(randomNode, aToken, aToken[4].value);
}

if (!Number.isFinite(bToken[4].value)) {
// If A is finite, but the difference between A and B is infinite, the result is NaN.
if (!Number.isFinite(bToken[4].value) || !Number.isFinite(bToken[4].value - aToken[4].value)) {
return resultToCalculation(randomNode, aToken, Number.NaN);
}

if (!Number.isFinite(bToken[4].value - aToken[4].value)) {
return resultToCalculation(randomNode, aToken, Number.NaN);
}
if (stepValueToken) {
// If C is NaN the result is NaN.
if (Number.isNaN(stepValueToken[4].value)) {
return resultToCalculation(randomNode, aToken, Number.NaN);
}

if (stepValueToken && Number.isNaN(stepValueToken[4].value)) {
return resultToCalculation(randomNode, aToken, Number.NaN);
// If C is infinite, the result is A.
if (!Number.isFinite(stepValueToken[4].value)) {
return resultToCalculation(randomNode, aToken, aToken[4].value);
}
}

if (stepValueToken && !Number.isFinite(stepValueToken[4].value)) {
return resultToCalculation(randomNode, aToken, aToken[4].value);
}
const rnd = ((): () => number => {
if (randomValueSharing.fixed !== -1) {
return (): number => {
return randomValueSharing.fixed;
};
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!options.randomCaching!.propertyN) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.randomCaching!.propertyN = 0;
}

const rnd = randomValueSharing.fixed === -1 ? sfc32(
crc32(
[
randomValueSharing.dashedIdent ? randomValueSharing.dashedIdent : "",
randomValueSharing.elementScoped ? options.randomCaching.elementID : "",
(randomValueSharing.propertyScoped || randomValueSharing.propertyIndexScoped) ? options.randomCaching.propertyName : "",
randomValueSharing.propertyIndexScoped ? options.randomCaching.propertyN : "",
options.randomCaching.documentID,
].join(NULL_CHAR),
),
) : () : number => { return randomValueSharing.fixed; };

let min = aToken[4].value;
return sfc32(
crc32(
[
randomValueSharing.dashedIdent ? randomValueSharing.dashedIdent : "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
randomValueSharing.elementScoped ? options.randomCaching!.elementID : "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(randomValueSharing.propertyScoped || randomValueSharing.propertyIndexScoped) ? options.randomCaching!.propertyName : "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
randomValueSharing.propertyIndexScoped ? options.randomCaching!.propertyN : "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.randomCaching!.documentID,
].join(NULL_CHAR),
),
);
})();

// If the maximum value is less than the minimum value, it behaves as if it's equal to the minimum value.
const min = aToken[4].value;
let max = bToken[4].value;
if (min > max) {
[min, max] = [max, min];
if (max < min) {
max = min;
}

// If C is negative, zero, or positive but close enough to zero that the range for the step multiplier would be infinite, the step must be ignored.
if (
stepValueToken && (
(stepValueToken[4].value <= 0) ||
Expand All @@ -105,31 +114,37 @@ export function solveRandom(randomNode: FunctionNode, randomValueSharing: Random
}

if (stepValueToken) {
const err = Math.max(stepValueToken[4].value / 1000, 0.000_000_001);

const steps = [min];
let lastStep = 0;
while (true) {
lastStep += stepValueToken[4].value;

const stepResult = min + lastStep;
if ((stepResult + err) < max) {
steps.push(stepResult);
} else {
steps.push(max);
break;
}

if ((stepResult + stepValueToken[4].value - err) > max) {
break;
}
const step = stepValueToken[4].value;

// Let epsilon be step / 1000, or the smallest representable value greater than zero if epsilon would round to zero.
const epsilon = Math.max(step / 1000, 0.000_000_001);

// Let N be the largest integer such that min + N * step is less than or equal to max.
let N = Math.floor((max - min) / step);

// If N produces a value that is not within epsilon of max, but N+1 would produce a value within epsilon of max, set N to N+1.
if (
Math.abs(min + N * step - max) >= epsilon &&
Math.abs(min + (N + 1) * step - max) < epsilon
) {
N = N + 1;
}

// Let step index be a random integer less than N+1, given R.
const stepIndex = Math.floor(rnd() * (N + 1));

// Let value be min + step index * step.
let value = min + stepIndex * step;

// If step index is N and value is within epsilon of max, return max.
if (stepIndex === N && Math.abs(value - max) < epsilon) {
value = max;
}

const randomValue = rnd();
return resultToCalculation(
randomNode,
aToken,
Number(steps[Math.floor(steps.length * randomValue)].toFixed(5))
Number(value.toFixed(5))
);
}

Expand Down
10 changes: 7 additions & 3 deletions packages/css-calc/src/functions/round.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,14 @@ export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a:
} else {
switch (roundingStrategy) {
case 'down':
result = Math.floor(aToken[4].value / bToken[4].value) * bToken[4].value;
// "Choose lower B" (the integer multiple of B closer to −∞).
// For a negative step, `Math.floor`/`Math.ceil` must be swapped.
result = (bToken[4].value > 0 ? Math.floor : Math.ceil)(aToken[4].value / bToken[4].value) * bToken[4].value;
break;
case 'up':
result = Math.ceil(aToken[4].value / bToken[4].value) * bToken[4].value;
// "Choose upper B" (the integer multiple of B closer to +∞).
// For a negative step, `Math.floor`/`Math.ceil` must be swapped.
result = (bToken[4].value > 0 ? Math.ceil : Math.floor)(aToken[4].value / bToken[4].value) * bToken[4].value;
break;
case 'to-zero':
result = Math.trunc(aToken[4].value / bToken[4].value) * bToken[4].value;
Expand All @@ -102,7 +106,7 @@ export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a:
const downDiff = Math.abs(aToken[4].value - down);
const upDiff = Math.abs(aToken[4].value - up);

if (roundingStrategy === 'line-width' && aToken[4].value >= 0 && (up === 0 || down === 0)) {
if (roundingStrategy === 'line-width' && (up === 0 || down === 0)) {
result = up !== 0 ? up : down;
} else if (downDiff === upDiff) {
result = up;
Expand Down
4 changes: 3 additions & 1 deletion packages/css-calc/src/functions/tan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export function solveTan(tanNode: FunctionNode, a: TokenNode): Calculation | -1
const isAsymptote = isNinetyMultiple && timesNinety % 2 !== 0;

if (isAsymptote) {
result = timesNinety > 0 ? Infinity : -Infinity;
// https://drafts.csswg.org/css-values-4/#trig-infinities
// +∞ for the asymptotes at 90deg + N*360deg, and −∞ for the asymptotes at -90deg + N*360deg.
result = ((timesNinety % 4) + 4) % 4 === 1 ? Infinity : -Infinity;
} else {
result = Math.tan(result);
}
Expand Down
Loading
Loading