summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs2
-rw-r--r--src/macro_processor/macro_processor.rs260
-rw-r--r--src/macro_processor/main.rs12
-rw-r--r--src/macro_processor/mod.rs2
-rw-r--r--src/skaldpress/main.rs153
-rw-r--r--src/skaldpress/mod.rs0
6 files changed, 429 insertions, 0 deletions
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<String, String>,
+}
+
+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<String, Box<dyn FnMut>> 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(&macro_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(&macro_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(&macro_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(&macro_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<String> = 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<String>),
+}
+
+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<String>"),
+ }
+ }
+}
+
+/// Currently, all files MUST have a metadata block, or this will fail completely
+fn extract_parse_yaml_metadata<'a>(markdown: &'a str) -> (HashMap<String, YamlValue>, &'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<String> = None;
+ let mut current_list: Vec<String> = 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<String, Box<dyn std::error::Error>> {
+ 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<dyn std::error::Error>> {
+ 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
--- /dev/null
+++ b/src/skaldpress/mod.rs