react-native-richify 1.0.2 → 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.
- package/lib/commonjs/components/OverlayText.d.js.map +1 -1
- package/lib/commonjs/components/OverlayText.js +8 -2
- package/lib/commonjs/components/OverlayText.js.map +1 -1
- package/lib/commonjs/components/RichTextInput.d.js.map +1 -1
- package/lib/commonjs/components/RichTextInput.js +73 -45
- package/lib/commonjs/components/RichTextInput.js.map +1 -1
- package/lib/commonjs/components/Toolbar.d.js.map +1 -1
- package/lib/commonjs/components/Toolbar.js +4 -7
- package/lib/commonjs/components/Toolbar.js.map +1 -1
- package/lib/commonjs/components/ToolbarButton.d.js.map +1 -1
- package/lib/commonjs/components/ToolbarButton.js.map +1 -1
- package/lib/commonjs/constants/defaultStyles.d.js.map +1 -1
- package/lib/commonjs/constants/defaultStyles.js +26 -1
- package/lib/commonjs/constants/defaultStyles.js.map +1 -1
- package/lib/commonjs/context/RichTextContext.d.js.map +1 -1
- package/lib/commonjs/context/RichTextContext.js.map +1 -1
- package/lib/commonjs/hooks/useFormatting.d.js.map +1 -1
- package/lib/commonjs/hooks/useFormatting.js +7 -1
- package/lib/commonjs/hooks/useFormatting.js.map +1 -1
- package/lib/commonjs/hooks/useRichText.d.js.map +1 -1
- package/lib/commonjs/hooks/useRichText.js +62 -7
- package/lib/commonjs/hooks/useRichText.js.map +1 -1
- package/lib/commonjs/hooks/useSelection.d.js.map +1 -1
- package/lib/commonjs/hooks/useSelection.js.map +1 -1
- package/lib/commonjs/index.d.js +19 -0
- package/lib/commonjs/index.d.js.map +1 -1
- package/lib/commonjs/index.js +19 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/types/index.d.js.map +1 -1
- package/lib/commonjs/types/index.js.map +1 -1
- package/lib/commonjs/utils/formatter.d.js.map +1 -1
- package/lib/commonjs/utils/formatter.js +1 -4
- package/lib/commonjs/utils/formatter.js.map +1 -1
- package/lib/commonjs/utils/parser.d.js.map +1 -1
- package/lib/commonjs/utils/parser.js.map +1 -1
- package/lib/commonjs/utils/serializer.d.js +6 -0
- package/lib/commonjs/utils/serializer.d.js.map +1 -0
- package/lib/commonjs/utils/serializer.js +163 -0
- package/lib/commonjs/utils/serializer.js.map +1 -0
- package/lib/commonjs/utils/styleMapper.d.js.map +1 -1
- package/lib/commonjs/utils/styleMapper.js.map +1 -1
- package/lib/module/components/OverlayText.d.js.map +1 -1
- package/lib/module/components/OverlayText.js +8 -2
- package/lib/module/components/OverlayText.js.map +1 -1
- package/lib/module/components/RichTextInput.d.js.map +1 -1
- package/lib/module/components/RichTextInput.js +75 -48
- package/lib/module/components/RichTextInput.js.map +1 -1
- package/lib/module/components/Toolbar.d.js.map +1 -1
- package/lib/module/components/Toolbar.js +4 -7
- package/lib/module/components/Toolbar.js.map +1 -1
- package/lib/module/components/ToolbarButton.d.js.map +1 -1
- package/lib/module/components/ToolbarButton.js.map +1 -1
- package/lib/module/constants/defaultStyles.d.js.map +1 -1
- package/lib/module/constants/defaultStyles.js +26 -1
- package/lib/module/constants/defaultStyles.js.map +1 -1
- package/lib/module/context/RichTextContext.d.js.map +1 -1
- package/lib/module/context/RichTextContext.js.map +1 -1
- package/lib/module/hooks/useFormatting.d.js.map +1 -1
- package/lib/module/hooks/useFormatting.js +7 -1
- package/lib/module/hooks/useFormatting.js.map +1 -1
- package/lib/module/hooks/useRichText.d.js.map +1 -1
- package/lib/module/hooks/useRichText.js +62 -7
- package/lib/module/hooks/useRichText.js.map +1 -1
- package/lib/module/hooks/useSelection.d.js.map +1 -1
- package/lib/module/hooks/useSelection.js.map +1 -1
- package/lib/module/index.d.js +1 -0
- package/lib/module/index.d.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/types/index.d.js.map +1 -1
- package/lib/module/types/index.js.map +1 -1
- package/lib/module/utils/formatter.d.js.map +1 -1
- package/lib/module/utils/formatter.js +1 -4
- package/lib/module/utils/formatter.js.map +1 -1
- package/lib/module/utils/parser.d.js.map +1 -1
- package/lib/module/utils/parser.js.map +1 -1
- package/lib/module/utils/serializer.d.js +4 -0
- package/lib/module/utils/serializer.d.js.map +1 -0
- package/lib/module/utils/serializer.js +157 -0
- package/lib/module/utils/serializer.js.map +1 -0
- package/lib/module/utils/styleMapper.d.js.map +1 -1
- package/lib/module/utils/styleMapper.js.map +1 -1
- package/lib/typescript/src/components/OverlayText.d.ts.map +1 -1
- package/lib/typescript/src/components/RichTextInput.d.ts +2 -13
- package/lib/typescript/src/components/RichTextInput.d.ts.map +1 -1
- package/lib/typescript/src/constants/defaultStyles.d.ts +2 -0
- package/lib/typescript/src/constants/defaultStyles.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useFormatting.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useRichText.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/index.d.ts +41 -11
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/formatter.d.ts.map +1 -1
- package/lib/typescript/src/utils/serializer.d.ts +14 -0
- package/lib/typescript/src/utils/serializer.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/OverlayText.tsx +11 -3
- package/src/components/RichTextInput.d.ts +3 -14
- package/src/components/RichTextInput.tsx +111 -48
- package/src/components/Toolbar.d.ts +1 -1
- package/src/components/Toolbar.tsx +5 -5
- package/src/components/ToolbarButton.d.ts +1 -1
- package/src/constants/defaultStyles.d.ts +4 -2
- package/src/constants/defaultStyles.ts +26 -1
- package/src/hooks/useFormatting.ts +14 -1
- package/src/hooks/useRichText.ts +103 -10
- package/src/index.d.ts +2 -1
- package/src/index.ts +8 -0
- package/src/types/index.d.ts +41 -11
- package/src/types/index.ts +44 -11
- package/src/utils/formatter.ts +1 -5
- package/src/utils/serializer.d.ts +13 -0
- package/src/utils/serializer.ts +223 -0
package/src/hooks/useRichText.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { useState, useCallback, useRef
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
2
|
import type {
|
|
3
3
|
StyledSegment,
|
|
4
|
-
FormatType,
|
|
5
4
|
FormatStyle,
|
|
6
|
-
|
|
5
|
+
OutputFormat,
|
|
7
6
|
SelectionRange,
|
|
8
7
|
RichTextState,
|
|
9
8
|
RichTextActions,
|
|
@@ -16,6 +15,8 @@ import {
|
|
|
16
15
|
reconcileTextChange,
|
|
17
16
|
findPositionInSegments,
|
|
18
17
|
} from '../utils/parser';
|
|
18
|
+
import { getSelectionStyle } from '../utils/formatter';
|
|
19
|
+
import { serializeSegments } from '../utils/serializer';
|
|
19
20
|
import { useSelection } from '../hooks/useSelection';
|
|
20
21
|
import { useFormatting } from '../hooks/useFormatting';
|
|
21
22
|
|
|
@@ -57,8 +58,11 @@ export function useRichText(
|
|
|
57
58
|
// Refs for stable access in callbacks
|
|
58
59
|
const segmentsRef = useRef(segments);
|
|
59
60
|
segmentsRef.current = segments;
|
|
61
|
+
const selectionRef = useRef(selection);
|
|
62
|
+
selectionRef.current = selection;
|
|
60
63
|
const activeStylesRef = useRef(activeStyles);
|
|
61
64
|
activeStylesRef.current = activeStyles;
|
|
65
|
+
const preserveActiveStylesRef = useRef(false);
|
|
62
66
|
|
|
63
67
|
// ─── Segment Change Handler ──────────────────────────────────────────────
|
|
64
68
|
|
|
@@ -86,7 +90,11 @@ export function useRichText(
|
|
|
86
90
|
const handleTextChange = useCallback(
|
|
87
91
|
(newText: string) => {
|
|
88
92
|
const currentSegments = segmentsRef.current;
|
|
89
|
-
const
|
|
93
|
+
const currentSelection = selectionRef.current;
|
|
94
|
+
const currentActiveStyles =
|
|
95
|
+
currentSelection.start === currentSelection.end
|
|
96
|
+
? activeStylesRef.current
|
|
97
|
+
: getSelectionStyle(currentSegments, currentSelection);
|
|
90
98
|
|
|
91
99
|
const newSegments = reconcileTextChange(
|
|
92
100
|
currentSegments,
|
|
@@ -103,8 +111,22 @@ export function useRichText(
|
|
|
103
111
|
|
|
104
112
|
const onSelectionChange = useCallback(
|
|
105
113
|
(newSelection: SelectionRange) => {
|
|
114
|
+
const previousSelection = selectionRef.current;
|
|
106
115
|
handleSelectionChange(newSelection);
|
|
107
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
|
+
|
|
108
130
|
// Update active styles based on cursor position
|
|
109
131
|
if (newSelection.start === newSelection.end) {
|
|
110
132
|
const pos = findPositionInSegments(
|
|
@@ -126,6 +148,13 @@ export function useRichText(
|
|
|
126
148
|
return segmentsToPlainText(segmentsRef.current);
|
|
127
149
|
}, []);
|
|
128
150
|
|
|
151
|
+
const getOutput = useCallback(
|
|
152
|
+
(format: OutputFormat = 'markdown'): string => {
|
|
153
|
+
return serializeSegments(segmentsRef.current, format);
|
|
154
|
+
},
|
|
155
|
+
[],
|
|
156
|
+
);
|
|
157
|
+
|
|
129
158
|
const exportJSON = useCallback((): StyledSegment[] => {
|
|
130
159
|
return JSON.parse(JSON.stringify(segmentsRef.current));
|
|
131
160
|
}, []);
|
|
@@ -142,8 +171,69 @@ export function useRichText(
|
|
|
142
171
|
const clear = useCallback(() => {
|
|
143
172
|
updateSegments([createSegment('')]);
|
|
144
173
|
setActiveStyles({ ...EMPTY_FORMAT_STYLE });
|
|
174
|
+
preserveActiveStylesRef.current = false;
|
|
145
175
|
}, [updateSegments]);
|
|
146
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
|
+
|
|
147
237
|
// ─── Build Return Value ──────────────────────────────────────────────────
|
|
148
238
|
|
|
149
239
|
const state: RichTextState = {
|
|
@@ -153,14 +243,17 @@ export function useRichText(
|
|
|
153
243
|
};
|
|
154
244
|
|
|
155
245
|
const actions: RichTextActions = {
|
|
156
|
-
toggleFormat
|
|
157
|
-
setStyleProperty
|
|
158
|
-
setHeading
|
|
159
|
-
setColor
|
|
160
|
-
setBackgroundColor
|
|
161
|
-
setFontSize
|
|
246
|
+
toggleFormat,
|
|
247
|
+
setStyleProperty,
|
|
248
|
+
setHeading,
|
|
249
|
+
setColor,
|
|
250
|
+
setBackgroundColor,
|
|
251
|
+
setFontSize,
|
|
162
252
|
handleTextChange,
|
|
163
253
|
handleSelectionChange: onSelectionChange,
|
|
254
|
+
isFormatActive: formatting.isFormatActive,
|
|
255
|
+
getSelectionStyle: formatting.currentSelectionStyle,
|
|
256
|
+
getOutput,
|
|
164
257
|
getPlainText,
|
|
165
258
|
exportJSON,
|
|
166
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, 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,
|
|
@@ -61,6 +67,8 @@ export type {
|
|
|
61
67
|
UseRichTextReturn,
|
|
62
68
|
RichTextTheme,
|
|
63
69
|
ToolbarItem,
|
|
70
|
+
ToolbarButtonRenderProps,
|
|
71
|
+
ToolbarRenderProps,
|
|
64
72
|
OverlayTextProps,
|
|
65
73
|
ToolbarButtonProps,
|
|
66
74
|
ToolbarProps,
|
package/src/types/index.d.ts
CHANGED
|
@@ -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
|
*/
|
|
@@ -75,6 +79,12 @@ export interface RichTextActions {
|
|
|
75
79
|
handleTextChange: (text: string) => void;
|
|
76
80
|
/** Handle selection change from TextInput. */
|
|
77
81
|
handleSelectionChange: (selection: SelectionRange) => void;
|
|
82
|
+
/** Check whether a format is active at the current cursor/selection. */
|
|
83
|
+
isFormatActive: (format: FormatType) => boolean;
|
|
84
|
+
/** Get the effective shared style at the current cursor/selection. */
|
|
85
|
+
getSelectionStyle: () => FormatStyle;
|
|
86
|
+
/** Serialize the current content as markdown or HTML. */
|
|
87
|
+
getOutput: (format?: OutputFormat) => string;
|
|
78
88
|
/** Get the full plain text content. */
|
|
79
89
|
getPlainText: () => string;
|
|
80
90
|
/** Export the segments as a serializable JSON array. */
|
|
@@ -99,10 +109,16 @@ export interface RichTextTheme {
|
|
|
99
109
|
containerStyle?: ViewStyle;
|
|
100
110
|
/** Style for the TextInput. */
|
|
101
111
|
inputStyle?: TextStyle;
|
|
102
|
-
/** Style for the overlay text container. */
|
|
112
|
+
/** Style for the legacy overlay text container. */
|
|
103
113
|
overlayContainerStyle?: ViewStyle;
|
|
104
114
|
/** Base text style applied to all segments before formatting. */
|
|
105
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;
|
|
106
122
|
/** Style for the toolbar container. */
|
|
107
123
|
toolbarStyle?: ViewStyle;
|
|
108
124
|
/** Style for toolbar buttons. */
|
|
@@ -150,11 +166,23 @@ export interface ToolbarItem {
|
|
|
150
166
|
/** Whether this item is currently active. */
|
|
151
167
|
active?: boolean;
|
|
152
168
|
/** Custom render function for the button. */
|
|
153
|
-
renderButton?: (props:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
169
|
+
renderButton?: (props: ToolbarButtonRenderProps) => React.ReactElement | null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Props passed to a custom toolbar button renderer.
|
|
173
|
+
*/
|
|
174
|
+
export interface ToolbarButtonRenderProps {
|
|
175
|
+
active: boolean;
|
|
176
|
+
onPress: () => void;
|
|
177
|
+
label: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Props passed to a custom toolbar renderer.
|
|
181
|
+
*/
|
|
182
|
+
export interface ToolbarRenderProps {
|
|
183
|
+
items: ToolbarItem[];
|
|
184
|
+
state: RichTextState;
|
|
185
|
+
actions: RichTextActions;
|
|
158
186
|
}
|
|
159
187
|
/**
|
|
160
188
|
* Props for the OverlayText component.
|
|
@@ -197,11 +225,7 @@ export interface ToolbarProps {
|
|
|
197
225
|
/** Whether to show the toolbar. */
|
|
198
226
|
visible?: boolean;
|
|
199
227
|
/** Custom render function for the entire toolbar. */
|
|
200
|
-
renderToolbar?: (props:
|
|
201
|
-
items: ToolbarItem[];
|
|
202
|
-
state: RichTextState;
|
|
203
|
-
actions: RichTextActions;
|
|
204
|
-
}) => React.ReactElement;
|
|
228
|
+
renderToolbar?: (props: ToolbarRenderProps) => React.ReactElement | null;
|
|
205
229
|
}
|
|
206
230
|
/**
|
|
207
231
|
* Props for the main RichTextInput component.
|
|
@@ -227,6 +251,12 @@ export interface RichTextInputProps {
|
|
|
227
251
|
toolbarItems?: ToolbarItem[];
|
|
228
252
|
/** Theme configuration. */
|
|
229
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;
|
|
230
260
|
/** Whether multiline input is enabled. */
|
|
231
261
|
multiline?: boolean;
|
|
232
262
|
/** Minimum height for the input area. */
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -101,6 +106,12 @@ export interface RichTextActions {
|
|
|
101
106
|
handleTextChange: (text: string) => void;
|
|
102
107
|
/** Handle selection change from TextInput. */
|
|
103
108
|
handleSelectionChange: (selection: SelectionRange) => void;
|
|
109
|
+
/** Check whether a format is active at the current cursor/selection. */
|
|
110
|
+
isFormatActive: (format: FormatType) => boolean;
|
|
111
|
+
/** Get the effective shared style at the current cursor/selection. */
|
|
112
|
+
getSelectionStyle: () => FormatStyle;
|
|
113
|
+
/** Serialize the current content as markdown or HTML. */
|
|
114
|
+
getOutput: (format?: OutputFormat) => string;
|
|
104
115
|
/** Get the full plain text content. */
|
|
105
116
|
getPlainText: () => string;
|
|
106
117
|
/** Export the segments as a serializable JSON array. */
|
|
@@ -131,10 +142,16 @@ export interface RichTextTheme {
|
|
|
131
142
|
containerStyle?: ViewStyle;
|
|
132
143
|
/** Style for the TextInput. */
|
|
133
144
|
inputStyle?: TextStyle;
|
|
134
|
-
/** Style for the overlay text container. */
|
|
145
|
+
/** Style for the legacy overlay text container. */
|
|
135
146
|
overlayContainerStyle?: ViewStyle;
|
|
136
147
|
/** Base text style applied to all segments before formatting. */
|
|
137
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;
|
|
138
155
|
/** Style for the toolbar container. */
|
|
139
156
|
toolbarStyle?: ViewStyle;
|
|
140
157
|
/** Style for toolbar buttons. */
|
|
@@ -185,11 +202,25 @@ export interface ToolbarItem {
|
|
|
185
202
|
/** Whether this item is currently active. */
|
|
186
203
|
active?: boolean;
|
|
187
204
|
/** Custom render function for the button. */
|
|
188
|
-
renderButton?: (props:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
205
|
+
renderButton?: (props: ToolbarButtonRenderProps) => React.ReactElement | null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Props passed to a custom toolbar button renderer.
|
|
210
|
+
*/
|
|
211
|
+
export interface ToolbarButtonRenderProps {
|
|
212
|
+
active: boolean;
|
|
213
|
+
onPress: () => void;
|
|
214
|
+
label: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Props passed to a custom toolbar renderer.
|
|
219
|
+
*/
|
|
220
|
+
export interface ToolbarRenderProps {
|
|
221
|
+
items: ToolbarItem[];
|
|
222
|
+
state: RichTextState;
|
|
223
|
+
actions: RichTextActions;
|
|
193
224
|
}
|
|
194
225
|
|
|
195
226
|
// ─── Component Props ─────────────────────────────────────────────────────────
|
|
@@ -237,11 +268,7 @@ export interface ToolbarProps {
|
|
|
237
268
|
/** Whether to show the toolbar. */
|
|
238
269
|
visible?: boolean;
|
|
239
270
|
/** Custom render function for the entire toolbar. */
|
|
240
|
-
renderToolbar?: (props:
|
|
241
|
-
items: ToolbarItem[];
|
|
242
|
-
state: RichTextState;
|
|
243
|
-
actions: RichTextActions;
|
|
244
|
-
}) => React.ReactElement;
|
|
271
|
+
renderToolbar?: (props: ToolbarRenderProps) => React.ReactElement | null;
|
|
245
272
|
}
|
|
246
273
|
|
|
247
274
|
/**
|
|
@@ -268,6 +295,12 @@ export interface RichTextInputProps {
|
|
|
268
295
|
toolbarItems?: ToolbarItem[];
|
|
269
296
|
/** Theme configuration. */
|
|
270
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;
|
|
271
304
|
/** Whether multiline input is enabled. */
|
|
272
305
|
multiline?: boolean;
|
|
273
306
|
/** Minimum height for the input area. */
|
package/src/utils/formatter.ts
CHANGED
|
@@ -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);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OutputFormat, StyledSegment } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Serialize styled segments as Markdown or HTML.
|
|
4
|
+
*/
|
|
5
|
+
export declare function serializeSegments(segments: StyledSegment[], format?: OutputFormat): string;
|
|
6
|
+
/**
|
|
7
|
+
* Convenience wrapper for Markdown output.
|
|
8
|
+
*/
|
|
9
|
+
export declare function segmentsToMarkdown(segments: StyledSegment[]): string;
|
|
10
|
+
/**
|
|
11
|
+
* Convenience wrapper for HTML output.
|
|
12
|
+
*/
|
|
13
|
+
export declare function segmentsToHTML(segments: StyledSegment[]): string;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FormatStyle,
|
|
3
|
+
HeadingLevel,
|
|
4
|
+
OutputFormat,
|
|
5
|
+
StyledSegment,
|
|
6
|
+
} from '../types';
|
|
7
|
+
|
|
8
|
+
type LineFragment = Pick<StyledSegment, 'text' | 'styles'>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Serialize styled segments as Markdown or HTML.
|
|
12
|
+
*/
|
|
13
|
+
export function serializeSegments(
|
|
14
|
+
segments: StyledSegment[],
|
|
15
|
+
format: OutputFormat = 'markdown',
|
|
16
|
+
): string {
|
|
17
|
+
const lines = splitSegmentsByLine(segments);
|
|
18
|
+
return lines.map((line) => serializeLine(line, format)).join('\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convenience wrapper for Markdown output.
|
|
23
|
+
*/
|
|
24
|
+
export function segmentsToMarkdown(segments: StyledSegment[]): string {
|
|
25
|
+
return serializeSegments(segments, 'markdown');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convenience wrapper for HTML output.
|
|
30
|
+
*/
|
|
31
|
+
export function segmentsToHTML(segments: StyledSegment[]): string {
|
|
32
|
+
return serializeSegments(segments, 'html');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function splitSegmentsByLine(segments: StyledSegment[]): LineFragment[][] {
|
|
36
|
+
const lines: LineFragment[][] = [[]];
|
|
37
|
+
|
|
38
|
+
for (const segment of segments) {
|
|
39
|
+
const parts = segment.text.split('\n');
|
|
40
|
+
|
|
41
|
+
parts.forEach((part, index) => {
|
|
42
|
+
if (part.length > 0) {
|
|
43
|
+
lines[lines.length - 1]?.push({
|
|
44
|
+
text: part,
|
|
45
|
+
styles: { ...segment.styles },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (index < parts.length - 1) {
|
|
50
|
+
lines.push([]);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function serializeLine(
|
|
59
|
+
line: LineFragment[],
|
|
60
|
+
format: OutputFormat,
|
|
61
|
+
): string {
|
|
62
|
+
const heading = getLineHeading(line);
|
|
63
|
+
const content = line
|
|
64
|
+
.map((fragment) => serializeFragment(fragment, format, heading))
|
|
65
|
+
.join('');
|
|
66
|
+
|
|
67
|
+
if (format === 'html') {
|
|
68
|
+
const blockTag = heading ?? 'p';
|
|
69
|
+
return `<${blockTag}>${content}</${blockTag}>`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const headingPrefix = getHeadingPrefix(heading);
|
|
73
|
+
if (!headingPrefix) {
|
|
74
|
+
return content;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return content.length > 0 ? `${headingPrefix} ${content}` : headingPrefix;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function serializeFragment(
|
|
81
|
+
fragment: LineFragment,
|
|
82
|
+
format: OutputFormat,
|
|
83
|
+
lineHeading?: HeadingLevel,
|
|
84
|
+
): string {
|
|
85
|
+
const normalizedStyles: FormatStyle = {
|
|
86
|
+
...fragment.styles,
|
|
87
|
+
heading: undefined,
|
|
88
|
+
// Markdown headings already express emphasis at the block level.
|
|
89
|
+
bold:
|
|
90
|
+
lineHeading && lineHeading !== 'none' ? false : fragment.styles.bold,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return format === 'html'
|
|
94
|
+
? serializeHtmlFragment(fragment.text, normalizedStyles)
|
|
95
|
+
: serializeMarkdownFragment(fragment.text, normalizedStyles);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function serializeHtmlFragment(text: string, styles: FormatStyle): string {
|
|
99
|
+
let result = escapeHtml(text);
|
|
100
|
+
|
|
101
|
+
if (styles.code) {
|
|
102
|
+
result = `<code>${result}</code>`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (styles.bold) {
|
|
106
|
+
result = `<strong>${result}</strong>`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (styles.italic) {
|
|
110
|
+
result = `<em>${result}</em>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (styles.underline) {
|
|
114
|
+
result = `<u>${result}</u>`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (styles.strikethrough) {
|
|
118
|
+
result = `<s>${result}</s>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const styleAttribute = buildInlineStyle(styles);
|
|
122
|
+
if (styleAttribute) {
|
|
123
|
+
result = `<span style="${styleAttribute}">${result}</span>`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function serializeMarkdownFragment(text: string, styles: FormatStyle): string {
|
|
130
|
+
let result = escapeMarkdown(text);
|
|
131
|
+
|
|
132
|
+
if (styles.code) {
|
|
133
|
+
result = wrapInlineCode(text);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (styles.bold) {
|
|
137
|
+
result = `**${result}**`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (styles.italic) {
|
|
141
|
+
result = `*${result}*`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (styles.strikethrough) {
|
|
145
|
+
result = `~~${result}~~`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (styles.underline) {
|
|
149
|
+
result = `<u>${result}</u>`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const styleAttribute = buildInlineStyle(styles);
|
|
153
|
+
if (styleAttribute) {
|
|
154
|
+
result = `<span style="${styleAttribute}">${result}</span>`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildInlineStyle(styles: FormatStyle): string {
|
|
161
|
+
const cssRules: string[] = [];
|
|
162
|
+
|
|
163
|
+
if (styles.color) {
|
|
164
|
+
cssRules.push(`color: ${styles.color}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (styles.backgroundColor) {
|
|
168
|
+
cssRules.push(`background-color: ${styles.backgroundColor}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (styles.fontSize) {
|
|
172
|
+
cssRules.push(`font-size: ${styles.fontSize}px`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return cssRules.join('; ');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getLineHeading(line: LineFragment[]): HeadingLevel | undefined {
|
|
179
|
+
for (const fragment of line) {
|
|
180
|
+
if (fragment.styles.heading && fragment.styles.heading !== 'none') {
|
|
181
|
+
return fragment.styles.heading;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getHeadingPrefix(heading?: HeadingLevel): string | undefined {
|
|
189
|
+
switch (heading) {
|
|
190
|
+
case 'h1':
|
|
191
|
+
return '#';
|
|
192
|
+
case 'h2':
|
|
193
|
+
return '##';
|
|
194
|
+
case 'h3':
|
|
195
|
+
return '###';
|
|
196
|
+
default:
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function escapeHtml(text: string): string {
|
|
202
|
+
return text
|
|
203
|
+
.replaceAll('&', '&')
|
|
204
|
+
.replaceAll('<', '<')
|
|
205
|
+
.replaceAll('>', '>')
|
|
206
|
+
.replaceAll('"', '"')
|
|
207
|
+
.replaceAll("'", ''');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function escapeMarkdown(text: string): string {
|
|
211
|
+
return text.replace(/([\\`*_~[\]])/g, '\\$1');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function wrapInlineCode(text: string): string {
|
|
215
|
+
const matches = text.match(/`+/g);
|
|
216
|
+
const longestBacktickRun = matches?.reduce(
|
|
217
|
+
(max, match) => Math.max(max, match.length),
|
|
218
|
+
0,
|
|
219
|
+
) ?? 0;
|
|
220
|
+
const fence = '`'.repeat(longestBacktickRun + 1);
|
|
221
|
+
|
|
222
|
+
return `${fence}${text}${fence}`;
|
|
223
|
+
}
|