react-native-enriched-markdown-scaffold 1.0.2-scaffold.3 → 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 +48 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +32 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +10 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +122 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +38 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +8 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +37 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +8 -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 +14 -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 +4 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +14 -0
- package/docs/API_REFERENCE.md +53 -0
- package/docs/MENTIONS.md +8 -1
- package/docs/STYLES.md +27 -0
- package/docs/TEXT.md +2 -0
- package/ios/EnrichedMarkdown.mm +48 -0
- package/ios/EnrichedMarkdownText.mm +39 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +48 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +32 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +10 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +122 -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/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.m +4 -1
- 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/TableContainerView.h +3 -0
- package/ios/views/TableContainerView.m +21 -0
- package/lib/module/EnrichedMarkdownNativeComponent.ts +41 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +41 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/EnrichedMarkdownText.js +96 -7
- 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 +40 -0
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +40 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- 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 +3 -3
- 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 +41 -0
- 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/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 +41 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +41 -0
- package/src/index.tsx +2 -0
- package/src/index.web.tsx +5 -1
- package/src/native/EnrichedMarkdownText.tsx +103 -6
- 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 +43 -0
- package/src/types/MarkdownTextProps.web.ts +43 -1
- 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,6 +1,9 @@
|
|
|
1
|
-
import { useMemo, useCallback, useRef, useEffect } from 'react';
|
|
1
|
+
import { useMemo, useCallback, useRef, useEffect, useState } from 'react';
|
|
2
2
|
import EnrichedMarkdownTextNativeComponent from '../EnrichedMarkdownTextNativeComponent';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
LinkLayoutEvent,
|
|
5
|
+
MarkdownStyleInternal,
|
|
6
|
+
} from '../EnrichedMarkdownTextNativeComponent';
|
|
4
7
|
import EnrichedMarkdownNativeComponent from '../EnrichedMarkdownNativeComponent';
|
|
5
8
|
import { normalizeMarkdownStyle } from '../normalizeMarkdownStyle';
|
|
6
9
|
import { resolveAccessibilityLabels } from '../accessibilityLabelDefaults';
|
|
@@ -8,7 +11,8 @@ import {
|
|
|
8
11
|
normalizeMenuItem,
|
|
9
12
|
normalizeLegacyBooleanMenuItem,
|
|
10
13
|
} from '../normalizeMenuItem';
|
|
11
|
-
import
|
|
14
|
+
import { StyleSheet, View } from 'react-native';
|
|
15
|
+
import type { LayoutChangeEvent, NativeSyntheticEvent } from 'react-native';
|
|
12
16
|
import type { MarkdownStyle, Md4cFlags } from '../types/MarkdownStyle';
|
|
13
17
|
import type {
|
|
14
18
|
EnrichedMarkdownTextProps,
|
|
@@ -16,6 +20,8 @@ import type {
|
|
|
16
20
|
ContextMenuItem,
|
|
17
21
|
SelectionMenuConfig,
|
|
18
22
|
SelectionMenuPluralLabels,
|
|
23
|
+
LinkDecorationInfo,
|
|
24
|
+
LinkRect,
|
|
19
25
|
} from '../types/MarkdownTextProps';
|
|
20
26
|
import type {
|
|
21
27
|
LinkPressEvent,
|
|
@@ -33,6 +39,8 @@ export type {
|
|
|
33
39
|
ContextMenuItem,
|
|
34
40
|
SelectionMenuConfig,
|
|
35
41
|
SelectionMenuPluralLabels,
|
|
42
|
+
LinkDecorationInfo,
|
|
43
|
+
LinkRect,
|
|
36
44
|
};
|
|
37
45
|
export type {
|
|
38
46
|
LinkPressEvent,
|
|
@@ -103,6 +111,34 @@ const buildPluralTemplates = (
|
|
|
103
111
|
return templates;
|
|
104
112
|
};
|
|
105
113
|
|
|
114
|
+
type LinkLayoutFragment = LinkLayoutEvent['fragments'][number];
|
|
115
|
+
|
|
116
|
+
/** Groups the flat per-line fragments native reports into one entry per link. */
|
|
117
|
+
const groupLinkFragments = (
|
|
118
|
+
fragments: ReadonlyArray<LinkLayoutFragment>
|
|
119
|
+
): LinkDecorationInfo[] => {
|
|
120
|
+
const byIndex = new Map<number, LinkDecorationInfo>();
|
|
121
|
+
for (const { linkIndex, url, text, x, y, width, height } of fragments) {
|
|
122
|
+
const rect: LinkRect = { x, y, width, height };
|
|
123
|
+
const info = byIndex.get(linkIndex);
|
|
124
|
+
if (info) {
|
|
125
|
+
info.rects.push(rect);
|
|
126
|
+
info.rect = rect;
|
|
127
|
+
} else {
|
|
128
|
+
byIndex.set(linkIndex, {
|
|
129
|
+
index: linkIndex,
|
|
130
|
+
url,
|
|
131
|
+
text,
|
|
132
|
+
rect,
|
|
133
|
+
rects: [rect],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [...byIndex.values()];
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const ZERO_OFFSET = { x: 0, y: 0 };
|
|
141
|
+
|
|
106
142
|
const defaultMd4cFlags: Md4cFlags = {
|
|
107
143
|
underline: false,
|
|
108
144
|
superscript: false,
|
|
@@ -142,6 +178,8 @@ export const EnrichedMarkdownText = ({
|
|
|
142
178
|
textBreakStrategy,
|
|
143
179
|
lineBreakStrategyIOS,
|
|
144
180
|
writingDirection = 'first-strong',
|
|
181
|
+
renderLinkDecoration,
|
|
182
|
+
onLayout,
|
|
145
183
|
...rest
|
|
146
184
|
}: EnrichedMarkdownTextProps) => {
|
|
147
185
|
const normalizedStyleRef = useRef<MarkdownStyleInternal | null>(null);
|
|
@@ -260,6 +298,23 @@ export const EnrichedMarkdownText = ({
|
|
|
260
298
|
);
|
|
261
299
|
const interceptCopy = onCopy != null;
|
|
262
300
|
|
|
301
|
+
const decorated = renderLinkDecoration != null;
|
|
302
|
+
const [linkLayout, setLinkLayout] = useState<LinkDecorationInfo[]>([]);
|
|
303
|
+
// Where the native view sits inside the wrapper (the wrapper's padding).
|
|
304
|
+
const [nativeOffset, setNativeOffset] = useState(ZERO_OFFSET);
|
|
305
|
+
|
|
306
|
+
const handleLinkLayout = useCallback(
|
|
307
|
+
(e: NativeSyntheticEvent<LinkLayoutEvent>) => {
|
|
308
|
+
setLinkLayout(groupLinkFragments(e.nativeEvent.fragments));
|
|
309
|
+
},
|
|
310
|
+
[]
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const handleNativeLayout = useCallback((e: LayoutChangeEvent) => {
|
|
314
|
+
const { x, y } = e.nativeEvent.layout;
|
|
315
|
+
setNativeOffset((prev) => (prev.x === x && prev.y === y ? prev : { x, y }));
|
|
316
|
+
}, []);
|
|
317
|
+
|
|
263
318
|
const tableMode = streamingConfig?.tableMode ?? 'progressive';
|
|
264
319
|
const codeBlockMode = streamingConfig?.codeBlockMode ?? 'progressive';
|
|
265
320
|
const normalizedStreamingConfig = useMemo(
|
|
@@ -365,11 +420,53 @@ export const EnrichedMarkdownText = ({
|
|
|
365
420
|
...rest,
|
|
366
421
|
};
|
|
367
422
|
|
|
368
|
-
|
|
369
|
-
|
|
423
|
+
const NativeComponent =
|
|
424
|
+
flavor === 'github'
|
|
425
|
+
? EnrichedMarkdownNativeComponent
|
|
426
|
+
: EnrichedMarkdownTextNativeComponent;
|
|
427
|
+
|
|
428
|
+
// Without decorations the native view is returned as-is (no wrapper, no
|
|
429
|
+
// layout events) so existing layouts are untouched.
|
|
430
|
+
if (!decorated) {
|
|
431
|
+
return <NativeComponent {...sharedProps} onLayout={onLayout} />;
|
|
370
432
|
}
|
|
371
433
|
|
|
372
|
-
return
|
|
434
|
+
return (
|
|
435
|
+
<View style={containerStyle} onLayout={onLayout}>
|
|
436
|
+
<NativeComponent
|
|
437
|
+
{...sharedProps}
|
|
438
|
+
style={undefined}
|
|
439
|
+
emitLinkLayout
|
|
440
|
+
onLinkLayout={handleLinkLayout}
|
|
441
|
+
onLayout={handleNativeLayout}
|
|
442
|
+
/>
|
|
443
|
+
{linkLayout.map((info) => {
|
|
444
|
+
const decoration = renderLinkDecoration(info);
|
|
445
|
+
if (decoration == null) return null;
|
|
446
|
+
return (
|
|
447
|
+
<View
|
|
448
|
+
key={info.index}
|
|
449
|
+
pointerEvents="box-none"
|
|
450
|
+
style={[
|
|
451
|
+
styles.decoration,
|
|
452
|
+
{
|
|
453
|
+
left: nativeOffset.x + info.rect.x,
|
|
454
|
+
top: nativeOffset.y + info.rect.y,
|
|
455
|
+
width: info.rect.width,
|
|
456
|
+
height: info.rect.height,
|
|
457
|
+
},
|
|
458
|
+
]}
|
|
459
|
+
>
|
|
460
|
+
{decoration}
|
|
461
|
+
</View>
|
|
462
|
+
);
|
|
463
|
+
})}
|
|
464
|
+
</View>
|
|
465
|
+
);
|
|
373
466
|
};
|
|
374
467
|
|
|
468
|
+
const styles = StyleSheet.create({
|
|
469
|
+
decoration: { position: 'absolute' },
|
|
470
|
+
});
|
|
471
|
+
|
|
375
472
|
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
|
-
|
|
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`
|
|
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,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';
|
|
@@ -9,6 +10,33 @@ import type {
|
|
|
9
10
|
CopyEvent,
|
|
10
11
|
} from './events';
|
|
11
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
|
+
|
|
12
40
|
/**
|
|
13
41
|
* Public context menu item. Each item includes a JS-side `onPress` callback
|
|
14
42
|
* that is called when the user taps the item in the selection context menu.
|
|
@@ -178,6 +206,21 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
|
|
|
178
206
|
* @platform ios, android, web
|
|
179
207
|
*/
|
|
180
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;
|
|
181
224
|
/**
|
|
182
225
|
* Callback fired when a task list checkbox is tapped.
|
|
183
226
|
*
|
|
@@ -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
|
*
|
|
@@ -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.).
|