mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 14:38:12 +02:00
KaTeX 0.16.11 embarqué et servi sous /katex/ (Go et façade)
Repris d'AJEAN (d6136caa, #123) : script, feuille de style, polices woff2 et licence MIT sous internal/loki/ui/katex/, embarqués par le go:embed et par build.rs de la façade. Pas de CDN : l'interface doit marcher hors ligne. Route /katex/ des deux côtés avec les mêmes en-têtes : katex.min.js (application/javascript), katex.min.css (text/css), fonts/*.woff2 (font/woff2), Cache-Control public 1 jour ; script et feuille compressés en gzip, polices non. Contrairement à AJEAN (http.FileServer), le Go (web_katex.go) ne sert que ces fichiers, avec un Content-Type fixe : ni index de dossier, ni type décidé par le registre MIME de l'OS. La façade (ui.rs) rend les mêmes réponses ; le reste de /katex/ part au Go (404). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6a45f19831
commit
99c96a6c33
28 files changed
+267
-1
No files matched your search
@@ -42,6 +42,32 @@ fn main() {
|
||||
}
|
||||
out.push_str("];\n");
|
||||
|
||||
// KaTeX (formules LaTeX, repris d'AJEAN) : script, feuille et polices
|
||||
// woff2 sous ui/katex/, les polices nommées par leur chemin relatif
|
||||
// (« fonts/x.woff2 »), comme les sert web_katex.go.
|
||||
let katex = ui.join("katex");
|
||||
println!("cargo:rerun-if-changed={}", katex.display());
|
||||
for (ident, name) in [("KATEX_JS", "katex.min.js"), ("KATEX_CSS", "katex.min.css")] {
|
||||
let p = katex.join(name);
|
||||
assert!(p.is_file(), "fichier KaTeX manquant : {}", p.display());
|
||||
println!("cargo:rerun-if-changed={}", p.display());
|
||||
writeln!(out, "pub static {ident}: &[u8] = include_bytes!({p:?});").unwrap();
|
||||
}
|
||||
let katex_fonts = katex.join("fonts");
|
||||
println!("cargo:rerun-if-changed={}", katex_fonts.display());
|
||||
let mut kfonts: Vec<PathBuf> = std::fs::read_dir(&katex_fonts)
|
||||
.unwrap_or_else(|e| panic!("{} : {e}", katex_fonts.display()))
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "woff2"))
|
||||
.collect();
|
||||
kfonts.sort();
|
||||
out.push_str("pub static KATEX_FONTS: &[(&str, &[u8])] = &[\n");
|
||||
for p in &kfonts {
|
||||
let name = format!("fonts/{}", p.file_name().unwrap().to_str().unwrap());
|
||||
writeln!(out, " ({name:?}, include_bytes!({p:?})),").unwrap();
|
||||
}
|
||||
out.push_str("];\n");
|
||||
|
||||
let dst = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("ui_assets.rs");
|
||||
std::fs::write(dst, out).unwrap();
|
||||
}
|
||||
@@ -48,6 +48,10 @@ static MANIFEST: Asset = Asset::new(
|
||||
"application/manifest+json",
|
||||
"public, max-age=3600",
|
||||
);
|
||||
/// KaTeX (formules LaTeX des réponses) : mêmes en-têtes que `web_katex.go`.
|
||||
const KATEX_CACHE: &str = "public, max-age=86400";
|
||||
static KATEX_JS: Asset = Asset::new(assets::KATEX_JS, "application/javascript", KATEX_CACHE);
|
||||
static KATEX_CSS: Asset = Asset::new(assets::KATEX_CSS, "text/css; charset=utf-8", KATEX_CACHE);
|
||||
|
||||
/// Réponse statique pour cette requête, ou `None` si la route n'est pas une
|
||||
/// ressource de l'UI (elle part alors au Go, qui rendra son 404 habituel).
|
||||
@@ -76,6 +80,9 @@ pub fn serve(method: &Method, path: &str, headers: &HeaderMap) -> Option<Respons
|
||||
}
|
||||
"/manifest.webmanifest" => asset(&MANIFEST, accepts_gzip),
|
||||
p if p.starts_with("/fonts/") => font(p)?,
|
||||
"/katex/katex.min.js" => asset(&KATEX_JS, accepts_gzip),
|
||||
"/katex/katex.min.css" => asset(&KATEX_CSS, accepts_gzip),
|
||||
p if p.starts_with("/katex/") => katex_font(p)?,
|
||||
_ => return None,
|
||||
};
|
||||
if accepts_gzip {
|
||||
@@ -130,6 +137,20 @@ fn font(path: &str) -> Option<Response<Body>> {
|
||||
Some(r)
|
||||
}
|
||||
|
||||
/// `/katex/fonts/<nom>.woff2` — chemin exact (pas le dernier segment seul,
|
||||
/// contrairement à `/fonts/`), comme `katexFile` dans `web_katex.go`. Le
|
||||
/// reste de `/katex/` part au Go, qui rend son 404.
|
||||
fn katex_font(path: &str) -> Option<Response<Body>> {
|
||||
let rel = path.strip_prefix("/katex/")?;
|
||||
let (_, body) = assets::KATEX_FONTS.iter().find(|(n, _)| *n == rel)?;
|
||||
let mut r = Response::new(full(Bytes::from_static(body)));
|
||||
let h = r.headers_mut();
|
||||
h.insert(header::CONTENT_TYPE, HeaderValue::from_static("font/woff2"));
|
||||
h.insert(header::CACHE_CONTROL, HeaderValue::from_static(KATEX_CACHE));
|
||||
h.insert(header::CONTENT_LENGTH, HeaderValue::from(body.len()));
|
||||
Some(r)
|
||||
}
|
||||
|
||||
fn compress(b: &[u8]) -> Vec<u8> {
|
||||
let mut enc = GzEncoder::new(Vec::with_capacity(b.len() / 3), Compression::default());
|
||||
enc.write_all(b).expect("gzip en mémoire");
|
||||
@@ -193,6 +214,46 @@ mod tests {
|
||||
assert!(get("/fonts/inter.ttf", false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn katex() {
|
||||
let r = get("/katex/katex.min.js", true).unwrap();
|
||||
assert_eq!(hv(&r, "content-type"), "application/javascript");
|
||||
assert_eq!(hv(&r, "cache-control"), "public, max-age=86400");
|
||||
assert_eq!(hv(&r, "content-encoding"), "gzip");
|
||||
assert!(
|
||||
String::from_utf8_lossy(assets::KATEX_JS).contains("version:\"0.16.11\""),
|
||||
"KaTeX 0.16.11 attendu"
|
||||
);
|
||||
let r = get("/katex/katex.min.css", false).unwrap();
|
||||
assert_eq!(hv(&r, "content-type"), "text/css; charset=utf-8");
|
||||
// Chaque police woff2 demandée par la feuille est servie, non compressée.
|
||||
let css = String::from_utf8_lossy(assets::KATEX_CSS);
|
||||
let polices: Vec<&str> = css
|
||||
.split("url(")
|
||||
.skip(1)
|
||||
.filter_map(|s| s.split(')').next())
|
||||
.filter(|u| u.ends_with(".woff2"))
|
||||
.collect();
|
||||
assert!(polices.len() >= 10, "{} polices", polices.len());
|
||||
for p in polices {
|
||||
let r = get(&format!("/katex/{p}"), true).unwrap_or_else(|| panic!("{p}"));
|
||||
assert_eq!(hv(&r, "content-type"), "font/woff2");
|
||||
assert_eq!(hv(&r, "content-encoding"), "");
|
||||
assert_eq!(hv(&r, "cache-control"), "public, max-age=86400");
|
||||
}
|
||||
// Le reste part au Go (404) : ni index, ni licence, ni dernier segment seul.
|
||||
for p in [
|
||||
"/katex/",
|
||||
"/katex/LICENSE",
|
||||
"/katex/fonts/",
|
||||
"/katex/fonts/absente.woff2",
|
||||
"/katex/KaTeX_Main-Regular.woff2",
|
||||
"/katex/x/fonts/KaTeX_Main-Regular.woff2",
|
||||
] {
|
||||
assert!(get(p, false).is_none(), "{p}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gzip_si_accepte() {
|
||||
let r = get("/", true).unwrap();
|
||||
|
||||
Reference in new issue
Block a user