Skip to main content

ooxml_core/opc/
rels.rs

1//! 关系(`.rels`)模型。
2//!
3//! 一个 `.rels` 文件是一组 `Relationship` 元素,每个元素表达一个 part
4//! 指向另一个 part(以"相对 URI"表达)的链接。OOXML 规范使用 `.rels`
5//! 文件把分散的 XML part 组织成有向图。
6//!
7//! # 文件位置约定
8//!
9//! 关系文件存放在被描述 part 的"兄弟目录 `_rels/`"中,文件名 `<part>.rels`:
10//!
11//! ```text
12//! ppt/slides/slide1.xml          ←  被描述的 part
13//! ppt/slides/_rels/slide1.xml.rels   ←  关系文件
14//! ```
15//!
16//! 根包的关系文件固定为 `/_rels/.rels`。
17//!
18//! # 解析与序列化
19//!
20//! - [`Relationships::to_xml`] 序列化为 XML 字符串;
21//! - [`Relationships::from_xml`] 解析 XML 字符串为模型。
22//!
23//! 两者互为逆操作;解析时遇到未知 `Type` URI 会返回 [`Error::Opc`](crate::Error::Opc)
24//! 以提示 schema 变更。
25
26use std::borrow::Cow;
27use std::collections::BTreeMap;
28
29use super::part::PartName;
30use crate::oxml::ns::NS_RELATIONSHIPS;
31
32/// 关系类型(OOXML 标准)。
33///
34/// 列举 OOXML 规范中"办公文档"命名空间下的常用关系类型;其它类型用
35/// [`RelType::Other`] 兜底(传 `&'static str` URI)。
36#[derive(Clone, Debug, Eq, PartialEq, Hash)]
37pub enum RelType {
38    /// `.../relationships/officeDocument`:根指向演示文稿。
39    OfficeDocument,
40    /// `.../relationships/slide`:幻灯片。
41    Slide,
42    /// `.../relationships/slideLayout`:幻灯片布局。
43    SlideLayout,
44    /// `.../relationships/slideMaster`:幻灯片母版。
45    SlideMaster,
46    /// `.../relationships/theme`:主题。
47    Theme,
48    /// `.../relationships/image`:图片。
49    Image,
50    /// `.../relationships/notesSlide`:备注页。
51    NotesSlide,
52    /// `.../relationships/notesMaster`:备注母版(TODO-045)。
53    ///
54    /// 对应 `/ppt/notesMasters/notesMasterN.xml`,被 `presentation.xml`
55    /// 的 `<p:notesMasterIdLst>` 引用。
56    NotesMaster,
57    /// `.../relationships/comments`:幻灯片评论(`/ppt/comments/commentN.xml`)。
58    Comments,
59    /// `.../relationships/commentAuthors`:评论作者列表(`/ppt/commentAuthors.xml`)。
60    CommentAuthors,
61    /// `.../relationships/hyperlink`:超链接(外链)。
62    Hyperlink,
63    /// `.../relationships/slide`:幻灯片(备查别名,与 `Slide` 等价)。
64    /// 当前未用——保留便于未来其它 part 引用。
65    SlideRel,
66    /// `.../relationships/chart`:图表(路线图中)。
67    Chart,
68    /// `.../relationships/customXml`:自定义 XML。
69    CustomXml,
70    /// `.../relationships/presProps`:演示文稿属性。
71    PresProps,
72    /// `.../relationships/viewProps`:视图属性。
73    ViewProps,
74    /// `.../relationships/tableStyles`:表格样式。
75    TableStyles,
76    /// `.../relationships/oleObject`:OLE 对象嵌入(TODO-043)。
77    ///
78    /// 对应 `/ppt/embeddings/oleObjectN.bin`,被 slide 的 `<p:oleObj r:id="..."/>` 引用。
79    /// 与 `Image` 关系配合使用:`oleObj` 内的 `<p:pic>` 用 `Image` 关系引用图标图片。
80    OleObject,
81    /// `.../relationships/video`:视频文件(TODO-033)。
82    ///
83    /// 对应 `/ppt/media/mediaN.mp4`(或其它视频扩展名),被 slide 的
84    /// `<p:pic><p:nvPicPr><p:nvPr><a:videoFile r:link="..."/></p:nvPr>` 引用。
85    /// 注意:使用 `r:link` 而非 `r:embed`(视频按外部链接方式存储)。
86    Video,
87    /// `.../relationships/audio`:音频文件(TODO-033)。
88    ///
89    /// 对应 `/ppt/media/mediaN.mp3`(或其它音频扩展名),被 slide 的
90    /// `<p:pic><p:nvPicPr><p:nvPr><a:audioFile r:link="..."/></p:nvPr>` 引用。
91    Audio,
92    /// `.../relationships/media`:媒体引用(TODO-033,用于 timeline 同步)。
93    ///
94    /// 在 `<p:timing>` 中通过 `<p:video>` / `<p:audio>` 引用,与 Video/Audio
95    /// 关系配合使用。当前库未实现 timing,仅保留 RelType 以支持 round-trip。
96    Media,
97    /// `.../relationships/diagramData`:SmartArt 数据 part(TODO-037)。
98    ///
99    /// 对应 `/ppt/diagrams/dataN.xml`,被 slide 的
100    /// `<p:graphicFrame><a:graphic><a:graphicData uri="...diagram"><dgm:relIds r:dm="..."/>` 引用。
101    /// 与 DiagramLayout / DiagramQuickStyle / DiagramColors 一起构成 SmartArt 完整 round-trip。
102    DiagramData,
103    /// `.../relationships/diagramLayout`:SmartArt 布局 part(TODO-037)。
104    ///
105    /// 对应 `/ppt/diagrams/layoutN.xml`,被 `<dgm:relIds r:lo="..."/>` 引用。
106    DiagramLayout,
107    /// `.../relationships/diagramQuickStyle`:SmartArt 快速样式 part(TODO-037)。
108    ///
109    /// 对应 `/ppt/diagrams/quickStylesN.xml`,被 `<dgm:relIds r:qs="..."/>` 引用。
110    DiagramQuickStyle,
111    /// `.../relationships/diagramColors`:SmartArt 颜色 part(TODO-037)。
112    ///
113    /// 对应 `/ppt/diagrams/colorsN.xml`,被 `<dgm:relIds r:cs="..."/>` 引用。
114    DiagramColors,
115    /// `.../relationships/package`:嵌入式包关系(TODO-004 Excel 嵌入)。
116    ///
117    /// OOXML 中通用"嵌入式包"关系类型。在 pptx-rs 当前主要用于 chart 的
118    /// `<c:externalData r:id="..."/>` 引用嵌入式 Excel 工作簿
119    /// (`/ppt/embeddings/Microsoft_Excel_WorksheetN.xlsx`)。
120    /// 也用于其它嵌入式包(如嵌入的 docx/pptx),但当前库仅支持 xlsx 场景。
121    Package,
122    /// 任意其它(用字符串表达)。
123    Other(String),
124}
125
126impl RelType {
127    /// 转 URI 字符串。
128    pub fn uri(&self) -> Cow<'_, str> {
129        match self {
130            RelType::OfficeDocument =>
131                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"),
132            RelType::Slide =>
133                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"),
134            RelType::SlideLayout =>
135                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"),
136            RelType::SlideMaster =>
137                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"),
138            RelType::Theme =>
139                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"),
140            RelType::Image =>
141                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"),
142            RelType::NotesSlide =>
143                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide"),
144            RelType::NotesMaster =>
145                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster"),
146            RelType::Comments =>
147                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"),
148            RelType::CommentAuthors =>
149                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors"),
150            RelType::Hyperlink =>
151                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"),
152            RelType::SlideRel =>
153                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"),
154            RelType::Chart =>
155                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"),
156            RelType::CustomXml =>
157                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml"),
158            RelType::PresProps =>
159                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps"),
160            RelType::ViewProps =>
161                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps"),
162            RelType::TableStyles =>
163                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles"),
164            RelType::OleObject =>
165                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject"),
166            RelType::Video =>
167                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/video"),
168            RelType::Audio =>
169                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio"),
170            RelType::Media =>
171                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/media"),
172            RelType::DiagramData =>
173                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData"),
174            RelType::DiagramLayout =>
175                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout"),
176            RelType::DiagramQuickStyle =>
177                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle"),
178            RelType::DiagramColors =>
179                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors"),
180            RelType::Package =>
181                Cow::Borrowed("http://schemas.openxmlformats.org/officeDocument/2006/relationships/package"),
182            RelType::Other(s) => Cow::Borrowed(s.as_str()),
183        }
184    }
185}
186
187impl std::fmt::Display for RelType {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        f.write_str(&self.uri())
190    }
191}
192
193/// 一条关系。
194#[derive(Debug, Clone)]
195pub struct Relationship {
196    /// 关系 ID(如 `rId1`)。
197    pub id: String,
198    /// 关系类型。
199    pub reltype: RelType,
200    /// 目标 part name(绝对),或外链(hyperlink)。
201    pub target: Target,
202    /// 是否为外部资源(hyperlink)。
203    pub is_external: bool,
204}
205
206impl Relationship {
207    /// 创建内部关系。
208    pub fn internal(id: impl Into<String>, reltype: RelType, target: PartName) -> Self {
209        Relationship {
210            id: id.into(),
211            reltype,
212            target: Target::Internal(target),
213            is_external: false,
214        }
215    }
216    /// 创建内部关系(target 用裸字符串,常用于相对路径如 `../theme/theme1.xml`)。
217    pub fn internal_str(
218        id: impl Into<String>,
219        reltype: RelType,
220        target: impl Into<String>,
221    ) -> Self {
222        Relationship {
223            id: id.into(),
224            reltype,
225            target: Target::InternalStr(target.into()),
226            is_external: false,
227        }
228    }
229    /// 创建外部关系(hyperlink)。
230    pub fn external(id: impl Into<String>, reltype: RelType, url: impl Into<String>) -> Self {
231        Relationship {
232            id: id.into(),
233            reltype,
234            target: Target::External(url.into()),
235            is_external: true,
236        }
237    }
238}
239
240/// 关系的目标:内部 part / 外部 URL。
241#[derive(Debug, Clone)]
242pub enum Target {
243    /// 内部 part(PartName 形式)。
244    Internal(PartName),
245    /// 内部 part(裸字符串 target,保留原始相对路径;如 `../theme/theme1.xml`)。
246    InternalStr(String),
247    /// 外部 URL。
248    External(String),
249}
250
251impl Target {
252    /// 写出 XML 时使用的字面量。
253    pub fn as_str(&self) -> &str {
254        match self {
255            Target::Internal(p) => p.as_str(),
256            Target::InternalStr(s) => s.as_str(),
257            Target::External(s) => s.as_str(),
258        }
259    }
260}
261
262/// 一个 `.rels` 文件对应的全部关系集合。
263///
264/// 内部以 `Vec<Relationship>` 保留插入顺序,同时维护 `by_id` 索引
265/// 提供 O(log n) 的按 id 查询。
266#[derive(Debug, Clone, Default)]
267pub struct Relationships {
268    items: Vec<Relationship>,
269    by_id: BTreeMap<String, usize>,
270}
271
272impl Relationships {
273    /// 空集合。
274    pub fn new() -> Self {
275        Relationships::default()
276    }
277
278    /// 添加一条关系,自动检查 ID 唯一。
279    ///
280    /// # 错误
281    /// - [`crate::Error::Opc`]:ID 重复。
282    pub fn add(&mut self, r: Relationship) -> crate::Result<&Relationship> {
283        if self.by_id.contains_key(&r.id) {
284            return Err(crate::Error::opc(format!(
285                "duplicate relationship id: {}",
286                r.id
287            )));
288        }
289        let idx = self.items.len();
290        self.by_id.insert(r.id.clone(), idx);
291        self.items.push(r);
292        Ok(&self.items[idx])
293    }
294
295    /// 取所有关系(按插入顺序)。
296    pub fn iter(&self) -> impl Iterator<Item = &Relationship> {
297        self.items.iter()
298    }
299
300    /// 数量。
301    pub fn len(&self) -> usize {
302        self.items.len()
303    }
304    /// 是否为空。
305    pub fn is_empty(&self) -> bool {
306        self.items.is_empty()
307    }
308
309    /// 按 ID 找。
310    pub fn get(&self, id: &str) -> Option<&Relationship> {
311        self.by_id.get(id).map(|&i| &self.items[i])
312    }
313
314    /// 按 reltype 过滤。
315    pub fn of_type<'a>(&'a self, t: RelType) -> impl Iterator<Item = &'a Relationship> + 'a {
316        self.items.iter().filter(move |r| r.reltype == t)
317    }
318
319    /// 转成 XML 字符串。
320    pub fn to_xml(&self) -> String {
321        let mut s = String::with_capacity(256);
322        s.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
323        s.push_str(&format!("<Relationships xmlns=\"{}\">", NS_RELATIONSHIPS));
324        for r in &self.items {
325            s.push_str(&format!(
326                "<Relationship Id=\"{}\" Type=\"{}\" Target=\"{}\"{}/>",
327                xml_escape(&r.id),
328                xml_escape(&r.reltype.uri()),
329                xml_escape(r.target.as_str()),
330                if r.is_external {
331                    " TargetMode=\"External\""
332                } else {
333                    ""
334                },
335            ));
336        }
337        s.push_str("</Relationships>");
338        s
339    }
340
341    /// 解析一段 XML,构造关系集合。
342    ///
343    /// # 错误
344    /// - [`crate::Error::Opc`]:元素属性缺失或 Type URI 不在已知清单中;
345    /// - [`crate::Error::Xml`]:XML 解析失败。
346    pub fn from_xml(xml: &str) -> crate::Result<Self> {
347        use quick_xml::events::Event;
348        use quick_xml::reader::Reader;
349
350        let mut r = Relationships::new();
351        let mut rd = Reader::from_str(xml);
352        rd.config_mut().trim_text(true);
353        let mut buf = Vec::new();
354        loop {
355            match rd.read_event_into(&mut buf) {
356                Ok(Event::Empty(e)) | Ok(Event::Start(e)) => {
357                    let name = e.name();
358                    if name.as_ref() != b"Relationship" {
359                        continue;
360                    }
361                    let mut id = None;
362                    let mut rtype = None;
363                    let mut target = None;
364                    let mut external = false;
365                    for attr in e.attributes().flatten() {
366                        let key = attr.key.as_ref();
367                        let v = attr
368                            .normalized_value(quick_xml::XmlVersion::Implicit1_0)
369                            .unwrap_or_default()
370                            .to_string();
371                        match key {
372                            b"Id" => id = Some(v),
373                            b"Type" => rtype = Some(v),
374                            b"Target" => target = Some(v),
375                            b"TargetMode" if v == "External" => external = true,
376                            _ => {}
377                        }
378                    }
379                    let (Some(id), Some(rtype), Some(target)) = (id, rtype, target) else {
380                        return Err(crate::Error::opc("malformed <Relationship> element"));
381                    };
382                    let reltype = match rtype.as_str() {
383                        x if x == RelType::OfficeDocument.uri() => RelType::OfficeDocument,
384                        x if x == RelType::Slide.uri() => RelType::Slide,
385                        x if x == RelType::SlideLayout.uri() => RelType::SlideLayout,
386                        x if x == RelType::SlideMaster.uri() => RelType::SlideMaster,
387                        x if x == RelType::Theme.uri() => RelType::Theme,
388                        x if x == RelType::Image.uri() => RelType::Image,
389                        x if x == RelType::NotesSlide.uri() => RelType::NotesSlide,
390                        x if x == RelType::NotesMaster.uri() => RelType::NotesMaster,
391                        x if x == RelType::Comments.uri() => RelType::Comments,
392                        x if x == RelType::CommentAuthors.uri() => RelType::CommentAuthors,
393                        x if x == RelType::Hyperlink.uri() => RelType::Hyperlink,
394                        x if x == RelType::Chart.uri() => RelType::Chart,
395                        x if x == RelType::CustomXml.uri() => RelType::CustomXml,
396                        x if x == RelType::PresProps.uri() => RelType::PresProps,
397                        x if x == RelType::ViewProps.uri() => RelType::ViewProps,
398                        x if x == RelType::TableStyles.uri() => RelType::TableStyles,
399                        x if x == RelType::OleObject.uri() => RelType::OleObject,
400                        x if x == RelType::Video.uri() => RelType::Video,
401                        x if x == RelType::Audio.uri() => RelType::Audio,
402                        x if x == RelType::Media.uri() => RelType::Media,
403                        x if x == RelType::DiagramData.uri() => RelType::DiagramData,
404                        x if x == RelType::DiagramLayout.uri() => RelType::DiagramLayout,
405                        x if x == RelType::DiagramQuickStyle.uri() => RelType::DiagramQuickStyle,
406                        x if x == RelType::DiagramColors.uri() => RelType::DiagramColors,
407                        x if x == RelType::Package.uri() => RelType::Package,
408                        // 未知关系类型用 Other 兜底,避免加载真实世界 .pptx 时因
409                        // handoutMaster / tags / comment 等未知类型而报错。
410                        other => RelType::Other(other.to_string()),
411                    };
412                    // 保留原始 target 字符串(相对路径如 "../notesSlides/notesSlide1.xml"),
413                    // 而非转为 PartName(会错误地加前导 '/' 导致 resolve_relative_partname
414                    // 误判为绝对路径)。调用方在需要绝对路径时自行调用 resolve_relative_partname。
415                    let rel = if external {
416                        Relationship::external(id, reltype, target)
417                    } else {
418                        Relationship::internal_str(id, reltype, target)
419                    };
420                    r.add(rel)?;
421                }
422                Ok(Event::Eof) => break,
423                Err(e) => return Err(crate::Error::Xml(format!("relationships parse: {e}"))),
424                _ => {}
425            }
426            buf.clear();
427        }
428        Ok(r)
429    }
430}
431
432/// XML 字符串属性转义(re-export from [`crate::escape::xml_escape`])。
433///
434/// 仅供 OPC 内部使用,故 `pub(crate)`;OOXML 序列化请走
435/// [`crate::oxml::writer::XmlWriter`]。
436pub(crate) fn xml_escape(s: &str) -> String {
437    crate::escape::xml_escape(s)
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    use crate::opc::part::new_part_name;
444
445    /// 关系集合的 XML round-trip 测试。
446    #[test]
447    fn round_trip() {
448        let mut r = Relationships::new();
449        r.add(Relationship::internal(
450            "rId1",
451            RelType::Slide,
452            new_part_name("/ppt/slides/slide1.xml"),
453        ))
454        .unwrap();
455        r.add(Relationship::internal(
456            "rId2",
457            RelType::SlideLayout,
458            new_part_name("/ppt/slideLayouts/slideLayout1.xml"),
459        ))
460        .unwrap();
461        let xml = r.to_xml();
462        let r2 = Relationships::from_xml(&xml).unwrap();
463        assert_eq!(r2.len(), 2);
464        assert!(r2.get("rId1").is_some());
465    }
466
467    // ==================== SmartArt 关系类型测试(TODO-037) ====================
468
469    /// 验证 4 个 diagram 关系类型的 URI 正确。
470    #[test]
471    fn diagram_reltype_uri_correct() {
472        assert_eq!(
473            RelType::DiagramData.uri(),
474            "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData"
475        );
476        assert_eq!(
477            RelType::DiagramLayout.uri(),
478            "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout"
479        );
480        assert_eq!(
481            RelType::DiagramQuickStyle.uri(),
482            "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle"
483        );
484        assert_eq!(
485            RelType::DiagramColors.uri(),
486            "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors"
487        );
488    }
489
490    /// 验证 `Relationships::from_xml` 能识别 4 个 diagram 关系类型(而非归为 Other)。
491    #[test]
492    fn from_xml_recognizes_diagram_reltypes() {
493        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
494<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
495<Relationship Id="rIdDgmData1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData" Target="../diagrams/data1.xml"/>
496<Relationship Id="rIdDgmLayout1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout" Target="../diagrams/layout1.xml"/>
497<Relationship Id="rIdDgmQs1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle" Target="../diagrams/quickStyles1.xml"/>
498<Relationship Id="rIdDgmColors1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors" Target="../diagrams/colors1.xml"/>
499</Relationships>"#;
500        let r = Relationships::from_xml(xml).expect("parse ok");
501        assert_eq!(r.len(), 4);
502
503        let data = r.get("rIdDgmData1").expect("data rel");
504        assert_eq!(data.reltype, RelType::DiagramData);
505
506        let layout = r.get("rIdDgmLayout1").expect("layout rel");
507        assert_eq!(layout.reltype, RelType::DiagramLayout);
508
509        let qs = r.get("rIdDgmQs1").expect("qs rel");
510        assert_eq!(qs.reltype, RelType::DiagramQuickStyle);
511
512        let colors = r.get("rIdDgmColors1").expect("colors rel");
513        assert_eq!(colors.reltype, RelType::DiagramColors);
514    }
515
516    /// 验证 4 个 diagram 关系能正确 round-trip(to_xml → from_xml)。
517    #[test]
518    fn diagram_reltype_round_trip() {
519        let mut r = Relationships::new();
520        r.add(Relationship::internal_str(
521            "rIdDgmData1",
522            RelType::DiagramData,
523            "../diagrams/data1.xml",
524        ))
525        .unwrap();
526        r.add(Relationship::internal_str(
527            "rIdDgmLayout1",
528            RelType::DiagramLayout,
529            "../diagrams/layout1.xml",
530        ))
531        .unwrap();
532        r.add(Relationship::internal_str(
533            "rIdDgmQs1",
534            RelType::DiagramQuickStyle,
535            "../diagrams/quickStyles1.xml",
536        ))
537        .unwrap();
538        r.add(Relationship::internal_str(
539            "rIdDgmColors1",
540            RelType::DiagramColors,
541            "../diagrams/colors1.xml",
542        ))
543        .unwrap();
544
545        let xml = r.to_xml();
546        let r2 = Relationships::from_xml(&xml).expect("parse ok");
547        assert_eq!(r2.len(), 4);
548        // 验证 Target 保留原始相对路径
549        assert_eq!(
550            r2.get("rIdDgmData1").unwrap().target.as_str(),
551            "../diagrams/data1.xml"
552        );
553        assert_eq!(
554            r2.get("rIdDgmColors1").unwrap().target.as_str(),
555            "../diagrams/colors1.xml"
556        );
557    }
558}