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<String, SMPError> {
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<String, SMPError> {
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<String, SMPError> {
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<String, SMPError> {
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<String, SMPError> {
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<String, SMPError> {
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<String, SMPError> {
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<String, SMPError> {
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<String, SMPError> {
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<String, SMPError> {
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<String, SMPError> {
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<String, SMPError> {
let indent_size = args[0].parse::<u32>().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<String, SMPError> {
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<String, SMPError> {
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<String, SMPError> {
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 smp_builtin_html_from_markdown(
smp: &mut MacroProcessor,
macro_name: &str,
args: &mut [String],
) -> Result<String, SMPError> {
if args.len() < 1 {
smp.warnings
.push(MacroProcessorWarning::from_macro_invocation(
macro_name,
args,
format!("Wrong number of arguments, expected 1"),
));
return Ok(macro_name.to_string());
}
let content = smp.process_input(&args[0])?;
let content = smp.process_input(&content)?;
markdown::to_html_with_options(
&content,
&markdown::Options {
parse: markdown::ParseOptions::gfm(),
compile: markdown::CompileOptions {
allow_dangerous_html: true,
allow_dangerous_protocol: true,
..markdown::CompileOptions::default()
},
},
)
.map_err(|e| SMPError::MarkdownError(15, e))
}
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<String, SMPError>,
),
/// Will be expanded in-place to the String
String(String),
Array(Vec<MacroType>),
}
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<String, MacroType>,
/// All macro invocations that has happened
pub macro_invocations: Vec<(String, Vec<String>)>,
/// Emitted warnings
pub warnings: Vec<MacroProcessorWarning>,
}
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 = "markdown")]
self.define_macro(
String::from("html_from_markdown"),
MacroType::Function(smp_builtin_html_from_markdown),
);
#[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<String, SMPError> {
// 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();
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<String, SMPError> {
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 current_indent = 0;
//let mut line_text_seen = false;
// We should keep track of filename, linenumber, and character number on line here
// So we can give sensible error messages
let mut quote_level = 0;
let mut parens_level = 0;
let mut chars = input.char_indices().peekable();
while let Some((i, c)) = chars.next() {
highlight_debug!(input, macro_name_start, i);
let peek = match chars.peek() {
Some((_, c)) => Some(c),
None => None,
};
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 == '%' && peek == Some(&'"') {
state = ParserState::InQuotes;
quote_level += 1;
chars.next();
} else if c.is_alphanumeric() {
state = ParserState::InMacro;
macro_name.push(c);
} else {
output.push(c);
}
}
ParserState::InQuotes => match c {
'%' if peek == Some(&'"') => {
quote_level += 1;
chars.next();
output.push_str(r#"%""#);
}
'"' if peek == Some(&'%') => {
quote_level -= 1;
if quote_level == 0 {
state = ParserState::Normal;
} else {
output.push_str(r#""%"#);
}
chars.next();
}
_ => {
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 == '%' && peek == Some(&'"') {
quote_level += 1;
chars.next();
argument.push_str(r#"%""#);
continue;
} else if c == '"' && peek == Some(&'%') {
quote_level -= 1;
chars.next();
argument.push_str(r#""%"#);
continue;
} else if quote_level > 0 {
argument.push(c);
continue;
}
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)
}
}