aboutsummaryrefslogtreecommitdiff
path: root/just_template/src
diff options
context:
space:
mode:
Diffstat (limited to 'just_template/src')
-rw-r--r--just_template/src/deprecated.rs45
-rw-r--r--just_template/src/expand.rs212
-rw-r--r--just_template/src/lib.rs58
-rw-r--r--just_template/src/template.rs54
-rw-r--r--just_template/src/test.rs187
5 files changed, 556 insertions, 0 deletions
diff --git a/just_template/src/deprecated.rs b/just_template/src/deprecated.rs
new file mode 100644
index 0000000..b218969
--- /dev/null
+++ b/just_template/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/just_template/src/expand.rs b/just_template/src/expand.rs
new file mode 100644
index 0000000..b609113
--- /dev/null
+++ b/just_template/src/expand.rs
@@ -0,0 +1,212 @@
+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 = "@@@ <<<";
+
+const IMPL_BEGIN: &str = ">>>>>>>>>>";
+
+const PARAM_BEGIN: &str = "<<<";
+const PARAM_BEND: &str = ">>>";
+
+impl Template {
+ pub fn expand(mut self) -> Option<String> {
+ // Extract template text
+ 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())
+ }
+}
+
+/// Read all ImplAreas (HashMap<Name, Codes>)
+fn read_impl_areas(content: String) -> Option<(String, HashMap<String, String>)> {
+ let mut striped_content = String::new();
+ let mut impl_areas: HashMap<String, String> = HashMap::new();
+
+ let mut current_area_name = String::default();
+ let mut current_area_codes: Vec<String> = Vec::new();
+
+ for line in content.split("\n") {
+ let trimmed_line = line.trim();
+
+ // Implementation block end
+ 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.is_empty() {
+ return None;
+ }
+
+ // Submit Impl Area
+ let name = std::mem::take(&mut current_area_name);
+ impl_areas.insert(name, current_area_codes.join("\n"));
+ current_area_codes.clear();
+ continue;
+ }
+
+ // Implementation block start
+ 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.is_empty() {
+ return None;
+ }
+
+ // Get a snake_case name
+ let snake_name = snake_case!(line.trim_start_matches(IMPL_AREA_BEGIN).trim());
+ current_area_name = snake_name;
+
+ // Continue to next line
+ continue;
+ }
+
+ // During implementation block
+ if !current_area_name.is_empty() {
+ // Add to current block code
+ current_area_codes.push(line.to_string());
+ continue;
+ } else {
+ // Add to remaining content
+ striped_content += "\n";
+ striped_content += line;
+ }
+ }
+
+ Some((striped_content, impl_areas))
+}
+
+/// Apply Template parameters to implementation block areas
+fn apply_impls(
+ template: &Template,
+ content: String,
+ impl_areas: HashMap<String, String>,
+) -> Option<String> {
+ let mut applied_content = String::new();
+
+ let mut impled_areas: HashMap<String, Vec<String>> = HashMap::new();
+ for (impl_area_name, impl_area_template) in impl_areas {
+ // Get user-provided parameters
+ let impl_items = template.impl_params.get(&impl_area_name);
+
+ // No parameters, return early
+ let Some(impl_items) = impl_items else {
+ impled_areas.insert(impl_area_name, Vec::new());
+ continue;
+ };
+
+ let mut impled_area_code_applied = Vec::new();
+
+ // Split items
+ for item in impl_items {
+ // Get base template
+ 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
+ applied = applied.replace(
+ &format!("{}{}{}", PARAM_BEGIN, param_name, PARAM_BEND),
+ param_value,
+ );
+ }
+
+ // Add applied template
+ impled_area_code_applied.push(applied);
+ }
+
+ impled_areas.insert(impl_area_name, impled_area_code_applied);
+ }
+
+ for line in content.split("\n") {
+ let trimmed_line = line.trim();
+
+ // Recognize implementation line
+ if trimmed_line.starts_with(IMPL_BEGIN) {
+ let impl_name = snake_case!(trimmed_line.trim_start_matches(IMPL_BEGIN).trim());
+
+ // Try to get implementation code block
+ let Some(impled_code) = impled_areas.get(&impl_name) else {
+ continue;
+ };
+
+ if !impled_code.is_empty() {
+ applied_content += "\n";
+ applied_content += impled_code.join("\n").as_str();
+ }
+ } else {
+ // Other content directly appended
+ applied_content += "\n";
+ applied_content += line;
+ }
+ }
+
+ 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() {
+ content = content.replace(&format!("{}{}{}", PARAM_BEGIN, k, PARAM_BEND), v);
+ }
+ Some(content)
+}
diff --git a/just_template/src/lib.rs b/just_template/src/lib.rs
new file mode 100644
index 0000000..7ff77f5
--- /dev/null
+++ b/just_template/src/lib.rs
@@ -0,0 +1,58 @@
+//! Template struct for storing template strings and their parameters.
+//!
+//! The template supports two types of parameters:
+//! - Simple parameters: key-value pairs used to replace simple placeholders (`<<<key>>>` format) in the template.
+//! - Implementation parameters: for implementation blocks (`>>>>>>>>> block_name` and `@@@ >>> block_name` format),
+//! can contain multiple parameter sets, each corresponding to an implementation instance.
+//!
+//! # Examples
+//! ```
+//! use just_template::Template;
+//!
+//! let mut tmpl = Template::from("Hello, <<<name>>>!".to_string());
+//! tmpl.insert_param("name".to_string(), "World".to_string());
+//! assert_eq!(tmpl.to_string(), "Hello, World!");
+//! ```
+//!
+//! Using the `tmpl_param!` macro makes it easier to add simple parameters:
+//! ```
+//! use just_template::{Template, tmpl_param};
+//!
+//! let mut tmpl = Template::from("<<<a>>> + <<<b>>> = <<<c>>>".to_string());
+//! tmpl_param!(tmpl, a = 1, b = 2, c = 3);
+//! assert_eq!(tmpl.to_string(), "1 + 2 = 3");
+//! ```
+//!
+//! Using the `tmpl!` macro adds implementation block parameters:
+//! ```
+//! use just_template::{Template, tmpl};
+//!
+//! let mut tmpl = Template::from("
+//! >>>>>>>>>> arms
+//! @@@ >>> arms
+//! <<<crate_name>>> => Some(<<<crate_name>>>::exec(data, params).await),
+//! @@@ <<<
+//! ".trim().to_string());
+//! tmpl!(tmpl,
+//! arms {
+//! crate_name = "my",
+//! crate_name = "you",
+//! }
+//! );
+//! // Output the expanded template
+//! let expanded = tmpl.to_string();
+//! assert_eq!(expanded, "
+//! my => Some(my::exec(data, params).await),
+//! you => Some(you::exec(data, params).await),
+//! ".trim().to_string());
+//! ```
+mod template;
+pub use template::*; // Re-export template to just_template
+
+pub mod expand;
+
+#[cfg(test)]
+pub mod test;
+
+#[deprecated]
+pub mod deprecated;
diff --git a/just_template/src/template.rs b/just_template/src/template.rs
new file mode 100644
index 0000000..e368917
--- /dev/null
+++ b/just_template/src/template.rs
@@ -0,0 +1,54 @@
+use std::collections::HashMap;
+
+#[derive(Default, Clone)]
+pub struct Template {
+ pub(crate) template_str: String,
+ pub(crate) params: HashMap<String, String>,
+ pub(crate) impl_params: HashMap<String, Vec<HashMap<String, String>>>,
+}
+
+impl Template {
+ /// Add a parameter
+ pub fn insert_param(&mut self, name: String, value: String) {
+ self.params.insert(name, value);
+ }
+
+ /// 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_default()
+ }
+}
+
+impl From<String> for Template {
+ fn from(s: String) -> Self {
+ Template {
+ template_str: s,
+ ..Default::default()
+ }
+ }
+}
+
+impl<'a> From<&'a str> for Template {
+ fn from(s: &'a str) -> Self {
+ Template {
+ template_str: s.to_string(),
+ ..Default::default()
+ }
+ }
+}
+
+impl<'a> From<std::borrow::Cow<'a, str>> for Template {
+ fn from(s: std::borrow::Cow<'a, str>) -> Self {
+ Template {
+ template_str: s.into_owned(),
+ ..Default::default()
+ }
+ }
+}
+
+impl std::fmt::Display for Template {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let cloned = self.clone();
+ write!(f, "{}", cloned.expand().unwrap_or_default())
+ }
+}
diff --git a/just_template/src/test.rs b/just_template/src/test.rs
new file mode 100644
index 0000000..ca7b357
--- /dev/null
+++ b/just_template/src/test.rs
@@ -0,0 +1,187 @@
+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"#));
+}