Skip to main content

fleuron/session/
faces.rs

1//! The files a sheet's `@font-face` rules name, and the faces a
2//! session registers from them.
3
4use std::collections::HashMap;
5use std::hash::{DefaultHasher, Hash, Hasher};
6
7use crate::Warning;
8use crate::style::FontLoader;
9
10use super::{Session, Stale};
11
12/// Font files, by the url a `@font-face` names them by.
13///
14/// The faces registered from them come after every face the host
15/// registered directly, so dropping them leaves the host's ids where
16/// they were.
17pub(super) struct FontFiles {
18    files: HashMap<String, (u64, Vec<u8>)>,
19    /// How many faces the host registered directly.
20    base: usize,
21    /// What the sheets and the files hashed to when their faces were
22    /// last registered.
23    print: Option<u64>,
24    /// What registering them warned about.
25    warnings: Vec<Warning>,
26}
27
28impl FontFiles {
29    pub(super) fn over(base: usize) -> FontFiles {
30        FontFiles {
31            files: HashMap::new(),
32            base,
33            print: None,
34            warnings: Vec::new(),
35        }
36    }
37
38    /// Keeps one file, and says whether its bytes are new.
39    fn insert(&mut self, url: &str, bytes: Vec<u8>) -> bool {
40        let mut h = DefaultHasher::new();
41        bytes.hash(&mut h);
42        let hash = h.finish();
43        if self.files.get(url).is_some_and(|(known, _)| *known == hash) {
44            return false;
45        }
46        self.files.insert(url.to_string(), (hash, bytes));
47        true
48    }
49
50    fn hash(&self, url: &str) -> Option<u64> {
51        self.files.get(url).map(|(hash, _)| *hash)
52    }
53}
54
55impl FontLoader for FontFiles {
56    fn load(&self, url: &str) -> Option<Vec<u8>> {
57        self.files.get(url).map(|(_, bytes)| bytes.clone())
58    }
59}
60
61impl Session<'_> {
62    /// Keeps a font file under the url a `@font-face` names it by,
63    /// and registers whatever the sheet declares from it.
64    ///
65    /// A url no rule names is kept until one does, so the file and
66    /// the sheet can arrive in either order. The same bytes at the
67    /// same url again cost nothing.
68    pub fn add_font_file(&mut self, url: &str, bytes: Vec<u8>) -> Result<(), super::AddFontError> {
69        if self.registry.get_mut().is_none() {
70            return Err(super::AddFontError::Borrowed);
71        }
72        if self.fonts.insert(url, bytes) && self.load_faces() {
73            self.recompile();
74        }
75        Ok(())
76    }
77
78    /// Registers a face the host names by its file alone, under the
79    /// faces the sheet declares.
80    pub(super) fn register_font(
81        &mut self,
82        source: crate::fonts::FontSource,
83    ) -> Result<Vec<u16>, super::AddFontError> {
84        let registry = self
85            .registry
86            .get_mut()
87            .ok_or(super::AddFontError::Borrowed)?;
88        registry.truncate(self.fonts.base);
89        let added = registry.add(source);
90        self.fonts.base = registry.len();
91        self.fonts.print = None;
92        self.load_faces();
93        Ok(added?)
94    }
95
96    /// Registers the faces the sheets declare, from the files the
97    /// host handed over, and says whether they changed. Sheets that
98    /// declare the faces already registered register nothing.
99    pub(super) fn load_faces(&mut self) -> bool {
100        let (Some(sheets), Some(registry)) = (&mut self.sheets, self.registry.get_mut()) else {
101            return false;
102        };
103        let mut h = DefaultHasher::new();
104        sheets.hash_faces(&mut h, |url| self.fonts.hash(url));
105        let print = h.finish();
106        if self.fonts.print == Some(print) {
107            sheets.set_font_warnings(self.fonts.warnings.clone());
108            return false;
109        }
110        registry.truncate(self.fonts.base);
111        sheets.load_fonts(registry, &self.fonts);
112        self.fonts.warnings = sheets.font_warnings().to_vec();
113        self.fonts.print = Some(print);
114        // An id that stood before can name another file now, and a
115        // section's key names the id, so no cached line survives.
116        self.output = None;
117        self.lines.clear();
118        self.settled.clear();
119        self.stale = self.stale.max(Stale::Break);
120        true
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use std::collections::BTreeSet;
127
128    use crate::LayoutOutput;
129    use crate::fonts::{FaceAttributes, FontSource, bundled_registry};
130    use crate::pages::DrawItem;
131    use crate::session::testing::{FELL, book, prose, registry, section, sheets};
132    use crate::session::{AddFontError, Session};
133    use crate::style::{NoFonts, Source, Stylesheets};
134
135    const FELL_CAPS: &str = "@font-face { font-family: 'Fell Caps'; src: url(fell.ttf); \
136        font-weight: 700; font-style: italic }\n\
137        p { font-family: 'Fell Caps', serif; font-weight: 700; font-style: italic }";
138
139    fn session() -> Session<'static> {
140        let mut session = Session::owning(bundled_registry().unwrap());
141        session.set_content(book(vec![section("one.md", prose("alpha", 4))]));
142        session
143    }
144
145    /// Every face a text run on any page names.
146    fn used(output: &LayoutOutput) -> BTreeSet<u16> {
147        output
148            .pages
149            .iter()
150            .flat_map(|page| &page.items)
151            .filter_map(|item| match item {
152                DrawItem::Text { font_id, .. } => Some(*font_id),
153                _ => None,
154            })
155            .collect()
156    }
157
158    fn family(output: &LayoutOutput, family: &str) -> Vec<u16> {
159        (0..output.fonts.len() as u16)
160            .filter(|id| output.fonts[usize::from(*id)].family == family)
161            .collect()
162    }
163
164    /// A rule whose url has bytes registers one face, under the
165    /// family, the weight and the slope the rule declares.
166    #[test]
167    fn a_font_face_with_bytes_registers_what_the_rule_declares() {
168        let mut session = session();
169        session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
170        session.set_style(sheets(FELL_CAPS));
171        let output = session.preview();
172        let fell = family(output, "fell caps");
173        assert_eq!(fell.len(), 1, "{:?}", output.fonts);
174        assert_eq!(
175            output.fonts[usize::from(fell[0])].attributes,
176            FaceAttributes {
177                italic: true,
178                weight: 700
179            },
180        );
181        assert!(output.warnings.is_empty(), "{:?}", output.warnings);
182    }
183
184    /// Text that asks for the family is set in that face, and the
185    /// export writes from the same pages.
186    #[test]
187    fn text_that_asks_for_the_family_sets_in_it() {
188        let mut session = session();
189        session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
190        session.set_style(sheets(FELL_CAPS));
191        let output = session.preview();
192        let fell = family(output, "fell caps");
193        assert_eq!(used(output), fell.into_iter().collect());
194        assert!(session.export().is_ok());
195    }
196
197    /// A rule whose url has no bytes warns the way loading the same
198    /// sheet through a loader that resolves nothing warns.
199    #[test]
200    fn a_font_face_without_bytes_warns_as_a_loader_does() {
201        let mut session = session();
202        session.set_style(sheets(FELL_CAPS));
203        let output = session.preview();
204
205        let mut loaded = sheets(FELL_CAPS);
206        loaded.load_fonts(&mut bundled_registry().unwrap(), &NoFonts);
207        assert!(!loaded.warnings().is_empty());
208        for warning in loaded.warnings() {
209            assert!(output.warnings.contains(warning), "{warning:?} is missing");
210        }
211    }
212
213    /// Bytes that arrive after the sheet register the face, and the
214    /// text that fell back sets in it.
215    #[test]
216    fn a_face_whose_bytes_arrive_after_the_sheet_sets_in_it() {
217        let mut session = session();
218        session.set_style(sheets(FELL_CAPS));
219        let before = session.preview();
220        assert!(family(before, "fell caps").is_empty());
221        assert!(!before.warnings.is_empty());
222
223        session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
224        let after = session.preview();
225        let fell = family(after, "fell caps");
226        assert_eq!(used(after), fell.into_iter().collect());
227        assert!(after.warnings.is_empty(), "{:?}", after.warnings);
228    }
229
230    /// The same sheet set again registers no face again and breaks
231    /// no line again, and still warns about a rule with no bytes.
232    #[test]
233    fn the_same_sheet_again_registers_nothing_again() {
234        let css =
235            format!("{FELL_CAPS}\n@font-face {{ font-family: Absent; src: url(absent.ttf) }}");
236        let mut session = session();
237        session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
238        session.set_style(sheets(&css));
239        let warnings = session.preview().warnings.clone();
240        let faces = session.fonts().len();
241        let before = session.stages();
242
243        session.set_style(sheets(&css));
244        session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
245        let output = session.preview();
246        assert_eq!(output.warnings, warnings);
247        assert_eq!(session.fonts().len(), faces);
248        assert_eq!(session.stages().lines, before.lines);
249    }
250
251    /// A rule renamed replaces the face it registered, and a rule
252    /// removed takes its face with it.
253    #[test]
254    fn changing_or_removing_a_rule_changes_the_faces_used() {
255        let mut session = session();
256        session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
257        session.set_style(sheets(FELL_CAPS));
258        let bundled = session.preview().fonts.len() - 1;
259
260        session.set_style(sheets(&FELL_CAPS.replace("Fell Caps", "Oxford")));
261        let renamed = session.preview();
262        assert!(family(renamed, "fell caps").is_empty());
263        let oxford = family(renamed, "oxford");
264        assert_eq!(used(renamed), oxford.into_iter().collect());
265
266        session.set_style(sheets("p { font-family: 'Fell Caps', serif }"));
267        let removed = session.preview();
268        assert_eq!(removed.fonts.len(), bundled);
269        assert!(used(removed).iter().all(|id| usize::from(*id) < bundled));
270    }
271
272    /// A face the host registers by its file after the sheet's faces
273    /// takes the next id after the host's own, and the sheet's face
274    /// still sets.
275    #[test]
276    fn a_face_the_host_adds_goes_under_the_sheets_faces() {
277        let mut session = session();
278        let bundled = session.fonts().len() as u16;
279        session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
280        session.set_style(sheets(FELL_CAPS));
281        session.preview();
282
283        let ids = session
284            .add_font(FontSource::from_bytes(FELL.to_vec()).unwrap())
285            .unwrap();
286        assert_eq!(ids, vec![bundled]);
287        let output = session.preview();
288        let fell = family(output, "fell caps");
289        assert_eq!(fell, vec![bundled + 1]);
290        assert_eq!(used(output), fell.into_iter().collect());
291    }
292
293    /// A session that borrowed its registry has no faces of its own to
294    /// register from a file.
295    #[test]
296    fn a_borrowed_registry_refuses_a_font_file() {
297        let mut session = Session::new(registry());
298        session.set_style(Stylesheets::parse(&[Source::author("a.css", FELL_CAPS)]));
299        assert!(matches!(
300            session.add_font_file("fell.ttf", FELL.to_vec()),
301            Err(AddFontError::Borrowed)
302        ));
303    }
304}