marser 0.1.0

Parser combinator toolkit with matcher-level backtracking and rich error reporting.
//! AI assistance: this file was written with AI assistance. The maintainer reviewed it and did not find errors.
//!
//! Pretty error rendering via [annotate-snippets](https://docs.rs/annotate-snippets).
//! Compiled only with the `annotate-snippets` feature.

use annotate_snippets::{
    AnnotationKind as SnippetAnnotationKind, Level as SnippetLevel, Renderer as SnippetRenderer,
    Snippet,
};

use super::{AnnotationKind, ParserError};

#[inline]
pub(super) fn render_errors_slice_into(
    errors: &[ParserError],
    out: &mut String,
    source_id: &str,
    source_text: &str,
) {
    let mut snippet = Snippet::source(source_text).path(source_id).line_start(1);
    let mut notes = Vec::new();
    let mut help = Vec::new();

    for (idx, error) in errors.iter().enumerate() {
        let label = match error {
            ParserError::FurthestFail(ff) => ff.main_message(source_text),
            ParserError::Inline(ie) => ie.message.clone(),
        };
        match error {
            ParserError::FurthestFail(ff) => {
                let span = normalized_span(ff.span, source_text.len());
                let kind = if idx == 0 {
                    SnippetAnnotationKind::Primary
                } else {
                    SnippetAnnotationKind::Context
                };
                snippet = snippet.annotation(kind.span(span).label(label));
                for ann in &ff.annotations {
                    let s = normalized_span(ann.span, source_text.len());
                    snippet = snippet.annotation(
                        annotation_kind_to_snippet(ann.kind)
                            .span(s)
                            .label(ann.message.clone()),
                    );
                }
                notes.extend(ff.notes.iter().cloned());
                help.extend(ff.helps.iter().cloned());
            }
            ParserError::Inline(ie) => {
                let span = normalized_span(ie.reporting_span(), source_text.len());
                let kind = if idx == 0 {
                    SnippetAnnotationKind::Primary
                } else {
                    SnippetAnnotationKind::Context
                };
                snippet = snippet.annotation(kind.span(span).label(label));
                for ann in &ie.annotations {
                    let s = normalized_span(ann.span, source_text.len());
                    snippet = snippet.annotation(
                        annotation_kind_to_snippet(ann.kind)
                            .span(s)
                            .label(ann.message.clone()),
                    );
                }
                notes.extend(ie.notes.iter().cloned());
                help.extend(ie.helps.iter().cloned());
            }
        }
    }

    let mut group = SnippetLevel::ERROR
        .primary_title("Parse Errors")
        .element(snippet);

    for note in notes {
        group = group.element(SnippetLevel::NOTE.message(note));
    }
    for help_line in help {
        group = group.element(SnippetLevel::HELP.message(help_line));
    }
    let report = [group];

    let rendered = SnippetRenderer::styled().render(&report).to_string();
    out.push_str(&rendered);
    if !rendered.ends_with('\n') {
        out.push('\n');
    }
}

#[inline]
fn normalized_span(span: (usize, usize), source_len: usize) -> std::ops::Range<usize> {
    let mut start = span.0.min(source_len);
    let mut end = span.1.min(source_len);

    if end > source_len {
        end = source_len;
    }

    if start == source_len && source_len > 0 {
        start = source_len - 1;
    }

    start..end
}

#[inline]
fn annotation_kind_to_snippet(kind: AnnotationKind) -> SnippetAnnotationKind {
    match kind {
        AnnotationKind::Primary => SnippetAnnotationKind::Primary,
        AnnotationKind::Secondary | AnnotationKind::Context => SnippetAnnotationKind::Context,
    }
}