react-native-jet-markdown 0.2.1 → 0.2.3-beta.0
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/JetMarkdown.podspec +1 -1
- package/README.md +31 -82
- package/android/src/main/cpp/JniBindings.cpp +0 -106
- package/android/src/main/java/com/jetmarkdown/JetMarkdownNative.kt +0 -79
- package/android/src/main/java/com/jetmarkdown/JetMarkdownPackage.kt +1 -2
- package/android/src/main/java/com/jetmarkdown/JetMarkdownView.kt +11 -11
- package/android/src/main/java/com/jetmarkdown/render/spans/MarkdownLineHeightSpan.kt +2 -13
- package/android/src/main/java/com/jetmarkdown/views/BlockStackView.kt +49 -22
- package/android/src/main/java/com/jetmarkdown/views/BlockTextView.kt +2 -0
- package/android/src/main/java/com/jetmarkdown/views/TableBlockView.kt +11 -3
- package/cpp/react/override/react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h +0 -4
- package/ios/JetMarkdownView.mm +9 -16
- package/ios/measure/JMDMarkdownMeasurer.mm +1 -1
- package/ios/render/JMDBlock.h +7 -0
- package/ios/render/JMDBlock.m +2 -0
- package/ios/render/JMDBlockRenderer.mm +27 -1
- package/ios/render/JMDRenderedContent.mm +36 -11
- package/ios/views/JMDBlockStackView.m +36 -14
- package/ios/views/JMDBlockTextView.h +6 -1
- package/ios/views/JMDBlockTextView.m +90 -127
- package/ios/views/JMDBoxViews.m +14 -11
- package/ios/views/JMDTableView.m +15 -8
- package/lib/module/index.js +0 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +0 -127
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +3 -5
- package/src/index.tsx +0 -9
- package/src/types.ts +0 -134
- package/android/src/main/java/com/jetmarkdown/editor/EditorSpans.kt +0 -217
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorManager.kt +0 -224
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorView.kt +0 -1736
- package/cpp/core/AstToMarkdown.cpp +0 -549
- package/cpp/core/AstToMarkdown.h +0 -16
- package/cpp/core/EditorRuns.cpp +0 -640
- package/cpp/core/EditorRuns.h +0 -94
- package/cpp/core/EditorText.cpp +0 -15
- package/cpp/core/EditorText.h +0 -18
- package/cpp/react/JetMarkdownEditorShadowNode.cpp +0 -54
- package/cpp/react/JetMarkdownEditorShadowNode.h +0 -55
- package/cpp/react/JetMarkdownEditorState.h +0 -35
- package/cpp/tests/parser_tests.cpp +0 -662
- package/cpp/tests/run_tests.sh +0 -25
- package/ios/editor/JetMarkdownEditor.h +0 -12
- package/ios/editor/JetMarkdownEditor.mm +0 -2002
- package/lib/module/JetMarkdownEditor.js +0 -85
- package/lib/module/JetMarkdownEditor.js.map +0 -1
- package/lib/module/JetMarkdownEditor.native.js +0 -244
- package/lib/module/JetMarkdownEditor.native.js.map +0 -1
- package/lib/module/JetMarkdownEditorNativeComponent.ts +0 -173
- package/lib/module/useJetMarkdownEditor.js +0 -41
- package/lib/module/useJetMarkdownEditor.js.map +0 -1
- package/lib/typescript/src/JetMarkdownEditor.d.ts +0 -6
- package/lib/typescript/src/JetMarkdownEditor.d.ts.map +0 -1
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts +0 -6
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts.map +0 -1
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts +0 -105
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts.map +0 -1
- package/lib/typescript/src/useJetMarkdownEditor.d.ts +0 -36
- package/lib/typescript/src/useJetMarkdownEditor.d.ts.map +0 -1
- package/src/JetMarkdownEditor.native.tsx +0 -301
- package/src/JetMarkdownEditor.tsx +0 -90
- package/src/JetMarkdownEditorNativeComponent.ts +0 -173
- package/src/useJetMarkdownEditor.ts +0 -50
package/JetMarkdown.podspec
CHANGED
|
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
|
|
|
11
11
|
s.authors = package["author"]
|
|
12
12
|
|
|
13
13
|
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
-
s.source = { :git => "https://github.com/alizahid/react-native-jet-markdown.git", :tag => "#{s.version}" }
|
|
14
|
+
s.source = { :git => "https://github.com/alizahid/react-native-jet-markdown.git", :tag => "v#{s.version}" }
|
|
15
15
|
|
|
16
16
|
# Headers are deliberately NOT in source_files. Xcode's project-wide
|
|
17
17
|
# header maps index every pod target's headers BY BASENAME, so adding
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img alt="react-native-jet-markdown — native markdown viewer
|
|
2
|
+
<img alt="react-native-jet-markdown — native markdown viewer for React Native" src="https://raw.githubusercontent.com/alizahid/react-native-jet-markdown/main/docs/hero.svg" width="900">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
Fast, fully native Markdown viewer
|
|
6
|
+
Fast, fully native Markdown viewer for React Native
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
@@ -33,6 +33,8 @@ cd ios && pod install
|
|
|
33
33
|
|
|
34
34
|
Requires React Native 0.86+ with the New Architecture (Fabric).
|
|
35
35
|
|
|
36
|
+
> **Android support is experimental.** The Android renderer implements the same feature set as iOS, but it has had far less real-world use. Expect rough edges and please [report](https://github.com/alizahid/react-native-jet-markdown/issues) anything that renders differently from iOS.
|
|
37
|
+
|
|
36
38
|
## Usage
|
|
37
39
|
|
|
38
40
|
```tsx
|
|
@@ -71,20 +73,29 @@ const styles: MarkdownStyles = mergeStyles({
|
|
|
71
73
|
| Prop | Type | Description |
|
|
72
74
|
| --- | --- | --- |
|
|
73
75
|
| `markdown` | `string` | The markdown source. |
|
|
74
|
-
| `style` | `MarkdownContainerStyle` |
|
|
75
|
-
| `styles` | `MarkdownStyles` | Per-element styles
|
|
76
|
-
| `images` | `{ url, width, height }[]` | Pre-sizing data. Listed images lay out at their final size immediately
|
|
76
|
+
| `style` | `MarkdownContainerStyle` | Container box (`backgroundColor`, `padding*`, `gap`) plus base text styles that cascade into every element. See [Container style](#container-style). |
|
|
77
|
+
| `styles` | `MarkdownStyles` | Per-element styles. See [Styling](#styling). Hoist to module scope or memoize. |
|
|
78
|
+
| `images` | `{ url, width, height }[]` | Pre-sizing data. Listed images lay out at their final size immediately (zero layout shift); unlisted ones render a full-width, 200pt-tall placeholder and snap to their real aspect once loaded. |
|
|
79
|
+
| `allowFontScaling` | `boolean` | Scale all text, including `lineHeight`, with the system font size setting. Default `true`. |
|
|
77
80
|
| `onLinkPress` | `({ url }) => void` | Link or mention tapped. Mentions arrive with their scheme (e.g. `users://ali`). |
|
|
78
81
|
| `onLinkLongPress` | `({ url }) => void` | Link long-pressed. |
|
|
79
82
|
| `onImagePress` | `({ url, x, y, width, height }) => void` | Image tapped. `width`/`height` are the rendered size and `x`/`y` the position relative to the screen (dp) — everything a lightbox needs for a zoom-from-thumbnail transition. |
|
|
80
83
|
|
|
81
84
|
## Styling
|
|
82
85
|
|
|
83
|
-
The viewer ships **unstyled by default
|
|
86
|
+
The viewer ships **unstyled by default**: with no `styles` prop the output is plain text (only bold/italic runs, monospace code, list markers, and the spoiler cover survive). Two exports cover the common cases:
|
|
84
87
|
|
|
85
88
|
- `defaultStyles` — a plain `MarkdownStyles` object with the classic markdown look (heading scale, blue links, code boxes, quote bar, table separators). It's just data: spread it, fork it, or use it as a reference.
|
|
86
89
|
- `mergeStyles(overrides)` — deep-merges your overrides into `defaultStyles` (element sections merge key-by-key, heading levels individually).
|
|
87
90
|
|
|
91
|
+
### Container style
|
|
92
|
+
|
|
93
|
+
The `style` prop styles the container itself: `backgroundColor`, `padding` (+ `paddingHorizontal`/`paddingVertical` and per-side), and `gap` (spacing between blocks; wins over `styles.gap`). Text keys on it (`fontSize`, `fontWeight`, `fontFamily`, `color`, `lineHeight`, `fontVariant`, `textDecoration*`) are the base of the cascade: every element inherits them unless its own section overrides. Element builtins survive the cascade — heading sizes and weights stay unless `headings.hN` overrides, and code keeps its monospace font unless `codeBlock` overrides.
|
|
94
|
+
|
|
95
|
+
All of these are measured natively. For outer layout (margin, width, flex), wrap the viewer in a `View`.
|
|
96
|
+
|
|
97
|
+
### Element styles
|
|
98
|
+
|
|
88
99
|
Two shared shapes compose every element style:
|
|
89
100
|
|
|
90
101
|
**Text** — `fontSize`, `fontWeight`, `fontFamily`, `color`, `backgroundColor` (run highlight), `fontVariant`, `lineHeight`, `textDecorationColor`, `textDecorationLine`, `textDecorationStyle`
|
|
@@ -125,6 +136,10 @@ Each column gets its natural (unwrapped) width, clamped to `[minColumnWidth, max
|
|
|
125
136
|
|
|
126
137
|
Mentions are plain markdown links with custom schemes — `[@ali](users://ali)`, `[#general](channels://general)` — classified by the `mention.variants` regexes and styled accordingly. Presses arrive through `onLinkPress`; branch on the URL scheme.
|
|
127
138
|
|
|
139
|
+
### Images
|
|
140
|
+
|
|
141
|
+
A paragraph containing only an image renders as a block image, aspect-fit to the container width. Pass `images` to pre-size known images and avoid layout shift. Loading runs on SDWebImage (iOS) and Glide (Android) — the same cores expo-image uses — with memory + disk caches, request dedupe, and animated GIF playback (plus APNG on iOS).
|
|
142
|
+
|
|
128
143
|
## Using in lists
|
|
129
144
|
|
|
130
145
|
Rendering hundreds of viewers in FlatList / FlashList / LegendList is a first-class use case:
|
|
@@ -150,100 +165,34 @@ Numbers from the shared C++ core (Apple M-series, `-O2`; the same code runs on d
|
|
|
150
165
|
| --- | --- | --- |
|
|
151
166
|
| Parse a typical feed post | 1.5 KB mixed markdown | **35 µs** |
|
|
152
167
|
| Parse a large document | 160 KB | **4.1 ms** |
|
|
153
|
-
| Open a document in the editor (`editorFromMarkdown`) | 160 KB, ~6,400 style runs | **4.7 ms** |
|
|
154
|
-
| Serialize the editor back to markdown | 160 KB | **5.8 ms** |
|
|
155
168
|
| Serialize the full `defaultStyles` object in JS | 980 B JSON | **3 µs**, memoized |
|
|
156
169
|
|
|
157
|
-
Beyond raw parsing: measurement runs on the Fabric layout thread (never on main), parse + layout results are cached and shared between measure and mount
|
|
170
|
+
Beyond raw parsing: measurement runs on the Fabric layout thread (never on main), and parse + layout results are cached and shared between measure and mount. The 500-item Feed screen in the example app scrolls at 60 fps on both platforms.
|
|
158
171
|
|
|
159
172
|
### Why styles cross as JSON
|
|
160
173
|
|
|
161
174
|
You may notice styles ship natively as one `stylesJson` string instead of a structured object. This is deliberate, not legacy: Fabric props cross via JSI either way (there is no old-bridge serialization in either design), codegen cannot express the open-keyed `mention.variants` map, and the string doubles as the cache key shared by the C++ measurer, iOS, and Android — a structured prop would need deep equality or native re-serialization to get the same caching. The cost is ~3 µs once per distinct styles object.
|
|
162
175
|
|
|
163
|
-
## Editor
|
|
164
|
-
|
|
165
|
-
> [!WARNING]
|
|
166
|
-
> The editor is not ready for production use yet. The viewer (`JetMarkdownView`) is stable; `JetMarkdownEditor` still has known issues and its API may change. Track progress in the issue tracker before shipping it.
|
|
167
|
-
|
|
168
|
-
`JetMarkdownEditor` is a WYSIWYG editor with markdown as the interchange format: no visible syntax while editing, `onChangeMarkdown` fires with serialized markdown on every edit, and `setValue`/`defaultValue`/paste parse markdown into styled content.
|
|
169
|
-
|
|
170
|
-
```tsx
|
|
171
|
-
import { JetMarkdownEditor, useJetMarkdownEditor } from "react-native-jet-markdown";
|
|
172
|
-
|
|
173
|
-
function Compose() {
|
|
174
|
-
const editor = useJetMarkdownEditor();
|
|
175
|
-
|
|
176
|
-
return (
|
|
177
|
-
<>
|
|
178
|
-
<JetMarkdownEditor
|
|
179
|
-
mentionTriggers={["@"]}
|
|
180
|
-
onChangeMarkdown={(markdown) => save(markdown)}
|
|
181
|
-
onMentionChange={({ query }) => search(query)}
|
|
182
|
-
placeholder="Write something..."
|
|
183
|
-
ref={editor.ref}
|
|
184
|
-
/>
|
|
185
|
-
<Button onPress={editor.toggleBold} title="B" />
|
|
186
|
-
</>
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Formatting
|
|
192
|
-
|
|
193
|
-
Inline marks — bold, italic, strikethrough, inline code, spoiler, superscript, subscript — toggle over the selection, or arm for the text typed next at a collapsed cursor (`toggleBold()`, `toggleItalic()`, `toggleStrikethrough()`, `toggleCode()`, `toggleSpoiler()`, `toggleSuperscript()`, `toggleSubscript()`).
|
|
194
|
-
|
|
195
|
-
Blocks apply per line: `toggleHeading(1-6)`, `toggleBlockQuote()`, `toggleCodeBlock()`, `toggleUnorderedList()`, `toggleOrderedList()`. Enter continues a list, Enter on an empty item exits it, Enter after a heading returns to paragraph, and backspace at the start of a formatted line clears its block first.
|
|
196
|
-
|
|
197
|
-
`onChangeState` reports the formatting at the cursor or selection (`isBold`, `headingLevel`, `isUnorderedList`, ...) for toolbar highlighting.
|
|
198
|
-
|
|
199
|
-
### Links & mentions
|
|
200
|
-
|
|
201
|
-
`insertLink(url, label?)` links the selection (or inserts a linked label), `removeLink()` strips it. Typing a bare `http(s)://` URL followed by a space emits `onLinkDetected` — call `insertLink` if you want it linkified.
|
|
202
|
-
|
|
203
|
-
Mentions start when a `mentionTriggers` character is typed at a word start: `onMentionStart` → `onMentionChange` (with the growing query) → `onMentionEnd`. Call `insertMention(trigger, label, url)` — e.g. `("@", "ali", "users://ali")` — to replace the query with an atomic token that deletes as one unit.
|
|
204
|
-
|
|
205
|
-
### Paste
|
|
206
|
-
|
|
207
|
-
Paste never inserts directly. `onPaste` receives `{ text?, images?, preventDefault() }` — images are reported (never auto-inserted) with `url`, `width`, `height`. Unless you call `preventDefault()` synchronously, the pasted text is parsed as markdown and inserted styled.
|
|
208
|
-
|
|
209
|
-
### Everything else
|
|
210
|
-
|
|
211
|
-
`getMarkdown()` resolves the current markdown; `focus()`, `blur()`, `setSelection(start, end)`, `setValue(markdown)`, and `insertMarkdown(markdown)` do what they say. Hardware keyboards get Cmd+B / Cmd+I (iOS) and Ctrl+B / Ctrl+I (Android). The editor shares the viewer's `style` / `styles` contract — the `base`/`paragraph` cascade drives the root text and `link.color` drives links. The viewer's `gap` is not applied while editing (a text field flows line to line); drive the editing feel with the text styles themselves.
|
|
212
|
-
|
|
213
176
|
## Platform notes & limitations
|
|
214
177
|
|
|
215
178
|
- New Architecture (Fabric) only; React Native 0.86+.
|
|
179
|
+
- Android support is experimental; iOS is the reference implementation.
|
|
216
180
|
- `textDecorationStyle`/`textDecorationColor` render fully on iOS; Android draws plain underline/strikethrough.
|
|
217
181
|
- `borderCurve: 'continuous'` is iOS-only.
|
|
218
182
|
- `fontVariant` supports `tabular-nums`, `proportional-nums`, `oldstyle-nums`, `lining-nums`, `small-caps` (font support required).
|
|
219
|
-
- `allowFontScaling` (default `true`) scales all text — including `lineHeight` — with the system font size setting on both components.
|
|
220
183
|
- Spoilers and inline formatting inside link labels stay literal; spoilers inside table cells are unsupported (the `|` delimiter conflicts).
|
|
221
184
|
- Code blocks render plain monospace (no syntax highlighting).
|
|
185
|
+
- The viewer fills the width it is given and sizes its own height. Under an unconstrained width (a horizontal `ScrollView`, `alignSelf: 'flex-start'`) it has nothing to wrap against and renders empty — give it a definite width.
|
|
222
186
|
|
|
223
187
|
## Sponsors
|
|
224
188
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
<b>Duet</b>
|
|
233
|
-
</a>
|
|
234
|
-
<br>
|
|
235
|
-
<sub>Your AI coworker that runs your business 24/7</sub>
|
|
236
|
-
</td>
|
|
237
|
-
<td align="center" width="200">
|
|
238
|
-
<a href="https://acorn.blue">
|
|
239
|
-
<img alt="Acorn" src="https://github.com/alizahid/react-native-jet-markdown/blob/main/.github/acorn.png?raw=true" width="96"><br>
|
|
240
|
-
<b>Acorn</b>
|
|
241
|
-
</a>
|
|
242
|
-
<br>
|
|
243
|
-
<sub>Reddit for mobile</sub>
|
|
244
|
-
</td>
|
|
245
|
-
</tr>
|
|
246
|
-
</table>
|
|
189
|
+
<p align="center">
|
|
190
|
+
<a href="https://acorn.blue"><img alt="Acorn" src="https://acorn.blue/images/acorn.png" width="96"></a>
|
|
191
|
+
</p>
|
|
192
|
+
|
|
193
|
+
<p align="center">
|
|
194
|
+
Built for and sponsored by <a href="https://acorn.blue">Acorn</a>, a Reddit client for iOS.
|
|
195
|
+
</p>
|
|
247
196
|
|
|
248
197
|
## License
|
|
249
198
|
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
#include <vector>
|
|
13
13
|
|
|
14
14
|
#include "core/AstSerializer.h"
|
|
15
|
-
#include "core/EditorRuns.h"
|
|
16
15
|
#include "core/Parser.h"
|
|
17
16
|
#include "react/JetMarkdownMeasurer.h"
|
|
18
17
|
|
|
@@ -87,111 +86,6 @@ Java_com_jetmarkdown_JetMarkdownNative_parse(JNIEnv* env, jclass, jbyteArray mar
|
|
|
87
86
|
return toByteArray(env, bytes.data(), bytes.size());
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
extern "C" JNIEXPORT jbyteArray JNICALL
|
|
91
|
-
Java_com_jetmarkdown_JetMarkdownNative_markdownFromEditorContent(
|
|
92
|
-
JNIEnv* env,
|
|
93
|
-
jclass,
|
|
94
|
-
jbyteArray text,
|
|
95
|
-
jintArray runs,
|
|
96
|
-
jintArray lineBlocks,
|
|
97
|
-
jintArray linkRanges,
|
|
98
|
-
jbyteArray linkUrls) {
|
|
99
|
-
std::vector<jetmarkdown::StyledRun> styledRuns;
|
|
100
|
-
if (runs != nullptr) {
|
|
101
|
-
const jsize length = env->GetArrayLength(runs);
|
|
102
|
-
std::vector<jint> values(static_cast<size_t>(length));
|
|
103
|
-
if (length > 0) {
|
|
104
|
-
env->GetIntArrayRegion(runs, 0, length, values.data());
|
|
105
|
-
}
|
|
106
|
-
for (jsize i = 0; i + 2 < length; i += 3) {
|
|
107
|
-
styledRuns.push_back(
|
|
108
|
-
{static_cast<uint32_t>(values[i]),
|
|
109
|
-
static_cast<uint32_t>(values[i + 1]),
|
|
110
|
-
static_cast<uint32_t>(values[i + 2])});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
std::vector<jetmarkdown::EditorLine> lines;
|
|
114
|
-
if (lineBlocks != nullptr) {
|
|
115
|
-
const jsize length = env->GetArrayLength(lineBlocks);
|
|
116
|
-
std::vector<jint> values(static_cast<size_t>(length));
|
|
117
|
-
if (length > 0) {
|
|
118
|
-
env->GetIntArrayRegion(lineBlocks, 0, length, values.data());
|
|
119
|
-
}
|
|
120
|
-
for (jsize i = 0; i + 1 < length; i += 2) {
|
|
121
|
-
lines.push_back(
|
|
122
|
-
{static_cast<jetmarkdown::EditorBlockType>(values[i]),
|
|
123
|
-
static_cast<uint8_t>(values[i + 1])});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// Link URLs cross as one newline-joined blob (URLs cannot contain '\n').
|
|
127
|
-
std::vector<jetmarkdown::LinkRun> links;
|
|
128
|
-
if (linkRanges != nullptr) {
|
|
129
|
-
const jsize length = env->GetArrayLength(linkRanges);
|
|
130
|
-
std::vector<jint> values(static_cast<size_t>(length));
|
|
131
|
-
if (length > 0) {
|
|
132
|
-
env->GetIntArrayRegion(linkRanges, 0, length, values.data());
|
|
133
|
-
}
|
|
134
|
-
const std::string urls = toStdString(env, linkUrls);
|
|
135
|
-
size_t urlStart = 0;
|
|
136
|
-
for (jsize i = 0; i + 1 < length; i += 2) {
|
|
137
|
-
const size_t urlEnd = urls.find('\n', urlStart);
|
|
138
|
-
links.push_back(
|
|
139
|
-
{static_cast<uint32_t>(values[i]),
|
|
140
|
-
static_cast<uint32_t>(values[i + 1]),
|
|
141
|
-
urls.substr(urlStart, urlEnd == std::string::npos ? std::string::npos
|
|
142
|
-
: urlEnd - urlStart)});
|
|
143
|
-
urlStart = urlEnd == std::string::npos ? urls.size() : urlEnd + 1;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
const std::string result = jetmarkdown::markdownFromEditor(
|
|
147
|
-
toStdString(env, text), styledRuns, lines, links);
|
|
148
|
-
return toByteArray(
|
|
149
|
-
env, reinterpret_cast<const uint8_t*>(result.data()), result.size());
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
extern "C" JNIEXPORT jbyteArray JNICALL
|
|
153
|
-
Java_com_jetmarkdown_JetMarkdownNative_editorFromMarkdownContent(
|
|
154
|
-
JNIEnv* env,
|
|
155
|
-
jclass,
|
|
156
|
-
jbyteArray markdown) {
|
|
157
|
-
const jetmarkdown::EditorDocument document =
|
|
158
|
-
jetmarkdown::editorFromMarkdown(toStdString(env, markdown));
|
|
159
|
-
// [int32 runCount][runCount x (start, end, flags)][int32 lineCount]
|
|
160
|
-
// [lineCount x (type, level)][utf8 text], little-endian.
|
|
161
|
-
std::vector<uint8_t> bytes;
|
|
162
|
-
bytes.reserve(
|
|
163
|
-
8 + document.runs.size() * 12 + document.lines.size() * 8 +
|
|
164
|
-
document.text.size());
|
|
165
|
-
const auto push32 = [&bytes](uint32_t value) {
|
|
166
|
-
bytes.push_back(static_cast<uint8_t>(value & 0xFF));
|
|
167
|
-
bytes.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
|
|
168
|
-
bytes.push_back(static_cast<uint8_t>((value >> 16) & 0xFF));
|
|
169
|
-
bytes.push_back(static_cast<uint8_t>((value >> 24) & 0xFF));
|
|
170
|
-
};
|
|
171
|
-
push32(static_cast<uint32_t>(document.runs.size()));
|
|
172
|
-
for (const jetmarkdown::StyledRun& run : document.runs) {
|
|
173
|
-
push32(run.start);
|
|
174
|
-
push32(run.end);
|
|
175
|
-
push32(run.flags);
|
|
176
|
-
}
|
|
177
|
-
push32(static_cast<uint32_t>(document.lines.size()));
|
|
178
|
-
for (const jetmarkdown::EditorLine& line : document.lines) {
|
|
179
|
-
push32(static_cast<uint32_t>(line.type));
|
|
180
|
-
push32(line.level);
|
|
181
|
-
}
|
|
182
|
-
push32(static_cast<uint32_t>(document.links.size()));
|
|
183
|
-
for (const jetmarkdown::LinkRun& link : document.links) {
|
|
184
|
-
push32(link.start);
|
|
185
|
-
push32(link.end);
|
|
186
|
-
push32(static_cast<uint32_t>(link.url.size()));
|
|
187
|
-
}
|
|
188
|
-
for (const jetmarkdown::LinkRun& link : document.links) {
|
|
189
|
-
bytes.insert(bytes.end(), link.url.begin(), link.url.end());
|
|
190
|
-
}
|
|
191
|
-
bytes.insert(bytes.end(), document.text.begin(), document.text.end());
|
|
192
|
-
return toByteArray(env, bytes.data(), bytes.size());
|
|
193
|
-
}
|
|
194
|
-
|
|
195
89
|
extern "C" JNIEXPORT void JNICALL
|
|
196
90
|
Java_com_jetmarkdown_JetMarkdownNative_installMeasurer(
|
|
197
91
|
JNIEnv* env,
|
|
@@ -29,86 +29,7 @@ object JetMarkdownNative {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
/** Decoded editor content: text, inline-mark runs, line blocks, links. */
|
|
33
|
-
class EditorContent(
|
|
34
|
-
val text: String,
|
|
35
|
-
/** Flat `[start, end, flags]` triples in UTF-16 offsets. */
|
|
36
|
-
val runs: IntArray,
|
|
37
|
-
/** Flat `[type, level]` pairs, one per text line. */
|
|
38
|
-
val lineBlocks: IntArray,
|
|
39
|
-
/** Flat `[start, end]` pairs, aligned with [linkUrls]. */
|
|
40
|
-
val linkRanges: IntArray,
|
|
41
|
-
val linkUrls: List<String>,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Editor: markdown from text + inline-mark runs + per-line blocks +
|
|
46
|
-
* links. Runs are flat `[start, end, flags]` triples in UTF-16 offsets
|
|
47
|
-
* (Spannable indices); lineBlocks are `[type, level]` pairs per line;
|
|
48
|
-
* links are `[start, end]` pairs with URLs joined by '\n'.
|
|
49
|
-
*/
|
|
50
|
-
fun markdownFromEditor(
|
|
51
|
-
text: String,
|
|
52
|
-
runs: IntArray,
|
|
53
|
-
lineBlocks: IntArray,
|
|
54
|
-
linkRanges: IntArray = IntArray(0),
|
|
55
|
-
linkUrls: List<String> = emptyList(),
|
|
56
|
-
): String {
|
|
57
|
-
return markdownFromEditorContent(
|
|
58
|
-
text.toByteArray(Charsets.UTF_8),
|
|
59
|
-
runs,
|
|
60
|
-
lineBlocks,
|
|
61
|
-
linkRanges,
|
|
62
|
-
linkUrls.joinToString("\n").toByteArray(Charsets.UTF_8),
|
|
63
|
-
).toString(Charsets.UTF_8)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Editor: markdown parsed into editor content. The native payload is
|
|
68
|
-
* `[int32 runCount][triples][int32 lineCount][pairs][int32 linkCount]
|
|
69
|
-
* [(start, end, urlLen) triples][url bytes][utf8 text]`, little-endian.
|
|
70
|
-
*/
|
|
71
|
-
fun editorFromMarkdown(markdown: String): EditorContent {
|
|
72
|
-
val bytes = editorFromMarkdownContent(markdown.toByteArray(Charsets.UTF_8))
|
|
73
|
-
val buffer = java.nio.ByteBuffer.wrap(bytes).order(java.nio.ByteOrder.LITTLE_ENDIAN)
|
|
74
|
-
val runs = IntArray(buffer.int * 3)
|
|
75
|
-
for (i in runs.indices) {
|
|
76
|
-
runs[i] = buffer.int
|
|
77
|
-
}
|
|
78
|
-
val lineBlocks = IntArray(buffer.int * 2)
|
|
79
|
-
for (i in lineBlocks.indices) {
|
|
80
|
-
lineBlocks[i] = buffer.int
|
|
81
|
-
}
|
|
82
|
-
val linkCount = buffer.int
|
|
83
|
-
val linkRanges = IntArray(linkCount * 2)
|
|
84
|
-
val urlLengths = IntArray(linkCount)
|
|
85
|
-
for (i in 0 until linkCount) {
|
|
86
|
-
linkRanges[i * 2] = buffer.int
|
|
87
|
-
linkRanges[i * 2 + 1] = buffer.int
|
|
88
|
-
urlLengths[i] = buffer.int
|
|
89
|
-
}
|
|
90
|
-
val linkUrls = ArrayList<String>(linkCount)
|
|
91
|
-
for (i in 0 until linkCount) {
|
|
92
|
-
val urlBytes = ByteArray(urlLengths[i])
|
|
93
|
-
buffer.get(urlBytes)
|
|
94
|
-
linkUrls.add(urlBytes.toString(Charsets.UTF_8))
|
|
95
|
-
}
|
|
96
|
-
val text = ByteArray(buffer.remaining())
|
|
97
|
-
buffer.get(text)
|
|
98
|
-
return EditorContent(text.toString(Charsets.UTF_8), runs, lineBlocks, linkRanges, linkUrls)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
32
|
@JvmStatic private external fun parse(markdown: ByteArray): ByteArray
|
|
102
33
|
|
|
103
|
-
@JvmStatic private external fun markdownFromEditorContent(
|
|
104
|
-
text: ByteArray,
|
|
105
|
-
runs: IntArray,
|
|
106
|
-
lineBlocks: IntArray,
|
|
107
|
-
linkRanges: IntArray,
|
|
108
|
-
linkUrls: ByteArray,
|
|
109
|
-
): ByteArray
|
|
110
|
-
|
|
111
|
-
@JvmStatic private external fun editorFromMarkdownContent(markdown: ByteArray): ByteArray
|
|
112
|
-
|
|
113
34
|
@JvmStatic private external fun installMeasurer(measurer: MarkdownMeasurer)
|
|
114
35
|
}
|
|
@@ -5,7 +5,6 @@ import com.facebook.react.bridge.NativeModule
|
|
|
5
5
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
6
|
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
7
|
import com.facebook.react.uimanager.ViewManager
|
|
8
|
-
import com.jetmarkdown.editor.JetMarkdownEditorManager
|
|
9
8
|
|
|
10
9
|
class JetMarkdownViewPackage : BaseReactPackage() {
|
|
11
10
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
@@ -13,7 +12,7 @@ class JetMarkdownViewPackage : BaseReactPackage() {
|
|
|
13
12
|
// font/color resolution must work from the very first measure. The view
|
|
14
13
|
// managers re-install with the themed context on view creation.
|
|
15
14
|
com.jetmarkdown.style.PlatformColorResolver.install(reactContext)
|
|
16
|
-
return listOf(JetMarkdownViewManager()
|
|
15
|
+
return listOf(JetMarkdownViewManager())
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
|
|
@@ -10,6 +10,7 @@ import com.facebook.react.uimanager.StateWrapper
|
|
|
10
10
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
11
11
|
import com.facebook.react.uimanager.events.Event
|
|
12
12
|
import com.jetmarkdown.render.ContentCache
|
|
13
|
+
import com.jetmarkdown.render.RenderedContent
|
|
13
14
|
import com.jetmarkdown.style.StyleConfig
|
|
14
15
|
import com.jetmarkdown.views.BlockStackView
|
|
15
16
|
import com.jetmarkdown.views.MarkdownHost
|
|
@@ -27,12 +28,14 @@ class JetMarkdownView(context: Context) : ViewGroup(context), MarkdownHost {
|
|
|
27
28
|
set(value) {
|
|
28
29
|
if (field != value) {
|
|
29
30
|
field = value
|
|
30
|
-
|
|
31
|
+
boundLayout = null
|
|
31
32
|
requestLayout()
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// Layouts are cached per (content, width, image sizes), and content per
|
|
36
|
+
// (markdown, styles, font scale, appearance): identity says whether the
|
|
37
|
+
// bound view tree is still current.
|
|
38
|
+
private var boundLayout: RenderedContent.WidthLayout? = null
|
|
36
39
|
private val stack = BlockStackView(context)
|
|
37
40
|
|
|
38
41
|
/** url -> [w, h] dp: from the images prop (wins) and loaded bitmaps. */
|
|
@@ -53,8 +56,7 @@ class JetMarkdownView(context: Context) : ViewGroup(context), MarkdownHost {
|
|
|
53
56
|
stylesJson = ""
|
|
54
57
|
allowFontScaling = true
|
|
55
58
|
stateWrapper = null
|
|
56
|
-
|
|
57
|
-
boundWidth = 0
|
|
59
|
+
boundLayout = null
|
|
58
60
|
propImageSizes.clear()
|
|
59
61
|
loadedImageSizes.clear()
|
|
60
62
|
revealedSpoilers.clear()
|
|
@@ -200,9 +202,9 @@ class JetMarkdownView(context: Context) : ViewGroup(context), MarkdownHost {
|
|
|
200
202
|
val paddingRightPx = (styles.paddingRight * density).toInt()
|
|
201
203
|
val paddingTopPx = (styles.paddingTop * density).toInt()
|
|
202
204
|
val contentWidthPx = (r - l) - paddingLeftPx - paddingRightPx
|
|
203
|
-
if (contentWidthPx <= 0
|
|
205
|
+
if (contentWidthPx <= 0) {
|
|
204
206
|
stack.setBlocks(emptyList(), 0f)
|
|
205
|
-
|
|
207
|
+
boundLayout = null
|
|
206
208
|
return
|
|
207
209
|
}
|
|
208
210
|
|
|
@@ -210,10 +212,8 @@ class JetMarkdownView(context: Context) : ViewGroup(context), MarkdownHost {
|
|
|
210
212
|
val imageSizes = mergedImageSizes()
|
|
211
213
|
val layout = content.layoutFor(contentWidthPx, imageSizes)
|
|
212
214
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
boundKey = key
|
|
216
|
-
boundWidth = contentWidthPx
|
|
215
|
+
if (layout !== boundLayout) {
|
|
216
|
+
boundLayout = layout
|
|
217
217
|
stack.setBlocks(layout.measured, content.gap)
|
|
218
218
|
}
|
|
219
219
|
|
|
@@ -1,26 +1,15 @@
|
|
|
1
1
|
package com.jetmarkdown.render.spans
|
|
2
2
|
|
|
3
3
|
import android.graphics.Paint
|
|
4
|
-
import android.text.TextPaint
|
|
5
4
|
import android.text.style.LineHeightSpan
|
|
6
|
-
import android.text.style.MetricAffectingSpan
|
|
7
5
|
import kotlin.math.ceil
|
|
8
6
|
import kotlin.math.floor
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
9
|
* React Native's lineHeight semantics: the line box is exactly [heightPx]
|
|
12
|
-
* tall with glyphs centered (mirrors RN's CustomLineHeightSpan).
|
|
13
|
-
* MetricAffectingSpan (a no-op there) so DynamicLayout — the editor —
|
|
14
|
-
* reflows when the span is attached; plain LineHeightSpan is not an
|
|
15
|
-
* UpdateLayout span.
|
|
10
|
+
* tall with glyphs centered (mirrors RN's CustomLineHeightSpan).
|
|
16
11
|
*/
|
|
17
|
-
|
|
18
|
-
MetricAffectingSpan(),
|
|
19
|
-
LineHeightSpan {
|
|
20
|
-
override fun updateMeasureState(paint: TextPaint) = Unit
|
|
21
|
-
|
|
22
|
-
override fun updateDrawState(paint: TextPaint) = Unit
|
|
23
|
-
|
|
12
|
+
class MarkdownLineHeightSpan(private val heightPx: Int) : LineHeightSpan {
|
|
24
13
|
override fun chooseHeight(
|
|
25
14
|
text: CharSequence,
|
|
26
15
|
start: Int,
|
|
@@ -17,45 +17,63 @@ class BlockStackView(context: Context) : ViewGroup(context) {
|
|
|
17
17
|
|
|
18
18
|
var host: MarkdownHost? = null
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
// Discard hook for views dropped for good: stop in-flight image requests
|
|
21
|
+
// from delivering into a recycled host.
|
|
22
|
+
private fun unbindImages(view: View) {
|
|
23
|
+
when (view) {
|
|
24
|
+
is MarkdownImageView -> view.unbind()
|
|
25
|
+
is ViewGroup -> for (index in 0 until view.childCount) unbindImages(view.getChildAt(index))
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Rebinds in place: a child of the right kind at the same index is reused,
|
|
31
|
+
* anything else is replaced. List recycling rebinds whole trees, so the
|
|
32
|
+
* common case (same block kinds, different content) allocates nothing.
|
|
33
|
+
*/
|
|
29
34
|
fun setBlocks(blocks: List<MeasuredBlock>, gap: Float) {
|
|
30
|
-
unbindImages(this)
|
|
31
35
|
measured = blocks
|
|
32
36
|
gapPx = gap
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
blocks.forEachIndexed { index, block ->
|
|
38
|
+
val old = getChildAt(index)
|
|
39
|
+
val view = bindView(old, block)
|
|
40
|
+
if (view !== old) {
|
|
41
|
+
if (old != null) {
|
|
42
|
+
unbindImages(old)
|
|
43
|
+
removeViewAt(index)
|
|
44
|
+
}
|
|
45
|
+
addView(view, index)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
while (childCount > blocks.size) {
|
|
49
|
+
val extra = getChildAt(childCount - 1)
|
|
50
|
+
unbindImages(extra)
|
|
51
|
+
removeViewAt(childCount - 1)
|
|
36
52
|
}
|
|
37
53
|
requestLayout()
|
|
38
54
|
}
|
|
39
55
|
|
|
40
|
-
private fun
|
|
56
|
+
private fun bindView(old: View?, measuredBlock: MeasuredBlock): View {
|
|
41
57
|
return when (val block = measuredBlock.block) {
|
|
42
|
-
is Block.Text -> BlockTextView(context).apply {
|
|
58
|
+
is Block.Text -> (old as? BlockTextView ?: BlockTextView(context)).apply {
|
|
43
59
|
this.host = this@BlockStackView.host
|
|
44
60
|
setBlock(block)
|
|
45
61
|
measuredBlock.textLayout?.let(::setTextLayout)
|
|
46
62
|
}
|
|
47
|
-
is Block.Code -> CodeBlockView(context).apply {
|
|
48
|
-
|
|
63
|
+
is Block.Code -> (old as? CodeBlockView ?: CodeBlockView(context)).apply {
|
|
64
|
+
bind(measuredBlock, block)
|
|
65
|
+
}
|
|
66
|
+
is Block.Quote -> (old as? QuoteView ?: QuoteView(context)).apply {
|
|
49
67
|
bind(measuredBlock, block, gapPx, this@BlockStackView.host)
|
|
50
68
|
}
|
|
51
|
-
is Block.ListBlock -> ListBlockView(context).apply {
|
|
69
|
+
is Block.ListBlock -> (old as? ListBlockView ?: ListBlockView(context)).apply {
|
|
52
70
|
bind(measuredBlock, block, gapPx, this@BlockStackView.host)
|
|
53
71
|
}
|
|
54
|
-
is Block.Divider -> DividerView(context).apply { color = block.color }
|
|
55
|
-
is Block.Table -> TableBlockView(context).apply {
|
|
72
|
+
is Block.Divider -> (old as? DividerView ?: DividerView(context)).apply { color = block.color }
|
|
73
|
+
is Block.Table -> (old as? TableBlockView ?: TableBlockView(context)).apply {
|
|
56
74
|
bind(measuredBlock, block, this@BlockStackView.host)
|
|
57
75
|
}
|
|
58
|
-
is Block.Image -> MarkdownImageView(context).apply {
|
|
76
|
+
is Block.Image -> (old as? MarkdownImageView ?: MarkdownImageView(context)).apply {
|
|
59
77
|
this.host = this@BlockStackView.host
|
|
60
78
|
bind(block)
|
|
61
79
|
}
|
|
@@ -228,13 +246,22 @@ class ListBlockView(context: Context) : ViewGroup(context) {
|
|
|
228
246
|
measured = measuredBlock
|
|
229
247
|
block = list
|
|
230
248
|
gapPx = gap
|
|
231
|
-
|
|
249
|
+
// Children are (marker, content) pairs; grow/shrink the pair list, then
|
|
250
|
+
// rebind every pair in place.
|
|
251
|
+
val needed = measuredBlock.markerLayouts.size * 2
|
|
252
|
+
while (childCount > needed) {
|
|
253
|
+
removeViewAt(childCount - 1)
|
|
254
|
+
}
|
|
255
|
+
while (childCount < needed) {
|
|
256
|
+
addView(BlockTextView(context))
|
|
257
|
+
addView(BlockStackView(context))
|
|
258
|
+
}
|
|
232
259
|
measuredBlock.markerLayouts.forEachIndexed { index, marker ->
|
|
233
|
-
|
|
234
|
-
|
|
260
|
+
(getChildAt(index * 2) as BlockTextView).setTextLayout(marker)
|
|
261
|
+
(getChildAt(index * 2 + 1) as BlockStackView).apply {
|
|
235
262
|
this.host = host
|
|
236
263
|
setBlocks(measuredBlock.rowContents[index], gap)
|
|
237
|
-
}
|
|
264
|
+
}
|
|
238
265
|
}
|
|
239
266
|
requestLayout()
|
|
240
267
|
}
|
|
@@ -54,6 +54,8 @@ class BlockTextView(context: Context) : View(context) {
|
|
|
54
54
|
|
|
55
55
|
fun setBlock(block: Block.Text) {
|
|
56
56
|
textBlock = block
|
|
57
|
+
// Spoiler styling may change without the layout changing.
|
|
58
|
+
invalidate()
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
@@ -74,13 +74,21 @@ class TableGridView(context: Context) : ViewGroup(context) {
|
|
|
74
74
|
fun bind(measuredBlock: MeasuredBlock, table: Block.Table, host: MarkdownHost? = null) {
|
|
75
75
|
measured = measuredBlock
|
|
76
76
|
block = table
|
|
77
|
-
|
|
77
|
+
// One text view per cell, row-major; grow/shrink then rebind in place.
|
|
78
|
+
val needed = measuredBlock.cellLayouts.sumOf { it.size }
|
|
79
|
+
while (childCount > needed) {
|
|
80
|
+
removeViewAt(childCount - 1)
|
|
81
|
+
}
|
|
82
|
+
while (childCount < needed) {
|
|
83
|
+
addView(BlockTextView(context))
|
|
84
|
+
}
|
|
85
|
+
var index = 0
|
|
78
86
|
for (row in measuredBlock.cellLayouts) {
|
|
79
87
|
for (cell in row) {
|
|
80
|
-
|
|
88
|
+
(getChildAt(index++) as BlockTextView).apply {
|
|
81
89
|
this.host = host
|
|
82
90
|
setTextLayout(cell)
|
|
83
|
-
}
|
|
91
|
+
}
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
invalidate()
|