diff options
author | Kornel <kornel@geekhood.net> | 2020-03-10 00:37:22 +0000 |
---|---|---|
committer | Kornel <kornel@geekhood.net> | 2020-03-10 00:43:54 +0000 |
commit | 54d2fbd88a38e4c95e6293e9e2067640afa4404a (patch) | |
tree | 4ed6532e3b30a4672988896af80a3359d29de89a | |
parent | 34bc2b12e637456c8c5140dfcb4a49d11ac544e5 (diff) |
Author page
-rw-r--r-- | crate_db/Cargo.toml | 2 | ||||
-rw-r--r-- | crate_db/src/lib_crate_db.rs | 16 | ||||
-rw-r--r-- | crates_io_client/Cargo.toml | 3 | ||||
-rw-r--r-- | crates_io_client/src/crate_owners.rs | 7 | ||||
-rw-r--r-- | datadump/src/main.rs | 5 | ||||
-rw-r--r-- | front_end/Cargo.toml | 2 | ||||
-rw-r--r-- | front_end/src/author_page.rs | 259 | ||||
-rw-r--r-- | front_end/src/cat_page.rs | 14 | ||||
-rw-r--r-- | front_end/src/crate_page.rs | 24 | ||||
-rw-r--r-- | front_end/src/front_end.rs | 31 | ||||
-rw-r--r-- | front_end/src/install_page.rs | 2 | ||||
-rw-r--r-- | front_end/src/urler.rs | 11 | ||||
-rw-r--r-- | front_end/templates/author.rs.html | 93 | ||||
-rw-r--r-- | front_end/templates/author_list.rs.html | 43 | ||||
-rw-r--r-- | front_end/templates/base.rs.html | 4 | ||||
-rw-r--r-- | front_end/templates/cat_page.rs.html | 3 | ||||
-rw-r--r-- | github_info/src/lib_github.rs | 17 | ||||
-rw-r--r-- | github_info/src/model.rs | 16 | ||||
-rw-r--r-- | github_v3/src/model.rs | 1 | ||||
-rw-r--r-- | kitchen_sink/Cargo.toml | 2 | ||||
-rw-r--r-- | kitchen_sink/src/lib_kitchen_sink.rs | 8 | ||||
-rw-r--r-- | server/Cargo.toml | 2 | ||||
m--------- | style | 0 | ||||
-rw-r--r-- | user_db/src/lib_user_db.rs | 2 |
24 files changed, 365 insertions, 202 deletions
diff --git a/crate_db/Cargo.toml b/crate_db/Cargo.toml index 558301a..d8ee715 100644 --- a/crate_db/Cargo.toml +++ b/crate_db/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2018" name = "crate_db" -version = "0.4.6" +version = "0.4.7" authors = ["Kornel <kornel@geekhood.net>"] description = "Internal index of crates used by crates.rs" diff --git a/crate_db/src/lib_crate_db.rs b/crate_db/src/lib_crate_db.rs index 9f0dd01..fde08f9 100644 --- a/crate_db/src/lib_crate_db.rs +++ b/crate_db/src/lib_crate_db.rs @@ -645,7 +645,7 @@ impl CrateDb { pub async fn crates_of_author(&self, github_id: u32) -> FResult<Vec<CrateOwnerRow>> { self.with_read("crates_of_author", |conn| { - let mut query = conn.prepare_cached(r#"SELECT c.origin, ac.invited_by_github_id, ac.invited_at, max(cv.created) + let mut query = conn.prepare_cached(r#"SELECT c.origin, ac.invited_by_github_id, ac.invited_at, max(cv.created), c.ranking FROM author_crates ac JOIN crate_versions cv USING(crate_id) JOIN crates c ON c.id = ac.crate_id @@ -656,13 +656,18 @@ impl CrateDb { let q = query.query_map(&[&github_id], |row| { let origin = Origin::from_str(row.get_raw(0).as_str().unwrap()); let invited_by_github_id: Option<u32> = row.get_unwrap(1); - let invited_at = row.get_raw(2).as_str().ok().and_then(|d| DateTime::parse_from_rfc3339(d).ok()); + let invited_at = row.get_raw(2).as_str().ok().map(|d| match Utc.datetime_from_str(d, "%Y-%m-%d %H:%M:%S") { + Ok(d) => d, + Err(e) => panic!("Can't parse {}, because {}", d, e), + }); let latest_timestamp: u32 = row.get_unwrap(3); + let crate_ranking: f64 = row.get_unwrap(4); Ok(CrateOwnerRow { origin, + crate_ranking: crate_ranking as f32, invited_by_github_id, invited_at, - latest_release: DateTime::from_utc(NaiveDateTime::from_timestamp(latest_timestamp as _, 0), FixedOffset::east(0)), + latest_release: DateTime::from_utc(NaiveDateTime::from_timestamp(latest_timestamp as _, 0), Utc), }) })?; Ok(q.filter_map(|x| x.ok()).collect()) @@ -1148,9 +1153,10 @@ impl KeywordInsert { #[derive(Debug)] pub struct CrateOwnerRow { pub origin: Origin, + pub crate_ranking: f32, pub invited_by_github_id: Option<u32>, - pub invited_at: Option<DateTime<FixedOffset>>, - pub latest_release: DateTime<FixedOffset>, + pub invited_at: Option<DateTime<Utc>>, + pub latest_release: DateTime<Utc>, } #[inline] diff --git a/crates_io_client/Cargo.toml b/crates_io_client/Cargo.toml index a1998cb..f1751b8 100644 --- a/crates_io_client/Cargo.toml +++ b/crates_io_client/Cargo.toml @@ -1,5 +1,5 @@ [package] -version = "0.8.2" +version = "0.8.3" edition = "2018" name = "crates_io_client" authors = ["Kornel <kornel@geekhood.net>"] @@ -19,3 +19,4 @@ parking_lot = "0.10.0" urlencoding = "1.0.0" tokio = { version = "0.2", features = ["macros", "sync"] } futures = "0.3.4" +chrono = "0.4.10" diff --git a/crates_io_client/src/crate_owners.rs b/crates_io_client/src/crate_owners.rs index dc4f795..c3861e7 100644 --- a/crates_io_client/src/crate_owners.rs +++ b/crates_io_client/src/crate_owners.rs @@ -1,3 +1,6 @@ +use chrono::DateTime; +use chrono::offset::TimeZone; +use chrono::Utc; use serde_derive::*; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -55,6 +58,10 @@ impl CrateOwner { } } + pub fn invited_at(&self) -> Option<DateTime<Utc>> { + self.invited_at.as_ref().and_then(|d| Utc.datetime_from_str(d, "%Y-%m-%d %H:%M:%S").ok()) + } + /// Be careful about case-insensitivity pub fn github_login(&self) -> Option<&str> { match self.kind { diff --git a/datadump/src/main.rs b/datadump/src/main.rs index c1da0e3..b60df79 100644 --- a/datadump/src/main.rs +++ b/datadump/src/main.rs @@ -123,6 +123,7 @@ async fn index_owners(crates: &CratesMap, owners: CrateOwners, teams: &Teams, us let owners: Vec<_> = owners .into_iter() .filter_map(|o| { + let invited_at = o.created_at.splitn(2, '.').next().unwrap().to_string(); // trim millis part let invited_by_github_id = o.created_by_id.and_then(|id| users.get(&id).map(|u| u.github_id as u32).or_else(|| teams.get(&id).map(|t| t.github_id))); Some(match o.owner_kind { @@ -134,7 +135,7 @@ async fn index_owners(crates: &CratesMap, owners: CrateOwners, teams: &Teams, us CrateOwner { id: o.owner_id as _, login: u.login.to_owned(), - invited_at: Some(o.created_at), + invited_at: Some(invited_at), invited_by_github_id, github_id: u.github_id.try_into().ok(), name: Some(u.name.to_owned()), @@ -148,7 +149,7 @@ async fn index_owners(crates: &CratesMap, owners: CrateOwners, teams: &Teams, us CrateOwner { id: o.owner_id as _, login: u.login.to_owned(), - invited_at: Some(o.created_at), + invited_at: Some(invited_at), github_id: Some(u.github_id), invited_by_github_id, name: Some(u.name.to_owned()), diff --git a/front_end/Cargo.toml b/front_end/Cargo.toml index 8889b9b..b3a8152 100644 --- a/front_end/Cargo.toml +++ b/front_end/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2018" name = "front_end" -version = "0.4.0" +version = "0.4.1" authors = ["Kornel <kornel@geekhood.net>"] autobins = true diff --git a/front_end/src/author_page.rs b/front_end/src/author_page.rs index 5bb2ae2..52fd568 100644 --- a/front_end/src/author_page.rs +++ b/front_end/src/author_page.rs @@ -1,62 +1,174 @@ +use chrono::prelude::*; use crate::Page; use crate::templates; +use crate::url_domain; use futures::stream::StreamExt; use kitchen_sink::CrateOwnerRow; use kitchen_sink::CResult; use kitchen_sink::KitchenSink; +use kitchen_sink::Org; +use kitchen_sink::OwnerKind; use kitchen_sink::RichAuthor; use kitchen_sink::RichCrateVersion; -use kitchen_sink::UserOrg; +use kitchen_sink::User; use kitchen_sink::UserType; use render_readme::Renderer; +use std::borrow::Cow; +use std::cmp::Ordering; +use std::collections::HashMap; use std::sync::Arc; -// pub struct User { -// pub id: u32, -// pub login: String, -// pub name: Option<String>, -// pub avatar_url: Option<String>, // "https://avatars0.githubusercontent.com/u/1111?v=4", -// pub gravatar_id: Option<String>, // "", -// pub html_url: String, // "https://github.com/zzzz", -// pub blog: Option<String>, // "https://example.com -// #[serde(rename = "type")] -// pub user_type: UserType, -// } +pub struct OtherOwner { + github_id: u32, + login: String, + invited_by_github_id: Option<u32>, + invited_at: DateTime<Utc>, + kind: OwnerKind, +} /// Data sources used in `author.rs.html` pub struct AuthorPage<'a> { - pub aut: &'a RichAuthor, - pub kitchen_sink: &'a KitchenSink, - pub markup: &'a Renderer, - pub crates: Vec<(Arc<RichCrateVersion>, CrateOwnerRow)>, - pub orgs: Vec<UserOrg>, + pub(crate) aut: &'a RichAuthor, + pub(crate) markup: &'a Renderer, + pub(crate) founder_crates: Vec<(Arc<RichCrateVersion>, u32, CrateOwnerRow, Vec<OtherOwner>)>, + pub(crate) member_crates: Vec<(Arc<RichCrateVersion>, u32, CrateOwnerRow, Vec<OtherOwner>)>, + pub(crate) orgs: Vec<Org>, + pub(crate) joined: DateTime<Utc>, + pub(crate) founder_total: usize, + pub(crate) member_total: usize, + pub(crate) keywords: Vec<String>, + pub(crate) collab: Vec<User>, } impl<'a> AuthorPage<'a> { pub async fn new(aut: &'a RichAuthor, kitchen_sink: &'a KitchenSink, markup: &'a Renderer) -> CResult<AuthorPage<'a>> { dbg!(&aut); let orgs = kitchen_sink.user_github_orgs(&aut.github.login).await?.unwrap_or_default(); - let mut rows = kitchen_sink.crates_of_author(aut).await?; - rows.sort_by(|a,b| b.latest_release.cmp(&a.latest_release)); - rows.truncate(200); - dbg!(&rows); + let orgs = futures::stream::iter(orgs).filter_map(|org| async move { + kitchen_sink.github_org(&org.login).await + .map_err(|e| eprintln!("org: {} {}", &org.login, e)) + .ok().and_then(|x| x) + }) + .collect().await; + let rows = kitchen_sink.crates_of_author(aut).await?; + let joined = rows.iter().filter_map(|row| row.invited_at).min().expect("no crates?"); + + let (mut founder, mut member): (Vec<_>, Vec<_>) = rows.into_iter().partition(|c| c.invited_by_github_id.is_none()); + let founder_total = founder.len(); + let member_total = member.len(); + founder.sort_by(|a,b| b.latest_release.cmp(&a.latest_release)); + founder.truncate(200); + + member.sort_by(|a,b| b.crate_ranking.partial_cmp(&a.crate_ranking).unwrap_or(Ordering::Equal)); + member.truncate(200); + + let founder_crates = Self::look_up(kitchen_sink, founder).await; + let member_crates = Self::look_up(kitchen_sink, member).await; + + // Most common keywords + let mut keywords = HashMap::new(); + let now = Utc::now(); + // Most collaborated with + let mut collab = HashMap::new(); + for (c, _, row, all_owners) in founder_crates.iter().chain(member_crates.iter()) { + for k in c.keywords() { + *keywords.entry(k).or_insert(0.) += row.crate_ranking + 0.5; + } + + if let Some(own) = all_owners.iter().find(|o| o.github_id == aut.github.id) { + let oldest = all_owners.iter().map(|o| o.invited_at).min().unwrap(); + // max discounts young crates + let max_days = now.signed_duration_since(oldest).num_days().max(30*6) as f32; + let own_days = now.signed_duration_since(own.invited_at).num_days(); + for o in all_owners { + if o.github_id == aut.github.id || o.kind != OwnerKind::User { + continue; + } + // How long co-owned together, relative to crate's age + let overlap = now.signed_duration_since(o.invited_at).num_days().min(own_days) as f32 / max_days; + let relationship = if own.invited_by_github_id == Some(o.github_id) {4.} + else if o.invited_by_github_id == Some(own.github_id) {2.} else {1.}; + collab.entry(o.github_id).or_insert((0., &o.login)).0 += (row.crate_ranking + 0.5) * overlap * relationship; + } + } + } + let mut keywords: Vec<_> = keywords.into_iter().collect(); + keywords.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)); + let keywords: Vec<_> = keywords.into_iter().take(7).map(|(k, _)| k.to_owned()).collect(); - let crates = futures::stream::iter(rows.into_iter()) - .filter_map(|row| async move { - let c = kitchen_sink.rich_crate_version_async(&row.origin).await.map_err(|e| eprintln!("{}", e)).ok()?; - Some((c, row)) - }) - .collect().await; + let mut collab: Vec<_> = collab.into_iter().map(|(_, v)| v).collect(); + collab.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal)); + let collab: Vec<_> = futures::stream::iter(collab.into_iter().take(100)).filter_map(|(_, login)| async move { + kitchen_sink.user_by_github_login(login).await.map_err(|e| eprintln!("{}: {}", login, e)).ok().and_then(|x| x) + }).collect().await; Ok(Self { - crates, + founder_crates, member_crates, + founder_total, member_total, aut, - kitchen_sink, markup, orgs, + joined, + keywords, + collab, }) } + async fn look_up(kitchen_sink: &KitchenSink, rows: Vec<CrateOwnerRow>) -> Vec<(Arc<RichCrateVersion>, u32, CrateOwnerRow, Vec<OtherOwner>)> { + futures::stream::iter(rows.into_iter()) + .filter_map(|row| async move { + let c = kitchen_sink.rich_crate_version_async(&row.origin).await.map_err(|e| eprintln!("{}", e)).ok()?; + let dl = kitchen_sink.downloads_per_month(&row.origin).await.map_err(|e| eprintln!("{}", e)).ok()?.unwrap_or(0) as u32; + let owners = kitchen_sink.crate_owners(&row.origin).await.map_err(|e| eprintln!("o: {}", e)).ok()?.into_iter().filter_map(|o| { + Some(OtherOwner { + invited_at: o.invited_at()?, + github_id: o.github_id?, + invited_by_github_id: o.invited_by_github_id, + login: o.login, + kind: o.kind, + }) + }).collect(); + Some((c, dl, row, owners)) + }) + .collect().await + } + + /// `(url, label)` + pub fn homepage_link(&self) -> Option<(&str, Cow<'_, str>)> { + if let Some(url) = self.aut.github.blog.as_deref() { + if url.starts_with("https://") || url.starts_with("http://") { + let label = url_domain(url) + .map(|host| { + format!("Home ({})", host).into() + }) + .unwrap_or_else(|| "Homepage".into()); + return Some((url, label)); + } + } + None + } + + pub fn joined_github(&self) -> Option<DateTime<FixedOffset>> { + if let Some(d) = &self.aut.github.created_at { + DateTime::parse_from_rfc3339(d).ok() + } else { + None + } + } + + pub fn org_name(org: &Org) -> &str { + if let Some(name) = &org.name { + if name.eq_ignore_ascii_case(&org.login) { + return &name; + } + } + &org.login + } + + pub fn format_month(date: &DateTime<Utc>) -> String { + date.format("%b %Y").to_string() + } + pub fn is_org(&self) -> bool { self.aut.github.user_type == UserType::Org } @@ -71,7 +183,9 @@ impl<'a> AuthorPage<'a> { pub fn page(&self) -> Page { Page { - title: format!("Rust crates by @{}", self.login()), + title: format!("@{}'s Rust crates", self.login()), + critical_css_data: Some(include_str!("../../style/public/author.css")), + critical_css_dev_url: Some("/author.css"), ..Default::default() } } @@ -79,89 +193,4 @@ impl<'a> AuthorPage<'a> { pub fn render_markdown_str(&self, s: &str) -> templates::Html<String> { templates::Html(self.markup.markdown_str(s, true, None)) } - - // fn block<O>(&self, f: impl Future<Output = O>) -> O { - // self.handle.enter(|| futures::executor::block_on(f)) - // } - - // pub fn format_number(&self, num: impl Display) -> String { - // Numeric::english().format_int(num) - // } - - // pub fn format_knumber(&self, num: usize) -> (String, &'static str) { - // let (num, unit) = match num { - // 0..=899 => (num, ""), - // 0..=8000 => return (format!("{}", ((num + 250) / 500) as f64 * 0.5), "K"), // 3.5K - // 0..=899_999 => ((num + 500) / 1000, "K"), - // 0..=9_999_999 => return (format!("{}", ((num + 250_000) / 500_000) as f64 * 0.5), "M"), // 3.5M - // _ => ((num + 500_000) / 1_000_000, "M"), // 10M - // }; - // (Numeric::english().format_int(num), unit) - // } - - // pub fn format_kbytes(&self, bytes: usize) -> String { - // let (num, unit) = match bytes { - // 0..=100_000 => ((bytes + 999) / 1000, "KB"), - // 0..=800_000 => ((bytes + 3999) / 5000 * 5, "KB"), - // 0..=9_999_999 => return format!("{}MB", ((bytes + 250_000) / 500_000) as f64 * 0.5), - // _ => ((bytes + 500_000) / 1_000_000, "MB"), - // }; - // format!("{}{}", Numeric::english().format_int(num), unit) - // } - - // fn format_number_frac(num: f64) -> String { - // if num > 0.05 && num < 10. && num.fract() > 0.09 && num.fract() < 0.9 { - // if num < 3. { - // format!("{:.1}", num) - // } else { - // format!("{}", (num * 2.).round() / 2.) - // } - // } else { - // Numeric::english().format_int(if num > 500. { - // (num / 10.).round() * 10. - // } else if num > 100. { - // (num / 5.).round() * 5. - // } else { - // num.round() - // }) - // } - // } - - // pub fn format_kbytes_range(&self, a: usize, b: usize) -> String { - // let min_bytes = a.min(b); - // let max_bytes = a.max(b); - - // // if the range is small, just display the upper number - // if min_bytes * 4 > max_bytes * 3 || max_bytes < 250_000 { - // return self.format_kbytes(max_bytes); - // } - - // let (denom, unit) = match max_bytes { - // 0..=800_000 => (1000., "KB"), - // _ => (1_000_000., "MB"), - // }; - // let mut low_val = min_bytes as f64 / denom; - // let high_val = max_bytes as f64 / denom; - // if low_val > 1. && high_val > 10. { - // low_val = low_val.round(); // spread is so high that precision of low end isn't relevant - // } - // format!("{}–{}{}", Self::format_number_frac(low_val), Self::format_number_frac(high_val), unit) - // } - - // /// Display number 0..1 as percent - // pub fn format_fraction(&self, num: f64) -> String { - // if num < 1.9 { - // format!("{:0.1}%", num) - // } else { - // format!("{}%", Numeric::english().format_int(num.round() as usize)) - // } - // } - - // pub fn format(date: &DateTime<FixedOffset>) -> String { - // date.format("%b %e, %Y").to_string() - // } - - // pub fn format_month(date: &DateTime<FixedOffset>) -> String { - // date.format("%b %Y").to_string() - // } } diff --git a/front_end/src/cat_page.rs b/front_end/src/cat_page.rs index 22698cc..eeaed03 100644 --- a/front_end/src/cat_page.rs +++ b/front_end/src/cat_page.rs @@ -99,20 +99,6 @@ impl<'a> CatPage<'a> { self.related.iter().map(|slug| CATEGORIES.from_slug(slug).0.into_iter().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, &'static 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 { diff --git a/front_end/src/crate_page.rs b/front_end/src/crate_page.rs index 14784c0..57286a8 100644 --- a/front_end/src/crate_page.rs +++ b/front_end/src/crate_page.rs @@ -33,7 +33,7 @@ use std::sync::Arc; use tokio::runtime::Handle; use udedokei::LanguageExt; use udedokei::{Language, Lines, Stats}; -use url::Url; +use crate::url_domain; pub struct CrateLicense { pub origin: Origin, @@ -574,26 +574,12 @@ impl<'a> CratePage<'a> { self.api_reference_url.as_deref() } - fn url_domain(url: &str) -> Option<Cow<'static, str>> { - Url::parse(url).ok().and_then(|url| { - url.host_str().and_then(|host| { - if host.ends_with(".github.io") { - Some("github.io".into()) - } else if host.ends_with(".githubusercontent.com") { - None - } else { - Some(host.trim_start_matches("www.").to_string().into()) - } - }) - }) - } - /// `(url, label)` pub fn homepage_link(&self) -> Option<(&str, Cow<'_, str>)> { self.ver.homepage().map(|url| { - let label = Self::url_domain(url) + let label = url_domain(url) .map(|host| { - let docs_on_same_host = self.ver.documentation().and_then(Self::url_domain).map_or(false, |doc_host| doc_host == host); + let docs_on_same_host = self.ver.documentation().and_then(url_domain).map_or(false, |doc_host| doc_host == host); if docs_on_same_host { Cow::Borrowed("Home") // there will be verbose label on docs link, so repeating it would be noisy @@ -609,7 +595,7 @@ impl<'a> CratePage<'a> { /// `(url, label)` pub fn documentation_link(&self) -> Option<(&str, Cow<'_, str>)> { self.ver.documentation().map(|url| { - let label = Self::url_domain(url) + let label = url_domain(url) .map(|host| if host == "docs.rs" { "API Reference".into() } else { Cow::Owned(format!("Documentation ({})", host)) }) .unwrap_or_else(|| "Documentation".into()); (url, label) @@ -622,7 +608,7 @@ impl<'a> CratePage<'a> { let label_prefix = repo.site_link_label(); let label = match repo.host() { RepoHost::GitHub(ref host) | RepoHost::GitLab(ref host) | RepoHost::BitBucket(ref host) => format!("{} ({})", label_prefix, host.owner), - RepoHost::Other => Self::url_domain(&url).map(|host| format!("{} ({})", label_prefix, host)).unwrap_or_else(|| label_prefix.to_string()), + RepoHost::Other => url_domain(&url).map(|host| format!("{} ({})", label_prefix, host)).unwrap_or_else(|| label_prefix.to_string()), }; (url, label) }) diff --git a/front_end/src/front_end.rs b/front_end/src/front_end.rs index 0cea8f3..c8f02d0 100644 --- a/front_end/src/front_end.rs +++ b/front_end/src/front_end.rs @@ -40,6 +40,7 @@ use semver::Version as SemVer; use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::io::Write; +use url::Url; include!(concat!(env!("OUT_DIR"), "/templates.rs")); @@ -338,3 +339,33 @@ pub(crate) fn date_now() -> String { pub(crate) fn render_markdown_str(s: &str, markup: &Renderer) -> templates::Html<String> { templates::Html(markup.markdown_str(s, false, None)) } + + +/// Nicely rounded number of downloads +/// +/// To show that these numbers are just approximate. +pub(crate) fn format_downloads(num: u32) -> (String, &'static 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"), + } +} + + +pub(crate) fn url_domain(url: &str) -> Option<Cow<'static, str>> { + Url::parse(url).ok().and_then(|url| { + url.host_str().and_then(|host| { + if host.ends_with(".github.io") { + Some("github.io".into()) + } else if host.ends_with(".githubusercontent.com") { + None + } else { + Some(host.trim_start_matches("www.").to_string().into()) + } + }) + }) +} diff --git a/front_end/src/install_page.rs b/front_end/src/install_page.rs index 338e175..c1222fd 100644 --- a/front_end/src/install_page.rs +++ b/front_end/src/install_page.rs @@ -39,6 +39,8 @@ impl<'a> InstallPage<'a> { item_description: self.ver.description().map(|d| d.to_string()), noindex: true, search_meta: false, + critical_css_data: Some(include_str!("../../style/public/install.css")), + critical_css_dev_url: Some("/install.css"), ..Default::default() } } diff --git a/front_end/src/urler.rs b/front_end/src/urler.rs index 8a1d13e..0e85680 100644 --- a/front_end/src/urler.rs +++ b/front_end/src/urler.rs @@ -140,9 +140,10 @@ impl Urler { /// This will probably change to a listing page rather than arbitrary personal URL pub fn author(&self, author: &CrateAuthor<'_>) -> Option<String> { if let Some(ref gh) = author.github { - Some(match gh.user_type { - UserType::User => format!("https://crates.io/users/{}", encode(&gh.login)), - UserType::Org | UserType::Bot => format!("https://github.com/{}", encode(&gh.login)), + Some(match (gh.user_type, author.owner) { + (UserType::User, true) => self.crates_io_user_by_github_login(&gh.login), + (UserType::User, _) => format!("https://crates.io/users/{}", encode(&gh.login)), + (UserType::Org, _) | (UserType::Bot, _) => format!("https://github.com/{}", encode(&gh.login)), }) } else if let Some(ref info) = author.info { if let Some(ref em) = info.email { @@ -162,6 +163,10 @@ impl Urler { } } + pub fn crates_io_user_by_github_login(&self, login: &str) -> String { + format!("/~{}", encode(login)) + } + pub fn search_crates_io(&self, query: &str) -> String { format!("https://crates.io/search?q={}", encode(query)) } diff --git a/front_end/templates/author.rs.html b/front_end/templates/author.rs.html index bc95198..f84dca3 100644 --- a/front_end/templates/author.rs.html +++ b/front_end/templates/author.rs.html @@ -1,6 +1,9 @@ +@use chrono_humanize::*; +@use crate::AuthorPage; +@use crate::iter::*; +@use crate::templates::author_list; @use crate::templates::base; @use crate::Urler; -@use crate::AuthorPage; @(url: &Urler, p: &AuthorPage) @@ -8,57 +11,91 @@ <header id="author"> <div class="inner-col"> <div class="breadcrumbs"> - <h1><a href="/">Lib.rs</a></h1> › @if p.is_org() { + <h1><a href="/">Lib.rs</a></h1> › <span class="has-keywords">@if p.is_org() { Orgs } else { Users - } + }</span> + + <span class="keywords"> + <span> + @for key in p.keywords.iter().take(3) { + <a href="@url.keyword(key)" class=keyword><span>#</span>@key</a> + } + </span> + @for key in p.keywords.iter().skip(3).take(3) { + <a href="@url.keyword(key)" class=keyword><span>#</span>@key</a> + } + </span> + </div> <h2> - @"@"@p.login() + @if p.orgs.iter().any(|o| o.login == "rust-lang") {<span class=labels><span title="Member of the rust-lang org">Rust team</span></span>} + @p.login() </h2> + @if !p.aut.name().is_empty() && p.aut.name() != p.login() { + <p class=desc>@p.aut.name()</p> + } + <p> + Joined crates-io @HumanTime::from(p.joined).to_text_en(Accuracy::Rough, Tense::Past)@if let Some(d) = p.joined_github() {. + Joined GitHub @HumanTime::from(d).to_text_en(Accuracy::Rough, Tense::Past). + } + </p> + <nav><ul> - <li><a href="@p.github_url()">GitHub</a></li> + <li><a rel="ugc nofollow" href="@p.github_url()">GitHub</a></li> + <li><a rel="ugc nofollow" href="https://crates.io/users/@p.aut.github.login">crates.io</a></li> + @if let Some((url, label)) = p.homepage_link() { + <li><a href="@url" rel="ugc nofollow" >@label</a></li> + } </ul></nav> </div> </header> <main> <div class="inner-col"> @if !p.orgs.is_empty() { - <section>Member of GitHub orgs</section> - <ul> - @for org in &p.orgs { - <li><a href="@org.url">@login</a></li> + <section> + <h3>Member of GitHub orgs</h3> + @for (last, org) in p.orgs.iter().identify_last() { + <a rel="ugc nofollow" href="@org.html_url">@AuthorPage::org_name(org)</a>@if !last {,} } - </ul> + </section> } - <section> - <h3>Crates by @p.aut.name()</h3> - <ul class=crates-list> - @for (k, r) in &p.crates { - <li> - <a href="@url.krate(&k)"> - <div class=h> - <h4> - @k.short_name() - </h4> - </div> - <div class=meta> - </div> - </a> - </li> + @if !p.collab.is_empty() { + <section> + <h3>Collaborated with</h3> + @for (last, user) in p.collab.iter().identify_last() { + <a href="@url.crates_io_user_by_github_login(&user.login)">@user.login</a>@if !last {,} } - </ul> - </section> + </section> + } + </div> + <div class="author-cols"> + <div class="inner-col"> + @if p.founder_total > 0 { + <section> + <h3>@p.aut.name() created @p.founder_total crate@if p.founder_total != 1 {s}</h3> + @:author_list(&p.founder_crates, url) + </section> + } |