pub struct Editor { /* private fields */ }Expand description
A span-splice editor over a document: applies lossless, in-place edits and
reparses after each one, so node addressing stays valid as the document
evolves. Every op is addressed by a locator — a dot-separated index path
("0.3.1") or a selector that must match exactly one node
(heading("Status")). A failed edit leaves the document unchanged.
Implementations§
Source§impl Editor
impl Editor
Sourcepub fn new(input: &[u8], format: Format) -> Result<Self, Error>
pub fn new(input: &[u8], format: Format) -> Result<Self, Error>
Create an editor over a private copy of input, parsed as format with
default options.
pub fn new_str(input: &str, format: Format) -> Result<Self, Error>
Sourcepub fn new_ext(
input: &[u8],
format: Format,
extensions: MarkdownExtensions,
) -> Result<Self, Error>
pub fn new_ext( input: &[u8], format: Format, extensions: MarkdownExtensions, ) -> Result<Self, Error>
Like Editor::new, plus Markdown extensions to enable (ignored for
other formats). The editor reparses with these after every edit, so a
directive-bearing document stays parseable — needed before
Editor::filter can match directive[...] selectors.
Sourcepub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error>
pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error>
Replace the whole source of the located node with text.
Sourcepub fn replace_content(
&mut self,
locator: &str,
text: &str,
) -> Result<(), Error>
pub fn replace_content( &mut self, locator: &str, text: &str, ) -> Result<(), Error>
Replace the interior (between-delimiters content) of the located container.
Sourcepub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error>
pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error>
Insert text immediately before the located node.
Sourcepub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error>
pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error>
Insert text immediately after the located node.
Sourcepub fn insert_child(
&mut self,
locator: &str,
index: usize,
text: &str,
) -> Result<(), Error>
pub fn insert_child( &mut self, locator: &str, index: usize, text: &str, ) -> Result<(), Error>
Insert text as the index-th child of the located container (an index
at or past the child count appends).
Sourcepub fn delete(&mut self, locator: &str) -> Result<(), Error>
pub fn delete(&mut self, locator: &str) -> Result<(), Error>
Delete the located node (removes exactly its span; no whitespace cleanup).
Sourcepub fn delete_smart(&mut self, locator: &str) -> Result<(), Error>
pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error>
Delete the located node, tidying surrounding blank lines for a whole-line (block) node; an inline node degrades to the exact delete.
Sourcepub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error>
pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error>
Unwrap the located node: replace it with its interior (drop the wrapper,
keep the children) — e.g. peel a :::vis{...} container. A node with no
interior (a leaf, or an empty container) is removed.
Sourcepub fn filter(
&mut self,
drop: &str,
keep: Option<&str>,
unwrap_kept: bool,
) -> Result<(), Error>
pub fn filter( &mut self, drop: &str, keep: Option<&str>, unwrap_kept: bool, ) -> Result<(), Error>
Prune the document in place: remove every node matching the drop
selector except those also matching keep (None spares nothing),
then — if unwrap_kept — unwrap the survivors. Read the result with
Editor::source.
Sourcepub fn source_str(&mut self) -> Result<String, Error>
pub fn source_str(&mut self) -> Result<String, Error>
The editor’s current source bytes as a UTF-8 string.
Sourcepub fn ast_json(&mut self) -> Result<Vec<u8>, Error>
pub fn ast_json(&mut self) -> Result<Vec<u8>, Error>
Encode the editor’s current tree as pretty-printed JSON — the live
counterpart of Document::ast_json, for inspecting between edits.
Sourcepub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error>
pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error>
Resolve a selector against the editor’s current tree — the live
counterpart of Document::query.
Sourcepub fn edit_range(
&mut self,
start: usize,
end: usize,
text: &str,
) -> Result<Change, Error>
pub fn edit_range( &mut self, start: usize, end: usize, text: &str, ) -> Result<Change, Error>
Splice [start, end) of the current source with text, reparse, and
return the Change the edit produced — the offset-addressed primitive
a caret editor is built on: a keystroke is edit_range(c, c, "x"),
backspace edit_range(c - 1, c, ""), a selection replace
edit_range(a, b, s). start <= end <= source length, else
Error::InvalidArgument. A reparse-breaking edit is rolled back and
returns Error::EditConflict, leaving the document untouched.
Sourcepub fn last_change(&mut self) -> Option<Change>
pub fn last_change(&mut self) -> Option<Change>
The byte effect of the last successful edit — including the locator ops
(Editor::replace, Editor::delete_smart, …), so any edit can
re-anchor a caret without re-diffing. None before the first successful
edit. (A multi-splice op such as Editor::filter reports only its
final splice.)
Sourcepub fn undo(&mut self) -> Result<Option<Change>, Error>
pub fn undo(&mut self) -> Result<Option<Change>, Error>
Undo the last edit step, restoring the previous source and reparsing.
Returns the Change the undo produced (current → restored) so a caret
can re-anchor, or None when there’s nothing to undo. History accrues
across every successful edit that funnels through the splice primitive.
Sourcepub fn redo(&mut self) -> Result<Option<Change>, Error>
pub fn redo(&mut self) -> Result<Option<Change>, Error>
Redo the most recently undone edit step; the inverse of Editor::undo.
Returns None when the redo stack is empty (nothing undone, or a fresh
edit has invalidated it).
Sourcepub fn coalesce_last_undo(&mut self) -> Result<(), Error>
pub fn coalesce_last_undo(&mut self) -> Result<(), Error>
Fold the most recent edit into the undo step before it, so a caret editor
can coalesce a run of keystrokes into a single undo. Call right after an
edit_range that continues a run (same kind, no intervening caret move);
a no-op unless there are at least two steps to merge.
Sourcepub fn revision(&mut self) -> u64
pub fn revision(&mut self) -> u64
A monotonic change token, bumped once per successful mutation of the document (every edit and every undo/redo). Never decreases and never repeats for the life of the editor; the initial parse is revision 0. Equal revision means a byte-identical document, so it can key a cache instead of hand-tracking “did anything change?”.
Sourcepub fn dirty_range(&mut self) -> Option<Range<usize>>
pub fn dirty_range(&mut self) -> Option<Range<usize>>
The cumulative dirty byte range since the last Editor::clear_dirty
(or since the editor was created) — the union of every mutation’s byte
effect over that window, in current source coordinates — or None when
the document is clean relative to the last clear.
The incremental-rebuild companion to Editor::revision: revision
says whether a cached view (glyph rows, syntax spans) needs rebuilding,
this says which bytes changed, so a consumer rebuilds only the affected
part instead of the whole document. A single conservative interval: it
always covers every changed byte and may over-cover the gap between edits
to disjoint regions, but never under-covers.
It reports where bytes differ — exact, because twig splices losslessly
and never reflows untouched bytes — not where the parse differs. An
edit can reinterpret bytes outside the range (opening a code fence, a #
promoting a paragraph to a heading), so a consumer rebuilding structure
from it should widen the range to the enclosing block(s) itself (e.g. via
Editor::node_at on each end). Typical loop: on a repaint, if
Editor::revision moved, read this range, rebuild the rows it (widened)
covers, then call Editor::clear_dirty.
Sourcepub fn clear_dirty(&mut self)
pub fn clear_dirty(&mut self)
Acknowledge the current dirty range: mark the document clean so a later
Editor::dirty_range reports only mutations made after this call. Call
it once you’ve consumed the range (rebuilt the affected view). Leaves the
document, Editor::revision, and Editor::last_change untouched.
Sourcepub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error>
pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error>
Attach an opaque, caller-owned blob (e.g. a serialized caret/selection)
to the editor’s current document state. Twig copies the bytes and never
interprets them; it only carries them through the undo history so
Editor::undo/Editor::redo hand back the caret matching the
restored source (via Editor::caret_blob). Set it with the pre-edit
caret before an edit so the retired undo step captures it. An empty
blob clears the current caret.
Sourcepub fn caret_blob(&mut self) -> Result<Vec<u8>, Error>
pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error>
The opaque caret blob for the editor’s current document state (see
Editor::set_caret_blob). After Editor::undo/Editor::redo this
is the restored state’s caret; after an edit it is empty until set again.
Returns an owned copy, so it outlives the next edit.
Sourcepub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error>
pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error>
Snapshot the current tree as a flat FlatNode array (the JSON-free
read path for a renderer), indexed so nodes[i].id == NodeId(i). Walk it
via the parent/first_child/next_sibling links; the root is the node
whose parent is None.
Sourcepub fn child_spans(
&mut self,
node: Option<NodeId>,
) -> Result<Vec<QueryMatch>, Error>
pub fn child_spans( &mut self, node: Option<NodeId>, ) -> Result<Vec<QueryMatch>, Error>
The direct children of node as QueryMatches (id, span, kind) —
None enumerates the document root’s children (the top-level blocks). The
cheap top-level enumeration an incremental renderer walks to decide which
blocks changed, without marshalling the whole arena; pair it with
Editor::subtree to then re-marshal only those that did. A childless
node yields an empty vec.
Sourcepub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error>
pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error>
Snapshot the subtree rooted at node as a self-contained FlatNode
array with local ids: array[0] is the root, every link is an index
into the returned vec (or None), and spans stay absolute. The
incremental-render companion to Editor::nodes — re-marshal one edited
block’s subtree instead of the whole document. The root’s parent and
next_sibling are None, so a walk from index 0 stays inside the
subtree. Error::InvalidArgument if node is out of range.
Sourcepub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error>
pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error>
The deepest node whose span contains byte offset (with offset equal
to the source length treated as inside the root) — mouse hit-testing and
cursor context. Ok(None) if no node covers the offset;
Error::InvalidArgument if offset exceeds the source length.
Sourcepub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error>
pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error>
The chain of nodes containing byte offset, root-first down to the
deepest (the node Editor::node_at returns) — the ancestor path for a
breadcrumb or context-scoped edit. Empty if no node covers the offset.
Sourcepub fn wrap_range(
&mut self,
start: usize,
end: usize,
kind: InlineKind,
) -> Result<Change, Error>
pub fn wrap_range( &mut self, start: usize, end: usize, kind: InlineKind, ) -> Result<Change, Error>
Wrap [start, end) with kind’s delimiters — the unconditional half of
the inline toolbar (always adds a mark; *word* → **word** stacks).
Error::UnsupportedFormat if the document’s format can’t spell kind
(e.g. a Markdown InlineKind::Mark); Error::InvalidArgument for a
bad range; Error::EditConflict if the result doesn’t reparse.
Sourcepub fn toggle_inline(
&mut self,
start: usize,
end: usize,
kind: InlineKind,
) -> Result<Change, Error>
pub fn toggle_inline( &mut self, start: usize, end: usize, kind: InlineKind, ) -> Result<Change, Error>
Toggle kind over [start, end): remove the mark if the range already
is a node of kind (its whole span or its rendered interior), else
wrap it — a rich editor’s Cmd-B. Same error rules as
Editor::wrap_range.
Sourcepub fn set_block(
&mut self,
offset: usize,
kind: BlockKind,
) -> Result<Change, Error>
pub fn set_block( &mut self, offset: usize, kind: BlockKind, ) -> Result<Change, Error>
Convert the innermost heading/paragraph covering byte offset to kind,
rewriting its leading marker while keeping its inline content (the
toolbar’s H1…H6 / Body switch). Djot and Markdown only, else
Error::UnsupportedFormat; Error::NotFound if no heading/paragraph
covers offset; Error::InvalidArgument for a heading level outside
1–6.
Sourcepub fn toggle_block_container(
&mut self,
start: usize,
end: usize,
kind: BlockContainerKind,
) -> Result<Change, Error>
pub fn toggle_block_container( &mut self, start: usize, end: usize, kind: BlockContainerKind, ) -> Result<Change, Error>
Toggle a block container over the blocks [start, end) covers — the
toolbar’s Quote / Bulleted list / Numbered list buttons. Djot and Markdown
only, else Error::UnsupportedFormat; Error::NotFound if the range
covers no block; Error::InvalidArgument for a bad range.
The range widens to whole lines of the blocks it touches (you cannot quote half a paragraph), and the prefix lands at column 0, so a container wraps the outermost structure on those lines.
Whether this adds or removes is decided from the AST — the ancestors
of start — not by looking for a > in the source. It removes the
container only when the range covers every block that container holds, and
then only one level (> > a → > a). A partly covered container nests
instead, since removing it would drag its uncovered siblings out with it:
selecting the first paragraph of > a\n>\n> b\n gives > > a\n>\n> b\n.
Toggling one list kind while inside the other converts in place
(- a → 1. a) rather than nesting.
Each covered block becomes one item, so an ordered list numbers a
multi-block range 1., 2., 3.… Removing a list inserts a blank line
between items that lacked one, keeping them separate blocks (a tight
- a\n- b\n stripped bare would be a single two-line paragraph).
Sourcepub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error>
pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error>
Renumber the ordered list at byte offset so its markers run 1, 2, 3, …,
each nesting level restarting at 1 — the numbering a caret editor keeps as
items are inserted, deleted, and nested, where a raw splice leaves the
source numbers stale (1. 2. 2. 3.). Djot and Markdown; the display of an
ordered list is renumbered by any CommonMark renderer regardless, so this
is source hygiene, not a render fix.
Error::NotFound when offset is not inside an ordered list. When the
numbering is already sequential this is a no-op that still returns Ok —
the source is left byte-for-byte unchanged. The Change is not returned
because a no-op has none; re-read Editor::source_str for the result.
Sourcepub fn table_insert_row(
&mut self,
offset: usize,
below: bool,
) -> Result<(), Error>
pub fn table_insert_row( &mut self, offset: usize, below: bool, ) -> Result<(), Error>
Insert an empty row below (below) or above the caret’s row.
Sourcepub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error>
pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error>
Delete the caret’s row. Error::NotEditable for the header row or the
last remaining body row.
Sourcepub fn table_insert_column(
&mut self,
offset: usize,
right: bool,
) -> Result<(), Error>
pub fn table_insert_column( &mut self, offset: usize, right: bool, ) -> Result<(), Error>
Insert an empty column right (right) or left of the caret’s column.
Sourcepub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error>
pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error>
Delete the caret’s column. Error::NotEditable when it is the only one.
Sourcepub fn table_set_alignment(
&mut self,
offset: usize,
alignment: Alignment,
) -> Result<(), Error>
pub fn table_set_alignment( &mut self, offset: usize, alignment: Alignment, ) -> Result<(), Error>
Set the caret’s column to alignment.
Sourcepub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error>
pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error>
Move the caret’s row one place down (down) or up, within the body rows.
Sourcepub fn table_move_column(
&mut self,
offset: usize,
right: bool,
) -> Result<(), Error>
pub fn table_move_column( &mut self, offset: usize, right: bool, ) -> Result<(), Error>
Move the caret’s column one place right (right) or left.
Sourcepub fn insert_link(
&mut self,
start: usize,
end: usize,
destination: &str,
) -> Result<Change, Error>
pub fn insert_link( &mut self, start: usize, end: usize, destination: &str, ) -> Result<Change, Error>
Link [start, end) to destination — [text](destination). Djot and
Markdown only, else Error::UnsupportedFormat;
Error::InvalidArgument for a bad range or a destination containing a
newline (neither format can carry one, and quietly rewriting the URL would
be worse than refusing).
An existing link covering the range has its destination replaced and
its text kept, so re-linking fixes a URL instead of nesting
[[t](a)](b); to unlink, use Editor::unwrap_node.
A range inside an existing autolink (<https://x.dev>) re-points it
the same way, but there is no text to keep — an autolink’s text is its
destination — so the node is replaced whole, respelled canonically for the
new destination. This covers a caret and any selection the autolink
contains, including one covering it exactly: an autolink’s URL is not
editable text, so no part of it can host a [, and “link half this URL”
has no spelling. A caret inside both an autolink and a link
([<https://x.dev>](d)) re-points the link, whose text is separable from
its destination and so survives.
A selection starting or ending strictly inside an autolink without
being contained by it — running from ordinary text into the middle of a
URL — is refused with [Status::NotEditable]: half of it is real text,
so there is nothing to re-point, and any splice would rewrite the URL.
A selection that contains an autolink whole is unaffected — it splices
at the edges and wraps as usual.
A link with no text — an empty range, or re-pointing an existing
[](old) — is spelled canonically for the destination given, never as
[](destination): a childless link has nothing to render, so consumers
fall back to showing the destination and a caret has nowhere to sit. A
destination the format can autolink (an absolute URL or an email, by that
format’s own rules) yields <destination>; anything else yields
[destination](destination), the destination doubling as the text so it
stays visible and editable. Which destinations autolink is not the
caller’s to guess — <foo> is raw HTML in Markdown, a relative path goes
literal in both, and the formats disagree (<mailto:a@b.dev> is a url in
Markdown, an email in Djot), so each is asked its own parser.
The destination is escaped for the format, so a ) or a space in it
cannot break the markup — and the two formats genuinely differ: Markdown
ends a destination at the first space ([t](a b) is not a link at all) so
whitespace moves it into the <…> form, while Djot takes spaces literally
and would read <a b> as the URL itself.
Sourcepub fn insert_image(
&mut self,
start: usize,
end: usize,
destination: &str,
) -> Result<Change, Error>
pub fn insert_image( &mut self, start: usize, end: usize, destination: &str, ) -> Result<Change, Error>
Spell [start, end) as an image pointing at destination —
, the selected source becoming the alt text.
The destination is escaped exactly as insert_link
escapes one, because it is the same grammar production: Markdown moves a
destination holding whitespace into the <…> form, Djot leaves it bare
because <…> there would read as the URL itself. That is the reason this
exists rather than being a format! at the call site — 
is not an image in Markdown at all, and no caller can fix that without
reproducing twig’s per-format escape table.
Two ways it is simpler than a link. An empty range stays empty:
 is a perfectly good image, where the childless
[](destination) that insert_link works to avoid has nothing to render
or put a caret in. And there is no autolink or re-point reasoning — an
image has no bare-URL spelling, and re-pointing an existing one is a read
of its destination plus an insert, above this op.
Returns Error::InvalidArgument for a destination holding a newline and
Error::UnsupportedFormat for a parse-only format (XML, HTML).
Sourcepub fn insert_literal(
&mut self,
offset: usize,
text: &str,
) -> Result<Change, Error>
pub fn insert_literal( &mut self, offset: usize, text: &str, ) -> Result<Change, Error>
Insert text at offset as a literal run: every byte the format reads as
markup is backslash-escaped so the run reparses as exactly text — a typed
*, # or ` stays that character rather than opening emphasis, a
heading or a code span. This is the inverse of serialization (which writes
an already-parsed run verbatim): it is what a WYSIWYG surface calls so that
keyboard input can never mint markup, leaving formatting to explicit
commands.
The escaping is positional and per-format, and neither is the caller’s to
reproduce: inline specials (*, `, [, <…) are escaped anywhere
on the line, while block markers (#, >, -…) are escaped only where
offset sits in its line’s leading whitespace — so an inserted “5 - 3”
keeps its - but “- item” at column zero does not become a bullet. An
embedded newline in text re-enters that line-start zone.
Two constructs a byte-alphabet cannot reach are left as typed: a GFM
bare-URL autolink (https://x.com, with no delimiter to escape) and an
ordered-list marker (1., special only after a digit run). Returns
Error::UnsupportedFormat for a parse-only format (XML, HTML) and
Error::InvalidArgument when offset is past the source.
Sourcepub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error>
pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error>
Insert a hard line break inside a table cell at offset, spelled the
format’s way (<br> for Markdown). A table row is one source line, so the
ordinary newline-based hard break can’t appear there; the spliced <br>
reparses as a semantic hard_break node — not opaque raw HTML — so the
break reads back as structure. Like the other gestures it leans on the
splice+reparse+rollback backstop: a break that would no longer parse as the
same table yields Error::EditConflict and changes nothing.
Returns Error::UnsupportedFormat for a format with no in-cell break
spelling — djot (no idiomatic in-cell break), HTML and XML (parse-only);
Error::NotFound when offset is not inside a table cell (only the
in-cell gesture is spelled today); and Error::InvalidArgument when
offset is past the source.