Skip to main content

myfav_output/
lib.rs

1use myfav_core::Favorite;
2use std::collections::BTreeMap;
3
4pub struct MarkdownFormatter;
5
6const START_MARKER: &str = "<!-- START_FAVORITES -->";
7const END_MARKER: &str = "<!-- END_FAVORITES -->";
8
9impl MarkdownFormatter {
10    pub fn format(favorites: &[Favorite], existing_content: Option<String>) -> String {
11        let tree = Self::build_category_tree(favorites);
12
13        let mut favorites_block = String::new();
14        favorites_block.push_str("## Table of Contents\n\n");
15        favorites_block.push_str(&Self::render_toc(&tree, 0));
16        favorites_block.push_str("\n---\n\n");
17        favorites_block.push_str(&Self::render_tree(&tree, 0));
18
19        let new_section = format!("{}\n\n{}\n{}", START_MARKER, favorites_block, END_MARKER);
20
21        match existing_content {
22            Some(content) => {
23                if content.contains(START_MARKER) && content.contains(END_MARKER) {
24                    let start_idx = content.find(START_MARKER).unwrap();
25                    let end_idx = content.find(END_MARKER).unwrap() + END_MARKER.len();
26                    let mut updated = content.clone();
27                    updated.replace_range(start_idx..end_idx, &new_section);
28                    updated
29                } else {
30                    format!("{}\n\n{}", content, new_section)
31                }
32            }
33            None => format!("# My Favorites\n\n{}\n", new_section),
34        }
35    }
36
37    fn render_toc(tree: &BTreeMap<String, CategoryNode>, level: usize) -> String {
38        let mut output = String::new();
39        let indent = "  ".repeat(level);
40        for (name, node) in tree {
41            let anchor = name
42                .to_lowercase()
43                .replace(' ', "-")
44                .replace(|c: char| !c.is_alphanumeric() && c != '-', "");
45            output.push_str(&format!("{}- [{}](#{})\n", indent, name, anchor));
46            output.push_str(&Self::render_toc(&node.subcategories, level + 1));
47        }
48        output
49    }
50
51    fn build_category_tree(favorites: &[Favorite]) -> BTreeMap<String, CategoryNode> {
52        let mut root: BTreeMap<String, CategoryNode> = BTreeMap::new();
53        for fav in favorites {
54            let mut current_level = &mut root;
55            for (i, cat) in fav.categories.iter().enumerate() {
56                let node = current_level
57                    .entry(cat.clone())
58                    .or_insert_with(CategoryNode::default);
59                if i == fav.categories.len() - 1 {
60                    node.favorites.push(fav.clone());
61                }
62                current_level = &mut node.subcategories;
63            }
64        }
65        root
66    }
67
68    fn render_tree(tree: &BTreeMap<String, CategoryNode>, level: usize) -> String {
69        let mut output = String::new();
70        let header_prefix = "#".repeat(level + 2);
71
72        for (name, node) in tree {
73            output.push_str(&format!("{} {}\n\n", header_prefix, name));
74
75            for fav in &node.favorites {
76                let status = if fav.deprecated { " [DEPRECATED]" } else { "" };
77                let icon_str = fav
78                    .icon
79                    .as_deref()
80                    .map(|i| format!("{} ", i))
81                    .unwrap_or_default();
82                output.push_str(&format!(
83                    "- {}[{}]({}){} - {}\n",
84                    icon_str, fav.title, fav.url, status, fav.description
85                ));
86                if let Some(alt) = &fav.alternative {
87                    output.push_str(&format!("  *Alternative: {}*\n", alt));
88                }
89                if !fav.tags.is_empty() {
90                    let badges = fav
91                        .tags
92                        .iter()
93                        .map(|t: &String| {
94                            let (tag_name, color) = if t.contains(':') {
95                                let mut parts = t.splitn(2, ':');
96                                (parts.next().unwrap_or(t), parts.next().unwrap_or("brown"))
97                            } else {
98                                (t.as_str(), "brown")
99                            };
100                            let tag_clean = tag_name.replace('-', "--").replace(' ', "_");
101                            format!(
102                                "![{}](https://img.shields.io/badge/{}-{}?style=for-the-badge)",
103                                tag_name, tag_clean, color
104                            )
105                        })
106                        .collect::<Vec<String>>()
107                        .join(" ");
108                    output.push_str(&format!("  {}\n", badges));
109                }
110            }
111            if !node.favorites.is_empty() {
112                output.push('\n');
113            }
114
115            output.push_str(&Self::render_tree(&node.subcategories, level + 1));
116        }
117        output
118    }
119}
120
121#[derive(Default)]
122struct CategoryNode {
123    favorites: Vec<Favorite>,
124    subcategories: BTreeMap<String, CategoryNode>,
125}
126
127pub struct JsonDistributionFormatter;
128
129impl JsonDistributionFormatter {
130    pub fn format(favorites: &[Favorite]) -> anyhow::Result<String> {
131        Ok(serde_json::to_string_pretty(favorites)?)
132    }
133}