const NOT_FI_TERMINATED: &[&str] = &[
"iff", "ifthenelse", "iflanguage", "iftoggle", "ifdef",
"ifcsdef",
"ifundef",
"ifcsundef",
"ifdefmacro",
"ifcsmacro",
"ifdefempty",
"ifcsempty",
"ifdefvoid",
"ifcsvoid",
"ifdefstring",
"ifcsstring",
"ifdefequal",
"ifcsequal",
"ifbool",
"ifboolexpr",
"ifboolexpe",
"ifstrequal",
"ifstrempty",
"ifblank",
"ifnumcomp",
"ifnumequal",
"ifnumgreater",
"ifnumless",
"ifnumodd",
"ifdimcomp",
"ifdimequal",
"ifdimgreater",
"ifdimless",
];
const OPERAND_SKIPS: &[(&str, u8)] = &[
("if", 2),
("ifx", 2),
("ifcat", 2),
("ifdefined", 1),
("newif", 1),
("let", 2),
];
const CSNAME_OPENER: &str = "ifcsname";
const CSNAME_CLOSER: &str = "endcsname";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlowWord {
Else,
Or,
Fi,
}
pub fn flow_word(name: &str) -> Option<FlowWord> {
match name {
"else" => Some(FlowWord::Else),
"or" => Some(FlowWord::Or),
"fi" => Some(FlowWord::Fi),
_ => None,
}
}
pub fn is_conditional_opener(name: &str) -> bool {
name.starts_with("if") && !NOT_FI_TERMINATED.contains(&name)
}
pub fn operand_skips(name: &str) -> Option<u8> {
OPERAND_SKIPS
.iter()
.find(|(op, _)| *op == name)
.map(|&(_, n)| n)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Word {
Opens,
Flow(FlowWord),
Inert,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct OpenerScan {
pending_skips: u8,
in_csname: bool,
}
impl OpenerScan {
pub fn new() -> Self {
Self::default()
}
pub fn visit(&mut self, name: &str) -> Word {
let flow = flow_word(name);
if self.in_csname {
if name == CSNAME_CLOSER {
self.in_csname = false;
return Word::Inert;
}
if flow.is_none() {
return Word::Inert;
}
self.in_csname = false;
}
if self.pending_skips > 0 {
if flow.is_none() {
self.pending_skips -= 1;
return Word::Inert;
}
self.pending_skips = 0;
}
if let Some(flow) = flow {
return Word::Flow(flow);
}
let opens = is_conditional_opener(name);
if opens && name == CSNAME_OPENER {
self.in_csname = true;
} else if let Some(n) = operand_skips(name) {
self.pending_skips = n;
}
if opens { Word::Opens } else { Word::Inert }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scan(names: &[&str]) -> Vec<Word> {
let mut s = OpenerScan::new();
names.iter().map(|n| s.visit(n)).collect()
}
#[test]
fn a_plain_conditional_opens_and_closes() {
assert_eq!(
scan(&["ifnum", "else", "fi"]),
[
Word::Opens,
Word::Flow(FlowWord::Else),
Word::Flow(FlowWord::Fi)
]
);
}
#[test]
fn newif_declares_rather_than_opens() {
assert_eq!(scan(&["newif", "if@foo"]), [Word::Inert, Word::Inert]);
}
#[test]
fn ifx_operands_are_inert_even_when_if_named() {
assert_eq!(
scan(&["ifx", "ifpdf", "iftrue"]),
[Word::Opens, Word::Inert, Word::Inert]
);
}
#[test]
fn let_aliases_two_tokens_without_opening() {
assert_eq!(
scan(&["let", "ifpdf", "iftrue"]),
[Word::Inert, Word::Inert, Word::Inert]
);
}
#[test]
fn brace_argument_tests_open_nothing() {
assert_eq!(
scan(&["ifthenelse", "ifnumgreater", "iftoggle", "iff"]),
[Word::Inert; 4]
);
}
#[test]
fn a_flow_word_cancels_a_pending_operand_run() {
assert_eq!(
scan(&["ifx", "ifone", "else", "ifnum"]),
[
Word::Opens,
Word::Inert,
Word::Flow(FlowWord::Else),
Word::Opens
]
);
}
#[test]
fn csname_bodies_hold_no_conditionals() {
assert_eq!(
scan(&["ifcsname", "ifnum", "endcsname", "ifdim"]),
[Word::Opens, Word::Inert, Word::Inert, Word::Opens]
);
}
#[test]
fn an_unclosed_csname_reopens_at_a_flow_word() {
assert_eq!(
scan(&["ifcsname", "ifnum", "fi", "ifdim"]),
[
Word::Opens,
Word::Inert,
Word::Flow(FlowWord::Fi),
Word::Opens
]
);
}
}