use std::ops::Range;
use pulldown_cmark::Event;
use crate::markdown::ast::{to_u32, LineSpan, SrcLines};
use crate::markdown::parse_offsets::{BlockKind, RangeTracker};
pub(super) struct LineIndex<'s> {
src: &'s str,
starts: Vec<usize>,
cursor: usize,
}
impl<'s> LineIndex<'s> {
pub(super) fn new(src: &'s str) -> Self {
let mut starts = Vec::with_capacity(src.len() / 32 + 1);
starts.push(0);
starts.extend(memchr::memchr_iter(b'\n', src.as_bytes()).map(|i| i + 1));
Self {
src,
starts,
cursor: 0,
}
}
fn line_of(&self, byte: usize) -> usize {
self.starts
.partition_point(|&s| s <= byte)
.saturating_sub(1)
}
fn line_at(&mut self, byte: usize) -> usize {
let at_or_past_cursor = self.starts.get(self.cursor).is_some_and(|&s| s <= byte);
if !at_or_past_cursor {
self.cursor = self.line_of(byte);
return self.cursor;
}
while self.starts.get(self.cursor + 1).is_some_and(|&s| s <= byte) {
self.cursor += 1;
if self.starts.get(self.cursor + 8).is_some_and(|&s| s <= byte) {
self.cursor = self.line_of(byte);
break;
}
}
self.cursor
}
fn end_line(&self, line: usize, range: &Range<usize>) -> usize {
let end = range.end.min(self.src.len());
if end <= range.start || self.starts.get(line + 1).is_none_or(|&next| end <= next) {
return line;
}
let body = &self.src.as_bytes()[range.start..end - 1];
if memchr::memchr(b'\n', body).is_none() {
return line;
}
line + memchr::memchr_iter(b'\n', body).count()
}
fn col_of(&self, line: usize, byte: usize) -> usize {
let mut byte = byte.min(self.src.len());
let line_start = self.starts.get(line).copied().unwrap_or(0).min(byte);
if byte > line_start && self.src.as_bytes()[byte - 1..].starts_with(b"\r\n") {
byte -= 1;
}
self.src[line_start..byte].chars().count()
}
fn line_text(&self, line: usize) -> &'s str {
let Some(&start) = self.starts.get(line) else {
return "";
};
let end = self
.starts
.get(line + 1)
.map_or(self.src.len(), |&next| next - 1);
self.src.get(start..end).unwrap_or("")
}
fn last_line(&self, line: usize, range: &Range<usize>) -> usize {
let bytes = self.src.as_bytes();
let mut end = range.end.min(bytes.len());
while end > range.start && bytes[end - 1] == b'\n' {
end -= 1;
}
self.end_line(line, &(range.start..end))
}
fn ends_mid_line(&self, range: &Range<usize>) -> bool {
let bytes = self.src.as_bytes();
let end = range.end;
let at_line_end = |rest: &[u8]| rest.starts_with(b"\n") || rest.starts_with(b"\r\n");
end > range.start
&& end < bytes.len()
&& bytes[end - 1] != b'\n'
&& !at_line_end(&bytes[end..])
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum LeafMode {
Prose,
Verbatim,
Table,
}
struct LeafRec {
mode: LeafMode,
line_pending: bool,
break_end: Option<usize>,
first: Option<usize>,
last: usize,
cols: Vec<Option<u32>>,
atomic: Option<(usize, usize)>,
}
impl LeafRec {
fn wants(&mut self, event: &Event<'_>, range: &Range<usize>) -> bool {
match self.mode {
LeafMode::Verbatim => true,
LeafMode::Table => matches!(
event,
Event::Start(pulldown_cmark::Tag::TableHead | pulldown_cmark::Tag::TableRow)
),
LeafMode::Prose => match event {
Event::SoftBreak | Event::HardBreak => {
let begins = self.take_pending();
self.line_pending = true;
self.break_end = Some(range.end);
begins
}
Event::Code(_)
| Event::InlineMath(_)
| Event::DisplayMath(_)
| Event::InlineHtml(_)
| Event::Html(_)
| Event::Start(pulldown_cmark::Tag::Image { .. }) => {
self.take_pending();
true
}
_ => self.take_pending(),
},
}
}
fn take_pending(&mut self) -> bool {
self.break_end = None;
std::mem::replace(&mut self.line_pending, false)
}
fn continues_atomic(&self, line: usize) -> bool {
self.atomic
.is_some_and(|(start, until)| line > start && line <= until)
}
fn has_col(&self, line: usize) -> bool {
self.first
.and_then(|first| line.checked_sub(first))
.and_then(|i| self.cols.get(i))
.is_some_and(Option::is_some)
}
fn note(&mut self, line: usize, col: usize) {
let first = *self.first.get_or_insert(line);
let Some(i) = line.checked_sub(first) else {
return;
};
let col = to_u32(col);
if self.cols.len() <= i {
self.cols.resize(i + 1, None);
}
let slot = &mut self.cols[i];
*slot = Some(slot.map_or(col, |c| c.min(col)));
}
}
pub(super) struct ContainerStart {
line: usize,
range: Range<usize>,
}
pub(super) struct EventStream<'s, I>
where
I: Iterator<Item = (Event<'s>, Range<usize>)>,
{
inner: I,
peeked: Option<Event<'s>>,
pulled_range: Range<usize>,
lines: LineIndex<'s>,
ranges: RangeTracker<fn(BlockKind) -> bool>,
spare_cols: Vec<Option<u32>>,
base_line: usize,
leaf: Option<LeafRec>,
}
impl<'s, I> EventStream<'s, I>
where
I: Iterator<Item = (Event<'s>, Range<usize>)>,
{
pub(super) fn new(src: &'s str, events: I) -> Self {
Self {
inner: events,
peeked: None,
pulled_range: 0..0,
lines: LineIndex::new(src),
ranges: RangeTracker::new(|_| true),
spare_cols: Vec::new(),
base_line: 0,
leaf: None,
}
}
#[inline(always)]
fn pull(&mut self) -> Option<Event<'s>> {
let (event, range) = self.inner.next()?;
self.ranges.observe(self.lines.src, &event, &range);
self.pulled_range = range;
Some(event)
}
#[inline(always)]
pub(super) fn peek(&mut self) -> Option<&Event<'s>> {
if self.peeked.is_none() {
self.peeked = self.pull();
}
self.peeked.as_ref()
}
#[inline(always)]
pub(super) fn next(&mut self) -> Option<Event<'s>> {
let event = match self.peeked.take() {
Some(event) => event,
None => self.pull()?,
};
let range = &self.pulled_range;
let wanted = match event {
Event::End(_) => {
let owed = self.leaf.as_ref().and_then(|leaf| leaf.break_end);
if let Some(break_end) = owed.filter(|&b| b < range.end) {
self.record_closing_line(break_end);
}
false
}
_ => self
.leaf
.as_mut()
.is_some_and(|leaf| leaf.wants(&event, range)),
};
if wanted {
self.record(&event, &self.pulled_range.clone());
}
Some(event)
}
pub(super) fn into_parts(mut self) -> (Vec<Range<usize>>, Vec<usize>) {
while self.next().is_some() {}
(self.ranges.into_ranges(), self.lines.starts)
}
pub(super) fn last_range(&self) -> Range<usize> {
debug_assert!(self.peeked.is_none(), "last_range after a peek");
self.pulled_range.clone()
}
pub(super) fn last_text(&self) -> &'s str {
self.lines.src.get(self.last_range()).unwrap_or("")
}
pub(super) fn open_container(&mut self) -> ContainerStart {
let range = self.last_range();
ContainerStart {
line: self.lines.line_at(range.start),
range,
}
}
pub(super) fn set_base(&mut self) {
if self.peek().is_some() {
self.base_line = self.lines.line_at(self.pulled_range.start);
}
}
fn rel(&self, line: usize) -> u32 {
to_u32(line.saturating_sub(self.base_line))
}
pub(super) fn begin_leaf(&mut self, start: Option<Range<usize>>, mode: LeafMode) {
let (first, last) = match &start {
Some(range) => {
let first = self.lines.line_at(range.start);
(Some(first), self.lines.last_line(first, range))
}
None => (None, 0),
};
let mut cols = std::mem::take(&mut self.spare_cols);
cols.clear();
self.leaf = Some(LeafRec {
mode,
line_pending: true,
break_end: None,
first,
last,
cols,
atomic: None,
});
}
pub(super) fn end_leaf(&mut self) -> SrcLines {
let Some(mut leaf) = self.leaf.take() else {
return SrcLines::default();
};
let src = match leaf.first {
Some(first) => {
leaf.cols.resize(leaf.last.max(first) - first + 1, None);
SrcLines::new(self.rel(first), &leaf.cols)
}
None => SrcLines::default(),
};
self.spare_cols = leaf.cols;
src
}
pub(super) fn with_formula_cols(&self, src: &SrcLines, body: &str) -> SrcLines {
let first = self.base_line + src.first as usize;
super::post_pass::with_formula_cols(src, Some(body), |k| self.lines.line_text(first + k))
}
#[inline(never)]
fn record(&mut self, event: &Event<'s>, range: &Range<usize>) {
let line = self.lines.line_at(range.start);
let end_line = self.lines.end_line(line, range);
let needs_col = self
.leaf
.as_ref()
.is_some_and(|leaf| !leaf.continues_atomic(line) && !leaf.has_col(line));
let col = match event {
_ if !needs_col => None,
Event::Text(t) if range.is_empty() && !t.is_empty() => {
let before = &self.lines.src.as_bytes()[..range.start];
let back = if before.last() == Some(&b'\t') {
1
} else {
before
.iter()
.rev()
.take(t.len())
.take_while(|&&b| b == b' ')
.count()
};
Some(self.lines.col_of(line, range.start - back))
}
_ => Some(self.lines.col_of(line, range.start)),
};
let Some(leaf) = self.leaf.as_mut() else {
return;
};
leaf.last = leaf.last.max(end_line);
if let Some(col) = col {
leaf.note(line, col);
}
match event {
Event::Text(_) | Event::Html(_) => {
for l in line + 1..=end_line {
if !leaf.continues_atomic(l) {
leaf.note(l, 0);
}
}
}
Event::Code(_)
| Event::InlineMath(_)
| Event::DisplayMath(_)
| Event::InlineHtml(_)
| Event::Start(pulldown_cmark::Tag::Image { .. })
if end_line > line =>
{
leaf.atomic = Some(match leaf.atomic {
Some((start, until)) if line >= start && line <= until => {
(start, until.max(end_line))
}
_ => (line, end_line),
});
}
_ => {}
}
}
#[inline(never)]
fn record_closing_line(&mut self, line_start: usize) {
let line = self.lines.line_at(line_start);
let Some(leaf) = self.leaf.as_mut() else {
return;
};
leaf.take_pending();
if leaf.continues_atomic(line) || leaf.has_col(line) {
return;
}
let col = self
.lines
.line_text(line)
.chars()
.take_while(|c| matches!(c, ' ' | '\t' | '>'))
.count();
leaf.last = leaf.last.max(line);
leaf.note(line, col);
}
pub(super) fn is_bare_line(&self, line: u32) -> bool {
self.lines
.line_text(self.base_line + line as usize)
.chars()
.all(|c| matches!(c, '>' | ' ' | '\t' | '\r'))
}
pub(super) fn container_span(
&self,
start: &ContainerStart,
children_end: Option<u32>,
count_range_end: bool,
) -> LineSpan {
let first = start.line;
let rel_first = self.rel(first);
let mut end = rel_first.saturating_add(1);
if let Some(children_end) = children_end {
end = end.max(children_end);
}
if count_range_end {
let mut last = self.lines.last_line(first, &start.range);
if self.lines.ends_mid_line(&start.range) && last > first {
last -= 1;
}
end = end.max(self.rel(last).saturating_add(1));
}
rel_first..end
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_index_answers_lines_and_char_columns() {
let src = "ab\nçd\n\nx";
let idx = LineIndex::new(src);
assert_eq!(idx.line_of(0), 0);
assert_eq!(idx.line_of(2), 0); assert_eq!(idx.line_of(3), 1);
assert_eq!(idx.line_of(7), 2);
assert_eq!(idx.line_of(8), 3);
assert_eq!(idx.line_of(99), 3);
assert_eq!(idx.col_of(1, 5), 1);
assert_eq!(idx.col_of(1, 6), 2);
assert_eq!(idx.col_of(1, 3), 0);
let idx = LineIndex::new("a\r\n\r\nb");
assert_eq!(idx.col_of(1, 4), 0);
assert_eq!(idx.col_of(0, 2), 1);
}
#[test]
fn end_line_counts_the_newlines_before_the_last_byte() {
let idx = LineIndex::new("é\nb\n\nç");
assert_eq!(idx.end_line(0, &(0..2)), 0); assert_eq!(idx.end_line(0, &(0..3)), 0); assert_eq!(idx.end_line(0, &(0..6)), 2);
assert_eq!(idx.end_line(3, &(6..8)), 3); }
#[test]
fn last_line_ignores_trailing_newlines() {
let idx = LineIndex::new("a\nb\n\n\nc\n");
assert_eq!(idx.last_line(0, &(0..5)), 1);
assert_eq!(idx.last_line(1, &(2..2)), 1);
}
}