Skip to main content

blit_webserver/
lib.rs

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