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::scriptmap::ScriptRequirement;
20use crate::search::TypgFontFaceMeta;
21use crate::tags::tag4;
22
23#[derive(Debug, Clone, Default)]
33pub struct Query {
34 axes: Vec<Tag>,
37
38 features: Vec<Tag>,
41
42 script_reqs: Vec<ScriptRequirement>,
50
51 tables: Vec<Tag>,
53
54 name_patterns: Vec<Regex>,
58
59 codepoints: Vec<char>,
62
63 variable_only: bool,
66
67 weight_range: Option<RangeInclusive<u16>>,
70
71 width_range: Option<RangeInclusive<u16>>,
74
75 family_class: Option<FamilyClassFilter>,
79
80 creator_patterns: Vec<Regex>,
85
86 license_patterns: Vec<Regex>,
90}
91
92impl Query {
93 pub fn new() -> Self {
95 Self::default()
96 }
97
98 pub fn with_axes(mut self, axes: Vec<Tag>) -> Self {
101 self.axes = axes;
102 self
103 }
104
105 pub fn with_features(mut self, features: Vec<Tag>) -> Self {
108 self.features = features;
109 self
110 }
111
112 pub fn with_scripts(mut self, scripts: Vec<ScriptRequirement>) -> Self {
116 self.script_reqs = scripts;
117 self
118 }
119
120 pub fn with_tables(mut self, tables: Vec<Tag>) -> Self {
123 self.tables = tables;
124 self
125 }
126
127 pub fn with_name_patterns(mut self, patterns: Vec<Regex>) -> Self {
130 self.name_patterns = patterns;
131 self
132 }
133
134 pub fn with_codepoints(mut self, cps: Vec<char>) -> Self {
137 self.codepoints = cps;
138 self
139 }
140
141 pub fn require_variable(mut self, yes: bool) -> Self {
143 self.variable_only = yes;
144 self
145 }
146
147 pub fn with_weight_range(mut self, range: Option<RangeInclusive<u16>>) -> Self {
149 self.weight_range = range;
150 self
151 }
152
153 pub fn with_width_range(mut self, range: Option<RangeInclusive<u16>>) -> Self {
155 self.width_range = range;
156 self
157 }
158
159 pub fn with_family_class(mut self, class: Option<FamilyClassFilter>) -> Self {
161 self.family_class = class;
162 self
163 }
164
165 pub fn with_creator_patterns(mut self, patterns: Vec<Regex>) -> Self {
167 self.creator_patterns = patterns;
168 self
169 }
170
171 pub fn with_license_patterns(mut self, patterns: Vec<Regex>) -> Self {
173 self.license_patterns = patterns;
174 self
175 }
176
177 pub fn axes(&self) -> &[Tag] {
179 &self.axes
180 }
181
182 pub fn features(&self) -> &[Tag] {
184 &self.features
185 }
186
187 pub fn scripts(&self) -> &[ScriptRequirement] {
189 &self.script_reqs
190 }
191
192 pub fn tables(&self) -> &[Tag] {
194 &self.tables
195 }
196
197 pub fn name_patterns(&self) -> &[Regex] {
199 &self.name_patterns
200 }
201
202 pub fn codepoints(&self) -> &[char] {
204 &self.codepoints
205 }
206
207 pub fn requires_variable(&self) -> bool {
209 self.variable_only
210 }
211
212 pub fn weight_range(&self) -> Option<&RangeInclusive<u16>> {
214 self.weight_range.as_ref()
215 }
216
217 pub fn width_range(&self) -> Option<&RangeInclusive<u16>> {
219 self.width_range.as_ref()
220 }
221
222 pub fn family_class(&self) -> Option<&FamilyClassFilter> {
224 self.family_class.as_ref()
225 }
226
227 pub fn creator_patterns(&self) -> &[Regex] {
229 &self.creator_patterns
230 }
231
232 pub fn license_patterns(&self) -> &[Regex] {
234 &self.license_patterns
235 }
236
237 pub fn matches(&self, meta: &TypgFontFaceMeta) -> bool {
247 if self.variable_only && !meta.is_variable {
248 return false;
249 }
250
251 if !contains_all_tags(&meta.axis_tags, &self.axes) {
252 return false;
253 }
254
255 if !contains_all_tags(&meta.feature_tags, &self.features) {
256 return false;
257 }
258
259 if !self.script_reqs.is_empty() {
260 let font_scripts: HashSet<Tag> = meta.script_tags.iter().copied().collect();
261 let all_ot = self
265 .script_reqs
266 .iter()
267 .all(|req| req.ot_satisfied(&font_scripts));
268 let all_unicode = || {
269 self.script_reqs
270 .iter()
271 .all(|req| req.unicode_satisfied(meta.codepoints.iter().copied()))
272 };
273 if !(all_ot || all_unicode()) {
274 return false;
275 }
276 }
277
278 if !contains_all_tags(&meta.table_tags, &self.tables) {
279 return false;
280 }
281
282 if let Some(range) = &self.weight_range {
283 match meta.weight_class {
284 Some(weight) if range.contains(&weight) => {}
285 _ => return false,
286 }
287 }
288
289 if let Some(range) = &self.width_range {
290 match meta.width_class {
291 Some(width) if range.contains(&width) => {}
292 _ => return false,
293 }
294 }
295
296 if let Some(filter) = &self.family_class {
297 match meta.family_class {
298 Some((class, subclass)) => {
299 if class != filter.major {
300 return false;
301 }
302 if let Some(expected_subclass) = filter.subclass {
303 if subclass != expected_subclass {
304 return false;
305 }
306 }
307 }
308 None => return false,
309 }
310 }
311
312 if !self.codepoints.is_empty() {
313 let available: HashSet<char> = meta.codepoints.iter().copied().collect();
314 if !self.codepoints.iter().all(|cp| available.contains(cp)) {
315 return false;
316 }
317 }
318
319 if !self.name_patterns.is_empty() {
320 let matched = meta
321 .names
322 .iter()
323 .any(|name| self.name_patterns.iter().any(|re| re.is_match(name)));
324 if !matched {
325 return false;
326 }
327 }
328
329 if !self.creator_patterns.is_empty() {
330 let matched = meta
331 .creator_names
332 .iter()
333 .any(|name| self.creator_patterns.iter().any(|re| re.is_match(name)));
334 if !matched {
335 return false;
336 }
337 }
338
339 if !self.license_patterns.is_empty() {
340 let matched = meta
341 .license_names
342 .iter()
343 .any(|name| self.license_patterns.iter().any(|re| re.is_match(name)));
344 if !matched {
345 return false;
346 }
347 }
348
349 true
350 }
351}
352
353fn contains_all_tags(haystack: &[Tag], needles: &[Tag]) -> bool {
356 if needles.is_empty() {
357 return true;
358 }
359 let set: HashSet<Tag> = haystack.iter().copied().collect();
360 needles.iter().all(|tag| set.contains(tag))
361}
362
363pub fn parse_codepoint_list(input: &str) -> Result<Vec<char>> {
368 let mut result = Vec::new();
369 if input.trim().is_empty() {
370 return Ok(result);
371 }
372
373 for part in input.split(',') {
374 if part.contains('-') {
375 let pieces: Vec<&str> = part.split('-').collect();
376 if pieces.len() != 2 {
377 return Err(anyhow!("invalid range: {part}"));
378 }
379 let start = parse_codepoint(pieces[0])? as u32;
380 let end = parse_codepoint(pieces[1])? as u32;
381 let (lo, hi) = if start <= end {
382 (start, end)
383 } else {
384 (end, start)
385 };
386 for cp in lo..=hi {
387 if let Some(ch) = char::from_u32(cp) {
388 result.push(ch);
389 }
390 }
391 } else {
392 result.push(parse_codepoint(part)?);
393 }
394 }
395
396 Ok(result)
397}
398
399fn parse_codepoint(token: &str) -> Result<char> {
400 if token.chars().count() == 1 {
401 return Ok(token.chars().next().unwrap());
402 }
403
404 let trimmed = token.trim_start_matches("U+").trim_start_matches("u+");
405 let cp = u32::from_str_radix(trimmed, 16).map_err(|_| anyhow!("invalid codepoint: {token}"))?;
406 char::from_u32(cp).ok_or_else(|| anyhow!("invalid Unicode scalar: U+{cp:04X}"))
407}
408
409pub fn parse_tag_list(raw: &[String]) -> Result<Vec<Tag>> {
413 raw.iter().map(|s| tag4(s)).collect()
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub struct FamilyClassFilter {
423 pub major: u8,
424 pub subclass: Option<u8>,
425}
426
427pub fn parse_family_class(input: &str) -> Result<FamilyClassFilter> {
432 let trimmed = input.trim();
433 if trimmed.is_empty() {
434 return Err(anyhow!("family class cannot be empty"));
435 }
436
437 let lower = trimmed.to_ascii_lowercase();
438 if let Some(major) = lookup_family_class_by_name(&lower) {
439 return Ok(FamilyClassFilter {
440 major,
441 subclass: None,
442 });
443 }
444
445 if let Some((major, subclass)) = parse_major_and_subclass(&lower) {
446 return Ok(FamilyClassFilter {
447 major,
448 subclass: Some(subclass),
449 });
450 }
451
452 let value = if let Some(stripped) = lower.strip_prefix("0x") {
453 u16::from_str_radix(stripped, 16)
454 .map_err(|_| anyhow!("invalid hex family class: {trimmed}"))?
455 } else {
456 lower
457 .parse::<u16>()
458 .map_err(|_| anyhow!("invalid family class: {trimmed}"))?
459 };
460
461 if value <= 0x00FF {
462 return Ok(FamilyClassFilter {
463 major: value as u8,
464 subclass: None,
465 });
466 }
467
468 let major = (value >> 8) as u8;
469 let subclass = (value & 0x00FF) as u8;
470
471 Ok(FamilyClassFilter {
472 major,
473 subclass: Some(subclass),
474 })
475}
476
477fn lookup_family_class_by_name(name: &str) -> Option<u8> {
478 let mut map: HashMap<&str, u8> = HashMap::new();
479 map.insert("none", 0);
480 map.insert("no-class", 0);
481 map.insert("uncategorized", 0);
482 map.insert("oldstyle", 1);
483 map.insert("old-style", 1);
484 map.insert("oldstyle-serif", 1);
485 map.insert("transitional", 2);
486 map.insert("modern", 3);
487 map.insert("clarendon", 4);
488 map.insert("slab", 5);
489 map.insert("slab-serif", 5);
490 map.insert("egyptian", 5);
491 map.insert("freeform", 7);
492 map.insert("freeform-serif", 7);
493 map.insert("sans", 8);
494 map.insert("sans-serif", 8);
495 map.insert("gothic", 8);
496 map.insert("ornamental", 9);
497 map.insert("decorative", 9);
498 map.insert("script", 10);
499 map.insert("symbolic", 12);
500 map.get(name).copied()
501}
502
503fn parse_major_and_subclass(raw: &str) -> Option<(u8, u8)> {
504 for sep in ['.', ':'] {
505 if let Some((major, sub)) = raw.split_once(sep) {
506 let major: u8 = major.parse().ok()?;
507 let subclass: u8 = sub.parse().ok()?;
508 return Some((major, subclass));
509 }
510 }
511 None
512}
513
514pub fn parse_u16_range(input: &str) -> Result<RangeInclusive<u16>> {
516 let trimmed = input.trim();
517 if trimmed.is_empty() {
518 return Err(anyhow!("range cannot be empty"));
519 }
520
521 if let Some((lo, hi)) = trimmed.split_once('-') {
522 let start: u16 = lo.trim().parse()?;
523 let end: u16 = hi.trim().parse()?;
524 let (min, max) = if start <= end {
525 (start, end)
526 } else {
527 (end, start)
528 };
529 Ok(min..=max)
530 } else {
531 let value: u16 = trimmed.parse()?;
532 Ok(value..=value)
533 }
534}