use crate::oxml::color::Color;
use crate::oxml::simpletypes::{
Alignment, MsoAnchor, MsoAutoSize, TabAlignment, TextWrapping, Underline,
};
use crate::units::{Emu, Pt, RGBColor};
#[derive(Copy, Clone, Debug, Default)]
pub struct TabStop {
pub pos: Emu,
pub alignment: TabAlignment,
}
#[derive(Copy, Clone, Debug, Default)]
pub struct Indent {
pub left: Option<Emu>,
pub right: Option<Emu>,
pub first_line: Option<Emu>,
pub hanging: Option<i32>,
}
#[derive(Clone, Debug, Default)]
pub enum BulletStyle {
#[default]
None,
Char {
char: String,
},
AutoNum {
auto_num_type: String,
start_at: Option<u32>,
},
}
#[derive(Clone, Debug, Default)]
pub struct ParagraphProperties {
pub alignment: Option<Alignment>,
pub indent: Indent,
pub line_spacing: Option<i32>,
pub line_spacing_pct: Option<i32>,
pub space_before: Option<Emu>,
pub space_after: Option<Emu>,
pub bullet: bool,
pub bullet_style: Option<BulletStyle>,
pub level: u8,
pub default_run_properties: Option<RunProperties>,
pub tab_stops: Vec<TabStop>,
}
impl ParagraphProperties {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
if self.alignment.is_none()
&& self.indent.left.is_none()
&& self.indent.right.is_none()
&& self.indent.first_line.is_none()
&& self.indent.hanging.is_none()
&& self.line_spacing.is_none()
&& self.line_spacing_pct.is_none()
&& self.space_before.is_none()
&& self.space_after.is_none()
&& !self.bullet
&& self.bullet_style.is_none()
&& self.level == 0
&& self.default_run_properties.is_none()
&& self.tab_stops.is_empty()
{
return;
}
let mut attrs: Vec<(&str, &str)> = Vec::new();
if let Some(a) = self.alignment {
attrs.push(("algn", a.as_str()));
}
let lvl_s = if self.level != 0 {
Some(self.level.to_string())
} else {
None
};
if let Some(s) = &lvl_s {
attrs.push(("lvl", s));
}
w.open_with("a:pPr", &attrs);
if self.indent.left.is_some()
|| self.indent.right.is_some()
|| self.indent.first_line.is_some()
|| self.indent.hanging.is_some()
{
let l_s = self.indent.left.map(|v| v.value().to_string());
let r_s = self.indent.right.map(|v| v.value().to_string());
let first_s = self.indent.first_line.map(|v| v.value().to_string());
let hang_s = self.indent.hanging.map(|v| v.to_string());
let mut iattrs: Vec<(&str, &str)> = Vec::new();
if let Some(s) = &l_s {
iattrs.push(("l", s));
}
if let Some(s) = &r_s {
iattrs.push(("r", s));
}
if let Some(s) = &first_s {
iattrs.push(("firstLine", s));
}
if let Some(s) = &hang_s {
iattrs.push(("hanging", s));
}
w.empty_with("a:indent", &iattrs);
}
if self.line_spacing.is_some() || self.line_spacing_pct.is_some() {
if let Some(p) = self.line_spacing_pct {
let s = p.to_string();
w.open("a:lnSpc");
w.empty_with("a:spcPct", &[("val", &s)]);
w.close("a:lnSpc");
}
if let Some(sp) = self.line_spacing {
let s = sp.to_string();
w.open("a:lnSpc");
w.empty_with("a:spcPts", &[("val", &s)]);
w.close("a:lnSpc");
}
}
if self.space_before.is_some() || self.space_after.is_some() {
let sb_s = self.space_before.map(|v| v.value().to_string());
let sa_s = self.space_after.map(|v| v.value().to_string());
let mut sattrs: Vec<(&str, &str)> = Vec::new();
if let Some(s) = &sb_s {
sattrs.push(("spcBef", s));
}
if let Some(s) = &sa_s {
sattrs.push(("spcAft", s));
}
w.empty_with("a:spcBef", &sattrs);
}
if let Some(bs) = &self.bullet_style {
match bs {
BulletStyle::None => {
w.empty("a:buNone");
}
BulletStyle::Char { char } => {
w.empty_with("a:buChar", &[("char", char.as_str())]);
}
BulletStyle::AutoNum {
auto_num_type,
start_at,
} => {
if let Some(sa) = start_at {
let sa_s = sa.to_string();
w.empty_with(
"a:buAutoNum",
&[("type", auto_num_type.as_str()), ("startAt", sa_s.as_str())],
);
} else {
w.empty_with("a:buAutoNum", &[("type", auto_num_type.as_str())]);
}
}
}
}
if !self.tab_stops.is_empty() {
w.open("a:tabLst");
for tab in &self.tab_stops {
let pos_s = tab.pos.value().to_string();
w.empty_with(
"a:tab",
&[("pos", pos_s.as_str()), ("algn", tab.alignment.as_str())],
);
}
w.close("a:tabLst");
}
if let Some(rpr) = &self.default_run_properties {
rpr.write_xml(w, "a:defRPr");
}
w.close("a:pPr");
}
}
#[derive(Clone, Debug, Default)]
pub struct Hyperlink {
pub rid: Option<String>,
pub tooltip: Option<String>,
pub action: Option<String>,
pub invalid: bool,
}
impl Hyperlink {
pub fn new(rid: impl Into<String>) -> Self {
Self {
rid: Some(rid.into()),
..Default::default()
}
}
pub fn new_slide_jump() -> Self {
Self {
action: Some("ppaction://hlinksldjump".to_string()),
..Default::default()
}
}
}
#[derive(Clone, Debug, Default)]
pub struct RunProperties {
pub size: Option<Pt>,
pub bold: bool,
pub italic: bool,
pub underline: Option<Underline>,
pub strike: bool,
pub strike_dbl: bool,
pub color: Color,
pub highlight: Option<Color>,
pub latin_font: Option<String>,
pub eastasia_font: Option<String>,
pub cs_font: Option<String>,
pub baseline: Option<i32>,
pub kerning: Option<i32>,
pub spc: Option<i32>,
pub caps: Caps,
pub lang: Option<String>,
pub alpha: Option<i32>,
pub hlink_click: Option<Hyperlink>,
pub hlink_hover: Option<Hyperlink>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
pub enum Caps {
#[default]
None,
Small,
All,
}
impl Caps {
pub fn as_str(self) -> &'static str {
match self {
Caps::None => "none",
Caps::Small => "small",
Caps::All => "all",
}
}
}
impl RunProperties {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
let sz_s = self
.size
.map(|sz| ((sz.value() * 100.0) as i32).to_string());
let baseline_s = self.baseline.map(|v| v.to_string());
let kerning_s = self.kerning.map(|v| v.to_string());
let spc_s = self.spc.map(|v| v.to_string());
let mut attrs: Vec<(&str, &str)> = Vec::new();
if let Some(s) = &sz_s {
attrs.push(("sz", s));
}
if self.bold {
attrs.push(("b", "1"));
}
if self.italic {
attrs.push(("i", "1"));
}
if let Some(u) = self.underline {
attrs.push(("u", u.as_str()));
}
if self.strike {
attrs.push(("strike", "sngStrike"));
}
if self.strike_dbl {
attrs.push(("strike", "dblStrike"));
}
if self.caps != Caps::None {
attrs.push(("cap", self.caps.as_str()));
}
if let Some(s) = &baseline_s {
attrs.push(("baseline", s));
}
if let Some(s) = &kerning_s {
attrs.push(("kern", s));
}
if let Some(s) = &spc_s {
attrs.push(("spc", s));
}
if let Some(l) = &self.lang {
attrs.push(("lang", l));
}
w.open_with(tag, &attrs);
if !matches!(self.color, Color::None) {
self.color.write_solid_fill_with_alpha(w, self.alpha);
} else if self.alpha.is_some() {
w.open("a:solidFill");
w.empty_with("a:srgbClr", &[("val", "000000")]);
if let Some(a) = self.alpha {
w.empty_with("a:alpha", &[("val", a.to_string().as_str())]);
}
w.close("a:srgbClr");
w.close("a:solidFill");
}
if let Some(h) = &self.highlight {
h.write_solid_fill(w);
}
if let Some(latin) = &self.latin_font {
w.empty_with("a:latin", &[("typeface", latin)]);
}
if let Some(ea) = &self.eastasia_font {
w.empty_with("a:ea", &[("typeface", ea)]);
}
if let Some(cs) = &self.cs_font {
w.empty_with("a:cs", &[("typeface", cs)]);
}
if let Some(h) = &self.hlink_click {
let mut hattrs: Vec<(&str, &str)> = Vec::new();
if let Some(rid) = &h.rid {
hattrs.push(("r:id", rid.as_str()));
}
if let Some(tip) = &h.tooltip {
hattrs.push(("tooltip", tip.as_str()));
}
if let Some(act) = &h.action {
hattrs.push(("action", act.as_str()));
}
if hattrs.is_empty() {
w.empty("a:hlinkClick");
} else {
w.empty_with("a:hlinkClick", &hattrs);
}
}
if let Some(h) = &self.hlink_hover {
let mut hattrs: Vec<(&str, &str)> = Vec::new();
if let Some(rid) = &h.rid {
hattrs.push(("r:id", rid.as_str()));
}
if let Some(tip) = &h.tooltip {
hattrs.push(("tooltip", tip.as_str()));
}
if hattrs.is_empty() {
w.empty("a:hlinkHover");
} else {
w.empty_with("a:hlinkHover", &hattrs);
}
}
w.close(tag);
}
#[doc(hidden)]
pub fn from_attrs_unused(_attrs: &super::parser::AttrMap) -> Self {
RunProperties::default()
}
pub fn copy(&self) -> Self {
self.clone()
}
}
#[derive(Clone, Debug, Default)]
pub struct Run {
pub text: String,
pub properties: RunProperties,
}
impl Run {
pub fn new(text: impl Into<String>) -> Self {
Run {
text: text.into(),
properties: RunProperties::default(),
}
}
pub fn line_break() -> Self {
Run {
text: String::from("\n"),
properties: RunProperties::default(),
}
}
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
if self.text == "\n" {
if !is_default_rpr(&self.properties) {
w.empty("a:br");
} else {
w.empty("a:br");
}
return;
}
w.open("a:r");
if !is_default_rpr(&self.properties) {
self.properties.write_xml(w, "a:rPr");
}
w.open("a:t");
w.text(&self.text);
w.close("a:t");
w.close("a:r");
}
pub fn text(&self) -> &str {
&self.text
}
pub fn set_text(&mut self, t: impl Into<String>) {
self.text = t.into();
}
pub fn size(&self) -> Option<Pt> {
self.properties.size
}
pub fn set_size(&mut self, v: Pt) {
self.properties.size = Some(v);
}
pub fn bold(&self) -> bool {
self.properties.bold
}
pub fn set_bold(&mut self, v: bool) {
self.properties.bold = v;
}
pub fn italic(&self) -> bool {
self.properties.italic
}
pub fn set_italic(&mut self, v: bool) {
self.properties.italic = v;
}
pub fn color(&self) -> Color {
self.properties.color.clone()
}
pub fn set_color(&mut self, c: impl Into<Color>) {
self.properties.color = c.into();
}
pub fn font_name(&self) -> Option<&str> {
self.properties.latin_font.as_deref()
}
pub fn set_font_name(&mut self, name: impl Into<String>) {
self.properties.latin_font = Some(name.into());
}
pub fn eastasia_name(&self) -> Option<&str> {
self.properties.eastasia_font.as_deref()
}
pub fn set_eastasia_name(&mut self, name: impl Into<String>) {
self.properties.eastasia_font = Some(name.into());
}
pub fn complex_script_name(&self) -> Option<&str> {
self.properties.cs_font.as_deref()
}
pub fn set_complex_script_name(&mut self, name: impl Into<String>) {
self.properties.cs_font = Some(name.into());
}
pub fn underline(&self) -> Option<Underline> {
self.properties.underline
}
pub fn set_underline(&mut self, v: Underline) {
self.properties.underline = Some(v);
}
pub fn strike(&self) -> bool {
self.properties.strike
}
pub fn set_strike(&mut self, v: bool) {
self.properties.strike = v;
}
pub fn double_strike(&self) -> bool {
self.properties.strike_dbl
}
pub fn set_double_strike(&mut self, v: bool) {
self.properties.strike_dbl = v;
}
pub fn highlight(&self) -> Option<&Color> {
self.properties.highlight.as_ref()
}
pub fn set_highlight(&mut self, color: Option<Color>) {
self.properties.highlight = color;
}
pub fn clear_highlight(&mut self) {
self.properties.highlight = None;
}
pub fn hlink_click(&self) -> Option<&Hyperlink> {
self.properties.hlink_click.as_ref()
}
pub fn set_hlink_click(&mut self, hl: Hyperlink) {
self.properties.hlink_click = Some(hl);
}
pub fn clear_hlink_click(&mut self) {
self.properties.hlink_click = None;
}
pub fn hlink_hover(&self) -> Option<&Hyperlink> {
self.properties.hlink_hover.as_ref()
}
pub fn set_hlink_hover(&mut self, hl: Hyperlink) {
self.properties.hlink_hover = Some(hl);
}
pub fn clear_hlink_hover(&mut self) {
self.properties.hlink_hover = None;
}
pub fn set_hyperlink(&mut self, rid: impl Into<String>, tooltip: Option<&str>) {
let mut hl = Hyperlink::new(rid);
if let Some(t) = tooltip {
hl.tooltip = Some(t.to_string());
}
self.properties.hlink_click = Some(hl);
}
pub fn set_slide_jump(&mut self) {
self.properties.hlink_click = Some(Hyperlink::new_slide_jump());
}
pub fn font(&mut self) -> Font<'_> {
Font::new(&mut self.properties)
}
}
fn is_default_rpr(p: &RunProperties) -> bool {
p.size.is_none()
&& !p.bold
&& !p.italic
&& p.underline.is_none()
&& !p.strike
&& !p.strike_dbl
&& matches!(p.color, Color::None)
&& p.highlight.is_none()
&& p.latin_font.is_none()
&& p.eastasia_font.is_none()
&& p.cs_font.is_none()
&& p.baseline.is_none()
&& p.kerning.is_none()
&& p.spc.is_none()
&& p.caps == Caps::None
&& p.lang.is_none()
&& p.alpha.is_none()
&& p.hlink_click.is_none()
&& p.hlink_hover.is_none()
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum FieldType {
#[default]
SlideNumber,
DateTime,
DateTime1,
DateTime2,
DateTime3,
Footer,
Custom(String),
}
impl FieldType {
pub fn as_str(&self) -> &str {
match self {
FieldType::SlideNumber => "slidenum",
FieldType::DateTime => "datetime",
FieldType::DateTime1 => "datetime1",
FieldType::DateTime2 => "datetime2",
FieldType::DateTime3 => "datetime3",
FieldType::Footer => "footer",
FieldType::Custom(s) => s.as_str(),
}
}
pub fn from_str_value(s: &str) -> Self {
match s {
"slidenum" => FieldType::SlideNumber,
"datetime" => FieldType::DateTime,
"datetime1" => FieldType::DateTime1,
"datetime2" => FieldType::DateTime2,
"datetime3" => FieldType::DateTime3,
"footer" => FieldType::Footer,
other => FieldType::Custom(other.to_string()),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Field {
pub id: String,
pub field_type: FieldType,
pub properties: RunProperties,
pub text: String,
}
impl Field {
pub fn new(field_type: FieldType, text: impl Into<String>) -> Self {
Field {
id: format!("{{{}}}", uuid_like()),
field_type,
properties: RunProperties::default(),
text: text.into(),
}
}
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open_with(
"a:fld",
&[("id", self.id.as_str()), ("type", self.field_type.as_str())],
);
self.properties.write_xml(w, "a:rPr");
w.open("a:t");
w.text(&self.text);
w.close("a:t");
w.close("a:fld");
}
}
fn uuid_like() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
format!("{:016x}-{:016x}", ts, n)
}
#[derive(Clone, Debug, Default)]
pub struct Paragraph {
pub properties: ParagraphProperties,
pub runs: Vec<Run>,
pub fields: Vec<Field>,
pub end_properties: Option<RunProperties>,
}
impl Paragraph {
pub fn new() -> Self {
Paragraph::default()
}
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open("a:p");
self.properties.write_xml(w);
for r in &self.runs {
r.write_xml(w);
}
for f in &self.fields {
f.write_xml(w);
}
if let Some(rpr) = &self.end_properties {
rpr.write_xml(w, "a:endParaRPr");
}
w.close("a:p");
}
pub fn add_run(&mut self) -> &mut Run {
self.runs.push(Run::default());
let idx = self.runs.len() - 1;
&mut self.runs[idx]
}
pub fn add_run_with_text(&mut self, text: impl Into<String>) -> &mut Run {
self.runs.push(Run::new(text));
let idx = self.runs.len() - 1;
&mut self.runs[idx]
}
pub fn add_line_break(&mut self) -> &mut Run {
self.runs.push(Run {
text: String::from("\n"),
properties: RunProperties::default(),
});
let idx = self.runs.len() - 1;
&mut self.runs[idx]
}
pub fn clear_runs(&mut self) {
self.runs.clear();
}
pub fn add_field(&mut self, field_type: FieldType, text: impl Into<String>) -> &mut Field {
self.fields.push(Field::new(field_type, text));
let idx = self.fields.len() - 1;
&mut self.fields[idx]
}
pub fn clear_fields(&mut self) {
self.fields.clear();
}
pub fn set_text(&mut self, text: impl Into<String>) -> &mut Run {
self.runs.clear();
self.runs.push(Run::new(text));
let idx = self.runs.len() - 1;
&mut self.runs[idx]
}
pub fn text(&self) -> String {
let mut out = String::new();
for r in &self.runs {
out.push_str(&r.text);
}
out
}
pub fn alignment(&self) -> Option<Alignment> {
self.properties.alignment
}
pub fn set_alignment(&mut self, v: Alignment) {
self.properties.alignment = Some(v);
}
pub fn level(&self) -> u8 {
self.properties.level
}
pub fn set_level(&mut self, lvl: u8) {
self.properties.level = lvl;
}
pub fn line_spacing(&self) -> Option<Pt> {
self.properties
.line_spacing
.map(|emu| Pt(emu as f64 / 12_700.0))
}
pub fn set_line_spacing(&mut self, v: Pt) {
let emu = (v.value() * 12_700.0) as i32;
self.properties.line_spacing = Some(emu);
self.properties.line_spacing_pct = None;
}
pub fn line_spacing_pct(&self) -> Option<f32> {
self.properties.line_spacing_pct.map(|v| v as f32 / 1000.0)
}
pub fn set_line_spacing_pct(&mut self, v: f32) {
self.properties.line_spacing_pct = Some((v * 1000.0) as i32);
self.properties.line_spacing = None;
}
pub fn space_before(&self) -> Option<Emu> {
self.properties.space_before
}
pub fn set_space_before(&mut self, emu: Emu) {
self.properties.space_before = Some(emu);
}
pub fn space_after(&self) -> Option<Emu> {
self.properties.space_after
}
pub fn set_space_after(&mut self, emu: Emu) {
self.properties.space_after = Some(emu);
}
pub fn indent(&self) -> Indent {
self.properties.indent
}
pub fn set_indent(
&mut self,
left: Option<Emu>,
right: Option<Emu>,
first_line: Option<Emu>,
hanging: Option<i32>,
) {
self.properties.indent = Indent {
left,
right,
first_line,
hanging,
};
}
pub fn end_para_rpr(&self) -> Option<&RunProperties> {
self.end_properties.as_ref()
}
pub fn end_para_rpr_mut(&mut self) -> Option<&mut RunProperties> {
self.end_properties.as_mut()
}
pub fn set_end_para_rpr(&mut self, rpr: RunProperties) {
self.end_properties = Some(rpr);
}
pub fn clear_end_para_rpr(&mut self) {
self.end_properties = None;
}
}
#[derive(Clone, Debug, Default)]
pub struct TextBody {
pub body_properties: Option<BodyProperties>,
pub paragraphs: Vec<Paragraph>,
}
impl TextBody {
pub fn new() -> Self {
TextBody::default()
}
pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
w.open(tag);
if let Some(bp) = &self.body_properties {
bp.write_xml(w);
}
for p in &self.paragraphs {
p.write_xml(w);
}
w.close(tag);
}
pub fn text(&self) -> String {
let mut out = String::new();
for (i, p) in self.paragraphs.iter().enumerate() {
if i > 0 {
out.push('\n');
}
for r in &p.runs {
out.push_str(&r.text);
}
}
out
}
pub fn first_paragraph(&self) -> Option<&Paragraph> {
self.paragraphs.first()
}
pub fn first_paragraph_mut(&mut self) -> Option<&mut Paragraph> {
self.paragraphs.first_mut()
}
pub fn paragraph(&self, idx: usize) -> Option<&Paragraph> {
self.paragraphs.get(idx)
}
pub fn paragraph_mut(&mut self, idx: usize) -> Option<&mut Paragraph> {
self.paragraphs.get_mut(idx)
}
pub fn add_paragraph(&mut self) -> &mut Paragraph {
self.paragraphs.push(Paragraph::new());
let idx = self.paragraphs.len() - 1;
&mut self.paragraphs[idx]
}
pub fn add_paragraph_with_text(&mut self, text: &str) -> &mut Paragraph {
for line in text.split('\n') {
let mut p = Paragraph::new();
p.runs.push(Run::new(line));
self.paragraphs.push(p);
}
let idx = self.paragraphs.len() - 1;
&mut self.paragraphs[idx]
}
pub fn remove_paragraph(&mut self, idx: usize) -> Option<Paragraph> {
if idx < self.paragraphs.len() {
Some(self.paragraphs.remove(idx))
} else {
None
}
}
pub fn clear(&mut self) {
self.paragraphs.clear();
self.paragraphs.push(Paragraph::new());
}
pub fn set_text(&mut self, text: &str) {
self.paragraphs.clear();
for line in text.split('\n') {
let mut p = Paragraph::new();
p.runs.push(Run::new(line));
self.paragraphs.push(p);
}
}
fn ensure_body_properties(&mut self) -> &mut BodyProperties {
if self.body_properties.is_none() {
self.body_properties = Some(BodyProperties::default());
}
match &mut self.body_properties {
Some(bp) => bp,
None => unreachable!("body_properties was just initialized above"),
}
}
pub fn auto_size(&self) -> MsoAutoSize {
self.body_properties
.as_ref()
.and_then(|b| b.auto_size())
.unwrap_or(MsoAutoSize::None)
}
pub fn set_auto_size(&mut self, v: MsoAutoSize) {
self.ensure_body_properties().set_auto_size(v);
}
pub fn vertical_anchor(&self) -> Option<MsoAnchor> {
self.body_properties.as_ref().and_then(|b| b.anchor)
}
pub fn set_vertical_anchor(&mut self, v: MsoAnchor) {
self.ensure_body_properties().anchor = Some(v);
}
pub fn word_wrap(&self) -> Option<bool> {
self.body_properties.as_ref().and_then(|b| match b.wrap {
Some(TextWrapping::Square) => Some(true),
Some(TextWrapping::None) => Some(false),
None => None,
})
}
pub fn set_word_wrap(&mut self, v: bool) {
self.ensure_body_properties().wrap = Some(if v {
TextWrapping::Square
} else {
TextWrapping::None
});
}
pub fn margin_left(&self) -> Option<Emu> {
self.body_properties
.as_ref()
.and_then(|b| b.insets.as_ref().map(|i| i.left))
}
pub fn margin_right(&self) -> Option<Emu> {
self.body_properties
.as_ref()
.and_then(|b| b.insets.as_ref().map(|i| i.right))
}
pub fn margin_top(&self) -> Option<Emu> {
self.body_properties
.as_ref()
.and_then(|b| b.insets.as_ref().map(|i| i.top))
}
pub fn margin_bottom(&self) -> Option<Emu> {
self.body_properties
.as_ref()
.and_then(|b| b.insets.as_ref().map(|i| i.bottom))
}
pub fn set_margins(&mut self, left: Emu, top: Emu, right: Emu, bottom: Emu) {
let bp = self.ensure_body_properties();
bp.insets = Some(Inset {
left,
top,
right,
bottom,
});
}
pub fn set_margin_left(&mut self, emu: Emu) {
let bp = self.ensure_body_properties();
let i = bp.insets.get_or_insert(Inset::default());
i.left = emu;
}
pub fn set_margin_right(&mut self, emu: Emu) {
let bp = self.ensure_body_properties();
let i = bp.insets.get_or_insert(Inset::default());
i.right = emu;
}
pub fn set_margin_top(&mut self, emu: Emu) {
let bp = self.ensure_body_properties();
let i = bp.insets.get_or_insert(Inset::default());
i.top = emu;
}
pub fn set_margin_bottom(&mut self, emu: Emu) {
let bp = self.ensure_body_properties();
let i = bp.insets.get_or_insert(Inset::default());
i.bottom = emu;
}
pub fn num_cols(&self) -> Option<u32> {
self.body_properties.as_ref().and_then(|b| b.num_cols)
}
pub fn set_num_cols(&mut self, count: u32) {
let bp = self.ensure_body_properties();
bp.num_cols = Some(count);
}
pub fn col_spacing(&self) -> Option<Emu> {
self.body_properties.as_ref().and_then(|b| b.col_spacing)
}
pub fn set_col_spacing(&mut self, emu: Emu) {
let bp = self.ensure_body_properties();
bp.col_spacing = Some(emu);
}
}
#[derive(Clone, Debug, Default)]
pub struct BodyProperties {
pub insets: Option<Inset>,
pub vertical: Option<String>,
pub rotation: Option<i32>,
pub wrap: Option<TextWrapping>,
pub sp_auto_fit: bool,
pub norm_autofit: bool,
pub anchor: Option<MsoAnchor>,
pub anchor_ctr: bool,
pub num_cols: Option<u32>,
pub col_spacing: Option<Emu>,
}
#[derive(Copy, Clone, Debug, Default)]
pub struct Inset {
pub left: Emu,
pub top: Emu,
pub right: Emu,
pub bottom: Emu,
}
impl BodyProperties {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
let l_s = self.insets.as_ref().map(|i| i.left.value().to_string());
let t_s = self.insets.as_ref().map(|i| i.top.value().to_string());
let r_s = self.insets.as_ref().map(|i| i.right.value().to_string());
let b_s = self.insets.as_ref().map(|i| i.bottom.value().to_string());
let rot_s = self.rotation.map(|v| v.to_string());
let wrap_s = self.wrap.map(|v| v.as_str());
let anchor_s = self.anchor.map(|v| v.as_str());
let numcol_s = self.num_cols.map(|v| v.to_string());
let spccol_s = self.col_spacing.map(|v| v.value().to_string());
let mut attrs: Vec<(&str, &str)> = Vec::new();
if let Some(s) = &l_s {
attrs.push(("lIns", s));
}
if let Some(s) = &t_s {
attrs.push(("tIns", s));
}
if let Some(s) = &r_s {
attrs.push(("rIns", s));
}
if let Some(s) = &b_s {
attrs.push(("bIns", s));
}
if let Some(v) = &self.vertical {
attrs.push(("vert", v));
}
if let Some(s) = &rot_s {
attrs.push(("rot", s));
}
if let Some(s) = &wrap_s {
attrs.push(("wrap", s));
}
if let Some(s) = &anchor_s {
attrs.push(("anchor", s));
}
if let Some(s) = &numcol_s {
attrs.push(("numCol", s));
}
if let Some(s) = &spccol_s {
attrs.push(("spcCol", s));
}
w.open_with("a:bodyPr", &attrs);
if self.sp_auto_fit {
w.empty("a:spAutoFit");
} else if self.norm_autofit {
w.empty("a:normAutofit");
}
w.close("a:bodyPr");
}
pub fn auto_size(&self) -> Option<MsoAutoSize> {
if self.sp_auto_fit {
Some(MsoAutoSize::ShapeToFitText)
} else if self.norm_autofit {
Some(MsoAutoSize::TextToFitShape)
} else {
None
}
}
pub fn set_auto_size(&mut self, v: MsoAutoSize) {
match v {
MsoAutoSize::None => {
self.sp_auto_fit = false;
self.norm_autofit = false;
}
MsoAutoSize::ShapeToFitText => {
self.sp_auto_fit = true;
self.norm_autofit = false;
}
MsoAutoSize::TextToFitShape => {
self.sp_auto_fit = false;
self.norm_autofit = true;
}
}
}
}
impl From<RGBColor> for Color {
fn from(c: RGBColor) -> Self {
Color::RGB(c)
}
}
use crate::oxml::color::ColorFormat;
#[derive(Debug)]
pub struct Font<'a> {
rpr: &'a mut RunProperties,
}
impl<'a> Font<'a> {
pub fn new(rpr: &'a mut RunProperties) -> Self {
Font { rpr }
}
pub fn rpr(&self) -> &RunProperties {
self.rpr
}
pub fn rpr_mut(&mut self) -> &mut RunProperties {
self.rpr
}
pub fn color(&mut self) -> ColorFormat<'_> {
ColorFormat::new(&mut self.rpr.color)
}
pub fn size(&self) -> Option<Pt> {
self.rpr.size
}
pub fn set_size(&mut self, v: Pt) {
self.rpr.size = Some(v);
}
pub fn clear_size(&mut self) {
self.rpr.size = None;
}
pub fn bold(&self) -> bool {
self.rpr.bold
}
pub fn set_bold(&mut self, v: bool) {
self.rpr.bold = v;
}
pub fn italic(&self) -> bool {
self.rpr.italic
}
pub fn set_italic(&mut self, v: bool) {
self.rpr.italic = v;
}
pub fn strike(&self) -> bool {
self.rpr.strike
}
pub fn set_strike(&mut self, v: bool) {
self.rpr.strike = v;
}
pub fn double_strike(&self) -> bool {
self.rpr.strike_dbl
}
pub fn set_double_strike(&mut self, v: bool) {
self.rpr.strike_dbl = v;
}
pub fn highlight(&self) -> Option<&Color> {
self.rpr.highlight.as_ref()
}
pub fn set_highlight(&mut self, color: Option<Color>) {
match color {
None => self.rpr.highlight = None,
Some(Color::None) => self.rpr.highlight = None,
Some(c) => self.rpr.highlight = Some(c),
}
}
pub fn underline(&self) -> Option<Underline> {
self.rpr.underline
}
pub fn set_underline(&mut self, v: Option<Underline>) {
self.rpr.underline = v;
}
pub fn name(&self) -> Option<&str> {
self.rpr.latin_font.as_deref()
}
pub fn set_name(&mut self, n: impl Into<String>) {
self.rpr.latin_font = Some(n.into());
}
pub fn clear_name(&mut self) {
self.rpr.latin_font = None;
}
pub fn eastasia_name(&self) -> Option<&str> {
self.rpr.eastasia_font.as_deref()
}
pub fn set_eastasia_name(&mut self, n: impl Into<String>) {
self.rpr.eastasia_font = Some(n.into());
}
pub fn clear_eastasia_name(&mut self) {
self.rpr.eastasia_font = None;
}
pub fn complex_script_name(&self) -> Option<&str> {
self.rpr.cs_font.as_deref()
}
pub fn set_complex_script_name(&mut self, n: impl Into<String>) {
self.rpr.cs_font = Some(n.into());
}
pub fn clear_complex_script_name(&mut self) {
self.rpr.cs_font = None;
}
pub fn baseline(&self) -> Option<i32> {
self.rpr.baseline
}
pub fn set_baseline(&mut self, v: i32) {
self.rpr.baseline = Some(v);
}
pub fn spacing(&self) -> Option<i32> {
self.rpr.spc
}
pub fn set_spacing(&mut self, v: i32) {
self.rpr.spc = Some(v);
}
pub fn hlink_click(&self) -> Option<&Hyperlink> {
self.rpr.hlink_click.as_ref()
}
pub fn set_hlink_click(&mut self, hl: Hyperlink) {
self.rpr.hlink_click = Some(hl);
}
pub fn clear_hlink_click(&mut self) {
self.rpr.hlink_click = None;
}
pub fn hlink_hover(&self) -> Option<&Hyperlink> {
self.rpr.hlink_hover.as_ref()
}
pub fn set_hlink_hover(&mut self, hl: Hyperlink) {
self.rpr.hlink_hover = Some(hl);
}
pub fn clear_hlink_hover(&mut self) {
self.rpr.hlink_hover = None;
}
pub fn set_hyperlink(&mut self, rid: impl Into<String>, tooltip: Option<&str>) {
let mut hl = Hyperlink::new(rid);
if let Some(t) = tooltip {
hl.tooltip = Some(t.to_string());
}
self.rpr.hlink_click = Some(hl);
}
pub fn set_slide_jump(&mut self) {
self.rpr.hlink_click = Some(Hyperlink::new_slide_jump());
}
}
impl<'a> From<&'a mut RunProperties> for Font<'a> {
fn from(r: &'a mut RunProperties) -> Self {
Font::new(r)
}
}
#[derive(Debug)]
pub struct ParagraphFormat<'a> {
ppr: &'a mut ParagraphProperties,
}
impl<'a> ParagraphFormat<'a> {
pub fn new(ppr: &'a mut ParagraphProperties) -> Self {
ParagraphFormat { ppr }
}
pub fn ppr(&self) -> &ParagraphProperties {
self.ppr
}
pub fn ppr_mut(&mut self) -> &mut ParagraphProperties {
self.ppr
}
pub fn alignment(&self) -> Option<Alignment> {
self.ppr.alignment
}
pub fn set_alignment(&mut self, v: Alignment) {
self.ppr.alignment = Some(v);
}
pub fn clear_alignment(&mut self) {
self.ppr.alignment = None;
}
pub fn level(&self) -> u8 {
self.ppr.level
}
pub fn set_level(&mut self, lvl: u8) {
self.ppr.level = lvl;
}
pub fn line_spacing(&self) -> Option<Pt> {
self.ppr.line_spacing.map(|emu| Pt(emu as f64 / 12_700.0))
}
pub fn set_line_spacing(&mut self, v: Pt) {
let emu = (v.value() * 12_700.0) as i32;
self.ppr.line_spacing = Some(emu);
self.ppr.line_spacing_pct = None;
}
pub fn line_spacing_pct(&self) -> Option<f32> {
self.ppr.line_spacing_pct.map(|v| v as f32 / 1000.0)
}
pub fn set_line_spacing_pct(&mut self, v: f32) {
self.ppr.line_spacing_pct = Some((v * 1000.0) as i32);
self.ppr.line_spacing = None;
}
pub fn clear_line_spacing(&mut self) {
self.ppr.line_spacing = None;
self.ppr.line_spacing_pct = None;
}
pub fn space_before(&self) -> Option<Emu> {
self.ppr.space_before
}
pub fn set_space_before(&mut self, emu: Emu) {
self.ppr.space_before = Some(emu);
}
pub fn space_after(&self) -> Option<Emu> {
self.ppr.space_after
}
pub fn set_space_after(&mut self, emu: Emu) {
self.ppr.space_after = Some(emu);
}
pub fn set_indent(
&mut self,
left: Option<Emu>,
right: Option<Emu>,
first_line: Option<Emu>,
hanging: Option<i32>,
) {
self.ppr.indent = Indent {
left,
right,
first_line,
hanging,
};
}
pub fn indent(&self) -> Indent {
self.ppr.indent
}
pub fn bullet_style(&self) -> Option<&BulletStyle> {
self.ppr.bullet_style.as_ref()
}
pub fn set_bullet_char(&mut self, ch: impl Into<String>) {
self.ppr.bullet = true;
self.ppr.bullet_style = Some(BulletStyle::Char { char: ch.into() });
}
pub fn set_bullet_numbered(&mut self, auto_num_type: impl Into<String>, start_at: Option<u32>) {
self.ppr.bullet = true;
self.ppr.bullet_style = Some(BulletStyle::AutoNum {
auto_num_type: auto_num_type.into(),
start_at,
});
}
pub fn clear_bullet(&mut self) {
self.ppr.bullet = false;
self.ppr.bullet_style = Some(BulletStyle::None);
}
pub fn has_bullet(&self) -> bool {
self.ppr.bullet
&& self
.ppr
.bullet_style
.as_ref()
.map(|bs| !matches!(bs, BulletStyle::None))
.unwrap_or(false)
}
pub fn tab_stops(&self) -> &[TabStop] {
&self.ppr.tab_stops
}
pub fn add_tab_stop(&mut self, pos: Emu, alignment: TabAlignment) {
self.ppr.tab_stops.push(TabStop { pos, alignment });
}
pub fn clear_tab_stops(&mut self) {
self.ppr.tab_stops.clear();
}
}
impl<'a> From<&'a mut ParagraphProperties> for ParagraphFormat<'a> {
fn from(p: &'a mut ParagraphProperties) -> Self {
ParagraphFormat::new(p)
}
}
#[derive(Debug)]
pub struct TextFrame<'a> {
body: &'a mut TextBody,
}
impl<'a> TextFrame<'a> {
pub fn new(body: &'a mut TextBody) -> Self {
TextFrame { body }
}
pub fn body(&self) -> &TextBody {
self.body
}
pub fn body_mut(&mut self) -> &mut TextBody {
self.body
}
pub fn len(&self) -> usize {
self.body.paragraphs.len()
}
pub fn is_empty(&self) -> bool {
self.body.paragraphs.is_empty()
}
pub fn paragraphs(&self) -> std::slice::Iter<'_, Paragraph> {
self.body.paragraphs.iter()
}
pub fn paragraphs_mut(&mut self) -> std::slice::IterMut<'_, Paragraph> {
self.body.paragraphs.iter_mut()
}
pub fn paragraph(&self, idx: usize) -> Option<&Paragraph> {
self.body.paragraphs.get(idx)
}
pub fn paragraph_mut(&mut self, idx: usize) -> Option<&mut Paragraph> {
self.body.paragraphs.get_mut(idx)
}
pub fn first_paragraph(&self) -> Option<&Paragraph> {
self.body.paragraphs.first()
}
pub fn first_paragraph_mut(&mut self) -> Option<&mut Paragraph> {
self.body.paragraphs.first_mut()
}
pub fn add_paragraph(&mut self) -> &mut Paragraph {
self.body.add_paragraph()
}
pub fn add_paragraph_with_text(&mut self, text: &str) -> &mut Paragraph {
self.body.add_paragraph_with_text(text)
}
pub fn remove_paragraph(&mut self, idx: usize) -> Option<Paragraph> {
self.body.remove_paragraph(idx)
}
pub fn clear(&mut self) {
self.body.clear();
}
pub fn text_getter(&self) -> String {
self.body.text()
}
pub fn set_text(&mut self, text: &str) {
self.body.set_text(text);
}
pub fn auto_size(&self) -> MsoAutoSize {
self.body.auto_size()
}
pub fn set_auto_size(&mut self, v: MsoAutoSize) {
self.body.set_auto_size(v);
}
pub fn vertical_anchor(&self) -> Option<MsoAnchor> {
self.body.vertical_anchor()
}
pub fn set_vertical_anchor(&mut self, v: MsoAnchor) {
self.body.set_vertical_anchor(v);
}
pub fn word_wrap(&self) -> Option<bool> {
self.body.word_wrap()
}
pub fn set_word_wrap(&mut self, v: bool) {
self.body.set_word_wrap(v);
}
pub fn margin_left(&self) -> Option<Emu> {
self.body.margin_left()
}
pub fn margin_right(&self) -> Option<Emu> {
self.body.margin_right()
}
pub fn margin_top(&self) -> Option<Emu> {
self.body.margin_top()
}
pub fn margin_bottom(&self) -> Option<Emu> {
self.body.margin_bottom()
}
pub fn set_margins(&mut self, l: Emu, t: Emu, r: Emu, b: Emu) {
self.body.set_margins(l, t, r, b);
}
pub fn set_margin_left(&mut self, emu: Emu) {
self.body.set_margin_left(emu);
}
pub fn set_margin_right(&mut self, emu: Emu) {
self.body.set_margin_right(emu);
}
pub fn set_margin_top(&mut self, emu: Emu) {
self.body.set_margin_top(emu);
}
pub fn set_margin_bottom(&mut self, emu: Emu) {
self.body.set_margin_bottom(emu);
}
pub fn num_cols(&self) -> Option<u32> {
self.body.num_cols()
}
pub fn set_num_cols(&mut self, count: u32) {
self.body.set_num_cols(count);
}
pub fn col_spacing(&self) -> Option<Emu> {
self.body.col_spacing()
}
pub fn set_col_spacing(&mut self, emu: Emu) {
self.body.set_col_spacing(emu);
}
pub fn set_columns(&mut self, count: u32, spacing: Option<Emu>) {
self.body.set_num_cols(count);
if let Some(s) = spacing {
self.body.set_col_spacing(s);
}
}
}
impl<'a> From<&'a mut TextBody> for TextFrame<'a> {
fn from(b: &'a mut TextBody) -> Self {
TextFrame::new(b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_paragraph_simple() {
let mut p = Paragraph::new();
let mut r = Run::new("Hello");
r.properties.size = Some(Pt(24.0));
r.properties.bold = true;
r.properties.color = RGBColor(0xFF, 0, 0).into();
p.runs.push(r);
let mut w = super::super::writer::XmlWriter::new();
p.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("Hello"));
assert!(s.contains("sz=\"2400\""));
assert!(s.contains("b=\"1\""));
assert!(s.contains("a:srgbClr"));
}
#[test]
fn textframe_view_mirrors_body() {
let mut tb = TextBody::new();
{
let mut tf = TextFrame::new(&mut tb);
let p = tf.add_paragraph();
p.add_run_with_text("first").set_bold(true);
tf.add_paragraph_with_text("second\nthird");
tf.set_word_wrap(false);
tf.set_margins(Emu(91440), Emu(45720), Emu(91440), Emu(45720));
}
assert_eq!(tb.paragraphs.len(), 3);
assert_eq!(tb.paragraphs[0].runs[0].text, "first");
assert!(tb.paragraphs[0].runs[0].properties.bold);
assert_eq!(tb.paragraphs[1].runs[0].text, "second");
assert_eq!(tb.paragraphs[2].runs[0].text, "third");
assert_eq!(tb.word_wrap(), Some(false));
}
#[test]
fn paragraph_format_line_spacing_mutex() {
let mut p = Paragraph::new();
p.set_line_spacing(Pt(20.0));
assert!(p.line_spacing().is_some());
assert!(p.line_spacing_pct().is_none());
p.set_line_spacing_pct(1.5);
assert!(p.line_spacing().is_none());
assert_eq!(p.line_spacing_pct(), Some(1.5));
let mut p2 = Paragraph::new();
{
let mut pf = ParagraphFormat::new(&mut p2.properties);
pf.set_line_spacing(Pt(15.0));
}
assert_eq!(p2.line_spacing(), Some(Pt(15.0)));
{
let mut pf = ParagraphFormat::new(&mut p2.properties);
pf.set_line_spacing_pct(2.0);
}
assert!(p2.line_spacing().is_none());
assert_eq!(p2.line_spacing_pct(), Some(2.0));
}
#[test]
fn font_strikethrough_api() {
let mut r = Run::new("text");
{
let mut f = Font::new(&mut r.properties);
f.set_strike(true);
}
assert!(r.properties.strike);
assert!(!r.properties.strike_dbl);
{
let mut f = Font::new(&mut r.properties);
f.set_double_strike(true);
}
assert!(r.properties.strike_dbl);
let f = Font::new(&mut r.properties);
assert!(f.strike());
assert!(f.double_strike());
}
#[test]
fn font_highlight_api() {
let mut r = Run::new("text");
assert!(r.properties.highlight.is_none());
{
let mut f = Font::new(&mut r.properties);
f.set_highlight(Some(Color::RGB(RGBColor(0xFF, 0xFF, 0x00))));
}
assert!(r.properties.highlight.is_some());
let f = Font::new(&mut r.properties);
let hl = f.highlight().expect("应有高亮色");
assert!(matches!(hl, Color::RGB(c) if c.0 == 0xFF && c.1 == 0xFF && c.2 == 0x00));
{
let mut f = Font::new(&mut r.properties);
f.set_highlight(None);
}
assert!(r.properties.highlight.is_none());
{
let mut f = Font::new(&mut r.properties);
f.set_highlight(Some(Color::RGB(RGBColor(0xFF, 0x00, 0x00))));
f.set_highlight(Some(Color::None));
}
assert!(r.properties.highlight.is_none());
}
#[test]
fn text_body_multi_column_api() {
let mut tb = TextBody::new();
assert!(tb.num_cols().is_none());
assert!(tb.col_spacing().is_none());
tb.set_num_cols(3);
tb.set_col_spacing(Emu(91440));
assert_eq!(tb.num_cols(), Some(3));
assert_eq!(tb.col_spacing(), Some(Emu(91440)));
let mut w = super::super::writer::XmlWriter::new();
tb.body_properties
.as_ref()
.expect("应有 body_properties")
.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("numCol=\"3\""), "应输出 numCol=\"3\",实际: {s}");
assert!(
s.contains("spcCol=\"91440\""),
"应输出 spcCol=\"91440\",实际: {s}"
);
}
#[test]
fn text_frame_set_columns() {
let mut tb = TextBody::new();
{
let mut tf = TextFrame::new(&mut tb);
tf.set_columns(2, Some(Emu(45720)));
}
assert_eq!(tb.num_cols(), Some(2));
assert_eq!(tb.col_spacing(), Some(Emu(45720)));
{
let mut tf = TextFrame::new(&mut tb);
tf.set_columns(4, None);
}
assert_eq!(tb.num_cols(), Some(4));
assert_eq!(tb.col_spacing(), Some(Emu(45720)));
{
let tf = TextFrame::new(&mut tb);
assert_eq!(tf.num_cols(), Some(4));
assert_eq!(tf.col_spacing(), Some(Emu(45720)));
}
}
#[test]
fn paragraph_format_bullet_char() {
let mut ppr = ParagraphProperties::default();
{
let mut pf = ParagraphFormat::new(&mut ppr);
pf.set_bullet_char("•");
}
assert!(ppr.bullet, "bullet 应为 true");
assert!(pf_has_bullet(&ppr), "has_bullet 应为 true");
let mut w = super::super::writer::XmlWriter::new();
ppr.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("buChar"), "应输出 buChar,实际: {s}");
assert!(s.contains("char=\"•\""), "应包含 char=\"•\",实际: {s}");
}
#[test]
fn paragraph_format_bullet_numbered() {
let mut ppr = ParagraphProperties::default();
{
let mut pf = ParagraphFormat::new(&mut ppr);
pf.set_bullet_numbered("arabicPeriod", Some(3));
}
assert!(ppr.bullet);
match &ppr.bullet_style {
Some(BulletStyle::AutoNum {
auto_num_type,
start_at,
}) => {
assert_eq!(auto_num_type, "arabicPeriod");
assert_eq!(*start_at, Some(3));
}
other => panic!("期望 AutoNum,实际: {other:?}"),
}
let mut w = super::super::writer::XmlWriter::new();
ppr.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("buAutoNum"), "应输出 buAutoNum,实际: {s}");
assert!(
s.contains("type=\"arabicPeriod\""),
"应包含 type,实际: {s}"
);
assert!(s.contains("startAt=\"3\""), "应包含 startAt,实际: {s}");
}
#[test]
fn paragraph_format_clear_bullet() {
let mut ppr = ParagraphProperties::default();
{
let mut pf = ParagraphFormat::new(&mut ppr);
pf.set_bullet_char("•");
}
assert!(pf_has_bullet(&ppr));
{
let mut pf = ParagraphFormat::new(&mut ppr);
pf.clear_bullet();
}
assert!(!ppr.bullet, "bullet 应为 false");
assert!(!pf_has_bullet(&ppr), "has_bullet 应为 false");
let mut w = super::super::writer::XmlWriter::new();
ppr.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("buNone"), "应输出 buNone,实际: {s}");
}
fn pf_has_bullet(ppr: &ParagraphProperties) -> bool {
ppr.bullet
&& ppr
.bullet_style
.as_ref()
.map(|bs| !matches!(bs, BulletStyle::None))
.unwrap_or(false)
}
#[test]
fn paragraph_format_tab_stops() {
let mut ppr = ParagraphProperties::default();
{
let mut pf = ParagraphFormat::new(&mut ppr);
pf.add_tab_stop(Emu(914400), TabAlignment::Left);
pf.add_tab_stop(Emu(1828800), TabAlignment::Right);
pf.add_tab_stop(Emu(2743200), TabAlignment::Center);
}
assert_eq!(ppr.tab_stops.len(), 3);
assert_eq!(ppr.tab_stops[0].pos.value(), 914400);
assert_eq!(ppr.tab_stops[0].alignment, TabAlignment::Left);
assert_eq!(ppr.tab_stops[1].pos.value(), 1828800);
assert_eq!(ppr.tab_stops[1].alignment, TabAlignment::Right);
assert_eq!(ppr.tab_stops[2].pos.value(), 2743200);
assert_eq!(ppr.tab_stops[2].alignment, TabAlignment::Center);
let mut w = super::super::writer::XmlWriter::new();
ppr.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("tabLst"), "应输出 tabLst,实际: {s}");
assert!(s.contains("pos=\"914400\""), "应包含 pos=914400,实际: {s}");
assert!(s.contains("algn=\"l\""), "应包含 algn=l,实际: {s}");
assert!(s.contains("algn=\"r\""), "应包含 algn=r,实际: {s}");
assert!(s.contains("algn=\"ctr\""), "应包含 algn=ctr,实际: {s}");
{
let mut pf = ParagraphFormat::new(&mut ppr);
pf.clear_tab_stops();
}
assert!(ppr.tab_stops.is_empty());
}
#[test]
fn paragraph_field_serialization() {
let mut p = Paragraph::new();
p.add_field(FieldType::SlideNumber, "1");
p.add_field(FieldType::DateTime, "1/1/2024");
assert_eq!(p.fields.len(), 2);
assert_eq!(p.fields[0].field_type, FieldType::SlideNumber);
assert_eq!(p.fields[0].text, "1");
assert_eq!(p.fields[1].field_type, FieldType::DateTime);
assert_eq!(p.fields[1].text, "1/1/2024");
let mut w = super::super::writer::XmlWriter::new();
p.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("a:fld"), "应输出 a:fld,实际: {s}");
assert!(
s.contains("type=\"slidenum\""),
"应包含 type=slidenum,实际: {s}"
);
assert!(
s.contains("type=\"datetime\""),
"应包含 type=datetime,实际: {s}"
);
assert!(s.contains(">1<"), "应包含文本 1,实际: {s}");
assert!(s.contains(">1/1/2024<"), "应包含文本 1/1/2024,实际: {s}");
}
#[test]
fn field_type_conversion() {
assert_eq!(FieldType::SlideNumber.as_str(), "slidenum");
assert_eq!(FieldType::DateTime.as_str(), "datetime");
assert_eq!(FieldType::DateTime1.as_str(), "datetime1");
assert_eq!(FieldType::Footer.as_str(), "footer");
assert_eq!(FieldType::Custom("custom1".to_string()).as_str(), "custom1");
assert_eq!(
FieldType::from_str_value("slidenum"),
FieldType::SlideNumber
);
assert_eq!(FieldType::from_str_value("datetime"), FieldType::DateTime);
assert_eq!(FieldType::from_str_value("datetime1"), FieldType::DateTime1);
assert_eq!(FieldType::from_str_value("footer"), FieldType::Footer);
assert_eq!(
FieldType::from_str_value("unknown"),
FieldType::Custom("unknown".to_string())
);
}
#[test]
fn font_hyperlink_api() {
let mut run = Run::new("链接文本");
run.font().set_hyperlink("rId3", Some("点击访问"));
let hl = run
.properties
.hlink_click
.as_ref()
.expect("hlink_click 应存在");
assert_eq!(hl.rid.as_deref(), Some("rId3"));
assert_eq!(hl.tooltip.as_deref(), Some("点击访问"));
run.font().set_slide_jump();
let hl = run
.properties
.hlink_click
.as_ref()
.expect("hlink_click 应存在");
assert_eq!(hl.action.as_deref(), Some("ppaction://hlinksldjump"));
run.font().clear_hlink_click();
assert!(run.properties.hlink_click.is_none());
}
#[test]
fn hyperlink_serialization_roundtrip() {
let mut run = Run::new("点击这里");
run.font().set_hyperlink("rId7", Some("提示文字"));
let mut w = super::super::writer::XmlWriter::new();
run.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("a:hlinkClick"), "应输出 a:hlinkClick,实际: {s}");
assert!(s.contains("r:id=\"rId7\""), "应包含 r:id=rId7,实际: {s}");
assert!(
s.contains("tooltip=\"提示文字\""),
"应包含 tooltip,实际: {s}"
);
}
#[test]
fn end_para_rpr_set_and_serialize() {
let mut p = Paragraph::new();
let rpr = RunProperties {
size: Some(Pt(24.0)),
latin_font: Some("Calibri".to_string()),
lang: Some("en-US".to_string()),
..Default::default()
};
p.set_end_para_rpr(rpr);
assert!(p.end_para_rpr().is_some());
let got = p.end_para_rpr().unwrap();
assert_eq!(got.size, Some(Pt(24.0)));
assert_eq!(got.latin_font.as_deref(), Some("Calibri"));
assert_eq!(got.lang.as_deref(), Some("en-US"));
let mut w = super::super::writer::XmlWriter::new();
p.write_xml(&mut w);
let s = w.into_string();
assert!(s.contains("a:endParaRPr"), "应输出 a:endParaRPr,实际: {s}");
assert!(s.contains("sz=\"2400\""), "应包含 sz=2400,实际: {s}");
assert!(s.contains("lang=\"en-US\""), "应包含 lang=en-US,实际: {s}");
assert!(s.contains("a:latin"), "应包含 a:latin,实际: {s}");
assert!(
s.contains("typeface=\"Calibri\""),
"应包含 typeface=Calibri,实际: {s}"
);
}
#[test]
fn end_para_rpr_clear() {
let mut p = Paragraph::new();
let rpr = RunProperties {
size: Some(Pt(18.0)),
..Default::default()
};
p.set_end_para_rpr(rpr);
assert!(p.end_para_rpr().is_some());
p.clear_end_para_rpr();
assert!(p.end_para_rpr().is_none());
let mut w = super::super::writer::XmlWriter::new();
p.write_xml(&mut w);
let s = w.into_string();
assert!(
!s.contains("a:endParaRPr"),
"不应输出 a:endParaRPr,实际: {s}"
);
}
#[test]
fn end_para_rpr_mut_modify() {
let mut p = Paragraph::new();
let rpr = RunProperties::default();
p.set_end_para_rpr(rpr);
if let Some(rpr) = p.end_para_rpr_mut() {
rpr.size = Some(Pt(32.0));
rpr.bold = true;
}
let got = p.end_para_rpr().expect("end_para_rpr 应存在");
assert_eq!(got.size, Some(Pt(32.0)));
assert!(got.bold);
}
#[test]
fn run_eastasia_name_setter_and_getter() {
let mut r = Run::new("你好");
assert!(r.eastasia_name().is_none());
r.set_eastasia_name("宋体");
assert_eq!(r.eastasia_name(), Some("宋体"));
assert_eq!(r.properties.eastasia_font.as_deref(), Some("宋体"));
}
#[test]
fn run_complex_script_name_setter_and_getter() {
let mut r = Run::new("مرحبا");
assert!(r.complex_script_name().is_none());
r.set_complex_script_name("Traditional Arabic");
assert_eq!(r.complex_script_name(), Some("Traditional Arabic"));
assert_eq!(r.properties.cs_font.as_deref(), Some("Traditional Arabic"));
}
#[test]
fn run_three_fonts_independent() {
let mut r = Run::new("Hello 你好 مرحبا");
r.set_font_name("Arial");
r.set_eastasia_name("Microsoft YaHei");
r.set_complex_script_name("Tahoma");
assert_eq!(r.font_name(), Some("Arial"));
assert_eq!(r.eastasia_name(), Some("Microsoft YaHei"));
assert_eq!(r.complex_script_name(), Some("Tahoma"));
assert_eq!(r.properties.latin_font.as_deref(), Some("Arial"));
assert_eq!(
r.properties.eastasia_font.as_deref(),
Some("Microsoft YaHei")
);
assert_eq!(r.properties.cs_font.as_deref(), Some("Tahoma"));
}
#[test]
fn font_clear_eastasia_name() {
let mut r = Run::new("文本");
r.set_eastasia_name("宋体");
assert_eq!(r.eastasia_name(), Some("宋体"));
{
let mut f = Font::new(&mut r.properties);
f.clear_eastasia_name();
}
assert!(r.eastasia_name().is_none());
assert!(r.properties.eastasia_font.is_none());
}
#[test]
fn font_clear_complex_script_name() {
let mut r = Run::new("text");
r.set_complex_script_name("Arial");
assert_eq!(r.complex_script_name(), Some("Arial"));
{
let mut f = Font::new(&mut r.properties);
f.clear_complex_script_name();
}
assert!(r.complex_script_name().is_none());
assert!(r.properties.cs_font.is_none());
}
#[test]
fn font_view_eastasia_cs_consistent_with_run() {
let mut r = Run::new("混合文本");
r.set_eastasia_name("黑体");
r.set_complex_script_name("Tahoma");
let run_ea = r.eastasia_name().map(|s| s.to_string());
let run_cs = r.complex_script_name().map(|s| s.to_string());
let f = Font::new(&mut r.properties);
assert_eq!(f.eastasia_name(), run_ea.as_deref());
assert_eq!(f.complex_script_name(), run_cs.as_deref());
}
#[test]
fn eastasia_font_serialization_order() {
let mut r = Run::new("你好");
r.set_font_name("Arial");
r.set_eastasia_name("宋体");
r.set_complex_script_name("Tahoma");
let mut p = Paragraph::new();
p.runs.push(r);
let mut w = crate::oxml::writer::XmlWriter::new();
p.write_xml(&mut w);
let xml = w.into_string();
assert!(
xml.contains(r#"<a:latin typeface="Arial"/>"#),
"xml: {}",
xml
);
assert!(xml.contains(r#"<a:ea typeface="宋体"/>"#), "xml: {}", xml);
assert!(xml.contains(r#"<a:cs typeface="Tahoma"/>"#), "xml: {}", xml);
let pos_latin = xml.find("<a:latin").expect("latin should exist");
let pos_ea = xml.find("<a:ea").expect("ea should exist");
let pos_cs = xml.find("<a:cs").expect("cs should exist");
assert!(pos_latin < pos_ea, "latin must come before ea: {}", xml);
assert!(pos_ea < pos_cs, "ea must come before cs: {}", xml);
}
}