samjeston / openhub Public
Sam Jeston Working vertical 9aa3c45 · History
173 lines· 4.3 KB ·Rust
Raw
1//! Small formatting helpers called from templates.
2
3use chrono::{DateTime, Utc};
4
5use crate::db::now;
6
7/// Lets helpers accept numbers by value or by (nested) reference, since
8/// templates pass fields by reference but method results by value.
9pub trait Num<T: Copy> {
10 fn value(&self) -> T;
11}
12
13impl Num<i64> for i64 {
14 fn value(&self) -> i64 {
15 *self
16 }
17}
18
19impl Num<usize> for usize {
20 fn value(&self) -> usize {
21 *self
22 }
23}
24
25impl<T: Copy, N: Num<T> + ?Sized> Num<T> for &N {
26 fn value(&self) -> T {
27 (**self).value()
28 }
29}
30
31pub fn icon(name: &str) -> String {
32 crate::icons::icon(name)
33}
34
35/// "3 days ago" style relative time.
36pub fn ago(ts: impl Num<i64>) -> String {
37 let delta = now() - ts.value();
38 if delta < 45 {
39 return "just now".into();
40 }
41 let (n, unit) = match delta {
42 d if d < 3600 => ((d + 30) / 60, "minute"),
43 d if d < 86400 => ((d + 1800) / 3600, "hour"),
44 d if d < 30 * 86400 => ((d + 43200) / 86400, "day"),
45 d if d < 365 * 86400 => (d / (30 * 86400), "month"),
46 d => (d / (365 * 86400), "year"),
47 };
48 let n = n.max(1);
49 format!("{n} {unit}{} ago", if n == 1 { "" } else { "s" })
50}
51
52fn datetime(ts: i64) -> DateTime<Utc> {
53 DateTime::from_timestamp(ts, 0).unwrap_or_default()
54}
55
56/// "Oct 10, 2026"
57pub fn date(ts: impl Num<i64>) -> String {
58 datetime(ts.value()).format("%b %-d, %Y").to_string()
59}
60
61/// "Oct 10, 2026, 14:05 UTC"
62pub fn datetime_full(ts: impl Num<i64>) -> String {
63 datetime(ts.value())
64 .format("%b %-d, %Y, %H:%M UTC")
65 .to_string()
66}
67
68pub fn short_sha(sha: &str) -> &str {
69 sha.get(..7).unwrap_or(sha)
70}
71
72pub fn bytes(n: impl Num<usize>) -> String {
73 let n = n.value() as f64;
74 if n < 1024.0 {
75 format!("{n} Bytes")
76 } else if n < 1024.0 * 1024.0 {
77 format!("{:.1} KB", n / 1024.0)
78 } else {
79 format!("{:.1} MB", n / (1024.0 * 1024.0))
80 }
81}
82
83pub fn plural(n: impl Num<usize>, word: &str) -> String {
84 let n = n.value();
85 if n == 1 {
86 format!("1 {word}")
87 } else if word.ends_with("ch") || word.ends_with('s') || word.ends_with('x') {
88 format!("{n} {word}es")
89 } else {
90 format!("{n} {word}s")
91 }
92}
93
94/// A coloured initial badge used in place of profile pictures.
95pub fn avatar(name: &str, size: u32) -> String {
96 let hue = name
97 .bytes()
98 .fold(7u32, |h, b| h.wrapping_mul(31).wrapping_add(b as u32))
99 % 360;
100 let initial = name
101 .chars()
102 .find(|c| c.is_alphanumeric())
103 .map(|c| c.to_uppercase().to_string())
104 .unwrap_or_else(|| "?".into());
105 let font = (size as f32 * 0.45).round() as u32;
106 format!(
107 r#"<span class="avatar" style="--hue:{hue};width:{size}px;height:{size}px;font-size:{font}px" aria-hidden="true">{}</span>"#,
108 crate::highlight::escape(&initial)
109 )
110}
111
112/// Percent-encodes a value for use in a query string.
113pub fn urlencode(s: &str) -> String {
114 let mut out = String::with_capacity(s.len());
115 for b in s.bytes() {
116 match b {
117 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
118 out.push(b as char)
119 }
120 _ => out.push_str(&format!("%{b:02X}")),
121 }
122 }
123 out
124}
125
126/// Percent-encodes a repository path or ref for use in a URL path, keeping `/`.
127pub fn urlpath(s: &str) -> String {
128 let mut out = String::with_capacity(s.len());
129 for b in s.bytes() {
130 match b {
131 b'A'..=b'Z'
132 | b'a'..=b'z'
133 | b'0'..=b'9'
134 | b'-'
135 | b'_'
136 | b'.'
137 | b'~'
138 | b'/'
139 | b'@'
140 | b'+'
141 | b'!'
142 | b'$'
143 | b'('
144 | b')'
145 | b','
146 | b';'
147 | b'='
148 | b':' => out.push(b as char),
149 _ => out.push_str(&format!("%{b:02X}")),
150 }
151 }
152 out
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn relative_times() {
161 let n = now();
162 assert_eq!(ago(n), "just now");
163 assert_eq!(ago(n - 60), "1 minute ago");
164 assert_eq!(ago(n - 2 * 3600), "2 hours ago");
165 assert_eq!(ago(n - 3 * 86400), "3 days ago");
166 }
167
168 #[test]
169 fn url_encoding() {
170 assert_eq!(urlpath("src/a b#c.rs"), "src/a%20b%23c.rs");
171 assert_eq!(urlencode("/a/b?c"), "%2Fa%2Fb%3Fc");
172 }
173}