diff --git a/package-lock.json b/package-lock.json index d3b735528..c8d1b0ac1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8201,6 +8201,9 @@ } ], "license": "MIT", + "devDependencies": { + "puppeteer": "^25.7.0" + }, "engines": { "node": ">=20.19.0" }, diff --git a/packages/css-calc/CHANGELOG.md b/packages/css-calc/CHANGELOG.md index de673405e..ec7490dad 100644 --- a/packages/css-calc/CHANGELOG.md +++ b/packages/css-calc/CHANGELOG.md @@ -6,13 +6,17 @@ - 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 `log(1, B)` to return `0⁺` as specified, except when `B` is `NaN` (NaN stays infectious, so `log(1, NaN)` is now `NaN`) - Fixed `random()` to not mutate the caller's `options` object - Fixed `random(fixed , ...)` 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` +- Fixed the `precision` option to not round values that serialize in scientific notation + (e.g. `calc(1e-10 * 1e-10)` was rounded to `0`, it is now left untouched) +- Fixed `random()` to not round values that serialize in scientific notation + (e.g. `random(fixed 0.5, 1e-20, 1e-10)` returned `0`) - 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) ### 3.4.0 diff --git a/packages/css-calc/README.md b/packages/css-calc/README.md index 1e20fae72..3b3539862 100644 --- a/packages/css-calc/README.md +++ b/packages/css-calc/README.md @@ -64,9 +64,12 @@ console.log(calcResultStr); #### `precision` : +The `precision` option is a number of decimals. The default precision is fairly high. It aims to be high enough to make rounding unnoticeable in the browser. +Values that serialize in scientific notation are left untouched. + You can set it to a lower number to suit your needs. ```mjs @@ -76,6 +79,8 @@ import { calc } from '@csstools/css-calc'; console.log(calc('calc(1 / 3)', { precision: 1 })); // '0.33' console.log(calc('calc(1 / 3)', { precision: 2 })); +// '1.0000000000000001e-20' +console.log(calc('calc(1e-10 * 1e-10)')); ``` #### `globals` : diff --git a/packages/css-calc/dist/index.mjs b/packages/css-calc/dist/index.mjs index c417ae06b..4bee99bdc 100644 --- a/packages/css-calc/dist/index.mjs +++ b/packages/css-calc/dist/index.mjs @@ -1 +1 @@ -import{sourceIndices as e,TokenNode as n,isTokenNode as t,isWhiteSpaceOrCommentNode as r,isSimpleBlockNode as a,isFunctionNode as u,FunctionNode as o,WhitespaceNode as i,parseCommaSeparatedListOfComponentValues as l,walk as c}from"@csstools/css-parser-algorithms";import{isTokenDimension as s,TokenType as v,NumberType as f,mutateUnit as p,isTokenNumber as m,isTokenPercentage as d,isTokenIdent as N,isTokenNumeric as C,isTokenOpenParen as g,isTokenDelim as h,isTokenComma as D,isToken as b,tokenizer as B,tokenize as A,stringify as F,isTokenColon as w,isTokenSemicolon as E}from"@csstools/css-tokenizer";class ParseError extends Error{sourceStart;sourceEnd;constructor(e,n,t){super(e),this.name="ParseError",this.sourceStart=n,this.sourceEnd=t}}class ParseErrorWithComponentValues extends ParseError{componentValues;constructor(n,t){super(n,...e(t)),this.componentValues=t}}const S={UnexpectedAdditionOfDimensionOrPercentageWithNumber:"Unexpected addition of a dimension or percentage with a number.",UnexpectedSubtractionOfDimensionOrPercentageWithNumber:"Unexpected subtraction of a dimension or percentage with a number."},I=/[A-Z]/g;function toLowerCaseAZ(e){return e.replace(I,e=>String.fromCharCode(e.charCodeAt(0)+32))}const y={cm:"px",in:"px",mm:"px",pc:"px",pt:"px",px:"px",q:"px",deg:"deg",grad:"deg",rad:"deg",turn:"deg",ms:"s",s:"s",hz:"hz",khz:"hz"},M=new Map([["cm",e=>e],["mm",e=>10*e],["q",e=>40*e],["in",e=>e/2.54],["pc",e=>e/2.54*6],["pt",e=>e/2.54*72],["px",e=>e/2.54*96]]),x=new Map([["deg",e=>e],["grad",e=>e/.9],["rad",e=>e/180*Math.PI],["turn",e=>e/360]]),T=new Map([["deg",e=>.9*e],["grad",e=>e],["rad",e=>.9*e/180*Math.PI],["turn",e=>.9*e/360]]),P=new Map([["hz",e=>e],["khz",e=>e/1e3]]),k=new Map([["cm",e=>2.54*e],["mm",e=>25.4*e],["q",e=>25.4*e*4],["in",e=>e],["pc",e=>6*e],["pt",e=>72*e],["px",e=>96*e]]),W=new Map([["hz",e=>1e3*e],["khz",e=>e]]),O=new Map([["cm",e=>e/10],["mm",e=>e],["q",e=>4*e],["in",e=>e/25.4],["pc",e=>e/25.4*6],["pt",e=>e/25.4*72],["px",e=>e/25.4*96]]),U=new Map([["ms",e=>e],["s",e=>e/1e3]]),L=new Map([["cm",e=>e/6*2.54],["mm",e=>e/6*25.4],["q",e=>e/6*25.4*4],["in",e=>e/6],["pc",e=>e],["pt",e=>e/6*72],["px",e=>e/6*96]]),$=new Map([["cm",e=>e/72*2.54],["mm",e=>e/72*25.4],["q",e=>e/72*25.4*4],["in",e=>e/72],["pc",e=>e/72*6],["pt",e=>e],["px",e=>e/72*96]]),V=new Map([["cm",e=>e/96*2.54],["mm",e=>e/96*25.4],["q",e=>e/96*25.4*4],["in",e=>e/96],["pc",e=>e/96*6],["pt",e=>e/96*72],["px",e=>e]]),Z=new Map([["cm",e=>e/4/10],["mm",e=>e/4],["q",e=>e],["in",e=>e/4/25.4],["pc",e=>e/4/25.4*6],["pt",e=>e/4/25.4*72],["px",e=>e/4/25.4*96]]),z=new Map([["deg",e=>180*e/Math.PI],["grad",e=>180*e/Math.PI/.9],["rad",e=>e],["turn",e=>180*e/Math.PI/360]]),q=new Map([["ms",e=>1e3*e],["s",e=>e]]),G=new Map([["deg",e=>360*e],["grad",e=>360*e/.9],["rad",e=>360*e/180*Math.PI],["turn",e=>e]]),R=new Map([["cm",M],["mm",O],["q",Z],["in",k],["pc",L],["pt",$],["px",V],["ms",U],["s",q],["deg",x],["grad",T],["rad",z],["turn",G],["hz",P],["khz",W]]);function convertUnit(e,n){if(!s(e))return n;if(!s(n))return n;const t=toLowerCaseAZ(e[4].unit),r=toLowerCaseAZ(n[4].unit);if(t===r)return n;const a=R.get(r);if(!a)return n;const u=a.get(t);if(!u)return n;const o=u(n[4].value),i=[v.Dimension,"",n[2],n[3],{...n[4],signCharacter:o<0?"-":void 0,type:Number.isInteger(o)?f.Integer:f.Number,value:o}];return p(i,e[4].unit),i}function toCanonicalUnit(e){if(!s(e))return e;const n=toLowerCaseAZ(e[4].unit),t=y[n];if(n===t)return e;const r=R.get(n);if(!r)return e;const a=r.get(t);if(!a)return e;const u=a(e[4].value),o=[v.Dimension,"",e[2],e[3],{...e[4],signCharacter:u<0?"-":void 0,type:Number.isInteger(u)?f.Integer:f.Number,value:u}];return p(o,t),o}function addition(e,t){if(2!==e.length)return-1;const r=e[0].value;let a=e[1].value;if(m(r)&&m(a)){const e=r[4].value+a[4].value;return new n([v.Number,e.toString(),r[2],a[3],{value:e,type:r[4].type===f.Integer&&a[4].type===f.Integer?f.Integer:f.Number}])}if(d(r)&&d(a)){const e=r[4].value+a[4].value;return new n([v.Percentage,e.toString()+"%",r[2],a[3],{value:e}])}if(s(r)&&s(a)&&(a=convertUnit(r,a),toLowerCaseAZ(r[4].unit)===toLowerCaseAZ(a[4].unit))){const e=r[4].value+a[4].value;return new n([v.Dimension,e.toString()+r[4].unit,r[2],a[3],{value:e,type:r[4].type===f.Integer&&a[4].type===f.Integer?f.Integer:f.Number,unit:r[4].unit}])}return(m(r)&&(s(a)||d(a))||m(a)&&(s(r)||d(r)))&&t.onParseError?.(new ParseErrorWithComponentValues(S.UnexpectedAdditionOfDimensionOrPercentageWithNumber,e)),-1}function division(e){if(2!==e.length)return-1;const t=e[0].value,r=e[1].value;if(m(t)&&m(r)){const e=t[4].value/r[4].value;return new n([v.Number,e.toString(),t[2],r[3],{value:e,type:Number.isInteger(e)?f.Integer:f.Number}])}if(d(t)&&m(r)){const e=t[4].value/r[4].value;return new n([v.Percentage,e.toString()+"%",t[2],r[3],{value:e}])}if(s(t)&&m(r)){const e=t[4].value/r[4].value;return new n([v.Dimension,e.toString()+t[4].unit,t[2],r[3],{value:e,type:Number.isInteger(e)?f.Integer:f.Number,unit:t[4].unit}])}return-1}function isCalculation(e){return!!e&&"object"==typeof e&&"inputs"in e&&Array.isArray(e.inputs)&&"operation"in e}function solve(e,n){if(-1===e)return-1;const r=[];for(let a=0;aconvertUnit(a,e.value));if(!arrayOfSameNumeric(u))return-1;if(u.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,a,Number.NaN);if(!r.rawPercentages&&d(a))return-1;const o=u.map(e=>e[4].value);if(o.some(Number.isNaN))return resultToCalculation(e,a,Number.NaN);const i=Math.hypot(...o);return resultToCalculation(e,a,i)}function solveMax(e,n,r){if(!n.every(t))return-1;const a=n[0].value;if(!C(a))return-1;const u=n.map(e=>convertUnit(a,e.value));if(!arrayOfSameNumeric(u))return-1;if(u.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,a,Number.NaN);if(!r.rawPercentages&&d(a))return-1;const o=u.map(e=>e[4].value),i=Math.max(...o);return resultToCalculation(e,a,i)}function solveMin(e,n,r){if(!n.every(t))return-1;const a=n[0].value;if(!C(a))return-1;const u=n.map(e=>convertUnit(a,e.value));if(!arrayOfSameNumeric(u))return-1;if(u.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,a,Number.NaN);if(!r.rawPercentages&&d(a))return-1;const o=u.map(e=>e[4].value),i=Math.min(...o);return resultToCalculation(e,a,i)}function solveMod(e,n,t){const r=n.value;if(!C(r))return-1;const a=convertUnit(r,t.value);if(!twoOfSameNumeric(r,a))return-1;let u;return u=Number.isNaN(r[4].value)||Number.isNaN(a[4].value)||0===a[4].value?Number.NaN:Number.isFinite(r[4].value)&&(Number.isFinite(a[4].value)||(a[4].value!==Number.POSITIVE_INFINITY||r[4].value!==Number.NEGATIVE_INFINITY&&!Object.is(0*r[4].value,-0))&&(a[4].value!==Number.NEGATIVE_INFINITY||r[4].value!==Number.POSITIVE_INFINITY&&!Object.is(0*r[4].value,0)))?Number.isFinite(a[4].value)?(r[4].value%a[4].value+a[4].value)%a[4].value:r[4].value:Number.NaN,resultToCalculation(e,r,u)}function solvePow(e,n,t){const r=n.value,a=t.value;if(!m(r))return-1;if(!twoOfSameNumeric(r,a))return-1;if(Number.isNaN(r[4].value)||Number.isNaN(a[4].value))return numberToCalculation(e,Number.NaN);return numberToCalculation(e,Math.pow(r[4].value,a[4].value))}function solveRem(e,n,t){const r=n.value;if(!C(r))return-1;const a=convertUnit(r,t.value);if(!twoOfSameNumeric(r,a))return-1;let u;return u=Number.isNaN(r[4].value)||Number.isNaN(a[4].value)||0===a[4].value?Number.NaN:Number.isFinite(r[4].value)?Number.isFinite(a[4].value)?r[4].value%a[4].value:r[4].value:Number.NaN,resultToCalculation(e,r,u)}function snapAsBorderWidth(e,n,t){if(!s(n))return-1;const r=t.devicePixelLength??1,a=[v.Dimension,`${r}px`,n[2],n[3],{value:r,type:f.Integer,unit:"px"}],u=convertUnit(a,n);if(!twoOfSameNumeric(u,a))return-1;if(Number.isInteger(u[4].value/r))return resultToCalculation(e,n,n[4].value);if(u[4].value>0){if(u[4].value0?1/0:Object.is(0,0*u[4].value)?0:-0;break;default:i=Object.is(0,0*u[4].value)?0:-0}else switch(n){case"down":i=(o[4].value>0?Math.floor:Math.ceil)(u[4].value/o[4].value)*o[4].value;break;case"up":i=(o[4].value>0?Math.ceil:Math.floor)(u[4].value/o[4].value)*o[4].value;break;case"to-zero":i=Math.trunc(u[4].value/o[4].value)*o[4].value;break;default:{let t=Math.floor(u[4].value/o[4].value)*o[4].value,r=Math.ceil(u[4].value/o[4].value)*o[4].value;if(t>r){const e=t;t=r,r=e}const l=Math.abs(u[4].value-t),c=Math.abs(u[4].value-r);if(i="line-width"!==n||0!==r&&0!==t?l===c?r:l!r(e));return 1===n.length&&isNone(n[0])}if(!t(e))return!1;const n=e.value;return!!N(n)&&j.test(n[4].value)}const Y=String.fromCodePoint(0);function solveRandom(e,n,t,r,a,u){if(-1===n.fixed&&!u.randomCaching)return-1;const o=t.value;if(!C(o))return-1;const i=convertUnit(o,r.value);if(!twoOfSameNumeric(o,i))return-1;let l=null;if(a&&(l=convertUnit(o,a.value),!twoOfSameNumeric(o,l)))return-1;if(Number.isNaN(o[4].value)||Number.isNaN(i[4].value))return resultToCalculation(e,o,Number.NaN);if(!Number.isFinite(o[4].value))return resultToCalculation(e,o,o[4].value);if(!Number.isFinite(i[4].value)||!Number.isFinite(i[4].value-o[4].value))return resultToCalculation(e,o,Number.NaN);if(l){if(Number.isNaN(l[4].value))return resultToCalculation(e,o,Number.NaN);if(!Number.isFinite(l[4].value))return resultToCalculation(e,o,o[4].value)}const c=-1!==n.fixed?()=>n.fixed:(u.randomCaching.propertyN||(u.randomCaching.propertyN=0),sfc32(crc32([n.dashedIdent?n.dashedIdent:"",n.elementScoped?u.randomCaching.elementID:"",n.propertyScoped||n.propertyIndexScoped?u.randomCaching.propertyName:"",n.propertyIndexScoped?u.randomCaching.propertyN:"",u.randomCaching.documentID].join(Y)))),s=o[4].value;let v=i[4].value;if(v1e10)&&(l=null),l){const n=l[4].value,t=Math.max(n/1e3,1e-9);let r=Math.floor((v-s)/n);Math.abs(s+r*n-v)>=t&&Math.abs(s+(r+1)*n-v){const a=((e|=0)+(n|=0)|0)+(r|=0)|0;return r=r+1|0,e=n^n>>>9,n=(t|=0)+(t<<3)|0,t=(t=t<<21|t>>>11)+a|0,(a>>>0)/4294967296}}function crc32(e){let n,t,r=0;r^=-1;for(let a=0,u=e.length;a>>8^n;return(-1^r)>>>0}function solveCalcMix(e,n){const t=n[0].calcSum.value;if(!C(t))return-1;const r=n.map(e=>({calcSum:convertUnit(t,e.calcSum.value),percentage:e.percentage}));if(!arrayOfSameNumeric(r.map(e=>e.calcSum)))return-1;const{items:a}=normalizeMixPercentages(r,!1),u=a.map(e=>e.calcSum[4].value*e.percentage/100);let o=0;for(let e=0;e100||u>0&&n)for(const e of a)e.percentage=e.percentage*(100/u);let o=0;return u<100&&(o=100-u),{items:a,leftover:o}}const _=new Map([["abs",function abs(e,n,t){return singleNodeSolver(e,n,t,solveAbs)}],["acos",function acos(e,n,t){return singleNodeSolver(e,n,t,solveACos)}],["asin",function asin(e,n,t){return singleNodeSolver(e,n,t,solveASin)}],["atan",function atan(e,n,t){return singleNodeSolver(e,n,t,solveATan)}],["atan2",function atan2(e,n,t){return twoCommaSeparatedNodesSolver(e,n,t,solveATan2)}],["calc",calc$1],["calc-mix",function calcMix(e,n,t){const r=variadicArgumentsCalcMix(e,e.value,n,t);if(-1===r)return-1;return solveCalcMix(e,r)}],["clamp",function clamp(a,u,i){const l=resolveGlobalsAndConstants([...a.value.filter(e=>!r(e))],u),c=[],s=[],f=[];{let e=c;for(let n=0;n!r(e)),n,t);if(-1===a)return-1;const[u,o]=a,i=variadicArguments(e,o,n,t);if(-1===i)return-1;const[l,c,s]=i;if(!l||!c)return-1;return solveRandom(e,u,l,c,s,t)}],["rem",function rem(e,n,t){return twoCommaSeparatedNodesSolver(e,n,t,solveRem)}],["round",function round(e,a,u){const o=resolveGlobalsAndConstants([...e.value.filter(e=>!r(e))],a);let i="",l=!1;const c=[],p=[];{let e=c;for(let n=0;n!r(e))],n);if(1===i.length&&t(i[0]))return{inputs:[i[0]],operation:unary};let l=0;for(;l!r(e))],n),u=solve(calc$1(calcWrapper(e,a),n,t),t);return-1===u?-1:u}function twoCommaSeparatedNodesSolver(e,n,t,r){const a=twoCommaSeparatedArguments(e,n,t);if(-1===a)return-1;const[u,o]=a;return r(e,u,o,t)}function twoCommaSeparatedArguments(e,n,a){const u=resolveGlobalsAndConstants([...e.value.filter(e=>!r(e))],n),o=[],i=[];{let e=o;for(let n=0;n!r(e))],a),i=[];{const n=[];let r=[];for(let e=0;e!r(e))],a),i=[];{const n=[];let l=[];for(let e=0;e=0;i--){if(r(n[o][i]))continue;if(l=solve(calc$1(calcWrapper(e,n[o].slice(0,i+1)),a,u),u),-1===l)continue;const s=n[o].slice(i+1).filter(e=>!r(e));if(s.length)if(1===s.length&&t(s[0])&&d(s[0].value)){if(c=s[0].value[4].value,c<0||c>100)return-1}else{const n=solve(calc$1(calcWrapper(e,s),a,u),u);if(-1===n)return-1;if(!d(n.value))return-1;c=n.value[4].value,c=Math.min(100,Math.max(0,c))}break}if(-1===l)return-1;i.push({calcSum:l,percentage:c})}}return i}const H=new Set(["nearest","line-width","up","down","to-zero"]);function parseRandomValueSharing(e,n,r,a){const u={dashedIdent:"",fixed:-1,elementScoped:!1,propertyScoped:!1,propertyIndexScoped:!1};let o=!1;const i=n[0];if(!t(i)||!N(i.value))return[u,n];for(let i=0;i1)return-1;u.fixed=Math.max(0,Math.min(l.value[4].value,1-Number.EPSILON/2));continue}if("auto"!==c){if(!c.startsWith("--"))return 0===i?[u,n]:-1;if(-1!==u.fixed||o||u.dashedIdent)return-1;u.dashedIdent=c}else{if(-1!==u.fixed||o||u.dashedIdent||u.elementScoped||u.propertyScoped||u.propertyIndexScoped)return-1;u.elementScoped=!0,u.propertyIndexScoped=!0,o=!0}}else{if(-1!==u.fixed||o||u.propertyScoped||u.propertyIndexScoped)return-1;u.propertyIndexScoped=!0}else{if(-1!==u.fixed||o||u.propertyScoped||u.propertyIndexScoped)return-1;u.propertyScoped=!0}else{if(-1!==u.fixed||o||u.elementScoped)return-1;u.elementScoped=!0}}return-1}function calcWrapper(e,n){return new o([v.Function,"calc(",e.name[2],e.name[3],{value:"calc"}],[v.CloseParen,")",e.endToken[2],e.endToken[3],void 0],n)}function maxWrapper(t,r,a){return new o([v.Function,"max(",t.name[2],t.name[3],{value:"max"}],[v.CloseParen,")",t.endToken[2],t.endToken[3],void 0],[r,new n([v.Comma,",",...e(r),void 0]),a])}function patchNaN(e){if(-1===e)return-1;if(u(e))return e;const t=e.value;return C(t)&&Number.isNaN(t[4].value)?m(t)?new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,"NaN",t[2],t[3],{value:"NaN"}])]):s(t)?new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,"NaN",t[2],t[3],{value:"NaN"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Dimension,"1"+t[4].unit,t[2],t[3],{value:1,type:f.Integer,unit:t[4].unit}])]):d(t)?new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,"NaN",t[2],t[3],{value:"NaN"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Percentage,"1%",t[2],t[3],{value:1}])]):-1:e}function patchInfinity(e){if(-1===e)return-1;if(u(e))return e;const t=e.value;if(!C(t))return e;if(Number.isFinite(t[4].value)||Number.isNaN(t[4].value))return e;let r="";return Number.NEGATIVE_INFINITY===t[4].value&&(r="-"),m(t)?new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}])]):s(t)?new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Dimension,"1"+t[4].unit,t[2],t[3],{value:1,type:f.Integer,unit:t[4].unit}])]):new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Percentage,"1%",t[2],t[3],{value:1}])])}function patchMinusZero(e){if(-1===e)return-1;if(u(e))return e;const n=e.value;return C(n)&&Object.is(-0,n[4].value)?("-0"===n[1]||(d(n)?n[1]="-0%":s(n)?n[1]="-0"+n[4].unit:n[1]="-0"),e):e}function patchPrecision(e,n=13){if(-1===e)return-1;if(n<=0)return e;if(u(e))return e;const t=e.value;if(!C(t))return e;if(Number.isInteger(t[4].value))return e;const r=Number(t[4].value.toFixed(n)).toString();return m(t)?t[1]=r:d(t)?t[1]=r+"%":s(t)&&(t[1]=r+t[4].unit),e}function patchCanonicalUnit(e){return-1===e?-1:u(e)?e:s(e.value)?(e.value=toCanonicalUnit(e.value),e):e}function patchCalcResult(e,n){let t=e;return n?.toCanonicalUnits&&(t=patchCanonicalUnit(t)),t=patchPrecision(t,n?.precision),t=patchMinusZero(t),n?.censorIntoStandardRepresentableValues||(t=patchNaN(t),t=patchInfinity(t)),t}function tokenizeGlobals(e){const n=new Map;if(!e)return n;for(const[t,r]of e)if(b(r))n.set(t,r);else if("string"==typeof r){const e=B({css:r}),a=e.nextToken();if(e.nextToken(),!e.endOfFile())continue;if(!C(a))continue;n.set(t,a);continue}return n}function calc(e,n){return calcFromComponentValues(l(A({css:e}),{}),n).map(e=>e.map(e=>F(...e.tokens())).join("")).join(",")}function calcFromComponentValues(e,n){const t=tokenizeGlobals(n?.globals);return replaceComponentValues(e,e=>{if(!u(e))return;const r=_.get(e.getName().toLowerCase());if(!r)return;const a=patchCalcResult(solve(r(e,t,n??{}),n??{}),n);return-1!==a?n?.calcWrapper?calcWrapper(e,[a]):a:void 0})}function replaceComponentValues(n,a){for(let u=0;u{if("number"!=typeof u)return;const o=a(n.node);if(!o)return;const l=[o],c=n.parent.value[u-1];t(c)&&h(c.value)&&("-"===c.value[4].value||"+"===c.value[4].value)&&l.splice(0,0,new i([[v.Whitespace," ",...e(n.node),void 0]]));const s=n.parent.value[u+1];!s||r(s)||t(s)&&(D(s.value)||w(s.value)||E(s.value)||h(s.value)&&"-"!==s.value[4].value&&"+"!==s.value[4].value)||l.push(new i([[v.Whitespace," ",...e(n.node),void 0]])),n.parent.value.splice(u,1,...l)})}return n}const J=new Set(_.keys());export{ParseError,S as ParseErrorMessage,ParseErrorWithComponentValues,calc,calcFromComponentValues,J as mathFunctionNames}; +import{sourceIndices as e,TokenNode as n,isTokenNode as t,isWhiteSpaceOrCommentNode as r,isFunctionNode as a,isSimpleBlockNode as u,FunctionNode as o,WhitespaceNode as i,parseCommaSeparatedListOfComponentValues as l,walk as c}from"@csstools/css-parser-algorithms";import{isTokenDimension as s,TokenType as v,NumberType as f,mutateUnit as p,isTokenNumber as m,isTokenPercentage as d,isTokenIdent as N,isTokenNumeric as C,isTokenOpenParen as g,isTokenDelim as h,isTokenComma as D,isToken as b,tokenizer as B,tokenize as A,stringify as w,isTokenColon as F,isTokenSemicolon as E}from"@csstools/css-tokenizer";class ParseError extends Error{sourceStart;sourceEnd;constructor(e,n,t){super(e),this.name="ParseError",this.sourceStart=n,this.sourceEnd=t}}class ParseErrorWithComponentValues extends ParseError{componentValues;constructor(n,t){super(n,...e(t)),this.componentValues=t}}const S={UnexpectedAdditionOfDimensionOrPercentageWithNumber:"Unexpected addition of a dimension or percentage with a number.",UnexpectedSubtractionOfDimensionOrPercentageWithNumber:"Unexpected subtraction of a dimension or percentage with a number."},I=/[A-Z]/g;function toLowerCaseAZ(e){return e.replace(I,e=>String.fromCharCode(e.charCodeAt(0)+32))}const y={cm:"px",in:"px",mm:"px",pc:"px",pt:"px",px:"px",q:"px",deg:"deg",grad:"deg",rad:"deg",turn:"deg",ms:"s",s:"s",hz:"hz",khz:"hz"},M=new Map([["cm",e=>e],["mm",e=>10*e],["q",e=>40*e],["in",e=>e/2.54],["pc",e=>e/2.54*6],["pt",e=>e/2.54*72],["px",e=>e/2.54*96]]),x=new Map([["deg",e=>e],["grad",e=>e/.9],["rad",e=>e/180*Math.PI],["turn",e=>e/360]]),T=new Map([["deg",e=>.9*e],["grad",e=>e],["rad",e=>.9*e/180*Math.PI],["turn",e=>.9*e/360]]),P=new Map([["hz",e=>e],["khz",e=>e/1e3]]),k=new Map([["cm",e=>2.54*e],["mm",e=>25.4*e],["q",e=>25.4*e*4],["in",e=>e],["pc",e=>6*e],["pt",e=>72*e],["px",e=>96*e]]),W=new Map([["hz",e=>1e3*e],["khz",e=>e]]),O=new Map([["cm",e=>e/10],["mm",e=>e],["q",e=>4*e],["in",e=>e/25.4],["pc",e=>e/25.4*6],["pt",e=>e/25.4*72],["px",e=>e/25.4*96]]),U=new Map([["ms",e=>e],["s",e=>e/1e3]]),L=new Map([["cm",e=>e/6*2.54],["mm",e=>e/6*25.4],["q",e=>e/6*25.4*4],["in",e=>e/6],["pc",e=>e],["pt",e=>e/6*72],["px",e=>e/6*96]]),$=new Map([["cm",e=>e/72*2.54],["mm",e=>e/72*25.4],["q",e=>e/72*25.4*4],["in",e=>e/72],["pc",e=>e/72*6],["pt",e=>e],["px",e=>e/72*96]]),V=new Map([["cm",e=>e/96*2.54],["mm",e=>e/96*25.4],["q",e=>e/96*25.4*4],["in",e=>e/96],["pc",e=>e/96*6],["pt",e=>e/96*72],["px",e=>e]]),Z=new Map([["cm",e=>e/4/10],["mm",e=>e/4],["q",e=>e],["in",e=>e/4/25.4],["pc",e=>e/4/25.4*6],["pt",e=>e/4/25.4*72],["px",e=>e/4/25.4*96]]),z=new Map([["deg",e=>180*e/Math.PI],["grad",e=>180*e/Math.PI/.9],["rad",e=>e],["turn",e=>180*e/Math.PI/360]]),q=new Map([["ms",e=>1e3*e],["s",e=>e]]),R=new Map([["deg",e=>360*e],["grad",e=>360*e/.9],["rad",e=>360*e/180*Math.PI],["turn",e=>e]]),G=new Map([["cm",M],["mm",O],["q",Z],["in",k],["pc",L],["pt",$],["px",V],["ms",U],["s",q],["deg",x],["grad",T],["rad",z],["turn",R],["hz",P],["khz",W]]);function convertUnit(e,n){if(!s(e))return n;if(!s(n))return n;const t=toLowerCaseAZ(e[4].unit),r=toLowerCaseAZ(n[4].unit);if(t===r)return n;const a=G.get(r);if(!a)return n;const u=a.get(t);if(!u)return n;const o=u(n[4].value),i=[v.Dimension,"",n[2],n[3],{...n[4],signCharacter:o<0?"-":void 0,type:Number.isInteger(o)?f.Integer:f.Number,value:o}];return p(i,e[4].unit),i}function toCanonicalUnit(e){if(!s(e))return e;const n=toLowerCaseAZ(e[4].unit),t=y[n];if(n===t)return e;const r=G.get(n);if(!r)return e;const a=r.get(t);if(!a)return e;const u=a(e[4].value),o=[v.Dimension,"",e[2],e[3],{...e[4],signCharacter:u<0?"-":void 0,type:Number.isInteger(u)?f.Integer:f.Number,value:u}];return p(o,t),o}function addition(e,t){if(2!==e.length)return-1;const r=e[0].value;let a=e[1].value;if(m(r)&&m(a)){const e=r[4].value+a[4].value;return new n([v.Number,e.toString(),r[2],a[3],{value:e,type:r[4].type===f.Integer&&a[4].type===f.Integer?f.Integer:f.Number}])}if(d(r)&&d(a)){const e=r[4].value+a[4].value;return new n([v.Percentage,e.toString()+"%",r[2],a[3],{value:e}])}if(s(r)&&s(a)&&(a=convertUnit(r,a),toLowerCaseAZ(r[4].unit)===toLowerCaseAZ(a[4].unit))){const e=r[4].value+a[4].value;return new n([v.Dimension,e.toString()+r[4].unit,r[2],a[3],{value:e,type:r[4].type===f.Integer&&a[4].type===f.Integer?f.Integer:f.Number,unit:r[4].unit}])}return(m(r)&&(s(a)||d(a))||m(a)&&(s(r)||d(r)))&&t.onParseError?.(new ParseErrorWithComponentValues(S.UnexpectedAdditionOfDimensionOrPercentageWithNumber,e)),-1}function division(e){if(2!==e.length)return-1;const t=e[0].value,r=e[1].value;if(m(t)&&m(r)){const e=t[4].value/r[4].value;return new n([v.Number,e.toString(),t[2],r[3],{value:e,type:Number.isInteger(e)?f.Integer:f.Number}])}if(d(t)&&m(r)){const e=t[4].value/r[4].value;return new n([v.Percentage,e.toString()+"%",t[2],r[3],{value:e}])}if(s(t)&&m(r)){const e=t[4].value/r[4].value;return new n([v.Dimension,e.toString()+t[4].unit,t[2],r[3],{value:e,type:Number.isInteger(e)?f.Integer:f.Number,unit:t[4].unit}])}return-1}function isCalculation(e){return!!e&&"object"==typeof e&&"inputs"in e&&Array.isArray(e.inputs)&&"operation"in e}function solve(e,n){if(-1===e)return-1;const r=[];for(let a=0;aconvertUnit(a,e.value));if(!arrayOfSameNumeric(u))return-1;if(u.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,a,Number.NaN);if(!r.rawPercentages&&d(a))return-1;const o=u.map(e=>e[4].value);if(o.some(Number.isNaN))return resultToCalculation(e,a,Number.NaN);const i=Math.hypot(...o);return resultToCalculation(e,a,i)}function solveMax(e,n,r){if(!n.every(t))return-1;const a=n[0].value;if(!C(a))return-1;const u=n.map(e=>convertUnit(a,e.value));if(!arrayOfSameNumeric(u))return-1;if(u.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,a,Number.NaN);if(!r.rawPercentages&&d(a))return-1;const o=u.map(e=>e[4].value),i=Math.max(...o);return resultToCalculation(e,a,i)}function solveMin(e,n,r){if(!n.every(t))return-1;const a=n[0].value;if(!C(a))return-1;const u=n.map(e=>convertUnit(a,e.value));if(!arrayOfSameNumeric(u))return-1;if(u.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,a,Number.NaN);if(!r.rawPercentages&&d(a))return-1;const o=u.map(e=>e[4].value),i=Math.min(...o);return resultToCalculation(e,a,i)}function solveMod(e,n,t){const r=n.value;if(!C(r))return-1;const a=convertUnit(r,t.value);if(!twoOfSameNumeric(r,a))return-1;let u;return u=Number.isNaN(r[4].value)||Number.isNaN(a[4].value)||0===a[4].value?Number.NaN:Number.isFinite(r[4].value)&&(Number.isFinite(a[4].value)||(a[4].value!==Number.POSITIVE_INFINITY||r[4].value!==Number.NEGATIVE_INFINITY&&!Object.is(0*r[4].value,-0))&&(a[4].value!==Number.NEGATIVE_INFINITY||r[4].value!==Number.POSITIVE_INFINITY&&!Object.is(0*r[4].value,0)))?Number.isFinite(a[4].value)?(r[4].value%a[4].value+a[4].value)%a[4].value:r[4].value:Number.NaN,resultToCalculation(e,r,u)}function solvePow(e,n,t){const r=n.value,a=t.value;if(!m(r))return-1;if(!twoOfSameNumeric(r,a))return-1;if(Number.isNaN(r[4].value)||Number.isNaN(a[4].value))return numberToCalculation(e,Number.NaN);return numberToCalculation(e,Math.pow(r[4].value,a[4].value))}function solveRem(e,n,t){const r=n.value;if(!C(r))return-1;const a=convertUnit(r,t.value);if(!twoOfSameNumeric(r,a))return-1;let u;return u=Number.isNaN(r[4].value)||Number.isNaN(a[4].value)||0===a[4].value?Number.NaN:Number.isFinite(r[4].value)?Number.isFinite(a[4].value)?r[4].value%a[4].value:r[4].value:Number.NaN,resultToCalculation(e,r,u)}function snapAsBorderWidth(e,n,t){if(!s(n))return-1;const r=t.devicePixelLength??1,a=[v.Dimension,`${r}px`,n[2],n[3],{value:r,type:f.Integer,unit:"px"}],u=convertUnit(a,n);if(!twoOfSameNumeric(u,a))return-1;if(Number.isInteger(u[4].value/r))return resultToCalculation(e,n,n[4].value);if(u[4].value>0){if(u[4].value0?1/0:Object.is(0,0*u[4].value)?0:-0;break;default:i=Object.is(0,0*u[4].value)?0:-0}else switch(n){case"down":i=(o[4].value>0?Math.floor:Math.ceil)(u[4].value/o[4].value)*o[4].value;break;case"up":i=(o[4].value>0?Math.ceil:Math.floor)(u[4].value/o[4].value)*o[4].value;break;case"to-zero":i=Math.trunc(u[4].value/o[4].value)*o[4].value;break;default:{let t=Math.floor(u[4].value/o[4].value)*o[4].value,r=Math.ceil(u[4].value/o[4].value)*o[4].value;if(t>r){const e=t;t=r,r=e}const l=Math.abs(u[4].value-t),c=Math.abs(u[4].value-r);if(i="line-width"!==n||0!==r&&0!==t?l===c?r:l!r(e));return 1===n.length&&isNone(n[0])}if(!t(e))return!1;const n=e.value;return!!N(n)&&j.test(n[4].value)}function patchPrecision(e,n=13){if(-1===e)return-1;if(n<=0)return e;if(a(e))return e;const t=e.value;if(!C(t))return e;if(shouldSkipPrecisionRounding(t[4].value))return e;const r=roundToPrecision(t[4].value,n).toString();return m(t)?t[1]=r:d(t)?t[1]=r+"%":s(t)&&(t[1]=r+t[4].unit),e}function shouldSkipPrecisionRounding(e){if(!Number.isFinite(e)||0===e)return!0;if(Number.isInteger(e))return!0;const n=e.toString();return n.includes("e")||n.includes("E")}function roundToPrecision(e,n=13){return shouldSkipPrecisionRounding(e)?e:Number(e.toFixed(n))}const Y=String.fromCodePoint(0);function solveRandom(e,n,t,r,a,u){if(-1===n.fixed&&!u.randomCaching)return-1;const o=t.value;if(!C(o))return-1;const i=convertUnit(o,r.value);if(!twoOfSameNumeric(o,i))return-1;let l=null;if(a&&(l=convertUnit(o,a.value),!twoOfSameNumeric(o,l)))return-1;if(Number.isNaN(o[4].value)||Number.isNaN(i[4].value))return resultToCalculation(e,o,Number.NaN);if(!Number.isFinite(o[4].value))return resultToCalculation(e,o,o[4].value);if(!Number.isFinite(i[4].value)||!Number.isFinite(i[4].value-o[4].value))return resultToCalculation(e,o,Number.NaN);if(l){if(Number.isNaN(l[4].value))return resultToCalculation(e,o,Number.NaN);if(!Number.isFinite(l[4].value))return resultToCalculation(e,o,o[4].value)}const c=-1!==n.fixed?()=>n.fixed:(u.randomCaching.propertyN||(u.randomCaching.propertyN=0),sfc32(crc32([n.dashedIdent?n.dashedIdent:"",n.elementScoped?u.randomCaching.elementID:"",n.propertyScoped||n.propertyIndexScoped?u.randomCaching.propertyName:"",n.propertyIndexScoped?u.randomCaching.propertyN:"",u.randomCaching.documentID].join(Y)))),s=o[4].value;let v=i[4].value;if(v1e10)&&(l=null),l){const n=l[4].value,t=Math.max(n/1e3,1e-9);let r=Math.floor((v-s)/n);Math.abs(s+r*n-v)>=t&&Math.abs(s+(r+1)*n-v){const a=((e|=0)+(n|=0)|0)+(r|=0)|0;return r=r+1|0,e=n^n>>>9,n=(t|=0)+(t<<3)|0,t=(t=t<<21|t>>>11)+a|0,(a>>>0)/4294967296}}function crc32(e){let n,t,r=0;r^=-1;for(let a=0,u=e.length;a>>8^n;return(-1^r)>>>0}function solveCalcMix(e,n){const t=n[0].calcSum.value;if(!C(t))return-1;const r=n.map(e=>({calcSum:convertUnit(t,e.calcSum.value),percentage:e.percentage}));if(!arrayOfSameNumeric(r.map(e=>e.calcSum)))return-1;const{items:a}=normalizeMixPercentages(r,!1),u=a.map(e=>e.calcSum[4].value*e.percentage/100);let o=0;for(let e=0;e100||u>0&&n)for(const e of a)e.percentage=e.percentage*(100/u);let o=0;return u<100&&(o=100-u),{items:a,leftover:o}}const _=new Map([["abs",function abs(e,n,t){return singleNodeSolver(e,n,t,solveAbs)}],["acos",function acos(e,n,t){return singleNodeSolver(e,n,t,solveACos)}],["asin",function asin(e,n,t){return singleNodeSolver(e,n,t,solveASin)}],["atan",function atan(e,n,t){return singleNodeSolver(e,n,t,solveATan)}],["atan2",function atan2(e,n,t){return twoCommaSeparatedNodesSolver(e,n,t,solveATan2)}],["calc",calc$1],["calc-mix",function calcMix(e,n,t){const r=variadicArgumentsCalcMix(e,e.value,n,t);if(-1===r)return-1;return solveCalcMix(e,r)}],["clamp",function clamp(a,u,i){const l=resolveGlobalsAndConstants([...a.value.filter(e=>!r(e))],u),c=[],s=[],f=[];{let e=c;for(let n=0;n!r(e)),n,t);if(-1===a)return-1;const[u,o]=a,i=variadicArguments(e,o,n,t);if(-1===i)return-1;const[l,c,s]=i;if(!l||!c)return-1;return solveRandom(e,u,l,c,s,t)}],["rem",function rem(e,n,t){return twoCommaSeparatedNodesSolver(e,n,t,solveRem)}],["round",function round(e,a,u){const o=resolveGlobalsAndConstants([...e.value.filter(e=>!r(e))],a);let i="",l=!1;const c=[],p=[];{let e=c;for(let n=0;n!r(e))],n);if(1===i.length&&t(i[0]))return{inputs:[i[0]],operation:unary};let l=0;for(;l!r(e))],n),u=solve(calc$1(calcWrapper(e,a),n,t),t);return-1===u?-1:u}function twoCommaSeparatedNodesSolver(e,n,t,r){const a=twoCommaSeparatedArguments(e,n,t);if(-1===a)return-1;const[u,o]=a;return r(e,u,o,t)}function twoCommaSeparatedArguments(e,n,a){const u=resolveGlobalsAndConstants([...e.value.filter(e=>!r(e))],n),o=[],i=[];{let e=o;for(let n=0;n!r(e))],a),i=[];{const n=[];let r=[];for(let e=0;e!r(e))],a),i=[];{const n=[];let l=[];for(let e=0;e=0;i--){if(r(n[o][i]))continue;if(l=solve(calc$1(calcWrapper(e,n[o].slice(0,i+1)),a,u),u),-1===l)continue;const s=n[o].slice(i+1).filter(e=>!r(e));if(s.length)if(1===s.length&&t(s[0])&&d(s[0].value)){if(c=s[0].value[4].value,c<0||c>100)return-1}else{const n=solve(calc$1(calcWrapper(e,s),a,u),u);if(-1===n)return-1;if(!d(n.value))return-1;c=n.value[4].value,c=Math.min(100,Math.max(0,c))}break}if(-1===l)return-1;i.push({calcSum:l,percentage:c})}}return i}const H=new Set(["nearest","line-width","up","down","to-zero"]);function parseRandomValueSharing(e,n,r,a){const u={dashedIdent:"",fixed:-1,elementScoped:!1,propertyScoped:!1,propertyIndexScoped:!1};let o=!1;const i=n[0];if(!t(i)||!N(i.value))return[u,n];for(let i=0;i1)return-1;u.fixed=Math.max(0,Math.min(l.value[4].value,1-Number.EPSILON/2));continue}if("auto"!==c){if(!c.startsWith("--"))return 0===i?[u,n]:-1;if(-1!==u.fixed||o||u.dashedIdent)return-1;u.dashedIdent=c}else{if(-1!==u.fixed||o||u.dashedIdent||u.elementScoped||u.propertyScoped||u.propertyIndexScoped)return-1;u.elementScoped=!0,u.propertyIndexScoped=!0,o=!0}}else{if(-1!==u.fixed||o||u.propertyScoped||u.propertyIndexScoped)return-1;u.propertyIndexScoped=!0}else{if(-1!==u.fixed||o||u.propertyScoped||u.propertyIndexScoped)return-1;u.propertyScoped=!0}else{if(-1!==u.fixed||o||u.elementScoped)return-1;u.elementScoped=!0}}return-1}function calcWrapper(e,n){return new o([v.Function,"calc(",e.name[2],e.name[3],{value:"calc"}],[v.CloseParen,")",e.endToken[2],e.endToken[3],void 0],n)}function maxWrapper(t,r,a){return new o([v.Function,"max(",t.name[2],t.name[3],{value:"max"}],[v.CloseParen,")",t.endToken[2],t.endToken[3],void 0],[r,new n([v.Comma,",",...e(r),void 0]),a])}function patchNaN(e){if(-1===e)return-1;if(a(e))return e;const t=e.value;return C(t)&&Number.isNaN(t[4].value)?m(t)?new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,"NaN",t[2],t[3],{value:"NaN"}])]):s(t)?new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,"NaN",t[2],t[3],{value:"NaN"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Dimension,"1"+t[4].unit,t[2],t[3],{value:1,type:f.Integer,unit:t[4].unit}])]):d(t)?new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,"NaN",t[2],t[3],{value:"NaN"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Percentage,"1%",t[2],t[3],{value:1}])]):-1:e}function patchInfinity(e){if(-1===e)return-1;if(a(e))return e;const t=e.value;if(!C(t))return e;if(Number.isFinite(t[4].value)||Number.isNaN(t[4].value))return e;let r="";return Number.NEGATIVE_INFINITY===t[4].value&&(r="-"),m(t)?new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}])]):s(t)?new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Dimension,"1"+t[4].unit,t[2],t[3],{value:1,type:f.Integer,unit:t[4].unit}])]):new o([v.Function,"calc(",t[2],t[3],{value:"calc"}],[v.CloseParen,")",t[2],t[3],void 0],[new n([v.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new i([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Percentage,"1%",t[2],t[3],{value:1}])])}function patchMinusZero(e){if(-1===e)return-1;if(a(e))return e;const n=e.value;return C(n)&&Object.is(-0,n[4].value)?("-0"===n[1]||(d(n)?n[1]="-0%":s(n)?n[1]="-0"+n[4].unit:n[1]="-0"),e):e}function patchCanonicalUnit(e){return-1===e?-1:a(e)?e:s(e.value)?(e.value=toCanonicalUnit(e.value),e):e}function patchCalcResult(e,n){let t=e;return n?.toCanonicalUnits&&(t=patchCanonicalUnit(t)),t=patchPrecision(t,n?.precision),t=patchMinusZero(t),n?.censorIntoStandardRepresentableValues||(t=patchNaN(t),t=patchInfinity(t)),t}function tokenizeGlobals(e){const n=new Map;if(!e)return n;for(const[t,r]of e)if(b(r))n.set(t,r);else if("string"==typeof r){const e=B({css:r}),a=e.nextToken();if(e.nextToken(),!e.endOfFile())continue;if(!C(a))continue;n.set(t,a);continue}return n}function calc(e,n){return calcFromComponentValues(l(A({css:e}),{}),n).map(e=>e.map(e=>w(...e.tokens())).join("")).join(",")}function calcFromComponentValues(e,n){const t=tokenizeGlobals(n?.globals);return replaceComponentValues(e,e=>{if(!a(e))return;const r=_.get(e.getName().toLowerCase());if(!r)return;const u=patchCalcResult(solve(r(e,t,n??{}),n??{}),n);return-1!==u?n?.calcWrapper?calcWrapper(e,[u]):u:void 0})}function replaceComponentValues(n,a){for(let u=0;u{if("number"!=typeof u)return;const o=a(n.node);if(!o)return;const l=[o],c=n.parent.value[u-1];t(c)&&h(c.value)&&("-"===c.value[4].value||"+"===c.value[4].value)&&l.splice(0,0,new i([[v.Whitespace," ",...e(n.node),void 0]]));const s=n.parent.value[u+1];!s||r(s)||t(s)&&(D(s.value)||F(s.value)||E(s.value)||h(s.value)&&"-"!==s.value[4].value&&"+"!==s.value[4].value)||l.push(new i([[v.Whitespace," ",...e(n.node),void 0]])),n.parent.value.splice(u,1,...l)})}return n}const J=new Set(_.keys());export{ParseError,S as ParseErrorMessage,ParseErrorWithComponentValues,calc,calcFromComponentValues,J as mathFunctionNames}; diff --git a/packages/css-calc/package.json b/packages/css-calc/package.json index c636d1416..6c4afffd6 100644 --- a/packages/css-calc/package.json +++ b/packages/css-calc/package.json @@ -44,13 +44,17 @@ "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.1" }, + "devDependencies": { + "puppeteer": "^25.7.0" + }, "scripts": { "build": "rollup -c ../../rollup/default.mjs", "docs": "node ../../.github/bin/generate-docs/api-documenter.mjs", "lint": "node ../../.github/bin/format-package-json.mjs", "prepublishOnly": "npm run build && npm run test", "stryker": "stryker run --logLevel error", - "test": "node --test ./test/test.mjs ./test/_import.mjs ./test/_require.cjs" + "test": "node --test ./test/test.mjs ./test/_import.mjs ./test/_require.cjs ./test/browser/_browser.mjs", + "test:browser": "BROWSER_TESTS=true node --test ./test/browser/_browser.mjs" }, "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/css-calc#readme", "repository": { diff --git a/packages/css-calc/src/functions/log.ts b/packages/css-calc/src/functions/log.ts index 03fd4f53a..e4d852420 100644 --- a/packages/css-calc/src/functions/log.ts +++ b/packages/css-calc/src/functions/log.ts @@ -42,6 +42,11 @@ export function solveLog(logNode: FunctionNode, solvedNodes: Array `0`). + */ +function shouldSkipPrecisionRounding(value: number): boolean { + if (!Number.isFinite(value) || value === 0) { + return true; + } + + if (Number.isInteger(value)) { + return true; + } + + const serialized = value.toString(); + return serialized.includes('e') || serialized.includes('E'); +} + +export function roundToPrecision(value: number, precision = 13): number { + if (shouldSkipPrecisionRounding(value)) { + return value; + } + + // Otherwise round to a number of decimals. + return Number(value.toFixed(precision)); +} diff --git a/packages/css-calc/test/additional/index.mjs b/packages/css-calc/test/additional/index.mjs index 05f5b1aab..40b098cbb 100644 --- a/packages/css-calc/test/additional/index.mjs +++ b/packages/css-calc/test/additional/index.mjs @@ -1,4 +1,5 @@ import './mod-rem-infinity.mjs'; +import './precision.mjs'; import './random.mjs'; import './sign-abs-equivalent.mjs'; import './tan-asymptotes.mjs'; diff --git a/packages/css-calc/test/additional/precision.mjs b/packages/css-calc/test/additional/precision.mjs new file mode 100644 index 000000000..f8daf24e8 --- /dev/null +++ b/packages/css-calc/test/additional/precision.mjs @@ -0,0 +1,51 @@ +import { calc } from '@csstools/css-calc'; +import assert from 'node:assert'; + +// Values that serialize without scientific notation are rounded to `precision` +// decimal places. +assert.strictEqual( + calc('calc(1 / 3)'), + '0.3333333333333', +); + +assert.strictEqual( + calc('calc(1 / 3)', { precision: 5 }), + '0.33333', +); + +assert.strictEqual( + calc('calc(0.1 + 0.2)'), + '0.3', +); + +// Values that serialize in scientific notation are left untouched. +// Reducing them to a number of decimals would destroy them (e.g. `1e-20` -> `0`). +assert.strictEqual( + calc('calc(1e-10 * 1e-10)'), + '1.0000000000000001e-20', +); + +assert.strictEqual( + calc('calc(1e-20 * 1)'), + '1e-20', +); + +assert.strictEqual( + calc('calc(1e-300 * 1)'), + '1e-300', +); + +assert.strictEqual( + calc('calc(1e300 * 1)'), + '1e+300', +); + +assert.strictEqual( + calc('calc(1e21 * 1)'), + '1e+21', +); + +assert.strictEqual( + calc('calc(1e-10 * 1e-10)', { precision: 5 }), + '1.0000000000000001e-20', +); diff --git a/packages/css-calc/test/additional/random.mjs b/packages/css-calc/test/additional/random.mjs index 80f60565a..aae46060d 100644 --- a/packages/css-calc/test/additional/random.mjs +++ b/packages/css-calc/test/additional/random.mjs @@ -46,6 +46,28 @@ assert.strictEqual( '500px', ); +// Values that serialize in scientific notation are left untouched. +// Rounding them to a number of decimals would destroy them (e.g. `1e-20` -> `0`). +assert.strictEqual( + calc('random(fixed 0.5, 1e-20, 1e-10)'), + '5.0000000005e-11', +); + +assert.strictEqual( + calc('random(fixed 0.5, 1e-20, 1e-10, 1e-30)'), + '5.0000000005e-11', +); + +assert.strictEqual( + calc('random(fixed 0, 1e-20, 1e-10)'), + '1e-20', +); + +assert.strictEqual( + calc('random(fixed 1, 1e-20, 1e-10)'), + '9.999999999999999e-11', +); + assert.strictEqual( calc('random(100px, 500px)', { randomCaching: { documentID: 'a', elementID: 'b', propertyName: 'c', propertyN: 0 } }), '494.67561px', diff --git a/packages/css-calc/test/browser/_browser.html b/packages/css-calc/test/browser/_browser.html new file mode 100644 index 000000000..af0e6791e --- /dev/null +++ b/packages/css-calc/test/browser/_browser.html @@ -0,0 +1,99 @@ + + + + + css-calc browser tests + + + + + +
+
+
+ + + + diff --git a/packages/css-calc/test/browser/_browser.mjs b/packages/css-calc/test/browser/_browser.mjs new file mode 100644 index 000000000..888dce7af --- /dev/null +++ b/packages/css-calc/test/browser/_browser.mjs @@ -0,0 +1,274 @@ +/* global window */ +import puppeteer from 'puppeteer'; +import http from 'node:http'; +import fs from 'node:fs/promises'; +import test from 'node:test'; +import assert from 'node:assert'; +import process from 'node:process'; +import { calc } from '@csstools/css-calc'; +import { approxEqual, propertyForResult } from './compare.mjs'; +import { corpus } from './corpus.mjs'; +import { generateCases } from './generate.mjs'; +import { knownBrowserPrecisionDivergences, knownDivergenceKey, knownDivergencesThatNoLongerOccur } from './known-divergences.mjs'; + +const requestListener = async function (req, res) { + const parsedUrl = new URL(req.url, 'http://localhost:8080'); + + switch (parsedUrl.pathname) { + case '': + case '/': + res.setHeader('Content-type', 'text/html'); + res.writeHead(200); + res.end(await fs.readFile('test/browser/_browser.html', 'utf8')); + break; + default: + res.setHeader('Content-type', 'text/plain'); + res.writeHead(404); + res.end('Not found'); + break; + } +}; + +function startServer() { + const server = http.createServer(requestListener); + server.listen(8080); + + return () => { + server.close(); + }; +} + +const CONFIGS = [ + { name: 'default', options: {} }, + { name: 'canonical-units', options: { toCanonicalUnits: true } }, + { name: 'raw-percentages', options: { rawPercentages: true } }, +]; + +const expressions = [ + ...new Set([ + ...corpus, + ...generateCases({ seed: 0xC5C401C, count: 1200 }), + ]), +]; + +function buildCases() { + const cases = []; + + for (const expression of expressions) { + for (const config of CONFIGS) { + let result; + + try { + result = calc(expression, config.options); + } catch { + continue; + } + + if (typeof result !== 'string') { + continue; + } + + const value = result.trim(); + if (!value || value === expression) { + // The expression was not solved by `css-calc`; nothing to compare. + continue; + } + + const target = propertyForResult(value); + if (!target) { + continue; + } + + cases.push({ + expression, + result: value, + property: target.property, + unit: target.unit, + config: config.name, + }); + } + } + + return cases; +} + +function verify(caseItem, observation) { + if (observation.expressionSpecified === '') { + // The browser does not support (or rejects) the original expression. + return null; + } + + if (observation.resultSpecified === '') { + return `[${caseItem.config}] css-calc result "${caseItem.result}" is invalid in the browser for "${caseItem.property}" (expression: "${caseItem.expression}")`; + } + + // `expr * 0 + 1` differs from `0` only for `NaN`/`infinity`. + // The computed value is `1` for finite input and `0` otherwise, which + // differentiates `NaN` from a real `0` without inspecting serialized text. + const expressionIsNonFinite = observation.expressionNaN === '0'; + const resultIsNonFinite = observation.resultNaN === '0'; + + if (expressionIsNonFinite || resultIsNonFinite) { + if (expressionIsNonFinite !== resultIsNonFinite) { + return `[${caseItem.config}] expected both "${caseItem.expression}" and "${caseItem.result}" to be NaN/infinity; got "${observation.expressionSpecified}" and "${observation.resultSpecified}"`; + } + + return null; + } + + if (observation.expressionComputed === '' || observation.resultComputed === '') { + return `[${caseItem.config}] missing computed value for "${caseItem.expression}" ("${observation.expressionComputed}") or "${caseItem.result}" ("${observation.resultComputed}")`; + } + + const expressionNumber = parseFloat(observation.expressionComputed); + const resultNumber = parseFloat(observation.resultComputed); + + if (Number.isNaN(expressionNumber) || Number.isNaN(resultNumber)) { + return `[${caseItem.config}] could not parse computed values "${observation.expressionComputed}" / "${observation.resultComputed}" (expression: "${caseItem.expression}")`; + } + + if (!approxEqual(expressionNumber, resultNumber)) { + return `[${caseItem.config}] expected "${caseItem.expression}" (${observation.expressionComputed}) and "${caseItem.result}" (${observation.resultComputed}) to be equivalent in "${caseItem.property}"`; + } + + // A `0` and a `-0` serialize the same, but are different values. + // The sign of the zero is observed through `1 / sign(...)`. + if (expressionNumber === 0 && resultNumber === 0 && observation.expressionZeroSign !== '' && observation.resultZeroSign !== '' && observation.expressionZeroSign !== observation.resultZeroSign) { + return `[${caseItem.config}] expected "${caseItem.expression}" (${observation.expressionZeroSign}) and "${caseItem.result}" (${observation.resultZeroSign}) to have the same zero sign`; + } + + return null; +} + +// The documented `cssCalc` value in `knownBrowserPrecisionDivergences` is +// deterministic, so it can be checked without launching a browser. This keeps +// the documentation from drifting when `css-calc` changes. +test('known browser precision divergences documentation', () => { + for (const divergence of knownBrowserPrecisionDivergences) { + const config = CONFIGS.find((entry) => entry.name === divergence.config); + assert.ok(config, `unknown config "${divergence.config}" for "${divergence.expression}"`); + + assert.strictEqual( + calc(divergence.expression, config.options), + divergence.cssCalc, + `documented cssCalc value for "${divergence.expression}" (${divergence.config}) is stale`, + ); + } +}); + +test('browser', { skip: !process.env.BROWSER_TESTS }, async () => { + const cases = buildCases(); + assert.ok(cases.length > 0, 'expected at least one solvable expression'); + + const cleanup = startServer(); + + let browser; + + try { + browser = await puppeteer.launch({ + headless: 'new', + }); + + const page = await browser.newPage(); + page.on('pageerror', (msg) => { + throw msg; + }); + + await page.goto('http://localhost:8080'); + + const observations = await page.evaluate((entries) => { + return entries.map((entry) => { + const expressionObservation = window.observe(entry.property, entry.expression); + const resultObservation = window.observe(entry.property, entry.result); + + return { + expressionSpecified: expressionObservation.specified, + expressionComputed: expressionObservation.computed, + resultSpecified: resultObservation.specified, + resultComputed: resultObservation.computed, + // `1` for finite, `0` for `NaN`/`infinity`, `''` for unsupported. + expressionNaN: window.observeNaN(entry.property, entry.expression, entry.unit), + resultNaN: window.observeNaN(entry.property, entry.result, entry.unit), + expressionZeroSign: '', + resultZeroSign: '', + }; + }); + }, cases); + + const zeroSignEntries = []; + for (let i = 0; i < cases.length; i++) { + const observation = observations[i]; + + if (observation.expressionNaN !== '1' || observation.resultNaN !== '1') { + continue; + } + + if (parseFloat(observation.expressionComputed) === 0 && parseFloat(observation.resultComputed) === 0) { + zeroSignEntries.push(i); + } + } + + const zeroSigns = await page.evaluate((entries) => { + return entries.map((entry) => { + return { + expression: window.observeZeroSign(entry.expression), + result: window.observeZeroSign(entry.result), + }; + }); + }, zeroSignEntries.map((i) => cases[i])); + + for (let i = 0; i < zeroSignEntries.length; i++) { + const observation = observations[zeroSignEntries[i]]; + observation.expressionZeroSign = zeroSigns[i].expression; + observation.resultZeroSign = zeroSigns[i].result; + } + + const failures = []; + const divergences = []; + const usedDivergences = new Set(); + let checked = 0; + + for (let i = 0; i < cases.length; i++) { + if (observations[i].expressionSpecified !== '') { + checked++; + } + + const failure = verify(cases[i], observations[i]); + if (!failure) { + continue; + } + + const divergenceKey = knownDivergenceKey(cases[i], failure); + if (divergenceKey) { + usedDivergences.add(divergenceKey); + divergences.push(failure); + continue; + } + + failures.push(failure); + } + + assert.ok(checked >= 100, `expected at least 100 browser-checked cases, got ${checked}`); + assert.deepStrictEqual( + failures.slice(0, 25), + [], + `${failures.length} css-calc result(s) disagree with the browser:\n${failures.slice(0, 25).join('\n')}\n\nAlso see test/browser/known-divergences.mjs for documented browser differences.`, + ); + + const unusedDivergences = knownDivergencesThatNoLongerOccur(usedDivergences); + assert.deepStrictEqual( + unusedDivergences, + [], + `${unusedDivergences.length} documented divergence(s) no longer occur. Remove them from test/browser/known-divergences.mjs:\n${unusedDivergences.join('\n')}`, + ); + + if (divergences.length) { + // eslint-disable-next-line no-console + console.log(`ℹ ${divergences.length} case(s) hit a documented browser divergence.`); + } + } finally { + await browser?.close(); + + await cleanup(); + } +}); diff --git a/packages/css-calc/test/browser/_fuzz.mjs b/packages/css-calc/test/browser/_fuzz.mjs new file mode 100644 index 000000000..bf7d705b1 --- /dev/null +++ b/packages/css-calc/test/browser/_fuzz.mjs @@ -0,0 +1,63 @@ +import test from 'node:test'; +import assert from 'node:assert'; +import process from 'node:process'; +import { calc } from '@csstools/css-calc'; +import { generateCases } from './generate.mjs'; + +const CONFIGS = [ + { name: 'default', options: {} }, + { name: 'canonical-units', options: { toCanonicalUnits: true } }, + { name: 'raw-percentages', options: { rawPercentages: true } }, + { name: 'censored', options: { censorIntoStandardRepresentableValues: true } }, +]; + +// Expressions that exercise options and code paths which are not part of the +// general generated grammar: +// - `random()` needs `randomCaching` +// - `calc-mix()` needs the parser to keep the mix percentage +// - `round(line-width, ...)` needs `devicePixelLength` +// - `onParseError` needs a callback +const targetedExpressions = [ + 'random(100px, 500px)', + 'random(fixed 0.5, 100px, 500px)', + 'random(auto, 100px, 500px)', + 'random(element-scoped, 100px, 500px)', + 'random(--foo, 100px, 500px)', + 'random(100px, NaN)', + 'calc-mix(10px, 20px)', + 'calc-mix(in srgb, red 40%, blue)', + 'calc(round(line-width, 1.2345px))', + 'round(line-width, 1.2345px)', + 'clamp(1px, 2px, none)', + 'clamp(none, 2px, 3px)', + 'clamp(1px, none, 3px)', + 'calc(1px + )', + 'calc(1 + )', + 'calc(1px + 2)', + 'calc(2 - 1px)', + 'round(1 + )', +]; + +const randomCaching = { + documentID: 'a', + elementID: 'b', + propertyName: 'width', + propertyN: 0, +}; + +// Large Node-only fuzz run. +// +// This does not launch a browser. It is used to: +// - make sure no generated expression crashes `css-calc` +// - measure function coverage of `dist/index.mjs` via +// `npm run test:coverage` +test('fuzz', () => { + const count = Number(process.env.FUZZ_ITERATIONS ?? 50000); + const cases = [...targetedExpressions, ...generateCases({ seed: 1, count })]; + + for (const expression of cases) { + for (const config of CONFIGS) { + assert.doesNotThrow(() => calc(expression, { ...config.options, randomCaching, devicePixelLength: 0.021, onParseError: () => { /* noop */ } }), `calc(${JSON.stringify(expression)}) threw for config ${config.name}`); + } + } +}); diff --git a/packages/css-calc/test/browser/compare.mjs b/packages/css-calc/test/browser/compare.mjs new file mode 100644 index 000000000..b4101d7f8 --- /dev/null +++ b/packages/css-calc/test/browser/compare.mjs @@ -0,0 +1,128 @@ +const NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?$/i; +const PERCENTAGE_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?%$/i; +const DIMENSION_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?([a-z]+)$/i; +const NON_FINITE_RE = /^calc\(\s*(-?)(nan|infinity)(?:\s*\*\s*1([a-z]+|%))?\s*\)$/i; + +const LENGTH_UNITS = new Set([ + 'px', 'cm', 'mm', 'q', 'in', 'pc', 'pt', + 'em', 'rem', 'ex', 'ch', 'lh', 'rlh', + 'vw', 'vh', 'vmin', 'vmax', 'vb', 'vi', +]); + +const ANGLE_UNITS = new Set(['deg', 'grad', 'rad', 'turn']); +const TIME_UNITS = new Set(['s', 'ms']); + +/** + * Determine which property to use when observing a css-calc result in a browser. + * + * Returns `null` when the result can't be observed (multi-value, unsolved calc, + * unsupported unit, ...). + * + * @param {string} result + * @returns {{ property: string, kind: string, unit: string } | null} + */ +export function propertyForResult(result) { + const value = result.trim(); + + const nonFinite = value.match(NON_FINITE_RE); + if (nonFinite) { + return propertyForUnit(nonFinite[3] ? nonFinite[3].toLowerCase() : ''); + } + + if (PERCENTAGE_RE.test(value)) { + return propertyForUnit('%'); + } + + if (NUMBER_RE.test(value)) { + return propertyForUnit(''); + } + + const dimension = value.match(DIMENSION_RE); + if (!dimension) { + return null; + } + + return propertyForUnit(dimension[1].toLowerCase()); +} + +function propertyForUnit(unit) { + if (unit === '') { + return { property: 'scale', kind: 'number', unit }; + } + + if (unit === '%') { + return { property: 'margin-left', kind: 'percentage', unit }; + } + + if (LENGTH_UNITS.has(unit)) { + return { property: 'margin-left', kind: 'length', unit }; + } + + if (ANGLE_UNITS.has(unit)) { + return { property: 'rotate', kind: 'angle', unit }; + } + + if (TIME_UNITS.has(unit)) { + return { property: 'transition-delay', kind: 'time', unit }; + } + + return null; +} + +/** + * Reduce a number to a given number of significant digits. + * + * The browser that computes the value keeps about 6 significant digits (e.g. + * `calc(1px / 3)` serializes as `0.333333px` and `calc(1e10px / 3)` as + * `3.33333e+09px`). `css-calc` keeps full double precision. + * + * WPT compares computed values with a fuzzy comparison + * (`assert_array_approx_equals`, see `css/support/color-testcommon.js`), but + * uses a fixed absolute epsilon. That does not work for values spanning many + * orders of magnitude, so we reduce both values to the same number of + * significant digits before comparing instead. + * + * @param {number} value + * @param {number} [significantDigits] + * @returns {number} + */ +export function reduceToSignificantDigits(value, significantDigits = 6) { + if (!Number.isFinite(value) || value === 0) { + return value; + } + + return Number(value.toPrecision(significantDigits)); +} + +/** + * Compare two computed values after accounting for the browser's precision. + * + * The browser keeps about 6 significant digits. That means very small values + * are indistinguishable from `0` there: for example `cos(90deg)` is really + * `6.12e-17`, but the browser's `rotate` serializes the computed angle as + * `0deg`. `css-calc` keeps the true value instead. + * + * To bridge that gap, values whose magnitude is at most `1e-(significantDigits-1)` + * are treated as `0` before the significant digit comparison. + * + * @param {number} a + * @param {number} b + * @param {number} [significantDigits] + * @returns {boolean} + */ +export function approxEqual(a, b, significantDigits = 6) { + if (a === b) { + return true; + } + + if (Object.is(a, -0) || Object.is(b, -0)) { + // Zero sign is compared separately. + return a === 0 && b === 0; + } + + // Below the browser's precision floor a value is indistinguishable from `0`. + const floor = Math.pow(10, -(significantDigits - 1)); + const reduce = (value) => reduceToSignificantDigits(Math.abs(value) < floor ? 0 : value, significantDigits); + + return reduce(a) === reduce(b); +} diff --git a/packages/css-calc/test/browser/corpus.mjs b/packages/css-calc/test/browser/corpus.mjs new file mode 100644 index 000000000..c5588768e --- /dev/null +++ b/packages/css-calc/test/browser/corpus.mjs @@ -0,0 +1,628 @@ +/** + * Curated math expressions. + * + * Ported from the WPT `css-values` test suite + * (https://github.com/web-platform-tests/wpt/tree/master/css/css-values) + * and from the CSS Values and Units specifications + * (https://drafts.csswg.org/css-values-4/, https://drafts.csswg.org/css-values-5/). + * + * These are intentionally a mix of: + * - expressions `css-calc` can solve (compared against a browser) + * - expressions `css-calc` can't solve (browser rejects or computes to the same value) + * - spec edge cases (infinities, infectious NaN, sign of zero, type analysis) + */ +export const corpus = [ + // calc() arithmetic + 'calc(1 + 2)', + 'calc(10 * 2)', + 'calc(15 / 5 / 3)', + 'calc(15 / (5 / 3))', + 'calc(15 / (3 / 5))', + 'calc(2 * 3 + 7 * 5)', + 'calc(((2 * 3) + 7) * 5)', + 'calc(1 / 3)', + 'calc(1 / 3 * 1px)', + 'calc(100px * 0 / 0)', + 'calc(100px / 0)', + 'calc(100px / (0))', + 'calc(100px / (2 - 2))', + 'calc(100px * (1 / 0))', + 'calc(1 / 0)', + 'calc(1 / -0)', + 'calc(-0)', + 'calc(0 * -1)', + 'calc(0 / -5)', + 'calc(1e10 * 1e10)', + 'calc(1e-10 * 1e-10)', + 'calc(9007199254740992 + 1)', + + // calc() with dimensions + 'calc(1px + 2px)', + 'calc(1cm + 1mm)', + 'calc(1in + 1px)', + 'calc(1Q + 1px)', + 'calc(1pc + 1pt)', + 'calc(1em + 1px)', + 'calc(1rem + 1px)', + 'calc(1vw + 1vh)', + 'calc(2px * 2)', + 'calc(2 * 2px)', + 'calc(2px / 2)', + 'calc(2px * 1px)', + 'calc(0.01khz + 10hz)', + 'calc(10hz + 0.01khz)', + 'calc(1s + 1ms)', + 'calc(1ms - 1s)', + 'calc(45deg + 45deg)', + 'calc(45deg + 1rad)', + 'calc(20deg + 200grad)', + 'calc(200deg + 0.5turn)', + 'calc(1rad + 0.5turn)', + 'calc(45grad + 45grad)', + 'calc(10grad + 0.5turn)', + 'calc(45deg - 15deg)', + 'calc(90deg * 2)', + 'calc(2 * 45rad)', + 'calc(90deg / 2)', + 'calc(45grad * 2)', + 'calc(90turn / 2)', + + // calc() with percentages + 'calc(50%)', + 'calc(50% * 2)', + 'calc(2 * 50%)', + 'calc(50% / 2)', + 'calc(10% + 5%)', + 'calc(10% - 5%)', + 'calc(10px + 20%)', + + // nested calc() + 'calc(calc(10px) + calc(20px))', + 'calc(10px + calc(20% + 10px))', + 'calc(calc(10px) + 10%)', + 'calc(calc(10px) - 20%)', + 'calc(2 * calc(3 + 4))', + + // min() / max() + 'min(1px, 2px)', + 'min(2px, 1px)', + 'max(1px, 2px)', + 'max(2px, 1px)', + 'min(1, 2)', + 'max(0.1, 0.2)', + 'min(1%, 2%)', + 'max(10%, 5%)', + 'min(1s, 2s)', + 'max(0.9s, 1000ms)', + 'min(1deg, 2deg)', + 'min(90deg, 0.26turn)', + 'max(1.58rad, 90deg)', + 'min(270deg, max(0.25turn, 3.14rad))', + 'calc(min(90deg, 1.58rad) + 0.25turn)', + 'calc(min(90deg, 1.58rad) - 0.125turn)', + 'calc(min(90deg, 1.58rad) * 2)', + 'calc(min(90deg, 1.58rad) / 2)', + 'min(1s + 100ms, 500ms * 3)', + 'calc(min(1s, 2s) + max(3s, 4s) + 10s)', + 'min(20%, max(10%, 15%))', + 'max(10%, min(20%, 15%))', + 'min(1px, 2em)', + 'max(1px, 2em)', + + // clamp() + 'clamp(10px, 20px, 30px)', + 'clamp(10px, 5px, 30px)', + 'clamp(10px, 35px, 30px)', + 'clamp(30px, 100px, 20px)', + 'clamp(1px, 2px, clamp(2px, 3px, 4px))', + 'clamp(1, 2, 3)', + 'clamp(1, 0, 3)', + 'clamp(100px, 80px, 50px)', + 'clamp(100px, -1px, 50px)', + 'clamp(100px, 101px, 50px)', + 'clamp(1px, 2px, 1px)', + 'clamp(1s, 2s, 3s)', + 'clamp(1deg, 2deg, 3deg)', + 'clamp(1%, 2%, 3%)', + 'clamp(1px, none, 3px)', + 'clamp(none, 2px, 3px)', + 'clamp(1px, 2px, none)', + 'clamp(none, none, 3px)', + + // round() / mod() / rem() + 'round(10, 10)', + 'round(23px, 10px)', + 'round(18px, 10px)', + 'round(-13px, 10px)', + 'round(-18px, 10px)', + 'round(nearest, 18px, 10px)', + 'round(up, 23px, 10px)', + 'round(up, -13px, 10px)', + 'round(down, 23px, 10px)', + 'round(down, -13px, 10px)', + 'round(to-zero, 13px, 10px)', + 'round(to-zero, -13px, 10px)', + 'round(10px, 6px)', + 'round(10s, 6000ms)', + 'round(10000ms, 6s)', + 'round(10deg, 6deg)', + 'round(23, 10)', + 'round(18, 10)', + 'round(15, 10)', + 'round(-18, 10)', + 'round(10, NaN)', + 'round(NaN, 10)', + 'round(10, 0)', + 'round(infinity, 10)', + 'round(10, infinity)', + 'round(infinity, infinity)', + 'mod(1, 1)', + 'mod(18, 5)', + 'mod(-18, 5)', + 'mod(140, -90)', + 'mod(-140, -90)', + 'mod(10px, 6px)', + 'mod(10s, 6000ms)', + 'mod(18%, 5%)', + 'mod(-18%, 5%)', + 'mod(18vw, 5vw)', + 'mod(5, NaN)', + 'mod(NaN, 5)', + 'mod(10, 0)', + 'mod(infinity, 10)', + 'mod(-infinity, 10)', + 'rem(1, 1)', + 'rem(18, 5)', + 'rem(-18, 5)', + 'rem(140, -90)', + 'rem(-140, -90)', + 'rem(10px, 6px)', + 'rem(10s, 6000ms)', + 'rem(-18vw, 5vw)', + 'rem(5, NaN)', + 'rem(10, 0)', + 'rem(infinity, 10)', + + // abs() / sign() + 'abs(1)', + 'abs(-1)', + 'abs(10px)', + 'abs(-10px)', + 'abs(10%)', + 'abs(-10%)', + 'abs(10deg)', + 'abs(-10deg)', + 'abs(1s)', + 'abs(infinity)', + 'abs(-infinity)', + 'abs(NaN)', + 'sign(1)', + 'sign(-1)', + 'sign(0)', + 'sign(-0)', + 'sign(10px)', + 'sign(-10px)', + 'sign(10%)', + 'sign(infinity)', + 'sign(-infinity)', + 'sign(NaN)', + 'calc(abs(-0.1 + 0.2) + 0.05)', + 'calc(sign(0.1 + 0.2) - 0.05)', + 'calc(2 - sign(1))', + 'calc(2 - sign(-1))', + 'calc(20 - abs(-10))', + 'calc(0.1 - 0.2)', + + // floor() / ceil() (rounding-strategy aliases) + 'calc(floor(1.5))', + 'calc(ceil(1.5))', + + // trig + 'sin(0)', + 'cos(0)', + 'tan(0)', + 'sin(90deg)', + 'cos(90deg)', + 'tan(45deg)', + 'sin(0.25turn)', + 'cos(100grad)', + 'sin(1.5707963267948966)', + 'cos(pi)', + 'sin(pi / 2)', + 'tan(pi / 4)', + 'sin(-90deg)', + 'cos(-180deg)', + 'tan(90deg)', + 'tan(-90deg)', + 'tan(270deg)', + 'sin(infinity)', + 'sin(NaN)', + 'cos(NaN)', + 'tan(NaN)', + 'sin(10px)', + 'sin(-0rad)', + 'tan(-0rad)', + 'sin(-0deg)', + 'tan(-0deg)', + 'sin(-0)', + 'tan(-0)', + 'calc(sin(cos(pi) + 1))', + 'calc(sin(tan(pi / 4) * pi / 2))', + + // inverse trig + 'asin(0)', + 'asin(0.5)', + 'asin(1)', + 'asin(-1)', + 'acos(0)', + 'acos(0.5)', + 'acos(1)', + 'acos(-1)', + 'atan(0)', + 'atan(1)', + 'atan(-1)', + 'atan(infinity)', + 'asin(1.5)', + 'acos(1.5)', + 'asin(NaN)', + 'atan2(1, 1)', + 'atan2(1, -1)', + 'atan2(-1, -1)', + 'atan2(0, 0)', + 'atan2(-0, -0)', + 'atan2(infinity, infinity)', + + // exp() / log() / sqrt() / pow() / hypot() + 'exp(0)', + 'exp(1)', + 'exp(-1)', + 'exp(infinity)', + 'exp(-infinity)', + 'log(1)', + 'log(e)', + 'log(2, 2)', + 'log(8, 2)', + 'log(0)', + 'log(-1)', + 'sqrt(0)', + 'sqrt(1)', + 'sqrt(2)', + 'sqrt(4)', + 'sqrt(-1)', + 'sqrt(infinity)', + 'pow(2, 3)', + 'pow(2, -1)', + 'pow(-2, 3)', + 'pow(-2, 0.5)', + 'pow(NaN, 0)', + 'pow(1, NaN)', + 'pow(0, 0)', + 'pow(infinity, 2)', + 'hypot(3, 4)', + 'hypot(3px, 4px)', + 'hypot(3px, 4em)', + 'hypot(3, 4, 12)', + 'hypot(infinity, 5)', + 'hypot(NaN, 5)', + 'hypot(1s, 1s)', + 'hypot(1deg, 1deg)', + + // Type analysis / invalid combinations + 'calc(1px + 2)', + 'calc(2 + 1px)', + 'calc(1px - 2)', + 'calc(2 - 1px)', + 'calc(1px * 1px)', + 'calc(1px / 1px)', + 'calc(1 + 1%)', + 'calc(1deg + 1px)', + 'min(1px, 1s)', + 'max(1, 1px)', + 'mod(1, 1px)', + 'round(1, 1%)', + + // Globals / unresolved expressions + 'calc(unknown * 2)', + 'calc(15 / unknown(5 / 3))', + 'var(--x)', + 'calc(var(--x) + 1)', + 'env(safe-area-inset-top)', + + // Non-math functions are left alone + 'foo(calc(1 + 2))', + 'rgb(calc(10 + 20) 0 0)', + + // Special-value grids (see https://drafts.csswg.org/css-values-4/#exponent-infinities, + // #trig-infinities, #round-infinities, and #calc-ieee). + // NaN is infectious in every math function, so these must stay NaN: + 'log(1, NaN)', + 'log(NaN, 1)', + 'exp(NaN)', + 'sqrt(NaN)', + 'round(NaN, infinity)', + 'mod(NaN, infinity)', + 'rem(NaN, infinity)', + 'atan2(NaN, 1)', + 'atan2(1, NaN)', + 'acos(NaN)', + 'atan(NaN)', + 'min(NaN, 1)', + 'max(1, NaN)', + 'clamp(0, NaN, 1)', + + // pow() special cases + 'pow(-infinity, -3)', + 'pow(-infinity, -2)', + 'pow(-0, -1)', + 'pow(-0, 2)', + 'pow(0, -1)', + 'pow(-2, infinity)', + 'pow(-1, infinity)', + 'pow(0.5, infinity)', + 'pow(1, infinity)', + 'pow(2, infinity)', + 'pow(-2, -infinity)', + 'pow(0.5, -infinity)', + 'pow(2, -infinity)', + + // mod()/rem() with infinite steps + 'mod(5, infinity)', + 'mod(-5, infinity)', + 'mod(0, -infinity)', + 'mod(-0, -infinity)', + 'mod(5, -infinity)', + 'mod(-5, -infinity)', + 'rem(-5, infinity)', + 'rem(0, -infinity)', + 'rem(-0, -infinity)', + + // atan2() special-value table + 'atan2(-infinity, -infinity)', + 'atan2(-infinity, -1)', + 'atan2(-1, -infinity)', + 'atan2(-0, -infinity)', + 'atan2(-0, 0)', + 'atan2(0, -0)', + 'atan2(1, -0)', + 'atan2(-infinity, infinity)', + + // round() with infinite steps + 'round(nearest, -1, infinity)', + 'round(nearest, 1, infinity)', + 'round(up, -1, infinity)', + 'round(up, 1, infinity)', + 'round(down, -1, infinity)', + 'round(down, 1, infinity)', + 'round(to-zero, -1, infinity)', + 'round(to-zero, 1, infinity)', + + // WPT `calc-infinity-nan-computed.html`. + // Infinity/NaN propagate through calc()/min()/max() and are censored per + // context: NaN/±infinity become 0 for /