- 1
//! Word ops. Every change to an existing document is a native tracked - 2
//! change under the runtime-supplied author (docs/design/72, R7), so it - 3
//! survives into Word as a redline a person can accept or reject there. A - 4
//! paragraph's new text is written as a redline of only the words that - 5
//! differ ([`super::redline`]). - 6
- 7
use std::io::{Read, Seek}; - 8
- 9
use super::{EditContext, EditError, Expect, Outcome, Work, fail, redline}; - 10
use crate::Limits; - 11
use crate::splice::{Splice, Tree, escape_attr, escape_text, start_tag}; - 12
- 13
const W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; - 14
const W_STRICT: &str = "http://purl.oclc.org/ooxml/wordprocessingml/main"; - 15
const W14: &str = "http://schemas.microsoft.com/office/word/2010/wordml"; - 16
const MC: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006"; - 17
- 18
/// Markup inside a paragraph that `delete_paragraph` will not rewrite: - 19
/// existing revisions, fields and text boxes. The error says what to do - 20
/// instead. - 21
const REFUSED_INSIDE: &[&str] = &[ - 22
"ins", - 23
"del", - 24
"moveFrom", - 25
"moveTo", - 26
"fldChar", - 27
"fldSimple", - 28
"instrText", - 29
"txbxContent", - 30
]; - 31
- 32
struct Doc { - 33
part: String, - 34
bytes: Vec<u8>, - 35
tree: Tree, - 36
w: String, - 37
} - 38
- 39
fn load<R: Read + Seek>(work: &mut Work<'_, R>) -> Result<Doc, EditError> { - 40
let part = work.main_part(); - 41
refuse_protected(work, &part)?; - 42
let bytes = work.get(&part)?; - 43
let tree = Tree::parse(&bytes, &part, work.limits())?; - 44
let Some(w) = tree.prefix_for(W).or_else(|| tree.prefix_for(W_STRICT)) else { - 45
return fail("the document does not declare the WordprocessingML namespace on its root"); - 46
}; - 47
Ok(Doc { - 48
part, - 49
bytes, - 50
tree, - 51
w, - 52
}) - 53
} - 54
- 55
/// Document protection (O8). Restricted editing that still allows tracked - 56
/// changes permits these ops, since every one of them is a tracked change; - 57
/// read-only, comments-only and forms-only protection refuse them. - 58
fn refuse_protected<R: Read + Seek>(work: &mut Work<'_, R>, main: &str) -> Result<(), EditError> { - 59
let Some(settings) = work.related(main, "settings")? else { - 60
return Ok(()); - 61
}; - 62
let bytes = work.get(&settings)?; - 63
let tree = Tree::parse(&bytes, &settings, work.limits())?; - 64
let Some(protection) = tree.descendants(0, "documentProtection").next() else { - 65
return Ok(()); - 66
}; - 67
let element = &tree.nodes[protection].element; - 68
let enforced = element - 69
.attr("enforcement") - 70
.is_some_and(|value| matches!(value, "1" | "true" | "on")); - 71
if enforced && element.attr("edit") != Some("trackedChanges") { - 72
return fail(format!( - 73
"the document is protected ({} editing only); the owner must remove the protection before it can be edited", - 74
element.attr("edit").unwrap_or("no") - 75
)); - 76
} - 77
Ok(()) - 78
} - 79
- 80
/// Paragraph anchors exactly as `read` assigns them: `p:<paraId>` when the - 81
/// paragraph has one, otherwise `p@<n>` counting paragraphs without one. - 82
fn paragraphs(tree: &Tree) -> Vec<(String, usize)> { - 83
let mut ordinal = 0usize; - 84
tree.descendants(0, "p") - 85
.map(|index| { - 86
let anchor = match tree.nodes[index].element.attr("paraId") { - 87
Some(id) => format!("p:{id}"), - 88
None => { - 89
ordinal += 1; - 90
format!("p@{ordinal}") - 91
} - 92
}; - 93
(anchor, index) - 94
}) - 95
.collect() - 96
} - 97
- 98
fn find(doc: &Doc, anchor: &str) -> Result<usize, EditError> { - 99
let all = paragraphs(&doc.tree); - 100
if let Some((_, index)) = all.iter().find(|(candidate, _)| candidate == anchor) { - 101
return Ok(*index); - 102
} - 103
Err(EditError { - 104
op: None, - 105
message: format!( - 106
"no paragraph {anchor}; use an anchor from doc_read on this exact file (anchors look like p:1A2B3C4D or p@12){}", - 107
suggestion(doc, &all, anchor) - 108
), - 109
}) - 110
} - 111
- 112
/// When a model names a paragraph by its text instead of its anchor - 113
/// (`p:Steady.`), the anchors of the paragraphs that text points at, so - 114
/// the call can be repaired without another read. - 115
fn suggestion(doc: &Doc, all: &[(String, usize)], anchor: &str) -> String { - 116
let needle = anchor - 117
.strip_prefix("p:") - 118
.or_else(|| anchor.strip_prefix("p@")) - 119
.unwrap_or(anchor) - 120
.trim(); - 121
if needle.is_empty() { - 122
return String::new(); - 123
} - 124
let lower = needle.to_lowercase(); - 125
let texts: Vec<(&str, String)> = all - 126
.iter() - 127
.map(|(candidate, index)| (candidate.as_str(), paragraph_text(doc, *index))) - 128
.collect(); - 129
let exact: Vec<&str> = texts - 130
.iter() - 131
.filter(|(_, text)| text.trim().to_lowercase() == lower) - 132
.map(|(candidate, _)| *candidate) - 133
.collect(); - 134
if !exact.is_empty() { - 135
return format!( - 136
". The paragraph whose text is {needle:?} is {}", - 137
exact.join(", ") - 138
); - 139
} - 140
if needle.chars().count() < 4 { - 141
return String::new(); - 142
} - 143
let containing: Vec<&str> = texts - 144
.iter() - 145
.filter(|(_, text)| text.to_lowercase().contains(&lower)) - 146
.map(|(candidate, _)| *candidate) - 147
.take(4) - 148
.collect(); - 149
match containing.len() { - 150
0 => String::new(), - 151
1..=3 => format!( - 152
". Paragraphs containing {needle:?}: {}", - 153
containing.join(", ") - 154
), - 155
_ => format!( - 156
". Several paragraphs contain {needle:?}, among them {}; read the file to choose", - 157
containing[..3].join(", ") - 158
), - 159
} - 160
} - 161
- 162
/// A paragraph's text from its `w:t` elements, for suggesting an anchor. - 163
fn paragraph_text(doc: &Doc, paragraph: usize) -> String { - 164
let limits = Limits::default(); - 165
doc.tree - 166
.descendants(paragraph, "t") - 167
.filter_map(|node| { - 168
let inner = doc.tree.nodes[node].inner.clone(); - 169
redline::decode_text(&doc.bytes[inner], &doc.part, &limits).ok() - 170
}) - 171
.collect() - 172
} - 173
- 174
fn refuse_complex(doc: &Doc, paragraph: &usize, anchor: &str) -> Result<(), EditError> { - 175
for local in REFUSED_INSIDE { - 176
if doc.tree.descendants(*paragraph, local).next().is_some() { - 177
return fail(format!( - 178
"{anchor} contains {} markup, which this op does not rewrite; insert a new paragraph after it instead", - 179
match *local { - 180
"ins" | "del" | "moveFrom" | "moveTo" => "tracked-change", - 181
"txbxContent" => "text-box", - 182
_ => "field", - 183
} - 184
)); - 185
} - 186
} - 187
Ok(()) - 188
} - 189
- 190
/// What removing a paragraph outright would break, in a new document where - 191
/// nothing is tracked: someone's tracked change, a section's layout, a - 192
/// comment or note left without its mark, or a field, bookmark or comment - 193
/// range that continues into another paragraph. - 194
fn refuse_unremovable(doc: &Doc, paragraph: usize, anchor: &str) -> Result<(), EditError> { - 195
let tree = &doc.tree; - 196
let has = |local: &str| tree.descendants(paragraph, local).next().is_some(); - 197
if ["ins", "del", "moveFrom", "moveTo"] - 198
.iter() - 199
.any(|local| has(local)) - 200
{ - 201
return fail(format!( - 202
"{anchor} holds tracked changes, which removing it would discard; accept or reject them in Word first" - 203
)); - 204
} - 205
if has("sectPr") { - 206
return fail(format!( - 207
"{anchor} ends a section of the document (its page layout), so removing it would merge two sections; replace its text with an empty string instead" - 208
)); - 209
} - 210
if ["commentReference", "footnoteReference", "endnoteReference"] - 211
.iter() - 212
.any(|local| has(local)) - 213
{ - 214
return fail(format!( - 215
"{anchor} holds the mark of a comment or a note, which would be left with nowhere to point; replace its text instead" - 216
)); - 217
} - 218
let field_marks = |kind: &str| { - 219
tree.descendants(paragraph, "fldChar") - 220
.filter(|node| tree.nodes[*node].element.attr("fldCharType") == Some(kind)) - 221
.count() - 222
}; - 223
let ids = |local: &str| -> std::collections::BTreeSet<String> { - 224
tree.descendants(paragraph, local) - 225
.filter_map(|node| tree.nodes[node].element.attr("id").map(str::to_string)) - 226
.collect() - 227
}; - 228
let split_range = [ - 229
("bookmarkStart", "bookmarkEnd"), - 230
("commentRangeStart", "commentRangeEnd"), - 231
("permStart", "permEnd"), - 232
] - 233
.iter() - 234
.any(|(start, end)| ids(start) != ids(end)); - 235
if field_marks("begin") != field_marks("end") || split_range { - 236
return fail(format!( - 237
"{anchor} holds one end of a field, bookmark or comment range that continues into another paragraph; replace its text instead" - 238
)); - 239
} - 240
Ok(()) - 241
} - 242
- 243
fn next_revision_id(doc: &Doc) -> u64 { - 244
let key = format!("{}id", doc.w); - 245
doc.tree - 246
.nodes - 247
.iter() - 248
.flat_map(|node| node.element.attributes.iter()) - 249
.filter(|(name, _)| *name == key) - 250
.filter_map(|(_, value)| value.parse::<u64>().ok()) - 251
.max() - 252
.map(|max| max + 1) - 253
.unwrap_or(1) - 254
} - 255
- 256
fn revision(doc: &Doc, id: u64, context: &EditContext) -> String { - 257
let w = &doc.w; - 258
format!( - 259
r#" {w}id="{id}" {w}author="{}" {w}date="{}""#, - 260
escape_attr(&context.author), - 261
escape_attr(&context.date) - 262
) - 263
} - 264
- 265
/// Run content for `text`: tabs and line breaks become `w:tab`/`w:br`. - 266
pub(super) fn run_content(w: &str, text: &str) -> String { - 267
let mut out = String::new(); - 268
for (line_index, line) in text.split('\n').enumerate() { - 269
if line_index > 0 { - 270
out.push_str(&format!("<{w}br/>")); - 271
} - 272
for (piece_index, piece) in line.split('\t').enumerate() { - 273
if piece_index > 0 { - 274
out.push_str(&format!("<{w}tab/>")); - 275
} - 276
if !piece.is_empty() { - 277
out.push_str(&format!( - 278
r#"<{w}t xml:space="preserve">{}</{w}t>"#, - 279
escape_text(piece) - 280
)); - 281
} - 282
} - 283
} - 284
out - 285
} - 286
- 287
/// The run's bytes with each `w:t` renamed `w:delText`, as a deletion - 288
/// requires. - 289
fn deleted_run(doc: &Doc, run: usize) -> Result<String, EditError> { - 290
let node = &doc.tree.nodes[run]; - 291
let mut splice = Splice::default(); - 292
for text in doc.tree.descendants(run, "t") { - 293
let text = &doc.tree.nodes[text]; - 294
let renamed = text.element.name.replace(":t", ":delText"); - 295
let renamed = if renamed == "t" { - 296
"delText".to_string() - 297
} else { - 298
renamed - 299
}; - 300
if text.is_empty_element() { - 301
splice.replace( - 302
text.span.start - node.span.start..text.span.end - node.span.start, - 303
start_tag(&renamed, &text.element.attributes, true), - 304
); - 305
} else { - 306
splice.replace( - 307
text.span.start - node.span.start..text.inner.start - node.span.start, - 308
start_tag(&renamed, &text.element.attributes, false), - 309
); - 310
splice.replace( - 311
text.inner.end - node.span.start..text.span.end - node.span.start, - 312
format!("</{renamed}>"), - 313
); - 314
} - 315
} - 316
let bytes = splice.apply(&doc.bytes[node.span.clone()], &doc.part)?; - 317
Ok(String::from_utf8_lossy(&bytes).into_owned()) - 318
} - 319
- 320
fn runs(doc: &Doc, paragraph: usize) -> Vec<usize> { - 321
doc.tree.descendants(paragraph, "r").collect() - 322
} - 323
- 324
/// Inserts `content` as the last content of `paragraph`, expanding an - 325
/// empty `<w:p/>` when needed. - 326
fn append_to_paragraph(doc: &Doc, splice: &mut Splice, paragraph: usize, content: &str) { - 327
let node = &doc.tree.nodes[paragraph]; - 328
if node.is_empty_element() { - 329
let open = String::from_utf8_lossy(&doc.bytes[node.span.start..node.span.end - 2]) - 330
.trim_end() - 331
.to_string(); - 332
splice.replace( - 333
node.span.clone(), - 334
format!("{open}>{content}</{}>", node.element.name), - 335
); - 336
} else { - 337
splice.insert(node.inner.end, content.to_string()); - 338
} - 339
} - 340
- 341
fn lines(text: &str) -> Vec<String> { - 342
text.split(['\n', '\t']) - 343
.map(str::trim) - 344
.filter(|line| !line.is_empty()) - 345
.map(str::to_string) - 346
.collect() - 347
} - 348
- 349
pub(super) fn replace_paragraph_text<R: Read + Seek>( - 350
work: &mut Work<'_, R>, - 351
context: &EditContext, - 352
anchor: &str, - 353
text: &str, - 354
) -> Result<Outcome, EditError> { - 355
let doc = load(work)?; - 356
let paragraph = find(&doc, anchor)?; - 357
let limits = *work.limits(); - 358
let part = redline::Part { - 359
name: &doc.part, - 360
bytes: &doc.bytes, - 361
tree: &doc.tree, - 362
limits: &limits, - 363
w: &doc.w, - 364
}; - 365
let replaced = redline::replace( - 366
&part, - 367
paragraph, - 368
anchor, - 369
text, - 370
context, - 371
next_revision_id(&doc), - 372
)?; - 373
if let Some(bytes) = replaced.bytes { - 374
work.put(&doc.part, bytes); - 375
} - 376
Ok(Outcome { - 377
summary: replaced.summary, - 378
expect: vec![Expect::Paragraph { - 379
anchor: anchor.to_string(), - 380
author: context.author.clone(), - 381
tracked: context.tracked, - 382
accepted: replaced.accepted, - 383
rejected: replaced.rejected, - 384
fixed: replaced.fixed, - 385
}], - 386
created: Vec::new(), - 387
}) - 388
} - 389
- 390
/// The paragraph `anchor` of a written main part, as a paragraph edit's - 391
/// postcondition reads it. - 392
pub(super) fn paragraph_views( - 393
name: &str, - 394
bytes: &[u8], - 395
limits: &Limits, - 396
anchor: &str, - 397
author: &str, - 398
tracked: bool, - 399
) -> Result<redline::Views, String> { - 400
let tree = Tree::parse(bytes, name, limits).map_err(|error| error.to_string())?; - 401
let Some(w) = tree.prefix_for(W).or_else(|| tree.prefix_for(W_STRICT)) else { - 402
return Err("the document does not declare the WordprocessingML namespace".into()); - 403
}; - 404
let Some((_, paragraph)) = paragraphs(&tree) - 405
.into_iter() - 406
.find(|(candidate, _)| candidate == anchor) - 407
else { - 408
return Err(format!("{anchor} is missing")); - 409
}; - 410
let part = redline::Part { - 411
name, - 412
bytes, - 413
tree: &tree, - 414
limits, - 415
w: &w, - 416
}; - 417
redline::views(&part, paragraph, author, tracked).map_err(|error| error.message) - 418
} - 419
- 420
/// Where new block content goes: after the paragraph `after` names, or at - 421
/// the end of the body, before its final section properties. Returns the - 422
/// byte offset and the paragraph directly above it, if the block there is - 423
/// one. - 424
fn insertion_point(doc: &Doc, after: Option<&str>) -> Result<(usize, Option<usize>), EditError> { - 425
if let Some(anchor) = after { - 426
let paragraph = find(doc, anchor)?; - 427
return Ok((doc.tree.nodes[paragraph].span.end, Some(paragraph))); - 428
} - 429
let Some(body) = doc.tree.descendants(0, "body").next() else { - 430
return fail("the document has no body"); - 431
}; - 432
let node = &doc.tree.nodes[body]; - 433
if node.is_empty_element() { - 434
return fail("the document's body is empty markup this op cannot extend"); - 435
} - 436
let at = doc - 437
.tree - 438
.children(body, "sectPr") - 439
.next() - 440
.map(|section| doc.tree.nodes[section].span.start) - 441
.unwrap_or(node.inner.end); - 442
let above = node - 443
.children - 444
.iter() - 445
.copied() - 446
.rfind(|child| { - 447
let child = &doc.tree.nodes[*child]; - 448
!child.skipped - 449
&& matches!(child.local(), "p" | "tbl" | "sdt" | "customXml") - 450
&& child.span.end <= at - 451
}) - 452
.filter(|block| doc.tree.nodes[*block].local() == "p"); - 453
Ok((at, above)) - 454
} - 455
- 456
fn inside(doc: &Doc, index: usize, local: &str) -> bool { - 457
let mut current = doc.tree.nodes[index].parent; - 458
while let Some(parent) = current { - 459
if doc.tree.nodes[parent].local() == local { - 460
return true; - 461
} - 462
current = doc.tree.nodes[parent].parent; - 463
} - 464
false - 465
} - 466
- 467
/// A paragraph property element (`pStyle`, `numPr`) of `paragraph`. - 468
fn paragraph_property(tree: &Tree, paragraph: usize, local: &str) -> Option<usize> { - 469
let properties = tree.children(paragraph, "pPr").next()?; - 470
tree.children(properties, local).next() - 471
} - 472
- 473
/// The numbering a new paragraph in style `style_id` writes on itself: - 474
/// `Some(numId)` for a numbered list that must restart or continue an - 475
/// explicit list, `None` to leave numbering to the style. A numbered - 476
/// paragraph continues the list of the paragraph directly above it when - 477
/// that has the same style, and otherwise starts a new list at 1: a new - 478
/// instance of the same list definition. Bullets never need this. - 479
fn list_numbering<R: Read + Seek>( - 480
work: &mut Work<'_, R>, - 481
doc: &Doc, - 482
style_id: &str, - 483
above: Option<usize>, - 484
) -> Result<Option<String>, EditError> { - 485
let Some(styles_part) = work.related(&doc.part, "styles")? else { - 486
return Ok(None); - 487
}; - 488
let styles = work.get(&styles_part)?; - 489
let styles_tree = Tree::parse(&styles, &styles_part, work.limits())?; - 490
let style_numbering = styles_tree - 491
.descendants(0, "style") - 492
.find(|style| styles_tree.nodes[*style].element.attr("styleId") == Some(style_id)) - 493
.and_then(|style| styles_tree.descendants(style, "numId").next()) - 494
.and_then(|number| styles_tree.nodes[number].element.attr("val")) - 495
.map(str::to_string); - 496
let Some(style_numbering) = style_numbering else { - 497
return Ok(None); - 498
}; - 499
let Some(numbering_part) = work.related(&doc.part, "numbering")? else { - 500
return Ok(None); - 501
}; - 502
let bytes = work.get(&numbering_part)?; - 503
let tree = Tree::parse(&bytes, &numbering_part, work.limits())?; - 504
let abstract_id = tree - 505
.children(0, "num") - 506
.find(|num| tree.nodes[*num].element.attr("numId") == Some(style_numbering.as_str())) - 507
.and_then(|num| tree.children(num, "abstractNumId").next()) - 508
.and_then(|reference| tree.nodes[reference].element.attr("val")) - 509
.map(str::to_string); - 510
let Some(abstract_id) = abstract_id else { - 511
return Ok(None); - 512
}; - 513
let format = tree - 514
.children(0, "abstractNum") - 515
.find(|definition| { - 516
tree.nodes[*definition].element.attr("abstractNumId") == Some(abstract_id.as_str()) - 517
}) - 518
.and_then(|definition| { - 519
tree.children(definition, "lvl") - 520
.find(|level| tree.nodes[*level].element.attr("ilvl") == Some("0")) - 521
}) - 522
.and_then(|level| tree.children(level, "numFmt").next()) - 523
.and_then(|format| tree.nodes[format].element.attr("val")) - 524
.map(str::to_string); - 525
if !format.is_some_and(|format| format != "bullet" && format != "none") { - 526
return Ok(None); - 527
} - 528
if let Some(above) = above { - 529
let same_style = paragraph_property(&doc.tree, above, "pStyle") - 530
.and_then(|style| doc.tree.nodes[style].element.attr("val")) - 531
== Some(style_id); - 532
if same_style { - 533
return Ok(paragraph_property(&doc.tree, above, "numPr") - 534
.and_then(|numbering| doc.tree.children(numbering, "numId").next()) - 535
.and_then(|number| doc.tree.nodes[number].element.attr("val")) - 536
.map(str::to_string)); - 537
} - 538
} - 539
let Some(w) = tree.prefix_for(W).or_else(|| tree.prefix_for(W_STRICT)) else { - 540
return fail("the numbering part does not declare the WordprocessingML namespace"); - 541
}; - 542
let root = tree.root(); - 543
if root.is_empty_element() { - 544
return fail("the numbering part is empty markup this op cannot extend"); - 545
} - 546
let definitions: Vec<usize> = tree.children(0, "abstractNum").collect(); - 547
let nums: Vec<usize> = tree.children(0, "num").collect(); - 548
let next = nums - 549
.iter() - 550
.filter_map(|num| tree.nodes[*num].element.attr("numId")?.parse::<u32>().ok()) - 551
.max() - 552
.unwrap_or(0) - 553
+ 1; - 554
let Some(definition) = definitions.iter().copied().find(|definition| { - 555
tree.nodes[*definition].element.attr("abstractNumId") == Some(abstract_id.as_str()) - 556
}) else { - 557
return Ok(None); - 558
}; - 559
if tree.nodes[definition].is_empty_element() { - 560
return Ok(None); - 561
} - 562
// A new list is its own copy of the list definition, which every - 563
// renderer numbers from its start; a second instance of one definition - 564
// with a start override restarts in Word but continues in some others. - 565
// The copy drops what must stay unique to the original: its list id - 566
// (`nsid`), the numbering style it defines, and the paragraph styles its - 567
// levels are linked to. - 568
let mut splice = Splice::default(); - 569
let restart = if tree.children(definition, "numStyleLink").next().is_some() { - 570
format!( - 571
r#"<{w}num {w}numId="{next}"><{w}abstractNumId {w}val="{}"/><{w}lvlOverride {w}ilvl="0"><{w}startOverride {w}val="1"/></{w}lvlOverride></{w}num>"#, - 572
escape_attr(&abstract_id) - 573
) - 574
} else { - 575
let copy_id = definitions - 576
.iter() - 577
.filter_map(|definition| { - 578
tree.nodes[*definition] - 579
.element - 580
.attr("abstractNumId")? - 581
.parse::<u32>() - 582
.ok() - 583
}) - 584
.max() - 585
.unwrap_or(0) - 586
+ 1; - 587
let node = &tree.nodes[definition]; - 588
let base = node.span.start; - 589
let mut copy = Splice::default(); - 590
let mut attributes = node.element.attributes.clone(); - 591
for (key, value) in &mut attributes { - 592
if crate::xml::local_name(key) == "abstractNumId" { - 593
*value = copy_id.to_string(); - 594
} - 595
} - 596
copy.replace( - 597
0..node.inner.start - base, - 598
start_tag(&node.element.name, &attributes, false), - 599
); - 600
for local in ["nsid", "styleLink", "pStyle"] { - 601
for dropped in tree.descendants(definition, local) { - 602
let span = &tree.nodes[dropped].span; - 603
copy.replace(span.start - base..span.end - base, ""); - 604
} - 605
} - 606
let copied = copy.apply(&bytes[node.span.clone()], &numbering_part)?; - 607
let at = definitions - 608
.last() - 609
.map(|last| tree.nodes[*last].span.end) - 610
.unwrap_or(root.inner.start); - 611
splice.insert(at, copied); - 612
format!(r#"<{w}num {w}numId="{next}"><{w}abstractNumId {w}val="{copy_id}"/></{w}num>"#) - 613
}; - 614
let at = nums - 615
.last() - 616
.or(definitions.last()) - 617
.map(|last| tree.nodes[*last].span.end) - 618
.unwrap_or(root.inner.end); - 619
splice.insert(at, restart); - 620
work.put(&numbering_part, splice.apply(&bytes, &numbering_part)?); - 621
Ok(Some(next.to_string())) - 622
} - 623
- 624
pub(super) fn add_paragraph<R: Read + Seek>( - 625
work: &mut Work<'_, R>, - 626
context: &EditContext, - 627
text: &str, - 628
style: Option<&str>, - 629
after: Option<&str>, - 630
) -> Result<Outcome, EditError> { - 631
if text.trim().is_empty() { - 632
return fail("text must not be empty"); - 633
} - 634
let style_id = match style { - 635
Some(style) => Some(resolve_style(work, style)?), - 636
None => None, - 637
}; - 638
let doc = load(work)?; - 639
let (at, above) = insertion_point(&doc, after)?; - 640
let numbering = match &style_id { - 641
Some(style_id) => list_numbering(work, &doc, style_id, above)?, - 642
None => None, - 643
}; - 644
let w = doc.w.clone(); - 645
let mut splice = Splice::default(); - 646
let (w14, root_tag) = ensure_w14(&doc)?; - 647
if let Some((range, tag)) = root_tag { - 648
splice.replace(range, tag); - 649
} - 650
let para_id = new_para_ids(&doc, 1).remove(0); - 651
let mut properties = style_id - 652
.map(|id| format!(r#"<{w}pStyle {w}val="{}"/>"#, escape_attr(&id))) - 653
.unwrap_or_default(); - 654
if let Some(number) = &numbering { - 655
properties.push_str(&format!( - 656
r#"<{w}numPr><{w}ilvl {w}val="0"/><{w}numId {w}val="{}"/></{w}numPr>"#, - 657
escape_attr(number) - 658
)); - 659
} - 660
let paragraph_xml = if context.tracked { - 661
let first = next_revision_id(&doc); - 662
format!( - 663
r#"<{w}p {w14}paraId="{para_id}"><{w}pPr>{properties}<{w}rPr><{w}ins{}/></{w}rPr></{w}pPr><{w}ins{}><{w}r>{}</{w}r></{w}ins></{w}p>"#, - 664
revision(&doc, first, context), - 665
revision(&doc, first + 1, context), - 666
run_content(&w, text) - 667
) - 668
} else { - 669
let properties = if properties.is_empty() { - 670
String::new() - 671
} else { - 672
format!("<{w}pPr>{properties}</{w}pPr>") - 673
}; - 674
format!( - 675
r#"<{w}p {w14}paraId="{para_id}">{properties}<{w}r>{}</{w}r></{w}p>"#, - 676
run_content(&w, text) - 677
) - 678
}; - 679
splice.insert(at, paragraph_xml); - 680
work.put(&doc.part, splice.apply(&doc.bytes, &doc.part)?); - 681
let new_anchor = format!("p:{para_id}"); - 682
let place = after - 683
.map(|anchor| format!("after {anchor}")) - 684
.unwrap_or_else(|| "at the end".into()); - 685
Ok(Outcome { - 686
summary: if context.tracked { - 687
format!("{new_anchor} added {place} as a tracked insertion") - 688
} else { - 689
format!("{new_anchor} added {place}") - 690
}, - 691
expect: vec![ - 692
Expect::UnitContains { - 693
anchor: new_anchor.clone(), - 694
needles: lines(text), - 695
}, - 696
Expect::ParagraphDelta(1), - 697
], - 698
created: vec![new_anchor], - 699
}) - 700
} - 701
- 702
/// The width text runs across in the document's last section, in twips. - 703
fn text_width(doc: &Doc) -> u32 { - 704
let tree = &doc.tree; - 705
let Some(section) = tree - 706
.descendants(0, "body") - 707
.next() - 708
.and_then(|body| tree.children(body, "sectPr").next()) - 709
else { - 710
return 9026; - 711
}; - 712
let number = |element: &str, attribute: &str| { - 713
tree.children(section, element) - 714
.next() - 715
.and_then(|node| tree.nodes[node].element.attr(attribute)) - 716
.and_then(|value| value.parse::<i64>().ok()) - 717
}; - 718
match number("pgSz", "w") { - 719
Some(width) => { - 720
let text = width - 721
- number("pgMar", "left").unwrap_or(1440) - 722
- number("pgMar", "right").unwrap_or(1440); - 723
u32::try_from(text.clamp(1440, 31_680)).unwrap_or(9026) - 724
} - 725
None => 9026, - 726
} - 727
} - 728
- 729
/// Largest table `add_table` writes: Word allows 63 columns. - 730
const MAX_TABLE_ROWS: usize = 1_000; - 731
const MAX_TABLE_COLUMNS: usize = 63; - 732
- 733
pub(super) fn add_table<R: Read + Seek>( - 734
work: &mut Work<'_, R>, - 735
context: &EditContext, - 736
rows: &[Vec<super::CellValue>], - 737
after: Option<&str>, - 738
header: bool, - 739
) -> Result<Outcome, EditError> { - 740
let columns = rows.iter().map(Vec::len).max().unwrap_or(0); - 741
if columns == 0 { - 742
return fail( - 743
"rows is empty; give at least one row of cells, e.g. [[\"Region\", \"Sales\"], [\"North\", \"120\"]]", - 744
); - 745
} - 746
if rows.len() > MAX_TABLE_ROWS || columns > MAX_TABLE_COLUMNS { - 747
return fail(format!( - 748
"a table can have at most {MAX_TABLE_ROWS} rows and {MAX_TABLE_COLUMNS} columns" - 749
)); - 750
} - 751
if work.strict() { - 752
return fail("adding a table to a Strict document is not supported yet"); - 753
} - 754
let doc = load(work)?; - 755
let (at, _) = insertion_point(&doc, after)?; - 756
if let Some(anchor) = after - 757
&& inside(&doc, find(&doc, anchor)?, "tbl") - 758
{ - 759
return fail(format!( - 760
"{anchor} is inside a table; name a paragraph outside any table, or leave out after to add the table at the end" - 761
)); - 762
} - 763
let w = doc.w.clone(); - 764
let mut splice = Splice::default(); - 765
let (w14, root_tag) = ensure_w14(&doc)?; - 766
if let Some((range, tag)) = root_tag { - 767
splice.replace(range, tag); - 768
} - 769
let ordinal = 1 + doc - 770
.tree - 771
.nodes - 772
.iter() - 773
.enumerate() - 774
.filter(|(index, node)| { - 775
!node.skipped - 776
&& node.local() == "tbl" - 777
&& node.span.start < at - 778
&& !inside(&doc, *index, "tbl") - 779
}) - 780
.count(); - 781
let width = text_width(&doc); - 782
let cell_width = width / u32::try_from(columns).unwrap_or(1); - 783
let ids = new_para_ids(&doc, rows.len() * columns); - 784
let mut revision_id = next_revision_id(&doc); - 785
let mut mark = |doc: &Doc| { - 786
let attributes = revision(doc, revision_id, context); - 787
revision_id += 1; - 788
attributes - 789
}; - 790
let border = |side: &str| { - 791
format!(r#"<{w}{side} {w}val="single" {w}sz="4" {w}space="0" {w}color="auto"/>"#) - 792
}; - 793
let borders: String = ["top", "left", "bottom", "right", "insideH", "insideV"] - 794
.iter() - 795
.map(|side| border(side)) - 796
.collect(); - 797
let mut xml = format!( - 798
r#"<{w}tbl><{w}tblPr><{w}tblW {w}w="5000" {w}type="pct"/><{w}tblBorders>{borders}</{w}tblBorders><{w}tblLayout {w}type="fixed"/><{w}tblLook {w}val="04A0" {w}firstRow="1" {w}lastRow="0" {w}firstColumn="1" {w}lastColumn="0" {w}noHBand="0" {w}noVBand="1"/></{w}tblPr><{w}tblGrid>"# - 799
); - 800
for _ in 0..columns { - 801
xml.push_str(&format!(r#"<{w}gridCol {w}w="{cell_width}"/>"#)); - 802
} - 803
xml.push_str(&format!("</{w}tblGrid>")); - 804
let mut expect = Vec::with_capacity(rows.len() + 1); - 805
let mut next_id = ids.iter(); - 806
for (row_index, row) in rows.iter().enumerate() { - 807
let is_header = header && row_index == 0; - 808
let mut row_properties = String::new(); - 809
if is_header { - 810
row_properties.push_str(&format!("<{w}tblHeader/>")); - 811
} - 812
if context.tracked { - 813
row_properties.push_str(&format!("<{w}ins{}/>", mark(&doc))); - 814
} - 815
xml.push_str(&format!("<{w}tr>")); - 816
if !row_properties.is_empty() { - 817
xml.push_str(&format!("<{w}trPr>{row_properties}</{w}trPr>")); - 818
} - 819
let mut needles = Vec::new(); - 820
for column in 0..columns { - 821
let text = row - 822
.get(column) - 823
.map(super::CellValue::as_text) - 824
.unwrap_or_default(); - 825
needles.extend(lines(&text)); - 826
let shading = if is_header { - 827
format!(r#"<{w}shd {w}val="clear" {w}color="auto" {w}fill="F2F2F2"/>"#) - 828
} else { - 829
String::new() - 830
}; - 831
let bold = if is_header { - 832
format!("<{w}b/>") - 833
} else { - 834
String::new() - 835
}; - 836
let inserted_mark = if context.tracked { - 837
format!("<{w}ins{}/>", mark(&doc)) - 838
} else { - 839
String::new() - 840
}; - 841
let mark_properties = if inserted_mark.is_empty() && bold.is_empty() { - 842
String::new() - 843
} else { - 844
format!("<{w}rPr>{inserted_mark}{bold}</{w}rPr>") - 845
}; - 846
let run = if text.is_empty() { - 847
String::new() - 848
} else { - 849
let run_properties = if bold.is_empty() { - 850
String::new() - 851
} else { - 852
format!("<{w}rPr>{bold}</{w}rPr>") - 853
}; - 854
let run = format!("<{w}r>{run_properties}{}</{w}r>", run_content(&w, &text)); - 855
if context.tracked { - 856
format!("<{w}ins{}>{run}</{w}ins>", mark(&doc)) - 857
} else { - 858
run - 859
} - 860
}; - 861
let para_id = next_id.next().cloned().unwrap_or_default(); - 862
xml.push_str(&format!( - 863
r#"<{w}tc><{w}tcPr><{w}tcW {w}w="{cell_width}" {w}type="dxa"/>{shading}</{w}tcPr><{w}p {w14}paraId="{para_id}"><{w}pPr><{w}spacing {w}before="40" {w}after="40" {w}line="240" {w}lineRule="auto"/>{mark_properties}</{w}pPr>{run}</{w}p></{w}tc>"# - 864
)); - 865
} - 866
xml.push_str(&format!("</{w}tr>")); - 867
expect.push(Expect::UnitContains { - 868
anchor: format!("tbl@{ordinal}/r{}", row_index + 1), - 869
needles, - 870
}); - 871
} - 872
xml.push_str(&format!("</{w}tbl>")); - 873
splice.insert(at, xml); - 874
work.put(&doc.part, splice.apply(&doc.bytes, &doc.part)?); - 875
expect.push(Expect::ParagraphDelta( - 876
i64::try_from(rows.len() * columns).unwrap_or(i64::MAX), - 877
)); - 878
let place = after - 879
.map(|anchor| format!("after {anchor}")) - 880
.unwrap_or_else(|| "at the end".into()); - 881
Ok(Outcome { - 882
summary: format!( - 883
"table tbl@{ordinal} ({} rows, {columns} columns) added {place}{}", - 884
rows.len(), - 885
if context.tracked { - 886
" as a tracked insertion" - 887
} else { - 888
"" - 889
} - 890
), - 891
expect, - 892
created: ids.into_iter().map(|id| format!("p:{id}")).collect(), - 893
}) - 894
} - 895
- 896
pub(super) fn delete_paragraph<R: Read + Seek>( - 897
work: &mut Work<'_, R>, - 898
context: &EditContext, - 899
anchor: &str, - 900
) -> Result<Outcome, EditError> { - 901
let doc = load(work)?; - 902
let paragraph = find(&doc, anchor)?; - 903
if context.tracked { - 904
refuse_complex(&doc, ¶graph, anchor)?; - 905
} else { - 906
refuse_unremovable(&doc, paragraph, anchor)?; - 907
} - 908
let node = &doc.tree.nodes[paragraph]; - 909
if let Some(parent) = node.parent { - 910
let parent_node = &doc.tree.nodes[parent]; - 911
let siblings: Vec<usize> = parent_node - 912
.children - 913
.iter() - 914
.copied() - 915
.filter(|child| { - 916
let child = &doc.tree.nodes[*child]; - 917
!child.skipped && matches!(child.local(), "p" | "tbl" | "sdt") - 918
}) - 919
.collect(); - 920
let last = siblings.last() == Some(¶graph); - 921
if parent_node.local() == "body" && last { - 922
return fail(format!( - 923
"{anchor} is the document's last paragraph, which Word requires; replace its text with an empty string instead" - 924
)); - 925
} - 926
if parent_node.local() == "tc" && siblings.len() == 1 { - 927
return fail(format!( - 928
"{anchor} is the only paragraph in its table cell, which Word requires; replace its text with an empty string instead" - 929
)); - 930
} - 931
} - 932
if !context.tracked { - 933
// A new document is written clean: the paragraph simply goes. - 934
let mut splice = Splice::default(); - 935
splice.replace(node.span.clone(), ""); - 936
work.put(&doc.part, splice.apply(&doc.bytes, &doc.part)?); - 937
let mut expect = vec![Expect::ParagraphDelta(-1)]; - 938
if anchor.starts_with("p:") { - 939
expect.push(Expect::Absent { - 940
anchor: anchor.to_string(), - 941
}); - 942
} - 943
return Ok(Outcome { - 944
summary: format!("{anchor} removed"), - 945
expect, - 946
created: Vec::new(), - 947
}); - 948
} - 949
let w = doc.w.clone(); - 950
let mut id = next_revision_id(&doc); - 951
let mut splice = Splice::default(); - 952
for run in runs(&doc, paragraph) { - 953
let deleted = deleted_run(&doc, run)?; - 954
splice.replace( - 955
doc.tree.nodes[run].span.clone(), - 956
format!("<{w}del{}>{deleted}</{w}del>", revision(&doc, id, context)), - 957
); - 958
id += 1; - 959
} - 960
let mark = format!("<{w}del{}/>", revision(&doc, id, context)); - 961
match doc.tree.children(paragraph, "pPr").next() { - 962
Some(properties) => { - 963
let properties_node = &doc.tree.nodes[properties]; - 964
match doc.tree.children(properties, "rPr").next() { - 965
Some(run_properties) => { - 966
let run_properties = &doc.tree.nodes[run_properties]; - 967
if run_properties.is_empty_element() { - 968
splice.replace( - 969
run_properties.span.clone(), - 970
format!("<{w}rPr>{mark}</{w}rPr>"), - 971
); - 972
} else { - 973
splice.insert(run_properties.inner.start, mark); - 974
} - 975
} - 976
None => { - 977
let before = doc - 978
.tree - 979
.children(properties, "sectPr") - 980
.chain(doc.tree.children(properties, "pPrChange")) - 981
.map(|child| doc.tree.nodes[child].span.start) - 982
.min(); - 983
let wrapped = format!("<{w}rPr>{mark}</{w}rPr>"); - 984
if properties_node.is_empty_element() { - 985
splice.replace( - 986
properties_node.span.clone(), - 987
format!("<{w}pPr>{wrapped}</{w}pPr>"), - 988
); - 989
} else { - 990
splice.insert(before.unwrap_or(properties_node.inner.end), wrapped); - 991
} - 992
} - 993
} - 994
} - 995
None => { - 996
let content = format!("<{w}pPr><{w}rPr>{mark}</{w}rPr></{w}pPr>"); - 997
if node.is_empty_element() { - 998
append_to_paragraph(&doc, &mut splice, paragraph, &content); - 999
} else { - 1000
splice.insert(node.inner.start, content);
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.