Skip to main content

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}