use crate::macro_processor::error::SMPError; use std::collections::HashMap; use std::fs; use std::process::Command; // print only with debug_assertions macro_rules! dprint { ($($x:tt)*) => { #[cfg(debug_assertions)] print!($($x)*) } } // Point to one or more ranges of a string, useful for highlighting parts of string macro_rules! highlight_debug { ($hi_col:expr, $str:expr $(, ($pos:tt -> $endpos:tt))*) => { for (i, _c) in $str.char_indices() { if false $(|| (i >= $pos) && (i < $endpos))* { dprint!("{}{}\x1b[0m", $hi_col, _c); } else { dprint!("{}", _c); } } dprint!("\n"); }; ($str:expr, $pos:expr, $endpos:expr) => { highlight_debug!("\x1b[7m", $str, ($pos -> $endpos)) }; ($str:expr, $pos:expr) => { highlight_debug!($str, $pos, $pos+1) }; } /// Builtin for defining a new macro fn smp_builtin_define( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 1 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 1"), )); return Ok(macro_name.to_string()); } let arg0 = smp.process_input(&args[0])?; if args.len() > 1 { let arg1 = smp.process_input(&args[1])?; smp.define_macro(arg0, MacroType::String(arg1)); } else { smp.define_macro(arg0, MacroType::String(String::new())); } Ok(String::new()) } /// Builtin for undefining a macro fn smp_builtin_undefine( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 1 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 1"), )); return Ok(macro_name.to_string()); } if let None = smp.macros.remove(&args[0]) { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Macro already not defined"), )); } Ok(String::new()) } /// Builtin for defining a new macro fn smp_builtin_define_array( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 1 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 1"), )); return Ok(macro_name.to_string()); } let arg0 = smp.process_input(&args[0])?; smp.define_macro(arg0, MacroType::Array(Vec::new())); Ok(String::new()) } /// If macro is defined, return second argument, else return third argument if provided fn smp_builtin_ifdef( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 2 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 2"), )); return Ok(macro_name.to_string()); } // We need to expand the first argument here as well, but we need to make the parser // support literal, and phrase strings if smp.macros.contains_key(&args[0]) { return smp.process_input(&args[1]); } if args.len() > 2 { return smp.process_input(&args[2]); } Ok(String::new()) } /// If macro is not defined, return second argument, else return third argument if provided fn smp_builtin_ifndef( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 2 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 2"), )); return Ok(macro_name.to_string()); } // We need to expand the first argument here as well, but we need to make the parser // support literal, and phrase strings if !smp.macros.contains_key(&args[0]) { return smp.process_input(&args[1]); } if args.len() > 2 { return smp.process_input(&args[2]); } Ok(String::new()) } /// If arguments are equal, return third argument, else return fourth argument if provided fn smp_builtin_ifeq( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 3 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 3"), )); return Ok(macro_name.to_string()); } let arg0 = smp.process_input(&args[0])?; let arg1 = smp.process_input(&args[1])?; if arg0 == arg1 { return smp.process_input(&args[2]); } if args.len() > 3 { return smp.process_input(&args[3]); } Ok(String::new()) } /// If arguments are not equal, return third argument, else return fourth argument if provided fn smp_builtin_ifneq( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 3 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 3"), )); return Ok(macro_name.to_string()); } let arg0 = smp.process_input(&args[0])?; let arg1 = smp.process_input(&args[1])?; if arg0 != arg1 { return smp.process_input(&args[2]); } if args.len() > 3 { return smp.process_input(&args[3]); } return Ok(String::new()); } /// Include a new file, and process it normally. There is no loop protection here! fn smp_builtin_include( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 1 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 1"), )); return Ok(macro_name.to_string()); } let arg0 = smp.process_input(&args[0])?; let input_file = fs::read_to_string(&arg0).map_err(|e| SMPError::IncludeError(2, e, arg0))?; return smp.process_input(&input_file); } /// Include a new file verbatim, don't do ANY additional processing fn smp_builtin_include_verbatim( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 1 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 1"), )); return Ok(macro_name.to_string()); } let arg0 = smp.process_input(&args[0])?; fs::read_to_string(&arg0).map_err(|e| SMPError::IncludeError(2, e, arg0)) } /// Simply execute argument as shell command fn smp_builtin_shell( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 1 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 1"), )); return Ok(macro_name.to_string()); } let arg0 = smp.process_input(&args[0])?; let res = Command::new("sh").arg("-c").arg(arg0).output(); match res { Ok(output) => String::from_utf8(output.stdout) .map_err(|e| SMPError::ShellCommandError(1, Box::new(e))), Err(e) => { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Error running shell command ({})", e), )); Ok(String::new()) } } } /// Would like one that is better than this tbh fn smp_builtin_expr( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 1 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 1"), )); return Ok(macro_name.to_string()); } for arg in args.iter_mut() { *arg = smp.process_input(&arg)?; } let args = args.to_vec(); let res = Command::new("expr").args(args.clone()).output(); match res { Ok(output) => String::from_utf8(output.stdout) .map_err(|e| SMPError::ShellCommandError(1, Box::new(e))), Err(e) => { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, &args, format!("Error running shell command ({})", e), )); Ok(String::new()) } } } /// Indent argument 2 by N spaces fn smp_builtin_indent( smp: &mut MacroProcessor, _macro_name: &str, args: &mut [String], ) -> Result { let indent_size = args[0].parse::().unwrap_or(0); let mut out = String::with_capacity(args[1].len()); for l in args[1].lines() { let mut lin = String::with_capacity(indent_size.try_into().unwrap_or(0) + l.len()); if args.len() <= 2 || (args[2] != "skip_first") { for _ in 0..indent_size { lin.push(' '); } } lin.push_str(&smp.process_input(&l)?); out.push_str(&lin); } Ok(out) } /// Push any arguments to array macro fn smp_builtin_array_push( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { let mut args_iter = args.iter(); let Some(array_name) = args_iter.next() else { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Invalid arguments to array_push"), )); return Ok(String::new()); }; let mut def = Vec::new(); while let Some(arg) = args_iter.next() { def.push(MacroType::String(smp.process_input(arg)?)); } if let Err(e) = smp.array_push(array_name, MacroType::Array(def)) { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Error executing array_push ({:?})", e), )); } Ok(String::new()) } /// Push any arguments to array macro /// Process each element in a array as a macro-invokation on the second argument /// Not the best way to do this, it is not sensibly recursive. fn smp_builtin_array_each( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { let Some(macro_body) = smp.macros.get(&args[0]) else { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("{:?} is not a macro", args[0]), )); return Ok(String::new()); }; let MacroType::Array(array) = macro_body else { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("{:?} is not a macro of type array", args[0]), )); return Ok(String::new()); }; let mut out = String::new(); for el in array.clone() { let exp = match el { MacroType::String(s) => smp.expand_macro(&args[1], &mut [s])?, MacroType::Array(a) => { let mut out = Vec::new(); for _el in a { out.push(_el.to_string()); } let expanded = smp.expand_macro(&args[1], &mut out)?; smp.process_input(&expanded)? } _ => String::new(), }; out.push_str(&exp); } Ok(out) } #[cfg(feature = "time")] fn smp_builtin_format_time( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result { if args.len() < 2 { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Wrong number of arguments, expected at least 2"), )); return Ok(macro_name.to_string()); } let timestamp = smp.process_input(&args[1])?; let dt = match chrono::DateTime::parse_from_rfc3339(×tamp) { Ok(dt) => dt, Err(e) => { smp.warnings .push(MacroProcessorWarning::from_macro_invocation( macro_name, args, format!("Could not parse datetime {} ({})", timestamp, e), )); return Ok(timestamp); } }; Ok(format!("{}", dt.format(&args[0]))) } fn macro_is_whitespace_deleting(s: &str) -> bool { s.chars().nth(s.len() - 1) == Some('_') } fn macro_name_clean<'a>(macro_name: &'a str) -> &'a str { let mut macro_name = macro_name; if macro_is_whitespace_deleting(macro_name) { let mut macro_chars = macro_name.chars(); macro_chars.next_back(); macro_name = macro_chars.as_str(); } macro_name } /// Types of macros, this is to make it easy to store both functions and strings #[derive(Clone, Debug)] pub enum MacroType { /// When expanded, the associated function will be expanded Function( fn( smp: &mut MacroProcessor, macro_name: &str, args: &mut [String], ) -> Result, ), /// Will be expanded in-place to the String String(String), Array(Vec), } use std::string::ToString; impl ToString for MacroType { fn to_string(&self) -> String { match self { MacroType::String(s) => s.to_string(), MacroType::Array(a) => { let mut out = String::from("["); for (i, el) in a.iter().enumerate() { out.push_str(&el.to_string()); if i < (a.len() - 1) { out.push_str(", "); } } out } MacroType::Function(_a) => String::from("Function()"), } } } /// Possible parser states #[derive(Debug, PartialEq)] enum ParserState { Normal, InQuotes, InMacro, InMacroArgs, DNL, } #[derive(Clone, Debug)] pub struct MacroProcessorWarning { pub description: String, } impl MacroProcessorWarning { pub fn new(description: String) -> Self { MacroProcessorWarning { description } } pub fn from_macro_invocation(macro_name: &str, args: &[String], description: String) -> Self { let mut desc = format!("{}(", macro_name); for (i, arg) in args.iter().enumerate() { desc.push_str(arg); if i < (args.len() - 1) { desc.push(','); } } desc.push_str(&format!(") -> {}", description)); MacroProcessorWarning { description: desc } } } /// Defines a MacroProcessor object, with it's associated state /// the state mostly includes the defined macros #[derive(Clone)] pub struct MacroProcessor { /// All currently defined macros in this MacroProcessor pub macros: HashMap, /// All macro invocations that has happened pub macro_invocations: Vec<(String, Vec)>, /// Emitted warnings pub warnings: Vec, } impl MacroProcessor { pub fn new() -> Self { let mut smp = Self { macros: HashMap::new(), macro_invocations: Vec::new(), warnings: Vec::new(), }; smp.define_builtins(); smp } /// Bootstrapping-function for defining all builtins, /// the same way all other macros might be defined fn define_builtins(&mut self) { self.define_macro( String::from("define"), MacroType::Function(smp_builtin_define), ); self.define_macro( String::from("define_array"), MacroType::Function(smp_builtin_define_array), ); self.define_macro( String::from("undefine"), MacroType::Function(smp_builtin_undefine), ); self.define_macro( String::from("ifdef"), MacroType::Function(smp_builtin_ifdef), ); self.define_macro( String::from("ifndef"), MacroType::Function(smp_builtin_ifndef), ); self.define_macro(String::from("ifeq"), MacroType::Function(smp_builtin_ifeq)); self.define_macro( String::from("ifneq"), MacroType::Function(smp_builtin_ifneq), ); self.define_macro( String::from("include"), MacroType::Function(smp_builtin_include), ); self.define_macro( String::from("include_verbatim"), MacroType::Function(smp_builtin_include_verbatim), ); self.define_macro( String::from("shell"), MacroType::Function(smp_builtin_shell), ); self.define_macro( String::from("indent"), MacroType::Function(smp_builtin_indent), ); self.define_macro(String::from("expr"), MacroType::Function(smp_builtin_expr)); self.define_macro( String::from("array_push"), MacroType::Function(smp_builtin_array_push), ); self.define_macro( String::from("array_each"), MacroType::Function(smp_builtin_array_each), ); #[cfg(feature = "time")] self.define_macro( String::from("format_time"), MacroType::Function(smp_builtin_format_time), ); #[cfg(feature = "webring")] self.define_macro( String::from("webring_rss"), MacroType::Function(smp_builtin_webring_rss), ); // format('Result id %d', 3282) } /// Define a new macro as a string that will be expanded in-place /// /// # Arguments /// /// * `name` - The name of the new macro /// * `body` - The body of the new macro, this will be expanded when macro is executed pub fn define_macro_string(&mut self, name: String, body: String) { self.macros.insert(name, MacroType::String(body)); } /// Define a new macro as any MacroType /// /// # Arguments /// /// * `name` - The name of the new macro /// * `macro_expansion` - The MacroType struct to use. pub fn define_macro(&mut self, name: String, macro_expansion: MacroType) { self.macros.insert(name, macro_expansion); } /// Push a MacroType into a array, this allows creating special macros that can iterate pub fn array_push(&mut self, name: &str, element: MacroType) -> Result<(), SMPError> { let Some(macro_body) = self.macros.get_mut(name) else { return Err(SMPError::UnknownError(4, None)); }; let MacroType::Array(array) = macro_body else { return Err(SMPError::UnknownError(5, None)); }; array.push(element); Ok(()) } /// This expands a macro definition, and it executes builtin functions, like define /// /// # Arguments /// /// * `macro_name` - Name of macro to expand if it exists /// * `args` - List of arguments parsed along with macro invokation (empty list if no arguments were parsed) fn expand_macro(&mut self, macro_name: &str, args: &mut [String]) -> Result { // Ignore trailing underscore in macro name, the parser will pop a space in front if // present, but we should ignore it for finding the macro. let macro_name = macro_name_clean(macro_name); let Some(macro_body) = self.macros.get(macro_name) else { if args.len() == 0 { return Ok(format!("{}", macro_name)); } let mut out = format!("{}(", macro_name); for (i, arg) in args.iter().enumerate() { out.push_str(&self.process_input(arg)?); if i < (args.len() - 1) { out.push(','); } } out.push(')'); return Ok(out); }; // Strip leading whitespace from arguments for arg in &mut *args { *arg = arg.trim().to_string(); } // Log macro invokation // The fact that we are here, does not ensure that the macro is actually expanded into // something useful, just that it exists, and was invoked self.macro_invocations .push((macro_name.to_string(), args.to_vec())); match macro_body { MacroType::String(body) => { let mut expanded = body.clone(); // The expanded macro, should _probably_ be expanded again // The below is a okay _idea_, but I am not sure if I want to have this syntax for // functions defined in normal smp code for (i, arg) in args.iter().enumerate() { let placeholder = format!("${}", i); expanded = expanded.replace(&placeholder, arg); } self.process_input(&expanded) } MacroType::Function(func) => { return func(self, macro_name, args); } MacroType::Array(vec) => return Ok(format!("Array[{}]", vec.len())), } } /// Do macro processing of a input string /// /// This is the main function used for processing a input string, /// will return the processed string. /// Will be called recursively if needed. /// Subsequent calls will keep the state from the previous call. /// This includes macro definitions. /// /// # Arguments /// /// * `input` - The text to process pub fn process_input(&mut self, input: &str) -> Result { let mut output = String::new(); let mut state = ParserState::Normal; let mut macro_name = String::new(); let mut macro_args = Vec::new(); let mut argument = String::new(); let mut macro_name_start = 0; let mut skip_next_line_ending = false; let mut in_quote_single = false; let mut in_quote_double = false; const QUOTE_START: char = '`'; const QUOTE_END: char = '\''; let mut quote_level = 0; let mut parens_level = 0; for (i, c) in input.char_indices() { highlight_debug!(input, macro_name_start, i); match state { ParserState::DNL => { if c == '\n' { state = ParserState::Normal; } } ParserState::Normal => { macro_name_start = i; if skip_next_line_ending && (c == '\n') { skip_next_line_ending = false; continue; } if c.is_alphanumeric() { state = ParserState::InMacro; macro_name.push(c); } else if c == QUOTE_START { state = ParserState::InQuotes; quote_level += 1; } else { output.push(c); } } ParserState::InQuotes => match c { QUOTE_START => { quote_level += 1; output.push(c); } QUOTE_END => { quote_level -= 1; if quote_level == 0 { state = ParserState::Normal; } else { output.push(c); } } _ => { output.push(c); } }, ParserState::InMacro => { if c.is_alphanumeric() || c == '_' { macro_name.push(c); } else if c == '(' { parens_level += 1; state = ParserState::InMacroArgs; } else { if macro_is_whitespace_deleting(¯o_name) { if output.chars().last() == Some(' ') { output.pop(); } macro_name = macro_name_clean(¯o_name).to_string(); } if self.macros.contains_key(¯o_name) { highlight_debug!("\x1b[32m\x1b[7m", input, (macro_name_start -> i)); } if macro_name == "SNNL" { skip_next_line_ending = c != '\n'; } else if macro_name == "DNL" { if c != '\n' { state = ParserState::DNL; } macro_name.clear(); continue; } else { let expanded = self.expand_macro(¯o_name, &mut [])?; output.push_str(&expanded); output.push(c); } macro_name.clear(); state = ParserState::Normal; } } ParserState::InMacroArgs => { if (c == ')') && (parens_level == 1) { if macro_is_whitespace_deleting(¯o_name) { if output.chars().last() == Some(' ') { output.pop(); } macro_name = macro_name_clean(¯o_name).to_string(); } highlight_debug!("\x1b[32m\x1b[7m", input, (macro_name_start -> i)); parens_level = 0; macro_args.push(argument.trim().to_string()); let expanded = self.expand_macro(¯o_name, &mut macro_args)?; output.push_str(&expanded); state = ParserState::Normal; macro_name.clear(); macro_args.clear(); argument.clear(); } else if (c == ',') && (parens_level == 1) { macro_args.push(argument.trim().to_string()); argument.clear(); } else { if c == '(' { parens_level += 1; } if c == ')' { parens_level -= 1; } argument.push(c); } } } } // Handle cases where the text ends with a macro without arguments if !macro_name.is_empty() { if macro_is_whitespace_deleting(¯o_name) { if output.chars().last() == Some(' ') { output.pop(); } macro_name = macro_name_clean(¯o_name).to_string(); } output.push_str(&self.expand_macro(¯o_name, &mut [])?); } Ok(output) } }