#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ParsedMnemonic {
pub stripped: String,
pub byte_index: Option<usize>,
pub char_index: Option<usize>,
pub key_lower: Option<char>,
}
impl ParsedMnemonic {
pub(crate) fn plain(s: &str) -> Self {
Self {
stripped: s.to_string(),
byte_index: None,
char_index: None,
key_lower: None,
}
}
pub(crate) fn has_mnemonic(&self) -> bool {
self.byte_index.is_some()
}
}
pub(crate) fn parse_mnemonic(s: &str) -> ParsedMnemonic {
let mut stripped = String::with_capacity(s.len());
let mut byte_index: Option<usize> = None;
let mut char_index: Option<usize> = None;
let mut key_lower: Option<char> = None;
let mut char_pos: usize = 0;
let mut chars = s.char_indices().peekable();
while let Some((_, c)) = chars.next() {
if c == '&' {
match chars.peek().copied() {
Some((_, '&')) => {
chars.next(); stripped.push('&');
char_pos += 1;
}
Some(_) if byte_index.is_none() => {
let (_, next_c) = chars.next().expect("peek was Some");
byte_index = Some(stripped.len());
char_index = Some(char_pos);
key_lower = Some(
next_c
.to_lowercase()
.next()
.unwrap_or(next_c)
.to_ascii_lowercase(),
);
stripped.push(next_c);
char_pos += 1;
}
Some(_) => {
stripped.push('&');
char_pos += 1;
}
None => {
stripped.push('&');
char_pos += 1;
}
}
} else {
stripped.push(c);
char_pos += 1;
}
}
ParsedMnemonic {
stripped,
byte_index,
char_index,
key_lower,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_no_marker() {
let p = parse_mnemonic("no marker");
assert_eq!(p.stripped, "no marker");
assert_eq!(p.byte_index, None);
assert_eq!(p.char_index, None);
assert_eq!(p.key_lower, None);
}
#[test]
fn parse_marker_at_start() {
let p = parse_mnemonic("&Save");
assert_eq!(p.stripped, "Save");
assert_eq!(p.byte_index, Some(0));
assert_eq!(p.char_index, Some(0));
assert_eq!(p.key_lower, Some('s'));
}
#[test]
fn parse_marker_in_middle() {
let p = parse_mnemonic("Sa&ve");
assert_eq!(p.stripped, "Save");
assert_eq!(p.byte_index, Some(2));
assert_eq!(p.char_index, Some(2));
assert_eq!(p.key_lower, Some('v'));
}
#[test]
fn parse_escaped_double_ampersand_collapses_to_literal() {
let p = parse_mnemonic("&&Save");
assert_eq!(p.stripped, "&Save");
assert_eq!(p.byte_index, None);
assert_eq!(p.char_index, None);
assert_eq!(p.key_lower, None);
}
#[test]
fn parse_escape_does_not_consume_real_marker_later() {
let p = parse_mnemonic("&&S&ave");
assert_eq!(p.stripped, "&Save");
assert_eq!(p.byte_index, Some(2));
assert_eq!(p.char_index, Some(2));
assert_eq!(p.key_lower, Some('a'));
}
#[test]
fn parse_second_unescaped_ampersand_kept_literal() {
let p = parse_mnemonic("&Sa&ve");
assert_eq!(p.stripped, "Sa&ve");
assert_eq!(p.byte_index, Some(0));
assert_eq!(p.char_index, Some(0));
assert_eq!(p.key_lower, Some('s'));
}
#[test]
fn parse_dangling_trailing_ampersand_kept_literal() {
let p = parse_mnemonic("trailing&");
assert_eq!(p.stripped, "trailing&");
assert_eq!(p.byte_index, None);
assert_eq!(p.key_lower, None);
}
#[test]
fn parse_empty_string() {
let p = parse_mnemonic("");
assert_eq!(p.stripped, "");
assert_eq!(p.byte_index, None);
assert_eq!(p.key_lower, None);
}
#[test]
fn parse_single_ampersand() {
let p = parse_mnemonic("&");
assert_eq!(p.stripped, "&");
assert_eq!(p.byte_index, None);
assert_eq!(p.key_lower, None);
}
#[test]
fn parse_multibyte_marked_character() {
let p = parse_mnemonic("&É");
assert_eq!(p.stripped, "É");
assert_eq!(p.byte_index, Some(0));
assert_eq!(p.char_index, Some(0));
assert_eq!(p.key_lower, Some('é'));
}
#[test]
fn parse_marker_before_multibyte_position() {
let p = parse_mnemonic("Café&!");
assert_eq!(p.stripped, "Café!");
assert_eq!(p.byte_index, Some(5));
assert_eq!(p.char_index, Some(4));
assert_eq!(p.key_lower, Some('!'));
}
#[test]
fn parse_byte_and_char_indices_differ_on_multibyte_prefix() {
let p = parse_mnemonic("é&q");
assert_eq!(p.stripped, "éq");
assert_eq!(p.byte_index, Some(2));
assert_eq!(p.char_index, Some(1));
assert_eq!(p.key_lower, Some('q'));
}
#[test]
fn parse_marker_is_case_insensitive_for_key_lower() {
let p = parse_mnemonic("&S");
assert_eq!(p.key_lower, Some('s'));
let p2 = parse_mnemonic("&s");
assert_eq!(p2.key_lower, Some('s'));
}
#[test]
fn has_mnemonic_true_only_when_marker_present() {
assert!(parse_mnemonic("&Save").has_mnemonic());
assert!(!parse_mnemonic("Save").has_mnemonic());
assert!(!parse_mnemonic("&&Save").has_mnemonic());
}
#[test]
fn plain_constructor_has_no_mnemonic() {
let p = ParsedMnemonic::plain("Hello");
assert_eq!(p.stripped, "Hello");
assert!(!p.has_mnemonic());
}
}