Contributing
react-native-enriched-markdown is an open source library being developed at
software-mansion/enriched-markdown, under the MIT license.
Contributions are always welcome, no matter how large or small - a bug report, a missing line in these docs, or a fix in the native layer. If something in the library surprised you, the fix might belong upstream, and this page is the short version of how to land it.
We want this community to be friendly and respectful to each other, so before contributing please read the code of conduct and follow it in all your interactions with the project.
Not sure where to start? Open an issue describing what you need. For changes to the public API or the native implementation, discuss the design with the maintainers there before writing the code - it saves a rewrite.
Ways to contribute
- Report a bug. Include the platform, the library version, and the Markdown that reproduces it. See Compatibility to check your version pairing first.
- Request a feature. Tell us which element or prop is missing and what you are building - the native packages trail the React Native one, so demand decides what gets implemented next.
- Improve the docs. Every page has an Edit this page link at the bottom that takes you straight to the file on GitHub.
- Send a pull request. Bug fixes, new features, platform parity work, and examples are all fair game.
Project layout
The project is a monorepo managed with Yarn workspaces, holding the library packages for every platform, an example app per platform, and this documentation site.
Packages
| Path | What it is |
|---|---|
packages/react-native-enriched-markdown/ | The React Native library published to npm - the JS API, its iOS and Android bridging code, and the web build. |
packages/core/ | The shared C++ core: the md4c parser and the Markdown AST. Every other package compiles these same sources - there is exactly one parser in the repository. |
packages/enriched-markdown-ios/ | The standalone iOS package - a SwiftPM package wrapping the core in a Swift API, plus two optional products: EnrichedMarkdownLaTeX for math and EnrichedMarkdownSyntaxHighlighting for code-block highlighting. |
packages/enriched-markdown-android/ | The standalone Android package - Gradle modules for the span-based ui renderer and its compose wrapper, over a parser module that is a JNI binding to the core, plus two optional modules: math, which renders LaTeX, and code-highlight, which colors fenced code blocks. |
The two native packages are standalone libraries for native iOS and Android apps. They render with their own platform text stack but parse through the same core as the React Native package, so a parser fix lands for every platform at once. They trail React Native in features, so parity work is always welcome.
Example apps
Each app lives in apps/ and is driven from the repository root:
| Path | Root command | What it covers |
|---|---|---|
apps/react-native-example/ | yarn react-native-example <start|ios|android> | The main development app - Storybook lives here. (The Maestro E2E suite lives at the repository root, in .maestro/.) |
apps/react-native-macos-example/ | yarn react-native-macos-example <start|macos> | The react-native-macos build. |
apps/react-native-web-example/ | yarn react-native-web-example web | The web build running in a browser (Expo). |
apps/ios-example/ | yarn ios-example | A native Swift app using the iOS package directly. |
apps/android-example/ | yarn android-example | A native Android app using the Android package directly. |
Documentation
| Path | What it is |
|---|---|
docs/ | This site - a standalone Docusaurus project; see Working on the docs. |
Setting up
You need the Node.js version from the repository's .nvmrc, plus the toolchains for whatever you touch:
| For | You need |
|---|---|
| Everything | Node (see .nvmrc), Yarn 4 (via Corepack - the repo pins it in packageManager) |
| iOS | Xcode (CI builds on 26.3), Ruby 3.3.8 (see .tool-versions) and CocoaPods via the example app's Gemfile (bundle install, then bundle exec pod install) |
| Android | JDK 17 and the Android SDK |
| Swift lint | SwiftLint 0.65.0 - brew install swiftlint |
| E2E tests | The Maestro CLI 2.5.0 or newer |
Then:
git clone https://github.com/software-mansion/enriched-markdown.git
cd enriched-markdown
# Install dependencies
yarn
# Build the library - required before running any app
yarn prepare
On iOS, install the pods for whichever example app you are running before the first build:
cd apps/react-native-example/ios && bundle install && bundle exec pod install
The project relies on Yarn workspaces, so you cannot use npm for development without manually migrating.
Running the examples
Every package has its own example app, and each one builds against the local sources - so your change shows up without publishing anything. Run yarn prepare in the repository root first; the React Native build, the iOS package's vendored LaTeX and tree-sitter sources and the Android code-highlight module's tree-sitter sources all come from it.
React Native
apps/react-native-example/ uses the local library, so it is where you test changes to packages/react-native-enriched-markdown/. JavaScript changes are picked up by Metro; native changes require rebuilding the app.
yarn react-native-example start # start the packager
yarn react-native-example ios # run on iOS
yarn react-native-example android # run on Android
The library is Fabric-only, and the example app turns the New Architecture on explicitly (newArchEnabled=true on Android, RCT_NEW_ARCH_ENABLED=1 in the Podfile), so there is nothing to switch on before you start.
To work on the package's native layer in a native IDE:
- iOS - open
apps/react-native-example/ios/EnrichedMarkdownExample.xcworkspacein Xcode; the Objective-C and Swift sources are under Pods > Development Pods > react-native-enriched-markdown. - Android - open
apps/react-native-example/androidin Android Studio; the Java and Kotlin sources are under react-native-enriched-markdown in the Android view.
The same package also powers two more apps: yarn react-native-macos-example macos for the macOS build and yarn react-native-web-example web for the web build.
Storybook
Storybook is embedded in the React Native example app as a dedicated screen - run the app and open Storybook from the home screen. Stories live in apps/react-native-example/.rnstorybook/stories/.
iOS package
apps/ios-example/ is a plain Swift app that references packages/enriched-markdown-ios/ as a local Swift package, so edits to the package land in the app on the next build - nothing to link or re-resolve.
yarn ios-example # build, boot a simulator, install, and launch
To work in Xcode, open apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample.xcodeproj. If you are only changing the package, open packages/enriched-markdown-ios/Package.swift instead.
Tests and lint run from the package itself, against a booted simulator (or set IOS_SIMULATOR_UDID):
yarn workspace @enriched-markdown/ios test:ios-native # xcodebuild test
yarn workspace @enriched-markdown/ios lint:ios-native # swiftlint lint --strict
Both optional products build from gitignored sources that are restored on demand. EnrichedMarkdownLaTeX reaches the RaTeX engine's Swift sources and the KaTeX fonts through symlinks into the React Native package's ios/vendor/, restored by node vendor/vendor-ratex.mjs. EnrichedMarkdownSyntaxHighlighting compiles tree-sitter and its grammars through symlinks into packages/core/cpp/highlight/vendor/, restored by node vendor/vendor-grammars.mjs. yarn prepare runs both. CI runs both scripts before every iOS job - build, tests, and lint - so do the same before building the package with a fresh checkout. (The RaTeX binary itself is a binaryTarget SwiftPM downloads from its pinned release, not a vendored file.)
Android package
apps/android-example/ is a plain Android app whose settings.gradle includes the package's ui, compose, parser, math, and code-highlight modules straight from packages/enriched-markdown-android/, so the app always builds the sources in your working tree.
yarn android-example # install the debug build and launch it on a device or emulator
Open apps/android-example/ in Android Studio to work on the app and the library modules side by side.
Tests and lint run from the package itself:
yarn workspace @enriched-markdown/android test:android-native # ./gradlew testDebugUnitTest
yarn workspace @enriched-markdown/android lint:android-native # ./gradlew ktlintCheck lintDebug
The code-highlight module compiles tree-sitter and its grammars from gitignored sources under packages/core/cpp/highlight/vendor/ - the same ones the iOS package uses - restored by node vendor/vendor-grammars.mjs (yarn prepare runs it). Without them, the module's build stops at its checkHighlightVendor task with that command in the error. CI restores them, through the restore-android-vendor action, before every Android package job - build, tests, and lint - so do the same before building with a fresh checkout.
Checks before you push
Lefthook covers part of this for you on every commit: commitlint on the message, ESLint and yarn typecheck on staged JS and TS, clang-format on staged C, C++ and Objective-C and ktlint formatting on staged Kotlin. The last two rewrite and re-stage your files rather than just complaining. Nothing else is automatic - the standalone packages' Swift and Kotlin checks and every test suite are yours to run, and CI runs them again.
Run the checks for whatever you touched.
React Native and shared code
yarn lint # ESLint + Prettier
yarn lint --fix # fix formatting errors
yarn typecheck # tsc
yarn test # Jest unit tests
yarn lint-clang # clang-format check on the package's iOS and Android native sources
yarn lint-clang:fix
yarn lint-kotlin # ktlint on the package's Android sources
yarn lint-kotlin:fix
yarn test:android # Robolectric JVM unit tests for the package's Android code
yarn test:ios # XCTest unit tests for the package's iOS code
yarn lint-clang checks the React Native package's ios/ and android/ directories. The shared C++ in packages/core/ is not covered by it, so keep that formatting tidy by hand.
Swift (iOS package)
The test task needs Xcode and a booted simulator (or IOS_SIMULATOR_UDID), and the vendored RaTeX and tree-sitter files restored - see the note above. The lint task is swiftlint lint --strict and needs neither.
yarn workspace @enriched-markdown/ios lint:ios-native # swiftlint lint --strict
yarn workspace @enriched-markdown/ios test:ios-native # xcodebuild test
yarn test:ios-native # root alias, what CI runs (hits the turbo cache)
Kotlin (Android package)
yarn workspace @enriched-markdown/android lint:android-native # ./gradlew ktlintCheck lintDebug
yarn workspace @enriched-markdown/android test:android-native # ./gradlew testDebugUnitTest
yarn test:android-native # root alias, what CI runs (hits the turbo cache)
Both tasks need the vendored tree-sitter files restored - see the note above.
The commit hook already formats staged Kotlin in both trees (it has a separate ktlint-android command rooted at packages/enriched-markdown-android/). lint:android-native is the verifying pass - ktlintCheck for formatting plus lintDebug for Android Lint - and it is what CI runs.
End-to-end tests
User-visible behavior is covered by Maestro E2E tests - add tests for your change when it is user-visible. A simulator or emulator must be available; the runner boots one for you.
The runner requires the Maestro CLI 2.5.0 or newer and stops with an error on anything older.
yarn test:e2e:ios
yarn test:e2e:android
yarn test:e2e:mobile # both platforms sequentially
Script names follow the pattern test:e2e[:<config>]:<platform>[:update-screenshots]:
<platform>-ios,android, ormobile(both sequentially).<config>- optional tag filter; omitting it runs all tests. Named configs aresmoke(quick sanity check with basic examples) andadvanced(extended tests with combinations of different elements, advanced configuration options, etc.).update-screenshots- instead of asserting, refreshes the stored screenshot baselines. Not available for config-filtered runs - there is notest:e2e:smoke:ios:update-screenshots.
The pattern is not a free combination. There are also ios-native and android-native platforms for the standalone example apps, and there the suffix order inverts: yarn test:e2e:android-native:smoke, not test:e2e:smoke:android-native. Run yarn run to see the exact list.
yarn test:e2e:smoke:ios # smoke tests on iOS only
yarn test:e2e:advanced:android # advanced tests on Android only
If your change affects visual output, refresh the baselines:
yarn test:e2e:ios:update-screenshots
yarn test:e2e:android:update-screenshots
Pass --rebuild to force a fresh build of the example app.
Docs site
yarn build and yarn check-examples inside docs/ - see Working on the docs.
What CI will run
Two sets of checks guard a pull request, and they are independent: the platform checks for the library code, and the docs checks for this site. A docs-only change runs no platform checks; a code-only change runs no docs checks.
Which platform checks fire
CI first works out which platforms your diff touches, and every check runs only for a platform that was touched. The matching is wider than it looks, because shared files count as all three platforms:
| Platform | Touched by changes to |
|---|---|
| React Native | packages/react-native-enriched-markdown/**, apps/react-native-example/** |
| Android | packages/enriched-markdown-android/**, apps/android-example/** |
| iOS | packages/enriched-markdown-ios/**, apps/ios-example/** |
| all three | packages/core/**, package.json, yarn.lock, .nvmrc, .yarnrc.yml, .github/** |
So a one-line change in the shared C++ core has to pass every platform's checks, while a change confined to one package only runs that package's.
What each check does
| Platform | Check | What it runs |
|---|---|---|
| React Native | rn-lint | yarn lint, yarn typecheck, yarn test |
| React Native | rn-build-library | yarn prepare |
| React Native | rn-build-android | Builds the React Native example app for Android. |
| React Native | rn-build-ios | Builds the React Native example app for iOS. |
| React Native | rn-android-unit-tests | Robolectric JVM unit tests for the package's Android sources. |
| React Native | rn-ios-unit-tests | App-hosted XCTest unit tests for the package's iOS sources, on a headless simulator. |
| Android | android-build | Builds apps/android-example/ against the package's Gradle modules. |
| Android | android-unit-tests | ./gradlew testDebugUnitTest |
| Android | android-kotlin-lint | ./gradlew ktlintCheck lintDebug (ktlint and Android Lint) |
| iOS | ios-build | Builds apps/ios-example/ against the local Swift package. |
| iOS | ios-unit-tests | xcodebuild test against a simulator. |
| iOS | ios-swift-lint | swiftlint lint --strict |
A final ci-success check is the one merging depends on: it fails if any check required for your platforms did not succeed, and passes immediately when your change touched none of them.
Docs
A pull request that touches docs/** gets two more checks, in parallel: one verifies that the interactive examples still mirror the pages that own them (yarn check-examples), the other builds the site (yarn build).
What CI does not run
Some checks exist only on your machine - if you skip them, nothing downstream will catch the problem:
- The Maestro E2E tests. Nothing runs them, on any platform. Screenshot baselines and flows are verified locally or not at all, so run the suites yourself when your change is user-visible.
yarn lint-clang. clang-format runs over staged files in the pre-commit hook, and its glob is repo-wide, so stagedpackages/core/C++ is reformatted. What is missing is verification: nothing checks the formatting of a file you did not stage.yarn lint-kotlin. The Kotlin lint check covers onlypackages/enriched-markdown-android/; the React Native package's Kotlin is left to the commit hook.- The macOS and web example apps. Neither is built by any check.
Working on the docs
This site is a standalone Docusaurus project inside docs/, with its own yarn.lock:
cd docs
yarn
yarn start # dev server
yarn build # must pass - broken links and anchors fail the build
yarn check-examples # verifies interactive examples match the pages that own them
Interactive examples run the library's web build, so run yarn prepare in the repository root first - otherwise the examples cannot resolve react-native-enriched-markdown.
Commit messages
We follow the conventional commits specification, and the pre-commit hooks verify the format:
| Prefix | Use for |
|---|---|
fix | Bug fixes, e.g. fix crash due to deprecated method. |
feat | New features, e.g. add new method to the module. |
refactor | Code refactor, e.g. migrate from class components to hooks. |
docs | Documentation changes, e.g. add usage example for the module. |
test | Adding or updating tests. |
chore | Tooling changes, e.g. change CI config. |
Sending a pull request
Keep a pull request small and focused on a single change - it is far easier to review, and far easier to revert if something turns out wrong. If the change touches the public API or the native implementation, open an issue and agree on the design with the maintainers before you write the code.
Opening a pull request fills the description from our pull request template, which asks for three things: what the change does and why it is needed, how to test it, and a checklist to confirm before requesting a review. Screenshots are welcome for anything visual - the template has commented-out markup for side-by-side iOS/Android and before/after tables.
The checklist is where most review rounds are saved. It asks you to confirm that the code compiles and runs on both iOS and Android, that you ran the example app to see the change, that the documentation is updated if the change is user-facing, and that the E2E tests pass and cover the new behavior. Those last two items matter more than they look: nothing in CI runs the E2E suites for you (see What CI will run), so an unticked box there is the only signal a reviewer gets.
CONTRIBUTING.md in the repository covers two things this page does not: publishing to npm, and the complete script list. This page is the only written description of the CI model, so the two are complements rather than one being a subset of the other.