From b7ef22235d9efa2e4a87540196a2a161c1dfda68 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Wed, 30 Sep 2026 19:39:54 +0200 Subject: [PATCH 1/6] improve performance --- packages/css-calc/CHANGELOG.md | 5 ++ packages/css-calc/dist/index.mjs | 2 +- packages/css-calc/src/functions/random.ts | 16 +++- packages/utilities/CHANGELOG.md | 4 + packages/utilities/dist/index.mjs | 2 +- packages/utilities/src/has-fallback-decl.ts | 84 ++++++++++++++++--- .../CHANGELOG.md | 5 ++ .../dist/index.mjs | 2 +- .../src/index.ts | 16 +++- .../CHANGELOG.md | 5 ++ .../dist/index.mjs | 2 +- .../src/index.ts | 27 +++++- plugins/postcss-custom-media/CHANGELOG.md | 5 ++ .../src/cascade-layers.ts | 15 +++- .../postcss-custom-properties/CHANGELOG.md | 6 ++ .../postcss-custom-properties/dist/index.mjs | 2 +- .../src/cascade-layers.ts | 15 +++- .../postcss-custom-properties/src/index.ts | 10 ++- .../src/transform-properties.ts | 33 ++++---- .../src/transform-value-ast.ts | 2 +- plugins/postcss-custom-selectors/CHANGELOG.md | 6 ++ .../postcss-custom-selectors/dist/index.mjs | 2 +- .../src/cascade-layers.ts | 15 +++- .../src/transform-rule.ts | 13 +++ plugins/postcss-design-tokens/CHANGELOG.md | 5 ++ plugins/postcss-design-tokens/dist/index.mjs | 2 +- .../style-dictionary/v3/dereference.ts | 19 +++++ .../CHANGELOG.md | 4 + .../dist/index.mjs | 2 +- .../src/index.ts | 17 +++- plugins/postcss-gap-properties/CHANGELOG.md | 4 + plugins/postcss-gap-properties/dist/index.mjs | 2 +- plugins/postcss-gap-properties/src/index.ts | 39 +++++++-- plugins/postcss-is-pseudo-class/CHANGELOG.md | 5 ++ .../postcss-is-pseudo-class/dist/index.mjs | 2 +- plugins/postcss-is-pseudo-class/src/index.ts | 21 +++-- .../src/split-selectors/split-selectors.ts | 21 ++++- .../CHANGELOG.md | 4 + .../dist/index.mjs | 2 +- .../src/lib/clone-declaration.ts | 55 ++++++++++-- plugins/postcss-logical-overflow/CHANGELOG.md | 4 + .../postcss-logical-overflow/dist/index.mjs | 2 +- .../src/lib/transform-axes.ts | 57 +++++++++++-- .../CHANGELOG.md | 4 + .../dist/index.mjs | 2 +- .../src/lib/transform-axes.ts | 57 +++++++++++-- plugins/postcss-logical-resize/CHANGELOG.md | 4 + plugins/postcss-logical-resize/dist/index.mjs | 2 +- .../src/lib/clone-declaration.ts | 55 ++++++++++-- plugins/postcss-logical/CHANGELOG.md | 4 + plugins/postcss-logical/dist/index.mjs | 2 +- plugins/postcss-logical/src/index.ts | 12 +-- .../src/lib/clone-declaration.ts | 53 ++++++++++-- plugins/postcss-media-minmax/CHANGELOG.md | 4 + plugins/postcss-media-minmax/dist/index.mjs | 2 +- plugins/postcss-media-minmax/src/transform.ts | 25 +++++- plugins/postcss-minify/CHANGELOG.md | 4 + plugins/postcss-minify/dist/index.mjs | 2 +- plugins/postcss-minify/src/index.ts | 18 +++- .../postcss-overflow-shorthand/CHANGELOG.md | 4 + .../postcss-overflow-shorthand/dist/index.mjs | 2 +- .../postcss-overflow-shorthand/src/index.ts | 21 +++-- plugins/postcss-place/CHANGELOG.md | 4 + plugins/postcss-place/dist/index.mjs | 2 +- .../postcss-place/src/on-css-declaration.ts | 21 +++-- .../CHANGELOG.md | 4 + .../dist/index.mjs | 2 +- .../src/index.ts | 7 +- .../CHANGELOG.md | 5 ++ .../dist/index.mjs | 2 +- .../src/index.ts | 26 +++++- plugins/postcss-rewrite-url/CHANGELOG.md | 4 + plugins/postcss-rewrite-url/dist/index.mjs | 2 +- plugins/postcss-rewrite-url/src/index.ts | 9 -- .../CHANGELOG.md | 4 + .../dist/index.mjs | 2 +- .../src/index.ts | 12 ++- .../CHANGELOG.md | 4 + .../dist/index.mjs | 2 +- .../src/index.ts | 25 +++--- 80 files changed, 792 insertions(+), 183 deletions(-) diff --git a/packages/css-calc/CHANGELOG.md b/packages/css-calc/CHANGELOG.md index fdf80960d9..3a5dcaa5dd 100644 --- a/packages/css-calc/CHANGELOG.md +++ b/packages/css-calc/CHANGELOG.md @@ -1,5 +1,10 @@ # Changes to CSS Calc +### Unreleased (patch) + +- Precompute the CRC32 lookup table instead of parsing a hex substring for every character, avoiding quadratic work when hashing `random()`. + + ### 3.4.1 _September 25, 2026_ diff --git a/packages/css-calc/dist/index.mjs b/packages/css-calc/dist/index.mjs index 12e92c8524..c5305a9b2d 100644 --- a/packages/css-calc/dist/index.mjs +++ b/packages/css-calc/dist/index.mjs @@ -1 +1 @@ -import{sourceIndices as e,TokenNode as n,isTokenNode as t,isWhiteSpaceOrCommentNode as r,isFunctionNode as u,isSimpleBlockNode as a,FunctionNode as i,WhitespaceNode as o,parseCommaSeparatedListOfComponentValues as l,walk as c}from"@csstools/css-parser-algorithms";import{isTokenDimension as s,TokenType as v,NumberType as f,mutateUnit as p,isTokenNumber as m,isTokenPercentage as d,isTokenIdent as N,isTokenNumeric as C,isTokenOpenParen as g,isTokenDelim as h,isTokenComma as D,isToken as b,tokenizer as B,tokenize as A,stringify as w,isTokenColon as F,isTokenSemicolon as E}from"@csstools/css-tokenizer";class ParseError extends Error{sourceStart;sourceEnd;constructor(e,n,t){super(e),this.name="ParseError",this.sourceStart=n,this.sourceEnd=t}}class ParseErrorWithComponentValues extends ParseError{componentValues;constructor(n,t){super(n,...e(t)),this.componentValues=t}}const S={UnexpectedAdditionOfDimensionOrPercentageWithNumber:"Unexpected addition of a dimension or percentage with a number.",UnexpectedSubtractionOfDimensionOrPercentageWithNumber:"Unexpected subtraction of a dimension or percentage with a number."},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]]),R=new Map([["deg",e=>360*e],["grad",e=>360*e/.9],["rad",e=>360*e/180*Math.PI],["turn",e=>e]]),G=new Map([["cm",x],["mm",O],["q",Z],["in",k],["pc",L],["pt",$],["px",V],["ms",U],["s",q],["deg",M],["grad",T],["rad",z],["turn",R],["hz",P],["khz",W]]);function convertUnit(e,n){if(!s(e))return n;if(!s(n))return n;const t=toLowerCaseAZ(e[4].unit),r=toLowerCaseAZ(n[4].unit);if(t===r)return n;const u=G.get(r);if(!u)return n;const a=u.get(t);if(!a)return n;const i=a(n[4].value),o=[v.Dimension,"",n[2],n[3],{...n[4],signCharacter:i<0?"-":void 0,type:Number.isInteger(i)?f.Integer:f.Number,value:i}];return p(o,e[4].unit),o}function toCanonicalUnit(e){if(!s(e))return e;const n=toLowerCaseAZ(e[4].unit),t=I[n];if(n===t)return e;const r=G.get(n);if(!r)return e;const u=r.get(t);if(!u)return e;const a=u(e[4].value),i=[v.Dimension,"",e[2],e[3],{...e[4],signCharacter:a<0?"-":void 0,type:Number.isInteger(a)?f.Integer:f.Number,value:a}];return p(i,t),i}function addition(e,t){if(2!==e.length)return-1;const r=e[0].value;let u=e[1].value;if(m(r)&&m(u)){const e=r[4].value+u[4].value;return new n([v.Number,e.toString(),r[2],u[3],{value:e,type:r[4].type===f.Integer&&u[4].type===f.Integer?f.Integer:f.Number}])}if(d(r)&&d(u)){const e=r[4].value+u[4].value;return new n([v.Percentage,e.toString()+"%",r[2],u[3],{value:e}])}if(s(r)&&s(u)&&(u=convertUnit(r,u),toLowerCaseAZ(r[4].unit)===toLowerCaseAZ(u[4].unit))){const e=r[4].value+u[4].value;return new n([v.Dimension,e.toString()+r[4].unit,r[2],u[3],{value:e,type:r[4].type===f.Integer&&u[4].type===f.Integer?f.Integer:f.Number,unit:r[4].unit}])}return(m(r)&&(s(u)||d(u))||m(u)&&(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=[{calculation:e,inputs:[],index:0}];let u=-1;for(;r.length;){const e=r[r.length-1];if(e.indexconvertUnit(u,e.value));if(!arrayOfSameNumeric(a))return-1;if(a.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,u,Number.NaN);if(!r.rawPercentages&&d(u))return-1;const i=a.map(e=>e[4].value);if(i.some(Number.isNaN))return resultToCalculation(e,u,Number.NaN);const o=Math.hypot(...i);return resultToCalculation(e,u,o)}function solveMax(e,n,r){if(!n.every(t))return-1;const u=n[0].value;if(!C(u))return-1;const a=n.map(e=>convertUnit(u,e.value));if(!arrayOfSameNumeric(a))return-1;if(a.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,u,Number.NaN);if(!r.rawPercentages&&d(u))return-1;const i=a.map(e=>e[4].value),o=Math.max(...i);return resultToCalculation(e,u,o)}function solveMin(e,n,r){if(!n.every(t))return-1;const u=n[0].value;if(!C(u))return-1;const a=n.map(e=>convertUnit(u,e.value));if(!arrayOfSameNumeric(a))return-1;if(a.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,u,Number.NaN);if(!r.rawPercentages&&d(u))return-1;const i=a.map(e=>e[4].value),o=Math.min(...i);return resultToCalculation(e,u,o)}function solveMod(e,n,t){const r=n.value;if(!C(r))return-1;const u=convertUnit(r,t.value);if(!twoOfSameNumeric(r,u))return-1;let a;return a=Number.isNaN(r[4].value)||Number.isNaN(u[4].value)||0===u[4].value?Number.NaN:Number.isFinite(r[4].value)&&(Number.isFinite(u[4].value)||(u[4].value!==Number.POSITIVE_INFINITY||r[4].value!==Number.NEGATIVE_INFINITY&&!Object.is(0*r[4].value,-0))&&(u[4].value!==Number.NEGATIVE_INFINITY||r[4].value!==Number.POSITIVE_INFINITY&&!Object.is(0*r[4].value,0)))?Number.isFinite(u[4].value)?(r[4].value%u[4].value+u[4].value)%u[4].value:r[4].value:Number.NaN,resultToCalculation(e,r,a)}function solvePow(e,n,t){const r=n.value,u=t.value;if(!m(r))return-1;if(!twoOfSameNumeric(r,u))return-1;if(Number.isNaN(r[4].value)||Number.isNaN(u[4].value))return numberToCalculation(e,Number.NaN);return numberToCalculation(e,Math.pow(r[4].value,u[4].value))}function solveRem(e,n,t){const r=n.value;if(!C(r))return-1;const u=convertUnit(r,t.value);if(!twoOfSameNumeric(r,u))return-1;let a;return a=Number.isNaN(r[4].value)||Number.isNaN(u[4].value)||0===u[4].value?Number.NaN:Number.isFinite(r[4].value)?Number.isFinite(u[4].value)?r[4].value%u[4].value:r[4].value:Number.NaN,resultToCalculation(e,r,a)}function snapAsBorderWidth(e,n,t){if(!s(n))return-1;const r=t.devicePixelLength??1,u=[v.Dimension,`${r}px`,n[2],n[3],{value:r,type:f.Integer,unit:"px"}],a=convertUnit(u,n);if(!twoOfSameNumeric(a,u))return-1;if(Number.isInteger(a[4].value/r))return resultToCalculation(e,n,n[4].value);if(a[4].value>0){if(a[4].value0?1/0:Object.is(0,0*a[4].value)?0:-0;break;default:o=Object.is(0,0*a[4].value)?0:-0}else switch(n){case"down":o=(i[4].value>0?Math.floor:Math.ceil)(a[4].value/i[4].value)*i[4].value;break;case"up":o=(i[4].value>0?Math.ceil:Math.floor)(a[4].value/i[4].value)*i[4].value;break;case"to-zero":o=Math.trunc(a[4].value/i[4].value)*i[4].value;break;default:{let t=Math.floor(a[4].value/i[4].value)*i[4].value,r=Math.ceil(a[4].value/i[4].value)*i[4].value;if(t>r){const e=t;t=r,r=e}const l=Math.abs(a[4].value-t),c=Math.abs(a[4].value-r);if(o="line-width"!==n||0!==r&&0!==t?l===c?r:l!r(e));return 1===n.length&&isNone(n[0])}if(!t(e))return!1;const n=e.value;return!!N(n)&&j.test(n[4].value)}function patchPrecision(e,n=13){if(-1===e)return-1;if(n<=0)return e;if(u(e))return e;const t=e.value;if(!C(t))return e;if(shouldSkipPrecisionRounding(t[4].value))return e;const r=roundToPrecision(t[4].value,n).toString();return m(t)?t[1]=r:d(t)?t[1]=r+"%":s(t)&&(t[1]=r+t[4].unit),e}function shouldSkipPrecisionRounding(e){if(!Number.isFinite(e)||0===e)return!0;if(Number.isInteger(e))return!0;const n=e.toString();return n.includes("e")||n.includes("E")}function roundToPrecision(e,n=13){return shouldSkipPrecisionRounding(e)?e:Number(e.toFixed(n))}const Y=String.fromCodePoint(0);function solveRandom(e,n,t,r,u,a){if(-1===n.fixed&&!a.randomCaching)return-1;const i=t.value;if(!C(i))return-1;const o=convertUnit(i,r.value);if(!twoOfSameNumeric(i,o))return-1;let l=null;if(u&&(l=convertUnit(i,u.value),!twoOfSameNumeric(i,l)))return-1;if(Number.isNaN(i[4].value)||Number.isNaN(o[4].value))return resultToCalculation(e,i,Number.NaN);if(!Number.isFinite(i[4].value))return resultToCalculation(e,i,i[4].value);if(!Number.isFinite(o[4].value)||!Number.isFinite(o[4].value-i[4].value))return resultToCalculation(e,i,Number.NaN);if(l){if(Number.isNaN(l[4].value))return resultToCalculation(e,i,Number.NaN);if(!Number.isFinite(l[4].value))return resultToCalculation(e,i,i[4].value)}const c=-1!==n.fixed?()=>n.fixed:(a.randomCaching.propertyN||(a.randomCaching.propertyN=0),sfc32(crc32([n.dashedIdent?n.dashedIdent:"",n.elementScoped?a.randomCaching.elementID:"",n.propertyScoped||n.propertyIndexScoped?a.randomCaching.propertyName:"",n.propertyIndexScoped?a.randomCaching.propertyN:"",a.randomCaching.documentID].join(Y)))),s=i[4].value;let v=o[4].value;if(v1e10)&&(l=null),l){const n=l[4].value,t=Math.max(n/1e3,1e-9);let r=Math.floor((v-s)/n);Math.abs(s+r*n-v)>=t&&Math.abs(s+(r+1)*n-v){const u=((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)+u|0,(u>>>0)/4294967296}}function crc32(e){let n,t,r=0;r^=-1;for(let u=0,a=e.length;u>>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:u}=normalizeMixPercentages(r,!1),a=u.map(e=>e.calcSum[4].value*e.percentage/100);let i=0;for(let e=0;e100||a>0&&n)for(const e of u)e.percentage=e.percentage*(100/a);let i=0;return a<100&&(i=100-a),{items:u,leftover:i}}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(u,a,o){const l=resolveGlobalsAndConstants([...u.value.filter(e=>!r(e))],a),c=[],s=[],f=[];{let e=c;for(let n=0;n!r(e)),n,t);if(-1===u)return-1;const[a,i]=u,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,u,a){const i=resolveGlobalsAndConstants([...e.value.filter(e=>!r(e))],u);let o="",l=!1;const c=[],p=[];{let e=c;for(let n=0;n!r(e))],n);if(o.length>H)throw new Error(`Maximum number of nodes in a math function (${H}) exceeded, reduce the complexity of your expression`);if(1===o.length&&t(o[0]))return{inputs:[o[0]],operation:unary};let l=0;for(;l!r(e))],n),a=solve(calc$1(calcWrapper(e,u),n,t),t);return-1===a?-1:a}function twoCommaSeparatedNodesSolver(e,n,t,r){const u=twoCommaSeparatedArguments(e,n,t);if(-1===u)return-1;const[a,i]=u;return r(e,a,i,t)}function twoCommaSeparatedArguments(e,n,u){const a=resolveGlobalsAndConstants([...e.value.filter(e=>!r(e))],n),i=[],o=[];{let e=i;for(let n=0;n!r(e))],u),o=[];{const n=[];let r=[];for(let e=0;e!r(e))],u),o=[];{const n=[];let l=[];for(let e=0;e=0;o--){if(r(n[i][o]))continue;if(l=solve(calc$1(calcWrapper(e,n[i].slice(0,o+1)),u,a),a),-1===l)continue;const s=n[i].slice(o+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),u,a),a);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;o.push({calcSum:l,percentage:c})}}return o}const J=new Set(["nearest","line-width","up","down","to-zero"]);function parseRandomValueSharing(e,n,r,u){const a={dashedIdent:"",fixed:-1,elementScoped:!1,propertyScoped:!1,propertyIndexScoped:!1};let i=!1;const o=n[0];if(!t(o)||!N(o.value))return[a,n];for(let o=0;o1)return-1;a.fixed=Math.max(0,Math.min(l.value[4].value,1-Number.EPSILON/2));continue}if("auto"!==c){if(!c.startsWith("--"))return 0===o?[a,n]:-1;if(-1!==a.fixed||i||a.dashedIdent)return-1;a.dashedIdent=c}else{if(-1!==a.fixed||i||a.dashedIdent||a.elementScoped||a.propertyScoped||a.propertyIndexScoped)return-1;a.elementScoped=!0,a.propertyIndexScoped=!0,i=!0}}else{if(-1!==a.fixed||i||a.propertyScoped||a.propertyIndexScoped)return-1;a.propertyIndexScoped=!0}else{if(-1!==a.fixed||i||a.propertyScoped||a.propertyIndexScoped)return-1;a.propertyScoped=!0}else{if(-1!==a.fixed||i||a.elementScoped)return-1;a.elementScoped=!0}}return-1}function calcWrapper(e,n){return new i([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,u){return new i([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]),u])}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 i([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 i([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 o([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new o([[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 i([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 o([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new o([[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 i([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 i([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 o([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new o([[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 i([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 o([[v.Whitespace," ",t[2],t[3],void 0]]),new n([v.Delim,"*",t[2],t[3],{value:"*"}]),new o([[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 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}),u=e.nextToken();if(e.nextToken(),!e.endOfFile())continue;if(!C(u))continue;n.set(t,u);continue}return n}function calc(e,n){return calcFromComponentValues(l(A({css:e}),{}),n).map(e=>e.map(e=>w(e.tokens())).join("")).join(",")}function calcFromComponentValues(e,n){const t=tokenizeGlobals(n?.globals);return replaceComponentValues(e,e=>{if(!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,u){for(let a=0;a{if("number"!=typeof a)return;const i=u(n.node);if(!i)return;const l=[i],c=n.parent.value[a-1];t(c)&&h(c.value)&&("-"===c.value[4].value||"+"===c.value[4].value)&&l.splice(0,0,new o([[v.Whitespace," ",...e(n.node),void 0]]));const s=n.parent.value[a+1];!s||r(s)||t(s)&&(D(s.value)||F(s.value)||E(s.value)||h(s.value)&&"-"!==s.value[4].value&&"+"!==s.value[4].value)||l.push(new o([[v.Whitespace," ",...e(n.node),void 0]])),n.parent.value.splice(a,1,...l)})}return n}const K=new Set(_.keys());export{ParseError,S as ParseErrorMessage,ParseErrorWithComponentValues,calc,calcFromComponentValues,K as mathFunctionNames}; +import{sourceIndices as e,TokenNode as n,isTokenNode as t,isWhiteSpaceOrCommentNode as r,isFunctionNode as u,isSimpleBlockNode as a,FunctionNode as o,WhitespaceNode as i,parseCommaSeparatedListOfComponentValues as l,walk as c}from"@csstools/css-parser-algorithms";import{isTokenDimension as s,TokenType as v,NumberType as f,mutateUnit as p,isTokenNumber as m,isTokenPercentage as d,isTokenIdent as N,isTokenNumeric as C,isTokenOpenParen as g,isTokenDelim as h,isTokenComma as D,isToken as b,tokenizer as B,tokenize as A,stringify as w,isTokenColon as F,isTokenSemicolon as E}from"@csstools/css-tokenizer";class ParseError extends Error{sourceStart;sourceEnd;constructor(e,n,t){super(e),this.name="ParseError",this.sourceStart=n,this.sourceEnd=t}}class ParseErrorWithComponentValues extends ParseError{componentValues;constructor(n,t){super(n,...e(t)),this.componentValues=t}}const S={UnexpectedAdditionOfDimensionOrPercentageWithNumber:"Unexpected addition of a dimension or percentage with a number.",UnexpectedSubtractionOfDimensionOrPercentageWithNumber:"Unexpected subtraction of a dimension or percentage with a number."},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]]),R=new Map([["deg",e=>360*e],["grad",e=>360*e/.9],["rad",e=>360*e/180*Math.PI],["turn",e=>e]]),G=new Map([["cm",x],["mm",O],["q",Z],["in",k],["pc",L],["pt",$],["px",V],["ms",U],["s",q],["deg",M],["grad",T],["rad",z],["turn",R],["hz",P],["khz",W]]);function convertUnit(e,n){if(!s(e))return n;if(!s(n))return n;const t=toLowerCaseAZ(e[4].unit),r=toLowerCaseAZ(n[4].unit);if(t===r)return n;const u=G.get(r);if(!u)return n;const a=u.get(t);if(!a)return n;const o=a(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=G.get(n);if(!r)return e;const u=r.get(t);if(!u)return e;const a=u(e[4].value),o=[v.Dimension,"",e[2],e[3],{...e[4],signCharacter:a<0?"-":void 0,type:Number.isInteger(a)?f.Integer:f.Number,value:a}];return p(o,t),o}function addition(e,t){if(2!==e.length)return-1;const r=e[0].value;let u=e[1].value;if(m(r)&&m(u)){const e=r[4].value+u[4].value;return new n([v.Number,e.toString(),r[2],u[3],{value:e,type:r[4].type===f.Integer&&u[4].type===f.Integer?f.Integer:f.Number}])}if(d(r)&&d(u)){const e=r[4].value+u[4].value;return new n([v.Percentage,e.toString()+"%",r[2],u[3],{value:e}])}if(s(r)&&s(u)&&(u=convertUnit(r,u),toLowerCaseAZ(r[4].unit)===toLowerCaseAZ(u[4].unit))){const e=r[4].value+u[4].value;return new n([v.Dimension,e.toString()+r[4].unit,r[2],u[3],{value:e,type:r[4].type===f.Integer&&u[4].type===f.Integer?f.Integer:f.Number,unit:r[4].unit}])}return(m(r)&&(s(u)||d(u))||m(u)&&(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=[{calculation:e,inputs:[],index:0}];let u=-1;for(;r.length;){const e=r[r.length-1];if(e.indexconvertUnit(u,e.value));if(!arrayOfSameNumeric(a))return-1;if(a.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,u,Number.NaN);if(!r.rawPercentages&&d(u))return-1;const o=a.map(e=>e[4].value);if(o.some(Number.isNaN))return resultToCalculation(e,u,Number.NaN);const i=Math.hypot(...o);return resultToCalculation(e,u,i)}function solveMax(e,n,r){if(!n.every(t))return-1;const u=n[0].value;if(!C(u))return-1;const a=n.map(e=>convertUnit(u,e.value));if(!arrayOfSameNumeric(a))return-1;if(a.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,u,Number.NaN);if(!r.rawPercentages&&d(u))return-1;const o=a.map(e=>e[4].value),i=Math.max(...o);return resultToCalculation(e,u,i)}function solveMin(e,n,r){if(!n.every(t))return-1;const u=n[0].value;if(!C(u))return-1;const a=n.map(e=>convertUnit(u,e.value));if(!arrayOfSameNumeric(a))return-1;if(a.some(e=>Number.isNaN(e[4].value)))return resultToCalculation(e,u,Number.NaN);if(!r.rawPercentages&&d(u))return-1;const o=a.map(e=>e[4].value),i=Math.min(...o);return resultToCalculation(e,u,i)}function solveMod(e,n,t){const r=n.value;if(!C(r))return-1;const u=convertUnit(r,t.value);if(!twoOfSameNumeric(r,u))return-1;let a;return a=Number.isNaN(r[4].value)||Number.isNaN(u[4].value)||0===u[4].value?Number.NaN:Number.isFinite(r[4].value)&&(Number.isFinite(u[4].value)||(u[4].value!==Number.POSITIVE_INFINITY||r[4].value!==Number.NEGATIVE_INFINITY&&!Object.is(0*r[4].value,-0))&&(u[4].value!==Number.NEGATIVE_INFINITY||r[4].value!==Number.POSITIVE_INFINITY&&!Object.is(0*r[4].value,0)))?Number.isFinite(u[4].value)?(r[4].value%u[4].value+u[4].value)%u[4].value:r[4].value:Number.NaN,resultToCalculation(e,r,a)}function solvePow(e,n,t){const r=n.value,u=t.value;if(!m(r))return-1;if(!twoOfSameNumeric(r,u))return-1;if(Number.isNaN(r[4].value)||Number.isNaN(u[4].value))return numberToCalculation(e,Number.NaN);return numberToCalculation(e,Math.pow(r[4].value,u[4].value))}function solveRem(e,n,t){const r=n.value;if(!C(r))return-1;const u=convertUnit(r,t.value);if(!twoOfSameNumeric(r,u))return-1;let a;return a=Number.isNaN(r[4].value)||Number.isNaN(u[4].value)||0===u[4].value?Number.NaN:Number.isFinite(r[4].value)?Number.isFinite(u[4].value)?r[4].value%u[4].value:r[4].value:Number.NaN,resultToCalculation(e,r,a)}function snapAsBorderWidth(e,n,t){if(!s(n))return-1;const r=t.devicePixelLength??1,u=[v.Dimension,`${r}px`,n[2],n[3],{value:r,type:f.Integer,unit:"px"}],a=convertUnit(u,n);if(!twoOfSameNumeric(a,u))return-1;if(Number.isInteger(a[4].value/r))return resultToCalculation(e,n,n[4].value);if(a[4].value>0){if(a[4].value0?1/0:Object.is(0,0*a[4].value)?0:-0;break;default:i=Object.is(0,0*a[4].value)?0:-0}else switch(n){case"down":i=(o[4].value>0?Math.floor:Math.ceil)(a[4].value/o[4].value)*o[4].value;break;case"up":i=(o[4].value>0?Math.ceil:Math.floor)(a[4].value/o[4].value)*o[4].value;break;case"to-zero":i=Math.trunc(a[4].value/o[4].value)*o[4].value;break;default:{let t=Math.floor(a[4].value/o[4].value)*o[4].value,r=Math.ceil(a[4].value/o[4].value)*o[4].value;if(t>r){const e=t;t=r,r=e}const l=Math.abs(a[4].value-t),c=Math.abs(a[4].value-r);if(i="line-width"!==n||0!==r&&0!==t?l===c?r:l!r(e));return 1===n.length&&isNone(n[0])}if(!t(e))return!1;const n=e.value;return!!N(n)&&j.test(n[4].value)}function patchPrecision(e,n=13){if(-1===e)return-1;if(n<=0)return e;if(u(e))return e;const t=e.value;if(!C(t))return e;if(shouldSkipPrecisionRounding(t[4].value))return e;const r=roundToPrecision(t[4].value,n).toString();return m(t)?t[1]=r:d(t)?t[1]=r+"%":s(t)&&(t[1]=r+t[4].unit),e}function shouldSkipPrecisionRounding(e){if(!Number.isFinite(e)||0===e)return!0;if(Number.isInteger(e))return!0;const n=e.toString();return n.includes("e")||n.includes("E")}function roundToPrecision(e,n=13){return shouldSkipPrecisionRounding(e)?e:Number(e.toFixed(n))}const Y=String.fromCodePoint(0);function solveRandom(e,n,t,r,u,a){if(-1===n.fixed&&!a.randomCaching)return-1;const o=t.value;if(!C(o))return-1;const i=convertUnit(o,r.value);if(!twoOfSameNumeric(o,i))return-1;let l=null;if(u&&(l=convertUnit(o,u.value),!twoOfSameNumeric(o,l)))return-1;if(Number.isNaN(o[4].value)||Number.isNaN(i[4].value))return resultToCalculation(e,o,Number.NaN);if(!Number.isFinite(o[4].value))return resultToCalculation(e,o,o[4].value);if(!Number.isFinite(i[4].value)||!Number.isFinite(i[4].value-o[4].value))return resultToCalculation(e,o,Number.NaN);if(l){if(Number.isNaN(l[4].value))return resultToCalculation(e,o,Number.NaN);if(!Number.isFinite(l[4].value))return resultToCalculation(e,o,o[4].value)}const c=-1!==n.fixed?()=>n.fixed:(a.randomCaching.propertyN||(a.randomCaching.propertyN=0),sfc32(crc32([n.dashedIdent?n.dashedIdent:"",n.elementScoped?a.randomCaching.elementID:"",n.propertyScoped||n.propertyIndexScoped?a.randomCaching.propertyName:"",n.propertyIndexScoped?a.randomCaching.propertyN:"",a.randomCaching.documentID].join(Y)))),s=o[4].value;let v=i[4].value;if(v1e10)&&(l=null),l){const n=l[4].value,t=Math.max(n/1e3,1e-9);let r=Math.floor((v-s)/n);Math.abs(s+r*n-v)>=t&&Math.abs(s+(r+1)*n-v){const u=((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)+u|0,(u>>>0)/4294967296}}const _=(()=>{const e=new Uint32Array(256);for(let n=0;n<256;n++)e[n]=Number("0x"+"00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D".substring(9*n,9*n+8));return e})();function crc32(e){let n,t,r=0;r^=-1;for(let u=0,a=e.length;u>>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:u}=normalizeMixPercentages(r,!1),a=u.map(e=>e.calcSum[4].value*e.percentage/100);let o=0;for(let e=0;e100||a>0&&n)for(const e of u)e.percentage=e.percentage*(100/a);let o=0;return a<100&&(o=100-a),{items:u,leftover:o}}const H=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(u,a,i){const l=resolveGlobalsAndConstants([...u.value.filter(e=>!r(e))],a),c=[],s=[],f=[];{let e=c;for(let n=0;n!r(e)),n,t);if(-1===u)return-1;const[a,o]=u,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,u,a){const o=resolveGlobalsAndConstants([...e.value.filter(e=>!r(e))],u);let i="",l=!1;const c=[],p=[];{let e=c;for(let n=0;n!r(e))],n);if(i.length>J)throw new Error(`Maximum number of nodes in a math function (${J}) exceeded, reduce the complexity of your expression`);if(1===i.length&&t(i[0]))return{inputs:[i[0]],operation:unary};let l=0;for(;l!r(e))],n),a=solve(calc$1(calcWrapper(e,u),n,t),t);return-1===a?-1:a}function twoCommaSeparatedNodesSolver(e,n,t,r){const u=twoCommaSeparatedArguments(e,n,t);if(-1===u)return-1;const[a,o]=u;return r(e,a,o,t)}function twoCommaSeparatedArguments(e,n,u){const a=resolveGlobalsAndConstants([...e.value.filter(e=>!r(e))],n),o=[],i=[];{let e=o;for(let n=0;n!r(e))],u),i=[];{const n=[];let r=[];for(let e=0;e!r(e))],u),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)),u,a),a),-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),u,a),a);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 K=new Set(["nearest","line-width","up","down","to-zero"]);function parseRandomValueSharing(e,n,r,u){const a={dashedIdent:"",fixed:-1,elementScoped:!1,propertyScoped:!1,propertyIndexScoped:!1};let o=!1;const i=n[0];if(!t(i)||!N(i.value))return[a,n];for(let i=0;i1)return-1;a.fixed=Math.max(0,Math.min(l.value[4].value,1-Number.EPSILON/2));continue}if("auto"!==c){if(!c.startsWith("--"))return 0===i?[a,n]:-1;if(-1!==a.fixed||o||a.dashedIdent)return-1;a.dashedIdent=c}else{if(-1!==a.fixed||o||a.dashedIdent||a.elementScoped||a.propertyScoped||a.propertyIndexScoped)return-1;a.elementScoped=!0,a.propertyIndexScoped=!0,o=!0}}else{if(-1!==a.fixed||o||a.propertyScoped||a.propertyIndexScoped)return-1;a.propertyIndexScoped=!0}else{if(-1!==a.fixed||o||a.propertyScoped||a.propertyIndexScoped)return-1;a.propertyScoped=!0}else{if(-1!==a.fixed||o||a.elementScoped)return-1;a.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,u){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]),u])}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 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}),u=e.nextToken();if(e.nextToken(),!e.endOfFile())continue;if(!C(u))continue;n.set(t,u);continue}return n}function calc(e,n){return calcFromComponentValues(l(A({css:e}),{}),n).map(e=>e.map(e=>w(e.tokens())).join("")).join(",")}function calcFromComponentValues(e,n){const t=tokenizeGlobals(n?.globals);return replaceComponentValues(e,e=>{if(!u(e))return;const r=H.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,u){for(let a=0;a{if("number"!=typeof a)return;const o=u(n.node);if(!o)return;const l=[o],c=n.parent.value[a-1];t(c)&&h(c.value)&&("-"===c.value[4].value||"+"===c.value[4].value)&&l.splice(0,0,new i([[v.Whitespace," ",...e(n.node),void 0]]));const s=n.parent.value[a+1];!s||r(s)||t(s)&&(D(s.value)||F(s.value)||E(s.value)||h(s.value)&&"-"!==s.value[4].value&&"+"!==s.value[4].value)||l.push(new i([[v.Whitespace," ",...e(n.node),void 0]])),n.parent.value.splice(a,1,...l)})}return n}const Q=new Set(H.keys());export{ParseError,S as ParseErrorMessage,ParseErrorWithComponentValues,calc,calcFromComponentValues,Q as mathFunctionNames}; diff --git a/packages/css-calc/src/functions/random.ts b/packages/css-calc/src/functions/random.ts index e65d922fb6..0d2be6a740 100644 --- a/packages/css-calc/src/functions/random.ts +++ b/packages/css-calc/src/functions/random.ts @@ -170,8 +170,20 @@ function sfc32(a: number = 0.34944106645296036, b: number = 0.19228640875738723, }; } +// The lookup table is parsed once instead of re-parsing a hex substring for +// every single character of the input. +const CRC32_TABLE: Uint32Array = ((): Uint32Array => { + const hex = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D"; + + const table = new Uint32Array(256); + for (let i = 0; i < 256; i++) { + table[i] = Number("0x" + hex.substring((i * 9), (i * 9) + 8)); + } + + return table; +})(); + function crc32(str: string): number { - const table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D"; let crc = 0; let x: number; let y: number; @@ -179,7 +191,7 @@ function crc32(str: string): number { crc = crc ^ (-1); for (let i = 0, iTop = str.length; i < iTop; i++) { y = (crc ^ str.charCodeAt(i)) & 0xFF; - x = Number("0x" + table.substring((y * 9), (y * 9) + 8)); + x = CRC32_TABLE[y]; crc = (crc >>> 8) ^ x; } diff --git a/packages/utilities/CHANGELOG.md b/packages/utilities/CHANGELOG.md index 93093474bc..22a4f4115b 100644 --- a/packages/utilities/CHANGELOG.md +++ b/packages/utilities/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to Utilities +### Unreleased (patch) + +- Improve performance of `hasFallback` so it is linear instead of quadratic in the number of declarations of a single container. + ### 3.0.0 _January 14, 2026_ diff --git a/packages/utilities/dist/index.mjs b/packages/utilities/dist/index.mjs index 282f970891..3f1311c71a 100644 --- a/packages/utilities/dist/index.mjs +++ b/packages/utilities/dist/index.mjs @@ -1 +1 @@ -function hasFallback(e){const t=e.parent;if(!t)return!1;const r=e.prop.toLowerCase(),o=t.index(e);for(let e=0;e(node: T): T { + return (node as T & { proxyOf?: T }).proxyOf ?? node; +} + +/** + * Per-container set of declarations that have a fallback: a declaration with + * the same (ASCII lowercased) property name appears before it. + * + * `hasFallback` is called once per matching declaration. The naive version + * (`parent.index(node)` + a sibling scan) is O(n) per call, which makes a rule + * with many declarations O(n^2). The answer for every declaration in a + * container is computed in a single pass and reused until the container's node + * list changes. + */ +type FallbackIndex = { + nodeCount: number, + firstNode: unknown, + lastNode: unknown, + declarationsWithFallback: WeakSet, +}; + +const fallbackIndexCache = new WeakMap(); + +function fallbackIndexFor(parent: Container): FallbackIndex { + const nodes = parent.nodes || []; + + let index = fallbackIndexCache.get(parent); + if ( + index && + index.nodeCount === nodes.length && + index.firstNode === nodes[0] && + index.lastNode === nodes[nodes.length - 1] + ) { + return index; + } + + const declarationsWithFallback = new WeakSet(); + const seenProps = new Set(); + + for (let i = 0; i < nodes.length; i++) { + const sibling = nodes[i]; + if (sibling.type !== 'decl') { + continue; + } + + const decl = sibling; + const prop = decl.prop.toLowerCase(); + if (seenProps.has(prop)) { + declarationsWithFallback.add(decl); + } else { + seenProps.add(prop); + } + } + + index = { + nodeCount: nodes.length, + firstNode: nodes[0], + lastNode: nodes[nodes.length - 1], + declarationsWithFallback, + }; + fallbackIndexCache.set(parent, index); + + return index; +} /** * Check if a declaration has a fallback. @@ -13,15 +83,5 @@ export function hasFallback(node: Declaration): boolean { return false; } - const nodeProp = node.prop.toLowerCase(); - - const currentNodeIndex = parent.index(node); - for (let i = 0; i < currentNodeIndex; i++) { - const precedingSibling = parent.nodes[i]; - if (precedingSibling.type === 'decl' && precedingSibling.prop.toLowerCase() === nodeProp) { - return true; - } - } - - return false; + return fallbackIndexFor(unwrapProxy(parent)).declarationsWithFallback.has(unwrapProxy(node)); } diff --git a/plugins/postcss-attribute-case-insensitive/CHANGELOG.md b/plugins/postcss-attribute-case-insensitive/CHANGELOG.md index 46851018ae..0379f4c8cd 100644 --- a/plugins/postcss-attribute-case-insensitive/CHANGELOG.md +++ b/plugins/postcss-attribute-case-insensitive/CHANGELOG.md @@ -1,5 +1,10 @@ # Changes to PostCSS Attribute Case Insensitive +### Unreleased (patch) + +- Bound the combined number of selector variants across all case insensitive attributes. + + ### 8.0.1 _September 25, 2026_ diff --git a/plugins/postcss-attribute-case-insensitive/dist/index.mjs b/plugins/postcss-attribute-case-insensitive/dist/index.mjs index e2a91930cf..ee2d52becd 100644 --- a/plugins/postcss-attribute-case-insensitive/dist/index.mjs +++ b/plugins/postcss-attribute-case-insensitive/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-selector-parser";function nodeIsInsensitiveAttribute(e){return"attribute"===e.type&&(e.insensitive??!1)}function selectorHasInsensitiveAttribute(e){return e.some(nodeIsInsensitiveAttribute)}function transformString(e,t,s){const r=s.charAt(t);if(""===r)return e;const n=r.toLocaleUpperCase(),o=n!==r;if(e.length*(o?2:1)>1e4)throw new Error("Too many selector variants when trying to resolve case insensitive attributes, reduce the complexity of your selectors");let c=e.map(e=>e+r);return o&&(c=c.concat(e.map(e=>e+n))),transformString(c,t+1,s)}function createNewSelectors(t){let s=[e.selector({value:"",nodes:[]})];return t.each(e=>{if(!nodeIsInsensitiveAttribute(e))return void s.forEach(t=>{t.append(e.clone())});const t=transformString([""],0,(r=e).value??"").map(e=>{const t=r.clone({spaces:{after:r.spaces.after,before:r.spaces.before},insensitive:!1});return t.setValue(e),t});var r;const n=[];t.forEach(e=>{s.forEach(t=>{const s=t.clone({});s.append(e),n.push(s)})}),s=n}),s}const creator=t=>{const s=Object.assign({preserve:!1},t);return{postcssPlugin:"postcss-attribute-case-insensitive",prepare(){const t=new WeakSet;return{postcssPlugin:"postcss-attribute-case-insensitive",Rule(r,{result:n}){if(t.has(r))return;if(!/i\s*\]/i.test(r.selector))return;let o;try{o=e(e=>{let t=[];e.each(e=>{selectorHasInsensitiveAttribute(e)&&(t=t.concat(createNewSelectors(e)),e.remove())}),t.length&&t.forEach(t=>e.append(t))}).processSync(r.selector)}catch(e){return void r.warn(n,`Failed to parse selector : "${r.selector}" with message: "${e instanceof Error?e.message:e}"`)}o!==r.selector&&(t.add(r),r.cloneBefore({selector:o}),s.preserve||r.remove())}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import e from"postcss-selector-parser";function nodeIsInsensitiveAttribute(e){return"attribute"===e.type&&(e.insensitive??!1)}function selectorHasInsensitiveAttribute(e){return e.some(nodeIsInsensitiveAttribute)}const t=1e4;function transformString(e,s,r){const n=r.charAt(s);if(""===n)return e;const o=n.toLocaleUpperCase(),c=o!==n;if(e.length*(c?2:1)>t)throw new Error("Too many selector variants when trying to resolve case insensitive attributes, reduce the complexity of your selectors");let i=e.map(e=>e+n);return c&&(i=i.concat(e.map(e=>e+o))),transformString(i,s+1,r)}function createNewSelectors(s){let r=[e.selector({value:"",nodes:[]})];return s.each(e=>{if(!nodeIsInsensitiveAttribute(e))return void r.forEach(t=>{t.append(e.clone())});const s=transformString([""],0,(n=e).value??"").map(e=>{const t=n.clone({spaces:{after:n.spaces.after,before:n.spaces.before},insensitive:!1});return t.setValue(e),t});var n;const o=[];if(s.length*r.length>t)throw new Error("Too many selector variants when trying to resolve case insensitive attributes, reduce the complexity of your selectors");s.forEach(e=>{r.forEach(t=>{const s=t.clone({});s.append(e),o.push(s)})}),r=o}),r}const creator=s=>{const r=Object.assign({preserve:!1},s);return{postcssPlugin:"postcss-attribute-case-insensitive",prepare(){const s=new WeakSet;return{postcssPlugin:"postcss-attribute-case-insensitive",Rule(n,{result:o}){if(s.has(n))return;if(!/i\s*\]/i.test(n.selector))return;let c;try{c=e(e=>{let s=[];e.each(e=>{if(selectorHasInsensitiveAttribute(e)){const r=createNewSelectors(e);if(s.length+r.length>t)throw new Error("Too many selector variants when trying to resolve case insensitive attributes, reduce the complexity of your selectors");s=s.concat(r),e.remove()}}),s.length&&s.forEach(t=>e.append(t))}).processSync(n.selector)}catch(e){return void n.warn(o,`Failed to parse selector : "${n.selector}" with message: "${e instanceof Error?e.message:e}"`)}c!==n.selector&&(s.add(n),n.cloneBefore({selector:c}),r.preserve||n.remove())}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-attribute-case-insensitive/src/index.ts b/plugins/postcss-attribute-case-insensitive/src/index.ts index 617e6a9071..cbe75ba299 100644 --- a/plugins/postcss-attribute-case-insensitive/src/index.ts +++ b/plugins/postcss-attribute-case-insensitive/src/index.ts @@ -66,6 +66,13 @@ function createNewSelectors(selector: selectorParser.Selector): Array = []; + // The per-attribute cap only bounds a single attribute. The number of + // generated selectors is the product across all insensitive attributes, + // so bound the combined total as well. + if (sensitiveAttributes.length * newSelectors.length > MAX_SELECTOR_VARIANTS) { + throw new Error(`Too many selector variants when trying to resolve case insensitive attributes, reduce the complexity of your selectors`); + } + sensitiveAttributes.forEach(newNode => { newSelectors.forEach(newSelector => { const newSelectorWithNewNode = newSelector.clone({}); @@ -115,12 +122,17 @@ const creator: PluginCreator = (opts?: pluginOptions) => { let modifiedSelector: string; try { - modifiedSelector = selectorParser((selectors) => { + modifiedSelector = selectorParser((selectors) => { let newSelectors: Array = []; selectors.each(selector => { if (selectorHasInsensitiveAttribute(selector)) { - newSelectors = newSelectors.concat(createNewSelectors(selector)); + const expanded = createNewSelectors(selector); + if (newSelectors.length + expanded.length > MAX_SELECTOR_VARIANTS) { + throw new Error(`Too many selector variants when trying to resolve case insensitive attributes, reduce the complexity of your selectors`); + } + + newSelectors = newSelectors.concat(expanded); selector.remove(); } }); diff --git a/plugins/postcss-container-rule-prelude-list/CHANGELOG.md b/plugins/postcss-container-rule-prelude-list/CHANGELOG.md index c54446eb7f..61de366e3a 100644 --- a/plugins/postcss-container-rule-prelude-list/CHANGELOG.md +++ b/plugins/postcss-container-rule-prelude-list/CHANGELOG.md @@ -1,5 +1,10 @@ # Changes to PostCSS Container Rule Prelude List +### Unreleased (patch) + +- Bound the total number of generated at-rules to prevent an exponential blowup from nested `@container` rules. + + ### 1.0.2 _September 25, 2026_ diff --git a/plugins/postcss-container-rule-prelude-list/dist/index.mjs b/plugins/postcss-container-rule-prelude-list/dist/index.mjs index 95ac856fd3..fd54f68a7f 100644 --- a/plugins/postcss-container-rule-prelude-list/dist/index.mjs +++ b/plugins/postcss-container-rule-prelude-list/dist/index.mjs @@ -1 +1 @@ -import{parseCommaSeparatedListOfComponentValues as s,stringify as e}from"@csstools/css-parser-algorithms";import{tokenize as r}from"@csstools/css-tokenizer";const t=/^container$/i,creator=o=>{const a=Object.assign({preserve:!1},o);return{postcssPlugin:"postcss-container-rule-prelude-list",AtRule(o){if(!t.test(o.name))return;if(!o.params.includes(","))return;const c=s(r({css:o.params})).map(s=>e([s]));c.length<=1||(c.forEach(s=>{o.cloneBefore({params:s.trim()})}),a.preserve||o.remove())}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parseCommaSeparatedListOfComponentValues as e,stringify as s}from"@csstools/css-parser-algorithms";import{tokenize as t}from"@csstools/css-tokenizer";const r=/^container$/i;function countAtRules(e){let s=1;return e.walkAtRules(()=>{s++}),s}const creator=o=>{const n=Object.assign({preserve:!1},o);let c=0;return{postcssPlugin:"postcss-container-rule-prelude-list",Once(){c=0},AtRule(o){if(!r.test(o.name))return;if(!o.params.includes(","))return;const i=e(t({css:o.params})).map(e=>s([e]));if(!(i.length<=1)){if(c+=i.length*countAtRules(o),c>1e4)throw new Error("Maximum @container expansion size exceeded, reduce the complexity of your stylesheet");i.forEach(e=>{o.cloneBefore({params:e.trim()})}),n.preserve||o.remove()}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-container-rule-prelude-list/src/index.ts b/plugins/postcss-container-rule-prelude-list/src/index.ts index be779063b1..8f580abe68 100644 --- a/plugins/postcss-container-rule-prelude-list/src/index.ts +++ b/plugins/postcss-container-rule-prelude-list/src/index.ts @@ -1,9 +1,24 @@ import { parseCommaSeparatedListOfComponentValues, stringify } from '@csstools/css-parser-algorithms'; import { tokenize } from '@csstools/css-tokenizer'; -import type { PluginCreator } from 'postcss'; +import type { AtRule, PluginCreator } from 'postcss'; const CONTAINER_NAME_REGEX = /^container$/i; +// A comma separated prelude is expanded by cloning the whole at-rule (including +// its children) once per list item. When such rules are nested, the clones are +// expanded again and the output grows exponentially with the nesting depth. +// Bound the total number of at-rules generated per stylesheet. +const MAX_EXPANDED_AT_RULES = 10_000; + +function countAtRules(node: AtRule): number { + let count = 1; + node.walkAtRules(() => { + count++; + }); + + return count; +} + /** postcss-container-rule-prelude-list plugin options */ export type pluginOptions = { /** Preserve the original notation. default: false */ @@ -20,8 +35,13 @@ const creator: PluginCreator = (opts?: pluginOptions) => { opts, ); + let expandedAtRules = 0; + return { postcssPlugin: 'postcss-container-rule-prelude-list', + Once(): void { + expandedAtRules = 0; + }, AtRule(rule): void { if (!CONTAINER_NAME_REGEX.test(rule.name)) { return; @@ -36,6 +56,11 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } + expandedAtRules += list.length * countAtRules(rule); + if (expandedAtRules > MAX_EXPANDED_AT_RULES) { + throw new Error('Maximum @container expansion size exceeded, reduce the complexity of your stylesheet'); + } + list.forEach((item) => { rule.cloneBefore({ params: item.trim() }); }); diff --git a/plugins/postcss-custom-media/CHANGELOG.md b/plugins/postcss-custom-media/CHANGELOG.md index f08c71d608..c5204907b4 100644 --- a/plugins/postcss-custom-media/CHANGELOG.md +++ b/plugins/postcss-custom-media/CHANGELOG.md @@ -1,5 +1,10 @@ # Changes to PostCSS Custom Media +### Unreleased (patch) + +- Resolve cascade layer order in linear time instead of scanning the layer list for every node. + + ### 12.0.2 _September 25, 2026_ diff --git a/plugins/postcss-custom-media/src/cascade-layers.ts b/plugins/postcss-custom-media/src/cascade-layers.ts index 6b993068a5..466ad1a344 100644 --- a/plugins/postcss-custom-media/src/cascade-layers.ts +++ b/plugins/postcss-custom-media/src/cascade-layers.ts @@ -108,16 +108,25 @@ export function collectCascadeLayerOrder(root: Root): WeakMap { addLayerToModel(layers, [layerName]); } + // Index the layer names once so resolving each reference is O(1) + // instead of scanning the whole layer list for every node. + const layerIndexByName = new Map(); + for (let i = 0; i < layers.length; i++) { + layerIndexByName.set(layerKey(layers[i]), i); + } + const out: WeakMap = new WeakMap(); for (const [node, layerName] of references) { - out.set(node, layers.findIndex((x) => { - return layerName.equal(x); - })); + out.set(node, layerIndexByName.get(layerKey(layerName)) ?? -1); } return out; } +function layerKey(layerName: LayerName): string { + return layerName.segments().join('\u0000'); +} + // 0 : node was not found // any number : node was found, higher numbers have higher priority // a very large number : node wasn't layered, highest priority diff --git a/plugins/postcss-custom-properties/CHANGELOG.md b/plugins/postcss-custom-properties/CHANGELOG.md index e87e9d2473..19a154184d 100644 --- a/plugins/postcss-custom-properties/CHANGELOG.md +++ b/plugins/postcss-custom-properties/CHANGELOG.md @@ -1,5 +1,11 @@ # Changes to PostCSS Custom Properties +### Unreleased (patch) + +- Share a single expansion budget across all declarations, and resolve cascade layer order in linear time. +- Avoid a quadratic sibling scan when checking for an existing fallback declaration. + + ### 15.0.2 _September 25, 2026_ diff --git a/plugins/postcss-custom-properties/dist/index.mjs b/plugins/postcss-custom-properties/dist/index.mjs index cd5b99dbdc..3e4ee7408c 100644 --- a/plugins/postcss-custom-properties/dist/index.mjs +++ b/plugins/postcss-custom-properties/dist/index.mjs @@ -1 +1 @@ -import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import n from"postcss-value-parser";import{hasSupportsAtRuleAncestor as o}from"@csstools/utilities";const r=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(n){const o=new Map,s=new Map,a=[];let i=1;n.walkAtRules(n=>{if("layer"!==n.name.toLowerCase())return;{let e=n.parent,t=0;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===n.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");e=e.parent}}let c;if(n.nodes)c=normalizeLayerName(n.params,i++);else{if(!n.params.trim())return;c=n.params}let l=e(c);if(l?.length){{let e=n.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);if(t){l=l.map(e=>t.concat(e));break}e=e.parent}}if(t(a,l),n.nodes){const e=l[0].concat(r);o.set(n,e),s.set(n,l[0])}}});for(const e of o.values())t(a,[e]);const c=new WeakMap;for(const[e,t]of o)c.set(e,a.findIndex(e=>t.equal(e)));return c}function normalizeLayerName(e,t){return e.trim()?e:`csstools-anon-layer--${t}`}const s=/(?:!\s*)?postcss-custom-properties:\s*off\b/i,a=new WeakMap;function isBlockIgnored(e){if(!e||!e.nodes)return!1;if(a.has(e))return a.get(e);const t=e.some(e=>isIgnoreComment(e,s));return a.set(e,t),t}const i=/(?:!\s*)?postcss-custom-properties:\s*ignore\s+next\b/i;function isDeclarationIgnored(e){return!!e&&(!!isBlockIgnored(e.parent)||isIgnoreComment(e.prev(),i))}function isIgnoreComment(e,t){return!!e&&"comment"===e.type&&t.test(e.text)}const c=new Set(["layer"]);function isProcessableRule(e){let t=e.parent;for(;t;){if("atrule"===t.type&&!c.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const l=/^html$/i,u=/^:where\(html\)$/i,p=/^:root$/i,f=/^:where\(:root\)$/i,d=/(html|:root)/i,m=/^var$/i;function isVarFunction(e){return"function"===e.type&&m.test(e.value)&&e.nodes?.length>0}const h=/\bvar\(/i;function parseOrCached(e,t){let o=t.get(e);return o||(o=n(e),t.set(e,o),o)}function toposort(e,t){let n=e.length;const o=new Array(n),r=new Set,s=makeOutgoingEdges(t),a=makeNodesHash(e);for(let t=e.length-1;t>=0;t--){if(r.has(t))continue;const i=e[t],c=new Set,l=[];for(r.add(t),c.add(i),l.push({node:i,outgoing:Array.from(s.get(i)||new Set),next:0});l.length;){const e=l[l.length-1];if(e.nexts.get(e)??0,getLowLink=e=>a.get(e)??0;for(const e of o){if(s.has(e))continue;const t=[{node:e,childIndex:0}];for(s.set(e,l),a.set(e,l),l++,c.push(e),i.add(e);t.length;){const e=t[t.length-1],o=r.get(e.node)??[];if(e.childIndex1)for(const e of t)n.add(e)}}}for(const t of n)e.delete(t);return n}function makeOutgoingEdges(e){const t=new Map;for(let n=0,o=e.length;n{if(o.remaining--,o.remaining<0)throw new Error("Maximum custom property expansion size exceeded, reduce the complexity of your custom properties");if(!isVarFunction(e))return;const a=parseVarFunction(e);if(!a)return;let i=!1;a.fallback&&n.walk(a.fallback,e=>{if(!isVarFunction(e))return;const n=parseVarFunction(e);return n?n.fallback||t.has(n.name.value)?void 0:(i=!0,!1):void 0});let c=t.get(a.name.value)?.nodes;c||!a.fallback||i||(c=a.fallback),void 0!==c&&(c.length?replaceNodeWithNodes(s,r,c):s.splice(r,1,{type:"div",value:" ",before:"",after:"",sourceIndex:e.sourceIndex,sourceEndIndex:e.sourceEndIndex}))}),n.stringify(e.nodes)):""}function walk(e,t){let n,o,r;for(n=0,o=e.length;n"comment"!==e.type&&"space"!==e.type);return 1===t.length&&("word"===t[0].type&&w.test(t[0].value))}function buildCustomPropertiesMap(e,t,o){if(!e.size)return t;const r=new Map(t);{const s=[];for(const[a,i]of e.entries()){const c=parseOrCached(i,o);let l=!1;n.walk(c.nodes,n=>{if(!isVarFunction(n))return;const o=parseVarFunction(n);o&&(o.fallback||e.has(o.name.value)||t.has(o.name.value)?s.push([o.name.value,a]):l=!0)}),l||r.set(a,c)}removeCyclicReferences(r,s)}{const e=[];for(const[t,o]of r.entries())n.walk(o.nodes,n=>{if(!isVarFunction(n))return;const o=parseVarFunction(n);o&&(o.fallback||r.has(o.name.value)?e.push([o.name.value,t]):r.delete(t))});for(let t=0;t{d.test(e.selector)&&e.nodes?.length&&isProcessableRule(e)&&(isBlockIgnored(e)||e.selectors.forEach(t=>{let s;if(u.test(t)||f.test(t))s=0;else if(l.test(t))s=1;else{if(!p.test(t))return;s=2}const a=(c=r,((i=e).parent&&"atrule"===i.parent.type&&"layer"===i.parent.name.toLowerCase()?c.has(i.parent)?c.get(i.parent)+1:0:1e7)+10+s);var i,c;e.each(e=>{if("decl"!==e.type)return;if(!e.variable||isDeclarationIgnored(e))return;if("initial"===e.value.toLowerCase().trim())return;const t=o.get(e.prop)??-1;a>=t&&(o.set(e.prop,a),n.set(e.prop,e.value))})}))}),buildCustomPropertiesMap(n,new Map,t)}function getCustomPropertiesFromSiblings(e,t,n){if(!e.parent)return t;const o=new Map;return e.parent.each(t=>{"decl"===t.type&&t.variable&&e!==t&&(isDeclarationIgnored(t)||o.set(t.prop,t.value))}),o.size?buildCustomPropertiesMap(o,t,n):t}function transformProperties(e,t,o){if(isTransformableDecl(e)&&!isDeclarationIgnored(e)){const r=e.raws?.value?.raw??e.value,s=transformValueAST(n(r),t);if(s===r)return;if(parentHasExactFallback(e,s))return void(o.preserve||e.remove());const a=e.cloneBefore({value:s});a.raws?.value?.raw&&(a.raws.value.raw=""),o?.preserve||e.remove()}}const isTransformableDecl=e=>!e.variable&&e.value.includes("--")&&e.value.toLowerCase().includes("var(");function parentHasExactFallback(e,t){if(!e||!e.parent)return!1;let n=!1;const o=e.parent.index(e);return e.parent.each((r,s)=>r!==e&&(!(s>=o)&&void("decl"===r.type&&r.prop.toLowerCase()===e.prop.toLowerCase()&&r.value===t&&(n=!0)))),n}const v=/\bvar\(|\(top: var\(--f\)/i,creator=e=>{const t=!("preserve"in Object(e))||Boolean(e?.preserve);if("importFrom"in Object(e))throw new Error('[postcss-custom-properties] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-properties] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-properties",prepare(){let e=new Map;const n=new WeakMap,r=new Map;return{postcssPlugin:"postcss-custom-properties",Once(t){e=getCustomPropertiesFromRoot(t,r)},Declaration(s){if(!h.test(s.value))return;if(o(s,v))return;let a=e;t&&s.parent&&(a=n.get(s.parent)??getCustomPropertiesFromSiblings(s,e,r),n.set(s.parent,a)),transformProperties(s,a,{preserve:t})}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import n from"postcss-value-parser";import{hasSupportsAtRuleAncestor as r}from"@csstools/utilities";const o=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(n){const r=new Map,s=new Map,a=[];let i=1;n.walkAtRules(n=>{if("layer"!==n.name.toLowerCase())return;{let e=n.parent,t=0;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===n.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");e=e.parent}}let c;if(n.nodes)c=normalizeLayerName(n.params,i++);else{if(!n.params.trim())return;c=n.params}let l=e(c);if(l?.length){{let e=n.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);if(t){l=l.map(e=>t.concat(e));break}e=e.parent}}if(t(a,l),n.nodes){const e=l[0].concat(o);r.set(n,e),s.set(n,l[0])}}});for(const e of r.values())t(a,[e]);const c=new Map;for(let e=0;eisIgnoreComment(e,s));return a.set(e,t),t}const i=/(?:!\s*)?postcss-custom-properties:\s*ignore\s+next\b/i;function isDeclarationIgnored(e){return!!e&&(!!isBlockIgnored(e.parent)||isIgnoreComment(e.prev(),i))}function isIgnoreComment(e,t){return!!e&&"comment"===e.type&&t.test(e.text)}const c=new Set(["layer"]);function isProcessableRule(e){let t=e.parent;for(;t;){if("atrule"===t.type&&!c.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const l=/^html$/i,u=/^:where\(html\)$/i,p=/^:root$/i,f=/^:where\(:root\)$/i,d=/(html|:root)/i,m=/^var$/i;function isVarFunction(e){return"function"===e.type&&m.test(e.value)&&e.nodes?.length>0}const g=/\bvar\(/i;function parseOrCached(e,t){let r=t.get(e);return r||(r=n(e),t.set(e,r),r)}function toposort(e,t){let n=e.length;const r=new Array(n),o=new Set,s=makeOutgoingEdges(t),a=makeNodesHash(e);for(let t=e.length-1;t>=0;t--){if(o.has(t))continue;const i=e[t],c=new Set,l=[];for(o.add(t),c.add(i),l.push({node:i,outgoing:Array.from(s.get(i)||new Set),next:0});l.length;){const e=l[l.length-1];if(e.nexts.get(e)??0,getLowLink=e=>a.get(e)??0;for(const e of r){if(s.has(e))continue;const t=[{node:e,childIndex:0}];for(s.set(e,l),a.set(e,l),l++,c.push(e),i.add(e);t.length;){const e=t[t.length-1],r=o.get(e.node)??[];if(e.childIndex1)for(const e of t)n.add(e)}}}for(const t of n)e.delete(t);return n}function makeOutgoingEdges(e){const t=new Map;for(let n=0,r=e.length;n{if(r.remaining--,r.remaining<0)throw new Error("Maximum custom property expansion size exceeded, reduce the complexity of your custom properties");if(!isVarFunction(e))return;const a=parseVarFunction(e);if(!a)return;let i=!1;a.fallback&&n.walk(a.fallback,e=>{if(!isVarFunction(e))return;const n=parseVarFunction(e);return n?n.fallback||t.has(n.name.value)?void 0:(i=!0,!1):void 0});let c=t.get(a.name.value)?.nodes;c||!a.fallback||i||(c=a.fallback),void 0!==c&&(c.length?replaceNodeWithNodes(s,o,c):s.splice(o,1,{type:"div",value:" ",before:"",after:"",sourceIndex:e.sourceIndex,sourceEndIndex:e.sourceEndIndex}))}),n.stringify(e.nodes)):""}function walk(e,t){let n,r,o;for(n=0,r=e.length;n"comment"!==e.type&&"space"!==e.type);return 1===t.length&&("word"===t[0].type&&w.test(t[0].value))}function buildCustomPropertiesMap(e,t,r){if(!e.size)return t;const o=new Map(t);{const s=[];for(const[a,i]of e.entries()){const c=parseOrCached(i,r);let l=!1;n.walk(c.nodes,n=>{if(!isVarFunction(n))return;const r=parseVarFunction(n);r&&(r.fallback||e.has(r.name.value)||t.has(r.name.value)?s.push([r.name.value,a]):l=!0)}),l||o.set(a,c)}removeCyclicReferences(o,s)}{const e=[];for(const[t,r]of o.entries())n.walk(r.nodes,n=>{if(!isVarFunction(n))return;const r=parseVarFunction(n);r&&(r.fallback||o.has(r.name.value)?e.push([r.name.value,t]):o.delete(t))});for(let t=0;t{d.test(e.selector)&&e.nodes?.length&&isProcessableRule(e)&&(isBlockIgnored(e)||e.selectors.forEach(t=>{let s;if(u.test(t)||f.test(t))s=0;else if(l.test(t))s=1;else{if(!p.test(t))return;s=2}const a=(c=o,((i=e).parent&&"atrule"===i.parent.type&&"layer"===i.parent.name.toLowerCase()?c.has(i.parent)?c.get(i.parent)+1:0:1e7)+10+s);var i,c;e.each(e=>{if("decl"!==e.type)return;if(!e.variable||isDeclarationIgnored(e))return;if("initial"===e.value.toLowerCase().trim())return;const t=r.get(e.prop)??-1;a>=t&&(r.set(e.prop,a),n.set(e.prop,e.value))})}))}),buildCustomPropertiesMap(n,new Map,t)}function getCustomPropertiesFromSiblings(e,t,n){if(!e.parent)return t;const r=new Map;return e.parent.each(t=>{"decl"===t.type&&t.variable&&e!==t&&(isDeclarationIgnored(t)||r.set(t.prop,t.value))}),r.size?buildCustomPropertiesMap(r,t,n):t}function transformProperties(e,t,r){if(isTransformableDecl(e)&&!isDeclarationIgnored(e)){const o=e.raws?.value?.raw??e.value,s=transformValueAST(n(o),t,r.budget);if(s===o)return;if(parentHasExactFallback(e,s))return void(r.preserve||e.remove());const a=e.cloneBefore({value:s});a.raws?.value?.raw&&(a.raws.value.raw=""),r?.preserve||e.remove()}}const isTransformableDecl=e=>!e.variable&&e.value.includes("--")&&e.value.toLowerCase().includes("var(");function parentHasExactFallback(e,t){if(!e||!e.parent)return!1;const n=e.prop.toLowerCase();let r=e.prev();for(;r;){if("decl"===r.type&&r.prop.toLowerCase()===n&&r.value===t)return!0;r=r.prev()}return!1}const y=/\bvar\(|\(top: var\(--f\)/i,creator=e=>{const t=!("preserve"in Object(e))||Boolean(e?.preserve);if("importFrom"in Object(e))throw new Error('[postcss-custom-properties] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-properties] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-properties",prepare(){let e=new Map;const n=new WeakMap,o=new Map;let s={remaining:h};return{postcssPlugin:"postcss-custom-properties",Once(t){s={remaining:h},e=getCustomPropertiesFromRoot(t,o)},Declaration(a){if(!g.test(a.value))return;if(r(a,y))return;let i=e;t&&a.parent&&(i=n.get(a.parent)??getCustomPropertiesFromSiblings(a,e,o),n.set(a.parent,i)),transformProperties(a,i,{preserve:t,budget:s})}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-custom-properties/src/cascade-layers.ts b/plugins/postcss-custom-properties/src/cascade-layers.ts index 6b993068a5..466ad1a344 100644 --- a/plugins/postcss-custom-properties/src/cascade-layers.ts +++ b/plugins/postcss-custom-properties/src/cascade-layers.ts @@ -108,16 +108,25 @@ export function collectCascadeLayerOrder(root: Root): WeakMap { addLayerToModel(layers, [layerName]); } + // Index the layer names once so resolving each reference is O(1) + // instead of scanning the whole layer list for every node. + const layerIndexByName = new Map(); + for (let i = 0; i < layers.length; i++) { + layerIndexByName.set(layerKey(layers[i]), i); + } + const out: WeakMap = new WeakMap(); for (const [node, layerName] of references) { - out.set(node, layers.findIndex((x) => { - return layerName.equal(x); - })); + out.set(node, layerIndexByName.get(layerKey(layerName)) ?? -1); } return out; } +function layerKey(layerName: LayerName): string { + return layerName.segments().join('\u0000'); +} + // 0 : node was not found // any number : node was found, higher numbers have higher priority // a very large number : node wasn't layered, highest priority diff --git a/plugins/postcss-custom-properties/src/index.ts b/plugins/postcss-custom-properties/src/index.ts index 9f2533498e..fa508a545c 100644 --- a/plugins/postcss-custom-properties/src/index.ts +++ b/plugins/postcss-custom-properties/src/index.ts @@ -6,6 +6,8 @@ import getCustomPropertiesFromSiblings from './get-custom-properties-from-siblin import { HAS_VAR_FUNCTION_REGEX } from './is-var-function'; import { hasSupportsAtRuleAncestor } from '@csstools/utilities'; import { transformProperties } from './transform-properties'; +import { MAX_TRANSFORMED_NODES } from './transform-value-ast'; +import type { TransformValueASTBudget } from './transform-value-ast'; /** postcss-custom-properties plugin options */ export type pluginOptions = { @@ -33,9 +35,15 @@ const creator: PluginCreator = (opts?: pluginOptions) => { const customPropertiesByParent: WeakMap> = new WeakMap(); const parsedValuesCache: Map = new Map(); + // Share a single expansion budget across all declarations in this + // document so that a value that resolves to a large subtree can not be + // inlined once per declaration. + let expansionBudget: TransformValueASTBudget = { remaining: MAX_TRANSFORMED_NODES }; + return { postcssPlugin: 'postcss-custom-properties', Once(root): void { + expansionBudget = { remaining: MAX_TRANSFORMED_NODES }; rootCustomProperties = getCustomPropertiesFromRoot(root, parsedValuesCache); }, Declaration(decl): void { @@ -54,7 +62,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { customPropertiesByParent.set(decl.parent, customProperties); } - transformProperties(decl, customProperties, { preserve: preserve }); + transformProperties(decl, customProperties, { preserve: preserve, budget: expansionBudget }); }, }; }, diff --git a/plugins/postcss-custom-properties/src/transform-properties.ts b/plugins/postcss-custom-properties/src/transform-properties.ts index da423c6401..fb531b3ad7 100644 --- a/plugins/postcss-custom-properties/src/transform-properties.ts +++ b/plugins/postcss-custom-properties/src/transform-properties.ts @@ -2,14 +2,15 @@ import type { Declaration } from 'postcss'; import valuesParser from 'postcss-value-parser'; import transformValueAST from './transform-value-ast'; +import type { TransformValueASTBudget } from './transform-value-ast'; import { isDeclarationIgnored } from './is-ignored'; // transform custom pseudo selectors with custom selectors -export function transformProperties(decl: Declaration, customProperties: Map, opts: { preserve?: boolean }): void { +export function transformProperties(decl: Declaration, customProperties: Map, opts: { preserve?: boolean, budget?: TransformValueASTBudget }): void { if (isTransformableDecl(decl) && !isDeclarationIgnored(decl)) { const originalValue = decl.raws?.value?.raw ?? decl.value; const valueAST = valuesParser(originalValue); - const value = transformValueAST(valueAST, customProperties); + const value = transformValueAST(valueAST, customProperties, opts.budget); if (value === originalValue) { return; @@ -45,25 +46,19 @@ function parentHasExactFallback(decl: Declaration, value: string): boolean { return false; } - let hasFallback = false; - const declIndex = decl.parent.index(decl); - decl.parent.each((sibling, index) => { - if (sibling === decl) { - return false; - } - - if (index >= declIndex) { - return false; - } + const prop = decl.prop.toLowerCase(); - if (sibling.type !== 'decl') { - return; + // Walk backwards from the declaration instead of scanning from the start of + // the container. `parent.index()` + a full sibling scan for every + // declaration is O(n^2) for a rule with many declarations. + let sibling = decl.prev(); + while (sibling) { + if (sibling.type === 'decl' && sibling.prop.toLowerCase() === prop && sibling.value === value) { + return true; } - if (sibling.prop.toLowerCase() === decl.prop.toLowerCase() && sibling.value === value) { - hasFallback = true; - } - }); + sibling = sibling.prev(); + } - return hasFallback; + return false; } diff --git a/plugins/postcss-custom-properties/src/transform-value-ast.ts b/plugins/postcss-custom-properties/src/transform-value-ast.ts index 732c501eb9..23810a14b6 100644 --- a/plugins/postcss-custom-properties/src/transform-value-ast.ts +++ b/plugins/postcss-custom-properties/src/transform-value-ast.ts @@ -9,7 +9,7 @@ import { parseVarFunction } from './parse-var-function'; * Custom properties can reference each other and duplicate subtrees, which can grow * exponentially. Sharing a single budget bounds the total expansion. */ -type TransformValueASTBudget = { +export type TransformValueASTBudget = { remaining: number, }; diff --git a/plugins/postcss-custom-selectors/CHANGELOG.md b/plugins/postcss-custom-selectors/CHANGELOG.md index b1beffe424..b395f0aa8a 100644 --- a/plugins/postcss-custom-selectors/CHANGELOG.md +++ b/plugins/postcss-custom-selectors/CHANGELOG.md @@ -1,5 +1,11 @@ # Changes to PostCSS Custom Selectors +### Unreleased (patch) + +- Bound the total number of generated selectors to prevent an exponential blowup from nested custom selectors. +- Resolve cascade layer order in linear time instead of scanning the layer list for every node. + + ### 9.0.2 _September 25, 2026_ diff --git a/plugins/postcss-custom-selectors/dist/index.mjs b/plugins/postcss-custom-selectors/dist/index.mjs index 5856301ff2..57a9cb8c80 100644 --- a/plugins/postcss-custom-selectors/dist/index.mjs +++ b/plugins/postcss-custom-selectors/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-selector-parser";import{parse as r,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";const s=r("csstools-implicit-layer")[0];function collectCascadeLayerOrder(e){const o=new Map,n=new Map,a=[];let c=1;e.walkAtRules(e=>{if("layer"!==e.name.toLowerCase())return;{let r=e.parent,t=0;for(;r;){if("atrule"!==r.type||"layer"!==r.name.toLowerCase()){if(r===e.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");r=r.parent}}let l;if(e.nodes)l=normalizeLayerName(e.params,c++);else{if(!e.params.trim())return;l=e.params}let u=r(l);if(u?.length){{let r=e.parent;for(;r&&"atrule"===r.type&&"layer"===r.name.toLowerCase();){const e=n.get(r);if(e){u=u.map(r=>e.concat(r));break}r=r.parent}}if(t(a,u),e.nodes){const r=u[0].concat(s);o.set(e,r),n.set(e,u[0])}}});for(const e of o.values())t(a,[e]);const l=new WeakMap;for(const[e,r]of o)l.set(e,a.findIndex(e=>r.equal(e)));return l}function normalizeLayerName(e,r){return e.trim()?e:`csstools-anon-layer--${r}`}const o=new Set(["scope","container","layer"]);function isProcessableCustomSelectorRule(e){if("atrule"!==e.type)return!1;if("custom-selector"!==e.name.toLowerCase())return!1;if(!e.params||!e.params.includes(":--"))return!1;if(e.nodes&&e.nodes.length>0)return!1;let r=e.parent;for(;r;){if("rule"===r.type)return!1;if("atrule"===r.type&&!o.has(r.name.toLowerCase()))return!1;r=r.parent}return!0}function getCustomSelectors(r,t,s){const o=new Map,n=new Map,a=collectCascadeLayerOrder(r);return r.walkAtRules(r=>{var c,l;if(isProcessableCustomSelectorRule(r))try{const t=r.params.trim(),u=e().astSync(t),p=u?.nodes?.[0]?.nodes?.[0];if(!p||"pseudo"!==p.type||!p.value.startsWith(":--"))return;const i=p.toString(),m=(l=a,(c=r).parent&&"atrule"===c.parent.type&&"layer"===c.parent.name.toLowerCase()?l.has(c.parent)?l.get(c.parent)+1:0:1e7),f=n.get(i)??-1;if(m&&m>=f&&(n.set(i,m),o.set(i,e().astSync(t.slice(i.length).trim()))),!s.preserve){const e=r.parent;r.remove(),removeEmptyAncestorBlocks(e)}}catch(e){r.warn(t,`Failed to parse selector : "${r.params}" with message: "${e instanceof Error?e.message:e}"`)}}),o}function removeEmptyAncestorBlocks(e){if(!e)return;let r=e;for(;r;){if(r.nodes&&r.nodes.length>0)return;const e=r.parent;r.remove(),r=e}}function transformRule(r,t,s){let o=r.selector;try{o=e(r=>{r.walkPseudos(r=>{if(!s.has(r.value))return;const t=e.pseudo({value:":is",nodes:[]}),o=s.get(r.value);o&&(o.each(e=>{t.append(e.clone())}),r.replaceWith(t))})}).processSync(r.selector)}catch(e){return r.warn(t,`Failed to parse selector : "${o}" with message: "${e instanceof Error?e.message:e}"`),r.selector}return o}const creator=e=>{const r=e?.preserve??!1;if("importFrom"in Object(e))throw new Error('[postcss-custom-selectors] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-selectors] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-selectors",prepare(){const e=new WeakSet;let t=new Map;return{postcssPlugin:"postcss-custom-selectors",Once(e,{result:s}){t=getCustomSelectors(e,s,{preserve:r})},Rule(s,{result:o}){if(e.has(s))return;if(!s.selector?.includes(":--"))return;const n=transformRule(s,o,t);if(n!==s.selector){if(n.length>1e5)throw s.error("Maximum custom selector expansion length exceeded, reduce the complexity of your custom selectors");e.add(s),s.cloneBefore({selector:n}),r||s.remove()}}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import e from"postcss-selector-parser";import{parse as r,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";const s=r("csstools-implicit-layer")[0];function collectCascadeLayerOrder(e){const o=new Map,n=new Map,a=[];let c=1;e.walkAtRules(e=>{if("layer"!==e.name.toLowerCase())return;{let r=e.parent,t=0;for(;r;){if("atrule"!==r.type||"layer"!==r.name.toLowerCase()){if(r===e.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");r=r.parent}}let l;if(e.nodes)l=normalizeLayerName(e.params,c++);else{if(!e.params.trim())return;l=e.params}let u=r(l);if(u?.length){{let r=e.parent;for(;r&&"atrule"===r.type&&"layer"===r.name.toLowerCase();){const e=n.get(r);if(e){u=u.map(r=>e.concat(r));break}r=r.parent}}if(t(a,u),e.nodes){const r=u[0].concat(s);o.set(e,r),n.set(e,u[0])}}});for(const e of o.values())t(a,[e]);const l=new Map;for(let e=0;e0)return!1;let r=e.parent;for(;r;){if("rule"===r.type)return!1;if("atrule"===r.type&&!o.has(r.name.toLowerCase()))return!1;r=r.parent}return!0}function getCustomSelectors(r,t,s){const o=new Map,n=new Map,a=collectCascadeLayerOrder(r);return r.walkAtRules(r=>{var c,l;if(isProcessableCustomSelectorRule(r))try{const t=r.params.trim(),u=e().astSync(t),i=u?.nodes?.[0]?.nodes?.[0];if(!i||"pseudo"!==i.type||!i.value.startsWith(":--"))return;const p=i.toString(),m=(l=a,(c=r).parent&&"atrule"===c.parent.type&&"layer"===c.parent.name.toLowerCase()?l.has(c.parent)?l.get(c.parent)+1:0:1e7),f=n.get(p)??-1;if(m&&m>=f&&(n.set(p,m),o.set(p,e().astSync(t.slice(p.length).trim()))),!s.preserve){const e=r.parent;r.remove(),removeEmptyAncestorBlocks(e)}}catch(e){r.warn(t,`Failed to parse selector : "${r.params}" with message: "${e instanceof Error?e.message:e}"`)}}),o}function removeEmptyAncestorBlocks(e){if(!e)return;let r=e;for(;r;){if(r.nodes&&r.nodes.length>0)return;const e=r.parent;r.remove(),r=e}}function transformRule(r,t,s){let o=r.selector;try{let t=1e5;o=e(r=>{r.walkPseudos(r=>{if(!s.has(r.value))return;const o=e.pseudo({value:":is",nodes:[]}),n=s.get(r.value);if(n){if(t--,t<0)throw new Error("Maximum custom selector expansion size exceeded, reduce the complexity of your custom selectors");n.each(e=>{o.append(e.clone())}),r.replaceWith(o)}})}).processSync(r.selector)}catch(e){return r.warn(t,`Failed to parse selector : "${o}" with message: "${e instanceof Error?e.message:e}"`),r.selector}return o}const creator=e=>{const r=e?.preserve??!1;if("importFrom"in Object(e))throw new Error('[postcss-custom-selectors] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-selectors] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-selectors",prepare(){const e=new WeakSet;let t=new Map;return{postcssPlugin:"postcss-custom-selectors",Once(e,{result:s}){t=getCustomSelectors(e,s,{preserve:r})},Rule(s,{result:o}){if(e.has(s))return;if(!s.selector?.includes(":--"))return;const n=transformRule(s,o,t);if(n!==s.selector){if(n.length>1e5)throw s.error("Maximum custom selector expansion length exceeded, reduce the complexity of your custom selectors");e.add(s),s.cloneBefore({selector:n}),r||s.remove()}}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-custom-selectors/src/cascade-layers.ts b/plugins/postcss-custom-selectors/src/cascade-layers.ts index 6b993068a5..466ad1a344 100644 --- a/plugins/postcss-custom-selectors/src/cascade-layers.ts +++ b/plugins/postcss-custom-selectors/src/cascade-layers.ts @@ -108,16 +108,25 @@ export function collectCascadeLayerOrder(root: Root): WeakMap { addLayerToModel(layers, [layerName]); } + // Index the layer names once so resolving each reference is O(1) + // instead of scanning the whole layer list for every node. + const layerIndexByName = new Map(); + for (let i = 0; i < layers.length; i++) { + layerIndexByName.set(layerKey(layers[i]), i); + } + const out: WeakMap = new WeakMap(); for (const [node, layerName] of references) { - out.set(node, layers.findIndex((x) => { - return layerName.equal(x); - })); + out.set(node, layerIndexByName.get(layerKey(layerName)) ?? -1); } return out; } +function layerKey(layerName: LayerName): string { + return layerName.segments().join('\u0000'); +} + // 0 : node was not found // any number : node was found, higher numbers have higher priority // a very large number : node wasn't layered, highest priority diff --git a/plugins/postcss-custom-selectors/src/transform-rule.ts b/plugins/postcss-custom-selectors/src/transform-rule.ts index e7ff0b9321..18c8917bef 100644 --- a/plugins/postcss-custom-selectors/src/transform-rule.ts +++ b/plugins/postcss-custom-selectors/src/transform-rule.ts @@ -2,11 +2,19 @@ import parser from 'postcss-selector-parser'; import type { Result, Rule } from 'postcss'; import type { Root } from 'postcss-selector-parser'; +// Custom selectors can reference each other and duplicate their contents. +// Every substitution can duplicate the remaining references, so the number of +// substitutions grows exponentially with the nesting depth. Bound the total +// number of substitutions performed for a single rule. +const MAX_EXPANSIONS = 100_000; + // transform custom pseudo selectors with custom selectors export function transformRule(rule: Rule, result: Result, customSelectors: Map): string { let selector = rule.selector; try { + let expansionBudget = MAX_EXPANSIONS; + selector = parser(selectors => { selectors.walkPseudos((pseudo) => { if (!customSelectors.has(pseudo.value)) { @@ -24,6 +32,11 @@ export function transformRule(rule: Rule, result: Result, customSelectors: Map { isWrapper.append(node.clone()); }); diff --git a/plugins/postcss-design-tokens/CHANGELOG.md b/plugins/postcss-design-tokens/CHANGELOG.md index 324f7a8c58..d86d3a88c1 100644 --- a/plugins/postcss-design-tokens/CHANGELOG.md +++ b/plugins/postcss-design-tokens/CHANGELOG.md @@ -1,5 +1,10 @@ # Changes to PostCSS Design Tokens +### Unreleased (patch) + +- Bound the total resolved token value length to prevent an exponential blowup from token references. + + ### 5.0.1 _September 25, 2026_ diff --git a/plugins/postcss-design-tokens/dist/index.mjs b/plugins/postcss-design-tokens/dist/index.mjs index 56bbf85944..9db6148d46 100644 --- a/plugins/postcss-design-tokens/dist/index.mjs +++ b/plugins/postcss-design-tokens/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-value-parser";import t from"node:path";import n from"node:fs/promises";import r from"node:module";import{parseListOfComponentValues as o,isFunctionNode as s,isWhitespaceNode as a,isCommentNode as i,isTokenNode as u}from"@csstools/css-parser-algorithms";import{tokenize as l,isTokenString as c,isTokenIdent as f}from"@csstools/css-tokenizer";function toposort(e,t){let n=e.length;const r=new Array(n),o=new Set,s=makeOutgoingEdges(t),a=makeNodesHash(e);t.forEach(function(e){if(!a.has(e[0])||!a.has(e[1]))throw new Error("Unknown token. Make sure to provide all tokens used in aliases.")});for(let t=e.length-1;t>=0;t--){if(o.has(t))continue;const i=e[t],u=new Set,l=[];for(o.add(t),u.add(i),l.push({node:i,outgoing:Array.from(s.get(i)||new Set),next:0});l.length;){const e=l[l.length-1];if(e.nextapplyTransformsToValue(o,e),name:String(e.name??"")||t,comment:String(e.comment??"")||void 0,metadata:{name:String(e.name??"")?t:void 0,path:[...n,t],filePath:r,isSource:!0}}}const p=new Map;function applyTransformsToValue(t,n){if(!t)return"";if(!n)return t;if(!n.toUnit)return t;const r=e.unit(t);if(!r||r.unit===n.toUnit)return t;if(!r.unit){if(p.has(n.toUnit)){if(p.get(n.toUnit))return`${t}${n.toUnit}`;throw new Error(`Invalid unit "${n.toUnit}" for "${t}"`)}try{const r=e.unit(`${t}${n.toUnit}`);if(r&&r.unit===n.toUnit)return p.set(n.toUnit,!0),`${t}${n.toUnit}`;p.set(n.toUnit,!1)}catch{p.set(n.toUnit,!1)}throw new Error(`Invalid unit "${n.toUnit}" for "${t}"`)}return"rem"===r.unit&&"px"===n.toUnit?remToPx(parseFloat(r.number),n.pluginOptions?.rootFontSize??16):"px"===r.unit&&"rem"===n.toUnit?pxToRem(parseFloat(r.number),n.pluginOptions?.rootFontSize??16):t}function remToPx(e,t){return`${formatFloat(e*t)}px`}function pxToRem(e,t){return`${formatFloat(e/t)}rem`}function formatFloat(e){if(Number.isInteger(e))return e.toString();let t=e.toFixed(5);for(let e=t.length-1;e>=0&&"."!==t[e]&&"0"===t[e];e--)t=t.slice(0,e);return t}function dereferenceTokenValues(e){const t=new Set,n=new Map;for(const[r,o]of e.entries()){const e=parseReferences(o.value);e.length&&(t.add(r),n.set(r,e))}for(const[r,o]of n.entries()){for(let n=0;n"value-reference"===e.type))continue;const s=o.map(e=>e.value).join(""),a=e.get(r);a.value=s,a.cssValue=e=>applyTransformsToValue(s,e),e.set(r,a),t.delete(r),n.delete(r)}if(0===t.size)return e;{const r=Array.from(e.keys()),o=[];for(const[e,t]of n.entries())for(let n=0;n"value-reference"===e.type))throw new Error('Token "'+r+'" can not be fully resolved');const s=o.map(e=>e.value).join(""),a=e.get(r);a.value=s,a.cssValue=e=>applyTransformsToValue(s,e),e.set(r,a),t.delete(r),n.delete(r)}if(0===t.size)return e}return e}function parseReferences(e){if("string"!=typeof e)return[];if(-1===e.indexOf("{"))return[];const t=[];let n=!1,r=!1,o="";for(let s=0;s0&&(t.push({type:"value-non-reference",value:o}),o=""),r=!0;break;case"}":if(!r)throw new Error('Unexpected "}" in "'+e+'" at '+s);if(0===o.length)throw new Error('Empty alias "{}" in "'+e+'" at '+s);{let e=o.trim();".value"===e.slice(-6)&&(e=e.slice(0,-6)),t.push({type:"value-reference",raw:e}),o=""}n=!0,r=!1;break;default:o+=a}}if(r)throw new Error('Unexpected end of alias in "'+e+'"');return o.length>0&&t.push({type:"value-non-reference",value:o}),n?t:[]}function isTokenGroup(e){return"object"==typeof e&&null!==e&&void 0===e.value}function extractTokens(e,t,n){const r=new Map;for(const o in e)if(Object.hasOwnProperty.call(e,o)){if(null===e[o]||"object"!=typeof e[o]||Array.isArray(e[o]))throw new Error(`Parsing error at "${[...t,o].join(".")}"`);const s=e[o];if(!s)throw new Error(`Parsing error at "${[...t,o].join(".")}"`);if(!isTokenGroup(s)){const e=extractStyleDictionaryV3Token(s,o,t,n);r.set(e.metadata.path.join("."),e);continue}for(const[e,a]of extractTokens(s,[...t,o],n).entries())r.set(e,a)}return r}function extractStyleDictionaryV3Tokens(e,t){return dereferenceTokenValues(extractTokens(e,[],t))}function extractStyleDictionaryTokens(e,t,n){return extractStyleDictionaryV3Tokens(t,n)}const d="6b4e71e7-4787-42f7-a092-8684961895db";function parseImport(t){const n=e(t),r={filePath:"",format:"standard",conditions:[d]};return n.walk(e=>{"function"===e.type&&"url"===e.value&&(r.filePath=e.nodes[0].value),"function"===e.type&&"format"===e.value&&(r.format=e.nodes[0].value),"function"===e.type&&"when"===e.value&&(r.conditions=e.nodes.filter(e=>"string"===e.type).map(e=>e.value))}),r.conditions.length||(r.conditions=[d]),r}async function tokensFromImport(e,o,s,a,i,u){const{filePath:l,format:c,conditions:f}=parseImport(i);if(!f.every(e=>s.includes(e)))return!1;let p;try{if(l.startsWith("node_modules://")){p=r.createRequire(t.dirname(a)).resolve(l.slice(15))}else if(l.startsWith("node_modules:")){p=r.createRequire(t.dirname(a)).resolve(l.slice(13))}else p=t.resolve(t.dirname(a),l)}catch(e){throw new Error(`Failed to read ${l} with error ${e instanceof Error?e.message:e}`,{cause:e})}if(u.has(p))return!1;o.result.messages.push({type:"dependency",plugin:"postcss-design-tokens",file:p,parent:e.source?.input?.file}),u.add(p);const d=await n.readFile(p,"utf8"),m=JSON.parse(d);if("style-dictionary3"===c)return{filePath:t.resolve(l),tokens:extractStyleDictionaryTokens(0,m,p)};throw new Error("Unsupported format: "+c)}function mergeTokens(e,t){const n=new Map(e);for(const[e,r]of t)n.set(e,r);return n}function parsePluginOptions(e){const t={importAtRuleName:"design-tokens",is:[d],unitsAndValues:{rootFontSize:16},valueFunctionName:"design-token"};return e?("object"!=typeof e||(Array.isArray(e.is)&&(t.is=e.is.filter(e=>"string"==typeof e)),0===t.is.length&&(t.is=[d]),"object"==typeof e.unitsAndValues&&"number"==typeof e.unitsAndValues.rootFontSize&&((n=e.unitsAndValues.rootFontSize)>0&&n!==1/0)&&(t.unitsAndValues.rootFontSize=e.unitsAndValues.rootFontSize),"string"==typeof e.valueFunctionName&&(t.valueFunctionName=e.valueFunctionName),"string"==typeof e.importAtRuleName&&(t.importAtRuleName=e.importAtRuleName)),t):t;var n}function parseComponentValues(e){return t=l({css:e},{onParseError:e=>{throw e}}),o(t,{onParseError:e=>{throw e}});var t}function transform(e,t,n,r,o){const s=parseComponentValues(r);let a=!1;return s.forEach((r,i)=>{if("walk"in r){{const u=transformComponentValue(r,e,t,n,o);if(u)return s.splice(i,1,...u),a=!0,!1}r.walk((r,s)=>{if("string"==typeof s)return;const i=transformComponentValue(r.node,e,t,n,o);return i?(r.parent.value.splice(s,1,...i),a=!0,!1):void 0})}}),a?s.map(e=>e.toString()).join(""):r}function transformComponentValue(e,t,n,r,o){if(!s(e))return;if(e.getName().toLowerCase()!==o.valueFunctionName)return;let l="",p="",d="";for(let t=0;t{const t=parsePluginOptions(e);return{postcssPlugin:"postcss-design-tokens",prepare(){let e=new Map,n=new Set;return{postcssPlugin:"postcss-design-tokens",OnceExit(){e=new Map,n=new Set},async Once(r,o){const s=[];r.walkAtRules(e=>{if(e.name.toLowerCase()!==t.importAtRuleName)return;if(!e?.source?.input?.file)return;const n=e.source.input.file,r=e.params;e.remove(),s.push({filePath:n,params:r,node:e})});for(const a of s.values()){let s;try{if(s=await tokensFromImport(r,o,t.is,a.filePath,a.params,n),!s)continue}catch(e){a.node.warn(o.result,`Failed to import design tokens from "${a.params}" with error:\n\t`+(e instanceof Error?e.message:e));continue}o.result.messages.push({type:"dependency",plugin:"postcss-design-tokens",file:s.filePath,parent:a.filePath}),e=mergeTokens(e,s.tokens)}},Declaration(n,{result:r}){if(n.value.toLowerCase().includes(t.valueFunctionName))try{const o=transform(e,r,n,n.value,t);if(o===n.value)return;n.value=o}catch{n.warn(r,`Failed to parse and transform "${n.value}"`)}},AtRule(n,{result:r}){if(n.params.toLowerCase().includes(t.valueFunctionName))try{const o=transform(e,r,n,n.params,t);if(o===n.params)return;n.params=o}catch{n.warn(r,`Failed to parse and transform "${n.params}"`)}}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import e from"postcss-value-parser";import t from"node:path";import n from"node:fs/promises";import r from"node:module";import{parseListOfComponentValues as o,isFunctionNode as s,isWhitespaceNode as a,isCommentNode as i,isTokenNode as u}from"@csstools/css-parser-algorithms";import{tokenize as l,isTokenString as c,isTokenIdent as f}from"@csstools/css-tokenizer";function toposort(e,t){let n=e.length;const r=new Array(n),o=new Set,s=makeOutgoingEdges(t),a=makeNodesHash(e);t.forEach(function(e){if(!a.has(e[0])||!a.has(e[1]))throw new Error("Unknown token. Make sure to provide all tokens used in aliases.")});for(let t=e.length-1;t>=0;t--){if(o.has(t))continue;const i=e[t],u=new Set,l=[];for(o.add(t),u.add(i),l.push({node:i,outgoing:Array.from(s.get(i)||new Set),next:0});l.length;){const e=l[l.length-1];if(e.nextapplyTransformsToValue(o,e),name:String(e.name??"")||t,comment:String(e.comment??"")||void 0,metadata:{name:String(e.name??"")?t:void 0,path:[...n,t],filePath:r,isSource:!0}}}const p=new Map;function applyTransformsToValue(t,n){if(!t)return"";if(!n)return t;if(!n.toUnit)return t;const r=e.unit(t);if(!r||r.unit===n.toUnit)return t;if(!r.unit){if(p.has(n.toUnit)){if(p.get(n.toUnit))return`${t}${n.toUnit}`;throw new Error(`Invalid unit "${n.toUnit}" for "${t}"`)}try{const r=e.unit(`${t}${n.toUnit}`);if(r&&r.unit===n.toUnit)return p.set(n.toUnit,!0),`${t}${n.toUnit}`;p.set(n.toUnit,!1)}catch{p.set(n.toUnit,!1)}throw new Error(`Invalid unit "${n.toUnit}" for "${t}"`)}return"rem"===r.unit&&"px"===n.toUnit?remToPx(parseFloat(r.number),n.pluginOptions?.rootFontSize??16):"px"===r.unit&&"rem"===n.toUnit?pxToRem(parseFloat(r.number),n.pluginOptions?.rootFontSize??16):t}function remToPx(e,t){return`${formatFloat(e*t)}px`}function pxToRem(e,t){return`${formatFloat(e/t)}rem`}function formatFloat(e){if(Number.isInteger(e))return e.toString();let t=e.toFixed(5);for(let e=t.length-1;e>=0&&"."!==t[e]&&"0"===t[e];e--)t=t.slice(0,e);return t}const d=1e6;function dereferenceTokenValues(e){const t=new Set,n=new Map;let r=0;for(const[r,o]of e.entries()){const e=parseReferences(o.value);e.length&&(t.add(r),n.set(r,e))}for(const[o,s]of n.entries()){for(let n=0;n"value-reference"===e.type))continue;const a=s.map(e=>e.value).join("");if(r+=a.length,r>d)throw new Error("Maximum design token expansion size exceeded, reduce the complexity of your tokens");const i=e.get(o);i.value=a,i.cssValue=e=>applyTransformsToValue(a,e),e.set(o,i),t.delete(o),n.delete(o)}if(0===t.size)return e;{const o=Array.from(e.keys()),s=[];for(const[e,t]of n.entries())for(let n=0;n"value-reference"===e.type))throw new Error('Token "'+o+'" can not be fully resolved');const a=s.map(e=>e.value).join("");if(r+=a.length,r>d)throw new Error("Maximum design token expansion size exceeded, reduce the complexity of your tokens");const i=e.get(o);i.value=a,i.cssValue=e=>applyTransformsToValue(a,e),e.set(o,i),t.delete(o),n.delete(o)}if(0===t.size)return e}return e}function parseReferences(e){if("string"!=typeof e)return[];if(-1===e.indexOf("{"))return[];const t=[];let n=!1,r=!1,o="";for(let s=0;s0&&(t.push({type:"value-non-reference",value:o}),o=""),r=!0;break;case"}":if(!r)throw new Error('Unexpected "}" in "'+e+'" at '+s);if(0===o.length)throw new Error('Empty alias "{}" in "'+e+'" at '+s);{let e=o.trim();".value"===e.slice(-6)&&(e=e.slice(0,-6)),t.push({type:"value-reference",raw:e}),o=""}n=!0,r=!1;break;default:o+=a}}if(r)throw new Error('Unexpected end of alias in "'+e+'"');return o.length>0&&t.push({type:"value-non-reference",value:o}),n?t:[]}function isTokenGroup(e){return"object"==typeof e&&null!==e&&void 0===e.value}function extractTokens(e,t,n){const r=new Map;for(const o in e)if(Object.hasOwnProperty.call(e,o)){if(null===e[o]||"object"!=typeof e[o]||Array.isArray(e[o]))throw new Error(`Parsing error at "${[...t,o].join(".")}"`);const s=e[o];if(!s)throw new Error(`Parsing error at "${[...t,o].join(".")}"`);if(!isTokenGroup(s)){const e=extractStyleDictionaryV3Token(s,o,t,n);r.set(e.metadata.path.join("."),e);continue}for(const[e,a]of extractTokens(s,[...t,o],n).entries())r.set(e,a)}return r}function extractStyleDictionaryV3Tokens(e,t){return dereferenceTokenValues(extractTokens(e,[],t))}function extractStyleDictionaryTokens(e,t,n){return extractStyleDictionaryV3Tokens(t,n)}const m="6b4e71e7-4787-42f7-a092-8684961895db";function parseImport(t){const n=e(t),r={filePath:"",format:"standard",conditions:[m]};return n.walk(e=>{"function"===e.type&&"url"===e.value&&(r.filePath=e.nodes[0].value),"function"===e.type&&"format"===e.value&&(r.format=e.nodes[0].value),"function"===e.type&&"when"===e.value&&(r.conditions=e.nodes.filter(e=>"string"===e.type).map(e=>e.value))}),r.conditions.length||(r.conditions=[m]),r}async function tokensFromImport(e,o,s,a,i,u){const{filePath:l,format:c,conditions:f}=parseImport(i);if(!f.every(e=>s.includes(e)))return!1;let p;try{if(l.startsWith("node_modules://")){p=r.createRequire(t.dirname(a)).resolve(l.slice(15))}else if(l.startsWith("node_modules:")){p=r.createRequire(t.dirname(a)).resolve(l.slice(13))}else p=t.resolve(t.dirname(a),l)}catch(e){throw new Error(`Failed to read ${l} with error ${e instanceof Error?e.message:e}`,{cause:e})}if(u.has(p))return!1;o.result.messages.push({type:"dependency",plugin:"postcss-design-tokens",file:p,parent:e.source?.input?.file}),u.add(p);const d=await n.readFile(p,"utf8"),m=JSON.parse(d);if("style-dictionary3"===c)return{filePath:t.resolve(l),tokens:extractStyleDictionaryTokens(0,m,p)};throw new Error("Unsupported format: "+c)}function mergeTokens(e,t){const n=new Map(e);for(const[e,r]of t)n.set(e,r);return n}function parsePluginOptions(e){const t={importAtRuleName:"design-tokens",is:[m],unitsAndValues:{rootFontSize:16},valueFunctionName:"design-token"};return e?("object"!=typeof e||(Array.isArray(e.is)&&(t.is=e.is.filter(e=>"string"==typeof e)),0===t.is.length&&(t.is=[m]),"object"==typeof e.unitsAndValues&&"number"==typeof e.unitsAndValues.rootFontSize&&((n=e.unitsAndValues.rootFontSize)>0&&n!==1/0)&&(t.unitsAndValues.rootFontSize=e.unitsAndValues.rootFontSize),"string"==typeof e.valueFunctionName&&(t.valueFunctionName=e.valueFunctionName),"string"==typeof e.importAtRuleName&&(t.importAtRuleName=e.importAtRuleName)),t):t;var n}function parseComponentValues(e){return t=l({css:e},{onParseError:e=>{throw e}}),o(t,{onParseError:e=>{throw e}});var t}function transform(e,t,n,r,o){const s=parseComponentValues(r);let a=!1;return s.forEach((r,i)=>{if("walk"in r){{const u=transformComponentValue(r,e,t,n,o);if(u)return s.splice(i,1,...u),a=!0,!1}r.walk((r,s)=>{if("string"==typeof s)return;const i=transformComponentValue(r.node,e,t,n,o);return i?(r.parent.value.splice(s,1,...i),a=!0,!1):void 0})}}),a?s.map(e=>e.toString()).join(""):r}function transformComponentValue(e,t,n,r,o){if(!s(e))return;if(e.getName().toLowerCase()!==o.valueFunctionName)return;let l="",p="",d="";for(let t=0;t{const t=parsePluginOptions(e);return{postcssPlugin:"postcss-design-tokens",prepare(){let e=new Map,n=new Set;return{postcssPlugin:"postcss-design-tokens",OnceExit(){e=new Map,n=new Set},async Once(r,o){const s=[];r.walkAtRules(e=>{if(e.name.toLowerCase()!==t.importAtRuleName)return;if(!e?.source?.input?.file)return;const n=e.source.input.file,r=e.params;e.remove(),s.push({filePath:n,params:r,node:e})});for(const a of s.values()){let s;try{if(s=await tokensFromImport(r,o,t.is,a.filePath,a.params,n),!s)continue}catch(e){a.node.warn(o.result,`Failed to import design tokens from "${a.params}" with error:\n\t`+(e instanceof Error?e.message:e));continue}o.result.messages.push({type:"dependency",plugin:"postcss-design-tokens",file:s.filePath,parent:a.filePath}),e=mergeTokens(e,s.tokens)}},Declaration(n,{result:r}){if(n.value.toLowerCase().includes(t.valueFunctionName))try{const o=transform(e,r,n,n.value,t);if(o===n.value)return;n.value=o}catch{n.warn(r,`Failed to parse and transform "${n.value}"`)}},AtRule(n,{result:r}){if(n.params.toLowerCase().includes(t.valueFunctionName))try{const o=transform(e,r,n,n.params,t);if(o===n.params)return;n.params=o}catch{n.warn(r,`Failed to parse and transform "${n.params}"`)}}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts index 89e4bb3ae9..cd4d787dbb 100644 --- a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts @@ -4,9 +4,16 @@ import { toposort } from '../../toposort/toposort'; import type { StyleDictionaryV3TokenValue } from './value'; import { applyTransformsToValue } from './value'; +// Tokens can reference each other and duplicate their contents, so a token +// chain that references the previous token twice grows the resolved values +// exponentially. Bound the total resolved value length so that a small token +// file can not exhaust memory. +const MAX_RESOLVED_VALUE_LENGTH = 1_000_000; + export function dereferenceTokenValues(tokens: Map): Map { const tainted = new Set(); const referenceASTs = new Map>(); + let resolvedLength = 0; // Gather all references. { @@ -53,6 +60,12 @@ export function dereferenceTokenValues(tokens: Map part.value).join(''); + + resolvedLength += value.length; + if (resolvedLength > MAX_RESOLVED_VALUE_LENGTH) { + throw new Error('Maximum design token expansion size exceeded, reduce the complexity of your tokens'); + } + const currentToken = tokens.get(id)!; currentToken.value = value; @@ -125,6 +138,12 @@ export function dereferenceTokenValues(tokens: Map part.value).join(''); + + resolvedLength += value.length; + if (resolvedLength > MAX_RESOLVED_VALUE_LENGTH) { + throw new Error('Maximum design token expansion size exceeded, reduce the complexity of your tokens'); + } + const currentToken = tokens.get(id)!; currentToken.value = value; diff --git a/plugins/postcss-double-position-gradients/CHANGELOG.md b/plugins/postcss-double-position-gradients/CHANGELOG.md index dcd6d0185f..83881938c4 100644 --- a/plugins/postcss-double-position-gradients/CHANGELOG.md +++ b/plugins/postcss-double-position-gradients/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Double Position Gradients +### Unreleased (patch) + +- Improve performance when a gradient contains multiple double-position stops. + ### 7.0.4 _September 6, 2026_ diff --git a/plugins/postcss-double-position-gradients/dist/index.mjs b/plugins/postcss-double-position-gradients/dist/index.mjs index 52821259bd..f19784579a 100644 --- a/plugins/postcss-double-position-gradients/dist/index.mjs +++ b/plugins/postcss-double-position-gradients/dist/index.mjs @@ -1 +1 @@ -import e from"@csstools/postcss-progressive-custom-properties";import t from"postcss-value-parser";import{hasFallback as s,hasSupportsAtRuleAncestor as r}from"@csstools/utilities";const o=/(?:repeating-)?(?:conic|linear|radial)-gradient\(/i,i=/^(?:repeating-)?(?:conic|linear|radial)-gradient$/i,n=["at","bottom","center","circle","closest-corner","closest-side","ellipse","farthest-corner","farthest-side","from","in","left","right","to","top"];function isPunctuationCommaNode(e){return!!e&&"div"===e.type&&","===e.value}function isNumericNode(e){if(!e)return!1;try{return!1!==t.unit(e.value)}catch{return!1}}const basePlugin=e=>({postcssPlugin:"postcss-double-position-gradients",Declaration(a,{result:c}){if(!o.test(a.value))return;if(s(a))return;if(r(a,o))return;let u;try{u=t(a.value)}catch{a.warn(c,`Failed to parse value '${a.value}' as a CSS gradient. Leaving the original value intact.`)}if(void 0===u)return;u.walk(e=>{if("function"!==e.type||!i.test(e.value))return;const t=e.nodes.filter(e=>"comment"!==e.type&&"space"!==e.type);let s=!1;t.forEach((t,r,o)=>{if("word"===t.type&&n.includes(t.value.toLowerCase())&&(s=!0),"div"===t.type&&","===t.value&&(s=!1),s)return;const i=o[r-1],a=o[r-2],c=o[r+1];if(a&&a.type&&isNumericNode(i)&&isNumericNode(t)){const s=a,r={type:"div",value:",",before:isPunctuationCommaNode(c)?c.before:"",after:isPunctuationCommaNode(c)?"":" ",sourceIndex:0,sourceEndIndex:0};e.nodes.splice(e.nodes.indexOf(t)-1,0,r,s)}})});const l=u.toString();l!==a.value&&(a.cloneBefore({value:l}),e?.preserve||a.remove())}});basePlugin.postcss=!0;const postcssPlugin=t=>{const s=Object.assign({enableProgressiveCustomProperties:!0,preserve:!0},t);return s.enableProgressiveCustomProperties&&s.preserve?{postcssPlugin:"postcss-double-position-gradients",plugins:[e(),basePlugin(s)]}:basePlugin(s)};postcssPlugin.postcss=!0;export{postcssPlugin as default,postcssPlugin as"module.exports"}; +import e from"@csstools/postcss-progressive-custom-properties";import t from"postcss-value-parser";import{hasFallback as r,hasSupportsAtRuleAncestor as s}from"@csstools/utilities";const o=/(?:repeating-)?(?:conic|linear|radial)-gradient\(/i,i=/^(?:repeating-)?(?:conic|linear|radial)-gradient$/i,n=["at","bottom","center","circle","closest-corner","closest-side","ellipse","farthest-corner","farthest-side","from","in","left","right","to","top"];function isPunctuationCommaNode(e){return!!e&&"div"===e.type&&","===e.value}function isNumericNode(e){if(!e)return!1;try{return!1!==t.unit(e.value)}catch{return!1}}const basePlugin=e=>({postcssPlugin:"postcss-double-position-gradients",Declaration(a,{result:c}){if(!o.test(a.value))return;if(r(a))return;if(s(a,o))return;let u;try{u=t(a.value)}catch{a.warn(c,`Failed to parse value '${a.value}' as a CSS gradient. Leaving the original value intact.`)}if(void 0===u)return;u.walk(e=>{if("function"!==e.type||!i.test(e.value))return;const t=e.nodes.filter(e=>"comment"!==e.type&&"space"!==e.type),r=new Map;e.nodes.forEach((e,t)=>{r.set(e,t)});let s=!1,o=0;t.forEach((t,i,a)=>{if("word"===t.type&&n.includes(t.value.toLowerCase())&&(s=!0),"div"===t.type&&","===t.value&&(s=!1),s)return;const c=a[i-1],u=a[i-2],l=a[i+1];if(u&&u.type&&isNumericNode(c)&&isNumericNode(t)){const s=u,i={type:"div",value:",",before:isPunctuationCommaNode(l)?l.before:"",after:isPunctuationCommaNode(l)?"":" ",sourceIndex:0,sourceEndIndex:0},n=r.get(t);if(void 0===n)return;e.nodes.splice(n+2*o-1,0,i,s),o++}})});const l=u.toString();l!==a.value&&(a.cloneBefore({value:l}),e?.preserve||a.remove())}});basePlugin.postcss=!0;const postcssPlugin=t=>{const r=Object.assign({enableProgressiveCustomProperties:!0,preserve:!0},t);return r.enableProgressiveCustomProperties&&r.preserve?{postcssPlugin:"postcss-double-position-gradients",plugins:[e(),basePlugin(r)]}:basePlugin(r)};postcssPlugin.postcss=!0;export{postcssPlugin as default,postcssPlugin as"module.exports"}; diff --git a/plugins/postcss-double-position-gradients/src/index.ts b/plugins/postcss-double-position-gradients/src/index.ts index 991bbf40e8..14d2ee64e7 100644 --- a/plugins/postcss-double-position-gradients/src/index.ts +++ b/plugins/postcss-double-position-gradients/src/index.ts @@ -85,7 +85,13 @@ const basePlugin: PluginCreator<{ preserve?: boolean }> = (opts?: { preserve?: b return x.type !== 'comment' && x.type !== 'space'; }); + const funcNodeIndexes = new Map(); + func.nodes.forEach((funcNode, funcNodeIndex) => { + funcNodeIndexes.set(funcNode, funcNodeIndex); + }); + let inPrefix = false; + let inserted = 0; nodes.forEach((node, index, currentNodes) => { if (node.type === 'word' && keywords.includes(node.value.toLowerCase())) { @@ -118,15 +124,22 @@ const basePlugin: PluginCreator<{ preserve?: boolean }> = (opts?: { preserve?: b sourceEndIndex: 0, }; + const funcNodeIndex = funcNodeIndexes.get(node); + if (typeof funcNodeIndex === 'undefined') { + return; + } + func.nodes.splice( - // 1 before the current node - func.nodes.indexOf(node) - 1, + // 1 before the current node, offset by the nodes inserted by earlier matches + funcNodeIndex + (inserted * 2) - 1, // remove none 0, // insert these : comma, color, ); + + inserted++; } }); }); diff --git a/plugins/postcss-gap-properties/CHANGELOG.md b/plugins/postcss-gap-properties/CHANGELOG.md index 27da9993ca..0da687421d 100644 --- a/plugins/postcss-gap-properties/CHANGELOG.md +++ b/plugins/postcss-gap-properties/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Gap Properties +### Unreleased (patch) + +- Improved performance by caching the `display: grid` lookup per rule + ### 7.0.0 _January 14, 2026_ diff --git a/plugins/postcss-gap-properties/dist/index.mjs b/plugins/postcss-gap-properties/dist/index.mjs index 934ba61c55..62ca406428 100644 --- a/plugins/postcss-gap-properties/dist/index.mjs +++ b/plugins/postcss-gap-properties/dist/index.mjs @@ -1 +1 @@ -const e=["column-gap","gap","row-gap"],creator=o=>{const r=Object.assign({preserve:!0},o);return{postcssPlugin:"postcss-gap-properties",Declaration(o){if(!e.includes(o.prop.toLowerCase()))return;if(!o.parent?.some(e=>"decl"===e.type&&("display"===e.prop.toLowerCase()&&"grid"===e.value.toLowerCase())))return;const s=`grid-${o.prop.toLowerCase()}`,p=o.parent?.some(e=>"decl"===e.type&&e.prop.toLowerCase()===s);p||(o.cloneBefore({prop:s}),r.preserve||o.remove())}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +const e=["column-gap","gap","row-gap"],r=new WeakMap;function parentHasDisplayGrid(e){const s=e.nodes||[],t=r.get(e);if(t&&t.length===s.length)return t.hasDisplayGrid;const o=s.some(e=>"decl"===e.type&&("display"===e.prop.toLowerCase()&&"grid"===e.value.toLowerCase()));return r.set(e,{length:s.length,hasDisplayGrid:o}),o}const creator=r=>{const s=Object.assign({preserve:!0},r);return{postcssPlugin:"postcss-gap-properties",Declaration(r){if(!e.includes(r.prop.toLowerCase()))return;if(!r.parent||!parentHasDisplayGrid(r.parent))return;const t=`grid-${r.prop.toLowerCase()}`,o=r.parent?.some(e=>"decl"===e.type&&e.prop.toLowerCase()===t);o||(r.cloneBefore({prop:t}),s.preserve||r.remove())}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-gap-properties/src/index.ts b/plugins/postcss-gap-properties/src/index.ts index 2460c9cd4a..857a927949 100644 --- a/plugins/postcss-gap-properties/src/index.ts +++ b/plugins/postcss-gap-properties/src/index.ts @@ -1,4 +1,4 @@ -import type { PluginCreator } from 'postcss'; +import type { Container, PluginCreator } from 'postcss'; /** postcss-gap-properties plugin options */ export type pluginOptions = { @@ -12,6 +12,34 @@ const gapProperties = [ 'row-gap', ]; +type DisplayGridCacheEntry = { + length: number, + hasDisplayGrid: boolean, +}; + +const displayGridCache = new WeakMap(); + +function parentHasDisplayGrid(parent: Container): boolean { + const nodes = parent.nodes || []; + const entry = displayGridCache.get(parent); + + if (entry && entry.length === nodes.length) { + return entry.hasDisplayGrid; + } + + const hasDisplayGrid = nodes.some((node) => { + if (node.type !== 'decl') { + return false; + } + + return node.prop.toLowerCase() === 'display' && node.value.toLowerCase() === 'grid'; + }); + + displayGridCache.set(parent, { length: nodes.length, hasDisplayGrid }); + + return hasDisplayGrid; +} + const creator: PluginCreator = (opts?: pluginOptions) => { const options = Object.assign( // Default options @@ -30,14 +58,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - const isNotDisplayGrid = !(decl.parent?.some((node) => { - if (node.type !== 'decl') { - return false; - } - - return node.prop.toLowerCase() === 'display' && node.value.toLowerCase() === 'grid'; - })); - if (isNotDisplayGrid) { + if (!decl.parent || !parentHasDisplayGrid(decl.parent)) { return; } diff --git a/plugins/postcss-is-pseudo-class/CHANGELOG.md b/plugins/postcss-is-pseudo-class/CHANGELOG.md index 998f789664..3c53de047e 100644 --- a/plugins/postcss-is-pseudo-class/CHANGELOG.md +++ b/plugins/postcss-is-pseudo-class/CHANGELOG.md @@ -1,5 +1,10 @@ # Changes to PostCSS Is Pseudo Class +### Unreleased (patch) + +- Bound the total number of generated selectors per rule and insert generated rules in a single batch. + + ### 6.0.1 _September 25, 2026_ diff --git a/plugins/postcss-is-pseudo-class/dist/index.mjs b/plugins/postcss-is-pseudo-class/dist/index.mjs index c869eea110..67f9b2696f 100644 --- a/plugins/postcss-is-pseudo-class/dist/index.mjs +++ b/plugins/postcss-is-pseudo-class/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-selector-parser";import{selectorSpecificity as s}from"@csstools/selector-specificity";function alwaysValidSelector(s){const o=e().astSync(s);let n=!0;return o.walk(e=>{if("class"!==e.type&&"comment"!==e.type&&"id"!==e.type&&"root"!==e.type&&"selector"!==e.type&&"string"!==e.type&&"tag"!==e.type&&"universal"!==e.type&&("attribute"!==e.type||e.insensitive)&&("combinator"!==e.type||"+"!==e.value&&">"!==e.value&&"~"!==e.value&&" "!==e.value)&&("pseudo"!==e.type||e.nodes?.length||":hover"!==e.value.toLowerCase()&&":focus"!==e.value.toLowerCase())){if("pseudo"===e.type&&1===e.nodes?.length&&":not"===e.value.toLowerCase()){let s=!0;if(e.nodes[0].walkCombinators(()=>{s=!1}),s)return}return n=!1,!1}}),n}function sortCompoundSelectorsInsideComplexSelector(s){if(!s||!s.nodes||1===s.nodes.length)return;const o=[];let n=[];for(let t=0;t"selector"===e.type&&"selector"===s.type&&e.nodes.length&&s.nodes.length?selectorTypeOrder(e.nodes[0],e.nodes[0].type)-selectorTypeOrder(s.nodes[0],s.nodes[0].type):"selector"===e.type&&e.nodes.length?selectorTypeOrder(e.nodes[0],e.nodes[0].type)-selectorTypeOrder(s,s.type):"selector"===s.type&&s.nodes.length?selectorTypeOrder(e,e.type)-selectorTypeOrder(s.nodes[0],s.nodes[0].type):selectorTypeOrder(e,e.type)-selectorTypeOrder(s,s.type));const n=new Set(s.map(e=>e.type)),r=n.has("universal")&&(n.has("tag")||n.has("attribute")||n.has("class")||n.has("id")||n.has("pseudo"));for(let e=0;e=0;o--){const n=t[o-1];if(t[o].remove(),n&&"tag"===n.type&&"tag"===t[o].type){const n=e.pseudo({value:":is",nodes:[e.selector({value:"",nodes:[t[o]]})]});n.parent=s,s.nodes.unshift(n)}else t[o].parent=s,s.nodes.unshift(t[o])}}function selectorTypeOrder(s,n){return e.isPseudoElement(s)?o.pseudoElement:o[n]}const o={universal:0,tag:1,pseudoElement:2,id:3,class:4,attribute:5,pseudo:6,selector:7,string:8,root:9,comment:10};function childAdjacentChild(e){return!(!e||!e.nodes)&&("selector"===e.type&&(3===e.nodes.length&&(!(!e.nodes[0]||"pseudo"!==e.nodes[0].type||":-csstools-matches"!==e.nodes[0].value)&&(!(!e.nodes[1]||"combinator"!==e.nodes[1].type||"+"!==e.nodes[1].value&&"~"!==e.nodes[1].value)&&(!(!e.nodes[2]||"pseudo"!==e.nodes[2].type||":-csstools-matches"!==e.nodes[2].value)&&(!(!e.nodes[0].nodes||1!==e.nodes[0].nodes.length)&&("selector"===e.nodes[0].nodes[0].type&&(!(!e.nodes[0].nodes[0].nodes||3!==e.nodes[0].nodes[0].nodes.length)&&(!(!e.nodes[0].nodes[0].nodes||"combinator"!==e.nodes[0].nodes[0].nodes[1].type||">"!==e.nodes[0].nodes[0].nodes[1].value)&&(!(!e.nodes[2].nodes||1!==e.nodes[2].nodes.length)&&("selector"===e.nodes[2].nodes[0].type&&(!(!e.nodes[2].nodes[0].nodes||3!==e.nodes[2].nodes[0].nodes.length)&&(!(!e.nodes[2].nodes[0].nodes||"combinator"!==e.nodes[2].nodes[0].nodes[1].type||">"!==e.nodes[2].nodes[0].nodes[1].value)&&(e.nodes[0].nodes[0].insertAfter(e.nodes[0].nodes[0].nodes[0],e.nodes[2].nodes[0].nodes[0].clone()),e.nodes[2].nodes[0].nodes[1].remove(),e.nodes[2].nodes[0].nodes[0].remove(),e.nodes[0].replaceWith(e.nodes[0].nodes[0]),e.nodes[2].replaceWith(e.nodes[2].nodes[0]),!0))))))))))))))}function isInCompoundWithOneOtherElement(s){if(!s||!s.nodes)return!1;if(!e.isSelector(s))return!1;if(2!==s.nodes.length)return!1;let o=-1,n=-1;s.nodes[0]&&e.isPseudoClass(s.nodes[0])&&":-csstools-matches"===s.nodes[0].value?(o=0,n=1):s.nodes[1]&&e.isPseudoClass(s.nodes[1])&&":-csstools-matches"===s.nodes[1].value&&(o=1,n=0);const t=s.nodes[o];if(!t||!e.isPseudoClass(t)||1!==t.nodes.length)return!1;const r=s.nodes[n];return!!r&&(!e.isCombinator(r)&&(!e.isPseudoElement(r)&&(t.nodes[0].append(r.clone()),t.replaceWith(...t.nodes[0].nodes),r.remove(),!0)))}function isPseudoInFirstCompound(s){if(!s||!s.nodes)return!1;if(!e.isSelector(s))return!1;let o=-1;for(let n=0;n{n.nodes[0].append(e.clone())}),r.forEach(e=>{n.nodes[0].append(e.clone())}),n.replaceWith(...n.nodes),t.forEach(e=>{e.remove()}),r.forEach(e=>{e.remove()}),!0}function samePrecedingElement(s){if(!s||!s.nodes)return!1;if("selector"!==s.type)return!1;let o=-1;for(let n=0;n"!==s.nodes[o].value&&"+"!==s.nodes[o].value)return!1;const t=s.nodes[o].value;if(!s.nodes[n]||"pseudo"!==s.nodes[n].type||":-csstools-matches"!==s.nodes[n].value)return!1;if(!s.nodes[n].nodes||1!==s.nodes[n].nodes.length)return!1;if("selector"!==s.nodes[n].nodes[0].type)return!1;if(!s.nodes[n].nodes[0].nodes||3!==s.nodes[n].nodes[0].nodes.length)return!1;if(!s.nodes[n].nodes[0].nodes||"combinator"!==s.nodes[n].nodes[0].nodes[1].type||s.nodes[n].nodes[0].nodes[1].value!==t)return!1;const r=s.nodes[n];if(!r||!e.isPseudoClass(r))return!1;const d=s.nodes.slice(0,o),l=s.nodes.slice(n+1);return s.each(e=>{e.remove()}),d.forEach(e=>{s.append(e)}),r.nodes[0].nodes.forEach(e=>{s.append(e)}),l.forEach(e=>{s.append(e)}),!0}function complexSelectors(s,o,n,t){return s.flatMap(s=>{if(-1===s.indexOf(":-csstools-matches")&&-1===s.toLowerCase().indexOf(":is"))return s;const r=e().astSync(s);return r.walkPseudos(s=>{if(":is"===s.value.toLowerCase()&&s.nodes&&s.nodes.length&&"selector"===s.nodes[0].type&&0===s.nodes[0].nodes.length)return s.value=":not",void s.nodes[0].append(e.universal());if(":-csstools-matches"===s.value)if(!s.nodes||s.nodes.length){if(s.walkPseudos(s=>{if(e.isPseudoElement(s)){let e=s.value;if(e.startsWith("::-csstools-invalid-"))return;for(;e.startsWith(":");)e=e.slice(1);s.value=`::-csstools-invalid-${e}`,t()}}),1===s.nodes.length&&"selector"===s.nodes[0].type){if(1===s.nodes[0].nodes.length)return void s.replaceWith(s.nodes[0].nodes[0]);if(!s.nodes[0].some(e=>"combinator"===e.type))return void s.replaceWith(...s.nodes[0].nodes)}1!==r.nodes.length||"selector"!==r.nodes[0].type||1!==r.nodes[0].nodes.length||r.nodes[0].nodes[0]!==s?childAdjacentChild(s.parent)||isInCompoundWithOneOtherElement(s.parent)||isPseudoInFirstCompound(s.parent)||samePrecedingElement(s.parent)||("warning"===o.onComplexSelector&&n(),s.value=":is"):s.replaceWith(...s.nodes[0].nodes)}else s.remove()}),r.walk(e=>{"selector"===e.type&&"nodes"in e&&1===e.nodes.length&&"selector"===e.nodes[0].type&&e.replaceWith(e.nodes[0])}),r.walk(e=>{"nodes"in e&&sortCompoundSelectorsInsideComplexSelector(e)}),r.toString()}).filter(e=>!!e)}function splitSelectors(o,n,t=0){const r=":not(#"+n.specificityMatchingName+")",d=":not(."+n.specificityMatchingName+")",l=":not("+n.specificityMatchingName+")";return o.flatMap(o=>{if(-1===o.toLowerCase().indexOf(":is"))return o;let i=!1;const a=[];if(e().astSync(o).walkPseudos(e=>{if(":is"!==e.value.toLowerCase()||!e.nodes||!e.nodes.length)return;if("selector"===e.nodes[0].type&&0===e.nodes[0].nodes.length)return;if("pseudo"===e.parent?.parent?.type&&":not"===e.parent?.parent?.value?.toLowerCase())return void a.push([{start:e.parent.parent.sourceIndex,end:e.parent.parent.sourceIndex+e.parent.parent.toString().length,option:`:not(${e.nodes.toString()})`}]);if("pseudo"===e.parent?.parent?.type&&":has"===e.parent?.parent?.value?.toLowerCase())return void(e.value=":-csstools-matches");let o=e.parent;for(;o;){if(o.value&&":is"===o.value.toLowerCase()&&"pseudo"===o.type)return void(i=!0);o=o.parent}const n=s(e),t=e.sourceIndex,c=t+e.toString().length,p=[];e.nodes.forEach(e=>{const o={start:t,end:c,option:""},i=s(e);let a=e.toString().trim();const u=Math.max(0,n.a-i.a),h=Math.max(0,n.b-i.b),f=Math.max(0,n.c-i.c);for(let e=0;e{let s="";for(let n=0;n!!e)}const n=1e4;function cartesianProduct(...e){let s=1;for(let o=0;on)throw new Error("Too many combinations when trying to resolve a selector with `:is()` lists, reduce the complexity of your selectors");const o=[],t=e.length-1;return function helper(s,n){for(let r=0,d=e[n].length;r{const s={specificityMatchingName:"does-not-exist",...e||{}};return{postcssPlugin:"postcss-is-pseudo-class",prepare(){const e=new WeakSet;return{postcssPlugin:"postcss-is-pseudo-class",Rule(o,{result:n}){if(!o.selector)return;if(!t.test(o.selector))return;if(e.has(o))return;let r=!1;const warnOnComplexSelector=()=>{"warning"===s.onComplexSelector&&(r||(r=!0,o.warn(n,`Complex selectors in '${o.selector}' can not be transformed to an equivalent selector without ':is()'.`)))};let d=!1;const warnOnPseudoElements=()=>{"warning"===s.onPseudoElement&&(d||(d=!0,o.warn(n,`Pseudo elements are not allowed in ':is()', unable to transform '${o.selector}'`)))};try{let n=!1;const t=[],r=complexSelectors(splitSelectors(o.selectors,{specificityMatchingName:s.specificityMatchingName}),{onComplexSelector:s.onComplexSelector},warnOnComplexSelector,warnOnPseudoElements);if(Array.from(new Set(r)).forEach(s=>{if(o.selectors.indexOf(s)>-1)t.push(s);else{if(alwaysValidSelector(s))return t.push(s),void(n=!0);e.add(o),o.cloneBefore({selector:s}),n=!0}}),t.length&&n&&(e.add(o),o.cloneBefore({selectors:t})),!s.preserve){if(!n)return;o.remove()}}catch(e){if(!(e instanceof Error))throw e;if(e.message.indexOf("call stack size exceeded")>-1)throw e;o.warn(n,`Failed to parse selector "${o.selector}" with error: ${e.message}`)}}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import e from"postcss-selector-parser";import{selectorSpecificity as s}from"@csstools/selector-specificity";function alwaysValidSelector(s){const o=e().astSync(s);let n=!0;return o.walk(e=>{if("class"!==e.type&&"comment"!==e.type&&"id"!==e.type&&"root"!==e.type&&"selector"!==e.type&&"string"!==e.type&&"tag"!==e.type&&"universal"!==e.type&&("attribute"!==e.type||e.insensitive)&&("combinator"!==e.type||"+"!==e.value&&">"!==e.value&&"~"!==e.value&&" "!==e.value)&&("pseudo"!==e.type||e.nodes?.length||":hover"!==e.value.toLowerCase()&&":focus"!==e.value.toLowerCase())){if("pseudo"===e.type&&1===e.nodes?.length&&":not"===e.value.toLowerCase()){let s=!0;if(e.nodes[0].walkCombinators(()=>{s=!1}),s)return}return n=!1,!1}}),n}function sortCompoundSelectorsInsideComplexSelector(s){if(!s||!s.nodes||1===s.nodes.length)return;const o=[];let n=[];for(let t=0;t"selector"===e.type&&"selector"===s.type&&e.nodes.length&&s.nodes.length?selectorTypeOrder(e.nodes[0],e.nodes[0].type)-selectorTypeOrder(s.nodes[0],s.nodes[0].type):"selector"===e.type&&e.nodes.length?selectorTypeOrder(e.nodes[0],e.nodes[0].type)-selectorTypeOrder(s,s.type):"selector"===s.type&&s.nodes.length?selectorTypeOrder(e,e.type)-selectorTypeOrder(s.nodes[0],s.nodes[0].type):selectorTypeOrder(e,e.type)-selectorTypeOrder(s,s.type));const n=new Set(s.map(e=>e.type)),r=n.has("universal")&&(n.has("tag")||n.has("attribute")||n.has("class")||n.has("id")||n.has("pseudo"));for(let e=0;e=0;o--){const n=t[o-1];if(t[o].remove(),n&&"tag"===n.type&&"tag"===t[o].type){const n=e.pseudo({value:":is",nodes:[e.selector({value:"",nodes:[t[o]]})]});n.parent=s,s.nodes.unshift(n)}else t[o].parent=s,s.nodes.unshift(t[o])}}function selectorTypeOrder(s,n){return e.isPseudoElement(s)?o.pseudoElement:o[n]}const o={universal:0,tag:1,pseudoElement:2,id:3,class:4,attribute:5,pseudo:6,selector:7,string:8,root:9,comment:10};function childAdjacentChild(e){return!(!e||!e.nodes)&&("selector"===e.type&&(3===e.nodes.length&&(!(!e.nodes[0]||"pseudo"!==e.nodes[0].type||":-csstools-matches"!==e.nodes[0].value)&&(!(!e.nodes[1]||"combinator"!==e.nodes[1].type||"+"!==e.nodes[1].value&&"~"!==e.nodes[1].value)&&(!(!e.nodes[2]||"pseudo"!==e.nodes[2].type||":-csstools-matches"!==e.nodes[2].value)&&(!(!e.nodes[0].nodes||1!==e.nodes[0].nodes.length)&&("selector"===e.nodes[0].nodes[0].type&&(!(!e.nodes[0].nodes[0].nodes||3!==e.nodes[0].nodes[0].nodes.length)&&(!(!e.nodes[0].nodes[0].nodes||"combinator"!==e.nodes[0].nodes[0].nodes[1].type||">"!==e.nodes[0].nodes[0].nodes[1].value)&&(!(!e.nodes[2].nodes||1!==e.nodes[2].nodes.length)&&("selector"===e.nodes[2].nodes[0].type&&(!(!e.nodes[2].nodes[0].nodes||3!==e.nodes[2].nodes[0].nodes.length)&&(!(!e.nodes[2].nodes[0].nodes||"combinator"!==e.nodes[2].nodes[0].nodes[1].type||">"!==e.nodes[2].nodes[0].nodes[1].value)&&(e.nodes[0].nodes[0].insertAfter(e.nodes[0].nodes[0].nodes[0],e.nodes[2].nodes[0].nodes[0].clone()),e.nodes[2].nodes[0].nodes[1].remove(),e.nodes[2].nodes[0].nodes[0].remove(),e.nodes[0].replaceWith(e.nodes[0].nodes[0]),e.nodes[2].replaceWith(e.nodes[2].nodes[0]),!0))))))))))))))}function isInCompoundWithOneOtherElement(s){if(!s||!s.nodes)return!1;if(!e.isSelector(s))return!1;if(2!==s.nodes.length)return!1;let o=-1,n=-1;s.nodes[0]&&e.isPseudoClass(s.nodes[0])&&":-csstools-matches"===s.nodes[0].value?(o=0,n=1):s.nodes[1]&&e.isPseudoClass(s.nodes[1])&&":-csstools-matches"===s.nodes[1].value&&(o=1,n=0);const t=s.nodes[o];if(!t||!e.isPseudoClass(t)||1!==t.nodes.length)return!1;const r=s.nodes[n];return!!r&&(!e.isCombinator(r)&&(!e.isPseudoElement(r)&&(t.nodes[0].append(r.clone()),t.replaceWith(...t.nodes[0].nodes),r.remove(),!0)))}function isPseudoInFirstCompound(s){if(!s||!s.nodes)return!1;if(!e.isSelector(s))return!1;let o=-1;for(let n=0;n{n.nodes[0].append(e.clone())}),r.forEach(e=>{n.nodes[0].append(e.clone())}),n.replaceWith(...n.nodes),t.forEach(e=>{e.remove()}),r.forEach(e=>{e.remove()}),!0}function samePrecedingElement(s){if(!s||!s.nodes)return!1;if("selector"!==s.type)return!1;let o=-1;for(let n=0;n"!==s.nodes[o].value&&"+"!==s.nodes[o].value)return!1;const t=s.nodes[o].value;if(!s.nodes[n]||"pseudo"!==s.nodes[n].type||":-csstools-matches"!==s.nodes[n].value)return!1;if(!s.nodes[n].nodes||1!==s.nodes[n].nodes.length)return!1;if("selector"!==s.nodes[n].nodes[0].type)return!1;if(!s.nodes[n].nodes[0].nodes||3!==s.nodes[n].nodes[0].nodes.length)return!1;if(!s.nodes[n].nodes[0].nodes||"combinator"!==s.nodes[n].nodes[0].nodes[1].type||s.nodes[n].nodes[0].nodes[1].value!==t)return!1;const r=s.nodes[n];if(!r||!e.isPseudoClass(r))return!1;const d=s.nodes.slice(0,o),l=s.nodes.slice(n+1);return s.each(e=>{e.remove()}),d.forEach(e=>{s.append(e)}),r.nodes[0].nodes.forEach(e=>{s.append(e)}),l.forEach(e=>{s.append(e)}),!0}function complexSelectors(s,o,n,t){return s.flatMap(s=>{if(-1===s.indexOf(":-csstools-matches")&&-1===s.toLowerCase().indexOf(":is"))return s;const r=e().astSync(s);return r.walkPseudos(s=>{if(":is"===s.value.toLowerCase()&&s.nodes&&s.nodes.length&&"selector"===s.nodes[0].type&&0===s.nodes[0].nodes.length)return s.value=":not",void s.nodes[0].append(e.universal());if(":-csstools-matches"===s.value)if(!s.nodes||s.nodes.length){if(s.walkPseudos(s=>{if(e.isPseudoElement(s)){let e=s.value;if(e.startsWith("::-csstools-invalid-"))return;for(;e.startsWith(":");)e=e.slice(1);s.value=`::-csstools-invalid-${e}`,t()}}),1===s.nodes.length&&"selector"===s.nodes[0].type){if(1===s.nodes[0].nodes.length)return void s.replaceWith(s.nodes[0].nodes[0]);if(!s.nodes[0].some(e=>"combinator"===e.type))return void s.replaceWith(...s.nodes[0].nodes)}1!==r.nodes.length||"selector"!==r.nodes[0].type||1!==r.nodes[0].nodes.length||r.nodes[0].nodes[0]!==s?childAdjacentChild(s.parent)||isInCompoundWithOneOtherElement(s.parent)||isPseudoInFirstCompound(s.parent)||samePrecedingElement(s.parent)||("warning"===o.onComplexSelector&&n(),s.value=":is"):s.replaceWith(...s.nodes[0].nodes)}else s.remove()}),r.walk(e=>{"selector"===e.type&&"nodes"in e&&1===e.nodes.length&&"selector"===e.nodes[0].type&&e.replaceWith(e.nodes[0])}),r.walk(e=>{"nodes"in e&&sortCompoundSelectorsInsideComplexSelector(e)}),r.toString()}).filter(e=>!!e)}function splitSelectors(o,n,r=0,d={remaining:t}){const l=":not(#"+n.specificityMatchingName+")",i=":not(."+n.specificityMatchingName+")",a=":not("+n.specificityMatchingName+")";return o.flatMap(o=>{if(-1===o.toLowerCase().indexOf(":is"))return o;let t=!1;const c=[];if(e().astSync(o).walkPseudos(e=>{if(":is"!==e.value.toLowerCase()||!e.nodes||!e.nodes.length)return;if("selector"===e.nodes[0].type&&0===e.nodes[0].nodes.length)return;if("pseudo"===e.parent?.parent?.type&&":not"===e.parent?.parent?.value?.toLowerCase())return void c.push([{start:e.parent.parent.sourceIndex,end:e.parent.parent.sourceIndex+e.parent.parent.toString().length,option:`:not(${e.nodes.toString()})`}]);if("pseudo"===e.parent?.parent?.type&&":has"===e.parent?.parent?.value?.toLowerCase())return void(e.value=":-csstools-matches");let o=e.parent;for(;o;){if(o.value&&":is"===o.value.toLowerCase()&&"pseudo"===o.type)return void(t=!0);o=o.parent}const n=s(e),r=e.sourceIndex,d=r+e.toString().length,p=[];e.nodes.forEach(e=>{const o={start:r,end:d,option:""},t=s(e);let c=e.toString().trim();const u=Math.max(0,n.a-t.a),h=Math.max(0,n.b-t.b),f=Math.max(0,n.c-t.c);for(let e=0;e{let s="";for(let n=0;n!!e)}const n=1e4,t=1e5;function cartesianProduct(...e){let s=1;for(let o=0;on)throw new Error("Too many combinations when trying to resolve a selector with `:is()` lists, reduce the complexity of your selectors");const o=[],t=e.length-1;return function helper(s,n){for(let r=0,d=e[n].length;r{const s={specificityMatchingName:"does-not-exist",...e||{}};return{postcssPlugin:"postcss-is-pseudo-class",prepare(){const e=new WeakSet;return{postcssPlugin:"postcss-is-pseudo-class",Rule(o,{result:n}){if(!o.selector)return;if(!r.test(o.selector))return;if(e.has(o))return;let t=!1;const warnOnComplexSelector=()=>{"warning"===s.onComplexSelector&&(t||(t=!0,o.warn(n,`Complex selectors in '${o.selector}' can not be transformed to an equivalent selector without ':is()'.`)))};let d=!1;const warnOnPseudoElements=()=>{"warning"===s.onPseudoElement&&(d||(d=!0,o.warn(n,`Pseudo elements are not allowed in ':is()', unable to transform '${o.selector}'`)))};try{let n=!1;const t=[],r=complexSelectors(splitSelectors(o.selectors,{specificityMatchingName:s.specificityMatchingName}),{onComplexSelector:s.onComplexSelector},warnOnComplexSelector,warnOnPseudoElements),d=Array.from(new Set(r)),l=new Set(o.selectors),i=[];d.forEach(e=>{if(l.has(e))t.push(e);else{if(alwaysValidSelector(e))return t.push(e),void(n=!0);i.push(e),n=!0}});const a=i.map(e=>o.clone({selector:e}));if(t.length&&n&&a.push(o.clone({selectors:t})),a.length&&(e.add(o),o.before(a)),!s.preserve){if(!n)return;o.remove()}}catch(e){if(!(e instanceof Error))throw e;if(e.message.indexOf("call stack size exceeded")>-1)throw e;o.warn(n,`Failed to parse selector "${o.selector}" with error: ${e.message}`)}}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-is-pseudo-class/src/index.ts b/plugins/postcss-is-pseudo-class/src/index.ts index 76461d3a8a..b97cc273f4 100644 --- a/plugins/postcss-is-pseudo-class/src/index.ts +++ b/plugins/postcss-is-pseudo-class/src/index.ts @@ -1,4 +1,4 @@ -import type { Plugin, PluginCreator } from 'postcss'; +import type { Plugin, PluginCreator, Rule } from 'postcss'; import alwaysValidSelector from './split-selectors/always-valid'; import complexSelectors from './split-selectors/complex'; import splitSelectors from './split-selectors/split-selectors'; @@ -106,12 +106,16 @@ const creator: PluginCreator = (opts?: pluginOptions) => { const uniqueResolvedComplexSelectors = Array.from(new Set(resolvedComplexSelectors)); // 4. Replace. + // `rule.selectors` re-splits the selector string on every access. + const originalSelectors = new Set(rule.selectors); + const clonedSelectors: Array = []; + uniqueResolvedComplexSelectors.forEach((modifiedSelector) => { // `::is()` is incorrect but can't be detected without parsing. // It will be left as is and will eventually trigger this condition. // This prevents an infinite loop. // didModify is the signal to prevent the infinite loop. - if (rule.selectors.indexOf(modifiedSelector) > -1) { + if (originalSelectors.has(modifiedSelector)) { selectorListOnOriginalNode.push(modifiedSelector); return; } @@ -122,14 +126,21 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - transformedNodes.add(rule); - rule.cloneBefore({ selector: modifiedSelector }); + clonedSelectors.push(modifiedSelector); didModify = true; }); + // Insert as a batch: resolving the reference index for every + // single insertion is quadratic. + const clones: Array = clonedSelectors.map((modifiedSelector) => rule.clone({ selector: modifiedSelector })); + if (selectorListOnOriginalNode.length && didModify) { + clones.push(rule.clone({ selectors: selectorListOnOriginalNode })); + } + + if (clones.length) { transformedNodes.add(rule); - rule.cloneBefore({ selectors: selectorListOnOriginalNode }); + rule.before(clones); } if (!options.preserve) { diff --git a/plugins/postcss-is-pseudo-class/src/split-selectors/split-selectors.ts b/plugins/postcss-is-pseudo-class/src/split-selectors/split-selectors.ts index 87683a8334..bc3064058c 100644 --- a/plugins/postcss-is-pseudo-class/src/split-selectors/split-selectors.ts +++ b/plugins/postcss-is-pseudo-class/src/split-selectors/split-selectors.ts @@ -4,7 +4,7 @@ import { selectorSpecificity } from '@csstools/selector-specificity'; // splitSelectors handles the forgiving list behavior of ":is". // After created all combinations it wraps the individual selectors in ":-csstools-matches". // This makes it easy to recursively resolve all ":is" selectors without infinite loops. -export default function splitSelectors(selectors: Array, pluginOptions: { specificityMatchingName: string }, recursionDepth = 0): Array { +export default function splitSelectors(selectors: Array, pluginOptions: { specificityMatchingName: string }, recursionDepth = 0, budget: { remaining: number } = { remaining: MAX_TOTAL_COMBINATIONS }): Array { const specificityMatchingNameId = ':not(#' + pluginOptions.specificityMatchingName + ')'; const specificityMatchingNameClass = ':not(.' + pluginOptions.specificityMatchingName + ')'; const specificityMatchingNameTag = ':not(' + pluginOptions.specificityMatchingName + ')'; @@ -99,6 +99,20 @@ export default function splitSelectors(selectors: Array, pluginOptions: } let results: Array = []; + + // The per-`:is()` combination cap only bounds a single selector group. + // A rule with several selectors multiplies the total work, so bound the + // total number of generated selectors for the whole rule as well. + let combinations = 1; + for (let i = 0; i < replacements.length; i++) { + combinations *= replacements[i].length; + } + + budget.remaining -= combinations; + if (budget.remaining < 0) { + throw new Error('Too many combinations when trying to resolve a selector with `:is()` lists, reduce the complexity of your selectors'); + } + cartesianProduct(...replacements).forEach((replacement) => { let result = ''; @@ -118,7 +132,7 @@ export default function splitSelectors(selectors: Array, pluginOptions: if (foundNestedIs && recursionDepth < 10) { // recursion to transform `:is(a :is(b,c))` - results = splitSelectors(results, pluginOptions, recursionDepth + 1); + results = splitSelectors(results, pluginOptions, recursionDepth + 1, budget); } return results; @@ -132,6 +146,9 @@ export default function splitSelectors(selectors: Array, pluginOptions: // bound a small selector can exhaust memory and CPU. const MAX_SELECTOR_COMBINATIONS = 10_000; +// The maximum number of generated selectors for a single rule. +const MAX_TOTAL_COMBINATIONS = 100_000; + // https://en.wikipedia.org/wiki/Cartesian_product function cartesianProduct(...args: Array>): Array> { let total = 1; diff --git a/plugins/postcss-logical-float-and-clear/CHANGELOG.md b/plugins/postcss-logical-float-and-clear/CHANGELOG.md index f845f1cc14..694d49a8d5 100644 --- a/plugins/postcss-logical-float-and-clear/CHANGELOG.md +++ b/plugins/postcss-logical-float-and-clear/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Logical Float And Clear +### Unreleased (patch) + +- Improved performance of declaration deduplication by caching declarations per parent + ### 4.0.0 _January 14, 2026_ diff --git a/plugins/postcss-logical-float-and-clear/dist/index.mjs b/plugins/postcss-logical-float-and-clear/dist/index.mjs index 80eed07ede..573b4a0e02 100644 --- a/plugins/postcss-logical-float-and-clear/dist/index.mjs +++ b/plugins/postcss-logical-float-and-clear/dist/index.mjs @@ -1 +1 @@ -const t="inline-start",o="inline-end";var e,n;function directionFlowToAxes(t){switch(t){case e.TopToBottom:return[n.Top,n.Bottom];case e.BottomToTop:return[n.Bottom,n.Top];case e.RightToLeft:return[n.Right,n.Left];case e.LeftToRight:return[n.Left,n.Right]}}function cloneDeclaration(t,o,e){t.parent&&t.parent.some(t=>"decl"===t.type&&t.prop===e&&t.value===o)||t.cloneBefore({value:o,prop:e})}!function(t){t.TopToBottom="top-to-bottom",t.BottomToTop="bottom-to-top",t.RightToLeft="right-to-left",t.LeftToRight="left-to-right"}(e||(e={})),function(t){t.Top="top",t.Right="right",t.Bottom="bottom",t.Left="left"}(n||(n={}));const creator=n=>{const i=Object.assign({inlineDirection:e.LeftToRight},n),c=Object.values(e);if(!c.includes(i.inlineDirection))throw new Error(`[postcss-logical-float-and-clear] "inlineDirection" must be one of ${c.join(", ")}`);if(![e.LeftToRight,e.RightToLeft].includes(i.inlineDirection))return{postcssPlugin:"postcss-logical-float-and-clear",Once(){}};const[r,l]=directionFlowToAxes(i.inlineDirection),makeTransform=e=>{const n=e.value.toLowerCase();if(![t,o].includes(n))return;cloneDeclaration(e,n===t?r:l,e.prop),e.remove()};return{postcssPlugin:"postcss-logical-float-and-clear",Declaration:{float:makeTransform,clear:makeTransform}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +const t="inline-start",o="inline-end";var e,n;function directionFlowToAxes(t){switch(t){case e.TopToBottom:return[n.Top,n.Bottom];case e.BottomToTop:return[n.Bottom,n.Top];case e.RightToLeft:return[n.Right,n.Left];case e.LeftToRight:return[n.Left,n.Right]}}!function(t){t.TopToBottom="top-to-bottom",t.BottomToTop="bottom-to-top",t.RightToLeft="right-to-left",t.LeftToRight="left-to-right"}(e||(e={})),function(t){t.Top="top",t.Right="right",t.Bottom="bottom",t.Left="left"}(n||(n={}));const i=new WeakMap;function declarationKey(t,o){return`${t}\0${o}`}function getDeclarationKeys(t){const o=t.nodes||[];let e=i.get(t);if(!e||e.length!==o.length){e={length:o.length,keys:new Set};for(const t of o)"decl"===t.type&&e.keys.add(declarationKey(t.prop,t.value));i.set(t,e)}return e.keys}function cloneDeclaration(t,o,e){const n=t.parent;if(n){const t=getDeclarationKeys(n),i=declarationKey(e,o);if(t.has(i))return;t.add(i)}t.before(t.clone({value:o,prop:e}))}const creator=n=>{const i=Object.assign({inlineDirection:e.LeftToRight},n),c=Object.values(e);if(!c.includes(i.inlineDirection))throw new Error(`[postcss-logical-float-and-clear] "inlineDirection" must be one of ${c.join(", ")}`);if(![e.LeftToRight,e.RightToLeft].includes(i.inlineDirection))return{postcssPlugin:"postcss-logical-float-and-clear",Once(){}};const[r,s]=directionFlowToAxes(i.inlineDirection),makeTransform=e=>{const n=e.value.toLowerCase();if(![t,o].includes(n))return;cloneDeclaration(e,n===t?r:s,e.prop),e.remove()};return{postcssPlugin:"postcss-logical-float-and-clear",Declaration:{float:makeTransform,clear:makeTransform}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-logical-float-and-clear/src/lib/clone-declaration.ts b/plugins/postcss-logical-float-and-clear/src/lib/clone-declaration.ts index 4fe79ba551..6b5295d1af 100644 --- a/plugins/postcss-logical-float-and-clear/src/lib/clone-declaration.ts +++ b/plugins/postcss-logical-float-and-clear/src/lib/clone-declaration.ts @@ -1,15 +1,58 @@ -import type { Declaration } from 'postcss'; +import type { Container, Declaration } from 'postcss'; + +type DeclarationCacheEntry = { + length: number, + keys: Set, +}; + +const declarationCache = new WeakMap(); + +function declarationKey(prop: string, value: string): string { + return `${prop}\0${value}`; +} + +function getDeclarationKeys(parent: Container): Set { + const nodes = parent.nodes || []; + let entry = declarationCache.get(parent); + + if (!entry || entry.length !== nodes.length) { + entry = { + length: nodes.length, + keys: new Set(), + }; + + for (const node of nodes) { + if (node.type === 'decl') { + entry.keys.add(declarationKey(node.prop, node.value)); + } + } + + declarationCache.set(parent, entry); + } + + return entry.keys; +} export function cloneDeclaration( declaration: Declaration, value: string, prop: string, ): void { - if (declaration.parent && declaration.parent.some((childNode) => { - return childNode.type === 'decl' && childNode.prop === prop && childNode.value === value; - })) { - return; + const parent = declaration.parent; + + if (parent) { + const keys = getDeclarationKeys(parent); + const key = declarationKey(prop, value); + + if (keys.has(key)) { + return; + } + + // The clone is inserted into the same parent below. + // Record it now so later declarations in this parent still dedupe + // against it when the parent length happens to stay the same. + keys.add(key); } - declaration.cloneBefore({ value, prop }); + declaration.before(declaration.clone({ value, prop })); } diff --git a/plugins/postcss-logical-overflow/CHANGELOG.md b/plugins/postcss-logical-overflow/CHANGELOG.md index eb633e57f4..60506a1e86 100644 --- a/plugins/postcss-logical-overflow/CHANGELOG.md +++ b/plugins/postcss-logical-overflow/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Logical Overflow +### Unreleased (patch) + +- Improved performance of declaration deduplication by caching declarations per parent + ### 3.0.0 _January 14, 2026_ diff --git a/plugins/postcss-logical-overflow/dist/index.mjs b/plugins/postcss-logical-overflow/dist/index.mjs index b6da215282..012d4b18c9 100644 --- a/plugins/postcss-logical-overflow/dist/index.mjs +++ b/plugins/postcss-logical-overflow/dist/index.mjs @@ -1 +1 @@ -var o;function transformAxes(o,t){const e=t?"-x":"-y",i=t?"-y":"-x",n=o.prop.toLowerCase().replace("-inline",e).replace("-block",i),s=o.value;o.parent?.some(o=>"decl"===o.type&&o.prop===n&&o.value===s)||(o.cloneBefore({prop:n,value:s}),o.remove())}!function(o){o.TopToBottom="top-to-bottom",o.BottomToTop="bottom-to-top",o.RightToLeft="right-to-left",o.LeftToRight="left-to-right"}(o||(o={}));const creator=t=>{const e=Object.assign({inlineDirection:o.LeftToRight},t);switch(e.inlineDirection){case o.LeftToRight:case o.RightToLeft:case o.TopToBottom:case o.BottomToTop:break;default:throw new Error(`[postcss-logical-viewport-units] "inlineDirection" must be one of ${Object.values(o).join(", ")}`)}const i=[o.LeftToRight,o.RightToLeft].includes(e.inlineDirection);return{postcssPlugin:"postcss-logical-overflow",Declaration:{"overflow-block":o=>transformAxes(o,i),"overflow-inline":o=>transformAxes(o,i)}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +var t;!function(t){t.TopToBottom="top-to-bottom",t.BottomToTop="bottom-to-top",t.RightToLeft="right-to-left",t.LeftToRight="left-to-right"}(t||(t={}));const e=new WeakMap;function declarationKey(t,e){return`${t}\0${e}`}function getDeclarationKeys(t){const o=t.nodes||[];let n=e.get(t);if(!n||n.length!==o.length){n={length:o.length,keys:new Set};for(const t of o)"decl"===t.type&&n.keys.add(declarationKey(t.prop,t.value));e.set(t,n)}return n.keys}function transformAxes(t,e){const o=e?"-x":"-y",n=e?"-y":"-x",i=t.prop.toLowerCase().replace("-inline",o).replace("-block",n),s=t.value,r=t.parent;if(r){const t=getDeclarationKeys(r),e=declarationKey(i,s);if(t.has(e))return;t.add(e)}t.before(t.clone({prop:i,value:s})),t.remove()}const creator=e=>{const o=Object.assign({inlineDirection:t.LeftToRight},e);switch(o.inlineDirection){case t.LeftToRight:case t.RightToLeft:case t.TopToBottom:case t.BottomToTop:break;default:throw new Error(`[postcss-logical-viewport-units] "inlineDirection" must be one of ${Object.values(t).join(", ")}`)}const n=[t.LeftToRight,t.RightToLeft].includes(o.inlineDirection);return{postcssPlugin:"postcss-logical-overflow",Declaration:{"overflow-block":t=>transformAxes(t,n),"overflow-inline":t=>transformAxes(t,n)}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-logical-overflow/src/lib/transform-axes.ts b/plugins/postcss-logical-overflow/src/lib/transform-axes.ts index 9017c900c6..af27985851 100644 --- a/plugins/postcss-logical-overflow/src/lib/transform-axes.ts +++ b/plugins/postcss-logical-overflow/src/lib/transform-axes.ts @@ -1,4 +1,37 @@ -import type { Declaration } from 'postcss'; +import type { Container, Declaration } from 'postcss'; + +type DeclarationCacheEntry = { + length: number, + keys: Set, +}; + +const declarationCache = new WeakMap(); + +function declarationKey(prop: string, value: string): string { + return `${prop}\0${value}`; +} + +function getDeclarationKeys(parent: Container): Set { + const nodes = parent.nodes || []; + let entry = declarationCache.get(parent); + + if (!entry || entry.length !== nodes.length) { + entry = { + length: nodes.length, + keys: new Set(), + }; + + for (const node of nodes) { + if (node.type === 'decl') { + entry.keys.add(declarationKey(node.prop, node.value)); + } + } + + declarationCache.set(parent, entry); + } + + return entry.keys; +} export function transformAxes(declaration: Declaration, isHorizontal: boolean): void { const inlineProp = isHorizontal ? '-x' : '-y'; @@ -9,15 +42,21 @@ export function transformAxes(declaration: Declaration, isHorizontal: boolean): const value = declaration.value; - if (declaration.parent?.some((x) => { - return x.type === 'decl' && x.prop === prop && x.value === value; - })) { - return; + const parent = declaration.parent; + if (parent) { + const keys = getDeclarationKeys(parent); + const key = declarationKey(prop, value); + + if (keys.has(key)) { + return; + } + + // The clone will be inserted into the same parent below. + // Record it now so later declarations in this parent still dedupe + // against it when the parent length happens to stay the same. + keys.add(key); } - declaration.cloneBefore({ - prop: prop, - value: value, - }); + declaration.before(declaration.clone({ prop, value })); declaration.remove(); } diff --git a/plugins/postcss-logical-overscroll-behavior/CHANGELOG.md b/plugins/postcss-logical-overscroll-behavior/CHANGELOG.md index bc4a33cf83..760d373197 100644 --- a/plugins/postcss-logical-overscroll-behavior/CHANGELOG.md +++ b/plugins/postcss-logical-overscroll-behavior/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Logical Overscroll Behavior +### Unreleased (patch) + +- Improved performance of declaration deduplication by caching declarations per parent + ### 3.0.0 _January 14, 2026_ diff --git a/plugins/postcss-logical-overscroll-behavior/dist/index.mjs b/plugins/postcss-logical-overscroll-behavior/dist/index.mjs index 9a17c9323a..de64caddc2 100644 --- a/plugins/postcss-logical-overscroll-behavior/dist/index.mjs +++ b/plugins/postcss-logical-overscroll-behavior/dist/index.mjs @@ -1 +1 @@ -var o;function transformAxes(o,t){const e=t?"-x":"-y",i=t?"-y":"-x",r=o.prop.toLowerCase().replace("-inline",e).replace("-block",i),s=o.value;o.parent?.some(o=>"decl"===o.type&&o.prop===r&&o.value===s)||(o.cloneBefore({prop:r,value:s}),o.remove())}!function(o){o.TopToBottom="top-to-bottom",o.BottomToTop="bottom-to-top",o.RightToLeft="right-to-left",o.LeftToRight="left-to-right"}(o||(o={}));const creator=t=>{const e=Object.assign({inlineDirection:o.LeftToRight},t);switch(e.inlineDirection){case o.LeftToRight:case o.RightToLeft:case o.TopToBottom:case o.BottomToTop:break;default:throw new Error(`[postcss-logical-viewport-units] "inlineDirection" must be one of ${Object.values(o).join(", ")}`)}const i=[o.LeftToRight,o.RightToLeft].includes(e.inlineDirection);return{postcssPlugin:"postcss-logical-overscroll-behavior",Declaration:{"overscroll-behavior-block":o=>transformAxes(o,i),"overscroll-behavior-inline":o=>transformAxes(o,i)}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +var e;!function(e){e.TopToBottom="top-to-bottom",e.BottomToTop="bottom-to-top",e.RightToLeft="right-to-left",e.LeftToRight="left-to-right"}(e||(e={}));const o=new WeakMap;function declarationKey(e,o){return`${e}\0${o}`}function getDeclarationKeys(e){const t=e.nodes||[];let n=o.get(e);if(!n||n.length!==t.length){n={length:t.length,keys:new Set};for(const e of t)"decl"===e.type&&n.keys.add(declarationKey(e.prop,e.value));o.set(e,n)}return n.keys}function transformAxes(e,o){const t=o?"-x":"-y",n=o?"-y":"-x",i=e.prop.toLowerCase().replace("-inline",t).replace("-block",n),r=e.value,s=e.parent;if(s){const e=getDeclarationKeys(s),o=declarationKey(i,r);if(e.has(o))return;e.add(o)}e.before(e.clone({prop:i,value:r})),e.remove()}const creator=o=>{const t=Object.assign({inlineDirection:e.LeftToRight},o);switch(t.inlineDirection){case e.LeftToRight:case e.RightToLeft:case e.TopToBottom:case e.BottomToTop:break;default:throw new Error(`[postcss-logical-viewport-units] "inlineDirection" must be one of ${Object.values(e).join(", ")}`)}const n=[e.LeftToRight,e.RightToLeft].includes(t.inlineDirection);return{postcssPlugin:"postcss-logical-overscroll-behavior",Declaration:{"overscroll-behavior-block":e=>transformAxes(e,n),"overscroll-behavior-inline":e=>transformAxes(e,n)}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-logical-overscroll-behavior/src/lib/transform-axes.ts b/plugins/postcss-logical-overscroll-behavior/src/lib/transform-axes.ts index 9017c900c6..af27985851 100644 --- a/plugins/postcss-logical-overscroll-behavior/src/lib/transform-axes.ts +++ b/plugins/postcss-logical-overscroll-behavior/src/lib/transform-axes.ts @@ -1,4 +1,37 @@ -import type { Declaration } from 'postcss'; +import type { Container, Declaration } from 'postcss'; + +type DeclarationCacheEntry = { + length: number, + keys: Set, +}; + +const declarationCache = new WeakMap(); + +function declarationKey(prop: string, value: string): string { + return `${prop}\0${value}`; +} + +function getDeclarationKeys(parent: Container): Set { + const nodes = parent.nodes || []; + let entry = declarationCache.get(parent); + + if (!entry || entry.length !== nodes.length) { + entry = { + length: nodes.length, + keys: new Set(), + }; + + for (const node of nodes) { + if (node.type === 'decl') { + entry.keys.add(declarationKey(node.prop, node.value)); + } + } + + declarationCache.set(parent, entry); + } + + return entry.keys; +} export function transformAxes(declaration: Declaration, isHorizontal: boolean): void { const inlineProp = isHorizontal ? '-x' : '-y'; @@ -9,15 +42,21 @@ export function transformAxes(declaration: Declaration, isHorizontal: boolean): const value = declaration.value; - if (declaration.parent?.some((x) => { - return x.type === 'decl' && x.prop === prop && x.value === value; - })) { - return; + const parent = declaration.parent; + if (parent) { + const keys = getDeclarationKeys(parent); + const key = declarationKey(prop, value); + + if (keys.has(key)) { + return; + } + + // The clone will be inserted into the same parent below. + // Record it now so later declarations in this parent still dedupe + // against it when the parent length happens to stay the same. + keys.add(key); } - declaration.cloneBefore({ - prop: prop, - value: value, - }); + declaration.before(declaration.clone({ prop, value })); declaration.remove(); } diff --git a/plugins/postcss-logical-resize/CHANGELOG.md b/plugins/postcss-logical-resize/CHANGELOG.md index 66cdd8bad3..270faff592 100644 --- a/plugins/postcss-logical-resize/CHANGELOG.md +++ b/plugins/postcss-logical-resize/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Logical Resize +### Unreleased (patch) + +- Improved performance of declaration deduplication by caching declarations per parent + ### 4.0.0 _January 14, 2026_ diff --git a/plugins/postcss-logical-resize/dist/index.mjs b/plugins/postcss-logical-resize/dist/index.mjs index c7945ce83c..04827dd242 100644 --- a/plugins/postcss-logical-resize/dist/index.mjs +++ b/plugins/postcss-logical-resize/dist/index.mjs @@ -1 +1 @@ -import o from"postcss-value-parser";var t,e,i;function cloneDeclaration(o,t,e){o.parent&&o.parent.some(o=>"decl"===o.type&&o.prop===e&&o.value===t)||o.cloneBefore({value:t,prop:e})}function transformResize(t){return e=>{const{prop:i,value:n}=e,r=o(n),c=t.inlineIsHorizontal?"horizontal":"vertical",s=t.inlineIsHorizontal?"vertical":"horizontal";r.nodes.forEach(o=>{if("word"===o.type){const t=o.value.toLowerCase();if("inline"===t)return void(o.value=c);"block"===t&&(o.value=s)}});const l=r.toString();return l!==n&&(cloneDeclaration(e,l,i),!0)}}function directionFlowToAxes(o){switch(o){case e.TopToBottom:return[i.Top,i.Bottom];case e.BottomToTop:return[i.Bottom,i.Top];case e.RightToLeft:return[i.Right,i.Left];case e.LeftToRight:return[i.Left,i.Right]}}!function(o){o.Block="block",o.Inline="inline"}(t||(t={})),function(o){o.TopToBottom="top-to-bottom",o.BottomToTop="bottom-to-top",o.RightToLeft="right-to-left",o.LeftToRight="left-to-right"}(e||(e={})),function(o){o.Top="top",o.Right="right",o.Bottom="bottom",o.Left="left"}(i||(i={}));const creator=o=>{const t=Object.assign({blockDirection:e.TopToBottom,inlineDirection:e.LeftToRight},o),n=Object.values(e);if(!n.includes(t.blockDirection))throw new Error(`[postcss-logical-resize] "blockDirection" must be one of ${n.join(", ")}`);if(!n.includes(t.inlineDirection))throw new Error(`[postcss-logical-resize] "inlineDirection" must be one of ${n.join(", ")}`);const[r,c]=directionFlowToAxes(t.blockDirection),[s,l]=directionFlowToAxes(t.inlineDirection);if(!Object.values(i).every(o=>[r,c,s,l].includes(o)))throw new Error('[postcss-logical-resize] "blockDirection" and "inlineDirection" must be on separate axes');const a={block:[r,c],inline:[s,l],inlineIsHorizontal:[e.LeftToRight,e.RightToLeft].includes(t.inlineDirection)};return{postcssPlugin:"postcss-logical-resize",Declaration:{resize:(u=transformResize(a),(o,{result:t})=>{if(!u)return;let e;try{e=u(o)}catch(e){return void o.warn(t,e instanceof Error?e.message:String(e))}e&&o.remove()})}};var u};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import o from"postcss-value-parser";var t,e,n;!function(o){o.Block="block",o.Inline="inline"}(t||(t={})),function(o){o.TopToBottom="top-to-bottom",o.BottomToTop="bottom-to-top",o.RightToLeft="right-to-left",o.LeftToRight="left-to-right"}(e||(e={})),function(o){o.Top="top",o.Right="right",o.Bottom="bottom",o.Left="left"}(n||(n={}));const i=new WeakMap;function declarationKey(o,t){return`${o}\0${t}`}function getDeclarationKeys(o){const t=o.nodes||[];let e=i.get(o);if(!e||e.length!==t.length){e={length:t.length,keys:new Set};for(const o of t)"decl"===o.type&&e.keys.add(declarationKey(o.prop,o.value));i.set(o,e)}return e.keys}function cloneDeclaration(o,t,e){const n=o.parent;if(n){const o=getDeclarationKeys(n),i=declarationKey(e,t);if(o.has(i))return;o.add(i)}o.before(o.clone({value:t,prop:e}))}function transformResize(t){return e=>{const{prop:n,value:i}=e,r=o(i),c=t.inlineIsHorizontal?"horizontal":"vertical",s=t.inlineIsHorizontal?"vertical":"horizontal";r.nodes.forEach(o=>{if("word"===o.type){const t=o.value.toLowerCase();if("inline"===t)return void(o.value=c);"block"===t&&(o.value=s)}});const l=r.toString();return l!==i&&(cloneDeclaration(e,l,n),!0)}}function directionFlowToAxes(o){switch(o){case e.TopToBottom:return[n.Top,n.Bottom];case e.BottomToTop:return[n.Bottom,n.Top];case e.RightToLeft:return[n.Right,n.Left];case e.LeftToRight:return[n.Left,n.Right]}}const creator=o=>{const t=Object.assign({blockDirection:e.TopToBottom,inlineDirection:e.LeftToRight},o),i=Object.values(e);if(!i.includes(t.blockDirection))throw new Error(`[postcss-logical-resize] "blockDirection" must be one of ${i.join(", ")}`);if(!i.includes(t.inlineDirection))throw new Error(`[postcss-logical-resize] "inlineDirection" must be one of ${i.join(", ")}`);const[r,c]=directionFlowToAxes(t.blockDirection),[s,l]=directionFlowToAxes(t.inlineDirection);if(!Object.values(n).every(o=>[r,c,s,l].includes(o)))throw new Error('[postcss-logical-resize] "blockDirection" and "inlineDirection" must be on separate axes');const a={block:[r,c],inline:[s,l],inlineIsHorizontal:[e.LeftToRight,e.RightToLeft].includes(t.inlineDirection)};return{postcssPlugin:"postcss-logical-resize",Declaration:{resize:(f=transformResize(a),(o,{result:t})=>{if(!f)return;let e;try{e=f(o)}catch(e){return void o.warn(t,e instanceof Error?e.message:String(e))}e&&o.remove()})}};var f};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-logical-resize/src/lib/clone-declaration.ts b/plugins/postcss-logical-resize/src/lib/clone-declaration.ts index 46f7f06652..6b5295d1af 100644 --- a/plugins/postcss-logical-resize/src/lib/clone-declaration.ts +++ b/plugins/postcss-logical-resize/src/lib/clone-declaration.ts @@ -1,15 +1,58 @@ -import type { Declaration } from 'postcss'; +import type { Container, Declaration } from 'postcss'; + +type DeclarationCacheEntry = { + length: number, + keys: Set, +}; + +const declarationCache = new WeakMap(); + +function declarationKey(prop: string, value: string): string { + return `${prop}\0${value}`; +} + +function getDeclarationKeys(parent: Container): Set { + const nodes = parent.nodes || []; + let entry = declarationCache.get(parent); + + if (!entry || entry.length !== nodes.length) { + entry = { + length: nodes.length, + keys: new Set(), + }; + + for (const node of nodes) { + if (node.type === 'decl') { + entry.keys.add(declarationKey(node.prop, node.value)); + } + } + + declarationCache.set(parent, entry); + } + + return entry.keys; +} export function cloneDeclaration( declaration: Declaration, value: string, prop: string, ): void { - if (declaration.parent && declaration.parent.some((x) => { - return x.type === 'decl' && x.prop === prop && x.value === value; - })) { - return; + const parent = declaration.parent; + + if (parent) { + const keys = getDeclarationKeys(parent); + const key = declarationKey(prop, value); + + if (keys.has(key)) { + return; + } + + // The clone is inserted into the same parent below. + // Record it now so later declarations in this parent still dedupe + // against it when the parent length happens to stay the same. + keys.add(key); } - declaration.cloneBefore({ value, prop }); + declaration.before(declaration.clone({ value, prop })); } diff --git a/plugins/postcss-logical/CHANGELOG.md b/plugins/postcss-logical/CHANGELOG.md index 68f2ec80ac..3fe0f96363 100644 --- a/plugins/postcss-logical/CHANGELOG.md +++ b/plugins/postcss-logical/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Logical Properties +### Unreleased (patch) + +- Improved performance of declaration deduplication by caching declarations per parent + ### 9.0.1 _September 25, 2026_ diff --git a/plugins/postcss-logical/dist/index.mjs b/plugins/postcss-logical/dist/index.mjs index 17f34370bb..fcbf835374 100644 --- a/plugins/postcss-logical/dist/index.mjs +++ b/plugins/postcss-logical/dist/index.mjs @@ -1 +1 @@ -import r from"postcss-value-parser";var e,n;!function(r){r.Block="block",r.Inline="inline"}(e||(e={})),function(r){r.Start="start",r.End="end"}(n||(n={}));const o={BlockStart:"block-start",BlockEnd:"block-end",InlineStart:"inline-start",InlineEnd:"inline-end"};var t,i;function directionFlowToAxes(r){switch(r){case t.TopToBottom:return[i.Top,i.Bottom];case t.BottomToTop:return[i.Bottom,i.Top];case t.RightToLeft:return[i.Right,i.Left];case t.LeftToRight:return[i.Left,i.Right]}}function cloneDeclaration(r,e,n){return r.parent&&r.parent.some(r=>"decl"===r.type&&r.prop===n&&r.value===e)?[]:[r.clone({value:e,prop:n})]}!function(r){r.TopToBottom="top-to-bottom",r.BottomToTop="bottom-to-top",r.RightToLeft="right-to-left",r.LeftToRight="left-to-right"}(t||(t={})),function(r){r.Top="top",r.Right="right",r.Bottom="bottom",r.Left="left"}(i||(i={}));function transformTransition(e,n,o){const{prop:t,value:i}=e,l=r(i),a=[];let s=[];l.nodes.forEach(r=>{if("div"===r.type&&","===r.value)return a.push(s),void(s=[]);s.push(r)}),a.push(s);for(let r=0;r1e4)throw new Error("Too many combinations when transforming a transition, reduce the complexity of your transition");for(let n=d.length-1;n>=0;n--){const o=JSON.parse(JSON.stringify(e));o[t].value=d[n].prop,a.splice(r+1,0,o)}a.splice(r,1),r++}}}const d=[];for(let r=0;r"space"!==r.type);if(n.length>2||0===n.length){const r=`[postcss-logical] Invalid number of values for ${e.prop}. Found ${n.length} values, expected 1 or 2.`;throw e.error(r)}let o,t;return 1===n.length?(o=r.stringify(n[0]),t=o):(o=r.stringify(n[0]),t=r.stringify(n[1])),[o,t]}function transformBorder(r,e){return n=>cloneDeclaration(n,n.value,`border-${e}-${r}`)}function transformBorderProperty(r,e){return n=>{const[o,t]=e,[i,l]=parseValueCouple(n);return[...cloneDeclaration(n,i,`border-${o}-${r}`),...cloneDeclaration(n,l,`border-${t}-${r}`)]}}function transformBorderShorthand(r){return e=>r.flatMap(r=>cloneDeclaration(e,e.value,`border-${r}`))}function transformBorderRadius(r){return e=>{let n;switch(e.prop.toLowerCase()){case"border-start-start-radius":n=`border-${r.inlineIsHorizontal?`${r.block[0]}-${r.inline[0]}`:`${r.inline[0]}-${r.block[0]}`}-radius`;break;case"border-start-end-radius":n=`border-${r.inlineIsHorizontal?`${r.block[0]}-${r.inline[1]}`:`${r.inline[1]}-${r.block[0]}`}-radius`;break;case"border-end-start-radius":n=`border-${r.inlineIsHorizontal?`${r.block[1]}-${r.inline[0]}`:`${r.inline[0]}-${r.block[1]}`}-radius`;break;case"border-end-end-radius":n=`border-${r.inlineIsHorizontal?`${r.block[1]}-${r.inline[1]}`:`${r.inline[1]}-${r.block[1]}`}-radius`;break}return n?cloneDeclaration(e,e.value,n):[]}}function transformLogicalSize(r){return e=>{const{value:n}=e,o=r.inlineIsHorizontal?"width":"height",t=r.inlineIsHorizontal?"height":"width",i=e.prop.toLowerCase().replace("inline-size",o).replace("block-size",t);return cloneDeclaration(e,n,i)}}function transformOffset(r){return e=>cloneDeclaration(e,e.value,r)}function transformOffsetShorthand(r){return e=>{const[n,o]=r,[t,i]=parseValueCouple(e);return[...cloneDeclaration(e,t,n),...cloneDeclaration(e,i,o)]}}function transformSide(r,e){return n=>cloneDeclaration(n,n.value,`${r}-${e}`)}function transformSideShorthand(r,e){return n=>{const[o,t]=e,[i,l]=parseValueCouple(n);return[...cloneDeclaration(n,i,`${r}-${o}`),...cloneDeclaration(n,l,`${r}-${t}`)]}}function logicalToPhysical(r,e){const[n,t]=e.block,[i,l]=e.inline;switch(r){case o.BlockStart:return n;case o.BlockEnd:return t;case o.InlineStart:return i;case o.InlineEnd:return l;default:throw new Error("Unsupported logical direction")}}function doTransform(e,n,o){const{prop:t,value:i}=e,l=r(i);l.nodes.forEach(r=>{if("word"===r.type){const e=r.value.toLowerCase();n.includes(e)&&(r.value=logicalToPhysical(e,o))}});const a=l.toString();return a!==i?cloneDeclaration(e,a,t):[]}function prepareTransforms(e,t,i,l,a){return{"caption-side":(d=e,r=>doTransform(r,Object.values(o),d)),"text-align":e.inlineIsHorizontal?(s=e.inline,e=>{const{prop:o,value:t}=e,i=r(t),[l,a]=s;i.nodes.forEach(r=>{if("word"===r.type){const e=r.value.toLowerCase();if(e===n.End)return void(r.value=a);e===n.Start&&(r.value=l)}});const d=i.toString();return d!==t?cloneDeclaration(e,d,o):[]}):null,"block-size":transformLogicalSize(e),"inline-size":transformLogicalSize(e),"min-block-size":transformLogicalSize(e),"max-block-size":transformLogicalSize(e),"min-inline-size":transformLogicalSize(e),"max-inline-size":transformLogicalSize(e),"margin-block-start":transformSide("margin",t),"margin-block-end":transformSide("margin",i),"margin-inline-start":transformSide("margin",l),"margin-inline-end":transformSide("margin",a),"margin-block":transformSideShorthand("margin",e.block),"margin-inline":transformSideShorthand("margin",e.inline),"inset-block":transformOffsetShorthand(e.block),"inset-block-start":transformOffset(t),"inset-block-end":transformOffset(i),"inset-inline":transformOffsetShorthand(e.inline),"inset-inline-start":transformOffset(l),"inset-inline-end":transformOffset(a),inset:e=>{const n=r(e.value).nodes.filter(r=>"space"!==r.type&&"comment"!==r.type);if(n.length>4){const r=`[postcss-logical] Invalid number of values for ${e.prop}. Found ${n.length} values, expected up to 4 values.`;throw e.error(r)}let o,t,i,l;if(1===n.length)o=r.stringify(n[0]),t=o,i=o,l=o;else if(2===n.length)o=r.stringify(n[0]),t=r.stringify(n[1]),i=o,l=t;else if(3===n.length)o=r.stringify(n[0]),t=r.stringify(n[1]),l=t,i=r.stringify(n[2]);else{if(4!==n.length)return[];o=r.stringify(n[0]),t=r.stringify(n[1]),i=r.stringify(n[2]),l=r.stringify(n[3])}return[...cloneDeclaration(e,o,"top"),...cloneDeclaration(e,t,"right"),...cloneDeclaration(e,i,"bottom"),...cloneDeclaration(e,l,"left")]},"padding-block-start":transformSide("padding",t),"padding-block-end":transformSide("padding",i),"padding-inline-start":transformSide("padding",l),"padding-inline-end":transformSide("padding",a),"padding-block":transformSideShorthand("padding",e.block),"padding-inline":transformSideShorthand("padding",e.inline),"border-block-start-width":transformBorder("width",t),"border-block-end-width":transformBorder("width",i),"border-inline-start-width":transformBorder("width",l),"border-inline-end-width":transformBorder("width",a),"border-block-width":transformBorderProperty("width",e.block),"border-inline-width":transformBorderProperty("width",e.inline),"border-block-start-style":transformBorder("style",t),"border-block-end-style":transformBorder("style",i),"border-inline-start-style":transformBorder("style",l),"border-inline-end-style":transformBorder("style",a),"border-block-style":transformBorderProperty("style",e.block),"border-inline-style":transformBorderProperty("style",e.inline),"border-block-start-color":transformBorder("color",t),"border-block-end-color":transformBorder("color",i),"border-inline-start-color":transformBorder("color",l),"border-inline-end-color":transformBorder("color",a),"border-block-color":transformBorderProperty("color",e.block),"border-inline-color":transformBorderProperty("color",e.inline),"border-block":transformBorderShorthand(e.block),"border-block-start":transformBorderShorthand([t]),"border-block-end":transformBorderShorthand([i]),"border-inline":transformBorderShorthand(e.inline),"border-inline-start":transformBorderShorthand([l]),"border-inline-end":transformBorderShorthand([a]),"border-start-start-radius":transformBorderRadius(e),"border-start-end-radius":transformBorderRadius(e),"border-end-start-radius":transformBorderRadius(e),"border-end-end-radius":transformBorderRadius(e)};var s,d}const l=/var\(/i,creator=r=>{const e=Object.assign({blockDirection:t.TopToBottom,inlineDirection:t.LeftToRight},r),n=Object.values(t);if(!n.includes(e.blockDirection))throw new Error(`[postcss-logical] "blockDirection" must be one of ${n.join(", ")}`);if(!n.includes(e.inlineDirection))throw new Error(`[postcss-logical] "inlineDirection" must be one of ${n.join(", ")}`);const[o,a]=directionFlowToAxes(e.blockDirection),[s,d]=directionFlowToAxes(e.inlineDirection);if(!Object.values(i).every(r=>[o,a,s,d].includes(r)))throw new Error('[postcss-logical] "blockDirection" and "inlineDirection" must be on separate axes');const makeTransform=r=>(n,{result:o})=>{if(!r)return;if(e.ignoreCustomProperties&&l.test(n.value))return;let t;try{t=r(n)}catch(r){return void n.warn(o,r instanceof Error?r.message:String(r))}t?.length&&(t.forEach(r=>{n.cloneBefore(r)}),n.remove())},c=prepareTransforms({block:[o,a],inline:[s,d],inlineIsHorizontal:[t.LeftToRight,t.RightToLeft].includes(e.inlineDirection)},o,a,s,d);return{postcssPlugin:"postcss-logical",Declaration:{"caption-side":makeTransform(c["caption-side"]),"text-align":makeTransform(c["text-align"]),"block-size":makeTransform(c["block-size"]),"inline-size":makeTransform(c["inline-size"]),"min-block-size":makeTransform(c["min-block-size"]),"max-block-size":makeTransform(c["max-block-size"]),"min-inline-size":makeTransform(c["min-inline-size"]),"max-inline-size":makeTransform(c["max-inline-size"]),"margin-block-start":makeTransform(c["margin-block-start"]),"margin-block-end":makeTransform(c["margin-block-end"]),"margin-inline-start":makeTransform(c["margin-inline-start"]),"margin-inline-end":makeTransform(c["margin-inline-end"]),"margin-block":makeTransform(c["margin-block"]),"margin-inline":makeTransform(c["margin-inline"]),"inset-block":makeTransform(c["inset-block"]),"inset-block-start":makeTransform(c["inset-block-start"]),"inset-block-end":makeTransform(c["inset-block-end"]),"inset-inline":makeTransform(c["inset-inline"]),"inset-inline-start":makeTransform(c["inset-inline-start"]),"inset-inline-end":makeTransform(c["inset-inline-end"]),inset:makeTransform(c.inset),"padding-block-start":makeTransform(c["padding-block-start"]),"padding-block-end":makeTransform(c["padding-block-end"]),"padding-inline-start":makeTransform(c["padding-inline-start"]),"padding-inline-end":makeTransform(c["padding-inline-end"]),"padding-block":makeTransform(c["padding-block"]),"padding-inline":makeTransform(c["padding-inline"]),"border-block-start-width":makeTransform(c["border-block-start-width"]),"border-block-end-width":makeTransform(c["border-block-end-width"]),"border-inline-start-width":makeTransform(c["border-inline-start-width"]),"border-inline-end-width":makeTransform(c["border-inline-end-width"]),"border-block-width":makeTransform(c["border-block-width"]),"border-inline-width":makeTransform(c["border-inline-width"]),"border-block-start-style":makeTransform(c["border-block-start-style"]),"border-block-end-style":makeTransform(c["border-block-end-style"]),"border-inline-start-style":makeTransform(c["border-inline-start-style"]),"border-inline-end-style":makeTransform(c["border-inline-end-style"]),"border-block-style":makeTransform(c["border-block-style"]),"border-inline-style":makeTransform(c["border-inline-style"]),"border-block-start-color":makeTransform(c["border-block-start-color"]),"border-block-end-color":makeTransform(c["border-block-end-color"]),"border-inline-start-color":makeTransform(c["border-inline-start-color"]),"border-inline-end-color":makeTransform(c["border-inline-end-color"]),"border-block-color":makeTransform(c["border-block-color"]),"border-inline-color":makeTransform(c["border-inline-color"]),"border-block":makeTransform(c["border-block"]),"border-block-start":makeTransform(c["border-block-start"]),"border-block-end":makeTransform(c["border-block-end"]),"border-inline":makeTransform(c["border-inline"]),"border-inline-start":makeTransform(c["border-inline-start"]),"border-inline-end":makeTransform(c["border-inline-end"]),"border-start-start-radius":makeTransform(c["border-start-start-radius"]),"border-start-end-radius":makeTransform(c["border-start-end-radius"]),"border-end-start-radius":makeTransform(c["border-end-start-radius"]),"border-end-end-radius":makeTransform(c["border-end-end-radius"]),transition:(r,{result:n,postcss:o})=>{if(e.ignoreCustomProperties&&l.test(r.value))return;let t;try{t=transformTransition(r,o,c)}catch(e){return void r.warn(n,e instanceof Error?e.message:String(e))}t?.length&&(t.forEach(e=>{r.cloneBefore(e)}),r.remove())}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import r from"postcss-value-parser";var e,n;!function(r){r.Block="block",r.Inline="inline"}(e||(e={})),function(r){r.Start="start",r.End="end"}(n||(n={}));const o={BlockStart:"block-start",BlockEnd:"block-end",InlineStart:"inline-start",InlineEnd:"inline-end"};var t,i;function directionFlowToAxes(r){switch(r){case t.TopToBottom:return[i.Top,i.Bottom];case t.BottomToTop:return[i.Bottom,i.Top];case t.RightToLeft:return[i.Right,i.Left];case t.LeftToRight:return[i.Left,i.Right]}}!function(r){r.TopToBottom="top-to-bottom",r.BottomToTop="bottom-to-top",r.RightToLeft="right-to-left",r.LeftToRight="left-to-right"}(t||(t={})),function(r){r.Top="top",r.Right="right",r.Bottom="bottom",r.Left="left"}(i||(i={}));const l=new WeakMap;function declarationKey(r,e){return`${r}\0${e}`}function getDeclarationKeys(r){const e=r.nodes||[];let n=l.get(r);if(!n||n.length!==e.length){n={length:e.length,keys:new Set};for(const r of e)"decl"===r.type&&n.keys.add(declarationKey(r.prop,r.value));l.set(r,n)}return n.keys}function cloneDeclaration(r,e,n){const o=r.parent;if(o){const r=getDeclarationKeys(o),t=declarationKey(n,e);if(r.has(t))return[];r.add(t)}return[r.clone({value:e,prop:n})]}function transformTransition(e,n,o){const{prop:t,value:i}=e,l=r(i),a=[];let s=[];l.nodes.forEach(r=>{if("div"===r.type&&","===r.value)return a.push(s),void(s=[]);s.push(r)}),a.push(s);for(let r=0;r1e4)throw new Error("Too many combinations when transforming a transition, reduce the complexity of your transition");for(let n=d.length-1;n>=0;n--){const o=JSON.parse(JSON.stringify(e));o[t].value=d[n].prop,a.splice(r+1,0,o)}a.splice(r,1),r++}}}const d=[];for(let r=0;r"space"!==r.type);if(n.length>2||0===n.length){const r=`[postcss-logical] Invalid number of values for ${e.prop}. Found ${n.length} values, expected 1 or 2.`;throw e.error(r)}let o,t;return 1===n.length?(o=r.stringify(n[0]),t=o):(o=r.stringify(n[0]),t=r.stringify(n[1])),[o,t]}function transformBorder(r,e){return n=>cloneDeclaration(n,n.value,`border-${e}-${r}`)}function transformBorderProperty(r,e){return n=>{const[o,t]=e,[i,l]=parseValueCouple(n);return[...cloneDeclaration(n,i,`border-${o}-${r}`),...cloneDeclaration(n,l,`border-${t}-${r}`)]}}function transformBorderShorthand(r){return e=>r.flatMap(r=>cloneDeclaration(e,e.value,`border-${r}`))}function transformBorderRadius(r){return e=>{let n;switch(e.prop.toLowerCase()){case"border-start-start-radius":n=`border-${r.inlineIsHorizontal?`${r.block[0]}-${r.inline[0]}`:`${r.inline[0]}-${r.block[0]}`}-radius`;break;case"border-start-end-radius":n=`border-${r.inlineIsHorizontal?`${r.block[0]}-${r.inline[1]}`:`${r.inline[1]}-${r.block[0]}`}-radius`;break;case"border-end-start-radius":n=`border-${r.inlineIsHorizontal?`${r.block[1]}-${r.inline[0]}`:`${r.inline[0]}-${r.block[1]}`}-radius`;break;case"border-end-end-radius":n=`border-${r.inlineIsHorizontal?`${r.block[1]}-${r.inline[1]}`:`${r.inline[1]}-${r.block[1]}`}-radius`;break}return n?cloneDeclaration(e,e.value,n):[]}}function transformLogicalSize(r){return e=>{const{value:n}=e,o=r.inlineIsHorizontal?"width":"height",t=r.inlineIsHorizontal?"height":"width",i=e.prop.toLowerCase().replace("inline-size",o).replace("block-size",t);return cloneDeclaration(e,n,i)}}function transformOffset(r){return e=>cloneDeclaration(e,e.value,r)}function transformOffsetShorthand(r){return e=>{const[n,o]=r,[t,i]=parseValueCouple(e);return[...cloneDeclaration(e,t,n),...cloneDeclaration(e,i,o)]}}function transformSide(r,e){return n=>cloneDeclaration(n,n.value,`${r}-${e}`)}function transformSideShorthand(r,e){return n=>{const[o,t]=e,[i,l]=parseValueCouple(n);return[...cloneDeclaration(n,i,`${r}-${o}`),...cloneDeclaration(n,l,`${r}-${t}`)]}}function logicalToPhysical(r,e){const[n,t]=e.block,[i,l]=e.inline;switch(r){case o.BlockStart:return n;case o.BlockEnd:return t;case o.InlineStart:return i;case o.InlineEnd:return l;default:throw new Error("Unsupported logical direction")}}function doTransform(e,n,o){const{prop:t,value:i}=e,l=r(i);l.nodes.forEach(r=>{if("word"===r.type){const e=r.value.toLowerCase();n.includes(e)&&(r.value=logicalToPhysical(e,o))}});const a=l.toString();return a!==i?cloneDeclaration(e,a,t):[]}function prepareTransforms(e,t,i,l,a){return{"caption-side":(d=e,r=>doTransform(r,Object.values(o),d)),"text-align":e.inlineIsHorizontal?(s=e.inline,e=>{const{prop:o,value:t}=e,i=r(t),[l,a]=s;i.nodes.forEach(r=>{if("word"===r.type){const e=r.value.toLowerCase();if(e===n.End)return void(r.value=a);e===n.Start&&(r.value=l)}});const d=i.toString();return d!==t?cloneDeclaration(e,d,o):[]}):null,"block-size":transformLogicalSize(e),"inline-size":transformLogicalSize(e),"min-block-size":transformLogicalSize(e),"max-block-size":transformLogicalSize(e),"min-inline-size":transformLogicalSize(e),"max-inline-size":transformLogicalSize(e),"margin-block-start":transformSide("margin",t),"margin-block-end":transformSide("margin",i),"margin-inline-start":transformSide("margin",l),"margin-inline-end":transformSide("margin",a),"margin-block":transformSideShorthand("margin",e.block),"margin-inline":transformSideShorthand("margin",e.inline),"inset-block":transformOffsetShorthand(e.block),"inset-block-start":transformOffset(t),"inset-block-end":transformOffset(i),"inset-inline":transformOffsetShorthand(e.inline),"inset-inline-start":transformOffset(l),"inset-inline-end":transformOffset(a),inset:e=>{const n=r(e.value).nodes.filter(r=>"space"!==r.type&&"comment"!==r.type);if(n.length>4){const r=`[postcss-logical] Invalid number of values for ${e.prop}. Found ${n.length} values, expected up to 4 values.`;throw e.error(r)}let o,t,i,l;if(1===n.length)o=r.stringify(n[0]),t=o,i=o,l=o;else if(2===n.length)o=r.stringify(n[0]),t=r.stringify(n[1]),i=o,l=t;else if(3===n.length)o=r.stringify(n[0]),t=r.stringify(n[1]),l=t,i=r.stringify(n[2]);else{if(4!==n.length)return[];o=r.stringify(n[0]),t=r.stringify(n[1]),i=r.stringify(n[2]),l=r.stringify(n[3])}return[...cloneDeclaration(e,o,"top"),...cloneDeclaration(e,t,"right"),...cloneDeclaration(e,i,"bottom"),...cloneDeclaration(e,l,"left")]},"padding-block-start":transformSide("padding",t),"padding-block-end":transformSide("padding",i),"padding-inline-start":transformSide("padding",l),"padding-inline-end":transformSide("padding",a),"padding-block":transformSideShorthand("padding",e.block),"padding-inline":transformSideShorthand("padding",e.inline),"border-block-start-width":transformBorder("width",t),"border-block-end-width":transformBorder("width",i),"border-inline-start-width":transformBorder("width",l),"border-inline-end-width":transformBorder("width",a),"border-block-width":transformBorderProperty("width",e.block),"border-inline-width":transformBorderProperty("width",e.inline),"border-block-start-style":transformBorder("style",t),"border-block-end-style":transformBorder("style",i),"border-inline-start-style":transformBorder("style",l),"border-inline-end-style":transformBorder("style",a),"border-block-style":transformBorderProperty("style",e.block),"border-inline-style":transformBorderProperty("style",e.inline),"border-block-start-color":transformBorder("color",t),"border-block-end-color":transformBorder("color",i),"border-inline-start-color":transformBorder("color",l),"border-inline-end-color":transformBorder("color",a),"border-block-color":transformBorderProperty("color",e.block),"border-inline-color":transformBorderProperty("color",e.inline),"border-block":transformBorderShorthand(e.block),"border-block-start":transformBorderShorthand([t]),"border-block-end":transformBorderShorthand([i]),"border-inline":transformBorderShorthand(e.inline),"border-inline-start":transformBorderShorthand([l]),"border-inline-end":transformBorderShorthand([a]),"border-start-start-radius":transformBorderRadius(e),"border-start-end-radius":transformBorderRadius(e),"border-end-start-radius":transformBorderRadius(e),"border-end-end-radius":transformBorderRadius(e)};var s,d}const a=/var\(/i,creator=r=>{const e=Object.assign({blockDirection:t.TopToBottom,inlineDirection:t.LeftToRight},r),n=Object.values(t);if(!n.includes(e.blockDirection))throw new Error(`[postcss-logical] "blockDirection" must be one of ${n.join(", ")}`);if(!n.includes(e.inlineDirection))throw new Error(`[postcss-logical] "inlineDirection" must be one of ${n.join(", ")}`);const[o,l]=directionFlowToAxes(e.blockDirection),[s,d]=directionFlowToAxes(e.inlineDirection);if(!Object.values(i).every(r=>[o,l,s,d].includes(r)))throw new Error('[postcss-logical] "blockDirection" and "inlineDirection" must be on separate axes');const makeTransform=r=>(n,{result:o})=>{if(!r)return;if(e.ignoreCustomProperties&&a.test(n.value))return;let t;try{t=r(n)}catch(r){return void n.warn(o,r instanceof Error?r.message:String(r))}t?.length&&(n.before(t),n.remove())},c=prepareTransforms({block:[o,l],inline:[s,d],inlineIsHorizontal:[t.LeftToRight,t.RightToLeft].includes(e.inlineDirection)},o,l,s,d);return{postcssPlugin:"postcss-logical",Declaration:{"caption-side":makeTransform(c["caption-side"]),"text-align":makeTransform(c["text-align"]),"block-size":makeTransform(c["block-size"]),"inline-size":makeTransform(c["inline-size"]),"min-block-size":makeTransform(c["min-block-size"]),"max-block-size":makeTransform(c["max-block-size"]),"min-inline-size":makeTransform(c["min-inline-size"]),"max-inline-size":makeTransform(c["max-inline-size"]),"margin-block-start":makeTransform(c["margin-block-start"]),"margin-block-end":makeTransform(c["margin-block-end"]),"margin-inline-start":makeTransform(c["margin-inline-start"]),"margin-inline-end":makeTransform(c["margin-inline-end"]),"margin-block":makeTransform(c["margin-block"]),"margin-inline":makeTransform(c["margin-inline"]),"inset-block":makeTransform(c["inset-block"]),"inset-block-start":makeTransform(c["inset-block-start"]),"inset-block-end":makeTransform(c["inset-block-end"]),"inset-inline":makeTransform(c["inset-inline"]),"inset-inline-start":makeTransform(c["inset-inline-start"]),"inset-inline-end":makeTransform(c["inset-inline-end"]),inset:makeTransform(c.inset),"padding-block-start":makeTransform(c["padding-block-start"]),"padding-block-end":makeTransform(c["padding-block-end"]),"padding-inline-start":makeTransform(c["padding-inline-start"]),"padding-inline-end":makeTransform(c["padding-inline-end"]),"padding-block":makeTransform(c["padding-block"]),"padding-inline":makeTransform(c["padding-inline"]),"border-block-start-width":makeTransform(c["border-block-start-width"]),"border-block-end-width":makeTransform(c["border-block-end-width"]),"border-inline-start-width":makeTransform(c["border-inline-start-width"]),"border-inline-end-width":makeTransform(c["border-inline-end-width"]),"border-block-width":makeTransform(c["border-block-width"]),"border-inline-width":makeTransform(c["border-inline-width"]),"border-block-start-style":makeTransform(c["border-block-start-style"]),"border-block-end-style":makeTransform(c["border-block-end-style"]),"border-inline-start-style":makeTransform(c["border-inline-start-style"]),"border-inline-end-style":makeTransform(c["border-inline-end-style"]),"border-block-style":makeTransform(c["border-block-style"]),"border-inline-style":makeTransform(c["border-inline-style"]),"border-block-start-color":makeTransform(c["border-block-start-color"]),"border-block-end-color":makeTransform(c["border-block-end-color"]),"border-inline-start-color":makeTransform(c["border-inline-start-color"]),"border-inline-end-color":makeTransform(c["border-inline-end-color"]),"border-block-color":makeTransform(c["border-block-color"]),"border-inline-color":makeTransform(c["border-inline-color"]),"border-block":makeTransform(c["border-block"]),"border-block-start":makeTransform(c["border-block-start"]),"border-block-end":makeTransform(c["border-block-end"]),"border-inline":makeTransform(c["border-inline"]),"border-inline-start":makeTransform(c["border-inline-start"]),"border-inline-end":makeTransform(c["border-inline-end"]),"border-start-start-radius":makeTransform(c["border-start-start-radius"]),"border-start-end-radius":makeTransform(c["border-start-end-radius"]),"border-end-start-radius":makeTransform(c["border-end-start-radius"]),"border-end-end-radius":makeTransform(c["border-end-end-radius"]),transition:(r,{result:n,postcss:o})=>{if(e.ignoreCustomProperties&&a.test(r.value))return;let t;try{t=transformTransition(r,o,c)}catch(e){return void r.warn(n,e instanceof Error?e.message:String(e))}t?.length&&(r.before(t),r.remove())}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-logical/src/index.ts b/plugins/postcss-logical/src/index.ts index e1bef3672b..38907c179a 100644 --- a/plugins/postcss-logical/src/index.ts +++ b/plugins/postcss-logical/src/index.ts @@ -79,9 +79,9 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - transformed.forEach((transformedDecl) => { - decl.cloneBefore(transformedDecl); - }); + // Insert as a batch: resolving the reference index for every single + // insertion is quadratic for a rule with many declarations. + decl.before(transformed); decl.remove(); }; @@ -177,9 +177,9 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - transformed.forEach((transformedDecl) => { - decl.cloneBefore(transformedDecl); - }); + // Insert as a batch: resolving the reference index for every single + // insertion is quadratic for a rule with many declarations. + decl.before(transformed); decl.remove(); }, diff --git a/plugins/postcss-logical/src/lib/clone-declaration.ts b/plugins/postcss-logical/src/lib/clone-declaration.ts index 9a99427eb5..f0e5a33baf 100644 --- a/plugins/postcss-logical/src/lib/clone-declaration.ts +++ b/plugins/postcss-logical/src/lib/clone-declaration.ts @@ -1,14 +1,57 @@ -import type { Declaration } from 'postcss'; +import type { Container, Declaration } from 'postcss'; + +type DeclarationCacheEntry = { + length: number, + keys: Set, +}; + +const declarationCache = new WeakMap(); + +function declarationKey(prop: string, value: string): string { + return `${prop}\0${value}`; +} + +function getDeclarationKeys(parent: Container): Set { + const nodes = parent.nodes || []; + let entry = declarationCache.get(parent); + + if (!entry || entry.length !== nodes.length) { + entry = { + length: nodes.length, + keys: new Set(), + }; + + for (const node of nodes) { + if (node.type === 'decl') { + entry.keys.add(declarationKey(node.prop, node.value)); + } + } + + declarationCache.set(parent, entry); + } + + return entry.keys; +} export function cloneDeclaration( declaration: Declaration, value: string, prop: string, ): Array { - if (declaration.parent && declaration.parent.some((x) => { - return x.type === 'decl' && x.prop === prop && x.value === value; - })) { - return []; + const parent = declaration.parent; + + if (parent) { + const keys = getDeclarationKeys(parent); + const key = declarationKey(prop, value); + + if (keys.has(key)) { + return []; + } + + // The clone will be inserted into the same parent by the caller. + // Record it now so later declarations in this parent still dedupe + // against it when the parent length happens to stay the same. + keys.add(key); } return [declaration.clone({ value, prop })]; diff --git a/plugins/postcss-media-minmax/CHANGELOG.md b/plugins/postcss-media-minmax/CHANGELOG.md index b8269d4a1b..0a8145d78f 100644 --- a/plugins/postcss-media-minmax/CHANGELOG.md +++ b/plugins/postcss-media-minmax/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Media MinMax +### Unreleased (patch) + +- Improve performance when transforming media queries with multiple range features. + ### 3.0.6 _September 25, 2026_ diff --git a/plugins/postcss-media-minmax/dist/index.mjs b/plugins/postcss-media-minmax/dist/index.mjs index cd3170f5a4..7905bde80f 100644 --- a/plugins/postcss-media-minmax/dist/index.mjs +++ b/plugins/postcss-media-minmax/dist/index.mjs @@ -1 +1 @@ -import{isFunctionNode as e,isTokenNode as t,gatherNodeAncestry as r}from"@csstools/css-parser-algorithms";import{isTokenNumeric as i,TokenType as a,NumberType as n,isTokenNumber as o,isTokenDimension as s,stringify as u,isTokenWhitespace as l}from"@csstools/css-tokenizer";import{invertComparison as c,MediaFeatureEQ as d,MediaFeatureLT as f,MediaFeatureGT as m,newMediaFeaturePlain as v,matchesRatioExactly as p,matchesRatio as h,isMediaFeatureRange as g,isMediaFeature as w,isMediaFeatureRangeNameValue as y,isMediaFeatureRangeValueName as x,isMediaInParens as N,MediaInParens as b,MediaAnd as P,MediaConditionListWithAnd as L,MediaCondition as I,isMediaConditionListWithAnd as Q,isMediaAnd as S,isMediaCondition as W,isMediaQuery as O,parseCustomMedia as M,parse as T}from"@csstools/media-query-list-parser";import{mathFunctionNames as _,calcFromComponentValues as A}from"@csstools/css-calc";const C={width:"px",height:"px","device-width":"px","device-height":"px","aspect-ratio":"","device-aspect-ratio":"",color:"","color-index":"",monochrome:"",resolution:"dpi"},E={width:!1,height:!1,"device-width":!1,"device-height":!1,"aspect-ratio":!1,"device-aspect-ratio":!1,color:!0,"color-index":!0,monochrome:!0,resolution:"dpi"};function featureNamePrefix(e){return e===f.LT||e===f.LT_OR_EQ?"max-":e===m.GT||e===m.GT_OR_EQ?"min-":""}const k={">":1,"<":-1},R=.001;function transformSingleNameValuePair(r,u,l,g){let w=l.before,y=l.after;if(g||(w=l.after,y=l.before),!g){const e=c(u);if(!1===e)return;u=e}if(u===d.EQ||u===f.LT_OR_EQ||u===m.GT_OR_EQ)return Array.isArray(l.value)?v(featureNamePrefix(u)+r,...w,...l.value.flatMap(e=>e.tokens()),...y):v(featureNamePrefix(u)+r,...w,...l.value.tokens(),...y);let x,N,b=!1;if(Array.isArray(l.value)){if(!p(l.value))return;if("aspect-ratio"!==r&&"device-aspect-ratio"!==r)return;const e=h(l.value);if(-1===e)return;b=!0,x=l.value[e[0]],N=[...l.value.slice(e[0]+1).flatMap(e=>e.tokens())]}else x=l.value,N=[];const P=C[r.toLowerCase()];if(e(x)){const e=x.getName().toLowerCase();if(_.has(e)||"env"===e){const[[e]]=A([[x]],{precision:5,toCanonicalUnits:!0});if(!(e&&t(e)&&i(e.value)&&Number.isInteger(e.value[4].value))){let e;if(void 0!==P){const t=k[u]*("px"===P?.02:R);e=[a.Dimension,`${t.toString()}${P}`,-1,-1,{value:t,unit:P,type:n.Integer}]}else if(!0===E[r]){const t=k[u];e=[a.Number,t.toString(),-1,-1,{value:t,type:n.Integer}]}else if(b){const t=k[u]*R;e=[a.Number,t.toString(),-1,-1,{value:t,type:n.Integer}]}else{const t=k[u];e=[a.Number,t.toString(),-1,-1,{value:t,type:n.Integer}]}return v(featureNamePrefix(u)+r,...w,[a.Function,"calc(",-1,-1,{value:"calc("}],[a.OpenParen,"(",-1,-1,void 0],...x.tokens().slice(1),[a.Whitespace," ",-1,-1,void 0],[a.Delim,"+",-1,-1,{value:"+"}],[a.Whitespace," ",-1,-1,void 0],e,[a.CloseParen,")",-1,-1,void 0],...N,...y)}x=e}}if(!t(x))return;let L,I=x.value,Q="";if(void 0!==P&&o(I)&&0===I[4].value)L=k[u],Q=P;else if(o(I)&&0===I[4].value)L=k[u],Q="";else if(s(I)&&0===I[4].value)L=k[u],Q=I[4].unit;else if(o(I)&&!0===E[r])L=I[4].value+k[u];else if(s(I)&&"px"===I[4].unit&&I[4].type===n.Integer)L=Number(Math.round(Number(I[4].value+.02*k[u]+"e6"))+"e-6");else{if(!s(I)&&!o(I))return;L=Number(Math.round(Number(I[4].value+R*k[u]+"e6"))+"e-6")}return Q&&(I=[a.Dimension,I[1],I[2],I[3],{value:I[4].value,unit:Q,type:I[4].type}]),I[4].value=L,s(I)?I[1]=I[4].value.toString()+I[4].unit:I[1]=I[4].value.toString(),v(featureNamePrefix(u)+r,...w,I,...N,...y)}const $=new Set(["aspect-ratio","color","color-index","device-aspect-ratio","device-height","device-width","height","horizontal-viewport-segments","monochrome","resolution","vertical-viewport-segments","width"]);function transform(e){return e.map((e,t)=>{const i=r(e);e.walk(t=>{const r=t.node;if(!g(r))return;const n=t.parent;if(!w(n))return;const o=r.name.getName().toLowerCase();if(!$.has(o))return;if(y(r)||x(r)){const e=r.operatorKind();if(!1===e)return;const t=transformSingleNameValuePair(o,e,r.value,y(r));return void(t&&(n.feature=t.feature))}const s=i.get(n);if(!N(s))return;let u=null,l=null;{const e=r.valueOneOperatorKind();if(!1===e)return;const t=transformSingleNameValuePair(o,e,r.valueOne,!1);if(!t)return;e===f.LT||e===f.LT_OR_EQ?(u=t,u.before=n.before):(l=t,l.after=n.after)}{const e=r.valueTwoOperatorKind();if(!1===e)return;const t=transformSingleNameValuePair(o,e,r.valueTwo,!0);if(!t)return;e===f.LT||e===f.LT_OR_EQ?(l=t,l.before=n.before):(u=t,u.after=n.after)}if(!u||!l)return;const c=new b(u),d=new b(l),m=getMediaConditionListWithAndFromAncestry(s,i);if(m)return m.leading===s?(m.leading=c,void(m.list=[new P([[a.Whitespace," ",-1,-1,void 0],[a.Ident,"and",-1,-1,{value:"and"}],[a.Whitespace," ",-1,-1,void 0]],d),...m.list])):void m.list.splice(m.indexOf(i.get(s)),1,new P([[a.Whitespace," ",-1,-1,void 0],[a.Ident,"and",-1,-1,{value:"and"}],[a.Whitespace," ",-1,-1,void 0]],c),new P([[a.Whitespace," ",-1,-1,void 0],[a.Ident,"and",-1,-1,{value:"and"}],[a.Whitespace," ",-1,-1,void 0]],d));const v=new L(c,[new P([[a.Whitespace," ",-1,-1,void 0],[a.Ident,"and",-1,-1,{value:"and"}],[a.Whitespace," ",-1,-1,void 0]],d)],[[a.Whitespace," ",-1,-1,void 0]]),p=getMediaConditionInShallowMediaQueryFromAncestry(s,e,i);p?p.media=v:s.media=new I(new b(new I(v),[[a.Whitespace," ",-1,-1,void 0],[a.OpenParen,"(",-1,-1,void 0]],[[a.CloseParen,")",-1,-1,void 0]]))});const n=e.tokens();return u(...n.filter((e,r)=>(0!==r||0!==t||!l(e))&&!(l(e)&&n[r+1]&&l(n[r+1]))))}).join(",")}function getMediaConditionListWithAndFromAncestry(e,t){let r=e;if(r){if(r=t.get(r),Q(r))return r;if(S(r))return r=t.get(r),Q(r)?r:void 0}}function getMediaConditionInShallowMediaQueryFromAncestry(e,t,r){let i=e;if(!i)return;if(i=r.get(i),!W(i))return;const a=i;return i=r.get(i),O(i)&&i===t?a:void 0}const creator=()=>({postcssPlugin:"postcss-media-minmax",AtRule:{media(e){if(!(e.params.includes("<")||e.params.includes(">")||e.params.includes("=")))return;const t=transform(T(e.params,{preserveInvalidMediaQueries:!0,onParseError:()=>{throw e.error(`Unable to parse media query "${e.params}"`)}}));e.params!==t&&(e.params=t)},"custom-media"(e){if(!(e.params.includes("<")||e.params.includes(">")||e.params.includes("=")))return;const t=M(e.params,{preserveInvalidMediaQueries:!0,onParseError:()=>{throw e.error(`Unable to parse media query "${e.params}"`)}});if(!t||!t.mediaQueryList)return;const r=t.mediaQueryList.map(e=>e.toString()).join(","),i=transform(t.mediaQueryList);r!==i&&(e.params=`${u(...t.name)} ${i}`)}}});creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{isFunctionNode as e,isTokenNode as t,gatherNodeAncestry as r}from"@csstools/css-parser-algorithms";import{isTokenNumeric as i,TokenType as n,NumberType as a,isTokenNumber as o,isTokenDimension as s,stringify as u,isTokenWhitespace as l}from"@csstools/css-tokenizer";import{invertComparison as c,MediaFeatureEQ as d,MediaFeatureLT as f,MediaFeatureGT as m,newMediaFeaturePlain as p,matchesRatioExactly as v,matchesRatio as h,isMediaFeatureRange as g,isMediaFeature as w,isMediaFeatureRangeNameValue as y,isMediaFeatureRangeValueName as x,isMediaInParens as N,MediaInParens as b,MediaAnd as P,MediaConditionListWithAnd as L,MediaCondition as W,isMediaConditionListWithAnd as I,isMediaAnd as M,isMediaCondition as Q,isMediaQuery as S,parseCustomMedia as O,parse as T}from"@csstools/media-query-list-parser";import{mathFunctionNames as _,calcFromComponentValues as A}from"@csstools/css-calc";const C={width:"px",height:"px","device-width":"px","device-height":"px","aspect-ratio":"","device-aspect-ratio":"",color:"","color-index":"",monochrome:"",resolution:"dpi"},E={width:!1,height:!1,"device-width":!1,"device-height":!1,"aspect-ratio":!1,"device-aspect-ratio":!1,color:!0,"color-index":!0,monochrome:!0,resolution:"dpi"};function featureNamePrefix(e){return e===f.LT||e===f.LT_OR_EQ?"max-":e===m.GT||e===m.GT_OR_EQ?"min-":""}const k={">":1,"<":-1},R=.001;function transformSingleNameValuePair(r,u,l,g){let w=l.before,y=l.after;if(g||(w=l.after,y=l.before),!g){const e=c(u);if(!1===e)return;u=e}if(u===d.EQ||u===f.LT_OR_EQ||u===m.GT_OR_EQ)return Array.isArray(l.value)?p(featureNamePrefix(u)+r,...w,...l.value.flatMap(e=>e.tokens()),...y):p(featureNamePrefix(u)+r,...w,...l.value.tokens(),...y);let x,N,b=!1;if(Array.isArray(l.value)){if(!v(l.value))return;if("aspect-ratio"!==r&&"device-aspect-ratio"!==r)return;const e=h(l.value);if(-1===e)return;b=!0,x=l.value[e[0]],N=[...l.value.slice(e[0]+1).flatMap(e=>e.tokens())]}else x=l.value,N=[];const P=C[r.toLowerCase()];if(e(x)){const e=x.getName().toLowerCase();if(_.has(e)||"env"===e){const[[e]]=A([[x]],{precision:5,toCanonicalUnits:!0});if(!(e&&t(e)&&i(e.value)&&Number.isInteger(e.value[4].value))){let e;if(void 0!==P){const t=k[u]*("px"===P?.02:R);e=[n.Dimension,`${t.toString()}${P}`,-1,-1,{value:t,unit:P,type:a.Integer}]}else if(!0===E[r]){const t=k[u];e=[n.Number,t.toString(),-1,-1,{value:t,type:a.Integer}]}else if(b){const t=k[u]*R;e=[n.Number,t.toString(),-1,-1,{value:t,type:a.Integer}]}else{const t=k[u];e=[n.Number,t.toString(),-1,-1,{value:t,type:a.Integer}]}return p(featureNamePrefix(u)+r,...w,[n.Function,"calc(",-1,-1,{value:"calc("}],[n.OpenParen,"(",-1,-1,void 0],...x.tokens().slice(1),[n.Whitespace," ",-1,-1,void 0],[n.Delim,"+",-1,-1,{value:"+"}],[n.Whitespace," ",-1,-1,void 0],e,[n.CloseParen,")",-1,-1,void 0],...N,...y)}x=e}}if(!t(x))return;let L,W=x.value,I="";if(void 0!==P&&o(W)&&0===W[4].value)L=k[u],I=P;else if(o(W)&&0===W[4].value)L=k[u],I="";else if(s(W)&&0===W[4].value)L=k[u],I=W[4].unit;else if(o(W)&&!0===E[r])L=W[4].value+k[u];else if(s(W)&&"px"===W[4].unit&&W[4].type===a.Integer)L=Number(Math.round(Number(W[4].value+.02*k[u]+"e6"))+"e-6");else{if(!s(W)&&!o(W))return;L=Number(Math.round(Number(W[4].value+R*k[u]+"e6"))+"e-6")}return I&&(W=[n.Dimension,W[1],W[2],W[3],{value:W[4].value,unit:I,type:W[4].type}]),W[4].value=L,s(W)?W[1]=W[4].value.toString()+W[4].unit:W[1]=W[4].value.toString(),p(featureNamePrefix(u)+r,...w,W,...N,...y)}const $=new Set(["aspect-ratio","color","color-index","device-aspect-ratio","device-height","device-width","height","horizontal-viewport-segments","monochrome","resolution","vertical-viewport-segments","width"]);function transform(e){return e.map((e,t)=>{const i=r(e),a=new WeakMap;e.walk(t=>{const r=t.node;if(!g(r))return;const o=t.parent;if(!w(o))return;const s=r.name.getName().toLowerCase();if(!$.has(s))return;if(y(r)||x(r)){const e=r.operatorKind();if(!1===e)return;const t=transformSingleNameValuePair(s,e,r.value,y(r));return void(t&&(o.feature=t.feature))}const u=i.get(o);if(!N(u))return;let l=null,c=null;{const e=r.valueOneOperatorKind();if(!1===e)return;const t=transformSingleNameValuePair(s,e,r.valueOne,!1);if(!t)return;e===f.LT||e===f.LT_OR_EQ?(l=t,l.before=o.before):(c=t,c.after=o.after)}{const e=r.valueTwoOperatorKind();if(!1===e)return;const t=transformSingleNameValuePair(s,e,r.valueTwo,!0);if(!t)return;e===f.LT||e===f.LT_OR_EQ?(c=t,c.before=o.before):(l=t,l.after=o.after)}if(!l||!c)return;const d=new b(l),m=new b(c),p=getMediaConditionListWithAndFromAncestry(u,i);if(p){let e=a.get(p);if(!e){const t=new Map;p.list.forEach((e,r)=>{t.set(e,r)}),e={indexes:t,inserted:0},a.set(p,e)}if(p.leading===u)return p.leading=d,p.list=[new P([[n.Whitespace," ",-1,-1,void 0],[n.Ident,"and",-1,-1,{value:"and"}],[n.Whitespace," ",-1,-1,void 0]],m),...p.list],void(e.inserted+=1);const t=i.get(u),r=e.indexes.get(t);return p.list.splice("number"==typeof r?r+e.inserted:p.list.indexOf(t),1,new P([[n.Whitespace," ",-1,-1,void 0],[n.Ident,"and",-1,-1,{value:"and"}],[n.Whitespace," ",-1,-1,void 0]],d),new P([[n.Whitespace," ",-1,-1,void 0],[n.Ident,"and",-1,-1,{value:"and"}],[n.Whitespace," ",-1,-1,void 0]],m)),void(e.inserted+=1)}const v=new L(d,[new P([[n.Whitespace," ",-1,-1,void 0],[n.Ident,"and",-1,-1,{value:"and"}],[n.Whitespace," ",-1,-1,void 0]],m)],[[n.Whitespace," ",-1,-1,void 0]]),h=getMediaConditionInShallowMediaQueryFromAncestry(u,e,i);h?h.media=v:u.media=new W(new b(new W(v),[[n.Whitespace," ",-1,-1,void 0],[n.OpenParen,"(",-1,-1,void 0]],[[n.CloseParen,")",-1,-1,void 0]]))});const o=e.tokens();return u(...o.filter((e,r)=>(0!==r||0!==t||!l(e))&&!(l(e)&&o[r+1]&&l(o[r+1]))))}).join(",")}function getMediaConditionListWithAndFromAncestry(e,t){let r=e;if(r){if(r=t.get(r),I(r))return r;if(M(r))return r=t.get(r),I(r)?r:void 0}}function getMediaConditionInShallowMediaQueryFromAncestry(e,t,r){let i=e;if(!i)return;if(i=r.get(i),!Q(i))return;const n=i;return i=r.get(i),S(i)&&i===t?n:void 0}const creator=()=>({postcssPlugin:"postcss-media-minmax",AtRule:{media(e){if(!(e.params.includes("<")||e.params.includes(">")||e.params.includes("=")))return;const t=transform(T(e.params,{preserveInvalidMediaQueries:!0,onParseError:()=>{throw e.error(`Unable to parse media query "${e.params}"`)}}));e.params!==t&&(e.params=t)},"custom-media"(e){if(!(e.params.includes("<")||e.params.includes(">")||e.params.includes("=")))return;const t=O(e.params,{preserveInvalidMediaQueries:!0,onParseError:()=>{throw e.error(`Unable to parse media query "${e.params}"`)}});if(!t||!t.mediaQueryList)return;const r=t.mediaQueryList.map(e=>e.toString()).join(","),i=transform(t.mediaQueryList);r!==i&&(e.params=`${u(...t.name)} ${i}`)}}});creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-media-minmax/src/transform.ts b/plugins/postcss-media-minmax/src/transform.ts index 673d7c43fc..34f42f8187 100644 --- a/plugins/postcss-media-minmax/src/transform.ts +++ b/plugins/postcss-media-minmax/src/transform.ts @@ -23,6 +23,11 @@ export function transform(mediaQueries: Array): string { return mediaQueries.map((mediaQuery, mediaQueryIndex) => { const ancestry = gatherNodeAncestry(mediaQuery); + // Every mutation of a `MediaConditionListWithAnd` inserts exactly one extra + // `MediaAnd`. Remembering the original position of each `MediaAnd` and how + // many insertions happened before it avoids a repeated `indexOf` per match. + const andListPositions = new WeakMap, inserted: number }>(); + mediaQuery.walk((entry) => { const node = entry.node; if (!isMediaFeatureRange(node)) { @@ -116,6 +121,17 @@ export function transform(mediaQueries: Array): string { // ((300px < width < 400px) and (color)) const andList = getMediaConditionListWithAndFromAncestry(grandParent, ancestry); if (andList) { + let andListPosition = andListPositions.get(andList); + if (!andListPosition) { + const indexes = new Map(); + andList.list.forEach((item, index) => { + indexes.set(item, index); + }); + + andListPosition = { indexes, inserted: 0 }; + andListPositions.set(andList, andListPosition); + } + if (andList.leading === grandParent) { andList.leading = parensOne; @@ -131,11 +147,16 @@ export function transform(mediaQueries: Array): string { ...andList.list, ]; + andListPosition.inserted += 1; + return; } + const target = ancestry.get(grandParent) as MediaAnd; + const originalIndex = andListPosition.indexes.get(target); + andList.list.splice( - andList.indexOf(ancestry.get(grandParent) as MediaAnd) as number, + typeof originalIndex === 'number' ? originalIndex + andListPosition.inserted : andList.list.indexOf(target), 1, new MediaAnd( [ @@ -155,6 +176,8 @@ export function transform(mediaQueries: Array): string { ), ); + andListPosition.inserted += 1; + return; } diff --git a/plugins/postcss-minify/CHANGELOG.md b/plugins/postcss-minify/CHANGELOG.md index 941739fb6f..d35467b5d3 100644 --- a/plugins/postcss-minify/CHANGELOG.md +++ b/plugins/postcss-minify/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Minify +### Unreleased (patch) + +- Bound the internal minification cache to avoid unbounded memory growth. + ### 3.0.1 _September 25, 2026_ diff --git a/plugins/postcss-minify/dist/index.mjs b/plugins/postcss-minify/dist/index.mjs index 7c86cc8862..278e0dac60 100644 --- a/plugins/postcss-minify/dist/index.mjs +++ b/plugins/postcss-minify/dist/index.mjs @@ -1 +1 @@ -import{tokenizer as e,isTokenWhitespace as r,isTokenComment as t}from"@csstools/css-tokenizer";const s=/license|copyright|sourcemappingurl/i,o=/\s|\/\*/,n=/^layer$/i;function minify(s,n){if(!n)return n;if(s.has(n))return s.get(n);const a=n.trim();if(""===a)return s.set(n,""),"";if(!o.test(a))return s.set(n,a),a;let i=!1,c=!1,m="";const f=e({css:a});for(;!f.endOfFile();){const e=f.nextToken();r(e)?(i||(m+=" "),i=!0,c=!1):t(e)?(c||(m+="/**/"),i=!1,c=!0):(i=!1,c=!1,m+=e[1])}return s.set(n,m),m}function removeEmptyNodes(e){if("rule"===e.type){if(0===e.nodes?.length){const r=e.parent;return!!r&&(e.remove(),removeEmptyNodes(r),!0)}}else if("atrule"===e.type&&0===e.nodes?.length&&!n.test(e.name)){const r=e.parent;return!!r&&(e.remove(),removeEmptyNodes(r),!0)}return!1}function setSemicolon(e){if(!e.raws.semicolon)return;const r=e.last;"decl"===r?.type&&r.variable||(e.raws.semicolon=!1)}const creator=()=>{const e=new Map;return{postcssPlugin:"postcss-minify",OnceExit(r){r.raws.before="",r.raws.after="\n",r.walk(r=>{switch(r.type){case"atrule":if(removeEmptyNodes(r))return;return r.raws.after="",r.raws.afterName=" ",r.raws.before="",r.raws.between="",r.raws.params=void 0,setSemicolon(r),void(r.params=minify(e,r.params));case"rule":if(removeEmptyNodes(r))return;return r.raws.after="",r.raws.before="",r.raws.between="",r.raws.selector=void 0,setSemicolon(r),void(r.selector=minify(e,r.selector));case"decl":return r.prop.startsWith("--")?void(r.raws.before=""):(r.raws.before="",r.raws.between=":",r.raws.important=r.important?"!important":"",r.raws.value=void 0,void(r.value=minify(e,r.value)));case"comment":if(r.text.startsWith("!")||s.test(r.text))return void(r.raws.before="");r.remove()}})}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{tokenizer as e,isTokenWhitespace as t,isTokenComment as r}from"@csstools/css-tokenizer";const s=/license|copyright|sourcemappingurl/i,o=/\s|\/\*/,n=/^layer$/i;function cacheSet(e,t,r){e.size>5e3&&e.clear(),e.set(t,r)}function minify(s,n){if(!n)return n;if(s.has(n))return s.get(n);const a=n.trim();if(""===a)return cacheSet(s,n,""),"";if(!o.test(a))return cacheSet(s,n,a),a;let i=!1,c=!1,m="";const f=e({css:a});for(;!f.endOfFile();){const e=f.nextToken();t(e)?(i||(m+=" "),i=!0,c=!1):r(e)?(c||(m+="/**/"),i=!1,c=!0):(i=!1,c=!1,m+=e[1])}return cacheSet(s,n,m),m}function removeEmptyNodes(e){if("rule"===e.type){if(0===e.nodes?.length){const t=e.parent;return!!t&&(e.remove(),removeEmptyNodes(t),!0)}}else if("atrule"===e.type&&0===e.nodes?.length&&!n.test(e.name)){const t=e.parent;return!!t&&(e.remove(),removeEmptyNodes(t),!0)}return!1}function setSemicolon(e){if(!e.raws.semicolon)return;const t=e.last;"decl"===t?.type&&t.variable||(e.raws.semicolon=!1)}const creator=()=>{const e=new Map;return{postcssPlugin:"postcss-minify",OnceExit(t){t.raws.before="",t.raws.after="\n",t.walk(t=>{switch(t.type){case"atrule":if(removeEmptyNodes(t))return;return t.raws.after="",t.raws.afterName=" ",t.raws.before="",t.raws.between="",t.raws.params=void 0,setSemicolon(t),void(t.params=minify(e,t.params));case"rule":if(removeEmptyNodes(t))return;return t.raws.after="",t.raws.before="",t.raws.between="",t.raws.selector=void 0,setSemicolon(t),void(t.selector=minify(e,t.selector));case"decl":return t.prop.startsWith("--")?void(t.raws.before=""):(t.raws.before="",t.raws.between=":",t.raws.important=t.important?"!important":"",t.raws.value=void 0,void(t.value=minify(e,t.value)));case"comment":if(t.text.startsWith("!")||s.test(t.text))return void(t.raws.before="");t.remove()}})}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-minify/src/index.ts b/plugins/postcss-minify/src/index.ts index 0b40df0637..571fcfa80e 100644 --- a/plugins/postcss-minify/src/index.ts +++ b/plugins/postcss-minify/src/index.ts @@ -5,6 +5,18 @@ const HAS_LEGAL_KEYWORDS_OR_SOURCE_MAP_REGEX = /license|copyright|sourcemappingu const HAS_WHITESPACE_OR_COMMENTS_REGEX = /\s|\/\*/; const IS_LAYER_REGEX = /^layer$/i; +// Bound the per-instance memoization cache so that processing many files in +// watch or bundler mode does not grow it without limit. +const MAX_CACHE_SIZE = 5000; + +function cacheSet(cache: Map, key: string, value: string): void { + if (cache.size > MAX_CACHE_SIZE) { + cache.clear(); + } + + cache.set(key, value); +} + function minify(cache: Map, x: string): string { if (!x) { return x; @@ -17,12 +29,12 @@ function minify(cache: Map, x: string): string { const y = x.trim(); if (y === '') { - cache.set(x, ''); + cacheSet(cache, x, ''); return ''; } if (!HAS_WHITESPACE_OR_COMMENTS_REGEX.test(y)) { - cache.set(x, y); + cacheSet(cache, x, y); return y; } @@ -56,7 +68,7 @@ function minify(cache: Map, x: string): string { } } - cache.set(x, minified); + cacheSet(cache, x, minified); return minified; } diff --git a/plugins/postcss-overflow-shorthand/CHANGELOG.md b/plugins/postcss-overflow-shorthand/CHANGELOG.md index 9cfb746474..e96ec07d8f 100644 --- a/plugins/postcss-overflow-shorthand/CHANGELOG.md +++ b/plugins/postcss-overflow-shorthand/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Overflow Shorthand +### Unreleased (patch) + +- Improved performance by inserting generated declarations as a single batch + ### 7.0.0 _January 14, 2026_ diff --git a/plugins/postcss-overflow-shorthand/dist/index.mjs b/plugins/postcss-overflow-shorthand/dist/index.mjs index 3425e99168..a6cfa082c8 100644 --- a/plugins/postcss-overflow-shorthand/dist/index.mjs +++ b/plugins/postcss-overflow-shorthand/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-value-parser";const creator=o=>{const r=Object.assign({preserve:!0},o);return{postcssPlugin:"postcss-overflow-shorthand",Declaration(o,{result:t}){if("overflow"!==o.prop.toLowerCase())return;let s,a;const l=o.value;try{const o=e(l).nodes.slice().filter(e=>"comment"!==e.type&&"space"!==e.type);if(o.length<2)return;s=e.stringify(o[0]),a=e.stringify(o[1])}catch{return void o.warn(t,`Failed to parse value '${l}' as a shorthand for "overflow". Leaving the original value intact.`)}s&&a&&(s.toLowerCase()===a.toLowerCase()?o.cloneBefore({value:s}):(o.cloneBefore({prop:"overflow-x",value:s}),o.cloneBefore({prop:"overflow-y",value:a})),r.preserve||o.remove())}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import e from"postcss-value-parser";const creator=o=>{const r=Object.assign({preserve:!0},o);return{postcssPlugin:"postcss-overflow-shorthand",Declaration(o,{result:t}){if("overflow"!==o.prop.toLowerCase())return;let s,a;const l=o.value;try{const o=e(l).nodes.slice().filter(e=>"comment"!==e.type&&"space"!==e.type);if(o.length<2)return;s=e.stringify(o[0]),a=e.stringify(o[1])}catch{return void o.warn(t,`Failed to parse value '${l}' as a shorthand for "overflow". Leaving the original value intact.`)}s&&a&&(s.toLowerCase()===a.toLowerCase()?o.cloneBefore({value:s}):o.before([o.clone({prop:"overflow-x",value:s}),o.clone({prop:"overflow-y",value:a})]),r.preserve||o.remove())}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-overflow-shorthand/src/index.ts b/plugins/postcss-overflow-shorthand/src/index.ts index d617b456db..b7cc5e8a29 100644 --- a/plugins/postcss-overflow-shorthand/src/index.ts +++ b/plugins/postcss-overflow-shorthand/src/index.ts @@ -58,15 +58,18 @@ const creator: PluginCreator = (opts?: pluginOptions) => { value: xValue, }); } else { - decl.cloneBefore({ - prop: 'overflow-x', - value: xValue, - }); - - decl.cloneBefore({ - prop: 'overflow-y', - value: yValue, - }); + // Insert as a batch: resolving the reference index for every single + // insertion is quadratic for a rule with many declarations. + decl.before([ + decl.clone({ + prop: 'overflow-x', + value: xValue, + }), + decl.clone({ + prop: 'overflow-y', + value: yValue, + }), + ]); } if (!options.preserve) { diff --git a/plugins/postcss-place/CHANGELOG.md b/plugins/postcss-place/CHANGELOG.md index e454743ad3..99be504e41 100644 --- a/plugins/postcss-place/CHANGELOG.md +++ b/plugins/postcss-place/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Place Properties +### Unreleased (patch) + +- Improved performance by inserting generated declarations as a single batch + ### 11.0.0 _January 14, 2026_ diff --git a/plugins/postcss-place/dist/index.mjs b/plugins/postcss-place/dist/index.mjs index 26bd4fe3db..70012ac753 100644 --- a/plugins/postcss-place/dist/index.mjs +++ b/plugins/postcss-place/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-value-parser";function onCSSDeclaration(o,s,r){const a=o.prop.match(t)?.[1].toLowerCase();if(!a)return;let n,l;try{n=e(o.value)}catch{o.warn(s,`Failed to parse value '${o.value}'. Leaving the original value intact.`)}void 0!==n&&(l=n.nodes.length?n.nodes.filter(e=>"word"===e.type||"function"===e.type).map(t=>e.stringify(t)):[e.stringify(n.nodes)],o.cloneBefore({prop:`align-${a}`,value:l[0]}),o.cloneBefore({prop:`justify-${a}`,value:l[1]||l[0]}),r.preserve||o.remove())}const t=/^place-(content|items|self)/i,creator=e=>{const o=Object.assign({preserve:!0},e);return{postcssPlugin:"postcss-place",Declaration(e,{result:s}){t.test(e.prop)&&onCSSDeclaration(e,s,o)}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import e from"postcss-value-parser";function onCSSDeclaration(o,s,r){const a=o.prop.match(t)?.[1].toLowerCase();if(!a)return;let n,l;try{n=e(o.value)}catch{o.warn(s,`Failed to parse value '${o.value}'. Leaving the original value intact.`)}void 0!==n&&(l=n.nodes.length?n.nodes.filter(e=>"word"===e.type||"function"===e.type).map(t=>e.stringify(t)):[e.stringify(n.nodes)],o.before([o.clone({prop:`align-${a}`,value:l[0]}),o.clone({prop:`justify-${a}`,value:l[1]||l[0]})]),r.preserve||o.remove())}const t=/^place-(content|items|self)/i,creator=e=>{const o=Object.assign({preserve:!0},e);return{postcssPlugin:"postcss-place",Declaration(e,{result:s}){t.test(e.prop)&&onCSSDeclaration(e,s,o)}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-place/src/on-css-declaration.ts b/plugins/postcss-place/src/on-css-declaration.ts index 468124f4d8..b7f08e3f9d 100644 --- a/plugins/postcss-place/src/on-css-declaration.ts +++ b/plugins/postcss-place/src/on-css-declaration.ts @@ -36,15 +36,18 @@ export function onCSSDeclaration(decl: Declaration, result: Result , options: { }); } - decl.cloneBefore({ - prop: `align-${alignment}`, - value: alignmentValues[0], - }); - - decl.cloneBefore({ - prop: `justify-${alignment}`, - value: alignmentValues[1] || alignmentValues[0], - }); + // Insert as a batch: resolving the reference index for every single + // insertion is quadratic for a rule with many declarations. + decl.before([ + decl.clone({ + prop: `align-${alignment}`, + value: alignmentValues[0], + }), + decl.clone({ + prop: `justify-${alignment}`, + value: alignmentValues[1] || alignmentValues[0], + }), + ]); // conditionally remove place-[alignment] if (!options.preserve) { diff --git a/plugins/postcss-progressive-custom-properties/CHANGELOG.md b/plugins/postcss-progressive-custom-properties/CHANGELOG.md index 3d8c1bdd94..7050c7d91d 100644 --- a/plugins/postcss-progressive-custom-properties/CHANGELOG.md +++ b/plugins/postcss-progressive-custom-properties/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Progressive Custom Properties +### Unreleased (patch) + +- Improve performance when inserting generated `@supports` rules. + ### 5.1.3 _September 6, 2026_ diff --git a/plugins/postcss-progressive-custom-properties/dist/index.mjs b/plugins/postcss-progressive-custom-properties/dist/index.mjs index 6d4a9996b4..c72404ab4d 100644 --- a/plugins/postcss-progressive-custom-properties/dist/index.mjs +++ b/plugins/postcss-progressive-custom-properties/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-value-parser";const r=["at","bottom","center","circle","closest-corner","closest-side","ellipse","farthest-corner","farthest-side","from","in","left","right","to","top"];function doublePositionGradients(e){const t=[],a=e.value.toLowerCase();if("function"===e.type&&("conic-gradient"===a||"linear-gradient"===a||"radial-gradient"===a||"repeating-conic-gradient"===a||"repeating-linear-gradient"===a||"repeating-radial-gradient"===a)){let a=0,o=!1,i=!1;e:for(let s=0;si.includes(e.sniff)&&(e.only_on_property??r.prop)===r.prop);let p=!1;try{e(i).walk(r=>{"function"===r.type&&a.test(r.value)&&(p=!0);try{const t=e.unit(r.value);!1!==t&&(r.dimension=t)}catch{}for(let e=0;e{"decl"===t.type&&t.prop===r.prop||e.append(t.clone())}),void e.append(r.clone());const t=o.get(r.prop.toLowerCase());if(!t?.length)return void e.append(r.clone());const a=[r];let i=r.next();for(;i;)"decl"===i.type&&t.includes(i.prop)&&a.push(i),i=i.next();a.forEach(r=>{e.append(r.clone())})}const creator=()=>({postcssPlugin:"postcss-progressive-custom-properties",prepare(){const e=new WeakMap;return{postcssPlugin:"postcss-progressive-custom-properties",OnceExit(r,{postcss:t}){r.walkAtRules(e=>{if(!p.test(e.name))return;if(inSupports(e))return;const r=[],a={propNames:new Set},o=e.clone();if(e.walkDecls(e=>{let t=e.prop;if(e.variable||(t=e.prop.toLowerCase()),!a.propNames.has(t))return void a.propNames.add(t);if(!e.variable&&!i.test(e.value))return;if(s.test(e.value))return;if(d.test(e.value))return;const o=conditionsFromValue(e,!e.variable);o.length&&(r.push(...o),e.remove())}),!r.length)return;const n=Array.from(new Set(r)).sort().join(" and ");if(!n)return;const l=t.atRule({name:"supports",params:n,source:e.source,raws:{before:"\n\n",after:"\n"}});l.append(o),e.after(l)}),r.walkDecls(r=>{if(!r.parent)return;if(inKeyframes(r))return;const a=e.get(r.parent)||{conditionalRules:[],propNames:new Set,lastConditionParams:void 0,lastConditionalRule:void 0};if(e.set(r.parent,a),r.variable){if(!a.propNames.has(r.prop))return void a.propNames.add(r.prop)}else{const e=r.prop.toLowerCase();if(!a.propNames.has(e))return void a.propNames.add(e)}if(!(r.variable||"atrule"===r.parent.type&&n.test(r.parent.name)||i.test(r.value)))return;if(s.test(r.value))return;if(d.test(r.value))return;const o=conditionsFromValue(r,!(r.variable||"atrule"===r.parent.type&&n.test(r.parent.name))).join(" and ");if(!o)return;if(a.lastConditionParams!==o&&(a.lastConditionalRule=void 0),a.lastConditionalRule)return cloneDeclarations(a.lastConditionalRule,r),void r.remove();const p=[];if(o&&p.push(t.atRule({name:"supports",params:o,source:r.parent.source,raws:{before:"\n\n",after:"\n"}})),!p.length)return;for(let e=0;e{const t=e.get(r);t&&0!==t.conditionalRules.length&&t.conditionalRules.reverse().forEach(e=>{r.after(e)})})}}}});creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import e from"postcss-value-parser";const r=["at","bottom","center","circle","closest-corner","closest-side","ellipse","farthest-corner","farthest-side","from","in","left","right","to","top"];function doublePositionGradients(e){const t=[],a=e.value.toLowerCase();if("function"===e.type&&("conic-gradient"===a||"linear-gradient"===a||"radial-gradient"===a||"repeating-conic-gradient"===a||"repeating-linear-gradient"===a||"repeating-radial-gradient"===a)){let a=0,o=!1,i=!1;e:for(let s=0;si.includes(e.sniff)&&(e.only_on_property??r.prop)===r.prop);let p=!1;try{e(i).walk(r=>{"function"===r.type&&a.test(r.value)&&(p=!0);try{const t=e.unit(r.value);!1!==t&&(r.dimension=t)}catch{}for(let e=0;e{"decl"===t.type&&t.prop===r.prop||e.append(t.clone())}),void e.append(r.clone());const t=o.get(r.prop.toLowerCase());if(!t?.length)return void e.append(r.clone());const a=[r];let i=r.next();for(;i;)"decl"===i.type&&t.includes(i.prop)&&a.push(i),i=i.next();a.forEach(r=>{e.append(r.clone())})}const creator=()=>({postcssPlugin:"postcss-progressive-custom-properties",prepare(){const e=new WeakMap;return{postcssPlugin:"postcss-progressive-custom-properties",OnceExit(r,{postcss:t}){r.walkAtRules(e=>{if(!p.test(e.name))return;if(inSupports(e))return;const r=[],a={propNames:new Set},o=e.clone();if(e.walkDecls(e=>{let t=e.prop;if(e.variable||(t=e.prop.toLowerCase()),!a.propNames.has(t))return void a.propNames.add(t);if(!e.variable&&!i.test(e.value))return;if(s.test(e.value))return;if(d.test(e.value))return;const o=conditionsFromValue(e,!e.variable);o.length&&(r.push(...o),e.remove())}),!r.length)return;const n=Array.from(new Set(r)).sort().join(" and ");if(!n)return;const l=t.atRule({name:"supports",params:n,source:e.source,raws:{before:"\n\n",after:"\n"}});l.append(o),e.after(l)}),r.walkDecls(r=>{if(!r.parent)return;if(inKeyframes(r))return;const a=e.get(r.parent)||{conditionalRules:[],propNames:new Set,lastConditionParams:void 0,lastConditionalRule:void 0};if(e.set(r.parent,a),r.variable){if(!a.propNames.has(r.prop))return void a.propNames.add(r.prop)}else{const e=r.prop.toLowerCase();if(!a.propNames.has(e))return void a.propNames.add(e)}if(!(r.variable||"atrule"===r.parent.type&&n.test(r.parent.name)||i.test(r.value)))return;if(s.test(r.value))return;if(d.test(r.value))return;const o=conditionsFromValue(r,!(r.variable||"atrule"===r.parent.type&&n.test(r.parent.name))).join(" and ");if(!o)return;if(a.lastConditionParams!==o&&(a.lastConditionalRule=void 0),a.lastConditionalRule)return cloneDeclarations(a.lastConditionalRule,r),void r.remove();const p=[];if(o&&p.push(t.atRule({name:"supports",params:o,source:r.parent.source,raws:{before:"\n\n",after:"\n"}})),!p.length)return;for(let e=0;e{const t=e.get(r);t&&0!==t.conditionalRules.length&&r.after(t.conditionalRules)})}}}});creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-progressive-custom-properties/src/index.ts b/plugins/postcss-progressive-custom-properties/src/index.ts index c03a878086..5c9faca882 100644 --- a/plugins/postcss-progressive-custom-properties/src/index.ts +++ b/plugins/postcss-progressive-custom-properties/src/index.ts @@ -291,11 +291,8 @@ const creator: PluginCreator = () => { return; } - // rule.after reverses the at rule order. - // reversing the call order gives in the correct order overall. - state.conditionalRules.reverse().forEach((atSupports) => { - node.after(atSupports); - }); + // `after` inserts an array of nodes in order. + node.after(state.conditionalRules); }); }, }; diff --git a/plugins/postcss-property-rule-prelude-list/CHANGELOG.md b/plugins/postcss-property-rule-prelude-list/CHANGELOG.md index 83670086dd..ee478deb0a 100644 --- a/plugins/postcss-property-rule-prelude-list/CHANGELOG.md +++ b/plugins/postcss-property-rule-prelude-list/CHANGELOG.md @@ -1,5 +1,10 @@ # Changes to PostCSS Property Rule Prelude List +### Unreleased (patch) + +- Bound the total number of generated at-rules to prevent an exponential blowup from nested `@property` rules. + + ### 2.0.1 _September 25, 2026_ diff --git a/plugins/postcss-property-rule-prelude-list/dist/index.mjs b/plugins/postcss-property-rule-prelude-list/dist/index.mjs index 7435cceca5..c85ac821fa 100644 --- a/plugins/postcss-property-rule-prelude-list/dist/index.mjs +++ b/plugins/postcss-property-rule-prelude-list/dist/index.mjs @@ -1 +1 @@ -import{parseCommaSeparatedListOfComponentValues as s,stringify as r}from"@csstools/css-parser-algorithms";import{tokenize as e}from"@csstools/css-tokenizer";const o=/^property$/i,creator=()=>({postcssPlugin:"postcss-property-rule-prelude-list",AtRule(t){if(!o.test(t.name))return;if(!t.params.includes(","))return;const p=s(e({css:t.params}));p.length<2||(p.forEach(s=>{t.cloneBefore({params:r([s]).trim()})}),t.remove())}});creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parseCommaSeparatedListOfComponentValues as e,stringify as s}from"@csstools/css-parser-algorithms";import{tokenize as t}from"@csstools/css-tokenizer";const r=/^property$/i;function countAtRules(e){let s=1;return e.walkAtRules(()=>{s++}),s}const creator=()=>{let o=0;return{postcssPlugin:"postcss-property-rule-prelude-list",Once(){o=0},AtRule(l){if(!r.test(l.name))return;if(!l.params.includes(","))return;const n=e(t({css:l.params}));if(!(n.length<2)){if(o+=n.length*countAtRules(l),o>1e4)throw new Error("Maximum @property expansion size exceeded, reduce the complexity of your stylesheet");n.forEach(e=>{l.cloneBefore({params:s([e]).trim()})}),l.remove()}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-property-rule-prelude-list/src/index.ts b/plugins/postcss-property-rule-prelude-list/src/index.ts index 9e9b3099f2..860f99e907 100644 --- a/plugins/postcss-property-rule-prelude-list/src/index.ts +++ b/plugins/postcss-property-rule-prelude-list/src/index.ts @@ -1,16 +1,35 @@ import { parseCommaSeparatedListOfComponentValues, stringify } from '@csstools/css-parser-algorithms'; import { tokenize } from '@csstools/css-tokenizer'; -import type { PluginCreator } from 'postcss'; +import type { AtRule, PluginCreator } from 'postcss'; /** postcss-property-rule-prelude-list plugin options */ export type pluginOptions = never; const IS_AT_PROPERTY_REGEX = /^property$/i; +// A comma separated prelude is expanded by cloning the whole at-rule (including +// its children) once per list item. When such rules are nested, the clones are +// expanded again and the output grows exponentially with the nesting depth. +// Bound the total number of at-rules generated per stylesheet. +const MAX_EXPANDED_AT_RULES = 10_000; + +function countAtRules(atRule: AtRule): number { + let count = 1; + atRule.walkAtRules(() => { + count++; + }); + + return count; +} + const creator: PluginCreator = () => { + let expandedAtRules = 0; return { postcssPlugin: 'postcss-property-rule-prelude-list', + Once(): void { + expandedAtRules = 0; + }, AtRule(atRule): void { if (!IS_AT_PROPERTY_REGEX.test(atRule.name)) { return; @@ -25,6 +44,11 @@ const creator: PluginCreator = () => { return; } + expandedAtRules += list.length * countAtRules(atRule); + if (expandedAtRules > MAX_EXPANDED_AT_RULES) { + throw new Error('Maximum @property expansion size exceeded, reduce the complexity of your stylesheet'); + } + list.forEach((params) => { atRule.cloneBefore({ params: stringify([params]).trim(), diff --git a/plugins/postcss-rewrite-url/CHANGELOG.md b/plugins/postcss-rewrite-url/CHANGELOG.md index d5b24e8110..7705e4d816 100644 --- a/plugins/postcss-rewrite-url/CHANGELOG.md +++ b/plugins/postcss-rewrite-url/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Rewrite URL +### Unreleased (patch) + +- Visit each declaration and at-rule only once, so the rewriter is called at most once per node. + ### 3.0.1 _September 25, 2026_ diff --git a/plugins/postcss-rewrite-url/dist/index.mjs b/plugins/postcss-rewrite-url/dist/index.mjs index 4c716de36c..5127ac53d4 100644 --- a/plugins/postcss-rewrite-url/dist/index.mjs +++ b/plugins/postcss-rewrite-url/dist/index.mjs @@ -1 +1 @@ -import{tokenize as r,isTokenString as e}from"@csstools/css-tokenizer";import{parseCommaSeparatedListOfComponentValues as t,replaceComponentValues as o,isFunctionNode as s,isWhiteSpaceOrCommentNode as i,isTokenNode as a,parseListOfComponentValues as l,stringify as n}from"@csstools/css-parser-algorithms";function serializeString(r){let e="";for(const t of r){const r=t.codePointAt(0);if(void 0!==r)switch(r){case 0:e+=String.fromCodePoint(65533);break;case 127:e+=`\\${r.toString(16)}`;break;case 34:case 39:case 92:e+=`\\${t}`;break;default:if(1<=r&&r<=31){e+=`\\${r.toString(16)} `;break}e+=t}else e+=String.fromCodePoint(65533)}return e}const u=/rewrite-url\(/i,c=/^rewrite-url$/i,creator=r=>{const e=r?.rewriter??(r=>r);return{postcssPlugin:"postcss-rewrite-url",Once(r,{result:t}){r.walkDecls(r=>{rewriteDeclaration(r,t,e)}),r.walkAtRules(r=>{rewriteAtRule(r,t,e)})},Declaration(r,{result:t}){rewriteDeclaration(r,t,e)},AtRule(r,{result:t}){rewriteAtRule(r,t,e)}}};function rewriteDeclaration(r,e,t){if(!u.test(r.value))return;const o={type:"declaration-value",rootFrom:e.opts.from,from:r.source?.input.from,property:r.prop},s=rewrite(t,r.value,o);s!==r.value&&(r.value=s)}function rewriteAtRule(r,e,t){if(!u.test(r.params))return;const o={type:"at-rule-prelude",rootFrom:e.opts.from,from:r.source?.input.from,atRuleName:r.name},s=rewrite(t,r.params,o);s!==r.params&&(r.params=s)}function rewrite(u,f,m){const p=t(r({css:f})),w=o(p,t=>{if(!s(t)||!c.test(t.getName()))return;const o=t.value.filter(r=>!i(r));for(let s=0;sr.toString())},m);if(!1===n)return;const c=l(r({css:[`"${serializeString(n.url)}"`,...n.urlModifiers??a].join(" ")}));return t.value=c,t.name[1]="url(",t.name[4].value="url",t}return}});return n(w)}creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{tokenize as r,isTokenString as e}from"@csstools/css-tokenizer";import{parseCommaSeparatedListOfComponentValues as t,replaceComponentValues as o,isFunctionNode as s,isWhiteSpaceOrCommentNode as i,isTokenNode as a,parseListOfComponentValues as n,stringify as u}from"@csstools/css-parser-algorithms";function serializeString(r){let e="";for(const t of r){const r=t.codePointAt(0);if(void 0!==r)switch(r){case 0:e+=String.fromCodePoint(65533);break;case 127:e+=`\\${r.toString(16)}`;break;case 34:case 39:case 92:e+=`\\${t}`;break;default:if(1<=r&&r<=31){e+=`\\${r.toString(16)} `;break}e+=t}else e+=String.fromCodePoint(65533)}return e}const l=/rewrite-url\(/i,c=/^rewrite-url$/i,creator=r=>{const e=r?.rewriter??(r=>r);return{postcssPlugin:"postcss-rewrite-url",Declaration(r,{result:t}){rewriteDeclaration(r,t,e)},AtRule(r,{result:t}){rewriteAtRule(r,t,e)}}};function rewriteDeclaration(r,e,t){if(!l.test(r.value))return;const o={type:"declaration-value",rootFrom:e.opts.from,from:r.source?.input.from,property:r.prop},s=rewrite(t,r.value,o);s!==r.value&&(r.value=s)}function rewriteAtRule(r,e,t){if(!l.test(r.params))return;const o={type:"at-rule-prelude",rootFrom:e.opts.from,from:r.source?.input.from,atRuleName:r.name},s=rewrite(t,r.params,o);s!==r.params&&(r.params=s)}function rewrite(l,f,m){const p=t(r({css:f})),v=o(p,t=>{if(!s(t)||!c.test(t.getName()))return;const o=t.value.filter(r=>!i(r));for(let s=0;sr.toString())},m);if(!1===u)return;const c=n(r({css:[`"${serializeString(u.url)}"`,...u.urlModifiers??a].join(" ")}));return t.value=c,t.name[1]="url(",t.name[4].value="url",t}return}});return u(v)}creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-rewrite-url/src/index.ts b/plugins/postcss-rewrite-url/src/index.ts index 11ceed683d..666724a255 100644 --- a/plugins/postcss-rewrite-url/src/index.ts +++ b/plugins/postcss-rewrite-url/src/index.ts @@ -33,15 +33,6 @@ const creator: PluginCreator = (options?: pluginOptions) => { return { postcssPlugin: 'postcss-rewrite-url', - Once(root, { result }): void { - root.walkDecls((decl) => { - rewriteDeclaration(decl, result, rewriter); - }); - - root.walkAtRules((atRule) => { - rewriteAtRule(atRule, result, rewriter); - }); - }, Declaration(decl, { result }): void { rewriteDeclaration(decl, result, rewriter); }, diff --git a/plugins/postcss-system-ui-font-family/CHANGELOG.md b/plugins/postcss-system-ui-font-family/CHANGELOG.md index 72f72042f7..e5fb3f10ea 100644 --- a/plugins/postcss-system-ui-font-family/CHANGELOG.md +++ b/plugins/postcss-system-ui-font-family/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS System UI Font Family +### Unreleased (patch) + +- Improved performance by inserting generated declarations as a single batch + ### 2.0.1 _September 25, 2026_ diff --git a/plugins/postcss-system-ui-font-family/dist/index.mjs b/plugins/postcss-system-ui-font-family/dist/index.mjs index 7800b2c946..6fe7c548da 100644 --- a/plugins/postcss-system-ui-font-family/dist/index.mjs +++ b/plugins/postcss-system-ui-font-family/dist/index.mjs @@ -1 +1 @@ -import{parseCommaSeparatedListOfComponentValues as e,isWhiteSpaceOrCommentNode as s,isTokenNode as t,stringify as o,TokenNode as n}from"@csstools/css-parser-algorithms";import{tokenize as r,isTokenIdent as i,TokenType as l}from"@csstools/css-tokenizer";const a=/^font(?:-family)?$/i,c=["system-ui","-apple-system","Segoe UI","Roboto","Ubuntu","Cantarell","Noto Sans","sans-serif"];const u=c.join(","),creator=p=>{const f=Object.assign({preserve:!0},p);return{postcssPlugin:"postcss-system-ui-font-family",Declaration(p){if(!p.variable&&!a.test(p.prop))return;if(p.value.includes(u))return;const m=p.value,v=r({css:m}),g=e(v),y=new Map;let h=-1;e:for(let e=0;e!("system-ui"!==e||!j)||!b.has(e)).map(e=>[new n([l.Ident,e,-1,-1,{value:e}])]));var b,j;if(0===d.length)return;g.splice(h,1,...d);const w=o(g);w!==m&&(p.cloneBefore({prop:p.prop,value:w}),p.remove())}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parseCommaSeparatedListOfComponentValues as e,isWhiteSpaceOrCommentNode as s,isTokenNode as t,stringify as o,TokenNode as n}from"@csstools/css-parser-algorithms";import{tokenize as r,isTokenIdent as i,TokenType as l}from"@csstools/css-tokenizer";const a=/^font(?:-family)?$/i,c=["system-ui","-apple-system","Segoe UI","Roboto","Ubuntu","Cantarell","Noto Sans","sans-serif"];const u=c.join(","),creator=p=>{const f=Object.assign({preserve:!0},p);return{postcssPlugin:"postcss-system-ui-font-family",Declaration(p){if(!p.variable&&!a.test(p.prop))return;if(p.value.includes(u))return;const m=p.value,v=r({css:m}),g=e(v),y=new Map;let h=-1;e:for(let e=0;e!("system-ui"!==e||!j)||!d.has(e)).map(e=>[new n([l.Ident,e,-1,-1,{value:e}])]));var d,j;if(0===b.length)return;g.splice(h,1,...b);const w=o(g);w!==m&&(p.before([p.clone({prop:p.prop,value:w})]),p.remove())}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-system-ui-font-family/src/index.ts b/plugins/postcss-system-ui-font-family/src/index.ts index 671ab349ba..ab68868ce1 100644 --- a/plugins/postcss-system-ui-font-family/src/index.ts +++ b/plugins/postcss-system-ui-font-family/src/index.ts @@ -115,10 +115,14 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - decl.cloneBefore({ - prop: decl.prop, - value: modified, - }); + // Insert as a batch: resolving the reference index for every single + // insertion is quadratic for a rule with many declarations. + decl.before([ + decl.clone({ + prop: decl.prop, + value: modified, + }), + ]); decl.remove(); }, diff --git a/plugins/postcss-text-decoration-shorthand/CHANGELOG.md b/plugins/postcss-text-decoration-shorthand/CHANGELOG.md index 787e65a86e..6f9240b4de 100644 --- a/plugins/postcss-text-decoration-shorthand/CHANGELOG.md +++ b/plugins/postcss-text-decoration-shorthand/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Text Decoration Shorthand +### Unreleased (patch) + +- Improved performance by removing the O(n) `parent.index()` lookup and sibling value scan per declaration + ### 5.0.6 _September 25, 2026_ diff --git a/plugins/postcss-text-decoration-shorthand/dist/index.mjs b/plugins/postcss-text-decoration-shorthand/dist/index.mjs index 44389a1184..13d474b6a9 100644 --- a/plugins/postcss-text-decoration-shorthand/dist/index.mjs +++ b/plugins/postcss-text-decoration-shorthand/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-value-parser";import{namedColors as t}from"@csstools/color-helpers";const o=/^text-decoration$/i,creator=t=>{const c=Object.assign({preserve:!0},t);return{postcssPlugin:"postcss-text-decoration-shorthand",prepare(){const t=new Map;return{postcssPlugin:"postcss-text-decoration-shorthand",OnceExit(){t.clear()},Declaration(i){if(!o.test(i.prop))return;const a=i.parent;if(!a)return;const u=a.index(i),d=[];for(let e=0;et.get(i.value)===e.value))return;const p=e(i.value),f=p.nodes.filter(e=>"space"!==e.type&&"comment"!==e.type);if(f.find(e=>"var"===e.value.toLowerCase()&&"function"===e.type))return;if(f.find(e=>"word"===e.type&&r.includes(e.value)))return;const v={line:[],style:null,color:null,thickness:null};for(let t=0;tt.get(i.value)===e.value))return;if(i.value.toLowerCase()===y.toLowerCase()){let e=i.next();for(;e&&"comment"===e.type;)e=e.next();return void(e&&"decl"===e.type&&"text-decoration"===e.prop.toLowerCase()||i.cloneBefore({prop:"-webkit-text-decoration",value:y}))}i.cloneBefore({prop:"text-decoration",value:y});const h=e.stringify([...v.line,{before:"",after:"",sourceIndex:0,sourceEndIndex:0,type:"space",value:" "},v.style,{before:"",after:"",sourceIndex:0,sourceEndIndex:0,type:"space",value:" "},v.color]);v.thickness&&i.cloneBefore({prop:"text-decoration",value:h}),v.thickness&&i.cloneBefore({prop:"text-decoration-thickness",value:e.stringify([v.thickness])}),t.set(h,y),c.preserve||i.remove()}}}}};function nodeIsAColor(e){return!("word"!==e.type||!e.value.startsWith("#"))||(!("word"!==e.type||!i.includes(e.value.toLowerCase()))||!("function"!==e.type||!c.includes(e.value.toLowerCase())))}creator.postcss=!0;const r=["unset","inherit","initial","revert","revert-layer"],n=["underline","overline","line-through","blink","spelling-error","grammar-error"],s=["solid","double","dotted","dashed","wavy"],l=["auto","from-font"],c=["color","color-mix","hsl","hsla","hwb","lab","lch","oklab","oklch","rgb","rgba"],i=["currentcolor","transparent",...Object.keys(t)];function genericNodeParts(){return{before:"",after:"",sourceIndex:0,sourceEndIndex:0}}export{creator as default,creator as"module.exports"}; +import e from"postcss-value-parser";import{namedColors as t}from"@csstools/color-helpers";const o=/^text-decoration$/i,creator=t=>{const c=Object.assign({preserve:!0},t);return{postcssPlugin:"postcss-text-decoration-shorthand",prepare(){const t=new Map;return{postcssPlugin:"postcss-text-decoration-shorthand",OnceExit(){t.clear()},Declaration(a){if(!o.test(a.prop))return;const i=a.parent;if(!i)return;const u=a.proxyOf??a,d=new Set;for(let e=0;e"space"!==e.type&&"comment"!==e.type);if(v.find(e=>"var"===e.value.toLowerCase()&&"function"===e.type))return;if(v.find(e=>"word"===e.type&&r.includes(e.value)))return;const y={line:[],style:null,color:null,thickness:null};for(let t=0;t = (opts?: pluginOptions) => { return; } - const ownIndex = parent.index(decl); + // Visitors receive a proxy node, while `parent.nodes` holds the + // underlying nodes. Unwrap to compare identity without an O(n) + // `parent.index()` lookup. + const selfNode = (decl as typeof decl & { proxyOf?: typeof decl }).proxyOf ?? decl; - const siblingTextDecorationProperties: Array = []; + const siblingValues = new Set(); for (let i = 0; i < parent.nodes.length; i++) { - if (i === ownIndex) { + const node = parent.nodes[i]; + if (node === selfNode) { continue; } - const node = parent.nodes[i]; if (node.type === 'decl' && IS_TEXT_DECORATION_REGEX.test(node.prop)) { - siblingTextDecorationProperties.push(node); + siblingValues.add(node.value); } } - if (siblingTextDecorationProperties.some((node) => { - return convertedValues.get(decl.value) === node.value; - })) { + let convertedValue = convertedValues.get(decl.value); + if (convertedValue !== undefined && siblingValues.has(convertedValue)) { return; } @@ -202,9 +204,8 @@ const creator: PluginCreator = (opts?: pluginOptions) => { const nonShortHandValue = valueParser.stringify(data.line); convertedValues.set(decl.value, nonShortHandValue); - if (siblingTextDecorationProperties.some((node) => { - return convertedValues.get(decl.value) === node.value; - })) { + convertedValue = convertedValues.get(decl.value); + if (convertedValue !== undefined && siblingValues.has(convertedValue)) { return; } From fbeab9f9f98b9e8ac39a9e92017dca9b20473f57 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Wed, 30 Sep 2026 19:50:06 +0200 Subject: [PATCH 2/6] fix --- packages/utilities/dist/index.mjs | 2 +- packages/utilities/src/has-fallback-decl.ts | 63 +++++++------------ .../dist/index.mjs | 2 +- .../src/index.ts | 9 +-- 4 files changed, 29 insertions(+), 47 deletions(-) diff --git a/packages/utilities/dist/index.mjs b/packages/utilities/dist/index.mjs index 3f1311c71a..db147ba417 100644 --- a/packages/utilities/dist/index.mjs +++ b/packages/utilities/dist/index.mjs @@ -1 +1 @@ -function unwrapProxy(t){return t.proxyOf??t}const t=new WeakMap;function fallbackIndexFor(e){const n=e.nodes||[];let a=t.get(e);if(a&&a.nodeCount===n.length&&a.firstNode===n[0]&&a.lastNode===n[n.length-1])return a;const r=new WeakSet,o=new Set;for(let t=0;t(node: T): T { - return (node as T & { proxyOf?: T }).proxyOf ?? node; -} - -/** - * Per-container set of declarations that have a fallback: a declaration with - * the same (ASCII lowercased) property name appears before it. + * Per container, whether the declaration at each index has a fallback. * - * `hasFallback` is called once per matching declaration. The naive version - * (`parent.index(node)` + a sibling scan) is O(n) per call, which makes a rule - * with many declarations O(n^2). The answer for every declaration in a - * container is computed in a single pass and reused until the container's node - * list changes. + * `hasFallback` is called once per matching declaration. Checking every + * declaration independently (`parent.index(node)` + a sibling scan) is O(n) per + * call, which makes a rule with many declarations O(n^2). The answer for every + * declaration in a container is computed in a single pass and reused until the + * container's node list changes. */ type FallbackIndex = { nodeCount: number, - firstNode: unknown, - lastNode: unknown, - declarationsWithFallback: WeakSet, + fallbackAtIndex: Array, }; const fallbackIndexCache = new WeakMap(); @@ -31,17 +19,15 @@ const fallbackIndexCache = new WeakMap(); function fallbackIndexFor(parent: Container): FallbackIndex { const nodes = parent.nodes || []; - let index = fallbackIndexCache.get(parent); - if ( - index && - index.nodeCount === nodes.length && - index.firstNode === nodes[0] && - index.lastNode === nodes[nodes.length - 1] - ) { - return index; + const cached = fallbackIndexCache.get(parent); + if (cached && cached.nodeCount === nodes.length) { + return cached; } - const declarationsWithFallback = new WeakSet(); + const fallbackAtIndex: Array = []; + for (let i = 0; i < nodes.length; i++) { + fallbackAtIndex.push(false); + } const seenProps = new Set(); for (let i = 0; i < nodes.length; i++) { @@ -50,21 +36,15 @@ function fallbackIndexFor(parent: Container): FallbackIndex { continue; } - const decl = sibling; - const prop = decl.prop.toLowerCase(); + const prop = sibling.prop.toLowerCase(); if (seenProps.has(prop)) { - declarationsWithFallback.add(decl); + fallbackAtIndex[i] = true; } else { seenProps.add(prop); } } - index = { - nodeCount: nodes.length, - firstNode: nodes[0], - lastNode: nodes[nodes.length - 1], - declarationsWithFallback, - }; + const index = { nodeCount: nodes.length, fallbackAtIndex }; fallbackIndexCache.set(parent, index); return index; @@ -83,5 +63,10 @@ export function hasFallback(node: Declaration): boolean { return false; } - return fallbackIndexFor(unwrapProxy(parent)).declarationsWithFallback.has(unwrapProxy(node)); + const nodeIndex = parent.index(node); + if (nodeIndex < 0) { + return false; + } + + return fallbackIndexFor(parent).fallbackAtIndex[nodeIndex] === true; } diff --git a/plugins/postcss-text-decoration-shorthand/dist/index.mjs b/plugins/postcss-text-decoration-shorthand/dist/index.mjs index 13d474b6a9..9f5f0cc850 100644 --- a/plugins/postcss-text-decoration-shorthand/dist/index.mjs +++ b/plugins/postcss-text-decoration-shorthand/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-value-parser";import{namedColors as t}from"@csstools/color-helpers";const o=/^text-decoration$/i,creator=t=>{const c=Object.assign({preserve:!0},t);return{postcssPlugin:"postcss-text-decoration-shorthand",prepare(){const t=new Map;return{postcssPlugin:"postcss-text-decoration-shorthand",OnceExit(){t.clear()},Declaration(a){if(!o.test(a.prop))return;const i=a.parent;if(!i)return;const u=a.proxyOf??a,d=new Set;for(let e=0;e"space"!==e.type&&"comment"!==e.type);if(v.find(e=>"var"===e.value.toLowerCase()&&"function"===e.type))return;if(v.find(e=>"word"===e.type&&r.includes(e.value)))return;const y={line:[],style:null,color:null,thickness:null};for(let t=0;t{const c=Object.assign({preserve:!0},t);return{postcssPlugin:"postcss-text-decoration-shorthand",prepare(){const t=new Map;return{postcssPlugin:"postcss-text-decoration-shorthand",OnceExit(){t.clear()},Declaration(i){if(!o.test(i.prop))return;const a=i.parent;if(!a)return;const u=a.index(i),d=new Set;for(let e=0;e"space"!==e.type&&"comment"!==e.type);if(v.find(e=>"var"===e.value.toLowerCase()&&"function"===e.type))return;if(v.find(e=>"word"===e.type&&r.includes(e.value)))return;const y={line:[],style:null,color:null,thickness:null};for(let t=0;t = (opts?: pluginOptions) => { return; } - // Visitors receive a proxy node, while `parent.nodes` holds the - // underlying nodes. Unwrap to compare identity without an O(n) - // `parent.index()` lookup. - const selfNode = (decl as typeof decl & { proxyOf?: typeof decl }).proxyOf ?? decl; + const selfIndex = parent.index(decl); const siblingValues = new Set(); for (let i = 0; i < parent.nodes.length; i++) { - const node = parent.nodes[i]; - if (node === selfNode) { + if (i === selfIndex) { continue; } + const node = parent.nodes[i]; if (node.type === 'decl' && IS_TEXT_DECORATION_REGEX.test(node.prop)) { siblingValues.add(node.value); } From 54f72ff07b03e757a988d233b51dbe35e6b1ab8b Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Wed, 30 Sep 2026 22:02:50 +0200 Subject: [PATCH 3/6] cleanup --- packages/utilities/CHANGELOG.md | 4 -- packages/utilities/dist/index.mjs | 2 +- packages/utilities/src/has-fallback-decl.ts | 65 +++---------------- plugins/postcss-custom-media/dist/index.mjs | 2 +- plugins/postcss-gap-properties/CHANGELOG.md | 4 -- plugins/postcss-gap-properties/dist/index.mjs | 2 +- plugins/postcss-gap-properties/src/index.ts | 39 +++-------- .../CHANGELOG.md | 4 -- .../dist/index.mjs | 2 +- .../src/lib/clone-declaration.ts | 55 ++-------------- plugins/postcss-logical-overflow/CHANGELOG.md | 4 -- .../postcss-logical-overflow/dist/index.mjs | 2 +- .../src/lib/transform-axes.ts | 57 +++------------- .../CHANGELOG.md | 4 -- .../dist/index.mjs | 2 +- .../src/lib/transform-axes.ts | 57 +++------------- plugins/postcss-logical-resize/CHANGELOG.md | 4 -- plugins/postcss-logical-resize/dist/index.mjs | 2 +- .../src/lib/clone-declaration.ts | 55 ++-------------- plugins/postcss-logical/CHANGELOG.md | 2 +- plugins/postcss-logical/dist/index.mjs | 2 +- .../src/lib/clone-declaration.ts | 53 ++------------- 22 files changed, 63 insertions(+), 360 deletions(-) diff --git a/packages/utilities/CHANGELOG.md b/packages/utilities/CHANGELOG.md index 22a4f4115b..93093474bc 100644 --- a/packages/utilities/CHANGELOG.md +++ b/packages/utilities/CHANGELOG.md @@ -1,9 +1,5 @@ # Changes to Utilities -### Unreleased (patch) - -- Improve performance of `hasFallback` so it is linear instead of quadratic in the number of declarations of a single container. - ### 3.0.0 _January 14, 2026_ diff --git a/packages/utilities/dist/index.mjs b/packages/utilities/dist/index.mjs index db147ba417..282f970891 100644 --- a/packages/utilities/dist/index.mjs +++ b/packages/utilities/dist/index.mjs @@ -1 +1 @@ -const t=new WeakMap;function fallbackIndexFor(e){const n=e.nodes||[],o=t.get(e);if(o&&o.nodeCount===n.length)return o;const r=[];for(let t=0;t, -}; - -const fallbackIndexCache = new WeakMap(); - -function fallbackIndexFor(parent: Container): FallbackIndex { - const nodes = parent.nodes || []; - - const cached = fallbackIndexCache.get(parent); - if (cached && cached.nodeCount === nodes.length) { - return cached; - } - - const fallbackAtIndex: Array = []; - for (let i = 0; i < nodes.length; i++) { - fallbackAtIndex.push(false); - } - const seenProps = new Set(); - - for (let i = 0; i < nodes.length; i++) { - const sibling = nodes[i]; - if (sibling.type !== 'decl') { - continue; - } - - const prop = sibling.prop.toLowerCase(); - if (seenProps.has(prop)) { - fallbackAtIndex[i] = true; - } else { - seenProps.add(prop); - } - } - - const index = { nodeCount: nodes.length, fallbackAtIndex }; - fallbackIndexCache.set(parent, index); - - return index; -} +import type { Declaration } from 'postcss'; /** * Check if a declaration has a fallback. @@ -63,10 +13,15 @@ export function hasFallback(node: Declaration): boolean { return false; } - const nodeIndex = parent.index(node); - if (nodeIndex < 0) { - return false; + const nodeProp = node.prop.toLowerCase(); + + const currentNodeIndex = parent.index(node); + for (let i = 0; i < currentNodeIndex; i++) { + const precedingSibling = parent.nodes[i]; + if (precedingSibling.type === 'decl' && precedingSibling.prop.toLowerCase() === nodeProp) { + return true; + } } - return fallbackIndexFor(parent).fallbackAtIndex[nodeIndex] === true; + return false; } diff --git a/plugins/postcss-custom-media/dist/index.mjs b/plugins/postcss-custom-media/dist/index.mjs index e68dac83b1..85d608450b 100644 --- a/plugins/postcss-custom-media/dist/index.mjs +++ b/plugins/postcss-custom-media/dist/index.mjs @@ -1 +1 @@ -import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{tokenizer as r,TokenType as n,NumberType as o,isTokenWhiteSpaceOrComment as a,isTokenIdent as s,cloneTokens as i,stringify as l}from"@csstools/css-tokenizer";import{parseFromTokens as c,parse as u,isMediaFeatureBoolean as p,isMediaFeature as d,newMediaFeaturePlain as m,isMediaQueryInvalid as f,isMediaQueryWithType as h,isMediaAnd as y,isMediaOr as g,isMediaNot as v,isMediaConditionList as w,isGeneralEnclosed as W}from"@csstools/media-query-list-parser";const M=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,o=new Map,a=[];let s=1;r.walkAtRules(r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent,t=0;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");e=e.parent}}let i;if(r.nodes)i=normalizeLayerName(r.params,s++);else{if(!r.params.trim())return;i=r.params}let l=e(i);if(l?.length){{let e=r.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=o.get(e);if(t){l=l.map(e=>t.concat(e));break}e=e.parent}}if(t(a,l),r.nodes){const e=l[0].concat(M);n.set(r,e),o.set(r,l[0])}}});for(const e of n.values())t(a,[e]);const i=new WeakMap;for(const[e,t]of n)i.set(e,a.findIndex(e=>t.equal(e)));return i}function normalizeLayerName(e,t){return e.trim()?e:`csstools-anon-layer--${t}`}const x=new Set(["scope","container","layer"]);function isProcessableCustomMediaRule(e){if("custom-media"!==e.name.toLowerCase())return!1;if(!e.params||!e.params.includes("--"))return!1;if(e.nodes&&e.nodes.length>0)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!x.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function removeCyclicReferences(e,t){const r=new Set,n=new Set(e.keys()),o=new Map;for(const e of n)o.set(e,[]);for(const e of t){const t=e[0],a=e[1];if(!n.has(t)||!n.has(a))continue;if(t===a){r.add(t);continue}const s=o.get(t);s&&s.push(a)}const a=new Map,s=new Map,i=new Set,l=[];let c=0;const getIndex=e=>a.get(e)??0,getLowLink=e=>s.get(e)??0;for(const e of n){if(a.has(e))continue;const t=[{node:e,childIndex:0}];for(a.set(e,c),s.set(e,c),c++,l.push(e),i.add(e);t.length;){const e=t[t.length-1],n=o.get(e.node)??[];if(e.childIndex1)for(const e of t)r.add(e)}}}for(const t of r)e.delete(t);return r}function atMediaParamsTokens(e){const t=r({css:e},{onParseError:()=>{throw new Error(`Unable to parse media query "${e}"`)}}),n=[];for(;!t.endOfFile();)n.push(t.nextToken());return n}const k=[[n.Ident,"max-color",0,0,{value:"max-color"}],[n.Colon,":",0,0,void 0],[n.Number,"2147477350",0,0,{value:2147477350,type:o.Integer}]],C=[[n.Ident,"color",0,0,{value:"color"}],[n.Colon,":",0,0,void 0],[n.Number,"2147477350",0,0,{value:2147477350,type:o.Integer}]];function replaceTrueAndFalseTokens(e){let t,r=[];for(let n=0;n{throw new Error(`Unable to parse media query "${l(...o)}"`)}}),p=c(i(o),{preserveInvalidMediaQueries:!0,onParseError:()=>{throw new Error(`Unable to parse media query "${l(...o)}"`)}}).map(e=>e.negateQuery());return{name:n,truthy:u,falsy:p,dependencies:Array.from(r).map(e=>[n,e])}}function getCustomMedia(e,t,r){const n=new Map,o=new Map,a=[],s=collectCascadeLayerOrder(e);e.walkAtRules(e=>{if(!isProcessableCustomMediaRule(e))return;const t=parseCustomMedia(e.params);if(!t)return;if(0===t.truthy.length)return;const i=(c=s,(l=e).parent&&"atrule"===l.parent.type&&"layer"===l.parent.name.toLowerCase()?c.has(l.parent)?c.get(l.parent)+1:0:1e7);var l,c;const u=o.get(t.name)??-1;if(i&&i>=u&&(o.set(t.name,i),n.set(t.name,{truthy:t.truthy,falsy:t.falsy}),a.push(...t.dependencies)),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}});const i=removeCyclicReferences(n,a);for(const r of i.values())e.warn(t,`@custom-media rules have cyclic dependencies for "${r}"`);return n}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}function transformAtMediaListTokens(e,t){const r=u(e,{preserveInvalidMediaQueries:!0,onParseError:()=>{throw new Error(`Unable to parse media query "${e}"`)}}),n=r.map(e=>e.toString());for(let e=0;en===e?r:{replaceWith:t})}const s=transformComplexMediaQuery(o,t);if(s&&0!==s.length&&s[0].replaceWith!==a)return n.flatMap((t,r)=>r===e?s:[{replaceWith:t}])}return[]}function transformSimpleMediaQuery(e,t){if(!mediaQueryIsSimple(e))return null;let r=null;return e.walk(e=>{const n=e.node;if(!p(n))return;const o=n.getName();if(!o.startsWith("--"))return;const a=t.get(o);return a?(r={replaceWith:a.truthy.map(e=>e.toString().trim()).join(",")},!1):void 0}),r}function transformComplexMediaQuery(e,t){let r=[];return e.walk(n=>{const o=n.node;if(!p(o))return;const a=n.parent;if(!d(a))return;const s=o.getName();if(!s.startsWith("--"))return;const i=t.get(s);if(i){if(1===i.truthy.length&&mediaQueryIsSimple(i.truthy[0])){let t=null;if(i.truthy[0].walk(e=>{if(d(e.node))return t=e.node,!1}),t&&t.feature)return a.feature=t.feature,r=[{replaceWith:e.toString()}],!1}const t=m(k[0][4].value,k[2]);a.feature=t.feature;const n=e.toString(),o=m(C[0][4].value,C[2]);a.feature=o.feature;const s=e.toString();return r=[{replaceWith:n,encapsulateWith:[i.truthy.map(e=>e.toString().trim()).join(",")]},{replaceWith:s,encapsulateWith:i.falsy.map(e=>e.map(e=>e.toString().trim()).join(",").toString().trim())}],!1}}),r}function mediaQueryIsSimple(e){if(f(e))return!1;if(h(e))return!1;let t=!0;return e.walk(e=>{if(y(e.node)||g(e.node)||v(e.node)||w(e.node)||W(e.node))return t=!1,!1}),t}const creator=e=>{const t=e?.preserve??!1;if("importFrom"in Object(e))throw new Error('[postcss-custom-media] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-media] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-media",prepare(){const e=new WeakSet;let r=new Map,n=1e4;const spendBudget=e=>{if(n-=e,n<0)throw new Error("Maximum custom media expansion size exceeded, reduce the complexity of your custom media")},checkLength=(...e)=>{for(const t of e)if(t>1e5)throw new Error("Maximum custom media expansion length exceeded, reduce the complexity of your custom media")};return{postcssPlugin:"postcss-custom-media",Once(e,{result:o}){n=1e4,r=getCustomMedia(e,o,{preserve:t})},AtRule(n,{result:o}){if(e.has(n))return;if("media"!==n.name.toLowerCase())return;if(!n.params)return;if(!n.params.includes("--"))return;let a;try{a=transformAtMediaListTokens(n.params,r)}catch(e){return void n.warn(o,`Failed to parse @custom-media params with error message: "${e instanceof Error?e.message:e}"`)}if(!a||0===a.length)return;if(1===a.length){if(n.params.trim()===a[0].replaceWith.trim())return;return spendBudget(1),checkLength(a[0].replaceWith.length),e.add(n),n.cloneBefore({params:a[0].replaceWith.trim()}),t?void 0:void n.remove()}if(!!!a.find(e=>!!e.encapsulateWith?.length))return spendBudget(a.length),checkLength(...a.map(e=>e.replaceWith.length)),e.add(n),n.cloneBefore({params:a.map(e=>e.replaceWith).join(",").trim()}),void(t||n.remove());a.forEach(t=>{if(!t.encapsulateWith?.length)return spendBudget(1),checkLength(t.replaceWith.length),void n.cloneBefore({params:t.replaceWith.trim()});spendBudget(t.encapsulateWith.length+1),checkLength(t.replaceWith.length,...t.encapsulateWith.map(e=>e.length));const r=n.clone({params:t.replaceWith});r.parent=void 0;let o=n.clone({params:t.encapsulateWith[0],nodes:[]});o.parent=void 0,o.append(r),t.encapsulateWith.slice(1).forEach(e=>{const t=n.clone({params:e,nodes:[]});t.parent=void 0,t.append(o),o=t}),e.add(n),n.before(o)}),t||n.remove()}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{tokenizer as r,TokenType as n,NumberType as o,isTokenWhiteSpaceOrComment as a,isTokenIdent as s,cloneTokens as i,stringify as l}from"@csstools/css-tokenizer";import{parseFromTokens as c,parse as u,isMediaFeatureBoolean as p,isMediaFeature as d,newMediaFeaturePlain as m,isMediaQueryInvalid as f,isMediaQueryWithType as h,isMediaAnd as y,isMediaOr as g,isMediaNot as w,isMediaConditionList as v,isGeneralEnclosed as M}from"@csstools/media-query-list-parser";const W=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,o=new Map,a=[];let s=1;r.walkAtRules(r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent,t=0;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");e=e.parent}}let i;if(r.nodes)i=normalizeLayerName(r.params,s++);else{if(!r.params.trim())return;i=r.params}let l=e(i);if(l?.length){{let e=r.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=o.get(e);if(t){l=l.map(e=>t.concat(e));break}e=e.parent}}if(t(a,l),r.nodes){const e=l[0].concat(W);n.set(r,e),o.set(r,l[0])}}});for(const e of n.values())t(a,[e]);const i=new Map;for(let e=0;e0)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!k.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function removeCyclicReferences(e,t){const r=new Set,n=new Set(e.keys()),o=new Map;for(const e of n)o.set(e,[]);for(const e of t){const t=e[0],a=e[1];if(!n.has(t)||!n.has(a))continue;if(t===a){r.add(t);continue}const s=o.get(t);s&&s.push(a)}const a=new Map,s=new Map,i=new Set,l=[];let c=0;const getIndex=e=>a.get(e)??0,getLowLink=e=>s.get(e)??0;for(const e of n){if(a.has(e))continue;const t=[{node:e,childIndex:0}];for(a.set(e,c),s.set(e,c),c++,l.push(e),i.add(e);t.length;){const e=t[t.length-1],n=o.get(e.node)??[];if(e.childIndex1)for(const e of t)r.add(e)}}}for(const t of r)e.delete(t);return r}function atMediaParamsTokens(e){const t=r({css:e},{onParseError:()=>{throw new Error(`Unable to parse media query "${e}"`)}}),n=[];for(;!t.endOfFile();)n.push(t.nextToken());return n}const x=[[n.Ident,"max-color",0,0,{value:"max-color"}],[n.Colon,":",0,0,void 0],[n.Number,"2147477350",0,0,{value:2147477350,type:o.Integer}]],C=[[n.Ident,"color",0,0,{value:"color"}],[n.Colon,":",0,0,void 0],[n.Number,"2147477350",0,0,{value:2147477350,type:o.Integer}]];function replaceTrueAndFalseTokens(e){let t,r=[];for(let n=0;n{throw new Error(`Unable to parse media query "${l(...o)}"`)}}),p=c(i(o),{preserveInvalidMediaQueries:!0,onParseError:()=>{throw new Error(`Unable to parse media query "${l(...o)}"`)}}).map(e=>e.negateQuery());return{name:n,truthy:u,falsy:p,dependencies:Array.from(r).map(e=>[n,e])}}function getCustomMedia(e,t,r){const n=new Map,o=new Map,a=[],s=collectCascadeLayerOrder(e);e.walkAtRules(e=>{if(!isProcessableCustomMediaRule(e))return;const t=parseCustomMedia(e.params);if(!t)return;if(0===t.truthy.length)return;const i=(c=s,(l=e).parent&&"atrule"===l.parent.type&&"layer"===l.parent.name.toLowerCase()?c.has(l.parent)?c.get(l.parent)+1:0:1e7);var l,c;const u=o.get(t.name)??-1;if(i&&i>=u&&(o.set(t.name,i),n.set(t.name,{truthy:t.truthy,falsy:t.falsy}),a.push(...t.dependencies)),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}});const i=removeCyclicReferences(n,a);for(const r of i.values())e.warn(t,`@custom-media rules have cyclic dependencies for "${r}"`);return n}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}function transformAtMediaListTokens(e,t){const r=u(e,{preserveInvalidMediaQueries:!0,onParseError:()=>{throw new Error(`Unable to parse media query "${e}"`)}}),n=r.map(e=>e.toString());for(let e=0;en===e?r:{replaceWith:t})}const s=transformComplexMediaQuery(o,t);if(s&&0!==s.length&&s[0].replaceWith!==a)return n.flatMap((t,r)=>r===e?s:[{replaceWith:t}])}return[]}function transformSimpleMediaQuery(e,t){if(!mediaQueryIsSimple(e))return null;let r=null;return e.walk(e=>{const n=e.node;if(!p(n))return;const o=n.getName();if(!o.startsWith("--"))return;const a=t.get(o);return a?(r={replaceWith:a.truthy.map(e=>e.toString().trim()).join(",")},!1):void 0}),r}function transformComplexMediaQuery(e,t){let r=[];return e.walk(n=>{const o=n.node;if(!p(o))return;const a=n.parent;if(!d(a))return;const s=o.getName();if(!s.startsWith("--"))return;const i=t.get(s);if(i){if(1===i.truthy.length&&mediaQueryIsSimple(i.truthy[0])){let t=null;if(i.truthy[0].walk(e=>{if(d(e.node))return t=e.node,!1}),t&&t.feature)return a.feature=t.feature,r=[{replaceWith:e.toString()}],!1}const t=m(x[0][4].value,x[2]);a.feature=t.feature;const n=e.toString(),o=m(C[0][4].value,C[2]);a.feature=o.feature;const s=e.toString();return r=[{replaceWith:n,encapsulateWith:[i.truthy.map(e=>e.toString().trim()).join(",")]},{replaceWith:s,encapsulateWith:i.falsy.map(e=>e.map(e=>e.toString().trim()).join(",").toString().trim())}],!1}}),r}function mediaQueryIsSimple(e){if(f(e))return!1;if(h(e))return!1;let t=!0;return e.walk(e=>{if(y(e.node)||g(e.node)||w(e.node)||v(e.node)||M(e.node))return t=!1,!1}),t}const creator=e=>{const t=e?.preserve??!1;if("importFrom"in Object(e))throw new Error('[postcss-custom-media] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-media] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-media",prepare(){const e=new WeakSet;let r=new Map,n=1e4;const spendBudget=e=>{if(n-=e,n<0)throw new Error("Maximum custom media expansion size exceeded, reduce the complexity of your custom media")},checkLength=(...e)=>{for(const t of e)if(t>1e5)throw new Error("Maximum custom media expansion length exceeded, reduce the complexity of your custom media")};return{postcssPlugin:"postcss-custom-media",Once(e,{result:o}){n=1e4,r=getCustomMedia(e,o,{preserve:t})},AtRule(n,{result:o}){if(e.has(n))return;if("media"!==n.name.toLowerCase())return;if(!n.params)return;if(!n.params.includes("--"))return;let a;try{a=transformAtMediaListTokens(n.params,r)}catch(e){return void n.warn(o,`Failed to parse @custom-media params with error message: "${e instanceof Error?e.message:e}"`)}if(!a||0===a.length)return;if(1===a.length){if(n.params.trim()===a[0].replaceWith.trim())return;return spendBudget(1),checkLength(a[0].replaceWith.length),e.add(n),n.cloneBefore({params:a[0].replaceWith.trim()}),t?void 0:void n.remove()}if(!!!a.find(e=>!!e.encapsulateWith?.length))return spendBudget(a.length),checkLength(...a.map(e=>e.replaceWith.length)),e.add(n),n.cloneBefore({params:a.map(e=>e.replaceWith).join(",").trim()}),void(t||n.remove());a.forEach(t=>{if(!t.encapsulateWith?.length)return spendBudget(1),checkLength(t.replaceWith.length),void n.cloneBefore({params:t.replaceWith.trim()});spendBudget(t.encapsulateWith.length+1),checkLength(t.replaceWith.length,...t.encapsulateWith.map(e=>e.length));const r=n.clone({params:t.replaceWith});r.parent=void 0;let o=n.clone({params:t.encapsulateWith[0],nodes:[]});o.parent=void 0,o.append(r),t.encapsulateWith.slice(1).forEach(e=>{const t=n.clone({params:e,nodes:[]});t.parent=void 0,t.append(o),o=t}),e.add(n),n.before(o)}),t||n.remove()}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-gap-properties/CHANGELOG.md b/plugins/postcss-gap-properties/CHANGELOG.md index 0da687421d..27da9993ca 100644 --- a/plugins/postcss-gap-properties/CHANGELOG.md +++ b/plugins/postcss-gap-properties/CHANGELOG.md @@ -1,9 +1,5 @@ # Changes to PostCSS Gap Properties -### Unreleased (patch) - -- Improved performance by caching the `display: grid` lookup per rule - ### 7.0.0 _January 14, 2026_ diff --git a/plugins/postcss-gap-properties/dist/index.mjs b/plugins/postcss-gap-properties/dist/index.mjs index 62ca406428..934ba61c55 100644 --- a/plugins/postcss-gap-properties/dist/index.mjs +++ b/plugins/postcss-gap-properties/dist/index.mjs @@ -1 +1 @@ -const e=["column-gap","gap","row-gap"],r=new WeakMap;function parentHasDisplayGrid(e){const s=e.nodes||[],t=r.get(e);if(t&&t.length===s.length)return t.hasDisplayGrid;const o=s.some(e=>"decl"===e.type&&("display"===e.prop.toLowerCase()&&"grid"===e.value.toLowerCase()));return r.set(e,{length:s.length,hasDisplayGrid:o}),o}const creator=r=>{const s=Object.assign({preserve:!0},r);return{postcssPlugin:"postcss-gap-properties",Declaration(r){if(!e.includes(r.prop.toLowerCase()))return;if(!r.parent||!parentHasDisplayGrid(r.parent))return;const t=`grid-${r.prop.toLowerCase()}`,o=r.parent?.some(e=>"decl"===e.type&&e.prop.toLowerCase()===t);o||(r.cloneBefore({prop:t}),s.preserve||r.remove())}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +const e=["column-gap","gap","row-gap"],creator=o=>{const r=Object.assign({preserve:!0},o);return{postcssPlugin:"postcss-gap-properties",Declaration(o){if(!e.includes(o.prop.toLowerCase()))return;if(!o.parent?.some(e=>"decl"===e.type&&("display"===e.prop.toLowerCase()&&"grid"===e.value.toLowerCase())))return;const s=`grid-${o.prop.toLowerCase()}`,p=o.parent?.some(e=>"decl"===e.type&&e.prop.toLowerCase()===s);p||(o.cloneBefore({prop:s}),r.preserve||o.remove())}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-gap-properties/src/index.ts b/plugins/postcss-gap-properties/src/index.ts index 857a927949..2460c9cd4a 100644 --- a/plugins/postcss-gap-properties/src/index.ts +++ b/plugins/postcss-gap-properties/src/index.ts @@ -1,4 +1,4 @@ -import type { Container, PluginCreator } from 'postcss'; +import type { PluginCreator } from 'postcss'; /** postcss-gap-properties plugin options */ export type pluginOptions = { @@ -12,34 +12,6 @@ const gapProperties = [ 'row-gap', ]; -type DisplayGridCacheEntry = { - length: number, - hasDisplayGrid: boolean, -}; - -const displayGridCache = new WeakMap(); - -function parentHasDisplayGrid(parent: Container): boolean { - const nodes = parent.nodes || []; - const entry = displayGridCache.get(parent); - - if (entry && entry.length === nodes.length) { - return entry.hasDisplayGrid; - } - - const hasDisplayGrid = nodes.some((node) => { - if (node.type !== 'decl') { - return false; - } - - return node.prop.toLowerCase() === 'display' && node.value.toLowerCase() === 'grid'; - }); - - displayGridCache.set(parent, { length: nodes.length, hasDisplayGrid }); - - return hasDisplayGrid; -} - const creator: PluginCreator = (opts?: pluginOptions) => { const options = Object.assign( // Default options @@ -58,7 +30,14 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - if (!decl.parent || !parentHasDisplayGrid(decl.parent)) { + const isNotDisplayGrid = !(decl.parent?.some((node) => { + if (node.type !== 'decl') { + return false; + } + + return node.prop.toLowerCase() === 'display' && node.value.toLowerCase() === 'grid'; + })); + if (isNotDisplayGrid) { return; } diff --git a/plugins/postcss-logical-float-and-clear/CHANGELOG.md b/plugins/postcss-logical-float-and-clear/CHANGELOG.md index 694d49a8d5..f845f1cc14 100644 --- a/plugins/postcss-logical-float-and-clear/CHANGELOG.md +++ b/plugins/postcss-logical-float-and-clear/CHANGELOG.md @@ -1,9 +1,5 @@ # Changes to PostCSS Logical Float And Clear -### Unreleased (patch) - -- Improved performance of declaration deduplication by caching declarations per parent - ### 4.0.0 _January 14, 2026_ diff --git a/plugins/postcss-logical-float-and-clear/dist/index.mjs b/plugins/postcss-logical-float-and-clear/dist/index.mjs index 573b4a0e02..80eed07ede 100644 --- a/plugins/postcss-logical-float-and-clear/dist/index.mjs +++ b/plugins/postcss-logical-float-and-clear/dist/index.mjs @@ -1 +1 @@ -const t="inline-start",o="inline-end";var e,n;function directionFlowToAxes(t){switch(t){case e.TopToBottom:return[n.Top,n.Bottom];case e.BottomToTop:return[n.Bottom,n.Top];case e.RightToLeft:return[n.Right,n.Left];case e.LeftToRight:return[n.Left,n.Right]}}!function(t){t.TopToBottom="top-to-bottom",t.BottomToTop="bottom-to-top",t.RightToLeft="right-to-left",t.LeftToRight="left-to-right"}(e||(e={})),function(t){t.Top="top",t.Right="right",t.Bottom="bottom",t.Left="left"}(n||(n={}));const i=new WeakMap;function declarationKey(t,o){return`${t}\0${o}`}function getDeclarationKeys(t){const o=t.nodes||[];let e=i.get(t);if(!e||e.length!==o.length){e={length:o.length,keys:new Set};for(const t of o)"decl"===t.type&&e.keys.add(declarationKey(t.prop,t.value));i.set(t,e)}return e.keys}function cloneDeclaration(t,o,e){const n=t.parent;if(n){const t=getDeclarationKeys(n),i=declarationKey(e,o);if(t.has(i))return;t.add(i)}t.before(t.clone({value:o,prop:e}))}const creator=n=>{const i=Object.assign({inlineDirection:e.LeftToRight},n),c=Object.values(e);if(!c.includes(i.inlineDirection))throw new Error(`[postcss-logical-float-and-clear] "inlineDirection" must be one of ${c.join(", ")}`);if(![e.LeftToRight,e.RightToLeft].includes(i.inlineDirection))return{postcssPlugin:"postcss-logical-float-and-clear",Once(){}};const[r,s]=directionFlowToAxes(i.inlineDirection),makeTransform=e=>{const n=e.value.toLowerCase();if(![t,o].includes(n))return;cloneDeclaration(e,n===t?r:s,e.prop),e.remove()};return{postcssPlugin:"postcss-logical-float-and-clear",Declaration:{float:makeTransform,clear:makeTransform}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +const t="inline-start",o="inline-end";var e,n;function directionFlowToAxes(t){switch(t){case e.TopToBottom:return[n.Top,n.Bottom];case e.BottomToTop:return[n.Bottom,n.Top];case e.RightToLeft:return[n.Right,n.Left];case e.LeftToRight:return[n.Left,n.Right]}}function cloneDeclaration(t,o,e){t.parent&&t.parent.some(t=>"decl"===t.type&&t.prop===e&&t.value===o)||t.cloneBefore({value:o,prop:e})}!function(t){t.TopToBottom="top-to-bottom",t.BottomToTop="bottom-to-top",t.RightToLeft="right-to-left",t.LeftToRight="left-to-right"}(e||(e={})),function(t){t.Top="top",t.Right="right",t.Bottom="bottom",t.Left="left"}(n||(n={}));const creator=n=>{const i=Object.assign({inlineDirection:e.LeftToRight},n),c=Object.values(e);if(!c.includes(i.inlineDirection))throw new Error(`[postcss-logical-float-and-clear] "inlineDirection" must be one of ${c.join(", ")}`);if(![e.LeftToRight,e.RightToLeft].includes(i.inlineDirection))return{postcssPlugin:"postcss-logical-float-and-clear",Once(){}};const[r,l]=directionFlowToAxes(i.inlineDirection),makeTransform=e=>{const n=e.value.toLowerCase();if(![t,o].includes(n))return;cloneDeclaration(e,n===t?r:l,e.prop),e.remove()};return{postcssPlugin:"postcss-logical-float-and-clear",Declaration:{float:makeTransform,clear:makeTransform}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-logical-float-and-clear/src/lib/clone-declaration.ts b/plugins/postcss-logical-float-and-clear/src/lib/clone-declaration.ts index 6b5295d1af..4fe79ba551 100644 --- a/plugins/postcss-logical-float-and-clear/src/lib/clone-declaration.ts +++ b/plugins/postcss-logical-float-and-clear/src/lib/clone-declaration.ts @@ -1,58 +1,15 @@ -import type { Container, Declaration } from 'postcss'; - -type DeclarationCacheEntry = { - length: number, - keys: Set, -}; - -const declarationCache = new WeakMap(); - -function declarationKey(prop: string, value: string): string { - return `${prop}\0${value}`; -} - -function getDeclarationKeys(parent: Container): Set { - const nodes = parent.nodes || []; - let entry = declarationCache.get(parent); - - if (!entry || entry.length !== nodes.length) { - entry = { - length: nodes.length, - keys: new Set(), - }; - - for (const node of nodes) { - if (node.type === 'decl') { - entry.keys.add(declarationKey(node.prop, node.value)); - } - } - - declarationCache.set(parent, entry); - } - - return entry.keys; -} +import type { Declaration } from 'postcss'; export function cloneDeclaration( declaration: Declaration, value: string, prop: string, ): void { - const parent = declaration.parent; - - if (parent) { - const keys = getDeclarationKeys(parent); - const key = declarationKey(prop, value); - - if (keys.has(key)) { - return; - } - - // The clone is inserted into the same parent below. - // Record it now so later declarations in this parent still dedupe - // against it when the parent length happens to stay the same. - keys.add(key); + if (declaration.parent && declaration.parent.some((childNode) => { + return childNode.type === 'decl' && childNode.prop === prop && childNode.value === value; + })) { + return; } - declaration.before(declaration.clone({ value, prop })); + declaration.cloneBefore({ value, prop }); } diff --git a/plugins/postcss-logical-overflow/CHANGELOG.md b/plugins/postcss-logical-overflow/CHANGELOG.md index 60506a1e86..eb633e57f4 100644 --- a/plugins/postcss-logical-overflow/CHANGELOG.md +++ b/plugins/postcss-logical-overflow/CHANGELOG.md @@ -1,9 +1,5 @@ # Changes to PostCSS Logical Overflow -### Unreleased (patch) - -- Improved performance of declaration deduplication by caching declarations per parent - ### 3.0.0 _January 14, 2026_ diff --git a/plugins/postcss-logical-overflow/dist/index.mjs b/plugins/postcss-logical-overflow/dist/index.mjs index 012d4b18c9..b6da215282 100644 --- a/plugins/postcss-logical-overflow/dist/index.mjs +++ b/plugins/postcss-logical-overflow/dist/index.mjs @@ -1 +1 @@ -var t;!function(t){t.TopToBottom="top-to-bottom",t.BottomToTop="bottom-to-top",t.RightToLeft="right-to-left",t.LeftToRight="left-to-right"}(t||(t={}));const e=new WeakMap;function declarationKey(t,e){return`${t}\0${e}`}function getDeclarationKeys(t){const o=t.nodes||[];let n=e.get(t);if(!n||n.length!==o.length){n={length:o.length,keys:new Set};for(const t of o)"decl"===t.type&&n.keys.add(declarationKey(t.prop,t.value));e.set(t,n)}return n.keys}function transformAxes(t,e){const o=e?"-x":"-y",n=e?"-y":"-x",i=t.prop.toLowerCase().replace("-inline",o).replace("-block",n),s=t.value,r=t.parent;if(r){const t=getDeclarationKeys(r),e=declarationKey(i,s);if(t.has(e))return;t.add(e)}t.before(t.clone({prop:i,value:s})),t.remove()}const creator=e=>{const o=Object.assign({inlineDirection:t.LeftToRight},e);switch(o.inlineDirection){case t.LeftToRight:case t.RightToLeft:case t.TopToBottom:case t.BottomToTop:break;default:throw new Error(`[postcss-logical-viewport-units] "inlineDirection" must be one of ${Object.values(t).join(", ")}`)}const n=[t.LeftToRight,t.RightToLeft].includes(o.inlineDirection);return{postcssPlugin:"postcss-logical-overflow",Declaration:{"overflow-block":t=>transformAxes(t,n),"overflow-inline":t=>transformAxes(t,n)}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +var o;function transformAxes(o,t){const e=t?"-x":"-y",i=t?"-y":"-x",n=o.prop.toLowerCase().replace("-inline",e).replace("-block",i),s=o.value;o.parent?.some(o=>"decl"===o.type&&o.prop===n&&o.value===s)||(o.cloneBefore({prop:n,value:s}),o.remove())}!function(o){o.TopToBottom="top-to-bottom",o.BottomToTop="bottom-to-top",o.RightToLeft="right-to-left",o.LeftToRight="left-to-right"}(o||(o={}));const creator=t=>{const e=Object.assign({inlineDirection:o.LeftToRight},t);switch(e.inlineDirection){case o.LeftToRight:case o.RightToLeft:case o.TopToBottom:case o.BottomToTop:break;default:throw new Error(`[postcss-logical-viewport-units] "inlineDirection" must be one of ${Object.values(o).join(", ")}`)}const i=[o.LeftToRight,o.RightToLeft].includes(e.inlineDirection);return{postcssPlugin:"postcss-logical-overflow",Declaration:{"overflow-block":o=>transformAxes(o,i),"overflow-inline":o=>transformAxes(o,i)}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-logical-overflow/src/lib/transform-axes.ts b/plugins/postcss-logical-overflow/src/lib/transform-axes.ts index af27985851..9017c900c6 100644 --- a/plugins/postcss-logical-overflow/src/lib/transform-axes.ts +++ b/plugins/postcss-logical-overflow/src/lib/transform-axes.ts @@ -1,37 +1,4 @@ -import type { Container, Declaration } from 'postcss'; - -type DeclarationCacheEntry = { - length: number, - keys: Set, -}; - -const declarationCache = new WeakMap(); - -function declarationKey(prop: string, value: string): string { - return `${prop}\0${value}`; -} - -function getDeclarationKeys(parent: Container): Set { - const nodes = parent.nodes || []; - let entry = declarationCache.get(parent); - - if (!entry || entry.length !== nodes.length) { - entry = { - length: nodes.length, - keys: new Set(), - }; - - for (const node of nodes) { - if (node.type === 'decl') { - entry.keys.add(declarationKey(node.prop, node.value)); - } - } - - declarationCache.set(parent, entry); - } - - return entry.keys; -} +import type { Declaration } from 'postcss'; export function transformAxes(declaration: Declaration, isHorizontal: boolean): void { const inlineProp = isHorizontal ? '-x' : '-y'; @@ -42,21 +9,15 @@ export function transformAxes(declaration: Declaration, isHorizontal: boolean): const value = declaration.value; - const parent = declaration.parent; - if (parent) { - const keys = getDeclarationKeys(parent); - const key = declarationKey(prop, value); - - if (keys.has(key)) { - return; - } - - // The clone will be inserted into the same parent below. - // Record it now so later declarations in this parent still dedupe - // against it when the parent length happens to stay the same. - keys.add(key); + if (declaration.parent?.some((x) => { + return x.type === 'decl' && x.prop === prop && x.value === value; + })) { + return; } - declaration.before(declaration.clone({ prop, value })); + declaration.cloneBefore({ + prop: prop, + value: value, + }); declaration.remove(); } diff --git a/plugins/postcss-logical-overscroll-behavior/CHANGELOG.md b/plugins/postcss-logical-overscroll-behavior/CHANGELOG.md index 760d373197..bc4a33cf83 100644 --- a/plugins/postcss-logical-overscroll-behavior/CHANGELOG.md +++ b/plugins/postcss-logical-overscroll-behavior/CHANGELOG.md @@ -1,9 +1,5 @@ # Changes to PostCSS Logical Overscroll Behavior -### Unreleased (patch) - -- Improved performance of declaration deduplication by caching declarations per parent - ### 3.0.0 _January 14, 2026_ diff --git a/plugins/postcss-logical-overscroll-behavior/dist/index.mjs b/plugins/postcss-logical-overscroll-behavior/dist/index.mjs index de64caddc2..9a17c9323a 100644 --- a/plugins/postcss-logical-overscroll-behavior/dist/index.mjs +++ b/plugins/postcss-logical-overscroll-behavior/dist/index.mjs @@ -1 +1 @@ -var e;!function(e){e.TopToBottom="top-to-bottom",e.BottomToTop="bottom-to-top",e.RightToLeft="right-to-left",e.LeftToRight="left-to-right"}(e||(e={}));const o=new WeakMap;function declarationKey(e,o){return`${e}\0${o}`}function getDeclarationKeys(e){const t=e.nodes||[];let n=o.get(e);if(!n||n.length!==t.length){n={length:t.length,keys:new Set};for(const e of t)"decl"===e.type&&n.keys.add(declarationKey(e.prop,e.value));o.set(e,n)}return n.keys}function transformAxes(e,o){const t=o?"-x":"-y",n=o?"-y":"-x",i=e.prop.toLowerCase().replace("-inline",t).replace("-block",n),r=e.value,s=e.parent;if(s){const e=getDeclarationKeys(s),o=declarationKey(i,r);if(e.has(o))return;e.add(o)}e.before(e.clone({prop:i,value:r})),e.remove()}const creator=o=>{const t=Object.assign({inlineDirection:e.LeftToRight},o);switch(t.inlineDirection){case e.LeftToRight:case e.RightToLeft:case e.TopToBottom:case e.BottomToTop:break;default:throw new Error(`[postcss-logical-viewport-units] "inlineDirection" must be one of ${Object.values(e).join(", ")}`)}const n=[e.LeftToRight,e.RightToLeft].includes(t.inlineDirection);return{postcssPlugin:"postcss-logical-overscroll-behavior",Declaration:{"overscroll-behavior-block":e=>transformAxes(e,n),"overscroll-behavior-inline":e=>transformAxes(e,n)}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +var o;function transformAxes(o,t){const e=t?"-x":"-y",i=t?"-y":"-x",r=o.prop.toLowerCase().replace("-inline",e).replace("-block",i),s=o.value;o.parent?.some(o=>"decl"===o.type&&o.prop===r&&o.value===s)||(o.cloneBefore({prop:r,value:s}),o.remove())}!function(o){o.TopToBottom="top-to-bottom",o.BottomToTop="bottom-to-top",o.RightToLeft="right-to-left",o.LeftToRight="left-to-right"}(o||(o={}));const creator=t=>{const e=Object.assign({inlineDirection:o.LeftToRight},t);switch(e.inlineDirection){case o.LeftToRight:case o.RightToLeft:case o.TopToBottom:case o.BottomToTop:break;default:throw new Error(`[postcss-logical-viewport-units] "inlineDirection" must be one of ${Object.values(o).join(", ")}`)}const i=[o.LeftToRight,o.RightToLeft].includes(e.inlineDirection);return{postcssPlugin:"postcss-logical-overscroll-behavior",Declaration:{"overscroll-behavior-block":o=>transformAxes(o,i),"overscroll-behavior-inline":o=>transformAxes(o,i)}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-logical-overscroll-behavior/src/lib/transform-axes.ts b/plugins/postcss-logical-overscroll-behavior/src/lib/transform-axes.ts index af27985851..9017c900c6 100644 --- a/plugins/postcss-logical-overscroll-behavior/src/lib/transform-axes.ts +++ b/plugins/postcss-logical-overscroll-behavior/src/lib/transform-axes.ts @@ -1,37 +1,4 @@ -import type { Container, Declaration } from 'postcss'; - -type DeclarationCacheEntry = { - length: number, - keys: Set, -}; - -const declarationCache = new WeakMap(); - -function declarationKey(prop: string, value: string): string { - return `${prop}\0${value}`; -} - -function getDeclarationKeys(parent: Container): Set { - const nodes = parent.nodes || []; - let entry = declarationCache.get(parent); - - if (!entry || entry.length !== nodes.length) { - entry = { - length: nodes.length, - keys: new Set(), - }; - - for (const node of nodes) { - if (node.type === 'decl') { - entry.keys.add(declarationKey(node.prop, node.value)); - } - } - - declarationCache.set(parent, entry); - } - - return entry.keys; -} +import type { Declaration } from 'postcss'; export function transformAxes(declaration: Declaration, isHorizontal: boolean): void { const inlineProp = isHorizontal ? '-x' : '-y'; @@ -42,21 +9,15 @@ export function transformAxes(declaration: Declaration, isHorizontal: boolean): const value = declaration.value; - const parent = declaration.parent; - if (parent) { - const keys = getDeclarationKeys(parent); - const key = declarationKey(prop, value); - - if (keys.has(key)) { - return; - } - - // The clone will be inserted into the same parent below. - // Record it now so later declarations in this parent still dedupe - // against it when the parent length happens to stay the same. - keys.add(key); + if (declaration.parent?.some((x) => { + return x.type === 'decl' && x.prop === prop && x.value === value; + })) { + return; } - declaration.before(declaration.clone({ prop, value })); + declaration.cloneBefore({ + prop: prop, + value: value, + }); declaration.remove(); } diff --git a/plugins/postcss-logical-resize/CHANGELOG.md b/plugins/postcss-logical-resize/CHANGELOG.md index 270faff592..66cdd8bad3 100644 --- a/plugins/postcss-logical-resize/CHANGELOG.md +++ b/plugins/postcss-logical-resize/CHANGELOG.md @@ -1,9 +1,5 @@ # Changes to PostCSS Logical Resize -### Unreleased (patch) - -- Improved performance of declaration deduplication by caching declarations per parent - ### 4.0.0 _January 14, 2026_ diff --git a/plugins/postcss-logical-resize/dist/index.mjs b/plugins/postcss-logical-resize/dist/index.mjs index 04827dd242..c7945ce83c 100644 --- a/plugins/postcss-logical-resize/dist/index.mjs +++ b/plugins/postcss-logical-resize/dist/index.mjs @@ -1 +1 @@ -import o from"postcss-value-parser";var t,e,n;!function(o){o.Block="block",o.Inline="inline"}(t||(t={})),function(o){o.TopToBottom="top-to-bottom",o.BottomToTop="bottom-to-top",o.RightToLeft="right-to-left",o.LeftToRight="left-to-right"}(e||(e={})),function(o){o.Top="top",o.Right="right",o.Bottom="bottom",o.Left="left"}(n||(n={}));const i=new WeakMap;function declarationKey(o,t){return`${o}\0${t}`}function getDeclarationKeys(o){const t=o.nodes||[];let e=i.get(o);if(!e||e.length!==t.length){e={length:t.length,keys:new Set};for(const o of t)"decl"===o.type&&e.keys.add(declarationKey(o.prop,o.value));i.set(o,e)}return e.keys}function cloneDeclaration(o,t,e){const n=o.parent;if(n){const o=getDeclarationKeys(n),i=declarationKey(e,t);if(o.has(i))return;o.add(i)}o.before(o.clone({value:t,prop:e}))}function transformResize(t){return e=>{const{prop:n,value:i}=e,r=o(i),c=t.inlineIsHorizontal?"horizontal":"vertical",s=t.inlineIsHorizontal?"vertical":"horizontal";r.nodes.forEach(o=>{if("word"===o.type){const t=o.value.toLowerCase();if("inline"===t)return void(o.value=c);"block"===t&&(o.value=s)}});const l=r.toString();return l!==i&&(cloneDeclaration(e,l,n),!0)}}function directionFlowToAxes(o){switch(o){case e.TopToBottom:return[n.Top,n.Bottom];case e.BottomToTop:return[n.Bottom,n.Top];case e.RightToLeft:return[n.Right,n.Left];case e.LeftToRight:return[n.Left,n.Right]}}const creator=o=>{const t=Object.assign({blockDirection:e.TopToBottom,inlineDirection:e.LeftToRight},o),i=Object.values(e);if(!i.includes(t.blockDirection))throw new Error(`[postcss-logical-resize] "blockDirection" must be one of ${i.join(", ")}`);if(!i.includes(t.inlineDirection))throw new Error(`[postcss-logical-resize] "inlineDirection" must be one of ${i.join(", ")}`);const[r,c]=directionFlowToAxes(t.blockDirection),[s,l]=directionFlowToAxes(t.inlineDirection);if(!Object.values(n).every(o=>[r,c,s,l].includes(o)))throw new Error('[postcss-logical-resize] "blockDirection" and "inlineDirection" must be on separate axes');const a={block:[r,c],inline:[s,l],inlineIsHorizontal:[e.LeftToRight,e.RightToLeft].includes(t.inlineDirection)};return{postcssPlugin:"postcss-logical-resize",Declaration:{resize:(f=transformResize(a),(o,{result:t})=>{if(!f)return;let e;try{e=f(o)}catch(e){return void o.warn(t,e instanceof Error?e.message:String(e))}e&&o.remove()})}};var f};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import o from"postcss-value-parser";var t,e,i;function cloneDeclaration(o,t,e){o.parent&&o.parent.some(o=>"decl"===o.type&&o.prop===e&&o.value===t)||o.cloneBefore({value:t,prop:e})}function transformResize(t){return e=>{const{prop:i,value:n}=e,r=o(n),c=t.inlineIsHorizontal?"horizontal":"vertical",s=t.inlineIsHorizontal?"vertical":"horizontal";r.nodes.forEach(o=>{if("word"===o.type){const t=o.value.toLowerCase();if("inline"===t)return void(o.value=c);"block"===t&&(o.value=s)}});const l=r.toString();return l!==n&&(cloneDeclaration(e,l,i),!0)}}function directionFlowToAxes(o){switch(o){case e.TopToBottom:return[i.Top,i.Bottom];case e.BottomToTop:return[i.Bottom,i.Top];case e.RightToLeft:return[i.Right,i.Left];case e.LeftToRight:return[i.Left,i.Right]}}!function(o){o.Block="block",o.Inline="inline"}(t||(t={})),function(o){o.TopToBottom="top-to-bottom",o.BottomToTop="bottom-to-top",o.RightToLeft="right-to-left",o.LeftToRight="left-to-right"}(e||(e={})),function(o){o.Top="top",o.Right="right",o.Bottom="bottom",o.Left="left"}(i||(i={}));const creator=o=>{const t=Object.assign({blockDirection:e.TopToBottom,inlineDirection:e.LeftToRight},o),n=Object.values(e);if(!n.includes(t.blockDirection))throw new Error(`[postcss-logical-resize] "blockDirection" must be one of ${n.join(", ")}`);if(!n.includes(t.inlineDirection))throw new Error(`[postcss-logical-resize] "inlineDirection" must be one of ${n.join(", ")}`);const[r,c]=directionFlowToAxes(t.blockDirection),[s,l]=directionFlowToAxes(t.inlineDirection);if(!Object.values(i).every(o=>[r,c,s,l].includes(o)))throw new Error('[postcss-logical-resize] "blockDirection" and "inlineDirection" must be on separate axes');const a={block:[r,c],inline:[s,l],inlineIsHorizontal:[e.LeftToRight,e.RightToLeft].includes(t.inlineDirection)};return{postcssPlugin:"postcss-logical-resize",Declaration:{resize:(u=transformResize(a),(o,{result:t})=>{if(!u)return;let e;try{e=u(o)}catch(e){return void o.warn(t,e instanceof Error?e.message:String(e))}e&&o.remove()})}};var u};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-logical-resize/src/lib/clone-declaration.ts b/plugins/postcss-logical-resize/src/lib/clone-declaration.ts index 6b5295d1af..46f7f06652 100644 --- a/plugins/postcss-logical-resize/src/lib/clone-declaration.ts +++ b/plugins/postcss-logical-resize/src/lib/clone-declaration.ts @@ -1,58 +1,15 @@ -import type { Container, Declaration } from 'postcss'; - -type DeclarationCacheEntry = { - length: number, - keys: Set, -}; - -const declarationCache = new WeakMap(); - -function declarationKey(prop: string, value: string): string { - return `${prop}\0${value}`; -} - -function getDeclarationKeys(parent: Container): Set { - const nodes = parent.nodes || []; - let entry = declarationCache.get(parent); - - if (!entry || entry.length !== nodes.length) { - entry = { - length: nodes.length, - keys: new Set(), - }; - - for (const node of nodes) { - if (node.type === 'decl') { - entry.keys.add(declarationKey(node.prop, node.value)); - } - } - - declarationCache.set(parent, entry); - } - - return entry.keys; -} +import type { Declaration } from 'postcss'; export function cloneDeclaration( declaration: Declaration, value: string, prop: string, ): void { - const parent = declaration.parent; - - if (parent) { - const keys = getDeclarationKeys(parent); - const key = declarationKey(prop, value); - - if (keys.has(key)) { - return; - } - - // The clone is inserted into the same parent below. - // Record it now so later declarations in this parent still dedupe - // against it when the parent length happens to stay the same. - keys.add(key); + if (declaration.parent && declaration.parent.some((x) => { + return x.type === 'decl' && x.prop === prop && x.value === value; + })) { + return; } - declaration.before(declaration.clone({ value, prop })); + declaration.cloneBefore({ value, prop }); } diff --git a/plugins/postcss-logical/CHANGELOG.md b/plugins/postcss-logical/CHANGELOG.md index 3fe0f96363..a2d757ca6e 100644 --- a/plugins/postcss-logical/CHANGELOG.md +++ b/plugins/postcss-logical/CHANGELOG.md @@ -2,7 +2,7 @@ ### Unreleased (patch) -- Improved performance of declaration deduplication by caching declarations per parent +- Improved performance by inserting transformed declarations in a single batch ### 9.0.1 diff --git a/plugins/postcss-logical/dist/index.mjs b/plugins/postcss-logical/dist/index.mjs index fcbf835374..ef41da05d8 100644 --- a/plugins/postcss-logical/dist/index.mjs +++ b/plugins/postcss-logical/dist/index.mjs @@ -1 +1 @@ -import r from"postcss-value-parser";var e,n;!function(r){r.Block="block",r.Inline="inline"}(e||(e={})),function(r){r.Start="start",r.End="end"}(n||(n={}));const o={BlockStart:"block-start",BlockEnd:"block-end",InlineStart:"inline-start",InlineEnd:"inline-end"};var t,i;function directionFlowToAxes(r){switch(r){case t.TopToBottom:return[i.Top,i.Bottom];case t.BottomToTop:return[i.Bottom,i.Top];case t.RightToLeft:return[i.Right,i.Left];case t.LeftToRight:return[i.Left,i.Right]}}!function(r){r.TopToBottom="top-to-bottom",r.BottomToTop="bottom-to-top",r.RightToLeft="right-to-left",r.LeftToRight="left-to-right"}(t||(t={})),function(r){r.Top="top",r.Right="right",r.Bottom="bottom",r.Left="left"}(i||(i={}));const l=new WeakMap;function declarationKey(r,e){return`${r}\0${e}`}function getDeclarationKeys(r){const e=r.nodes||[];let n=l.get(r);if(!n||n.length!==e.length){n={length:e.length,keys:new Set};for(const r of e)"decl"===r.type&&n.keys.add(declarationKey(r.prop,r.value));l.set(r,n)}return n.keys}function cloneDeclaration(r,e,n){const o=r.parent;if(o){const r=getDeclarationKeys(o),t=declarationKey(n,e);if(r.has(t))return[];r.add(t)}return[r.clone({value:e,prop:n})]}function transformTransition(e,n,o){const{prop:t,value:i}=e,l=r(i),a=[];let s=[];l.nodes.forEach(r=>{if("div"===r.type&&","===r.value)return a.push(s),void(s=[]);s.push(r)}),a.push(s);for(let r=0;r1e4)throw new Error("Too many combinations when transforming a transition, reduce the complexity of your transition");for(let n=d.length-1;n>=0;n--){const o=JSON.parse(JSON.stringify(e));o[t].value=d[n].prop,a.splice(r+1,0,o)}a.splice(r,1),r++}}}const d=[];for(let r=0;r"space"!==r.type);if(n.length>2||0===n.length){const r=`[postcss-logical] Invalid number of values for ${e.prop}. Found ${n.length} values, expected 1 or 2.`;throw e.error(r)}let o,t;return 1===n.length?(o=r.stringify(n[0]),t=o):(o=r.stringify(n[0]),t=r.stringify(n[1])),[o,t]}function transformBorder(r,e){return n=>cloneDeclaration(n,n.value,`border-${e}-${r}`)}function transformBorderProperty(r,e){return n=>{const[o,t]=e,[i,l]=parseValueCouple(n);return[...cloneDeclaration(n,i,`border-${o}-${r}`),...cloneDeclaration(n,l,`border-${t}-${r}`)]}}function transformBorderShorthand(r){return e=>r.flatMap(r=>cloneDeclaration(e,e.value,`border-${r}`))}function transformBorderRadius(r){return e=>{let n;switch(e.prop.toLowerCase()){case"border-start-start-radius":n=`border-${r.inlineIsHorizontal?`${r.block[0]}-${r.inline[0]}`:`${r.inline[0]}-${r.block[0]}`}-radius`;break;case"border-start-end-radius":n=`border-${r.inlineIsHorizontal?`${r.block[0]}-${r.inline[1]}`:`${r.inline[1]}-${r.block[0]}`}-radius`;break;case"border-end-start-radius":n=`border-${r.inlineIsHorizontal?`${r.block[1]}-${r.inline[0]}`:`${r.inline[0]}-${r.block[1]}`}-radius`;break;case"border-end-end-radius":n=`border-${r.inlineIsHorizontal?`${r.block[1]}-${r.inline[1]}`:`${r.inline[1]}-${r.block[1]}`}-radius`;break}return n?cloneDeclaration(e,e.value,n):[]}}function transformLogicalSize(r){return e=>{const{value:n}=e,o=r.inlineIsHorizontal?"width":"height",t=r.inlineIsHorizontal?"height":"width",i=e.prop.toLowerCase().replace("inline-size",o).replace("block-size",t);return cloneDeclaration(e,n,i)}}function transformOffset(r){return e=>cloneDeclaration(e,e.value,r)}function transformOffsetShorthand(r){return e=>{const[n,o]=r,[t,i]=parseValueCouple(e);return[...cloneDeclaration(e,t,n),...cloneDeclaration(e,i,o)]}}function transformSide(r,e){return n=>cloneDeclaration(n,n.value,`${r}-${e}`)}function transformSideShorthand(r,e){return n=>{const[o,t]=e,[i,l]=parseValueCouple(n);return[...cloneDeclaration(n,i,`${r}-${o}`),...cloneDeclaration(n,l,`${r}-${t}`)]}}function logicalToPhysical(r,e){const[n,t]=e.block,[i,l]=e.inline;switch(r){case o.BlockStart:return n;case o.BlockEnd:return t;case o.InlineStart:return i;case o.InlineEnd:return l;default:throw new Error("Unsupported logical direction")}}function doTransform(e,n,o){const{prop:t,value:i}=e,l=r(i);l.nodes.forEach(r=>{if("word"===r.type){const e=r.value.toLowerCase();n.includes(e)&&(r.value=logicalToPhysical(e,o))}});const a=l.toString();return a!==i?cloneDeclaration(e,a,t):[]}function prepareTransforms(e,t,i,l,a){return{"caption-side":(d=e,r=>doTransform(r,Object.values(o),d)),"text-align":e.inlineIsHorizontal?(s=e.inline,e=>{const{prop:o,value:t}=e,i=r(t),[l,a]=s;i.nodes.forEach(r=>{if("word"===r.type){const e=r.value.toLowerCase();if(e===n.End)return void(r.value=a);e===n.Start&&(r.value=l)}});const d=i.toString();return d!==t?cloneDeclaration(e,d,o):[]}):null,"block-size":transformLogicalSize(e),"inline-size":transformLogicalSize(e),"min-block-size":transformLogicalSize(e),"max-block-size":transformLogicalSize(e),"min-inline-size":transformLogicalSize(e),"max-inline-size":transformLogicalSize(e),"margin-block-start":transformSide("margin",t),"margin-block-end":transformSide("margin",i),"margin-inline-start":transformSide("margin",l),"margin-inline-end":transformSide("margin",a),"margin-block":transformSideShorthand("margin",e.block),"margin-inline":transformSideShorthand("margin",e.inline),"inset-block":transformOffsetShorthand(e.block),"inset-block-start":transformOffset(t),"inset-block-end":transformOffset(i),"inset-inline":transformOffsetShorthand(e.inline),"inset-inline-start":transformOffset(l),"inset-inline-end":transformOffset(a),inset:e=>{const n=r(e.value).nodes.filter(r=>"space"!==r.type&&"comment"!==r.type);if(n.length>4){const r=`[postcss-logical] Invalid number of values for ${e.prop}. Found ${n.length} values, expected up to 4 values.`;throw e.error(r)}let o,t,i,l;if(1===n.length)o=r.stringify(n[0]),t=o,i=o,l=o;else if(2===n.length)o=r.stringify(n[0]),t=r.stringify(n[1]),i=o,l=t;else if(3===n.length)o=r.stringify(n[0]),t=r.stringify(n[1]),l=t,i=r.stringify(n[2]);else{if(4!==n.length)return[];o=r.stringify(n[0]),t=r.stringify(n[1]),i=r.stringify(n[2]),l=r.stringify(n[3])}return[...cloneDeclaration(e,o,"top"),...cloneDeclaration(e,t,"right"),...cloneDeclaration(e,i,"bottom"),...cloneDeclaration(e,l,"left")]},"padding-block-start":transformSide("padding",t),"padding-block-end":transformSide("padding",i),"padding-inline-start":transformSide("padding",l),"padding-inline-end":transformSide("padding",a),"padding-block":transformSideShorthand("padding",e.block),"padding-inline":transformSideShorthand("padding",e.inline),"border-block-start-width":transformBorder("width",t),"border-block-end-width":transformBorder("width",i),"border-inline-start-width":transformBorder("width",l),"border-inline-end-width":transformBorder("width",a),"border-block-width":transformBorderProperty("width",e.block),"border-inline-width":transformBorderProperty("width",e.inline),"border-block-start-style":transformBorder("style",t),"border-block-end-style":transformBorder("style",i),"border-inline-start-style":transformBorder("style",l),"border-inline-end-style":transformBorder("style",a),"border-block-style":transformBorderProperty("style",e.block),"border-inline-style":transformBorderProperty("style",e.inline),"border-block-start-color":transformBorder("color",t),"border-block-end-color":transformBorder("color",i),"border-inline-start-color":transformBorder("color",l),"border-inline-end-color":transformBorder("color",a),"border-block-color":transformBorderProperty("color",e.block),"border-inline-color":transformBorderProperty("color",e.inline),"border-block":transformBorderShorthand(e.block),"border-block-start":transformBorderShorthand([t]),"border-block-end":transformBorderShorthand([i]),"border-inline":transformBorderShorthand(e.inline),"border-inline-start":transformBorderShorthand([l]),"border-inline-end":transformBorderShorthand([a]),"border-start-start-radius":transformBorderRadius(e),"border-start-end-radius":transformBorderRadius(e),"border-end-start-radius":transformBorderRadius(e),"border-end-end-radius":transformBorderRadius(e)};var s,d}const a=/var\(/i,creator=r=>{const e=Object.assign({blockDirection:t.TopToBottom,inlineDirection:t.LeftToRight},r),n=Object.values(t);if(!n.includes(e.blockDirection))throw new Error(`[postcss-logical] "blockDirection" must be one of ${n.join(", ")}`);if(!n.includes(e.inlineDirection))throw new Error(`[postcss-logical] "inlineDirection" must be one of ${n.join(", ")}`);const[o,l]=directionFlowToAxes(e.blockDirection),[s,d]=directionFlowToAxes(e.inlineDirection);if(!Object.values(i).every(r=>[o,l,s,d].includes(r)))throw new Error('[postcss-logical] "blockDirection" and "inlineDirection" must be on separate axes');const makeTransform=r=>(n,{result:o})=>{if(!r)return;if(e.ignoreCustomProperties&&a.test(n.value))return;let t;try{t=r(n)}catch(r){return void n.warn(o,r instanceof Error?r.message:String(r))}t?.length&&(n.before(t),n.remove())},c=prepareTransforms({block:[o,l],inline:[s,d],inlineIsHorizontal:[t.LeftToRight,t.RightToLeft].includes(e.inlineDirection)},o,l,s,d);return{postcssPlugin:"postcss-logical",Declaration:{"caption-side":makeTransform(c["caption-side"]),"text-align":makeTransform(c["text-align"]),"block-size":makeTransform(c["block-size"]),"inline-size":makeTransform(c["inline-size"]),"min-block-size":makeTransform(c["min-block-size"]),"max-block-size":makeTransform(c["max-block-size"]),"min-inline-size":makeTransform(c["min-inline-size"]),"max-inline-size":makeTransform(c["max-inline-size"]),"margin-block-start":makeTransform(c["margin-block-start"]),"margin-block-end":makeTransform(c["margin-block-end"]),"margin-inline-start":makeTransform(c["margin-inline-start"]),"margin-inline-end":makeTransform(c["margin-inline-end"]),"margin-block":makeTransform(c["margin-block"]),"margin-inline":makeTransform(c["margin-inline"]),"inset-block":makeTransform(c["inset-block"]),"inset-block-start":makeTransform(c["inset-block-start"]),"inset-block-end":makeTransform(c["inset-block-end"]),"inset-inline":makeTransform(c["inset-inline"]),"inset-inline-start":makeTransform(c["inset-inline-start"]),"inset-inline-end":makeTransform(c["inset-inline-end"]),inset:makeTransform(c.inset),"padding-block-start":makeTransform(c["padding-block-start"]),"padding-block-end":makeTransform(c["padding-block-end"]),"padding-inline-start":makeTransform(c["padding-inline-start"]),"padding-inline-end":makeTransform(c["padding-inline-end"]),"padding-block":makeTransform(c["padding-block"]),"padding-inline":makeTransform(c["padding-inline"]),"border-block-start-width":makeTransform(c["border-block-start-width"]),"border-block-end-width":makeTransform(c["border-block-end-width"]),"border-inline-start-width":makeTransform(c["border-inline-start-width"]),"border-inline-end-width":makeTransform(c["border-inline-end-width"]),"border-block-width":makeTransform(c["border-block-width"]),"border-inline-width":makeTransform(c["border-inline-width"]),"border-block-start-style":makeTransform(c["border-block-start-style"]),"border-block-end-style":makeTransform(c["border-block-end-style"]),"border-inline-start-style":makeTransform(c["border-inline-start-style"]),"border-inline-end-style":makeTransform(c["border-inline-end-style"]),"border-block-style":makeTransform(c["border-block-style"]),"border-inline-style":makeTransform(c["border-inline-style"]),"border-block-start-color":makeTransform(c["border-block-start-color"]),"border-block-end-color":makeTransform(c["border-block-end-color"]),"border-inline-start-color":makeTransform(c["border-inline-start-color"]),"border-inline-end-color":makeTransform(c["border-inline-end-color"]),"border-block-color":makeTransform(c["border-block-color"]),"border-inline-color":makeTransform(c["border-inline-color"]),"border-block":makeTransform(c["border-block"]),"border-block-start":makeTransform(c["border-block-start"]),"border-block-end":makeTransform(c["border-block-end"]),"border-inline":makeTransform(c["border-inline"]),"border-inline-start":makeTransform(c["border-inline-start"]),"border-inline-end":makeTransform(c["border-inline-end"]),"border-start-start-radius":makeTransform(c["border-start-start-radius"]),"border-start-end-radius":makeTransform(c["border-start-end-radius"]),"border-end-start-radius":makeTransform(c["border-end-start-radius"]),"border-end-end-radius":makeTransform(c["border-end-end-radius"]),transition:(r,{result:n,postcss:o})=>{if(e.ignoreCustomProperties&&a.test(r.value))return;let t;try{t=transformTransition(r,o,c)}catch(e){return void r.warn(n,e instanceof Error?e.message:String(e))}t?.length&&(r.before(t),r.remove())}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import r from"postcss-value-parser";var e,n;!function(r){r.Block="block",r.Inline="inline"}(e||(e={})),function(r){r.Start="start",r.End="end"}(n||(n={}));const o={BlockStart:"block-start",BlockEnd:"block-end",InlineStart:"inline-start",InlineEnd:"inline-end"};var t,i;function directionFlowToAxes(r){switch(r){case t.TopToBottom:return[i.Top,i.Bottom];case t.BottomToTop:return[i.Bottom,i.Top];case t.RightToLeft:return[i.Right,i.Left];case t.LeftToRight:return[i.Left,i.Right]}}function cloneDeclaration(r,e,n){return r.parent&&r.parent.some(r=>"decl"===r.type&&r.prop===n&&r.value===e)?[]:[r.clone({value:e,prop:n})]}!function(r){r.TopToBottom="top-to-bottom",r.BottomToTop="bottom-to-top",r.RightToLeft="right-to-left",r.LeftToRight="left-to-right"}(t||(t={})),function(r){r.Top="top",r.Right="right",r.Bottom="bottom",r.Left="left"}(i||(i={}));function transformTransition(e,n,o){const{prop:t,value:i}=e,l=r(i),a=[];let s=[];l.nodes.forEach(r=>{if("div"===r.type&&","===r.value)return a.push(s),void(s=[]);s.push(r)}),a.push(s);for(let r=0;r1e4)throw new Error("Too many combinations when transforming a transition, reduce the complexity of your transition");for(let n=d.length-1;n>=0;n--){const o=JSON.parse(JSON.stringify(e));o[t].value=d[n].prop,a.splice(r+1,0,o)}a.splice(r,1),r++}}}const d=[];for(let r=0;r"space"!==r.type);if(n.length>2||0===n.length){const r=`[postcss-logical] Invalid number of values for ${e.prop}. Found ${n.length} values, expected 1 or 2.`;throw e.error(r)}let o,t;return 1===n.length?(o=r.stringify(n[0]),t=o):(o=r.stringify(n[0]),t=r.stringify(n[1])),[o,t]}function transformBorder(r,e){return n=>cloneDeclaration(n,n.value,`border-${e}-${r}`)}function transformBorderProperty(r,e){return n=>{const[o,t]=e,[i,l]=parseValueCouple(n);return[...cloneDeclaration(n,i,`border-${o}-${r}`),...cloneDeclaration(n,l,`border-${t}-${r}`)]}}function transformBorderShorthand(r){return e=>r.flatMap(r=>cloneDeclaration(e,e.value,`border-${r}`))}function transformBorderRadius(r){return e=>{let n;switch(e.prop.toLowerCase()){case"border-start-start-radius":n=`border-${r.inlineIsHorizontal?`${r.block[0]}-${r.inline[0]}`:`${r.inline[0]}-${r.block[0]}`}-radius`;break;case"border-start-end-radius":n=`border-${r.inlineIsHorizontal?`${r.block[0]}-${r.inline[1]}`:`${r.inline[1]}-${r.block[0]}`}-radius`;break;case"border-end-start-radius":n=`border-${r.inlineIsHorizontal?`${r.block[1]}-${r.inline[0]}`:`${r.inline[0]}-${r.block[1]}`}-radius`;break;case"border-end-end-radius":n=`border-${r.inlineIsHorizontal?`${r.block[1]}-${r.inline[1]}`:`${r.inline[1]}-${r.block[1]}`}-radius`;break}return n?cloneDeclaration(e,e.value,n):[]}}function transformLogicalSize(r){return e=>{const{value:n}=e,o=r.inlineIsHorizontal?"width":"height",t=r.inlineIsHorizontal?"height":"width",i=e.prop.toLowerCase().replace("inline-size",o).replace("block-size",t);return cloneDeclaration(e,n,i)}}function transformOffset(r){return e=>cloneDeclaration(e,e.value,r)}function transformOffsetShorthand(r){return e=>{const[n,o]=r,[t,i]=parseValueCouple(e);return[...cloneDeclaration(e,t,n),...cloneDeclaration(e,i,o)]}}function transformSide(r,e){return n=>cloneDeclaration(n,n.value,`${r}-${e}`)}function transformSideShorthand(r,e){return n=>{const[o,t]=e,[i,l]=parseValueCouple(n);return[...cloneDeclaration(n,i,`${r}-${o}`),...cloneDeclaration(n,l,`${r}-${t}`)]}}function logicalToPhysical(r,e){const[n,t]=e.block,[i,l]=e.inline;switch(r){case o.BlockStart:return n;case o.BlockEnd:return t;case o.InlineStart:return i;case o.InlineEnd:return l;default:throw new Error("Unsupported logical direction")}}function doTransform(e,n,o){const{prop:t,value:i}=e,l=r(i);l.nodes.forEach(r=>{if("word"===r.type){const e=r.value.toLowerCase();n.includes(e)&&(r.value=logicalToPhysical(e,o))}});const a=l.toString();return a!==i?cloneDeclaration(e,a,t):[]}function prepareTransforms(e,t,i,l,a){return{"caption-side":(d=e,r=>doTransform(r,Object.values(o),d)),"text-align":e.inlineIsHorizontal?(s=e.inline,e=>{const{prop:o,value:t}=e,i=r(t),[l,a]=s;i.nodes.forEach(r=>{if("word"===r.type){const e=r.value.toLowerCase();if(e===n.End)return void(r.value=a);e===n.Start&&(r.value=l)}});const d=i.toString();return d!==t?cloneDeclaration(e,d,o):[]}):null,"block-size":transformLogicalSize(e),"inline-size":transformLogicalSize(e),"min-block-size":transformLogicalSize(e),"max-block-size":transformLogicalSize(e),"min-inline-size":transformLogicalSize(e),"max-inline-size":transformLogicalSize(e),"margin-block-start":transformSide("margin",t),"margin-block-end":transformSide("margin",i),"margin-inline-start":transformSide("margin",l),"margin-inline-end":transformSide("margin",a),"margin-block":transformSideShorthand("margin",e.block),"margin-inline":transformSideShorthand("margin",e.inline),"inset-block":transformOffsetShorthand(e.block),"inset-block-start":transformOffset(t),"inset-block-end":transformOffset(i),"inset-inline":transformOffsetShorthand(e.inline),"inset-inline-start":transformOffset(l),"inset-inline-end":transformOffset(a),inset:e=>{const n=r(e.value).nodes.filter(r=>"space"!==r.type&&"comment"!==r.type);if(n.length>4){const r=`[postcss-logical] Invalid number of values for ${e.prop}. Found ${n.length} values, expected up to 4 values.`;throw e.error(r)}let o,t,i,l;if(1===n.length)o=r.stringify(n[0]),t=o,i=o,l=o;else if(2===n.length)o=r.stringify(n[0]),t=r.stringify(n[1]),i=o,l=t;else if(3===n.length)o=r.stringify(n[0]),t=r.stringify(n[1]),l=t,i=r.stringify(n[2]);else{if(4!==n.length)return[];o=r.stringify(n[0]),t=r.stringify(n[1]),i=r.stringify(n[2]),l=r.stringify(n[3])}return[...cloneDeclaration(e,o,"top"),...cloneDeclaration(e,t,"right"),...cloneDeclaration(e,i,"bottom"),...cloneDeclaration(e,l,"left")]},"padding-block-start":transformSide("padding",t),"padding-block-end":transformSide("padding",i),"padding-inline-start":transformSide("padding",l),"padding-inline-end":transformSide("padding",a),"padding-block":transformSideShorthand("padding",e.block),"padding-inline":transformSideShorthand("padding",e.inline),"border-block-start-width":transformBorder("width",t),"border-block-end-width":transformBorder("width",i),"border-inline-start-width":transformBorder("width",l),"border-inline-end-width":transformBorder("width",a),"border-block-width":transformBorderProperty("width",e.block),"border-inline-width":transformBorderProperty("width",e.inline),"border-block-start-style":transformBorder("style",t),"border-block-end-style":transformBorder("style",i),"border-inline-start-style":transformBorder("style",l),"border-inline-end-style":transformBorder("style",a),"border-block-style":transformBorderProperty("style",e.block),"border-inline-style":transformBorderProperty("style",e.inline),"border-block-start-color":transformBorder("color",t),"border-block-end-color":transformBorder("color",i),"border-inline-start-color":transformBorder("color",l),"border-inline-end-color":transformBorder("color",a),"border-block-color":transformBorderProperty("color",e.block),"border-inline-color":transformBorderProperty("color",e.inline),"border-block":transformBorderShorthand(e.block),"border-block-start":transformBorderShorthand([t]),"border-block-end":transformBorderShorthand([i]),"border-inline":transformBorderShorthand(e.inline),"border-inline-start":transformBorderShorthand([l]),"border-inline-end":transformBorderShorthand([a]),"border-start-start-radius":transformBorderRadius(e),"border-start-end-radius":transformBorderRadius(e),"border-end-start-radius":transformBorderRadius(e),"border-end-end-radius":transformBorderRadius(e)};var s,d}const l=/var\(/i,creator=r=>{const e=Object.assign({blockDirection:t.TopToBottom,inlineDirection:t.LeftToRight},r),n=Object.values(t);if(!n.includes(e.blockDirection))throw new Error(`[postcss-logical] "blockDirection" must be one of ${n.join(", ")}`);if(!n.includes(e.inlineDirection))throw new Error(`[postcss-logical] "inlineDirection" must be one of ${n.join(", ")}`);const[o,a]=directionFlowToAxes(e.blockDirection),[s,d]=directionFlowToAxes(e.inlineDirection);if(!Object.values(i).every(r=>[o,a,s,d].includes(r)))throw new Error('[postcss-logical] "blockDirection" and "inlineDirection" must be on separate axes');const makeTransform=r=>(n,{result:o})=>{if(!r)return;if(e.ignoreCustomProperties&&l.test(n.value))return;let t;try{t=r(n)}catch(r){return void n.warn(o,r instanceof Error?r.message:String(r))}t?.length&&(n.before(t),n.remove())},c=prepareTransforms({block:[o,a],inline:[s,d],inlineIsHorizontal:[t.LeftToRight,t.RightToLeft].includes(e.inlineDirection)},o,a,s,d);return{postcssPlugin:"postcss-logical",Declaration:{"caption-side":makeTransform(c["caption-side"]),"text-align":makeTransform(c["text-align"]),"block-size":makeTransform(c["block-size"]),"inline-size":makeTransform(c["inline-size"]),"min-block-size":makeTransform(c["min-block-size"]),"max-block-size":makeTransform(c["max-block-size"]),"min-inline-size":makeTransform(c["min-inline-size"]),"max-inline-size":makeTransform(c["max-inline-size"]),"margin-block-start":makeTransform(c["margin-block-start"]),"margin-block-end":makeTransform(c["margin-block-end"]),"margin-inline-start":makeTransform(c["margin-inline-start"]),"margin-inline-end":makeTransform(c["margin-inline-end"]),"margin-block":makeTransform(c["margin-block"]),"margin-inline":makeTransform(c["margin-inline"]),"inset-block":makeTransform(c["inset-block"]),"inset-block-start":makeTransform(c["inset-block-start"]),"inset-block-end":makeTransform(c["inset-block-end"]),"inset-inline":makeTransform(c["inset-inline"]),"inset-inline-start":makeTransform(c["inset-inline-start"]),"inset-inline-end":makeTransform(c["inset-inline-end"]),inset:makeTransform(c.inset),"padding-block-start":makeTransform(c["padding-block-start"]),"padding-block-end":makeTransform(c["padding-block-end"]),"padding-inline-start":makeTransform(c["padding-inline-start"]),"padding-inline-end":makeTransform(c["padding-inline-end"]),"padding-block":makeTransform(c["padding-block"]),"padding-inline":makeTransform(c["padding-inline"]),"border-block-start-width":makeTransform(c["border-block-start-width"]),"border-block-end-width":makeTransform(c["border-block-end-width"]),"border-inline-start-width":makeTransform(c["border-inline-start-width"]),"border-inline-end-width":makeTransform(c["border-inline-end-width"]),"border-block-width":makeTransform(c["border-block-width"]),"border-inline-width":makeTransform(c["border-inline-width"]),"border-block-start-style":makeTransform(c["border-block-start-style"]),"border-block-end-style":makeTransform(c["border-block-end-style"]),"border-inline-start-style":makeTransform(c["border-inline-start-style"]),"border-inline-end-style":makeTransform(c["border-inline-end-style"]),"border-block-style":makeTransform(c["border-block-style"]),"border-inline-style":makeTransform(c["border-inline-style"]),"border-block-start-color":makeTransform(c["border-block-start-color"]),"border-block-end-color":makeTransform(c["border-block-end-color"]),"border-inline-start-color":makeTransform(c["border-inline-start-color"]),"border-inline-end-color":makeTransform(c["border-inline-end-color"]),"border-block-color":makeTransform(c["border-block-color"]),"border-inline-color":makeTransform(c["border-inline-color"]),"border-block":makeTransform(c["border-block"]),"border-block-start":makeTransform(c["border-block-start"]),"border-block-end":makeTransform(c["border-block-end"]),"border-inline":makeTransform(c["border-inline"]),"border-inline-start":makeTransform(c["border-inline-start"]),"border-inline-end":makeTransform(c["border-inline-end"]),"border-start-start-radius":makeTransform(c["border-start-start-radius"]),"border-start-end-radius":makeTransform(c["border-start-end-radius"]),"border-end-start-radius":makeTransform(c["border-end-start-radius"]),"border-end-end-radius":makeTransform(c["border-end-end-radius"]),transition:(r,{result:n,postcss:o})=>{if(e.ignoreCustomProperties&&l.test(r.value))return;let t;try{t=transformTransition(r,o,c)}catch(e){return void r.warn(n,e instanceof Error?e.message:String(e))}t?.length&&(r.before(t),r.remove())}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-logical/src/lib/clone-declaration.ts b/plugins/postcss-logical/src/lib/clone-declaration.ts index f0e5a33baf..9a99427eb5 100644 --- a/plugins/postcss-logical/src/lib/clone-declaration.ts +++ b/plugins/postcss-logical/src/lib/clone-declaration.ts @@ -1,57 +1,14 @@ -import type { Container, Declaration } from 'postcss'; - -type DeclarationCacheEntry = { - length: number, - keys: Set, -}; - -const declarationCache = new WeakMap(); - -function declarationKey(prop: string, value: string): string { - return `${prop}\0${value}`; -} - -function getDeclarationKeys(parent: Container): Set { - const nodes = parent.nodes || []; - let entry = declarationCache.get(parent); - - if (!entry || entry.length !== nodes.length) { - entry = { - length: nodes.length, - keys: new Set(), - }; - - for (const node of nodes) { - if (node.type === 'decl') { - entry.keys.add(declarationKey(node.prop, node.value)); - } - } - - declarationCache.set(parent, entry); - } - - return entry.keys; -} +import type { Declaration } from 'postcss'; export function cloneDeclaration( declaration: Declaration, value: string, prop: string, ): Array { - const parent = declaration.parent; - - if (parent) { - const keys = getDeclarationKeys(parent); - const key = declarationKey(prop, value); - - if (keys.has(key)) { - return []; - } - - // The clone will be inserted into the same parent by the caller. - // Record it now so later declarations in this parent still dedupe - // against it when the parent length happens to stay the same. - keys.add(key); + if (declaration.parent && declaration.parent.some((x) => { + return x.type === 'decl' && x.prop === prop && x.value === value; + })) { + return []; } return [declaration.clone({ value, prop })]; From cd598c727d99c47270e4f36f913208192cf604d6 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Wed, 30 Sep 2026 22:34:57 +0200 Subject: [PATCH 4/6] cleanup --- .../cascade-layer-name-parser/dist/index.d.ts | 3 +- .../cascade-layer-name-parser/dist/index.mjs | 2 +- .../docs/cascade-layer-name-parser.api.json | 4 +- .../cascade-layer-name-parser.layername.md | 2 + ...scade-layer-name-parser.layername.parts.md | 2 +- .../src/nodes/layer-name.ts | 51 +++++++++---------- .../src/index.ts | 2 +- .../dist/index.mjs | 2 +- .../src/index.ts | 17 +------ plugins/postcss-custom-media/CHANGELOG.md | 5 -- plugins/postcss-custom-media/dist/index.mjs | 2 +- .../src/cascade-layers.ts | 15 ++---- .../postcss-custom-properties/CHANGELOG.md | 2 +- .../postcss-custom-properties/dist/index.mjs | 2 +- .../src/cascade-layers.ts | 15 ++---- plugins/postcss-custom-selectors/CHANGELOG.md | 1 - .../postcss-custom-selectors/dist/index.mjs | 2 +- .../src/cascade-layers.ts | 15 ++---- plugins/postcss-design-tokens/CHANGELOG.md | 5 -- plugins/postcss-design-tokens/dist/index.mjs | 2 +- .../style-dictionary/v3/dereference.ts | 19 ------- .../dist/index.mjs | 2 +- .../src/index.ts | 15 ++---- .../CHANGELOG.md | 4 -- .../dist/index.mjs | 2 +- .../src/index.ts | 12 ++--- 26 files changed, 59 insertions(+), 146 deletions(-) diff --git a/packages/cascade-layer-name-parser/dist/index.d.ts b/packages/cascade-layer-name-parser/dist/index.d.ts index 9b4e85c6fc..2cfb712bf7 100644 --- a/packages/cascade-layer-name-parser/dist/index.d.ts +++ b/packages/cascade-layer-name-parser/dist/index.d.ts @@ -4,9 +4,10 @@ import { ParseError } from '@csstools/css-tokenizer'; export declare function addLayerToModel(layers: Array, currentLayerNames: Array): void; export declare class LayerName { - parts: Array; + #private; constructor(parts: Array); tokens(): Array; + get parts(): Array; slice(start: number, end: number): LayerName; concat(other: LayerName): LayerName; segments(): Array; diff --git a/packages/cascade-layer-name-parser/dist/index.mjs b/packages/cascade-layer-name-parser/dist/index.mjs index 2782302d35..cb211094a3 100644 --- a/packages/cascade-layer-name-parser/dist/index.mjs +++ b/packages/cascade-layer-name-parser/dist/index.mjs @@ -1 +1 @@ -import{isTokenIdent as e,TokenType as t,isTokenDelim as r,tokenize as n,ParseError as a,isTokenWhiteSpaceOrComment as s,isTokenWhitespace as o,isTokenComment as i}from"@csstools/css-tokenizer";import{parseCommaSeparatedListOfComponentValues as l,isTokenNode as c,isCommentNode as m,isWhitespaceNode as p}from"@csstools/css-parser-algorithms";class LayerName{parts;constructor(e){this.parts=e}tokens(){return[...this.parts]}slice(t,r){const n=[];for(let t=0;te(t)||r(t)),a,...n.parts.filter(t=>e(t)||r(t))])}segments(){return this.parts.filter(t=>e(t)).map(e=>e[4].value)}name(){return this.parts.filter(t=>e(t)||r(t)).map(e=>e[1]).join("")}equal(e){const t=this.segments(),r=e.segments();if(t.length!==r.length)return!1;for(let e=0;ee[1]).join("")}toJSON(){return{parts:this.parts,segments:this.segments(),name:this.name()}}}function addLayerToModel(e,t){const r=e.map(e=>e.segments());t.forEach(t=>{const n=t.segments();for(let a=0;a=i&&(o=t,i=n)}if(l)continue;if(e.length>=4096)throw new Error("Too many cascade layers, reduce the complexity of your stylesheet.");const c=t.slice(0,a+1);-1===o?(e.push(c),r.push(s)):(e.splice(o+1,0,c),r.splice(o+1,0,s))}})}function parseFromTokens(t,n){const u=l(t,{onParseError:n?.onParseError}),h=n?.onParseError??(()=>{}),f=["6.4.2. Layer Naming and Nesting","Layer name syntax"," = [ '.' ]*"],d=t[0][2],y=t[t.length-1][3],g=[];for(let t=0;te.tokens());let w=!1,v=!1,L=null;for(let t=0;te(t)||r(t)),n,...a.#e.filter(t=>e(t)||r(t))])}segments(){return this.#e.filter(t=>e(t)).map(e=>e[4].value)}name(){return this.#t||(this.#t=this.#e.filter(t=>e(t)||r(t)).map(e=>e[1]).join("")),this.#t}equal(e){return this.name()===e.name()}toString(){return this.#e.map(e=>e[1]).join("")}toJSON(){return{parts:this.#e,segments:this.segments(),name:this.name()}}}function addLayerToModel(e,t){const r=e.map(e=>e.segments());t.forEach(t=>{const a=t.segments();for(let n=0;n=i&&(o=t,i=a)}if(l)continue;if(e.length>=4096)throw new Error("Too many cascade layers, reduce the complexity of your stylesheet.");const c=t.slice(0,n+1);-1===o?(e.push(c),r.push(s)):(e.splice(o+1,0,c),r.splice(o+1,0,s))}})}function parseFromTokens(t,a){const u=l(t,{onParseError:a?.onParseError}),h=a?.onParseError??(()=>{}),d=["6.4.2. Layer Naming and Nesting","Layer name syntax"," = [ '.' ]*"],f=t[0][2],y=t[t.length-1][3],g=[];for(let t=0;te.tokens());let w=!1,v=!1,L=null;for(let t=0;t +`readonly` + diff --git a/packages/cascade-layer-name-parser/docs/cascade-layer-name-parser.layername.parts.md b/packages/cascade-layer-name-parser/docs/cascade-layer-name-parser.layername.parts.md index 63ca85a6b3..373cdfd58c 100644 --- a/packages/cascade-layer-name-parser/docs/cascade-layer-name-parser.layername.parts.md +++ b/packages/cascade-layer-name-parser/docs/cascade-layer-name-parser.layername.parts.md @@ -7,5 +7,5 @@ **Signature:** ```typescript -parts: Array; +get parts(): Array; ``` diff --git a/packages/cascade-layer-name-parser/src/nodes/layer-name.ts b/packages/cascade-layer-name-parser/src/nodes/layer-name.ts index 46719afdaf..f32d1403cb 100644 --- a/packages/cascade-layer-name-parser/src/nodes/layer-name.ts +++ b/packages/cascade-layer-name-parser/src/nodes/layer-name.ts @@ -4,28 +4,33 @@ import type { CSSToken, TokenIdent} from '@csstools/css-tokenizer'; import { TokenType } from '@csstools/css-tokenizer'; export class LayerName { - parts: Array; + #parts: Array; + #name: string = ''; constructor(parts: Array) { - this.parts = parts; + this.#parts = parts; } tokens(): Array { return [ - ...this.parts, + ...this.#parts, ]; } + get parts(): Array { + return this.#parts.slice(); + } + slice(start: number, end: number): LayerName { const indices = []; - for (let i = 0; i < this.parts.length; i++) { - if (isTokenIdent(this.parts[i])) { + for (let i = 0; i < this.#parts.length; i++) { + if (isTokenIdent(this.#parts[i])) { indices.push(i); } } const slice = indices.slice(start, end); - return new LayerName(this.parts.slice(slice[0], slice[slice.length-1]+1)); + return new LayerName(this.#parts.slice(slice[0], slice[slice.length-1]+1)); } concat(other: LayerName): LayerName { @@ -38,18 +43,18 @@ export class LayerName { ]; return new LayerName([ - ...this.parts.filter((x) => { + ...this.#parts.filter((x) => { return isTokenIdent(x) || isTokenDelim(x); }), dot, - ...other.parts.filter((x) => { + ...other.#parts.filter((x) => { return isTokenIdent(x) || isTokenDelim(x); }), ]); } segments(): Array { - return this.parts.filter((x): x is TokenIdent => { + return this.#parts.filter((x): x is TokenIdent => { return isTokenIdent(x); }).map((x) => { return x[4].value; @@ -57,33 +62,25 @@ export class LayerName { } name(): string { - return this.parts.filter((x): x is TokenIdent | TokenDelim => { + if (this.#name) { + return this.#name; + } + + this.#name = this.#parts.filter((x): x is TokenIdent | TokenDelim => { return isTokenIdent(x) || isTokenDelim(x); }).map((x) => { return x[1]; }).join(''); + + return this.#name; } equal(other: LayerName): boolean { - const a = this.segments(); - const b = other.segments(); - if (a.length !== b.length) { - return false; - } - - for (let i = 0; i < a.length; i++) { - const aa = a[i]; - const bb = b[i]; - if (aa !== bb) { - return false; - } - } - - return true; + return this.name() === other.name(); } toString(): string { - return this.parts.map((x) => x[1]).join(''); + return this.#parts.map((x) => x[1]).join(''); } /** @@ -93,7 +90,7 @@ export class LayerName { */ toJSON(): Record{ return { - parts: this.parts, + parts: this.#parts, segments: this.segments(), name: this.name(), }; diff --git a/plugins/postcss-attribute-case-insensitive/src/index.ts b/plugins/postcss-attribute-case-insensitive/src/index.ts index cbe75ba299..0f41b00f73 100644 --- a/plugins/postcss-attribute-case-insensitive/src/index.ts +++ b/plugins/postcss-attribute-case-insensitive/src/index.ts @@ -122,7 +122,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { let modifiedSelector: string; try { - modifiedSelector = selectorParser((selectors) => { + modifiedSelector = selectorParser((selectors) => { let newSelectors: Array = []; selectors.each(selector => { diff --git a/plugins/postcss-container-rule-prelude-list/dist/index.mjs b/plugins/postcss-container-rule-prelude-list/dist/index.mjs index fd54f68a7f..b1aae50c76 100644 --- a/plugins/postcss-container-rule-prelude-list/dist/index.mjs +++ b/plugins/postcss-container-rule-prelude-list/dist/index.mjs @@ -1 +1 @@ -import{parseCommaSeparatedListOfComponentValues as e,stringify as s}from"@csstools/css-parser-algorithms";import{tokenize as t}from"@csstools/css-tokenizer";const r=/^container$/i;function countAtRules(e){let s=1;return e.walkAtRules(()=>{s++}),s}const creator=o=>{const n=Object.assign({preserve:!1},o);let c=0;return{postcssPlugin:"postcss-container-rule-prelude-list",Once(){c=0},AtRule(o){if(!r.test(o.name))return;if(!o.params.includes(","))return;const i=e(t({css:o.params})).map(e=>s([e]));if(!(i.length<=1)){if(c+=i.length*countAtRules(o),c>1e4)throw new Error("Maximum @container expansion size exceeded, reduce the complexity of your stylesheet");i.forEach(e=>{o.cloneBefore({params:e.trim()})}),n.preserve||o.remove()}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parseCommaSeparatedListOfComponentValues as e,stringify as s}from"@csstools/css-parser-algorithms";import{tokenize as r}from"@csstools/css-tokenizer";const t=/^container$/i,creator=o=>{const n=Object.assign({preserve:!1},o);let c=0;return{postcssPlugin:"postcss-container-rule-prelude-list",Once(){c=0},AtRule(o){if(!t.test(o.name))return;if(!o.params.includes(","))return;const i=e(r({css:o.params})).map(e=>s([e]));if(!(i.length<=1)){if(c+=i.length,c>1e4)throw new Error("Maximum @container expansion size exceeded, reduce the complexity of your stylesheet");i.forEach(e=>{o.cloneBefore({params:e.trim()})}),n.preserve||o.remove()}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-container-rule-prelude-list/src/index.ts b/plugins/postcss-container-rule-prelude-list/src/index.ts index 8f580abe68..b0b4644736 100644 --- a/plugins/postcss-container-rule-prelude-list/src/index.ts +++ b/plugins/postcss-container-rule-prelude-list/src/index.ts @@ -1,24 +1,11 @@ import { parseCommaSeparatedListOfComponentValues, stringify } from '@csstools/css-parser-algorithms'; import { tokenize } from '@csstools/css-tokenizer'; -import type { AtRule, PluginCreator } from 'postcss'; +import type { PluginCreator } from 'postcss'; const CONTAINER_NAME_REGEX = /^container$/i; -// A comma separated prelude is expanded by cloning the whole at-rule (including -// its children) once per list item. When such rules are nested, the clones are -// expanded again and the output grows exponentially with the nesting depth. -// Bound the total number of at-rules generated per stylesheet. const MAX_EXPANDED_AT_RULES = 10_000; -function countAtRules(node: AtRule): number { - let count = 1; - node.walkAtRules(() => { - count++; - }); - - return count; -} - /** postcss-container-rule-prelude-list plugin options */ export type pluginOptions = { /** Preserve the original notation. default: false */ @@ -56,7 +43,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - expandedAtRules += list.length * countAtRules(rule); + expandedAtRules += list.length; if (expandedAtRules > MAX_EXPANDED_AT_RULES) { throw new Error('Maximum @container expansion size exceeded, reduce the complexity of your stylesheet'); } diff --git a/plugins/postcss-custom-media/CHANGELOG.md b/plugins/postcss-custom-media/CHANGELOG.md index c5204907b4..f08c71d608 100644 --- a/plugins/postcss-custom-media/CHANGELOG.md +++ b/plugins/postcss-custom-media/CHANGELOG.md @@ -1,10 +1,5 @@ # Changes to PostCSS Custom Media -### Unreleased (patch) - -- Resolve cascade layer order in linear time instead of scanning the layer list for every node. - - ### 12.0.2 _September 25, 2026_ diff --git a/plugins/postcss-custom-media/dist/index.mjs b/plugins/postcss-custom-media/dist/index.mjs index 85d608450b..e68dac83b1 100644 --- a/plugins/postcss-custom-media/dist/index.mjs +++ b/plugins/postcss-custom-media/dist/index.mjs @@ -1 +1 @@ -import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{tokenizer as r,TokenType as n,NumberType as o,isTokenWhiteSpaceOrComment as a,isTokenIdent as s,cloneTokens as i,stringify as l}from"@csstools/css-tokenizer";import{parseFromTokens as c,parse as u,isMediaFeatureBoolean as p,isMediaFeature as d,newMediaFeaturePlain as m,isMediaQueryInvalid as f,isMediaQueryWithType as h,isMediaAnd as y,isMediaOr as g,isMediaNot as w,isMediaConditionList as v,isGeneralEnclosed as M}from"@csstools/media-query-list-parser";const W=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,o=new Map,a=[];let s=1;r.walkAtRules(r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent,t=0;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");e=e.parent}}let i;if(r.nodes)i=normalizeLayerName(r.params,s++);else{if(!r.params.trim())return;i=r.params}let l=e(i);if(l?.length){{let e=r.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=o.get(e);if(t){l=l.map(e=>t.concat(e));break}e=e.parent}}if(t(a,l),r.nodes){const e=l[0].concat(W);n.set(r,e),o.set(r,l[0])}}});for(const e of n.values())t(a,[e]);const i=new Map;for(let e=0;e0)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!k.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function removeCyclicReferences(e,t){const r=new Set,n=new Set(e.keys()),o=new Map;for(const e of n)o.set(e,[]);for(const e of t){const t=e[0],a=e[1];if(!n.has(t)||!n.has(a))continue;if(t===a){r.add(t);continue}const s=o.get(t);s&&s.push(a)}const a=new Map,s=new Map,i=new Set,l=[];let c=0;const getIndex=e=>a.get(e)??0,getLowLink=e=>s.get(e)??0;for(const e of n){if(a.has(e))continue;const t=[{node:e,childIndex:0}];for(a.set(e,c),s.set(e,c),c++,l.push(e),i.add(e);t.length;){const e=t[t.length-1],n=o.get(e.node)??[];if(e.childIndex1)for(const e of t)r.add(e)}}}for(const t of r)e.delete(t);return r}function atMediaParamsTokens(e){const t=r({css:e},{onParseError:()=>{throw new Error(`Unable to parse media query "${e}"`)}}),n=[];for(;!t.endOfFile();)n.push(t.nextToken());return n}const x=[[n.Ident,"max-color",0,0,{value:"max-color"}],[n.Colon,":",0,0,void 0],[n.Number,"2147477350",0,0,{value:2147477350,type:o.Integer}]],C=[[n.Ident,"color",0,0,{value:"color"}],[n.Colon,":",0,0,void 0],[n.Number,"2147477350",0,0,{value:2147477350,type:o.Integer}]];function replaceTrueAndFalseTokens(e){let t,r=[];for(let n=0;n{throw new Error(`Unable to parse media query "${l(...o)}"`)}}),p=c(i(o),{preserveInvalidMediaQueries:!0,onParseError:()=>{throw new Error(`Unable to parse media query "${l(...o)}"`)}}).map(e=>e.negateQuery());return{name:n,truthy:u,falsy:p,dependencies:Array.from(r).map(e=>[n,e])}}function getCustomMedia(e,t,r){const n=new Map,o=new Map,a=[],s=collectCascadeLayerOrder(e);e.walkAtRules(e=>{if(!isProcessableCustomMediaRule(e))return;const t=parseCustomMedia(e.params);if(!t)return;if(0===t.truthy.length)return;const i=(c=s,(l=e).parent&&"atrule"===l.parent.type&&"layer"===l.parent.name.toLowerCase()?c.has(l.parent)?c.get(l.parent)+1:0:1e7);var l,c;const u=o.get(t.name)??-1;if(i&&i>=u&&(o.set(t.name,i),n.set(t.name,{truthy:t.truthy,falsy:t.falsy}),a.push(...t.dependencies)),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}});const i=removeCyclicReferences(n,a);for(const r of i.values())e.warn(t,`@custom-media rules have cyclic dependencies for "${r}"`);return n}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}function transformAtMediaListTokens(e,t){const r=u(e,{preserveInvalidMediaQueries:!0,onParseError:()=>{throw new Error(`Unable to parse media query "${e}"`)}}),n=r.map(e=>e.toString());for(let e=0;en===e?r:{replaceWith:t})}const s=transformComplexMediaQuery(o,t);if(s&&0!==s.length&&s[0].replaceWith!==a)return n.flatMap((t,r)=>r===e?s:[{replaceWith:t}])}return[]}function transformSimpleMediaQuery(e,t){if(!mediaQueryIsSimple(e))return null;let r=null;return e.walk(e=>{const n=e.node;if(!p(n))return;const o=n.getName();if(!o.startsWith("--"))return;const a=t.get(o);return a?(r={replaceWith:a.truthy.map(e=>e.toString().trim()).join(",")},!1):void 0}),r}function transformComplexMediaQuery(e,t){let r=[];return e.walk(n=>{const o=n.node;if(!p(o))return;const a=n.parent;if(!d(a))return;const s=o.getName();if(!s.startsWith("--"))return;const i=t.get(s);if(i){if(1===i.truthy.length&&mediaQueryIsSimple(i.truthy[0])){let t=null;if(i.truthy[0].walk(e=>{if(d(e.node))return t=e.node,!1}),t&&t.feature)return a.feature=t.feature,r=[{replaceWith:e.toString()}],!1}const t=m(x[0][4].value,x[2]);a.feature=t.feature;const n=e.toString(),o=m(C[0][4].value,C[2]);a.feature=o.feature;const s=e.toString();return r=[{replaceWith:n,encapsulateWith:[i.truthy.map(e=>e.toString().trim()).join(",")]},{replaceWith:s,encapsulateWith:i.falsy.map(e=>e.map(e=>e.toString().trim()).join(",").toString().trim())}],!1}}),r}function mediaQueryIsSimple(e){if(f(e))return!1;if(h(e))return!1;let t=!0;return e.walk(e=>{if(y(e.node)||g(e.node)||w(e.node)||v(e.node)||M(e.node))return t=!1,!1}),t}const creator=e=>{const t=e?.preserve??!1;if("importFrom"in Object(e))throw new Error('[postcss-custom-media] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-media] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-media",prepare(){const e=new WeakSet;let r=new Map,n=1e4;const spendBudget=e=>{if(n-=e,n<0)throw new Error("Maximum custom media expansion size exceeded, reduce the complexity of your custom media")},checkLength=(...e)=>{for(const t of e)if(t>1e5)throw new Error("Maximum custom media expansion length exceeded, reduce the complexity of your custom media")};return{postcssPlugin:"postcss-custom-media",Once(e,{result:o}){n=1e4,r=getCustomMedia(e,o,{preserve:t})},AtRule(n,{result:o}){if(e.has(n))return;if("media"!==n.name.toLowerCase())return;if(!n.params)return;if(!n.params.includes("--"))return;let a;try{a=transformAtMediaListTokens(n.params,r)}catch(e){return void n.warn(o,`Failed to parse @custom-media params with error message: "${e instanceof Error?e.message:e}"`)}if(!a||0===a.length)return;if(1===a.length){if(n.params.trim()===a[0].replaceWith.trim())return;return spendBudget(1),checkLength(a[0].replaceWith.length),e.add(n),n.cloneBefore({params:a[0].replaceWith.trim()}),t?void 0:void n.remove()}if(!!!a.find(e=>!!e.encapsulateWith?.length))return spendBudget(a.length),checkLength(...a.map(e=>e.replaceWith.length)),e.add(n),n.cloneBefore({params:a.map(e=>e.replaceWith).join(",").trim()}),void(t||n.remove());a.forEach(t=>{if(!t.encapsulateWith?.length)return spendBudget(1),checkLength(t.replaceWith.length),void n.cloneBefore({params:t.replaceWith.trim()});spendBudget(t.encapsulateWith.length+1),checkLength(t.replaceWith.length,...t.encapsulateWith.map(e=>e.length));const r=n.clone({params:t.replaceWith});r.parent=void 0;let o=n.clone({params:t.encapsulateWith[0],nodes:[]});o.parent=void 0,o.append(r),t.encapsulateWith.slice(1).forEach(e=>{const t=n.clone({params:e,nodes:[]});t.parent=void 0,t.append(o),o=t}),e.add(n),n.before(o)}),t||n.remove()}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{tokenizer as r,TokenType as n,NumberType as o,isTokenWhiteSpaceOrComment as a,isTokenIdent as s,cloneTokens as i,stringify as l}from"@csstools/css-tokenizer";import{parseFromTokens as c,parse as u,isMediaFeatureBoolean as p,isMediaFeature as d,newMediaFeaturePlain as m,isMediaQueryInvalid as f,isMediaQueryWithType as h,isMediaAnd as y,isMediaOr as g,isMediaNot as v,isMediaConditionList as w,isGeneralEnclosed as W}from"@csstools/media-query-list-parser";const M=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,o=new Map,a=[];let s=1;r.walkAtRules(r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent,t=0;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");e=e.parent}}let i;if(r.nodes)i=normalizeLayerName(r.params,s++);else{if(!r.params.trim())return;i=r.params}let l=e(i);if(l?.length){{let e=r.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=o.get(e);if(t){l=l.map(e=>t.concat(e));break}e=e.parent}}if(t(a,l),r.nodes){const e=l[0].concat(M);n.set(r,e),o.set(r,l[0])}}});for(const e of n.values())t(a,[e]);const i=new WeakMap;for(const[e,t]of n)i.set(e,a.findIndex(e=>t.equal(e)));return i}function normalizeLayerName(e,t){return e.trim()?e:`csstools-anon-layer--${t}`}const x=new Set(["scope","container","layer"]);function isProcessableCustomMediaRule(e){if("custom-media"!==e.name.toLowerCase())return!1;if(!e.params||!e.params.includes("--"))return!1;if(e.nodes&&e.nodes.length>0)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!x.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function removeCyclicReferences(e,t){const r=new Set,n=new Set(e.keys()),o=new Map;for(const e of n)o.set(e,[]);for(const e of t){const t=e[0],a=e[1];if(!n.has(t)||!n.has(a))continue;if(t===a){r.add(t);continue}const s=o.get(t);s&&s.push(a)}const a=new Map,s=new Map,i=new Set,l=[];let c=0;const getIndex=e=>a.get(e)??0,getLowLink=e=>s.get(e)??0;for(const e of n){if(a.has(e))continue;const t=[{node:e,childIndex:0}];for(a.set(e,c),s.set(e,c),c++,l.push(e),i.add(e);t.length;){const e=t[t.length-1],n=o.get(e.node)??[];if(e.childIndex1)for(const e of t)r.add(e)}}}for(const t of r)e.delete(t);return r}function atMediaParamsTokens(e){const t=r({css:e},{onParseError:()=>{throw new Error(`Unable to parse media query "${e}"`)}}),n=[];for(;!t.endOfFile();)n.push(t.nextToken());return n}const k=[[n.Ident,"max-color",0,0,{value:"max-color"}],[n.Colon,":",0,0,void 0],[n.Number,"2147477350",0,0,{value:2147477350,type:o.Integer}]],C=[[n.Ident,"color",0,0,{value:"color"}],[n.Colon,":",0,0,void 0],[n.Number,"2147477350",0,0,{value:2147477350,type:o.Integer}]];function replaceTrueAndFalseTokens(e){let t,r=[];for(let n=0;n{throw new Error(`Unable to parse media query "${l(...o)}"`)}}),p=c(i(o),{preserveInvalidMediaQueries:!0,onParseError:()=>{throw new Error(`Unable to parse media query "${l(...o)}"`)}}).map(e=>e.negateQuery());return{name:n,truthy:u,falsy:p,dependencies:Array.from(r).map(e=>[n,e])}}function getCustomMedia(e,t,r){const n=new Map,o=new Map,a=[],s=collectCascadeLayerOrder(e);e.walkAtRules(e=>{if(!isProcessableCustomMediaRule(e))return;const t=parseCustomMedia(e.params);if(!t)return;if(0===t.truthy.length)return;const i=(c=s,(l=e).parent&&"atrule"===l.parent.type&&"layer"===l.parent.name.toLowerCase()?c.has(l.parent)?c.get(l.parent)+1:0:1e7);var l,c;const u=o.get(t.name)??-1;if(i&&i>=u&&(o.set(t.name,i),n.set(t.name,{truthy:t.truthy,falsy:t.falsy}),a.push(...t.dependencies)),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}});const i=removeCyclicReferences(n,a);for(const r of i.values())e.warn(t,`@custom-media rules have cyclic dependencies for "${r}"`);return n}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}function transformAtMediaListTokens(e,t){const r=u(e,{preserveInvalidMediaQueries:!0,onParseError:()=>{throw new Error(`Unable to parse media query "${e}"`)}}),n=r.map(e=>e.toString());for(let e=0;en===e?r:{replaceWith:t})}const s=transformComplexMediaQuery(o,t);if(s&&0!==s.length&&s[0].replaceWith!==a)return n.flatMap((t,r)=>r===e?s:[{replaceWith:t}])}return[]}function transformSimpleMediaQuery(e,t){if(!mediaQueryIsSimple(e))return null;let r=null;return e.walk(e=>{const n=e.node;if(!p(n))return;const o=n.getName();if(!o.startsWith("--"))return;const a=t.get(o);return a?(r={replaceWith:a.truthy.map(e=>e.toString().trim()).join(",")},!1):void 0}),r}function transformComplexMediaQuery(e,t){let r=[];return e.walk(n=>{const o=n.node;if(!p(o))return;const a=n.parent;if(!d(a))return;const s=o.getName();if(!s.startsWith("--"))return;const i=t.get(s);if(i){if(1===i.truthy.length&&mediaQueryIsSimple(i.truthy[0])){let t=null;if(i.truthy[0].walk(e=>{if(d(e.node))return t=e.node,!1}),t&&t.feature)return a.feature=t.feature,r=[{replaceWith:e.toString()}],!1}const t=m(k[0][4].value,k[2]);a.feature=t.feature;const n=e.toString(),o=m(C[0][4].value,C[2]);a.feature=o.feature;const s=e.toString();return r=[{replaceWith:n,encapsulateWith:[i.truthy.map(e=>e.toString().trim()).join(",")]},{replaceWith:s,encapsulateWith:i.falsy.map(e=>e.map(e=>e.toString().trim()).join(",").toString().trim())}],!1}}),r}function mediaQueryIsSimple(e){if(f(e))return!1;if(h(e))return!1;let t=!0;return e.walk(e=>{if(y(e.node)||g(e.node)||v(e.node)||w(e.node)||W(e.node))return t=!1,!1}),t}const creator=e=>{const t=e?.preserve??!1;if("importFrom"in Object(e))throw new Error('[postcss-custom-media] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-media] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-media",prepare(){const e=new WeakSet;let r=new Map,n=1e4;const spendBudget=e=>{if(n-=e,n<0)throw new Error("Maximum custom media expansion size exceeded, reduce the complexity of your custom media")},checkLength=(...e)=>{for(const t of e)if(t>1e5)throw new Error("Maximum custom media expansion length exceeded, reduce the complexity of your custom media")};return{postcssPlugin:"postcss-custom-media",Once(e,{result:o}){n=1e4,r=getCustomMedia(e,o,{preserve:t})},AtRule(n,{result:o}){if(e.has(n))return;if("media"!==n.name.toLowerCase())return;if(!n.params)return;if(!n.params.includes("--"))return;let a;try{a=transformAtMediaListTokens(n.params,r)}catch(e){return void n.warn(o,`Failed to parse @custom-media params with error message: "${e instanceof Error?e.message:e}"`)}if(!a||0===a.length)return;if(1===a.length){if(n.params.trim()===a[0].replaceWith.trim())return;return spendBudget(1),checkLength(a[0].replaceWith.length),e.add(n),n.cloneBefore({params:a[0].replaceWith.trim()}),t?void 0:void n.remove()}if(!!!a.find(e=>!!e.encapsulateWith?.length))return spendBudget(a.length),checkLength(...a.map(e=>e.replaceWith.length)),e.add(n),n.cloneBefore({params:a.map(e=>e.replaceWith).join(",").trim()}),void(t||n.remove());a.forEach(t=>{if(!t.encapsulateWith?.length)return spendBudget(1),checkLength(t.replaceWith.length),void n.cloneBefore({params:t.replaceWith.trim()});spendBudget(t.encapsulateWith.length+1),checkLength(t.replaceWith.length,...t.encapsulateWith.map(e=>e.length));const r=n.clone({params:t.replaceWith});r.parent=void 0;let o=n.clone({params:t.encapsulateWith[0],nodes:[]});o.parent=void 0,o.append(r),t.encapsulateWith.slice(1).forEach(e=>{const t=n.clone({params:e,nodes:[]});t.parent=void 0,t.append(o),o=t}),e.add(n),n.before(o)}),t||n.remove()}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-custom-media/src/cascade-layers.ts b/plugins/postcss-custom-media/src/cascade-layers.ts index 466ad1a344..6b993068a5 100644 --- a/plugins/postcss-custom-media/src/cascade-layers.ts +++ b/plugins/postcss-custom-media/src/cascade-layers.ts @@ -108,25 +108,16 @@ export function collectCascadeLayerOrder(root: Root): WeakMap { addLayerToModel(layers, [layerName]); } - // Index the layer names once so resolving each reference is O(1) - // instead of scanning the whole layer list for every node. - const layerIndexByName = new Map(); - for (let i = 0; i < layers.length; i++) { - layerIndexByName.set(layerKey(layers[i]), i); - } - const out: WeakMap = new WeakMap(); for (const [node, layerName] of references) { - out.set(node, layerIndexByName.get(layerKey(layerName)) ?? -1); + out.set(node, layers.findIndex((x) => { + return layerName.equal(x); + })); } return out; } -function layerKey(layerName: LayerName): string { - return layerName.segments().join('\u0000'); -} - // 0 : node was not found // any number : node was found, higher numbers have higher priority // a very large number : node wasn't layered, highest priority diff --git a/plugins/postcss-custom-properties/CHANGELOG.md b/plugins/postcss-custom-properties/CHANGELOG.md index 19a154184d..06b76bef1a 100644 --- a/plugins/postcss-custom-properties/CHANGELOG.md +++ b/plugins/postcss-custom-properties/CHANGELOG.md @@ -2,7 +2,7 @@ ### Unreleased (patch) -- Share a single expansion budget across all declarations, and resolve cascade layer order in linear time. +- Share a single expansion budget across all declarations. - Avoid a quadratic sibling scan when checking for an existing fallback declaration. diff --git a/plugins/postcss-custom-properties/dist/index.mjs b/plugins/postcss-custom-properties/dist/index.mjs index 3e4ee7408c..759b8fc230 100644 --- a/plugins/postcss-custom-properties/dist/index.mjs +++ b/plugins/postcss-custom-properties/dist/index.mjs @@ -1 +1 @@ -import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import n from"postcss-value-parser";import{hasSupportsAtRuleAncestor as r}from"@csstools/utilities";const o=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(n){const r=new Map,s=new Map,a=[];let i=1;n.walkAtRules(n=>{if("layer"!==n.name.toLowerCase())return;{let e=n.parent,t=0;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===n.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");e=e.parent}}let c;if(n.nodes)c=normalizeLayerName(n.params,i++);else{if(!n.params.trim())return;c=n.params}let l=e(c);if(l?.length){{let e=n.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);if(t){l=l.map(e=>t.concat(e));break}e=e.parent}}if(t(a,l),n.nodes){const e=l[0].concat(o);r.set(n,e),s.set(n,l[0])}}});for(const e of r.values())t(a,[e]);const c=new Map;for(let e=0;eisIgnoreComment(e,s));return a.set(e,t),t}const i=/(?:!\s*)?postcss-custom-properties:\s*ignore\s+next\b/i;function isDeclarationIgnored(e){return!!e&&(!!isBlockIgnored(e.parent)||isIgnoreComment(e.prev(),i))}function isIgnoreComment(e,t){return!!e&&"comment"===e.type&&t.test(e.text)}const c=new Set(["layer"]);function isProcessableRule(e){let t=e.parent;for(;t;){if("atrule"===t.type&&!c.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const l=/^html$/i,u=/^:where\(html\)$/i,p=/^:root$/i,f=/^:where\(:root\)$/i,d=/(html|:root)/i,m=/^var$/i;function isVarFunction(e){return"function"===e.type&&m.test(e.value)&&e.nodes?.length>0}const g=/\bvar\(/i;function parseOrCached(e,t){let r=t.get(e);return r||(r=n(e),t.set(e,r),r)}function toposort(e,t){let n=e.length;const r=new Array(n),o=new Set,s=makeOutgoingEdges(t),a=makeNodesHash(e);for(let t=e.length-1;t>=0;t--){if(o.has(t))continue;const i=e[t],c=new Set,l=[];for(o.add(t),c.add(i),l.push({node:i,outgoing:Array.from(s.get(i)||new Set),next:0});l.length;){const e=l[l.length-1];if(e.nexts.get(e)??0,getLowLink=e=>a.get(e)??0;for(const e of r){if(s.has(e))continue;const t=[{node:e,childIndex:0}];for(s.set(e,l),a.set(e,l),l++,c.push(e),i.add(e);t.length;){const e=t[t.length-1],r=o.get(e.node)??[];if(e.childIndex1)for(const e of t)n.add(e)}}}for(const t of n)e.delete(t);return n}function makeOutgoingEdges(e){const t=new Map;for(let n=0,r=e.length;n{if(r.remaining--,r.remaining<0)throw new Error("Maximum custom property expansion size exceeded, reduce the complexity of your custom properties");if(!isVarFunction(e))return;const a=parseVarFunction(e);if(!a)return;let i=!1;a.fallback&&n.walk(a.fallback,e=>{if(!isVarFunction(e))return;const n=parseVarFunction(e);return n?n.fallback||t.has(n.name.value)?void 0:(i=!0,!1):void 0});let c=t.get(a.name.value)?.nodes;c||!a.fallback||i||(c=a.fallback),void 0!==c&&(c.length?replaceNodeWithNodes(s,o,c):s.splice(o,1,{type:"div",value:" ",before:"",after:"",sourceIndex:e.sourceIndex,sourceEndIndex:e.sourceEndIndex}))}),n.stringify(e.nodes)):""}function walk(e,t){let n,r,o;for(n=0,r=e.length;n"comment"!==e.type&&"space"!==e.type);return 1===t.length&&("word"===t[0].type&&w.test(t[0].value))}function buildCustomPropertiesMap(e,t,r){if(!e.size)return t;const o=new Map(t);{const s=[];for(const[a,i]of e.entries()){const c=parseOrCached(i,r);let l=!1;n.walk(c.nodes,n=>{if(!isVarFunction(n))return;const r=parseVarFunction(n);r&&(r.fallback||e.has(r.name.value)||t.has(r.name.value)?s.push([r.name.value,a]):l=!0)}),l||o.set(a,c)}removeCyclicReferences(o,s)}{const e=[];for(const[t,r]of o.entries())n.walk(r.nodes,n=>{if(!isVarFunction(n))return;const r=parseVarFunction(n);r&&(r.fallback||o.has(r.name.value)?e.push([r.name.value,t]):o.delete(t))});for(let t=0;t{d.test(e.selector)&&e.nodes?.length&&isProcessableRule(e)&&(isBlockIgnored(e)||e.selectors.forEach(t=>{let s;if(u.test(t)||f.test(t))s=0;else if(l.test(t))s=1;else{if(!p.test(t))return;s=2}const a=(c=o,((i=e).parent&&"atrule"===i.parent.type&&"layer"===i.parent.name.toLowerCase()?c.has(i.parent)?c.get(i.parent)+1:0:1e7)+10+s);var i,c;e.each(e=>{if("decl"!==e.type)return;if(!e.variable||isDeclarationIgnored(e))return;if("initial"===e.value.toLowerCase().trim())return;const t=r.get(e.prop)??-1;a>=t&&(r.set(e.prop,a),n.set(e.prop,e.value))})}))}),buildCustomPropertiesMap(n,new Map,t)}function getCustomPropertiesFromSiblings(e,t,n){if(!e.parent)return t;const r=new Map;return e.parent.each(t=>{"decl"===t.type&&t.variable&&e!==t&&(isDeclarationIgnored(t)||r.set(t.prop,t.value))}),r.size?buildCustomPropertiesMap(r,t,n):t}function transformProperties(e,t,r){if(isTransformableDecl(e)&&!isDeclarationIgnored(e)){const o=e.raws?.value?.raw??e.value,s=transformValueAST(n(o),t,r.budget);if(s===o)return;if(parentHasExactFallback(e,s))return void(r.preserve||e.remove());const a=e.cloneBefore({value:s});a.raws?.value?.raw&&(a.raws.value.raw=""),r?.preserve||e.remove()}}const isTransformableDecl=e=>!e.variable&&e.value.includes("--")&&e.value.toLowerCase().includes("var(");function parentHasExactFallback(e,t){if(!e||!e.parent)return!1;const n=e.prop.toLowerCase();let r=e.prev();for(;r;){if("decl"===r.type&&r.prop.toLowerCase()===n&&r.value===t)return!0;r=r.prev()}return!1}const y=/\bvar\(|\(top: var\(--f\)/i,creator=e=>{const t=!("preserve"in Object(e))||Boolean(e?.preserve);if("importFrom"in Object(e))throw new Error('[postcss-custom-properties] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-properties] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-properties",prepare(){let e=new Map;const n=new WeakMap,o=new Map;let s={remaining:h};return{postcssPlugin:"postcss-custom-properties",Once(t){s={remaining:h},e=getCustomPropertiesFromRoot(t,o)},Declaration(a){if(!g.test(a.value))return;if(r(a,y))return;let i=e;t&&a.parent&&(i=n.get(a.parent)??getCustomPropertiesFromSiblings(a,e,o),n.set(a.parent,i)),transformProperties(a,i,{preserve:t,budget:s})}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import n from"postcss-value-parser";import{hasSupportsAtRuleAncestor as r}from"@csstools/utilities";const o=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(n){const r=new Map,s=new Map,a=[];let i=1;n.walkAtRules(n=>{if("layer"!==n.name.toLowerCase())return;{let e=n.parent,t=0;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===n.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");e=e.parent}}let c;if(n.nodes)c=normalizeLayerName(n.params,i++);else{if(!n.params.trim())return;c=n.params}let l=e(c);if(l?.length){{let e=n.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);if(t){l=l.map(e=>t.concat(e));break}e=e.parent}}if(t(a,l),n.nodes){const e=l[0].concat(o);r.set(n,e),s.set(n,l[0])}}});for(const e of r.values())t(a,[e]);const c=new WeakMap;for(const[e,t]of r)c.set(e,a.findIndex(e=>t.equal(e)));return c}function normalizeLayerName(e,t){return e.trim()?e:`csstools-anon-layer--${t}`}const s=/(?:!\s*)?postcss-custom-properties:\s*off\b/i,a=new WeakMap;function isBlockIgnored(e){if(!e||!e.nodes)return!1;if(a.has(e))return a.get(e);const t=e.some(e=>isIgnoreComment(e,s));return a.set(e,t),t}const i=/(?:!\s*)?postcss-custom-properties:\s*ignore\s+next\b/i;function isDeclarationIgnored(e){return!!e&&(!!isBlockIgnored(e.parent)||isIgnoreComment(e.prev(),i))}function isIgnoreComment(e,t){return!!e&&"comment"===e.type&&t.test(e.text)}const c=new Set(["layer"]);function isProcessableRule(e){let t=e.parent;for(;t;){if("atrule"===t.type&&!c.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const l=/^html$/i,u=/^:where\(html\)$/i,p=/^:root$/i,f=/^:where\(:root\)$/i,d=/(html|:root)/i,m=/^var$/i;function isVarFunction(e){return"function"===e.type&&m.test(e.value)&&e.nodes?.length>0}const g=/\bvar\(/i;function parseOrCached(e,t){let r=t.get(e);return r||(r=n(e),t.set(e,r),r)}function toposort(e,t){let n=e.length;const r=new Array(n),o=new Set,s=makeOutgoingEdges(t),a=makeNodesHash(e);for(let t=e.length-1;t>=0;t--){if(o.has(t))continue;const i=e[t],c=new Set,l=[];for(o.add(t),c.add(i),l.push({node:i,outgoing:Array.from(s.get(i)||new Set),next:0});l.length;){const e=l[l.length-1];if(e.nexts.get(e)??0,getLowLink=e=>a.get(e)??0;for(const e of r){if(s.has(e))continue;const t=[{node:e,childIndex:0}];for(s.set(e,l),a.set(e,l),l++,c.push(e),i.add(e);t.length;){const e=t[t.length-1],r=o.get(e.node)??[];if(e.childIndex1)for(const e of t)n.add(e)}}}for(const t of n)e.delete(t);return n}function makeOutgoingEdges(e){const t=new Map;for(let n=0,r=e.length;n{if(r.remaining--,r.remaining<0)throw new Error("Maximum custom property expansion size exceeded, reduce the complexity of your custom properties");if(!isVarFunction(e))return;const a=parseVarFunction(e);if(!a)return;let i=!1;a.fallback&&n.walk(a.fallback,e=>{if(!isVarFunction(e))return;const n=parseVarFunction(e);return n?n.fallback||t.has(n.name.value)?void 0:(i=!0,!1):void 0});let c=t.get(a.name.value)?.nodes;c||!a.fallback||i||(c=a.fallback),void 0!==c&&(c.length?replaceNodeWithNodes(s,o,c):s.splice(o,1,{type:"div",value:" ",before:"",after:"",sourceIndex:e.sourceIndex,sourceEndIndex:e.sourceEndIndex}))}),n.stringify(e.nodes)):""}function walk(e,t){let n,r,o;for(n=0,r=e.length;n"comment"!==e.type&&"space"!==e.type);return 1===t.length&&("word"===t[0].type&&w.test(t[0].value))}function buildCustomPropertiesMap(e,t,r){if(!e.size)return t;const o=new Map(t);{const s=[];for(const[a,i]of e.entries()){const c=parseOrCached(i,r);let l=!1;n.walk(c.nodes,n=>{if(!isVarFunction(n))return;const r=parseVarFunction(n);r&&(r.fallback||e.has(r.name.value)||t.has(r.name.value)?s.push([r.name.value,a]):l=!0)}),l||o.set(a,c)}removeCyclicReferences(o,s)}{const e=[];for(const[t,r]of o.entries())n.walk(r.nodes,n=>{if(!isVarFunction(n))return;const r=parseVarFunction(n);r&&(r.fallback||o.has(r.name.value)?e.push([r.name.value,t]):o.delete(t))});for(let t=0;t{d.test(e.selector)&&e.nodes?.length&&isProcessableRule(e)&&(isBlockIgnored(e)||e.selectors.forEach(t=>{let s;if(u.test(t)||f.test(t))s=0;else if(l.test(t))s=1;else{if(!p.test(t))return;s=2}const a=(c=o,((i=e).parent&&"atrule"===i.parent.type&&"layer"===i.parent.name.toLowerCase()?c.has(i.parent)?c.get(i.parent)+1:0:1e7)+10+s);var i,c;e.each(e=>{if("decl"!==e.type)return;if(!e.variable||isDeclarationIgnored(e))return;if("initial"===e.value.toLowerCase().trim())return;const t=r.get(e.prop)??-1;a>=t&&(r.set(e.prop,a),n.set(e.prop,e.value))})}))}),buildCustomPropertiesMap(n,new Map,t)}function getCustomPropertiesFromSiblings(e,t,n){if(!e.parent)return t;const r=new Map;return e.parent.each(t=>{"decl"===t.type&&t.variable&&e!==t&&(isDeclarationIgnored(t)||r.set(t.prop,t.value))}),r.size?buildCustomPropertiesMap(r,t,n):t}function transformProperties(e,t,r){if(isTransformableDecl(e)&&!isDeclarationIgnored(e)){const o=e.raws?.value?.raw??e.value,s=transformValueAST(n(o),t,r.budget);if(s===o)return;if(parentHasExactFallback(e,s))return void(r.preserve||e.remove());const a=e.cloneBefore({value:s});a.raws?.value?.raw&&(a.raws.value.raw=""),r?.preserve||e.remove()}}const isTransformableDecl=e=>!e.variable&&e.value.includes("--")&&e.value.toLowerCase().includes("var(");function parentHasExactFallback(e,t){if(!e||!e.parent)return!1;const n=e.prop.toLowerCase();let r=e.prev();for(;r;){if("decl"===r.type&&r.prop.toLowerCase()===n&&r.value===t)return!0;r=r.prev()}return!1}const v=/\bvar\(|\(top: var\(--f\)/i,creator=e=>{const t=!("preserve"in Object(e))||Boolean(e?.preserve);if("importFrom"in Object(e))throw new Error('[postcss-custom-properties] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-properties] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-properties",prepare(){let e=new Map;const n=new WeakMap,o=new Map;let s={remaining:h};return{postcssPlugin:"postcss-custom-properties",Once(t){s={remaining:h},e=getCustomPropertiesFromRoot(t,o)},Declaration(a){if(!g.test(a.value))return;if(r(a,v))return;let i=e;t&&a.parent&&(i=n.get(a.parent)??getCustomPropertiesFromSiblings(a,e,o),n.set(a.parent,i)),transformProperties(a,i,{preserve:t,budget:s})}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-custom-properties/src/cascade-layers.ts b/plugins/postcss-custom-properties/src/cascade-layers.ts index 466ad1a344..6b993068a5 100644 --- a/plugins/postcss-custom-properties/src/cascade-layers.ts +++ b/plugins/postcss-custom-properties/src/cascade-layers.ts @@ -108,25 +108,16 @@ export function collectCascadeLayerOrder(root: Root): WeakMap { addLayerToModel(layers, [layerName]); } - // Index the layer names once so resolving each reference is O(1) - // instead of scanning the whole layer list for every node. - const layerIndexByName = new Map(); - for (let i = 0; i < layers.length; i++) { - layerIndexByName.set(layerKey(layers[i]), i); - } - const out: WeakMap = new WeakMap(); for (const [node, layerName] of references) { - out.set(node, layerIndexByName.get(layerKey(layerName)) ?? -1); + out.set(node, layers.findIndex((x) => { + return layerName.equal(x); + })); } return out; } -function layerKey(layerName: LayerName): string { - return layerName.segments().join('\u0000'); -} - // 0 : node was not found // any number : node was found, higher numbers have higher priority // a very large number : node wasn't layered, highest priority diff --git a/plugins/postcss-custom-selectors/CHANGELOG.md b/plugins/postcss-custom-selectors/CHANGELOG.md index b395f0aa8a..901e5321c1 100644 --- a/plugins/postcss-custom-selectors/CHANGELOG.md +++ b/plugins/postcss-custom-selectors/CHANGELOG.md @@ -3,7 +3,6 @@ ### Unreleased (patch) - Bound the total number of generated selectors to prevent an exponential blowup from nested custom selectors. -- Resolve cascade layer order in linear time instead of scanning the layer list for every node. ### 9.0.2 diff --git a/plugins/postcss-custom-selectors/dist/index.mjs b/plugins/postcss-custom-selectors/dist/index.mjs index 57a9cb8c80..ed24883bd3 100644 --- a/plugins/postcss-custom-selectors/dist/index.mjs +++ b/plugins/postcss-custom-selectors/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-selector-parser";import{parse as r,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";const s=r("csstools-implicit-layer")[0];function collectCascadeLayerOrder(e){const o=new Map,n=new Map,a=[];let c=1;e.walkAtRules(e=>{if("layer"!==e.name.toLowerCase())return;{let r=e.parent,t=0;for(;r;){if("atrule"!==r.type||"layer"!==r.name.toLowerCase()){if(r===e.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");r=r.parent}}let l;if(e.nodes)l=normalizeLayerName(e.params,c++);else{if(!e.params.trim())return;l=e.params}let u=r(l);if(u?.length){{let r=e.parent;for(;r&&"atrule"===r.type&&"layer"===r.name.toLowerCase();){const e=n.get(r);if(e){u=u.map(r=>e.concat(r));break}r=r.parent}}if(t(a,u),e.nodes){const r=u[0].concat(s);o.set(e,r),n.set(e,u[0])}}});for(const e of o.values())t(a,[e]);const l=new Map;for(let e=0;e0)return!1;let r=e.parent;for(;r;){if("rule"===r.type)return!1;if("atrule"===r.type&&!o.has(r.name.toLowerCase()))return!1;r=r.parent}return!0}function getCustomSelectors(r,t,s){const o=new Map,n=new Map,a=collectCascadeLayerOrder(r);return r.walkAtRules(r=>{var c,l;if(isProcessableCustomSelectorRule(r))try{const t=r.params.trim(),u=e().astSync(t),i=u?.nodes?.[0]?.nodes?.[0];if(!i||"pseudo"!==i.type||!i.value.startsWith(":--"))return;const p=i.toString(),m=(l=a,(c=r).parent&&"atrule"===c.parent.type&&"layer"===c.parent.name.toLowerCase()?l.has(c.parent)?l.get(c.parent)+1:0:1e7),f=n.get(p)??-1;if(m&&m>=f&&(n.set(p,m),o.set(p,e().astSync(t.slice(p.length).trim()))),!s.preserve){const e=r.parent;r.remove(),removeEmptyAncestorBlocks(e)}}catch(e){r.warn(t,`Failed to parse selector : "${r.params}" with message: "${e instanceof Error?e.message:e}"`)}}),o}function removeEmptyAncestorBlocks(e){if(!e)return;let r=e;for(;r;){if(r.nodes&&r.nodes.length>0)return;const e=r.parent;r.remove(),r=e}}function transformRule(r,t,s){let o=r.selector;try{let t=1e5;o=e(r=>{r.walkPseudos(r=>{if(!s.has(r.value))return;const o=e.pseudo({value:":is",nodes:[]}),n=s.get(r.value);if(n){if(t--,t<0)throw new Error("Maximum custom selector expansion size exceeded, reduce the complexity of your custom selectors");n.each(e=>{o.append(e.clone())}),r.replaceWith(o)}})}).processSync(r.selector)}catch(e){return r.warn(t,`Failed to parse selector : "${o}" with message: "${e instanceof Error?e.message:e}"`),r.selector}return o}const creator=e=>{const r=e?.preserve??!1;if("importFrom"in Object(e))throw new Error('[postcss-custom-selectors] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-selectors] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-selectors",prepare(){const e=new WeakSet;let t=new Map;return{postcssPlugin:"postcss-custom-selectors",Once(e,{result:s}){t=getCustomSelectors(e,s,{preserve:r})},Rule(s,{result:o}){if(e.has(s))return;if(!s.selector?.includes(":--"))return;const n=transformRule(s,o,t);if(n!==s.selector){if(n.length>1e5)throw s.error("Maximum custom selector expansion length exceeded, reduce the complexity of your custom selectors");e.add(s),s.cloneBefore({selector:n}),r||s.remove()}}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import e from"postcss-selector-parser";import{parse as r,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";const s=r("csstools-implicit-layer")[0];function collectCascadeLayerOrder(e){const o=new Map,n=new Map,a=[];let c=1;e.walkAtRules(e=>{if("layer"!==e.name.toLowerCase())return;{let r=e.parent,t=0;for(;r;){if("atrule"!==r.type||"layer"!==r.name.toLowerCase()){if(r===e.root())break;return}if(t++,t>100)throw new Error("Maximum nested @layer depth exceeded, reduce the complexity of your layers");r=r.parent}}let l;if(e.nodes)l=normalizeLayerName(e.params,c++);else{if(!e.params.trim())return;l=e.params}let u=r(l);if(u?.length){{let r=e.parent;for(;r&&"atrule"===r.type&&"layer"===r.name.toLowerCase();){const e=n.get(r);if(e){u=u.map(r=>e.concat(r));break}r=r.parent}}if(t(a,u),e.nodes){const r=u[0].concat(s);o.set(e,r),n.set(e,u[0])}}});for(const e of o.values())t(a,[e]);const l=new WeakMap;for(const[e,r]of o)l.set(e,a.findIndex(e=>r.equal(e)));return l}function normalizeLayerName(e,r){return e.trim()?e:`csstools-anon-layer--${r}`}const o=new Set(["scope","container","layer"]);function isProcessableCustomSelectorRule(e){if("atrule"!==e.type)return!1;if("custom-selector"!==e.name.toLowerCase())return!1;if(!e.params||!e.params.includes(":--"))return!1;if(e.nodes&&e.nodes.length>0)return!1;let r=e.parent;for(;r;){if("rule"===r.type)return!1;if("atrule"===r.type&&!o.has(r.name.toLowerCase()))return!1;r=r.parent}return!0}function getCustomSelectors(r,t,s){const o=new Map,n=new Map,a=collectCascadeLayerOrder(r);return r.walkAtRules(r=>{var c,l;if(isProcessableCustomSelectorRule(r))try{const t=r.params.trim(),u=e().astSync(t),i=u?.nodes?.[0]?.nodes?.[0];if(!i||"pseudo"!==i.type||!i.value.startsWith(":--"))return;const p=i.toString(),m=(l=a,(c=r).parent&&"atrule"===c.parent.type&&"layer"===c.parent.name.toLowerCase()?l.has(c.parent)?l.get(c.parent)+1:0:1e7),f=n.get(p)??-1;if(m&&m>=f&&(n.set(p,m),o.set(p,e().astSync(t.slice(p.length).trim()))),!s.preserve){const e=r.parent;r.remove(),removeEmptyAncestorBlocks(e)}}catch(e){r.warn(t,`Failed to parse selector : "${r.params}" with message: "${e instanceof Error?e.message:e}"`)}}),o}function removeEmptyAncestorBlocks(e){if(!e)return;let r=e;for(;r;){if(r.nodes&&r.nodes.length>0)return;const e=r.parent;r.remove(),r=e}}function transformRule(r,t,s){let o=r.selector;try{let t=1e5;o=e(r=>{r.walkPseudos(r=>{if(!s.has(r.value))return;const o=e.pseudo({value:":is",nodes:[]}),n=s.get(r.value);if(n){if(t--,t<0)throw new Error("Maximum custom selector expansion size exceeded, reduce the complexity of your custom selectors");n.each(e=>{o.append(e.clone())}),r.replaceWith(o)}})}).processSync(r.selector)}catch(e){return r.warn(t,`Failed to parse selector : "${o}" with message: "${e instanceof Error?e.message:e}"`),r.selector}return o}const creator=e=>{const r=e?.preserve??!1;if("importFrom"in Object(e))throw new Error('[postcss-custom-selectors] "importFrom" is no longer supported');if("exportTo"in Object(e))throw new Error('[postcss-custom-selectors] "exportTo" is no longer supported');return{postcssPlugin:"postcss-custom-selectors",prepare(){const e=new WeakSet;let t=new Map;return{postcssPlugin:"postcss-custom-selectors",Once(e,{result:s}){t=getCustomSelectors(e,s,{preserve:r})},Rule(s,{result:o}){if(e.has(s))return;if(!s.selector?.includes(":--"))return;const n=transformRule(s,o,t);if(n!==s.selector){if(n.length>1e5)throw s.error("Maximum custom selector expansion length exceeded, reduce the complexity of your custom selectors");e.add(s),s.cloneBefore({selector:n}),r||s.remove()}}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-custom-selectors/src/cascade-layers.ts b/plugins/postcss-custom-selectors/src/cascade-layers.ts index 466ad1a344..6b993068a5 100644 --- a/plugins/postcss-custom-selectors/src/cascade-layers.ts +++ b/plugins/postcss-custom-selectors/src/cascade-layers.ts @@ -108,25 +108,16 @@ export function collectCascadeLayerOrder(root: Root): WeakMap { addLayerToModel(layers, [layerName]); } - // Index the layer names once so resolving each reference is O(1) - // instead of scanning the whole layer list for every node. - const layerIndexByName = new Map(); - for (let i = 0; i < layers.length; i++) { - layerIndexByName.set(layerKey(layers[i]), i); - } - const out: WeakMap = new WeakMap(); for (const [node, layerName] of references) { - out.set(node, layerIndexByName.get(layerKey(layerName)) ?? -1); + out.set(node, layers.findIndex((x) => { + return layerName.equal(x); + })); } return out; } -function layerKey(layerName: LayerName): string { - return layerName.segments().join('\u0000'); -} - // 0 : node was not found // any number : node was found, higher numbers have higher priority // a very large number : node wasn't layered, highest priority diff --git a/plugins/postcss-design-tokens/CHANGELOG.md b/plugins/postcss-design-tokens/CHANGELOG.md index d86d3a88c1..324f7a8c58 100644 --- a/plugins/postcss-design-tokens/CHANGELOG.md +++ b/plugins/postcss-design-tokens/CHANGELOG.md @@ -1,10 +1,5 @@ # Changes to PostCSS Design Tokens -### Unreleased (patch) - -- Bound the total resolved token value length to prevent an exponential blowup from token references. - - ### 5.0.1 _September 25, 2026_ diff --git a/plugins/postcss-design-tokens/dist/index.mjs b/plugins/postcss-design-tokens/dist/index.mjs index 9db6148d46..56bbf85944 100644 --- a/plugins/postcss-design-tokens/dist/index.mjs +++ b/plugins/postcss-design-tokens/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-value-parser";import t from"node:path";import n from"node:fs/promises";import r from"node:module";import{parseListOfComponentValues as o,isFunctionNode as s,isWhitespaceNode as a,isCommentNode as i,isTokenNode as u}from"@csstools/css-parser-algorithms";import{tokenize as l,isTokenString as c,isTokenIdent as f}from"@csstools/css-tokenizer";function toposort(e,t){let n=e.length;const r=new Array(n),o=new Set,s=makeOutgoingEdges(t),a=makeNodesHash(e);t.forEach(function(e){if(!a.has(e[0])||!a.has(e[1]))throw new Error("Unknown token. Make sure to provide all tokens used in aliases.")});for(let t=e.length-1;t>=0;t--){if(o.has(t))continue;const i=e[t],u=new Set,l=[];for(o.add(t),u.add(i),l.push({node:i,outgoing:Array.from(s.get(i)||new Set),next:0});l.length;){const e=l[l.length-1];if(e.nextapplyTransformsToValue(o,e),name:String(e.name??"")||t,comment:String(e.comment??"")||void 0,metadata:{name:String(e.name??"")?t:void 0,path:[...n,t],filePath:r,isSource:!0}}}const p=new Map;function applyTransformsToValue(t,n){if(!t)return"";if(!n)return t;if(!n.toUnit)return t;const r=e.unit(t);if(!r||r.unit===n.toUnit)return t;if(!r.unit){if(p.has(n.toUnit)){if(p.get(n.toUnit))return`${t}${n.toUnit}`;throw new Error(`Invalid unit "${n.toUnit}" for "${t}"`)}try{const r=e.unit(`${t}${n.toUnit}`);if(r&&r.unit===n.toUnit)return p.set(n.toUnit,!0),`${t}${n.toUnit}`;p.set(n.toUnit,!1)}catch{p.set(n.toUnit,!1)}throw new Error(`Invalid unit "${n.toUnit}" for "${t}"`)}return"rem"===r.unit&&"px"===n.toUnit?remToPx(parseFloat(r.number),n.pluginOptions?.rootFontSize??16):"px"===r.unit&&"rem"===n.toUnit?pxToRem(parseFloat(r.number),n.pluginOptions?.rootFontSize??16):t}function remToPx(e,t){return`${formatFloat(e*t)}px`}function pxToRem(e,t){return`${formatFloat(e/t)}rem`}function formatFloat(e){if(Number.isInteger(e))return e.toString();let t=e.toFixed(5);for(let e=t.length-1;e>=0&&"."!==t[e]&&"0"===t[e];e--)t=t.slice(0,e);return t}const d=1e6;function dereferenceTokenValues(e){const t=new Set,n=new Map;let r=0;for(const[r,o]of e.entries()){const e=parseReferences(o.value);e.length&&(t.add(r),n.set(r,e))}for(const[o,s]of n.entries()){for(let n=0;n"value-reference"===e.type))continue;const a=s.map(e=>e.value).join("");if(r+=a.length,r>d)throw new Error("Maximum design token expansion size exceeded, reduce the complexity of your tokens");const i=e.get(o);i.value=a,i.cssValue=e=>applyTransformsToValue(a,e),e.set(o,i),t.delete(o),n.delete(o)}if(0===t.size)return e;{const o=Array.from(e.keys()),s=[];for(const[e,t]of n.entries())for(let n=0;n"value-reference"===e.type))throw new Error('Token "'+o+'" can not be fully resolved');const a=s.map(e=>e.value).join("");if(r+=a.length,r>d)throw new Error("Maximum design token expansion size exceeded, reduce the complexity of your tokens");const i=e.get(o);i.value=a,i.cssValue=e=>applyTransformsToValue(a,e),e.set(o,i),t.delete(o),n.delete(o)}if(0===t.size)return e}return e}function parseReferences(e){if("string"!=typeof e)return[];if(-1===e.indexOf("{"))return[];const t=[];let n=!1,r=!1,o="";for(let s=0;s0&&(t.push({type:"value-non-reference",value:o}),o=""),r=!0;break;case"}":if(!r)throw new Error('Unexpected "}" in "'+e+'" at '+s);if(0===o.length)throw new Error('Empty alias "{}" in "'+e+'" at '+s);{let e=o.trim();".value"===e.slice(-6)&&(e=e.slice(0,-6)),t.push({type:"value-reference",raw:e}),o=""}n=!0,r=!1;break;default:o+=a}}if(r)throw new Error('Unexpected end of alias in "'+e+'"');return o.length>0&&t.push({type:"value-non-reference",value:o}),n?t:[]}function isTokenGroup(e){return"object"==typeof e&&null!==e&&void 0===e.value}function extractTokens(e,t,n){const r=new Map;for(const o in e)if(Object.hasOwnProperty.call(e,o)){if(null===e[o]||"object"!=typeof e[o]||Array.isArray(e[o]))throw new Error(`Parsing error at "${[...t,o].join(".")}"`);const s=e[o];if(!s)throw new Error(`Parsing error at "${[...t,o].join(".")}"`);if(!isTokenGroup(s)){const e=extractStyleDictionaryV3Token(s,o,t,n);r.set(e.metadata.path.join("."),e);continue}for(const[e,a]of extractTokens(s,[...t,o],n).entries())r.set(e,a)}return r}function extractStyleDictionaryV3Tokens(e,t){return dereferenceTokenValues(extractTokens(e,[],t))}function extractStyleDictionaryTokens(e,t,n){return extractStyleDictionaryV3Tokens(t,n)}const m="6b4e71e7-4787-42f7-a092-8684961895db";function parseImport(t){const n=e(t),r={filePath:"",format:"standard",conditions:[m]};return n.walk(e=>{"function"===e.type&&"url"===e.value&&(r.filePath=e.nodes[0].value),"function"===e.type&&"format"===e.value&&(r.format=e.nodes[0].value),"function"===e.type&&"when"===e.value&&(r.conditions=e.nodes.filter(e=>"string"===e.type).map(e=>e.value))}),r.conditions.length||(r.conditions=[m]),r}async function tokensFromImport(e,o,s,a,i,u){const{filePath:l,format:c,conditions:f}=parseImport(i);if(!f.every(e=>s.includes(e)))return!1;let p;try{if(l.startsWith("node_modules://")){p=r.createRequire(t.dirname(a)).resolve(l.slice(15))}else if(l.startsWith("node_modules:")){p=r.createRequire(t.dirname(a)).resolve(l.slice(13))}else p=t.resolve(t.dirname(a),l)}catch(e){throw new Error(`Failed to read ${l} with error ${e instanceof Error?e.message:e}`,{cause:e})}if(u.has(p))return!1;o.result.messages.push({type:"dependency",plugin:"postcss-design-tokens",file:p,parent:e.source?.input?.file}),u.add(p);const d=await n.readFile(p,"utf8"),m=JSON.parse(d);if("style-dictionary3"===c)return{filePath:t.resolve(l),tokens:extractStyleDictionaryTokens(0,m,p)};throw new Error("Unsupported format: "+c)}function mergeTokens(e,t){const n=new Map(e);for(const[e,r]of t)n.set(e,r);return n}function parsePluginOptions(e){const t={importAtRuleName:"design-tokens",is:[m],unitsAndValues:{rootFontSize:16},valueFunctionName:"design-token"};return e?("object"!=typeof e||(Array.isArray(e.is)&&(t.is=e.is.filter(e=>"string"==typeof e)),0===t.is.length&&(t.is=[m]),"object"==typeof e.unitsAndValues&&"number"==typeof e.unitsAndValues.rootFontSize&&((n=e.unitsAndValues.rootFontSize)>0&&n!==1/0)&&(t.unitsAndValues.rootFontSize=e.unitsAndValues.rootFontSize),"string"==typeof e.valueFunctionName&&(t.valueFunctionName=e.valueFunctionName),"string"==typeof e.importAtRuleName&&(t.importAtRuleName=e.importAtRuleName)),t):t;var n}function parseComponentValues(e){return t=l({css:e},{onParseError:e=>{throw e}}),o(t,{onParseError:e=>{throw e}});var t}function transform(e,t,n,r,o){const s=parseComponentValues(r);let a=!1;return s.forEach((r,i)=>{if("walk"in r){{const u=transformComponentValue(r,e,t,n,o);if(u)return s.splice(i,1,...u),a=!0,!1}r.walk((r,s)=>{if("string"==typeof s)return;const i=transformComponentValue(r.node,e,t,n,o);return i?(r.parent.value.splice(s,1,...i),a=!0,!1):void 0})}}),a?s.map(e=>e.toString()).join(""):r}function transformComponentValue(e,t,n,r,o){if(!s(e))return;if(e.getName().toLowerCase()!==o.valueFunctionName)return;let l="",p="",d="";for(let t=0;t{const t=parsePluginOptions(e);return{postcssPlugin:"postcss-design-tokens",prepare(){let e=new Map,n=new Set;return{postcssPlugin:"postcss-design-tokens",OnceExit(){e=new Map,n=new Set},async Once(r,o){const s=[];r.walkAtRules(e=>{if(e.name.toLowerCase()!==t.importAtRuleName)return;if(!e?.source?.input?.file)return;const n=e.source.input.file,r=e.params;e.remove(),s.push({filePath:n,params:r,node:e})});for(const a of s.values()){let s;try{if(s=await tokensFromImport(r,o,t.is,a.filePath,a.params,n),!s)continue}catch(e){a.node.warn(o.result,`Failed to import design tokens from "${a.params}" with error:\n\t`+(e instanceof Error?e.message:e));continue}o.result.messages.push({type:"dependency",plugin:"postcss-design-tokens",file:s.filePath,parent:a.filePath}),e=mergeTokens(e,s.tokens)}},Declaration(n,{result:r}){if(n.value.toLowerCase().includes(t.valueFunctionName))try{const o=transform(e,r,n,n.value,t);if(o===n.value)return;n.value=o}catch{n.warn(r,`Failed to parse and transform "${n.value}"`)}},AtRule(n,{result:r}){if(n.params.toLowerCase().includes(t.valueFunctionName))try{const o=transform(e,r,n,n.params,t);if(o===n.params)return;n.params=o}catch{n.warn(r,`Failed to parse and transform "${n.params}"`)}}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import e from"postcss-value-parser";import t from"node:path";import n from"node:fs/promises";import r from"node:module";import{parseListOfComponentValues as o,isFunctionNode as s,isWhitespaceNode as a,isCommentNode as i,isTokenNode as u}from"@csstools/css-parser-algorithms";import{tokenize as l,isTokenString as c,isTokenIdent as f}from"@csstools/css-tokenizer";function toposort(e,t){let n=e.length;const r=new Array(n),o=new Set,s=makeOutgoingEdges(t),a=makeNodesHash(e);t.forEach(function(e){if(!a.has(e[0])||!a.has(e[1]))throw new Error("Unknown token. Make sure to provide all tokens used in aliases.")});for(let t=e.length-1;t>=0;t--){if(o.has(t))continue;const i=e[t],u=new Set,l=[];for(o.add(t),u.add(i),l.push({node:i,outgoing:Array.from(s.get(i)||new Set),next:0});l.length;){const e=l[l.length-1];if(e.nextapplyTransformsToValue(o,e),name:String(e.name??"")||t,comment:String(e.comment??"")||void 0,metadata:{name:String(e.name??"")?t:void 0,path:[...n,t],filePath:r,isSource:!0}}}const p=new Map;function applyTransformsToValue(t,n){if(!t)return"";if(!n)return t;if(!n.toUnit)return t;const r=e.unit(t);if(!r||r.unit===n.toUnit)return t;if(!r.unit){if(p.has(n.toUnit)){if(p.get(n.toUnit))return`${t}${n.toUnit}`;throw new Error(`Invalid unit "${n.toUnit}" for "${t}"`)}try{const r=e.unit(`${t}${n.toUnit}`);if(r&&r.unit===n.toUnit)return p.set(n.toUnit,!0),`${t}${n.toUnit}`;p.set(n.toUnit,!1)}catch{p.set(n.toUnit,!1)}throw new Error(`Invalid unit "${n.toUnit}" for "${t}"`)}return"rem"===r.unit&&"px"===n.toUnit?remToPx(parseFloat(r.number),n.pluginOptions?.rootFontSize??16):"px"===r.unit&&"rem"===n.toUnit?pxToRem(parseFloat(r.number),n.pluginOptions?.rootFontSize??16):t}function remToPx(e,t){return`${formatFloat(e*t)}px`}function pxToRem(e,t){return`${formatFloat(e/t)}rem`}function formatFloat(e){if(Number.isInteger(e))return e.toString();let t=e.toFixed(5);for(let e=t.length-1;e>=0&&"."!==t[e]&&"0"===t[e];e--)t=t.slice(0,e);return t}function dereferenceTokenValues(e){const t=new Set,n=new Map;for(const[r,o]of e.entries()){const e=parseReferences(o.value);e.length&&(t.add(r),n.set(r,e))}for(const[r,o]of n.entries()){for(let n=0;n"value-reference"===e.type))continue;const s=o.map(e=>e.value).join(""),a=e.get(r);a.value=s,a.cssValue=e=>applyTransformsToValue(s,e),e.set(r,a),t.delete(r),n.delete(r)}if(0===t.size)return e;{const r=Array.from(e.keys()),o=[];for(const[e,t]of n.entries())for(let n=0;n"value-reference"===e.type))throw new Error('Token "'+r+'" can not be fully resolved');const s=o.map(e=>e.value).join(""),a=e.get(r);a.value=s,a.cssValue=e=>applyTransformsToValue(s,e),e.set(r,a),t.delete(r),n.delete(r)}if(0===t.size)return e}return e}function parseReferences(e){if("string"!=typeof e)return[];if(-1===e.indexOf("{"))return[];const t=[];let n=!1,r=!1,o="";for(let s=0;s0&&(t.push({type:"value-non-reference",value:o}),o=""),r=!0;break;case"}":if(!r)throw new Error('Unexpected "}" in "'+e+'" at '+s);if(0===o.length)throw new Error('Empty alias "{}" in "'+e+'" at '+s);{let e=o.trim();".value"===e.slice(-6)&&(e=e.slice(0,-6)),t.push({type:"value-reference",raw:e}),o=""}n=!0,r=!1;break;default:o+=a}}if(r)throw new Error('Unexpected end of alias in "'+e+'"');return o.length>0&&t.push({type:"value-non-reference",value:o}),n?t:[]}function isTokenGroup(e){return"object"==typeof e&&null!==e&&void 0===e.value}function extractTokens(e,t,n){const r=new Map;for(const o in e)if(Object.hasOwnProperty.call(e,o)){if(null===e[o]||"object"!=typeof e[o]||Array.isArray(e[o]))throw new Error(`Parsing error at "${[...t,o].join(".")}"`);const s=e[o];if(!s)throw new Error(`Parsing error at "${[...t,o].join(".")}"`);if(!isTokenGroup(s)){const e=extractStyleDictionaryV3Token(s,o,t,n);r.set(e.metadata.path.join("."),e);continue}for(const[e,a]of extractTokens(s,[...t,o],n).entries())r.set(e,a)}return r}function extractStyleDictionaryV3Tokens(e,t){return dereferenceTokenValues(extractTokens(e,[],t))}function extractStyleDictionaryTokens(e,t,n){return extractStyleDictionaryV3Tokens(t,n)}const d="6b4e71e7-4787-42f7-a092-8684961895db";function parseImport(t){const n=e(t),r={filePath:"",format:"standard",conditions:[d]};return n.walk(e=>{"function"===e.type&&"url"===e.value&&(r.filePath=e.nodes[0].value),"function"===e.type&&"format"===e.value&&(r.format=e.nodes[0].value),"function"===e.type&&"when"===e.value&&(r.conditions=e.nodes.filter(e=>"string"===e.type).map(e=>e.value))}),r.conditions.length||(r.conditions=[d]),r}async function tokensFromImport(e,o,s,a,i,u){const{filePath:l,format:c,conditions:f}=parseImport(i);if(!f.every(e=>s.includes(e)))return!1;let p;try{if(l.startsWith("node_modules://")){p=r.createRequire(t.dirname(a)).resolve(l.slice(15))}else if(l.startsWith("node_modules:")){p=r.createRequire(t.dirname(a)).resolve(l.slice(13))}else p=t.resolve(t.dirname(a),l)}catch(e){throw new Error(`Failed to read ${l} with error ${e instanceof Error?e.message:e}`,{cause:e})}if(u.has(p))return!1;o.result.messages.push({type:"dependency",plugin:"postcss-design-tokens",file:p,parent:e.source?.input?.file}),u.add(p);const d=await n.readFile(p,"utf8"),m=JSON.parse(d);if("style-dictionary3"===c)return{filePath:t.resolve(l),tokens:extractStyleDictionaryTokens(0,m,p)};throw new Error("Unsupported format: "+c)}function mergeTokens(e,t){const n=new Map(e);for(const[e,r]of t)n.set(e,r);return n}function parsePluginOptions(e){const t={importAtRuleName:"design-tokens",is:[d],unitsAndValues:{rootFontSize:16},valueFunctionName:"design-token"};return e?("object"!=typeof e||(Array.isArray(e.is)&&(t.is=e.is.filter(e=>"string"==typeof e)),0===t.is.length&&(t.is=[d]),"object"==typeof e.unitsAndValues&&"number"==typeof e.unitsAndValues.rootFontSize&&((n=e.unitsAndValues.rootFontSize)>0&&n!==1/0)&&(t.unitsAndValues.rootFontSize=e.unitsAndValues.rootFontSize),"string"==typeof e.valueFunctionName&&(t.valueFunctionName=e.valueFunctionName),"string"==typeof e.importAtRuleName&&(t.importAtRuleName=e.importAtRuleName)),t):t;var n}function parseComponentValues(e){return t=l({css:e},{onParseError:e=>{throw e}}),o(t,{onParseError:e=>{throw e}});var t}function transform(e,t,n,r,o){const s=parseComponentValues(r);let a=!1;return s.forEach((r,i)=>{if("walk"in r){{const u=transformComponentValue(r,e,t,n,o);if(u)return s.splice(i,1,...u),a=!0,!1}r.walk((r,s)=>{if("string"==typeof s)return;const i=transformComponentValue(r.node,e,t,n,o);return i?(r.parent.value.splice(s,1,...i),a=!0,!1):void 0})}}),a?s.map(e=>e.toString()).join(""):r}function transformComponentValue(e,t,n,r,o){if(!s(e))return;if(e.getName().toLowerCase()!==o.valueFunctionName)return;let l="",p="",d="";for(let t=0;t{const t=parsePluginOptions(e);return{postcssPlugin:"postcss-design-tokens",prepare(){let e=new Map,n=new Set;return{postcssPlugin:"postcss-design-tokens",OnceExit(){e=new Map,n=new Set},async Once(r,o){const s=[];r.walkAtRules(e=>{if(e.name.toLowerCase()!==t.importAtRuleName)return;if(!e?.source?.input?.file)return;const n=e.source.input.file,r=e.params;e.remove(),s.push({filePath:n,params:r,node:e})});for(const a of s.values()){let s;try{if(s=await tokensFromImport(r,o,t.is,a.filePath,a.params,n),!s)continue}catch(e){a.node.warn(o.result,`Failed to import design tokens from "${a.params}" with error:\n\t`+(e instanceof Error?e.message:e));continue}o.result.messages.push({type:"dependency",plugin:"postcss-design-tokens",file:s.filePath,parent:a.filePath}),e=mergeTokens(e,s.tokens)}},Declaration(n,{result:r}){if(n.value.toLowerCase().includes(t.valueFunctionName))try{const o=transform(e,r,n,n.value,t);if(o===n.value)return;n.value=o}catch{n.warn(r,`Failed to parse and transform "${n.value}"`)}},AtRule(n,{result:r}){if(n.params.toLowerCase().includes(t.valueFunctionName))try{const o=transform(e,r,n,n.params,t);if(o===n.params)return;n.params=o}catch{n.warn(r,`Failed to parse and transform "${n.params}"`)}}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts index cd4d787dbb..89e4bb3ae9 100644 --- a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts @@ -4,16 +4,9 @@ import { toposort } from '../../toposort/toposort'; import type { StyleDictionaryV3TokenValue } from './value'; import { applyTransformsToValue } from './value'; -// Tokens can reference each other and duplicate their contents, so a token -// chain that references the previous token twice grows the resolved values -// exponentially. Bound the total resolved value length so that a small token -// file can not exhaust memory. -const MAX_RESOLVED_VALUE_LENGTH = 1_000_000; - export function dereferenceTokenValues(tokens: Map): Map { const tainted = new Set(); const referenceASTs = new Map>(); - let resolvedLength = 0; // Gather all references. { @@ -60,12 +53,6 @@ export function dereferenceTokenValues(tokens: Map part.value).join(''); - - resolvedLength += value.length; - if (resolvedLength > MAX_RESOLVED_VALUE_LENGTH) { - throw new Error('Maximum design token expansion size exceeded, reduce the complexity of your tokens'); - } - const currentToken = tokens.get(id)!; currentToken.value = value; @@ -138,12 +125,6 @@ export function dereferenceTokenValues(tokens: Map part.value).join(''); - - resolvedLength += value.length; - if (resolvedLength > MAX_RESOLVED_VALUE_LENGTH) { - throw new Error('Maximum design token expansion size exceeded, reduce the complexity of your tokens'); - } - const currentToken = tokens.get(id)!; currentToken.value = value; diff --git a/plugins/postcss-property-rule-prelude-list/dist/index.mjs b/plugins/postcss-property-rule-prelude-list/dist/index.mjs index c85ac821fa..a09758759e 100644 --- a/plugins/postcss-property-rule-prelude-list/dist/index.mjs +++ b/plugins/postcss-property-rule-prelude-list/dist/index.mjs @@ -1 +1 @@ -import{parseCommaSeparatedListOfComponentValues as e,stringify as s}from"@csstools/css-parser-algorithms";import{tokenize as t}from"@csstools/css-tokenizer";const r=/^property$/i;function countAtRules(e){let s=1;return e.walkAtRules(()=>{s++}),s}const creator=()=>{let o=0;return{postcssPlugin:"postcss-property-rule-prelude-list",Once(){o=0},AtRule(l){if(!r.test(l.name))return;if(!l.params.includes(","))return;const n=e(t({css:l.params}));if(!(n.length<2)){if(o+=n.length*countAtRules(l),o>1e4)throw new Error("Maximum @property expansion size exceeded, reduce the complexity of your stylesheet");n.forEach(e=>{l.cloneBefore({params:s([e]).trim()})}),l.remove()}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parseCommaSeparatedListOfComponentValues as e,stringify as s}from"@csstools/css-parser-algorithms";import{tokenize as r}from"@csstools/css-tokenizer";const t=/^property$/i,creator=()=>{let o=0;return{postcssPlugin:"postcss-property-rule-prelude-list",Once(){o=0},AtRule(p){if(!t.test(p.name))return;if(!p.params.includes(","))return;const c=e(r({css:p.params}));if(!(c.length<2)){if(o+=c.length,o>5e3)throw new Error("Maximum @property expansion size exceeded, reduce the complexity of your stylesheet");c.forEach(e=>{p.cloneBefore({params:s([e]).trim()})}),p.remove()}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-property-rule-prelude-list/src/index.ts b/plugins/postcss-property-rule-prelude-list/src/index.ts index 860f99e907..126ee8450d 100644 --- a/plugins/postcss-property-rule-prelude-list/src/index.ts +++ b/plugins/postcss-property-rule-prelude-list/src/index.ts @@ -1,6 +1,6 @@ import { parseCommaSeparatedListOfComponentValues, stringify } from '@csstools/css-parser-algorithms'; import { tokenize } from '@csstools/css-tokenizer'; -import type { AtRule, PluginCreator } from 'postcss'; +import type { PluginCreator } from 'postcss'; /** postcss-property-rule-prelude-list plugin options */ export type pluginOptions = never; @@ -11,16 +11,7 @@ const IS_AT_PROPERTY_REGEX = /^property$/i; // its children) once per list item. When such rules are nested, the clones are // expanded again and the output grows exponentially with the nesting depth. // Bound the total number of at-rules generated per stylesheet. -const MAX_EXPANDED_AT_RULES = 10_000; - -function countAtRules(atRule: AtRule): number { - let count = 1; - atRule.walkAtRules(() => { - count++; - }); - - return count; -} +const MAX_EXPANDED_AT_RULES = 5_000; const creator: PluginCreator = () => { let expandedAtRules = 0; @@ -44,7 +35,7 @@ const creator: PluginCreator = () => { return; } - expandedAtRules += list.length * countAtRules(atRule); + expandedAtRules += list.length; if (expandedAtRules > MAX_EXPANDED_AT_RULES) { throw new Error('Maximum @property expansion size exceeded, reduce the complexity of your stylesheet'); } diff --git a/plugins/postcss-system-ui-font-family/CHANGELOG.md b/plugins/postcss-system-ui-font-family/CHANGELOG.md index e5fb3f10ea..72f72042f7 100644 --- a/plugins/postcss-system-ui-font-family/CHANGELOG.md +++ b/plugins/postcss-system-ui-font-family/CHANGELOG.md @@ -1,9 +1,5 @@ # Changes to PostCSS System UI Font Family -### Unreleased (patch) - -- Improved performance by inserting generated declarations as a single batch - ### 2.0.1 _September 25, 2026_ diff --git a/plugins/postcss-system-ui-font-family/dist/index.mjs b/plugins/postcss-system-ui-font-family/dist/index.mjs index 6fe7c548da..7800b2c946 100644 --- a/plugins/postcss-system-ui-font-family/dist/index.mjs +++ b/plugins/postcss-system-ui-font-family/dist/index.mjs @@ -1 +1 @@ -import{parseCommaSeparatedListOfComponentValues as e,isWhiteSpaceOrCommentNode as s,isTokenNode as t,stringify as o,TokenNode as n}from"@csstools/css-parser-algorithms";import{tokenize as r,isTokenIdent as i,TokenType as l}from"@csstools/css-tokenizer";const a=/^font(?:-family)?$/i,c=["system-ui","-apple-system","Segoe UI","Roboto","Ubuntu","Cantarell","Noto Sans","sans-serif"];const u=c.join(","),creator=p=>{const f=Object.assign({preserve:!0},p);return{postcssPlugin:"postcss-system-ui-font-family",Declaration(p){if(!p.variable&&!a.test(p.prop))return;if(p.value.includes(u))return;const m=p.value,v=r({css:m}),g=e(v),y=new Map;let h=-1;e:for(let e=0;e!("system-ui"!==e||!j)||!d.has(e)).map(e=>[new n([l.Ident,e,-1,-1,{value:e}])]));var d,j;if(0===b.length)return;g.splice(h,1,...b);const w=o(g);w!==m&&(p.before([p.clone({prop:p.prop,value:w})]),p.remove())}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parseCommaSeparatedListOfComponentValues as e,isWhiteSpaceOrCommentNode as s,isTokenNode as t,stringify as o,TokenNode as n}from"@csstools/css-parser-algorithms";import{tokenize as r,isTokenIdent as i,TokenType as l}from"@csstools/css-tokenizer";const a=/^font(?:-family)?$/i,c=["system-ui","-apple-system","Segoe UI","Roboto","Ubuntu","Cantarell","Noto Sans","sans-serif"];const u=c.join(","),creator=p=>{const f=Object.assign({preserve:!0},p);return{postcssPlugin:"postcss-system-ui-font-family",Declaration(p){if(!p.variable&&!a.test(p.prop))return;if(p.value.includes(u))return;const m=p.value,v=r({css:m}),g=e(v),y=new Map;let h=-1;e:for(let e=0;e!("system-ui"!==e||!j)||!b.has(e)).map(e=>[new n([l.Ident,e,-1,-1,{value:e}])]));var b,j;if(0===d.length)return;g.splice(h,1,...d);const w=o(g);w!==m&&(p.cloneBefore({prop:p.prop,value:w}),p.remove())}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-system-ui-font-family/src/index.ts b/plugins/postcss-system-ui-font-family/src/index.ts index ab68868ce1..671ab349ba 100644 --- a/plugins/postcss-system-ui-font-family/src/index.ts +++ b/plugins/postcss-system-ui-font-family/src/index.ts @@ -115,14 +115,10 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - // Insert as a batch: resolving the reference index for every single - // insertion is quadratic for a rule with many declarations. - decl.before([ - decl.clone({ - prop: decl.prop, - value: modified, - }), - ]); + decl.cloneBefore({ + prop: decl.prop, + value: modified, + }); decl.remove(); }, From 0bc465998f13983ba06955a23eec6b3722333d52 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Wed, 30 Sep 2026 22:38:47 +0200 Subject: [PATCH 5/6] fix --- plugins/postcss-rewrite-url/CHANGELOG.md | 4 ---- plugins/postcss-rewrite-url/dist/index.mjs | 2 +- plugins/postcss-rewrite-url/src/index.ts | 9 +++++++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/postcss-rewrite-url/CHANGELOG.md b/plugins/postcss-rewrite-url/CHANGELOG.md index 7705e4d816..d5b24e8110 100644 --- a/plugins/postcss-rewrite-url/CHANGELOG.md +++ b/plugins/postcss-rewrite-url/CHANGELOG.md @@ -1,9 +1,5 @@ # Changes to PostCSS Rewrite URL -### Unreleased (patch) - -- Visit each declaration and at-rule only once, so the rewriter is called at most once per node. - ### 3.0.1 _September 25, 2026_ diff --git a/plugins/postcss-rewrite-url/dist/index.mjs b/plugins/postcss-rewrite-url/dist/index.mjs index 5127ac53d4..4c716de36c 100644 --- a/plugins/postcss-rewrite-url/dist/index.mjs +++ b/plugins/postcss-rewrite-url/dist/index.mjs @@ -1 +1 @@ -import{tokenize as r,isTokenString as e}from"@csstools/css-tokenizer";import{parseCommaSeparatedListOfComponentValues as t,replaceComponentValues as o,isFunctionNode as s,isWhiteSpaceOrCommentNode as i,isTokenNode as a,parseListOfComponentValues as n,stringify as u}from"@csstools/css-parser-algorithms";function serializeString(r){let e="";for(const t of r){const r=t.codePointAt(0);if(void 0!==r)switch(r){case 0:e+=String.fromCodePoint(65533);break;case 127:e+=`\\${r.toString(16)}`;break;case 34:case 39:case 92:e+=`\\${t}`;break;default:if(1<=r&&r<=31){e+=`\\${r.toString(16)} `;break}e+=t}else e+=String.fromCodePoint(65533)}return e}const l=/rewrite-url\(/i,c=/^rewrite-url$/i,creator=r=>{const e=r?.rewriter??(r=>r);return{postcssPlugin:"postcss-rewrite-url",Declaration(r,{result:t}){rewriteDeclaration(r,t,e)},AtRule(r,{result:t}){rewriteAtRule(r,t,e)}}};function rewriteDeclaration(r,e,t){if(!l.test(r.value))return;const o={type:"declaration-value",rootFrom:e.opts.from,from:r.source?.input.from,property:r.prop},s=rewrite(t,r.value,o);s!==r.value&&(r.value=s)}function rewriteAtRule(r,e,t){if(!l.test(r.params))return;const o={type:"at-rule-prelude",rootFrom:e.opts.from,from:r.source?.input.from,atRuleName:r.name},s=rewrite(t,r.params,o);s!==r.params&&(r.params=s)}function rewrite(l,f,m){const p=t(r({css:f})),v=o(p,t=>{if(!s(t)||!c.test(t.getName()))return;const o=t.value.filter(r=>!i(r));for(let s=0;sr.toString())},m);if(!1===u)return;const c=n(r({css:[`"${serializeString(u.url)}"`,...u.urlModifiers??a].join(" ")}));return t.value=c,t.name[1]="url(",t.name[4].value="url",t}return}});return u(v)}creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{tokenize as r,isTokenString as e}from"@csstools/css-tokenizer";import{parseCommaSeparatedListOfComponentValues as t,replaceComponentValues as o,isFunctionNode as s,isWhiteSpaceOrCommentNode as i,isTokenNode as a,parseListOfComponentValues as l,stringify as n}from"@csstools/css-parser-algorithms";function serializeString(r){let e="";for(const t of r){const r=t.codePointAt(0);if(void 0!==r)switch(r){case 0:e+=String.fromCodePoint(65533);break;case 127:e+=`\\${r.toString(16)}`;break;case 34:case 39:case 92:e+=`\\${t}`;break;default:if(1<=r&&r<=31){e+=`\\${r.toString(16)} `;break}e+=t}else e+=String.fromCodePoint(65533)}return e}const u=/rewrite-url\(/i,c=/^rewrite-url$/i,creator=r=>{const e=r?.rewriter??(r=>r);return{postcssPlugin:"postcss-rewrite-url",Once(r,{result:t}){r.walkDecls(r=>{rewriteDeclaration(r,t,e)}),r.walkAtRules(r=>{rewriteAtRule(r,t,e)})},Declaration(r,{result:t}){rewriteDeclaration(r,t,e)},AtRule(r,{result:t}){rewriteAtRule(r,t,e)}}};function rewriteDeclaration(r,e,t){if(!u.test(r.value))return;const o={type:"declaration-value",rootFrom:e.opts.from,from:r.source?.input.from,property:r.prop},s=rewrite(t,r.value,o);s!==r.value&&(r.value=s)}function rewriteAtRule(r,e,t){if(!u.test(r.params))return;const o={type:"at-rule-prelude",rootFrom:e.opts.from,from:r.source?.input.from,atRuleName:r.name},s=rewrite(t,r.params,o);s!==r.params&&(r.params=s)}function rewrite(u,f,m){const p=t(r({css:f})),w=o(p,t=>{if(!s(t)||!c.test(t.getName()))return;const o=t.value.filter(r=>!i(r));for(let s=0;sr.toString())},m);if(!1===n)return;const c=l(r({css:[`"${serializeString(n.url)}"`,...n.urlModifiers??a].join(" ")}));return t.value=c,t.name[1]="url(",t.name[4].value="url",t}return}});return n(w)}creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-rewrite-url/src/index.ts b/plugins/postcss-rewrite-url/src/index.ts index 666724a255..11ceed683d 100644 --- a/plugins/postcss-rewrite-url/src/index.ts +++ b/plugins/postcss-rewrite-url/src/index.ts @@ -33,6 +33,15 @@ const creator: PluginCreator = (options?: pluginOptions) => { return { postcssPlugin: 'postcss-rewrite-url', + Once(root, { result }): void { + root.walkDecls((decl) => { + rewriteDeclaration(decl, result, rewriter); + }); + + root.walkAtRules((atRule) => { + rewriteAtRule(atRule, result, rewriter); + }); + }, Declaration(decl, { result }): void { rewriteDeclaration(decl, result, rewriter); }, From dae17bffd8186614eaa6272cdb10e37c4bfd55e3 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Thu, 1 Oct 2026 07:26:08 +0200 Subject: [PATCH 6/6] revert --- .../cascade-layer-name-parser/dist/index.d.ts | 3 +- .../cascade-layer-name-parser/dist/index.mjs | 2 +- .../docs/cascade-layer-name-parser.api.json | 4 +- .../cascade-layer-name-parser.layername.md | 2 - ...scade-layer-name-parser.layername.parts.md | 2 +- .../src/nodes/layer-name.ts | 51 ++++++++++--------- .../postcss-custom-properties/test/_tape.mjs | 3 ++ .../test/cascade-layers-escaped.css | 15 ++++++ .../test/cascade-layers-escaped.expect.css | 16 ++++++ 9 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 plugins/postcss-custom-properties/test/cascade-layers-escaped.css create mode 100644 plugins/postcss-custom-properties/test/cascade-layers-escaped.expect.css diff --git a/packages/cascade-layer-name-parser/dist/index.d.ts b/packages/cascade-layer-name-parser/dist/index.d.ts index 2cfb712bf7..9b4e85c6fc 100644 --- a/packages/cascade-layer-name-parser/dist/index.d.ts +++ b/packages/cascade-layer-name-parser/dist/index.d.ts @@ -4,10 +4,9 @@ import { ParseError } from '@csstools/css-tokenizer'; export declare function addLayerToModel(layers: Array, currentLayerNames: Array): void; export declare class LayerName { - #private; + parts: Array; constructor(parts: Array); tokens(): Array; - get parts(): Array; slice(start: number, end: number): LayerName; concat(other: LayerName): LayerName; segments(): Array; diff --git a/packages/cascade-layer-name-parser/dist/index.mjs b/packages/cascade-layer-name-parser/dist/index.mjs index cb211094a3..2782302d35 100644 --- a/packages/cascade-layer-name-parser/dist/index.mjs +++ b/packages/cascade-layer-name-parser/dist/index.mjs @@ -1 +1 @@ -import{isTokenIdent as e,TokenType as t,isTokenDelim as r,tokenize as a,ParseError as n,isTokenWhiteSpaceOrComment as s,isTokenWhitespace as o,isTokenComment as i}from"@csstools/css-tokenizer";import{parseCommaSeparatedListOfComponentValues as l,isTokenNode as c,isCommentNode as m,isWhitespaceNode as p}from"@csstools/css-parser-algorithms";class LayerName{#e;#t="";constructor(e){this.#e=e}tokens(){return[...this.#e]}get parts(){return this.#e.slice()}slice(t,r){const a=[];for(let t=0;te(t)||r(t)),n,...a.#e.filter(t=>e(t)||r(t))])}segments(){return this.#e.filter(t=>e(t)).map(e=>e[4].value)}name(){return this.#t||(this.#t=this.#e.filter(t=>e(t)||r(t)).map(e=>e[1]).join("")),this.#t}equal(e){return this.name()===e.name()}toString(){return this.#e.map(e=>e[1]).join("")}toJSON(){return{parts:this.#e,segments:this.segments(),name:this.name()}}}function addLayerToModel(e,t){const r=e.map(e=>e.segments());t.forEach(t=>{const a=t.segments();for(let n=0;n=i&&(o=t,i=a)}if(l)continue;if(e.length>=4096)throw new Error("Too many cascade layers, reduce the complexity of your stylesheet.");const c=t.slice(0,n+1);-1===o?(e.push(c),r.push(s)):(e.splice(o+1,0,c),r.splice(o+1,0,s))}})}function parseFromTokens(t,a){const u=l(t,{onParseError:a?.onParseError}),h=a?.onParseError??(()=>{}),d=["6.4.2. Layer Naming and Nesting","Layer name syntax"," = [ '.' ]*"],f=t[0][2],y=t[t.length-1][3],g=[];for(let t=0;te.tokens());let w=!1,v=!1,L=null;for(let t=0;te(t)||r(t)),a,...n.parts.filter(t=>e(t)||r(t))])}segments(){return this.parts.filter(t=>e(t)).map(e=>e[4].value)}name(){return this.parts.filter(t=>e(t)||r(t)).map(e=>e[1]).join("")}equal(e){const t=this.segments(),r=e.segments();if(t.length!==r.length)return!1;for(let e=0;ee[1]).join("")}toJSON(){return{parts:this.parts,segments:this.segments(),name:this.name()}}}function addLayerToModel(e,t){const r=e.map(e=>e.segments());t.forEach(t=>{const n=t.segments();for(let a=0;a=i&&(o=t,i=n)}if(l)continue;if(e.length>=4096)throw new Error("Too many cascade layers, reduce the complexity of your stylesheet.");const c=t.slice(0,a+1);-1===o?(e.push(c),r.push(s)):(e.splice(o+1,0,c),r.splice(o+1,0,s))}})}function parseFromTokens(t,n){const u=l(t,{onParseError:n?.onParseError}),h=n?.onParseError??(()=>{}),f=["6.4.2. Layer Naming and Nesting","Layer name syntax"," = [ '.' ]*"],d=t[0][2],y=t[t.length-1][3],g=[];for(let t=0;te.tokens());let w=!1,v=!1,L=null;for(let t=0;t -`readonly` - diff --git a/packages/cascade-layer-name-parser/docs/cascade-layer-name-parser.layername.parts.md b/packages/cascade-layer-name-parser/docs/cascade-layer-name-parser.layername.parts.md index 373cdfd58c..63ca85a6b3 100644 --- a/packages/cascade-layer-name-parser/docs/cascade-layer-name-parser.layername.parts.md +++ b/packages/cascade-layer-name-parser/docs/cascade-layer-name-parser.layername.parts.md @@ -7,5 +7,5 @@ **Signature:** ```typescript -get parts(): Array; +parts: Array; ``` diff --git a/packages/cascade-layer-name-parser/src/nodes/layer-name.ts b/packages/cascade-layer-name-parser/src/nodes/layer-name.ts index f32d1403cb..46719afdaf 100644 --- a/packages/cascade-layer-name-parser/src/nodes/layer-name.ts +++ b/packages/cascade-layer-name-parser/src/nodes/layer-name.ts @@ -4,33 +4,28 @@ import type { CSSToken, TokenIdent} from '@csstools/css-tokenizer'; import { TokenType } from '@csstools/css-tokenizer'; export class LayerName { - #parts: Array; - #name: string = ''; + parts: Array; constructor(parts: Array) { - this.#parts = parts; + this.parts = parts; } tokens(): Array { return [ - ...this.#parts, + ...this.parts, ]; } - get parts(): Array { - return this.#parts.slice(); - } - slice(start: number, end: number): LayerName { const indices = []; - for (let i = 0; i < this.#parts.length; i++) { - if (isTokenIdent(this.#parts[i])) { + for (let i = 0; i < this.parts.length; i++) { + if (isTokenIdent(this.parts[i])) { indices.push(i); } } const slice = indices.slice(start, end); - return new LayerName(this.#parts.slice(slice[0], slice[slice.length-1]+1)); + return new LayerName(this.parts.slice(slice[0], slice[slice.length-1]+1)); } concat(other: LayerName): LayerName { @@ -43,18 +38,18 @@ export class LayerName { ]; return new LayerName([ - ...this.#parts.filter((x) => { + ...this.parts.filter((x) => { return isTokenIdent(x) || isTokenDelim(x); }), dot, - ...other.#parts.filter((x) => { + ...other.parts.filter((x) => { return isTokenIdent(x) || isTokenDelim(x); }), ]); } segments(): Array { - return this.#parts.filter((x): x is TokenIdent => { + return this.parts.filter((x): x is TokenIdent => { return isTokenIdent(x); }).map((x) => { return x[4].value; @@ -62,25 +57,33 @@ export class LayerName { } name(): string { - if (this.#name) { - return this.#name; - } - - this.#name = this.#parts.filter((x): x is TokenIdent | TokenDelim => { + return this.parts.filter((x): x is TokenIdent | TokenDelim => { return isTokenIdent(x) || isTokenDelim(x); }).map((x) => { return x[1]; }).join(''); - - return this.#name; } equal(other: LayerName): boolean { - return this.name() === other.name(); + const a = this.segments(); + const b = other.segments(); + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; i++) { + const aa = a[i]; + const bb = b[i]; + if (aa !== bb) { + return false; + } + } + + return true; } toString(): string { - return this.#parts.map((x) => x[1]).join(''); + return this.parts.map((x) => x[1]).join(''); } /** @@ -90,7 +93,7 @@ export class LayerName { */ toJSON(): Record{ return { - parts: this.#parts, + parts: this.parts, segments: this.segments(), name: this.name(), }; diff --git a/plugins/postcss-custom-properties/test/_tape.mjs b/plugins/postcss-custom-properties/test/_tape.mjs index 7f4636aba1..19ee5af476 100644 --- a/plugins/postcss-custom-properties/test/_tape.mjs +++ b/plugins/postcss-custom-properties/test/_tape.mjs @@ -18,6 +18,9 @@ postcssTape(plugin)({ 'cascade-layers-nested': { message: 'qualifies nested cascade layer names exactly once', }, + 'cascade-layers-escaped': { + message: 'treats escaped layer names as their unescaped equivalent', + }, 'cyclic-on-different-element-1': { message: 'supports cyclic variables on different elements', }, diff --git a/plugins/postcss-custom-properties/test/cascade-layers-escaped.css b/plugins/postcss-custom-properties/test/cascade-layers-escaped.css new file mode 100644 index 0000000000..1ed28c163e --- /dev/null +++ b/plugins/postcss-custom-properties/test/cascade-layers-escaped.css @@ -0,0 +1,15 @@ +@layer foo { + :root { + --x: red; + } +} + +@layer \66 oo { + :root { + --x: blue; + } +} + +a { + color: var(--x); +} diff --git a/plugins/postcss-custom-properties/test/cascade-layers-escaped.expect.css b/plugins/postcss-custom-properties/test/cascade-layers-escaped.expect.css new file mode 100644 index 0000000000..e96cd1788f --- /dev/null +++ b/plugins/postcss-custom-properties/test/cascade-layers-escaped.expect.css @@ -0,0 +1,16 @@ +@layer foo { + :root { + --x: red; + } +} + +@layer \66 oo { + :root { + --x: blue; + } +} + +a { + color: blue; + color: var(--x); +}