1use serde::{Deserialize, Serialize};
4
5use crate::style::{ColorRef, Fill, Line};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase")]
9pub enum Anchor {
10 Top,
11 Middle,
12 Bottom,
13 Justified,
14 Distributed,
15}
16
17impl Anchor {
18 pub fn xml(self) -> &'static str {
19 match self {
20 Anchor::Top => "t",
21 Anchor::Middle => "ctr",
22 Anchor::Bottom => "b",
23 Anchor::Justified => "just",
24 Anchor::Distributed => "dist",
25 }
26 }
27 pub fn from_xml(s: &str) -> Option<Self> {
28 Some(match s {
29 "t" => Anchor::Top,
30 "ctr" => Anchor::Middle,
31 "b" => Anchor::Bottom,
32 "just" => Anchor::Justified,
33 "dist" => Anchor::Distributed,
34 _ => return None,
35 })
36 }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase", tag = "kind")]
41pub enum AutoFit {
42 None,
44 Shrink {
46 font_scale: f64,
47 line_reduction: f64,
48 },
49 Shape,
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub enum TextDir {
56 Horizontal,
57 Vertical,
58 Vertical270,
59 Stacked,
60 EaVertical,
61}
62
63impl TextDir {
64 pub fn xml(self) -> &'static str {
65 match self {
66 TextDir::Horizontal => "horz",
67 TextDir::Vertical => "vert",
68 TextDir::Vertical270 => "vert270",
69 TextDir::Stacked => "wordArtVert",
70 TextDir::EaVertical => "eaVert",
71 }
72 }
73 pub fn from_xml(s: &str) -> Option<Self> {
74 Some(match s {
75 "horz" => TextDir::Horizontal,
76 "vert" | "mongolianVert" => TextDir::Vertical,
77 "vert270" => TextDir::Vertical270,
78 "wordArtVert" | "wordArtVertRtl" => TextDir::Stacked,
79 "eaVert" => TextDir::EaVertical,
80 _ => return None,
81 })
82 }
83}
84
85#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
87#[serde(default, rename_all = "camelCase")]
88pub struct BodyProps {
89 pub inset_l: Option<f64>,
90 pub inset_t: Option<f64>,
91 pub inset_r: Option<f64>,
92 pub inset_b: Option<f64>,
93 pub anchor: Option<Anchor>,
94 pub anchor_ctr: Option<bool>,
95 pub wrap: Option<bool>,
96 pub autofit: Option<AutoFit>,
97 pub vert: Option<TextDir>,
98 pub rot: Option<f64>,
100 pub columns: Option<u32>,
101 pub col_spacing: Option<f64>,
102 pub warp: Option<String>,
104 #[serde(skip_serializing_if = "Vec::is_empty")]
107 pub warp_adj: Vec<f64>,
108 pub rtl_col: Option<bool>,
109 pub upright: Option<bool>,
111}
112
113impl BodyProps {
114 pub fn inherit(&mut self, p: &BodyProps) {
115 self.inset_l = self.inset_l.or(p.inset_l);
116 self.inset_t = self.inset_t.or(p.inset_t);
117 self.inset_r = self.inset_r.or(p.inset_r);
118 self.inset_b = self.inset_b.or(p.inset_b);
119 self.anchor = self.anchor.or(p.anchor);
120 self.anchor_ctr = self.anchor_ctr.or(p.anchor_ctr);
121 self.wrap = self.wrap.or(p.wrap);
122 self.autofit = self.autofit.or(p.autofit);
123 self.vert = self.vert.or(p.vert);
124 self.rot = self.rot.or(p.rot);
125 self.columns = self.columns.or(p.columns);
126 self.col_spacing = self.col_spacing.or(p.col_spacing);
127 if self.warp.is_none() {
128 self.warp.clone_from(&p.warp);
129 self.warp_adj.clone_from(&p.warp_adj);
130 }
131 self.rtl_col = self.rtl_col.or(p.rtl_col);
132 self.upright = self.upright.or(p.upright);
133 }
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub enum Align {
139 Left,
140 Center,
141 Right,
142 Justify,
143 JustLow,
145 Distributed,
146}
147
148impl Align {
149 pub fn xml(self) -> &'static str {
150 match self {
151 Align::Left => "l",
152 Align::Center => "ctr",
153 Align::Right => "r",
154 Align::Justify => "just",
155 Align::JustLow => "justLow",
156 Align::Distributed => "dist",
157 }
158 }
159 pub fn from_xml(s: &str) -> Option<Self> {
160 Some(match s {
161 "l" => Align::Left,
162 "ctr" => Align::Center,
163 "r" => Align::Right,
164 "just" | "thaiDist" => Align::Justify,
165 "justLow" => Align::JustLow,
166 "dist" => Align::Distributed,
167 _ => return None,
168 })
169 }
170}
171
172#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase", tag = "kind", content = "v")]
175pub enum Spacing {
176 Pct(f64),
177 Pts(f64),
178}
179
180#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase", tag = "kind")]
182pub enum Bullet {
183 None,
184 Char {
185 char: String,
186 },
187 AutoNum {
190 scheme: String,
191 #[serde(default = "start_one")]
192 start_at: u32,
193 },
194 Picture {
195 media: crate::MediaId,
196 },
197}
198
199fn start_one() -> u32 {
200 1
201}
202
203#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
204#[serde(rename_all = "camelCase")]
205pub struct TabStop {
206 pub pos: f64,
207 pub align: String,
209}
210
211#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
212#[serde(default, rename_all = "camelCase")]
213pub struct ParaProps {
214 pub align: Option<Align>,
215 pub margin_left: Option<f64>,
217 pub margin_right: Option<f64>,
218 pub indent: Option<f64>,
220 pub line_spacing: Option<Spacing>,
221 pub space_before: Option<Spacing>,
222 pub space_after: Option<Spacing>,
223 pub bullet: Option<Bullet>,
224 pub bullet_font: Option<String>,
225 pub bullet_color: Option<ColorRef>,
227 pub bullet_size: Option<f64>,
229 pub tabs: Option<Vec<TabStop>>,
230 pub default_tab: Option<f64>,
231 pub rtl: Option<bool>,
232 pub font_align: Option<String>,
233 pub east_asian_line_break: Option<bool>,
234 pub latin_line_break: Option<bool>,
236 pub hanging_punct: Option<bool>,
238}
239
240impl ParaProps {
241 pub fn inherit(&mut self, p: &ParaProps) {
242 self.align = self.align.or(p.align);
243 self.margin_left = self.margin_left.or(p.margin_left);
244 self.margin_right = self.margin_right.or(p.margin_right);
245 self.indent = self.indent.or(p.indent);
246 self.line_spacing = self.line_spacing.or(p.line_spacing);
247 self.space_before = self.space_before.or(p.space_before);
248 self.space_after = self.space_after.or(p.space_after);
249 if self.bullet.is_none() {
250 self.bullet.clone_from(&p.bullet);
251 }
252 if self.bullet_font.is_none() {
253 self.bullet_font.clone_from(&p.bullet_font);
254 }
255 if self.bullet_color.is_none() {
256 self.bullet_color.clone_from(&p.bullet_color);
257 }
258 self.bullet_size = self.bullet_size.or(p.bullet_size);
259 if self.tabs.is_none() {
260 self.tabs.clone_from(&p.tabs);
261 }
262 self.default_tab = self.default_tab.or(p.default_tab);
263 self.rtl = self.rtl.or(p.rtl);
264 if self.font_align.is_none() {
265 self.font_align.clone_from(&p.font_align);
266 }
267 self.east_asian_line_break = self.east_asian_line_break.or(p.east_asian_line_break);
268 self.latin_line_break = self.latin_line_break.or(p.latin_line_break);
269 self.hanging_punct = self.hanging_punct.or(p.hanging_punct);
270 }
271}
272
273#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub enum Caps {
276 None,
277 Small,
278 All,
279}
280
281#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(rename_all = "camelCase")]
283pub enum Strike {
284 None,
285 Single,
286 Double,
287}
288
289#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase", tag = "kind")]
292pub enum Action {
293 Url { url: String },
294 Slide { slide: crate::SlideId },
295 NextSlide,
296 PreviousSlide,
297 FirstSlide,
298 LastSlide,
299 EndShow,
300 LastViewed,
301 CustomShow { name: String },
302 Program { path: String },
303 PlayMedia,
304}
305
306#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct Hyperlink {
309 pub action: Action,
310 #[serde(default)]
311 pub tooltip: String,
312 #[serde(default)]
313 pub highlight_click: bool,
314}
315
316#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
317#[serde(default, rename_all = "camelCase")]
318pub struct RunProps {
319 pub font: Option<String>,
321 pub font_ea: Option<String>,
322 pub font_cs: Option<String>,
323 pub font_sym: Option<String>,
324 pub size: Option<f64>,
326 pub bold: Option<bool>,
327 pub italic: Option<bool>,
328 pub underline: Option<String>,
330 pub underline_color: Option<ColorRef>,
331 pub strike: Option<Strike>,
332 pub baseline: Option<f64>,
334 pub caps: Option<Caps>,
335 pub spacing: Option<f64>,
337 pub kern: Option<f64>,
339 pub fill: Option<Fill>,
340 pub outline: Option<Line>,
341 pub highlight: Option<ColorRef>,
342 pub link: Option<Hyperlink>,
343 pub lang: Option<String>,
344 pub shadow: Option<crate::style::Shadow>,
345 pub glow: Option<crate::style::Glow>,
346 pub no_proof: Option<bool>,
348}
349
350impl RunProps {
351 pub fn inherit(&mut self, p: &RunProps) {
352 macro_rules! take {
353 ($($f:ident),*) => { $( if self.$f.is_none() { self.$f.clone_from(&p.$f); } )* };
354 }
355 take!(
356 font,
357 font_ea,
358 font_cs,
359 font_sym,
360 size,
361 bold,
362 italic,
363 underline,
364 underline_color,
365 strike,
366 baseline,
367 caps,
368 spacing,
369 kern,
370 fill,
371 outline,
372 highlight,
373 link,
374 lang,
375 shadow,
376 glow,
377 no_proof
378 );
379 }
380}
381
382#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
383#[serde(rename_all = "camelCase", tag = "kind")]
384pub enum RunKind {
385 Text,
386 Break,
388 Field {
390 field: String,
391 },
392 Math {
394 omml: String,
395 },
396}
397
398#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
399#[serde(rename_all = "camelCase")]
400pub struct Run {
401 #[serde(default)]
402 pub text: String,
403 #[serde(default, skip_serializing_if = "is_default_props")]
404 pub props: RunProps,
405 #[serde(default = "text_kind", skip_serializing_if = "is_text_kind")]
406 pub kind: RunKind,
407}
408
409fn is_default_props(p: &RunProps) -> bool {
410 *p == RunProps::default()
411}
412fn text_kind() -> RunKind {
413 RunKind::Text
414}
415fn is_text_kind(k: &RunKind) -> bool {
416 *k == RunKind::Text
417}
418
419impl Run {
420 pub fn new(text: impl Into<String>) -> Self {
421 Run {
422 text: text.into(),
423 props: RunProps::default(),
424 kind: RunKind::Text,
425 }
426 }
427 pub fn with(text: impl Into<String>, props: RunProps) -> Self {
428 Run {
429 text: text.into(),
430 props,
431 kind: RunKind::Text,
432 }
433 }
434 pub fn content(&self) -> &str {
436 match self.kind {
437 RunKind::Break => "\u{b}",
438 _ => &self.text,
439 }
440 }
441 pub fn char_len(&self) -> usize {
442 self.content().chars().count()
443 }
444}
445
446#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
447#[serde(default, rename_all = "camelCase")]
448pub struct Paragraph {
449 pub level: u8,
451 #[serde(skip_serializing_if = "is_default_para")]
452 pub props: ParaProps,
453 pub runs: Vec<Run>,
454 #[serde(skip_serializing_if = "is_default_props")]
456 pub end_props: RunProps,
457}
458
459fn is_default_para(p: &ParaProps) -> bool {
460 *p == ParaProps::default()
461}
462
463impl Paragraph {
464 pub fn new(text: &str) -> Self {
465 Paragraph {
466 runs: if text.is_empty() {
467 vec![]
468 } else {
469 vec![Run::new(text)]
470 },
471 ..Default::default()
472 }
473 }
474 pub fn text(&self) -> String {
475 self.runs.iter().map(|r| r.content()).collect()
476 }
477 pub fn char_len(&self) -> usize {
478 self.runs.iter().map(Run::char_len).sum()
479 }
480 pub fn is_empty(&self) -> bool {
481 self.char_len() == 0
482 }
483 pub fn normalize(&mut self) {
485 let mut out: Vec<Run> = Vec::with_capacity(self.runs.len());
486 for r in self.runs.drain(..) {
487 if r.kind == RunKind::Text && r.text.is_empty() {
488 continue;
489 }
490 if let Some(last) = out.last_mut()
491 && last.kind == RunKind::Text
492 && r.kind == RunKind::Text
493 && last.props == r.props
494 {
495 last.text.push_str(&r.text);
496 continue;
497 }
498 out.push(r);
499 }
500 self.runs = out;
501 }
502}
503
504#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
506#[serde(default, rename_all = "camelCase")]
507pub struct LevelStyle {
508 pub para: ParaProps,
509 pub run: RunProps,
510}
511
512#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
513#[serde(default, rename_all = "camelCase")]
514pub struct ListStyle {
515 pub levels: Vec<Option<LevelStyle>>,
517}
518
519impl ListStyle {
520 pub fn level(&self, l: u8) -> Option<&LevelStyle> {
521 self.levels.get(l as usize).and_then(|x| x.as_ref())
522 }
523 pub fn set(&mut self, l: u8, s: LevelStyle) {
524 let i = (l as usize).min(8);
525 if self.levels.len() <= i {
526 self.levels.resize(i + 1, None);
527 }
528 if let Some(slot) = self.levels.get_mut(i) {
529 *slot = Some(s);
530 }
531 }
532 pub fn is_empty(&self) -> bool {
533 self.levels.iter().all(Option::is_none)
534 }
535}
536
537#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
538#[serde(default, rename_all = "camelCase")]
539pub struct TextBody {
540 pub body: BodyProps,
541 #[serde(skip_serializing_if = "ListStyle::is_empty")]
542 pub list_style: ListStyle,
543 pub paragraphs: Vec<Paragraph>,
544}
545
546impl TextBody {
547 pub fn from_text(text: &str) -> Self {
548 TextBody {
549 paragraphs: text.split('\n').map(Paragraph::new).collect(),
550 ..Default::default()
551 }
552 }
553 pub fn text(&self) -> String {
555 self.paragraphs
556 .iter()
557 .map(Paragraph::text)
558 .collect::<Vec<_>>()
559 .join("\n")
560 }
561 pub fn is_empty(&self) -> bool {
562 self.paragraphs.iter().all(Paragraph::is_empty)
563 }
564 pub fn char_len(&self) -> usize {
566 let n: usize = self.paragraphs.iter().map(Paragraph::char_len).sum();
567 n + self.paragraphs.len().saturating_sub(1)
568 }
569}