react-native-enriched-markdown-scaffold 1.0.2-scaffold.1 → 1.0.2-scaffold.10

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 (163) hide show
  1. package/android/build.gradle +2 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +18 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +4 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +18 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +4 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +148 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +30 -0
  9. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +210 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +176 -2
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +45 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +35 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +112 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +34 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +46 -8
  16. package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
  17. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
  18. package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +43 -0
  19. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +25 -3
  20. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/InputConnectionWrapper.kt +7 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +37 -7
  22. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +5 -1
  23. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +13 -99
  24. package/android/src/main/java/com/swmansion/enriched/markdown/spans/InlineChipPainter.kt +140 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkChipSpans.kt +177 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +1 -6
  27. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +16 -1
  28. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +36 -1
  29. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkVariantEntry.kt +7 -3
  30. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +3 -0
  31. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +51 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +34 -2
  33. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +2 -1
  34. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +6 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/HeadlessActionMode.kt +88 -0
  36. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt +100 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +53 -14
  38. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +232 -0
  39. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +64 -0
  40. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +29 -3
  41. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +30 -7
  42. package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +1 -0
  43. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +13 -6
  44. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathMeasureHelper.kt +2 -1
  45. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +25 -9
  46. package/docs/API_REFERENCE.md +183 -4
  47. package/docs/COPY_OPTIONS.md +40 -0
  48. package/docs/INPUT.md +3 -1
  49. package/docs/MENTIONS.md +8 -1
  50. package/docs/STYLES.md +27 -0
  51. package/docs/TEXT.md +2 -0
  52. package/docs/WEB.md +1 -0
  53. package/ios/EnrichedMarkdown.mm +321 -9
  54. package/ios/EnrichedMarkdownText.mm +203 -6
  55. package/ios/attachments/ENRMMathInlineAttachment+macOS.m +2 -1
  56. package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
  57. package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
  58. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +148 -0
  59. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
  60. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +30 -0
  61. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +210 -0
  62. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
  63. package/ios/internals/MeasurementCache.h +8 -2
  64. package/ios/math/ENRMRaTeXBridge.swift +23 -5
  65. package/ios/renderer/ENRMMathInlineRenderer.m +1 -0
  66. package/ios/renderer/LinkRenderer.m +47 -15
  67. package/ios/styles/StyleConfig.h +20 -0
  68. package/ios/styles/StyleConfig.mm +84 -0
  69. package/ios/utils/CodeBackground.m +3 -65
  70. package/ios/utils/ENRMBlockMenu.h +66 -0
  71. package/ios/utils/ENRMChipDrawing.h +24 -0
  72. package/ios/utils/ENRMChipDrawing.m +83 -0
  73. package/ios/utils/ENRMLinkChip.h +70 -0
  74. package/ios/utils/ENRMLinkChip.m +213 -0
  75. package/ios/utils/ENRMLinkLayout.h +50 -0
  76. package/ios/utils/ENRMLinkLayout.m +45 -0
  77. package/ios/utils/ENRMSelectionChange.h +120 -0
  78. package/ios/utils/ENRMSelectionChange.m +282 -0
  79. package/ios/utils/EditMenuUtils+macOS.m +4 -1
  80. package/ios/utils/EditMenuUtils.h +35 -6
  81. package/ios/utils/EditMenuUtils.m +171 -15
  82. package/ios/utils/HTMLGenerator.m +2 -1
  83. package/ios/utils/MarkdownAccessibilityElementBuilder.m +3 -1
  84. package/ios/utils/MarkdownExtractor.m +2 -1
  85. package/ios/utils/RTFExportUtils.m +2 -0
  86. package/ios/utils/StylePropsUtils.h +47 -1
  87. package/ios/utils/TextViewLayoutManager.mm +3 -0
  88. package/ios/views/ENRMCodeBlockContainerView.h +16 -2
  89. package/ios/views/ENRMCodeBlockContainerView.m +22 -19
  90. package/ios/views/ENRMMathContainerView.h +11 -0
  91. package/ios/views/ENRMMathContainerView.m +17 -20
  92. package/ios/views/TableContainerView.h +14 -0
  93. package/ios/views/TableContainerView.m +34 -18
  94. package/lib/module/EnrichedMarkdownNativeComponent.ts +138 -1
  95. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +138 -1
  96. package/lib/module/index.js.map +1 -1
  97. package/lib/module/jest/index.js +14 -0
  98. package/lib/module/jest/index.js.map +1 -1
  99. package/lib/module/native/EnrichedMarkdownText.js +169 -11
  100. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  101. package/lib/module/normalizeMarkdownStyle.js +17 -4
  102. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  103. package/lib/module/normalizeMarkdownStyle.web.js +17 -4
  104. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  105. package/lib/module/web/EnrichedMarkdownText.js +6 -3
  106. package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
  107. package/lib/module/web/renderers/InlineRenderers.js +96 -3
  108. package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
  109. package/lib/module/web/styles.js +18 -0
  110. package/lib/module/web/styles.js.map +1 -1
  111. package/lib/module/web/utils.js +13 -0
  112. package/lib/module/web/utils.js.map +1 -1
  113. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +123 -2
  114. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  115. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +123 -2
  116. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  117. package/lib/typescript/src/index.d.ts +2 -2
  118. package/lib/typescript/src/index.d.ts.map +1 -1
  119. package/lib/typescript/src/index.web.d.ts +1 -1
  120. package/lib/typescript/src/index.web.d.ts.map +1 -1
  121. package/lib/typescript/src/jest/index.d.ts +1 -1
  122. package/lib/typescript/src/jest/index.d.ts.map +1 -1
  123. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  124. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  125. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  126. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  127. package/lib/typescript/src/types/MarkdownStyle.d.ts +50 -5
  128. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  129. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +12 -0
  130. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  131. package/lib/typescript/src/types/MarkdownTextProps.d.ts +144 -11
  132. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  133. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
  134. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
  135. package/lib/typescript/src/types/events.d.ts +66 -0
  136. package/lib/typescript/src/types/events.d.ts.map +1 -1
  137. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
  138. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
  139. package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
  140. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  141. package/lib/typescript/src/web/types.d.ts +4 -0
  142. package/lib/typescript/src/web/types.d.ts.map +1 -1
  143. package/lib/typescript/src/web/utils.d.ts +4 -0
  144. package/lib/typescript/src/web/utils.d.ts.map +1 -1
  145. package/package.json +1 -1
  146. package/src/EnrichedMarkdownNativeComponent.ts +138 -1
  147. package/src/EnrichedMarkdownTextNativeComponent.ts +138 -1
  148. package/src/index.tsx +6 -0
  149. package/src/index.web.tsx +5 -1
  150. package/src/jest/index.tsx +22 -1
  151. package/src/native/EnrichedMarkdownText.tsx +220 -9
  152. package/src/normalizeMarkdownStyle.ts +15 -0
  153. package/src/normalizeMarkdownStyle.web.ts +13 -0
  154. package/src/types/MarkdownStyle.ts +51 -5
  155. package/src/types/MarkdownStyleInternal.ts +12 -0
  156. package/src/types/MarkdownTextProps.ts +151 -11
  157. package/src/types/MarkdownTextProps.web.ts +43 -1
  158. package/src/types/events.ts +69 -0
  159. package/src/web/EnrichedMarkdownText.tsx +10 -3
  160. package/src/web/renderers/InlineRenderers.tsx +111 -3
  161. package/src/web/styles.ts +25 -0
  162. package/src/web/types.ts +4 -0
  163. package/src/web/utils.ts +8 -0
@@ -1,4 +1,11 @@
1
- import type { ColorValue, ViewProps, ViewStyle, TextStyle } from 'react-native';
1
+ import type { ReactNode, RefObject } from 'react';
2
+ import type {
3
+ ColorValue,
4
+ HostInstance,
5
+ ViewProps,
6
+ ViewStyle,
7
+ TextStyle,
8
+ } from 'react-native';
2
9
  import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle';
3
10
  import type { AccessibilityLabels } from './AccessibilityLabels';
4
11
  import type {
@@ -6,8 +13,50 @@ import type {
6
13
  LinkLongPressEvent,
7
14
  TaskListItemPressEvent,
8
15
  CopyPressEvent,
16
+ CopyEvent,
17
+ SelectionChangeEvent,
9
18
  } from './events';
10
19
 
20
+ /** Imperative handle of `EnrichedMarkdownText`, reached through its `ref`. */
21
+ export interface EnrichedMarkdownTextInstance {
22
+ /**
23
+ * Clears the text selection (and any long-pressed block) and fires
24
+ * `onSelectionChange` with `active: false`.
25
+ * @platform ios, android
26
+ */
27
+ clearSelection: () => void;
28
+ measure: HostInstance['measure'];
29
+ measureInWindow: HostInstance['measureInWindow'];
30
+ measureLayout: HostInstance['measureLayout'];
31
+ }
32
+
33
+ /**
34
+ * A rect in dp, relative to the component's content box (inside any
35
+ * `containerStyle` padding).
36
+ */
37
+ export interface LinkRect {
38
+ x: number;
39
+ y: number;
40
+ width: number;
41
+ height: number;
42
+ }
43
+
44
+ /** Layout of one rendered link, passed to `renderLinkDecoration`. */
45
+ export interface LinkDecorationInfo {
46
+ /** Position of the link in the document (0-based) — stable key for a link. */
47
+ index: number;
48
+ url: string;
49
+ /** The link's visible text. */
50
+ text: string;
51
+ /**
52
+ * Anchor rect. Native: the drawn chip (glyphs + padding) of the link's last
53
+ * line fragment. Web: the link's inline box.
54
+ */
55
+ rect: LinkRect;
56
+ /** Every line fragment of the link (several when it wraps). Web: `[rect]`. */
57
+ rects: LinkRect[];
58
+ }
59
+
11
60
  /**
12
61
  * Public context menu item. Each item includes a JS-side `onPress` callback
13
62
  * that is called when the user taps the item in the selection context menu.
@@ -57,22 +106,31 @@ export interface SelectionMenuPluralLabels {
57
106
  * Controls the built-in items added to the native text selection menu and
58
107
  * lets you localize their labels.
59
108
  *
60
- * Each item accepts an object: `{ enabled }` toggles visibility (the system
61
- * `copy` item can't be hidden — only relabeled) and `label` overrides the
62
- * English default. Wire `label` to your i18n library to match the rest of your
63
- * app's UI. Labels apply to the main text selection menu as well as the table
64
- * and math block copy menus.
109
+ * Each item accepts an object: `{ enabled }` toggles visibility and `label`
110
+ * overrides the English default. Wire `label` to your i18n library to match
111
+ * the rest of your app's UI. Items and labels apply to the main text selection
112
+ * menu as well as the table, math and code block copy menus.
113
+ *
114
+ * `copy.enabled`, `copy.format` and `systemItems` are ignored on macOS.
65
115
  *
66
116
  * @platform ios, android, macos
67
117
  */
68
118
  export interface SelectionMenuConfig {
69
119
  /**
70
- * The system "Copy" item. It can't be hidden — only its label is configurable.
71
- * @default { label: "Copy" }
120
+ * The "Copy" item.
121
+ * - `enabled`: `false` removes it from the text selection menu, the table /
122
+ * math / code block menus and the code block VoiceOver copy action.
123
+ * - `format`: `'rich'` copies rich text (plain + HTML, plus RTF on iOS) from
124
+ * text selections and the raw content from blocks; `'markdown'` copies
125
+ * Markdown as plain text everywhere the item appears (inline math becomes
126
+ * `$…$`, math blocks `$$…$$`, code blocks are fenced). The code block header
127
+ * button and VoiceOver action always copy the raw code.
128
+ * @default { enabled: true, label: "Copy", format: "rich" }
72
129
  */
73
- copy?: { label?: string };
130
+ copy?: { enabled?: boolean; label?: string; format?: 'rich' | 'markdown' };
74
131
  /**
75
- * The built-in "Copy as Markdown" action for text selections.
132
+ * The built-in "Copy as Markdown" action, in the text selection menu and in
133
+ * the table / math / code block menus.
76
134
  * @default { enabled: true, label: "Copy as Markdown" }
77
135
  */
78
136
  copyAsMarkdown?: { enabled?: boolean; label?: string };
@@ -87,6 +145,23 @@ export interface SelectionMenuConfig {
87
145
  label?: string;
88
146
  pluralLabels?: SelectionMenuPluralLabels;
89
147
  };
148
+ /**
149
+ * Which system items the text selection menu keeps.
150
+ * - `'default'`: keep everything the OS adds (Select All, Look Up, Share,
151
+ * Translate, app text actions…), with Copy replaced by the built-in one.
152
+ * - `'none'`: drop every system item; the menu only holds the built-in items
153
+ * above and `contextMenuItems`.
154
+ * @default 'default'
155
+ * @platform ios, android
156
+ */
157
+ systemItems?: 'default' | 'none';
158
+ /**
159
+ * The copy button in the code block header. `false` hides the button; the
160
+ * header and language label stay.
161
+ * @default { enabled: true }
162
+ * @platform ios, android, macos
163
+ */
164
+ codeBlockCopyButton?: { enabled?: boolean };
90
165
  }
91
166
 
92
167
  export interface StreamingConfig {
@@ -114,6 +189,11 @@ export interface StreamingConfig {
114
189
  }
115
190
 
116
191
  export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
192
+ /**
193
+ * Imperative handle: `clearSelection()` plus the host `measure*` methods.
194
+ * @platform ios, android
195
+ */
196
+ ref?: RefObject<EnrichedMarkdownTextInstance | null>;
117
197
  /**
118
198
  * Markdown content to render.
119
199
  * @platform ios, android, web
@@ -151,6 +231,21 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
151
231
  * @platform ios, android, web
152
232
  */
153
233
  onLinkLongPress?: (event: LinkLongPressEvent) => void;
234
+ /**
235
+ * Renders a decoration (e.g. a badge or icon) over each link. The returned
236
+ * node is placed in an absolutely positioned box matching `info.rect` (the
237
+ * link's last line fragment, including chip padding) with
238
+ * `pointerEvents="box-none"`; position content relative to it (negative
239
+ * offsets overflow the box). Return `null` to leave a link undecorated.
240
+ *
241
+ * Native caveats: rects arrive from native after layout, so decorations
242
+ * appear one frame after the text; links inside tables (`flavor="github"`)
243
+ * are not reported; content overflowing the component bounds does not
244
+ * receive touches on Android. Toggling this prop between set and unset
245
+ * remounts the native view.
246
+ * @platform ios, android, web
247
+ */
248
+ renderLinkDecoration?: (info: LinkDecorationInfo) => ReactNode | null;
154
249
  /**
155
250
  * Callback fired when a task list checkbox is tapped.
156
251
  *
@@ -179,13 +274,44 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
179
274
  * Callback fired when code is copied from a fenced code block, via the header
180
275
  * copy button, the long-press context-menu "Copy" action, or the VoiceOver
181
276
  * copy action. Receives the copied code and its language. Does not fire for
182
- * "Copy as Markdown".
277
+ * "Copy as Markdown", nor for the context-menu "Copy" when
278
+ * `selectionMenuConfig.copy.format` is `'markdown'`.
183
279
  *
184
280
  * Only fires when `flavor="github"` (the copy button is part of the GitHub
185
281
  * flavor's container-based code block renderer).
186
282
  * @platform ios, android, macos
187
283
  */
188
284
  onCopyPress?: (event: CopyPressEvent) => void;
285
+ /**
286
+ * Takes over the "Copy" item of the text selection menu and of the table /
287
+ * math / code block menus. When set, native does **not** write the
288
+ * clipboard: it calls this with the copied content and the handler writes
289
+ * whatever it wants (e.g. with `expo-clipboard`). Overrides
290
+ * `selectionMenuConfig.copy.format`; `copy.enabled: false` still hides the
291
+ * item. "Copy as Markdown", the code block header button and the VoiceOver
292
+ * copy action are unaffected, and `onCopyPress` does not fire for an
293
+ * intercepted Copy.
294
+ * @platform ios, android
295
+ */
296
+ onCopy?: (event: CopyEvent) => void;
297
+ /**
298
+ * Reports the text selection so the app can draw its own menu (pair it with
299
+ * `contextMenuHidden`). Fires at most once per frame while the selection
300
+ * changes, and with `active: false` once it is gone. With
301
+ * `contextMenuHidden`, a long press on a table / math / code block fires it
302
+ * too (`source` is the block kind, `whole: true`), since those blocks have no
303
+ * text selection of their own. Only one part of a component holds a
304
+ * selection at a time; clearing the selection of *other* components is up to
305
+ * the app (`ref.current.clearSelection()`).
306
+ *
307
+ * Rects are relative to the view that `ref.current.measureInWindow`
308
+ * measures — add that origin to position UI on screen. On iOS, `adjusting`
309
+ * is `true` while a selection handle is dragged: hide the menu until it is
310
+ * `false` again. Android reports every step of the drag instead (with
311
+ * `adjusting: false`), so debounce before showing a menu there.
312
+ * @platform ios, android
313
+ */
314
+ onSelectionChange?: (event: SelectionChangeEvent) => void;
189
315
  /**
190
316
  * Controls whether the system link preview is shown on long press (iOS only).
191
317
  *
@@ -199,6 +325,16 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
199
325
  * @platform ios
200
326
  */
201
327
  enableLinkPreview?: boolean;
328
+ /**
329
+ * When `true`, long-pressing a link selects its label and shows the
330
+ * selection edit menu, like a manual text selection, instead of the system
331
+ * link preview. Takes precedence over `enableLinkPreview` and
332
+ * `onLinkLongPress`.
333
+ *
334
+ * @default false
335
+ * @platform ios
336
+ */
337
+ linkLongPressSelects?: boolean;
202
338
  /**
203
339
  * Controls text selection.
204
340
  * - iOS: Controls text selection and link previews on long press.
@@ -315,6 +451,10 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
315
451
  *
316
452
  * It does not suppress the system "Save to Camera Roll / Copy" sheet that
317
453
  * UIKit shows when long-pressing an image.
454
+ *
455
+ * On Android no selection toolbar is created at all, so ROMs that draw
456
+ * their own (MIUI) stay hidden too. A selection made while this is on
457
+ * keeps no menu if it is turned off mid-selection; the next one gets it.
318
458
  * @default false
319
459
  * @platform ios, android, macos
320
460
  */
@@ -1,4 +1,4 @@
1
- import type { CSSProperties, HTMLAttributes } from 'react';
1
+ import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
2
2
  import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle';
3
3
  import type {
4
4
  LinkPressEvent,
@@ -6,6 +6,33 @@ import type {
6
6
  TaskListItemPressEvent,
7
7
  } from './events';
8
8
 
9
+ /**
10
+ * A rect in dp, relative to the component's content box (inside any
11
+ * `containerStyle` padding).
12
+ */
13
+ export interface LinkRect {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ }
19
+
20
+ /** Layout of one rendered link, passed to `renderLinkDecoration`. */
21
+ export interface LinkDecorationInfo {
22
+ /** Position of the link in the document (0-based) — stable key for a link. */
23
+ index: number;
24
+ url: string;
25
+ /** The link's visible text. */
26
+ text: string;
27
+ /**
28
+ * Anchor rect. Native: the drawn chip (glyphs + padding) of the link's last
29
+ * line fragment. Web: the link's inline box.
30
+ */
31
+ rect: LinkRect;
32
+ /** Every line fragment of the link (several when it wraps). Web: `[rect]`. */
33
+ rects: LinkRect[];
34
+ }
35
+
9
36
  export interface EnrichedMarkdownTextProps extends Omit<
10
37
  HTMLAttributes<HTMLDivElement>,
11
38
  'style' | 'dir'
@@ -47,6 +74,21 @@ export interface EnrichedMarkdownTextProps extends Omit<
47
74
  * @platform ios, android, web
48
75
  */
49
76
  onLinkLongPress?: (event: LinkLongPressEvent) => void;
77
+ /**
78
+ * Renders a decoration (e.g. a badge or icon) over each link. The returned
79
+ * node is placed in an absolutely positioned box matching `info.rect` (the
80
+ * link's last line fragment, including chip padding) with
81
+ * `pointerEvents="box-none"`; position content relative to it (negative
82
+ * offsets overflow the box). Return `null` to leave a link undecorated.
83
+ *
84
+ * Native caveats: rects arrive from native after layout, so decorations
85
+ * appear one frame after the text; links inside tables (`flavor="github"`)
86
+ * are not reported; content overflowing the component bounds does not
87
+ * receive touches on Android. Toggling this prop between set and unset
88
+ * remounts the native view.
89
+ * @platform ios, android, web
90
+ */
91
+ renderLinkDecoration?: (info: LinkDecorationInfo) => ReactNode | null;
50
92
  /**
51
93
  * Callback fired when a task list checkbox is tapped.
52
94
  *
@@ -17,6 +17,75 @@ export interface CopyPressEvent {
17
17
  language: string;
18
18
  }
19
19
 
20
+ /**
21
+ * Payload of `onCopy`: what the Copy item would have copied. With `onCopy`
22
+ * set, native writes nothing to the clipboard — the handler owns the write.
23
+ */
24
+ export interface CopyEvent {
25
+ /** Which menu the Copy item belongs to. */
26
+ source: 'selection' | 'math' | 'table' | 'code';
27
+ /**
28
+ * Markdown of the copied content — what `copy.format: 'markdown'` writes.
29
+ * Text selections: the original source when `whole`, otherwise rebuilt from
30
+ * the rendered styles (falls back to `text`). Blocks: `$$…$$`, the table as
31
+ * Markdown, or the fenced code block.
32
+ */
33
+ markdown: string;
34
+ /** Rendered text of the selection, or the block's raw content. */
35
+ text: string;
36
+ /**
37
+ * `true` when the selection covers the whole component, so `markdown` is the
38
+ * original source. Always `true` for blocks.
39
+ */
40
+ whole: boolean;
41
+ }
42
+
43
+ /**
44
+ * A rect in dp, relative to the component's native view: add the origin from
45
+ * the component ref's `measureInWindow` to get window coordinates.
46
+ */
47
+ export interface SelectionRect {
48
+ x: number;
49
+ y: number;
50
+ width: number;
51
+ height: number;
52
+ }
53
+
54
+ /**
55
+ * Payload of `onSelectionChange`: the current text selection, or a table /
56
+ * math / code block long-pressed while `contextMenuHidden` is on. Content
57
+ * fields carry what `onCopy` would receive for the same selection.
58
+ */
59
+ export interface SelectionChangeEvent {
60
+ /** `false` once the selection is gone; every other field is then empty / zero. */
61
+ active: boolean;
62
+ /** `'selection'` for text; the block kind for a long-pressed block. */
63
+ source: CopyEvent['source'];
64
+ /** Rendered text of the selection, or the block's raw content. */
65
+ text: string;
66
+ /** Same as `CopyEvent.markdown` for this selection. */
67
+ markdown: string;
68
+ /** Same as `CopyEvent.whole`. Always `true` for blocks. */
69
+ whole: boolean;
70
+ /**
71
+ * Bounding box of the selection (full content width when it spans several
72
+ * lines); the block's frame for blocks.
73
+ */
74
+ rect: SelectionRect;
75
+ /** Selected part of the first line (block: same as `rect`). */
76
+ startRect: SelectionRect;
77
+ /** Selected part of the last line (block: same as `rect`). */
78
+ endRect: SelectionRect;
79
+ /**
80
+ * `true` while a selection handle is dragged (or the loupe is held), with the
81
+ * selection as it was when the drag began; `false` again, with the new
82
+ * selection, once the finger lifts. Hide your menu while it is `true`.
83
+ * Always `false` on Android, which reports the selection continuously
84
+ * during the drag.
85
+ */
86
+ adjusting: boolean;
87
+ }
88
+
20
89
  /**
21
90
  * Native-level context menu item config sent to the native component.
22
91
  * Does not include the `onPress` callback — callbacks are managed on the JS side.
@@ -9,7 +9,7 @@ import {
9
9
  import { parseMarkdown } from './parseMarkdown';
10
10
  import { RenderNode } from './renderers';
11
11
  import type { ASTNode, RendererCallbacks, RenderCapabilities } from './types';
12
- import { indexTaskItems, markInlineImages } from './utils';
12
+ import { indexLinks, indexTaskItems, markInlineImages } from './utils';
13
13
  import { loadKaTeX } from './katex';
14
14
  import type { KaTeXInstance } from './katex';
15
15
  import { ENRM_TEXT_CLASS, ENRM_SELECTION_BG_VAR } from './globalStyles';
@@ -27,6 +27,7 @@ export const EnrichedMarkdownText = ({
27
27
  selectable = true,
28
28
  dir,
29
29
  selectionColor,
30
+ renderLinkDecoration,
30
31
  ...rest
31
32
  }: EnrichedMarkdownTextProps) => {
32
33
  const normalizedStyle = useMemo(
@@ -66,6 +67,7 @@ export const EnrichedMarkdownText = ({
66
67
  .then(([result, katexInstance]) => {
67
68
  if (!cancelled) {
68
69
  indexTaskItems(result);
70
+ indexLinks(result);
69
71
  markInlineImages(result);
70
72
 
71
73
  setParseError(false);
@@ -99,8 +101,13 @@ export const EnrichedMarkdownText = ({
99
101
  ]);
100
102
 
101
103
  const callbacks = useMemo<RendererCallbacks>(
102
- () => ({ onLinkPress, onLinkLongPress, onTaskListItemPress }),
103
- [onLinkPress, onLinkLongPress, onTaskListItemPress]
104
+ () => ({
105
+ onLinkPress,
106
+ onLinkLongPress,
107
+ onTaskListItemPress,
108
+ renderLinkDecoration,
109
+ }),
110
+ [onLinkPress, onLinkLongPress, onTaskListItemPress, renderLinkDecoration]
104
111
  );
105
112
 
106
113
  const capabilities = useMemo<RenderCapabilities>(
@@ -1,8 +1,16 @@
1
- import type { MouseEvent } from 'react';
1
+ import {
2
+ useLayoutEffect,
3
+ useRef,
4
+ useState,
5
+ type MouseEvent,
6
+ type ReactNode,
7
+ } from 'react';
8
+ import type { LinkRect } from '../../types/MarkdownTextProps.web';
2
9
  import type { RendererProps, RendererMap } from '../types';
3
10
  import { extractNodeText } from '../utils';
4
11
  import { KaTeXRenderer } from './KaTeXRenderer';
5
12
  import { linkStyleForUrl } from '../styles';
13
+ import type { CSSProperties } from 'react';
6
14
 
7
15
  function TextRenderer({ node }: RendererProps) {
8
16
  return <>{node.content ?? ''}</>;
@@ -78,16 +86,116 @@ function LinkRenderer({
78
86
  }
79
87
  };
80
88
 
89
+ const linkStyle = linkStyleForUrl(style, url);
90
+ const children = renderChildren(node);
91
+
92
+ if (callbacks.renderLinkDecoration) {
93
+ return (
94
+ <DecoratedLink
95
+ url={url}
96
+ index={node.attributes?.linkIndex ?? 0}
97
+ text={extractNodeText(node)}
98
+ linkStyle={linkStyle}
99
+ renderLinkDecoration={callbacks.renderLinkDecoration}
100
+ onClick={handleClick}
101
+ onContextMenu={handleContextMenu}
102
+ >
103
+ {children}
104
+ </DecoratedLink>
105
+ );
106
+ }
107
+
81
108
  return (
82
109
  <a
83
110
  href={url}
84
- style={linkStyleForUrl(style, url)}
111
+ style={linkStyle}
85
112
  target="_blank"
86
113
  rel="noopener noreferrer"
87
114
  onClick={handleClick}
88
115
  onContextMenu={handleContextMenu}
89
116
  >
90
- {renderChildren(node)}
117
+ {children}
118
+ </a>
119
+ );
120
+ }
121
+
122
+ const EMPTY_RECT: LinkRect = { x: 0, y: 0, width: 0, height: 0 };
123
+
124
+ const decorationLayerStyle: CSSProperties = {
125
+ position: 'absolute',
126
+ inset: 0,
127
+ pointerEvents: 'none',
128
+ };
129
+
130
+ const decorationContentStyle: CSSProperties = { pointerEvents: 'auto' };
131
+
132
+ /**
133
+ * A link with a `renderLinkDecoration` overlay. Unlike native, the overlay
134
+ * anchors to the link's whole inline box (not the last line fragment of a
135
+ * wrapping link), and `rect` is that box relative to itself.
136
+ */
137
+ function DecoratedLink({
138
+ url,
139
+ index,
140
+ text,
141
+ linkStyle,
142
+ renderLinkDecoration,
143
+ onClick,
144
+ onContextMenu,
145
+ children,
146
+ }: {
147
+ url: string;
148
+ index: number;
149
+ text: string;
150
+ linkStyle: ReturnType<typeof linkStyleForUrl>;
151
+ renderLinkDecoration: NonNullable<
152
+ RendererProps['callbacks']['renderLinkDecoration']
153
+ >;
154
+ onClick: (event: MouseEvent) => void;
155
+ onContextMenu: (event: MouseEvent) => void;
156
+ children: ReactNode;
157
+ }) {
158
+ const anchorRef = useRef<HTMLAnchorElement>(null);
159
+ const [rect, setRect] = useState<LinkRect>(EMPTY_RECT);
160
+
161
+ // The overlay itself is sized by CSS (inset: 0); `rect` is informational, so
162
+ // re-measuring when the content or chip style changes is enough.
163
+ useLayoutEffect(() => {
164
+ const anchor = anchorRef.current;
165
+ if (!anchor) return;
166
+ const { width, height } = anchor.getBoundingClientRect();
167
+ setRect((prev) =>
168
+ prev.width === width && prev.height === height
169
+ ? prev
170
+ : { x: 0, y: 0, width, height }
171
+ );
172
+ }, [text, linkStyle]);
173
+
174
+ const decoration = renderLinkDecoration({
175
+ index,
176
+ url,
177
+ text,
178
+ rect,
179
+ rects: [rect],
180
+ });
181
+
182
+ return (
183
+ <a
184
+ ref={anchorRef}
185
+ href={url}
186
+ style={{ ...linkStyle, position: 'relative' }} // eslint-disable-line react-native/no-inline-styles
187
+ target="_blank"
188
+ rel="noopener noreferrer"
189
+ onClick={onClick}
190
+ onContextMenu={onContextMenu}
191
+ >
192
+ {children}
193
+ {decoration != null && (
194
+ <span aria-hidden style={decorationLayerStyle}>
195
+ {/* Mirrors native pointerEvents="box-none": only the decoration itself is hit-testable. */}
196
+ <span style={decorationContentStyle}>{decoration}</span>
197
+ </span>
198
+ )}
91
199
  </a>
92
200
  );
93
201
  }
package/src/web/styles.ts CHANGED
@@ -323,6 +323,12 @@ export function linkStyleForUrl(
323
323
  variantIndex !== -1 ? style.linkVariants[variantIndex]! : base;
324
324
 
325
325
  const backgroundColor = resolved.backgroundColor;
326
+ const hasBorder =
327
+ resolved.borderWidth > 0 &&
328
+ !!resolved.borderColor &&
329
+ resolved.borderColor !== 'transparent';
330
+ const hasPadding =
331
+ resolved.paddingHorizontal > 0 || resolved.paddingVertical > 0;
326
332
  return {
327
333
  color: resolved.color,
328
334
  fontFamily: normalizeFontFamily(base.fontFamily),
@@ -330,6 +336,25 @@ export function linkStyleForUrl(
330
336
  ...(backgroundColor && backgroundColor !== 'transparent'
331
337
  ? { backgroundColor }
332
338
  : undefined),
339
+ // Chip: inline padding reserves horizontal space like native; vertical
340
+ // padding only paints (inline boxes don't grow the line box). `clone`
341
+ // rounds and pads each fragment of a wrapping link.
342
+ ...(hasPadding
343
+ ? {
344
+ padding: `${resolved.paddingVertical}px ${resolved.paddingHorizontal}px`,
345
+ }
346
+ : undefined),
347
+ ...(resolved.borderRadius > 0
348
+ ? { borderRadius: resolved.borderRadius }
349
+ : undefined),
350
+ ...(hasBorder
351
+ ? {
352
+ border: `${resolved.borderWidth}px solid ${resolved.borderColor}`,
353
+ }
354
+ : undefined),
355
+ ...(hasPadding || hasBorder || resolved.borderRadius > 0
356
+ ? { boxDecorationBreak: 'clone', WebkitBoxDecorationBreak: 'clone' }
357
+ : undefined),
333
358
  };
334
359
  }
335
360
 
package/src/web/types.ts CHANGED
@@ -6,6 +6,7 @@ import type {
6
6
  LinkLongPressEvent,
7
7
  TaskListItemPressEvent,
8
8
  } from '../types/events';
9
+ import type { LinkDecorationInfo } from '../types/MarkdownTextProps.web';
9
10
  import type { KaTeXInstance } from './katex';
10
11
 
11
12
  export type NodeType =
@@ -52,6 +53,8 @@ export interface NodeAttributes {
52
53
  taskIndex?: number;
53
54
  /** Stamped by markInlineImages() — not present in the raw WASM output. */
54
55
  isInline?: boolean;
56
+ /** Stamped by indexLinks() — document-order link index, not in the raw WASM output. */
57
+ linkIndex?: number;
55
58
  colCount?: string;
56
59
  headRowCount?: string;
57
60
  bodyRowCount?: string;
@@ -72,6 +75,7 @@ export interface RendererCallbacks {
72
75
  onLinkPress?: (event: LinkPressEvent) => void;
73
76
  onLinkLongPress?: (event: LinkLongPressEvent) => void;
74
77
  onTaskListItemPress?: (event: TaskListItemPressEvent) => void;
78
+ renderLinkDecoration?: (info: LinkDecorationInfo) => ReactNode | null;
75
79
  }
76
80
 
77
81
  export interface RenderCapabilities {
package/src/web/utils.ts CHANGED
@@ -30,6 +30,14 @@ export function indexTaskItems(node: ASTNode, counter = { value: 0 }): void {
30
30
  node.children?.forEach((child) => indexTaskItems(child, counter));
31
31
  }
32
32
 
33
+ /** Stamps Link nodes with their document-order index (the `renderLinkDecoration` key). */
34
+ export function indexLinks(node: ASTNode, counter = { value: 0 }): void {
35
+ if (node.type === 'Link') {
36
+ node.attributes = { ...node.attributes, linkIndex: counter.value++ };
37
+ }
38
+ node.children?.forEach((child) => indexLinks(child, counter));
39
+ }
40
+
33
41
  /**
34
42
  * Stamps Image nodes with `isInline` when they appear inside a paragraph
35
43
  * that also contains other content (text, links, etc.).