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.
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +1 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +1 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +72 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +50 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +10 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +134 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +62 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +13 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +42 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +37 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +13 -99
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/InlineChipPainter.kt +140 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkChipSpans.kt +177 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +1 -6
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +36 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkVariantEntry.kt +7 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +33 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt +100 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +24 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +27 -8
- package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +9 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +18 -2
- package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +3 -1
- package/docs/API_REFERENCE.md +101 -0
- package/docs/COPY_OPTIONS.md +14 -0
- package/docs/MENTIONS.md +8 -1
- package/docs/STYLES.md +27 -0
- package/docs/TEXT.md +2 -0
- package/ios/EnrichedMarkdown.mm +91 -2
- package/ios/EnrichedMarkdownText.mm +64 -3
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +72 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +50 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +10 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +134 -0
- package/ios/internals/MeasurementCache.h +5 -0
- package/ios/renderer/LinkRenderer.m +47 -15
- package/ios/styles/StyleConfig.h +16 -0
- package/ios/styles/StyleConfig.mm +60 -0
- package/ios/utils/CodeBackground.m +3 -65
- package/ios/utils/ENRMBlockMenu.h +12 -3
- package/ios/utils/ENRMChipDrawing.h +24 -0
- package/ios/utils/ENRMChipDrawing.m +83 -0
- package/ios/utils/ENRMLinkChip.h +70 -0
- package/ios/utils/ENRMLinkChip.m +194 -0
- package/ios/utils/ENRMLinkLayout.h +50 -0
- package/ios/utils/ENRMLinkLayout.m +45 -0
- package/ios/utils/EditMenuUtils+macOS.m +4 -1
- package/ios/utils/EditMenuUtils.h +10 -1
- package/ios/utils/EditMenuUtils.m +22 -5
- package/ios/utils/HTMLGenerator.m +2 -1
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +4 -1
- package/ios/utils/MarkdownExtractor.m +2 -1
- package/ios/utils/RTFExportUtils.m +2 -0
- package/ios/utils/StylePropsUtils.h +37 -1
- package/ios/utils/TextViewLayoutManager.mm +3 -0
- package/ios/views/ENRMCodeBlockContainerView.h +4 -0
- package/ios/views/ENRMCodeBlockContainerView.m +8 -1
- package/ios/views/ENRMMathContainerView.h +4 -0
- package/ios/views/ENRMMathContainerView.m +8 -1
- package/ios/views/TableContainerView.h +7 -0
- package/ios/views/TableContainerView.m +30 -3
- package/lib/module/EnrichedMarkdownNativeComponent.ts +57 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +57 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/EnrichedMarkdownText.js +119 -9
- package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.js +13 -2
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.web.js +13 -2
- package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
- package/lib/module/web/EnrichedMarkdownText.js +6 -3
- package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/web/renderers/InlineRenderers.js +96 -3
- package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
- package/lib/module/web/styles.js +18 -0
- package/lib/module/web/styles.js.map +1 -1
- package/lib/module/web/utils.js +13 -0
- package/lib/module/web/utils.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +53 -0
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +53 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +1 -1
- package/lib/typescript/src/index.web.d.ts.map +1 -1
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyle.d.ts +28 -5
- package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +10 -0
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.d.ts +54 -1
- package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +22 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -1
- package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
- package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
- package/lib/typescript/src/web/styles.d.ts.map +1 -1
- package/lib/typescript/src/web/types.d.ts +4 -0
- package/lib/typescript/src/web/types.d.ts.map +1 -1
- package/lib/typescript/src/web/utils.d.ts +4 -0
- package/lib/typescript/src/web/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownNativeComponent.ts +57 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +57 -0
- package/src/index.tsx +3 -0
- package/src/index.web.tsx +5 -1
- package/src/native/EnrichedMarkdownText.tsx +127 -7
- package/src/normalizeMarkdownStyle.ts +13 -0
- package/src/normalizeMarkdownStyle.web.ts +11 -0
- package/src/types/MarkdownStyle.ts +29 -5
- package/src/types/MarkdownStyleInternal.ts +10 -0
- package/src/types/MarkdownTextProps.ts +56 -0
- package/src/types/MarkdownTextProps.web.ts +43 -1
- package/src/types/events.ts +23 -0
- package/src/web/EnrichedMarkdownText.tsx +10 -3
- package/src/web/renderers/InlineRenderers.tsx +111 -3
- package/src/web/styles.ts +25 -0
- package/src/web/types.ts +4 -0
- 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
|
*
|
package/src/types/events.ts
CHANGED
|
@@ -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
|
-
() => ({
|
|
103
|
-
|
|
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
|
|
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={
|
|
111
|
+
style={linkStyle}
|
|
85
112
|
target="_blank"
|
|
86
113
|
rel="noopener noreferrer"
|
|
87
114
|
onClick={handleClick}
|
|
88
115
|
onContextMenu={handleContextMenu}
|
|
89
116
|
>
|
|
90
|
-
{
|
|
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.).
|