react-native-enriched-markdown-scaffold 1.0.2-scaffold.3 → 1.0.2-scaffold.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.
Files changed (140) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +15 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +3 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +15 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +3 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +122 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +100 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +20 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +124 -0
  9. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +145 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +19 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +22 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +85 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +22 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +42 -0
  16. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +37 -7
  17. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +5 -1
  18. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +13 -99
  19. package/android/src/main/java/com/swmansion/enriched/markdown/spans/InlineChipPainter.kt +140 -0
  20. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkChipSpans.kt +177 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +1 -6
  22. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +36 -1
  23. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkVariantEntry.kt +7 -3
  24. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +28 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +2 -1
  26. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +6 -0
  27. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt +100 -0
  28. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +4 -2
  29. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +230 -0
  30. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +11 -1
  31. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +22 -1
  32. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +8 -1
  33. package/docs/API_REFERENCE.md +111 -0
  34. package/docs/MENTIONS.md +8 -1
  35. package/docs/STYLES.md +27 -0
  36. package/docs/TEXT.md +2 -0
  37. package/ios/EnrichedMarkdown.mm +221 -1
  38. package/ios/EnrichedMarkdownText.mm +142 -1
  39. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +122 -0
  40. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +100 -0
  41. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +20 -0
  42. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +124 -0
  43. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
  44. package/ios/internals/MeasurementCache.h +5 -0
  45. package/ios/renderer/LinkRenderer.m +47 -15
  46. package/ios/styles/StyleConfig.h +16 -0
  47. package/ios/styles/StyleConfig.mm +60 -0
  48. package/ios/utils/CodeBackground.m +3 -65
  49. package/ios/utils/ENRMChipDrawing.h +24 -0
  50. package/ios/utils/ENRMChipDrawing.m +83 -0
  51. package/ios/utils/ENRMLinkChip.h +70 -0
  52. package/ios/utils/ENRMLinkChip.m +194 -0
  53. package/ios/utils/ENRMLinkLayout.h +50 -0
  54. package/ios/utils/ENRMLinkLayout.m +45 -0
  55. package/ios/utils/ENRMSelectionChange.h +96 -0
  56. package/ios/utils/ENRMSelectionChange.m +204 -0
  57. package/ios/utils/EditMenuUtils+macOS.m +4 -1
  58. package/ios/utils/EditMenuUtils.m +4 -1
  59. package/ios/utils/HTMLGenerator.m +2 -1
  60. package/ios/utils/MarkdownAccessibilityElementBuilder.m +3 -1
  61. package/ios/utils/MarkdownExtractor.m +2 -1
  62. package/ios/utils/RTFExportUtils.m +2 -0
  63. package/ios/utils/StylePropsUtils.h +37 -1
  64. package/ios/utils/TextViewLayoutManager.mm +3 -0
  65. package/ios/views/ENRMCodeBlockContainerView.h +3 -0
  66. package/ios/views/ENRMCodeBlockContainerView.m +3 -0
  67. package/ios/views/ENRMMathContainerView.h +3 -0
  68. package/ios/views/ENRMMathContainerView.m +3 -0
  69. package/ios/views/TableContainerView.h +6 -0
  70. package/ios/views/TableContainerView.m +24 -0
  71. package/lib/module/EnrichedMarkdownNativeComponent.ts +101 -1
  72. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +101 -1
  73. package/lib/module/index.js.map +1 -1
  74. package/lib/module/jest/index.js +13 -0
  75. package/lib/module/jest/index.js.map +1 -1
  76. package/lib/module/native/EnrichedMarkdownText.js +138 -9
  77. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  78. package/lib/module/normalizeMarkdownStyle.js +13 -2
  79. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  80. package/lib/module/normalizeMarkdownStyle.web.js +13 -2
  81. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  82. package/lib/module/web/EnrichedMarkdownText.js +6 -3
  83. package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
  84. package/lib/module/web/renderers/InlineRenderers.js +96 -3
  85. package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
  86. package/lib/module/web/styles.js +18 -0
  87. package/lib/module/web/styles.js.map +1 -1
  88. package/lib/module/web/utils.js +13 -0
  89. package/lib/module/web/utils.js.map +1 -1
  90. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +93 -2
  91. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  92. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +93 -2
  93. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  94. package/lib/typescript/src/index.d.ts +2 -2
  95. package/lib/typescript/src/index.d.ts.map +1 -1
  96. package/lib/typescript/src/index.web.d.ts +1 -1
  97. package/lib/typescript/src/index.web.d.ts.map +1 -1
  98. package/lib/typescript/src/jest/index.d.ts +1 -1
  99. package/lib/typescript/src/jest/index.d.ts.map +1 -1
  100. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  101. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  102. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  103. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  104. package/lib/typescript/src/types/MarkdownStyle.d.ts +28 -5
  105. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  106. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +10 -0
  107. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  108. package/lib/typescript/src/types/MarkdownTextProps.d.ts +76 -2
  109. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  110. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
  111. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
  112. package/lib/typescript/src/types/events.d.ts +36 -0
  113. package/lib/typescript/src/types/events.d.ts.map +1 -1
  114. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
  115. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
  116. package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
  117. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  118. package/lib/typescript/src/web/types.d.ts +4 -0
  119. package/lib/typescript/src/web/types.d.ts.map +1 -1
  120. package/lib/typescript/src/web/utils.d.ts +4 -0
  121. package/lib/typescript/src/web/utils.d.ts.map +1 -1
  122. package/package.json +1 -1
  123. package/src/EnrichedMarkdownNativeComponent.ts +101 -1
  124. package/src/EnrichedMarkdownTextNativeComponent.ts +101 -1
  125. package/src/index.tsx +5 -0
  126. package/src/index.web.tsx +5 -1
  127. package/src/jest/index.tsx +21 -1
  128. package/src/native/EnrichedMarkdownText.tsx +181 -8
  129. package/src/normalizeMarkdownStyle.ts +13 -0
  130. package/src/normalizeMarkdownStyle.web.ts +11 -0
  131. package/src/types/MarkdownStyle.ts +29 -5
  132. package/src/types/MarkdownStyleInternal.ts +10 -0
  133. package/src/types/MarkdownTextProps.ts +85 -1
  134. package/src/types/MarkdownTextProps.web.ts +43 -1
  135. package/src/types/events.ts +38 -0
  136. package/src/web/EnrichedMarkdownText.tsx +10 -3
  137. package/src/web/renderers/InlineRenderers.tsx +111 -3
  138. package/src/web/styles.ts +25 -0
  139. package/src/web/types.ts +4 -0
  140. package/src/web/utils.ts +8 -0
package/src/index.web.tsx CHANGED
@@ -1,5 +1,9 @@
1
1
  export { EnrichedMarkdownText, default } from './web/EnrichedMarkdownText';
2
- export type { EnrichedMarkdownTextProps } from './types/MarkdownTextProps.web';
2
+ export type {
3
+ EnrichedMarkdownTextProps,
4
+ LinkDecorationInfo,
5
+ LinkRect,
6
+ } from './types/MarkdownTextProps.web';
3
7
  export type { MarkdownStyle, Md4cFlags } from './types/MarkdownStyle';
4
8
  export type {
5
9
  LinkPressEvent,
@@ -26,7 +26,10 @@ import type {
26
26
  EnrichedMarkdownTextInputProps,
27
27
  InsertMathOptions,
28
28
  } from '../EnrichedMarkdownTextInput';
29
- import type { EnrichedMarkdownTextProps } from '../native/EnrichedMarkdownText';
29
+ import type {
30
+ EnrichedMarkdownTextInstance,
31
+ EnrichedMarkdownTextProps,
32
+ } from '../native/EnrichedMarkdownText';
30
33
 
31
34
  export {
32
35
  DEFAULT_ACCESSIBILITY_LABELS,
@@ -165,6 +168,7 @@ export const EnrichedMarkdownTextInput = ({
165
168
  };
166
169
 
167
170
  export const EnrichedMarkdownText = ({
171
+ ref,
168
172
  markdown,
169
173
  containerStyle,
170
174
  testID,
@@ -178,6 +182,7 @@ export const EnrichedMarkdownText = ({
178
182
  onLinkPress: _onLinkPress,
179
183
  onLinkLongPress: _onLinkLongPress,
180
184
  onTaskListItemPress: _onTaskListItemPress,
185
+ onSelectionChange: _onSelectionChange,
181
186
  enableLinkPreview: _enableLinkPreview,
182
187
  selectable: _selectable,
183
188
  md4cFlags: _md4cFlags,
@@ -198,6 +203,21 @@ export const EnrichedMarkdownText = ({
198
203
  lineBreakStrategyIOS: _lineBreakStrategyIOS,
199
204
  writingDirection: _writingDirection,
200
205
  }: EnrichedMarkdownTextProps) => {
206
+ const instanceRef = useRef<EnrichedMarkdownTextInstance | null>(null);
207
+ if (instanceRef.current === null) {
208
+ instanceRef.current = {
209
+ // No native selection under Jest; the mock records the call.
210
+ clearSelection: spy(() => {}),
211
+ measure: spy((callback) => callback(0, 0, 0, 0, 0, 0)),
212
+ measureInWindow: spy((callback) => callback(0, 0, 0, 0)),
213
+ measureLayout: spy((_relativeToNativeNode, onSuccess) =>
214
+ onSuccess(0, 0, 0, 0)
215
+ ),
216
+ };
217
+ }
218
+
219
+ useImperativeHandle(ref, () => instanceRef.current!);
220
+
201
221
  return (
202
222
  <Text
203
223
  style={containerStyle}
@@ -1,21 +1,44 @@
1
- import { useMemo, useCallback, useRef, useEffect } from 'react';
2
- import EnrichedMarkdownTextNativeComponent from '../EnrichedMarkdownTextNativeComponent';
3
- import type { MarkdownStyleInternal } from '../EnrichedMarkdownTextNativeComponent';
4
- import EnrichedMarkdownNativeComponent from '../EnrichedMarkdownNativeComponent';
1
+ import {
2
+ useMemo,
3
+ useCallback,
4
+ useRef,
5
+ useEffect,
6
+ useState,
7
+ useImperativeHandle,
8
+ } from 'react';
9
+ import EnrichedMarkdownTextNativeComponent, {
10
+ Commands as TextCommands,
11
+ } from '../EnrichedMarkdownTextNativeComponent';
12
+ import type {
13
+ LinkLayoutEvent,
14
+ MarkdownStyleInternal,
15
+ SelectionChangeEvent as NativeSelectionChangeEvent,
16
+ } from '../EnrichedMarkdownTextNativeComponent';
17
+ import EnrichedMarkdownNativeComponent, {
18
+ Commands as MarkdownCommands,
19
+ } from '../EnrichedMarkdownNativeComponent';
5
20
  import { normalizeMarkdownStyle } from '../normalizeMarkdownStyle';
6
21
  import { resolveAccessibilityLabels } from '../accessibilityLabelDefaults';
7
22
  import {
8
23
  normalizeMenuItem,
9
24
  normalizeLegacyBooleanMenuItem,
10
25
  } from '../normalizeMenuItem';
11
- import type { NativeSyntheticEvent } from 'react-native';
26
+ import { StyleSheet, View } from 'react-native';
27
+ import type {
28
+ HostInstance,
29
+ LayoutChangeEvent,
30
+ NativeSyntheticEvent,
31
+ } from 'react-native';
12
32
  import type { MarkdownStyle, Md4cFlags } from '../types/MarkdownStyle';
13
33
  import type {
14
34
  EnrichedMarkdownTextProps,
35
+ EnrichedMarkdownTextInstance,
15
36
  StreamingConfig,
16
37
  ContextMenuItem,
17
38
  SelectionMenuConfig,
18
39
  SelectionMenuPluralLabels,
40
+ LinkDecorationInfo,
41
+ LinkRect,
19
42
  } from '../types/MarkdownTextProps';
20
43
  import type {
21
44
  LinkPressEvent,
@@ -24,15 +47,20 @@ import type {
24
47
  CopyPressEvent,
25
48
  CopyEvent,
26
49
  OnContextMenuItemPressEvent,
50
+ SelectionChangeEvent,
51
+ SelectionRect,
27
52
  } from '../types/events';
28
53
 
29
54
  export type { MarkdownStyle, Md4cFlags };
30
55
  export type {
31
56
  EnrichedMarkdownTextProps,
57
+ EnrichedMarkdownTextInstance,
32
58
  StreamingConfig,
33
59
  ContextMenuItem,
34
60
  SelectionMenuConfig,
35
61
  SelectionMenuPluralLabels,
62
+ LinkDecorationInfo,
63
+ LinkRect,
36
64
  };
37
65
  export type {
38
66
  LinkPressEvent,
@@ -40,6 +68,8 @@ export type {
40
68
  TaskListItemPressEvent,
41
69
  CopyPressEvent,
42
70
  CopyEvent,
71
+ SelectionChangeEvent,
72
+ SelectionRect,
43
73
  };
44
74
 
45
75
  // Default English labels for the built-in selection menu actions. Defaults are
@@ -103,6 +133,34 @@ const buildPluralTemplates = (
103
133
  return templates;
104
134
  };
105
135
 
136
+ type LinkLayoutFragment = LinkLayoutEvent['fragments'][number];
137
+
138
+ /** Groups the flat per-line fragments native reports into one entry per link. */
139
+ const groupLinkFragments = (
140
+ fragments: ReadonlyArray<LinkLayoutFragment>
141
+ ): LinkDecorationInfo[] => {
142
+ const byIndex = new Map<number, LinkDecorationInfo>();
143
+ for (const { linkIndex, url, text, x, y, width, height } of fragments) {
144
+ const rect: LinkRect = { x, y, width, height };
145
+ const info = byIndex.get(linkIndex);
146
+ if (info) {
147
+ info.rects.push(rect);
148
+ info.rect = rect;
149
+ } else {
150
+ byIndex.set(linkIndex, {
151
+ index: linkIndex,
152
+ url,
153
+ text,
154
+ rect,
155
+ rects: [rect],
156
+ });
157
+ }
158
+ }
159
+ return [...byIndex.values()];
160
+ };
161
+
162
+ const ZERO_OFFSET = { x: 0, y: 0 };
163
+
106
164
  const defaultMd4cFlags: Md4cFlags = {
107
165
  underline: false,
108
166
  superscript: false,
@@ -113,6 +171,7 @@ const defaultMd4cFlags: Md4cFlags = {
113
171
  };
114
172
 
115
173
  export const EnrichedMarkdownText = ({
174
+ ref,
116
175
  markdown,
117
176
  markdownStyle = {},
118
177
  containerStyle,
@@ -122,6 +181,7 @@ export const EnrichedMarkdownText = ({
122
181
  enableTaskListItemToggle = true,
123
182
  onCopyPress,
124
183
  onCopy,
184
+ onSelectionChange,
125
185
  enableLinkPreview,
126
186
  selectable = true,
127
187
  md4cFlags = defaultMd4cFlags,
@@ -142,6 +202,8 @@ export const EnrichedMarkdownText = ({
142
202
  textBreakStrategy,
143
203
  lineBreakStrategyIOS,
144
204
  writingDirection = 'first-strong',
205
+ renderLinkDecoration,
206
+ onLayout,
145
207
  ...rest
146
208
  }: EnrichedMarkdownTextProps) => {
147
209
  const normalizedStyleRef = useRef<MarkdownStyleInternal | null>(null);
@@ -260,6 +322,71 @@ export const EnrichedMarkdownText = ({
260
322
  );
261
323
  const interceptCopy = onCopy != null;
262
324
 
325
+ const decorated = renderLinkDecoration != null;
326
+ const [linkLayout, setLinkLayout] = useState<LinkDecorationInfo[]>([]);
327
+ // Where the native view sits inside the wrapper (the wrapper's padding).
328
+ const [nativeOffset, setNativeOffset] = useState(ZERO_OFFSET);
329
+
330
+ const handleLinkLayout = useCallback(
331
+ (e: NativeSyntheticEvent<LinkLayoutEvent>) => {
332
+ setLinkLayout(groupLinkFragments(e.nativeEvent.fragments));
333
+ },
334
+ []
335
+ );
336
+
337
+ const handleNativeLayout = useCallback((e: LayoutChangeEvent) => {
338
+ const { x, y } = e.nativeEvent.layout;
339
+ setNativeOffset((prev) => (prev.x === x && prev.y === y ? prev : { x, y }));
340
+ }, []);
341
+
342
+ // Rects stay relative to the native view — the view the ref's
343
+ // `measureInWindow` measures — even when decorations wrap it.
344
+ const handleSelectionChange = useCallback(
345
+ // Codegen types `source` as a plain string; native only sends the four
346
+ // values of SelectionChangeEvent['source'].
347
+ (e: NativeSyntheticEvent<NativeSelectionChangeEvent>) => {
348
+ const { source, ...payload } = e.nativeEvent;
349
+ onSelectionChange?.({
350
+ ...payload,
351
+ source: source as SelectionChangeEvent['source'],
352
+ });
353
+ },
354
+ [onSelectionChange]
355
+ );
356
+
357
+ const nativeRef = useRef<HostInstance | null>(null);
358
+ const isGithubFlavor = flavor === 'github';
359
+ useImperativeHandle(
360
+ ref,
361
+ () => ({
362
+ clearSelection: () => {
363
+ const node = nativeRef.current;
364
+ if (node == null) return;
365
+ // Codegen's ViewRef resolves to `never` with RN 0.84's function-based
366
+ // HostComponent type — the cast is safe at runtime.
367
+ if (isGithubFlavor) {
368
+ MarkdownCommands.clearSelection(
369
+ node as Parameters<(typeof MarkdownCommands)['clearSelection']>[0]
370
+ );
371
+ } else {
372
+ TextCommands.clearSelection(
373
+ node as Parameters<(typeof TextCommands)['clearSelection']>[0]
374
+ );
375
+ }
376
+ },
377
+ measure: (callback) => nativeRef.current?.measure(callback),
378
+ measureInWindow: (callback) =>
379
+ nativeRef.current?.measureInWindow(callback),
380
+ measureLayout: (relativeToNativeNode, onSuccess, onFail) =>
381
+ nativeRef.current?.measureLayout(
382
+ relativeToNativeNode,
383
+ onSuccess,
384
+ onFail
385
+ ),
386
+ }),
387
+ [isGithubFlavor]
388
+ );
389
+
263
390
  const tableMode = streamingConfig?.tableMode ?? 'progressive';
264
391
  const codeBlockMode = streamingConfig?.codeBlockMode ?? 'progressive';
265
392
  const normalizedStreamingConfig = useMemo(
@@ -341,6 +468,8 @@ export const EnrichedMarkdownText = ({
341
468
  enableTaskListItemToggle,
342
469
  onCopyPress: handleCopyPress,
343
470
  onCopyRequest: handleCopyRequest,
471
+ emitSelectionChange: onSelectionChange != null,
472
+ onSelectionChange: handleSelectionChange,
344
473
  enableLinkPreview: onLinkLongPress == null && (enableLinkPreview ?? true),
345
474
  selectable,
346
475
  md4cFlags: normalizedMd4cFlags,
@@ -365,11 +494,55 @@ export const EnrichedMarkdownText = ({
365
494
  ...rest,
366
495
  };
367
496
 
368
- if (flavor === 'github') {
369
- return <EnrichedMarkdownNativeComponent {...sharedProps} />;
497
+ const NativeComponent = isGithubFlavor
498
+ ? EnrichedMarkdownNativeComponent
499
+ : EnrichedMarkdownTextNativeComponent;
500
+
501
+ // Without decorations the native view is returned as-is (no wrapper, no
502
+ // layout events) so existing layouts are untouched.
503
+ if (!decorated) {
504
+ return (
505
+ <NativeComponent {...sharedProps} ref={nativeRef} onLayout={onLayout} />
506
+ );
370
507
  }
371
508
 
372
- return <EnrichedMarkdownTextNativeComponent {...sharedProps} />;
509
+ return (
510
+ <View style={containerStyle} onLayout={onLayout}>
511
+ <NativeComponent
512
+ {...sharedProps}
513
+ ref={nativeRef}
514
+ style={undefined}
515
+ emitLinkLayout
516
+ onLinkLayout={handleLinkLayout}
517
+ onLayout={handleNativeLayout}
518
+ />
519
+ {linkLayout.map((info) => {
520
+ const decoration = renderLinkDecoration(info);
521
+ if (decoration == null) return null;
522
+ return (
523
+ <View
524
+ key={info.index}
525
+ pointerEvents="box-none"
526
+ style={[
527
+ styles.decoration,
528
+ {
529
+ left: nativeOffset.x + info.rect.x,
530
+ top: nativeOffset.y + info.rect.y,
531
+ width: info.rect.width,
532
+ height: info.rect.height,
533
+ },
534
+ ]}
535
+ >
536
+ {decoration}
537
+ </View>
538
+ );
539
+ })}
540
+ </View>
541
+ );
373
542
  };
374
543
 
544
+ const styles = StyleSheet.create({
545
+ decoration: { position: 'absolute' },
546
+ });
547
+
375
548
  export default EnrichedMarkdownText;
@@ -183,6 +183,11 @@ const DEFAULT_NORMALIZED_STYLE = Object.freeze({
183
183
  color: normalizeColor('#2563EB')!,
184
184
  underline: true,
185
185
  backgroundColor: normalizeColor('transparent')!,
186
+ borderRadius: 0,
187
+ paddingHorizontal: 0,
188
+ paddingVertical: 0,
189
+ borderColor: normalizeColor('transparent')!,
190
+ borderWidth: 0,
186
191
  },
187
192
  linkVariants: [],
188
193
  strong: { fontFamily: '', fontWeight: 'bold', color: undefined },
@@ -338,9 +343,17 @@ export const normalizeMarkdownStyle = (
338
343
  color: ((override.color ? normalizeColor(override.color) : null) ??
339
344
  linkBase.color) as string,
340
345
  underline: override.underline ?? linkBase.underline,
346
+ // Chip ("box") properties never inherit from the base link style.
341
347
  backgroundColor: (override.backgroundColor
342
348
  ? (normalizeColor(override.backgroundColor) ?? transparent)
343
349
  : transparent) as string,
350
+ borderRadius: override.borderRadius ?? 0,
351
+ paddingHorizontal: override.paddingHorizontal ?? 0,
352
+ paddingVertical: override.paddingVertical ?? 0,
353
+ borderColor: (override.borderColor
354
+ ? (normalizeColor(override.borderColor) ?? transparent)
355
+ : transparent) as string,
356
+ borderWidth: override.borderWidth ?? 0,
344
357
  };
345
358
  }
346
359
  );
@@ -147,6 +147,11 @@ const DEFAULT_NORMALIZED_STYLE: MarkdownStyleInternal = Object.freeze({
147
147
  color: '#2563EB',
148
148
  underline: true,
149
149
  backgroundColor: 'transparent',
150
+ borderRadius: 0,
151
+ paddingHorizontal: 0,
152
+ paddingVertical: 0,
153
+ borderColor: 'transparent',
154
+ borderWidth: 0,
150
155
  },
151
156
  linkVariants: [],
152
157
  strong: { fontFamily: '', fontWeight: 'bold', color: undefined },
@@ -289,7 +294,13 @@ export const normalizeMarkdownStyle = (
289
294
  pattern,
290
295
  color: override.color ?? linkBase.color,
291
296
  underline: override.underline ?? linkBase.underline,
297
+ // Chip ("box") properties never inherit from the base link style.
292
298
  backgroundColor: override.backgroundColor ?? 'transparent',
299
+ borderRadius: override.borderRadius ?? 0,
300
+ paddingHorizontal: override.paddingHorizontal ?? 0,
301
+ paddingVertical: override.paddingVertical ?? 0,
302
+ borderColor: override.borderColor ?? 'transparent',
303
+ borderWidth: override.borderWidth ?? 0,
293
304
  };
294
305
  }
295
306
  );
@@ -84,17 +84,39 @@ interface CodeBlockStyle extends BaseBlockStyle {
84
84
  syntaxColors?: CodeBlockSyntaxColors;
85
85
  }
86
86
 
87
- export interface LinkStyle {
87
+ /**
88
+ * Chip ("box") properties shared by `link` and `linkVariants`. Unlike `color`
89
+ * and `underline`, these never inherit from the base `link` style: a variant
90
+ * that omits them renders without a chip (`0` / `transparent`).
91
+ */
92
+ interface LinkChipStyle {
93
+ backgroundColor?: string;
94
+ /** Corner radius of the chip, in dp. */
95
+ borderRadius?: number;
96
+ /**
97
+ * Horizontal padding, in dp. Reserves real space in the line on both outer
98
+ * edges of the link (affects measurement and wrapping).
99
+ */
100
+ paddingHorizontal?: number;
101
+ /**
102
+ * Vertical padding, in dp. Only affects the drawn chip (it bleeds into the
103
+ * surrounding line spacing); the line box is unchanged.
104
+ */
105
+ paddingVertical?: number;
106
+ borderColor?: string;
107
+ /** Border width, in dp. Drawn inset, within the chip bounds. */
108
+ borderWidth?: number;
109
+ }
110
+
111
+ export interface LinkStyle extends LinkChipStyle {
88
112
  fontFamily?: string;
89
113
  color?: string;
90
114
  underline?: boolean;
91
- backgroundColor?: string;
92
115
  }
93
116
 
94
- export interface LinkVariantStyle {
117
+ export interface LinkVariantStyle extends LinkChipStyle {
95
118
  color?: string;
96
119
  underline?: boolean;
97
- backgroundColor?: string;
98
120
  }
99
121
 
100
122
  interface StrongStyle {
@@ -350,7 +372,9 @@ export interface MarkdownStyle {
350
372
  * specific patterns take precedence.
351
373
  *
352
374
  * `color` and `underline` inherit from the base `link` style when omitted.
353
- * `backgroundColor` defaults to `transparent`.
375
+ * Chip properties (`backgroundColor`, `borderRadius`, `paddingHorizontal`,
376
+ * `paddingVertical`, `borderColor`, `borderWidth`) do not inherit: when
377
+ * omitted they default to `0` / `transparent`.
354
378
  * `fontFamily` always follows the base `link` style and cannot be overridden per-variant.
355
379
  *
356
380
  * @example
@@ -88,6 +88,11 @@ interface LinkStyleInternal {
88
88
  color: string;
89
89
  underline: boolean;
90
90
  backgroundColor: string;
91
+ borderRadius: number;
92
+ paddingHorizontal: number;
93
+ paddingVertical: number;
94
+ borderColor: string;
95
+ borderWidth: number;
91
96
  }
92
97
 
93
98
  export interface LinkVariantEntryInternal {
@@ -95,6 +100,11 @@ export interface LinkVariantEntryInternal {
95
100
  color: string;
96
101
  underline: boolean;
97
102
  backgroundColor: string;
103
+ borderRadius: number;
104
+ paddingHorizontal: number;
105
+ paddingVertical: number;
106
+ borderColor: string;
107
+ borderWidth: number;
98
108
  }
99
109
 
100
110
  interface StrongStyleInternal {
@@ -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 {
@@ -7,8 +14,49 @@ import type {
7
14
  TaskListItemPressEvent,
8
15
  CopyPressEvent,
9
16
  CopyEvent,
17
+ SelectionChangeEvent,
10
18
  } from './events';
11
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
+
12
60
  /**
13
61
  * Public context menu item. Each item includes a JS-side `onPress` callback
14
62
  * that is called when the user taps the item in the selection context menu.
@@ -141,6 +189,11 @@ export interface StreamingConfig {
141
189
  }
142
190
 
143
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>;
144
197
  /**
145
198
  * Markdown content to render.
146
199
  * @platform ios, android, web
@@ -178,6 +231,21 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
178
231
  * @platform ios, android, web
179
232
  */
180
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;
181
249
  /**
182
250
  * Callback fired when a task list checkbox is tapped.
183
251
  *
@@ -226,6 +294,22 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
226
294
  * @platform ios, android
227
295
  */
228
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. There is no
309
+ * "dragging" flag: debounce before showing a menu.
310
+ * @platform ios, android
311
+ */
312
+ onSelectionChange?: (event: SelectionChangeEvent) => void;
229
313
  /**
230
314
  * Controls whether the system link preview is shown on long press (iOS only).
231
315
  *
@@ -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
  *