diff --git a/Cargo.lock b/Cargo.lock index dfa58e1..0f855f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -112,7 +103,6 @@ dependencies = [ "chrono", "once_cell", "pulldown-cmark", - "regex", "serde", "tokio", "tower 0.4.13", @@ -526,35 +516,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - [[package]] name = "rustversion" version = "1.0.22" diff --git a/Cargo.toml b/Cargo.toml index 20dbf91..12461c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,5 +11,4 @@ tower-http = { version = "0.5", features = ["fs"] } pulldown-cmark = "0.12" serde = { version = "1.0", features = ["derive"] } chrono = "0.4" -regex = "1.10" once_cell = "1.19" diff --git a/src/template_engine.rs b/src/template_engine.rs index 5ca3f3b..0ea1b9a 100644 --- a/src/template_engine.rs +++ b/src/template_engine.rs @@ -1,5 +1,5 @@ use crate::post_manager::PostManager; -use regex::Regex; +use std::collections::HashMap; use std::fs; pub fn render_template( @@ -7,105 +7,195 @@ pub fn render_template( post_manager: &PostManager, current_post: Option<&str>, ) -> Result { - let mut result = template.to_string(); + parse_template(template, post_manager, current_post, 0) +} - // Process includes first - result = process_includes(&result)?; - - // Process post-html tag - if let Some(post_name) = current_post { - result = process_post_html(&result, post_manager, post_name)?; +fn parse_template( + template: &str, + post_manager: &PostManager, + current_post: Option<&str>, + depth: usize, +) -> Result { + const MAX_DEPTH: usize = 10; + if depth > MAX_DEPTH { + return Err("Too many nested includes (max 10)".to_string()); } - // Process post-list tag - result = process_post_list(&result, post_manager)?; + let mut result = String::new(); + let chars: Vec = template.chars().collect(); + let mut i = 0; + + while i < chars.len() { + // Check for $< tag start + if i + 1 < chars.len() && chars[i] == '$' && chars[i + 1] == '<' { + // Try to parse tag + if let Some((tag_name, attrs, end_pos)) = parse_tag(&chars, i) { + // Process tag immediately based on type + let content = process_tag(&tag_name, &attrs, post_manager, current_post, depth)?; + result.push_str(&content); + i = end_pos; + continue; + } + } + + // Regular character + result.push(chars[i]); + i += 1; + } Ok(result) } -fn process_includes(template: &str) -> Result { - let include_regex = Regex::new(r#"\$"#).unwrap(); - let mut result = template.to_string(); - let mut iteration = 0; - const MAX_ITERATIONS: usize = 10; +fn parse_tag(chars: &[char], start: usize) -> Option<(String, HashMap, usize)> { + let mut i = start + 2; // Skip $< + + // Skip whitespace + while i < chars.len() && chars[i].is_whitespace() { + i += 1; + } + + // Parse tag name + let mut tag_name = String::new(); + while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '/' && chars[i] != '>' { + tag_name.push(chars[i]); + i += 1; + } + + if tag_name.is_empty() { + return None; + } + + // Parse attributes + let mut attrs = HashMap::new(); loop { - iteration += 1; - if iteration > MAX_ITERATIONS { - return Err("Too many nested includes (max 10)".to_string()); + // Skip whitespace + while i < chars.len() && chars[i].is_whitespace() { + i += 1; } - let result_copy = result.clone(); - let captures: Vec<_> = include_regex.captures_iter(&result_copy).collect(); - - if captures.is_empty() { - break; + // Check for end of tag + if i >= chars.len() { + return None; } - for cap in captures { - let full_match = cap.get(0).unwrap().as_str(); - let src = cap.get(1).unwrap().as_str(); + if chars[i] == '/' { + i += 1; + if i < chars.len() && chars[i] == '>' { + return Some((tag_name, attrs, i + 1)); + } + return None; + } + + if chars[i] == '>' { + return Some((tag_name, attrs, i + 1)); + } + + // Parse attribute name + let mut attr_name = String::new(); + while i < chars.len() && chars[i] != '=' && !chars[i].is_whitespace() { + attr_name.push(chars[i]); + i += 1; + } + + if attr_name.is_empty() { + return None; + } + + // Skip whitespace + while i < chars.len() && chars[i].is_whitespace() { + i += 1; + } + + // Expect = + if i >= chars.len() || chars[i] != '=' { + return None; + } + i += 1; + + // Skip whitespace + while i < chars.len() && chars[i].is_whitespace() { + i += 1; + } + + // Parse attribute value (quoted) + if i >= chars.len() || chars[i] != '"' { + return None; + } + i += 1; // Skip opening quote + + let mut attr_value = String::new(); + while i < chars.len() && chars[i] != '"' { + attr_value.push(chars[i]); + i += 1; + } + + if i >= chars.len() { + return None; + } + i += 1; // Skip closing quote + + attrs.insert(attr_name, attr_value); + } +} + +fn process_tag( + tag_name: &str, + attrs: &HashMap, + post_manager: &PostManager, + current_post: Option<&str>, + depth: usize, +) -> Result { + match tag_name { + "include" => { + let src = attrs.get("src") + .ok_or_else(|| "include tag missing 'src' attribute".to_string())?; let include_path = format!("templates/{}", src); let content = fs::read_to_string(&include_path) .map_err(|e| format!("Failed to read include file '{}': {}", src, e))?; - result = result.replace(full_match, &content); + // Recursively parse the included content + parse_template(&content, post_manager, current_post, depth + 1) } - } - Ok(result) -} + "post-html" => { + let post_name = current_post + .ok_or_else(|| "post-html tag used outside of post context".to_string())?; -fn process_post_html( - template: &str, - post_manager: &PostManager, - post_name: &str, -) -> Result { - let post_html_regex = Regex::new(r"\$").unwrap(); + let post = post_manager + .get_post(post_name) + .ok_or_else(|| format!("Post '{}' not found", post_name))?; - let post = post_manager - .get_post(post_name) - .ok_or_else(|| format!("Post '{}' not found", post_name))?; - - Ok(post_html_regex.replace_all(template, post.html_content.as_str()).to_string()) -} - -fn process_post_list( - template: &str, - post_manager: &PostManager, -) -> Result { - let post_list_regex = Regex::new(r#"\$"#).unwrap(); - - let mut result = template.to_string(); - - for cap in post_list_regex.captures_iter(template) { - let full_match = cap.get(0).unwrap().as_str(); - let limit_attr = cap.get(1).and_then(|m| m.as_str().parse::().ok()); - - let limit = limit_attr; - - let posts = if let Some(l) = limit { - post_manager.get_posts_limited(l) - } else { - post_manager.get_all_posts() - }; - - let mut list_html = String::from("
    \n"); - for post in posts { - list_html.push_str(&format!( - "
  • {} {}
  • \n", - post.name, - post.name, - post.created_at.format("%Y-%m-%d") - )); + Ok(post.html_content.clone()) } - list_html.push_str("
"); - result = result.replace(full_match, &list_html); + "post-list" => { + let limit = attrs.get("limit") + .and_then(|s| s.parse::().ok()); + + let posts = if let Some(l) = limit { + post_manager.get_posts_limited(l) + } else { + post_manager.get_all_posts() + }; + + let mut list_html = String::from("
    \n"); + for post in posts { + list_html.push_str(&format!( + "
  • {} {}
  • \n", + post.name, + post.name, + post.created_at.format("%Y-%m-%d") + )); + } + list_html.push_str("
"); + + Ok(list_html) + } + + _ => Err(format!("Unknown tag: {}", tag_name)) } - - Ok(result) } #[cfg(test)] @@ -113,26 +203,50 @@ mod tests { use super::*; #[test] - fn test_post_list_regex() { - let regex = Regex::new(r#"\$"#).unwrap(); + fn test_parse_tag_post_list() { + let chars: Vec = "$".chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); + let (tag_name, attrs, _) = result.unwrap(); + assert_eq!(tag_name, "post-list"); + assert!(attrs.is_empty()); - assert!(regex.is_match("$")); - assert!(regex.is_match("$")); - assert!(regex.is_match("$")); - assert!(regex.is_match("$")); + let chars: Vec = "$".chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); - let cap = regex.captures("$").unwrap(); - assert_eq!(cap.get(1).unwrap().as_str(), "50"); + let chars: Vec = r#"$"#.chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); + let (tag_name, attrs, _) = result.unwrap(); + assert_eq!(tag_name, "post-list"); + assert_eq!(attrs.get("limit"), Some(&"50".to_string())); } #[test] - fn test_include_regex() { - let regex = Regex::new(r#"\$"#).unwrap(); + fn test_parse_tag_include() { + let chars: Vec = r#"$"#.chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); + let (tag_name, attrs, _) = result.unwrap(); + assert_eq!(tag_name, "include"); + assert_eq!(attrs.get("src"), Some(&"header.html".to_string())); - assert!(regex.is_match(r#"$"#)); - assert!(regex.is_match(r#"$"#)); + let chars: Vec = r#"$"#.chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); + let (tag_name, attrs, _) = result.unwrap(); + assert_eq!(tag_name, "include"); + assert_eq!(attrs.get("src"), Some(&"header.html".to_string())); + } - let cap = regex.captures(r#"$"#).unwrap(); - assert_eq!(cap.get(1).unwrap().as_str(), "header.html"); + #[test] + fn test_parse_tag_post_html() { + let chars: Vec = "$".chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); + let (tag_name, attrs, _) = result.unwrap(); + assert_eq!(tag_name, "post-html"); + assert!(attrs.is_empty()); } }