mant-query 0.11.0

Bounded semantic queries over existing ManT document snapshots
Documentation
//! Map canonical Markdown bytes to visible text and presentation coordinates.
use mant_codec::markdown_mapping::{InlineMappingKind, map_inline_characters};
use mant_protocol::SearchScope;
use pulldown_cmark::{Event, Parser, TagEnd};
use std::ops::Range;

#[cfg(test)]
pub(super) fn display_markdown_line(line: &str) -> String {
    LineIndex::new(line).presented_line(line, 0).text
}

pub(super) struct AnchorStrippedLine {
    pub(super) text: String,
    segments: Vec<OffsetSegment>,
}

impl AnchorStrippedLine {
    pub(super) fn new(line: &str, hidden: impl Iterator<Item = Range<usize>>) -> Self {
        let mut text = String::with_capacity(line.len());
        let mut segments = Vec::new();
        let mut cursor = 0;
        for range in hidden {
            push_retained_line_segment(
                line,
                cursor..range.start.min(line.len()),
                &mut text,
                &mut segments,
            );
            cursor = range.end.min(line.len());
        }
        push_retained_line_segment(line, cursor..line.len(), &mut text, &mut segments);
        Self { text, segments }
    }

    pub(super) fn map_range(&self, source: Range<usize>) -> Vec<Range<usize>> {
        self.segments
            .iter()
            .filter_map(|segment| {
                let start = source.start.max(segment.markdown.start);
                let end = source.end.min(segment.markdown.end);
                (start < end).then(|| {
                    segment.visible.start + start.saturating_sub(segment.markdown.start)
                        ..segment.visible.start + end.saturating_sub(segment.markdown.start)
                })
            })
            .collect()
    }
}

fn push_retained_line_segment(
    line: &str,
    source: Range<usize>,
    text: &mut String,
    segments: &mut Vec<OffsetSegment>,
) {
    if source.is_empty() {
        return;
    }
    let visible_start = text.len();
    text.push_str(&line[source.clone()]);
    segments.push(OffsetSegment {
        visible: visible_start..text.len(),
        markdown: source,
        linear: true,
    });
}

pub(super) struct TextPosition {
    pub(super) line_index: usize,
    pub(super) column: usize,
}

pub(super) struct LineIndex {
    starts: Vec<usize>,
    anchors: Vec<Range<usize>>,
}

impl LineIndex {
    #[cfg(test)]
    pub(super) fn new(text: &str) -> Self {
        Self::with_anchors(text, Vec::new())
    }

    pub(super) fn with_anchors(text: &str, anchors: Vec<Range<usize>>) -> Self {
        let mut starts = vec![0];
        starts.extend(
            text.bytes()
                .enumerate()
                .filter_map(|(index, byte)| (byte == b'\n').then_some(index + 1)),
        );
        Self { starts, anchors }
    }

    pub(super) fn presented_line(&self, text: &str, index: usize) -> AnchorStrippedLine {
        let start = self.start(index);
        let line = self.line(text, index).trim_end();
        let first = self.anchors.partition_point(|range| range.end <= start);
        let hidden = self.anchors[first..]
            .iter()
            .take_while(|range| range.start < start + line.len())
            .map(|range| range.start.saturating_sub(start)..range.end.saturating_sub(start));
        AnchorStrippedLine::new(line, hidden)
    }

    pub(super) fn count(&self) -> usize {
        self.starts.len()
    }

    pub(super) fn position(&self, text: &str, offset: usize) -> TextPosition {
        let offset = offset.min(text.len());
        let line_index = self.starts.partition_point(|start| *start <= offset) - 1;
        let line_start = self.starts[line_index];
        TextPosition {
            line_index,
            column: text[line_start..offset].chars().count().saturating_add(1),
        }
    }

    pub(super) fn line_index_at_byte(&self, offset: usize) -> usize {
        self.starts.partition_point(|start| *start <= offset) - 1
    }

    pub(super) fn line<'a>(&self, text: &'a str, line_index: usize) -> &'a str {
        let start = self.starts[line_index];
        let end = self
            .starts
            .get(line_index + 1)
            .copied()
            .unwrap_or(text.len());
        text[start..end]
            .strip_suffix('\n')
            .unwrap_or(&text[start..end])
    }

    pub(super) fn start(&self, line_index: usize) -> usize {
        self.starts[line_index]
    }
}

pub(super) struct SearchableText {
    pub(super) text: String,
    segments: Vec<OffsetSegment>,
    pub(super) direct_markdown: bool,
}

#[derive(Debug)]
struct OffsetSegment {
    visible: Range<usize>,
    markdown: Range<usize>,
    linear: bool,
}

impl SearchableText {
    pub(super) fn new(markdown: &str, scope: SearchScope) -> Self {
        if scope == SearchScope::Markdown {
            return Self {
                text: markdown.to_owned(),
                segments: Vec::new(),
                direct_markdown: true,
            };
        }

        let mut visible = VisibleBuilder::new(markdown);
        for (event, source) in Parser::new(markdown).into_offset_iter() {
            match event {
                Event::Text(value) | Event::InlineMath(value) | Event::DisplayMath(value) => {
                    visible.push_mapped(&value, source, InlineMappingKind::Text);
                }
                Event::Code(value) => {
                    visible.push_mapped(&value, source, InlineMappingKind::Code);
                }
                Event::SoftBreak | Event::HardBreak | Event::Rule => visible.push_break(source),
                Event::End(
                    TagEnd::Paragraph
                    | TagEnd::Heading(_)
                    | TagEnd::Item
                    | TagEnd::CodeBlock
                    | TagEnd::TableRow,
                ) => visible.push_break(source.end..source.end),
                Event::Start(_)
                | Event::End(_)
                | Event::Html(_)
                | Event::InlineHtml(_)
                | Event::FootnoteReference(_)
                | Event::TaskListMarker(_) => {}
            }
        }
        visible.finish()
    }

    pub(super) fn markdown_start(&self, offset: usize) -> usize {
        if self.direct_markdown {
            return offset;
        }
        self.segment_at(offset).map_or(0, |segment| {
            if segment.linear {
                segment.markdown.start + offset.saturating_sub(segment.visible.start)
            } else {
                segment.markdown.start
            }
        })
    }

    pub(super) fn markdown_end(&self, offset: usize) -> usize {
        if self.direct_markdown {
            return offset;
        }
        if offset == 0 {
            return 0;
        }
        self.segment_at(offset - 1).map_or(0, |segment| {
            if segment.linear {
                segment.markdown.start + offset.saturating_sub(segment.visible.start)
            } else {
                segment.markdown.end
            }
        })
    }

    fn segment_at(&self, offset: usize) -> Option<&OffsetSegment> {
        let index = self
            .segments
            .partition_point(|segment| segment.visible.end <= offset);
        self.segments
            .get(index)
            .filter(|segment| segment.visible.contains(&offset))
    }
}

struct VisibleBuilder<'a> {
    markdown: &'a str,
    text: String,
    segments: Vec<OffsetSegment>,
}

impl<'a> VisibleBuilder<'a> {
    pub(super) fn new(markdown: &'a str) -> Self {
        Self {
            markdown,
            text: String::new(),
            segments: Vec::new(),
        }
    }

    pub(super) fn push_mapped(
        &mut self,
        value: &str,
        source: Range<usize>,
        kind: InlineMappingKind,
    ) {
        for mapped in map_inline_characters(self.markdown, value, source, kind) {
            let visible_start = self.text.len();
            self.text.push(mapped.value);
            let visible_end = self.text.len();
            self.push_segment(OffsetSegment {
                visible: visible_start..visible_end,
                markdown: mapped.source,
                linear: mapped.linear,
            });
        }
    }

    pub(super) fn push_break(&mut self, markdown: Range<usize>) {
        if self.text.ends_with('\n') || self.text.is_empty() {
            return;
        }
        let start = self.text.len();
        self.text.push('\n');
        self.push_segment(OffsetSegment {
            visible: start..self.text.len(),
            markdown,
            linear: false,
        });
    }

    pub(super) fn push_segment(&mut self, segment: OffsetSegment) {
        if let Some(previous) = self.segments.last_mut() {
            let contiguous = previous.visible.end == segment.visible.start
                && previous.markdown.end == segment.markdown.start
                && previous.linear
                && segment.linear;
            if contiguous {
                previous.visible.end = segment.visible.end;
                previous.markdown.end = segment.markdown.end;
                return;
            }
        }
        self.segments.push(segment);
    }

    pub(super) fn finish(self) -> SearchableText {
        SearchableText {
            text: self.text,
            segments: self.segments,
            direct_markdown: false,
        }
    }
}