1use handlebars::{Context, Helper, HelperDef, Output, RenderContext, RenderError};
7use thiserror::Error;
8
9#[allow(dead_code)]
10#[derive(Debug, Error)]
12pub enum HelperError {
13 #[error("Failed to convert character to lowercase")]
15 LowercaseConversionError,
16}
17
18#[derive(Clone, Copy)]
20pub struct LowercaseHelper;
21
22impl HelperDef for LowercaseHelper {
23 fn call<'reg: 'rc, 'rc>(
24 &self,
25 h: &Helper<'rc>,
26 _: &'reg handlebars::Handlebars<'reg>,
27 _: &'rc Context,
28 _: &mut RenderContext<'reg, 'rc>,
29 out: &mut dyn Output,
30 ) -> handlebars::HelperResult {
31 let param = h.param(0).ok_or_else(|| {
32 RenderError::from(handlebars::RenderErrorReason::ParamNotFoundForIndex(
33 "lowercase",
34 0,
35 ))
36 })?;
37
38 let value = param.value().as_str().unwrap_or_default().to_lowercase();
39 out.write(&value)?;
40
41 Ok(())
42 }
43}
44
45#[derive(Clone, Copy)]
47pub struct UppercaseHelper;
48
49impl HelperDef for UppercaseHelper {
50 fn call<'reg: 'rc, 'rc>(
51 &self,
52 h: &Helper<'rc>,
53 _: &'reg handlebars::Handlebars<'reg>,
54 _: &'rc Context,
55 _: &mut RenderContext<'reg, 'rc>,
56 out: &mut dyn Output,
57 ) -> handlebars::HelperResult {
58 let param = h.param(0).ok_or_else(|| {
59 RenderError::from(handlebars::RenderErrorReason::ParamNotFoundForIndex(
60 "uppercase",
61 0,
62 ))
63 })?;
64
65 let value = param.value().as_str().unwrap_or_default().to_uppercase();
66 out.write(&value)?;
67
68 Ok(())
69 }
70}
71
72#[derive(Clone, Copy)]
74pub struct SnakeCaseHelper;
75
76impl HelperDef for SnakeCaseHelper {
77 fn call<'reg: 'rc, 'rc>(
78 &self,
79 h: &Helper<'rc>,
80 _: &'reg handlebars::Handlebars<'reg>,
81 _: &'rc Context,
82 _: &mut RenderContext<'reg, 'rc>,
83 out: &mut dyn Output,
84 ) -> handlebars::HelperResult {
85 let param = h.param(0).ok_or_else(|| {
86 RenderError::from(handlebars::RenderErrorReason::ParamNotFoundForIndex(
87 "snake_case",
88 0,
89 ))
90 })?;
91
92 let input = param.value().as_str().unwrap_or_default();
93 let value = to_snake_case(input);
94 out.write(&value)?;
95
96 Ok(())
97 }
98}
99
100#[derive(Clone, Copy)]
102pub struct KebabCaseHelper;
103
104impl HelperDef for KebabCaseHelper {
105 fn call<'reg: 'rc, 'rc>(
106 &self,
107 h: &Helper<'rc>,
108 _: &'reg handlebars::Handlebars<'reg>,
109 _: &'rc Context,
110 _: &mut RenderContext<'reg, 'rc>,
111 out: &mut dyn Output,
112 ) -> handlebars::HelperResult {
113 let param = h.param(0).ok_or_else(|| {
114 RenderError::from(handlebars::RenderErrorReason::ParamNotFoundForIndex(
115 "kebab_case",
116 0,
117 ))
118 })?;
119
120 let input = param.value().as_str().unwrap_or_default();
121 let value = to_kebab_case(input);
122 out.write(&value)?;
123
124 Ok(())
125 }
126}
127
128pub fn to_snake_case(input: &str) -> String {
130 let mut result = String::new();
131 let mut prev_is_sep = false;
132 for (i, c) in input.char_indices() {
133 if c.is_uppercase() {
134 if i > 0 && !prev_is_sep {
135 result.push('_');
136 }
137 if let Some(lc) = c.to_lowercase().next() {
138 result.push(lc);
139 }
140 prev_is_sep = false;
141 } else if c == '-' || c == ' ' || c == '_' {
142 if !prev_is_sep {
143 result.push('_');
144 prev_is_sep = true;
145 }
146 } else {
147 result.push(c);
148 prev_is_sep = false;
149 }
150 }
151 result
152}
153
154pub fn to_kebab_case(input: &str) -> String {
156 let mut result = String::new();
157 let mut prev_is_sep = false;
158 for (i, c) in input.char_indices() {
159 if c.is_uppercase() {
160 if i > 0 && !prev_is_sep {
161 result.push('-');
162 }
163 if let Some(lc) = c.to_lowercase().next() {
164 result.push(lc);
165 }
166 prev_is_sep = false;
167 } else if c == '_' || c == ' ' || c == '-' {
168 if !prev_is_sep {
169 result.push('-');
170 prev_is_sep = true;
171 }
172 } else {
173 result.push(c);
174 prev_is_sep = false;
175 }
176 }
177 result
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use handlebars::Handlebars;
184 use pretty_assertions::assert_eq;
185
186 #[test]
187 fn test_lowercase_helper() {
188 let mut handlebars = Handlebars::new();
189 handlebars.register_helper("lowercase", Box::new(LowercaseHelper));
190
191 let template = "{{lowercase value}}";
192 let mut data = std::collections::HashMap::new();
193 data.insert("value", "TEST-PROJECT");
194
195 let result = handlebars.render_template(template, &data).unwrap();
196 assert_eq!(result, "test-project");
197 }
198
199 #[test]
200 fn test_uppercase_helper() {
201 let mut handlebars = Handlebars::new();
202 handlebars.register_helper("uppercase", Box::new(UppercaseHelper));
203
204 let template = "{{uppercase value}}";
205 let mut data = std::collections::HashMap::new();
206 data.insert("value", "test-project");
207
208 let result = handlebars.render_template(template, &data).unwrap();
209 assert_eq!(result, "TEST-PROJECT");
210 }
211
212 #[test]
213 fn test_snake_case_helper() {
214 let mut handlebars = Handlebars::new();
215 handlebars.register_helper("snake_case", Box::new(SnakeCaseHelper));
216
217 let template = "{{snake_case value}}";
218 let mut data = std::collections::HashMap::new();
219 data.insert("value", "TestProject");
220
221 let result = handlebars.render_template(template, &data).unwrap();
222 assert_eq!(result, "test_project");
223 }
224
225 #[test]
226 fn test_kebab_case_helper() {
227 let mut handlebars = Handlebars::new();
228 handlebars.register_helper("kebab_case", Box::new(KebabCaseHelper));
229
230 let template = "{{kebab_case value}}";
231 let mut data = std::collections::HashMap::new();
232 data.insert("value", "test_project");
233
234 let result = handlebars.render_template(template, &data).unwrap();
235 assert_eq!(result, "test-project");
236 }
237
238 #[test]
239 fn test_helper_missing_param() {
240 let mut handlebars = Handlebars::new();
241 handlebars.register_helper("lowercase", Box::new(LowercaseHelper));
242 handlebars.register_helper("uppercase", Box::new(UppercaseHelper));
243 handlebars.register_helper("snake_case", Box::new(SnakeCaseHelper));
244 handlebars.register_helper("kebab_case", Box::new(KebabCaseHelper));
245
246 let helpers = ["lowercase", "uppercase", "snake_case", "kebab_case"];
247 for helper in helpers {
248 let template = format!("{{{{{helper}}}}}"); let result = handlebars
250 .render_template(&template, &std::collections::HashMap::<&str, &str>::new());
251 assert!(
252 result.is_err(),
253 "helper '{helper}' should error on missing param"
254 );
255 }
256 }
257
258 #[test]
259 fn test_helper_non_string_param() {
260 let mut handlebars = Handlebars::new();
261 handlebars.register_helper("lowercase", Box::new(LowercaseHelper));
262 handlebars.register_helper("uppercase", Box::new(UppercaseHelper));
263 handlebars.register_helper("snake_case", Box::new(SnakeCaseHelper));
264 handlebars.register_helper("kebab_case", Box::new(KebabCaseHelper));
265
266 let mut data = std::collections::HashMap::new();
267 data.insert("value", 123);
268
269 let helpers = ["lowercase", "uppercase", "snake_case", "kebab_case"];
270 for helper in helpers {
271 let template = format!("{{{{{helper} value}}}}");
272 let result = handlebars.render_template(&template, &data);
273 assert!(result.is_ok());
275 assert_eq!(result.unwrap(), "");
276 }
277 }
278
279 #[test]
280 fn test_to_snake_case_edge_cases() {
281 assert_eq!(to_snake_case("").as_str(), "");
282 assert_eq!(to_snake_case("Already_Snake").as_str(), "already_snake");
283 assert_eq!(to_snake_case("with-dash").as_str(), "with_dash");
284 assert_eq!(to_snake_case("with space").as_str(), "with_space");
285 assert_eq!(to_snake_case("123ABC").as_str(), "123_a_b_c");
286 }
287
288 #[test]
289 fn test_to_kebab_case_edge_cases() {
290 assert_eq!(to_kebab_case("").as_str(), "");
291 assert_eq!(to_kebab_case("Already-Kebab").as_str(), "already-kebab");
292 assert_eq!(to_kebab_case("with_underscore").as_str(), "with-underscore");
293 assert_eq!(to_kebab_case("with space").as_str(), "with-space");
294 assert_eq!(to_kebab_case("123ABC").as_str(), "123-a-b-c");
295 }
296}