use regex::Regex;
#[cfg(feature = "serde")]
use serde::Serialize;
use crate::{
common_types::{Coordinate, Dimension},
packaging::relationship::{
raw_target_for_id, rel_for_id, XlsxRelationships, EXTERNAL_TARGET_MODE,
},
raw::{
drawing::text::hyperlink_on_event::XlsxHyperlinkOnEvent,
spreadsheet::{
sheet::worksheet::hyperlink::XlsxHyperlink, workbook::defined_name::XlsxDefinedNames,
},
},
};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Hyperlink {
Inernal(InternalHyperlink),
External(ExternalHyperlink),
}
impl Hyperlink {
pub(crate) fn from_raw(
hyperlink: XlsxHyperlink,
worksheet_rels: XlsxRelationships,
defined_names: XlsxDefinedNames,
) -> Option<Self> {
if let Some(r_id) = hyperlink.r_id {
if let Some(v) = raw_target_for_id(&worksheet_rels.clone(), &r_id) {
return Some(Self::External(ExternalHyperlink::from_string(&v)));
}
}
if let Some(location) = hyperlink.location {
let target_name: XlsxDefinedNames = defined_names
.into_iter()
.filter(|n| n.name == Some(location.clone()) && n.value.is_some())
.collect();
if let Some(target_name) = target_name.first() {
return Some(Self::Inernal(InternalHyperlink::from_location_string(
&target_name.value.clone().unwrap_or("".to_string()),
)));
}
return Some(Self::Inernal(InternalHyperlink::from_location_string(
&location,
)));
}
return None;
}
#[allow(dead_code)]
pub(crate) fn from_hlink_event(
raw: Option<XlsxHyperlinkOnEvent>,
drawing_rels: XlsxRelationships,
defined_names: XlsxDefinedNames,
) -> Option<Self> {
let Some(raw) = raw else { return None };
let Some(id) = raw.id else { return None };
let Some(rel) = rel_for_id(&drawing_rels.clone(), &id) else {
return None;
};
let target = rel.target.to_string();
if rel.target_mode == Some(EXTERNAL_TARGET_MODE.to_string()) {
if let Some(invalid_uri) = raw.invalid_url {
return Some(Self::External(ExternalHyperlink::Url(invalid_uri)));
} else {
return Some(Self::External(ExternalHyperlink::from_string(&target)));
}
}
let target_name: XlsxDefinedNames = defined_names
.into_iter()
.filter(|n| n.name == Some(target.clone()) && n.value.is_some())
.collect();
if let Some(target_name) = target_name.first() {
return Some(Self::Inernal(InternalHyperlink::from_location_string(
&target_name.value.clone().unwrap_or("".to_string()),
)));
}
return Some(Self::Inernal(InternalHyperlink::from_location_string(
&target,
)));
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct InternalHyperlink {
pub sheet_name: String,
pub cell_range: Dimension,
}
impl InternalHyperlink {
pub(crate) fn from_location_string(location: &str) -> Self {
let location = location.trim_start_matches("#");
let location_trimmed = location.trim_start_matches("'").trim_end_matches("'");
let default = Self {
sheet_name: location_trimmed.to_string(),
cell_range: Dimension {
start: Coordinate { row: 1, col: 1 },
end: Coordinate { row: 1, col: 1 },
},
};
let Ok(re) = Regex::new(r"('?)(?<name>.+?)('?)!(?<ref>.*?)$") else {
return default;
};
if let Some(caps) = re.captures(location) {
let name = caps["name"].to_string();
let reference = caps["ref"].to_string().replace("$", "");
let dimension = if let Some(d) = Dimension::from_r1c1(&reference) {
Some(d)
} else if let Some(d) = Dimension::from_a1(reference.as_bytes()) {
Some(d)
} else {
None
};
if let Some(dimension) = dimension {
return Self {
sheet_name: name,
cell_range: dimension,
};
};
let coordinate = if let Some(c) = Coordinate::from_r1c1(&reference) {
c
} else if let Some(c) = Coordinate::from_a1(reference.as_bytes()) {
c
} else {
Coordinate::from_point((1, 1))
};
return Self {
sheet_name: name,
cell_range: Dimension {
start: coordinate,
end: coordinate,
},
};
};
return default;
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum ExternalHyperlink {
Url(String),
Email(EmailHyperlink),
}
impl ExternalHyperlink {
pub(crate) fn from_string(s: &str) -> Self {
if let Ok(re) = Regex::new(r"mailto:(?<email>.+?)(\?subject=)(?<subject>.+?)$") {
if let Some(caps) = re.captures(s) {
let email = &caps["email"];
let email = urlencoding::decode(email)
.unwrap_or(std::borrow::Cow::Borrowed(email))
.to_string();
let subject = &caps["subject"];
let subject = urlencoding::decode(subject)
.unwrap_or(std::borrow::Cow::Borrowed(subject))
.to_string();
return Self::Email(EmailHyperlink {
mail_to: email,
subject,
});
};
};
if let Ok(re) = Regex::new(r"mailto:(?<email>.+?)$") {
if let Some(caps) = re.captures(s) {
let email = &caps["email"];
let email = urlencoding::decode(email)
.unwrap_or(std::borrow::Cow::Borrowed(email))
.to_string();
return Self::Email(EmailHyperlink {
mail_to: email,
subject: "".to_string(),
});
};
};
let url = urlencoding::decode(s)
.unwrap_or(std::borrow::Cow::Borrowed(s))
.to_string();
return Self::Url(url);
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct EmailHyperlink {
pub mail_to: String,
pub subject: String,
}