@react-email/editor 0.0.0-experimental.2 → 0.0.0-experimental.20

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/dist/index.mjs CHANGED
@@ -1,176 +1,97 @@
1
- import { Extension, Mark, Node, findChildren, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
2
- import { jsx } from "react/jsx-runtime";
3
1
  import * as ReactEmailComponents from "@react-email/components";
4
- import { Button as Button$1, CodeBlock, Column, Row, Section as Section$1 } from "@react-email/components";
2
+ import { Body as Body$1, Button as Button$1, CodeBlock, Column, Head, Heading as Heading$1, Hr, Html, Link as Link$1, Preview, Row, Section as Section$1, pretty, render, toPlainText } from "@react-email/components";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ import { Extension, InputRule, Mark, Node as Node$1, findChildren, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
5
+ import { UndoRedo } from "@tiptap/extensions";
6
+ import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, ReactRenderer, useCurrentEditor, useEditor as useEditor$1, useEditorState } from "@tiptap/react";
7
+ import * as React from "react";
8
+ import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
9
+ import TipTapStarterKit from "@tiptap/starter-kit";
10
+ import BlockquoteBase from "@tiptap/extension-blockquote";
11
+ import BulletListBase from "@tiptap/extension-bullet-list";
12
+ import CodeBase from "@tiptap/extension-code";
5
13
  import CodeBlock$1 from "@tiptap/extension-code-block";
6
- import { Plugin, PluginKey } from "@tiptap/pm/state";
14
+ import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
7
15
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
8
16
  import { fromHtml } from "hast-util-from-html";
9
17
  import Prism from "prismjs";
10
- import Placeholder from "@tiptap/extension-placeholder";
18
+ import HorizontalRule from "@tiptap/extension-horizontal-rule";
19
+ import HardBreakBase from "@tiptap/extension-hard-break";
20
+ import { Heading as Heading$2 } from "@tiptap/extension-heading";
21
+ import ItalicBase from "@tiptap/extension-italic";
22
+ import TiptapLink from "@tiptap/extension-link";
23
+ import ListItemBase from "@tiptap/extension-list-item";
24
+ import OrderedListBase from "@tiptap/extension-ordered-list";
25
+ import ParagraphBase from "@tiptap/extension-paragraph";
26
+ import TipTapPlaceholder from "@tiptap/extension-placeholder";
27
+ import StrikeBase from "@tiptap/extension-strike";
28
+ import { generateJSON } from "@tiptap/html";
29
+ import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, CaseUpperIcon, Check, ChevronDown, Code as Code$1, CodeIcon, Columns2, Columns3, Columns4, ExternalLinkIcon, Heading1, Heading2, Heading3, ItalicIcon, LinkIcon, List, ListOrdered, MousePointer, PencilIcon, Rows2, SplitSquareVertical, SquareCode, StrikethroughIcon, Text, TextIcon, TextQuote, UnderlineIcon, UnlinkIcon } from "lucide-react";
30
+ import * as Popover from "@radix-ui/react-popover";
31
+ import { BubbleMenu as BubbleMenu$1 } from "@tiptap/react/menus";
32
+ import Suggestion from "@tiptap/suggestion";
33
+ import tippy from "tippy.js";
11
34
 
12
- //#region src/core/email-node.ts
13
- var EmailNode = class EmailNode extends Node {
14
- constructor(config) {
15
- super(config);
35
+ //#region src/core/event-bus.ts
36
+ const EVENT_PREFIX = "@react-email/editor:";
37
+ var EditorEventBus = class {
38
+ prefixEventName(eventName) {
39
+ return `${EVENT_PREFIX}${String(eventName)}`;
16
40
  }
17
- /**
18
- * Create a new Node instance
19
- * @param config - Node configuration object or a function that returns a configuration object
20
- */
21
- static create(config) {
22
- return new EmailNode(typeof config === "function" ? config() : config);
41
+ dispatch(eventName, payload, options) {
42
+ const target = options?.target ?? window;
43
+ const prefixedEventName = this.prefixEventName(eventName);
44
+ const event = new CustomEvent(prefixedEventName, {
45
+ detail: payload,
46
+ bubbles: false,
47
+ cancelable: false
48
+ });
49
+ target.dispatchEvent(event);
23
50
  }
24
- static from(node, renderToReactEmail) {
25
- const customNode = EmailNode.create({});
26
- Object.assign(customNode, { ...node });
27
- customNode.config = {
28
- ...node.config,
29
- renderToReactEmail
51
+ on(eventName, handler, options) {
52
+ const target = options?.target ?? window;
53
+ const prefixedEventName = this.prefixEventName(eventName);
54
+ const abortController = new AbortController();
55
+ const wrappedHandler = (event) => {
56
+ const customEvent = event;
57
+ const result = handler(customEvent.detail);
58
+ if (result instanceof Promise) result.catch((error) => {
59
+ console.error(`Error in async event handler for ${prefixedEventName}:`, {
60
+ event: customEvent.detail,
61
+ error
62
+ });
63
+ });
30
64
  };
31
- return customNode;
32
- }
33
- configure(options) {
34
- return super.configure(options);
35
- }
36
- extend(extendedConfig) {
37
- const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
38
- return super.extend(resolvedConfig);
65
+ target.addEventListener(prefixedEventName, wrappedHandler, {
66
+ ...options,
67
+ signal: abortController.signal
68
+ });
69
+ return { unsubscribe: () => {
70
+ abortController.abort();
71
+ } };
39
72
  }
40
73
  };
74
+ const editorEventBus = new EditorEventBus();
41
75
 
42
76
  //#endregion
43
- //#region src/extensions/alignment-attribute.tsx
44
- const AlignmentAttribute = Extension.create({
45
- name: "alignmentAttribute",
46
- addOptions() {
47
- return {
48
- types: [],
49
- alignments: [
50
- "left",
51
- "center",
52
- "right",
53
- "justify"
54
- ]
55
- };
56
- },
57
- addGlobalAttributes() {
58
- return [{
59
- types: this.options.types,
60
- attributes: { alignment: {
61
- parseHTML: (element) => {
62
- const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
63
- if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
64
- return null;
65
- },
66
- renderHTML: (attributes) => {
67
- if (attributes.alignment === "left") return {};
68
- return { alignment: attributes.alignment };
69
- }
70
- } }
71
- }];
72
- },
73
- addCommands() {
74
- return { setAlignment: (alignment) => ({ commands }) => {
75
- if (!this.options.alignments.includes(alignment)) return false;
76
- return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
77
- } };
78
- },
79
- addKeyboardShortcuts() {
80
- return {
81
- Enter: () => {
82
- const { from } = this.editor.state.selection;
83
- const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
84
- if (currentAlignment) requestAnimationFrame(() => {
85
- this.editor.commands.setAlignment(currentAlignment);
86
- });
87
- return false;
88
- },
89
- "Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
90
- "Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
91
- "Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
92
- "Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
77
+ //#region src/core/is-document-visually-empty.ts
78
+ function isDocumentVisuallyEmpty(doc) {
79
+ let nonGlobalNodeCount = 0;
80
+ let firstNonGlobalNode = null;
81
+ for (let index = 0; index < doc.childCount; index += 1) {
82
+ const node = doc.child(index);
83
+ if (node.type.name === "globalContent") continue;
84
+ nonGlobalNodeCount += 1;
85
+ if (firstNonGlobalNode === null) firstNonGlobalNode = {
86
+ type: node.type,
87
+ textContent: node.textContent,
88
+ childCount: node.content.childCount
93
89
  };
94
90
  }
95
- });
96
-
97
- //#endregion
98
- //#region src/utils/attribute-helpers.ts
99
- /**
100
- * Creates TipTap attribute definitions for a list of HTML attributes.
101
- * Each attribute will have the same pattern:
102
- * - default: null
103
- * - parseHTML: extracts the attribute from the element
104
- * - renderHTML: conditionally renders the attribute if it has a value
105
- *
106
- * @param attributeNames - Array of HTML attribute names to create definitions for
107
- * @returns Object with TipTap attribute definitions
108
- *
109
- * @example
110
- * const attrs = createStandardAttributes(['class', 'id', 'title']);
111
- * // Returns:
112
- * // {
113
- * // class: {
114
- * // default: null,
115
- * // parseHTML: (element) => element.getAttribute('class'),
116
- * // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
117
- * // },
118
- * // ...
119
- * // }
120
- */
121
- function createStandardAttributes(attributeNames) {
122
- return Object.fromEntries(attributeNames.map((attr) => [attr, {
123
- default: null,
124
- parseHTML: (element) => element.getAttribute(attr),
125
- renderHTML: (attributes) => {
126
- if (!attributes[attr]) return {};
127
- return { [attr]: attributes[attr] };
128
- }
129
- }]));
91
+ if (nonGlobalNodeCount === 0) return true;
92
+ if (nonGlobalNodeCount !== 1) return false;
93
+ return firstNonGlobalNode?.type.name === "paragraph" && firstNonGlobalNode.textContent.trim().length === 0 && firstNonGlobalNode.childCount === 0;
130
94
  }
131
- /**
132
- * Common HTML attributes used across multiple extensions.
133
- * These preserve attributes during HTML import and editing for better
134
- * fidelity when importing existing email templates.
135
- */
136
- const COMMON_HTML_ATTRIBUTES = [
137
- "id",
138
- "class",
139
- "title",
140
- "lang",
141
- "dir",
142
- "data-id"
143
- ];
144
- /**
145
- * Layout-specific HTML attributes used for positioning and sizing.
146
- */
147
- const LAYOUT_ATTRIBUTES = [
148
- "align",
149
- "width",
150
- "height"
151
- ];
152
- /**
153
- * Table-specific HTML attributes used for table layout and styling.
154
- */
155
- const TABLE_ATTRIBUTES = [
156
- "border",
157
- "cellpadding",
158
- "cellspacing"
159
- ];
160
- /**
161
- * Table cell-specific HTML attributes.
162
- */
163
- const TABLE_CELL_ATTRIBUTES = [
164
- "valign",
165
- "bgcolor",
166
- "colspan",
167
- "rowspan"
168
- ];
169
- /**
170
- * Table header cell-specific HTML attributes.
171
- * These are additional attributes that only apply to <th> elements.
172
- */
173
- const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
174
95
 
175
96
  //#endregion
176
97
  //#region src/utils/styles.ts
@@ -342,483 +263,355 @@ function resolveConflictingStyles(resetStyles, inlineStyles) {
342
263
  }
343
264
 
344
265
  //#endregion
345
- //#region src/extensions/body.tsx
346
- const Body = EmailNode.create({
347
- name: "body",
348
- group: "block",
349
- content: "block+",
350
- defining: true,
351
- isolating: true,
352
- addAttributes() {
353
- return { ...createStandardAttributes([...COMMON_HTML_ATTRIBUTES, ...LAYOUT_ATTRIBUTES]) };
354
- },
355
- parseHTML() {
356
- return [{
357
- tag: "body",
358
- getAttrs: (node) => {
359
- if (typeof node === "string") return false;
360
- const element = node;
361
- const attrs = {};
362
- Array.from(element.attributes).forEach((attr) => {
363
- attrs[attr.name] = attr.value;
364
- });
365
- return attrs;
366
- }
367
- }];
368
- },
369
- renderHTML({ HTMLAttributes }) {
370
- return [
371
- "div",
372
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
373
- 0
374
- ];
375
- },
376
- renderToReactEmail({ children, node, styles }) {
377
- const inlineStyles = inlineCssToJs(node.attrs?.style);
378
- return /* @__PURE__ */ jsx("div", {
379
- className: node.attrs?.class || void 0,
380
- style: {
381
- ...styles.reset,
382
- ...inlineStyles
383
- },
384
- children
385
- });
266
+ //#region src/core/serializer/default-base-template.tsx
267
+ function DefaultBaseTemplate({ children, previewText }) {
268
+ return /* @__PURE__ */ jsxs(Html, { children: [
269
+ /* @__PURE__ */ jsxs(Head, { children: [
270
+ /* @__PURE__ */ jsx("meta", {
271
+ content: "width=device-width",
272
+ name: "viewport"
273
+ }),
274
+ /* @__PURE__ */ jsx("meta", {
275
+ content: "IE=edge",
276
+ httpEquiv: "X-UA-Compatible"
277
+ }),
278
+ /* @__PURE__ */ jsx("meta", { name: "x-apple-disable-message-reformatting" }),
279
+ /* @__PURE__ */ jsx("meta", {
280
+ content: "telephone=no,address=no,email=no,date=no,url=no",
281
+ name: "format-detection"
282
+ })
283
+ ] }),
284
+ previewText && previewText !== "" && /* @__PURE__ */ jsx(Preview, { children: previewText }),
285
+ /* @__PURE__ */ jsx(Body$1, { children: /* @__PURE__ */ jsx(Section$1, {
286
+ width: "100%",
287
+ align: "center",
288
+ children: /* @__PURE__ */ jsx(Section$1, {
289
+ style: { width: "100%" },
290
+ children
291
+ })
292
+ }) })
293
+ ] });
294
+ }
295
+
296
+ //#endregion
297
+ //#region src/core/serializer/email-mark.ts
298
+ var EmailMark = class EmailMark extends Mark {
299
+ constructor(config) {
300
+ super(config);
386
301
  }
387
- });
302
+ /**
303
+ * Create a new Mark instance
304
+ * @param config - Mark configuration object or a function that returns a configuration object
305
+ */
306
+ static create(config) {
307
+ return new EmailMark(typeof config === "function" ? config() : config);
308
+ }
309
+ static from(mark, renderToReactEmail) {
310
+ const customMark = EmailMark.create({});
311
+ Object.assign(customMark, { ...mark });
312
+ customMark.config = {
313
+ ...mark.config,
314
+ renderToReactEmail
315
+ };
316
+ return customMark;
317
+ }
318
+ configure(options) {
319
+ return super.configure(options);
320
+ }
321
+ extend(extendedConfig) {
322
+ const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
323
+ return super.extend(resolvedConfig);
324
+ }
325
+ };
388
326
 
389
327
  //#endregion
390
- //#region src/extensions/bold.ts
391
- /**
392
- * Matches bold text via `**` as input.
393
- */
394
- const starInputRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))$/;
395
- /**
396
- * Matches bold text via `**` while pasting.
397
- */
398
- const starPasteRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))/g;
399
- /**
400
- * Matches bold text via `__` as input.
401
- */
402
- const underscoreInputRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))$/;
403
- /**
404
- * Matches bold text via `__` while pasting.
405
- */
406
- const underscorePasteRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g;
407
- /**
408
- * This extension allows you to mark text as bold.
409
- * @see https://tiptap.dev/api/marks/bold
410
- */
411
- const Bold = Mark.create({
412
- name: "bold",
413
- addOptions() {
414
- return { HTMLAttributes: {} };
415
- },
416
- parseHTML() {
417
- return [
418
- { tag: "strong" },
419
- {
420
- tag: "b",
421
- getAttrs: (node) => node.style.fontWeight !== "normal" && null
422
- },
423
- {
424
- style: "font-weight=400",
425
- clearMark: (mark) => mark.type.name === this.name
426
- }
427
- ];
428
- },
429
- renderHTML({ HTMLAttributes }) {
430
- return [
431
- "strong",
432
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
433
- 0
434
- ];
435
- },
436
- addCommands() {
437
- return {
438
- setBold: () => ({ commands }) => {
439
- return commands.setMark(this.name);
440
- },
441
- toggleBold: () => ({ commands }) => {
442
- return commands.toggleMark(this.name);
443
- },
444
- unsetBold: () => ({ commands }) => {
445
- return commands.unsetMark(this.name);
446
- }
447
- };
448
- },
449
- addKeyboardShortcuts() {
450
- return {
451
- "Mod-b": () => this.editor.commands.toggleBold(),
452
- "Mod-B": () => this.editor.commands.toggleBold()
328
+ //#region src/core/serializer/email-node.ts
329
+ var EmailNode = class EmailNode extends Node$1 {
330
+ constructor(config) {
331
+ super(config);
332
+ }
333
+ /**
334
+ * Create a new Node instance
335
+ * @param config - Node configuration object or a function that returns a configuration object
336
+ */
337
+ static create(config) {
338
+ return new EmailNode(typeof config === "function" ? config() : config);
339
+ }
340
+ static from(node, renderToReactEmail) {
341
+ const customNode = EmailNode.create({});
342
+ Object.assign(customNode, { ...node });
343
+ customNode.config = {
344
+ ...node.config,
345
+ renderToReactEmail
453
346
  };
454
- },
455
- addInputRules() {
456
- return [markInputRule({
457
- find: starInputRegex,
458
- type: this.type
459
- }), markInputRule({
460
- find: underscoreInputRegex,
461
- type: this.type
462
- })];
463
- },
464
- addPasteRules() {
465
- return [markPasteRule({
466
- find: starPasteRegex,
467
- type: this.type
468
- }), markPasteRule({
469
- find: underscorePasteRegex,
470
- type: this.type
471
- })];
347
+ return customNode;
472
348
  }
473
- });
349
+ configure(options) {
350
+ return super.configure(options);
351
+ }
352
+ extend(extendedConfig) {
353
+ const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
354
+ return super.extend(resolvedConfig);
355
+ }
356
+ };
474
357
 
475
358
  //#endregion
476
- //#region src/extensions/button.tsx
477
- const Button = EmailNode.create({
478
- name: "button",
479
- group: "block",
480
- content: "inline*",
481
- defining: true,
482
- draggable: true,
483
- marks: "bold",
484
- addAttributes() {
485
- return {
486
- class: { default: "button" },
487
- href: { default: "#" },
488
- alignment: { default: "left" }
489
- };
490
- },
491
- parseHTML() {
492
- return [{
493
- tag: "a[data-id=\"react-email-button\"]",
494
- getAttrs: (node) => {
495
- if (typeof node === "string") return false;
496
- const element = node;
497
- const attrs = {};
498
- Array.from(element.attributes).forEach((attr) => {
499
- attrs[attr.name] = attr.value;
500
- });
501
- return attrs;
359
+ //#region src/core/serializer/compose-react-email.tsx
360
+ const MARK_ORDER = {
361
+ preservedStyle: 0,
362
+ italic: 1,
363
+ strike: 2,
364
+ underline: 3,
365
+ link: 4,
366
+ bold: 5,
367
+ code: 6
368
+ };
369
+ const NODES_WITH_INCREMENTED_CHILD_DEPTH = new Set(["bulletList", "orderedList"]);
370
+ function getOrderedMarks(marks) {
371
+ if (!marks) return [];
372
+ return [...marks].sort((a, b) => (MARK_ORDER[a.type] ?? Number.MAX_SAFE_INTEGER) - (MARK_ORDER[b.type] ?? Number.MAX_SAFE_INTEGER));
373
+ }
374
+ const composeReactEmail = async ({ editor, preview }) => {
375
+ const data = editor.getJSON();
376
+ const extensions = editor.extensionManager.extensions;
377
+ const serializerPlugin = extensions.map((ext) => ext.options?.serializerPlugin).filter((p) => Boolean(p)).at(-1);
378
+ const emailNodeComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailNode).map((extension) => [extension.name, extension.config.renderToReactEmail]));
379
+ const emailMarkComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailMark).map((extension) => [extension.name, extension.config.renderToReactEmail]));
380
+ function renderMark(mark, node, children, depth) {
381
+ const markStyle = serializerPlugin?.getNodeStyles({
382
+ type: mark.type,
383
+ attrs: mark.attrs ?? {}
384
+ }, depth, editor) ?? {};
385
+ const markRenderer = emailMarkComponentRegistry[mark.type];
386
+ if (markRenderer) return markRenderer({
387
+ mark,
388
+ node,
389
+ style: markStyle,
390
+ children
391
+ });
392
+ return children;
393
+ }
394
+ function parseContent(content, depth = 0) {
395
+ if (!content) return;
396
+ return content.map((node, index) => {
397
+ const style = serializerPlugin?.getNodeStyles(node, depth, editor) ?? {};
398
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
399
+ if (node.type && emailNodeComponentRegistry[node.type]) {
400
+ const Component = emailNodeComponentRegistry[node.type];
401
+ const childDepth = NODES_WITH_INCREMENTED_CHILD_DEPTH.has(node.type) ? depth + 1 : depth;
402
+ return /* @__PURE__ */ jsx(Component, {
403
+ node: node.type === "table" && inlineStyles.width && !node.attrs?.width ? {
404
+ ...node,
405
+ attrs: {
406
+ ...node.attrs,
407
+ width: inlineStyles.width
408
+ }
409
+ } : node,
410
+ style,
411
+ children: parseContent(node.content, childDepth)
412
+ }, index);
502
413
  }
503
- }];
504
- },
505
- renderHTML({ HTMLAttributes }) {
506
- return [
507
- "div",
508
- mergeAttributes({ class: `align-${HTMLAttributes?.alignment}` }),
509
- [
510
- "a",
511
- mergeAttributes({
512
- class: `node-button ${HTMLAttributes?.class}`,
513
- style: HTMLAttributes?.style,
514
- "data-id": "react-email-button",
515
- "data-href": HTMLAttributes?.href
516
- }),
517
- 0
518
- ]
519
- ];
520
- },
521
- addCommands() {
522
- return {
523
- updateButton: (attributes) => ({ commands }) => {
524
- return commands.updateAttributes("button", attributes);
525
- },
526
- setButton: () => ({ commands }) => {
527
- return commands.insertContent({
528
- type: "button",
529
- content: [{
530
- type: "text",
531
- text: "Button"
532
- }]
533
- });
414
+ switch (node.type) {
415
+ case "text": {
416
+ let wrappedText = node.text;
417
+ getOrderedMarks(node.marks).forEach((mark) => {
418
+ wrappedText = renderMark(mark, node, wrappedText, depth);
419
+ });
420
+ const textAttributes = node.marks?.find((mark) => mark.type === "textStyle")?.attrs;
421
+ return /* @__PURE__ */ jsx("span", {
422
+ style: {
423
+ ...textAttributes,
424
+ ...style
425
+ },
426
+ children: wrappedText
427
+ }, index);
428
+ }
429
+ default: return null;
534
430
  }
535
- };
536
- },
537
- renderToReactEmail({ children, node, styles }) {
538
- const inlineStyles = inlineCssToJs(node.attrs?.style);
539
- return /* @__PURE__ */ jsx(Row, { children: /* @__PURE__ */ jsx(Column, {
540
- align: node.attrs?.align || node.attrs?.alignment,
541
- children: /* @__PURE__ */ jsx(Button$1, {
542
- className: node.attrs?.class || void 0,
543
- href: node.attrs?.href,
544
- style: {
545
- ...styles.reset,
546
- ...styles.button,
547
- ...inlineStyles
548
- },
549
- children
550
- })
551
- }) });
431
+ });
552
432
  }
553
- });
433
+ const unformattedHtml = await render(/* @__PURE__ */ jsx(serializerPlugin?.BaseTemplate ?? DefaultBaseTemplate, {
434
+ previewText: preview,
435
+ editor,
436
+ children: parseContent(data.content)
437
+ }));
438
+ const [prettyHtml, text] = await Promise.all([pretty(unformattedHtml), toPlainText(unformattedHtml)]);
439
+ return {
440
+ html: prettyHtml,
441
+ text
442
+ };
443
+ };
554
444
 
555
445
  //#endregion
556
- //#region src/extensions/class-attribute.tsx
557
- const ClassAttribute = Extension.create({
558
- name: "classAttribute",
446
+ //#region src/extensions/alignment-attribute.tsx
447
+ const AlignmentAttribute = Extension.create({
448
+ name: "alignmentAttribute",
559
449
  addOptions() {
560
450
  return {
561
451
  types: [],
562
- class: []
452
+ alignments: [
453
+ "left",
454
+ "center",
455
+ "right",
456
+ "justify"
457
+ ]
563
458
  };
564
459
  },
565
460
  addGlobalAttributes() {
566
461
  return [{
567
462
  types: this.options.types,
568
- attributes: { class: {
569
- default: "",
570
- parseHTML: (element) => element.className || "",
463
+ attributes: { alignment: {
464
+ parseHTML: (element) => {
465
+ const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
466
+ if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
467
+ return null;
468
+ },
571
469
  renderHTML: (attributes) => {
572
- return attributes.class ? { class: attributes.class } : {};
470
+ if (attributes.alignment === "left") return {};
471
+ return { alignment: attributes.alignment };
573
472
  }
574
473
  } }
575
474
  }];
576
475
  },
577
476
  addCommands() {
477
+ return { setAlignment: (alignment) => ({ commands }) => {
478
+ if (!this.options.alignments.includes(alignment)) return false;
479
+ return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
480
+ } };
481
+ },
482
+ addKeyboardShortcuts() {
578
483
  return {
579
- unsetClass: () => ({ commands }) => {
580
- return this.options.types.every((type) => commands.resetAttributes(type, "class"));
484
+ Enter: () => {
485
+ const { from } = this.editor.state.selection;
486
+ const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
487
+ if (currentAlignment) requestAnimationFrame(() => {
488
+ this.editor.commands.setAlignment(currentAlignment);
489
+ });
490
+ return false;
581
491
  },
582
- setClass: (classList) => ({ commands }) => {
583
- return this.options.types.every((type) => commands.updateAttributes(type, { class: classList }));
584
- }
492
+ "Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
493
+ "Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
494
+ "Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
495
+ "Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
585
496
  };
586
- },
587
- addKeyboardShortcuts() {
588
- return { Enter: ({ editor }) => {
589
- requestAnimationFrame(() => {
590
- editor.commands.resetAttributes("paragraph", "class");
591
- });
592
- return false;
593
- } };
594
497
  }
595
498
  });
596
499
 
597
500
  //#endregion
598
- //#region src/utils/prism-utils.ts
599
- const publicURL = "/styles/prism";
600
- function loadPrismTheme(theme) {
601
- const link = document.createElement("link");
602
- link.rel = "stylesheet";
603
- link.href = `${publicURL}/prism-${theme}.css`;
604
- link.setAttribute("data-prism-theme", "");
605
- document.head.appendChild(link);
606
- }
607
- function removePrismTheme() {
608
- const existingTheme = document.querySelectorAll("link[rel=\"stylesheet\"][data-prism-theme]");
609
- if (existingTheme.length > 0) existingTheme.forEach((cssLinkTag) => {
610
- cssLinkTag.remove();
611
- });
612
- }
613
- function hasPrismThemeLoaded(theme) {
614
- return !!document.querySelector(`link[rel="stylesheet"][data-prism-theme][href="${publicURL}/prism-${theme}.css"]`);
501
+ //#region src/utils/get-text-alignment.ts
502
+ function getTextAlignment(alignment) {
503
+ switch (alignment) {
504
+ case "left": return { textAlign: "left" };
505
+ case "center": return { textAlign: "center" };
506
+ case "right": return { textAlign: "right" };
507
+ default: return {};
508
+ }
615
509
  }
616
510
 
617
511
  //#endregion
618
- //#region src/extensions/prism-plugin.ts
619
- const PRISM_LANGUAGE_LOADED_META = "prismLanguageLoaded";
620
- function parseNodes(nodes, className = []) {
621
- return nodes.flatMap((node) => {
622
- const classes = [...className, ...node.properties ? node.properties.className : []];
623
- if (node.children) return parseNodes(node.children, classes);
624
- return {
625
- text: node.value ?? "",
626
- classes
627
- };
628
- });
629
- }
630
- function getHighlightNodes(html) {
631
- return fromHtml(html, { fragment: true }).children;
632
- }
633
- function registeredLang(aliasOrLanguage) {
634
- const allSupportLang = Object.keys(Prism.languages).filter((id) => typeof Prism.languages[id] === "object");
635
- return Boolean(allSupportLang.find((x) => x === aliasOrLanguage));
636
- }
637
- function getDecorations({ doc, name, defaultLanguage, defaultTheme, loadingLanguages, onLanguageLoaded }) {
638
- const decorations = [];
639
- findChildren(doc, (node) => node.type.name === name).forEach((block) => {
640
- let from = block.pos + 1;
641
- const language = block.node.attrs.language || defaultLanguage;
642
- const theme = block.node.attrs.theme || defaultTheme;
643
- let html = "";
644
- try {
645
- if (!registeredLang(language) && !loadingLanguages.has(language)) {
646
- loadingLanguages.add(language);
647
- import(`prismjs/components/prism-${language}`).then(() => {
648
- loadingLanguages.delete(language);
649
- onLanguageLoaded(language);
650
- }).catch(() => {
651
- loadingLanguages.delete(language);
652
- });
653
- }
654
- if (!hasPrismThemeLoaded(theme)) loadPrismTheme(theme);
655
- html = Prism.highlight(block.node.textContent, Prism.languages[language], language);
656
- } catch {
657
- html = Prism.highlight(block.node.textContent, Prism.languages.javascript, "js");
658
- }
659
- parseNodes(getHighlightNodes(html)).forEach((node) => {
660
- const to = from + node.text.length;
661
- if (node.classes.length) {
662
- const decoration = Decoration.inline(from, to, { class: node.classes.join(" ") });
663
- decorations.push(decoration);
664
- }
665
- from = to;
666
- });
667
- });
668
- return DecorationSet.create(doc, decorations);
669
- }
670
- function PrismPlugin({ name, defaultLanguage, defaultTheme }) {
671
- if (!defaultLanguage) throw Error("You must specify the defaultLanguage parameter");
672
- const loadingLanguages = /* @__PURE__ */ new Set();
673
- let pluginView = null;
674
- const onLanguageLoaded = (language) => {
675
- if (pluginView) pluginView.dispatch(pluginView.state.tr.setMeta(PRISM_LANGUAGE_LOADED_META, language));
676
- };
677
- const prismjsPlugin = new Plugin({
678
- key: new PluginKey("prism"),
679
- view(view) {
680
- pluginView = view;
681
- return { destroy() {
682
- pluginView = null;
683
- } };
684
- },
685
- state: {
686
- init: (_, { doc }) => {
687
- return getDecorations({
688
- doc,
689
- name,
690
- defaultLanguage,
691
- defaultTheme,
692
- loadingLanguages,
693
- onLanguageLoaded
694
- });
695
- },
696
- apply: (transaction, decorationSet, oldState, newState) => {
697
- const oldNodeName = oldState.selection.$head.parent.type.name;
698
- const newNodeName = newState.selection.$head.parent.type.name;
699
- const oldNodes = findChildren(oldState.doc, (node) => node.type.name === name);
700
- const newNodes = findChildren(newState.doc, (node) => node.type.name === name);
701
- if (transaction.getMeta(PRISM_LANGUAGE_LOADED_META) || transaction.docChanged && ([oldNodeName, newNodeName].includes(name) || newNodes.length !== oldNodes.length || transaction.steps.some((step) => {
702
- const rangeStep = step;
703
- return rangeStep.from !== void 0 && rangeStep.to !== void 0 && oldNodes.some((node) => {
704
- return node.pos >= rangeStep.from && node.pos + node.node.nodeSize <= rangeStep.to;
705
- });
706
- }))) return getDecorations({
707
- doc: transaction.doc,
708
- name,
709
- defaultLanguage,
710
- defaultTheme,
711
- loadingLanguages,
712
- onLanguageLoaded
713
- });
714
- return decorationSet.map(transaction.mapping, transaction.doc);
715
- }
716
- },
717
- props: { decorations(state) {
718
- return prismjsPlugin.getState(state);
719
- } },
720
- destroy() {
721
- pluginView = null;
722
- removePrismTheme();
723
- }
724
- });
725
- return prismjsPlugin;
726
- }
512
+ //#region src/extensions/blockquote.tsx
513
+ const Blockquote = EmailNode.from(BlockquoteBase, ({ children, node, style }) => /* @__PURE__ */ jsx("blockquote", {
514
+ className: node.attrs?.class || void 0,
515
+ style: {
516
+ ...style,
517
+ ...inlineCssToJs(node.attrs?.style),
518
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
519
+ },
520
+ children
521
+ }));
727
522
 
728
523
  //#endregion
729
- //#region src/extensions/code-block.tsx
730
- const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
731
- addOptions() {
732
- return {
733
- languageClassPrefix: "language-",
734
- exitOnTripleEnter: false,
735
- exitOnArrowDown: false,
736
- enableTabIndentation: true,
737
- tabSize: 2,
738
- defaultLanguage: "javascript",
739
- defaultTheme: "default",
740
- HTMLAttributes: {}
741
- };
742
- },
743
- addAttributes() {
744
- return {
745
- ...this.parent?.(),
746
- language: {
747
- default: this.options.defaultLanguage,
748
- parseHTML: (element) => {
749
- if (!element) return null;
750
- const { languageClassPrefix } = this.options;
751
- if (!languageClassPrefix) return null;
752
- const language = [...element.firstElementChild?.classList || []].filter((className) => className.startsWith(languageClassPrefix || "")).map((className) => className.replace(languageClassPrefix, ""))[0];
753
- if (!language) return null;
754
- return language;
755
- },
756
- rendered: false
757
- },
758
- theme: {
759
- default: this.options.defaultTheme,
760
- rendered: false
761
- }
762
- };
763
- },
764
- renderHTML({ node, HTMLAttributes }) {
765
- return [
766
- "pre",
767
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { class: node.attrs.language ? `${this.options.languageClassPrefix}${node.attrs.language}` : null }, { "data-theme": node.attrs.theme }),
768
- [
769
- "code",
770
- { class: node.attrs.language ? `${this.options.languageClassPrefix}${node.attrs.language} node-codeTag` : "node-codeTag" },
771
- 0
772
- ]
773
- ];
774
- },
775
- addProseMirrorPlugins() {
776
- return [...this.parent?.() || [], PrismPlugin({
777
- name: this.name,
778
- defaultLanguage: this.options.defaultLanguage,
779
- defaultTheme: this.options.defaultTheme
780
- })];
781
- }
782
- }), ({ node, styles }) => {
783
- const language = node.attrs?.language ? `${node.attrs.language}` : "javascript";
784
- const userTheme = ReactEmailComponents[node.attrs?.theme];
785
- const theme = userTheme ? {
786
- ...userTheme,
787
- base: {
788
- ...userTheme.base,
789
- borderRadius: "0.125rem",
790
- padding: "0.75rem 1rem"
791
- }
792
- } : { base: {
793
- color: "#1e293b",
794
- background: "#f1f5f9",
795
- lineHeight: "1.5",
796
- fontFamily: "\"Fira Code\", \"Fira Mono\", Menlo, Consolas, \"DejaVu Sans Mono\", monospace",
797
- padding: "0.75rem 1rem",
798
- borderRadius: "0.125rem"
799
- } };
800
- return /* @__PURE__ */ jsx(CodeBlock, {
801
- code: node.content?.[0]?.text ?? "",
802
- language,
803
- theme,
804
- style: {
805
- width: "auto",
806
- ...styles.codeBlock
524
+ //#region src/utils/attribute-helpers.ts
525
+ /**
526
+ * Creates TipTap attribute definitions for a list of HTML attributes.
527
+ * Each attribute will have the same pattern:
528
+ * - default: null
529
+ * - parseHTML: extracts the attribute from the element
530
+ * - renderHTML: conditionally renders the attribute if it has a value
531
+ *
532
+ * @param attributeNames - Array of HTML attribute names to create definitions for
533
+ * @returns Object with TipTap attribute definitions
534
+ *
535
+ * @example
536
+ * const attrs = createStandardAttributes(['class', 'id', 'title']);
537
+ * // Returns:
538
+ * // {
539
+ * // class: {
540
+ * // default: null,
541
+ * // parseHTML: (element) => element.getAttribute('class'),
542
+ * // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
543
+ * // },
544
+ * // ...
545
+ * // }
546
+ */
547
+ function createStandardAttributes(attributeNames) {
548
+ return Object.fromEntries(attributeNames.map((attr) => [attr, {
549
+ default: null,
550
+ parseHTML: (element) => element.getAttribute(attr),
551
+ renderHTML: (attributes) => {
552
+ if (!attributes[attr]) return {};
553
+ return { [attr]: attributes[attr] };
807
554
  }
808
- });
809
- });
555
+ }]));
556
+ }
557
+ /**
558
+ * Common HTML attributes used across multiple extensions.
559
+ * These preserve attributes during HTML import and editing for better
560
+ * fidelity when importing existing email templates.
561
+ */
562
+ const COMMON_HTML_ATTRIBUTES = [
563
+ "id",
564
+ "class",
565
+ "title",
566
+ "lang",
567
+ "dir",
568
+ "data-id"
569
+ ];
570
+ /**
571
+ * Layout-specific HTML attributes used for positioning and sizing.
572
+ */
573
+ const LAYOUT_ATTRIBUTES = [
574
+ "align",
575
+ "width",
576
+ "height"
577
+ ];
578
+ /**
579
+ * Table-specific HTML attributes used for table layout and styling.
580
+ */
581
+ const TABLE_ATTRIBUTES = [
582
+ "border",
583
+ "cellpadding",
584
+ "cellspacing"
585
+ ];
586
+ /**
587
+ * Table cell-specific HTML attributes.
588
+ */
589
+ const TABLE_CELL_ATTRIBUTES = [
590
+ "valign",
591
+ "bgcolor",
592
+ "colspan",
593
+ "rowspan"
594
+ ];
595
+ /**
596
+ * Table header cell-specific HTML attributes.
597
+ * These are additional attributes that only apply to <th> elements.
598
+ */
599
+ const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
810
600
 
811
601
  //#endregion
812
- //#region src/extensions/div.tsx
813
- const Div = EmailNode.create({
814
- name: "div",
602
+ //#region src/extensions/body.tsx
603
+ const Body = EmailNode.create({
604
+ name: "body",
815
605
  group: "block",
816
606
  content: "block+",
817
607
  defining: true,
818
608
  isolating: true,
609
+ addAttributes() {
610
+ return { ...createStandardAttributes([...COMMON_HTML_ATTRIBUTES, ...LAYOUT_ATTRIBUTES]) };
611
+ },
819
612
  parseHTML() {
820
613
  return [{
821
- tag: "div:not([data-type])",
614
+ tag: "body",
822
615
  getAttrs: (node) => {
823
616
  if (typeof node === "string") return false;
824
617
  const element = node;
@@ -837,15 +630,12 @@ const Div = EmailNode.create({
837
630
  0
838
631
  ];
839
632
  },
840
- addAttributes() {
841
- return { ...createStandardAttributes([...COMMON_HTML_ATTRIBUTES, ...LAYOUT_ATTRIBUTES]) };
842
- },
843
- renderToReactEmail({ children, node, styles }) {
633
+ renderToReactEmail({ children, node, style }) {
844
634
  const inlineStyles = inlineCssToJs(node.attrs?.style);
845
635
  return /* @__PURE__ */ jsx("div", {
846
636
  className: node.attrs?.class || void 0,
847
637
  style: {
848
- ...styles.reset,
638
+ ...style,
849
639
  ...inlineStyles
850
640
  },
851
641
  children
@@ -854,269 +644,223 @@ const Div = EmailNode.create({
854
644
  });
855
645
 
856
646
  //#endregion
857
- //#region src/extensions/max-nesting.ts
858
- const MaxNesting = Extension.create({
859
- name: "maxNesting",
647
+ //#region src/extensions/bold.tsx
648
+ /**
649
+ * Matches bold text via `**` as input.
650
+ */
651
+ const starInputRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))$/;
652
+ /**
653
+ * Matches bold text via `**` while pasting.
654
+ */
655
+ const starPasteRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))/g;
656
+ /**
657
+ * Matches bold text via `__` as input.
658
+ */
659
+ const underscoreInputRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))$/;
660
+ /**
661
+ * Matches bold text via `__` while pasting.
662
+ */
663
+ const underscorePasteRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g;
664
+ /**
665
+ * This extension allows you to mark text as bold.
666
+ * @see https://tiptap.dev/api/marks/bold
667
+ */
668
+ const Bold = EmailMark.create({
669
+ name: "bold",
860
670
  addOptions() {
861
- return {
862
- maxDepth: 3,
863
- nodeTypes: void 0
864
- };
671
+ return { HTMLAttributes: {} };
865
672
  },
866
- addProseMirrorPlugins() {
867
- const { maxDepth, nodeTypes } = this.options;
868
- if (typeof maxDepth !== "number" || maxDepth < 1) throw new Error("maxDepth must be a positive number");
869
- return [new Plugin({
870
- key: new PluginKey("maxNesting"),
871
- appendTransaction(transactions, _oldState, newState) {
872
- if (!transactions.some((tr$1) => tr$1.docChanged)) return null;
873
- const rangesToLift = [];
874
- newState.doc.descendants((node, pos) => {
875
- let depth = 0;
876
- let currentPos = pos;
877
- let currentNode = node;
878
- while (currentNode && depth <= maxDepth) {
879
- if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
880
- const $pos = newState.doc.resolve(currentPos);
881
- if ($pos.depth === 0) break;
882
- currentPos = $pos.before($pos.depth);
883
- currentNode = newState.doc.nodeAt(currentPos);
884
- }
885
- if (depth > maxDepth) {
886
- const $pos = newState.doc.resolve(pos);
887
- if ($pos.depth > 0) {
888
- const range = $pos.blockRange();
889
- if (range && "canReplace" in newState.schema.nodes.doc && typeof newState.schema.nodes.doc.canReplace === "function" && newState.schema.nodes.doc.canReplace(range.start - 1, range.end + 1, newState.doc.slice(range.start, range.end).content)) rangesToLift.push({
890
- range,
891
- target: range.start - 1
892
- });
893
- }
894
- }
895
- });
896
- if (rangesToLift.length === 0) return null;
897
- const tr = newState.tr;
898
- for (let i = rangesToLift.length - 1; i >= 0; i--) {
899
- const { range, target } = rangesToLift[i];
900
- tr.lift(range, target);
901
- }
902
- return tr;
673
+ parseHTML() {
674
+ return [
675
+ { tag: "strong" },
676
+ {
677
+ tag: "b",
678
+ getAttrs: (node) => node.style.fontWeight !== "normal" && null
903
679
  },
904
- filterTransaction(tr) {
905
- if (!tr.docChanged) return true;
906
- let wouldCreateDeepNesting = false;
907
- const newDoc = tr.doc;
908
- newDoc.descendants((node, pos) => {
909
- if (wouldCreateDeepNesting) return false;
910
- let depth = 0;
911
- let currentPos = pos;
912
- let currentNode = node;
913
- while (currentNode && depth <= maxDepth) {
914
- if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
915
- const $pos = newDoc.resolve(currentPos);
916
- if ($pos.depth === 0) break;
917
- currentPos = $pos.before($pos.depth);
918
- currentNode = newDoc.nodeAt(currentPos);
919
- }
920
- if (depth > maxDepth) {
921
- wouldCreateDeepNesting = true;
922
- return false;
923
- }
924
- });
925
- return !wouldCreateDeepNesting;
680
+ {
681
+ style: "font-weight=400",
682
+ clearMark: (mark) => mark.type.name === this.name
683
+ }
684
+ ];
685
+ },
686
+ renderHTML({ HTMLAttributes }) {
687
+ return [
688
+ "strong",
689
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
690
+ 0
691
+ ];
692
+ },
693
+ renderToReactEmail({ children, style }) {
694
+ return /* @__PURE__ */ jsx("strong", {
695
+ style,
696
+ children
697
+ });
698
+ },
699
+ addCommands() {
700
+ return {
701
+ setBold: () => ({ commands }) => {
702
+ return commands.setMark(this.name);
703
+ },
704
+ toggleBold: () => ({ commands }) => {
705
+ return commands.toggleMark(this.name);
706
+ },
707
+ unsetBold: () => ({ commands }) => {
708
+ return commands.unsetMark(this.name);
926
709
  }
710
+ };
711
+ },
712
+ addKeyboardShortcuts() {
713
+ return {
714
+ "Mod-b": () => this.editor.commands.toggleBold(),
715
+ "Mod-B": () => this.editor.commands.toggleBold()
716
+ };
717
+ },
718
+ addInputRules() {
719
+ return [markInputRule({
720
+ find: starInputRegex,
721
+ type: this.type
722
+ }), markInputRule({
723
+ find: underscoreInputRegex,
724
+ type: this.type
725
+ })];
726
+ },
727
+ addPasteRules() {
728
+ return [markPasteRule({
729
+ find: starPasteRegex,
730
+ type: this.type
731
+ }), markPasteRule({
732
+ find: underscorePasteRegex,
733
+ type: this.type
927
734
  })];
928
735
  }
929
736
  });
930
737
 
931
738
  //#endregion
932
- //#region src/extensions/placeholder.ts
933
- const createPlaceholderExtension = (options) => {
934
- return Placeholder.configure({
935
- placeholder: ({ node }) => {
936
- if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
937
- return "Press '/' for commands";
938
- },
939
- includeChildren: true,
940
- ...options
941
- });
942
- };
943
- const placeholder = createPlaceholderExtension();
739
+ //#region src/extensions/bullet-list.tsx
740
+ const BulletList = EmailNode.from(BulletListBase, ({ children, node, style }) => /* @__PURE__ */ jsx("ul", {
741
+ className: node.attrs?.class || void 0,
742
+ style: {
743
+ ...style,
744
+ ...inlineCssToJs(node.attrs?.style)
745
+ },
746
+ children
747
+ }));
944
748
 
945
749
  //#endregion
946
- //#region src/extensions/preserved-style.ts
947
- const PreservedStyle = Mark.create({
948
- name: "preservedStyle",
750
+ //#region src/extensions/button.tsx
751
+ const Button = EmailNode.create({
752
+ name: "button",
753
+ group: "block",
754
+ content: "inline*",
755
+ defining: true,
756
+ draggable: true,
757
+ marks: "bold",
949
758
  addAttributes() {
950
- return { style: {
951
- default: null,
952
- parseHTML: (element) => element.getAttribute("style"),
953
- renderHTML: (attributes) => {
954
- if (!attributes.style) return {};
955
- return { style: attributes.style };
956
- }
957
- } };
759
+ return {
760
+ class: { default: "button" },
761
+ href: { default: "#" },
762
+ alignment: { default: "left" }
763
+ };
958
764
  },
959
765
  parseHTML() {
960
766
  return [{
961
- tag: "span[style]",
962
- getAttrs: (element) => {
963
- if (typeof element === "string") return false;
964
- const style = element.getAttribute("style");
965
- if (style && hasPreservableStyles(style)) return { style };
966
- return false;
767
+ tag: "a[data-id=\"react-email-button\"]",
768
+ getAttrs: (node) => {
769
+ if (typeof node === "string") return false;
770
+ const element = node;
771
+ const attrs = {};
772
+ Array.from(element.attributes).forEach((attr) => {
773
+ attrs[attr.name] = attr.value;
774
+ });
775
+ return attrs;
967
776
  }
968
777
  }];
969
778
  },
970
779
  renderHTML({ HTMLAttributes }) {
971
780
  return [
972
- "span",
973
- mergeAttributes(HTMLAttributes),
974
- 0
781
+ "div",
782
+ mergeAttributes({ class: `align-${HTMLAttributes?.alignment}` }),
783
+ [
784
+ "a",
785
+ mergeAttributes({
786
+ class: `node-button ${HTMLAttributes?.class}`,
787
+ style: HTMLAttributes?.style,
788
+ "data-id": "react-email-button",
789
+ "data-href": HTMLAttributes?.href
790
+ }),
791
+ 0
792
+ ]
975
793
  ];
976
- }
977
- });
978
- const LINK_INDICATOR_STYLES = [
979
- "color",
980
- "text-decoration",
981
- "text-decoration-line",
982
- "text-decoration-color",
983
- "text-decoration-style"
984
- ];
985
- function parseStyleString(styleString) {
986
- const temp = document.createElement("div");
987
- temp.style.cssText = styleString;
988
- return temp.style;
989
- }
990
- function hasBackground(style) {
991
- const bgColor = style.backgroundColor;
992
- const bg = style.background;
993
- if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") return true;
994
- if (bg && bg !== "transparent" && bg !== "none" && bg !== "rgba(0, 0, 0, 0)") return true;
995
- return false;
996
- }
997
- function hasPreservableStyles(styleString) {
998
- return processStylesForUnlink(styleString) !== null;
999
- }
1000
- /**
1001
- * Processes styles when unlinking:
1002
- * - Has background (button-like): preserve all styles
1003
- * - No background: strip link-indicator styles (color, text-decoration), keep the rest
1004
- */
1005
- function processStylesForUnlink(styleString) {
1006
- if (!styleString) return null;
1007
- const style = parseStyleString(styleString);
1008
- if (hasBackground(style)) return styleString;
1009
- const filtered = [];
1010
- for (let i = 0; i < style.length; i++) {
1011
- const prop = style[i];
1012
- if (LINK_INDICATOR_STYLES.includes(prop)) continue;
1013
- const value = style.getPropertyValue(prop);
1014
- if (value) filtered.push(`${prop}: ${value}`);
1015
- }
1016
- return filtered.length > 0 ? filtered.join("; ") : null;
1017
- }
1018
-
1019
- //#endregion
1020
- //#region src/utils/get-text-alignment.ts
1021
- function getTextAlignment(alignment) {
1022
- switch (alignment) {
1023
- case "left": return { textAlign: "left" };
1024
- case "center": return { textAlign: "center" };
1025
- case "right": return { textAlign: "right" };
1026
- default: return {};
1027
- }
1028
- }
1029
-
1030
- //#endregion
1031
- //#region src/extensions/section.tsx
1032
- const Section = EmailNode.create({
1033
- name: "section",
1034
- group: "block",
1035
- content: "block+",
1036
- isolating: true,
1037
- defining: true,
1038
- parseHTML() {
1039
- return [{ tag: "section[data-type=\"section\"]" }];
1040
- },
1041
- renderHTML({ HTMLAttributes }) {
1042
- return [
1043
- "section",
1044
- mergeAttributes({
1045
- "data-type": "section",
1046
- class: "node-section"
1047
- }, HTMLAttributes),
1048
- 0
1049
- ];
1050
- },
1051
- addCommands() {
1052
- return { insertSection: () => ({ commands }) => {
1053
- return commands.insertContent({
1054
- type: this.name,
1055
- content: [{
1056
- type: "paragraph",
1057
- content: []
1058
- }]
1059
- });
1060
- } };
1061
- },
1062
- renderToReactEmail({ children, node, styles }) {
1063
- const inlineStyles = inlineCssToJs(node.attrs?.style);
1064
- const textAlign = node.attrs?.align || node.attrs?.alignment;
1065
- return /* @__PURE__ */ jsx(Section$1, {
1066
- className: node.attrs?.class || void 0,
1067
- align: textAlign,
1068
- style: {
1069
- ...styles.section,
1070
- ...inlineStyles,
1071
- ...getTextAlignment(textAlign)
1072
- },
1073
- children
1074
- });
794
+ },
795
+ addCommands() {
796
+ return {
797
+ updateButton: (attributes) => ({ commands }) => {
798
+ return commands.updateAttributes("button", attributes);
799
+ },
800
+ setButton: () => ({ commands }) => {
801
+ return commands.insertContent({
802
+ type: "button",
803
+ content: [{
804
+ type: "text",
805
+ text: "Button"
806
+ }]
807
+ });
808
+ }
809
+ };
810
+ },
811
+ renderToReactEmail({ children, node, style }) {
812
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
813
+ return /* @__PURE__ */ jsx(Row, { children: /* @__PURE__ */ jsx(Column, {
814
+ align: node.attrs?.align || node.attrs?.alignment,
815
+ children: /* @__PURE__ */ jsx(Button$1, {
816
+ className: node.attrs?.class || void 0,
817
+ href: node.attrs?.href,
818
+ style: {
819
+ ...style,
820
+ ...inlineStyles
821
+ },
822
+ children
823
+ })
824
+ }) });
1075
825
  }
1076
826
  });
1077
827
 
1078
828
  //#endregion
1079
- //#region src/extensions/style-attribute.tsx
1080
- const StyleAttribute = Extension.create({
1081
- name: "styleAttribute",
1082
- priority: 101,
829
+ //#region src/extensions/class-attribute.tsx
830
+ const ClassAttribute = Extension.create({
831
+ name: "classAttribute",
1083
832
  addOptions() {
1084
833
  return {
1085
834
  types: [],
1086
- style: []
835
+ class: []
1087
836
  };
1088
837
  },
1089
838
  addGlobalAttributes() {
1090
839
  return [{
1091
840
  types: this.options.types,
1092
- attributes: { style: {
841
+ attributes: { class: {
1093
842
  default: "",
1094
- parseHTML: (element) => element.getAttribute("style") || "",
843
+ parseHTML: (element) => element.className || "",
1095
844
  renderHTML: (attributes) => {
1096
- return { style: attributes.style ?? "" };
845
+ return attributes.class ? { class: attributes.class } : {};
1097
846
  }
1098
847
  } }
1099
848
  }];
1100
849
  },
1101
850
  addCommands() {
1102
851
  return {
1103
- unsetStyle: () => ({ commands }) => {
1104
- return this.options.types.every((type) => commands.resetAttributes(type, "style"));
852
+ unsetClass: () => ({ commands }) => {
853
+ return this.options.types.every((type) => commands.resetAttributes(type, "class"));
1105
854
  },
1106
- setStyle: (style) => ({ commands }) => {
1107
- return this.options.types.every((type) => commands.updateAttributes(type, { style }));
855
+ setClass: (classList) => ({ commands }) => {
856
+ return this.options.types.every((type) => commands.updateAttributes(type, { class: classList }));
1108
857
  }
1109
858
  };
1110
859
  },
1111
860
  addKeyboardShortcuts() {
1112
861
  return { Enter: ({ editor }) => {
1113
- const { state } = editor.view;
1114
- const { selection } = state;
1115
- const { $from } = selection;
1116
- const textBefore = $from.nodeBefore?.text || "";
1117
- if (textBefore.includes("{{") || textBefore.includes("{{{")) return false;
1118
862
  requestAnimationFrame(() => {
1119
- editor.commands.resetAttributes("paragraph", "style");
863
+ editor.commands.resetAttributes("paragraph", "class");
1120
864
  });
1121
865
  return false;
1122
866
  } };
@@ -1124,160 +868,259 @@ const StyleAttribute = Extension.create({
1124
868
  });
1125
869
 
1126
870
  //#endregion
1127
- //#region src/extensions/sup.ts
1128
- /**
1129
- * This extension allows you to mark text as superscript.
1130
- * @see https://tiptap.dev/api/marks/superscript
1131
- */
1132
- const Sup = Mark.create({
1133
- name: "sup",
1134
- addOptions() {
1135
- return { HTMLAttributes: {} };
1136
- },
1137
- parseHTML() {
1138
- return [{ tag: "sup" }];
1139
- },
1140
- renderHTML({ HTMLAttributes }) {
1141
- return [
1142
- "sup",
1143
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
1144
- 0
1145
- ];
871
+ //#region src/extensions/code.tsx
872
+ const Code = EmailMark.from(CodeBase, ({ children, node, style }) => /* @__PURE__ */ jsx("code", {
873
+ style: {
874
+ ...style,
875
+ ...inlineCssToJs(node.attrs?.style)
1146
876
  },
1147
- addCommands() {
1148
- return {
1149
- setSup: () => ({ commands }) => {
1150
- return commands.setMark(this.name);
1151
- },
1152
- toggleSup: () => ({ commands }) => {
1153
- return commands.toggleMark(this.name);
1154
- },
1155
- unsetSup: () => ({ commands }) => {
1156
- return commands.unsetMark(this.name);
1157
- }
1158
- };
1159
- }
1160
- });
877
+ children
878
+ }));
1161
879
 
1162
880
  //#endregion
1163
- //#region src/extensions/table.tsx
1164
- const Table = EmailNode.create({
1165
- name: "table",
1166
- group: "block",
1167
- content: "tableRow+",
1168
- isolating: true,
1169
- tableRole: "table",
1170
- addAttributes() {
1171
- return { ...createStandardAttributes([
1172
- ...TABLE_ATTRIBUTES,
1173
- ...LAYOUT_ATTRIBUTES,
1174
- ...COMMON_HTML_ATTRIBUTES
1175
- ]) };
1176
- },
1177
- parseHTML() {
1178
- return [{
1179
- tag: "table",
1180
- getAttrs: (node) => {
1181
- if (typeof node === "string") return false;
1182
- const element = node;
1183
- const attrs = {};
1184
- Array.from(element.attributes).forEach((attr) => {
1185
- attrs[attr.name] = attr.value;
881
+ //#region src/utils/prism-utils.ts
882
+ const publicURL = "/styles/prism";
883
+ function loadPrismTheme(theme) {
884
+ const link = document.createElement("link");
885
+ link.rel = "stylesheet";
886
+ link.href = `${publicURL}/prism-${theme}.css`;
887
+ link.setAttribute("data-prism-theme", "");
888
+ document.head.appendChild(link);
889
+ }
890
+ function removePrismTheme() {
891
+ const existingTheme = document.querySelectorAll("link[rel=\"stylesheet\"][data-prism-theme]");
892
+ if (existingTheme.length > 0) existingTheme.forEach((cssLinkTag) => {
893
+ cssLinkTag.remove();
894
+ });
895
+ }
896
+ function hasPrismThemeLoaded(theme) {
897
+ return !!document.querySelector(`link[rel="stylesheet"][data-prism-theme][href="${publicURL}/prism-${theme}.css"]`);
898
+ }
899
+
900
+ //#endregion
901
+ //#region src/extensions/prism-plugin.ts
902
+ const PRISM_LANGUAGE_LOADED_META = "prismLanguageLoaded";
903
+ function parseNodes(nodes, className = []) {
904
+ return nodes.flatMap((node) => {
905
+ const classes = [...className, ...node.properties ? node.properties.className : []];
906
+ if (node.children) return parseNodes(node.children, classes);
907
+ return {
908
+ text: node.value ?? "",
909
+ classes
910
+ };
911
+ });
912
+ }
913
+ function getHighlightNodes(html) {
914
+ return fromHtml(html, { fragment: true }).children;
915
+ }
916
+ function registeredLang(aliasOrLanguage) {
917
+ const allSupportLang = Object.keys(Prism.languages).filter((id) => typeof Prism.languages[id] === "object");
918
+ return Boolean(allSupportLang.find((x) => x === aliasOrLanguage));
919
+ }
920
+ function getDecorations({ doc, name, defaultLanguage, defaultTheme, loadingLanguages, onLanguageLoaded }) {
921
+ const decorations = [];
922
+ findChildren(doc, (node) => node.type.name === name).forEach((block) => {
923
+ let from = block.pos + 1;
924
+ const language = block.node.attrs.language || defaultLanguage;
925
+ const theme = block.node.attrs.theme || defaultTheme;
926
+ let html = "";
927
+ try {
928
+ if (!registeredLang(language) && !loadingLanguages.has(language)) {
929
+ loadingLanguages.add(language);
930
+ import(`prismjs/components/prism-${language}`).then(() => {
931
+ loadingLanguages.delete(language);
932
+ onLanguageLoaded(language);
933
+ }).catch(() => {
934
+ loadingLanguages.delete(language);
1186
935
  });
1187
- return attrs;
1188
936
  }
1189
- }];
1190
- },
1191
- renderHTML({ HTMLAttributes }) {
1192
- return [
1193
- "table",
1194
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
1195
- [
1196
- "tbody",
1197
- {},
1198
- 0
1199
- ]
1200
- ];
1201
- },
1202
- renderToReactEmail({ children, node, styles }) {
1203
- const inlineStyles = inlineCssToJs(node.attrs?.style);
1204
- const alignment = node.attrs?.align || node.attrs?.alignment;
1205
- const width = node.attrs?.width;
1206
- const centeringStyles = alignment === "center" ? {
1207
- marginLeft: "auto",
1208
- marginRight: "auto"
1209
- } : {};
1210
- return /* @__PURE__ */ jsx(Section$1, {
1211
- className: node.attrs?.class || void 0,
1212
- align: alignment,
1213
- style: resolveConflictingStyles(styles.reset, {
1214
- ...inlineStyles,
1215
- ...centeringStyles
1216
- }),
1217
- ...width !== void 0 ? { width } : {},
1218
- children
937
+ if (!hasPrismThemeLoaded(theme)) loadPrismTheme(theme);
938
+ html = Prism.highlight(block.node.textContent, Prism.languages[language], language);
939
+ } catch {
940
+ html = Prism.highlight(block.node.textContent, Prism.languages.javascript, "js");
941
+ }
942
+ parseNodes(getHighlightNodes(html)).forEach((node) => {
943
+ const to = from + node.text.length;
944
+ if (node.classes.length) {
945
+ const decoration = Decoration.inline(from, to, { class: node.classes.join(" ") });
946
+ decorations.push(decoration);
947
+ }
948
+ from = to;
1219
949
  });
1220
- }
1221
- });
1222
- const TableRow = EmailNode.create({
1223
- name: "tableRow",
1224
- group: "tableRow",
1225
- content: "(tableCell | tableHeader)+",
1226
- addAttributes() {
1227
- return { ...createStandardAttributes([
1228
- ...TABLE_CELL_ATTRIBUTES,
1229
- ...LAYOUT_ATTRIBUTES,
1230
- ...COMMON_HTML_ATTRIBUTES
1231
- ]) };
1232
- },
1233
- parseHTML() {
1234
- return [{
1235
- tag: "tr",
1236
- getAttrs: (node) => {
1237
- if (typeof node === "string") return false;
1238
- const element = node;
1239
- const attrs = {};
1240
- Array.from(element.attributes).forEach((attr) => {
1241
- attrs[attr.name] = attr.value;
950
+ });
951
+ return DecorationSet.create(doc, decorations);
952
+ }
953
+ function PrismPlugin({ name, defaultLanguage, defaultTheme }) {
954
+ if (!defaultLanguage) throw Error("You must specify the defaultLanguage parameter");
955
+ const loadingLanguages = /* @__PURE__ */ new Set();
956
+ let pluginView = null;
957
+ const onLanguageLoaded = (language) => {
958
+ if (pluginView) pluginView.dispatch(pluginView.state.tr.setMeta(PRISM_LANGUAGE_LOADED_META, language));
959
+ };
960
+ const prismjsPlugin = new Plugin({
961
+ key: new PluginKey("prism"),
962
+ view(view) {
963
+ pluginView = view;
964
+ return { destroy() {
965
+ pluginView = null;
966
+ } };
967
+ },
968
+ state: {
969
+ init: (_, { doc }) => {
970
+ return getDecorations({
971
+ doc,
972
+ name,
973
+ defaultLanguage,
974
+ defaultTheme,
975
+ loadingLanguages,
976
+ onLanguageLoaded
1242
977
  });
1243
- return attrs;
978
+ },
979
+ apply: (transaction, decorationSet, oldState, newState) => {
980
+ const oldNodeName = oldState.selection.$head.parent.type.name;
981
+ const newNodeName = newState.selection.$head.parent.type.name;
982
+ const oldNodes = findChildren(oldState.doc, (node) => node.type.name === name);
983
+ const newNodes = findChildren(newState.doc, (node) => node.type.name === name);
984
+ if (transaction.getMeta(PRISM_LANGUAGE_LOADED_META) || transaction.docChanged && ([oldNodeName, newNodeName].includes(name) || newNodes.length !== oldNodes.length || transaction.steps.some((step) => {
985
+ const rangeStep = step;
986
+ return rangeStep.from !== void 0 && rangeStep.to !== void 0 && oldNodes.some((node) => {
987
+ return node.pos >= rangeStep.from && node.pos + node.node.nodeSize <= rangeStep.to;
988
+ });
989
+ }))) return getDecorations({
990
+ doc: transaction.doc,
991
+ name,
992
+ defaultLanguage,
993
+ defaultTheme,
994
+ loadingLanguages,
995
+ onLanguageLoaded
996
+ });
997
+ return decorationSet.map(transaction.mapping, transaction.doc);
1244
998
  }
1245
- }];
999
+ },
1000
+ props: { decorations(state) {
1001
+ return prismjsPlugin.getState(state);
1002
+ } },
1003
+ destroy() {
1004
+ pluginView = null;
1005
+ removePrismTheme();
1006
+ }
1007
+ });
1008
+ return prismjsPlugin;
1009
+ }
1010
+
1011
+ //#endregion
1012
+ //#region src/extensions/code-block.tsx
1013
+ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
1014
+ addOptions() {
1015
+ return {
1016
+ languageClassPrefix: "language-",
1017
+ exitOnTripleEnter: false,
1018
+ exitOnArrowDown: false,
1019
+ enableTabIndentation: true,
1020
+ tabSize: 2,
1021
+ defaultLanguage: "javascript",
1022
+ defaultTheme: "default",
1023
+ HTMLAttributes: {}
1024
+ };
1246
1025
  },
1247
- renderHTML({ HTMLAttributes }) {
1026
+ addAttributes() {
1027
+ return {
1028
+ ...this.parent?.(),
1029
+ language: {
1030
+ default: this.options.defaultLanguage,
1031
+ parseHTML: (element) => {
1032
+ if (!element) return null;
1033
+ const { languageClassPrefix } = this.options;
1034
+ if (!languageClassPrefix) return null;
1035
+ const language = [...element.firstElementChild?.classList || []].filter((className) => className.startsWith(languageClassPrefix || "")).map((className) => className.replace(languageClassPrefix, ""))[0];
1036
+ if (!language) return null;
1037
+ return language;
1038
+ },
1039
+ rendered: false
1040
+ },
1041
+ theme: {
1042
+ default: this.options.defaultTheme,
1043
+ rendered: false
1044
+ }
1045
+ };
1046
+ },
1047
+ renderHTML({ node, HTMLAttributes }) {
1248
1048
  return [
1249
- "tr",
1250
- HTMLAttributes,
1251
- 0
1049
+ "pre",
1050
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { class: node.attrs.language ? `${this.options.languageClassPrefix}${node.attrs.language}` : null }, { "data-theme": node.attrs.theme }),
1051
+ [
1052
+ "code",
1053
+ { class: node.attrs.language ? `${this.options.languageClassPrefix}${node.attrs.language} node-codeTag` : "node-codeTag" },
1054
+ 0
1055
+ ]
1252
1056
  ];
1253
1057
  },
1254
- renderToReactEmail({ children, node, styles }) {
1255
- const inlineStyles = inlineCssToJs(node.attrs?.style);
1256
- return /* @__PURE__ */ jsx("tr", {
1257
- className: node.attrs?.class || void 0,
1258
- style: {
1259
- ...styles.reset,
1260
- ...inlineStyles
1261
- },
1262
- children
1263
- });
1058
+ addKeyboardShortcuts() {
1059
+ return {
1060
+ ...this.parent?.(),
1061
+ "Mod-a": ({ editor }) => {
1062
+ const { state } = editor;
1063
+ const { selection } = state;
1064
+ const { $from } = selection;
1065
+ for (let depth = $from.depth; depth >= 1; depth--) if ($from.node(depth).type.name === this.name) {
1066
+ const blockStart = $from.start(depth);
1067
+ const blockEnd = $from.end(depth);
1068
+ if (selection.from === blockStart && selection.to === blockEnd) return false;
1069
+ const tr = state.tr.setSelection(TextSelection.create(state.doc, blockStart, blockEnd));
1070
+ editor.view.dispatch(tr);
1071
+ return true;
1072
+ }
1073
+ return false;
1074
+ }
1075
+ };
1076
+ },
1077
+ addProseMirrorPlugins() {
1078
+ return [...this.parent?.() || [], PrismPlugin({
1079
+ name: this.name,
1080
+ defaultLanguage: this.options.defaultLanguage,
1081
+ defaultTheme: this.options.defaultTheme
1082
+ })];
1264
1083
  }
1084
+ }), ({ node, style }) => {
1085
+ const language = node.attrs?.language ? `${node.attrs.language}` : "javascript";
1086
+ const userTheme = ReactEmailComponents[node.attrs?.theme];
1087
+ const theme = userTheme ? {
1088
+ ...userTheme,
1089
+ base: {
1090
+ ...userTheme.base,
1091
+ borderRadius: "0.125rem",
1092
+ padding: "0.75rem 1rem"
1093
+ }
1094
+ } : { base: {
1095
+ color: "#1e293b",
1096
+ background: "#f1f5f9",
1097
+ lineHeight: "1.5",
1098
+ fontFamily: "\"Fira Code\", \"Fira Mono\", Menlo, Consolas, \"DejaVu Sans Mono\", monospace",
1099
+ padding: "0.75rem 1rem",
1100
+ borderRadius: "0.125rem"
1101
+ } };
1102
+ return /* @__PURE__ */ jsx(CodeBlock, {
1103
+ code: node.content?.[0]?.text ?? "",
1104
+ language,
1105
+ theme,
1106
+ style: {
1107
+ width: "auto",
1108
+ ...style
1109
+ }
1110
+ });
1265
1111
  });
1266
- const TableCell = EmailNode.create({
1267
- name: "tableCell",
1268
- group: "tableCell",
1112
+
1113
+ //#endregion
1114
+ //#region src/extensions/div.tsx
1115
+ const Div = EmailNode.create({
1116
+ name: "div",
1117
+ group: "block",
1269
1118
  content: "block+",
1119
+ defining: true,
1270
1120
  isolating: true,
1271
- addAttributes() {
1272
- return { ...createStandardAttributes([
1273
- ...TABLE_CELL_ATTRIBUTES,
1274
- ...LAYOUT_ATTRIBUTES,
1275
- ...COMMON_HTML_ATTRIBUTES
1276
- ]) };
1277
- },
1278
1121
  parseHTML() {
1279
1122
  return [{
1280
- tag: "td",
1123
+ tag: "div:not([data-type])",
1281
1124
  getAttrs: (node) => {
1282
1125
  if (typeof node === "string") return false;
1283
1126
  const element = node;
@@ -1291,60 +1134,2992 @@ const TableCell = EmailNode.create({
1291
1134
  },
1292
1135
  renderHTML({ HTMLAttributes }) {
1293
1136
  return [
1294
- "td",
1295
- HTMLAttributes,
1137
+ "div",
1138
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
1296
1139
  0
1297
1140
  ];
1298
1141
  },
1299
- renderToReactEmail({ children, node, styles }) {
1142
+ addAttributes() {
1143
+ return { ...createStandardAttributes([...COMMON_HTML_ATTRIBUTES, ...LAYOUT_ATTRIBUTES]) };
1144
+ },
1145
+ renderToReactEmail({ children, node, style }) {
1300
1146
  const inlineStyles = inlineCssToJs(node.attrs?.style);
1301
- return /* @__PURE__ */ jsx(Column, {
1147
+ return /* @__PURE__ */ jsx("div", {
1302
1148
  className: node.attrs?.class || void 0,
1303
- align: node.attrs?.align || node.attrs?.alignment,
1304
1149
  style: {
1305
- ...styles.reset,
1150
+ ...style,
1306
1151
  ...inlineStyles
1307
1152
  },
1308
1153
  children
1309
1154
  });
1310
1155
  }
1311
1156
  });
1312
- const TableHeader = Node.create({
1313
- name: "tableHeader",
1314
- group: "tableCell",
1315
- content: "block+",
1316
- isolating: true,
1157
+
1158
+ //#endregion
1159
+ //#region src/extensions/divider.tsx
1160
+ const Divider = EmailNode.from(HorizontalRule.extend({
1317
1161
  addAttributes() {
1318
- return { ...createStandardAttributes([
1319
- ...TABLE_HEADER_ATTRIBUTES,
1320
- ...TABLE_CELL_ATTRIBUTES,
1321
- ...LAYOUT_ATTRIBUTES,
1322
- ...COMMON_HTML_ATTRIBUTES
1323
- ]) };
1162
+ return { class: { default: "divider" } };
1324
1163
  },
1325
- parseHTML() {
1326
- return [{
1327
- tag: "th",
1328
- getAttrs: (node) => {
1329
- if (typeof node === "string") return false;
1330
- const element = node;
1331
- const attrs = {};
1332
- Array.from(element.attributes).forEach((attr) => {
1333
- attrs[attr.name] = attr.value;
1334
- });
1335
- return attrs;
1164
+ addInputRules() {
1165
+ return [new InputRule({
1166
+ find: /^(?:---|—-|___\s|\*\*\*\s)$/,
1167
+ handler: ({ state, range }) => {
1168
+ const attributes = {};
1169
+ const { tr } = state;
1170
+ const start = range.from;
1171
+ const end = range.to;
1172
+ tr.insert(start - 1, this.type.create(attributes)).delete(tr.mapping.map(start), tr.mapping.map(end));
1336
1173
  }
1337
- }];
1174
+ })];
1338
1175
  },
1339
- renderHTML({ HTMLAttributes }) {
1340
- return [
1341
- "th",
1342
- HTMLAttributes,
1343
- 0
1344
- ];
1176
+ addNodeView() {
1177
+ return ReactNodeViewRenderer((props) => {
1178
+ const node = props.node;
1179
+ const { class: className, ...rest } = node.attrs;
1180
+ return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsx(Hr, {
1181
+ ...rest,
1182
+ className: "node-hr",
1183
+ style: inlineCssToJs(node.attrs.style)
1184
+ }) });
1185
+ });
1345
1186
  }
1187
+ }), ({ node, style }) => {
1188
+ return /* @__PURE__ */ jsx(Hr, {
1189
+ className: node.attrs?.class || void 0,
1190
+ style: {
1191
+ ...style,
1192
+ ...inlineCssToJs(node.attrs?.style)
1193
+ }
1194
+ });
1346
1195
  });
1347
1196
 
1348
1197
  //#endregion
1349
- export { AlignmentAttribute, Body, Bold, Button, ClassAttribute, CodeBlockPrism, Div, EmailNode, MaxNesting, PreservedStyle, Section, StyleAttribute, Sup, Table, TableCell, TableHeader, TableRow, createPlaceholderExtension, placeholder, processStylesForUnlink };
1198
+ //#region src/extensions/global-content.ts
1199
+ const GLOBAL_CONTENT_NODE_TYPE = "globalContent";
1200
+ let cachedGlobalPosition = null;
1201
+ function findGlobalContentPositions(doc) {
1202
+ const positions = [];
1203
+ doc.descendants((node, position) => {
1204
+ if (node.type.name === GLOBAL_CONTENT_NODE_TYPE) positions.push(position);
1205
+ });
1206
+ return positions;
1207
+ }
1208
+ function getCachedGlobalContentPosition(doc) {
1209
+ if (cachedGlobalPosition != null) try {
1210
+ if (doc.nodeAt(cachedGlobalPosition)?.type.name === GLOBAL_CONTENT_NODE_TYPE) return cachedGlobalPosition;
1211
+ } catch {
1212
+ cachedGlobalPosition = null;
1213
+ }
1214
+ cachedGlobalPosition = findGlobalContentPositions(doc)[0] ?? null;
1215
+ return cachedGlobalPosition;
1216
+ }
1217
+ function getGlobalContent(key, editor) {
1218
+ const position = getCachedGlobalContentPosition(editor.state.doc);
1219
+ if (cachedGlobalPosition == null) return null;
1220
+ return editor.state.doc.nodeAt(position)?.attrs.data[key] ?? null;
1221
+ }
1222
+ const GlobalContent = Node$1.create({
1223
+ name: GLOBAL_CONTENT_NODE_TYPE,
1224
+ addOptions() {
1225
+ return {
1226
+ key: GLOBAL_CONTENT_NODE_TYPE,
1227
+ data: {}
1228
+ };
1229
+ },
1230
+ group: "block",
1231
+ selectable: false,
1232
+ draggable: false,
1233
+ atom: true,
1234
+ addAttributes() {
1235
+ return { data: { default: this.options.data } };
1236
+ },
1237
+ parseHTML() {
1238
+ return [{ tag: `div[data-type="${this.name}"]` }];
1239
+ },
1240
+ renderHTML({ HTMLAttributes }) {
1241
+ return ["div", mergeAttributes(HTMLAttributes, {
1242
+ "data-type": this.name,
1243
+ style: "width: 100%; height: 1px; visibility: hidden;"
1244
+ })];
1245
+ },
1246
+ addCommands() {
1247
+ return { setGlobalContent: (key, value) => ({ tr, dispatch }) => {
1248
+ const ensureGlobalPosition = () => {
1249
+ const positions = findGlobalContentPositions(tr.doc);
1250
+ for (let i = positions.length - 1; i > 0; i--) tr.delete(positions[i], positions[i] + 1);
1251
+ const pos = positions[0] ?? -1;
1252
+ if (pos >= 0) cachedGlobalPosition = pos;
1253
+ else {
1254
+ cachedGlobalPosition = 0;
1255
+ tr.insert(0, this.type.create());
1256
+ }
1257
+ };
1258
+ if (dispatch) {
1259
+ ensureGlobalPosition();
1260
+ if (cachedGlobalPosition == null) return false;
1261
+ tr.setNodeAttribute(cachedGlobalPosition, "data", {
1262
+ ...tr.doc.nodeAt(cachedGlobalPosition)?.attrs.data,
1263
+ [key]: value
1264
+ });
1265
+ }
1266
+ return true;
1267
+ } };
1268
+ }
1269
+ });
1270
+
1271
+ //#endregion
1272
+ //#region src/extensions/hard-break.tsx
1273
+ const HardBreak = EmailNode.from(HardBreakBase, () => /* @__PURE__ */ jsx("br", {}));
1274
+
1275
+ //#endregion
1276
+ //#region src/extensions/heading.tsx
1277
+ const Heading = EmailNode.from(Heading$2.extend({ addNodeView() {
1278
+ return ReactNodeViewRenderer(({ node }) => {
1279
+ const level = node.attrs.level ?? 1;
1280
+ const { class: className, ...rest } = node.attrs;
1281
+ const attrs = {
1282
+ ...rest,
1283
+ className: `node-h${level} ${className}`,
1284
+ style: inlineCssToJs(node.attrs.style)
1285
+ };
1286
+ return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsx(Heading$1, {
1287
+ as: `h${level}`,
1288
+ ...attrs,
1289
+ children: /* @__PURE__ */ jsx(NodeViewContent, {})
1290
+ }) });
1291
+ });
1292
+ } }), ({ children, node, style }) => {
1293
+ return /* @__PURE__ */ jsx(Heading$1, {
1294
+ as: `h${node.attrs?.level ?? 1}`,
1295
+ className: node.attrs?.class || void 0,
1296
+ style: {
1297
+ ...style,
1298
+ ...inlineCssToJs(node.attrs?.style),
1299
+ ...getTextAlignment(node.attrs?.align ?? node.attrs?.alignment)
1300
+ },
1301
+ children
1302
+ });
1303
+ });
1304
+
1305
+ //#endregion
1306
+ //#region src/extensions/italic.tsx
1307
+ const Italic = EmailMark.from(ItalicBase, ({ children, style }) => /* @__PURE__ */ jsx("em", {
1308
+ style,
1309
+ children
1310
+ }));
1311
+
1312
+ //#endregion
1313
+ //#region src/extensions/preserved-style.tsx
1314
+ const PreservedStyle = EmailMark.create({
1315
+ name: "preservedStyle",
1316
+ addAttributes() {
1317
+ return { style: {
1318
+ default: null,
1319
+ parseHTML: (element) => element.getAttribute("style"),
1320
+ renderHTML: (attributes) => {
1321
+ if (!attributes.style) return {};
1322
+ return { style: attributes.style };
1323
+ }
1324
+ } };
1325
+ },
1326
+ parseHTML() {
1327
+ return [{
1328
+ tag: "span[style]",
1329
+ getAttrs: (element) => {
1330
+ if (typeof element === "string") return false;
1331
+ const style = element.getAttribute("style");
1332
+ if (style && hasPreservableStyles(style)) return { style };
1333
+ return false;
1334
+ }
1335
+ }];
1336
+ },
1337
+ renderHTML({ HTMLAttributes }) {
1338
+ return [
1339
+ "span",
1340
+ mergeAttributes(HTMLAttributes),
1341
+ 0
1342
+ ];
1343
+ },
1344
+ renderToReactEmail({ children, mark }) {
1345
+ return /* @__PURE__ */ jsx("span", {
1346
+ style: mark.attrs?.style ? inlineCssToJs(mark.attrs.style) : void 0,
1347
+ children
1348
+ });
1349
+ }
1350
+ });
1351
+ const LINK_INDICATOR_STYLES = [
1352
+ "color",
1353
+ "text-decoration",
1354
+ "text-decoration-line",
1355
+ "text-decoration-color",
1356
+ "text-decoration-style"
1357
+ ];
1358
+ function parseStyleString(styleString) {
1359
+ const temp = document.createElement("div");
1360
+ temp.style.cssText = styleString;
1361
+ return temp.style;
1362
+ }
1363
+ function hasBackground(style) {
1364
+ const bgColor = style.backgroundColor;
1365
+ const bg = style.background;
1366
+ if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") return true;
1367
+ if (bg && bg !== "transparent" && bg !== "none" && bg !== "rgba(0, 0, 0, 0)") return true;
1368
+ return false;
1369
+ }
1370
+ function hasPreservableStyles(styleString) {
1371
+ return processStylesForUnlink(styleString) !== null;
1372
+ }
1373
+ /**
1374
+ * Processes styles when unlinking:
1375
+ * - Has background (button-like): preserve all styles
1376
+ * - No background: strip link-indicator styles (color, text-decoration), keep the rest
1377
+ */
1378
+ function processStylesForUnlink(styleString) {
1379
+ if (!styleString) return null;
1380
+ const style = parseStyleString(styleString);
1381
+ if (hasBackground(style)) return styleString;
1382
+ const filtered = [];
1383
+ for (let i = 0; i < style.length; i++) {
1384
+ const prop = style[i];
1385
+ if (LINK_INDICATOR_STYLES.includes(prop)) continue;
1386
+ const value = style.getPropertyValue(prop);
1387
+ if (value) filtered.push(`${prop}: ${value}`);
1388
+ }
1389
+ return filtered.length > 0 ? filtered.join("; ") : null;
1390
+ }
1391
+
1392
+ //#endregion
1393
+ //#region src/extensions/link.tsx
1394
+ const Link = EmailMark.from(TiptapLink, ({ children, mark, style }) => {
1395
+ const linkMarkStyle = mark.attrs?.style ? inlineCssToJs(mark.attrs.style) : {};
1396
+ return /* @__PURE__ */ jsx(Link$1, {
1397
+ href: mark.attrs?.href ?? void 0,
1398
+ rel: mark.attrs?.rel ?? void 0,
1399
+ style: {
1400
+ ...style,
1401
+ ...linkMarkStyle
1402
+ },
1403
+ target: mark.attrs?.target ?? void 0,
1404
+ ...mark.attrs?.["ses:no-track"] ? { "ses:no-track": mark.attrs["ses:no-track"] } : {},
1405
+ children
1406
+ });
1407
+ }).extend({
1408
+ parseHTML() {
1409
+ return [{
1410
+ tag: "a[target]:not([data-id=\"react-email-button\"])",
1411
+ getAttrs: (node) => {
1412
+ if (typeof node === "string") return false;
1413
+ const element = node;
1414
+ const attrs = {};
1415
+ Array.from(element.attributes).forEach((attr) => {
1416
+ attrs[attr.name] = attr.value;
1417
+ });
1418
+ return attrs;
1419
+ }
1420
+ }, {
1421
+ tag: "a[href]:not([data-id=\"react-email-button\"])",
1422
+ getAttrs: (node) => {
1423
+ if (typeof node === "string") return false;
1424
+ const element = node;
1425
+ const attrs = {};
1426
+ Array.from(element.attributes).forEach((attr) => {
1427
+ attrs[attr.name] = attr.value;
1428
+ });
1429
+ return attrs;
1430
+ }
1431
+ }];
1432
+ },
1433
+ addAttributes() {
1434
+ return {
1435
+ ...this.parent?.(),
1436
+ "ses:no-track": {
1437
+ default: null,
1438
+ parseHTML: (element) => element.getAttribute("ses:no-track")
1439
+ }
1440
+ };
1441
+ },
1442
+ addCommands() {
1443
+ return {
1444
+ ...this.parent?.(),
1445
+ unsetLink: () => ({ state, chain }) => {
1446
+ const { from } = state.selection;
1447
+ const linkStyle = state.doc.resolve(from).marks().find((m) => m.type.name === "link")?.attrs?.style ?? null;
1448
+ const preservedStyle = processStylesForUnlink(linkStyle);
1449
+ const shouldRemoveUnderline = preservedStyle !== linkStyle;
1450
+ if (preservedStyle) {
1451
+ const cmd = chain().extendMarkRange("link").unsetMark("link").setMark("preservedStyle", { style: preservedStyle });
1452
+ return shouldRemoveUnderline ? cmd.unsetMark("underline").run() : cmd.run();
1453
+ }
1454
+ return chain().extendMarkRange("link").unsetMark("link").unsetMark("underline").run();
1455
+ }
1456
+ };
1457
+ },
1458
+ addKeyboardShortcuts() {
1459
+ return { "Mod-k": () => {
1460
+ editorEventBus.dispatch("bubble-menu:add-link", void 0);
1461
+ return this.editor.chain().focus().toggleLink({ href: "" }).run();
1462
+ } };
1463
+ }
1464
+ });
1465
+
1466
+ //#endregion
1467
+ //#region src/extensions/list-item.tsx
1468
+ const ListItem = EmailNode.from(ListItemBase, ({ children, node, style }) => /* @__PURE__ */ jsx("li", {
1469
+ className: node.attrs?.class || void 0,
1470
+ style: {
1471
+ ...style,
1472
+ ...inlineCssToJs(node.attrs?.style),
1473
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
1474
+ },
1475
+ children
1476
+ }));
1477
+
1478
+ //#endregion
1479
+ //#region src/extensions/max-nesting.ts
1480
+ const MaxNesting = Extension.create({
1481
+ name: "maxNesting",
1482
+ addOptions() {
1483
+ return {
1484
+ maxDepth: 3,
1485
+ nodeTypes: void 0
1486
+ };
1487
+ },
1488
+ addProseMirrorPlugins() {
1489
+ const { maxDepth, nodeTypes } = this.options;
1490
+ if (typeof maxDepth !== "number" || maxDepth < 1) throw new Error("maxDepth must be a positive number");
1491
+ return [new Plugin({
1492
+ key: new PluginKey("maxNesting"),
1493
+ appendTransaction(transactions, _oldState, newState) {
1494
+ if (!transactions.some((tr$1) => tr$1.docChanged)) return null;
1495
+ const rangesToLift = [];
1496
+ newState.doc.descendants((node, pos) => {
1497
+ let depth = 0;
1498
+ let currentPos = pos;
1499
+ let currentNode = node;
1500
+ while (currentNode && depth <= maxDepth) {
1501
+ if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
1502
+ const $pos = newState.doc.resolve(currentPos);
1503
+ if ($pos.depth === 0) break;
1504
+ currentPos = $pos.before($pos.depth);
1505
+ currentNode = newState.doc.nodeAt(currentPos);
1506
+ }
1507
+ if (depth > maxDepth) {
1508
+ const $pos = newState.doc.resolve(pos);
1509
+ if ($pos.depth > 0) {
1510
+ const range = $pos.blockRange();
1511
+ if (range && "canReplace" in newState.schema.nodes.doc && typeof newState.schema.nodes.doc.canReplace === "function" && newState.schema.nodes.doc.canReplace(range.start - 1, range.end + 1, newState.doc.slice(range.start, range.end).content)) rangesToLift.push({
1512
+ range,
1513
+ target: range.start - 1
1514
+ });
1515
+ }
1516
+ }
1517
+ });
1518
+ if (rangesToLift.length === 0) return null;
1519
+ const tr = newState.tr;
1520
+ for (let i = rangesToLift.length - 1; i >= 0; i--) {
1521
+ const { range, target } = rangesToLift[i];
1522
+ tr.lift(range, target);
1523
+ }
1524
+ return tr;
1525
+ },
1526
+ filterTransaction(tr) {
1527
+ if (!tr.docChanged) return true;
1528
+ let wouldCreateDeepNesting = false;
1529
+ const newDoc = tr.doc;
1530
+ newDoc.descendants((node, pos) => {
1531
+ if (wouldCreateDeepNesting) return false;
1532
+ let depth = 0;
1533
+ let currentPos = pos;
1534
+ let currentNode = node;
1535
+ while (currentNode && depth <= maxDepth) {
1536
+ if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
1537
+ const $pos = newDoc.resolve(currentPos);
1538
+ if ($pos.depth === 0) break;
1539
+ currentPos = $pos.before($pos.depth);
1540
+ currentNode = newDoc.nodeAt(currentPos);
1541
+ }
1542
+ if (depth > maxDepth) {
1543
+ wouldCreateDeepNesting = true;
1544
+ return false;
1545
+ }
1546
+ });
1547
+ return !wouldCreateDeepNesting;
1548
+ }
1549
+ })];
1550
+ }
1551
+ });
1552
+
1553
+ //#endregion
1554
+ //#region src/extensions/ordered-list.tsx
1555
+ const OrderedList = EmailNode.from(OrderedListBase, ({ children, node, style }) => /* @__PURE__ */ jsx("ol", {
1556
+ className: node.attrs?.class || void 0,
1557
+ start: node.attrs?.start,
1558
+ style: {
1559
+ ...style,
1560
+ ...inlineCssToJs(node.attrs?.style)
1561
+ },
1562
+ children
1563
+ }));
1564
+
1565
+ //#endregion
1566
+ //#region src/extensions/paragraph.tsx
1567
+ const Paragraph = EmailNode.from(ParagraphBase, ({ children, node, style }) => {
1568
+ const isEmpty = !node.content || node.content.length === 0;
1569
+ return /* @__PURE__ */ jsx("p", {
1570
+ className: node.attrs?.class || void 0,
1571
+ style: {
1572
+ ...style,
1573
+ ...inlineCssToJs(node.attrs?.style),
1574
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
1575
+ },
1576
+ children: isEmpty ? /* @__PURE__ */ jsx("br", {}) : children
1577
+ });
1578
+ });
1579
+
1580
+ //#endregion
1581
+ //#region src/extensions/placeholder.ts
1582
+ const Placeholder = TipTapPlaceholder.configure({
1583
+ placeholder: ({ node }) => {
1584
+ if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
1585
+ return "Press '/' for commands";
1586
+ },
1587
+ includeChildren: true
1588
+ });
1589
+
1590
+ //#endregion
1591
+ //#region src/extensions/preview-text.ts
1592
+ const PreviewText = Node$1.create({
1593
+ name: "previewText",
1594
+ group: "block",
1595
+ selectable: false,
1596
+ draggable: false,
1597
+ atom: true,
1598
+ addOptions() {
1599
+ return { HTMLAttributes: {} };
1600
+ },
1601
+ addStorage() {
1602
+ return { previewText: null };
1603
+ },
1604
+ renderHTML() {
1605
+ return ["div", { style: "display: none" }];
1606
+ },
1607
+ parseHTML() {
1608
+ return [{
1609
+ tag: "div[data-skip-in-text=\"true\"]",
1610
+ getAttrs: (node) => {
1611
+ if (typeof node === "string") return false;
1612
+ const element = node;
1613
+ let directText = "";
1614
+ for (const child of element.childNodes) if (child.nodeType === 3) directText += child.textContent || "";
1615
+ const cleanText = directText.trim();
1616
+ if (cleanText) this.storage.previewText = cleanText;
1617
+ return false;
1618
+ }
1619
+ }, {
1620
+ tag: "span.preheader",
1621
+ getAttrs: (node) => {
1622
+ if (typeof node === "string") return false;
1623
+ const preheaderText = node.textContent?.trim();
1624
+ if (preheaderText) this.storage.previewText = preheaderText;
1625
+ return false;
1626
+ }
1627
+ }];
1628
+ }
1629
+ });
1630
+
1631
+ //#endregion
1632
+ //#region src/extensions/section.tsx
1633
+ const Section = EmailNode.create({
1634
+ name: "section",
1635
+ group: "block",
1636
+ content: "block+",
1637
+ isolating: true,
1638
+ defining: true,
1639
+ parseHTML() {
1640
+ return [{ tag: "section[data-type=\"section\"]" }];
1641
+ },
1642
+ renderHTML({ HTMLAttributes }) {
1643
+ return [
1644
+ "section",
1645
+ mergeAttributes({
1646
+ "data-type": "section",
1647
+ class: "node-section"
1648
+ }, HTMLAttributes),
1649
+ 0
1650
+ ];
1651
+ },
1652
+ addCommands() {
1653
+ return { insertSection: () => ({ commands }) => {
1654
+ return commands.insertContent({
1655
+ type: this.name,
1656
+ content: [{
1657
+ type: "paragraph",
1658
+ content: []
1659
+ }]
1660
+ });
1661
+ } };
1662
+ },
1663
+ renderToReactEmail({ children, node, style }) {
1664
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
1665
+ const textAlign = node.attrs?.align || node.attrs?.alignment;
1666
+ return /* @__PURE__ */ jsx(Section$1, {
1667
+ className: node.attrs?.class || void 0,
1668
+ align: textAlign,
1669
+ style: {
1670
+ ...style,
1671
+ ...inlineStyles,
1672
+ ...getTextAlignment(textAlign)
1673
+ },
1674
+ children
1675
+ });
1676
+ }
1677
+ });
1678
+
1679
+ //#endregion
1680
+ //#region src/extensions/strike.tsx
1681
+ const Strike = EmailMark.from(StrikeBase, ({ children, style }) => /* @__PURE__ */ jsx("s", {
1682
+ style,
1683
+ children
1684
+ }));
1685
+
1686
+ //#endregion
1687
+ //#region src/extensions/style-attribute.tsx
1688
+ const StyleAttribute = Extension.create({
1689
+ name: "styleAttribute",
1690
+ priority: 101,
1691
+ addOptions() {
1692
+ return {
1693
+ types: [],
1694
+ style: []
1695
+ };
1696
+ },
1697
+ addGlobalAttributes() {
1698
+ return [{
1699
+ types: this.options.types,
1700
+ attributes: { style: {
1701
+ default: "",
1702
+ parseHTML: (element) => element.getAttribute("style") || "",
1703
+ renderHTML: (attributes) => {
1704
+ return { style: attributes.style ?? "" };
1705
+ }
1706
+ } }
1707
+ }];
1708
+ },
1709
+ addCommands() {
1710
+ return {
1711
+ unsetStyle: () => ({ commands }) => {
1712
+ return this.options.types.every((type) => commands.resetAttributes(type, "style"));
1713
+ },
1714
+ setStyle: (style) => ({ commands }) => {
1715
+ return this.options.types.every((type) => commands.updateAttributes(type, { style }));
1716
+ }
1717
+ };
1718
+ },
1719
+ addKeyboardShortcuts() {
1720
+ return { Enter: ({ editor }) => {
1721
+ const { state } = editor.view;
1722
+ const { selection } = state;
1723
+ const { $from } = selection;
1724
+ const textBefore = $from.nodeBefore?.text || "";
1725
+ if (textBefore.includes("{{") || textBefore.includes("{{{")) return false;
1726
+ requestAnimationFrame(() => {
1727
+ editor.commands.resetAttributes("paragraph", "style");
1728
+ });
1729
+ return false;
1730
+ } };
1731
+ }
1732
+ });
1733
+
1734
+ //#endregion
1735
+ //#region src/extensions/sup.tsx
1736
+ /**
1737
+ * This extension allows you to mark text as superscript.
1738
+ * @see https://tiptap.dev/api/marks/superscript
1739
+ */
1740
+ const Sup = EmailMark.create({
1741
+ name: "sup",
1742
+ addOptions() {
1743
+ return { HTMLAttributes: {} };
1744
+ },
1745
+ parseHTML() {
1746
+ return [{ tag: "sup" }];
1747
+ },
1748
+ renderHTML({ HTMLAttributes }) {
1749
+ return [
1750
+ "sup",
1751
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
1752
+ 0
1753
+ ];
1754
+ },
1755
+ renderToReactEmail({ children, style }) {
1756
+ return /* @__PURE__ */ jsx("sup", {
1757
+ style,
1758
+ children
1759
+ });
1760
+ },
1761
+ addCommands() {
1762
+ return {
1763
+ setSup: () => ({ commands }) => {
1764
+ return commands.setMark(this.name);
1765
+ },
1766
+ toggleSup: () => ({ commands }) => {
1767
+ return commands.toggleMark(this.name);
1768
+ },
1769
+ unsetSup: () => ({ commands }) => {
1770
+ return commands.unsetMark(this.name);
1771
+ }
1772
+ };
1773
+ }
1774
+ });
1775
+
1776
+ //#endregion
1777
+ //#region src/extensions/table.tsx
1778
+ const Table = EmailNode.create({
1779
+ name: "table",
1780
+ group: "block",
1781
+ content: "tableRow+",
1782
+ isolating: true,
1783
+ tableRole: "table",
1784
+ addAttributes() {
1785
+ return { ...createStandardAttributes([
1786
+ ...TABLE_ATTRIBUTES,
1787
+ ...LAYOUT_ATTRIBUTES,
1788
+ ...COMMON_HTML_ATTRIBUTES
1789
+ ]) };
1790
+ },
1791
+ parseHTML() {
1792
+ return [{
1793
+ tag: "table",
1794
+ getAttrs: (node) => {
1795
+ if (typeof node === "string") return false;
1796
+ const element = node;
1797
+ const attrs = {};
1798
+ Array.from(element.attributes).forEach((attr) => {
1799
+ attrs[attr.name] = attr.value;
1800
+ });
1801
+ return attrs;
1802
+ }
1803
+ }];
1804
+ },
1805
+ renderHTML({ HTMLAttributes }) {
1806
+ return [
1807
+ "table",
1808
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
1809
+ [
1810
+ "tbody",
1811
+ {},
1812
+ 0
1813
+ ]
1814
+ ];
1815
+ },
1816
+ renderToReactEmail({ children, node, style }) {
1817
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
1818
+ const alignment = node.attrs?.align || node.attrs?.alignment;
1819
+ const width = node.attrs?.width;
1820
+ const centeringStyles = alignment === "center" ? {
1821
+ marginLeft: "auto",
1822
+ marginRight: "auto"
1823
+ } : {};
1824
+ return /* @__PURE__ */ jsx(Section$1, {
1825
+ className: node.attrs?.class || void 0,
1826
+ align: alignment,
1827
+ style: resolveConflictingStyles(style, {
1828
+ ...inlineStyles,
1829
+ ...centeringStyles
1830
+ }),
1831
+ ...width !== void 0 ? { width } : {},
1832
+ children
1833
+ });
1834
+ }
1835
+ });
1836
+ const TableRow = EmailNode.create({
1837
+ name: "tableRow",
1838
+ group: "tableRow",
1839
+ content: "(tableCell | tableHeader)+",
1840
+ addAttributes() {
1841
+ return { ...createStandardAttributes([
1842
+ ...TABLE_CELL_ATTRIBUTES,
1843
+ ...LAYOUT_ATTRIBUTES,
1844
+ ...COMMON_HTML_ATTRIBUTES
1845
+ ]) };
1846
+ },
1847
+ parseHTML() {
1848
+ return [{
1849
+ tag: "tr",
1850
+ getAttrs: (node) => {
1851
+ if (typeof node === "string") return false;
1852
+ const element = node;
1853
+ const attrs = {};
1854
+ Array.from(element.attributes).forEach((attr) => {
1855
+ attrs[attr.name] = attr.value;
1856
+ });
1857
+ return attrs;
1858
+ }
1859
+ }];
1860
+ },
1861
+ renderHTML({ HTMLAttributes }) {
1862
+ return [
1863
+ "tr",
1864
+ HTMLAttributes,
1865
+ 0
1866
+ ];
1867
+ },
1868
+ renderToReactEmail({ children, node, style }) {
1869
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
1870
+ return /* @__PURE__ */ jsx("tr", {
1871
+ className: node.attrs?.class || void 0,
1872
+ style: {
1873
+ ...style,
1874
+ ...inlineStyles
1875
+ },
1876
+ children
1877
+ });
1878
+ }
1879
+ });
1880
+ const TableCell = EmailNode.create({
1881
+ name: "tableCell",
1882
+ group: "tableCell",
1883
+ content: "block+",
1884
+ isolating: true,
1885
+ addAttributes() {
1886
+ return { ...createStandardAttributes([
1887
+ ...TABLE_CELL_ATTRIBUTES,
1888
+ ...LAYOUT_ATTRIBUTES,
1889
+ ...COMMON_HTML_ATTRIBUTES
1890
+ ]) };
1891
+ },
1892
+ parseHTML() {
1893
+ return [{
1894
+ tag: "td",
1895
+ getAttrs: (node) => {
1896
+ if (typeof node === "string") return false;
1897
+ const element = node;
1898
+ const attrs = {};
1899
+ Array.from(element.attributes).forEach((attr) => {
1900
+ attrs[attr.name] = attr.value;
1901
+ });
1902
+ return attrs;
1903
+ }
1904
+ }];
1905
+ },
1906
+ renderHTML({ HTMLAttributes }) {
1907
+ return [
1908
+ "td",
1909
+ HTMLAttributes,
1910
+ 0
1911
+ ];
1912
+ },
1913
+ renderToReactEmail({ children, node, style }) {
1914
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
1915
+ return /* @__PURE__ */ jsx(Column, {
1916
+ className: node.attrs?.class || void 0,
1917
+ align: node.attrs?.align || node.attrs?.alignment,
1918
+ style: {
1919
+ ...style,
1920
+ ...inlineStyles
1921
+ },
1922
+ children
1923
+ });
1924
+ }
1925
+ });
1926
+ const TableHeader = Node$1.create({
1927
+ name: "tableHeader",
1928
+ group: "tableCell",
1929
+ content: "block+",
1930
+ isolating: true,
1931
+ addAttributes() {
1932
+ return { ...createStandardAttributes([
1933
+ ...TABLE_HEADER_ATTRIBUTES,
1934
+ ...TABLE_CELL_ATTRIBUTES,
1935
+ ...LAYOUT_ATTRIBUTES,
1936
+ ...COMMON_HTML_ATTRIBUTES
1937
+ ]) };
1938
+ },
1939
+ parseHTML() {
1940
+ return [{
1941
+ tag: "th",
1942
+ getAttrs: (node) => {
1943
+ if (typeof node === "string") return false;
1944
+ const element = node;
1945
+ const attrs = {};
1946
+ Array.from(element.attributes).forEach((attr) => {
1947
+ attrs[attr.name] = attr.value;
1948
+ });
1949
+ return attrs;
1950
+ }
1951
+ }];
1952
+ },
1953
+ renderHTML({ HTMLAttributes }) {
1954
+ return [
1955
+ "th",
1956
+ HTMLAttributes,
1957
+ 0
1958
+ ];
1959
+ }
1960
+ });
1961
+
1962
+ //#endregion
1963
+ //#region src/extensions/uppercase.tsx
1964
+ const Uppercase = EmailMark.create({
1965
+ name: "uppercase",
1966
+ addOptions() {
1967
+ return { HTMLAttributes: {} };
1968
+ },
1969
+ parseHTML() {
1970
+ return [{
1971
+ tag: "span",
1972
+ getAttrs: (node) => {
1973
+ if (node.style.textTransform === "uppercase") return {};
1974
+ return false;
1975
+ }
1976
+ }];
1977
+ },
1978
+ renderHTML({ HTMLAttributes }) {
1979
+ return [
1980
+ "span",
1981
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { style: "text-transform: uppercase" }),
1982
+ 0
1983
+ ];
1984
+ },
1985
+ renderToReactEmail({ children, style }) {
1986
+ return /* @__PURE__ */ jsx("span", {
1987
+ style: {
1988
+ ...style,
1989
+ textTransform: "uppercase"
1990
+ },
1991
+ children
1992
+ });
1993
+ },
1994
+ addCommands() {
1995
+ return {
1996
+ setUppercase: () => ({ commands }) => {
1997
+ return commands.setMark(this.name);
1998
+ },
1999
+ toggleUppercase: () => ({ commands }) => {
2000
+ return commands.toggleMark(this.name);
2001
+ },
2002
+ unsetUppercase: () => ({ commands }) => {
2003
+ return commands.unsetMark(this.name);
2004
+ }
2005
+ };
2006
+ }
2007
+ });
2008
+
2009
+ //#endregion
2010
+ //#region src/extensions/columns.tsx
2011
+ const COLUMN_PARENT_TYPES = [
2012
+ "twoColumns",
2013
+ "threeColumns",
2014
+ "fourColumns"
2015
+ ];
2016
+ const COLUMN_PARENT_SET = new Set(COLUMN_PARENT_TYPES);
2017
+ const MAX_COLUMNS_DEPTH = 3;
2018
+ function getColumnsDepth(doc, from) {
2019
+ const $from = doc.resolve(from);
2020
+ let depth = 0;
2021
+ for (let d = $from.depth; d > 0; d--) if (COLUMN_PARENT_SET.has($from.node(d).type.name)) depth++;
2022
+ return depth;
2023
+ }
2024
+ const VARIANTS = [
2025
+ {
2026
+ name: "twoColumns",
2027
+ columnCount: 2,
2028
+ content: "columnsColumn columnsColumn",
2029
+ dataType: "two-columns"
2030
+ },
2031
+ {
2032
+ name: "threeColumns",
2033
+ columnCount: 3,
2034
+ content: "columnsColumn columnsColumn columnsColumn",
2035
+ dataType: "three-columns"
2036
+ },
2037
+ {
2038
+ name: "fourColumns",
2039
+ columnCount: 4,
2040
+ content: "columnsColumn{4}",
2041
+ dataType: "four-columns"
2042
+ }
2043
+ ];
2044
+ const NODE_TYPE_MAP = {
2045
+ 2: "twoColumns",
2046
+ 3: "threeColumns",
2047
+ 4: "fourColumns"
2048
+ };
2049
+ function createColumnsNode(config, includeCommands) {
2050
+ return EmailNode.create({
2051
+ name: config.name,
2052
+ group: "block",
2053
+ content: config.content,
2054
+ isolating: true,
2055
+ defining: true,
2056
+ addAttributes() {
2057
+ return createStandardAttributes([...LAYOUT_ATTRIBUTES, ...COMMON_HTML_ATTRIBUTES]);
2058
+ },
2059
+ parseHTML() {
2060
+ return [{ tag: `div[data-type="${config.dataType}"]` }];
2061
+ },
2062
+ renderHTML({ HTMLAttributes }) {
2063
+ return [
2064
+ "div",
2065
+ mergeAttributes({
2066
+ "data-type": config.dataType,
2067
+ class: "node-columns"
2068
+ }, HTMLAttributes),
2069
+ 0
2070
+ ];
2071
+ },
2072
+ ...includeCommands && { addCommands() {
2073
+ return { insertColumns: (count) => ({ commands, state }) => {
2074
+ if (getColumnsDepth(state.doc, state.selection.from) >= MAX_COLUMNS_DEPTH) return false;
2075
+ const nodeType = NODE_TYPE_MAP[count];
2076
+ const children = Array.from({ length: count }, () => ({
2077
+ type: "columnsColumn",
2078
+ content: [{
2079
+ type: "paragraph",
2080
+ content: []
2081
+ }]
2082
+ }));
2083
+ return commands.insertContent({
2084
+ type: nodeType,
2085
+ content: children
2086
+ });
2087
+ } };
2088
+ } },
2089
+ renderToReactEmail({ children, node, style }) {
2090
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
2091
+ return /* @__PURE__ */ jsx(Row, {
2092
+ className: node.attrs?.class || void 0,
2093
+ style: {
2094
+ ...style,
2095
+ ...inlineStyles
2096
+ },
2097
+ children
2098
+ });
2099
+ }
2100
+ });
2101
+ }
2102
+ const TwoColumns = createColumnsNode(VARIANTS[0], true);
2103
+ const ThreeColumns = createColumnsNode(VARIANTS[1], false);
2104
+ const FourColumns = createColumnsNode(VARIANTS[2], false);
2105
+ const ColumnsColumn = EmailNode.create({
2106
+ name: "columnsColumn",
2107
+ group: "columnsColumn",
2108
+ content: "block+",
2109
+ isolating: true,
2110
+ addAttributes() {
2111
+ return { ...createStandardAttributes([...LAYOUT_ATTRIBUTES, ...COMMON_HTML_ATTRIBUTES]) };
2112
+ },
2113
+ parseHTML() {
2114
+ return [{ tag: "div[data-type=\"column\"]" }];
2115
+ },
2116
+ renderHTML({ HTMLAttributes }) {
2117
+ return [
2118
+ "div",
2119
+ mergeAttributes({
2120
+ "data-type": "column",
2121
+ class: "node-column"
2122
+ }, HTMLAttributes),
2123
+ 0
2124
+ ];
2125
+ },
2126
+ addKeyboardShortcuts() {
2127
+ return {
2128
+ Backspace: ({ editor }) => {
2129
+ const { state } = editor;
2130
+ const { selection } = state;
2131
+ const { empty, $from } = selection;
2132
+ if (!empty) return false;
2133
+ for (let depth = $from.depth; depth >= 1; depth--) {
2134
+ if ($from.pos !== $from.start(depth)) break;
2135
+ const indexInParent = $from.index(depth - 1);
2136
+ if (indexInParent === 0) continue;
2137
+ const prevNode = $from.node(depth - 1).child(indexInParent - 1);
2138
+ if (COLUMN_PARENT_SET.has(prevNode.type.name)) {
2139
+ const deleteFrom = $from.before(depth) - prevNode.nodeSize;
2140
+ const deleteTo = $from.before(depth);
2141
+ editor.view.dispatch(state.tr.delete(deleteFrom, deleteTo));
2142
+ return true;
2143
+ }
2144
+ break;
2145
+ }
2146
+ return false;
2147
+ },
2148
+ "Mod-a": ({ editor }) => {
2149
+ const { state } = editor;
2150
+ const { $from } = state.selection;
2151
+ for (let d = $from.depth; d > 0; d--) {
2152
+ if ($from.node(d).type.name !== "columnsColumn") continue;
2153
+ const columnStart = $from.start(d);
2154
+ const columnEnd = $from.end(d);
2155
+ const { from, to } = state.selection;
2156
+ if (from === columnStart && to === columnEnd) return false;
2157
+ editor.view.dispatch(state.tr.setSelection(TextSelection.create(state.doc, columnStart, columnEnd)));
2158
+ return true;
2159
+ }
2160
+ return false;
2161
+ }
2162
+ };
2163
+ },
2164
+ renderToReactEmail({ children, node, style }) {
2165
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
2166
+ const width = node.attrs?.width;
2167
+ return /* @__PURE__ */ jsx(Column, {
2168
+ className: node.attrs?.class || void 0,
2169
+ style: {
2170
+ ...style,
2171
+ ...inlineStyles,
2172
+ ...width ? { width } : {}
2173
+ },
2174
+ children
2175
+ });
2176
+ }
2177
+ });
2178
+
2179
+ //#endregion
2180
+ //#region src/extensions/index.ts
2181
+ const starterKitExtensions = {
2182
+ CodeBlockPrism,
2183
+ Code,
2184
+ Paragraph,
2185
+ BulletList,
2186
+ OrderedList,
2187
+ Blockquote,
2188
+ ListItem,
2189
+ HardBreak,
2190
+ Italic,
2191
+ Placeholder,
2192
+ PreviewText,
2193
+ Bold,
2194
+ Strike,
2195
+ Heading,
2196
+ Divider,
2197
+ Link,
2198
+ Sup,
2199
+ Uppercase,
2200
+ PreservedStyle,
2201
+ Table,
2202
+ TableRow,
2203
+ TableCell,
2204
+ TableHeader,
2205
+ Body,
2206
+ Div,
2207
+ Button,
2208
+ Section,
2209
+ GlobalContent,
2210
+ AlignmentAttribute,
2211
+ StyleAttribute,
2212
+ ClassAttribute,
2213
+ MaxNesting
2214
+ };
2215
+ const StarterKit = Extension.create({
2216
+ name: "reactEmailStarterKit",
2217
+ addOptions() {
2218
+ return {
2219
+ TiptapStarterKit: {},
2220
+ CodeBlockPrism: {
2221
+ defaultLanguage: "javascript",
2222
+ HTMLAttributes: { class: "prism node-codeBlock" }
2223
+ },
2224
+ Code: { HTMLAttributes: {
2225
+ class: "node-inlineCode",
2226
+ spellcheck: "false"
2227
+ } },
2228
+ Paragraph: { HTMLAttributes: { class: "node-paragraph" } },
2229
+ BulletList: { HTMLAttributes: { class: "node-bulletList" } },
2230
+ OrderedList: { HTMLAttributes: { class: "node-orderedList" } },
2231
+ Blockquote: { HTMLAttributes: { class: "node-blockquote" } },
2232
+ ListItem: {},
2233
+ HardBreak: {},
2234
+ Italic: {},
2235
+ Placeholder: {},
2236
+ PreviewText: {},
2237
+ Bold: {},
2238
+ Strike: {},
2239
+ Heading: {},
2240
+ Divider: {},
2241
+ Link: {},
2242
+ Sup: {},
2243
+ Uppercase: {},
2244
+ PreservedStyle: {},
2245
+ Table: {},
2246
+ TableRow: {},
2247
+ TableCell: {},
2248
+ TableHeader: {},
2249
+ Body: {},
2250
+ Div: {},
2251
+ Button: {},
2252
+ Section: {},
2253
+ GlobalContent: {},
2254
+ AlignmentAttribute: { types: [
2255
+ "heading",
2256
+ "paragraph",
2257
+ "image",
2258
+ "blockquote",
2259
+ "codeBlock",
2260
+ "bulletList",
2261
+ "orderedList",
2262
+ "listItem",
2263
+ "button",
2264
+ "youtube",
2265
+ "twitter",
2266
+ "table",
2267
+ "tableRow",
2268
+ "tableCell",
2269
+ "tableHeader",
2270
+ "columnsColumn"
2271
+ ] },
2272
+ StyleAttribute: { types: [
2273
+ "heading",
2274
+ "paragraph",
2275
+ "image",
2276
+ "blockquote",
2277
+ "codeBlock",
2278
+ "bulletList",
2279
+ "orderedList",
2280
+ "listItem",
2281
+ "button",
2282
+ "youtube",
2283
+ "twitter",
2284
+ "horizontalRule",
2285
+ "footer",
2286
+ "section",
2287
+ "div",
2288
+ "body",
2289
+ "table",
2290
+ "tableRow",
2291
+ "tableCell",
2292
+ "tableHeader",
2293
+ "columnsColumn",
2294
+ "link"
2295
+ ] },
2296
+ ClassAttribute: { types: [
2297
+ "heading",
2298
+ "paragraph",
2299
+ "image",
2300
+ "blockquote",
2301
+ "bulletList",
2302
+ "orderedList",
2303
+ "listItem",
2304
+ "button",
2305
+ "youtube",
2306
+ "twitter",
2307
+ "horizontalRule",
2308
+ "footer",
2309
+ "section",
2310
+ "div",
2311
+ "body",
2312
+ "table",
2313
+ "tableRow",
2314
+ "tableCell",
2315
+ "tableHeader",
2316
+ "columnsColumn",
2317
+ "link"
2318
+ ] },
2319
+ MaxNesting: {
2320
+ maxDepth: 50,
2321
+ nodeTypes: [
2322
+ "section",
2323
+ "bulletList",
2324
+ "orderedList"
2325
+ ]
2326
+ }
2327
+ };
2328
+ },
2329
+ addExtensions() {
2330
+ const extensions = [];
2331
+ if (this.options.TiptapStarterKit !== false) extensions.push(TipTapStarterKit.configure({
2332
+ undoRedo: false,
2333
+ heading: false,
2334
+ link: false,
2335
+ underline: false,
2336
+ trailingNode: false,
2337
+ bold: false,
2338
+ italic: false,
2339
+ strike: false,
2340
+ code: false,
2341
+ paragraph: false,
2342
+ bulletList: false,
2343
+ orderedList: false,
2344
+ listItem: false,
2345
+ blockquote: false,
2346
+ hardBreak: false,
2347
+ gapcursor: false,
2348
+ codeBlock: false,
2349
+ horizontalRule: false,
2350
+ dropcursor: {
2351
+ color: "#61a8f8",
2352
+ class: "rounded-full animate-[fade-in_300ms_ease-in-out] !z-40",
2353
+ width: 4
2354
+ },
2355
+ ...this.options.TiptapStarterKit
2356
+ }));
2357
+ for (const [name, extension] of Object.entries(starterKitExtensions)) {
2358
+ const key = name;
2359
+ const extensionOptions = this.options[key];
2360
+ if (extensionOptions !== false) extensions.push(extension.configure(extensionOptions));
2361
+ }
2362
+ return extensions;
2363
+ }
2364
+ });
2365
+
2366
+ //#endregion
2367
+ //#region src/core/create-drop-handler.ts
2368
+ function createDropHandler({ onPaste, onUploadImage }) {
2369
+ return (view, event, _slice, moved) => {
2370
+ if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
2371
+ event.preventDefault();
2372
+ const file = event.dataTransfer.files[0];
2373
+ if (onPaste?.(file, view)) return true;
2374
+ if (file.type.includes("image/") && onUploadImage) {
2375
+ onUploadImage(file, view, (view.posAtCoords({
2376
+ left: event.clientX,
2377
+ top: event.clientY
2378
+ })?.pos || 0) - 1);
2379
+ return true;
2380
+ }
2381
+ }
2382
+ return false;
2383
+ };
2384
+ }
2385
+
2386
+ //#endregion
2387
+ //#region src/utils/paste-sanitizer.ts
2388
+ /**
2389
+ * Sanitizes pasted HTML.
2390
+ * - From editor (has node-* classes): pass through as-is
2391
+ * - From external: strip all styles/classes, keep only semantic HTML
2392
+ */
2393
+ /**
2394
+ * Detects content from the Resend editor by checking for node-* class names.
2395
+ */
2396
+ const EDITOR_CLASS_PATTERN = /class="[^"]*node-/;
2397
+ /**
2398
+ * Attributes to preserve on specific elements for EXTERNAL content.
2399
+ * Only functional attributes - NO style or class.
2400
+ */
2401
+ const PRESERVED_ATTRIBUTES = {
2402
+ a: [
2403
+ "href",
2404
+ "target",
2405
+ "rel"
2406
+ ],
2407
+ img: [
2408
+ "src",
2409
+ "alt",
2410
+ "width",
2411
+ "height"
2412
+ ],
2413
+ td: ["colspan", "rowspan"],
2414
+ th: [
2415
+ "colspan",
2416
+ "rowspan",
2417
+ "scope"
2418
+ ],
2419
+ table: [
2420
+ "border",
2421
+ "cellpadding",
2422
+ "cellspacing"
2423
+ ],
2424
+ "*": ["id"]
2425
+ };
2426
+ function isFromEditor(html) {
2427
+ return EDITOR_CLASS_PATTERN.test(html);
2428
+ }
2429
+ function sanitizePastedHtml(html) {
2430
+ if (isFromEditor(html)) return html;
2431
+ const doc = new DOMParser().parseFromString(html, "text/html");
2432
+ sanitizeNode(doc.body);
2433
+ return doc.body.innerHTML;
2434
+ }
2435
+ function sanitizeNode(node) {
2436
+ if (node.nodeType === Node.ELEMENT_NODE) sanitizeElement(node);
2437
+ for (const child of Array.from(node.childNodes)) sanitizeNode(child);
2438
+ }
2439
+ function sanitizeElement(el) {
2440
+ const allowedForTag = PRESERVED_ATTRIBUTES[el.tagName.toLowerCase()] || [];
2441
+ const allowedGlobal = PRESERVED_ATTRIBUTES["*"] || [];
2442
+ const allowed = new Set([...allowedForTag, ...allowedGlobal]);
2443
+ const attributesToRemove = [];
2444
+ for (const attr of Array.from(el.attributes)) {
2445
+ if (attr.name.startsWith("data-")) {
2446
+ attributesToRemove.push(attr.name);
2447
+ continue;
2448
+ }
2449
+ if (!allowed.has(attr.name)) attributesToRemove.push(attr.name);
2450
+ }
2451
+ for (const attr of attributesToRemove) el.removeAttribute(attr);
2452
+ }
2453
+
2454
+ //#endregion
2455
+ //#region src/core/create-paste-handler.ts
2456
+ function createPasteHandler({ onPaste, onUploadImage, extensions }) {
2457
+ return (view, event, slice) => {
2458
+ const text = event.clipboardData?.getData("text/plain");
2459
+ if (text && onPaste?.(text, view)) {
2460
+ event.preventDefault();
2461
+ return true;
2462
+ }
2463
+ if (event.clipboardData?.files?.[0]) {
2464
+ const file = event.clipboardData.files[0];
2465
+ if (onPaste?.(file, view)) {
2466
+ event.preventDefault();
2467
+ return true;
2468
+ }
2469
+ if (file.type.includes("image/") && onUploadImage) {
2470
+ const pos = view.state.selection.from;
2471
+ onUploadImage(file, view, pos);
2472
+ return true;
2473
+ }
2474
+ }
2475
+ /**
2476
+ * If the coming content has a single child, we can assume
2477
+ * it's a plain text and doesn't need to be parsed and
2478
+ * be introduced in a new line
2479
+ */
2480
+ if (slice.content.childCount === 1) return false;
2481
+ if (event.clipboardData?.getData?.("text/html")) {
2482
+ event.preventDefault();
2483
+ const jsonContent = generateJSON(sanitizePastedHtml(event.clipboardData.getData("text/html")), extensions);
2484
+ const node = view.state.schema.nodeFromJSON(jsonContent);
2485
+ const transaction = view.state.tr.replaceSelectionWith(node, false);
2486
+ view.dispatch(transaction);
2487
+ return true;
2488
+ }
2489
+ return false;
2490
+ };
2491
+ }
2492
+
2493
+ //#endregion
2494
+ //#region src/core/use-editor.ts
2495
+ const COLLABORATION_EXTENSION_NAMES = new Set(["liveblocksExtension", "collaboration"]);
2496
+ function hasCollaborationExtension(exts) {
2497
+ return exts.some((ext) => COLLABORATION_EXTENSION_NAMES.has(ext.name));
2498
+ }
2499
+ function useEditor({ content, extensions = [], onUpdate, onPaste, onUploadImage, onReady, editable = true, ...rest }) {
2500
+ const [contentError, setContentError] = React.useState(null);
2501
+ const isCollaborative = hasCollaborationExtension(extensions);
2502
+ const effectiveExtensions = React.useMemo(() => [
2503
+ StarterKit,
2504
+ ...isCollaborative ? [] : [UndoRedo],
2505
+ ...extensions
2506
+ ], [extensions, isCollaborative]);
2507
+ const editor = useEditor$1({
2508
+ content: isCollaborative ? void 0 : content,
2509
+ extensions: effectiveExtensions,
2510
+ editable,
2511
+ immediatelyRender: false,
2512
+ enableContentCheck: true,
2513
+ onContentError({ editor: editor$1, error, disableCollaboration }) {
2514
+ disableCollaboration();
2515
+ setContentError(error);
2516
+ console.error(error);
2517
+ editor$1.setEditable(false);
2518
+ },
2519
+ onCreate({ editor: editor$1 }) {
2520
+ onReady?.(editor$1);
2521
+ },
2522
+ onUpdate({ editor: editor$1, transaction }) {
2523
+ onUpdate?.(editor$1, transaction);
2524
+ },
2525
+ editorProps: {
2526
+ handleDOMEvents: { click: (view, event) => {
2527
+ if (!view.editable) {
2528
+ if (event.target.closest("a")) {
2529
+ event.preventDefault();
2530
+ return true;
2531
+ }
2532
+ }
2533
+ return false;
2534
+ } },
2535
+ handlePaste: createPasteHandler({
2536
+ onPaste,
2537
+ onUploadImage,
2538
+ extensions: effectiveExtensions
2539
+ }),
2540
+ handleDrop: createDropHandler({
2541
+ onPaste,
2542
+ onUploadImage
2543
+ })
2544
+ },
2545
+ ...rest
2546
+ });
2547
+ return {
2548
+ editor,
2549
+ isEditorEmpty: useEditorState({
2550
+ editor,
2551
+ selector: (context) => {
2552
+ if (!context.editor) return true;
2553
+ return isDocumentVisuallyEmpty(context.editor.state.doc);
2554
+ }
2555
+ }) ?? true,
2556
+ extensions: effectiveExtensions,
2557
+ contentError,
2558
+ isCollaborative
2559
+ };
2560
+ }
2561
+
2562
+ //#endregion
2563
+ //#region src/utils/set-text-alignment.ts
2564
+ function setTextAlignment(editor, alignment) {
2565
+ const { from, to } = editor.state.selection;
2566
+ const tr = editor.state.tr;
2567
+ editor.state.doc.nodesBetween(from, to, (node, pos) => {
2568
+ if (node.isTextblock) {
2569
+ const prop = "align" in node.attrs ? "align" : "alignment";
2570
+ tr.setNodeMarkup(pos, null, {
2571
+ ...node.attrs,
2572
+ [prop]: alignment
2573
+ });
2574
+ }
2575
+ });
2576
+ editor.view.dispatch(tr);
2577
+ }
2578
+
2579
+ //#endregion
2580
+ //#region src/ui/bubble-menu/context.tsx
2581
+ const BubbleMenuContext = React.createContext(null);
2582
+ function useBubbleMenuContext() {
2583
+ const context = React.useContext(BubbleMenuContext);
2584
+ if (!context) throw new Error("BubbleMenu compound components must be used within <BubbleMenu.Root>");
2585
+ return context;
2586
+ }
2587
+
2588
+ //#endregion
2589
+ //#region src/ui/bubble-menu/item.tsx
2590
+ function BubbleMenuItem({ name, isActive, onCommand, className, children, ...rest }) {
2591
+ return /* @__PURE__ */ jsx("button", {
2592
+ type: "button",
2593
+ "aria-label": name,
2594
+ "aria-pressed": isActive,
2595
+ className,
2596
+ "data-re-bubble-menu-item": "",
2597
+ "data-item": name,
2598
+ ...isActive ? { "data-active": "" } : {},
2599
+ onMouseDown: (e) => e.preventDefault(),
2600
+ onClick: onCommand,
2601
+ ...rest,
2602
+ children
2603
+ });
2604
+ }
2605
+
2606
+ //#endregion
2607
+ //#region src/ui/bubble-menu/align-center.tsx
2608
+ function BubbleMenuAlignCenter({ className, children }) {
2609
+ const { editor } = useBubbleMenuContext();
2610
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
2611
+ name: "align-center",
2612
+ isActive: useEditorState({
2613
+ editor,
2614
+ selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "center" }) ?? false
2615
+ }),
2616
+ onCommand: () => setTextAlignment(editor, "center"),
2617
+ className,
2618
+ children: children ?? /* @__PURE__ */ jsx(AlignCenterIcon, {})
2619
+ });
2620
+ }
2621
+
2622
+ //#endregion
2623
+ //#region src/ui/bubble-menu/align-left.tsx
2624
+ function BubbleMenuAlignLeft({ className, children }) {
2625
+ const { editor } = useBubbleMenuContext();
2626
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
2627
+ name: "align-left",
2628
+ isActive: useEditorState({
2629
+ editor,
2630
+ selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "left" }) ?? false
2631
+ }),
2632
+ onCommand: () => setTextAlignment(editor, "left"),
2633
+ className,
2634
+ children: children ?? /* @__PURE__ */ jsx(AlignLeftIcon, {})
2635
+ });
2636
+ }
2637
+
2638
+ //#endregion
2639
+ //#region src/ui/bubble-menu/align-right.tsx
2640
+ function BubbleMenuAlignRight({ className, children }) {
2641
+ const { editor } = useBubbleMenuContext();
2642
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
2643
+ name: "align-right",
2644
+ isActive: useEditorState({
2645
+ editor,
2646
+ selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "right" }) ?? false
2647
+ }),
2648
+ onCommand: () => setTextAlignment(editor, "right"),
2649
+ className,
2650
+ children: children ?? /* @__PURE__ */ jsx(AlignRightIcon, {})
2651
+ });
2652
+ }
2653
+
2654
+ //#endregion
2655
+ //#region src/ui/bubble-menu/create-mark-bubble-item.tsx
2656
+ function createMarkBubbleItem(config) {
2657
+ function MarkBubbleItem({ className, children }) {
2658
+ const { editor } = useBubbleMenuContext();
2659
+ const isActive = useEditorState({
2660
+ editor,
2661
+ selector: ({ editor: editor$1 }) => {
2662
+ if (config.activeParams) return editor$1?.isActive(config.activeName, config.activeParams) ?? false;
2663
+ return editor$1?.isActive(config.activeName) ?? false;
2664
+ }
2665
+ });
2666
+ const handleCommand = () => {
2667
+ const chain = editor.chain().focus();
2668
+ const method = chain[config.command];
2669
+ if (method) method.call(chain).run();
2670
+ };
2671
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
2672
+ name: config.name,
2673
+ isActive,
2674
+ onCommand: handleCommand,
2675
+ className,
2676
+ children: children ?? config.icon
2677
+ });
2678
+ }
2679
+ MarkBubbleItem.displayName = `BubbleMenu${config.name.charAt(0).toUpperCase() + config.name.slice(1)}`;
2680
+ return MarkBubbleItem;
2681
+ }
2682
+
2683
+ //#endregion
2684
+ //#region src/ui/bubble-menu/bold.tsx
2685
+ const BubbleMenuBold = createMarkBubbleItem({
2686
+ name: "bold",
2687
+ activeName: "bold",
2688
+ command: "toggleBold",
2689
+ icon: /* @__PURE__ */ jsx(BoldIcon, {})
2690
+ });
2691
+
2692
+ //#endregion
2693
+ //#region src/ui/bubble-menu/code.tsx
2694
+ const BubbleMenuCode = createMarkBubbleItem({
2695
+ name: "code",
2696
+ activeName: "code",
2697
+ command: "toggleCode",
2698
+ icon: /* @__PURE__ */ jsx(CodeIcon, {})
2699
+ });
2700
+
2701
+ //#endregion
2702
+ //#region src/ui/bubble-menu/group.tsx
2703
+ function BubbleMenuItemGroup({ className, children }) {
2704
+ return /* @__PURE__ */ jsx("fieldset", {
2705
+ className,
2706
+ "data-re-bubble-menu-group": "",
2707
+ children
2708
+ });
2709
+ }
2710
+
2711
+ //#endregion
2712
+ //#region src/ui/bubble-menu/italic.tsx
2713
+ const BubbleMenuItalic = createMarkBubbleItem({
2714
+ name: "italic",
2715
+ activeName: "italic",
2716
+ command: "toggleItalic",
2717
+ icon: /* @__PURE__ */ jsx(ItalicIcon, {})
2718
+ });
2719
+
2720
+ //#endregion
2721
+ //#region src/ui/bubble-menu/utils.ts
2722
+ const SAFE_PROTOCOLS = new Set([
2723
+ "http:",
2724
+ "https:",
2725
+ "mailto:",
2726
+ "tel:"
2727
+ ]);
2728
+ /**
2729
+ * Basic URL validation and auto-prefixing.
2730
+ * Rejects dangerous schemes (javascript:, data:, vbscript:, etc.).
2731
+ * Returns the valid URL string or null.
2732
+ */
2733
+ function getUrlFromString(str) {
2734
+ if (str === "#") return str;
2735
+ try {
2736
+ const url = new URL(str);
2737
+ if (SAFE_PROTOCOLS.has(url.protocol)) return str;
2738
+ return null;
2739
+ } catch {}
2740
+ try {
2741
+ if (str.includes(".") && !str.includes(" ")) return new URL(`https://${str}`).toString();
2742
+ } catch {}
2743
+ return null;
2744
+ }
2745
+ function setLinkHref(editor, href) {
2746
+ if (href.length === 0) {
2747
+ editor.chain().unsetLink().run();
2748
+ return;
2749
+ }
2750
+ const { from, to } = editor.state.selection;
2751
+ if (from === to) {
2752
+ editor.chain().extendMarkRange("link").setLink({ href }).setTextSelection({
2753
+ from,
2754
+ to
2755
+ }).run();
2756
+ return;
2757
+ }
2758
+ editor.chain().setLink({ href }).run();
2759
+ }
2760
+ function focusEditor(editor) {
2761
+ setTimeout(() => {
2762
+ editor.commands.focus();
2763
+ }, 0);
2764
+ }
2765
+
2766
+ //#endregion
2767
+ //#region src/ui/bubble-menu/link-selector.tsx
2768
+ function BubbleMenuLinkSelector({ className, showToggle = true, validateUrl, onLinkApply, onLinkRemove, children, open: controlledOpen, onOpenChange }) {
2769
+ const { editor } = useBubbleMenuContext();
2770
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
2771
+ const isControlled = controlledOpen !== void 0;
2772
+ const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
2773
+ const setIsOpen = React.useCallback((value) => {
2774
+ if (!isControlled) setUncontrolledOpen(value);
2775
+ onOpenChange?.(value);
2776
+ }, [isControlled, onOpenChange]);
2777
+ const editorState = useEditorState({
2778
+ editor,
2779
+ selector: ({ editor: editor$1 }) => ({
2780
+ isLinkActive: editor$1?.isActive("link") ?? false,
2781
+ hasLink: Boolean(editor$1?.getAttributes("link").href),
2782
+ currentHref: editor$1?.getAttributes("link").href || ""
2783
+ })
2784
+ });
2785
+ const setIsOpenRef = React.useRef(setIsOpen);
2786
+ setIsOpenRef.current = setIsOpen;
2787
+ React.useEffect(() => {
2788
+ const subscription = editorEventBus.on("bubble-menu:add-link", () => {
2789
+ setIsOpenRef.current(true);
2790
+ });
2791
+ return () => {
2792
+ setIsOpenRef.current(false);
2793
+ subscription.unsubscribe();
2794
+ };
2795
+ }, []);
2796
+ if (!editorState) return null;
2797
+ const handleOpenLink = () => {
2798
+ setIsOpen(!isOpen);
2799
+ };
2800
+ return /* @__PURE__ */ jsxs("div", {
2801
+ "data-re-link-selector": "",
2802
+ ...isOpen ? { "data-open": "" } : {},
2803
+ ...editorState.hasLink ? { "data-has-link": "" } : {},
2804
+ className,
2805
+ children: [showToggle && /* @__PURE__ */ jsx("button", {
2806
+ type: "button",
2807
+ "aria-expanded": isOpen,
2808
+ "aria-haspopup": "true",
2809
+ "aria-label": "Add link",
2810
+ "aria-pressed": editorState.isLinkActive && editorState.hasLink,
2811
+ "data-re-link-selector-trigger": "",
2812
+ onClick: handleOpenLink,
2813
+ children: /* @__PURE__ */ jsx(LinkIcon, {})
2814
+ }), isOpen && /* @__PURE__ */ jsx(LinkForm, {
2815
+ editor,
2816
+ currentHref: editorState.currentHref,
2817
+ validateUrl,
2818
+ onLinkApply,
2819
+ onLinkRemove,
2820
+ setIsOpen,
2821
+ children
2822
+ })]
2823
+ });
2824
+ }
2825
+ function LinkForm({ editor, currentHref, validateUrl, onLinkApply, onLinkRemove, setIsOpen, children }) {
2826
+ const inputRef = React.useRef(null);
2827
+ const formRef = React.useRef(null);
2828
+ const displayHref = currentHref === "#" ? "" : currentHref;
2829
+ const [inputValue, setInputValue] = React.useState(displayHref);
2830
+ React.useEffect(() => {
2831
+ const timeoutId = setTimeout(() => {
2832
+ inputRef.current?.focus();
2833
+ }, 0);
2834
+ return () => clearTimeout(timeoutId);
2835
+ }, []);
2836
+ React.useEffect(() => {
2837
+ const handleKeyDown = (event) => {
2838
+ if (event.key === "Escape") {
2839
+ if (editor.getAttributes("link").href === "#") editor.chain().unsetLink().run();
2840
+ setIsOpen(false);
2841
+ }
2842
+ };
2843
+ const handleClickOutside = (event) => {
2844
+ if (formRef.current && !formRef.current.contains(event.target)) {
2845
+ const form = formRef.current;
2846
+ const submitEvent = new Event("submit", {
2847
+ bubbles: true,
2848
+ cancelable: true
2849
+ });
2850
+ form.dispatchEvent(submitEvent);
2851
+ setIsOpen(false);
2852
+ }
2853
+ };
2854
+ document.addEventListener("mousedown", handleClickOutside);
2855
+ window.addEventListener("keydown", handleKeyDown);
2856
+ return () => {
2857
+ window.removeEventListener("keydown", handleKeyDown);
2858
+ document.removeEventListener("mousedown", handleClickOutside);
2859
+ };
2860
+ }, [editor, setIsOpen]);
2861
+ function handleSubmit(e) {
2862
+ e.preventDefault();
2863
+ const value = inputValue.trim();
2864
+ if (value === "") {
2865
+ setLinkHref(editor, "");
2866
+ setIsOpen(false);
2867
+ focusEditor(editor);
2868
+ onLinkRemove?.();
2869
+ return;
2870
+ }
2871
+ const finalValue = (validateUrl ?? getUrlFromString)(value);
2872
+ if (!finalValue) {
2873
+ setLinkHref(editor, "");
2874
+ setIsOpen(false);
2875
+ focusEditor(editor);
2876
+ onLinkRemove?.();
2877
+ return;
2878
+ }
2879
+ setLinkHref(editor, finalValue);
2880
+ setIsOpen(false);
2881
+ focusEditor(editor);
2882
+ onLinkApply?.(finalValue);
2883
+ }
2884
+ function handleUnlink(e) {
2885
+ e.stopPropagation();
2886
+ setLinkHref(editor, "");
2887
+ setIsOpen(false);
2888
+ focusEditor(editor);
2889
+ onLinkRemove?.();
2890
+ }
2891
+ return /* @__PURE__ */ jsxs("form", {
2892
+ ref: formRef,
2893
+ "data-re-link-selector-form": "",
2894
+ onMouseDown: (e) => e.stopPropagation(),
2895
+ onClick: (e) => e.stopPropagation(),
2896
+ onKeyDown: (e) => e.stopPropagation(),
2897
+ onSubmit: handleSubmit,
2898
+ children: [
2899
+ /* @__PURE__ */ jsx("input", {
2900
+ ref: inputRef,
2901
+ "data-re-link-selector-input": "",
2902
+ value: inputValue,
2903
+ onFocus: (e) => e.stopPropagation(),
2904
+ onChange: (e) => setInputValue(e.target.value),
2905
+ placeholder: "Paste a link",
2906
+ type: "text"
2907
+ }),
2908
+ children,
2909
+ displayHref ? /* @__PURE__ */ jsx("button", {
2910
+ type: "button",
2911
+ "aria-label": "Remove link",
2912
+ "data-re-link-selector-unlink": "",
2913
+ onClick: handleUnlink,
2914
+ children: /* @__PURE__ */ jsx(UnlinkIcon, {})
2915
+ }) : /* @__PURE__ */ jsx("button", {
2916
+ type: "submit",
2917
+ "aria-label": "Apply link",
2918
+ "data-re-link-selector-apply": "",
2919
+ onMouseDown: (e) => e.stopPropagation(),
2920
+ children: /* @__PURE__ */ jsx(Check, {})
2921
+ })
2922
+ ]
2923
+ });
2924
+ }
2925
+
2926
+ //#endregion
2927
+ //#region src/ui/bubble-menu/node-selector.tsx
2928
+ const NodeSelectorContext = React.createContext(null);
2929
+ function useNodeSelectorContext() {
2930
+ const context = React.useContext(NodeSelectorContext);
2931
+ if (!context) throw new Error("NodeSelector compound components must be used within <NodeSelector.Root>");
2932
+ return context;
2933
+ }
2934
+ function NodeSelectorRoot({ omit = [], open: controlledOpen, onOpenChange, className, children }) {
2935
+ const { editor } = useBubbleMenuContext();
2936
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
2937
+ const isControlled = controlledOpen !== void 0;
2938
+ const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
2939
+ const setIsOpen = React.useCallback((value) => {
2940
+ if (!isControlled) setUncontrolledOpen(value);
2941
+ onOpenChange?.(value);
2942
+ }, [isControlled, onOpenChange]);
2943
+ const editorState = useEditorState({
2944
+ editor,
2945
+ selector: ({ editor: editor$1 }) => ({
2946
+ isParagraphActive: (editor$1?.isActive("paragraph") ?? false) && !editor$1?.isActive("bulletList") && !editor$1?.isActive("orderedList"),
2947
+ isHeading1Active: editor$1?.isActive("heading", { level: 1 }) ?? false,
2948
+ isHeading2Active: editor$1?.isActive("heading", { level: 2 }) ?? false,
2949
+ isHeading3Active: editor$1?.isActive("heading", { level: 3 }) ?? false,
2950
+ isBulletListActive: editor$1?.isActive("bulletList") ?? false,
2951
+ isOrderedListActive: editor$1?.isActive("orderedList") ?? false,
2952
+ isBlockquoteActive: editor$1?.isActive("blockquote") ?? false,
2953
+ isCodeBlockActive: editor$1?.isActive("codeBlock") ?? false
2954
+ })
2955
+ });
2956
+ const allItems = React.useMemo(() => [
2957
+ {
2958
+ name: "Text",
2959
+ icon: TextIcon,
2960
+ command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").run(),
2961
+ isActive: editorState?.isParagraphActive ?? false
2962
+ },
2963
+ {
2964
+ name: "Title",
2965
+ icon: Heading1,
2966
+ command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(),
2967
+ isActive: editorState?.isHeading1Active ?? false
2968
+ },
2969
+ {
2970
+ name: "Subtitle",
2971
+ icon: Heading2,
2972
+ command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(),
2973
+ isActive: editorState?.isHeading2Active ?? false
2974
+ },
2975
+ {
2976
+ name: "Heading",
2977
+ icon: Heading3,
2978
+ command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(),
2979
+ isActive: editorState?.isHeading3Active ?? false
2980
+ },
2981
+ {
2982
+ name: "Bullet List",
2983
+ icon: List,
2984
+ command: () => editor.chain().focus().clearNodes().toggleBulletList().run(),
2985
+ isActive: editorState?.isBulletListActive ?? false
2986
+ },
2987
+ {
2988
+ name: "Numbered List",
2989
+ icon: ListOrdered,
2990
+ command: () => editor.chain().focus().clearNodes().toggleOrderedList().run(),
2991
+ isActive: editorState?.isOrderedListActive ?? false
2992
+ },
2993
+ {
2994
+ name: "Quote",
2995
+ icon: TextQuote,
2996
+ command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
2997
+ isActive: editorState?.isBlockquoteActive ?? false
2998
+ },
2999
+ {
3000
+ name: "Code",
3001
+ icon: Code$1,
3002
+ command: () => editor.chain().focus().clearNodes().toggleCodeBlock().run(),
3003
+ isActive: editorState?.isCodeBlockActive ?? false
3004
+ }
3005
+ ], [editor, editorState]);
3006
+ const items = React.useMemo(() => allItems.filter((item) => !omit.includes(item.name)), [allItems, omit]);
3007
+ const activeItem = React.useMemo(() => items.find((item) => item.isActive) ?? { name: "Multiple" }, [items]);
3008
+ const contextValue = React.useMemo(() => ({
3009
+ items,
3010
+ activeItem,
3011
+ isOpen,
3012
+ setIsOpen
3013
+ }), [
3014
+ items,
3015
+ activeItem,
3016
+ isOpen,
3017
+ setIsOpen
3018
+ ]);
3019
+ if (!editorState || items.length === 0) return null;
3020
+ return /* @__PURE__ */ jsx(NodeSelectorContext.Provider, {
3021
+ value: contextValue,
3022
+ children: /* @__PURE__ */ jsx(Popover.Root, {
3023
+ open: isOpen,
3024
+ onOpenChange: setIsOpen,
3025
+ children: /* @__PURE__ */ jsx("div", {
3026
+ "data-re-node-selector": "",
3027
+ ...isOpen ? { "data-open": "" } : {},
3028
+ className,
3029
+ children
3030
+ })
3031
+ })
3032
+ });
3033
+ }
3034
+ function NodeSelectorTrigger({ className, children }) {
3035
+ const { activeItem, isOpen, setIsOpen } = useNodeSelectorContext();
3036
+ return /* @__PURE__ */ jsx(Popover.Trigger, {
3037
+ "data-re-node-selector-trigger": "",
3038
+ className,
3039
+ onClick: () => setIsOpen(!isOpen),
3040
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children: activeItem.name }), /* @__PURE__ */ jsx(ChevronDown, {})] })
3041
+ });
3042
+ }
3043
+ function NodeSelectorContent({ className, align = "start", children }) {
3044
+ const { items, setIsOpen } = useNodeSelectorContext();
3045
+ return /* @__PURE__ */ jsx(Popover.Content, {
3046
+ align,
3047
+ "data-re-node-selector-content": "",
3048
+ className,
3049
+ children: children ? children(items, () => setIsOpen(false)) : items.map((item) => {
3050
+ const Icon = item.icon;
3051
+ return /* @__PURE__ */ jsxs("button", {
3052
+ type: "button",
3053
+ "data-re-node-selector-item": "",
3054
+ ...item.isActive ? { "data-active": "" } : {},
3055
+ onClick: () => {
3056
+ item.command();
3057
+ setIsOpen(false);
3058
+ },
3059
+ children: [
3060
+ /* @__PURE__ */ jsx(Icon, {}),
3061
+ /* @__PURE__ */ jsx("span", { children: item.name }),
3062
+ item.isActive && /* @__PURE__ */ jsx(Check, {})
3063
+ ]
3064
+ }, item.name);
3065
+ })
3066
+ });
3067
+ }
3068
+ function BubbleMenuNodeSelector({ omit = [], className, triggerContent, open, onOpenChange }) {
3069
+ return /* @__PURE__ */ jsxs(NodeSelectorRoot, {
3070
+ omit,
3071
+ open,
3072
+ onOpenChange,
3073
+ className,
3074
+ children: [/* @__PURE__ */ jsx(NodeSelectorTrigger, { children: triggerContent }), /* @__PURE__ */ jsx(NodeSelectorContent, {})]
3075
+ });
3076
+ }
3077
+
3078
+ //#endregion
3079
+ //#region src/ui/bubble-menu/root.tsx
3080
+ function BubbleMenuRoot({ excludeNodes = [], placement = "bottom", offset = 8, onHide, className, children }) {
3081
+ const { editor } = useCurrentEditor();
3082
+ if (!editor) return null;
3083
+ return /* @__PURE__ */ jsx(BubbleMenu$1, {
3084
+ editor,
3085
+ "data-re-bubble-menu": "",
3086
+ shouldShow: ({ editor: editor$1, view }) => {
3087
+ for (const node of excludeNodes) if (editor$1.isActive(node)) return false;
3088
+ if (view.dom.classList.contains("dragging")) return false;
3089
+ return editor$1.view.state.selection.content().size > 0;
3090
+ },
3091
+ options: {
3092
+ placement,
3093
+ offset,
3094
+ onHide
3095
+ },
3096
+ className,
3097
+ children: /* @__PURE__ */ jsx(BubbleMenuContext.Provider, {
3098
+ value: { editor },
3099
+ children
3100
+ })
3101
+ });
3102
+ }
3103
+
3104
+ //#endregion
3105
+ //#region src/ui/bubble-menu/strike.tsx
3106
+ const BubbleMenuStrike = createMarkBubbleItem({
3107
+ name: "strike",
3108
+ activeName: "strike",
3109
+ command: "toggleStrike",
3110
+ icon: /* @__PURE__ */ jsx(StrikethroughIcon, {})
3111
+ });
3112
+
3113
+ //#endregion
3114
+ //#region src/ui/bubble-menu/underline.tsx
3115
+ const BubbleMenuUnderline = createMarkBubbleItem({
3116
+ name: "underline",
3117
+ activeName: "underline",
3118
+ command: "toggleUnderline",
3119
+ icon: /* @__PURE__ */ jsx(UnderlineIcon, {})
3120
+ });
3121
+
3122
+ //#endregion
3123
+ //#region src/ui/bubble-menu/uppercase.tsx
3124
+ const BubbleMenuUppercase = createMarkBubbleItem({
3125
+ name: "uppercase",
3126
+ activeName: "uppercase",
3127
+ command: "toggleUppercase",
3128
+ icon: /* @__PURE__ */ jsx(CaseUpperIcon, {})
3129
+ });
3130
+
3131
+ //#endregion
3132
+ //#region src/ui/bubble-menu/default.tsx
3133
+ function BubbleMenuDefault({ excludeItems = [], excludeNodes, placement, offset, onHide, className }) {
3134
+ const [isNodeSelectorOpen, setIsNodeSelectorOpen] = React.useState(false);
3135
+ const [isLinkSelectorOpen, setIsLinkSelectorOpen] = React.useState(false);
3136
+ const has = (item) => !excludeItems.includes(item);
3137
+ const handleNodeSelectorOpenChange = React.useCallback((open) => {
3138
+ setIsNodeSelectorOpen(open);
3139
+ if (open) setIsLinkSelectorOpen(false);
3140
+ }, []);
3141
+ const handleLinkSelectorOpenChange = React.useCallback((open) => {
3142
+ setIsLinkSelectorOpen(open);
3143
+ if (open) setIsNodeSelectorOpen(false);
3144
+ }, []);
3145
+ const handleHide = React.useCallback(() => {
3146
+ setIsNodeSelectorOpen(false);
3147
+ setIsLinkSelectorOpen(false);
3148
+ onHide?.();
3149
+ }, [onHide]);
3150
+ const hasFormattingItems = has("bold") || has("italic") || has("underline") || has("strike") || has("code") || has("uppercase");
3151
+ const hasAlignmentItems = has("align-left") || has("align-center") || has("align-right");
3152
+ return /* @__PURE__ */ jsxs(BubbleMenuRoot, {
3153
+ excludeNodes,
3154
+ placement,
3155
+ offset,
3156
+ onHide: handleHide,
3157
+ className,
3158
+ children: [
3159
+ has("node-selector") && /* @__PURE__ */ jsx(BubbleMenuNodeSelector, {
3160
+ open: isNodeSelectorOpen,
3161
+ onOpenChange: handleNodeSelectorOpenChange
3162
+ }),
3163
+ has("link-selector") && /* @__PURE__ */ jsx(BubbleMenuLinkSelector, {
3164
+ open: isLinkSelectorOpen,
3165
+ onOpenChange: handleLinkSelectorOpenChange
3166
+ }),
3167
+ hasFormattingItems && /* @__PURE__ */ jsxs(BubbleMenuItemGroup, { children: [
3168
+ has("bold") && /* @__PURE__ */ jsx(BubbleMenuBold, {}),
3169
+ has("italic") && /* @__PURE__ */ jsx(BubbleMenuItalic, {}),
3170
+ has("underline") && /* @__PURE__ */ jsx(BubbleMenuUnderline, {}),
3171
+ has("strike") && /* @__PURE__ */ jsx(BubbleMenuStrike, {}),
3172
+ has("code") && /* @__PURE__ */ jsx(BubbleMenuCode, {}),
3173
+ has("uppercase") && /* @__PURE__ */ jsx(BubbleMenuUppercase, {})
3174
+ ] }),
3175
+ hasAlignmentItems && /* @__PURE__ */ jsxs(BubbleMenuItemGroup, { children: [
3176
+ has("align-left") && /* @__PURE__ */ jsx(BubbleMenuAlignLeft, {}),
3177
+ has("align-center") && /* @__PURE__ */ jsx(BubbleMenuAlignCenter, {}),
3178
+ has("align-right") && /* @__PURE__ */ jsx(BubbleMenuAlignRight, {})
3179
+ ] })
3180
+ ]
3181
+ });
3182
+ }
3183
+
3184
+ //#endregion
3185
+ //#region src/ui/bubble-menu/separator.tsx
3186
+ function BubbleMenuSeparator({ className }) {
3187
+ return /* @__PURE__ */ jsx("hr", {
3188
+ className,
3189
+ "data-re-bubble-menu-separator": ""
3190
+ });
3191
+ }
3192
+
3193
+ //#endregion
3194
+ //#region src/ui/bubble-menu/index.ts
3195
+ const BubbleMenu = {
3196
+ Root: BubbleMenuRoot,
3197
+ ItemGroup: BubbleMenuItemGroup,
3198
+ Separator: BubbleMenuSeparator,
3199
+ Item: BubbleMenuItem,
3200
+ Bold: BubbleMenuBold,
3201
+ Italic: BubbleMenuItalic,
3202
+ Underline: BubbleMenuUnderline,
3203
+ Strike: BubbleMenuStrike,
3204
+ Code: BubbleMenuCode,
3205
+ Uppercase: BubbleMenuUppercase,
3206
+ AlignLeft: BubbleMenuAlignLeft,
3207
+ AlignCenter: BubbleMenuAlignCenter,
3208
+ AlignRight: BubbleMenuAlignRight,
3209
+ NodeSelector: Object.assign(BubbleMenuNodeSelector, {
3210
+ Root: NodeSelectorRoot,
3211
+ Trigger: NodeSelectorTrigger,
3212
+ Content: NodeSelectorContent
3213
+ }),
3214
+ LinkSelector: BubbleMenuLinkSelector,
3215
+ Default: BubbleMenuDefault
3216
+ };
3217
+
3218
+ //#endregion
3219
+ //#region src/ui/button-bubble-menu/context.tsx
3220
+ const ButtonBubbleMenuContext = React.createContext(null);
3221
+ function useButtonBubbleMenuContext() {
3222
+ const context = React.useContext(ButtonBubbleMenuContext);
3223
+ if (!context) throw new Error("ButtonBubbleMenu compound components must be used within <ButtonBubbleMenu.Root>");
3224
+ return context;
3225
+ }
3226
+
3227
+ //#endregion
3228
+ //#region src/ui/button-bubble-menu/edit-link.tsx
3229
+ function ButtonBubbleMenuEditLink({ className, children, onClick, onMouseDown, ...rest }) {
3230
+ const { setIsEditing } = useButtonBubbleMenuContext();
3231
+ return /* @__PURE__ */ jsx("button", {
3232
+ ...rest,
3233
+ type: "button",
3234
+ "aria-label": "Edit link",
3235
+ "data-re-btn-bm-item": "",
3236
+ "data-item": "edit-link",
3237
+ className,
3238
+ onMouseDown: (e) => {
3239
+ e.preventDefault();
3240
+ onMouseDown?.(e);
3241
+ },
3242
+ onClick: (e) => {
3243
+ onClick?.(e);
3244
+ setIsEditing(true);
3245
+ },
3246
+ children: children ?? /* @__PURE__ */ jsx(LinkIcon, {})
3247
+ });
3248
+ }
3249
+
3250
+ //#endregion
3251
+ //#region src/ui/button-bubble-menu/root.tsx
3252
+ function ButtonBubbleMenuRoot({ onHide, placement = "top", offset = 8, className, children }) {
3253
+ const { editor } = useCurrentEditor();
3254
+ const [isEditing, setIsEditing] = React.useState(false);
3255
+ if (!editor) return null;
3256
+ return /* @__PURE__ */ jsx(BubbleMenu$1, {
3257
+ editor,
3258
+ "data-re-btn-bm": "",
3259
+ shouldShow: ({ editor: e, view }) => e.isActive("button") && !view.dom.classList.contains("dragging"),
3260
+ options: {
3261
+ placement,
3262
+ offset,
3263
+ onHide: () => {
3264
+ setIsEditing(false);
3265
+ onHide?.();
3266
+ }
3267
+ },
3268
+ className,
3269
+ children: /* @__PURE__ */ jsx(ButtonBubbleMenuContext.Provider, {
3270
+ value: {
3271
+ editor,
3272
+ isEditing,
3273
+ setIsEditing
3274
+ },
3275
+ children
3276
+ })
3277
+ });
3278
+ }
3279
+
3280
+ //#endregion
3281
+ //#region src/ui/button-bubble-menu/toolbar.tsx
3282
+ function ButtonBubbleMenuToolbar({ children, ...rest }) {
3283
+ const { isEditing } = useButtonBubbleMenuContext();
3284
+ if (isEditing) return null;
3285
+ return /* @__PURE__ */ jsx("div", {
3286
+ "data-re-btn-bm-toolbar": "",
3287
+ ...rest,
3288
+ children
3289
+ });
3290
+ }
3291
+
3292
+ //#endregion
3293
+ //#region src/ui/button-bubble-menu/default.tsx
3294
+ function ButtonBubbleMenuDefault({ excludeItems = [], placement, offset, onHide, className }) {
3295
+ return /* @__PURE__ */ jsx(ButtonBubbleMenuRoot, {
3296
+ placement,
3297
+ offset,
3298
+ onHide,
3299
+ className,
3300
+ children: !excludeItems.includes("edit-link") && /* @__PURE__ */ jsx(ButtonBubbleMenuToolbar, { children: /* @__PURE__ */ jsx(ButtonBubbleMenuEditLink, {}) })
3301
+ });
3302
+ }
3303
+
3304
+ //#endregion
3305
+ //#region src/ui/button-bubble-menu/index.ts
3306
+ const ButtonBubbleMenu = {
3307
+ Root: ButtonBubbleMenuRoot,
3308
+ Toolbar: ButtonBubbleMenuToolbar,
3309
+ EditLink: ButtonBubbleMenuEditLink,
3310
+ Default: ButtonBubbleMenuDefault
3311
+ };
3312
+
3313
+ //#endregion
3314
+ //#region src/ui/image-bubble-menu/context.tsx
3315
+ const ImageBubbleMenuContext = React.createContext(null);
3316
+ function useImageBubbleMenuContext() {
3317
+ const context = React.useContext(ImageBubbleMenuContext);
3318
+ if (!context) throw new Error("ImageBubbleMenu compound components must be used within <ImageBubbleMenu.Root>");
3319
+ return context;
3320
+ }
3321
+
3322
+ //#endregion
3323
+ //#region src/ui/image-bubble-menu/edit-link.tsx
3324
+ function ImageBubbleMenuEditLink({ className, children, onClick, onMouseDown, ...rest }) {
3325
+ const { setIsEditing } = useImageBubbleMenuContext();
3326
+ return /* @__PURE__ */ jsx("button", {
3327
+ ...rest,
3328
+ type: "button",
3329
+ "aria-label": "Edit link",
3330
+ "data-re-img-bm-item": "",
3331
+ "data-item": "edit-link",
3332
+ className,
3333
+ onMouseDown: (e) => {
3334
+ e.preventDefault();
3335
+ onMouseDown?.(e);
3336
+ },
3337
+ onClick: (e) => {
3338
+ onClick?.(e);
3339
+ setIsEditing(true);
3340
+ },
3341
+ children: children ?? /* @__PURE__ */ jsx(LinkIcon, {})
3342
+ });
3343
+ }
3344
+
3345
+ //#endregion
3346
+ //#region src/ui/image-bubble-menu/root.tsx
3347
+ function ImageBubbleMenuRoot({ onHide, placement = "top", offset = 8, className, children }) {
3348
+ const { editor } = useCurrentEditor();
3349
+ const [isEditing, setIsEditing] = React.useState(false);
3350
+ if (!editor) return null;
3351
+ return /* @__PURE__ */ jsx(BubbleMenu$1, {
3352
+ editor,
3353
+ "data-re-img-bm": "",
3354
+ shouldShow: ({ editor: e, view }) => e.isActive("image") && !view.dom.classList.contains("dragging"),
3355
+ options: {
3356
+ placement,
3357
+ offset,
3358
+ onHide: () => {
3359
+ setIsEditing(false);
3360
+ onHide?.();
3361
+ }
3362
+ },
3363
+ className,
3364
+ children: /* @__PURE__ */ jsx(ImageBubbleMenuContext.Provider, {
3365
+ value: {
3366
+ editor,
3367
+ isEditing,
3368
+ setIsEditing
3369
+ },
3370
+ children
3371
+ })
3372
+ });
3373
+ }
3374
+
3375
+ //#endregion
3376
+ //#region src/ui/image-bubble-menu/toolbar.tsx
3377
+ function ImageBubbleMenuToolbar({ children, ...rest }) {
3378
+ const { isEditing } = useImageBubbleMenuContext();
3379
+ if (isEditing) return null;
3380
+ return /* @__PURE__ */ jsx("div", {
3381
+ "data-re-img-bm-toolbar": "",
3382
+ ...rest,
3383
+ children
3384
+ });
3385
+ }
3386
+
3387
+ //#endregion
3388
+ //#region src/ui/image-bubble-menu/default.tsx
3389
+ function ImageBubbleMenuDefault({ excludeItems = [], placement, offset, onHide, className }) {
3390
+ return /* @__PURE__ */ jsx(ImageBubbleMenuRoot, {
3391
+ placement,
3392
+ offset,
3393
+ onHide,
3394
+ className,
3395
+ children: !excludeItems.includes("edit-link") && /* @__PURE__ */ jsx(ImageBubbleMenuToolbar, { children: /* @__PURE__ */ jsx(ImageBubbleMenuEditLink, {}) })
3396
+ });
3397
+ }
3398
+
3399
+ //#endregion
3400
+ //#region src/ui/image-bubble-menu/index.ts
3401
+ const ImageBubbleMenu = {
3402
+ Root: ImageBubbleMenuRoot,
3403
+ Toolbar: ImageBubbleMenuToolbar,
3404
+ EditLink: ImageBubbleMenuEditLink,
3405
+ Default: ImageBubbleMenuDefault
3406
+ };
3407
+
3408
+ //#endregion
3409
+ //#region src/ui/link-bubble-menu/context.tsx
3410
+ const LinkBubbleMenuContext = React.createContext(null);
3411
+ function useLinkBubbleMenuContext() {
3412
+ const context = React.useContext(LinkBubbleMenuContext);
3413
+ if (!context) throw new Error("LinkBubbleMenu compound components must be used within <LinkBubbleMenu.Root>");
3414
+ return context;
3415
+ }
3416
+
3417
+ //#endregion
3418
+ //#region src/ui/link-bubble-menu/edit-link.tsx
3419
+ function LinkBubbleMenuEditLink({ className, children, onClick, onMouseDown, ...rest }) {
3420
+ const { setIsEditing } = useLinkBubbleMenuContext();
3421
+ return /* @__PURE__ */ jsx("button", {
3422
+ type: "button",
3423
+ "aria-label": "Edit link",
3424
+ "data-re-link-bm-item": "",
3425
+ "data-item": "edit-link",
3426
+ className,
3427
+ onMouseDown: (e) => {
3428
+ e.preventDefault();
3429
+ onMouseDown?.(e);
3430
+ },
3431
+ onClick: (e) => {
3432
+ onClick?.(e);
3433
+ setIsEditing(true);
3434
+ },
3435
+ ...rest,
3436
+ children: children ?? /* @__PURE__ */ jsx(PencilIcon, {})
3437
+ });
3438
+ }
3439
+
3440
+ //#endregion
3441
+ //#region src/ui/link-bubble-menu/form.tsx
3442
+ function LinkBubbleMenuForm({ className, validateUrl, onLinkApply, onLinkRemove, children }) {
3443
+ const { editor, linkHref, isEditing, setIsEditing } = useLinkBubbleMenuContext();
3444
+ const inputRef = React.useRef(null);
3445
+ const formRef = React.useRef(null);
3446
+ const displayHref = linkHref === "#" ? "" : linkHref;
3447
+ const [inputValue, setInputValue] = React.useState(displayHref);
3448
+ React.useEffect(() => {
3449
+ if (!isEditing) return;
3450
+ const timeoutId = setTimeout(() => {
3451
+ inputRef.current?.focus();
3452
+ }, 0);
3453
+ return () => clearTimeout(timeoutId);
3454
+ }, [isEditing]);
3455
+ React.useEffect(() => {
3456
+ if (!isEditing) return;
3457
+ const handleKeyDown = (event) => {
3458
+ if (event.key === "Escape") setIsEditing(false);
3459
+ };
3460
+ const handleClickOutside = (event) => {
3461
+ if (formRef.current && !formRef.current.contains(event.target)) {
3462
+ const form = formRef.current;
3463
+ const submitEvent = new Event("submit", {
3464
+ bubbles: true,
3465
+ cancelable: true
3466
+ });
3467
+ form.dispatchEvent(submitEvent);
3468
+ setIsEditing(false);
3469
+ }
3470
+ };
3471
+ document.addEventListener("mousedown", handleClickOutside);
3472
+ window.addEventListener("keydown", handleKeyDown);
3473
+ return () => {
3474
+ window.removeEventListener("keydown", handleKeyDown);
3475
+ document.removeEventListener("mousedown", handleClickOutside);
3476
+ };
3477
+ }, [isEditing, setIsEditing]);
3478
+ if (!isEditing) return null;
3479
+ function handleSubmit(e) {
3480
+ e.preventDefault();
3481
+ const value = inputValue.trim();
3482
+ if (value === "") {
3483
+ setLinkHref(editor, "");
3484
+ setIsEditing(false);
3485
+ focusEditor(editor);
3486
+ onLinkRemove?.();
3487
+ return;
3488
+ }
3489
+ const finalValue = (validateUrl ?? getUrlFromString)(value);
3490
+ if (!finalValue) {
3491
+ setLinkHref(editor, "");
3492
+ setIsEditing(false);
3493
+ focusEditor(editor);
3494
+ onLinkRemove?.();
3495
+ return;
3496
+ }
3497
+ setLinkHref(editor, finalValue);
3498
+ setIsEditing(false);
3499
+ focusEditor(editor);
3500
+ onLinkApply?.(finalValue);
3501
+ }
3502
+ function handleUnlink(e) {
3503
+ e.stopPropagation();
3504
+ setLinkHref(editor, "");
3505
+ setIsEditing(false);
3506
+ focusEditor(editor);
3507
+ onLinkRemove?.();
3508
+ }
3509
+ return /* @__PURE__ */ jsxs("form", {
3510
+ ref: formRef,
3511
+ "data-re-link-bm-form": "",
3512
+ className,
3513
+ onMouseDown: (e) => e.stopPropagation(),
3514
+ onClick: (e) => e.stopPropagation(),
3515
+ onKeyDown: (e) => e.stopPropagation(),
3516
+ onSubmit: handleSubmit,
3517
+ children: [
3518
+ /* @__PURE__ */ jsx("input", {
3519
+ ref: inputRef,
3520
+ "data-re-link-bm-input": "",
3521
+ value: inputValue,
3522
+ onFocus: (e) => e.stopPropagation(),
3523
+ onChange: (e) => setInputValue(e.target.value),
3524
+ placeholder: "Paste a link",
3525
+ type: "text"
3526
+ }),
3527
+ children,
3528
+ displayHref ? /* @__PURE__ */ jsx("button", {
3529
+ type: "button",
3530
+ "aria-label": "Remove link",
3531
+ "data-re-link-bm-unlink": "",
3532
+ onClick: handleUnlink,
3533
+ children: /* @__PURE__ */ jsx(UnlinkIcon, {})
3534
+ }) : /* @__PURE__ */ jsx("button", {
3535
+ type: "submit",
3536
+ "aria-label": "Apply link",
3537
+ "data-re-link-bm-apply": "",
3538
+ onMouseDown: (e) => e.stopPropagation(),
3539
+ children: /* @__PURE__ */ jsx(Check, {})
3540
+ })
3541
+ ]
3542
+ });
3543
+ }
3544
+
3545
+ //#endregion
3546
+ //#region src/ui/link-bubble-menu/open-link.tsx
3547
+ function LinkBubbleMenuOpenLink({ className, children, ...rest }) {
3548
+ const { linkHref } = useLinkBubbleMenuContext();
3549
+ return /* @__PURE__ */ jsx("a", {
3550
+ ...rest,
3551
+ href: linkHref,
3552
+ target: "_blank",
3553
+ rel: "noopener noreferrer",
3554
+ "aria-label": "Open link",
3555
+ "data-re-link-bm-item": "",
3556
+ "data-item": "open-link",
3557
+ className,
3558
+ children: children ?? /* @__PURE__ */ jsx(ExternalLinkIcon, {})
3559
+ });
3560
+ }
3561
+
3562
+ //#endregion
3563
+ //#region src/ui/link-bubble-menu/root.tsx
3564
+ function LinkBubbleMenuRoot({ onHide, placement = "top", offset = 8, className, children }) {
3565
+ const { editor } = useCurrentEditor();
3566
+ const [isEditing, setIsEditing] = React.useState(false);
3567
+ const linkHref = useEditorState({
3568
+ editor,
3569
+ selector: ({ editor: e }) => e?.getAttributes("link").href ?? ""
3570
+ });
3571
+ if (!editor) return null;
3572
+ return /* @__PURE__ */ jsx(BubbleMenu$1, {
3573
+ editor,
3574
+ "data-re-link-bm": "",
3575
+ shouldShow: ({ editor: e }) => e.isActive("link") && e.view.state.selection.content().size === 0,
3576
+ options: {
3577
+ placement,
3578
+ offset,
3579
+ onHide: () => {
3580
+ setIsEditing(false);
3581
+ onHide?.();
3582
+ }
3583
+ },
3584
+ className,
3585
+ children: /* @__PURE__ */ jsx(LinkBubbleMenuContext.Provider, {
3586
+ value: {
3587
+ editor,
3588
+ linkHref: linkHref ?? "",
3589
+ isEditing,
3590
+ setIsEditing
3591
+ },
3592
+ children
3593
+ })
3594
+ });
3595
+ }
3596
+
3597
+ //#endregion
3598
+ //#region src/ui/link-bubble-menu/toolbar.tsx
3599
+ function LinkBubbleMenuToolbar({ children, ...rest }) {
3600
+ const { isEditing } = useLinkBubbleMenuContext();
3601
+ if (isEditing) return null;
3602
+ return /* @__PURE__ */ jsx("div", {
3603
+ "data-re-link-bm-toolbar": "",
3604
+ ...rest,
3605
+ children
3606
+ });
3607
+ }
3608
+
3609
+ //#endregion
3610
+ //#region src/ui/link-bubble-menu/unlink.tsx
3611
+ function LinkBubbleMenuUnlink({ className, children, onClick, onMouseDown, ...rest }) {
3612
+ const { editor } = useLinkBubbleMenuContext();
3613
+ return /* @__PURE__ */ jsx("button", {
3614
+ type: "button",
3615
+ "aria-label": "Remove link",
3616
+ "data-re-link-bm-item": "",
3617
+ "data-item": "unlink",
3618
+ className,
3619
+ onMouseDown: (e) => {
3620
+ e.preventDefault();
3621
+ onMouseDown?.(e);
3622
+ },
3623
+ onClick: (e) => {
3624
+ onClick?.(e);
3625
+ editor.chain().focus().unsetLink().run();
3626
+ },
3627
+ ...rest,
3628
+ children: children ?? /* @__PURE__ */ jsx(UnlinkIcon, {})
3629
+ });
3630
+ }
3631
+
3632
+ //#endregion
3633
+ //#region src/ui/link-bubble-menu/default.tsx
3634
+ function LinkBubbleMenuDefault({ excludeItems = [], placement, offset, onHide, className, validateUrl, onLinkApply, onLinkRemove }) {
3635
+ const has = (item) => !excludeItems.includes(item);
3636
+ return /* @__PURE__ */ jsxs(LinkBubbleMenuRoot, {
3637
+ placement,
3638
+ offset,
3639
+ onHide,
3640
+ className,
3641
+ children: [(has("edit-link") || has("open-link") || has("unlink")) && /* @__PURE__ */ jsxs(LinkBubbleMenuToolbar, { children: [
3642
+ has("edit-link") && /* @__PURE__ */ jsx(LinkBubbleMenuEditLink, {}),
3643
+ has("open-link") && /* @__PURE__ */ jsx(LinkBubbleMenuOpenLink, {}),
3644
+ has("unlink") && /* @__PURE__ */ jsx(LinkBubbleMenuUnlink, {})
3645
+ ] }), /* @__PURE__ */ jsx(LinkBubbleMenuForm, {
3646
+ validateUrl,
3647
+ onLinkApply,
3648
+ onLinkRemove
3649
+ })]
3650
+ });
3651
+ }
3652
+
3653
+ //#endregion
3654
+ //#region src/ui/link-bubble-menu/index.ts
3655
+ const LinkBubbleMenu = {
3656
+ Root: LinkBubbleMenuRoot,
3657
+ Toolbar: LinkBubbleMenuToolbar,
3658
+ Form: LinkBubbleMenuForm,
3659
+ EditLink: LinkBubbleMenuEditLink,
3660
+ Unlink: LinkBubbleMenuUnlink,
3661
+ OpenLink: LinkBubbleMenuOpenLink,
3662
+ Default: LinkBubbleMenuDefault
3663
+ };
3664
+
3665
+ //#endregion
3666
+ //#region src/ui/slash-command/utils.ts
3667
+ function isInsideNode(editor, type) {
3668
+ const { $from } = editor.state.selection;
3669
+ for (let d = $from.depth; d > 0; d--) if ($from.node(d).type.name === type) return true;
3670
+ return false;
3671
+ }
3672
+ function isAtMaxColumnsDepth(editor) {
3673
+ const { from } = editor.state.selection;
3674
+ return getColumnsDepth(editor.state.doc, from) >= MAX_COLUMNS_DEPTH;
3675
+ }
3676
+ function updateScrollView(container, item) {
3677
+ const containerRect = container.getBoundingClientRect();
3678
+ const itemRect = item.getBoundingClientRect();
3679
+ if (itemRect.top < containerRect.top) container.scrollTop -= containerRect.top - itemRect.top;
3680
+ else if (itemRect.bottom > containerRect.bottom) container.scrollTop += itemRect.bottom - containerRect.bottom;
3681
+ }
3682
+
3683
+ //#endregion
3684
+ //#region src/ui/slash-command/command-list.tsx
3685
+ const CATEGORY_ORDER = [
3686
+ "Text",
3687
+ "Media",
3688
+ "Layout",
3689
+ "Utility"
3690
+ ];
3691
+ function groupByCategory(items) {
3692
+ const seen = /* @__PURE__ */ new Map();
3693
+ for (const item of items) {
3694
+ const existing = seen.get(item.category);
3695
+ if (existing) existing.push(item);
3696
+ else seen.set(item.category, [item]);
3697
+ }
3698
+ const ordered = [];
3699
+ for (const cat of CATEGORY_ORDER) {
3700
+ const group = seen.get(cat);
3701
+ if (group) {
3702
+ ordered.push({
3703
+ category: cat,
3704
+ items: group
3705
+ });
3706
+ seen.delete(cat);
3707
+ }
3708
+ }
3709
+ for (const [category, group] of seen) ordered.push({
3710
+ category,
3711
+ items: group
3712
+ });
3713
+ return ordered;
3714
+ }
3715
+ function CommandItem({ item, selected, onSelect }) {
3716
+ return /* @__PURE__ */ jsxs("button", {
3717
+ "data-re-slash-command-item": "",
3718
+ "data-selected": selected || void 0,
3719
+ onClick: onSelect,
3720
+ type: "button",
3721
+ children: [item.icon, /* @__PURE__ */ jsx("span", { children: item.title })]
3722
+ });
3723
+ }
3724
+ function CommandList({ items, command, query, ref }) {
3725
+ const [selectedIndex, setSelectedIndex] = useState(0);
3726
+ const containerRef = useRef(null);
3727
+ useEffect(() => {
3728
+ setSelectedIndex(0);
3729
+ }, [items]);
3730
+ useLayoutEffect(() => {
3731
+ const container = containerRef.current;
3732
+ if (!container) return;
3733
+ const selected = container.querySelector("[data-selected]");
3734
+ if (selected) updateScrollView(container, selected);
3735
+ }, [selectedIndex]);
3736
+ const selectItem = useCallback((index) => {
3737
+ const item = items[index];
3738
+ if (item) command(item);
3739
+ }, [items, command]);
3740
+ useImperativeHandle(ref, () => ({ onKeyDown: ({ event }) => {
3741
+ if (items.length === 0) return false;
3742
+ if (event.key === "ArrowUp") {
3743
+ setSelectedIndex((i) => (i + items.length - 1) % items.length);
3744
+ return true;
3745
+ }
3746
+ if (event.key === "ArrowDown") {
3747
+ setSelectedIndex((i) => (i + 1) % items.length);
3748
+ return true;
3749
+ }
3750
+ if (event.key === "Enter") {
3751
+ selectItem(selectedIndex);
3752
+ return true;
3753
+ }
3754
+ return false;
3755
+ } }), [
3756
+ items.length,
3757
+ selectItem,
3758
+ selectedIndex
3759
+ ]);
3760
+ if (items.length === 0) return /* @__PURE__ */ jsx("div", {
3761
+ "data-re-slash-command": "",
3762
+ children: /* @__PURE__ */ jsx("div", {
3763
+ "data-re-slash-command-empty": "",
3764
+ children: "No results"
3765
+ })
3766
+ });
3767
+ if (query.trim().length > 0) return /* @__PURE__ */ jsx("div", {
3768
+ "data-re-slash-command": "",
3769
+ ref: containerRef,
3770
+ children: items.map((item, index) => /* @__PURE__ */ jsx(CommandItem, {
3771
+ item,
3772
+ onSelect: () => selectItem(index),
3773
+ selected: index === selectedIndex
3774
+ }, item.title))
3775
+ });
3776
+ const groups = groupByCategory(items);
3777
+ let flatIndex = 0;
3778
+ return /* @__PURE__ */ jsx("div", {
3779
+ "data-re-slash-command": "",
3780
+ ref: containerRef,
3781
+ children: groups.map((group) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
3782
+ "data-re-slash-command-category": "",
3783
+ children: group.category
3784
+ }), group.items.map((item) => {
3785
+ const currentIndex = flatIndex++;
3786
+ return /* @__PURE__ */ jsx(CommandItem, {
3787
+ item,
3788
+ onSelect: () => selectItem(currentIndex),
3789
+ selected: currentIndex === selectedIndex
3790
+ }, item.title);
3791
+ })] }, group.category))
3792
+ });
3793
+ }
3794
+
3795
+ //#endregion
3796
+ //#region src/ui/slash-command/commands.tsx
3797
+ const TEXT = {
3798
+ title: "Text",
3799
+ description: "Plain text block",
3800
+ icon: /* @__PURE__ */ jsx(Text, { size: 20 }),
3801
+ category: "Text",
3802
+ searchTerms: ["p", "paragraph"],
3803
+ command: ({ editor, range }) => {
3804
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
3805
+ }
3806
+ };
3807
+ const H1 = {
3808
+ title: "Title",
3809
+ description: "Large heading",
3810
+ icon: /* @__PURE__ */ jsx(Heading1, { size: 20 }),
3811
+ category: "Text",
3812
+ searchTerms: [
3813
+ "title",
3814
+ "big",
3815
+ "large",
3816
+ "h1"
3817
+ ],
3818
+ command: ({ editor, range }) => {
3819
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
3820
+ }
3821
+ };
3822
+ const H2 = {
3823
+ title: "Subtitle",
3824
+ description: "Medium heading",
3825
+ icon: /* @__PURE__ */ jsx(Heading2, { size: 20 }),
3826
+ category: "Text",
3827
+ searchTerms: [
3828
+ "subtitle",
3829
+ "medium",
3830
+ "h2"
3831
+ ],
3832
+ command: ({ editor, range }) => {
3833
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
3834
+ }
3835
+ };
3836
+ const H3 = {
3837
+ title: "Heading",
3838
+ description: "Small heading",
3839
+ icon: /* @__PURE__ */ jsx(Heading3, { size: 20 }),
3840
+ category: "Text",
3841
+ searchTerms: [
3842
+ "subtitle",
3843
+ "small",
3844
+ "h3"
3845
+ ],
3846
+ command: ({ editor, range }) => {
3847
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
3848
+ }
3849
+ };
3850
+ const BULLET_LIST = {
3851
+ title: "Bullet list",
3852
+ description: "Unordered list",
3853
+ icon: /* @__PURE__ */ jsx(List, { size: 20 }),
3854
+ category: "Text",
3855
+ searchTerms: ["unordered", "point"],
3856
+ command: ({ editor, range }) => {
3857
+ editor.chain().focus().deleteRange(range).toggleBulletList().run();
3858
+ }
3859
+ };
3860
+ const NUMBERED_LIST = {
3861
+ title: "Numbered list",
3862
+ description: "Ordered list",
3863
+ icon: /* @__PURE__ */ jsx(ListOrdered, { size: 20 }),
3864
+ category: "Text",
3865
+ searchTerms: ["ordered"],
3866
+ command: ({ editor, range }) => {
3867
+ editor.chain().focus().deleteRange(range).toggleOrderedList().run();
3868
+ }
3869
+ };
3870
+ const QUOTE = {
3871
+ title: "Quote",
3872
+ description: "Block quote",
3873
+ icon: /* @__PURE__ */ jsx(TextQuote, { size: 20 }),
3874
+ category: "Text",
3875
+ searchTerms: ["blockquote"],
3876
+ command: ({ editor, range }) => {
3877
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
3878
+ }
3879
+ };
3880
+ const CODE = {
3881
+ title: "Code block",
3882
+ description: "Code snippet",
3883
+ icon: /* @__PURE__ */ jsx(SquareCode, { size: 20 }),
3884
+ category: "Text",
3885
+ searchTerms: ["codeblock"],
3886
+ command: ({ editor, range }) => {
3887
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
3888
+ }
3889
+ };
3890
+ const BUTTON = {
3891
+ title: "Button",
3892
+ description: "Clickable button",
3893
+ icon: /* @__PURE__ */ jsx(MousePointer, { size: 20 }),
3894
+ category: "Layout",
3895
+ searchTerms: ["button"],
3896
+ command: ({ editor, range }) => {
3897
+ editor.chain().focus().deleteRange(range).setButton().run();
3898
+ }
3899
+ };
3900
+ const DIVIDER = {
3901
+ title: "Divider",
3902
+ description: "Horizontal separator",
3903
+ icon: /* @__PURE__ */ jsx(SplitSquareVertical, { size: 20 }),
3904
+ category: "Layout",
3905
+ searchTerms: [
3906
+ "hr",
3907
+ "divider",
3908
+ "separator"
3909
+ ],
3910
+ command: ({ editor, range }) => {
3911
+ editor.chain().focus().deleteRange(range).setHorizontalRule().run();
3912
+ }
3913
+ };
3914
+ const SECTION = {
3915
+ title: "Section",
3916
+ description: "Content section",
3917
+ icon: /* @__PURE__ */ jsx(Rows2, { size: 20 }),
3918
+ category: "Layout",
3919
+ searchTerms: [
3920
+ "section",
3921
+ "row",
3922
+ "container"
3923
+ ],
3924
+ command: ({ editor, range }) => {
3925
+ editor.chain().focus().deleteRange(range).insertSection().run();
3926
+ }
3927
+ };
3928
+ const TWO_COLUMNS = {
3929
+ title: "2 columns",
3930
+ description: "Two column layout",
3931
+ icon: /* @__PURE__ */ jsx(Columns2, { size: 20 }),
3932
+ category: "Layout",
3933
+ searchTerms: [
3934
+ "columns",
3935
+ "column",
3936
+ "layout",
3937
+ "grid",
3938
+ "split",
3939
+ "side-by-side",
3940
+ "multi-column",
3941
+ "row",
3942
+ "two",
3943
+ "2"
3944
+ ],
3945
+ command: ({ editor, range }) => {
3946
+ editor.chain().focus().deleteRange(range).insertColumns(2).run();
3947
+ }
3948
+ };
3949
+ const THREE_COLUMNS = {
3950
+ title: "3 columns",
3951
+ description: "Three column layout",
3952
+ icon: /* @__PURE__ */ jsx(Columns3, { size: 20 }),
3953
+ category: "Layout",
3954
+ searchTerms: [
3955
+ "columns",
3956
+ "column",
3957
+ "layout",
3958
+ "grid",
3959
+ "split",
3960
+ "multi-column",
3961
+ "row",
3962
+ "three",
3963
+ "3"
3964
+ ],
3965
+ command: ({ editor, range }) => {
3966
+ editor.chain().focus().deleteRange(range).insertColumns(3).run();
3967
+ }
3968
+ };
3969
+ const FOUR_COLUMNS = {
3970
+ title: "4 columns",
3971
+ description: "Four column layout",
3972
+ icon: /* @__PURE__ */ jsx(Columns4, { size: 20 }),
3973
+ category: "Layout",
3974
+ searchTerms: [
3975
+ "columns",
3976
+ "column",
3977
+ "layout",
3978
+ "grid",
3979
+ "split",
3980
+ "multi-column",
3981
+ "row",
3982
+ "four",
3983
+ "4"
3984
+ ],
3985
+ command: ({ editor, range }) => {
3986
+ editor.chain().focus().deleteRange(range).insertColumns(4).run();
3987
+ }
3988
+ };
3989
+ const defaultSlashCommands = [
3990
+ TEXT,
3991
+ H1,
3992
+ H2,
3993
+ H3,
3994
+ BULLET_LIST,
3995
+ NUMBERED_LIST,
3996
+ QUOTE,
3997
+ CODE,
3998
+ BUTTON,
3999
+ DIVIDER,
4000
+ SECTION,
4001
+ TWO_COLUMNS,
4002
+ THREE_COLUMNS,
4003
+ FOUR_COLUMNS
4004
+ ];
4005
+
4006
+ //#endregion
4007
+ //#region src/ui/slash-command/extension.ts
4008
+ const SlashCommandExtension = Extension.create({
4009
+ name: "slash-command",
4010
+ addOptions() {
4011
+ return { suggestion: {
4012
+ char: "/",
4013
+ allow: ({ editor }) => !editor.isActive("codeBlock"),
4014
+ command: ({ editor, range, props }) => {
4015
+ props.command({
4016
+ editor,
4017
+ range
4018
+ });
4019
+ }
4020
+ } };
4021
+ },
4022
+ addProseMirrorPlugins() {
4023
+ return [Suggestion({
4024
+ pluginKey: new PluginKey("slash-command"),
4025
+ editor: this.editor,
4026
+ ...this.options.suggestion
4027
+ })];
4028
+ }
4029
+ });
4030
+
4031
+ //#endregion
4032
+ //#region src/ui/slash-command/render.tsx
4033
+ function createRenderItems(component = CommandList) {
4034
+ return () => {
4035
+ let renderer = null;
4036
+ let popup = null;
4037
+ return {
4038
+ onStart: (props) => {
4039
+ renderer = new ReactRenderer(component, {
4040
+ props,
4041
+ editor: props.editor
4042
+ });
4043
+ if (!props.clientRect) return;
4044
+ popup = tippy("body", {
4045
+ getReferenceClientRect: props.clientRect,
4046
+ appendTo: () => document.body,
4047
+ content: renderer.element,
4048
+ showOnCreate: true,
4049
+ interactive: true,
4050
+ trigger: "manual",
4051
+ placement: "bottom-start"
4052
+ });
4053
+ },
4054
+ onUpdate: (props) => {
4055
+ if (!renderer) return;
4056
+ renderer.updateProps(props);
4057
+ if (popup?.[0] && props.clientRect) popup[0].setProps({ getReferenceClientRect: props.clientRect });
4058
+ },
4059
+ onKeyDown: (props) => {
4060
+ if (props.event.key === "Escape") {
4061
+ popup?.[0]?.hide();
4062
+ return true;
4063
+ }
4064
+ return renderer?.ref?.onKeyDown(props) ?? false;
4065
+ },
4066
+ onExit: () => {
4067
+ popup?.[0]?.destroy();
4068
+ renderer?.destroy();
4069
+ popup = null;
4070
+ renderer = null;
4071
+ }
4072
+ };
4073
+ };
4074
+ }
4075
+
4076
+ //#endregion
4077
+ //#region src/ui/slash-command/search.ts
4078
+ function scoreItem(item, query) {
4079
+ if (!query) return 100;
4080
+ const q = query.toLowerCase();
4081
+ const title = item.title.toLowerCase();
4082
+ const description = item.description.toLowerCase();
4083
+ const terms = item.searchTerms?.map((t) => t.toLowerCase()) ?? [];
4084
+ if (title === q) return 100;
4085
+ if (title.startsWith(q)) return 90;
4086
+ if (title.split(/\s+/).some((w) => w.startsWith(q))) return 80;
4087
+ if (terms.some((t) => t === q)) return 70;
4088
+ if (terms.some((t) => t.startsWith(q))) return 60;
4089
+ if (title.includes(q)) return 40;
4090
+ if (terms.some((t) => t.includes(q))) return 30;
4091
+ if (description.includes(q)) return 20;
4092
+ return 0;
4093
+ }
4094
+ function filterAndRankItems(items, query) {
4095
+ const trimmed = query.trim();
4096
+ if (!trimmed) return items;
4097
+ const scored = items.map((item) => ({
4098
+ item,
4099
+ score: scoreItem(item, trimmed)
4100
+ })).filter(({ score }) => score > 0);
4101
+ scored.sort((a, b) => b.score - a.score);
4102
+ return scored.map(({ item }) => item);
4103
+ }
4104
+
4105
+ //#endregion
4106
+ //#region src/ui/slash-command/create-slash-command.ts
4107
+ function defaultFilterItems(items, query, editor) {
4108
+ return filterAndRankItems(isAtMaxColumnsDepth(editor) ? items.filter((item) => item.category !== "Layout" || !item.title.includes("column")) : items, query);
4109
+ }
4110
+ function createSlashCommand(options) {
4111
+ const items = options?.items ?? defaultSlashCommands;
4112
+ const filterFn = options?.filterItems ?? defaultFilterItems;
4113
+ return SlashCommandExtension.configure({ suggestion: {
4114
+ items: ({ query, editor }) => filterFn(items, query, editor),
4115
+ render: createRenderItems(options?.component)
4116
+ } });
4117
+ }
4118
+
4119
+ //#endregion
4120
+ //#region src/ui/slash-command/index.ts
4121
+ const SlashCommand = createSlashCommand();
4122
+
4123
+ //#endregion
4124
+ export { AlignmentAttribute, BULLET_LIST, BUTTON, Blockquote, Body, Bold, BubbleMenu, BubbleMenuAlignCenter, BubbleMenuAlignLeft, BubbleMenuAlignRight, BubbleMenuBold, BubbleMenuCode, BubbleMenuDefault, BubbleMenuItalic, BubbleMenuItem, BubbleMenuItemGroup, BubbleMenuLinkSelector, BubbleMenuNodeSelector, BubbleMenuRoot, BubbleMenuSeparator, BubbleMenuStrike, BubbleMenuUnderline, BubbleMenuUppercase, BulletList, Button, ButtonBubbleMenu, ButtonBubbleMenuDefault, ButtonBubbleMenuEditLink, ButtonBubbleMenuRoot, ButtonBubbleMenuToolbar, CODE, COLUMN_PARENT_TYPES, ClassAttribute, Code, CodeBlockPrism, ColumnsColumn, CommandList, DIVIDER, Div, Divider, EmailNode, FOUR_COLUMNS, FourColumns, GlobalContent, H1, H2, H3, HardBreak, Heading, ImageBubbleMenu, ImageBubbleMenuDefault, ImageBubbleMenuEditLink, ImageBubbleMenuRoot, ImageBubbleMenuToolbar, Italic, Link, LinkBubbleMenu, LinkBubbleMenuDefault, LinkBubbleMenuEditLink, LinkBubbleMenuForm, LinkBubbleMenuOpenLink, LinkBubbleMenuRoot, LinkBubbleMenuToolbar, LinkBubbleMenuUnlink, ListItem, MAX_COLUMNS_DEPTH, MaxNesting, NUMBERED_LIST, NodeSelectorContent, NodeSelectorRoot, NodeSelectorTrigger, OrderedList, Paragraph, Placeholder, PreservedStyle, PreviewText, QUOTE, SECTION, Section, SlashCommand, StarterKit, Strike, StyleAttribute, Sup, TEXT, THREE_COLUMNS, TWO_COLUMNS, Table, TableCell, TableHeader, TableRow, ThreeColumns, TwoColumns, Uppercase, composeReactEmail, createSlashCommand, defaultSlashCommands, editorEventBus, filterAndRankItems, getColumnsDepth, getGlobalContent, isAtMaxColumnsDepth, isDocumentVisuallyEmpty, isInsideNode, processStylesForUnlink, scoreItem, setTextAlignment, useButtonBubbleMenuContext, useEditor, useImageBubbleMenuContext, useLinkBubbleMenuContext };
1350
4125
  //# sourceMappingURL=index.mjs.map