Files
Ely-GPUI-Components/src/documents/document/format/clear.rs
T
ZacharyZhang-NY dceca9f3c6
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / site (push) Successful in 35s
CI / crate (push) Failing after 35s
CI / platforms (windows-latest) (push) Successful in 5m41s
Documents: inline formulas typeset in text; \( and \[ read as math
2026-10-18 22:42:07 -04:00

86 lines
3.0 KiB
Rust

//! Clear Format: every inline mark off the touched blocks.
use std::ops::Range;
use markdown::mdast::Node;
use super::{Change, EditRefusal, after, block, cut, holder, marks, room};
use crate::documents::markdown::{math_inside, span};
/// The backslashes of escapes in a text node's span.
fn escapes(text: &str, whole: Range<usize>) -> Vec<Range<usize>> {
let mut out = Vec::new();
let mut chars = text[whole.clone()].char_indices().peekable();
while let Some((at, ch)) = chars.next() {
if ch == '\\'
&& chars
.peek()
.is_some_and(|(_, next)| next.is_ascii_punctuation())
{
out.push(whole.start + at..whole.start + at + 1);
chars.next();
}
}
out
}
/// A node's inline marks, as Typora's `.md-meta`.
fn meta(node: &Node, text: &str, autolinks: &[Range<usize>]) -> Vec<Range<usize>> {
let whole = span(node);
match node {
Node::Html(_) => vec![whole],
// Autolink text keeps its backslashes.
Node::Text(_) if holder(autolinks, &whole).is_some() => Vec::new(),
Node::Text(_) => escapes(text, whole),
Node::InlineMath(_) => {
let inner = math_inside(text, whole.clone());
vec![whole.start..inner.start, inner.end..whole.end]
}
Node::FootnoteReference(_) => vec![whole.start..whole.start + 2, whole.end - 1..whole.end],
_ => marks(node, text).map(Vec::from).unwrap_or_default(),
}
}
/// Every inline mark in the blocks `selection` touches.
pub(super) fn clear(
text: &str,
nodes: &[&Node],
selection: Range<usize>,
) -> Result<Change, EditRefusal> {
let mut blocks: Vec<Range<usize>> = (nodes.iter())
.filter(|node| block(node))
.map(|node| room(node, text))
.filter(|range| range.start <= selection.end && selection.start <= range.end)
.collect();
if blocks.is_empty() {
return Err(EditRefusal::Outside);
}
blocks.sort_by_key(|block| block.start);
let mut autolinks: Vec<Range<usize>> = (nodes.iter())
.filter(|node| matches!(node, Node::Link(_)))
.map(|node| span(node))
.filter(|whole| !text[whole.clone()].starts_with('['))
.collect();
autolinks.sort_by_key(|link| link.start);
let mut inside: Vec<Vec<Range<usize>>> = vec![Vec::new(); blocks.len()];
for hole in (nodes.iter()).flat_map(|node| meta(node, text, &autolinks)) {
if let Some(at) = holder(&blocks, &hole).filter(|_| !hole.is_empty()) {
inside[at].push(hole);
}
}
let mut holes = Vec::new();
let mut edits = Vec::new();
for (block, mut cuts) in blocks.into_iter().zip(inside) {
if cuts.is_empty() {
continue;
}
cuts.sort_by_key(|hole| hole.start);
edits.push((block.clone(), cut(text, block, &cuts)));
holes.extend(cuts);
}
Ok((
edits,
after(selection.start, &holes)..after(selection.end, &holes),
))
}