use crate::core::escape_xml;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HyperlinkAction {
Url(String),
Slide(u32),
FirstSlide,
LastSlide,
NextSlide,
PreviousSlide,
EndShow,
Email { address: String, subject: Option<String> },
File(String),
}
impl HyperlinkAction {
pub fn url(url: &str) -> Self {
HyperlinkAction::Url(url.to_string())
}
pub fn slide(slide_num: u32) -> Self {
HyperlinkAction::Slide(slide_num)
}
pub fn email(address: &str) -> Self {
HyperlinkAction::Email {
address: address.to_string(),
subject: None,
}
}
pub fn email_with_subject(address: &str, subject: &str) -> Self {
HyperlinkAction::Email {
address: address.to_string(),
subject: Some(subject.to_string()),
}
}
pub fn file(path: &str) -> Self {
HyperlinkAction::File(path.to_string())
}
pub fn relationship_target(&self) -> String {
match self {
HyperlinkAction::Url(url) => url.clone(),
HyperlinkAction::Slide(num) => format!("slide{}.xml", num),
HyperlinkAction::FirstSlide => "ppaction://hlinkshowjump?jump=firstslide".to_string(),
HyperlinkAction::LastSlide => "ppaction://hlinkshowjump?jump=lastslide".to_string(),
HyperlinkAction::NextSlide => "ppaction://hlinkshowjump?jump=nextslide".to_string(),
HyperlinkAction::PreviousSlide => "ppaction://hlinkshowjump?jump=previousslide".to_string(),
HyperlinkAction::EndShow => "ppaction://hlinkshowjump?jump=endshow".to_string(),
HyperlinkAction::Email { address, subject } => {
let mut mailto = format!("mailto:{}", address);
if let Some(subj) = subject {
mailto.push_str(&format!("?subject={}", subj));
}
mailto
}
HyperlinkAction::File(path) => format!("file:///{}", path.replace('\\', "/")),
}
}
pub fn is_external(&self) -> bool {
matches!(
self,
HyperlinkAction::Url(_) | HyperlinkAction::Email { .. } | HyperlinkAction::File(_)
)
}
pub fn action_type(&self) -> Option<&'static str> {
match self {
HyperlinkAction::FirstSlide => Some("ppaction://hlinkshowjump?jump=firstslide"),
HyperlinkAction::LastSlide => Some("ppaction://hlinkshowjump?jump=lastslide"),
HyperlinkAction::NextSlide => Some("ppaction://hlinkshowjump?jump=nextslide"),
HyperlinkAction::PreviousSlide => Some("ppaction://hlinkshowjump?jump=previousslide"),
HyperlinkAction::EndShow => Some("ppaction://hlinkshowjump?jump=endshow"),
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub struct Hyperlink {
pub action: HyperlinkAction,
pub tooltip: Option<String>,
pub highlight_click: bool,
pub r_id: Option<String>,
}
impl Hyperlink {
pub fn new(action: HyperlinkAction) -> Self {
Hyperlink {
action,
tooltip: None,
highlight_click: true,
r_id: None,
}
}
pub fn url(url: &str) -> Self {
Self::new(HyperlinkAction::url(url))
}
pub fn slide(slide_num: u32) -> Self {
Self::new(HyperlinkAction::slide(slide_num))
}
pub fn email(address: &str) -> Self {
Self::new(HyperlinkAction::email(address))
}
pub fn with_tooltip(mut self, tooltip: &str) -> Self {
self.tooltip = Some(tooltip.to_string());
self
}
pub fn with_highlight_click(mut self, highlight: bool) -> Self {
self.highlight_click = highlight;
self
}
pub fn with_r_id(mut self, r_id: &str) -> Self {
self.r_id = Some(r_id.to_string());
self
}
}
pub fn generate_text_hyperlink_xml(hyperlink: &Hyperlink, r_id: &str) -> String {
let mut xml = format!(r#"<a:hlinkClick r:id="{}""#, r_id);
if let Some(tooltip) = &hyperlink.tooltip {
xml.push_str(&format!(r#" tooltip="{}""#, escape_xml(tooltip)));
}
if hyperlink.highlight_click {
xml.push_str(r#" highlightClick="1""#);
}
if let Some(action) = hyperlink.action.action_type() {
xml.push_str(&format!(r#" action="{}""#, action));
}
xml.push_str("/>");
xml
}
pub fn generate_shape_hyperlink_xml(hyperlink: &Hyperlink, r_id: &str) -> String {
let mut xml = format!(r#"<a:hlinkClick r:id="{}""#, r_id);
if let Some(tooltip) = &hyperlink.tooltip {
xml.push_str(&format!(r#" tooltip="{}""#, escape_xml(tooltip)));
}
if hyperlink.highlight_click {
xml.push_str(r#" highlightClick="1""#);
}
if let Some(action) = hyperlink.action.action_type() {
xml.push_str(&format!(r#" action="{}""#, action));
}
xml.push_str("/>");
xml
}
pub fn generate_hyperlink_relationship_xml(hyperlink: &Hyperlink, r_id: &str) -> String {
let target = hyperlink.action.relationship_target();
let target_mode = if hyperlink.action.is_external() {
r#" TargetMode="External""#
} else {
""
};
format!(
r#"<Relationship Id="{}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="{}"{}/>"#,
r_id,
escape_xml(&target),
target_mode
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hyperlink_url() {
let link = Hyperlink::url("https://example.com");
assert!(matches!(link.action, HyperlinkAction::Url(_)));
assert!(link.action.is_external());
}
#[test]
fn test_hyperlink_slide() {
let link = Hyperlink::slide(3);
assert!(matches!(link.action, HyperlinkAction::Slide(3)));
assert!(!link.action.is_external());
}
#[test]
fn test_hyperlink_email() {
let link = Hyperlink::email("test@example.com");
assert!(link.action.is_external());
assert!(link.action.relationship_target().starts_with("mailto:"));
}
#[test]
fn test_hyperlink_with_tooltip() {
let link = Hyperlink::url("https://example.com")
.with_tooltip("Click here");
assert_eq!(link.tooltip, Some("Click here".to_string()));
}
#[test]
fn test_hyperlink_action_types() {
assert!(HyperlinkAction::FirstSlide.action_type().is_some());
assert!(HyperlinkAction::LastSlide.action_type().is_some());
assert!(HyperlinkAction::NextSlide.action_type().is_some());
assert!(HyperlinkAction::PreviousSlide.action_type().is_some());
assert!(HyperlinkAction::EndShow.action_type().is_some());
assert!(HyperlinkAction::url("test").action_type().is_none());
}
#[test]
fn test_generate_text_hyperlink_xml() {
let link = Hyperlink::url("https://example.com")
.with_tooltip("Example");
let xml = generate_text_hyperlink_xml(&link, "rId1");
assert!(xml.contains("hlinkClick"));
assert!(xml.contains("rId1"));
assert!(xml.contains("Example"));
}
#[test]
fn test_generate_relationship_xml() {
let link = Hyperlink::url("https://example.com");
let xml = generate_hyperlink_relationship_xml(&link, "rId1");
assert!(xml.contains("Relationship"));
assert!(xml.contains("hyperlink"));
assert!(xml.contains("External"));
}
#[test]
fn test_email_with_subject() {
let action = HyperlinkAction::email_with_subject("test@example.com", "Hello");
let target = action.relationship_target();
assert!(target.contains("mailto:"));
assert!(target.contains("subject=Hello"));
}
}