summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKornel <kornel@geekhood.net>2020-03-10 00:37:22 +0000
committerKornel <kornel@geekhood.net>2020-03-10 00:43:54 +0000
commit54d2fbd88a38e4c95e6293e9e2067640afa4404a (patch)
tree4ed6532e3b30a4672988896af80a3359d29de89a
parent34bc2b12e637456c8c5140dfcb4a49d11ac544e5 (diff)
Author page
-rw-r--r--crate_db/Cargo.toml2
-rw-r--r--crate_db/src/lib_crate_db.rs16
-rw-r--r--crates_io_client/Cargo.toml3
-rw-r--r--crates_io_client/src/crate_owners.rs7
-rw-r--r--datadump/src/main.rs5
-rw-r--r--front_end/Cargo.toml2
-rw-r--r--front_end/src/author_page.rs259
-rw-r--r--front_end/src/cat_page.rs14
-rw-r--r--front_end/src/crate_page.rs24
-rw-r--r--front_end/src/front_end.rs31
-rw-r--r--front_end/src/install_page.rs2
-rw-r--r--front_end/src/urler.rs11
-rw-r--r--front_end/templates/author.rs.html93
-rw-r--r--front_end/templates/author_list.rs.html43
-rw-r--r--front_end/templates/base.rs.html4
-rw-r--r--front_end/templates/cat_page.rs.html3
-rw-r--r--github_info/src/lib_github.rs17
-rw-r--r--github_info/src/model.rs16
-rw-r--r--github_v3/src/model.rs1
-rw-r--r--kitchen_sink/Cargo.toml2
-rw-r--r--kitchen_sink/src/lib_kitchen_sink.rs8
-rw-r--r--server/Cargo.toml2
m---------style0
-rw-r--r--user_db/src/lib_user_db.rs2
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>
+ }