@apollohg/react-native-prose-editor 0.5.1 → 0.5.3

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 (31) hide show
  1. package/README.md +18 -15
  2. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +4 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +33 -1
  4. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +23 -0
  5. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +39 -6
  6. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +15 -1
  7. package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +44 -7
  8. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +24 -4
  9. package/dist/NativeEditorBridge.d.ts +8 -0
  10. package/dist/NativeEditorBridge.js +16 -0
  11. package/dist/NativeProseViewer.d.ts +25 -5
  12. package/dist/NativeProseViewer.js +212 -13
  13. package/dist/NativeRichTextEditor.d.ts +2 -0
  14. package/dist/NativeRichTextEditor.js +417 -31
  15. package/dist/addons.d.ts +20 -0
  16. package/dist/addons.js +4 -0
  17. package/dist/index.d.ts +2 -2
  18. package/ios/EditorAddons.swift +2 -0
  19. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  20. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  21. package/ios/EditorLayoutManager.swift +10 -1
  22. package/ios/EditorTheme.swift +25 -0
  23. package/ios/NativeEditorExpoView.swift +56 -6
  24. package/ios/NativeEditorModule.swift +14 -1
  25. package/ios/NativeProseViewerExpoView.swift +62 -11
  26. package/ios/RenderBridge.swift +40 -16
  27. package/ios/RichTextEditorView.swift +4 -0
  28. package/package.json +1 -1
  29. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  30. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  31. package/rust/android/x86_64/libeditor_core.so +0 -0
@@ -17,6 +17,7 @@ function getNativeProseViewerModule() {
17
17
  return nativeProseViewerModule;
18
18
  }
19
19
  const serializedJsonCache = new WeakMap();
20
+ const EMPTY_TEXT_BLOCK_PLACEHOLDER = '\u200B';
20
21
  function stringifyCachedJson(value) {
21
22
  if (value != null && typeof value === 'object') {
22
23
  const cached = serializedJsonCache.get(value);
@@ -56,7 +57,21 @@ function normalizeMentionAttrs(node) {
56
57
  }
57
58
  return attrs;
58
59
  }
59
- function collectMentionPayloadsByDocPos(document) {
60
+ function baseMentionLabelFromAttrs(attrs) {
61
+ const label = attrs.label;
62
+ return typeof label === 'string' && label.length > 0 ? label : 'mention';
63
+ }
64
+ function resolveMentionPrefix(mentionPrefix, mention) {
65
+ const rawPrefix = typeof mentionPrefix === 'function' ? mentionPrefix(mention) : mentionPrefix;
66
+ return typeof rawPrefix === 'string' && rawPrefix.length > 0 ? rawPrefix : undefined;
67
+ }
68
+ function applyMentionPrefix(label, prefix) {
69
+ if (!prefix || label.startsWith(prefix)) {
70
+ return label;
71
+ }
72
+ return `${prefix}${label}`;
73
+ }
74
+ function collectMentionPayloadsByDocPos(document, mentionPrefix, resolveMentionTheme) {
60
75
  const mentions = new Map();
61
76
  const visit = (node, pos, isRoot = false) => {
62
77
  if (node == null || typeof node !== 'object') {
@@ -71,8 +86,15 @@ function collectMentionPayloadsByDocPos(document) {
71
86
  }
72
87
  if (nodeType === 'mention') {
73
88
  const attrs = normalizeMentionAttrs(nodeRecord);
74
- const label = typeof attrs.label === 'string' ? attrs.label : undefined;
75
- mentions.set(pos, { label, attrs });
89
+ const label = baseMentionLabelFromAttrs(attrs);
90
+ const mentionContext = { docPos: pos, label, attrs };
91
+ const renderedLabel = applyMentionPrefix(label, resolveMentionPrefix(mentionPrefix, mentionContext));
92
+ const mentionTheme = resolveMentionTheme?.(mentionContext) ?? undefined;
93
+ mentions.set(pos, {
94
+ ...mentionContext,
95
+ renderedLabel,
96
+ mentionTheme,
97
+ });
76
98
  }
77
99
  if (isRoot && nodeType === 'doc') {
78
100
  let nextPos = pos;
@@ -93,6 +115,132 @@ function collectMentionPayloadsByDocPos(document) {
93
115
  visit(document, 0, true);
94
116
  return mentions;
95
117
  }
118
+ function applyResolvedMentionRendering(renderJson, mentionPayloadsByDocPos) {
119
+ if (mentionPayloadsByDocPos.size === 0) {
120
+ return renderJson;
121
+ }
122
+ let parsedElements;
123
+ try {
124
+ parsedElements = JSON.parse(renderJson);
125
+ }
126
+ catch {
127
+ return renderJson;
128
+ }
129
+ if (!Array.isArray(parsedElements)) {
130
+ return renderJson;
131
+ }
132
+ let didChange = false;
133
+ const nextElements = parsedElements.map((element) => {
134
+ if (element == null || typeof element !== 'object' || Array.isArray(element)) {
135
+ return element;
136
+ }
137
+ const renderElement = element;
138
+ if (renderElement.type !== 'opaqueInlineAtom' ||
139
+ renderElement.nodeType !== 'mention' ||
140
+ typeof renderElement.docPos !== 'number') {
141
+ return element;
142
+ }
143
+ const mention = mentionPayloadsByDocPos.get(renderElement.docPos);
144
+ if (!mention) {
145
+ return element;
146
+ }
147
+ let nextElement = renderElement;
148
+ if (renderElement.label !== mention.renderedLabel) {
149
+ nextElement = { ...nextElement, label: mention.renderedLabel };
150
+ didChange = true;
151
+ }
152
+ if (mention.mentionTheme && Object.keys(mention.mentionTheme).length > 0) {
153
+ nextElement =
154
+ nextElement === renderElement ? { ...nextElement } : nextElement;
155
+ nextElement.mentionTheme = mention.mentionTheme;
156
+ didChange = true;
157
+ }
158
+ return nextElement;
159
+ });
160
+ return didChange ? JSON.stringify(nextElements) : renderJson;
161
+ }
162
+ function isTopLevelSingleElementBlock(element) {
163
+ return element.type === 'voidBlock' || element.type === 'opaqueBlockAtom';
164
+ }
165
+ function isEmptyParagraphPlaceholderText(text) {
166
+ if (text.length === 0) {
167
+ return false;
168
+ }
169
+ return Array.from(text).every((char) => char === EMPTY_TEXT_BLOCK_PLACEHOLDER);
170
+ }
171
+ function isTrailingEmptyParagraphRange(elements, start, endExclusive) {
172
+ const startElement = elements[start];
173
+ const endElement = elements[endExclusive - 1];
174
+ if (startElement?.type !== 'blockStart' ||
175
+ startElement.nodeType !== 'paragraph' ||
176
+ startElement.depth !== 0 ||
177
+ endElement?.type !== 'blockEnd') {
178
+ return false;
179
+ }
180
+ const innerElements = elements.slice(start + 1, endExclusive - 1);
181
+ return (innerElements.length > 0 &&
182
+ innerElements.every((element) => element.type === 'textRun' &&
183
+ typeof element.text === 'string' &&
184
+ isEmptyParagraphPlaceholderText(element.text)));
185
+ }
186
+ function collapseTrailingEmptyParagraphRenderElements(renderJson) {
187
+ let parsedElements;
188
+ try {
189
+ parsedElements = JSON.parse(renderJson);
190
+ }
191
+ catch {
192
+ return renderJson;
193
+ }
194
+ if (!Array.isArray(parsedElements)) {
195
+ return renderJson;
196
+ }
197
+ const elements = parsedElements;
198
+ const topLevelRanges = [];
199
+ for (let index = 0; index < elements.length; index += 1) {
200
+ const element = elements[index];
201
+ if (!element || typeof element !== 'object' || Array.isArray(element)) {
202
+ continue;
203
+ }
204
+ if (element.type === 'blockStart' && element.depth === 0) {
205
+ let nestingDepth = 1;
206
+ let cursor = index + 1;
207
+ while (cursor < elements.length && nestingDepth > 0) {
208
+ const current = elements[cursor];
209
+ if (current?.type === 'blockStart') {
210
+ nestingDepth += 1;
211
+ }
212
+ else if (current?.type === 'blockEnd') {
213
+ nestingDepth -= 1;
214
+ }
215
+ cursor += 1;
216
+ }
217
+ if (nestingDepth !== 0) {
218
+ return renderJson;
219
+ }
220
+ topLevelRanges.push({ start: index, endExclusive: cursor });
221
+ index = cursor - 1;
222
+ continue;
223
+ }
224
+ if (isTopLevelSingleElementBlock(element)) {
225
+ topLevelRanges.push({ start: index, endExclusive: index + 1 });
226
+ }
227
+ }
228
+ if (topLevelRanges.length <= 1) {
229
+ return renderJson;
230
+ }
231
+ let trimStart = null;
232
+ for (let rangeIndex = topLevelRanges.length - 1; rangeIndex >= 1; rangeIndex -= 1) {
233
+ const range = topLevelRanges[rangeIndex];
234
+ if (!isTrailingEmptyParagraphRange(elements, range.start, range.endExclusive)) {
235
+ break;
236
+ }
237
+ trimStart = range.start;
238
+ }
239
+ if (trimStart == null) {
240
+ return renderJson;
241
+ }
242
+ return JSON.stringify(elements.slice(0, trimStart));
243
+ }
96
244
  function serializeDocumentInput(document, schema) {
97
245
  if (typeof document === 'string') {
98
246
  try {
@@ -129,34 +277,73 @@ function extractRenderError(json) {
129
277
  return null;
130
278
  }
131
279
  }
132
- function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, style, allowBase64Images = false, onPressMention, }) {
280
+ function NativeProseViewer({ ...props }) {
281
+ const { contentRevision, contentJSONRevision, schema, theme, style, allowBase64Images = false, collapseTrailingEmptyParagraphs = true, enableLinkTaps = true, mentionPrefix, resolveMentionTheme, onPressLink, onPressMention, } = props;
282
+ const contentJSON = 'contentJSON' in props ? props.contentJSON : undefined;
283
+ const contentHTML = 'contentHTML' in props ? props.contentHTML : undefined;
284
+ const resolvedContentRevision = contentRevision ?? contentJSONRevision;
133
285
  const documentSchema = (0, react_1.useMemo)(() => (0, addons_1.withMentionsSchema)(schema ?? schemas_1.tiptapSchema), [schema]);
134
- const { normalizedDocument, serializedContentJson } = (0, react_1.useMemo)(() => serializeDocumentInput(contentJSON, documentSchema), [contentJSON, contentJSONRevision, documentSchema]);
286
+ const { normalizedDocument, serializedContentJson } = (0, react_1.useMemo)(() => {
287
+ if (contentJSON === undefined) {
288
+ return {
289
+ normalizedDocument: null,
290
+ serializedContentJson: null,
291
+ };
292
+ }
293
+ return serializeDocumentInput(contentJSON, documentSchema);
294
+ }, [contentJSON, resolvedContentRevision, documentSchema]);
135
295
  const themeJson = (0, react_1.useMemo)(() => (0, EditorTheme_1.serializeEditorTheme)(theme), [theme]);
136
296
  const mentionPayloadsByDocPos = (0, react_1.useMemo)(() => normalizedDocument == null
137
297
  ? new Map()
138
- : collectMentionPayloadsByDocPos(normalizedDocument), [normalizedDocument]);
298
+ : collectMentionPayloadsByDocPos(normalizedDocument, mentionPrefix, resolveMentionTheme), [mentionPrefix, normalizedDocument, resolveMentionTheme]);
139
299
  const renderJson = (0, react_1.useMemo)(() => {
140
300
  const configJson = JSON.stringify({
141
301
  schema: documentSchema,
142
302
  ...(allowBase64Images ? { allowBase64Images } : {}),
143
303
  });
144
- const nextRenderJson = getNativeProseViewerModule().renderDocumentJson(configJson, serializedContentJson);
304
+ const nextRenderJson = serializedContentJson != null
305
+ ? getNativeProseViewerModule().renderDocumentJson(configJson, serializedContentJson)
306
+ : getNativeProseViewerModule().renderDocumentHtml(configJson, contentHTML ?? '');
145
307
  const renderError = extractRenderError(nextRenderJson);
146
308
  if (renderError != null) {
147
309
  console.error(`NativeProseViewer: ${renderError}`);
148
310
  return '[]';
149
311
  }
150
312
  if (looksLikeRenderElementsJson(nextRenderJson)) {
151
- return nextRenderJson;
313
+ const collapsedRenderJson = collapseTrailingEmptyParagraphs
314
+ ? collapseTrailingEmptyParagraphRenderElements(nextRenderJson)
315
+ : nextRenderJson;
316
+ return applyResolvedMentionRendering(collapsedRenderJson, mentionPayloadsByDocPos);
152
317
  }
153
318
  console.error('NativeProseViewer: native renderDocumentJson returned an invalid payload.');
154
319
  return '[]';
155
- }, [allowBase64Images, documentSchema, serializedContentJson]);
320
+ }, [
321
+ allowBase64Images,
322
+ collapseTrailingEmptyParagraphs,
323
+ contentHTML,
324
+ documentSchema,
325
+ mentionPayloadsByDocPos,
326
+ serializedContentJson,
327
+ ]);
156
328
  const [contentHeight, setContentHeight] = (0, react_1.useState)(null);
329
+ const allowContentHeightShrinkRef = (0, react_1.useRef)(true);
330
+ (0, react_1.useEffect)(() => {
331
+ allowContentHeightShrinkRef.current = true;
332
+ }, [resolvedContentRevision, renderJson, themeJson]);
157
333
  const handleContentHeightChange = (0, react_1.useCallback)((event) => {
158
334
  const nextHeight = event.nativeEvent.contentHeight;
159
- setContentHeight((currentHeight) => currentHeight === nextHeight ? currentHeight : nextHeight);
335
+ if (nextHeight <= 0)
336
+ return;
337
+ setContentHeight((currentHeight) => currentHeight == null ||
338
+ nextHeight >= currentHeight ||
339
+ allowContentHeightShrinkRef.current
340
+ ? (() => {
341
+ allowContentHeightShrinkRef.current = false;
342
+ return currentHeight === nextHeight
343
+ ? currentHeight
344
+ : nextHeight;
345
+ })()
346
+ : currentHeight);
160
347
  }, []);
161
348
  const handlePressMention = (0, react_1.useCallback)((event) => {
162
349
  if (!onPressMention)
@@ -165,10 +352,22 @@ function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, st
165
352
  const resolvedMention = mentionPayloadsByDocPos.get(docPos);
166
353
  onPressMention({
167
354
  docPos,
168
- label: resolvedMention?.label ?? label,
355
+ label: resolvedMention?.renderedLabel ?? label,
169
356
  attrs: resolvedMention?.attrs ?? {},
170
357
  });
171
358
  }, [mentionPayloadsByDocPos, onPressMention]);
172
- const nativeStyle = (0, react_1.useMemo)(() => [{ minHeight: 1 }, style, contentHeight != null ? { height: contentHeight } : null], [contentHeight, style]);
173
- return ((0, jsx_runtime_1.jsx)(NativeProseViewerView, { style: nativeStyle, renderJson: renderJson, themeJson: themeJson, onContentHeightChange: handleContentHeightChange, onPressMention: typeof onPressMention === 'function' ? handlePressMention : undefined }));
359
+ const handlePressLink = (0, react_1.useCallback)((event) => {
360
+ if (!onPressLink)
361
+ return;
362
+ onPressLink({
363
+ href: event.nativeEvent.href,
364
+ text: event.nativeEvent.text,
365
+ });
366
+ }, [onPressLink]);
367
+ const nativeStyle = (0, react_1.useMemo)(() => [
368
+ { minHeight: 1 },
369
+ style,
370
+ contentHeight != null ? { minHeight: contentHeight } : null,
371
+ ], [contentHeight, style]);
372
+ return ((0, jsx_runtime_1.jsx)(NativeProseViewerView, { style: nativeStyle, renderJson: renderJson, themeJson: themeJson, enableLinkTaps: enableLinkTaps, interceptLinkTaps: typeof onPressLink === 'function', onContentHeightChange: handleContentHeightChange, onPressLink: typeof onPressLink === 'function' ? handlePressLink : undefined, onPressMention: typeof onPressMention === 'function' ? handlePressMention : undefined }));
174
373
  }
@@ -63,6 +63,8 @@ export interface NativeRichTextEditorProps {
63
63
  onRequestLink?: (context: LinkRequestContext) => void;
64
64
  /** Called when a toolbar image item is pressed so the host can choose an image source. */
65
65
  onRequestImage?: (context: ImageRequestContext) => void;
66
+ /** Whether plain URLs typed or pasted into the editor should be converted into link marks automatically. */
67
+ autoDetectLinks?: boolean;
66
68
  /** Whether `data:image/...` sources are accepted for image insertion and HTML parsing. */
67
69
  allowBase64Images?: boolean;
68
70
  /** Whether selected images show native resize handles. */