1use std::sync::OnceLock;
2
3use topcoat_core::context::Cx;
4use topcoat_router::{
5 Body, HeaderValue, Method, Methods, Path, PathBuf, Route, RouteFuture, RouteId, RouterBuilder,
6 header::{CACHE_CONTROL, CONTENT_TYPE},
7 response::Response,
8};
9
10use crate::{Font, FontResolver};
11
12const FONT_ROUTE_PREFIX: &str = "/_topcoat/fonts";
13
14const CACHE_CONTROL_VALUE: HeaderValue =
17 HeaderValue::from_static("public, max-age=31536000, immutable");
18
19fn font_route_path(font: Font, write: &mut dyn std::fmt::Write) -> std::fmt::Result {
26 write.write_str(FONT_ROUTE_PREFIX)?;
27 write.write_str("/")?;
28 for ch in font.family().chars() {
29 write.write_char(if ch.is_ascii_alphanumeric() { ch } else { '-' })?;
30 }
31 write!(write, "-{:016x}.css", font.hash())
32}
33
34pub struct FontRoute {
35 id: RouteId,
36 path: PathBuf,
37 font: Font,
38 cache: OnceLock<String>,
39}
40
41impl FontRoute {
42 #[must_use]
43 pub fn new(font: Font) -> Self {
44 let mut path = String::new();
45 let _ = font_route_path(font, &mut path);
46 Self {
47 id: RouteId::new(),
48 path: Path::new(&path).to_owned(),
49 font,
50 cache: OnceLock::new(),
51 }
52 }
53}
54
55impl Route for FontRoute {
56 fn id(&self) -> RouteId {
57 self.id
58 }
59
60 fn methods(&self) -> Methods<'_> {
61 Methods::Only(&[Method::GET])
62 }
63
64 fn path(&self) -> &Path {
65 &self.path
66 }
67
68 fn handle<'cx>(&'cx self, cx: &'cx Cx, _body: Body) -> RouteFuture<'cx> {
69 Box::pin(async {
70 let cached_css = self.cache.get_or_init(|| {
72 let mut css = String::new();
73 let _ = self.font.faces().fmt(cx, &mut css);
74 css
75 });
76
77 let mut response = Response::new(Body::from(cached_css.clone()));
78 let headers = response.headers_mut();
79 headers.insert(
80 CONTENT_TYPE,
81 HeaderValue::from_static("text/css; charset=utf-8"),
82 );
83 headers.insert(CACHE_CONTROL, CACHE_CONTROL_VALUE);
84 Ok(response)
85 })
86 }
87}
88
89pub trait RouterBuilderFontExt {
90 #[must_use]
91 fn font(self, font: Font) -> Self;
92
93 #[cfg(feature = "discover")]
94 #[must_use]
95 fn discover_fonts(self) -> Self;
96}
97
98impl RouterBuilderFontExt for RouterBuilder {
99 fn font(mut self, font: Font) -> Self {
100 self = self.route(FontRoute::new(font));
101 if self.get_app_context::<FontResolver>().is_none() {
104 self = self.app_context(FontResolver::new(Box::new(font_route_path)));
105 }
106 self
107 }
108
109 #[cfg(feature = "discover")]
110 fn discover_fonts(mut self) -> Self {
111 for font in inventory::iter::<crate::Font> {
112 self = self.font(*font);
113 }
114 self
115 }
116}