| 1044 |
1044 |
|
out
|
| 1045 |
1045 |
|
}
|
| 1046 |
1046 |
|
|
|
1047 |
+ |
// ============================================================================
|
|
1048 |
+ |
// Typography — layer 0, the app override.
|
|
1049 |
+ |
//
|
|
1050 |
+ |
// Wiki `typography-standard`, GO makeover `174ab3c1`. Layer 1 above is what
|
|
1051 |
+ |
// every product shares; this is the one declaration a product is allowed to
|
|
1052 |
+ |
// make for itself:
|
|
1053 |
+ |
//
|
|
1054 |
+ |
// layer 0 app override per product, optional MNW display -> Young Serif
|
|
1055 |
+ |
// layer 1 house default the quasi-* slot font quasi-mono -> Quasi Mono
|
|
1056 |
+ |
// layer 2 system generic one hop, no further monospace / sans-serif
|
|
1057 |
+ |
//
|
|
1058 |
+ |
// The brand tier was already exempt by decision (`cdf8ac09`), and the exemption
|
|
1059 |
+ |
// was enforced by those faces simply not being in the vocabulary — so each
|
|
1060 |
+ |
// product reached its own face through a hardcoded `font-family` and an
|
|
1061 |
+ |
// `@font-face` block it maintained by hand, which is the exact shape the
|
|
1062 |
+ |
// unification is deleting everywhere else. This turns the carve-out into a
|
|
1063 |
+ |
// mechanism: the per-product face is declared once, in the build script that
|
|
1064 |
+ |
// already writes the typography layer, and is readable as an override rather
|
|
1065 |
+ |
// than as a stylesheet nobody unified.
|
|
1066 |
+ |
//
|
|
1067 |
+ |
// It permits overriding `mono` and `sans` too. No product wants that today,
|
|
1068 |
+ |
// and a layer that only allows overriding the slot nobody describes is not a
|
|
1069 |
+ |
// layer, it is the exemption restated.
|
|
1070 |
+ |
//
|
|
1071 |
+ |
// **One declaration per product per slot.** [`Typography::with_override`]
|
|
1072 |
+ |
// panics on a second override of the same slot rather than letting the last
|
|
1073 |
+ |
// one win: a product with two answers for a slot has the vocabulary wrong, and
|
|
1074 |
+ |
// that is the thing to fix.
|
|
1075 |
+ |
//
|
|
1076 |
+ |
// # What a renderer does when it cannot honour one
|
|
1077 |
+ |
//
|
|
1078 |
+ |
// Declare once, renderers honour what they can. Today only the webview surface
|
|
1079 |
+ |
// has a face to honour at all — neither `makeover-tui` nor `makeover-immediate`
|
|
1080 |
+ |
// emits a `font-family` from anywhere, because the terminal owns the face in
|
|
1081 |
+ |
// one and the app loads its own font stack in the other. So an override is
|
|
1082 |
+ |
// honoured by the generated stylesheet and ignored, silently and correctly, by
|
|
1083 |
+ |
// the other two. A renderer that gains font control later reads
|
|
1084 |
+ |
// [`Typography::resolve`] rather than the CSS, which is why the resolution is
|
|
1085 |
+ |
// a method on the data and not a string-building detail.
|
|
1086 |
+ |
// ============================================================================
|
|
1087 |
+ |
|
|
1088 |
+ |
/// A slot in the house font vocabulary — the unit an override replaces.
|
|
1089 |
+ |
///
|
|
1090 |
+ |
/// Three, and the third is deliberately empty by default: `display` is the
|
|
1091 |
+ |
/// brand tier, it has no house answer, and a product that does not override it
|
|
1092 |
+ |
/// leaves the token undefined so whatever the consumer wrote as a fallback
|
|
1093 |
+ |
/// renders. The MNW embeds rely on exactly that.
|
|
1094 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
1095 |
+ |
pub enum FontSlot {
|
|
1096 |
+ |
/// Code, data, identifiers, cell grids. [`FONT_MONO`] by default.
|
|
1097 |
+ |
Mono,
|
|
1098 |
+ |
/// Body and UI text: everything that is not mono or brand. [`FONT_SANS`].
|
|
1099 |
+ |
Sans,
|
|
1100 |
+ |
/// The brand / display tier. No house default, per `cdf8ac09`.
|
|
1101 |
+ |
Display,
|
|
1102 |
+ |
}
|
|
1103 |
+ |
|
|
1104 |
+ |
impl FontSlot {
|
|
1105 |
+ |
/// Every slot, in the order they are emitted.
|
|
1106 |
+ |
pub const ALL: [FontSlot; 3] = [FontSlot::Mono, FontSlot::Sans, FontSlot::Display];
|
|
1107 |
+ |
|
|
1108 |
+ |
/// The custom property this slot is read through.
|
|
1109 |
+ |
pub fn token(self) -> &'static str {
|
|
1110 |
+ |
match self {
|
|
1111 |
+ |
FontSlot::Mono => "--font-mono",
|
|
1112 |
+ |
FontSlot::Sans => "--font-sans",
|
|
1113 |
+ |
FontSlot::Display => "--font-display",
|
|
1114 |
+ |
}
|
|
1115 |
+ |
}
|
|
1116 |
+ |
|
|
1117 |
+ |
/// The house stack, or `None` for the brand tier.
|
|
1118 |
+ |
pub fn house_default(self) -> Option<&'static str> {
|
|
1119 |
+ |
match self {
|
|
1120 |
+ |
FontSlot::Mono => Some(FONT_MONO),
|
|
1121 |
+ |
FontSlot::Sans => Some(FONT_SANS),
|
|
1122 |
+ |
FontSlot::Display => None,
|
|
1123 |
+ |
}
|
|
1124 |
+ |
}
|
|
1125 |
+ |
}
|
|
1126 |
+ |
|
|
1127 |
+ |
/// One `@font-face` an override brings with it.
|
|
1128 |
+ |
///
|
|
1129 |
+ |
/// A product overriding a slot usually has to ship the face too, and the two
|
|
1130 |
+ |
/// halves have to agree on a family name. Declaring them together is what
|
|
1131 |
+ |
/// makes that agreement structural rather than a string typed twice.
|
|
1132 |
+ |
#[derive(Debug, Clone)]
|
|
1133 |
+ |
pub struct FontFace {
|
|
1134 |
+ |
family: String,
|
|
1135 |
+ |
sources: Vec<String>,
|
|
1136 |
+ |
weight: Option<String>,
|
|
1137 |
+ |
style: Option<String>,
|
|
1138 |
+ |
}
|
|
1139 |
+ |
|
|
1140 |
+ |
impl FontFace {
|
|
1141 |
+ |
/// A face named `family`, fetched from `sources`.
|
|
1142 |
+ |
///
|
|
1143 |
+ |
/// Each source is either a bare filename, resolved against the
|
|
1144 |
+ |
/// [`Typography`] base URL, or an absolute one (`/…` or `https://…`) taken
|
|
1145 |
+ |
/// as written. The `format()` hint is inferred from the extension —
|
|
1146 |
+ |
/// `woff2`, `woff`, `ttf`, `otf` — and omitted for anything else rather
|
|
1147 |
+ |
/// than guessed, since a wrong hint is worse than none.
|
|
1148 |
+ |
pub fn new<S: Into<String>>(
|
|
1149 |
+ |
family: impl Into<String>,
|
|
1150 |
+ |
sources: impl IntoIterator<Item = S>,
|
|
1151 |
+ |
) -> Self {
|
|
1152 |
+ |
Self {
|
|
1153 |
+ |
family: family.into(),
|
|
1154 |
+ |
sources: sources.into_iter().map(Into::into).collect(),
|
|
1155 |
+ |
weight: None,
|
|
1156 |
+ |
style: None,
|
|
1157 |
+ |
}
|
|
1158 |
+ |
}
|
|
1159 |
+ |
|
|
1160 |
+ |
/// `font-weight`, as CSS writes it: `"700"`, or `"200 800"` for a variable
|
|
1161 |
+ |
/// axis. Omitted when unset, which means `normal`.
|
|
1162 |
+ |
///
|
|
1163 |
+ |
/// A variable face MUST name its range here for the same reason the house
|
|
1164 |
+ |
/// faces do: a `@font-face` with no range makes the browser resolve every
|
|
1165 |
+ |
/// weight to the file's default instance.
|
|
1166 |
+ |
#[must_use]
|
|
1167 |
+ |
pub fn weight(mut self, weight: impl Into<String>) -> Self {
|
|
1168 |
+ |
self.weight = Some(weight.into());
|
|
1169 |
+ |
self
|
|
1170 |
+ |
}
|
|
1171 |
+ |
|
|
1172 |
+ |
/// `font-style`. Omitted when unset, which means `normal`.
|
|
1173 |
+ |
#[must_use]
|
|
1174 |
+ |
pub fn style(mut self, style: impl Into<String>) -> Self {
|
|
1175 |
+ |
self.style = Some(style.into());
|
|
1176 |
+ |
self
|
|
1177 |
+ |
}
|
|
1178 |
+ |
|
|
1179 |
+ |
fn css(&self, base: &str) -> String {
|
|
1180 |
+ |
use std::fmt::Write as _;
|
|
1181 |
+ |
|
|
1182 |
+ |
let src = self
|
|
1183 |
+ |
.sources
|
|
1184 |
+ |
.iter()
|
|
1185 |
+ |
.map(|s| {
|
|
1186 |
+ |
let url = if s.starts_with('/') || s.contains("://") {
|
|
1187 |
+ |
s.clone()
|
|
1188 |
+ |
} else {
|
|
1189 |
+ |
format!("{base}/{s}")
|
|
1190 |
+ |
};
|
|
1191 |
+ |
match font_format(s) {
|
|
1192 |
+ |
Some(fmt) => format!("url(\"{url}\") format(\"{fmt}\")"),
|
|
1193 |
+ |
None => format!("url(\"{url}\")"),
|
|
1194 |
+ |
}
|
|
1195 |
+ |
})
|
|
1196 |
+ |
.collect::<Vec<_>>()
|
|
1197 |
+ |
.join(",\n ");
|
|
1198 |
+ |
|
|
1199 |
+ |
let mut out = format!(
|
|
1200 |
+ |
"@font-face {{\n font-family: \"{}\";\n src: {src};\n",
|
|
1201 |
+ |
self.family
|
|
1202 |
+ |
);
|
|
1203 |
+ |
if let Some(w) = &self.weight {
|
|
1204 |
+ |
let _ = writeln!(out, " font-weight: {w};");
|
|
1205 |
+ |
}
|
|
1206 |
+ |
if let Some(s) = &self.style {
|
|
1207 |
+ |
let _ = writeln!(out, " font-style: {s};");
|
|
1208 |
+ |
}
|
|
1209 |
+ |
out.push_str(" font-display: swap;\n}\n\n");
|
|
1210 |
+ |
out
|
|
1211 |
+ |
}
|
|
1212 |
+ |
}
|
|
1213 |
+ |
|
|
1214 |
+ |
/// The `format()` hint for a source, by extension. `None` when unrecognised.
|
|
1215 |
+ |
fn font_format(source: &str) -> Option<&'static str> {
|
|
1216 |
+ |
match source.rsplit('.').next()?.to_ascii_lowercase().as_str() {
|
|
1217 |
+ |
"woff2" => Some("woff2"),
|
|
1218 |
+ |
"woff" => Some("woff"),
|
|
1219 |
+ |
"ttf" => Some("truetype"),
|
|
1220 |
+ |
"otf" => Some("opentype"),
|
|
1221 |
+ |
_ => None,
|
|
1222 |
+ |
}
|
|
1223 |
+ |
}
|
|
1224 |
+ |
|
|
1225 |
+ |
/// One product's answer for one slot: the stack, and any faces it ships.
|
|
1226 |
+ |
#[derive(Debug, Clone)]
|
|
1227 |
+ |
pub struct FontOverride {
|
|
1228 |
+ |
slot: FontSlot,
|
|
1229 |
+ |
stack: String,
|
|
1230 |
+ |
faces: Vec<FontFace>,
|
|
1231 |
+ |
}
|
|
1232 |
+ |
|
|
1233 |
+ |
impl FontOverride {
|
|
1234 |
+ |
/// Point `slot` at `stack`.
|
|
1235 |
+ |
///
|
|
1236 |
+ |
/// `stack` is the CSS value the token takes, written the way the house
|
|
1237 |
+ |
/// stacks are: the family, then one hop to a system generic. Layer 2 is
|
|
1238 |
+ |
/// still one hop and no further — an override is a different answer to the
|
|
1239 |
+ |
/// slot, not a licence to write the fallback chain the standard deleted.
|
|
1240 |
+ |
pub fn new(slot: FontSlot, stack: impl Into<String>) -> Self {
|
|
1241 |
+ |
Self {
|
|
1242 |
+ |
slot,
|
|
1243 |
+ |
stack: stack.into(),
|
|
1244 |
+ |
faces: Vec::new(),
|
|
1245 |
+ |
}
|
|
1246 |
+ |
}
|
|
1247 |
+ |
|
|
1248 |
+ |
/// Ship a face with the override.
|
|
1249 |
+ |
#[must_use]
|
|
1250 |
+ |
pub fn with_face(mut self, face: FontFace) -> Self {
|
|
1251 |
+ |
self.faces.push(face);
|
|
1252 |
+ |
self
|
|
1253 |
+ |
}
|
|
1254 |
+ |
|
|
1255 |
+ |
/// The slot this answers.
|
|
1256 |
+ |
pub fn slot(&self) -> FontSlot {
|
|
1257 |
+ |
self.slot
|
|
1258 |
+ |
}
|
|
1259 |
+ |
|
|
1260 |
+ |
/// The stack it resolves to.
|
|
1261 |
+ |
pub fn stack(&self) -> &str {
|
|
1262 |
+ |
&self.stack
|
|
1263 |
+ |
}
|
|
1264 |
+ |
}
|
|
1265 |
+ |
|
|
1266 |
+ |
/// The whole typography layer for one product: the house defaults, plus
|
|
1267 |
+ |
/// whatever it overrides.
|
|
1268 |
+ |
///
|
|
1269 |
+ |
/// This is what a build script composes and what
|
|
1270 |
+ |
/// `makeover_build::typography_css_from` writes. [`typography_css_vars`] and
|
|
1271 |
+ |
/// [`font_face_css`] are the no-override case of it and stay for callers that
|
|
1272 |
+ |
/// have nothing to declare.
|
|
1273 |
+ |
#[derive(Debug, Clone)]
|
|
1274 |
+ |
pub struct Typography {
|
|
1275 |
+ |
base_url: String,
|
|
1276 |
+ |
overrides: Vec<FontOverride>,
|
|
1277 |
+ |
}
|
|
1278 |
+ |
|
|
1279 |
+ |
impl Typography {
|
|
1280 |
+ |
/// The house layer alone, fetching faces from `base_url` — the directory
|
|
1281 |
+ |
/// the consumer serves fonts from, with or without a trailing slash.
|
|
1282 |
+ |
pub fn house(base_url: impl Into<String>) -> Self {
|
|
1283 |
+ |
Self {
|
|
1284 |
+ |
base_url: base_url.into(),
|
|
1285 |
+ |
overrides: Vec::new(),
|
|
1286 |
+ |
}
|
|
1287 |
+ |
}
|
|
1288 |
+ |
|
|
1289 |
+ |
/// Add one product override.
|
|
1290 |
+ |
///
|
|
1291 |
+ |
/// # Panics
|
|
1292 |
+ |
///
|
|
1293 |
+ |
/// If the slot is already overridden. One declaration per product per
|
|
1294 |
+ |
/// slot: a second is not a merge to resolve, it is two answers to a
|
|
1295 |
+ |
/// question that has one, and the vocabulary is what wants fixing.
|
|
1296 |
+ |
#[must_use]
|
|
1297 |
+ |
pub fn with_override(mut self, ov: FontOverride) -> Self {
|
|
1298 |
+ |
assert!(
|
|
1299 |
+ |
!self.overrides.iter().any(|o| o.slot == ov.slot),
|
|
1300 |
+ |
"{} is overridden twice; one declaration per product per slot",
|
|
1301 |
+ |
ov.slot.token()
|
|
1302 |
+ |
);
|
|
1303 |
+ |
self.overrides.push(ov);
|
|
1304 |
+ |
self
|
|
1305 |
+ |
}
|
|
1306 |
+ |
|
|
1307 |
+ |
/// What `slot` resolves to under this layer, or `None` for a brand slot
|
|
1308 |
+ |
/// nobody overrode.
|
|
1309 |
+ |
///
|
|
1310 |
+ |
/// The resolution, for a renderer that has a face to choose rather than a
|
|
1311 |
+ |
/// stylesheet to emit.
|
|
1312 |
+ |
pub fn resolve(&self, slot: FontSlot) -> Option<&str> {
|
|
1313 |
+ |
self.overrides
|
|
1314 |
+ |
.iter()
|
|
1315 |
+ |
.find(|o| o.slot == slot)
|
|
1316 |
+ |
.map(|o| o.stack.as_str())
|
|
1317 |
+ |
.or_else(|| slot.house_default())
|
|
1318 |
+ |
}
|
|
1319 |
+ |
|
|
1320 |
+ |
/// The `@font-face` rules: the two house faces, then each override's.
|
|
1321 |
+ |
pub fn font_face_css(&self) -> String {
|
|
1322 |
+ |
let base = self.base_url.trim_end_matches('/');
|
|
1323 |
+ |
let mut out = font_face_css(base);
|
|
1324 |
+ |
for ov in &self.overrides {
|
|
1325 |
+ |
for face in &ov.faces {
|
|
1326 |
+ |
out.push_str(&face.css(base));
|
|
1327 |
+ |
}
|
|
1328 |
+ |
}
|
|
1329 |
+ |
out
|
|
1330 |
+ |
}
|
|
1331 |
+ |
|
|
1332 |
+ |
/// The resolved tokens as CSS declarations, no selector.
|
|
1333 |
+ |
pub fn css_declarations(&self) -> String {
|
|
1334 |
+ |
use std::fmt::Write as _;
|
|
1335 |
+ |
|
|
1336 |
+ |
let mut out = String::new();
|
|
1337 |
+ |
for slot in FontSlot::ALL {
|
|
1338 |
+ |
if let Some(stack) = self.resolve(slot) {
|
|
1339 |
+ |
let _ = writeln!(out, " {}: {stack};", slot.token());
|
|
1340 |
+ |
}
|
|
1341 |
+ |
}
|
|
1342 |
+ |
out
|
|
1343 |
+ |
}
|
|
1344 |
+ |
|
|
1345 |
+ |
/// The resolved tokens as a `:root { … }` block.
|
|
1346 |
+ |
pub fn css_vars(&self) -> String {
|
|
1347 |
+ |
format!(":root {{\n{}}}\n", self.css_declarations())
|
|
1348 |
+ |
}
|
|
1349 |
+ |
|
|
1350 |
+ |
/// Faces then tokens, in the order a stylesheet wants them.
|
|
1351 |
+ |
pub fn css(&self) -> String {
|
|
1352 |
+ |
format!("{}{}", self.font_face_css(), self.css_vars())
|
|
1353 |
+ |
}
|
|
1354 |
+ |
}
|
|
1355 |
+ |
|
| 1047 |
1356 |
|
// ============================================================================
|
| 1048 |
1357 |
|
// Loading / parsing
|
| 1049 |
1358 |
|
// ============================================================================
|
| 2705 |
3014 |
|
assert!(font_face_css("fonts").contains("url(\"fonts/QuasiMono.woff2\")"));
|
| 2706 |
3015 |
|
}
|
| 2707 |
3016 |
|
|
|
3017 |
+ |
// ---- typography, layer 0 ----
|
|
3018 |
+ |
|
|
3019 |
+ |
/// The live case: MNW's Young Serif, which reached the page through a
|
|
3020 |
+ |
/// hand-maintained `@font-face` and a `--font-heading` nothing else knew
|
|
3021 |
+ |
/// about.
|
|
3022 |
+ |
fn young_serif() -> FontOverride {
|
|
3023 |
+ |
FontOverride::new(FontSlot::Display, "\"Young Serif\", serif")
|
|
3024 |
+ |
.with_face(FontFace::new("Young Serif", ["ysrf.woff2", "ysrf.ttf"]))
|
|
3025 |
+ |
}
|
|
3026 |
+ |
|
|
3027 |
+ |
#[test]
|
|
3028 |
+ |
fn the_house_layer_alone_is_exactly_what_the_free_functions_emit() {
|
|
3029 |
+ |
let t = Typography::house("/static/fonts");
|
|
3030 |
+ |
assert_eq!(t.font_face_css(), font_face_css("/static/fonts"));
|
|
3031 |
+ |
assert_eq!(t.css_vars(), typography_css_vars());
|
|
3032 |
+ |
}
|
|
3033 |
+ |
|
|
3034 |
+ |
#[test]
|
|
3035 |
+ |
fn an_unoverridden_display_slot_defines_no_token_at_all() {
|
|
3036 |
+ |
// Not "defined empty": undefined, so the consumer's own fallback in
|
|
3037 |
+ |
// `var(--font-display, …)` renders. The MNW embeds depend on it.
|
|
3038 |
+ |
let t = Typography::house("fonts");
|
|
3039 |
+ |
assert!(!t.css_vars().contains("--font-display"));
|
|
3040 |
+ |
assert_eq!(t.resolve(FontSlot::Display), None);
|
|
3041 |
+ |
assert_eq!(t.css_vars().matches("--font-").count(), 2);
|
|
3042 |
+ |
}
|
|
3043 |
+ |
|
|
3044 |
+ |
#[test]
|
|
3045 |
+ |
fn an_override_adds_its_token_and_its_face_without_touching_the_house_two() {
|
|
3046 |
+ |
let t = Typography::house("/static/fonts").with_override(young_serif());
|
|
3047 |
+ |
|
|
3048 |
+ |
assert!(
|
|
3049 |
+ |
t.css_vars()
|
|
3050 |
+ |
.contains(" --font-display: \"Young Serif\", serif;\n")
|
|
3051 |
+ |
);
|
|
3052 |
+ |
assert!(
|
|
3053 |
+ |
t.css_vars()
|
|
3054 |
+ |
.contains(" --font-mono: \"Quasi Mono\", monospace;\n")
|
|
3055 |
+ |
);
|
|
3056 |
+ |
assert!(
|
|
3057 |
+ |
t.css_vars()
|
|
3058 |
+ |
.contains(" --font-sans: \"Quasi Body\", sans-serif;\n")
|
|
3059 |
+ |
);
|
|
3060 |
+ |
assert_eq!(t.resolve(FontSlot::Display), Some("\"Young Serif\", serif"));
|
|
3061 |
+ |
|
|
3062 |
+ |
let faces = t.font_face_css();
|
|
3063 |
+ |
assert_eq!(faces.matches("@font-face").count(), 3);
|
|
3064 |
+ |
assert!(faces.contains("font-family: \"Young Serif\";"));
|
|
3065 |
+ |
assert!(faces.contains("url(\"/static/fonts/ysrf.woff2\") format(\"woff2\")"));
|
|
3066 |
+ |
assert!(faces.contains("url(\"/static/fonts/ysrf.ttf\") format(\"truetype\")"));
|
|
3067 |
+ |
|
|
3068 |
+ |
// The house faces still come first, so a product face never shadows a
|
|
3069 |
+ |
// slot it did not claim.
|
|
3070 |
+ |
assert!(faces.find("Quasi Mono").unwrap() < faces.find("Young Serif").unwrap());
|
|
3071 |
+ |
}
|
|
3072 |
+ |
|
|
3073 |
+ |
#[test]
|
|
3074 |
+ |
fn overriding_mono_or_sans_replaces_the_house_stack_rather_than_adding_to_it() {
|
|
3075 |
+ |
// Nobody wants this today. A layer that only permits overriding the
|
|
3076 |
+ |
// slot nobody describes is the exemption restated, not a layer.
|
|
3077 |
+ |
let t = Typography::house("fonts").with_override(FontOverride::new(
|
|
3078 |
+ |
FontSlot::Mono,
|
|
3079 |
+ |
"\"Departure Mono\", monospace",
|
|
3080 |
+ |
));
|
|
3081 |
+ |
|
|
3082 |
+ |
assert!(
|
|
3083 |
+ |
t.css_vars()
|
|
3084 |
+ |
.contains(" --font-mono: \"Departure Mono\", monospace;\n")
|
|
3085 |
+ |
);
|
|
3086 |
+ |
assert!(!t.css_vars().contains("Quasi Mono"));
|
|
3087 |
+ |
assert_eq!(t.css_vars().matches("--font-").count(), 2);
|
|
3088 |
+ |
}
|
|
3089 |
+ |
|
|
3090 |
+ |
#[test]
|
|
3091 |
+ |
#[should_panic(expected = "--font-display is overridden twice")]
|
|
3092 |
+ |
fn a_second_override_of_one_slot_is_a_vocabulary_bug_and_says_so() {
|
|
3093 |
+ |
let _ = Typography::house("fonts")
|
|
3094 |
+ |
.with_override(young_serif())
|
|
3095 |
+ |
.with_override(FontOverride::new(FontSlot::Display, "\"Reglo\", serif"));
|
|
3096 |
+ |
}
|
|
3097 |
+ |
|
|
3098 |
+ |
#[test]
|
|
3099 |
+ |
fn an_absolute_source_is_taken_as_written_and_a_relative_one_joins_the_base() {
|
|
3100 |
+ |
let t = Typography::house("/static/fonts").with_override(
|
|
3101 |
+ |
FontOverride::new(FontSlot::Display, "\"Reglo\", serif").with_face(
|
|
3102 |
+ |
FontFace::new(
|
|
3103 |
+ |
"Reglo",
|
|
3104 |
+ |
["Reglo-Bold.woff2", "https://cdn.example/reglo.woff2"],
|
|
3105 |
+ |
)
|
|
3106 |
+ |
.weight("700"),
|
|
3107 |
+ |
),
|
|
3108 |
+ |
);
|
|
3109 |
+ |
let faces = t.font_face_css();
|
|
3110 |
+ |
assert!(faces.contains("url(\"/static/fonts/Reglo-Bold.woff2\")"));
|
|
3111 |
+ |
assert!(faces.contains("url(\"https://cdn.example/reglo.woff2\")"));
|
|
3112 |
+ |
assert!(faces.contains(" font-weight: 700;\n"));
|
|
3113 |
+ |
}
|
|
3114 |
+ |
|
|
3115 |
+ |
#[test]
|
|
3116 |
+ |
fn an_unrecognised_extension_gets_no_format_hint_rather_than_a_guessed_one() {
|
|
3117 |
+ |
let t = Typography::house("fonts").with_override(
|
|
3118 |
+ |
FontOverride::new(FontSlot::Display, "\"Odd\", serif")
|
|
3119 |
+ |
.with_face(FontFace::new("Odd", ["odd.eot"])),
|
|
3120 |
+ |
);
|
|
3121 |
+ |
assert!(t.font_face_css().contains("url(\"fonts/odd.eot\");"));
|
|
3122 |
+ |
assert!(!t.font_face_css().contains("format(\"eot\")"));
|
|
3123 |
+ |
}
|
|
3124 |
+ |
|
|
3125 |
+ |
#[test]
|
|
3126 |
+ |
fn css_puts_the_faces_before_the_tokens_that_name_them() {
|
|
3127 |
+ |
let t = Typography::house("fonts").with_override(young_serif());
|
|
3128 |
+ |
let css = t.css();
|
|
3129 |
+ |
assert!(css.starts_with("@font-face"));
|
|
3130 |
+ |
assert!(css.find("@font-face").unwrap() < css.find(":root").unwrap());
|
|
3131 |
+ |
}
|
|
3132 |
+ |
|
| 2708 |
3133 |
|
// ---- loading / fs ----
|
| 2709 |
3134 |
|
|
| 2710 |
3135 |
|
#[test]
|