1mod actions;
2pub mod collector;
3mod html_tag;
4pub mod injector;
5
6use core::fmt;
7use std::{collections::BTreeMap, path::PathBuf, str::FromStr};
8
9use pest_derive::Parser;
10use serde::{Deserialize, Serialize};
11
12#[cfg(feature = "exec")]
13use crate::parser::actions::exec;
14
15#[derive(Parser)]
16#[grammar = "snippet.pest"]
17pub struct SnippetParse;
18
19#[derive(Serialize, Deserialize, Debug)]
20pub struct Snippet {
21 pub id: String,
22 pub content: String,
23 pub kind: SnippetKind,
24 pub path: PathBuf,
25}
26
27#[derive(Serialize, Deserialize, Debug, Clone)]
28pub struct SnippetTemplate {
29 pub content: String,
30}
31
32#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
33#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
34pub enum SnippetKind {
35 Yaml,
36 Code,
37 #[default]
38 Any,
39}
40
41impl fmt::Display for SnippetKind {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(f, "{self:?}")
44 }
45}
46
47impl FromStr for SnippetKind {
48 type Err = ();
49
50 fn from_str(input: &str) -> std::result::Result<Self, ()> {
51 match input {
52 "yaml" => Ok(Self::Yaml),
53 "code" => Ok(Self::Code),
54 "any" => Ok(Self::Any),
55 _ => Err(()),
56 }
57 }
58}
59
60impl Snippet {
61 #[must_use]
64 pub fn create_content(
65 &self,
66 inject_actions: &injector::InjectContentAction,
67 custom_templates: &BTreeMap<String, SnippetTemplate>,
68 ) -> String {
69 #[cfg(feature = "exec")]
70 let content = if inject_actions.kind == injector::InjectAction::Exec {
71 exec::run(&self.content).unwrap_or_else(|err| {
72 tracing::error!(err, "execute snippet command failed");
73 self.content.to_string()
74 })
75 } else {
76 self.content.to_string()
77 };
78
79 #[cfg(not(feature = "exec"))]
80 let content = self.content.to_string();
81
82 let content = inject_actions
83 .template
84 .before_inject(&content, custom_templates);
85
86 let content = content
87 .lines()
88 .filter_map(|line| {
89 if line.contains("<snip") || line.contains("</snip") {
91 return None;
92 }
93 let line = inject_actions.strip_prefix.as_ref().map_or_else(
94 || line.to_string(),
95 |prefix_inject| line.strip_prefix(prefix_inject).unwrap_or(line).to_string(),
96 );
97
98 if let Some(add_prefix) = &inject_actions.add_prefix {
99 Some(format!("{add_prefix}{line}"))
100 } else {
101 Some(line)
102 }
103 })
104 .collect::<Vec<_>>()
105 .join(crate::LINE_ENDING);
106
107 inject_actions
108 .template
109 .after_inject(&content, &inject_actions.kind)
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use insta::{assert_debug_snapshot, with_settings};
116
117 use super::*;
118 use crate::{
119 parser::injector::{InjectAction, Template},
120 tests_cfg,
121 };
122
123 #[test]
124 fn can_get_snippet_content_without_action() {
125 let snippet = tests_cfg::get_snippet();
126
127 let action = injector::InjectContentAction {
128 kind: InjectAction::Copy,
129 snippet_id: "id".to_string(),
130 inject_from: SnippetKind::Any,
131 strip_prefix: None,
132 add_prefix: None,
133 template: Template::default(),
134 };
135
136 with_settings!({filters => tests_cfg::redact::all()}, {
137 assert_debug_snapshot!(snippet.create_content(&action, &BTreeMap::new()));
138 });
139 }
140
141 #[test]
142 fn can_get_snippet_content_with_template_action() {
143 let snippet = tests_cfg::get_snippet();
144
145 let action = injector::InjectContentAction {
146 kind: InjectAction::Copy,
147 snippet_id: "id".to_string(),
148 inject_from: SnippetKind::Any,
149 strip_prefix: None,
150 add_prefix: None,
151 template: Template::new("```sh\n{snippet}\n```"),
152 };
153
154 with_settings!({filters => tests_cfg::redact::all()}, {
155 assert_debug_snapshot!(snippet.create_content(&action, &BTreeMap::new()));
156 });
157 }
158
159 #[test]
160 fn can_get_snippet_content_with_custom_template_action() {
161 let snippet = tests_cfg::get_snippet();
162
163 let action = injector::InjectContentAction {
164 kind: InjectAction::Copy,
165 snippet_id: "id".to_string(),
166 inject_from: SnippetKind::Any,
167 strip_prefix: None,
168 add_prefix: None,
169 template: Template::new("CUSTOM_ID_1"),
170 };
171
172 with_settings!({filters => tests_cfg::redact::all()}, {
173 assert_debug_snapshot!(snippet.create_content(&action, &tests_cfg::get_custom_templates()));
174 });
175 }
176
177 #[test]
178 fn can_get_snippet_content_with_strip_prefix_action() {
179 let snippet = tests_cfg::get_snippet();
180
181 let action = injector::InjectContentAction {
182 kind: InjectAction::Copy,
183 snippet_id: "id".to_string(),
184 inject_from: SnippetKind::Any,
185 strip_prefix: Some("$ ".to_string()),
186 add_prefix: None,
187 template: Template::default(),
188 };
189
190 with_settings!({filters => tests_cfg::redact::all()}, {
191 assert_debug_snapshot!(snippet.create_content(&action, &BTreeMap::new()));
192 });
193 }
194
195 #[test]
196 fn can_get_snippet_content_with_add_prefix_action() {
197 let snippet = tests_cfg::get_snippet();
198
199 let action = injector::InjectContentAction {
200 kind: InjectAction::Copy,
201 snippet_id: "id".to_string(),
202 inject_from: SnippetKind::Any,
203 strip_prefix: None,
204 add_prefix: Some("$".to_string()),
205 template: Template::default(),
206 };
207
208 with_settings!({filters => tests_cfg::redact::all()}, {
209 assert_debug_snapshot!(snippet.create_content(&action, &BTreeMap::new()));
210 });
211 }
212
213 #[test]
214 fn can_get_snippet_content_with_combination_action() {
215 let snippet = tests_cfg::get_snippet();
216
217 let action = injector::InjectContentAction {
218 kind: InjectAction::Copy,
219 snippet_id: "id".to_string(),
220 inject_from: SnippetKind::Any,
221 strip_prefix: Some("$ ".to_string()),
222 add_prefix: Some("- ".to_string()),
223 template: Template::new("```sh\n{snippet}\n```"),
224 };
225
226 with_settings!({filters => tests_cfg::redact::all()}, {
227 assert_debug_snapshot!(snippet.create_content(&action, &BTreeMap::new()));
228 });
229 }
230
231 #[cfg(all(feature = "exec", not(target_os = "windows")))]
232 #[test]
233 fn can_get_snippet_with_exec_action_with_template() {
234 let mut snippet = tests_cfg::get_snippet();
235 snippet.content = r"echo calc result: $((1+1))".to_string();
236
237 let action = injector::InjectContentAction {
238 kind: InjectAction::Exec,
239 snippet_id: "id".to_string(),
240 inject_from: SnippetKind::Any,
241 strip_prefix: None,
242 add_prefix: None,
243 template: Template::new("```sh\n{snippet}\n```"),
244 };
245
246 assert_debug_snapshot!(
247 "unix_can_get_snippet_with_exec_action_with_template",
248 snippet.create_content(&action, &BTreeMap::new())
249 );
250 }
251
252 #[cfg(all(feature = "exec", target_os = "windows"))]
253 #[test]
254 fn can_get_snippet_with_exec_action_with_template() {
255 let mut snippet = tests_cfg::get_snippet();
256 snippet.content = r"echo calc result: $((1+1))".to_string();
257
258 let action = injector::InjectContentAction {
259 kind: InjectAction::Exec,
260 snippet_id: "id".to_string(),
261 inject_from: SnippetKind::Any,
262 strip_prefix: None,
263 add_prefix: None,
264 template: Template::new("```sh\n{snippet}\n```"),
265 };
266
267 assert_debug_snapshot!(
268 "windows_can_get_snippet_with_exec_action_with_template",
269 snippet.create_content(&action, &BTreeMap::new())
270 );
271 }
272}