1pub mod bash;
7pub mod circleci;
8pub mod dockerfile;
9pub mod github_action;
10pub mod gitlab_ci;
11pub mod makefile;
12pub mod markdown;
13pub mod parameterize;
14
15pub use bash::export_bash;
16pub use circleci::export_circleci;
17pub use dockerfile::export_dockerfile;
18pub use github_action::export_github_action;
19pub use gitlab_ci::export_gitlab_ci;
20pub use makefile::export_makefile;
21pub use markdown::export_markdown;
22pub use parameterize::{Parameter, apply_parameters, detect_all_parameters};
23
24#[must_use]
30pub fn escape_yaml(s: &str) -> String {
31 let needs_quoting = s.is_empty()
32 || s.starts_with(' ')
33 || s.starts_with('#')
34 || s.starts_with('{')
35 || s.starts_with('[')
36 || s.starts_with('*')
37 || s.starts_with('&')
38 || s.starts_with('!')
39 || s.starts_with('|')
40 || s.starts_with('>')
41 || s.starts_with('%')
42 || s.starts_with('@')
43 || s.starts_with('`')
44 || s.starts_with('?')
45 || s.starts_with('-')
46 || s.starts_with(',')
47 || s.starts_with('\'')
48 || s.starts_with('"')
49 || s.contains(": ")
50 || s.contains(" #")
51 || s.contains('{')
52 || s.contains('}')
53 || s.contains('[')
54 || s.contains(']');
55
56 if needs_quoting {
57 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
58 format!("\"{escaped}\"")
59 } else {
60 s.to_string()
61 }
62}
63
64#[must_use]
68pub fn truncate_step_name(command: &str, max_len: usize) -> String {
69 if command.len() <= max_len {
70 command.to_string()
71 } else {
72 format!("{}...", &command[..max_len])
73 }
74}
75
76#[must_use]
81pub fn escape_makefile(s: &str) -> String {
82 s.replace('$', "$$")
83}
84
85#[must_use]
89pub fn format_timestamp(ts: f64) -> String {
90 use chrono::{DateTime, Local};
91
92 let secs = ts as i64;
93 let nanos = ((ts - secs as f64) * 1_000_000_000.0) as u32;
94
95 match DateTime::from_timestamp(secs, nanos) {
96 Some(utc) => {
97 let local = utc.with_timezone(&Local);
98 local.format("%Y-%m-%d %H:%M").to_string()
99 }
100 None => "unknown".to_string(),
101 }
102}
103
104#[must_use]
108pub fn format_duration(seconds: f64) -> String {
109 let total_secs = seconds as u64;
110 let hours = total_secs / 3600;
111 let minutes = (total_secs % 3600) / 60;
112 let secs = total_secs % 60;
113
114 if hours > 0 {
115 format!("{hours}h {minutes}m {secs}s")
116 } else if minutes > 0 {
117 format!("{minutes}m {secs}s")
118 } else {
119 format!("{secs}s")
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_escape_makefile() {
129 assert_eq!(escape_makefile("echo $HOME"), "echo $$HOME");
130 assert_eq!(escape_makefile("no dollars"), "no dollars");
131 assert_eq!(escape_makefile("$A $B"), "$$A $$B");
132 assert_eq!(escape_makefile(""), "");
133 }
134
135 #[test]
136 fn test_format_timestamp() {
137 let result = format_timestamp(1767225600.0);
139 assert!(result.contains("2026") || result.contains("2025")); assert_ne!(result, "unknown");
142 }
143
144 #[test]
145 fn test_format_timestamp_unknown() {
146 let result = format_timestamp(-1e18);
148 assert_eq!(result, "unknown");
149 }
150
151 #[test]
152 fn test_escape_yaml_plain_string() {
153 assert_eq!(escape_yaml("echo hello"), "echo hello");
154 assert_eq!(escape_yaml("npm install"), "npm install");
155 }
156
157 #[test]
158 fn test_escape_yaml_colon_space() {
159 assert_eq!(escape_yaml("echo: world"), "\"echo: world\"");
160 }
161
162 #[test]
163 fn test_escape_yaml_hash_start() {
164 assert_eq!(escape_yaml("#comment"), "\"#comment\"");
165 }
166
167 #[test]
168 fn test_escape_yaml_brace() {
169 assert_eq!(escape_yaml("echo {foo}"), "\"echo {foo}\"");
170 }
171
172 #[test]
173 fn test_escape_yaml_empty_string() {
174 assert_eq!(escape_yaml(""), "\"\"");
175 }
176
177 #[test]
178 fn test_escape_yaml_double_quote_inside() {
179 assert_eq!(escape_yaml("\"hello\""), "\"\\\"hello\\\"\"");
181 }
182
183 #[test]
184 fn test_escape_yaml_backslash_inside() {
185 assert_eq!(escape_yaml("key: val\\ue"), "\"key: val\\\\ue\"");
187 }
188
189 #[test]
190 fn test_escape_yaml_no_quoting_needed() {
191 assert_eq!(escape_yaml("echo hello"), "echo hello");
193 }
194
195 #[test]
196 fn test_truncate_step_name_short() {
197 assert_eq!(truncate_step_name("echo hello", 60), "echo hello");
198 }
199
200 #[test]
201 fn test_truncate_step_name_long() {
202 let long = "a".repeat(61);
203 let result = truncate_step_name(&long, 60);
204 assert_eq!(result.len(), 63); assert!(result.ends_with("..."));
206 assert!(result.starts_with(&"a".repeat(60)));
207 }
208
209 #[test]
210 fn test_format_duration() {
211 assert_eq!(format_duration(5.0), "5s");
212 assert_eq!(format_duration(65.0), "1m 5s");
213 assert_eq!(format_duration(3665.0), "1h 1m 5s");
214 assert_eq!(format_duration(0.0), "0s");
215 assert_eq!(format_duration(3600.0), "1h 0m 0s");
216 }
217}