use regex::Regex;
use std::sync::LazyLock;
static TAG_TOKEN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"`(@[^`]+)`").unwrap());
pub const MIN_TABLE_INDENT: usize = 2;
pub const MAX_TABLE_INDENT: usize = 5;
pub fn is_table_row(line: &str) -> bool {
let indent = line.bytes().take_while(|&byte| byte == b' ' || byte == b'\t').count();
(MIN_TABLE_INDENT..=MAX_TABLE_INDENT).contains(&indent) && line.as_bytes().get(indent) == Some(&b'|')
}
pub fn is_tag_line(line: &str) -> bool {
TAG_TOKEN.is_match(line)
}
pub fn keyword_split(text: &str) -> Option<(&str, &str)> {
let colon = text.find(':')?;
(!text[..colon].contains('`')).then(|| text.split_at(colon + 1))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_row_accepts_gherkins_whole_indent_range() {
for indent in [" ", " ", " ", " ", "\t\t", " \t", "\t \t"] {
assert!(
is_table_row(&format!("{indent}| a | b |")),
"{indent:?} indents a table"
);
}
}
#[test]
fn table_row_rejects_an_indent_outside_the_range() {
for indent in ["", " ", " ", "\t\t\t\t\t\t"] {
assert!(
!is_table_row(&format!("{indent}| a | b |")),
"{indent:?} is outside Gherkin's range"
);
}
}
#[test]
fn table_row_needs_a_pipe_behind_its_indent() {
for line in [" > | a | b |", " - | a | b |", " text | a |", " ", " "] {
assert!(!is_table_row(line), "{line:?} is not a table row");
}
}
#[test]
fn tag_line_matches_gherkin_reference_scan() {
for line in [
"`@browser`",
"`@checkout` `@smoke`",
" `@a`\t`@b` ",
"`@a``@b`",
"`@comment_tag1` #a comment",
"prose `@a` after",
"`@a b`",
"`@comment_tag#2` #a comment",
] {
assert!(is_tag_line(line), "{line:?} is a tag line");
}
}
#[test]
fn tag_line_requires_at_least_one_complete_wrapped_tag() {
for line in ["", " ", "plain prose", "@browser", "`browser`", "`@`", "`@a"] {
assert!(!is_tag_line(line), "{line:?} is not a tag line");
}
}
#[test]
fn keyword_split_keeps_the_colon_with_the_keyword() {
assert_eq!(keyword_split("Feature: Checkout"), Some(("Feature:", " Checkout")));
assert_eq!(keyword_split("Scenario:name"), Some(("Scenario:", "name")));
assert_eq!(keyword_split("Examples:"), Some(("Examples:", "")));
}
#[test]
fn keyword_split_takes_the_first_colon() {
assert_eq!(keyword_split("Scenario: a: b"), Some(("Scenario:", " a: b")));
}
#[test]
fn keyword_split_declines_a_colon_behind_a_backtick() {
assert_eq!(keyword_split("A `b: c` d"), None);
assert_eq!(keyword_split("`a: b"), None);
}
#[test]
fn keyword_split_declines_text_without_a_colon() {
assert_eq!(keyword_split("Notes"), None);
assert_eq!(keyword_split(""), None);
}
#[test]
fn keyword_split_takes_a_keyword_colon_that_precedes_a_backtick() {
assert_eq!(keyword_split("Scenario: a `b` c"), Some(("Scenario:", " a `b` c")));
}
}