react-native-enriched-markdown-scaffold 1.0.2-scaffold.2 → 1.0.2-scaffold.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +3 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +1 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +3 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +1 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +72 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +50 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +10 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +134 -0
  9. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +62 -7
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +4 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +13 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +42 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +12 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -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 +33 -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 +24 -2
  29. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +27 -8
  30. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +9 -1
  31. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +18 -2
  32. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +3 -1
  33. package/docs/API_REFERENCE.md +101 -0
  34. package/docs/COPY_OPTIONS.md +14 -0
  35. package/docs/MENTIONS.md +8 -1
  36. package/docs/STYLES.md +27 -0
  37. package/docs/TEXT.md +2 -0
  38. package/ios/EnrichedMarkdown.mm +91 -2
  39. package/ios/EnrichedMarkdownText.mm +64 -3
  40. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +72 -0
  41. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +50 -0
  42. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +10 -0
  43. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +134 -0
  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/ENRMBlockMenu.h +12 -3
  50. package/ios/utils/ENRMChipDrawing.h +24 -0
  51. package/ios/utils/ENRMChipDrawing.m +83 -0
  52. package/ios/utils/ENRMLinkChip.h +70 -0
  53. package/ios/utils/ENRMLinkChip.m +194 -0
  54. package/ios/utils/ENRMLinkLayout.h +50 -0
  55. package/ios/utils/ENRMLinkLayout.m +45 -0
  56. package/ios/utils/EditMenuUtils+macOS.m +4 -1
  57. package/ios/utils/EditMenuUtils.h +10 -1
  58. package/ios/utils/EditMenuUtils.m +22 -5
  59. package/ios/utils/HTMLGenerator.m +2 -1
  60. package/ios/utils/MarkdownAccessibilityElementBuilder.m +4 -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 +4 -0
  66. package/ios/views/ENRMCodeBlockContainerView.m +8 -1
  67. package/ios/views/ENRMMathContainerView.h +4 -0
  68. package/ios/views/ENRMMathContainerView.m +8 -1
  69. package/ios/views/TableContainerView.h +7 -0
  70. package/ios/views/TableContainerView.m +30 -3
  71. package/lib/module/EnrichedMarkdownNativeComponent.ts +57 -0
  72. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +57 -0
  73. package/lib/module/index.js.map +1 -1
  74. package/lib/module/native/EnrichedMarkdownText.js +119 -9
  75. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  76. package/lib/module/normalizeMarkdownStyle.js +13 -2
  77. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  78. package/lib/module/normalizeMarkdownStyle.web.js +13 -2
  79. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  80. package/lib/module/web/EnrichedMarkdownText.js +6 -3
  81. package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
  82. package/lib/module/web/renderers/InlineRenderers.js +96 -3
  83. package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
  84. package/lib/module/web/styles.js +18 -0
  85. package/lib/module/web/styles.js.map +1 -1
  86. package/lib/module/web/utils.js +13 -0
  87. package/lib/module/web/utils.js.map +1 -1
  88. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +53 -0
  89. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  90. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +53 -0
  91. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  92. package/lib/typescript/src/index.d.ts +2 -2
  93. package/lib/typescript/src/index.d.ts.map +1 -1
  94. package/lib/typescript/src/index.web.d.ts +1 -1
  95. package/lib/typescript/src/index.web.d.ts.map +1 -1
  96. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  97. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  98. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  99. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  100. package/lib/typescript/src/types/MarkdownStyle.d.ts +28 -5
  101. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  102. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +10 -0
  103. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  104. package/lib/typescript/src/types/MarkdownTextProps.d.ts +54 -1
  105. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  106. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
  107. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
  108. package/lib/typescript/src/types/events.d.ts +22 -0
  109. package/lib/typescript/src/types/events.d.ts.map +1 -1
  110. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
  111. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
  112. package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
  113. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  114. package/lib/typescript/src/web/types.d.ts +4 -0
  115. package/lib/typescript/src/web/types.d.ts.map +1 -1
  116. package/lib/typescript/src/web/utils.d.ts +4 -0
  117. package/lib/typescript/src/web/utils.d.ts.map +1 -1
  118. package/package.json +1 -1
  119. package/src/EnrichedMarkdownNativeComponent.ts +57 -0
  120. package/src/EnrichedMarkdownTextNativeComponent.ts +57 -0
  121. package/src/index.tsx +3 -0
  122. package/src/index.web.tsx +5 -1
  123. package/src/native/EnrichedMarkdownText.tsx +127 -7
  124. package/src/normalizeMarkdownStyle.ts +13 -0
  125. package/src/normalizeMarkdownStyle.web.ts +11 -0
  126. package/src/types/MarkdownStyle.ts +29 -5
  127. package/src/types/MarkdownStyleInternal.ts +10 -0
  128. package/src/types/MarkdownTextProps.ts +56 -0
  129. package/src/types/MarkdownTextProps.web.ts +43 -1
  130. package/src/types/events.ts +23 -0
  131. package/src/web/EnrichedMarkdownText.tsx +10 -3
  132. package/src/web/renderers/InlineRenderers.tsx +111 -3
  133. package/src/web/styles.ts +25 -0
  134. package/src/web/types.ts +4 -0
  135. package/src/web/utils.ts +8 -0
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from 'react';
1
2
  import type { ColorValue, ViewProps, ViewStyle, TextStyle } from 'react-native';
2
3
  import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle';
3
4
  import type { AccessibilityLabels } from './AccessibilityLabels';
@@ -6,8 +7,36 @@ import type {
6
7
  LinkLongPressEvent,
7
8
  TaskListItemPressEvent,
8
9
  CopyPressEvent,
10
+ CopyEvent,
9
11
  } from './events';
10
12
 
13
+ /**
14
+ * A rect in dp, relative to the component's content box (inside any
15
+ * `containerStyle` padding).
16
+ */
17
+ export interface LinkRect {
18
+ x: number;
19
+ y: number;
20
+ width: number;
21
+ height: number;
22
+ }
23
+
24
+ /** Layout of one rendered link, passed to `renderLinkDecoration`. */
25
+ export interface LinkDecorationInfo {
26
+ /** Position of the link in the document (0-based) — stable key for a link. */
27
+ index: number;
28
+ url: string;
29
+ /** The link's visible text. */
30
+ text: string;
31
+ /**
32
+ * Anchor rect. Native: the drawn chip (glyphs + padding) of the link's last
33
+ * line fragment. Web: the link's inline box.
34
+ */
35
+ rect: LinkRect;
36
+ /** Every line fragment of the link (several when it wraps). Web: `[rect]`. */
37
+ rects: LinkRect[];
38
+ }
39
+
11
40
  /**
12
41
  * Public context menu item. Each item includes a JS-side `onPress` callback
13
42
  * that is called when the user taps the item in the selection context menu.
@@ -177,6 +206,21 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
177
206
  * @platform ios, android, web
178
207
  */
179
208
  onLinkLongPress?: (event: LinkLongPressEvent) => void;
209
+ /**
210
+ * Renders a decoration (e.g. a badge or icon) over each link. The returned
211
+ * node is placed in an absolutely positioned box matching `info.rect` (the
212
+ * link's last line fragment, including chip padding) with
213
+ * `pointerEvents="box-none"`; position content relative to it (negative
214
+ * offsets overflow the box). Return `null` to leave a link undecorated.
215
+ *
216
+ * Native caveats: rects arrive from native after layout, so decorations
217
+ * appear one frame after the text; links inside tables (`flavor="github"`)
218
+ * are not reported; content overflowing the component bounds does not
219
+ * receive touches on Android. Toggling this prop between set and unset
220
+ * remounts the native view.
221
+ * @platform ios, android, web
222
+ */
223
+ renderLinkDecoration?: (info: LinkDecorationInfo) => ReactNode | null;
180
224
  /**
181
225
  * Callback fired when a task list checkbox is tapped.
182
226
  *
@@ -213,6 +257,18 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
213
257
  * @platform ios, android, macos
214
258
  */
215
259
  onCopyPress?: (event: CopyPressEvent) => void;
260
+ /**
261
+ * Takes over the "Copy" item of the text selection menu and of the table /
262
+ * math / code block menus. When set, native does **not** write the
263
+ * clipboard: it calls this with the copied content and the handler writes
264
+ * whatever it wants (e.g. with `expo-clipboard`). Overrides
265
+ * `selectionMenuConfig.copy.format`; `copy.enabled: false` still hides the
266
+ * item. "Copy as Markdown", the code block header button and the VoiceOver
267
+ * copy action are unaffected, and `onCopyPress` does not fire for an
268
+ * intercepted Copy.
269
+ * @platform ios, android
270
+ */
271
+ onCopy?: (event: CopyEvent) => void;
216
272
  /**
217
273
  * Controls whether the system link preview is shown on long press (iOS only).
218
274
  *
@@ -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,29 @@ 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
+
20
43
  /**
21
44
  * Native-level context menu item config sent to the native component.
22
45
  * 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.).