documents: format checks nested marks, reports late refusals, reads small texts at once

Unwrapping a style now runs the crossing check on the found node, so an
end in a nested link address or strike mark refuses. Clear keeps an
autolink's backslashes. Block lookups use a sorted search, so the walk
grows linearly. A long format refuses a second one as Busy and reports
refusals, a changed text and a shown preview through FormatRefused.
markdown-rs grows with the square of its nodes, so texts read at once
only up to 16 KiB.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-14 12:47:18 -04:00
1 parent d238e3b4f0
commit 0289fdaa19
6 files changed
+236 -51

No files matched your search

+31 -8
View File
@@ -41,6 +41,10 @@ pub enum FormatRefusal {
Across,
/// Past the largest read: source alone.
TooLong,
/// A long format still runs.
Busy,
/// The text changed before a long format finished.
Changed,
}
/// The edits and the selection after them.
@@ -60,7 +64,7 @@ fn content(node: &Node) -> Range<usize> {
}
}
/// Room for text: a cell inside its pipes, end spaces too.
/// Room for text: inside a cell's pipes, trailing spaces too.
fn room(node: &Node, text: &str) -> Range<usize> {
let inner = match node {
Node::TableCell(_) => {
@@ -76,6 +80,12 @@ fn room(node: &Node, text: &str) -> Range<usize> {
inner.start..inner.end + spaces
}
/// Which of `sorted`, disjoint spans, holds `inner`.
pub(super) fn holder(sorted: &[Range<usize>], inner: &Range<usize>) -> Option<usize> {
let at = sorted.partition_point(|span| span.start <= inner.start);
(at > 0 && inner.end <= sorted[at - 1].end).then(|| at - 1)
}
fn block(node: &Node) -> bool {
matches!(
node,
@@ -151,11 +161,12 @@ fn verbatim(
selection: &Range<usize>,
format: InlineFormat,
) -> Result<(), FormatRefusal> {
let blocks: Vec<Range<usize>> = nodes
let mut blocks: Vec<Range<usize>> = nodes
.iter()
.filter(|node| block(node))
.map(|node| span(node))
.collect();
blocks.sort_by_key(|block| block.start);
for node in nodes {
let whole = span(node);
let touches = |edge: bool| {
@@ -166,9 +177,6 @@ fn verbatim(
false => whole.start < at && at < whole.end,
})
};
let inline = blocks
.iter()
.any(|block| block.start <= whole.start && whole.end <= block.end);
// Clear takes a block's inline code and tags too.
let held = format != InlineFormat::Clear && touches(false);
let why = match node {
@@ -177,7 +185,11 @@ fn verbatim(
Node::Yaml(_) | Node::Toml(_) if touches(true) => FormatRefusal::FrontMatter,
Node::InlineCode(_) if format != InlineFormat::Code && held => FormatRefusal::Code,
Node::InlineMath(_) if held => FormatRefusal::Math,
Node::Html(_) if (inline && held) || (!inline && touches(true)) => FormatRefusal::Html,
Node::Html(_) => match holder(&blocks, &whole).is_some() {
true if held => FormatRefusal::Html,
false if touches(true) => FormatRefusal::Html,
_ => continue,
},
_ => continue,
};
return Err(why);
@@ -222,9 +234,18 @@ pub(super) fn toggle(
selection: Range<usize>,
format: InlineFormat,
) -> Result<Change, FormatRefusal> {
let root = file_tree(text);
toggle_tree(text, &file_tree(text), selection, format)
}
/// `toggle` on `root`, the tree `text` parses to.
pub(super) fn toggle_tree(
text: &str,
root: &Node,
selection: Range<usize>,
format: InlineFormat,
) -> Result<Change, FormatRefusal> {
let mut nodes = Vec::new();
walk(&root, &mut nodes);
walk(root, &mut nodes);
verbatim(&nodes, &selection, format)?;
if format == InlineFormat::Clear {
return clear::clear(text, &nodes, selection);
@@ -248,6 +269,7 @@ pub(super) fn toggle(
.filter(|node| styled(format, node) && holds(node))
.min_by_key(|node| span(node).len());
if let Some(node) = found {
crossed(text, &nodes, &span(node), s, e, format)?;
return Ok(unwrap(
span(node),
marks(node, text).expect("a styled node has marks"),
@@ -283,6 +305,7 @@ pub(super) fn toggle(
if format == InlineFormat::Underline
&& let Some(tags) = underline(&nodes, &within, s, e)
{
crossed(text, &nodes, &(tags[0].start..tags[1].end), s, e, format)?;
return Ok(unwrap(tags[0].start..tags[1].end, tags));
}
if s != e {