diff --git a/Cargo.lock b/Cargo.lock index 157fde7..c92a2e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,7 +76,7 @@ dependencies = [ [[package]] name = "cirru_parser" -version = "0.2.5" +version = "0.2.8" dependencies = [ "criterion", "serde", diff --git a/Cargo.toml b/Cargo.toml index 465eae2..aa02aa3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cirru_parser" -version = "0.2.5" +version = "0.2.8" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/src/parser.rs b/src/parser.rs index 8a35048..2302af3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -46,7 +46,7 @@ use tree::{resolve_comma, resolve_dollar}; pub use primes::{Cirru, CirruLexItem, CirruLexItemList, escape_cirru_leaf}; pub use s_expr::format_to_lisp; -pub use writer::{CirruOneLinerExt, CirruWriterOptions, format, format_expr_one_liner}; +pub use writer::{CirruOneLinerExt, CirruWriterOptions, format, format_expr_one_liner, generate_leaf}; /// Helper function to format and print a detailed error pub fn print_error(error: &CirruError, source_code: Option<&str>) { diff --git a/src/primes.rs b/src/primes.rs index 525155c..5812aef 100644 --- a/src/primes.rs +++ b/src/primes.rs @@ -172,6 +172,32 @@ impl Cirru { Cirru::Leaf(s) => &(**s) == ";" || &(**s) == ";;", } } + + /// Returns `true` if this node is a `Leaf`. + pub fn is_leaf(&self) -> bool { + matches!(self, Self::Leaf(_)) + } + + /// Returns `true` if this node is a `List`. + pub fn is_list(&self) -> bool { + matches!(self, Self::List(_)) + } + + /// Returns the leaf string slice if this node is a `Leaf`, otherwise `None`. + pub fn as_leaf_str(&self) -> Option<&str> { + match self { + Self::Leaf(s) => Some(s), + _ => None, + } + } + + /// Returns the first child node if this is a non-empty `List`, otherwise `None`. + pub fn head(&self) -> Option<&Cirru> { + match self { + Self::List(xs) => xs.first(), + _ => None, + } + } } #[derive(fmt::Debug, PartialEq, Eq)] diff --git a/src/writer.rs b/src/writer.rs index 1c71d12..7817236 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -62,7 +62,11 @@ fn is_char_allowed(x: char) -> bool { ALLOWED_CHARS.find(x).is_some() } -fn generate_leaf(s: &str) -> String { +/// Format a Cirru leaf token: returns the bare string if all characters are allowed +/// in Cirru without quoting; otherwise wraps in double quotes with escape sequences. +/// This mirrors the exact quoting behaviour used by the Cirru formatter when emitting +/// leaf nodes, so callers outside this crate get consistent output. +pub fn generate_leaf(s: &str) -> String { let mut all_allowed = true; for x in s.chars() { if !is_char_allowed(x) { @@ -182,14 +186,6 @@ fn get_node_kind(cursor: &Cirru) -> WriterNode { } } -fn should_insist_nested_head(ys: &[Cirru], idx: usize, prev_kind: WriterNode) -> bool { - if prev_kind == WriterNode::BoxedExpr || prev_kind == WriterNode::Expr { - return true; - } - - idx > 1 && matches!(ys.first(), Some(Cirru::List(head)) if head.len() > 1) -} - fn generate_tree( xs: &[Cirru], insist_head: bool, @@ -204,6 +200,7 @@ fn generate_tree( for (idx, cursor) in xs.iter().enumerate() { let kind = get_node_kind(cursor); let next_level = level + 1; + let child_insist_head = (prev_kind == WriterNode::BoxedExpr) || (prev_kind == WriterNode::Expr) || idx > 1; let at_tail = idx != 0 && !in_tail && prev_kind == WriterNode::Leaf && idx == xs.len() - 1; // println!("\nloop {:?} {:?}", prev_kind, kind); @@ -213,7 +210,6 @@ fn generate_tree( let child: String = match cursor { Cirru::Leaf(s) => generate_leaf(s), Cirru::List(ys) => { - let child_insist_head = should_insist_nested_head(ys, idx, prev_kind); if at_tail { if ys.is_empty() { String::from("$") @@ -242,12 +238,8 @@ fn generate_tree( generate_empty_expr() // special since empty expr is treated as leaf } } else if kind == WriterNode::SimpleExpr { - if prev_kind == WriterNode::Leaf && (idx == 1 || level > base_level || xs.len().saturating_sub(idx) <= 2) { + if prev_kind == WriterNode::Leaf { generate_inline_expr(ys) - } else if prev_kind == WriterNode::Leaf { - let mut ret = render_newline(next_level); - ret.push_str(&generate_tree(ys, child_insist_head, options, next_level, false)?); - ret } else if options.use_inline && prev_kind == WriterNode::SimpleExpr { let mut ret = String::from(" "); ret.push_str(&generate_inline_expr(ys)); @@ -268,7 +260,12 @@ fn generate_tree( } } else if kind == WriterNode::BoxedExpr { let content = generate_tree(ys, child_insist_head, options, next_level, false)?; - if prev_kind == WriterNode::Nil || prev_kind == WriterNode::Leaf || prev_kind == WriterNode::SimpleExpr { + if child_insist_head { + // special case for boxed expr when it insists head, it has both indentation and brackets + let mut ret = render_newline(next_level); + ret.push_str(&content); + ret + } else if prev_kind == WriterNode::Nil || prev_kind == WriterNode::Leaf || prev_kind == WriterNode::SimpleExpr { content } else { let mut ret = render_newline(next_level); @@ -276,7 +273,7 @@ fn generate_tree( ret } } else { - return Err(String::from("Unpected condition")); + return Err(String::from("Unexpected condition")); } } }; @@ -285,7 +282,7 @@ fn generate_tree( let chunk = if at_tail || (prev_kind == WriterNode::Leaf && kind == WriterNode::Leaf) - || (prev_kind == WriterNode::Leaf && kind == WriterNode::SimpleExpr && !child.starts_with('\n')) + || (prev_kind == WriterNode::Leaf && kind == WriterNode::SimpleExpr) || prev_kind == WriterNode::SimpleExpr && kind == WriterNode::Leaf { let mut ret = String::from(" "); diff --git a/tests/cirru/match.cirru b/tests/cirru/match.cirru index a39ab03..664f047 100644 --- a/tests/cirru/match.cirru +++ b/tests/cirru/match.cirru @@ -1,6 +1,6 @@ match x - :dyn 1 + (:dyn) 1 (:dyn x) 2 (:dyn x y) 3 (:dyn x y z) 4 \ No newline at end of file diff --git a/tests/data/match.json b/tests/data/match.json index a7f40b5..c3fa48d 100644 --- a/tests/data/match.json +++ b/tests/data/match.json @@ -2,7 +2,7 @@ [ "match", "x", - [":dyn", "1"], + [[":dyn"], "1"], [[":dyn", "x"], "2"], [[":dyn", "x", "y"], "3"], [[":dyn", "x", "y", "z"], "4"] diff --git a/tests/writer_cirru/comma-indent.cirru b/tests/writer_cirru/comma-indent.cirru index dc00716..a16a0e1 100644 --- a/tests/writer_cirru/comma-indent.cirru +++ b/tests/writer_cirru/comma-indent.cirru @@ -1,5 +1,4 @@ c $ d (e) - a - , d (f) - g + (a) d (f) + g diff --git a/tests/writer_cirru/indent.cirru b/tests/writer_cirru/indent.cirru index 35166de..ba82b65 100644 --- a/tests/writer_cirru/indent.cirru +++ b/tests/writer_cirru/indent.cirru @@ -2,5 +2,5 @@ a $ b (c) e f - g + (g) h diff --git a/tests/writer_cirru/match.cirru b/tests/writer_cirru/match.cirru index c28bc16..b4d1f0a 100644 --- a/tests/writer_cirru/match.cirru +++ b/tests/writer_cirru/match.cirru @@ -1,6 +1,6 @@ match x - :dyn 1 + (:dyn) 1 (:dyn x) 2 (:dyn x y) 3 (:dyn x y z) 4 diff --git a/tests/writer_data/match.json b/tests/writer_data/match.json index a7f40b5..c3fa48d 100644 --- a/tests/writer_data/match.json +++ b/tests/writer_data/match.json @@ -2,7 +2,7 @@ [ "match", "x", - [":dyn", "1"], + [[":dyn"], "1"], [[":dyn", "x"], "2"], [[":dyn", "x", "y"], "3"], [[":dyn", "x", "y", "z"], "4"] diff --git a/tests/writer_test.rs b/tests/writer_test.rs index 1654941..7abc4cc 100644 --- a/tests/writer_test.rs +++ b/tests/writer_test.rs @@ -206,7 +206,7 @@ fn format_match_without_bending_later_clauses() -> Result<(), String> { let xs = vec![Cirru::List(vec![ Cirru::leaf("match"), Cirru::leaf("x"), - Cirru::List(vec![Cirru::leaf(":dyn"), Cirru::leaf("1")]), + Cirru::List(vec![Cirru::List(vec![Cirru::leaf(":dyn")]), Cirru::leaf("1")]), Cirru::List(vec![Cirru::List(vec![Cirru::leaf(":dyn"), Cirru::leaf("x")]), Cirru::leaf("2")]), Cirru::List(vec![ Cirru::List(vec![Cirru::leaf(":dyn"), Cirru::leaf("x"), Cirru::leaf("y")]), @@ -220,10 +220,7 @@ fn format_match_without_bending_later_clauses() -> Result<(), String> { let rendered = format(&xs, CirruWriterOptions::from(false))?; - assert_eq!( - "\nmatch x\n :dyn 1\n (:dyn x) 2\n (:dyn x y) 3\n (:dyn x y z) 4\n", - rendered - ); + assert_eq!("\nmatch x\n (:dyn) 1\n (:dyn x) 2\n (:dyn x y) 3\n (:dyn x y z) 4\n", rendered); Ok(()) }