use std::borrow::Cow;
use crate::{Result, EdError};
#[derive(Debug)]
#[cfg_attr(feature="serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature="serde", serde(rename_all="lowercase"))]
pub enum NrArguments {
Any,
None,
Exactly(usize),
Between{incl_min: usize, incl_max: usize},
}
#[derive(Debug)]
#[cfg_attr(feature="serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature="serde", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct Macro {
pub input: Cow<'static, str>,
pub nr_arguments: NrArguments,
}
impl Macro {
pub fn new<T: Into<Cow<'static, str>>>(
input: T,
) -> Self {
Self{
input: input.into(),
nr_arguments: NrArguments::Any,
}
}
pub fn nr_arguments(mut self, nr: NrArguments) -> Self {
self.nr_arguments = nr;
self
}
}
pub trait MacroGetter {
fn get_macro(&self, name: &str) -> Result<Option<&Macro>>;
}
impl MacroGetter for std::collections::HashMap<&str, Macro> {
fn get_macro(&self, name: &str) -> Result<Option<&Macro>> {
Ok(self.get(name))
}
}
pub fn apply_arguments<
S: std::ops::Deref<Target = str>,
>(
mac: &Macro,
args: &[S],
) -> Result<String> {
use NrArguments as NA;
match mac.nr_arguments {
NA::None => {
if !args.is_empty() { return Err(EdError::ArgumentsWrongNr{
expected: "absolutely no".into(),
received: args.len(),
}); }
return Ok(mac.input.to_string());
},
NA::Exactly(x) => {
if args.len() != x { return Err(EdError::ArgumentsWrongNr{
expected: format!("{}",x).into(),
received: args.len(),
}); }
},
NA::Between{incl_min, incl_max} => {
if args.len() > incl_max || args.len() < incl_min {
return Err(EdError::ArgumentsWrongNr{
expected: format!("between {} and {}", incl_min, incl_max).into(),
received: args.len(),
});
}
},
NA::Any => {},
}
let mut active_dollar_index = None;
let mut output = String::new();
let mut partial_number = String::new();
for (i,c) in mac.input.char_indices() {
match (c, active_dollar_index) {
('$', None) => {
active_dollar_index = Some(i);
},
('$', Some(j)) if j+1 == i => {
output.push('$');
active_dollar_index = None;
},
(x, Some(_)) if x.is_ascii_digit() => {
partial_number.push(x);
},
(x, Some(j)) if j+1 == i => {
output.push('$');
output.push(x);
active_dollar_index = None;
},
(x, Some(_)) => {
let index = partial_number.parse::<usize>().unwrap();
partial_number.clear();
if index == 0 {
for (i, arg) in args.iter().enumerate() {
if i != 0 { output.push(' '); } output.push_str(&arg);
}
}
else {
output.push_str(args
.get(index-1)
.map(|x|->&str {&x})
.unwrap_or("")
);
}
match x {
'$' => {
active_dollar_index = Some(i);
},
x => {
active_dollar_index = None;
output.push(x);
},
}
},
(x, None) => {
output.push(x);
},
}
}
if let Some(_) = active_dollar_index {
if !partial_number.is_empty() {
let index = partial_number.parse::<usize>().unwrap();
partial_number.clear();
if index == 0 {
for (i, arg) in args.iter().enumerate() {
if i != 0 { output.push(' '); } output.push_str(&arg);
}
}
else {
output.push_str(args
.get(index-1)
.map(|x|->&str {&x})
.unwrap_or("")
);
}
}
else {
output.push('$');
}
}
Ok(output)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn argument_substitution() {
let mac = Macro::new("$1 world. Test$2")
.nr_arguments(NrArguments::Exactly(2))
;
let args = ["Hello", "ing"];
let output = apply_arguments(&mac, &args).unwrap();
assert_eq!(
&output,
"Hello world. Testing",
"$<integer> should be replaced with argument at index <integer> - 1."
);
}
#[test]
fn dollar_escaping() {
let mac = Macro::new("$$1 and $1.");
let args = ["one dollar"];
let output = apply_arguments(&mac, &args).unwrap();
assert_eq!(
&output,
"$1 and one dollar.",
"$$ in macro should be replaced with $, to enable escaping $ characters."
);
}
#[test]
fn ignore_bad_escape() {
let mac = Macro::new("$a $");
let args = ["shouldn't appear"];
let output = apply_arguments(&mac, &args).unwrap();
assert_eq!(
&output,
"$a $",
"Invalid argument references ($ not followed by integer) should be left as is."
);
}
#[test]
fn all_arguments() {
let mac = Macro::new("hi $0");
let args = ["alice,","bob,","carol"];
let output = apply_arguments(&mac, &args).unwrap();
assert_eq!(
&output,
"hi alice, bob, carol",
"$0 should be replaced with all arguments (space separated)."
);
}
#[test]
fn no_arguments() {
let mac = Macro::new("test $$ test")
.nr_arguments(NrArguments::None)
;
let args: &[&str] = &[];
let output = apply_arguments(&mac, &args).unwrap();
assert_eq!(
&output,
"test $$ test",
"When no arguments are allowed no substitution should be done."
);
}
}