309 lines
9.1 KiB
Rust
309 lines
9.1 KiB
Rust
use crate::git_tracker::get_git_version;
|
|
use crate::post_manager::PostManager;
|
|
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::future::Future;
|
|
use std::pin::Pin;
|
|
|
|
pub async fn render_template(
|
|
template: &str,
|
|
post_manager: &PostManager,
|
|
current_post: Option<&str>,
|
|
) -> Result<String, String> {
|
|
parse_template(template, post_manager, current_post, 0).await
|
|
}
|
|
|
|
fn parse_template<'a>(
|
|
template: &'a str,
|
|
post_manager: &'a PostManager,
|
|
current_post: Option<&'a str>,
|
|
depth: usize,
|
|
) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send + 'a>> {
|
|
Box::pin(async move { parse_template_impl(template, post_manager, current_post, depth).await })
|
|
}
|
|
|
|
async fn parse_template_impl(
|
|
template: &str,
|
|
post_manager: &PostManager,
|
|
current_post: Option<&str>,
|
|
depth: usize,
|
|
) -> Result<String, String> {
|
|
const MAX_DEPTH: usize = 10;
|
|
if depth > MAX_DEPTH {
|
|
return Err("Too many nested includes (max 10)".to_string());
|
|
}
|
|
|
|
let mut result = String::new();
|
|
let chars: Vec<char> = 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).await?;
|
|
result.push_str(&content);
|
|
i = end_pos;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Regular character
|
|
result.push(chars[i]);
|
|
i += 1;
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
fn parse_tag(chars: &[char], start: usize) -> Option<(String, HashMap<String, String>, 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 {
|
|
// Skip whitespace
|
|
while i < chars.len() && chars[i].is_whitespace() {
|
|
i += 1;
|
|
}
|
|
|
|
// Check for end of tag
|
|
if i >= chars.len() {
|
|
return None;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
async fn process_tag(
|
|
tag_name: &str,
|
|
attrs: &HashMap<String, String>,
|
|
post_manager: &PostManager,
|
|
current_post: Option<&str>,
|
|
depth: usize,
|
|
) -> Result<String, String> {
|
|
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))?;
|
|
|
|
// Recursively parse the included content
|
|
parse_template(&content, post_manager, current_post, depth + 1).await
|
|
}
|
|
|
|
"post-html" => {
|
|
let post_name = current_post
|
|
.ok_or_else(|| "post-html tag used outside of post context".to_string())?;
|
|
|
|
let post = post_manager
|
|
.get_post(post_name)
|
|
.await
|
|
.ok_or_else(|| format!("Post '{}' not found", post_name))?;
|
|
|
|
Ok(post.html_content.clone())
|
|
}
|
|
|
|
"title" => {
|
|
if let Some(post_name) = current_post {
|
|
let post = post_manager
|
|
.get_post(post_name)
|
|
.await
|
|
.ok_or_else(|| format!("Post '{}' not found", post_name))?;
|
|
|
|
let none: String = "".to_string();
|
|
let pre = attrs.get("pre").unwrap_or(&none);
|
|
let postfix = attrs.get("post").unwrap_or(&none);
|
|
|
|
Ok(format!("{}{}{}", pre, post.title, postfix))
|
|
} else {
|
|
let def = attrs
|
|
.get("default")
|
|
.and_then(|s| Some(s.clone()))
|
|
.unwrap_or("<title not set>".to_string());
|
|
Ok(def.to_string())
|
|
}
|
|
}
|
|
|
|
"updated" => {
|
|
// Insert the last updated time of the current post (or most recent post)
|
|
let post_ts = if let Some(p) = current_post {
|
|
post_manager
|
|
.get_post(p)
|
|
.await
|
|
.ok_or_else(|| format!("Post '{}' not found", p))?
|
|
.modified_at
|
|
} else {
|
|
post_manager.get_update_timestamp().await?
|
|
};
|
|
|
|
Ok(post_ts.format("%Y-%m-%d").to_string())
|
|
}
|
|
|
|
"version" => {
|
|
let git_dir = post_manager.get_root_dir().clone();
|
|
let git_version = get_git_version(&git_dir)?;
|
|
Ok(format!("{}/{}", git_version.branch, git_version.commit))
|
|
}
|
|
|
|
"post-list" => {
|
|
let limit = attrs.get("limit").and_then(|s| s.parse::<usize>().ok());
|
|
|
|
let posts = if let Some(l) = limit {
|
|
post_manager.get_posts_limited(l).await
|
|
} else {
|
|
post_manager.get_all_posts().await
|
|
};
|
|
|
|
let mut list_html = String::from("<ul class=\"post-list\">\n");
|
|
for post in posts {
|
|
list_html.push_str(&format!(
|
|
" <li><a href=\"/p/{}\">{}</a> <span class=\"date\">{}</span></li>\n",
|
|
post.name,
|
|
post.title,
|
|
post.created_at.format("%Y-%m-%d")
|
|
));
|
|
}
|
|
list_html.push_str("</ul>");
|
|
|
|
Ok(list_html)
|
|
}
|
|
|
|
_ => Err(format!("Unknown tag: {}", tag_name)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_parse_tag_post_list() {
|
|
let chars: Vec<char> = "$<post-list/>".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());
|
|
|
|
let chars: Vec<char> = "$<post-list />".chars().collect();
|
|
let result = parse_tag(&chars, 0);
|
|
assert!(result.is_some());
|
|
|
|
let chars: Vec<char> = r#"$<post-list limit="50"/>"#.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_parse_tag_include() {
|
|
let chars: Vec<char> = r#"$<include src="header.html"/>"#.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 chars: Vec<char> = r#"$<include src="header.html" />"#.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()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_tag_post_html() {
|
|
let chars: Vec<char> = "$<post-html/>".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());
|
|
}
|
|
}
|