1use crate::annotations::Annotation;
4use crate::geometry::Rectangle;
5use crate::objects::{Dictionary, Object, ObjectReference};
6
7#[cfg(test)]
8use crate::annotations::AnnotationType;
9#[cfg(test)]
10use crate::graphics::Color;
11
12#[derive(Debug, Clone)]
14pub enum LinkDestination {
15 XYZ {
17 page: ObjectReference,
19 left: Option<f64>,
21 top: Option<f64>,
23 zoom: Option<f64>,
25 },
26 Fit {
28 page: ObjectReference,
30 },
31 FitH {
33 page: ObjectReference,
35 top: Option<f64>,
37 },
38 FitV {
40 page: ObjectReference,
42 left: Option<f64>,
44 },
45 FitR {
47 page: ObjectReference,
49 rect: Rectangle,
51 },
52 Named(String),
54}
55
56impl LinkDestination {
57 pub fn to_array(&self) -> Object {
59 match self {
60 LinkDestination::XYZ {
61 page,
62 left,
63 top,
64 zoom,
65 } => {
66 let mut array = vec![Object::Reference(*page), Object::Name("XYZ".to_string())];
67
68 array.push(left.map(Object::Real).unwrap_or(Object::Null));
69 array.push(top.map(Object::Real).unwrap_or(Object::Null));
70 array.push(zoom.map(Object::Real).unwrap_or(Object::Null));
71
72 Object::Array(array)
73 }
74 LinkDestination::Fit { page } => Object::Array(vec![
75 Object::Reference(*page),
76 Object::Name("Fit".to_string()),
77 ]),
78 LinkDestination::FitH { page, top } => Object::Array(vec![
79 Object::Reference(*page),
80 Object::Name("FitH".to_string()),
81 top.map(Object::Real).unwrap_or(Object::Null),
82 ]),
83 LinkDestination::FitV { page, left } => Object::Array(vec![
84 Object::Reference(*page),
85 Object::Name("FitV".to_string()),
86 left.map(Object::Real).unwrap_or(Object::Null),
87 ]),
88 LinkDestination::FitR { page, rect } => Object::Array(vec![
89 Object::Reference(*page),
90 Object::Name("FitR".to_string()),
91 Object::Real(rect.lower_left.x),
92 Object::Real(rect.lower_left.y),
93 Object::Real(rect.upper_right.x),
94 Object::Real(rect.upper_right.y),
95 ]),
96 LinkDestination::Named(name) => Object::String(name.clone()),
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
103pub enum LinkAction {
104 GoTo(LinkDestination),
106 GoToR {
108 file: String,
110 destination: LinkDestination,
112 },
113 Launch {
115 file: String,
117 },
118 URI {
120 uri: String,
122 },
123 Named {
125 name: String,
127 },
128}
129
130impl LinkAction {
131 pub fn to_dict(&self) -> Dictionary {
133 let mut dict = Dictionary::new();
134
135 match self {
136 LinkAction::GoTo(dest) => {
137 dict.set("S", Object::Name("GoTo".to_string()));
138 dict.set("D", dest.to_array());
139 }
140 LinkAction::GoToR { file, destination } => {
141 dict.set("S", Object::Name("GoToR".to_string()));
142 dict.set("F", Object::String(file.clone()));
143 dict.set("D", destination.to_array());
144 }
145 LinkAction::Launch { file } => {
146 dict.set("S", Object::Name("Launch".to_string()));
147 dict.set("F", Object::String(file.clone()));
148 }
149 LinkAction::URI { uri } => {
150 dict.set("S", Object::Name("URI".to_string()));
151 dict.set("URI", Object::String(uri.clone()));
152 }
153 LinkAction::Named { name } => {
154 dict.set("S", Object::Name("Named".to_string()));
155 dict.set("N", Object::Name(name.clone()));
156 }
157 }
158
159 dict
160 }
161}
162
163#[derive(Debug, Clone)]
165pub struct LinkAnnotation {
166 pub annotation: Annotation,
168 pub action: LinkAction,
170 pub highlight_mode: HighlightMode,
172}
173
174#[derive(Debug, Clone, Copy, Default)]
176pub enum HighlightMode {
177 None,
179 #[default]
181 Invert,
182 Outline,
184 Push,
186}
187
188impl HighlightMode {
189 pub fn pdf_name(&self) -> &'static str {
191 match self {
192 HighlightMode::None => "N",
193 HighlightMode::Invert => "I",
194 HighlightMode::Outline => "O",
195 HighlightMode::Push => "P",
196 }
197 }
198}
199
200impl LinkAnnotation {
201 pub fn new(rect: Rectangle, action: LinkAction) -> Self {
203 let annotation = Annotation::new(crate::annotations::AnnotationType::Link, rect);
204
205 Self {
206 annotation,
207 action,
208 highlight_mode: HighlightMode::default(),
209 }
210 }
211
212 pub fn to_page(rect: Rectangle, page: ObjectReference) -> Self {
214 let action = LinkAction::GoTo(LinkDestination::Fit { page });
215 Self::new(rect, action)
216 }
217
218 pub fn to_uri(rect: Rectangle, uri: impl Into<String>) -> Self {
220 let action = LinkAction::URI { uri: uri.into() };
221 Self::new(rect, action)
222 }
223
224 pub fn with_highlight_mode(mut self, mode: HighlightMode) -> Self {
226 self.highlight_mode = mode;
227 self
228 }
229
230 pub fn to_annotation(self) -> Annotation {
232 let mut annotation = self.annotation;
233
234 annotation
236 .properties
237 .set("A", Object::Dictionary(self.action.to_dict()));
238
239 annotation.properties.set(
241 "H",
242 Object::Name(self.highlight_mode.pdf_name().to_string()),
243 );
244
245 annotation.border = None;
247
248 annotation
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::geometry::Point;
256
257 #[test]
258 fn test_destination_xyz() {
259 let dest = LinkDestination::XYZ {
260 page: ObjectReference::new(1, 0),
261 left: Some(100.0),
262 top: Some(700.0),
263 zoom: Some(1.5),
264 };
265
266 if let Object::Array(arr) = dest.to_array() {
267 assert_eq!(arr.len(), 5);
268 assert!(matches!(arr[1], Object::Name(ref s) if s == "XYZ"));
269 } else {
270 panic!("Expected array");
271 }
272 }
273
274 #[test]
275 fn test_link_action_uri() {
276 let action = LinkAction::URI {
277 uri: "https://example.com".to_string(),
278 };
279
280 let dict = action.to_dict();
281 assert_eq!(dict.get("S"), Some(&Object::Name("URI".to_string())));
282 assert_eq!(
283 dict.get("URI"),
284 Some(&Object::String("https://example.com".to_string()))
285 );
286 }
287
288 #[test]
289 fn test_link_annotation_to_page() {
290 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
291 let page_ref = ObjectReference::new(2, 0);
292
293 let link = LinkAnnotation::to_page(rect, page_ref);
294 assert!(matches!(link.action, LinkAction::GoTo(_)));
295 }
296
297 #[test]
298 fn test_link_annotation_to_uri() {
299 let rect = Rectangle::new(Point::new(50.0, 50.0), Point::new(150.0, 70.0));
300
301 let link = LinkAnnotation::to_uri(rect, "https://example.com")
302 .with_highlight_mode(HighlightMode::Outline);
303
304 assert!(matches!(link.action, LinkAction::URI { .. }));
305 assert!(matches!(link.highlight_mode, HighlightMode::Outline));
306 }
307
308 #[test]
309 fn test_highlight_mode() {
310 assert_eq!(HighlightMode::None.pdf_name(), "N");
311 assert_eq!(HighlightMode::Invert.pdf_name(), "I");
312 assert_eq!(HighlightMode::Outline.pdf_name(), "O");
313 assert_eq!(HighlightMode::Push.pdf_name(), "P");
314 }
315
316 #[test]
317 fn test_all_link_destinations() {
318 let xyz_combinations = vec![
320 (Some(100.0), Some(700.0), Some(1.5)),
321 (None, Some(700.0), Some(1.5)),
322 (Some(100.0), None, Some(1.5)),
323 (Some(100.0), Some(700.0), None),
324 (None, None, None),
325 ];
326
327 for (left, top, zoom) in xyz_combinations {
328 let dest = LinkDestination::XYZ {
329 page: ObjectReference::new(1, 0),
330 left,
331 top,
332 zoom,
333 };
334
335 if let Object::Array(arr) = dest.to_array() {
336 assert_eq!(arr.len(), 5);
337 assert!(matches!(arr[0], Object::Reference(_)));
338 assert_eq!(arr[1], Object::Name("XYZ".to_string()));
339
340 assert!(left.is_some() || matches!(arr[2], Object::Null));
342 assert!(top.is_some() || matches!(arr[3], Object::Null));
343 assert!(zoom.is_some() || matches!(arr[4], Object::Null));
344 } else {
345 panic!("Expected array");
346 }
347 }
348 }
349
350 #[test]
351 fn test_destination_fit_variants() {
352 let page_ref = ObjectReference::new(5, 0);
353
354 let fit = LinkDestination::Fit { page: page_ref };
356 if let Object::Array(arr) = fit.to_array() {
357 assert_eq!(arr.len(), 2);
358 assert_eq!(arr[0], Object::Reference(page_ref));
359 assert_eq!(arr[1], Object::Name("Fit".to_string()));
360 }
361
362 let fith = LinkDestination::FitH {
364 page: page_ref,
365 top: Some(500.0),
366 };
367 if let Object::Array(arr) = fith.to_array() {
368 assert_eq!(arr.len(), 3);
369 assert_eq!(arr[0], Object::Reference(page_ref));
370 assert_eq!(arr[1], Object::Name("FitH".to_string()));
371 assert_eq!(arr[2], Object::Real(500.0));
372 }
373
374 let fith_none = LinkDestination::FitH {
376 page: page_ref,
377 top: None,
378 };
379 if let Object::Array(arr) = fith_none.to_array() {
380 assert_eq!(arr[2], Object::Null);
381 }
382
383 let fitv = LinkDestination::FitV {
385 page: page_ref,
386 left: Some(100.0),
387 };
388 if let Object::Array(arr) = fitv.to_array() {
389 assert_eq!(arr.len(), 3);
390 assert_eq!(arr[0], Object::Reference(page_ref));
391 assert_eq!(arr[1], Object::Name("FitV".to_string()));
392 assert_eq!(arr[2], Object::Real(100.0));
393 }
394
395 let rect = Rectangle::new(Point::new(50.0, 100.0), Point::new(550.0, 700.0));
397 let fitr = LinkDestination::FitR {
398 page: page_ref,
399 rect,
400 };
401 if let Object::Array(arr) = fitr.to_array() {
402 assert_eq!(arr.len(), 6);
403 assert_eq!(arr[0], Object::Reference(page_ref));
404 assert_eq!(arr[1], Object::Name("FitR".to_string()));
405 assert_eq!(arr[2], Object::Real(50.0));
406 assert_eq!(arr[3], Object::Real(100.0));
407 assert_eq!(arr[4], Object::Real(550.0));
408 assert_eq!(arr[5], Object::Real(700.0));
409 }
410 }
411
412 #[test]
413 fn test_named_destination() {
414 let named_dest = LinkDestination::Named("Chapter3".to_string());
415 if let Object::String(name) = named_dest.to_array() {
416 assert_eq!(name, "Chapter3");
417 } else {
418 panic!("Expected string for named destination");
419 }
420
421 let special_dest = LinkDestination::Named("Section 1.2.3: Introduction".to_string());
423 if let Object::String(name) = special_dest.to_array() {
424 assert_eq!(name, "Section 1.2.3: Introduction");
425 }
426 }
427
428 #[test]
429 fn test_all_link_actions() {
430 let goto_dest = LinkDestination::Fit {
432 page: ObjectReference::new(3, 0),
433 };
434 let goto_action = LinkAction::GoTo(goto_dest);
435 let goto_dict = goto_action.to_dict();
436 assert_eq!(goto_dict.get("S"), Some(&Object::Name("GoTo".to_string())));
437 assert!(goto_dict.get("D").is_some());
438
439 let gotor_dest = LinkDestination::XYZ {
441 page: ObjectReference::new(1, 0),
442 left: Some(0.0),
443 top: Some(792.0),
444 zoom: None,
445 };
446 let gotor_action = LinkAction::GoToR {
447 file: "external.pdf".to_string(),
448 destination: gotor_dest,
449 };
450 let gotor_dict = gotor_action.to_dict();
451 assert_eq!(
452 gotor_dict.get("S"),
453 Some(&Object::Name("GoToR".to_string()))
454 );
455 assert_eq!(
456 gotor_dict.get("F"),
457 Some(&Object::String("external.pdf".to_string()))
458 );
459 assert!(gotor_dict.get("D").is_some());
460
461 let launch_action = LinkAction::Launch {
463 file: "document.doc".to_string(),
464 };
465 let launch_dict = launch_action.to_dict();
466 assert_eq!(
467 launch_dict.get("S"),
468 Some(&Object::Name("Launch".to_string()))
469 );
470 assert_eq!(
471 launch_dict.get("F"),
472 Some(&Object::String("document.doc".to_string()))
473 );
474
475 let uri_action = LinkAction::URI {
477 uri: "https://www.example.com/page?id=123&lang=en".to_string(),
478 };
479 let uri_dict = uri_action.to_dict();
480 assert_eq!(uri_dict.get("S"), Some(&Object::Name("URI".to_string())));
481 assert_eq!(
482 uri_dict.get("URI"),
483 Some(&Object::String(
484 "https://www.example.com/page?id=123&lang=en".to_string()
485 ))
486 );
487
488 let named_action = LinkAction::Named {
490 name: "NextPage".to_string(),
491 };
492 let named_dict = named_action.to_dict();
493 assert_eq!(
494 named_dict.get("S"),
495 Some(&Object::Name("Named".to_string()))
496 );
497 assert_eq!(
498 named_dict.get("N"),
499 Some(&Object::Name("NextPage".to_string()))
500 );
501 }
502
503 #[test]
504 fn test_link_annotation_creation_variations() {
505 let rect = Rectangle::new(Point::new(100.0, 500.0), Point::new(200.0, 520.0));
506
507 let actions = vec![
509 LinkAction::GoTo(LinkDestination::Fit {
510 page: ObjectReference::new(1, 0),
511 }),
512 LinkAction::URI {
513 uri: "mailto:test@example.com".to_string(),
514 },
515 LinkAction::Named {
516 name: "FirstPage".to_string(),
517 },
518 ];
519
520 for action in actions {
521 let link = LinkAnnotation::new(rect, action.clone());
522 assert_eq!(link.annotation.annotation_type, AnnotationType::Link);
523
524 let annotation = link.to_annotation();
525 let dict = annotation.to_dict();
526
527 assert!(dict.get("A").is_some());
529 assert!(dict.get("H").is_some());
530 assert_eq!(dict.get("Subtype"), Some(&Object::Name("Link".to_string())));
531 }
532 }
533
534 #[test]
535 fn test_link_highlight_modes() {
536 let rect = Rectangle::new(Point::new(50.0, 50.0), Point::new(150.0, 70.0));
537 let page_ref = ObjectReference::new(2, 0);
538
539 let modes = vec![
540 HighlightMode::None,
541 HighlightMode::Invert,
542 HighlightMode::Outline,
543 HighlightMode::Push,
544 ];
545
546 for mode in modes {
547 let link = LinkAnnotation::to_page(rect, page_ref).with_highlight_mode(mode);
548
549 let annotation = link.to_annotation();
550 let dict = annotation.to_dict();
551
552 assert_eq!(
553 dict.get("H"),
554 Some(&Object::Name(mode.pdf_name().to_string()))
555 );
556 }
557 }
558
559 #[test]
560 fn test_link_annotation_with_border() {
561 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 150.0));
562
563 let link = LinkAnnotation::to_uri(rect, "https://example.org");
564 let annotation = link.to_annotation();
565
566 assert!(annotation.border.is_none());
568
569 let dict = annotation.to_dict();
571 assert!(!dict.contains_key("BS"));
572 }
573
574 #[test]
575 fn test_link_destination_edge_cases() {
576 let extreme_page = ObjectReference::new(u32::MAX, u16::MAX);
578 let dest = LinkDestination::Fit { page: extreme_page };
579
580 if let Object::Array(arr) = dest.to_array() {
581 assert_eq!(arr[0], Object::Reference(extreme_page));
582 }
583
584 let extreme_rect = Rectangle::new(
586 Point::new(f64::MIN, f64::MIN),
587 Point::new(f64::MAX, f64::MAX),
588 );
589 let dest_rect = LinkDestination::FitR {
590 page: ObjectReference::new(1, 0),
591 rect: extreme_rect,
592 };
593
594 if let Object::Array(arr) = dest_rect.to_array() {
595 assert_eq!(arr.len(), 6);
596 assert!(matches!(arr[2], Object::Real(_)));
597 assert!(matches!(arr[3], Object::Real(_)));
598 assert!(matches!(arr[4], Object::Real(_)));
599 assert!(matches!(arr[5], Object::Real(_)));
600 }
601 }
602
603 #[test]
604 fn test_link_action_with_special_characters() {
605 let special_uri =
607 "https://example.com/search?q=hello+world&category=test%20category#section-1";
608 let uri_action = LinkAction::URI {
609 uri: special_uri.to_string(),
610 };
611 let dict = uri_action.to_dict();
612 assert_eq!(
613 dict.get("URI"),
614 Some(&Object::String(special_uri.to_string()))
615 );
616
617 let special_file = "C:\\Documents and Settings\\User\\My Documents\\file (1).pdf";
619 let launch_action = LinkAction::Launch {
620 file: special_file.to_string(),
621 };
622 let dict = launch_action.to_dict();
623 assert_eq!(
624 dict.get("F"),
625 Some(&Object::String(special_file.to_string()))
626 );
627
628 let unicode_file = "文档/документ.pdf";
630 let gotor_action = LinkAction::GoToR {
631 file: unicode_file.to_string(),
632 destination: LinkDestination::Named("Start".to_string()),
633 };
634 let dict = gotor_action.to_dict();
635 assert_eq!(
636 dict.get("F"),
637 Some(&Object::String(unicode_file.to_string()))
638 );
639 }
640
641 #[test]
642 fn test_highlight_mode_default() {
643 let default_mode = HighlightMode::default();
644 assert!(matches!(default_mode, HighlightMode::Invert));
645 assert_eq!(default_mode.pdf_name(), "I");
646 }
647
648 #[test]
649 fn test_highlight_mode_debug_clone_copy() {
650 let mode = HighlightMode::Push;
651
652 let debug_str = format!("{mode:?}");
654 assert!(debug_str.contains("Push"));
655
656 let cloned = mode;
658 assert!(matches!(cloned, HighlightMode::Push));
659
660 let copied: HighlightMode = mode;
662 assert!(matches!(copied, HighlightMode::Push));
663 }
664
665 #[test]
666 fn test_link_destination_debug_clone() {
667 let dest = LinkDestination::XYZ {
668 page: ObjectReference::new(1, 0),
669 left: Some(100.0),
670 top: Some(700.0),
671 zoom: Some(1.5),
672 };
673
674 let debug_str = format!("{dest:?}");
675 assert!(debug_str.contains("XYZ"));
676 assert!(debug_str.contains("100.0"));
677
678 let cloned = dest;
679 if let LinkDestination::XYZ {
680 left, top, zoom, ..
681 } = cloned
682 {
683 assert_eq!(left, Some(100.0));
684 assert_eq!(top, Some(700.0));
685 assert_eq!(zoom, Some(1.5));
686 }
687 }
688
689 #[test]
690 fn test_link_action_debug_clone() {
691 let action = LinkAction::URI {
692 uri: "https://test.com".to_string(),
693 };
694
695 let debug_str = format!("{action:?}");
696 assert!(debug_str.contains("URI"));
697 assert!(debug_str.contains("https://test.com"));
698
699 let cloned = action;
700 if let LinkAction::URI { uri } = cloned {
701 assert_eq!(uri, "https://test.com");
702 }
703 }
704
705 #[test]
706 fn test_link_annotation_debug_clone() {
707 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 20.0));
708 let link = LinkAnnotation::to_uri(rect, "https://example.com")
709 .with_highlight_mode(HighlightMode::Outline);
710
711 let debug_str = format!("{link:?}");
712 assert!(debug_str.contains("LinkAnnotation"));
713
714 let cloned = link;
715 assert!(matches!(cloned.highlight_mode, HighlightMode::Outline));
716 }
717
718 #[test]
719 fn test_named_action_standard_names() {
720 let standard_names = vec![
722 "NextPage",
723 "PrevPage",
724 "FirstPage",
725 "LastPage",
726 "GoBack",
727 "GoForward",
728 "GoToPage",
729 "Find",
730 "Print",
731 "SaveAs",
732 ];
733
734 for name in standard_names {
735 let action = LinkAction::Named {
736 name: name.to_string(),
737 };
738 let dict = action.to_dict();
739 assert_eq!(dict.get("S"), Some(&Object::Name("Named".to_string())));
740 assert_eq!(dict.get("N"), Some(&Object::Name(name.to_string())));
741 }
742 }
743
744 #[test]
745 fn test_link_annotation_to_dict_complete() {
746 let rect = Rectangle::new(Point::new(100.0, 600.0), Point::new(400.0, 620.0));
747 let dest = LinkDestination::XYZ {
748 page: ObjectReference::new(10, 0),
749 left: Some(50.0),
750 top: Some(700.0),
751 zoom: Some(2.0),
752 };
753
754 let link = LinkAnnotation::new(rect, LinkAction::GoTo(dest))
755 .with_highlight_mode(HighlightMode::Push);
756
757 let mut annotation = link.to_annotation();
758 annotation.contents = Some("Click to go to page 10".to_string());
759 annotation.color = Some(Color::Rgb(0.0, 0.0, 1.0));
760
761 let dict = annotation.to_dict();
762
763 assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
765 assert_eq!(dict.get("Subtype"), Some(&Object::Name("Link".to_string())));
766 assert!(dict.get("Rect").is_some());
767 assert!(dict.get("A").is_some());
768 assert_eq!(dict.get("H"), Some(&Object::Name("P".to_string())));
769 assert!(dict.get("Contents").is_some());
770 assert!(dict.get("C").is_some());
771 assert!(!dict.contains_key("BS")); }
773
774 #[test]
775 fn test_link_destination_fit_rectangle_precision() {
776 let rect = Rectangle::new(Point::new(72.125, 144.375), Point::new(540.875, 697.625));
777
778 let dest = LinkDestination::FitR {
779 page: ObjectReference::new(1, 0),
780 rect,
781 };
782
783 if let Object::Array(arr) = dest.to_array() {
784 assert_eq!(arr[2], Object::Real(72.125));
785 assert_eq!(arr[3], Object::Real(144.375));
786 assert_eq!(arr[4], Object::Real(540.875));
787 assert_eq!(arr[5], Object::Real(697.625));
788 }
789 }
790
791 #[test]
792 fn test_empty_strings_in_actions() {
793 let empty_uri = LinkAction::URI {
795 uri: "".to_string(),
796 };
797 let dict = empty_uri.to_dict();
798 assert_eq!(dict.get("URI"), Some(&Object::String("".to_string())));
799
800 let empty_file = LinkAction::Launch {
802 file: "".to_string(),
803 };
804 let dict = empty_file.to_dict();
805 assert_eq!(dict.get("F"), Some(&Object::String("".to_string())));
806
807 let empty_named = LinkAction::Named {
809 name: "".to_string(),
810 };
811 let dict = empty_named.to_dict();
812 assert_eq!(dict.get("N"), Some(&Object::Name("".to_string())));
813 }
814
815 #[test]
816 fn test_link_annotation_convenience_methods() {
817 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 20.0));
818
819 let page_ref = ObjectReference::new(5, 0);
821 let page_link = LinkAnnotation::to_page(rect, page_ref);
822
823 if let LinkAction::GoTo(dest) = &page_link.action {
824 if let LinkDestination::Fit { page } = dest {
825 assert_eq!(*page, page_ref);
826 } else {
827 panic!("Expected Fit destination");
828 }
829 } else {
830 panic!("Expected GoTo action");
831 }
832
833 let uri = "ftp://files.example.com/document.pdf";
835 let uri_link = LinkAnnotation::to_uri(rect, uri);
836
837 if let LinkAction::URI { uri: link_uri } = &uri_link.action {
838 assert_eq!(link_uri, uri);
839 } else {
840 panic!("Expected URI action");
841 }
842 }
843}