1
- import { Doc , doc , FastPath , ParserOptions } from 'prettier' ;
1
+ import { Doc , doc , FastPath , Options } from 'prettier' ;
2
2
import { getText } from './lib/getText' ;
3
3
import { snippedTagContentAttribute } from './lib/snipTagContent' ;
4
- import { isBracketSameLine } from './options' ;
4
+ import { isBracketSameLine , ParserOptions } from './options' ;
5
5
import { PrintFn } from './print' ;
6
6
import { isLine , removeParentheses , trimRight } from './print/doc-helpers' ;
7
- import { groupConcat , printWithPrependedAttributeLine } from './print/helpers' ;
7
+ import { isASTNode , printWithPrependedAttributeLine } from './print/helpers' ;
8
8
import {
9
+ assignCommentsToNodes ,
9
10
getAttributeTextValue ,
10
11
getLeadingComment ,
11
12
isIgnoreDirective ,
13
+ isInsideQuotedAttribute ,
12
14
isNodeSupportedLanguage ,
13
15
isPugTemplate ,
14
16
isTypeScript ,
15
17
printRaw ,
16
18
} from './print/node-helpers' ;
17
19
import { CommentNode , ElementNode , Node , ScriptNode , StyleNode } from './print/nodes' ;
20
+ import { extractAttributes } from './lib/extractAttributes' ;
18
21
19
22
const {
20
- builders : { concat , hardline, softline, indent, dedent, literalline } ,
23
+ builders : { group , hardline, softline, indent, dedent, literalline } ,
21
24
utils : { removeLines } ,
22
25
} = doc ;
23
26
24
- export function embed (
25
- path : FastPath ,
26
- print : PrintFn ,
27
- textToDoc : ( text : string , options : object ) => Doc ,
28
- options : ParserOptions ,
29
- ) : Doc | null {
27
+ // Embed works like this in Prettier v3:
28
+ // - do depth first traversal of all node properties
29
+ // - deepest property is calling embed first
30
+ // - if embed returns a function, it will be called after the traversal in a second pass, in the same order (deepest first)
31
+ // For performance reasons we try to only return functions when we're sure we need to transform something.
32
+ export function embed ( path : FastPath , _options : Options ) {
30
33
const node : Node = path . getNode ( ) ;
34
+ const options = _options as ParserOptions ;
35
+ if ( ! options . locStart || ! options . locEnd || ! options . originalText ) {
36
+ throw new Error ( 'Missing required options' ) ;
37
+ }
38
+
39
+ if ( isASTNode ( node ) ) {
40
+ assignCommentsToNodes ( node ) ;
41
+ if ( node . module ) {
42
+ node . module . type = 'Script' ;
43
+ node . module . attributes = extractAttributes ( getText ( node . module , options ) ) ;
44
+ }
45
+ if ( node . instance ) {
46
+ node . instance . type = 'Script' ;
47
+ node . instance . attributes = extractAttributes ( getText ( node . instance , options ) ) ;
48
+ }
49
+ if ( node . css ) {
50
+ node . css . type = 'Style' ;
51
+ node . css . content . type = 'StyleProgram' ;
52
+ }
53
+ return null ;
54
+ }
55
+
56
+ // embed does depth first traversal with deepest node called first, therefore we need to
57
+ // check the parent to see if we are inside an expression that should be embedded.
58
+ const parent = path . getParentNode ( ) ;
59
+ const printJsExpression = ( ) =>
60
+ ( parent as any ) . expression
61
+ ? printJS ( parent , options . svelteStrictMode ?? false , false , false , 'expression' )
62
+ : undefined ;
63
+ const printSvelteBlockJS = ( name : string ) => printJS ( parent , false , true , false , name ) ;
64
+
65
+ switch ( parent . type ) {
66
+ case 'IfBlock' :
67
+ case 'ElseBlock' :
68
+ case 'AwaitBlock' :
69
+ case 'KeyBlock' :
70
+ printSvelteBlockJS ( 'expression' ) ;
71
+ break ;
72
+ case 'EachBlock' :
73
+ printSvelteBlockJS ( 'expression' ) ;
74
+ printSvelteBlockJS ( 'key' ) ;
75
+ break ;
76
+ case 'Element' :
77
+ printJS ( parent , options . svelteStrictMode ?? false , false , false , 'tag' ) ;
78
+ break ;
79
+ case 'MustacheTag' :
80
+ printJS ( parent , isInsideQuotedAttribute ( path , options ) , false , false , 'expression' ) ;
81
+ break ;
82
+ case 'RawMustacheTag' :
83
+ printJS ( parent , false , false , false , 'expression' ) ;
84
+ break ;
85
+ case 'Spread' :
86
+ printJS ( parent , false , false , false , 'expression' ) ;
87
+ break ;
88
+ case 'ConstTag' :
89
+ printJS ( parent , false , false , true , 'expression' ) ;
90
+ break ;
91
+ case 'EventHandler' :
92
+ case 'Binding' :
93
+ case 'Class' :
94
+ case 'Let' :
95
+ case 'Transition' :
96
+ case 'Action' :
97
+ case 'Animation' :
98
+ case 'InlineComponent' :
99
+ printJsExpression ( ) ;
100
+ break ;
101
+ }
31
102
32
103
if ( node . isJS ) {
33
- try {
34
- const embeddedOptions : any = {
35
- parser : expressionParser ,
36
- } ;
37
- if ( node . forceSingleQuote ) {
38
- embeddedOptions . singleQuote = true ;
39
- }
104
+ return async (
105
+ textToDoc : ( text : string , options : Options ) => Promise < Doc > ,
106
+ ) : Promise < Doc > => {
107
+ try {
108
+ const embeddedOptions = {
109
+ // Prettier only allows string references as parsers from v3 onwards,
110
+ // so we need to have another public parser and defer to that
111
+ parser : 'svelteExpressionParser' ,
112
+ singleQuote : node . forceSingleQuote ? true : options . singleQuote ,
113
+ } ;
40
114
41
- let docs = textToDoc (
42
- forceIntoExpression (
43
- // If we have snipped content, it was done wrongly and we need to unsnip it.
44
- // This happens for example for {@html `<script>{foo}</script>` }
45
- getText ( node , options , true ) ,
46
- ) ,
47
- embeddedOptions ,
48
- ) ;
49
- if ( node . forceSingleLine ) {
50
- docs = removeLines ( docs ) ;
51
- }
52
- if ( node . removeParentheses ) {
53
- docs = removeParentheses ( docs ) ;
115
+ let docs = await textToDoc (
116
+ forceIntoExpression (
117
+ // If we have snipped content, it was done wrongly and we need to unsnip it.
118
+ // This happens for example for {@html `<script>{foo}</script>` }
119
+ getText ( node , options , true ) ,
120
+ ) ,
121
+ embeddedOptions ,
122
+ ) ;
123
+ if ( node . forceSingleLine ) {
124
+ docs = removeLines ( docs ) ;
125
+ }
126
+ if ( node . removeParentheses ) {
127
+ docs = removeParentheses ( docs ) ;
128
+ }
129
+ return docs ;
130
+ } catch ( e ) {
131
+ return getText ( node , options , true ) ;
54
132
}
55
- return docs ;
56
- } catch ( e ) {
57
- return getText ( node , options , true ) ;
58
- }
133
+ } ;
59
134
}
60
135
61
136
const embedType = (
62
137
tag : 'script' | 'style' | 'template' ,
63
138
parser : 'typescript' | 'babel-ts' | 'css' | 'pug' ,
64
139
isTopLevel : boolean ,
65
- ) =>
66
- embedTag (
67
- tag ,
68
- options . originalText ,
69
- path ,
70
- ( content ) => formatBodyContent ( content , parser , textToDoc , options ) ,
71
- print ,
72
- isTopLevel ,
73
- options ,
74
- ) ;
140
+ ) => {
141
+ return async (
142
+ textToDoc : ( text : string , options : Options ) => Promise < Doc > ,
143
+ print : PrintFn ,
144
+ ) : Promise < Doc > => {
145
+ return embedTag (
146
+ tag ,
147
+ options . originalText ,
148
+ path ,
149
+ ( content ) => formatBodyContent ( content , parser , textToDoc , options ) ,
150
+ print ,
151
+ isTopLevel ,
152
+ options ,
153
+ ) ;
154
+ } ;
155
+ } ;
75
156
76
157
const embedScript = ( isTopLevel : boolean ) =>
77
158
embedType (
@@ -111,19 +192,17 @@ function forceIntoExpression(statement: string) {
111
192
return `(${ statement } \n)` ;
112
193
}
113
194
114
- function expressionParser ( text : string , parsers : any , options : any ) {
115
- const ast = parsers . babel ( text , parsers , options ) ;
116
-
117
- return { ...ast , program : ast . program . body [ 0 ] . expression } ;
118
- }
119
-
120
195
function preformattedBody ( str : string ) : Doc {
196
+ if ( ! str ) {
197
+ return '' ;
198
+ }
199
+
121
200
const firstNewline = / ^ [ \t \f \r ] * \n / ;
122
201
const lastNewline = / \n [ \t \f \r ] * $ / ;
123
202
124
203
// If we do not start with a new line prettier might try to break the opening tag
125
204
// to keep it together with the string. Use a literal line to skip indentation.
126
- return concat ( [ literalline , str . replace ( firstNewline , '' ) . replace ( lastNewline , '' ) , hardline ] ) ;
205
+ return [ literalline , str . replace ( firstNewline , '' ) . replace ( lastNewline , '' ) , hardline ] ;
127
206
}
128
207
129
208
function getSnippedContent ( node : Node ) {
@@ -136,14 +215,14 @@ function getSnippedContent(node: Node) {
136
215
}
137
216
}
138
217
139
- function formatBodyContent (
218
+ async function formatBodyContent (
140
219
content : string ,
141
220
parser : 'typescript' | 'babel-ts' | 'css' | 'pug' ,
142
- textToDoc : ( text : string , options : object ) => Doc ,
221
+ textToDoc : ( text : string , options : object ) => Promise < Doc > ,
143
222
options : ParserOptions & { pugTabWidth ?: number } ,
144
223
) {
145
224
try {
146
- const body = textToDoc ( content , { parser } ) ;
225
+ const body = await textToDoc ( content , { parser } ) ;
147
226
148
227
if ( parser === 'pug' && typeof body === 'string' ) {
149
228
// Pug returns no docs but a final string.
@@ -159,13 +238,13 @@ function formatBodyContent(
159
238
. split ( '\n' )
160
239
. map ( ( line ) => ( line ? whitespace + line : line ) )
161
240
. join ( '\n' ) ;
162
- return concat ( [ hardline , pugBody ] ) ;
241
+ return [ hardline , pugBody ] ;
163
242
}
164
243
165
244
const indentIfDesired = ( doc : Doc ) =>
166
245
options . svelteIndentScriptAndStyle ? indent ( doc ) : doc ;
167
246
trimRight ( [ body ] , isLine ) ;
168
- return concat ( [ indentIfDesired ( concat ( [ hardline , body ] ) ) , hardline ] ) ;
247
+ return [ indentIfDesired ( [ hardline , body ] ) , hardline ] ;
169
248
} catch ( error ) {
170
249
if ( process . env . PRETTIER_DEBUG ) {
171
250
throw error ;
@@ -181,11 +260,11 @@ function formatBodyContent(
181
260
}
182
261
}
183
262
184
- function embedTag (
263
+ async function embedTag (
185
264
tag : 'script' | 'style' | 'template' ,
186
265
text : string ,
187
266
path : FastPath ,
188
- formatBodyContent : ( content : string ) => Doc ,
267
+ formatBodyContent : ( content : string ) => Promise < Doc > ,
189
268
print : PrintFn ,
190
269
isTopLevel : boolean ,
191
270
options : ParserOptions ,
@@ -209,24 +288,24 @@ function embedTag(
209
288
) ) ;
210
289
const body : Doc = canFormat
211
290
? content . trim ( ) !== ''
212
- ? formatBodyContent ( content )
291
+ ? await formatBodyContent ( content )
213
292
: content === ''
214
293
? ''
215
294
: hardline
216
295
: preformattedBody ( content ) ;
217
296
218
- const openingTag = groupConcat ( [
297
+ const openingTag = group ( [
219
298
'<' ,
220
299
tag ,
221
300
indent (
222
- groupConcat ( [
301
+ group ( [
223
302
...path . map ( printWithPrependedAttributeLine ( node , options , print ) , 'attributes' ) ,
224
303
isBracketSameLine ( options ) ? '' : dedent ( softline ) ,
225
304
] ) ,
226
305
) ,
227
306
'>' ,
228
307
] ) ;
229
- let result = groupConcat ( [ openingTag , body , '</' , tag , '>' ] ) ;
308
+ let result : Doc = group ( [ openingTag , body , '</' , tag , '>' ] ) ;
230
309
231
310
const comments = [ ] ;
232
311
for ( const comment of previousComments ) {
@@ -241,8 +320,24 @@ function embedTag(
241
320
// top level embedded nodes have been moved from their normal position in the
242
321
// node tree. if there is a comment referring to it, it must be recreated at
243
322
// the new position.
244
- return concat ( [ ...comments , result , hardline ] ) ;
323
+ return [ ...comments , result , hardline ] ;
245
324
} else {
246
- return comments . length ? concat ( [ ...comments , result ] ) : result ;
325
+ return comments . length ? [ ...comments , result ] : result ;
326
+ }
327
+ }
328
+
329
+ function printJS (
330
+ node : any ,
331
+ forceSingleQuote : boolean ,
332
+ forceSingleLine : boolean ,
333
+ removeParentheses : boolean ,
334
+ name : string ,
335
+ ) {
336
+ if ( ! node [ name ] ) {
337
+ return ;
247
338
}
339
+ node [ name ] . isJS = true ;
340
+ node [ name ] . forceSingleQuote = forceSingleQuote ;
341
+ node [ name ] . forceSingleLine = forceSingleLine ;
342
+ node [ name ] . removeParentheses = removeParentheses ;
248
343
}
0 commit comments