@discord/markdown-react-native

中文文档
0.14.0发布于 2026/11/05MITTypeScriptES Module近 30 天下载 19.7万

React Native (JSI) bindings for parsing Discord Markdown

原始使用文档
文档目录7 节
  1. Usage
  2. Native usage (Kotlin and Swift)
  3. How it works
  4. Building
  5. Example app
  6. End-to-end tests
  7. Known limitations

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 native 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. The same library is also callable from Kotlin and Swift.

Requires the New Architecture (React Native 0.77+).

Usage

import { Syntax, parse } from "@discord/markdown-react-native";

const blocks = parse("# Hello **world**");
// [{ type: "heading", value: { level: 1, content: [...] } }]

// Optionally pick the syntax to parse, as an OR of `Syntax` bits (`Syntax.ALL` by default):
const restricted = parse("**bold** _italic_", Syntax.BOLD);
const noMentions = parse("@everyone **hi**", Syntax.ALL & ~Syntax.MENTIONS);

// Optionally cap how much work a parse may do:
const bounded = parse(untrustedContent, null, 50_000);

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.

Because the parse blocks the JS thread, the third argument is worth knowing about: it is the parser's fuel budget, which bounds the work an adversarial input can cause. Omitting it takes a default sized for message-length content with room to spare, so most callers should. Content that exhausts the budget throws parser ran out of fuel and produces no partial AST — a budget set too low rejects legitimate content, so measure your own before tightening it.

For rich text, parseLossless(content, flags?, fuel?) returns the lossless tree instead: nodes for what the markdown means, and tokens holding every character of content in order, down to delimiters, escapes, markers and line endings, so joining their text gives it back exactly. Every node and token has a span in UTF-16 code units, so content.slice(span.start, span.end) is its text. The types are LosslessNode and friends in @discord/markdown-types.

Native usage (Kotlin and Swift)

Installing the package also gives the app's own native code a direct line to the parser, with no JS runtime involved. Each platform gets a small wrapper over the same parser the TurboModule uses:

import com.discord.markdown.DiscordMarkdown

val ast: String = DiscordMarkdown.parse("# Hello **world**")
val restricted = DiscordMarkdown.parse("**bold** _italic_", flags = DiscordMarkdown.Syntax.BOLD)
import DiscordMarkdown

let ast: String = try DiscordMarkdownParser.parse("# Hello **world**")
let restricted = try DiscordMarkdownParser.parse("**bold** _italic_", syntax: .bold)

Both return the AST as a JSON string, in the same shape as JS's except for 64-bit integers. Those are plain JSON numbers at full precision, not {"$bigint": "…"} objects. Snowflakes are unsigned and may exceed Long.MAX_VALUE, so decode them as UInt64 (Swift's JSONDecoder) or ULong (kotlinx.serialization). Timestamps are signed: decode them as Int64 / Long. Android's built-in org.json is not enough, because it reads any integer past Long.MAX_VALUE as a Double. Both take the same optional fuel budget as the JS parse. Failures throw DiscordMarkdownException (Kotlin) or DiscordMarkdownError (Swift), carrying the parser's message. Calls are synchronous and keep no state, so they are safe from any thread.

parseLossless (Kotlin) and DiscordMarkdownParser.parseLossless (Swift) return the lossless tree as JSON, with the same integers. Its spans count UTF-16 code units, so they index a Kotlin String or Spannable directly, and in Swift are an NSRange into the content as an NSString.

The wrappers live in android/src/main/java/com/discord/markdown/DiscordMarkdown.kt (over JNI entry points in ffi/jni) and ios/DiscordMarkdownParser.swift (over the C ABI, through the DiscordMarkdownC module map in the xcframework). They ship inside the React Native package, so they are only available to targets that link it: an iOS app extension, for example, cannot use them without the React Native pod.

How it works

  • src/NativeDiscordMarkdown.ts is the TurboModule spec; codegen (configured through codegenConfig in package.json) generates the typed C++ base class NativeDiscordMarkdownCxxSpec.
  • cpp/NativeDiscordMarkdown.cpp implements the spec by calling the Rust parser's C ABI (dmd_parse and dmd_parse_lossless). parseToAstString and parseLosslessToAstString are synchronous JSI methods — no bridge, no async hop.
  • Registration is declarative, with no app changes required:
    • Android: react-native.config.cjs points autolinking at android/CMakeLists.txt; the generated autolinking.cpp instantiates the module, and the C++ compiles inside the app's native build, linked against the Rust parser as libdiscord_markdown_jni.so (ffi/jni, which re-exports the C ABI alongside its JNI entry points). The Android Gradle plugin packages that shared library into the APK, where the Kotlin wrapper loads the same copy. The Gradle project here runs codegen and builds the Kotlin wrapper (and runs cargo, in-repo).
    • iOS: codegenConfig.ios.modulesProvider maps the module to ios/DiscordMarkdownModuleProvider.mm.
  • The JS wrapper decodes the returned JSON. The TurboModule passes the C ABI's js_bigints flag, so 64-bit integers cross the boundary as {"$bigint": "…"} wrappers (see ffi/c/src/bigint.rs) and are revived as BigInts, since a plain JSON.parse would corrupt them as float64s. The Kotlin and Swift wrappers leave the flag off and get plain numbers.

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 on ffi/jni, 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-ndk

iOS (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-ios

JS tests (the wrapper and the decoding layer; the parser and its C ABI, including the fuel argument, are tested in cargo test -p discord-markdown-c):

pnpm test

Example 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.xcworkspace

End-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. The app's MainActivity.kt and AppDelegate.swift also round-trip content through the Kotlin or Swift wrapper at launch, and the flow checks the result the app reports. 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.getEnforcing throws at import time.
  • Worklet runtimes (Reanimated, etc.) are separate JSI runtimes; parse is only callable from the main JS runtime.

版本与发布记录

@discord/markdown-react-native 共发布 16 个版本,首次发布于 2026/07/15;过去 12 个月发布了 16 个版本。

版本发布日期距今
0.14.0latest2026/11/057 天前
0.13.02026/10/2716 天前
0.12.02026/10/2419 天前
0.11.02026/10/1627 天前
0.10.02026/10/1429 天前
0.9.02026/10/071 个月前

关于 @discord/markdown-react-native 的常见问题

@discord/markdown-react-native 是什么?

React Native (JSI) bindings for parsing Discord Markdown

如何安装 @discord/markdown-react-native?

在项目目录运行 npm install @discord/markdown-react-native,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react-native(>=0.77)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@discord/markdown-react-native 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@discord/markdown-react-native 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@discord/markdown-react-native 可以商用吗?

@discord/markdown-react-native 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@discord/markdown-react-native 的下载量有多少?

根据 npm 官方下载统计,@discord/markdown-react-native 在 2026/10/13 – 2026/11/11 这 30 天内被下载 197,079 次,日均约 6,569 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@discord/markdown-react-native 还在维护吗?

最新版本 0.14.0 发布于 2026/11/05(7 天前);至今共发布 16 个版本,其中过去 12 个月发布了 16 个;首次发布于 2026/07/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。