farben_core/errors.rs
1//! Error types for farben markup tokenization and registry operations.
2//!
3//! [`LexError`] covers failures during tokenization of markup strings like `[bold red]text[/]`.
4//! [`RegistryError`] covers failures in registry operations such as `set_prefix` and `insert_style`.
5//! [`LexErrorDisplay`] wraps a [`LexError`] with the original input to produce compiler-style
6//! diagnostic output with a caret pointing at the offending byte offset.
7
8/// Errors produced by registry operations (`set_prefix`, `insert_style`).
9///
10/// These errors carry no source position because registry calls happen outside of markup
11/// parsing, where no input string is available to point into.
12#[derive(Debug)]
13pub enum RegistryError {
14 /// The `prefix!` macro was called with a style name that has not been registered.
15 UnknownStyle(String),
16}
17
18impl std::fmt::Display for RegistryError {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 match self {
21 Self::UnknownStyle(s) => write!(f, "unknown style: '{s}' has not been registered"),
22 }
23 }
24}
25
26/// Diagnostic formatter that renders a [`LexError`] alongside the original markup input.
27///
28/// Formats output in the style of the Rust compiler: the input string on one line, followed by
29/// a caret (`^`) on the next line aligned to the byte offset stored in the error variant.
30///
31/// # Example
32///
33/// ```text
34/// | [bold unknown]text[/]
35/// | ^^^^ invalid tag: 'bold unknown'
36/// ```
37pub struct LexErrorDisplay<'a> {
38 /// The error to render.
39 pub error: &'a LexError,
40 /// The full markup string that was being tokenized when the error occurred.
41 pub input: &'a str,
42}
43
44impl std::fmt::Display for LexErrorDisplay<'_> {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 writeln!(f, " | {}", self.input)?;
47 match self.error {
48 LexError::UnclosedTag(position) => {
49 write!(f, " | {}^ unclosed tag", " ".repeat(*position))
50 }
51 LexError::InvalidTag {
52 tag_content,
53 position,
54 } => {
55 write!(
56 f,
57 " | {}{} invalid tag: '{tag_content}'",
58 " ".repeat(*position + 1),
59 "^".repeat(tag_content.len())
60 )
61 }
62 LexError::UnclosedValue(position) => {
63 write!(
64 f,
65 " | {}^ unclosed parentheses for color value",
66 " ".repeat(*position + 1)
67 )
68 }
69 LexError::InvalidArgumentCount {
70 expected,
71 got,
72 position,
73 } => {
74 write!(
75 f,
76 " | {}^ expected {expected} arguments, got {got}",
77 " ".repeat(*position + 1)
78 )
79 }
80 LexError::InvalidValue { value, position } => {
81 write!(
82 f,
83 " | {}{} invalid value: '{value}'",
84 " ".repeat(*position + 1),
85 "^".repeat(value.len())
86 )
87 }
88 LexError::InvalidResetTarget(position) => {
89 write!(
90 f,
91 " | {}^ reset target must be a color or emphasis tag",
92 " ".repeat(*position + 1)
93 )
94 }
95 }
96 }
97}
98
99/// Errors produced during tokenization of a farben markup string.
100#[derive(Debug)]
101pub enum LexError {
102 /// A `[` was found with no matching `]`.
103 UnclosedTag(usize),
104 /// The tag name is not a recognized keyword or color form.
105 InvalidTag {
106 /// The raw text inside the brackets, e.g. `"floob"` for `[floob]`.
107 tag_content: String,
108 /// Byte offset of the opening `[` in the original input.
109 position: usize,
110 },
111 /// A color value function (e.g. `rgb(` or `ansi(`) was opened but never closed.
112 UnclosedValue(usize),
113 /// A color function received the wrong number of arguments.
114 InvalidArgumentCount {
115 /// How many arguments the function requires (e.g. 3 for `rgb`).
116 expected: usize,
117 /// How many arguments were actually provided.
118 got: usize,
119 /// Byte offset of the function call in the original input.
120 position: usize,
121 },
122 /// An argument could not be parsed into the expected numeric type.
123 InvalidValue {
124 /// The offending argument as written.
125 value: String,
126 /// Byte offset of the function call in the original input.
127 position: usize,
128 },
129 /// A reset tag was given a target that cannot be reset (e.g. `[/reset]` or `[/prefix]`).
130 InvalidResetTarget(usize),
131}
132
133impl LexError {
134 /// Returns a helper that formats this error with a caret pointing at the
135 /// offending byte in the original input.
136 ///
137 /// # Example
138 ///
139 /// ```
140 /// use farben_core::errors::LexError;
141 /// use farben_core::lexer::tokenize;
142 ///
143 /// let input = "[bold red unkn]oops";
144 /// match tokenize(input) {
145 /// Ok(_) => {}
146 /// Err(e) => eprintln!("{}", e.display(input)),
147 /// }
148 /// ```
149 pub fn display<'a>(&'a self, input: &'a str) -> LexErrorDisplay<'a> {
150 LexErrorDisplay { error: self, input }
151 }
152}
153
154impl std::fmt::Display for LexError {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 match self {
157 LexError::UnclosedTag(pos) => write!(f, "unclosed tag at position {pos}"),
158 LexError::InvalidTag {
159 tag_content,
160 position,
161 } => write!(f, "invalid tag '{tag_content}' at position {position}"),
162 LexError::UnclosedValue(pos) => {
163 write!(f, "unclosed parentheses for color value at position {pos}")
164 }
165 LexError::InvalidArgumentCount {
166 expected,
167 got,
168 position,
169 } => {
170 write!(
171 f,
172 "expected {expected} arguments, got {got} at position {position}"
173 )
174 }
175 LexError::InvalidValue { value, position } => {
176 write!(f, "invalid value '{value}' at position {position}")
177 }
178 LexError::InvalidResetTarget(pos) => write!(
179 f,
180 "reset target must be a color or emphasis tag at position {pos}"
181 ),
182 }
183 }
184}
185
186impl std::error::Error for LexError {}