react-native-richify 1.0.3 → 1.0.4

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 (63) hide show
  1. package/lib/commonjs/components/RichTextInput.js +71 -46
  2. package/lib/commonjs/components/RichTextInput.js.map +1 -1
  3. package/lib/commonjs/constants/defaultStyles.js +26 -1
  4. package/lib/commonjs/constants/defaultStyles.js.map +1 -1
  5. package/lib/commonjs/hooks/useFormatting.js +7 -1
  6. package/lib/commonjs/hooks/useFormatting.js.map +1 -1
  7. package/lib/commonjs/hooks/useRichText.js +55 -6
  8. package/lib/commonjs/hooks/useRichText.js.map +1 -1
  9. package/lib/commonjs/index.d.js +19 -0
  10. package/lib/commonjs/index.d.js.map +1 -1
  11. package/lib/commonjs/index.js +19 -0
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/utils/formatter.js +1 -4
  14. package/lib/commonjs/utils/formatter.js.map +1 -1
  15. package/lib/commonjs/utils/serializer.d.js +6 -0
  16. package/lib/commonjs/utils/serializer.d.js.map +1 -0
  17. package/lib/commonjs/utils/serializer.js +163 -0
  18. package/lib/commonjs/utils/serializer.js.map +1 -0
  19. package/lib/module/components/RichTextInput.js +73 -49
  20. package/lib/module/components/RichTextInput.js.map +1 -1
  21. package/lib/module/constants/defaultStyles.js +26 -1
  22. package/lib/module/constants/defaultStyles.js.map +1 -1
  23. package/lib/module/hooks/useFormatting.js +7 -1
  24. package/lib/module/hooks/useFormatting.js.map +1 -1
  25. package/lib/module/hooks/useRichText.js +55 -6
  26. package/lib/module/hooks/useRichText.js.map +1 -1
  27. package/lib/module/index.d.js +1 -0
  28. package/lib/module/index.d.js.map +1 -1
  29. package/lib/module/index.js +1 -0
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/module/utils/formatter.js +1 -4
  32. package/lib/module/utils/formatter.js.map +1 -1
  33. package/lib/module/utils/serializer.d.js +4 -0
  34. package/lib/module/utils/serializer.d.js.map +1 -0
  35. package/lib/module/utils/serializer.js +157 -0
  36. package/lib/module/utils/serializer.js.map +1 -0
  37. package/lib/typescript/src/components/RichTextInput.d.ts +2 -13
  38. package/lib/typescript/src/components/RichTextInput.d.ts.map +1 -1
  39. package/lib/typescript/src/constants/defaultStyles.d.ts +2 -0
  40. package/lib/typescript/src/constants/defaultStyles.d.ts.map +1 -1
  41. package/lib/typescript/src/hooks/useFormatting.d.ts.map +1 -1
  42. package/lib/typescript/src/hooks/useRichText.d.ts.map +1 -1
  43. package/lib/typescript/src/index.d.ts +2 -1
  44. package/lib/typescript/src/index.d.ts.map +1 -1
  45. package/lib/typescript/src/types/index.d.ts +19 -1
  46. package/lib/typescript/src/types/index.d.ts.map +1 -1
  47. package/lib/typescript/src/utils/formatter.d.ts.map +1 -1
  48. package/lib/typescript/src/utils/serializer.d.ts +14 -0
  49. package/lib/typescript/src/utils/serializer.d.ts.map +1 -0
  50. package/package.json +1 -1
  51. package/src/components/RichTextInput.d.ts +3 -14
  52. package/src/components/RichTextInput.tsx +107 -50
  53. package/src/constants/defaultStyles.d.ts +3 -1
  54. package/src/constants/defaultStyles.ts +26 -1
  55. package/src/hooks/useFormatting.ts +14 -1
  56. package/src/hooks/useRichText.ts +92 -6
  57. package/src/index.d.ts +2 -1
  58. package/src/index.ts +6 -0
  59. package/src/types/index.d.ts +19 -1
  60. package/src/types/index.ts +20 -1
  61. package/src/utils/formatter.ts +1 -5
  62. package/src/utils/serializer.d.ts +13 -0
  63. package/src/utils/serializer.ts +223 -0
@@ -1,20 +1,9 @@
1
1
  import React from 'react';
2
2
  import type { RichTextInputProps } from '../types';
3
3
  /**
4
- * RichTextInput The main rich text editor component.
4
+ * RichTextInput — The main rich text editor component.
5
5
  *
6
- * Uses the Overlay Technique:
7
- * - A transparent `TextInput` on top captures user input and selection
8
- * - A styled `<Text>` layer behind it renders the formatted content
9
- * - Both share identical font metrics for pixel-perfect alignment
10
- *
11
- * @example
12
- * ```tsx
13
- * <RichTextInput
14
- * placeholder="Start typing..."
15
- * showToolbar
16
- * onChangeSegments={(segments) => console.log(segments)}
17
- * />
18
- * ```
6
+ * Uses a plain `TextInput` for editing and renders the serialized rich output
7
+ * below it as Markdown or HTML.
19
8
  */
20
9
  export declare const RichTextInput: React.FC<RichTextInputProps>;
@@ -1,34 +1,35 @@
1
- import React, { useEffect, useCallback } from 'react';
1
+ import React, { useEffect, useCallback, useMemo, useRef } from 'react';
2
2
  import {
3
- View,
4
- TextInput,
3
+ Animated,
4
+ Easing,
5
+ ScrollView,
5
6
  StyleSheet,
7
+ Text,
8
+ TextInput,
9
+ View,
6
10
  type NativeSyntheticEvent,
7
11
  type TextInputSelectionChangeEventData,
8
12
  } from 'react-native';
9
13
  import type { RichTextInputProps } from '../types';
10
14
  import { DEFAULT_THEME } from '../constants/defaultStyles';
11
- import { segmentsToPlainText } from '../utils/parser';
12
15
  import { useRichText } from '../hooks/useRichText';
13
- import { OverlayText } from './OverlayText';
16
+ import { segmentsToPlainText } from '../utils/parser';
17
+ import { serializeSegments } from '../utils/serializer';
14
18
  import { Toolbar } from './Toolbar';
15
19
 
20
+ const OUTPUT_PANEL_HEIGHT = 180;
21
+ const isJestRuntime =
22
+ typeof (
23
+ globalThis as {
24
+ process?: { env?: { JEST_WORKER_ID?: string } };
25
+ }
26
+ ).process?.env?.JEST_WORKER_ID === 'string';
27
+
16
28
  /**
17
29
  * RichTextInput — The main rich text editor component.
18
30
  *
19
- * Uses the Overlay Technique:
20
- * - A transparent `TextInput` on top captures user input and selection
21
- * - A styled `<Text>` layer behind it renders the formatted content
22
- * - Both share identical font metrics for pixel-perfect alignment
23
- *
24
- * @example
25
- * ```tsx
26
- * <RichTextInput
27
- * placeholder="Start typing..."
28
- * showToolbar
29
- * onChangeSegments={(segments) => console.log(segments)}
30
- * />
31
- * ```
31
+ * Uses a plain `TextInput` for editing and renders the serialized rich output
32
+ * below it as Markdown or HTML.
32
33
  */
33
34
  export const RichTextInput: React.FC<RichTextInputProps> = ({
34
35
  initialSegments,
@@ -41,6 +42,9 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
41
42
  toolbarPosition = 'top',
42
43
  toolbarItems,
43
44
  theme,
45
+ showOutputPreview = true,
46
+ outputFormat = 'markdown',
47
+ onChangeOutput,
44
48
  multiline = true,
45
49
  minHeight = 120,
46
50
  maxHeight,
@@ -50,6 +54,7 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
50
54
  onReady,
51
55
  }) => {
52
56
  const resolvedTheme = theme ?? DEFAULT_THEME;
57
+ const previewProgress = useRef(new Animated.Value(0)).current;
53
58
 
54
59
  const { state, actions } = useRichText({
55
60
  initialSegments,
@@ -57,15 +62,35 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
57
62
  onChangeText,
58
63
  });
59
64
 
60
- // Expose actions via onReady callback
61
65
  useEffect(() => {
62
66
  onReady?.(actions);
63
- }, [onReady, actions]);
67
+ }, [actions, onReady]);
64
68
 
65
- // Build plain text for the TextInput value
66
69
  const plainText = segmentsToPlainText(state.segments);
70
+ const serializedOutput = useMemo(
71
+ () => serializeSegments(state.segments, outputFormat),
72
+ [outputFormat, state.segments],
73
+ );
74
+ const shouldShowOutputPreview = showOutputPreview && plainText.length > 0;
75
+
76
+ useEffect(() => {
77
+ onChangeOutput?.(serializedOutput, outputFormat);
78
+ }, [onChangeOutput, outputFormat, serializedOutput]);
79
+
80
+ useEffect(() => {
81
+ if (isJestRuntime) {
82
+ previewProgress.setValue(shouldShowOutputPreview ? 1 : 0);
83
+ return;
84
+ }
85
+
86
+ Animated.timing(previewProgress, {
87
+ toValue: shouldShowOutputPreview ? 1 : 0,
88
+ duration: 180,
89
+ easing: Easing.out(Easing.cubic),
90
+ useNativeDriver: false,
91
+ }).start();
92
+ }, [previewProgress, shouldShowOutputPreview]);
67
93
 
68
- // Handle selection change from TextInput
69
94
  const onSelectionChange = useCallback(
70
95
  (e: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
71
96
  const { start, end } = e.nativeEvent.selection;
@@ -74,28 +99,49 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
74
99
  [actions],
75
100
  );
76
101
 
77
- // Container style
78
102
  const containerStyle = [
79
103
  resolvedTheme.containerStyle ?? DEFAULT_THEME.containerStyle,
80
104
  ];
81
-
82
- // Input area style
83
105
  const inputAreaStyle = [
84
106
  styles.inputArea,
85
107
  { minHeight },
86
108
  maxHeight ? { maxHeight } : undefined,
87
109
  ];
88
-
89
- // Input style
90
110
  const inputStyle = [
91
111
  styles.textInput,
92
112
  resolvedTheme.baseTextStyle ?? DEFAULT_THEME.baseTextStyle,
93
113
  resolvedTheme.inputStyle ?? DEFAULT_THEME.inputStyle,
94
114
  textInputProps?.style,
95
- styles.hiddenInputText,
115
+ ];
116
+ const outputAnimatedStyle = {
117
+ maxHeight: previewProgress.interpolate({
118
+ inputRange: [0, 1],
119
+ outputRange: [0, OUTPUT_PANEL_HEIGHT],
120
+ }),
121
+ opacity: previewProgress,
122
+ marginTop: previewProgress.interpolate({
123
+ inputRange: [0, 1],
124
+ outputRange: [0, 12],
125
+ }),
126
+ transform: [
127
+ {
128
+ translateY: previewProgress.interpolate({
129
+ inputRange: [0, 1],
130
+ outputRange: [-8, 0],
131
+ }),
132
+ },
133
+ ],
134
+ };
135
+ const outputContainerStyle = [
136
+ resolvedTheme.outputContainerStyle ?? DEFAULT_THEME.outputContainerStyle,
137
+ ];
138
+ const outputLabelStyle = [
139
+ resolvedTheme.outputLabelStyle ?? DEFAULT_THEME.outputLabelStyle,
140
+ ];
141
+ const outputTextStyle = [
142
+ resolvedTheme.outputTextStyle ?? DEFAULT_THEME.outputTextStyle,
96
143
  ];
97
144
 
98
- // Toolbar component
99
145
  const toolbarComponent = showToolbar ? (
100
146
  <Toolbar
101
147
  actions={actions}
@@ -106,29 +152,28 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
106
152
  />
107
153
  ) : null;
108
154
 
109
- // Toolbar border
110
155
  const toolbarBorderStyle =
111
156
  toolbarPosition === 'top'
112
- ? { borderBottomWidth: 1, borderBottomColor: resolvedTheme.colors?.toolbarBorder ?? DEFAULT_THEME.colors?.toolbarBorder }
113
- : { borderTopWidth: 1, borderTopColor: resolvedTheme.colors?.toolbarBorder ?? DEFAULT_THEME.colors?.toolbarBorder };
157
+ ? {
158
+ borderBottomWidth: 1,
159
+ borderBottomColor:
160
+ resolvedTheme.colors?.toolbarBorder ??
161
+ DEFAULT_THEME.colors?.toolbarBorder,
162
+ }
163
+ : {
164
+ borderTopWidth: 1,
165
+ borderTopColor:
166
+ resolvedTheme.colors?.toolbarBorder ??
167
+ DEFAULT_THEME.colors?.toolbarBorder,
168
+ };
114
169
 
115
170
  return (
116
171
  <View style={containerStyle}>
117
- {/* Toolbar — Top */}
118
172
  {toolbarPosition === 'top' && toolbarComponent && (
119
173
  <View style={toolbarBorderStyle}>{toolbarComponent}</View>
120
174
  )}
121
175
 
122
- {/* Editor Area */}
123
176
  <View style={inputAreaStyle}>
124
- {/* Overlay — Styled text rendering (behind TextInput) */}
125
- <OverlayText
126
- segments={state.segments}
127
- baseTextStyle={resolvedTheme.baseTextStyle}
128
- theme={resolvedTheme}
129
- />
130
-
131
- {/* TextInput — Transparent layer on top for input capture */}
132
177
  <TextInput
133
178
  {...textInputProps}
134
179
  style={inputStyle}
@@ -144,16 +189,32 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
144
189
  editable={editable}
145
190
  maxLength={maxLength}
146
191
  autoFocus={autoFocus}
147
- underlineColorAndroid="transparent"
148
192
  selectionColor={
149
193
  resolvedTheme.colors?.cursor ?? DEFAULT_THEME.colors?.cursor
150
194
  }
151
195
  textAlignVertical="top"
152
196
  scrollEnabled={typeof maxHeight === 'number'}
153
197
  />
198
+
199
+ {showOutputPreview && (
200
+ <Animated.View
201
+ pointerEvents={shouldShowOutputPreview ? 'auto' : 'none'}
202
+ style={[styles.outputAnimatedWrapper, outputAnimatedStyle]}
203
+ >
204
+ <View style={outputContainerStyle}>
205
+ <Text style={outputLabelStyle}>
206
+ {outputFormat === 'html' ? 'HTML output' : 'Markdown output'}
207
+ </Text>
208
+ <ScrollView showsVerticalScrollIndicator={false}>
209
+ <Text selectable style={outputTextStyle}>
210
+ {serializedOutput}
211
+ </Text>
212
+ </ScrollView>
213
+ </View>
214
+ </Animated.View>
215
+ )}
154
216
  </View>
155
217
 
156
- {/* Toolbar — Bottom */}
157
218
  {toolbarPosition === 'bottom' && toolbarComponent && (
158
219
  <View style={toolbarBorderStyle}>{toolbarComponent}</View>
159
220
  )}
@@ -169,12 +230,8 @@ const styles = StyleSheet.create({
169
230
  },
170
231
  textInput: {
171
232
  position: 'relative',
172
- zIndex: 1,
173
233
  },
174
- hiddenInputText: {
175
- // Keep the editable layer invisible while preserving the caret.
176
- color: 'transparent',
177
- backgroundColor: 'transparent',
178
- textShadowColor: 'transparent',
234
+ outputAnimatedWrapper: {
235
+ overflow: 'hidden',
179
236
  },
180
237
  });
@@ -9,6 +9,8 @@ export declare const DEFAULT_COLORS: {
9
9
  readonly placeholder: "#9CA3AF";
10
10
  readonly toolbarBackground: "#F9FAFB";
11
11
  readonly toolbarBorder: "#E5E7EB";
12
+ readonly outputBackground: "#F8FAFC";
13
+ readonly outputLabel: "#475569";
12
14
  readonly cursor: "#6366F1";
13
15
  readonly activeButtonBg: "#EEF2FF";
14
16
  readonly codeBackground: "#F3F4F6";
@@ -32,7 +34,7 @@ export declare const DEFAULT_BASE_TEXT_STYLE: {
32
34
  readonly fontFamily: undefined;
33
35
  };
34
36
  /**
35
- * Empty format style no formatting applied.
37
+ * Empty format style — no formatting applied.
36
38
  */
37
39
  export declare const EMPTY_FORMAT_STYLE: FormatStyle;
38
40
  /**
@@ -10,6 +10,8 @@ export const DEFAULT_COLORS = {
10
10
  placeholder: '#9CA3AF',
11
11
  toolbarBackground: '#F9FAFB',
12
12
  toolbarBorder: '#E5E7EB',
13
+ outputBackground: '#F8FAFC',
14
+ outputLabel: '#475569',
13
15
  cursor: '#6366F1',
14
16
  activeButtonBg: '#EEF2FF',
15
17
  codeBackground: '#F3F4F6',
@@ -64,7 +66,7 @@ export const DEFAULT_THEME: RichTextTheme = {
64
66
  inputStyle: {
65
67
  fontSize: DEFAULT_BASE_TEXT_STYLE.fontSize,
66
68
  lineHeight: DEFAULT_BASE_TEXT_STYLE.lineHeight,
67
- color: 'transparent',
69
+ color: DEFAULT_COLORS.text,
68
70
  paddingHorizontal: 16,
69
71
  paddingVertical: 12,
70
72
  textAlignVertical: 'top',
@@ -83,6 +85,29 @@ export const DEFAULT_THEME: RichTextTheme = {
83
85
  lineHeight: DEFAULT_BASE_TEXT_STYLE.lineHeight,
84
86
  color: DEFAULT_COLORS.text,
85
87
  },
88
+ outputContainerStyle: {
89
+ marginHorizontal: 12,
90
+ marginBottom: 12,
91
+ padding: 12,
92
+ borderRadius: 10,
93
+ borderWidth: 1,
94
+ borderColor: DEFAULT_COLORS.toolbarBorder,
95
+ backgroundColor: DEFAULT_COLORS.outputBackground,
96
+ },
97
+ outputLabelStyle: {
98
+ marginBottom: 8,
99
+ fontSize: 12,
100
+ fontWeight: '700',
101
+ letterSpacing: 0.4,
102
+ color: DEFAULT_COLORS.outputLabel,
103
+ textTransform: 'uppercase',
104
+ },
105
+ outputTextStyle: {
106
+ fontSize: 14,
107
+ lineHeight: 20,
108
+ color: DEFAULT_COLORS.text,
109
+ fontFamily: 'monospace',
110
+ },
86
111
  toolbarStyle: {
87
112
  flexDirection: 'row',
88
113
  alignItems: 'center',
@@ -78,10 +78,23 @@ export function useFormatting({
78
78
 
79
79
  const setHeading = useCallback(
80
80
  (level: HeadingLevel) => {
81
+ if (selection.start === selection.end) {
82
+ onActiveStylesChange({
83
+ ...activeStyles,
84
+ heading: level === 'none' ? undefined : level,
85
+ });
86
+ }
87
+
81
88
  const newSegments = setHeadingOnLine(segments, selection, level);
82
89
  onSegmentsChange(newSegments);
83
90
  },
84
- [segments, selection, onSegmentsChange],
91
+ [
92
+ activeStyles,
93
+ onActiveStylesChange,
94
+ onSegmentsChange,
95
+ segments,
96
+ selection,
97
+ ],
85
98
  );
86
99
 
87
100
  const setColor = useCallback(
@@ -2,6 +2,7 @@ import { useState, useCallback, useRef } from 'react';
2
2
  import type {
3
3
  StyledSegment,
4
4
  FormatStyle,
5
+ OutputFormat,
5
6
  SelectionRange,
6
7
  RichTextState,
7
8
  RichTextActions,
@@ -15,6 +16,7 @@ import {
15
16
  findPositionInSegments,
16
17
  } from '../utils/parser';
17
18
  import { getSelectionStyle } from '../utils/formatter';
19
+ import { serializeSegments } from '../utils/serializer';
18
20
  import { useSelection } from '../hooks/useSelection';
19
21
  import { useFormatting } from '../hooks/useFormatting';
20
22
 
@@ -60,6 +62,7 @@ export function useRichText(
60
62
  selectionRef.current = selection;
61
63
  const activeStylesRef = useRef(activeStyles);
62
64
  activeStylesRef.current = activeStyles;
65
+ const preserveActiveStylesRef = useRef(false);
63
66
 
64
67
  // ─── Segment Change Handler ──────────────────────────────────────────────
65
68
 
@@ -108,8 +111,22 @@ export function useRichText(
108
111
 
109
112
  const onSelectionChange = useCallback(
110
113
  (newSelection: SelectionRange) => {
114
+ const previousSelection = selectionRef.current;
111
115
  handleSelectionChange(newSelection);
112
116
 
117
+ const shouldPreserveActiveStyles =
118
+ preserveActiveStylesRef.current &&
119
+ previousSelection.start === previousSelection.end &&
120
+ newSelection.start === newSelection.end &&
121
+ newSelection.start >= previousSelection.start &&
122
+ newSelection.start - previousSelection.start <= 1;
123
+
124
+ if (shouldPreserveActiveStyles) {
125
+ return;
126
+ }
127
+
128
+ preserveActiveStylesRef.current = false;
129
+
113
130
  // Update active styles based on cursor position
114
131
  if (newSelection.start === newSelection.end) {
115
132
  const pos = findPositionInSegments(
@@ -131,6 +148,13 @@ export function useRichText(
131
148
  return segmentsToPlainText(segmentsRef.current);
132
149
  }, []);
133
150
 
151
+ const getOutput = useCallback(
152
+ (format: OutputFormat = 'markdown'): string => {
153
+ return serializeSegments(segmentsRef.current, format);
154
+ },
155
+ [],
156
+ );
157
+
134
158
  const exportJSON = useCallback((): StyledSegment[] => {
135
159
  return JSON.parse(JSON.stringify(segmentsRef.current));
136
160
  }, []);
@@ -147,8 +171,69 @@ export function useRichText(
147
171
  const clear = useCallback(() => {
148
172
  updateSegments([createSegment('')]);
149
173
  setActiveStyles({ ...EMPTY_FORMAT_STYLE });
174
+ preserveActiveStylesRef.current = false;
150
175
  }, [updateSegments]);
151
176
 
177
+ const toggleFormat = useCallback<RichTextActions['toggleFormat']>(
178
+ (format) => {
179
+ if (selectionRef.current.start === selectionRef.current.end) {
180
+ preserveActiveStylesRef.current = true;
181
+ }
182
+ formatting.toggleFormat(format);
183
+ },
184
+ [formatting],
185
+ );
186
+
187
+ const setStyleProperty = useCallback<RichTextActions['setStyleProperty']>(
188
+ (key, value) => {
189
+ if (selectionRef.current.start === selectionRef.current.end) {
190
+ preserveActiveStylesRef.current = true;
191
+ }
192
+ formatting.setStyleProperty(key, value);
193
+ },
194
+ [formatting],
195
+ );
196
+
197
+ const setHeading = useCallback<RichTextActions['setHeading']>(
198
+ (level) => {
199
+ if (selectionRef.current.start === selectionRef.current.end) {
200
+ preserveActiveStylesRef.current = true;
201
+ }
202
+ formatting.setHeading(level);
203
+ },
204
+ [formatting],
205
+ );
206
+
207
+ const setColor = useCallback<RichTextActions['setColor']>(
208
+ (color) => {
209
+ if (selectionRef.current.start === selectionRef.current.end) {
210
+ preserveActiveStylesRef.current = true;
211
+ }
212
+ formatting.setColor(color);
213
+ },
214
+ [formatting],
215
+ );
216
+
217
+ const setBackgroundColor = useCallback<RichTextActions['setBackgroundColor']>(
218
+ (color) => {
219
+ if (selectionRef.current.start === selectionRef.current.end) {
220
+ preserveActiveStylesRef.current = true;
221
+ }
222
+ formatting.setBackgroundColor(color);
223
+ },
224
+ [formatting],
225
+ );
226
+
227
+ const setFontSize = useCallback<RichTextActions['setFontSize']>(
228
+ (size) => {
229
+ if (selectionRef.current.start === selectionRef.current.end) {
230
+ preserveActiveStylesRef.current = true;
231
+ }
232
+ formatting.setFontSize(size);
233
+ },
234
+ [formatting],
235
+ );
236
+
152
237
  // ─── Build Return Value ──────────────────────────────────────────────────
153
238
 
154
239
  const state: RichTextState = {
@@ -158,16 +243,17 @@ export function useRichText(
158
243
  };
159
244
 
160
245
  const actions: RichTextActions = {
161
- toggleFormat: formatting.toggleFormat,
162
- setStyleProperty: formatting.setStyleProperty,
163
- setHeading: formatting.setHeading,
164
- setColor: formatting.setColor,
165
- setBackgroundColor: formatting.setBackgroundColor,
166
- setFontSize: formatting.setFontSize,
246
+ toggleFormat,
247
+ setStyleProperty,
248
+ setHeading,
249
+ setColor,
250
+ setBackgroundColor,
251
+ setFontSize,
167
252
  handleTextChange,
168
253
  handleSelectionChange: onSelectionChange,
169
254
  isFormatActive: formatting.isFormatActive,
170
255
  getSelectionStyle: formatting.currentSelectionStyle,
256
+ getOutput,
171
257
  getPlainText,
172
258
  exportJSON,
173
259
  importJSON,
package/src/index.d.ts CHANGED
@@ -11,5 +11,6 @@ export type { RichTextProviderProps } from './context/RichTextContext';
11
11
  export { createSegment, segmentsToPlainText, getTotalLength, mergeAdjacentSegments, reconcileTextChange, } from './utils/parser';
12
12
  export { toggleFormatOnSelection, setStyleOnSelection, setHeadingOnLine, isFormatActiveInSelection, getSelectionStyle, } from './utils/formatter';
13
13
  export { formatStyleToTextStyle, segmentToTextStyle, segmentsToTextStyles, } from './utils/styleMapper';
14
+ export { serializeSegments, segmentsToMarkdown, segmentsToHTML, } from './utils/serializer';
14
15
  export { DEFAULT_COLORS, DEFAULT_THEME, DEFAULT_TOOLBAR_ITEMS, DEFAULT_BASE_TEXT_STYLE, HEADING_FONT_SIZES, EMPTY_FORMAT_STYLE, } from './constants/defaultStyles';
15
- export type { FormatType, HeadingLevel, ListType, FormatStyle, StyledSegment, SelectionRange, RichTextState, RichTextActions, UseRichTextReturn, RichTextTheme, ToolbarItem, ToolbarButtonRenderProps, ToolbarRenderProps, OverlayTextProps, ToolbarButtonProps, ToolbarProps, RichTextInputProps, } from './types';
16
+ export type { FormatType, HeadingLevel, ListType, OutputFormat, FormatStyle, StyledSegment, SelectionRange, RichTextState, RichTextActions, UseRichTextReturn, RichTextTheme, ToolbarItem, ToolbarButtonRenderProps, ToolbarRenderProps, OverlayTextProps, ToolbarButtonProps, ToolbarProps, RichTextInputProps, } from './types';
package/src/index.ts CHANGED
@@ -37,6 +37,11 @@ export {
37
37
  segmentToTextStyle,
38
38
  segmentsToTextStyles,
39
39
  } from './utils/styleMapper';
40
+ export {
41
+ serializeSegments,
42
+ segmentsToMarkdown,
43
+ segmentsToHTML,
44
+ } from './utils/serializer';
40
45
 
41
46
  // ─── Constants ───────────────────────────────────────────────────────────────
42
47
  export {
@@ -53,6 +58,7 @@ export type {
53
58
  FormatType,
54
59
  HeadingLevel,
55
60
  ListType,
61
+ OutputFormat,
56
62
  FormatStyle,
57
63
  StyledSegment,
58
64
  SelectionRange,
@@ -11,6 +11,10 @@ export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'none';
11
11
  * List type for a line/paragraph.
12
12
  */
13
13
  export type ListType = 'bullet' | 'ordered' | 'none';
14
+ /**
15
+ * Serialized output formats supported by the editor.
16
+ */
17
+ export type OutputFormat = 'markdown' | 'html';
14
18
  /**
15
19
  * Inline formatting styles attached to a text segment.
16
20
  */
@@ -79,6 +83,8 @@ export interface RichTextActions {
79
83
  isFormatActive: (format: FormatType) => boolean;
80
84
  /** Get the effective shared style at the current cursor/selection. */
81
85
  getSelectionStyle: () => FormatStyle;
86
+ /** Serialize the current content as markdown or HTML. */
87
+ getOutput: (format?: OutputFormat) => string;
82
88
  /** Get the full plain text content. */
83
89
  getPlainText: () => string;
84
90
  /** Export the segments as a serializable JSON array. */
@@ -103,10 +109,16 @@ export interface RichTextTheme {
103
109
  containerStyle?: ViewStyle;
104
110
  /** Style for the TextInput. */
105
111
  inputStyle?: TextStyle;
106
- /** Style for the overlay text container. */
112
+ /** Style for the legacy overlay text container. */
107
113
  overlayContainerStyle?: ViewStyle;
108
114
  /** Base text style applied to all segments before formatting. */
109
115
  baseTextStyle?: TextStyle;
116
+ /** Style for the serialized output container. */
117
+ outputContainerStyle?: ViewStyle;
118
+ /** Label style for the serialized output header. */
119
+ outputLabelStyle?: TextStyle;
120
+ /** Style for the serialized output text. */
121
+ outputTextStyle?: TextStyle;
110
122
  /** Style for the toolbar container. */
111
123
  toolbarStyle?: ViewStyle;
112
124
  /** Style for toolbar buttons. */
@@ -239,6 +251,12 @@ export interface RichTextInputProps {
239
251
  toolbarItems?: ToolbarItem[];
240
252
  /** Theme configuration. */
241
253
  theme?: RichTextTheme;
254
+ /** Whether to show the serialized output preview below the input. */
255
+ showOutputPreview?: boolean;
256
+ /** Format used for the serialized output preview. */
257
+ outputFormat?: OutputFormat;
258
+ /** Callback when the serialized output changes. */
259
+ onChangeOutput?: (output: string, format: OutputFormat) => void;
242
260
  /** Whether multiline input is enabled. */
243
261
  multiline?: boolean;
244
262
  /** Minimum height for the input area. */
@@ -22,6 +22,11 @@ export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'none';
22
22
  */
23
23
  export type ListType = 'bullet' | 'ordered' | 'none';
24
24
 
25
+ /**
26
+ * Serialized output formats supported by the editor.
27
+ */
28
+ export type OutputFormat = 'markdown' | 'html';
29
+
25
30
  // ─── Style Types ─────────────────────────────────────────────────────────────
26
31
 
27
32
  /**
@@ -105,6 +110,8 @@ export interface RichTextActions {
105
110
  isFormatActive: (format: FormatType) => boolean;
106
111
  /** Get the effective shared style at the current cursor/selection. */
107
112
  getSelectionStyle: () => FormatStyle;
113
+ /** Serialize the current content as markdown or HTML. */
114
+ getOutput: (format?: OutputFormat) => string;
108
115
  /** Get the full plain text content. */
109
116
  getPlainText: () => string;
110
117
  /** Export the segments as a serializable JSON array. */
@@ -135,10 +142,16 @@ export interface RichTextTheme {
135
142
  containerStyle?: ViewStyle;
136
143
  /** Style for the TextInput. */
137
144
  inputStyle?: TextStyle;
138
- /** Style for the overlay text container. */
145
+ /** Style for the legacy overlay text container. */
139
146
  overlayContainerStyle?: ViewStyle;
140
147
  /** Base text style applied to all segments before formatting. */
141
148
  baseTextStyle?: TextStyle;
149
+ /** Style for the serialized output container. */
150
+ outputContainerStyle?: ViewStyle;
151
+ /** Label style for the serialized output header. */
152
+ outputLabelStyle?: TextStyle;
153
+ /** Style for the serialized output text. */
154
+ outputTextStyle?: TextStyle;
142
155
  /** Style for the toolbar container. */
143
156
  toolbarStyle?: ViewStyle;
144
157
  /** Style for toolbar buttons. */
@@ -282,6 +295,12 @@ export interface RichTextInputProps {
282
295
  toolbarItems?: ToolbarItem[];
283
296
  /** Theme configuration. */
284
297
  theme?: RichTextTheme;
298
+ /** Whether to show the serialized output preview below the input. */
299
+ showOutputPreview?: boolean;
300
+ /** Format used for the serialized output preview. */
301
+ outputFormat?: OutputFormat;
302
+ /** Callback when the serialized output changes. */
303
+ onChangeOutput?: (output: string, format: OutputFormat) => void;
285
304
  /** Whether multiline input is enabled. */
286
305
  multiline?: boolean;
287
306
  /** Minimum height for the input area. */
@@ -8,11 +8,9 @@ import type {
8
8
  import {
9
9
  createSegment,
10
10
  findPositionInSegments,
11
- splitSegment,
12
11
  mergeAdjacentSegments,
13
12
  segmentsToPlainText,
14
13
  } from '../utils/parser';
15
- import { HEADING_FONT_SIZES } from '../constants/defaultStyles';
16
14
 
17
15
  /**
18
16
  * Toggle an inline format (bold, italic, etc.) on the selected range.
@@ -73,9 +71,7 @@ export function setHeadingOnLine(
73
71
  const { lineStart, lineEnd } = getLineRange(plainText, selection.start);
74
72
 
75
73
  const headingStyle: Partial<FormatStyle> = {
76
- heading: level,
77
- fontSize: HEADING_FONT_SIZES[level],
78
- bold: level !== 'none' ? true : undefined,
74
+ heading: level === 'none' ? undefined : level,
79
75
  };
80
76
 
81
77
  return applyStyleToRange(segments, lineStart, lineEnd, headingStyle);