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.
Files changed (157) hide show
  1. package/JetMarkdown.podspec +42 -0
  2. package/LICENSE +20 -0
  3. package/README.md +247 -0
  4. package/android/CMakeLists.txt +52 -0
  5. package/android/build.gradle +55 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/JniBindings.cpp +243 -0
  8. package/android/src/main/java/com/jetmarkdown/JetMarkdownNative.kt +114 -0
  9. package/android/src/main/java/com/jetmarkdown/JetMarkdownPackage.kt +22 -0
  10. package/android/src/main/java/com/jetmarkdown/JetMarkdownView.kt +235 -0
  11. package/android/src/main/java/com/jetmarkdown/JetMarkdownViewManager.kt +75 -0
  12. package/android/src/main/java/com/jetmarkdown/editor/EditorSpans.kt +217 -0
  13. package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorManager.kt +224 -0
  14. package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorView.kt +1736 -0
  15. package/android/src/main/java/com/jetmarkdown/measure/MarkdownMeasurer.kt +54 -0
  16. package/android/src/main/java/com/jetmarkdown/parser/AstDecoder.kt +59 -0
  17. package/android/src/main/java/com/jetmarkdown/parser/MdNode.kt +43 -0
  18. package/android/src/main/java/com/jetmarkdown/render/ContentCache.kt +53 -0
  19. package/android/src/main/java/com/jetmarkdown/render/RenderedContent.kt +318 -0
  20. package/android/src/main/java/com/jetmarkdown/render/SpannableRenderer.kt +658 -0
  21. package/android/src/main/java/com/jetmarkdown/render/spans/ChipSpan.kt +17 -0
  22. package/android/src/main/java/com/jetmarkdown/render/spans/InteractiveSpans.kt +11 -0
  23. package/android/src/main/java/com/jetmarkdown/render/spans/MarkdownLineHeightSpan.kt +59 -0
  24. package/android/src/main/java/com/jetmarkdown/render/spans/RunSpan.kt +40 -0
  25. package/android/src/main/java/com/jetmarkdown/style/Fonts.kt +34 -0
  26. package/android/src/main/java/com/jetmarkdown/style/LayoutStyleSpec.kt +71 -0
  27. package/android/src/main/java/com/jetmarkdown/style/PlatformColorResolver.kt +77 -0
  28. package/android/src/main/java/com/jetmarkdown/style/StyleConfig.kt +80 -0
  29. package/android/src/main/java/com/jetmarkdown/style/TextStyleSpec.kt +61 -0
  30. package/android/src/main/java/com/jetmarkdown/views/BlockStackView.kt +292 -0
  31. package/android/src/main/java/com/jetmarkdown/views/BlockTextView.kt +378 -0
  32. package/android/src/main/java/com/jetmarkdown/views/BoxDrawing.kt +50 -0
  33. package/android/src/main/java/com/jetmarkdown/views/MarkdownHost.kt +12 -0
  34. package/android/src/main/java/com/jetmarkdown/views/MarkdownImageView.kt +113 -0
  35. package/android/src/main/java/com/jetmarkdown/views/NestedHorizontalScrollView.kt +87 -0
  36. package/android/src/main/java/com/jetmarkdown/views/TableBlockView.kt +139 -0
  37. package/cpp/core/Ast.h +85 -0
  38. package/cpp/core/AstJson.cpp +129 -0
  39. package/cpp/core/AstJson.h +12 -0
  40. package/cpp/core/AstSerializer.cpp +40 -0
  41. package/cpp/core/AstSerializer.h +21 -0
  42. package/cpp/core/AstToMarkdown.cpp +549 -0
  43. package/cpp/core/AstToMarkdown.h +16 -0
  44. package/cpp/core/EditorRuns.cpp +640 -0
  45. package/cpp/core/EditorRuns.h +94 -0
  46. package/cpp/core/EditorText.cpp +15 -0
  47. package/cpp/core/EditorText.h +18 -0
  48. package/cpp/core/InlineExtensions.cpp +305 -0
  49. package/cpp/core/InlineExtensions.h +17 -0
  50. package/cpp/core/Parser.cpp +470 -0
  51. package/cpp/core/Parser.h +16 -0
  52. package/cpp/core/Preprocess.cpp +181 -0
  53. package/cpp/core/Preprocess.h +14 -0
  54. package/cpp/md4c/LICENSE +22 -0
  55. package/cpp/md4c/VERSION +1 -0
  56. package/cpp/md4c/md4c.c +6462 -0
  57. package/cpp/md4c/md4c.h +407 -0
  58. package/cpp/react/JetMarkdownEditorShadowNode.cpp +54 -0
  59. package/cpp/react/JetMarkdownEditorShadowNode.h +55 -0
  60. package/cpp/react/JetMarkdownEditorState.h +35 -0
  61. package/cpp/react/JetMarkdownMeasurer.cpp +32 -0
  62. package/cpp/react/JetMarkdownMeasurer.h +41 -0
  63. package/cpp/react/JetMarkdownShadowNode.cpp +100 -0
  64. package/cpp/react/JetMarkdownShadowNode.h +53 -0
  65. package/cpp/react/JetMarkdownState.h +52 -0
  66. package/cpp/react/override/react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h +21 -0
  67. package/cpp/tests/parser_tests.cpp +662 -0
  68. package/cpp/tests/run_tests.sh +25 -0
  69. package/ios/JetMarkdownView.h +14 -0
  70. package/ios/JetMarkdownView.mm +468 -0
  71. package/ios/editor/JetMarkdownEditor.h +12 -0
  72. package/ios/editor/JetMarkdownEditor.mm +2002 -0
  73. package/ios/measure/JMDMarkdownMeasurer.h +22 -0
  74. package/ios/measure/JMDMarkdownMeasurer.mm +38 -0
  75. package/ios/render/JMDBlock.h +97 -0
  76. package/ios/render/JMDBlock.m +28 -0
  77. package/ios/render/JMDBlockRenderer.h +17 -0
  78. package/ios/render/JMDBlockRenderer.mm +852 -0
  79. package/ios/render/JMDContentCache.h +17 -0
  80. package/ios/render/JMDContentCache.mm +42 -0
  81. package/ios/render/JMDRenderedContent.h +35 -0
  82. package/ios/render/JMDRenderedContent.mm +274 -0
  83. package/ios/style/JMDFontScale.h +33 -0
  84. package/ios/style/JMDLayoutStyle.h +37 -0
  85. package/ios/style/JMDLayoutStyle.mm +68 -0
  86. package/ios/style/JMDStyleConfig.h +39 -0
  87. package/ios/style/JMDStyleConfig.mm +111 -0
  88. package/ios/style/JMDTextStyle.h +25 -0
  89. package/ios/style/JMDTextStyle.mm +68 -0
  90. package/ios/views/JMDBlockStackView.h +18 -0
  91. package/ios/views/JMDBlockStackView.m +97 -0
  92. package/ios/views/JMDBlockTextView.h +24 -0
  93. package/ios/views/JMDBlockTextView.m +340 -0
  94. package/ios/views/JMDBoxViews.h +33 -0
  95. package/ios/views/JMDBoxViews.m +292 -0
  96. package/ios/views/JMDImageView.h +19 -0
  97. package/ios/views/JMDImageView.m +68 -0
  98. package/ios/views/JMDMarkdownHost.h +16 -0
  99. package/ios/views/JMDTableView.h +16 -0
  100. package/ios/views/JMDTableView.m +172 -0
  101. package/lib/module/JetMarkdownEditor.js +85 -0
  102. package/lib/module/JetMarkdownEditor.js.map +1 -0
  103. package/lib/module/JetMarkdownEditor.native.js +244 -0
  104. package/lib/module/JetMarkdownEditor.native.js.map +1 -0
  105. package/lib/module/JetMarkdownEditorNativeComponent.ts +173 -0
  106. package/lib/module/JetMarkdownView.js +19 -0
  107. package/lib/module/JetMarkdownView.js.map +1 -0
  108. package/lib/module/JetMarkdownView.native.js +44 -0
  109. package/lib/module/JetMarkdownView.native.js.map +1 -0
  110. package/lib/module/JetMarkdownViewNativeComponent.ts +35 -0
  111. package/lib/module/defaultStyles.js +122 -0
  112. package/lib/module/defaultStyles.js.map +1 -0
  113. package/lib/module/index.js +9 -0
  114. package/lib/module/index.js.map +1 -0
  115. package/lib/module/package.json +1 -0
  116. package/lib/module/serializeStyles.js +259 -0
  117. package/lib/module/serializeStyles.js.map +1 -0
  118. package/lib/module/types.js +4 -0
  119. package/lib/module/types.js.map +1 -0
  120. package/lib/module/useJetMarkdownEditor.js +41 -0
  121. package/lib/module/useJetMarkdownEditor.js.map +1 -0
  122. package/lib/typescript/package.json +1 -0
  123. package/lib/typescript/src/JetMarkdownEditor.d.ts +6 -0
  124. package/lib/typescript/src/JetMarkdownEditor.d.ts.map +1 -0
  125. package/lib/typescript/src/JetMarkdownEditor.native.d.ts +6 -0
  126. package/lib/typescript/src/JetMarkdownEditor.native.d.ts.map +1 -0
  127. package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts +105 -0
  128. package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts.map +1 -0
  129. package/lib/typescript/src/JetMarkdownView.d.ts +3 -0
  130. package/lib/typescript/src/JetMarkdownView.d.ts.map +1 -0
  131. package/lib/typescript/src/JetMarkdownView.native.d.ts +3 -0
  132. package/lib/typescript/src/JetMarkdownView.native.d.ts.map +1 -0
  133. package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts +28 -0
  134. package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts.map +1 -0
  135. package/lib/typescript/src/defaultStyles.d.ts +20 -0
  136. package/lib/typescript/src/defaultStyles.d.ts.map +1 -0
  137. package/lib/typescript/src/index.d.ts +7 -0
  138. package/lib/typescript/src/index.d.ts.map +1 -0
  139. package/lib/typescript/src/serializeStyles.d.ts +34 -0
  140. package/lib/typescript/src/serializeStyles.d.ts.map +1 -0
  141. package/lib/typescript/src/types.d.ts +352 -0
  142. package/lib/typescript/src/types.d.ts.map +1 -0
  143. package/lib/typescript/src/useJetMarkdownEditor.d.ts +36 -0
  144. package/lib/typescript/src/useJetMarkdownEditor.d.ts.map +1 -0
  145. package/package.json +150 -0
  146. package/react-native.config.js +11 -0
  147. package/src/JetMarkdownEditor.native.tsx +301 -0
  148. package/src/JetMarkdownEditor.tsx +90 -0
  149. package/src/JetMarkdownEditorNativeComponent.ts +173 -0
  150. package/src/JetMarkdownView.native.tsx +67 -0
  151. package/src/JetMarkdownView.tsx +16 -0
  152. package/src/JetMarkdownViewNativeComponent.ts +35 -0
  153. package/src/defaultStyles.ts +102 -0
  154. package/src/index.tsx +34 -0
  155. package/src/serializeStyles.ts +369 -0
  156. package/src/types.ts +438 -0
  157. 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 &amp; 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,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -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
+ }