use std::path::PathBuf;
use regex::Regex;
use super::{ViewConfig, ViewError};
pub fn apply_layout(content: &str, config: &ViewConfig) -> Result<String, ViewError> {
let mut result = content.to_string();
if config.layout_on {
if result.contains("{__NOLAYOUT__}") {
result = result.replace("{__NOLAYOUT__}", "");
} else {
let layout_file = resolve_template_path(&config.layout_name, config);
if !layout_file.is_file() {
return Err(ViewError::TemplateNotFound(format!(
"布局模板: {} (解析路径: {})",
config.layout_name,
layout_file.display()
)));
}
let layout_content = std::fs::read_to_string(&layout_file)?;
result = layout_content.replace(&config.layout_item, &result);
}
} else {
result = result.replace("{__NOLAYOUT__}", "");
}
result = parse_layout_tag(&result, config)?;
Ok(result)
}
fn parse_layout_tag(content: &str, config: &ViewConfig) -> Result<String, ViewError> {
let begin = regex::escape(&config.taglib_begin);
let end = regex::escape(&config.taglib_end);
let pattern = format!(r"{}layout\b\s+([^}}]+){}", begin, end);
let re = Regex::new(&pattern).map_err(|e| ViewError::SyntaxError(e.to_string()))?;
if let Some(caps) = re.captures(content) {
let full_match = caps.get(0).expect("正则捕获组 0 必定存在");
let tag_name = parse_attr(full_match.as_str(), "name");
let tag_name = match tag_name {
Some(name) if !name.is_empty() => name,
_ => {
return Ok(content.replace("{__NOLAYOUT__}", ""));
}
};
let result = content.replace(full_match.as_str(), "");
if config.layout_on && config.layout_name == tag_name {
return Ok(result);
}
let replace = parse_attr(full_match.as_str(), "replace")
.unwrap_or_else(|| config.layout_item.clone());
let layout_file = resolve_template_path(&tag_name, config);
if !layout_file.is_file() {
return Err(ViewError::TemplateNotFound(format!(
"布局模板: {} (解析路径: {})",
tag_name,
layout_file.display()
)));
}
let layout_content = std::fs::read_to_string(&layout_file)?;
Ok(layout_content.replace(&replace, &result))
} else {
Ok(content.replace("{__NOLAYOUT__}", ""))
}
}
fn parse_attr(tag: &str, attr_name: &str) -> Option<String> {
let pattern = format!(r#"{}\s*=\s*["']([^"']*)["']"#, regex::escape(attr_name));
let re = Regex::new(&pattern).ok()?;
re.captures(tag).map(|c| c[1].to_string())
}
fn resolve_template_path(name: &str, config: &ViewConfig) -> PathBuf {
if std::path::Path::new(name).extension().is_some() {
return PathBuf::from(name);
}
let name = name.strip_prefix('/').unwrap_or(name);
let normalized = name.replace(['/', ':'], &config.view_depr);
let suffix = config.view_suffix.trim_start_matches('.');
let file_name = format!("{}.{}", normalized, suffix);
config.view_path.join(file_name)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
static TEMP_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
fn make_temp_dir() -> PathBuf {
let id = TEMP_DIR_COUNTER.fetch_add(1, Ordering::SeqCst);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let dir = std::env::temp_dir().join(format!("sz_rust_layout_test_{}_{}", nanos, id));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write_template(dir: &std::path::Path, name: &str, content: &str) {
let path = dir.join(format!("{}.html", name));
std::fs::write(&path, content).unwrap();
}
fn cleanup_dir(dir: &std::path::Path) {
let _ = std::fs::remove_dir_all(dir);
}
fn make_config(view_path: PathBuf) -> ViewConfig {
ViewConfig {
view_path,
..Default::default()
}
}
fn make_config_layout_on(view_path: PathBuf) -> ViewConfig {
ViewConfig {
view_path,
layout_on: true,
..Default::default()
}
}
#[test]
fn test_config_layout_on_basic() {
let dir = make_temp_dir();
write_template(&dir, "layout", "<html><body>{__CONTENT__}</body></html>");
let config = make_config_layout_on(dir.clone());
let result = apply_layout("<h1>Hello</h1>", &config).unwrap();
assert_eq!(result, "<html><body><h1>Hello</h1></body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_config_layout_on_with_nolayout() {
let dir = make_temp_dir();
write_template(&dir, "layout", "<html><body>{__CONTENT__}</body></html>");
let config = make_config_layout_on(dir.clone());
let result = apply_layout("{__NOLAYOUT__}<h1>Hello</h1>", &config).unwrap();
assert_eq!(result, "<h1>Hello</h1>");
cleanup_dir(&dir);
}
#[test]
fn test_config_layout_on_custom_name() {
let dir = make_temp_dir();
write_template(&dir, "custom", "<div class=\"wrapper\">{__CONTENT__}</div>");
let config = ViewConfig {
view_path: dir.clone(),
layout_on: true,
layout_name: "custom".to_string(),
..Default::default()
};
let result = apply_layout("<p>Content</p>", &config).unwrap();
assert_eq!(result, "<div class=\"wrapper\"><p>Content</p></div>");
cleanup_dir(&dir);
}
#[test]
fn test_config_layout_on_custom_item() {
let dir = make_temp_dir();
write_template(&dir, "layout", "<html><body>{__BODY__}</body></html>");
let config = ViewConfig {
view_path: dir.clone(),
layout_on: true,
layout_item: "{__BODY__}".to_string(),
..Default::default()
};
let result = apply_layout("<h1>Hello</h1>", &config).unwrap();
assert_eq!(result, "<html><body><h1>Hello</h1></body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_config_layout_on_file_not_found() {
let dir = make_temp_dir();
let config = make_config_layout_on(dir.clone());
let result = apply_layout("<h1>Hello</h1>", &config);
assert!(matches!(result, Err(ViewError::TemplateNotFound(_))));
cleanup_dir(&dir);
}
#[test]
fn test_config_layout_off_basic() {
let dir = make_temp_dir();
let config = make_config(dir.clone());
let result = apply_layout("<h1>Hello</h1>", &config).unwrap();
assert_eq!(result, "<h1>Hello</h1>");
cleanup_dir(&dir);
}
#[test]
fn test_config_layout_off_with_nolayout() {
let dir = make_temp_dir();
let config = make_config(dir.clone());
let result = apply_layout("{__NOLAYOUT__}<h1>Hello</h1>", &config).unwrap();
assert_eq!(result, "<h1>Hello</h1>");
cleanup_dir(&dir);
}
#[test]
fn test_tag_layout_basic() {
let dir = make_temp_dir();
write_template(&dir, "custom", "<html><body>{__CONTENT__}</body></html>");
let config = make_config(dir.clone());
let result = apply_layout(r#"{layout name="custom" /}<h1>Hello</h1>"#, &config).unwrap();
assert_eq!(result, "<html><body><h1>Hello</h1></body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_tag_layout_without_self_closing() {
let dir = make_temp_dir();
write_template(&dir, "custom", "<html><body>{__CONTENT__}</body></html>");
let config = make_config(dir.clone());
let result = apply_layout(r#"{layout name="custom"}<h1>Hello</h1>"#, &config).unwrap();
assert_eq!(result, "<html><body><h1>Hello</h1></body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_tag_layout_custom_replace() {
let dir = make_temp_dir();
write_template(&dir, "custom", "<html><body>BODY</body></html>");
let config = make_config(dir.clone());
let result = apply_layout(
r#"{layout name="custom" replace="BODY"}<h1>Hello</h1>"#,
&config,
)
.unwrap();
assert_eq!(result, "<html><body><h1>Hello</h1></body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_tag_layout_replace_with_brace_php_bug() {
let dir = make_temp_dir();
write_template(&dir, "custom", "<html><body>{__BODY__}</body></html>");
let config = make_config(dir.clone());
let result = apply_layout(
r#"{layout name="custom" replace="{__BODY__}"}<h1>Hello</h1>"#,
&config,
)
.unwrap();
assert_eq!(result, "<html><body>{__BODY__}</body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_tag_layout_file_not_found() {
let dir = make_temp_dir();
let config = make_config(dir.clone());
let result = apply_layout(r#"{layout name="nonexistent" /}<h1>Hello</h1>"#, &config);
assert!(matches!(result, Err(ViewError::TemplateNotFound(_))));
cleanup_dir(&dir);
}
#[test]
fn test_tag_layout_missing_name_attr() {
let dir = make_temp_dir();
write_template(&dir, "custom", "<html><body>{__CONTENT__}</body></html>");
let config = make_config(dir.clone());
let result =
apply_layout(r#"{layout replace="{__CONTENT__}"}<h1>Hello</h1>"#, &config).unwrap();
assert_eq!(result, r#"{layout replace="{__CONTENT__}"}<h1>Hello</h1>"#);
cleanup_dir(&dir);
}
#[test]
fn test_tag_layout_empty_name() {
let dir = make_temp_dir();
let config = make_config(dir.clone());
let result = apply_layout(r#"{layout name="" /}<h1>Hello</h1>"#, &config).unwrap();
assert_eq!(result, r#"{layout name="" /}<h1>Hello</h1>"#);
cleanup_dir(&dir);
}
#[test]
fn test_config_on_tag_same_name_skip() {
let dir = make_temp_dir();
write_template(&dir, "layout", "<html><body>{__CONTENT__}</body></html>");
let config = make_config_layout_on(dir.clone());
let result = apply_layout(r#"{layout name="layout" /}<h1>Hello</h1>"#, &config).unwrap();
assert_eq!(result, "<html><body><h1>Hello</h1></body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_config_on_tag_different_name_double_layout() {
let dir = make_temp_dir();
write_template(&dir, "layout", "<html><body>{__CONTENT__}</body></html>");
write_template(&dir, "other", "<wrapper>{__CONTENT__}</wrapper>");
let config = make_config_layout_on(dir.clone());
let result = apply_layout(r#"{layout name="other" /}<h1>Hello</h1>"#, &config).unwrap();
assert_eq!(
result,
"<wrapper><html><body><h1>Hello</h1></body></html></wrapper>"
);
cleanup_dir(&dir);
}
#[test]
fn test_config_off_tag_layout_applied() {
let dir = make_temp_dir();
write_template(&dir, "custom", "<html><body>{__CONTENT__}</body></html>");
let config = make_config(dir.clone());
let result = apply_layout(r#"{layout name="custom" /}<h1>Hello</h1>"#, &config).unwrap();
assert_eq!(result, "<html><body><h1>Hello</h1></body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_nolayout_with_config_on() {
let dir = make_temp_dir();
write_template(&dir, "layout", "<html><body>{__CONTENT__}</body></html>");
let config = make_config_layout_on(dir.clone());
let result = apply_layout("{__NOLAYOUT__}<h1>Hello</h1>", &config).unwrap();
assert_eq!(result, "<h1>Hello</h1>");
cleanup_dir(&dir);
}
#[test]
fn test_nolayout_with_config_off() {
let dir = make_temp_dir();
let config = make_config(dir.clone());
let result = apply_layout("{__NOLAYOUT__}<h1>Hello</h1>", &config).unwrap();
assert_eq!(result, "<h1>Hello</h1>");
cleanup_dir(&dir);
}
#[test]
fn test_nolayout_in_layout_file() {
let dir = make_temp_dir();
write_template(
&dir,
"layout",
"{__NOLAYOUT__}<html><body>{__CONTENT__}</body></html>",
);
let config = make_config_layout_on(dir.clone());
let result = apply_layout("<h1>Hello</h1>", &config).unwrap();
assert_eq!(result, "<html><body><h1>Hello</h1></body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_str_replace_non_recursive() {
let dir = make_temp_dir();
write_template(&dir, "layout", "<html><body>{__CONTENT__}</body></html>");
let config = make_config_layout_on(dir.clone());
let result = apply_layout("{__CONTENT__}<h1>Hello</h1>", &config).unwrap();
assert_eq!(
result,
"<html><body>{__CONTENT__}<h1>Hello</h1></body></html>"
);
cleanup_dir(&dir);
}
#[test]
fn test_multiple_content_placeholders() {
let dir = make_temp_dir();
write_template(
&dir,
"layout",
"<header>{__CONTENT__}</header><main>{__CONTENT__}</main>",
);
let config = make_config_layout_on(dir.clone());
let result = apply_layout("<h1>Hello</h1>", &config).unwrap();
assert_eq!(
result,
"<header><h1>Hello</h1></header><main><h1>Hello</h1></main>"
);
cleanup_dir(&dir);
}
#[test]
fn test_resolve_path_simple() {
let config = make_config(PathBuf::from("/view"));
let path = resolve_template_path("layout", &config);
assert_eq!(path, PathBuf::from("/view/layout.html"));
}
#[test]
fn test_resolve_path_with_slash_prefix() {
let config = make_config(PathBuf::from("/view"));
let path = resolve_template_path("/layout", &config);
assert_eq!(path, PathBuf::from("/view/layout.html"));
}
#[test]
fn test_resolve_path_nested() {
let config = make_config(PathBuf::from("/view"));
let path = resolve_template_path("admin/layout", &config);
assert_eq!(path, PathBuf::from("/view/admin/layout.html"));
}
#[test]
fn test_resolve_path_with_colon() {
let config = make_config(PathBuf::from("/view"));
let path = resolve_template_path("admin:layout", &config);
assert_eq!(path, PathBuf::from("/view/admin/layout.html"));
}
#[test]
fn test_resolve_path_with_extension() {
let config = make_config(PathBuf::from("/view"));
let path = resolve_template_path("layout.tpl", &config);
assert_eq!(path, PathBuf::from("layout.tpl"));
}
#[test]
fn test_resolve_path_custom_view_depr() {
let config = ViewConfig {
view_path: PathBuf::from("/view"),
view_depr: ".".to_string(),
..Default::default()
};
let path = resolve_template_path("admin/layout", &config);
assert_eq!(path, PathBuf::from("/view/admin.layout.html"));
}
#[test]
fn test_parse_attr_double_quote() {
let val = parse_attr(r#"name="custom""#, "name");
assert_eq!(val, Some("custom".to_string()));
}
#[test]
fn test_parse_attr_single_quote() {
let val = parse_attr(r#"name='custom'"#, "name");
assert_eq!(val, Some("custom".to_string()));
}
#[test]
fn test_parse_attr_missing() {
let val = parse_attr(r#"other="value""#, "name");
assert_eq!(val, None);
}
#[test]
fn test_parse_attr_with_spaces() {
let val = parse_attr(r#"name = "custom""#, "name");
assert_eq!(val, Some("custom".to_string()));
}
#[test]
fn test_parse_attr_multiple_attrs() {
let tag = r#"{layout name="custom" replace="{__BODY__}"}"#;
assert_eq!(parse_attr(tag, "name"), Some("custom".to_string()));
assert_eq!(parse_attr(tag, "replace"), Some("{__BODY__}".to_string()));
}
#[test]
fn test_empty_content() {
let dir = make_temp_dir();
let config = make_config(dir.clone());
let result = apply_layout("", &config).unwrap();
assert_eq!(result, "");
cleanup_dir(&dir);
}
#[test]
fn test_empty_content_with_layout_on() {
let dir = make_temp_dir();
write_template(&dir, "layout", "<html><body>{__CONTENT__}</body></html>");
let config = make_config_layout_on(dir.clone());
let result = apply_layout("", &config).unwrap();
assert_eq!(result, "<html><body></body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_no_layout_tag_no_nolayout() {
let dir = make_temp_dir();
let config = make_config(dir.clone());
let result = apply_layout("<h1>Hello</h1>", &config).unwrap();
assert_eq!(result, "<h1>Hello</h1>");
cleanup_dir(&dir);
}
#[test]
fn test_layout_tag_not_first() {
let dir = make_temp_dir();
write_template(&dir, "custom", "<html><body>{__CONTENT__}</body></html>");
let config = make_config(dir.clone());
let result = apply_layout(r#"Hello {layout name="custom" /}World"#, &config).unwrap();
assert_eq!(result, "<html><body>Hello World</body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_custom_taglib_delimiters() {
let dir = make_temp_dir();
write_template(&dir, "custom", "<html><body>{__CONTENT__}</body></html>");
let config = ViewConfig {
view_path: dir.clone(),
taglib_begin: "<".to_string(),
taglib_end: "/>".to_string(),
..Default::default()
};
let result = apply_layout(r#"<layout name="custom" /><h1>Hello</h1>"#, &config).unwrap();
assert_eq!(result, "<html><body><h1>Hello</h1></body></html>");
cleanup_dir(&dir);
}
#[test]
fn test_layout_preserves_variable_tags() {
let dir = make_temp_dir();
write_template(
&dir,
"layout",
"<html><body>{__CONTENT__}</body><title>{$title}</title></html>",
);
let config = make_config_layout_on(dir.clone());
let result = apply_layout("<h1>{$name}</h1>", &config).unwrap();
assert_eq!(
result,
"<html><body><h1>{$name}</h1></body><title>{$title}</title></html>"
);
cleanup_dir(&dir);
}
}