text-document
A rich text document model for Rust, inspired by Qt's QTextDocument/QTextCursor API.
Built on Qleany-generated Clean Architecture with redb (embedded ACID database), full undo/redo, and multi-cursor support.
Features
- Rich text model: Frames, Blocks, InlineElements with
InlineContent::Text | Image - Multi-cursor editing: Qt-style cursors with automatic position adjustment
- Full undo/redo: Snapshot-based, with composite grouping (
begin_edit_block/end_edit_block) - Import/Export: Plain text, Markdown, HTML, LaTeX, DOCX
- Search: Find, find all, regex, replace (undoable)
- Formatting: Character format (
bold,italic,underline, ...), block format (alignment,heading_level, ...), frame format - Tables: Insert, remove, row/column operations, cell merge/split, table/cell formatting, cursor-position-based convenience methods
- Layout engine API: Read-only handles (
TextBlock,TextFrame,TextTable,TextTableCell,TextList), flow traversal, fragment-based text shaping, atomic snapshots, incremental change events - Event system: Callback-based (
on_change) and polling-based (poll_events), withFormatChangeKind(Block vs Character) and flow-level insert/remove events - Thread-safe:
Send + Syncthroughout,Arc<Mutex<...>>interior mutability - Resources: Image and stylesheet storage with base64 encoding
Quick start
use ;
let doc = new;
doc.set_plain_text.unwrap;
// Cursor-based editing
let cursor = doc.cursor;
cursor.move_position;
cursor.insert_text.unwrap; // replaces "Hello"
// Multiple cursors
let c1 = doc.cursor;
let c2 = doc.cursor_at;
c1.insert_text.unwrap;
// c2's position is automatically adjusted
// Undo
doc.undo.unwrap;
// Search
use FindOptions;
let matches = doc.find_all.unwrap;
// Export
let html = doc.to_html.unwrap;
let markdown = doc.to_markdown.unwrap;
Layout engine API
Read-only handles for building a layout/rendering engine on top of the document model:
use ;
let doc = new;
doc.set_plain_text.unwrap;
// Walk the document's visual flow
for element in doc.flow
// Atomic snapshot for full layout
let snap = doc.snapshot_flow;
// Direct block access
let block = doc.block_at_position.unwrap;
let next = block.next; // O(n) traversal
let snap = block.snapshot; // all data in one lock
Table operations
use TextDocument;
let doc = new;
doc.set_plain_text.unwrap;
let cursor = doc.cursor_at;
// Insert a 3x2 table, get a handle back
let table = cursor.insert_table.unwrap;
assert_eq!;
// Explicit-ID mutations
cursor.insert_table_row.unwrap; // insert row at index 1
cursor.remove_table_column.unwrap; // remove first column
// Position-based convenience (cursor must be inside a table cell)
// cursor.insert_row_above().unwrap();
// cursor.remove_current_column().unwrap();
// cursor.set_current_table_format(&format).unwrap();
CLI
A command-line tool for format conversion and text processing:
# Convert between formats (detected by file extension)
# Show document statistics
# Find text (grep-like output)
# Find and replace
# Print to stdout in a different format
Supported formats:
| Extension | Import | Export |
|---|---|---|
.txt |
yes | yes |
.md |
yes | yes |
.html/.htm |
yes | yes |
.tex/.latex |
- | yes |
.docx |
- | yes |
Document structure
Root
+-- Document
+-- Frame (root frame)
| +-- Block
| | +-- InlineElement (Text "Hello ")
| | +-- InlineElement (Text "world" with bold)
| | +-- InlineElement (Image { name, width, height })
| +-- Block
| +-- InlineElement (Text "Second paragraph")
+-- Table (rows: 2, columns: 3)
| +-- TableCell (row: 0, col: 0)
| | +-- Frame (cell frame)
| | +-- Block
| | +-- InlineElement (Text "Cell content")
| +-- TableCell (row: 0, col: 1) ...
+-- List (style: Decimal, indent: 1)
+-- Resource (image data, stylesheets)
- Frame: contains Blocks and child Frames.
child_orderinterleaves them (positive = block ID, negative = sub-frame ID). - Block: a paragraph. Contains InlineElements. Has
document_positionfor O(log n) lookup. - InlineElement: either
Text(String),Image { name, width, height, quality }, orEmpty. - List: styling for list items (Disc, Decimal, LowerAlpha, ...). Blocks reference lists via weak relationship.
- Table: grid of TableCells, each with an optional cell frame containing Blocks.
- Resource: binary data (images, stylesheets) stored as base64.
All format fields are Option<T> — None means "inherit from parent/default", Some(value) means "explicitly set".
Public API handles
| Handle | Obtained from | Purpose |
|---|---|---|
TextDocument |
TextDocument::new() |
Document-level operations, flow traversal |
TextCursor |
doc.cursor() / doc.cursor_at(pos) |
All mutations (text, formatting, tables, lists) |
TextBlock |
doc.flow(), doc.block_at_position(), etc. |
Read-only block data, fragments, list membership |
TextFrame |
block.frame(), FlowElement::Frame |
Read-only frame data, nested flow |
TextTable |
cursor.insert_table(), FlowElement::Table |
Read-only table structure, cell access, snapshot |
TextTableCell |
table.cell(row, col) |
Read-only cell data, blocks within cell |
TextList |
block.list() |
Read-only list properties, item markers |
All handles are Clone + Send + Sync (backed by Arc<Mutex<...>> + entity ID).
Architecture
Generated by Qleany v1.5.1, following Clean Architecture with Package by Feature (Vertical Slice):
crates/
+-- public_api/ # TextDocument, TextCursor, DocumentEvent (the public crate)
+-- cli/ # Command-line tool
+-- frontend/ # AppContext, commands, event hub client
+-- common/ # Entities, database (redb), events, undo/redo, repositories
+-- macros/ # #[uow_action] proc macro
+-- direct_access/ # Entity CRUD controllers + DTOs
+-- document_editing/ # 19 use cases (insert, delete, block, image, frame, list, fragment, table CRUD, merge/split cells, ...)
+-- document_formatting/ # 6 use cases (set/merge text format, block format, frame format, table format, cell format)
+-- document_io/ # 8 use cases (import/export plain text, markdown, HTML, LaTeX, DOCX)
+-- document_search/ # 3 use cases (find, find_all, replace)
+-- document_inspection/ # 4 use cases (stats, text at position, block at position, extract fragment)
+-- test_harness/ # Shared test setup utilities
Data flow: TextDocument / TextCursor -> frontend::commands -> controllers -> use cases -> UoW -> repositories -> redb
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
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.