1use sheetkit_xml::relationships::{rel_types, Relationship, Relationships};
8use sheetkit_xml::worksheet::{Hyperlink, Hyperlinks, WorksheetXml};
9
10use crate::error::Result;
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum HyperlinkType {
15 External(String),
17 Internal(String),
19 Email(String),
21}
22
23#[derive(Debug, Clone, PartialEq)]
25pub struct HyperlinkInfo {
26 pub link_type: HyperlinkType,
28 pub display: Option<String>,
30 pub tooltip: Option<String>,
32}
33
34pub fn set_cell_hyperlink(
43 ws: &mut WorksheetXml,
44 rels: &mut Relationships,
45 cell: &str,
46 link: &HyperlinkType,
47 display: Option<&str>,
48 tooltip: Option<&str>,
49) -> Result<()> {
50 delete_cell_hyperlink(ws, rels, cell)?;
52
53 let hyperlinks = ws
54 .hyperlinks
55 .get_or_insert_with(|| Hyperlinks { hyperlinks: vec![] });
56
57 match link {
58 HyperlinkType::External(url) | HyperlinkType::Email(url) => {
59 let rid = next_rel_id(rels);
60 rels.relationships.push(Relationship {
61 id: rid.clone(),
62 rel_type: rel_types::HYPERLINK.to_string(),
63 target: url.clone(),
64 target_mode: Some("External".to_string()),
65 });
66 hyperlinks.hyperlinks.push(Hyperlink {
67 reference: cell.to_string(),
68 r_id: Some(rid),
69 location: None,
70 display: display.map(|s| s.to_string()),
71 tooltip: tooltip.map(|s| s.to_string()),
72 });
73 }
74 HyperlinkType::Internal(location) => {
75 hyperlinks.hyperlinks.push(Hyperlink {
76 reference: cell.to_string(),
77 r_id: None,
78 location: Some(location.clone()),
79 display: display.map(|s| s.to_string()),
80 tooltip: tooltip.map(|s| s.to_string()),
81 });
82 }
83 }
84
85 Ok(())
86}
87
88pub fn get_cell_hyperlink(
92 ws: &WorksheetXml,
93 rels: &Relationships,
94 cell: &str,
95) -> Result<Option<HyperlinkInfo>> {
96 let hyperlinks = match &ws.hyperlinks {
97 Some(h) => h,
98 None => return Ok(None),
99 };
100
101 let hl = match hyperlinks.hyperlinks.iter().find(|h| h.reference == cell) {
102 Some(h) => h,
103 None => return Ok(None),
104 };
105
106 let link_type = if let Some(ref rid) = hl.r_id {
107 let rel = rels.relationships.iter().find(|r| r.id == *rid);
109 match rel {
110 Some(r) => {
111 let target = &r.target;
112 if target.starts_with("mailto:") {
113 HyperlinkType::Email(target.clone())
114 } else {
115 HyperlinkType::External(target.clone())
116 }
117 }
118 None => {
119 HyperlinkType::External(String::new())
121 }
122 }
123 } else if let Some(ref location) = hl.location {
124 HyperlinkType::Internal(location.clone())
125 } else {
126 return Ok(None);
128 };
129
130 Ok(Some(HyperlinkInfo {
131 link_type,
132 display: hl.display.clone(),
133 tooltip: hl.tooltip.clone(),
134 }))
135}
136
137pub fn delete_cell_hyperlink(
142 ws: &mut WorksheetXml,
143 rels: &mut Relationships,
144 cell: &str,
145) -> Result<()> {
146 let hyperlinks = match &mut ws.hyperlinks {
147 Some(h) => h,
148 None => return Ok(()),
149 };
150
151 let removed: Vec<Hyperlink> = hyperlinks
153 .hyperlinks
154 .extract_if(.., |h| h.reference == cell)
155 .collect();
156
157 for hl in &removed {
159 if let Some(ref rid) = hl.r_id {
160 rels.relationships.retain(|r| r.id != *rid);
161 }
162 }
163
164 if hyperlinks.hyperlinks.is_empty() {
166 ws.hyperlinks = None;
167 }
168
169 Ok(())
170}
171
172fn next_rel_id(rels: &Relationships) -> String {
174 let max = rels
175 .relationships
176 .iter()
177 .filter_map(|r| r.id.strip_prefix("rId").and_then(|n| n.parse::<u32>().ok()))
178 .max()
179 .unwrap_or(0);
180 format!("rId{}", max + 1)
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use sheetkit_xml::namespaces;
187
188 fn empty_rels() -> Relationships {
189 Relationships {
190 xmlns: namespaces::PACKAGE_RELATIONSHIPS.to_string(),
191 relationships: vec![],
192 }
193 }
194
195 #[test]
196 fn test_set_external_hyperlink() {
197 let mut ws = WorksheetXml::default();
198 let mut rels = empty_rels();
199
200 set_cell_hyperlink(
201 &mut ws,
202 &mut rels,
203 "A1",
204 &HyperlinkType::External("https://example.com".to_string()),
205 None,
206 None,
207 )
208 .unwrap();
209
210 let hls = ws.hyperlinks.as_ref().unwrap();
212 assert_eq!(hls.hyperlinks.len(), 1);
213 assert_eq!(hls.hyperlinks[0].reference, "A1");
214 assert!(hls.hyperlinks[0].r_id.is_some());
215 assert!(hls.hyperlinks[0].location.is_none());
216
217 assert_eq!(rels.relationships.len(), 1);
219 assert_eq!(rels.relationships[0].target, "https://example.com");
220 assert_eq!(
221 rels.relationships[0].target_mode,
222 Some("External".to_string())
223 );
224 assert_eq!(rels.relationships[0].rel_type, rel_types::HYPERLINK);
225 }
226
227 #[test]
228 fn test_set_internal_hyperlink() {
229 let mut ws = WorksheetXml::default();
230 let mut rels = empty_rels();
231
232 set_cell_hyperlink(
233 &mut ws,
234 &mut rels,
235 "B2",
236 &HyperlinkType::Internal("Sheet2!A1".to_string()),
237 None,
238 None,
239 )
240 .unwrap();
241
242 let hls = ws.hyperlinks.as_ref().unwrap();
243 assert_eq!(hls.hyperlinks.len(), 1);
244 assert_eq!(hls.hyperlinks[0].reference, "B2");
245 assert!(hls.hyperlinks[0].r_id.is_none());
246 assert_eq!(hls.hyperlinks[0].location, Some("Sheet2!A1".to_string()));
247
248 assert!(rels.relationships.is_empty());
250 }
251
252 #[test]
253 fn test_set_email_hyperlink() {
254 let mut ws = WorksheetXml::default();
255 let mut rels = empty_rels();
256
257 set_cell_hyperlink(
258 &mut ws,
259 &mut rels,
260 "C3",
261 &HyperlinkType::Email("mailto:user@example.com".to_string()),
262 None,
263 None,
264 )
265 .unwrap();
266
267 let hls = ws.hyperlinks.as_ref().unwrap();
268 assert_eq!(hls.hyperlinks.len(), 1);
269 assert!(hls.hyperlinks[0].r_id.is_some());
270
271 assert_eq!(rels.relationships.len(), 1);
272 assert_eq!(rels.relationships[0].target, "mailto:user@example.com");
273 assert_eq!(
274 rels.relationships[0].target_mode,
275 Some("External".to_string())
276 );
277 }
278
279 #[test]
280 fn test_get_hyperlink_external() {
281 let mut ws = WorksheetXml::default();
282 let mut rels = empty_rels();
283
284 set_cell_hyperlink(
285 &mut ws,
286 &mut rels,
287 "A1",
288 &HyperlinkType::External("https://rust-lang.org".to_string()),
289 Some("Rust"),
290 Some("Visit Rust"),
291 )
292 .unwrap();
293
294 let info = get_cell_hyperlink(&ws, &rels, "A1").unwrap().unwrap();
295 assert_eq!(
296 info.link_type,
297 HyperlinkType::External("https://rust-lang.org".to_string())
298 );
299 assert_eq!(info.display, Some("Rust".to_string()));
300 assert_eq!(info.tooltip, Some("Visit Rust".to_string()));
301 }
302
303 #[test]
304 fn test_get_hyperlink_internal() {
305 let mut ws = WorksheetXml::default();
306 let mut rels = empty_rels();
307
308 set_cell_hyperlink(
309 &mut ws,
310 &mut rels,
311 "D4",
312 &HyperlinkType::Internal("Summary!B10".to_string()),
313 Some("Go to Summary"),
314 None,
315 )
316 .unwrap();
317
318 let info = get_cell_hyperlink(&ws, &rels, "D4").unwrap().unwrap();
319 assert_eq!(
320 info.link_type,
321 HyperlinkType::Internal("Summary!B10".to_string())
322 );
323 assert_eq!(info.display, Some("Go to Summary".to_string()));
324 assert!(info.tooltip.is_none());
325 }
326
327 #[test]
328 fn test_get_hyperlink_email() {
329 let mut ws = WorksheetXml::default();
330 let mut rels = empty_rels();
331
332 set_cell_hyperlink(
333 &mut ws,
334 &mut rels,
335 "A1",
336 &HyperlinkType::Email("mailto:test@test.com".to_string()),
337 None,
338 None,
339 )
340 .unwrap();
341
342 let info = get_cell_hyperlink(&ws, &rels, "A1").unwrap().unwrap();
343 assert_eq!(
344 info.link_type,
345 HyperlinkType::Email("mailto:test@test.com".to_string())
346 );
347 }
348
349 #[test]
350 fn test_delete_hyperlink() {
351 let mut ws = WorksheetXml::default();
352 let mut rels = empty_rels();
353
354 set_cell_hyperlink(
355 &mut ws,
356 &mut rels,
357 "A1",
358 &HyperlinkType::External("https://example.com".to_string()),
359 None,
360 None,
361 )
362 .unwrap();
363
364 assert!(ws.hyperlinks.is_some());
365 assert_eq!(rels.relationships.len(), 1);
366
367 delete_cell_hyperlink(&mut ws, &mut rels, "A1").unwrap();
368
369 assert!(ws.hyperlinks.is_none());
371 assert!(rels.relationships.is_empty());
373 }
374
375 #[test]
376 fn test_delete_internal_hyperlink() {
377 let mut ws = WorksheetXml::default();
378 let mut rels = empty_rels();
379
380 set_cell_hyperlink(
381 &mut ws,
382 &mut rels,
383 "A1",
384 &HyperlinkType::Internal("Sheet2!A1".to_string()),
385 None,
386 None,
387 )
388 .unwrap();
389
390 delete_cell_hyperlink(&mut ws, &mut rels, "A1").unwrap();
391
392 assert!(ws.hyperlinks.is_none());
393 assert!(rels.relationships.is_empty());
394 }
395
396 #[test]
397 fn test_hyperlink_with_display_and_tooltip() {
398 let mut ws = WorksheetXml::default();
399 let mut rels = empty_rels();
400
401 set_cell_hyperlink(
402 &mut ws,
403 &mut rels,
404 "A1",
405 &HyperlinkType::External("https://example.com".to_string()),
406 Some("Click here"),
407 Some("Opens example.com"),
408 )
409 .unwrap();
410
411 let hls = ws.hyperlinks.as_ref().unwrap();
412 assert_eq!(hls.hyperlinks[0].display, Some("Click here".to_string()));
413 assert_eq!(
414 hls.hyperlinks[0].tooltip,
415 Some("Opens example.com".to_string())
416 );
417
418 let info = get_cell_hyperlink(&ws, &rels, "A1").unwrap().unwrap();
419 assert_eq!(info.display, Some("Click here".to_string()));
420 assert_eq!(info.tooltip, Some("Opens example.com".to_string()));
421 }
422
423 #[test]
424 fn test_overwrite_hyperlink() {
425 let mut ws = WorksheetXml::default();
426 let mut rels = empty_rels();
427
428 set_cell_hyperlink(
430 &mut ws,
431 &mut rels,
432 "A1",
433 &HyperlinkType::External("https://old.com".to_string()),
434 None,
435 None,
436 )
437 .unwrap();
438
439 set_cell_hyperlink(
441 &mut ws,
442 &mut rels,
443 "A1",
444 &HyperlinkType::External("https://new.com".to_string()),
445 Some("New Link"),
446 None,
447 )
448 .unwrap();
449
450 let hls = ws.hyperlinks.as_ref().unwrap();
452 assert_eq!(hls.hyperlinks.len(), 1);
453
454 assert_eq!(rels.relationships.len(), 1);
456 assert_eq!(rels.relationships[0].target, "https://new.com");
457
458 let info = get_cell_hyperlink(&ws, &rels, "A1").unwrap().unwrap();
459 assert_eq!(
460 info.link_type,
461 HyperlinkType::External("https://new.com".to_string())
462 );
463 assert_eq!(info.display, Some("New Link".to_string()));
464 }
465
466 #[test]
467 fn test_overwrite_external_with_internal() {
468 let mut ws = WorksheetXml::default();
469 let mut rels = empty_rels();
470
471 set_cell_hyperlink(
472 &mut ws,
473 &mut rels,
474 "A1",
475 &HyperlinkType::External("https://example.com".to_string()),
476 None,
477 None,
478 )
479 .unwrap();
480
481 assert_eq!(rels.relationships.len(), 1);
482
483 set_cell_hyperlink(
485 &mut ws,
486 &mut rels,
487 "A1",
488 &HyperlinkType::Internal("Sheet2!A1".to_string()),
489 None,
490 None,
491 )
492 .unwrap();
493
494 assert!(rels.relationships.is_empty());
496
497 let hls = ws.hyperlinks.as_ref().unwrap();
498 assert_eq!(hls.hyperlinks.len(), 1);
499 assert!(hls.hyperlinks[0].r_id.is_none());
500 assert_eq!(hls.hyperlinks[0].location, Some("Sheet2!A1".to_string()));
501 }
502
503 #[test]
504 fn test_get_nonexistent_hyperlink() {
505 let ws = WorksheetXml::default();
506 let rels = empty_rels();
507
508 let result = get_cell_hyperlink(&ws, &rels, "Z99").unwrap();
509 assert!(result.is_none());
510 }
511
512 #[test]
513 fn test_get_nonexistent_hyperlink_with_empty_container() {
514 let mut ws = WorksheetXml::default();
515 ws.hyperlinks = Some(Hyperlinks { hyperlinks: vec![] });
516 let rels = empty_rels();
517
518 let result = get_cell_hyperlink(&ws, &rels, "A1").unwrap();
519 assert!(result.is_none());
520 }
521
522 #[test]
523 fn test_delete_nonexistent_hyperlink() {
524 let mut ws = WorksheetXml::default();
525 let mut rels = empty_rels();
526
527 delete_cell_hyperlink(&mut ws, &mut rels, "A1").unwrap();
529 assert!(ws.hyperlinks.is_none());
530 }
531
532 #[test]
533 fn test_multiple_hyperlinks() {
534 let mut ws = WorksheetXml::default();
535 let mut rels = empty_rels();
536
537 set_cell_hyperlink(
538 &mut ws,
539 &mut rels,
540 "A1",
541 &HyperlinkType::External("https://example.com".to_string()),
542 Some("Example"),
543 None,
544 )
545 .unwrap();
546
547 set_cell_hyperlink(
548 &mut ws,
549 &mut rels,
550 "B1",
551 &HyperlinkType::Internal("Sheet2!A1".to_string()),
552 Some("Sheet 2"),
553 None,
554 )
555 .unwrap();
556
557 set_cell_hyperlink(
558 &mut ws,
559 &mut rels,
560 "C1",
561 &HyperlinkType::Email("mailto:info@example.com".to_string()),
562 Some("Email Us"),
563 Some("Send email"),
564 )
565 .unwrap();
566
567 let hls = ws.hyperlinks.as_ref().unwrap();
568 assert_eq!(hls.hyperlinks.len(), 3);
569
570 assert_eq!(rels.relationships.len(), 2);
572
573 let a1 = get_cell_hyperlink(&ws, &rels, "A1").unwrap().unwrap();
575 assert_eq!(
576 a1.link_type,
577 HyperlinkType::External("https://example.com".to_string())
578 );
579 assert_eq!(a1.display, Some("Example".to_string()));
580
581 let b1 = get_cell_hyperlink(&ws, &rels, "B1").unwrap().unwrap();
582 assert_eq!(
583 b1.link_type,
584 HyperlinkType::Internal("Sheet2!A1".to_string())
585 );
586 assert_eq!(b1.display, Some("Sheet 2".to_string()));
587
588 let c1 = get_cell_hyperlink(&ws, &rels, "C1").unwrap().unwrap();
589 assert_eq!(
590 c1.link_type,
591 HyperlinkType::Email("mailto:info@example.com".to_string())
592 );
593 assert_eq!(c1.display, Some("Email Us".to_string()));
594 assert_eq!(c1.tooltip, Some("Send email".to_string()));
595 }
596
597 #[test]
598 fn test_delete_one_of_multiple() {
599 let mut ws = WorksheetXml::default();
600 let mut rels = empty_rels();
601
602 set_cell_hyperlink(
603 &mut ws,
604 &mut rels,
605 "A1",
606 &HyperlinkType::External("https://a.com".to_string()),
607 None,
608 None,
609 )
610 .unwrap();
611
612 set_cell_hyperlink(
613 &mut ws,
614 &mut rels,
615 "B1",
616 &HyperlinkType::External("https://b.com".to_string()),
617 None,
618 None,
619 )
620 .unwrap();
621
622 assert_eq!(ws.hyperlinks.as_ref().unwrap().hyperlinks.len(), 2);
623 assert_eq!(rels.relationships.len(), 2);
624
625 delete_cell_hyperlink(&mut ws, &mut rels, "A1").unwrap();
627
628 let hls = ws.hyperlinks.as_ref().unwrap();
629 assert_eq!(hls.hyperlinks.len(), 1);
630 assert_eq!(hls.hyperlinks[0].reference, "B1");
631
632 assert_eq!(rels.relationships.len(), 1);
634 assert_eq!(rels.relationships[0].target, "https://b.com");
635 }
636
637 #[test]
638 fn test_next_rel_id_empty() {
639 let rels = empty_rels();
640 assert_eq!(next_rel_id(&rels), "rId1");
641 }
642
643 #[test]
644 fn test_next_rel_id_with_existing() {
645 let rels = Relationships {
646 xmlns: namespaces::PACKAGE_RELATIONSHIPS.to_string(),
647 relationships: vec![
648 Relationship {
649 id: "rId1".to_string(),
650 rel_type: rel_types::HYPERLINK.to_string(),
651 target: "https://a.com".to_string(),
652 target_mode: Some("External".to_string()),
653 },
654 Relationship {
655 id: "rId3".to_string(),
656 rel_type: rel_types::HYPERLINK.to_string(),
657 target: "https://b.com".to_string(),
658 target_mode: Some("External".to_string()),
659 },
660 ],
661 };
662 assert_eq!(next_rel_id(&rels), "rId4");
663 }
664}