1use std::fmt;
6
7#[derive(Debug, Clone)]
9pub enum RustyleError {
10 CssParseError {
12 message: String,
13 line: Option<usize>,
14 column: Option<usize>,
15 source_snippet: Option<String>,
16 file: Option<String>,
17 },
18 InvalidProperty {
20 property: String,
21 suggestions: Vec<String>,
22 context: Option<String>,
23 line: Option<usize>,
24 column: Option<usize>,
25 },
26 InvalidValue {
28 property: String,
29 value: String,
30 suggestions: Vec<String>,
31 expected_types: Vec<String>,
32 line: Option<usize>,
33 column: Option<usize>,
34 },
35 MissingProperty {
37 property: String,
38 context: Option<String>,
39 suggestions: Vec<String>,
40 },
41 RegistrationError {
43 message: String,
44 class_name: Option<String>,
45 },
46 InvalidTokens { errors: Vec<String> },
48 BrowserCompatibility {
50 property: String,
51 value: Option<String>,
52 unsupported_browsers: Vec<String>,
53 suggestion: Option<String>,
54 },
55 InvalidSelector {
57 selector: String,
58 message: String,
59 suggestion: Option<String>,
60 },
61}
62
63impl fmt::Display for RustyleError {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 RustyleError::CssParseError {
67 message,
68 line,
69 column,
70 source_snippet,
71 file,
72 } => {
73 if let Some(file_name) = file {
74 write!(f, "Error in {}: ", file_name)?;
75 }
76 write!(f, "CSS parsing error: {}", message)?;
77 if let Some(l) = line {
78 write!(f, " (line {})", l)?;
79 }
80 if let Some(c) = column {
81 write!(f, " (column {})", c)?;
82 }
83 if let Some(snippet) = source_snippet {
84 write!(f, "\n\nCode snippet:\n{}", snippet)?;
85 }
86 Ok(())
87 }
88 RustyleError::InvalidProperty {
89 property,
90 suggestions,
91 context,
92 line,
93 column,
94 } => {
95 write!(f, "Invalid CSS property: '{}'", property)?;
96 if let Some(l) = line {
97 write!(f, " (line {})", l)?;
98 }
99 if let Some(c) = column {
100 write!(f, " (column {})", c)?;
101 }
102 if let Some(ctx) = context {
103 write!(f, "\nContext: {}", ctx)?;
104 }
105 if !suggestions.is_empty() {
106 write!(f, "\n\nDid you mean one of these?")?;
107 for (i, sug) in suggestions.iter().take(3).enumerate() {
108 write!(f, "\n {}. {}", i + 1, sug)?;
109 }
110 }
111 Ok(())
112 }
113 RustyleError::InvalidValue {
114 property,
115 value,
116 suggestions,
117 expected_types,
118 line,
119 column,
120 } => {
121 write!(f, "Invalid value '{}' for property '{}'", value, property)?;
122 if let Some(l) = line {
123 write!(f, " (line {})", l)?;
124 }
125 if let Some(c) = column {
126 write!(f, " (column {})", c)?;
127 }
128 if !expected_types.is_empty() {
129 write!(f, "\n\nExpected one of: {}", expected_types.join(", "))?;
130 }
131 if !suggestions.is_empty() {
132 write!(f, "\n\nDid you mean one of these?")?;
133 for (i, sug) in suggestions.iter().take(3).enumerate() {
134 write!(f, "\n {}. {}", i + 1, sug)?;
135 }
136 }
137 Ok(())
138 }
139 RustyleError::MissingProperty {
140 property,
141 context,
142 suggestions,
143 } => {
144 write!(f, "Missing required property: '{}'", property)?;
145 if let Some(c) = context {
146 write!(f, " in context: {}", c)?;
147 }
148 if !suggestions.is_empty() {
149 write!(f, "\n\nSimilar properties: {}", suggestions.join(", "))?;
150 }
151 Ok(())
152 }
153 RustyleError::RegistrationError {
154 message,
155 class_name,
156 } => {
157 write!(f, "Style registration error: {}", message)?;
158 if let Some(class) = class_name {
159 write!(f, " (class: {})", class)?;
160 }
161 Ok(())
162 }
163 RustyleError::InvalidTokens { errors } => {
164 write!(f, "Invalid design tokens:")?;
165 for error in errors {
166 write!(f, "\n - {}", error)?;
167 }
168 Ok(())
169 }
170 RustyleError::BrowserCompatibility {
171 property,
172 value,
173 unsupported_browsers,
174 suggestion,
175 } => {
176 write!(
177 f,
178 "Browser compatibility warning for property '{}'",
179 property
180 )?;
181 if let Some(val) = value {
182 write!(f, " with value '{}'", val)?;
183 }
184 write!(f, "\nNot supported in: {}", unsupported_browsers.join(", "))?;
185 if let Some(sug) = suggestion {
186 write!(f, "\nSuggestion: {}", sug)?;
187 }
188 Ok(())
189 }
190 RustyleError::InvalidSelector {
191 selector,
192 message,
193 suggestion,
194 } => {
195 write!(f, "Invalid CSS selector '{}': {}", selector, message)?;
196 if let Some(sug) = suggestion {
197 write!(f, "\nSuggestion: {}", sug)?;
198 }
199 Ok(())
200 }
201 }
202 }
203}
204
205impl std::error::Error for RustyleError {}
206
207pub fn suggest_property(property: &str) -> Vec<String> {
209 let common_properties = vec![
210 "background-color",
211 "background",
212 "color",
213 "padding",
214 "margin",
215 "border",
216 "border-radius",
217 "border-width",
218 "border-style",
219 "border-color",
220 "font-size",
221 "font-weight",
222 "font-family",
223 "font-style",
224 "line-height",
225 "display",
226 "flex-direction",
227 "justify-content",
228 "align-items",
229 "flex-wrap",
230 "width",
231 "height",
232 "max-width",
233 "min-width",
234 "max-height",
235 "min-height",
236 "opacity",
237 "transform",
238 "transition",
239 "animation",
240 "box-shadow",
241 "text-align",
242 "text-decoration",
243 "text-transform",
244 "overflow",
245 "position",
246 "top",
247 "right",
248 "bottom",
249 "left",
250 "z-index",
251 "cursor",
252 "pointer-events",
253 ];
254
255 let mut suggestions: Vec<(&str, usize)> = Vec::new();
257
258 for prop in &common_properties {
259 let distance = levenshtein_distance(property, prop);
260 if distance <= 3 {
261 suggestions.push((prop, distance));
262 }
263 }
264
265 suggestions.sort_by_key(|(_, dist)| *dist);
267 suggestions
268 .into_iter()
269 .take(5)
270 .map(|(prop, _)| prop.to_string())
271 .collect()
272}
273
274pub fn get_all_css_properties() -> Vec<&'static str> {
276 vec![
277 "display",
279 "position",
280 "top",
281 "right",
282 "bottom",
283 "left",
284 "z-index",
285 "float",
286 "clear",
287 "overflow",
288 "overflow-x",
289 "overflow-y",
290 "flex",
292 "flex-direction",
293 "flex-wrap",
294 "flex-flow",
295 "justify-content",
296 "align-items",
297 "align-content",
298 "align-self",
299 "flex-grow",
300 "flex-shrink",
301 "flex-basis",
302 "grid",
304 "grid-template",
305 "grid-template-rows",
306 "grid-template-columns",
307 "grid-template-areas",
308 "grid-auto-rows",
309 "grid-auto-columns",
310 "grid-auto-flow",
311 "grid-gap",
312 "grid-row-gap",
313 "grid-column-gap",
314 "grid-row",
315 "grid-column",
316 "width",
318 "height",
319 "min-width",
320 "max-width",
321 "min-height",
322 "max-height",
323 "margin",
324 "margin-top",
325 "margin-right",
326 "margin-bottom",
327 "margin-left",
328 "padding",
329 "padding-top",
330 "padding-right",
331 "padding-bottom",
332 "padding-left",
333 "box-sizing",
334 "border",
335 "border-width",
336 "border-style",
337 "border-color",
338 "border-top",
339 "border-right",
340 "border-bottom",
341 "border-left",
342 "border-radius",
343 "border-top-left-radius",
344 "border-top-right-radius",
345 "border-bottom-left-radius",
346 "border-bottom-right-radius",
347 "font",
349 "font-family",
350 "font-size",
351 "font-weight",
352 "font-style",
353 "font-variant",
354 "line-height",
355 "text-align",
356 "text-decoration",
357 "text-transform",
358 "text-indent",
359 "letter-spacing",
360 "word-spacing",
361 "white-space",
362 "word-wrap",
363 "text-overflow",
364 "color",
366 "background",
367 "background-color",
368 "background-image",
369 "background-repeat",
370 "background-position",
371 "background-size",
372 "background-attachment",
373 "opacity",
375 "visibility",
376 "cursor",
377 "pointer-events",
378 "user-select",
379 "box-shadow",
380 "text-shadow",
381 "outline",
382 "outline-width",
383 "outline-style",
384 "outline-color",
385 "transform",
387 "transform-origin",
388 "transition",
389 "transition-property",
390 "transition-duration",
391 "transition-timing-function",
392 "transition-delay",
393 "animation",
394 "animation-name",
395 "animation-duration",
396 "animation-timing-function",
397 "animation-delay",
398 "animation-iteration-count",
399 "animation-direction",
400 "animation-fill-mode",
401 "content",
403 "quotes",
404 "counter-reset",
405 "counter-increment",
406 "resize",
407 "clip",
408 ]
409}
410
411fn levenshtein_distance(s1: &str, s2: &str) -> usize {
413 let s1_chars: Vec<char> = s1.chars().collect();
414 let s2_chars: Vec<char> = s2.chars().collect();
415 let s1_len = s1_chars.len();
416 let s2_len = s2_chars.len();
417
418 if s1_len == 0 {
419 return s2_len;
420 }
421 if s2_len == 0 {
422 return s1_len;
423 }
424
425 let mut matrix = vec![vec![0; s2_len + 1]; s1_len + 1];
426
427 for i in 0..=s1_len {
428 matrix[i][0] = i;
429 }
430 for j in 0..=s2_len {
431 matrix[0][j] = j;
432 }
433
434 for i in 1..=s1_len {
435 for j in 1..=s2_len {
436 let cost = if s1_chars[i - 1] == s2_chars[j - 1] {
437 0
438 } else {
439 1
440 };
441 matrix[i][j] = (matrix[i - 1][j] + 1)
442 .min(matrix[i][j - 1] + 1)
443 .min(matrix[i - 1][j - 1] + cost);
444 }
445 }
446
447 matrix[s1_len][s2_len]
448}
449
450pub fn create_error_message(
452 error: &RustyleError,
453 file: Option<&str>,
454 line: Option<usize>,
455) -> String {
456 let mut msg = String::new();
457
458 match error {
460 RustyleError::CssParseError {
461 file: err_file,
462 line: err_line,
463 ..
464 } => {
465 if let Some(f) = file.or(err_file.as_ref().map(|s| s.as_str())) {
466 msg.push_str(&format!("Error in {}: ", f));
467 }
468 if let Some(l) = line.or(*err_line) {
469 msg.push_str(&format!("line {}: ", l));
470 }
471 }
472 _ => {
473 if let Some(f) = file {
474 msg.push_str(&format!("Error in {}: ", f));
475 }
476 if let Some(l) = line {
477 msg.push_str(&format!("line {}: ", l));
478 }
479 }
480 }
481
482 msg.push_str(&error.to_string());
483
484 msg.push_str("\n\nFor more information:");
486 msg.push_str("\n - Documentation: https://github.com/usvx/rustyle");
487 msg.push_str("\n - CSS Reference: https://developer.mozilla.org/en-US/docs/Web/CSS");
488
489 if let RustyleError::InvalidProperty { property, .. } = error {
491 if property.contains("colour") {
492 msg.push_str("\n\n💡 Tip: Use 'color' instead of 'colour' (American spelling)");
493 }
494 }
495
496 msg
497}
498
499pub fn extract_code_snippet(
501 source: &str,
502 line: usize,
503 column: Option<usize>,
504 context_lines: usize,
505) -> String {
506 let lines: Vec<&str> = source.lines().collect();
507 let line_num = line.saturating_sub(1);
508
509 if line_num >= lines.len() {
510 return String::new();
511 }
512
513 let start = line_num.saturating_sub(context_lines);
514 let end = (line_num + context_lines + 1).min(lines.len());
515
516 let mut snippet = String::new();
517 for i in start..end {
518 let line_content = lines[i];
519 let line_no = i + 1;
520
521 snippet.push_str(&format!("{:4} | ", line_no));
523 snippet.push_str(line_content);
524 snippet.push('\n');
525
526 if i == line_num {
528 if let Some(col) = column {
529 let spaces = " | ".len() + col.saturating_sub(1);
530 snippet.push_str(&" ".repeat(spaces));
531 snippet.push_str("^\n");
532 }
533 }
534 }
535
536 snippet
537}
538
539pub fn validate_property(property: &str) -> Result<(), RustyleError> {
541 let valid_properties = get_all_css_properties();
542
543 if valid_properties.contains(&property) {
544 return Ok(());
545 }
546
547 let suggestions = suggest_property(property);
548 Err(RustyleError::InvalidProperty {
549 property: property.to_string(),
550 suggestions,
551 context: None,
552 line: None,
553 column: None,
554 })
555}
556
557pub fn validate_value(property: &str, value: &str) -> Result<(), RustyleError> {
559 if value.trim().is_empty() {
561 return Err(RustyleError::InvalidValue {
562 property: property.to_string(),
563 value: value.to_string(),
564 suggestions: vec![],
565 expected_types: vec!["non-empty value".to_string()],
566 line: None,
567 column: None,
568 });
569 }
570
571 Ok(())
572}