use crate::config::{FontFace, IndexFonts};
use super::escape;
const SANS_FALLBACK: &str = "ui-sans-serif,system-ui,sans-serif";
const MONO_FALLBACK: &str = r#"ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace"#;
/// which of the page's faces its first screen draws
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct Drawn {
/// a person's header names a did
pub(super) did_heads: bool,
/// a commons row names a did
pub(super) did_rows: bool,
}
/// preloads and css for the head, empty without fonts
pub(super) fn head(fonts: Option<&IndexFonts>, drawn: Drawn) -> String {
let Some(fonts) = fonts else {
return String::new();
};
let text = fonts.sans.as_deref().or(fonts.mono.as_deref());
let did = fonts.mono.as_deref();
let mut wanted = vec![(text, 400), (text, 700)];
if drawn.did_heads {
wanted.push((did, 700));
}
if drawn.did_rows {
wanted.push((did, 400));
}
let mut preloads: Vec<&FontFace> = Vec::new();
for (family, weight) in wanted {
if let Some(face) = family.and_then(|family| matched(&fonts.faces, family, weight))
&& !preloads.contains(&face)
{
preloads.push(face);
}
}
let mut out = String::new();
for face in preloads {
out.push_str(&format!(
"\n<link rel=\"preload\" href=\"{}\" as=\"font\" type=\"font/woff2\" crossorigin>",
escape(&face.url)
));
}
out.push_str("\n<style>");
for face in &fonts.faces {
let format = if face.url.ends_with(".woff2") {
" format(\"woff2\")"
} else {
""
};
let style = if face.italic { "italic" } else { "normal" };
out.push_str(&format!(
"@font-face{{font-family:\"{}\";src:url(\"{}\"){format};font-weight:{};font-style:{style};font-display:block}}\n",
face.family, face.url, face.weight
));
}
let mono = fonts
.mono
.as_ref()
.map_or(MONO_FALLBACK.to_string(), |mono| {
format!("\"{mono}\",{MONO_FALLBACK}")
});
let body = fonts
.sans
.as_ref()
.map_or(mono.clone(), |sans| format!("\"{sans}\",{SANS_FALLBACK}"));
// one x-height whichever face draws, and digits that hold their columns
out.push_str(&format!(
"html{{font-size-adjust:ex-height .5}}\nbody{{font-family:{body};font-variant-numeric:tabular-nums}}\n.did{{font-family:{mono}}}</style>"
));
out
}
/// the face css font matching picks for `weight` in `family`
fn matched<'a>(faces: &'a [FontFace], family: &str, weight: u16) -> Option<&'a FontFace> {
let upright: Vec<&FontFace> = faces
.iter()
.filter(|face| face.family == family && !face.italic)
.collect();
let lighter = |face: &&&FontFace| face.weight <= weight;
let heavier = |face: &&&FontFace| face.weight >= weight;
let below = upright
.iter()
.filter(lighter)
.max_by_key(|face| face.weight);
let above = upright
.iter()
.filter(heavier)
.min_by_key(|face| face.weight);
let found = match weight {
400..=500 => upright
.iter()
.filter(|face| (weight..=500).contains(&face.weight))
.min_by_key(|face| face.weight)
.or(below)
.or(above),
..400 => below.or(above),
_ => above.or(below),
};
found.copied()
}