react-native-enriched-markdown 0.7.0 → 0.7.2
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/LICENSE +20 -0
- package/README.md +215 -0
- package/app.plugin.js +2 -1
- package/docs/ACCESSIBILITY.md +172 -0
- package/docs/API_REFERENCE.md +948 -0
- package/docs/COPY_OPTIONS.md +127 -0
- package/docs/ELEMENTS_STRUCTURE.md +112 -0
- package/docs/IMAGE_CACHING.md +25 -0
- package/docs/INPUT.md +233 -0
- package/docs/LATEX_MATH.md +136 -0
- package/docs/MACOS.md +19 -0
- package/docs/MARKDOWN_STREAMING.md +36 -0
- package/docs/MENTIONS.md +97 -0
- package/docs/RTL.md +82 -0
- package/docs/STYLES.md +519 -0
- package/docs/TEXT.md +110 -0
- package/docs/WEB.md +49 -0
- package/package.json +7 -4
- package/plugin/build/withAndroidMath.js +19 -0
- package/plugin/build/withIosMath.js +28 -0
- package/plugin/build/withReactNativeEnrichedMarkdown.js +11 -0
- package/plugin/tsconfig.build.json +16 -0
- package/lib/module/plugin/withAndroidMath.js +0 -20
- package/lib/module/plugin/withAndroidMath.js.map +0 -1
- package/lib/module/plugin/withIosMath.js +0 -23
- package/lib/module/plugin/withIosMath.js.map +0 -1
- package/lib/module/plugin/withReactNativeEnrichedMarkdown.js +0 -16
- package/lib/module/plugin/withReactNativeEnrichedMarkdown.js.map +0 -1
- package/lib/typescript/src/plugin/withAndroidMath.d.ts +0 -5
- package/lib/typescript/src/plugin/withAndroidMath.d.ts.map +0 -1
- package/lib/typescript/src/plugin/withIosMath.d.ts +0 -5
- package/lib/typescript/src/plugin/withIosMath.d.ts.map +0 -1
- package/lib/typescript/src/plugin/withReactNativeEnrichedMarkdown.d.ts +0 -6
- package/lib/typescript/src/plugin/withReactNativeEnrichedMarkdown.d.ts.map +0 -1
- /package/{src/plugin → plugin/src}/withAndroidMath.ts +0 -0
- /package/{src/plugin → plugin/src}/withIosMath.ts +0 -0
- /package/{src/plugin → plugin/src}/withReactNativeEnrichedMarkdown.ts +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Markdown Streaming
|
|
2
|
+
|
|
3
|
+
If you need to render markdown that streams token-by-token from an LLM, check out [react-native-streamdown](https://github.com/software-mansion-labs/react-native-streamdown) — a streaming-ready markdown component built on top of `react-native-enriched-markdown`.
|
|
4
|
+
|
|
5
|
+
It combines [remend](https://www.npmjs.com/package/remend) for fixing incomplete markdown on the fly with [react-native-worklets](https://docs.swmansion.com/react-native-worklets/) **Bundle Mode** to run all processing off the JS thread, keeping your UI responsive while tokens arrive.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { StreamdownText } from 'react-native-streamdown';
|
|
9
|
+
|
|
10
|
+
<StreamdownText markdown={partialMarkdown} />;
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`StreamdownText` accepts all props from `EnrichedMarkdownText` and adds a `remendConfig` prop for customizing the markdown repair pipeline. See the [react-native-streamdown README](https://github.com/software-mansion-labs/react-native-streamdown#readme) for full setup instructions including the required Babel and Metro configuration for Bundle Mode.
|
|
14
|
+
|
|
15
|
+
## Table Streaming (GFM)
|
|
16
|
+
|
|
17
|
+
When using `flavor="github"` with streaming content, tables require special handling because they are block-level elements that can't be rendered until the parser has enough structure (at minimum a header row and separator line).
|
|
18
|
+
|
|
19
|
+
The `streamingConfig` prop controls this behavior:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<EnrichedMarkdownText
|
|
23
|
+
markdown={streamingMarkdown}
|
|
24
|
+
flavor="github"
|
|
25
|
+
streamingAnimation
|
|
26
|
+
streamingConfig={{ tableMode: 'hidden' }}
|
|
27
|
+
/>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Table Modes
|
|
31
|
+
|
|
32
|
+
| Mode | Behavior |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `'progressive'` (default) | Renders the table row-by-row as content arrives. New rows fade in when `streamingAnimation` is enabled. Incomplete trailing rows are automatically trimmed. |
|
|
35
|
+
| `'hidden'` | The table is completely hidden until it is followed by a blank line, indicating the table is complete. Prevents visual jank from partially formed tables. |
|
|
36
|
+
|
package/docs/MENTIONS.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Mentions
|
|
2
|
+
|
|
3
|
+
`EnrichedMarkdownTextInput` supports mention flows — token-triggered inline entities (e.g. `@user`, `#channel`) rendered as styled links.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
1. User types an indicator (`@`, `#`) or toolbar calls `startMention(indicator)`.
|
|
8
|
+
2. `onStartMention` fires → show suggestion list.
|
|
9
|
+
3. `onChangeMention` fires on each keystroke → filter suggestions by query.
|
|
10
|
+
4. User picks a suggestion → call `insertMention(displayText, url)`.
|
|
11
|
+
5. `onEndMention` fires → hide suggestions.
|
|
12
|
+
|
|
13
|
+
## Example
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<EnrichedMarkdownTextInput
|
|
17
|
+
ref={ref}
|
|
18
|
+
mentionIndicators={['@', '#']}
|
|
19
|
+
markdownStyle={{
|
|
20
|
+
link: { color: '#2563EB', underline: true },
|
|
21
|
+
linkVariants: {
|
|
22
|
+
'^user:': { color: '#1264A3', backgroundColor: '#E8F5FB', underline: false },
|
|
23
|
+
'^channel:': { color: '#065F46', backgroundColor: '#D1FAE5', underline: false },
|
|
24
|
+
},
|
|
25
|
+
}}
|
|
26
|
+
onStartMention={({ indicator }) => setShowSuggestions(true)}
|
|
27
|
+
onChangeMention={({ indicator, text }) => setQuery(text)}
|
|
28
|
+
onEndMention={() => setShowSuggestions(false)}
|
|
29
|
+
onCaretRectChange={setCaretRect} // for positioning the popup
|
|
30
|
+
/>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
When the user selects a suggestion:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
ref.current?.insertMention(`@${item.name}`, item.url);
|
|
37
|
+
// Markdown output: [@Alice](user://u_1)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Props
|
|
41
|
+
|
|
42
|
+
| Prop | Type | Default | Description |
|
|
43
|
+
| ---- | ---- | ------- | ----------- |
|
|
44
|
+
| `mentionIndicators` | `string[]` | `[]` | Trigger strings that start a mention flow. |
|
|
45
|
+
|
|
46
|
+
## Events
|
|
47
|
+
|
|
48
|
+
| Event | Payload | When |
|
|
49
|
+
| ----- | ------- | ---- |
|
|
50
|
+
| `onStartMention` | `{ indicator }` | Mention flow starts. |
|
|
51
|
+
| `onChangeMention` | `{ indicator, text }` | Query text changes (each keystroke). |
|
|
52
|
+
| `onEndMention` | `{ indicator }` | Mention flow ends (cancel, insert, or cursor moved away). |
|
|
53
|
+
|
|
54
|
+
## Ref Methods
|
|
55
|
+
|
|
56
|
+
| Method | Description |
|
|
57
|
+
| ------ | ----------- |
|
|
58
|
+
| `startMention(indicator)` | Inserts the indicator at cursor and triggers the mention flow. Must be in `mentionIndicators`. |
|
|
59
|
+
| `insertMention(displayText, url)` | Replaces the active mention token with a styled link. Only works during an active flow. |
|
|
60
|
+
|
|
61
|
+
## Link Variants (Styling)
|
|
62
|
+
|
|
63
|
+
Mentions are links — style them per URL pattern via `linkVariants` in `markdownStyle`:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
linkVariants: {
|
|
67
|
+
'^user:': { color: '#1264A3', backgroundColor: '#E8F5FB', underline: false },
|
|
68
|
+
'^channel:': { color: '#065F46', backgroundColor: '#D1FAE5', underline: false },
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Each key is a regex tested against the link URL. First match wins. Unspecified properties inherit from the base `link` style. Patterns are auto-sorted longest-first.
|
|
73
|
+
|
|
74
|
+
## Positioning the Suggestion List
|
|
75
|
+
|
|
76
|
+
Use `onCaretRectChange` to get the caret's `{ x, y, width, height }` relative to the input. Combine with the input's position (via `onLayout`) to place a floating popup:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
<View
|
|
80
|
+
style={{
|
|
81
|
+
position: 'absolute',
|
|
82
|
+
left: inputLayout.x + caretRect.x,
|
|
83
|
+
top: inputLayout.y + caretRect.y + caretRect.height + 4,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{/* suggestions */}
|
|
87
|
+
</View>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For simpler layouts, just render the list adjacent to the input without caret tracking.
|
|
91
|
+
|
|
92
|
+
## Behavior Notes
|
|
93
|
+
|
|
94
|
+
- **Atomic deletion**: Backspacing into a mention deletes it entirely (Slack-like).
|
|
95
|
+
- **Debounce**: `onChangeMention` fires every keystroke — debounce network requests.
|
|
96
|
+
- **Toolbar**: Call `focus()` before `startMention()` if the input isn't focused.
|
|
97
|
+
- **URL schemes**: Use custom schemes (`user://`, `channel://`) to distinguish mention types from regular links.
|
package/docs/RTL.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# RTL Support
|
|
2
|
+
|
|
3
|
+
`react-native-enriched-markdown` resolves writing direction **per paragraph** on both platforms: each paragraph picks its own base direction from its first strong directional character. Arabic, Hebrew, and Persian content right-aligns automatically, even inside an LTR app and even when mixed with English paragraphs in the same document.
|
|
4
|
+
|
|
5
|
+
This document describes the read-only [`EnrichedMarkdownText`](TEXT.md) renderer. The rich [`EnrichedMarkdownTextInput`](INPUT.md#rtl-support) follows the same per-paragraph rules — see its [RTL section](INPUT.md#rtl-support) for input-specific caveats (placeholder, code blocks, etc.).
|
|
6
|
+
|
|
7
|
+
## Platform Setup
|
|
8
|
+
|
|
9
|
+
No setup is required. Both platforms autodetect direction per paragraph out of the box.
|
|
10
|
+
|
|
11
|
+
### Android
|
|
12
|
+
|
|
13
|
+
Uses Android's `TEXT_DIRECTION_FIRST_STRONG` heuristic on every `StaticLayout`. Paragraphs with no strong character fall back to the view's resolved layout direction (inherits ancestor `<View style={{ direction: 'rtl' }}>` and `I18nManager.isRTL`).
|
|
14
|
+
|
|
15
|
+
### iOS
|
|
16
|
+
|
|
17
|
+
iOS TextKit's `NSWritingDirectionNatural` does **not** do per-paragraph first-strong — it follows the app's global UI layout direction. The library implements first-strong itself as a post-render pass, matching Android's behavior. The mode is controlled by the [`writingDirection`](#writingdirection-prop-ios) prop and defaults to `'first-strong'`.
|
|
18
|
+
|
|
19
|
+
> **Note:** Earlier versions documented `I18nManager.forceRTL(true)` as a requirement on iOS. That is no longer needed for content direction — `first-strong` resolves each paragraph from its content. `I18nManager.forceRTL` still affects the surrounding app layout (Yoga direction, navigation, etc.) and remains useful if you want the whole app in RTL mode, but it's no longer a precondition for Markdown to render right-aligned.
|
|
20
|
+
|
|
21
|
+
## `writingDirection` prop (iOS)
|
|
22
|
+
|
|
23
|
+
| Value | Behavior |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `'first-strong'` (default) | Per-paragraph autodetection. Neutral-only paragraphs fall back to the view's resolved layout direction. Matches Android. |
|
|
26
|
+
| `'auto'` | React Native parity. TextKit follows the app's `userInterfaceLayoutDirection`; mixed-direction documents do not auto-resolve. |
|
|
27
|
+
| `'ltr'` | Forces LTR on every paragraph. |
|
|
28
|
+
| `'rtl'` | Forces RTL on every paragraph. |
|
|
29
|
+
|
|
30
|
+
Code blocks always render LTR regardless of this prop.
|
|
31
|
+
|
|
32
|
+
Android ignores the prop; it always uses first-strong via the platform.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Default — mixed Arabic/English document renders each paragraph correctly.
|
|
36
|
+
<EnrichedMarkdownText markdown={mixedContent} />
|
|
37
|
+
|
|
38
|
+
// Force RTL for the whole document (e.g. forms or admin UI in an Arabic locale).
|
|
39
|
+
<EnrichedMarkdownText writingDirection="rtl" markdown={content} />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Element RTL Behavior
|
|
43
|
+
|
|
44
|
+
Each element follows the **paragraph it belongs to**, not a global flag — so a single document can mix sides cleanly.
|
|
45
|
+
|
|
46
|
+
| Element | Behavior |
|
|
47
|
+
|---|---|
|
|
48
|
+
| **Paragraphs & headings** | Base direction set per paragraph from first-strong (or forced by the prop). |
|
|
49
|
+
| **Unordered lists** | Bullet drawn on the side that matches the item's paragraph direction. |
|
|
50
|
+
| **Ordered lists** | Number drawn on the side that matches the item's paragraph direction. |
|
|
51
|
+
| **Task lists** | Checkbox drawn on the matching side; tap hit-test follows the same side. |
|
|
52
|
+
| **Blockquotes** | Border drawn on the side that matches the quoted paragraph. |
|
|
53
|
+
| **Tables** (`flavor="github"`) | Each cell resolves its own direction independently from cell content. |
|
|
54
|
+
| **Code blocks** | Always LTR. |
|
|
55
|
+
| **Inline code** | Inherits its paragraph's direction; characters flow correctly via TextKit Bidi. |
|
|
56
|
+
|
|
57
|
+
## Yoga layout-direction inheritance
|
|
58
|
+
|
|
59
|
+
For paragraphs with no strong directional character (digits, punctuation, block spacers), the library falls back to the view's **Yoga-resolved layout direction**. That means:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// "123 456 789." has no strong character. Inside an RTL ancestor view,
|
|
63
|
+
// it right-aligns; otherwise it left-aligns.
|
|
64
|
+
<View style={{ direction: 'rtl' }}>
|
|
65
|
+
<EnrichedMarkdownText markdown="123 456 789." />
|
|
66
|
+
</View>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This mirrors Android's first-strong fallback and lets you place an LTR app in an RTL screen (or vice versa) without surprises for neutral content.
|
|
70
|
+
|
|
71
|
+
## Copy-as-HTML caveat
|
|
72
|
+
|
|
73
|
+
When you copy markdown content to the clipboard, the HTML representation carries a single `dir` attribute on `<html>` (or on `<table>` for table copies). That attribute is read from the document's first paragraph:
|
|
74
|
+
|
|
75
|
+
- `first-strong` document starting with Arabic → `<html dir="rtl">`
|
|
76
|
+
- `first-strong` document starting with English → `<html dir="ltr">`
|
|
77
|
+
- `'auto'` → `<html dir="auto">` (receivers do best-effort first-strong)
|
|
78
|
+
- `'ltr'` / `'rtl'` → forced
|
|
79
|
+
|
|
80
|
+
**Receivers (Gmail, Notes, Word, etc.) apply their own Bidi algorithm to the pasted HTML.** Mixed-direction documents may not visually reproduce the per-paragraph layout you see in-app — receivers can only honor what the markup encodes, and HTML's `dir` attribute is scoped to elements, not paragraphs within them. The plain-text and Markdown clipboard representations are unaffected and round-trip cleanly.
|
|
81
|
+
|
|
82
|
+
If you need precise mixed-direction output in a specific receiver, the rendered HTML carries no per-`<p>` `dir` attribute today — that would be a future enhancement.
|