Krolyxon
Commit 3cb9866

ferreniumsrcrendermod.rs

116 lines · 3.2 KB · Rust

1//! Presentation helpers shared by handlers and templates.
2
3pub mod diff;
4pub mod highlight;
5pub mod markdown;
6pub mod time;
7
8use std::fmt::Write;
9
10/// Percent-encode a slash-separated path for use in a URL, keeping `/`.
11pub fn encode_path(path: &str) -> String {
12 encode_bytes(path.as_bytes())
13}
14
15/// [`encode_path`] for paths that may not be UTF-8 (git tree paths).
16pub fn encode_bytes(path: &[u8]) -> String {
17 let mut out = String::with_capacity(path.len());
18 for &b in path {
19 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
20 out.push(b as char);
21 } else {
22 let _ = write!(out, "%{b:02X}");
23 }
24 }
25 out
26}
27
28/// Decode `%XX` escapes (invalid escapes are kept literally).
29pub fn percent_decode(s: &str) -> Vec<u8> {
30 let bytes = s.as_bytes();
31 let mut out = Vec::with_capacity(bytes.len());
32 let mut i = 0;
33 while i < bytes.len() {
34 let hex = |b: u8| (b as char).to_digit(16);
35 if bytes[i] == b'%'
36 && let (Some(&h), Some(&l)) = (bytes.get(i + 1), bytes.get(i + 2))
37 && let (Some(h), Some(l)) = (hex(h), hex(l))
38 {
39 out.push((h * 16 + l) as u8);
40 i += 3;
41 } else {
42 out.push(bytes[i]);
43 i += 1;
44 }
45 }
46 out
47}
48
49/// Append `text` to `out` with `&<>"'` escaped.
50pub fn escape_html(out: &mut String, text: &str) {
51 for c in text.chars() {
52 match c {
53 '&' => out.push_str("&amp;"),
54 '<' => out.push_str("&lt;"),
55 '>' => out.push_str("&gt;"),
56 '"' => out.push_str("&quot;"),
57 '\'' => out.push_str("&#39;"),
58 c => out.push(c),
59 }
60 }
61}
62
63/// "12.4 KB" (binary multiples, conventional labels).
64pub fn human_size(bytes: u64) -> String {
65 const UNITS: [&str; 4] = ["KB", "MB", "GB", "TB"];
66 if bytes < 1024 {
67 return format!("{bytes} B");
68 }
69 let mut value = bytes as f64 / 1024.0;
70 let mut unit = 0;
71 while value >= 1024.0 && unit < UNITS.len() - 1 {
72 value /= 1024.0;
73 unit += 1;
74 }
75 if value >= 100.0 {
76 format!("{value:.0} {}", UNITS[unit])
77 } else {
78 format!("{value:.1} {}", UNITS[unit])
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn percent_decoding() {
88 assert_eq!(percent_decode("a%20b%2Fc"), b"a b/c");
89 assert_eq!(percent_decode("%C3%BC"), "ü".as_bytes());
90 assert_eq!(percent_decode("100%"), b"100%");
91 assert_eq!(percent_decode("%zz%4"), b"%zz%4");
92 }
93
94 #[test]
95 fn sizes() {
96 assert_eq!(human_size(0), "0 B");
97 assert_eq!(human_size(1023), "1023 B");
98 assert_eq!(human_size(12_698), "12.4 KB");
99 assert_eq!(human_size(300 * 1024), "300 KB");
100 assert_eq!(human_size(5 * 1024 * 1024), "5.0 MB");
101 }
102
103 #[test]
104 fn escaping() {
105 let mut s = String::new();
106 escape_html(&mut s, "<a href=\"x\">&'");
107 assert_eq!(s, "&lt;a href=&quot;x&quot;&gt;&amp;&#39;");
108 }
109
110 #[test]
111 fn encodes_reserved_but_keeps_slashes() {
112 assert_eq!(encode_path("forks/foo.rs"), "forks/foo.rs");
113 assert_eq!(encode_path("a b#?%"), "a%20b%23%3F%25");
114 assert_eq!(encode_path("ünï"), "%C3%BCn%C3%AF");
115 }
116}