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 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 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 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 caret in 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. Only a caret: a selection carries text of its own to
link, and one straddling the autolink’s edges has no re-point to mean, so
it wraps as usual. 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 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.