1use std::{fmt, path::PathBuf};
2
3use crate::model::{Location, Position, SectionLevel};
4
5#[non_exhaustive]
6#[derive(thiserror::Error, Debug)]
7pub enum Error {
8 #[error("Invalid include path: {1}, position: {0}")]
9 InvalidIncludePath(Box<SourceLocation>, PathBuf),
10
11 #[error("Invalid line range: {1}, position: {0}")]
12 InvalidLineRange(Box<SourceLocation>, String),
13
14 #[error("Parsing error: {1}, position: {0}")]
15 Parse(Box<SourceLocation>, String),
16
17 #[error("PEG parsing error: {1}, position {0}")]
18 PegParse(Box<SourceLocation>, String),
19
20 #[error("Parsing error: {0}")]
21 ParseGrammar(#[from] peg::error::ParseError<peg::str::LineCol>),
22
23 #[error("section level mismatch: {1} (expected '{2}'), position: {0}")]
24 NestedSectionLevelMismatch(Box<SourceLocation>, SectionLevel, SectionLevel),
25
26 #[error("mismatched delimiters: {1}, position: {0}")]
27 MismatchedDelimiters(Box<SourceLocation>, String),
28
29 #[error("Invalid admonition variant: {1}, position: {0}")]
30 InvalidAdmonitionVariant(Box<SourceLocation>, String),
31
32 #[error("Invalid conditional directive, position: {0}")]
33 InvalidConditionalDirective(Box<SourceLocation>),
34
35 #[error("Invalid include directive: {1}, position: {0}")]
36 InvalidIncludeDirective(Box<SourceLocation>, String),
37
38 #[error("Invalid indent: {1}, position: {0}")]
39 InvalidIndent(Box<SourceLocation>, String),
40
41 #[error("Invalid level offset: {1}, position: {0}")]
42 InvalidLevelOffset(Box<SourceLocation>, String),
43
44 #[error("I/O error: {0}")]
45 Io(#[from] std::io::Error),
46
47 #[error("URL error: {0}")]
48 Url(#[from] url::ParseError),
49
50 #[error("ParseInt error: {0}")]
51 ParseInt(#[from] std::num::ParseIntError),
52
53 #[error("Invalid ifeval directive, position: {0}")]
54 InvalidIfEvalDirectiveMismatchedTypes(Box<SourceLocation>),
55
56 #[error("Unknown encoding: {0}")]
57 UnknownEncoding(String),
58
59 #[error("Unrecognized encoding in file: {0}")]
60 UnrecognizedEncodingInFile(String),
61
62 #[cfg(feature = "network")]
63 #[error("Unable to retrieve HTTP response: {0}")]
64 HttpRequest(String),
65
66 #[cfg(not(feature = "network"))]
67 #[error(
68 "Network support is disabled (compile with 'network' feature to enable remote includes)"
69 )]
70 NetworkDisabled,
71
72 #[error("Could not convert from int: {0}")]
73 TryFromIntError(#[from] std::num::TryFromIntError),
74
75 #[error("Non-conforming manpage title: {1}, position: {0}")]
76 NonConformingManpageTitle(Box<SourceLocation>, String),
77}
78
79impl Error {
80 #[must_use]
82 pub(crate) fn mismatched_delimiters(detail: SourceLocation, block_type: &str) -> Self {
83 Self::MismatchedDelimiters(Box::new(detail), block_type.to_string())
84 }
85
86 #[must_use]
89 pub fn source_location(&self) -> Option<&SourceLocation> {
90 match self {
91 Self::NestedSectionLevelMismatch(detail, ..)
92 | Self::MismatchedDelimiters(detail, ..)
93 | Self::InvalidAdmonitionVariant(detail, ..)
94 | Self::Parse(detail, ..)
95 | Self::PegParse(detail, ..)
96 | Self::InvalidIncludePath(detail, ..)
97 | Self::InvalidLineRange(detail, ..)
98 | Self::InvalidConditionalDirective(detail)
99 | Self::InvalidIncludeDirective(detail, ..)
100 | Self::InvalidIndent(detail, ..)
101 | Self::InvalidLevelOffset(detail, ..)
102 | Self::InvalidIfEvalDirectiveMismatchedTypes(detail)
103 | Self::NonConformingManpageTitle(detail, ..) => Some(detail),
104 Self::ParseGrammar(_)
105 | Self::Io(_)
106 | Self::Url(_)
107 | Self::ParseInt(_)
108 | Self::UnknownEncoding(_)
109 | Self::UnrecognizedEncodingInFile(_)
110 | Self::TryFromIntError(_) => None,
111 #[cfg(feature = "network")]
112 Self::HttpRequest(_) => None,
113 #[cfg(not(feature = "network"))]
114 Self::NetworkDisabled => None,
115 }
116 }
117
118 #[must_use]
121 pub fn advice(&self) -> Option<&'static str> {
122 match self {
123 Self::NestedSectionLevelMismatch(..) => Some(
124 "Section levels must increment by at most 1. For example, level 2 (==) cannot be followed directly by level 4 (====)",
125 ),
126 Self::MismatchedDelimiters(..) => Some(
127 "Delimited blocks must use the same delimiter to open and close (e.g., '====' to open, '====' to close)",
128 ),
129 Self::InvalidAdmonitionVariant(..) => {
130 Some("Valid admonition types are: NOTE, TIP, IMPORTANT, WARNING, CAUTION")
131 }
132 Self::InvalidIfEvalDirectiveMismatchedTypes(..) => Some(
133 "ifeval expressions must compare values of the same type (both numbers or both strings)",
134 ),
135 Self::InvalidConditionalDirective(..) => Some(
136 "Valid conditional directives are: ifdef, ifndef, ifeval, endif. Check the syntax of your conditional block.",
137 ),
138 Self::InvalidLineRange(..) => Some(
139 "Line ranges must be in the format 'start..end' where start and end are positive integers",
140 ),
141 Self::InvalidIncludeDirective(..) => Some(
142 "Valid include directive attributes are: leveloffset, lines, tag, tags, indent, encoding, opts",
143 ),
144 Self::InvalidIndent(..) => Some(
145 "The indent attribute must be a non-negative integer specifying the number of spaces to indent included content",
146 ),
147 Self::InvalidLevelOffset(..) => Some(
148 "The leveloffset attribute must be a signed integer (e.g., +1, -1, 0) to adjust section levels in included content",
149 ),
150 Self::InvalidIncludePath(..) => Some(
151 "Include paths must have a valid parent directory. Check that the path is not empty or relative to a non-existent location",
152 ),
153 Self::Parse(..) => Some(
154 "Check the AsciiDoc syntax at the indicated location. Common issues: incorrect block delimiters, malformed section headings, or invalid attribute syntax",
155 ),
156 Self::PegParse(..) => Some(
157 "The parser encountered unexpected syntax. Verify that block delimiters match, section levels increment correctly, and all syntax follows AsciiDoc specification",
158 ),
159 Self::Url(..) => Some(
160 "Verify the URL syntax is correct (e.g., https://example.com/file.adoc). Check for typos in the protocol, domain, or path",
161 ),
162 #[cfg(feature = "network")]
163 Self::HttpRequest(..) => Some(
164 "Check that the URL is accessible, the server is reachable, and you have network connectivity. For includes, consider using safe mode restrictions",
165 ),
166 #[cfg(not(feature = "network"))]
167 Self::NetworkDisabled => Some(
168 "Remote includes require the 'network' feature. Rebuild with `cargo build --features network` or use local file includes instead",
169 ),
170 Self::UnknownEncoding(..) | Self::UnrecognizedEncodingInFile(..) => Some(
171 "We only support UTF-8 or UTF-16 encoded files. Ensure the specified encoding is correct and the file is saved with that encoding",
172 ),
173 Self::NonConformingManpageTitle(..) => Some(
174 "Manpage document titles must be in the format 'name(volume)', e.g., 'git-commit(1)'. Remove --strict flag to use fallback values.",
175 ),
176 Self::ParseGrammar(_) | Self::Io(_) | Self::ParseInt(_) | Self::TryFromIntError(_) => {
177 None
178 }
179 }
180 }
181}
182
183#[derive(Debug, PartialEq)]
185pub enum Positioning {
186 Location(Location),
187 Position(Position),
188}
189
190impl fmt::Display for Positioning {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 match self {
193 Positioning::Location(location) => write!(
194 f,
195 "start(line: {}, column: {}), end(line: {}, column: {})",
196 location.start.line, location.start.column, location.end.line, location.end.column
197 ),
198 Positioning::Position(position) => {
199 write!(f, "line: {}, column: {}", position.line, position.column)
200 }
201 }
202 }
203}
204
205#[derive(Debug, PartialEq)]
207#[non_exhaustive]
208pub struct SourceLocation {
209 pub file: Option<PathBuf>,
210 pub positioning: Positioning,
211}
212
213impl fmt::Display for SourceLocation {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 write!(f, "{}", self.positioning)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_error_detail_display() {
225 let detail = SourceLocation {
226 file: None,
227 positioning: Positioning::Location(Location {
228 absolute_start: 2,
229 absolute_end: 20,
230 start: Position { line: 1, column: 2 },
231 end: Position { line: 3, column: 4 },
232 }),
233 };
234 assert_eq!(
235 format!("{detail}"),
236 "start(line: 1, column: 2), end(line: 3, column: 4)"
237 );
238 }
239
240 #[test]
241 fn test_error_nested_section_level_mismatch_display() {
242 let error = Error::NestedSectionLevelMismatch(
243 Box::new(SourceLocation {
244 file: None,
245 positioning: Positioning::Location(Location {
246 absolute_start: 2,
247 absolute_end: 20,
248 start: Position { line: 1, column: 2 },
249 end: Position { line: 3, column: 4 },
250 }),
251 }),
252 1,
253 2,
254 );
255 assert_eq!(
256 format!("{error}"),
257 "section level mismatch: 1 (expected '2'), position: start(line: 1, column: 2), end(line: 3, column: 4)"
258 );
259 }
260
261 #[test]
262 fn test_error_invalid_admonition_variant_display() {
263 let error = Error::InvalidAdmonitionVariant(
264 Box::new(SourceLocation {
265 file: None,
266 positioning: Positioning::Location(Location {
267 absolute_start: 10,
268 absolute_end: 25,
269 start: Position { line: 2, column: 1 },
270 end: Position {
271 line: 2,
272 column: 15,
273 },
274 }),
275 }),
276 "INVALID".to_string(),
277 );
278 assert_eq!(
279 format!("{error}"),
280 "Invalid admonition variant: INVALID, position: start(line: 2, column: 1), end(line: 2, column: 15)"
281 );
282 }
283
284 #[test]
285 fn test_error_mismatched_delimiters_display() {
286 let error = Error::MismatchedDelimiters(
287 Box::new(SourceLocation {
288 file: None,
289 positioning: Positioning::Location(Location {
290 absolute_start: 0,
291 absolute_end: 50,
292 start: Position { line: 1, column: 1 },
293 end: Position { line: 5, column: 5 },
294 }),
295 }),
296 "example".to_string(),
297 );
298 assert_eq!(
299 format!("{error}"),
300 "mismatched delimiters: example, position: start(line: 1, column: 1), end(line: 5, column: 5)"
301 );
302 }
303
304 #[test]
305 fn test_error_parse_display() {
306 let error = Error::Parse(
307 Box::new(SourceLocation {
308 file: None,
309 positioning: Positioning::Position(Position { line: 1, column: 6 }),
310 }),
311 "unexpected token".to_string(),
312 );
313 assert_eq!(
314 format!("{error}"),
315 "Parsing error: unexpected token, position: line: 1, column: 6"
316 );
317 }
318}