use std::borrow::Cow;
use std::collections::BTreeMap;
use super::part::PartName;
use crate::oxml::ns::NS_RELATIONSHIPS;
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum RelType {
OfficeDocument,
Slide,
SlideLayout,
SlideMaster,
Theme,
Image,
NotesSlide,
NotesMaster,
Comments,
CommentAuthors,
Hyperlink,
SlideRel,
Chart,
CustomXml,
PresProps,
ViewProps,
TableStyles,
OleObject,
Video,
Audio,
Media,
DiagramData,
DiagramLayout,
DiagramQuickStyle,
DiagramColors,
Package,
Other(String),
}
impl RelType {
pub fn uri(&self) -> Cow<'_, str> {
match self {
RelType::OfficeDocument =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"),
RelType::Slide =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"),
RelType::SlideLayout =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"),
RelType::SlideMaster =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"),
RelType::Theme =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"),
RelType::Image =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"),
RelType::NotesSlide =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide"),
RelType::NotesMaster =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster"),
RelType::Comments =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"),
RelType::CommentAuthors =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors"),
RelType::Hyperlink =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"),
RelType::SlideRel =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"),
RelType::Chart =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"),
RelType::CustomXml =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml"),
RelType::PresProps =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps"),
RelType::ViewProps =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps"),
RelType::TableStyles =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles"),
RelType::OleObject =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject"),
RelType::Video =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/video"),
RelType::Audio =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio"),
RelType::Media =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/media"),
RelType::DiagramData =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData"),
RelType::DiagramLayout =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout"),
RelType::DiagramQuickStyle =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle"),
RelType::DiagramColors =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors"),
RelType::Package =>
Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/package"),
RelType::Other(s) => Cow::Borrowed(s.as_str()),
}
}
}
impl std::fmt::Display for RelType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.uri())
}
}
#[derive(Debug, Clone)]
pub struct Relationship {
pub id: String,
pub reltype: RelType,
pub target: Target,
pub is_external: bool,
}
impl Relationship {
pub fn internal(id: impl Into<String>, reltype: RelType, target: PartName) -> Self {
Relationship {
id: id.into(),
reltype,
target: Target::Internal(target),
is_external: false,
}
}
pub fn internal_str(
id: impl Into<String>,
reltype: RelType,
target: impl Into<String>,
) -> Self {
Relationship {
id: id.into(),
reltype,
target: Target::InternalStr(target.into()),
is_external: false,
}
}
pub fn external(id: impl Into<String>, reltype: RelType, url: impl Into<String>) -> Self {
Relationship {
id: id.into(),
reltype,
target: Target::External(url.into()),
is_external: true,
}
}
}
#[derive(Debug, Clone)]
pub enum Target {
Internal(PartName),
InternalStr(String),
External(String),
}
impl Target {
pub fn as_str(&self) -> &str {
match self {
Target::Internal(p) => p.as_str(),
Target::InternalStr(s) => s.as_str(),
Target::External(s) => s.as_str(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Relationships {
items: Vec<Relationship>,
by_id: BTreeMap<String, usize>,
}
impl Relationships {
pub fn new() -> Self {
Relationships::default()
}
pub fn add(&mut self, r: Relationship) -> crate::Result<&Relationship> {
if self.by_id.contains_key(&r.id) {
return Err(crate::Error::opc(format!(
"duplicate relationship id: {}",
r.id
)));
}
let idx = self.items.len();
self.by_id.insert(r.id.clone(), idx);
self.items.push(r);
Ok(&self.items[idx])
}
pub fn iter(&self) -> impl Iterator<Item = &Relationship> {
self.items.iter()
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn get(&self, id: &str) -> Option<&Relationship> {
self.by_id.get(id).map(|&i| &self.items[i])
}
pub fn of_type<'a>(&'a self, t: RelType) -> impl Iterator<Item = &'a Relationship> + 'a {
self.items.iter().filter(move |r| r.reltype == t)
}
pub fn to_xml(&self) -> String {
let mut s = String::with_capacity(256);
s.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
s.push_str(&format!("<Relationships xmlns=\"{}\">", NS_RELATIONSHIPS));
for r in &self.items {
s.push_str(&format!(
"<Relationship Id=\"{}\" Type=\"{}\" Target=\"{}\"{}/>",
xml_escape(&r.id),
xml_escape(&r.reltype.uri()),
xml_escape(r.target.as_str()),
if r.is_external {
" TargetMode=\"External\""
} else {
""
},
));
}
s.push_str("</Relationships>");
s
}
pub fn from_xml(xml: &str) -> crate::Result<Self> {
use quick_xml::events::Event;
use quick_xml::reader::Reader;
let mut r = Relationships::new();
let mut rd = Reader::from_str(xml);
rd.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Empty(e)) | Ok(Event::Start(e)) => {
let name = e.name();
if name.as_ref() != b"Relationship" {
continue;
}
let mut id = None;
let mut rtype = None;
let mut target = None;
let mut external = false;
for attr in e.attributes().flatten() {
let key = attr.key.as_ref();
let v = attr
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default()
.to_string();
match key {
b"Id" => id = Some(v),
b"Type" => rtype = Some(v),
b"Target" => target = Some(v),
b"TargetMode" if v == "External" => external = true,
_ => {}
}
}
let (Some(id), Some(rtype), Some(target)) = (id, rtype, target) else {
return Err(crate::Error::opc("malformed <Relationship> element"));
};
let reltype = match rtype.as_str() {
x if x == RelType::OfficeDocument.uri() => RelType::OfficeDocument,
x if x == RelType::Slide.uri() => RelType::Slide,
x if x == RelType::SlideLayout.uri() => RelType::SlideLayout,
x if x == RelType::SlideMaster.uri() => RelType::SlideMaster,
x if x == RelType::Theme.uri() => RelType::Theme,
x if x == RelType::Image.uri() => RelType::Image,
x if x == RelType::NotesSlide.uri() => RelType::NotesSlide,
x if x == RelType::NotesMaster.uri() => RelType::NotesMaster,
x if x == RelType::Comments.uri() => RelType::Comments,
x if x == RelType::CommentAuthors.uri() => RelType::CommentAuthors,
x if x == RelType::Hyperlink.uri() => RelType::Hyperlink,
x if x == RelType::Chart.uri() => RelType::Chart,
x if x == RelType::CustomXml.uri() => RelType::CustomXml,
x if x == RelType::PresProps.uri() => RelType::PresProps,
x if x == RelType::ViewProps.uri() => RelType::ViewProps,
x if x == RelType::TableStyles.uri() => RelType::TableStyles,
x if x == RelType::OleObject.uri() => RelType::OleObject,
x if x == RelType::Video.uri() => RelType::Video,
x if x == RelType::Audio.uri() => RelType::Audio,
x if x == RelType::Media.uri() => RelType::Media,
x if x == RelType::DiagramData.uri() => RelType::DiagramData,
x if x == RelType::DiagramLayout.uri() => RelType::DiagramLayout,
x if x == RelType::DiagramQuickStyle.uri() => RelType::DiagramQuickStyle,
x if x == RelType::DiagramColors.uri() => RelType::DiagramColors,
x if x == RelType::Package.uri() => RelType::Package,
other => RelType::Other(other.to_string()),
};
let rel = if external {
Relationship::external(id, reltype, target)
} else {
Relationship::internal_str(id, reltype, target)
};
r.add(rel)?;
}
Ok(Event::Eof) => break,
Err(e) => return Err(crate::Error::Xml(format!("relationships parse: {e}"))),
_ => {}
}
buf.clear();
}
Ok(r)
}
}
pub(crate) fn xml_escape(s: &str) -> String {
crate::escape::xml_escape(s)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::opc::part::new_part_name;
#[test]
fn round_trip() {
let mut r = Relationships::new();
r.add(Relationship::internal(
"rId1",
RelType::Slide,
new_part_name("/ppt/slides/slide1.xml"),
))
.unwrap();
r.add(Relationship::internal(
"rId2",
RelType::SlideLayout,
new_part_name("/ppt/slideLayouts/slideLayout1.xml"),
))
.unwrap();
let xml = r.to_xml();
let r2 = Relationships::from_xml(&xml).unwrap();
assert_eq!(r2.len(), 2);
assert!(r2.get("rId1").is_some());
}
#[test]
fn diagram_reltype_uri_correct() {
assert_eq!(
RelType::DiagramData.uri(),
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData"
);
assert_eq!(
RelType::DiagramLayout.uri(),
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout"
);
assert_eq!(
RelType::DiagramQuickStyle.uri(),
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle"
);
assert_eq!(
RelType::DiagramColors.uri(),
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors"
);
}
#[test]
fn from_xml_recognizes_diagram_reltypes() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rIdDgmData1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData" Target="../diagrams/data1.xml"/>
<Relationship Id="rIdDgmLayout1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout" Target="../diagrams/layout1.xml"/>
<Relationship Id="rIdDgmQs1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle" Target="../diagrams/quickStyles1.xml"/>
<Relationship Id="rIdDgmColors1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors" Target="../diagrams/colors1.xml"/>
</Relationships>"#;
let r = Relationships::from_xml(xml).expect("parse ok");
assert_eq!(r.len(), 4);
let data = r.get("rIdDgmData1").expect("data rel");
assert_eq!(data.reltype, RelType::DiagramData);
let layout = r.get("rIdDgmLayout1").expect("layout rel");
assert_eq!(layout.reltype, RelType::DiagramLayout);
let qs = r.get("rIdDgmQs1").expect("qs rel");
assert_eq!(qs.reltype, RelType::DiagramQuickStyle);
let colors = r.get("rIdDgmColors1").expect("colors rel");
assert_eq!(colors.reltype, RelType::DiagramColors);
}
#[test]
fn diagram_reltype_round_trip() {
let mut r = Relationships::new();
r.add(Relationship::internal_str(
"rIdDgmData1",
RelType::DiagramData,
"../diagrams/data1.xml",
))
.unwrap();
r.add(Relationship::internal_str(
"rIdDgmLayout1",
RelType::DiagramLayout,
"../diagrams/layout1.xml",
))
.unwrap();
r.add(Relationship::internal_str(
"rIdDgmQs1",
RelType::DiagramQuickStyle,
"../diagrams/quickStyles1.xml",
))
.unwrap();
r.add(Relationship::internal_str(
"rIdDgmColors1",
RelType::DiagramColors,
"../diagrams/colors1.xml",
))
.unwrap();
let xml = r.to_xml();
let r2 = Relationships::from_xml(&xml).expect("parse ok");
assert_eq!(r2.len(), 4);
assert_eq!(
r2.get("rIdDgmData1").unwrap().target.as_str(),
"../diagrams/data1.xml"
);
assert_eq!(
r2.get("rIdDgmColors1").unwrap().target.as_str(),
"../diagrams/colors1.xml"
);
}
}