@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.
- package/LICENSE +373 -0
- package/dist/blocknote-xl-email-exporter.js +1097 -0
- package/dist/blocknote-xl-email-exporter.js.map +1 -0
- package/dist/blocknote-xl-email-exporter.umd.cjs +31 -0
- package/dist/blocknote-xl-email-exporter.umd.cjs.map +1 -0
- package/dist/webpack-stats.json +1 -0
- package/package.json +87 -0
- package/src/index.ts +1 -0
- package/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap +35 -0
- package/src/react-email/defaultSchema/blocks.tsx +350 -0
- package/src/react-email/defaultSchema/index.ts +9 -0
- package/src/react-email/defaultSchema/inlinecontent.tsx +26 -0
- package/src/react-email/defaultSchema/styles.tsx +61 -0
- package/src/react-email/index.ts +2 -0
- package/src/react-email/reactEmailExporter.test.tsx +780 -0
- package/src/react-email/reactEmailExporter.tsx +334 -0
- package/src/vite-env.d.ts +11 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/react-email/defaultSchema/blocks.d.ts +4 -0
- package/types/src/react-email/defaultSchema/index.d.ts +654 -0
- package/types/src/react-email/defaultSchema/inlinecontent.d.ts +4 -0
- package/types/src/react-email/defaultSchema/styles.d.ts +3 -0
- package/types/src/react-email/index.d.ts +2 -0
- package/types/src/react-email/reactEmailExporter.d.ts +36 -0
- package/types/src/react-email/reactEmailExporter.test.d.ts +1 -0
|
@@ -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>>;
|