- 1
//! L3: the typed op set and its one apply engine (O3–O5, O8, O10). - 2
//! - 3
//! Ops name anchors a read returned. Each op splices the parts it changes - 4
//! (O1), and after the last op the engine writes the package, re-reads it, - 5
//! and checks every op's postcondition against the re-read projection (O5). - 6
//! A failed postcondition fails the whole apply, so a caller never writes a - 7
//! file whose edits did not land. - 8
- 9
mod deck; - 10
mod redline; - 11
mod sheet; - 12
mod textdiff; - 13
mod word; - 14
- 15
use std::collections::BTreeMap; - 16
use std::fmt; - 17
use std::io::Cursor; - 18
- 19
use serde::{Deserialize, Serialize}; - 20
- 21
use crate::package::{ - 22
Package, Relationship, Vocabulary, parse_relationships, part_key, rels_part_name, - 23
}; - 24
use crate::read::{self, Document}; - 25
use crate::splice::{Splice, Tree, escape_attr}; - 26
use crate::{Error, Limits}; - 27
- 28
/// A cell value: a number, a boolean, or text. Text starting with `=` is a - 29
/// formula; a leading `'` writes the rest as literal text. - 30
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - 31
#[serde(untagged)] - 32
pub enum CellValue { - 33
Bool(bool), - 34
Number(f64), - 35
Text(String), - 36
} - 37
- 38
impl CellValue { - 39
/// The value as the text of a Word table cell, where nothing is a - 40
/// formula. - 41
pub fn as_text(&self) -> String { - 42
match self { - 43
CellValue::Bool(true) => "TRUE".into(), - 44
CellValue::Bool(false) => "FALSE".into(), - 45
CellValue::Number(number) => number.to_string(), - 46
CellValue::Text(text) => text.clone(), - 47
} - 48
} - 49
} - 50
- 51
/// One paragraph or several, one per line. - 52
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 53
#[serde(untagged)] - 54
pub enum TextValue { - 55
Lines(Vec<String>), - 56
One(String), - 57
} - 58
- 59
impl TextValue { - 60
pub fn lines(&self) -> Vec<String> { - 61
match self { - 62
TextValue::Lines(lines) => lines.clone(), - 63
TextValue::One(text) => text.split('\n').map(str::to_string).collect(), - 64
} - 65
} - 66
} - 67
- 68
/// The op set (docs/design/72-openxml-documents.md, "P2 op set" and - 69
/// "Creating from scratch"). Every op a new file needs works without an - 70
/// anchor, because a model cannot know the anchors an op mints. - 71
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - 72
#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)] - 73
pub enum OfficeOp { - 74
ReplaceParagraphText { - 75
anchor: String, - 76
text: String, - 77
}, - 78
/// After the paragraph `after` names, or at the end of the document. - 79
AddParagraph { - 80
text: String, - 81
#[serde(default)] - 82
style: Option<String>, - 83
#[serde(default)] - 84
after: Option<String>, - 85
}, - 86
/// After the paragraph `after` names, or at the end of the document; - 87
/// the first row is a header row unless `header` is false. - 88
AddTable { - 89
rows: Vec<Vec<CellValue>>, - 90
#[serde(default)] - 91
after: Option<String>, - 92
#[serde(default)] - 93
header: Option<bool>, - 94
}, - 95
DeleteParagraph { - 96
anchor: String, - 97
}, - 98
SetCells { - 99
sheet: String, - 100
cells: BTreeMap<String, CellValue>, - 101
}, - 102
AppendRows { - 103
sheet: String, - 104
rows: Vec<Vec<CellValue>>, - 105
}, - 106
AddSheet { - 107
name: String, - 108
}, - 109
RenameSheet { - 110
sheet: String, - 111
name: String, - 112
}, - 113
FormatCells { - 114
sheet: String, - 115
range: String, - 116
#[serde(default)] - 117
bold: Option<bool>, - 118
#[serde(default)] - 119
italic: Option<bool>, - 120
#[serde(default)] - 121
number_format: Option<String>, - 122
#[serde(default)] - 123
fill: Option<String>, - 124
#[serde(default)] - 125
wrap: Option<bool>, - 126
}, - 127
SetColumnWidths { - 128
sheet: String, - 129
widths: BTreeMap<String, f64>, - 130
}, - 131
AddSlideFromLayout { - 132
layout: String, - 133
#[serde(default)] - 134
after: Option<String>, - 135
#[serde(default)] - 136
placeholders: BTreeMap<String, TextValue>, - 137
#[serde(default)] - 138
notes: Option<String>, - 139
}, - 140
SetPlaceholderText { - 141
anchor: String, - 142
text: TextValue, - 143
}, - 144
SetNotes { - 145
anchor: String, - 146
text: String, - 147
}, - 148
DeleteSlide { - 149
anchor: String, - 150
}, - 151
MoveSlide { - 152
anchor: String, - 153
#[serde(default)] - 154
after: Option<String>, - 155
}, - 156
SetTitle { - 157
title: String, - 158
}, - 159
} - 160
- 161
impl OfficeOp { - 162
pub fn name(&self) -> &'static str { - 163
match self { - 164
OfficeOp::ReplaceParagraphText { .. } => "replace_paragraph_text", - 165
OfficeOp::AddParagraph { .. } => "add_paragraph", - 166
OfficeOp::AddTable { .. } => "add_table", - 167
OfficeOp::DeleteParagraph { .. } => "delete_paragraph", - 168
OfficeOp::SetCells { .. } => "set_cells", - 169
OfficeOp::AppendRows { .. } => "append_rows", - 170
OfficeOp::AddSheet { .. } => "add_sheet", - 171
OfficeOp::RenameSheet { .. } => "rename_sheet", - 172
OfficeOp::FormatCells { .. } => "format_cells", - 173
OfficeOp::SetColumnWidths { .. } => "set_column_widths", - 174
OfficeOp::AddSlideFromLayout { .. } => "add_slide_from_layout", - 175
OfficeOp::SetPlaceholderText { .. } => "set_placeholder_text", - 176
OfficeOp::SetNotes { .. } => "set_notes", - 177
OfficeOp::DeleteSlide { .. } => "delete_slide", - 178
OfficeOp::MoveSlide { .. } => "move_slide", - 179
OfficeOp::SetTitle { .. } => "set_title", - 180
} - 181
} - 182
- 183
fn vocabulary(&self) -> Option<Vocabulary> { - 184
match self { - 185
OfficeOp::ReplaceParagraphText { .. } - 186
| OfficeOp::AddParagraph { .. } - 187
| OfficeOp::AddTable { .. } - 188
| OfficeOp::DeleteParagraph { .. } => Some(Vocabulary::Word), - 189
OfficeOp::SetCells { .. } - 190
| OfficeOp::AppendRows { .. } - 191
| OfficeOp::AddSheet { .. } - 192
| OfficeOp::RenameSheet { .. } - 193
| OfficeOp::FormatCells { .. } - 194
| OfficeOp::SetColumnWidths { .. } => Some(Vocabulary::Excel), - 195
OfficeOp::AddSlideFromLayout { .. } - 196
| OfficeOp::SetPlaceholderText { .. } - 197
| OfficeOp::SetNotes { .. } - 198
| OfficeOp::DeleteSlide { .. } - 199
| OfficeOp::MoveSlide { .. } => Some(Vocabulary::PowerPoint), - 200
OfficeOp::SetTitle { .. } => None, - 201
} - 202
} - 203
} - 204
- 205
/// Who and when, stamped on tracked changes, and whether there are any. - 206
/// Supplied by the runtime, never by the model. - 207
#[derive(Debug, Clone)] - 208
pub struct EditContext { - 209
pub author: String, - 210
/// ISO 8601, e.g. `2026-09-24T10:00:00Z`. - 211
pub date: String, - 212
/// Word edits are tracked changes in an existing document; a new - 213
/// document (one the edit creates, as from a template) is written - 214
/// clean (docs/design/72, R7). - 215
pub tracked: bool, - 216
} - 217
- 218
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 219
pub struct OpResult { - 220
pub op: String, - 221
pub summary: String, - 222
/// The postcondition, checked against a re-read of the written package. - 223
pub check: String, - 224
/// The anchors this op minted, in order (`p:<paraId>`, `slide:<id>`; - 225
/// a table mints one per cell paragraph), which later ops may name. - 226
/// Replaying a subset remaps them (`review`). - 227
pub created: Vec<String>, - 228
} - 229
- 230
#[derive(Debug, Clone)] - 231
pub struct Applied { - 232
pub bytes: Vec<u8>, - 233
pub results: Vec<OpResult>, - 234
pub document: Document, - 235
/// What the engine did beyond the ops, for the record: a signature it - 236
/// removed because the edit invalidated it (D4). - 237
pub notices: Vec<String>, - 238
} - 239
- 240
/// Why an apply failed. `op` is the 0-based op index when one op is at - 241
/// fault; the message says how to repair the call. - 242
#[derive(Debug, Clone, PartialEq, Eq)] - 243
pub struct EditError { - 244
pub op: Option<(usize, &'static str)>, - 245
pub message: String, - 246
} - 247
- 248
impl fmt::Display for EditError { - 249
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - 250
match self.op { - 251
Some((index, name)) => write!(f, "op {} ({name}): {}", index + 1, self.message), - 252
None => f.write_str(&self.message), - 253
} - 254
} - 255
} - 256
- 257
impl std::error::Error for EditError {} - 258
- 259
impl From<Error> for EditError { - 260
fn from(error: Error) -> Self { - 261
EditError { - 262
op: None, - 263
message: error.to_string(), - 264
} - 265
} - 266
} - 267
- 268
pub(crate) fn fail<T>(message: impl Into<String>) -> Result<T, EditError> { - 269
Err(EditError { - 270
op: None, - 271
message: message.into(), - 272
}) - 273
} - 274
- 275
/// What an op promises about the re-read document. - 276
#[derive(Debug, Clone)] - 277
pub(crate) enum Expect { - 278
/// The unit with this anchor exists and its text contains each needle. - 279
UnitContains { - 280
anchor: String, - 281
needles: Vec<String>, - 282
}, - 283
/// Every piece of the unit's text is marked deleted, or the unit is gone. - 284
UnitDeleted { - 285
anchor: String, - 286
}, - 287
/// Some unit's text contains `needle`, under an anchor starting `prefix`. - 288
AnyUnitContains { - 289
prefix: String, - 290
needle: String, - 291
}, - 292
/// No unit has this anchor. - 293
Absent { - 294
anchor: String, - 295
}, - 296
/// The slide outline is exactly this order of slide anchors. - 297
SlideOrder(Vec<String>), - 298
/// A section (sheet, slide, heading) with this anchor exists. - 299
Section(String), - 300
/// No section has this anchor. - 301
NoSection(String), - 302
Title(String), - 303
/// Each named cell of the sheet reads back with this formatting. - 304
CellFormat { - 305
sheet: String, - 306
cells: Vec<String>, - 307
format: sheet::Format, - 308
}, - 309
/// The sheet's columns read back with these widths, by letter. - 310
ColumnWidths { - 311
sheet: String, - 312
widths: BTreeMap<String, f64>, - 313
}, - 314
/// The op adds (1) or removes (-1) one Word paragraph; the written - 315
/// document's paragraph count is checked against every op's together. - 316
ParagraphDelta(i64), - 317
/// The Word paragraph reads `accepted` (compared folded, as - 318
/// [`textdiff::fold`] does) with `author`'s tracked changes accepted, - 319
/// reads `rejected` exactly with them rejected, and still holds the - 320
/// content the reader does not show (`fixed`, by element name). - 321
/// A clean edit (`tracked` false) leaves no revisions, so the text - 322
/// with them rejected is the text asked for too. - 323
Paragraph { - 324
anchor: String, - 325
author: String, - 326
tracked: bool, - 327
accepted: String, - 328
rejected: String, - 329
fixed: Vec<String>, - 330
}, - 331
} - 332
- 333
impl Expect { - 334
fn anchor(&self) -> Option<&str> { - 335
match self { - 336
Expect::UnitContains { anchor, .. } - 337
| Expect::UnitDeleted { anchor } - 338
| Expect::Absent { anchor } - 339
| Expect::Paragraph { anchor, .. } => Some(anchor), - 340
_ => None, - 341
} - 342
} - 343
} - 344
- 345
pub(crate) struct Outcome { - 346
pub summary: String, - 347
pub expect: Vec<Expect>, - 348
pub created: Vec<String>, - 349
} - 350
- 351
impl Outcome { - 352
/// Paragraphs whose whole text this op sets: an earlier op's promise - 353
/// about one of them is superseded by this op's. - 354
fn rewritten(&self) -> impl Iterator<Item = &str> { - 355
self.expect.iter().filter_map(|expect| match expect { - 356
Expect::Paragraph { anchor, .. } - 357
| Expect::UnitDeleted { anchor } - 358
| Expect::Absent { anchor } => Some(anchor.as_str()), - 359
_ => None, - 360
}) - 361
} - 362
} - 363
- 364
/// Applies `ops` in order to the package in `source` and returns the new - 365
/// package, having re-read it and checked every op's postcondition. - 366
/// - 367
/// `target` is the format the output file will be named as. A template - 368
/// becomes a document by changing only its main part's content type. The - 369
/// vocabulary never changes, and a macro-free file never becomes - 370
/// macro-enabled or the reverse (O10). - 371
pub fn apply( - 372
source: &[u8], - 373
ops: &[OfficeOp], - 374
context: &EditContext, - 375
limits: Limits, - 376
target: Option<crate::Format>, - 377
) -> Result<Applied, EditError> { - 378
let mut package = Package::open(Cursor::new(source.to_vec()), limits)?; - 379
let format = package.format(); - 380
let vocabulary = format.vocabulary; - 381
let retarget = target.filter(|target| *target != format); - 382
if let Some(target) = retarget { - 383
if target.vocabulary != vocabulary { - 384
return fail(format!( - 385
"the source is {} and cannot be saved as .{}", - 386
vocabulary.with_article(), - 387
target.extension() - 388
)); - 389
} - 390
if target.macro_enabled != format.macro_enabled { - 391
return fail(format!( - 392
"the source is .{} and the output .{}; Vakyartha never turns a file macro-enabled or strips its macros by renaming, so keep the .{} extension", - 393
format.extension(), - 394
target.extension(), - 395
format.extension() - 396
)); - 397
} - 398
} - 399
if ops.is_empty() && retarget.is_none() { - 400
return fail("no ops given"); - 401
} - 402
let mut work = Work::new(&mut package); - 403
if let Some(target) = retarget { - 404
let main = work.main_part(); - 405
work.set_override(&main, target.main_content_type())?; - 406
} - 407
let paragraphs_before = if vocabulary == Vocabulary::Word { - 408
let main = work.main_part(); - 409
let bytes = work.get(&main)?; - 410
Some(count_paragraphs(&bytes, &main, work.limits())?) - 411
} else { - 412
None - 413
}; - 414
let mut outcomes = Vec::with_capacity(ops.len()); - 415
for (index, op) in ops.iter().enumerate() { - 416
let tag = |error: EditError| EditError { - 417
op: Some((index, op.name())), - 418
message: error.message, - 419
}; - 420
if let Some(needed) = op.vocabulary() - 421
&& needed != vocabulary - 422
{ - 423
return Err(tag(EditError { - 424
op: None, - 425
message: format!( - 426
"this op edits {}, and this file is {}", - 427
needed.with_article(), - 428
vocabulary.with_article() - 429
), - 430
})); - 431
} - 432
let outcome = match op { - 433
OfficeOp::ReplaceParagraphText { anchor, text } => { - 434
word::replace_paragraph_text(&mut work, context, anchor, text) - 435
} - 436
OfficeOp::AddParagraph { text, style, after } => { - 437
word::add_paragraph(&mut work, context, text, style.as_deref(), after.as_deref()) - 438
} - 439
OfficeOp::AddTable { - 440
rows, - 441
after, - 442
header, - 443
} => word::add_table( - 444
&mut work, - 445
context, - 446
rows, - 447
after.as_deref(), - 448
header.unwrap_or(true), - 449
), - 450
OfficeOp::DeleteParagraph { anchor } => { - 451
word::delete_paragraph(&mut work, context, anchor) - 452
} - 453
OfficeOp::SetCells { sheet, cells } => sheet::set_cells(&mut work, sheet, cells), - 454
OfficeOp::AppendRows { sheet, rows } => sheet::append_rows(&mut work, sheet, rows), - 455
OfficeOp::AddSheet { name } => sheet::add_sheet(&mut work, name), - 456
OfficeOp::RenameSheet { sheet, name } => sheet::rename_sheet(&mut work, sheet, name), - 457
OfficeOp::FormatCells { - 458
sheet, - 459
range, - 460
bold, - 461
italic, - 462
number_format, - 463
fill, - 464
wrap, - 465
} => sheet::format_cells( - 466
&mut work, - 467
sheet, - 468
range, - 469
&sheet::Format { - 470
bold: *bold, - 471
italic: *italic, - 472
number_format: number_format.clone(), - 473
fill: fill.clone(), - 474
wrap: *wrap, - 475
}, - 476
), - 477
OfficeOp::SetColumnWidths { sheet, widths } => { - 478
sheet::set_column_widths(&mut work, sheet, widths) - 479
} - 480
OfficeOp::AddSlideFromLayout { - 481
layout, - 482
after, - 483
placeholders, - 484
notes, - 485
} => deck::add_slide_from_layout( - 486
&mut work, - 487
layout, - 488
after.as_deref(), - 489
placeholders, - 490
notes.as_deref(), - 491
), - 492
OfficeOp::SetPlaceholderText { anchor, text } => { - 493
deck::set_placeholder_text(&mut work, anchor, text) - 494
} - 495
OfficeOp::SetNotes { anchor, text } => deck::set_notes(&mut work, anchor, text), - 496
OfficeOp::DeleteSlide { anchor } => deck::delete_slide(&mut work, anchor), - 497
OfficeOp::MoveSlide { anchor, after } => { - 498
deck::move_slide(&mut work, anchor, after.as_deref()) - 499
} - 500
OfficeOp::SetTitle { title } => set_title(&mut work, title), - 501
} - 502
.map_err(tag)?; - 503
outcomes.push((index, op.name(), outcome)); - 504
} - 505
let mut notices = Vec::new(); - 506
let signatures = drop_signatures(&mut work)?; - 507
if signatures > 0 { - 508
notices.push(format!( - 509
"the source was digitally signed; its {signatures} signature(s) were removed, because any edit invalidates them and a file must not claim a signature it no longer has" - 510
)); - 511
} - 512
let edits = std::mem::take(&mut work.parts); - 513
drop(work); - 514
let bytes = package - 515
.rewrite(Cursor::new(Vec::new()), &edits)? - 516
.into_inner(); - 517
let document = read::read(Cursor::new(bytes.clone()), limits).map_err(|error| EditError { - 518
op: None, - 519
message: format!("the edited package does not read back: {error}"), - 520
})?; - 521
// Every op that changes the slide list expects the order it left, so a - 522
// later one's expectation covers an earlier one's; the earlier one, - 523
// checked against the final deck, would fail on the later op's change. - 524
let superseded: Vec<bool> = (0..outcomes.len()) - 525
.map(|position| { - 526
outcomes[position + 1..].iter().any(|(_, name, _)| { - 527
matches!( - 528
*name, - 529
"add_slide_from_layout" | "delete_slide" | "move_slide" - 530
) - 531
}) - 532
}) - 533
.collect(); - 534
// A later op that sets a paragraph's whole text supersedes what an - 535
// earlier op promised about that paragraph. - 536
let rewritten_later: Vec<Vec<String>> = (0..outcomes.len()) - 537
.map(|position| { - 538
outcomes[position + 1..] - 539
.iter() - 540
.flat_map(|(_, _, outcome)| outcome.rewritten()) - 541
.map(str::to_string) - 542
.collect() - 543
}) - 544
.collect(); - 545
let mut written = Written { - 546
bytes: &bytes, - 547
limits, - 548
package: None, - 549
main: None, - 550
}; - 551
if let Some(before) = paragraphs_before { - 552
let delta: i64 = outcomes - 553
.iter() - 554
.flat_map(|(_, _, outcome)| &outcome.expect) - 555
.map(|expect| match expect { - 556
Expect::ParagraphDelta(delta) => *delta, - 557
_ => 0, - 558
}) - 559
.sum(); - 560
let (name, main) = written.main().map_err(|message| EditError { - 561
op: None, - 562
message: format!("postcondition failed after writing: {message}"), - 563
})?; - 564
let after = count_paragraphs(main, name, &limits).map_err(|error| EditError { - 565
op: None, - 566
message: format!("postcondition failed after writing: {}", error.message), - 567
})?; - 568
let expected = i64::try_from(before).unwrap_or(i64::MAX) + delta; - 569
if i64::try_from(after).unwrap_or(i64::MAX) != expected { - 570
return Err(EditError { - 571
op: None, - 572
message: format!( - 573
"postcondition failed after writing: the document has {after} paragraphs, and its edits should leave {expected}" - 574
), - 575
}); - 576
} - 577
} - 578
let mut results = Vec::with_capacity(outcomes.len()); - 579
for (position, (index, name, outcome)) in outcomes.into_iter().enumerate() { - 580
for expect in &outcome.expect { - 581
if superseded[position] && matches!(expect, Expect::SlideOrder(_)) { - 582
continue; - 583
} - 584
if expect.anchor().is_some_and(|anchor| { - 585
rewritten_later[position] - 586
.iter() - 587
.any(|later| later == anchor) - 588
}) { - 589
continue; - 590
} - 591
check(&document, &mut written, expect).map_err(|message| EditError { - 592
op: Some((index, name)), - 593
message: format!("postcondition failed after writing: {message}"), - 594
})?; - 595
} - 596
results.push(OpResult { - 597
op: name.to_string(), - 598
summary: outcome.summary, - 599
check: "passed: re-read of the written package confirms the change".into(), - 600
created: outcome.created, - 601
}); - 602
} - 603
Ok(Applied { - 604
bytes, - 605
results, - 606
document, - 607
notices, - 608
}) - 609
} - 610
- 611
/// Removes the package's digital signatures (D4): the signature origin, its - 612
/// relationship from the package, every signature part and their content - 613
/// types. Returns how many signatures there were. Vakyartha does not verify - 614
/// signatures, but it knows an edit breaks every one of them. - 615
fn drop_signatures<R: std::io::Read + std::io::Seek>( - 616
work: &mut Work<'_, R>, - 617
) -> Result<usize, EditError> { - 618
let origins: Vec<Relationship> = work - 619
.relationships("")? - 620
.into_iter() - 621
.filter(|relationship| { - 622
!relationship.external && relationship.kind == crate::package::REL_SIGNATURE_ORIGIN - 623
}) - 624
.collect(); - 625
let mut signatures: Vec<String> = Vec::new(); - 626
for origin in &origins { - 627
for relationship in work.relationships(&origin.target)? { - 628
if !relationship.external - 629
&& relationship.short_kind() == "signature" - 630
&& !signatures.contains(&relationship.target) - 631
{ - 632
signatures.push(relationship.target); - 633
} - 634
} - 635
} - 636
let name = "[Content_Types].xml"; - 637
let bytes = work.get(name)?; - 638
let tree = Tree::parse(&bytes, name, work.limits())?; - 639
for index in tree.descendants(0, "Override") { - 640
let element = &tree.nodes[index].element; - 641
if element - 642
.attr("ContentType") - 643
.is_some_and(|kind| kind.eq_ignore_ascii_case(crate::package::CT_SIGNATURE)) - 644
&& let Some(part) = element.attr("PartName") - 645
{ - 646
let part = part.trim_start_matches('/').to_string(); - 647
if !signatures - 648
.iter() - 649
.any(|known| part_key(known) == part_key(&part)) - 650
{ - 651
signatures.push(part); - 652
} - 653
} - 654
} - 655
for part in &signatures { - 656
work.remove(part); - 657
work.remove(&rels_part_name(part)); - 658
work.remove_override(part)?; - 659
} - 660
for origin in &origins { - 661
work.remove(&origin.target); - 662
work.remove(&rels_part_name(&origin.target)); - 663
work.remove_override(&origin.target)?; - 664
work.remove_relationship("", &origin.id)?; - 665
} - 666
Ok(signatures.len().max(usize::from(!origins.is_empty()))) - 667
} - 668
- 669
/// The written package, opened and its main part read when a check needs - 670
/// them. - 671
struct Written<'a> { - 672
bytes: &'a [u8], - 673
limits: Limits, - 674
package: Option<Package<Cursor<Vec<u8>>>>, - 675
main: Option<(String, Vec<u8>)>, - 676
} - 677
- 678
impl Written<'_> { - 679
fn package(&mut self) -> Result<&mut Package<Cursor<Vec<u8>>>, String> { - 680
if self.package.is_none() { - 681
let package = Package::open(Cursor::new(self.bytes.to_vec()), self.limits) - 682
.map_err(|error| error.to_string())?; - 683
self.package = Some(package); - 684
} - 685
self.package - 686
.as_mut() - 687
.ok_or_else(|| "the written package could not be opened".to_string()) - 688
} - 689
- 690
fn main(&mut self) -> Result<(&str, &[u8]), String> { - 691
if self.main.is_none() { - 692
let package = self.package()?; - 693
let name = package.main_part().to_string(); - 694
let bytes = package - 695
.read_part(&name) - 696
.map_err(|error| error.to_string())?; - 697
self.main = Some((name, bytes)); - 698
} - 699
match &self.main { - 700
Some((name, bytes)) => Ok((name.as_str(), bytes.as_slice())), - 701
None => Err("the main part could not be read".into()), - 702
} - 703
} - 704
} - 705
- 706
/// Word paragraphs in a main part, as anchors count them. - 707
fn count_paragraphs(bytes: &[u8], part: &str, limits: &Limits) -> Result<usize, EditError> { - 708
Ok(Tree::parse(bytes, part, limits)? - 709
.descendants(0, "p") - 710
.count()) - 711
} - 712
- 713
/// `p@N` → N. - 714
fn ordinal(anchor: &str) -> Option<usize> { - 715
anchor.strip_prefix("p@")?.split('/').next()?.parse().ok() - 716
} - 717
- 718
/// The Word paragraphs an op names. - 719
fn paragraph_references(op: &OfficeOp) -> Vec<&str> { - 720
match op { - 721
OfficeOp::ReplaceParagraphText { anchor, .. } | OfficeOp::DeleteParagraph { anchor } => { - 722
vec![anchor.as_str()] - 723
} - 724
OfficeOp::AddParagraph { after, .. } | OfficeOp::AddTable { after, .. } => { - 725
after.as_deref().into_iter().collect() - 726
} - 727
_ => Vec::new(), - 728
} - 729
} - 730
- 731
/// Whether op `later` names a paragraph that removing the paragraph op - 732
/// `earlier` deletes renumbers. In a new document (written clean) a - 733
/// deleted paragraph is removed, so the `p@` paragraphs after a removed - 734
/// `p@N` move up one; `p:` anchors never move. - 735
pub fn renumbered_by(earlier: &OfficeOp, later: &OfficeOp, context: &EditContext) -> bool { - 736
let OfficeOp::DeleteParagraph { anchor } = earlier else { - 737
return false; - 738
}; - 739
let Some(removed) = ordinal(anchor) else { - 740
return false; - 741
}; - 742
!context.tracked - 743
&& paragraph_references(later) - 744
.iter() - 745
.filter_map(|reference| ordinal(reference)) - 746
.any(|named| named >= removed) - 747
} - 748
- 749
/// Refuses ops written against one read that removing a `p@` paragraph - 750
/// earlier in the same call would renumber (see [`renumbered_by`]): they - 751
/// would land on the wrong paragraph. Checked where one call's ops are - 752
/// applied, never on a replay of several calls, whose later ops were - 753
/// written against the renumbered draft. - 754
pub fn check_renumbering(ops: &[OfficeOp], context: &EditContext) -> Result<(), EditError> { - 755
for (earlier, op) in ops.iter().enumerate() { - 756
if let Some(later) = - 757
(earlier + 1..ops.len()).find(|later| renumbered_by(op, &ops[*later], context)) - 758
{ - 759
return Err(EditError { - 760
op: Some((later, ops[later].name())), - 761
message: format!( - 762
"it names a paragraph that removing {} (op {}) renumbers in this new document; put paragraph deletions after the ops that name later paragraphs, or read the draft again and use its anchors", - 763
paragraph_references(op) - 764
.first() - 765
.copied() - 766
.unwrap_or_default(), - 767
earlier + 1 - 768
), - 769
}); - 770
} - 771
} - 772
Ok(()) - 773
} - 774
- 775
fn check(document: &Document, written: &mut Written<'_>, expect: &Expect) -> Result<(), String> { - 776
// A paragraph in a Word table cell is not a unit of its own: it is read - 777
// as part of its row, under its own anchor. - 778
let unit = |anchor: &str| -> Option<&str> { - 779
document - 780
.units - 781
.iter() - 782
.find(|unit| unit.anchor == anchor) - 783
.map(|unit| unit.text.as_str()) - 784
.or_else(|| { - 785
document - 786
.units - 787
.iter() - 788
.flat_map(|unit| unit.row_cells.iter().flatten()) - 789
.find(|(paragraph, _)| paragraph == anchor) - 790
.map(|(_, text)| text.as_str()) - 791
}) - 792
}; - 793
match expect { - 794
Expect::UnitContains { anchor, needles } => { - 795
let text = unit(anchor).ok_or_else(|| format!("{anchor} is missing"))?; - 796
match needles - 797
.iter() - 798
.find(|needle| !text.contains(needle.as_str())) - 799
{ - 800
Some(missing) => Err(format!("{anchor} does not contain {missing:?}")), - 801
None => Ok(()), - 802
} - 803
} - 804
Expect::UnitDeleted { anchor } => match unit(anchor) { - 805
None => Ok(()), - 806
Some(text) => { - 807
let mut rest = text; - 808
while let Some(start) = rest.find("[deleted by ") { - 809
if !rest[..start].trim().is_empty() { - 810
return Err(format!("{anchor} still has undeleted text")); - 811
} - 812
let Some(end) = rest[start..].find(']') else { - 813
break; - 814
}; - 815
rest = &rest[start + end + 1..]; - 816
} - 817
if rest.trim().is_empty() { - 818
Ok(()) - 819
} else { - 820
Err(format!("{anchor} still has undeleted text")) - 821
} - 822
} - 823
}, - 824
Expect::AnyUnitContains { prefix, needle } => { - 825
if document.units.iter().any(|unit| { - 826
unit.anchor.starts_with(prefix.as_str()) && unit.text.contains(needle.as_str()) - 827
}) { - 828
Ok(()) - 829
} else { - 830
Err(format!("no {prefix}… unit contains {needle:?}")) - 831
} - 832
} - 833
Expect::Absent { anchor } => match unit(anchor) { - 834
None => Ok(()), - 835
Some(_) => Err(format!("{anchor} is still present")), - 836
}, - 837
Expect::SlideOrder(order) => { - 838
let actual: Vec<&str> = document - 839
.sections - 840
.iter() - 841
.map(|section| section.anchor.as_str()) - 842
.collect(); - 843
if actual == order.iter().map(String::as_str).collect::<Vec<_>>() { - 844
Ok(()) - 845
} else { - 846
Err(format!("slide order is {actual:?}, expected {order:?}")) - 847
} - 848
} - 849
Expect::Section(anchor) => { - 850
if document - 851
.sections - 852
.iter() - 853
.any(|section| section.anchor == *anchor) - 854
{ - 855
Ok(()) - 856
} else { - 857
Err(format!("no section {anchor}")) - 858
} - 859
} - 860
Expect::NoSection(anchor) => { - 861
if document - 862
.sections - 863
.iter() - 864
.any(|section| section.anchor == *anchor) - 865
{ - 866
Err(format!("section {anchor} is still present")) - 867
} else { - 868
Ok(()) - 869
} - 870
} - 871
Expect::Title(title) => { - 872
if document.title.as_deref() == Some(title.as_str()) { - 873
Ok(()) - 874
} else { - 875
Err(format!("title is {:?}", document.title)) - 876
} - 877
} - 878
Expect::CellFormat { - 879
sheet, - 880
cells, - 881
format, - 882
} => sheet::check_format(written.package()?, sheet, cells, format), - 883
Expect::ColumnWidths { sheet, widths } => { - 884
sheet::check_widths(written.package()?, sheet, widths) - 885
} - 886
Expect::ParagraphDelta(_) => Ok(()), - 887
Expect::Paragraph { - 888
anchor, - 889
author, - 890
tracked, - 891
accepted, - 892
rejected, - 893
fixed, - 894
} => { - 895
let limits = written.limits; - 896
let (name, bytes) = written.main()?; - 897
let views = word::paragraph_views(name, bytes, &limits, anchor, author, *tracked)?; - 898
if textdiff::fold_text(&views.accepted) != *accepted { - 899
return Err(format!( - 900
"{anchor} reads {:?} with the changes accepted, not the text asked for", - 901
views.accepted - 902
)); - 903
} - 904
if !*tracked && textdiff::fold_text(&views.rejected) != *accepted { - 905
return Err(format!( - 906
"{anchor} was to be written clean, and reads {:?} with its changes rejected", - 907
views.rejected - 908
)); - 909
} - 910
if *tracked && views.rejected != *rejected { - 911
return Err(format!( - 912
"{anchor} reads {:?} with the changes rejected, not what it read before", - 913
views.rejected - 914
)); - 915
} - 916
if views.fixed != *fixed { - 917
return Err(format!( - 918
"{anchor} no longer holds the content the edit had to keep ({} before, {} after)", - 919
fixed.join(", "), - 920
views.fixed.join(", ") - 921
)); - 922
} - 923
Ok(()) - 924
} - 925
} - 926
} - 927
- 928
fn set_title<R: std::io::Read + std::io::Seek>( - 929
work: &mut Work<'_, R>, - 930
title: &str, - 931
) -> Result<Outcome, EditError> { - 932
let Some(part) = work.related("", "core-properties")? else { - 933
return fail("this file has no core properties part to hold a title"); - 934
}; - 935
let bytes = work.get(&part)?; - 936
let tree = Tree::parse(&bytes, &part, work.limits())?; - 937
let text = crate::splice::escape_text(title); - 938
let mut splice = Splice::default(); - 939
match tree.descendants(0, "title").next() { - 940
Some(node) => { - 941
let node = &tree.nodes[node]; - 942
if node.is_empty_element() { - 943
let name = node.element.name.clone(); - 944
splice.replace(node.span.clone(), format!("<{name}>{text}</{name}>")); - 945
} else { - 946
splice.replace(node.inner.clone(), text); - 947
} - 948
} - 949
None => { - 950
let Some(prefix) = tree.prefix_for("http://purl.org/dc/elements/1.1/") else { - 951
return fail("the core properties part does not declare the Dublin Core namespace"); - 952
}; - 953
splice.insert( - 954
tree.root().inner.end, - 955
format!("<{prefix}title>{text}</{prefix}title>"), - 956
); - 957
} - 958
} - 959
work.put(&part, splice.apply(&bytes, &part)?); - 960
Ok(Outcome { - 961
summary: format!("title set to {title:?}"), - 962
expect: vec![Expect::Title(title.to_string())], - 963
created: Vec::new(), - 964
}) - 965
} - 966
- 967
/// Parts as the ops leave them. `None` means removed. - 968
pub(crate) struct Work<'a, R: std::io::Read + std::io::Seek> { - 969
package: &'a mut Package<R>, - 970
parts: BTreeMap<String, Option<Vec<u8>>>, - 971
} - 972
- 973
impl<'a, R: std::io::Read + std::io::Seek> Work<'a, R> { - 974
fn new(package: &'a mut Package<R>) -> Self { - 975
Self { - 976
package, - 977
parts: BTreeMap::new(), - 978
} - 979
} - 980
- 981
pub fn limits(&self) -> &Limits { - 982
self.package.limits() - 983
} - 984
- 985
pub fn main_part(&self) -> String { - 986
self.package.main_part().to_string() - 987
} - 988
- 989
pub fn strict(&self) -> bool { - 990
self.package.conformance() == crate::Conformance::Strict - 991
} - 992
- 993
fn entry(&self, name: &str) -> Option<&Option<Vec<u8>>> { - 994
let key = part_key(name); - 995
self.parts - 996
.iter() - 997
.find(|(existing, _)| part_key(existing) == key) - 998
.map(|(_, value)| value) - 999
} - 1000
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.