use std::{env, fs, path::Path};
use base64::{Engine, engine::general_purpose::STANDARD};
const FACES: [Face; 4] = [
Face {
family: "Junicode",
style: "normal",
weight: "300 700",
file: "JunicodeVF-Roman.woff2",
},
Face {
family: "Junicode",
style: "italic",
weight: "300 700",
file: "JunicodeVF-Italic.woff2",
},
Face {
family: "UnifrakturCook",
style: "normal",
weight: "400",
file: "UnifrakturCook.woff2",
},
Face {
family: "Fira Code",
style: "normal",
weight: "300 700",
file: "FiraCode-VF.woff2",
},
];
struct Face {
family: &'static str,
style: &'static str,
weight: &'static str,
file: &'static str,
}
fn main() {
let fonts = Path::new("src/fonts");
let out = Path::new(&env::var("OUT_DIR").expect("cargo sets OUT_DIR")).to_path_buf();
write(&out.join("font-faces-whole.css"), &face_block(fonts));
write(
&out.join("font-faces-cut.css"),
&face_block(&fonts.join("cut")),
);
write(
&out.join("dropped.rs"),
&dropped(&fonts.join("cut/dropped.txt")),
);
}
fn face_block(dir: &Path) -> String {
let mut block = String::new();
for face in FACES {
let path = dir.join(face.file);
println!("cargo::rerun-if-changed={}", path.display());
let woff2 = fs::read(&path).unwrap_or_else(|error| {
panic!("reading {}: {error}", path.display());
});
block.push_str(&format!(
"@font-face{{font-family:\"{}\";font-style:{};font-weight:{};font-display:swap;\
src:url(data:font/woff2;base64,{}) format(\"woff2\")}}",
face.family,
face.style,
face.weight,
STANDARD.encode(woff2),
));
}
block
}
fn dropped(path: &Path) -> String {
println!("cargo::rerun-if-changed={}", path.display());
let text = fs::read_to_string(path)
.unwrap_or_else(|error| panic!("reading {}: {error}", path.display()));
let mut ranges = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (low, high) = line
.split_once('-')
.unwrap_or_else(|| panic!("{}: {line:?} is not a `LOW-HIGH` range", path.display()));
let parse = |value: &str| {
u32::from_str_radix(value, 16)
.unwrap_or_else(|error| panic!("{}: {value:?}: {error}", path.display()))
};
ranges.push((parse(low), parse(high)));
}
assert!(
ranges.windows(2).all(|pair| pair[0].1 < pair[1].0),
"{}: ranges must be sorted and disjoint for a binary search",
path.display()
);
let entries: Vec<String> = ranges
.iter()
.map(|(low, high)| format!("({low:#06X}, {high:#06X})"))
.collect();
format!(
"/// Codepoints an embedded face carries whole but not cut, as sorted,\n\
/// disjoint, inclusive ranges. Generated by `build.rs` from\n\
/// `src/fonts/cut/dropped.txt`.\n\
pub const DROPPED: &[(u32, u32)] = &[{}];\n",
entries.join(", ")
)
}
fn write(path: &Path, contents: &str) {
fs::write(path, contents).unwrap_or_else(|error| panic!("writing {}: {error}", path.display()));
}