@alpaca-editor/core 1.0.4053 → 1.0.4054

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.
Files changed (37) hide show
  1. package/dist/editor/ai/AgentTerminal.js +1 -0
  2. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  3. package/dist/editor/ai/Agents.js +0 -2
  4. package/dist/editor/ai/Agents.js.map +1 -1
  5. package/dist/editor/ai/AiResponseMessage.js +1 -1
  6. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  7. package/dist/editor/ai/AiTerminal.d.ts +1 -0
  8. package/dist/editor/ai/AiTerminal.js.map +1 -1
  9. package/dist/editor/field-types/richtext/components/ReactSlate.js +141 -101
  10. package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
  11. package/dist/editor/field-types/richtext/config/pluginFactory.d.ts +1 -0
  12. package/dist/editor/field-types/richtext/config/pluginFactory.js +3 -1
  13. package/dist/editor/field-types/richtext/config/pluginFactory.js.map +1 -1
  14. package/dist/editor/field-types/richtext/types.d.ts +1 -0
  15. package/dist/editor/field-types/richtext/types.js.map +1 -1
  16. package/dist/editor/field-types/richtext/utils/conversion.js +27 -9
  17. package/dist/editor/field-types/richtext/utils/conversion.js.map +1 -1
  18. package/dist/editor/field-types/richtext/utils/plugins.d.ts +46 -1
  19. package/dist/editor/field-types/richtext/utils/plugins.js +67 -8
  20. package/dist/editor/field-types/richtext/utils/plugins.js.map +1 -1
  21. package/dist/editor/services/agentService.js +2 -0
  22. package/dist/editor/services/agentService.js.map +1 -1
  23. package/dist/revision.d.ts +2 -2
  24. package/dist/revision.js +2 -2
  25. package/package.json +2 -2
  26. package/src/editor/ai/AgentTerminal.tsx +1 -0
  27. package/src/editor/ai/Agents.tsx +0 -3
  28. package/src/editor/ai/AiResponseMessage.tsx +2 -2
  29. package/src/editor/ai/AiTerminal.tsx +1 -0
  30. package/src/editor/field-types/richtext/components/ReactSlate.css +41 -55
  31. package/src/editor/field-types/richtext/components/ReactSlate.tsx +237 -155
  32. package/src/editor/field-types/richtext/config/pluginFactory.tsx +3 -1
  33. package/src/editor/field-types/richtext/types.ts +3 -0
  34. package/src/editor/field-types/richtext/utils/conversion.ts +25 -9
  35. package/src/editor/field-types/richtext/utils/plugins.ts +86 -9
  36. package/src/editor/services/agentService.ts +2 -0
  37. package/src/revision.ts +2 -2
@@ -8,7 +8,7 @@ import React, {
8
8
  memo,
9
9
  } from "react";
10
10
  import { createEditor, Descendant, Editor, Element, Transforms } from "slate";
11
- import { Slate, Editable, withReact, ReactEditor } from "slate-react";
11
+ import { Slate, Editable, withReact, ReactEditor, useSlateSelector } from "slate-react";
12
12
  import { withHistory } from "slate-history";
13
13
  import "./ReactSlate.css";
14
14
 
@@ -38,38 +38,32 @@ import { createKeyboardHandler } from "../utils/plugins";
38
38
  import { useCachedSimplifiedProfile } from "../hooks/useProfileCache";
39
39
  import { classNames } from "primereact/utils";
40
40
 
41
- // Toolbar button component - not memoized to allow reactivity to editor state changes
41
+ // Memoized toolbar button component using useSlateSelector for performance
42
42
  const ToolbarButtonWrapper: React.FC<{
43
43
  option: ToolbarOptionConfig;
44
44
  icon: React.ReactNode;
45
- editor: Editor;
46
45
  onMouseDown: (event: React.MouseEvent<HTMLButtonElement>) => void;
47
- }> = ({ option, icon, editor, onMouseDown }) => {
48
- // Calculate active state on every render - this is lightweight and ensures reactivity
49
- let isActive = false;
50
- switch (option.type) {
51
- case "mark":
52
- isActive = editor.isMarkActive(option.id);
53
- break;
54
- case "block":
55
- isActive = editor.isBlockActive(option.id);
56
- break;
57
- case "alignment":
58
- isActive = editor.isAlignActive(SLATE_ALIGNMENTS[option.id].value);
59
- break;
60
- case "link":
61
- isActive = editor.isLinkActive();
62
- break;
63
- case "list":
64
- const listType = option.id === "unordered-list" ? "unordered" : "ordered";
65
- isActive = editor.isListActive(listType);
66
- break;
67
- case "insertion":
68
- isActive = false; // Insertion buttons are never active
69
- break;
70
- default:
71
- isActive = false;
72
- }
46
+ }> = memo(({ option, icon, onMouseDown }) => {
47
+ // Use useSlateSelector to only re-render when the active state changes
48
+ const isActive = useSlateSelector((editor) => {
49
+ switch (option.type) {
50
+ case "mark":
51
+ return editor.isMarkActive(option.id);
52
+ case "block":
53
+ return editor.isBlockActive(option.id);
54
+ case "alignment":
55
+ return editor.isAlignActive(SLATE_ALIGNMENTS[option.id].value);
56
+ case "link":
57
+ return editor.isLinkActive();
58
+ case "list":
59
+ const listType = option.id === "unordered-list" ? "unordered" : "ordered";
60
+ return editor.isListActive(listType);
61
+ case "insertion":
62
+ return false; // Insertion buttons are never active
63
+ default:
64
+ return false;
65
+ }
66
+ });
73
67
 
74
68
  return (
75
69
  <ToolbarButton
@@ -78,7 +72,9 @@ const ToolbarButtonWrapper: React.FC<{
78
72
  onMouseDown={onMouseDown}
79
73
  />
80
74
  );
81
- };
75
+ });
76
+
77
+ ToolbarButtonWrapper.displayName = "ToolbarButtonWrapper";
82
78
 
83
79
  // Memoized dropdown component to prevent unnecessary re-renders
84
80
  const MemoizedEditorDropdown = memo<{
@@ -239,6 +235,11 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
239
235
  });
240
236
  }, [editor]);
241
237
 
238
+ const handleStripFormattingClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
239
+ event.preventDefault();
240
+ editor.stripFormatting();
241
+ }, [editor]);
242
+
242
243
  // Memoize option handlers to prevent creating new functions on every render
243
244
  const optionHandlers = useMemo(() => {
244
245
  const handlers: Record<string, (editor: Editor, event: React.MouseEvent) => void> = {};
@@ -468,7 +469,6 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
468
469
  key={`${option.type}-${option.id}`}
469
470
  option={option}
470
471
  icon={optionObj.icon}
471
- editor={editor}
472
472
  onMouseDown={handler}
473
473
  />
474
474
  );
@@ -664,7 +664,185 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
664
664
  );
665
665
 
666
666
  // Use the keyboard handler from plugins
667
- const handleKeyDown = useMemo(() => createKeyboardHandler(editor), [editor]);
667
+ const handleKeyDown = useMemo(() => createKeyboardHandler(editor, simplifiedProfile), [editor, simplifiedProfile]);
668
+
669
+ // Calculate proper list numbering for ordered lists
670
+ const calculateListNumbers = useCallback((elements: Descendant[]): Map<string, string> => {
671
+ const numberMap = new Map<string, string>();
672
+ const counters: number[] = [0, 0, 0, 0, 0, 0]; // Support up to 6 levels
673
+
674
+ elements.forEach((element, index) => {
675
+ if (Element.isElement(element) &&
676
+ element.type === 'list-item' &&
677
+ (element as any).listType === 'ordered') {
678
+ const indent = ((element as any).indent || 0);
679
+
680
+ // Ensure indent is within bounds
681
+ if (indent < 0 || indent >= counters.length) return;
682
+
683
+ // Increment counter for current level
684
+ counters[indent] = (counters[indent] || 0) + 1;
685
+
686
+ // Reset all deeper level counters
687
+ for (let i = indent + 1; i < counters.length; i++) {
688
+ counters[i] = 0;
689
+ }
690
+
691
+ // Build number string for current level only (e.g., "3.")
692
+ const numberString = counters[indent] + '.';
693
+ numberMap.set(`${index}`, numberString);
694
+ }
695
+ });
696
+
697
+ return numberMap;
698
+ }, []);
699
+
700
+ // Calculate list numbers for the entire editor content (memoized for performance)
701
+ const listNumbers = useMemo(() => {
702
+ return calculateListNumbers(editor.children);
703
+ }, [editor.children, calculateListNumbers]);
704
+
705
+ // Memoize renderElement to prevent unnecessary re-renders as per Slate performance docs
706
+ const renderElement = useCallback(({ attributes, children, element }: any) => {
707
+ const style: React.CSSProperties = {
708
+ textAlign: element.align || "left",
709
+ };
710
+
711
+ if (element.type === "link") {
712
+ const isInternal = element.link?.type === "internal";
713
+ const url = isInternal
714
+ ? "#"
715
+ : element.url || element.link?.url || "#";
716
+
717
+ return (
718
+ <a
719
+ {...attributes}
720
+ href={url}
721
+ style={style}
722
+ className={`slate-link ${isInternal ? "internal-link" : "external-link"}`}
723
+ onClick={(e) => {
724
+ if (!readOnly) {
725
+ e.preventDefault();
726
+ editLink(element);
727
+ }
728
+ }}
729
+ >
730
+ {children}
731
+ </a>
732
+ );
733
+ }
734
+
735
+ if (element.type === "list-item") {
736
+ const indent = element.indent || 0;
737
+ const listType = element.listType || "unordered";
738
+
739
+ const listStyle: React.CSSProperties = {
740
+ ...style,
741
+ position: "relative",
742
+ listStyleType: "none",
743
+ };
744
+
745
+ // Find the element's index in the editor children to get the correct number
746
+ const elementIndex = editor.children.findIndex((child: any) => child === element);
747
+ const listNumber = listNumbers.get(`${elementIndex}`) || '';
748
+
749
+ return (
750
+ <div
751
+ {...attributes}
752
+ style={listStyle}
753
+ className={`slate-list-item slate-list-${listType}`}
754
+ data-indent={indent}
755
+ data-list-number={listNumber}
756
+ >
757
+ <span className="slate-list-bullet"></span>
758
+ <div className="slate-list-content">{children}</div>
759
+ </div>
760
+ );
761
+ }
762
+
763
+ if (element.type === "horizontal-rule") {
764
+ return (
765
+ <div {...attributes} contentEditable={false} style={{ ...style, userSelect: "none" }}>
766
+ {children}
767
+ <hr style={{
768
+ border: "none",
769
+ borderTop: "1px solid #ccc",
770
+ margin: "1em 0",
771
+ width: "100%"
772
+ }} />
773
+ </div>
774
+ );
775
+ }
776
+
777
+ if (element.type === "line-break") {
778
+ return (
779
+ <span {...attributes} contentEditable={false}>
780
+ {children}
781
+ <br />
782
+ </span>
783
+ );
784
+ }
785
+
786
+ // Handle different block types using built-in SLATE_BLOCKS configuration
787
+ const isValidBlockId = element.type in SLATE_BLOCKS;
788
+ const blockConfig = isValidBlockId ? SLATE_BLOCKS[element.type as BlockId] : undefined;
789
+ if (blockConfig && element.type === "no-tag") {
790
+ // Special handling for no-tag blocks (plain text without wrapper)
791
+ return (
792
+ <div {...attributes} style={style}>
793
+ {children}
794
+ </div>
795
+ );
796
+ }
797
+
798
+ // For standard blocks, use the appropriate HTML tag
799
+ if (blockConfig) {
800
+ const tagName = blockConfig.htmlTag;
801
+ return React.createElement(
802
+ tagName,
803
+ { ...attributes, style },
804
+ children,
805
+ );
806
+ }
807
+ // Default fallback to paragraph
808
+ return (
809
+ <p {...attributes} style={style}>
810
+ {children}
811
+ </p>
812
+ );
813
+ }, [readOnly, editLink, editor.children, listNumbers]);
814
+
815
+ // Memoize renderLeaf to prevent unnecessary re-renders as per Slate performance docs
816
+ const renderLeaf = useCallback(({ attributes, children, leaf }: any) => {
817
+
818
+ let el = <span {...attributes}>{children}</span>;
819
+
820
+ // Apply marks using the built-in SLATE_MARKS configuration
821
+ simplifiedProfile.marks.forEach((markId) => {
822
+ if ((leaf as any)[markId]) {
823
+ const markConfig = SLATE_MARKS[markId];
824
+ if (markConfig) {
825
+ if (markId === "extrabold") {
826
+ // Special handling for extrabold with CSS class
827
+ el = <span className="extrabold">{el}</span>;
828
+ } else {
829
+ // Standard HTML tag rendering
830
+ const tagName = markConfig.htmlTag;
831
+ el = React.createElement(tagName, {}, el);
832
+ }
833
+ }
834
+ }
835
+ });
836
+
837
+ return el;
838
+ }, [simplifiedProfile.marks]);
839
+
840
+ // Memoize renderPlaceholder for consistency
841
+ const renderPlaceholder = useCallback(({ attributes, children }: any) => (
842
+ <span {...attributes} className="p-2 text-gray-500">
843
+ {children}
844
+ </span>
845
+ ), []);
668
846
 
669
847
  return (
670
848
  <div className={`slate-editor ${props.className}`}>
@@ -675,147 +853,51 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
675
853
  >
676
854
  {!readOnly && (
677
855
  <div className="toolbar">
678
- {toolbarStructure.map(([rowIndex, rowGroups]) => (
856
+ {toolbarStructure.map(([rowIndex, rowGroups], mapIndex) => (
679
857
  <div key={`row-${rowIndex}`} className="toolbar-row">
680
858
  {rowGroups.map((group) =>
681
859
  renderToolbarGroup(group, parseInt(rowIndex)),
682
860
  )}
861
+ {/* Add strip formatting button to the last row */}
862
+ {mapIndex === toolbarStructure.length - 1 && (
863
+ <div className="toolbar-button-group">
864
+ <ToolbarButton
865
+ icon="✗"
866
+ active={false}
867
+ onMouseDown={handleStripFormattingClick}
868
+ />
869
+ </div>
870
+ )}
683
871
  </div>
684
872
  ))}
873
+ {/* Fallback: if no toolbar structure exists, create a row for strip formatting */}
874
+ {toolbarStructure.length === 0 && (
875
+ <div className="toolbar-row">
876
+ <div className="toolbar-button-group">
877
+ <ToolbarButton
878
+ icon="✗"
879
+ active={false}
880
+ onMouseDown={handleStripFormattingClick}
881
+ />
882
+ </div>
883
+ </div>
884
+ )}
685
885
  </div>
686
886
  )}
687
887
  <Editable
688
888
  className={classNames(
689
889
  readOnly ? "bg-gray-4" : "bg-gray-5",
690
890
  "focus-shadow p-2",
891
+ "slate-editable"
691
892
  )}
692
893
  readOnly={readOnly}
693
894
  placeholder={placeholder}
694
- renderPlaceholder={({ attributes, children }) => (
695
- <span {...attributes} className="p-2 text-gray-500">
696
- {children}
697
- </span>
698
- )}
895
+ renderPlaceholder={renderPlaceholder}
699
896
  onFocus={onFocus}
700
897
  onBlur={onBlur}
701
898
  onKeyDown={handleKeyDown}
702
- renderElement={({ attributes, children, element }) => {
703
- const style: React.CSSProperties = {
704
- textAlign: element.align || "left",
705
- };
706
-
707
- if (element.type === "link") {
708
- const isInternal = element.link?.type === "internal";
709
- const url = isInternal
710
- ? "#"
711
- : element.url || element.link?.url || "#";
712
-
713
- return (
714
- <a
715
- {...attributes}
716
- href={url}
717
- style={style}
718
- className={`slate-link ${isInternal ? "internal-link" : "external-link"}`}
719
- onClick={(e) => {
720
- if (!readOnly) {
721
- e.preventDefault();
722
- editLink(element);
723
- }
724
- }}
725
- >
726
- {children}
727
- </a>
728
- );
729
- }
730
-
731
- if (element.type === "list-item") {
732
- const indent = element.indent || 0;
733
- const listType = element.listType || "unordered";
734
- const isOrdered = listType === "ordered";
735
-
736
- const listStyle: React.CSSProperties = {
737
- ...style,
738
- position: "relative",
739
- listStyleType: "none",
740
- };
741
-
742
- return (
743
- <div
744
- {...attributes}
745
- style={listStyle}
746
- className={`slate-list-item slate-list-${listType}`}
747
- data-indent={indent}
748
- >
749
- <span className="slate-list-bullet"></span>
750
- <div className="slate-list-content">{children}</div>
751
- </div>
752
- );
753
- }
754
-
755
- if (element.type === "horizontal-rule") {
756
- return (
757
- <div {...attributes} contentEditable={false} style={{ ...style, userSelect: "none" }}>
758
- <hr style={{
759
- border: "none",
760
- borderTop: "1px solid #ccc",
761
- margin: "1em 0",
762
- width: "100%"
763
- }} />
764
- {children}
765
- </div>
766
- );
767
- }
768
-
769
- // Handle different block types using built-in SLATE_BLOCKS configuration
770
- const isValidBlockId = element.type in SLATE_BLOCKS;
771
- const blockConfig = isValidBlockId ? SLATE_BLOCKS[element.type as BlockId] : undefined;
772
- if (blockConfig && element.type === "no-tag") {
773
- // Special handling for no-tag blocks (plain text without wrapper)
774
- return (
775
- <div {...attributes} style={style}>
776
- {children}
777
- </div>
778
- );
779
- }
780
-
781
- // For standard blocks, use the appropriate HTML tag
782
- if (blockConfig) {
783
- const tagName = blockConfig.htmlTag;
784
- return React.createElement(
785
- tagName,
786
- { ...attributes, style },
787
- children,
788
- );
789
- }
790
- // Default fallback to paragraph
791
- return (
792
- <p {...attributes} style={style}>
793
- {children}
794
- </p>
795
- );
796
- }}
797
- renderLeaf={({ attributes, children, leaf }) => {
798
- let el = <span {...attributes}>{children}</span>;
799
-
800
- // Apply marks using the built-in SLATE_MARKS configuration
801
- simplifiedProfile.marks.forEach((markId) => {
802
- if ((leaf as any)[markId]) {
803
- const markConfig = SLATE_MARKS[markId];
804
- if (markConfig) {
805
- if (markId === "extrabold") {
806
- // Special handling for extrabold with CSS class
807
- el = <span className="extrabold">{el}</span>;
808
- } else {
809
- // Standard HTML tag rendering
810
- const tagName = markConfig.htmlTag;
811
- el = React.createElement(tagName, {}, el);
812
- }
813
- }
814
- }
815
- });
816
-
817
- return el;
818
- }}
899
+ renderElement={renderElement}
900
+ renderLeaf={renderLeaf}
819
901
  />
820
902
  </Slate>
821
903
 
@@ -1,5 +1,5 @@
1
1
  import { Editor } from 'slate';
2
- import { withMark, withBlock, withAlignment, withLink, withList, withNormalization, withInsertion } from '../utils/plugins';
2
+ import { withMark, withBlock, withAlignment, withLink, withList, withNormalization, withInsertion, withFormatting, withVoidElements } from '../utils/plugins';
3
3
 
4
4
  // Create plugins from configuration
5
5
  export const createPluginsFromConfig = (editor: Editor) => {
@@ -11,6 +11,8 @@ export const createPluginsFromConfig = (editor: Editor) => {
11
11
  enhancedEditor = withList(enhancedEditor);
12
12
  enhancedEditor = withLink(enhancedEditor);
13
13
  enhancedEditor = withInsertion(enhancedEditor);
14
+ enhancedEditor = withFormatting(enhancedEditor);
15
+ enhancedEditor = withVoidElements(enhancedEditor);
14
16
 
15
17
  // Add normalization plugin last to ensure it can normalize all other plugin behaviors
16
18
  enhancedEditor = withNormalization(enhancedEditor);
@@ -82,6 +82,8 @@ declare module "slate" {
82
82
  outdentList(): void;
83
83
 
84
84
  insertHorizontalRule(): void;
85
+
86
+ stripFormatting(): void;
85
87
  };
86
88
  Element: CustomElement;
87
89
  Text: CustomText;
@@ -100,6 +102,7 @@ export type CustomText = {
100
102
  readonly superscript?: boolean;
101
103
  readonly extrabold?: boolean;
102
104
  readonly isRawHtml?: boolean; // Flag to indicate this text should be preserved as raw HTML
105
+
103
106
  };
104
107
 
105
108
  export type Alignment = "left" | "center" | "right" | "justify";
@@ -206,7 +206,11 @@ const htmlToSlateInternal = (html: string, profile: SimplifiedProfile): Descenda
206
206
  }
207
207
 
208
208
  if (tagName === 'br') {
209
- results.push({ text: '<br>' });
209
+ // Handle inline br elements as line-break elements
210
+ results.push({
211
+ type: 'line-break',
212
+ children: [{ text: '' }]
213
+ } as any);
210
214
  } else if (tagName === 'a') {
211
215
  const href = childElement.getAttribute('href') || '';
212
216
  const target = childElement.getAttribute('target') || '_self';
@@ -273,7 +277,11 @@ const htmlToSlateInternal = (html: string, profile: SimplifiedProfile): Descenda
273
277
  const tagName = childElement.tagName.toLowerCase();
274
278
 
275
279
  if (tagName === 'br') {
276
- results.push({ text: '<br>' });
280
+ // Handle inline br elements as line-break elements
281
+ results.push({
282
+ type: 'line-break',
283
+ children: [{ text: '' }]
284
+ } as any);
277
285
  } else if (tagName === 'a') {
278
286
  const href = childElement.getAttribute('href') || '';
279
287
  const target = childElement.getAttribute('target') || '_self';
@@ -342,7 +350,11 @@ const htmlToSlateInternal = (html: string, profile: SimplifiedProfile): Descenda
342
350
  const tagName = element.tagName.toLowerCase();
343
351
 
344
352
  if (tagName === 'br') {
345
- combinedChildren.push({ text: '<br>' });
353
+ // Handle inline br elements as line-break elements
354
+ combinedChildren.push({
355
+ type: 'line-break',
356
+ children: [{ text: '' }]
357
+ } as any);
346
358
  } else {
347
359
  // Process other inline elements
348
360
  const inlineChildren = processNodeWithInlines(element, profile.marks);
@@ -370,10 +382,10 @@ const htmlToSlateInternal = (html: string, profile: SimplifiedProfile): Descenda
370
382
  const align = alignMatch ? alignMatch[1] as Alignment : undefined;
371
383
 
372
384
  if (tagName === 'br') {
373
- // Handle standalone br elements at top level
385
+ // Handle standalone br elements as line-break elements
374
386
  nodes.push({
375
- type: defaultBlockType,
376
- children: [{ text: '<br>' }]
387
+ type: 'line-break',
388
+ children: [{ text: '' }]
377
389
  });
378
390
  } else if (tagName === 'hr') {
379
391
  // Handle horizontal rule elements
@@ -448,8 +460,7 @@ export const formatTextWithMarks = (child: CustomText, configuredMarks: readonly
448
460
 
449
461
  let text = child.text;
450
462
 
451
- // Convert literal <br> text back to <br> tags
452
- text = text.replace(/<br>/g, '<br>');
463
+ // Line breaks are now handled as elements, not text nodes
453
464
 
454
465
  // Apply marks using Slate's built-in mark system with strict typing
455
466
  configuredMarks.forEach(markId => {
@@ -563,7 +574,10 @@ const slateToHtmlInternal = (value: Descendant[], profile: SimplifiedProfile): s
563
574
 
564
575
  children.forEach(child => {
565
576
  if (SlateElement.isElement(child)) {
566
- if (child.type === 'link') {
577
+ if (child.type === 'line-break') {
578
+ // Handle inline line break elements
579
+ result += '<br>';
580
+ } else if (child.type === 'link') {
567
581
  const linkElement = child as LinkElement;
568
582
  const isInternal = linkElement.link?.type === 'internal';
569
583
 
@@ -619,6 +633,8 @@ const slateToHtmlInternal = (value: Descendant[], profile: SimplifiedProfile): s
619
633
  // Handle horizontal rule elements
620
634
  if (element.type === 'horizontal-rule') {
621
635
  html += '<hr>';
636
+ } else if (element.type === 'line-break') {
637
+ html += '<br>';
622
638
  } else if (element.type === 'preserved-element') {
623
639
  // Handle preserved elements by restoring their original HTML
624
640
  const preservedElement = element as any;