Skip to content
Merged
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
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ Unless a row uses an explicit module-root path such as `sample/src/...`, paths a

**EditorState** — immutable snapshot: `blocks`, `focusedBlockId`, `selectedBlockIds`, `dragState`, `slashCommandState`. Cursor position is NOT in EditorState — it lives in `TextFieldState` managed by `BlockTextStates`. **Invariant:** `focusedBlockId` and `selectedBlockIds` are mutually exclusive — enforced by reducers, not UI code. Selection reducers clear focus; focus reducers (with non-null target) clear selection. `ClearFocus` and `ClearSelection` are orthogonal and do not enforce this on each other.

**Editor configuration and interaction policy** — `CascadeEditorConfig` is a public behavior config passed as the final defaulted `CascadeEditor` parameter and exposed to custom renderer/chrome content through `LocalCascadeEditorConfig`. It does not replace direct ownership, styling, registry, slot, state, or callback parameters. Built-in code converts it once with `CascadeEditorConfig.toInteractionPolicy()` inside `CascadeEditor` and provides the resulting internal `EditorInteractionPolicy` through `LocalEditorInteractionPolicy`; built-in policy checks should use named capabilities from that policy instead of reading `config.readOnly` directly. Formatting, indentation, and link calculators/actions receive the policy explicitly from `CascadeEditor` so custom chrome locals expose disabled state and gated action facades from the same policy source. Their action objects also read policy through invocation-time providers, so stale formatting, indentation, link, link-popup, and toolbar-slash callbacks captured before a runtime policy change cannot keep mutating after the current policy disables them. Text-block input reads the internal policy in `TextBlockField`, forwards `readOnly = !canEditText` into the shared state-based `BackspaceAwareTextField`, and gates side-effectful Enter, sentinel-backspace, paste-history, formatting-shortcut, editor-history-shortcut, and list auto-detect paths before they reach mutation callbacks or text-history batching. The slash subsystem is gated once in `CascadeEditor` from `SlashCommandSlot` plus `canUseSlashCommands`; observer construction, popup rendering, key handling, and executor wiring use that gate, while the default toolbar slash button also requires `canEditText` before it can write to the focused text field. Block-level gesture wiring receives the policy explicitly in `blockGestures(...)`; read-only mode omits the editor-owned pointer detector, while editable configs can independently disable built-in block selection or block dragging through `blockSelectionEnabled` and `blockDraggingEnabled`. Gesture dispatch, drag/selection/drop-target actions, empty-space edit focus, and drag auto-scroll/drop/preview affordances all follow the derived capabilities. `CascadeEditor` runs one transition cleanup when the active policy disables editor-owned mutation workflows: it closes slash sessions, cancels drags, clears block selection, and dismisses editor-owned link popup sessions without changing document blocks or text focus. `TodoBlockRenderer` reads `LocalEditorInteractionPolicy` and resolves policy/callbacks at checkbox invocation time so stale checkbox lambdas cannot toggle todo state after the policy changes. The read-only policy disables every current interaction capability, while reducers and app-owned mutation surfaces remain outside this UI policy boundary.
**Editor configuration and interaction policy** — `CascadeEditorConfig` is a public behavior config passed as the final defaulted `CascadeEditor` parameter and exposed to custom renderer/chrome content through `LocalCascadeEditorConfig`. It does not replace direct ownership, styling, registry, slot, state, or callback parameters. Built-in code converts it once with `CascadeEditorConfig.toInteractionPolicy()` inside `CascadeEditor` and provides the resulting internal `EditorInteractionPolicy` through `LocalEditorInteractionPolicy`; built-in policy checks should use named capabilities from that policy instead of reading config flags directly. Formatting, indentation, and link calculators/actions receive the policy explicitly from `CascadeEditor` so custom chrome locals expose disabled state and gated action facades from the same policy source. Their action objects also read policy through invocation-time providers, so stale formatting, indentation, link, link-popup, and toolbar-slash callbacks captured before a runtime policy change cannot keep mutating after the current policy disables them. Text-block input reads the internal policy in `TextBlockField`, forwards `readOnly = !canEditText` into the shared state-based `BackspaceAwareTextField`, and gates side-effectful Enter, sentinel-backspace, paste-history, formatting-shortcut, editor-history-shortcut, and list auto-detect paths before they reach mutation callbacks or text-history batching. The slash subsystem is gated once in `CascadeEditor` from `SlashCommandSlot` plus `canUseSlashCommands`; observer construction, popup rendering, key handling, and executor wiring use that gate, while the default toolbar slash button also requires `canEditText` before it can write to the focused text field. Block-level gesture wiring receives the policy explicitly in `blockGestures(...)`; read-only mode omits the editor-owned pointer detector, while editable configs can independently disable built-in block selection, block dragging, or indentation changes through `blockSelectionEnabled`, `blockDraggingEnabled`, and `blockIndentationEnabled`. Disabling indentation keeps drag/reorder available, forces drag hover to preserve the payload's original depth, rejects destinations that would require a depth rewrite, and disables indentation command state/actions. Gesture dispatch, drag/selection/drop-target actions, empty-space edit focus, and drag auto-scroll/drop/preview affordances all follow the derived capabilities. `CascadeEditor` runs one transition cleanup when the active policy disables editor-owned mutation workflows: it closes slash sessions, cancels active drags when dragging or indentation changes become unavailable, clears block selection, and dismisses editor-owned link popup sessions without changing document blocks or text focus. `TodoBlockRenderer` reads `LocalEditorInteractionPolicy` and resolves policy/callbacks at checkbox invocation time so stale checkbox lambdas cannot toggle todo state after the policy changes. The read-only policy disables every current interaction capability, while reducers and app-owned mutation surfaces remain outside this UI policy boundary.

**Read-only boundary** — read-only mode is documented in [`docs/ReadOnlyMode.md`](docs/ReadOnlyMode.md). It is intentionally a `CascadeEditor` UI boundary, not reducer-level authorization. App-owned calls such as `EditorStateHolder.dispatch(...)`, `setState(...)`, `undo()`, `redo()`, `loadFromJson(...)`, `loadFromHtml(...)`, autosave, remote sync, and direct `BlockTextStates` / `BlockSpanStates` / `TextFieldState` writes remain mutable unless the application gates them. Custom renderer and chrome code should read `LocalCascadeEditorConfig.current.readOnly`; built-in code should continue to use the internal policy.

Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Changelog

## [1.8.0] - 2026-07-23

### Added

- Added an experimental, profile-driven Markdown codec with `MarkdownSchema` and `EditorStateHolder` import/export helpers, CommonMark/GFM-oriented block and inline support, policy-driven HTML bridging, structured warnings, bounded resource limits, source preservation for unsupported syntax, and fidelity analysis that recommends native editing or a raw-text fallback
- Added the Swift-facing `:editor-ios-sdk` static framework with a controller and UIKit host, JSON/HTML document APIs, toolbar state and actions, localization, change callbacks, native custom blocks, and native slash commands; also added an XCFramework build script and a native Swift sample
- Added `CascadeEditorConfig(blockIndentationEnabled = ...)` to disable built-in indentation commands while keeping drag-and-drop at the payload's original indentation
- Added public integration seams for external editor chrome and platform bridges: `EditorStateHolder.dispatchStructuralAction(...)`, `EditorStateHolder.resolveDocumentBlocks(...)`, `BlockRenderer.supportsDragPreview`, and stable built-in slash-command ID helpers
- Added comments-composer and Markdown round-trip examples to the sample app

### Changed

- Lowered the Android minimum SDK from 28 to 26
- Updated Compose Multiplatform from 1.11.0 to 1.11.1

### Fixed

- Fixed block renderers and slash commands registered after the editor is mounted not becoming available until an unrelated state change
- Fixed built-in slash conversion to non-text custom blocks so blank anchors are replaced safely and nonblank anchor text is never discarded
- Fixed JSON, HTML, and Markdown link persistence rewriting relative, fragment, `mailto:`, `tel:`, and custom-scheme targets as HTTPS URLs
- Fixed drag previews for platform-view renderers by allowing them to opt out of duplicate live composition and use a lightweight placeholder

## [1.7.0] - 2026-06-28

### Added
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Rich text input | HTML/JSON round-trip | Read-only rendering | Custom toolbars |
[![Kotlin](https://img.shields.io/badge/Kotlin-2.3-7F52FF?logo=kotlin)](https://kotlinlang.org/docs/multiplatform.html)
[![Compose](https://img.shields.io/badge/Compose_Multiplatform-1.11-4285F4?logo=jetpackcompose)](https://www.jetbrains.com/compose-multiplatform/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Android](https://img.shields.io/badge/Android-minSdk_28-3DDC84?logo=android)](https://developer.android.com/)
[![Android](https://img.shields.io/badge/Android-minSdk_26-3DDC84?logo=android)](https://developer.android.com/)
[![iOS](https://img.shields.io/badge/iOS-16+-000000?logo=apple)](https://developer.apple.com/)
[![Android Weekly](https://androidweekly.net/issues/issue-721/badge)](https://androidweekly.net/issues/issue-721)

Expand Down Expand Up @@ -105,7 +105,7 @@ Some of the harder problems handled by the editor core:
Start with the smallest useful setup: one paragraph block, a limited toolbar, and slash commands disabled.

```groovy
implementation("io.github.linreal:cascade-editor:1.7.0")
implementation("io.github.linreal:cascade-editor:1.8.0")
```

```kotlin
Expand Down Expand Up @@ -573,7 +573,7 @@ Most of this logic lives in `editor/src/commonMain`, with platform-specific code
|---|---|
| Kotlin | 2.3.21 |
| Compose Multiplatform | 1.11.0 |
| Android minSdk | 28 |
| Android minSdk | 26 |
| Android compileSdk | 36 |
| iOS min version | 16.0 |
| iOS targets | arm64, simulatorArm64 |
Expand Down
2 changes: 1 addition & 1 deletion docs/iOsNativeSdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ The built-in slash executor also distinguishes text and non-text conversion targ
### SDK identity and configuration

- `CascadeEditorSdk.version: String` — currently `"1.0.0"`.
- `CascadeEditorConfiguration` — immutable configuration containing `readOnly`, `toolbarMode`, `slashCommandsEnabled`, `blockSelectionEnabled`, `blockDraggingEnabled`, `isDark`, and `crashPolicy`.
- `CascadeEditorConfiguration` — immutable configuration containing `readOnly`, `toolbarMode`, `slashCommandsEnabled`, `blockSelectionEnabled`, `blockDraggingEnabled`, `blockIndentationEnabled`, `isDark`, and `crashPolicy`. Setting `blockIndentationEnabled` to `false` disables indentation commands and keeps block drag/reorder at the payload's original indentation depth.
- `CascadeToolbarMode` — `builtIn` or `none`; `none` disables both the built-in formatting toolbar and its link popup.
- `CascadeCrashPolicy` — `containAndReport` or `rethrow`.

Expand Down
6 changes: 5 additions & 1 deletion editor-ios-sdk/api/editor-ios-sdk.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,12 @@ final class io.github.linreal.cascade.ios.controller/CascadeDocumentLoadResult {
final class io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration { // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration|null[0]
constructor <init>() // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.<init>|<init>(){}[0]
constructor <init>(kotlin/Boolean, io.github.linreal.cascade.ios.controller/CascadeToolbarMode, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, io.github.linreal.cascade.ios.controller/CascadeCrashPolicy) // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.<init>|<init>(kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeToolbarMode;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeCrashPolicy){}[0]
constructor <init>(kotlin/Boolean, io.github.linreal.cascade.ios.controller/CascadeToolbarMode, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, io.github.linreal.cascade.ios.controller/CascadeCrashPolicy, kotlin/Boolean) // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.<init>|<init>(kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeToolbarMode;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeCrashPolicy;kotlin.Boolean){}[0]

final val blockDraggingEnabled // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.blockDraggingEnabled|{}blockDraggingEnabled[0]
final fun <get-blockDraggingEnabled>(): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.blockDraggingEnabled.<get-blockDraggingEnabled>|<get-blockDraggingEnabled>(){}[0]
final val blockIndentationEnabled // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.blockIndentationEnabled|{}blockIndentationEnabled[0]
final fun <get-blockIndentationEnabled>(): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.blockIndentationEnabled.<get-blockIndentationEnabled>|<get-blockIndentationEnabled>(){}[0]
final val blockSelectionEnabled // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.blockSelectionEnabled|{}blockSelectionEnabled[0]
final fun <get-blockSelectionEnabled>(): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.blockSelectionEnabled.<get-blockSelectionEnabled>|<get-blockSelectionEnabled>(){}[0]
final val crashPolicy // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.crashPolicy|{}crashPolicy[0]
Expand All @@ -175,7 +178,8 @@ final class io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration
final fun component5(): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.component5|component5(){}[0]
final fun component6(): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.component6|component6(){}[0]
final fun component7(): io.github.linreal.cascade.ios.controller/CascadeCrashPolicy // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.component7|component7(){}[0]
final fun copy(kotlin/Boolean = ..., io.github.linreal.cascade.ios.controller/CascadeToolbarMode = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., io.github.linreal.cascade.ios.controller/CascadeCrashPolicy = ...): io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.copy|copy(kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeToolbarMode;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeCrashPolicy){}[0]
final fun component8(): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.component8|component8(){}[0]
final fun copy(kotlin/Boolean = ..., io.github.linreal.cascade.ios.controller/CascadeToolbarMode = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., io.github.linreal.cascade.ios.controller/CascadeCrashPolicy = ..., kotlin/Boolean = ...): io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.copy|copy(kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeToolbarMode;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeCrashPolicy;kotlin.Boolean){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.toString|toString(){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import kotlin.native.ObjCName
@OptIn(ExperimentalObjCName::class)
@ObjCName("CascadeEditorSdk", exact = true)
public object CascadeEditorSdk {
public const val version: String = "1.7.0"
public const val version: String = "1.8.0"
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,27 @@ public data class CascadeEditorConfiguration(
public val blockDraggingEnabled: Boolean,
public val isDark: Boolean,
public val crashPolicy: CascadeCrashPolicy,
public val blockIndentationEnabled: Boolean,
) {
public constructor(
readOnly: Boolean,
toolbarMode: CascadeToolbarMode,
slashCommandsEnabled: Boolean,
blockSelectionEnabled: Boolean,
blockDraggingEnabled: Boolean,
isDark: Boolean,
crashPolicy: CascadeCrashPolicy,
) : this(
readOnly = readOnly,
toolbarMode = toolbarMode,
slashCommandsEnabled = slashCommandsEnabled,
blockSelectionEnabled = blockSelectionEnabled,
blockDraggingEnabled = blockDraggingEnabled,
isDark = isDark,
crashPolicy = crashPolicy,
blockIndentationEnabled = true,
)

public constructor() : this(
readOnly = false,
toolbarMode = CascadeToolbarMode.builtIn,
Expand All @@ -39,6 +59,7 @@ public data class CascadeEditorConfiguration(
blockDraggingEnabled = true,
isDark = false,
crashPolicy = CascadeCrashPolicy.containAndReport,
blockIndentationEnabled = true,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public fun CascadeEditorController.makeViewController(): UIViewController = onMa
readOnly = configurationState.readOnly,
blockSelectionEnabled = configurationState.blockSelectionEnabled,
blockDraggingEnabled = configurationState.blockDraggingEnabled,
blockIndentationEnabled = configurationState.blockIndentationEnabled,
crashPolicy = configurationState.crashPolicy.toCoreCrashPolicy(),
onInternalError = { error ->
reportInternalError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class CascadeEditorControllerTest {

assertEquals(CascadeToolbarMode.builtIn, configuration.toolbarMode)
assertEquals(CascadeCrashPolicy.containAndReport, configuration.crashPolicy)
assertTrue(configuration.blockIndentationEnabled)
assertEquals(configuration, emptyController.configuration)
assertEquals("Seed", jsonController.exportPlainText())
}
Expand Down
Loading
Loading