@blocknote/xl-email-exporter 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,334 @@
1
+ import {
2
+ Block,
3
+ BlockNoteSchema,
4
+ BlockSchema,
5
+ COLORS_DEFAULT,
6
+ DefaultProps,
7
+ Exporter,
8
+ ExporterOptions,
9
+ InlineContentSchema,
10
+ StyleSchema,
11
+ StyledText,
12
+ } from "@blocknote/core";
13
+ import {
14
+ Body,
15
+ Container,
16
+ Head,
17
+ Html,
18
+ Link,
19
+ Preview,
20
+ Section,
21
+ Tailwind,
22
+ } from "@react-email/components";
23
+ import { render as renderEmail } from "@react-email/render";
24
+ import React, { CSSProperties } from "react";
25
+
26
+ export class ReactEmailExporter<
27
+ B extends BlockSchema,
28
+ S extends StyleSchema,
29
+ I extends InlineContentSchema,
30
+ > extends Exporter<
31
+ B,
32
+ I,
33
+ S,
34
+ React.ReactElement<any>,
35
+ React.ReactElement<typeof Link> | React.ReactElement<HTMLSpanElement>,
36
+ CSSProperties,
37
+ React.ReactElement<HTMLSpanElement>
38
+ > {
39
+ public constructor(
40
+ public readonly schema: BlockNoteSchema<B, I, S>,
41
+ mappings: Exporter<
42
+ NoInfer<B>,
43
+ NoInfer<I>,
44
+ NoInfer<S>,
45
+ React.ReactElement<any>,
46
+ React.ReactElement<typeof Link> | React.ReactElement<HTMLSpanElement>,
47
+ CSSProperties,
48
+ React.ReactElement<HTMLSpanElement>
49
+ >["mappings"],
50
+ options?: Partial<ExporterOptions>,
51
+ ) {
52
+ const defaults = {
53
+ colors: COLORS_DEFAULT,
54
+ } satisfies Partial<ExporterOptions>;
55
+
56
+ const newOptions = {
57
+ ...defaults,
58
+ ...options,
59
+ };
60
+ super(schema, mappings, newOptions);
61
+ }
62
+
63
+ public transformStyledText(styledText: StyledText<S>) {
64
+ const stylesArray = this.mapStyles(styledText.styles);
65
+ const styles = Object.assign({}, ...stylesArray);
66
+ return <span style={styles}>{styledText.text}</span>;
67
+ }
68
+
69
+ private async renderGroupedListBlocks(
70
+ blocks: Block<B, I, S>[],
71
+ startIndex: number,
72
+ nestingLevel: number,
73
+ ): Promise<{ element: React.ReactElement; nextIndex: number }> {
74
+ const listType = blocks[startIndex].type;
75
+ const listItems: React.ReactElement<any>[] = [];
76
+ let j = startIndex;
77
+
78
+ for (
79
+ let itemIndex = 1;
80
+ j < blocks.length && blocks[j].type === listType;
81
+ j++, itemIndex++
82
+ ) {
83
+ const block = blocks[j];
84
+ const liContent = (await this.mapBlock(
85
+ block as any,
86
+ nestingLevel,
87
+ itemIndex,
88
+ )) as any;
89
+ let nestedList: React.ReactElement<any>[] = [];
90
+ if (block.children && block.children.length > 0) {
91
+ nestedList = await this.renderNestedLists(
92
+ block.children,
93
+ nestingLevel + 1,
94
+ block.id,
95
+ );
96
+ }
97
+ listItems.push(
98
+ <React.Fragment key={block.id}>
99
+ {liContent}
100
+ {nestedList.length > 0 && nestedList}
101
+ </React.Fragment>,
102
+ );
103
+ }
104
+ let element: React.ReactElement;
105
+ if (listType === "bulletListItem" || listType === "toggleListItem") {
106
+ element = (
107
+ <ul className="mb-2 list-disc pl-6" key={blocks[startIndex].id + "-ul"}>
108
+ {listItems}
109
+ </ul>
110
+ );
111
+ } else {
112
+ element = (
113
+ <ol
114
+ className="mb-2 list-decimal pl-6"
115
+ start={1}
116
+ key={blocks[startIndex].id + "-ol"}
117
+ >
118
+ {listItems}
119
+ </ol>
120
+ );
121
+ }
122
+ return { element, nextIndex: j };
123
+ }
124
+
125
+ private async renderNestedLists(
126
+ children: Block<B, I, S>[],
127
+ nestingLevel: number,
128
+ parentId: string,
129
+ ): Promise<React.ReactElement<any>[]> {
130
+ const nestedList: React.ReactElement<any>[] = [];
131
+ let i = 0;
132
+ while (i < children.length) {
133
+ const child = children[i];
134
+ if (
135
+ child.type === "bulletListItem" ||
136
+ child.type === "numberedListItem"
137
+ ) {
138
+ // Group consecutive list items of the same type
139
+ const listType = child.type;
140
+ const listItems: React.ReactElement<any>[] = [];
141
+ let j = i;
142
+
143
+ for (
144
+ let itemIndex = 1;
145
+ j < children.length && children[j].type === listType;
146
+ j++, itemIndex++
147
+ ) {
148
+ const listItem = children[j];
149
+ const liContent = (await this.mapBlock(
150
+ listItem as any,
151
+ nestingLevel,
152
+ itemIndex,
153
+ )) as any;
154
+ const style = this.blocknoteDefaultPropsToReactEmailStyle(
155
+ listItem.props as any,
156
+ );
157
+ let nestedContent: React.ReactElement<any>[] = [];
158
+ if (listItem.children && listItem.children.length > 0) {
159
+ // If children are list items, render as nested list; otherwise, as normal blocks
160
+ if (
161
+ listItem.children[0] &&
162
+ (listItem.children[0].type === "bulletListItem" ||
163
+ listItem.children[0].type === "numberedListItem")
164
+ ) {
165
+ nestedContent = await this.renderNestedLists(
166
+ listItem.children,
167
+ nestingLevel + 1,
168
+ listItem.id,
169
+ );
170
+ } else {
171
+ nestedContent = await this.transformBlocks(
172
+ listItem.children,
173
+ nestingLevel + 1,
174
+ );
175
+ }
176
+ }
177
+ listItems.push(
178
+ <li key={listItem.id} style={style}>
179
+ {liContent}
180
+ {nestedContent.length > 0 && (
181
+ <div style={{ marginTop: "8px" }}>{nestedContent}</div>
182
+ )}
183
+ </li>,
184
+ );
185
+ }
186
+ if (listType === "bulletListItem") {
187
+ nestedList.push(
188
+ <ul
189
+ className="mb-2 list-disc pl-6"
190
+ key={parentId + "-ul-nested-" + i}
191
+ >
192
+ {listItems}
193
+ </ul>,
194
+ );
195
+ } else {
196
+ nestedList.push(
197
+ <ol
198
+ className="mb-2 list-decimal pl-6"
199
+ start={1}
200
+ key={parentId + "-ol-nested-" + i}
201
+ >
202
+ {listItems}
203
+ </ol>,
204
+ );
205
+ }
206
+ i = j;
207
+ } else {
208
+ // Non-list child, render as normal with indentation
209
+ const childBlocks = await this.transformBlocks([child], nestingLevel);
210
+ nestedList.push(
211
+ <Section key={child.id} style={{ marginLeft: "24px" }}>
212
+ {childBlocks}
213
+ </Section>,
214
+ );
215
+ i++;
216
+ }
217
+ }
218
+ return nestedList;
219
+ }
220
+
221
+ public async transformBlocks(
222
+ blocks: Block<B, I, S>[],
223
+ nestingLevel = 0,
224
+ ): Promise<React.ReactElement<any>[]> {
225
+ const ret: React.ReactElement<any>[] = [];
226
+ let i = 0;
227
+ while (i < blocks.length) {
228
+ const b = blocks[i];
229
+ if (b.type === "bulletListItem" || b.type === "numberedListItem") {
230
+ const { element, nextIndex } = await this.renderGroupedListBlocks(
231
+ blocks,
232
+ i,
233
+ nestingLevel,
234
+ );
235
+ ret.push(element);
236
+ i = nextIndex;
237
+ continue;
238
+ }
239
+ // Non-list blocks
240
+ const children = await this.transformBlocks(b.children, nestingLevel + 1);
241
+ const self = (await this.mapBlock(b as any, nestingLevel, 0)) as any;
242
+ const style = this.blocknoteDefaultPropsToReactEmailStyle(b.props as any);
243
+ ret.push(
244
+ <React.Fragment key={b.id}>
245
+ <Section style={style}>{self}</Section>
246
+ {children.length > 0 && (
247
+ <div style={{ marginLeft: "24px" }}>{children}</div>
248
+ )}
249
+ </React.Fragment>,
250
+ );
251
+ i++;
252
+ }
253
+ return ret;
254
+ }
255
+
256
+ public async toReactEmailDocument(
257
+ blocks: Block<B, I, S>[],
258
+ options?: {
259
+ /**
260
+ * Inject elements into the {@link Head} element
261
+ * @see https://react.email/docs/components/head
262
+ */
263
+ head?: React.ReactElement;
264
+ /**
265
+ * Set the preview text for the email
266
+ * @see https://react.email/docs/components/preview
267
+ */
268
+ preview?: string | string[];
269
+ /**
270
+ * Add a header to every page.
271
+ * The React component passed must be a React-Email component
272
+ * @see https://react.email/components
273
+ */
274
+ header?: React.ReactElement;
275
+ /**
276
+ * Add a footer to every page.
277
+ * The React component passed must be a React-Email component
278
+ * @see https://react.email/components
279
+ */
280
+ footer?: React.ReactElement;
281
+ },
282
+ ) {
283
+ const transformedBlocks = await this.transformBlocks(blocks);
284
+ return renderEmail(
285
+ <Html>
286
+ <Head>{options?.head}</Head>
287
+ <Body
288
+ style={{
289
+ fontFamily:
290
+ "'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif",
291
+ fontSize: "16px",
292
+ lineHeight: "1.5",
293
+ color: "#333",
294
+ }}
295
+ >
296
+ {options?.preview && <Preview>{options.preview}</Preview>}
297
+ <Tailwind>
298
+ <Container>
299
+ {options?.header}
300
+ {transformedBlocks}
301
+ {options?.footer}
302
+ </Container>
303
+ </Tailwind>
304
+ </Body>
305
+ </Html>,
306
+ );
307
+ }
308
+
309
+ protected blocknoteDefaultPropsToReactEmailStyle(
310
+ props: Partial<DefaultProps>,
311
+ ): any {
312
+ return {
313
+ textAlign: props.textAlignment,
314
+ backgroundColor:
315
+ props.backgroundColor === "default" || !props.backgroundColor
316
+ ? undefined
317
+ : this.options.colors[
318
+ props.backgroundColor as keyof typeof this.options.colors
319
+ ].background,
320
+ color:
321
+ props.textColor === "default" || !props.textColor
322
+ ? undefined
323
+ : this.options.colors[
324
+ props.textColor as keyof typeof this.options.colors
325
+ ].text,
326
+ alignItems:
327
+ props.textAlignment === "right"
328
+ ? "flex-end"
329
+ : props.textAlignment === "center"
330
+ ? "center"
331
+ : undefined,
332
+ };
333
+ }
334
+ }
@@ -0,0 +1,11 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
4
+ interface ImportMetaEnv {
5
+ // readonly VITE_APP_TITLE: string;
6
+ // more env variables...
7
+ }
8
+
9
+ interface ImportMeta {
10
+ readonly env: ImportMetaEnv;
11
+ }
@@ -0,0 +1 @@
1
+ export * from "./react-email/index.js";
@@ -0,0 +1,4 @@
1
+ import { DefaultBlockSchema, pageBreakSchema } from "@blocknote/core";
2
+ import { BlockMapping } from "@blocknote/core/src/exporter/mapping.js";
3
+ import { Link } from "@react-email/components";
4
+ export declare const reactEmailBlockMappingForDefaultSchema: BlockMapping<DefaultBlockSchema & typeof pageBreakSchema.blockSchema, any, any, React.ReactElement<any>, React.ReactElement<typeof Link> | React.ReactElement<HTMLSpanElement>>;