diff --git a/plugin-packs/postcss-preset-env/test/basic.autoprefixer.expect.css b/plugin-packs/postcss-preset-env/test/basic.autoprefixer.expect.css index 3cf65bdb2..337c472e4 100644 --- a/plugin-packs/postcss-preset-env/test/basic.autoprefixer.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.autoprefixer.expect.css @@ -673,7 +673,7 @@ .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) rgb(141, 0, 0); - color: var(--csstools-color-scheme--dark) rgb(141, 0, 0); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(rgb(141, 0, 0), magenta); } diff --git a/plugin-packs/postcss-preset-env/test/basic.autoprefixer.false.expect.css b/plugin-packs/postcss-preset-env/test/basic.autoprefixer.false.expect.css index c92fb8129..8f9a3dfe2 100644 --- a/plugin-packs/postcss-preset-env/test/basic.autoprefixer.false.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.autoprefixer.false.expect.css @@ -678,7 +678,7 @@ .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) rgb(141, 0, 0); - color: var(--csstools-color-scheme--dark) rgb(141, 0, 0); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(rgb(141, 0, 0), magenta); } diff --git a/plugin-packs/postcss-preset-env/test/basic.autoprefixer.remove.false.expect.css b/plugin-packs/postcss-preset-env/test/basic.autoprefixer.remove.false.expect.css index e22f7c866..fb66bb13e 100644 --- a/plugin-packs/postcss-preset-env/test/basic.autoprefixer.remove.false.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.autoprefixer.remove.false.expect.css @@ -697,7 +697,7 @@ .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) rgb(141, 0, 0); - color: var(--csstools-color-scheme--dark) rgb(141, 0, 0); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(rgb(141, 0, 0), magenta); } diff --git a/plugin-packs/postcss-preset-env/test/basic.ch38.expect.css b/plugin-packs/postcss-preset-env/test/basic.ch38.expect.css index cfa872731..7e6edfedd 100644 --- a/plugin-packs/postcss-preset-env/test/basic.ch38.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.ch38.expect.css @@ -585,7 +585,7 @@ .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) rgb(141, 0, 0); - color: var(--csstools-color-scheme--dark) rgb(141, 0, 0); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(rgb(141, 0, 0), magenta); } diff --git a/plugin-packs/postcss-preset-env/test/basic.expect.css b/plugin-packs/postcss-preset-env/test/basic.expect.css index 340804b8e..e839ed785 100644 --- a/plugin-packs/postcss-preset-env/test/basic.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.expect.css @@ -692,7 +692,7 @@ .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) rgb(141, 0, 0); - color: var(--csstools-color-scheme--dark) rgb(141, 0, 0); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(rgb(141, 0, 0), magenta); } diff --git a/plugin-packs/postcss-preset-env/test/basic.hebrew.expect.css b/plugin-packs/postcss-preset-env/test/basic.hebrew.expect.css index 35c923f9d..bab0df223 100644 --- a/plugin-packs/postcss-preset-env/test/basic.hebrew.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.hebrew.expect.css @@ -688,7 +688,7 @@ h1.test-custom-selectors:not(.does-not-exist), h2.test-custom-selectors:not(.doe .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) rgb(141, 0, 0); - color: var(--csstools-color-scheme--dark) rgb(141, 0, 0); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(rgb(141, 0, 0), magenta); } diff --git a/plugin-packs/postcss-preset-env/test/basic.ie10.expect.css b/plugin-packs/postcss-preset-env/test/basic.ie10.expect.css index 426d93bab..e911a6237 100644 --- a/plugin-packs/postcss-preset-env/test/basic.ie10.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.ie10.expect.css @@ -709,7 +709,7 @@ .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) rgb(141, 0, 0); - color: var(--csstools-color-scheme--dark) rgb(141, 0, 0); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(rgb(141, 0, 0), magenta); } diff --git a/plugin-packs/postcss-preset-env/test/basic.nesting.false.expect.css b/plugin-packs/postcss-preset-env/test/basic.nesting.false.expect.css index cb0e0da02..2b95a78ef 100644 --- a/plugin-packs/postcss-preset-env/test/basic.nesting.false.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.nesting.false.expect.css @@ -690,7 +690,7 @@ h1.test-custom-selectors:not(.does-not-exist), h2.test-custom-selectors:not(.doe .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) rgb(141, 0, 0); - color: var(--csstools-color-scheme--dark) rgb(141, 0, 0); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(rgb(141, 0, 0), magenta); } diff --git a/plugin-packs/postcss-preset-env/test/basic.op_mini.expect.css b/plugin-packs/postcss-preset-env/test/basic.op_mini.expect.css index 8554da0d6..6f52edfaf 100644 --- a/plugin-packs/postcss-preset-env/test/basic.op_mini.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.op_mini.expect.css @@ -670,7 +670,7 @@ h1.test-custom-selectors:not(.does-not-exist), h2.test-custom-selectors:not(.doe .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) rgb(141, 0, 0); - color: var(--csstools-color-scheme--dark) rgb(141, 0, 0); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(rgb(141, 0, 0), magenta); } diff --git a/plugin-packs/postcss-preset-env/test/basic.preserve.true.expect.css b/plugin-packs/postcss-preset-env/test/basic.preserve.true.expect.css index c0cf769e0..bf63b82e9 100644 --- a/plugin-packs/postcss-preset-env/test/basic.preserve.true.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.preserve.true.expect.css @@ -1133,7 +1133,7 @@ h1.test-custom-selectors:not(.does-not-exist), h2.test-custom-selectors:not(.doe .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) rgb(141, 0, 0); - color: var(--csstools-color-scheme--dark) rgb(141, 0, 0); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(rgb(141, 0, 0), magenta); } diff --git a/plugin-packs/postcss-preset-env/test/basic.stage0.expect.css b/plugin-packs/postcss-preset-env/test/basic.stage0.expect.css index 559040870..4e9c66a9f 100644 --- a/plugin-packs/postcss-preset-env/test/basic.stage0.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.stage0.expect.css @@ -688,7 +688,7 @@ h1.test-custom-selectors:not(.does-not-exist), h2.test-custom-selectors:not(.doe .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) rgb(141, 0, 0); - color: var(--csstools-color-scheme--dark) rgb(141, 0, 0); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(rgb(141, 0, 0), magenta); } diff --git a/plugin-packs/postcss-preset-env/test/basic.vendors-1.expect.css b/plugin-packs/postcss-preset-env/test/basic.vendors-1.expect.css index 62009b4b7..6c20b825c 100644 --- a/plugin-packs/postcss-preset-env/test/basic.vendors-1.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.vendors-1.expect.css @@ -670,7 +670,7 @@ .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) lch(from rgb(141, 0, 0) l c h); - color: var(--csstools-color-scheme--dark) lch(from rgb(141, 0, 0) l c h); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(lch(from rgb(141, 0, 0) l c h), magenta); color: light-dark(lch(from color(display-p3 0.50566 0.0781 0) l c h), magenta); diff --git a/plugin-packs/postcss-preset-env/test/basic.vendors-2.expect.css b/plugin-packs/postcss-preset-env/test/basic.vendors-2.expect.css index 62009b4b7..6c20b825c 100644 --- a/plugin-packs/postcss-preset-env/test/basic.vendors-2.expect.css +++ b/plugin-packs/postcss-preset-env/test/basic.vendors-2.expect.css @@ -670,7 +670,7 @@ .schemed-colors { --csstools-light-dark-toggle--1: var(--csstools-color-scheme--dark) lch(from rgb(141, 0, 0) l c h); - color: var(--csstools-color-scheme--dark) lch(from rgb(141, 0, 0) l c h); + color: magenta; color: var(--csstools-light-dark-toggle--1, magenta); color: light-dark(lch(from rgb(141, 0, 0) l c h), magenta); color: light-dark(lch(from color(display-p3 0.50566 0.0781 0) l c h), magenta); diff --git a/plugins/postcss-custom-properties/CHANGELOG.md b/plugins/postcss-custom-properties/CHANGELOG.md index 6031b1b39..48067c186 100644 --- a/plugins/postcss-custom-properties/CHANGELOG.md +++ b/plugins/postcss-custom-properties/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to PostCSS Custom Properties +### Unreleased (patch) + +- Improve handling of cross references between computed values of custom properties + ### 13.3.6 _March 13, 2024_ diff --git a/plugins/postcss-custom-properties/dist/index.cjs b/plugins/postcss-custom-properties/dist/index.cjs index 9fa18ce50..9b5f697ea 100644 --- a/plugins/postcss-custom-properties/dist/index.cjs +++ b/plugins/postcss-custom-properties/dist/index.cjs @@ -1 +1 @@ -"use strict";var e=require("postcss-value-parser"),t=require("@csstools/cascade-layer-name-parser"),r=require("@csstools/utilities");const n=t.parse("csstools-implicit-layer")[0];function collectCascadeLayerOrder(e){const r=new Map,s=new Map,o=[];e.walkAtRules((e=>{if("layer"!==e.name.toLowerCase())return;{let t=e.parent;for(;t;){if("atrule"!==t.type||"layer"!==t.name.toLowerCase()){if(t===e.root())break;return}t=t.parent}}let a;if(e.nodes)a=normalizeLayerName(e.params,1);else{if(!e.params.trim())return;a=e.params}let i=t.parse(a);if(i?.length){{let t=e.parent;for(;t&&"atrule"===t.type&&"layer"===t.name.toLowerCase();){const e=s.get(t);e?(i=i.map((t=>e.concat(t))),t=t.parent):t=t.parent}}if(t.addLayerToModel(o,i),e.nodes){const t=i[0].concat(n);r.set(e,t),s.set(e,i[0])}}}));for(const e of r.values())t.addLayerToModel(o,[e]);const a=new WeakMap;for(const[e,t]of r)a.set(e,o.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const s=/(!\s*)?postcss-custom-properties:\s*off\b/i,o=new WeakMap;function isBlockIgnored(e){if(!e||!e.nodes)return!1;if(o.has(e))return o.get(e);const t=e.some((e=>isIgnoreComment(e,s)));return o.set(e,t),t}const a=/(!\s*)?postcss-custom-properties:\s*ignore\s+next\b/i;function isDeclarationIgnored(e){return!!e&&(!!isBlockIgnored(e.parent)||isIgnoreComment(e.prev(),a))}function isIgnoreComment(e,t){return!!e&&"comment"===e.type&&t.test(e.text)}const i=new Set(["layer"]);function isProcessableRule(e){let t=e.parent;for(;t;){if("atrule"===t.type&&!i.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const l=/^html$/i,c=/^:where\(html\)$/i,u=/^:root$/i,p=/^:where\(:root\)$/i,f=/(html|:root)/i,d=/^var$/i;function isVarFunction(e){return"function"===e.type&&d.test(e.value)&&Object(e.nodes).length>0}const w=/\bvar\(/i;function removeCyclicReferences(e,t){const r=new Set;for(;e.size>0;){const n=findCyclicNode(Array.from(e.keys()),t);if(!n)return r;e.delete(n),r.add(n),t=t.filter((e=>-1===e.indexOf(n)))}return r}function findCyclicNode(e,t){let r=e.length;const n=new Array(r),s={};let o=r;const a=makeOutgoingEdges(t),i=makeNodesHash(e);for(;o--;)if(!s[o]){const t=visit(e[o],o,new Set);if(!t)continue;return t}function visit(e,t,o){if(o.has(e))return e;if(!i.has(e))return;if(s[t])return;s[t]=!0;const l=Array.from(a.get(e)||new Set);if(t=l.length){o.add(e);do{const e=l[--t],r=visit(e,i.get(e),o);if(r)return r}while(t);o.delete(e)}n[--r]=e}}function makeOutgoingEdges(e){const t=new Map;for(let r=0,n=e.length;r{f.test(e.selector)&&e.nodes?.length&&isProcessableRule(e)&&(isBlockIgnored(e)||e.selectors.forEach((t=>{let r=-1;if(c.test(t)||p.test(t))r=0;else if(l.test(t))r=1;else{if(!u.test(t))return;r=2}const a=(f=o,((i=e).parent&&"atrule"===i.parent.type&&"layer"===i.parent.name.toLowerCase()?f.has(i.parent)?f.get(i.parent)+1:0:1e7)+10+r);var i,f;e.each((e=>{if("decl"!==e.type)return;if(!e.variable||isDeclarationIgnored(e))return;if("initial"===e.value.toLowerCase().trim())return;const t=s.get(e.prop)??-1;a>=t&&(s.set(e.prop,a),n.set(e.prop,e.value))}))})))}));const a=[],i=new Map;for(const[t,s]of n.entries()){const n=parseOrCached(s,r);e.walk(n.nodes,(e=>{if(isVarFunction(e)){const[r]=e.nodes.filter((e=>"word"===e.type));a.push([r.value,t])}})),i.set(t,n)}return removeCyclicReferences(i,a),i}function transformValueAST(t,r,n){if(!t.nodes?.length)return"";const s=new Map;return t.nodes.forEach((e=>{s.set(e,t)})),e.walk(t.nodes,(e=>{"nodes"in e&&e.nodes.length&&e.nodes.forEach((t=>{s.set(t,e)}))})),e.walk(t.nodes,(t=>{if(!isVarFunction(t))return;const[o,...a]=t.nodes.filter((e=>"div"!==e.type)),{value:i}=o,l=s.get(t);if(!l)return;const c=l.nodes.indexOf(t);if(-1===c)return;let u=!1;a&&e.walk(a,(e=>{if(isVarFunction(e)){const[t]=e.nodes.filter((e=>"word"===e.type));if(r.has(t.value)||n.has(t.value))return;return u=!0,!1}}));let p=n.get(i)?.nodes??r.get(i)?.nodes;p||!a.length||u||(p=t.nodes.slice(t.nodes.indexOf(a[0]))),void 0!==p&&(p.length?(l.nodes.splice(c,1,...p),l.nodes.forEach((e=>s.set(e,l)))):(l.nodes.splice(c,1,{type:"comment",value:"",sourceIndex:t.sourceIndex,sourceEndIndex:t.sourceEndIndex}),l.nodes.forEach((e=>s.set(e,l)))))}),!0),e.stringify(t.nodes)}function transformProperties(t,r,n,s,o){if(isTransformableDecl(t)&&!isDeclarationIgnored(t)){const s=t.value;let a=transformValueAST(e(s),r,n);const i=new Set;for(;w.test(a)&&!i.has(a);){i.add(a);a=transformValueAST(e(a),r,n)}if(a!==s){if(parentHasExactFallback(t,a))return void(o.preserve||t.remove());if(o.preserve){const e=t.cloneBefore({value:a});hasTrailingComment(e)&&e.raws?.value&&(e.raws.value.value=e.value.replace(v,"$1"),e.raws.value.raw=e.raws.value.value+e.raws.value.raw.replace(v,"$2"))}else t.value=a,hasTrailingComment(t)&&t.raws?.value&&(t.raws.value.value=t.value.replace(v,"$1"),t.raws.value.raw=t.raws.value.value+t.raws.value.raw.replace(v,"$2"))}}}const isTransformableDecl=e=>!e.variable&&e.value.includes("--")&&e.value.toLowerCase().includes("var("),hasTrailingComment=e=>"value"in Object(Object(e.raws).value)&&"raw"in(e.raws?.value??{})&&v.test(e.raws.value?.raw??""),v=/^([\W\w]+)(\s*\/\*[\W\w]+?\*\/)$/;function parentHasExactFallback(e,t){if(!e||!e.parent)return!1;let r=!1;const n=e.parent.index(e);return e.parent.each(((s,o)=>s!==e&&(!(o>=n)&&void("decl"===s.type&&s.prop.toLowerCase()===e.prop.toLowerCase()&&s.value===t&&(r=!0))))),r}const m=/^initial$/i,g=/(?:\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 Map;return{postcssPlugin:"postcss-custom-properties",Once(t){e=getCustomPropertiesFromRoot(t,n)},Declaration(s){if(!w.test(s.value))return;if(r.hasSupportsAtRuleAncestor(s,g))return;const o=new Map;t&&s.parent&&s.parent.each((e=>{"decl"===e.type&&e.variable&&s!==e&&(isDeclarationIgnored(e)||(m.test(e.value)?o.delete(e.prop):o.set(e.prop,parseOrCached(e.value,n))))})),transformProperties(s,e,o,0,{preserve:t})}}}}};creator.postcss=!0,module.exports=creator; +"use strict";var e=require("@csstools/cascade-layer-name-parser"),t=require("postcss-value-parser"),r=require("@csstools/utilities");const n=e.parse("csstools-implicit-layer")[0];function collectCascadeLayerOrder(t){const r=new Map,o=new Map,s=[];t.walkAtRules((t=>{if("layer"!==t.name.toLowerCase())return;{let e=t.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===t.root())break;return}e=e.parent}}let a;if(t.nodes)a=normalizeLayerName(t.params,1);else{if(!t.params.trim())return;a=t.params}let i=e.parse(a);if(i?.length){{let e=t.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=o.get(e);t?(i=i.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(e.addLayerToModel(s,i),t.nodes){const e=i[0].concat(n);r.set(t,e),o.set(t,i[0])}}}));for(const t of r.values())e.addLayerToModel(s,[t]);const a=new WeakMap;for(const[e,t]of r)a.set(e,s.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const o=/(!\s*)?postcss-custom-properties:\s*off\b/i,s=new WeakMap;function isBlockIgnored(e){if(!e||!e.nodes)return!1;if(s.has(e))return s.get(e);const t=e.some((e=>isIgnoreComment(e,o)));return s.set(e,t),t}const a=/(!\s*)?postcss-custom-properties:\s*ignore\s+next\b/i;function isDeclarationIgnored(e){return!!e&&(!!isBlockIgnored(e.parent)||isIgnoreComment(e.prev(),a))}function isIgnoreComment(e,t){return!!e&&"comment"===e.type&&t.test(e.text)}const i=new Set(["layer"]);function isProcessableRule(e){let t=e.parent;for(;t;){if("atrule"===t.type&&!i.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const l=/^html$/i,c=/^:where\(html\)$/i,u=/^:root$/i,p=/^:where\(:root\)$/i,f=/(html|:root)/i,d=/^var$/i;function isVarFunction(e){return"function"===e.type&&d.test(e.value)&&Object(e.nodes).length>0}const m=/\bvar\(/i;function parseOrCached(e,r){let n=r.get(e);return n||(n=t(e),r.set(e,n),n)}function toposort(e,t){let r=e.length;const n=new Array(r),o={};let s=r;const a=makeOutgoingEdges(t),i=makeNodesHash(e);for(;s--;)o[s]||visit(e[s],s,new Set);return n;function visit(e,t,s){if(s.has(e)){let t;try{t=", node was:"+JSON.stringify(e)}catch(e){t=""}throw new Error("Cyclic dependency"+t)}if(!i.has(e))throw new Error("Found unknown node. Make sure to provided all involved nodes. Unknown node: "+JSON.stringify(e));if(o[t])return;o[t]=!0;const l=Array.from(a.get(e)||new Set);if(t=l.length){s.add(e);do{const e=l[--t];visit(e,i.get(e),s)}while(t);s.delete(e)}n[--r]=e}}function removeCyclicReferences(e,t){const r=new Set;for(;e.size>0;){const n=findCyclicNode(Array.from(e.keys()),t);if(!n)return r;e.delete(n),r.add(n),t=t.filter((e=>-1===e.indexOf(n)))}return r}function findCyclicNode(e,t){let r=e.length;const n=new Array(r),o={};let s=r;const a=makeOutgoingEdges(t),i=makeNodesHash(e);for(;s--;)if(!o[s]){const t=visit(e[s],s,new Set);if(!t)continue;return t}function visit(e,t,s){if(s.has(e))return e;if(!i.has(e))return;if(o[t])return;o[t]=!0;const l=Array.from(a.get(e)||new Set);if(t=l.length){s.add(e);do{const e=l[--t],r=visit(e,i.get(e),s);if(r)return r}while(t);s.delete(e)}n[--r]=e}}function makeOutgoingEdges(e){const t=new Map;for(let r=0,n=e.length;r{if(!isVarFunction(e))return;const s=parseVarFunction(e);if(!s)return;let a=!1;s.fallback&&t.walk(s.fallback,(e=>{if(!isVarFunction(e))return;const t=parseVarFunction(e);return t?t.fallback||r.has(t.name.value)?void 0:(a=!0,!1):void 0}));let i=r.get(s.name.value)?.nodes;i||!s.fallback||a||(i=s.fallback),void 0!==i&&(i.length?o.splice(n,1,...i):o.splice(n,1,{type:"div",value:" ",before:"",after:"",sourceIndex:e.sourceIndex,sourceEndIndex:e.sourceEndIndex}))})),t.stringify(e.nodes)):""}function walk(e,t){let r,n,o;for(r=0,n=e.length;r"comment"!==e.type&&"space"!==e.type));return 1===t.length&&("word"===t[0].type&&v.test(t[0].value))}function buildCustomPropertiesMap(e,r,n){if(!e.size)return r;const o=new Map(r);{const s=[];for(const[a,i]of e.entries()){const l=parseOrCached(i,n);let c=!1;t.walk(l.nodes,(t=>{if(!isVarFunction(t))return;const n=parseVarFunction(t);n&&(n.fallback||e.has(n.name.value)||r.has(n.name.value)?s.push([n.name.value,a]):c=!0)})),c||o.set(a,l)}removeCyclicReferences(o,s)}{const e=[];for(const[r,n]of o.entries())t.walk(n.nodes,(t=>{if(!isVarFunction(t))return;const n=parseVarFunction(t);n&&(n.fallback||o.has(n.name.value)?e.push([n.name.value,r]):o.delete(r))}));const r=toposort(Array.from(o.keys()),e);for(const e of r){const t=o.get(e);if(!t)continue;const r=parseOrCached(transformValueAST(t,o),n);o.set(e,r)}}for(const[e,t]of o.entries())isInitial(t)&&o.delete(e);return o}function getCustomPropertiesFromRoot(e,t){const r=new Map,n=new Map,o=collectCascadeLayerOrder(e);return e.walkRules((e=>{f.test(e.selector)&&e.nodes?.length&&isProcessableRule(e)&&(isBlockIgnored(e)||e.selectors.forEach((t=>{let s=-1;if(c.test(t)||p.test(t))s=0;else if(l.test(t))s=1;else{if(!u.test(t))return;s=2}const a=(f=o,((i=e).parent&&"atrule"===i.parent.type&&"layer"===i.parent.name.toLowerCase()?f.has(i.parent)?f.get(i.parent)+1:0:1e7)+10+s);var i,f;e.each((e=>{if("decl"!==e.type)return;if(!e.variable||isDeclarationIgnored(e))return;if("initial"===e.value.toLowerCase().trim())return;const t=n.get(e.prop)??-1;a>=t&&(n.set(e.prop,a),r.set(e.prop,e.value))}))})))})),buildCustomPropertiesMap(r,new Map,t)}function getCustomPropertiesFromSiblings(e,t,r){if(!e.parent)return t;const n=new Map;return e.parent.each((t=>{"decl"===t.type&&t.variable&&e!==t&&(isDeclarationIgnored(t)||n.set(t.prop,t.value))})),n.size?buildCustomPropertiesMap(n,t,r):t}function transformProperties(e,r,n){if(isTransformableDecl(e)&&!isDeclarationIgnored(e)){const o=e.value,s=transformValueAST(t(o),r);if(s===o)return;if(parentHasExactFallback(e,s))return void(n.preserve||e.remove());if(n.preserve){const t=e.cloneBefore({value:s});hasTrailingComment(t)&&t.raws?.value&&(t.raws.value.value=t.value.replace(w,"$1"),t.raws.value.raw=t.raws.value.value+t.raws.value.raw.replace(w,"$2"))}else e.value=s,hasTrailingComment(e)&&e.raws?.value&&(e.raws.value.value=e.value.replace(w,"$1"),e.raws.value.raw=e.raws.value.value+e.raws.value.raw.replace(w,"$2"))}}const isTransformableDecl=e=>!e.variable&&e.value.includes("--")&&e.value.toLowerCase().includes("var("),hasTrailingComment=e=>"value"in Object(Object(e.raws).value)&&"raw"in(e.raws?.value??{})&&w.test(e.raws.value?.raw??""),w=/^([\W\w]+)(\s*\/\*[\W\w]+?\*\/)$/;function parentHasExactFallback(e,t){if(!e||!e.parent)return!1;let r=!1;const n=e.parent.index(e);return e.parent.each(((o,s)=>o!==e&&(!(s>=n)&&void("decl"===o.type&&o.prop.toLowerCase()===e.prop.toLowerCase()&&o.value===t&&(r=!0))))),r}const g=/(?:\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;return{postcssPlugin:"postcss-custom-properties",Once(t){e=getCustomPropertiesFromRoot(t,o)},Declaration(s){if(!m.test(s.value))return;if(r.hasSupportsAtRuleAncestor(s,g))return;let a=e;t&&s.parent&&(a=n.get(s.parent)??getCustomPropertiesFromSiblings(s,e,o),n.set(s.parent,a)),transformProperties(s,a,{preserve:t})}}}}};creator.postcss=!0,module.exports=creator; diff --git a/plugins/postcss-custom-properties/dist/index.mjs b/plugins/postcss-custom-properties/dist/index.mjs index 05c20c650..0959f748e 100644 --- a/plugins/postcss-custom-properties/dist/index.mjs +++ b/plugins/postcss-custom-properties/dist/index.mjs @@ -1 +1 @@ -import e from"postcss-value-parser";import{parse as t,addLayerToModel as r}from"@csstools/cascade-layer-name-parser";import{hasSupportsAtRuleAncestor as n}from"@csstools/utilities";const o=t("csstools-implicit-layer")[0];function collectCascadeLayerOrder(e){const n=new Map,s=new Map,a=[];e.walkAtRules((e=>{if("layer"!==e.name.toLowerCase())return;{let t=e.parent;for(;t;){if("atrule"!==t.type||"layer"!==t.name.toLowerCase()){if(t===e.root())break;return}t=t.parent}}let i;if(e.nodes)i=normalizeLayerName(e.params,1);else{if(!e.params.trim())return;i=e.params}let l=t(i);if(l?.length){{let t=e.parent;for(;t&&"atrule"===t.type&&"layer"===t.name.toLowerCase();){const e=s.get(t);e?(l=l.map((t=>e.concat(t))),t=t.parent):t=t.parent}}if(r(a,l),e.nodes){const t=l[0].concat(o);n.set(e,t),s.set(e,l[0])}}}));for(const e of n.values())r(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 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 l=new Set(["layer"]);function isProcessableRule(e){let t=e.parent;for(;t;){if("atrule"===t.type&&!l.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const c=/^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)&&Object(e.nodes).length>0}const w=/\bvar\(/i;function removeCyclicReferences(e,t){const r=new Set;for(;e.size>0;){const n=findCyclicNode(Array.from(e.keys()),t);if(!n)return r;e.delete(n),r.add(n),t=t.filter((e=>-1===e.indexOf(n)))}return r}function findCyclicNode(e,t){let r=e.length;const n=new Array(r),o={};let s=r;const a=makeOutgoingEdges(t),i=makeNodesHash(e);for(;s--;)if(!o[s]){const t=visit(e[s],s,new Set);if(!t)continue;return t}function visit(e,t,s){if(s.has(e))return e;if(!i.has(e))return;if(o[t])return;o[t]=!0;const l=Array.from(a.get(e)||new Set);if(t=l.length){s.add(e);do{const e=l[--t],r=visit(e,i.get(e),s);if(r)return r}while(t);s.delete(e)}n[--r]=e}}function makeOutgoingEdges(e){const t=new Map;for(let r=0,n=e.length;r{d.test(e.selector)&&e.nodes?.length&&isProcessableRule(e)&&(isBlockIgnored(e)||e.selectors.forEach((t=>{let r=-1;if(u.test(t)||f.test(t))r=0;else if(c.test(t))r=1;else{if(!p.test(t))return;r=2}const a=(l=s,((i=e).parent&&"atrule"===i.parent.type&&"layer"===i.parent.name.toLowerCase()?l.has(i.parent)?l.get(i.parent)+1:0:1e7)+10+r);var i,l;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))}))})))}));const a=[],i=new Map;for(const[t,o]of n.entries()){const n=parseOrCached(o,r);e.walk(n.nodes,(e=>{if(isVarFunction(e)){const[r]=e.nodes.filter((e=>"word"===e.type));a.push([r.value,t])}})),i.set(t,n)}return removeCyclicReferences(i,a),i}function transformValueAST(t,r,n){if(!t.nodes?.length)return"";const o=new Map;return t.nodes.forEach((e=>{o.set(e,t)})),e.walk(t.nodes,(e=>{"nodes"in e&&e.nodes.length&&e.nodes.forEach((t=>{o.set(t,e)}))})),e.walk(t.nodes,(t=>{if(!isVarFunction(t))return;const[s,...a]=t.nodes.filter((e=>"div"!==e.type)),{value:i}=s,l=o.get(t);if(!l)return;const c=l.nodes.indexOf(t);if(-1===c)return;let u=!1;a&&e.walk(a,(e=>{if(isVarFunction(e)){const[t]=e.nodes.filter((e=>"word"===e.type));if(r.has(t.value)||n.has(t.value))return;return u=!0,!1}}));let p=n.get(i)?.nodes??r.get(i)?.nodes;p||!a.length||u||(p=t.nodes.slice(t.nodes.indexOf(a[0]))),void 0!==p&&(p.length?(l.nodes.splice(c,1,...p),l.nodes.forEach((e=>o.set(e,l)))):(l.nodes.splice(c,1,{type:"comment",value:"",sourceIndex:t.sourceIndex,sourceEndIndex:t.sourceEndIndex}),l.nodes.forEach((e=>o.set(e,l)))))}),!0),e.stringify(t.nodes)}function transformProperties(t,r,n,o,s){if(isTransformableDecl(t)&&!isDeclarationIgnored(t)){const o=t.value;let a=transformValueAST(e(o),r,n);const i=new Set;for(;w.test(a)&&!i.has(a);){i.add(a);a=transformValueAST(e(a),r,n)}if(a!==o){if(parentHasExactFallback(t,a))return void(s.preserve||t.remove());if(s.preserve){const e=t.cloneBefore({value:a});hasTrailingComment(e)&&e.raws?.value&&(e.raws.value.value=e.value.replace(v,"$1"),e.raws.value.raw=e.raws.value.value+e.raws.value.raw.replace(v,"$2"))}else t.value=a,hasTrailingComment(t)&&t.raws?.value&&(t.raws.value.value=t.value.replace(v,"$1"),t.raws.value.raw=t.raws.value.value+t.raws.value.raw.replace(v,"$2"))}}}const isTransformableDecl=e=>!e.variable&&e.value.includes("--")&&e.value.toLowerCase().includes("var("),hasTrailingComment=e=>"value"in Object(Object(e.raws).value)&&"raw"in(e.raws?.value??{})&&v.test(e.raws.value?.raw??""),v=/^([\W\w]+)(\s*\/\*[\W\w]+?\*\/)$/;function parentHasExactFallback(e,t){if(!e||!e.parent)return!1;let r=!1;const n=e.parent.index(e);return e.parent.each(((o,s)=>o!==e&&(!(s>=n)&&void("decl"===o.type&&o.prop.toLowerCase()===e.prop.toLowerCase()&&o.value===t&&(r=!0))))),r}const g=/^initial$/i,h=/(?:\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 r=new Map;return{postcssPlugin:"postcss-custom-properties",Once(t){e=getCustomPropertiesFromRoot(t,r)},Declaration(o){if(!w.test(o.value))return;if(n(o,h))return;const s=new Map;t&&o.parent&&o.parent.each((e=>{"decl"===e.type&&e.variable&&o!==e&&(isDeclarationIgnored(e)||(g.test(e.value)?s.delete(e.prop):s.set(e.prop,parseOrCached(e.value,r))))})),transformProperties(o,e,s,0,{preserve:t})}}}}};creator.postcss=!0;export{creator as default}; +import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import r from"postcss-value-parser";import{hasSupportsAtRuleAncestor as n}from"@csstools/utilities";const o=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,s=new Map,a=[];r.walkAtRules((r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}e=e.parent}}let i;if(r.nodes)i=normalizeLayerName(r.params,1);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=s.get(e);t?(l=l.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(t(a,l),r.nodes){const e=l[0].concat(o);n.set(r,e),s.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 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 l=new Set(["layer"]);function isProcessableRule(e){let t=e.parent;for(;t;){if("atrule"===t.type&&!l.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const c=/^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)&&Object(e.nodes).length>0}const v=/\bvar\(/i;function parseOrCached(e,t){let n=t.get(e);return n||(n=r(e),t.set(e,n),n)}function toposort(e,t){let r=e.length;const n=new Array(r),o={};let s=r;const a=makeOutgoingEdges(t),i=makeNodesHash(e);for(;s--;)o[s]||visit(e[s],s,new Set);return n;function visit(e,t,s){if(s.has(e)){let t;try{t=", node was:"+JSON.stringify(e)}catch(e){t=""}throw new Error("Cyclic dependency"+t)}if(!i.has(e))throw new Error("Found unknown node. Make sure to provided all involved nodes. Unknown node: "+JSON.stringify(e));if(o[t])return;o[t]=!0;const l=Array.from(a.get(e)||new Set);if(t=l.length){s.add(e);do{const e=l[--t];visit(e,i.get(e),s)}while(t);s.delete(e)}n[--r]=e}}function removeCyclicReferences(e,t){const r=new Set;for(;e.size>0;){const n=findCyclicNode(Array.from(e.keys()),t);if(!n)return r;e.delete(n),r.add(n),t=t.filter((e=>-1===e.indexOf(n)))}return r}function findCyclicNode(e,t){let r=e.length;const n=new Array(r),o={};let s=r;const a=makeOutgoingEdges(t),i=makeNodesHash(e);for(;s--;)if(!o[s]){const t=visit(e[s],s,new Set);if(!t)continue;return t}function visit(e,t,s){if(s.has(e))return e;if(!i.has(e))return;if(o[t])return;o[t]=!0;const l=Array.from(a.get(e)||new Set);if(t=l.length){s.add(e);do{const e=l[--t],r=visit(e,i.get(e),s);if(r)return r}while(t);s.delete(e)}n[--r]=e}}function makeOutgoingEdges(e){const t=new Map;for(let r=0,n=e.length;r{if(!isVarFunction(e))return;const s=parseVarFunction(e);if(!s)return;let a=!1;s.fallback&&r.walk(s.fallback,(e=>{if(!isVarFunction(e))return;const r=parseVarFunction(e);return r?r.fallback||t.has(r.name.value)?void 0:(a=!0,!1):void 0}));let i=t.get(s.name.value)?.nodes;i||!s.fallback||a||(i=s.fallback),void 0!==i&&(i.length?o.splice(n,1,...i):o.splice(n,1,{type:"div",value:" ",before:"",after:"",sourceIndex:e.sourceIndex,sourceEndIndex:e.sourceEndIndex}))})),r.stringify(e.nodes)):""}function walk(e,t){let r,n,o;for(r=0,n=e.length;r"comment"!==e.type&&"space"!==e.type));return 1===t.length&&("word"===t[0].type&&w.test(t[0].value))}function buildCustomPropertiesMap(e,t,n){if(!e.size)return t;const o=new Map(t);{const s=[];for(const[a,i]of e.entries()){const l=parseOrCached(i,n);let c=!1;r.walk(l.nodes,(r=>{if(!isVarFunction(r))return;const n=parseVarFunction(r);n&&(n.fallback||e.has(n.name.value)||t.has(n.name.value)?s.push([n.name.value,a]):c=!0)})),c||o.set(a,l)}removeCyclicReferences(o,s)}{const e=[];for(const[t,n]of o.entries())r.walk(n.nodes,(r=>{if(!isVarFunction(r))return;const n=parseVarFunction(r);n&&(n.fallback||o.has(n.name.value)?e.push([n.name.value,t]):o.delete(t))}));const t=toposort(Array.from(o.keys()),e);for(const e of t){const t=o.get(e);if(!t)continue;const r=parseOrCached(transformValueAST(t,o),n);o.set(e,r)}}for(const[e,t]of o.entries())isInitial(t)&&o.delete(e);return o}function getCustomPropertiesFromRoot(e,t){const r=new Map,n=new Map,o=collectCascadeLayerOrder(e);return e.walkRules((e=>{d.test(e.selector)&&e.nodes?.length&&isProcessableRule(e)&&(isBlockIgnored(e)||e.selectors.forEach((t=>{let s=-1;if(u.test(t)||f.test(t))s=0;else if(c.test(t))s=1;else{if(!p.test(t))return;s=2}const a=(l=o,((i=e).parent&&"atrule"===i.parent.type&&"layer"===i.parent.name.toLowerCase()?l.has(i.parent)?l.get(i.parent)+1:0:1e7)+10+s);var i,l;e.each((e=>{if("decl"!==e.type)return;if(!e.variable||isDeclarationIgnored(e))return;if("initial"===e.value.toLowerCase().trim())return;const t=n.get(e.prop)??-1;a>=t&&(n.set(e.prop,a),r.set(e.prop,e.value))}))})))})),buildCustomPropertiesMap(r,new Map,t)}function getCustomPropertiesFromSiblings(e,t,r){if(!e.parent)return t;const n=new Map;return e.parent.each((t=>{"decl"===t.type&&t.variable&&e!==t&&(isDeclarationIgnored(t)||n.set(t.prop,t.value))})),n.size?buildCustomPropertiesMap(n,t,r):t}function transformProperties(e,t,n){if(isTransformableDecl(e)&&!isDeclarationIgnored(e)){const o=e.value,s=transformValueAST(r(o),t);if(s===o)return;if(parentHasExactFallback(e,s))return void(n.preserve||e.remove());if(n.preserve){const t=e.cloneBefore({value:s});hasTrailingComment(t)&&t.raws?.value&&(t.raws.value.value=t.value.replace(g,"$1"),t.raws.value.raw=t.raws.value.value+t.raws.value.raw.replace(g,"$2"))}else e.value=s,hasTrailingComment(e)&&e.raws?.value&&(e.raws.value.value=e.value.replace(g,"$1"),e.raws.value.raw=e.raws.value.value+e.raws.value.raw.replace(g,"$2"))}}const isTransformableDecl=e=>!e.variable&&e.value.includes("--")&&e.value.toLowerCase().includes("var("),hasTrailingComment=e=>"value"in Object(Object(e.raws).value)&&"raw"in(e.raws?.value??{})&&g.test(e.raws.value?.raw??""),g=/^([\W\w]+)(\s*\/\*[\W\w]+?\*\/)$/;function parentHasExactFallback(e,t){if(!e||!e.parent)return!1;let r=!1;const n=e.parent.index(e);return e.parent.each(((o,s)=>o!==e&&(!(s>=n)&&void("decl"===o.type&&o.prop.toLowerCase()===e.prop.toLowerCase()&&o.value===t&&(r=!0))))),r}const h=/(?:\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 r=new WeakMap,o=new Map;return{postcssPlugin:"postcss-custom-properties",Once(t){e=getCustomPropertiesFromRoot(t,o)},Declaration(s){if(!v.test(s.value))return;if(n(s,h))return;let a=e;t&&s.parent&&(a=r.get(s.parent)??getCustomPropertiesFromSiblings(s,e,o),r.set(s.parent,a)),transformProperties(s,a,{preserve:t})}}}}};creator.postcss=!0;export{creator as default}; diff --git a/plugins/postcss-custom-properties/src/build-custom-properties-map.ts b/plugins/postcss-custom-properties/src/build-custom-properties-map.ts new file mode 100644 index 000000000..5889fdaf7 --- /dev/null +++ b/plugins/postcss-custom-properties/src/build-custom-properties-map.ts @@ -0,0 +1,102 @@ +import valuesParser from 'postcss-value-parser'; +import { isVarFunction } from './is-var-function'; +import { parseOrCached } from './parse-or-cached'; +import { removeCyclicReferences, toposort } from './toposort'; +import transformValueAST from './transform-value-ast'; +import { isInitial } from './is-initial'; +import { parseVarFunction } from './parse-var-function'; + +export function buildCustomPropertiesMap(customProperties: Map, inherited: Map, parsedValuesCache: Map): Map { + if (!customProperties.size) { + return inherited; + } + + const out: Map = new Map(inherited); + + { + const customPropertyGraph: Array<[string, string]> = []; + + for (const [name, value] of customProperties.entries()) { + const parsedValue = parseOrCached(value, parsedValuesCache); + let assuredInvalid = false; + + valuesParser.walk(parsedValue.nodes, (node) => { + if (!isVarFunction(node)) { + return; + } + + const parsed = parseVarFunction(node); + if (!parsed) { + return; + } + + if ( + !parsed.fallback && + !customProperties.has(parsed.name.value) && + !inherited.has(parsed.name.value) + ) { + assuredInvalid = true; + return; + } + + customPropertyGraph.push([parsed.name.value, name]); + }); + + if (!assuredInvalid) { + out.set(name, parsedValue); + } + } + + removeCyclicReferences(out, customPropertyGraph); + } + + { + const customPropertyGraph: Array<[string, string]> = []; + + for (const [name, parsedValue] of out.entries()) { + valuesParser.walk(parsedValue.nodes, (node) => { + if (!isVarFunction(node)) { + return; + } + + const parsed = parseVarFunction(node); + if (!parsed) { + return; + } + + if ( + !parsed.fallback && + !out.has(parsed.name.value) + ) { + out.delete(name); + return; + } + + customPropertyGraph.push([parsed.name.value, name]); + }); + } + + const sortedCustomPropertyNames = toposort(Array.from(out.keys()), customPropertyGraph); + + for (const customPropertyName of sortedCustomPropertyNames) { + const value = out.get(customPropertyName); + if (!value) { + continue; + } + + const transformed = transformValueAST(value, out); + const parsedValue = parseOrCached(transformed, parsedValuesCache); + + out.set(customPropertyName, parsedValue); + } + } + + for (const [name, value] of out.entries()) { + if (isInitial(value)) { + out.delete(name); + continue; + } + } + + return out; +} diff --git a/plugins/postcss-custom-properties/src/get-custom-properties-from-root.ts b/plugins/postcss-custom-properties/src/get-custom-properties-from-root.ts index fe97e45d3..27c8005f1 100644 --- a/plugins/postcss-custom-properties/src/get-custom-properties-from-root.ts +++ b/plugins/postcss-custom-properties/src/get-custom-properties-from-root.ts @@ -3,9 +3,7 @@ import valuesParser from 'postcss-value-parser'; import { cascadeLayerNumberForNode, collectCascadeLayerOrder } from './cascade-layers'; import { isBlockIgnored, isDeclarationIgnored } from './is-ignored'; import { HTML_SELECTOR_REGEX, HTML_WHERE_SELECTOR_REGEX, MAYBE_HTML_OR_ROOT_RULE_REGEX, ROOT_SELECTOR_REGEX, ROOT_WHERE_SELECTOR_REGEX, isProcessableRule } from './is-processable-rule'; -import { isVarFunction } from './is-var-function'; -import { removeCyclicReferences } from './toposort'; -import { parseOrCached } from './parse-or-cached'; +import { buildCustomPropertiesMap } from './build-custom-properties-map'; // return custom selectors from the css root, conditionally removing them export default function getCustomPropertiesFromRoot(root: Root, parsedValuesCache: Map): Map { @@ -67,25 +65,5 @@ export default function getCustomPropertiesFromRoot(root: Root, parsedValuesCach }); }); - const customPropertyGraph: Array<[string, string]> = []; - const out: Map = new Map(); - - for (const [name, value] of customProperties.entries()) { - const parsedValue = parseOrCached(value, parsedValuesCache); - - valuesParser.walk(parsedValue.nodes, (node) => { - if (isVarFunction(node)) { - const [nestedVariableNode] = node.nodes.filter((x) => x.type === 'word'); - - customPropertyGraph.push([nestedVariableNode.value, name]); - } - }); - - out.set(name, parsedValue); - } - - removeCyclicReferences(out, customPropertyGraph); - - // return all custom properties, preferring :root properties over html properties - return out; + return buildCustomPropertiesMap(customProperties, new Map(), parsedValuesCache); } diff --git a/plugins/postcss-custom-properties/src/get-custom-properties-from-siblings.ts b/plugins/postcss-custom-properties/src/get-custom-properties-from-siblings.ts new file mode 100644 index 000000000..2f1dcf3a4 --- /dev/null +++ b/plugins/postcss-custom-properties/src/get-custom-properties-from-siblings.ts @@ -0,0 +1,35 @@ +import type { Declaration } from 'postcss'; +import valuesParser from 'postcss-value-parser'; + +import { buildCustomPropertiesMap } from './build-custom-properties-map'; +import { isDeclarationIgnored } from './is-ignored'; + +export default function getCustomPropertiesFromSiblings(decl: Declaration, inherited: Map, parsedValuesCache: Map): Map { + if (!decl.parent) { + return inherited; + } + + const customProperties: Map = new Map(); + + decl.parent.each((siblingDecl) => { + if (siblingDecl.type !== 'decl' || !siblingDecl.variable) { + return; + } + + if (decl === siblingDecl) { + return; + } + + if (isDeclarationIgnored(siblingDecl)) { + return; + } + + customProperties.set(siblingDecl.prop, siblingDecl.value); + }); + + if (!customProperties.size) { + return inherited; + } + + return buildCustomPropertiesMap(customProperties, inherited, parsedValuesCache); +} diff --git a/plugins/postcss-custom-properties/src/index.ts b/plugins/postcss-custom-properties/src/index.ts index 6282be294..eaec56051 100644 --- a/plugins/postcss-custom-properties/src/index.ts +++ b/plugins/postcss-custom-properties/src/index.ts @@ -1,12 +1,11 @@ -import type { Plugin, PluginCreator } from 'postcss'; -import type valuesParser from 'postcss-value-parser'; +import type { Node, Plugin, PluginCreator } from 'postcss'; +import valuesParser from 'postcss-value-parser'; import getCustomPropertiesFromRoot from './get-custom-properties-from-root'; -import { isDeclarationIgnored } from './is-ignored'; -import { transformProperties } from './transform-properties'; -import { hasSupportsAtRuleAncestor } from '@csstools/utilities'; -import { parseOrCached } from './parse-or-cached'; +import getCustomPropertiesFromSiblings from './get-custom-properties-from-siblings'; import { HAS_VAR_FUNCTION_REGEX } from './is-var-function'; +import { hasSupportsAtRuleAncestor } from '@csstools/utilities'; +import { transformProperties } from './transform-properties'; /** postcss-custom-properties plugin options */ export type pluginOptions = { @@ -14,7 +13,6 @@ export type pluginOptions = { preserve?: boolean, }; -const IS_INITIAL_REGEX = /^initial$/i; const SUPPORTS_REGEX = /(?:\bvar\()|(?:\(top: var\(--f\))/i; const creator: PluginCreator = (opts?: pluginOptions) => { @@ -31,13 +29,14 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return { postcssPlugin: 'postcss-custom-properties', prepare(): Plugin { - let customProperties: Map = new Map(); + let rootCustomProperties: Map = new Map(); + const customPropertiesByParent: WeakMap> = new WeakMap(); const parsedValuesCache: Map = new Map(); return { postcssPlugin: 'postcss-custom-properties', Once(root): void { - customProperties = getCustomPropertiesFromRoot(root, parsedValuesCache); + rootCustomProperties = getCustomPropertiesFromRoot(root, parsedValuesCache); }, Declaration(decl): void { if (!HAS_VAR_FUNCTION_REGEX.test(decl.value)) { @@ -48,31 +47,14 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - const localCustomProperties : Map = new Map(); - if (preserve && decl.parent) { - decl.parent.each((siblingDecl) => { - if (siblingDecl.type !== 'decl' || !siblingDecl.variable) { - return; - } + let customProperties = rootCustomProperties; - if (decl === siblingDecl) { - return; - } - - if (isDeclarationIgnored(siblingDecl)) { - return; - } - - if (IS_INITIAL_REGEX.test(siblingDecl.value)) { - localCustomProperties.delete(siblingDecl.prop); - return; - } - - localCustomProperties.set(siblingDecl.prop, parseOrCached(siblingDecl.value, parsedValuesCache)); - }); + if (preserve && decl.parent) { + customProperties = customPropertiesByParent.get(decl.parent) ?? getCustomPropertiesFromSiblings(decl, rootCustomProperties, parsedValuesCache); + customPropertiesByParent.set(decl.parent, customProperties); } - transformProperties(decl, customProperties, localCustomProperties, parsedValuesCache, { preserve: preserve }); + transformProperties(decl, customProperties, { preserve: preserve }); }, }; }, diff --git a/plugins/postcss-custom-properties/src/is-initial.ts b/plugins/postcss-custom-properties/src/is-initial.ts new file mode 100644 index 000000000..d3ca3ddfe --- /dev/null +++ b/plugins/postcss-custom-properties/src/is-initial.ts @@ -0,0 +1,13 @@ +import valueParser from 'postcss-value-parser'; + +const IS_INITIAL_REGEX = /^initial$/i; + +export function isInitial(parsedValue: valueParser.ParsedValue): boolean { + const relevantNodes = parsedValue.nodes.filter((node) => node.type !== 'comment' && node.type !== 'space'); + + if (relevantNodes.length !== 1) { + return false; + } + + return relevantNodes[0].type === 'word' && IS_INITIAL_REGEX.test(relevantNodes[0].value); +} diff --git a/plugins/postcss-custom-properties/src/parse-var-function.ts b/plugins/postcss-custom-properties/src/parse-var-function.ts new file mode 100644 index 000000000..646344027 --- /dev/null +++ b/plugins/postcss-custom-properties/src/parse-var-function.ts @@ -0,0 +1,37 @@ +import type { FunctionNode, Node, WordNode } from 'postcss-value-parser'; + +export function parseVarFunction(node: FunctionNode): {name: WordNode, fallback?: Array } | void { + let name: WordNode | undefined; + let comma: boolean = false; + let fallback: Array | undefined; + + for (const child of node.nodes) { + if (!name && child.type === 'word') { + name = child; + continue; + } + + if (name && !comma && child.type === 'div' && child.value === ',') { + comma = true; + fallback = []; + continue; + } + + if (comma && Array.isArray(fallback)) { + fallback.push(child); + continue; + } + + if (child.type === 'space' || (child.type === 'div' && child.value.trim() === '')) { + continue; + } + + return; + } + + if (!name) { + return; + } + + return {name: name, fallback}; +} diff --git a/plugins/postcss-custom-properties/src/toposort.ts b/plugins/postcss-custom-properties/src/toposort.ts index fff20af35..00d613d32 100644 --- a/plugins/postcss-custom-properties/src/toposort.ts +++ b/plugins/postcss-custom-properties/src/toposort.ts @@ -19,6 +19,59 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +export function toposort(nodes: Array, edges: Array>): Array { + let cursor = nodes.length; + const sorted: Array = new Array(cursor); + const visited: Record = {}; + let i = cursor; + // Better data structures make algorithm much faster. + const outgoingEdges = makeOutgoingEdges(edges); + const nodesHash = makeNodesHash(nodes); + + while (i--) { + if (!visited[i]) { + visit(nodes[i], i, new Set()); + } + } + + return sorted; + + function visit(node: string, j: number, predecessors: Set): string | undefined { + if (predecessors.has(node)) { + let nodeRep; + try { + nodeRep = ', node was:' + JSON.stringify(node); + } catch (e) { + nodeRep = ''; + } + throw new Error('Cyclic dependency' + nodeRep); + } + + if (!nodesHash.has(node)) { + throw new Error('Found unknown node. Make sure to provided all involved nodes. Unknown node: ' + JSON.stringify(node)); + } + + if (visited[j]) { + return; + } + visited[j] = true; + + const outgoing: Array = Array.from(outgoingEdges.get(node) || new Set()); + + // eslint-disable-next-line no-cond-assign + if (j = outgoing.length) { + predecessors.add(node); + do { + const child = outgoing[--j]; + visit(child, nodesHash.get(child)!, predecessors); + } while (j); + predecessors.delete(node); + } + + sorted[--cursor] = node; + } +} + // We (ab)use `toposort` to find cyclic references. // At the moment this is not optimized and uses a brute force approach. // diff --git a/plugins/postcss-custom-properties/src/transform-properties.ts b/plugins/postcss-custom-properties/src/transform-properties.ts index 3e1ba04e4..053e75bc7 100644 --- a/plugins/postcss-custom-properties/src/transform-properties.ts +++ b/plugins/postcss-custom-properties/src/transform-properties.ts @@ -1,49 +1,41 @@ +import type { Declaration } from 'postcss'; import valuesParser from 'postcss-value-parser'; + import transformValueAST from './transform-value-ast'; -import type { Declaration } from 'postcss'; import { isDeclarationIgnored } from './is-ignored'; -import { HAS_VAR_FUNCTION_REGEX } from './is-var-function'; // transform custom pseudo selectors with custom selectors -export function transformProperties(decl: Declaration, customProperties: Map, localCustomProperties: Map, parsedValuesCache: Map, opts: { preserve?: boolean }): void { +export function transformProperties(decl: Declaration, customProperties: Map, opts: { preserve?: boolean }): void { if (isTransformableDecl(decl) && !isDeclarationIgnored(decl)) { const originalValue = decl.value; const valueAST = valuesParser(originalValue); - let value = transformValueAST(valueAST, customProperties, localCustomProperties); + const value = transformValueAST(valueAST, customProperties); - // protect against circular references - const valueSet = new Set(); - - while (HAS_VAR_FUNCTION_REGEX.test(value) && !valueSet.has(value)) { - valueSet.add(value); - const parsedValueAST = valuesParser(value); - value = transformValueAST(parsedValueAST, customProperties, localCustomProperties); + if (value === originalValue) { + return; } - // conditionally transform values that have changed - if (value !== originalValue) { - if (parentHasExactFallback(decl, value)) { - if (!opts.preserve) { - decl.remove(); - } - - return; + if (parentHasExactFallback(decl, value)) { + if (!opts.preserve) { + decl.remove(); } - if (opts.preserve) { - const beforeDecl = decl.cloneBefore({ value }); + return; + } - if (hasTrailingComment(beforeDecl) && beforeDecl.raws?.value) { - beforeDecl.raws.value.value = beforeDecl.value.replace(TRAILING_COMMENT_REGEX, '$1'); - beforeDecl.raws.value.raw = beforeDecl.raws.value.value + beforeDecl.raws.value.raw.replace(TRAILING_COMMENT_REGEX, '$2'); - } - } else { - decl.value = value; + if (opts.preserve) { + const beforeDecl = decl.cloneBefore({ value }); + + if (hasTrailingComment(beforeDecl) && beforeDecl.raws?.value) { + beforeDecl.raws.value.value = beforeDecl.value.replace(TRAILING_COMMENT_REGEX, '$1'); + beforeDecl.raws.value.raw = beforeDecl.raws.value.value + beforeDecl.raws.value.raw.replace(TRAILING_COMMENT_REGEX, '$2'); + } + } else { + decl.value = value; - if (hasTrailingComment(decl) && decl.raws?.value) { - decl.raws.value.value = decl.value.replace(TRAILING_COMMENT_REGEX, '$1'); - decl.raws.value.raw = decl.raws.value.value + decl.raws.value.raw.replace(TRAILING_COMMENT_REGEX, '$2'); - } + if (hasTrailingComment(decl) && decl.raws?.value) { + decl.raws.value.value = decl.value.replace(TRAILING_COMMENT_REGEX, '$1'); + decl.raws.value.raw = decl.raws.value.value + decl.raws.value.raw.replace(TRAILING_COMMENT_REGEX, '$2'); } } } diff --git a/plugins/postcss-custom-properties/src/transform-value-ast.ts b/plugins/postcss-custom-properties/src/transform-value-ast.ts index b7904eb71..7307c442e 100644 --- a/plugins/postcss-custom-properties/src/transform-value-ast.ts +++ b/plugins/postcss-custom-properties/src/transform-value-ast.ts @@ -1,83 +1,89 @@ -import valuesParser from 'postcss-value-parser'; -import type { FunctionNode, Node } from 'postcss-value-parser'; +import valuesParser, { Node } from 'postcss-value-parser'; + import { isVarFunction } from './is-var-function'; +import { parseVarFunction } from './parse-var-function'; -export default function transformValueAST(root: valuesParser.ParsedValue, customProperties: Map, localCustomProperties: Map): string { +export default function transformValueAST(root: valuesParser.ParsedValue, customProperties: Map): string { if (!root.nodes?.length) { return ''; } - const ancestry: Map = new Map(); - root.nodes.forEach((child) => { - ancestry.set(child, root); - }); - - valuesParser.walk(root.nodes, (child) => { - if (('nodes' in child) && child.nodes.length) { - child.nodes.forEach((grandChild) => { - ancestry.set(grandChild, child); - }); - } - }); - - valuesParser.walk(root.nodes, (child) => { + walk(root.nodes, (child, index, nodes) => { if (!isVarFunction(child)) { return; } - const [propertyNode, ...fallbacks] = child.nodes.filter((x) => x.type !== 'div'); - const { value: name } = propertyNode; - - const parent = ancestry.get(child); - if (!parent) { - return; - } - - const index = parent.nodes.indexOf(child); - if (index === -1) { + const parsed = parseVarFunction(child); + if (!parsed) { return; } let fallbackContainsUnknownVariables = false; - if (fallbacks) { - valuesParser.walk(fallbacks, (childNodeInFallback) => { - if (isVarFunction(childNodeInFallback)) { - const [fallbackPropertyNode] = childNodeInFallback.nodes.filter((x) => x.type === 'word'); - if (customProperties.has(fallbackPropertyNode.value) || localCustomProperties.has(fallbackPropertyNode.value)) { - return; - } + if (parsed.fallback) { + valuesParser.walk(parsed.fallback, (childNodeInFallback) => { + if (!isVarFunction(childNodeInFallback)) { + return; + } + + const parsedFallback = parseVarFunction(childNodeInFallback); + if (!parsedFallback) { + return; + } + if ( + !parsedFallback.fallback && + !customProperties.has(parsedFallback.name.value) + ) { fallbackContainsUnknownVariables = true; return false; } }); } - let nodes = localCustomProperties.get(name)?.nodes ?? customProperties.get(name)?.nodes; - if (!nodes && fallbacks.length && !fallbackContainsUnknownVariables) { + let resolvedNodes = customProperties.get(parsed.name.value)?.nodes; + if (!resolvedNodes && parsed.fallback && !fallbackContainsUnknownVariables) { // No match, but fallback available - nodes = child.nodes.slice(child.nodes.indexOf(fallbacks[0])); + resolvedNodes = parsed.fallback; } - if (typeof nodes === 'undefined') { + if (typeof resolvedNodes === 'undefined') { return; } - if (nodes.length) { - parent.nodes.splice(index, 1, ...nodes); - parent.nodes.forEach((x) => ancestry.set(x, parent)); + if (resolvedNodes.length) { + nodes.splice(index, 1, ...resolvedNodes); } else { // `postcss-value-parser` throws when removing nodes. // Inserting an empty comment produces equivalent CSS source code and avoids the exception. - parent.nodes.splice(index, 1, { - type: 'comment', - value: '', + nodes.splice(index, 1, { + type: 'div', + value: ' ', + before: '', + after: '', sourceIndex: child.sourceIndex, sourceEndIndex: child.sourceEndIndex, }); - parent.nodes.forEach((x) => ancestry.set(x, parent)); } - }, true); + }); return valuesParser.stringify(root.nodes); } + +function walk(nodes: Array, cb: valuesParser.WalkCallback): void { + let i, max, node, result; + + for (i = 0, max = nodes.length; i < max; i += 1) { + node = nodes[i]; + + if ( + result !== false && + node.type === 'function' && + Array.isArray(node.nodes) + ) { + walk(node.nodes, cb); + } + + cb(node, i, nodes); + max = nodes.length; + } +} diff --git a/plugins/postcss-custom-properties/test/_tape.mjs b/plugins/postcss-custom-properties/test/_tape.mjs index a53fecd7a..a390ae158 100644 --- a/plugins/postcss-custom-properties/test/_tape.mjs +++ b/plugins/postcss-custom-properties/test/_tape.mjs @@ -15,6 +15,12 @@ postcssTape(plugin)({ 'cascade-layers': { message: 'supports cascade layers', }, + 'cyclic-on-different-element-1': { + message: 'supports cyclic variables on different elements', + }, + 'cyclic-on-different-element-2': { + message: 'supports cyclic variables on different elements', + }, 'chained': { message: 'supports chained variables', }, diff --git a/plugins/postcss-custom-properties/test/basic.css b/plugins/postcss-custom-properties/test/basic.css index 2ccbf5dd3..c5b2a485f 100644 --- a/plugins/postcss-custom-properties/test/basic.css +++ b/plugins/postcss-custom-properties/test/basic.css @@ -241,3 +241,17 @@ html { left: var(--something-local); } } + +:root { + --with-fallback-on-root-1: var(--does-not-exist, 2); + --with-fallback-on-root-2: var(--does-not-exist, ); +} + +.el { + order: var(--with-fallback-on-root-1); +} + +.el { + order: var(--with-fallback-on-root-2); + color: rgb(calc(120)var(--with-fallback-on-root-2)130 140); +} diff --git a/plugins/postcss-custom-properties/test/basic.expect.css b/plugins/postcss-custom-properties/test/basic.expect.css index 33303fd6b..62c32be06 100644 --- a/plugins/postcss-custom-properties/test/basic.expect.css +++ b/plugins/postcss-custom-properties/test/basic.expect.css @@ -93,7 +93,6 @@ html { } .test--circular_var { - color: var(--circular-2); color: var(--circular); } @@ -191,7 +190,7 @@ html { } .space-values { - a: list("a",/**/,"c"); + a: list("a", ,"c"); a: list("a",var(--space-1),"c"); b: list("a", ,"c"); b: list("a",var(--space-2),"c"); @@ -277,3 +276,20 @@ html { left: var(--something-local); } } + +:root { + --with-fallback-on-root-1: var(--does-not-exist, 2); + --with-fallback-on-root-2: var(--does-not-exist, ); +} + +.el { + order: 2; + order: var(--with-fallback-on-root-1); +} + +.el { + order: ; + order: var(--with-fallback-on-root-2); + color: rgb(calc(120) 130 140); + color: rgb(calc(120)var(--with-fallback-on-root-2)130 140); +} diff --git a/plugins/postcss-custom-properties/test/basic.preserve.expect.css b/plugins/postcss-custom-properties/test/basic.preserve.expect.css index 20acb54e6..1be4ee949 100644 --- a/plugins/postcss-custom-properties/test/basic.preserve.expect.css +++ b/plugins/postcss-custom-properties/test/basic.preserve.expect.css @@ -81,7 +81,7 @@ html { } .test--circular_var { - color: var(--circular-2); + color: var(--circular); } .test--z-index { @@ -162,7 +162,7 @@ html { } .space-values { - a: list("a",/**/,"c"); + a: list("a", ,"c"); b: list("a", ,"c"); c: list("a", ,"c"); } @@ -236,3 +236,17 @@ html { left: var(--something-local); } } + +:root { + --with-fallback-on-root-1: var(--does-not-exist, 2); + --with-fallback-on-root-2: var(--does-not-exist, ); +} + +.el { + order: 2; +} + +.el { + order: ; + color: rgb(calc(120) 130 140); +} diff --git a/plugins/postcss-custom-properties/test/cyclic-on-different-element-1.css b/plugins/postcss-custom-properties/test/cyclic-on-different-element-1.css new file mode 100644 index 000000000..857d81fc2 --- /dev/null +++ b/plugins/postcss-custom-properties/test/cyclic-on-different-element-1.css @@ -0,0 +1,8 @@ +:root { + --a: var(--b); +} + +.foo { + color: var(--b); + --b: var(--a); +} diff --git a/plugins/postcss-custom-properties/test/cyclic-on-different-element-1.expect.css b/plugins/postcss-custom-properties/test/cyclic-on-different-element-1.expect.css new file mode 100644 index 000000000..857d81fc2 --- /dev/null +++ b/plugins/postcss-custom-properties/test/cyclic-on-different-element-1.expect.css @@ -0,0 +1,8 @@ +:root { + --a: var(--b); +} + +.foo { + color: var(--b); + --b: var(--a); +} diff --git a/plugins/postcss-custom-properties/test/cyclic-on-different-element-2.css b/plugins/postcss-custom-properties/test/cyclic-on-different-element-2.css new file mode 100644 index 000000000..e4599eb56 --- /dev/null +++ b/plugins/postcss-custom-properties/test/cyclic-on-different-element-2.css @@ -0,0 +1,9 @@ +:root { + --a: var(--b); + --b: 1; +} + +.foo { + color: var(--b); + --b: var(--a); +} diff --git a/plugins/postcss-custom-properties/test/cyclic-on-different-element-2.expect.css b/plugins/postcss-custom-properties/test/cyclic-on-different-element-2.expect.css new file mode 100644 index 000000000..76dd5b811 --- /dev/null +++ b/plugins/postcss-custom-properties/test/cyclic-on-different-element-2.expect.css @@ -0,0 +1,10 @@ +:root { + --a: var(--b); + --b: 1; +} + +.foo { + color: 1; + color: var(--b); + --b: var(--a); +}