1use std::fmt;
10use std::path::PathBuf;
11
12use mos_core::{CoreError, Diagnostic, SourceSpan, codes};
13
14#[derive(Clone, Debug, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum CslParseErrorKind {
19 MalformedXml(String),
21 UnexpectedRoot(String),
23 ForeignNamespace(String),
25 MissingVersion,
27 MissingClass,
29 UnknownClass(String),
31 UnsupportedVersion(String),
33 MissingMacroName,
35 MissingLayout,
37 TextWithoutSource,
39 TextWithMultipleSources,
41 InvalidChooseOrder,
43 UnsupportedElement(String),
45}
46
47impl CslParseErrorKind {
48 fn describe(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match self {
50 Self::MalformedXml(message) => write!(f, "malformed XML: {message}"),
51 Self::UnexpectedRoot(name) => {
52 write!(f, "expected a <style> root element, found <{name}>")
53 }
54 Self::ForeignNamespace(namespace) => {
55 write!(
56 f,
57 "<style> is in an unsupported namespace `{namespace}` (expected the CSL namespace or none)"
58 )
59 }
60 Self::MissingVersion => {
61 f.write_str("<style> is missing the required `version` attribute")
62 }
63 Self::MissingClass => f.write_str("<style> is missing the required `class` attribute"),
64 Self::UnknownClass(class) => {
65 write!(
66 f,
67 "unknown style class `{class}` (expected `in-text` or `note`)"
68 )
69 }
70 Self::UnsupportedVersion(version) => {
71 write!(
72 f,
73 "unsupported CSL version `{version}` (expected `1.0` or a `1.0.x` release)"
74 )
75 }
76 Self::MissingMacroName => {
77 f.write_str("<macro> is missing the required `name` attribute")
78 }
79 Self::MissingLayout => {
80 f.write_str("<citation>/<bibliography> is missing the required <layout>")
81 }
82 Self::TextWithoutSource => {
83 f.write_str("<text> must select a variable, macro, term, or value")
84 }
85 Self::TextWithMultipleSources => {
86 f.write_str("<text> must select exactly one variable, macro, term, or value")
87 }
88 Self::InvalidChooseOrder => {
89 f.write_str("<choose> must contain <if>, then <else-if>, then optional <else>")
90 }
91 Self::UnsupportedElement(name) => write!(f, "unsupported CSL element <{name}>"),
92 }
93 }
94}
95
96#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct CslParseError {
114 kind: CslParseErrorKind,
115 offset: usize,
116}
117
118impl CslParseError {
119 pub(crate) const fn new(kind: CslParseErrorKind, offset: usize) -> Self {
120 Self { kind, offset }
121 }
122
123 #[must_use]
125 pub const fn kind(&self) -> &CslParseErrorKind {
126 &self.kind
127 }
128
129 #[must_use]
131 pub const fn offset(&self) -> usize {
132 self.offset
133 }
134
135 #[must_use]
140 pub fn line_col(&self, src: &str) -> (usize, usize) {
141 mos_core::linecol(src, self.offset)
142 }
143
144 #[must_use]
148 pub fn to_diagnostic(&self, file: impl Into<PathBuf>) -> Diagnostic {
149 let span = SourceSpan::new(file.into(), self.offset, self.offset);
150 Diagnostic::simple(&codes::MOS0044, Some(span), self.to_string())
151 }
152}
153
154impl fmt::Display for CslParseError {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 write!(f, "CSL parse error at byte {}: ", self.offset)?;
157 self.kind.describe(f)
158 }
159}
160
161impl std::error::Error for CslParseError {}
162
163impl From<CslParseError> for CoreError {
164 fn from(err: CslParseError) -> Self {
165 Self::Diagnostic(Box::new(Diagnostic::simple(
168 &codes::MOS0044,
169 None,
170 err.to_string(),
171 )))
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn display_covers_error_kinds() {
181 let cases = [
182 (
183 CslParseErrorKind::MalformedXml("bad token".to_owned()),
184 "CSL parse error at byte 7: malformed XML: bad token",
185 ),
186 (
187 CslParseErrorKind::UnexpectedRoot("not-style".to_owned()),
188 "CSL parse error at byte 7: expected a <style> root element, found <not-style>",
189 ),
190 (
191 CslParseErrorKind::ForeignNamespace("urn:not-csl".to_owned()),
192 "CSL parse error at byte 7: <style> is in an unsupported namespace `urn:not-csl` (expected the CSL namespace or none)",
193 ),
194 (
195 CslParseErrorKind::MissingVersion,
196 "CSL parse error at byte 7: <style> is missing the required `version` attribute",
197 ),
198 (
199 CslParseErrorKind::MissingClass,
200 "CSL parse error at byte 7: <style> is missing the required `class` attribute",
201 ),
202 (
203 CslParseErrorKind::UnknownClass("weird".to_owned()),
204 "CSL parse error at byte 7: unknown style class `weird` (expected `in-text` or `note`)",
205 ),
206 (
207 CslParseErrorKind::UnsupportedVersion("1.1".to_owned()),
208 "CSL parse error at byte 7: unsupported CSL version `1.1` (expected `1.0` or a `1.0.x` release)",
209 ),
210 (
211 CslParseErrorKind::MissingMacroName,
212 "CSL parse error at byte 7: <macro> is missing the required `name` attribute",
213 ),
214 (
215 CslParseErrorKind::MissingLayout,
216 "CSL parse error at byte 7: <citation>/<bibliography> is missing the required <layout>",
217 ),
218 (
219 CslParseErrorKind::TextWithoutSource,
220 "CSL parse error at byte 7: <text> must select a variable, macro, term, or value",
221 ),
222 (
223 CslParseErrorKind::TextWithMultipleSources,
224 "CSL parse error at byte 7: <text> must select exactly one variable, macro, term, or value",
225 ),
226 (
227 CslParseErrorKind::InvalidChooseOrder,
228 "CSL parse error at byte 7: <choose> must contain <if>, then <else-if>, then optional <else>",
229 ),
230 (
231 CslParseErrorKind::UnsupportedElement("magic".to_owned()),
232 "CSL parse error at byte 7: unsupported CSL element <magic>",
233 ),
234 ];
235
236 for (kind, expected) in cases {
237 assert_eq!(CslParseError::new(kind, 7).to_string(), expected);
238 }
239 }
240
241 #[test]
242 fn error_carries_offset_line_col_and_diagnostic() {
243 let src = "<style version=\"1.0\" class=\"in-text\">\n <citation/>\n</style>";
244 let err = crate::parse_style(src).expect_err("citation without layout");
245 assert_eq!(err.kind(), &CslParseErrorKind::MissingLayout);
246 assert_eq!(err.line_col(src), (2, 3));
247
248 let diagnostic = err.to_diagnostic("style.csl");
249 assert_eq!(diagnostic.def().code().to_string(), "MOS0044");
250 let span = diagnostic.span().expect("diagnostic should carry a span");
251 assert_eq!(span.start(), err.offset());
252 }
253
254 #[test]
255 fn from_error_yields_core_diagnostic() {
256 let err = CslParseError::new(CslParseErrorKind::MissingClass, 3);
257 let code = match CoreError::from(err) {
258 CoreError::Diagnostic(diagnostic) => diagnostic.def().code().to_string(),
259 CoreError::Unimplemented(_) => String::new(),
260 };
261 assert_eq!(code, "MOS0044");
262 }
263}