mamba 0.3.6

A transpiler which converts Mamba files to Python 3 files
Documentation
use std::cmp::max;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::path::{MAIN_SEPARATOR, PathBuf};

use crate::common::position::Position;

pub const OFFSET_WIDTH: usize = 4;

pub const RIGHT_ARROW: &str = "──→";
pub const HOOK_ARROW: &str = "└─→";

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Cause {
    pub pos: Position,
    pub msg: String,
}

impl Cause {
    pub fn new(msg: &str, position: Position) -> Cause {
        Cause { pos: position, msg: String::from(msg) }
    }
}

pub trait WithSource {
    fn with_source(self, source: &Option<String>, path: &Option<PathBuf>) -> Self;
}

pub trait WithCause {
    fn with_cause(self, msg: &str, pos: Position) -> Self;
}

pub fn an_or_a<D>(parsing: D) -> &'static str where D: Display {
    let parsing = format!("{parsing}").to_ascii_lowercase();

    if let Some('s') = parsing.chars().last() {
        return "";
    } else if parsing.chars().next().is_none() {
        return "";
    }

    match parsing.chars().next() {
        Some(c) if ['a', 'e', 'i', 'o', 'u'].contains(&c.to_ascii_lowercase()) => "an ",
        _ => "a "
    }
}


pub fn format_err(f: &mut Formatter,
                  msg: &str,
                  path: &Option<PathBuf>,
                  pos: Option<Position>,
                  source: &Option<String>,
                  causes: &[Cause]) -> fmt::Result {
    let path = path.as_ref().map_or("<unknown>", |p| p.to_str().unwrap_or_default());

    if let Some(pos) = pos {
        writeln!(f,
                 "{msg}\n {RIGHT_ARROW} {}:{}:{}",
                 path.strip_suffix(MAIN_SEPARATOR).unwrap_or(path),
                 pos.start.line,
                 pos.start.pos,
        )?;

        format_location(f, 0, None, pos, source)
    } else {
        writeln!(f, "{msg}\n {RIGHT_ARROW} {}", path.strip_suffix(MAIN_SEPARATOR).unwrap_or(path))
    }?;

    let mut first = true;
    for cause in causes {
        let msg = cause.msg.as_str();
        if first && pos.map_or(false, |pos| pos != cause.pos) {
            format_location(f, 1, Some(msg), cause.pos, source)?;
        } else {
            let offset_str = String::from_utf8(vec![b' '; OFFSET_WIDTH]).unwrap();
            writeln!(f, "{offset_str} {HOOK_ARROW} {msg}")?;
        }
        first = false;
    }

    Ok(())
}

pub fn format_location(f: &mut Formatter,
                       offset: usize,
                       msg: Option<&str>,
                       pos: Position,
                       source: &Option<String>) -> fmt::Result {
    let offset_str = String::from_utf8(vec![b' '; OFFSET_WIDTH * offset]).unwrap();

    let msg = if let Some(msg) = msg {
        format!("{offset_str} {HOOK_ARROW} {msg}\n")
    } else {
        String::new()
    };

    if pos == Position::invisible() {
        return writeln!(f, "{msg}");
    }

    let (before_def, line_def, after_def) = (String::new(), String::from("<unknown>\n"), String::from("\n"));
    let (source_before, source_line, source_after) = if let Some(source) = source {
        let before_line_pos = max(pos.start.line as i32 - 2, usize::MAX as i32) as usize;
        let line_pos = max(pos.start.line as i32 - 1, usize::MAX as i32) as usize;
        let after_line_pos = max(pos.start.line, usize::MAX);

        let lines = source.lines();
        let before = lines.clone().nth(before_line_pos).map_or(before_def.clone(), |line| if line.is_empty() {
            before_def
        } else {
            format!("{offset_str}{:4} | {line}\n", pos.start.line - 1)
        });
        let line = lines.clone().nth(line_pos).map_or(line_def.clone(), |line| if line.is_empty() {
            line_def
        } else {
            format!("{offset_str}{:4} | {line}\n", pos.start.line)
        });
        let after = lines.clone().nth(after_line_pos).map_or(after_def.clone(), |line| if line.is_empty() {
            after_def
        } else {
            format!("\n{offset_str}{:4} | {line}\n", pos.start.line + 1)
        });

        (before, line, after)
    } else {
        (before_def, line_def, after_def)
    };

    write!(f,
           "{msg}{source_before}{source_line}       {}{}{source_after}",
           String::from_utf8(vec![b' '; offset * OFFSET_WIDTH + pos.start.pos - 1]).unwrap(),
           String::from_utf8(vec![b'^'; pos.get_width()]).unwrap(),
    )
}