1use crate::audio::Audio;
11use crate::vid::Vid;
12use regex::Regex;
13use std::borrow::Cow;
14use std::sync::LazyLock;
15
16const PDF_EMBED_HEIGHT: &str = "600px";
17
18static EXTENSION_RE: LazyLock<Regex> = LazyLock::new(|| {
19 Regex::new(r"\.([0-9a-zA-Z]+)([?#].*)?$").expect("Invalid EXTENSION_RE regex pattern")
20});
21
22static YOUTUBE_RE: LazyLock<Regex> = LazyLock::new(|| {
23 Regex::new(
24 r"(?:youtube(?:-nocookie)?\.com/watch\?.*v=|youtu\.be/|youtube(?:-nocookie)?\.com/embed/|youtube(?:-nocookie)?\.com/v/)([a-zA-Z0-9_-]{11})",
25 )
26 .expect("Invalid YOUTUBE_RE regex pattern")
27});
28
29#[derive(Debug, PartialEq)]
31pub enum MediaEmbed {
32 Video(Vid),
34 Audio(Audio),
36 YouTube {
38 video_id: String,
39 caption: Option<String>,
40 },
41 Pdf {
43 url: String,
44 caption: Option<String>,
45 },
46}
47
48impl MediaEmbed {
49 pub fn from_bare_url(url: &str) -> Option<Self> {
54 Self::from_url_and_title(url, "")
55 }
56
57 pub fn from_url_and_title(url: &str, title: &str) -> Option<Self> {
67 if let Some(video_id) = Self::extract_youtube_id(url) {
69 return Some(MediaEmbed::YouTube {
70 video_id,
71 caption: if title.is_empty() {
72 None
73 } else {
74 Some(title.to_string())
75 },
76 });
77 }
78
79 if let Some(ext) = Self::extension_from_url(url) {
81 let ext_lower = ext.to_lowercase();
82
83 if let Some(vid) = Vid::from_url_and_title(url, title) {
85 return Some(MediaEmbed::Video(vid));
86 }
87
88 if let Some(audio) = Audio::from_url_and_title(url, title) {
90 return Some(MediaEmbed::Audio(audio));
91 }
92
93 if ext_lower == "pdf" {
95 return Some(MediaEmbed::Pdf {
96 url: url.to_string(),
97 caption: if title.is_empty() {
98 None
99 } else {
100 Some(title.to_string())
101 },
102 });
103 }
104 }
105
106 None
107 }
108
109 pub fn to_html(&self, open_only: bool, server_mode: bool, transcode_enabled: bool) -> String {
115 match self {
116 MediaEmbed::Video(vid) => vid.to_html(open_only, server_mode, transcode_enabled),
117 MediaEmbed::Audio(audio) => audio.to_html(open_only),
118 MediaEmbed::YouTube { video_id, caption } => {
119 Self::youtube_to_html(video_id, caption.as_deref(), open_only)
120 }
121 MediaEmbed::Pdf { url, caption } => {
122 Self::pdf_to_html(url, caption.as_deref(), open_only)
123 }
124 }
125 }
126
127 pub fn html_close(&self) -> String {
129 match self {
130 MediaEmbed::Video(_) => Vid::html_close(),
131 MediaEmbed::Audio(_) => Audio::html_close().to_string(),
132 MediaEmbed::YouTube { .. } | MediaEmbed::Pdf { .. } => {
133 "</figcaption></figure>".to_string()
134 }
135 }
136 }
137
138 fn extract_youtube_id(url: &str) -> Option<String> {
139 YOUTUBE_RE
140 .captures(url)
141 .and_then(|caps| caps.get(1))
142 .map(|id| id.as_str().to_string())
143 }
144
145 fn extension_from_url(url: &str) -> Option<String> {
146 EXTENSION_RE.captures(url).map(|cap| cap[1].to_string())
147 }
148
149 fn escaped_caption(caption: Option<&str>) -> Cow<'_, str> {
154 caption.map(html_escape::encode_text).unwrap_or_default()
155 }
156
157 fn youtube_to_html(video_id: &str, caption: Option<&str>, open_only: bool) -> String {
158 format!(
159 r#"
160 <figure class="video-embed youtube-embed">
161 <iframe
162 width="{yt_width}"
163 height="{yt_height}"
164 src="https://www.youtube-nocookie.com/embed/{video_id}"
165 title="YouTube video player"
166 frameborder="0"
167 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
168 referrerpolicy="strict-origin-when-cross-origin"
169 allowfullscreen>
170 </iframe>
171 <figcaption>{caption}{close}"#,
172 yt_width = crate::constants::YOUTUBE_EMBED_WIDTH,
173 yt_height = crate::constants::YOUTUBE_EMBED_HEIGHT,
174 video_id = html_escape::encode_double_quoted_attribute(video_id),
175 caption = Self::escaped_caption(caption),
176 close = if open_only {
177 ""
178 } else {
179 "</figcaption></figure>"
180 }
181 )
182 }
183
184 fn pdf_to_html(url: &str, caption: Option<&str>, open_only: bool) -> String {
185 let escaped_url = html_escape::encode_double_quoted_attribute(url);
193 format!(
194 r#"
195 <figure class="pdf-embed" data-pdf-url="{url}">
196 <object data="{url}" type="application/pdf" width="100%" height="{pdf_height}">
197 <p class="pdf-fallback">
198 PDF cannot be displayed inline.
199 <a href="{url}" download data-pdf-fallback>Download PDF</a>
200 </p>
201 </object>
202 <figcaption>{caption}{close}"#,
203 url = escaped_url,
204 pdf_height = PDF_EMBED_HEIGHT,
205 caption = Self::escaped_caption(caption),
206 close = if open_only {
207 ""
208 } else {
209 "</figcaption></figure>"
210 }
211 )
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum MediaViewerType {
221 Video,
222 Pdf,
223 Audio,
224 Image,
225}
226
227impl MediaViewerType {
228 #[must_use]
240 pub fn from_route(path: &str) -> Option<Self> {
241 match path {
242 "/.mbr/videos/" => Some(Self::Video),
243 "/.mbr/pdfs/" => Some(Self::Pdf),
244 "/.mbr/audio/" => Some(Self::Audio),
245 "/.mbr/images/" => Some(Self::Image),
246 _ => None,
247 }
248 }
249
250 #[must_use]
252 pub const fn template_name(&self) -> &'static str {
253 "media_viewer.html"
254 }
255
256 #[must_use]
258 pub const fn label(&self) -> &'static str {
259 match self {
260 Self::Video => "Video",
261 Self::Pdf => "PDF",
262 Self::Audio => "Audio",
263 Self::Image => "Image",
264 }
265 }
266
267 #[must_use]
269 pub const fn as_str(&self) -> &'static str {
270 match self {
271 Self::Video => "video",
272 Self::Pdf => "pdf",
273 Self::Audio => "audio",
274 Self::Image => "image",
275 }
276 }
277
278 #[must_use]
282 pub fn from_extension(ext: &str) -> Option<Self> {
283 match ext.to_ascii_lowercase().as_str() {
284 "mp4" | "m4v" | "mov" | "webm" | "flv" | "mpg" | "mpeg" | "avi" | "3gp" | "wmv"
286 | "mkv" | "ts" | "mts" | "m2ts" | "vob" | "divx" | "xvid" | "asf" | "rm" | "rmvb"
287 | "f4v" | "ogv" => Some(Self::Video),
288 "mp3" | "wav" | "ogg" | "flac" | "aac" | "m4a" | "aiff" | "aif" | "oga" | "opus"
290 | "wma" => Some(Self::Audio),
291 "jpg" | "jpeg" | "png" | "webp" | "gif" | "bmp" | "tif" | "tiff" | "svg" => {
293 Some(Self::Image)
294 }
295 "pdf" => Some(Self::Pdf),
297 _ => None,
298 }
299 }
300
301 #[must_use]
305 pub fn from_path(path: &std::path::Path) -> Option<Self> {
306 path.extension()
307 .and_then(|ext| ext.to_str())
308 .and_then(Self::from_extension)
309 }
310
311 #[must_use]
313 pub const fn route_path(&self) -> &'static str {
314 match self {
315 Self::Video => "/.mbr/videos/",
316 Self::Pdf => "/.mbr/pdfs/",
317 Self::Audio => "/.mbr/audio/",
318 Self::Image => "/.mbr/images/",
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use std::path::Path;
327
328 #[test]
330 fn test_youtube_watch_url() {
331 let embed =
332 MediaEmbed::from_url_and_title("https://www.youtube.com/watch?v=dQw4w9WgXcQ", "Title");
333 assert!(matches!(
334 embed,
335 Some(MediaEmbed::YouTube { video_id, .. }) if video_id == "dQw4w9WgXcQ"
336 ));
337 }
338
339 #[test]
340 fn test_youtube_short_url() {
341 let embed = MediaEmbed::from_url_and_title("https://youtu.be/dQw4w9WgXcQ", "");
342 assert!(matches!(
343 embed,
344 Some(MediaEmbed::YouTube { video_id, caption }) if video_id == "dQw4w9WgXcQ" && caption.is_none()
345 ));
346 }
347
348 #[test]
349 fn test_youtube_embed_url() {
350 let embed =
351 MediaEmbed::from_url_and_title("https://www.youtube.com/embed/dQw4w9WgXcQ", "Caption");
352 assert!(matches!(
353 embed,
354 Some(MediaEmbed::YouTube { video_id, caption }) if video_id == "dQw4w9WgXcQ" && caption == Some("Caption".to_string())
355 ));
356 }
357
358 #[test]
359 fn test_youtube_with_extra_params() {
360 let embed = MediaEmbed::from_url_and_title(
361 "https://www.youtube-nocookie.com/watch?v=dQw4w9WgXcQ&t=30s",
362 "",
363 );
364 assert!(matches!(
365 embed,
366 Some(MediaEmbed::YouTube { video_id, .. }) if video_id == "dQw4w9WgXcQ"
367 ));
368 }
369
370 #[test]
372 fn test_video_mp4() {
373 let embed = MediaEmbed::from_url_and_title("video.mp4", "My Video");
374 assert!(matches!(embed, Some(MediaEmbed::Video(_))));
375 }
376
377 #[test]
378 fn test_video_webm_not_detected_by_vid() {
379 let embed = MediaEmbed::from_url_and_title("video.webm", "");
382 assert!(matches!(embed, Some(MediaEmbed::Audio(_))));
383 }
384
385 #[test]
387 fn test_audio_mp3() {
388 let embed = MediaEmbed::from_url_and_title("podcast.mp3", "Episode 1");
389 assert!(matches!(embed, Some(MediaEmbed::Audio(_))));
390 }
391
392 #[test]
393 fn test_audio_wav() {
394 let embed = MediaEmbed::from_url_and_title("sound.wav", "");
395 assert!(matches!(embed, Some(MediaEmbed::Audio(_))));
396 }
397
398 #[test]
400 fn test_pdf() {
401 let embed = MediaEmbed::from_url_and_title("document.pdf", "Important Doc");
402 assert!(matches!(
403 embed,
404 Some(MediaEmbed::Pdf { url, caption }) if url == "document.pdf" && caption == Some("Important Doc".to_string())
405 ));
406 }
407
408 #[test]
409 fn test_pdf_with_path() {
410 let embed = MediaEmbed::from_url_and_title("/docs/report.pdf", "");
411 assert!(matches!(
412 embed,
413 Some(MediaEmbed::Pdf { url, caption }) if url == "/docs/report.pdf" && caption.is_none()
414 ));
415 }
416
417 #[test]
418 fn test_pdf_case_insensitive() {
419 let embed = MediaEmbed::from_url_and_title("document.PDF", "");
420 assert!(matches!(embed, Some(MediaEmbed::Pdf { .. })));
421 }
422
423 #[test]
425 fn test_image_not_detected() {
426 assert!(MediaEmbed::from_url_and_title("photo.jpg", "").is_none());
427 assert!(MediaEmbed::from_url_and_title("image.png", "").is_none());
428 assert!(MediaEmbed::from_url_and_title("graphic.gif", "").is_none());
429 }
430
431 #[test]
432 fn test_unknown_extension() {
433 assert!(MediaEmbed::from_url_and_title("file.xyz", "").is_none());
434 }
435
436 #[test]
437 fn test_no_extension() {
438 assert!(MediaEmbed::from_url_and_title("https://example.com/page", "").is_none());
439 }
440
441 #[test]
443 fn test_youtube_html() {
444 let embed = MediaEmbed::YouTube {
445 video_id: "abc123xyz".to_string(),
446 caption: Some("Test Video".to_string()),
447 };
448 let html = embed.to_html(false, false, false);
449 assert!(html.contains("youtube-embed"));
450 assert!(html.contains("https://www.youtube-nocookie.com/embed/abc123xyz"));
451 assert!(html.contains("<figcaption>Test Video</figcaption>"));
452 }
453
454 #[test]
455 fn test_pdf_html() {
456 let embed = MediaEmbed::Pdf {
457 url: "/docs/test.pdf".to_string(),
458 caption: Some("My PDF".to_string()),
459 };
460 let html = embed.to_html(false, false, false);
461 assert!(html.contains("pdf-embed"));
462 assert!(html.contains(r#"data="/docs/test.pdf""#));
463 assert!(html.contains(r#"type="application/pdf""#));
464 assert!(html.contains("data-pdf-fallback"));
465 assert!(html.contains("<figcaption>My PDF</figcaption>"));
466 }
467
468 #[test]
469 fn test_pdf_html_open_only() {
470 let embed = MediaEmbed::Pdf {
471 url: "doc.pdf".to_string(),
472 caption: None,
473 };
474 let html = embed.to_html(true, false, false);
475 assert!(html.contains("<object"));
476 assert!(!html.contains("</figcaption></figure>"));
477 }
478
479 #[test]
480 fn test_youtube_v_url() {
481 let embed = MediaEmbed::from_url_and_title("https://www.youtube.com/v/dQw4w9WgXcQ", "");
482 assert!(matches!(
483 embed,
484 Some(MediaEmbed::YouTube { video_id, .. }) if video_id == "dQw4w9WgXcQ"
485 ));
486 }
487
488 #[test]
489 fn test_youtube_without_www() {
490 let embed = MediaEmbed::from_url_and_title("https://youtube.com/watch?v=dQw4w9WgXcQ", "");
491 assert!(matches!(
492 embed,
493 Some(MediaEmbed::YouTube { video_id, .. }) if video_id == "dQw4w9WgXcQ"
494 ));
495 }
496
497 #[test]
498 fn test_youtube_invalid_id_length() {
499 let embed = MediaEmbed::from_url_and_title("https://www.youtube.com/watch?v=short", "");
500 assert!(embed.is_none());
501 }
502
503 #[test]
504 fn test_youtube_not_youtube() {
505 let embed = MediaEmbed::from_url_and_title("https://example.com/watch?v=dQw4w9WgXcQ", "");
506 assert!(embed.is_none());
507 }
508
509 #[test]
510 fn test_youtube_nocookie_embed_url() {
511 let embed = MediaEmbed::from_url_and_title(
512 "https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ",
513 "Caption",
514 );
515 assert!(matches!(
516 embed,
517 Some(MediaEmbed::YouTube { video_id, caption }) if video_id == "dQw4w9WgXcQ" && caption == Some("Caption".to_string())
518 ));
519 }
520
521 #[test]
522 fn test_youtube_nocookie_v_url() {
523 let embed =
524 MediaEmbed::from_url_and_title("https://www.youtube-nocookie.com/v/dQw4w9WgXcQ", "");
525 assert!(matches!(
526 embed,
527 Some(MediaEmbed::YouTube { video_id, .. }) if video_id == "dQw4w9WgXcQ"
528 ));
529 }
530
531 #[test]
534 fn test_pdf_html_escapes_hostile_url() {
535 let embed =
536 MediaEmbed::from_url_and_title(r#"a"onerror="alert(1)"b.pdf"#, "").expect("pdf embed");
537 let html = embed.to_html(false, false, false);
538 assert!(
539 html.contains("""),
540 "double quotes must be escaped: {html}"
541 );
542 assert_eq!(
545 html.matches(r#"a"onerror="alert(1)"b.pdf"#)
546 .count(),
547 3,
548 "data-pdf-url, object data, and href must all be escaped: {html}"
549 );
550 assert!(
551 !html.contains(r#""onerror=""#),
552 "must not emit an injected attribute: {html}"
553 );
554 }
555
556 #[test]
559 fn test_pdf_html_escapes_ampersand_in_url() {
560 let embed = MediaEmbed::from_url_and_title("/docs/Q&A-report.pdf", "").expect("pdf embed");
561 let html = embed.to_html(false, false, false);
562 assert!(
563 !html.contains("Q&A-report"),
564 "bare ampersand emitted: {html}"
565 );
566 assert_eq!(
567 html.matches("/docs/Q&A-report.pdf").count(),
568 3,
569 "data-pdf-url, object data, and href must all be encoded: {html}"
570 );
571 }
572
573 #[test]
575 fn test_pdf_html_escapes_caption() {
576 let embed = MediaEmbed::from_url_and_title("doc.pdf", "<script>alert(1)</script> & more")
577 .expect("pdf embed");
578 let html = embed.to_html(false, false, false);
579 assert!(
580 html.contains("<script>alert(1)</script> & more"),
581 "caption must be escaped: {html}"
582 );
583 assert!(
584 !html.contains("<script>"),
585 "must not emit a raw script tag: {html}"
586 );
587 }
588
589 #[test]
591 fn test_pdf_html_ordinary_values_round_trip_unchanged() {
592 let embed =
593 MediaEmbed::from_url_and_title("/docs/my_report-v2.pdf", "My PDF").expect("pdf embed");
594 let html = embed.to_html(false, false, false);
595 assert!(html.contains(r#"data-pdf-url="/docs/my_report-v2.pdf""#));
596 assert!(html.contains(r#"data="/docs/my_report-v2.pdf""#));
597 assert!(html.contains(r#"href="/docs/my_report-v2.pdf""#));
598 assert!(html.contains("<figcaption>My PDF</figcaption>"));
599 assert!(!html.contains("&"), "nothing to escape here: {html}");
600 }
601
602 #[test]
604 fn test_youtube_html_escapes_video_id_and_caption() {
605 let embed = MediaEmbed::YouTube {
606 video_id: r#"x"onload="alert(1)"#.to_string(),
607 caption: Some("<b>hi</b> & bye".to_string()),
608 };
609 let html = embed.to_html(false, false, false);
610 assert!(
611 html.contains("""),
612 "double quotes must be escaped: {html}"
613 );
614 assert!(
615 html.contains(
616 r#"src="https://www.youtube-nocookie.com/embed/x"onload="alert(1)""#
617 ),
618 "iframe src must be fully escaped: {html}"
619 );
620 assert!(
621 !html.contains(r#""onload=""#),
622 "must not emit an injected attribute: {html}"
623 );
624 assert!(html.contains("<b>hi</b> & bye"));
625 }
626
627 #[test]
628 fn test_html_close() {
629 let youtube = MediaEmbed::YouTube {
630 video_id: "x".to_string(),
631 caption: None,
632 };
633 let pdf = MediaEmbed::Pdf {
634 url: "x.pdf".to_string(),
635 caption: None,
636 };
637 assert_eq!(youtube.html_close(), "</figcaption></figure>");
638 assert_eq!(pdf.html_close(), "</figcaption></figure>");
639 }
640
641 #[test]
644 fn test_media_viewer_type_from_route_videos() {
645 assert_eq!(
646 MediaViewerType::from_route("/.mbr/videos/"),
647 Some(MediaViewerType::Video)
648 );
649 }
650
651 #[test]
652 fn test_media_viewer_type_from_route_pdfs() {
653 assert_eq!(
654 MediaViewerType::from_route("/.mbr/pdfs/"),
655 Some(MediaViewerType::Pdf)
656 );
657 }
658
659 #[test]
660 fn test_media_viewer_type_from_route_audio() {
661 assert_eq!(
662 MediaViewerType::from_route("/.mbr/audio/"),
663 Some(MediaViewerType::Audio)
664 );
665 }
666
667 #[test]
668 fn test_media_viewer_type_from_route_images() {
669 assert_eq!(
670 MediaViewerType::from_route("/.mbr/images/"),
671 Some(MediaViewerType::Image)
672 );
673 }
674
675 #[test]
676 fn test_media_viewer_type_from_route_invalid() {
677 assert_eq!(MediaViewerType::from_route("/some/other/path"), None);
678 assert_eq!(MediaViewerType::from_route("/.mbr/videos"), None); assert_eq!(MediaViewerType::from_route("/.mbr/unknown/"), None);
680 }
681
682 #[test]
683 fn test_media_viewer_type_template_name() {
684 assert_eq!(MediaViewerType::Video.template_name(), "media_viewer.html");
685 assert_eq!(MediaViewerType::Pdf.template_name(), "media_viewer.html");
686 assert_eq!(MediaViewerType::Audio.template_name(), "media_viewer.html");
687 }
688
689 #[test]
690 fn test_media_viewer_type_label() {
691 assert_eq!(MediaViewerType::Video.label(), "Video");
692 assert_eq!(MediaViewerType::Pdf.label(), "PDF");
693 assert_eq!(MediaViewerType::Audio.label(), "Audio");
694 }
695
696 #[test]
697 fn test_media_viewer_type_as_str() {
698 assert_eq!(MediaViewerType::Video.as_str(), "video");
699 assert_eq!(MediaViewerType::Pdf.as_str(), "pdf");
700 assert_eq!(MediaViewerType::Audio.as_str(), "audio");
701 }
702
703 #[test]
704 fn test_media_viewer_type_from_extension_video() {
705 for ext in &[
706 "mp4", "m4v", "mov", "webm", "flv", "mpg", "mpeg", "avi", "3gp", "wmv", "mkv", "ts",
707 "mts", "m2ts", "vob", "divx", "xvid", "asf", "rm", "rmvb", "f4v", "ogv",
708 ] {
709 assert_eq!(
710 MediaViewerType::from_extension(ext),
711 Some(MediaViewerType::Video),
712 "Expected Video for extension '{ext}'"
713 );
714 }
715 }
716
717 #[test]
718 fn test_media_viewer_type_from_extension_audio() {
719 for ext in &[
720 "mp3", "wav", "ogg", "flac", "aac", "m4a", "aiff", "aif", "oga", "opus", "wma",
721 ] {
722 assert_eq!(
723 MediaViewerType::from_extension(ext),
724 Some(MediaViewerType::Audio),
725 "Expected Audio for extension '{ext}'"
726 );
727 }
728 }
729
730 #[test]
731 fn test_media_viewer_type_from_extension_image() {
732 for ext in &[
733 "jpg", "jpeg", "png", "webp", "gif", "bmp", "tif", "tiff", "svg",
734 ] {
735 assert_eq!(
736 MediaViewerType::from_extension(ext),
737 Some(MediaViewerType::Image),
738 "Expected Image for extension '{ext}'"
739 );
740 }
741 }
742
743 #[test]
744 fn test_media_viewer_type_from_extension_pdf() {
745 assert_eq!(
746 MediaViewerType::from_extension("pdf"),
747 Some(MediaViewerType::Pdf)
748 );
749 }
750
751 #[test]
752 fn test_media_viewer_type_from_extension_case_insensitive() {
753 assert_eq!(
754 MediaViewerType::from_extension("MP4"),
755 Some(MediaViewerType::Video)
756 );
757 assert_eq!(
758 MediaViewerType::from_extension("Pdf"),
759 Some(MediaViewerType::Pdf)
760 );
761 assert_eq!(
762 MediaViewerType::from_extension("JPG"),
763 Some(MediaViewerType::Image)
764 );
765 }
766
767 #[test]
768 fn test_media_viewer_type_from_extension_unknown() {
769 assert_eq!(MediaViewerType::from_extension("md"), None);
770 assert_eq!(MediaViewerType::from_extension("html"), None);
771 assert_eq!(MediaViewerType::from_extension("rs"), None);
772 assert_eq!(MediaViewerType::from_extension(""), None);
773 }
774
775 #[test]
776 fn test_media_viewer_type_from_path() {
777 assert_eq!(
778 MediaViewerType::from_path(Path::new("videos/demo.mp4")),
779 Some(MediaViewerType::Video)
780 );
781 assert_eq!(
782 MediaViewerType::from_path(Path::new("music/song.mp3")),
783 Some(MediaViewerType::Audio)
784 );
785 assert_eq!(
786 MediaViewerType::from_path(Path::new("images/photo.jpg")),
787 Some(MediaViewerType::Image)
788 );
789 assert_eq!(
790 MediaViewerType::from_path(Path::new("docs/paper.pdf")),
791 Some(MediaViewerType::Pdf)
792 );
793 assert_eq!(MediaViewerType::from_path(Path::new("readme.md")), None);
794 assert_eq!(MediaViewerType::from_path(Path::new("noext")), None);
795 }
796
797 #[test]
798 fn test_media_viewer_type_route_path() {
799 assert_eq!(MediaViewerType::Video.route_path(), "/.mbr/videos/");
800 assert_eq!(MediaViewerType::Pdf.route_path(), "/.mbr/pdfs/");
801 assert_eq!(MediaViewerType::Audio.route_path(), "/.mbr/audio/");
802 assert_eq!(MediaViewerType::Image.route_path(), "/.mbr/images/");
803 }
804
805 #[test]
806 fn test_media_viewer_type_route_path_roundtrips_with_from_route() {
807 for media_type in &[
808 MediaViewerType::Video,
809 MediaViewerType::Pdf,
810 MediaViewerType::Audio,
811 MediaViewerType::Image,
812 ] {
813 assert_eq!(
814 MediaViewerType::from_route(media_type.route_path()),
815 Some(*media_type),
816 "route_path -> from_route roundtrip failed for {media_type:?}"
817 );
818 }
819 }
820}