- 1
//! Creating a file from scratch (docs/design/72-openxml-documents.md, - 2
//! "Creating from scratch"): each built-in blank is a valid, empty, - 3
//! deterministic package, and the anchor-free ops fill a new file in one - 4
//! call, checked by a re-read like every edit. - 5
- 6
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 7
- 8
use std::collections::BTreeMap; - 9
use std::io::Cursor; - 10
- 11
use vak_ooxml::blank::{DECK_LAYOUTS, DOCUMENT_STYLES, WORKBOOK_SHEET, blank}; - 12
use vak_ooxml::edit::{self, CellValue, EditContext, OfficeOp, TextValue}; - 13
use vak_ooxml::read::{self, UnitKind}; - 14
use vak_ooxml::{Format, FormatKind, Limits, Package, Vocabulary, fixtures, review}; - 15
- 16
fn format(extension: &str) -> Format { - 17
Format::from_extension(extension).unwrap() - 18
} - 19
- 20
fn clean() -> EditContext { - 21
EditContext { - 22
author: "Vakyartha".into(), - 23
date: "2026-09-27T10:00:00Z".into(), - 24
tracked: false, - 25
} - 26
} - 27
- 28
fn create(extension: &str, ops: Vec<OfficeOp>) -> Result<edit::Applied, edit::EditError> { - 29
let target = format(extension); - 30
edit::apply( - 31
&blank(target).unwrap(), - 32
&ops, - 33
&clean(), - 34
Limits::default(), - 35
Some(target), - 36
) - 37
} - 38
- 39
fn part(bytes: &[u8], name: &str) -> String { - 40
let mut package = Package::open(Cursor::new(bytes.to_vec()), Limits::default()).unwrap(); - 41
String::from_utf8(package.read_part(name).unwrap()).unwrap() - 42
} - 43
- 44
fn paragraph(text: &str, style: Option<&str>) -> OfficeOp { - 45
OfficeOp::AddParagraph { - 46
text: text.into(), - 47
style: style.map(str::to_string), - 48
after: None, - 49
} - 50
} - 51
- 52
fn text(value: &str) -> CellValue { - 53
CellValue::Text(value.into()) - 54
} - 55
- 56
#[test] - 57
fn each_blank_is_an_empty_deterministic_package_of_its_format() { - 58
for extension in ["docx", "xlsx", "pptx"] { - 59
let bytes = blank(format(extension)).unwrap(); - 60
assert_eq!(bytes, blank(format(extension)).unwrap(), "{extension}"); - 61
let document = read::read(Cursor::new(bytes), Limits::default()).unwrap(); - 62
assert_eq!(document.inspection.format, format(extension), "{extension}"); - 63
assert!(document.inspection.flags().is_empty(), "{extension}"); - 64
match extension { - 65
"xlsx" => { - 66
assert!(document.units.is_empty(), "{:?}", document.units); - 67
assert_eq!(document.sections.len(), 1); - 68
assert_eq!(document.sections[0].anchor, format!("{WORKBOOK_SHEET}!")); - 69
} - 70
_ => assert!( - 71
document.units.is_empty(), - 72
"{extension}: {:?}", - 73
document.units - 74
), - 75
} - 76
} - 77
} - 78
- 79
#[test] - 80
fn a_blank_is_refused_for_macro_enabled_names_and_visio() { - 81
for extension in ["docm", "xlsm", "pptm", "xlam", "ppam"] { - 82
let error = blank(format(extension)).unwrap_err(); - 83
assert!( - 84
error.contains("never makes a macro-enabled file"), - 85
"{error}" - 86
); - 87
} - 88
let error = blank(format("vsdx")).unwrap_err(); - 89
assert!(error.contains("Visio"), "{error}"); - 90
} - 91
- 92
#[test] - 93
fn a_word_document_is_written_from_scratch_in_one_call() { - 94
let applied = create( - 95
"docx", - 96
vec![ - 97
OfficeOp::SetTitle { - 98
title: "Q3 review".into(), - 99
}, - 100
paragraph("Q3 review", Some("Title")), - 101
paragraph("Prepared for the leadership team", Some("Subtitle")), - 102
paragraph("Summary", Some("Heading 1")), - 103
paragraph("Revenue grew 12% on the quarter.", None), - 104
paragraph("Close the Pune office", Some("List Number")), - 105
paragraph("Hire two engineers", Some("List Number")), - 106
paragraph("Next steps", Some("Heading 2")), - 107
paragraph("Draft the budget", Some("List Number")), - 108
paragraph("Review it in October", Some("List Number")), - 109
paragraph("Travel is paused", Some("List Bullet")), - 110
OfficeOp::AddTable { - 111
rows: vec![ - 112
vec![text("Region"), text("Revenue")], - 113
vec![text("North"), CellValue::Number(120.0)], - 114
vec![text("South"), CellValue::Number(95.5)], - 115
], - 116
after: None, - 117
header: None, - 118
}, - 119
paragraph("Figures are unaudited.", Some("Quote")), - 120
], - 121
) - 122
.unwrap(); - 123
let document = &applied.document; - 124
assert_eq!(document.title.as_deref(), Some("Q3 review")); - 125
let texts: Vec<&str> = document - 126
.units - 127
.iter() - 128
.map(|unit| unit.text.as_str()) - 129
.collect(); - 130
assert_eq!( - 131
texts, - 132
[ - 133
"Q3 review", - 134
"Prepared for the leadership team", - 135
"Summary", - 136
"Revenue grew 12% on the quarter.", - 137
"Close the Pune office", - 138
"Hire two engineers", - 139
"Next steps", - 140
"Draft the budget", - 141
"Review it in October", - 142
"Travel is paused", - 143
"Region | Revenue", - 144
"North | 120", - 145
"South | 95.5", - 146
"Figures are unaudited.", - 147
] - 148
); - 149
let headings: Vec<(&str, u8)> = document - 150
.sections - 151
.iter() - 152
.map(|section| (section.title.as_str(), section.level)) - 153
.collect(); - 154
assert_eq!( - 155
headings, - 156
[("Q3 review", 1), ("Summary", 1), ("Next steps", 2)] - 157
); - 158
assert_eq!(document.units[10].kind, UnitKind::TableRow); - 159
assert_eq!(document.units[10].anchor, "tbl@1/r1"); - 160
let stat = |name: &str| { - 161
document - 162
.stats - 163
.iter() - 164
.find(|(stat, _)| stat == name) - 165
.map(|(_, count)| *count) - 166
}; - 167
assert_eq!(stat("tracked changes"), Some(0), "a new document is clean"); - 168
assert_eq!(stat("tables"), Some(1)); - 169
- 170
// Each numbered list starts at 1: each is its own copy of the list - 171
// definition, without the original's style link, and the second item - 172
// of each list continues its own list. - 173
let body = part(&applied.bytes, "word/document.xml"); - 174
let numbering = part(&applied.bytes, "word/numbering.xml"); - 175
assert!( - 176
numbering.contains(r#"<w:num w:numId="3"><w:abstractNumId w:val="2"/></w:num>"#), - 177
"{numbering}" - 178
); - 179
assert!( - 180
numbering.contains(r#"<w:num w:numId="4"><w:abstractNumId w:val="3"/></w:num>"#), - 181
"{numbering}" - 182
); - 183
assert_eq!( - 184
numbering - 185
.matches(r#"<w:pStyle w:val="ListNumber"/>"#) - 186
.count(), - 187
1, - 188
"{numbering}" - 189
); - 190
assert!( - 191
numbering - 192
.find(r#"<w:abstractNum w:abstractNumId="3">"#) - 193
.unwrap() - 194
< numbering.find(r#"<w:num w:numId="1">"#).unwrap(), - 195
"definitions come before instances: {numbering}" - 196
); - 197
assert_eq!(body.matches(r#"<w:numId w:val="3"/>"#).count(), 2, "{body}"); - 198
assert_eq!(body.matches(r#"<w:numId w:val="4"/>"#).count(), 2, "{body}"); - 199
assert!( - 200
!body.contains(r#"<w:pStyle w:val="ListBullet"/><w:numPr>"#), - 201
"bullets need no list instance" - 202
); - 203
// The header row repeats on each page and is bold; cells span the text - 204
// width of an A4 page with 2.54 cm margins. - 205
assert!(body.contains("<w:tblHeader/>"), "{body}"); - 206
assert!( - 207
body.contains(r#"<w:gridCol w:w="4513"/><w:gridCol w:w="4513"/>"#), - 208
"{body}" - 209
); - 210
assert!( - 211
!body.contains("<w:ins "), - 212
"nothing is tracked in a new document" - 213
); - 214
// Every op minted anchors a later op could name. - 215
assert!( - 216
applied - 217
.results - 218
.iter() - 219
.all(|result| result.check.starts_with("passed")) - 220
); - 221
assert_eq!( - 222
applied.results[11].created.len(), - 223
6, - 224
"{:?}", - 225
applied.results[11] - 226
); - 227
} - 228
- 229
#[test] - 230
fn every_style_a_blank_document_offers_can_be_used_by_name() { - 231
let ops = DOCUMENT_STYLES - 232
.iter() - 233
.map(|style| paragraph(&format!("In {style}"), Some(style))) - 234
.collect(); - 235
let applied = create("docx", ops).unwrap(); - 236
assert_eq!(applied.document.units.len(), DOCUMENT_STYLES.len()); - 237
} - 238
- 239
#[test] - 240
fn a_table_added_to_an_existing_document_is_a_tracked_insertion() { - 241
let context = EditContext { - 242
tracked: true, - 243
..clean() - 244
}; - 245
let applied = edit::apply( - 246
&fixtures::docx(), - 247
&[OfficeOp::AddTable { - 248
rows: vec![ - 249
vec![text("Owner"), text("Due")], - 250
vec![text("Mira"), text("Friday")], - 251
], - 252
after: Some("p@1".into()), - 253
header: None, - 254
}], - 255
&context, - 256
Limits::default(), - 257
None, - 258
) - 259
.unwrap(); - 260
let body = part(&applied.bytes, "word/document.xml"); - 261
assert!( - 262
body.contains(r#"<w:trPr><w:tblHeader/><w:ins w:id="#), - 263
"{body}" - 264
); - 265
let row = applied - 266
.document - 267
.units - 268
.iter() - 269
.find(|unit| unit.text.contains("Friday")) - 270
.unwrap(); - 271
assert!( - 272
row.text.contains("[inserted by Vakyartha: Friday]"), - 273
"{}", - 274
row.text - 275
); - 276
assert_eq!( - 277
row.anchor, "tbl@1/r2", - 278
"the new table comes before the fixture's own" - 279
); - 280
- 281
// A table is never nested in another table's cell. - 282
let fixture = read::read(Cursor::new(fixtures::docx()), Limits::default()).unwrap(); - 283
let cell = fixture - 284
.units - 285
.iter() - 286
.find(|unit| !unit.row_cells.is_empty()) - 287
.map(|unit| unit.row_cells[0][0].0.clone()) - 288
.unwrap(); - 289
let inside = edit::apply( - 290
&fixtures::docx(), - 291
&[OfficeOp::AddTable { - 292
rows: vec![vec![text("x")]], - 293
after: Some(cell), - 294
header: None, - 295
}], - 296
&context, - 297
Limits::default(), - 298
None, - 299
) - 300
.unwrap_err(); - 301
assert!(inside.message.contains("is inside a table"), "{inside}"); - 302
} - 303
- 304
#[test] - 305
fn a_workbook_is_built_from_scratch_with_names_formats_and_widths() { - 306
let cells: BTreeMap<String, CellValue> = [ - 307
("A1", text("Item")), - 308
("B1", text("Q3")), - 309
("C1", text("Q4")), - 310
("A2", text("Rent")), - 311
("B2", CellValue::Number(1200.0)), - 312
("C2", CellValue::Number(1250.5)), - 313
("A3", text("Total")), - 314
("B3", text("=SUM(B2:B2)")), - 315
("C3", text("=SUM(C2:C2)")), - 316
] - 317
.into_iter() - 318
.map(|(address, value)| (address.to_string(), value)) - 319
.collect(); - 320
let applied = create( - 321
"xlsx", - 322
vec![ - 323
OfficeOp::RenameSheet { - 324
sheet: WORKBOOK_SHEET.into(), - 325
name: "Budget".into(), - 326
}, - 327
OfficeOp::SetCells { - 328
sheet: "Budget".into(), - 329
cells, - 330
}, - 331
OfficeOp::FormatCells { - 332
sheet: "Budget".into(), - 333
range: "A1:C1".into(), - 334
bold: Some(true), - 335
italic: None, - 336
number_format: None, - 337
fill: Some("#D9E2F3".into()), - 338
wrap: None, - 339
}, - 340
OfficeOp::FormatCells { - 341
sheet: "Budget".into(), - 342
range: "B2:C3".into(), - 343
bold: None, - 344
italic: None, - 345
number_format: Some("#,##0.00".into()), - 346
fill: None, - 347
wrap: None, - 348
}, - 349
OfficeOp::FormatCells { - 350
sheet: "Budget".into(), - 351
range: "A3:C3".into(), - 352
bold: Some(true), - 353
italic: None, - 354
number_format: None, - 355
fill: None, - 356
wrap: None, - 357
}, - 358
OfficeOp::FormatCells { - 359
sheet: "Budget".into(), - 360
range: "D2".into(), - 361
bold: None, - 362
italic: None, - 363
number_format: Some("0.0%".into()), - 364
fill: None, - 365
wrap: None, - 366
}, - 367
OfficeOp::SetColumnWidths { - 368
sheet: "Budget".into(), - 369
widths: [ - 370
("A".to_string(), 28.0), - 371
("B".to_string(), 14.0), - 372
("C".to_string(), 14.0), - 373
] - 374
.into_iter() - 375
.collect(), - 376
}, - 377
OfficeOp::AddSheet { - 378
name: "Notes".into(), - 379
}, - 380
], - 381
) - 382
.unwrap(); - 383
let sections: Vec<&str> = applied - 384
.document - 385
.sections - 386
.iter() - 387
.map(|section| section.anchor.as_str()) - 388
.collect(); - 389
assert_eq!(sections, ["Budget!", "Notes!"]); - 390
let lines = applied.document.lines().join("\n"); - 391
assert!(lines.contains("B2: 1200"), "{lines}"); - 392
assert!( - 393
lines.contains("B3: =SUM(B2:B2) [not calculated yet]"), - 394
"{lines}" - 395
); - 396
let styles = part(&applied.bytes, "xl/styles.xml"); - 397
// #,##0.00 is built in (id 4); 0.0% is declared once, as the first - 398
// custom format. - 399
assert!(styles.contains(r#"numFmtId="4""#), "{styles}"); - 400
assert!( - 401
styles.contains( - 402
r#"<numFmts count="1"><numFmt numFmtId="164" formatCode="0.0%"/></numFmts><fonts"# - 403
), - 404
"{styles}" - 405
); - 406
assert!(styles.contains(r#"<fgColor rgb="FFD9E2F3"/>"#), "{styles}"); - 407
// B3 is both a total (bold) and a number (#,##0.00): the second format - 408
// kept the first. - 409
let sheet = part(&applied.bytes, "xl/worksheets/sheet1.xml"); - 410
assert!( - 411
sheet.contains(r#"<cols><col min="1" max="1" width="28" customWidth="1"/>"#), - 412
"{sheet}" - 413
); - 414
let style_of = |cell: &str| { - 415
let start = sheet.find(&format!(r#"r="{cell}""#)).unwrap(); - 416
let tag = &sheet[start..start + sheet[start..].find('>').unwrap()]; - 417
tag.split("s=\"") - 418
.nth(1) - 419
.unwrap() - 420
.split('"') - 421
.next() - 422
.unwrap() - 423
.to_string() - 424
}; - 425
let b3 = style_of("B3"); - 426
assert_ne!(b3, style_of("B2"), "B3 is bold as well"); - 427
assert_ne!(b3, style_of("A3"), "B3 has the number format as well"); - 428
assert!( - 429
applied - 430
.results - 431
.iter() - 432
.all(|result| result.check.starts_with("passed")) - 433
); - 434
} - 435
- 436
#[test] - 437
fn a_sheet_is_not_renamed_while_anything_names_it() { - 438
let error = create( - 439
"xlsx", - 440
vec![ - 441
OfficeOp::SetCells { - 442
sheet: WORKBOOK_SHEET.into(), - 443
cells: [ - 444
("A1".to_string(), CellValue::Number(2.0)), - 445
("A2".to_string(), text("=Sheet1!A1*2")), - 446
] - 447
.into_iter() - 448
.collect(), - 449
}, - 450
OfficeOp::RenameSheet { - 451
sheet: WORKBOOK_SHEET.into(), - 452
name: "Budget".into(), - 453
}, - 454
], - 455
) - 456
.unwrap_err(); - 457
assert_eq!(error.op, Some((1, "rename_sheet"))); - 458
assert!( - 459
error.message.contains("xl/worksheets/sheet1.xml"), - 460
"{error}" - 461
); - 462
- 463
let error = create( - 464
"xlsx", - 465
vec![OfficeOp::RenameSheet { - 466
sheet: "Missing".into(), - 467
name: "Budget".into(), - 468
}], - 469
) - 470
.unwrap_err(); - 471
assert!(error.message.contains("sheets: Sheet1"), "{error}"); - 472
} - 473
- 474
#[test] - 475
fn a_deck_is_built_from_the_blanks_layouts_with_speaker_notes() { - 476
let slide = |layout: &str, placeholders: &[(&str, TextValue)], notes: Option<&str>| { - 477
OfficeOp::AddSlideFromLayout { - 478
layout: layout.into(), - 479
after: None, - 480
placeholders: placeholders - 481
.iter() - 482
.map(|(key, value)| (key.to_string(), value.clone())) - 483
.collect(), - 484
notes: notes.map(str::to_string), - 485
} - 486
}; - 487
let one = |text: &str| TextValue::One(text.into()); - 488
let lines = - 489
|items: &[&str]| TextValue::Lines(items.iter().map(|item| item.to_string()).collect()); - 490
let applied = create( - 491
"pptx", - 492
vec![ - 493
slide( - 494
"Title Slide", - 495
&[ - 496
("title", one("Launch plan")), - 497
("subtitle", one("October 2026")), - 498
], - 499
None, - 500
), - 501
slide( - 502
"Title and Content", - 503
&[ - 504
("title", one("Goals")), - 505
("body", lines(&["Ship v1", "Sign ten customers"])), - 506
], - 507
Some("Keep this short.\nPause for questions."), - 508
), - 509
slide( - 510
"Section Header", - 511
&[("title", one("Timeline")), ("body", one("Q4 milestones"))], - 512
None, - 513
), - 514
slide( - 515
"Two Content", - 516
&[ - 517
("title", one("Trade-offs")), - 518
("idx:1", lines(&["Faster"])), - 519
("idx:2", lines(&["Riskier"])), - 520
], - 521
None, - 522
), - 523
slide("Title Only", &[("title", one("Questions?"))], None), - 524
slide("Blank", &[], None), - 525
OfficeOp::SetTitle { - 526
title: "Launch plan".into(), - 527
}, - 528
], - 529
) - 530
.unwrap(); - 531
let document = &applied.document; - 532
let titles: Vec<&str> = document - 533
.sections - 534
.iter() - 535
.map(|section| section.title.as_str()) - 536
.collect(); - 537
assert_eq!( - 538
titles, - 539
[ - 540
"Slide 1: Launch plan", - 541
"Slide 2: Goals", - 542
"Slide 3: Timeline", - 543
"Slide 4: Trade-offs", - 544
"Slide 5: Questions?", - 545
"Slide 6: Slide 6", - 546
] - 547
); - 548
let text = document.lines().join("\n"); - 549
assert!( - 550
text.contains("[slide:257/notes] Keep this short. ¶ Pause for questions."), - 551
"{text}" - 552
); - 553
assert!(text.contains("Ship v1 ¶ Sign ten customers"), "{text}"); - 554
// A text placeholder keeps its type; a content placeholder has none. - 555
assert!( - 556
part(&applied.bytes, "ppt/slides/slide3.xml").contains(r#"<p:ph type="body" idx="1"/>"#) - 557
); - 558
assert!(part(&applied.bytes, "ppt/slides/slide2.xml").contains(r#"<p:ph idx="1"/>"#)); - 559
let notes_rels = part(&applied.bytes, "ppt/notesSlides/_rels/notesSlide1.xml.rels"); - 560
assert!( - 561
notes_rels.contains("../notesMasters/notesMaster1.xml"), - 562
"{notes_rels}" - 563
); - 564
assert!(notes_rels.contains("../slides/slide2.xml"), "{notes_rels}"); - 565
assert!( - 566
part(&applied.bytes, "[Content_Types].xml") - 567
.contains(r#"<Override PartName="/ppt/notesSlides/notesSlide1.xml""#) - 568
); - 569
assert_eq!(DECK_LAYOUTS.len(), 6); - 570
- 571
// set_notes gives a slide without notes its notes page. - 572
let with_notes = edit::apply( - 573
&applied.bytes, - 574
&[OfficeOp::SetNotes { - 575
anchor: "slide:256".into(), - 576
text: "Welcome everyone.".into(), - 577
}], - 578
&clean(), - 579
Limits::default(), - 580
None, - 581
) - 582
.unwrap(); - 583
assert!( - 584
with_notes - 585
.document - 586
.lines() - 587
.join("\n") - 588
.contains("[slide:256/notes] Welcome everyone.") - 589
); - 590
} - 591
- 592
#[test] - 593
fn a_template_name_makes_a_template_from_the_blank() { - 594
for (extension, kind) in [ - 595
("dotx", FormatKind::Template), - 596
("potx", FormatKind::Template), - 597
("ppsx", FormatKind::Slideshow), - 598
] { - 599
let op = match format(extension).vocabulary { - 600
Vocabulary::Word => paragraph("Letterhead", Some("Title")), - 601
_ => OfficeOp::AddSlideFromLayout { - 602
layout: "Title Slide".into(), - 603
after: None, - 604
placeholders: BTreeMap::new(), - 605
notes: None, - 606
}, - 607
}; - 608
let applied = create(extension, vec![op]).unwrap(); - 609
assert_eq!(applied.document.inspection.format.kind, kind, "{extension}"); - 610
} - 611
} - 612
- 613
#[test] - 614
fn a_from_scratch_draft_is_offered_change_by_change() { - 615
let source = blank(format("docx")).unwrap(); - 616
let ops = vec![ - 617
paragraph("Summary", Some("Heading 1")), - 618
OfficeOp::AddTable { - 619
rows: vec![vec![text("A"), text("B")]], - 620
after: None, - 621
header: None, - 622
}, - 623
// Written against a read of the first draft: after the table's - 624
// second cell paragraph, which op 2 minted. - 625
OfficeOp::AddParagraph { - 626
text: "In the cell".into(), - 627
style: None, - 628
after: Some("p:1A000003".into()), - 629
}, - 630
paragraph("Closing line", None), - 631
]; - 632
let full = edit::apply(&source, &ops, &clean(), Limits::default(), None).unwrap(); - 633
let offered = review::choices( - 634
&source, - 635
&ops, - 636
&clean(), - 637
Limits::default(), - 638
None, - 639
&full.document, - 640
) - 641
.unwrap(); - 642
let requires: Vec<Vec<String>> = offered - 643
.iter() - 644
.map(|choice| choice.requires.clone()) - 645
.collect(); - 646
assert_eq!(requires, [vec![], vec![], vec!["1".to_string()], vec![]]); - 647
assert_eq!(offered[0].label, "New paragraph: Summary"); - 648
assert_eq!(offered[1].label, "New table, 1 row by 2 columns"); - 649
- 650
// Leaving the heading out still puts the cell paragraph in the table's - 651
// second cell: the replay remaps the anchors it mints. - 652
let narrowed = review::narrow( - 653
&source, - 654
&ops, - 655
&["1".into(), "2".into(), "3".into()], - 656
&clean(), - 657
Limits::default(), - 658
None, - 659
&full.document, - 660
) - 661
.unwrap(); - 662
let kept = narrowed.document.lines().join("\n"); - 663
assert!(!kept.contains("Summary"), "{kept}"); - 664
assert!( - 665
kept.contains("[p:1A000002] B [p:1A000003] In the cell"), - 666
"{kept}" - 667
); - 668
- 669
let excel_source = blank(format("xlsx")).unwrap(); - 670
let excel_ops = vec![ - 671
OfficeOp::RenameSheet { - 672
sheet: "Sheet1".into(), - 673
name: "Budget".into(), - 674
}, - 675
OfficeOp::SetCells { - 676
sheet: "Budget".into(), - 677
cells: [("A1".to_string(), text("Rent"))].into_iter().collect(), - 678
}, - 679
]; - 680
let excel = edit::apply(&excel_source, &excel_ops, &clean(), Limits::default(), None).unwrap(); - 681
let offered = review::choices( - 682
&excel_source, - 683
&excel_ops, - 684
&clean(), - 685
Limits::default(), - 686
None, - 687
&excel.document, - 688
) - 689
.unwrap(); - 690
assert_eq!( - 691
offered[1].requires, - 692
["0"], - 693
"a cell on the renamed sheet needs the rename" - 694
); - 695
} - 696
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.