summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-02-27 20:25:05 +0800
committer魏曹先生 <1992414357@qq.com>2026-02-27 22:31:21 +0800
commit932050d82212ee7db6bd4cb746fb79f40aa7cbf9 (patch)
treec4c868ead7191510d5a23c998658ba319dc3cdb7 /src
Initialize project with Cargo.toml and basic structure
Diffstat (limited to 'src')
-rw-r--r--src/expand.rs156
-rw-r--r--src/lib.rs71
-rw-r--r--src/test.rs28
-rw-r--r--src/test_expect.txt14
-rw-r--r--src/test_input.txt16
5 files changed, 285 insertions, 0 deletions
diff --git a/src/expand.rs b/src/expand.rs
new file mode 100644
index 0000000..2bcbd06
--- /dev/null
+++ b/src/expand.rs
@@ -0,0 +1,156 @@
+use std::{collections::HashMap, mem::replace};
+
+use just_fmt::snake_case;
+
+use crate::Template;
+
+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 = replace(&mut self.template_str, String::default());
+
+ let (expanded, impl_areas) = read_impl_areas(expanded)?;
+ let expanded = apply_impls(&self, expanded, impl_areas)?;
+ 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.len() < 1 {
+ return None;
+ }
+
+ // Submit Impl Area
+ let name = replace(&mut current_area_name, String::default());
+ 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.len() > 0 {
+ 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.len() > 0 {
+ // 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 template = impl_area_template.clone();
+
+ // Extract parameters
+ for (param_name, param_value) in item {
+ // Apply parameter
+ template = template.replace(
+ &format!("{}{}{}", PARAM_BEGIN, param_name, PARAM_BEND),
+ param_value,
+ );
+ }
+
+ // Add applied template
+ impled_area_code_applied.push(template);
+ }
+
+ 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;
+ };
+
+ 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)
+}
+
+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/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..2df8e46
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,71 @@
+use std::collections::HashMap;
+
+pub mod expand;
+pub mod test;
+
+#[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_insert_with(|| Vec::<HashMap<String, String>>::new())
+ }
+}
+
+impl From<String> for Template {
+ fn from(s: String) -> Self {
+ Template {
+ template_str: s,
+ ..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())
+ }
+}
+
+#[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
+ });
+ )*
+ )*
+ }};
+}
diff --git a/src/test.rs b/src/test.rs
new file mode 100644
index 0000000..f824b1b
--- /dev/null
+++ b/src/test.rs
@@ -0,0 +1,28 @@
+#[cfg(test)]
+mod tests {
+ use crate::{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);
+ }
+}
diff --git a/src/test_expect.txt b/src/test_expect.txt
new file mode 100644
index 0000000..c967707
--- /dev/null
+++ b/src/test_expect.txt
@@ -0,0 +1,14 @@
+// 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
new file mode 100644
index 0000000..97c91c4
--- /dev/null
+++ b/src/test_input.txt
@@ -0,0 +1,16 @@
+// 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,
+ }
+}