pptx_rs/oxml/section.rs
1//! 演示文稿分组(Section,`<p14:sectionLst>` 扩展元素)。
2//!
3//! 本模块对应 PowerPoint 2010 引入的"章节分组"功能——把若干 slide
4//! 归入命名分组,便于在"大纲视图"中折叠 / 跳转。
5//!
6//! # OOXML 结构
7//!
8//! `sectionLst` 是 `<p:presentation>` 内 `<p:extLst>` 的扩展元素,
9//! 命名空间为 `p14`(PowerPoint 2010 main):
10//!
11//! ```xml
12//! <p:presentation>
13//! ...
14//! <p:extLst>
15//! <p:ext uri="{521415D9-36F7-43E2-AB2F-B90AF26B5E64}">
16//! <p14:sectionLst xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main">
17//! <p14:section name="章节一">
18//! <p14:sldIdLst>
19//! <p14:sldId id="256"/>
20//! <p14:sldId id="257"/>
21//! </p14:sldIdLst>
22//! </p14:section>
23//! </p14:sectionLst>
24//! </p:ext>
25//! </p:extLst>
26//! </p:presentation>
27//! ```
28//!
29//! # 与 python-pptx 的对应
30//!
31//! python-pptx 截至 v1.0 仍未提供 section API,本模块是 pptx-rs 的扩展实现,
32//! 参考 OOXML 规范 [MS-PPT] 2.6.4 节。
33//!
34//! # 序列化约束
35//!
36//! - **必须**放在 `<p:extLst>` 内的 `<p:ext>` 元素中,且 uri 固定为
37//! `{521415D9-36F7-43E2-AB2F-B90AF26B5E64}`;
38//! - `<p:extLst>` 在 `<p:defaultTextStyle>` 之后;
39//! - section 的 `<p14:sldId>` 只有 `id` 属性(**无** `r:id`)。
40
41use crate::oxml::writer::XmlWriter;
42
43/// sectionLst 扩展元素的固定 URI(PowerPoint 2010 section 特征 GUID)。
44///
45/// 该 URI 必须出现在 `<p:ext uri="...">` 属性中,PowerPoint 通过它识别
46/// 扩展内容为 section 列表。
47pub const SECTION_EXT_URI: &str = "{521415D9-36F7-43E2-AB2F-B90AF26B5E64}";
48
49/// 单个章节(`<p14:section>`)。
50///
51/// 一个 section 由 `name` 与若干 slide id 组成。
52/// slide id 必须已存在于 `<p:sldIdLst>` 中——section 只是按 id 把它们
53/// "逻辑分组",并不持有额外的关系。
54#[derive(Clone, Debug, Default)]
55pub struct Section {
56 /// 章节名(`name` 属性,必填)。
57 pub name: String,
58 /// 归入本章节的 slide id 列表(`<p14:sldId id="..."/>`)。
59 ///
60 /// 这里的 `id` 与 `<p:sldIdLst>/<p:sldId>` 的 `id` 同源——
61 /// 由 `Presentation` 在写路径中分配(一般从 256 起递增)。
62 pub slide_ids: Vec<u32>,
63}
64
65impl Section {
66 /// 创建一个空章节(仅指定名称)。
67 pub fn new(name: impl Into<String>) -> Self {
68 Self {
69 name: name.into(),
70 slide_ids: Vec::new(),
71 }
72 }
73
74 /// 向本章节追加一个 slide id。
75 pub fn push(&mut self, id: u32) {
76 self.slide_ids.push(id);
77 }
78
79 /// 写 XML——输出**仅** `<p14:section>` 元素(不含外层 `<p14:sectionLst>`)。
80 ///
81 /// 调用方(通常是 [`SectionList::write_xml`])负责提供 `<p14:sectionLst>`
82 /// 与 `<p:extLst>/<p:ext>` 外壳。
83 pub fn write_xml(&self, w: &mut XmlWriter) {
84 let attrs: Vec<(&str, &str)> = vec![("name", self.name.as_str())];
85 w.open_with("p14:section", &attrs);
86 if !self.slide_ids.is_empty() {
87 w.open("p14:sldIdLst");
88 for id in &self.slide_ids {
89 let id_s = id.to_string();
90 w.empty_with("p14:sldId", &[("id", id_s.as_str())]);
91 }
92 w.close("p14:sldIdLst");
93 }
94 w.close("p14:section");
95 }
96}
97
98/// 章节列表(`<p14:sectionLst>`)。
99///
100/// 在 [`crate::oxml::presentation::PresentationRoot`] 中以 `sections` 字段
101/// 携带,写路径在 `<p:extLst>` 内展开为 `<p14:sectionLst>`。
102///
103/// # 序列化
104///
105/// 空列表时**不**输出任何内容(保持 presentation.xml 干净)。
106#[derive(Clone, Debug, Default)]
107pub struct SectionList {
108 /// 所有章节(按文档顺序)。
109 pub items: Vec<Section>,
110}
111
112impl SectionList {
113 /// 新建空列表。
114 pub fn new() -> Self {
115 Self::default()
116 }
117
118 /// 章节数量。
119 pub fn len(&self) -> usize {
120 self.items.len()
121 }
122
123 /// 是否为空。
124 pub fn is_empty(&self) -> bool {
125 self.items.is_empty()
126 }
127
128 /// 追加一个章节。
129 pub fn push(&mut self, section: Section) {
130 self.items.push(section);
131 }
132
133 /// 按 slide id 查询所属章节名。
134 ///
135 /// 同一个 slide id 理论上不应出现在多个 section 内;
136 /// 若被多次追加,返回第一个匹配的章节名。
137 pub fn section_name_of(&self, slide_id: u32) -> Option<&str> {
138 for s in &self.items {
139 if s.slide_ids.contains(&slide_id) {
140 return Some(s.name.as_str());
141 }
142 }
143 None
144 }
145
146 /// 写 XML——输出**完整** `<p:extLst><p:ext ...><p14:sectionLst>...</p14:sectionLst></p:ext></p:extLst>`。
147 ///
148 /// 当且仅当列表非空时输出;空列表返回空字符串。
149 pub fn write_xml(&self) -> String {
150 if self.is_empty() {
151 return String::new();
152 }
153 let mut w = XmlWriter::new();
154 w.open("p:extLst");
155 let ext_attrs: Vec<(&str, &str)> = vec![("uri", SECTION_EXT_URI)];
156 w.open_with("p:ext", &ext_attrs);
157 // sectionLst 自身无属性;xmlns:p14 由 presentation 根元素统一声明。
158 w.open("p14:sectionLst");
159 for s in &self.items {
160 s.write_xml(&mut w);
161 }
162 w.close("p14:sectionLst");
163 w.close("p:ext");
164 w.close("p:extLst");
165 w.into_string()
166 }
167}