From 541f983def407f1f2a3ebed859e37d9f00c83111 Mon Sep 17 00:00:00 2001 From: Qrius Date: Thu, 26 Sep 2024 00:11:05 +0200 Subject: Initial commit --- src/lib.rs | 2 + src/macro_processor/macro_processor.rs | 260 +++++++++++++++++++++++++++++++++ src/macro_processor/main.rs | 12 ++ src/macro_processor/mod.rs | 2 + src/skaldpress/main.rs | 153 +++++++++++++++++++ src/skaldpress/mod.rs | 0 6 files changed, 429 insertions(+) create mode 100644 src/lib.rs create mode 100644 src/macro_processor/macro_processor.rs create mode 100644 src/macro_processor/main.rs create mode 100644 src/macro_processor/mod.rs create mode 100644 src/skaldpress/main.rs create mode 100644 src/skaldpress/mod.rs (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d81aca7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod skaldpress; +pub mod macro_processor; diff --git a/src/macro_processor/macro_processor.rs b/src/macro_processor/macro_processor.rs new file mode 100644 index 0000000..306aa04 --- /dev/null +++ b/src/macro_processor/macro_processor.rs @@ -0,0 +1,260 @@ +use std::collections::HashMap; +use std::fs; + +// print only with debug_assertions +macro_rules! dprint { + ($($x:tt)*) => { + #[cfg(debug_assertions)] + print!($($x)*) + } +} + +// // println only with debug_assertions +// macro_rules! dprintln { +// ($($x:tt)*) => { +// #[cfg(debug_assertions)] +// println!($($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) + }; +} + +#[derive(Debug)] +pub struct MacroProcessor { + macros: HashMap, +} + +impl MacroProcessor { + pub fn new() -> Self { + Self { + macros: HashMap::new(), + } + } + + pub fn define_macro(&mut self, name: String, body: String) { + self.macros.insert(name, body); + } + + /// This expands a macro definition, and it executes builtin functions, like define + /// Currently, you cannot overwrite builtin's. + /// This is partly by design, and I don't currently see why I would want that. + /// In the future, this may change + /// + /// (The HashMap might become a HashMap> or something similar. + /// Then, all builtins would also be functions, and "normal" macros, would simply + /// be closures returning the string) + fn expand_macro(&mut self, macro_name: &str, args: &mut [String]) -> String { + if macro_name == "define" { + if args.len() < 1 { + println!("Missing argument(s) to `define`, found {} but expected 1 or 2", args.len()); + return String::new(); + } + let arg0 = self.process_input(&args[0]); + if args.len() > 1 { + let arg1 = self.process_input(&args[1]); + self.define_macro(arg0, arg1); + } else { + self.define_macro(arg0, String::new()); + } + return String::new(); + } + + if macro_name == "ifdef" { + if args.len() < 2 { + println!("Missing argument(s) to `ifdef`, found {} but expected 2 or 3", args.len()); + return String::new(); + } + // We need to expand the first argument here as well, but we need to make the parser + // support literal, and phrase strings + if self.macros.contains_key(&args[0]) { + return self.process_input(&args[1]); + } + if args.len() > 2 { + return self.process_input(&args[2]); + } + return String::new(); + } + + if macro_name == "ifndef" { + if args.len() < 2 { + println!("Missing argument(s) to `ifndef`, found {} but expected 2 or 3", args.len()); + return String::new(); + } + // We need to expand the first argument here as well, but we need to make the parser + // support literal, and phrase strings + if !self.macros.contains_key(&args[0]) { + return self.process_input(&args[1]); + } + if args.len() > 2 { + return self.process_input(&args[2]); + } + return String::new(); + } + + if macro_name == "ifeq" { + if args.len() < 3 { + println!("Missing argument(s) to `ifeq`, found {} but expected 3 or 4", args.len()); + return String::new(); + } + let arg0 = self.process_input(&args[0]); + let arg1 = self.process_input(&args[1]); + if arg0 == arg1 { + return self.process_input(&args[2]); + } + if args.len() > 3 { + return self.process_input(&args[3]); + } + return String::new(); + } + + if macro_name == "ifneq" { + if args.len() < 3 { + println!("Missing argument(s) to `ifneq`, found {} but expected 3 or 4", args.len()); + return String::new(); + } + let arg0 = self.process_input(&args[0]); + let arg1 = self.process_input(&args[1]); + if arg0 != arg1 { + return self.process_input(&args[2]); + } + if args.len() > 3 { + return self.process_input(&args[3]); + } + return String::new(); + } + + if macro_name == "include" { + if args.len() < 1 { + println!("Missing argument(s) to `include`, found {} but expected 1", args.len()); + return String::new(); + } + let arg0 = self.process_input(&args[0]); + let input_file = fs::read_to_string(&arg0).expect("Failed to read input file"); + return self.process_input(&input_file); + } + + //Expand macro name? somwhat unsure on how to do this safely + //let macro_name = self.process_input(macro_name); + + let Some(macro_body) = self.macros.get(macro_name) else { + return format!("{}", macro_name); + }; + + let mut expanded = macro_body.clone(); + for (i, arg) in args.iter().enumerate() { + let placeholder = format!("${}", i + 1); + expanded = expanded.replace(&placeholder, arg); + } + expanded + } + + pub fn process_input(&mut self, input: &str) -> String { + let mut output = String::new(); + let mut state = ParserState::Normal; + let mut state_previous = 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; + + for (i, c) in input.char_indices() { + highlight_debug!(input, macro_name_start, i); + + match state { + 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; + state_previous = ParserState::Normal; + macro_name.push(c); + } else { + output.push(c); + } + } + ParserState::InMacro => { + if c.is_alphanumeric() || c == '_' { + macro_name.push(c); + } else if c == '(' { + state = ParserState::InMacroArgs; + state_previous = ParserState::InMacro; + } else { + if self.macros.contains_key(¯o_name) { + highlight_debug!("\x1b[32m\x1b[7m", input, (macro_name_start -> i)); + } + if macro_name == "DNL" { + skip_next_line_ending = c != '\n'; + } else { + let expanded = self.expand_macro(¯o_name, &mut []); + output.push_str(&expanded); + output.push(c); + } + macro_name.clear(); + state = ParserState::Normal; + state_previous = ParserState::InMacro; + } + } + ParserState::InMacroArgs => { + if c == ')' { + highlight_debug!("\x1b[32m\x1b[7m", input, (macro_name_start -> i)); + + macro_args.push(argument.trim().to_string()); + let expanded = self.expand_macro(¯o_name, &mut macro_args); + output.push_str(&expanded); + state = ParserState::Normal; + state_previous = ParserState::InMacroArgs; + macro_name.clear(); + macro_args.clear(); + argument.clear(); + } else if c == ',' { + macro_args.push(argument.trim().to_string()); + argument.clear(); + } else { + argument.push(c); + } + } + } + } + + // Handle cases where the text ends with a macro without arguments + if !macro_name.is_empty() { + output.push_str(&self.expand_macro(¯o_name, &mut [])); + } + + output + } +} + +#[derive(Debug, PartialEq)] +enum ParserState { + Normal, + InMacro, + InMacroArgs, +} diff --git a/src/macro_processor/main.rs b/src/macro_processor/main.rs new file mode 100644 index 0000000..5575aa6 --- /dev/null +++ b/src/macro_processor/main.rs @@ -0,0 +1,12 @@ +use std::env; +use std::fs; +use skaldpress::macro_processor::MacroProcessor; + +fn main() { + let args: Vec = env::args().collect(); + let input_file = fs::read_to_string(&args[1]).expect("Failed to read input file"); + + let mut macro_processor = MacroProcessor::new(); + let final_output = macro_processor.process_input(&input_file); + println!("{}", final_output); +} diff --git a/src/macro_processor/mod.rs b/src/macro_processor/mod.rs new file mode 100644 index 0000000..2fccbda --- /dev/null +++ b/src/macro_processor/mod.rs @@ -0,0 +1,2 @@ +pub mod macro_processor; +pub use macro_processor::MacroProcessor; diff --git a/src/skaldpress/main.rs b/src/skaldpress/main.rs new file mode 100644 index 0000000..de6088f --- /dev/null +++ b/src/skaldpress/main.rs @@ -0,0 +1,153 @@ +use std::fmt; +use std::fs; +use std::path::Path; + +use skaldpress::macro_processor::MacroProcessor; +use std::collections::HashMap; + +const TEMPLATES_DIR: &str = "templates/"; +const CONTENT_DIR: &str = "content/"; +const BUILD_DIR: &str = "build/"; + +#[derive(Debug)] +enum YamlValue { + Scalar(String), + List(Vec), +} + +impl fmt::Display for YamlValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + YamlValue::Scalar(x) => write!(f, "{}", x), + YamlValue::List(_l) => write!(f, "List"), + } + } +} + +/// Currently, all files MUST have a metadata block, or this will fail completely +fn extract_parse_yaml_metadata<'a>(markdown: &'a str) -> (HashMap, &'a str) { + let mut yaml_map = HashMap::new(); + let lines = markdown.lines(); + let mut yaml_started = false; + let mut yaml_ended = false; + let mut end_index = 0; + let mut current_key: Option = None; + let mut current_list: Vec = Vec::new(); + + for (i, line) in lines.enumerate() { + if line.trim() == "---" { + if yaml_started { + yaml_ended = true; + end_index = markdown.lines().take(i + 1).map(|l| l.len() + 1).sum(); + break; + } else { + yaml_started = true; + } + } else if yaml_started && !yaml_ended { + if line.trim().starts_with('-') && current_key.is_some() { + current_list.push(line.trim().trim_start_matches('-').trim().to_string()); + } else if let Some((key, value)) = line.split_once(':') { + if let Some(key) = current_key.take() { + if !current_list.is_empty() { + yaml_map.insert(key, YamlValue::List(current_list.clone())); + current_list.clear(); + } + } + current_key = Some(key.trim().to_string()); + if !value.trim().is_empty() { + yaml_map.insert( + current_key.clone().unwrap(), + YamlValue::Scalar(value.trim().to_string()), + ); + current_key = None; + } + } + } + } + + if let Some(key) = current_key.take() { + if !current_list.is_empty() { + yaml_map.insert(key, YamlValue::List(current_list)); + } + } + + if !yaml_ended { + end_index = markdown.len(); + } + + (yaml_map, &markdown[end_index..]) +} + +fn compile_file(file_path: &Path) -> Result> { + let extension = file_path.extension().expect("SP14"); + + let file_content = fs::read_to_string(file_path).expect("Failed to read file"); + let (map, file_content) = extract_parse_yaml_metadata(&file_content); + let file_content = match extension.to_str().expect("SP15") { + "md" => markdown::to_html(file_content), + _ => file_content.to_string(), + }; + + let Some(template) = map.get("template") else { + return Ok(file_content); + }; + + let template_file = format!( + "{}{}.html", + TEMPLATES_DIR, + template + ); + //println!( + // "Processing template {} for content file {:?}", + // template_file, file_path + //); + let template = fs::read_to_string(template_file).expect("Failed to read template"); + + let mut macro_processor = MacroProcessor::new(); + for (key, value) in map { + macro_processor.define_macro(format!("METADATA_{}", key), value.to_string()); + } + macro_processor.define_macro(String::from("CONTENT"), file_content); + let final_output = macro_processor.process_input(&template); + + Ok(final_output) +} + +fn compile_file_and_write(source_file_path: &Path) -> Result<(), Box> { + let dest_file_path = Path::new(BUILD_DIR).join(source_file_path.strip_prefix(CONTENT_DIR).expect("SP14")).with_extension("html"); + + std::fs::create_dir_all(&dest_file_path.parent().expect("SP19")).expect("SP10"); + + let file_content = compile_file(&source_file_path)?; + fs::write(&dest_file_path, file_content)?; + Ok(()) +} + +fn compile_files_in_directory(directory: &Path) { + for entry in fs::read_dir(directory).expect("SP4") { + let entry = entry.expect("SP5"); + let path = entry.path(); + + let metadata = fs::metadata(&path).expect("SP6"); + if metadata.is_file() { + println!( + "Compiling {:#?}", + path.as_path() + ); + compile_file_and_write(path.as_path()).expect("SP12"); + } else if metadata.is_dir() { + compile_files_in_directory(path.as_path()); + } + } + +} + +fn main() { + std::fs::remove_dir_all(Path::new(BUILD_DIR)).expect("SP21"); + // Should run twice, or at least the files which uses macros which has to be generated during + // runtime + compile_files_in_directory(Path::new(CONTENT_DIR)); + + // Just for testing + //compile_file_and_write(Path::new("content/test.html")).expect("AYYYYO"); +} diff --git a/src/skaldpress/mod.rs b/src/skaldpress/mod.rs new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3