mos-parse 0.0.1

Parser for the Mosaic source language (manifest §3, §6 stages 1–2).
Documentation
use crate::Item;
use crate::parser::Parser;
use crate::support::{strip_leading_label, strip_trailing_label};

impl Parser<'_> {
    pub(crate) fn parse_heading(&mut self) {
        let (line_start, content_end, line_end) = self.current_line_bounds();
        let bytes = self.src.as_bytes();
        let mut level: u8 = 0;
        let mut i = line_start;
        while i < content_end && bytes[i] == b'=' {
            level = level.saturating_add(1);
            i += 1;
        }
        if i >= content_end || !bytes[i].is_ascii_whitespace() {
            self.parse_paragraph();
            return;
        }
        while i < content_end && (bytes[i] == b' ' || bytes[i] == b'\t') {
            i += 1;
        }
        let (text_end, parsed_label) = strip_trailing_label(self.src, i, content_end);
        let label_span = parsed_label
            .as_ref()
            .map(|label| self.span(label.start, label.end));
        let label = parsed_label.map(|label| label.text);
        let content = &self.src[i..text_end];
        let inlines = self.parse_inlines(content, i);
        let span = self.span(line_start, content_end);
        self.items.push(Item::Heading {
            level,
            inlines,
            label,
            label_span,
            span,
        });
        self.pos = line_end;
    }

    pub(crate) fn parse_paragraph(&mut self) {
        let bytes = self.src.as_bytes();
        let para_start = self.pos;
        let mut para_end = self.pos;
        let mut text_start: Option<usize> = None;
        loop {
            if self.pos >= bytes.len() || self.at_blank_line() {
                break;
            }
            if self.starts_with("=") && self.heading_level_of_current_line().is_some() {
                break;
            }
            if self.at_directive_keyword().is_some() || self.at_list_marker() {
                break;
            }
            let (line_start, content_end, line_end) = self.current_line_bounds();
            if text_start.is_none() {
                text_start = Some(line_start);
            }
            para_end = content_end;
            self.pos = line_end;
        }
        if let Some(start) = text_start {
            let (body_start, parsed_label) = strip_leading_label(self.src, start, para_end);
            let label_span = parsed_label
                .as_ref()
                .map(|label| self.span(label.start, label.end));
            let label = parsed_label.map(|label| label.text);
            let slice = &self.src[body_start..para_end];
            let mut inlines = self.parse_inlines(slice, body_start);
            for inline in &mut inlines {
                if inline.text.contains("\r\n") {
                    inline.text = inline.text.replace("\r\n", "\n");
                }
            }
            let span = self.span(para_start, para_end);
            self.items.push(Item::Paragraph {
                inlines,
                label,
                label_span,
                span,
            });
        }
    }

    /// Returns `Some(level)` if the current line is a well-formed
    /// heading of `=`+ followed by ASCII whitespace.
    fn heading_level_of_current_line(&self) -> Option<u8> {
        let (start, content_end, _) = self.current_line_bounds();
        let bytes = self.src.as_bytes();
        let mut i = start;
        let mut level: u8 = 0;
        while i < content_end && bytes[i] == b'=' {
            level = level.saturating_add(1);
            i += 1;
        }
        if level == 0 {
            return None;
        }
        if i < content_end && bytes[i].is_ascii_whitespace() {
            Some(level)
        } else {
            None
        }
    }
}