use super::signature::{ArgKind, ArgSpec};
pub fn parse_spec(spec: &str) -> Vec<ArgSpec> {
let chars: Vec<char> = spec.chars().collect();
let mut cursor = Cursor {
chars: &chars,
i: 0,
};
let mut args = Vec::new();
loop {
cursor.skip_modifiers();
cursor.skip_ws();
let Some(c) = cursor.bump() else { break };
match c {
'm' => args.push(brace(true)),
'o' => args.push(bracket(false)),
'O' => {
cursor.skip_group();
args.push(bracket(false));
}
'r' | 'R' | 'd' | 'D' => {
let required = matches!(c, 'r' | 'R');
let open = cursor.read_token();
let close = cursor.read_token();
if matches!(c, 'R' | 'D') {
cursor.skip_group(); }
if let Some(kind) = delimiter_kind(open.as_deref(), close.as_deref()) {
args.push(ArgSpec {
required,
kind,
prose: false,
collapse: false,
});
}
}
't' => {
cursor.read_token(); }
'e' => {
cursor.skip_group(); }
'E' => {
cursor.skip_group(); cursor.skip_group(); }
's' | 'v' => {}
_ => break,
}
}
args
}
fn brace(required: bool) -> ArgSpec {
ArgSpec {
required,
kind: ArgKind::Brace,
prose: false,
collapse: false,
}
}
fn bracket(required: bool) -> ArgSpec {
ArgSpec {
required,
kind: ArgKind::Bracket,
prose: false,
collapse: false,
}
}
fn delimiter_kind(open: Option<&str>, close: Option<&str>) -> Option<ArgKind> {
match (open, close) {
(Some("["), Some("]")) => Some(ArgKind::Bracket),
(Some("{"), Some("}")) => Some(ArgKind::Brace),
_ => None,
}
}
struct Cursor<'a> {
chars: &'a [char],
i: usize,
}
impl Cursor<'_> {
fn peek(&self) -> Option<char> {
self.chars.get(self.i).copied()
}
fn bump(&mut self) -> Option<char> {
let c = self.peek()?;
self.i += 1;
Some(c)
}
fn skip_ws(&mut self) {
while self.peek().is_some_and(char::is_whitespace) {
self.i += 1;
}
}
fn skip_modifiers(&mut self) {
loop {
self.skip_ws();
match self.peek() {
Some('+') | Some('!') => self.i += 1,
Some('>') => {
self.i += 1;
self.skip_group();
}
_ => break,
}
}
}
fn read_token(&mut self) -> Option<String> {
self.skip_ws();
let first = self.bump()?;
if first != '\\' {
return Some(first.to_string());
}
let mut token = String::from('\\');
match self.peek() {
Some(c) if c.is_ascii_alphabetic() => {
while self.peek().is_some_and(|c| c.is_ascii_alphabetic()) {
token.push(self.bump().expect("peeked"));
}
}
Some(_) => token.push(self.bump().expect("peeked")),
None => {}
}
Some(token)
}
fn skip_group(&mut self) {
self.skip_ws();
if self.peek() != Some('{') {
return;
}
let mut depth = 0;
while let Some(c) = self.bump() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return;
}
}
_ => {}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn kinds(spec: &str) -> Vec<(bool, ArgKind)> {
parse_spec(spec)
.into_iter()
.map(|a| (a.required, a.kind))
.collect()
}
#[test]
fn mandatory_and_optional_basics() {
assert_eq!(
kinds("m o m"),
vec![
(true, ArgKind::Brace),
(false, ArgKind::Bracket),
(true, ArgKind::Brace),
]
);
}
#[test]
fn optional_with_default_consumes_group() {
assert_eq!(
kinds("O{0} m"),
vec![(false, ArgKind::Bracket), (true, ArgKind::Brace)]
);
}
#[test]
fn star_and_token_yield_no_slot() {
assert_eq!(kinds("s m"), vec![(true, ArgKind::Brace)]);
assert_eq!(kinds("t* m"), vec![(true, ArgKind::Brace)]);
}
#[test]
fn verbatim_yields_no_slot() {
assert_eq!(kinds("v"), vec![]);
}
#[test]
fn bracket_delimited_maps_to_bracket() {
assert_eq!(kinds("d[]"), vec![(false, ArgKind::Bracket)]);
assert_eq!(kinds("r[]"), vec![(true, ArgKind::Bracket)]);
}
#[test]
fn paren_delimited_yields_no_slot() {
assert_eq!(kinds("d() m"), vec![(true, ArgKind::Brace)]);
assert_eq!(kinds("r<> m"), vec![(true, ArgKind::Brace)]);
}
#[test]
fn required_delimited_with_default_consumes_group() {
assert_eq!(kinds("R(){x} m"), vec![(true, ArgKind::Brace)]);
assert_eq!(
kinds("D[]{x} m"),
vec![(false, ArgKind::Bracket), (true, ArgKind::Brace)]
);
}
#[test]
fn embellishments_consume_their_groups() {
assert_eq!(kinds("e{^_} m"), vec![(true, ArgKind::Brace)]);
assert_eq!(kinds("E{^_}{00} m"), vec![(true, ArgKind::Brace)]);
}
#[test]
fn modifiers_and_processors_skipped() {
assert_eq!(kinds("+m"), vec![(true, ArgKind::Brace)]);
assert_eq!(kinds("!o"), vec![(false, ArgKind::Bracket)]);
assert_eq!(kinds(">{\\TrimSpaces} m"), vec![(true, ArgKind::Brace)]);
}
#[test]
fn empty_and_whitespace_specs() {
assert_eq!(kinds(""), vec![]);
assert_eq!(kinds(" "), vec![]);
assert_eq!(
kinds(" m o "),
vec![(true, ArgKind::Brace), (false, ArgKind::Bracket)]
);
}
#[test]
fn unknown_letter_stops_scan() {
assert_eq!(kinds("m z m"), vec![(true, ArgKind::Brace)]);
}
#[test]
fn control_sequence_delimiters_consumed() {
assert_eq!(kinds("d\\langle\\rangle m"), vec![(true, ArgKind::Brace)]);
}
}