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,127 @@
|
|
|
1
|
+
# Copy Options
|
|
2
|
+
|
|
3
|
+
When text is selected, `react-native-enriched-markdown` provides enhanced copy functionality through the context menu on both platforms.
|
|
4
|
+
|
|
5
|
+
## Smart Copy
|
|
6
|
+
|
|
7
|
+
The default **Copy** action copies the selected text with rich formatting support:
|
|
8
|
+
|
|
9
|
+
### iOS
|
|
10
|
+
|
|
11
|
+
Copies in multiple formats simultaneously — receiving apps pick the richest format they support:
|
|
12
|
+
|
|
13
|
+
| Format | Description |
|
|
14
|
+
|--------|-------------|
|
|
15
|
+
| **Plain Text** | Basic text without formatting |
|
|
16
|
+
| **Markdown** | Original Markdown syntax preserved |
|
|
17
|
+
| **HTML** | Rich HTML representation |
|
|
18
|
+
| **RTF** | Rich Text Format for apps like Notes, Pages |
|
|
19
|
+
| **RTFD** | RTF with embedded images |
|
|
20
|
+
|
|
21
|
+
### Android
|
|
22
|
+
|
|
23
|
+
Copies as both **Plain Text** and **HTML** — apps that support rich text (like Gmail, Google Docs) will preserve formatting.
|
|
24
|
+
|
|
25
|
+
## Copy as Markdown
|
|
26
|
+
|
|
27
|
+
A dedicated **Copy as Markdown** option is available in the context menu on both platforms. This copies only the Markdown source text, useful when you want to preserve the original syntax.
|
|
28
|
+
|
|
29
|
+
## Copy Image URL
|
|
30
|
+
|
|
31
|
+
When selecting text that contains images, a **Copy Image URL** option appears to copy the image's source URL. On Android, if multiple images are selected, all URLs are copied (one per line).
|
|
32
|
+
|
|
33
|
+
## Controlling Built-in Menu Items
|
|
34
|
+
|
|
35
|
+
Use `selectionMenuConfig` to hide built-in selection menu actions while keeping the native menu and any `contextMenuItems` intact. Each item takes an object — `{ enabled }` toggles visibility:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<EnrichedMarkdownText
|
|
39
|
+
markdown={content}
|
|
40
|
+
selectionMenuConfig={{
|
|
41
|
+
copyAsMarkdown: { enabled: false },
|
|
42
|
+
copyImageUrl: { enabled: false },
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`EnrichedMarkdownTextInput` supports the same `{ enabled, label }` shape. In addition to `copyAsMarkdown`, the input's `selectionMenuConfig` exposes the built-in **Format** submenu, and `formatMenuConfig` controls the items inside it:
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<EnrichedMarkdownTextInput
|
|
51
|
+
selectionMenuConfig={{
|
|
52
|
+
format: { enabled: false },
|
|
53
|
+
copyAsMarkdown: { enabled: false },
|
|
54
|
+
}}
|
|
55
|
+
formatMenuConfig={{
|
|
56
|
+
spoiler: { enabled: false },
|
|
57
|
+
link: { enabled: false },
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Localizing Menu Labels
|
|
63
|
+
|
|
64
|
+
The built-in copy actions are shown in English by default (**Copy**, **Copy as
|
|
65
|
+
Markdown**, **Copy Image URL**). Set a `label` on each `selectionMenuConfig` item
|
|
66
|
+
to translate it so it matches the rest of your app's UI — typically wired to your
|
|
67
|
+
i18n library:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
<EnrichedMarkdownText
|
|
71
|
+
markdown={content}
|
|
72
|
+
selectionMenuConfig={{
|
|
73
|
+
copy: { label: t('copy') }, // "Copia"
|
|
74
|
+
copyAsMarkdown: { label: t('copyAsMarkdown') }, // "Copia come Markdown"
|
|
75
|
+
copyImageUrl: {
|
|
76
|
+
label: t('copyImageUrl'), // "Copia URL immagine" (single image)
|
|
77
|
+
pluralLabels: {
|
|
78
|
+
// Forms for multiple images, chosen with Intl.PluralRules.
|
|
79
|
+
other: t('copyImageUrls'), // "Copia {count} URL immagine"
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Notes:
|
|
87
|
+
|
|
88
|
+
- Any `label` left `undefined` keeps its English default, so you can override
|
|
89
|
+
only the strings you need.
|
|
90
|
+
- `pluralLabels` provides the forms used when several images are selected. The
|
|
91
|
+
right form is picked at runtime from the number of selected images with
|
|
92
|
+
`Intl.PluralRules` (resolved against the app's default locale), keyed by CLDR
|
|
93
|
+
plural category (`zero`, `one`, `two`, `few`, `many`, `other`). Only `other` is
|
|
94
|
+
required; any category left `undefined` falls back to it. The `{count}` token
|
|
95
|
+
is replaced by the number of selected images.
|
|
96
|
+
- The labels apply to the main text selection menu as well as the table and math
|
|
97
|
+
block copy menus.
|
|
98
|
+
- OS-provided actions (Look Up, Translate…) and the system **Cut / Paste /
|
|
99
|
+
Select All** items are localized by the platform and are not affected by this
|
|
100
|
+
config.
|
|
101
|
+
|
|
102
|
+
### Input (`EnrichedMarkdownTextInput`)
|
|
103
|
+
|
|
104
|
+
The input exposes the same `{ enabled, label }` shape on `selectionMenuConfig`
|
|
105
|
+
(for the **Format** submenu title and **Copy as Markdown** action) and on
|
|
106
|
+
`formatMenuConfig` (for each item inside the Format submenu — Bold, Italic,
|
|
107
|
+
Underline, Strikethrough, Spoiler, Link):
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
<EnrichedMarkdownTextInput
|
|
111
|
+
selectionMenuConfig={{
|
|
112
|
+
format: { label: t('format') },
|
|
113
|
+
copyAsMarkdown: { label: t('copyAsMarkdown') },
|
|
114
|
+
}}
|
|
115
|
+
formatMenuConfig={{
|
|
116
|
+
bold: { label: t('bold') },
|
|
117
|
+
italic: { label: t('italic') },
|
|
118
|
+
underline: { label: t('underline') },
|
|
119
|
+
strikethrough: { label: t('strikethrough') },
|
|
120
|
+
spoiler: { label: t('spoiler') },
|
|
121
|
+
link: { label: t('link') },
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> The simplest way to keep these in sync with the device language is to feed the
|
|
127
|
+
> same translation function you already use for the rest of your UI.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Element Structure
|
|
2
|
+
|
|
3
|
+
Markdown elements in `react-native-enriched-markdown` are organized into block and inline categories, each with distinct rendering behaviors.
|
|
4
|
+
|
|
5
|
+
## Supported Markdown Elements
|
|
6
|
+
|
|
7
|
+
`react-native-enriched-markdown` supports a comprehensive set of Markdown elements:
|
|
8
|
+
|
|
9
|
+
### Block Elements
|
|
10
|
+
|
|
11
|
+
| Element | Syntax | Style Property | Description |
|
|
12
|
+
|---------|--------|----------------|-------------|
|
|
13
|
+
| Headings | `# H1` to `###### H6` | `h1` - `h6` | Six levels of headings |
|
|
14
|
+
| Paragraphs | Plain text | `paragraph` | Default text container |
|
|
15
|
+
| Blockquotes | `> Quote` | `blockquote` | Quoted content with accent bar, unlimited nesting |
|
|
16
|
+
| Code Blocks | ` ``` code ``` ` | `codeBlock` | Multi-line code containers |
|
|
17
|
+
| Unordered Lists | `- Item`, `* Item`, or `+ Item` | `list` | Bullet lists with unlimited nesting |
|
|
18
|
+
| Ordered Lists | `1. Item` | `list` | Numbered lists with unlimited nesting |
|
|
19
|
+
| Task Lists | `- [x] Done`, `- [ ] Todo` | `taskList` | Interactive checkboxes (requires `flavor="github"`) |
|
|
20
|
+
| Thematic Break | `---`, `***`, or `___` | `thematicBreak` | Horizontal rule separator |
|
|
21
|
+
| Images | `` | `image` | Block-level images with spacing |
|
|
22
|
+
| Tables | `| col | col |` | `table` | GFM tables with alignment support (requires `flavor="github"`) |
|
|
23
|
+
| Math Block | `$$...$$` | `math` | Block-level LaTeX math (display equations) (requires `flavor="github"`) |
|
|
24
|
+
|
|
25
|
+
### Inline Elements
|
|
26
|
+
|
|
27
|
+
| Element | Syntax | Style Property | Inherits From | Adds |
|
|
28
|
+
|---------|--------|----------------|---------------|------|
|
|
29
|
+
| Bold | `**text**` or `__text__` | `strong` | Parent block | Bold weight, optional color |
|
|
30
|
+
| Italic | `*text*` or `_text_` | `em` | Parent block | Italic style, optional color |
|
|
31
|
+
| Underline | `_text_` | `underline` | Parent block | Underline with custom color (iOS only; requires `md4cFlags`) |
|
|
32
|
+
| Strikethrough | `~~text~~` | `strikethrough` | Parent block | Strike line with custom color (iOS only) |
|
|
33
|
+
| Bold + Italic | `***text***`, `___text___`, etc. | `strong` + `em` | Parent block | Combined emphasis |
|
|
34
|
+
| Links | `[text](url)` | `link` | Parent block | Optional font family, color, underline |
|
|
35
|
+
| Inline Code | `` `code` `` | `code` | Parent block | Monospace font, background, optional fontSize |
|
|
36
|
+
| Inline Images | `` | `inlineImage` | N/A | Inline images within text flow |
|
|
37
|
+
| Inline Math | `$...$` | `inlineMath` | Parent block | LaTeX math rendered within the text flow |
|
|
38
|
+
| Spoiler | `\|\|text\|\|` | `spoiler` | Parent block | Text concealed behind animated particle overlay, tap to reveal. Can wrap inline text or entire blocks (e.g. a full paragraph) |
|
|
39
|
+
| Superscript | `^text^` | `superscript` | Parent block | Raised text at a reduced font size (requires `md4cFlags={{ superscript: true }}`) |
|
|
40
|
+
| Subscript | `~text~` | `subscript` | Parent block | Lowered text at a reduced font size (requires `md4cFlags={{ subscript: true }}`) |
|
|
41
|
+
|
|
42
|
+
> **Note:** Spoiler syntax (`||text||`) is always enabled. Any double-pipe delimiters in your content will be parsed as spoilers — for example, `a || b || c` would render `b` as a spoiler span rather than plain text.
|
|
43
|
+
|
|
44
|
+
> **Note:** Underscore syntax (`__text__`, `_text_`) works for bold/italic by default. Enable underline via `md4cFlags={{ underline: true }}` to treat `_text_` as underline instead of emphasis.
|
|
45
|
+
|
|
46
|
+
> **Note:** Enabling subscript (`md4cFlags={{ subscript: true }}`) changes the behaviour of single tildes — `~text~` becomes subscript instead of strikethrough. Double tildes (`~~text~~`) continue to work as strikethrough regardless.
|
|
47
|
+
|
|
48
|
+
### Nested Lists Example
|
|
49
|
+
|
|
50
|
+
```markdown
|
|
51
|
+
- First level
|
|
52
|
+
- Second level
|
|
53
|
+
- Third level
|
|
54
|
+
- Fourth level (unlimited depth!)
|
|
55
|
+
|
|
56
|
+
1. First item
|
|
57
|
+
1. Nested numbered
|
|
58
|
+
1. Deep nested
|
|
59
|
+
2. Another nested
|
|
60
|
+
2. Second item
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Nested Blockquotes Example
|
|
64
|
+
|
|
65
|
+
```markdown
|
|
66
|
+
> Level 1 quote
|
|
67
|
+
> > Level 2 nested
|
|
68
|
+
> > > Level 3 nested (unlimited depth!)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Superscript and Subscript Examples
|
|
72
|
+
|
|
73
|
+
```markdown
|
|
74
|
+
E = mc^2^
|
|
75
|
+
|
|
76
|
+
H~2~O H~2~SO~4~
|
|
77
|
+
|
|
78
|
+
^14^C dating (isotope notation)
|
|
79
|
+
|
|
80
|
+
H~3~O^+^ (mixed superscript and subscript)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> Superscript and subscript can be nested inside other inline elements such as bold, italic, and links. They cannot be nested inside each other.
|
|
84
|
+
|
|
85
|
+
## Block vs Inline Elements
|
|
86
|
+
|
|
87
|
+
Markdown elements are divided into two categories:
|
|
88
|
+
|
|
89
|
+
### Block Elements
|
|
90
|
+
|
|
91
|
+
Block elements are structural containers that define the layout and establish their own typography context.
|
|
92
|
+
|
|
93
|
+
### Inline Elements
|
|
94
|
+
|
|
95
|
+
Inline elements modify text within blocks and apply additional styling on top of the block's typography.
|
|
96
|
+
|
|
97
|
+
## Images: Block vs Inline
|
|
98
|
+
|
|
99
|
+
Images are automatically detected as block or inline based on context:
|
|
100
|
+
|
|
101
|
+
- **Block images**: When an image is the only content in a paragraph (standalone), it's treated as a block image and uses block-level spacing
|
|
102
|
+
- **Inline images**: When an image appears alongside other text content, it's treated as inline and aligns with the text baseline
|
|
103
|
+
|
|
104
|
+
You don't need to specify which type—the renderer automatically determines this based on the image's position in the content.
|
|
105
|
+
|
|
106
|
+
## Nested Elements
|
|
107
|
+
|
|
108
|
+
Some elements support unlimited nesting depth with automatic indentation:
|
|
109
|
+
|
|
110
|
+
- **Blockquotes**: Each level adds a new accent bar
|
|
111
|
+
- **Unordered Lists**: Each level indents with `marginLeft`
|
|
112
|
+
- **Ordered Lists**: Each level indents and maintains separate numbering
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Image Caching
|
|
2
|
+
|
|
3
|
+
Images in Markdown content are loaded, cached, and reused automatically — no configuration required.
|
|
4
|
+
|
|
5
|
+
## Cache Layers
|
|
6
|
+
|
|
7
|
+
The library uses a three-tier caching strategy on both platforms:
|
|
8
|
+
|
|
9
|
+
| Layer | Android | iOS | Size |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| **Originals (memory)** | `LruCache` | `NSCache` | 20 MB |
|
|
12
|
+
| **Processed variants (memory)** | `LruCache` | `NSCache` | 30 MB |
|
|
13
|
+
| **Disk** | OkHttp `Cache` | `NSURLCache` | 100 MB |
|
|
14
|
+
|
|
15
|
+
- **Original cache** stores decoded images keyed by URL. On Android, large images are downsampled to screen width during decode to reduce peak memory.
|
|
16
|
+
- **Processed cache** stores scaled and clipped variants keyed by URL + dimensions + border radius, so repeated layouts with the same geometry skip all image processing.
|
|
17
|
+
- **Disk cache** persists raw HTTP responses across app launches, respecting standard HTTP cache headers.
|
|
18
|
+
|
|
19
|
+
## Request Deduplication
|
|
20
|
+
|
|
21
|
+
When multiple components request the same image URL simultaneously (e.g. during a re-render), only one network request is made. All pending callbacks are coalesced and dispatched together once the download completes.
|
|
22
|
+
|
|
23
|
+
## Instance Reuse
|
|
24
|
+
|
|
25
|
+
`ImageSpan` (Android) and `ENRMImageAttachment` (iOS) instances are reused across re-renders for the same URL, avoiding redundant object allocation and image reloading.
|
package/docs/INPUT.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# EnrichedMarkdownTextInput
|
|
2
|
+
|
|
3
|
+
`EnrichedMarkdownTextInput` is a rich text input component that outputs Markdown. It is an uncontrolled input — it doesn't use any state or props to store its value, but instead directly interacts with the underlying platform-specific components. Thanks to this, the component is really performant and simple to use.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Here's a simple example of an input that lets you toggle bold on its text and shows whether bold is currently active via the button color.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { useRef, useState } from 'react';
|
|
11
|
+
import { View, Button, StyleSheet } from 'react-native';
|
|
12
|
+
import {
|
|
13
|
+
EnrichedMarkdownTextInput,
|
|
14
|
+
type EnrichedMarkdownTextInputInstance,
|
|
15
|
+
type StyleState,
|
|
16
|
+
} from 'react-native-enriched-markdown';
|
|
17
|
+
|
|
18
|
+
export default function App() {
|
|
19
|
+
const ref = useRef<EnrichedMarkdownTextInputInstance>(null);
|
|
20
|
+
const [state, setState] = useState<StyleState | null>(null);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<View style={styles.container}>
|
|
24
|
+
<EnrichedMarkdownTextInput
|
|
25
|
+
ref={ref}
|
|
26
|
+
placeholder="Type here..."
|
|
27
|
+
onChangeState={setState}
|
|
28
|
+
style={styles.input}
|
|
29
|
+
/>
|
|
30
|
+
<View style={styles.toolbar}>
|
|
31
|
+
<Button
|
|
32
|
+
title={state?.bold.isActive ? 'Unbold' : 'Bold'}
|
|
33
|
+
color={state?.bold.isActive ? 'green' : 'gray'}
|
|
34
|
+
onPress={() => ref.current?.toggleBold()}
|
|
35
|
+
/>
|
|
36
|
+
</View>
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const styles = StyleSheet.create({
|
|
42
|
+
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
|
43
|
+
input: { width: '100%', fontSize: 20, padding: 10, maxHeight: 200, backgroundColor: 'lightgray' },
|
|
44
|
+
toolbar: { flexDirection: 'row', gap: 8, marginTop: 8 },
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Summary of what happens here:
|
|
49
|
+
|
|
50
|
+
1. Any methods imperatively called on the input to e.g. toggle some style must be used through a `ref` of `EnrichedMarkdownTextInputInstance` type. Here, `toggleBold` method that is called on the button press calls `ref.current?.toggleBold()`, which toggles the bold styling within the current selection.
|
|
51
|
+
2. All style state information is emitted by the `onChangeState` callback. The callback payload provides a nested object for each style (e.g., `bold`, `italic`), containing an `isActive` property to guide your UI logic — indicating if the style is currently applied (highlight the button).
|
|
52
|
+
|
|
53
|
+
## Inline Styles
|
|
54
|
+
|
|
55
|
+
Supported styles:
|
|
56
|
+
|
|
57
|
+
- bold
|
|
58
|
+
- italic
|
|
59
|
+
- underline
|
|
60
|
+
- strikethrough
|
|
61
|
+
- spoiler
|
|
62
|
+
|
|
63
|
+
Each of the styles can be toggled the same way as in the example from [usage section](#usage); call a proper `toggle` function on the component ref.
|
|
64
|
+
|
|
65
|
+
Each call toggles the style within the current text selection. They are being toggled on exactly the character range that is currently selected. When toggling the style with just the cursor in place (no selection), the style is ready to be used and will be applied to the next characters that the user inputs.
|
|
66
|
+
|
|
67
|
+
Styles are also available through the built-in native format bar that appears on text selection, and through the system context menu.
|
|
68
|
+
|
|
69
|
+
## Links
|
|
70
|
+
|
|
71
|
+
Links are a piece of text with a URL attributed to it. They can be managed by calling methods on the input ref:
|
|
72
|
+
|
|
73
|
+
- [`setLink(url)`](API_REFERENCE.md#setlinkurl-string) — applies a link to the currently selected text.
|
|
74
|
+
- [`insertLink(text, url)`](API_REFERENCE.md#insertlinktext-string-url-string) — inserts a new link at the cursor position with the given text and URL. Useful when there is no selection.
|
|
75
|
+
- [`removeLink()`](API_REFERENCE.md#removelink) — removes the link from the current selection.
|
|
76
|
+
|
|
77
|
+
The built-in native format bar also includes a link option that presents a URL prompt when text is selected.
|
|
78
|
+
|
|
79
|
+
A complete example of a setup that supports both setting links on the selected text, as well as inserting them at the cursor position can be found in the example app code.
|
|
80
|
+
|
|
81
|
+
## Auto-Link Detection
|
|
82
|
+
|
|
83
|
+
`EnrichedMarkdownTextInput` can automatically detect URLs as the user types and convert them into Markdown links. Detected links are visually styled in the input and serialized as `[text](url)` in the Markdown output.
|
|
84
|
+
|
|
85
|
+
### Basic usage
|
|
86
|
+
|
|
87
|
+
Auto-link detection is enabled by default. URLs like `google.com`, `www.google.com`, and `https://google.com` are detected when followed by a space or newline.
|
|
88
|
+
|
|
89
|
+
Bare domains and `www.` prefixes are automatically normalized with `https://` (e.g., `google.com` becomes `[google.com](https://google.com)`).
|
|
90
|
+
|
|
91
|
+
### Custom regex
|
|
92
|
+
|
|
93
|
+
You can provide a custom regex pattern to control which text is detected as a link:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<EnrichedMarkdownTextInput
|
|
97
|
+
linkRegex={/https?:\/\/[^\s]+/i}
|
|
98
|
+
/>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Pass `null` to disable auto-link detection entirely:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
<EnrichedMarkdownTextInput linkRegex={null} />
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Listening for detections
|
|
108
|
+
|
|
109
|
+
Use the `onLinkDetected` callback to be notified when a new link is detected:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<EnrichedMarkdownTextInput
|
|
113
|
+
onLinkDetected={({ text, url, start, end }) => {
|
|
114
|
+
console.log(`Detected link: ${text} -> ${url} at [${start}, ${end}]`);
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The callback fires only for newly detected links — not for links that were already detected and remain unchanged.
|
|
120
|
+
|
|
121
|
+
### Interaction with manual links
|
|
122
|
+
|
|
123
|
+
When a manual link is applied (via `setLink` or `insertLink`) over an auto-detected link, the auto-detected link is replaced by the manual one. Auto-link detection skips ranges that already contain a manual link.
|
|
124
|
+
|
|
125
|
+
## Caret Position Tracking
|
|
126
|
+
|
|
127
|
+
`EnrichedMarkdownTextInput` can report the caret's pixel position relative to the input, which is useful when the input is embedded in a scrollable container with `scrollEnabled={false}` and you need to keep the caret visible.
|
|
128
|
+
|
|
129
|
+
### `onCaretRectChange`
|
|
130
|
+
|
|
131
|
+
A push-based callback that fires whenever the caret moves (typing, selection change, content reflow). The native side diffs the caret rect before emitting, so redundant events are suppressed automatically.
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
<EnrichedMarkdownTextInput
|
|
135
|
+
scrollEnabled={false}
|
|
136
|
+
onCaretRectChange={(rect) => {
|
|
137
|
+
console.log(rect);
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### `getCaretRect()`
|
|
143
|
+
|
|
144
|
+
An imperative, pull-based method for one-off queries. Returns a Promise that resolves with the current caret rect.
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
const rect = await ref.current?.getCaretRect();
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Mentions
|
|
151
|
+
|
|
152
|
+
`EnrichedMarkdownTextInput` supports mention flows with configurable trigger indicators, lifecycle events for showing suggestion lists, and per-pattern styling via `linkVariants`.
|
|
153
|
+
|
|
154
|
+
See [Mentions](MENTIONS.md) for full documentation on setup, events, ref methods, and styling.
|
|
155
|
+
|
|
156
|
+
## Clipboard
|
|
157
|
+
|
|
158
|
+
The input's content can be copied to the system clipboard from a ref, without requiring the user to select text and open the context menu:
|
|
159
|
+
|
|
160
|
+
- [`copyToClipboard()`](API_REFERENCE.md#copytoclipboard) — copies the full content to the system clipboard, matching the result of selecting all text and pressing the context menu's copy action. The selection is left unchanged, and calling it on an empty input is a no-op.
|
|
161
|
+
|
|
162
|
+
On iOS and macOS the clipboard receives both plain text and a private Markdown pasteboard type, so pasting back into an `EnrichedMarkdownTextInput` restores the formatting; external apps receive plain text only. On Android the clipboard receives plain text only — inline styles are not preserved for any paste target.
|
|
163
|
+
|
|
164
|
+
## Style Detection
|
|
165
|
+
|
|
166
|
+
All of the above styles can be detected with the use of [onChangeState](API_REFERENCE.md#onchangestate) callback payload.
|
|
167
|
+
|
|
168
|
+
You can find some examples in the [usage section](#usage) or in the example app.
|
|
169
|
+
|
|
170
|
+
## Other Events
|
|
171
|
+
|
|
172
|
+
`EnrichedMarkdownTextInput` emits a few more events that may be of use:
|
|
173
|
+
|
|
174
|
+
- [onFocus](API_REFERENCE.md#onfocus-1) - emits whenever input focuses.
|
|
175
|
+
- [onBlur](API_REFERENCE.md#onblur) - emits whenever input blurs.
|
|
176
|
+
- [onChangeText](API_REFERENCE.md#onchangetext) - returns the input's plain text (without Markdown syntax) anytime it changes.
|
|
177
|
+
- [onChangeMarkdown](API_REFERENCE.md#onchangemarkdown) - returns the Markdown string parsed from current input text and styles anytime it would change. As parsing the Markdown on each input change can be expensive, not assigning the event's callback will skip the serialization for better performance.
|
|
178
|
+
- [onChangeSelection](API_REFERENCE.md#onchangeselection) - returns `{ start, end }` of the current selection, useful for working with [links](#links).
|
|
179
|
+
|
|
180
|
+
## RTL Support
|
|
181
|
+
|
|
182
|
+
`EnrichedMarkdownTextInput` resolves writing direction **per paragraph**, matching the read-only [`EnrichedMarkdownText`](RTL.md) renderer. Arabic, Hebrew, and Persian content right-aligns automatically as the user types — even mixed with English paragraphs in the same input.
|
|
183
|
+
|
|
184
|
+
### Platform setup
|
|
185
|
+
|
|
186
|
+
No setup is required. Both platforms autodetect direction per paragraph out of the box.
|
|
187
|
+
|
|
188
|
+
- **Android** — `EditText` resolves direction per paragraph via `View.TEXT_DIRECTION_FIRST_STRONG` (the platform default). The [`writingDirection`](#writingdirection-prop-ios) prop is accepted but has no effect.
|
|
189
|
+
- **iOS** — TextKit's `NSWritingDirectionNatural` follows the app's global UI layout direction and does not do per-paragraph first-strong. The library applies first-strong itself after every formatting pass. The mode is controlled by [`writingDirection`](#writingdirection-prop-ios) and defaults to `'first-strong'`.
|
|
190
|
+
|
|
191
|
+
### `writingDirection` prop (iOS)
|
|
192
|
+
|
|
193
|
+
| Value | Behavior |
|
|
194
|
+
|---|---|
|
|
195
|
+
| `'first-strong'` (default) | Per-paragraph autodetection. Neutral-only paragraphs fall back to the view's resolved layout direction. Matches Android. |
|
|
196
|
+
| `'auto'` | React Native parity. TextKit follows the app's `userInterfaceLayoutDirection`; mixed-direction documents do not auto-resolve. |
|
|
197
|
+
| `'ltr'` | Forces LTR on every paragraph. |
|
|
198
|
+
| `'rtl'` | Forces RTL on every paragraph. |
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
<EnrichedMarkdownTextInput writingDirection="rtl" placeholder="اكتب هنا..." />
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Known limitations
|
|
205
|
+
|
|
206
|
+
- **Placeholder** follows the host view's layout direction, not the prop. If you need an RTL placeholder, wrap the input in `<View style={{ direction: 'rtl' }}>` or set `I18nManager.forceRTL(true)`.
|
|
207
|
+
- **Mixed paragraphs while typing** — newly inserted characters in an empty paragraph briefly inherit the previous paragraph's direction; the first-strong pass corrects this on the next input event.
|
|
208
|
+
- **Code blocks, tables, blockquotes, lists** are not supported in the input (it's a flat inline-formatting surface). For those, use [`EnrichedMarkdownText`](TEXT.md).
|
|
209
|
+
|
|
210
|
+
See [RTL Support](RTL.md) for the full per-element behavior on the rendered output side.
|
|
211
|
+
|
|
212
|
+
## Customizing \<EnrichedMarkdownTextInput /> Styles
|
|
213
|
+
|
|
214
|
+
`EnrichedMarkdownTextInput` accepts a `markdownStyle` prop for customizing how formatted text appears in the input:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
<EnrichedMarkdownTextInput
|
|
218
|
+
markdownStyle={{
|
|
219
|
+
strong: { color: '#1D4ED8' },
|
|
220
|
+
em: { color: '#7C3AED' },
|
|
221
|
+
link: { color: '#2563EB', underline: true },
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Available style properties:
|
|
227
|
+
|
|
228
|
+
- `strong.color` — text color for bold text (defaults to the input's text color).
|
|
229
|
+
- `em.color` — text color for italic text (defaults to the input's text color).
|
|
230
|
+
- `link.color` — text color for links (defaults to `#2563EB`).
|
|
231
|
+
- `link.underline` — whether links are underlined (defaults to `true`).
|
|
232
|
+
- `spoiler.color` — text color for spoiler text.
|
|
233
|
+
- `spoiler.backgroundColor` — background color for spoiler text.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# LaTeX Math
|
|
2
|
+
|
|
3
|
+
LaTeX math rendering is supported for both block and inline equations:
|
|
4
|
+
|
|
5
|
+
- **Block math (`$$...$$`)**: Rendered as a standalone display element. Requires `flavor="github"`.
|
|
6
|
+
- **Inline math (`$...$`)**: Rendered within the text flow. Works with both `flavor="commonmark"` and `flavor="github"`.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
<EnrichedMarkdownText
|
|
12
|
+
flavor="github"
|
|
13
|
+
markdown={`
|
|
14
|
+
The quadratic formula:
|
|
15
|
+
|
|
16
|
+
$$x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}$$
|
|
17
|
+
|
|
18
|
+
Einstein's mass-energy equivalence $E = mc^2$ is one of the most famous equations.
|
|
19
|
+
`}
|
|
20
|
+
markdownStyle={{
|
|
21
|
+
math: {
|
|
22
|
+
fontSize: 20,
|
|
23
|
+
color: '#1F2937',
|
|
24
|
+
backgroundColor: '#F3F4F6',
|
|
25
|
+
padding: 12,
|
|
26
|
+
textAlign: 'center',
|
|
27
|
+
},
|
|
28
|
+
inlineMath: {
|
|
29
|
+
color: '#1F2937',
|
|
30
|
+
},
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Block math equations are rendered as standalone display elements with spacing and an optional background. Inline math inherits the surrounding block's typography.
|
|
36
|
+
|
|
37
|
+
> [!IMPORTANT]
|
|
38
|
+
> LaTeX commands use backslashes (e.g. `\frac`, `\alpha`). In regular JS strings and template literals, backslashes are escape characters. Use `String.raw` or double backslashes (`\\frac`) to preserve them. Block math (`$$...$$`) must be on its own line to render as a display element.
|
|
39
|
+
|
|
40
|
+
## Web
|
|
41
|
+
|
|
42
|
+
On web the library renders LaTeX via [KaTeX](https://katex.org/) in **MathML output mode**. Browsers render MathML natively — no CSS or font files are required.
|
|
43
|
+
|
|
44
|
+
### Installation
|
|
45
|
+
|
|
46
|
+
Install the optional peer dependency:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm install katex
|
|
50
|
+
# or
|
|
51
|
+
yarn add katex
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
KaTeX is loaded lazily the first time a LaTeX node is encountered, so it has no impact on pages that do not render math. No stylesheet or `<link>` tag is needed.
|
|
55
|
+
|
|
56
|
+
> [!NOTE]
|
|
57
|
+
> MathML is supported natively in Chrome 109+, Firefox, and Safari. Older browsers will display the raw LaTeX source as a text fallback.
|
|
58
|
+
|
|
59
|
+
### Disabling on web
|
|
60
|
+
|
|
61
|
+
Pass `latexMath: false` in `md4cFlags` to skip parsing and treat `$` as plain text:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
<EnrichedMarkdownText markdown="Price is $5" md4cFlags={{ latexMath: false }} />
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This also prevents KaTeX from being loaded at runtime.
|
|
68
|
+
|
|
69
|
+
## Disabling LaTeX Math (reducing bundle size)
|
|
70
|
+
|
|
71
|
+
LaTeX math rendering relies on **RaTeX** — a native, KaTeX-compatible math engine — on both iOS and Android. It is included by default but can be excluded to reduce your app's binary size (~3–5 MB on iOS, varies on Android).
|
|
72
|
+
|
|
73
|
+
### 1. Disable at the parser level (JS)
|
|
74
|
+
|
|
75
|
+
Set `latexMath: false` in `md4cFlags` so the parser treats `$` as plain text:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<EnrichedMarkdownText markdown="Price is $5" md4cFlags={{ latexMath: false }} />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This alone prevents math rendering without any native changes. The steps below go further by removing the native math libraries from your binary entirely.
|
|
82
|
+
|
|
83
|
+
### 2. Remove the native iOS dependency
|
|
84
|
+
|
|
85
|
+
Add the following to your Podfile and re-run `pod install`:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
ENV['ENRICHED_MARKDOWN_ENABLE_MATH'] = '0'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This excludes **RaTeX** from the build. Rebuild the app after running `pod install`.
|
|
92
|
+
|
|
93
|
+
> [!NOTE]
|
|
94
|
+
> When math is **enabled** (the default), your Podfile must use dynamic frameworks:
|
|
95
|
+
> ```ruby
|
|
96
|
+
> use_frameworks! :linkage => :dynamic
|
|
97
|
+
> ```
|
|
98
|
+
> This is required for CocoaPods to resolve the RaTeX Swift Package dependency.
|
|
99
|
+
|
|
100
|
+
> [!NOTE]
|
|
101
|
+
> **macOS**: LaTeX math is currently not supported on macOS because `react-native-macos` does not support `use_frameworks!` ([microsoft/react-native-macos#1969](https://github.com/microsoft/react-native-macos/issues/1969)). Math is automatically disabled in the macOS example app.
|
|
102
|
+
|
|
103
|
+
### 3. Remove the native Android dependency
|
|
104
|
+
|
|
105
|
+
Add the following to your project's `gradle.properties`:
|
|
106
|
+
|
|
107
|
+
```properties
|
|
108
|
+
enrichedMarkdown.enableMath=false
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This excludes **RaTeX** from the Android build. Rebuild the app after changing this property.
|
|
112
|
+
|
|
113
|
+
### 4. Expo config plugin
|
|
114
|
+
|
|
115
|
+
If you are using Expo, you can use the built-in config plugin to disable LaTeX math rendering on both platforms at once.
|
|
116
|
+
|
|
117
|
+
Add the following to your `app.json` or `app.config.js`:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"expo": {
|
|
122
|
+
"plugins": [
|
|
123
|
+
[
|
|
124
|
+
"react-native-enriched-markdown",
|
|
125
|
+
{
|
|
126
|
+
"enableMath": false
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This will automatically apply both the [iOS](#2-remove-the-native-ios-dependency) and [Android](#3-remove-the-native-android-dependency) native changes listed above during `npx expo prebuild`.
|
|
135
|
+
|
|
136
|
+
If you later re-enable math (e.g. remove the plugin or set `enableMath: true`), run `npx expo prebuild --clean` so native projects are regenerated without the disable flags, then rebuild.
|
package/docs/MACOS.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# macOS Support
|
|
2
|
+
|
|
3
|
+
`react-native-enriched-markdown` supports macOS via [react-native-macos](https://github.com/microsoft/react-native-macos). The native layer shares code with iOS through a platform abstraction header (`ENRMUIKit.h`), with macOS-specific implementations for context menus, text selection, and clipboard handling.
|
|
4
|
+
|
|
5
|
+
The macOS implementation supports the same rendering elements as iOS — CommonMark, GitHub Flavored Markdown (tables, task lists, strikethrough), images, code blocks, blockquotes, and all other supported elements. `EnrichedMarkdownTextInput` is also available on macOS with full support for inline styles, links, and the native context menu.
|
|
6
|
+
|
|
7
|
+
## Known limitations
|
|
8
|
+
|
|
9
|
+
These will be addressed in upcoming releases:
|
|
10
|
+
|
|
11
|
+
- **LaTeX math** (both inline and block) is not available on macOS. The math engine (RaTeX) is distributed as a Swift Package, which requires `use_frameworks!` in CocoaPods. `react-native-macos` does not currently support `use_frameworks!` due to circular dependencies between `React-Core` and `React-RCTText` ([microsoft/react-native-macos#1969](https://github.com/microsoft/react-native-macos/issues/1969)). Math will be enabled once this upstream issue is resolved.
|
|
12
|
+
- **Tail fade-in animation** falls back to instant reveal (no `CADisplayLink` on macOS)
|
|
13
|
+
- **VoiceOver** accessibility is stubbed (pending `NSAccessibility` implementation)
|
|
14
|
+
- **Font scale observation** does not respond to system font size changes
|
|
15
|
+
- **`selectionColor`** affects only the selection background. The iOS-style caret + handle tinting isn't available on macOS, since AppKit's `NSTextView` doesn't expose them via `tintColor`.
|
|
16
|
+
|
|
17
|
+
## Example app
|
|
18
|
+
|
|
19
|
+
See the [react-native-macos-example/](../apps/react-native-macos-example/) directory for a working example app.
|