1use std::collections::{HashMap, HashSet};
13use std::ops::RangeInclusive;
14
15use anyhow::{anyhow, Result};
16use read_fonts::types::Tag;
17use regex::Regex;
18
19use crate::search::TypgFontFaceMeta;
20use crate::tags::tag4;
21
22#[derive(Debug, Clone, Default)]
32pub struct Query {
33 axes: Vec<Tag>,
36
37 features: Vec<Tag>,
40
41 scripts: Vec<Tag>,
44
45 tables: Vec<Tag>,
47
48 name_patterns: Vec<Regex>,
52
53 codepoints: Vec<char>,
56
57 variable_only: bool,
60
61 weight_range: Option<RangeInclusive<u16>>,
64
65 width_range: Option<RangeInclusive<u16>>,
68
69 family_class: Option<FamilyClassFilter>,
73
74 creator_patterns: Vec<Regex>,
79
80 license_patterns: Vec<Regex>,
84}
85
86impl Query {
87 pub fn new() -> Self {
89 Self::default()
90 }
91
92 pub fn with_axes(mut self, axes: Vec<Tag>) -> Self {
95 self.axes = axes;
96 self
97 }
98
99 pub fn with_features(mut self, features: Vec<Tag>) -> Self {
102 self.features = features;
103 self
104 }
105
106 pub fn with_scripts(mut self, scripts: Vec<Tag>) -> Self {
109 self.scripts = scripts;
110 self
111 }
112
113 pub fn with_tables(mut self, tables: Vec<Tag>) -> Self {
116 self.tables = tables;
117 self
118 }
119
120 pub fn with_name_patterns(mut self, patterns: Vec<Regex>) -> Self {
123 self.name_patterns = patterns;
124 self
125 }
126
127 pub fn with_codepoints(mut self, cps: Vec<char>) -> Self {
130 self.codepoints = cps;
131 self
132 }
133
134 pub fn require_variable(mut self, yes: bool) -> Self {
136 self.variable_only = yes;
137 self
138 }
139
140 pub fn with_weight_range(mut self, range: Option<RangeInclusive<u16>>) -> Self {
142 self.weight_range = range;
143 self
144 }
145
146 pub fn with_width_range(mut self, range: Option<RangeInclusive<u16>>) -> Self {
148 self.width_range = range;
149 self
150 }
151
152 pub fn with_family_class(mut self, class: Option<FamilyClassFilter>) -> Self {
154 self.family_class = class;
155 self
156 }
157
158 pub fn with_creator_patterns(mut self, patterns: Vec<Regex>) -> Self {
160 self.creator_patterns = patterns;
161 self
162 }
163
164 pub fn with_license_patterns(mut self, patterns: Vec<Regex>) -> Self {
166 self.license_patterns = patterns;
167 self
168 }
169
170 pub fn axes(&self) -> &[Tag] {
172 &self.axes
173 }
174
175 pub fn features(&self) -> &[Tag] {
177 &self.features
178 }
179
180 pub fn scripts(&self) -> &[Tag] {
182 &self.scripts
183 }
184
185 pub fn tables(&self) -> &[Tag] {
187 &self.tables
188 }
189
190 pub fn name_patterns(&self) -> &[Regex] {
192 &self.name_patterns
193 }
194
195 pub fn codepoints(&self) -> &[char] {
197 &self.codepoints
198 }
199
200 pub fn requires_variable(&self) -> bool {
202 self.variable_only
203 }
204
205 pub fn weight_range(&self) -> Option<&RangeInclusive<u16>> {
207 self.weight_range.as_ref()
208 }
209
210 pub fn width_range(&self) -> Option<&RangeInclusive<u16>> {
212 self.width_range.as_ref()
213 }
214
215 pub fn family_class(&self) -> Option<&FamilyClassFilter> {
217 self.family_class.as_ref()
218 }
219
220 pub fn creator_patterns(&self) -> &[Regex] {
222 &self.creator_patterns
223 }
224
225 pub fn license_patterns(&self) -> &[Regex] {
227 &self.license_patterns
228 }
229
230 pub fn matches(&self, meta: &TypgFontFaceMeta) -> bool {
240 if self.variable_only && !meta.is_variable {
241 return false;
242 }
243
244 if !contains_all_tags(&meta.axis_tags, &self.axes) {
245 return false;
246 }
247
248 if !contains_all_tags(&meta.feature_tags, &self.features) {
249 return false;
250 }
251
252 if !contains_all_tags(&meta.script_tags, &self.scripts) {
253 return false;
254 }
255
256 if !contains_all_tags(&meta.table_tags, &self.tables) {
257 return false;
258 }
259
260 if let Some(range) = &self.weight_range {
261 match meta.weight_class {
262 Some(weight) if range.contains(&weight) => {}
263 _ => return false,
264 }
265 }
266
267 if let Some(range) = &self.width_range {
268 match meta.width_class {
269 Some(width) if range.contains(&width) => {}
270 _ => return false,
271 }
272 }
273
274 if let Some(filter) = &self.family_class {
275 match meta.family_class {
276 Some((class, subclass)) => {
277 if class != filter.major {
278 return false;
279 }
280 if let Some(expected_subclass) = filter.subclass {
281 if subclass != expected_subclass {
282 return false;
283 }
284 }
285 }
286 None => return false,
287 }
288 }
289
290 if !self.codepoints.is_empty() {
291 let available: HashSet<char> = meta.codepoints.iter().copied().collect();
292 if !self.codepoints.iter().all(|cp| available.contains(cp)) {
293 return false;
294 }
295 }
296
297 if !self.name_patterns.is_empty() {
298 let matched = meta
299 .names
300 .iter()
301 .any(|name| self.name_patterns.iter().any(|re| re.is_match(name)));
302 if !matched {
303 return false;
304 }
305 }
306
307 if !self.creator_patterns.is_empty() {
308 let matched = meta
309 .creator_names
310 .iter()
311 .any(|name| self.creator_patterns.iter().any(|re| re.is_match(name)));
312 if !matched {
313 return false;
314 }
315 }
316
317 if !self.license_patterns.is_empty() {
318 let matched = meta
319 .license_names
320 .iter()
321 .any(|name| self.license_patterns.iter().any(|re| re.is_match(name)));
322 if !matched {
323 return false;
324 }
325 }
326
327 true
328 }
329}
330
331fn contains_all_tags(haystack: &[Tag], needles: &[Tag]) -> bool {
334 if needles.is_empty() {
335 return true;
336 }
337 let set: HashSet<Tag> = haystack.iter().copied().collect();
338 needles.iter().all(|tag| set.contains(tag))
339}
340
341pub fn parse_codepoint_list(input: &str) -> Result<Vec<char>> {
346 let mut result = Vec::new();
347 if input.trim().is_empty() {
348 return Ok(result);
349 }
350
351 for part in input.split(',') {
352 if part.contains('-') {
353 let pieces: Vec<&str> = part.split('-').collect();
354 if pieces.len() != 2 {
355 return Err(anyhow!("invalid range: {part}"));
356 }
357 let start = parse_codepoint(pieces[0])? as u32;
358 let end = parse_codepoint(pieces[1])? as u32;
359 let (lo, hi) = if start <= end {
360 (start, end)
361 } else {
362 (end, start)
363 };
364 for cp in lo..=hi {
365 if let Some(ch) = char::from_u32(cp) {
366 result.push(ch);
367 }
368 }
369 } else {
370 result.push(parse_codepoint(part)?);
371 }
372 }
373
374 Ok(result)
375}
376
377fn parse_codepoint(token: &str) -> Result<char> {
378 if token.chars().count() == 1 {
379 return Ok(token.chars().next().unwrap());
380 }
381
382 let trimmed = token.trim_start_matches("U+").trim_start_matches("u+");
383 let cp = u32::from_str_radix(trimmed, 16).map_err(|_| anyhow!("invalid codepoint: {token}"))?;
384 char::from_u32(cp).ok_or_else(|| anyhow!("invalid Unicode scalar: U+{cp:04X}"))
385}
386
387pub fn parse_tag_list(raw: &[String]) -> Result<Vec<Tag>> {
391 raw.iter().map(|s| tag4(s)).collect()
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
400pub struct FamilyClassFilter {
401 pub major: u8,
402 pub subclass: Option<u8>,
403}
404
405pub fn parse_family_class(input: &str) -> Result<FamilyClassFilter> {
410 let trimmed = input.trim();
411 if trimmed.is_empty() {
412 return Err(anyhow!("family class cannot be empty"));
413 }
414
415 let lower = trimmed.to_ascii_lowercase();
416 if let Some(major) = lookup_family_class_by_name(&lower) {
417 return Ok(FamilyClassFilter {
418 major,
419 subclass: None,
420 });
421 }
422
423 if let Some((major, subclass)) = parse_major_and_subclass(&lower) {
424 return Ok(FamilyClassFilter {
425 major,
426 subclass: Some(subclass),
427 });
428 }
429
430 let value = if let Some(stripped) = lower.strip_prefix("0x") {
431 u16::from_str_radix(stripped, 16)
432 .map_err(|_| anyhow!("invalid hex family class: {trimmed}"))?
433 } else {
434 lower
435 .parse::<u16>()
436 .map_err(|_| anyhow!("invalid family class: {trimmed}"))?
437 };
438
439 if value <= 0x00FF {
440 return Ok(FamilyClassFilter {
441 major: value as u8,
442 subclass: None,
443 });
444 }
445
446 let major = (value >> 8) as u8;
447 let subclass = (value & 0x00FF) as u8;
448
449 Ok(FamilyClassFilter {
450 major,
451 subclass: Some(subclass),
452 })
453}
454
455fn lookup_family_class_by_name(name: &str) -> Option<u8> {
456 let mut map: HashMap<&str, u8> = HashMap::new();
457 map.insert("none", 0);
458 map.insert("no-class", 0);
459 map.insert("uncategorized", 0);
460 map.insert("oldstyle", 1);
461 map.insert("old-style", 1);
462 map.insert("oldstyle-serif", 1);
463 map.insert("transitional", 2);
464 map.insert("modern", 3);
465 map.insert("clarendon", 4);
466 map.insert("slab", 5);
467 map.insert("slab-serif", 5);
468 map.insert("egyptian", 5);
469 map.insert("freeform", 7);
470 map.insert("freeform-serif", 7);
471 map.insert("sans", 8);
472 map.insert("sans-serif", 8);
473 map.insert("gothic", 8);
474 map.insert("ornamental", 9);
475 map.insert("decorative", 9);
476 map.insert("script", 10);
477 map.insert("symbolic", 12);
478 map.get(name).copied()
479}
480
481fn parse_major_and_subclass(raw: &str) -> Option<(u8, u8)> {
482 for sep in ['.', ':'] {
483 if let Some((major, sub)) = raw.split_once(sep) {
484 let major: u8 = major.parse().ok()?;
485 let subclass: u8 = sub.parse().ok()?;
486 return Some((major, subclass));
487 }
488 }
489 None
490}
491
492pub fn parse_u16_range(input: &str) -> Result<RangeInclusive<u16>> {
494 let trimmed = input.trim();
495 if trimmed.is_empty() {
496 return Err(anyhow!("range cannot be empty"));
497 }
498
499 if let Some((lo, hi)) = trimmed.split_once('-') {
500 let start: u16 = lo.trim().parse()?;
501 let end: u16 = hi.trim().parse()?;
502 let (min, max) = if start <= end {
503 (start, end)
504 } else {
505 (end, start)
506 };
507 Ok(min..=max)
508 } else {
509 let value: u16 = trimmed.parse()?;
510 Ok(value..=value)
511 }
512}