- 1
//! A paragraph edit as a redline that marks only what changed - 2
//! (docs/design/72-openxml-documents.md, R7 and O1). - 3
//! - 4
//! The paragraph is read the way `doc_read` shows it, and its text is - 5
//! compared with the text it should read word by word - 6
//! ([`super::textdiff`]). Only the words that differ become tracked - 7
//! changes: a run the change splits keeps its formatting on every piece, - 8
//! new text takes the formatting of the text it replaces (or of the text - 9
//! just before it), and every element the change does not touch is copied - 10
//! byte for byte. - 11
//! - 12
//! What a paragraph edit never changes: - 13
//! - text Word computes or someone else owns: a field's result, another - 14
//! author's tracked change, text inside an equation or a drawing. A - 15
//! change that would alter it is refused, naming it; - 16
//! - content the reader does not show: footnote and endnote marks, images, - 17
//! bookmarks and comment ranges, field codes, hidden and white text, and - 18
//! another author's deletions. It stays where it is, so an edit never - 19
//! removes what the model could not see; - 20
//! - the paragraph's leading and trailing whitespace, which the reader - 21
//! trims. - 22
//! - 23
//! The author's own earlier tracked changes are revised, never stacked: - 24
//! the comparison is made against the paragraph as it read before them, so - 25
//! revising a draft's change leaves one change, and asking for the original - 26
//! text withdraws it. - 27
- 28
use std::collections::{BTreeMap, HashMap, HashSet}; - 29
use std::ops::Range; - 30
- 31
use super::textdiff::{self, fold_text}; - 32
use super::word::run_content; - 33
use super::{EditContext, EditError, fail}; - 34
use crate::Limits; - 35
use crate::splice::{Splice, Tree, escape_attr, escape_text, start_tag}; - 36
use crate::xml::{self, XmlEvent}; - 37
- 38
/// The WordprocessingML part being edited. - 39
pub(super) struct Part<'a> { - 40
pub name: &'a str, - 41
pub bytes: &'a [u8], - 42
pub tree: &'a Tree, - 43
pub limits: &'a Limits, - 44
/// The WordprocessingML prefix with its colon (`w:`). - 45
pub w: &'a str, - 46
} - 47
- 48
/// The paragraph after an edit, as the postcondition checks it. - 49
pub(super) struct Views { - 50
/// Its text with the author's tracked changes accepted. - 51
pub accepted: String, - 52
/// Its text with the author's tracked changes rejected. - 53
pub rejected: String, - 54
/// The content the reader does not show, by element name, in order. - 55
pub fixed: Vec<String>, - 56
} - 57
- 58
/// What [`replace`] produced. - 59
pub(super) struct Replaced { - 60
pub summary: String, - 61
/// The part's new bytes; `None` when the paragraph already reads so. - 62
pub bytes: Option<Vec<u8>>, - 63
/// The requested text, folded as [`textdiff::fold`] compares it. - 64
pub accepted: String, - 65
pub rejected: String, - 66
pub fixed: Vec<String>, - 67
} - 68
- 69
/// Reader markers a paragraph's new text must not carry: the text is what - 70
/// the paragraph should read, and the marked content stays where it is. - 71
const MARKERS: &[&str] = &[ - 72
"[inserted by ", - 73
"[deleted by ", - 74
"[hidden: ", - 75
"[white text: ", - 76
]; - 77
- 78
/// Zero-width elements that may sit inside a stretch folded into a change. - 79
const NEUTRAL: &[&str] = &[ - 80
"bookmarkStart", - 81
"bookmarkEnd", - 82
"commentRangeStart", - 83
"commentRangeEnd", - 84
"proofErr", - 85
"permStart", - 86
"permEnd", - 87
"moveFromRangeStart", - 88
"moveFromRangeEnd", - 89
"moveToRangeStart", - 90
"moveToRangeEnd", - 91
"customXmlInsRangeStart", - 92
"customXmlInsRangeEnd", - 93
"customXmlDelRangeStart", - 94
"customXmlDelRangeEnd", - 95
"customXmlMoveFromRangeStart", - 96
"customXmlMoveFromRangeEnd", - 97
"customXmlMoveToRangeStart", - 98
"customXmlMoveToRangeEnd", - 99
"lastRenderedPageBreak", - 100
]; - 101
- 102
/// Why a stretch of visible text cannot change. - 103
#[derive(Debug, Clone, PartialEq, Eq)] - 104
enum Guard { - 105
/// A field's code or result, which Word computes. - 106
Field, - 107
/// Part of a tracked change by another author. - 108
Tracked(String), - 109
/// Text inside an element this op does not edit. - 110
Element(String), - 111
} - 112
- 113
#[derive(Debug, Clone, Copy, PartialEq, Eq)] - 114
enum Role { - 115
/// Visible and untracked. - 116
Plain, - 117
/// Deleted by the author's own earlier tracked change: part of the - 118
/// paragraph as it read before that change, so it may be restored. - 119
OwnDeleted, - 120
/// Visible and fixed; the group says why. - 121
Guarded(usize), - 122
/// Visible text of another author's plain tracked insertion (the - 123
/// wrapper). It may be struck, as a deletion nested inside their - 124
/// insertion as Word writes it, and new text beside it splits their - 125
/// insertion rather than nesting inside it. - 126
Theirs(usize), - 127
} - 128
- 129
/// Where new text goes. - 130
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - 131
enum Spot { - 132
/// Before or after an element, among its siblings. - 133
Before(usize), - 134
After(usize), - 135
/// Inside a run, before character `offset` of its content element - 136
/// `child` (`child` equal to the run's element count is its end). - 137
InRun { - 138
run: usize, - 139
child: usize, - 140
offset: usize, - 141
}, - 142
/// At the end of the paragraph's content. - 143
End, - 144
} - 145
- 146
/// One content element's text. - 147
#[derive(Debug, Clone)] - 148
struct Chunk { - 149
node: usize, - 150
run: Option<usize>, - 151
/// The element's position among its run's content elements. - 152
child: usize, - 153
text: Vec<char>, - 154
/// Index of its first character in the paragraph's text. - 155
start: usize, - 156
role: Role, - 157
/// A `w:t` or `w:delText`, which may be split; anything else is whole. - 158
splittable: bool, - 159
link: Option<usize>, - 160
order: usize, - 161
} - 162
- 163
/// Visible text that cannot change, and where text placed around it goes. - 164
#[derive(Debug, Clone)] - 165
struct Group { - 166
guard: Guard, - 167
/// `None` when the group starts or ends outside this paragraph. - 168
before: Option<Spot>, - 169
after: Option<Spot>, - 170
end_order: usize, - 171
} - 172
- 173
/// A hyperlink: a `w:hyperlink`, or a HYPERLINK field's result. - 174
#[derive(Debug, Clone)] - 175
struct Link { - 176
before: Option<Spot>, - 177
after: Option<Spot>, - 178
end_order: usize, - 179
} - 180
- 181
#[derive(Debug, Clone, Copy, PartialEq, Eq)] - 182
enum Weight { - 183
/// A footnote or endnote mark: new text after the text it follows goes - 184
/// after it, so the mark stays with its sentence. - 185
Attach, - 186
/// Bookmarks, comment ranges and the like. - 187
Neutral, - 188
Significant, - 189
} - 190
- 191
/// Zero-width content the reader does not show. - 192
#[derive(Debug, Clone)] - 193
struct Fixed { - 194
gap: usize, - 195
order: usize, - 196
weight: Weight, - 197
after: Spot, - 198
name: String, - 199
} - 200
- 201
/// The author's own earlier tracked insertion. - 202
#[derive(Debug, Clone)] - 203
struct OwnInsertion { - 204
node: usize, - 205
gap: usize, - 206
text: String, - 207
} - 208
- 209
#[derive(Debug, Default)] - 210
struct Paragraph { - 211
node: usize, - 212
chars: Vec<char>, - 213
/// The chunk each character belongs to. - 214
owner: Vec<usize>, - 215
chunks: Vec<Chunk>, - 216
chunk_of: HashMap<usize, usize>, - 217
groups: Vec<Group>, - 218
links: Vec<Link>, - 219
fixed: Vec<Fixed>, - 220
own_insertions: Vec<OwnInsertion>, - 221
/// The author's own simple deletions, each with its runs. - 222
own_deletions: HashMap<usize, Vec<usize>>, - 223
in_own_deletion: HashMap<usize, usize>, - 224
/// Other authors' plain tracked insertions, each with its runs in - 225
/// order (runs inside the author's own deletions within it included). - 226
theirs: HashMap<usize, Vec<usize>>, - 227
/// Run, or the author's own deletion, → the other author's insertion - 228
/// it sits in. - 229
in_theirs: HashMap<usize, usize>, - 230
/// Tracked changes (an existing document) or clean text (a new one). - 231
tracked: bool, - 232
/// Each run's content elements (everything but `w:rPr`), in order. - 233
run_children: HashMap<usize, Vec<usize>>, - 234
/// `w:pPr/w:rPr`: the paragraph mark's formatting. - 235
mark_properties: Option<usize>, - 236
} - 237
- 238
/// Changes the paragraph at node `paragraph` to read `text`, as tracked - 239
/// changes by `context.author` numbered from `first_id`. - 240
pub(super) fn replace( - 241
part: &Part<'_>, - 242
paragraph: usize, - 243
anchor: &str, - 244
text: &str, - 245
context: &EditContext, - 246
first_id: u64, - 247
) -> Result<Replaced, EditError> { - 248
let model = read(part, paragraph, &context.author, context.tracked)?; - 249
let wanted = text.replace("\r\n", "\n").replace('\r', "\n"); - 250
let original: String = model.chars.iter().collect(); - 251
for marker in MARKERS { - 252
if wanted.matches(marker).count() > original.matches(marker).count() { - 253
return fail(format!( - 254
"{anchor}: the text contains the reader's marker {:?}; give the paragraph's text as it should read, with no markers (tracked changes by others, hidden text and fields stay where they are)", - 255
marker.trim_end() - 256
)); - 257
} - 258
} - 259
let old = &model.chars; - 260
let lead = old.iter().take_while(|c| c.is_whitespace()).count(); - 261
let trail = old[lead..] - 262
.iter() - 263
.rev() - 264
.take_while(|c| c.is_whitespace()) - 265
.count(); - 266
let mut new: Vec<char> = old[..lead].to_vec(); - 267
new.extend(wanted.trim().chars()); - 268
new.extend_from_slice(&old[old.len() - trail..]); - 269
- 270
let significant: HashSet<usize> = model - 271
.fixed - 272
.iter() - 273
.filter(|fixed| fixed.weight != Weight::Neutral) - 274
.map(|fixed| fixed.gap) - 275
.collect(); - 276
let changes = textdiff::changes(old, &new, |range: Range<usize>| { - 277
!range.clone().any(|index| model.guarded(index).is_some()) - 278
&& !(range.start..=range.end).any(|gap| significant.contains(&gap)) - 279
}); - 280
- 281
for change in &changes { - 282
if let Some(index) = change - 283
.delete - 284
.clone() - 285
.find(|index| model.guarded(*index).is_some()) - 286
{ - 287
return Err(model.refusal(anchor, index, false)); - 288
} - 289
} - 290
let mut keep = vec![true; old.len()]; - 291
for change in &changes { - 292
for index in change.delete.clone() { - 293
keep[index] = false; - 294
} - 295
} - 296
- 297
let mut by_gap: BTreeMap<usize, Vec<&OwnInsertion>> = BTreeMap::new(); - 298
for own in &model.own_insertions { - 299
by_gap.entry(own.gap).or_default().push(own); - 300
} - 301
let mut kept_own: HashSet<usize> = HashSet::new(); - 302
let mut placed: Vec<(Spot, Option<usize>, bool, String)> = Vec::new(); - 303
for change in changes.iter().filter(|change| !change.insert.is_empty()) { - 304
let gap = change.delete.end; - 305
let insert: String = new[change.insert.clone()].iter().collect(); - 306
let existing: String = by_gap - 307
.get(&gap) - 308
.map(|owns| owns.iter().map(|own| own.text.as_str()).collect()) - 309
.unwrap_or_default(); - 310
if !existing.is_empty() && fold_text(&existing) == fold_text(&insert) { - 311
kept_own.extend(by_gap.get(&gap).into_iter().flatten().map(|own| own.node)); - 312
continue; - 313
} - 314
let (spot, run, strip_style) = model.placement(anchor, gap, change.delete.clone())?; - 315
placed.push((spot, run, strip_style, insert)); - 316
} - 317
let dropped: HashSet<usize> = model - 318
.own_insertions - 319
.iter() - 320
.map(|own| own.node) - 321
.filter(|node| !kept_own.contains(node)) - 322
.collect(); - 323
// Whether any character changes state: newly deleted, or restored from - 324
// the author's own earlier deletion. - 325
let restates = model.chunks.iter().any(|chunk| { - 326
(chunk.start..chunk.start + chunk.text.len()).any(|index| match chunk.role { - 327
Role::Plain | Role::Theirs(_) => !keep[index], - 328
Role::OwnDeleted => keep[index], - 329
Role::Guarded(_) => false, - 330
}) - 331
}); - 332
- 333
let accepted = fold_text(&new.iter().collect::<String>()); - 334
let fixed: Vec<String> = model.fixed.iter().map(|fixed| fixed.name.clone()).collect(); - 335
if placed.is_empty() && dropped.is_empty() && !restates { - 336
return Ok(Replaced { - 337
summary: format!("{anchor}: no change; it already reads that"), - 338
bytes: None, - 339
accepted, - 340
rejected: original, - 341
fixed, - 342
}); - 343
} - 344
- 345
let mut at: BTreeMap<Spot, Vec<(String, String)>> = BTreeMap::new(); - 346
for (spot, run, strip_style, insert) in placed { - 347
let properties = model.properties(part, run, strip_style)?; - 348
at.entry(model.normalize(spot)) - 349
.or_default() - 350
.push((properties, insert)); - 351
} - 352
let mut emitter = Emitter::new(part, &model, &keep, at, dropped, context, first_id); - 353
let inner = emitter.children(model.node)?; - 354
if !emitter.at.is_empty() { - 355
return fail(format!( - 356
"{anchor}: the change could not be placed in the paragraph; nothing was written" - 357
)); - 358
} - 359
let node = &part.tree.nodes[model.node]; - 360
let mut splice = Splice::default(); - 361
if node.is_empty_element() { - 362
let open = String::from_utf8_lossy(&part.bytes[node.span.start..node.span.end - 2]) - 363
.trim_end() - 364
.to_string(); - 365
let mut whole = format!("{open}>").into_bytes(); - 366
whole.extend(inner); - 367
whole.extend(format!("</{}>", node.element.name).into_bytes()); - 368
splice.replace(node.span.clone(), whole); - 369
} else { - 370
splice.replace(node.inner.clone(), inner); - 371
} - 372
let bytes = splice.apply(part.bytes, part.name)?; - 373
- 374
let summary = if changes.is_empty() { - 375
format!( - 376
"{anchor}: its earlier tracked changes are withdrawn; it reads as it did before them" - 377
) - 378
} else { - 379
let described: Vec<String> = changes - 380
.iter() - 381
.map(|change| { - 382
describe( - 383
&old[change.delete.clone()].iter().collect::<String>(), - 384
&new[change.insert.clone()].iter().collect::<String>(), - 385
) - 386
}) - 387
.collect(); - 388
let shown = described.len().min(3); - 389
let mut list = described[..shown].join(", "); - 390
if described.len() > shown { - 391
list.push_str(&format!(" and {} more", described.len() - shown)); - 392
} - 393
format!( - 394
"{anchor}: {} {}change{}, only where the text differs: {list}", - 395
described.len(), - 396
if context.tracked { - 397
"tracked " - 398
} else { - 399
"clean " - 400
}, - 401
if described.len() == 1 { "" } else { "s" } - 402
) - 403
}; - 404
Ok(Replaced { - 405
summary, - 406
bytes: Some(bytes), - 407
accepted, - 408
rejected: original, - 409
fixed, - 410
}) - 411
} - 412
- 413
/// The paragraph's text with `author`'s changes accepted and rejected, and - 414
/// the content the reader does not show. - 415
pub(super) fn views( - 416
part: &Part<'_>, - 417
paragraph: usize, - 418
author: &str, - 419
tracked: bool, - 420
) -> Result<Views, EditError> { - 421
let model = read(part, paragraph, author, tracked)?; - 422
let mut accepted = String::new(); - 423
let mut own = model.own_insertions.iter().peekable(); - 424
for (index, character) in model.chars.iter().enumerate() { - 425
while let Some(insertion) = own.next_if(|insertion| insertion.gap == index) { - 426
accepted.push_str(&insertion.text); - 427
} - 428
if model.chunks[model.owner[index]].role != Role::OwnDeleted { - 429
accepted.push(*character); - 430
} - 431
} - 432
for insertion in own { - 433
accepted.push_str(&insertion.text); - 434
} - 435
Ok(Views { - 436
accepted, - 437
rejected: model.chars.iter().collect(), - 438
fixed: model.fixed.iter().map(|fixed| fixed.name.clone()).collect(), - 439
}) - 440
} - 441
- 442
/// `"old" → "new"`, `"old" deleted` or `"new" inserted`. - 443
fn describe(old: &str, new: &str) -> String { - 444
fn excerpt(text: &str) -> String { - 445
let characters: Vec<char> = text.chars().collect(); - 446
if characters.len() <= 40 { - 447
format!("{text:?}") - 448
} else { - 449
format!( - 450
"{:?}", - 451
format!("{}…", characters[..39].iter().collect::<String>()) - 452
) - 453
} - 454
} - 455
match (old.is_empty(), new.is_empty()) { - 456
(false, false) => format!("{} → {}", excerpt(old), excerpt(new)), - 457
(false, true) => format!("{} deleted", excerpt(old)), - 458
_ => format!("{} inserted", excerpt(new)), - 459
} - 460
} - 461
- 462
/// The text of a `w:t` (or any element holding only text), decoded. - 463
pub(super) fn decode_text(inner: &[u8], part: &str, limits: &Limits) -> Result<String, EditError> { - 464
if !inner.contains(&b'&') && !inner.contains(&b'<') { - 465
return std::str::from_utf8(inner) - 466
.map(str::to_string) - 467
.map_err(|_| EditError { - 468
op: None, - 469
message: format!("part {part} is not valid UTF-8"), - 470
}); - 471
} - 472
let mut wrapped = Vec::with_capacity(inner.len() + 7); - 473
wrapped.extend_from_slice(b"<t>"); - 474
wrapped.extend_from_slice(inner); - 475
wrapped.extend_from_slice(b"</t>"); - 476
let mut text = String::new(); - 477
xml::walk(&wrapped, part, limits, |event| { - 478
if let XmlEvent::Text(piece) = event { - 479
text.push_str(&piece); - 480
} - 481
Ok(()) - 482
})?; - 483
Ok(text) - 484
} - 485
- 486
// ---- Reading the paragraph ------------------------------------------------------ - 487
- 488
#[derive(Debug, Clone, Copy, Default)] - 489
struct Context { - 490
link: Option<usize>, - 491
guard: Option<usize>, - 492
own_deletion: Option<usize>, - 493
theirs: Option<usize>, - 494
} - 495
- 496
#[derive(Debug, Clone)] - 497
struct Field { - 498
instruction: String, - 499
in_result: bool, - 500
/// The group guarding the field's code and, unless the field is a - 501
/// link, its result. Fields inside a computed field share its group. - 502
group: usize, - 503
owns_group: bool, - 504
link: Option<usize>, - 505
} - 506
- 507
struct Walker<'p, 'a> { - 508
part: &'p Part<'a>, - 509
author: &'p str, - 510
order: usize, - 511
fields: Vec<Field>, - 512
model: Paragraph, - 513
} - 514
- 515
fn read( - 516
part: &Part<'_>, - 517
paragraph: usize, - 518
author: &str, - 519
tracked: bool, - 520
) -> Result<Paragraph, EditError> { - 521
let mut walker = Walker { - 522
part, - 523
author, - 524
order: 0, - 525
fields: Vec::new(), - 526
model: Paragraph { - 527
node: paragraph, - 528
tracked, - 529
..Paragraph::default() - 530
}, - 531
}; - 532
walker.carry_fields()?; - 533
walker.model.mark_properties = part - 534
.tree - 535
.children(paragraph, "pPr") - 536
.next() - 537
.and_then(|properties| part.tree.children(properties, "rPr").next()); - 538
walker.container(paragraph, Context::default())?; - 539
Ok(walker.model) - 540
} - 541
- 542
fn is_on(element: &crate::xml::Element) -> bool { - 543
!matches!(element.attr("val"), Some("0" | "false" | "off")) - 544
} - 545
- 546
/// Whether a run is hidden or white, from its own `w:rPr`, as the reader - 547
/// decides it. - 548
fn run_flags(tree: &Tree, run: usize) -> (bool, bool) { - 549
let Some(properties) = tree.children(run, "rPr").next() else { - 550
return (false, false); - 551
}; - 552
let mut hidden = false; - 553
let mut white = false; - 554
for &child in &tree.nodes[properties].children { - 555
let node = &tree.nodes[child]; - 556
match node.local() { - 557
"vanish" | "specVanish" if is_on(&node.element) => hidden = true, - 558
"color" => { - 559
white = node - 560
.element - 561
.attr("val") - 562
.is_some_and(|value| value.eq_ignore_ascii_case("FFFFFF")); - 563
} - 564
_ => {} - 565
} - 566
} - 567
(hidden, white) - 568
} - 569
- 570
fn is_link_instruction(instruction: &str) -> bool { - 571
instruction - 572
.split_whitespace() - 573
.next() - 574
.is_some_and(|word| word.eq_ignore_ascii_case("HYPERLINK")) - 575
} - 576
- 577
impl Walker<'_, '_> { - 578
fn next_order(&mut self) -> usize { - 579
self.order += 1; - 580
self.order - 581
} - 582
- 583
fn decode(&self, node: usize) -> Result<String, EditError> { - 584
let inner = self.part.tree.nodes[node].inner.clone(); - 585
decode_text(&self.part.bytes[inner], self.part.name, self.part.limits) - 586
} - 587
- 588
/// Fields a paragraph starts inside (a table of contents, an index): - 589
/// their begin is in an earlier paragraph. - 590
fn carry_fields(&mut self) -> Result<(), EditError> { - 591
let tree = self.part.tree; - 592
let mut open: Vec<(String, bool)> = Vec::new(); - 593
for index in 0..self.model.node { - 594
let node = &tree.nodes[index]; - 595
if node.skipped { - 596
continue; - 597
} - 598
match node.local() { - 599
"fldChar" => match node.element.attr("fldCharType") { - 600
Some("begin") => open.push((String::new(), false)), - 601
Some("separate") => { - 602
if let Some(top) = open.last_mut() { - 603
top.1 = true; - 604
} - 605
} - 606
Some("end") => { - 607
open.pop(); - 608
} - 609
_ => {} - 610
}, - 611
"instrText" => { - 612
let text = self.decode(index)?; - 613
if let Some((instruction, false)) = open.last_mut() { - 614
instruction.push_str(&text); - 615
instruction.push(' '); - 616
} - 617
} - 618
_ => {} - 619
} - 620
} - 621
for (instruction, in_result) in open { - 622
self.open_field(instruction, None); - 623
if in_result { - 624
self.enter_result(); - 625
} - 626
} - 627
Ok(()) - 628
} - 629
- 630
fn open_field(&mut self, instruction: String, before: Option<Spot>) { - 631
let shared = self - 632
.fields - 633
.iter() - 634
.find(|field| field.link.is_none()) - 635
.map(|field| field.group); - 636
let (group, owns_group) = match shared { - 637
Some(group) => (group, false), - 638
None => (self.group(Guard::Field, before, None), true), - 639
}; - 640
self.fields.push(Field { - 641
instruction, - 642
in_result: false, - 643
group, - 644
owns_group, - 645
link: None, - 646
}); - 647
} - 648
- 649
fn enter_result(&mut self) { - 650
let (group, is_link) = match self.fields.last() { - 651
Some(field) if !field.in_result => ( - 652
field.group, - 653
field.owns_group && is_link_instruction(&field.instruction), - 654
), - 655
_ => return, - 656
}; - 657
let link = if is_link { - 658
let before = self.model.groups[group].before; - 659
self.model.links.push(Link { - 660
before, - 661
after: None, - 662
end_order: usize::MAX, - 663
}); - 664
Some(self.model.links.len() - 1) - 665
} else { - 666
None - 667
}; - 668
if let Some(field) = self.fields.last_mut() { - 669
field.in_result = true; - 670
field.link = link; - 671
} - 672
} - 673
- 674
fn close_field(&mut self, after: Spot, order: usize) { - 675
let Some(field) = self.fields.pop() else { - 676
return; - 677
}; - 678
if field.owns_group { - 679
let group = &mut self.model.groups[field.group]; - 680
group.after = Some(after); - 681
group.end_order = order; - 682
} - 683
if let Some(link) = field.link { - 684
let link = &mut self.model.links[link]; - 685
link.after = Some(after); - 686
link.end_order = order; - 687
} - 688
} - 689
- 690
fn group(&mut self, guard: Guard, before: Option<Spot>, after: Option<Spot>) -> usize { - 691
self.model.groups.push(Group { - 692
guard, - 693
before, - 694
after, - 695
end_order: usize::MAX, - 696
}); - 697
self.model.groups.len() - 1 - 698
} - 699
- 700
/// The guard that already covers content here: an enclosing guarded - 701
/// element, or a computed field's code or result. - 702
fn enclosing_guard(&self, context: Context) -> Option<usize> { - 703
context.guard.or_else(|| { - 704
self.fields - 705
.last() - 706
.filter(|field| !(field.in_result && field.link.is_some())) - 707
.map(|field| field.group) - 708
}) - 709
} - 710
- 711
fn link_here(&self, context: Context) -> Option<usize> { - 712
self.fields - 713
.last() - 714
.filter(|field| field.in_result) - 715
.and_then(|field| field.link) - 716
.or(context.link) - 717
} - 718
- 719
fn fixed(&mut self, node: usize, weight: Weight, after: Spot) -> usize { - 720
let order = self.next_order(); - 721
let name = self.part.tree.nodes[node].local().to_string(); - 722
self.model.fixed.push(Fixed { - 723
gap: self.model.chars.len(), - 724
order, - 725
weight, - 726
after, - 727
name, - 728
}); - 729
order - 730
} - 731
- 732
#[allow(clippy::too_many_arguments)] - 733
fn chunk( - 734
&mut self, - 735
node: usize, - 736
run: Option<usize>, - 737
child: usize, - 738
text: &str, - 739
role: Role, - 740
splittable: bool, - 741
link: Option<usize>, - 742
) -> usize { - 743
let start = self.model.chars.len(); - 744
let index = self.model.chunks.len(); - 745
let characters: Vec<char> = text.chars().collect(); - 746
self.model - 747
.owner - 748
.extend(std::iter::repeat_n(index, characters.len())); - 749
self.model.chars.extend(characters.iter().copied()); - 750
self.model.chunk_of.insert(node, index); - 751
let order = self.next_order(); - 752
self.model.chunks.push(Chunk { - 753
node, - 754
run, - 755
child, - 756
text: characters, - 757
start, - 758
role, - 759
splittable, - 760
link, - 761
order, - 762
}); - 763
order - 764
} - 765
- 766
fn container(&mut self, node: usize, context: Context) -> Result<(), EditError> { - 767
let tree = self.part.tree; - 768
for &child in &tree.nodes[node].children { - 769
let child_node = &tree.nodes[child]; - 770
if child_node.skipped || child_node.local().ends_with("Pr") { - 771
continue; - 772
} - 773
match child_node.local() { - 774
"r" => self.run(child, context)?, - 775
"hyperlink" => { - 776
self.model.links.push(Link { - 777
before: Some(Spot::Before(child)), - 778
after: Some(Spot::After(child)), - 779
end_order: usize::MAX, - 780
}); - 781
let link = self.model.links.len() - 1; - 782
self.container( - 783
child, - 784
Context { - 785
link: Some(link), - 786
..context - 787
}, - 788
)?; - 789
self.model.links[link].end_order = self.next_order(); - 790
} - 791
"smartTag" | "customXml" | "sdt" | "sdtContent" | "dir" | "bdo" => { - 792
self.container(child, context)?; - 793
} - 794
"fldSimple" => self.simple_field(child, context)?, - 795
"ins" | "del" | "moveTo" | "moveFrom" => self.revision(child, context)?, - 796
_ => self.element(child, context, None)?, - 797
} - 798
} - 799
Ok(()) - 800
} - 801
- 802
fn simple_field(&mut self, node: usize, context: Context) -> Result<(), EditError> { - 803
let instruction = self.part.tree.nodes[node] - 804
.element - 805
.attr("instr") - 806
.unwrap_or_default(); - 807
if let Some(group) = self.enclosing_guard(context) { - 808
return self.container( - 809
node, - 810
Context { - 811
guard: Some(group), - 812
..context - 813
}, - 814
); - 815
} - 816
if is_link_instruction(instruction) { - 817
self.model.links.push(Link { - 818
before: Some(Spot::Before(node)), - 819
after: Some(Spot::After(node)), - 820
end_order: usize::MAX, - 821
}); - 822
let link = self.model.links.len() - 1; - 823
self.container( - 824
node, - 825
Context { - 826
link: Some(link), - 827
..context - 828
}, - 829
)?; - 830
self.model.links[link].end_order = self.next_order(); - 831
return Ok(()); - 832
} - 833
let group = self.group( - 834
Guard::Field, - 835
Some(Spot::Before(node)), - 836
Some(Spot::After(node)), - 837
); - 838
self.container( - 839
node, - 840
Context { - 841
guard: Some(group), - 842
..context - 843
}, - 844
)?; - 845
self.model.groups[group].end_order = self.next_order(); - 846
Ok(()) - 847
} - 848
- 849
/// A tracked change the author owns can be revised only when it is - 850
/// plain: runs of text and nothing else. - 851
fn simple(&self, wrapper: usize) -> bool { - 852
let tree = self.part.tree; - 853
tree.nodes[wrapper].children.iter().all(|&child| { - 854
let node = &tree.nodes[child]; - 855
if node.skipped || node.local() != "r" { - 856
return false; - 857
} - 858
let (hidden, white) = run_flags(tree, child); - 859
!hidden - 860
&& !white - 861
&& node.children.iter().all(|&content| { - 862
let content = &tree.nodes[content]; - 863
!content.skipped - 864
&& matches!( - 865
content.local(), - 866
"rPr" | "t" | "delText" | "tab" | "br" | "cr" | "noBreakHyphen" - 867
) - 868
}) - 869
}) - 870
} - 871
- 872
fn simple_text(&self, wrapper: usize) -> Result<String, EditError> { - 873
let tree = self.part.tree; - 874
let mut text = String::new(); - 875
for &run in &tree.nodes[wrapper].children { - 876
for &content in &tree.nodes[run].children { - 877
match tree.nodes[content].local() { - 878
"t" | "delText" => text.push_str(&self.decode(content)?), - 879
"tab" => text.push('\t'), - 880
"br" | "cr" => text.push(' '), - 881
"noBreakHyphen" => text.push('-'), - 882
_ => {} - 883
} - 884
} - 885
} - 886
Ok(text) - 887
} - 888
- 889
/// Another author's insertion can be struck or split when it is plain: - 890
/// runs of text, and the author's own plain deletions of them. - 891
fn simple_theirs(&self, wrapper: usize) -> bool { - 892
let tree = self.part.tree; - 893
tree.nodes[wrapper].children.iter().all(|&child| { - 894
let node = &tree.nodes[child]; - 895
if node.skipped { - 896
return false; - 897
} - 898
match node.local() { - 899
"r" => { - 900
let (hidden, white) = run_flags(tree, child); - 901
!hidden - 902
&& !white - 903
&& node.children.iter().all(|&content| { - 904
let content = &tree.nodes[content]; - 905
!content.skipped - 906
&& matches!( - 907
content.local(), - 908
"rPr" | "t" | "tab" | "br" | "cr" | "noBreakHyphen" - 909
) - 910
}) - 911
} - 912
"del" => node.element.attr("author") == Some(self.author) && self.simple(child), - 913
_ => false, - 914
} - 915
}) - 916
} - 917
- 918
fn revision(&mut self, node: usize, context: Context) -> Result<(), EditError> { - 919
let tree = self.part.tree; - 920
let element = &tree.nodes[node].element; - 921
let local = element.local(); - 922
let author = element.attr("author").unwrap_or("unknown").to_string(); - 923
let guard = self.enclosing_guard(context); - 924
let tracked = self.model.tracked; - 925
let own = tracked - 926
&& matches!(local, "ins" | "del") - 927
&& author == self.author - 928
&& guard.is_none() - 929
&& context.own_deletion.is_none() - 930
&& !(local == "ins" && context.theirs.is_some()) - 931
&& self.simple(node); - 932
match (local, own) { - 933
("ins", true) => { - 934
let text = self.simple_text(node)?; - 935
self.next_order(); - 936
self.model.own_insertions.push(OwnInsertion { - 937
node, - 938
gap: self.model.chars.len(), - 939
text, - 940
}); - 941
} - 942
("del", true) => { - 943
let runs: Vec<usize> = tree.nodes[node].children.clone(); - 944
for &run in &runs { - 945
self.model.in_own_deletion.insert(run, node); - 946
if let Some(theirs) = context.theirs { - 947
self.model.in_theirs.insert(run, theirs); - 948
} - 949
} - 950
if let Some(theirs) = context.theirs { - 951
self.model.in_theirs.insert(node, theirs); - 952
} - 953
self.model.own_deletions.insert(node, runs.clone()); - 954
for run in runs { - 955
self.run( - 956
run, - 957
Context { - 958
own_deletion: Some(node), - 959
..context - 960
}, - 961
)?; - 962
} - 963
} - 964
("ins", false) - 965
if tracked - 966
&& guard.is_none() - 967
&& context.own_deletion.is_none() - 968
&& context.theirs.is_none() - 969
&& self.simple_theirs(node) => - 970
{ - 971
let mut runs = Vec::new(); - 972
for &child in &tree.nodes[node].children { - 973
match tree.nodes[child].local() { - 974
"r" => runs.push(child), - 975
_ => runs.extend(tree.nodes[child].children.iter().copied()), - 976
} - 977
} - 978
for &run in &runs { - 979
self.model.in_theirs.insert(run, node); - 980
} - 981
self.model.theirs.insert(node, runs); - 982
let inside = Context { - 983
theirs: Some(node), - 984
..context - 985
}; - 986
for &child in &tree.nodes[node].children { - 987
match tree.nodes[child].local() { - 988
"r" => self.run(child, inside)?, - 989
_ => self.revision(child, inside)?, - 990
} - 991
} - 992
} - 993
("ins" | "moveTo", false) => { - 994
let group = match guard { - 995
Some(group) => group, - 996
None => self.group( - 997
Guard::Tracked(author), - 998
Some(Spot::Before(node)), - 999
Some(Spot::After(node)), - 1000
),
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.