1use std::collections::HashSet;
2use std::sync::Arc;
3
4use ooxml_drawingml::{ShapeFill, Theme};
5use pptx_parse::{PptxPackage, ShapeNode, Slide};
6use serde::de::DeserializeOwned;
7use yrs::{
8 Any, Array, ArrayPrelim, ArrayRef, Doc, Map, MapPrelim, MapRef, Out, ReadTxn, TextRef,
9 Transact, TransactionMut, WriteTxn,
10};
11
12use crate::story::{seed_plain_story, seed_story, snapshot_story, validate_story};
13use crate::{
14 DeckSession, DeckSnapshot, EditCtx, EditError, EditResult, META, SHAPES, SLIDE_ORDER, SLIDES,
15 STORIES, ShapeDraft, ShapeKind, ShapeReceipt, ShapeRect, ShapeSnapshot, SlideReceipt,
16 SlideSnapshot, TransformReceipt,
17};
18
19const SCHEMA_VERSION: f64 = 1.0;
20const MAX_GEOMETRY: i64 = 1_000_000_000_000_000;
21const MAX_SHAPE_DEPTH: usize = 128;
22
23pub(crate) fn seed_doc(doc: &Doc, package: &PptxPackage, fingerprint: &str) -> EditResult<()> {
24 let package_json =
25 serde_json::to_vec(package).map_err(|error| EditError::Json(error.to_string()))?;
26 let mut txn = doc.transact_mut_with("pptx:bootstrap");
27 let meta = txn.get_or_insert_map(META);
28 meta.insert(&mut txn, "schemaVersion", SCHEMA_VERSION);
29 meta.insert(&mut txn, "fingerprint", fingerprint);
30 meta.insert(&mut txn, "widthEmu", package.presentation.width_emu as f64);
31 meta.insert(
32 &mut txn,
33 "heightEmu",
34 package.presentation.height_emu as f64,
35 );
36 meta.insert(
37 &mut txn,
38 "packageJson",
39 Any::Buffer(Arc::from(package_json)),
40 );
41 let order = txn.get_or_insert_array(SLIDE_ORDER);
42 let slides = txn.get_or_insert_map(SLIDES);
43 let shapes = txn.get_or_insert_map(SHAPES);
44 let stories = txn.get_or_insert_map(STORIES);
45
46 for (slide_index, slide) in package.slides.iter().enumerate() {
47 let theme = slide_theme(package, slide);
48 let reference = &package.presentation.slides[slide_index];
49 let slide_id = format!("slide:{slide_index}:{}", reference.id);
50 order.push_back(&mut txn, slide_id.as_str());
51 let slide_map = slides.insert(&mut txn, slide_id.as_str(), MapPrelim::default());
52 slide_map.insert(&mut txn, "id", slide_id.as_str());
53 slide_map.insert(&mut txn, "sourcePartPath", slide.part_path.as_str());
54 if let Some(layout) = &slide.layout_part_path {
55 slide_map.insert(&mut txn, "layoutPartPath", layout.as_str());
56 }
57 if let Some(name) = &slide.name {
58 slide_map.insert(&mut txn, "name", name.as_str());
59 }
60 let shape_order = slide_map.insert(&mut txn, "shapes", ArrayPrelim::default());
61 for (shape_index, shape) in slide.shapes.iter().enumerate() {
62 let shape_id = seed_shape(
63 &shapes,
64 &stories,
65 &mut txn,
66 &slide_id,
67 &shape_index.to_string(),
68 shape,
69 theme,
70 )?;
71 shape_order.push_back(&mut txn, shape_id.as_str());
72 }
73 }
74 Ok(())
75}
76
77fn seed_shape(
78 shapes: &MapRef,
79 stories: &MapRef,
80 txn: &mut TransactionMut<'_>,
81 slide_id: &str,
82 path: &str,
83 shape: &ShapeNode,
84 theme: Option<&Theme>,
85) -> EditResult<String> {
86 let shape_id = format!("{slide_id}:shape:{path}");
87 let shape_map = shapes.insert(txn, shape_id.as_str(), MapPrelim::default());
88 let base = match shape {
89 ShapeNode::Shape(shape) => &shape.base,
90 ShapeNode::Picture(shape) => &shape.base,
91 ShapeNode::GraphicFrame(shape) => &shape.base,
92 ShapeNode::Group(shape) => &shape.base,
93 };
94 shape_map.insert(txn, "id", shape_id.as_str());
95 shape_map.insert(txn, "sourceId", base.id as f64);
96 shape_map.insert(txn, "name", base.name.as_str());
97 shape_map.insert(txn, "x", base.transform.x as f64);
98 shape_map.insert(txn, "y", base.transform.y as f64);
99 shape_map.insert(txn, "width", base.transform.width as f64);
100 shape_map.insert(txn, "height", base.transform.height as f64);
101 shape_map.insert(txn, "rotationDeg", base.transform.rotation_deg);
102 shape_map.insert(txn, "flipH", base.transform.flip_h);
103 shape_map.insert(txn, "flipV", base.transform.flip_v);
104 insert_json(
105 &shape_map,
106 txn,
107 "placeholderJson",
108 base.placeholder.as_ref(),
109 )?;
110
111 let mut text_story_ids = Vec::new();
112 let mut child_ids = Vec::new();
113 match shape {
114 ShapeNode::Shape(shape) => {
115 shape_map.insert(txn, "kind", "shape");
116 shape_map.insert(txn, "geometry", shape.geometry.as_str());
117 insert_json(&shape_map, txn, "fillJson", shape.fill.as_ref())?;
118 insert_json(&shape_map, txn, "outlineJson", shape.outline.as_ref())?;
119 if let Some(body) = &shape.text {
120 let story_id = format!("story:{shape_id}:0");
121 seed_story(stories, txn, &story_id, body, theme)?;
122 text_story_ids.push(story_id);
123 }
124 }
125 ShapeNode::Picture(picture) => {
126 shape_map.insert(txn, "kind", "picture");
127 shape_map.insert(txn, "geometry", "rect");
128 insert_json(&shape_map, txn, "fillJson", picture.fill.as_ref())?;
129 insert_json(&shape_map, txn, "outlineJson", picture.outline.as_ref())?;
130 if let Some(media) = &picture.media_part_path {
131 shape_map.insert(txn, "mediaPartPath", media.as_str());
132 }
133 }
134 ShapeNode::GraphicFrame(frame) => {
135 shape_map.insert(txn, "kind", "graphicFrame");
136 shape_map.insert(txn, "geometry", "rect");
137 insert_json(&shape_map, txn, "graphicJson", Some(&frame.data))?;
138 if let pptx_parse::GraphicFrameData::Table { rows } = &frame.data {
139 for (row_index, row) in rows.iter().enumerate() {
140 for (cell_index, body) in row.iter().enumerate() {
141 let story_id = format!("story:{shape_id}:table:{row_index}:{cell_index}");
142 seed_story(stories, txn, &story_id, body, theme)?;
143 text_story_ids.push(story_id);
144 }
145 }
146 }
147 }
148 ShapeNode::Group(group) => {
149 shape_map.insert(txn, "kind", "group");
150 shape_map.insert(txn, "geometry", "group");
151 for (child_index, child) in group.children.iter().enumerate() {
152 child_ids.push(seed_shape(
153 shapes,
154 stories,
155 txn,
156 slide_id,
157 &format!("{path}.{child_index}"),
158 child,
159 theme,
160 )?);
161 }
162 }
163 }
164 shape_map.insert(txn, "textStories", string_array(&text_story_ids));
165 shape_map.insert(txn, "children", string_array(&child_ids));
166 Ok(shape_id)
167}
168
169fn slide_theme<'a>(package: &'a PptxPackage, slide: &Slide) -> Option<&'a Theme> {
170 let layout = slide.layout_part_path.as_deref().and_then(|path| {
171 package
172 .layouts
173 .iter()
174 .find(|layout| layout.part_path == path)
175 });
176 let master = layout
177 .and_then(|layout| layout.master_part_path.as_deref())
178 .and_then(|path| {
179 package
180 .masters
181 .iter()
182 .find(|master| master.part_path == path)
183 })
184 .or_else(|| {
185 layout.and_then(|layout| {
186 package.masters.iter().find(|master| {
187 master
188 .layout_part_paths
189 .iter()
190 .any(|path| path == &layout.part_path)
191 })
192 })
193 });
194 master
195 .and_then(|master| master.theme_part_path.as_deref())
196 .and_then(|path| package.themes.iter().find(|theme| theme.part_path == path))
197 .map(|part| &part.theme)
198 .or_else(|| package.themes.first().map(|part| &part.theme))
199}
200
201fn insert_json<T: serde::Serialize>(
202 map: &MapRef,
203 txn: &mut TransactionMut<'_>,
204 key: &str,
205 value: Option<&T>,
206) -> EditResult<()> {
207 if let Some(value) = value {
208 let json =
209 serde_json::to_string(value).map_err(|error| EditError::Json(error.to_string()))?;
210 map.insert(txn, key, json);
211 }
212 Ok(())
213}
214
215impl DeckSession {
216 pub fn snapshot(&self) -> EditResult<DeckSnapshot> {
217 snapshot_doc(&self.doc)
218 }
219
220 pub fn insert_slide(
221 &self,
222 context: &EditCtx,
223 index: u32,
224 layout_part_path: Option<&str>,
225 ) -> EditResult<SlideReceipt> {
226 let slide_id = self.next_id("slide");
227 let mut txn = self.transact_for(context);
228 let order = required_order(&txn)?;
229 let length = order.len(&txn);
230 if index > length {
231 return Err(EditError::OutOfBounds { index, length });
232 }
233 let slides = required_map(&txn, SLIDES)?;
234 let slide = slides.insert(&mut txn, slide_id.as_str(), MapPrelim::default());
235 slide.insert(&mut txn, "id", slide_id.as_str());
236 slide.insert(&mut txn, "name", format!("Slide {}", length + 1));
237 if let Some(layout_part_path) = layout_part_path {
238 slide.insert(&mut txn, "layoutPartPath", layout_part_path);
239 }
240 slide.insert(&mut txn, "shapes", ArrayPrelim::default());
241 order.insert(&mut txn, index, slide_id.as_str());
242 Ok(SlideReceipt {
243 slide_id,
244 from_index: None,
245 to_index: Some(index),
246 })
247 }
248
249 pub fn delete_slide(&self, context: &EditCtx, slide_id: &str) -> EditResult<SlideReceipt> {
250 let mut txn = self.transact_for(context);
251 let order = required_order(&txn)?;
252 let index = array_index(&order, &txn, slide_id)
253 .ok_or_else(|| EditError::SlideNotFound(slide_id.to_owned()))?;
254 let slides = required_map(&txn, SLIDES)?;
255 let slide = slide_ref(&txn, slide_id)?;
256 let shape_order = slide_shape_order(&slide, &txn)?;
257 let shape_ids = string_array_ref(&shape_order, &txn);
258 remove_shape_entries(&mut txn, &shape_ids)?;
259 order.remove(&mut txn, index);
260 slides.remove(&mut txn, slide_id);
261 Ok(SlideReceipt {
262 slide_id: slide_id.to_owned(),
263 from_index: Some(index),
264 to_index: None,
265 })
266 }
267
268 pub fn move_slide(
269 &self,
270 context: &EditCtx,
271 slide_id: &str,
272 to_index: u32,
273 ) -> EditResult<SlideReceipt> {
274 let mut txn = self.transact_for(context);
275 let order = required_order(&txn)?;
276 let length = order.len(&txn);
277 if to_index >= length {
278 return Err(EditError::OutOfBounds {
279 index: to_index,
280 length,
281 });
282 }
283 let from_index = array_index(&order, &txn, slide_id)
284 .ok_or_else(|| EditError::SlideNotFound(slide_id.to_owned()))?;
285 if from_index != to_index {
286 order.remove(&mut txn, from_index);
287 order.insert(&mut txn, to_index, slide_id);
288 }
289 Ok(SlideReceipt {
290 slide_id: slide_id.to_owned(),
291 from_index: Some(from_index),
292 to_index: Some(to_index),
293 })
294 }
295
296 pub fn add_text_box(
297 &self,
298 context: &EditCtx,
299 slide_id: &str,
300 draft: &ShapeDraft,
301 ) -> EditResult<ShapeReceipt> {
302 validate_rect(draft.rect)?;
303 let shape_id = self.next_id("shape");
304 let story_id = format!("story:{shape_id}:0");
305 let paragraph_id = self.next_id("para");
306 let mut txn = self.transact_for(context);
307 let slide = slide_ref(&txn, slide_id)?;
308 let order = slide_shape_order(&slide, &txn)?;
309 let index = order.len(&txn);
310 let shapes = required_map(&txn, SHAPES)?;
311 let stories = required_map(&txn, STORIES)?;
312 seed_plain_story(
313 &stories,
314 &mut txn,
315 &story_id,
316 ¶graph_id,
317 &draft.text,
318 &draft.style,
319 );
320 let shape = shapes.insert(&mut txn, shape_id.as_str(), MapPrelim::default());
321 shape.insert(&mut txn, "id", shape_id.as_str());
322 shape.insert(&mut txn, "sourceId", 0_f64);
323 shape.insert(&mut txn, "kind", "shape");
324 shape.insert(&mut txn, "name", draft.name.as_str());
325 shape.insert(&mut txn, "x", draft.rect.x as f64);
326 shape.insert(&mut txn, "y", draft.rect.y as f64);
327 shape.insert(&mut txn, "width", draft.rect.width as f64);
328 shape.insert(&mut txn, "height", draft.rect.height as f64);
329 shape.insert(&mut txn, "rotationDeg", 0_f64);
330 shape.insert(&mut txn, "flipH", false);
331 shape.insert(&mut txn, "flipV", false);
332 shape.insert(&mut txn, "geometry", "rect");
333 insert_json(
334 &shape,
335 &mut txn,
336 "fillJson",
337 Some(&ShapeFill::named("none")),
338 )?;
339 shape.insert(
340 &mut txn,
341 "textStories",
342 string_array(std::slice::from_ref(&story_id)),
343 );
344 shape.insert(&mut txn, "children", string_array(&[]));
345 order.push_back(&mut txn, shape_id.as_str());
346 Ok(ShapeReceipt {
347 slide_id: slide_id.to_owned(),
348 shape_id,
349 index,
350 })
351 }
352
353 pub fn remove_shape(
354 &self,
355 context: &EditCtx,
356 slide_id: &str,
357 shape_id: &str,
358 ) -> EditResult<ShapeReceipt> {
359 let mut txn = self.transact_for(context);
360 let slide = slide_ref(&txn, slide_id)?;
361 let order = slide_shape_order(&slide, &txn)?;
362 let index = array_index(&order, &txn, shape_id)
363 .ok_or_else(|| EditError::ShapeNotFound(shape_id.to_owned()))?;
364 remove_shape_entries(&mut txn, &[shape_id.to_owned()])?;
365 order.remove(&mut txn, index);
366 Ok(ShapeReceipt {
367 slide_id: slide_id.to_owned(),
368 shape_id: shape_id.to_owned(),
369 index,
370 })
371 }
372
373 pub fn move_shape(
374 &self,
375 context: &EditCtx,
376 slide_id: &str,
377 shape_id: &str,
378 x: i64,
379 y: i64,
380 ) -> EditResult<TransformReceipt> {
381 validate_coordinate(x)?;
382 validate_coordinate(y)?;
383 let mut txn = self.transact_for(context);
384 require_shape_membership(&txn, slide_id, shape_id)?;
385 let shape = shape_ref(&txn, shape_id)?;
386 let before = shape_rect(&shape, &txn)?;
387 shape.insert(&mut txn, "x", x as f64);
388 shape.insert(&mut txn, "y", y as f64);
389 Ok(TransformReceipt {
390 slide_id: slide_id.to_owned(),
391 shape_id: shape_id.to_owned(),
392 before,
393 after: ShapeRect { x, y, ..before },
394 })
395 }
396
397 pub fn resize_shape(
398 &self,
399 context: &EditCtx,
400 slide_id: &str,
401 shape_id: &str,
402 width: i64,
403 height: i64,
404 ) -> EditResult<TransformReceipt> {
405 let rect = ShapeRect {
406 width,
407 height,
408 ..ShapeRect::default()
409 };
410 validate_rect(rect)?;
411 let mut txn = self.transact_for(context);
412 require_shape_membership(&txn, slide_id, shape_id)?;
413 let shape = shape_ref(&txn, shape_id)?;
414 let before = shape_rect(&shape, &txn)?;
415 shape.insert(&mut txn, "width", width as f64);
416 shape.insert(&mut txn, "height", height as f64);
417 Ok(TransformReceipt {
418 slide_id: slide_id.to_owned(),
419 shape_id: shape_id.to_owned(),
420 before,
421 after: ShapeRect {
422 width,
423 height,
424 ..before
425 },
426 })
427 }
428}
429
430pub(crate) fn validate_doc(doc: &Doc) -> EditResult<()> {
431 let snapshot = snapshot_doc(doc)?;
432 if snapshot.width_emu <= 0 || snapshot.height_emu <= 0 {
433 return Err(EditError::InvalidState(
434 "slide dimensions must be positive".to_owned(),
435 ));
436 }
437 let txn = doc.transact();
438 let meta = required_map(&txn, META)?;
439 if map_number(&meta, &txn, "schemaVersion") != Some(SCHEMA_VERSION) {
440 return Err(EditError::InvalidState(
441 "unsupported deck schema version".to_owned(),
442 ));
443 }
444 if map_string(&meta, &txn, "fingerprint").is_none() {
445 return Err(EditError::InvalidState("missing fingerprint".to_owned()));
446 }
447 package_from_meta(&meta, &txn)?;
448 let stories = required_map(&txn, STORIES)?;
449 for (story_id, value) in stories.iter(&txn) {
450 let story = value
451 .cast::<TextRef>()
452 .map_err(|_| EditError::InvalidState(format!("story {story_id} is not text")))?;
453 validate_story(&story, &txn, story_id)?;
454 }
455 Ok(())
456}
457
458pub(crate) fn package_from_doc(doc: &Doc) -> EditResult<PptxPackage> {
459 let txn = doc.transact();
460 let meta = required_map(&txn, META)?;
461 package_from_meta(&meta, &txn)
462}
463
464fn package_from_meta<T: ReadTxn>(meta: &MapRef, txn: &T) -> EditResult<PptxPackage> {
465 let Some(Out::Any(Any::Buffer(bytes))) = meta.get(txn, "packageJson") else {
466 return Err(EditError::InvalidState("missing package data".to_owned()));
467 };
468 serde_json::from_slice(&bytes).map_err(|error| EditError::InvalidState(error.to_string()))
469}
470
471fn snapshot_doc(doc: &Doc) -> EditResult<DeckSnapshot> {
472 let txn = doc.transact();
473 let meta = required_map(&txn, META)?;
474 let order = required_order(&txn)?;
475 let slides = required_map(&txn, SLIDES)?;
476 let shapes = required_map(&txn, SHAPES)?;
477 let stories = required_map(&txn, STORIES)?;
478 let mut seen_slides = HashSet::new();
479 let mut slide_snapshots = Vec::new();
480 for slide_id in string_array_ref(&order, &txn) {
481 if !seen_slides.insert(slide_id.clone()) {
482 continue;
483 }
484 let slide = slides
485 .get(&txn, &slide_id)
486 .and_then(|value| value.cast::<MapRef>().ok())
487 .ok_or_else(|| EditError::InvalidState(format!("missing slide {slide_id}")))?;
488 let shape_order = slide_shape_order(&slide, &txn)?;
489 let mut seen_shapes = HashSet::new();
490 let mut shape_snapshots = Vec::new();
491 for shape_id in string_array_ref(&shape_order, &txn) {
492 if seen_shapes.insert(shape_id.clone()) {
493 shape_snapshots.push(snapshot_shape(
494 &shapes,
495 &stories,
496 &txn,
497 &shape_id,
498 &mut HashSet::new(),
499 )?);
500 }
501 }
502 slide_snapshots.push(SlideSnapshot {
503 id: slide_id,
504 source_part_path: map_string(&slide, &txn, "sourcePartPath"),
505 layout_part_path: map_string(&slide, &txn, "layoutPartPath"),
506 name: map_string(&slide, &txn, "name"),
507 shapes: shape_snapshots,
508 });
509 }
510 Ok(DeckSnapshot {
511 width_emu: required_i64(&meta, &txn, "widthEmu")?,
512 height_emu: required_i64(&meta, &txn, "heightEmu")?,
513 slides: slide_snapshots,
514 })
515}
516
517fn snapshot_shape<T: ReadTxn>(
518 shapes: &MapRef,
519 stories: &MapRef,
520 txn: &T,
521 shape_id: &str,
522 visiting: &mut HashSet<String>,
523) -> EditResult<ShapeSnapshot> {
524 if visiting.len() >= MAX_SHAPE_DEPTH {
525 return Err(EditError::InvalidState(format!(
526 "shape nesting exceeds {MAX_SHAPE_DEPTH} levels"
527 )));
528 }
529 if !visiting.insert(shape_id.to_owned()) {
530 return Err(EditError::InvalidState(format!(
531 "shape cycle at {shape_id}"
532 )));
533 }
534 let shape = shapes
535 .get(txn, shape_id)
536 .and_then(|value| value.cast::<MapRef>().ok())
537 .ok_or_else(|| EditError::InvalidState(format!("missing shape {shape_id}")))?;
538 let mut text_snapshots = Vec::new();
539 for story_id in map_string_array(&shape, txn, "textStories")? {
540 let story = stories
541 .get(txn, &story_id)
542 .and_then(|value| value.cast::<TextRef>().ok())
543 .ok_or_else(|| EditError::InvalidState(format!("missing story {story_id}")))?;
544 text_snapshots.push(snapshot_story(&story, txn, &story_id)?);
545 }
546 let mut children = Vec::new();
547 for child_id in map_string_array(&shape, txn, "children")? {
548 children.push(snapshot_shape(shapes, stories, txn, &child_id, visiting)?);
549 }
550 visiting.remove(shape_id);
551 Ok(ShapeSnapshot {
552 id: shape_id.to_owned(),
553 source_id: required_u32(&shape, txn, "sourceId")?,
554 kind: parse_shape_kind(&required_string(&shape, txn, "kind")?)?,
555 name: required_string(&shape, txn, "name")?,
556 x: required_i64(&shape, txn, "x")?,
557 y: required_i64(&shape, txn, "y")?,
558 width: required_i64(&shape, txn, "width")?,
559 height: required_i64(&shape, txn, "height")?,
560 rotation_deg: map_number(&shape, txn, "rotationDeg").unwrap_or_default(),
561 flip_h: map_bool(&shape, txn, "flipH").unwrap_or_default(),
562 flip_v: map_bool(&shape, txn, "flipV").unwrap_or_default(),
563 geometry: required_string(&shape, txn, "geometry")?,
564 placeholder: optional_json(&shape, txn, "placeholderJson")?,
565 fill: optional_json(&shape, txn, "fillJson")?,
566 outline: optional_json(&shape, txn, "outlineJson")?,
567 media_part_path: map_string(&shape, txn, "mediaPartPath"),
568 graphic: optional_json(&shape, txn, "graphicJson")?,
569 text_stories: text_snapshots,
570 children,
571 })
572}
573
574fn parse_shape_kind(value: &str) -> EditResult<ShapeKind> {
575 match value {
576 "shape" => Ok(ShapeKind::Shape),
577 "picture" => Ok(ShapeKind::Picture),
578 "graphicFrame" => Ok(ShapeKind::GraphicFrame),
579 "group" => Ok(ShapeKind::Group),
580 _ => Err(EditError::InvalidState(format!(
581 "unknown shape kind {value}"
582 ))),
583 }
584}
585
586fn required_order<T: ReadTxn>(txn: &T) -> EditResult<ArrayRef> {
587 txn.get_array(SLIDE_ORDER)
588 .ok_or_else(|| EditError::InvalidState("missing slide order".to_owned()))
589}
590
591fn required_map<T: ReadTxn>(txn: &T, name: &str) -> EditResult<MapRef> {
592 txn.get_map(name)
593 .ok_or_else(|| EditError::InvalidState(format!("missing {name}")))
594}
595
596fn slide_ref<T: ReadTxn>(txn: &T, slide_id: &str) -> EditResult<MapRef> {
597 required_map(txn, SLIDES)?
598 .get(txn, slide_id)
599 .and_then(|value| value.cast::<MapRef>().ok())
600 .ok_or_else(|| EditError::SlideNotFound(slide_id.to_owned()))
601}
602
603fn shape_ref<T: ReadTxn>(txn: &T, shape_id: &str) -> EditResult<MapRef> {
604 required_map(txn, SHAPES)?
605 .get(txn, shape_id)
606 .and_then(|value| value.cast::<MapRef>().ok())
607 .ok_or_else(|| EditError::ShapeNotFound(shape_id.to_owned()))
608}
609
610fn slide_shape_order<T: ReadTxn>(slide: &MapRef, txn: &T) -> EditResult<ArrayRef> {
611 slide
612 .get(txn, "shapes")
613 .and_then(|value| value.cast::<ArrayRef>().ok())
614 .ok_or_else(|| EditError::InvalidState("slide has no shape order".to_owned()))
615}
616
617fn require_shape_membership<T: ReadTxn>(txn: &T, slide_id: &str, shape_id: &str) -> EditResult<()> {
618 let slide = slide_ref(txn, slide_id)?;
619 let order = slide_shape_order(&slide, txn)?;
620 if array_index(&order, txn, shape_id).is_some() {
621 Ok(())
622 } else {
623 Err(EditError::ShapeNotFound(shape_id.to_owned()))
624 }
625}
626
627fn remove_shape_entries(txn: &mut TransactionMut<'_>, root_shape_ids: &[String]) -> EditResult<()> {
628 let shapes = required_map(txn, SHAPES)?;
629 let stories = required_map(txn, STORIES)?;
630 let mut entries = ShapeEntries::default();
631 for shape_id in root_shape_ids {
632 collect_shape_entries(&shapes, txn, shape_id, &mut HashSet::new(), &mut entries)?;
633 }
634 for story_id in entries.story_ids {
635 stories.remove(txn, &story_id);
636 }
637 for shape_id in entries.shape_ids {
638 shapes.remove(txn, &shape_id);
639 }
640 Ok(())
641}
642
643#[derive(Default)]
644struct ShapeEntries {
645 shape_ids: Vec<String>,
646 seen_shape_ids: HashSet<String>,
647 story_ids: Vec<String>,
648 seen_story_ids: HashSet<String>,
649}
650
651fn collect_shape_entries<T: ReadTxn>(
652 shapes: &MapRef,
653 txn: &T,
654 shape_id: &str,
655 visiting: &mut HashSet<String>,
656 entries: &mut ShapeEntries,
657) -> EditResult<()> {
658 if visiting.len() >= MAX_SHAPE_DEPTH {
659 return Err(EditError::InvalidState(format!(
660 "shape nesting exceeds {MAX_SHAPE_DEPTH} levels"
661 )));
662 }
663 if !visiting.insert(shape_id.to_owned()) {
664 return Err(EditError::InvalidState(format!(
665 "shape cycle at {shape_id}"
666 )));
667 }
668 if entries.seen_shape_ids.contains(shape_id) {
669 visiting.remove(shape_id);
670 return Ok(());
671 }
672 let shape = shapes
673 .get(txn, shape_id)
674 .and_then(|value| value.cast::<MapRef>().ok())
675 .ok_or_else(|| EditError::ShapeNotFound(shape_id.to_owned()))?;
676 for story_id in map_string_array(&shape, txn, "textStories")? {
677 if entries.seen_story_ids.insert(story_id.clone()) {
678 entries.story_ids.push(story_id);
679 }
680 }
681 for child_id in map_string_array(&shape, txn, "children")? {
682 collect_shape_entries(shapes, txn, &child_id, visiting, entries)?;
683 }
684 visiting.remove(shape_id);
685 entries.seen_shape_ids.insert(shape_id.to_owned());
686 entries.shape_ids.push(shape_id.to_owned());
687 Ok(())
688}
689
690fn array_index<T: ReadTxn>(array: &ArrayRef, txn: &T, value: &str) -> Option<u32> {
691 array
692 .iter(txn)
693 .enumerate()
694 .find(|(_, item)| out_string(item).as_deref() == Some(value))
695 .map(|(index, _)| index as u32)
696}
697
698fn string_array_ref<T: ReadTxn>(array: &ArrayRef, txn: &T) -> Vec<String> {
699 array
700 .iter(txn)
701 .filter_map(|value| out_string(&value))
702 .collect()
703}
704
705fn map_string_array<T: ReadTxn>(map: &MapRef, txn: &T, key: &str) -> EditResult<Vec<String>> {
706 match map.get(txn, key) {
707 Some(Out::Any(Any::Array(values))) => Ok(values
708 .iter()
709 .filter_map(|value| match value {
710 Any::String(value) => Some(value.to_string()),
711 _ => None,
712 })
713 .collect()),
714 None => Ok(Vec::new()),
715 _ => Err(EditError::InvalidState(format!("{key} is not an array"))),
716 }
717}
718
719fn string_array(values: &[String]) -> Any {
720 Any::Array(Arc::from(
721 values
722 .iter()
723 .map(|value| Any::from(value.as_str()))
724 .collect::<Vec<_>>(),
725 ))
726}
727
728fn shape_rect<T: ReadTxn>(shape: &MapRef, txn: &T) -> EditResult<ShapeRect> {
729 Ok(ShapeRect {
730 x: required_i64(shape, txn, "x")?,
731 y: required_i64(shape, txn, "y")?,
732 width: required_i64(shape, txn, "width")?,
733 height: required_i64(shape, txn, "height")?,
734 })
735}
736
737fn required_u32<T: ReadTxn>(map: &MapRef, txn: &T, key: &str) -> EditResult<u32> {
738 let value = required_i64(map, txn, key)?;
739 u32::try_from(value).map_err(|_| {
740 EditError::InvalidState(format!("{key} value {value} is outside the u32 range"))
741 })
742}
743
744fn validate_rect(rect: ShapeRect) -> EditResult<()> {
745 validate_coordinate(rect.x)?;
746 validate_coordinate(rect.y)?;
747 if rect.width <= 0 || rect.height <= 0 {
748 return Err(EditError::InvalidGeometry(
749 "shape width and height must be positive".to_owned(),
750 ));
751 }
752 validate_coordinate(rect.width)?;
753 validate_coordinate(rect.height)
754}
755
756fn validate_coordinate(value: i64) -> EditResult<()> {
757 if value.unsigned_abs() > MAX_GEOMETRY as u64 {
758 return Err(EditError::InvalidGeometry(format!(
759 "coordinate {value} exceeds the safe range"
760 )));
761 }
762 Ok(())
763}
764
765fn required_string<T: ReadTxn>(map: &MapRef, txn: &T, key: &str) -> EditResult<String> {
766 map_string(map, txn, key)
767 .ok_or_else(|| EditError::InvalidState(format!("missing string {key}")))
768}
769
770fn map_string<T: ReadTxn>(map: &MapRef, txn: &T, key: &str) -> Option<String> {
771 map.get(txn, key).and_then(|value| out_string(&value))
772}
773
774fn out_string(value: &Out) -> Option<String> {
775 match value {
776 Out::Any(Any::String(value)) => Some(value.to_string()),
777 _ => None,
778 }
779}
780
781fn required_i64<T: ReadTxn>(map: &MapRef, txn: &T, key: &str) -> EditResult<i64> {
782 let number = map_number(map, txn, key)
783 .ok_or_else(|| EditError::InvalidState(format!("missing number {key}")))?;
784 if !number.is_finite() || number.fract() != 0.0 || number.abs() > MAX_GEOMETRY as f64 {
785 return Err(EditError::InvalidState(format!("invalid integer {key}")));
786 }
787 Ok(number as i64)
788}
789
790fn map_number<T: ReadTxn>(map: &MapRef, txn: &T, key: &str) -> Option<f64> {
791 match map.get(txn, key) {
792 Some(Out::Any(Any::Number(value))) if value.is_finite() => Some(value),
793 Some(Out::Any(Any::BigInt(value))) => Some(value as f64),
794 _ => None,
795 }
796}
797
798fn map_bool<T: ReadTxn>(map: &MapRef, txn: &T, key: &str) -> Option<bool> {
799 match map.get(txn, key) {
800 Some(Out::Any(Any::Bool(value))) => Some(value),
801 _ => None,
802 }
803}
804
805fn optional_json<T: DeserializeOwned, R: ReadTxn>(
806 map: &MapRef,
807 txn: &R,
808 key: &str,
809) -> EditResult<Option<T>> {
810 map_string(map, txn, key)
811 .map(|json| {
812 serde_json::from_str(&json).map_err(|error| EditError::InvalidState(error.to_string()))
813 })
814 .transpose()
815}
816
817#[cfg(test)]
818mod tests {
819 use super::*;
820 use crate::TextStyle;
821
822 const FIXTURE: &[u8] = include_bytes!("../../../apps/demo/public/betteroffice-demo.pptx");
823
824 #[test]
825 fn delete_operations_remove_owned_map_entries() {
826 let session = DeckSession::open(FIXTURE, 101).unwrap();
827 let context = EditCtx::local("test");
828 let baseline = map_lengths(&session);
829
830 for cycle in 0..3 {
831 let index = session.snapshot().unwrap().slides.len() as u32;
832 let slide = session.insert_slide(&context, index, None).unwrap();
833 let first = session
834 .add_text_box(&context, &slide.slide_id, &draft(cycle, 0))
835 .unwrap();
836 session
837 .add_text_box(&context, &slide.slide_id, &draft(cycle, 1))
838 .unwrap();
839 assert_eq!(map_lengths(&session), add_lengths(baseline, (1, 2, 2)));
840
841 session
842 .remove_shape(&context, &slide.slide_id, &first.shape_id)
843 .unwrap();
844 assert_eq!(map_lengths(&session), add_lengths(baseline, (1, 1, 1)));
845
846 session.delete_slide(&context, &slide.slide_id).unwrap();
847 assert_eq!(map_lengths(&session), baseline);
848 }
849 }
850
851 #[test]
852 fn deleting_seeded_slide_cascades_descendant_entries() {
853 let session = DeckSession::open(FIXTURE, 202).unwrap();
854 let slide = session.snapshot().unwrap().slides[0].clone();
855 let before = map_lengths(&session);
856 let owned = slide.shapes.iter().fold((0, 0), |total, shape| {
857 let next = shape_entry_lengths(shape);
858 (total.0 + next.0, total.1 + next.1)
859 });
860
861 session
862 .delete_slide(&EditCtx::local("test"), &slide.id)
863 .unwrap();
864
865 assert!(owned.0 > slide.shapes.len() as u32);
866 assert_eq!(
867 map_lengths(&session),
868 (before.0 - 1, before.1 - owned.0, before.2 - owned.1)
869 );
870 }
871
872 fn draft(cycle: u32, index: u32) -> ShapeDraft {
873 ShapeDraft {
874 name: format!("Shape {cycle}:{index}"),
875 rect: ShapeRect {
876 x: 100_000,
877 y: 100_000,
878 width: 1_000_000,
879 height: 500_000,
880 },
881 text: "Text".to_owned(),
882 style: TextStyle::default(),
883 }
884 }
885
886 fn map_lengths(session: &DeckSession) -> (u32, u32, u32) {
887 let txn = session.doc.transact();
888 (
889 required_map(&txn, SLIDES).unwrap().len(&txn),
890 required_map(&txn, SHAPES).unwrap().len(&txn),
891 required_map(&txn, STORIES).unwrap().len(&txn),
892 )
893 }
894
895 fn shape_entry_lengths(shape: &ShapeSnapshot) -> (u32, u32) {
896 shape.children.iter().fold(
897 (1, shape.text_stories.len() as u32),
898 |(shape_count, story_count), child| {
899 let child_counts = shape_entry_lengths(child);
900 (shape_count + child_counts.0, story_count + child_counts.1)
901 },
902 )
903 }
904
905 fn add_lengths(left: (u32, u32, u32), right: (u32, u32, u32)) -> (u32, u32, u32) {
906 (left.0 + right.0, left.1 + right.1, left.2 + right.2)
907 }
908}