use std::cmp::max;
use crate::fmt;
use crate::types::span::Span;
pub struct Error {
kind: ErrorKind,
name: Option<String>,
reason: Option<String>,
pretty: Option<Pretty>,
}
#[derive(Debug)]
enum ErrorKind {
Syntax,
#[cfg(feature = "serde")]
Serialize,
Render,
#[cfg(feature = "filters")]
Filter,
Format,
Io(std::io::Error),
}
impl Error {
pub(crate) fn syntax(reason: impl Into<String>, source: &str, span: impl Into<Span>) -> Self {
Self {
kind: ErrorKind::Syntax,
name: None,
reason: Some(reason.into()),
pretty: Some(Pretty::build(source, span.into())),
}
}
pub(crate) fn render(reason: impl Into<String>, source: &str, span: impl Into<Span>) -> Self {
Self {
kind: ErrorKind::Render,
name: None,
reason: Some(reason.into()),
pretty: Some(Pretty::build(source, span.into())),
}
}
#[cfg(feature = "filters")]
pub(crate) fn render_plain(reason: impl Into<String>) -> Self {
Self {
kind: ErrorKind::Render,
name: None,
reason: Some(reason.into()),
pretty: None,
}
}
pub(crate) fn max_include_depth(max: usize) -> Self {
Self {
kind: ErrorKind::Render,
name: None,
reason: Some(format!("reached maximum include depth ({max})")),
pretty: None,
}
}
pub(crate) fn with_template_name(mut self, name: String) -> Self {
self.name.get_or_insert(name);
self
}
#[cfg(feature = "filters")]
pub(crate) fn enrich(mut self, source: &str, span: impl Into<Span>) -> Self {
self.pretty
.get_or_insert_with(|| Pretty::build(source, span.into()));
self
}
#[cfg(feature = "filters")]
pub(crate) fn filter(reason: impl Into<String>) -> Self {
Self {
kind: ErrorKind::Filter,
name: None,
reason: Some(reason.into()),
pretty: None,
}
}
pub(crate) fn format(err: fmt::Error, source: &str, span: impl Into<Span>) -> Self {
Self {
kind: ErrorKind::Format,
name: None,
reason: err.message(),
pretty: Some(Pretty::build(source, span.into())),
}
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Self {
kind: ErrorKind::Io(err),
name: None,
reason: None,
pretty: None,
}
}
}
impl From<std::fmt::Error> for Error {
fn from(_: std::fmt::Error) -> Self {
Self {
kind: ErrorKind::Format,
name: None,
reason: None,
pretty: None,
}
}
}
#[cfg(feature = "serde")]
#[doc(hidden)]
impl serde::ser::Error for Error {
fn custom<T>(msg: T) -> Self
where
T: std::fmt::Display,
{
Self {
kind: ErrorKind::Serialize,
name: None,
reason: Some(msg.to_string()),
pretty: None,
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.kind {
ErrorKind::Io(err) => Some(err),
_ => None,
}
}
}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !f.alternate() {
writeln!(f, "{self:#}")?;
}
f.debug_struct("Error")
.field("kind", &self.kind)
.field("name", &self.name)
.field("reason", &self.reason)
.field("pretty", &self.pretty)
.finish()?;
Ok(())
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match &self.kind {
ErrorKind::Syntax => "invalid syntax",
ErrorKind::Render => "render error",
#[cfg(feature = "filters")]
ErrorKind::Filter => "filter error",
ErrorKind::Format => "format error",
#[cfg(feature = "serde")]
ErrorKind::Serialize => "serialize error",
ErrorKind::Io(_) => "io error",
};
match (&self.reason, &self.pretty) {
(Some(r), Some(p)) if f.alternate() => {
write!(f, "{msg}")?;
p.fmt_with_reason(f, self.name.as_deref(), r)
}
(Some(reason), _) => write!(f, "{msg}: {reason}"),
_ => write!(f, "{msg}"),
}
}
}
#[derive(Debug)]
struct Pretty {
ln: usize,
col: usize,
width: usize,
text: String,
}
impl Pretty {
fn build(source: &str, span: Span) -> Self {
let lines: Vec<_> = source.split_terminator('\n').collect();
let (ln, col) = to_ln_col(&lines, span.m);
let width = max(1, display_width(&source[span]));
let text = lines
.get(ln)
.unwrap_or_else(|| lines.last().unwrap())
.to_string();
Self {
ln,
col,
width,
text,
}
}
fn fmt_with_reason(
&self,
f: &mut std::fmt::Formatter<'_>,
name: Option<&str>,
reason: &str,
) -> std::fmt::Result {
let num = (self.ln + 1).to_string();
let col = self.col + 1;
let pad = display_width(&num);
let align = self.col + self.width;
let z = "";
let pipe = "|";
let equals = "=";
let underline = "^".repeat(self.width);
let extra = "-".repeat(3_usize.saturating_sub(self.width));
let name = name.unwrap_or("<anonymous>");
let text = &self.text;
write!(
f,
"\n\n {z:pad$}--> {name}:{num}:{col}\
\n {z:pad$} {pipe}\
\n {num:>} {pipe} {text}\
\n {z:pad$} {pipe} {underline:>align$}{extra}\
\n {z:pad$} {pipe}\
\n {z:pad$} {equals} reason: {reason}\n",
)
}
}
fn to_ln_col(lines: &[&str], offset: usize) -> (usize, usize) {
let mut n = 0;
for (i, line) in lines.iter().enumerate() {
let len = display_width(line) + 1;
if n + len > offset {
return (i, offset - n);
}
n += len;
}
(
lines.len(),
lines.last().map(|l| display_width(l)).unwrap_or(0),
)
}
#[cfg(feature = "unicode")]
fn display_width(s: &str) -> usize {
unicode_width::UnicodeWidthStr::width(s)
}
#[cfg(not(feature = "unicode"))]
fn display_width(s: &str) -> usize {
s.chars().count()
}