Skip to content

Commit c93111d

Browse files
romainmenkehlukhani
andauthored
postcss-text-decoration-shorthand: reduce redundant fallbacks (#1776)
Co-authored-by: hlukhani <[email protected]>
1 parent 1f8f022 commit c93111d

8 files changed

Lines changed: 201 additions & 8 deletions

File tree

plugins/postcss-text-decoration-shorthand/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changes to PostCSS Text Decoration Shorthand
22

3+
### Unreleased (patch)
4+
5+
- Reduce redundant fallbacks for both `text-decoration` and `-webkit-text-decoration`
6+
37
### 5.0.1
48

59
_January 25, 2026_
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
import e from"postcss-value-parser";import{namedColors as o}from"@csstools/color-helpers";const t=/^text-decoration$/i,creator=o=>{const c=Object.assign({preserve:!0},o);return{postcssPlugin:"postcss-text-decoration-shorthand",prepare(){const o=new Map;return{postcssPlugin:"postcss-text-decoration-shorthand",OnceExit(){o.clear()},Declaration(i){if(!t.test(i.prop))return;const a=i.parent;if(!a)return;const u=a.index(i);if(a.nodes.some(e=>"decl"===e.type&&t.test(e.prop)&&o.get(i.value)===e.value&&a.index(e)!==u))return;const d=e(i.value),p=d.nodes.filter(e=>"space"!==e.type&&"comment"!==e.type);if(p.find(e=>"var"===e.value.toLowerCase()&&"function"===e.type))return;if(p.find(e=>"word"===e.type&&r.includes(e.value)))return;const f={line:[],style:null,color:null,thickness:null};for(let o=0;o<p.length;o++){const t=p[o];if(!f.line.length&&"word"===t.type&&n.includes(t.value.toLowerCase())){const e=t;let r=t;for(;;){const e=p[o+1];if(!e||"word"!==e.type||!n.includes(e.value.toLowerCase()))break;r=e,o++}f.line=d.nodes.slice(d.nodes.indexOf(e),d.nodes.indexOf(r)+1);continue}if(f.line.length||"word"!==t.type||"none"!==t.value.toLowerCase())if(f.style||"word"!==t.type||!s.includes(t.value.toLowerCase()))if(f.thickness||"word"!==t.type||!l.includes(t.value.toLowerCase()))if(f.thickness||"function"!==t.type||"calc"!==t.value.toLowerCase()){if(f.color||!nodeIsAColor(t)){if("word"===t.type){let o;try{o=e.unit(t.value)}catch{return}if(!o||!o.unit)return;f.thickness=t,"%"===o.unit&&(f.thickness={...genericNodeParts(),type:"function",value:"calc",nodes:[{...genericNodeParts(),type:"word",value:"0.01em"},{...genericNodeParts(),type:"space",value:" "},{...genericNodeParts(),type:"word",value:"*"},{...genericNodeParts(),type:"space",value:" "},{...genericNodeParts(),type:"word",value:o.number}]});continue}return}f.color=t}else f.thickness=t;else f.thickness=t;else f.style=t;else f.line.push(t)}f.line.length||f.line.push({before:"",after:"",sourceIndex:0,sourceEndIndex:0,type:"word",value:"none"}),f.style||(f.style={before:"",after:"",sourceIndex:0,sourceEndIndex:0,type:"word",value:"solid"}),f.color||(f.color={before:"",after:"",sourceIndex:0,sourceEndIndex:0,type:"word",value:"currentColor"});const v=e.stringify(f.line);if(i.value.toLowerCase()===v.toLowerCase()){const e=i.next();return void(e&&"decl"===e.type&&"text-decoration"===e.prop.toLowerCase()||i.cloneBefore({prop:"-webkit-text-decoration",value:v}))}i.cloneBefore({prop:"text-decoration",value:v});const y=e.stringify([...f.line,{before:"",after:"",sourceIndex:0,sourceEndIndex:0,type:"space",value:" "},f.style,{before:"",after:"",sourceIndex:0,sourceEndIndex:0,type:"space",value:" "},f.color]);f.thickness&&i.cloneBefore({prop:"text-decoration",value:y}),f.thickness&&i.cloneBefore({prop:"text-decoration-thickness",value:e.stringify([f.thickness])}),o.set(i.value,v),o.set(y,v),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(o)];function genericNodeParts(){return{before:"",after:"",sourceIndex:0,sourceEndIndex:0}}export{creator as default,creator as"module.exports"};
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=a.nodes.filter(e=>"decl"===e.type&&o.test(e.prop)&&a.index(e)!==u);if(d.some(e=>t.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;t<f.length;t++){const o=f[t];if(!v.line.length&&"word"===o.type&&n.includes(o.value.toLowerCase())){const e=o;let r=o;for(;;){const e=f[t+1];if(!e||"word"!==e.type||!n.includes(e.value.toLowerCase()))break;r=e,t++}v.line=p.nodes.slice(p.nodes.indexOf(e),p.nodes.indexOf(r)+1);continue}if(v.line.length||"word"!==o.type||"none"!==o.value.toLowerCase())if(v.style||"word"!==o.type||!s.includes(o.value.toLowerCase()))if(v.thickness||"word"!==o.type||!l.includes(o.value.toLowerCase()))if(v.thickness||"function"!==o.type||"calc"!==o.value.toLowerCase()){if(v.color||!nodeIsAColor(o)){if("word"===o.type){let t;try{t=e.unit(o.value)}catch{return}if(!t||!t.unit)return;v.thickness=o,"%"===t.unit&&(v.thickness={...genericNodeParts(),type:"function",value:"calc",nodes:[{...genericNodeParts(),type:"word",value:"0.01em"},{...genericNodeParts(),type:"space",value:" "},{...genericNodeParts(),type:"word",value:"*"},{...genericNodeParts(),type:"space",value:" "},{...genericNodeParts(),type:"word",value:t.number}]});continue}return}v.color=o}else v.thickness=o;else v.thickness=o;else v.style=o;else v.line.push(o)}v.line.length||v.line.push({before:"",after:"",sourceIndex:0,sourceEndIndex:0,type:"word",value:"none"}),v.style||(v.style={before:"",after:"",sourceIndex:0,sourceEndIndex:0,type:"word",value:"solid"}),v.color||(v.color={before:"",after:"",sourceIndex:0,sourceEndIndex:0,type:"word",value:"currentColor"});const y=e.stringify(v.line);if(t.set(i.value,y),d.some(e=>t.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"};

plugins/postcss-text-decoration-shorthand/src/index.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Plugin, PluginCreator } from 'postcss';
1+
import type { Declaration, Plugin, PluginCreator } from 'postcss';
22
import valueParser from 'postcss-value-parser';
33
import { namedColors } from '@csstools/color-helpers';
44

@@ -41,13 +41,16 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
4141
}
4242

4343
const ownIndex = parent.index(decl);
44-
const hasFallbacksOrOverrides = parent.nodes.some((node) => {
44+
45+
const siblingTextDecorationProperties = parent.nodes.filter((node) => {
4546
return node.type === 'decl' &&
4647
IS_TEXT_DECORATION_REGEX.test(node.prop) &&
47-
convertedValues.get(decl.value) === node.value &&
4848
parent.index(node) !== ownIndex;
49-
});
50-
if (hasFallbacksOrOverrides) {
49+
}) as Array<Declaration>;
50+
51+
if (siblingTextDecorationProperties.some((node) => {
52+
return convertedValues.get(decl.value) === node.value
53+
})) {
5154
return;
5255
}
5356

@@ -191,8 +194,20 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
191194
}
192195

193196
const nonShortHandValue = valueParser.stringify(data.line);
197+
convertedValues.set(decl.value, nonShortHandValue);
198+
199+
if (siblingTextDecorationProperties.some((node) => {
200+
return convertedValues.get(decl.value) === node.value
201+
})) {
202+
return;
203+
}
204+
194205
if (decl.value.toLowerCase() === nonShortHandValue.toLowerCase()) {
195-
const next = decl.next();
206+
let next = decl.next();
207+
while (next && next.type === 'comment') {
208+
next = next.next();
209+
}
210+
196211
if (!next || next.type !== 'decl' || next.prop.toLowerCase() !== 'text-decoration') {
197212

198213
// "-webkit-text-decoration" is a shorthand and sets omitted constituent properties to their initial value.
@@ -243,7 +258,6 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
243258
});
244259
}
245260

246-
convertedValues.set(decl.value, nonShortHandValue);
247261
convertedValues.set(shortHandValue, nonShortHandValue);
248262

249263
if (!options.preserve) {

plugins/postcss-text-decoration-shorthand/test/basic.autoprefixer.expect.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,45 @@
185185
-webkit-text-decoration: underline red overline;
186186
text-decoration: underline red overline;
187187
}
188+
189+
.manual-fallback-a {
190+
text-decoration: underline;
191+
-webkit-text-decoration: underline dotted;
192+
text-decoration: underline dotted;
193+
}
194+
195+
.manual-fallback-b {
196+
text-decoration: overline;
197+
-webkit-text-decoration: overline solid purple;
198+
text-decoration: overline solid purple;
199+
-webkit-text-decoration: overline purple 4px;
200+
text-decoration: overline purple 4px;
201+
}
202+
203+
.manual-fallback-c {
204+
text-decoration: overline;
205+
-webkit-text-decoration: overline solid purple;
206+
text-decoration: overline solid purple;
207+
-webkit-text-decoration: overline purple 4px;
208+
text-decoration: overline purple 4px;
209+
}
210+
211+
.fallback-without-comment {
212+
text-decoration: underline;
213+
-webkit-text-decoration: underline dotted;
214+
text-decoration: underline dotted;
215+
}
216+
217+
.fallback-with-comment-a {
218+
text-decoration: underline; /* 2 */
219+
-webkit-text-decoration: underline dotted;
220+
text-decoration: underline dotted;
221+
}
222+
223+
.fallback-with-comment-b {
224+
text-decoration: underline;
225+
-webkit-text-decoration: underline /* 2 */;
226+
text-decoration: underline /* 2 */;
227+
-webkit-text-decoration: underline dotted;
228+
text-decoration: underline dotted;
229+
}

plugins/postcss-text-decoration-shorthand/test/basic.autoprefixer.preserve-false.expect.css

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,41 @@
152152
-webkit-text-decoration: underline red overline;
153153
text-decoration: underline red overline;
154154
}
155+
156+
.manual-fallback-a {
157+
text-decoration: underline;
158+
-webkit-text-decoration: underline dotted;
159+
text-decoration: underline dotted;
160+
}
161+
162+
.manual-fallback-b {
163+
text-decoration: overline;
164+
-webkit-text-decoration: overline purple 4px;
165+
text-decoration: overline purple 4px;
166+
}
167+
168+
.manual-fallback-c {
169+
text-decoration: overline;
170+
-webkit-text-decoration: overline solid purple;
171+
text-decoration: overline solid purple;
172+
-webkit-text-decoration: overline purple 4px;
173+
text-decoration: overline purple 4px;
174+
}
175+
176+
.fallback-without-comment {
177+
text-decoration: underline;
178+
-webkit-text-decoration: underline dotted;
179+
text-decoration: underline dotted;
180+
}
181+
182+
.fallback-with-comment-a {
183+
text-decoration: underline; /* 2 */
184+
-webkit-text-decoration: underline dotted;
185+
text-decoration: underline dotted;
186+
}
187+
188+
.fallback-with-comment-b {
189+
text-decoration: underline;
190+
-webkit-text-decoration: underline dotted;
191+
text-decoration: underline dotted;
192+
}

plugins/postcss-text-decoration-shorthand/test/basic.css

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,34 @@
9797
.ignored--d {
9898
text-decoration: underline red overline;
9999
}
100+
101+
.manual-fallback-a {
102+
text-decoration: underline;
103+
text-decoration: underline dotted;
104+
}
105+
106+
.manual-fallback-b {
107+
text-decoration: overline solid purple;
108+
text-decoration: overline purple 4px;
109+
}
110+
111+
.manual-fallback-c {
112+
text-decoration: overline;
113+
text-decoration: overline solid purple;
114+
text-decoration: overline purple 4px;
115+
}
116+
117+
.fallback-without-comment {
118+
text-decoration: underline;
119+
text-decoration: underline dotted;
120+
}
121+
122+
.fallback-with-comment-a {
123+
text-decoration: underline; /* 2 */
124+
text-decoration: underline dotted;
125+
}
126+
127+
.fallback-with-comment-b {
128+
text-decoration: underline /* 2 */;
129+
text-decoration: underline dotted;
130+
}

plugins/postcss-text-decoration-shorthand/test/basic.expect.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,36 @@
148148
.ignored--d {
149149
text-decoration: underline red overline;
150150
}
151+
152+
.manual-fallback-a {
153+
text-decoration: underline;
154+
text-decoration: underline dotted;
155+
}
156+
157+
.manual-fallback-b {
158+
text-decoration: overline;
159+
text-decoration: overline solid purple;
160+
text-decoration: overline purple 4px;
161+
}
162+
163+
.manual-fallback-c {
164+
text-decoration: overline;
165+
text-decoration: overline solid purple;
166+
text-decoration: overline purple 4px;
167+
}
168+
169+
.fallback-without-comment {
170+
text-decoration: underline;
171+
text-decoration: underline dotted;
172+
}
173+
174+
.fallback-with-comment-a {
175+
text-decoration: underline; /* 2 */
176+
text-decoration: underline dotted;
177+
}
178+
179+
.fallback-with-comment-b {
180+
text-decoration: underline;
181+
text-decoration: underline /* 2 */;
182+
text-decoration: underline dotted;
183+
}

plugins/postcss-text-decoration-shorthand/test/basic.preserve-false.expect.css

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,34 @@
133133
.ignored--d {
134134
text-decoration: underline red overline;
135135
}
136+
137+
.manual-fallback-a {
138+
text-decoration: underline;
139+
text-decoration: underline dotted;
140+
}
141+
142+
.manual-fallback-b {
143+
text-decoration: overline;
144+
text-decoration: overline purple 4px;
145+
}
146+
147+
.manual-fallback-c {
148+
text-decoration: overline;
149+
text-decoration: overline solid purple;
150+
text-decoration: overline purple 4px;
151+
}
152+
153+
.fallback-without-comment {
154+
text-decoration: underline;
155+
text-decoration: underline dotted;
156+
}
157+
158+
.fallback-with-comment-a {
159+
text-decoration: underline; /* 2 */
160+
text-decoration: underline dotted;
161+
}
162+
163+
.fallback-with-comment-b {
164+
text-decoration: underline;
165+
text-decoration: underline dotted;
166+
}

0 commit comments

Comments
 (0)