From 1eb98255bf48c856d0065a0a4663f771f67e9dcf Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 3 Nov 2025 02:09:32 +0100 Subject: [PATCH 1/4] Rust: Add colored output for `parse` command and `tree_inspect` --- rust/Cargo.toml | 1 + rust/Makefile | 2 +- rust/src/token.rs | 7 ++++- rust/tests/error_handling_test.rs | 6 ++-- templates/rust/src/errors.rs.erb | 29 ++++++++++-------- templates/rust/src/nodes.rs.erb | 50 +++++++++++++++++-------------- 6 files changed, 56 insertions(+), 39 deletions(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index f20b08d80..e9404ad13 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -21,6 +21,7 @@ path = "src/main.rs" [dependencies] libc = "0.2" +colored = "2" [dev-dependencies] insta = "1.40" diff --git a/rust/Makefile b/rust/Makefile index 4390698cf..a0ae1e4f3 100644 --- a/rust/Makefile +++ b/rust/Makefile @@ -26,7 +26,7 @@ cli: build @./bin/$(BIN_NAME) || true test: build - cargo +stable test --verbose -- --test-threads=1 + NO_COLOR=1 cargo +stable test --verbose -- --test-threads=1 format: cargo +nightly fmt diff --git a/rust/src/token.rs b/rust/src/token.rs index 2ddee20fb..60bc2c9e3 100644 --- a/rust/src/token.rs +++ b/rust/src/token.rs @@ -1,5 +1,6 @@ use crate::location::Location; use crate::range::Range; +use colored::*; use std::fmt; #[derive(Debug, Clone, PartialEq, Eq)] @@ -48,7 +49,11 @@ impl Token { } pub fn tree_inspect(&self) -> String { - format!("\"{}\" (location: {})", self.escaped_value(), self.location) + format!( + "{} {}", + format!("\"{}\"", self.escaped_value()).green(), + format!("(location: {})", self.location).dimmed() + ) } } diff --git a/rust/tests/error_handling_test.rs b/rust/tests/error_handling_test.rs index 5d04632e1..b94f2a6c3 100644 --- a/rust/tests/error_handling_test.rs +++ b/rust/tests/error_handling_test.rs @@ -6,9 +6,9 @@ fn test_unclosed_element_error() { let result = parse(source).unwrap(); let tree_inspect = result.tree_inspect(); - assert!(tree_inspect.contains("UNCLOSED_ELEMENT_ERROR")); + assert!(tree_inspect.contains("UnclosedElementError")); assert!(tree_inspect.contains("Tag `
` opened at (1:1) was never closed")); - assert!(tree_inspect.contains("MISSING_CLOSING_TAG_ERROR")); + assert!(tree_inspect.contains("MissingClosingTagError")); assert!(tree_inspect.contains("Opening tag `
` at (1:1) doesn't have a matching closing tag")); } @@ -18,7 +18,7 @@ fn test_tag_names_mismatch_error() { let result = parse(source).unwrap(); let tree_inspect = result.tree_inspect(); - assert!(tree_inspect.contains("TAG_NAMES_MISMATCH_ERROR")); + assert!(tree_inspect.contains("TagNamesMismatchError")); assert!(tree_inspect.contains("Opening tag `
` at (1:1) closed with ``")); } diff --git a/templates/rust/src/errors.rs.erb b/templates/rust/src/errors.rs.erb index dbd859601..447120543 100644 --- a/templates/rust/src/errors.rs.erb +++ b/templates/rust/src/errors.rs.erb @@ -1,4 +1,5 @@ use crate::{Location, Token}; +use colored::*; fn escape_string(s: &str) -> String { s.replace('\\', "\\\\") @@ -9,25 +10,25 @@ fn escape_string(s: &str) -> String { } fn format_string_value(value: &str) -> String { - format!("\"{}\"", escape_string(value)) + format!("\"{}\"", escape_string(value)).green().to_string() } fn format_token_value(token: &Option) -> String { - token.as_ref().map(|t| t.tree_inspect()).unwrap_or_else(|| "∅".to_string()) + token.as_ref().map(|t| t.tree_inspect()).unwrap_or_else(|| "∅".magenta().to_string()) } fn format_token_type_value(token_type: &Option) -> String { - token_type.as_ref().map(|t| format!("\"{}\"", t)).unwrap_or_else(|| "∅".to_string()) + token_type.as_ref().map(|t| format!("\"{}\"", t).green().to_string()).unwrap_or_else(|| "∅".magenta().to_string()) } #[allow(dead_code)] fn format_position_value(position: &Option) -> String { - position.as_ref().map(|p| p.to_string()).unwrap_or_else(|| "∅".to_string()) + position.as_ref().map(|p| p.to_string()).unwrap_or_else(|| "∅".magenta().to_string()) } #[allow(dead_code)] fn format_size_value(value: usize) -> String { - value.to_string() + value.to_string().magenta().bold().to_string() } #[derive(Debug, Clone, PartialEq)] @@ -167,22 +168,26 @@ impl <%= error.name %> { pub fn tree_inspect(&self) -> String { let mut output = String::new(); - output.push_str(&format!("@ <%= error.type %> {}\n", self.location)); + output.push_str(&format!("{} {} {}\n", + "@".white(), + "<%= error.name %>".red().bold(), + format!("(location: {})", self.location).dimmed() + )); <%- symbol = error.fields.any? ? "├──" : "└──" -%> - output.push_str(&format!("<%= symbol %> message: {}\n", format_string_value(&self.message))); + output.push_str(&format!("{} {}: {}\n", "<%= symbol %>".white(), "message".white(), format_string_value(&self.message))); <%- error.fields.each do |field| -%> <%- symbol = error.fields.last == field ? "└──" : "├──" -%> <%- case field -%> <%- when Herb::Template::StringField -%> - output.push_str(&format!("<%= symbol %> <%= field.name %>: {}\n", format_string_value(&self.<%= field.name %>))); + output.push_str(&format!("{} {}: {}\n", "<%= symbol %>".white(), "<%= field.name %>".white(), format_string_value(&self.<%= field.name %>))); <%- when Herb::Template::TokenField -%> - output.push_str(&format!("<%= symbol %> <%= field.name %>: {}\n", format_token_value(&self.<%= field.name %>))); + output.push_str(&format!("{} {}: {}\n", "<%= symbol %>".white(), "<%= field.name %>".white(), format_token_value(&self.<%= field.name %>))); <%- when Herb::Template::TokenTypeField -%> - output.push_str(&format!("<%= symbol %> <%= field.name %>: {}\n", format_token_type_value(&self.<%= field.name %>))); + output.push_str(&format!("{} {}: {}\n", "<%= symbol %>".white(), "<%= field.name %>".white(), format_token_type_value(&self.<%= field.name %>))); <%- when Herb::Template::PositionField -%> - output.push_str(&format!("<%= symbol %> <%= field.name %>: {}\n", format_position_value(&self.<%= field.name %>))); + output.push_str(&format!("{} {}: {}\n", "<%= symbol %>".white(), "<%= field.name %>".white(), format_position_value(&self.<%= field.name %>))); <%- when Herb::Template::SizeTField -%> - output.push_str(&format!("<%= symbol %> <%= field.name %>: {}\n", format_size_value(self.<%= field.name %>))); + output.push_str(&format!("{} {}: {}\n", "<%= symbol %>".white(), "<%= field.name %>".white(), format_size_value(self.<%= field.name %>))); <%- end -%> <%- end -%> diff --git a/templates/rust/src/nodes.rs.erb b/templates/rust/src/nodes.rs.erb index c68784233..d79814628 100644 --- a/templates/rust/src/nodes.rs.erb +++ b/templates/rust/src/nodes.rs.erb @@ -1,5 +1,6 @@ use crate::errors::{AnyError, ErrorNode}; use crate::{Location, Token}; +use colored::*; fn escape_string(s: &str) -> String { s.replace('\\', "\\\\") @@ -10,15 +11,15 @@ fn escape_string(s: &str) -> String { } fn format_string_value(value: &str) -> String { - format!("\"{}\"", escape_string(value)) + format!("\"{}\"", escape_string(value)).green().to_string() } fn format_token_value(token: &Option) -> String { - token.as_ref().map(|t| t.tree_inspect()).unwrap_or_else(|| "∅".to_string()) + token.as_ref().map(|t| t.tree_inspect()).unwrap_or_else(|| "∅".magenta().to_string()) } fn format_bool_value(value: bool) -> String { - value.to_string() + value.to_string().magenta().bold().to_string() } fn format_node_value(node: &Option>, prefix: &str, add_spacing: bool) -> String { @@ -31,7 +32,7 @@ fn format_node_value(node: &Option>, prefix: &str, add_ } output } else { - "∅\n".to_string() + format!("{}\n", "∅".magenta()) } } @@ -49,9 +50,10 @@ fn inspect_errors(errors: &[AnyError], prefix: &str) -> String { } let mut output = String::new(); - output.push_str(&format!("├── errors: ({} error{})\n", - errors.len(), - if errors.len() == 1 { "" } else { "s" } + output.push_str(&format!("{} {}: {}\n", + "├──".white(), + "errors".red().bold(), + format!("({} error{})", errors.len(), if errors.len() == 1 { "" } else { "s" }).dimmed() )); for (i, error) in errors.iter().enumerate() { @@ -61,10 +63,10 @@ fn inspect_errors(errors: &[AnyError], prefix: &str) -> String { let tree = error.tree_inspect(); let tree = tree.trim_end_matches('\n'); - output.push_str(&format!("{}{}{}\n", prefix, symbol, tree.replace('\n', &format!("\n{}{}", prefix, next_prefix)))); + output.push_str(&format!("{}{}{}\n", prefix, symbol.white(), tree.replace('\n', &format!("\n{}{}", prefix, next_prefix)))); if !is_last { - output.push_str(&format!("{}│ \n", prefix)); + output.push_str(&format!("{}{}\n", prefix, "│ ".white())); } } @@ -74,11 +76,11 @@ fn inspect_errors(errors: &[AnyError], prefix: &str) -> String { fn inspect_array(array: &[AnyNode], prefix: &str) -> String { if array.is_empty() { - return "[]\n".to_string(); + return format!("{}\n", "[]".dimmed()); } let mut output = String::new(); - output.push_str(&format!("({} item{})\n", array.len(), if array.len() == 1 { "" } else { "s" })); + output.push_str(&format!("{}\n", format!("({} item{})", array.len(), if array.len() == 1 { "" } else { "s" }).dimmed())); for (i, item) in array.iter().enumerate() { let is_last = i == array.len() - 1; @@ -89,13 +91,13 @@ fn inspect_array(array: &[AnyNode], prefix: &str) -> String { let tree = tree.trim_end_matches('\n'); output.push_str(prefix); - output.push_str(symbol); + output.push_str(&symbol.white().to_string()); output.push_str(&tree.replace('\n', &format!("\n{}{}", prefix, next_prefix))); output.push('\n'); if !is_last { output.push_str(prefix); - output.push_str(next_prefix); + output.push_str(&next_prefix.white().to_string()); output.push('\n'); } } @@ -109,12 +111,12 @@ fn inspect_node_field(node: &T, prefix: &str) -> String { let lines: Vec<&str> = tree.split('\n').collect(); if lines.is_empty() { - return "∅\n".to_string(); + return format!("{}\n", "∅".magenta()); } let mut result = String::new(); result.push_str(prefix); - result.push_str("└── "); + result.push_str(&"└── ".white().to_string()); result.push_str(lines[0]); result.push('\n'); @@ -336,7 +338,11 @@ impl Node for <%= node.name %> { fn tree_inspect(&self) -> String { let mut output = String::new(); - output.push_str(&format!("@ <%= node.name %> (location: {})\n", self.location)); + output.push_str(&format!("{} {} {}\n", + "@".white(), + "<%= node.name %>".yellow().bold(), + format!("(location: {})", self.location).dimmed() + )); output.push_str(&format_errors_field(&self.errors, <%- if node.fields.any? -%>"│ "<%- else -%>" "<%- end -%>)); <%- if node.fields.any? -%> <%- node.fields.each_with_index do |field, index| -%> @@ -344,19 +350,19 @@ impl Node for <%= node.name %> { <%- symbol = is_last ? "└── " : "├── " -%> <%- case field -%> <%- when Herb::Template::StringField, Herb::Template::ElementSourceField -%> - output.push_str(&format!("<%= symbol %><%= field.name %>: {}\n", format_string_value(&self.<%= field.name %>))); + output.push_str(&format!("{}{}: {}\n", "<%= symbol %>".white(), "<%= field.name %>".white(), format_string_value(&self.<%= field.name %>))); <%- when Herb::Template::TokenField -%> - output.push_str(&format!("<%= symbol %><%= field.name %>: {}\n", format_token_value(&self.<%= field.name %>))); + output.push_str(&format!("{}{}: {}\n", "<%= symbol %>".white(), "<%= field.name %>".white(), format_token_value(&self.<%= field.name %>))); <%- when Herb::Template::BooleanField -%> - output.push_str(&format!("<%= symbol %><%= field.name %>: {}\n", format_bool_value(self.<%= field.name %>))); + output.push_str(&format!("{}{}: {}\n", "<%= symbol %>".white(), "<%= field.name %>".white(), format_bool_value(self.<%= field.name %>))); <%- when Herb::Template::ArrayField -%> - output.push_str(&format!("<%= symbol %><%= field.name %>: {}", format_array_value(&self.<%= field.name %>, "<%= is_last ? " " : "│ " %>"))); + output.push_str(&format!("{}{}: {}", "<%= symbol %>".white(), "<%= field.name %>".white(), format_array_value(&self.<%= field.name %>, "<%= is_last ? " " : "│ " %>"))); <%- when Herb::Template::NodeField -%> - output.push_str(&format!("<%= symbol %><%= field.name %>: {}", format_node_value(&self.<%= field.name %>, "<%= is_last ? " " : "│ " %>", <%= !is_last %>))); + output.push_str(&format!("{}{}: {}", "<%= symbol %>".white(), "<%= field.name %>".white(), format_node_value(&self.<%= field.name %>, "<%= is_last ? " " : "│ " %>", <%= !is_last %>))); <%- end -%> <%- end -%> <%- else -%> - output.push_str("└── (no fields)\n"); + output.push_str(&format!("{} {}\n", "└──".white(), "(no fields)".dimmed())); <%- end -%> output From 8e6d41ce552eba5c3e5c2e0f3de979239be1293c Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 3 Nov 2025 02:12:10 +0100 Subject: [PATCH 2/4] Remove unused ParserOptions --- rust/src/ffi.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs index 98bb12fd4..5a2c98e50 100644 --- a/rust/src/ffi.rs +++ b/rust/src/ffi.rs @@ -1,8 +1,3 @@ -#[repr(C)] -pub struct ParserOptions { - _private: [u8; 0], -} - pub use crate::bindings::{ ast_node_free, element_source_to_string, hb_array_get, hb_array_size, hb_string_T, herb_extract, herb_free_tokens, herb_lex, herb_parse, herb_prism_version, herb_version, token_type_to_string, From effe1c9f84a3cf8d94176a5c95962404c8ef9d75 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 3 Nov 2025 02:23:32 +0100 Subject: [PATCH 3/4] Color next prefix --- templates/rust/src/nodes.rs.erb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/templates/rust/src/nodes.rs.erb b/templates/rust/src/nodes.rs.erb index d79814628..1b8e4c8ed 100644 --- a/templates/rust/src/nodes.rs.erb +++ b/templates/rust/src/nodes.rs.erb @@ -59,7 +59,8 @@ fn inspect_errors(errors: &[AnyError], prefix: &str) -> String { for (i, error) in errors.iter().enumerate() { let is_last = i == errors.len() - 1; let symbol = if is_last { "└── " } else { "├── " }; - let next_prefix = if is_last { " " } else { "│ " }; + let next_prefix_str = if is_last { " " } else { "│ " }; + let next_prefix = next_prefix_str.white().to_string(); let tree = error.tree_inspect(); let tree = tree.trim_end_matches('\n'); @@ -85,7 +86,8 @@ fn inspect_array(array: &[AnyNode], prefix: &str) -> String { for (i, item) in array.iter().enumerate() { let is_last = i == array.len() - 1; let symbol = if is_last { "└── " } else { "├── " }; - let next_prefix = if is_last { " " } else { "│ " }; + let next_prefix_str = if is_last { " " } else { "│ " }; + let next_prefix = next_prefix_str.white().to_string(); let tree = item.tree_inspect(); let tree = tree.trim_end_matches('\n'); @@ -97,7 +99,7 @@ fn inspect_array(array: &[AnyNode], prefix: &str) -> String { if !is_last { output.push_str(prefix); - output.push_str(&next_prefix.white().to_string()); + output.push_str(&next_prefix); output.push('\n'); } } From 21daaf5aebd1c0edb8c4af343a0983fb1c94c17f Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 3 Nov 2025 02:25:29 +0100 Subject: [PATCH 4/4] Color next prefix --- templates/rust/src/nodes.rs.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/rust/src/nodes.rs.erb b/templates/rust/src/nodes.rs.erb index 1b8e4c8ed..3bc8b4123 100644 --- a/templates/rust/src/nodes.rs.erb +++ b/templates/rust/src/nodes.rs.erb @@ -345,7 +345,7 @@ impl Node for <%= node.name %> { "<%= node.name %>".yellow().bold(), format!("(location: {})", self.location).dimmed() )); - output.push_str(&format_errors_field(&self.errors, <%- if node.fields.any? -%>"│ "<%- else -%>" "<%- end -%>)); + output.push_str(&format_errors_field(&self.errors, &<%- if node.fields.any? -%>"│ "<%- else -%>" "<%- end -%>.white().to_string())); <%- if node.fields.any? -%> <%- node.fields.each_with_index do |field, index| -%> <%- is_last = index == node.fields.length - 1 -%> @@ -358,9 +358,9 @@ impl Node for <%= node.name %> { <%- when Herb::Template::BooleanField -%> output.push_str(&format!("{}{}: {}\n", "<%= symbol %>".white(), "<%= field.name %>".white(), format_bool_value(self.<%= field.name %>))); <%- when Herb::Template::ArrayField -%> - output.push_str(&format!("{}{}: {}", "<%= symbol %>".white(), "<%= field.name %>".white(), format_array_value(&self.<%= field.name %>, "<%= is_last ? " " : "│ " %>"))); + output.push_str(&format!("{}{}: {}", "<%= symbol %>".white(), "<%= field.name %>".white(), format_array_value(&self.<%= field.name %>, &"<%= is_last ? " " : "│ " %>".white().to_string()))); <%- when Herb::Template::NodeField -%> - output.push_str(&format!("{}{}: {}", "<%= symbol %>".white(), "<%= field.name %>".white(), format_node_value(&self.<%= field.name %>, "<%= is_last ? " " : "│ " %>", <%= !is_last %>))); + output.push_str(&format!("{}{}: {}", "<%= symbol %>".white(), "<%= field.name %>".white(), format_node_value(&self.<%= field.name %>, &"<%= is_last ? " " : "│ " %>".white().to_string(), <%= !is_last %>))); <%- end -%> <%- end -%> <%- else -%>