use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
InvalidSyntax,
UnknownProperty,
InvalidValue,
MissingBrace,
MissingSemicolon,
InvalidSelector,
UndefinedVariable,
InvalidColor,
InvalidNumber,
EmptyRule,
}
impl ErrorCode {
pub fn code(&self) -> &'static str {
match self {
Self::InvalidSyntax => "E001",
Self::UnknownProperty => "E002",
Self::InvalidValue => "E003",
Self::MissingBrace => "E004",
Self::MissingSemicolon => "E005",
Self::InvalidSelector => "E006",
Self::UndefinedVariable => "E007",
Self::InvalidColor => "E008",
Self::InvalidNumber => "E009",
Self::EmptyRule => "E010",
}
}
pub fn description(&self) -> &'static str {
match self {
Self::InvalidSyntax => "invalid syntax",
Self::UnknownProperty => "unknown CSS property",
Self::InvalidValue => "invalid property value",
Self::MissingBrace => "missing closing brace '}'",
Self::MissingSemicolon => "missing semicolon ';'",
Self::InvalidSelector => "invalid CSS selector",
Self::UndefinedVariable => "undefined CSS variable",
Self::InvalidColor => "invalid color format",
Self::InvalidNumber => "invalid number or unit",
Self::EmptyRule => "empty CSS rule",
}
}
pub fn help(&self) -> &'static str {
match self {
Self::InvalidSyntax => {
"Check for mismatched brackets, quotes, or unexpected characters"
}
Self::UnknownProperty => "Check spelling or see the supported properties list",
Self::InvalidValue => "The value format doesn't match what this property expects",
Self::MissingBrace => "Every '{' must have a matching '}'",
Self::MissingSemicolon => "Each CSS declaration should end with ';'",
Self::InvalidSelector => "Selectors should be like '.class', '#id', or 'element'",
Self::UndefinedVariable => "Define variables in :root { --name: value; }",
Self::InvalidColor => "Use formats like #rgb, #rrggbb, rgb(r,g,b), or named colors",
Self::InvalidNumber => "Numbers should be like '10', '10px', '50%', or '0.5'",
Self::EmptyRule => "Add at least one property declaration inside the rule",
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.code())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Hint,
}
impl Severity {
pub fn color(&self) -> &'static str {
match self {
Self::Error => "\x1b[31m", Self::Warning => "\x1b[33m", Self::Hint => "\x1b[36m", }
}
pub fn label(&self) -> &'static str {
match self {
Self::Error => "error",
Self::Warning => "warning",
Self::Hint => "hint",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SourceLocation {
pub line: usize,
pub column: usize,
pub offset: usize,
pub length: usize,
}
impl SourceLocation {
pub fn new(line: usize, column: usize, offset: usize, length: usize) -> Self {
Self {
line,
column,
offset,
length,
}
}
pub fn from_offset(source: &str, offset: usize) -> Self {
let mut line = 1;
let mut column = 1;
for (i, ch) in source.char_indices() {
if i >= offset {
break;
}
if ch == '\n' {
line += 1;
column = 1;
} else {
column += 1;
}
}
Self {
line,
column,
offset,
length: 1,
}
}
pub fn from_offset_len(source: &str, offset: usize, length: usize) -> Self {
let mut loc = Self::from_offset(source, offset);
loc.length = length;
loc
}
}
#[derive(Debug, Clone)]
pub struct Suggestion {
pub message: String,
pub replacement: Option<String>,
}
impl Suggestion {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
replacement: None,
}
}
pub fn with_fix(message: impl Into<String>, replacement: impl Into<String>) -> Self {
Self {
message: message.into(),
replacement: Some(replacement.into()),
}
}
}
#[derive(Debug, Clone)]
pub struct RichParseError {
pub code: ErrorCode,
pub severity: Severity,
pub message: String,
pub location: SourceLocation,
pub suggestions: Vec<Suggestion>,
pub notes: Vec<String>,
}
impl RichParseError {
pub fn new(code: ErrorCode, message: impl Into<String>, location: SourceLocation) -> Self {
Self {
code,
severity: Severity::Error,
message: message.into(),
location,
suggestions: Vec::new(),
notes: Vec::new(),
}
}
pub fn severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub fn suggest(mut self, suggestion: Suggestion) -> Self {
self.suggestions.push(suggestion);
self
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.notes.push(note.into());
self
}
pub fn pretty_print(&self, source: &str) -> String {
let mut output = String::new();
let reset = "\x1b[0m";
let bold = "\x1b[1m";
let dim = "\x1b[2m";
let blue = "\x1b[34m";
let cyan = "\x1b[36m";
output.push_str(&format!(
"{}{}{}: {}[{}]{} {}\n",
bold,
self.severity.color(),
self.severity.label(),
reset,
self.code,
reset,
self.message
));
output.push_str(&format!(
" {}-->{} line {}, column {}\n",
blue, reset, self.location.line, self.location.column
));
let lines: Vec<&str> = source.lines().collect();
let line_idx = self.location.line.saturating_sub(1);
if line_idx < lines.len() {
let line_num_width = (self.location.line + 1).to_string().len().max(3);
if line_idx > 0 {
output.push_str(&format!(
" {}{:>width$} |{} {}\n",
dim,
line_idx,
reset,
lines[line_idx - 1],
width = line_num_width
));
}
output.push_str(&format!(
" {}{:>width$} |{} {}\n",
blue,
self.location.line,
reset,
lines[line_idx],
width = line_num_width
));
let pointer_offset = self.location.column.saturating_sub(1);
let pointer_len = self.location.length.max(1);
output.push_str(&format!(
" {:>width$} {} {}{}{}{}",
"",
"|",
" ".repeat(pointer_offset),
self.severity.color(),
"^".repeat(pointer_len),
reset,
width = line_num_width
));
if !self.suggestions.is_empty() {
output.push_str(&format!(" {}", self.suggestions[0].message));
}
output.push('\n');
if line_idx + 1 < lines.len() {
output.push_str(&format!(
" {}{:>width$} |{} {}\n",
dim,
self.location.line + 1,
reset,
lines[line_idx + 1],
width = line_num_width
));
}
}
for suggestion in &self.suggestions {
if let Some(replacement) = &suggestion.replacement {
output.push_str(&format!("\n {}help:{} try `{}`", cyan, reset, replacement));
}
}
for note in &self.notes {
output.push_str(&format!("\n {}note:{} {}", cyan, reset, note));
}
output.push_str(&format!(
"\n {}help:{} {}\n",
cyan,
reset,
self.code.help()
));
output
}
pub fn plain_text(&self, source: &str) -> String {
let mut output = String::new();
output.push_str(&format!(
"{}: [{}] {}\n",
self.severity.label(),
self.code,
self.message
));
output.push_str(&format!(
" --> line {}, column {}\n",
self.location.line, self.location.column
));
let lines: Vec<&str> = source.lines().collect();
let line_idx = self.location.line.saturating_sub(1);
if line_idx < lines.len() {
output.push_str(&format!(" {} | {}\n", self.location.line, lines[line_idx]));
let pointer_offset = self.location.column.saturating_sub(1);
let pointer_len = self.location.length.max(1);
output.push_str(&format!(
" | {}{}\n",
" ".repeat(pointer_offset),
"^".repeat(pointer_len)
));
}
for suggestion in &self.suggestions {
output.push_str(&format!(" help: {}\n", suggestion.message));
if let Some(replacement) = &suggestion.replacement {
output.push_str(&format!(" try: {}\n", replacement));
}
}
for note in &self.notes {
output.push_str(&format!(" note: {}\n", note));
}
output
}
}
impl std::error::Error for RichParseError {}
impl fmt::Display for RichParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {} (line {}, column {})",
self.code, self.message, self.location.line, self.location.column
)
}
}
#[derive(Debug, Default)]
pub struct ParseErrors {
errors: Vec<RichParseError>,
max_errors: usize,
}
impl ParseErrors {
pub fn new() -> Self {
Self {
errors: Vec::new(),
max_errors: 10,
}
}
pub fn max_errors(mut self, max: usize) -> Self {
self.max_errors = max;
self
}
pub fn push(&mut self, error: RichParseError) {
self.errors.push(error);
}
pub fn is_full(&self) -> bool {
self.errors.len() >= self.max_errors
}
pub fn has_errors(&self) -> bool {
self.errors.iter().any(|e| e.severity == Severity::Error)
}
pub fn errors(&self) -> &[RichParseError] {
&self.errors
}
pub fn len(&self) -> usize {
self.errors.len()
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn pretty_print(&self, source: &str) -> String {
let mut output = String::new();
for error in &self.errors {
output.push_str(&error.pretty_print(source));
output.push('\n');
}
let error_count = self
.errors
.iter()
.filter(|e| e.severity == Severity::Error)
.count();
let warning_count = self
.errors
.iter()
.filter(|e| e.severity == Severity::Warning)
.count();
if error_count > 0 || warning_count > 0 {
output.push_str(&format!(
"\x1b[1m{} error(s), {} warning(s)\x1b[0m\n",
error_count, warning_count
));
}
output
}
}
pub const KNOWN_PROPERTIES: &[&str] = &[
"color",
"background",
"background-color",
"border",
"border-color",
"border-width",
"border-style",
"border-radius",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"width",
"height",
"min-width",
"min-height",
"max-width",
"max-height",
"display",
"flex-direction",
"justify-content",
"align-items",
"align-self",
"flex-grow",
"flex-shrink",
"flex-basis",
"flex-wrap",
"gap",
"position",
"top",
"right",
"bottom",
"left",
"font-weight",
"font-style",
"text-align",
"text-decoration",
"opacity",
"visibility",
"overflow",
"cursor",
"transition",
"animation",
"grid-template-columns",
"grid-template-rows",
"grid-column",
"grid-row",
];
pub fn suggest_property(unknown: &str) -> Vec<&'static str> {
let mut suggestions: Vec<(&str, usize)> = KNOWN_PROPERTIES
.iter()
.filter_map(|prop| {
let dist = levenshtein_distance(unknown, prop);
if dist <= 3 && dist < unknown.len() {
Some((*prop, dist))
} else {
None
}
})
.collect();
suggestions.sort_by_key(|(_, d)| *d);
suggestions.into_iter().take(3).map(|(p, _)| p).collect()
}
fn levenshtein_distance(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let a_len = a_chars.len();
let b_len = b_chars.len();
if a_len == 0 {
return b_len;
}
if b_len == 0 {
return a_len;
}
let mut prev: Vec<usize> = (0..=b_len).collect();
let mut curr = vec![0; b_len + 1];
for i in 1..=a_len {
curr[0] = i;
for j in 1..=b_len {
let cost = if a_chars[i - 1] == b_chars[j - 1] {
0
} else {
1
};
curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[b_len]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_levenshtein_identical() {
assert_eq!(levenshtein_distance("test", "test"), 0);
}
#[test]
fn test_levenshtein_one_char_diff() {
assert_eq!(levenshtein_distance("test", "tset"), 2); assert_eq!(levenshtein_distance("test", "tests"), 1); assert_eq!(levenshtein_distance("test", "tes"), 1); assert_eq!(levenshtein_distance("test", "fest"), 1); }
#[test]
fn test_levenshtein_empty_strings() {
assert_eq!(levenshtein_distance("", ""), 0);
assert_eq!(levenshtein_distance("abc", ""), 3);
assert_eq!(levenshtein_distance("", "xyz"), 3);
}
#[test]
fn test_levenshtein_completely_different() {
assert_eq!(levenshtein_distance("abc", "xyz"), 3);
}
}