1use std::path::Path;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::{PdfConvertError, Result};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum InputKind {
11 Pdf,
12 Doc,
13 Docx,
14 Ppt,
15 Pptx,
16 Html,
17 Asciidoc,
18 Markdown,
19 Csv,
20 Xlsx,
21 Xls,
22 Odt,
23 Ods,
24 Odp,
25 Epub,
26 Email,
27 Image,
28 XmlUspto,
29 XmlJats,
30 XmlXbrl,
31 XmlDoclang,
32 MetsGbs,
33 JsonDocling,
34 Dclx,
35 Audio,
36 Video,
37 Vtt,
38 Boxnote,
39 Latex,
40 Text,
41}
42
43#[derive(Debug, Clone, Copy)]
44struct InputKindSpec {
45 kind: InputKind,
46 extensions: &'static [&'static str],
47 media_types: &'static [&'static str],
48 default_extension: &'static str,
49 default_media_type: &'static str,
50 from_formats_value: &'static str,
51 parse_aliases: &'static [&'static str],
52 reading_label: &'static str,
53 auto_detect: bool,
54 generic_convert_options: bool,
55 supports_vlm: bool,
56}
57
58const INPUT_KIND_SPECS: &[InputKindSpec] = &[
59 InputKindSpec {
60 kind: InputKind::Pdf,
61 extensions: &["pdf"],
62 media_types: &["application/pdf"],
63 default_extension: "pdf",
64 default_media_type: "application/pdf",
65 from_formats_value: "pdf",
66 parse_aliases: &["pdf"],
67 reading_label: "Reading PDF...",
68 auto_detect: true,
69 generic_convert_options: false,
70 supports_vlm: true,
71 },
72 InputKindSpec {
73 kind: InputKind::Doc,
74 extensions: &["doc"],
75 media_types: &["application/msword"],
76 default_extension: "doc",
77 default_media_type: "application/msword",
78 from_formats_value: "doc",
79 parse_aliases: &["doc"],
80 reading_label: "Reading DOC...",
81 auto_detect: true,
82 generic_convert_options: true,
83 supports_vlm: true,
84 },
85 InputKindSpec {
86 kind: InputKind::Docx,
87 extensions: &["docx"],
88 media_types: &["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
89 default_extension: "docx",
90 default_media_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
91 from_formats_value: "docx",
92 parse_aliases: &["docx"],
93 reading_label: "Reading DOCX...",
94 auto_detect: true,
95 generic_convert_options: true,
96 supports_vlm: true,
97 },
98 InputKindSpec {
99 kind: InputKind::Pptx,
100 extensions: &["pptx"],
101 media_types: &["application/vnd.openxmlformats-officedocument.presentationml.presentation"],
102 default_extension: "pptx",
103 default_media_type: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
104 from_formats_value: "pptx",
105 parse_aliases: &["pptx"],
106 reading_label: "Reading PPTX...",
107 auto_detect: true,
108 generic_convert_options: true,
109 supports_vlm: false,
110 },
111 InputKindSpec {
112 kind: InputKind::Ppt,
113 extensions: &["ppt"],
114 media_types: &["application/vnd.ms-powerpoint"],
115 default_extension: "ppt",
116 default_media_type: "application/vnd.ms-powerpoint",
117 from_formats_value: "ppt",
118 parse_aliases: &["ppt"],
119 reading_label: "Reading PPT...",
120 auto_detect: true,
121 generic_convert_options: true,
122 supports_vlm: false,
123 },
124 InputKindSpec {
125 kind: InputKind::Html,
126 extensions: &["html", "htm", "xhtml"],
127 media_types: &["text/html", "application/xhtml+xml"],
128 default_extension: "html",
129 default_media_type: "text/html",
130 from_formats_value: "html",
131 parse_aliases: &["html"],
132 reading_label: "Reading HTML...",
133 auto_detect: true,
134 generic_convert_options: true,
135 supports_vlm: false,
136 },
137 InputKindSpec {
138 kind: InputKind::Asciidoc,
139 extensions: &["adoc", "asciidoc", "asc"],
140 media_types: &["text/asciidoc", "text/x-asciidoc"],
141 default_extension: "adoc",
142 default_media_type: "text/asciidoc",
143 from_formats_value: "asciidoc",
144 parse_aliases: &["asciidoc", "adoc"],
145 reading_label: "Reading AsciiDoc...",
146 auto_detect: true,
147 generic_convert_options: true,
148 supports_vlm: false,
149 },
150 InputKindSpec {
151 kind: InputKind::Markdown,
152 extensions: &["md", "markdown"],
153 media_types: &["text/markdown", "text/x-markdown"],
154 default_extension: "md",
155 default_media_type: "text/markdown",
156 from_formats_value: "md",
157 parse_aliases: &["markdown", "md"],
158 reading_label: "Reading Markdown...",
159 auto_detect: true,
160 generic_convert_options: true,
161 supports_vlm: true,
162 },
163 InputKindSpec {
164 kind: InputKind::Csv,
165 extensions: &["csv"],
166 media_types: &["text/csv", "application/csv"],
167 default_extension: "csv",
168 default_media_type: "text/csv",
169 from_formats_value: "csv",
170 parse_aliases: &["csv"],
171 reading_label: "Reading CSV...",
172 auto_detect: true,
173 generic_convert_options: true,
174 supports_vlm: false,
175 },
176 InputKindSpec {
177 kind: InputKind::Xlsx,
178 extensions: &["xlsx"],
179 media_types: &["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
180 default_extension: "xlsx",
181 default_media_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
182 from_formats_value: "xlsx",
183 parse_aliases: &["xlsx"],
184 reading_label: "Reading XLSX...",
185 auto_detect: true,
186 generic_convert_options: true,
187 supports_vlm: false,
188 },
189 InputKindSpec {
190 kind: InputKind::Xls,
191 extensions: &["xls"],
192 media_types: &["application/vnd.ms-excel"],
193 default_extension: "xls",
194 default_media_type: "application/vnd.ms-excel",
195 from_formats_value: "xls",
196 parse_aliases: &["xls"],
197 reading_label: "Reading XLS...",
198 auto_detect: true,
199 generic_convert_options: true,
200 supports_vlm: false,
201 },
202 InputKindSpec {
203 kind: InputKind::Odt,
204 extensions: &["odt"],
205 media_types: &["application/vnd.oasis.opendocument.text"],
206 default_extension: "odt",
207 default_media_type: "application/vnd.oasis.opendocument.text",
208 from_formats_value: "odt",
209 parse_aliases: &["odt"],
210 reading_label: "Reading ODT...",
211 auto_detect: true,
212 generic_convert_options: true,
213 supports_vlm: false,
214 },
215 InputKindSpec {
216 kind: InputKind::Ods,
217 extensions: &["ods"],
218 media_types: &["application/vnd.oasis.opendocument.spreadsheet"],
219 default_extension: "ods",
220 default_media_type: "application/vnd.oasis.opendocument.spreadsheet",
221 from_formats_value: "ods",
222 parse_aliases: &["ods"],
223 reading_label: "Reading ODS...",
224 auto_detect: true,
225 generic_convert_options: true,
226 supports_vlm: false,
227 },
228 InputKindSpec {
229 kind: InputKind::Odp,
230 extensions: &["odp"],
231 media_types: &["application/vnd.oasis.opendocument.presentation"],
232 default_extension: "odp",
233 default_media_type: "application/vnd.oasis.opendocument.presentation",
234 from_formats_value: "odp",
235 parse_aliases: &["odp"],
236 reading_label: "Reading ODP...",
237 auto_detect: true,
238 generic_convert_options: true,
239 supports_vlm: false,
240 },
241 InputKindSpec {
242 kind: InputKind::Epub,
243 extensions: &["epub"],
244 media_types: &["application/epub+zip"],
245 default_extension: "epub",
246 default_media_type: "application/epub+zip",
247 from_formats_value: "epub",
248 parse_aliases: &["epub"],
249 reading_label: "Reading EPUB...",
250 auto_detect: true,
251 generic_convert_options: true,
252 supports_vlm: false,
253 },
254 InputKindSpec {
255 kind: InputKind::Email,
256 extensions: &["eml", "msg"],
257 media_types: &["message/rfc822", "application/vnd.ms-outlook"],
258 default_extension: "eml",
259 default_media_type: "message/rfc822",
260 from_formats_value: "email",
261 parse_aliases: &["email", "eml", "msg"],
262 reading_label: "Reading email...",
263 auto_detect: true,
264 generic_convert_options: true,
265 supports_vlm: false,
266 },
267 InputKindSpec {
268 kind: InputKind::Image,
269 extensions: &[
270 "png", "jpg", "jpeg", "gif", "bmp", "tif", "tiff", "webp", "svg",
271 ],
272 media_types: &[
273 "image/png",
274 "image/jpeg",
275 "image/gif",
276 "image/bmp",
277 "image/tiff",
278 "image/webp",
279 "image/svg+xml",
280 ],
281 default_extension: "png",
282 default_media_type: "image/png",
283 from_formats_value: "image",
284 parse_aliases: &["image"],
285 reading_label: "Reading image...",
286 auto_detect: true,
287 generic_convert_options: true,
288 supports_vlm: true,
289 },
290 InputKindSpec {
291 kind: InputKind::XmlUspto,
292 extensions: &["xml"],
293 media_types: &["application/xml", "text/xml"],
294 default_extension: "xml",
295 default_media_type: "application/xml",
296 from_formats_value: "xml_uspto",
297 parse_aliases: &["xml_uspto"],
298 reading_label: "Reading XML USPTO...",
299 auto_detect: false,
300 generic_convert_options: true,
301 supports_vlm: false,
302 },
303 InputKindSpec {
304 kind: InputKind::XmlJats,
305 extensions: &["xml"],
306 media_types: &["application/xml", "text/xml"],
307 default_extension: "xml",
308 default_media_type: "application/xml",
309 from_formats_value: "xml_jats",
310 parse_aliases: &["xml_jats"],
311 reading_label: "Reading XML JATS...",
312 auto_detect: false,
313 generic_convert_options: true,
314 supports_vlm: false,
315 },
316 InputKindSpec {
317 kind: InputKind::XmlXbrl,
318 extensions: &["xml"],
319 media_types: &["application/xml", "text/xml"],
320 default_extension: "xml",
321 default_media_type: "application/xml",
322 from_formats_value: "xml_xbrl",
323 parse_aliases: &["xml_xbrl"],
324 reading_label: "Reading XML XBRL...",
325 auto_detect: false,
326 generic_convert_options: true,
327 supports_vlm: false,
328 },
329 InputKindSpec {
330 kind: InputKind::XmlDoclang,
331 extensions: &["xml"],
332 media_types: &["application/xml", "text/xml"],
333 default_extension: "xml",
334 default_media_type: "application/xml",
335 from_formats_value: "xml_doclang",
336 parse_aliases: &["xml_doclang"],
337 reading_label: "Reading XML Docling...",
338 auto_detect: false,
339 generic_convert_options: true,
340 supports_vlm: false,
341 },
342 InputKindSpec {
343 kind: InputKind::MetsGbs,
344 extensions: &["xml"],
345 media_types: &["application/xml", "text/xml"],
346 default_extension: "xml",
347 default_media_type: "application/xml",
348 from_formats_value: "mets_gbs",
349 parse_aliases: &["mets_gbs"],
350 reading_label: "Reading METS GBS...",
351 auto_detect: false,
352 generic_convert_options: true,
353 supports_vlm: false,
354 },
355 InputKindSpec {
356 kind: InputKind::JsonDocling,
357 extensions: &["json"],
358 media_types: &["application/json", "text/json"],
359 default_extension: "json",
360 default_media_type: "application/json",
361 from_formats_value: "json_docling",
362 parse_aliases: &["json_docling"],
363 reading_label: "Reading JSON Docling...",
364 auto_detect: false,
365 generic_convert_options: true,
366 supports_vlm: false,
367 },
368 InputKindSpec {
369 kind: InputKind::Latex,
370 extensions: &["tex"],
371 media_types: &[
372 "application/x-tex",
373 "application/x-latex",
374 "text/x-tex",
375 "text/x-latex",
376 ],
377 default_extension: "tex",
378 default_media_type: "application/x-tex",
379 from_formats_value: "latex",
380 parse_aliases: &["latex", "tex"],
381 reading_label: "Reading LaTeX...",
382 auto_detect: true,
383 generic_convert_options: true,
384 supports_vlm: false,
385 },
386 InputKindSpec {
387 kind: InputKind::Dclx,
388 extensions: &["dclx"],
389 media_types: &["application/zip", "application/vnd.docling.dclx"],
390 default_extension: "dclx",
391 default_media_type: "application/vnd.docling.dclx",
392 from_formats_value: "dclx",
393 parse_aliases: &["dclx"],
394 reading_label: "Reading DCLX...",
395 auto_detect: true,
396 generic_convert_options: true,
397 supports_vlm: false,
398 },
399 InputKindSpec {
400 kind: InputKind::Audio,
401 extensions: &["wav", "mp3", "m4a", "flac", "ogg", "aac"],
402 media_types: &[
403 "audio/wav",
404 "audio/x-wav",
405 "audio/mpeg",
406 "audio/mp4",
407 "audio/flac",
408 "audio/ogg",
409 "audio/aac",
410 ],
411 default_extension: "wav",
412 default_media_type: "audio/wav",
413 from_formats_value: "audio",
414 parse_aliases: &["audio"],
415 reading_label: "Reading audio...",
416 auto_detect: true,
417 generic_convert_options: true,
418 supports_vlm: false,
419 },
420 InputKindSpec {
421 kind: InputKind::Video,
422 extensions: &["mp4", "mov", "avi", "mkv", "webm"],
423 media_types: &[
424 "video/mp4",
425 "video/quicktime",
426 "video/x-msvideo",
427 "video/x-matroska",
428 "video/webm",
429 ],
430 default_extension: "mp4",
431 default_media_type: "video/mp4",
432 from_formats_value: "video",
433 parse_aliases: &["video"],
434 reading_label: "Reading video...",
435 auto_detect: true,
436 generic_convert_options: true,
437 supports_vlm: false,
438 },
439 InputKindSpec {
440 kind: InputKind::Vtt,
441 extensions: &["vtt"],
442 media_types: &["text/vtt"],
443 default_extension: "vtt",
444 default_media_type: "text/vtt",
445 from_formats_value: "vtt",
446 parse_aliases: &["vtt"],
447 reading_label: "Reading VTT...",
448 auto_detect: true,
449 generic_convert_options: true,
450 supports_vlm: false,
451 },
452 InputKindSpec {
453 kind: InputKind::Boxnote,
454 extensions: &["boxnote"],
455 media_types: &["application/boxnote", "application/vnd.box.note"],
456 default_extension: "boxnote",
457 default_media_type: "application/boxnote",
458 from_formats_value: "boxnote",
459 parse_aliases: &["boxnote"],
460 reading_label: "Reading Box Note...",
461 auto_detect: true,
462 generic_convert_options: true,
463 supports_vlm: false,
464 },
465 InputKindSpec {
466 kind: InputKind::Text,
467 extensions: &["txt"],
468 media_types: &["text/plain"],
469 default_extension: "txt",
470 default_media_type: "text/plain",
471 from_formats_value: "text",
472 parse_aliases: &["text", "txt"],
473 reading_label: "Reading text file...",
474 auto_detect: true,
475 generic_convert_options: false,
476 supports_vlm: false,
477 },
478];
479
480impl InputKind {
481 pub fn from_path(path: &Path) -> Option<Self> {
482 let file_name = path.file_name().and_then(|name| name.to_str())?;
483 Self::from_filename_and_media_type(file_name, None)
484 }
485
486 pub fn from_filename_and_media_type(filename: &str, media_type: Option<&str>) -> Option<Self> {
487 let ext = normalized_extension(filename);
488 let media_type = normalized_media_type(media_type);
489
490 if Self::requires_explicit_override(filename, media_type.as_deref()) {
491 return None;
492 }
493
494 detect_auto_kind(ext.as_deref(), media_type.as_deref())
495 }
496
497 pub fn requires_explicit_override(filename: &str, media_type: Option<&str>) -> bool {
498 let ext = normalized_extension(filename);
499 let media_type = normalized_media_type(media_type);
500
501 matches!(ext.as_deref(), Some("xml") | Some("json"))
502 || matches!(
503 media_type.as_deref(),
504 Some("application/xml")
505 | Some("text/xml")
506 | Some("application/json")
507 | Some("text/json")
508 )
509 }
510
511 pub fn media_type(self) -> &'static str {
512 self.spec().default_media_type
513 }
514
515 pub fn canonical_media_type(self, filename: &str, media_type: Option<&str>) -> &'static str {
516 let ext = normalized_extension(filename);
517 let media_type = normalized_media_type(media_type);
518
519 match self {
520 Self::Html => match (ext.as_deref(), media_type.as_deref()) {
521 (Some("xhtml"), _) | (_, Some("application/xhtml+xml")) => "application/xhtml+xml",
522 _ => self.media_type(),
523 },
524 Self::Email => match (ext.as_deref(), media_type.as_deref()) {
525 (Some("msg"), _) | (_, Some("application/vnd.ms-outlook")) => {
526 "application/vnd.ms-outlook"
527 }
528 _ => self.media_type(),
529 },
530 Self::Image => image_media_type(ext.as_deref(), media_type.as_deref()),
531 Self::Audio => audio_media_type(ext.as_deref(), media_type.as_deref()),
532 Self::Video => video_media_type(ext.as_deref(), media_type.as_deref()),
533 _ => self.media_type(),
534 }
535 }
536
537 pub fn default_extension(self) -> &'static str {
538 self.spec().default_extension
539 }
540
541 pub fn default_extension_for_media_type(self, media_type: Option<&str>) -> &'static str {
542 let media_type = normalized_media_type(media_type);
543
544 match self {
545 Self::Html => match media_type.as_deref() {
546 Some("application/xhtml+xml") => "xhtml",
547 _ => self.default_extension(),
548 },
549 Self::Email => match media_type.as_deref() {
550 Some("application/vnd.ms-outlook") => "msg",
551 _ => self.default_extension(),
552 },
553 Self::Image => image_extension(media_type.as_deref()),
554 _ => self.default_extension(),
555 }
556 }
557
558 pub fn reading_label(self) -> &'static str {
559 self.spec().reading_label
560 }
561
562 pub fn from_formats_value(self) -> &'static str {
563 self.spec().from_formats_value
564 }
565
566 pub fn uses_generic_convert_options(self) -> bool {
567 self.spec().generic_convert_options
568 }
569
570 pub fn supports_vlm(self) -> bool {
571 self.spec().supports_vlm
572 }
573
574 fn spec(self) -> &'static InputKindSpec {
575 INPUT_KIND_SPECS
576 .iter()
577 .find(|spec| spec.kind == self)
578 .expect("missing InputKindSpec")
579 }
580}
581
582impl std::fmt::Display for InputKind {
583 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584 f.write_str(self.from_formats_value())
585 }
586}
587
588impl FromStr for InputKind {
589 type Err = PdfConvertError;
590
591 fn from_str(value: &str) -> Result<Self> {
592 let normalized = value.trim().to_ascii_lowercase();
593
594 INPUT_KIND_SPECS
595 .iter()
596 .find(|spec| spec.parse_aliases.contains(&normalized.as_str()))
597 .map(|spec| spec.kind)
598 .ok_or_else(|| {
599 PdfConvertError::validation_error(
600 "input_format",
601 format!("unsupported input format: {}", normalized),
602 )
603 })
604 }
605}
606
607fn detect_auto_kind(extension: Option<&str>, media_type: Option<&str>) -> Option<InputKind> {
608 INPUT_KIND_SPECS
609 .iter()
610 .filter(|spec| spec.auto_detect)
611 .find(|spec| {
612 extension.is_some_and(|ext| spec.extensions.contains(&ext))
613 || media_type.is_some_and(|mime| spec.media_types.contains(&mime))
614 })
615 .map(|spec| spec.kind)
616}
617
618fn image_media_type(extension: Option<&str>, media_type: Option<&str>) -> &'static str {
619 match (extension, media_type) {
620 (Some("jpg" | "jpeg"), _) | (_, Some("image/jpeg")) => "image/jpeg",
621 (Some("gif"), _) | (_, Some("image/gif")) => "image/gif",
622 (Some("bmp"), _) | (_, Some("image/bmp")) => "image/bmp",
623 (Some("tif" | "tiff"), _) | (_, Some("image/tiff")) => "image/tiff",
624 (Some("webp"), _) | (_, Some("image/webp")) => "image/webp",
625 (Some("svg"), _) | (_, Some("image/svg+xml")) => "image/svg+xml",
626 _ => "image/png",
627 }
628}
629
630fn image_extension(media_type: Option<&str>) -> &'static str {
631 match media_type {
632 Some("image/jpeg") => "jpg",
633 Some("image/gif") => "gif",
634 Some("image/bmp") => "bmp",
635 Some("image/tiff") => "tiff",
636 Some("image/webp") => "webp",
637 Some("image/svg+xml") => "svg",
638 _ => "png",
639 }
640}
641
642fn audio_media_type(extension: Option<&str>, media_type: Option<&str>) -> &'static str {
643 match (extension, media_type) {
644 (Some("mp3"), _) | (_, Some("audio/mpeg")) => "audio/mpeg",
645 (Some("m4a"), _) | (_, Some("audio/mp4")) => "audio/mp4",
646 (Some("flac"), _) | (_, Some("audio/flac")) => "audio/flac",
647 (Some("ogg"), _) | (_, Some("audio/ogg")) => "audio/ogg",
648 (Some("aac"), _) | (_, Some("audio/aac")) => "audio/aac",
649 _ => "audio/wav",
650 }
651}
652
653fn video_media_type(extension: Option<&str>, media_type: Option<&str>) -> &'static str {
654 match (extension, media_type) {
655 (Some("mov"), _) | (_, Some("video/quicktime")) => "video/quicktime",
656 (Some("avi"), _) | (_, Some("video/x-msvideo")) => "video/x-msvideo",
657 (Some("mkv"), _) | (_, Some("video/x-matroska")) => "video/x-matroska",
658 (Some("webm"), _) | (_, Some("video/webm")) => "video/webm",
659 _ => "video/mp4",
660 }
661}
662
663fn normalized_extension(filename: &str) -> Option<String> {
664 Path::new(filename)
665 .extension()
666 .and_then(|ext| ext.to_str())
667 .map(|ext| ext.to_ascii_lowercase())
668}
669
670fn normalized_media_type(media_type: Option<&str>) -> Option<String> {
671 media_type.map(|value| {
672 value
673 .split(';')
674 .next()
675 .unwrap_or(value)
676 .trim()
677 .to_ascii_lowercase()
678 })
679}
680
681#[cfg(test)]
682mod tests {
683 use super::*;
684
685 #[test]
686 fn metadata_methods_match_specs() {
687 for spec in INPUT_KIND_SPECS {
688 assert_eq!(spec.kind.default_extension(), spec.default_extension);
689 assert_eq!(spec.kind.media_type(), spec.default_media_type);
690 assert_eq!(spec.kind.from_formats_value(), spec.from_formats_value);
691 assert_eq!(spec.kind.reading_label(), spec.reading_label);
692 assert_eq!(
693 spec.kind.uses_generic_convert_options(),
694 spec.generic_convert_options
695 );
696 assert_eq!(spec.kind.supports_vlm(), spec.supports_vlm);
697 }
698 }
699
700 #[test]
701 fn detects_first_wave_and_latex_input_kinds_from_extension() {
702 for spec in INPUT_KIND_SPECS.iter().filter(|spec| spec.auto_detect) {
703 for extension in spec.extensions {
704 let filename = format!("sample.{}", extension);
705 assert_eq!(
706 InputKind::from_path(Path::new(&filename)),
707 Some(spec.kind),
708 "failed to detect extension '{}'",
709 extension
710 );
711 }
712 }
713 }
714
715 #[test]
716 fn generic_xml_and_json_do_not_auto_map() {
717 assert_eq!(InputKind::from_path(Path::new("a.xml")), None);
718 assert_eq!(InputKind::from_path(Path::new("a.json")), None);
719 assert_eq!(
720 InputKind::from_filename_and_media_type("downloaded", Some("application/xml")),
721 None
722 );
723 assert_eq!(
724 InputKind::from_filename_and_media_type("downloaded", Some("application/json")),
725 None
726 );
727 }
728
729 #[test]
730 fn detects_supported_input_kinds_from_mime_type() {
731 for spec in INPUT_KIND_SPECS.iter().filter(|spec| spec.auto_detect) {
732 for media_type in spec.media_types {
733 assert_eq!(
734 InputKind::from_filename_and_media_type("upload", Some(media_type)),
735 Some(spec.kind),
736 "failed to detect media type '{}'",
737 media_type
738 );
739 }
740 }
741 }
742
743 #[test]
744 fn reports_override_only_sources() {
745 assert!(InputKind::requires_explicit_override("paper.xml", None));
746 assert!(InputKind::requires_explicit_override(
747 "paper",
748 Some("application/xml")
749 ));
750 assert!(InputKind::requires_explicit_override("paper.json", None));
751 assert!(InputKind::requires_explicit_override(
752 "paper",
753 Some("application/json")
754 ));
755 assert!(!InputKind::requires_explicit_override("paper.tex", None));
756 }
757
758 #[test]
759 fn derives_second_wave_defaults() {
760 assert_eq!(InputKind::XmlJats.default_extension(), "xml");
761 assert_eq!(InputKind::JsonDocling.default_extension(), "json");
762 assert_eq!(InputKind::Latex.default_extension(), "tex");
763 assert_eq!(InputKind::XmlUspto.from_formats_value(), "xml_uspto");
764 assert_eq!(InputKind::JsonDocling.from_formats_value(), "json_docling");
765 assert_eq!(InputKind::Latex.from_formats_value(), "latex");
766 }
767
768 #[test]
769 fn parses_input_format_strings() {
770 for spec in INPUT_KIND_SPECS {
771 for alias in spec.parse_aliases {
772 assert_eq!(
773 alias.parse::<InputKind>().unwrap(),
774 spec.kind,
775 "failed to parse input alias '{alias}'"
776 );
777 }
778 }
779 assert!("xml".parse::<InputKind>().is_err());
780 }
781
782 #[test]
783 fn preserves_specific_media_types() {
784 assert_eq!(
785 InputKind::Image.canonical_media_type("cover.jpg", None),
786 "image/jpeg"
787 );
788 assert_eq!(
789 InputKind::Email.canonical_media_type("message.msg", None),
790 "application/vnd.ms-outlook"
791 );
792 assert_eq!(
793 InputKind::Html.canonical_media_type("page.xhtml", None),
794 "application/xhtml+xml"
795 );
796 assert_eq!(
797 InputKind::Audio.canonical_media_type("recording.mp3", None),
798 "audio/mpeg"
799 );
800 assert_eq!(
801 InputKind::Audio.canonical_media_type("recording.m4a", None),
802 "audio/mp4"
803 );
804 assert_eq!(
805 InputKind::Video.canonical_media_type("clip.mov", None),
806 "video/quicktime"
807 );
808 assert_eq!(
809 InputKind::Video.canonical_media_type("clip.mkv", None),
810 "video/x-matroska"
811 );
812 }
813}