Skip to main content

cooklang_import/pipelines/
mod.rs

1pub mod image;
2pub mod text;
3pub mod url;
4
5/// Components extracted from a recipe source.
6/// All fields can be empty strings if the data is not available.
7#[derive(Debug, Clone, Default)]
8pub struct RecipeComponents {
9    /// Recipe text containing ingredients and instructions
10    pub text: String,
11    /// YAML-formatted metadata (without --- delimiters)
12    pub metadata: String,
13    /// Recipe name/title (always single-line)
14    pub name: String,
15}
16
17/// Collapse any whitespace (newlines, tabs, multiple spaces) into a single space.
18pub fn sanitize_name(name: &str) -> String {
19    name.split_whitespace().collect::<Vec<_>>().join(" ")
20}
21
22/// Build a YAML metadata string from a Recipe's fields.
23/// Handles nested values (e.g. nutrition) by parsing pre-formatted YAML blocks.
24pub fn metadata_to_yaml(entries: &[(String, String)]) -> String {
25    use serde_yaml::Value;
26
27    let mut mapping = serde_yaml::Mapping::new();
28
29    for (key, value) in entries {
30        if value.starts_with('\n') {
31            // Pre-formatted nested YAML (e.g. nutrition) — parse as nested mapping
32            let yaml_str = format!("{}:{}", key, value);
33            if let Ok(parsed) = serde_yaml::from_str::<serde_yaml::Mapping>(&yaml_str) {
34                for (k, v) in parsed {
35                    mapping.insert(k, v);
36                }
37                continue;
38            }
39        }
40        mapping.insert(Value::String(key.clone()), Value::String(value.clone()));
41    }
42
43    if mapping.is_empty() {
44        String::new()
45    } else {
46        serde_yaml::to_string(&mapping).unwrap_or_default()
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_metadata_to_yaml_simple() {
56        let entries = vec![
57            ("source".to_string(), "http://example.com".to_string()),
58            ("servings".to_string(), "4".to_string()),
59        ];
60        let yaml = metadata_to_yaml(&entries);
61        assert!(yaml.contains("source: http://example.com"));
62        assert!(yaml.contains("servings: '4'"));
63    }
64
65    #[test]
66    fn test_metadata_to_yaml_with_colon() {
67        let entries = vec![("description".to_string(), "test : sub".to_string())];
68        let yaml = metadata_to_yaml(&entries);
69        assert!(yaml.contains("description: 'test : sub'"));
70    }
71
72    #[test]
73    fn test_metadata_to_yaml_nested() {
74        let entries = vec![(
75            "nutrition".to_string(),
76            "\n  calories: 330 calories\n  fat: 18 grams fat".to_string(),
77        )];
78        let yaml = metadata_to_yaml(&entries);
79        assert!(yaml.contains("nutrition:"));
80        assert!(yaml.contains("calories: 330 calories"));
81        assert!(yaml.contains("fat: 18 grams fat"));
82        // Should NOT be quoted as a single string
83        assert!(!yaml.contains("\""));
84    }
85
86    #[test]
87    fn test_sanitize_name() {
88        assert_eq!(sanitize_name("hello  world\n test"), "hello world test");
89    }
90}