ppt_rs/generator/
hyperlinks.rs1use crate::core::escape_xml;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum HyperlinkAction {
10 Url(String),
12 Slide(u32),
14 FirstSlide,
16 LastSlide,
18 NextSlide,
20 PreviousSlide,
22 EndShow,
24 Email { address: String, subject: Option<String> },
26 File(String),
28}
29
30impl HyperlinkAction {
31 pub fn url(url: &str) -> Self {
33 HyperlinkAction::Url(url.to_string())
34 }
35
36 pub fn slide(slide_num: u32) -> Self {
38 HyperlinkAction::Slide(slide_num)
39 }
40
41 pub fn email(address: &str) -> Self {
43 HyperlinkAction::Email {
44 address: address.to_string(),
45 subject: None,
46 }
47 }
48
49 pub fn email_with_subject(address: &str, subject: &str) -> Self {
51 HyperlinkAction::Email {
52 address: address.to_string(),
53 subject: Some(subject.to_string()),
54 }
55 }
56
57 pub fn file(path: &str) -> Self {
59 HyperlinkAction::File(path.to_string())
60 }
61
62 pub fn relationship_target(&self) -> String {
64 match self {
65 HyperlinkAction::Url(url) => url.clone(),
66 HyperlinkAction::Slide(num) => format!("slide{}.xml", num),
67 HyperlinkAction::FirstSlide => "ppaction://hlinkshowjump?jump=firstslide".to_string(),
68 HyperlinkAction::LastSlide => "ppaction://hlinkshowjump?jump=lastslide".to_string(),
69 HyperlinkAction::NextSlide => "ppaction://hlinkshowjump?jump=nextslide".to_string(),
70 HyperlinkAction::PreviousSlide => "ppaction://hlinkshowjump?jump=previousslide".to_string(),
71 HyperlinkAction::EndShow => "ppaction://hlinkshowjump?jump=endshow".to_string(),
72 HyperlinkAction::Email { address, subject } => {
73 let mut mailto = format!("mailto:{}", address);
74 if let Some(subj) = subject {
75 mailto.push_str(&format!("?subject={}", subj));
76 }
77 mailto
78 }
79 HyperlinkAction::File(path) => format!("file:///{}", path.replace('\\', "/")),
80 }
81 }
82
83 pub fn is_external(&self) -> bool {
85 matches!(
86 self,
87 HyperlinkAction::Url(_) | HyperlinkAction::Email { .. } | HyperlinkAction::File(_)
88 )
89 }
90
91 pub fn action_type(&self) -> Option<&'static str> {
93 match self {
94 HyperlinkAction::FirstSlide => Some("ppaction://hlinkshowjump?jump=firstslide"),
95 HyperlinkAction::LastSlide => Some("ppaction://hlinkshowjump?jump=lastslide"),
96 HyperlinkAction::NextSlide => Some("ppaction://hlinkshowjump?jump=nextslide"),
97 HyperlinkAction::PreviousSlide => Some("ppaction://hlinkshowjump?jump=previousslide"),
98 HyperlinkAction::EndShow => Some("ppaction://hlinkshowjump?jump=endshow"),
99 _ => None,
100 }
101 }
102}
103
104#[derive(Clone, Debug)]
106pub struct Hyperlink {
107 pub action: HyperlinkAction,
109 pub tooltip: Option<String>,
111 pub highlight_click: bool,
113 pub r_id: Option<String>,
115}
116
117impl Hyperlink {
118 pub fn new(action: HyperlinkAction) -> Self {
120 Hyperlink {
121 action,
122 tooltip: None,
123 highlight_click: true,
124 r_id: None,
125 }
126 }
127
128 pub fn url(url: &str) -> Self {
130 Self::new(HyperlinkAction::url(url))
131 }
132
133 pub fn slide(slide_num: u32) -> Self {
135 Self::new(HyperlinkAction::slide(slide_num))
136 }
137
138 pub fn email(address: &str) -> Self {
140 Self::new(HyperlinkAction::email(address))
141 }
142
143 pub fn with_tooltip(mut self, tooltip: &str) -> Self {
145 self.tooltip = Some(tooltip.to_string());
146 self
147 }
148
149 pub fn with_highlight_click(mut self, highlight: bool) -> Self {
151 self.highlight_click = highlight;
152 self
153 }
154
155 pub fn with_r_id(mut self, r_id: &str) -> Self {
157 self.r_id = Some(r_id.to_string());
158 self
159 }
160}
161
162pub fn generate_text_hyperlink_xml(hyperlink: &Hyperlink, r_id: &str) -> String {
164 let mut xml = format!(r#"<a:hlinkClick r:id="{}""#, r_id);
165
166 if let Some(tooltip) = &hyperlink.tooltip {
167 xml.push_str(&format!(r#" tooltip="{}""#, escape_xml(tooltip)));
168 }
169
170 if hyperlink.highlight_click {
171 xml.push_str(r#" highlightClick="1""#);
172 }
173
174 if let Some(action) = hyperlink.action.action_type() {
176 xml.push_str(&format!(r#" action="{}""#, action));
177 }
178
179 xml.push_str("/>");
180 xml
181}
182
183pub fn generate_shape_hyperlink_xml(hyperlink: &Hyperlink, r_id: &str) -> String {
185 let mut xml = format!(r#"<a:hlinkClick r:id="{}""#, r_id);
186
187 if let Some(tooltip) = &hyperlink.tooltip {
188 xml.push_str(&format!(r#" tooltip="{}""#, escape_xml(tooltip)));
189 }
190
191 if hyperlink.highlight_click {
192 xml.push_str(r#" highlightClick="1""#);
193 }
194
195 if let Some(action) = hyperlink.action.action_type() {
196 xml.push_str(&format!(r#" action="{}""#, action));
197 }
198
199 xml.push_str("/>");
200 xml
201}
202
203pub fn generate_hyperlink_relationship_xml(hyperlink: &Hyperlink, r_id: &str) -> String {
205 let target = hyperlink.action.relationship_target();
206 let target_mode = if hyperlink.action.is_external() {
207 r#" TargetMode="External""#
208 } else {
209 ""
210 };
211
212 format!(
213 r#"<Relationship Id="{}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="{}"{}/>"#,
214 r_id,
215 escape_xml(&target),
216 target_mode
217 )
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_hyperlink_url() {
226 let link = Hyperlink::url("https://example.com");
227 assert!(matches!(link.action, HyperlinkAction::Url(_)));
228 assert!(link.action.is_external());
229 }
230
231 #[test]
232 fn test_hyperlink_slide() {
233 let link = Hyperlink::slide(3);
234 assert!(matches!(link.action, HyperlinkAction::Slide(3)));
235 assert!(!link.action.is_external());
236 }
237
238 #[test]
239 fn test_hyperlink_email() {
240 let link = Hyperlink::email("test@example.com");
241 assert!(link.action.is_external());
242 assert!(link.action.relationship_target().starts_with("mailto:"));
243 }
244
245 #[test]
246 fn test_hyperlink_with_tooltip() {
247 let link = Hyperlink::url("https://example.com")
248 .with_tooltip("Click here");
249 assert_eq!(link.tooltip, Some("Click here".to_string()));
250 }
251
252 #[test]
253 fn test_hyperlink_action_types() {
254 assert!(HyperlinkAction::FirstSlide.action_type().is_some());
255 assert!(HyperlinkAction::LastSlide.action_type().is_some());
256 assert!(HyperlinkAction::NextSlide.action_type().is_some());
257 assert!(HyperlinkAction::PreviousSlide.action_type().is_some());
258 assert!(HyperlinkAction::EndShow.action_type().is_some());
259 assert!(HyperlinkAction::url("test").action_type().is_none());
260 }
261
262 #[test]
263 fn test_generate_text_hyperlink_xml() {
264 let link = Hyperlink::url("https://example.com")
265 .with_tooltip("Example");
266 let xml = generate_text_hyperlink_xml(&link, "rId1");
267 assert!(xml.contains("hlinkClick"));
268 assert!(xml.contains("rId1"));
269 assert!(xml.contains("Example"));
270 }
271
272 #[test]
273 fn test_generate_relationship_xml() {
274 let link = Hyperlink::url("https://example.com");
275 let xml = generate_hyperlink_relationship_xml(&link, "rId1");
276 assert!(xml.contains("Relationship"));
277 assert!(xml.contains("hyperlink"));
278 assert!(xml.contains("External"));
279 }
280
281 #[test]
282 fn test_email_with_subject() {
283 let action = HyperlinkAction::email_with_subject("test@example.com", "Hello");
284 let target = action.relationship_target();
285 assert!(target.contains("mailto:"));
286 assert!(target.contains("subject=Hello"));
287 }
288}