From a9048b724cb996ea6f60a8812abe46de4199997d Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Sun, 12 Apr 2026 17:35:33 +0200 Subject: [PATCH] css-calc: line-width --- packages/css-calc/CHANGELOG.md | 5 + packages/css-calc/dist/index.d.ts | 5 + packages/css-calc/dist/index.mjs | 2 +- packages/css-calc/docs/css-calc.api.json | 2 +- .../docs/css-calc.conversionoptions.md | 1 + packages/css-calc/src/functions/calc.ts | 22 +++- packages/css-calc/src/functions/round.ts | 34 ++++- packages/css-calc/src/options.ts | 6 + .../css-calc/src/util/snap-to-border-width.ts | 46 +++++++ packages/css-calc/test/additional/index.mjs | 1 + .../test/additional/round-line-width.mjs | 117 ++++++++++++++++++ 11 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 packages/css-calc/src/util/snap-to-border-width.ts create mode 100644 packages/css-calc/test/additional/round-line-width.mjs diff --git a/packages/css-calc/CHANGELOG.md b/packages/css-calc/CHANGELOG.md index 2f25989315..0f41e3e71c 100644 --- a/packages/css-calc/CHANGELOG.md +++ b/packages/css-calc/CHANGELOG.md @@ -1,5 +1,10 @@ # Changes to CSS Calc +### Unreleased (minor) + +- Add support for `round(line-width, 1.2345px)` +- Add `devicePixelLength` option + ### 3.1.1 _February 13, 2026_ diff --git a/packages/css-calc/dist/index.d.ts b/packages/css-calc/dist/index.d.ts index 28b8481509..2c41cf823c 100644 --- a/packages/css-calc/dist/index.d.ts +++ b/packages/css-calc/dist/index.d.ts @@ -26,6 +26,11 @@ export declare type conversionOptions = { * You can set it to a lower number to suite your needs. */ precision?: number; + /** + * The CSS pixel length of one device pixel. + * Used when rounding to `line-width` and similar features + */ + devicePixelLength?: number; /** * By default this package will try to preserve units. * The heuristic to do this is very simplistic. diff --git a/packages/css-calc/dist/index.mjs b/packages/css-calc/dist/index.mjs index ff08b55b72..ca4cb0450c 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 m,NumberType as p,mutateUnit as C,isTokenNumber as d,isTokenPercentage as g,isTokenIdent as D,isTokenNumeric as N,isTokenOpenParen as h,isTokenDelim as B,isTokenComma as A,isToken as b,tokenizer as F,tokenize as w,stringify as E,isTokenColon as S,isTokenSemicolon as I}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 y={UnexpectedAdditionOfDimensionOrPercentageWithNumber:"Unexpected addition of a dimension or percentage with a number.",UnexpectedSubtractionOfDimensionOrPercentageWithNumber:"Unexpected subtraction of a dimension or percentage with a number."},M=/[A-Z]/g;function toLowerCaseAZ(e){return e.replace(M,e=>String.fromCharCode(e.charCodeAt(0)+32))}const T={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]]),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]]),O=new Map([["hz",e=>e],["khz",e=>e/1e3]]),W=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]]),V=new Map([["ms",e=>e],["s",e=>e/1e3]]),$=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",x],["mm",L],["q",q],["in",W],["pc",$],["pt",Z],["px",z],["ms",V],["s",R],["deg",P],["grad",k],["rad",G],["turn",j],["hz",O],["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=[m.Dimension,"",n[2],n[3],{...n[4],signCharacter:o<0?"-":void 0,type:Number.isInteger(o)?p.Integer:p.Number,value:o}];return C(i,e[4].unit),i}function toCanonicalUnit(e){if(!f(e))return e;const n=toLowerCaseAZ(e[4].unit),t=T[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=[m.Dimension,"",e[2],e[3],{...e[4],signCharacter:u<0?"-":void 0,type:Number.isInteger(u)?p.Integer:p.Number,value:u}];return C(o,t),o}function addition(e,t){if(2!==e.length)return-1;const r=e[0].value;let a=e[1].value;if(d(r)&&d(a)){const e=r[4].value+a[4].value;return new n([m.Number,e.toString(),r[2],a[3],{value:e,type:r[4].type===p.Integer&&a[4].type===p.Integer?p.Integer:p.Number}])}if(g(r)&&g(a)){const e=r[4].value+a[4].value;return new n([m.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([m.Dimension,e.toString()+r[4].unit,r[2],a[3],{value:e,type:r[4].type===p.Integer&&a[4].type===p.Integer?p.Integer:p.Number,unit:r[4].unit}])}return(d(r)&&(f(a)||g(a))||d(a)&&(f(r)||g(r)))&&t.onParseError?.(new ParseErrorWithComponentValues(y.UnexpectedAdditionOfDimensionOrPercentageWithNumber,e)),-1}function division(e){if(2!==e.length)return-1;const t=e[0].value,r=e[1].value;if(d(t)&&d(r)){const e=t[4].value/r[4].value;return new n([m.Number,e.toString(),t[2],r[3],{value:e,type:Number.isInteger(e)?p.Integer:p.Number}])}if(g(t)&&d(r)){const e=t[4].value/r[4].value;return new n([m.Percentage,e.toString()+"%",t[2],r[3],{value:e}])}if(f(t)&&d(r)){const e=t[4].value/r[4].value;return new n([m.Dimension,e.toString()+t[4].unit,t[2],r[3],{value:e,type:Number.isInteger(e)?p.Integer:p.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(!N(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(!N(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(!N(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(!d(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(!N(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 solveRound(e,n,t,r,a){const u=t.value;if(!N(u))return-1;if(!a.rawPercentages&&g(u))return-1;const o=convertUnit(u,r.value);if(!twoOfSameNumeric(u,o))return-1;let i;if(0===o[4].value)i=Number.NaN;else if(Number.isFinite(u[4].value)||Number.isFinite(o[4].value))if(!Number.isFinite(u[4].value)&&Number.isFinite(o[4].value))i=u[4].value;else if(Number.isFinite(u[4].value)&&!Number.isFinite(o[4].value))switch(n){case"down":i=u[4].value<0?-1/0:Object.is(-0,0*u[4].value)?-0:0;break;case"up":i=u[4].value>0?1/0:Object.is(0,0*u[4].value)?0:-0;break;default:i=Object.is(0,0*u[4].value)?0:-0}else if(Number.isFinite(o[4].value))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 e=Math.floor(u[4].value/o[4].value)*o[4].value,n=Math.ceil(u[4].value/o[4].value)*o[4].value;if(e>n){const t=e;e=n,n=t}const t=Math.abs(u[4].value-e),r=Math.abs(u[4].value-n);i=t===r?n:t0?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(d(r)&&d(a)){const e=r[4].value-a[4].value;return new n([m.Number,e.toString(),r[2],a[3],{value:e,type:r[4].type===p.Integer&&a[4].type===p.Integer?p.Integer:p.Number}])}if(g(r)&&g(a)){const e=r[4].value-a[4].value;return new n([m.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([m.Dimension,e.toString()+r[4].unit,r[2],a[3],{value:e,type:r[4].type===p.Integer&&a[4].type===p.Integer?p.Integer:p.Number,unit:r[4].unit}])}return(d(r)&&(f(a)||g(a))||d(a)&&(f(r)||g(r)))&&t.onParseError?.(new ParseErrorWithComponentValues(y.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(!d(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(!d(a))return-1;const u=n[1];if(!u||!t(u))return-1;const o=u.value;if(!d(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(!N(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:`${u.randomCaching?.propertyName} ${u.randomCaching.propertyN++}`,n.elementShared?"":u.randomCaching.elementID,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}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],["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;e1)return-1;u.fixed=Math.max(0,Math.min(i.value[4].value,1-1e-9));continue}if("auto"!==l)if(l.startsWith("--")){if(-1!==u.fixed||u.isAuto)return-1;u.dashedIdent=l}else;else{if(-1!==u.fixed||u.dashedIdent)return-1;u.isAuto=!0}}else{if(-1!==u.fixed)return-1;u.elementShared=!0}}return-1}function calcWrapper(e,n){return new l([m.Function,"calc(",e.name[2],e.name[3],{value:"calc"}],[m.CloseParen,")",e.endToken[2],e.endToken[3],void 0],n)}function maxWrapper(t,r,a){return new l([m.Function,"max(",t.name[2],t.name[3],{value:"max"}],[m.CloseParen,")",t.endToken[2],t.endToken[3],void 0],[r,new n([m.Comma,",",...e(r),void 0]),a])}function patchNaN(e){if(-1===e)return-1;if(i(e))return e;const t=e.value;return N(t)&&Number.isNaN(t[4].value)?d(t)?new l([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,"NaN",t[2],t[3],{value:"NaN"}])]):f(t)?new l([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,"NaN",t[2],t[3],{value:"NaN"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Delim,"*",t[2],t[3],{value:"*"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Dimension,"1"+t[4].unit,t[2],t[3],{value:1,type:p.Integer,unit:t[4].unit}])]):g(t)?new l([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,"NaN",t[2],t[3],{value:"NaN"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Delim,"*",t[2],t[3],{value:"*"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.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(!N(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="-"),d(t)?new l([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}])]):f(t)?new l([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Delim,"*",t[2],t[3],{value:"*"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Dimension,"1"+t[4].unit,t[2],t[3],{value:1,type:p.Integer,unit:t[4].unit}])]):new l([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Delim,"*",t[2],t[3],{value:"*"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.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 N(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(!N(t))return e;if(Number.isInteger(t[4].value))return e;const r=Number(t[4].value.toFixed(n)).toString();return d(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(!N(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?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([[m.Whitespace," ",...e(n.node),void 0]]));const s=n.parent.value[a+1];!s||u(s)||t(s)&&(A(s.value)||S(s.value)||I(s.value)||B(s.value)&&"-"!==s.value[4].value&&"+"!==s.value[4].value)||i.push(new c([[m.Whitespace," ",...e(n.node),void 0]])),n.parent.value.splice(a,1,...i)})}return n}const Q=new Set(J.keys());export{ParseError,y as ParseErrorMessage,ParseErrorWithComponentValues,calc,calcFromComponentValues,Q as mathFunctionNames}; +import{sourceIndices as e,TokenNode as n,isTokenNode as t,isWhitespaceNode as r,isCommentNode as a,isWhiteSpaceOrCommentNode as u,isSimpleBlockNode as i,isFunctionNode as o,FunctionNode as l,WhitespaceNode as c,parseCommaSeparatedListOfComponentValues as s,walk as v}from"@csstools/css-parser-algorithms";import{isTokenDimension as f,TokenType as m,NumberType as p,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 I,isTokenSemicolon as S}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 y={UnexpectedAdditionOfDimensionOrPercentageWithNumber:"Unexpected addition of a dimension or percentage with a number.",UnexpectedSubtractionOfDimensionOrPercentageWithNumber:"Unexpected subtraction of a dimension or percentage with a number."},M=/[A-Z]/g;function toLowerCaseAZ(e){return e.replace(M,e=>String.fromCharCode(e.charCodeAt(0)+32))}const x={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 i=u(n[4].value),o=[m.Dimension,"",n[2],n[3],{...n[4],signCharacter:i<0?"-":void 0,type:Number.isInteger(i)?p.Integer:p.Number,value:i}];return d(o,e[4].unit),o}function toCanonicalUnit(e){if(!f(e))return e;const n=toLowerCaseAZ(e[4].unit),t=x[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),i=[m.Dimension,"",e[2],e[3],{...e[4],signCharacter:u<0?"-":void 0,type:Number.isInteger(u)?p.Integer:p.Number,value:u}];return d(i,t),i}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([m.Number,e.toString(),r[2],a[3],{value:e,type:r[4].type===p.Integer&&a[4].type===p.Integer?p.Integer:p.Number}])}if(g(r)&&g(a)){const e=r[4].value+a[4].value;return new n([m.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([m.Dimension,e.toString()+r[4].unit,r[2],a[3],{value:e,type:r[4].type===p.Integer&&a[4].type===p.Integer?p.Integer:p.Number,unit:r[4].unit}])}return(C(r)&&(f(a)||g(a))||C(a)&&(f(r)||g(r)))&&t.onParseError?.(new ParseErrorWithComponentValues(y.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([m.Number,e.toString(),t[2],r[3],{value:e,type:Number.isInteger(e)?p.Integer:p.Number}])}if(g(t)&&C(r)){const e=t[4].value/r[4].value;return new n([m.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([m.Dimension,e.toString()+t[4].unit,t[2],r[3],{value:e,type:Number.isInteger(e)?p.Integer:p.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 i=u.map(e=>e[4].value),o=Math.hypot(...i);return resultToCalculation(e,a,o)}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 i=u.map(e=>e[4].value),o=Math.max(...i);return resultToCalculation(e,a,o)}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 i=u.map(e=>e[4].value),o=Math.min(...i);return resultToCalculation(e,a,o)}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=[m.Dimension,`${r}px`,n[2],n[3],{value:r,type:p.Integer,unit:"px"}],u=convertUnit(a,n);if(!twoOfSameNumeric(u,a))return-1;if(u[4].value<0)return-1;if(Number.isInteger(u[4].value/r))return resultToCalculation(e,n,n[4].value);if(u[4].value>0&&u[4].valuer){const t=Math.floor(u[4].value/a[4].value)*a[4].value;return u[4].value=t,resultToCalculation(e,n,convertUnit(n,u)[4].value)}return resultToCalculation(e,n,n[4].value)}function solveRound(e,n,t,r,a){const u=t.value;if(!h(u))return-1;if("line-width"===n&&!f(u))return-1;if(!a.rawPercentages&&g(u))return-1;const i=convertUnit(u,r.value);if(!twoOfSameNumeric(u,i))return-1;let o;if(0===i[4].value)o=Number.NaN;else if(Number.isFinite(u[4].value)||Number.isFinite(i[4].value))if(!Number.isFinite(u[4].value)&&Number.isFinite(i[4].value))o=u[4].value;else if(Number.isFinite(u[4].value)&&!Number.isFinite(i[4].value))switch(n){case"down":o=u[4].value<0?-1/0:Object.is(-0,0*u[4].value)?-0:0;break;case"up":o=u[4].value>0?1/0:Object.is(0,0*u[4].value)?0:-0;break;default:o=Object.is(0,0*u[4].value)?0:-0}else switch(n){case"down":o=Math.floor(u[4].value/i[4].value)*i[4].value;break;case"up":o=Math.ceil(u[4].value/i[4].value)*i[4].value;break;case"to-zero":o=Math.trunc(u[4].value/i[4].value)*i[4].value;break;default:{let t=Math.floor(u[4].value/i[4].value)*i[4].value,r=Math.ceil(u[4].value/i[4].value)*i[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(o="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([m.Number,e.toString(),r[2],a[3],{value:e,type:r[4].type===p.Integer&&a[4].type===p.Integer?p.Integer:p.Number}])}if(g(r)&&g(a)){const e=r[4].value-a[4].value;return new n([m.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([m.Dimension,e.toString()+r[4].unit,r[2],a[3],{value:e,type:r[4].type===p.Integer&&a[4].type===p.Integer?p.Integer:p.Number,unit:r[4].unit}])}return(C(r)&&(f(a)||g(a))||C(a)&&(f(r)||g(r)))&&t.onParseError?.(new ParseErrorWithComponentValues(y.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 i=u.value;if(!C(i))return-1;return numberToCalculation(e,Math.log(a[4].value)/Math.log(i[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 i=t.value;if(!h(i))return-1;const o=convertUnit(i,r.value);if(!twoOfSameNumeric(i,o))return-1;let l=null;if(a&&(l=convertUnit(i,a.value),!twoOfSameNumeric(i,l)))return-1;if(!Number.isFinite(i[4].value))return resultToCalculation(e,i,Number.NaN);if(!Number.isFinite(o[4].value))return resultToCalculation(e,i,Number.NaN);if(!Number.isFinite(o[4].value-i[4].value))return resultToCalculation(e,i,Number.NaN);if(l&&!Number.isFinite(l[4].value))return resultToCalculation(e,i,i[4].value);const c=-1===n.fixed?sfc32(crc32([n.dashedIdent?n.dashedIdent:`${u.randomCaching?.propertyName} ${u.randomCaching.propertyN++}`,n.elementShared?"":u.randomCaching.elementID,u.randomCaching.documentID].join(H))):()=>n.fixed;let s=i[4].value,v=o[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,i,Number(t[Math.floor(t.length*a)].toFixed(5)))}const f=c();return resultToCalculation(e,i,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}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],["clamp",function clamp(r,a,i){const o=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,i]=r,o=variadicArguments(e,i,n,t);if(-1===o)return-1;const[l,c,s]=o;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 i=resolveGlobalsAndConstants([...e.value.filter(e=>!u(e))],r);let o="",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,i]=a;return r(e,u,i,t)}function twoCommaSeparatedArguments(e,n,r){const a=resolveGlobalsAndConstants([...e.value.filter(e=>!u(e))],n),i=[],o=[];{let e=i;for(let n=0;n!u(e))],r),o=[];{const n=[];let u=[];for(let e=0;e1)return-1;u.fixed=Math.max(0,Math.min(o.value[4].value,1-1e-9));continue}if("auto"!==l)if(l.startsWith("--")){if(-1!==u.fixed||u.isAuto)return-1;u.dashedIdent=l}else;else{if(-1!==u.fixed||u.dashedIdent)return-1;u.isAuto=!0}}else{if(-1!==u.fixed)return-1;u.elementShared=!0}}return-1}function calcWrapper(e,n){return new l([m.Function,"calc(",e.name[2],e.name[3],{value:"calc"}],[m.CloseParen,")",e.endToken[2],e.endToken[3],void 0],n)}function maxWrapper(t,r,a){return new l([m.Function,"max(",t.name[2],t.name[3],{value:"max"}],[m.CloseParen,")",t.endToken[2],t.endToken[3],void 0],[r,new n([m.Comma,",",...e(r),void 0]),a])}function patchNaN(e){if(-1===e)return-1;if(o(e))return e;const t=e.value;return h(t)&&Number.isNaN(t[4].value)?C(t)?new l([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,"NaN",t[2],t[3],{value:"NaN"}])]):f(t)?new l([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,"NaN",t[2],t[3],{value:"NaN"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Delim,"*",t[2],t[3],{value:"*"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Dimension,"1"+t[4].unit,t[2],t[3],{value:1,type:p.Integer,unit:t[4].unit}])]):g(t)?new l([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,"NaN",t[2],t[3],{value:"NaN"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Delim,"*",t[2],t[3],{value:"*"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Percentage,"1%",t[2],t[3],{value:1}])]):-1:e}function patchInfinity(e){if(-1===e)return-1;if(o(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([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}])]):f(t)?new l([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Delim,"*",t[2],t[3],{value:"*"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Dimension,"1"+t[4].unit,t[2],t[3],{value:1,type:p.Integer,unit:t[4].unit}])]):new l([m.Function,"calc(",t[2],t[3],{value:"calc"}],[m.CloseParen,")",t[2],t[3],void 0],[new n([m.Ident,r+"infinity",t[2],t[3],{value:r+"infinity"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Delim,"*",t[2],t[3],{value:"*"}]),new c([[m.Whitespace," ",t[2],t[3],void 0]]),new n([m.Percentage,"1%",t[2],t[3],{value:1}])])}function patchMinusZero(e){if(-1===e)return-1;if(o(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(o(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:o(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(!o(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?a:void 0})}function replaceComponentValues(n,r){for(let a=0;a{if("number"!=typeof a)return;const i=r(n.node);if(!i)return;const o=[i],l=n.parent.value[a-1];t(l)&&B(l.value)&&("-"===l.value[4].value||"+"===l.value[4].value)&&o.splice(0,0,new c([[m.Whitespace," ",...e(n.node),void 0]]));const s=n.parent.value[a+1];!s||u(s)||t(s)&&(A(s.value)||I(s.value)||S(s.value)||B(s.value)&&"-"!==s.value[4].value&&"+"!==s.value[4].value)||o.push(new c([[m.Whitespace," ",...e(n.node),void 0]])),n.parent.value.splice(a,1,...o)})}return n}const Q=new Set(J.keys());export{ParseError,y as ParseErrorMessage,ParseErrorWithComponentValues,calc,calcFromComponentValues,Q as mathFunctionNames}; diff --git a/packages/css-calc/docs/css-calc.api.json b/packages/css-calc/docs/css-calc.api.json index bed7fb51d7..7d402c56ce 100644 --- a/packages/css-calc/docs/css-calc.api.json +++ b/packages/css-calc/docs/css-calc.api.json @@ -386,7 +386,7 @@ }, { "kind": "Content", - "text": ";\n precision?: number;\n toCanonicalUnits?: boolean;\n censorIntoStandardRepresentableValues?: boolean;\n rawPercentages?: boolean;\n randomCaching?: {\n propertyName: string;\n propertyN: number;\n elementID: string;\n documentID: string;\n };\n}" + "text": ";\n precision?: number;\n devicePixelLength?: number;\n toCanonicalUnits?: boolean;\n censorIntoStandardRepresentableValues?: boolean;\n rawPercentages?: boolean;\n randomCaching?: {\n propertyName: string;\n propertyN: number;\n elementID: string;\n documentID: string;\n };\n}" }, { "kind": "Content", diff --git a/packages/css-calc/docs/css-calc.conversionoptions.md b/packages/css-calc/docs/css-calc.conversionoptions.md index 3ba189613b..649dae8fe0 100644 --- a/packages/css-calc/docs/css-calc.conversionoptions.md +++ b/packages/css-calc/docs/css-calc.conversionoptions.md @@ -11,6 +11,7 @@ export type conversionOptions = { onParseError?: (error: ParseError) => void; globals?: GlobalsWithStrings; precision?: number; + devicePixelLength?: number; toCanonicalUnits?: boolean; censorIntoStandardRepresentableValues?: boolean; rawPercentages?: boolean; diff --git a/packages/css-calc/src/functions/calc.ts b/packages/css-calc/src/functions/calc.ts index fa937836b9..256db27026 100644 --- a/packages/css-calc/src/functions/calc.ts +++ b/packages/css-calc/src/functions/calc.ts @@ -1,7 +1,8 @@ import type { Calculation } from '../calculation'; import type { ComponentValue, SimpleBlockNode } from '@csstools/css-parser-algorithms'; import type { Globals } from '../util/globals'; -import { TokenType, NumberType, isTokenOpenParen, isTokenDelim, isTokenComma, isTokenIdent, isTokenNumber } from '@csstools/css-tokenizer'; +import type { CSSToken, TokenDimension } from '@csstools/css-tokenizer'; +import { TokenType, NumberType, isTokenOpenParen, isTokenDelim, isTokenComma, isTokenIdent, isTokenNumber, isTokenDimension } from '@csstools/css-tokenizer'; import { addition } from '../operation/addition'; import { division } from '../operation/division'; import { isCalculation, solve } from '../calculation'; @@ -34,6 +35,8 @@ import { isNone } from '../util/is-none'; import type { conversionOptions } from '../options'; import type { RandomValueSharing} from './random'; import { solveRandom } from './random'; +import { snapAsBorderWidth } from '../util/snap-to-border-width'; +import { convertUnit } from '../unit-conversions'; type mathFunction = (node: FunctionNode, globals: Globals, options: conversionOptions) => Calculation | -1; @@ -435,6 +438,7 @@ function min(minNode: FunctionNode, globals: Globals, options: conversionOptions const roundingStrategies = new Set([ 'nearest', + 'line-width', 'up', 'down', 'to-zero', @@ -492,7 +496,23 @@ function round(roundNode: FunctionNode, globals: Globals, options: conversionOpt return -1; } + if (roundingStrategy === 'line-width') { + const dummyPx: CSSToken = [TokenType.Dimension, '1px', a.value[2], a.value[3], { value: 1, type: NumberType.Integer, unit: 'px' }]; + const asPx = convertUnit(dummyPx, a.value); + if (!isTokenDimension(asPx) || asPx[4].unit !== 'px') { + return -1; + } + } + if (!hasComma && bValue.length === 0) { + if (roundingStrategy === 'line-width') { + if ((a.value as TokenDimension)[4].value <= 0) { + return -1; + } + + return snapAsBorderWidth(roundNode, a.value, options); + } + bValue.push( new TokenNode( [TokenType.Number, '1', a.value[2], a.value[3], { value: 1, type: NumberType.Integer }], diff --git a/packages/css-calc/src/functions/round.ts b/packages/css-calc/src/functions/round.ts index 996de1769c..8a36448a57 100644 --- a/packages/css-calc/src/functions/round.ts +++ b/packages/css-calc/src/functions/round.ts @@ -1,10 +1,11 @@ -import type { Calculation } from '../calculation'; +import { solve, type Calculation } from '../calculation'; import type { FunctionNode, TokenNode } from '@csstools/css-parser-algorithms'; import { convertUnit } from '../unit-conversions'; import { resultToCalculation } from './result-to-calculation'; import { twoOfSameNumeric } from '../util/kind-of-number'; -import { isTokenNumeric, isTokenPercentage } from '@csstools/css-tokenizer'; +import { isTokenDimension, isTokenNumeric, isTokenPercentage } from '@csstools/css-tokenizer'; import type { conversionOptions } from '../options'; +import { snapAsBorderWidth } from '../util/snap-to-border-width'; export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a: TokenNode, b: TokenNode, options: conversionOptions): Calculation | -1 { const aToken = a.value; @@ -12,6 +13,10 @@ export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a: return -1; } + if (roundingStrategy === 'line-width' && !isTokenDimension(aToken)) { + return -1; + } + if (!options.rawPercentages && isTokenPercentage(aToken)) { return -1; } @@ -22,14 +27,20 @@ export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a: } let result; + // https://drafts.csswg.org/css-values-4/#round-infinities if (bToken[4].value === 0) { + // In round(A, B), if B is 0, the result is NaN. result = Number.NaN; } else if (!Number.isFinite(aToken[4].value) && !Number.isFinite(bToken[4].value)) { + // If A and B are both infinite, the result is NaN. result = Number.NaN; } else if (!Number.isFinite(aToken[4].value) && Number.isFinite(bToken[4].value)) { + // If A is infinite but B is finite, the result is the same infinity. result = aToken[4].value; } else if (Number.isFinite(aToken[4].value) && !Number.isFinite(bToken[4].value)) { + // If A is finite but B is infinite, the result depends on the and the sign of A: switch (roundingStrategy) { + // If A is negative (not zero), return −∞. If A is 0⁻, return 0⁻. Otherwise, return 0⁺. case 'down': if (aToken[4].value < 0) { result = -Infinity; @@ -40,6 +51,7 @@ export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a: } break; case 'up': + // If A is positive (not zero), return +∞. If A is 0⁺, return 0⁺. Otherwise, return 0⁻. if (aToken[4].value > 0) { result = +Infinity; } else if (Object.is(+0, aToken[4].value * 0)) { @@ -50,7 +62,9 @@ export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a: break; case 'to-zero': case 'nearest': + case 'line-width': default: { + // If A is positive or 0⁺, return 0⁺. Otherwise, return 0⁻. if (Object.is(+0, aToken[4].value * 0)) { result = +0; } else { @@ -58,8 +72,6 @@ export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a: } } } - } else if (!Number.isFinite(bToken[4].value)) { - result = aToken[4].value; } else { switch (roundingStrategy) { case 'down': @@ -72,6 +84,7 @@ export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a: result = Math.trunc(aToken[4].value / bToken[4].value) * bToken[4].value; break; case 'nearest': + case 'line-width': default: { let down = Math.floor(aToken[4].value / bToken[4].value) * bToken[4].value; let up = Math.ceil(aToken[4].value / bToken[4].value) * bToken[4].value; @@ -84,7 +97,9 @@ export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a: const downDiff = Math.abs(aToken[4].value - down); const upDiff = Math.abs(aToken[4].value - up); - if (downDiff === upDiff) { + if (roundingStrategy === 'line-width' && aToken[4].value > 0 && (up === 0 || down === 0)) { + result = up !== 0 ? up : down; + } else if (downDiff === upDiff) { result = up; } else if (downDiff < upDiff) { result = down; @@ -92,6 +107,15 @@ export function solveRound(roundNode: FunctionNode, roundingStrategy: string, a: result = up; } + if (roundingStrategy === 'line-width') { + const solved = solve(resultToCalculation(roundNode, aToken, result), options); + if (solved === -1) { + return -1; + } + + return snapAsBorderWidth(roundNode, solved.value, options); + } + break; } } diff --git a/packages/css-calc/src/options.ts b/packages/css-calc/src/options.ts index b9afbb4f15..47fbbd1098 100644 --- a/packages/css-calc/src/options.ts +++ b/packages/css-calc/src/options.ts @@ -24,6 +24,12 @@ export type conversionOptions = { */ precision?: number, + /** + * The CSS pixel length of one device pixel. + * Used when rounding to `line-width` and similar features + */ + devicePixelLength?: number, + /** * By default this package will try to preserve units. * The heuristic to do this is very simplistic. diff --git a/packages/css-calc/src/util/snap-to-border-width.ts b/packages/css-calc/src/util/snap-to-border-width.ts new file mode 100644 index 0000000000..22c4c1fa0a --- /dev/null +++ b/packages/css-calc/src/util/snap-to-border-width.ts @@ -0,0 +1,46 @@ +import type { CSSToken, TokenDimension} from "@csstools/css-tokenizer"; +import { isTokenDimension, NumberType, TokenType } from "@csstools/css-tokenizer"; +import type { conversionOptions } from "../options"; +import { convertUnit } from "../unit-conversions"; +import { twoOfSameNumeric } from "./kind-of-number"; +import { resultToCalculation } from "../functions/result-to-calculation"; +import type { FunctionNode } from "@csstools/css-parser-algorithms"; +import type { Calculation } from "../calculation"; + +export function snapAsBorderWidth(fnNode: FunctionNode, aToken: CSSToken, options: conversionOptions): Calculation | -1 { + if (!isTokenDimension(aToken)) { + return -1; + } + + const devicePixelLength = options.devicePixelLength ?? 1; + const devicePixelLengthToken: TokenDimension = [TokenType.Dimension, `${devicePixelLength}px`, aToken[2], aToken[3], { value: devicePixelLength, type: NumberType.Integer, unit: 'px' }]; + + const aTokenAsPixels = convertUnit(devicePixelLengthToken, aToken); + if (!twoOfSameNumeric(aTokenAsPixels, devicePixelLengthToken)) { + return -1; + } + + if (aTokenAsPixels[4].value < 0) { + // Assert: len is non-negative. + return -1; + } + + if (Number.isInteger(aTokenAsPixels[4].value / devicePixelLength)) { + // If len is an integer number of device pixels, do nothing. + return resultToCalculation(fnNode, aToken, aToken[4].value); + } + + if (aTokenAsPixels[4].value > 0 && aTokenAsPixels[4].value < devicePixelLength) { + // If len is greater than zero, but less than 1 device pixel, round len up to 1 device pixel. + return resultToCalculation(fnNode, aToken, convertUnit(aToken, devicePixelLengthToken)[4].value); + } + + if (aTokenAsPixels[4].value > devicePixelLength) { + // If len is greater than 1 device pixel, round it down to the nearest integer number of device pixels. + const down = Math.floor(aTokenAsPixels[4].value / devicePixelLengthToken[4].value) * devicePixelLengthToken[4].value; + aTokenAsPixels[4].value = down; + return resultToCalculation(fnNode, aToken, convertUnit(aToken, aTokenAsPixels)[4].value); + } + + return resultToCalculation(fnNode, aToken, aToken[4].value); +} diff --git a/packages/css-calc/test/additional/index.mjs b/packages/css-calc/test/additional/index.mjs index 3a2a0f033c..05f5b1aaba 100644 --- a/packages/css-calc/test/additional/index.mjs +++ b/packages/css-calc/test/additional/index.mjs @@ -2,3 +2,4 @@ import './mod-rem-infinity.mjs'; import './random.mjs'; import './sign-abs-equivalent.mjs'; import './tan-asymptotes.mjs'; +import './round-line-width.mjs'; diff --git a/packages/css-calc/test/additional/round-line-width.mjs b/packages/css-calc/test/additional/round-line-width.mjs new file mode 100644 index 0000000000..2961717fd8 --- /dev/null +++ b/packages/css-calc/test/additional/round-line-width.mjs @@ -0,0 +1,117 @@ +import { calc } from '@csstools/css-calc'; +import assert from 'node:assert'; + +assert.strictEqual( + calc('round(line-width, 0.001px)'), + '1px', +); + +assert.strictEqual( + calc('round(line-width, 0px)'), + 'round(line-width, 0px)', +); + +assert.strictEqual( + calc('round(line-width, -1px)'), + 'round(line-width, -1px)', +); + +assert.strictEqual( + calc('round(line-width, 0.001px)', { devicePixelLength: 0.5 }), + '0.5px', +); + +assert.strictEqual( + calc('round(line-width, 23.45343px)', { devicePixelLength: 0.5 }), + '23px', +); + +assert.strictEqual( + calc('round(line-width, 23.45343px, 11px)', { devicePixelLength: 0.5 }), + '22px', +); + +assert.strictEqual( + calc('round(line-width, 5px, 1.3px)', { devicePixelLength: 0.5 }), + '5px', +); + +assert.strictEqual( + calc('round(line-width, 5px, 1.6px)', { devicePixelLength: 0.5 }), + '4.5px', +); + +assert.strictEqual( + calc('round(line-width, 5px, 1.7px)', { devicePixelLength: 0.5 }), + '5px', +); + +assert.strictEqual( + calc('round(line-width, 5px, 1.8px)', { devicePixelLength: 0.5 }), + '5px', +); + +assert.strictEqual( + calc('round(line-width, 5px, 1.9px)', { devicePixelLength: 0.5 }), + '5.5px', +); + +assert.strictEqual( + calc('round(line-width, 5px, 2px)', { devicePixelLength: 0.5 }), + '6px', +); + +assert.strictEqual( + calc('round(line-width, 5pt, 2pt)', { devicePixelLength: 0.5 }), + '6pt', +); + +assert.strictEqual( + calc('round(line-width, 5mm, 2mm)', { devicePixelLength: 0.5 }), + '5.953125mm', +); + +assert.strictEqual( + calc('round(line-width, -5px, 2px)', { devicePixelLength: 0.5 }), + 'round(line-width, -5px, 2px)', +); + +assert.strictEqual( + calc('round(line-width, 0.25px)', { devicePixelLength: 0.5 }), + '0.5px', +); + +assert.strictEqual( + calc('round(line-width, 0.99px)', { devicePixelLength: 0.5 }), + '0.5px', +); + +assert.strictEqual( + calc('round(line-width, 0.99px, 0.01px)', { devicePixelLength: 0.5 }), + '0.5px', +); + +assert.strictEqual( + calc('round(line-width, 1.01px)', { devicePixelLength: 0.5 }), + '1px', +); + +assert.strictEqual( + calc('round(line-width, 1.01px, 0.01px)', { devicePixelLength: 0.5 }), + '1px', +); + +assert.strictEqual( + calc('round(line-width, 11)', { devicePixelLength: 0.5 }), + 'round(line-width, 11)', +); + +assert.strictEqual( + calc('round(line-width, 11, 1)', { devicePixelLength: 0.5 }), + 'round(line-width, 11, 1)', +); + +assert.strictEqual( + calc('round(line-width, 11px, 1)', { devicePixelLength: 0.5 }), + 'round(line-width, 11px, 1)', +);