1use ooxml_drawingml::{ShapeFill, ShapeOutline};
2use pptx_parse::{GraphicFrameData, Placeholder};
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub enum EditOrigin {
9 #[default]
10 Local,
11 Agent,
12 Remote,
13 System,
14}
15
16#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct EditCtx {
19 pub origin: EditOrigin,
20 pub author: String,
21}
22
23impl EditCtx {
24 pub fn local(author: impl Into<String>) -> Self {
25 Self {
26 origin: EditOrigin::Local,
27 author: author.into(),
28 }
29 }
30}
31
32#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct TextStyle {
35 pub bold: Option<bool>,
36 pub italic: Option<bool>,
37 pub font_size_pt: Option<f64>,
38 pub color: Option<String>,
39 pub font_family: Option<String>,
40 pub underline: Option<String>,
41}
42
43#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct TextStylePatch {
46 pub bold: Option<bool>,
47 pub italic: Option<bool>,
48 pub font_size_pt: Option<f64>,
49 pub color: Option<String>,
50 pub font_family: Option<String>,
51 pub underline: Option<String>,
52}
53
54#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct TextRunSnapshot {
57 pub text: String,
58 pub style: TextStyle,
59}
60
61#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct ParagraphSnapshot {
64 pub id: String,
65 pub alignment: Option<String>,
66 pub level: u32,
67 pub bullet_json: Option<String>,
68 pub runs: Vec<TextRunSnapshot>,
69}
70
71#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct StorySnapshot {
74 pub id: String,
75 pub length: u32,
76 pub paragraphs: Vec<ParagraphSnapshot>,
77}
78
79impl StorySnapshot {
80 pub fn plain_text(&self) -> String {
81 self.paragraphs
82 .iter()
83 .map(|paragraph| {
84 paragraph
85 .runs
86 .iter()
87 .map(|run| run.text.as_str())
88 .collect::<String>()
89 })
90 .collect::<Vec<_>>()
91 .join("\n")
92 }
93}
94
95#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97pub struct ShapeSnapshot {
98 pub id: String,
99 pub source_id: u32,
100 pub kind: ShapeKind,
101 pub name: String,
102 pub x: i64,
103 pub y: i64,
104 pub width: i64,
105 pub height: i64,
106 pub rotation_deg: f64,
107 pub flip_h: bool,
108 pub flip_v: bool,
109 pub geometry: String,
110 pub placeholder: Option<Placeholder>,
111 pub fill: Option<ShapeFill>,
112 pub outline: Option<ShapeOutline>,
113 pub media_part_path: Option<String>,
114 pub graphic: Option<GraphicFrameData>,
115 pub text_stories: Vec<StorySnapshot>,
116 pub children: Vec<ShapeSnapshot>,
117}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub enum ShapeKind {
122 Shape,
123 Picture,
124 GraphicFrame,
125 Group,
126}
127
128#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct SlideSnapshot {
131 pub id: String,
132 pub source_part_path: Option<String>,
133 pub layout_part_path: Option<String>,
134 pub name: Option<String>,
135 pub shapes: Vec<ShapeSnapshot>,
136}
137
138#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct DeckSnapshot {
141 pub width_emu: i64,
142 pub height_emu: i64,
143 pub slides: Vec<SlideSnapshot>,
144}
145
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct SlideReceipt {
149 pub slide_id: String,
150 pub from_index: Option<u32>,
151 pub to_index: Option<u32>,
152}
153
154#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct ShapeReceipt {
157 pub slide_id: String,
158 pub shape_id: String,
159 pub index: u32,
160}
161
162#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct TransformReceipt {
165 pub slide_id: String,
166 pub shape_id: String,
167 pub before: ShapeRect,
168 pub after: ShapeRect,
169}
170
171#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
172pub struct ShapeRect {
173 pub x: i64,
174 pub y: i64,
175 pub width: i64,
176 pub height: i64,
177}
178
179#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct TextReceipt {
182 pub story_id: String,
183 pub start: u32,
184 pub end: u32,
185 pub text: String,
186}
187
188#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct ShapeDraft {
191 pub name: String,
192 pub rect: ShapeRect,
193 pub text: String,
194 pub style: TextStyle,
195}
196
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum UpdateOrigin {
199 Local,
200 Remote,
201}
202
203#[derive(Clone, Debug, Eq, PartialEq)]
204pub struct UpdateEvent {
205 pub update: Vec<u8>,
206 pub origin: UpdateOrigin,
207}
208
209#[derive(Debug, Error)]
210pub enum EditError {
211 #[error("invalid client ID {0}")]
212 InvalidClientId(u64),
213 #[error("could not parse PPTX: {0}")]
214 Parse(String),
215 #[error("invalid deck state: {0}")]
216 InvalidState(String),
217 #[error("invalid yrs update: {0}")]
218 InvalidUpdate(String),
219 #[error("invalid yrs state vector: {0}")]
220 InvalidStateVector(String),
221 #[error("slide {0:?} was not found")]
222 SlideNotFound(String),
223 #[error("shape {0:?} was not found")]
224 ShapeNotFound(String),
225 #[error("story {0:?} was not found")]
226 StoryNotFound(String),
227 #[error("index {index} is outside length {length}")]
228 OutOfBounds { index: u32, length: u32 },
229 #[error("text range {start}..{end} crosses a paragraph boundary")]
230 ParagraphBoundary { start: u32, end: u32 },
231 #[error("invalid shape geometry: {0}")]
232 InvalidGeometry(String),
233 #[error("update observer failed: {0}")]
234 Observer(String),
235 #[error("JSON boundary error: {0}")]
236 Json(String),
237}
238
239pub type EditResult<T> = Result<T, EditError>;