Skip to content

Commit

Permalink
css-calc: rawPercentages (#1461)
Browse files Browse the repository at this point in the history
  • Loading branch information
romainmenke authored Aug 18, 2024
1 parent 36d1a2f commit b298240
Show file tree
Hide file tree
Showing 27 changed files with 319 additions and 158 deletions.
2 changes: 2 additions & 0 deletions packages/css-calc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Unreleased (patch)

- Add `rawPercentages` option to indicate that percentage values do not resolve against external values.
- Skip some calculations for values with percentages as those only have a known positive or negative value in a browser context.
- Updated [`@csstools/css-tokenizer`](https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer) to [`3.0.1`](https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer/CHANGELOG.md#301) (patch)
- Updated [`@csstools/css-parser-algorithms`](https://github.com/csstools/postcss-plugins/tree/main/packages/css-parser-algorithms) to [`3.0.1`](https://github.com/csstools/postcss-plugins/tree/main/packages/css-parser-algorithms/CHANGELOG.md#301) (patch)

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

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions packages/css-calc/dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export declare type conversionOptions = {
* Convert NaN, Infinity, ... into standard representable values.
*/
censorIntoStandardRepresentableValues?: boolean;
/**
* Some percentages resolve against other values and might be negative or positive depending on context.
* Raw percentages are more likely to be safe to simplify outside of a browser context
*
* @see https://drafts.csswg.org/css-values-4/#calc-simplification
*/
rawPercentages?: boolean;
};

export declare type GlobalsWithStrings = Map<string, TokenDimension | TokenNumber | TokenPercentage | string>;
Expand Down
2 changes: 1 addition & 1 deletion packages/css-calc/dist/index.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/css-calc/docs/css-calc.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@
},
{
"kind": "Content",
"text": ";\n precision?: number;\n toCanonicalUnits?: boolean;\n censorIntoStandardRepresentableValues?: boolean;\n}"
"text": ";\n precision?: number;\n toCanonicalUnits?: boolean;\n censorIntoStandardRepresentableValues?: boolean;\n rawPercentages?: boolean;\n}"
},
{
"kind": "Content",
Expand Down
1 change: 1 addition & 0 deletions packages/css-calc/docs/css-calc.conversionoptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type conversionOptions = {
precision?: number;
toCanonicalUnits?: boolean;
censorIntoStandardRepresentableValues?: boolean;
rawPercentages?: boolean;
};
```
**References:** [GlobalsWithStrings](./css-calc.globalswithstrings.md)
Expand Down
11 changes: 8 additions & 3 deletions packages/css-calc/src/functions/abs.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type { Calculation } from '../calculation';
import type { FunctionNode, TokenNode } from '@csstools/css-parser-algorithms';
import { resultToCalculation } from './result-to-calculation';
import { isDimensionOrNumber } from '../util/kind-of-number';
import type { conversionOptions } from '../options';
import { isTokenNumeric, isTokenPercentage } from '@csstools/css-tokenizer';

export function solveAbs(absNode: FunctionNode, a: TokenNode): Calculation | -1 {
export function solveAbs(absNode: FunctionNode, a: TokenNode, options: conversionOptions): Calculation | -1 {
const aToken = a.value;
if (!isDimensionOrNumber(aToken)) {
if (!isTokenNumeric(aToken)) {
return -1;
}

if (!options.rawPercentages && isTokenPercentage(aToken)) {
return -1;
}

Expand Down
131 changes: 67 additions & 64 deletions packages/css-calc/src/functions/calc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ import { subtraction } from '../operation/subtraction';
import { unary } from '../operation/unary';
import { solveLog } from './log';
import { isNone } from '../util/is-none';
import type { conversionOptions } from '../options';

export const mathFunctions = new Map([
type mathFunction = (node: FunctionNode, globals: Globals, options: conversionOptions) => Calculation | -1

export const mathFunctions: Map<string, mathFunction> = new Map([
['abs', abs],
['acos', acos],
['asin', asin],
Expand All @@ -56,7 +59,7 @@ export const mathFunctions = new Map([
['tan', tan],
]);

function calc(calcNode: FunctionNode | SimpleBlockNode, globals: Globals): Calculation | -1 {
function calc(calcNode: FunctionNode | SimpleBlockNode, globals: Globals, options: conversionOptions): Calculation | -1 {
const nodes: Array<ComponentValue | Calculation> = resolveGlobalsAndConstants(
[...(calcNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
globals,
Expand All @@ -74,7 +77,7 @@ function calc(calcNode: FunctionNode | SimpleBlockNode, globals: Globals): Calcu
while (i < nodes.length) {
const child = nodes[i];
if (isSimpleBlockNode(child) && isTokenOpenParen(child.startToken)) {
const subCalc = calc(child, globals);
const subCalc = calc(child, globals, options);
if (subCalc === -1) {
return -1;
}
Expand All @@ -88,7 +91,7 @@ function calc(calcNode: FunctionNode | SimpleBlockNode, globals: Globals): Calcu
return -1;
}

const subCalc = mathFunction(child, globals);
const subCalc = mathFunction(child, globals, options);
if (subCalc === -1) {
return -1;
}
Expand Down Expand Up @@ -211,21 +214,21 @@ function calc(calcNode: FunctionNode | SimpleBlockNode, globals: Globals): Calcu
return -1;
}

function singleNodeSolver(fnNode: FunctionNode, globals: Globals, solveFn: (node: FunctionNode, a: TokenNode) => Calculation | -1): Calculation | -1 {
function singleNodeSolver(fnNode: FunctionNode, globals: Globals, options: conversionOptions, solveFn: (node: FunctionNode, a: TokenNode, options: conversionOptions) => Calculation | -1): Calculation | -1 {
const nodes: Array<ComponentValue> = resolveGlobalsAndConstants(
[...(fnNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
globals,
);

const a = solve(calc(calcWrapper(nodes), globals));
const a = solve(calc(calcWrapper(nodes), globals, options));
if (a === -1) {
return -1;
}

return solveFn(fnNode, a);
return solveFn(fnNode, a, options);
}

function twoCommaSeparatedNodesSolver(fnNode: FunctionNode, globals: Globals, solveFn: (node: FunctionNode, a: TokenNode, b: TokenNode) => Calculation | -1): Calculation | -1 {
function twoCommaSeparatedNodesSolver(fnNode: FunctionNode, globals: Globals, options: conversionOptions, solveFn: (node: FunctionNode, a: TokenNode, b: TokenNode, options: conversionOptions) => Calculation | -1): Calculation | -1 {
const nodes: Array<ComponentValue> = resolveGlobalsAndConstants(
[...(fnNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
globals,
Expand Down Expand Up @@ -257,20 +260,20 @@ function twoCommaSeparatedNodesSolver(fnNode: FunctionNode, globals: Globals, so
}
}

const a = solve(calc(calcWrapper(aValue), globals));
const a = solve(calc(calcWrapper(aValue), globals, options));
if (a === -1) {
return -1;
}

const b = solve(calc(calcWrapper(bValue), globals));
const b = solve(calc(calcWrapper(bValue), globals, options));
if (b === -1) {
return -1;
}

return solveFn(fnNode, a, b);
return solveFn(fnNode, a, b, options);
}

function variadicNodesSolver(fnNode: FunctionNode, globals: Globals, solveFn: (node: FunctionNode, x: Array<ComponentValue>) => Calculation | -1): Calculation | -1 {
function variadicNodesSolver(fnNode: FunctionNode, globals: Globals, options: conversionOptions, solveFn: (node: FunctionNode, x: Array<ComponentValue>, options: conversionOptions) => Calculation | -1): Calculation | -1 {
const nodes: Array<ComponentValue> = resolveGlobalsAndConstants(
[...(fnNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
globals,
Expand Down Expand Up @@ -299,7 +302,7 @@ function variadicNodesSolver(fnNode: FunctionNode, globals: Globals, solveFn: (n
return -1;
}

const solvedChunk = solve(calc(calcWrapper(chunks[i]), globals));
const solvedChunk = solve(calc(calcWrapper(chunks[i]), globals, options));
if (solvedChunk === -1) {
return -1;
}
Expand All @@ -308,10 +311,10 @@ function variadicNodesSolver(fnNode: FunctionNode, globals: Globals, solveFn: (n
}
}

return solveFn(fnNode, solvedNodes);
return solveFn(fnNode, solvedNodes, options);
}

function clamp(clampNode: FunctionNode, globals: Globals): Calculation | -1 {
function clamp(clampNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
const nodes: Array<ComponentValue> = resolveGlobalsAndConstants(
[...(clampNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
globals,
Expand Down Expand Up @@ -351,51 +354,51 @@ function clamp(clampNode: FunctionNode, globals: Globals): Calculation | -1 {
const minimumIsNone = isNone(minimumValue);
const maximumIsNone = isNone(maximumValue);
if (minimumIsNone && maximumIsNone) {
return calc(calcWrapper(centralValue), globals);
return calc(calcWrapper(centralValue), globals, options);
}

const central = solve(calc(calcWrapper(centralValue), globals));
const central = solve(calc(calcWrapper(centralValue), globals, options));
if (central === -1) {
return -1;
}

{
if (minimumIsNone) {
const maximum = solve(calc(calcWrapper(maximumValue), globals));
const maximum = solve(calc(calcWrapper(maximumValue), globals, options));
if (maximum === -1) {
return -1;
}

return solveMin(minWrapper(central, maximum), [central, maximum]);
return solveMin(minWrapper(central, maximum), [central, maximum], options);
} else if (maximumIsNone) {
const minimum = solve(calc(calcWrapper(minimumValue), globals));
const minimum = solve(calc(calcWrapper(minimumValue), globals, options));
if (minimum === -1) {
return -1;
}

return solveMax(maxWrapper(minimum, central), [minimum, central]);
return solveMax(maxWrapper(minimum, central), [minimum, central], options);
}
}

const minimum = solve(calc(calcWrapper(minimumValue), globals));
const minimum = solve(calc(calcWrapper(minimumValue), globals, options));
if (minimum === -1) {
return -1;
}

const maximum = solve(calc(calcWrapper(maximumValue), globals));
const maximum = solve(calc(calcWrapper(maximumValue), globals, options));
if (maximum === -1) {
return -1;
}

return solveClamp(clampNode, minimum, central, maximum);
return solveClamp(clampNode, minimum, central, maximum, options);
}

function max(maxNode: FunctionNode, globals: Globals): Calculation | -1 {
return variadicNodesSolver(maxNode, globals, solveMax);
function max(maxNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return variadicNodesSolver(maxNode, globals, options, solveMax);
}

function min(minNode: FunctionNode, globals: Globals): Calculation | -1 {
return variadicNodesSolver(minNode, globals, solveMin);
function min(minNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return variadicNodesSolver(minNode, globals, options, solveMin);
}

const roundingStrategies = new Set([
Expand All @@ -405,7 +408,7 @@ const roundingStrategies = new Set([
'to-zero',
]);

function round(roundNode: FunctionNode, globals: Globals): Calculation | -1 {
function round(roundNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
const nodes: Array<ComponentValue> = resolveGlobalsAndConstants(
[...(roundNode.value.filter(x => !isCommentNode(x) && !isWhitespaceNode(x)))],
globals,
Expand Down Expand Up @@ -452,7 +455,7 @@ function round(roundNode: FunctionNode, globals: Globals): Calculation | -1 {
}
}

const a = solve(calc(calcWrapper(aValue), globals));
const a = solve(calc(calcWrapper(aValue), globals, options));
if (a === -1) {
return -1;
}
Expand All @@ -465,7 +468,7 @@ function round(roundNode: FunctionNode, globals: Globals): Calculation | -1 {
);
}

const b = solve(calc(calcWrapper(bValue), globals));
const b = solve(calc(calcWrapper(bValue), globals, options));
if (b === -1) {
return -1;
}
Expand All @@ -474,71 +477,71 @@ function round(roundNode: FunctionNode, globals: Globals): Calculation | -1 {
roundingStrategy = 'nearest';
}

return solveRound(roundNode, roundingStrategy, a, b);
return solveRound(roundNode, roundingStrategy, a, b, options);
}

function mod(modNode: FunctionNode, globals: Globals): Calculation | -1 {
return twoCommaSeparatedNodesSolver(modNode, globals, solveMod);
function mod(modNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return twoCommaSeparatedNodesSolver(modNode, globals, options, solveMod);
}

function rem(remNode: FunctionNode, globals: Globals): Calculation | -1 {
return twoCommaSeparatedNodesSolver(remNode, globals, solveRem);
function rem(remNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return twoCommaSeparatedNodesSolver(remNode, globals, options, solveRem);
}

function abs(absNode: FunctionNode, globals: Globals): Calculation | -1 {
return singleNodeSolver(absNode, globals, solveAbs);
function abs(absNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return singleNodeSolver(absNode, globals, options, solveAbs);
}

function sign(signNode: FunctionNode, globals: Globals): Calculation | -1 {
return singleNodeSolver(signNode, globals, solveSign);
function sign(signNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return singleNodeSolver(signNode, globals, options, solveSign);
}

function sin(sinNode: FunctionNode, globals: Globals): Calculation | -1 {
return singleNodeSolver(sinNode, globals, solveSin);
function sin(sinNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return singleNodeSolver(sinNode, globals, options, solveSin);
}

function cos(codNode: FunctionNode, globals: Globals): Calculation | -1 {
return singleNodeSolver(codNode, globals, solveCos);
function cos(codNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return singleNodeSolver(codNode, globals, options, solveCos);
}

function tan(tanNode: FunctionNode, globals: Globals): Calculation | -1 {
return singleNodeSolver(tanNode, globals, solveTan);
function tan(tanNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return singleNodeSolver(tanNode, globals, options, solveTan);
}

function asin(asinNode: FunctionNode, globals: Globals): Calculation | -1 {
return singleNodeSolver(asinNode, globals, solveASin);
function asin(asinNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return singleNodeSolver(asinNode, globals, options, solveASin);
}

function acos(acosNode: FunctionNode, globals: Globals): Calculation | -1 {
return singleNodeSolver(acosNode, globals, solveACos);
function acos(acosNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return singleNodeSolver(acosNode, globals, options, solveACos);
}

function atan(atanNode: FunctionNode, globals: Globals): Calculation | -1 {
return singleNodeSolver(atanNode, globals, solveATan);
function atan(atanNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return singleNodeSolver(atanNode, globals, options, solveATan);
}

function atan2(atan2Node: FunctionNode, globals: Globals): Calculation | -1 {
return twoCommaSeparatedNodesSolver(atan2Node, globals, solveATan2);
function atan2(atan2Node: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return twoCommaSeparatedNodesSolver(atan2Node, globals, options, solveATan2);
}

function exp(expNode: FunctionNode, globals: Globals): Calculation | -1 {
return singleNodeSolver(expNode, globals, solveExp);
function exp(expNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return singleNodeSolver(expNode, globals, options, solveExp);
}

function sqrt(sqrtNode: FunctionNode, globals: Globals): Calculation | -1 {
return singleNodeSolver(sqrtNode, globals, solveSqrt);
function sqrt(sqrtNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return singleNodeSolver(sqrtNode, globals, options, solveSqrt);
}

function pow(powNode: FunctionNode, globals: Globals): Calculation | -1 {
return twoCommaSeparatedNodesSolver(powNode, globals, solvePow);
function pow(powNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return twoCommaSeparatedNodesSolver(powNode, globals, options, solvePow);
}

function hypot(hypotNode: FunctionNode, globals: Globals): Calculation | -1 {
return variadicNodesSolver(hypotNode, globals, solveHypot);
function hypot(hypotNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return variadicNodesSolver(hypotNode, globals, options, solveHypot);
}

function log(logNode: FunctionNode, globals: Globals): Calculation | -1 {
return variadicNodesSolver(logNode, globals, solveLog);
function log(logNode: FunctionNode, globals: Globals, options: conversionOptions): Calculation | -1 {
return variadicNodesSolver(logNode, globals, options, solveLog);
}

function calcWrapper(v: Array<ComponentValue>): FunctionNode {
Expand Down
9 changes: 7 additions & 2 deletions packages/css-calc/src/functions/clamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { convertUnit } from '../unit-conversions';
import { isTokenNode } from '@csstools/css-parser-algorithms';
import { resultToCalculation } from './result-to-calculation';
import { twoOfSameNumeric } from '../util/kind-of-number';
import { isTokenNumeric } from '@csstools/css-tokenizer';
import type { conversionOptions } from '../options';
import { isTokenNumeric, isTokenPercentage } from '@csstools/css-tokenizer';

export function solveClamp(clampNode: FunctionNode, minimum: TokenNode | -1, central: TokenNode | -1, maximum: TokenNode | -1): Calculation | -1 {
export function solveClamp(clampNode: FunctionNode, minimum: TokenNode | -1, central: TokenNode | -1, maximum: TokenNode | -1, options: conversionOptions): Calculation | -1 {
if (
!isTokenNode(minimum) ||
!isTokenNode(central) ||
Expand All @@ -20,6 +21,10 @@ export function solveClamp(clampNode: FunctionNode, minimum: TokenNode | -1, cen
return -1;
}

if (!options.rawPercentages && isTokenPercentage(minimumToken)) {
return -1;
}

const centralToken = convertUnit(minimumToken, central.value);
if (!twoOfSameNumeric(minimumToken, centralToken)) {
return -1;
Expand Down
Loading

0 comments on commit b298240

Please sign in to comment.