From 6af9f573fce9c167487e10c7327feff357327d6a Mon Sep 17 00:00:00 2001 From: Qrius Date: Thu, 26 Sep 2024 00:11:05 +0200 Subject: Change around some things, don't clone MacroProcessor for nested templates, so we can keep state from parents --- skaldpress.1 | 5 + src/macro_processor/macro_processor.rs | 188 +++++++++++++++++++++++++++++++-- src/skaldpress/main.rs | 73 +++++++++++-- 3 files changed, 244 insertions(+), 22 deletions(-) diff --git a/skaldpress.1 b/skaldpress.1 index dfa8ea8..eec8dd3 100644 --- a/skaldpress.1 +++ b/skaldpress.1 @@ -49,6 +49,11 @@ since it is recursive, any metadata in templates will overwrite any metadata fro but will keep metadata that is not overwritten. This means templates can add additional context. +.IP "\fBkeep_states\fR" +List or string where every listed state/variable will be kept for subsequenct compilations. +Meaning that you can e.g. construct a array in the first compilation, which is then used the second time around. +Setting this, means that the file will always be recompiled, regardeless of other instances (unless a \fB--filter\fR is set). + .SH OPTIONS .IP "\fB-o, --out, --output\fR \fIpath\fR Specifies the directory to output the compiled files to, defaults to \fIbuild\fR. diff --git a/src/macro_processor/macro_processor.rs b/src/macro_processor/macro_processor.rs index 7839963..ee7e269 100644 --- a/src/macro_processor/macro_processor.rs +++ b/src/macro_processor/macro_processor.rs @@ -56,6 +56,52 @@ fn smp_builtin_define( 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, @@ -295,19 +341,77 @@ fn smp_builtin_array_push( macro_name: &str, args: &mut [String], ) -> Result { - for arg in args.iter() { - if let Err(e) = smp.array_push(macro_name, MacroType::String(arg.to_string())) { - smp.warnings - .push(MacroProcessorWarning::from_macro_invocation( - macro_name, - args, - format!("Error executing array_push ({:?})", e), - )); - } + 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, @@ -354,7 +458,7 @@ fn macro_name_clean<'a>(macro_name: &'a str) -> &'a str { } /// Types of macros, this is to make it easy to store both functions and strings -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum MacroType { /// When expanded, the associated function will be expanded Function( @@ -369,10 +473,31 @@ pub enum MacroType { 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, @@ -431,6 +556,14 @@ impl MacroProcessor { 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), @@ -465,11 +598,21 @@ impl MacroProcessor { 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) } @@ -548,7 +691,7 @@ impl MacroProcessor { // 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 + 1); + let placeholder = format!("${}", i); expanded = expanded.replace(&placeholder, arg); } self.process_input(&expanded) @@ -584,6 +727,9 @@ impl MacroProcessor { 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() { @@ -606,10 +752,30 @@ impl MacroProcessor { 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); diff --git a/src/skaldpress/main.rs b/src/skaldpress/main.rs index 425b2d7..69f934a 100644 --- a/src/skaldpress/main.rs +++ b/src/skaldpress/main.rs @@ -31,6 +31,7 @@ struct CompiledFile { /// The compiled files destination-extension, this will be the templates extension, /// or the file itself if it didn't have a extension extension: String, + stored_smp_state: HashMap, source_path: String, needs_recompilation: bool, } @@ -142,7 +143,8 @@ fn sp_all_tagged_by( for doc_i in tagged_files { let file = &compiled_files[doc_i]; - let mut smp_local = macro_processor(&file.metadata, Some(smp)); + let mut smp_local = smp.clone(); + macro_processor_initialize(&file.metadata, &mut smp_local, None); out.push_str(&sp_template( &mut smp_local, "template", @@ -153,15 +155,12 @@ fn sp_all_tagged_by( Ok(out) } -fn macro_processor( +fn macro_processor_initialize( metadata: &HashMap, - old_macro_processor: Option<&MacroProcessor>, -) -> MacroProcessor { - let mut macro_processor = match old_macro_processor { - Some(macro_processor) => macro_processor.clone(), - None => MacroProcessor::new(), - }; - + old_macro_processor: &mut MacroProcessor, + additional_state: Option<&HashMap>, +) { + let macro_processor = old_macro_processor; macro_processor.define_macro( String::from("all_tagged_by"), MacroType::Function(sp_all_tagged_by), @@ -170,8 +169,18 @@ fn macro_processor( for (key, value) in metadata { macro_processor.define_macro_string(format!("METADATA_{}", key), value.to_string()); } + if let Some(additional_state) = additional_state { + for (key, value) in additional_state { + macro_processor.define_macro(key.to_string(), value.clone()); + } + } - macro_processor + if let Some(publish_date) = metadata.get("publish_date") { + if !metadata.contains_key("change_date") { + macro_processor + .define_macro_string(format!("METADATA_change_date"), publish_date.to_string()); + } + } } fn get_template_path(template: &str, opts: &Opts) -> String { @@ -224,6 +233,9 @@ fn wrap_template( } fn needs_recompilation(macro_processor: &MacroProcessor) -> bool { + if macro_processor.macros.contains_key("METADATA_keep_states") { + return true; + } for (macro_name, _) in ¯o_processor.macro_invocations { if macro_name == "all_tagged_by" { return true; @@ -238,6 +250,34 @@ fn print_warnings(macro_processor: &MacroProcessor) { } } +fn extract_requested_macro_processor_state( + macro_processor: &mut MacroProcessor, + metadata: &HashMap, +) -> HashMap { + if let Some(requested_keys) = metadata.get("keep_states") { + let requested_keys = match requested_keys { + YamlValue::List(l) => l.clone(), + YamlValue::Scalar(s) => vec![s.to_string()], + _ => { + macro_processor + .warnings + .push(MacroProcessorWarning::new(format!( + "keep_states specification must be list or scalar", + ))); + return HashMap::new(); + } + }; + let mut res = HashMap::new(); + for stored_key in requested_keys { + if let Some(stored_value) = macro_processor.macros.get(&stored_key) { + res.insert(stored_key.to_string(), stored_value.clone()); + } + } + return res; + } + HashMap::new() +} + /// Will attempt to compile a specific file, potentially storing some state about the file fn compile_file(file_path: &Path, opts: &Opts) -> Result { let extension = file_path.extension().unwrap_or(std::ffi::OsStr::new("")); @@ -283,12 +323,21 @@ fn compile_file(file_path: &Path, opts: &Opts) -> Result Result Result