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 "",
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}