Skip to main content

Editor

Struct Editor 

Source
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

Source

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.

Source

pub fn new_str(input: &str, format: Format) -> Result<Self, Error>

Source

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.

They also decide what the authoring gestures may write, since a gesture may only mint bytes this editor’s own reparse reads back: MarkdownExtensions::highlight makes ==x== a highlight Editor::toggle_inline can add and remove, and MarkdownExtensions::highlight_colors makes Editor::set_mark_color available on top of it. Without them those calls are Error::UnsupportedFormat — see Format::supports_with, which answers for the extensions rather than for the format alone.

Source

pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error>

Replace the whole source of the located node with text.

Source

pub fn replace_content( &mut self, locator: &str, text: &str, ) -> Result<(), Error>

Replace the interior (between-delimiters content) of the located container.

Source

pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error>

Insert text immediately before the located node.

Source

pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error>

Insert text immediately after the located node.

Source

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).

Source

pub fn delete(&mut self, locator: &str) -> Result<(), Error>

Delete the located node (removes exactly its span; no whitespace cleanup).

Source

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.

Source

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.

Source

pub fn move_before(&mut self, locator: &str, anchor: &str) -> Result<(), Error>

Move the node locator names to immediately before the node anchor names — a canvas’s “send backward”, a list’s reorder — in one splice and one undo step, the bytes between the two copied verbatim in their new order. The node travels with the whitespace run ahead of it (the line break and indentation a pretty-printed document separates siblings with), which lands after it here, so every sibling keeps its separator: <g>\n <a/>\n <b/>\n</g> reorders to <g>\n <b/>\n <a/>\n</g>, never to a line holding both. The rule is about bytes, not structure — a block quote’s > prefixes do not travel — and the anchor need not be a sibling: next to a node in another container is a reparent.

Error::InvalidArgument when either node’s span holds the other’s, or the two are one node; Error::NotFound and Error::Ambiguous as every other tree op; Error::EditConflict when the moved document no longer parses, in which case nothing changed.

Source

pub fn move_after(&mut self, locator: &str, anchor: &str) -> Result<(), Error>

Move the node locator names to immediately after the node anchor names — a canvas’s “bring forward”. The whitespace run ahead of the node travels with it and stays ahead of it. Otherwise Editor::move_before.

Source

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.

Source

pub fn source(&mut self) -> Result<Vec<u8>, Error>

The editor’s current (edited) source bytes.

Source

pub fn source_str(&mut self) -> Result<String, Error>

The editor’s current source bytes as a UTF-8 string.

Source

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.

Source

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.

Source

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.

Source

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.)

Source

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.

Source

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).

Source

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.

Source

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?”.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn document(&mut self) -> Result<DocumentView<'_>, Error>

The editor’s current tree as a borrowed Document, so the whole document read surface (Document::nodes, Document::children, Document::subtree, Document::node_at, Document::query, Document::span, …) applies to a document being edited.

The view borrows the editor mutably, so no edit can land while it is alive and the ids it yields cannot go stale; drop it to edit again. See DocumentView for the two methods it cannot serve.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

A range crossing a block boundary gets one pair per block, in a single splice — so one undo step and one Change:

one two\n\nthree four   ->   **one two**\n\n**three four**

rather than one pair straddling the blank line, which reparses as four literal asterisks and no mark at all. A block’s own marker stays outside the pair (a heading keeps its # , a list item its - ), and a code block inside the range is stepped over — ** in a program is two asterisks. A code span the range cuts into is taken whole, so the pair closes around its backticks (**`word`** from a selection of word) rather than inside them. A range with no inline content anywhere in it, one wholly inside a fence, is Error::NotEditable. A zero-width range is exempt from all of this: it crosses nothing, and opening an empty pair for the caret to type between is the gesture.

Source

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 — covers its whole rendered interior and reaches no further than its own delimiters, or is a mark of another kind that is nothing but it (***word*** selected whole is the strong inside the emphasis) — else wrap it — a rich editor’s Cmd-B. Same error rules as Editor::wrap_range, and the same per-block cutting: remove-or-wrap is decided once per block the range touches, so a second press over a multi-block selection takes off every mark the first one put on instead of nesting a second pair around each.

Source

pub fn set_mark_color( &mut self, offset: usize, color: Option<MarkColor>, ) -> Result<Change, Error>

Set — or clear, with None — the colour of the highlight (a mark) the caret at offset is inside.

Markdown only, and only for an editor created with MarkdownExtensions::highlight_colors (which needs MarkdownExtensions::highlight with it) — else Error::UnsupportedFormat. Ask Format::supports_with with Gesture::SetMarkColor and the same extensions.

Setting a colour on an uncoloured highlight inserts the prefix, setting one on a coloured highlight replaces it, and None removes it — with the space after the emoji, which is part of the spelling. An existing prefix keeps its own spacing: ==🔴text== recolours tight, because that is what its author wrote.

Error::NotEditable when the caret is not inside a highlight. Clearing a colour a highlight does not have is a no-op that succeeds, and the Change it returns then describes the most recent prior edit (or an empty one), so it is not proof the source moved.

Authoring a coloured highlight from nothing is two gestures — a colour is a property of a highlight that already exists:

let exts = MarkdownExtensions {
    highlight: true,
    highlight_colors: true,
    ..Default::default()
};
let mut ed = Editor::new_ext(b"a word b\n", Format::Markdown, exts)?;
ed.toggle_inline(2, 6, twig::InlineKind::Mark)?; // a ==word== b
ed.set_mark_color(4, Some(MarkColor::Red))?;     // a ==🔴 word== b
Source

pub fn set_block( &mut self, offset: usize, kind: BlockKind, ) -> Result<Change, Error>

Convert the innermost heading/paragraph covering byte offset to kind (the toolbar’s H1…H6 / Body switch). Where the format spells a heading with a leading marker (Djot, Markdown, AsciiDoc) that marker is rewritten and the inline content kept byte for byte; where it spells one as a tag pair (HTML) the block is rebuilt as a node of the new kind and printed by the format’s own serializer, so <p>a <em>b</em></p> becomes <h2>a <em>b</em></h2> with its attributes along. Error::UnsupportedFormat for a format that can do neither (XML); Error::InvalidArgument for a heading level outside 1–6.

On a BLANK LINE this OPENS the block rather than converting one, so “H2, then type” works from an empty line the way it works from a full one — there is no node there to rewrite, since no format spells an empty paragraph. The marker is blank-separated from whatever precedes it (Djot does not let a heading interrupt a paragraph, so a marker flush under one is read as that paragraph’s text) and carries the line’s quote markers, so a heading opened on a quote’s blank line stays inside the quote. BlockKind::Paragraph there is a no-op: a blank line already holds no marker.

Error::NotEditable when the blank line is INTERIOR to a block rather than between blocks — inside a fenced code block, or a table.

Source

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 (- a1. 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).

Source

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.

Only lines the PARSER reads as items are touched, so this never rewrites a digit the author wrote as prose. That is not a corner case across formats: Djot doesn’t let a list marker interrupt a paragraph, so in 1. a\n 2. b the second line is text inside item a, while Markdown reads it as a nested item — the same bytes, renumbered in one format and left alone in the other.

Source

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.

Source

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.

Source

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.

Source

pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error>

Delete the caret’s column. Error::NotEditable when it is the only one.

Source

pub fn table_set_alignment( &mut self, offset: usize, alignment: Alignment, ) -> Result<(), Error>

Set the caret’s column to alignment.

Source

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.

Source

pub fn table_move_column( &mut self, offset: usize, right: bool, ) -> Result<(), Error>

Move the caret’s column one place right (right) or left.

Source

pub fn insert_table( &mut self, offset: usize, rows: usize, cols: usize, ) -> Result<Change, Error>

Insert a fresh table — one header row, rows body rows, cols columns, every cell empty — as its own block after the block offset sits in.

The placement is Editor::insert_thematic_break’s, decision for decision: after the caret’s block rather than at the caret, blank-line separated on both sides, carrying a block quote’s prefix on every line, and at column zero after a list item. The blank above is load-bearing here too — GFM can read a table’s header row out of the paragraph it follows. The bytes are the format’s own table spelling, through the same emitter the table_* edits re-spell with, so the table this writes is one they can edit.

There is no Error::NotFound: an empty document is a fine place for a table. Error::InvalidArgument for rows == 0 or cols == 0 — a header with nothing under it is the shape Editor::table_delete_row refuses to leave — or an offset past the source; Error::UnsupportedFormat where the format has no table spelling, before anything is read. Gesture::InsertTable answers ahead of time.

Source

pub fn insert_directive( &mut self, offset: usize, name: &str, label: Option<&str>, attrs: &[(&str, Option<&str>)], ) -> Result<Change, Error>

Insert a leaf directive — Markdown’s ::name[label]{attrs} — as its own block after the block offset sits in.

The placement is Editor::insert_thematic_break’s, decision for decision: after the caret’s block rather than at the caret, blank-line separated on both sides, a block quote’s prefix on every line (djot spells this over two lines, and a marker on the opener alone would leave the closing fence outside the quote), and column zero after a list item.

What a name means is the application’s. Twig writes a named container and reads one back; "page-break" and "embed" are your words and nothing here interprets them. The bytes are the format’s own spelling of that node: ::name{…} in Markdown, an empty ::: name fence in djot (whose div is anonymous, so the name comes back as a class), <name>…</name> in HTML.

label is the bracketed text, None for none — a different document from Some("") (::name against ::name[]). attrs is the (key, Some(value)) / (key, None) pair list Builder::set_attrs takes.

A name is an ASCII letter followed by letters, digits, - and _ — the grammar every format reads one back by, checked before anything is written because a name goes where a delimiter would otherwise be. "page-break" and "x-embed" are names; "a b", "a:b", "]{" and "1x" are not, and each is a different wrong document per format (::a b is a paragraph holding an inline directive named a, ::: a:b is a paragraph of colons). A label may not carry a line end or a square bracket, either of which closes the […] early.

Error::InvalidArgument for a name outside that grammar (an empty one included), such a label, or an offset past the source. There is no Error::NotFound: an empty document is a fine place for one. Error::UnsupportedFormat where the format would not read the printed bytes back as a container carrying the name, before anything is read — and for Markdown that is the parse config’s answer, so Format::supports reports false while Format::supports_with reports true:

let exts = MarkdownExtensions { directives: true, ..Default::default() };
assert!(!Format::Markdown.supports(Gesture::InsertDirective));
assert!(Format::Markdown.supports_with(exts, Gesture::InsertDirective));

The editor must have been created with the same extensions (Editor::new_ext) or the call itself refuses.

Source

pub fn set_block_attrs( &mut self, offset: usize, attrs: &[(&str, Option<&str>)], ) -> Result<Change, Error>

Replace the attribute set of the block offset sits in — a paragraph or heading, the block Editor::set_block rewrites — with attrs, the (key, Some(value)) list Builder::set_attrs takes. Replace, not merge: read the node’s attributes, edit the list, pass it back whole; an empty list clears them.

What a key means is yours, as a directive’s name is: a centred paragraph is set_block_attrs(off, &[("class", Some("center"))]) and twig spells the pair without interpreting either half. The spelling is the format’s — djot’s {…} line before the block, rewritten in place with the block’s bytes untouched; HTML’s tag and AsciiDoc’s […] line, the block re-printed; and in Markdown a <div …> around the block, blank-separated, which every Markdown renderer passes through and twig’s own parser pairs back into a container only under MarkdownExtensions::html_elements. A block already the sole child of such a div has the div’s attributes replaced instead, and an empty list unwraps it.

An attribute must be one every format reads back: a key that is an ASCII letter or _ followed by letters, digits, -, _ and :, a Some value (djot has no bare attribute), and no line end or double quote in it. Error::InvalidArgument otherwise, or for an offset past the source. Error::NotFound when no paragraph or heading holds offset. Error::NotEditable where the spelling cannot be placed: a djot block starting on a list item’s marker line, or whose attributes came from more than one {…} block; a Markdown block inside a list item. Error::UnsupportedFormat where the format would not read the printed attributes back, before anything is read — for Markdown the parse config’s answer, so Format::supports reports false while Format::supports_with reports true:

let exts = MarkdownExtensions { html_elements: true, ..Default::default() };
assert!(!Format::Markdown.supports(Gesture::SetBlockAttrs));
assert!(Format::Markdown.supports_with(exts, Gesture::SetBlockAttrs));
assert!(Format::Djot.supports(Gesture::SetBlockAttrs));
Source

pub fn set_node_attrs( &mut self, node: NodeId, attrs: &[(&str, Option<&str>)], ) -> Result<Change, Error>

Replace the attribute set of the element node — an id from Editor::nodes, valid against the current tree, so read the tree again after any successful edit — with attrs, the same list Editor::set_block_attrs takes; an empty list clears them. Replace, not merge.

The node-addressed sibling of Editor::set_block_attrs, for the caller that holds a tree rather than a caret: a canvas editor over an SVG names the <rect> it is dragging, and no byte offset stands for it. The run is written on the element’s own start tag, at the span Document::attrs_span reports, as key="value" pairs with &, <, > and " as entities. Nothing else in the element moves, its children included — a <g> holding a thousand paths is not re-printed to change its transform. An element with no attributes yet has the run inserted right after its name.

Error::UnsupportedFormat where the format keeps a node’s attributes anywhere but on the node’s own tag — every format but Format::Xml today; Format::supports with Gesture::SetNodeAttrs says which. Error::InvalidArgument for an id past the tree or an attribute no format reads back (the rule Editor::set_block_attrs states); Error::NotEditable for a node that is not an element — a text run, a comment.

let mut ed = Editor::new_str("<svg><rect x=\"1\"/></svg>", Format::Xml)?;
let rect = ed.nodes()?.into_iter().find(|n| n.name.as_deref() == Some("rect")).unwrap();
ed.set_node_attrs(rect.id, &[("x", Some("10")), ("fill", Some("red"))])?;
assert_eq!(ed.source_str()?, "<svg><rect x=\"10\" fill=\"red\"/></svg>");
Source

pub fn wrap_range_attrs( &mut self, start: usize, end: usize, attrs: &[(&str, Option<&str>)], ) -> Result<Change, Error>

Wrap [start, end) in an anonymous inline container carrying attrs — djot’s [text]{…}, HTML’s and Markdown’s <span …> — or, when the range already lies inside such a span, replace that span’s attributes rather than nest a second; an empty list there unwraps it, keeping the content bytes. That is Editor::insert_link’s rule for a link covering the range, for the same reason. A span named span and an anonymous one are the same node here; a :span[…] the Markdown parser read as a directive is neither.

The inline half of Editor::set_block_attrs, with its vocabulary rule and its attribute grammar (Error::InvalidArgument for a key or value no format reads back, or a bad range). The covered inline nodes are printed under the container by the format’s own serializer, so a mark inside the range rides along. Error::NotEditable for an empty range with no span to re-style, or a range cutting a node the gesture cannot slice. Error::UnsupportedFormat where the format would not read the printed span back: AsciiDoc, whose [#id.role]#text# keeps an id and a role and drops any other key, and Markdown without MarkdownExtensions::html_elements — ask Format::supports_with.

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 Error::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.

Source

pub fn insert_image( &mut self, start: usize, end: usize, destination: &str, ) -> Result<Change, Error>

Spell [start, end) as an image pointing at destination![alt](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 — ![](my file.png) 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: ![](destination) 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).

Source

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 escaped the format’s way 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. In the backslash formats (Djot, Markdown, AsciiDoc) 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 — and an embedded newline in text re-enters that line-start zone. Inside a code span, code block or raw node the run is written as it is, since a backslash there would show. HTML escapes with entities (&lt;, &amp;) in every position.

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) and Error::InvalidArgument when offset is past the source.

Source

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.

Source

pub fn insert_thematic_break(&mut self, offset: usize) -> Result<Change, Error>

Insert a thematic break (a horizontal rule) as its own block, on the line after the block offset sits in. A rule is a block, so there is no spelling for one mid-paragraph.

The rule is blank-line separated from its neighbours, and that is load-bearing rather than cosmetic: Markdown reads --- on the line directly under a paragraph as a setext <h2> underline, so a rule written flush against its predecessor silently becomes a heading and swallows it. The blank below is added only when the next line isn’t already blank. The spelling is the format’s (--- for Markdown, * * * for djot) and not the caller’s to reproduce.

Inside a block quote the rule inherits the quote’s prefix and stays in the quote. Inside a list it lands at column zero after the caret’s item, which splits the list in two with the rule between — a real document, nothing swallowed. There is no Error::NotFound: an empty document is a fine place for a rule. Error::UnsupportedFormat for a parse-only format (XML, HTML); Error::InvalidArgument when offset is past the source.

Source

pub fn split_block(&mut self, offset: usize) -> Result<Change, Error>

Split the block at offset in two at the caret, both halves the same kind — Enter in the middle of a paragraph, and the gesture Editor::insert_thematic_break deliberately is not. A host wanting “rule at the caret” calls this and then that.

Nearly a pure insertion at offset: what is minted is the separator between the halves, and the only bytes removed are the second half’s leading spaces and tabs, which are structure rather than content at the start of a block — a split at - b| c that kept its space would write - c, setting that item’s content indent to three. A code block sheds nothing, because there leading whitespace is the content.

  • A paragraph gets a blank line. Inside a quote the blank carries the quote’s marker and the second half its full prefix, so the split happens inside the quote rather than ending it.
  • A paragraph in a list item gets the item’s marker instead of a blank, so the second half is a sibling item: - this is |a list item becomes - this is and - a list item. The marker is repeated verbatim, ordered numbers included, so a split 1. item yields two 1. items — both formats renumber on render, and Editor::renumber_ordered_lists is the gesture for fixing the source. A task item’s new half is an unchecked box whatever the original’s state. A nested item’s leading indent rides along with its marker, so the new sibling stays in its own list rather than dropping to column zero and joining the enclosing one.
  • A heading repeats its own marker at its own level; Editor::set_block is how a caller demotes the second half instead.
  • A code block becomes two code blocks, the opening fence line reproduced verbatim so its width and info string both survive. A consumer that doesn’t want the gesture offered there can ask the tree what block the caret is in before calling.

At a block boundary this still splits, which is what makes it Enter: at the end of a list item it opens an empty sibling item, which is the block the caller wants to type into. A paragraph is the one place that empty block cannot be spelled — no format has an empty paragraph — so the source gains a blank line and reparses as one paragraph; the node appears when there is text to hold.

Error::NotEditable where a caret-split has no honest meaning: a table (a newline mid-cell destroys rather than divides; splitting one table into two is a table gesture, not this one), a setext heading (whose --- underline would end up under the second half alone — Editor::set_block normalises one to ATX, which makes this work), and an indented code block (where a blank line is interior, so the split would parse back as one block). Error::NotFound when nothing covers offset; Error::InvalidArgument when offset is past the source.

Source

pub fn join_blocks(&mut self, offset: usize) -> Result<Change, Error>

Join the block at offset into the block before it — Backspace at the start of a block, and forward Delete at the end of the one above it. The inverse of Editor::split_block, and the reason it is a gesture rather than a host’s own delete: what joins two blocks is a fact about the format. Deleting the newline between them is right only for two Markdown paragraphs at the top level — in HTML that byte is the > of </p>, under a heading it leaves two blocks, and after a Markdown <div> it deletes the blank line the div needed and breaks the div.

B is the innermost paragraph/heading covering offset. A is the leaf block immediately before it in document order, not the sibling before it: the block visually above below in above / <div> / hello / </div> / below is hello, three levels down, and joining into above would be the wrong paragraph.

  • What is written is a line break plus the container prefix A’s own line sits behind — a quote’s > repeated, a list item’s marker’s width in spaces, nothing at the top level — which is what keeps the joined line inside its containers in a format with no lazy continuation. A heading A with a leading marker (# Title, AsciiDoc’s == Title) is one line by its own spelling, so what joins there is a single space: # Title + below is # Title below.
  • What travels is A’s own closing markup (an ATX closing # run, a setext underline, </p>) and the closers of every container A is in that B is not (a Markdown </div>, a djot ::: fence), carried past the text that was pulled in — so the joined block keeps A’s presentation and stays where it was.
  • What is dropped is everything between them: the blank line, B’s markers, B’s attribute line (djot’s {…}, AsciiDoc’s […]), B’s opening tags. B’s attributes go on purpose — the joined text is A’s block, so it takes A’s presentation.
  • A prefix container (a quote, a list item, a list, a section) has no closing bytes, so whatever follows B inside one stays where it is: joining the first item’s text out of a list leaves the rest a list.

Error::NotFound when no block covers offset, and when B is the document’s first block — the ordinary Backspace-at-the-top answer. Error::NotEditable when A is not a paragraph or a heading (a code block, a table, a rule: there is no text to join into), when either block is in a table cell, when B is a setext heading (whose underline is how it is spelled at all — Editor::set_block normalises one to ATX, which makes this work), when B would have to leave a delimited container that still has content after it, which is the one shape this refuses rather than guesses at, and when the gap between A and B holds anything but separation — an empty container, or a definition the splice would destroy and no tree walk could see (Markdown keeps a link reference and a footnote definition as a lookup table, not as a node). Error::InvalidArgument when offset is past the source.

Error::UnsupportedFormat where a block cannot span lines at all — and note this is a different, wider gate than split_block’s. Ask Format::supports with Gesture::JoinBlocks.

Source

pub fn toggle_code_block( &mut self, start: usize, end: usize, language: Option<&str>, ) -> Result<Change, Error>

Toggle a fenced code block over the blocks [start, end) covers: fence them if the caret is not in a code block, unfence the one it is in if it is. language tags the opening fence and is ignored when unfencing.

None and Some("") are different requests: both write a bare fence, but the second says the caller asked for an empty info string. Reading the language back gives None either way — the distinction is in the ask, not the bytes. (Across the C ABI this rides as the (ptr, len, has_*) triple, the same spelling Builder::add_code_block uses for the same value.)

Fencing inserts at the covered region’s edges rather than rewriting its lines, so a body already carrying a quote’s > keeps it and the fence lines get the same prefix. The fence is measured — one character longer than the longest run of the fence character in the body — so fencing text that itself contains a fence nests instead of closing early.

Unfencing peels the opening line and, when there is one, the closing fence line; a Markdown indented code block has no fence to peel and is dedented instead, so the toggle stays reversible on the older spelling. Note that unfencing can yield a different tree than the one that was fenced: a code body is by definition text the parser did not read as markup, so # x inside a fence becomes a heading once the fence is gone.

Error::NotEditable inside a list item, in both directions: a quote’s marker is on every line, a list item’s is on its first line only, so a fence at column zero there would pull the - into the code body and the item would stop being an item. Error::InvalidArgument for an info string the fence cannot carry (a line end, the fence character, or — in Markdown, whose info string ends at whitespace — a space); Error::UnsupportedFormat for a parse-only format; Error::NotFound when no block covers the range.

Source

pub fn set_code_language( &mut self, offset: usize, language: Option<&str>, ) -> Result<Change, Error>

Retag the code block at offset with language, or clear its info string with None — the language dropdown beside a code block. Same None/Some("") distinction as Editor::toggle_code_block.

Only the info string is rewritten; the fence’s own width is kept, because it was measured against a body this does not touch. Error::NotEditable for an indented Markdown code block, which has no fence and so nowhere to carry a language; Error::NotFound when offset is not in a code block.

Source

pub fn toggle_task_item(&mut self, offset: usize) -> Result<Change, Error>

Add a checkbox to the list item at offset, or take one away — the gesture that converts between a plain list item and a task list item. A box is added unchecked; Editor::set_task_checked ticks it.

The box is inline content of the item’s first paragraph, not part of its marker, so adding or removing one leaves the item’s continuation-line indentation alone. An item inside a quote is found past the quote markers. Error::NotFound when offset is in no list item; Error::NotEditable when the item’s line carries no recognizable list marker; Error::UnsupportedFormat for a format with no checkbox.

Source

pub fn set_task_checked( &mut self, offset: usize, checked: bool, ) -> Result<(), Error>

Tick or untick the task item at offset — a checkbox click when the caller knows which way it should end up.

Rewrites the box alone, never the space after it, so an item spelled with unusual spacing keeps it. A capital [X] is read as checked.

When the box is already in the requested state 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.

Error::NotEditable when the item has no box: minting one here would make “set checked” silently convert a bullet into a task, which is Editor::toggle_task_item’s job to do explicitly.

Source

pub fn toggle_task_checked(&mut self, offset: usize) -> Result<Change, Error>

Flip the task item at offset — what a checkbox click actually is when the caller does not already know the state. Always edits or fails, so unlike Editor::set_task_checked there is no silent no-op and the Change is always real.

Source

pub fn insert_footnote( &mut self, offset: usize, label: &str, ) -> Result<Change, Error>

Insert a footnote reference at offset and, unless the label is already defined, the matching definition at the end of the document.

It writes both halves, because in neither format is half a footnote a footnote: a bare [^a] with nothing defining it renders as four literal characters. The definition body is left empty — that parses, and the caller then types into it like any other block. A label that is already defined gets only the reference, so referring to one footnote twice does not append a second, dead definition.

It is one edit, spanning the caret to the end of the document even though the halves are far apart: two edits would take two undos to reverse, and the returned Change would describe only the second, omitting the reference the caret is sitting in.

Error::InvalidArgument for a label that is empty or holds a line end or a reference bracket; Error::UnsupportedFormat for a format with no footnotes.

Trait Implementations§

Source§

impl Debug for Editor

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Drop for Editor

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.