Skip to content

Commit 539acc3

Browse files
author
hlukhani
committed
fix: avoid adding duplicate text-decoration fallback when multiple declarations exist
1 parent 1f8f022 commit 539acc3

5 files changed

Lines changed: 61 additions & 8 deletions

File tree

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;function hasFollowingTextDecoration(e){let t=e.next();for(;t;){if("comment"!==t.type)return"decl"===t.type&&o.test(t.prop);t=t.next()}return!1}function hasPreviousSameTextDecoration(e,t){let r=e.prev();for(;r;){if("comment"!==r.type)return"decl"===r.type&&(o.test(r.prop)&&r.value.toLowerCase()===t.toLowerCase());r=r.prev()}return!1}const 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);if(a.nodes.some(e=>"decl"===e.type&&o.test(e.prop)&&t.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 t=0;t<p.length;t++){const o=p[t];if(!f.line.length&&"word"===o.type&&n.includes(o.value.toLowerCase())){const e=o;let r=o;for(;;){const e=p[t+1];if(!e||"word"!==e.type||!n.includes(e.value.toLowerCase()))break;r=e,t++}f.line=d.nodes.slice(d.nodes.indexOf(e),d.nodes.indexOf(r)+1);continue}if(f.line.length||"word"!==o.type||"none"!==o.value.toLowerCase())if(f.style||"word"!==o.type||!s.includes(o.value.toLowerCase()))if(f.thickness||"word"!==o.type||!l.includes(o.value.toLowerCase()))if(f.thickness||"function"!==o.type||"calc"!==o.value.toLowerCase()){if(f.color||!nodeIsAColor(o)){if("word"===o.type){let t;try{t=e.unit(o.value)}catch{return}if(!t||!t.unit)return;f.thickness=o,"%"===t.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:t.number}]});continue}return}f.color=o}else f.thickness=o;else f.thickness=o;else f.style=o;else f.line.push(o)}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())return void(hasFollowingTextDecoration(i)||i.cloneBefore({prop:"-webkit-text-decoration",value:v}));hasPreviousSameTextDecoration(i,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])}),t.set(i.value,v),t.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(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: 42 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

@@ -10,6 +10,40 @@ export type pluginOptions = {
1010

1111
const IS_TEXT_DECORATION_REGEX = /^text-decoration$/i;
1212

13+
function hasFollowingTextDecoration(decl: Declaration): boolean {
14+
let current = decl.next();
15+
16+
while (current) {
17+
if (current.type === 'comment') {
18+
current = current.next();
19+
continue;
20+
}
21+
22+
return current.type === 'decl' && IS_TEXT_DECORATION_REGEX.test(current.prop);
23+
}
24+
25+
return false;
26+
}
27+
28+
function hasPreviousSameTextDecoration(decl: Declaration, valueToCheck: string): boolean {
29+
let current = decl.prev();
30+
31+
while (current) {
32+
if (current.type === 'comment') {
33+
current = current.prev();
34+
continue;
35+
}
36+
37+
if (current.type !== 'decl') {
38+
return false;
39+
}
40+
41+
return IS_TEXT_DECORATION_REGEX.test(current.prop) &&
42+
current.value.toLowerCase() === valueToCheck.toLowerCase();
43+
}
44+
45+
return false;
46+
}
1347
const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
1448
const options = Object.assign(
1549
// Default options
@@ -192,8 +226,7 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
192226

193227
const nonShortHandValue = valueParser.stringify(data.line);
194228
if (decl.value.toLowerCase() === nonShortHandValue.toLowerCase()) {
195-
const next = decl.next();
196-
if (!next || next.type !== 'decl' || next.prop.toLowerCase() !== 'text-decoration') {
229+
if (!hasFollowingTextDecoration(decl)) {
197230

198231
// "-webkit-text-decoration" is a shorthand and sets omitted constituent properties to their initial value.
199232
// "text-decoration" is a longhand in older browsers and does not have this behavior.
@@ -206,10 +239,12 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
206239
return;
207240
}
208241

209-
decl.cloneBefore({
210-
prop: 'text-decoration',
211-
value: nonShortHandValue,
212-
});
242+
if (!hasPreviousSameTextDecoration(decl, nonShortHandValue)) {
243+
decl.cloneBefore({
244+
prop: 'text-decoration',
245+
value: nonShortHandValue,
246+
});
247+
}
213248

214249
const shortHandValue = valueParser.stringify([
215250
...data.line,

plugins/postcss-text-decoration-shorthand/test/_tape.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ postcssTape(plugin)({
3232
}),
3333
],
3434
},
35+
'multiple-declarations:autoprefixer': {
36+
message: 'supports basic usage with autoprefixer',
37+
plugins: [
38+
plugin(),
39+
autoprefixer({
40+
overrideBrowserslist: ['Safari >= 8'],
41+
}),
42+
],
43+
},
3544
'examples/example': {
3645
message: 'minimal example',
3746
},
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.line-only-then-shorthand {
2+
text-decoration: underline; /* 2 */
3+
-webkit-text-decoration: underline dotted;
4+
text-decoration: underline dotted;
5+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.line-only-then-shorthand {
2+
text-decoration: underline; /* 2 */
3+
text-decoration: underline dotted;
4+
}

0 commit comments

Comments
 (0)