- 1
//! L2 read projections with anchors and O6 labels. - 2
//! - 3
//! A projection is data for a reader, never instructions: hidden runs, - 4
//! tracked deletions, comments, hidden slides and sheets, off-slide shapes - 5
//! and speaker notes are all kept, and each is labelled for what it is so a - 6
//! prompt injection hidden in a document is visible as hidden content. - 7
- 8
use std::collections::HashMap; - 9
use std::io::{Read, Seek}; - 10
use std::ops::Range; - 11
- 12
use serde::Serialize; - 13
- 14
use crate::package::{Inspection, Package, Vocabulary}; - 15
use crate::xml::{self, Element, XmlEvent}; - 16
use crate::{Error, Limits}; - 17
- 18
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] - 19
#[serde(rename_all = "snake_case")] - 20
pub enum UnitKind { - 21
Heading, - 22
Paragraph, - 23
TableRow, - 24
Comment, - 25
SheetRow, - 26
DefinedName, - 27
Slide, - 28
Shape, - 29
Notes, - 30
Page, - 31
} - 32
- 33
/// One addressable piece of a document. `anchor` is what an op or a - 34
/// citation names (docs/design/72, "Anchors"). - 35
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 36
pub struct Unit { - 37
pub anchor: String, - 38
pub kind: UnitKind, - 39
/// Heading level; 0 for everything that is not a heading. - 40
pub level: u8, - 41
pub text: String, - 42
pub labels: Vec<String>, - 43
/// For a sheet row, its cells as (address, shown value); empty - 44
/// otherwise. - 45
#[serde(skip_serializing_if = "Vec::is_empty")] - 46
pub cells: Vec<(String, String)>, - 47
/// For a Word table row, each cell's paragraphs as (anchor, text), so a - 48
/// cell can be named and changed like any paragraph; empty otherwise. - 49
#[serde(skip_serializing_if = "Vec::is_empty")] - 50
pub row_cells: Vec<Vec<(String, String)>>, - 51
} - 52
- 53
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 54
pub struct Section { - 55
pub anchor: String, - 56
pub title: String, - 57
pub level: u8, - 58
pub units: Range<usize>, - 59
} - 60
- 61
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 62
pub struct Table { - 63
pub anchor: String, - 64
pub title: String, - 65
/// First row is the header row. - 66
pub rows: Vec<Vec<String>>, - 67
pub labels: Vec<String>, - 68
/// Used columns beyond [`MAX_TABLE_COLUMNS`] that the grid leaves out; - 69
/// the anchored lines still carry every cell. - 70
pub omitted_columns: usize, - 71
} - 72
- 73
/// Widest grid a table projection builds. A sheet can use 16,384 columns, - 74
/// and a dense rows-by-columns grid of that width is a memory bomb. - 75
pub const MAX_TABLE_COLUMNS: usize = 64; - 76
- 77
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 78
pub struct Document { - 79
pub inspection: Inspection, - 80
pub title: Option<String>, - 81
pub stats: Vec<(String, usize)>, - 82
pub units: Vec<Unit>, - 83
pub sections: Vec<Section>, - 84
pub tables: Vec<Table>, - 85
/// Parts of the vocabulary this reader does not project yet. Stated so - 86
/// a reader never mistakes an omission for absence. - 87
pub not_read: Vec<&'static str>, - 88
} - 89
- 90
/// Opens and projects one package. - 91
pub fn read<R: Read + Seek>(reader: R, limits: Limits) -> Result<Document, Error> { - 92
let mut package = Package::open(reader, limits)?; - 93
project(&mut package) - 94
} - 95
- 96
pub fn project<R: Read + Seek>(package: &mut Package<R>) -> Result<Document, Error> { - 97
package.check_main_root()?; - 98
let inspection = package.inspect()?; - 99
let title = core_title(package)?; - 100
let mut document = Document { - 101
inspection, - 102
title, - 103
stats: Vec::new(), - 104
units: Vec::new(), - 105
sections: Vec::new(), - 106
tables: Vec::new(), - 107
not_read: Vec::new(), - 108
}; - 109
match package.format().vocabulary { - 110
Vocabulary::Word => word(package, &mut document)?, - 111
Vocabulary::Excel => excel(package, &mut document)?, - 112
Vocabulary::PowerPoint => powerpoint(package, &mut document)?, - 113
Vocabulary::Visio => visio(package, &mut document)?, - 114
} - 115
Ok(document) - 116
} - 117
- 118
/// Longest unit text one line carries. A longer unit continues on further - 119
/// lines that name the same anchor and their part, so no text is dropped - 120
/// and no single line can outgrow a page. - 121
pub const MAX_LINE_CHARS: usize = 16_000; - 122
- 123
impl Document { - 124
/// The document as anchored lines: `[anchor] text ⟨labels⟩`. - 125
pub fn lines(&self) -> Vec<String> { - 126
self.units.iter().flat_map(render_unit).collect() - 127
} - 128
- 129
/// Anchored lines for a range of units. - 130
pub fn lines_of(&self, units: Range<usize>) -> Vec<String> { - 131
self.units[units].iter().flat_map(render_unit).collect() - 132
} - 133
- 134
/// A section by anchor, exact title, or title fragment, in that order. - 135
pub fn section(&self, wanted: &str) -> Option<&Section> { - 136
let wanted = wanted.trim().trim_start_matches('#').trim().to_lowercase(); - 137
self.sections - 138
.iter() - 139
.find(|section| section.anchor.to_lowercase() == wanted) - 140
.or_else(|| { - 141
self.sections - 142
.iter() - 143
.find(|section| section.title.trim().to_lowercase() == wanted) - 144
}) - 145
.or_else(|| { - 146
self.sections - 147
.iter() - 148
.find(|section| section.title.to_lowercase().contains(&wanted)) - 149
}) - 150
} - 151
- 152
/// A table by anchor, exact title or title fragment; the first table - 153
/// when nothing is named. - 154
pub fn table(&self, wanted: Option<&str>) -> Option<&Table> { - 155
match wanted.map(|value| value.trim().to_lowercase()) { - 156
None => self.tables.first(), - 157
Some(wanted) => self - 158
.tables - 159
.iter() - 160
.find(|table| { - 161
table.anchor.to_lowercase() == wanted || table.title.to_lowercase() == wanted - 162
}) - 163
.or_else(|| { - 164
self.tables - 165
.iter() - 166
.find(|table| table.title.to_lowercase().contains(&wanted)) - 167
}), - 168
} - 169
} - 170
- 171
pub fn outline(&self) -> Vec<String> { - 172
self.sections - 173
.iter() - 174
.map(|section| { - 175
let indent = " ".repeat(usize::from(section.level.saturating_sub(1))); - 176
format!("{indent}- [{}] {}", section.anchor, section.title) - 177
}) - 178
.collect() - 179
} - 180
} - 181
- 182
/// Separates the paragraphs of one shape's text in a slide unit, so a - 183
/// bullet boundary is never confused with a slash in the text itself. - 184
pub const PARAGRAPH_BREAK: &str = " ¶ "; - 185
- 186
/// A unit's text as a line shows it: a Word table row names each cell's - 187
/// paragraphs by anchor, so a cell can be cited and changed. - 188
fn display_text(unit: &Unit) -> String { - 189
if unit.row_cells.is_empty() { - 190
return unit.text.clone(); - 191
} - 192
unit.row_cells - 193
.iter() - 194
.map(|paragraphs| { - 195
paragraphs - 196
.iter() - 197
.map(|(anchor, text)| { - 198
if text.is_empty() { - 199
format!("[{anchor}]") - 200
} else { - 201
format!("[{anchor}] {text}") - 202
} - 203
}) - 204
.collect::<Vec<_>>() - 205
.join(" ") - 206
}) - 207
.collect::<Vec<_>>() - 208
.join(" | ") - 209
} - 210
- 211
fn render_unit(unit: &Unit) -> Vec<String> { - 212
let text = display_text(unit); - 213
let characters: Vec<char> = text.chars().collect(); - 214
let chunks: Vec<String> = if characters.len() <= MAX_LINE_CHARS { - 215
vec![text] - 216
} else { - 217
characters - 218
.chunks(MAX_LINE_CHARS) - 219
.map(|chunk| chunk.iter().collect()) - 220
.collect() - 221
}; - 222
let parts = chunks.len(); - 223
chunks - 224
.into_iter() - 225
.enumerate() - 226
.map(|(index, text)| { - 227
let mut line = if parts == 1 { - 228
format!("[{}] ", unit.anchor) - 229
} else { - 230
format!("[{} ⟨part {} of {parts}⟩] ", unit.anchor, index + 1) - 231
}; - 232
if unit.kind == UnitKind::Heading && index == 0 { - 233
line.push_str(&"#".repeat(usize::from(unit.level.clamp(1, 6)))); - 234
line.push(' '); - 235
} - 236
line.push_str(&text); - 237
if !unit.labels.is_empty() { - 238
line.push_str(" ⟨"); - 239
line.push_str(&unit.labels.join("; ")); - 240
line.push('⟩'); - 241
} - 242
line - 243
}) - 244
.collect() - 245
} - 246
- 247
fn core_title<R: Read + Seek>(package: &mut Package<R>) -> Result<Option<String>, Error> { - 248
let Some(part) = package - 249
.related_part("", "core-properties")? - 250
.filter(|part| package.has_part(part)) - 251
else { - 252
return Ok(None); - 253
}; - 254
let bytes = package.read_part(&part)?; - 255
let mut title = String::new(); - 256
let mut inside = false; - 257
xml::walk(&bytes, &part, package.limits(), |event| { - 258
match event { - 259
XmlEvent::Open(element) if element.local() == "title" => inside = true, - 260
XmlEvent::Close(name) if xml::local_name(&name) == "title" => inside = false, - 261
XmlEvent::Text(text) if inside => title.push_str(&text), - 262
_ => {} - 263
} - 264
Ok(()) - 265
})?; - 266
let title = title.trim().to_string(); - 267
Ok((!title.is_empty()).then_some(title)) - 268
} - 269
- 270
fn is_on(element: &Element) -> bool { - 271
!matches!(element.attr("val"), Some("0" | "false" | "off")) - 272
} - 273
- 274
fn words(text: &str) -> usize { - 275
text.split_whitespace().count() - 276
} - 277
- 278
// ---- Word ------------------------------------------------------------------ - 279
- 280
#[derive(Default)] - 281
struct Paragraph { - 282
anchor: String, - 283
style: Option<String>, - 284
outline: Option<u8>, - 285
text: String, - 286
/// The text a person reading the document sees: no reader markers, no - 287
/// deleted or hidden text. Words are counted from it. - 288
visible: String, - 289
labels: Vec<String>, - 290
comments: Vec<String>, - 291
in_table: bool, - 292
text_box: bool, - 293
} - 294
- 295
#[derive(Default)] - 296
struct Run { - 297
hidden: bool, - 298
white: bool, - 299
} - 300
- 301
#[derive(Default)] - 302
struct WordTotals { - 303
hidden_runs: usize, - 304
tracked: usize, - 305
} - 306
- 307
fn word<R: Read + Seek>(package: &mut Package<R>, document: &mut Document) -> Result<(), Error> { - 308
let main = package.main_part().to_string(); - 309
let styles = match package.related_part(&main, "styles")? { - 310
Some(part) => { - 311
let bytes = package.read_part(&part)?; - 312
heading_styles(&bytes, &part, package.limits())? - 313
} - 314
None => HashMap::new(), - 315
}; - 316
let comments = match package.related_part(&main, "comments")? { - 317
Some(part) => { - 318
let bytes = package.read_part(&part)?; - 319
word_comments(&bytes, &part, package.limits())? - 320
} - 321
None => HashMap::new(), - 322
}; - 323
let bytes = package.read_part(&main)?; - 324
let limits = *package.limits(); - 325
- 326
let mut stack: Vec<Paragraph> = Vec::new(); - 327
let mut run = Run::default(); - 328
let mut in_run = false; - 329
let mut in_run_properties = false; - 330
let mut revision: Vec<(bool, String)> = Vec::new(); - 331
let mut in_text = false; - 332
let mut in_instruction = false; - 333
// One entry per open complex field: its instruction text and whether - 334
// it has been evaluated (at `separate`, or at `end` without one). - 335
let mut fields: Vec<(String, bool)> = Vec::new(); - 336
// Inside `w:rPrChange` and friends: formatting that was, not that is. - 337
let mut change_depth = 0usize; - 338
let mut table_depth = 0usize; - 339
let mut table_rows: Vec<Vec<String>> = Vec::new(); - 340
let mut row: Vec<String> = Vec::new(); - 341
let mut cell = String::new(); - 342
// The same rows, each cell as its paragraphs with their anchors. - 343
let mut table_row_cells: Vec<Vec<Vec<(String, String)>>> = Vec::new(); - 344
let mut row_cells: Vec<Vec<(String, String)>> = Vec::new(); - 345
let mut cell_paragraphs: Vec<(String, String)> = Vec::new(); - 346
let mut visible_words = 0usize; - 347
let mut paragraph_ordinal = 0usize; - 348
let mut table_ordinal = 0usize; - 349
let mut units: Vec<Unit> = Vec::new(); - 350
let mut tables: Vec<Table> = Vec::new(); - 351
let mut totals = WordTotals::default(); - 352
let mut risky_fields = 0usize; - 353
let mut text_box_depth = 0usize; - 354
- 355
xml::walk(&bytes, &main, &limits, |event| { - 356
if let XmlEvent::Open(element) = &event - 357
&& is_property_change(element.local()) - 358
{ - 359
change_depth += 1; - 360
return Ok(()); - 361
} - 362
if let XmlEvent::Close(name) = &event - 363
&& is_property_change(xml::local_name(name)) - 364
{ - 365
change_depth = change_depth.saturating_sub(1); - 366
return Ok(()); - 367
} - 368
if change_depth > 0 { - 369
return Ok(()); - 370
} - 371
match event { - 372
XmlEvent::Open(element) => match element.local() { - 373
"p" => { - 374
let nested = !stack.is_empty(); - 375
let anchor = match element.attr("paraId") { - 376
Some(id) => format!("p:{id}"), - 377
None => { - 378
paragraph_ordinal += 1; - 379
format!("p@{paragraph_ordinal}") - 380
} - 381
}; - 382
stack.push(Paragraph { - 383
anchor, - 384
in_table: table_depth > 0, - 385
text_box: nested || text_box_depth > 0, - 386
..Paragraph::default() - 387
}); - 388
} - 389
"txbxContent" => text_box_depth += 1, - 390
"pStyle" => { - 391
if let (Some(paragraph), Some(value)) = (stack.last_mut(), element.attr("val")) - 392
{ - 393
paragraph.style = Some(value.to_string()); - 394
} - 395
} - 396
"outlineLvl" => { - 397
if let (Some(paragraph), Some(value)) = (stack.last_mut(), element.attr("val")) - 398
{ - 399
paragraph.outline = value.parse::<u8>().ok().map(|level| level + 1); - 400
} - 401
} - 402
"r" => { - 403
run = Run::default(); - 404
in_run = true; - 405
} - 406
"rPr" => in_run_properties = true, - 407
"vanish" | "specVanish" if in_run_properties && is_on(&element) => { - 408
run.hidden = true - 409
} - 410
"color" if in_run_properties => { - 411
run.white = element - 412
.attr("val") - 413
.is_some_and(|value| value.eq_ignore_ascii_case("FFFFFF")); - 414
} - 415
"ins" | "moveTo" => { - 416
totals.tracked += 1; - 417
revision.push((true, element.attr("author").unwrap_or("unknown").into())); - 418
} - 419
"del" | "moveFrom" => { - 420
totals.tracked += 1; - 421
revision.push((false, element.attr("author").unwrap_or("unknown").into())); - 422
} - 423
"t" | "delText" => in_text = true, - 424
"instrText" => in_instruction = true, - 425
"fldSimple" => { - 426
if let Some(field) = element.attr("instr").and_then(risky_field) { - 427
flag_field(&mut stack, field, &mut risky_fields); - 428
} - 429
} - 430
"fldChar" => match element.attr("fldCharType") { - 431
Some("begin") => fields.push((String::new(), false)), - 432
Some("separate") => { - 433
if let Some((instruction, evaluated)) = fields.last_mut() - 434
&& !*evaluated - 435
{ - 436
*evaluated = true; - 437
if let Some(field) = risky_field(instruction) { - 438
flag_field(&mut stack, field, &mut risky_fields); - 439
} - 440
} - 441
} - 442
Some("end") => { - 443
if let Some((instruction, evaluated)) = fields.pop() - 444
&& !evaluated - 445
&& let Some(field) = risky_field(&instruction) - 446
{ - 447
flag_field(&mut stack, field, &mut risky_fields); - 448
} - 449
} - 450
_ => {} - 451
}, - 452
"tab" if in_run => push_word_text(&mut stack, &run, &revision, "\t", &mut totals), - 453
"br" | "cr" if in_run => { - 454
push_word_text(&mut stack, &run, &revision, " ", &mut totals) - 455
} - 456
"noBreakHyphen" if in_run => { - 457
push_word_text(&mut stack, &run, &revision, "-", &mut totals) - 458
} - 459
"commentReference" => { - 460
if let (Some(paragraph), Some(id)) = (stack.last_mut(), element.attr("id")) { - 461
paragraph.comments.push(id.to_string()); - 462
} - 463
} - 464
"tbl" => { - 465
table_depth += 1; - 466
if table_depth == 1 { - 467
table_rows.clear(); - 468
table_row_cells.clear(); - 469
} - 470
} - 471
"tr" if table_depth == 1 => { - 472
row.clear(); - 473
row_cells.clear(); - 474
} - 475
"tc" if table_depth == 1 => { - 476
cell.clear(); - 477
cell_paragraphs.clear(); - 478
} - 479
_ => {} - 480
}, - 481
XmlEvent::Text(text) => { - 482
if in_text { - 483
push_word_text(&mut stack, &run, &revision, &text, &mut totals); - 484
} else if in_instruction && let Some((instruction, false)) = fields.last_mut() { - 485
instruction.push_str(&text); - 486
} - 487
} - 488
XmlEvent::Close(name) => match xml::local_name(&name) { - 489
"t" | "delText" => in_text = false, - 490
"instrText" => { - 491
in_instruction = false; - 492
if let Some((instruction, false)) = fields.last_mut() { - 493
instruction.push(' '); - 494
} - 495
} - 496
"rPr" => in_run_properties = false, - 497
"r" => in_run = false, - 498
"ins" | "del" | "moveTo" | "moveFrom" => { - 499
revision.pop(); - 500
} - 501
"txbxContent" => text_box_depth = text_box_depth.saturating_sub(1), - 502
"p" => { - 503
let Some(paragraph) = stack.pop() else { - 504
return Ok(()); - 505
}; - 506
let text = paragraph.text.trim().to_string(); - 507
visible_words += words(¶graph.visible); - 508
let level = heading_level(¶graph, &styles); - 509
let in_cell = paragraph.in_table && !paragraph.text_box; - 510
if in_cell { - 511
if !cell.is_empty() && !text.is_empty() { - 512
cell.push(' '); - 513
} - 514
cell.push_str(&text); - 515
cell_paragraphs.push((paragraph.anchor.clone(), text)); - 516
} else if !text.is_empty() || !paragraph.labels.is_empty() { - 517
let mut labels = paragraph.labels.clone(); - 518
if paragraph.text_box { - 519
labels.push("text box".into()); - 520
} - 521
units.push(Unit { - 522
anchor: paragraph.anchor.clone(), - 523
kind: if level > 0 { - 524
UnitKind::Heading - 525
} else { - 526
UnitKind::Paragraph - 527
}, - 528
level, - 529
text, - 530
labels, - 531
cells: Vec::new(), - 532
row_cells: Vec::new(), - 533
}); - 534
} - 535
attach_comments(&mut units, ¶graph, &comments); - 536
} - 537
"tc" if table_depth == 1 => { - 538
row.push(std::mem::take(&mut cell)); - 539
row_cells.push(std::mem::take(&mut cell_paragraphs)); - 540
} - 541
"tr" if table_depth == 1 => { - 542
table_rows.push(std::mem::take(&mut row)); - 543
table_row_cells.push(std::mem::take(&mut row_cells)); - 544
} - 545
"tbl" => { - 546
table_depth = table_depth.saturating_sub(1); - 547
if table_depth == 0 { - 548
table_ordinal += 1; - 549
let anchor = format!("tbl@{table_ordinal}"); - 550
for (index, cells) in table_rows.iter().enumerate() { - 551
units.push(Unit { - 552
anchor: format!("{anchor}/r{}", index + 1), - 553
kind: UnitKind::TableRow, - 554
level: 0, - 555
text: cells.join(" | "), - 556
labels: Vec::new(), - 557
cells: Vec::new(), - 558
row_cells: table_row_cells.get(index).cloned().unwrap_or_default(), - 559
}); - 560
} - 561
tables.push(Table { - 562
anchor, - 563
title: format!("Table {table_ordinal}"), - 564
rows: std::mem::take(&mut table_rows), - 565
labels: Vec::new(), - 566
omitted_columns: 0, - 567
}); - 568
} - 569
} - 570
_ => {} - 571
}, - 572
} - 573
Ok(()) - 574
})?; - 575
- 576
let mut sections = Vec::new(); - 577
for (index, unit) in units.iter().enumerate() { - 578
if unit.kind != UnitKind::Heading { - 579
continue; - 580
} - 581
let end = units[index + 1..] - 582
.iter() - 583
.position(|next| next.kind == UnitKind::Heading && next.level <= unit.level) - 584
.map(|offset| index + 1 + offset) - 585
.unwrap_or(units.len()); - 586
sections.push(Section { - 587
anchor: unit.anchor.clone(), - 588
title: unit.text.clone(), - 589
level: unit.level, - 590
units: index..end, - 591
}); - 592
} - 593
let paragraphs = units - 594
.iter() - 595
.filter(|unit| matches!(unit.kind, UnitKind::Paragraph | UnitKind::Heading)) - 596
.count(); - 597
document.stats = vec![ - 598
("paragraphs".into(), paragraphs), - 599
("words".into(), visible_words), - 600
("headings".into(), sections.len()), - 601
("tables".into(), tables.len()), - 602
("comments".into(), comments.len()), - 603
("tracked changes".into(), totals.tracked), - 604
("hidden runs".into(), totals.hidden_runs), - 605
("risky fields".into(), risky_fields), - 606
]; - 607
document.units = units; - 608
document.sections = sections; - 609
document.tables = tables; - 610
document.not_read = vec![ - 611
"headers and footers", - 612
"footnotes and endnotes", - 613
"images and charts", - 614
"heading levels inherited through style basedOn chains", - 615
]; - 616
Ok(()) - 617
} - 618
- 619
fn push_word_text( - 620
stack: &mut [Paragraph], - 621
run: &Run, - 622
revision: &[(bool, String)], - 623
text: &str, - 624
totals: &mut WordTotals, - 625
) { - 626
let Some(paragraph) = stack.last_mut() else { - 627
return; - 628
}; - 629
let marked = match revision.last() { - 630
Some((true, author)) => format!("[inserted by {author}: {text}]"), - 631
Some((false, author)) => format!("[deleted by {author}: {text}]"), - 632
None => text.to_string(), - 633
}; - 634
if !run.hidden && !matches!(revision.last(), Some((false, _))) { - 635
paragraph.visible.push_str(text); - 636
} - 637
if run.hidden { - 638
totals.hidden_runs += 1; - 639
paragraph.text.push_str(&format!("[hidden: {marked}]")); - 640
add_label(&mut paragraph.labels, "hidden text"); - 641
} else if run.white && !text.trim().is_empty() { - 642
paragraph.text.push_str(&format!("[white text: {marked}]")); - 643
add_label(&mut paragraph.labels, "white text"); - 644
} else { - 645
paragraph.text.push_str(&marked); - 646
} - 647
} - 648
- 649
fn add_label(labels: &mut Vec<String>, label: &str) { - 650
if !labels.iter().any(|existing| existing == label) { - 651
labels.push(label.to_string()); - 652
} - 653
} - 654
- 655
fn attach_comments( - 656
units: &mut Vec<Unit>, - 657
paragraph: &Paragraph, - 658
comments: &HashMap<String, (String, String)>, - 659
) { - 660
for id in ¶graph.comments { - 661
if let Some((author, text)) = comments.get(id) { - 662
units.push(Unit { - 663
anchor: format!("{}/comment:{id}", paragraph.anchor), - 664
kind: UnitKind::Comment, - 665
level: 0, - 666
text: text.clone(), - 667
labels: vec![format!("comment by {author}")], - 668
cells: Vec::new(), - 669
row_cells: Vec::new(), - 670
}); - 671
} - 672
} - 673
} - 674
- 675
/// Tracked formatting changes (`w:rPrChange`, `w:tblPrExChange`, ...) - 676
/// hold the properties before the change; they describe no visible text. - 677
fn is_property_change(local: &str) -> bool { - 678
local.ends_with("PrChange") || local == "tblPrExChange" || local == "numberingChange" - 679
} - 680
- 681
fn flag_field(stack: &mut [Paragraph], field: &str, count: &mut usize) { - 682
*count += 1; - 683
if let Some(paragraph) = stack.last_mut() { - 684
add_label( - 685
&mut paragraph.labels, - 686
&format!("{field} field (never executed)"), - 687
); - 688
} - 689
} - 690
- 691
fn risky_field(instructions: &str) -> Option<&'static str> { - 692
let upper = instructions.to_ascii_uppercase(); - 693
match upper.split_whitespace().next()? { - 694
"DDE" | "DDEAUTO" => Some("DDE"), - 695
"INCLUDETEXT" => Some("INCLUDETEXT"), - 696
"INCLUDEPICTURE" => Some("INCLUDEPICTURE"), - 697
"MACROBUTTON" => Some("MACROBUTTON"), - 698
_ => None, - 699
} - 700
} - 701
- 702
fn heading_level(paragraph: &Paragraph, styles: &HashMap<String, u8>) -> u8 { - 703
if let Some(level) = paragraph.outline - 704
&& level <= 9 - 705
{ - 706
return level; - 707
} - 708
paragraph - 709
.style - 710
.as_ref() - 711
.and_then(|style| styles.get(style).copied()) - 712
.unwrap_or(0) - 713
} - 714
- 715
/// Style id → heading level, from built-in style names (`heading 1`, - 716
/// `Title`), which stay English whatever the document's language, or from - 717
/// an explicit outline level on the style. - 718
fn heading_styles(bytes: &[u8], part: &str, limits: &Limits) -> Result<HashMap<String, u8>, Error> { - 719
let mut levels = HashMap::new(); - 720
let mut current: Option<String> = None; - 721
xml::walk(bytes, part, limits, |event| { - 722
match event { - 723
XmlEvent::Open(element) => match element.local() { - 724
"style" => current = element.attr("styleId").map(str::to_string), - 725
"name" => { - 726
if let (Some(id), Some(name)) = (¤t, element.attr("val")) { - 727
let name = name.to_ascii_lowercase(); - 728
if name == "title" { - 729
levels.insert(id.clone(), 1); - 730
} else if let Some(level) = name - 731
.strip_prefix("heading ") - 732
.and_then(|level| level.parse::<u8>().ok()) - 733
{ - 734
levels.insert(id.clone(), level); - 735
} - 736
} - 737
} - 738
"outlineLvl" => { - 739
if let (Some(id), Some(level)) = (¤t, element.attr("val")) - 740
&& let Ok(level) = level.parse::<u8>() - 741
&& level < 9 - 742
{ - 743
levels.entry(id.clone()).or_insert(level + 1); - 744
} - 745
} - 746
_ => {} - 747
}, - 748
XmlEvent::Close(name) if xml::local_name(&name) == "style" => current = None, - 749
_ => {} - 750
} - 751
Ok(()) - 752
})?; - 753
Ok(levels) - 754
} - 755
- 756
fn word_comments( - 757
bytes: &[u8], - 758
part: &str, - 759
limits: &Limits, - 760
) -> Result<HashMap<String, (String, String)>, Error> { - 761
let mut comments = HashMap::new(); - 762
let mut current: Option<(String, String)> = None; - 763
let mut text = String::new(); - 764
let mut in_text = false; - 765
xml::walk(bytes, part, limits, |event| { - 766
match event { - 767
XmlEvent::Open(element) => match element.local() { - 768
"comment" => { - 769
current = element.attr("id").map(|id| { - 770
( - 771
id.to_string(), - 772
element.attr("author").unwrap_or("unknown").to_string(), - 773
) - 774
}); - 775
text.clear(); - 776
} - 777
"t" => in_text = true, - 778
"p" if !text.is_empty() => text.push(' '), - 779
_ => {} - 780
}, - 781
XmlEvent::Text(value) if in_text => text.push_str(&value), - 782
XmlEvent::Close(name) => match xml::local_name(&name) { - 783
"t" => in_text = false, - 784
"comment" => { - 785
if let Some((id, author)) = current.take() { - 786
comments.insert(id, (author, text.trim().to_string())); - 787
} - 788
} - 789
_ => {} - 790
}, - 791
_ => {} - 792
} - 793
Ok(()) - 794
})?; - 795
Ok(comments) - 796
} - 797
- 798
// ---- Excel ----------------------------------------------------------------- - 799
- 800
fn excel<R: Read + Seek>(package: &mut Package<R>, document: &mut Document) -> Result<(), Error> { - 801
let main = package.main_part().to_string(); - 802
let limits = *package.limits(); - 803
let bytes = package.read_part(&main)?; - 804
let mut sheets: Vec<(String, Option<String>, String)> = Vec::new(); - 805
let mut defined_names: Vec<(String, String)> = Vec::new(); - 806
let mut current_name: Option<String> = None; - 807
let mut name_text = String::new(); - 808
let mut recalculate_on_open = false; - 809
xml::walk(&bytes, &main, &limits, |event| { - 810
match event { - 811
XmlEvent::Open(element) => match element.local() { - 812
"calcPr" => { - 813
recalculate_on_open = element - 814
.attr("fullCalcOnLoad") - 815
.is_some_and(|value| value == "1" || value == "true"); - 816
} - 817
"sheet" => { - 818
if let (Some(name), Some(id)) = - 819
(element.attr("name"), element.attr_prefixed("id")) - 820
{ - 821
sheets.push(( - 822
name.to_string(), - 823
element.attr("state").map(str::to_string), - 824
id.to_string(), - 825
)); - 826
} - 827
} - 828
"definedName" => { - 829
current_name = element.attr("name").map(str::to_string); - 830
name_text.clear(); - 831
} - 832
_ => {} - 833
}, - 834
XmlEvent::Text(text) if current_name.is_some() => name_text.push_str(&text), - 835
XmlEvent::Close(name) if xml::local_name(&name) == "definedName" => { - 836
if let Some(name) = current_name.take() { - 837
defined_names.push((name, name_text.trim().to_string())); - 838
} - 839
} - 840
_ => {} - 841
} - 842
Ok(()) - 843
})?; - 844
let shared = match package.related_part(&main, "sharedStrings")? { - 845
Some(part) => { - 846
let bytes = package.read_part(&part)?; - 847
shared_strings(&bytes, &part, &limits)? - 848
} - 849
None => Vec::new(), - 850
}; - 851
- 852
let mut units = Vec::new(); - 853
let mut sections = Vec::new(); - 854
let mut tables = Vec::new(); - 855
let mut total_rows = 0usize; - 856
let mut formulas = 0usize; - 857
let sheet_count = sheets.len(); - 858
for (name, state, relationship) in sheets { - 859
let quoted = quote_sheet(&name); - 860
let anchor = format!("{quoted}!"); - 861
let mut labels = Vec::new(); - 862
match state.as_deref() { - 863
Some("hidden") => labels.push("hidden sheet".to_string()), - 864
Some("veryHidden") => labels.push("very hidden sheet".to_string()), - 865
_ => {} - 866
} - 867
let start = units.len(); - 868
let Some(part) = package.part_by_relationship_id(&main, &relationship)? else { - 869
continue; - 870
}; - 871
let is_worksheet = package - 872
.content_type(&part) - 873
.is_some_and(|content_type| content_type.contains("worksheet")); - 874
if !is_worksheet { - 875
sections.push(Section { - 876
anchor, - 877
title: format!("{name} (chart or dialog sheet, not read)"), - 878
level: 1, - 879
units: start..start, - 880
}); - 881
continue; - 882
} - 883
let bytes = package.read_part(&part)?; - 884
let rows = sheet_rows( - 885
&bytes, - 886
&part, - 887
&limits, - 888
&shared, - 889
recalculate_on_open, - 890
&mut formulas, - 891
)?; - 892
let used: std::collections::BTreeSet<u32> = rows - 893
.iter() - 894
.flat_map(|row| row.cells.iter().map(|(column, _)| *column)) - 895
.collect(); - 896
let omitted_columns = used.len().saturating_sub(MAX_TABLE_COLUMNS); - 897
let shown: Vec<u32> = used.into_iter().take(MAX_TABLE_COLUMNS).collect(); - 898
let slot_of: HashMap<u32, usize> = shown - 899
.iter() - 900
.enumerate() - 901
.map(|(slot, column)| (*column, slot)) - 902
.collect(); - 903
let mut table_rows = vec![ - 904
std::iter::once(String::new()) - 905
.chain(shown.iter().copied().map(column_name)) - 906
.collect::<Vec<_>>(), - 907
]; - 908
for row in &rows { - 909
total_rows += 1; - 910
let mut dense = vec![String::new(); shown.len()]; - 911
for (column, value) in &row.cells { - 912
if let Some(slot) = slot_of.get(column) { - 913
dense[*slot] = value.clone(); - 914
} - 915
} - 916
let first = row.cells.first().map(|(column, _)| *column).unwrap_or(1); - 917
let last = row.cells.last().map(|(column, _)| *column).unwrap_or(1); - 918
let mut row_labels = labels.clone(); - 919
if row.hidden { - 920
row_labels.push("hidden row".into()); - 921
} - 922
units.push(Unit { - 923
anchor: format!( - 924
"{quoted}!{}{}:{}{}", - 925
column_name(first), - 926
row.number, - 927
column_name(last), - 928
row.number - 929
), - 930
kind: UnitKind::SheetRow, - 931
level: 0, - 932
text: row - 933
.cells - 934
.iter() - 935
.map(|(column, value)| { - 936
format!("{}{}: {value}", column_name(*column), row.number) - 937
}) - 938
.collect::<Vec<_>>() - 939
.join(" | "), - 940
labels: row_labels, - 941
cells: row - 942
.cells - 943
.iter() - 944
.map(|(column, value)| { - 945
( - 946
format!("{}{}", column_name(*column), row.number), - 947
value.clone(), - 948
) - 949
}) - 950
.collect(), - 951
row_cells: Vec::new(), - 952
}); - 953
table_rows.push( - 954
std::iter::once(row.number.to_string()) - 955
.chain(dense) - 956
.collect(), - 957
); - 958
} - 959
sections.push(Section { - 960
anchor: anchor.clone(), - 961
title: name.clone(), - 962
level: 1, - 963
units: start..units.len(), - 964
}); - 965
tables.push(Table { - 966
anchor, - 967
title: name, - 968
rows: table_rows, - 969
labels, - 970
omitted_columns, - 971
}); - 972
} - 973
document.stats = vec![ - 974
("sheets".into(), sheet_count), - 975
("rows".into(), total_rows), - 976
("formulas".into(), formulas), - 977
("defined names".into(), defined_names.len()), - 978
]; - 979
for (name, reference) in defined_names { - 980
units.push(Unit { - 981
anchor: name.clone(), - 982
kind: UnitKind::DefinedName, - 983
level: 0, - 984
text: format!("defined name {name} = {reference}"), - 985
labels: Vec::new(), - 986
cells: Vec::new(), - 987
row_cells: Vec::new(), - 988
}); - 989
} - 990
document.units = units; - 991
document.sections = sections; - 992
document.tables = tables; - 993
document.not_read = vec![ - 994
"number formats (values are shown as stored)", - 995
"charts, pivot tables and cell comments", - 996
"recalculation (a formula shows the value cached in the file, which may be stale)", - 997
]; - 998
Ok(()) - 999
} - 1000
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.