katana-markdown-engine 0.1.0

Renderer-neutral Markdown document model for the KatanA ecosystem
Documentation
use crate::{ByteRange, LineColumn, LineColumnRange, RawSnippet, SourceSpan};

#[derive(Debug, Clone)]
pub(crate) struct SourceLine {
    pub number: usize,
    pub start: usize,
    pub end: usize,
    pub text: String,
}

#[derive(Debug, Clone)]
pub(crate) struct LineIndex {
    lines: Vec<SourceLine>,
}

impl LineIndex {
    pub fn new(source: &str) -> Self {
        let mut lines = Vec::new();
        let mut offset = 0;
        for (index, segment) in source.split_inclusive('\n').enumerate() {
            let text = segment.trim_end_matches('\n').to_string();
            let end = offset + text.len();
            lines.push(SourceLine {
                number: index + 1,
                start: offset,
                end,
                text,
            });
            offset += segment.len();
        }
        if source.is_empty() {
            lines.push(SourceLine {
                number: 1,
                start: 0,
                end: 0,
                text: String::new(),
            });
        }
        Self { lines }
    }

    pub fn lines(&self) -> &[SourceLine] {
        &self.lines
    }

    pub fn source_span(&self, source: &str, start_line: usize, end_line: usize) -> SourceSpan {
        let first = &self.lines[start_line];
        let last = &self.lines[end_line - 1];
        let end = last.end;
        self.source_span_for_byte_range(source, first.start, end)
    }

    pub fn source_span_for_byte_range(
        &self,
        source: &str,
        start_offset: usize,
        end_offset: usize,
    ) -> SourceSpan {
        let start = self.line_column(start_offset);
        let end = self.line_column(end_offset);
        SourceSpan {
            byte_range: ByteRange {
                start: start_offset,
                end: end_offset,
            },
            line_column_range: LineColumnRange { start, end },
            raw: RawSnippet::new(source[start_offset..end_offset].to_string()),
        }
    }

    fn line_column(&self, offset: usize) -> LineColumn {
        let line = self
            .lines
            .iter()
            .find(|line| line.start <= offset && offset <= line.end)
            .unwrap_or_else(|| self.lines.last().expect("line index must not be empty"));
        LineColumn {
            line: line.number,
            column: source_column(&line.text, offset - line.start),
        }
    }
}

fn source_column(line: &str, byte_offset: usize) -> usize {
    line[..byte_offset].chars().count() + 1
}