From 37a596ce568851c734b5f039b23423444581c12c Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Sat, 19 Sep 2026 10:12:46 +0200 Subject: [PATCH] css-calc: infectious NaN --- packages/css-calc/CHANGELOG.md | 1 + packages/css-calc/dist/index.mjs | 2 +- packages/css-calc/src/functions/abs.ts | 5 + packages/css-calc/src/functions/clamp.ts | 17 +- packages/css-calc/src/functions/hypot.ts | 12 +- packages/css-calc/src/functions/max.ts | 11 +- packages/css-calc/src/functions/min.ts | 11 +- packages/css-calc/src/functions/mod.ts | 5 +- packages/css-calc/src/functions/pow.ts | 7 + packages/css-calc/src/functions/random.ts | 8 + packages/css-calc/src/functions/rem.ts | 5 +- packages/css-calc/src/functions/round.ts | 11 +- packages/css-calc/src/functions/sign.ts | 5 + packages/css-calc/src/util/is-none.ts | 4 +- packages/css-calc/test/additional/random.mjs | 65 +++ .../test/wpt/hypot-pow-sqrt-computed.mjs | 46 ++ packages/css-calc/test/wpt/index.mjs | 1 + packages/css-calc/test/wpt/infectious-nan.mjs | 532 ++++++++++++++++++ .../test/wpt/round-mod-rem-computed.mjs | 21 + 19 files changed, 748 insertions(+), 21 deletions(-) create mode 100644 packages/css-calc/test/wpt/infectious-nan.mjs diff --git a/packages/css-calc/CHANGELOG.md b/packages/css-calc/CHANGELOG.md index db1718b834..f873ea2743 100644 --- a/packages/css-calc/CHANGELOG.md +++ b/packages/css-calc/CHANGELOG.md @@ -2,6 +2,7 @@ ### Unreleased (patch) +- 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) ### 3.4.0 diff --git a/packages/css-calc/dist/index.mjs b/packages/css-calc/dist/index.mjs index 225f2f19fc..238f40f200 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,isWhitespaceNode as r,isCommentNode as a,isWhiteSpaceOrCommentNode as u,isSimpleBlockNode as o,isFunctionNode as i,FunctionNode as l,WhitespaceNode as c,parseCommaSeparatedListOfComponentValues as s,walk as v}from"@csstools/css-parser-algorithms";import{isTokenDimension as f,TokenType as p,NumberType as m,mutateUnit as d,isTokenNumber as C,isTokenPercentage as g,isTokenIdent as D,isTokenNumeric as h,isTokenOpenParen as N,isTokenDelim as B,isTokenComma as A,isToken as b,tokenizer as F,tokenize as w,stringify as E,isTokenColon as S,isTokenSemicolon as y}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 I={UnexpectedAdditionOfDimensionOrPercentageWithNumber:"Unexpected addition of a dimension or percentage with a number.",UnexpectedSubtractionOfDimensionOrPercentageWithNumber:"Unexpected subtraction of a dimension or percentage with a number."},x=/[A-Z]/g;function toLowerCaseAZ(e){return e.replace(x,e=>String.fromCharCode(e.charCodeAt(0)+32))}const M={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"},T=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]]),P=new Map([["deg",e=>e],["grad",e=>e/.9],["rad",e=>e/180*Math.PI],["turn",e=>e/360]]),k=new Map([["deg",e=>.9*e],["grad",e=>e],["rad",e=>.9*e/180*Math.PI],["turn",e=>.9*e/360]]),W=new Map([["hz",e=>e],["khz",e=>e/1e3]]),O=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]]),U=new Map([["hz",e=>1e3*e],["khz",e=>e]]),L=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]]),$=new Map([["ms",e=>e],["s",e=>e/1e3]]),V=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]]),Z=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]]),z=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]]),q=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]]),G=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]]),R=new Map([["ms",e=>1e3*e],["s",e=>e]]),j=new Map([["deg",e=>360*e],["grad",e=>360*e/.9],["rad",e=>360*e/180*Math.PI],["turn",e=>e]]),Y=new Map([["cm",T],["mm",L],["q",q],["in",O],["pc",V],["pt",Z],["px",z],["ms",$],["s",R],["deg",P],["grad",k],["rad",G],["turn",j],["hz",W],["khz",U]]);function convertUnit(e,n){if(!f(e))return n;if(!f(n))return n;const t=toLowerCaseAZ(e[4].unit),r=toLowerCaseAZ(n[4].unit);if(t===r)return n;const a=Y.get(r);if(!a)return n;const u=a.get(t);if(!u)return n;const o=u(n[4].value),i=[p.Dimension,"",n[2],n[3],{...n[4],signCharacter:o<0?"-":void 0,type:Number.isInteger(o)?m.Integer:m.Number,value:o}];return d(i,e[4].unit),i}function toCanonicalUnit(e){if(!f(e))return e;const n=toLowerCaseAZ(e[4].unit),t=M[n];if(n===t)return e;const r=Y.get(n);if(!r)return e;const a=r.get(t);if(!a)return e;const u=a(e[4].value),o=[p.Dimension,"",e[2],e[3],{...e[4],signCharacter:u<0?"-":void 0,type:Number.isInteger(u)?m.Integer:m.Number,value:u}];return d(o,t),o}function addition(e,t){if(2!==e.length)return-1;const r=e[0].value;let a=e[1].value;if(C(r)&&C(a)){const e=r[4].value+a[4].value;return new n([p.Number,e.toString(),r[2],a[3],{value:e,type:r[4].type===m.Integer&&a[4].type===m.Integer?m.Integer:m.Number}])}if(g(r)&&g(a)){const e=r[4].value+a[4].value;return new n([p.Percentage,e.toString()+"%",r[2],a[3],{value:e}])}if(f(r)&&f(a)&&(a=convertUnit(r,a),toLowerCaseAZ(r[4].unit)===toLowerCaseAZ(a[4].unit))){const e=r[4].value+a[4].value;return new n([p.Dimension,e.toString()+r[4].unit,r[2],a[3],{value:e,type:r[4].type===m.Integer&&a[4].type===m.Integer?m.Integer:m.Number,unit:r[4].unit}])}return(C(r)&&(f(a)||g(a))||C(a)&&(f(r)||g(r)))&&t.onParseError?.(new ParseErrorWithComponentValues(I.UnexpectedAdditionOfDimensionOrPercentageWithNumber,e)),-1}function division(e){if(2!==e.length)return-1;const t=e[0].value,r=e[1].value;if(C(t)&&C(r)){const e=t[4].value/r[4].value;return new n([p.Number,e.toString(),t[2],r[3],{value:e,type:Number.isInteger(e)?m.Integer:m.Number}])}if(g(t)&&C(r)){const e=t[4].value/r[4].value;return new n([p.Percentage,e.toString()+"%",t[2],r[3],{value:e}])}if(f(t)&&C(r)){const e=t[4].value/r[4].value;return new n([p.Dimension,e.toString()+t[4].unit,t[2],r[3],{value:e,type:Number.isInteger(e)?m.Integer:m.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;const o=u.map(e=>e[4].value),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(!h(a))return-1;if(!r.rawPercentages&&g(a))return-1;const u=n.map(e=>convertUnit(a,e.value));if(!arrayOfSameNumeric(u))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(!h(a))return-1;if(!r.rawPercentages&&g(a))return-1;const u=n.map(e=>convertUnit(a,e.value));if(!arrayOfSameNumeric(u))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(!h(r))return-1;const a=convertUnit(r,t.value);if(!twoOfSameNumeric(r,a))return-1;let u;return u=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(!C(r))return-1;if(!twoOfSameNumeric(r,a))return-1;return numberToCalculation(e,Math.pow(r[4].value,a[4].value))}function solveRem(e,n,t){const r=n.value;if(!h(r))return-1;const a=convertUnit(r,t.value);if(!twoOfSameNumeric(r,a))return-1;let u;return u=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(!f(n))return-1;const r=t.devicePixelLength??1,a=[p.Dimension,`${r}px`,n[2],n[3],{value:r,type:m.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=Math.floor(u[4].value/o[4].value)*o[4].value;break;case"up":i=Math.ceil(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&&u[4].value>=0&&(0===r||0===t)?0!==r?r:t:l===c?r:l0?1/0:-1/0:Math.tan(u),numberToCalculation(e,u)}function subtraction(e,t){if(2!==e.length)return-1;const r=e[0].value;let a=e[1].value;if(C(r)&&C(a)){const e=r[4].value-a[4].value;return new n([p.Number,e.toString(),r[2],a[3],{value:e,type:r[4].type===m.Integer&&a[4].type===m.Integer?m.Integer:m.Number}])}if(g(r)&&g(a)){const e=r[4].value-a[4].value;return new n([p.Percentage,e.toString()+"%",r[2],a[3],{value:e}])}if(f(r)&&f(a)&&(a=convertUnit(r,a),toLowerCaseAZ(r[4].unit)===toLowerCaseAZ(a[4].unit))){const e=r[4].value-a[4].value;return new n([p.Dimension,e.toString()+r[4].unit,r[2],a[3],{value:e,type:r[4].type===m.Integer&&a[4].type===m.Integer?m.Integer:m.Number,unit:r[4].unit}])}return(C(r)&&(f(a)||g(a))||C(a)&&(f(r)||g(r)))&&t.onParseError?.(new ParseErrorWithComponentValues(I.UnexpectedSubtractionOfDimensionOrPercentageWithNumber,e)),-1}function solveLog(e,n){if(1===n.length){const r=n[0];if(!r||!t(r))return-1;const a=r.value;if(!C(a))return-1;return numberToCalculation(e,Math.log(a[4].value))}if(2===n.length){const r=n[0];if(!r||!t(r))return-1;const a=r.value;if(!C(a))return-1;const u=n[1];if(!u||!t(u))return-1;const o=u.value;if(!C(o))return-1;return numberToCalculation(e,Math.log(a[4].value)/Math.log(o[4].value))}return-1}const _=/^none$/i;function isNone(e){if(Array.isArray(e)){const n=e.filter(e=>!(r(e)&&a(e)));return 1===n.length&&isNone(n[0])}if(!t(e))return!1;const n=e.value;return!!D(n)&&_.test(n[4].value)}const H=String.fromCodePoint(0);function solveRandom(e,n,t,r,a,u){if(-1===n.fixed&&!u.randomCaching)return-1;u.randomCaching||(u.randomCaching={propertyName:"",propertyN:0,elementID:"",documentID:""}),u.randomCaching&&!u.randomCaching.propertyN&&(u.randomCaching.propertyN=0);const o=t.value;if(!h(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.isFinite(o[4].value))return resultToCalculation(e,o,Number.NaN);if(!Number.isFinite(i[4].value))return resultToCalculation(e,o,Number.NaN);if(!Number.isFinite(i[4].value-o[4].value))return resultToCalculation(e,o,Number.NaN);if(l&&!Number.isFinite(l[4].value))return resultToCalculation(e,o,o[4].value);const c=-1===n.fixed?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(H))):()=>n.fixed;let s=o[4].value,v=i[4].value;if(s>v&&([s,v]=[v,s]),l&&(l[4].value<=0||Math.abs(s-v)/l[4].value>1e10)&&(l=null),l){const n=Math.max(l[4].value/1e3,1e-9),t=[s];let r=0;for(;;){r+=l[4].value;const e=s+r;if(!(e+nv)break}const a=c();return resultToCalculation(e,o,Number(t[Math.floor(t.length*a)].toFixed(5)))}const f=c();return resultToCalculation(e,o,Number((f*(v-s)+s).toFixed(5)))}function sfc32(e=.34944106645296036,n=.19228640875738723,t=.8784393832007205,r=.04850964319275053){return()=>{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(!h(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 J=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(r,a,o){const i=resolveGlobalsAndConstants([...r.value.filter(e=>!u(e))],a),c=[],s=[],v=[];{let e=c;for(let n=0;n!u(e)),n,t);if(-1===r)return-1;const[a,o]=r,i=variadicArguments(e,o,n,t);if(-1===i)return-1;const[l,c,s]=i;if(!l||!c)return-1;return solveRandom(e,a,l,c,s,t)}],["rem",function rem(e,n,t){return twoCommaSeparatedNodesSolver(e,n,t,solveRem)}],["round",function round(e,r,a){const o=resolveGlobalsAndConstants([...e.value.filter(e=>!u(e))],r);let i="",l=!1;const c=[],s=[];{let e=c;for(let n=0;n!u(e))],n);if(1===a.length&&t(a[0]))return{inputs:[a[0]],operation:unary};let l=0;for(;l!u(e))],n),a=solve(calc$1(calcWrapper(e,r),n,t),t);return-1===a?-1:a}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,r){const a=resolveGlobalsAndConstants([...e.value.filter(e=>!u(e))],n),o=[],i=[];{let e=o;for(let n=0;n!u(e))],r),i=[];{const n=[];let u=[];for(let e=0;e!u(e))],r),i=[];{const n=[];let l=[];for(let e=0;e=0;i--){if(u(n[o][i]))continue;if(l=solve(calc$1(calcWrapper(e,n[o].slice(0,i+1)),r,a),a),-1===l)continue;const s=n[o].slice(i+1).filter(e=>!u(e));if(s.length)if(1===s.length&&t(s[0])&&g(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),r,a),a);if(-1===n)return-1;if(!g(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 K=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)||!D(i.value))return[u,n];for(let i=0;i1)return-1;u.fixed=Math.max(0,Math.min(l.value[4].value,1-1e-9));continue}if("auto"!==c){if(!c.startsWith("--"))return-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 l([p.Function,"calc(",e.name[2],e.name[3],{value:"calc"}],[p.CloseParen,")",e.endToken[2],e.endToken[3],void 0],n)}function maxWrapper(t,r,a){return new l([p.Function,"max(",t.name[2],t.name[3],{value:"max"}],[p.CloseParen,")",t.endToken[2],t.endToken[3],void 0],[r,new n([p.Comma,",",...e(r),void 0]),a])}function patchNaN(e){if(-1===e)return-1;if(i(e))return e;const t=e.value;return h(t)&&Number.isNaN(t[4].value)?C(t)?new l([p.Function,"calc(",t[2],t[3],{value:"calc"}],[p.CloseParen,")",t[2],t[3],void 0],[new n([p.Ident,"NaN",t[2],t[3],{value:"NaN"}])]):f(t)?new l([p.Function,"calc(",t[2],t[3],{value:"calc"}],[p.CloseParen,")",t[2],t[3],void 0],[new n([p.Ident,"NaN",t[2],t[3],{value:"NaN"}]),new c([[p.Whitespace," ",t[2],t[3],void 0]]),new n([p.Delim,"*",t[2],t[3],{value:"*"}]),new c([[p.Whitespace," ",t[2],t[3],void 0]]),new n([p.Dimension,"1"+t[4].unit,t[2],t[3],{value:1,type:m.Integer,unit:t[4].unit}])]):g(t)?new l([p.Function,"calc(",t[2],t[3],{value:"calc"}],[p.CloseParen,")",t[2],t[3],void 0],[new n([p.Ident,"NaN",t[2],t[3],{value:"NaN"}]),new c([[p.Whitespace," ",t[2],t[3],void 0]]),new n([p.Delim,"*",t[2],t[3],{value:"*"}]),new c([[p.Whitespace," ",t[2],t[3],void 0]]),new n([p.Percentage,"1%",t[2],t[3],{value:1}])]):-1:e}function patchInfinity(e){if(-1===e)return-1;if(i(e))return e;const t=e.value;if(!h(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="-"),C(t)?new l([p.Function,"calc(",t[2],t[3],{value:"calc"}],[p.CloseParen,")",t[2],t[3],void 0],[new n([p.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}])]):f(t)?new l([p.Function,"calc(",t[2],t[3],{value:"calc"}],[p.CloseParen,")",t[2],t[3],void 0],[new n([p.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}]),new c([[p.Whitespace," ",t[2],t[3],void 0]]),new n([p.Delim,"*",t[2],t[3],{value:"*"}]),new c([[p.Whitespace," ",t[2],t[3],void 0]]),new n([p.Dimension,"1"+t[4].unit,t[2],t[3],{value:1,type:m.Integer,unit:t[4].unit}])]):new l([p.Function,"calc(",t[2],t[3],{value:"calc"}],[p.CloseParen,")",t[2],t[3],void 0],[new n([p.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}]),new c([[p.Whitespace," ",t[2],t[3],void 0]]),new n([p.Delim,"*",t[2],t[3],{value:"*"}]),new c([[p.Whitespace," ",t[2],t[3],void 0]]),new n([p.Percentage,"1%",t[2],t[3],{value:1}])])}function patchMinusZero(e){if(-1===e)return-1;if(i(e))return e;const n=e.value;return h(n)&&Object.is(-0,n[4].value)?("-0"===n[1]||(g(n)?n[1]="-0%":f(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(i(e))return e;const t=e.value;if(!h(t))return e;if(Number.isInteger(t[4].value))return e;const r=Number(t[4].value.toFixed(n)).toString();return C(t)?t[1]=r:g(t)?t[1]=r+"%":f(t)&&(t[1]=r+t[4].unit),e}function patchCanonicalUnit(e){return-1===e?-1:i(e)?e:f(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=F({css:r}),a=e.nextToken();if(e.nextToken(),!e.endOfFile())continue;if(!h(a))continue;n.set(t,a);continue}return n}function calc(e,n){return calcFromComponentValues(s(w({css:e}),{}),n).map(e=>e.map(e=>E(...e.tokens())).join("")).join(",")}function calcFromComponentValues(e,n){const t=tokenizeGlobals(n?.globals);return replaceComponentValues(e,e=>{if(!i(e))return;const r=J.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,r){for(let a=0;a{if("number"!=typeof a)return;const o=r(n.node);if(!o)return;const i=[o],l=n.parent.value[a-1];t(l)&&B(l.value)&&("-"===l.value[4].value||"+"===l.value[4].value)&&i.splice(0,0,new c([[p.Whitespace," ",...e(n.node),void 0]]));const s=n.parent.value[a+1];!s||u(s)||t(s)&&(A(s.value)||S(s.value)||y(s.value)||B(s.value)&&"-"!==s.value[4].value&&"+"!==s.value[4].value)||i.push(new c([[p.Whitespace," ",...e(n.node),void 0]])),n.parent.value.splice(a,1,...i)})}return n}const Q=new Set(J.keys());export{ParseError,I as ParseErrorMessage,ParseErrorWithComponentValues,calc,calcFromComponentValues,Q as mathFunctionNames}; +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 D,isTokenComma as h,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."},y=/[A-Z]/g;function toLowerCaseAZ(e){return e.replace(y,e=>String.fromCharCode(e.charCodeAt(0)+32))}const I={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"},x=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]]),M=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",x],["mm",O],["q",Z],["in",k],["pc",L],["pt",$],["px",V],["ms",U],["s",q],["deg",M],["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=I[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),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=Math.floor(u[4].value/o[4].value)*o[4].value;break;case"up":i=Math.ceil(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&&u[4].value>=0&&(0===r||0===t)?0!==r?r:t:l===c?r:l0?1/0:-1/0:Math.tan(u),numberToCalculation(e,u)}function subtraction(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.UnexpectedSubtractionOfDimensionOrPercentageWithNumber,e)),-1}function solveLog(e,n){if(1===n.length){const r=n[0];if(!r||!t(r))return-1;const a=r.value;if(!m(a))return-1;return numberToCalculation(e,Math.log(a[4].value))}if(2===n.length){const r=n[0];if(!r||!t(r))return-1;const a=r.value;if(!m(a))return-1;const u=n[1];if(!u||!t(u))return-1;const o=u.value;if(!m(o))return-1;return numberToCalculation(e,Math.log(a[4].value)/Math.log(o[4].value))}return-1}const j=/^none$/i;function isNone(e){if(Array.isArray(e)){const n=e.filter(e=>!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;u.randomCaching||(u.randomCaching={propertyName:"",propertyN:0,elementID:"",documentID:""}),u.randomCaching&&!u.randomCaching.propertyN&&(u.randomCaching.propertyN=0);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,Number.NaN);if(!Number.isFinite(i[4].value))return resultToCalculation(e,o,Number.NaN);if(!Number.isFinite(i[4].value-o[4].value))return resultToCalculation(e,o,Number.NaN);if(l&&Number.isNaN(l[4].value))return resultToCalculation(e,o,Number.NaN);if(l&&!Number.isFinite(l[4].value))return resultToCalculation(e,o,o[4].value);const c=-1===n.fixed?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))):()=>n.fixed;let s=o[4].value,v=i[4].value;if(s>v&&([s,v]=[v,s]),l&&(l[4].value<=0||Math.abs(s-v)/l[4].value>1e10)&&(l=null),l){const n=Math.max(l[4].value/1e3,1e-9),t=[s];let r=0;for(;;){r+=l[4].value;const e=s+r;if(!(e+nv)break}const a=c();return resultToCalculation(e,o,Number(t[Math.floor(t.length*a)].toFixed(5)))}const f=c();return resultToCalculation(e,o,Number((f*(v-s)+s).toFixed(5)))}function sfc32(e=.34944106645296036,n=.19228640875738723,t=.8784393832007205,r=.04850964319275053){return()=>{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-1e-9));continue}if("auto"!==c){if(!c.startsWith("--"))return-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)&&D(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)&&(h(s.value)||w(s.value)||E(s.value)||D(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/src/functions/abs.ts b/packages/css-calc/src/functions/abs.ts index 00f4db5c15..f38a339c7f 100644 --- a/packages/css-calc/src/functions/abs.ts +++ b/packages/css-calc/src/functions/abs.ts @@ -10,6 +10,11 @@ export function solveAbs(absNode: FunctionNode, a: TokenNode, options: conversio return -1; } + // NaN is infectious, forcing the function to return NaN if any argument calculation is NaN. + if (Number.isNaN(aToken[4].value)) { + return resultToCalculation(absNode, aToken, Number.NaN); + } + if (!options.rawPercentages && isTokenPercentage(aToken)) { return -1; } diff --git a/packages/css-calc/src/functions/clamp.ts b/packages/css-calc/src/functions/clamp.ts index f7a1fbceff..9c4e21774a 100644 --- a/packages/css-calc/src/functions/clamp.ts +++ b/packages/css-calc/src/functions/clamp.ts @@ -21,10 +21,6 @@ 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; @@ -35,6 +31,19 @@ export function solveClamp(clampNode: FunctionNode, minimum: TokenNode | -1, cen return -1; } + // NaN is infectious, forcing the function to return NaN if any argument calculation is NaN. + if ( + Number.isNaN(minimumToken[4].value) || + Number.isNaN(centralToken[4].value) || + Number.isNaN(maximumToken[4].value) + ) { + return resultToCalculation(clampNode, minimumToken, Number.NaN); + } + + if (!options.rawPercentages && isTokenPercentage(minimumToken)) { + return -1; + } + const result = Math.max(minimumToken[4].value, Math.min(centralToken[4].value, maximumToken[4].value)); return resultToCalculation(clampNode, minimumToken, result); diff --git a/packages/css-calc/src/functions/hypot.ts b/packages/css-calc/src/functions/hypot.ts index f83b6c45d8..9b04a58af0 100644 --- a/packages/css-calc/src/functions/hypot.ts +++ b/packages/css-calc/src/functions/hypot.ts @@ -19,12 +19,18 @@ export function solveHypot(hypotNode: FunctionNode, solvedNodes: Array convertUnit(firstSolvedToken, x.value)); + if (!arrayOfSameNumeric(tokens)) { return -1; } - const tokens = solvedNodes.map((x) => convertUnit(firstSolvedToken, x.value)); - if (!arrayOfSameNumeric(tokens)) { + // https://drafts.csswg.org/css-values-4/#exponent-ranges + // 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); + } + + if (!options.rawPercentages && isTokenPercentage(firstSolvedToken)) { return -1; } diff --git a/packages/css-calc/src/functions/max.ts b/packages/css-calc/src/functions/max.ts index 9893a21ac6..ed146f3bc1 100644 --- a/packages/css-calc/src/functions/max.ts +++ b/packages/css-calc/src/functions/max.ts @@ -19,12 +19,17 @@ export function solveMax(maxNode: FunctionNode, solvedNodes: Array convertUnit(firstSolvedToken, x.value)); + if (!arrayOfSameNumeric(tokens)) { return -1; } - const tokens = solvedNodes.map((x) => convertUnit(firstSolvedToken, x.value)); - if (!arrayOfSameNumeric(tokens)) { + // 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(maxNode, firstSolvedToken, Number.NaN); + } + + if (!options.rawPercentages && isTokenPercentage(firstSolvedToken)) { return -1; } diff --git a/packages/css-calc/src/functions/min.ts b/packages/css-calc/src/functions/min.ts index 24f21f94b6..30d646c18e 100644 --- a/packages/css-calc/src/functions/min.ts +++ b/packages/css-calc/src/functions/min.ts @@ -19,12 +19,17 @@ export function solveMin(minNode: FunctionNode, solvedNodes: Array convertUnit(firstSolvedToken, x.value)); + if (!arrayOfSameNumeric(tokens)) { return -1; } - const tokens = solvedNodes.map((x) => convertUnit(firstSolvedToken, x.value)); - if (!arrayOfSameNumeric(tokens)) { + // 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(minNode, firstSolvedToken, Number.NaN); + } + + if (!options.rawPercentages && isTokenPercentage(firstSolvedToken)) { return -1; } diff --git a/packages/css-calc/src/functions/mod.ts b/packages/css-calc/src/functions/mod.ts index 41bd512942..0f543d3524 100644 --- a/packages/css-calc/src/functions/mod.ts +++ b/packages/css-calc/src/functions/mod.ts @@ -17,7 +17,10 @@ export function solveMod(modNode: FunctionNode, a: TokenNode, b: TokenNode): Cal } let result; - if (bToken[4].value === 0) { + if (Number.isNaN(aToken[4].value) || Number.isNaN(bToken[4].value)) { + // NaN is infectious, forcing the function to return NaN if any argument calculation is NaN. + result = Number.NaN; + } else if (bToken[4].value === 0) { result = Number.NaN; } else if (!Number.isFinite(aToken[4].value)) { result = Number.NaN; diff --git a/packages/css-calc/src/functions/pow.ts b/packages/css-calc/src/functions/pow.ts index e8dbfd08d7..8de054ac25 100644 --- a/packages/css-calc/src/functions/pow.ts +++ b/packages/css-calc/src/functions/pow.ts @@ -15,6 +15,13 @@ export function solvePow(powNode: FunctionNode, a: TokenNode, b: TokenNode): Cal return -1; } + // https://drafts.csswg.org/css-values-4/#exponent-infinities + // NaN is infectious, forcing the function to return NaN if any argument calculation is NaN. + // Note: `Math.pow(NaN, 0)` returns `1` in JS, but CSS requires NaN. + if (Number.isNaN(aToken[4].value) || Number.isNaN(bToken[4].value)) { + return numberToCalculation(powNode, Number.NaN); + } + const result = Math.pow(aToken[4].value, bToken[4].value); return numberToCalculation(powNode, result); diff --git a/packages/css-calc/src/functions/random.ts b/packages/css-calc/src/functions/random.ts index 10f2ba59dd..ed4b87e860 100644 --- a/packages/css-calc/src/functions/random.ts +++ b/packages/css-calc/src/functions/random.ts @@ -53,6 +53,10 @@ export function solveRandom(randomNode: FunctionNode, randomValueSharing: Random } } + if (Number.isNaN(aToken[4].value) || Number.isNaN(bToken[4].value)) { + return resultToCalculation(randomNode, aToken, Number.NaN); + } + if (!Number.isFinite(aToken[4].value)) { return resultToCalculation(randomNode, aToken, Number.NaN); } @@ -65,6 +69,10 @@ export function solveRandom(randomNode: FunctionNode, randomValueSharing: Random return resultToCalculation(randomNode, aToken, Number.NaN); } + if (stepValueToken && Number.isNaN(stepValueToken[4].value)) { + return resultToCalculation(randomNode, aToken, Number.NaN); + } + if (stepValueToken && !Number.isFinite(stepValueToken[4].value)) { return resultToCalculation(randomNode, aToken, aToken[4].value); } diff --git a/packages/css-calc/src/functions/rem.ts b/packages/css-calc/src/functions/rem.ts index 4467beb4e2..8228837ab5 100644 --- a/packages/css-calc/src/functions/rem.ts +++ b/packages/css-calc/src/functions/rem.ts @@ -17,7 +17,10 @@ export function solveRem(remNode: FunctionNode, a: TokenNode, b: TokenNode): Cal } let result; - if (bToken[4].value === 0) { + if (Number.isNaN(aToken[4].value) || Number.isNaN(bToken[4].value)) { + // NaN is infectious, forcing the function to return NaN if any argument calculation is NaN. + result = Number.NaN; + } else if (bToken[4].value === 0) { result = Number.NaN; } else if (!Number.isFinite(aToken[4].value)) { result = Number.NaN; diff --git a/packages/css-calc/src/functions/round.ts b/packages/css-calc/src/functions/round.ts index 4c6eda48e3..2938e67e29 100644 --- a/packages/css-calc/src/functions/round.ts +++ b/packages/css-calc/src/functions/round.ts @@ -17,12 +17,17 @@ export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a: return -1; } - if (!options.rawPercentages && isTokenPercentage(aToken)) { + const bToken = convertUnit(aToken, b.value); + if (!twoOfSameNumeric(aToken, bToken)) { return -1; } - const bToken = convertUnit(aToken, b.value); - if (!twoOfSameNumeric(aToken, bToken)) { + // NaN is infectious, forcing the function to return NaN if any argument calculation is NaN. + if (Number.isNaN(aToken[4].value) || Number.isNaN(bToken[4].value)) { + return resultToCalculation(roundNode, aToken, Number.NaN); + } + + if (!options.rawPercentages && isTokenPercentage(aToken)) { return -1; } diff --git a/packages/css-calc/src/functions/sign.ts b/packages/css-calc/src/functions/sign.ts index d65b08c735..46934baac6 100644 --- a/packages/css-calc/src/functions/sign.ts +++ b/packages/css-calc/src/functions/sign.ts @@ -10,6 +10,11 @@ export function solveSign(signNode: FunctionNode, a: TokenNode, options: convers return -1; } + // NaN is infectious, forcing the function to return NaN if any argument calculation is NaN. + if (Number.isNaN(aToken[4].value)) { + return numberToCalculation(signNode, Number.NaN); + } + if (!options.rawPercentages && isTokenPercentage(aToken)) { return -1; } diff --git a/packages/css-calc/src/util/is-none.ts b/packages/css-calc/src/util/is-none.ts index ac74f5401c..2ce00fd4fd 100644 --- a/packages/css-calc/src/util/is-none.ts +++ b/packages/css-calc/src/util/is-none.ts @@ -1,12 +1,12 @@ import type { ComponentValue } from '@csstools/css-parser-algorithms'; -import { isCommentNode, isTokenNode, isWhitespaceNode } from '@csstools/css-parser-algorithms'; +import { isTokenNode, isWhiteSpaceOrCommentNode } from '@csstools/css-parser-algorithms'; import { isTokenIdent } from '@csstools/css-tokenizer'; const IS_NONE_REGEX = /^none$/i; export function isNone(v: Array | ComponentValue): boolean { if (Array.isArray(v)) { - const relevantNodes = v.filter((x) => !(isWhitespaceNode(x) && isCommentNode(x))); + const relevantNodes = v.filter((x) => !isWhiteSpaceOrCommentNode(x)); if (relevantNodes.length === 1) { return isNone(relevantNodes[0]); } diff --git a/packages/css-calc/test/additional/random.mjs b/packages/css-calc/test/additional/random.mjs index b7f89f2a6b..1dbd8cd051 100644 --- a/packages/css-calc/test/additional/random.mjs +++ b/packages/css-calc/test/additional/random.mjs @@ -440,3 +440,68 @@ for (let i = 0; i < 100; i++) { assert.ok(result <= 190); assert.ok(result === 100 || result === 130 || result === 160 || result === 190); } + +// NaN is infectious: any NaN argument forces random() to return NaN. +// https://drafts.csswg.org/css-values-5/#random-infinities +{ + assert.strictEqual( + calc('random(100, NaN)', { randomCaching: { documentID: 'a', elementID: 'b', propertyName: 'c', propertyN: 0 } }), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('random(calc(NaN), 100)', { randomCaching: { documentID: 'a', elementID: 'b', propertyName: 'c', propertyN: 0 } }), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('random(calc(NaN * 1px), 100px)', { randomCaching: { documentID: 'a', elementID: 'b', propertyName: 'c', propertyN: 0 } }), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('random(100px, calc(NaN * 1px))', { randomCaching: { documentID: 'a', elementID: 'b', propertyName: 'c', propertyN: 0 } }), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('random(0%, calc(NaN * 1%))', { randomCaching: { documentID: 'a', elementID: 'b', propertyName: 'c', propertyN: 0 } }), + 'calc(NaN * 1%)', + ); + + assert.strictEqual( + calc('random(100px, 500px, calc(NaN * 1px))', { randomCaching: { documentID: 'a', elementID: 'b', propertyName: 'c', propertyN: 0 } }), + 'calc(NaN * 1px)', + ); + + // NaN in any of the three arguments, including the optional step. + assert.strictEqual( + calc('random(fixed 0.5, NaN, 100)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('random(fixed 0.5, 10, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('random(fixed 0.5, NaN * 1px, 100px)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('random(fixed 0.5, 10px, NaN * 1px, 5px)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('random(fixed 0.5, NaN, 100, 5)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('random(fixed 0.5, NaN * 1%, 100%)'), + 'calc(NaN * 1%)', + ); +} diff --git a/packages/css-calc/test/wpt/hypot-pow-sqrt-computed.mjs b/packages/css-calc/test/wpt/hypot-pow-sqrt-computed.mjs index d251c4426b..9e305bd169 100644 --- a/packages/css-calc/test/wpt/hypot-pow-sqrt-computed.mjs +++ b/packages/css-calc/test/wpt/hypot-pow-sqrt-computed.mjs @@ -190,3 +190,49 @@ assert.strictEqual( calc('hypot(1turn)'), '1turn', ); + +assert.strictEqual( + calc('hypot(NaN, 5)'), + 'calc(NaN)', +); + +assert.strictEqual( + calc('hypot(infinity, NaN)'), + 'calc(NaN)', +); + +assert.strictEqual( + calc('hypot(NaN, infinity)'), + 'calc(NaN)', +); + +assert.strictEqual( + calc('hypot(1, NaN)'), + 'calc(NaN)', +); + +assert.strictEqual( + calc('hypot(infinity, 5)'), + 'calc(infinity)', +); + +assert.strictEqual( + calc('hypot(5, infinity)'), + 'calc(infinity)', +); + +// NaN is infectious, even for `pow(NaN, 0)` where `Math.pow` would return 1. +assert.strictEqual( + calc('pow(NaN, 0)'), + 'calc(NaN)', +); + +assert.strictEqual( + calc('pow(1, NaN)'), + 'calc(NaN)', +); + +assert.strictEqual( + calc('pow(NaN, NaN)'), + 'calc(NaN)', +); diff --git a/packages/css-calc/test/wpt/index.mjs b/packages/css-calc/test/wpt/index.mjs index 5fd5ddd705..8005127950 100644 --- a/packages/css-calc/test/wpt/index.mjs +++ b/packages/css-calc/test/wpt/index.mjs @@ -15,6 +15,7 @@ import './clamp-length-computed.mjs'; import './clamp-length-invalid.mjs'; import './exp-log-compute.mjs'; import './hypot-pow-sqrt-computed.mjs'; +import './infectious-nan.mjs'; import './invalid.mjs'; import './max-20-arguments.mjs'; import './minmax-angle-computed.mjs'; diff --git a/packages/css-calc/test/wpt/infectious-nan.mjs b/packages/css-calc/test/wpt/infectious-nan.mjs new file mode 100644 index 0000000000..2b1f2702a5 --- /dev/null +++ b/packages/css-calc/test/wpt/infectious-nan.mjs @@ -0,0 +1,532 @@ +import { calc } from '@csstools/css-calc'; +import assert from 'node:assert'; + +// NaN is infectious: any operation or math function with a NaN argument produces NaN. +// https://drafts.csswg.org/css-values-4/#calc-ieee + +// calc() arithmetic +{ + assert.strictEqual( + calc('calc(NaN + 2)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('calc(2 + NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('calc(NaN - 2)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('calc(2 - NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('calc(NaN * 2)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('calc(2 * NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('calc(NaN / 2)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('calc(2 / NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('calc(NaN * 1px)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('calc(1px * NaN)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('calc(NaN * 1%)'), + 'calc(NaN * 1%)', + ); + + assert.strictEqual( + calc('calc(10.135262721212548pc - 199pt / NaN)'), + 'calc(NaN * 1pc)', + ); +} + +// min() / max() / clamp() +{ + assert.strictEqual( + calc('min(NaN, 1)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('min(1, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('max(NaN, 1)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('max(1, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('clamp(NaN, 1, 2)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('clamp(1, NaN, 2)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('clamp(1, 2, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('min(1px, NaN * 1px)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('max(NaN * 1px, 15px)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('clamp(1px, NaN * 1px, 2px)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('max(NaN * 1%, 50%)'), + 'calc(NaN * 1%)', + ); + + assert.strictEqual( + calc('clamp(NaN * 1%, 1%, 2%)'), + 'calc(NaN * 1%)', + ); + + assert.strictEqual( + calc('clamp(1%, NaN * 1%, 2%)'), + 'calc(NaN * 1%)', + ); + + assert.strictEqual( + calc('clamp(1%, 2%, NaN * 1%)'), + 'calc(NaN * 1%)', + ); + + // NaN is infectious even when mixed with infinities. + // https://drafts.csswg.org/css-values-4/#exponent-infinities + assert.strictEqual( + calc('max(infinity, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('max(NaN, -infinity)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('min(infinity, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('min(NaN, -infinity)'), + 'calc(NaN)', + ); + + // NaN propagates through nested math functions. + assert.strictEqual( + calc('min(max(NaN, 1), 2)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('max(min(NaN, 1), 2)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('clamp(max(NaN, 1), 2, 3)'), + 'calc(NaN)', + ); +} + +// round() / mod() / rem() +{ + assert.strictEqual( + calc('round(NaN, 5)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('round(5, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('round(up, 5, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('round(down, NaN, 5)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('round(5px, NaN * 1px)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('round(NaN * 1px, 5px)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('round(line-width, NaN * 1px)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('mod(NaN, 5)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('mod(5, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('rem(NaN, 5)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('rem(5, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('mod(5px, NaN * 1px)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('rem(5px, NaN * 1px)'), + 'calc(NaN * 1px)', + ); + + // NaN is infectious even when the arguments are percentages. + assert.strictEqual( + calc('round(NaN * 1%, 5%)'), + 'calc(NaN * 1%)', + ); + + assert.strictEqual( + calc('round(5%, NaN * 1%)'), + 'calc(NaN * 1%)', + ); + + assert.strictEqual( + calc('mod(NaN * 1%, 5%)'), + 'calc(NaN * 1%)', + ); + + assert.strictEqual( + calc('rem(5%, NaN * 1%)'), + 'calc(NaN * 1%)', + ); + + // NaN is infectious even when it would otherwise be masked by the + // "B is 0" / "both infinite" rules in https://drafts.csswg.org/css-values-4/#round-infinities + assert.strictEqual( + calc('round(NaN, 0)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('round(down, NaN, 0)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('mod(NaN, 0)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('rem(NaN, 0)'), + 'calc(NaN)', + ); +} + +// abs() / sign() +{ + assert.strictEqual( + calc('abs(NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('sign(NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('abs(NaN * 1px)'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('sign(NaN * 1px)'), + 'calc(NaN)', + ); + + // NaN is infectious even when the argument is a percentage. + assert.strictEqual( + calc('abs(NaN * 1%)'), + 'calc(NaN * 1%)', + ); + + assert.strictEqual( + calc('sign(NaN * 1%)'), + 'calc(NaN)', + ); + + // NaN is infectious even when produced by a nested calculation. + assert.strictEqual( + calc('abs(calc(1px * NaN))'), + 'calc(NaN * 1px)', + ); + + assert.strictEqual( + calc('sign(calc(NaN + 2))'), + 'calc(NaN)', + ); +} + +// trigonometric functions +{ + assert.strictEqual( + calc('sin(NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('cos(NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('tan(NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('asin(NaN)'), + 'calc(NaN * 1rad)', + ); + + assert.strictEqual( + calc('acos(NaN)'), + 'calc(NaN * 1rad)', + ); + + assert.strictEqual( + calc('atan(NaN)'), + 'calc(NaN * 1rad)', + ); + + assert.strictEqual( + calc('atan2(NaN, 1)'), + 'calc(NaN * 1rad)', + ); + + assert.strictEqual( + calc('atan2(1, NaN)'), + 'calc(NaN * 1rad)', + ); +} + +// exponential functions +{ + assert.strictEqual( + calc('pow(NaN, 0)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('pow(0, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('sqrt(NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('exp(NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('hypot(NaN, 1)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('hypot(1, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('log(NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('log(NaN, 2)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('log(2, NaN)'), + 'calc(NaN)', + ); + + // NaN is infectious even when the arguments are percentages. + assert.strictEqual( + calc('hypot(NaN * 1%, 50%)'), + 'calc(NaN * 1%)', + ); + + assert.strictEqual( + calc('hypot(50%, NaN * 1%)'), + 'calc(NaN * 1%)', + ); + + // hypot() with any infinite input is +infinity, unless NaN is present. + // https://drafts.csswg.org/css-values-4/#exponent-infinities + assert.strictEqual( + calc('hypot(-infinity, 5)'), + 'calc(infinity)', + ); + + assert.strictEqual( + calc('hypot(5, -infinity)'), + 'calc(infinity)', + ); + + assert.strictEqual( + calc('hypot(-infinity, -infinity)'), + 'calc(infinity)', + ); + + assert.strictEqual( + calc('hypot(-infinity, NaN)'), + 'calc(NaN)', + ); + + // pow() with NaN in either argument is NaN, overriding the + // "B is 0 -> 1" / infinite base rules. + assert.strictEqual( + calc('pow(0, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('pow(infinity, NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('pow(-infinity, NaN)'), + 'calc(NaN)', + ); +} + +// calc-mix() +{ + assert.strictEqual( + calc('calc-mix(NaN)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('calc-mix(NaN 50%, 1 50%)'), + 'calc(NaN)', + ); + + assert.strictEqual( + calc('calc-mix(1 50%, NaN 50%)'), + 'calc(NaN)', + ); +} + +// Percentages that are not NaN must remain unsolved (conservative static analysis). +{ + assert.strictEqual( + calc('abs(50%)'), + 'abs(50%)', + ); + + assert.strictEqual( + calc('sign(50%)'), + 'sign(50%)', + ); + + assert.strictEqual( + calc('round(50%, 10%)'), + 'round(50%, 10%)', + ); + + assert.strictEqual( + calc('min(50%, 100%)'), + 'min(50%, 100%)', + ); + + assert.strictEqual( + calc('max(50%, 100%)'), + 'max(50%, 100%)', + ); + + assert.strictEqual( + calc('hypot(50%, 30%)'), + 'hypot(50%, 30%)', + ); + + assert.strictEqual( + calc('clamp(0%, 50%, 100%)'), + 'clamp(0%, 50%, 100%)', + ); +} diff --git a/packages/css-calc/test/wpt/round-mod-rem-computed.mjs b/packages/css-calc/test/wpt/round-mod-rem-computed.mjs index 7420acb0b9..cf4a63f2c1 100644 --- a/packages/css-calc/test/wpt/round-mod-rem-computed.mjs +++ b/packages/css-calc/test/wpt/round-mod-rem-computed.mjs @@ -570,3 +570,24 @@ assert.strictEqual( calc('rem(-18vw,5vw)'), '-3vw', ); + +// NaN is infectious. +assert.strictEqual( + calc('mod(5, NaN)'), + 'calc(NaN)', +); + +assert.strictEqual( + calc('mod(NaN, 5)'), + 'calc(NaN)', +); + +assert.strictEqual( + calc('rem(5, NaN)'), + 'calc(NaN)', +); + +assert.strictEqual( + calc('rem(NaN, 5)'), + 'calc(NaN)', +);