snipdoc/parser/
mod.rs

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    /// Returns the snippet content, filtered based on `strip_prefix` if
62    /// specified.
63    #[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                // validate if i can remove this code
90                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}