typg_core/discovery.rs
1/// The neighborhood explorer who knows where all the fonts are hiding
2///
3/// Think of us as the friendly neighborhood font scout who goes door-to-door
4/// finding every font that calls your filesystem home. We'll climb through
5/// directory trees, peek into folder corners, and come back with a complete
6/// census of all the typographic residents in your chosen neighborhoods.
7///
8/// Whether fonts are living openly in plain sight or hiding in nested
9/// subdirectories, we'll find them. We're even brave enough to follow
10/// symlinks if you give us permission - those mysterious pathways often
11/// lead to the most interesting font discoveries.
12///
13/// Made with adventurous spirit at FontLab https://www.fontlab.com/
14use std::path::{Path, PathBuf};
15
16use anyhow::{anyhow, Result};
17use walkdir::WalkDir;
18
19/// A business card for every font we meet on our explorations
20///
21/// When we find a font during our neighborhood adventures, we give it
22/// this simple calling card that tells you exactly where it lives. No
23/// frills, no fuss - just the perfect address for later visits.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct TypgFontSourceRef {
26 /// The exact street address where this font hangs out
27 pub path: PathBuf,
28}
29
30/// The explorer's contract - how we promise to find fonts for you
31///
32/// Any font discovery service must be brave enough to venture into
33/// the unknown and return with tales of all the fonts they met.
34/// Whether they're crawling filesystems, consulting databases, or
35/// reading cache indices, they all come back with the same treasure:
36/// a list of fonts waiting to be discovered.
37pub trait FontDiscovery {
38 /// Sets out on expedition and returns with all the fonts found
39 fn discover(&self) -> Result<Vec<TypgFontSourceRef>>;
40}
41
42/// The brave filesystem explorer who never says no to an adventure
43///
44/// We're the expedition leaders who'll climb any directory tree,
45/// cross any symlink bridge, and search every nook and cranny for
46/// typographic treasures. Give us a list of neighborhoods to explore
47/// and we'll come back with a complete census of every font that
48/// calls those places home.
49#[derive(Debug, Clone)]
50pub struct PathDiscovery {
51 /// The starting points for our font-finding expeditions
52 roots: Vec<PathBuf>,
53 /// Should we be brave enough to follow those mysterious symlink shortcuts?
54 follow_symlinks: bool,
55}
56
57impl PathDiscovery {
58 /// Assembles our expedition team and maps out our adventure route
59 ///
60 /// Give us your list of neighborhoods to explore and we'll prepare
61 /// our expedition kit. By default, we play it safe and stick to the
62 /// beaten path - no mysterious symlink shortcuts unless you say so.
63 pub fn new<I, P>(roots: I) -> Self
64 where
65 I: IntoIterator<Item = P>,
66 P: Into<PathBuf>,
67 {
68 let roots = roots.into_iter().map(Into::into).collect();
69 Self {
70 roots,
71 follow_symlinks: false,
72 }
73 }
74
75 /// Decides whether we're brave enough to follow mysterious shortcuts
76 ///
77 /// Symlinks are like teleportation portals in the filesystem - they
78 /// can lead to wondrous discoveries or endless loops. We'll follow them
79 /// if you're feeling adventurous, but we're happy to stay on solid ground
80 /// if you prefer the conservative approach.
81 pub fn follow_symlinks(mut self, follow: bool) -> Self {
82 self.follow_symlinks = follow;
83 self
84 }
85}
86
87impl FontDiscovery for PathDiscovery {
88 /// Sets out on our grand font-finding expedition through the filesystem jungle
89 ///
90 /// We'll visit every neighborhood on our map, climb directory trees with
91 /// the agility of a seasoned explorer, and carefully examine every file
92 /// we encounter. Only the true typographic treasures get added to our
93 /// collection - we're discerning explorers who know quality when we see it.
94 ///
95 /// Returns: A complete catalog of every font we discovered on our adventure.
96 fn discover(&self) -> Result<Vec<TypgFontSourceRef>> {
97 let mut found = Vec::new();
98
99 for root in &self.roots {
100 if !root.exists() {
101 return Err(anyhow!("root path does not exist: {}", root.display()));
102 }
103
104 for entry in WalkDir::new(root).follow_links(self.follow_symlinks) {
105 let entry = entry?;
106 if entry.file_type().is_file() && is_font(entry.path()) {
107 found.push(TypgFontSourceRef {
108 path: entry.path().to_path_buf(),
109 });
110 }
111 }
112 }
113
114 Ok(found)
115 }
116}
117
118/// The expert detective who can spot a font from just its file extension
119///
120/// We've seen thousands of fonts in our day, and we've learned to
121/// recognize them by their distinctive signatures. TTF, OTF, TTC, OTC -
122/// we know them all. Case doesn't matter to us - we're equal-opportunity
123/// font identifiers who believe every font deserves to be discovered.
124///
125/// Returns true if this extension belongs to a legitimate format.
126fn is_font(path: &Path) -> bool {
127 let ext = match path.extension().and_then(|e| e.to_str()) {
128 Some(ext) => ext.to_ascii_lowercase(),
129 None => return false,
130 };
131
132 matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc")
133}
134
135#[cfg(test)]
136mod tests {
137 use super::is_font;
138 use super::FontDiscovery;
139 use super::PathDiscovery;
140 use std::fs;
141 use tempfile::tempdir;
142
143 #[test]
144 fn recognises_font_extensions() {
145 assert!(is_font("/A/B/font.ttf".as_ref()));
146 assert!(is_font("/A/B/font.OTF".as_ref()));
147 assert!(!is_font("/A/B/font.txt".as_ref()));
148 assert!(!is_font("/A/B/font".as_ref()));
149 }
150
151 #[test]
152 fn discovers_nested_fonts() {
153 let tmp = tempdir().expect("tempdir");
154 let nested = tmp.path().join("a/b");
155 fs::create_dir_all(&nested).expect("mkdir");
156 let font_path = nested.join("sample.ttf");
157 fs::write(&font_path, b"").expect("touch font");
158
159 let discovery = PathDiscovery::new([tmp.path()]);
160 let fonts = discovery.discover().expect("discover");
161
162 assert!(fonts.iter().any(|f| f.path == font_path));
163 }
164
165 #[cfg(unix)]
166 #[test]
167 fn follows_symlinks_when_enabled() {
168 use std::os::unix::fs::symlink;
169
170 let tmp = tempdir().expect("tempdir");
171 let real_dir = tmp.path().join("real");
172 let link_dir = tmp.path().join("link");
173 fs::create_dir_all(&real_dir).expect("mkdir real");
174 let font_path = real_dir.join("linked.otf");
175 fs::write(&font_path, b"").expect("touch font");
176 symlink(&real_dir, &link_dir).expect("symlink");
177
178 let discovery = PathDiscovery::new([&link_dir]).follow_symlinks(true);
179 let fonts = discovery.discover().expect("discover");
180
181 assert!(fonts.iter().any(|f| f.path.ends_with("linked.otf")));
182 }
183}