use super::*;
pub fn parse_at(source: &str, offset: usize, marks: &Marks) -> (Doc, Cursor) {
let (doc, at) = parse_at_many(source, &[offset], marks);
let at = at[0].unwrap_or_else(|| Cursor::default().clamp(&doc));
(doc, at)
}
pub fn parse_at_many(source: &str, offsets: &[usize], marks: &Marks) -> (Doc, Vec<Option<Cursor>>) {
let plain = parse_with(source, marks);
let mut at = vec![None; offsets.len()];
if !source
.chars()
.any(|c| crate::serialize::sentinel_index(c).is_some())
{
let offset = |ix: usize| offsets[ix].min(source.len());
let mut which: Vec<usize> = (0..offsets.len())
.filter(|&ix| {
source.is_char_boundary(offset(ix)) && crate::serialize::sentinel(ix).is_some()
})
.collect();
which.sort_by_key(|&ix| offset(ix));
place(source, &offset, &which, &plain, marks, &mut at);
}
(plain, at)
}
fn place(
source: &str,
offset: &impl Fn(usize) -> usize,
which: &[usize],
plain: &Doc,
marks: &Marks,
out: &mut [Option<Cursor>],
) {
if which.is_empty() {
return;
}
if let Some(found) = lifted(source, offset, which, plain, marks) {
for (ix, at) in found {
out[ix] = Some(at);
}
} else if which.len() > 1 {
let (left, right) = which.split_at(which.len() / 2);
place(source, offset, left, plain, marks, out);
place(source, offset, right, plain, marks, out);
}
}
fn lifted(
source: &str,
offset: &impl Fn(usize) -> usize,
which: &[usize],
plain: &Doc,
marks: &Marks,
) -> Option<Vec<(usize, Cursor)>> {
let mut marked = String::with_capacity(source.len() + 4 * which.len());
let mut from = 0;
for &ix in which {
marked.push_str(&source[from..offset(ix)]);
marked.push(crate::serialize::sentinel(ix)?);
from = offset(ix);
}
marked.push_str(&source[from..]);
let mut doc = parse_with(&marked, marks);
let mut found = Vec::with_capacity(which.len());
for (block_ix, block) in doc.blocks.iter_mut().enumerate() {
for part in block.parts() {
let Some(text) = block.text_at_mut(part) else {
continue;
};
let hits: Vec<(usize, char)> = text
.text
.char_indices()
.filter(|(_, c)| crate::serialize::sentinel_index(*c).is_some())
.collect();
let mut removed = 0;
for &(at, c) in &hits {
let ix = crate::serialize::sentinel_index(c)?;
found.push((ix, Cursor::new(block_ix, part, at - removed)));
removed += c.len_utf8();
}
for &(at, c) in hits.iter().rev() {
text.remove(at..at + c.len_utf8());
}
}
}
(found.len() == which.len() && doc == *plain).then_some(found)
}
pub(super) const OPEN: char = '\u{E010}';
pub(super) const CLOSE: char = '\u{E011}';
pub(super) fn tag(ix: usize) -> Option<char> {
char::from_u32(0xE020 + u32::try_from(ix).ok()?).filter(|_| ix < 0x100)
}
pub(super) fn tag_index(c: char) -> Option<usize> {
(0xE020..0xE120)
.contains(&(c as u32))
.then(|| c as usize - 0xE020)
}
pub(super) fn lift(source: &str, marks: &Marks) -> (String, Vec<(usize, usize)>) {
let skipped = literal(source);
let mut aligned = Vec::new();
let entries = marks.sorted();
let mut out = String::with_capacity(source.len());
let mut open: Vec<(usize, &str)> = Vec::new();
let mut at = 0usize;
while at < source.len() {
if let Some(range) = covering(&skipped, at) {
out.push_str(&source[at..range.end]);
at = range.end;
continue;
}
let rest = &source[at..];
if let Some(escaped) = rest.strip_prefix('\\') {
let width = escaped.chars().next().map_or(1, |c| 1 + c.len_utf8());
out.push_str(&rest[..width.min(rest.len())]);
at += width.min(rest.len());
continue;
}
let found = entries
.iter()
.find(|entry| rest.starts_with(entry.delimiter.as_ref()));
if let Some(entry) = found {
let delimiter: &str = entry.delimiter.as_ref();
let closes = open.last().is_some_and(|(_, open)| *open == delimiter);
if closes && !source[..at].ends_with(char::is_whitespace) {
out.push(CLOSE);
open.pop();
at += delimiter.len();
aligned.push((out.len(), at));
continue;
}
if !closes
&& let Some(ix) = marks.position(entry)
&& let Some(tag) = tag(ix)
&& closing(
source,
at + delimiter.len(),
delimiter,
&skipped,
line_end(source, at),
)
{
out.push(OPEN);
out.push(tag);
open.push((ix, delimiter));
at += delimiter.len();
aligned.push((out.len(), at));
continue;
}
}
let c = rest.chars().next().unwrap_or_default();
out.push(c);
at += c.len_utf8();
}
(out, aligned)
}
pub(super) fn unlift(aligned: &[(usize, usize)], lifted: usize) -> usize {
let ix = aligned.partition_point(|(at, _)| *at <= lifted);
match ix.checked_sub(1) {
Some(ix) => aligned[ix].1 + (lifted - aligned[ix].0),
None => lifted,
}
}
pub(super) fn closing(
source: &str,
from: usize,
delimiter: &str,
skipped: &[Range<usize>],
line_end: usize,
) -> bool {
if source[from..].starts_with(char::is_whitespace) {
return false;
}
let mut at = from;
while let Some(found) = source[at..line_end.max(at)].find(delimiter) {
let found = at + found;
let escaped = source[..found].ends_with('\\');
let literal = covering(skipped, found).is_some();
let spaced = source[..found].ends_with(char::is_whitespace);
if !escaped && !literal && !spaced && found > from {
return true;
}
at = found + delimiter.len();
}
false
}
pub(super) fn line_end(source: &str, at: usize) -> usize {
source[at..].find('\n').map_or(source.len(), |ix| at + ix)
}
fn covering(ranges: &[Range<usize>], at: usize) -> Option<&Range<usize>> {
let ix = ranges.partition_point(|range| range.end <= at);
ranges.get(ix).filter(|range| range.start <= at)
}
pub(super) fn literal(source: &str) -> Vec<Range<usize>> {
let mut out: Vec<Range<usize>> = Vec::new();
for (event, range) in Parser::new_ext(source, OPTIONS).into_offset_iter() {
match event {
Event::Code(_) | Event::Html(_) | Event::InlineHtml(_) => out.push(range),
Event::Start(Tag::CodeBlock(_)) => out.push(range),
Event::Start(Tag::Link { .. }) => {
let at = source[range.clone()]
.rfind("](")
.map_or(range.start, |ix| range.start + ix);
out.push(at..range.end);
}
Event::Start(Tag::Image { .. }) => out.push(range),
_ => {}
}
}
out.retain(|range| !range.is_empty());
out.sort_by_key(|range| range.start);
let mut merged: Vec<Range<usize>> = Vec::with_capacity(out.len());
for range in out {
match merged.last_mut() {
Some(last) if range.start < last.end => last.end = last.end.max(range.end),
_ => merged.push(range),
}
}
merged
}
pub(super) fn settle(text: &mut Text, marks: &Marks) {
if !text.text.contains(OPEN) {
return;
}
let mut settled = String::with_capacity(text.text.len());
let mut cut: Vec<(usize, usize)> = Vec::new();
let mut open: Vec<(usize, usize, usize)> = Vec::new();
let mut found: Vec<(usize, MarkSpan)> = Vec::new();
let mut opened = 0usize;
let mut chars = text.text.char_indices();
while let Some((at, c)) = chars.next() {
match c {
OPEN => {
let width = match chars.next() {
Some((_, tag)) => {
if let Some(ix) = tag_index(tag) {
open.push((ix, settled.len(), opened));
opened += 1;
}
OPEN.len_utf8() + tag.len_utf8()
}
None => OPEN.len_utf8(),
};
cut.push((at, width));
}
CLOSE => {
if let Some((ix, from, seq)) = open.pop()
&& let Some(entry) = marks.index(ix)
{
found.push((
seq,
MarkSpan {
range: from..settled.len(),
mark: Mark::Custom(entry.name.to_string()),
},
));
}
cut.push((at, CLOSE.len_utf8()));
}
_ => settled.push(c),
}
}
let moved = |offset: usize| {
offset
- cut
.iter()
.filter(|(at, _)| *at < offset)
.map(|(_, width)| width)
.sum::<usize>()
};
for span in &mut text.marks {
span.range = moved(span.range.start)..moved(span.range.end);
}
text.text = settled;
found.sort_by_key(|(seq, _)| *seq);
text.marks.extend(found.into_iter().map(|(_, span)| span));
text.marks
.sort_by_key(|span| (span.range.start, std::cmp::Reverse(span.range.end)));
text.marks.retain(|span| !span.range.is_empty());
}