Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_file/src/office/odf/sheet.rs

28.4 KiB, 40 runs

created by r1870400018:22908, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! `.ods`: an OpenDocument spreadsheet, written from and read back into the neutral spreadsheet.
2//!
3//! # Everything a cell is, is on the cell
4//!
5//! `<table:table-cell office:value-type="float" office:value="3.4" table:formula="of:=B2*C2">` says
6//! its type, its value and its formula in three attributes. A `.xlsx` needs a shared string table for
7//! the first, a style table and a number format for a date, and two hops through `numbering.xml` for
8//! a list -- none of which exists here. The three traps that make SpreadsheetML hard are all absent.
9//!
10//! What is present instead is repetition: a run of identical cells is written once with
11//! `table:number-columns-repeated`, and a reader that ignored it puts every later value in the wrong
12//! column. That is this format's version of the same mistake.
13//!
14//! # The stored value is still the value
15//!
16//! `office:value` is what the last calculation left, and nothing here recalculates. See
17//! [`crate::office::sheet`] for why that is the correct answer rather than a shortcut.
18//!
19//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
20//! Anthropic Claude
21
22use crate::office::odf::{
23 NS_FO,
24 NS_NUMBER,
25 NS_OF,
26 NS_OFFICE,
27 NS_STYLE,
28 NS_TABLE,
29 NS_TEXT,
30 pkg,
31};
32use crate::office::sheet::{
33 Book,
34 Cell,
35 MAX_COL,
36 Ref,
37 Sheet,
38 Value,
39 stored,
40 tab_names,
41 typed,
42};
43use crate::zip::{
44 Method,
45 Zip,
46};
47
48use oxedyne_fe2o3_core::prelude::*;
49use oxedyne_fe2o3_text::xml::{
50 Elem,
51 Span,
52 Xml,
53};
54use oxedyne_fe2o3_text::xml::write::{
55 Out,
56 escape_attr,
57};
58
59// Declared in the package's first member, which is what names the file.
60pub const MEDIA: &str = "application/vnd.oasis.opendocument.spreadsheet";
61
62// The most a single part is inflated to. An `.ods` is one `content.xml` holding every sheet, so this
63// is the whole workbook rather than a piece of it -- which is the trade OpenDocument makes for
64// having no relationship parts.
65pub const MAX_PART: u64 = 96 * 1024 * 1024;
66
67pub fn write(book: &Book) -> Outcome<Vec<u8>> {
68 let mut owned;
69 let book = match book.sheets.is_empty() {
70 false => book,
71 true => {
72 owned = Book::new();
73 owned.sheets.push(Sheet::new("Sheet1"));
74 &owned
75 }
76 };
77 let mut out = Out::declared();
78 out.open("office:document-content", &[
79 ("xmlns:office", NS_OFFICE),
80 ("xmlns:table", NS_TABLE),
81 ("xmlns:text", NS_TEXT),
82 ("xmlns:style", NS_STYLE),
83 ("xmlns:number", NS_NUMBER),
84 ("xmlns:fo", NS_FO),
85 // Without this, every formula in the file fails to parse and its stored value is replaced
86 // by an error. See `NS_OF`.
87 ("xmlns:of", NS_OF),
88 ("office:version", pkg::VERSION),
89 ]);
90 // A date is a date because its cell says so, and the DATA STYLE is what makes a reader show it
91 // as one rather than as a serial. One style serves every date in the book.
92 out.open("office:automatic-styles", &[]);
93 out.open("number:date-style", &[("style:name", "ND1")]);
94 out.empty("number:year", &[("number:style", "long")]);
95 out.leaf("number:text", &[], "-");
96 out.empty("number:month", &[("number:style", "long")]);
97 out.leaf("number:text", &[], "-");
98 out.empty("number:day", &[("number:style", "long")]);
99 res!(out.close("number:date-style"));
100 out.empty("style:style", &[
101 ("style:name", "CD1"),
102 ("style:family", "table-cell"),
103 ("style:data-style-name", "ND1"),
104 ]);
105 res!(out.close("office:automatic-styles"));
106 out.open("office:body", &[]);
107 out.open("office:spreadsheet", &[]);
108 // The same tab names the `.xlsx` writer settles on, and for the same reason: two tables of one
109 // name is a document a reader silently renames. See `crate::office::sheet::tab_names`.
110 let tabs = tab_names(book);
111 for (i, s) in book.sheets.iter().enumerate() {
112 out.open("table:table", &[("table:name", &tabs[i])]);
113 let (_, cols) = s.size();
114 out.empty("table:table-column", &[
115 ("table:number-columns-repeated", &fmt!("{}", cols.max(1))),
116 ]);
117 for row in &s.rows {
118 out.open("table:table-row", &[]);
119 for cell in row {
120 res!(cell_part(&mut out, cell));
121 }
122 res!(out.close("table:table-row"));
123 }
124 res!(out.close("table:table"));
125 }
126 res!(out.close("office:spreadsheet"));
127 res!(out.close("office:body"));
128 res!(out.close("office:document-content"));
129
130 let mut zip = pkg::start(MEDIA);
131 zip.set("content.xml", res!(out.finish()).into_bytes(), Method::Deflate);
132 zip.set("styles.xml", res!(pkg::styles_for(MEDIA)).into_bytes(), Method::Deflate);
133 zip.set("meta.xml", res!(pkg::meta()).into_bytes(), Method::Deflate);
134 res!(pkg::finish(&mut zip, MEDIA));
135 zip.write()
136}
137
138/// One cell: its type, its value and its formula, all on the element.
139fn cell_part(out: &mut Out, cell: &Cell) -> Outcome<()> {
140 let shown = cell.value.show();
141 // A formula is written in OpenFormula, which is NOT the A1 syntax a `.xlsx` uses: a reference
142 // has to be bracketed, so `B2*C2` becomes `[.B2]*[.C2]`. Written the other way LibreOffice
143 // cannot parse it, RECALCULATES, and replaces the cached value with `Err:510` -- so a wrong
144 // formula does not merely fail to work, it destroys the number that was there.
145 let formula = cell.formula.as_ref().map(|f| fmt!("of:={}", openformula(f)));
146 let mut attrs: Vec<(&str, &str)> = Vec::new();
147 if let Some(f) = &formula {
148 attrs.push(("table:formula", f));
149 }
150 let num;
151 match &cell.value {
152 Value::Empty => {}
153 Value::Text(_) => attrs.push(("office:value-type", "string")),
154 Value::Number(n) => {
155 num = repr(*n);
156 attrs.push(("office:value-type", "float"));
157 attrs.push(("office:value", &num));
158 }
159 Value::Bool(b) => {
160 attrs.push(("office:value-type", "boolean"));
161 attrs.push(("office:boolean-value", match b {
162 true => "true",
163 false => "false",
164 }));
165 }
166 Value::Date(d) => {
167 attrs.push(("table:style-name", "CD1"));
168 attrs.push(("office:value-type", "date"));
169 attrs.push(("office:date-value", d));
170 }
171 // An error has no value type of its own here; it is a string cell holding what the last
172 // calculation produced, which is what a reader shows.
173 Value::Error(_) => attrs.push(("office:value-type", "string")),
174 }
175 if cell.is_empty() {
176 out.empty("table:table-cell", &[]);
177 return Ok(());
178 }
179 out.open("table:table-cell", &attrs);
180 if !shown.is_empty() {
181 out.leaf("text:p", &[], &shown);
182 }
183 res!(out.close("table:table-cell"));
184 Ok(())
185}
186
187/// A formula's references, bracketed as OpenFormula requires.
188///
189/// `B2*C2` becomes `[.B2]*[.C2]` and `SUM(D2:D3)` becomes `SUM([.D2:.D3])`. A leading `.` means "this
190/// sheet", which is what every reference written without one means.
191///
192/// What is NOT a reference is left alone: a function name is followed by `(`, and anything inside
193/// quotation marks is text. Those two rules are the whole of it, and they are the two that would
194/// otherwise turn `SUM` into a cell and a quoted `A1` into a reference.
195pub fn openformula(f: &str) -> String {
196 let b = f.as_bytes();
197 let mut out = String::with_capacity(f.len() + 8);
198 let mut i = 0;
199 while i < b.len() {
200 let c = b[i] as char;
201 if c == '"' {
202 // A quoted run is text and passes through whole, quotes included.
203 out.push(c);
204 i += 1;
205 while i < b.len() {
206 out.push(b[i] as char);
207 i += 1;
208 if b[i - 1] == b'"' {
209 break;
210 }
211 }
212 continue;
213 }
214 match reference(b, i) {
215 None => {
216 out.push(c);
217 i += 1;
218 }
219 Some(end) => {
220 // A range is two references with a colon between them, and it is bracketed ONCE
221 // with both sides dotted -- `[.D2:.D3]`, not `[.D2]:[.D3]`.
222 let first = &f[i..end];
223 let mut at = end;
224 if b.get(at) == Some(&b':') {
225 if let Some(second) = reference(b, at + 1) {
226 out.push_str(&fmt!("[.{}:.{}]", first, &f[at + 1..second]));
227 i = second;
228 continue;
229 }
230 at = end;
231 }
232 let _ = at;
233 out.push_str(&fmt!("[.{}]", first));
234 i = end;
235 }
236 }
237 }
238 out
239}
240
241/// A formula's references with the OpenFormula bracketing taken off: the inverse of [`openformula`].
242///
243/// `[.B2]*[.C2]` becomes `B2*C2` and `SUM([.D2:.D3])` becomes `SUM(D2:D3)`.
244///
245/// **This existed nowhere until a round-trip test asked for it, and its absence was invisible.** The
246/// writer's test checked the bytes it produced and the reader's test checked that a formula came back
247/// AT ALL, so between two passing tests sat the fact that a formula written by this crate read back as
248/// `[.B2]*[.C2]` -- and the same workbook as a `.xlsx` read back as `B2*C2`. A caller comparing the two
249/// formats, or handing a formula to a model, met a difference that is nothing to do with the data.
250///
251/// Anything inside quotation marks is text and passes through untouched, as it does on the way out.
252pub fn plain(f: &str) -> String {
253 let b = f.as_bytes();
254 let mut out = String::with_capacity(f.len());
255 let mut i = 0;
256 while i < b.len() {
257 let c = b[i] as char;
258 if c == '"' {
259 out.push(c);
260 i += 1;
261 while i < b.len() {
262 out.push(b[i] as char);
263 i += 1;
264 if b[i - 1] == b'"' {
265 break;
266 }
267 }
268 continue;
269 }
270 if c != '[' {
271 out.push(c);
272 i += 1;
273 continue;
274 }
275 match f[i..].find(']') {
276 // An unclosed bracket is left exactly as written. Guessing where it ended would rewrite an
277 // expression nobody can check.
278 None => {
279 out.push(c);
280 i += 1;
281 }
282 Some(k) => {
283 let inside = &f[i + 1..i + k];
284 // A range is bracketed once with both sides dotted, so each side loses its own dot.
285 let parts: Vec<&str> = inside.split(':')
286 .map(|p| p.strip_prefix('.').unwrap_or(p))
287 .collect();
288 out.push_str(&parts.join(":"));
289 i += k + 1;
290 }
291 }
292 }
293 out
294}
295
296/// Where a cell reference starting at an offset ends, if one starts there.
297///
298/// A reference is letters then digits, not preceded by a letter, a digit or a `$` -- so the `A1` in
299/// `BA12` is not one -- and not followed by `(`, which is what makes `LOG10(x)` a function rather
300/// than a cell in column LOG.
301fn reference(b: &[u8], at: usize) -> Option<usize> {
302 if at > 0 {
303 let prev = b[at - 1];
304 if prev.is_ascii_alphanumeric() || prev == b'$' || prev == b'.' || prev == b'_' {
305 return None;
306 }
307 }
308 let mut i = at;
309 let mut letters = 0;
310 while i < b.len() && b[i].is_ascii_alphabetic() && letters < 3 {
311 i += 1;
312 letters += 1;
313 }
314 if letters == 0 {
315 return None;
316 }
317 let digits_at = i;
318 while i < b.len() && b[i].is_ascii_digit() {
319 i += 1;
320 }
321 if i == digits_at {
322 return None;
323 }
324 // A name followed by a bracket is a function, however much it looks like a cell.
325 if b.get(i) == Some(&b'(') {
326 return None;
327 }
328 // And a trailing letter means it was never a reference: `A1B` is a name.
329 if b.get(i).map(|c| c.is_ascii_alphanumeric()).unwrap_or(false) {
330 return None;
331 }
332 Some(i)
333}
334
335/// A number as the file stores it, which is not how a person reads it.
336fn repr(n: f64) -> String {
337 if !n.is_finite() {
338 return "0".to_string();
339 }
340 if n == n.trunc() && n.abs() < 1e15 {
341 return fmt!("{}", n as i64);
342 }
343 fmt!("{}", n)
344}
345
346#[derive(Clone, Debug, Default)]
347pub struct Reading {
348 pub book: Book,
349 pub macros: bool, // a macro project is present; said, never run
350 pub formulas: usize, // cells carrying a formula
351}
352
353pub fn read(bytes: &[u8]) -> Outcome<Reading> {
354 let zip = res!(Zip::read(bytes.to_vec()));
355 let mut out = Reading::default();
356 out.macros = zip.names().iter().any(|n| n.starts_with("Basic/"));
357 let src = res!(String::from_utf8(res!(zip.content_capped("content.xml", MAX_PART))),
358 Decode, String);
359 let xml = res!(Xml::parse(&src));
360 let body = res!(res!(xml.root()).find(&["office:body", "office:spreadsheet"])
361 .ok_or_else(|| err!(
362 "This package has no <office:spreadsheet>, so it is not a spreadsheet.";
363 Invalid, Input, Missing)));
364 for t in body.children("table:table") {
365 let mut sheet = Sheet::new(t.attr("table:name").unwrap_or("Sheet"));
366 for tr in t.children("table:table-row") {
367 // A run of identical rows is written once and repeated. Ignoring the count collapses
368 // them, which moves every row after the run.
369 let n = tr.attr("table:number-rows-repeated")
370 .and_then(|v| v.parse::<usize>().ok())
371 .unwrap_or(1);
372 let line = row_of(&xml, tr, &mut out.formulas);
373 // An INTERIOR run of empty rows has to be expanded, or everything after the gap moves up:
374 // collapsing it to one put the total row two rows early. A run at the END is padding --
375 // a sheet says "nothing until row 1048576" that way -- and the trailing trim below is
376 // what deals with that, so the expansion only needs a bound rather than a special case.
377 let n = n.min(4096);
378 for _ in 0..n {
379 sheet.rows.push(line.clone());
380 }
381 }
382 // Trailing empty rows are the sheet's padding, not its content.
383 while sheet.rows.last().map(|r| r.iter().all(|c| c.is_empty())).unwrap_or(false) {
384 sheet.rows.pop();
385 }
386 out.book.sheets.push(sheet);
387 }
388 Ok(out)
389}
390
391/// One row of cells, expanding the repeats.
392fn row_of(xml: &Xml, tr: &Elem, formulas: &mut usize) -> Vec<Cell> {
393 let mut out: Vec<Cell> = Vec::new();
394 for tc in tr.elems() {
395 let covered = match tc.name.qname.as_str() {
396 "table:table-cell" => false,
397 "table:covered-table-cell" => true,
398 _ => continue,
399 };
400 let n = tc.attr("table:number-columns-repeated")
401 .and_then(|v| v.parse::<usize>().ok())
402 .unwrap_or(1);
403 let cell = match covered {
404 true => Cell::empty(),
405 false => cell_of(xml, tc),
406 };
407 if cell.formula.is_some() {
408 *formulas += 1;
409 }
410 // The same as the rows, and for the same reason: an interior run of empty cells is a GAP and
411 // moves everything after it, while a run at the end is padding and is trimmed below.
412 let n = n.min(MAX_COL as usize);
413 for _ in 0..n {
414 out.push(cell.clone());
415 }
416 }
417 while out.last().map(|c| c.is_empty()).unwrap_or(false) {
418 out.pop();
419 }
420 out
421}
422
423fn cell_of(xml: &Xml, tc: &Elem) -> Cell {
424 // The formula is read and never evaluated. The `of:=` prefix is the format's own namespace
425 // marker, not part of the expression, so it comes off.
426 let formula = tc.attr("table:formula").map(|f| {
427 plain(f.strip_prefix("of:=").or_else(|| f.strip_prefix('=')).unwrap_or(f))
428 });
429 let shown = || -> String {
430 tc.children("text:p").iter().map(|p| xml.text_of(p)).collect::<Vec<_>>().join("\n")
431 };
432 let value = match tc.attr("office:value-type") {
433 Some("float") | Some("percentage") | Some("currency") => {
434 match tc.attr("office:value").and_then(|v| v.trim().parse::<f64>().ok()) {
435 Some(n) => Value::Number(n),
436 None => Value::Empty,
437 }
438 }
439 Some("boolean") => Value::Bool(tc.attr("office:boolean-value") == Some("true")),
440 Some("date") => match tc.attr("office:date-value") {
441 Some(d) => Value::Date(d.to_string()),
442 None => Value::Empty,
443 },
444 Some("time") => match tc.attr("office:time-value") {
445 Some(d) => Value::Date(d.to_string()),
446 None => Value::Empty,
447 },
448 Some("string") => {
449 // The displayed text is the value, unless the cell carries one explicitly.
450 let t = tc.attr("office:string-value").map(|s| s.to_string()).unwrap_or_else(shown);
451 match t.is_empty() {
452 true => Value::Empty,
453 false => Value::Text(t),
454 }
455 }
456 _ => {
457 let t = shown();
458 match t.is_empty() {
459 true => Value::Empty,
460 false => Value::Text(t),
461 }
462 }
463 };
464 Cell { value, formula }
465}
466
467// ---------------------------------------------------------------------------
468// Editing an `.ods` in place
469// ---------------------------------------------------------------------------
470
471/// One cell to write.
472#[derive(Clone, Debug)]
473pub struct Set {
474 pub sheet: Option<String>, // by the name on the tab; None means the first
475 pub at: Ref,
476 // The value as a person would type it. `sheet::typed` decides what it is; an empty string
477 // empties the cell.
478 pub value: Option<String>,
479 pub formula: Option<String>, // without its leading `=`, which is stripped if present
480}
481
482/// What an edit of an `.ods` produced.
483#[derive(Clone, Debug, Default)]
484pub struct Edited {
485 pub bytes: Vec<u8>,
486 pub cells: usize,
487 pub sheets: Vec<String>, // the tabs that were touched
488}
489
490/// Writes cells into an `.ods`, leaving every other byte of the package as it arrived.
491///
492/// # Repetition is the whole of the difficulty
493///
494/// `.ods` addresses nothing. A run of identical cells is written once as
495/// `<table:table-cell table:number-columns-repeated="8"/>`, and a run of identical rows the same way,
496/// so writing `C4` means finding which run covers it and SPLITTING that run into the part before, the
497/// cell itself, and the part after. Written any other way -- appended, or with the count left alone --
498/// every value to the right of the edit moves one column, which is a corruption that looks like a
499/// spreadsheet.
500///
501/// The rest of `content.xml` is copied byte for byte, and `styles.xml`, `meta.xml`, the manifest and
502/// anything else in the package are never opened.
503pub fn edit(bytes: &[u8], sets: &[Set]) -> Outcome<Edited> {
504 if sets.is_empty() {
505 return Err(err!("A write to a spreadsheet was asked for with no cells in it."; Invalid, Input));
506 }
507 let mut zip = res!(Zip::read(bytes.to_vec()));
508 let src = res!(String::from_utf8(res!(zip.content_capped("content.xml", MAX_PART))),
509 Decode, String);
510 let mut xml = res!(Xml::parse(&src));
511 let body = res!(res!(xml.root()).find(&["office:body", "office:spreadsheet"])
512 .ok_or_else(|| err!(
513 "This package has no <office:spreadsheet>, so it is not a spreadsheet.";
514 Invalid, Input, Missing)))
515 .clone();
516 let tables: Vec<Elem> = body.children("table:table").into_iter().cloned().collect();
517 if tables.is_empty() {
518 return Err(err!("This spreadsheet holds no sheets, so there is nowhere to write.";
519 Invalid, Input, Missing));
520 }
521 let names: Vec<String> = tables.iter()
522 .map(|t| t.attr("table:name").unwrap_or("Sheet").to_string())
523 .collect();
524
525 let mut jobs: Vec<(usize, Vec<&Set>)> = Vec::new();
526 for set in sets {
527 let i = match &set.sheet {
528 None => 0,
529 Some(want) => {
530 let found = names.iter().position(|n| n == want)
531 .or_else(|| names.iter().position(|n| n.eq_ignore_ascii_case(want)));
532 res!(found.ok_or_else(|| err!(
533 "This workbook has no sheet named '{}'. It has: {}. Nothing has been written.",
534 want, names.join(", "); Invalid, Input, Missing)))
535 }
536 };
537 match jobs.iter_mut().find(|(k, _)| *k == i) {
538 Some(j) => j.1.push(set),
539 None => jobs.push((i, vec![set])),
540 }
541 }
542 // Refused rather than resolved, for the reason `xlsx::edit` gives: there is no order for two
543 // writes to one cell to be applied in, so picking one would be a rule about how a caller happened
544 // to build its list. The two formats answer this the same way, which is the point.
545 for (i, sets) in &jobs {
546 for (k, one) in sets.iter().enumerate() {
547 if sets[..k].iter().any(|s| s.at == one.at) {
548 return Err(err!(
549 "{} of sheet '{}' is written twice in one call, and there is no order in which \
550 to apply the two. Nothing has been written.", one.at.name(), names[*i];
551 Invalid, Input, Conflict));
552 }
553 }
554 }
555
556 let mut splices: Vec<(Span, String)> = Vec::new();
557 let mut touched = Vec::new();
558 for (i, sets) in &jobs {
559 res!(table_splices(&xml, &tables[*i], sets, &mut splices));
560 touched.push(names[*i].clone());
561 }
562 splices.sort_by_key(|(s, _)| (s.start, s.end));
563 for (span, text) in splices {
564 res!(xml.splice(span, text));
565 }
566 zip.set("content.xml", xml.render().into_bytes(), Method::Deflate);
567 Ok(Edited { bytes: res!(zip.write()), cells: sets.len(), sheets: touched })
568}
569
570/// One run of identical rows or cells: the element that carries it, and the span of the grid it covers.
571struct Run<'a> {
572 elem: &'a Elem,
573 from: u32,
574 n: u32,
575}
576
577/// The runs a table's rows make, and how many rows they cover between them.
578fn runs<'a>(at: &'a Elem, kinds: &[&str], attr: &str) -> (Vec<Run<'a>>, u32) {
579 let mut out = Vec::new();
580 let mut from = 0u32;
581 for kid in at.elems() {
582 if !kinds.iter().any(|k| kid.name.qname == *k) {
583 continue;
584 }
585 let n = kid.attr(attr)
586 .and_then(|v| v.parse::<u32>().ok())
587 .unwrap_or(1)
588 .max(1);
589 out.push(Run { elem: kid, from, n });
590 from = from.saturating_add(n);
591 }
592 (out, from)
593}
594
595/// The splices one table needs, added to the list the whole document's edit will make.
596fn table_splices(
597 xml: &Xml,
598 table: &Elem,
599 sets: &[&Set],
600 into: &mut Vec<(Span, String)>,
601)
602 -> Outcome<()>
603{
604 let (rows, total) = runs(table, &["table:table-row"], "table:number-rows-repeated");
605
606 // By the run that covers them, because splitting a run is one replacement of one element however
607 // many of its repeats are being written into.
608 let mut by_run: Vec<(usize, Vec<&Set>)> = Vec::new();
609 let mut appended: Vec<&Set> = Vec::new();
610 for set in sets {
611 match rows.iter().position(|r| set.at.row >= r.from && set.at.row < r.from + r.n) {
612 Some(k) => match by_run.iter_mut().find(|(j, _)| *j == k) {
613 Some(g) => g.1.push(set),
614 None => by_run.push((k, vec![set])),
615 },
616 None => appended.push(set),
617 }
618 }
619
620 for (k, group) in &by_run {
621 let run = &rows[*k];
622 let mut text = String::new();
623 let mut at = 0u32;
624 while at < run.n {
625 let here = run.from + at;
626 let mine: Vec<&Set> = group.iter().filter(|s| s.at.row == here).copied().collect();
627 if mine.is_empty() {
628 // The untouched repeats either side of an edit keep the run they were in, with the
629 // count reduced to what is left of it.
630 let next = group.iter()
631 .filter_map(|s| s.at.row.checked_sub(run.from))
632 .filter(|o| *o > at)
633 .min()
634 .unwrap_or(run.n);
635 text.push_str(&repeated(xml, run.elem, "table:number-rows-repeated", next - at));
636 at = next;
637 continue;
638 }
639 text.push_str(&res!(row_markup(xml, run.elem, &mine)));
640 at += 1;
641 }
642 into.push((run.elem.span.clone(), text));
643 }
644
645 if appended.is_empty() {
646 return Ok(());
647 }
648 // Rows past the end of the sheet: the gap, then a row for each that is being written.
649 let mut by_row: Vec<(u32, Vec<&Set>)> = Vec::new();
650 for set in &appended {
651 match by_row.iter_mut().find(|(r, _)| *r == set.at.row) {
652 Some(g) => g.1.push(set),
653 None => by_row.push((set.at.row, vec![set])),
654 }
655 }
656 by_row.sort_by_key(|(r, _)| *r);
657 let mut text = String::new();
658 let mut at = total;
659 for (row, group) in &by_row {
660 if *row > at {
661 text.push_str(&fmt!(
662 "<table:table-row table:number-rows-repeated=\"{}\"><table:table-cell/>\
663 </table:table-row>", row - at));
664 }
665 let mut cells = String::new();
666 let mut col = 0u32;
667 let mut group = group.clone();
668 group.sort_by_key(|s| s.at.col);
669 for set in &group {
670 if set.at.col > col {
671 cells.push_str(&fmt!(
672 "<table:table-cell table:number-columns-repeated=\"{}\"/>", set.at.col - col));
673 }
674 cells.push_str(&cell_markup(set, None));
675 col = set.at.col + 1;
676 }
677 text.push_str(&fmt!("<table:table-row>{}</table:table-row>", cells));
678 at = row + 1;
679 }
680 let at = res!(table.inner.clone().ok_or_else(|| err!(
681 "This sheet is written <table:table/>, with nothing inside it at all."; Invalid, Input)));
682 into.push((at.end..at.end, text));
683 Ok(())
684}
685
686/// One row of the source with a run count put on it, for the repeats an edit did not touch.
687fn repeated(xml: &Xml, elem: &Elem, attr: &str, n: u32) -> String {
688 let base = xml.raw(&elem.span);
689 let held = elem.attrs.iter().find(|a| a.name.qname == attr);
690 match held {
691 // The attribute is in the open tag, so its span is inside the element's and the offsets are
692 // the element's own.
693 Some(a) => {
694 let from = a.val_span.start - elem.span.start;
695 let to = a.val_span.end - elem.span.start;
696 fmt!("{}{}{}", &base[..from], n, &base[to..])
697 }
698 None if n == 1 => base.to_string(),
699 None => {
700 // A run of one that has to become a run of many needs the attribute adding, which goes
701 // straight after the element's name.
702 let head = elem.name.span.end - elem.span.start;
703 fmt!("{} {}=\"{}\"{}", &base[..head], attr, n, &base[head..])
704 }
705 }
706}
707
708/// One row of the source, with the cells an edit named replaced and the run count taken off it.
709fn row_markup(xml: &Xml, tr: &Elem, sets: &[&Set]) -> Outcome<String> {
710 let base = xml.raw(&tr.span).to_string();
711 let start = tr.span.start;
712 let mut edits: Vec<(usize, usize, String)> = Vec::new();
713
714 // The row is now one row, so whatever count it carried goes.
715 if let Some(a) = tr.attrs.iter().find(|a| a.name.qname == "table:number-rows-repeated") {
716 edits.push((a.span.start - start, a.span.end - start, String::new()));
717 }
718
719 let kinds = ["table:table-cell", "table:covered-table-cell"];
720 let (cells, total) = runs(tr, &kinds, "table:number-columns-repeated");
721 let mut by_run: Vec<(usize, Vec<&Set>)> = Vec::new();
722 let mut appended: Vec<&Set> = Vec::new();
723 for set in sets {
724 match cells.iter().position(|c| set.at.col >= c.from && set.at.col < c.from + c.n) {
725 Some(k) => match by_run.iter_mut().find(|(j, _)| *j == k) {
726 Some(g) => g.1.push(set),
727 None => by_run.push((k, vec![*set])),
728 },
729 None => appended.push(set),
730 }
731 }
732
733 for (k, group) in &by_run {
734 let run = &cells[*k];
735 if run.elem.name.qname == "table:covered-table-cell" {
736 let names: Vec<String> = group.iter().map(|s| s.at.name()).collect();
737 return Err(err!(
738 "{} is covered by a merged cell, so writing to it would put a value where nothing is \
739 drawn. Write to the top left cell of the merge instead. Nothing has been written.",
740 names.join(", "); Invalid, Input));
741 }
742 let style = run.elem.attr("table:style-name").map(|s| s.to_string());
743 let mut text = String::new();
744 let mut at = 0u32;
745 while at < run.n {
746 let here = run.from + at;
747 let mine = group.iter().find(|s| s.at.col == here);
748 match mine {
749 None => {
750 let next = group.iter()
751 .filter_map(|s| s.at.col.checked_sub(run.from))
752 .filter(|o| *o > at)
753 .min()
754 .unwrap_or(run.n);
755 text.push_str(&repeated(
756 xml, run.elem, "table:number-columns-repeated", next - at));
757 at = next;
758 }
759 Some(set) => {
760 text.push_str(&cell_markup(set, style.as_deref()));
761 at += 1;
762 }
763 }
764 }
765 edits.push((run.elem.span.start - start, run.elem.span.end - start, text));
766 }
767
768 if !appended.is_empty() {
769 let mut appended = appended;
770 appended.sort_by_key(|s| s.at.col);
771 let mut text = String::new();
772 let mut col = total;
773 for set in &appended {
774 if set.at.col > col {
775 text.push_str(&fmt!(
776 "<table:table-cell table:number-columns-repeated=\"{}\"/>", set.at.col - col));
777 }
778 text.push_str(&cell_markup(set, None));
779 col = set.at.col + 1;
780 }
781 match &tr.inner {
782 Some(inner) => {
783 let at = inner.end - start;
784 edits.push((at, at, text));
785 }
786 // A `<table:table-row/>` has no inside to append to, so the whole element is rebuilt.
787 None => {
788 let head = res!(base.strip_suffix("/>").ok_or_else(|| err!(
789 "A row with no content did not end '/>': {}", base; Bug)));
790 return Ok(fmt!("{}>{}</table:table-row>", head, text));
791 }
792 }
793 }
794
795 edits.sort_by_key(|(from, to, _)| (*from, *to));
796 let mut out = String::with_capacity(base.len() + 64);
797 let mut at = 0usize;
798 for (from, to, text) in &edits {
799 out.push_str(&base[at..*from]);
800 out.push_str(text);
801 at = *to;
802 }
803 out.push_str(&base[at..]);
804 Ok(out)
805}
806
807/// One `<table:table-cell>` holding what the caller asked for.
808///
809/// Everything is on the element -- the type, the value and the formula -- which is what makes this
810/// format the easier of the two to write into. The displayed `<text:p>` goes in as well, because a
811/// reader that does not recalculate shows that and not `office:value`.
812fn cell_markup(set: &Set, style: Option<&str>) -> String {
813 let value = set.value.as_deref().map(typed).unwrap_or(Value::Empty);
814 let mut out = String::from("<table:table-cell");
815 if let Some(s) = style {
816 out.push_str(&fmt!(" table:style-name=\"{}\"", escape_attr(s)));
817 }
818 if let Some(f) = &set.formula {
819 let f = f.trim_start_matches('=');
820 if !f.is_empty() {
821 // Bracketed, because OpenFormula requires it: written as `of:=B2*C2` LibreOffice fails to
822 // parse the formula, RECALCULATES the cell, and writes `Err:510` over the stored value.
823 out.push_str(&fmt!(" table:formula=\"{}\"", escape_attr(&fmt!("of:={}", openformula(f)))));
824 }
825 }
826 match &value {
827 Value::Empty => {}
828 Value::Text(_) | Value::Error(_) => out.push_str(" office:value-type=\"string\""),
829 Value::Number(n) => out.push_str(&fmt!(
830 " office:value-type=\"float\" office:value=\"{}\"", stored(*n))),
831 Value::Bool(b) => out.push_str(&fmt!(
832 " office:value-type=\"boolean\" office:boolean-value=\"{}\"", b)),
833 Value::Date(d) => out.push_str(&fmt!(
834 " office:value-type=\"date\" office:date-value=\"{}\"", escape_attr(d))),
835 }
836 let shown = value.show();
837 if shown.is_empty() {
838 out.push_str("/>");
839 return out;
840 }
841 // THE DISPLAYED TEXT GOES IN A `<text:p>` AND NOT AS BARE CHARACTER DATA. Written bare, a numeric
842 // cell still shows -- it has `office:value` -- and a STRING cell shows nothing at all, because a
843 // string cell's value IS its paragraph. So the mistake loses exactly the cells it is hardest to
844 // notice losing, and LibreOffice is what found it.
845 let p = crate::office::odf::text::content_markup(&shown);
846 out.push_str(">");
847 out.push_str("<text:p>");
848 out.push_str(&p);
849 out.push_str("</text:p>");
850 out.push_str("</table:table-cell>");
851 out
852}