1
1
import {
2
+ BlockMapping ,
2
3
DefaultBlockSchema ,
3
4
mapTableCell ,
4
5
pageBreakSchema ,
5
6
StyledText ,
6
7
} from "@blocknote/core" ;
7
- import { BlockMapping } from "@blocknote/core/src/exporter/mapping.js" ;
8
8
import {
9
9
CodeBlock ,
10
10
dracula ,
@@ -15,27 +15,168 @@ import {
15
15
Text ,
16
16
} from "@react-email/components" ;
17
17
18
- export const reactEmailBlockMappingForDefaultSchema : BlockMapping <
18
+ // Define TextProps type based on React Email Text component
19
+ type TextProps = React . ComponentPropsWithoutRef < typeof Text > ;
20
+
21
+ // Define the styles interface for configurable Text components
22
+ export interface ReactEmailTextStyles {
23
+ paragraph ?: Partial < TextProps > ;
24
+ bulletListItem ?: Partial < TextProps > ;
25
+ toggleListItem ?: Partial < TextProps > ;
26
+ numberedListItem ?: Partial < TextProps > ;
27
+ checkListItem ?: Partial < TextProps > ;
28
+ quote ?: Partial < TextProps > ;
29
+ tableError ?: Partial < TextProps > ;
30
+ tableCell ?: Partial < TextProps > ;
31
+ caption ?: Partial < TextProps > ;
32
+ heading1 ?: Partial < TextProps > ;
33
+ heading2 ?: Partial < TextProps > ;
34
+ heading3 ?: Partial < TextProps > ;
35
+ heading4 ?: Partial < TextProps > ;
36
+ heading5 ?: Partial < TextProps > ;
37
+ heading6 ?: Partial < TextProps > ;
38
+ codeBlock ?: Partial < React . ComponentProps < typeof CodeBlock > > ;
39
+ }
40
+
41
+ const defaultTextStyle : TextProps [ "style" ] = {
42
+ fontSize : 16 ,
43
+ lineHeight : 1.5 ,
44
+ margin : 3 ,
45
+ } ;
46
+
47
+ // Default styles for Text components
48
+ export const defaultReactEmailTextStyles = {
49
+ paragraph : {
50
+ style : defaultTextStyle ,
51
+ } ,
52
+ bulletListItem : {
53
+ style : defaultTextStyle ,
54
+ } ,
55
+ toggleListItem : {
56
+ style : defaultTextStyle ,
57
+ } ,
58
+ numberedListItem : {
59
+ style : defaultTextStyle ,
60
+ } ,
61
+ checkListItem : {
62
+ style : defaultTextStyle ,
63
+ } ,
64
+ quote : {
65
+ style : defaultTextStyle ,
66
+ } ,
67
+ tableError : {
68
+ style : defaultTextStyle ,
69
+ } ,
70
+ tableCell : {
71
+ style : defaultTextStyle ,
72
+ } ,
73
+ caption : {
74
+ style : defaultTextStyle ,
75
+ } ,
76
+ heading1 : {
77
+ style : {
78
+ fontSize : 48 ,
79
+ margin : 3 ,
80
+ } ,
81
+ } ,
82
+ heading2 : {
83
+ style : {
84
+ fontSize : 36 ,
85
+ margin : 3 ,
86
+ } ,
87
+ } ,
88
+ heading3 : {
89
+ style : {
90
+ fontSize : 24 ,
91
+ margin : 3 ,
92
+ } ,
93
+ } ,
94
+ heading4 : {
95
+ style : {
96
+ fontSize : 20 ,
97
+ margin : 3 ,
98
+ } ,
99
+ } ,
100
+ heading5 : {
101
+ style : {
102
+ fontSize : 18 ,
103
+ margin : 3 ,
104
+ } ,
105
+ } ,
106
+ heading6 : {
107
+ style : {
108
+ fontSize : 16 ,
109
+ margin : 3 ,
110
+ } ,
111
+ } ,
112
+ codeBlock : {
113
+ style : defaultTextStyle ,
114
+ } ,
115
+ } satisfies ReactEmailTextStyles ;
116
+
117
+ export const createReactEmailBlockMappingForDefaultSchema = (
118
+ textStyles : ReactEmailTextStyles = defaultReactEmailTextStyles ,
119
+ ) : BlockMapping <
19
120
DefaultBlockSchema & typeof pageBreakSchema . blockSchema ,
20
121
any ,
21
122
any ,
22
123
React . ReactElement < any > ,
23
124
React . ReactElement < typeof Link > | React . ReactElement < HTMLSpanElement >
24
- > = {
125
+ > => ( {
25
126
paragraph : ( block , t ) => {
26
- return < Text > { t . transformInlineContent ( block . content ) } </ Text > ;
127
+ return (
128
+ < Text
129
+ { ...textStyles . paragraph }
130
+ style = { {
131
+ ...defaultReactEmailTextStyles . paragraph . style ,
132
+ ...textStyles . paragraph ?. style ,
133
+ } }
134
+ >
135
+ { t . transformInlineContent ( block . content ) }
136
+ </ Text >
137
+ ) ;
27
138
} ,
28
139
bulletListItem : ( block , t ) => {
29
140
// Return only the <li> for grouping in the exporter
30
- return < Text > { t . transformInlineContent ( block . content ) } </ Text > ;
141
+ return (
142
+ < Text
143
+ { ...textStyles . bulletListItem }
144
+ style = { {
145
+ ...defaultReactEmailTextStyles . bulletListItem . style ,
146
+ ...textStyles . bulletListItem ?. style ,
147
+ } }
148
+ >
149
+ { t . transformInlineContent ( block . content ) }
150
+ </ Text >
151
+ ) ;
31
152
} ,
32
153
toggleListItem : ( block , t ) => {
33
154
// Return only the <li> for grouping in the exporter
34
- return < Text > { t . transformInlineContent ( block . content ) } </ Text > ;
155
+ return (
156
+ < Text
157
+ { ...textStyles . toggleListItem }
158
+ style = { {
159
+ ...defaultReactEmailTextStyles . toggleListItem . style ,
160
+ ...textStyles . toggleListItem ?. style ,
161
+ } }
162
+ >
163
+ { t . transformInlineContent ( block . content ) }
164
+ </ Text >
165
+ ) ;
35
166
} ,
36
167
numberedListItem : ( block , t , _nestingLevel ) => {
37
168
// Return only the <li> for grouping in the exporter
38
- return < Text > { t . transformInlineContent ( block . content ) } </ Text > ;
169
+ return (
170
+ < Text
171
+ { ...textStyles . numberedListItem }
172
+ style = { {
173
+ ...defaultReactEmailTextStyles . numberedListItem . style ,
174
+ ...textStyles . numberedListItem ?. style ,
175
+ } }
176
+ >
177
+ { t . transformInlineContent ( block . content ) }
178
+ </ Text >
179
+ ) ;
39
180
} ,
40
181
checkListItem : ( block , t ) => {
41
182
// Render a checkbox using inline SVG for better appearance in email
@@ -85,15 +226,28 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
85
226
</ svg >
86
227
) ;
87
228
return (
88
- < Text >
229
+ < Text
230
+ { ...textStyles . checkListItem }
231
+ style = { {
232
+ ...defaultReactEmailTextStyles . checkListItem . style ,
233
+ ...textStyles . checkListItem ?. style ,
234
+ } }
235
+ >
89
236
{ checkboxSvg }
90
237
< span > { t . transformInlineContent ( block . content ) } </ span >
91
238
</ Text >
92
239
) ;
93
240
} ,
94
241
heading : ( block , t ) => {
95
242
return (
96
- < Heading as = { `h${ block . props . level } ` } >
243
+ < Heading
244
+ as = { `h${ block . props . level } ` }
245
+ { ...textStyles [ `heading${ block . props . level } ` ] }
246
+ style = { {
247
+ ...defaultReactEmailTextStyles [ `heading${ block . props . level } ` ] . style ,
248
+ ...textStyles [ `heading${ block . props . level } ` ] ?. style ,
249
+ } }
250
+ >
97
251
{ t . transformInlineContent ( block . content ) }
98
252
</ Heading >
99
253
) ;
@@ -108,6 +262,11 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
108
262
fontFamily = "'CommitMono', monospace"
109
263
language = { block . props . language as PrismLanguage }
110
264
theme = { dracula }
265
+ { ...textStyles . codeBlock }
266
+ style = { {
267
+ ...defaultReactEmailTextStyles . codeBlock . style ,
268
+ ...textStyles . codeBlock ?. style ,
269
+ } }
111
270
/>
112
271
) ;
113
272
} ,
@@ -136,7 +295,11 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
136
295
defaultText = "Open audio file"
137
296
icon = { icon }
138
297
/>
139
- < Caption caption = { block . props . caption } width = { previewWidth } />
298
+ < Caption
299
+ caption = { block . props . caption }
300
+ width = { previewWidth }
301
+ textStyles = { textStyles }
302
+ />
140
303
</ div >
141
304
) ;
142
305
} ,
@@ -165,7 +328,11 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
165
328
defaultText = "Open video file"
166
329
icon = { icon }
167
330
/>
168
- < Caption caption = { block . props . caption } width = { previewWidth } />
331
+ < Caption
332
+ caption = { block . props . caption }
333
+ width = { previewWidth }
334
+ textStyles = { textStyles }
335
+ />
169
336
</ div >
170
337
) ;
171
338
} ,
@@ -194,7 +361,11 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
194
361
defaultText = "Open file"
195
362
icon = { icon }
196
363
/>
197
- < Caption caption = { block . props . caption } width = { previewWidth } />
364
+ < Caption
365
+ caption = { block . props . caption }
366
+ width = { previewWidth }
367
+ textStyles = { textStyles }
368
+ />
198
369
</ div >
199
370
) ;
200
371
} ,
@@ -211,7 +382,7 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
211
382
// Render table using standard HTML table elements for email compatibility
212
383
const table = block . content ;
213
384
if ( ! table || typeof table !== "object" || ! Array . isArray ( table . rows ) ) {
214
- return < Text > Table data not available</ Text > ;
385
+ return < Text { ... textStyles . tableError } > Table data not available</ Text > ;
215
386
}
216
387
const headerRowsCount = ( table . headerRows as number ) ?? 0 ;
217
388
const headerColsCount = ( table . headerCols as number ) ?? 0 ;
@@ -258,6 +429,8 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
258
429
normalizedCell . props . textColor !== "default"
259
430
? normalizedCell . props . textColor
260
431
: "inherit" ,
432
+ ...defaultReactEmailTextStyles . tableCell . style ,
433
+ ...textStyles . tableCell ?. style ,
261
434
} }
262
435
{ ...( ( normalizedCell . props . colspan || 1 ) > 1 && {
263
436
colSpan : normalizedCell . props . colspan || 1 ,
@@ -280,6 +453,7 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
280
453
// Render block quote with a left border and subtle background for email compatibility
281
454
return (
282
455
< Text
456
+ { ...textStyles . quote }
283
457
style = { {
284
458
borderLeft : "4px solid #bdbdbd" ,
285
459
background : "#f9f9f9" ,
@@ -288,6 +462,8 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
288
462
fontStyle : "italic" ,
289
463
color : "#555" ,
290
464
display : "block" ,
465
+ ...defaultReactEmailTextStyles . quote . style ,
466
+ ...textStyles . quote ?. style ,
291
467
} }
292
468
>
293
469
{ t . transformInlineContent ( block . content ) }
@@ -306,7 +482,11 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
306
482
/>
307
483
) ;
308
484
} ,
309
- } ;
485
+ } ) ;
486
+
487
+ // Export the original mapping for backward compatibility
488
+ export const reactEmailBlockMappingForDefaultSchema =
489
+ createReactEmailBlockMappingForDefaultSchema ( ) ;
310
490
311
491
// Helper for file-like blocks (audio, video, file)
312
492
function FileLink ( {
@@ -338,12 +518,30 @@ function FileLink({
338
518
) ;
339
519
}
340
520
341
- function Caption ( { caption, width } : { caption ?: string ; width ?: number } ) {
521
+ function Caption ( {
522
+ caption,
523
+ width,
524
+ textStyles,
525
+ } : {
526
+ caption ?: string ;
527
+ width ?: number ;
528
+ textStyles : ReactEmailTextStyles ;
529
+ } ) {
342
530
if ( ! caption ) {
343
531
return null ;
344
532
}
345
533
return (
346
- < Text style = { { width, fontSize : 13 , color : "#888" , margin : "4px 0 0 0" } } >
534
+ < Text
535
+ { ...textStyles . caption }
536
+ style = { {
537
+ width,
538
+ fontSize : 13 ,
539
+ color : "#888" ,
540
+ margin : "4px 0 0 0" ,
541
+ ...defaultReactEmailTextStyles . caption . style ,
542
+ ...textStyles . caption ?. style ,
543
+ } }
544
+ >
347
545
{ caption }
348
546
</ Text >
349
547
) ;
0 commit comments