1use crate::error::{HwpError, Result};
2use crate::parser::record::Record;
3
4#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum HyperlinkType {
7 Url = 0,
9 Email = 1,
11 File = 2,
13 Bookmark = 3,
15 ExternalBookmark = 4,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum HyperlinkDisplay {
22 TextOnly = 0,
24 UrlOnly = 1,
26 Both = 2,
28}
29
30#[derive(Debug, Clone)]
32pub struct Hyperlink {
33 pub hyperlink_type: HyperlinkType,
35 pub display_text: String,
37 pub target_url: String,
39 pub tooltip: Option<String>,
41 pub display_mode: HyperlinkDisplay,
43 pub text_color: u32,
45 pub visited_color: u32,
47 pub underline: bool,
49 pub visited: bool,
51 pub open_in_new_window: bool,
53 pub start_position: u32,
55 pub length: u32,
57}
58
59impl Default for Hyperlink {
60 fn default() -> Self {
61 Self {
62 hyperlink_type: HyperlinkType::Url,
63 display_text: String::new(),
64 target_url: String::new(),
65 tooltip: None,
66 display_mode: HyperlinkDisplay::TextOnly,
67 text_color: 0x0000FF, visited_color: 0x800080, underline: true,
70 visited: false,
71 open_in_new_window: false,
72 start_position: 0,
73 length: 0,
74 }
75 }
76}
77
78impl Hyperlink {
79 pub fn new_url(display_text: &str, url: &str) -> Self {
81 Self {
82 hyperlink_type: HyperlinkType::Url,
83 display_text: display_text.to_string(),
84 target_url: url.to_string(),
85 length: display_text.chars().count() as u32,
86 ..Default::default()
87 }
88 }
89
90 pub fn new_email(display_text: &str, email: &str) -> Self {
92 let mailto_url = if email.starts_with("mailto:") {
93 email.to_string()
94 } else {
95 format!("mailto:{}", email)
96 };
97
98 Self {
99 hyperlink_type: HyperlinkType::Email,
100 display_text: display_text.to_string(),
101 target_url: mailto_url,
102 length: display_text.chars().count() as u32,
103 ..Default::default()
104 }
105 }
106
107 pub fn new_file(display_text: &str, file_path: &str) -> Self {
109 Self {
110 hyperlink_type: HyperlinkType::File,
111 display_text: display_text.to_string(),
112 target_url: file_path.to_string(),
113 length: display_text.chars().count() as u32,
114 ..Default::default()
115 }
116 }
117
118 pub fn new_bookmark(display_text: &str, bookmark_name: &str) -> Self {
120 Self {
121 hyperlink_type: HyperlinkType::Bookmark,
122 display_text: display_text.to_string(),
123 target_url: format!("#{}", bookmark_name),
124 length: display_text.chars().count() as u32,
125 ..Default::default()
126 }
127 }
128
129 pub fn with_position(mut self, start_position: u32) -> Self {
131 self.start_position = start_position;
132 self
133 }
134
135 pub fn with_length(mut self, length: u32) -> Self {
137 self.length = length;
138 self
139 }
140
141 pub fn with_tooltip(mut self, tooltip: &str) -> Self {
143 self.tooltip = Some(tooltip.to_string());
144 self
145 }
146
147 pub fn with_display_mode(mut self, mode: HyperlinkDisplay) -> Self {
149 self.display_mode = mode;
150 self
151 }
152
153 pub fn with_text_color(mut self, color: u32) -> Self {
155 self.text_color = color;
156 self
157 }
158
159 pub fn with_visited_color(mut self, color: u32) -> Self {
161 self.visited_color = color;
162 self
163 }
164
165 pub fn with_underline(mut self, underline: bool) -> Self {
167 self.underline = underline;
168 self
169 }
170
171 pub fn with_new_window(mut self, new_window: bool) -> Self {
173 self.open_in_new_window = new_window;
174 self
175 }
176
177 pub fn to_bytes(&self) -> Vec<u8> {
179 use crate::utils::encoding::string_to_utf16le;
180 use byteorder::{LittleEndian, WriteBytesExt};
181 use std::io::{Cursor, Write};
182
183 let mut data = Vec::new();
184 let mut writer = Cursor::new(&mut data);
185
186 writer.write_u8(self.hyperlink_type as u8).unwrap();
188 writer.write_u8(self.display_mode as u8).unwrap();
189 writer.write_u32::<LittleEndian>(self.text_color).unwrap();
190 writer
191 .write_u32::<LittleEndian>(self.visited_color)
192 .unwrap();
193
194 let mut flags = 0u8;
196 if self.underline {
197 flags |= 0x01;
198 }
199 if self.visited {
200 flags |= 0x02;
201 }
202 if self.open_in_new_window {
203 flags |= 0x04;
204 }
205 writer.write_u8(flags).unwrap();
206
207 writer
209 .write_u32::<LittleEndian>(self.start_position)
210 .unwrap();
211 writer.write_u32::<LittleEndian>(self.length).unwrap();
212
213 let display_text_utf16 = string_to_utf16le(&self.display_text);
215 writer
216 .write_u16::<LittleEndian>(display_text_utf16.len() as u16 / 2)
217 .unwrap();
218 writer.write_all(&display_text_utf16).unwrap();
219
220 let target_url_utf16 = string_to_utf16le(&self.target_url);
222 writer
223 .write_u16::<LittleEndian>(target_url_utf16.len() as u16 / 2)
224 .unwrap();
225 writer.write_all(&target_url_utf16).unwrap();
226
227 if let Some(tooltip) = &self.tooltip {
229 let tooltip_utf16 = string_to_utf16le(tooltip);
230 writer
231 .write_u16::<LittleEndian>(tooltip_utf16.len() as u16 / 2)
232 .unwrap();
233 writer.write_all(&tooltip_utf16).unwrap();
234 } else {
235 writer.write_u16::<LittleEndian>(0).unwrap();
236 }
237
238 data
239 }
240
241 pub fn from_record(record: &Record) -> Result<Self> {
243 let data = &record.data;
244
245 if data.len() < 19 {
247 return Err(HwpError::InvalidFormat(
248 "Record too small for hyperlink".to_string(),
249 ));
250 }
251
252 let mut offset = 0;
253
254 let hyperlink_type = match data[offset] {
256 0 => HyperlinkType::Url,
257 1 => HyperlinkType::Email,
258 2 => HyperlinkType::File,
259 3 => HyperlinkType::Bookmark,
260 4 => HyperlinkType::ExternalBookmark,
261 _ => HyperlinkType::Url,
262 };
263 offset += 1;
264
265 let display_mode = match data[offset] {
267 0 => HyperlinkDisplay::TextOnly,
268 1 => HyperlinkDisplay::UrlOnly,
269 2 => HyperlinkDisplay::Both,
270 _ => HyperlinkDisplay::TextOnly,
271 };
272 offset += 1;
273
274 let text_color = u32::from_le_bytes([
276 data[offset],
277 data[offset + 1],
278 data[offset + 2],
279 data[offset + 3],
280 ]);
281 offset += 4;
282
283 let visited_color = u32::from_le_bytes([
285 data[offset],
286 data[offset + 1],
287 data[offset + 2],
288 data[offset + 3],
289 ]);
290 offset += 4;
291
292 let flags = data[offset];
294 let underline = (flags & 0x01) != 0;
295 let visited = (flags & 0x02) != 0;
296 let open_in_new_window = (flags & 0x04) != 0;
297 offset += 1;
298
299 let start_position = u32::from_le_bytes([
301 data[offset],
302 data[offset + 1],
303 data[offset + 2],
304 data[offset + 3],
305 ]);
306 offset += 4;
307
308 let length = u32::from_le_bytes([
310 data[offset],
311 data[offset + 1],
312 data[offset + 2],
313 data[offset + 3],
314 ]);
315 offset += 4;
316
317 if offset + 2 > data.len() {
319 return Err(HwpError::InvalidFormat(
320 "Not enough data for display text length".to_string(),
321 ));
322 }
323 let display_text_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
324 offset += 2;
325
326 let mut display_text = String::new();
328 if display_text_len > 0 {
329 if offset + display_text_len * 2 > data.len() {
330 return Err(HwpError::InvalidFormat(
331 "Not enough data for display text".to_string(),
332 ));
333 }
334 let mut utf16_chars = Vec::new();
335 for i in 0..display_text_len {
336 let char_offset = offset + i * 2;
337 let char_val = u16::from_le_bytes([data[char_offset], data[char_offset + 1]]);
338 utf16_chars.push(char_val);
339 }
340 display_text = String::from_utf16(&utf16_chars).map_err(|_| {
341 HwpError::InvalidFormat("Invalid UTF-16 in display text".to_string())
342 })?;
343 offset += display_text_len * 2;
344 }
345
346 if offset + 2 > data.len() {
348 return Err(HwpError::InvalidFormat(
349 "Not enough data for URL length".to_string(),
350 ));
351 }
352 let target_url_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
353 offset += 2;
354
355 let mut target_url = String::new();
357 if target_url_len > 0 {
358 if offset + target_url_len * 2 > data.len() {
359 return Err(HwpError::InvalidFormat(
360 "Not enough data for target URL".to_string(),
361 ));
362 }
363 let mut utf16_chars = Vec::new();
364 for i in 0..target_url_len {
365 let char_offset = offset + i * 2;
366 let char_val = u16::from_le_bytes([data[char_offset], data[char_offset + 1]]);
367 utf16_chars.push(char_val);
368 }
369 target_url = String::from_utf16(&utf16_chars)
370 .map_err(|_| HwpError::InvalidFormat("Invalid UTF-16 in target URL".to_string()))?;
371 offset += target_url_len * 2;
372 }
373
374 let tooltip = if offset + 2 <= data.len() {
376 let tooltip_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
377 offset += 2;
378
379 if tooltip_len > 0 && offset + tooltip_len * 2 <= data.len() {
380 let mut utf16_chars = Vec::new();
381 for i in 0..tooltip_len {
382 let char_offset = offset + i * 2;
383 let char_val = u16::from_le_bytes([data[char_offset], data[char_offset + 1]]);
384 utf16_chars.push(char_val);
385 }
386 String::from_utf16(&utf16_chars).ok()
387 } else {
388 None
389 }
390 } else {
391 None
392 };
393
394 Ok(Self {
395 hyperlink_type,
396 display_text,
397 target_url,
398 tooltip,
399 display_mode,
400 text_color,
401 visited_color,
402 underline,
403 visited,
404 open_in_new_window,
405 start_position,
406 length,
407 })
408 }
409}
410
411impl Hyperlink {
413 pub fn web_link(text: &str, url: &str) -> Self {
415 Self::new_url(text, url)
416 .with_text_color(0x0000FF) .with_underline(true)
418 }
419
420 pub fn email_link(text: &str, email: &str) -> Self {
422 Self::new_email(text, email)
423 .with_text_color(0x0000FF) .with_underline(true)
425 }
426
427 pub fn file_link(text: &str, file_path: &str) -> Self {
429 Self::new_file(text, file_path)
430 .with_text_color(0x008000) .with_underline(true)
432 }
433
434 pub fn internal_link(text: &str, bookmark: &str) -> Self {
436 Self::new_bookmark(text, bookmark)
437 .with_text_color(0x800080) .with_underline(true)
439 }
440
441 pub fn plain_link(text: &str, url: &str) -> Self {
443 Self::new_url(text, url)
444 .with_text_color(0x0000FF) .with_underline(false)
446 }
447
448 pub fn external_link(text: &str, url: &str) -> Self {
450 Self::new_url(text, url)
451 .with_text_color(0x0000FF) .with_underline(true)
453 .with_new_window(true)
454 }
455}