summaryrefslogtreecommitdiffstats
path: root/front_end/src/cat_page.rs
blob: cef868aa627168b75819a60632c571a239fe10e9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
use crate::templates;
use crate::Page;
use categories::Category;
use categories::CATEGORIES;
use failure::Error;
use kitchen_sink::KitchenSink;
use rayon::prelude::*;
use render_readme::Renderer;
use rich_crate::RichCrateVersion;
use std::collections::HashSet;

/// Data for category page template
pub struct CatPage<'a> {
    pub cat: &'a Category,
    pub keywords: Vec<String>,
    pub crates: Vec<(RichCrateVersion, u32)>,
    pub related: Vec<String>,
    pub markup: &'a Renderer,
    pub count: usize,
}

impl<'a> CatPage<'a> {
    pub async fn new(cat: &'a Category, crates: &'a KitchenSink, markup: &'a Renderer) -> Result<CatPage<'a>, Error> {
        Ok(Self {
            count: crates.category_crate_count(&cat.slug)? as usize,
            keywords: crates.top_keywords_in_category(cat)?,
            related: crates.related_categories(&cat.slug)?,
            crates: crates
                .top_crates_in_category(&cat.slug).await?
                .par_iter()
                .with_max_len(1)
                .filter_map(|o| {
                    let c = match crates.rich_crate_version(&o) {
                        Ok(c) => c,
                        Err(e) => {
                            eprintln!("Skipping {:?} because {}", o, e);
                            return None;
                        },
                    };
                    if c.is_yanked() {
                        return None;
                    }
                    let d = match crates.downloads_per_month_or_equivalent(&o) {
                        Ok(d) => d.unwrap_or(0) as u32,
                        Err(e) => {
                            eprintln!("Skipping {:?} because dl {}", o, e);
                            return None;
                        },
                    };
                    Some((c, d))
                })
                .collect::<Vec<_>>(),
            cat,
            markup,
        })
    }

    pub fn has_subcategories_and_siblings(&self) -> bool {
        !self.cat.sub.is_empty() || !self.cat.siblings.is_empty()
    }

    pub fn subcategories_and_siblings(&self) -> impl Iterator<Item = &Category> {
        self.cat.sub.values().chain(self.cat.siblings.iter().flat_map(|slug| CATEGORIES.from_slug(slug)))
    }

    /// Used to render descriptions
    pub fn render_markdown_str(&self, s: &str) -> templates::Html<String> {
        templates::Html(self.markup.markdown_str(s, false, None))
    }

    /// For color of the version
    ///
    /// It tries to guess which versions seem "unstable".
    ///
    /// TODO: Merge with the better version history analysis from the individual crate page.
    pub fn version_class(&self, c: &RichCrateVersion) -> &str {
        let v = c.version_semver().unwrap();
        match (v.major, v.minor, v.patch, v.is_prerelease()) {
            (1..=15, _, _, false) => "stable",
            (0, m, p, false) if m >= 2 && p >= 3 => "stable",
            (m, ..) if m >= 1 => "okay",
            (0, 1, p, _) if p >= 10 => "okay",
            (0, 3..=10, p, _) if p > 0 => "okay",
            _ => "unstable",
        }
    }

    pub fn description(&self) -> &str {
        self.cat.description.trim_end_matches('.')
    }

    /// "See also" feature
    pub fn related_categories(&self) -> Vec<Vec<&Category>> {
        let mut seen = HashSet::with_capacity(self.related.len());
        self.related.iter().map(|slug| CATEGORIES.from_slug(slug).filter(|c| seen.insert(&c.slug)).collect()).filter(|v: &Vec<_>| !v.is_empty()).collect()
    }

    /// Nicely rounded number of downloads
    ///
    /// To show that these numbers are just approximate.
    pub fn downloads(&self, num: u32) -> (String, &str) {
        match num {
            a @ 0..=99 => (format!("{}", a), ""),
            a @ 0..=500 => (format!("{}", a / 10 * 10), ""),
            a @ 0..=999 => (format!("{}", a / 50 * 50), ""),
            a @ 0..=9999 => (format!("{}.{}", a / 1000, a % 1000 / 100), "K"),
            a @ 0..=999_999 => (format!("{}", a / 1000), "K"),
            a => (format!("{}.{}", a / 1_000_000, a % 1_000_000 / 100_000), "M"),
        }
    }

    /// Metadata about the category
    pub fn page(&self) -> Page {
        Page {
            title: format!("{} — list of Rust libraries/crates", self.cat.standalone_name()),
            description: Some(self.cat.description.clone()),
            item_name: Some(self.cat.name.clone()),
            item_description: Some(self.cat.short_description.clone()),
            keywords: Some(self.keywords.join(", ")),
            noindex: false,
            search_meta: true,
            ..Default::default()
        }
    }

    /// For breadcrumbs
    pub fn parent_categories(&self) -> Vec<&Category> {
        let mut c: Vec<_> = CATEGORIES.from_slug(&self.cat.slug).collect();
        c.pop();
        c
    }
}