use crate::oxml::sppr::ShapeProperties;
use crate::oxml::txbody::TextBody;
use crate::units::Emu;
#[derive(Clone, Debug, Default)]
pub struct ShapeLocks {
pub no_grp: bool,
pub no_drilldown: bool,
pub no_select: bool,
pub no_change_aspect: bool,
pub no_move: bool,
pub no_resize: bool,
pub no_rot: bool,
pub no_edit_points: bool,
pub no_adjust_handles: bool,
pub no_change_arrowheads: bool,
pub no_change_shape_type: bool,
pub no_crop: bool,
}
impl ShapeLocks {
pub fn is_empty(&self) -> bool {
!self.no_grp
&& !self.no_drilldown
&& !self.no_select
&& !self.no_change_aspect
&& !self.no_move
&& !self.no_resize
&& !self.no_rot
&& !self.no_edit_points
&& !self.no_adjust_handles
&& !self.no_change_arrowheads
&& !self.no_change_shape_type
&& !self.no_crop
}
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
let mut attrs: Vec<(&str, &str)> = Vec::new();
if self.no_grp {
attrs.push(("noGrp", "1"));
}
if self.no_drilldown {
attrs.push(("noDrilldown", "1"));
}
if self.no_select {
attrs.push(("noSelect", "1"));
}
if self.no_change_aspect {
attrs.push(("noChangeAspect", "1"));
}
if self.no_move {
attrs.push(("noMove", "1"));
}
if self.no_resize {
attrs.push(("noResize", "1"));
}
if self.no_rot {
attrs.push(("noRot", "1"));
}
if self.no_edit_points {
attrs.push(("noEditPoints", "1"));
}
if self.no_adjust_handles {
attrs.push(("noAdjustHandles", "1"));
}
if self.no_change_arrowheads {
attrs.push(("noChangeArrowheads", "1"));
}
if self.no_change_shape_type {
attrs.push(("noChangeShapeType", "1"));
}
if self.no_crop {
attrs.push(("noCrop", "1"));
}
w.empty_with("a:spLocks", &attrs);
}
pub fn set_lock(&mut self, lock_type: LockType, locked: bool) {
match lock_type {
LockType::Grouping => self.no_grp = locked,
LockType::Drilldown => self.no_drilldown = locked,
LockType::Select => self.no_select = locked,
LockType::ChangeAspect => self.no_change_aspect = locked,
LockType::Move => self.no_move = locked,
LockType::Resize => self.no_resize = locked,
LockType::Rotate => self.no_rot = locked,
LockType::EditPoints => self.no_edit_points = locked,
LockType::AdjustHandles => self.no_adjust_handles = locked,
LockType::ChangeArrowheads => self.no_change_arrowheads = locked,
LockType::ChangeShapeType => self.no_change_shape_type = locked,
LockType::Crop => self.no_crop = locked,
}
}
pub fn get_lock(&self, lock_type: LockType) -> bool {
match lock_type {
LockType::Grouping => self.no_grp,
LockType::Drilldown => self.no_drilldown,
LockType::Select => self.no_select,
LockType::ChangeAspect => self.no_change_aspect,
LockType::Move => self.no_move,
LockType::Resize => self.no_resize,
LockType::Rotate => self.no_rot,
LockType::EditPoints => self.no_edit_points,
LockType::AdjustHandles => self.no_adjust_handles,
LockType::ChangeArrowheads => self.no_change_arrowheads,
LockType::ChangeShapeType => self.no_change_shape_type,
LockType::Crop => self.no_crop,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum LockType {
Grouping,
Drilldown,
Select,
ChangeAspect,
Move,
Resize,
Rotate,
EditPoints,
AdjustHandles,
ChangeArrowheads,
ChangeShapeType,
Crop,
}
#[derive(Clone, Debug, Default)]
pub struct Sp {
pub id: u32,
pub name: String,
pub is_placeholder: bool,
pub ph_idx: Option<u32>,
pub ph_type: Option<String>,
pub properties: ShapeProperties,
pub style: Option<ShapeStyle>,
pub text: TextBody,
pub ext_lst: Option<ExtensionList>,
pub c_nv_sp_pr_tx_box: bool,
pub locks: Option<ShapeLocks>,
}
impl Sp {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open("p:sp");
w.open("p:nvSpPr");
let id_s = self.id.to_string();
w.empty_with(
"p:cNvPr",
&[("id", id_s.as_str()), ("name", self.name.as_str())],
);
let idx_s = self.ph_idx.map(|v| v.to_string());
if self.is_placeholder {
let mut pattrs: Vec<(&str, &str)> = Vec::new();
let type_str: &str = self.ph_type.as_deref().unwrap_or("body");
pattrs.push(("type", type_str));
if let Some(s) = &idx_s {
pattrs.push(("idx", s.as_str()));
}
w.empty_with("p:ph", &pattrs);
}
let has_locks = self.locks.as_ref().map(|l| !l.is_empty()).unwrap_or(false);
if has_locks {
if self.c_nv_sp_pr_tx_box {
w.open_with("p:cNvSpPr", &[("txBox", "1")]);
} else {
w.open("p:cNvSpPr");
}
self.locks.as_ref().unwrap().write_xml(w);
w.close("p:cNvSpPr");
} else if self.c_nv_sp_pr_tx_box {
w.empty_with("p:cNvSpPr", &[("txBox", "1")]);
} else {
w.empty_with("p:cNvSpPr", &[]);
}
w.empty("p:nvPr");
w.close("p:nvSpPr");
self.properties.write_xml(w, "p:spPr");
if let Some(style) = &self.style {
style.write_xml(w);
}
self.text.write_xml(w, "p:txBody");
write_extlst(w, self.ext_lst.as_ref());
w.close("p:sp");
}
}
#[derive(Clone, Debug, Default)]
pub struct Pic {
pub id: u32,
pub name: String,
pub rid: String, pub is_placeholder: bool,
pub ph_idx: Option<u32>,
pub ph_type: Option<String>,
pub properties: ShapeProperties,
pub src_rect: Option<(i32, i32, i32, i32)>, pub fill_mode: crate::oxml::sppr::BlipFillMode,
pub alpha: Option<i32>,
pub style: Option<ShapeStyle>,
pub ext_lst: Option<ExtensionList>,
pub media: Option<MediaKind>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MediaKind {
Video {
rid: String,
},
Audio {
rid: String,
},
}
impl Default for MediaKind {
fn default() -> Self {
MediaKind::Video { rid: String::new() }
}
}
impl Pic {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open("p:pic");
w.open("p:nvPicPr");
let id_s = self.id.to_string();
w.empty_with("p:cNvPr", &[("id", &id_s), ("name", &self.name)]);
w.empty("p:cNvPicPr");
if self.is_placeholder || self.media.is_some() {
w.open_with("p:nvPr", &[("xmlns:r", crate::oxml::ns::NS_DRAWING_RELS)]);
if self.is_placeholder {
let idx_s = self.ph_idx.map(|i| i.to_string());
let mut pattrs: Vec<(&str, &str)> = Vec::new();
if let Some(t) = self.ph_type.as_deref() {
pattrs.push(("type", t));
}
if let Some(s) = idx_s.as_deref() {
pattrs.push(("idx", s));
}
w.empty_with("p:ph", &pattrs);
}
if let Some(media) = &self.media {
match media {
MediaKind::Video { rid } => {
w.empty_with("a:videoFile", &[("r:link", rid.as_str())]);
}
MediaKind::Audio { rid } => {
w.empty_with("a:audioFile", &[("r:link", rid.as_str())]);
}
}
}
w.close("p:nvPr");
} else {
w.empty("p:nvPr");
}
w.close("p:nvPicPr");
w.open_with(
"p:blipFill",
&[("xmlns:r", crate::oxml::ns::NS_DRAWING_RELS)],
);
if let Some(alpha_val) = self.alpha {
w.open_with("a:blip", &[("r:embed", self.rid.as_str())]);
let amt_s = alpha_val.to_string();
w.empty_with("a:alphaModFix", &[("amt", amt_s.as_str())]);
w.close("a:blip");
} else {
w.empty_with("a:blip", &[("r:embed", self.rid.as_str())]);
}
if let Some((l, t, r, b)) = self.src_rect {
let l_s = l.to_string();
let t_s = t.to_string();
let r_s = r.to_string();
let b_s = b.to_string();
w.empty_with(
"a:srcRect",
&[("l", &l_s), ("t", &t_s), ("r", &r_s), ("b", &b_s)],
);
}
self.fill_mode.write_xml(w);
w.close("p:blipFill");
self.properties.write_xml(w, "p:spPr");
if let Some(style) = &self.style {
style.write_xml(w);
}
write_extlst(w, self.ext_lst.as_ref());
w.close("p:pic");
}
}
#[derive(Clone, Debug, Default)]
pub struct Group {
pub id: u32,
pub name: String,
pub properties: ShapeProperties,
pub children: Vec<GroupChild>,
pub ext: (Emu, Emu),
pub off: (Emu, Emu),
pub style: Option<ShapeStyle>,
pub ext_lst: Option<ExtensionList>,
}
#[derive(Clone, Debug)]
pub enum GroupChild {
Sp(Sp),
Pic(Pic),
CxnSp(Connector),
Group(Box<Group>),
GraphicFrame(GraphicFrame),
}
impl Group {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open("p:grpSp");
w.open("p:nvGrpSpPr");
let id_s = self.id.to_string();
w.empty_with("p:cNvPr", &[("id", &id_s), ("name", &self.name)]);
w.empty("p:cNvGrpSpPr");
w.empty("p:nvPr");
w.close("p:nvGrpSpPr");
w.open("p:grpSpPr");
w.open("a:xfrm");
let off_x_s = self.off.0.value().to_string();
let off_y_s = self.off.1.value().to_string();
let ext_cx_s = self.ext.0.value().to_string();
let ext_cy_s = self.ext.1.value().to_string();
w.empty_with("a:off", &[("x", &off_x_s), ("y", &off_y_s)]);
w.empty_with("a:ext", &[("cx", &ext_cx_s), ("cy", &ext_cy_s)]);
w.empty_with("a:chOff", &[("x", "0"), ("y", "0")]);
w.empty_with("a:chExt", &[("cx", &ext_cx_s), ("cy", &ext_cy_s)]);
w.close("a:xfrm");
w.close("p:grpSpPr");
for c in &self.children {
match c {
GroupChild::Sp(s) => s.write_xml(w),
GroupChild::Pic(p) => p.write_xml(w),
GroupChild::CxnSp(c) => c.write_xml(w),
GroupChild::Group(g) => g.write_xml(w),
GroupChild::GraphicFrame(g) => g.write_xml(w),
}
}
if let Some(style) = &self.style {
style.write_xml(w);
}
write_extlst(w, self.ext_lst.as_ref());
w.close("p:grpSp");
}
}
#[derive(Clone, Debug, Default)]
pub struct Connector {
pub id: u32,
pub name: String,
pub properties: ShapeProperties,
pub begin: Option<(Emu, Emu)>,
pub end: Option<(Emu, Emu)>,
pub st_cxn: Option<(u32, u32)>,
pub end_cxn: Option<(u32, u32)>,
pub connector_type: Option<crate::oxml::simpletypes::MsoConnectorType>,
pub style: Option<ShapeStyle>,
pub ext_lst: Option<ExtensionList>,
}
impl Connector {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open("p:cxnSp");
w.open("p:nvCxnSpPr");
let id_s = self.id.to_string();
w.empty_with("p:cNvPr", &[("id", &id_s), ("name", &self.name)]);
w.empty("p:cNvCxnSpPr");
w.empty("p:nvPr");
if let Some((shape_id, idx)) = &self.st_cxn {
let sid = shape_id.to_string();
let idxs = idx.to_string();
w.empty_with("p:stCxn", &[("id", &sid), ("idx", &idxs)]);
}
if let Some((shape_id, idx)) = &self.end_cxn {
let sid = shape_id.to_string();
let idxs = idx.to_string();
w.empty_with("p:endCxn", &[("id", &sid), ("idx", &idxs)]);
}
w.close("p:nvCxnSpPr");
self.properties.write_xml(w, "p:spPr");
if let Some(style) = &self.style {
style.write_xml(w);
}
write_extlst(w, self.ext_lst.as_ref());
w.close("p:cxnSp");
}
}
#[derive(Clone, Debug, Default)]
pub struct GraphicFrame {
pub id: u32,
pub name: String,
pub properties: ShapeProperties,
pub graphic: Graphic,
pub ext_lst: Option<ExtensionList>,
pub is_placeholder: bool,
pub ph_idx: Option<u32>,
pub ph_type: Option<String>,
}
#[derive(Clone, Debug)]
pub enum Graphic {
Table(super::table::Table),
Chart(super::chart::Chart),
OleObject(super::ole::OleObject),
SmartArt(SmartArtRef),
}
#[derive(Clone, Debug, Default)]
pub struct SmartArtRef {
pub raw_xml: String,
pub dm_rid: Option<String>,
pub lo_rid: Option<String>,
pub qs_rid: Option<String>,
pub cs_rid: Option<String>,
}
impl SmartArtRef {
pub fn from_rids(dm_rid: &str, lo_rid: &str, qs_rid: &str, cs_rid: &str) -> Self {
let mut w = super::writer::XmlWriter::new();
w.open_with(
"a:graphicData",
&[(
"uri",
"http://schemas.openxmlformats.org/drawingml/2006/diagram",
)],
);
w.empty_with(
"dgm:relIds",
&[
("xmlns:dgm", crate::oxml::ns::NS_DIAGRAM),
("xmlns:r", crate::oxml::ns::NS_DRAWING_RELS),
("r:dm", dm_rid),
("r:lo", lo_rid),
("r:qs", qs_rid),
("r:cs", cs_rid),
],
);
w.close("a:graphicData");
Self {
raw_xml: w.into_string(),
dm_rid: Some(dm_rid.to_string()),
lo_rid: Some(lo_rid.to_string()),
qs_rid: Some(qs_rid.to_string()),
cs_rid: Some(cs_rid.to_string()),
}
}
}
impl Default for Graphic {
fn default() -> Self {
Graphic::Table(super::table::Table::default())
}
}
impl GraphicFrame {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open("p:graphicFrame");
w.open("p:nvGraphicFramePr");
let id_s = self.id.to_string();
w.empty_with("p:cNvPr", &[("id", &id_s), ("name", &self.name)]);
w.empty("p:cNvGraphicFramePr");
if self.is_placeholder {
w.open("p:nvPr");
let idx_s = self.ph_idx.map(|i| i.to_string());
let mut pattrs: Vec<(&str, &str)> = Vec::new();
if let Some(t) = self.ph_type.as_deref() {
pattrs.push(("type", t));
}
if let Some(s) = idx_s.as_deref() {
pattrs.push(("idx", s));
}
w.empty_with("p:ph", &pattrs);
w.close("p:nvPr");
} else {
w.empty("p:nvPr");
}
w.close("p:nvGraphicFramePr");
self.properties.write_xfrm_only(w, "p:xfrm");
w.open("a:graphic");
if let Graphic::SmartArt(s) = &self.graphic {
w.raw(&s.raw_xml);
} else {
let uri = match &self.graphic {
Graphic::Table(_) => "http://schemas.openxmlformats.org/drawingml/2006/table",
Graphic::Chart(_) => "http://schemas.openxmlformats.org/drawingml/2006/chart",
Graphic::OleObject(_) => super::ole::OLE_GRAPHIC_DATA_URI,
Graphic::SmartArt(_) => unreachable!(), };
w.open_with("a:graphicData", &[("uri", uri)]);
match &self.graphic {
Graphic::Table(t) => {
t.write_xml(w);
}
Graphic::Chart(c) => {
let rid = if c.rid.is_empty() {
"rId1"
} else {
c.rid.as_str()
};
w.empty_with(
"c:chart",
&[
("xmlns:c", "http://schemas.openxmlformats.org/drawingml/2006/chart"),
("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"),
("r:id", rid),
],
);
}
Graphic::OleObject(o) => {
o.write_xml(w);
}
Graphic::SmartArt(_) => unreachable!(),
}
w.close("a:graphicData");
}
w.close("a:graphic");
write_extlst(w, self.ext_lst.as_ref());
w.close("p:graphicFrame");
}
}
#[derive(Clone, Debug, Default)]
pub struct ShapeStyle {
pub line_ref: Option<StyleRef>,
pub fill_ref: Option<StyleRef>,
pub effect_ref: Option<StyleRef>,
pub font_ref: Option<StyleRef>,
}
impl ShapeStyle {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open("p:style");
if let Some(r) = &self.line_ref {
write_style_ref(w, "a:lnRef", r);
}
if let Some(r) = &self.fill_ref {
write_style_ref(w, "a:fillRef", r);
}
if let Some(r) = &self.effect_ref {
write_style_ref(w, "a:effectRef", r);
}
if let Some(r) = &self.font_ref {
let idx_attr = r.idx.as_deref().unwrap_or("minor");
w.open_with("a:fontRef", &[("idx", idx_attr)]);
if let Some(scheme) = &r.scheme_color {
w.empty_with("a:schemeClr", &[("val", scheme)]);
}
w.close("a:fontRef");
}
w.close("p:style");
}
}
#[derive(Clone, Debug, Default)]
pub struct StyleRef {
pub idx: Option<String>,
pub scheme_color: Option<String>,
}
fn write_style_ref(w: &mut super::writer::XmlWriter, tag: &str, r: &StyleRef) {
let idx_s = r.idx.clone().unwrap_or_else(|| "1".to_string());
w.open_with(tag, &[("idx", &idx_s)]);
if let Some(scheme) = &r.scheme_color {
w.empty_with("a:schemeClr", &[("val", scheme)]);
}
w.close(tag);
}
#[derive(Clone, Debug, Default)]
pub struct ExtensionList {
pub entries: Vec<ExtensionEntry>,
}
#[derive(Clone, Debug)]
pub struct ExtensionEntry {
pub uri: String,
pub xml: String,
}
impl ExtensionList {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
if self.entries.is_empty() {
return;
}
w.open("p:extLst");
for e in &self.entries {
w.open_with("p:ext", &[("uri", e.uri.as_str())]);
w.raw(&e.xml);
w.close("p:ext");
}
w.close("p:extLst");
}
}
fn write_extlst(w: &mut super::writer::XmlWriter, ext: Option<&ExtensionList>) {
if let Some(e) = ext {
e.write_xml(w);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shape_locks_serialization() {
let locks = ShapeLocks {
no_grp: true,
no_select: true,
no_resize: true,
..Default::default()
};
let mut w = super::super::writer::XmlWriter::new();
locks.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("noGrp=\"1\""), "应包含 noGrp=1,实际: {s}");
assert!(s.contains("noSelect=\"1\""), "应包含 noSelect=1,实际: {s}");
assert!(s.contains("noResize=\"1\""), "应包含 noResize=1,实际: {s}");
assert!(!s.contains("noMove"), "不应包含 noMove,实际: {s}");
assert!(!s.contains("noRot"), "不应包含 noRot,实际: {s}");
}
#[test]
fn shape_locks_is_empty() {
let empty = ShapeLocks::default();
assert!(empty.is_empty());
let non_empty = ShapeLocks {
no_grp: true,
..Default::default()
};
assert!(!non_empty.is_empty());
}
#[test]
fn sp_with_locks_serialization() {
let sp = Sp {
id: 1,
name: "Test".into(),
locks: Some(ShapeLocks {
no_select: true,
no_move: true,
..Default::default()
}),
..Default::default()
};
let mut w = super::super::writer::XmlWriter::new();
sp.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("a:spLocks"), "应包含 a:spLocks,实际: {s}");
assert!(s.contains("noSelect=\"1\""), "应包含 noSelect=1,实际: {s}");
assert!(s.contains("noMove=\"1\""), "应包含 noMove=1,实际: {s}");
assert!(
s.contains("<p:cNvSpPr>") || s.contains("<p:cNvSpPr "),
"cNvSpPr 应为 open 标签,实际: {s}"
);
assert!(s.contains("</p:cNvSpPr>"), "应包含 </p:cNvSpPr>,实际: {s}");
}
#[test]
fn pic_with_video_media_serialize() {
let pic = Pic {
id: 1,
name: "Video 1".into(),
rid: "rIdImg1".into(),
media: Some(MediaKind::Video {
rid: "rIdVideo1".into(),
}),
..Default::default()
};
let mut w = super::super::writer::XmlWriter::new();
pic.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("<a:videoFile"), "应包含 a:videoFile,实际: {s}");
assert!(
s.contains("r:link=\"rIdVideo1\""),
"应包含 r:link=\"rIdVideo1\",实际: {s}"
);
assert!(
!s.contains("r:embed=\"rIdVideo1\""),
"视频不应使用 r:embed,实际: {s}"
);
assert!(
s.contains("xmlns:r="),
"nvPr 应声明 xmlns:r 命名空间,实际: {s}"
);
}
#[test]
fn pic_with_audio_media_serialize() {
let pic = Pic {
id: 2,
name: "Audio 1".into(),
rid: "rIdImg2".into(),
media: Some(MediaKind::Audio {
rid: "rIdAudio1".into(),
}),
..Default::default()
};
let mut w = super::super::writer::XmlWriter::new();
pic.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("<a:audioFile"), "应包含 a:audioFile,实际: {s}");
assert!(
s.contains("r:link=\"rIdAudio1\""),
"应包含 r:link=\"rIdAudio1\",实际: {s}"
);
}
#[test]
fn pic_without_media_no_video_file() {
let pic = Pic {
id: 3,
name: "Plain Picture".into(),
rid: "rIdImg3".into(),
..Default::default()
};
let mut w = super::super::writer::XmlWriter::new();
pic.write_xml(&mut w);
let s = w.into_string();
assert!(
!s.contains("videoFile"),
"普通图片不应包含 videoFile,实际: {s}"
);
assert!(
!s.contains("audioFile"),
"普通图片不应包含 audioFile,实际: {s}"
);
}
#[test]
fn media_kind_eq_and_default() {
let d = MediaKind::default();
assert!(matches!(d, MediaKind::Video { ref rid } if rid.is_empty()));
let v1 = MediaKind::Video { rid: "rId1".into() };
let v2 = MediaKind::Video { rid: "rId1".into() };
let v3 = MediaKind::Video { rid: "rId2".into() };
let a1 = MediaKind::Audio { rid: "rId1".into() };
assert_eq!(v1, v2, "相同 rid 的 Video 应相等");
assert_ne!(v1, v3, "不同 rid 的 Video 应不等");
assert_ne!(v1, a1, "Video 与 Audio 即使 rid 相同也不等");
}
#[test]
fn graphic_frame_with_smartart_serialize() {
let raw_xml =
"<a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\">\
<dgm:relIds r:dm=\"rId1\" r:lo=\"rId2\" r:qs=\"rId3\" r:cs=\"rId4\"/>\
</a:graphicData>";
let frame = GraphicFrame {
id: 100,
name: "SmartArt 1".into(),
graphic: Graphic::SmartArt(SmartArtRef {
raw_xml: raw_xml.into(),
dm_rid: Some("rId1".into()),
lo_rid: Some("rId2".into()),
qs_rid: Some("rId3".into()),
cs_rid: Some("rId4".into()),
}),
..Default::default()
};
let mut w = super::super::writer::XmlWriter::new();
frame.write_xml(&mut w);
let s = w.into_string();
assert!(
s.contains("uri=\"http://schemas.openxmlformats.org/drawingml/2006/diagram\""),
"应包含 diagram uri,实际: {s}"
);
assert!(
s.contains("<dgm:relIds r:dm=\"rId1\" r:lo=\"rId2\" r:qs=\"rId3\" r:cs=\"rId4\"/>"),
"应包含原始 dgm:relIds,实际: {s}"
);
let count = s.matches("<a:graphicData").count();
assert_eq!(
count, 1,
"SmartArt 不应重复写出 graphicData 外壳,实际出现 {} 次: {s}",
count
);
}
#[test]
fn smartart_ref_default() {
let r = SmartArtRef::default();
assert!(r.raw_xml.is_empty(), "默认 raw_xml 应为空");
assert!(r.dm_rid.is_none(), "默认 dm_rid 应为 None");
assert!(r.lo_rid.is_none(), "默认 lo_rid 应为 None");
assert!(r.qs_rid.is_none(), "默认 qs_rid 应为 None");
assert!(r.cs_rid.is_none(), "默认 cs_rid 应为 None");
}
}