- 1
//! PowerPoint ops. New slides come only from the deck's own layouts and - 2
//! their placeholders, never free-positioned shapes, so output follows the - 3
//! template and stays reliable with small models. - 4
- 5
use std::collections::BTreeMap; - 6
use std::io::{Read, Seek}; - 7
- 8
use super::{EditError, Expect, Outcome, TextValue, Work, fail, free_part_name}; - 9
use crate::package::rels_part_name; - 10
use crate::splice::{Splice, Tree, escape_attr, escape_text}; - 11
- 12
const P: &str = "http://schemas.openxmlformats.org/presentationml/2006/main"; - 13
const P_STRICT: &str = "http://purl.oclc.org/ooxml/presentationml/main"; - 14
const A: &str = "http://schemas.openxmlformats.org/drawingml/2006/main"; - 15
const A_STRICT: &str = "http://purl.oclc.org/ooxml/drawingml/main"; - 16
const R: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; - 17
const R_STRICT: &str = "http://purl.oclc.org/ooxml/officeDocument/relationships"; - 18
const SLIDE_TYPE: &str = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml"; - 19
const LAYOUT_TYPE: &str = - 20
"application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"; - 21
const NOTES_TYPE: &str = - 22
"application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"; - 23
- 24
struct Deck { - 25
part: String, - 26
bytes: Vec<u8>, - 27
tree: Tree, - 28
p: String, - 29
r: String, - 30
list: Option<usize>, - 31
/// (slide id, relationship id, sldId node) in presentation order. - 32
slides: Vec<(String, String, usize)>, - 33
} - 34
- 35
fn deck<R2: Read + Seek>(work: &mut Work<'_, R2>) -> Result<Deck, EditError> { - 36
let part = work.main_part(); - 37
let bytes = work.get(&part)?; - 38
let tree = Tree::parse(&bytes, &part, work.limits())?; - 39
let Some(p) = tree.prefix_for(P).or_else(|| tree.prefix_for(P_STRICT)) else { - 40
return fail("the presentation does not declare the PresentationML namespace"); - 41
}; - 42
if tree.children(0, "modifyVerifier").next().is_some() { - 43
return fail( - 44
"the presentation has a password to modify (O8); the owner must remove it before it can be edited", - 45
); - 46
} - 47
let r = tree - 48
.prefix_for(R) - 49
.or_else(|| tree.prefix_for(R_STRICT)) - 50
.unwrap_or_else(|| "r:".into()); - 51
let list = tree.children(0, "sldIdLst").next(); - 52
let slides = list - 53
.map(|list| { - 54
tree.children(list, "sldId") - 55
.filter_map(|node| { - 56
let element = &tree.nodes[node].element; - 57
Some(( - 58
element.attr_unprefixed("id")?.to_string(), - 59
element.attr_prefixed("id")?.to_string(), - 60
node, - 61
)) - 62
}) - 63
.collect() - 64
}) - 65
.unwrap_or_default(); - 66
Ok(Deck { - 67
part, - 68
bytes, - 69
tree, - 70
p, - 71
r, - 72
list, - 73
slides, - 74
}) - 75
} - 76
- 77
/// `slide:256` or `slide:256/...` → `256`. - 78
fn slide_id(anchor: &str) -> Result<&str, EditError> { - 79
anchor - 80
.strip_prefix("slide:") - 81
.map(|rest| rest.split('/').next().unwrap_or(rest)) - 82
.filter(|id| !id.is_empty()) - 83
.ok_or_else(|| EditError { - 84
op: None, - 85
message: format!( - 86
"{anchor:?} is not a slide anchor; use one like slide:256 from doc_read" - 87
), - 88
}) - 89
} - 90
- 91
fn slide_part<R2: Read + Seek>( - 92
work: &mut Work<'_, R2>, - 93
deck: &Deck, - 94
id: &str, - 95
) -> Result<String, EditError> { - 96
let Some((_, relationship, _)) = deck.slides.iter().find(|(slide, _, _)| slide == id) else { - 97
let known: Vec<String> = deck - 98
.slides - 99
.iter() - 100
.map(|(slide, _, _)| format!("slide:{slide}")) - 101
.collect(); - 102
return fail(format!("no slide:{id}; slides: {}", known.join(", "))); - 103
}; - 104
match work.by_id(&deck.part, relationship)? { - 105
Some(part) => Ok(part), - 106
None => fail(format!("slide:{id} points at no part")), - 107
} - 108
} - 109
- 110
fn prefixes(tree: &Tree) -> Result<(String, String), EditError> { - 111
let p = tree.prefix_for(P).or_else(|| tree.prefix_for(P_STRICT)); - 112
let a = tree.prefix_for(A).or_else(|| tree.prefix_for(A_STRICT)); - 113
match (p, a) { - 114
(Some(p), Some(a)) => Ok((p, a)), - 115
_ => fail( - 116
"the slide does not declare the PresentationML and DrawingML namespaces on its root", - 117
), - 118
} - 119
} - 120
- 121
/// A shape's `(id, name, placeholder type, placeholder idx)`. - 122
fn shape_facts(tree: &Tree, shape: usize) -> (String, String, Option<String>, Option<String>) { - 123
let (id, name) = tree - 124
.descendants(shape, "cNvPr") - 125
.next() - 126
.map(|node| { - 127
let element = &tree.nodes[node].element; - 128
( - 129
element.attr("id").unwrap_or_default().to_string(), - 130
element.attr("name").unwrap_or_default().to_string(), - 131
) - 132
}) - 133
.unwrap_or_default(); - 134
let placeholder = tree - 135
.descendants(shape, "ph") - 136
.next() - 137
.map(|node| &tree.nodes[node].element); - 138
let kind = placeholder.map(|element| element.attr("type").unwrap_or("body").to_string()); - 139
let index = placeholder.and_then(|element| element.attr("idx").map(str::to_string)); - 140
(id, name, kind, index) - 141
} - 142
- 143
/// Whether a placeholder of `kind`/`index` answers to `key` (`title`, - 144
/// `subtitle`, `body`, a raw type, or `idx:N`). - 145
fn placeholder_matches(key: &str, kind: Option<&str>, index: Option<&str>) -> bool { - 146
let key = key.trim().to_ascii_lowercase(); - 147
if let Some(wanted) = key.strip_prefix("idx:") { - 148
return index == Some(wanted); - 149
} - 150
match (key.as_str(), kind) { - 151
("title", Some("title" | "ctrTitle")) => true, - 152
("subtitle", Some("subTitle")) => true, - 153
(wanted, Some(kind)) => kind.eq_ignore_ascii_case(wanted), - 154
_ => false, - 155
} - 156
} - 157
- 158
fn paragraphs( - 159
a: &str, - 160
lines: &[String], - 161
paragraph_properties: &str, - 162
run_properties: &str, - 163
) -> String { - 164
if lines.iter().all(|line| line.is_empty()) { - 165
return format!("<{a}p/>"); - 166
} - 167
lines - 168
.iter() - 169
.map(|line| { - 170
if line.is_empty() { - 171
format!("<{a}p>{paragraph_properties}</{a}p>") - 172
} else { - 173
format!( - 174
"<{a}p>{paragraph_properties}<{a}r>{run_properties}<{a}t>{}</{a}t></{a}r></{a}p>", - 175
escape_text(line) - 176
) - 177
} - 178
}) - 179
.collect() - 180
} - 181
- 182
/// Replaces the text of shape `shape` in `tree`, keeping its body and list - 183
/// properties and the first paragraph's and run's formatting. - 184
fn replace_shape_text( - 185
splice: &mut Splice, - 186
bytes: &[u8], - 187
tree: &Tree, - 188
shape: usize, - 189
lines: &[String], - 190
) -> Result<(), EditError> { - 191
let (p, a) = prefixes(tree)?; - 192
match tree.children(shape, "txBody").next() { - 193
Some(body) => { - 194
let existing: Vec<usize> = tree.children(body, "p").collect(); - 195
let slice = |index: usize| { - 196
String::from_utf8_lossy(&bytes[tree.nodes[index].span.clone()]).into_owned() - 197
}; - 198
let paragraph_properties = existing - 199
.first() - 200
.and_then(|first| tree.children(*first, "pPr").next()) - 201
.map(slice) - 202
.unwrap_or_default(); - 203
let run_properties = existing - 204
.first() - 205
.and_then(|first| tree.descendants(*first, "rPr").next()) - 206
.map(slice) - 207
.unwrap_or_default(); - 208
let new = paragraphs(&a, lines, ¶graph_properties, &run_properties); - 209
match (existing.first(), existing.last()) { - 210
(Some(first), Some(last)) => splice.replace( - 211
tree.nodes[*first].span.start..tree.nodes[*last].span.end, - 212
new, - 213
), - 214
_ => { - 215
let node = &tree.nodes[body]; - 216
if node.is_empty_element() { - 217
return fail("the shape's text body is empty markup this op cannot extend"); - 218
} - 219
splice.insert(node.inner.end, new); - 220
} - 221
} - 222
} - 223
None => { - 224
let after = tree - 225
.children(shape, "style") - 226
.chain(tree.children(shape, "spPr")) - 227
.map(|index| tree.nodes[index].span.end) - 228
.max(); - 229
let Some(after) = after else { - 230
return fail("the shape has no shape properties to place text after"); - 231
}; - 232
splice.insert( - 233
after, - 234
format!( - 235
"<{p}txBody><{a}bodyPr/><{a}lstStyle/>{}</{p}txBody>", - 236
paragraphs(&a, lines, "", "") - 237
), - 238
); - 239
} - 240
} - 241
Ok(()) - 242
} - 243
- 244
fn expect_text(anchor: String, lines: &[String]) -> Expect { - 245
let needles: Vec<String> = lines - 246
.iter() - 247
.map(|line| line.trim().to_string()) - 248
.filter(|line| !line.is_empty()) - 249
.collect(); - 250
if needles.is_empty() { - 251
Expect::Absent { anchor } - 252
} else { - 253
Expect::UnitContains { anchor, needles } - 254
} - 255
} - 256
- 257
pub(super) fn set_placeholder_text<R2: Read + Seek>( - 258
work: &mut Work<'_, R2>, - 259
anchor: &str, - 260
text: &TextValue, - 261
) -> Result<Outcome, EditError> { - 262
let deck = deck(work)?; - 263
let id = slide_id(anchor)?.to_string(); - 264
let target = anchor - 265
.split_once('/') - 266
.map(|(_, rest)| rest.to_string()) - 267
.ok_or_else(|| EditError { - 268
op: None, - 269
message: format!( - 270
"{anchor:?} names no shape; use slide:ID/shape:N or slide:ID/placeholder:title" - 271
), - 272
})?; - 273
let part = slide_part(work, &deck, &id)?; - 274
let bytes = work.get(&part)?; - 275
let tree = Tree::parse(&bytes, &part, work.limits())?; - 276
let mut shapes = Vec::new(); - 277
let mut found = None; - 278
for shape in tree.descendants(0, "sp") { - 279
let (shape_id, name, kind, index) = shape_facts(&tree, shape); - 280
let hit = match target.split_once(':') { - 281
Some(("shape", wanted)) => shape_id == wanted, - 282
Some(("placeholder", wanted)) => { - 283
placeholder_matches(wanted, kind.as_deref(), index.as_deref()) - 284
} - 285
_ => false, - 286
}; - 287
if hit && found.is_none() { - 288
found = Some((shape, shape_id.clone())); - 289
} - 290
shapes.push(match kind { - 291
Some(kind) => format!("shape:{shape_id} ({name}, placeholder {kind})"), - 292
None => format!("shape:{shape_id} ({name})"), - 293
}); - 294
} - 295
let Some((shape, shape_id)) = found else { - 296
return fail(format!( - 297
"slide:{id} has no text shape {target}; shapes: {}", - 298
shapes.join(", ") - 299
)); - 300
}; - 301
let lines = text.lines(); - 302
let mut splice = Splice::default(); - 303
replace_shape_text(&mut splice, &bytes, &tree, shape, &lines)?; - 304
work.put(&part, splice.apply(&bytes, &part)?); - 305
Ok(Outcome { - 306
summary: format!( - 307
"slide:{id}/shape:{shape_id} text set ({} line(s))", - 308
lines.len() - 309
), - 310
expect: vec![expect_text(format!("slide:{id}/shape:{shape_id}"), &lines)], - 311
created: Vec::new(), - 312
}) - 313
} - 314
- 315
/// Creates `slide`'s notes page, holding `lines`, from the deck's notes - 316
/// master, and returns its part. - 317
fn create_notes<R2: Read + Seek>( - 318
work: &mut Work<'_, R2>, - 319
deck_part: &str, - 320
slide: &str, - 321
lines: &[String], - 322
) -> Result<String, EditError> { - 323
let Some(master) = work.related(deck_part, "notesMaster")? else { - 324
return fail( - 325
"this deck has no notes master, so speaker notes cannot be added to a slide that has none; open it in PowerPoint and add notes there once", - 326
); - 327
}; - 328
let part = free_part_name(work, "ppt/notesSlides/notesSlide", ".xml"); - 329
work.put( - 330
&part, - 331
format!( - 332
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?> - 333
<p:notes xmlns:a="{A}" xmlns:r="{R}" xmlns:p="{P}"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr><p:sp><p:nvSpPr><p:cNvPr id="2" name="Slide Image Placeholder 1"/><p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldImg"/></p:nvPr></p:nvSpPr><p:spPr/></p:sp><p:sp><p:nvSpPr><p:cNvPr id="3" name="Notes Placeholder 2"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/>{}</p:txBody></p:sp></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:notes>"#, - 334
paragraphs("a:", lines, "", "") - 335
) - 336
.into_bytes(), - 337
); - 338
work.set_override(&part, NOTES_TYPE)?; - 339
work.add_relationship(&part, &format!("{R}/notesMaster"), &master)?; - 340
work.add_relationship(&part, &format!("{R}/slide"), slide)?; - 341
work.add_relationship(slide, &format!("{R}/notesSlide"), &part)?; - 342
Ok(part) - 343
} - 344
- 345
pub(super) fn set_notes<R2: Read + Seek>( - 346
work: &mut Work<'_, R2>, - 347
anchor: &str, - 348
text: &str, - 349
) -> Result<Outcome, EditError> { - 350
let deck = deck(work)?; - 351
let id = slide_id(anchor)?.to_string(); - 352
let part = slide_part(work, &deck, &id)?; - 353
let Some(notes) = work.related(&part, "notesSlide")? else { - 354
if work.strict() { - 355
return fail("adding a notes page to a Strict presentation is not supported yet"); - 356
} - 357
let lines: Vec<String> = text.split('\n').map(str::to_string).collect(); - 358
create_notes(work, &deck.part, &part, &lines)?; - 359
return Ok(Outcome { - 360
summary: format!("slide:{id} speaker notes added"), - 361
expect: vec![expect_text(format!("slide:{id}/notes"), &lines)], - 362
created: Vec::new(), - 363
}); - 364
}; - 365
let bytes = work.get(¬es)?; - 366
let tree = Tree::parse(&bytes, ¬es, work.limits())?; - 367
let body = tree.descendants(0, "sp").find(|shape| { - 368
let (_, _, kind, _) = shape_facts(&tree, *shape); - 369
kind.as_deref() == Some("body") - 370
}); - 371
let Some(body) = body else { - 372
return fail(format!("slide:{id}'s notes page has no notes placeholder")); - 373
}; - 374
let lines: Vec<String> = text.split('\n').map(str::to_string).collect(); - 375
let mut splice = Splice::default(); - 376
replace_shape_text(&mut splice, &bytes, &tree, body, &lines)?; - 377
work.put(¬es, splice.apply(&bytes, ¬es)?); - 378
Ok(Outcome { - 379
summary: format!("slide:{id} speaker notes set"), - 380
expect: vec![expect_text(format!("slide:{id}/notes"), &lines)], - 381
created: Vec::new(), - 382
}) - 383
} - 384
- 385
pub(super) fn add_slide_from_layout<R2: Read + Seek>( - 386
work: &mut Work<'_, R2>, - 387
layout: &str, - 388
after: Option<&str>, - 389
placeholders: &BTreeMap<String, TextValue>, - 390
notes: Option<&str>, - 391
) -> Result<Outcome, EditError> { - 392
if work.strict() { - 393
return fail("adding a slide to a Strict presentation is not supported yet"); - 394
} - 395
let mut layouts = Vec::new(); - 396
let mut chosen = None; - 397
for part in work.part_names() { - 398
if work.content_type(&part)?.as_deref() != Some(LAYOUT_TYPE) { - 399
continue; - 400
} - 401
let bytes = work.get(&part)?; - 402
let tree = Tree::parse(&bytes, &part, work.limits())?; - 403
let name = tree - 404
.descendants(0, "cSld") - 405
.next() - 406
.and_then(|node| tree.nodes[node].element.attr("name")) - 407
.unwrap_or_default() - 408
.to_string(); - 409
if chosen.is_none() && name.trim().eq_ignore_ascii_case(layout.trim()) { - 410
chosen = Some((part.clone(), tree)); - 411
} - 412
layouts.push(name); - 413
} - 414
let Some((layout_part, layout_tree)) = chosen else { - 415
layouts.sort(); - 416
layouts.dedup(); - 417
return fail(format!( - 418
"no layout {layout:?}; this deck's layouts: {}", - 419
layouts.join(", ") - 420
)); - 421
}; - 422
- 423
let mut shapes = Vec::new(); - 424
let mut keys = Vec::new(); - 425
for shape in layout_tree.descendants(0, "sp") { - 426
let (_, name, kind, index) = shape_facts(&layout_tree, shape); - 427
let Some(kind) = kind else { - 428
continue; - 429
}; - 430
if matches!(kind.as_str(), "dt" | "ftr" | "sldNum" | "hdr") { - 431
continue; - 432
} - 433
keys.push(match kind.as_str() { - 434
"title" | "ctrTitle" => "title".to_string(), - 435
"subTitle" => "subtitle".to_string(), - 436
other => match &index { - 437
Some(index) if other == "body" => format!("body (or idx:{index})"), - 438
_ => other.to_string(), - 439
}, - 440
}); - 441
// The slide's placeholder names the layout's exactly: a content - 442
// placeholder has no type (it means `obj`), a text one says `body`. - 443
let written_type = layout_tree - 444
.descendants(shape, "ph") - 445
.next() - 446
.and_then(|ph| layout_tree.nodes[ph].element.attr("type")) - 447
.map(str::to_string); - 448
shapes.push((name, kind, index, written_type)); - 449
} - 450
for key in placeholders.keys() { - 451
if !shapes - 452
.iter() - 453
.any(|(_, kind, index, _)| placeholder_matches(key, Some(kind), index.as_deref())) - 454
{ - 455
return fail(format!( - 456
"layout {layout:?} has no placeholder {key:?}; it has: {}", - 457
keys.join(", ") - 458
)); - 459
} - 460
} - 461
- 462
let deck = deck(work)?; - 463
let a = "a:"; - 464
let mut used = std::collections::HashSet::new(); - 465
let mut body = String::new(); - 466
for (position, (name, kind, index, written_type)) in shapes.iter().enumerate() { - 467
let text = placeholders - 468
.iter() - 469
.find(|(key, _)| { - 470
placeholder_matches(key, Some(kind), index.as_deref()) - 471
&& used.insert((*key).clone()) - 472
}) - 473
.map(|(_, text)| text.lines()) - 474
.unwrap_or_default(); - 475
let type_attr = written_type - 476
.as_ref() - 477
.map(|kind| format!(r#" type="{}""#, escape_attr(kind))) - 478
.unwrap_or_default(); - 479
let index_attr = index - 480
.as_ref() - 481
.map(|index| format!(r#" idx="{}""#, escape_attr(index))) - 482
.unwrap_or_default(); - 483
body.push_str(&format!( - 484
r#"<p:sp><p:nvSpPr><p:cNvPr id="{}" name="{}"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph{type_attr}{index_attr}/></p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/>{}</p:txBody></p:sp>"#, - 485
position + 2, - 486
escape_attr(name), - 487
paragraphs(a, &text, "", "") - 488
)); - 489
} - 490
let slide = free_part_name(work, "ppt/slides/slide", ".xml"); - 491
work.put( - 492
&slide, - 493
format!( - 494
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?> - 495
<p:sld xmlns:a="{A}" xmlns:r="{R}" xmlns:p="{P}"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>{body}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>"# - 496
) - 497
.into_bytes(), - 498
); - 499
work.set_override(&slide, SLIDE_TYPE)?; - 500
work.add_relationship(&slide, &format!("{R}/slideLayout"), &layout_part)?; - 501
let relationship = work.add_relationship(&deck.part, &format!("{R}/slide"), &slide)?; - 502
- 503
let used_ids = deck - 504
.tree - 505
.nodes - 506
.iter() - 507
.filter(|node| node.local() == "sldId") - 508
.filter_map(|node| { - 509
node.element - 510
.attr_unprefixed("id") - 511
.and_then(|id| id.parse::<u32>().ok()) - 512
}); - 513
let new_id = used_ids.max().map(|max| max + 1).unwrap_or(256).max(256); - 514
let (p, r) = (&deck.p, &deck.r); - 515
let entry = format!(r#"<{p}sldId id="{new_id}" {r}id="{relationship}"/>"#); - 516
let mut splice = Splice::default(); - 517
let mut order: Vec<String> = deck - 518
.slides - 519
.iter() - 520
.map(|(id, _, _)| format!("slide:{id}")) - 521
.collect(); - 522
match (deck.list, after) { - 523
(Some(_), Some(after)) => { - 524
let after_id = slide_id(after)?; - 525
let Some(position) = deck.slides.iter().position(|(id, _, _)| id == after_id) else { - 526
return fail(format!("no slide {after} to add after")); - 527
}; - 528
splice.insert(deck.tree.nodes[deck.slides[position].2].span.end, entry); - 529
order.insert(position + 1, format!("slide:{new_id}")); - 530
} - 531
(Some(list), None) => { - 532
let node = &deck.tree.nodes[list]; - 533
if node.is_empty_element() { - 534
splice.replace( - 535
node.span.clone(), - 536
format!("<{p}sldIdLst>{entry}</{p}sldIdLst>"), - 537
); - 538
} else { - 539
splice.insert(node.inner.end, entry); - 540
} - 541
order.push(format!("slide:{new_id}")); - 542
} - 543
(None, _) => { - 544
let before = deck - 545
.tree - 546
.children(0, "sldSz") - 547
.chain(deck.tree.children(0, "notesSz")) - 548
.map(|index| deck.tree.nodes[index].span.start) - 549
.min(); - 550
let Some(before) = before else { - 551
return fail( - 552
"the presentation has no slide list and no slide size to place one before", - 553
); - 554
}; - 555
splice.insert(before, format!("<{p}sldIdLst>{entry}</{p}sldIdLst>")); - 556
order.push(format!("slide:{new_id}")); - 557
} - 558
} - 559
work.put(&deck.part, splice.apply(&deck.bytes, &deck.part)?); - 560
- 561
let mut expect = vec![Expect::SlideOrder(order)]; - 562
if let Some(notes) = notes.filter(|notes| !notes.trim().is_empty()) { - 563
let lines: Vec<String> = notes.split('\n').map(str::to_string).collect(); - 564
create_notes(work, &deck.part, &slide, &lines)?; - 565
expect.push(expect_text(format!("slide:{new_id}/notes"), &lines)); - 566
} - 567
for (key, text) in placeholders { - 568
if let Some(first) = text - 569
.lines() - 570
.into_iter() - 571
.map(|line| line.trim().to_string()) - 572
.find(|line| !line.is_empty()) - 573
{ - 574
if key.eq_ignore_ascii_case("title") { - 575
expect.push(Expect::UnitContains { - 576
anchor: format!("slide:{new_id}"), - 577
needles: vec![first], - 578
}); - 579
} else { - 580
expect.push(Expect::AnyUnitContains { - 581
prefix: format!("slide:{new_id}/shape:"), - 582
needle: first, - 583
}); - 584
} - 585
} - 586
} - 587
Ok(Outcome { - 588
summary: format!("slide:{new_id} added from layout {layout:?}"), - 589
expect, - 590
created: vec![format!("slide:{new_id}")], - 591
}) - 592
} - 593
- 594
pub(super) fn delete_slide<R2: Read + Seek>( - 595
work: &mut Work<'_, R2>, - 596
anchor: &str, - 597
) -> Result<Outcome, EditError> { - 598
let deck = deck(work)?; - 599
let id = slide_id(anchor)?.to_string(); - 600
let part = slide_part(work, &deck, &id)?; - 601
let Some((_, relationship, _)) = deck - 602
.slides - 603
.iter() - 604
.find(|(slide, _, _)| *slide == id) - 605
.cloned() - 606
else { - 607
return fail(format!("no slide:{id}")); - 608
}; - 609
if deck.slides.len() == 1 { - 610
return fail("this is the deck's only slide; add another before deleting it"); - 611
} - 612
let notes = work.related(&part, "notesSlide")?; - 613
let own_rels = [ - 614
rels_part_name(&deck.part), - 615
rels_part_name(&part), - 616
notes.as_deref().map(rels_part_name).unwrap_or_default(), - 617
]; - 618
for rels in work.part_names() { - 619
if !rels.to_ascii_lowercase().ends_with(".rels") || own_rels.contains(&rels) { - 620
continue; - 621
} - 622
let source = rels - 623
.rsplit_once("_rels/") - 624
.map(|(directory, file)| format!("{directory}{}", file.trim_end_matches(".rels"))) - 625
.unwrap_or_default(); - 626
if work - 627
.relationships(&source)? - 628
.iter() - 629
.any(|relationship| !relationship.external && relationship.target == part) - 630
{ - 631
return fail(format!( - 632
"slide:{id} is referenced by {source} (a link or custom show); remove that reference first" - 633
)); - 634
} - 635
} - 636
let mut splice = Splice::default(); - 637
for node in &deck.tree.nodes { - 638
if node.local() == "sldId" && node.element.attr_unprefixed("id") == Some(id.as_str()) { - 639
splice.replace(node.span.clone(), ""); - 640
} - 641
} - 642
work.put(&deck.part, splice.apply(&deck.bytes, &deck.part)?); - 643
work.remove_relationship(&deck.part, &relationship)?; - 644
if let Some(notes) = notes { - 645
work.remove(¬es); - 646
work.remove(&rels_part_name(¬es)); - 647
work.remove_override(¬es)?; - 648
} - 649
work.remove(&part); - 650
work.remove(&rels_part_name(&part)); - 651
work.remove_override(&part)?; - 652
Ok(Outcome { - 653
summary: format!("slide:{id} deleted"), - 654
expect: vec![ - 655
Expect::Absent { - 656
anchor: format!("slide:{id}"), - 657
}, - 658
Expect::SlideOrder( - 659
deck.slides - 660
.iter() - 661
.filter(|(slide, _, _)| *slide != id) - 662
.map(|(slide, _, _)| format!("slide:{slide}")) - 663
.collect(), - 664
), - 665
], - 666
created: Vec::new(), - 667
}) - 668
} - 669
- 670
pub(super) fn move_slide<R2: Read + Seek>( - 671
work: &mut Work<'_, R2>, - 672
anchor: &str, - 673
after: Option<&str>, - 674
) -> Result<Outcome, EditError> { - 675
let deck = deck(work)?; - 676
let id = slide_id(anchor)?.to_string(); - 677
let Some(from) = deck.slides.iter().position(|(slide, _, _)| *slide == id) else { - 678
return fail(format!("no slide:{id}")); - 679
}; - 680
let Some(list) = deck.list else { - 681
return fail("the presentation has no slide list"); - 682
}; - 683
let moving = &deck.tree.nodes[deck.slides[from].2]; - 684
let entry = String::from_utf8_lossy(&deck.bytes[moving.span.clone()]).into_owned(); - 685
let mut order: Vec<String> = deck - 686
.slides - 687
.iter() - 688
.map(|(id, _, _)| format!("slide:{id}")) - 689
.collect(); - 690
let moved = order.remove(from); - 691
let mut splice = Splice::default(); - 692
splice.replace(moving.span.clone(), ""); - 693
match after { - 694
Some(after) => { - 695
let after_id = slide_id(after)?; - 696
if after_id == id { - 697
return fail("a slide cannot move after itself"); - 698
} - 699
let Some(target) = deck - 700
.slides - 701
.iter() - 702
.position(|(slide, _, _)| slide == after_id) - 703
else { - 704
return fail(format!("no slide {after} to move after")); - 705
}; - 706
splice.insert(deck.tree.nodes[deck.slides[target].2].span.end, entry); - 707
let position = order - 708
.iter() - 709
.position(|slide| *slide == format!("slide:{after_id}")) - 710
.unwrap_or(0); - 711
order.insert(position + 1, moved); - 712
} - 713
None => { - 714
splice.insert(deck.tree.nodes[list].inner.start, entry); - 715
order.insert(0, moved); - 716
} - 717
} - 718
work.put(&deck.part, splice.apply(&deck.bytes, &deck.part)?); - 719
Ok(Outcome { - 720
summary: format!( - 721
"slide:{id} moved {}", - 722
after - 723
.map(|after| format!("after {after}")) - 724
.unwrap_or_else(|| "to the start".into()) - 725
), - 726
expect: vec![Expect::SlideOrder(order)], - 727
created: Vec::new(), - 728
}) - 729
} - 730
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.