From 7a2dfce8dc595d4a3fc2ed14996286cc88747fa3 Mon Sep 17 00:00:00 2001 From: vladkvit Date: Sun, 1 Jun 2025 18:54:28 -0400 Subject: [PATCH 1/4] Add tests for desired new functionality --- src/comment_parsing.rs | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/comment_parsing.rs b/src/comment_parsing.rs index a556cb2..ef0b7bb 100644 --- a/src/comment_parsing.rs +++ b/src/comment_parsing.rs @@ -253,4 +253,57 @@ mod tests { assert_eq!(parsed, "12:34:56.12345"); assert_eq!(remaining, ""); } + + #[test] + fn test_unknown_tag_is_parsed_as_text() { + let input = "[%timestamp 12345] some text"; + let result = parse_comments(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!( + parsed, + vec![ + // The whole unknown tag becomes a text element + CommentContent::Text("[%timestamp 12345]".to_string()), + CommentContent::Text(" some text".to_string()) + ] + ); + assert_eq!(remaining, ""); + } + + #[test] + fn test_clk_tag_with_newline_spacing() { + let input = "[%clk\n0:09:36.2]"; + let result = parse_comments(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!(parsed, vec![CommentContent::ClkTime((0, 9, 36.2))]); + assert_eq!(remaining, ""); + } + + #[test] + fn test_eval_tag_with_newline_spacing() { + let input = "[%eval\n-0.5]"; + let result = parse_comments(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!(parsed, vec![CommentContent::Eval(-0.5)]); + assert_eq!(remaining, ""); + } + + #[test] + fn test_text_containing_brackets_not_tag() { + let input = "Text with [normal brackets] and then [%clk 1:2:3]"; + let result = parse_comments(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!( + parsed, + vec![ + CommentContent::Text("Text with [normal brackets] and then ".to_string()), + CommentContent::ClkTime((1, 2, 3.0)) + ] + ); + assert_eq!(remaining, ""); + } } From ebf6694afa06854a41bf4214a1a9da711e87aa8a Mon Sep 17 00:00:00 2001 From: vladkvit Date: Sun, 1 Jun 2025 19:09:16 -0400 Subject: [PATCH 2/4] tweak test name and comment --- src/comment_parsing.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/comment_parsing.rs b/src/comment_parsing.rs index ef0b7bb..2121806 100644 --- a/src/comment_parsing.rs +++ b/src/comment_parsing.rs @@ -191,8 +191,9 @@ mod tests { assert_eq!(remaining, ""); } + // clk expects a particular format: 00:00:00 #[test] - fn test_tag_parser_incorrect_name() { + fn test_tag_parser_incorrect_value() { let input = "[%clk 123]"; let result = tag_parser(input); assert!(result.is_err()); From 600149b637106defc3c7f066f0f4cbacae903910 Mon Sep 17 00:00:00 2001 From: vladkvit Date: Sun, 1 Jun 2025 20:41:07 -0400 Subject: [PATCH 3/4] fairly large refactor of comment parsing --- src/comment_parsing.rs | 357 ++++++++++++++++++++++++++--------------- src/lib.rs | 58 ++++--- 2 files changed, 264 insertions(+), 151 deletions(-) diff --git a/src/comment_parsing.rs b/src/comment_parsing.rs index 2121806..2f7cc3e 100644 --- a/src/comment_parsing.rs +++ b/src/comment_parsing.rs @@ -2,124 +2,132 @@ use nom::{ branch::alt, bytes::complete::{is_not, tag}, character::complete::{char, digit1}, - combinator::{map, opt, recognize}, + combinator::{map, map_res, opt, recognize}, multi::{many0, many1}, sequence::{delimited, pair, preceded}, IResult, Parser, }; +#[derive(Debug, PartialEq)] +pub enum ParsedTag { + Eval(f64), + Mate(i32), + ClkTime { + hours: u32, + minutes: u8, + seconds: f64, + }, +} + #[derive(Debug, PartialEq)] pub enum CommentContent { Text(String), - Eval(f64), - ClkTime((u32, u8, f64)), + Tag(ParsedTag), } pub fn parse_comments(input: &str) -> IResult<&str, Vec> { many0(alt(( - map(tag_parser, |s| match s.as_str() { - eval if eval.starts_with("[eval ") => { - let value = &eval[6..eval.len() - 1]; - CommentContent::Eval(value.parse().unwrap_or_default()) - } - clk if clk.starts_with("[clk ") => { - let time_parts: Vec<&str> = clk[5..clk.len() - 1].split(':').collect(); - let hours = time_parts - .get(0) - .and_then(|h| h.parse().ok()) - .unwrap_or_default(); - let minutes = time_parts - .get(1) - .and_then(|m| m.parse().ok()) - .unwrap_or_default(); - let seconds = time_parts - .get(2) - .and_then(|s| s.parse().ok()) - .unwrap_or_default(); - CommentContent::ClkTime((hours, minutes, seconds)) - } - _ => unreachable!(), - }), - map(text, |s| CommentContent::Text(s.to_string())), + map(parse_structured_tag, CommentContent::Tag), + map(text_content, |s| CommentContent::Text(s.to_string())), ))) .parse(input) } -/// Parser for a tag -fn tag_parser(input: &str) -> IResult<&str, String> { +/// Parser for a complete tag like [%eval ...] or [%clk ...] +fn parse_structured_tag(input: &str) -> IResult<&str, ParsedTag> { delimited( - (char('['), char('%')), - alt((eval_parser, clk_parser)), + tag("[%"), + alt((parse_eval_content, parse_clk_content)), char(']'), ) .parse(input) } -/// Parser for an eval tag -fn eval_parser(input: &str) -> IResult<&str, String> { - map( - (tag("eval"), spacing, alt((signed_number, mate_eval))), - |(_, _, value)| format!("[eval {}]", value), +/// Parses the content of an eval tag, e.g., "eval 12.3" or "eval #3" +fn parse_eval_content(input: &str) -> IResult<&str, ParsedTag> { + preceded( + tag("eval"), + preceded( + spacing, + alt(( + map(parse_mate_value, ParsedTag::Mate), + map(parse_signed_float, ParsedTag::Eval), + )), + ), ) .parse(input) } -/// Parser for a clk tag -fn clk_parser(input: &str) -> IResult<&str, String> { - map((tag("clk"), spacing, time_value), |(_, _, value)| { - format!("[clk {}]", value) - }) +/// Parses the content of a clk tag, e.g., "clk 1:23:45.6" +fn parse_clk_content(input: &str) -> IResult<&str, ParsedTag> { + preceded( + tag("clk"), + preceded( + spacing, + map(parse_hms_time, |(h, m, s)| ParsedTag::ClkTime { + hours: h, + minutes: m, + seconds: s, + }), + ), + ) .parse(input) } -/// Parser for a signed number -fn signed_number(input: &str) -> IResult<&str, String> { - map( +/// Parser for a signed floating-point number, e.g., "-123.45", "+3.0", "7" +fn parse_signed_float(input: &str) -> IResult<&str, f64> { + map_res( recognize(pair( opt(alt((char('+'), char('-')))), recognize(pair(digit1, opt(preceded(char('.'), digit1)))), )), - |s: &str| s.to_string(), + |s: &str| s.parse::(), ) .parse(input) } -fn mate_eval(input: &str) -> IResult<&str, String> { - let signed_integer = recognize(( - opt(char('-')), // Optional minus sign - digit1, // One or more digits - )); - map(preceded(char('#'), signed_integer), String::from).parse(input) +/// Parser for a mate value, e.g., "#-3", "#5" +fn parse_mate_value(input: &str) -> IResult<&str, i32> { + preceded( + char('#'), + map_res( + recognize(pair(opt(char('-')), digit1)), // Recognizes signed integer + |s: &str| s.parse::(), + ), + ) + .parse(input) } -/// Parser for a time value -fn time_value(input: &str) -> IResult<&str, String> { +/// Parser for a time value in H:M:S format, e.g., "12:34:56" or "1:2:3.45" +fn parse_hms_time(input: &str) -> IResult<&str, (u32, u8, f64)> { map( ( - digit1, // Hours - char(':'), // Colon separator - digit1, // Minutes - char(':'), // Colon separator - digit1, // Seconds - opt(preceded( - char('.'), // Dot separator - digit1, // Fractional seconds - )), + map_res(digit1, |s: &str| s.parse::()), // Hours + char(':'), + map_res(digit1, |s: &str| s.parse::()), // Minutes (0-255, typically 0-59) + char(':'), + parse_seconds_with_fraction, // Seconds with optional fraction ), - |(h, _, m, _, s, frac)| match frac { - Some(f) => format!("{}:{}:{}.{}", h, m, s, f), - None => format!("{}:{}:{}", h, m, s), - }, + |(h, _, m, _, s)| (h, m, s), ) .parse(input) } -/// Parser for text (any characters except '[' and ']') -fn text(input: &str) -> IResult<&str, &str> { +/// Parser for seconds, which can be an integer or have a fractional part +fn parse_seconds_with_fraction(input: &str) -> IResult<&str, f64> { + map_res( + recognize(pair(digit1, opt(preceded(char('.'), digit1)))), + |s: &str| s.parse::(), + ) + .parse(input) +} + +/// Parser for text content (any characters except '[' and ']') +fn text_content(input: &str) -> IResult<&str, &str> { is_not("[]").parse(input) } -/// Parser for spacing (one or more spaces) +/// Parser for one or more spaces fn spacing(input: &str) -> IResult<&str, &str> { recognize(many1(char(' '))).parse(input) } @@ -137,9 +145,13 @@ mod tests { assert_eq!( parsed, vec![ - CommentContent::Eval(123.0), + CommentContent::Tag(ParsedTag::Eval(123.0)), CommentContent::Text(" some text ".to_string()), - CommentContent::ClkTime((12, 34, 56.0)) + CommentContent::Tag(ParsedTag::ClkTime { + hours: 12, + minutes: 34, + seconds: 56.0 + }) ] ); assert_eq!(remaining, ""); @@ -154,7 +166,11 @@ mod tests { assert_eq!( parsed, vec![ - CommentContent::ClkTime((12, 34, 56.0)), + CommentContent::Tag(ParsedTag::ClkTime { + hours: 12, + minutes: 34, + seconds: 56.0 + }), CommentContent::Text(" some text ".to_string()) ] ); @@ -167,58 +183,124 @@ mod tests { let result = parse_comments(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); - assert_eq!(parsed, vec![CommentContent::ClkTime((12, 34, 56.0123)),]); + assert_eq!( + parsed, + vec![CommentContent::Tag(ParsedTag::ClkTime { + hours: 12, + minutes: 34, + seconds: 56.0123 + })] + ); assert_eq!(remaining, ""); } #[test] - fn test_tag_parser() { + fn test_parse_structured_tag_eval() { let input = "[%eval 123]"; - let result = tag_parser(input); + let result = parse_structured_tag(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); - assert_eq!(parsed, "[eval 123]"); + assert_eq!(parsed, ParsedTag::Eval(123.0)); assert_eq!(remaining, ""); } #[test] - fn test_eval_mate() { + fn test_parse_structured_tag_eval_mate() { let input = "[%eval #-3]"; - let result = tag_parser(input); + let result = parse_structured_tag(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); - assert_eq!(parsed, "[eval -3]"); // TODO mark the mates + assert_eq!(parsed, ParsedTag::Mate(-3)); assert_eq!(remaining, ""); } - // clk expects a particular format: 00:00:00 #[test] - fn test_tag_parser_incorrect_value() { - let input = "[%clk 123]"; - let result = tag_parser(input); - assert!(result.is_err()); + fn test_parse_structured_tag_clk() { + let input = "[%clk 1:2:3.45]"; + let result = parse_structured_tag(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!( + parsed, + ParsedTag::ClkTime { + hours: 1, + minutes: 2, + seconds: 3.45 + } + ); + assert_eq!(remaining, ""); + } + + // clk expects a particular format: H:M:S - this test ensures incorrect formats fail + #[test] + fn test_tag_parser_incorrect_clk_value() { + let input = "[%clk 123]"; // Incorrect format for clk time + let result = parse_structured_tag(input); + assert!( + result.is_err(), + "Parser should fail for incorrect clk format" + ); } #[test] - fn test_clk_parser() { + fn test_tag_parser_incorrect_eval_value() { + let input = "[%eval notanumber]"; + let result = parse_structured_tag(input); + assert!( + result.is_err(), + "Parser should fail for non-numeric eval value" + ); + } + + #[test] + fn test_parse_clk_content_correct() { let input = "clk 12:34:56"; - let result = clk_parser(input); + let result = parse_clk_content(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); - assert_eq!(parsed, "[clk 12:34:56]"); + assert_eq!( + parsed, + ParsedTag::ClkTime { + hours: 12, + minutes: 34, + seconds: 56.0 + } + ); assert_eq!(remaining, ""); } + #[test] - fn test_clk_parser_incorrect_name() { + fn test_parse_clk_content_incorrect_name() { + // This tests if parse_clk_content correctly fails if "clk" is not the prefix let input = "eval 123"; - let result = clk_parser(input); + let result = parse_clk_content(input); assert!(result.is_err()); } #[test] - fn test_text() { + fn test_parse_eval_content_correct_float() { + let input = "eval -42.5"; + let result = parse_eval_content(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!(parsed, ParsedTag::Eval(-42.5)); + assert_eq!(remaining, ""); + } + + #[test] + fn test_parse_eval_content_correct_mate() { + let input = "eval #5"; + let result = parse_eval_content(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!(parsed, ParsedTag::Mate(5)); + assert_eq!(remaining, ""); + } + + #[test] + fn test_text_content() { let input = "some text"; - let result = text(input); + let result = text_content(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); assert_eq!(parsed, "some text"); @@ -226,85 +308,96 @@ mod tests { } #[test] - fn test_signed_number() { - let input = "-123.45"; - let result = signed_number(input); + fn test_parse_signed_float_positive() { + let input = "123.45"; + let result = parse_signed_float(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); - assert_eq!(parsed, "-123.45"); + assert_eq!(parsed, 123.45); assert_eq!(remaining, ""); } #[test] - fn test_time_value() { - let input = "12:34:56"; - let result = time_value(input); + fn test_parse_signed_float_negative() { + let input = "-0.5"; + let result = parse_signed_float(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); - assert_eq!(parsed, "12:34:56"); + assert_eq!(parsed, -0.5); assert_eq!(remaining, ""); } #[test] - fn test_time_value_fractional() { - let input = "12:34:56.12345"; - let result = time_value(input); + fn test_parse_mate_value_positive() { + let input = "#7"; + let result = parse_mate_value(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); - assert_eq!(parsed, "12:34:56.12345"); + assert_eq!(parsed, 7); assert_eq!(remaining, ""); } #[test] - fn test_unknown_tag_is_parsed_as_text() { - let input = "[%timestamp 12345] some text"; - let result = parse_comments(input); + fn test_parse_mate_value_negative() { + let input = "#-2"; + let result = parse_mate_value(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); - assert_eq!( - parsed, - vec![ - // The whole unknown tag becomes a text element - CommentContent::Text("[%timestamp 12345]".to_string()), - CommentContent::Text(" some text".to_string()) - ] - ); + assert_eq!(parsed, -2); assert_eq!(remaining, ""); } #[test] - fn test_clk_tag_with_newline_spacing() { - let input = "[%clk\n0:09:36.2]"; - let result = parse_comments(input); + fn test_parse_hms_time_simple() { + let input = "12:34:56"; + let result = parse_hms_time(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); - assert_eq!(parsed, vec![CommentContent::ClkTime((0, 9, 36.2))]); + assert_eq!(parsed, (12, 34, 56.0)); assert_eq!(remaining, ""); } #[test] - fn test_eval_tag_with_newline_spacing() { - let input = "[%eval\n-0.5]"; - let result = parse_comments(input); + fn test_parse_hms_time_fractional() { + let input = "01:02:03.123"; + let result = parse_hms_time(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); - assert_eq!(parsed, vec![CommentContent::Eval(-0.5)]); + assert_eq!(parsed, (1, 2, 3.123)); assert_eq!(remaining, ""); } #[test] - fn test_text_containing_brackets_not_tag() { - let input = "Text with [normal brackets] and then [%clk 1:2:3]"; - let result = parse_comments(input); + fn test_parse_hms_time_invalid_char_in_hour() { + let input = "1a:00:00"; + let result = parse_hms_time(input); + assert!(result.is_err()); + } + + #[test] + fn test_parse_hms_time_missing_minutes() { + let input = "1::00"; + let result = parse_hms_time(input); // digit1 for minutes will fail on ':' + assert!(result.is_err()); + } + + #[test] + fn test_parse_seconds_with_fraction_integer() { + let input = "42"; + let result = parse_seconds_with_fraction(input); assert!(result.is_ok()); let (remaining, parsed) = result.unwrap(); - assert_eq!( - parsed, - vec![ - CommentContent::Text("Text with [normal brackets] and then ".to_string()), - CommentContent::ClkTime((1, 2, 3.0)) - ] - ); + assert_eq!(parsed, 42.0); + assert_eq!(remaining, ""); + } + + #[test] + fn test_parse_seconds_with_fraction_decimal() { + let input = "3.141"; + let result = parse_seconds_with_fraction(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!(parsed, 3.141); assert_eq!(remaining, ""); } } diff --git a/src/lib.rs b/src/lib.rs index f290104..65b4de0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ -use crate::comment_parsing::parse_comments; -use crate::comment_parsing::CommentContent; +use crate::comment_parsing::{parse_comments, CommentContent, ParsedTag}; use arrow_array::{Array, LargeStringArray, StringArray}; use pgn_reader::{BufferedReader, RawComment, RawHeader, SanPlus, Skip, Visitor}; use pyo3::prelude::*; @@ -253,25 +252,46 @@ impl Visitor for MoveExtractor { move_comments.push_str(&text); } } - CommentContent::Eval(eval_value) => { - if eval_encountered { - eprintln!("Multiple Eval values found in comment: {:?}", _comment); - return; + CommentContent::Tag(tag_content) => match tag_content { + ParsedTag::Eval(eval_value) => { + if eval_encountered { + eprintln!( + "Multiple Eval values found in comment: {:?}", + _comment + ); + // Potentially skip this eval or handle as an error + continue; + } + eval_encountered = true; + self.evals.push(eval_value); } - eval_encountered = true; - self.evals.push(eval_value); - } - CommentContent::ClkTime(clk_time) => { - if clk_time_encountered { - eprintln!( - "Multiple ClkTime values found in comment: {:?}", - _comment - ); - return; + ParsedTag::Mate(mate_value) => { + // For now, add mate info to the general comments string + // A dedicated field could be added to MoveExtractor if needed + if !move_comments.is_empty() && !move_comments.ends_with(' ') { + move_comments.push(' '); + } + move_comments.push_str(&format!("[Mate {}]", mate_value)); + // If evals should also store mate, convert mate_value to f64 + // e.g., self.evals.push(mate_value as f64 * 1000.0); // Or some indicator } - clk_time_encountered = true; - self.clock_times.push(clk_time); - } + ParsedTag::ClkTime { + hours, + minutes, + seconds, + } => { + if clk_time_encountered { + eprintln!( + "Multiple ClkTime values found in comment: {:?}", + _comment + ); + // Potentially skip this clk or handle as an error + continue; + } + clk_time_encountered = true; + self.clock_times.push((hours, minutes, seconds)); + } + }, } } self.comments.push(move_comments); From 22baef1c0eb306daf477f9b08ec21fe920bc3f74 Mon Sep 17 00:00:00 2001 From: vladkvit Date: Sun, 1 Jun 2025 20:56:38 -0400 Subject: [PATCH 4/4] Add better handling of newlines and unknown tags --- src/comment_parsing.rs | 103 +++++++++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 20 deletions(-) diff --git a/src/comment_parsing.rs b/src/comment_parsing.rs index 2f7cc3e..6d966a1 100644 --- a/src/comment_parsing.rs +++ b/src/comment_parsing.rs @@ -1,8 +1,8 @@ use nom::{ branch::alt, bytes::complete::{is_not, tag}, - character::complete::{char, digit1}, - combinator::{map, map_res, opt, recognize}, + character::complete::{char, digit1, multispace1}, + combinator::{map, map_res, not, opt, peek, recognize}, multi::{many0, many1}, sequence::{delimited, pair, preceded}, IResult, Parser, @@ -27,8 +27,22 @@ pub enum CommentContent { pub fn parse_comments(input: &str) -> IResult<&str, Vec> { many0(alt(( + // Attempt to parse a known structured tag first map(parse_structured_tag, CommentContent::Tag), - map(text_content, |s| CommentContent::Text(s.to_string())), + // If not a known tag, but looks like a tag (e.g. [%unknown ...]), parse as text + map( + recognize(delimited(tag("[%"), is_not("]"), char(']'))), + |s: &str| CommentContent::Text(s.to_string()), + ), + // Otherwise, parse as regular text content. This must not be empty. + map( + recognize(many1(alt(( + is_not("["), // Takes any char except '[' + // Takes a '[' if it's NOT followed by '%' (to allow "[abc]" as text) + recognize(preceded(char('['), peek(not(char('%'))))), + )))), + |s: &str| CommentContent::Text(s.to_string()), + ), ))) .parse(input) } @@ -122,14 +136,9 @@ fn parse_seconds_with_fraction(input: &str) -> IResult<&str, f64> { .parse(input) } -/// Parser for text content (any characters except '[' and ']') -fn text_content(input: &str) -> IResult<&str, &str> { - is_not("[]").parse(input) -} - -/// Parser for one or more spaces +/// Parser for one or more whitespace characters (spaces, newlines, tabs, etc.) fn spacing(input: &str) -> IResult<&str, &str> { - recognize(many1(char(' '))).parse(input) + multispace1(input) } #[cfg(test)] @@ -297,16 +306,6 @@ mod tests { assert_eq!(remaining, ""); } - #[test] - fn test_text_content() { - let input = "some text"; - let result = text_content(input); - assert!(result.is_ok()); - let (remaining, parsed) = result.unwrap(); - assert_eq!(parsed, "some text"); - assert_eq!(remaining, ""); - } - #[test] fn test_parse_signed_float_positive() { let input = "123.45"; @@ -400,4 +399,68 @@ mod tests { assert_eq!(parsed, 3.141); assert_eq!(remaining, ""); } + + #[test] + fn test_unknown_tag_is_parsed_as_text() { + let input = "[%timestamp 12345] some text"; + let result = parse_comments(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!( + parsed, + vec![ + // The whole unknown tag becomes a text element + CommentContent::Text("[%timestamp 12345]".to_string()), + CommentContent::Text(" some text".to_string()) // Note: leading space is part of the text + ] + ); + assert_eq!(remaining, ""); + } + + #[test] + fn test_clk_tag_with_newline_spacing() { + let input = "[%clk\n0:09:36.2]"; + let result = parse_comments(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!( + parsed, + vec![CommentContent::Tag(ParsedTag::ClkTime { + hours: 0, + minutes: 9, + seconds: 36.2 + })] + ); + assert_eq!(remaining, ""); + } + + #[test] + fn test_eval_tag_with_newline_spacing() { + let input = "[%eval\n-0.5]"; + let result = parse_comments(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!(parsed, vec![CommentContent::Tag(ParsedTag::Eval(-0.5))]); + assert_eq!(remaining, ""); + } + + #[test] + fn test_text_containing_brackets_not_tag() { + let input = "Text with [normal brackets] and then [%clk 1:2:3]"; + let result = parse_comments(input); + assert!(result.is_ok()); + let (remaining, parsed) = result.unwrap(); + assert_eq!( + parsed, + vec![ + CommentContent::Text("Text with [normal brackets] and then ".to_string()), + CommentContent::Tag(ParsedTag::ClkTime { + hours: 1, + minutes: 2, + seconds: 3.0 + }) + ] + ); + assert_eq!(remaining, ""); + } }