react-native-jet-markdown 0.2.0-beta.7
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 +42 -0
- package/LICENSE +20 -0
- package/README.md +247 -0
- package/android/CMakeLists.txt +52 -0
- package/android/build.gradle +55 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/JniBindings.cpp +243 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownNative.kt +114 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownPackage.kt +22 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownView.kt +235 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownViewManager.kt +75 -0
- package/android/src/main/java/com/jetmarkdown/editor/EditorSpans.kt +217 -0
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorManager.kt +224 -0
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorView.kt +1736 -0
- package/android/src/main/java/com/jetmarkdown/measure/MarkdownMeasurer.kt +54 -0
- package/android/src/main/java/com/jetmarkdown/parser/AstDecoder.kt +59 -0
- package/android/src/main/java/com/jetmarkdown/parser/MdNode.kt +43 -0
- package/android/src/main/java/com/jetmarkdown/render/ContentCache.kt +53 -0
- package/android/src/main/java/com/jetmarkdown/render/RenderedContent.kt +318 -0
- package/android/src/main/java/com/jetmarkdown/render/SpannableRenderer.kt +658 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/ChipSpan.kt +17 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/InteractiveSpans.kt +11 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/MarkdownLineHeightSpan.kt +59 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/RunSpan.kt +40 -0
- package/android/src/main/java/com/jetmarkdown/style/Fonts.kt +34 -0
- package/android/src/main/java/com/jetmarkdown/style/LayoutStyleSpec.kt +71 -0
- package/android/src/main/java/com/jetmarkdown/style/PlatformColorResolver.kt +77 -0
- package/android/src/main/java/com/jetmarkdown/style/StyleConfig.kt +80 -0
- package/android/src/main/java/com/jetmarkdown/style/TextStyleSpec.kt +61 -0
- package/android/src/main/java/com/jetmarkdown/views/BlockStackView.kt +292 -0
- package/android/src/main/java/com/jetmarkdown/views/BlockTextView.kt +378 -0
- package/android/src/main/java/com/jetmarkdown/views/BoxDrawing.kt +50 -0
- package/android/src/main/java/com/jetmarkdown/views/MarkdownHost.kt +12 -0
- package/android/src/main/java/com/jetmarkdown/views/MarkdownImageView.kt +113 -0
- package/android/src/main/java/com/jetmarkdown/views/NestedHorizontalScrollView.kt +87 -0
- package/android/src/main/java/com/jetmarkdown/views/TableBlockView.kt +139 -0
- package/cpp/core/Ast.h +85 -0
- package/cpp/core/AstJson.cpp +129 -0
- package/cpp/core/AstJson.h +12 -0
- package/cpp/core/AstSerializer.cpp +40 -0
- package/cpp/core/AstSerializer.h +21 -0
- package/cpp/core/AstToMarkdown.cpp +549 -0
- package/cpp/core/AstToMarkdown.h +16 -0
- package/cpp/core/EditorRuns.cpp +640 -0
- package/cpp/core/EditorRuns.h +94 -0
- package/cpp/core/EditorText.cpp +15 -0
- package/cpp/core/EditorText.h +18 -0
- package/cpp/core/InlineExtensions.cpp +305 -0
- package/cpp/core/InlineExtensions.h +17 -0
- package/cpp/core/Parser.cpp +470 -0
- package/cpp/core/Parser.h +16 -0
- package/cpp/core/Preprocess.cpp +181 -0
- package/cpp/core/Preprocess.h +14 -0
- package/cpp/md4c/LICENSE +22 -0
- package/cpp/md4c/VERSION +1 -0
- package/cpp/md4c/md4c.c +6462 -0
- package/cpp/md4c/md4c.h +407 -0
- package/cpp/react/JetMarkdownEditorShadowNode.cpp +54 -0
- package/cpp/react/JetMarkdownEditorShadowNode.h +55 -0
- package/cpp/react/JetMarkdownEditorState.h +35 -0
- package/cpp/react/JetMarkdownMeasurer.cpp +32 -0
- package/cpp/react/JetMarkdownMeasurer.h +41 -0
- package/cpp/react/JetMarkdownShadowNode.cpp +100 -0
- package/cpp/react/JetMarkdownShadowNode.h +53 -0
- package/cpp/react/JetMarkdownState.h +52 -0
- package/cpp/react/override/react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h +21 -0
- package/cpp/tests/parser_tests.cpp +662 -0
- package/cpp/tests/run_tests.sh +25 -0
- package/ios/JetMarkdownView.h +14 -0
- package/ios/JetMarkdownView.mm +468 -0
- package/ios/editor/JetMarkdownEditor.h +12 -0
- package/ios/editor/JetMarkdownEditor.mm +2002 -0
- package/ios/measure/JMDMarkdownMeasurer.h +22 -0
- package/ios/measure/JMDMarkdownMeasurer.mm +38 -0
- package/ios/render/JMDBlock.h +97 -0
- package/ios/render/JMDBlock.m +28 -0
- package/ios/render/JMDBlockRenderer.h +17 -0
- package/ios/render/JMDBlockRenderer.mm +852 -0
- package/ios/render/JMDContentCache.h +17 -0
- package/ios/render/JMDContentCache.mm +42 -0
- package/ios/render/JMDRenderedContent.h +35 -0
- package/ios/render/JMDRenderedContent.mm +274 -0
- package/ios/style/JMDFontScale.h +33 -0
- package/ios/style/JMDLayoutStyle.h +37 -0
- package/ios/style/JMDLayoutStyle.mm +68 -0
- package/ios/style/JMDStyleConfig.h +39 -0
- package/ios/style/JMDStyleConfig.mm +111 -0
- package/ios/style/JMDTextStyle.h +25 -0
- package/ios/style/JMDTextStyle.mm +68 -0
- package/ios/views/JMDBlockStackView.h +18 -0
- package/ios/views/JMDBlockStackView.m +97 -0
- package/ios/views/JMDBlockTextView.h +24 -0
- package/ios/views/JMDBlockTextView.m +340 -0
- package/ios/views/JMDBoxViews.h +33 -0
- package/ios/views/JMDBoxViews.m +292 -0
- package/ios/views/JMDImageView.h +19 -0
- package/ios/views/JMDImageView.m +68 -0
- package/ios/views/JMDMarkdownHost.h +16 -0
- package/ios/views/JMDTableView.h +16 -0
- package/ios/views/JMDTableView.m +172 -0
- package/lib/module/JetMarkdownEditor.js +85 -0
- package/lib/module/JetMarkdownEditor.js.map +1 -0
- package/lib/module/JetMarkdownEditor.native.js +244 -0
- package/lib/module/JetMarkdownEditor.native.js.map +1 -0
- package/lib/module/JetMarkdownEditorNativeComponent.ts +173 -0
- package/lib/module/JetMarkdownView.js +19 -0
- package/lib/module/JetMarkdownView.js.map +1 -0
- package/lib/module/JetMarkdownView.native.js +44 -0
- package/lib/module/JetMarkdownView.native.js.map +1 -0
- package/lib/module/JetMarkdownViewNativeComponent.ts +35 -0
- package/lib/module/defaultStyles.js +122 -0
- package/lib/module/defaultStyles.js.map +1 -0
- package/lib/module/index.js +9 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/serializeStyles.js +259 -0
- package/lib/module/serializeStyles.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useJetMarkdownEditor.js +41 -0
- package/lib/module/useJetMarkdownEditor.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/JetMarkdownEditor.d.ts +6 -0
- package/lib/typescript/src/JetMarkdownEditor.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts +6 -0
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts +105 -0
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownView.d.ts +3 -0
- package/lib/typescript/src/JetMarkdownView.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownView.native.d.ts +3 -0
- package/lib/typescript/src/JetMarkdownView.native.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts +28 -0
- package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/defaultStyles.d.ts +20 -0
- package/lib/typescript/src/defaultStyles.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +7 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/serializeStyles.d.ts +34 -0
- package/lib/typescript/src/serializeStyles.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +352 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/useJetMarkdownEditor.d.ts +36 -0
- package/lib/typescript/src/useJetMarkdownEditor.d.ts.map +1 -0
- package/package.json +150 -0
- package/react-native.config.js +11 -0
- package/src/JetMarkdownEditor.native.tsx +301 -0
- package/src/JetMarkdownEditor.tsx +90 -0
- package/src/JetMarkdownEditorNativeComponent.ts +173 -0
- package/src/JetMarkdownView.native.tsx +67 -0
- package/src/JetMarkdownView.tsx +16 -0
- package/src/JetMarkdownViewNativeComponent.ts +35 -0
- package/src/defaultStyles.ts +102 -0
- package/src/index.tsx +34 -0
- package/src/serializeStyles.ts +369 -0
- package/src/types.ts +438 -0
- package/src/useJetMarkdownEditor.ts +50 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "JetMarkdown"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/alizahid/react-native-jet-markdown.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
# Headers are deliberately NOT in source_files. Xcode's project-wide
|
|
17
|
+
# header maps index every pod target's headers BY BASENAME, so adding
|
|
18
|
+
# generically named headers (md4c.h, Parser.h) to the Pods project
|
|
19
|
+
# shadows other pods' same-named headers in THEIR quoted includes —
|
|
20
|
+
# react-native-enriched-markdown's forked md4c and unistyles' parser
|
|
21
|
+
# both stopped compiling. Our own includes are file-relative or resolve
|
|
22
|
+
# through the search paths below; the headers ship in the npm package
|
|
23
|
+
# and stay on disk (RN pods integrate via :path, which never cleans).
|
|
24
|
+
s.source_files = "ios/**/*.{m,mm,swift,cpp}",
|
|
25
|
+
"cpp/core/**/*.cpp",
|
|
26
|
+
"cpp/react/**/*.cpp",
|
|
27
|
+
"cpp/md4c/*.c"
|
|
28
|
+
s.preserve_paths = "ios/**/*.h", "cpp/**/*.h"
|
|
29
|
+
|
|
30
|
+
s.pod_target_xcconfig = {
|
|
31
|
+
# The override directory must precede generated headers so the custom
|
|
32
|
+
# component descriptor shadows the codegen default.
|
|
33
|
+
"HEADER_SEARCH_PATHS" => '"$(PODS_TARGET_SRCROOT)/cpp/react/override" "$(PODS_TARGET_SRCROOT)/cpp"',
|
|
34
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Image pipeline (same core expo-image uses): animated GIF/APNG with lazy
|
|
38
|
+
# frame decoding, memory + disk caches, request dedupe and cancellation.
|
|
39
|
+
s.dependency "SDWebImage", "~> 5.21"
|
|
40
|
+
|
|
41
|
+
install_modules_dependencies(s)
|
|
42
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ali Zahid
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img alt="react-native-jet-markdown — native markdown viewer and WYSIWYG editor for React Native" src="https://raw.githubusercontent.com/alizahid/react-native-jet-markdown/main/docs/hero.svg" width="900">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
Fast, fully native Markdown viewer & WYSIWYG editor for React Native
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/react-native-jet-markdown"><img src="https://img.shields.io/npm/v/react-native-jet-markdown?color=A02F6F&label=npm" alt="npm"></a>
|
|
11
|
+
<a href="https://github.com/alizahid/react-native-jet-markdown/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/react-native-jet-markdown?color=668C0B" alt="license"></a>
|
|
12
|
+
<img src="https://img.shields.io/badge/platform-iOS%20%7C%20Android-5E409D" alt="platform">
|
|
13
|
+
<img src="https://img.shields.io/badge/architecture-Fabric-205EA6" alt="architecture">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
- **One native Fabric view** — markdown is parsed natively with [md4c](https://github.com/mity/md4c) and rendered as attributed text (TextKit on iOS, StaticLayout/Spannable on Android). No WebView, no JS-side layout, no nested `<Text>` trees.
|
|
19
|
+
- **Self-sizing** — the component measures its own height on the Fabric layout thread with a custom C++ shadow node, so it behaves like `<Text>` in any layout, list, or scroll view.
|
|
20
|
+
- **Built for lists** — parse and layout results are cached and shared between measurement and mounting; views recycle cleanly in FlatList / FlashList / LegendList.
|
|
21
|
+
- **Deeply styleable** — a typed, per-element styles API with cascading text styles, box styles, and regex-based mention variants.
|
|
22
|
+
|
|
23
|
+
## Supported markdown
|
|
24
|
+
|
|
25
|
+
Headings, paragraphs, images, GFM tables (horizontal scroll with intelligently sized columns), spoilers (Reddit `>!text!<` **and** Discord `||text||`), superscript (`^sup^`, Reddit `^word` and `^(multi word)`), subscript (`~sub~`), bold / italic / strikethrough, ordered + unordered lists (nested), links, mentions, inline code, code blocks (horizontally scrolling, monospace), block quotes, and thematic breaks.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install react-native-jet-markdown
|
|
31
|
+
cd ios && pod install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires React Native 0.86+ with the New Architecture (Fabric).
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import {
|
|
40
|
+
JetMarkdownView,
|
|
41
|
+
mergeStyles,
|
|
42
|
+
type MarkdownStyles,
|
|
43
|
+
} from 'react-native-jet-markdown';
|
|
44
|
+
|
|
45
|
+
// Default look + overrides. Or pass `defaultStyles` as-is, or your own
|
|
46
|
+
// object from scratch — with no `styles` prop the viewer renders fully
|
|
47
|
+
// plain text.
|
|
48
|
+
const styles: MarkdownStyles = mergeStyles({
|
|
49
|
+
paragraph: { fontSize: 16, color: '#1F2937' },
|
|
50
|
+
link: { color: '#2563EB', textDecorationLine: 'underline' },
|
|
51
|
+
mention: {
|
|
52
|
+
fontWeight: '600',
|
|
53
|
+
variants: {
|
|
54
|
+
'^users://': { color: '#DB2777' },
|
|
55
|
+
'^channels://': { color: '#059669' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
spoiler: { backgroundColor: '#374151', borderRadius: 4 },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
<JetMarkdownView
|
|
62
|
+
markdown={'Hello **world**, ping [@ali](users://ali)!'}
|
|
63
|
+
styles={styles}
|
|
64
|
+
style={{ padding: 16, gap: 12 }}
|
|
65
|
+
onLinkPress={({ url }) => Linking.openURL(url)}
|
|
66
|
+
/>;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Props
|
|
70
|
+
|
|
71
|
+
| Prop | Type | Description |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `markdown` | `string` | The markdown source. |
|
|
74
|
+
| `style` | `MarkdownContainerStyle` | Main container style: `backgroundColor`, `padding`/`padding{Left,Right,Top,Bottom}`, `gap` (spacing between blocks), plus base text styles (`fontSize`, `fontWeight`, `fontFamily`, `color`, `fontVariant`, `textDecoration*`) that cascade into every text element unless overridden per-element via `styles`. Element builtins survive the cascade: heading sizes/weight stay unless `headings.hN` overrides, and code blocks keep their monospace font unless `codeBlock` overrides. For outer layout (margin, width, flex), wrap the viewer in a `View`. |
|
|
75
|
+
| `styles` | `MarkdownStyles` | Per-element styles (below). Omitted = fully plain output (no colors, boxes, heading sizes — only bold/italic runs, monospace code, list markers, and the spoiler cover). Pass the exported `defaultStyles` for the classic look, or `mergeStyles(overrides)` for defaults + your changes. Hoist to module scope or memoize. |
|
|
76
|
+
| `images` | `{ url, width, height }[]` | Pre-sizing data. Listed images lay out at their final size immediately — zero layout shift. Unlisted images render a styled full-width, 200pt-tall placeholder, then snap to their real aspect once loaded. 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). |
|
|
77
|
+
| `onLinkPress` | `({ url }) => void` | Link or mention tapped. Mentions arrive with their scheme (e.g. `users://ali`). |
|
|
78
|
+
| `onLinkLongPress` | `({ url }) => void` | Link long-pressed. |
|
|
79
|
+
| `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
|
+
|
|
81
|
+
## Styling
|
|
82
|
+
|
|
83
|
+
The viewer ships **unstyled by default**. Two exports cover the common cases:
|
|
84
|
+
|
|
85
|
+
- `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
|
+
- `mergeStyles(overrides)` — deep-merges your overrides into `defaultStyles` (element sections merge key-by-key, heading levels individually).
|
|
87
|
+
|
|
88
|
+
Two shared shapes compose every element style:
|
|
89
|
+
|
|
90
|
+
**Text** — `fontSize`, `fontWeight`, `fontFamily`, `color`, `backgroundColor` (run highlight), `fontVariant`, `lineHeight`, `textDecorationColor`, `textDecorationLine`, `textDecorationStyle`
|
|
91
|
+
|
|
92
|
+
**Layout** — `backgroundColor`, `padding` (+ `paddingHorizontal`/`paddingVertical` and per-side), `borderRadius`, `borderCurve` (iOS), `borderColor`/`borderWidth` (+ per-side)
|
|
93
|
+
|
|
94
|
+
Every color accepts platform colors (`PlatformColor`, `DynamicColorIOS`) as well as static values — on iOS they stay dynamic and adapt to light/dark automatically; on Android they resolve against the current theme.
|
|
95
|
+
|
|
96
|
+
| Key | Accepts | Notes |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `headings.h1`–`h6` | text | Do **not** inherit `paragraph`, and ignore the base `lineHeight` (a body line height would clip taller headings) — set `headings.hN.lineHeight` explicitly if needed. `defaultStyles`: 32/26/22/18/16/14, bold. |
|
|
99
|
+
| `paragraph` | text | Base for body text; inline styles cascade on top. |
|
|
100
|
+
| `bold`, `italic`, `strikethrough` | text | |
|
|
101
|
+
| `link` | text + `borderRadius`, `borderCurve` (iOS) | `backgroundColor` draws as a rounded chip behind the run. `defaultStyles`: system blue. |
|
|
102
|
+
| `mention` | text + `borderRadius`, `borderCurve` (iOS) + `variants` | `backgroundColor` draws as a rounded chip. `variants` maps a regex (tested against the link URL, longest pattern first) to a style. A link matching any variant is a mention. |
|
|
103
|
+
| `inlineCode` | text + `backgroundColor`, `borderRadius`, `borderCurve` (iOS), `padding(Horizontal/Left/Right)` | Always monospace; the background draws as a rounded chip. `defaultStyles`: 8% black background. |
|
|
104
|
+
| `superscript`, `subscript` | text | Default: 0.7x size with baseline shift. |
|
|
105
|
+
| `spoiler` | `backgroundColor`, `borderRadius`, `borderCurve` | The tap-to-reveal cover — one contiguous polygon even across line wraps. Unstyled: plain black. |
|
|
106
|
+
| `codeBlock` | text + layout | Always monospace; long lines scroll horizontally. `defaultStyles`: 14pt, 8% black, radius 6, padding 12. |
|
|
107
|
+
| `blockQuote` | text + layout | Text styles cascade into quoted content. `defaultStyles`: 3pt left border, 12pt left padding. |
|
|
108
|
+
| `list` | `marginLeft` | |
|
|
109
|
+
| `listMarker` | `width`, `marginLeft`, `color` | Fixed-width marker column. Unstyled: sized to the widest marker. `defaultStyles`: 24. |
|
|
110
|
+
| `listItem` | text | Cascades into item content. |
|
|
111
|
+
| `image` | `borderRadius`, `backgroundColor`, `height`, `maxHeight` | `backgroundColor` shows while loading. |
|
|
112
|
+
| `table` | layout + `minColumnWidth`, `maxColumnWidth` | Unstyled: natural column widths. `defaultStyles`: clamps to `[44, 320]`. |
|
|
113
|
+
| `tableRow` | layout | Base for all rows. `defaultStyles`: 1pt bottom-border separator. |
|
|
114
|
+
| `tableHeaderRow`, `tableBodyRow` | layout | Layer over `tableRow` for header vs body rows. |
|
|
115
|
+
| `tableCell` | text + `padding*` | Header cells are always bold. `defaultStyles`: padding 8. |
|
|
116
|
+
| `tableHeaderCell` | text + `padding*` | Layers over `tableCell` for header cells. |
|
|
117
|
+
| `divider` | `color`, `height` | Thematic break (`---`). Unstyled: 1pt black. `defaultStyles`: subtle hairline. |
|
|
118
|
+
| `gap` | number | Vertical spacing between blocks. The `style` prop's `gap` wins when both are set. Unstyled: 0. `defaultStyles`: 12. |
|
|
119
|
+
|
|
120
|
+
### Tables
|
|
121
|
+
|
|
122
|
+
Each column gets its natural (unwrapped) width, clamped to `[minColumnWidth, maxColumnWidth]`. If the table fits the container, the surplus is distributed proportionally so it fills the line; if not, columns keep their readable widths and the table scrolls horizontally.
|
|
123
|
+
|
|
124
|
+
### Mentions
|
|
125
|
+
|
|
126
|
+
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
|
+
|
|
128
|
+
## Using in lists
|
|
129
|
+
|
|
130
|
+
Rendering hundreds of viewers in FlatList / FlashList / LegendList is a first-class use case:
|
|
131
|
+
|
|
132
|
+
- Hoist `styles` (and `images` when static) to module scope — every item then shares one parsed native style config.
|
|
133
|
+
- Parse + layout are cached natively and computed on the Fabric layout thread, so scrolling never parses markdown on the main thread.
|
|
134
|
+
- Views reset correctly when recycled; rebinding a view cancels its previous image request, and caches make re-scroll loads instant.
|
|
135
|
+
- Wrapping items in `Pressable` works: taps on links, mentions, and spoilers are claimed by the markdown view; taps on plain text reach your `Pressable`.
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
const renderItem = ({ item }) => (
|
|
139
|
+
<Pressable onPress={() => openPost(item)}>
|
|
140
|
+
<JetMarkdownView markdown={item.body} styles={styles} onLinkPress={openLink} />
|
|
141
|
+
</Pressable>
|
|
142
|
+
);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Performance
|
|
146
|
+
|
|
147
|
+
Numbers from the shared C++ core (Apple M-series, `-O2`; the same code runs on device — expect a few multiples slower on mid-range phones, still far below frame budget):
|
|
148
|
+
|
|
149
|
+
| Operation | Input | Time |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| Parse a typical feed post | 1.5 KB mixed markdown | **35 µs** |
|
|
152
|
+
| 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
|
+
| Serialize the full `defaultStyles` object in JS | 980 B JSON | **3 µs**, memoized |
|
|
156
|
+
|
|
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, editor keystrokes only rebuild the derived styling of the lines they touch, and markdown serialization is coalesced to once per frame. The 500-item Feed screen in the example app scrolls at 60 fps on both platforms.
|
|
158
|
+
|
|
159
|
+
### Why styles cross as JSON
|
|
160
|
+
|
|
161
|
+
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
|
+
|
|
163
|
+
## Editor
|
|
164
|
+
|
|
165
|
+
`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.
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
import { JetMarkdownEditor, useJetMarkdownEditor } from "react-native-jet-markdown";
|
|
169
|
+
|
|
170
|
+
function Compose() {
|
|
171
|
+
const editor = useJetMarkdownEditor();
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<>
|
|
175
|
+
<JetMarkdownEditor
|
|
176
|
+
mentionTriggers={["@"]}
|
|
177
|
+
onChangeMarkdown={(markdown) => save(markdown)}
|
|
178
|
+
onMentionChange={({ query }) => search(query)}
|
|
179
|
+
placeholder="Write something..."
|
|
180
|
+
ref={editor.ref}
|
|
181
|
+
/>
|
|
182
|
+
<Button onPress={editor.toggleBold} title="B" />
|
|
183
|
+
</>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Formatting
|
|
189
|
+
|
|
190
|
+
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()`).
|
|
191
|
+
|
|
192
|
+
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.
|
|
193
|
+
|
|
194
|
+
`onChangeState` reports the formatting at the cursor or selection (`isBold`, `headingLevel`, `isUnorderedList`, ...) for toolbar highlighting.
|
|
195
|
+
|
|
196
|
+
### Links & mentions
|
|
197
|
+
|
|
198
|
+
`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.
|
|
199
|
+
|
|
200
|
+
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.
|
|
201
|
+
|
|
202
|
+
### Paste
|
|
203
|
+
|
|
204
|
+
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.
|
|
205
|
+
|
|
206
|
+
### Everything else
|
|
207
|
+
|
|
208
|
+
`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.
|
|
209
|
+
|
|
210
|
+
## Platform notes & limitations
|
|
211
|
+
|
|
212
|
+
- New Architecture (Fabric) only; React Native 0.86+.
|
|
213
|
+
- `textDecorationStyle`/`textDecorationColor` render fully on iOS; Android draws plain underline/strikethrough.
|
|
214
|
+
- `borderCurve: 'continuous'` is iOS-only.
|
|
215
|
+
- `fontVariant` supports `tabular-nums`, `proportional-nums`, `oldstyle-nums`, `lining-nums`, `small-caps` (font support required).
|
|
216
|
+
- `allowFontScaling` (default `true`) scales all text — including `lineHeight` — with the system font size setting on both components.
|
|
217
|
+
- Spoilers and inline formatting inside link labels stay literal; spoilers inside table cells are unsupported (the `|` delimiter conflicts).
|
|
218
|
+
- Code blocks render plain monospace (no syntax highlighting).
|
|
219
|
+
|
|
220
|
+
## Sponsors
|
|
221
|
+
|
|
222
|
+
Development of react-native-jet-markdown is supported by:
|
|
223
|
+
|
|
224
|
+
<table>
|
|
225
|
+
<tr>
|
|
226
|
+
<td align="center" width="200">
|
|
227
|
+
<a href="https://duet.so">
|
|
228
|
+
<img alt="Duet" src="https://github.com/alizahid/react-native-jet-markdown/blob/main/.github/duet.png?raw=true" width="96"><br>
|
|
229
|
+
<b>Duet</b>
|
|
230
|
+
</a>
|
|
231
|
+
<br>
|
|
232
|
+
<sub>Your AI coworker that runs your business 24/7</sub>
|
|
233
|
+
</td>
|
|
234
|
+
<td align="center" width="200">
|
|
235
|
+
<a href="https://acorn.blue">
|
|
236
|
+
<img alt="Acorn" src="https://github.com/alizahid/react-native-jet-markdown/blob/main/.github/acorn.png?raw=true" width="96"><br>
|
|
237
|
+
<b>Acorn</b>
|
|
238
|
+
</a>
|
|
239
|
+
<br>
|
|
240
|
+
<sub>Reddit for mobile</sub>
|
|
241
|
+
</td>
|
|
242
|
+
</tr>
|
|
243
|
+
</table>
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
MIT
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Supersedes the codegen-generated CMakeLists (react-native.config.js points
|
|
2
|
+
# autolinking here) so the library can compile the shared C++ core and swap
|
|
3
|
+
# the codegen component descriptor for the custom measurable shadow node.
|
|
4
|
+
|
|
5
|
+
cmake_minimum_required(VERSION 3.13)
|
|
6
|
+
set(CMAKE_VERBOSE_MAKEFILE on)
|
|
7
|
+
|
|
8
|
+
set(lib_android_DIR ${CMAKE_CURRENT_SOURCE_DIR})
|
|
9
|
+
set(lib_cpp_DIR ${lib_android_DIR}/../cpp)
|
|
10
|
+
set(lib_codegen_DIR ${lib_android_DIR}/build/generated/source/codegen/jni)
|
|
11
|
+
|
|
12
|
+
file(GLOB codegen_SRCS CONFIGURE_DEPENDS
|
|
13
|
+
${lib_codegen_DIR}/*.cpp
|
|
14
|
+
${lib_codegen_DIR}/react/renderer/components/JetMarkdownViewSpec/*.cpp)
|
|
15
|
+
# The custom descriptor in cpp/react/override replaces the generated one.
|
|
16
|
+
list(FILTER codegen_SRCS EXCLUDE REGEX "ComponentDescriptors\\.cpp$")
|
|
17
|
+
|
|
18
|
+
file(GLOB lib_SRCS CONFIGURE_DEPENDS
|
|
19
|
+
${lib_cpp_DIR}/core/*.cpp
|
|
20
|
+
${lib_cpp_DIR}/react/*.cpp)
|
|
21
|
+
|
|
22
|
+
# md4c is C; keep it out of the C++-flagged reactnative target.
|
|
23
|
+
add_library(jetmarkdown_md4c STATIC ${lib_cpp_DIR}/md4c/md4c.c)
|
|
24
|
+
set_target_properties(jetmarkdown_md4c PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
|
25
|
+
|
|
26
|
+
add_library(
|
|
27
|
+
react_codegen_JetMarkdownViewSpec
|
|
28
|
+
OBJECT
|
|
29
|
+
${codegen_SRCS}
|
|
30
|
+
${lib_SRCS}
|
|
31
|
+
${lib_android_DIR}/src/main/cpp/JniBindings.cpp
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# The override directory must come first so
|
|
35
|
+
# <react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h>
|
|
36
|
+
# resolves to the custom descriptor everywhere (autolinking included).
|
|
37
|
+
target_include_directories(react_codegen_JetMarkdownViewSpec PUBLIC
|
|
38
|
+
${lib_cpp_DIR}/react/override
|
|
39
|
+
${lib_codegen_DIR}
|
|
40
|
+
${lib_codegen_DIR}/react/renderer/components/JetMarkdownViewSpec
|
|
41
|
+
${lib_cpp_DIR}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
target_link_libraries(
|
|
45
|
+
react_codegen_JetMarkdownViewSpec
|
|
46
|
+
jetmarkdown_md4c
|
|
47
|
+
fbjni
|
|
48
|
+
jsi
|
|
49
|
+
reactnative
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
target_compile_reactnative_options(react_codegen_JetMarkdownViewSpec PRIVATE)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.JetMarkdown = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
ext.getExtOrDefault = { prop ->
|
|
9
|
+
if (rootProject.ext.has(prop)) {
|
|
10
|
+
return rootProject.ext.get(prop)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return JetMarkdown[prop]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
repositories {
|
|
17
|
+
google()
|
|
18
|
+
mavenCentral()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
dependencies {
|
|
22
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
23
|
+
// noinspection DifferentKotlinGradleVersion
|
|
24
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
apply plugin: "com.android.library"
|
|
30
|
+
apply plugin: "kotlin-android"
|
|
31
|
+
|
|
32
|
+
apply plugin: "com.facebook.react"
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace "com.jetmarkdown"
|
|
36
|
+
|
|
37
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
38
|
+
|
|
39
|
+
defaultConfig {
|
|
40
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
compileOptions {
|
|
44
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
45
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
dependencies {
|
|
50
|
+
implementation "com.facebook.react:react-android"
|
|
51
|
+
// Image pipeline (same core expo-image uses, same major version to avoid
|
|
52
|
+
// classpath conflicts): animated GIF, downsampling, memory + disk caches,
|
|
53
|
+
// request cancellation.
|
|
54
|
+
implementation "com.github.bumptech.glide:glide:5.0.5"
|
|
55
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// JNI surface for com.jetmarkdown.JetMarkdownNative. This TU is linked
|
|
2
|
+
// into the app's libappmodules.so (which owns JNI_OnLoad), so natives are
|
|
3
|
+
// exported by name instead of registered from an OnLoad hook.
|
|
4
|
+
//
|
|
5
|
+
// Strings cross as UTF-8 byte arrays: JNI's NewStringUTF/GetStringUTFChars
|
|
6
|
+
// speak modified UTF-8 and corrupt supplementary characters (emoji).
|
|
7
|
+
|
|
8
|
+
#include <jni.h>
|
|
9
|
+
#include <pthread.h>
|
|
10
|
+
|
|
11
|
+
#include <string>
|
|
12
|
+
#include <vector>
|
|
13
|
+
|
|
14
|
+
#include "core/AstSerializer.h"
|
|
15
|
+
#include "core/EditorRuns.h"
|
|
16
|
+
#include "core/Parser.h"
|
|
17
|
+
#include "react/JetMarkdownMeasurer.h"
|
|
18
|
+
|
|
19
|
+
namespace {
|
|
20
|
+
|
|
21
|
+
JavaVM* g_vm = nullptr;
|
|
22
|
+
jobject g_measurer = nullptr;
|
|
23
|
+
jmethodID g_measureMethod = nullptr;
|
|
24
|
+
|
|
25
|
+
std::string toStdString(JNIEnv* env, jbyteArray value) {
|
|
26
|
+
if (value == nullptr) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
const jsize length = env->GetArrayLength(value);
|
|
30
|
+
std::string result(static_cast<size_t>(length), '\0');
|
|
31
|
+
if (length > 0) {
|
|
32
|
+
env->GetByteArrayRegion(value, 0, length, reinterpret_cast<jbyte*>(result.data()));
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
jbyteArray toByteArray(JNIEnv* env, const uint8_t* data, size_t size) {
|
|
38
|
+
jbyteArray result = env->NewByteArray(static_cast<jsize>(size));
|
|
39
|
+
if (result != nullptr && size > 0) {
|
|
40
|
+
env->SetByteArrayRegion(
|
|
41
|
+
result, 0, static_cast<jsize>(size), reinterpret_cast<const jbyte*>(data));
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Threads WE attach must detach before exiting or ART aborts the process
|
|
47
|
+
// ("Native thread exiting without having called DetachCurrentThread"); a
|
|
48
|
+
// pthread key destructor covers early exits.
|
|
49
|
+
pthread_key_t g_detachKey;
|
|
50
|
+
pthread_once_t g_detachKeyOnce = PTHREAD_ONCE_INIT;
|
|
51
|
+
|
|
52
|
+
void detachCurrentThread(void*) {
|
|
53
|
+
if (g_vm != nullptr) {
|
|
54
|
+
g_vm->DetachCurrentThread();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
void makeDetachKey() {
|
|
59
|
+
pthread_key_create(&g_detachKey, detachCurrentThread);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
JNIEnv* currentEnv() {
|
|
63
|
+
if (g_vm == nullptr) {
|
|
64
|
+
return nullptr;
|
|
65
|
+
}
|
|
66
|
+
JNIEnv* env = nullptr;
|
|
67
|
+
const jint state = g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
|
|
68
|
+
if (state == JNI_EDETACHED) {
|
|
69
|
+
if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) {
|
|
70
|
+
return nullptr;
|
|
71
|
+
}
|
|
72
|
+
pthread_once(&g_detachKeyOnce, makeDetachKey);
|
|
73
|
+
pthread_setspecific(g_detachKey, env);
|
|
74
|
+
} else if (state != JNI_OK) {
|
|
75
|
+
return nullptr;
|
|
76
|
+
}
|
|
77
|
+
return env;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
} // namespace
|
|
81
|
+
|
|
82
|
+
extern "C" JNIEXPORT jbyteArray JNICALL
|
|
83
|
+
Java_com_jetmarkdown_JetMarkdownNative_parse(JNIEnv* env, jclass, jbyteArray markdown) {
|
|
84
|
+
const std::string input = toStdString(env, markdown);
|
|
85
|
+
const auto document = jetmarkdown::parseMarkdown(input);
|
|
86
|
+
const std::vector<uint8_t> bytes = jetmarkdown::serializeAst(document->root);
|
|
87
|
+
return toByteArray(env, bytes.data(), bytes.size());
|
|
88
|
+
}
|
|
89
|
+
|
|
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
|
+
extern "C" JNIEXPORT void JNICALL
|
|
196
|
+
Java_com_jetmarkdown_JetMarkdownNative_installMeasurer(
|
|
197
|
+
JNIEnv* env,
|
|
198
|
+
jclass,
|
|
199
|
+
jobject measurer) {
|
|
200
|
+
env->GetJavaVM(&g_vm);
|
|
201
|
+
if (g_measurer != nullptr) {
|
|
202
|
+
env->DeleteGlobalRef(g_measurer);
|
|
203
|
+
}
|
|
204
|
+
g_measurer = env->NewGlobalRef(measurer);
|
|
205
|
+
|
|
206
|
+
jclass measurerClass = env->GetObjectClass(measurer);
|
|
207
|
+
g_measureMethod = env->GetMethodID(measurerClass, "measure", "([B[B[BFF)F");
|
|
208
|
+
env->DeleteLocalRef(measurerClass);
|
|
209
|
+
|
|
210
|
+
jetmarkdown::JetMarkdownMeasurer::shared().install(
|
|
211
|
+
[](const std::string& markdown,
|
|
212
|
+
const std::string& stylesJson,
|
|
213
|
+
const std::string& imagesJson,
|
|
214
|
+
float maxWidth,
|
|
215
|
+
float fontScale) -> float {
|
|
216
|
+
JNIEnv* jniEnv = currentEnv();
|
|
217
|
+
if (jniEnv == nullptr || g_measurer == nullptr || g_measureMethod == nullptr) {
|
|
218
|
+
return 0.0f;
|
|
219
|
+
}
|
|
220
|
+
jbyteArray jMarkdown = toByteArray(
|
|
221
|
+
jniEnv, reinterpret_cast<const uint8_t*>(markdown.data()), markdown.size());
|
|
222
|
+
jbyteArray jStyles = toByteArray(
|
|
223
|
+
jniEnv, reinterpret_cast<const uint8_t*>(stylesJson.data()), stylesJson.size());
|
|
224
|
+
jbyteArray jImages = toByteArray(
|
|
225
|
+
jniEnv, reinterpret_cast<const uint8_t*>(imagesJson.data()), imagesJson.size());
|
|
226
|
+
if (jMarkdown == nullptr || jStyles == nullptr || jImages == nullptr) {
|
|
227
|
+
// NewByteArray OOM leaves a pending exception; calling into Java
|
|
228
|
+
// with one pending is undefined behavior.
|
|
229
|
+
jniEnv->ExceptionClear();
|
|
230
|
+
return 0.0f;
|
|
231
|
+
}
|
|
232
|
+
const jfloat height = jniEnv->CallFloatMethod(
|
|
233
|
+
g_measurer, g_measureMethod, jMarkdown, jStyles, jImages, maxWidth, fontScale);
|
|
234
|
+
jniEnv->DeleteLocalRef(jMarkdown);
|
|
235
|
+
jniEnv->DeleteLocalRef(jStyles);
|
|
236
|
+
jniEnv->DeleteLocalRef(jImages);
|
|
237
|
+
if (jniEnv->ExceptionCheck()) {
|
|
238
|
+
jniEnv->ExceptionClear();
|
|
239
|
+
return 0.0f;
|
|
240
|
+
}
|
|
241
|
+
return static_cast<float>(height);
|
|
242
|
+
});
|
|
243
|
+
}
|