oxvg_diagnostics/
lib.rs

1use std::str::Utf8Error;
2
3use miette::{Diagnostic, NamedSource, Report, Result, SourceSpan};
4use quick_xml::{escape::EscapeError, events::attributes::AttrError};
5use thiserror::Error;
6
7#[derive(Debug, Error, Diagnostic)]
8#[error("Error parsing SVG!")]
9#[diagnostic()]
10pub struct SVGErrors {
11    #[source_code]
12    src: NamedSource<String>,
13    #[related]
14    errors: Vec<SVGError>,
15}
16
17#[derive(Debug)]
18pub enum ElementParseError {
19    Attribute(AttributeParseError),
20    Name(Utf8Error),
21}
22
23#[derive(Debug)]
24pub enum AttributeParseError {
25    AttrError(AttrError),
26    Utf8Error(Utf8Error),
27}
28
29impl SVGErrors {
30    /// Creates a new `SVGErrors` object with the given source code and errors
31    pub fn from_errors(src: NamedSource<String>, errors: Vec<SVGError>) -> Self {
32        Self { src, errors }
33    }
34
35    /// Returns a miette `Result` with an error, if any errors are present
36    ///
37    /// # Errors
38    /// If there are any contained errors
39    pub fn emit(self) -> Result<()> {
40        if self.errors.is_empty() {
41            return Ok(());
42        }
43        if self.errors.len() == 1 {
44            match self.errors.first() {
45                Some(e) => e.clone().emit(self.src),
46                None => Ok(()),
47            }
48        } else {
49            Err(self.into())
50        }
51    }
52}
53
54#[derive(Debug, PartialEq, Clone, Diagnostic, Error)]
55#[error("{label}")]
56#[diagnostic()]
57pub struct SVGError {
58    label: String,
59    #[label]
60    span: Option<SourceSpan>,
61    #[help]
62    advice: Option<String>,
63    #[label("Caused by this")]
64    cause: Option<SourceSpan>,
65}
66
67impl SVGError {
68    /// Creates a new `SVGError` with an associated label and span
69    pub fn new(label: &str, span: Option<SourceSpan>) -> Self {
70        SVGError {
71            label: label.into(),
72            span,
73            advice: None,
74            cause: None,
75        }
76    }
77
78    /// Creates a new `SVGError` from the existing, with help text
79    pub fn with_advice(self, advice: &str) -> Self {
80        Self {
81            advice: Some(advice.into()),
82            ..self
83        }
84    }
85
86    /// Creates a new `SVGError` from the existing, with a related cause
87    pub fn with_cause(self, cause: SourceSpan) -> Self {
88        Self {
89            cause: Some(cause),
90            ..self
91        }
92    }
93
94    /// Returns a miette `Result` with an error
95    ///
96    /// # Errors
97    /// always returns error
98    pub fn emit(self, src: NamedSource<String>) -> Result<()> {
99        let report: Report = self.into();
100        Err(report.with_source_code(src))
101    }
102}
103
104impl From<(quick_xml::Error, usize)> for SVGError {
105    /// Convert from a pair of quick-xml error and the position it occured
106    fn from(value: (quick_xml::Error, usize)) -> Self {
107        use quick_xml::Error::{
108            EmptyDocType, EndEventMismatch, EscapeError, InvalidAttr, InvalidPrefixBind, Io,
109            NonDecodable, TextNotFound, UnexpectedBang, UnexpectedEof, UnexpectedToken,
110            UnknownPrefix, XmlDeclWithoutVersion,
111        };
112
113        let (error, position) = value;
114        dbg!(&error, &position);
115        let position: SourceSpan = position.into();
116        match error {
117            EmptyDocType => SVGError::new("The doctype has no content", Some(position)),
118            EndEventMismatch { expected, found } => SVGError::new(
119                &format!("Expected to find closing tag for {expected}, but found {found} instead"),
120                Some(position),
121            ),
122            EscapeError(error) => ( error, value.1 ).into(),
123            InvalidAttr(error) => ( error, value.1 ).into(),
124            InvalidPrefixBind { prefix, namespace } => {
125                let prefix = String::from_utf8_lossy(&prefix);
126                let namespace = String::from_utf8_lossy(&namespace);
127                SVGError::new(&format!(r#"Cannot bind "{namespace}" to "{prefix}""#), Some(position))
128            },
129            Io(error) => SVGError::new(&error.kind().to_string(), None),
130            NonDecodable(error) => match error {
131                Some(error) => SVGError::new(&error.to_string(), Some(position)),
132                None => SVGError::new("Couldn't decode file format for some reason.", None),
133            },
134            TextNotFound => SVGError::new(
135                "Expected text here but found something else",
136                Some(position),
137            ),
138            UnexpectedBang(_) => SVGError::new(r#"Unexpected "!" here"#, Some(position)),
139            UnexpectedEof(error) | UnexpectedToken(error) => SVGError::new(&error, Some(position)),
140            UnknownPrefix(error) => SVGError::new(&format!(r#"The namespace prefix "{}" is unknown"#, String::from_utf8_lossy(&error)), Some(position)),
141            XmlDeclWithoutVersion(error) => {
142                match error {
143                    Some(attribute) => SVGError::new(&format!("Expected xml declaration to start with a `version` attribute, but found {attribute} instead"), Some(position)),
144                    None => SVGError::new("Expected xml declaration to start with a `version attribute`", Some(position))
145                }
146            }
147        }
148    }
149}
150
151impl From<(AttrError, usize)> for SVGError {
152    fn from(value: (AttrError, usize)) -> Self {
153        use AttrError::{Duplicated, ExpectedEq, ExpectedQuote, ExpectedValue, UnquotedValue};
154
155        let (error, error_position) = value;
156        match error {
157            Duplicated(position, other_position) => SVGError::new(
158                "Found duplicate attributes",
159                Some((error_position..position).into()),
160            )
161            .with_cause(other_position.into()),
162            ExpectedEq(position) => {
163                dbg!(&error, &error_position);
164                SVGError::new("Expected an `=` for this attribute", Some(position.into()))
165            }
166            ExpectedQuote(position, char) => {
167                let char = &[char];
168                let char = String::from_utf8_lossy(char);
169                SVGError::new(
170                    &format!(r#"Expected a quote (`'` or `"`), but found {char} instead"#),
171                    Some(position.into()),
172                )
173            }
174            ExpectedValue(position) => {
175                if error_position == position {
176                    SVGError::new("Expected a value after `=`", Some(position.into()))
177                } else {
178                    SVGError::new(
179                        "Expected a value directly after `=`, but found something else instead",
180                        Some((error_position..position).into()),
181                    )
182                }
183            }
184            UnquotedValue(position) => SVGError::new(
185                "Expected quotes around value",
186                Some((error_position..position).into()),
187            ),
188        }
189    }
190}
191
192impl From<(EscapeError, usize)> for SVGError {
193    fn from(value: (EscapeError, usize)) -> Self {
194        use EscapeError::{
195            EntityWithNull, InvalidCodepoint, InvalidDecimal, InvalidHexadecimal, TooLongDecimal,
196            TooLongHexadecimal, UnrecognizedSymbol, UnterminatedEntity,
197        };
198
199        let (error, position) = value;
200        match error {
201            EntityWithNull(range) => {
202                SVGError::new("Entity is a null character", Some(range.into()))
203            }
204            InvalidCodepoint(point) => SVGError::new(
205                &format!("{point} is not a valid unicode codepoint"),
206                Some(position.into()),
207            ),
208            InvalidDecimal(char) => SVGError::new(
209                &format!("Found invalid decimal character `{char}`"),
210                Some(position.into()),
211            ),
212            InvalidHexadecimal(char) => SVGError::new(
213                &format!("Found invalid hex character `{char}`"),
214                Some(position.into()),
215            ),
216            TooLongDecimal => SVGError::new("Decimal entity is too long", Some(position.into())),
217            TooLongHexadecimal => SVGError::new("Hex entity is too long", Some(position.into())),
218            UnrecognizedSymbol(range, symbol) => SVGError::new(
219                &format!("{symbol} is not a recognised symbol"),
220                Some(range.into()),
221            ),
222            UnterminatedEntity(range) => {
223                SVGError::new("Cannot find `;` after `&`", Some(range.into()))
224            }
225        }
226    }
227}
228
229impl From<(ElementParseError, usize)> for SVGError {
230    fn from(value: (ElementParseError, usize)) -> Self {
231        let (error, position) = value;
232        match error {
233            ElementParseError::Name(error)
234            | ElementParseError::Attribute(AttributeParseError::Utf8Error(error)) => {
235                SVGError::new(&error.to_string(), Some(position.into()))
236            }
237            ElementParseError::Attribute(AttributeParseError::AttrError(error)) => {
238                (error, position).into()
239            }
240        }
241    }
242}