mustache2 1.0.7

Logic-less templates.
Documentation
use std::{borrow::Cow, env};

use crate::{
    render::{RenderManager, provider::SourceProvider},
    source::Span,
};

/// Error encountered while parsing a template. Compared to [`Error`], it does not have a source id yet.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct InternalError {
    pub(crate) message: Cow<'static, str>,
    pub(crate) source: Option<Span>,
}

impl InternalError {
    pub(crate) fn new(message: impl Into<Cow<'static, str>>, source: impl Into<Span>) -> Self {
        Self {
            message: message.into(),
            source: Some(source.into()),
        }
    }

    pub(crate) fn with_source_id(self, id: usize) -> Error {
        Error {
            message: self.message,
            source: self.source.map(|sp| (id, sp)),
        }
    }
}

/// Error encountered while parsing a template.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Error {
    /// Human readable error message. As a rule, error messages should be all
    /// lowercase and not have a period at the end.
    pub message: Cow<'static, str>,
    /// The source of the error.
    pub source: Option<(usize, Span)>,
}

impl Error {
    pub(crate) fn new_general(message: impl Into<Cow<'static, str>>) -> Self {
        Self {
            message: message.into(),
            source: None,
        }
    }
}

const FMT_RESET: &str = "\x1b[0m";
const FMT_GUTTER: &str = "\x1b[97m";
const FMT_ITALICS: &str = "\x1b[3m";
const FMT_DIM: &str = "\x1b[2m";

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "\x1b[1;31merror:{FMT_RESET} {}", self.message)
    }
}

impl std::error::Error for Error {}

impl<P: SourceProvider> RenderManager<'_, P> {
    /// Print an error message to the standard output.
    pub fn display_error(&self, error: Error) {
        let fmt_hl = if env::var("TERM")
            .is_ok_and(|v| ["xterm-kitty", "alacritty", "foot", "footclient"].contains(&v.as_str()))
        {
            "\x1b[58;5;1;4:3m"
        } else {
            "\x1b[1m"
        };

        fn digits(x: usize) -> usize {
            x.ilog10() as usize + 1
        }

        eprintln!("\x1b[1;31merror:{FMT_RESET} {}", error.message);

        let Some((src_id, span)) = error.source else {
            return;
        };
        let (key, source) = &self.sources[src_id];
        let span = Span {
            start: span.start.min(source.len()),
            end: span.end.min(source.len() + 1),
        };

        let mut lines = Vec::new();
        let mut ch = 0;
        for line in source.lines() {
            lines.push(Span {
                start: ch,
                end: ch + line.len(),
            });
            ch += line.len() + 1;
        }
        let index_to_line_span = |idx: usize| -> (usize, usize, Span) {
            for (ln, span @ Span { start, end }) in lines.iter().enumerate() {
                if idx <= *end {
                    return (ln + 1, idx - start + 1, *span);
                }
            }
            return lines.last().map_or_else(
                || (lines.len() + 1, 1, Span::char(source.len() - 1)),
                |span @ Span { start, end: _ }| (lines.len(), idx - start + 1, *span),
            );
        };

        let (ln_start, ch_start, first_line) = index_to_line_span(span.start);
        let (ln_end, _, last_line) = index_to_line_span(span.end);
        let ln_digits = digits(ln_end);
        let pad_start = " ".repeat(ln_digits);

        eprintln!(
            " {}{FMT_GUTTER}--> {FMT_ITALICS}{}:{ln_start}:{ch_start}{FMT_RESET}",
            pad_start,
            P::display_key(key),
        );
        eprintln!("{FMT_GUTTER} {} |", pad_start);

        if (span.end >= source.len()
            || (span.start == first_line.end && span.end == last_line.start))
            && (ln_start + 1 == ln_end || ln_start == ln_end)
        {
            eprintln!(
                " {}{ln_start} | {FMT_RESET}{}{fmt_hl} {FMT_RESET}",
                &pad_start[0..ln_digits - digits(ln_start)],
                &source[first_line.start..first_line.end]
            );
        } else if ln_start != ln_end {
            eprintln!(
                " {}{ln_start} | {FMT_RESET}{FMT_DIM}{}{FMT_RESET}{fmt_hl}{}{FMT_RESET}",
                &pad_start[0..ln_digits - digits(ln_start)],
                &source[first_line.start..span.start],
                &source[span.start..first_line.end],
            );
            for ln in ln_start + 1..ln_end {
                let Span { start, end } = lines[ln - 1];
                eprintln!(
                    " {}{FMT_GUTTER}{ln} | {FMT_RESET}{fmt_hl}{}{FMT_RESET}",
                    &pad_start[0..ln_digits - digits(ln)],
                    &source[start..end],
                );
            }
            eprintln!(
                " {FMT_GUTTER}{ln_end} | {FMT_RESET}{fmt_hl}{}{FMT_RESET}{FMT_DIM}{}{FMT_RESET}",
                &source[last_line.start..span.end],
                &source[span.end..last_line.end],
            );
        } else {
            eprintln!(
                " {ln_end} | {FMT_RESET}{FMT_DIM}{}{FMT_RESET}{fmt_hl}{}{FMT_RESET}{FMT_DIM}{}{FMT_RESET}",
                &source[first_line.start..span.start],
                &source[span.start..span.end],
                &source[span.end.min(source.len())..first_line.end],
            );
        }

        eprintln!(" {}{FMT_GUTTER} |{FMT_RESET}", pad_start);
    }
}