- 1
//! What a view shows of a file (docs/design/72, P4, "The views"). - 2
//! - 3
//! A projection is the reader's own output, paged for a client: the outline - 4
//! (headings, sheets or slides, each with the index of its first unit), and - 5
//! one page of units under a byte budget. A client asks for the next page, - 6
//! or for the page that starts at a section, and never unzips a package or - 7
//! mounts document markup; everything it draws is text with anchors and - 8
//! labels (O6). - 9
- 10
use serde::Serialize; - 11
- 12
use crate::package::{Conformance, Package, Vocabulary}; - 13
use crate::read::{Document, Unit}; - 14
use crate::{Error, Limits}; - 15
- 16
/// A page's default size, well inside the worker protocol's limit even - 17
/// when every character needs escaping. - 18
pub const PAGE_BYTES: usize = 256 * 1024; - 19
- 20
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 21
pub struct Projection { - 22
pub vocabulary: Vocabulary, - 23
/// "Word document", "Excel workbook", … - 24
pub kind: &'static str, - 25
pub extension: &'static str, - 26
pub macro_enabled: bool, - 27
pub strict: bool, - 28
pub title: Option<String>, - 29
pub stats: Vec<(String, usize)>, - 30
/// One line each: macros, external links, hidden content and the like. - 31
pub flags: Vec<String>, - 32
pub sensitivity_labels: Vec<String>, - 33
pub outline: Vec<OutlineEntry>, - 34
pub total_units: usize, - 35
/// Index of the first unit on this page. - 36
pub from: usize, - 37
/// Where the next page starts; absent on the last page. - 38
pub next: Option<usize>, - 39
pub units: Vec<Unit>, - 40
/// What this reader does not show yet, so an omission is never read as - 41
/// absence. - 42
pub not_read: Vec<&'static str>, - 43
} - 44
- 45
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 46
pub struct OutlineEntry { - 47
pub anchor: String, - 48
pub title: String, - 49
pub level: u8, - 50
pub first_unit: usize, - 51
pub units: usize, - 52
} - 53
- 54
/// The page of `document` starting at unit `from`, holding as many units as - 55
/// fit in `budget` bytes, and at least one. A unit larger than the whole - 56
/// budget is cut, and says so in its labels. - 57
pub fn project(document: &Document, from: usize, budget: usize) -> Projection { - 58
let inspection = &document.inspection; - 59
let from = from.min(document.units.len()); - 60
let mut used = 0usize; - 61
let mut units = Vec::new(); - 62
let mut next = None; - 63
for (index, unit) in document.units.iter().enumerate().skip(from) { - 64
let size = weight(unit); - 65
if !units.is_empty() && used + size > budget { - 66
next = Some(index); - 67
break; - 68
} - 69
used += size; - 70
units.push(if size > budget { - 71
cut(unit, budget) - 72
} else { - 73
unit.clone() - 74
}); - 75
} - 76
Projection { - 77
vocabulary: inspection.format.vocabulary, - 78
kind: inspection.format.vocabulary.label(), - 79
extension: inspection.format.extension(), - 80
macro_enabled: inspection.format.macro_enabled, - 81
strict: inspection.conformance == Conformance::Strict, - 82
title: document.title.clone(), - 83
stats: document.stats.clone(), - 84
flags: inspection.flags(), - 85
sensitivity_labels: inspection.sensitivity_labels.clone(), - 86
outline: document - 87
.sections - 88
.iter() - 89
.map(|section| OutlineEntry { - 90
anchor: section.anchor.clone(), - 91
title: section.title.clone(), - 92
level: section.level, - 93
first_unit: section.units.start, - 94
units: section.units.len(), - 95
}) - 96
.collect(), - 97
total_units: document.units.len(), - 98
from, - 99
next, - 100
units, - 101
not_read: document.not_read.clone(), - 102
} - 103
} - 104
- 105
/// The index of the unit a citation names: the unit with exactly that - 106
/// anchor; for a cell or range (`Budget!B4`, `Budget!B4:D9`), the row that - 107
/// holds its first cell; otherwise the first unit inside the section or - 108
/// slide it names. `None` when the file has no such place. - 109
pub fn locate(document: &Document, anchor: &str) -> Option<usize> { - 110
let anchor = anchor.trim(); - 111
if let Some(index) = document.units.iter().position(|unit| unit.anchor == anchor) { - 112
return Some(index); - 113
} - 114
if let Some((sheet, cells)) = anchor.rsplit_once('!') { - 115
let first = cells - 116
.split(':') - 117
.next() - 118
.unwrap_or_default() - 119
.to_ascii_uppercase(); - 120
let prefix = format!("{sheet}!"); - 121
if let Some(index) = document.units.iter().position(|unit| { - 122
unit.anchor.starts_with(&prefix) - 123
&& unit - 124
.cells - 125
.iter() - 126
.any(|(address, _)| address.eq_ignore_ascii_case(&first)) - 127
}) { - 128
return Some(index); - 129
} - 130
let row: String = first - 131
.chars() - 132
.skip_while(char::is_ascii_alphabetic) - 133
.collect(); - 134
if !row.is_empty() { - 135
let row_suffix = |unit: &Unit| { - 136
unit.anchor - 137
.rsplit_once(':') - 138
.map(|(_, last)| { - 139
last.trim_start_matches(|c: char| c.is_ascii_alphabetic()) == row - 140
}) - 141
.unwrap_or(false) - 142
}; - 143
if let Some(index) = document - 144
.units - 145
.iter() - 146
.position(|unit| unit.anchor.starts_with(&prefix) && row_suffix(unit)) - 147
{ - 148
return Some(index); - 149
} - 150
} - 151
} - 152
// A paragraph in a Word table cell is cited by its own anchor and - 153
// shown in its row. - 154
if let Some(index) = document.units.iter().position(|unit| { - 155
unit.row_cells - 156
.iter() - 157
.flatten() - 158
.any(|(paragraph, _)| paragraph == anchor) - 159
}) { - 160
return Some(index); - 161
} - 162
document.units.iter().position(|unit| { - 163
unit.anchor.starts_with(anchor) - 164
&& matches!( - 165
unit.anchor.as_bytes().get(anchor.len()), - 166
Some(b'/' | b'!' | b':') - 167
) - 168
}) - 169
} - 170
- 171
/// Where a page opened at the cited unit `focus` starts: the start of the - 172
/// innermost section that holds it (a sheet's first row, the heading above - 173
/// a paragraph, a slide's title), so a citation opens with its context, - 174
/// when everything from there through the cited unit fits in `budget`; - 175
/// otherwise the cited unit itself. - 176
pub fn page_start(document: &Document, focus: usize, budget: usize) -> usize { - 177
if focus >= document.units.len() { - 178
return focus; - 179
} - 180
let start = document - 181
.sections - 182
.iter() - 183
.filter(|section| section.units.contains(&focus)) - 184
.map(|section| section.units.start) - 185
.max() - 186
.unwrap_or(focus); - 187
let span: usize = document.units[start..=focus].iter().map(weight).sum(); - 188
if span <= budget { start } else { focus } - 189
} - 190
- 191
/// Roughly what a unit costs once serialised. - 192
fn weight(unit: &Unit) -> usize { - 193
64 + unit.anchor.len() - 194
+ unit.text.len() - 195
+ unit - 196
.labels - 197
.iter() - 198
.map(|label| label.len() + 4) - 199
.sum::<usize>() - 200
+ unit - 201
.cells - 202
.iter() - 203
.map(|(address, value)| address.len() + value.len() + 8) - 204
.sum::<usize>() - 205
+ unit - 206
.row_cells - 207
.iter() - 208
.flatten() - 209
.map(|(anchor, text)| anchor.len() + text.len() + 8) - 210
.sum::<usize>() - 211
} - 212
- 213
fn cut(unit: &Unit, budget: usize) -> Unit { - 214
let keep = budget.saturating_sub(1024).max(1024); - 215
let mut end = keep.min(unit.text.len()); - 216
while !unit.text.is_char_boundary(end) { - 217
end -= 1; - 218
} - 219
let mut cut = unit.clone(); - 220
cut.text.truncate(end); - 221
cut.labels.push(format!( - 222
"shown up to {} of {} characters; doc_read pages through the rest", - 223
cut.text.chars().count(), - 224
unit.text.chars().count() - 225
)); - 226
cut.cells.clear(); - 227
cut.row_cells.clear(); - 228
cut - 229
} - 230
- 231
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 232
pub struct Structure { - 233
pub main_part: String, - 234
pub parts: Vec<PartInfo>, - 235
pub relationships: Vec<RelationshipInfo>, - 236
pub untyped_parts: Vec<String>, - 237
} - 238
- 239
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 240
pub struct PartInfo { - 241
pub name: String, - 242
pub content_type: Option<String>, - 243
/// Uncompressed size, as the archive's directory states it. - 244
pub size: u64, - 245
} - 246
- 247
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] - 248
pub struct RelationshipInfo { - 249
/// The part the relationship belongs to; empty for the package itself. - 250
pub source: String, - 251
pub id: String, - 252
/// Last segment of the relationship type (`image`, `hyperlink`). - 253
pub kind: String, - 254
pub target: String, - 255
/// External targets are recorded and never followed. - 256
pub external: bool, - 257
} - 258
- 259
/// The package's parts, their content types and every relationship, for - 260
/// the Structure view (U4). Reads only the relationship parts. - 261
pub fn structure<R: std::io::Read + std::io::Seek>( - 262
reader: R, - 263
limits: Limits, - 264
) -> Result<Structure, Error> { - 265
let mut package = Package::open(reader, limits)?; - 266
let parts: Vec<PartInfo> = package - 267
.part_sizes() - 268
.into_iter() - 269
.map(|(name, size)| PartInfo { - 270
content_type: package.content_type(&name), - 271
name, - 272
size, - 273
}) - 274
.collect(); - 275
let mut relationships = Vec::new(); - 276
let sources: Vec<String> = parts - 277
.iter() - 278
.filter_map(|part| crate::package::rels_source(&part.name)) - 279
.collect(); - 280
for source in sources { - 281
for relationship in package.relationships(&source)? { - 282
relationships.push(RelationshipInfo { - 283
source: source.clone(), - 284
id: relationship.id.clone(), - 285
kind: relationship.short_kind().to_string(), - 286
target: relationship.target.clone(), - 287
external: relationship.external, - 288
}); - 289
} - 290
} - 291
let inspection = package.inspect()?; - 292
Ok(Structure { - 293
main_part: package.main_part().to_string(), - 294
parts, - 295
relationships, - 296
untyped_parts: inspection.untyped_parts, - 297
}) - 298
} - 299
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.