1
1
import type { AST } from "svelte-eslint-parser"
2
+ import type * as ESTree from "estree"
3
+ import type { Root } from "postcss"
2
4
import { parse as parseCss } from "postcss"
3
5
import { createRule } from "../utils"
4
6
@@ -11,6 +13,13 @@ function safeParseCss(cssCode: string) {
11
13
}
12
14
}
13
15
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
+
14
23
export default createRule ( "prefer-style-directive" , {
15
24
meta : {
16
25
docs : {
@@ -27,6 +36,179 @@ export default createRule("prefer-style-directive", {
27
36
} ,
28
37
create ( context ) {
29
38
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
+
30
212
return {
31
213
"SvelteStartTag > SvelteAttribute" (
32
214
node : AST . SvelteAttribute & {
@@ -37,9 +219,8 @@ export default createRule("prefer-style-directive", {
37
219
return
38
220
}
39
221
const mustacheTags = node . value . filter (
40
- ( v ) => v . type === "SvelteMustacheTag" ,
222
+ ( v ) : v is AST . SvelteMustacheTagText => v . type === "SvelteMustacheTag" ,
41
223
)
42
- const valueStartIndex = node . value [ 0 ] . range [ 0 ]
43
224
const cssCode = node . value
44
225
. map ( ( value ) => {
45
226
if ( value . type === "SvelteMustacheTag" ) {
@@ -49,61 +230,11 @@ export default createRule("prefer-style-directive", {
49
230
} )
50
231
. join ( "" )
51
232
const root = safeParseCss ( cssCode )
52
- if ( ! root ) {
53
- return
233
+ if ( root ) {
234
+ processStyleValue ( node , root , mustacheTags )
235
+ } else {
236
+ processMustacheTags ( mustacheTags , node )
54
237
}
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
- } )
107
238
} ,
108
239
}
109
240
} ,
0 commit comments