Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ Pre-1.0, the JS API may evolve between minor versions. See the README's roadmap.

## [Unreleased]

### Added

- **Android support.** Bare RN Fabric component wrapping [Sora-Editor](https://github.com/Rosemoe/sora-editor) 0.23.6 (LGPL-2.1, consumed dynamically as a Gradle `implementation` AAR). Targets react-native 0.81 New Architecture, minSdk 24. ([#34](https://github.com/workspace-sh/react-native-source-editor/pull/34), closes [#33](https://github.com/workspace-sh/react-native-source-editor/issues/33))
- **TextMate syntax highlighting on Android** for the existing six languages (markdown / json / javascript / typescript / html). VS Code 1.94.0 grammars + `darcula.json` theme bundled as Android assets. ([#36](https://github.com/workspace-sh/react-native-source-editor/pull/36), closes [#35](https://github.com/workspace-sh/react-native-source-editor/issues/35))
- **`font`, `contentInsets`, `lineNumbers` on Android.** `font` → Sora `typefaceText` + `textSize`; `contentInsets` → `View.setPadding` with dp→px conversion; `lineNumbers` → `editor.isLineNumberEnabled`. ([#38](https://github.com/workspace-sh/react-native-source-editor/pull/38), closes [#37](https://github.com/workspace-sh/react-native-source-editor/issues/37))
- **`theme` prop on Android.** `light` / `dark` map to bundled TextMate themes; `auto` reads `Configuration.UI_MODE_NIGHT_MASK`. ([#40](https://github.com/workspace-sh/react-native-source-editor/pull/40), closes [#39](https://github.com/workspace-sh/react-native-source-editor/issues/39))
- **Expo config plugin: Android support.** `app.plugin.js` now also injects `coreLibraryDesugaring` into `android/app/build.gradle` (Sora's TextMate AAR requires it on minSdk < 26). Same plugin entry covers both platforms. ([#36](https://github.com/workspace-sh/react-native-source-editor/pull/36))
- **CI: Android + macOS jobs.** GitHub Actions now builds all three platforms on every PR. ([#42](https://github.com/workspace-sh/react-native-source-editor/pull/42), closes [#41](https://github.com/workspace-sh/react-native-source-editor/issues/41))

### Changed

- **`example/ios-app/` → `example/expo-app/`.** Same Expo CNG project now serves both iOS and Android — Expo prebuild generates `ios/` and `android/` from the same source. The bare-RN `example/macos-app/` stays separate (Expo doesn't support macOS). ([#34](https://github.com/workspace-sh/react-native-source-editor/pull/34))

## [0.1.0] — 2026-05-10

First milestone tag. Both target platforms shipping; library not yet on npm (install via local path until v1.0).
Expand Down
46 changes: 32 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# react-native-source-editor

A native source editor component for React Native, built as a Fabric component wrapping [STTextView](https://github.com/krzyzanowskim/STTextView) on iOS and macOS. Designed to drop into both Expo CNG apps (via the included config plugin) and bare React Native / react-native-macos hosts.
A native source editor component for React Native. Fabric component wrapping [STTextView](https://github.com/krzyzanowskim/STTextView) on iOS and macOS, and [Sora-Editor](https://github.com/Rosemoe/sora-editor) on Android. Designed to drop into both Expo CNG apps (via the included config plugin) and bare React Native / react-native-macos hosts.

## Status

iOS and macOS both ship today on the New Architecture. iOS targets Expo CNG (with the bundled config plugin); macOS targets bare react-native-macos 0.81. Pre-1.0, expect the JS API to evolve.
iOS, macOS, and Android all ship today on the New Architecture. iOS and Android target Expo CNG (with the bundled config plugin); macOS targets bare react-native-macos 0.81. Pre-1.0, expect the JS API to evolve.

Project tracking lives on the [project board](https://github.com/orgs/workspace-sh/projects/2).

Expand All @@ -14,22 +14,24 @@ Project tracking lives on the [project board](https://github.com/orgs/workspace-
| --- | --- | --- |
| iOS | Shipping (Fabric / New Architecture) | 16.0 |
| macOS | Shipping (Fabric / New Architecture) | 14.0 |
| Android | Shipping (Fabric / New Architecture) | API 24 |
| iPadOS | Roadmap | — |
| Android | Stub only | — |
| Windows | Roadmap | — |
| Linux | Roadmap | — |
| Web | Out of scope for v1 | — |

iOS / macOS minimums are STTextView's floors, not ours.
iOS / macOS minimums are STTextView's floors, Android's is Sora-Editor's — not ours.

## Quick start

```sh
# Library is not yet on npm — install via local path while v1 is in flight
npm install /path/to/react-native-source-editor
gem install cocoapods-spm # required: STTextView is SPM-only
gem install cocoapods-spm # iOS only: STTextView is SPM-only on Apple platforms
```

(macOS and Android skip `cocoapods-spm` — macOS uses RN's first-party `spm_dependency` helper; Android pulls Sora-Editor from Maven Central.)

```tsx
import SourceEditor from '@workspace-sh/react-native-source-editor';

Expand All @@ -45,14 +47,15 @@ export default function Editor() {
}
```

For Expo CNG apps, register the plugin in `app.json` so `cocoapods-spm` + the STTextView SPM package get injected into the Podfile during `expo prebuild`:
For Expo CNG apps, register the plugin in `app.json` so the iOS Podfile gets `cocoapods-spm` + the STTextView SPM package, and the Android `app/build.gradle` gets `coreLibraryDesugaring` (Sora-Editor's TextMate AAR requires it on minSdk < 26):

```json
{
"plugins": [
"@workspace-sh/react-native-source-editor",
["expo-build-properties", {
"ios": { "deploymentTarget": "16.0", "useFrameworks": "static" }
"ios": { "deploymentTarget": "16.0", "useFrameworks": "static" },
"android": { "minSdkVersion": 24, "newArchEnabled": true }
}]
]
}
Expand All @@ -66,9 +69,9 @@ Bare-RN hosts: see [docs/installation.md](docs/installation.md) for the manual `

## Roadmap

- **v0.x (now)** — bare RN Fabric library, iOS + macOS shipping. JS API: text, selection, font, theme, language (markdown / json / js / ts / html), `lineNumbers` gutter toggle, `contentInsets`, imperative `focus`/`blur`.
- **v0.x (now)** — bare RN Fabric library, iOS + macOS + Android shipping. JS API: text, selection, font, theme, language (markdown / json / js / ts / html), `lineNumbers` gutter toggle, `contentInsets`, imperative `focus`/`blur`.
- **v1.0** — expanded font customisation, npm publish under `@workspace-sh/react-native-source-editor`.
- **Post-1.0** — iPadOS polish, Android (TextKit alternative), Windows, Linux, Web.
- **Post-1.0** — iPadOS polish, Windows, Linux, Web.

## Contributing

Expand All @@ -78,9 +81,10 @@ Contributions and bug reports are welcome.

- `src/` — TypeScript surface. `SourceEditor.tsx` is the high-level wrapper; `SourceEditorView.tsx` exposes the lower-level codegen Fabric component.
- `ios/` — Native sources, shared across iOS + macOS. `SourceEditor.{h,mm}` is the Fabric Obj-C++ wrapper; `SourceEditorImpl.swift` and `Highlighter.swift` are the Swift impl (UIKit/AppKit branched via `#if os(iOS)` / `#elseif os(macOS)`). `ReactNativeSourceEditor.h` is the framework's umbrella anchor.
- `example/ios-app/` — Expo SDK 55 host. Uses Expo CNG, owned by `expo prebuild` — never run `pod install` here manually.
- `android/` — Kotlin Fabric component wrapping Sora-Editor. `SourceEditorPackage.kt` (autolinked entry), `SourceEditorViewManager.kt` (codegen-driven), `SourceEditorView.kt` (Sora `CodeEditor` host), `SoraTextMate.kt` (grammar + theme registry), `events/*.kt`. TextMate grammar JSONs and themes live under `src/main/assets/textmate/`.
- `example/expo-app/` — Expo SDK 55 host serving both iOS and Android. Uses Expo CNG, owned by `expo prebuild` — never run `pod install` (iOS) or hand-edit `android/` here manually.
- `example/macos-app/` — react-native-macos 0.81 host. Standard bare-RN toolchain (`pod install`, `react-native run-macos`). Wires STTextView via RN's first-party `spm_dependency` helper rather than the third-party `cocoapods-spm` plugin used on iOS; see the app's [README](example/macos-app/README.md).
- `app.plugin.js` — Expo config plugin. Injects `cocoapods-spm` + the `STTextView` SPM package into the consumer's `Podfile` during `expo prebuild` (iOS path only).
- `app.plugin.js` — Expo config plugin. Injects `cocoapods-spm` + the `STTextView` SPM package into the iOS Podfile and `coreLibraryDesugaring` into the Android `app/build.gradle` during `expo prebuild`.
- `ReactNativeSourceEditor.podspec` — root podspec; bare RN libraries put it here so autolinking finds it.

### Development setup
Expand All @@ -94,13 +98,25 @@ npm run typecheck

```sh
npm run ios:plugin # one-time: gem install cocoapods-spm
npm run ios:install # one-time: install example/ios-app deps
npm run ios:install # one-time: install example/expo-app deps
npm run ios:run # build + launch on iOS simulator
npm run ios:dev # concurrently: clean Metro + run:ios
```

Other scripts: `ios:start`, `ios:clear`, `ios:run:device`, `ios:run:device:release`, `ios:clean` (wipes generated `ios/`), `ios:prebuild`. Pod install is owned by `expo run:ios` — there is intentionally no `ios:pods` script.

### Running the Android example

Same Expo project as iOS — different `android:*` script set:

```sh
npm run android:install # one-time: install example/expo-app deps
npm run android:run # prebuild + gradle build + launch on emulator/device
npm run android:dev # concurrently: clean Metro + run-android
```

Other scripts: `android:start`, `android:clear`, `android:run:device`, `android:clean` (wipes generated `android/`), `android:prebuild`. Requires JDK 17 + an Android SDK with API 35 + an emulator or device.

### Running the macOS example

```sh
Expand All @@ -116,10 +132,12 @@ Other scripts: `macos:start`, `macos:clear`, `macos:run`, `macos:clean` (wipes `
- Branch off `develop`; PRs target `develop`. `main` mirrors the latest release.
- One PR per project-board issue. Reference the issue in the PR description.
- Squash-merge with the PR title as the commit subject.
- CI runs typecheck + iOS build via `.github/workflows/ci.yml`.
- CI runs typecheck + iOS + Android + macOS builds via `.github/workflows/ci.yml`.

## License

[MIT](LICENSE) © Leslie Owusu-Appiah.

Built on top of [STTextView](https://github.com/krzyzanowskim/STTextView) by [Marcin Krzyzanowski](https://github.com/krzyzanowskim) (BSD-2-Clause). Without it, this library would not exist.
Built on top of [STTextView](https://github.com/krzyzanowskim/STTextView) by [Marcin Krzyzanowski](https://github.com/krzyzanowskim) (BSD-2-Clause) on Apple platforms, and [Sora-Editor](https://github.com/Rosemoe/sora-editor) by [Rosemoe](https://github.com/Rosemoe) (LGPL-2.1) on Android. Without them, this library would not exist.

The Sora-Editor dependency is consumed dynamically as a Gradle `implementation` AAR (no source modification, no static linking) — the standard pattern for LGPL compliance in MIT-licensed downstream apps. If you fork the AAR or relink statically, you take on the LGPL relinking obligations yourself.
14 changes: 9 additions & 5 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ Each example is a runnable React Native app under [`example/`](../example/).

| Platform | App | Toolchain | Status |
| --- | --- | --- | --- |
| iOS | [`example/ios-app/`](../example/ios-app/) | Expo SDK 55 (CNG) | Shipping |
| iOS | [`example/expo-app/`](../example/expo-app/) | Expo SDK 55 (CNG) | Shipping |
| Android | [`example/expo-app/`](../example/expo-app/) | Expo SDK 55 (CNG) | Shipping |
| macOS | [`example/macos-app/`](../example/macos-app/) | react-native-macos 0.81 (bare RN) | Shipping |

Each app demonstrates the editor in a split-pane layout with markdown on one side and TypeScript on the other, alongside standard RN components (`Switch`, `Button`, `SafeAreaView`) so the integration story is visible.
iOS and Android share the same Expo CNG project (`expo-app/`) — Expo prebuild generates `ios/` and `android/` from the same `App.tsx` / `App.android.tsx` pair. The macOS example lives in its own bare-RN-macos project because Expo doesn't support the platform.

The two examples deliberately use different toolchains so the library is exercised through both the Expo CNG path (config plugin injects the Podfile mods) and the bare-RN path (hand-edited Podfile, manual `pod install`).
Each app demonstrates the editor with a multi-language tab picker, syntax highlighting per language, and a Source/Preview toggle (Markdown / HTML / JS / TS) backed by an in-page WebView.

The two project layouts deliberately exercise different toolchains so the library is verified through both the Expo CNG path (config plugin injects the Podfile + Gradle mods) and the bare-RN path (hand-edited Podfile, manual `pod install`).

See each app's `README.md` for prerequisites and run instructions, or use the root-level scripts:

```sh
npm run ios:run # iOS example via Expo
npm run macos:dev # macOS example via bare react-native-macos
npm run ios:run # iOS via Expo CNG
npm run android:run # Android via Expo CNG (same project as iOS)
npm run macos:dev # macOS via bare react-native-macos
```
54 changes: 51 additions & 3 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ Once v1 ships, it will be published as `@workspace-sh/react-native-source-editor
| --- | --- |
| iOS | 16.0 |
| macOS | 14.0 |
| Android | API 24 (Android 7.0) |

These are STTextView's floors. Bump your app's targets accordingly.
iOS / macOS minimums are STTextView's floors; Android's is Sora-Editor's. Bump your app's targets accordingly.

## Swift Package Manager bridging
## Backing libraries

- **iOS / macOS**: [STTextView](https://github.com/krzyzanowskim/STTextView) (Swift / SPM-only).
- **Android**: [Sora-Editor](https://github.com/Rosemoe/sora-editor) (Kotlin, Maven Central).

## Swift Package Manager bridging (iOS / macOS)

STTextView is distributed only via SPM. The two supported toolchains use different bridges, picked by an env var the library podspec reads:

Expand Down Expand Up @@ -107,7 +113,49 @@ target 'YourApp' do
end
```

## Android (Expo CNG)

No SPM, no extra gem — Sora-Editor comes from Maven Central, autolinked via the standard React Native gradle plugin.

The same Expo config plugin entry that handles iOS also injects `coreLibraryDesugaring` into `android/app/build.gradle`. Sora's `language-textmate` AAR declares a desugaring requirement (its [Joni](https://github.com/jruby/joni) regex engine uses `java.time` on `minSdk < 26`); without it, your Android build fails at `checkDebugAarMetadata`.

```json
{
"plugins": [
"@workspace-sh/react-native-source-editor",
["expo-build-properties", {
"ios": { "deploymentTarget": "16.0", "useFrameworks": "static" },
"android": { "minSdkVersion": 24, "newArchEnabled": true }
}]
]
}
```

Build with `expo run:android`. Same as iOS, **don't hand-edit the generated `android/` directory** — Expo CNG owns it and your changes will be overwritten on the next prebuild.

### Bare React Native — Android

If you're not on Expo CNG, your `android/app/build.gradle` needs the same desugaring config that the plugin injects. Add to the `android { ... }` block:

```groovy
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
```

…and to the `dependencies { ... }` block:

```groovy
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
```

### License note for Android

Sora-Editor is **LGPL-2.1**. We consume it dynamically as a Gradle `implementation` AAR — no source modification, no static linking — which is the standard pattern that lets MIT-licensed downstream apps depend on this library without inheriting LGPL relinking obligations on their own code. **Don't fork the AAR or relink statically** unless you're prepared to take on those obligations yourself.

## Working examples

- iOS (Expo CNG): [`example/ios-app/`](../example/ios-app/) — runnable with `npm run ios:run` from the repo root.
- iOS + Android (Expo CNG): [`example/expo-app/`](../example/expo-app/) — runnable with `npm run ios:run` and `npm run android:run` from the repo root.
- macOS (bare RN-macos): [`example/macos-app/`](../example/macos-app/) — runnable with `npm run macos:dev` from the repo root.
6 changes: 3 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export default function Editor() {
| `editable` | `boolean` | `true` | Disables editing when `false`. |
| `font` | `{ family?: string; size?: number }` | system mono @ 14 | Falls back to monospaced system font if `family` is missing or unresolvable. |
| `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | `auto` follows system appearance. |
| `language` | `'plaintext' \| 'markdown' \| 'json' \| 'javascript' \| 'typescript' \| 'html'` | `'plaintext'` | Syntax highlighting via attributed text. `html` highlights nested CSS (in `<style>`) and JS (in `<script>`). Colours respect `theme` and adapt to system semantic colours. |
| `lineNumbers` | `boolean` | `false` | Toggles STTextView's line-number gutter. Runtime-settable — flipping the prop adds/removes the gutter view in place without remounting. Default off so the editor is gutter-less unless explicitly opted in. |
| `contentInsets` | `{ top?, bottom?, left?, right? }` (numbers) | `0` | Padding inside the text container. Use to keep first/last lines clear of floating UI like translucent headers/footers. |
| `language` | `'plaintext' \| 'markdown' \| 'json' \| 'javascript' \| 'typescript' \| 'html'` | `'plaintext'` | Syntax highlighting. iOS / macOS use STTextView's attributed-text highlighter; Android uses Sora-Editor's TextMate registry with VS Code grammars (1.94.0). On iOS / macOS, `html` highlights nested CSS (in `<style>`) and JS (in `<script>`); on Android the TextMate HTML grammar covers tags + attributes but does not recurse into nested CSS / JS. Colours respect `theme`. |
| `lineNumbers` | `boolean` | `false` | Toggles the line-number gutter. Runtime-settable on all platforms — flipping the prop adds/removes the gutter in place without remounting. |
| `contentInsets` | `{ top?, bottom?, left?, right? }` (numbers) | `0` | Padding inside the text container. Use to keep first/last lines clear of floating UI like translucent headers/footers. iOS / macOS use the platform's `UIEdgeInsets` semantics on the text container; Android maps to `View.setPadding` (dp → raw px via display-metrics). |
| `onChangeText` | `(text: string) => void` | — | Fires on every text change. |
| `onSelectionChange` | `(selection: { start: number; end: number }) => void` | — | Fires on selection updates. |
| `style` | `StyleProp<ViewStyle>` | — | Standard RN style. |
Expand Down
Loading