Skip to main content

blit_webserver/
lib.rs

1use axum::http::header;
2use axum::response::{Html, IntoResponse, Response};
3
4/// Serve the monospace font family list as JSON.
5pub fn fonts_list_response(cors_origin: Option<&str>) -> Response {
6    let families = blit_fonts::list_monospace_font_families();
7    let json = format!(
8        "[{}]",
9        families
10            .iter()
11            .map(|f| format!("\"{}\"", f.replace('"', "\\\"")))
12            .collect::<Vec<_>>()
13            .join(",")
14    );
15    let mut resp = (
16        [
17            (header::CONTENT_TYPE, "application/json"),
18            (header::CACHE_CONTROL, "public, max-age=3600"),
19        ],
20        json,
21    )
22        .into_response();
23    add_cors(&mut resp, cors_origin);
24    resp
25}
26
27/// Serve a font's @font-face CSS by family name, or 404.
28pub fn font_response(name: &str, cors_origin: Option<&str>) -> Response {
29    match blit_fonts::font_face_css(name) {
30        Some(css) => {
31            let mut resp = (
32                [
33                    (header::CONTENT_TYPE, "text/css"),
34                    (header::CACHE_CONTROL, "public, max-age=86400, immutable"),
35                ],
36                css,
37            )
38                .into_response();
39            add_cors(&mut resp, cors_origin);
40            resp
41        }
42        None => (axum::http::StatusCode::NOT_FOUND, "font not found").into_response(),
43    }
44}
45
46/// Serve font metrics (advance ratio) as JSON.
47pub fn font_metrics_response(name: &str, cors_origin: Option<&str>) -> Response {
48    match blit_fonts::font_advance_ratio(name) {
49        Some(ratio) => {
50            let json = format!("{{\"advanceRatio\":{}}}", ratio);
51            let mut resp = (
52                [
53                    (header::CONTENT_TYPE, "application/json"),
54                    (header::CACHE_CONTROL, "public, max-age=86400, immutable"),
55                ],
56                json,
57            )
58                .into_response();
59            add_cors(&mut resp, cors_origin);
60            resp
61        }
62        None => (axum::http::StatusCode::NOT_FOUND, "font not found").into_response(),
63    }
64}
65
66fn add_cors(resp: &mut Response, origin: Option<&str>) {
67    if let Some(origin) = origin {
68        if let Ok(val) = origin.parse() {
69            resp.headers_mut()
70                .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, val);
71        }
72    }
73}
74
75/// Serve HTML with ETag support. Returns 304 if the client's `If-None-Match`
76/// matches `etag`.
77pub fn html_response(html: &'static str, etag: &str, if_none_match: Option<&[u8]>) -> Response {
78    if let Some(inm) = if_none_match {
79        if inm == etag.as_bytes() {
80            return (
81                axum::http::StatusCode::NOT_MODIFIED,
82                [(axum::http::header::ETAG, etag)],
83            )
84                .into_response();
85        }
86    }
87    ([(axum::http::header::ETAG, etag)], Html(html)).into_response()
88}
89
90/// Try to match a font route from a raw request path (any prefix).
91/// Handles `/fonts`, `/vt/fonts`, `/font/Name`, `/vt/font/Name%20With%20Spaces`.
92/// Returns `Some(response)` if the path matched a font route, `None` otherwise.
93pub fn try_font_route(path: &str, cors_origin: Option<&str>) -> Option<Response> {
94    if path == "/fonts" || path.ends_with("/fonts") {
95        return Some(fonts_list_response(cors_origin));
96    }
97    if let Some(raw) = path.rsplit_once("/font-metrics/").map(|(_, n)| n) {
98        if !raw.contains('/') && !raw.is_empty() {
99            let name = percent_encoding::percent_decode_str(raw).decode_utf8_lossy();
100            return Some(font_metrics_response(&name, cors_origin));
101        }
102    }
103    if let Some(raw) = path.rsplit_once("/font/").map(|(_, n)| n) {
104        if !raw.contains('/') && !raw.is_empty() {
105            let name = percent_encoding::percent_decode_str(raw).decode_utf8_lossy();
106            return Some(font_response(&name, cors_origin));
107        }
108    }
109    None
110}
111
112/// Compute an ETag string from HTML content.
113pub fn html_etag(html: &str) -> String {
114    use std::hash::{Hash, Hasher};
115    let mut h = std::collections::hash_map::DefaultHasher::new();
116    html.hash(&mut h);
117    format!("\"blit-{:x}\"", h.finish())
118}