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
109 changes: 57 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,59 @@
# Interactive OS Editable

`@interactive-os/editable` is a JSON-backed contenteditable experiment focused
on one hard invariant: native IME input and application rendering must never
replace the same live DOM node at the same time.

The current implementation keeps `@interactive-os/json-document` as canonical
state and mounts one coordinator that owns the entire editable subtree. Pure
command planning, keyed DOM projection, and native-mutation grammar sit behind
small internal seams; browser event ordering and the live IME lease remain
together in the coordinator.
Ordinary input is converted to model commands before the browser mutates DOM.
During composition the module pins the browser-owned text node and lends that
block to the IME; proven-disjoint `remote` updates are queued until settling,
while local or same-block commands return `composition_conflict` and must be
retried afterward.

`getJsonEditableDocumentHost(editor)` exposes a document-host capability for
delayed or causal changes without widening the established `JsonEditable`
shape. It flushes pending native input before a ready change, defers while the
browser still owns a composition island, and identifies coordinator-owned
native, application, history, and ready publications with a pre-commit
sequence. The demo wires the SHA-pinned `json-document` causal inbox through
that capability; “지연 편집 추적” exercises both positional rebase after a
local insertion, selection correction, and settle-time retry during IME
composition. Ready changes always update canonical selection, but automatic
settle and causal publication restore DOM selection only while the editable
root already owns focus. A background change therefore cannot reclaim focus
from another control.

Enter is handled as a structural intent, independently from DOM ownership. A
semantic paragraph event is retained and replayed once composition settles;
candidate-confirming `keydown Enter` by itself is never treated as a paragraph.
Non-cancelable native splits are accepted only when they exactly match one
bounded, expected split and are then rebuilt from canonical JSON.

React renders the toolbar and diagnostics only. It provides an empty root to the
editor and never renders descendants inside that root.

The package enforces a direct-child layer rule:
This repository explores a durable editing boundary:

```txt
public facade -> browser -> core
- `@interactive-os/json-document` is the canonical, headless JSON state
protocol.
- `@interactive-os/editable` owns DOM and Input Events normalization.
- document semantics live in adapters, with Markdown supplied as a reference
candidate rather than root policy.

The stable package root deliberately exposes only `mountEditor`. Its adapter
contract is open to Docs-, Sheet-, Slides-, canvas-, block-, or source-oriented
models without putting Markdown text semantics into the host.

The engine owns only the host's `contenteditable` state and
`data-editable-owner` marker. Each product supplies its own ARIA role,
spellcheck, and focus behavior; editor-owned subtrees cannot overlap or nest.

```ts
import { mountEditor } from "@interactive-os/editable";
import {
createMarkdownAdapter,
createMarkdownDocument,
} from "@interactive-os/editable/markdown";

const document = createMarkdownDocument({
id: "note",
source: "# 제목 없는 노트",
});
const adapter = createMarkdownAdapter();
const editor = mountEditor({ root, document, adapter });
```

Cross-layer imports must use the child layer's `index.ts`; public-to-core
grandchild imports, browser-to-core implementation imports, and upward imports
are rejected by `pnpm run check:editable-layers`.
The editor snapshot atomically exposes the published JSON value, logical
selection, and `isComposing`. Document edits carry a namespaced transaction ID
and logical `selectionAfter`; external changes are mapped by the adapter.
Selection-only changes do not write the document. History is a separate
extension concern.

The previous implementation is preserved unchanged under
[`archive/pre-composition-island`](./archive/pre-composition-island).
Markdown keeps one `/source` string canonical. Rich and Live Preview modes are
DOM projections of that source. Composition uses a browser-owned island,
rebases disjoint external changes, and rejects overlap.

```txt
packages/editable/
index.ts stable DOM facade
browser/contract.ts stable protocol types
browser/genericEditor.ts generic DOM state machine
markdown/index.ts reference Markdown adapter

src/note-editor/ app-owned demo
vendor/json-document-v2/ pinned pre-release dependency artifact
```

The vendored JSON document tarball is pinned because v2 is not yet published;
its source commit and SHA-256 are recorded beside the artifact.

## Run

Expand All @@ -57,17 +62,17 @@ pnpm install
pnpm dev
```

Open `http://localhost:3000/`.

## Verify

```bash
pnpm run test:core
pnpm exec tsc --noEmit
pnpm run test:package
pnpm run build:package
pnpm run check:editable-layers
pnpm run build
pnpm run verify:browser
pnpm run verify:internal
```

Synthetic composition tests verify the coordinator protocol only. Actual IME
acceptance still requires the
[`manual IME acceptance`](./docs/manual-ime-acceptance.md) matrix.
Package verification also installs the packed artifact in a clean temporary
project. The root-only fixture proves that the Markdown parser is not installed;
the Markdown fixture adds its declared optional peer and imports the subpath.
4 changes: 4 additions & 0 deletions archive/pre-composition-island/ARCHIVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ island architecture. The archived scope is:
At the baseline, `pnpm exec tsc --noEmit` passed and the core Vitest suite passed
all **96 tests**.

On **2026-07-16**, superseded editor policy audits that describe this retired
architecture were moved under `docs`. Evidence manifests retain links to those
historical records without presenting them as current runtime documentation.

This is reference material only. It is excluded from the active build, typecheck,
test suites, and package surface. It is not maintained, fixed, or kept compatible
with the replacement implementation. Copy ideas deliberately; do not import or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ The current duplicate final composition input guard is sufficient: the late fina

## Issue #84 Live Markdown Shortcut History Contract

Live markdown shortcuts are not part of the current editor product surface. The editor should not add a markdown transform registry until a producer needs it.
Live Markdown shortcuts remain opt-in at the package mount seam. The product
installs `plugins: [bearMarkdown()]`, starts it enabled, and exposes its runtime
toggle in the toolbar. The first producer supports paragraph-start `# ` and
`> ` rules. Package-provided extensions receive accepted canonical input, not
raw DOM events or mutation records, and cannot write the content DOM directly.
The opaque install surface is not a public plugin-authoring SDK.

If live markdown shortcuts are added later, transform success must be exposed as an explicit history origin, for example `markdownShortcutTransform`, rather than being hidden inside the native text input commit.
Successful product transforms use the explicit `plugin:bear-markdown` history
origin instead of hiding inside the native text input commit.

History grouping policy:

Expand All @@ -43,10 +49,25 @@ History grouping policy:

This keeps accidental markdown transforms recoverable with one undo while preserving the typed input as a separate user action. Failed transforms do not create a transform history unit.

Required fixtures when implemented:
Implemented fixtures:

- Typing `# ` at paragraph start transforms to heading; undo once restores literal `# ` in a paragraph; undo twice removes `# `.
- Typing `- ` at paragraph start transforms to a bullet/list item with the same two-step undo behavior.
- Typing a bold/italic inline shortcut transforms marks with delimiter restoration on first undo.
- Typing `> ` at paragraph start transforms to a quote.
- Transform inside active composition is deferred until composition commit; no transform history unit is created during preedit.
- Pasting markdown-like text does not run live shortcut transforms unless the producer explicitly opts into paste transforms with a separate origin.
- A marker split across ordinary input and a settled composition remains one
literal undo unit in either order.
- Plugin triggers are queued in accepted-input order and retain stable block
ids plus offsets, not positional JSON paths. A later selection move or a
block insertion cannot retarget a deferred shortcut to another literal
marker; multiple deferred/current markers in one native turn all run.
- Pasting or programmatically dispatching markdown-like text does not run live shortcut transforms.
- A composition-time Enter still creates its separate paragraph history unit.

When a synchronous out-of-band publication makes input ownership ambiguous,
the shortcut does not run and document history is cleared. This is a deliberate
failure mode: keeping an unprovable nested history entry can make Undo resurrect
an old marker. An unambiguous disjoint external write remains separate from the
literal input and transform units.

List and inline delimiter rules remain future producers; they should reuse this
accepted-input and separate-transform history contract.
4 changes: 4 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
"ignoreUnknown": false,
"includes": [
"**/src/**/*",
"**/packages/editable/**/*.ts",
"**/packages/editable/**/*.mjs",
"**/packages/editable/package.json",
"**/scripts/**/*.mjs",
"**/tools/**/*.mjs",
"**/.vscode/**/*",
"**/index.html",
"**/vite.config.ts",
"!archive",
"!**/dist/**/*",
"!**/src/routeTree.gen.ts",
"!**/src/vendor/**/*"
]
Expand Down
Loading