use std::collections::HashMap;
use std::collections::HashSet;
use std::path::PathBuf;
use indexmap::IndexMap;
use regex::Regex;
use super::{ViewConfig, ViewError};
pub fn apply_inheritance(content: &str, config: &ViewConfig) -> Result<String, ViewError> {
let mut state = ExtendState::default();
find_extend_recursive(content, config, &mut state)?;
if state.extend.is_empty() {
return Ok(content.to_string());
}
let result = merge_blocks(&state.extend, &state.base_blocks, &mut state.blocks);
Ok(result)
}
#[derive(Default)]
struct ExtendState {
extend: String,
blocks: IndexMap<String, BlockInfo>,
base_blocks: IndexMap<String, BlockInfo>,
visited: HashSet<String>,
}
#[derive(Debug, Clone)]
struct BlockInfo {
#[allow(dead_code)]
name: String,
begin: String,
content: String,
end: String,
parent: String,
}
fn find_extend_recursive(
template: &str,
config: &ViewConfig,
state: &mut ExtendState,
) -> Result<(), ViewError> {
let extend_name = parse_extend_name(template, config);
if let Some(name) = extend_name {
if !state.visited.contains(&name) {
state.visited.insert(name.clone());
let extend_file = resolve_template_path(&name, config);
if !extend_file.is_file() {
return Err(ViewError::TemplateNotFound(format!(
"继承模板: {} (解析路径: {})",
name,
extend_file.display()
)));
}
let extend_content = std::fs::read_to_string(&extend_file)?;
state.extend = extend_content.clone();
find_extend_recursive(&extend_content, config, state)?;
let current_blocks = parse_blocks(template, false, config);
for (k, v) in current_blocks {
state.blocks.insert(k, v);
}
}
} else {
state.base_blocks = parse_blocks(template, true, config);
if state.extend.is_empty() {
state.extend = template.to_string();
}
}
Ok(())
}
fn parse_extend_name(content: &str, config: &ViewConfig) -> Option<String> {
let begin = regex::escape(&config.taglib_begin);
let end = regex::escape(&config.taglib_end);
let end_char = regex::escape(&config.taglib_end);
let pattern = format!(
"{begin}extend\\b\\s+[^{end_char}]+{end}",
begin = begin,
end = end,
end_char = end_char,
);
let re = Regex::new(&pattern).ok()?;
let caps = re.captures(content)?;
let full_match = caps.get(0)?;
let name = parse_attr(full_match.as_str(), "name");
match name {
Some(n) if !n.is_empty() => Some(n),
_ => None,
}
}
fn parse_blocks(content: &str, sort: bool, config: &ViewConfig) -> IndexMap<String, BlockInfo> {
let begin = regex::escape(&config.taglib_begin);
let end = regex::escape(&config.taglib_end);
let end_char = regex::escape(&config.taglib_end);
let pattern = format!(
"{begin}(?:block\\b\\s+[^{end_char}]+|/block){end}",
begin = begin,
end = end,
end_char = end_char,
);
let re = match Regex::new(&pattern) {
Ok(r) => r,
Err(_) => return IndexMap::new(),
};
let mut stack: Vec<(String, usize, String)> = Vec::new();
let mut result: IndexMap<String, BlockInfo> = IndexMap::new();
let mut keys: IndexMap<String, usize> = IndexMap::new();
let block_open_prefix = format!("{}block", config.taglib_begin);
let block_close_prefix = format!("{}/block", config.taglib_begin);
for caps in re.captures_iter(content) {
let full_match = caps.get(0).expect("正则捕获组 0 必定存在");
let full_str = full_match.as_str();
if full_str.starts_with(&block_close_prefix) {
if let Some((name, offset, tag)) = stack.pop() {
let start = offset + tag.len();
let block_content = &content[start..full_match.start()];
let parent = stack.last().map(|(n, _, _)| n.clone()).unwrap_or_default();
let end_pos = full_match.end();
result.insert(
name.clone(),
BlockInfo {
name: name.clone(),
begin: tag,
content: block_content.to_string(),
end: full_str.to_string(),
parent,
},
);
keys.insert(name, end_pos);
}
} else if full_str.starts_with(&block_open_prefix) {
let name = parse_attr(full_str, "name").unwrap_or_default();
if !name.is_empty() {
stack.push((name, full_match.start(), full_str.to_string()));
}
}
}
if sort {
let mut pairs: Vec<(String, BlockInfo, usize)> = keys
.into_iter()
.map(|(name, pos)| {
let info = result
.get(&name)
.cloned()
.expect("keys 与 result 同源,name 必定存在");
(name, info, pos)
})
.collect();
pairs.sort_by_key(|(_, _, pos)| *pos);
result.clear();
for (name, info, _) in pairs {
result.insert(name, info);
}
}
result
}
fn merge_blocks(
extend: &str,
base_blocks: &IndexMap<String, BlockInfo>,
blocks: &mut IndexMap<String, BlockInfo>,
) -> String {
let mut result = extend.to_string();
let mut children: HashMap<String, Vec<String>> = HashMap::new();
for (name, val) in base_blocks.iter() {
let name = name.clone();
let val = val.clone();
let mut replace = val.content.clone();
let mut skip_top_replace = false;
if let Some(child_keys) = children.get(&name).cloned() {
for key in &child_keys {
if let (Some(child_base), Some(child_block)) =
(base_blocks.get(key), blocks.get(key))
{
let search = format!(
"{}{}{}",
child_base.begin, child_base.content, child_base.end
);
let replacement = &child_block.content;
replace = replace.replace(&search, replacement);
}
}
}
if let Some(block) = blocks.get(&name).cloned() {
let merged = block
.content
.replace("{__BLOCK__}", &replace)
.replace("{__block__}", &replace);
if !val.parent.is_empty() {
let parent = val.parent.clone();
let search = format!("{}{}{}", block.begin, block.content, block.end);
if let Some(parent_block) = blocks.get_mut(&parent) {
parent_block.content = parent_block.content.replace(&search, &merged);
}
if let Some(b) = blocks.get_mut(&name) {
b.content = merged.clone();
}
children.entry(parent).or_default().push(name);
skip_top_replace = true;
} else {
replace = merged;
}
} else if !val.parent.is_empty() {
children
.entry(val.parent.clone())
.or_default()
.push(name.clone());
blocks.insert(name, val.clone());
}
if !skip_top_replace && val.parent.is_empty() {
let search = format!("{}{}{}", val.begin, val.content, val.end);
result = result.replace(&search, &replace);
}
}
result
}
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;
fn make_temp_dir() -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"sz_rust_inheritance_test_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
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()
}
}
#[test]
fn test_basic_inheritance() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
"<html>{block name=\"content\"}default{/block}</html>",
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="content"}hello{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, "<html>hello</html>");
cleanup_dir(&dir);
}
#[test]
fn test_inheritance_block_not_overridden() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
"<html>{block name=\"content\"}default{/block}</html>",
);
let config = make_config(dir.clone());
let result = apply_inheritance(r#"{extend name="base"}"#, &config).unwrap();
assert_eq!(result, "<html>default</html>");
cleanup_dir(&dir);
}
#[test]
fn test_no_extend_no_block() {
let dir = make_temp_dir();
let config = make_config(dir.clone());
let result = apply_inheritance("<h1>Hello</h1>", &config).unwrap();
assert_eq!(result, "<h1>Hello</h1>");
cleanup_dir(&dir);
}
#[test]
fn test_no_extend_but_has_block() {
let dir = make_temp_dir();
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"<html>{block name="content"}hello{/block}</html>"#,
&config,
)
.unwrap();
assert_eq!(result, r#"<html>hello</html>"#);
cleanup_dir(&dir);
}
#[test]
fn test_block_merge_with_block_uppercase() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
"<html>{block name=\"content\"}base{/block}</html>",
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="content"}{__BLOCK__} child{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, "<html>base child</html>");
cleanup_dir(&dir);
}
#[test]
fn test_block_merge_with_block_lowercase() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
"<html>{block name=\"content\"}base{/block}</html>",
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="content"}{__block__} child{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, "<html>base child</html>");
cleanup_dir(&dir);
}
#[test]
fn test_block_override_without_merge_marker() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
"<html>{block name=\"content\"}base{/block}</html>",
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="content"}child{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, "<html>child</html>");
cleanup_dir(&dir);
}
#[test]
fn test_multiple_blocks() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
r#"<html><head>{block name="title"}default title{/block}</head><body>{block name="content"}default content{/block}</body></html>"#,
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="title"}My Title{/block}{block name="content"}My Content{/block}"#,
&config,
)
.unwrap();
assert_eq!(
result,
r#"<html><head>My Title</head><body>My Content</body></html>"#
);
cleanup_dir(&dir);
}
#[test]
fn test_multiple_blocks_partial_override() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
r#"<html><head>{block name="title"}default title{/block}</head><body>{block name="content"}default content{/block}</body></html>"#,
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="title"}My Title{/block}"#,
&config,
)
.unwrap();
assert_eq!(
result,
r#"<html><head>My Title</head><body>default content</body></html>"#
);
cleanup_dir(&dir);
}
#[test]
fn test_nested_block_override_inner() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
r#"<html>{block name="outer"}outer {block name="inner"}inner default{/block}{/block}</html>"#,
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="inner"}inner override{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, "<html>outer inner override</html>");
cleanup_dir(&dir);
}
#[test]
fn test_nested_block_override_outer() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
r#"<html>{block name="outer"}outer {block name="inner"}inner default{/block}{/block}</html>"#,
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="outer"}completely replaced{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, "<html>completely replaced</html>");
cleanup_dir(&dir);
}
#[test]
fn test_nested_block_override_both() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
r#"<html>{block name="outer"}outer {block name="inner"}inner default{/block}{/block}</html>"#,
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="outer"}new outer {block name="inner"}new inner{/block}{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, "<html>new outer new inner</html>");
cleanup_dir(&dir);
}
#[test]
fn test_multi_level_inheritance() {
let dir = make_temp_dir();
write_template(
&dir,
"grandparent",
"<html>{block name=\"content\"}grandparent{/block}</html>",
);
write_template(
&dir,
"parent",
r#"{extend name="grandparent"}{block name="content"}parent{/block}"#,
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="parent"}{block name="content"}child{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, "<html>child</html>");
cleanup_dir(&dir);
}
#[test]
fn test_multi_level_inheritance_partial_override() {
let dir = make_temp_dir();
write_template(
&dir,
"grandparent",
r#"<html>{block name="title"}gp title{/block}{block name="content"}gp content{/block}</html>"#,
);
write_template(
&dir,
"parent",
r#"{extend name="grandparent"}{block name="title"}p title{/block}"#,
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="parent"}{block name="content"}child content{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, r#"<html>p titlechild content</html>"#);
cleanup_dir(&dir);
}
#[test]
fn test_circular_inheritance_php_bug() {
let dir = make_temp_dir();
write_template(
&dir,
"a",
r#"{extend name="b"}{block name="content"}a content{/block}"#,
);
write_template(
&dir,
"b",
r#"{extend name="a"}{block name="content"}b content{/block}"#,
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="a"}{block name="content"}start{/block}"#,
&config,
)
.unwrap();
assert_eq!(
result,
r#"{extend name="a"}{block name="content"}b content{/block}"#
);
cleanup_dir(&dir);
}
#[test]
fn test_self_inheritance_php_bug() {
let dir = make_temp_dir();
write_template(
&dir,
"self_ref",
r#"{extend name="self_ref"}{block name="content"}content{/block}"#,
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="self_ref"}{block name="content"}start{/block}"#,
&config,
)
.unwrap();
assert_eq!(
result,
r#"{extend name="self_ref"}{block name="content"}content{/block}"#
);
cleanup_dir(&dir);
}
#[test]
fn test_extend_template_not_found() {
let dir = make_temp_dir();
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="nonexistent"}{block name="content"}hello{/block}"#,
&config,
);
assert!(matches!(result, Err(ViewError::TemplateNotFound(_))));
cleanup_dir(&dir);
}
#[test]
fn test_parse_blocks_basic() {
let config = ViewConfig::default();
let blocks = parse_blocks(
r#"<html>{block name="a"}content a{/block}</html>"#,
false,
&config,
);
assert_eq!(blocks.len(), 1);
let block_a = blocks.get("a").unwrap();
assert_eq!(block_a.name, "a");
assert_eq!(block_a.content, "content a");
assert_eq!(block_a.parent, "");
}
#[test]
fn test_parse_blocks_nested() {
let config = ViewConfig::default();
let blocks = parse_blocks(
r#"{block name="outer"}outer {block name="inner"}inner{/block}{/block}"#,
false,
&config,
);
assert_eq!(blocks.len(), 2);
let outer = blocks.get("outer").unwrap();
let inner = blocks.get("inner").unwrap();
assert_eq!(outer.parent, "");
assert_eq!(inner.parent, "outer");
assert_eq!(outer.content, r#"outer {block name="inner"}inner{/block}"#);
assert_eq!(inner.content, "inner");
}
#[test]
fn test_parse_blocks_sort() {
let config = ViewConfig::default();
let blocks = parse_blocks(
r#"{block name="z"}z{/block}{block name="a"}a{/block}"#,
true,
&config,
);
let keys: Vec<&String> = blocks.keys().collect();
assert_eq!(keys[0], "z");
assert_eq!(keys[1], "a");
}
#[test]
fn test_parse_blocks_no_blocks() {
let config = ViewConfig::default();
let blocks = parse_blocks("<html>no blocks</html>", false, &config);
assert_eq!(blocks.len(), 0);
}
#[test]
fn test_parse_blocks_same_name_overrides() {
let config = ViewConfig::default();
let blocks = parse_blocks(
r#"{block name="a"}first{/block}{block name="a"}second{/block}"#,
false,
&config,
);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks.get("a").unwrap().content, "second");
}
#[test]
fn test_parse_extend_name_basic() {
let config = ViewConfig::default();
let name = parse_extend_name(r#"{extend name="base"}"#, &config);
assert_eq!(name, Some("base".to_string()));
}
#[test]
fn test_parse_extend_name_no_extend() {
let config = ViewConfig::default();
let name = parse_extend_name("<html>no extend</html>", &config);
assert_eq!(name, None);
}
#[test]
fn test_parse_extend_name_no_name_attr() {
let config = ViewConfig::default();
let name = parse_extend_name(r#"{extend foo}"#, &config);
assert_eq!(name, None);
}
#[test]
fn test_resolve_template_path_basic() {
let config = ViewConfig {
view_path: PathBuf::from("view"),
..Default::default()
};
let path = resolve_template_path("base", &config);
assert_eq!(path, PathBuf::from("view/base.html"));
}
#[test]
fn test_resolve_template_path_with_slash() {
let config = ViewConfig {
view_path: PathBuf::from("view"),
..Default::default()
};
let path = resolve_template_path("/base", &config);
assert_eq!(path, PathBuf::from("view/base.html"));
}
#[test]
fn test_resolve_template_path_with_extension() {
let config = ViewConfig {
view_path: PathBuf::from("view"),
..Default::default()
};
let path = resolve_template_path("base.tpl", &config);
assert_eq!(path, PathBuf::from("base.tpl"));
}
#[test]
fn test_resolve_template_path_custom_depr() {
let config = ViewConfig {
view_path: PathBuf::from("view"),
view_depr: ".".to_string(),
..Default::default()
};
let path = resolve_template_path("admin/base", &config);
assert_eq!(path, PathBuf::from("view/admin.base.html"));
}
#[test]
fn test_r5_php_basic_inheritance_alignment() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
"<html>{block name=\"content\"}default{/block}</html>",
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="content"}override{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, "<html>override</html>");
cleanup_dir(&dir);
}
#[test]
fn test_r5_php_block_merge_alignment() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
"<html>{block name=\"content\"}base{/block}</html>",
);
let config = make_config(dir.clone());
let result_upper = apply_inheritance(
r#"{extend name="base"}{block name="content"}{__BLOCK__} extended{/block}"#,
&config,
)
.unwrap();
assert_eq!(result_upper, "<html>base extended</html>");
let result_lower = apply_inheritance(
r#"{extend name="base"}{block name="content"}{__block__} extended{/block}"#,
&config,
)
.unwrap();
assert_eq!(result_lower, "<html>base extended</html>");
cleanup_dir(&dir);
}
#[test]
fn test_r5_php_nested_block_alignment() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
r#"<html>{block name="outer"}o-{block name="inner"}i{/block}{/block}</html>"#,
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="inner"}I{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, "<html>o-I</html>");
cleanup_dir(&dir);
}
#[test]
fn test_r5_php_multi_level_inheritance_alignment() {
let dir = make_temp_dir();
write_template(&dir, "c", "<html>{block name=\"x\"}C{/block}</html>");
write_template(&dir, "b", r#"{extend name="c"}{block name="x"}B{/block}"#);
let config = make_config(dir.clone());
let result =
apply_inheritance(r#"{extend name="b"}{block name="x"}A{/block}"#, &config).unwrap();
assert_eq!(result, "<html>A</html>");
cleanup_dir(&dir);
}
#[test]
fn test_r5_php_circular_inheritance_bug_alignment() {
let dir = make_temp_dir();
write_template(&dir, "a", r#"{extend name="b"}A{/block}"#);
write_template(&dir, "b", r#"{extend name="a"}B{/block}"#);
let config = make_config(dir.clone());
let result = apply_inheritance(r#"{extend name="a"}"#, &config).unwrap();
assert_eq!(result, r#"{extend name="a"}B{/block}"#);
cleanup_dir(&dir);
}
#[test]
fn test_r5_php_sort_alignment() {
let config = ViewConfig::default();
let blocks = parse_blocks(
r#"{block name="a"}a{/block}{block name="b"}b{/block}"#,
true,
&config,
);
let keys: Vec<&String> = blocks.keys().collect();
assert_eq!(keys[0], "a");
assert_eq!(keys[1], "b");
}
#[test]
fn test_r5_php_no_extend_has_block_alignment() {
let dir = make_temp_dir();
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"<html>{block name="content"}hello{/block}</html>"#,
&config,
)
.unwrap();
assert_eq!(result, r#"<html>hello</html>"#);
cleanup_dir(&dir);
}
#[test]
fn test_r5_php_multiple_blocks_order_alignment() {
let dir = make_temp_dir();
write_template(
&dir,
"base",
r#"<html>{block name="header"}H{/block}{block name="footer"}F{/block}</html>"#,
);
let config = make_config(dir.clone());
let result = apply_inheritance(
r#"{extend name="base"}{block name="footer"}new footer{/block}{block name="header"}new header{/block}"#,
&config,
)
.unwrap();
assert_eq!(result, r#"<html>new headernew footer</html>"#);
cleanup_dir(&dir);
}
}