11import type { AST } from "svelte-eslint-parser"
2+ import type * as ESTree from "estree"
3+ import type { Root } from "postcss"
24import { parse as parseCss } from "postcss"
35import { createRule } from "../utils"
46
@@ -11,6 +13,13 @@ function safeParseCss(cssCode: string) {
1113 }
1214}
1315
16+ /** Checks wether the given node is string literal or not */
17+ function isStringLiteral (
18+ node : ESTree . Expression ,
19+ ) : node is ESTree . Literal & { value : string } {
20+ return node . type === "Literal" && typeof node . value === "string"
21+ }
22+
1423export default createRule ( "prefer-style-directive" , {
1524 meta : {
1625 docs : {
@@ -27,6 +36,179 @@ export default createRule("prefer-style-directive", {
2736 } ,
2837 create ( context ) {
2938 const sourceCode = context . getSourceCode ( )
39+
40+ /**
41+ * Process for `style=" ... "`
42+ */
43+ function processStyleValue (
44+ node : AST . SvelteAttribute ,
45+ root : Root ,
46+ mustacheTags : AST . SvelteMustacheTagText [ ] ,
47+ ) {
48+ const valueStartIndex = node . value [ 0 ] . range [ 0 ]
49+
50+ root . walkDecls ( ( decl ) => {
51+ if (
52+ node . parent . attributes . some (
53+ ( attr ) =>
54+ attr . type === "SvelteStyleDirective" &&
55+ attr . key . name . name === decl . prop ,
56+ )
57+ ) {
58+ // has style directive
59+ return
60+ }
61+
62+ const declRange : AST . Range = [
63+ valueStartIndex + decl . source ! . start ! . offset ,
64+ valueStartIndex + decl . source ! . end ! . offset + 1 ,
65+ ]
66+ if (
67+ mustacheTags . some (
68+ ( tag ) =>
69+ ( tag . range [ 0 ] < declRange [ 0 ] && declRange [ 0 ] < tag . range [ 1 ] ) ||
70+ ( tag . range [ 0 ] < declRange [ 1 ] && declRange [ 1 ] < tag . range [ 1 ] ) ,
71+ )
72+ ) {
73+ // intersection
74+ return
75+ }
76+ const declValueStartIndex =
77+ declRange [ 0 ] + decl . prop . length + ( decl . raws . between || "" ) . length
78+ const declValueRange : AST . Range = [
79+ declValueStartIndex ,
80+ declValueStartIndex + ( decl . raws . value ?. value || decl . value ) . length ,
81+ ]
82+
83+ context . report ( {
84+ node,
85+ messageId : "unexpected" ,
86+ * fix ( fixer ) {
87+ const styleDirective = `style:${ decl . prop } ="${ sourceCode . text . slice (
88+ ...declValueRange ,
89+ ) } "`
90+ if ( root . nodes . length === 1 && root . nodes [ 0 ] === decl ) {
91+ yield fixer . replaceTextRange ( node . range , styleDirective )
92+ } else {
93+ yield fixer . removeRange ( declRange )
94+ yield fixer . insertTextAfterRange ( node . range , ` ${ styleDirective } ` )
95+ }
96+ } ,
97+ } )
98+ } )
99+ }
100+
101+ /**
102+ * Process for `style="{a ? 'color: red;': ''}"`
103+ */
104+ function processMustacheTags (
105+ mustacheTags : AST . SvelteMustacheTagText [ ] ,
106+ attrNode : AST . SvelteAttribute ,
107+ ) {
108+ for ( const mustacheTag of mustacheTags ) {
109+ processMustacheTag ( mustacheTag , attrNode )
110+ }
111+ }
112+
113+ /**
114+ * Process for `style="{a ? 'color: red;': ''}"`
115+ */
116+ function processMustacheTag (
117+ mustacheTag : AST . SvelteMustacheTagText ,
118+ attrNode : AST . SvelteAttribute ,
119+ ) {
120+ const node = mustacheTag . expression
121+
122+ if ( node . type !== "ConditionalExpression" ) {
123+ return
124+ }
125+ if (
126+ ! isStringLiteral ( node . consequent ) ||
127+ ! isStringLiteral ( node . alternate )
128+ ) {
129+ return
130+ }
131+ if ( node . consequent . value && node . alternate . value ) {
132+ // e.g. t ? 'top: 20px' : 'left: 30px'
133+ return
134+ }
135+ const positive = node . alternate . value === ""
136+ const root = safeParseCss (
137+ positive ? node . consequent . value : node . alternate . value ,
138+ )
139+ if ( ! root || root . nodes . length !== 1 ) {
140+ return
141+ }
142+ const decl = root . nodes [ 0 ]
143+ if ( decl . type !== "decl" ) {
144+ return
145+ }
146+ if (
147+ attrNode . parent . attributes . some (
148+ ( attr ) =>
149+ attr . type === "SvelteStyleDirective" &&
150+ attr . key . name . name === decl . prop ,
151+ )
152+ ) {
153+ // has style directive
154+ return
155+ }
156+
157+ context . report ( {
158+ node,
159+ messageId : "unexpected" ,
160+ * fix ( fixer ) {
161+ let valueText = sourceCode . text . slice (
162+ node . test . range ! [ 0 ] ,
163+ node . consequent . range ! [ 0 ] ,
164+ )
165+ if ( positive ) {
166+ valueText +=
167+ sourceCode . text [ node . consequent . range ! [ 0 ] ] +
168+ decl . value +
169+ sourceCode . text [ node . consequent . range ! [ 1 ] - 1 ]
170+ } else {
171+ valueText += "null"
172+ }
173+ valueText += sourceCode . text . slice (
174+ node . consequent . range ! [ 1 ] ,
175+ node . alternate . range ! [ 0 ] ,
176+ )
177+ if ( positive ) {
178+ valueText += "null"
179+ } else {
180+ valueText +=
181+ sourceCode . text [ node . alternate . range ! [ 0 ] ] +
182+ decl . value +
183+ sourceCode . text [ node . alternate . range ! [ 1 ] - 1 ]
184+ }
185+ const styleDirective = `style:${ decl . prop } ={${ valueText } }`
186+ if (
187+ attrNode . value
188+ . filter ( ( v ) => v !== mustacheTag )
189+ . every ( ( v ) => v . type === "SvelteLiteral" && ! v . value . trim ( ) )
190+ ) {
191+ yield fixer . replaceTextRange ( attrNode . range , styleDirective )
192+ } else {
193+ const first = attrNode . value [ 0 ]
194+ if ( first !== mustacheTag ) {
195+ yield fixer . replaceTextRange (
196+ [ first . range [ 0 ] , mustacheTag . range [ 0 ] ] ,
197+ sourceCode . text
198+ . slice ( first . range [ 0 ] , mustacheTag . range [ 0 ] )
199+ . trimEnd ( ) ,
200+ )
201+ }
202+ yield fixer . removeRange ( mustacheTag . range )
203+ yield fixer . insertTextAfterRange (
204+ attrNode . range ,
205+ ` ${ styleDirective } ` ,
206+ )
207+ }
208+ } ,
209+ } )
210+ }
211+
30212 return {
31213 "SvelteStartTag > SvelteAttribute" (
32214 node : AST . SvelteAttribute & {
@@ -37,9 +219,8 @@ export default createRule("prefer-style-directive", {
37219 return
38220 }
39221 const mustacheTags = node . value . filter (
40- ( v ) => v . type === "SvelteMustacheTag" ,
222+ ( v ) : v is AST . SvelteMustacheTagText => v . type === "SvelteMustacheTag" ,
41223 )
42- const valueStartIndex = node . value [ 0 ] . range [ 0 ]
43224 const cssCode = node . value
44225 . map ( ( value ) => {
45226 if ( value . type === "SvelteMustacheTag" ) {
@@ -49,61 +230,11 @@ export default createRule("prefer-style-directive", {
49230 } )
50231 . join ( "" )
51232 const root = safeParseCss ( cssCode )
52- if ( ! root ) {
53- return
233+ if ( root ) {
234+ processStyleValue ( node , root , mustacheTags )
235+ } else {
236+ processMustacheTags ( mustacheTags , node )
54237 }
55- root . walkDecls ( ( decl ) => {
56- if (
57- node . parent . attributes . some (
58- ( attr ) =>
59- attr . type === "SvelteStyleDirective" &&
60- attr . key . name . name === decl . prop ,
61- )
62- ) {
63- // has style directive
64- return
65- }
66-
67- const declRange : AST . Range = [
68- valueStartIndex + decl . source ! . start ! . offset ,
69- valueStartIndex + decl . source ! . end ! . offset + 1 ,
70- ]
71- if (
72- mustacheTags . some (
73- ( tag ) =>
74- ( tag . range [ 0 ] < declRange [ 0 ] && declRange [ 0 ] < tag . range [ 1 ] ) ||
75- ( tag . range [ 0 ] < declRange [ 1 ] && declRange [ 1 ] < tag . range [ 1 ] ) ,
76- )
77- ) {
78- // intersection
79- return
80- }
81- const declValueStartIndex =
82- declRange [ 0 ] + decl . prop . length + ( decl . raws . between || "" ) . length
83- const declValueRange : AST . Range = [
84- declValueStartIndex ,
85- declValueStartIndex + ( decl . raws . value ?. value || decl . value ) . length ,
86- ]
87-
88- context . report ( {
89- node,
90- messageId : "unexpected" ,
91- * fix ( fixer ) {
92- const styleDirective = `style:${
93- decl . prop
94- } ="${ sourceCode . text . slice ( ...declValueRange ) } "`
95- if ( root . nodes . length === 1 && root . nodes [ 0 ] === decl ) {
96- yield fixer . replaceTextRange ( node . range , styleDirective )
97- } else {
98- yield fixer . removeRange ( declRange )
99- yield fixer . insertTextAfterRange (
100- node . range ,
101- ` ${ styleDirective } ` ,
102- )
103- }
104- } ,
105- } )
106- } )
107238 } ,
108239 }
109240 } ,
0 commit comments