Native Rust WYSIWYG rich-text editor framework for Leptos — pure Rust at runtime, no JavaScript bridge.
taino-edit is a ProseMirror/TipTap-inspired editor — typed document model,
invertible transforms, history, commands and a Leptos component — built
reactive-first for Rust web frameworks. Unlike leptos-tiptap (a
wasm-bindgen wrapper around the TypeScript TipTap bundle), there is no
JS dependency at runtime.
It is part of the taino-* family, following taino-dnd-*.
Seven crates on crates.io. v0.6 brings Leptos SSR: <TainoEditor>
server-renders the initial document as real HTML (visible and indexable
before any wasm loads) and hydrates it into the live editor with no visual
change — backed by a browser-pinned markup contract (doc_view_html ↔
EditorView::mount). The adapter is now render-mode neutral (no forced
csr; your app picks csr/hydrate/ssr), and the new
ssr-leptos example ships the full axum +
cargo leptos setup. Before that, the v0.5 line added a schema! macro,
inline (range-level) decorations drawn as a scroll-aware overlay,
editor-owned keyboard input in both adapters (synchronous,
live-selection — no stale-caret bugs), and a string of editing fixes
surfaced by dog-fooding (caret motion across block boundaries, ordered-list
numbering, multi-block commands, selection-mirror race). Tests pass
workspace-wide:
| Host tests | 214 (model, schema, content automaton, replace, steps, transforms, state, history, commands, keymap, input-rules, plugin registry, Markdown serializer + parser, the Selection::Cell variant, and 13 extensions including the full table command set) |
| Browser tests | wasm-bindgen cases in headless Chromium 148 — mount, diff/patch, selection sync, DOM-typing → Transform, IME, clipboard, drag/drop, focus, node + inline decorations, Leptos + Dioxus component/event/keymap wiring, table rendering, the ViewPlugin infra, and TableView pointer interaction (cell drag-select, highlight, resize) |
See DESIGN_NOTES.md for the architecture, the scope budget, and the resolved design decisions; ROADMAP.md tracks phase progress and what's deferred.
schema! { .. }macro — declarative sugar overSchemaBuilder(plainmacro_rules!, no proc-macro crate).- Inline (range-level) decorations —
Decoration::Inlinehighlights arbitrary inline ranges (search hits, comments, remote selections) as a scroll-aware overlay that never splits the editable text nodes. - The editor owns keyboard input in both adapters:
keydownreads the live DOM selection, runs the keymap command and applies the result synchronously, eliminating the "stale model selection" class of bugs. - Coordinate-free table primitives —
select_caret_row()/select_caret_column()make "merge this row/column" one-liners that stay correct as the table grows. Codeinline mark (Mod-e) with Markdown backtick round-trip (shipped in v0.3.1).- Dioxus
ViewPluginparity — interactive tables (drag-select, highlight, resize) in Dioxus too;basic-dioxusruns the same 13-extension toolbar asbasic-leptos. - Editing fixes: caret motion across block boundaries, ordered-list numbering after lifting a middle item, block commands applying to every selected block, empty-textblock focusability, and the selection-mirror race that clipped multi-word drag-selections.
- Full tables — a
Tableextension (table/table_row/table_cellwith colspan/rowspan/header/colwidth) whose every command is span-correct: insert, add/delete rows & columns, header toggle, Tab cell-navigation, cell-range selection, merge/split, and column resize. A logical-grid placement model + compaction render guarantee no orphan spans or empty rows under any sequence of edits. ViewPluginplatform (taino-edit-dom) — DOM-aware event + decoration hooks so an extension can add real pointer interaction without coupling the generic adapter to it. New cratetaino-edit-table-viewimplements table cell drag-select, selection highlight and column-resize on top of it; the Leptos<TainoEditor>takes an optionalpluginsprop.- Dioxus adapter parity — input → transform, IME, paste and
selectionchangeall wired, matching the Leptos adapter.
- Complete list UX (smart Enter / sink / lift), the
Plugintrait + typed-state registry, Markdown round-trip, and the first real Dioxus adapter.
- A typed, immutable document tree (ProseMirror-style
Node/Mark/Fragment/Slice). - A
Schema+SchemaBuilderwith a Thompson-NFA-to-DFA content automaton (paragraph+,(text | image)*,+ * ?). - Schema-checked JSON round-trip (
Node::to_json↔Schema::node_from_json). - A dependency-free escaped HTML serializer and a strict, depth-bounded HTML parser (rejects unknown tags, can't be tricked into injecting markup).
- Invertible, mappable Steps (
ReplaceStep,ReplaceAroundStep,AddMark/RemoveMark/AttrStep),Mappingwith mirror/recover, and aTransformbuilder. - An
EditorStatewithSelection,Transaction, and a bounded groupable undo/redoHistory. - A standard command vocabulary (
select_all,toggle_mark,set_block_type,wrap_in,lift,split_block,join_…, …), a cross-platformKeymap(Mod= Ctrl/Cmd) and abase_keymap. - Regex input rules (
##→ heading,>→ blockquote, …). - A real
contenteditableDOM bridge (taino-edit-dom): mount, incremental diff/patch, bidirectional selection sync, IME composition, clipboard paste sanitized through the schema, drag-and-drop primitives, focus management and node decorations. - A first-class Leptos adapter:
<TainoEditor state=signal />mounts the editor, wires every event (includingselectionchange) back through the state signal, and is tested inside the real Leptos CSR runtime. - Twelve built-in extensions, enough to drop into a real project:
- Inline marks:
Bold(Mod-b),Italic(Mod-i),Link(set_link/remove_linkcommands; the host wires the URL prompt). - Block nodes:
Paragraph(Mod-Alt-0),HeadingH1–H3 (Mod-Alt-1..3),Blockquote(Mod->),CodeBlock(Mod-`), and theListstrio (BulletList/OrderedListListItem,Mod-Shift-8/Mod-Shift-7+Shift-Tabto lift).
- Inline atoms:
Image(insert_imagecommand). - Attribute / selection commands:
Align(align_left/center/right/justify,Mod-Shift-{l,e,r,j}),TransformCase(to_uppercase/to_lowercase). - Undo/redo:
History(Mod-z/Mod-Shift-z).
- Inline marks:
Explicitly deferred to v0.2: generic plugin registry, inline-range
decorations, a richer per-node NodeView trait, the Dioxus adapter,
loro CRDT integration behind a collab feature, Markdown
serializer/parser, smart Enter / nested-list sink (indent) for lists,
and richer extensions (tables, footnotes, mentions, math).
| Crate | Role |
|---|---|
taino-edit-core |
Framework-agnostic model, transforms, state, history, commands, keymap, input rules, Markdown, Plugin trait |
taino-edit-dom |
contenteditable/DOM bridge + ViewPlugin (web-sys, wasm-bindgen, js-sys) |
taino-edit-extensions |
The 13 built-in extensions (marks, blocks, lists, tables, …) + the Extension trait |
taino-edit-leptos |
Leptos adapter (<TainoEditor>) |
taino-edit-dioxus |
Dioxus adapter (<TainoEditor>) |
taino-edit-table-view |
Table pointer interaction (cell drag-select, resize) as a ViewPlugin |
taino-edit |
Umbrella crate, feature-gated re-exports |
Examples under examples/:
basic-leptos— atrunk serve-buildable demo with the full toolbar, tables (drag-select / merge / resize) and live JSON + HTML panels.basic-dioxus— the same editor in Dioxus.ssr-leptos— Leptos SSR + hydration: the initial document is server-rendered HTML (curl it!), then hydrated into a live editor. Run withcargo leptos watch. (Its own workspace — Leptos'scsrandssr/hydratemodes can't share one feature graph.)headless-core— server-side / CLI demo provingtaino-edit-coreruns identically without a DOM.
[dependencies]
taino-edit = { version = "0.6", features = ["leptos"] } # or "dioxus"No adapter is enabled by default — pick leptos or dioxus. Add the
table-view feature for table pointer interaction.
The Leptos adapter is render-mode neutral: it does not enable any of
Leptos's csr/hydrate/ssr features (a library never should — the modes
are mutually exclusive and cargo unifies features). Your app enables exactly
one, as it already does for every other Leptos component library.
use leptos::prelude::*;
use taino_edit_leptos::{
build_keymap_with, build_schema_with, Bold, DomSpec, EditorState,
Italic, NodeSpec, SchemaBuilder, TainoEditor,
};
#[component]
fn App() -> impl IntoView {
// Compose a schema on top of the universal doc/text primitives.
let base = SchemaBuilder::new()
.node("doc", NodeSpec { content: Some("block+".into()), ..Default::default() })
.node("text", NodeSpec { group: Some("inline".into()), ..Default::default() });
// Paragraph etc. come from `taino-edit-extensions`.
let exts: Vec<&dyn taino_edit_extensions::Extension> =
vec![&taino_edit_extensions::Paragraph, &Bold, &Italic];
let schema = build_schema_with(base, &exts, "doc").unwrap();
let txt = schema.text("Hello from Rust!", vec![]).unwrap();
let para = schema.node("paragraph", Default::default(), vec![txt], vec![]).unwrap();
let doc = schema.node("doc", Default::default(), vec![para], vec![]).unwrap();
let state = RwSignal::new(EditorState::new(doc, schema));
view! { <TainoEditor state=state /> }
}The same component SSRs the initial document as real HTML — no
JavaScript required for first paint, and the content is indexable. Under
ssr, <TainoEditor> serializes the initial doc with
doc_view_html, which emits — by
tested contract — exactly the markup the mounted editor builds, so
hydration swaps the live editor in with no visual change. The pre-hydration
document is deliberately not editable (edits typed before the wasm boots
would be lost); it becomes editable the moment the editor mounts.
See examples/ssr-leptos for a full axum +
cargo leptos setup.
Requires the Rust toolchain pinned in rust-toolchain.toml
(stable, MSRV 1.80).
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo doc --no-deps --all-featuresBrowser tests for taino-edit-dom and taino-edit-leptos use a small
locally-patched wasm-bindgen-cli; first time only run
./scripts/install-wasm-test-runner.sh, after that
./scripts/wasm-test.sh runs them in headless Chromium 148. See
vendor/README.md for the rationale.
See CONTRIBUTING.md. The roadmap marks community contribution surfaces (the Dioxus adapter, richer extensions, native renderers) explicitly.
Dual-licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.