@discord/markdown-react-native
v0.2.2
Published
React Native (JSI) bindings for parsing Discord Markdown
Readme
markdown-react-native
React Native bindings for the Discord Markdown parser (@discord/markdown-react-native),
implemented as a pure C++ TurboModule. The Rust parser is compiled to a static library
(ffi/c) and linked into a single C++ module
(cpp/NativeDiscordMarkdown.cpp) that both platforms share;
there is no per-call bridge traffic and no platform-specific parser code.
Requires the New Architecture (React Native 0.77+).
Usage
import { parse } from "@discord/markdown-react-native";
const blocks = parse("# Hello **world**");
// [{ type: "heading", value: { level: 1, content: [...] } }]
// Optionally restrict which rules parse:
const restricted = parse("**bold** _italic_", ["bold"]);parse runs synchronously on the JS thread and returns the same typed AST
(Block[] from @discord/markdown-types) as the wasm binding, so rendering code
can be shared between web and native. Snowflake IDs and timestamp values are BigInts, also
matching the wasm binding.
How it works
src/NativeDiscordMarkdown.tsis the TurboModule spec; codegen (configured throughcodegenConfiginpackage.json) generates the typed C++ base classNativeDiscordMarkdownCxxSpec.cpp/NativeDiscordMarkdown.cppimplements the spec by calling the Rust parser's C ABI (dmd_parse).parseToAstStringis a synchronous JSI method — no bridge, no async hop.- Registration is declarative, with no app changes required:
- Android:
react-native.config.cjspoints autolinking atandroid/CMakeLists.txt; the generatedautolinking.cppinstantiates the module, and the C++ compiles inside the app's native build. The Gradle project here only runs codegen (and cargo, in-repo). - iOS:
codegenConfig.ios.modulesProvidermaps the module toios/DiscordMarkdownModuleProvider.mm.
- Android:
- The JS wrapper decodes the returned JSON. 64-bit integers cross the boundary as
{"$bigint": "…"}wrappers (seeffi/c/src/bigint.rs) and are revived asBigInts, since a plainJSON.parsewould corrupt them as float64s.
Building
Inside this repo, the native builds drive cargo themselves; consumers from npm receive the
Rust parser prebuilt (android/prebuilt/, ios/DiscordMarkdownC.xcframework), built at
publish time.
Android (Gradle runs cargo-ndk for armeabi-v7a,
arm64-v8a, x86, and x86_64, or the subset in reactNativeArchitectures):
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
cargo install cargo-ndkiOS (the example app's Podfile runs ios/build-rust.sh, which also
assembles the vendored xcframework; re-run it after changing the parser):
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-iosJS tests (the decoding layer; the native layer is tested in cargo test -p discord-markdown-c):
pnpm testExample app
example/ is a React Native playground app (a workspace package, like
site for web): a live markdown → AST view over the native parser, for manual
verification and development. From the repo root:
pnpm install
cd lib/react-native/example
pnpm android # or open example/android in Android Studio
cd ios && bundle exec pod install # then: pnpm ios, or open MarkdownExample.xcworkspaceEnd-to-end tests
The e2e_react_native CI jobs build the example app for
release on both platforms — exercising codegen, autolinking, and the in-repo Rust builds —
then drive it on an Android emulator and an iOS simulator with
Maestro (example/.maestro/parse.yaml),
asserting on the AST the app renders: default content parses to a heading with a
full-precision BigInt mention id, and typed content re-parses live. They then pack the npm
tarball and assert the prebuilt Rust artifacts ship in it. The same flow runs locally with
maestro test -e APP_ID=… .maestro/parse.yaml against any running emulator or simulator.
Known limitations
- New Architecture only. On the old architecture
TurboModuleRegistry.getEnforcingthrows at import time. - Worklet runtimes (Reanimated, etc.) are separate JSI runtimes;
parseis only callable from the main JS runtime.
