aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-29 03:31:44 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-29 03:31:44 +0800
commitff70307869a547b13850d1eec3f72e8ca3bca265 (patch)
treea6cc55e7cb993697dce95b3d449538ec296693fc
parent03003aec99fc00c2a079bad9fb4e721432a6e6f0 (diff)
feat: add display block support and refactor tests
Deprecate old macros in favor of new deprecated module, replace the external file-based test with inline unit tests, and introduce the `??? >>>` / `??? <<<` display block syntax for conditional content inclusion per arm or globally.
-rw-r--r--src/deprecated.rs45
-rw-r--r--src/expand.rs74
-rw-r--r--src/lib.rs49
-rw-r--r--src/template.rs4
-rw-r--r--src/test.rs213
-rw-r--r--src/test_expect.txt14
-rw-r--r--src/test_input.txt16
7 files changed, 300 insertions, 115 deletions
diff --git a/src/deprecated.rs b/src/deprecated.rs
new file mode 100644
index 0000000..b218969
--- /dev/null
+++ b/src/deprecated.rs
@@ -0,0 +1,45 @@
+#[macro_export]
+macro_rules! tmpl_param {
+ ($template:ident, $($key:ident = $value:expr),* $(,)?) => {{
+ $(
+ $template.insert_param(stringify!($key).to_string(), $value.to_string());
+ )*
+ }};
+}
+
+#[macro_export]
+macro_rules! tmpl {
+ ($template:ident, $($name:ident {
+ $($key:ident = $value:expr),* $(,)?
+ }),* $(,)?) => {{
+ $(
+ let $name = $template.add_impl(stringify!($name).to_string());
+ $(
+ $name.push({
+ let mut params = std::collections::HashMap::new();
+ params.insert(stringify!($key).to_string(), $value.to_string());
+ params
+ });
+ )*
+ )*
+ }};
+
+ // Old syntax
+ ($template:ident += {
+ $($name:ident {
+ $(($($key:ident = $value:expr),* $(,)?)),*
+ $(,)?
+ }),*
+ }) => {{
+ $(
+ let $name = $template.add_impl(stringify!($name).to_string());
+ $(
+ $name.push({
+ let mut params = std::collections::HashMap::new();
+ $(params.insert(stringify!($key).to_string(), $value.to_string());)*
+ params
+ });
+ )*
+ )*
+ }};
+}
diff --git a/src/expand.rs b/src/expand.rs
index 5933b2c..b609113 100644
--- a/src/expand.rs
+++ b/src/expand.rs
@@ -1,9 +1,12 @@
-use std::{collections::HashMap, mem::replace};
+use std::collections::HashMap;
use just_fmt::snake_case;
use crate::template::Template;
+const DISPLAY_BLOCK_BEGIN: &str = "??? >>> ";
+const DISPLAY_BLOCK_END: &str = "??? <<<";
+
const IMPL_AREA_BEGIN: &str = "@@@ >>> ";
const IMPL_AREA_END: &str = "@@@ <<<";
@@ -15,10 +18,11 @@ const PARAM_BEND: &str = ">>>";
impl Template {
pub fn expand(mut self) -> Option<String> {
// Extract template text
- let expanded = replace(&mut self.template_str, String::default());
+ let expanded = std::mem::take(&mut self.template_str);
let (expanded, impl_areas) = read_impl_areas(expanded)?;
let expanded = apply_impls(&self, expanded, impl_areas)?;
+ let expanded = apply_display_blocks(&self.params, expanded);
let expanded = apply_param(&self, expanded)?;
Some(expanded.trim().to_string())
}
@@ -39,12 +43,12 @@ fn read_impl_areas(content: String) -> Option<(String, HashMap<String, String>)>
if trimmed_line.starts_with(IMPL_AREA_END) {
// If the current ImplArea name length is less than 1, it means no block is being matched,
// so matching fails, exit early
- if current_area_name.len() < 1 {
+ if current_area_name.is_empty() {
return None;
}
// Submit Impl Area
- let name = replace(&mut current_area_name, String::default());
+ let name = std::mem::take(&mut current_area_name);
impl_areas.insert(name, current_area_codes.join("\n"));
current_area_codes.clear();
continue;
@@ -54,7 +58,7 @@ fn read_impl_areas(content: String) -> Option<(String, HashMap<String, String>)>
if trimmed_line.starts_with(IMPL_AREA_BEGIN) {
// If the current ImplArea name length is greater than 0, it means we are already inside a block,
// since nesting is not allowed, matching fails, exit early
- if current_area_name.len() > 0 {
+ if !current_area_name.is_empty() {
return None;
}
@@ -67,7 +71,7 @@ fn read_impl_areas(content: String) -> Option<(String, HashMap<String, String>)>
}
// During implementation block
- if current_area_name.len() > 0 {
+ if !current_area_name.is_empty() {
// Add to current block code
current_area_codes.push(line.to_string());
continue;
@@ -105,19 +109,26 @@ fn apply_impls(
// Split items
for item in impl_items {
// Get base template
- let mut template = impl_area_template.clone();
+ let mut applied = impl_area_template.clone();
+
+ // Merge global params with arm-specific params for display block check
+ let mut display_params = template.params.clone();
+ for (k, v) in item {
+ display_params.insert(k.clone(), v.clone());
+ }
+ applied = apply_display_blocks(&display_params, applied);
// Extract parameters
for (param_name, param_value) in item {
// Apply parameter
- template = template.replace(
+ applied = applied.replace(
&format!("{}{}{}", PARAM_BEGIN, param_name, PARAM_BEND),
param_value,
);
}
// Add applied template
- impled_area_code_applied.push(template);
+ impled_area_code_applied.push(applied);
}
impled_areas.insert(impl_area_name, impled_area_code_applied);
@@ -135,7 +146,7 @@ fn apply_impls(
continue;
};
- if impled_code.len() > 0 {
+ if !impled_code.is_empty() {
applied_content += "\n";
applied_content += impled_code.join("\n").as_str();
}
@@ -149,6 +160,49 @@ fn apply_impls(
Some(applied_content)
}
+/// Process display blocks (`??? >>> name` / `??? <<<`).
+///
+/// If `params` contains a key matching the block name, the block content is
+/// included (with markers removed). Otherwise the entire block is omitted.
+fn apply_display_blocks(params: &HashMap<String, String>, content: String) -> String {
+ let mut result = String::new();
+ let lines: Vec<&str> = content.split("\n").collect();
+ let mut i = 0;
+ let mut first = true;
+
+ while i < lines.len() {
+ let line = lines[i];
+ let trimmed = line.trim();
+
+ if trimmed.starts_with(DISPLAY_BLOCK_BEGIN) {
+ let block_name = trimmed.trim_start_matches(DISPLAY_BLOCK_BEGIN).trim();
+ let show = params.contains_key(block_name);
+ i += 1;
+
+ while i < lines.len() && !lines[i].trim().starts_with(DISPLAY_BLOCK_END) {
+ if show {
+ if !first {
+ result += "\n";
+ }
+ result += lines[i];
+ first = false;
+ }
+ i += 1;
+ }
+ } else if !trimmed.starts_with(DISPLAY_BLOCK_END) {
+ if !first {
+ result += "\n";
+ }
+ result += line;
+ first = false;
+ }
+
+ i += 1;
+ }
+
+ result
+}
+
fn apply_param(template: &Template, content: String) -> Option<String> {
let mut content = content;
for (k, v) in template.params.iter() {
diff --git a/src/lib.rs b/src/lib.rs
index cf9d66e..7ff77f5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -50,50 +50,9 @@ mod template;
pub use template::*; // Re-export template to just_template
pub mod expand;
-pub mod test;
-
-#[macro_export]
-macro_rules! tmpl_param {
- ($template:ident, $($key:ident = $value:expr),* $(,)?) => {{
- $(
- $template.insert_param(stringify!($key).to_string(), $value.to_string());
- )*
- }};
-}
-#[macro_export]
-macro_rules! tmpl {
- ($template:ident, $($name:ident {
- $($key:ident = $value:expr),* $(,)?
- }),* $(,)?) => {{
- $(
- let $name = $template.add_impl(stringify!($name).to_string());
- $(
- $name.push({
- let mut params = std::collections::HashMap::new();
- params.insert(stringify!($key).to_string(), $value.to_string());
- params
- });
- )*
- )*
- }};
+#[cfg(test)]
+pub mod test;
- // Old syntax
- ($template:ident += {
- $($name:ident {
- $(($($key:ident = $value:expr),* $(,)?)),*
- $(,)?
- }),*
- }) => {{
- $(
- let $name = $template.add_impl(stringify!($name).to_string());
- $(
- $name.push({
- let mut params = std::collections::HashMap::new();
- $(params.insert(stringify!($key).to_string(), $value.to_string());)*
- params
- });
- )*
- )*
- }};
-}
+#[deprecated]
+pub mod deprecated;
diff --git a/src/template.rs b/src/template.rs
index 27e14df..e368917 100644
--- a/src/template.rs
+++ b/src/template.rs
@@ -15,9 +15,7 @@ impl Template {
/// Add an implementation block and return a HashMap to set its parameters
pub fn add_impl(&mut self, impl_name: String) -> &mut Vec<HashMap<String, String>> {
- self.impl_params
- .entry(impl_name)
- .or_insert_with(|| Vec::<HashMap<String, String>>::new())
+ self.impl_params.entry(impl_name).or_default()
}
}
diff --git a/src/test.rs b/src/test.rs
index 91407fc..ca7b357 100644
--- a/src/test.rs
+++ b/src/test.rs
@@ -1,28 +1,187 @@
-#[cfg(test)]
-mod tests {
- use crate::{template::Template, tmpl, tmpl_param};
-
- #[test]
- fn expand() {
- let input = std::fs::read_to_string("./src/test_input.txt")
- .unwrap()
- .trim()
- .to_string();
- let expect = std::fs::read_to_string("./src/test_expect.txt")
- .unwrap()
- .trim()
- .to_string();
-
- let mut tmpl = Template::from(input);
- tmpl_param!(tmpl, func_name = "my_func");
- tmpl!(tmpl,
- arms {
- crate_name = "my",
- crate_name = "you",
- }
- );
-
- let expanded = tmpl.expand().unwrap();
- assert_eq!(expanded, expect);
- }
+use std::collections::HashMap;
+
+use crate::template::Template;
+
+#[test]
+fn basic_param() {
+ let mut tmpl = Template::from("Hello, <<<name>>>!".to_string());
+ tmpl.insert_param("name".to_string(), "World".to_string());
+ assert_eq!(tmpl.expand().unwrap(), "Hello, World!");
+}
+
+#[test]
+fn multi_param() {
+ let mut tmpl = Template::from("<<<a>>> + <<<b>>> = <<<c>>>".to_string());
+ tmpl.insert_param("a".to_string(), "1".to_string());
+ tmpl.insert_param("b".to_string(), "2".to_string());
+ tmpl.insert_param("c".to_string(), "3".to_string());
+ assert_eq!(tmpl.expand().unwrap(), "1 + 2 = 3");
+}
+
+#[test]
+fn impl_blocks() {
+ let mut tmpl = Template::from(
+ r#"
+>>>>>>>>>> arms
+@@@ >>> arms
+ "<<<crate_name>>>" => Some(<<<crate_name>>>::exec(data, params).await),
+@@@ <<<
+"#
+ .trim()
+ .to_string(),
+ );
+
+ let arms = tmpl.add_impl("arms".to_string());
+ arms.push(HashMap::from([(
+ "crate_name".to_string(),
+ "my".to_string(),
+ )]));
+ arms.push(HashMap::from([(
+ "crate_name".to_string(),
+ "you".to_string(),
+ )]));
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains(r#""my" => Some(my::exec(data, params).await)"#));
+ assert!(expanded.contains(r#""you" => Some(you::exec(data, params).await)"#));
+}
+
+#[test]
+fn display_block_global_hidden_by_default() {
+ let tmpl = Template::from(
+ r#"
+visible line
+??? >>> debug
+ hidden line
+??? <<<
+visible end
+"#
+ .trim()
+ .to_string(),
+ );
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains("visible line"));
+ assert!(expanded.contains("visible end"));
+ assert!(!expanded.contains("hidden line"));
+}
+
+#[test]
+fn display_block_global_shown_via_param() {
+ let mut tmpl = Template::from(
+ r#"
+visible line
+??? >>> debug
+ shown line
+??? <<<
+visible end
+"#
+ .trim()
+ .to_string(),
+ );
+
+ tmpl.insert_param("debug".to_string(), "".to_string());
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains("visible line"));
+ assert!(expanded.contains("visible end"));
+ assert!(expanded.contains("shown line"));
+}
+
+#[test]
+fn display_block_inside_impl_area_hidden_by_default() {
+ let mut tmpl = Template::from(
+ r#"
+>>>>>>>>>> arms
+@@@ >>> arms
+ <<<crate_name>>> => exec,
+??? >>> extra
+ <<<crate_name>>> => metrics,
+??? <<<
+@@@ <<<
+"#
+ .trim()
+ .to_string(),
+ );
+
+ let arms = tmpl.add_impl("arms".to_string());
+ arms.push(HashMap::from([(
+ "crate_name".to_string(),
+ "my".to_string(),
+ )]));
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains(r#"my => exec"#));
+ assert!(!expanded.contains(r#"my => metrics"#));
+}
+
+#[test]
+fn display_block_inside_impl_area_shown_by_global_param() {
+ let mut tmpl = Template::from(
+ r#"
+>>>>>>>>>> arms
+@@@ >>> arms
+ <<<crate_name>>> => exec,
+??? >>> extra
+ <<<crate_name>>> => metrics,
+??? <<<
+@@@ <<<
+"#
+ .trim()
+ .to_string(),
+ );
+
+ // Enable via global param — shows for ALL arms
+ tmpl.insert_param("extra".to_string(), "".to_string());
+
+ let arms = tmpl.add_impl("arms".to_string());
+ arms.push(HashMap::from([(
+ "crate_name".to_string(),
+ "my".to_string(),
+ )]));
+ arms.push(HashMap::from([(
+ "crate_name".to_string(),
+ "you".to_string(),
+ )]));
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains(r#"my => exec"#));
+ assert!(expanded.contains(r#"my => metrics"#));
+ assert!(expanded.contains(r#"you => exec"#));
+ assert!(expanded.contains(r#"you => metrics"#));
+}
+
+#[test]
+fn display_block_inside_impl_area_shown_by_arm_param() {
+ let mut tmpl = Template::from(
+ r#"
+>>>>>>>>>> arms
+@@@ >>> arms
+ <<<crate_name>>> => exec,
+??? >>> extra
+ <<<crate_name>>> => metrics,
+??? <<<
+@@@ <<<
+"#
+ .trim()
+ .to_string(),
+ );
+
+ let arms = tmpl.add_impl("arms".to_string());
+ // Arm 1: no "extra" → hidden
+ arms.push(HashMap::from([(
+ "crate_name".to_string(),
+ "my".to_string(),
+ )]));
+ // Arm 2: has "extra" → shown for this arm only
+ arms.push(HashMap::from([
+ ("crate_name".to_string(), "you".to_string()),
+ ("extra".to_string(), "".to_string()),
+ ]));
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains(r#"my => exec"#));
+ assert!(!expanded.contains(r#"my => metrics"#));
+ assert!(expanded.contains(r#"you => exec"#));
+ assert!(expanded.contains(r#"you => metrics"#));
}
diff --git a/src/test_expect.txt b/src/test_expect.txt
deleted file mode 100644
index c967707..0000000
--- a/src/test_expect.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-// Auto generated
-use std::collections::HashMap;
-
-pub async fn my_func(
- name: &str,
- data: &[u8],
- params: &HashMap<String, String>,
-) -> Option<Result<Vec<u32>, std::io::Error>> {
- match name {
- "my" => Some(my::exec(data, params).await),
- "you" => Some(you::exec(data, params).await),
- _ => None,
- }
-}
diff --git a/src/test_input.txt b/src/test_input.txt
deleted file mode 100644
index 97c91c4..0000000
--- a/src/test_input.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-// Auto generated
-use std::collections::HashMap;
-
-pub async fn <<<func_name>>>(
- name: &str,
- data: &[u8],
- params: &HashMap<String, String>,
-) -> Option<Result<Vec<u32>, std::io::Error>> {
- match name {
->>>>>>>>>> arms
-@@@ >>> arms
- "<<<crate_name>>>" => Some(<<<crate_name>>>::exec(data, params).await),
-@@@ <<<
- _ => None,
- }
-}