///|
/// Tokens produced by the cucumber expression tokenizer.
pub(all) enum Token {
/// Literal text content.
Text(String)
/// Opening brace `{` — start of parameter.
BeginParameter
/// Closing brace `}` — end of parameter.
EndParameter
/// Opening parenthesis `(` — start of optional.
BeginOptional
/// Closing parenthesis `)` — end of optional.
EndOptional
/// Forward slash `/` — alternation separator.
Alternation
/// Whitespace run.
WhiteSpace(String)
} derive(Show, Eq)
///|
/// Tokenize a cucumber expression string into an array of tokens.
///
/// Converts the raw expression into a flat token stream. Does not validate
/// matching braces or parentheses — that is the parser's responsibility.
pub fn tokenize(expression : String) -> Array[Token] raise ExpressionError {
let tokens : Array[Token] = []
let buf = StringBuilder::new()
let chars = expression.to_array()
let len = chars.length()
let mut i = 0
while i < len {
let ch = chars[i]
match ch {
'\\' => {
if i + 1 >= len {
raise ExpressionError::UnexpectedEscapeEnd(
position=i,
message="The end of line can not be escaped",
)
}
let next = chars[i + 1]
if is_escapable(next) {
buf.write_char(next)
i = i + 2
} else {
raise ExpressionError::CannotEscape(
position=i,
character=next,
message="Can't escape '\{next}'",
)
}
}
'{' => {
flush_text(tokens, buf)
tokens.push(BeginParameter)
i = i + 1
}
'}' => {
flush_text(tokens, buf)
tokens.push(EndParameter)
i = i + 1
}
'(' => {
flush_text(tokens, buf)
tokens.push(BeginOptional)
i = i + 1
}
')' => {
flush_text(tokens, buf)
tokens.push(EndOptional)
i = i + 1
}
'/' => {
flush_text(tokens, buf)
tokens.push(Alternation)
i = i + 1
}
_ =>
if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
flush_text(tokens, buf)
let ws = StringBuilder::new()
ws.write_char(ch)
i = i + 1
while i < len {
let wch = chars[i]
if wch == ' ' || wch == '\t' || wch == '\n' || wch == '\r' {
ws.write_char(wch)
i = i + 1
} else {
break
}
}
tokens.push(WhiteSpace(ws.to_string()))
} else {
buf.write_char(ch)
i = i + 1
}
}
}
flush_text(tokens, buf)
tokens
}
///|
fn flush_text(tokens : Array[Token], buf : StringBuilder) -> Unit {
let s = buf.to_string()
if s.length() > 0 {
tokens.push(Text(s))
buf.reset()
}
}
///|
fn is_escapable(ch : Char) -> Bool {
match ch {
'(' | ')' | '{' | '}' | '/' | '\\' | ' ' => true
_ => false
}
}