react-native-richify 1.0.3 → 1.0.5
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/RenderedOutput.js +168 -0
- package/lib/commonjs/components/RenderedOutput.js.map +1 -0
- package/lib/commonjs/components/RichTextInput.js +196 -52
- package/lib/commonjs/components/RichTextInput.js.map +1 -1
- package/lib/commonjs/components/Toolbar.js +41 -2
- package/lib/commonjs/components/Toolbar.js.map +1 -1
- package/lib/commonjs/constants/defaultStyles.js +81 -2
- package/lib/commonjs/constants/defaultStyles.js.map +1 -1
- package/lib/commonjs/hooks/useFormatting.js +46 -2
- package/lib/commonjs/hooks/useFormatting.js.map +1 -1
- package/lib/commonjs/hooks/useRichText.js +130 -12
- package/lib/commonjs/hooks/useRichText.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/utils/formatter.js +48 -12
- package/lib/commonjs/utils/formatter.js.map +1 -1
- package/lib/commonjs/utils/parser.js +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 +259 -0
- package/lib/commonjs/utils/serializer.js.map +1 -0
- package/lib/commonjs/utils/styleMapper.js +11 -0
- package/lib/commonjs/utils/styleMapper.js.map +1 -1
- package/lib/module/components/RenderedOutput.js +163 -0
- package/lib/module/components/RenderedOutput.js.map +1 -0
- package/lib/module/components/RichTextInput.js +198 -55
- package/lib/module/components/RichTextInput.js.map +1 -1
- package/lib/module/components/Toolbar.js +41 -2
- package/lib/module/components/Toolbar.js.map +1 -1
- package/lib/module/constants/defaultStyles.js +81 -2
- package/lib/module/constants/defaultStyles.js.map +1 -1
- package/lib/module/hooks/useFormatting.js +47 -3
- package/lib/module/hooks/useFormatting.js.map +1 -1
- package/lib/module/hooks/useRichText.js +130 -12
- package/lib/module/hooks/useRichText.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/utils/formatter.js +46 -12
- package/lib/module/utils/formatter.js.map +1 -1
- package/lib/module/utils/parser.js +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 +253 -0
- package/lib/module/utils/serializer.js.map +1 -0
- package/lib/module/utils/styleMapper.js +11 -0
- package/lib/module/utils/styleMapper.js.map +1 -1
- package/lib/typescript/src/components/RenderedOutput.d.ts +9 -0
- package/lib/typescript/src/components/RenderedOutput.d.ts.map +1 -0
- 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/components/Toolbar.d.ts.map +1 -1
- package/lib/typescript/src/constants/defaultStyles.d.ts +3 -0
- package/lib/typescript/src/constants/defaultStyles.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useFormatting.d.ts +4 -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 +112 -1
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/formatter.d.ts +9 -1
- package/lib/typescript/src/utils/formatter.d.ts.map +1 -1
- package/lib/typescript/src/utils/parser.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/lib/typescript/src/utils/styleMapper.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/RenderedOutput.tsx +231 -0
- package/src/components/RichTextInput.d.ts +3 -14
- package/src/components/RichTextInput.tsx +291 -56
- package/src/components/Toolbar.tsx +54 -2
- package/src/constants/defaultStyles.d.ts +3 -0
- package/src/constants/defaultStyles.ts +47 -1
- package/src/hooks/useFormatting.ts +89 -2
- package/src/hooks/useRichText.ts +193 -11
- package/src/index.d.ts +2 -1
- package/src/index.ts +10 -0
- package/src/types/index.d.ts +112 -1
- package/src/types/index.ts +123 -1
- package/src/utils/formatter.ts +60 -10
- package/src/utils/parser.ts +6 -1
- package/src/utils/serializer.d.ts +13 -0
- package/src/utils/serializer.ts +365 -0
- package/src/utils/styleMapper.ts +21 -0
|
@@ -4,12 +4,16 @@ import type {
|
|
|
4
4
|
FormatType,
|
|
5
5
|
FormatStyle,
|
|
6
6
|
HeadingLevel,
|
|
7
|
+
ListType,
|
|
7
8
|
SelectionRange,
|
|
9
|
+
TextAlign,
|
|
8
10
|
} from '../types';
|
|
9
11
|
import {
|
|
10
12
|
toggleFormatOnSelection,
|
|
11
13
|
setStyleOnSelection,
|
|
12
14
|
setHeadingOnLine,
|
|
15
|
+
setListTypeOnLine,
|
|
16
|
+
setTextAlignOnLine,
|
|
13
17
|
isFormatActiveInSelection,
|
|
14
18
|
getSelectionStyle,
|
|
15
19
|
} from '../utils/formatter';
|
|
@@ -78,10 +82,90 @@ export function useFormatting({
|
|
|
78
82
|
|
|
79
83
|
const setHeading = useCallback(
|
|
80
84
|
(level: HeadingLevel) => {
|
|
81
|
-
const
|
|
85
|
+
const currentHeading =
|
|
86
|
+
selection.start === selection.end
|
|
87
|
+
? getSelectionStyle(segments, selection).heading ?? activeStyles.heading
|
|
88
|
+
: getSelectionStyle(segments, selection).heading;
|
|
89
|
+
const nextHeading: HeadingLevel =
|
|
90
|
+
currentHeading === level ? 'none' : level;
|
|
91
|
+
|
|
92
|
+
if (selection.start === selection.end) {
|
|
93
|
+
onActiveStylesChange({
|
|
94
|
+
...activeStyles,
|
|
95
|
+
heading: nextHeading === 'none' ? undefined : nextHeading,
|
|
96
|
+
listType:
|
|
97
|
+
nextHeading === 'none' ? activeStyles.listType : undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const newSegments = setHeadingOnLine(segments, selection, nextHeading);
|
|
102
|
+
onSegmentsChange(newSegments);
|
|
103
|
+
},
|
|
104
|
+
[
|
|
105
|
+
activeStyles,
|
|
106
|
+
onActiveStylesChange,
|
|
107
|
+
onSegmentsChange,
|
|
108
|
+
segments,
|
|
109
|
+
selection,
|
|
110
|
+
],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const setListType = useCallback(
|
|
114
|
+
(listType: ListType) => {
|
|
115
|
+
if (selection.start === selection.end) {
|
|
116
|
+
onActiveStylesChange({
|
|
117
|
+
...activeStyles,
|
|
118
|
+
listType: listType === 'none' ? undefined : listType,
|
|
119
|
+
heading: listType === 'none' ? activeStyles.heading : undefined,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const newSegments = setListTypeOnLine(segments, selection, listType);
|
|
124
|
+
onSegmentsChange(newSegments);
|
|
125
|
+
},
|
|
126
|
+
[
|
|
127
|
+
activeStyles,
|
|
128
|
+
onActiveStylesChange,
|
|
129
|
+
onSegmentsChange,
|
|
130
|
+
segments,
|
|
131
|
+
selection,
|
|
132
|
+
],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const setTextAlign = useCallback(
|
|
136
|
+
(textAlign: TextAlign) => {
|
|
137
|
+
if (selection.start === selection.end) {
|
|
138
|
+
onActiveStylesChange({
|
|
139
|
+
...activeStyles,
|
|
140
|
+
textAlign,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const newSegments = setTextAlignOnLine(segments, selection, textAlign);
|
|
82
145
|
onSegmentsChange(newSegments);
|
|
83
146
|
},
|
|
84
|
-
[
|
|
147
|
+
[
|
|
148
|
+
activeStyles,
|
|
149
|
+
onActiveStylesChange,
|
|
150
|
+
onSegmentsChange,
|
|
151
|
+
segments,
|
|
152
|
+
selection,
|
|
153
|
+
],
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const setLink = useCallback(
|
|
157
|
+
(url?: string) => {
|
|
158
|
+
if (selection.start === selection.end) {
|
|
159
|
+
onActiveStylesChange({
|
|
160
|
+
...activeStyles,
|
|
161
|
+
link: url,
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
const newSegments = setStyleOnSelection(segments, selection, 'link', url);
|
|
165
|
+
onSegmentsChange(newSegments);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
[segments, selection, activeStyles, onSegmentsChange, onActiveStylesChange],
|
|
85
169
|
);
|
|
86
170
|
|
|
87
171
|
const setColor = useCallback(
|
|
@@ -126,6 +210,9 @@ export function useFormatting({
|
|
|
126
210
|
toggleFormat,
|
|
127
211
|
setStyleProperty,
|
|
128
212
|
setHeading,
|
|
213
|
+
setListType,
|
|
214
|
+
setTextAlign,
|
|
215
|
+
setLink,
|
|
129
216
|
setColor,
|
|
130
217
|
setBackgroundColor,
|
|
131
218
|
setFontSize,
|
package/src/hooks/useRichText.ts
CHANGED
|
@@ -2,9 +2,12 @@ import { useState, useCallback, useRef } from 'react';
|
|
|
2
2
|
import type {
|
|
3
3
|
StyledSegment,
|
|
4
4
|
FormatStyle,
|
|
5
|
+
ListType,
|
|
6
|
+
OutputFormat,
|
|
5
7
|
SelectionRange,
|
|
6
8
|
RichTextState,
|
|
7
9
|
RichTextActions,
|
|
10
|
+
TextAlign,
|
|
8
11
|
UseRichTextReturn,
|
|
9
12
|
} from '../types';
|
|
10
13
|
import { EMPTY_FORMAT_STYLE } from '../constants/defaultStyles';
|
|
@@ -15,6 +18,7 @@ import {
|
|
|
15
18
|
findPositionInSegments,
|
|
16
19
|
} from '../utils/parser';
|
|
17
20
|
import { getSelectionStyle } from '../utils/formatter';
|
|
21
|
+
import { serializeSegments } from '../utils/serializer';
|
|
18
22
|
import { useSelection } from '../hooks/useSelection';
|
|
19
23
|
import { useFormatting } from '../hooks/useFormatting';
|
|
20
24
|
|
|
@@ -60,6 +64,7 @@ export function useRichText(
|
|
|
60
64
|
selectionRef.current = selection;
|
|
61
65
|
const activeStylesRef = useRef(activeStyles);
|
|
62
66
|
activeStylesRef.current = activeStyles;
|
|
67
|
+
const preserveActiveStylesRef = useRef(false);
|
|
63
68
|
|
|
64
69
|
// ─── Segment Change Handler ──────────────────────────────────────────────
|
|
65
70
|
|
|
@@ -88,10 +93,11 @@ export function useRichText(
|
|
|
88
93
|
(newText: string) => {
|
|
89
94
|
const currentSegments = segmentsRef.current;
|
|
90
95
|
const currentSelection = selectionRef.current;
|
|
91
|
-
const currentActiveStyles =
|
|
96
|
+
const currentActiveStyles = sanitizeTypingStyles(
|
|
92
97
|
currentSelection.start === currentSelection.end
|
|
93
98
|
? activeStylesRef.current
|
|
94
|
-
: getSelectionStyle(currentSegments, currentSelection)
|
|
99
|
+
: getSelectionStyle(currentSegments, currentSelection),
|
|
100
|
+
);
|
|
95
101
|
|
|
96
102
|
const newSegments = reconcileTextChange(
|
|
97
103
|
currentSegments,
|
|
@@ -108,8 +114,22 @@ export function useRichText(
|
|
|
108
114
|
|
|
109
115
|
const onSelectionChange = useCallback(
|
|
110
116
|
(newSelection: SelectionRange) => {
|
|
117
|
+
const previousSelection = selectionRef.current;
|
|
111
118
|
handleSelectionChange(newSelection);
|
|
112
119
|
|
|
120
|
+
const shouldPreserveActiveStyles =
|
|
121
|
+
preserveActiveStylesRef.current &&
|
|
122
|
+
previousSelection.start === previousSelection.end &&
|
|
123
|
+
newSelection.start === newSelection.end &&
|
|
124
|
+
newSelection.start >= previousSelection.start &&
|
|
125
|
+
newSelection.start - previousSelection.start <= 1;
|
|
126
|
+
|
|
127
|
+
if (shouldPreserveActiveStyles) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
preserveActiveStylesRef.current = false;
|
|
132
|
+
|
|
113
133
|
// Update active styles based on cursor position
|
|
114
134
|
if (newSelection.start === newSelection.end) {
|
|
115
135
|
const pos = findPositionInSegments(
|
|
@@ -118,7 +138,7 @@ export function useRichText(
|
|
|
118
138
|
);
|
|
119
139
|
if (segmentsRef.current.length > 0) {
|
|
120
140
|
const seg = segmentsRef.current[pos.segmentIndex];
|
|
121
|
-
setActiveStyles(
|
|
141
|
+
setActiveStyles(sanitizeTypingStyles(seg.styles));
|
|
122
142
|
}
|
|
123
143
|
}
|
|
124
144
|
},
|
|
@@ -131,6 +151,13 @@ export function useRichText(
|
|
|
131
151
|
return segmentsToPlainText(segmentsRef.current);
|
|
132
152
|
}, []);
|
|
133
153
|
|
|
154
|
+
const getOutput = useCallback(
|
|
155
|
+
(format: OutputFormat = 'markdown'): string => {
|
|
156
|
+
return serializeSegments(segmentsRef.current, format);
|
|
157
|
+
},
|
|
158
|
+
[],
|
|
159
|
+
);
|
|
160
|
+
|
|
134
161
|
const exportJSON = useCallback((): StyledSegment[] => {
|
|
135
162
|
return JSON.parse(JSON.stringify(segmentsRef.current));
|
|
136
163
|
}, []);
|
|
@@ -140,14 +167,143 @@ export function useRichText(
|
|
|
140
167
|
const safeSegments =
|
|
141
168
|
newSegments.length > 0 ? newSegments : [createSegment('')];
|
|
142
169
|
updateSegments(safeSegments);
|
|
170
|
+
handleSelectionChange({ start: 0, end: 0 });
|
|
171
|
+
setActiveStyles({ ...EMPTY_FORMAT_STYLE });
|
|
143
172
|
},
|
|
144
|
-
[updateSegments],
|
|
173
|
+
[handleSelectionChange, updateSegments],
|
|
145
174
|
);
|
|
146
175
|
|
|
147
176
|
const clear = useCallback(() => {
|
|
148
177
|
updateSegments([createSegment('')]);
|
|
178
|
+
handleSelectionChange({ start: 0, end: 0 });
|
|
149
179
|
setActiveStyles({ ...EMPTY_FORMAT_STYLE });
|
|
150
|
-
|
|
180
|
+
preserveActiveStylesRef.current = false;
|
|
181
|
+
}, [handleSelectionChange, updateSegments]);
|
|
182
|
+
|
|
183
|
+
const toggleFormat = useCallback<RichTextActions['toggleFormat']>(
|
|
184
|
+
(format) => {
|
|
185
|
+
if (selectionRef.current.start === selectionRef.current.end) {
|
|
186
|
+
preserveActiveStylesRef.current = true;
|
|
187
|
+
}
|
|
188
|
+
formatting.toggleFormat(format);
|
|
189
|
+
},
|
|
190
|
+
[formatting],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const setStyleProperty = useCallback<RichTextActions['setStyleProperty']>(
|
|
194
|
+
(key, value) => {
|
|
195
|
+
if (selectionRef.current.start === selectionRef.current.end) {
|
|
196
|
+
preserveActiveStylesRef.current = true;
|
|
197
|
+
}
|
|
198
|
+
formatting.setStyleProperty(key, value);
|
|
199
|
+
},
|
|
200
|
+
[formatting],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const setHeading = useCallback<RichTextActions['setHeading']>(
|
|
204
|
+
(level) => {
|
|
205
|
+
if (selectionRef.current.start === selectionRef.current.end) {
|
|
206
|
+
preserveActiveStylesRef.current = true;
|
|
207
|
+
}
|
|
208
|
+
formatting.setHeading(level);
|
|
209
|
+
},
|
|
210
|
+
[formatting],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const setListType = useCallback<RichTextActions['setListType']>(
|
|
214
|
+
(type: ListType) => {
|
|
215
|
+
if (selectionRef.current.start === selectionRef.current.end) {
|
|
216
|
+
preserveActiveStylesRef.current = true;
|
|
217
|
+
}
|
|
218
|
+
formatting.setListType(type);
|
|
219
|
+
},
|
|
220
|
+
[formatting],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const setTextAlign = useCallback<RichTextActions['setTextAlign']>(
|
|
224
|
+
(align: TextAlign) => {
|
|
225
|
+
if (selectionRef.current.start === selectionRef.current.end) {
|
|
226
|
+
preserveActiveStylesRef.current = true;
|
|
227
|
+
}
|
|
228
|
+
formatting.setTextAlign(align);
|
|
229
|
+
},
|
|
230
|
+
[formatting],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const setLink = useCallback<RichTextActions['setLink']>(
|
|
234
|
+
(url) => {
|
|
235
|
+
if (selectionRef.current.start === selectionRef.current.end) {
|
|
236
|
+
preserveActiveStylesRef.current = true;
|
|
237
|
+
}
|
|
238
|
+
formatting.setLink(url);
|
|
239
|
+
},
|
|
240
|
+
[formatting],
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const setColor = useCallback<RichTextActions['setColor']>(
|
|
244
|
+
(color) => {
|
|
245
|
+
if (selectionRef.current.start === selectionRef.current.end) {
|
|
246
|
+
preserveActiveStylesRef.current = true;
|
|
247
|
+
}
|
|
248
|
+
formatting.setColor(color);
|
|
249
|
+
},
|
|
250
|
+
[formatting],
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const setBackgroundColor = useCallback<RichTextActions['setBackgroundColor']>(
|
|
254
|
+
(color) => {
|
|
255
|
+
if (selectionRef.current.start === selectionRef.current.end) {
|
|
256
|
+
preserveActiveStylesRef.current = true;
|
|
257
|
+
}
|
|
258
|
+
formatting.setBackgroundColor(color);
|
|
259
|
+
},
|
|
260
|
+
[formatting],
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const setFontSize = useCallback<RichTextActions['setFontSize']>(
|
|
264
|
+
(size) => {
|
|
265
|
+
if (selectionRef.current.start === selectionRef.current.end) {
|
|
266
|
+
preserveActiveStylesRef.current = true;
|
|
267
|
+
}
|
|
268
|
+
formatting.setFontSize(size);
|
|
269
|
+
},
|
|
270
|
+
[formatting],
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const insertImage = useCallback<RichTextActions['insertImage']>(
|
|
274
|
+
(source, options) => {
|
|
275
|
+
const currentSegments = segmentsRef.current;
|
|
276
|
+
const currentSelection = selectionRef.current;
|
|
277
|
+
const plainText = segmentsToPlainText(currentSegments);
|
|
278
|
+
const start = Math.min(currentSelection.start, currentSelection.end);
|
|
279
|
+
const end = Math.max(currentSelection.start, currentSelection.end);
|
|
280
|
+
const placeholder =
|
|
281
|
+
options?.placeholder ?? buildImagePlaceholder(source, options?.alt);
|
|
282
|
+
const nextText = `${plainText.slice(0, start)}${placeholder}${plainText.slice(end)}`;
|
|
283
|
+
const insertionStyles = sanitizeTypingStyles(
|
|
284
|
+
start === end
|
|
285
|
+
? activeStylesRef.current
|
|
286
|
+
: getSelectionStyle(currentSegments, currentSelection),
|
|
287
|
+
);
|
|
288
|
+
const nextSegments = reconcileTextChange(
|
|
289
|
+
currentSegments,
|
|
290
|
+
nextText,
|
|
291
|
+
{
|
|
292
|
+
...insertionStyles,
|
|
293
|
+
imageSrc: source,
|
|
294
|
+
imageAlt: options?.alt,
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
updateSegments(nextSegments);
|
|
299
|
+
handleSelectionChange({
|
|
300
|
+
start: start + placeholder.length,
|
|
301
|
+
end: start + placeholder.length,
|
|
302
|
+
});
|
|
303
|
+
preserveActiveStylesRef.current = false;
|
|
304
|
+
},
|
|
305
|
+
[handleSelectionChange, updateSegments],
|
|
306
|
+
);
|
|
151
307
|
|
|
152
308
|
// ─── Build Return Value ──────────────────────────────────────────────────
|
|
153
309
|
|
|
@@ -158,16 +314,21 @@ export function useRichText(
|
|
|
158
314
|
};
|
|
159
315
|
|
|
160
316
|
const actions: RichTextActions = {
|
|
161
|
-
toggleFormat
|
|
162
|
-
setStyleProperty
|
|
163
|
-
setHeading
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
317
|
+
toggleFormat,
|
|
318
|
+
setStyleProperty,
|
|
319
|
+
setHeading,
|
|
320
|
+
setListType,
|
|
321
|
+
setTextAlign,
|
|
322
|
+
setLink,
|
|
323
|
+
insertImage,
|
|
324
|
+
setColor,
|
|
325
|
+
setBackgroundColor,
|
|
326
|
+
setFontSize,
|
|
167
327
|
handleTextChange,
|
|
168
328
|
handleSelectionChange: onSelectionChange,
|
|
169
329
|
isFormatActive: formatting.isFormatActive,
|
|
170
330
|
getSelectionStyle: formatting.currentSelectionStyle,
|
|
331
|
+
getOutput,
|
|
171
332
|
getPlainText,
|
|
172
333
|
exportJSON,
|
|
173
334
|
importJSON,
|
|
@@ -176,3 +337,24 @@ export function useRichText(
|
|
|
176
337
|
|
|
177
338
|
return { state, actions };
|
|
178
339
|
}
|
|
340
|
+
|
|
341
|
+
function sanitizeTypingStyles(style: FormatStyle): FormatStyle {
|
|
342
|
+
return {
|
|
343
|
+
...style,
|
|
344
|
+
imageSrc: undefined,
|
|
345
|
+
imageAlt: undefined,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildImagePlaceholder(source: string, alt?: string): string {
|
|
350
|
+
if (alt && alt.trim().length > 0) {
|
|
351
|
+
return `[Image: ${alt.trim()}]`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const fileName = source.split('/').pop()?.split('?')[0]?.trim();
|
|
355
|
+
if (fileName) {
|
|
356
|
+
return `[Image: ${fileName}]`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return '[Image]';
|
|
360
|
+
}
|
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, TextAlign, OutputFormat, OutputPreviewMode, FormatStyle, StyledSegment, SelectionRange, RichTextState, RichTextActions, UseRichTextReturn, RichTextTheme, ToolbarItem, ToolbarButtonRenderProps, ToolbarRenderProps, LinkRequestPayload, ImageRequestPayload, 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,9 @@ export type {
|
|
|
53
58
|
FormatType,
|
|
54
59
|
HeadingLevel,
|
|
55
60
|
ListType,
|
|
61
|
+
TextAlign,
|
|
62
|
+
OutputFormat,
|
|
63
|
+
OutputPreviewMode,
|
|
56
64
|
FormatStyle,
|
|
57
65
|
StyledSegment,
|
|
58
66
|
SelectionRange,
|
|
@@ -63,6 +71,8 @@ export type {
|
|
|
63
71
|
ToolbarItem,
|
|
64
72
|
ToolbarButtonRenderProps,
|
|
65
73
|
ToolbarRenderProps,
|
|
74
|
+
LinkRequestPayload,
|
|
75
|
+
ImageRequestPayload,
|
|
66
76
|
OverlayTextProps,
|
|
67
77
|
ToolbarButtonProps,
|
|
68
78
|
ToolbarProps,
|
package/src/types/index.d.ts
CHANGED
|
@@ -11,6 +11,18 @@ 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
|
+
* Paragraph alignment presets.
|
|
16
|
+
*/
|
|
17
|
+
export type TextAlign = 'left' | 'center' | 'right';
|
|
18
|
+
/**
|
|
19
|
+
* Serialized output formats supported by the editor.
|
|
20
|
+
*/
|
|
21
|
+
export type OutputFormat = 'markdown' | 'html';
|
|
22
|
+
/**
|
|
23
|
+
* Output preview modes supported by the editor.
|
|
24
|
+
*/
|
|
25
|
+
export type OutputPreviewMode = 'literal' | 'rendered';
|
|
14
26
|
/**
|
|
15
27
|
* Inline formatting styles attached to a text segment.
|
|
16
28
|
*/
|
|
@@ -24,6 +36,11 @@ export interface FormatStyle {
|
|
|
24
36
|
backgroundColor?: string;
|
|
25
37
|
fontSize?: number;
|
|
26
38
|
heading?: HeadingLevel;
|
|
39
|
+
listType?: ListType;
|
|
40
|
+
textAlign?: TextAlign;
|
|
41
|
+
link?: string;
|
|
42
|
+
imageSrc?: string;
|
|
43
|
+
imageAlt?: string;
|
|
27
44
|
}
|
|
28
45
|
/**
|
|
29
46
|
* A segment of text with uniform formatting.
|
|
@@ -65,6 +82,17 @@ export interface RichTextActions {
|
|
|
65
82
|
setStyleProperty: <K extends keyof FormatStyle>(key: K, value: FormatStyle[K]) => void;
|
|
66
83
|
/** Apply a heading level to the current line. */
|
|
67
84
|
setHeading: (level: HeadingLevel) => void;
|
|
85
|
+
/** Apply a list style to the current line. */
|
|
86
|
+
setListType: (type: ListType) => void;
|
|
87
|
+
/** Apply paragraph alignment to the current line. */
|
|
88
|
+
setTextAlign: (align: TextAlign) => void;
|
|
89
|
+
/** Apply or clear a hyperlink on the current selection. */
|
|
90
|
+
setLink: (url?: string) => void;
|
|
91
|
+
/** Insert an image placeholder into the document. */
|
|
92
|
+
insertImage: (source: string, options?: {
|
|
93
|
+
alt?: string;
|
|
94
|
+
placeholder?: string;
|
|
95
|
+
}) => void;
|
|
68
96
|
/** Set the text color for the current selection. */
|
|
69
97
|
setColor: (color: string) => void;
|
|
70
98
|
/** Set the background color for the current selection. */
|
|
@@ -79,6 +107,8 @@ export interface RichTextActions {
|
|
|
79
107
|
isFormatActive: (format: FormatType) => boolean;
|
|
80
108
|
/** Get the effective shared style at the current cursor/selection. */
|
|
81
109
|
getSelectionStyle: () => FormatStyle;
|
|
110
|
+
/** Serialize the current content as markdown or HTML. */
|
|
111
|
+
getOutput: (format?: OutputFormat) => string;
|
|
82
112
|
/** Get the full plain text content. */
|
|
83
113
|
getPlainText: () => string;
|
|
84
114
|
/** Export the segments as a serializable JSON array. */
|
|
@@ -103,10 +133,18 @@ export interface RichTextTheme {
|
|
|
103
133
|
containerStyle?: ViewStyle;
|
|
104
134
|
/** Style for the TextInput. */
|
|
105
135
|
inputStyle?: TextStyle;
|
|
106
|
-
/** Style for the overlay text container. */
|
|
136
|
+
/** Style for the legacy overlay text container. */
|
|
107
137
|
overlayContainerStyle?: ViewStyle;
|
|
108
138
|
/** Base text style applied to all segments before formatting. */
|
|
109
139
|
baseTextStyle?: TextStyle;
|
|
140
|
+
/** Style for the serialized output container. */
|
|
141
|
+
outputContainerStyle?: ViewStyle;
|
|
142
|
+
/** Label style for the serialized output header. */
|
|
143
|
+
outputLabelStyle?: TextStyle;
|
|
144
|
+
/** Style for the serialized output text. */
|
|
145
|
+
outputTextStyle?: TextStyle;
|
|
146
|
+
/** Style for the rendered output preview content. */
|
|
147
|
+
renderedOutputStyle?: ViewStyle;
|
|
110
148
|
/** Style for the toolbar container. */
|
|
111
149
|
toolbarStyle?: ViewStyle;
|
|
112
150
|
/** Style for toolbar buttons. */
|
|
@@ -133,6 +171,8 @@ export interface RichTextTheme {
|
|
|
133
171
|
toolbarBackground?: string;
|
|
134
172
|
/** Toolbar border color. */
|
|
135
173
|
toolbarBorder?: string;
|
|
174
|
+
/** Default link color. */
|
|
175
|
+
link?: string;
|
|
136
176
|
/** Cursor / caret color. */
|
|
137
177
|
cursor?: ColorValue;
|
|
138
178
|
};
|
|
@@ -149,6 +189,16 @@ export interface ToolbarItem {
|
|
|
149
189
|
format?: FormatType;
|
|
150
190
|
/** The heading level this button sets. */
|
|
151
191
|
heading?: HeadingLevel;
|
|
192
|
+
/** The list type this button sets. */
|
|
193
|
+
listType?: ListType;
|
|
194
|
+
/** The alignment this button sets. */
|
|
195
|
+
textAlign?: TextAlign;
|
|
196
|
+
/** The output format this button toggles to. */
|
|
197
|
+
outputFormat?: OutputFormat;
|
|
198
|
+
/** The output preview mode this button toggles to. */
|
|
199
|
+
outputPreviewMode?: OutputPreviewMode;
|
|
200
|
+
/** Special toolbar action. */
|
|
201
|
+
actionType?: 'link' | 'image';
|
|
152
202
|
/** Custom action handler (overrides default behavior). */
|
|
153
203
|
onPress?: () => void;
|
|
154
204
|
/** Whether this item is currently active. */
|
|
@@ -171,6 +221,33 @@ export interface ToolbarRenderProps {
|
|
|
171
221
|
items: ToolbarItem[];
|
|
172
222
|
state: RichTextState;
|
|
173
223
|
actions: RichTextActions;
|
|
224
|
+
outputFormat: OutputFormat;
|
|
225
|
+
outputPreviewMode: OutputPreviewMode;
|
|
226
|
+
onOutputFormatChange: (format: OutputFormat) => void;
|
|
227
|
+
onOutputPreviewModeChange: (mode: OutputPreviewMode) => void;
|
|
228
|
+
onRequestLink?: () => void;
|
|
229
|
+
onRequestImage?: () => void;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Payload passed when the built-in link button requests a URL.
|
|
233
|
+
*/
|
|
234
|
+
export interface LinkRequestPayload {
|
|
235
|
+
/** Selected plain text at the time of the request. */
|
|
236
|
+
selectedText: string;
|
|
237
|
+
/** Existing URL on the selection, when present. */
|
|
238
|
+
currentUrl?: string;
|
|
239
|
+
/** Apply or clear the URL on the current selection. */
|
|
240
|
+
applyLink: (url?: string) => void;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Payload passed when the built-in image button requests an image source.
|
|
244
|
+
*/
|
|
245
|
+
export interface ImageRequestPayload {
|
|
246
|
+
/** Insert an image placeholder into the document. */
|
|
247
|
+
insertImage: (source: string, options?: {
|
|
248
|
+
alt?: string;
|
|
249
|
+
placeholder?: string;
|
|
250
|
+
}) => void;
|
|
174
251
|
}
|
|
175
252
|
/**
|
|
176
253
|
* Props for the OverlayText component.
|
|
@@ -212,6 +289,18 @@ export interface ToolbarProps {
|
|
|
212
289
|
theme?: RichTextTheme;
|
|
213
290
|
/** Whether to show the toolbar. */
|
|
214
291
|
visible?: boolean;
|
|
292
|
+
/** Currently selected serialized output format. */
|
|
293
|
+
outputFormat?: OutputFormat;
|
|
294
|
+
/** Currently selected preview mode. */
|
|
295
|
+
outputPreviewMode?: OutputPreviewMode;
|
|
296
|
+
/** Called when the output format changes from the toolbar. */
|
|
297
|
+
onOutputFormatChange?: (format: OutputFormat) => void;
|
|
298
|
+
/** Called when the preview mode changes from the toolbar. */
|
|
299
|
+
onOutputPreviewModeChange?: (mode: OutputPreviewMode) => void;
|
|
300
|
+
/** Called when the link button is pressed. */
|
|
301
|
+
onRequestLink?: () => void;
|
|
302
|
+
/** Called when the image button is pressed. */
|
|
303
|
+
onRequestImage?: () => void;
|
|
215
304
|
/** Custom render function for the entire toolbar. */
|
|
216
305
|
renderToolbar?: (props: ToolbarRenderProps) => React.ReactElement | null;
|
|
217
306
|
}
|
|
@@ -239,6 +328,28 @@ export interface RichTextInputProps {
|
|
|
239
328
|
toolbarItems?: ToolbarItem[];
|
|
240
329
|
/** Theme configuration. */
|
|
241
330
|
theme?: RichTextTheme;
|
|
331
|
+
/** Whether to show the serialized output preview below the input. */
|
|
332
|
+
showOutputPreview?: boolean;
|
|
333
|
+
/** Controlled format used for the serialized output preview. */
|
|
334
|
+
outputFormat?: OutputFormat;
|
|
335
|
+
/** Initial format used for the serialized output preview. */
|
|
336
|
+
defaultOutputFormat?: OutputFormat;
|
|
337
|
+
/** Controlled preview mode for the output panel. */
|
|
338
|
+
outputPreviewMode?: OutputPreviewMode;
|
|
339
|
+
/** Initial preview mode for the output panel. */
|
|
340
|
+
defaultOutputPreviewMode?: OutputPreviewMode;
|
|
341
|
+
/** Maximum height for the output preview panel. */
|
|
342
|
+
maxOutputHeight?: number;
|
|
343
|
+
/** Callback when the serialized output changes. */
|
|
344
|
+
onChangeOutput?: (output: string, format: OutputFormat) => void;
|
|
345
|
+
/** Callback when the output format changes. */
|
|
346
|
+
onChangeOutputFormat?: (format: OutputFormat) => void;
|
|
347
|
+
/** Callback when the output preview mode changes. */
|
|
348
|
+
onChangeOutputPreviewMode?: (mode: OutputPreviewMode) => void;
|
|
349
|
+
/** Invoked when the built-in link button needs a URL. */
|
|
350
|
+
onRequestLink?: (payload: LinkRequestPayload) => void;
|
|
351
|
+
/** Invoked when the built-in image button needs an image source. */
|
|
352
|
+
onRequestImage?: (payload: ImageRequestPayload) => void;
|
|
242
353
|
/** Whether multiline input is enabled. */
|
|
243
354
|
multiline?: boolean;
|
|
244
355
|
/** Minimum height for the input area. */
|